|
| 1 | +# Theming v2 Design (issue #73) |
| 2 | + |
| 3 | +Design tokens via CSS custom properties, schema-validated theme files, four |
| 4 | +built-in themes, a theme editor, and a documented migration path from the |
| 5 | +`accentColor`-only API. |
| 6 | + |
| 7 | +## Constraints |
| 8 | + |
| 9 | +- **No breaking changes.** The CDN `@1` channel auto-updates production sites. |
| 10 | + `theme: "light" | "dark" | "auto"`, `accentColor`, and the existing |
| 11 | + `--claudius-*` custom properties must keep working unchanged. |
| 12 | +- Default appearance must be pixel-identical before/after the refactor; the |
| 13 | + existing 220 widget tests are the regression net. |
| 14 | + |
| 15 | +## Token set (`--cl-*`) |
| 16 | + |
| 17 | +| Group | Tokens | Notes | |
| 18 | +|-------|--------|-------| |
| 19 | +| Colors | `accent`, `accent-text`, `surface`, `surface-muted`, `text`, `text-muted`, `border`, `user-bubble`, `user-bubble-text`, `assistant-bubble`, `assistant-bubble-text`, `error`, `error-surface`, `scrim` | `user-bubble` defaults to `var(--cl-color-accent)`; `assistant-bubble` to `surface-muted` (fixes the wart where `accentColor` recolored the header but not user bubbles) | |
| 20 | +| Radii | `sm` (8px), `md` (12px), `lg` (16px), `full` (9999px), `tail` (2px) | window/bubbles=lg, buttons/inputs=md, bubble tail=tail | |
| 21 | +| Shadows | `elevated` (window), `floating` (toggle), `floating-hover` | | |
| 22 | +| Fonts | `heading`, `body` | carried over from `--claudius-font-*` | |
| 23 | + |
| 24 | +**Dark mode:** light values are token defaults; `[data-claudius-dark="true"]` |
| 25 | +reassigns each color token to `var(--cl-color-<name>-dark, <current dark value>)`. |
| 26 | +The wrapper splits into an outer div carrying inline theme vars and an inner |
| 27 | +div carrying `data-claudius-dark`, so the attribute rule beats inherited |
| 28 | +inline light values. Today's hard-coded `dark:` utilities become the `-dark` |
| 29 | +defaults, keeping default dark mode pixel-identical. |
| 30 | + |
| 31 | +**Legacy aliases:** Tailwind palette entries become fallback chains, e.g. |
| 32 | +`var(--claudius-primary, var(--cl-color-accent, #2563eb))` — anyone setting |
| 33 | +`--claudius-*` externally keeps working and still wins. |
| 34 | + |
| 35 | +## API |
| 36 | + |
| 37 | +```ts |
| 38 | +type ClaudiusThemeInput = |
| 39 | + | "light" | "dark" | "auto" // existing: mode only |
| 40 | + | "default" | "minimal" | "playful" | "corporate" // built-in themes |
| 41 | + | ClaudiusTheme // inline token object |
| 42 | + | (string & {}); // URL to a theme JSON |
| 43 | + |
| 44 | +interface ClaudiusTheme { |
| 45 | + $schema?: string; |
| 46 | + name?: string; |
| 47 | + colorScheme?: "light" | "dark" | "auto"; // defaults to "light" |
| 48 | + colors?: Partial<Record<ThemeColorToken, string>>; // camelCase keys |
| 49 | + colorsDark?: Partial<Record<ThemeColorToken, string>>; // dark overrides |
| 50 | + radii?: Partial<Record<"sm" | "md" | "lg" | "full" | "tail", string>>; |
| 51 | + shadows?: Partial<Record<"elevated" | "floating" | "floatingHover", string>>; |
| 52 | + fonts?: Partial<Record<"heading" | "body", string>>; |
| 53 | +} |
| 54 | +``` |
| 55 | + |
| 56 | +- Strings `light|dark|auto` keep their exact current meaning (mode only). |
| 57 | +- Built-in names resolve to exported theme objects (`builtinThemes`); each |
| 58 | + carries its own `colorScheme`. Combining a built-in with a different mode: |
| 59 | + `theme={{ ...builtinThemes.corporate, colorScheme: "dark" }}`. |
| 60 | +- Any other string is treated as a URL: fetched, JSON-parsed, structurally |
| 61 | + checked. On fetch/parse/shape failure: console.error and fall back to the |
| 62 | + default theme — the widget never breaks because a theme file is down. |
| 63 | +- `accentColor` still works and **wins over** the theme's accent (it is the |
| 64 | + older, more specific contract). Internally it now sets `--cl-color-accent`. |
| 65 | +- Web component: `theme` attribute accepts mode strings, built-in names, and |
| 66 | + URLs (attributes can't express objects; documented). |
| 67 | +- Package exports gain `ClaudiusTheme` and `builtinThemes`. |
| 68 | + |
| 69 | +## Built-in themes |
| 70 | + |
| 71 | +- `default` — empty overrides (the baked-in defaults) |
| 72 | +- `minimal` — monochrome: near-black accent, square-ish radii (4/6/10px), hairline shadows |
| 73 | +- `playful` — violet accent, pill radii (16/20/24px), soft colored shadows, rounded font stack |
| 74 | +- `corporate` — navy accent, tight radii (4/6/8px), subtle shadows, neutral grays |
| 75 | + |
| 76 | +## Schema |
| 77 | + |
| 78 | +JSON Schema draft-07 (same dialect as `clients/_schema.json`), source of truth |
| 79 | +at `widget/src/theme/theme.v1.schema.json`, committed copy served from |
| 80 | +`docs/public/schema/theme.v1.json` → published at |
| 81 | +`https://claudius-docs.pages.dev/schema/theme.v1.json` (the issue's |
| 82 | +`claudius.dev` host doesn't exist yet; same "or equivalent" precedent as #74 — |
| 83 | +when the domain is attached the path carries over). A widget test asserts the |
| 84 | +two files are byte-identical (drift guard, no cross-package build coupling). |
| 85 | +All leaf values: non-empty strings (CSS color syntax is too varied to regex); |
| 86 | +`additionalProperties: false` everywhere catches typos. Runtime widget |
| 87 | +validation is a lightweight structural check (no AJV in the bundle); AJV is a |
| 88 | +widget devDependency for tests, which validate all four built-ins against the |
| 89 | +schema. |
| 90 | + |
| 91 | +## Theme editor |
| 92 | + |
| 93 | +A custom docs-site page at `/theme-editor/` (Starlight `<StarlightPage>` + |
| 94 | +vanilla JS island): grouped controls for all tokens (light + dark), a live |
| 95 | +preview pane — a static widget replica styled exclusively by the same |
| 96 | +`--cl-*` tokens — and export as download/copy of a JSON file containing only |
| 97 | +non-default values plus `$schema`. Built-ins selectable as starting points. |
| 98 | +The issue places the editor on the playground site, which doesn't exist yet |
| 99 | +(#48/#83); the docs site is its interim home, and the editor moves when the |
| 100 | +playground lands. |
| 101 | + |
| 102 | +## Out of scope |
| 103 | + |
| 104 | +- Playground site itself (#48/#83); per-tenant theme storage; CLI validation |
| 105 | + of theme files (possible follow-up to `pnpm claudius validate`). |
0 commit comments