Skip to content

Render arbitrary HTML in HtmlViewer without altering it#1023

Merged
backnotprop merged 1 commit into
mainfrom
feat/fix-html-viewer
Jul 8, 2026
Merged

Render arbitrary HTML in HtmlViewer without altering it#1023
backnotprop merged 1 commit into
mainfrom
feat/fix-html-viewer

Conversation

@backnotprop

Copy link
Copy Markdown
Owner

The bug

HtmlViewer injected host-app state into rendered documents and clobbered author CSS. A document defining common token names (--muted, --background, --border, …) rendered with wrong colors — e.g. body text set via the author's --muted turned near-invisible in a dark host because the host's --muted (a background color) overwrote the author's (a text gray). Reproduced in Plannotator and in Workspaces (which consumes @plannotator/ui); root-caused to three injection sites:

  1. srcdoc build — ~26 bare host tokens copied into a :root block inside the document.
  2. bridge theme handler — on every host theme flip, tokens re-applied inline on the author's documentElement (beats any author stylesheet), plus a light class toggled on their root.
  3. asymmetric color-scheme:light injection, and ins/del diff styles injected unconditionally, restyling legitimate author markup.

The fix

Arbitrary documents now render exactly as in a standalone browser tab — the viewer writes nothing into the document's namespace:

  • Host tokens are pushed only under the viewer-owned --pn-* prefix (packages/ui/components/html-viewer/srcdoc.ts, new pure module). ANNOTATION_HIGHLIGHT_CSS and the bridge read only var(--pn-…, fallback).
  • The bridge theme handler refuses non---pn- writes and never touches the author root's class list unless the document opts in. No color-scheme injection — light/dark resolves from the document + OS.
  • Host theme-following is opt-in via <meta name="plannotator-theme" content="host">, which restores the bare-token push, the light class, and a now-symmetric color-scheme sync. Plannotator's own artifact generator (visual-explainer skill) emits the tag, so generated artifacts keep following the host theme.
  • Diff CSS is gated and scoped: injected only while the version-diff view is active, and scoped to ins.plannotator-diff / del.plannotator-diff, which htmlDiff now emits on its generated wrappers. Author <ins>/<del> markup is never restyled, even in diff view.

Acceptance tests (pinned)

packages/ui/components/html-viewer/srcdoc.test.ts:

  • No bare custom-property declarations reach a non-opted-in document (mutation guard — reintroducing one goes red).
  • No color-scheme injection in either host theme; opt-in restores bare tokens + symmetric color-scheme.
  • Diff CSS absent on plain renders; no bare ins/del selectors ever.
  • Viewer CSS and bridge script reference only --pn-* variables.
  • DOM test runs the actual bridge script, fires a theme-flip message, and asserts nothing lands on the author's documentElement except --pn-* properties (and that the opt-in restores the old behavior).

packages/shared/html-diff.test.ts: updated for the class-tagged wrappers + new case pinning that author-written <ins>/<del> pass through untagged.

Notes

  • Artifacts generated before this change lack the meta tag and will stop live-following the host theme (they fall back to their own :root defaults + prefers-color-scheme) — intended: arbitrary documents get zero unprefixed injection by default.
  • Workspaces will pick this up via an @plannotator/ui version bump (supersedes their 2-line patch / H-ask-1).

Test plan

  • bun test — 1980 pass, 0 fail (incl. DOM_TESTS=1 for the bridge DOM test)
  • bun run typecheck — clean, including the strict-consumer tsconfig
  • bun run --cwd apps/review build && bun run build:hook — bundles build

https://claude.ai/code/session_01MDqD8jdgTbdjiXcVVigzV2

HtmlViewer injected host-app state into rendered documents: ~26 bare
theme tokens (--muted, --background, ...) written into :root and
re-applied inline on the author's documentElement on every host theme
flip, a `light` class toggled on the author's root, an asymmetric
color-scheme:light injection, and unconditional ins/del diff styles.
Documents defining the same token names rendered with wrong colors in
both Plannotator and @plannotator/ui consumers.

Arbitrary documents now render exactly as in a standalone tab:

- Host tokens are pushed only under the viewer-owned --pn-* prefix;
  annotation CSS and the bridge read only var(--pn-*, fallback). The
  bridge refuses non---pn- writes and never touches the root class
  list unless the document opts in.
- Host theme-following is opt-in per document via
  <meta name="plannotator-theme" content="host">, which restores the
  bare-token push, the light class, and a symmetric color-scheme sync.
  The visual-explainer skill now emits the tag in generated artifacts.
- Diff CSS is injected only while the version-diff view is active and
  scoped to ins/del.plannotator-diff, which htmlDiff now emits on its
  generated wrappers; author <ins>/<del> markup is never restyled.

The srcdoc injection logic moves to a pure module (srcdoc.ts) with
tests pinning the neutrality contract, including a DOM test that runs
the actual bridge script and asserts a theme flip lands nothing on the
author's root except --pn-* properties.

Claude-Session: https://claude.ai/code/session_01MDqD8jdgTbdjiXcVVigzV2
@backnotprop backnotprop merged commit 1047539 into main Jul 8, 2026
13 checks passed
backnotprop added a commit that referenced this pull request Jul 8, 2026
HtmlViewer no longer injects bare theme tokens, root classes, or
color-scheme into rendered documents (#1023); host theming is opt-in
via <meta name="plannotator-theme" content="host">. Supersedes
Workspaces' H-ask-1 patch.

Claude-Session: https://claude.ai/code/session_01MDqD8jdgTbdjiXcVVigzV2
@backnotprop

Copy link
Copy Markdown
Owner Author

This is now released in version 0.23.0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant