From 0207d855537ef8398b8e7c3839bdd7490c9892dd Mon Sep 17 00:00:00 2001 From: AnnaXWang <6621137+AnnaXWang@users.noreply.github.com> Date: Thu, 2 Jul 2026 02:10:56 +0000 Subject: [PATCH 01/10] Make the theme toggle repaint the document, not just the chrome MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- app/d/[slug]/CommentsShell.tsx | 9 ++++++++ lib/docs/overlay.ts | 39 ++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/app/d/[slug]/CommentsShell.tsx b/app/d/[slug]/CommentsShell.tsx index 3759567..fa61154 100644 --- a/app/d/[slug]/CommentsShell.tsx +++ b/app/d/[slug]/CommentsShell.tsx @@ -470,6 +470,15 @@ export default function CommentsShell(props: Props) { if (overlayReady) postToOverlay({ type: "jh:active", id: activeId }); }, [activeId, overlayReady, postToOverlay]); + // Forward the theme toggle into the iframe so the overlay repaints the DOCUMENT + // (not just the chrome): "dark"/"light" force the doc's color-scheme + background, + // "auto" leaves it as authored. The overlay re-samples after applying, so the + // chrome palette and highlight treatment follow the doc through the existing + // jh:theme round-trip — no separate wiring needed here. + useEffect(() => { + if (overlayReady) postToOverlay({ type: "jh:themeMode", mode }); + }, [mode, overlayReady, postToOverlay]); + const visibleThreads = useMemo( () => threads.filter((t) => showResolved || !t.resolved), [threads, showResolved] diff --git a/lib/docs/overlay.ts b/lib/docs/overlay.ts index 6bcf6c9..f8efc4a 100644 --- a/lib/docs/overlay.ts +++ b/lib/docs/overlay.ts @@ -12,6 +12,7 @@ // { type:"jh:focus", key } (focus a key from the rail; null clears) // { type:"jh:scrollTo", id } // { type:"jh:clearSelection" } +// { type:"jh:themeMode", mode } ("dark"|"light" force doc theme; else auto) // overlay → shell: { type:"jh:ready" } // { type:"jh:positions", positions:{ [id]: yTopPx }, docHeight, scrollY } // (comment highlight y in doc space; doc scroll for rail sync) @@ -77,6 +78,7 @@ export const OVERLAY_SCRIPT = String.raw` // isDark uses WCAG relative luminance with a small hysteresis dead-band so a // mid-tone bg doesn't flip-flop across re-emits. var lastDark = null; // hysteresis memory across re-emits + var forcedScheme = null; // viewer toggle: null = auto (doc as authored); "dark"|"light" force it function rxParse(s){ if (!s) return null; var m = String(s).match(/rgba?\(\s*([\d.]+)[,\s]+([\d.]+)[,\s]+([\d.]+)(?:[,\s\/]+([\d.%]+))?/i); @@ -133,6 +135,38 @@ export const OVERLAY_SCRIPT = String.raw` } catch(e){} } + // ---- forced document theme (viewer's light/dark toggle) ---- + // The toggle themes the chrome (bar/rail) in the shell; this makes it also repaint + // the DOCUMENT. color-scheme flips UA defaults (canvas, form controls, scrollbars); + // the !important bg/color forces the canvas even over an authored background, so an + // explicit pick wins. Per-element authored colors still cascade — we can't invert an + // arbitrary design, and @media(prefers-color-scheme) can't be driven from script. + // "auto" removes the override entirely, restoring the doc exactly as authored. + function applyDocScheme(){ + try { + var de = document.documentElement; if (!de) return; + var st = document.getElementById("jh-doc-theme"); + if (!forcedScheme){ + de.classList.remove("jh-force-dark", "jh-force-light"); + de.style.colorScheme = ""; + if (st && st.parentNode) st.parentNode.removeChild(st); + return; + } + if (!st){ + st = document.createElement("style"); st.id = "jh-doc-theme"; + st.textContent = + "html.jh-force-dark{color-scheme:dark}" + + "html.jh-force-dark,html.jh-force-dark body{background-color:#0d1117!important;color:#c9d1d9!important}" + + "html.jh-force-light{color-scheme:light}" + + "html.jh-force-light,html.jh-force-light body{background-color:#ffffff!important;color:#111111!important}"; + (document.head || de).appendChild(st); + } + de.classList.toggle("jh-force-dark", forcedScheme === "dark"); + de.classList.toggle("jh-force-light", forcedScheme === "light"); + de.style.colorScheme = forcedScheme; + } catch(e){} + } + // ---- text-content walker (anchor resolution against the live DOM) ---- // We snapshot the text model ONCE per paint (over the pristine DOM, before any // segment wrapping), resolve every anchor's [start,end) against it, then split. @@ -660,6 +694,11 @@ export const OVERLAY_SCRIPT = String.raw` } else if (d.type === "jh:scrollTo"){ var sk = (typeof d.id === "number") ? "c:"+d.id : String(d.id); scrollToKey(sk); } else if (d.type === "jh:clearSelection"){ var s=window.getSelection(); if(s) s.removeAllRanges(); } + else if (d.type === "jh:themeMode"){ + forcedScheme = (d.mode === "dark" || d.mode === "light") ? d.mode : null; + applyDocScheme(); + sampleTheme(); // re-read colors so the chrome + highlight follow the forced doc theme + } else if (d.type === "jh:ping"){ send({type:"jh:ready"}); } }); From 3f85bdd7dee2cf9e32cddbb99b84aecbf6211d57 Mon Sep 17 00:00:00 2001 From: AnnaXWang <6621137+AnnaXWang@users.noreply.github.com> Date: Thu, 2 Jul 2026 03:14:31 +0000 Subject: [PATCH 02/10] Use pure white for body text in forced dark mode 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 --- lib/docs/overlay.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/docs/overlay.ts b/lib/docs/overlay.ts index f8efc4a..307afcb 100644 --- a/lib/docs/overlay.ts +++ b/lib/docs/overlay.ts @@ -156,7 +156,7 @@ export const OVERLAY_SCRIPT = String.raw` st = document.createElement("style"); st.id = "jh-doc-theme"; st.textContent = "html.jh-force-dark{color-scheme:dark}" - + "html.jh-force-dark,html.jh-force-dark body{background-color:#0d1117!important;color:#c9d1d9!important}" + + "html.jh-force-dark,html.jh-force-dark body{background-color:#0d1117!important;color:#ffffff!important}" + "html.jh-force-light{color-scheme:light}" + "html.jh-force-light,html.jh-force-light body{background-color:#ffffff!important;color:#111111!important}"; (document.head || de).appendChild(st); From d6ce92643ce4792962e7d5637f548bf16a0c1263 Mon Sep 17 00:00:00 2001 From: AnnaXWang <6621137+AnnaXWang@users.noreply.github.com> Date: Thu, 2 Jul 2026 03:37:09 +0000 Subject: [PATCH 03/10] Whiten all document text in dark mode, preserving backgrounded blocks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- lib/docs/overlay.ts | 77 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 69 insertions(+), 8 deletions(-) diff --git a/lib/docs/overlay.ts b/lib/docs/overlay.ts index 307afcb..e79a802 100644 --- a/lib/docs/overlay.ts +++ b/lib/docs/overlay.ts @@ -136,12 +136,69 @@ export const OVERLAY_SCRIPT = String.raw` } // ---- forced document theme (viewer's light/dark toggle) ---- - // The toggle themes the chrome (bar/rail) in the shell; this makes it also repaint - // the DOCUMENT. color-scheme flips UA defaults (canvas, form controls, scrollbars); - // the !important bg/color forces the canvas even over an authored background, so an - // explicit pick wins. Per-element authored colors still cascade — we can't invert an - // arbitrary design, and @media(prefers-color-scheme) can't be driven from script. - // "auto" removes the override entirely, restoring the doc exactly as authored. + // The toggle themes the chrome (bar/rail) in the shell; this repaints the DOCUMENT. + // Setting body color alone doesn't work — authored rules like p,li{color:#1a1a1a} + // beat inheritance. And blanket-whitening every element breaks anything with its own + // (light) background: code blocks, badges, callout boxes would get white-on-light. + // So: force color-scheme + the page background, then WALK the DOM and recolor only + // the text of elements sitting ON that page background. Any element with its own + // background (or a code block) keeps its authored colors, and so does its subtree — + // "leave code alone", generalized. Links keep their accent. "auto" removes it all. + // (@media(prefers-color-scheme) still can't be driven from script.) + var FG_SKIP = { SCRIPT:1, STYLE:1, PRE:1, CODE:1, SVG:1, IMG:1, CANVAS:1, VIDEO:1, IFRAME:1, A:1, BUTTON:1, INPUT:1, SELECT:1, TEXTAREA:1, OPTION:1, NOSCRIPT:1 }; + function ownsBackground(el){ + try { + var bg = getComputedStyle(el).backgroundColor; + if (!bg || bg === "transparent") return false; + var m = bg.match(/rgba?\(([^)]+)\)/); + if (m){ var p = m[1].split(","); return p.length < 4 || parseFloat(p[3]) > 0.05; } + return true; + } catch(e){ return false; } + } + function isSurface(el, tag){ return tag === "PRE" || tag === "CODE" || ownsBackground(el); } + // PASS 1 — before any whitening, pin each surface's authored text color inline. A + // surface (code block / anything with its own background) often has no color of its + // own and relies on inheriting the body's dark text; once we whiten its ancestor + // that inheritance would turn its text white on a light surface. A direct inline + // color beats inheritance, so this preserves the authored look ("keep font color"). + function pinSurfaces(el){ + var kids = el.children; + for (var i = 0; i < kids.length; i++){ + var c = kids[i], tag = c.tagName; + if (tag === "SCRIPT" || tag === "STYLE") continue; + if (isSurface(c, tag)){ + if (!c.hasAttribute("data-jh-fg-pin")){ + c.setAttribute("data-jh-fg-pin", c.style.color); + c.style.color = getComputedStyle(c).color; + } + } else { + pinSurfaces(c); + } + } + } + // PASS 2 — recolor text of elements sitting ON the page background; skip surfaces + // (and their subtrees), links, media, and painted highlight segments. + function whitenPage(el){ + var kids = el.children; + for (var i = 0; i < kids.length; i++){ + var c = kids[i], tag = c.tagName; + if (tag === "SCRIPT" || tag === "STYLE") continue; + if (isSurface(c, tag)) continue; + if (!FG_SKIP[tag] && !c.hasAttribute("data-jh-seg")) c.classList.add("jh-doc-fg"); + whitenPage(c); + } + } + function markForcedText(){ + try { + var pinned = document.querySelectorAll("[data-jh-fg-pin]"); + for (var i = 0; i < pinned.length; i++){ var e = pinned[i]; e.style.color = e.getAttribute("data-jh-fg-pin") || ""; e.removeAttribute("data-jh-fg-pin"); } + var whited = document.querySelectorAll(".jh-doc-fg"); + for (var j = 0; j < whited.length; j++) whited[j].classList.remove("jh-doc-fg"); + if (!forcedScheme || !document.body) return; + pinSurfaces(document.body); + whitenPage(document.body); + } catch(e){} + } function applyDocScheme(){ try { var de = document.documentElement; if (!de) return; @@ -150,20 +207,24 @@ export const OVERLAY_SCRIPT = String.raw` de.classList.remove("jh-force-dark", "jh-force-light"); de.style.colorScheme = ""; if (st && st.parentNode) st.parentNode.removeChild(st); + markForcedText(); return; } if (!st){ st = document.createElement("style"); st.id = "jh-doc-theme"; st.textContent = "html.jh-force-dark{color-scheme:dark}" - + "html.jh-force-dark,html.jh-force-dark body{background-color:#0d1117!important;color:#ffffff!important}" + + "html.jh-force-dark,html.jh-force-dark body{background-color:#0d1117!important}" + + "html.jh-force-dark .jh-doc-fg{color:#fff!important}" + "html.jh-force-light{color-scheme:light}" - + "html.jh-force-light,html.jh-force-light body{background-color:#ffffff!important;color:#111111!important}"; + + "html.jh-force-light,html.jh-force-light body{background-color:#ffffff!important}" + + "html.jh-force-light .jh-doc-fg{color:#111!important}"; (document.head || de).appendChild(st); } de.classList.toggle("jh-force-dark", forcedScheme === "dark"); de.classList.toggle("jh-force-light", forcedScheme === "light"); de.style.colorScheme = forcedScheme; + markForcedText(); } catch(e){} } From e728b12a17532942c877c5af654699df98c9f270 Mon Sep 17 00:00:00 2001 From: AnnaXWang <6621137+AnnaXWang@users.noreply.github.com> Date: Thu, 2 Jul 2026 03:46:06 +0000 Subject: [PATCH 04/10] Make comment (rail) text white in forced dark mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- app/d/[slug]/CommentsShell.tsx | 2 +- lib/docs/overlay.ts | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/d/[slug]/CommentsShell.tsx b/app/d/[slug]/CommentsShell.tsx index fa61154..25247a3 100644 --- a/app/d/[slug]/CommentsShell.tsx +++ b/app/d/[slug]/CommentsShell.tsx @@ -41,7 +41,7 @@ const THEME_MODE_KEY = "jh:theme-mode"; // Fallback dark base when a viewer FORCES dark on a doc we didn't sample as dark // (a light doc, or before the overlay's first jh:theme). Real dark docs keep // their own sampled colors so the chrome matches the page. -const DEFAULT_DARK: ThemeSample = { bg: "#0d1117", fg: "#c9d1d9", isDark: true }; +const DEFAULT_DARK: ThemeSample = { bg: "#0d1117", fg: "#ffffff", isDark: true }; type Reaction = { emoji: string; count: number; authors: string[] }; type Anchor = { exact: string; prefix?: string; suffix?: string; start?: number; end?: number } | null; diff --git a/lib/docs/overlay.ts b/lib/docs/overlay.ts index e79a802..eef51e5 100644 --- a/lib/docs/overlay.ts +++ b/lib/docs/overlay.ts @@ -105,6 +105,12 @@ export const OVERLAY_SCRIPT = String.raw` if (!bgRgb) bgRgb = [255,255,255]; // both transparent → treat as white (light) // fg: body color (fall back to documentElement). var fgRgb = (bodyCS && rxParse(bodyCS.color)) || (deCS && rxParse(deCS.color)) || [17,17,17]; + // When the viewer FORCES a theme, report the forced bg/fg so the chrome palette + // (rail + comment cards, derived from this sample) matches the forced document — + // e.g. comment text is white in forced dark, not a gray lifted from the doc's own + // authored fg. Auto (no force) keeps sampling the doc so the chrome adapts to it. + if (forcedScheme === "dark"){ bgRgb = [13,17,23]; fgRgb = [255,255,255]; } + else if (forcedScheme === "light"){ bgRgb = [255,255,255]; fgRgb = [17,17,17]; } // accent: first , else first heading. var accStr = null; var aEl = document.querySelector("a[href], a"); From 5b009981f4b176e81f2dd5162e4b1c4cf69b49b5 Mon Sep 17 00:00:00 2001 From: AnnaXWang <6621137+AnnaXWang@users.noreply.github.com> Date: Mon, 6 Jul 2026 16:19:28 +0000 Subject: [PATCH 05/10] Recolor highlight-segment text with the forced theme MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- lib/docs/overlay.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/lib/docs/overlay.ts b/lib/docs/overlay.ts index eef51e5..81b60ae 100644 --- a/lib/docs/overlay.ts +++ b/lib/docs/overlay.ts @@ -161,7 +161,11 @@ export const OVERLAY_SCRIPT = String.raw` return true; } catch(e){ return false; } } - function isSurface(el, tag){ return tag === "PRE" || tag === "CODE" || ownsBackground(el); } + // A "surface" starts its own background and keeps its authored text. Highlight + // segments are NOT surfaces even though they carry a wash background in light mode — + // their text is document prose and must recolor with the page (else it goes + // dark-on-dark once the wash turns to an underline under jh-dark). + function isSurface(el, tag){ return !el.hasAttribute("data-jh-seg") && (tag === "PRE" || tag === "CODE" || ownsBackground(el)); } // PASS 1 — before any whitening, pin each surface's authored text color inline. A // surface (code block / anything with its own background) often has no color of its // own and relies on inheriting the body's dark text; once we whiten its ancestor @@ -183,14 +187,17 @@ export const OVERLAY_SCRIPT = String.raw` } } // PASS 2 — recolor text of elements sitting ON the page background; skip surfaces - // (and their subtrees), links, media, and painted highlight segments. + // (and their subtrees), links, and media. Highlight segments (data-jh-seg) ARE + // recolored here — they're prose and must follow the page — with an !important that + // beats any authored span color rule; segments inside code sit inside a surface and + // are never reached, so highlighted code text keeps its own color. function whitenPage(el){ var kids = el.children; for (var i = 0; i < kids.length; i++){ var c = kids[i], tag = c.tagName; if (tag === "SCRIPT" || tag === "STYLE") continue; if (isSurface(c, tag)) continue; - if (!FG_SKIP[tag] && !c.hasAttribute("data-jh-seg")) c.classList.add("jh-doc-fg"); + if (!FG_SKIP[tag]) c.classList.add("jh-doc-fg"); whitenPage(c); } } @@ -491,6 +498,10 @@ export const OVERLAY_SCRIPT = String.raw` applyFocusStyles(); reportPositions(); + // Re-apply the forced-theme text coloring: paint just (re)created the highlight + // segments, and a re-paint (reload after a comment, resize) carries no themeMode + // message, so newly wrapped segments would otherwise miss the recolor. + if (forcedScheme) markForcedText(); } // ---- segment interaction (doc → rail focus model) ---- From cc87ac08a0a7f8455ea79987a728cd2870a28e80 Mon Sep 17 00:00:00 2001 From: AnnaXWang <6621137+AnnaXWang@users.noreply.github.com> Date: Mon, 6 Jul 2026 17:12:09 +0000 Subject: [PATCH 06/10] Apply forced theme on every overlay ready and through focus restyles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- app/d/[slug]/CommentsShell.tsx | 9 +++++++++ lib/docs/overlay.ts | 4 +++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/app/d/[slug]/CommentsShell.tsx b/app/d/[slug]/CommentsShell.tsx index 25247a3..5c36372 100644 --- a/app/d/[slug]/CommentsShell.tsx +++ b/app/d/[slug]/CommentsShell.tsx @@ -142,6 +142,10 @@ export default function CommentsShell(props: Props) { // localStorage as a GLOBAL preference (not per-doc). localStorage is // client-only, so we start "auto" for SSR and hydrate on mount. const [mode, setMode] = useState("auto"); + // 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(mode); + modeRef.current = mode; useEffect(() => { try { const saved = localStorage.getItem(THEME_MODE_KEY); @@ -266,6 +270,11 @@ export default function CommentsShell(props: Props) { switch (d.type) { case "jh:ready": setOverlayReady(true); + // Send the forced theme FIRST, so the overlay applies it before it paints + // (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 }); postToOverlay({ type: "jh:anchors", anchors: paintAnchors }); postToOverlay({ type: "jh:reactions", groups: paintReactionGroups, me, avatars }); break; diff --git a/lib/docs/overlay.ts b/lib/docs/overlay.ts index 81b60ae..5124484 100644 --- a/lib/docs/overlay.ts +++ b/lib/docs/overlay.ts @@ -574,7 +574,9 @@ export const OVERLAY_SCRIPT = String.raw` var cover = (el.getAttribute("data-cover")||"").split(",").filter(Boolean); el.classList.remove("jh-hover","jh-focus","jh-dim"); var depth = Math.min(3, cover.length); - el.className = "d"+depth; // reset base depth class + // Rebuild the base class but KEEP jh-doc-fg — otherwise a hover/focus recolor + // strips the forced-theme text color and the segment reverts to dark-on-dark. + el.className = "d"+depth + (el.classList.contains("jh-doc-fg") ? " jh-doc-fg" : ""); if (focusKey){ if (cover.indexOf(focusKey) !== -1) el.classList.add("jh-focus"); else el.classList.add("jh-dim"); From f998c3b6371b00024d02070eff7e9320a66c9e77 Mon Sep 17 00:00:00 2001 From: AnnaXWang <6621137+AnnaXWang@users.noreply.github.com> Date: Mon, 6 Jul 2026 19:45:56 +0000 Subject: [PATCH 07/10] Always render the viewer shell so the theme toggle is universal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- app/d/[slug]/CommentsShell.tsx | 2 +- app/d/[slug]/PlainShell.tsx | 91 ---------------------------------- app/d/[slug]/page.tsx | 24 +++------ lib/docs/theme.ts | 5 +- 4 files changed, 11 insertions(+), 111 deletions(-) delete mode 100644 app/d/[slug]/PlainShell.tsx diff --git a/app/d/[slug]/CommentsShell.tsx b/app/d/[slug]/CommentsShell.tsx index 5c36372..b802653 100644 --- a/app/d/[slug]/CommentsShell.tsx +++ b/app/d/[slug]/CommentsShell.tsx @@ -1340,7 +1340,7 @@ const railCloseStyle: React.CSSProperties = { }; // Viewer chrome bar — variant A (LOCKED 2026-06-13): same weights/colors as the -// page footer. Matches PlainShell's bar so both viewer paths share one chrome. +// page footer. const barStyle: React.CSSProperties = { display: "flex", justifyContent: "space-between", diff --git a/app/d/[slug]/PlainShell.tsx b/app/d/[slug]/PlainShell.tsx deleted file mode 100644 index ce3b668..0000000 --- a/app/d/[slug]/PlainShell.tsx +++ /dev/null @@ -1,91 +0,0 @@ -// Plain viewer shell — the cold path (zero comments AND a viewer who can't -// comment). Thin chrome (title + "made with justhtml.sh") wrapping the sandboxed -// iframe to /d/:slug/raw. No rail, no overlay, no client JS — behaviorally -// identical to the pre-B10 page. Server component; the root layout supplies -// / and the monospace brand. -// -// Adaptive chrome (variant D): there is NO JS here to sample the doc, so the -// server pre-derives a COARSE theme from the stored HTML's unconditional -// html/body background (lib/docs/theme.ts detectServerTheme/buildChromePalette) -// and themes the bar + iframe backdrop at SSR. Light docs (or any ambiguity) get -// `theme === null` → today's EXACT light chrome (#fff / #ccc), byte-identical. - -import { buildChromePalette, type ThemeSample } from "@/lib/docs/theme"; - -const MONO = `ui-monospace, "SF Mono", Menlo, Consolas, "Courier New", monospace`; - -// Viewer chrome bar — variant A (LOCKED 2026-06-13): bold title left, quiet -// "made with justhtml.sh" right, same weights/colors as the page footer. Colors -// read from CSS vars with the literal light values as fallbacks, so an UNSET var -// set (light doc) resolves to today's exact bytes. -const BAR: React.CSSProperties = { - display: "flex", - justifyContent: "space-between", - alignItems: "center", - height: "2.4rem", - padding: "0 1.25rem", - fontFamily: MONO, - fontSize: 13, - borderBottom: "1px solid var(--jh-bar-border, #ccc)", - color: "var(--jh-bar-fg, #111)", - background: "var(--jh-bar-bg, #fff)", -}; - -export default function PlainShell({ - title, - rawSrc, - theme, -}: { - title: string; - rawSrc: string; - theme: ThemeSample | null; -}) { - // Only DARK themes recolor the chrome; light/unknown keep the literal chrome. - const dark = theme && theme.isDark ? theme : null; - const p = dark ? buildChromePalette(dark) : null; - const vars: React.CSSProperties | undefined = p - ? { - ["--jh-bar-bg" as string]: p.barBg, - ["--jh-bar-border" as string]: p.barBorder, - ["--jh-bar-fg" as string]: p.barFg, - ["--jh-bar-muted" as string]: p.barMuted, - ["--jh-stage-bg" as string]: p.stageBg, - } - : undefined; - - return ( -
- -