What this does
The phone's colours are now web's by token, not hand-copied hex values. Every theme family and scheme web has (six families × light and dark) paints the same on the phone, and a token change on web cannot drift from it.
The generator (packages/app-core)
scripts/theme-tokens.mjs reads styles/globals.css and styles/fluid.css, applies the cascade the way the browser does for each data-theme scheme on the root element (every :root / html rule plus the matching [data-theme=…] and :is(…) rules, by specificity then source order; Tailwind's @theme block counts as root; descendants, pseudo-elements, media queries and utilities are not part of the root cascade), substitutes var(), evaluates color-mix() (premultiplied alpha, as the spec says, so mixing with transparent keeps the hue), oklch(), rgb(… / a) and friends with culori, and writes src/lib/theme-tokens.generated.ts: for each scheme, every custom property as #rrggbb, rgba(r, g, b, a) or its text, plus the shadow recipes parsed into layers.
- The file is checked in.
tests/theme-tokens.test.ts(node environment) regenerates in memory and fails when the file is stale, so changing a token inglobals.csswithout regenerating failsci:app-core.pnpm --filter @openmanager/app-core theme-tokensregenerates;theme-tokens:checkis the same check as a script. - The test also pins the semantics: every scheme from
theme-families.tsand no other; novar()or colour function survives; each family's canvas and ink match its swatch; a family override beats the root default by specificity (neutralcanvas#171717, not globals'#141414); light schemes carry opaque pastel washes while dark ones carry translucent ones. src/lib/theme-tokens.tswraps the data (themeTokens(scheme),themeShadows(scheme),colorChannels,withAlpha) and is exported as@openmanager/app-core/lib/theme-tokens;./lib/theme-familiesis exported too, so the phone imports both without the full logic graph (Metro cannot yet resolve that graph's.jsspecifiers — CAL-148). The logic entry re-exports theme-tokens.- New devDependencies for the script only:
postcss,culori. The generated file is prettier-ignored (it is generated).
The phone (apps/mobile)
src/theme/tokens.ts:phoneTheme(scheme)derives aPhoneThemefrom the generated map — the roles the mockups use (canvas, ink, muted, faint, rule, hover, active, surface-2, bubble, action, float, scrim, tint, status colours and washes), glass as the floating surface at a scheme-specific opacity, shadows asboxShadowtext and as layers — andthemeVarsturns it into NativeWind variables.legacyBasiskeeps the first build's token shape so its screens still compile and paint.tailwind.config.js: every colour points at a variable; new role names (bg-canvas,text-ink,bg-hover,bg-glass,text-status-done…) alongside the first build's names.src/theme/appearance.ts+ThemeProvider.tsx: family + mode (light / dark / system) in AsyncStorage under web's key and JSON shape (openmanager-appearance), resolved against the OS scheme when the mode is system, the old mode-only key carried over once, the window background set to the canvas.useTheme()exposes appearance, resolved scheme andPhoneTheme;usePhoneTheme()for imperative styles.- Settings → Appearance gains the six family swatches; Settings → Theme → "Preview every scheme" opens
src/app/theme-preview.tsx, which paints the current scheme and all twelve. - vitest (node) on the appearance and derivation logic:
pnpm --filter @openmanager/mobile test, run in CI through a new rootci:mobilescript (typecheck + tests) in the desktop validation job; the roottestchain includes mobile too. - The first build's session status dot and user bubble now read the selected family (bubble on
basis-chat-user-bgwith no outline, as web paints it); destructive colours come fromcolor-destructive/color-destructive-foregroundrather than copied hex. apps/mobilenow depends on@openmanager/app-core(workspace). Only the two theme modules are imported, so nothing from app-core's UI reaches the bundle.
Not done: the UI font setting (web's Inter / Public Sans switch) — the phone ships Geist and JetBrains Mono as before; a font setting is cheap to add later but is not part of this change.
Reviews
gpt-6.1-sol (high) derived eight tokens by hand for neutral, carbon, claude-light and lovable and matched the generated file throughout, verified the specificity/order behaviour, premultiplied alpha, :is() and font selectors, descendant exclusion, boxShadow on RN 0.86 and NativeWind vars() with rgba, and found three P2s — live consumers (status dot, user bubble) and the destructive constants bypassing the generated roles — all fixed in the second commit. Greptile found the same three plus the migration catch, shadow-color as a shadow, and mobile tests outside CI; all fixed and answered inline.
Verification
| Check | Result |
|---|---|
theme-tokens.mjs --check | up to date |
app-core tests/theme-tokens.test.ts | 7 pass |
| app-core typecheck / lint / full tests | clean / 0 errors / 48 files, 516 tests pass |
| web, desktop typecheck | clean |
| mobile typecheck | clean |
mobile tests/theme.test.ts | 11 pass (also via pnpm run ci:mobile) |
npx expo export --platform android | bundles (11 MB hbc) with app-core's theme modules |
npx expo export --platform web + headless Chrome | /theme-preview renders all 12 schemes, 0 page errors; screenshots in ~/.t3/reviews/mobile-phase1/cal-306/shots/ |
The screenshots show, for example, Neutral dark painting #171717 / #ededed with the violet–blue–amber–pink–green–red status marks, and Claude light on #faf9f5 with the clay tint, exactly as web does.
🤖 Generated with Claude Code
Activity
23[Medium impact] The PR appears safe to merge; all six previous findings are addressed and no new actionable issues were found.
Summary
Shares web's theme colors and shadows with mobile through generated data for all twelve schemes.
- Web’s CSS now produces a shared map of colors and shadows for every theme.
- The phone’s saved appearance now picks a web theme family and color mode.
- Mobile settings now show family swatches and a preview of every scheme.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart LR
CSS["globals.css + fluid.css"] --> Generator["theme-tokens.mjs"]
Generator --> Data["theme-tokens.generated.ts"]
Data --> Phone["phoneTheme"]
Saved["Saved family and mode"] --> Provider["ThemeProvider"]
OS["OS light or dark mode"] --> Provider
Provider --> Phone
Phone --> Vars["themeVars"]
Vars --> Screens["Mobile screens"]
Phone --> Hooks["useTokens / usePhoneTheme"]
Hooks --> ScreensReviews (2) · Last reviewed commit: "Paint the live consumers from the genera..." · Reviewed by Greptile
🚀 Deploying Preview to Cloudflare 🚀
Preview Deployments by commit
View logs ↗
View logs ↗