From d1ae9e8123edaaebe1e71e2819b7baaad677e807 Mon Sep 17 00:00:00 2001 From: core-devops Date: Sat, 13 Jun 2026 02:41:20 -0700 Subject: [PATCH] feat(mobile): mobile palette adopts canvas design SSOT (purple, warm-paper) + drift gate (core#mobile-design-parity) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mobile-web screens used a separate hardcoded palette (MOL_LIGHT/MOL_DARK from a Claude Design handoff) with a GREEN accent (#2f9e6a) + lighter warm-paper surfaces — diverging from the desktop canvas design. Align the CORE tokens (bg/surface/surface2/border/divider/text/text2/text3 + the brand accent) byte-for-byte to the canvas @theme SSOT in src/app/globals.css: canvas warm-paper light + near-black dark surfaces and the PURPLE accent (#7c3aed / #a78bfa). green/online map to the canvas `good`; status + tier badges stay mobile-specific. Adds palette.ssot.test.ts — a drift gate that PARSES globals.css and asserts the mobile core tokens equal the canvas tokens, so the two can't silently re-diverge. Full mobile suite green (247). Co-Authored-By: Claude Fable 5 --- .../mobile/__tests__/palette.ssot.test.ts | 86 +++++++++++++++++++ canvas/src/components/mobile/palette.ts | 65 ++++++++------ 2 files changed, 126 insertions(+), 25 deletions(-) create mode 100644 canvas/src/components/mobile/__tests__/palette.ssot.test.ts diff --git a/canvas/src/components/mobile/__tests__/palette.ssot.test.ts b/canvas/src/components/mobile/__tests__/palette.ssot.test.ts new file mode 100644 index 00000000..84cdca9d --- /dev/null +++ b/canvas/src/components/mobile/__tests__/palette.ssot.test.ts @@ -0,0 +1,86 @@ +// Design-token SSOT drift gate (core#mobile-design-parity). +// +// The mobile palette's CORE tokens must stay equal to the canonical canvas +// @theme tokens in src/app/globals.css — the single source of truth for the +// warm-paper + near-black-dark surfaces and the PURPLE brand accent. This +// test PARSES globals.css (not a second hardcoded copy) and asserts the +// mobile MOL_LIGHT / MOL_DARK core values match, so the two can't silently +// re-diverge (the mobile UI once shipped a green accent + lighter surfaces). +// +// Status/tier badge colors are intentionally mobile-specific and NOT gated +// here — only the core design language (surfaces, text, borders, accent, and +// good→green) is the cross-surface SSOT. + +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; +import { MOL_DARK, MOL_LIGHT } from "../palette"; + +const css = readFileSync( + fileURLToPath(new URL("../../../app/globals.css", import.meta.url)), + "utf8", +); + +// Extract the `--color-: ;` map from the FIRST block opened by +// `selector {` that actually defines --color-surface (globals.css has +// several blocks per selector — e.g. an @theme bg block + a +// [data-theme="dark"] .shadow-embossed rule — that don't hold the palette). +function tokensInBlock(selector: string): Record { + let from = 0; + for (;;) { + const start = css.indexOf(selector + " {", from); + if (start === -1) throw new Error(`palette block not found for: ${selector}`); + const open = css.indexOf("{", start); + const close = css.indexOf("\n}", open); + const body = css.slice(open, close); + if (body.includes("--color-surface")) { + const out: Record = {}; + for (const m of body.matchAll(/--color-([a-z0-9-]+):\s*([^;]+);/g)) { + out[m[1]] = m[2].trim(); + } + return out; + } + from = open + 1; + } +} + +// The warm-paper light palette lives in the first `@theme {` block that +// defines --color-surface; dark values under `[data-theme="dark"] {`. +const light = tokensInBlock("@theme"); +const dark = tokensInBlock('[data-theme="dark"]'); + +describe("mobile palette ↔ canvas @theme SSOT", () => { + it("MOL_LIGHT core tokens equal the canvas light @theme", () => { + expect(MOL_LIGHT.bg).toBe(light["surface"]); + expect(MOL_LIGHT.surface).toBe(light["surface-elevated"]); + expect(MOL_LIGHT.surface2).toBe(light["surface-card"]); + expect(MOL_LIGHT.border).toBe(light["line"]); + expect(MOL_LIGHT.divider).toBe(light["line-soft"]); + expect(MOL_LIGHT.text).toBe(light["ink"]); + expect(MOL_LIGHT.text2).toBe(light["ink-mid"]); + expect(MOL_LIGHT.text3).toBe(light["ink-soft"]); + expect(MOL_LIGHT.accent).toBe(light["accent"]); + expect(MOL_LIGHT.green).toBe(light["good"]); + expect(MOL_LIGHT.online).toBe(light["good"]); + }); + + it("MOL_DARK core tokens equal the canvas dark [data-theme] block", () => { + expect(MOL_DARK.bg).toBe(dark["surface"]); + expect(MOL_DARK.surface).toBe(dark["surface-elevated"]); + expect(MOL_DARK.surface2).toBe(dark["surface-card"]); + expect(MOL_DARK.border).toBe(dark["line"]); + expect(MOL_DARK.divider).toBe(dark["line-soft"]); + expect(MOL_DARK.text).toBe(dark["ink"]); + expect(MOL_DARK.text2).toBe(dark["ink-mid"]); + expect(MOL_DARK.text3).toBe(dark["ink-soft"]); + expect(MOL_DARK.accent).toBe(dark["accent"]); + expect(MOL_DARK.green).toBe(dark["good"]); + expect(MOL_DARK.online).toBe(dark["good"]); + }); + + it("the brand accent is the canvas purple, not the legacy mobile green", () => { + expect(MOL_LIGHT.accent.toLowerCase()).toBe("#7c3aed"); + expect(MOL_DARK.accent.toLowerCase()).toBe("#a78bfa"); + expect(MOL_LIGHT.accent).not.toBe("#2f9e6a"); + }); +}); diff --git a/canvas/src/components/mobile/palette.ts b/canvas/src/components/mobile/palette.ts index b64eb4cf..c38c409e 100644 --- a/canvas/src/components/mobile/palette.ts +++ b/canvas/src/components/mobile/palette.ts @@ -1,7 +1,17 @@ -// Mobile design system tokens — verbatim from the Claude Design handoff -// (molecules-ai-mobile-app/project/shared.jsx). Kept as an inline-style -// palette object so screens can mirror the design 1:1; theming routes -// through `usePalette(dark)` exactly like the prototype. +// Mobile design system tokens. +// +// SSOT (core#mobile-design-parity): the CORE palette — bg/surface/surface2/ +// border/divider/text/text2/text3 + the PURPLE brand accent — is kept in +// sync with the canonical canvas @theme in +// `molecule-core/canvas/src/app/globals.css` (the same app this mobile UI +// ships inside). Earlier this palette shipped a divergent set built from a +// Claude Design handoff (GREEN accent #2f9e6a + lighter warm-paper) — it now +// adopts the canvas warm-paper + near-black-dark surfaces and the purple +// accent so the mobile version has the SAME design as the desktop canvas. +// `palette.ssot.test.ts` asserts these core values equal the canvas tokens; +// `green`/`online` map to the canvas `good`, status/tier badges stay +// mobile-specific. Don't hand-edit the core values to differ from canvas — +// change the canvas SSOT and re-sync. export interface MobilePalette { bg: string; @@ -37,16 +47,19 @@ export interface MobilePalette { } export const MOL_LIGHT: MobilePalette = { - bg: "#f6f4ef", + // Core — canvas @theme light SSOT (surface / surface-elevated / + // surface-card / line / line-soft / ink / ink-mid / ink-soft). + bg: "#f1efe8", surface: "#ffffff", - surface2: "#fbf9f4", - border: "rgba(40,30,20,0.08)", - divider: "rgba(40,30,20,0.06)", - text: "#29261b", - text2: "rgba(41,38,27,0.62)", - text3: "rgba(41,38,27,0.42)", + surface2: "#faf9f4", + border: "#ddd9cf", + divider: "#ebe8df", + text: "#21201b", + text2: "#5c5a52", + text3: "#6f6c62", - green: "#2f9e6a", + // green/online map to the canvas `good` (#0c8a52); soft/ink tints derived. + green: "#0c8a52", greenSoft: "#d9ebe0", greenInk: "#1f6a47", @@ -57,7 +70,7 @@ export const MOL_LIGHT: MobilePalette = { t4SoftCard: "#f9ece0", - online: "#2f9e6a", + online: "#0c8a52", starting: "#e9b53b", degraded: "#d28a2a", failed: "#c8472a", @@ -66,20 +79,22 @@ export const MOL_LIGHT: MobilePalette = { remote: "#7a4dd1", remoteBg: "#ede2ff", - accent: "#2f9e6a", + accent: "#7c3aed", // canvas purple brand (was green #2f9e6a) }; export const MOL_DARK: MobilePalette = { - bg: "#15140f", - surface: "#1d1c17", - surface2: "#22211c", - border: "rgba(255,250,240,0.08)", - divider: "rgba(255,250,240,0.06)", - text: "#f1eee5", - text2: "rgba(241,238,229,0.6)", - text3: "rgba(241,238,229,0.38)", + // Core — canvas @theme dark SSOT (near-black surfaces + bright ink). + bg: "#08080a", + surface: "#16161d", + surface2: "#1b1b23", + border: "#26262e", + divider: "#1b1b22", + text: "#ececf1", + text2: "#9b9baa", + text3: "#65656f", - green: "#3eb37c", + // green/online map to the canvas dark `good` (#34d399). + green: "#34d399", greenSoft: "#1f3a2c", greenInk: "#7fd3a8", @@ -90,7 +105,7 @@ export const MOL_DARK: MobilePalette = { t4SoftCard: "#2a1f17", - online: "#3eb37c", + online: "#34d399", starting: "#e9b53b", degraded: "#d28a2a", failed: "#d65a3e", @@ -99,7 +114,7 @@ export const MOL_DARK: MobilePalette = { remote: "#a38aff", remoteBg: "#2a1f44", - accent: "#3eb37c", + accent: "#a78bfa", // canvas dark purple brand (was green #3eb37c) }; /** -- 2.52.0