@ttoss/fsl-theme 1.1.11

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.
Files changed (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +390 -0
  3. package/dist/Types-6tR0_2Ss.d.ts +1452 -0
  4. package/dist/css.d.ts +164 -0
  5. package/dist/dataviz/index.d.ts +62 -0
  6. package/dist/dtcg.d.ts +49 -0
  7. package/dist/esm/chunk-4Q4P3JBB.js +185 -0
  8. package/dist/esm/chunk-5PWPAQMC.js +9 -0
  9. package/dist/esm/chunk-BXKVVQEP.js +29 -0
  10. package/dist/esm/chunk-DU4QDQUC.js +29 -0
  11. package/dist/esm/chunk-FBVUI2PK.js +147 -0
  12. package/dist/esm/chunk-HRNXVRS3.js +54 -0
  13. package/dist/esm/chunk-IJGA42O6.js +141 -0
  14. package/dist/esm/chunk-PQPQNZ73.js +262 -0
  15. package/dist/esm/chunk-SE5Z52RE.js +1898 -0
  16. package/dist/esm/chunk-TPMN75JM.js +29 -0
  17. package/dist/esm/chunk-UMRQ4OTX.js +11 -0
  18. package/dist/esm/chunk-VL6EGE6Z.js +222 -0
  19. package/dist/esm/chunk-WVQSTQD5.js +192 -0
  20. package/dist/esm/css.js +6 -0
  21. package/dist/esm/dataviz/index.js +19 -0
  22. package/dist/esm/dtcg.js +65 -0
  23. package/dist/esm/index.js +10 -0
  24. package/dist/esm/react.js +8 -0
  25. package/dist/esm/runtime-entry.js +4 -0
  26. package/dist/esm/themes/bruttal.js +6 -0
  27. package/dist/esm/themes/corporate.js +6 -0
  28. package/dist/esm/themes/oca.js +6 -0
  29. package/dist/esm/themes/ventures.js +6 -0
  30. package/dist/esm/vars.js +28 -0
  31. package/dist/index.d.ts +86 -0
  32. package/dist/react.d.ts +346 -0
  33. package/dist/runtime-entry.d.ts +95 -0
  34. package/dist/themes/bruttal.d.ts +5 -0
  35. package/dist/themes/corporate.d.ts +5 -0
  36. package/dist/themes/oca.d.ts +5 -0
  37. package/dist/themes/ventures.d.ts +5 -0
  38. package/dist/vars.d.ts +127 -0
  39. package/llms.txt +731 -0
  40. package/package.json +88 -0
package/llms.txt ADDED
@@ -0,0 +1,731 @@
1
+ # @ttoss/fsl-theme
2
+
3
+ > Design token system with a strict semantic contract. Two layers (`core` = raw values, `semantic` = stable design meaning); components consume only `semantic.*`. The semantic layer is the public API — type-safe, mode-agnostic, emitted as CSS custom properties with zero runtime cost.
4
+
5
+ This file is the LLM-facing guide for using the package in an external application. It covers two jobs: **using tokens** (creating components) and **customizing the theme** (overrides). All token paths and per-token semantics are also available via JSDoc on `vars.*` after `import { vars } from '@ttoss/fsl-theme/vars'`.
6
+
7
+ ---
8
+
9
+ ## Entry points
10
+
11
+ ```ts
12
+ import { createTheme, baseTheme, bruttal, corporate, oca, ventures } from '@ttoss/fsl-theme';
13
+ import { ThemeProvider, useColorMode, useResolvedTokens } from '@ttoss/fsl-theme/react';
14
+ import { vars } from '@ttoss/fsl-theme/vars'; // typed CSS var(--tt-*) refs — CSS environments
15
+ import { toCssVarName } from '@ttoss/fsl-theme/css'; // raw --tt-* names — tooling / PostCSS
16
+ ```
17
+
18
+ `vars.*` is a static map: every leaf is a `var(--tt-*)` string. Zero runtime cost. The values behind those CSS vars are injected by `ThemeProvider` — without it, the properties are unset and components render with no token values.
19
+
20
+ `useTokens()` returns **unresolved** `TokenRef` strings (e.g. `'{core.colors.brand.500}'`) — for introspection only, never for styling. Use `vars.*` for CSS, `useResolvedTokens()` for non-CSS.
21
+
22
+ ---
23
+
24
+ ## Getting started — minimal app
25
+
26
+ ```tsx
27
+ // theme.ts — create once, import everywhere
28
+ import { createTheme } from '@ttoss/fsl-theme';
29
+ export const theme = createTheme(); // default base + dark alternate included
30
+
31
+ // main.tsx (or _app.tsx, layout.tsx, etc.)
32
+ import { ThemeProvider } from '@ttoss/fsl-theme/react';
33
+ import { theme } from './theme';
34
+
35
+ export function App() {
36
+ return (
37
+ <ThemeProvider theme={theme}>
38
+ <YourApp />
39
+ </ThemeProvider>
40
+ );
41
+ }
42
+ ```
43
+
44
+ `ThemeProvider` injects all `--tt-*` CSS custom properties into `<head>`, listens to `prefers-color-scheme`, and persists the user's choice to `localStorage`. Pass `defaultMode="dark"` to start in dark mode; pass `alternate={null}` to `createTheme` to opt out of dark mode entirely.
45
+
46
+ **Built-in themes** — extend instead of building from scratch:
47
+
48
+ ```ts
49
+ import { bruttal, corporate, oca, ventures } from '@ttoss/fsl-theme';
50
+ // Use directly as-is:
51
+ <ThemeProvider theme={bruttal}>...</ThemeProvider>
52
+ // Or extend with brand overrides:
53
+ import { createTheme } from '@ttoss/fsl-theme';
54
+ const myTheme = createTheme({ extends: bruttal, overrides: { core: { colors: { brand: { 500: '#FF6B00' } } } } });
55
+ ```
56
+
57
+ ---
58
+
59
+ ## Token architecture
60
+
61
+ ```
62
+ ThemeTokens
63
+ ├── core raw primitives (immutable across modes)
64
+ └── semantic {core.*} references (the public contract; remapped per mode)
65
+ ```
66
+
67
+ Components consume **only** `semantic.*`. Core values never change between light and dark modes — only semantic references remap. Customization touches `core` (values) or `semantic` (references), never component code.
68
+
69
+ ---
70
+
71
+ ## CSS var naming scheme
72
+
73
+ `vars.*` gives TypeScript autocomplete. For raw CSS or CSS Modules, use the `--tt-*` names directly.
74
+
75
+ | Token path prefix | CSS var prefix | Example |
76
+ | --- | --- | --- |
77
+ | `semantic.colors.` | `--tt-colors-` | `vars.colors.action.primary.background.default` → `var(--tt-colors-action-primary-background-default)` |
78
+ | `semantic.text.` | `--tt-text-` | `vars.text.body.md.fontSize` → `var(--tt-text-body-md-fontSize)` |
79
+ | `semantic.spacing.` | `--tt-spacing-` | `vars.spacing.inset.control.md` → `var(--tt-spacing-inset-control-md)` |
80
+ | `semantic.sizing.` | `--tt-sizing-` | `vars.sizing.hit.base` → `var(--tt-sizing-hit-base)` |
81
+ | `semantic.radii.` | `--tt-radii-` | `vars.radii.control` → `var(--tt-radii-control)` |
82
+ | `semantic.border.` | `--tt-border-` | `vars.border.outline.control.width` → `var(--tt-border-outline-control-width)` |
83
+ | `semantic.focus.` | `--tt-focus-` | `vars.focus.ring.color` → `var(--tt-focus-ring-color)` |
84
+ | `semantic.elevation.` | `--tt-elevation-` | `vars.elevation.surface.raised` → `var(--tt-elevation-surface-raised)` |
85
+ | `semantic.opacity.` | `--tt-opacity-` | `vars.opacity.disabled` → `var(--tt-opacity-disabled)` |
86
+ | `semantic.overlay.` | `--tt-overlay-` | `vars.overlay.scrim` → `var(--tt-overlay-scrim)` |
87
+ | `semantic.motion.` | `--tt-motion-` | `vars.motion.feedback.duration` → `var(--tt-motion-feedback-duration)` |
88
+ | `semantic.zIndex.layer.` | `--tt-z-index-` | `vars.zIndex.layer.overlay` → `var(--tt-z-index-overlay)` |
89
+
90
+ The table is the contract — paths are not a pure mechanical transform (`zIndex` → `z-index`; `zIndex.layer.` collapses to `--tt-z-index-`). camelCase leaf properties (`fontSize`, `fontFamily`) preserve case.
91
+
92
+ ---
93
+
94
+ # JOB 1 — Using tokens to build components
95
+
96
+ ## FSL color axes
97
+
98
+ Every `vars.colors.*` token is `{ux}.{role}.{dimension}.{state}`. The four axes are defined once here; later sections reference them.
99
+
100
+ **`ux` — FSL Entity Kind (what the element does):**
101
+ - `action` — button, link-as-CTA, command (mutates state outside the form lifecycle).
102
+ - `input` — text field, checkbox, radio, switch, segmented control (mutates a value the form submits).
103
+ - `navigation` — tab, menu item, breadcrumb, nav link (changes user location in IA).
104
+ - `feedback` — alert, banner, toast, progress (system-initiated communication about an event).
105
+ - `informational` — card, badge, chip, stat, page surface (passive content surface).
106
+
107
+ **`role` — Evaluation (emphasis / valence):**
108
+ - `primary` — the single most important instance per `{ux}` per view.
109
+ - `secondary` — coexisting alternative to `primary`.
110
+ - `accent` — brand pop / high-emphasis variant.
111
+ - `muted` — subdued / low-priority.
112
+ - `positive` / `caution` / `negative` — outcome valence (success / warning / failure or destructive).
113
+
114
+ **`dimension`:** `background` · `border` · `text`.
115
+
116
+ **`state`:** `default` · `hover` · `active` · `focused` · `disabled` · `selected` · `pressed` · `checked` · `current` · `visited` · `expanded` · `indeterminate` · `droptarget`.
117
+
118
+ Legal `{ux, role}` and `{ux, state}` combinations are enforced by the TypeScript types of `vars` — invalid paths fail at the call site, not at runtime:
119
+
120
+ | `ux` | valid `role` values |
121
+ | --- | --- |
122
+ | `action` | `primary` · `secondary` · `accent` · `muted` · `negative` |
123
+ | `input` | `primary` · `secondary` · `muted` · `positive` · `caution` · `negative` |
124
+ | `navigation` | `primary` · `secondary` · `accent` · `muted` |
125
+ | `feedback` | `primary` · `muted` · `positive` · `caution` · `negative` (`feedback.primary` carries no valence — it's neutral feedback) |
126
+ | `informational` | `primary` · `secondary` · `accent` · `muted` · `positive` · `caution` · `negative` |
127
+
128
+ Valid `{ux, state}` combinations: see [Color state legality](#color-state-legality-per-ux) below.
129
+
130
+ ---
131
+
132
+ ## Token grammar by family
133
+
134
+ | Family | Path shape | Leaf type |
135
+ | --- | --- | --- |
136
+ | **colors** | `vars.colors.{ux}.{role}.{dimension}.{state?}` | CSS color |
137
+ | **spacing** | `vars.spacing.inset.{control,surface}.{sm,md,lg}` | CSS length |
138
+ | | `vars.spacing.gap.{stack,inline}.{xs,sm,md,lg,xl}` | CSS length |
139
+ | | `vars.spacing.gutter.{page,section}` | CSS length / `clamp()` |
140
+ | | `vars.spacing.separation.interactive.min` | CSS length |
141
+ | **text** | `vars.text.{display,headline,title,body,label,code}.{lg,md,sm}` | TextStyle (object — see below) |
142
+ | **sizing** | `vars.sizing.hit.{min,base,prominent}` | CSS length |
143
+ | | `vars.sizing.icon.{sm,md,lg}` | CSS length |
144
+ | | `vars.sizing.identity.{sm,md,lg,xl}` | CSS length |
145
+ | | `vars.sizing.measure.reading` | CSS `ch` / `clamp()` |
146
+ | | `vars.sizing.surface.maxWidth` | CSS length |
147
+ | | `vars.sizing.viewport.{height,width}.full` | dvh / dvw |
148
+ | **radii** | `vars.radii.{control,surface,round}` | CSS length |
149
+ | **border** | `vars.border.divider.{width,style}` | object |
150
+ | | `vars.border.outline.{surface,control,selected}.{width,style}` | object |
151
+ | **focus** | `vars.focus.ring.{width,style,color}` | object |
152
+ | **elevation** | `vars.elevation.surface.{flat,raised,overlay,blocking}` | box-shadow |
153
+ | | `vars.elevation.tonal.{raised,overlay,blocking}` (optional) | CSS color |
154
+ | **opacity** | `vars.opacity.{scrim,loading,disabled}` | number in (0,1) |
155
+ | **overlay** | `vars.overlay.scrim` | CSS color with alpha |
156
+ | **motion** | `vars.motion.{feedback,emphasis,decorative}.{duration,easing}` | object |
157
+ | | `vars.motion.transition.{enter,exit}.{duration,easing}` | object |
158
+ | **zIndex** | `vars.zIndex.layer.{base,sticky,overlay,blocking,transient}` | integer |
159
+
160
+ ## Pick a token in 60 seconds — by CSS property
161
+
162
+ Find the CSS property you are setting; follow the branch.
163
+
164
+ **`color` / `background` / `border-color` / `fill` → `vars.colors.{ux}.{role}.{dimension}.{state}`** — axes defined in [FSL color axes](#fsl-color-axes); state legality in [Color state legality](#color-state-legality-per-ux).
165
+
166
+ **`padding` → `spacing.inset`**
167
+
168
+ - Interactive control (button, input, toggle) → `vars.spacing.inset.control.{sm|md|lg}`
169
+ - Container (card, panel, dialog, section) → `vars.spacing.inset.surface.{sm|md|lg}`
170
+ - Default step at any level → `md`
171
+
172
+ **`gap` → `spacing.gap`**
173
+
174
+ - Vertical sibling flow (form fields, list, stacked sections) → `vars.spacing.gap.stack.{xs|sm|md|lg|xl}`
175
+ - Horizontal sibling flow (icon + label, toolbar, chip row) → `vars.spacing.gap.inline.{xs|sm|md|lg|xl}`
176
+ - Adjacent independently focusable targets → `vars.spacing.separation.interactive.min`
177
+
178
+ **`padding` on a page or section wrapper → `spacing.gutter`**
179
+
180
+ - Page-level horizontal gutter → `vars.spacing.gutter.page`
181
+ - Section-level vertical gutter → `vars.spacing.gutter.section`
182
+
183
+ **`font-*` / `line-height` → `text.{role}.{step}`**
184
+
185
+ | Element role | Token |
186
+ | --- | --- |
187
+ | Hero / landing emphasis | `vars.text.display.{lg,md,sm}` |
188
+ | Page or section heading | `vars.text.headline.{lg,md,sm}` |
189
+ | Surface heading (card, panel, dialog) | `vars.text.title.{lg,md,sm}` |
190
+ | Reading prose / description | `vars.text.body.{lg,md,sm}` |
191
+ | UI label / button text / badge | `vars.text.label.{lg,md,sm}` |
192
+ | Code / identifiers / monospace | `vars.text.code.{md,sm}` |
193
+
194
+ `vars.text.*.*` is a **TextStyle object** — each field is its own `var(--tt-text-*)` string. Apply each field individually to the corresponding CSS property:
195
+
196
+ ```tsx
197
+ // Inline styles:
198
+ style={{
199
+ fontFamily: vars.text.body.md.fontFamily,
200
+ fontSize: vars.text.body.md.fontSize,
201
+ fontWeight: vars.text.body.md.fontWeight,
202
+ lineHeight: vars.text.body.md.lineHeight,
203
+ letterSpacing: vars.text.body.md.letterSpacing,
204
+ }}
205
+
206
+ // CSS / CSS Modules:
207
+ // font-family: var(--tt-text-body-md-fontFamily);
208
+ // font-size: var(--tt-text-body-md-fontSize);
209
+ // font-weight: var(--tt-text-body-md-fontWeight);
210
+ // line-height: var(--tt-text-body-md-lineHeight);
211
+ // letter-spacing: var(--tt-text-body-md-letterSpacing);
212
+ ```
213
+
214
+ Available fields: `fontFamily` · `fontSize` · `fontWeight` · `lineHeight` · `letterSpacing` · `fontOpticalSizing?` · `fontVariantNumeric?`
215
+
216
+ **`width` / `height` / `min-width` / `min-height` → `sizing`**
217
+
218
+ - Interactive area (hit target) → `vars.sizing.hit.{min|base|prominent}` (apply via `min-width`/`min-height`, not visual size)
219
+ - Icon glyph → `vars.sizing.icon.{sm|md|lg}`
220
+ - Avatar / identity → `vars.sizing.identity.{sm|md|lg|xl}`
221
+ - Reading line length → `vars.sizing.measure.reading` (use as `max-width`)
222
+ - Surface max width → `vars.sizing.surface.maxWidth`
223
+ - Full viewport → `vars.sizing.viewport.{height|width}.full`
224
+
225
+ **`border-radius` → `radii`**
226
+
227
+ - Interactive control → `vars.radii.control`
228
+ - Containing surface → `vars.radii.surface`
229
+ - Pill / capsule / circular avatar → `vars.radii.round`
230
+
231
+ **`border-width` / `border-style` → `border`** (pair with a `colors` token for the line color)
232
+
233
+ - Separator between content groups → `vars.border.divider`
234
+ - Surface enclosing edge → `vars.border.outline.surface`
235
+ - Control enclosing edge → `vars.border.outline.control`
236
+ - Selected/current state line → `vars.border.outline.selected`
237
+
238
+ **`outline` (keyboard focus) → `focus`** — see co-occurrence rule §1 below.
239
+
240
+ - Component has a clear `{ux}` → `vars.colors.{ux}.{role}.border.focused`
241
+ - No clear `{ux}` (focusable card, custom widget) → `vars.focus.ring.{width,style,color}`
242
+
243
+ **`box-shadow` → `elevation.surface`**
244
+
245
+ - Flush with page → `vars.elevation.surface.flat`
246
+ - Card / panel → `vars.elevation.surface.raised`
247
+ - Dropdown / popover → `vars.elevation.surface.overlay`
248
+ - Dialog / sheet → `vars.elevation.surface.blocking`
249
+
250
+ **`z-index` → `zIndex.layer`** — pick by *blocking behaviour and persistence*, not by component name.
251
+
252
+ - Normal flow → `vars.zIndex.layer.base`
253
+ - Sticky header / nav → `vars.zIndex.layer.sticky`
254
+ - Non-blocking floating surface (dropdown, popover) → `vars.zIndex.layer.overlay`
255
+ - Blocking surface (dialog with scrim) → `vars.zIndex.layer.blocking`
256
+ - Transient non-blocking (toast) → `vars.zIndex.layer.transient`
257
+
258
+ **`opacity` (whole element) → `opacity`** — see co-occurrence rule §3 below.
259
+
260
+ - Modal backdrop dimming → `vars.opacity.scrim`
261
+ - Content visible during async work → `vars.opacity.loading`
262
+ - Disabled visual asset (image, avatar) → `vars.opacity.disabled`
263
+
264
+ **`transition` / `animation` → `motion`**
265
+
266
+ - Response to user input on a single element → `vars.motion.feedback`
267
+ - Element entering layout → `vars.motion.transition.enter`
268
+ - Element leaving layout → `vars.motion.transition.exit`
269
+ - Must-notice in-place change → `vars.motion.emphasis`
270
+ - Ambient / decorative loop → `vars.motion.decorative`
271
+
272
+ **Modal backdrop fill → `vars.overlay.scrim`**
273
+
274
+ ## Color state legality (per `ux`)
275
+
276
+ Not every state is legal in every `ux` context. Illegal combinations are TypeScript errors at the call site. Missing optional states on a sparse theme fall back to `default` (§8).
277
+
278
+ | State | action | input | navigation | feedback | informational |
279
+ | --- | --- | --- | --- | --- | --- |
280
+ | `default` | ✅ | ✅ | ✅ | ✅ | ✅ |
281
+ | `hover` | ✅ | ✅ | ✅ | — | ✅ |
282
+ | `active` | ✅ | ✅ | ✅ | — | ✅ |
283
+ | `focused` | ✅ | ✅ | ✅ | ✅ (focusable wrapper or close btn) | ✅ |
284
+ | `disabled` | ✅ | ✅ | ✅ | ✅ | ✅ |
285
+ | `selected` | — | ✅ (set membership) | ✅ | — | ✅ |
286
+ | `pressed` | ✅ (toggle) | ✅ (toggle input) | — | — | — |
287
+ | `expanded` | ✅ (disclosure trigger) | ✅ (combobox) | ✅ (collapsible nav) | — | ✅ (accordion) |
288
+ | `checked` | — | ✅ (two-state control) | — | — | — |
289
+ | `indeterminate` | — | ✅ | — | — | — |
290
+ | `current` | — | — | ✅ (active route) | — | — |
291
+ | `visited` | — | — | ✅ | — | ✅ (link in content) |
292
+ | `droptarget` | — | ✅ (file input) | — | — | ✅ |
293
+
294
+ State disambiguation rules:
295
+ - **`active` vs `pressed`**: `active` = the brief moment of clicking (transient). `pressed` = persistent toggle engaged ("Bold" button). Never use `active` for toggle state.
296
+ - **`selected` vs `checked`**: `selected` = one-of-many in a set (segment, picker option). `checked` = two-state on/off (checkbox, radio, switch).
297
+ - **`selected` vs `current`**: `selected` = set membership. `current` = the user's actual location in the navigation set.
298
+ - **Validation failure is not a state**: an invalid input renders with the `input.negative.*` role, not with an `invalid` state on `input.primary.*`.
299
+
300
+ ## Co-occurrence rules (cross-family)
301
+
302
+ These rules cannot be expressed in a single token's JSDoc — they are obligations between families.
303
+
304
+ **§1 — Every focusable element needs a focus indicator.** Pick one:
305
+ - Component has a clear FSL Entity Kind (`Action`, `Input`, `Navigation`, `Feedback`) → use `vars.colors.{ux}.{role}.border.focused`.
306
+ - Component has no clear `{ux}` (focusable card, custom widget) → use `vars.focus.ring.{width,style,color}`.
307
+ - Input with `negative` or `caution` valence where focus must inherit valence → `vars.colors.input.{negative|caution}.border.focused` overrides the system default.
308
+
309
+ Never both. Never neither.
310
+
311
+ **§2 — `colors.background` requires a paired `colors.text`.** Whenever you set a semantic background, set the matching `colors.{ux}.{role}.text.{state}` for the contained text. Mixing a `feedback.positive.background.default` with an unrelated text token breaks the contrast contract.
312
+
313
+ **§3 — `opacity.disabled` is for visual media only.** Disabled controls and disabled text **must** use `vars.colors.{ux}.{role}.{dimension}.disabled` color tokens (which carry contrast guarantees). `vars.opacity.disabled` is exclusive to images, avatars, illustrations — assets without a color contract.
314
+
315
+ **§4 — `sizing.hit.*` is enforced via `min-*`, not visual size.** Apply as `min-width` / `min-height`. The CSS layer auto-injects coarse-pointer overrides under `@media (any-pointer: coarse)` — do not write that media query yourself.
316
+
317
+ **§5 — `tonal` elevation requires its shadow counterpart.** Using `vars.elevation.tonal.raised` is only valid when `vars.elevation.surface.raised` is also applied to the same element. Tonal alone breaks depth perception when the theme switches modes.
318
+
319
+ **§6 — Density propagates across families.** A `sm` / `md` / `lg` decision must be applied uniformly across `spacing.inset.*`, `spacing.gap.*`, `sizing.icon.*`, and `text.label.*` for the same component. Mixing `spacing.inset.control.sm` with `sizing.icon.lg` produces visually inconsistent components.
320
+
321
+ **§7 — `motion` auto-suppression.** `prefers-reduced-motion` is handled by the CSS pipeline emitted by `toCssVars` — never re-implement reduced-motion logic in component code.
322
+
323
+ **§8 — Optional families have fallback rules:**
324
+ - `elevation.tonal` absent → fall back to `elevation.surface` alone.
325
+ - `dataviz` absent → the consumer's app does not opt in to dataviz; do not generate dataviz components.
326
+ - Optional state (e.g. `hover`) absent on a leaf → fall back to `default`.
327
+
328
+ ## Disambiguation — confusable picks
329
+
330
+ For each common LLM mistake, the wrong pick → the correct pick + the rule that disambiguates:
331
+
332
+ | User intent | Tempting (wrong) | Correct | Rule |
333
+ | --- | --- | --- | --- |
334
+ | Page background | `core.colors.neutral.0` | `vars.colors.informational.primary.background.default` | Components never consume `core.*`. |
335
+ | Cancel / dismiss button | `vars.colors.action.negative.*` | `vars.colors.action.muted.*` (or `.secondary.*`) | `negative` = *destructive* intent (delete). Low priority is `muted`. |
336
+ | Delete / destroy button | `vars.colors.feedback.negative.*` | `vars.colors.action.negative.*` | Triggers → `action`. Reporting an outcome → `feedback`. |
337
+ | Success toast | `vars.colors.informational.positive.*` | `vars.colors.feedback.positive.*` | System *reports* a result → `feedback`. Passive surface → `informational`. |
338
+ | Warning text inside a card | `vars.colors.feedback.caution.*` | `vars.colors.informational.caution.*` | If it's part of static content (not a system event), it's `informational`. |
339
+ | Form field with validation error | `input.primary.*` + `invalid` state | `vars.colors.input.negative.*` | Validation failure is a *role*, not a state. (FSL Lexicon §5) |
340
+ | Disabled icon button | `opacity: vars.opacity.disabled` | `color: vars.colors.action.{role}.text.disabled` | §3 — controls/text carry contrast; only assets without color contract use `opacity.disabled`. |
341
+ | Active tab indicator | `border.outline.selected` *only* | `border.outline.selected` + `colors.navigation.{role}.border.current` | Width via `border.outline.selected`; color via the `current` state. |
342
+ | Toggle button engaged | `state: active` | `state: pressed` | `active` is transient (mouse down); `pressed` is persistent toggle. |
343
+ | Focus on input in error | `focus.ring.color` | `vars.colors.input.negative.border.focused` | §1 — valence inputs override the system default. |
344
+
345
+ ## Focus ring — implementation patterns
346
+
347
+ `:focus-visible` cannot be expressed in inline styles. Use one of:
348
+
349
+ **Option A — CSS class (canonical)**
350
+
351
+ ```css
352
+ /* button.css (or a global stylesheet) */
353
+ /* §1 — action ux: per-context focused border token */
354
+ .btn:focus-visible {
355
+ outline-width: var(--tt-focus-ring-width);
356
+ outline-style: var(--tt-focus-ring-style);
357
+ outline-color: var(--tt-colors-action-primary-border-focused);
358
+ outline-offset: 2px;
359
+ }
360
+ .btn:hover:not(:disabled) {
361
+ background: var(--tt-colors-action-primary-background-hover);
362
+ }
363
+ .btn:active:not(:disabled) {
364
+ background: var(--tt-colors-action-primary-background-active);
365
+ }
366
+
367
+ /* No clear ux (focusable card, custom widget) — system default ring */
368
+ .focusable-card:focus-visible {
369
+ outline-width: var(--tt-focus-ring-width);
370
+ outline-style: var(--tt-focus-ring-style);
371
+ outline-color: var(--tt-focus-ring-color);
372
+ outline-offset: 2px;
373
+ }
374
+ ```
375
+
376
+ **Option B — CSS-in-JS / Ark UI / React Aria**
377
+
378
+ Headless libraries (Ark UI, React Aria) manage `data-focused` / `data-hovered` attributes automatically. Wire them to token vars in your CSS-in-JS framework.
379
+
380
+ ---
381
+
382
+ ## Component pattern — complete example
383
+
384
+ ```tsx
385
+ import { vars } from '@ttoss/fsl-theme/vars';
386
+ import './button.css'; // contains :focus-visible, :hover, :active rules (see above)
387
+
388
+ // Applies: §1 (focus via CSS), §2 (bg+text pair), §3 (disabled via color token not opacity),
389
+ // §4 (hit target via min-height), §6 (density uniform across padding/sizing/label)
390
+ function PrimaryButton({
391
+ children,
392
+ disabled,
393
+ ...props
394
+ }: React.ButtonHTMLAttributes<HTMLButtonElement>) {
395
+ return (
396
+ <button
397
+ {...props}
398
+ disabled={disabled}
399
+ className="btn"
400
+ style={{
401
+ // §2 — background + text always paired
402
+ background: disabled
403
+ ? vars.colors.action.primary.background.disabled
404
+ : vars.colors.action.primary.background.default,
405
+ color: disabled
406
+ ? vars.colors.action.primary.text.disabled // §3: color token, not opacity
407
+ : vars.colors.action.primary.text.default,
408
+ // §6 — density uniform: inset / sizing / typography all md
409
+ padding: `0 ${vars.spacing.inset.control.md}`,
410
+ minHeight: vars.sizing.hit.base, // §4: min-height enforces hit target
411
+ // typography — each TextStyle field is its own var() string
412
+ fontFamily: vars.text.label.md.fontFamily,
413
+ fontSize: vars.text.label.md.fontSize,
414
+ fontWeight: vars.text.label.md.fontWeight,
415
+ lineHeight: vars.text.label.md.lineHeight,
416
+ letterSpacing: vars.text.label.md.letterSpacing,
417
+ // surface
418
+ borderRadius: vars.radii.control,
419
+ borderWidth: vars.border.outline.control.width,
420
+ borderStyle: vars.border.outline.control.style,
421
+ borderColor: disabled
422
+ ? vars.colors.action.primary.border.disabled
423
+ : vars.colors.action.primary.border.default,
424
+ // motion
425
+ transitionDuration: vars.motion.feedback.duration,
426
+ transitionTimingFunction: vars.motion.feedback.easing,
427
+ transitionProperty: 'background, color, border-color',
428
+ // layout
429
+ display: 'inline-flex',
430
+ alignItems: 'center',
431
+ gap: vars.spacing.gap.inline.sm,
432
+ cursor: disabled ? 'not-allowed' : 'pointer',
433
+ }}
434
+ >
435
+ {children}
436
+ </button>
437
+ );
438
+ }
439
+ ```
440
+
441
+ ## Pattern: Input with validation error
442
+
443
+ New vs. Button: validation is a *role swap*, not a state (`input.primary.*` → `input.negative.*`); focus inherits valence (§1 valence override); the validation message text consumes the same negative role.
444
+
445
+ ```tsx
446
+ function TextField({
447
+ invalid,
448
+ message,
449
+ ...props
450
+ }: React.InputHTMLAttributes<HTMLInputElement> & { invalid?: boolean; message?: string }) {
451
+ const role = invalid ? 'negative' : 'primary'; // role swap, not state
452
+ return (
453
+ <div style={{ display: 'flex', flexDirection: 'column', gap: vars.spacing.gap.stack.xs }}>
454
+ <input
455
+ {...props}
456
+ aria-invalid={invalid || undefined}
457
+ className={invalid ? 'input input--negative' : 'input'}
458
+ style={{
459
+ background: vars.colors.input[role].background.default,
460
+ color: vars.colors.input[role].text.default,
461
+ borderColor: vars.colors.input[role].border.default,
462
+ borderWidth: vars.border.outline.control.width,
463
+ borderStyle: vars.border.outline.control.style,
464
+ borderRadius: vars.radii.control,
465
+ minHeight: vars.sizing.hit.base,
466
+ padding: `0 ${vars.spacing.inset.control.md}`,
467
+ fontSize: vars.text.body.md.fontSize,
468
+ lineHeight: vars.text.body.md.lineHeight,
469
+ }}
470
+ />
471
+ {message && (
472
+ <span style={{
473
+ color: vars.colors.input.negative.text.default, // negative role serves both control + message
474
+ fontSize: vars.text.label.sm.fontSize,
475
+ }}>
476
+ {message}
477
+ </span>
478
+ )}
479
+ </div>
480
+ );
481
+ }
482
+ ```
483
+
484
+ ```css
485
+ .input:focus-visible { outline-color: var(--tt-colors-input-primary-border-focused); }
486
+ .input--negative:focus-visible { outline-color: var(--tt-colors-input-negative-border-focused); /* §1 valence override */ }
487
+ ```
488
+
489
+ ## Pattern: Modal dialog (five families coordinated)
490
+
491
+ New vs. Button: scrim composes `overlay.scrim` (color) with `opacity.scrim` (alpha); blocking surface uses `elevation.surface.blocking` + `zIndex.layer.blocking`; enter/exit motion is `motion.transition.*` (not `motion.feedback`).
492
+
493
+ ```tsx
494
+ import { useResolvedTokens } from '@ttoss/fsl-theme/react';
495
+
496
+ function Modal({ children }: { children: React.ReactNode }) {
497
+ // Numeric z-index needed for arithmetic (scrim = blocking - 1)
498
+ const resolved = useResolvedTokens();
499
+ const blockingZ = Number(resolved['semantic.zIndex.layer.blocking']);
500
+
501
+ return (
502
+ <>
503
+ {/* Scrim: dim everything behind */}
504
+ <div style={{
505
+ position: 'fixed', inset: 0,
506
+ background: vars.overlay.scrim,
507
+ opacity: vars.opacity.scrim,
508
+ zIndex: blockingZ - 1,
509
+ }} />
510
+ {/* Dialog surface: blocking stratum, max depth */}
511
+ <div role="dialog" aria-modal="true" style={{
512
+ position: 'fixed', top: '50%', left: '50%', transform: 'translate(-50%, -50%)',
513
+ zIndex: blockingZ,
514
+ background: vars.colors.informational.primary.background.default,
515
+ color: vars.colors.informational.primary.text.default,
516
+ boxShadow: vars.elevation.surface.blocking,
517
+ borderRadius: vars.radii.surface,
518
+ padding: vars.spacing.inset.surface.md,
519
+ maxWidth: vars.sizing.surface.maxWidth,
520
+ // enter/exit motion — transition.* not feedback.*
521
+ transitionProperty: 'opacity, transform',
522
+ transitionDuration: vars.motion.transition.enter.duration,
523
+ transitionTimingFunction: vars.motion.transition.enter.easing,
524
+ }}>
525
+ {children}
526
+ </div>
527
+ </>
528
+ );
529
+ }
530
+ ```
531
+
532
+ ---
533
+
534
+ ## Self-audit checklist (run before emitting code)
535
+
536
+ For every component you generate, verify:
537
+
538
+ 1. **Path validity** — every `vars.colors.*` path exists in the [role matrix](#fsl-color-axes); no `action.positive`, no `feedback.accent`, no `navigation.{positive,caution,negative}`.
539
+ 2. **Focus** (§1) — every focusable element has *exactly one* of: per-ux `{ux}.{role}.border.focused`, system `focus.ring.*`, or valence-aware input override. Never both. Never neither.
540
+ 3. **Pairing** (§2) — every `colors.{ux}.{role}.background.{state}` is paired with `colors.{ux}.{role}.text.{state}` at the same state.
541
+ 4. **Disabled** (§3) — disabled controls and text use `colors.{ux}.{role}.{dimension}.disabled`; `opacity.disabled` appears only on `<img>`, `<Avatar>`, illustrations.
542
+ 5. **Hit target** (§4) — interactive elements set `minWidth` or `minHeight` (not `width`/`height`) to `vars.sizing.hit.*`.
543
+ 6. **Density** (§6) — the same step (`sm`/`md`/`lg`) is used across `spacing.inset.*`, `spacing.gap.*`, `sizing.icon.*`, and `text.label.*` within the component.
544
+ 7. **Validation** — invalid inputs use `input.negative.*` *role*, never `input.primary.*` + a fictitious `invalid` state.
545
+ 8. **Stateful CSS** — `:hover`, `:focus-visible`, `:active`, `:disabled` are written in a `.css` file or CSS-in-JS; inline `style` cannot express pseudo-classes.
546
+ 9. **No `core.*` consumption** — the component imports `vars` only; `core.*` paths appear only inside `createTheme({ overrides })`.
547
+ 10. **Mode-agnostic** — the component consumes `semantic.*` only; no `useColorMode()` branching to swap tokens (the cascade handles it).
548
+
549
+ ---
550
+
551
+ ## Mode switching
552
+
553
+ ```tsx
554
+ import { useColorMode } from '@ttoss/fsl-theme/react';
555
+
556
+ // Must be a descendant of <ThemeProvider>
557
+ const DarkModeToggle = () => {
558
+ const { mode, resolvedMode, setMode } = useColorMode();
559
+ return (
560
+ <button onClick={() => setMode(mode === 'dark' ? 'light' : 'dark')}>
561
+ {resolvedMode === 'dark' ? 'Switch to light' : 'Switch to dark'}
562
+ </button>
563
+ );
564
+ };
565
+ ```
566
+
567
+ `mode` = user intent: `'light' | 'dark' | 'system'`. `resolvedMode` = actual rendered mode: `'light' | 'dark'`. Persisted automatically to `localStorage` (`'tt-theme'` key by default).
568
+
569
+ ---
570
+
571
+ ## Non-CSS environments — React Native, canvas, PDF
572
+
573
+ `vars.*` produces CSS custom property strings — they resolve only in a browser with `ThemeProvider`. For non-CSS environments, use `useResolvedTokens()`:
574
+
575
+ ```tsx
576
+ import { useResolvedTokens } from '@ttoss/fsl-theme/react';
577
+
578
+ // Keys are full semantic dot-paths; values are resolved raw CSS values (hex, px, etc.)
579
+ const NativeButton = ({ disabled }: { disabled?: boolean }) => {
580
+ const resolved = useResolvedTokens();
581
+ return (
582
+ <View
583
+ style={{
584
+ backgroundColor: disabled
585
+ ? resolved['semantic.colors.action.primary.background.disabled']
586
+ : resolved['semantic.colors.action.primary.background.default'],
587
+ minHeight: resolved['semantic.sizing.hit.base'],
588
+ paddingHorizontal: resolved['semantic.spacing.inset.control.md'],
589
+ }}
590
+ />
591
+ );
592
+ };
593
+ ```
594
+
595
+ `useResolvedTokens()` automatically applies coarse-pointer hit target overrides — mirrors the `@media (any-pointer: coarse)` block that `toCssVars` injects for CSS consumers.
596
+
597
+ ---
598
+
599
+ # JOB 2 — Customizing a theme
600
+
601
+ `createTheme({ overrides })` accepts a `DeepPartial<ThemeTokens>` — TypeScript validates *shape*, not semantic coherence. The intent → override map below specifies the minimal correct set of paths to change for each common design intent, and the invariants to verify after.
602
+
603
+ ## API
604
+
605
+ ```ts
606
+ import { createTheme } from '@ttoss/fsl-theme';
607
+
608
+ const theme = createTheme({
609
+ extends?: ThemeBundle, // built-in theme to extend
610
+ baseMode?: 'light' | 'dark', // which mode the base represents
611
+ overrides?: DeepPartial<ThemeTokens>, // changes to the base
612
+ alternate?: ModeOverride | null, // dark alternate (null = single-mode)
613
+ });
614
+ ```
615
+
616
+ - Default: `createTheme()` returns the built-in `baseTheme` + built-in `darkAlternate`.
617
+ - `extends`: inherit from a built-in (`bruttal`, `corporate`, `oca`, `ventures`).
618
+ - `alternate: null`: opt out of dark mode entirely (single-mode theme).
619
+
620
+ ## Intent → override map
621
+
622
+ ### Apply brand colors
623
+
624
+ **Override path:** `core.colors.brand.{50..900}` — set the full scale.
625
+
626
+ ```ts
627
+ createTheme({
628
+ overrides: {
629
+ core: { colors: { brand: { 50: '#…', 100: '#…', /* … */ 900: '#…' } } },
630
+ },
631
+ });
632
+ ```
633
+
634
+ **Invariants to verify:**
635
+ - `brand.500` is the canonical mid-point and is required. Tune it for AA contrast against `neutral.0` if it is used as a filled background with light text (see `bruttal.ts` for the worked example: `brand.500 = #6D5D4F` ≈ 7.3:1 contrast).
636
+ - All semantic tokens that reference `{core.colors.brand.*}` automatically pick up the new values. No semantic remap needed unless contrast breaks.
637
+ - For dark mode, the built-in `darkAlternate` already remaps semantic references to lighter brand steps — usually no extra work.
638
+
639
+ **Reference patterns:** `corporate.ts`, `oca.ts`, `ventures.ts` are minimal brand-only overrides.
640
+
641
+ ### Adjust density
642
+
643
+ **Override paths (all together):**
644
+ - `core.spacing.{1..16}` — base spacing scale steps
645
+ - `core.sizing.hit.fine.{min,base,prominent}` — fine-pointer hit targets
646
+ - `core.sizing.icon.*` — icon ramp
647
+ - `core.font.size.*` (optional) — type scale
648
+
649
+ **Invariants:**
650
+ - Touch targets (`core.sizing.hit.coarse.*`) must remain ≥ 44px regardless of fine-pointer values (a11y minimum).
651
+ - Density propagates uniformly: if you reduce `core.spacing.4` by 25%, reduce other spacing steps proportionally to preserve the scale ratios.
652
+ - Hit target floors in `clamp()` expressions must remain ergonomic (≥ 32px for fine pointer).
653
+
654
+ ### Flat / no-shadow style
655
+
656
+ **Override paths:**
657
+ - `semantic.radii.{control,surface}` → `'{core.radii.none}'` (sharp corners)
658
+ - `semantic.elevation.surface.{flat,raised,overlay,blocking}` → all → `'{core.elevation.level.0}'`
659
+ - `alternate.semantic.elevation.surface.*` → all → `'{core.elevation.emphatic.0}'` (preserves flatness in dark mode)
660
+
661
+ **Reference pattern:** `bruttal.ts` is the canonical implementation — copy its `semantic.radii` + `semantic.elevation.surface` + `alternate.semantic.elevation.surface` blocks together.
662
+
663
+ **Invariant:** If you flatten elevation in `base`, you must also flatten in `alternate` — otherwise dark mode will render shadows that contradict the visual identity.
664
+
665
+ ### Custom typography
666
+
667
+ **Override paths (likely all together):**
668
+ - `core.font.family.{sans,mono,serif?}` — font stack
669
+ - `core.font.weight.*` — verify the new typeface supports the weights used
670
+ - `core.font.leading.*` (optional) — adjust if the typeface needs different line heights
671
+ - `core.font.tracking.*` (optional) — letter-spacing per typeface
672
+
673
+ **Invariants:**
674
+ - A new typeface may not support all referenced weights — verify `regular`, `medium`, `semibold`, `bold` all exist in the chosen font.
675
+ - `core.font.leading.normal` is calibrated for the default `sans`; some typefaces need wider leading to remain legible.
676
+ - Do **not** override `semantic.text.*` styles unless changing the *intent* of a text role. Family swaps live in `core.font.family.sans`; semantic styles inherit automatically.
677
+
678
+ ### Custom dark mode
679
+
680
+ **Override path:** `alternate.semantic.*` only. **Never** `alternate.core.*` — core is immutable across modes (model invariant §1 / §4).
681
+
682
+ ```ts
683
+ createTheme({
684
+ overrides: { core: { colors: { brand: { 500: '#…' } } } },
685
+ alternate: {
686
+ semantic: {
687
+ colors: {
688
+ informational: {
689
+ primary: { background: { default: '{core.colors.neutral.900}' } },
690
+ },
691
+ },
692
+ },
693
+ },
694
+ });
695
+ ```
696
+
697
+ **Invariants:**
698
+ - Modes remap **semantic references**, never core values. If a semantic token needs a value that doesn't exist in core, enrich `core` (extending the palette), don't mutate it per mode.
699
+ - Validate every semantic *pairing* (text on background, border on adjacent surface) in both modes — not isolated swatches.
700
+ - Remap neutrals first; remap brand/hue only when contrast or legibility breaks.
701
+
702
+ ### Single-mode theme (no dark)
703
+
704
+ ```ts
705
+ createTheme({ alternate: null });
706
+ ```
707
+
708
+ ## What you must NOT do when customizing
709
+
710
+ - **Never** consume `core.*` directly from a component. Core is theme-definition only.
711
+ - **Never** create parallel semantic vocabulary (e.g. `semantic.colors.brandDark`, `semantic.text.buttonLarge`). If the meaning exists, reuse it; if it truly doesn't, follow governance to add it.
712
+ - **Never** mutate `core.*` per mode. Modes remap semantic references only.
713
+ - **Never** name semantic tokens by appearance (hue, raw style, component, mode). Names express *intent*.
714
+ - **Never** override `semantic.*` to reference a raw value — always reference a `{core.*}` token. Raw values in semantic are a registered exception list (see `model.md`), not a customization path.
715
+
716
+ ---
717
+
718
+ ## Validation in development
719
+
720
+ In non-production builds, `createTheme` runs `validateRefs` over the merged theme — broken `{core.*}` references throw early. In production builds the validation is stripped for performance.
721
+
722
+ Cross-token semantic correctness (the rules above) is **not** statically validated today. The rules in this file are the authoritative checklist for an LLM producing component or customization code.
723
+
724
+ ---
725
+
726
+ ## Reference index
727
+
728
+ - README — full grammar tables, usage examples per import path
729
+ - `dist/Types-*.d.ts` — every token has co-located JSDoc with semantics + anti-pattern notes
730
+ - `src/themes/{bruttal,corporate,oca,ventures}.ts` — worked customization patterns
731
+ - Family specs (full grammar, decision matrices, ADRs): `docs/design/01-design-system/02-design-tokens/02-families/{family}.md` in the ttoss monorepo