@docsxai/viewer
Static-HTML interactive viewer + burned-annotation renderer + Starlight docs-site emitter. The viewer overlays a pulsing halo, a numbered badge (when a step has multiple call-outs), and a hover-revealed Popper-placed callout from annotations.json over clean screenshots at render time. Per-annotation nudge: { x, y } lets the author shift a callout aside when two would otherwise overlap; the halo stays anchored on the target.
PNGs in the doc pack stay clean (no baked annotations) - re-stylable, re-localisable, and machine-inspectable. For delivery surfaces that can’t run the interactive viewer (Confluence, Notion, plain wikis), burn bakes the same annotations into copies of the PNGs.
Surface
Section titled “Surface”buildViewer({ docsDir, outDir })- reads<docsDir>/<flow>/annotations.json+ screenshots, emits<outDir>/index.html+ per-flow pages. Idempotent.placeCallout(input)insrc/placement.ts- Popper-like placement logic. Pure, coordinate-space-agnostic; tested independently. The single placement implementation shared by the browser overlay and the burner.burnAnnotations({ screenshotPath | screenshotBuffer, annotations, options? })- returns the burned PNG as aBuffer.burnFlow({ docsDir, flow, outDir? })- batch helper: burns every screenshot of a flow intodocs/<flow>/burned/(annotation-less steps are copied unchanged so the directory is the complete drop-in image set).emitStarlightSite({ workspaceDir, outDir, config? })/buildStarlightSite({ siteDir })- the production docs-site renderer; see Starlight site.docsxai-viewerbin:docsxai-viewer build <docs-dir> <out-dir> [--flow <name>]...- the engine’sdocsxai rendershells out to this.docsxai-viewer burn <workspace> [--flow <name>]... [--out <dir>]- writesdocs/<flow>/burned/<step>.png.docsxai-viewer site <workspace> [--out <dir>] [--build] [--title <t>] [--accent <hex>] [--flow <name>]...- emits (and with--buildbuilds) the Starlight site.
Starlight site
Section titled “Starlight site”emitStarlightSite writes a complete, buildable Astro Starlight project from a doc pack - the production docs-site renderer beside the single-file interactive viewer, not a replacement for it. The first-party plugin package @docsxai/plugin-starlight exposes it to the engine’s plugin runtime as the starlight:site renderer.
What gets emitted:
- One MDX page per flow - an H2 per step, the step’s
<step>.mdprose verbatim, and an<AnnotatedShot>figure per screenshot. The figure’s caption lists each annotation’s copy, numbered (<li value>) to match the badge indexes burned into the image - caption numbering and burned pixels can’t drift apart. - Burned-image preference -
docs/<flow>/burned/<step>.pngis copied when present, the clean screenshot is the fallback, and a missing image becomes a placeholder plus a warning (never a failure). - A landing page of flow link-cards with step/annotation counts, and a sidebar ordered by the workspace’s flow
extendsgraph (roots alphabetical, children nested DFS; flows without a flow-file append alphabetically) - the same shape asdocsxai flow-tree. - Theme from the style artifact -
docs/style.json’svisualkeys (brand_color>accent>primary_color) become a derived--sl-color-accent-*scale for both color schemes;visual.logois copied in. Explicit--title/--accent/--logoconfig overrides win. An unparsable style accent is a warning; an unparsable explicit accent is an error. - Pinned, self-contained output - the emitted
package.jsonexact-pinsastro@6.4.6+@astrojs/starlight@0.40.0(both MIT; matching this package’s devDependencies, where the pair is tested). No remote fonts, no CDN imports anywhere in the emitted tree - Starlight ships its own assets and Pagefind search at build time; a test greps every emitted text file for external URLs. - Deterministic - same doc pack + same config → byte-identical file tree (no timestamps, sorted writes), asserted by a two-emit golden test.
buildStarlightSite({ siteDir }) runs astro build programmatically: it resolves the astro bin from this package’s own install and, when the emitted site has no node_modules, symlinks the astro + starlight installs in individually - so building never touches the network. ASTRO_TELEMETRY_DISABLED=1 is always set. That zero-install shortcut requires the site directory to share a filesystem ancestor with the docsxai install (the normal case - the site is emitted inside the repo that installed it); for a fully detached site directory, npm install inside the emitted site and build there. The real-build E2E test is opt-in via DOCSX_STARLIGHT_BUILD=1 (the default test run never invokes astro).
Overlay single-sourcing
Section titled “Overlay single-sourcing”The script inlined into every flow page is generated, not hand-maintained. src/overlay-runtime.ts (browser-side DOM logic) imports the real placeCallout from src/placement.ts; the package build runs scripts/bundle-overlay.mjs (esbuild API) before tsc, bundling it to dist/generated/overlay.js - an unminified es2019 IIFE with no sourcemap, kept readable for auditability. render.ts reads that bundle at render time (resolved relative to import.meta.url, with a src/ → ../dist/ fallback for running from source) and inlines it into each page. The bundle is byte-deterministic for a given esbuild version and is not committed; pnpm build (or pnpm test, which bundles first) produces it.
CSP posture
Section titled “CSP posture”Every emitted page carries
Content-Security-Policy: default-src 'none'; img-src 'self' data:; style-src 'unsafe-inline'; script-src 'unsafe-inline'
- matching the inline-asset reality (inline
<style>/<script>, workspace-local images) while blocking all network egress: no CDN fetches, no remote fonts, no beacons. The emitted HTML is fully self-contained.
Step write-ups
Section titled “Step write-ups”docs/<flow>/<step>.md files render through micromark in its safe default mode: raw HTML in the markdown is escaped and dangerous link protocols are dropped, so a write-up can’t introduce markup or script into the page.
Burned annotations (burn.ts)
Section titled “Burned annotations (burn.ts)”Design constraints, in order:
- Browser-free. No Chromium, no Playwright, no DOM. The pipeline is Satori (HTML/CSS-subset flexbox layout → SVG) →
@resvg/resvg-js(SVG → PNG). A regression test asserts no viewer source module imports playwright. - Deterministic. Same inputs → byte-identical PNG, asserted by a two-run golden test. The clean screenshot is embedded in the Satori tree as a data-URI
<img>, so the whole frame rasterises in a single resvg pass - one encoder produces every output byte and there is no separate composite/re-encode step. Satori layout and resvg rasterisation are pure functions of their inputs; system fonts are never loaded; text is emitted as glyph paths; resvg writes no timestamps. - Faithful to the interactive viewer. Halo (accent border + glow) on the bounding box, numbered badge when
indexis present, rounded-rect callout (white background, 1px border) with copy wrapped to the same 280px outer clamp, triangle arrow perarrow_style(8 directions),nudgeoffsets applied to callout + arrow only. Placement reusesplaceCallout; text measurement/wrapping uses the vendored font’s own cmap/hmtx metrics (src/font-metrics.ts) as the burner’s stand-in for the overlay’s DOM measuring probe. - Engine-decoupled. The annotation record type is redeclared structurally in
src/annotations.ts- it mirrors thedocsxai/annotations@1schema; the viewer never imports the engine package.
Vendored font
Section titled “Vendored font”assets/fonts/inter-regular.ttf - Inter Regular v4.1 from the official rsms/inter release, licensed under the SIL Open Font License 1.1 (assets/fonts/LICENSE.txt). Satori needs raw font bytes; only the Regular weight ships, so bold-ish elements (the badge) render in Regular.
License
Section titled “License”Apache-2.0. Runtime deps: satori (MPL-2.0), @resvg/resvg-js (MPL-2.0), micromark (MIT); vendored Inter font (OFL-1.1).