@app-studio/components 0.10.17 → 0.10.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +41 -0
- package/dist/native/components/Alert/Alert/Alert.style.js +20 -20
- package/dist/native/components/Alert/Alert/Alert.style.js.map +1 -1
- package/dist/native/components/Alert/Alert/Alert.view.native.js.map +1 -1
- package/dist/native/components/AudioInput/AudioInput/AudioInput.view.js +7 -4
- package/dist/native/components/AudioInput/AudioInput/AudioInput.view.js.map +1 -1
- package/dist/native/components/Background/Background/Background.view.native.js.map +1 -1
- package/dist/native/components/Badge/Badge/Badge.style.js +7 -11
- package/dist/native/components/Badge/Badge/Badge.style.js.map +1 -1
- package/dist/native/components/Badge/Badge/Badge.view.js +2 -6
- package/dist/native/components/Badge/Badge/Badge.view.js.map +1 -1
- package/dist/native/components/Button/Button/Button.view.js +2 -2
- package/dist/native/components/Button/Button/Button.view.js.map +1 -1
- package/dist/native/components/Button/Button/Button.view.native.js.map +1 -1
- package/dist/native/components/Card/Card/Card.style.js +10 -10
- package/dist/native/components/Card/Card/Card.style.js.map +1 -1
- package/dist/native/components/Carousel/Carousel/Carousel.view.native.js +10 -6
- package/dist/native/components/Carousel/Carousel/Carousel.view.native.js.map +1 -1
- package/dist/native/components/Chart/Chart/Chart.style.js +3 -3
- package/dist/native/components/Chart/Chart/Chart.style.js.map +1 -1
- package/dist/native/components/ChatInput/ChatInput/ChatInput.style.js +1 -1
- package/dist/native/components/ChatInput/ChatInput/ChatInput.style.js.map +1 -1
- package/dist/native/components/ChatWidget/ChatWidget/ChatWidget.style.js +16 -16
- package/dist/native/components/ChatWidget/ChatWidget/ChatWidget.style.js.map +1 -1
- package/dist/native/components/ChatWidget/ChatWidget/ChatWidget.view.js +2 -2
- package/dist/native/components/ChatWidget/ChatWidget/ChatWidget.view.js.map +1 -1
- package/dist/native/components/ColorPicker/ColorPicker/ColorPicker.style.js +6 -6
- package/dist/native/components/ColorPicker/ColorPicker/ColorPicker.style.js.map +1 -1
- package/dist/native/components/ColorPicker/ColorPicker/ColorPicker.view.js +1 -1
- package/dist/native/components/ColorPicker/ColorPicker/ColorPicker.view.js.map +1 -1
- package/dist/native/components/ContextMenu/ContextMenu/ContextMenu.view.native.js +4 -4
- package/dist/native/components/ContextMenu/ContextMenu/ContextMenu.view.native.js.map +1 -1
- package/dist/native/components/CookieConsent/CookieConsent/CookieConsent.style.js +18 -20
- package/dist/native/components/CookieConsent/CookieConsent/CookieConsent.style.js.map +1 -1
- package/dist/native/components/DropZone/DropZone/DropZone.view.js +3 -3
- package/dist/native/components/DropZone/DropZone/DropZone.view.js.map +1 -1
- package/dist/native/components/DropdownMenu/DropdownMenu/DropdownMenu.view.native.js +4 -4
- package/dist/native/components/DropdownMenu/DropdownMenu/DropdownMenu.view.native.js.map +1 -1
- package/dist/native/components/EditComponent/EditPanel.js.map +1 -1
- package/dist/native/components/EmojiPicker/EmojiPicker/EmojiPicker.view.js +3 -3
- package/dist/native/components/EmojiPicker/EmojiPicker/EmojiPicker.view.js.map +1 -1
- package/dist/native/components/Form/Checkbox/Checkbox/Checkbox.view.native.js +1 -5
- package/dist/native/components/Form/Checkbox/Checkbox/Checkbox.view.native.js.map +1 -1
- package/dist/native/components/Form/CountryPicker/CountryPicker/CountryPicker.view.js +2 -8
- package/dist/native/components/Form/CountryPicker/CountryPicker/CountryPicker.view.js.map +1 -1
- package/dist/native/components/Form/DatePicker/DatePicker/DatePicker.view.js +1 -7
- package/dist/native/components/Form/DatePicker/DatePicker/DatePicker.view.js.map +1 -1
- package/dist/native/components/Form/Radio/Radio/Radio.view.native.js +1 -5
- package/dist/native/components/Form/Radio/Radio/Radio.view.native.js.map +1 -1
- package/dist/native/components/Form/Select/Select/Select.view.native.js +3 -9
- package/dist/native/components/Form/Select/Select/Select.view.native.js.map +1 -1
- package/dist/native/components/Form/TextArea/TextArea/TextArea.view.native.js.map +1 -1
- package/dist/native/components/Form/TextField/TextField/TextField.view.native.js.map +1 -1
- package/dist/native/components/Gradient/Gradient/Gradient.view.native.js +2 -6
- package/dist/native/components/Gradient/Gradient/Gradient.view.native.js.map +1 -1
- package/dist/native/components/HoverCard/HoverCard/HoverCard.view.js +1 -1
- package/dist/native/components/HoverCard/HoverCard/HoverCard.view.js.map +1 -1
- package/dist/native/components/HoverCard/HoverCard/HoverCard.view.native.js.map +1 -1
- package/dist/native/components/Icon/Icon.js.map +1 -1
- package/dist/native/components/Icon/Icon.native.js.map +1 -1
- package/dist/native/components/IconPicker/IconPicker/IconPicker.view.js +2 -2
- package/dist/native/components/IconPicker/IconPicker/IconPicker.view.js.map +1 -1
- package/dist/native/components/Input/FieldLayout/FieldLayout.js +1 -1
- package/dist/native/components/Input/FieldLayout/FieldLayout.js.map +1 -1
- package/dist/native/components/Link/Link/Link.view.native.js +1 -3
- package/dist/native/components/Link/Link/Link.view.native.js.map +1 -1
- package/dist/native/components/Loader/Loader/Loader.view.native.js +4 -4
- package/dist/native/components/Loader/Loader/Loader.view.native.js.map +1 -1
- package/dist/native/components/Menubar/Menubar/Menubar.view.native.js +2 -2
- package/dist/native/components/Menubar/Menubar/Menubar.view.native.js.map +1 -1
- package/dist/native/components/Message/Message/Message.view.js +1 -1
- package/dist/native/components/Message/Message/Message.view.js.map +1 -1
- package/dist/native/components/Modal/Modal/Modal.style.js +1 -1
- package/dist/native/components/Modal/Modal/Modal.style.js.map +1 -1
- package/dist/native/components/NavigationMenu/NavigationMenu/NavigationMenu.view.native.js +2 -2
- package/dist/native/components/NavigationMenu/NavigationMenu/NavigationMenu.view.native.js.map +1 -1
- package/dist/native/components/OTPInput/OTPInput/OTPInput.view.native.js +18 -2
- package/dist/native/components/OTPInput/OTPInput/OTPInput.view.native.js.map +1 -1
- package/dist/native/components/ProgressBar/ProgressBar/ProgressBar.view.js +2 -2
- package/dist/native/components/ProgressBar/ProgressBar/ProgressBar.view.js.map +1 -1
- package/dist/native/components/ProgressBar/ProgressBar/ProgressBar.view.native.js +6 -3
- package/dist/native/components/ProgressBar/ProgressBar/ProgressBar.view.native.js.map +1 -1
- package/dist/native/components/Resizable/Resizable/Resizable.view.native.js +9 -1
- package/dist/native/components/Resizable/Resizable/Resizable.view.native.js.map +1 -1
- package/dist/native/components/Sidebar/Sidebar/Sidebar.style.js +11 -11
- package/dist/native/components/Sidebar/Sidebar/Sidebar.style.js.map +1 -1
- package/dist/native/components/Sidebar/Sidebar/Sidebar.view.native.js.map +1 -1
- package/dist/native/components/Slider/Slider/Slider.view.native.js +3 -1
- package/dist/native/components/Slider/Slider/Slider.view.native.js.map +1 -1
- package/dist/native/components/Tabs/Tabs/Tabs.view.js +10 -10
- package/dist/native/components/Tabs/Tabs/Tabs.view.js.map +1 -1
- package/dist/native/components/Tabs/Tabs/Tabs.view.native.js +2 -2
- package/dist/native/components/Tabs/Tabs/Tabs.view.native.js.map +1 -1
- package/dist/native/components/Title/Title/Title.view.native.js.map +1 -1
- package/dist/native/components/Title/Title/TypewriterEffect.native.js +1 -3
- package/dist/native/components/Title/Title/TypewriterEffect.native.js.map +1 -1
- package/dist/native/components/Toast/Toast/Toast.view.native.js.map +1 -1
- package/dist/native/components/Toggle/Toggle/Toggle.style.js +7 -7
- package/dist/native/components/Toggle/Toggle/Toggle.style.js.map +1 -1
- package/dist/native/components/Toggle/Toggle/Toggle.view.js +2 -2
- package/dist/native/components/Toggle/Toggle/Toggle.view.js.map +1 -1
- package/dist/native/components/Toggle/Toggle/Toggle.view.native.js +3 -1
- package/dist/native/components/Toggle/Toggle/Toggle.view.native.js.map +1 -1
- package/dist/native/components/Uploader/Uploader/Uploader.view.js +2 -2
- package/dist/native/design-system/DesignSystemProvider.d.ts +8 -1
- package/dist/native/design-system/DesignSystemProvider.js +10 -4
- package/dist/native/design-system/DesignSystemProvider.js.map +1 -1
- package/dist/native/design-system/configs/airbnb.json +87 -87
- package/dist/native/design-system/configs/apple.json +89 -89
- package/dist/native/design-system/configs/coinbase.json +89 -89
- package/dist/native/design-system/configs/default.json +556 -0
- package/dist/native/design-system/configs/figma.json +63 -63
- package/dist/native/design-system/configs/index.d.ts +6 -0
- package/dist/native/design-system/configs/index.js +8 -1
- package/dist/native/design-system/configs/index.js.map +1 -1
- package/dist/native/design-system/configs/linear.json +86 -86
- package/dist/native/design-system/configs/nike.json +60 -60
- package/dist/native/design-system/configs/notion.json +88 -88
- package/dist/native/design-system/configs/revolut.json +89 -89
- package/dist/native/design-system/configs/shopify.json +76 -76
- package/dist/native/design-system/configs/spacex.json +68 -68
- package/dist/native/design-system/configs/spotify.json +89 -89
- package/dist/native/design-system/configs/stripe.json +88 -88
- package/dist/native/design-system/configs/tesla.json +75 -75
- package/dist/native/design-system/configs/uber.json +41 -41
- package/dist/native/design-system/configs/vercel.json +65 -65
- package/dist/native/design-system/types.d.ts +28 -0
- package/dist/native/pages/designSystem.page.js +103 -85
- package/dist/native/pages/designSystem.page.js.map +1 -1
- package/dist/web.cjs.js +8 -8
- package/dist/web.cjs.js.map +1 -1
- package/dist/web.esm.js +11786 -10987
- package/dist/web.esm.js.map +1 -1
- package/dist/web.umd.js +10 -10
- package/dist/web.umd.js.map +1 -1
- package/docs/README.md +1 -0
- package/docs/app-studio/Theming.md +7 -0
- package/docs/design-system/component-library.md +78 -42
- package/docs/design-system/llm-design-system.md +426 -0
- package/docs/design-system/theming.md +73 -13
- package/package.json +4 -4
package/docs/README.md
CHANGED
|
@@ -12,6 +12,7 @@ build with the same public API.
|
|
|
12
12
|
- **[Component Development](./component-development/guide.md)** — Building and contributing components (including `.native.tsx` siblings)
|
|
13
13
|
- **[API Integration](./api-integration.md)** — Backend integration patterns
|
|
14
14
|
- **[Design System](./design-system/theming.md)** — Theming and styling guidelines
|
|
15
|
+
- **[Generating a Design-System Config](./design-system/design-system.md)** — Step-by-step recipe (for agents) to author a new brand config that works in light + dark
|
|
15
16
|
|
|
16
17
|
### Specialized Documentation
|
|
17
18
|
- **[ADK Components](../README-ADK.md)** — Agent Development Kit integration
|
|
@@ -487,3 +487,10 @@ Each palette has these shades: `50, 100, 200, 300, 400, 500, 600, 700, 800, 900`
|
|
|
487
487
|
- **Dark Mode**: Uses `defaultDarkPalette` and `defaultDarkColors`
|
|
488
488
|
- Colors automatically switch based on `themeMode` unless using `light.*` or `dark.*` prefix
|
|
489
489
|
- Color values are inverted for dark mode (e.g., `color-white` becomes black in dark mode)
|
|
490
|
+
|
|
491
|
+
### Making a theme slot adaptive vs constant
|
|
492
|
+
The value you assign to a theme slot decides whether it follows the mode or stays fixed:
|
|
493
|
+
- **Constant** — a literal value (`primary: '#2563eb'`) is emitted as-is and never changes between modes. Use for brand identity.
|
|
494
|
+
- **Adaptive** — a token reference (`text: 'color-black'`, `canvas: 'color-white'`) is emitted as `var(--color-…)` and **flips with the mode** (`color-black` → white in dark). Use for structural neutrals.
|
|
495
|
+
|
|
496
|
+
This is the foundation of the design-system layer's "one config, both modes" rule — see [docs/design-system/theming.md §2.1](../design-system/theming.md#21-one-config-both-modes--the-adaptive-rule).
|
|
@@ -105,21 +105,31 @@ Switching brand at runtime is just a state update of the `config` (or `configId`
|
|
|
105
105
|
### 3.2 `theme` (11 semantic colors — the single source of truth)
|
|
106
106
|
```jsonc
|
|
107
107
|
{
|
|
108
|
+
// Brand identity — raw hex → stays constant in light AND dark
|
|
108
109
|
"primary": "#ff385c",
|
|
109
|
-
"secondary": "#
|
|
110
|
-
"success": "#
|
|
111
|
-
"warning": "#
|
|
112
|
-
"error": "#
|
|
113
|
-
"
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
"
|
|
117
|
-
"
|
|
118
|
-
"
|
|
110
|
+
"secondary": "#7c3aed",
|
|
111
|
+
"success": "#16a34a",
|
|
112
|
+
"warning": "#d97706",
|
|
113
|
+
"error": "#dc2626",
|
|
114
|
+
"onPrimary": "#ffffff",
|
|
115
|
+
|
|
116
|
+
// Structural neutrals — color-* token → flips automatically with the mode
|
|
117
|
+
"canvas": "color-white", // white in light, black in dark
|
|
118
|
+
"surface": "color-gray-50", // near-white in light, near-black in dark
|
|
119
|
+
"text": "color-black", // black in light, white in dark
|
|
120
|
+
"muted": "color-gray-500", // readable mid-grey in both
|
|
121
|
+
"border": "color-gray-200" // light hairline → dark hairline
|
|
119
122
|
}
|
|
120
123
|
```
|
|
121
124
|
|
|
122
|
-
`theme` is the **only** place
|
|
125
|
+
`theme` is the **only** place these eleven roles are defined. Everything else — `tokens.colors[]`, `components.*.views`, page-level inline styles — references them via `theme-*` token strings. Change one value here and every button, badge, focus ring, link, and surface updates across the design-system page.
|
|
126
|
+
|
|
127
|
+
The **form of each value** decides its light/dark behaviour — this is the heart of the adaptive system ([theming.md §2.1](./theming.md#21-one-config-both-modes--the-adaptive-rule)):
|
|
128
|
+
|
|
129
|
+
- **Raw hex** (`"#ff385c"`) → constant across modes. Use for brand slots: `primary`, `secondary`, `success`, `warning`, `error`, `onPrimary`.
|
|
130
|
+
- **`color-*` token** (`"color-black"`) → flips automatically. Use for structural neutrals: `canvas`, `surface`, `text`, `muted`, `border`.
|
|
131
|
+
|
|
132
|
+
This is why one config serves both modes: the brand colours stay put while the neutrals invert. Do **not** hand-author a second dark config, and do **not** put a constant dark hex in `text`/`canvas` — that's what breaks a "light" brand the moment the user toggles to dark.
|
|
123
133
|
|
|
124
134
|
### 3.3 `tokens` (raw extracted material)
|
|
125
135
|
```jsonc
|
|
@@ -486,6 +496,11 @@ Use these inside bespoke page-level compositions (brand snapshots, tier-pricing
|
|
|
486
496
|
|
|
487
497
|
## 6. Authoring a config for a new brand
|
|
488
498
|
|
|
499
|
+
> **For a focused, copy-paste recipe (especially for AI agents), use the
|
|
500
|
+
> dedicated guide: [design-system.md](./design-system.md).** It walks the five
|
|
501
|
+
> sections in order with the coherence rules, a validation checklist, and an
|
|
502
|
+
> anti-pattern table. The summary below remains the quick reference.
|
|
503
|
+
|
|
489
504
|
A config is a JSON file under [src/design-system/configs/](../../src/design-system/configs/). To add `mybrand`:
|
|
490
505
|
|
|
491
506
|
1. **Create `mybrand.json`** with all five sections (`metadata`, `theme`, `tokens`, `components`, `personality`). Use an existing config as a template — `airbnb.json` (light) or `linear.json` (dark) are the cleanest references.
|
|
@@ -618,9 +633,9 @@ If a brand source HTML/page exists ([§9.10](#910-when-a-source-html-exists)), e
|
|
|
618
633
|
|
|
619
634
|
### 9.2 Decision flow (the 7 ordered steps)
|
|
620
635
|
|
|
621
|
-
1. **Pick the primary hex.** One color. Everything else is derived.
|
|
622
|
-
2. **Pick appearance
|
|
623
|
-
3. **Build the 11 theme slots** following [§9.3](#93-theme-palette-recipe).
|
|
636
|
+
1. **Pick the primary hex.** One color. Everything else is derived. It must read on **both** a white and a black canvas (it stays constant across modes — see step 3).
|
|
637
|
+
2. **Pick the default appearance** (`light` or `dark`). This only sets which mode the config *opens in* — you do **not** hand-pick a canvas/text hex per mode; the neutrals adapt automatically.
|
|
638
|
+
3. **Build the 11 theme slots** following [§9.3](#93-theme-palette-recipe): brand slots as **hex** (constant), neutral slots (`canvas`, `surface`, `text`, `muted`, `border`) as **`color-*` tokens** (auto-flip). One config, both modes.
|
|
624
639
|
4. **Pick the typography fingerprint** from [§9.4](#94-typography-fingerprints) and assemble `tokens.typography` + `metadata.googleFontLinks`.
|
|
625
640
|
5. **Pick the personality archetype** from [§9.5](#95-personality-archetypes); copy its block; tweak `signatureMotif` and `voice` to fit the brand.
|
|
626
641
|
6. **Fill `components.*` defaults** following the component patterns in [§9.7](#97-component-config-patterns). Use `theme-*` tokens, **never hex**.
|
|
@@ -628,26 +643,38 @@ If a brand source HTML/page exists ([§9.10](#910-when-a-source-html-exists)), e
|
|
|
628
643
|
|
|
629
644
|
### 9.3 Theme palette recipe (the 11 slots)
|
|
630
645
|
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
|
636
|
-
|
|
637
|
-
| `
|
|
638
|
-
| `
|
|
639
|
-
| `
|
|
640
|
-
| `
|
|
641
|
-
| `
|
|
642
|
-
| `
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
646
|
+
Each slot is either **constant** (a raw `hex` — brand identity, identical in light and dark) or **adaptive** (a `color-*` token — flips automatically with the mode). Author both groups in one config and the dark theme is derived for free ([theming.md §2.1](./theming.md#21-one-config-both-modes--the-adaptive-rule)). **Do not author a second dark config.**
|
|
647
|
+
|
|
648
|
+
**Brand slots — write a `hex`, stays constant in both modes:**
|
|
649
|
+
|
|
650
|
+
| Slot | What it paints | Construction rule |
|
|
651
|
+
|-------------|--------------------------------------------------|----------------------------------------------------------------------------------------------------------------|
|
|
652
|
+
| `primary` | CTA backgrounds, links, focus rings, active tabs | The signature hex. Must clear **≥ 3:1 on both white and black** — it never flips, so it has to read in dark too. |
|
|
653
|
+
| `secondary` | Secondary CTAs, hovers, accent emphasis | A *related* hue (analogous or lighter tint of primary), not a clash. **Not body ink** — ink is `text`. |
|
|
654
|
+
| `success` | Positive / "operational" states | Green family that survives next to `primary` without vibrating. ~`#16a34a`–`#1DB954`. |
|
|
655
|
+
| `warning` | Cautions / "degraded" states | Amber/orange (`#d97706`-ish); some brands repurpose a brand hue (Linear uses a desaturated indigo). |
|
|
656
|
+
| `error` | Destructive / "outage" states | Red family (`#dc2626`–`#ef4444`). Coinbase / Stripe / Airbnb tune this toward their primary's temperature. |
|
|
657
|
+
| `onPrimary` | Ink *on top of* `primary` (filled-button label) | Max contrast vs `primary`. ~99% use `#ffffff`; `#000` only when `primary` is light (yellow, mint, pale gold). |
|
|
658
|
+
|
|
659
|
+
**Neutral slots — write a `color-*` token, flips with the mode:**
|
|
660
|
+
|
|
661
|
+
| Slot | What it paints | Recommended token → what it becomes in dark |
|
|
662
|
+
|-----------|-----------------------------------------|-----------------------------------------------------------------------------------------|
|
|
663
|
+
| `canvas` | Page background | `color-white` → black. (Want off-white instead of pure white? `color-gray-50`.) |
|
|
664
|
+
| `surface` | Cards, popovers, raised regions | `color-gray-50` (or `color-gray-100`) → near-black. Must stay distinct from `canvas`. |
|
|
665
|
+
| `text` | Body copy | `color-black` → white. (Softer ink: `color-gray-900`.) |
|
|
666
|
+
| `muted` | Secondary copy, captions, placeholders | `color-gray-500` → a readable mid-grey in both modes. |
|
|
667
|
+
| `border` | Dividers, outlines, input borders | `color-gray-200` → dark hairline. (Stronger: `color-gray-300`.) |
|
|
668
|
+
|
|
669
|
+
> **Tint the neutrals for brand character** while keeping auto-flip: swap the grey family — `color-slate-*` (cool), `color-stone-*` (warm), `color-zinc-*`, `color-neutral-*`. Pick **one** family and use it for all five neutral slots so they stay coherent.
|
|
670
|
+
|
|
671
|
+
> **Contrast invariants** to verify in **both** light and dark (the default `color-gray-*` ramp passes these by construction — re-check only if you hand-pick shades or tint the family):
|
|
672
|
+
> - `text` vs `canvas`: WCAG AA (≥ 4.5:1).
|
|
647
673
|
> - `onPrimary` vs `primary`: WCAG AA (≥ 4.5:1).
|
|
648
|
-
> - `
|
|
649
|
-
> - `
|
|
650
|
-
> - `
|
|
674
|
+
> - `primary` vs `canvas`: ≥ 3:1 on **both** white and black (primary doesn't flip).
|
|
675
|
+
> - `surface` vs `canvas`: distinguishable when adjacent (≥ 1.1:1).
|
|
676
|
+
> - `border` vs `surface`: visible but subtle (1.2–1.6:1).
|
|
677
|
+
> - `muted` vs `canvas`: legible but recessive (≥ 3:1).
|
|
651
678
|
|
|
652
679
|
### 9.4 Typography fingerprints
|
|
653
680
|
|
|
@@ -743,12 +770,12 @@ Save this as `src/design-system/configs/<brandid>.json`, replace every `___` pla
|
|
|
743
770
|
"success": "#___",
|
|
744
771
|
"warning": "#___",
|
|
745
772
|
"error": "#___",
|
|
746
|
-
"
|
|
747
|
-
"
|
|
748
|
-
"
|
|
749
|
-
"
|
|
750
|
-
"
|
|
751
|
-
"
|
|
773
|
+
"onPrimary": "#ffffff",
|
|
774
|
+
"canvas": "color-white",
|
|
775
|
+
"surface": "color-gray-50",
|
|
776
|
+
"text": "color-black",
|
|
777
|
+
"muted": "color-gray-500",
|
|
778
|
+
"border": "color-gray-200"
|
|
752
779
|
},
|
|
753
780
|
"tokens": {
|
|
754
781
|
"rawCssVars": {},
|
|
@@ -837,9 +864,16 @@ Run this list against the output before declaring done:
|
|
|
837
864
|
- [ ] `label.color` is **absent** on `checkbox`/`radio`/`switch`/`input`/`textarea`/`select` ([§7.2](#72-strip-labelcolor-from-brand-component-configs)).
|
|
838
865
|
- [ ] Every font family in `components.*.views.*.fontFamily` matches `tokens.typography.fontFamily`.
|
|
839
866
|
|
|
840
|
-
**
|
|
867
|
+
**Light/dark adaptivity** (so one config serves both modes — [theming.md §2.1](./theming.md#21-one-config-both-modes--the-adaptive-rule))
|
|
868
|
+
- [ ] Neutral slots `canvas`, `surface`, `text`, `muted`, `border` are **`color-*` token references**, not raw hex — so they flip with the theme mode.
|
|
869
|
+
- [ ] Brand slots `primary`, `secondary`, `success`, `warning`, `error`, `onPrimary` are **raw hex** — constant across modes.
|
|
870
|
+
- [ ] The five neutral slots all use **one** grey family (`color-gray-*`, or one tint family) — not mixed.
|
|
871
|
+
- [ ] Components reference `theme-text` / `theme-muted` for body ink — **never** a constant brand slot like `theme-secondary` (which would stay dark on a dark canvas).
|
|
872
|
+
|
|
873
|
+
**Contrast & legibility** (verify in **both** light and dark)
|
|
841
874
|
- [ ] `text` vs `canvas` ≥ 4.5:1.
|
|
842
875
|
- [ ] `onPrimary` vs `primary` ≥ 4.5:1.
|
|
876
|
+
- [ ] `primary` ≥ 3:1 against **both** `color-white` and `color-black` (it doesn't flip).
|
|
843
877
|
- [ ] `surface` ≠ `canvas` (visually distinguishable).
|
|
844
878
|
- [ ] `muted` vs `canvas` ≥ 3:1.
|
|
845
879
|
|
|
@@ -871,7 +905,8 @@ To anchor the playbook, here's how it applies end-to-end to a fictional brand: *
|
|
|
871
905
|
1. **Primary hex**: `#00d4ff` (electric cyan — owns the "speed/transit" lane without colliding with Tesla blue).
|
|
872
906
|
2. **Appearance**: `dark` (premium-technical brands default dark).
|
|
873
907
|
3. **Theme** (built per [§9.3](#93-theme-palette-recipe)):
|
|
874
|
-
- `primary #00d4ff`, `secondary #00a4cc` (darker analog), `success #00d68f`, `warning #ffb547`, `error #ff5757`, `
|
|
908
|
+
- Brand (hex, constant): `primary #00d4ff`, `secondary #00a4cc` (darker analog), `success #00d68f`, `warning #ffb547`, `error #ff5757`, `onPrimary #001218` (dark ink on cyan for AA). `primary` reads on both white and black ✓.
|
|
909
|
+
- Neutrals (`color-*` tokens, adaptive): `canvas color-white`, `surface color-gray-50`, `text color-black`, `muted color-gray-500`, `border color-gray-200`. With `defaultAppearance: "dark"` the config **opens** with a near-black canvas + light ink, and still works if the user toggles to light. *(If a brand truly needs exact, bespoke dark-only neutrals — e.g. a glass `#0f1115` surface — pin those as hex instead, accepting they won't flip.)*
|
|
875
910
|
4. **Typography**: *Modern-Mono-Hybrid* — body `Inter`, mono `JetBrains Mono`, plus `googleFontLinks` for both.
|
|
876
911
|
5. **Personality**: *Dev-Editorial* archetype, but tune `signatureMotif: '→'`, `accentTreatment: 'glow'`, `voice: 'velocity-precision'`.
|
|
877
912
|
6. **Components**: from the starter template; replace generic `Inter` with the chosen family; `tabs.activeTab` underline style (`backgroundColor: "transparent"`); `button.color: "theme-primary"`.
|
|
@@ -887,7 +922,8 @@ The output is a config that *renders correctly* and *unmistakably reads as Hyper
|
|
|
887
922
|
- Provider & hooks: [src/design-system/DesignSystemProvider.tsx](../../src/design-system/DesignSystemProvider.tsx)
|
|
888
923
|
- Types (incl. `BrandPersonality`): [src/design-system/types.ts](../../src/design-system/types.ts)
|
|
889
924
|
- Merge utilities: [src/design-system/utils.ts](../../src/design-system/utils.ts) — `deepMerge` (skips top-level `undefined`/`null`; recurses only when both sides are plain objects, so React-element props stay reference-only and don't trigger cycles), `getDesignSystemComponentProps` (runs `stripNullsDeep` so the schema's `null` placeholders never reach the merge — see [§3.6](#36-uniform-schema-across-configs)), `mergeDesignSystemComponentProps`, `normalizeDesignSystemComponentProps`
|
|
890
|
-
-
|
|
925
|
+
- Default adaptive config: [src/design-system/configs/default.json](../../src/design-system/configs/default.json) — the brand-neutral fallback; brand slots are hex (constant), neutral slots are `color-*` tokens (auto-flip), so it renders correctly in light **and** dark. The reference implementation of [theming.md §2.1](./theming.md#21-one-config-both-modes--the-adaptive-rule).
|
|
926
|
+
- 15 brand configs: [src/design-system/configs/](../../src/design-system/configs/) — fully tokenized; all `components.*.views.*` colors are `theme-*` references. (These are brand *snapshots* and currently pin their neutral slots as hex for their native appearance — convert a slot to a `color-*` token per §2.1 to make that brand adapt.)
|
|
891
927
|
- Live showcase: [src/pages/designSystem.page.tsx](../../src/pages/designSystem.page.tsx) — ergonomic patterns for `componentRadius`, `getPersonality`, `personality*` helpers, `BrandSnapshotSample`, `PaletteFrame`
|
|
892
928
|
- Theming primer (token vocabulary): [docs/design-system/theming.md](./theming.md)
|
|
893
929
|
- Wider conventions: [docs/conventions.md](../conventions.md)
|
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
# Generating a Design-System Config — Agent Guide
|
|
2
|
+
|
|
3
|
+
This is a **prescriptive recipe** for an AI agent (or a human) to generate a new
|
|
4
|
+
brand design-system config from scratch. Follow it top to bottom and you will
|
|
5
|
+
produce a single JSON file that renders correctly in **both light and dark
|
|
6
|
+
mode** across every component — with **no component code changes** and **no
|
|
7
|
+
light/dark branching**.
|
|
8
|
+
|
|
9
|
+
> **Mental model.** A config is *data*. Components are *dumb consumers* — they
|
|
10
|
+
> read tokens and do their job, nothing more. All the intelligence lives in the
|
|
11
|
+
> **shape of the values you choose**. Get the tokens right and dark mode,
|
|
12
|
+
> contrast, and brand identity all fall out automatically.
|
|
13
|
+
|
|
14
|
+
If you want the deep "how the resolver works" reference, read
|
|
15
|
+
[theming.md](./theming.md). This doc is the **build instructions**; theming.md
|
|
16
|
+
is the **physics**.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 0. TL;DR — the five rules that make a config coherent
|
|
21
|
+
|
|
22
|
+
1. **Stay vs adapt is decided by the value's *form*.**
|
|
23
|
+
- **Raw hex** (`"#2563eb"`) → **constant** in both modes. Use for brand identity.
|
|
24
|
+
- **`color-*` token** (`"color-black"`) → **flips** automatically. Use for structural neutrals.
|
|
25
|
+
2. **Brand colors are FILLS, not foreground.** `theme-primary` paints a
|
|
26
|
+
background and pairs with `theme-onPrimary` ink. **Never** use `theme-primary`
|
|
27
|
+
as the text/border color of something sitting directly on a surface — for a
|
|
28
|
+
black/white brand it collides with the surface and vanishes.
|
|
29
|
+
3. **Surface-contrasting foreground uses neutrals.** Body text, borders,
|
|
30
|
+
dividers, helper text → `theme-text` / `theme-muted` / `theme-border` (or the
|
|
31
|
+
`color-*` neutrals). These flip with the surface, so they stay readable on any
|
|
32
|
+
brand in any mode.
|
|
33
|
+
4. **`primary` and `onPrimary` must use the *same form*.** Both hex, or both
|
|
34
|
+
`color-*`. That keeps the fill and its ink flipping together (or staying
|
|
35
|
+
together) so the label never disappears.
|
|
36
|
+
5. **Never put a token into a raw inline `style={{}}` or a gradient string.** The
|
|
37
|
+
resolver only runs on component **props**. Tokens in raw CSS silently break
|
|
38
|
+
(fine for hex, invisible for `color-*`). This rule is about *consuming* configs;
|
|
39
|
+
you don't hit it while authoring one, but it's why neutrals must be tokens.
|
|
40
|
+
|
|
41
|
+
Everything below is an expansion of these five rules.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## 1. What you are producing
|
|
46
|
+
|
|
47
|
+
- **One file:** `src/design-system/configs/<brand>.json`.
|
|
48
|
+
- **One registration line** in `src/design-system/configs/index.ts`.
|
|
49
|
+
|
|
50
|
+
The fastest correct path is: **copy `default.json`, then change values** — its
|
|
51
|
+
structure is already coherent. Do not invent a new shape.
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
cp src/design-system/configs/default.json src/design-system/configs/<brand>.json
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The file has exactly five top-level keys, in this order:
|
|
58
|
+
|
|
59
|
+
```jsonc
|
|
60
|
+
{
|
|
61
|
+
"metadata": { ... }, // identity + fonts
|
|
62
|
+
"theme": { ... }, // the 11 semantic color slots ← the heart of the config
|
|
63
|
+
"tokens": { ... }, // typography, spacing, radii, shadows (mostly tooling)
|
|
64
|
+
"components": { ... }, // per-component slot overrides
|
|
65
|
+
"personality":{ ... } // non-color brand cues (shape, weight, density, motif)
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The TypeScript contract is [`DesignSystemConfig`](../../src/design-system/types.ts) —
|
|
70
|
+
keep it valid against that.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## 2. `metadata`
|
|
75
|
+
|
|
76
|
+
```jsonc
|
|
77
|
+
"metadata": {
|
|
78
|
+
"id": "acme", // unique kebab id; MUST match the filename and the index.ts key
|
|
79
|
+
"label": "Acme", // human label shown in the showcase switcher
|
|
80
|
+
"sourcePath": "", // optional reference URL/path; "" is fine
|
|
81
|
+
"sourceTitle": "Acme brand system (light + dark)",
|
|
82
|
+
"defaultAppearance": "light", // which mode the brand opens in: "light" | "dark"
|
|
83
|
+
"googleFontLinks": [ // REQUIRED if typography.fontFamily names a web font
|
|
84
|
+
"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap"
|
|
85
|
+
]
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
- `defaultAppearance` only sets the **opening** mode — the config still works in
|
|
90
|
+
both. Pick the mode that flatters the brand (e.g. SpaceX → `dark`).
|
|
91
|
+
- **Every font family referenced in `tokens.typography` must be loaded** via
|
|
92
|
+
`googleFontLinks`, or it silently falls back to the next family in the stack.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## 3. `theme` — the 11 slots (the part that matters most)
|
|
97
|
+
|
|
98
|
+
Eleven slots. Each is either a **constant hex** or an **adaptive `color-*`
|
|
99
|
+
token**. This single block serves light *and* dark — you never write a second
|
|
100
|
+
config.
|
|
101
|
+
|
|
102
|
+
```jsonc
|
|
103
|
+
"theme": {
|
|
104
|
+
"primary": "#2563eb", // hex → CONSTANT brand color (CTAs, links, focus)
|
|
105
|
+
"secondary": "#7c3aed", // hex → CONSTANT accent
|
|
106
|
+
"success": "#16a34a", // hex → CONSTANT
|
|
107
|
+
"warning": "#d97706", // hex → CONSTANT
|
|
108
|
+
"error": "#dc2626", // hex → CONSTANT
|
|
109
|
+
"onPrimary": "#ffffff", // hex → CONSTANT ink that sits on `primary`
|
|
110
|
+
|
|
111
|
+
"canvas": "color-white", // token → ADAPTS: white in light, black in dark
|
|
112
|
+
"surface": "color-gray-50", // token → ADAPTS: near-white → near-black
|
|
113
|
+
"text": "color-black", // token → ADAPTS: black in light, white in dark
|
|
114
|
+
"muted": "color-gray-500", // token → ADAPTS: readable mid-grey in both
|
|
115
|
+
"border": "color-gray-200" // token → ADAPTS: hairline in both
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### 3.1 The decision table
|
|
120
|
+
|
|
121
|
+
| Slot | Role | Form | Why |
|
|
122
|
+
|------|------|------|-----|
|
|
123
|
+
| `primary` | Brand CTA / link / focus | **hex** | Brand identity, same in both modes |
|
|
124
|
+
| `secondary` | Supporting accent | **hex** | Same |
|
|
125
|
+
| `success` / `warning` / `error` | Status accents | **hex** | Same |
|
|
126
|
+
| `onPrimary` | Ink on top of `primary` | **hex** | Contrast for the primary fill |
|
|
127
|
+
| `canvas` | Page background | **`color-*`** | Must flip to go dark |
|
|
128
|
+
| `surface` | Card / elevated background | **`color-*`** | Must flip |
|
|
129
|
+
| `text` | Primary ink / body copy | **`color-*`** | Must flip to stay readable |
|
|
130
|
+
| `muted` | Secondary ink (captions, helper) | **`color-*`** | Must flip |
|
|
131
|
+
| `border` | Hairlines, dividers, control borders | **`color-*`** | Must flip |
|
|
132
|
+
|
|
133
|
+
### 3.2 Hard constraints (verify these)
|
|
134
|
+
|
|
135
|
+
- **`primary` and `onPrimary` use the same form.** Both hex (typical), or both
|
|
136
|
+
`color-*`. Never mix — a hex fill with a flipping ink (or vice-versa) loses its
|
|
137
|
+
label in one mode.
|
|
138
|
+
- **`onPrimary` actually contrasts `primary`.** ≥ 4.5:1. Usually `#ffffff` for a
|
|
139
|
+
mid/dark `primary`; use `#000000` for a light/vivid `primary` (e.g. a bright
|
|
140
|
+
green or yellow).
|
|
141
|
+
- **`primary` reads on *both* `canvas` colors.** Since `primary` doesn't flip, a
|
|
142
|
+
hue chosen only for white can wash out on black. Aim for ≥ 3:1 against **both**
|
|
143
|
+
white and black. Most mid-tone brand colors pass; very light or very dark ones
|
|
144
|
+
don't (see monochrome brands, §3.3).
|
|
145
|
+
- **Body ink is never a brand slot.** Use `text` / `muted` for copy. A constant
|
|
146
|
+
dark `secondary` used as text becomes invisible on a dark canvas.
|
|
147
|
+
|
|
148
|
+
### 3.3 Monochrome / black-or-white brands (Nike, Vercel, Uber, SpaceX…)
|
|
149
|
+
|
|
150
|
+
A brand whose primary *is* black or white is the one case the form rule has to be
|
|
151
|
+
applied deliberately. Two valid strategies — pick one and be consistent:
|
|
152
|
+
|
|
153
|
+
| Strategy | `primary` | `onPrimary` | Result |
|
|
154
|
+
|----------|-----------|-------------|--------|
|
|
155
|
+
| **A — Constant brand (recommended)** | `"#111111"` (hex) | `"#ffffff"` (hex) | CTA stays the **same** black/white in both modes. Brand-stable. |
|
|
156
|
+
| **B — Adaptive monochrome (authentic Vercel/Linear)** | `"color-black"` | `"color-white"` | CTA **inverts**: black-on-light, white-on-dark. |
|
|
157
|
+
|
|
158
|
+
- Either way, **both forms match** (A = both hex, B = both `color-*`) — that's
|
|
159
|
+
rule 4.
|
|
160
|
+
- **Do not** then reuse `primary` for outline/ghost foreground or icons — on the
|
|
161
|
+
matching surface it disappears. For those, use `theme-text` (which flips with
|
|
162
|
+
the surface) or a chromatic `secondary`. This is the bug that makes an "outline
|
|
163
|
+
badge" vanish on the dark card.
|
|
164
|
+
|
|
165
|
+
### 3.4 Give neutrals character without breaking the flip
|
|
166
|
+
|
|
167
|
+
Swap the grey *family* — the shade still inverts, only the hue changes:
|
|
168
|
+
|
|
169
|
+
- `color-slate-*` (cool blue-grey), `color-stone-*` (warm), `color-zinc-*`,
|
|
170
|
+
`color-neutral-*`.
|
|
171
|
+
|
|
172
|
+
```jsonc
|
|
173
|
+
"surface": "color-slate-50", "muted": "color-slate-500", "border": "color-slate-200"
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
**Valid shades:** `50, 100, 200, 300, 400, 500, 600, 700, 800, 900` only.
|
|
177
|
+
`920` / `950` / `960` etc. are **not valid** and will fail to resolve.
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## 4. `tokens`
|
|
182
|
+
|
|
183
|
+
Mostly typography (functional) plus documentation/tooling arrays.
|
|
184
|
+
|
|
185
|
+
```jsonc
|
|
186
|
+
"tokens": {
|
|
187
|
+
"rawCssVars": { "primary": "#2563eb", "secondary": "#7c3aed", ... }, // descriptive mirror of brand hexes
|
|
188
|
+
"colors": [ { "name": "primary", "value": "#2563eb", "role": "primary" }, ... ], // descriptive
|
|
189
|
+
"typography": {
|
|
190
|
+
"fontFamily": "'Inter', system-ui, -apple-system, sans-serif", // FUNCTIONAL — used by components
|
|
191
|
+
"monoFamily": "ui-monospace, 'SF Mono', Menlo, monospace",
|
|
192
|
+
"fontSizes": ["12px","13px","14px","16px","18px","22px","28px","36px","48px","64px"],
|
|
193
|
+
"fontWeights": ["400","500","600","700","800"],
|
|
194
|
+
"lineHeights": ["1.0","1.1","1.2","1.4","1.5","1.6"]
|
|
195
|
+
},
|
|
196
|
+
"spacing": ["2px","4px","8px","12px","16px","24px","32px","48px","64px"], // 4px grid, documentation
|
|
197
|
+
"radii": ["2px","4px","6px","8px","12px","16px","9999px"], // documentation
|
|
198
|
+
"shadows": [ // lightest first; components pick by index
|
|
199
|
+
"0 1px 2px rgba(0,0,0,0.04)",
|
|
200
|
+
"0 4px 12px rgba(0,0,0,0.08)",
|
|
201
|
+
"0 12px 32px rgba(0,0,0,0.12)"
|
|
202
|
+
]
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
- **`typography.fontFamily` / `monoFamily` are the only fields components read at
|
|
207
|
+
runtime.** Always end the stack with a system fallback (`system-ui`,
|
|
208
|
+
`sans-serif`) so a missing web font degrades gracefully. List any web font in
|
|
209
|
+
`metadata.googleFontLinks`.
|
|
210
|
+
- `rawCssVars`, `colors`, and the `spacing` / `radii` / `shadows` arrays are
|
|
211
|
+
reference/tooling material — keep them consistent with the brand but they don't
|
|
212
|
+
drive rendering on their own.
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## 5. `components` — per-slot overrides
|
|
217
|
+
|
|
218
|
+
Each component has a top-level config (variant/size/shape) plus a `views` map of
|
|
219
|
+
named slots. Use overrides to apply the brand's **shape, font, and accent
|
|
220
|
+
tokens** — **not** to re-specify colors a component already handles correctly.
|
|
221
|
+
|
|
222
|
+
**The token-role rule for every slot:**
|
|
223
|
+
|
|
224
|
+
| What the slot paints | Use | Examples |
|
|
225
|
+
|----------------------|-----|----------|
|
|
226
|
+
| A **filled** element's background + its ink | `theme-primary` + `theme-onPrimary` | filled badge/button, checkbox `_checked`, radio `dot`, slider `progress`, progress `bar` |
|
|
227
|
+
| A **surface** background | `theme-surface` / `theme-canvas` or a `color-*` neutral | card/input/table/accordion container |
|
|
228
|
+
| A **border / divider** | `theme-border` or `color-gray-200` | container borders, separators, table lines |
|
|
229
|
+
| **Body / heading ink** on a surface | `theme-text` or `color-black`, or `"inherit"` | titles, cell text, labels |
|
|
230
|
+
| **Secondary ink** | `theme-muted` or `color-gray-500` | helper text, captions, descriptions |
|
|
231
|
+
| A **brand-colored accent foreground** | `theme-primary` **only for chromatic brands** | active-tab label, link, alert icon |
|
|
232
|
+
|
|
233
|
+
> **The trap:** using `theme-primary` for *foreground on a surface* (an outline
|
|
234
|
+
> border, a link, an active-tab label) looks great for a blue brand and
|
|
235
|
+
> **disappears** for a black/white brand, because their `primary` equals the
|
|
236
|
+
> surface in one mode. If the brand is monochrome, switch those to `theme-text`
|
|
237
|
+
> or `theme-secondary`.
|
|
238
|
+
|
|
239
|
+
Example — a button override that changes only shape/typography and keeps the
|
|
240
|
+
correct fill tokens:
|
|
241
|
+
|
|
242
|
+
```jsonc
|
|
243
|
+
"button": {
|
|
244
|
+
"variant": "filled", "size": "md", "shape": "rounded",
|
|
245
|
+
"color": "theme-primary", "textColor": "theme-onPrimary",
|
|
246
|
+
"views": {
|
|
247
|
+
"container": {
|
|
248
|
+
"borderRadius": 8,
|
|
249
|
+
"fontFamily": "'Inter', system-ui, sans-serif",
|
|
250
|
+
"fontWeight": 600,
|
|
251
|
+
"borderColor": "theme-primary",
|
|
252
|
+
"letterSpacing": "-0.01em"
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
**Do not lock `label.color`** on form controls (Checkbox, Radio, Switch,
|
|
259
|
+
FieldLabel, StatusIndicator, Table cells). Omit it so the component's `inherit`
|
|
260
|
+
default carries the surrounding ink — see
|
|
261
|
+
[component-library.md §7.2](./component-library.md).
|
|
262
|
+
|
|
263
|
+
The complete slot map for all 22 overridable components is the
|
|
264
|
+
[`default.json`](../../src/design-system/configs/default.json) you copied —
|
|
265
|
+
treat it as the canonical template and change values slot-by-slot. The component
|
|
266
|
+
name list and slot types are in [types.ts](../../src/design-system/types.ts)
|
|
267
|
+
(`DesignSystemComponentName`).
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## 6. `personality` — non-color brand cues
|
|
272
|
+
|
|
273
|
+
Pages and composition helpers read these for brand character that 11 colors can't
|
|
274
|
+
express. Keep it; tune the values.
|
|
275
|
+
|
|
276
|
+
```jsonc
|
|
277
|
+
"personality": {
|
|
278
|
+
"cornerStyle": "soft", // "sharp" | "soft" | "pill"
|
|
279
|
+
"typeWeight": "regular", // "light" | "regular" | "bold" | "black"
|
|
280
|
+
"typeCase": "normal", // "normal" | "uppercase"
|
|
281
|
+
"typeStyle": "normal", // "normal" | "italic"
|
|
282
|
+
"letterSpacing": "-0.01em",
|
|
283
|
+
"accentTreatment": "flat", // "flat" | "gradient" | "stripe" | "glow" | "halftone"
|
|
284
|
+
"signatureMotif": "●", // a glyph used as a brand tick/bullet
|
|
285
|
+
"density": "comfortable", // "tight" | "comfortable" | "spacious"
|
|
286
|
+
"surfaceTone": "paper", // "paper" | "glass" | "matte" | "mono"
|
|
287
|
+
"cardRadius": 12, "pillRadius": 9999, "badgeRadius": 6,
|
|
288
|
+
"voice": "neutral-clear" // free-text descriptor used to pick brand copy
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Match these to the brand: Nike → `cornerStyle: "sharp"`, `typeCase: "uppercase"`,
|
|
293
|
+
`typeWeight: "black"`; Revolut/Spotify → `cornerStyle: "pill"`.
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
## 7. Register the config
|
|
298
|
+
|
|
299
|
+
Add two lines to [`src/design-system/configs/index.ts`](../../src/design-system/configs/index.ts):
|
|
300
|
+
|
|
301
|
+
```ts
|
|
302
|
+
import acme from './acme.json'; // 1. import
|
|
303
|
+
|
|
304
|
+
export const designSystemConfigs = {
|
|
305
|
+
// ...
|
|
306
|
+
acme: acme as DesignSystemConfig, // 2. register (key MUST equal metadata.id)
|
|
307
|
+
};
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
That makes it available as `configId="acme"` and adds it to the showcase
|
|
311
|
+
switcher. Use it anywhere:
|
|
312
|
+
|
|
313
|
+
```tsx
|
|
314
|
+
<DesignSystemProvider configId="acme">
|
|
315
|
+
<App />
|
|
316
|
+
</DesignSystemProvider>
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
---
|
|
320
|
+
|
|
321
|
+
## 8. Validation checklist (self-check before you finish)
|
|
322
|
+
|
|
323
|
+
Run through every item — these map 1:1 to the failure modes this system has hit:
|
|
324
|
+
|
|
325
|
+
- [ ] **JSON is valid** and matches `DesignSystemConfig` (5 top-level keys).
|
|
326
|
+
- [ ] `metadata.id` === filename === `index.ts` key.
|
|
327
|
+
- [ ] **`canvas`, `surface`, `text`, `muted`, `border` are `color-*` tokens** (they MUST flip).
|
|
328
|
+
- [ ] **`primary`, `secondary`, `success`, `warning`, `error`, `onPrimary` are hex** (constant) — *unless* you chose monochrome Strategy B for `primary`/`onPrimary` (then both are `color-*`).
|
|
329
|
+
- [ ] **`primary` and `onPrimary` use the same form.**
|
|
330
|
+
- [ ] `onPrimary` contrasts `primary` ≥ 4.5:1.
|
|
331
|
+
- [ ] `primary` reads ≥ 3:1 on **both** white and black.
|
|
332
|
+
- [ ] **No `color-*` shade outside `50…900`.** No `920/950/960`.
|
|
333
|
+
- [ ] **No brand slot used as body ink**; body uses `text` / `muted`.
|
|
334
|
+
- [ ] **No `theme-primary` used as foreground-on-surface for a monochrome brand** (use `theme-text` / `theme-secondary`).
|
|
335
|
+
- [ ] Every font in `typography` is listed in `metadata.googleFontLinks`; the stack ends in a system fallback.
|
|
336
|
+
- [ ] `label.color` is **omitted** on form-control overrides (let `inherit` work).
|
|
337
|
+
- [ ] Registered in `index.ts`.
|
|
338
|
+
- [ ] **Eyeball both modes** in the showcase — toggle light/dark and confirm: no invisible text, no white-on-white, no vanished outlines, filled CTAs keep readable labels.
|
|
339
|
+
|
|
340
|
+
---
|
|
341
|
+
|
|
342
|
+
## 9. Anti-patterns (do NOT do these)
|
|
343
|
+
|
|
344
|
+
| ❌ Anti-pattern | Why it breaks | ✅ Instead |
|
|
345
|
+
|----------------|---------------|-----------|
|
|
346
|
+
| `"text": "#111111"` | Frozen dark ink → invisible on dark canvas | `"text": "color-black"` |
|
|
347
|
+
| `"canvas": "#ffffff"` | Frozen white page → never goes dark | `"canvas": "color-white"` |
|
|
348
|
+
| `"primary": "color-blue-600"` for a chromatic brand | Brand color shifts between modes unexpectedly | `"primary": "#2563eb"` (hex, constant) |
|
|
349
|
+
| `primary` hex + `onPrimary` `"color-white"` | Mismatched forms → label flips off the fill | Same form for both |
|
|
350
|
+
| `theme-primary` as outline/link color on a **black** brand | Collides with the surface, vanishes | `theme-text` or `theme-secondary` |
|
|
351
|
+
| Writing a separate `<brand>.dark.json` | Unnecessary; dark is derived | One config, adaptive tokens |
|
|
352
|
+
| Adding `appearance === 'dark' ? … : …` anywhere | The system flips for you via tokens | Use the right token form |
|
|
353
|
+
| `color-gray-950` | Invalid shade | `color-gray-900` |
|
|
354
|
+
|
|
355
|
+
---
|
|
356
|
+
|
|
357
|
+
## 10. Minimal valid template (copy, then fill in)
|
|
358
|
+
|
|
359
|
+
```jsonc
|
|
360
|
+
{
|
|
361
|
+
"metadata": {
|
|
362
|
+
"id": "acme", "label": "Acme", "sourcePath": "",
|
|
363
|
+
"sourceTitle": "Acme brand system (light + dark)",
|
|
364
|
+
"defaultAppearance": "light",
|
|
365
|
+
"googleFontLinks": ["https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap"]
|
|
366
|
+
},
|
|
367
|
+
"theme": {
|
|
368
|
+
"primary": "#2563eb", "secondary": "#7c3aed",
|
|
369
|
+
"success": "#16a34a", "warning": "#d97706", "error": "#dc2626",
|
|
370
|
+
"onPrimary": "#ffffff",
|
|
371
|
+
"canvas": "color-white", "surface": "color-gray-50",
|
|
372
|
+
"text": "color-black", "muted": "color-gray-500", "border": "color-gray-200"
|
|
373
|
+
},
|
|
374
|
+
"tokens": {
|
|
375
|
+
"rawCssVars": { "primary": "#2563eb", "secondary": "#7c3aed", "success": "#16a34a", "warning": "#d97706", "error": "#dc2626" },
|
|
376
|
+
"colors": [
|
|
377
|
+
{ "name": "primary", "value": "#2563eb", "role": "primary" },
|
|
378
|
+
{ "name": "secondary", "value": "#7c3aed", "role": "secondary" }
|
|
379
|
+
],
|
|
380
|
+
"typography": {
|
|
381
|
+
"fontFamily": "'Inter', system-ui, -apple-system, sans-serif",
|
|
382
|
+
"monoFamily": "ui-monospace, 'SF Mono', Menlo, monospace",
|
|
383
|
+
"fontSizes": ["12px","13px","14px","16px","18px","22px","28px","36px","48px","64px"],
|
|
384
|
+
"fontWeights": ["400","500","600","700","800"],
|
|
385
|
+
"lineHeights": ["1.0","1.1","1.2","1.4","1.5","1.6"]
|
|
386
|
+
},
|
|
387
|
+
"spacing": ["2px","4px","8px","12px","16px","24px","32px","48px","64px"],
|
|
388
|
+
"radii": ["2px","4px","6px","8px","12px","16px","9999px"],
|
|
389
|
+
"shadows": ["0 1px 2px rgba(0,0,0,0.04)","0 4px 12px rgba(0,0,0,0.08)","0 12px 32px rgba(0,0,0,0.12)"]
|
|
390
|
+
},
|
|
391
|
+
"components": {
|
|
392
|
+
"button": {
|
|
393
|
+
"variant": "filled", "size": "md", "shape": "rounded",
|
|
394
|
+
"color": "theme-primary", "textColor": "theme-onPrimary",
|
|
395
|
+
"views": { "container": { "borderRadius": 8, "fontWeight": 600, "borderColor": "theme-primary" } }
|
|
396
|
+
},
|
|
397
|
+
"card": {
|
|
398
|
+
"variant": "outlined", "shape": "rounded",
|
|
399
|
+
"views": {
|
|
400
|
+
"container": { "backgroundColor": "color-gray-50", "borderColor": "color-gray-200", "color": "color-black", "borderRadius": 12 },
|
|
401
|
+
"header": { "color": "color-black", "borderColor": "color-gray-200" },
|
|
402
|
+
"content": { "color": "color-gray-500" }
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
},
|
|
406
|
+
"personality": {
|
|
407
|
+
"cornerStyle": "soft", "typeWeight": "regular", "typeCase": "normal", "typeStyle": "normal",
|
|
408
|
+
"letterSpacing": "-0.01em", "accentTreatment": "flat", "signatureMotif": "●",
|
|
409
|
+
"density": "comfortable", "surfaceTone": "paper",
|
|
410
|
+
"cardRadius": 12, "pillRadius": 9999, "badgeRadius": 6, "voice": "neutral-clear"
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
> Start from the **full** [`default.json`](../../src/design-system/configs/default.json)
|
|
416
|
+
> (all 22 component overrides), not this minimal one, unless you have a reason to
|
|
417
|
+
> omit components — missing components simply fall back to library defaults.
|
|
418
|
+
|
|
419
|
+
---
|
|
420
|
+
|
|
421
|
+
## See also
|
|
422
|
+
|
|
423
|
+
- [theming.md](./theming.md) — how the token resolver and CSS-variable flip work.
|
|
424
|
+
- [component-library.md](./component-library.md) — authoring components against the system.
|
|
425
|
+
- [types.ts](../../src/design-system/types.ts) — the `DesignSystemConfig` contract.
|
|
426
|
+
- [configs/](../../src/design-system/configs/) — 15 shipped brand configs to learn from.
|