@teamblind-chorus/ui 1.1.0 → 2.0.0

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 (148) hide show
  1. package/README.md +3 -3
  2. package/agents/AGENTS.md +6 -6
  3. package/agents/DESIGN.md +245 -244
  4. package/agents/LOVABLE.md +40 -11
  5. package/agents/catalog.md +10 -8
  6. package/agents/components/avatar-rail/avatar-rail.md +2 -4
  7. package/agents/components/avatar-rail/avatar-rail.spec.json +27 -12
  8. package/agents/components/badge/role.md +7 -9
  9. package/agents/components/badge/role.spec.json +6 -6
  10. package/agents/components/badge/update.md +6 -8
  11. package/agents/components/badge/update.spec.json +5 -5
  12. package/agents/components/banner/banner.family.json +3 -1
  13. package/agents/components/banner/banner.md +66 -15
  14. package/agents/components/banner/banner.spec.json +37 -14
  15. package/agents/components/bottom-sheet/bottom-sheet.md +4 -6
  16. package/agents/components/bottom-sheet/bottom-sheet.spec.json +5 -5
  17. package/agents/components/bubble/bubble.md +8 -10
  18. package/agents/components/bubble/bubble.spec.json +11 -11
  19. package/agents/components/button/button.md +1 -1
  20. package/agents/components/button/check.md +9 -11
  21. package/agents/components/button/check.spec.json +25 -8
  22. package/agents/components/button/fab.md +7 -9
  23. package/agents/components/button/fab.spec.json +27 -10
  24. package/agents/components/button/group.spec.json +4 -4
  25. package/agents/components/button/icon.md +21 -23
  26. package/agents/components/button/icon.spec.json +29 -12
  27. package/agents/components/button/standard.md +40 -42
  28. package/agents/components/button/standard.spec.json +37 -20
  29. package/agents/components/button/text.md +21 -23
  30. package/agents/components/button/text.spec.json +30 -13
  31. package/agents/components/button/toggle.md +7 -9
  32. package/agents/components/button/toggle.spec.json +27 -10
  33. package/agents/components/button/toolbar.md +24 -26
  34. package/agents/components/button/toolbar.spec.json +10 -12
  35. package/agents/components/carousel/carousel.md +1 -1
  36. package/agents/components/carousel/post.md +15 -21
  37. package/agents/components/carousel/post.spec.json +17 -17
  38. package/agents/components/carousel/profile.md +9 -45
  39. package/agents/components/carousel/profile.spec.json +17 -17
  40. package/agents/components/chip/chip.md +1 -1
  41. package/agents/components/chip/filter.md +22 -24
  42. package/agents/components/chip/filter.spec.json +34 -11
  43. package/agents/components/chip/tag.md +22 -24
  44. package/agents/components/chip/tag.spec.json +36 -13
  45. package/agents/components/dialog/dialog.md +1 -3
  46. package/agents/components/dialog/dialog.spec.json +3 -3
  47. package/agents/components/directory-list/directory-list.md +1 -3
  48. package/agents/components/directory-list/directory-list.spec.json +2 -2
  49. package/agents/components/divider/divider.family.json +1 -1
  50. package/agents/components/divider/divider.md +12 -14
  51. package/agents/components/divider/divider.spec.json +8 -8
  52. package/agents/components/empty-state/empty-state.family.json +28 -0
  53. package/agents/components/empty-state/empty-state.md +69 -0
  54. package/agents/components/empty-state/empty-state.spec.json +87 -0
  55. package/agents/components/feed/ad.md +2 -4
  56. package/agents/components/feed/ad.spec.json +10 -10
  57. package/agents/components/feed/post.md +41 -43
  58. package/agents/components/feed/post.spec.json +35 -39
  59. package/agents/components/form-field/form-field.md +1 -1
  60. package/agents/components/form-field/input.md +32 -34
  61. package/agents/components/form-field/input.spec.json +39 -31
  62. package/agents/components/form-field/search.md +2 -4
  63. package/agents/components/form-field/search.spec.json +24 -16
  64. package/agents/components/form-field/select.md +18 -20
  65. package/agents/components/form-field/select.spec.json +36 -27
  66. package/agents/components/form-field/textarea.md +3 -5
  67. package/agents/components/form-field/textarea.spec.json +37 -29
  68. package/agents/components/header/main.md +4 -6
  69. package/agents/components/header/main.spec.json +3 -3
  70. package/agents/components/header/sub.md +6 -8
  71. package/agents/components/header/sub.spec.json +3 -3
  72. package/agents/components/list/accordion.md +34 -45
  73. package/agents/components/list/accordion.spec.json +26 -17
  74. package/agents/components/list/entry.md +59 -81
  75. package/agents/components/list/entry.spec.json +37 -21
  76. package/agents/components/list/list.md +2 -2
  77. package/agents/components/list/radio.md +13 -20
  78. package/agents/components/list/radio.spec.json +33 -18
  79. package/agents/components/list/standard.md +88 -64
  80. package/agents/components/list/standard.spec.json +52 -20
  81. package/agents/components/metadata/compact.md +4 -6
  82. package/agents/components/metadata/compact.spec.json +6 -6
  83. package/agents/components/metadata/metadata.md +1 -1
  84. package/agents/components/metadata/standard.md +12 -14
  85. package/agents/components/metadata/standard.spec.json +10 -10
  86. package/agents/components/nav-card/nav-card.md +25 -27
  87. package/agents/components/nav-card/nav-card.spec.json +25 -16
  88. package/agents/components/nav-list/nav-list.md +2 -8
  89. package/agents/components/nav-list/nav-list.spec.json +3 -3
  90. package/agents/components/navigation-bar/main.md +9 -11
  91. package/agents/components/navigation-bar/main.spec.json +6 -6
  92. package/agents/components/navigation-bar/search.md +6 -8
  93. package/agents/components/navigation-bar/search.spec.json +9 -9
  94. package/agents/components/navigation-bar/sub.md +9 -11
  95. package/agents/components/navigation-bar/sub.spec.json +7 -7
  96. package/agents/components/page-shell/page-shell.family.json +1 -1
  97. package/agents/components/page-shell/page-shell.md +33 -0
  98. package/agents/components/page-shell/page-shell.spec.json +85 -0
  99. package/agents/components/pagination/pagination.family.json +1 -1
  100. package/agents/components/pagination/pagination.md +3 -3
  101. package/agents/components/pagination/pagination.spec.json +5 -5
  102. package/agents/components/profile-header/profile-header.md +9 -11
  103. package/agents/components/profile-header/profile-header.spec.json +9 -9
  104. package/agents/components/progress/progress.family.json +1 -1
  105. package/agents/components/progress/progress.md +5 -5
  106. package/agents/components/progress/progress.spec.json +8 -8
  107. package/agents/components/side-sheet/side-sheet.md +11 -13
  108. package/agents/components/side-sheet/side-sheet.spec.json +3 -3
  109. package/agents/components/skeleton/skeleton.md +7 -9
  110. package/agents/components/skeleton/skeleton.spec.json +5 -5
  111. package/agents/components/spinner/spinner.family.json +27 -0
  112. package/agents/components/spinner/spinner.md +96 -0
  113. package/agents/components/spinner/spinner.spec.json +82 -0
  114. package/agents/components/status-tag/status-tag.md +7 -9
  115. package/agents/components/status-tag/status-tag.spec.json +5 -5
  116. package/agents/components/suggestion-list/suggestion-list.md +3 -7
  117. package/agents/components/suggestion-list/suggestion-list.spec.json +8 -12
  118. package/agents/components/switch/switch.md +12 -14
  119. package/agents/components/switch/switch.spec.json +23 -15
  120. package/agents/components/tab-bar/tab-bar.md +9 -11
  121. package/agents/components/tab-bar/tab-bar.spec.json +37 -23
  122. package/agents/components/tabs/rounded.md +6 -8
  123. package/agents/components/tabs/rounded.spec.json +34 -13
  124. package/agents/components/tabs/segmented.md +4 -6
  125. package/agents/components/tabs/segmented.spec.json +4 -8
  126. package/agents/components/tabs/underline.md +9 -11
  127. package/agents/components/tabs/underline.spec.json +31 -14
  128. package/agents/components/thumbnail/thumbnail.md +5 -7
  129. package/agents/components/thumbnail/thumbnail.spec.json +8 -8
  130. package/agents/components/toast/toast.md +5 -7
  131. package/agents/components/toast/toast.spec.json +3 -3
  132. package/agents/components/tooltip/tooltip.md +6 -8
  133. package/agents/components/tooltip/tooltip.spec.json +4 -4
  134. package/agents/manifest.json +8 -6
  135. package/agents/tokens.usage.json +71 -226
  136. package/agents/usage.json +12 -0
  137. package/dist/index.cjs +531 -262
  138. package/dist/index.cjs.map +1 -1
  139. package/dist/index.d.cts +57 -13
  140. package/dist/index.d.ts +57 -13
  141. package/dist/index.js +530 -263
  142. package/dist/index.js.map +1 -1
  143. package/dist/styles.css +560 -379
  144. package/eslint/rules.js +7 -7
  145. package/package.json +2 -3
  146. package/agents/anti-patterns.md +0 -533
  147. package/agents/compose.md +0 -240
  148. package/agents/images.md +0 -66
@@ -39,7 +39,7 @@
39
39
  "helper": {
40
40
  "type": "node",
41
41
  "optional": true,
42
- "description": "Assistive text rendered below the field box, left-aligned. **Optional on every appearance** — omit the prop to render the field without an assistive rung; the box and label keep their footprint. On the `error` appearance the helper re-tones to `sys.color.error` so the message reads as the error caption; an error field may still be shown without a helper, in which case only the field box re-tones. Mutually exclusive with `maxLength` — if both are given, the character count is shown and `helper` is ignored."
42
+ "description": "Assistive text rendered below the field box, left-aligned. **Optional on every appearance** — omit the prop to render the field without an assistive rung; the box and label keep their footprint. On the `error` appearance the helper re-tones to `sys.color.text.danger` so the message reads as the error caption; an error field may still be shown without a helper, in which case only the field box re-tones. Mutually exclusive with `maxLength` — if both are given, the character count is shown and `helper` is ignored."
43
43
  },
44
44
  "maxLength": {
45
45
  "type": "number",
@@ -59,7 +59,7 @@
59
59
  },
60
60
  "label": {
61
61
  "required": false,
62
- "description": "Visible label above the box. `sys.typo.label.md`, `sys.color.onSurface`. Associated with the input via `htmlFor`.",
62
+ "description": "Visible label above the box. `sys.typo.label.md`, `sys.color.text.default`. Associated with the input via `htmlFor`.",
63
63
  "accepts": [
64
64
  "text"
65
65
  ]
@@ -83,14 +83,14 @@
83
83
  },
84
84
  "helper": {
85
85
  "required": false,
86
- "description": "Assistive text below the box, left-aligned. `sys.typo.body.sm`, `sys.color.onSurfaceVariant`; on the `error` appearance the colour re-tones to `sys.color.error` so the message reads as the error caption. Referenced by the input's `aria-describedby`. Not rendered when a `maxLength` count is present, and intentionally omittable on every appearance (including `error`) — pass nothing and the field renders without an assistive rung.",
86
+ "description": "Assistive text below the box, left-aligned. `sys.typo.body.sm`, `sys.color.text.subtle`; on the `error` appearance the colour re-tones to `sys.color.text.danger` so the message reads as the error caption. Referenced by the input's `aria-describedby`. Not rendered when a `maxLength` count is present, and intentionally omittable on every appearance (including `error`) — pass nothing and the field renders without an assistive rung.",
87
87
  "accepts": [
88
88
  "text"
89
89
  ]
90
90
  },
91
91
  "count": {
92
92
  "required": false,
93
- "description": "`current/max` character count below the box, right-aligned, present when `maxLength` is set. `sys.typo.body.sm`, `sys.color.onSurfaceVariant`; the current-count number is `sys.typo.label.md` weight in `sys.color.onSurface`. Referenced by the input's `aria-describedby`; updates `aria-live=\"polite\"`.",
93
+ "description": "`current/max` character count below the box, right-aligned, present when `maxLength` is set. `sys.typo.body.sm`, `sys.color.text.subtle`; the current-count number is `sys.typo.label.md` weight in `sys.color.text.default`. Referenced by the input's `aria-describedby`; updates `aria-live=\"polite\"`.",
94
94
  "accepts": [
95
95
  "text"
96
96
  ]
@@ -103,7 +103,7 @@
103
103
  "slotGap": "sys.layout.inline.md",
104
104
  "radius": "sys.radius.md",
105
105
  "borderWidth": "sys.borderWidth.hairline",
106
- "activeStrokeWeight": "sys.borderWidth.thin",
106
+ "activeStrokeWeight": "sys.borderWidth.hairline",
107
107
  "groupGap": "sys.layout.stack.xs",
108
108
  "labelTypo": "sys.typo.label.md",
109
109
  "helperTypo": "sys.typo.body.sm",
@@ -113,28 +113,28 @@
113
113
  "iconSize": "sys.icon.md"
114
114
  },
115
115
  "groupColors": {
116
- "label": "sys.color.onSurface",
117
- "helper": "sys.color.onSurfaceVariant",
118
- "helperError": "sys.color.error",
119
- "count": "sys.color.onSurfaceVariant",
120
- "countCurrent": "sys.color.onSurface"
116
+ "label": "sys.color.text.default",
117
+ "helper": "sys.color.text.subtle",
118
+ "helperError": "sys.color.text.danger",
119
+ "count": "sys.color.text.subtle",
120
+ "countCurrent": "sys.color.text.default"
121
121
  },
122
122
  "appearances": {
123
123
  "default": {
124
124
  "background": "transparent",
125
- "text": "sys.color.onSurface",
126
- "placeholder": "sys.color.outline",
127
- "borderRest": "sys.color.outlineVariant",
128
- "borderHover": "sys.color.outline",
129
- "borderActive": "sys.color.onSurface"
125
+ "text": "sys.color.text.default",
126
+ "placeholder": "sys.color.border.boldest",
127
+ "borderRest": "sys.color.border.default",
128
+ "borderHover": "sys.color.border.boldest",
129
+ "borderActive": "sys.color.border.focused"
130
130
  },
131
131
  "error": {
132
- "background": "sys.color.errorContainer",
133
- "text": "sys.color.onErrorContainer",
134
- "placeholder": "sys.color.onErrorContainer",
135
- "borderRest": "sys.color.error",
136
- "borderHover": "sys.color.error",
137
- "borderActive": "sys.color.error"
132
+ "background": "sys.color.background.danger",
133
+ "text": "sys.color.text.danger",
134
+ "placeholder": "sys.color.text.danger",
135
+ "borderRest": "sys.color.border.danger",
136
+ "borderHover": "sys.color.border.danger",
137
+ "borderActive": "sys.color.border.danger"
138
138
  }
139
139
  },
140
140
  "states": {
@@ -155,35 +155,43 @@
155
155
  "nestedActionScope": "The pressed overlay (and hover stroke) is suppressed while the pointer presses / hovers the trailing clear button — that '×' is an independent nested action, so the small control owns the state and the large field does not also read as pressed. The visual-state boundary matches the action boundary."
156
156
  },
157
157
  "active": {
158
+ "isFocusState": true,
158
159
  "overlay": null,
159
160
  "border": "borderActive",
160
161
  "strokeWeight": "activeStrokeWeight",
161
162
  "caret": "visible",
162
163
  "showsClearWhenValue": true,
163
- "note": "The stroke steps from its rest `hairline` (1px) to `activeStrokeWeight` (2px) and re-tones to `borderActive` — but it is an inset `box-shadow`, not a `border`, so the box model is untouched: the field's footprint, caret, and text position are pixel-stable as it goes active. Nothing reflows. The clear button is shown only in this state (and only when the value is non-empty)."
164
+ "focusRing": {
165
+ "composition": "outward",
166
+ "layer": "::after overlay — position:absolute, inset:0, no reflow (DESIGN.md Focus ring composition)",
167
+ "innerCounterRing": { "width": "sys.borderWidth.hairline", "color": "sys.color.border.focused" },
168
+ "outerRing": { "width": "sys.borderWidth.thin", "color": "sys.color.border.focused" }
169
+ },
170
+ "note": "This IS the field's keyboard/input-focus state — `active` (caret visible, input engaged) and `:focus-visible` coincide for a text field, so there is no separate `focused` state; the focus ring described here (and in the parallel `focusIndicator` block) is the focus affordance. The stroke stays at `hairline` (1px) and re-tones to `borderActive` on active (no thickening) — but it is an inset `box-shadow`, not a `border`, so the box model is untouched: the field's footprint, caret, and text position are pixel-stable as it goes active. Nothing reflows. The clear button is shown only in this state (and only when the value is non-empty)."
164
171
  },
165
172
  "disabled": {
166
173
  "overlay": null,
167
- "background": "sys.color.surfaceContainerLow",
168
- "containerOpacity": "sys.state.disabled",
174
+ "background": "sys.color.background.disabled",
175
+ "text": "sys.color.text.disabled",
176
+ "placeholder": "sys.color.text.disabled",
177
+ "border": "sys.color.border.bold",
169
178
  "suppressClear": true,
170
179
  "suppressFocusRing": true,
171
- "cursor": "not-allowed"
180
+ "cursor": "not-allowed",
181
+ "note": "Explicit disabled (no opacity): neutral disabled fill + bold border + disabled text/placeholder."
172
182
  }
173
183
  },
174
184
  "focusIndicator": {
175
185
  "description": "Keyboard-focus visual — an accessibility indicator, not a lifecycle state. Composes over whichever lifecycle state the field is in (most commonly `active` since focus implies the caret is in the box). The `states.focused` block above is kept for JSX runtime consumers; this block is the parallel external-reader contract.",
176
186
  "composition": "outward",
177
- "compositionReason": "Action affordance with breathing room around it; the 3px outward extent is reserved by the surrounding layout.",
187
+ "compositionReason": "Action affordance with breathing room around it; the 1px outward ring is reserved by the surrounding layout.",
178
188
  "overlay": {
179
189
  "color": "label",
180
190
  "opacity": "sys.state.focus"
181
191
  },
182
192
  "ring": {
183
- "outerWidth": "sys.borderWidth.thin",
184
- "outerColor": "sys.color.focus",
185
- "insetWidth": "sys.borderWidth.hairline",
186
- "insetColor": "sys.color.focusInset"
193
+ "width": "sys.borderWidth.hairline",
194
+ "color": "sys.color.border.focused"
187
195
  },
188
196
  "trigger": ":focus-visible (keyboard / programmatic focus, never plain mouse click)"
189
197
  },
@@ -196,7 +204,7 @@
196
204
  },
197
205
  "forbidden": [
198
206
  "raw <input> styled with Tailwind / inline color — the input is wrapped in the chorus-field chrome that owns the stroke",
199
- "active-state stroke painted with sys.color.primary / a container tier — the active stroke is sys.color.onSurface (default appearance) or sys.color.error (error appearance), never primary or a container tier",
207
+ "active-state stroke painted with sys.color.background.primary / a container tier — the active stroke is sys.color.text.default (default appearance) or sys.color.text.danger (error appearance), never primary or a container tier",
200
208
  "stroke painted via `border:` — stroke is an inset box-shadow on the field",
201
209
  "helper text rendered outside the helperText slot"
202
210
  ]
@@ -10,7 +10,7 @@ Search-shaped single-line field — sibling of [Input](./input.md) with a leadin
10
10
 
11
11
  ## Default
12
12
 
13
- Neutral at-rest search bar — transparent fill, hairline `outlineVariant` stroke, `SearchIcon` left, placeholder in faint `outline` colour. Type into the specimen: placeholder → full-strength `onSurface` text, stroke steps to `active`, trailing clear ("×") appears at the right edge.
13
+ Neutral at-rest search bar — transparent fill, hairline `border.default` stroke, `SearchIcon` left, placeholder in faint `outline` colour. Type into the specimen: placeholder → full-strength `onSurface` text, stroke steps to `active`, trailing clear ("×") appears at the right edge.
14
14
 
15
15
  ```preview
16
16
  form-field/search/default
@@ -20,9 +20,7 @@ import { FormField } from '@teamblind-chorus/ui';
20
20
  <FormField variant="search" placeholder="Search" />
21
21
  ```
22
22
 
23
- ## Use cases
24
-
25
- ### Focus indicator
23
+ ## Focused state
26
24
 
27
25
  Focus ring layered on top of the `active` pill border. Same composition as [Input → Focus indicator](./input.md#focus-indicator).
28
26
 
@@ -41,7 +41,7 @@
41
41
  },
42
42
  "leading": {
43
43
  "required": true,
44
- "description": "The leading `SearchIcon` glyph pinned at the box's inner-left edge. Inherits the field's text colour (`sys.color.onSurface`); decorative — not a real button, has `aria-hidden`. 16px (`sys.icon.md`), matching the clear button's footprint so the two affixes balance.",
44
+ "description": "The leading `SearchIcon` glyph pinned at the box's inner-left edge. Inherits the field's text colour (`sys.color.text.default`); decorative — not a real button, has `aria-hidden`. 16px (`sys.icon.md`), matching the clear button's footprint so the two affixes balance.",
45
45
  "intrinsic": true
46
46
  },
47
47
  "input": {
@@ -64,18 +64,18 @@
64
64
  "slotGap": "sys.layout.inline.md",
65
65
  "radius": "sys.radius.full",
66
66
  "borderWidth": "sys.borderWidth.hairline",
67
- "activeStrokeWeight": "sys.borderWidth.thin",
67
+ "activeStrokeWeight": "sys.borderWidth.hairline",
68
68
  "textTypo": "sys.typo.body.md",
69
69
  "iconSize": "sys.icon.md"
70
70
  },
71
71
  "appearances": {
72
72
  "default": {
73
73
  "background": "transparent",
74
- "text": "sys.color.onSurface",
75
- "placeholder": "sys.color.outline",
76
- "borderRest": "sys.color.outlineVariant",
77
- "borderHover": "sys.color.outline",
78
- "borderActive": "sys.color.onSurface"
74
+ "text": "sys.color.text.default",
75
+ "placeholder": "sys.color.border.boldest",
76
+ "borderRest": "sys.color.border.default",
77
+ "borderHover": "sys.color.border.boldest",
78
+ "borderActive": "sys.color.border.focused"
79
79
  }
80
80
  },
81
81
  "states": {
@@ -96,35 +96,43 @@
96
96
  "nestedActionScope": "The pressed overlay (and hover stroke) is suppressed while the pointer presses / hovers the trailing clear button — that '×' is an independent nested action, so the small control owns the state and the large field does not also read as pressed. The visual-state boundary matches the action boundary."
97
97
  },
98
98
  "active": {
99
+ "isFocusState": true,
99
100
  "overlay": null,
100
101
  "border": "borderActive",
101
102
  "strokeWeight": "activeStrokeWeight",
102
103
  "caret": "visible",
103
104
  "showsClearWhenValue": true,
104
- "note": "The stroke steps from its rest `hairline` (1px) to `activeStrokeWeight` (2px) and re-tones to `borderActive` — but it is an inset `box-shadow`, not a `border`, so the box model is untouched: the field's footprint, caret, and text position are pixel-stable as it goes active. Nothing reflows. The clear button is shown only in this state (and only when the value is non-empty)."
105
+ "focusRing": {
106
+ "composition": "outward",
107
+ "layer": "::after overlay — position:absolute, inset:0, no reflow (DESIGN.md Focus ring composition)",
108
+ "innerCounterRing": { "width": "sys.borderWidth.hairline", "color": "sys.color.border.focused" },
109
+ "outerRing": { "width": "sys.borderWidth.thin", "color": "sys.color.border.focused" }
110
+ },
111
+ "note": "This IS the field's keyboard/input-focus state — `active` (caret visible, input engaged) and `:focus-visible` coincide for a text field, so there is no separate `focused` state; the focus ring described here (and in the parallel `focusIndicator` block) is the focus affordance. The stroke stays at `hairline` (1px) and re-tones to `borderActive` on active (no thickening) — but it is an inset `box-shadow`, not a `border`, so the box model is untouched: the field's footprint, caret, and text position are pixel-stable as it goes active. Nothing reflows. The clear button is shown only in this state (and only when the value is non-empty)."
105
112
  },
106
113
  "disabled": {
107
114
  "overlay": null,
108
- "background": "sys.color.surfaceContainerLow",
109
- "containerOpacity": "sys.state.disabled",
115
+ "background": "sys.color.background.disabled",
116
+ "text": "sys.color.text.disabled",
117
+ "placeholder": "sys.color.text.disabled",
118
+ "border": "sys.color.border.bold",
110
119
  "suppressClear": true,
111
120
  "suppressFocusRing": true,
112
- "cursor": "not-allowed"
121
+ "cursor": "not-allowed",
122
+ "note": "Explicit disabled (no opacity): neutral disabled fill + bold border + disabled text/placeholder."
113
123
  }
114
124
  },
115
125
  "focusIndicator": {
116
126
  "description": "Keyboard-focus visual — an accessibility indicator, not a lifecycle state. Composes over whichever lifecycle state the field is in. The `states.focused` block above is kept for JSX runtime consumers; this block is the parallel external-reader contract.",
117
127
  "composition": "outward",
118
- "compositionReason": "Action affordance with breathing room around it; the 3px outward extent is reserved by the surrounding layout.",
128
+ "compositionReason": "Action affordance with breathing room around it; the 1px outward ring is reserved by the surrounding layout.",
119
129
  "overlay": {
120
130
  "color": "label",
121
131
  "opacity": "sys.state.focus"
122
132
  },
123
133
  "ring": {
124
- "outerWidth": "sys.borderWidth.thin",
125
- "outerColor": "sys.color.focus",
126
- "insetWidth": "sys.borderWidth.hairline",
127
- "insetColor": "sys.color.focusInset"
134
+ "width": "sys.borderWidth.hairline",
135
+ "color": "sys.color.border.focused"
128
136
  },
129
137
  "trigger": ":focus-visible (keyboard / programmatic focus, never plain mouse click)"
130
138
  },
@@ -10,7 +10,7 @@ Input-shaped picker — same box, label, helper, and error re-tone as [Input](./
10
10
 
11
11
  ## Default
12
12
 
13
- Neutral at-rest field — transparent fill, hairline `outlineVariant` stroke, placeholder in faint `outline` colour. The trailing 16px chevron signals that the field opens a sheet rather than a caret.
13
+ Neutral at-rest field — transparent fill, hairline `border.default` stroke, placeholder in faint `outline` colour. The trailing 16px chevron signals that the field opens a sheet rather than a caret.
14
14
 
15
15
  ```preview
16
16
  form-field/select/default
@@ -20,9 +20,7 @@ import { FormField } from '@teamblind-chorus/ui';
20
20
  <FormField variant="select" placeholder="Select an option" onOpen={() => {}} />
21
21
  ```
22
22
 
23
- ## Use cases
24
-
25
- ### With leading icon
23
+ ## Leading icon
26
24
 
27
25
  Optional `leadingIcon` (16px / `sys.icon.md`) pins inner-left — same affordance as Search bar's glyph. Available on `input` and `select`.
28
26
 
@@ -40,22 +38,7 @@ import { GlobeIcon } from '@teamblind-chorus/ui/icons';
40
38
  />
41
39
  ```
42
40
 
43
- ### Group
44
-
45
- Pair a Select with an Input on one row via `<FormFieldGroup direction="horizontal">`. The group owns one shared label above and helper below; children render as bare boxes joined at `sys.layout.inline.md` gap. Typical pattern: leading Select (country dial code, currency, unit) + trailing real Input.
46
-
47
- ```preview
48
- form-field/select/group
49
- ---
50
- import { FormField, FormFieldGroup } from '@teamblind-chorus/ui';
51
-
52
- <FormFieldGroup direction="horizontal" label="Phone number" helper="We'll text a one-time code">
53
- <FormField variant="select" value="+82" onOpen={() => {}} style={{ flex: '0 0 96px' }} />
54
- <FormField variant="input" placeholder="010-0000-0000" />
55
- </FormFieldGroup>
56
- ```
57
-
58
- ### Focus indicator
41
+ ## Focused state
59
42
 
60
43
  Same as [Input → Focus indicator](./input.md#focus-indicator) — layered on top of the `active` border re-tone.
61
44
 
@@ -72,6 +55,21 @@ import { FormField } from '@teamblind-chorus/ui';
72
55
  />
73
56
  ```
74
57
 
58
+ ## Group
59
+
60
+ Pair a Select with an Input on one row via `<FormFieldGroup direction="horizontal">`. The group owns one shared label above and helper below; children render as bare boxes joined at `sys.layout.inline.md` gap. Typical pattern: leading Select (country dial code, currency, unit) + trailing real Input.
61
+
62
+ ```preview
63
+ form-field/select/group
64
+ ---
65
+ import { FormField, FormFieldGroup } from '@teamblind-chorus/ui';
66
+
67
+ <FormFieldGroup direction="horizontal" label="Phone number" helper="We'll text a one-time code">
68
+ <FormField variant="select" value="+82" onOpen={() => {}} style={{ flex: '0 0 96px' }} />
69
+ <FormField variant="input" placeholder="010-0000-0000" />
70
+ </FormFieldGroup>
71
+ ```
72
+
75
73
  ## Appearance
76
74
 
77
75
  Same two-appearance axis as [Input → Appearance](./input.md#appearance) — `default` and `error`. The error re-tone covers box, chevron, label and helper rung; placeholder text steps to `onErrorContainer`.
@@ -42,7 +42,7 @@
42
42
  "leadingIcon": {
43
43
  "type": "node",
44
44
  "optional": true,
45
- "description": "Optional 16px (`sys.icon.md`) decorative glyph pinned at the inner-left edge of the field. Tracks the field's active text colour (`sys.color.onSurface` on the default appearance, `sys.color.onErrorContainer` on `error`) so the glyph reads as part of the typed content."
45
+ "description": "Optional 16px (`sys.icon.md`) decorative glyph pinned at the inner-left edge of the field. Tracks the field's active text colour (`sys.color.text.default` on the default appearance, `sys.color.text.danger` on `error`) so the glyph reads as part of the typed content."
46
46
  },
47
47
  "onOpen": {
48
48
  "type": "function",
@@ -99,7 +99,7 @@
99
99
  "slotGap": "sys.layout.inline.md",
100
100
  "radius": "sys.radius.md",
101
101
  "borderWidth": "sys.borderWidth.hairline",
102
- "activeStrokeWeight": "sys.borderWidth.thin",
102
+ "activeStrokeWeight": "sys.borderWidth.hairline",
103
103
  "groupGap": "sys.layout.stack.xs",
104
104
  "labelTypo": "sys.typo.label.md",
105
105
  "helperTypo": "sys.typo.body.sm",
@@ -109,28 +109,28 @@
109
109
  "iconSize": "sys.icon.md"
110
110
  },
111
111
  "groupColors": {
112
- "label": "sys.color.onSurface",
113
- "helper": "sys.color.onSurfaceVariant",
114
- "helperError": "sys.color.error",
115
- "count": "sys.color.onSurfaceVariant",
116
- "countCurrent": "sys.color.onSurface"
112
+ "label": "sys.color.text.default",
113
+ "helper": "sys.color.text.subtle",
114
+ "helperError": "sys.color.text.danger",
115
+ "count": "sys.color.text.subtle",
116
+ "countCurrent": "sys.color.text.default"
117
117
  },
118
118
  "appearances": {
119
119
  "default": {
120
120
  "background": "transparent",
121
- "text": "sys.color.onSurface",
122
- "placeholder": "sys.color.outline",
123
- "borderRest": "sys.color.outlineVariant",
124
- "borderHover": "sys.color.outline",
125
- "borderActive": "sys.color.onSurface"
121
+ "text": "sys.color.text.default",
122
+ "placeholder": "sys.color.border.boldest",
123
+ "borderRest": "sys.color.border.default",
124
+ "borderHover": "sys.color.border.boldest",
125
+ "borderActive": "sys.color.border.focused"
126
126
  },
127
127
  "error": {
128
- "background": "sys.color.errorContainer",
129
- "text": "sys.color.onErrorContainer",
130
- "placeholder": "sys.color.onErrorContainer",
131
- "borderRest": "sys.color.error",
132
- "borderHover": "sys.color.error",
133
- "borderActive": "sys.color.error"
128
+ "background": "sys.color.background.danger",
129
+ "text": "sys.color.text.danger",
130
+ "placeholder": "sys.color.text.danger",
131
+ "borderRest": "sys.color.border.danger",
132
+ "borderHover": "sys.color.border.danger",
133
+ "borderActive": "sys.color.border.danger"
134
134
  }
135
135
  },
136
136
  "states": {
@@ -150,20 +150,31 @@
150
150
  }
151
151
  },
152
152
  "active": {
153
+ "isFocusState": true,
153
154
  "overlay": null,
154
155
  "border": "borderActive",
155
- "strokeWeight": "activeStrokeWeight"
156
+ "strokeWeight": "activeStrokeWeight",
157
+ "focusRing": {
158
+ "composition": "outward",
159
+ "layer": "::after overlay — position:absolute, inset:0, no reflow (DESIGN.md Focus ring composition)",
160
+ "innerCounterRing": { "width": "sys.borderWidth.hairline", "color": "sys.color.border.focused" },
161
+ "outerRing": { "width": "sys.borderWidth.thin", "color": "sys.color.border.focused" }
162
+ },
163
+ "note": "This IS the trigger's keyboard-focus / open state — `:focus-visible` and the engaged (open) state coincide for the select trigger, so there is no separate `focused` state; the focus ring described here (and in the parallel `focusIndicator` block) is the focus affordance. The stroke re-tones to `borderActive` at `activeStrokeWeight` (1px, = rest) as an inset box-shadow, pixel-stable (no reflow)."
156
164
  },
157
165
  "disabled": {
158
166
  "overlay": null,
159
- "background": "sys.color.surfaceContainerLow",
160
- "containerOpacity": "sys.state.disabled",
167
+ "background": "sys.color.background.disabled",
168
+ "text": "sys.color.text.disabled",
169
+ "placeholder": "sys.color.text.disabled",
170
+ "border": "sys.color.border.bold",
161
171
  "suppressFocusRing": true,
162
- "cursor": "not-allowed"
172
+ "cursor": "not-allowed",
173
+ "note": "Explicit disabled (no opacity): neutral disabled fill + bold border + disabled text/placeholder."
163
174
  }
164
175
  },
165
176
  "focusIndicator": {
166
- "description": "Same keyboard-focus indicator as Input — outward two-layer ring composed over the active stroke.",
177
+ "description": "Same keyboard-focus indicator as Input — outward single ring composed over the active stroke.",
167
178
  "composition": "outward",
168
179
  "compositionReason": "Action affordance with breathing room around it.",
169
180
  "overlay": {
@@ -171,10 +182,8 @@
171
182
  "opacity": "sys.state.focus"
172
183
  },
173
184
  "ring": {
174
- "outerWidth": "sys.borderWidth.thin",
175
- "outerColor": "sys.color.focus",
176
- "insetWidth": "sys.borderWidth.hairline",
177
- "insetColor": "sys.color.focusInset"
185
+ "width": "sys.borderWidth.hairline",
186
+ "color": "sys.color.border.focused"
178
187
  },
179
188
  "trigger": ":focus-visible"
180
189
  },
@@ -25,11 +25,9 @@ import { FormField } from '@teamblind-chorus/ui';
25
25
  />
26
26
  ```
27
27
 
28
- ## Use cases
28
+ ## Error
29
29
 
30
- ### Error appearance
31
-
32
- `appearance="error"` re-tones container to `errorContainer` and stroke to `error`. The optional `helper` paints in `sys.color.error` as the error caption.
30
+ `appearance="error"` re-tones container to `errorContainer` and stroke to `error`. The optional `helper` paints in `sys.color.text.danger` as the error caption.
33
31
 
34
32
  ```preview
35
33
  form-field/textarea-error
@@ -45,7 +43,7 @@ import { FormField } from '@teamblind-chorus/ui';
45
43
  />
46
44
  ```
47
45
 
48
- ### With character count
46
+ ## Character count
49
47
 
50
48
  `maxLength` caps value length and renders a `current/max` count below the box (right-aligned). Mutually exclusive with `helper` — count wins.
51
49
 
@@ -36,7 +36,7 @@
36
36
  "helper": {
37
37
  "type": "node",
38
38
  "optional": true,
39
- "description": "Assistive text rendered below the field box, left-aligned. Same rules as [input.helper](./input.md): mutually exclusive with `maxLength`, optional on every appearance, re-tones to `sys.color.error` on the error appearance."
39
+ "description": "Assistive text rendered below the field box, left-aligned. Same rules as [input.helper](./input.md): mutually exclusive with `maxLength`, optional on every appearance, re-tones to `sys.color.text.danger` on the error appearance."
40
40
  },
41
41
  "maxLength": {
42
42
  "type": "number",
@@ -61,7 +61,7 @@
61
61
  },
62
62
  "label": {
63
63
  "required": false,
64
- "description": "Visible label above the box. `sys.typo.label.md`, `sys.color.onSurface`. Associated with the textarea via `htmlFor`.",
64
+ "description": "Visible label above the box. `sys.typo.label.md`, `sys.color.text.default`. Associated with the textarea via `htmlFor`.",
65
65
  "accepts": ["text"]
66
66
  },
67
67
  "container": {
@@ -92,7 +92,7 @@
92
92
  "slotGap": "sys.layout.inline.md",
93
93
  "radius": "sys.radius.md",
94
94
  "borderWidth": "sys.borderWidth.hairline",
95
- "activeStrokeWeight": "sys.borderWidth.thin",
95
+ "activeStrokeWeight": "sys.borderWidth.hairline",
96
96
  "groupGap": "sys.layout.stack.xs",
97
97
  "labelTypo": "sys.typo.label.md",
98
98
  "helperTypo": "sys.typo.body.sm",
@@ -104,28 +104,28 @@
104
104
  "resize": "vertical"
105
105
  },
106
106
  "groupColors": {
107
- "label": "sys.color.onSurface",
108
- "helper": "sys.color.onSurfaceVariant",
109
- "helperError": "sys.color.error",
110
- "count": "sys.color.onSurfaceVariant",
111
- "countCurrent": "sys.color.onSurface"
107
+ "label": "sys.color.text.default",
108
+ "helper": "sys.color.text.subtle",
109
+ "helperError": "sys.color.text.danger",
110
+ "count": "sys.color.text.subtle",
111
+ "countCurrent": "sys.color.text.default"
112
112
  },
113
113
  "appearances": {
114
114
  "default": {
115
115
  "background": "transparent",
116
- "text": "sys.color.onSurface",
117
- "placeholder": "sys.color.outline",
118
- "borderRest": "sys.color.outlineVariant",
119
- "borderHover": "sys.color.outline",
120
- "borderActive": "sys.color.onSurface"
116
+ "text": "sys.color.text.default",
117
+ "placeholder": "sys.color.border.boldest",
118
+ "borderRest": "sys.color.border.default",
119
+ "borderHover": "sys.color.border.boldest",
120
+ "borderActive": "sys.color.border.focused"
121
121
  },
122
122
  "error": {
123
- "background": "sys.color.errorContainer",
124
- "text": "sys.color.onErrorContainer",
125
- "placeholder": "sys.color.onErrorContainer",
126
- "borderRest": "sys.color.error",
127
- "borderHover": "sys.color.error",
128
- "borderActive": "sys.color.error"
123
+ "background": "sys.color.background.danger",
124
+ "text": "sys.color.text.danger",
125
+ "placeholder": "sys.color.text.danger",
126
+ "borderRest": "sys.color.border.danger",
127
+ "borderHover": "sys.color.border.danger",
128
+ "borderActive": "sys.color.border.danger"
129
129
  }
130
130
  },
131
131
  "states": {
@@ -137,33 +137,41 @@
137
137
  "nestedActionScope": "The pressed overlay (and hover stroke) is suppressed while the pointer presses / hovers the trailing clear button — that '×' is an independent nested action, so the small control owns the state and the large field does not also read as pressed. The visual-state boundary matches the action boundary."
138
138
  },
139
139
  "active": {
140
+ "isFocusState": true,
140
141
  "overlay": null,
141
142
  "border": "borderActive",
142
143
  "strokeWeight": "activeStrokeWeight",
143
144
  "caret": "visible",
144
- "note": "Stroke steps from `hairline` (1px) to `activeStrokeWeight` (2px) as an inset box-shadow — same pixel-stable contract as input."
145
+ "focusRing": {
146
+ "composition": "outward",
147
+ "layer": "::after overlay — position:absolute, inset:0, no reflow (DESIGN.md Focus ring composition)",
148
+ "innerCounterRing": { "width": "sys.borderWidth.hairline", "color": "sys.color.border.focused" },
149
+ "outerRing": { "width": "sys.borderWidth.thin", "color": "sys.color.border.focused" }
150
+ },
151
+ "note": "This IS the field's keyboard/input-focus state — `active` (caret visible, input engaged) and `:focus-visible` coincide for a text field, so there is no separate `focused` state; the focus ring described here (and in the parallel `focusIndicator` block) is the focus affordance. Stroke stays at `hairline` (1px), re-toning to `borderActive` as an inset box-shadow (no thickening) — same pixel-stable contract as input."
145
152
  },
146
153
  "disabled": {
147
154
  "overlay": null,
148
- "background": "sys.color.surfaceContainerLow",
149
- "containerOpacity": "sys.state.disabled",
155
+ "background": "sys.color.background.disabled",
156
+ "text": "sys.color.text.disabled",
157
+ "placeholder": "sys.color.text.disabled",
158
+ "border": "sys.color.border.bold",
150
159
  "suppressFocusRing": true,
151
- "cursor": "not-allowed"
160
+ "cursor": "not-allowed",
161
+ "note": "Explicit disabled (no opacity): neutral disabled fill + bold border + disabled text/placeholder."
152
162
  }
153
163
  },
154
164
  "focusIndicator": {
155
- "description": "Same outward 3-layer ring as input.focusIndicator.",
165
+ "description": "Same outward single ring as input.focusIndicator.",
156
166
  "composition": "outward",
157
- "compositionReason": "Action affordance with breathing room around it; the 3px outward extent is reserved by the surrounding layout.",
167
+ "compositionReason": "Action affordance with breathing room around it; the 1px outward ring is reserved by the surrounding layout.",
158
168
  "overlay": {
159
169
  "color": "label",
160
170
  "opacity": "sys.state.focus"
161
171
  },
162
172
  "ring": {
163
- "outerWidth": "sys.borderWidth.thin",
164
- "outerColor": "sys.color.focus",
165
- "insetWidth": "sys.borderWidth.hairline",
166
- "insetColor": "sys.color.focusInset"
173
+ "width": "sys.borderWidth.hairline",
174
+ "color": "sys.color.border.focused"
167
175
  },
168
176
  "trigger": ":focus-visible (keyboard / programmatic focus, never plain mouse click)"
169
177
  },
@@ -23,9 +23,7 @@ import { Header } from '@teamblind-chorus/ui';
23
23
  />
24
24
  ```
25
25
 
26
- ## Use cases
27
-
28
- ### With drill-in chevron
26
+ ## Drill-in chevron
29
27
 
30
28
  `trailingIcon` mode — the chevron is a [Button `variant="icon"`](../button/icon.md) at `size="medium"` (32 × 32 capsule, 16-glyph) that owns its own tap target. The chevron paints in `onSurfaceVariant` and rotates -90° to read as chevron-right. The surrounding `<header>` element stays non-interactive — clicks land on the Icon Button, not on the row chrome. Use when the trailing affordance is "open this surface", not "commit a labelled action". `headerAction` and `trailingIcon` are mutually exclusive.
31
29
 
@@ -42,7 +40,7 @@ import { Header } from '@teamblind-chorus/ui';
42
40
  />
43
41
  ```
44
42
 
45
- ### With dropdown
43
+ ## Dropdown
46
44
 
47
45
  `headerDropdown` mode — the trailing affordance is a [Text Button dropdown](../button/text.md#dropdown) (`size="xsmall"`, default appearance) whose **label is the current value** ("Top", "Newest", "All time") and whose trailing chevron flips with `open` as a state signal. Use when the heading row needs an inline sort / filter / range disclosure — "Recommended channels [Top ▾]", "Posts [Last 7 days ▾]". Consumer owns the menu surface (Menu, popover, ListBox) and the `open` state; Header only renders the trigger.
48
46
 
@@ -69,7 +67,7 @@ function Example() {
69
67
  }
70
68
  ```
71
69
 
72
- ### Label only
70
+ ## Label only
73
71
 
74
72
  Set only `label` (omit `headerAction`, `trailingIcon`, and `headerDropdown`) for a labelled region that needs a heading without a trailing affordance. The row collapses to a single heading at the requested size — no spacer, no empty container.
75
73
 
@@ -84,7 +82,7 @@ import { Header } from '@teamblind-chorus/ui';
84
82
  ## Slots
85
83
 
86
84
  - **container** — outer row. Flex with `space-between`; label leads, action / icon / dropdown trails, 8px (`layout.inline.md`) gap. Full-bleed — transparent background and its own padding: `container.md` (16) inline (the shared content rail), `stack.lg` (24) block-start, `stack.md` (16) block-end — so the label lands on the same rail as the rows beneath it; bundling hosts add no inline padding of their own and absorb only the block padding. Stays a non-interactive `<header>` / `<div>` — the trailing affordance owns its own hit target.
87
- - **label** *(optional)* — heading text. `<h3>` by default; override the wrapper with `as="div"` when the surrounding host already owns the heading semantics. Color `sys.color.onSurface`. Typo per size.
85
+ - **label** *(optional)* — heading text. `<h3>` by default; override the wrapper with `as="div"` when the surrounding host already owns the heading semantics. Color `sys.color.text.default`. Typo per size.
88
86
  - **action** *(optional, headerAction mode)* — trailing Text Button. Fixed at `size="xsmall"`, `appearance="accent"`.
89
87
  - **icon** *(optional, trailingIcon mode)* — trailing [Icon Button](../button/icon.md) (`variant="icon"` `size="medium"`) hosting a 16px glyph (canonical: chevron-right). Its own tap target — clicks land on the Icon Button, not the surrounding header. Supply `aria-label` for the icon-only button (defaults to `"Open <label>"` when `label` is a string).
90
88
  - **dropdown** *(optional, headerDropdown mode)* — trailing [Text Button dropdown](../button/text.md#dropdown) (`size="xsmall"`, default appearance). Label is the current value; trailing chevron flips between `ChevronDownIcon` (closed) and `ChevronUpIcon` (open) tied to `headerDropdown.open`. Owns its own tap target — clicks land on the dropdown trigger, not the surrounding header.