@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
@@ -31,9 +31,7 @@ import { Feed } from '@teamblind-chorus/ui';
31
31
  />
32
32
  ```
33
33
 
34
- ## Use cases
35
-
36
- ### With flag
34
+ ## Flag
37
35
 
38
36
  Optional single-word editorial label (`HOT`, `NEW`, `PINNED`). Use sparingly.
39
37
 
@@ -57,9 +55,9 @@ import { Feed } from '@teamblind-chorus/ui';
57
55
  />
58
56
  ```
59
57
 
60
- ### With poll
58
+ ## Poll
61
59
 
62
- Inline poll module between body and mention/footer. Leading `PollFillIcon` + label paint in `sys.color.brand`; label constrained to the literal `Poll`.
60
+ Inline poll module between body and mention/footer. Leading `PollFillIcon` + label paint in `sys.color.text.brand`; label constrained to the literal `Poll`.
63
61
 
64
62
  ```preview
65
63
  feed/post-with-poll
@@ -81,9 +79,9 @@ import { Feed } from '@teamblind-chorus/ui';
81
79
  />
82
80
  ```
83
81
 
84
- ### With offer evaluation
82
+ ## Offer evaluation
85
83
 
86
- Same chrome as `poll`; leading glyph swaps to `CompensationFillIcon` and both glyph and label paint in `sys.color.success`. Label constrained to the literal `Offer`.
84
+ Same chrome as `poll`; leading glyph swaps to `CompensationFillIcon` and both glyph and label paint in `sys.color.text.success`. Label constrained to the literal `Offer`.
87
85
 
88
86
  ```preview
89
87
  feed/post-with-offer
@@ -105,7 +103,7 @@ import { Feed } from '@teamblind-chorus/ui';
105
103
  />
106
104
  ```
107
105
 
108
- ### With citation
106
+ ## Citation
109
107
 
110
108
  Citation module naming an external source. Hero image flush-left at 120px wide; title clamps to two lines.
111
109
 
@@ -128,9 +126,37 @@ import { Feed } from '@teamblind-chorus/ui';
128
126
  />
129
127
  ```
130
128
 
131
- ### Group
129
+ ## Full composition
130
+
131
+ Every optional slot present.
132
+
133
+ ```preview
134
+ feed/post-full
135
+ ---
136
+ import { Feed } from '@teamblind-chorus/ui';
137
+
138
+ <Feed
139
+ flag="HOT"
140
+ channel="Channel"
141
+ timestamp="Now"
142
+ followAction
143
+ meta={['Company', 'Job Function', 'Username']}
144
+ title="Title"
145
+ body="Body textBody textBody textBody textBody textBody textBody…"
146
+ thumbnail={{ alt: 'Cover', stacked: true }}
147
+ poll={{ label: 'Poll', participants: 'Number' }}
148
+ citation={{
149
+ title: 'Keep subject area text on two lines or less.',
150
+ source: 'Source',
151
+ }}
152
+ mention="@Mention"
153
+ engagement={{ likes: 999, comments: 999, views: 999 }}
154
+ />
155
+ ```
156
+
157
+ ## Group
132
158
 
133
- Three (or more) Post cards bundled inside `<FeedGroup>` for thread-grouped or topic-bundled feeds. The wrapper adds no chrome — inner Posts keep their padding and divider; the wrapper carries intent (`role="region"` + optional `aria-label`).
159
+ Three (or more) Post cards bundled inside `<FeedGroup>` for thread-grouped or topic-bundled feeds — the realistic composition the single-variant sections below each isolate one facet of. The wrapper adds no chrome — inner Posts keep their padding and divider; the wrapper carries intent (`role="region"` + optional `aria-label`). The last Post's bottom divider closes the bundle the same way a standalone Post would.
134
160
 
135
161
  ```preview
136
162
  feed/post-group
@@ -162,34 +188,6 @@ import { Feed, FeedGroup } from '@teamblind-chorus/ui';
162
188
  </FeedGroup>
163
189
  ```
164
190
 
165
- ### Full composition
166
-
167
- Every optional slot present.
168
-
169
- ```preview
170
- feed/post-full
171
- ---
172
- import { Feed } from '@teamblind-chorus/ui';
173
-
174
- <Feed
175
- flag="HOT"
176
- channel="Channel"
177
- timestamp="Now"
178
- followAction
179
- meta={['Company', 'Job Function', 'Username']}
180
- title="Title"
181
- body="Body textBody textBody textBody textBody textBody textBody…"
182
- thumbnail={{ alt: 'Cover', stacked: true }}
183
- poll={{ label: 'Poll', participants: 'Number' }}
184
- citation={{
185
- title: 'Keep subject area text on two lines or less.',
186
- source: 'Source',
187
- }}
188
- mention="@Mention"
189
- engagement={{ likes: 999, comments: 999, views: 999 }}
190
- />
191
- ```
192
-
193
191
  ## Slots
194
192
 
195
193
  - **flag** *(optional)* — single-word editorial label.
@@ -202,7 +200,7 @@ import { Feed } from '@teamblind-chorus/ui';
202
200
  - **citation** *(optional)* — inline link-share card with leading hero and source mark.
203
201
  - **mention** *(optional)* — tap-anywhere `@Mention` line under the body.
204
202
  - **engagement** — footer row of `xsmall` [Text Buttons](../button/text.md) — Likes / Comments commit, Views non-interactive.
205
- - **bottom divider** *(intrinsic)* — hairline `outlineVariant` seam at the card's bottom edge.
203
+ - **bottom divider** *(intrinsic)* — hairline `border.default` seam at the card's bottom edge.
206
204
 
207
205
  ## Anatomy
208
206
 
@@ -211,7 +209,7 @@ import { Feed } from '@teamblind-chorus/ui';
211
209
  | container | `surface` fill, `sys.layout.container.lg` (24/32) block × `sys.layout.container.md` (16) inline padding, `sys.layout.stack.md` between blocks |
212
210
  | flag | `label.sm` / Semibold, `brand` foreground |
213
211
  | avatar | [Thumbnail](../thumbnail/thumbnail.md) `size={32}` — delegated verbatim |
214
- | channel | 12 / Semibold, `onSurface`. `<a>`; hover underline; focus = hairline `sys.color.focus` at `sys.radius.xs` |
212
+ | channel | 12 / Semibold, `onSurface`. `<a>`; hover underline; focus = hairline `sys.color.border.focused` at `sys.radius.xs` |
215
213
  | timestamp | 12 / Semibold, `outline` |
216
214
  | followAction | 12 / Semibold, `primary` (inactive) → `onSurfaceVariant` (active) |
217
215
  | meta | 12 / Semibold, `onSurfaceVariant`. Each `<a>`; middot at `sys.layout.inline.sm` (4px), decorative |
@@ -221,8 +219,8 @@ import { Feed } from '@teamblind-chorus/ui';
221
219
  | poll / offer | `surfaceVariant` fill, `radius.md`, 12 × 16 padding, 48px min-height, 14px body. Leading icon + label at 4px gap, 12px to divider, 12px to count. `poll` paints `brand`; `offer` paints `success` (`ref.palette.green.500`) |
222
220
  | citation | Text-column `surfaceVariant`, `radius.md`, 120px-wide hero. 12px padding, 8px gap title↔source. All text 12px. Source mark 16 × 16 at 4px radius, 4px to source name |
223
221
  | mention | `body.sm`, `primary`, italic |
224
- | engagement | `xsmall` [Text Buttons](../button/text.md) + static `<span>` (Views). 16px glyph + 12 / Semibold label, 4 × 8 padding, 4px gap. Row gap 12px. Active Like retones label to `sys.color.brand` with `HeartFillIcon` |
225
- | bottom divider | `sys.borderWidth.hairline` × `sys.color.outlineVariant` — `border-bottom` on the card |
222
+ | engagement | `xsmall` [Text Buttons](../button/text.md) + static `<span>` (Views). 16px glyph + 12 / Semibold label, 4 × 8 padding, 4px gap. Row gap 12px. Active Like retones label to `sys.color.text.brand` with `HeartFillIcon` |
223
+ | bottom divider | `sys.borderWidth.hairline` × `sys.color.border.default` — `border-bottom` on the card |
226
224
 
227
225
  ## States
228
226
 
@@ -236,7 +234,7 @@ Feed itself is not a focus target; each focusable control paints its own ring pe
236
234
 
237
235
  - **Slot omission collapses without a gap.** Optional blocks drop out entirely.
238
236
  - **Truncation, not wrap.** `meta` / `title` truncate; `body` clamps to two lines; thumbnail is a flex sibling so the clamp computes against reduced inline width.
239
- - **Like is a toggle.** Tapping swaps `HeartIcon` → `HeartFillIcon` in `sys.color.brand` via `--button-text-label` and increments the count. Controlled (`liked` + `onLikeChange`) or uncontrolled. Aligns via Text Button's [optical alignment](../button/text.md#optical-alignment).
237
+ - **Like is a toggle.** Tapping swaps `HeartIcon` → `HeartFillIcon` in `sys.color.text.brand` via `--button-text-label` and increments the count. Controlled (`liked` + `onLikeChange`) or uncontrolled. Aligns via Text Button's [optical alignment](../button/text.md#optical-alignment).
240
238
  - **Comments commits; Views does not.** Views is a non-interactive `<span>`.
241
239
  - **Channel and meta are independent links.** Middot separators decorative (`aria-hidden`), outside link hit areas.
242
240
  - **`<FeedGroup>` bundles consecutive Posts.** Semantic wrapper only — inner Posts keep their own padding and bottom divider.
@@ -84,7 +84,7 @@
84
84
  "offer": {
85
85
  "type": "object",
86
86
  "optional": true,
87
- "description": "{ label, participants } — inline offer-evaluation banner. Same chrome as `poll` (surfaceVariant slab, leading glyph + label + divider + participant count), but the leading icon is `CompensationFillIcon` and the glyph + label paint in `sys.color.success` (resolves to `ref.palette.green.500`). Surfaces a 'compensation / offer evaluation' post — the author publishes their current salary or a competing offer and asks the community for better-option signals. `label` is constrained to the literal `\"Offer\"` (see `tagBanner.labelEnum`); `participants` is the participant count shown after the divider.",
87
+ "description": "{ label, participants } — inline offer-evaluation banner. Same chrome as `poll` (surfaceVariant slab, leading glyph + label + divider + participant count), but the leading icon is `CompensationFillIcon` and the glyph + label paint in `sys.color.text.success` (resolves to `ref.palette.green.500`). Surfaces a 'compensation / offer evaluation' post — the author publishes their current salary or a competing offer and asks the community for better-option signals. `label` is constrained to the literal `\"Offer\"` (see `tagBanner.labelEnum`); `participants` is the participant count shown after the divider.",
88
88
  "fields": {
89
89
  "label": {
90
90
  "type": "enum",
@@ -137,7 +137,7 @@
137
137
  },
138
138
  "channel": {
139
139
  "required": true,
140
- "description": "Channel / author name, rendered by the [Metadata](../metadata/metadata.md) cluster as an `<a>` link to the channel page. 12px / Semibold / onSurface. No underline at rest; hover underlines the link alone; focus paints a hairline `sys.color.focus` outline at `sys.radius.xs`.",
140
+ "description": "Channel / author name, rendered by the [Metadata](../metadata/metadata.md) cluster as an `<a>` link to the channel page. 12px / Semibold / onSurface. No underline at rest; hover underlines the link alone; focus paints a hairline `sys.color.border.focused` outline at `sys.radius.xs`.",
141
141
  "accepts": [
142
142
  "text"
143
143
  ]
@@ -182,7 +182,7 @@
182
182
  "required": false,
183
183
  "agentRequired": true,
184
184
  "omittedBehavior": "collapse",
185
- "fallbackOnMissingSrc": "sys.color.surfaceContainerHigh",
185
+ "fallbackOnMissingSrc": "sys.color.surface.sunken",
186
186
  "description": "80×80 square image at the trailing edge of the title/body block. radius.sm. `omittedBehavior: collapse` is a runtime safety net — when the prop is undefined the slot drops out of the layout (no reserved whitespace). `agentRequired: true` overrides this for scaffold/generation time: agents MUST always pass a `thumbnail` prop, falling back to `src: \"/placeholder.png\"` when no real subject is implied. `fallbackOnMissingSrc` is the dim-tone fill the component paints when `thumbnail` is present but `src` fails to load (network error or empty string). When the post carries 2+ images, overlays a `SquareStackIcon` badge at the top-right corner (16px / white / 4 inset on top + right).",
187
187
  "accepts": [
188
188
  "thumbnail"
@@ -190,11 +190,11 @@
190
190
  },
191
191
  "poll": {
192
192
  "required": false,
193
- "description": "Inline banner naming an attached poll. surfaceVariant fill, 48px min-height. Leading glyph `PollFillIcon` + label, both painted in `sys.color.brand`; label is constrained to the literal `Poll`."
193
+ "description": "Inline banner naming an attached poll. surfaceVariant fill, 48px min-height. Leading glyph `PollFillIcon` + label, both painted in `sys.color.text.brand`; label is constrained to the literal `Poll`."
194
194
  },
195
195
  "offer": {
196
196
  "required": false,
197
- "description": "Inline banner naming an attached offer-evaluation post. Identical chrome to `poll` — surfaceVariant fill, 48px min-height — with the leading glyph swapped to `CompensationFillIcon` and the glyph + label painted in `sys.color.success` (resolves to `ref.palette.green.500`). Label is constrained to the literal `Offer`."
197
+ "description": "Inline banner naming an attached offer-evaluation post. Identical chrome to `poll` — surfaceVariant fill, 48px min-height — with the leading glyph swapped to `CompensationFillIcon` and the glyph + label painted in `sys.color.text.success` (resolves to `ref.palette.green.500`). Label is constrained to the literal `Offer`."
198
198
  },
199
199
  "citation": {
200
200
  "required": false,
@@ -209,39 +209,39 @@
209
209
  },
210
210
  "engagement": {
211
211
  "required": false,
212
- "description": "Footer row of three counters laid out as an `xsmall` Text Button group. Likes + Comments are real Text Buttons (`text` / `xsmall` / `secondary`); Views renders as a non-interactive `<span>` matching the same 16-glyph / 12-label rhythm. Row gap follows the xsmall-rung group rule (`sys.layout.inline.sm` = 4px). Like is a toggle: active state retones the label to `sys.color.brand` (via `--button-text-label` override) and swaps `HeartIcon` → `HeartFillIcon`, with the count incrementing in lockstep. Never wraps."
212
+ "description": "Footer row of three counters laid out as an `xsmall` Text Button group. Likes + Comments are real Text Buttons (`text` / `xsmall` / `secondary`); Views renders as a non-interactive `<span>` matching the same 16-glyph / 12-label rhythm. Row gap follows the xsmall-rung group rule (`sys.layout.inline.sm` = 4px). Like is a toggle: active state retones the label to `sys.color.text.brand` (via `--button-text-label` override) and swaps `HeartIcon` → `HeartFillIcon`, with the count incrementing in lockstep. Never wraps."
213
213
  }
214
214
  },
215
215
  "sizing": {
216
- "containerFill": "sys.color.surface",
216
+ "containerFill": "sys.color.surface.default",
217
217
  "containerPaddingBlock": "sys.layout.container.lg",
218
218
  "containerPaddingInline": "sys.layout.container.md",
219
219
  "interBlockGap": "sys.layout.stack.md",
220
220
  "flagTypo": "sys.typo.label.sm",
221
- "flagColor": "sys.color.brand",
221
+ "flagColor": "sys.color.text.brand",
222
222
  "avatarSize": 32,
223
223
  "channelTypo": "sys.typo.label.sm",
224
- "channelColor": "sys.color.onSurface",
224
+ "channelColor": "sys.color.text.default",
225
225
  "timestampTypo": "sys.typo.label.sm",
226
- "timestampColor": "sys.color.outline",
226
+ "timestampColor": "sys.color.border.boldest",
227
227
  "followActionTypo": "sys.typo.label.sm",
228
- "followActionColorInactive": "sys.color.primary",
229
- "followActionColorActive": "sys.color.onSurfaceVariant",
228
+ "followActionColorInactive": "sys.color.text.link",
229
+ "followActionColorActive": "sys.color.text.subtle",
230
230
  "channelRowGap": "sys.layout.inline.md",
231
231
  "metaTypo": "sys.typo.label.sm",
232
- "metaColor": "sys.color.onSurfaceVariant",
232
+ "metaColor": "sys.color.text.subtle",
233
233
  "metaSeparatorGap": "sys.layout.inline.sm",
234
234
  "titleBodyGap": "ref.space.100",
235
235
  "titleTypo": "sys.typo.heading.sm",
236
- "titleColor": "sys.color.onSurface",
236
+ "titleColor": "sys.color.text.default",
237
237
  "containerBottomDividerWidth": "sys.borderWidth.hairline",
238
- "containerBottomDividerColor": "sys.color.outlineVariant",
238
+ "containerBottomDividerColor": "sys.color.border.default",
239
239
  "bodyTypo": "sys.typo.body.sm",
240
- "bodyColor": "sys.color.onSurfaceVariant",
240
+ "bodyColor": "sys.color.text.subtle",
241
241
  "bodyLineClamp": 2,
242
242
  "thumbnailSize": "80 × 80",
243
243
  "thumbnailRadius": "sys.radius.sm",
244
- "thumbnailFallbackFill": "sys.color.surfaceContainerHigh",
244
+ "thumbnailFallbackFill": "sys.color.surface.sunken",
245
245
  "thumbnailMultipleBadge": {
246
246
  "trigger": "thumbnail.stacked === true (2+ images attached to the post)",
247
247
  "icon": "SquareStackIcon",
@@ -254,49 +254,49 @@
254
254
  },
255
255
  "engagementCounterShape": "xsmall Text Button (Likes / Comments) or matching static span (Views)",
256
256
  "engagementLabelTypo": "sys.typo.label.sm",
257
- "engagementLabelColor": "sys.color.onSurfaceVariant",
257
+ "engagementLabelColor": "sys.color.text.subtle",
258
258
  "engagementIconSize": "sys.icon.md",
259
259
  "engagementSlotGap": "sys.layout.inline.sm",
260
260
  "engagementRowGap": "sys.layout.inline.lg",
261
- "likeActiveColor": "sys.color.brand",
261
+ "likeActiveColor": "sys.color.text.brand",
262
262
  "likeActiveIcon": "HeartFillIcon",
263
263
  "likeRestIcon": "HeartIcon",
264
264
  "likeLeadingOffset": "Inherited from Text Button's optical-alignment default — no per-call offset; the heart glyph sits at the card's content rail automatically."
265
265
  },
266
266
  "poll": {
267
- "fill": "sys.color.surfaceVariant",
267
+ "fill": "sys.color.surface.sunken",
268
268
  "radius": "sys.radius.md",
269
269
  "paddingBlock": "ref.space.150",
270
270
  "paddingInline": "ref.space.200",
271
271
  "minHeight": "ref.space.600",
272
272
  "bodyTypo": "14",
273
273
  "glyph": "PollFillIcon",
274
- "glyphColor": "sys.color.brand",
275
- "labelColor": "sys.color.brand",
274
+ "glyphColor": "sys.color.text.brand",
275
+ "labelColor": "sys.color.text.brand",
276
276
  "labelLiteral": "Poll",
277
- "anatomy": "Leading 'poll-title' sub-container groups glyph (brand tone) + label at 4px gap; 12px flex gap to vertical outlineVariant divider; 12px gap to participant count."
277
+ "anatomy": "Leading 'poll-title' sub-container groups glyph (brand tone) + label at 4px gap; 12px flex gap to vertical border.default divider; 12px gap to participant count."
278
278
  },
279
279
  "offer": {
280
- "fill": "sys.color.surfaceVariant",
280
+ "fill": "sys.color.surface.sunken",
281
281
  "radius": "sys.radius.md",
282
282
  "paddingBlock": "ref.space.150",
283
283
  "paddingInline": "ref.space.200",
284
284
  "minHeight": "ref.space.600",
285
285
  "bodyTypo": "14",
286
286
  "glyph": "CompensationFillIcon",
287
- "glyphColor": "sys.color.success",
287
+ "glyphColor": "sys.color.icon.success",
288
288
  "glyphColorResolved": "ref.palette.green.500",
289
- "labelColor": "sys.color.success",
289
+ "labelColor": "sys.color.text.success",
290
290
  "labelLiteral": "Offer",
291
291
  "anatomy": "Identical to `poll` — same surfaceVariant slab, same 48 min-height, same leading sub-container + divider + participant count. Differs only in the glyph (CompensationFillIcon) and the editorial tone (success / green-500 instead of brand)."
292
292
  },
293
293
  "tagBanner": {
294
294
  "shared": "`poll` and `offer` are siblings of the same banner family — same chrome, same anatomy. They differ only on the glyph + editorial tone.",
295
- "labelEnum": "The leading-icon label is constrained to exactly two literals across both modules combined: `\"Poll\"` (for the poll banner) and `\"Offer\"` (for the offer-evaluation banner). Consumers (including Lovable) MUST pass one of these two values, or omit `label` to use the default for the chosen banner kind. No other label string is valid.",
295
+ "labelEnum": "The leading-icon label is constrained to exactly two literals across both modules combined: `\"Poll\"` (for the poll banner) and `\"Offer\"` (for the offer-evaluation banner). Consumers MUST pass one of these two values, or omit `label` to use the default for the chosen banner kind. No other label string is valid.",
296
296
  "renderOrder": "When both modules are present on the same post, `poll` renders above `offer`. Either may stand alone."
297
297
  },
298
298
  "citation": {
299
- "textColumnFill": "sys.color.surfaceVariant",
299
+ "textColumnFill": "sys.color.surface.sunken",
300
300
  "outerRadius": "sys.radius.md",
301
301
  "heroWidth": "120px",
302
302
  "heroBleed": "Flush against the card's leading edge — no container padding, no inter-column gap.",
@@ -311,7 +311,7 @@
311
311
  },
312
312
  "mention": {
313
313
  "typo": "sys.typo.body.sm",
314
- "color": "sys.color.primary",
314
+ "color": "sys.color.text.link",
315
315
  "fontStyle": "italic"
316
316
  },
317
317
  "states": {
@@ -326,25 +326,21 @@
326
326
  "opacity": "sys.state.focus"
327
327
  },
328
328
  "ring": {
329
- "outerWidth": "sys.borderWidth.thin",
330
- "outerColor": "sys.color.focus",
331
- "outerLayerPosition": "depth 0..2px from the card edge (the outer stroke)",
332
- "insetWidth": "sys.borderWidth.hairline",
333
- "insetColor": "sys.color.focusInset",
334
- "insetLayerPosition": "depth 2..3px from the card edge (the counter-ring just inside the outer stroke)",
329
+ "width": "sys.borderWidth.hairline",
330
+ "color": "sys.color.border.focused",
335
331
  "implementation": "inset box-shadow constrained strictly inside the card's footprint."
336
332
  },
337
333
  "trigger": ":focus-visible (keyboard / programmatic focus, never plain mouse click)"
338
334
  },
339
335
  "behavior": {
340
336
  "slotOmissionCollapses": "Runtime safety net — when these optional blocks (flag, followAction, thumbnail, poll, offer, citation, mention) are absent the layout reflows cleanly with no reserved whitespace. This is a graceful-degradation contract for downstream consumers, NOT a license for agents to omit slots at scaffold time. `thumbnail` in particular is `agentRequired` (see props.thumbnail) — agents must always pass it with `/placeholder.png` when no subject photo is implied.",
341
- "containerBottomDivider": "Each Post card carries a hairline bottom divider (`sys.borderWidth.hairline` × `sys.color.outlineVariant`) so consecutive cards in a stream read with a deliberate inter-card seam. The divider is `border-bottom` on the card itself — layout-safe under `box-sizing: border-box`, no shadow tricks needed. Inside a `<FeedGroup>` (the 3-up bundle case), the inner posts keep their dividers and the group's last card carries the same outer-bottom divider as a standalone Post — the rhythm is identical, the wrapper is purely semantic.",
337
+ "containerBottomDivider": "Each Post card carries a hairline bottom divider (`sys.borderWidth.hairline` × `sys.color.border.default`) so consecutive cards in a stream read with a deliberate inter-card seam. The divider is `border-bottom` on the card itself — layout-safe under `box-sizing: border-box`, no shadow tricks needed. Inside a `<FeedGroup>` (the 3-up bundle case), the inner posts keep their dividers and the group's last card carries the same outer-bottom divider as a standalone Post — the rhythm is identical, the wrapper is purely semantic.",
342
338
  "groupCase": "Three (or more) Post cards bundled vertically inside a `<FeedGroup>` semantic wrapper. Used for thread-grouped or topic-bundled feeds where consecutive posts belong together. The group adds no extra surface chrome — each Post keeps its own `surface` fill, `container.lg/md` padding, and bottom divider; the wrapper only contributes a `role=\"region\"` + optional `aria-label` for the bundle's intent. There is no fixed item count contract; 3 is the canonical demo case.",
343
339
  "tagBannerLabelConstrained": "The poll and offer banners share one closed enum for their leading label: `\"Poll\"` or `\"Offer\"`. Each module defaults to its own literal when `label` is omitted; passing any other string is a contract violation.",
344
340
  "truncationNotWrap": "meta and title truncate; body clamps to two lines. The card never grows to fit a long title.",
345
341
  "thumbnailFlexSibling": "Title and body share their inline space with the thumbnail when both are present; thumbnail is a flex sibling, not floated, so the body's two-line clamp computes against the reduced inline width.",
346
342
  "engagementNoReflow": "Footer row stays single-line; tiny screens scroll the row rather than wrapping.",
347
- "likeToggle": "Tapping Likes increments the count and swaps the rest-state HeartIcon / secondary appearance for the active-state HeartFillIcon with the label re-toned to `sys.color.brand` (via a `--button-text-label` override — not the Text Button `primary` appearance — because Feed's active-like colour is the editorial brand tone, not the interactive primary). Glyph re-colours via `currentColor`. Tapping again decrements and reverts. Controlled via `liked` + `onLikeChange`, or left uncontrolled for in-demo behaviour.",
343
+ "likeToggle": "Tapping Likes increments the count and swaps the rest-state HeartIcon / secondary appearance for the active-state HeartFillIcon with the label re-toned to `sys.color.text.brand` (via a `--button-text-label` override — not the Text Button `primary` appearance — because Feed's active-like colour is the editorial brand tone, not the interactive primary). Glyph re-colours via `currentColor`. Tapping again decrements and reverts. Controlled via `liked` + `onLikeChange`, or left uncontrolled for in-demo behaviour.",
348
344
  "likeLeadingNudge": "The Like glyph aligns flush with the card's content rail via Text Button's optical-alignment default — the chrome bleeds outward by its own padding on every side, so the visible heart sits at the rail without any per-call offset. The 4px row gap to Comments stays unaffected.",
349
345
  "viewsNonInteractive": "Views renders as a non-interactive `<span>` matching the xsmall Text Button visual rhythm — no hover / pressed / focus, no `cursor: pointer`. It is a metric display, not an action that has been turned off, so `aria-disabled` is the wrong shape.",
350
346
  "channelAndMetaAreLinks": "Channel name and every `meta` item are independent links. The middot separator between meta items is decorative (`aria-hidden`) and sits outside each link's hit area, so the hover underline never spans the separator."
@@ -353,8 +349,8 @@
353
349
  "thumbnail slot omitted at scaffold / agent time — every feed post MUST pass a `thumbnail` prop. Fill `src` with a real subject photo when implied, the bundled `/placeholder.png` otherwise. The runtime `slotOmissionCollapses` reflow is a safety net for downstream consumers, NOT permission for agents to skip the slot",
354
350
  "author meta collapsed into a single line — the two meta rows (channel · time / workplace · role · username) are anatomy invariants",
355
351
  "engagement footer rendered with raw <button> — like / comments / views are button/text + non-interactive span (views) per the spec",
356
- "active-like color painted with sys.color.primary — active-like uses sys.color.brand via the --button-text-label plumbing var",
357
- "raw <a> for hashtag / mention — inline link tone uses sys.color.primary via the body markup contract, not raw anchor styling",
352
+ "active-like color painted with sys.color.background.primary — active-like uses sys.color.text.brand via the --button-text-label plumbing var",
353
+ "raw <a> for hashtag / mention — inline link tone uses sys.color.background.primary via the body markup contract, not raw anchor styling",
358
354
  "Feed wrapped in a padding-inline div / className=\"px-*\" / style={{ padding }} — Feed is layoutInset=\"full-bleed\" and pays its own page rail once; an outer padding wrapper double-pays the gutter and misaligns the card with NavigationBar / TabBar (the runtime useFullBleedGuard warns on this)",
359
355
  "Feed wrapped in an external <Link>/<a> to make the card navigable — pass the `onClick` prop instead (the card surface becomes the target, interior affordances route independently). An outer anchor re-pays the page rail as a gutter AND nests the card's own anchors (channel link, citation, mention), which breaks DOM parsing"
360
356
  ]
@@ -8,6 +8,6 @@ The text-entry primitives — controls the user types into or picks a value from
8
8
 
9
9
  ## Sub-components
10
10
 
11
- - **[Input](./input.md)** — Single-line text input. Surface-toned box, hairline `outlineVariant` rest stroke, `onSurface` border while active, optional trailing clear ("×"). Corner radius `sys.radius.md`. `error` appearance re-tones to error family. Compose multiple via the **Group** use case.
11
+ - **[Input](./input.md)** — Single-line text input. Surface-toned box, hairline `border.default` rest stroke, `onSurface` border while active, optional trailing clear ("×"). Corner radius `sys.radius.md`. `error` appearance re-tones to error family. Compose multiple via the **Group** use case.
12
12
  - **[Search bar](./search.md)** — Same anatomy and state model as Input with three deltas: leading `SearchIcon` glyph, corner radius stepped to `sys.radius.full` (pill), bare-box-only (no `label`, `helper`, or `maxLength`).
13
13
  - **[Select](./select.md)** — Input-shaped picker. Read-only field with a trailing `ArrowDownIcon` (16px) chevron; clicking opens a `BottomSheet` with the option list. Supports the same optional leading icon as Input, plus a horizontal **Group** use case (country code + number, currency + amount).
@@ -10,7 +10,7 @@ Single-line text field — a bordered, transparent-fill box for short values. Op
10
10
 
11
11
  ## Default
12
12
 
13
- Transparent fill, hairline `outlineVariant` stroke, placeholder in faint `outline`. Type to see the lifecycle: placeholder → value, stroke steps up, trailing clear (*×*) appears.
13
+ Transparent fill, hairline `border.default` stroke, placeholder in faint `outline`. Type to see the lifecycle: placeholder → value, stroke steps up, trailing clear (*×*) appears.
14
14
 
15
15
  ```preview
16
16
  form-field/input/default
@@ -20,11 +20,9 @@ import { FormField } from '@teamblind-chorus/ui';
20
20
  <FormField variant="input" placeholder="Place holder" />
21
21
  ```
22
22
 
23
- ## Use cases
23
+ ## Error
24
24
 
25
- ### Error
26
-
27
- `errorContainer` wash, full-strength `error` stroke, `onErrorContainer` text. Optional helper rung re-tones to `sys.color.error`.
25
+ `errorContainer` wash, full-strength `error` stroke, `onErrorContainer` text. Optional helper rung re-tones to `sys.color.text.danger`.
28
26
 
29
27
  ```preview
30
28
  form-field/input/error
@@ -55,7 +53,7 @@ import { FormField } from '@teamblind-chorus/ui';
55
53
  />
56
54
  ```
57
55
 
58
- ### Label, assistive text & Count
56
+ ## Label, helper & count
59
57
 
60
58
  When any of `label` / `helper` / `maxLength` is set, the box wraps in a `.chorus-field-group` flex column at `sys.layout.stack.xs` between rungs — label above, helper left or count right below. Helper and count are mutually exclusive; pass both and count wins.
61
59
 
@@ -88,7 +86,7 @@ import { FormField } from '@teamblind-chorus/ui';
88
86
  />
89
87
  ```
90
88
 
91
- ### Leading icon
89
+ ## Leading icon
92
90
 
93
91
  Optional `leadingIcon` (16px / `sys.icon.md`) pinned inner-left. Decorative (`aria-hidden`); tracks the field's active text colour. Also available on [`select`](./select.md).
94
92
 
@@ -107,23 +105,7 @@ import { LocationIcon } from '@teamblind-chorus/ui/icons';
107
105
  />
108
106
  ```
109
107
 
110
- ### Group
111
-
112
- Compose multiple Inputs into a column via `<FormFieldGroup>`. Each rung keeps its own label / helper / count; group inserts `sys.layout.stack.md` (16px) between rungs — sign-up and profile forms.
113
-
114
- ```preview
115
- form-field/input/group
116
- ---
117
- import { FormField, FormFieldGroup } from '@teamblind-chorus/ui';
118
-
119
- <FormFieldGroup direction="vertical">
120
- <FormField variant="input" label="Name" placeholder="Your name" />
121
- <FormField variant="input" label="Email" placeholder="you@example.com" helper="We'll send a confirmation" />
122
- <FormField variant="input" label="Bio" placeholder="One sentence about you" />
123
- </FormFieldGroup>
124
- ```
125
-
126
- ### Focus indicator
108
+ ## Focused state
127
109
 
128
110
  Focus ring layered on top of the `active` border re-tone.
129
111
 
@@ -139,22 +121,38 @@ import { FormField } from '@teamblind-chorus/ui';
139
121
  />
140
122
  ```
141
123
 
124
+ ## Group
125
+
126
+ Compose multiple Inputs into a column via `<FormFieldGroup>`. Each rung keeps its own label / helper / count; group inserts `sys.layout.stack.md` (16px) between rungs — sign-up and profile forms.
127
+
128
+ ```preview
129
+ form-field/input/group
130
+ ---
131
+ import { FormField, FormFieldGroup } from '@teamblind-chorus/ui';
132
+
133
+ <FormFieldGroup direction="vertical">
134
+ <FormField variant="input" label="Name" placeholder="Your name" />
135
+ <FormField variant="input" label="Email" placeholder="you@example.com" helper="We'll send a confirmation" />
136
+ <FormField variant="input" label="Bio" placeholder="One sentence about you" />
137
+ </FormFieldGroup>
138
+ ```
139
+
142
140
  ## Slots
143
141
 
144
142
  - **container** — the box. Owns transparent fill, the inset-`box-shadow` stroke, radius, padding, and focus ring.
145
143
  - **input** — editable text. Single line; value-driven placeholder swap.
146
144
  - **clear** — trailing *×* button (`XCircleFillIcon`). Shown only while the box is active and holds a non-empty value.
147
145
  - **group** *(optional)* — wrapper holding label / box / helper / count when any is supplied.
148
- - **label** *(optional)* — visible label above the box. `sys.typo.label.md` / `sys.color.onSurface`, bound via `htmlFor`.
149
- - **helper** *(optional)* — assistive text below the box, left-aligned. `sys.typo.body.sm` / `sys.color.onSurfaceVariant`, referenced by `aria-describedby`; re-tones to `error` on the error appearance. Mutually exclusive with `count`.
150
- - **count** *(optional)* — `current/max` count below the box, right-aligned. `sys.typo.body.sm` / `sys.color.onSurfaceVariant`; live digit steps to `label.md` weight. `aria-live="polite"`. Mutually exclusive with `helper`.
146
+ - **label** *(optional)* — visible label above the box. `sys.typo.label.md` / `sys.color.text.default`, bound via `htmlFor`.
147
+ - **helper** *(optional)* — assistive text below the box, left-aligned. `sys.typo.body.sm` / `sys.color.text.subtle`, referenced by `aria-describedby`; re-tones to `error` on the error appearance. Mutually exclusive with `count`.
148
+ - **count** *(optional)* — `current/max` count below the box, right-aligned. `sys.typo.body.sm` / `sys.color.text.subtle`; live digit steps to `label.md` weight. `aria-live="polite"`. Mutually exclusive with `helper`.
151
149
 
152
150
  ## Appearance
153
151
 
154
152
  | Appearance | Background | Border (rest) | Text | Placeholder |
155
153
  |------------|-------------------------------|-----------------------------------------------------|-------------------------------|-------------------------------|
156
- | `default` | `transparent` | `sys.color.outlineVariant` (`borderWidth.hairline`) | `sys.color.onSurface` | `sys.color.outline` |
157
- | `error` | `sys.color.errorContainer` | `sys.color.error` (`borderWidth.hairline`) | `sys.color.onErrorContainer` | `sys.color.onErrorContainer` |
154
+ | `default` | `transparent` | `sys.color.border.default` (`borderWidth.hairline`) | `sys.color.text.default` | `sys.color.border.boldest` |
155
+ | `error` | `sys.color.background.danger` | `sys.color.text.danger` (`borderWidth.hairline`) | `sys.color.text.danger` | `sys.color.text.danger` |
158
156
 
159
157
  Placeholder vs. value is value-driven, not focus-driven.
160
158
 
@@ -179,14 +177,14 @@ A single fixed footprint.
179
177
 
180
178
  ## States
181
179
 
182
- Four interactive states. Load-bearing: `active` — the field has the caret; stroke re-tones to `sys.color.onSurface` at 2px.
180
+ Four interactive states. Load-bearing: `active` — the field has the caret; stroke re-tones to `sys.color.text.default` at 2px.
183
181
 
184
182
  | State | Stroke (inset box-shadow) | Additional |
185
183
  |------------|---------------------------------------------------------------------------|------------|
186
- | `default` | 1px, `borderRest` (`outlineVariant` / `error`) | Caret hidden. |
187
- | `hovered` | 1px, `sys.color.outline` (error: stays `error`) | `:hover`. |
188
- | `pressed` | 1px, `sys.color.outline` + `text` overlay at `sys.state.pressed` | `:active`. |
189
- | `active` | 2px, `sys.color.onSurface` (error: `error`) | Caret visible per the [system caret rule](../../DESIGN.md#caret). |
184
+ | `default` | 1px, `borderRest` (`border.default` / `error`) | Caret hidden. |
185
+ | `hovered` | 1px, `sys.color.border.boldest` (error: stays `error`) | `:hover`. |
186
+ | `pressed` | 1px, `sys.color.border.boldest` + `text` overlay at `sys.state.pressed` | `:active`. |
187
+ | `active` | 2px, `sys.color.text.default` (error: `error`) | Caret visible per the [system caret rule](../../DESIGN.md#caret). |
190
188
  | `disabled` | 1px, `borderRest`; container at `sys.state.disabled` opacity | Fill steps to `surfaceContainerLow`; overlays / clear suppressed; `cursor: not-allowed`. |
191
189
 
192
190
  ## Focus indicator