Skip to content

Dark-mode the document background, not just the rail, update text fonts in dark mode#6

Open
AnnaXWang wants to merge 6 commits into
mainfrom
hypeship/unify-doc-rail-scroll
Open

Dark-mode the document background, not just the rail, update text fonts in dark mode#6
AnnaXWang wants to merge 6 commits into
mainfrom
hypeship/unify-doc-rail-scroll

Conversation

@AnnaXWang

@AnnaXWang AnnaXWang commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Summary

The light/dark toggle themed the bar and rail but left the author's document (rendered in the sandboxed iframe) untouched, so picking dark left the doc area light — visibly inconsistent.

This forwards the toggle mode into the iframe via a new jh:themeMode message. The overlay (the only code that can touch the opaque-origin document) forces the document's color-scheme + background:

  • dark/light force the doc canvas + default text (!important, so an explicit pick wins over an authored background); auto removes the override and restores the doc exactly as authored.
  • The overlay re-samples after applying, so the chrome palette and dark-highlight treatment follow the document through the existing jh:theme round-trip — bar, rail, doc, and highlights all end up consistent.

Small, presentation-only change: +9 lines in the shell (one effect), +39 in the overlay (a jh:themeMode handler that sets color-scheme and an injected bg/fg style). No API/DB/anchoring changes.

Inherent limits (documented at the injection site)

  • Per-element authored colors still cascade — we can't safely invert an arbitrary design, so element-level color declarations are left alone.
  • @media (prefers-color-scheme) can't be driven from script, so a doc that themes itself only via that media query won't respond to the toggle.

Verification

  • tsc clean, vitest 108/108, next build clean.
  • Harness with a UA-default doc and an explicitly-light-authored doc: forced-dark flips both canvases dark with body text inverted; forced-light normalizes to white; auto restores the authored look.

Note: this PR was scoped down to just the dark-mode document change. The earlier single-scrollbar work (and its iframe-height-ratchet fix) is preserved on the hypeship/unify-scroll-archive branch, out of this PR.


Note

Low Risk
Presentation-only postMessage and DOM styling in the sandboxed overlay; no API, auth, or persistence changes.

Overview
The light/dark/auto toggle now themes the document inside the sandboxed iframe, not only the comment rail and chrome.

CommentsShell sends jh:themeMode when the viewer changes mode and again on every jh:ready (via modeRef) so iframe reloads do not flash the authored theme. DEFAULT_DARK foreground is white for forced-dark fallback chrome.

Overlay handles jh:themeMode by forcing color-scheme and page background, walking the DOM to recolor prose on the page canvas while skipping surfaces (code blocks, elements with their own background) and links. Forced modes report fixed bg/fg in jh:theme so rail chrome matches; highlight segments keep .jh-doc-fg through focus/hover and repaints call markForcedText() when needed.

Reviewed by Cursor Bugbot for commit cc87ac0. Bugbot is set up for automated code reviews on this repo. Configure here.

@vercel

vercel Bot commented Jul 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
justhtml Ready Ready Preview, Comment Jul 6, 2026 5:12pm

@AnnaXWang AnnaXWang changed the title Unify the doc and comment rail into one scrollbar Viewer: unify doc+rail scroll, and dark-mode the document Jul 2, 2026
@AnnaXWang AnnaXWang closed this Jul 2, 2026
The light/dark toggle themed the bar and rail but left the author's
document (rendered in the sandboxed iframe) untouched, so picking dark
left the doc area light — visibly inconsistent.

Forward the toggle mode into the iframe via a new jh:themeMode message.
The overlay (the only code that can touch the opaque-origin document)
forces the doc's color-scheme and, with !important, its background/text
so an explicit pick wins over an authored background; "auto" removes the
override and restores the doc exactly as authored. The overlay re-samples
after applying, so the chrome palette and dark-highlight treatment follow
the document through the existing jh:theme round-trip.

Per-element authored colors still cascade (we can't invert an arbitrary
design) and @media(prefers-color-scheme) can't be driven from script;
both are inherent and documented at the injection site.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@AnnaXWang AnnaXWang changed the title Viewer: unify doc+rail scroll, and dark-mode the document Dark-mode the document background, not just the rail Jul 2, 2026
@AnnaXWang AnnaXWang reopened this Jul 2, 2026
@AnnaXWang AnnaXWang force-pushed the hypeship/unify-doc-rail-scroll branch from a0fbf0c to 0207d85 Compare July 2, 2026 03:05
@AnnaXWang AnnaXWang changed the title Dark-mode the document background, not just the rail Dark-mode the document background, not just the rail, update text fonts in dark mode Jul 2, 2026
The forced-dark override set body text to #c9d1d9 (a light gray); use
#ffffff so default document text is white on the dark canvas.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Setting body color white didn't work: authored element rules (p,li
{color:#1a1a1a}, th{color:#444}) beat inheritance, so most text stayed
dark. But blanket-whitening every element breaks anything with its own
light background — code chips, pill badges, callout boxes would render
white-on-light.

Walk the DOM instead: recolor the text of every element sitting on the
page background, and skip any element with its own background (or a code
block) plus its subtree — generalizing "leave code alone" to badges and
boxes too. A first pass pins each such surface's authored text color
inline (so a whitened ancestor can't leak white into a code chip that
inherits its color). Links keep their accent.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The rail/comment-card colors are derived from the doc's sampled fg. On a
light doc forced dark, that fg is the authored dark text lifted only to
AA — a gray, so comment text read gray, not white. When the viewer forces
a theme, report the forced fg/bg from the overlay's sample so the chrome
palette matches the forced document (white comment text in dark), and set
the DEFAULT_DARK fallback fg to white for the pre-sample moment. Auto mode
still samples the doc so the chrome adapts to genuinely-dark docs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@AnnaXWang AnnaXWang marked this pull request as ready for review July 2, 2026 03:47
@AnnaXWang AnnaXWang requested a review from rgarcia July 2, 2026 03:48
Comment thread lib/docs/overlay.ts
Comment thread lib/docs/overlay.ts
Two review findings: forcing dark left anchored-comment (highlight) text
dark-on-dark. The segment spans carry a wash background in light mode, so
the surface pass pinned their authored dark color; and whitenPage skipped
them, so an authored span color rule kept them dark once jh-dark turned
the wash into an underline-only treatment.

Treat data-jh-seg spans as prose, not surfaces: don't pin them, and DO
recolor them (color:#fff !important in dark, beating any span rule). A
segment inside a code block still sits inside a surface and is never
reached, so highlighted code text keeps its own color. Re-run the recolor
at the end of paint() so segments (re)created on reload/resize — which
carry no themeMode message — are recolored too.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@AnnaXWang

Copy link
Copy Markdown
Contributor Author

Addressed both highlight-segment findings (commit 5b00998).

Root cause: highlight wrapper spans (data-jh-seg) carry a wash background in light mode, so (1) the surface pass pinned their authored dark text color inline, and (2) whitenPage skipped them — so once jh-dark turned the wash into an underline-only treatment, the anchored-comment text stayed dark on the dark canvas.

Fix: treat data-jh-seg spans as prose, not surfaces — don't pin them, and recolor them via .jh-doc-fg (color:#fff !important in dark, which beats any authored span{color} rule). Segments inside a code block still sit within a surface and are never reached, so highlighted code text keeps its own color. Also re-run the recolor at the end of paint() so segments recreated on reload/resize (which carry no themeMode message) get recolored.

Verified against a real doc: the anchored "What's missing for prod" heading now renders white with its amber underline instead of near-invisible dark-on-dark.

Comment thread app/d/[slug]/CommentsShell.tsx
Comment thread app/d/[slug]/CommentsShell.tsx
Comment thread lib/docs/overlay.ts
Three review findings on the forced-theme flow:

- Theme applied after paint / omitted on ready: jh:themeMode was sent only
  from a useEffect keyed on overlayReady/mode. On jh:ready the shell posted
  anchors+reactions (→ paint) before that effect ran, briefly showing the
  authored doc; and on an iframe reload overlayReady stays true so the
  effect never re-fired, leaving the fresh overlay on the authored theme.
  Now the jh:ready handler sends jh:themeMode FIRST, on every ready, via a
  modeRef so it reads the current mode.

- Focus styles strip forced text class (High): applyFocusStyles rebuilt each
  segment with el.className = "d"+depth, dropping jh-doc-fg, so hover/focus
  reverted highlighted prose to dark-on-dark. It now preserves jh-doc-fg.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@AnnaXWang

Copy link
Copy Markdown
Contributor Author

Addressed all three findings (commit cc87ac0).

  • Theme mode applied after paint / omitted on ready — the shell now sends jh:themeMode from the jh:ready handler itself, before jh:anchors, so the overlay forces the theme before it paints (no authored-doc flash) and on every ready, including an iframe reload (where overlayReady stays true and the mode useEffect wouldn't re-fire). It reads the live value via a modeRef so there's no stale closure.

  • Focus styles strip forced text class (High)applyFocusStyles rebuilt each segment as el.className = "d"+depth, dropping jh-doc-fg; it now preserves it, so hover/focus keeps the forced text color instead of reverting highlighted prose to dark-on-dark.

Verified: after forcing dark and hovering an anchored segment, its class is d1 jh-doc-fg jh-hover with computed color: rgb(255,255,255). tsc / 108 tests / build green.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit cc87ac0. Configure here.

// (no authored-doc flash), and on EVERY ready — including an iframe reload,
// where overlayReady stays true so the mode effect below won't re-fire and
// the freshly-loaded overlay would otherwise reset to the authored theme.
postToOverlay({ type: "jh:themeMode", mode: modeRef.current });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale mode on iframe ready

Medium Severity

On jh:ready, the shell sends jh:themeMode using modeRef, but modeRef only tracks React mode, which stays "auto" until a mount effect reads localStorage. If the iframe becomes ready before that re-render, the overlay gets mode: "auto", paints the authored document, and only later receives the saved dark/light preference—contradicting the no-flash intent for persisted theme choice.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit cc87ac0. Configure here.

// Mirror mode into a ref so the jh:ready handler (not in mode's deps) reads the
// current value — it must send the forced theme on every ready, including reloads.
const modeRef = useRef<ThemeMode>(mode);
modeRef.current = mode;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale theme tints auto chrome

Medium Severity

After leaving forced dark or light, auto still drives the bar and rail from the last jh:theme sample until a new one arrives. That sample reflects the forced document colors, not the authored page, so chrome can stay dark on a light doc (or light on a dark doc) while the iframe already restored the authored look.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit cc87ac0. Configure here.

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