/** * The Nearle theme for Astryx — PHASE 2. * * Every value here already existed in the codebase; nothing was invented for * aesthetics. The light column is lifted verbatim from the eleven hardcoded * constants at the top of `components/consoleUi.tsx` and from the two status * colour maps beneath them. The dark column is new, because the console has * never had a dark mode and there was nothing to lift. * * Written by hand rather than by `astryx theme`: that CLI command is an * interactive wizard, and a generated file would still have needed every value * below typed into it. * * TOKEN VALUES take one of two forms (see defineTheme's contract): * 'string' — same in both modes * ['light', 'dark'] — compiled to CSS light-dark() * * HOW MODE IS CHOSEN. `theme-neutral` ships no `prefers-color-scheme` block at * all; Astryx switches on a `data-theme` attribute that the `` provider * stamps. So `mode="system"` on the provider is what connects this to the OS * setting — the stylesheet alone will not do it. */ import { defineTheme } from '@astryxdesign/core'; /** * The dark accent is the one value in this file that is not simply lifted, and * it is the one that needs a human's approval. * * `BRAND` #662582 scores 9.76:1 on white — comfortably AA. On Astryx's dark * surface #1F1F22 it is unreadable, and `BRAND_LIGHT` #9255AB only reaches * **3.22:1**, which is below the 4.5:1 threshold. Measured candidates: * * #9255AB 3.22:1 BRAND_LIGHT as-is fails AA * #A96FC4 4.51:1 passes by 0.01 no headroom * #B57FD0 5.41:1 passes with room ← chosen * #C48FDD 6.51:1 passes, drifts pastel * * #B57FD0 is the lightest value that still reads as the Nearle purple rather * than as lilac. **Brand owner to confirm before dark mode ships.** */ const BRAND_DARK = '#B57FD0'; export const nearleTheme = defineTheme({ name: 'nearle', typography: { // Both families are already pulled in by the Google Fonts @import at the // top of index.css, so this re-points Astryx at them rather than adding a // request. Astryx does not load fonts itself — the consumer must. body: { family: 'Public Sans', fallbacks: 'ui-sans-serif, system-ui, sans-serif' }, heading: { family: 'Public Sans', fallbacks: 'ui-sans-serif, system-ui, sans-serif' }, code: { family: 'JetBrains Mono', fallbacks: 'ui-monospace, SFMono-Regular, monospace' }, }, tokens: { // ── Brand ─────────────────────────────────────────────────────────────── '--color-accent': ['#662582', BRAND_DARK], '--color-on-accent': ['#FFFFFF', '#1B1420'], // The alpha suffixes mirror consoleUi's `soft()` helper (18 ≈ 9%), which is // what every tinted brand surface in the app is currently built from. '--color-accent-muted': ['#66258218', `${BRAND_DARK}24`], '--color-text-accent': ['#662582', BRAND_DARK], '--color-icon-accent': ['#662582', BRAND_DARK], // ── Text ── TEXT / TEXT_2 / TEXT_3 ────────────────────────────────────── '--color-text-primary': ['#0f172a', '#ECE6F1'], '--color-text-secondary': ['#64748b', '#B3A8BD'], '--color-text-disabled': ['#94a3b8', '#85798F'], // ── Surfaces ── SURFACE_ALT / DIVIDER ─────────────────────────────────── // The dark greys carry a slight violet bias so they sit under the accent // as a chosen neutral rather than a stock grey. '--color-background-surface': ['#FFFFFF', '#1C1724'], '--color-background-body': ['#f8fafc', '#14101A'], '--color-background-card': ['#FFFFFF', '#1C1724'], '--color-background-popover': ['#FFFFFF', '#241D2E'], '--color-background-muted': ['#f1f5f9', '#241D2E'], // ── Lines ── BORDER / DIVIDER ─────────────────────────────────────────── '--color-border': ['#e2e8f0', '#352C40'], '--color-border-emphasized': ['#cbd5e1', '#4A3E58'], '--color-skeleton': ['#f1f5f9', '#241D2E'], // ── Semantic ──────────────────────────────────────────────────────────── // Taken from ORDER_STATUS / DELIVERY_STATUS so a Badge and a status pill // agree. Dark variants are lightened for the same contrast reason as the // accent — the source values are tuned for white. '--color-success': ['#10b981', '#34D399'], '--color-error': ['#ef4444', '#F87171'], '--color-warning': ['#f59e0b', '#FBBF24'], // ── Elevation ── SHADOW_MD / SOFT / POP ───────────────────────────────── // Dark mode gets its own shadows rather than the same rgba: a shadow tuned // for a white ground is invisible on a dark one, so the alpha is raised. '--shadow-low': ['0 8px 24px rgba(15, 23, 42, 0.08)', '0 8px 24px rgba(0, 0, 0, 0.45)'], '--shadow-med': ['0 14px 40px rgba(15, 23, 42, 0.10)', '0 14px 40px rgba(0, 0, 0, 0.55)'], '--shadow-high': ['0 18px 50px rgba(15, 23, 42, 0.18)', '0 18px 50px rgba(0, 0, 0, 0.65)'], }, }); export default nearleTheme;