@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.
- package/LICENSE +21 -0
- package/README.md +76 -0
- package/dist/_virtual/_rolldown/runtime.cjs +30 -0
- package/dist/components/Breadcrumbs/Breadcrumbs.cjs +129 -0
- package/dist/components/Breadcrumbs/Breadcrumbs.d.cts +61 -0
- package/dist/components/Breadcrumbs/Breadcrumbs.d.mts +61 -0
- package/dist/components/Breadcrumbs/Breadcrumbs.mjs +126 -0
- package/dist/components/Button/Button.cjs +80 -0
- package/dist/components/Button/Button.d.cts +68 -0
- package/dist/components/Button/Button.d.mts +68 -0
- package/dist/components/Button/Button.mjs +79 -0
- package/dist/components/Checkbox/Checkbox.cjs +183 -0
- package/dist/components/Checkbox/Checkbox.d.cts +45 -0
- package/dist/components/Checkbox/Checkbox.d.mts +45 -0
- package/dist/components/Checkbox/Checkbox.mjs +182 -0
- package/dist/components/CheckboxGroup/CheckboxGroup.cjs +97 -0
- package/dist/components/CheckboxGroup/CheckboxGroup.d.cts +56 -0
- package/dist/components/CheckboxGroup/CheckboxGroup.d.mts +56 -0
- package/dist/components/CheckboxGroup/CheckboxGroup.mjs +96 -0
- package/dist/components/FileTrigger/FileTrigger.cjs +51 -0
- package/dist/components/FileTrigger/FileTrigger.d.cts +52 -0
- package/dist/components/FileTrigger/FileTrigger.d.mts +52 -0
- package/dist/components/FileTrigger/FileTrigger.mjs +50 -0
- package/dist/components/GridList/GridList.cjs +220 -0
- package/dist/components/GridList/GridList.d.cts +72 -0
- package/dist/components/GridList/GridList.d.mts +72 -0
- package/dist/components/GridList/GridList.mjs +217 -0
- package/dist/components/Group/Group.cjs +77 -0
- package/dist/components/Group/Group.d.cts +61 -0
- package/dist/components/Group/Group.d.mts +61 -0
- package/dist/components/Group/Group.mjs +74 -0
- package/dist/components/Icon/Icon.cjs +52 -0
- package/dist/components/Icon/Icon.mjs +52 -0
- package/dist/components/Icon/glyphs.cjs +72 -0
- package/dist/components/Icon/glyphs.mjs +63 -0
- package/dist/components/Link/Link.cjs +62 -0
- package/dist/components/Link/Link.d.cts +38 -0
- package/dist/components/Link/Link.d.mts +38 -0
- package/dist/components/Link/Link.mjs +61 -0
- package/dist/components/ListBox/ListBox.cjs +126 -0
- package/dist/components/ListBox/ListBox.d.cts +63 -0
- package/dist/components/ListBox/ListBox.d.mts +63 -0
- package/dist/components/ListBox/ListBox.mjs +123 -0
- package/dist/components/Meter/Meter.cjs +126 -0
- package/dist/components/Meter/Meter.d.cts +63 -0
- package/dist/components/Meter/Meter.d.mts +63 -0
- package/dist/components/Meter/Meter.mjs +125 -0
- package/dist/components/NumberField/NumberField.cjs +221 -0
- package/dist/components/NumberField/NumberField.d.cts +68 -0
- package/dist/components/NumberField/NumberField.d.mts +68 -0
- package/dist/components/NumberField/NumberField.mjs +220 -0
- package/dist/components/ProgressBar/ProgressBar.cjs +132 -0
- package/dist/components/ProgressBar/ProgressBar.d.cts +56 -0
- package/dist/components/ProgressBar/ProgressBar.d.mts +56 -0
- package/dist/components/ProgressBar/ProgressBar.mjs +129 -0
- package/dist/components/RadioGroup/RadioGroup.cjs +218 -0
- package/dist/components/RadioGroup/RadioGroup.d.cts +82 -0
- package/dist/components/RadioGroup/RadioGroup.d.mts +82 -0
- package/dist/components/RadioGroup/RadioGroup.mjs +215 -0
- package/dist/components/Select/Select.cjs +253 -0
- package/dist/components/Select/Select.d.cts +86 -0
- package/dist/components/Select/Select.d.mts +86 -0
- package/dist/components/Select/Select.mjs +250 -0
- package/dist/components/Separator/Separator.cjs +64 -0
- package/dist/components/Separator/Separator.d.cts +44 -0
- package/dist/components/Separator/Separator.d.mts +44 -0
- package/dist/components/Separator/Separator.mjs +63 -0
- package/dist/components/Slider/Slider.cjs +160 -0
- package/dist/components/Slider/Slider.d.cts +45 -0
- package/dist/components/Slider/Slider.d.mts +45 -0
- package/dist/components/Slider/Slider.mjs +159 -0
- package/dist/components/Switch/Switch.cjs +176 -0
- package/dist/components/Switch/Switch.d.cts +43 -0
- package/dist/components/Switch/Switch.d.mts +43 -0
- package/dist/components/Switch/Switch.mjs +175 -0
- package/dist/components/Tabs/Tabs.cjs +180 -0
- package/dist/components/Tabs/Tabs.d.cts +85 -0
- package/dist/components/Tabs/Tabs.d.mts +85 -0
- package/dist/components/Tabs/Tabs.mjs +173 -0
- package/dist/components/Toast/Toast.cjs +218 -0
- package/dist/components/Toast/Toast.d.cts +96 -0
- package/dist/components/Toast/Toast.d.mts +96 -0
- package/dist/components/Toast/Toast.mjs +214 -0
- package/dist/components/ToggleButton/ToggleButton.cjs +125 -0
- package/dist/components/ToggleButton/ToggleButton.d.cts +43 -0
- package/dist/components/ToggleButton/ToggleButton.d.mts +43 -0
- package/dist/components/ToggleButton/ToggleButton.mjs +124 -0
- package/dist/components/ToggleButtonGroup/ToggleButtonGroup.cjs +48 -0
- package/dist/components/ToggleButtonGroup/ToggleButtonGroup.d.cts +41 -0
- package/dist/components/ToggleButtonGroup/ToggleButtonGroup.d.mts +41 -0
- package/dist/components/ToggleButtonGroup/ToggleButtonGroup.mjs +47 -0
- package/dist/components/Toolbar/Toolbar.cjs +67 -0
- package/dist/components/Toolbar/Toolbar.d.cts +60 -0
- package/dist/components/Toolbar/Toolbar.d.mts +60 -0
- package/dist/components/Toolbar/Toolbar.mjs +66 -0
- package/dist/composites/Accordion/Accordion.cjs +279 -0
- package/dist/composites/Accordion/Accordion.d.cts +176 -0
- package/dist/composites/Accordion/Accordion.d.mts +176 -0
- package/dist/composites/Accordion/Accordion.mjs +270 -0
- package/dist/composites/ConfirmationDialog/ConfirmationDialog.cjs +206 -0
- package/dist/composites/ConfirmationDialog/ConfirmationDialog.d.cts +166 -0
- package/dist/composites/ConfirmationDialog/ConfirmationDialog.d.mts +166 -0
- package/dist/composites/ConfirmationDialog/ConfirmationDialog.mjs +203 -0
- package/dist/composites/Dialog/Dialog.cjs +372 -0
- package/dist/composites/Dialog/Dialog.d.cts +234 -0
- package/dist/composites/Dialog/Dialog.d.mts +234 -0
- package/dist/composites/Dialog/Dialog.mjs +360 -0
- package/dist/composites/Disclosure/Disclosure.cjs +214 -0
- package/dist/composites/Disclosure/Disclosure.d.cts +118 -0
- package/dist/composites/Disclosure/Disclosure.d.mts +118 -0
- package/dist/composites/Disclosure/Disclosure.mjs +207 -0
- package/dist/composites/Form/Form.cjs +161 -0
- package/dist/composites/Form/Form.d.cts +176 -0
- package/dist/composites/Form/Form.d.mts +176 -0
- package/dist/composites/Form/Form.mjs +156 -0
- package/dist/composites/Menu/Menu.cjs +190 -0
- package/dist/composites/Menu/Menu.d.cts +158 -0
- package/dist/composites/Menu/Menu.d.mts +158 -0
- package/dist/composites/Menu/Menu.mjs +186 -0
- package/dist/composites/Popover/Popover.cjs +80 -0
- package/dist/composites/Popover/Popover.d.cts +56 -0
- package/dist/composites/Popover/Popover.d.mts +56 -0
- package/dist/composites/Popover/Popover.mjs +78 -0
- package/dist/composites/SearchField/SearchField.cjs +194 -0
- package/dist/composites/SearchField/SearchField.d.cts +81 -0
- package/dist/composites/SearchField/SearchField.d.mts +81 -0
- package/dist/composites/SearchField/SearchField.mjs +189 -0
- package/dist/composites/TagGroup/TagGroup.cjs +212 -0
- package/dist/composites/TagGroup/TagGroup.d.cts +84 -0
- package/dist/composites/TagGroup/TagGroup.d.mts +84 -0
- package/dist/composites/TagGroup/TagGroup.mjs +209 -0
- package/dist/composites/TextArea/TextArea.cjs +190 -0
- package/dist/composites/TextArea/TextArea.d.cts +95 -0
- package/dist/composites/TextArea/TextArea.d.mts +95 -0
- package/dist/composites/TextArea/TextArea.mjs +181 -0
- package/dist/composites/TextField/TextField.cjs +200 -0
- package/dist/composites/TextField/TextField.d.cts +111 -0
- package/dist/composites/TextField/TextField.d.mts +111 -0
- package/dist/composites/TextField/TextField.mjs +191 -0
- package/dist/composites/Tooltip/Tooltip.cjs +79 -0
- package/dist/composites/Tooltip/Tooltip.d.cts +53 -0
- package/dist/composites/Tooltip/Tooltip.d.mts +53 -0
- package/dist/composites/Tooltip/Tooltip.mjs +77 -0
- package/dist/composites/Wizard/Wizard.cjs +333 -0
- package/dist/composites/Wizard/Wizard.d.cts +206 -0
- package/dist/composites/Wizard/Wizard.d.mts +206 -0
- package/dist/composites/Wizard/Wizard.mjs +324 -0
- package/dist/composites/scope.cjs +124 -0
- package/dist/composites/scope.mjs +121 -0
- package/dist/index.cjs +186 -0
- package/dist/index.d.cts +37 -0
- package/dist/index.d.mts +37 -0
- package/dist/index.mjs +39 -0
- package/dist/semantics/componentMeta.d.cts +37 -0
- package/dist/semantics/componentMeta.d.mts +37 -0
- package/dist/semantics/index.cjs +16 -0
- package/dist/semantics/index.d.cts +4 -0
- package/dist/semantics/index.d.mts +4 -0
- package/dist/semantics/index.mjs +3 -0
- package/dist/semantics/taxonomy.cjs +150 -0
- package/dist/semantics/taxonomy.d.cts +153 -0
- package/dist/semantics/taxonomy.d.mts +153 -0
- package/dist/semantics/taxonomy.mjs +140 -0
- package/dist/tokens/escapeHatch.cjs +13 -0
- package/dist/tokens/escapeHatch.mjs +13 -0
- package/dist/tokens/focusRing.cjs +28 -0
- package/dist/tokens/focusRing.mjs +28 -0
- package/dist/tokens/keyframes.cjs +76 -0
- package/dist/tokens/keyframes.mjs +75 -0
- package/dist/tokens/resolveInteractiveStyle.cjs +41 -0
- package/dist/tokens/resolveInteractiveStyle.mjs +41 -0
- package/llms.txt +142 -0
- package/package.json +70 -0
- 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.
|