From 0425e29004edd1f6408cf01d40e4358d6790eed8 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Wed, 13 May 2026 09:27:21 -0700 Subject: [PATCH 01/17] docs: cockpit dark mode token system design Spec for cockpit-only dark mode: dark by default with light toggle, typed `cssVars(theme)` resolution, cookie source of truth, per-frame postMessage sync for embedded iframes, paired OG image flip. Co-Authored-By: Claude Opus 4.7 --- .../2026-05-13-cockpit-dark-mode-design.md | 250 ++++++++++++++++++ 1 file changed, 250 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-13-cockpit-dark-mode-design.md diff --git a/docs/superpowers/specs/2026-05-13-cockpit-dark-mode-design.md b/docs/superpowers/specs/2026-05-13-cockpit-dark-mode-design.md new file mode 100644 index 00000000..d86675ae --- /dev/null +++ b/docs/superpowers/specs/2026-05-13-cockpit-dark-mode-design.md @@ -0,0 +1,250 @@ +# Cockpit Dark Mode Token System — Design + +**Date:** 2026-05-13 +**Scope:** Cockpit app only (marketing site, docs, emails stay light) +**Status:** Spec — pending implementation plan + +## Goal + +Give cockpit a dark default with a light toggle, driven by typed tokens that flow through the existing `cssVars` bridge. Keep the embedded cockpit-example iframe visually consistent with its host by syncing theme across the frame boundary. Flip the cockpit OG image to match the new default. + +Non-goals: +- Dark mode for marketing site, docs, or email renderings +- A re-themed code editor (tokyo-night dark stays in both themes by design) +- Theming run-mode iframes (user's own app preview — out of scope) +- A `⌘K` command palette entry for theme (no command palette in cockpit yet) + +## Decisions + +| # | Decision | Choice | +|---|---|---| +| 1 | Scope | Cockpit only | +| 2 | Mechanism | Dark by default, light toggle (devtools convention) | +| 3 | Palette family | Brand-blue undertone (`#0e1117`/`#161b25`/`#23293a`) | +| 4 | Token architecture | Themes resolved in TypeScript via `cssVars(theme)`, not CSS cascade | +| 5 | Persistence | `theme` cookie source of truth + `useOptimistic` for instant client feel | +| 6 | Toggle placement | Sidebar footer in `cockpit-shell.tsx` | +| 7 | Coverage | Chrome + chat/agent panels; run-mode iframes untouched; code editor stays tokyo-night | +| 8 | Iframe sync | `` component owns its iframe + postMessage lifecycle (per-frame, not global) | +| 9 | Token diff | Shared `baseTokens` + small `lightOverrides` / `darkOverrides` | +| 10 | OG image | Flip cockpit OG to dark in the same PR | + +## Architecture + +**Cookie is the source of truth.** Cockpit's root layout reads `theme` cookie server-side via `next/headers`. The layout passes the resolved theme into `cssVars(theme)`, which produces a CSS-variable object inlined onto `` via the `style` prop. This means: + +- First paint ships with the correct variables — no FOUC, no blocking pre-hydration script +- Satori OG renderers (server-side) read from `cssVars('dark')` and stay consistent with the live app +- Toggle flow: `useOptimistic` flips client state instantly → POST to `/api/theme` writes cookie → `router.refresh()` reconciles server + +**`useOptimistic` for toggle feel.** Server round-trip on toggle would feel laggy in a devtool. The toggle component flips `` and re-applies `cssVars(next)` to `document.documentElement.style` synchronously, then writes the cookie and refreshes RSC in the background. The optimistic state and refreshed state converge to the same value. + +**Iframe sync via per-frame component, not global broadcast.** A `` component in `@ngaf/ui-react` wraps `