Skip to content

Commit fa4e901

Browse files
committed
Extract color scheme logic to shared utilities module
Created website/src/utils/color-scheme.ts with reusable functions for: - Getting/setting color scheme from sessionStorage - Applying color scheme to documents and iframes - Generating inline script for FOUC prevention - Syncing color scheme changes to playground iframes Refactored existing code to use shared utilities: - code-preview.ts: Uses getColorSchemeScript() for iframe initialization - color-scheme-toggle.ts: Uses applyColorScheme() and syncIframes() - root.ts: Uses getColorSchemeScript() for SSR script injection Benefits: - Eliminates code duplication across 3+ files - Consistent color scheme handling everywhere - Single source of truth for color values - Makes future updates easier - Better iframe synchronization with postMessage support 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent bcccb44 commit fa4e901

File tree

4 files changed

+144
-64
lines changed

4 files changed

+144
-64
lines changed

website/src/components/code-preview.ts

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {css} from "@emotion/css";
44
import {debounce} from "../utils/fns.js";
55
import {transform} from "../plugins/babel.js";
66
import {extractData} from "./serialize-javascript.js";
7+
import {getColorSchemeScript} from "../utils/color-scheme.js";
78

89
function generateJavaScriptIFrameHTML(
910
id: number,
@@ -17,27 +18,7 @@ function generateJavaScriptIFrameHTML(
1718
<meta charset="utf-8">
1819
<meta name="viewport" content="width=device-width,initial-scale=1">
1920
<script>
20-
// Detect and apply color scheme immediately
21-
const colorScheme = sessionStorage.getItem("color-scheme") ||
22-
(
23-
window.matchMedia &&
24-
window.matchMedia("(prefers-color-scheme: dark)").matches
25-
? "dark"
26-
: "light"
27-
);
28-
29-
// Set CSS variables as inline styles - these have higher specificity
30-
// than external CSS and will not be overridden
31-
const isDark = colorScheme === "dark";
32-
const bgColor = isDark ? "#0a0e1f" : "#e7f4f5";
33-
const textColor = isDark ? "#f5f9ff" : "#0a0e1f";
34-
35-
document.documentElement.style.setProperty("--bg-color", bgColor);
36-
document.documentElement.style.setProperty("--text-color", textColor);
37-
38-
if (!isDark) {
39-
document.documentElement.classList.add("color-scheme-light");
40-
}
21+
${getColorSchemeScript()}
4122
</script>
4223
<style>
4324
/* Ensure colors cascade to all elements */

website/src/components/color-scheme-toggle.ts

Lines changed: 13 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,18 @@
11
import {jsx} from "@b9g/crank/standalone";
22
import type {Context} from "@b9g/crank/standalone";
3+
import {
4+
getColorScheme,
5+
setColorScheme,
6+
applyColorScheme,
7+
syncIframes,
8+
type ColorScheme,
9+
} from "../utils/color-scheme.js";
310

411
// the website defaults to dark mode
5-
let colorScheme: string | undefined;
12+
let colorScheme: ColorScheme | undefined;
613
if (typeof window !== "undefined") {
7-
colorScheme =
8-
sessionStorage.getItem("color-scheme") ||
9-
(window.matchMedia("(prefers-color-scheme: dark)").matches
10-
? "dark"
11-
: "light");
12-
if (colorScheme === "dark") {
13-
document.body.classList.remove("color-scheme-light");
14-
} else {
15-
document.body.classList.add("color-scheme-light");
16-
}
14+
colorScheme = getColorScheme();
15+
applyColorScheme(colorScheme);
1716
}
1817

1918
// This component would not work with multiple instances, insofar as clicking
@@ -22,30 +21,13 @@ if (typeof window !== "undefined") {
2221
export function ColorSchemeToggle(this: Context) {
2322
const onclick = () => {
2423
colorScheme = colorScheme === "dark" ? "light" : "dark";
25-
sessionStorage.setItem("color-scheme", colorScheme);
24+
setColorScheme(colorScheme);
2625
this.refresh();
2726
};
2827

2928
if (typeof window !== "undefined") {
30-
if (colorScheme === "dark") {
31-
document.body.classList.remove("color-scheme-light");
32-
for (const iframe of Array.from(
33-
document.querySelectorAll(".playground-iframe"),
34-
)) {
35-
(
36-
iframe as HTMLIFrameElement
37-
).contentWindow?.document.body.classList.remove("color-scheme-light");
38-
}
39-
} else {
40-
document.body.classList.add("color-scheme-light");
41-
for (const iframe of Array.from(
42-
document.querySelectorAll(".playground-iframe"),
43-
)) {
44-
(
45-
iframe as HTMLIFrameElement
46-
).contentWindow?.document.body.classList.add("color-scheme-light");
47-
}
48-
}
29+
applyColorScheme(colorScheme!);
30+
syncIframes(colorScheme!);
4931
}
5032

5133
return jsx`

website/src/components/root.ts

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,12 @@ import {extractCritical} from "@emotion/server";
55
import {Page, Link, Script, Storage} from "./esbuild.js";
66
import {Navbar} from "./navbar.js";
77
import {StaticURLsJSON} from "./static-urls-json.js";
8+
import {getColorSchemeScript} from "../utils/color-scheme.js";
89

910
function ColorSchemeScript() {
1011
// This script must be executed as early as possible to prevent a FOUC.
1112
// It also cannot be `type="module"` because that will also cause an FOUC.
12-
const scriptText = `
13-
(() => {
14-
const colorScheme = sessionStorage.getItem("color-scheme") ||
15-
(window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches
16-
? "dark" : "light"
17-
);
18-
if (colorScheme === "dark") {
19-
document.body.classList.remove("color-scheme-light");
20-
} else {
21-
document.body.classList.add("color-scheme-light");
22-
}
23-
})()`;
13+
const scriptText = `(() => { ${getColorSchemeScript()} })()`;
2414
return jsx`
2515
<script>
2616
<${Raw} value=${scriptText} />

website/src/utils/color-scheme.ts

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/**
2+
* Shared color scheme utilities for consistent dark/light mode handling
3+
*/
4+
5+
export type ColorScheme = "dark" | "light";
6+
7+
/**
8+
* Gets the current color scheme from sessionStorage or system preference
9+
*/
10+
export function getColorScheme(): ColorScheme {
11+
if (typeof window === "undefined") {
12+
return "dark"; // SSR default
13+
}
14+
15+
const stored = sessionStorage.getItem("color-scheme");
16+
if (stored === "dark" || stored === "light") {
17+
return stored;
18+
}
19+
20+
// Fall back to system preference
21+
return window.matchMedia &&
22+
window.matchMedia("(prefers-color-scheme: dark)").matches
23+
? "dark"
24+
: "light";
25+
}
26+
27+
/**
28+
* Sets the color scheme in sessionStorage
29+
*/
30+
export function setColorScheme(scheme: ColorScheme): void {
31+
if (typeof window === "undefined") return;
32+
sessionStorage.setItem("color-scheme", scheme);
33+
}
34+
35+
/**
36+
* Applies color scheme classes and CSS variables to a document element
37+
*/
38+
export function applyColorScheme(
39+
scheme: ColorScheme,
40+
target: Document | HTMLElement = document.documentElement,
41+
): void {
42+
const root = target instanceof Document ? target.documentElement : target;
43+
const body =
44+
target instanceof Document ? target.body : target.ownerDocument?.body;
45+
46+
const isDark = scheme === "dark";
47+
const bgColor = isDark ? "#0a0e1f" : "#e7f4f5";
48+
const textColor = isDark ? "#f5f9ff" : "#0a0e1f";
49+
50+
// Apply CSS variables as inline styles for highest specificity
51+
root.style.setProperty("--bg-color", bgColor);
52+
root.style.setProperty("--text-color", textColor);
53+
54+
// Apply classes
55+
if (isDark) {
56+
root.classList.remove("color-scheme-light");
57+
body?.classList.remove("color-scheme-light");
58+
} else {
59+
root.classList.add("color-scheme-light");
60+
body?.classList.add("color-scheme-light");
61+
}
62+
}
63+
64+
/**
65+
* Returns the inline script code for applying color scheme before render
66+
* Use this in server-rendered HTML to prevent FOUC
67+
*/
68+
export function getColorSchemeScript(): string {
69+
return `
70+
const colorScheme = sessionStorage.getItem("color-scheme") ||
71+
(window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches
72+
? "dark" : "light");
73+
74+
const isDark = colorScheme === "dark";
75+
const bgColor = isDark ? "#0a0e1f" : "#e7f4f5";
76+
const textColor = isDark ? "#f5f9ff" : "#0a0e1f";
77+
78+
document.documentElement.style.setProperty("--bg-color", bgColor);
79+
document.documentElement.style.setProperty("--text-color", textColor);
80+
81+
if (!isDark) {
82+
document.documentElement.classList.add("color-scheme-light");
83+
}
84+
`.trim();
85+
}
86+
87+
/**
88+
* Syncs color scheme to all playground iframes via postMessage
89+
*/
90+
export function syncIframes(scheme: ColorScheme): void {
91+
if (typeof window === "undefined") return;
92+
93+
const iframes = document.querySelectorAll<HTMLIFrameElement>(
94+
".playground-iframe",
95+
);
96+
97+
for (const iframe of iframes) {
98+
// Send message to iframe
99+
iframe.contentWindow?.postMessage(
100+
JSON.stringify({type: "color-scheme-change", scheme}),
101+
window.location.origin,
102+
);
103+
104+
// Also apply directly (for iframes that don't have message listener yet)
105+
if (iframe.contentDocument) {
106+
applyColorScheme(scheme, iframe.contentDocument);
107+
}
108+
}
109+
}
110+
111+
/**
112+
* Sets up a message listener in an iframe to receive color scheme updates
113+
*/
114+
export function setupIframeColorSchemeListener(): void {
115+
if (typeof window === "undefined") return;
116+
117+
window.addEventListener("message", (ev) => {
118+
try {
119+
const data = JSON.parse(ev.data);
120+
if (data.type === "color-scheme-change") {
121+
applyColorScheme(data.scheme as ColorScheme);
122+
}
123+
} catch {
124+
// Ignore non-JSON messages
125+
}
126+
});
127+
}

0 commit comments

Comments
 (0)