forked from molecule-ai/molecule-core
Brings the canvas onto the warm-paper design system already shipped to landing, marketplace, and SaaS surfaces, and migrates the build from Tailwind v3 → v4 to match molecule-app. Plumbing: - swap tailwindcss v3 → v4, drop autoprefixer, add @tailwindcss/postcss - delete tailwind.config.ts (v4 reads tokens from @theme blocks in CSS) - globals.css: @import "tailwindcss" + @plugin "@tailwindcss/typography" - two @theme blocks: warm-paper light defaults + always-dark surface tokens (bg-bg / ink-mute / line-strong) for terminal/console panels - [data-theme="dark"] cascade overrides the warm-paper tokens for dark - React Flow edge stroke + scrollbar + selection colour pull from semantic tokens so they flip with the theme Theme infra (ported from molecule-app, identical contracts): - lib/theme-cookie.ts: mol_theme cookie + boot script (no "use client" so server components can read the constants) - lib/theme-provider.tsx: ThemeProvider + useTheme + cookie writer with Domain=.moleculesai.app so the preference follows the user across canvas/app/market/landing subdomains AND tenant subdomains - lib/theme.ts: ColorToken union + cssVar() helper - components/ThemeToggle.tsx: 3-way System/Light/Dark picker - layout.tsx: SSR cookie read + nonce'd inline boot script (CSP needs the explicit nonce — strict-dynamic doesn't forgive an un-nonce'd inline sibling) + ThemeProvider wrapper + bg-surface/text-ink body Component migration (62 files): - Mechanical bg-zinc-* / text-zinc-* / border-zinc-* / text-white → semantic surface/ink/line tokens via perl negative-lookahead pass (preserves opacity modifiers like /80, /60) - bg-blue-500/600 → bg-accent / bg-accent-strong - text-red-* / amber-* / emerald-* → text-bad / warm / good - Tinted-state banner backgrounds (bg-red-950, bg-amber-950, bg-blue-950 etc.) intentionally left literal — they remain readable on warm-paper in light mode without inventing new state-soft tokens - TerminalTab.tsx skipped — xterm renders to canvas, not DOM - 3 unit-test assertions updated to match new token strings (credits pillTone, AuthGate overlay class, A2AEdge accent) Verification: - pnpm test: 1214/1214 pass - pnpm tsc --noEmit: clean - next build: ✓ Compiled successfully (8 routes) - dev server inspection: html data-theme stamped, body uses bg-surface text-ink, boot script carries nonce, compiled CSS contains both @theme blocks + [data-theme="dark"] override Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
41 lines
1.8 KiB
TypeScript
41 lines
1.8 KiB
TypeScript
/**
|
|
* Theme cookie constants + boot script.
|
|
*
|
|
* No "use client" pragma — these are imported by both server components
|
|
* (app/layout.tsx, which calls cookies() during SSR) and client
|
|
* components (lib/theme-provider.tsx). Constants exported from a
|
|
* "use client" file get rewritten by Next.js as client-reference
|
|
* placeholders, so a server importer sees a Function instead of the
|
|
* underlying value. Keeping shared primitives here avoids that trap.
|
|
*
|
|
* Aligned with molecule-app's matching module — same cookie name, same
|
|
* three-value enum — so the preference follows the user across surfaces
|
|
* (app, market, landing, canvas) when the cookie is set with
|
|
* Domain=.moleculesai.app.
|
|
*/
|
|
|
|
export type ThemePreference = "system" | "light" | "dark";
|
|
export type ResolvedTheme = "light" | "dark";
|
|
|
|
export const THEME_COOKIE = "mol_theme";
|
|
|
|
export function readThemeCookie(value: string | undefined): ThemePreference {
|
|
if (value === "light" || value === "dark" || value === "system") {
|
|
return value;
|
|
}
|
|
return "system";
|
|
}
|
|
|
|
/**
|
|
* Inline boot script. Stringified verbatim by app/layout.tsx so it runs
|
|
* synchronously before the body paints — preventing a flash of the wrong
|
|
* theme. Reads cookie via document.cookie regex (no parser available
|
|
* yet), falls back to matchMedia, and stamps data-theme on <html>.
|
|
*
|
|
* Must remain tiny and dependency-free — runs before hydration. The
|
|
* canvas's middleware sets a strict CSP with nonce-based script-src in
|
|
* production; the layout passes the nonce on the <script> tag so this
|
|
* passes the inline-script gate.
|
|
*/
|
|
export const themeBootScript = `(()=>{try{var m=document.cookie.match(/(?:^|;\\s*)${THEME_COOKIE}=(system|light|dark)/);var p=m?m[1]:"system";var r=p==="system"?(window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"):p;document.documentElement.dataset.theme=r;}catch(e){}})();`;
|