Dark-mode the document background, not just the rail, update text fonts in dark mode#6
Dark-mode the document background, not just the rail, update text fonts in dark mode#6AnnaXWang wants to merge 10 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
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>
a0fbf0c to
0207d85
Compare
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>
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>
|
Addressed both highlight-segment findings (commit 5b00998). Root cause: highlight wrapper spans ( Fix: treat 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. |
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>
|
Addressed all three findings (commit cc87ac0).
Verified: after forcing dark and hovering an anchored segment, its class is |
The light/dark toggle lived only in CommentsShell, so the cold path (PlainShell — no comments and a viewer who can't comment) had no way to toggle themes. The toggle is a viewer feature, not a comments one, so it must be available on every doc. Route every doc through CommentsShell and retire PlainShell. On a doc with no comments the rail just starts collapsed; a viewer who can't comment gets no composer — but the theme toggle (and the overlay that repaints the document) is always present. The SSR theme detection is unchanged; it now seeds CommentsShell's initial theme in every case. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
On a doc with no comments the toggle themed the chrome bar but not the document. The document theming is driven by jh:themeMode, which the shell only posts once overlayReady is true — and overlayReady is set solely from the overlay's one-shot jh:ready. When the iframe loads before the shell's message listener mounts (fast or cached loads), that jh:ready is missed, overlayReady stays false, and jh:themeMode (plus anchors) is silently never sent; the bar still themes because that's pure React state. Ping the overlay from the iframe's onLoad — it replies jh:ready — so overlayReady is set reliably regardless of load timing, and the jh:ready handler then (re)sends the forced theme. Also fixes the same race for anchored highlights on cached loads. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two review findings on the theme sampling timing: - Stale forced sample drives auto chrome: while forced, jh:theme reports the forced colors. After switching back to auto the shell kept driving the bar/rail from that stale sample (e.g. dark on a light doc) until a fresh sample arrived. Reset the shell's theme to the authored SSR baseline on every mode change; the overlay re-samples right after. - Premature bootstrap sample: the overlay ran sampleTheme() at init while forcedScheme was still unset, reporting the authored theme before the shell's jh:themeMode could apply a forced one. Drop that eager sample — the jh:themeMode handler (sent on jh:ready) does the first, forced-aware sample, with the window.load and settle-timeout re-emits as the safety net for late CSS or a host that never sends jh:themeMode. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
Addressed both theme-sampling timing findings (commit 9a354b6).
tsc / 108 tests / build green. |
Two coupled fragilities kept producing regressions; fix them at the root. Readiness handshake: overlayReady (which gates jh:themeMode, anchors and reactions) was set only from the overlay's one-shot jh:ready, which is missed if the iframe loads before the shell's listener mounts (fast/cached loads) — so the document silently never received forced theme. The shell now pings the overlay until it replies jh:ready (capped), immune to load timing. Removes the onLoad ping. Theme sampling: jh:theme conflated authored vs forced colors, and the patches around it (forced bg/fg override, setTheme(initialTheme) reset) each broke another mode. The overlay now ALWAYS reports the doc's AUTHORED colors (captured while unforced, cached, reported even while forced); the in-doc highlight darkness (jh-dark) follows the EFFECTIVE darkness; and the shell gates chrome purely by mode (auto → authored sample; dark → sample-if-dark else DEFAULT_DARK white base; light → literals). This removes both the stale forced tint after switching to auto and the wrong light chrome on a dark doc the server couldn't coarse-detect (initialTheme null) — no reset needed. Verified: forced dark reports authored + jh-dark on + doc canvas dark (white prose, code/badges/boxes preserved, highlighted text white); auto restores authored with jh-dark off and no stale tint. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
Addressed both findings at the root (commit 144b54b), after planning to stop the symptom-by-symptom churn. Ready handshake can fail silently (High) — Auto mode resets chrome wrongly (Medium) — root cause was that Verified via harness on a real doc: forced dark → reports authored + |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 144b54b. 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; |
There was a problem hiding this comment.
Auto chrome beats document
Medium Severity
Switching to auto recomputes effectiveTheme from stored authored theme immediately, while the iframe document stays on forced light/dark until jh:themeMode is handled. That yields a visible window where bar/rail/stage colors match the authored doc but the canvas still shows the previous forced override (or the reverse).
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 144b54b. Configure here.


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:themeModemessage. The overlay (the only code that can touch the opaque-origin document) forces the document'scolor-scheme+ background:dark/lightforce the doc canvas + default text (!important, so an explicit pick wins over an authored background);autoremoves the override and restores the doc exactly as authored.jh:themeround-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:themeModehandler that setscolor-schemeand an injected bg/fg style). No API/DB/anchoring changes.Inherent limits (documented at the injection site)
colordeclarations 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
tscclean,vitest108/108,next buildclean.autorestores 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-archivebranch, out of this PR.Note
Medium Risk
Presentation-only but touches iframe postMessage, overlay DOM mutation, and removes the zero-JS cold path—regressions could affect theme flash, highlights on forced dark, or load behavior on comment-less docs.
Overview
The light/dark toggle now repaints the sandboxed document, not only the bar and rail.
CommentsShellsendsjh:themeMode(auto/light/dark) into the iframe on mode changes, on everyjh:ready(viamodeRef), and after ajh:pingretry loop so fast loads don’t miss the handshake.In
lib/docs/overlay.ts, the overlay applies forcedcolor-schemeand page background, walks the DOM to recolor prose on the page canvas while skipping surfaces (code blocks, elements with their own background), and keepsjh:themereporting authored colors so chrome inautodoesn’t stay tinted. Highlights use effective darkness; forced text is reapplied after comment paint and when focus classes are rebuilt.PlainShellis removed andpage.tsxalways rendersCommentsShellwithoverlay=1, so the theme control exists on every doc (rail can stay collapsed).DEFAULT_DARKfallback foreground is#ffffff.Reviewed by Cursor Bugbot for commit 144b54b. Bugbot is set up for automated code reviews on this repo. Configure here.