@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
@@ -20,9 +20,7 @@ import { Button } from '@teamblind-chorus/ui';
20
20
  <Button variant="text">Not now</Button>
21
21
  ```
22
22
 
23
- ## Use cases
24
-
25
- ### Accent
23
+ ## Accent
26
24
 
27
25
  Brand-blue label (`primary`) — the inline commit. Reach for `accent` when the button reads as a navigational link (*See all*, *Follow*, *View details*).
28
26
 
@@ -34,7 +32,7 @@ import { Button } from '@teamblind-chorus/ui';
34
32
  <Button variant="text" appearance="accent">Skip</Button>
35
33
  ```
36
34
 
37
- ### On primary
35
+ ## On primary
38
36
 
39
37
  Always-white label on top of a `primary`-filled host (Tooltip `default`). Theme-stable.
40
38
 
@@ -46,7 +44,7 @@ import { Button } from '@teamblind-chorus/ui';
46
44
  <Button variant="text" appearance="onPrimary">Got it</Button>
47
45
  ```
48
46
 
49
- ### Inverse
47
+ ## Inverse
50
48
 
51
49
  For inverse hosts (Toast, coach-mark, snackbar). Label paints `inverseOnSurface`; tokens flip with theme.
52
50
 
@@ -58,7 +56,7 @@ import { Button } from '@teamblind-chorus/ui';
58
56
  <Button variant="text" appearance="inverse">Undo</Button>
59
57
  ```
60
58
 
61
- ### With leading icon
59
+ ## Leading icon
62
60
 
63
61
  16px (`sys.icon.md`) glyph before the label at 4px gap — fixed across rungs.
64
62
 
@@ -70,7 +68,7 @@ import { Button, ChevronLeftIcon } from '@teamblind-chorus/ui';
70
68
  <Button variant="text" leadingIcon={<ChevronLeftIcon />}>Back</Button>
71
69
  ```
72
70
 
73
- ### With trailing icon
71
+ ## Trailing icon
74
72
 
75
73
  Destination glyph after the label — chevron-right *Continue*, external-link *Open in new tab*.
76
74
 
@@ -82,7 +80,7 @@ import { Button, ChevronRightIcon } from '@teamblind-chorus/ui';
82
80
  <Button variant="text" trailingIcon={<ChevronRightIcon />}>Continue</Button>
83
81
  ```
84
82
 
85
- ### Dropdown
83
+ ## Dropdown
86
84
 
87
85
  Disclosure trigger: the label reads as the **current value**, the trailing chevron flips with state — `ChevronDownIcon` at rest, `ChevronUpIcon` while the menu is open. Pair `aria-haspopup` + `aria-expanded` on the trigger and portal the menu so it escapes any clipping ancestor; never freeze the chevron when the menu is open.
88
86
 
@@ -112,31 +110,31 @@ function Example() {
112
110
  }
113
111
  ```
114
112
 
115
- ### Group
113
+ ## Focus ring
116
114
 
117
- Optical alignment means chrome-to-chrome gap **is** the visible label-to-label distance. Row gap: `medium`/`small` → 16px (`sys.layout.inline.xl`); `xsmall` → 12px (`sys.layout.inline.lg`).
115
+ Standard ring.
118
116
 
119
117
  ```preview
120
- button/text/group
118
+ button/text/focused
121
119
  ---
122
120
  import { Button } from '@teamblind-chorus/ui';
123
121
 
124
- <div style={{ display: 'inline-flex', gap: 'var(--sys-layout-inline-md)' }}>
125
- <Button variant="text">Cancel</Button>
126
- <Button variant="text" appearance="accent">Save</Button>
127
- </div>
122
+ <Button variant="text" state="focused">Skip</Button>
128
123
  ```
129
124
 
130
- ### Focus indicator
125
+ ## Group
131
126
 
132
- Standard ring.
127
+ Optical alignment means chrome-to-chrome gap **is** the visible label-to-label distance. Row gap: `medium`/`small` → 16px (`sys.layout.inline.xl`); `xsmall` → 12px (`sys.layout.inline.lg`).
133
128
 
134
129
  ```preview
135
- button/text/focused
130
+ button/text/group
136
131
  ---
137
132
  import { Button } from '@teamblind-chorus/ui';
138
133
 
139
- <Button variant="text" state="focused">Skip</Button>
134
+ <div style={{ display: 'inline-flex', gap: 'var(--sys-layout-inline-md)' }}>
135
+ <Button variant="text">Cancel</Button>
136
+ <Button variant="text" appearance="accent">Save</Button>
137
+ </div>
140
138
  ```
141
139
 
142
140
  ## Slots
@@ -151,10 +149,10 @@ A **destructive** flavor swaps the label to `error` across every appearance.
151
149
 
152
150
  | Appearance | Background (rest) | Label color | When to reach for it |
153
151
  |-------------|-------------------|-----------------------------------|--------------------------------------------------------------------------------------|
154
- | `default` | `transparent` | `sys.color.onSurfaceVariant` | Base inline action — "Not now", secondary inline trail commits. |
155
- | `accent` | `transparent` | `sys.color.primary` | Brand-blue inline commit — "Skip", "See all". One per row. |
156
- | `onPrimary` | `transparent` | `sys.color.onPrimary` | On a `primary`-filled host (Tooltip `default`). Theme-stable. |
157
- | `inverse` | `transparent` | `sys.color.inverseOnSurface` | Inside an inverse host (Toast, coach-mark). |
152
+ | `default` | `transparent` | `sys.color.text.subtle` | Base inline action — "Not now", secondary inline trail commits. |
153
+ | `accent` | `transparent` | `sys.color.background.primary` | Brand-blue inline commit — "Skip", "See all". One per row. |
154
+ | `onPrimary` | `transparent` | `sys.color.text.onFill` | On a `primary`-filled host (Tooltip `default`). Theme-stable. |
155
+ | `inverse` | `transparent` | `sys.color.text.inverse` | Inside an inverse host (Toast, coach-mark). |
158
156
 
159
157
  ## Sizes
160
158
 
@@ -100,26 +100,26 @@
100
100
  "default": {
101
101
  "background": "transparent",
102
102
  "border": null,
103
- "label": "sys.color.onSurfaceVariant",
103
+ "label": "sys.color.text.subtle",
104
104
  "note": "The base neutral inline action — the canonical Text Button. Quiet enough to live next to typographic content without claiming commit-rank attention."
105
105
  },
106
106
  "accent": {
107
107
  "background": "transparent",
108
108
  "border": null,
109
- "label": "sys.color.primary",
109
+ "label": "sys.color.text.link",
110
110
  "note": "Brand-blue label for the inline commit affordance. Use sparingly — never two `accent` Text Buttons in the same row.",
111
111
  "linkAffordanceRecommendation": "Prefer `accent` whenever the Text Button reads as a **link affordance** — a section header's trailing 'See all' / 'See more', a card-header 'Follow', an inline 'View details' next to a body paragraph. Link-like affordances should carry chromatic emphasis so the navigational intent is unambiguous; `default` (onSurfaceVariant) is for quieter inline commits that should recede into the body copy. Override to `default` only when a parent surface (e.g. a busy chrome bar) already carries enough chromatic weight that an `accent` label would compete."
112
112
  },
113
113
  "onPrimary": {
114
114
  "background": "transparent",
115
115
  "border": null,
116
- "label": "sys.color.onPrimary",
116
+ "label": "sys.color.text.onFill",
117
117
  "note": "Always-white label for use on top of a `primary`-filled host (e.g. the Tooltip `default` appearance). Both `primary` and `onPrimary` are theme-stable (blue / white in light and dark mode), so the label reads as white against the brand-blue fill in either theme — unlike `inverse`, which flips with the theme."
118
118
  },
119
119
  "inverse": {
120
120
  "background": "transparent",
121
121
  "border": null,
122
- "label": "sys.color.inverseOnSurface",
122
+ "label": "sys.color.text.inverse",
123
123
  "note": "Mirror for use inside an inverse host (Toast, coach-mark, snackbar). Label paints in `inverseOnSurface` so it reads against the host's `inverseSurface` fill; state overlays mix from the same token so the recipe carries over without per-host tuning. The `inverseOnSurface` token FLIPS with the theme (white in light mode, dark in dark mode) — that's correct against the also-flipping `inverseSurface` host. When the host fill does NOT flip (e.g. a `primary`-filled Tooltip in `default` appearance), reach for `onPrimary` instead so the label stays white in both themes."
124
124
  }
125
125
  },
@@ -130,22 +130,22 @@
130
130
  "default": {
131
131
  "background": "transparent",
132
132
  "border": null,
133
- "label": "sys.color.error"
133
+ "label": "sys.color.text.danger"
134
134
  },
135
135
  "accent": {
136
136
  "background": "transparent",
137
137
  "border": null,
138
- "label": "sys.color.error"
138
+ "label": "sys.color.text.danger"
139
139
  },
140
140
  "onPrimary": {
141
141
  "background": "transparent",
142
142
  "border": null,
143
- "label": "sys.color.error"
143
+ "label": "sys.color.text.danger"
144
144
  },
145
145
  "inverse": {
146
146
  "background": "transparent",
147
147
  "border": null,
148
- "label": "sys.color.error"
148
+ "label": "sys.color.text.danger"
149
149
  }
150
150
  }
151
151
  }
@@ -166,6 +166,25 @@
166
166
  "opacity": "sys.state.pressed"
167
167
  }
168
168
  },
169
+ "focused": {
170
+ "overlay": {
171
+ "color": "label",
172
+ "opacity": "sys.state.focus"
173
+ },
174
+ "focusRing": {
175
+ "composition": "outward",
176
+ "layer": "::after overlay — position:absolute, inset:0, no reflow (DESIGN.md Focus ring composition)",
177
+ "innerCounterRing": {
178
+ "width": "sys.borderWidth.hairline",
179
+ "color": "sys.color.border.focused"
180
+ },
181
+ "outerRing": {
182
+ "width": "sys.borderWidth.thin",
183
+ "color": "sys.color.border.focused"
184
+ }
185
+ },
186
+ "note": "Keyboard-focus (:focus-visible) visual. Mirrors the `focusIndicator` block (the external-reader contract); kept here so spec-only renderers see focus in the states map. Composes over the lifecycle state the button is in; never via plain mouse click."
187
+ },
169
188
  "disabled": {
170
189
  "overlay": null,
171
190
  "containerOpacity": "sys.state.disabled",
@@ -176,16 +195,14 @@
176
195
  "focusIndicator": {
177
196
  "description": "Keyboard-focus visual — an accessibility indicator, not a lifecycle state. Composes over whichever lifecycle state the button is in. The `states.focused` block above is kept for JSX runtime consumers; this block is the parallel external-reader contract.",
178
197
  "composition": "outward",
179
- "compositionReason": "Action affordance with breathing room around it; the 3px outward extent is reserved by the surrounding layout.",
198
+ "compositionReason": "Action affordance with breathing room around it; the 1px outward ring is reserved by the surrounding layout.",
180
199
  "overlay": {
181
200
  "color": "label",
182
201
  "opacity": "sys.state.focus"
183
202
  },
184
203
  "ring": {
185
- "outerWidth": "sys.borderWidth.thin",
186
- "outerColor": "sys.color.focus",
187
- "insetWidth": "sys.borderWidth.hairline",
188
- "insetColor": "sys.color.focusInset"
204
+ "width": "sys.borderWidth.hairline",
205
+ "color": "sys.color.border.focused"
189
206
  },
190
207
  "trigger": ":focus-visible (keyboard / programmatic focus, never plain mouse click)"
191
208
  },
@@ -2,7 +2,7 @@
2
2
 
3
3
  > 🇰🇷 한국어: [`i18n/ko/schema/components/button/toggle.md`](../../../i18n/ko/schema/components/button/toggle.md)
4
4
 
5
- Commit-and-record action — a Toolbar-footprint button with two states. **Inactive** invites the commit (`primary` fill); **active** records it (`transparent` fill + hairline `outlineVariant` outline).
5
+ Commit-and-record action — a Toolbar-footprint button with two states. **Inactive** invites the commit (`primary` fill); **active** records it (`transparent` fill + hairline `border.default` outline).
6
6
 
7
7
  **Reach for this when** you need a reversible commit that persists across views — *Follow / Following*, *Subscribe / Subscribed*, *Join / Joined*. **Skip when** the action is one-shot ([Standard Button](./standard.md)), the row is a dense toolbar ([Toolbar Button](./toolbar.md)), or the toggle belongs to a filter set ([Filter Chip](../chip/filter.md)).
8
8
 
@@ -24,7 +24,7 @@ import { Button } from '@teamblind-chorus/ui';
24
24
 
25
25
  ## Active
26
26
 
27
- The committed form — `transparent` fill with hairline `outlineVariant` stroke. The transparent fill lets the button sit on any host surface tier (page `surface`, card `surfaceContainer`, raised `surfaceContainerHigh`) without re-painting a background that would clash with the host. Use the same element across both states and toggle the `active` flag; the consumer swaps the label text. Reports state via `aria-pressed`.
27
+ The committed form — `transparent` fill with hairline `border.default` stroke. The transparent fill lets the button sit on any host surface tier (page `surface`, card `surfaceContainer`, raised `surfaceContainerHigh`) without re-painting a background that would clash with the host. Use the same element across both states and toggle the `active` flag; the consumer swaps the label text. Reports state via `aria-pressed`.
28
28
 
29
29
  ```preview
30
30
  button/toggle/active
@@ -36,9 +36,7 @@ import { Button } from '@teamblind-chorus/ui';
36
36
  </Button>
37
37
  ```
38
38
 
39
- ## Use cases
40
-
41
- ### With icon
39
+ ## Leading icon
42
40
 
43
41
  A check glyph on commit reinforces the active read. Inactive form stays glyph-less.
44
42
 
@@ -53,7 +51,7 @@ import { CheckedIcon } from '@teamblind-chorus/ui/icons';
53
51
  </Button>
54
52
  ```
55
53
 
56
- ### Focus indicator
54
+ ## Focus ring
57
55
 
58
56
  Both forms take the same standard ring; below shows inactive. See [Focus ring composition](../../DESIGN.md#focus-ring-composition).
59
57
 
@@ -71,7 +69,7 @@ import { Button } from '@teamblind-chorus/ui';
71
69
 
72
70
  - **label** — accessible name. Required, single line. Consumer swaps the verb between states ("Follow" → "Following"); no auto-rewrite.
73
71
  - **leadingIcon** (optional) — context glyph before the label. Inherits colour via `currentColor` per the [family rule](./button.md#icon-colour-inheritance-family-wide).
74
- - **trailingIcon** (optional) — directional/destination glyph after the label. Same contract as [Toolbar Button](./toolbar.md#with-trailing-icon).
72
+ - **trailingIcon** (optional) — directional/destination glyph after the label. Same contract as [Toolbar Button](./toolbar.md#trailing-icon).
75
73
 
76
74
  ## Sizes
77
75
 
@@ -93,8 +91,8 @@ A single visual variant — inactive/active is expressed as a state on the same
93
91
 
94
92
  | State | Background | Border (always 1px `sys.borderWidth.hairline`) | Label / icon color | Notes |
95
93
  |--------------|-------------------------------------|---------------------------------------------------------|-----------------------------------|----------------------------------------------------------------------|
96
- | inactive | `sys.color.primary` | `transparent` | `sys.color.onPrimary` | Brand-loud fill inviting commit. Border `transparent` but 1px width held so footprint never changes between states. |
97
- | active | `transparent` | `sys.color.outlineVariant` | `sys.color.onSurface` | Committed form — hairline-outlined ghost over whatever host surface the button sits on. Transparent fill records state without claiming attention or clashing with the host tier. |
94
+ | inactive | `sys.color.background.primary` | `transparent` | `sys.color.text.onFill` | Brand-loud fill inviting commit. Border `transparent` but 1px width held so footprint never changes between states. |
95
+ | active | `transparent` | `sys.color.border.default` | `sys.color.text.default` | Committed form — hairline-outlined ghost over whatever host surface the button sits on. Transparent fill records state without claiming attention or clashing with the host tier. |
98
96
 
99
97
  ## States
100
98
 
@@ -63,16 +63,16 @@
63
63
  },
64
64
  "selectionStates": {
65
65
  "unselected": {
66
- "background": "sys.color.primary",
67
- "label": "sys.color.onPrimary",
66
+ "background": "sys.color.background.primary",
67
+ "label": "sys.color.text.onFill",
68
68
  "border": null
69
69
  },
70
70
  "selected": {
71
71
  "background": "transparent",
72
- "label": "sys.color.onSurface",
72
+ "label": "sys.color.text.default",
73
73
  "border": {
74
74
  "width": "sys.borderWidth.hairline",
75
- "color": "sys.color.outlineVariant"
75
+ "color": "sys.color.border.default"
76
76
  }
77
77
  }
78
78
  },
@@ -92,6 +92,25 @@
92
92
  "opacity": "sys.state.pressed"
93
93
  }
94
94
  },
95
+ "focused": {
96
+ "overlay": {
97
+ "color": "label",
98
+ "opacity": "sys.state.focus"
99
+ },
100
+ "focusRing": {
101
+ "composition": "outward",
102
+ "layer": "::after overlay — position:absolute, inset:0, no reflow (DESIGN.md Focus ring composition)",
103
+ "innerCounterRing": {
104
+ "width": "sys.borderWidth.hairline",
105
+ "color": "sys.color.border.focused"
106
+ },
107
+ "outerRing": {
108
+ "width": "sys.borderWidth.thin",
109
+ "color": "sys.color.border.focused"
110
+ }
111
+ },
112
+ "note": "Keyboard-focus (:focus-visible) visual. Mirrors the `focusIndicator` block (the external-reader contract); kept here so spec-only renderers see focus in the states map. Composes over the lifecycle state the button is in; never via plain mouse click."
113
+ },
95
114
  "disabled": {
96
115
  "overlay": null,
97
116
  "containerOpacity": "sys.state.disabled",
@@ -102,21 +121,19 @@
102
121
  "focusIndicator": {
103
122
  "description": "Keyboard-focus visual — an accessibility indicator, not a lifecycle state. Composes over whichever lifecycle state the button is in. The `states.focused` block above is kept for JSX runtime consumers; this block is the parallel external-reader contract.",
104
123
  "composition": "outward",
105
- "compositionReason": "Action affordance with breathing room around it; the 3px outward extent is reserved by the surrounding layout.",
124
+ "compositionReason": "Action affordance with breathing room around it; the 1px outward ring is reserved by the surrounding layout.",
106
125
  "overlay": {
107
126
  "color": "label",
108
127
  "opacity": "sys.state.focus"
109
128
  },
110
129
  "ring": {
111
- "outerWidth": "sys.borderWidth.thin",
112
- "outerColor": "sys.color.focus",
113
- "insetWidth": "sys.borderWidth.hairline",
114
- "insetColor": "sys.color.focusInset"
130
+ "width": "sys.borderWidth.hairline",
131
+ "color": "sys.color.border.focused"
115
132
  },
116
133
  "trigger": ":focus-visible (keyboard / programmatic focus, never plain mouse click)"
117
134
  },
118
135
  "forbidden": [
119
- "active state painted with sys.color.primary fill — active is transparent + hairline outline (the active state recedes, not asserts)",
136
+ "active state painted with sys.color.background.primary fill — active is transparent + hairline outline (the active state recedes, not asserts)",
120
137
  "active state painted with any opaque surface fill (surface, surfaceContainer, surfaceContainerHigh) — the committed form is transparent so the host surface shows through; do not re-bind to a tier'd fill",
121
138
  "rest state without an explicit `active={false}` — toggle is a binary contract, never tristate",
122
139
  "manual width override that breaks the full-card stretch when used inside ProfileCarousel.followAction"
@@ -22,9 +22,7 @@ import { Button } from '@teamblind-chorus/ui';
22
22
  </Button>
23
23
  ```
24
24
 
25
- ## Use cases
26
-
27
- ### Accent
25
+ ## Accent
28
26
 
29
27
  Brand-blue fill, `onPrimary` label — the single-commit form. Used when the Toolbar Button IS the surface's commit affordance.
30
28
 
@@ -38,7 +36,7 @@ import { Button } from '@teamblind-chorus/ui';
38
36
  </Button>
39
37
  ```
40
38
 
41
- ### Inverse
39
+ ## Inverse
42
40
 
43
41
  Mirror for inverse hosts (snackbars, coach-mark surfaces). Geometry identical; colour pair flips.
44
42
 
@@ -52,7 +50,7 @@ import { Button } from '@teamblind-chorus/ui';
52
50
  </Button>
53
51
  ```
54
52
 
55
- ### With icon
53
+ ## Leading icon
56
54
 
57
55
  Context glyph before the label — tag for *Filters*, calendar for *Pick date*.
58
56
 
@@ -70,7 +68,7 @@ import { PlusIcon } from '@teamblind-chorus/ui/icons';
70
68
  </Button>
71
69
  ```
72
70
 
73
- ### With trailing icon
71
+ ## Trailing icon
74
72
 
75
73
  Directional/destination glyph — chevron-down to open a menu, "×" to clear. Unlike standard [Button](./button.md), Toolbar Button carries trailing icons because its role is often *trigger* rather than commit.
76
74
 
@@ -88,7 +86,7 @@ import { CheckedIcon } from '@teamblind-chorus/ui/icons';
88
86
  </Button>
89
87
  ```
90
88
 
91
- ### Icon only
89
+ ## Icon only
92
90
 
93
91
  Glyph-only 32×32 square. Requires `aria-label`. When the label slot is absent, inline padding drops to `sys.layout.container.xs` (8) so the glyph centers.
94
92
 
@@ -105,7 +103,22 @@ import { PlusIcon } from '@teamblind-chorus/ui/icons';
105
103
  />
106
104
  ```
107
105
 
108
- ### Group
106
+ ## Focus ring
107
+
108
+ Hairline stroke is kept underneath the standard ring.
109
+
110
+ ```preview
111
+ button/toolbar/focused
112
+ ---
113
+ import { Button } from '@teamblind-chorus/ui';
114
+ import { PlusIcon } from '@teamblind-chorus/ui/icons';
115
+
116
+ <Button variant="toolbar" state="focused" leadingIcon={<PlusIcon />}>
117
+ Add row
118
+ </Button>
119
+ ```
120
+
121
+ ## Group
109
122
 
110
123
  Adjacent Toolbar Buttons share `4px` gap (`sys.layout.inline.sm`) with Filter chips. Mix freely.
111
124
 
@@ -128,21 +141,6 @@ import { PlusIcon, CheckedIcon } from '@teamblind-chorus/ui/icons';
128
141
  </div>
129
142
  ```
130
143
 
131
- ### Focus indicator
132
-
133
- Hairline stroke is kept underneath the standard ring.
134
-
135
- ```preview
136
- button/toolbar/focused
137
- ---
138
- import { Button } from '@teamblind-chorus/ui';
139
- import { PlusIcon } from '@teamblind-chorus/ui/icons';
140
-
141
- <Button variant="toolbar" state="focused" leadingIcon={<PlusIcon />}>
142
- Add row
143
- </Button>
144
- ```
145
-
146
144
  ## Slots
147
145
 
148
146
  - **label** — accessible name. Required, single line.
@@ -155,9 +153,9 @@ Only the container ↔ label colour pair flips; geometry identical. For inline d
155
153
 
156
154
  | Appearance | Background | Border | Label / icon | When to reach for it |
157
155
  |-------------|-------------------------------------|--------------------------------------------------|------------------------------------|----------------------|
158
- | `default` | `sys.color.surfaceContainerHigh` | 1px `sys.color.outlineVariant` | `sys.color.onSurface` | Quiet inline action — toolbar opener, "Edit" beside a row title. |
159
- | `accent` | `sys.color.primary` | none | `sys.color.onPrimary` | The single commit — a [Page](../navigation-bar/sub.md) bar's "Save". Never two in a row. |
160
- | `inverse` | `sys.color.inverseSurface` | none | `sys.color.inverseOnSurface` | Inside an inverse host (Toast, coach-mark). |
156
+ | `default` | `sys.color.surfaceContainerHigh` | 1px `sys.color.border.default` | `sys.color.text.default` | Quiet inline action — toolbar opener, "Edit" beside a row title. |
157
+ | `accent` | `sys.color.background.primary` | none | `sys.color.text.onFill` | The single commit — a [Page](../navigation-bar/sub.md) bar's "Save". Never two in a row. |
158
+ | `inverse` | `sys.color.background.inverse` | none | `sys.color.text.inverse` | Inside an inverse host (Toast, coach-mark). |
161
159
 
162
160
  ## Sizes
163
161
 
@@ -61,23 +61,23 @@
61
61
  },
62
62
  "appearances": {
63
63
  "default": {
64
- "background": "sys.color.surfaceContainerHigh",
65
- "label": "sys.color.onSurface",
64
+ "background": "sys.color.surface.default",
65
+ "label": "sys.color.text.default",
66
66
  "border": {
67
67
  "width": "sys.borderWidth.hairline",
68
- "color": "sys.color.outlineVariant"
68
+ "color": "sys.color.border.default"
69
69
  },
70
70
  "note": "The canonical Toolbar Button chrome (mirrors Filter chip's unselected state). The chip-shape signals an inline action that lives within a row of peers (a toolbar, a card footer, a Page bar where the affordance is one of several actions the user might pick)."
71
71
  },
72
72
  "accent": {
73
- "background": "sys.color.primary",
74
- "label": "sys.color.onPrimary",
73
+ "background": "sys.color.background.primary",
74
+ "label": "sys.color.text.onFill",
75
75
  "border": null,
76
76
  "note": "Brand-blue commit chip — reach for it when the Toolbar Button IS the surface's commit affordance (a Page bar's 'Save', a sheet's 'Confirm', a flow step's 'Done'). The accent fill claims commit-rank attention while keeping the 32px Toolbar Button footprint, so the bar reads at one density even though the action is the screen's most important. Pair sparingly — never two accent Toolbar Buttons in the same row."
77
77
  },
78
78
  "inverse": {
79
- "background": "sys.color.inverseSurface",
80
- "label": "sys.color.inverseOnSurface",
79
+ "background": "sys.color.background.inverse",
80
+ "label": "sys.color.text.inverse",
81
81
  "border": null,
82
82
  "note": "Inverse-toned chip for use inside an inverse host (snackbars, coach-mark surfaces, dark coach overlays). Same chrome geometry as `default`; the colour pair flips to the inverse cluster so the chip reads against the host's `inverseSurface` fill without a per-host tweak."
83
83
  }
@@ -88,16 +88,14 @@
88
88
  "focusIndicator": {
89
89
  "description": "Keyboard-focus visual — an accessibility indicator, not a lifecycle state. Composes over whichever lifecycle state the button is in. Visual ring is delegated to Filter chip's composition; this block restates the contract for external readers.",
90
90
  "composition": "outward",
91
- "compositionReason": "Action affordance with breathing room around it; the 3px outward extent is reserved by the surrounding toolbar row.",
91
+ "compositionReason": "Action affordance with breathing room around it; the 1px outward ring is reserved by the surrounding toolbar row.",
92
92
  "overlay": {
93
93
  "color": "label",
94
94
  "opacity": "sys.state.focus"
95
95
  },
96
96
  "ring": {
97
- "outerWidth": "sys.borderWidth.thin",
98
- "outerColor": "sys.color.focus",
99
- "insetWidth": "sys.borderWidth.hairline",
100
- "insetColor": "sys.color.focusInset"
97
+ "width": "sys.borderWidth.hairline",
98
+ "color": "sys.color.border.focused"
101
99
  },
102
100
  "trigger": ":focus-visible (keyboard / programmatic focus, never plain mouse click)"
103
101
  },
@@ -19,7 +19,7 @@ Both subs share the same pager geometry — a `ref.space.500` (40px) trailing pe
19
19
 
20
20
  Every Carousel paints a [Header](../header/header.md) (`size="large"`) at the top:
21
21
 
22
- - **Label** *(required)* — `sys.typo.heading.md` / Semibold / `sys.color.onSurface`. Leading position.
22
+ - **Label** *(required)* — `sys.typo.heading.md` / Semibold / `sys.color.text.default`. Leading position.
23
23
  - **headerAction** *(optional)* — trailing [Text Button](../button/text.md) (`size={'xsmall'}`, `appearance={'accent'}`) per the link-affordance rule. Extends the header when there's an index page to route to.
24
24
 
25
25
  Carousel forwards `label` and `headerAction` to [Header](../header/header.md) verbatim; the header anatomy lives in Header's spec. Other hosts (in-sheet sub-sections, bounded cards, [SuggestionList](../suggestion-list/suggestion-list.md)) reach for `<Header />` directly — Carousel is the labelled-region host, Header the leading-row primitive it composes.
@@ -55,9 +55,7 @@ import { Carousel, PostCarousel } from '@teamblind-chorus/ui';
55
55
  </Carousel>
56
56
  ```
57
57
 
58
- ## Use cases
59
-
60
- ### With header action
58
+ ## Header action
61
59
 
62
60
  Extend the header with a trailing `accent` Text Button when there's an index page to route to. Lifts the `headerAction` prop on the `<Carousel>` wrapper.
63
61
 
@@ -69,17 +67,15 @@ import { Carousel, PostCarousel } from '@teamblind-chorus/ui';
69
67
  <Carousel label="Trending right now" headerAction={{ label: 'See all', href: '#' }}>
70
68
  <PostCarousel
71
69
  items={[
72
- { avatar: { src: '/placeholder.png', alt: 'Channel' }, channel: 'Engineering', verified: true, followAction: true, title: 'The migration that finally landed after three quarters', body: 'Internal postmortem turned editorial — the scaffolding that held the rewrite together when the timeline did not.', mention: '@infra-talk', views: '14K' },
73
- { avatar: { src: '/placeholder.png', alt: 'Channel' }, channel: 'Compensation', verified: true, followAction: true, title: 'Equity refresh negotiations — what actually moves', body: 'A read on the conversations that get an actual refresh on the calendar versus the ones that get a polite no.', mention: '@career', views: '9K' },
74
- { avatar: { src: '/placeholder.png', alt: 'Channel' }, channel: 'Plant People', verified: false, followAction: true, title: 'Monstera dropping aerial roots — repot or train?', body: 'Two-year-old monstera, roots crawling out of the drainage holes. Light and watering are dialed in.', mention: '@plant-parents', views: '3K' },
70
+ { avatar: { src: '/placeholder.png', alt: 'Channel' }, channel: 'Engineering', verified: true, followAction: true, title: 'The migration that finally landed after three quarters', body: 'Internal postmortem turned editorial — the scaffolding that held the rewrite together when the timeline did not.', mention: '@infra-talk', views: '14K' },
75
71
  ]}
76
72
  />
77
73
  </Carousel>
78
74
  ```
79
75
 
80
- ### Editorial cards (no follow, no verified)
76
+ ## Editorial card
81
77
 
82
- Cards drop `verified` and `followAction` — each card's header collapses to avatar + channel name. Reach for it on editorial collections where the card is informational only (round-ups, archives, *what we're reading*); the surface should not invite a per-card commit.
78
+ The card drops `verified` and `followAction` — its header collapses to avatar + channel name. Reach for it on editorial collections where the card is informational only (round-ups, archives, *what we're reading*); the surface should not invite a per-card commit.
83
79
 
84
80
  ```preview
85
81
  carousel/post-editorial
@@ -89,9 +85,7 @@ import { Carousel, PostCarousel } from '@teamblind-chorus/ui';
89
85
  <Carousel label="Editor picks">
90
86
  <PostCarousel
91
87
  items={[
92
- { avatar: { src: '/placeholder.png', alt: 'Channel' }, channel: 'Career', title: 'The quiet math of staying versus leaving', body: 'Salary checks, offer evaluations, and the long thread that runs longer than any single conversation can.', views: '18K' },
93
- { avatar: { src: '/placeholder.png', alt: 'Channel' }, channel: 'Compensation', title: 'Equity refresh negotiations — what actually moves', body: 'A read on the conversations that get an actual refresh on the calendar versus the ones that get a polite no.', views: '9K' },
94
- { avatar: { src: '/placeholder.png', alt: 'Channel' }, channel: 'Engineering', title: 'The migration that finally landed after three quarters', body: 'Internal postmortem turned editorial — the scaffolding that held the rewrite together when the timeline did not.', views: '14K' },
88
+ { avatar: { src: '/placeholder.png', alt: 'Channel' }, channel: 'Career', title: 'The quiet math of staying versus leaving', body: 'Salary checks, offer evaluations, and the long thread that runs longer than any single conversation can.', views: '18K' },
95
89
  ]}
96
90
  />
97
91
  </Carousel>
@@ -103,12 +97,12 @@ import { Carousel, PostCarousel } from '@teamblind-chorus/ui';
103
97
  - **pager** — horizontal scroll-snap track. `scroll-snap-type: x mandatory`; native scrollbar hidden.
104
98
  - **card** — one compact post card per page; outline-only surface.
105
99
  - **avatar** — [Thumbnail](../thumbnail/thumbnail.md) `size={40}`, every prop (`src`, `alt`, `updateDot`, `logoBadge`) forwarded verbatim.
106
- - **verified** *(optional)* — `VerifiedFillIcon` (`sys.icon.md`, `sys.color.primary`) to the LEFT of the channel name so the trust signal reads first.
107
- - **channel** — channel / author name. `sys.typo.label.md` / Semibold / `sys.color.onSurface`.
108
- - **followAction** *(optional)* — trailing [Text Button](../button/text.md) (`xsmall`). `accent` inactive (link-affordance) → `default` active (recedes). Same trailing-Text-Button shape as [List/entry](../list/entry.md)'s [attribution-row case](../list/entry.md#with-trailing-text-button-compact-attribution-row) — see the Behavior note below.
109
- - **title** — `sys.typo.label.md` / Semibold / `sys.color.onSurface`. Single line, truncates.
110
- - **body** — `sys.typo.body.sm` / `sys.color.onSurfaceVariant`. Three-line clamp.
111
- - **mention** *(optional)* — `sys.typo.body.sm` / `sys.color.primary` (not italic).
100
+ - **verified** *(optional)* — `VerifiedFillIcon` (`sys.icon.md`, `sys.color.background.primary`) to the LEFT of the channel name so the trust signal reads first.
101
+ - **channel** — channel / author name. `sys.typo.label.md` / Semibold / `sys.color.text.default`.
102
+ - **followAction** *(optional)* — trailing [Text Button](../button/text.md) (`xsmall`). `accent` inactive (link-affordance) → `default` active (recedes). Same trailing-Text-Button shape as [List/entry](../list/entry.md)'s [attribution-row case](../list/entry.md#trailing-text-button) — see the Behavior note below.
103
+ - **title** — `sys.typo.label.md` / Semibold / `sys.color.text.default`. Single line, truncates.
104
+ - **body** — `sys.typo.body.sm` / `sys.color.text.subtle`. Three-line clamp.
105
+ - **mention** *(optional)* — `sys.typo.body.sm` / `sys.color.background.primary` (not italic).
112
106
  - **footer** — leading 'See more' [Text Button](../button/text.md) (`xsmall` / `secondary`) + trailing view count (`EyeIcon` + count `<span>`).
113
107
  - **pagination** — [Pagination](../pagination/pagination.md) component, one dot per card (`count` = card count, `activeIndex` from scroll position). Decorative — dot tokens and the `aria-hidden` contract live on its spec; the carousel centers the intrinsic-width row (`align-self: center`).
114
108
 
@@ -118,11 +112,11 @@ import { Carousel, PostCarousel } from '@teamblind-chorus/ui';
118
112
  |-------------------|----------------|
119
113
  | container | No fill / padding — surrounding [Carousel](../carousel/carousel.md) provides chrome; `sys.layout.stack.md` pager→dots gap |
120
114
  | pager | `gap: sys.layout.inline.md`, `padding-left: sys.layout.container.md` + `scroll-padding-left: sys.layout.container.md` (the 16 rail; full-bleed host → trailing edge reached intrinsically, no negative margin), `scroll-snap-type: x mandatory`, `scrollbar-width: none` |
121
- | card | `flex: 0 0 calc(100% - sys.layout.inline.md - ref.space.500)`; `sys.color.surface` fill, `sys.radius.md`, `sys.borderWidth.hairline sys.color.outlineVariant` outline (inset box-shadow), `sys.layout.container.md` padding, `sys.layout.stack.sm` between blocks, `scroll-snap-align: start` |
115
+ | card | `flex: 0 0 calc(100% - sys.layout.inline.md - ref.space.500)`; `sys.color.surface` fill, `sys.radius.md`, `sys.borderWidth.hairline sys.color.border.default` outline (inset box-shadow), `sys.layout.container.md` padding, `sys.layout.stack.sm` between blocks, `scroll-snap-align: start` |
122
116
  | header | Row: avatar + verified mark + name + spacer + follow action; `align-items: center`; `sys.layout.inline.md` gap |
123
- | avatar / verified | [Thumbnail](../thumbnail/thumbnail.md) `size={40}` delegated verbatim · `VerifiedFillIcon` at `sys.icon.md` / `sys.color.primary` (resolves to `ref.palette.blue.500`), leading of the channel name |
124
- | channel / title | `sys.typo.label.md`, `sys.color.onSurface`, single-line truncate |
125
- | body / mention | `sys.typo.body.sm` / `sys.color.onSurfaceVariant` (three-line clamp) · mention in `sys.color.primary` |
117
+ | avatar / verified | [Thumbnail](../thumbnail/thumbnail.md) `size={40}` delegated verbatim · `VerifiedFillIcon` at `sys.icon.md` / `sys.color.background.primary` (resolves to `ref.palette.blue.500`), leading of the channel name |
118
+ | channel / title | `sys.typo.label.md`, `sys.color.text.default`, single-line truncate |
119
+ | body / mention | `sys.typo.body.sm` / `sys.color.text.subtle` (three-line clamp) · mention in `sys.color.background.primary` |
126
120
  | followAction | [Text Button](../button/text.md) `size={'xsmall'}`, `appearance={'accent'}` inactive → `appearance={'default'}` active; state tokens delegate to Text Button |
127
121
  | footer | Row: leading 'See more' Text Button (`xsmall` / `secondary`) + trailing view count `<span>`; `justify-content: space-between` |
128
122
  | pagination dot | [Pagination](../pagination/pagination.md) component delegated verbatim — 6 × 6 dots, active/inactive colors, and row gap bind on its spec |