Theme, i18n, telemetry, read-only mode
Theme, i18n, telemetry, read-only mode
Section titled “Theme, i18n, telemetry, read-only mode”Three of the ViewerServices fields are infrastructure rather than data:
tokens, i18n, telemetry. They always have safe defaults; override
when you need to plug into your brand palette, translation table, or
analytics. The readOnly flag lives on ViewerHostContext and toggles
write-only UI.
Theme tokens
Section titled “Theme tokens”interface ThemeTokens { readonly primary: string; // brand primary readonly accent: string; // brand accent readonly bg: string; // surface background readonly fg: string; // foreground / body text readonly border: string; // hairline border // Optional brand-identity fields read by <LensPDFDemo> as a // fallback when the equivalent props (`brand`, `brandLogoUrl`) // aren't set. Lets a host bundle palette + logo + label into a // single tokens object. readonly logoUrl?: string; // brand logo image URL readonly logoText?: string; // brand label (default: "LensPDF") readonly logoMaxHeight?: number; // pixel cap on logo height (default: 24) readonly logoAlt?: string; // alt text for the logo <img>}defaultThemeTokens is a neutral light palette:
import { defaultThemeTokens } from "@printwithsynergy/lens-pdf/plugin";
// {// primary: "#0f172a",// accent: "#3b82f6",// bg: "#ffffff",// fg: "#0f172a",// border: "#e2e8f0",// }darkThemeTokens is a dark palette preset for demo and dark-mode UIs:
import { darkThemeTokens } from "@printwithsynergy/lens-pdf/plugin";
// {// primary: "#0f172a",// accent: "#3b82f6",// bg: "#0e0a14",// fg: "#f5f3f7",// border: "#2b2138",// }<LensPDFDemo> uses darkThemeTokens by default; <LensPDFViewer>
uses defaultThemeTokens.
Pass your own through services.tokens:
const services: ViewerServices = { // … tokens: { primary: "#1a3a7a", accent: "#2563eb", bg: "#ffffff", fg: "#0f172a", border: "#e2e8f0", },};Plugins read from ctx.services.tokens rather than hardcoding hex
strings, so swapping a brand palette is a single context-value change.
interface I18nService { t(key: string, params?: Record<string, string | number>): string;}The noopI18n default returns the key unchanged with {param}
placeholders substituted. Drop in a real translator as needed:
import type { I18nService } from "@printwithsynergy/lens-pdf/plugin";
export const i18n: I18nService = { t: (key, params) => translateWithICU(key, params),};Suitable for English-only environments and tests, the no-op behaves like:
noopI18n.t("hello.name", { name: "Ada" }); // "hello.name"// (the key is returned because no entry exists; placeholders still// substitute when present in the key text itself)Telemetry
Section titled “Telemetry”interface TelemetryService { track(event: string, properties?: Record<string, unknown>): void;}noopTelemetry drops every event on the floor. Wire your analytics by
overriding:
import type { TelemetryService } from "@printwithsynergy/lens-pdf/plugin";
export const telemetry: TelemetryService = { track: (event, props) => window.analytics?.track(event, props),};OSS hosts that don’t want to ship analytics can leave the no-op default — no events will leave the browser.
Read-only mode
Section titled “Read-only mode”Set ViewerHostContext.readOnly to true to suppress write-only UI.
<ViewerHostContext.Provider value={{ apiBase: "/api/share/abc123", jobApiBase: "/api/share/abc123", readOnly: true, }}> …</ViewerHostContext.Provider>What flips:
AnnotationCanvasskips its autosave path entirely (reads still work, saves are a no-op).MobileDrawerhides annotation, share, and verdict controls based on the same flag.- Your own host UI should branch on
useViewerHost().readOnlyto hide any control that mutates server state.
Public-token / share-link viewers typically run with readOnly: true and
a constrained apiBase (/api/share/<token> etc.), with annotations
read but not written. The annotation service can be wired to a no-op
saveForPage / remove even when readOnly is false, but flipping the
host flag is the standard pattern.