@ttoss/fsl-ui 0.2.4

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 (174) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +76 -0
  3. package/dist/_virtual/_rolldown/runtime.cjs +30 -0
  4. package/dist/components/Breadcrumbs/Breadcrumbs.cjs +129 -0
  5. package/dist/components/Breadcrumbs/Breadcrumbs.d.cts +61 -0
  6. package/dist/components/Breadcrumbs/Breadcrumbs.d.mts +61 -0
  7. package/dist/components/Breadcrumbs/Breadcrumbs.mjs +126 -0
  8. package/dist/components/Button/Button.cjs +80 -0
  9. package/dist/components/Button/Button.d.cts +68 -0
  10. package/dist/components/Button/Button.d.mts +68 -0
  11. package/dist/components/Button/Button.mjs +79 -0
  12. package/dist/components/Checkbox/Checkbox.cjs +183 -0
  13. package/dist/components/Checkbox/Checkbox.d.cts +45 -0
  14. package/dist/components/Checkbox/Checkbox.d.mts +45 -0
  15. package/dist/components/Checkbox/Checkbox.mjs +182 -0
  16. package/dist/components/CheckboxGroup/CheckboxGroup.cjs +97 -0
  17. package/dist/components/CheckboxGroup/CheckboxGroup.d.cts +56 -0
  18. package/dist/components/CheckboxGroup/CheckboxGroup.d.mts +56 -0
  19. package/dist/components/CheckboxGroup/CheckboxGroup.mjs +96 -0
  20. package/dist/components/FileTrigger/FileTrigger.cjs +51 -0
  21. package/dist/components/FileTrigger/FileTrigger.d.cts +52 -0
  22. package/dist/components/FileTrigger/FileTrigger.d.mts +52 -0
  23. package/dist/components/FileTrigger/FileTrigger.mjs +50 -0
  24. package/dist/components/GridList/GridList.cjs +220 -0
  25. package/dist/components/GridList/GridList.d.cts +72 -0
  26. package/dist/components/GridList/GridList.d.mts +72 -0
  27. package/dist/components/GridList/GridList.mjs +217 -0
  28. package/dist/components/Group/Group.cjs +77 -0
  29. package/dist/components/Group/Group.d.cts +61 -0
  30. package/dist/components/Group/Group.d.mts +61 -0
  31. package/dist/components/Group/Group.mjs +74 -0
  32. package/dist/components/Icon/Icon.cjs +52 -0
  33. package/dist/components/Icon/Icon.mjs +52 -0
  34. package/dist/components/Icon/glyphs.cjs +72 -0
  35. package/dist/components/Icon/glyphs.mjs +63 -0
  36. package/dist/components/Link/Link.cjs +62 -0
  37. package/dist/components/Link/Link.d.cts +38 -0
  38. package/dist/components/Link/Link.d.mts +38 -0
  39. package/dist/components/Link/Link.mjs +61 -0
  40. package/dist/components/ListBox/ListBox.cjs +126 -0
  41. package/dist/components/ListBox/ListBox.d.cts +63 -0
  42. package/dist/components/ListBox/ListBox.d.mts +63 -0
  43. package/dist/components/ListBox/ListBox.mjs +123 -0
  44. package/dist/components/Meter/Meter.cjs +126 -0
  45. package/dist/components/Meter/Meter.d.cts +63 -0
  46. package/dist/components/Meter/Meter.d.mts +63 -0
  47. package/dist/components/Meter/Meter.mjs +125 -0
  48. package/dist/components/NumberField/NumberField.cjs +221 -0
  49. package/dist/components/NumberField/NumberField.d.cts +68 -0
  50. package/dist/components/NumberField/NumberField.d.mts +68 -0
  51. package/dist/components/NumberField/NumberField.mjs +220 -0
  52. package/dist/components/ProgressBar/ProgressBar.cjs +132 -0
  53. package/dist/components/ProgressBar/ProgressBar.d.cts +56 -0
  54. package/dist/components/ProgressBar/ProgressBar.d.mts +56 -0
  55. package/dist/components/ProgressBar/ProgressBar.mjs +129 -0
  56. package/dist/components/RadioGroup/RadioGroup.cjs +218 -0
  57. package/dist/components/RadioGroup/RadioGroup.d.cts +82 -0
  58. package/dist/components/RadioGroup/RadioGroup.d.mts +82 -0
  59. package/dist/components/RadioGroup/RadioGroup.mjs +215 -0
  60. package/dist/components/Select/Select.cjs +253 -0
  61. package/dist/components/Select/Select.d.cts +86 -0
  62. package/dist/components/Select/Select.d.mts +86 -0
  63. package/dist/components/Select/Select.mjs +250 -0
  64. package/dist/components/Separator/Separator.cjs +64 -0
  65. package/dist/components/Separator/Separator.d.cts +44 -0
  66. package/dist/components/Separator/Separator.d.mts +44 -0
  67. package/dist/components/Separator/Separator.mjs +63 -0
  68. package/dist/components/Slider/Slider.cjs +160 -0
  69. package/dist/components/Slider/Slider.d.cts +45 -0
  70. package/dist/components/Slider/Slider.d.mts +45 -0
  71. package/dist/components/Slider/Slider.mjs +159 -0
  72. package/dist/components/Switch/Switch.cjs +176 -0
  73. package/dist/components/Switch/Switch.d.cts +43 -0
  74. package/dist/components/Switch/Switch.d.mts +43 -0
  75. package/dist/components/Switch/Switch.mjs +175 -0
  76. package/dist/components/Tabs/Tabs.cjs +180 -0
  77. package/dist/components/Tabs/Tabs.d.cts +85 -0
  78. package/dist/components/Tabs/Tabs.d.mts +85 -0
  79. package/dist/components/Tabs/Tabs.mjs +173 -0
  80. package/dist/components/Toast/Toast.cjs +218 -0
  81. package/dist/components/Toast/Toast.d.cts +96 -0
  82. package/dist/components/Toast/Toast.d.mts +96 -0
  83. package/dist/components/Toast/Toast.mjs +214 -0
  84. package/dist/components/ToggleButton/ToggleButton.cjs +125 -0
  85. package/dist/components/ToggleButton/ToggleButton.d.cts +43 -0
  86. package/dist/components/ToggleButton/ToggleButton.d.mts +43 -0
  87. package/dist/components/ToggleButton/ToggleButton.mjs +124 -0
  88. package/dist/components/ToggleButtonGroup/ToggleButtonGroup.cjs +48 -0
  89. package/dist/components/ToggleButtonGroup/ToggleButtonGroup.d.cts +41 -0
  90. package/dist/components/ToggleButtonGroup/ToggleButtonGroup.d.mts +41 -0
  91. package/dist/components/ToggleButtonGroup/ToggleButtonGroup.mjs +47 -0
  92. package/dist/components/Toolbar/Toolbar.cjs +67 -0
  93. package/dist/components/Toolbar/Toolbar.d.cts +60 -0
  94. package/dist/components/Toolbar/Toolbar.d.mts +60 -0
  95. package/dist/components/Toolbar/Toolbar.mjs +66 -0
  96. package/dist/composites/Accordion/Accordion.cjs +279 -0
  97. package/dist/composites/Accordion/Accordion.d.cts +176 -0
  98. package/dist/composites/Accordion/Accordion.d.mts +176 -0
  99. package/dist/composites/Accordion/Accordion.mjs +270 -0
  100. package/dist/composites/ConfirmationDialog/ConfirmationDialog.cjs +206 -0
  101. package/dist/composites/ConfirmationDialog/ConfirmationDialog.d.cts +166 -0
  102. package/dist/composites/ConfirmationDialog/ConfirmationDialog.d.mts +166 -0
  103. package/dist/composites/ConfirmationDialog/ConfirmationDialog.mjs +203 -0
  104. package/dist/composites/Dialog/Dialog.cjs +372 -0
  105. package/dist/composites/Dialog/Dialog.d.cts +234 -0
  106. package/dist/composites/Dialog/Dialog.d.mts +234 -0
  107. package/dist/composites/Dialog/Dialog.mjs +360 -0
  108. package/dist/composites/Disclosure/Disclosure.cjs +214 -0
  109. package/dist/composites/Disclosure/Disclosure.d.cts +118 -0
  110. package/dist/composites/Disclosure/Disclosure.d.mts +118 -0
  111. package/dist/composites/Disclosure/Disclosure.mjs +207 -0
  112. package/dist/composites/Form/Form.cjs +161 -0
  113. package/dist/composites/Form/Form.d.cts +176 -0
  114. package/dist/composites/Form/Form.d.mts +176 -0
  115. package/dist/composites/Form/Form.mjs +156 -0
  116. package/dist/composites/Menu/Menu.cjs +190 -0
  117. package/dist/composites/Menu/Menu.d.cts +158 -0
  118. package/dist/composites/Menu/Menu.d.mts +158 -0
  119. package/dist/composites/Menu/Menu.mjs +186 -0
  120. package/dist/composites/Popover/Popover.cjs +80 -0
  121. package/dist/composites/Popover/Popover.d.cts +56 -0
  122. package/dist/composites/Popover/Popover.d.mts +56 -0
  123. package/dist/composites/Popover/Popover.mjs +78 -0
  124. package/dist/composites/SearchField/SearchField.cjs +194 -0
  125. package/dist/composites/SearchField/SearchField.d.cts +81 -0
  126. package/dist/composites/SearchField/SearchField.d.mts +81 -0
  127. package/dist/composites/SearchField/SearchField.mjs +189 -0
  128. package/dist/composites/TagGroup/TagGroup.cjs +212 -0
  129. package/dist/composites/TagGroup/TagGroup.d.cts +84 -0
  130. package/dist/composites/TagGroup/TagGroup.d.mts +84 -0
  131. package/dist/composites/TagGroup/TagGroup.mjs +209 -0
  132. package/dist/composites/TextArea/TextArea.cjs +190 -0
  133. package/dist/composites/TextArea/TextArea.d.cts +95 -0
  134. package/dist/composites/TextArea/TextArea.d.mts +95 -0
  135. package/dist/composites/TextArea/TextArea.mjs +181 -0
  136. package/dist/composites/TextField/TextField.cjs +200 -0
  137. package/dist/composites/TextField/TextField.d.cts +111 -0
  138. package/dist/composites/TextField/TextField.d.mts +111 -0
  139. package/dist/composites/TextField/TextField.mjs +191 -0
  140. package/dist/composites/Tooltip/Tooltip.cjs +79 -0
  141. package/dist/composites/Tooltip/Tooltip.d.cts +53 -0
  142. package/dist/composites/Tooltip/Tooltip.d.mts +53 -0
  143. package/dist/composites/Tooltip/Tooltip.mjs +77 -0
  144. package/dist/composites/Wizard/Wizard.cjs +333 -0
  145. package/dist/composites/Wizard/Wizard.d.cts +206 -0
  146. package/dist/composites/Wizard/Wizard.d.mts +206 -0
  147. package/dist/composites/Wizard/Wizard.mjs +324 -0
  148. package/dist/composites/scope.cjs +124 -0
  149. package/dist/composites/scope.mjs +121 -0
  150. package/dist/index.cjs +186 -0
  151. package/dist/index.d.cts +37 -0
  152. package/dist/index.d.mts +37 -0
  153. package/dist/index.mjs +39 -0
  154. package/dist/semantics/componentMeta.d.cts +37 -0
  155. package/dist/semantics/componentMeta.d.mts +37 -0
  156. package/dist/semantics/index.cjs +16 -0
  157. package/dist/semantics/index.d.cts +4 -0
  158. package/dist/semantics/index.d.mts +4 -0
  159. package/dist/semantics/index.mjs +3 -0
  160. package/dist/semantics/taxonomy.cjs +150 -0
  161. package/dist/semantics/taxonomy.d.cts +153 -0
  162. package/dist/semantics/taxonomy.d.mts +153 -0
  163. package/dist/semantics/taxonomy.mjs +140 -0
  164. package/dist/tokens/escapeHatch.cjs +13 -0
  165. package/dist/tokens/escapeHatch.mjs +13 -0
  166. package/dist/tokens/focusRing.cjs +28 -0
  167. package/dist/tokens/focusRing.mjs +28 -0
  168. package/dist/tokens/keyframes.cjs +76 -0
  169. package/dist/tokens/keyframes.mjs +75 -0
  170. package/dist/tokens/resolveInteractiveStyle.cjs +41 -0
  171. package/dist/tokens/resolveInteractiveStyle.mjs +41 -0
  172. package/llms.txt +142 -0
  173. package/package.json +70 -0
  174. package/src/tokens/CONTRACT.md +505 -0
@@ -0,0 +1,505 @@
1
+ # Token Contract
2
+
3
+ > **Purpose**: Given an Entity, this document tells you exactly which `vars.*` paths to use,
4
+ > how to construct the full token path, and how to wire state and evaluation.
5
+ >
6
+ > This is the Layer 2 boundary artifact.
7
+ > For token family semantics (rules, valid values, responsive behaviour):
8
+ > see `@ttoss/fsl-theme` `Types.ts` and `/docs/design/design-system/design-tokens/`.
9
+
10
+ ```typescript
11
+ import { vars } from '@ttoss/fsl-theme/vars';
12
+ import type { ComponentMeta, EvaluationsFor } from '@ttoss/fsl-ui/semantics';
13
+ ```
14
+
15
+ ---
16
+
17
+ ## §0 — Component Implementation Pattern
18
+
19
+ Every component follows these four steps in order.
20
+
21
+ ### Step 1 — Declare semantic identity (Layer 1)
22
+
23
+ ```typescript
24
+ import type { ComponentMeta } from '../../semantics';
25
+
26
+ export const fooMeta = {
27
+ displayName: 'Foo',
28
+ entity: 'Action', // Entity from taxonomy.ts — drives everything below
29
+ structure: 'root', // StructuresFor<'Action'>
30
+ } as const satisfies ComponentMeta<'Action'>;
31
+ ```
32
+
33
+ `entity` is the key: it determines which row of §1 to read.
34
+
35
+ ### Step 2 — Derive valid evaluations (Layer 1 → type system)
36
+
37
+ ```typescript
38
+ import type { EvaluationsFor } from '../../semantics';
39
+
40
+ // Type is derived — no manual union to maintain.
41
+ // Source of truth: ENTITY_EVALUATION in taxonomy.ts.
42
+ type FooEvaluation = EvaluationsFor<(typeof fooMeta)['entity']>;
43
+ // → 'primary' | 'secondary' | 'accent' | 'muted' | 'negative'
44
+ ```
45
+
46
+ `evaluation` and `consequence` are orthogonal: `consequence: 'destructive'`
47
+ (FSL §6) drives the interaction _mechanism_ (e.g. ConfirmationDialog arming);
48
+ `evaluation: 'negative'` drives the adverse _color voice_. A destructive
49
+ action typically pairs both, but neither implies the other — see §6 and
50
+ ENTITY_CONSEQUENCE in `taxonomy.ts`.
51
+
52
+ ### Step 3 — Read token paths from §1
53
+
54
+ Look up `fooMeta.entity` in the Entity → Token Map (§1).
55
+ Each column gives you the `vars.*` subtree. Use §2 to construct the exact path.
56
+
57
+ ### Step 4 — Wire state and colors (§3)
58
+
59
+ Apply the State Priority Rule (§3) to resolve which color token to use for
60
+ the current combination of React Aria state booleans.
61
+
62
+ ---
63
+
64
+ ## §1 — Entity → Token Map
65
+
66
+ A component MUST use ONLY tokens from its Entity row.
67
+
68
+ | Entity | Colors | Radii | Border | Sizing | Spacing | Typography | Motion | Elevation |
69
+ | -------------- | --------------- | --------- | ----------------------------- | ------ | --------------- | ------------------------ | ------------ | ---------------- |
70
+ | **Action** | `action` | `control` | `outline.control` | `hit` | `inset.control` | `label` | `feedback` | `flat` |
71
+ | **Input** | `input` | `control` | `outline.control` | `hit` | `inset.control` | `label` | `feedback` | `flat` |
72
+ | **Selection** | `input` | `control` | `outline.control`, `selected` | `hit` | `inset.control` | `label` | `feedback` | `flat` |
73
+ | **Navigation** | `navigation` | `control` | `outline.control` | `hit` | `inset.control` | `label` | `feedback` | `flat` |
74
+ | **Disclosure** | `navigation` | `control` | `outline.control` | `hit` | `inset.control` | `label` | `transition` | `flat` |
75
+ | **Overlay** | `informational` | `surface` | `outline.surface` | — | `inset.surface` | `title`, `body`, `label` | `transition` | `overlay` |
76
+ | **Feedback** | `feedback` | `surface` | `outline.surface` | — | `inset.surface` | `body`, `label` | `feedback` | `raised` |
77
+ | **Collection** | `informational` | `surface` | `outline.surface`, `divider` | — | `inset.surface` | `body`, `label` | — | `flat`, `raised` |
78
+ | **Structure** | `informational` | `surface` | `outline.surface`, `divider` | — | `inset.surface` | `title`, `body`, `label` | — | `flat`, `raised` |
79
+
80
+ **Cross-cutting** (apply to ALL interactive entities — not in the table because they are entity-agnostic):
81
+
82
+ | Token family | Path |
83
+ | ---------------- | --------------------------------------------- | ------ | ------- | -------- | ----------- |
84
+ | Focus ring | `vars.focus.ring.width` / `.style` / `.color` |
85
+ | Disabled opacity | `vars.opacity.disabled` |
86
+ | Scrim opacity | `vars.opacity.scrim` |
87
+ | Scrim color | `vars.overlay.scrim` |
88
+ | Z-Index | `vars.zIndex.layer.{base | sticky | overlay | blocking | transient}` |
89
+
90
+ ### §1.1 — Mapping Rationale
91
+
92
+ The Entity → Token Map above groups 9 entities into 5 UX color contexts.
93
+ The grouping criterion is a single discriminant question:
94
+
95
+ > **"What is the user's primary cognitive mode when interacting with this entity?"**
96
+
97
+ | Cognitive Mode | UX Context | Entities | Why they share tokens |
98
+ | -------------------------------------------------------------------------- | --------------- | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
99
+ | **Deciding** — evaluating consequences before triggering an effect | `action` | Action | The user weighs risk/reward before committing. Visual identity must signal _actionability_ and _consequence weight_. |
100
+ | **Providing** — supplying or selecting data for the system | `input` | Input, Selection | Both involve data provision. Selection is constrained input — the user picks from a set rather than entering freeform, but the cognitive task is the same: "give the system a value." |
101
+ | **Orienting** — navigating across or revealing within an information space | `navigation` | Navigation, Disclosure | Navigation moves the user across destinations; Disclosure reveals structure in place. Both answer "where am I / what's here?" — spatial and structural orientation share the same visual language. |
102
+ | **Receiving** — consuming a system-initiated status or outcome message | `feedback` | Feedback | The user is the audience, not the initiator. Tokens must communicate valence (positive/caution/negative) without implying interactivity. |
103
+ | **Reading** — consuming organized, persistent content | `informational` | Overlay, Collection, Structure | All are content-carrying surfaces. Overlay is temporary content elevated above the page, Collection is a grouped set of items, Structure is the organizational frame. They share surface-level visual treatment because their tokens serve the _content they carry_, not the container's interaction mode. |
104
+
105
+ **How to use this table when adding a new entity or component:**
106
+
107
+ 1. Ask: "What is the user's primary cognitive mode?"
108
+ 2. Find the matching row → that is the UX context → that is the `Colors` column for §1.
109
+ 3. The remaining columns (Radii, Border, Sizing, etc.) follow from the **surface type**: interactive entities use `control` tokens, content-carrying entities use `surface` tokens.
110
+
111
+ **Legal vs required.** The §1 row is the **legal** set of token families a component MAY consume — not a list it MUST consume. A frame-only `Structure` (e.g. `Form`, `Wizard`) that composes children without painting a surface lawfully reads zero tokens from `vars.colors.informational.*` and only consumes `spacing` / `typography`. Reading **outside** the row remains forbidden; reading a **subset** of it is normal.
112
+
113
+ **Stacking inside `informational`.** When two `informational` surfaces overlap (Card inside Dialog, Dialog over page, …) they may resolve to the same `background` colour. Differentiation is paid in `elevation` first, `border` second, never in colour. See [colors.md → Stacking informational surfaces](/docs/design/design-system/design-tokens/colors#stacking-informational-surfaces) for the operational rule.
114
+
115
+ **Collection containers with Selection items (per-part entity split).** A selectable list composite (`ListBox`, `GridList`) is allowed to declare **two entities across its parts**: the container root is `Collection` (an `informational` surface — the frame that carries the items) while each selectable item is `Selection` (`input` chrome, `selected` State). The item's selection chrome is therefore identical to `Select`/`Checkbox`/`RadioGroup` (`vars.colors.input.*`), and the container reads `vars.colors.informational.*`. This is intentional and enforced-compatible: the entity→ux-context contract test unions the contexts of all entities declared in a file, so both reads are legal. See ADR-007 for the rationale.
116
+
117
+ **Surface type rule** (derives all non-color columns):
118
+
119
+ | Surface type | Radii | Border | Sizing | Spacing | Elevation |
120
+ | ---------------------------------------- | --------- | ----------------- | ------ | --------------- | -------------------------------------------- |
121
+ | `control` — user operates this directly | `control` | `outline.control` | `hit` | `inset.control` | `flat` |
122
+ | `surface` — carries content for the user | `surface` | `outline.surface` | — | `inset.surface` | per entity (flat, raised, overlay, blocking) |
123
+
124
+ This means the full §1 row for any entity is determined by two decisions:
125
+
126
+ 1. **Cognitive mode** → Colors column
127
+ 2. **Surface type** → all other columns (except Typography, Motion, and Elevation which have entity-specific assignments)
128
+
129
+ ---
130
+
131
+ ## §2 — vars Path Formulas
132
+
133
+ Given the column value from §1, here is the exact path formula for each family.
134
+
135
+ ### Colors
136
+
137
+ ```
138
+ vars.colors.{Colors}[evaluation][dimension][state]
139
+ ```
140
+
141
+ - `{Colors}` — the value from the Colors column (e.g. `action`, `navigation`)
142
+ - `evaluation` — `EvaluationsFor<E>` from `taxonomy.ts` → `ENTITY_EVALUATION[entity]`
143
+ - `dimension` — `background` | `border` | `text`
144
+ - `state` — one of the names listed in `STATES` (e.g. `default`, `hover`, `focused`, `disabled`, `pressed`, `selected`, `invalid`); resolved at runtime by React Aria render props, not authorially declared.
145
+
146
+ Example:
147
+
148
+ ```typescript
149
+ const c = vars.colors.action[evaluation]; // evaluation = 'primary'
150
+ c.background.default; // → var(--tt-colors-action-primary-background-default)
151
+ c.border.focused; // → var(--tt-colors-action-primary-border-focused)
152
+ c.text.disabled; // → var(--tt-colors-action-primary-text-disabled)
153
+ ```
154
+
155
+ Not every dimension/state combination is defined in every theme — optional chaining (`?.`) is required.
156
+
157
+ ### Radii
158
+
159
+ ```
160
+ vars.radii.{Radii}
161
+ ```
162
+
163
+ Example: `vars.radii.control`
164
+
165
+ ### Border
166
+
167
+ ```
168
+ vars.border.{Border}.width
169
+ vars.border.{Border}.style
170
+ ```
171
+
172
+ Example: `vars.border.outline.control.width`, `vars.border.outline.control.style`
173
+
174
+ ### Sizing (interactive entities)
175
+
176
+ ```
177
+ vars.sizing.hit.{step}
178
+ ```
179
+
180
+ Standard step for all interactive components: **`base`**.
181
+ `min` and `prominent` are reserved for components with a distinct semantic identity (e.g. compact toolbar action, prominent CTA).
182
+ Hit targets are ergonomic minimums — CSS automatically responds to `@media (any-pointer: coarse)`.
183
+
184
+ ### Spacing
185
+
186
+ ```
187
+ vars.spacing.inset.{Spacing}.{step}
188
+ ```
189
+
190
+ Standard step: **`md`**.
191
+
192
+ Example: `vars.spacing.inset.control.md`
193
+
194
+ ### Typography
195
+
196
+ ```
197
+ vars.text.{Typography}.{step}
198
+ ```
199
+
200
+ Standard step: **`md`**. Spread the whole object: `...(vars.text.label.md as React.CSSProperties)`.
201
+
202
+ ### Motion — `feedback`
203
+
204
+ ```
205
+ vars.motion.feedback.duration
206
+ vars.motion.feedback.easing
207
+ ```
208
+
209
+ Apply to `transitionDuration` + `transitionTimingFunction`. Always declare `transitionProperty` explicitly.
210
+
211
+ ### Motion — `transition`
212
+
213
+ ```
214
+ vars.motion.transition.enter.duration / .easing
215
+ vars.motion.transition.exit.duration / .easing
216
+ ```
217
+
218
+ ### Elevation
219
+
220
+ ```
221
+ vars.elevation.surface.{Elevation}
222
+ ```
223
+
224
+ Example: `vars.elevation.surface.flat`, `vars.elevation.surface.overlay`
225
+
226
+ ---
227
+
228
+ ## §3 — State Priority Rule
229
+
230
+ When wiring state-dependent colors, evaluate conditions in the canonical
231
+ order defined by `STATE_PRIORITY` in
232
+ [`src/semantics/taxonomy.ts`](../semantics/taxonomy.ts). Highest priority
233
+ first:
234
+
235
+ ```
236
+ disabled > invalid > expanded > indeterminate > selected
237
+ > focusVisible > pressed > hovered > default
238
+ ```
239
+
240
+ `STATE_PRIORITY` is the single source of truth for this cascade. Do **not**
241
+ duplicate the order in component code — use `resolveInteractiveStyle` (§3.1).
242
+ The tuple also binds each React Aria flag to the token-state key it selects
243
+ (e.g. `isSelected → checked`, `isPressed → active`, `isHovered → hover`).
244
+
245
+ Template (React Aria pattern, background / border / text dimensions):
246
+
247
+ ```typescript
248
+ style={({ isHovered, isPressed, isDisabled, isFocusVisible }) => ({
249
+ backgroundColor: resolveInteractiveStyle(c?.background, { isHovered, isPressed, isDisabled }),
250
+ borderColor: resolveInteractiveStyle(c?.border, { isDisabled, isFocusVisible }),
251
+ color: resolveInteractiveStyle(c?.text, { isHovered, isPressed, isDisabled })
252
+ ?? c?.text?.default,
253
+
254
+ // Focus ring — always via outline, never via border (avoids layout shift).
255
+ // Applied through the single-source helper, same rule as the state cascade.
256
+ outline: focusRingOutline(isFocusVisible),
257
+ })}
258
+ ```
259
+
260
+ The focus ring is applied via `focusRingOutline` (`src/tokens/focusRing.ts`) —
261
+ the single source of truth for the `vars.focus.ring.*` outline, mirroring how
262
+ `resolveInteractiveStyle` centralises the state cascade. Do **not** inline the
263
+ `vars.focus.ring.*` ternary in component code.
264
+
265
+ ### §3.1 — `resolveInteractiveStyle` helper
266
+
267
+ Interactive components MUST use `resolveInteractiveStyle` (in `src/tokens/`)
268
+ to apply the `STATE_PRIORITY` cascade **per color dimension**. The helper
269
+ iterates the tuple — no component re-implements the ternary chain.
270
+
271
+ Pass only the flags the dimension respects — e.g. `background` usually
272
+ ignores `isFocusVisible`, `border` usually ignores `isHovered` /
273
+ `isPressed`. Omitted flags short-circuit that level of the cascade.
274
+
275
+ Structural tokens (`radii`, `border.*.width/style`, `sizing`, `spacing`,
276
+ `typography`, `motion`) are read as literals from `vars.*` following the
277
+ component's entity row in §1. They are intentionally **not** abstracted into
278
+ a helper: the literal read is the contract's grep-able audit trail.
279
+
280
+ ```typescript
281
+ backgroundColor: resolveInteractiveStyle(c?.background, { isHovered, isPressed, isDisabled }),
282
+ borderColor: resolveInteractiveStyle(c?.border, { isDisabled, isFocusVisible }),
283
+ color: resolveInteractiveStyle(c?.text, { isHovered, isPressed, isDisabled })
284
+ ?? c?.text?.default,
285
+ ```
286
+
287
+ ---
288
+
289
+ ## §4 — Standard Step Rule
290
+
291
+ > A component picks a **fixed** step that matches its semantic identity.
292
+ > There is no `size` prop. A component that needs different density or typography
293
+ > has a different semantic identity and is a different component.
294
+
295
+ | Family | Standard step | Token path |
296
+ | ---------- | ------------- | ------------------------------------------ |
297
+ | Sizing | `base` | `vars.sizing.hit.base` |
298
+ | Spacing | `md` | `vars.spacing.inset.{control\|surface}.md` |
299
+ | Typography | `md` | `vars.text.label.md` |
300
+
301
+ If a design calls for a "small button", the question is: **why is it smaller semantically?**
302
+ Is it a toolbar action? A chip? A compact selection control? Name it, give it an entity, and it gets its own fixed step.
303
+
304
+ ---
305
+
306
+ ## §5 — data-\* Attribute Convention
307
+
308
+ Every component root MUST carry the identity attributes (`data-scope`, `data-part`); other attributes are emitted only when the dimension applies.
309
+
310
+ | Attribute | Where | Type / value | When emitted |
311
+ | ------------------ | ------------------------------------------- | ------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
312
+ | `data-scope` | every element | `kebab-case(meta.displayName)` — e.g. `"button"`, `"dialog"` | Always. |
313
+ | `data-part` | every element | `meta.structure` — e.g. `"root"`, `"label"`, `"control"` | Always. |
314
+ | `data-evaluation` | parts that consume evaluation tokens | `EvaluationsFor<E>` — e.g. `"primary"`, `"negative"` | When the part renders evaluation-dependent colors. |
315
+ | `data-consequence` | leaf Action elements that declare an effect | `ConsequencesFor<E>` — `"neutral" \| "committing" \| "destructive"` | When the component accepts a `consequence` prop (`Button`, `MenuItem`, `FormSubmit`). |
316
+ | `data-composition` | leaves that play a parent slot | `CompositionsFor<E>` — e.g. `"primaryAction"` | When the component accepts a `composition` prop. Read at runtime by composites (e.g. `DialogActions` reorders by it). |
317
+ | `data-platform` | `DialogActions` only | `"ios" \| "windows"` | Always on `DialogActions`. Reflects the active ordering convention. |
318
+ | `data-pending` | `FormSubmit` only | `"true"` (omitted otherwise) | While `isPending` is `true`. Lets host CSS/tests show spinner without re-wiring the disabled path. |
319
+ | `data-arming` | `ConfirmationDialog` confirm button only | `"true"` (omitted otherwise) | While a `destructive` confirmation is awaiting its second click. Selected at runtime from `consequence` — the proof that Consequence drives behavior. |
320
+
321
+ **Sub-part identity convention** — composites reuse the host's `data-scope` and pin the per-part `data-part`:
322
+
323
+ ```tsx
324
+ <div data-scope="dialog" data-part="actions">…</div>
325
+ <button data-scope="button" data-part="root" data-composition="primaryAction">…</button>
326
+ ```
327
+
328
+ The contract test [`components.contract.test.tsx`](../../tests/unit/tests/components.contract.test.tsx) auto-discovers every `*Meta` and asserts each attribute value is legal per the matrices in `taxonomy.ts`.
329
+
330
+ ---
331
+
332
+ ## §6 — Color Role Coverage
333
+
334
+ > **Source of truth: `ENTITY_EVALUATION` in `taxonomy.ts`.**
335
+ > Read it directly — do not rely on any other copy.
336
+
337
+ ```typescript
338
+ import { ENTITY_EVALUATION } from '@ttoss/fsl-ui/semantics';
339
+
340
+ // Which evaluations are valid for a given entity:
341
+ const valid = ENTITY_EVALUATION['Action'];
342
+ // → ['primary', 'secondary', 'accent', 'muted', 'negative']
343
+ //
344
+ // Note: 'negative' on Action is the adverse color *voice*. It does not
345
+ // imply behavior — effect-on-state is expressed separately through
346
+ // `consequence: 'destructive'` (see ENTITY_CONSEQUENCE), which drives
347
+ // interaction mechanics (e.g. ConfirmationDialog arming).
348
+ ```
349
+
350
+ ---
351
+
352
+ ## §7 — Escape Hatches: Composite-Scoped CSS Custom Properties
353
+
354
+ Composites own their layout: they expose no `style`/`className` and no
355
+ visual props. The **single sanctioned customization channel** is a
356
+ composite-scoped CSS custom property — a _knob_ — named
357
+ `--fsl-<scope>-<knob>` and consumed through the `fslVar` helper
358
+ (`src/tokens/escapeHatch.ts`):
359
+
360
+ ```typescript
361
+ maxWidth: fslVar('--fsl-dialog-max-width', DIALOG_MAX_WIDTH_DEFAULT),
362
+ ```
363
+
364
+ Hosts customize with ordinary CSS — no component code involved:
365
+
366
+ ```css
367
+ /* Wider dialogs across the app */
368
+ [data-scope='dialog'] {
369
+ --fsl-dialog-max-width: 720px;
370
+ }
371
+
372
+ /* One specific menu */
373
+ .settings-menu [data-scope='menu'] {
374
+ --fsl-menu-max-width: 480px;
375
+ }
376
+ ```
377
+
378
+ Rules (enforced by the contract tests):
379
+
380
+ 1. Every knob read MUST go through `fslVar` and MUST carry a fallback —
381
+ the component's default. A knob without a fallback is a violation.
382
+ 2. The `--fsl-` namespace is reserved for host knobs. `--tt-*` theme
383
+ tokens never take fallbacks (that ban is unchanged — fallbacks on theme
384
+ tokens mask missing token coverage).
385
+ 3. Knobs are for **geometry the host legitimately owns** (widths, heights
386
+ of overlay surfaces). Colors, spacing steps, typography, and anything
387
+ else covered by a semantic token are NOT knobs — they belong to the
388
+ theme.
389
+ 4. Where the underlying React Aria primitive exposes safe positioning
390
+ props (`placement`, `offset`, `crossOffset`, `shouldFlip`,
391
+ `containerPadding`), the composite forwards them as ordinary props —
392
+ positioning is behavior, not chrome.
393
+
394
+ Registered knobs:
395
+
396
+ | Knob | Component | Fallback |
397
+ | ------------------------- | ------------- | ------------------ |
398
+ | `--fsl-dialog-max-width` | `DialogModal` | `min(500px, 90vw)` |
399
+ | `--fsl-dialog-max-height` | `DialogModal` | `90vh` |
400
+ | `--fsl-menu-min-width` | `Menu` | `12rem` |
401
+ | `--fsl-menu-max-width` | `Menu` | `min(320px, 90vw)` |
402
+ | `--fsl-popover-max-width` | `Popover` | `min(320px, 90vw)` |
403
+ | `--fsl-tooltip-max-width` | `Tooltip` | `min(280px, 90vw)` |
404
+
405
+ ---
406
+
407
+ ## §8 — Full Example: Button (Entity = Action)
408
+
409
+ `entity: 'Action'` → §1 row: colors=`action`, radii=`control`, border=`outline.control`,
410
+ sizing=`hit.base`, spacing=`inset.control.md`, typography=`label.md`, motion=`feedback`, elevation=`flat`.
411
+
412
+ ```typescript
413
+ import { vars } from '@ttoss/fsl-theme/vars';
414
+ import type { ComponentMeta, EvaluationsFor } from '../../semantics';
415
+
416
+ // Step 1 — identity
417
+ export const buttonMeta = {
418
+ displayName: 'Button',
419
+ entity: 'Action',
420
+ structure: 'root',
421
+ } as const satisfies ComponentMeta<'Action'>;
422
+
423
+ // Step 2 — valid evaluations, derived from taxonomy
424
+ type ButtonEvaluation = EvaluationsFor<(typeof buttonMeta)['entity']>;
425
+
426
+ // Step 3 — wire props
427
+ export const Button = ({ evaluation = 'primary', ...props }: ButtonProps) => {
428
+ const c = vars.colors.action[evaluation]; // §2 Colors formula
429
+
430
+ return (
431
+ <RACButton
432
+ data-scope="button" // §5
433
+ data-part="root"
434
+ data-evaluation={evaluation}
435
+ style={({ isHovered, isPressed, isDisabled, isFocusVisible }) => ({
436
+ // Static layout — §2 formulas
437
+ display: 'inline-flex',
438
+ alignItems: 'center',
439
+ justifyContent: 'center',
440
+ borderRadius: vars.radii.control,
441
+ borderWidth: vars.border.outline.control.width,
442
+ borderStyle: vars.border.outline.control.style,
443
+ minHeight: vars.sizing.hit.base,
444
+ paddingBlock: vars.spacing.inset.control.md,
445
+ paddingInline: vars.spacing.inset.control.md,
446
+ ...(vars.text.label.md as React.CSSProperties),
447
+ transitionDuration: vars.motion.feedback.duration,
448
+ transitionTimingFunction: vars.motion.feedback.easing,
449
+ transitionProperty: 'background-color, border-color, color',
450
+
451
+ // State-dependent colors — §3 priority rule
452
+ backgroundColor: isDisabled ? c?.background?.disabled
453
+ : isPressed ? c?.background?.active
454
+ : isHovered ? c?.background?.hover
455
+ : c?.background?.default,
456
+ borderColor: isFocusVisible ? c?.border?.focused
457
+ : isDisabled ? c?.border?.disabled
458
+ : c?.border?.default,
459
+ color: isDisabled ? c?.text?.disabled
460
+ : isPressed ? (c?.text?.active ?? c?.text?.default)
461
+ : isHovered ? (c?.text?.hover ?? c?.text?.default)
462
+ : c?.text?.default,
463
+ outline: isFocusVisible
464
+ ? `${vars.focus.ring.width} ${vars.focus.ring.style} ${vars.focus.ring.color}`
465
+ : 'none',
466
+ })}
467
+ />
468
+ );
469
+ };
470
+ ```
471
+
472
+ ---
473
+
474
+ ## §9 — Icons (internal glyph layer)
475
+
476
+ When a component needs a glyph (chevron, check, close, …), do **not** hardcode
477
+ a unicode character or hand-author SVG. Use the internal `Icon`
478
+ (`src/components/Icon/`) — a semantic layer over the Iconify provider
479
+ (ADR-005):
480
+
481
+ ```typescript
482
+ import { Icon } from '../Icon'; // from src/components/*; '../../components/Icon' from composites
483
+
484
+ <Icon intent="disclosure.expand" /> // named by meaning, not glyph
485
+ <Icon intent="action.close" size="sm" /> // sm | md (default) | lg → vars.sizing.icon.*
486
+ <Icon intent="action.search" label={label} /> // labelled (role=img) instead of decorative
487
+ ```
488
+
489
+ Rules:
490
+
491
+ 1. **Intent, not glyph.** Pick an `icon.{family}.{intent}` from
492
+ `src/components/Icon/intents.ts`. Need one that is not there yet? Add the
493
+ intent to `intents.ts` **and** its Lucide glyph to `glyphs.ts` (a two-line
494
+ edit; the `satisfies Record<IconIntent, …>` makes a missing glyph a compile
495
+ error). The registry grows only when a real component needs it.
496
+ 2. **Color is inherited.** Icon renders `currentColor`; set the color on the
497
+ surrounding element (as Checkbox's `indicator` does). Icon reads no color
498
+ token.
499
+ 3. **Size is the only token Icon reads** — `vars.sizing.icon.{sm|md|lg}` via
500
+ the `size` prop. This is not the §4 density `size` (that governs interactive
501
+ hit targets); a glyph legitimately scales with its context.
502
+ 4. **Decorative by default** (`aria-hidden`). Pass `label` only when the icon
503
+ is the sole carrier of meaning — and pass caller-localized copy (§6/i18n).
504
+ 5. Icon is **internal** — never re-export it from `src/index.ts`. It is the
505
+ seed of a future standalone `@ttoss/fsl-icon` package.