@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
@@ -2,7 +2,7 @@
2
2
 
3
3
  > 🇰🇷 한국어: [`i18n/ko/schema/components/banner/banner.md`](../../../i18n/ko/schema/components/banner/banner.md)
4
4
 
5
- An in-body explanation block — a tinted card sitting within the reading flow with an optional heading line, a short paragraph, and an optional follow-through link. Four axes: **appearance** (`default` / `accent` / `destructive`), **outline** (`outlined` / none), **leading slot** (`icon` / `thumbnail` / none), **trailing slot** (`trailingIcon` / none).
5
+ An in-body explanation block — a tinted card sitting within the reading flow with an optional heading line, a short paragraph, and an optional follow-through link. Five axes: **appearance** (`default` / `accent` / `destructive`), **foreground** (tonal, or `neutralBody` to lay the Default neutral text over the accent fill), **outline** (`outlined` / none), **leading slot** (`icon` / `thumbnail` / none), **trailing slot** (`trailingIcon` / `trailingAction` Text Button / none).
6
6
 
7
7
  **Reach for this when** a passage needs a brief aside the reader can scan or skip. **Skip when** the message demands a decision ([Dialog](../dialog/dialog.md) / [Bottom sheet](../bottom-sheet/bottom-sheet.md)) or confirms a recent user action ([Toast](../toast/toast.md)).
8
8
 
@@ -25,9 +25,7 @@ import { Banner } from '@teamblind-chorus/ui';
25
25
  </Banner>
26
26
  ```
27
27
 
28
- ## Use cases
29
-
30
- ### Accent
28
+ ## Accent
31
29
 
32
30
  The primary-tinted appearance. Body and action both paint in the primary family — reach for it when the aside should pull more attention.
33
31
 
@@ -44,7 +42,26 @@ import { Banner } from '@teamblind-chorus/ui';
44
42
  </Banner>
45
43
  ```
46
44
 
47
- ### Destructive
45
+ ## Neutral body
46
+
47
+ The `accent` fill kept, but the copy re-toned to the **Default** appearance's neutral foreground — title + body in `sys.color.text.default`, action stepping to `sys.color.background.primary`. Pass `neutralBody`. This decouples the background tone from the text tone: the `primaryContainer` tint still pulls the eye, but the copy reads as quiet, high-legibility body text rather than tonal `onPrimaryContainer`. Reach for it on longer explainers or denser asides where primary-family body copy would tire the reader. No effect on `default` (already `onSurface`) or `destructive` (the warning tone must carry through the copy).
48
+
49
+ ```preview
50
+ banner/accent-neutral-body
51
+ ---
52
+ import { Banner } from '@teamblind-chorus/ui';
53
+
54
+ <Banner
55
+ appearance="accent"
56
+ neutralBody
57
+ title="Level up faster"
58
+ action={{ label: 'How levels work', href: '#level' }}
59
+ >
60
+ Stay active in the community to level up and unlock more of what the app offers.
61
+ </Banner>
62
+ ```
63
+
64
+ ## Destructive
48
65
 
49
66
  The error-tinted appearance — `errorContainer` fill with `onErrorContainer` foreground. Reach for it when the aside is a blocking error or rejection (failed approvals, integration outages, billing). Use sparingly.
50
67
 
@@ -61,7 +78,7 @@ import { Banner } from '@teamblind-chorus/ui';
61
78
  </Banner>
62
79
  ```
63
80
 
64
- ### With thumbnail
81
+ ## Thumbnail
65
82
 
66
83
  A leading [Thumbnail](../thumbnail/thumbnail.md) at the top-left — reach for it when the aside is anchored to a channel, author, or sub-brand image. Thumbnail owns its diameter and corner shape; the slot only top-aligns it next to the content column.
67
84
 
@@ -79,9 +96,9 @@ import { Banner, Thumbnail } from '@teamblind-chorus/ui';
79
96
  </Banner>
80
97
  ```
81
98
 
82
- ### Outlined
99
+ ## Outlined
83
100
 
84
- An optional `sys.borderWidth.hairline` (1) inset stroke toned to the appearance's color family and kept deliberately faint, so the outline reads as a soft edge of the same tint rather than a frame — the subtle gray hairline (`sys.color.outlineVariant`) on `default`'s gray-tinted scrim, `primary` at 40% on `accent`'s blue-tinted container, `error` at 40% on `destructive`. Painted as an inset box-shadow, never a real border, so toggling it cannot change the banner's footprint. Reach for it when the tinted fill alone doesn't separate the banner from its host surface.
101
+ An optional `sys.borderWidth.hairline` (1) inset stroke toned to the appearance's color family and kept deliberately faint, so the outline reads as a soft edge of the same tint rather than a frame — the subtle gray hairline (`sys.color.border.default`) on `default`'s gray-tinted scrim, `primary` at 40% on `accent`'s blue-tinted container, `error` at 40% on `destructive`. Painted as an inset box-shadow, never a real border, so toggling it cannot change the banner's footprint. Reach for it when the tinted fill alone doesn't separate the banner from its host surface.
85
102
 
86
103
  ```preview
87
104
  banner/outlined
@@ -99,7 +116,7 @@ import { Banner } from '@teamblind-chorus/ui';
99
116
  </div>
100
117
  ```
101
118
 
102
- ### With title
119
+ ## Title
103
120
 
104
121
  An optional heading line above the body — `label.md` (14 / Semibold) in the container's foreground, separated from the body by `sys.layout.stack.2xs` (4) so the pair reads as one passage. Reach for it when the aside needs a scannable lead-in; omit it for single-thought asides where the body carries itself.
105
122
 
@@ -117,7 +134,7 @@ import { Banner } from '@teamblind-chorus/ui';
117
134
  </Banner>
118
135
  ```
119
136
 
120
- ### With trailing icon
137
+ ## Trailing icon
121
138
 
122
139
  A 16 × 16 (`sys.icon.md`) glyph at the trailing edge, vertically centred against the whole block and painted in `currentColor`. Reach for it when the banner leads somewhere — a forward affordance such as `ForwardCircleFillIcon` signals the aside opens a destination.
123
140
 
@@ -136,7 +153,38 @@ import { ForwardCircleFillIcon } from '@teamblind-chorus/ui/icons';
136
153
  </Banner>
137
154
  ```
138
155
 
139
- ### With icon
156
+ ## Trailing action
157
+
158
+ A [Text Button](../button/text.md) (`<Button variant="text">`) in the trailing slot, vertically centred against the block — a compact inline commit beside the copy (*Dismiss*, *Enable*, *Undo*), distinct from `action` (the follow-through link below the body). The button keeps full control of its own `size` and `appearance` per the button/text spec, but **default the appearance to the banner's colour family** so the commit reads as part of the tinted block — `accent` banner → `appearance="accent"`, `default` banner → `appearance="default"`, `destructive` banner → the Text Button `destructive` flavor. It also keeps the button/text `leadingIcon` / `trailingIcon` slots, so the commit can carry an in-button glyph — e.g. a trailing `ChevronRightIcon` on a *Enable* / *Continue* commit. When both `trailingAction` and the banner-level `trailingIcon` are passed, the action wins the slot.
159
+
160
+ ```preview
161
+ banner/with-trailing-action
162
+ ---
163
+ import { Banner, Button } from '@teamblind-chorus/ui';
164
+ import { ChevronRightIcon } from '@teamblind-chorus/ui/icons';
165
+
166
+ // vertical 8 between sibling banners is the parent column's job (safe zone)
167
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--sys-layout-stack-xs)' }}>
168
+ <Banner
169
+ appearance="accent"
170
+ trailingAction={(
171
+ <Button variant="text" appearance="accent" size="small" trailingIcon={<ChevronRightIcon />}>
172
+ Enable
173
+ </Button>
174
+ )}
175
+ >
176
+ Turn on notifications to hear back the moment someone replies.
177
+ </Banner>
178
+ <Banner
179
+ appearance="default"
180
+ trailingAction={<Button variant="text" size="small">Dismiss</Button>}
181
+ >
182
+ Stay active in the community to level up and unlock more of what the app offers.
183
+ </Banner>
184
+ </div>
185
+ ```
186
+
187
+ ## Icon
140
188
 
141
189
  A 16 × 16 (`sys.icon.md`) glyph at the leading edge, painted in `currentColor`. The slot is sized to the body's first-line height so the glyph centres on the first line — multi-line bodies keep the icon anchored to the first-line cap, not the block centre. Reach for it when the aside leads with a meaning-bearing glyph rather than a brand image.
142
190
 
@@ -161,9 +209,10 @@ Two appearances on the *emphasis* axis (plus `destructive` for errors). Banner c
161
209
 
162
210
  | Appearance | Container fill | Body / action color | Outline (when `outlined`) | When to use |
163
211
  |---------------|---------------------------------------------------------------------------------------------|--------------------------------------------------------------------|----------------------------|------------------------------------------------------------------------------|
164
- | `default` | `sys.color.scrimSubtle` (translucent inverse-tone scrim — ~8% black light / ~8% white dark) | body in `sys.color.onSurface`, action steps to `sys.color.primary` | `sys.color.outlineVariant` (subtle gray) | Supplementary asides the reader can pass over without missing the main flow. |
165
- | `accent` | `sys.color.primaryContainer` | body in `onPrimaryContainer`, action inherits | `sys.color.primary` at 40% (soft blue) | Asides worth pulling the eye toward — new-feature explainers, capability nudges. |
166
- | `destructive` | `sys.color.errorContainer` | body in `onErrorContainer`, action inherits | `sys.color.error` at 40% | Blocking errors or rejections failed approvals, outages, billing. |
212
+ | `default` | `sys.color.background.neutral` (translucent inverse-tone scrim — ~8% black light / ~8% white dark) | body in `sys.color.text.default`, action steps to `sys.color.background.primary` | `sys.color.border.default` (subtle gray) | Supplementary asides the reader can pass over without missing the main flow. |
213
+ | `accent` | `sys.color.background.selected` | body in `onPrimaryContainer`, action inherits | `sys.color.background.primary` at 40% (soft blue) | Asides worth pulling the eye toward — new-feature explainers, capability nudges. |
214
+ | `accent` + `neutralBody` | `sys.color.background.selected` | title + body in `onSurface`, action steps to `sys.color.background.primary` | `sys.color.background.primary` at 40% (soft blue) | Accent tint pulls the eye, but the copy stays quiet, high-legibility body text — longer explainers, dense asides. |
215
+ | `destructive` | `sys.color.background.danger` | body in `onErrorContainer`, action inherits | `sys.color.text.danger` at 40% | Blocking errors or rejections — failed approvals, outages, billing. |
167
216
 
168
217
  ## Slots
169
218
 
@@ -175,6 +224,7 @@ Two appearances on the *emphasis* axis (plus `destructive` for errors). Banner c
175
224
  - **body** — explanation copy. `body.sm` / Regular / inherits container foreground. Required.
176
225
  - **action** *(optional)* — follow-through link below the body. `label.md` / Semibold / underlined.
177
226
  - **trailingIcon** *(optional)* — 16 × 16 glyph at the trailing edge, vertically centred against the container. Paints in `currentColor`.
227
+ - **trailingAction** *(optional)* — a [Text Button](../button/text.md) at the trailing edge, vertically centred. Owns its own size + appearance; default the appearance to the banner's colour family. Takes precedence over `trailingIcon`.
178
228
 
179
229
  ## Anatomy
180
230
 
@@ -186,8 +236,9 @@ Two appearances on the *emphasis* axis (plus `destructive` for errors). Banner c
186
236
  | content | Flex column, `flex: 1 1 auto`, `sys.layout.stack.xs` (8) body↔action gap, `sys.layout.stack.2xs` (4) title↔body gap |
187
237
  | title | `sys.typo.label.md` (14 / Semibold 600), color inherits |
188
238
  | body | `sys.typo.body.sm` (14 / Regular), color inherits |
189
- | action | `sys.typo.label.md` (14 / Semibold), underlined. Steps to `sys.color.primary` in `default`; inherits in `accent` / `destructive`. |
239
+ | action | `sys.typo.label.md` (14 / Semibold), underlined. Steps to `sys.color.background.primary` in `default`; inherits in `accent` / `destructive`. |
190
240
  | trailingIcon | `sys.icon.md` (16 × 16) glyph, `align-self: center` against the container, `color: currentColor` |
241
+ | trailingAction | [Text Button](../button/text.md) (`<Button variant="text">`), `flex: 0 0 auto`, `align-self: center`. Size + appearance owned by the Button; default appearance to the banner's colour family. Wins the slot over `trailingIcon` |
191
242
 
192
243
  ## States
193
244
 
@@ -19,7 +19,14 @@
19
19
  "type": "boolean",
20
20
  "optional": true,
21
21
  "default": false,
22
- "description": "Paints a `sys.borderWidth.hairline` (1) inset stroke around the container, toned to the appearance's color family and kept deliberately faint so it reads as a soft edge of the same tint, not a frame — the subtle gray hairline (`sys.color.outlineVariant`) on `default`'s gray-tinted fill, `primary` at 40% (`color-mix(sys.color.primary, 40%)`) on `accent`'s blue-tinted fill, `error` at 40% on `destructive`. Rendered as an inset box-shadow, never a real border, so toggling it cannot change the banner's footprint (see DESIGN.md → Border & Stroke). Reach for it when the tinted fill alone doesn't separate the banner from its host surface."
22
+ "description": "Paints a `sys.borderWidth.hairline` (1) inset stroke around the container, toned to the appearance's color family and kept deliberately faint so it reads as a soft edge of the same tint, not a frame — the subtle gray hairline (`sys.color.border.default`) on `default`'s gray-tinted fill, `primary` at 40% (`color-mix(sys.color.background.primary, 40%)`) on `accent`'s blue-tinted fill, `error` at 40% on `destructive`. Rendered as an inset box-shadow, never a real border, so toggling it cannot change the banner's footprint (see DESIGN.md → Border & Stroke). Reach for it when the tinted fill alone doesn't separate the banner from its host surface."
23
+ },
24
+ "neutralBody": {
25
+ "type": "boolean",
26
+ "optional": true,
27
+ "default": false,
28
+ "appliesTo": "accent",
29
+ "description": "On `accent`, paints the title + body in the neutral default foreground (`sys.color.text.default`) and steps the action to `sys.color.background.primary` — i.e. the **Default appearance's** foreground treatment laid over the accent fill, decoupling the background tone from the text tone. Reach for it when the `primaryContainer` tint should still pull the eye but the copy should read as quiet, high-legibility body text rather than tonal `onPrimaryContainer` primary-family text (long-form explainers, dense asides). No effect on `default` (already `onSurface`) or `destructive` (the warning tone must carry through the copy)."
23
30
  },
24
31
  "title": {
25
32
  "type": "node",
@@ -46,6 +53,11 @@
46
53
  "optional": true,
47
54
  "description": "{ label, href? , onClick? } — a follow-through link rendered as a block child below the body."
48
55
  },
56
+ "trailingAction": {
57
+ "type": "node",
58
+ "optional": true,
59
+ "description": "A [Text Button](../button/text.md) (`<Button variant=\"text\">`) rendered at the container's trailing edge, vertically centered against the whole block (`align-self: center`). Distinct from `action` (a follow-through link below the body): `trailingAction` is a compact inline commit that sits beside the copy — Dismiss, Enable, Undo. The button keeps full control of its own `size` and `appearance` per the button/text spec; **by default pick the appearance whose color family matches the banner fill** so the commit reads as part of the tinted block — `accent` banner → `appearance=\"accent\"`, `default` banner → `appearance=\"default\"`, `destructive` banner → the Text Button `destructive` flavor. Override only when a denser rung (`size=\"small\"` / `\"xsmall\"`) or a different emphasis is deliberately wanted. The button also keeps its own `leadingIcon` / `trailingIcon` slots, so the commit can carry an in-button glyph (e.g. a trailing `ChevronRightIcon` on an *Enable* / *Continue* commit). Takes precedence over the banner-level `trailingIcon` when both are passed."
60
+ },
49
61
  "children": {
50
62
  "type": "node",
51
63
  "required": true,
@@ -104,6 +116,13 @@
104
116
  "accepts": [
105
117
  "icon"
106
118
  ]
119
+ },
120
+ "trailingAction": {
121
+ "required": false,
122
+ "description": "Trailing-edge slot hosting a Text Button (`<Button variant=\"text\">`). Footprint-preserving (`flex: 0 0 auto`) and vertically centered against the container (`align-self: center`). The Button owns its own size + appearance; default the appearance to the banner's color family (accent → `accent`, default → `default`, destructive → `destructive` flavor). Takes precedence over `trailingIcon`.",
123
+ "accepts": [
124
+ "button"
125
+ ]
107
126
  }
108
127
  },
109
128
  "sizing": {
@@ -138,33 +157,37 @@
138
157
  },
139
158
  "appearances": {
140
159
  "default": {
141
- "background": "sys.color.scrimSubtle",
142
- "foreground": "sys.color.onSurface",
143
- "actionColor": "sys.color.primary",
144
- "outlineColor": "sys.color.outlineVariant",
145
- "note": "Body sits in `onSurface`; the action link steps to primary so it carries the only chromatic emphasis. Background is the translucent inverse-tone scrim (`sys.color.scrimSubtle` — ~8% black light / ~8% white dark) so the banner stays harmonious on any underlying surface — body, raised card, BottomSheet, Dialog — by tinting one step darker (light mode) or lighter (dark mode) instead of pinning to a fixed neutral step that can collide with the surface ladder. Same scrim used by Chip / Tag default, Progress track, StatusTag neutral, and Skeleton."
160
+ "background": "sys.color.background.neutral",
161
+ "foreground": "sys.color.text.default",
162
+ "actionColor": "sys.color.text.link",
163
+ "outlineColor": "sys.color.border.default",
164
+ "note": "Body sits in `onSurface`; the action link steps to primary so it carries the only chromatic emphasis. Background is the translucent inverse-tone scrim (`sys.color.background.neutral` — ~8% black light / ~8% white dark) so the banner stays harmonious on any underlying surface — body, raised card, BottomSheet, Dialog — by tinting one step darker (light mode) or lighter (dark mode) instead of pinning to a fixed neutral step that can collide with the surface ladder. Same scrim used by Chip / Tag default, Progress track, StatusTag neutral, and Skeleton."
146
165
  },
147
166
  "accent": {
148
- "background": "sys.color.primaryContainer",
149
- "foreground": "sys.color.onPrimaryContainer",
167
+ "background": "sys.color.background.selected",
168
+ "foreground": "sys.color.text.link",
150
169
  "actionColor": "inherit",
151
- "outlineColor": "color-mix(sys.color.primary, 40%)",
152
- "note": "Both body and action paint in the primary family so the whole banner reads as one highlighted block. Reach for `accent` when the aside should pull more attention — feature explainers, capability nudges."
170
+ "outlineColor": "color-mix(sys.color.background.primary, 40%)",
171
+ "note": "Both body and action paint in the primary family so the whole banner reads as one highlighted block. Reach for `accent` when the aside should pull more attention — feature explainers, capability nudges. Pass `neutralBody` to keep the accent fill but swap the copy to the Default appearance's neutral foreground (`onSurface` body, `primary` action) when the tint should pull the eye while the text stays quiet, high-legibility body copy."
153
172
  },
154
173
  "destructive": {
155
- "background": "sys.color.errorContainer",
156
- "foreground": "sys.color.onErrorContainer",
174
+ "background": "sys.color.background.danger",
175
+ "foreground": "sys.color.text.danger",
157
176
  "actionColor": "inherit",
158
- "outlineColor": "color-mix(sys.color.error, 40%)",
177
+ "outlineColor": "color-mix(sys.color.text.danger, 40%)",
159
178
  "note": "Body and action paint in the error family so the whole banner reads as one warning block. Reach for `destructive` when the aside is a blocking error or rejection — failed approvals, integration outages, billing problems. Use sparingly — every destructive banner on a screen competes with the others for the user's alarm budget."
160
179
  }
161
180
  },
162
181
  "behavior": {
163
182
  "actionLink": "When present, renders as an <a> and accepts either href (browser navigation) or onClick (consumer-controlled). Underline persists at rest so the link reads as actionable inside the muted block.",
183
+ "trailingAction": "A `<Button variant=\"text\">` in the trailing slot is a real interactive control (not aria-hidden, unlike the trailing icon). It carries its own size + appearance per the button/text spec; the default appearance follows the banner's color family so the commit reads as part of the tinted block (accent → accent, default → default, destructive → destructive flavor). When both `trailingAction` and `trailingIcon` are passed, the action wins the slot.",
184
+ "neutralForeground": "`neutralBody` re-tones only the accent appearance: the container foreground becomes `onSurface` (title + body) and the action steps to `primary`, matching the Default appearance's foreground treatment. Ignored on `default` and `destructive`.",
164
185
  "role": "Container carries role='note' so screen readers announce the banner as an aside."
165
186
  },
166
187
  "forbidden": [
167
- "default banner background painted with sys.color.brandContainerinformational banners use sys.color.primaryContainer; promotional banners use sys.color.surfaceContainerLow",
188
+ "banner trailing-edge commit rendered as a raw <a> / <button> or a filled/outlined Button the trailing action is button/text, defaulted to the banner's color family",
189
+ "neutralBody applied to default or destructive — it only decouples the accent fill from its foreground; default is already onSurface and destructive must carry the warning tone through the copy",
190
+ "default banner background painted with a brand-tinted fill — informational banners use sys.color.background.selected; promotional banners use sys.color.surface.sunken",
168
191
  "banner thumbnail slot omitted when banner role carries imagery — empty image area is forbidden, fall back to /placeholder.png",
169
192
  "banner used for transient confirmations — that role is the `toast` family (locked)",
170
193
  "banner CTA rendered as raw <a> / <button> — use button/text inside the action slot",
@@ -33,9 +33,7 @@ const [open, setOpen] = useState(false);
33
33
  </>
34
34
  ```
35
35
 
36
- ## Use cases
37
-
38
- ### Overflow
36
+ ## Overflow
39
37
 
40
38
  When content exceeds the card's `max-height`, the content slot scrolls internally — handle and actions stay pinned, footer gains its `is-elevated` upward shadow.
41
39
 
@@ -62,7 +60,7 @@ const [open, setOpen] = useState(false);
62
60
  </>
63
61
  ```
64
62
 
65
- ### Keyboard
63
+ ## Keyboard
66
64
 
67
65
  When the sheet hosts an input that summons the virtual keyboard, the card lifts above the keyboard's top edge so the actions footer stays reachable. Handle and footer pinned; content scrolls to keep the focused input in view.
68
66
 
@@ -88,7 +86,7 @@ const [open, setOpen] = useState(false);
88
86
  </>
89
87
  ```
90
88
 
91
- ### Nested step
89
+ ## Nested step
92
90
 
93
91
  The sheet can host a **drill-in step** without spawning a second modal. The consumer swaps title, content, and primary action between renders; passing `onBack` paints a leading back chevron — an Icon Button rendering `ChevronLeftIcon` at `sys.icon.lg` (24), with `sys.layout.inline.md` (8) between glyph and title. Card chrome, scrim, drag handle, and actions footer stay identical across steps.
94
92
 
@@ -151,7 +149,7 @@ const [value, setValue] = useState('');
151
149
  | drag handle | 48 × 4px pill, `onSurfaceVariant @ 40%`, `sys.radius.full`, 8px vertical gutter |
152
150
  | content | Flex column, 16px padding, 16px between children, vertical scroll on overflow |
153
151
  | title | `sys.typo.heading.lg` (24 / Semibold), `onSurface` |
154
- | back chevron | [Icon Button](../button/icon.md) → `ChevronLeftIcon` at `sys.icon.lg` (24), `sys.color.onSurface`. Glyph aligns to the title's leading edge via Icon Button optical-alignment. `sys.layout.inline.md` (8) glyph → title gap. |
152
+ | back chevron | [Icon Button](../button/icon.md) → `ChevronLeftIcon` at `sys.icon.lg` (24), `sys.color.text.default`. Glyph aligns to the title's leading edge via Icon Button optical-alignment. `sys.layout.inline.md` (8) glyph → title gap. |
155
153
  | body | `sys.typo.body.md` (16 / Regular), `onSurfaceVariant` |
156
154
  | actions | Flex column, 8px between buttons, 16px padding on all four sides |
157
155
  | primary CTA | [Button](../button/button.md) `appearance="primary"`, `size="large"`, `fullWidth` |
@@ -104,14 +104,14 @@
104
104
  },
105
105
  "sizing": {
106
106
  "scrimTint": "ref.palette.black.600",
107
- "containerFill": "sys.color.surfaceContainerHigh",
107
+ "containerFill": "sys.color.surface.default",
108
108
  "containerRadiusTop": "sys.radius.xl",
109
109
  "containerRadiusBottom": "0",
110
110
  "elevation": "sys.elevation.sheet",
111
111
  "maxWidth": "480px",
112
112
  "maxHeight": "90vh",
113
113
  "dragHandleSize": "48 × 4px",
114
- "dragHandleFill": "sys.color.onSurfaceVariant @ 40%",
114
+ "dragHandleFill": "sys.color.text.subtle @ 40%",
115
115
  "dragHandleRadius": "sys.radius.full",
116
116
  "dragHandleGutter": "sys.layout.container.xs",
117
117
  "contentPadding": "sys.layout.container.md",
@@ -119,13 +119,13 @@
119
119
  "actionsStackGap": "sys.layout.stack.xs",
120
120
  "actionsPadding": "sys.layout.container.md",
121
121
  "titleTypo": "sys.typo.heading.lg",
122
- "titleColor": "sys.color.onSurface",
122
+ "titleColor": "sys.color.text.default",
123
123
  "backIcon": "ChevronLeftIcon",
124
124
  "backIconSize": "sys.icon.lg",
125
- "backIconColor": "sys.color.onSurface",
125
+ "backIconColor": "sys.color.icon.default",
126
126
  "backToTitleGap": "sys.layout.inline.md",
127
127
  "bodyTypo": "sys.typo.body.md",
128
- "bodyColor": "sys.color.onSurfaceVariant"
128
+ "bodyColor": "sys.color.text.subtle"
129
129
  },
130
130
  "states": {
131
131
  "open": {
@@ -8,7 +8,7 @@ A small persistent annotation pill with a tail pointing at an anchor — a chat-
8
8
 
9
9
  **Layout inset.** `inline` — the bubble ships no positioning. The host anchors it to the target element by *visual alignment* (CSS anchor positioning, or a positioned wrapper around the anchor): the tail's TIP sits flush on the anchor's content edge (padding excluded) — the bubble body is set back from that edge by the tail's own protrusion (`ref.space.50 / √2`) so the tail meets the anchor cleanly rather than overlapping it — and the bubble centres on the anchor's visual centre so the tail sits at the anchor's bottom-centre. The bubble caps its own `max-width` so it always keeps an 8-token margin from every viewport edge; position-clamping is the host's job. Only when centring would push the bubble past that safe margin does the host shift it inward and flip `tailAlign` so the tail still points at the anchor — `start` for left-edge anchors, `end` for right-edge anchors, `center` whenever the anchor has room on both sides (the default).
10
10
 
11
- **Colour tuning.** Default fill `sys.color.primary` / label `sys.color.onPrimary` — both theme-stable. Operations re-tint per campaign by setting `--bubble-fill` and `--bubble-ink` on inline style; the tail's `background: inherit` follows automatically.
11
+ **Colour tuning.** Default fill `sys.color.background.primary` / label `sys.color.text.onFill` — both theme-stable. Operations re-tint per campaign by setting `--bubble-fill` and `--bubble-ink` on inline style; the tail's `background: inherit` follows automatically.
12
12
 
13
13
  ## Default
14
14
 
@@ -20,9 +20,7 @@ import { Bubble } from '@teamblind-chorus/ui';
20
20
  <Bubble>5 new messages + gift</Bubble>
21
21
  ```
22
22
 
23
- ## Use cases
24
-
25
- ### Anchored to a top-bar icon
23
+ ## Anchored icon
26
24
 
27
25
  A [Navigation bar (home)](../navigation-bar/main.md) with three trailing actions, bubble anchored to the chat glyph itself. The glyph carries `anchor-name: --chat-icon`; the bubble pins to the glyph's bottom — padding excluded — set back by the tail's own protrusion (`top: calc(anchor(bottom) + var(--bubble-tail-protrusion))`, the system token = `ref.space.50 / √2`) so the tail's top vertex lands *flush* on the glyph's bottom edge rather than poking into it, and centres on the glyph's visual centre (`left: anchor(center)` + `translateX(-50%)`). The tail tip thus sits on the chat icon's bottom-centre regardless of where the bar reflows — no hardcoded pixel offsets, `tailAlign="center"`.
28
26
 
@@ -70,7 +68,7 @@ import { SearchIcon, ChatIcon, ProfileIcon } from '@teamblind-chorus/ui/icons';
70
68
  </div>
71
69
  ```
72
70
 
73
- ### Tail alignment
71
+ ## Tail alignment
74
72
 
75
73
  Three tail positions stacked so the offset reads at a glance — pick by where the anchor sits.
76
74
 
@@ -86,7 +84,7 @@ import { Bubble } from '@teamblind-chorus/ui';
86
84
  </div>
87
85
  ```
88
86
 
89
- ### Operations re-tint
87
+ ## Operations re-tint
90
88
 
91
89
  Brand red instead of primary blue — the tail inherits the fill, so a single declaration covers both surfaces.
92
90
 
@@ -95,12 +93,12 @@ bubble/recoloured
95
93
  ---
96
94
  import { Bubble } from '@teamblind-chorus/ui';
97
95
 
98
- <Bubble style={{ '--bubble-fill': 'var(--sys-color-brand)', '--bubble-ink': 'var(--sys-color-onBrand)' }}>
96
+ <Bubble style={{ '--bubble-fill': 'var(--sys-color-text-brand)', '--bubble-ink': 'var(--sys-color-text-onFill)' }}>
99
97
  Free daily tarot
100
98
  </Bubble>
101
99
  ```
102
100
 
103
- ### Long copy
101
+ ## Long copy
104
102
 
105
103
  Copy that exceeds the host width truncates with an ellipsis. If the message can't fit on one line, it belongs in a [Banner](../banner/banner.md) instead.
106
104
 
@@ -124,8 +122,8 @@ import { Bubble } from '@teamblind-chorus/ui';
124
122
 
125
123
  | Slot | Token bindings |
126
124
  |-----------|----------------|
127
- | container | Fill `--bubble-fill` (default `sys.color.primary`), ink `--bubble-ink` (default `sys.color.onPrimary`), `sys.layout.container.2xs` padding-block, `ref.space.75` padding-inline, `sys.radius.full`, viewport-safe `max-width` cap |
128
- | body | `sys.typo.caption` |
125
+ | container | Fill `--bubble-fill` (default `sys.color.background.primary`), ink `--bubble-ink` (default `sys.color.text.onFill`), `sys.layout.container.2xs` padding-block, `ref.space.75` padding-inline, `sys.radius.full`, viewport-safe `max-width` cap |
126
+ | body | `sys.typo.label.xs` |
129
127
  | tail | `ref.space.50` square, rotated 45° |
130
128
 
131
129
  ## Behavior
@@ -2,7 +2,7 @@
2
2
  "$schema": "../../spec.schema.json",
3
3
  "name": "Bubble",
4
4
  "family": "bubble",
5
- "description": "Always-on annotation bubble — a pill-shaped label with a small tail that points at the anchor UI element. Ships with a default brand-blue fill (`sys.color.primary` / `sys.color.onPrimary` — both theme-stable so the bubble reads identically in light and dark mode) and exposes two CSS custom properties (`--bubble-fill`, `--bubble-ink`) so operations can re-tint per campaign without forking the component. Distinct from Tooltip on three axes: (1) persistent rather than transient — Bubble stays in view as part of the UI's resting state; (2) lower visual priority — no elevation shadow, smaller padding, single-line truncation; (3) does NOT occlude its neighbours — the host positions it inline next to the anchor, never as a portal-mounted overlay.",
5
+ "description": "Always-on annotation bubble — a pill-shaped label with a small tail that points at the anchor UI element. Ships with a default brand-blue fill (`sys.color.background.primary` / `sys.color.text.onFill` — both theme-stable so the bubble reads identically in light and dark mode) and exposes two CSS custom properties (`--bubble-fill`, `--bubble-ink`) so operations can re-tint per campaign without forking the component. Distinct from Tooltip on three axes: (1) persistent rather than transient — Bubble stays in view as part of the UI's resting state; (2) lower visual priority — no elevation shadow, smaller padding, single-line truncation; (3) does NOT occlude its neighbours — the host positions it inline next to the anchor, never as a portal-mounted overlay.",
6
6
  "element": "div",
7
7
  "props": {
8
8
  "children": {
@@ -36,12 +36,12 @@
36
36
  "slots": {
37
37
  "container": {
38
38
  "required": true,
39
- "description": "The pill body. `position: relative` so the tail can pin to its edge; `display: inline-flex` so the bubble shrink-wraps its body. Carries the fill (`--bubble-fill` defaulting to `sys.color.primary`), ink (`--bubble-ink` defaulting to `sys.color.onPrimary`), 4 / 6 padding, and pill radius. `role='note'` so the annotation reads as supplementary rather than as a main UI control.",
39
+ "description": "The pill body. `position: relative` so the tail can pin to its edge; `display: inline-flex` so the bubble shrink-wraps its body. Carries the fill (`--bubble-fill` defaulting to `sys.color.background.primary`), ink (`--bubble-ink` defaulting to `sys.color.text.onFill`), 4 / 6 padding, and pill radius. `role='note'` so the annotation reads as supplementary rather than as a main UI control.",
40
40
  "intrinsic": true
41
41
  },
42
42
  "body": {
43
43
  "required": true,
44
- "description": "Bubble copy in `sys.typo.caption` (10 / Semibold). Single line — `white-space: nowrap` + `text-overflow: ellipsis` truncate overflow rather than wrap. Inherits the container's ink colour.",
44
+ "description": "Bubble copy in `sys.typo.label.xs` (10 / Semibold). Single line — `white-space: nowrap` + `text-overflow: ellipsis` truncate overflow rather than wrap. Inherits the container's ink colour.",
45
45
  "accepts": ["text"]
46
46
  },
47
47
  "tail": {
@@ -51,9 +51,9 @@
51
51
  }
52
52
  },
53
53
  "sizing": {
54
- "background": "sys.color.primary",
55
- "foreground": "sys.color.onPrimary",
56
- "labelTypo": "sys.typo.caption",
54
+ "background": "sys.color.background.primary",
55
+ "foreground": "sys.color.text.onFill",
56
+ "labelTypo": "sys.typo.label.xs",
57
57
  "paddingBlock": "sys.layout.container.2xs",
58
58
  "paddingInline": "ref.space.75",
59
59
  "radius": "sys.radius.full",
@@ -62,9 +62,9 @@
62
62
  "viewportSafeArea": "sys.layout.container.xs"
63
63
  },
64
64
  "appearance": {
65
- "background": "sys.color.primary",
66
- "foreground": "sys.color.onPrimary",
67
- "note": "Single canonical appearance — Bubble has no `default` / `accent` / `destructive` axis. Default fill is `sys.color.primary` and label is `sys.color.onPrimary`; both are theme-stable so the bubble reads identically in light and dark mode. Operations re-tint by setting `--bubble-fill` and `--bubble-ink` on the bubble's inline style or a wrapping class — the tail inherits the fill via `background: inherit`, so a colour swap on the container covers the whole bubble in one declaration. Re-tints should keep the contrast above WCAG AA against `--bubble-ink` (4.5:1); the system does not enforce this at runtime."
65
+ "background": "sys.color.background.primary",
66
+ "foreground": "sys.color.text.onFill",
67
+ "note": "Single canonical appearance — Bubble has no `default` / `accent` / `destructive` axis. Default fill is `sys.color.background.primary` and label is `sys.color.text.onFill`; both are theme-stable so the bubble reads identically in light and dark mode. Operations re-tint by setting `--bubble-fill` and `--bubble-ink` on the bubble's inline style or a wrapping class — the tail inherits the fill via `background: inherit`, so a colour swap on the container covers the whole bubble in one declaration. Re-tints should keep the contrast above WCAG AA against `--bubble-ink` (4.5:1); the system does not enforce this at runtime."
68
68
  },
69
69
  "behavior": {
70
70
  "ariaRole": "Container carries `role='note'` so screen readers announce the bubble as a supplementary note rather than as a main UI control. The decorative tail carries `aria-hidden='true'`.",
@@ -74,12 +74,12 @@
74
74
  "zeroGapToAnchor": "**The tail's TIP sits flush on the anchor's CONTENT edge (padding excluded) — no overlap, no gap.** This is the canonical home of the set-back math. Align to the anchor's content box, not its padding box: the tail-bearing edge (top for `tailSide='top'`, bottom for `tailSide='bottom'`) is set BACK from the anchor by the tail's own protrusion so the tail's outer vertex lands exactly on the anchor's content edge. The tail is a `ref.space.50` (4) square rotated 45°, so its vertex pokes `ref.space.50 / √2` ≈ 2.83 past the bubble edge; the system exposes this at :root as `--bubble-tail-protrusion`, and the host adds exactly that set-back (e.g. `top: calc(anchor(bottom) + var(--bubble-tail-protrusion))`). Overlapping the anchor reads as a collision; a gap larger than the protrusion leaves the tail unattached. For more breathing room, widen the *anchor's* padding — never enlarge the set-back. This √2 set-back is the single derived constant in the contract; the horizontal axis is pure visual alignment.",
75
75
  "tailAlignSelection": "`tailAlign='center'` is the default and preferred case — the bubble centres on the anchor so the tail sits dead-centre and its tip falls on the anchor's centreX by construction (see tailTipCentredOnAnchor). It flips off-centre only as an edge fallback: when the anchor sits so near a viewport edge that centring would push the bubble past the 8-token safe margin, the bubble shifts AWAY from the edge and `tailAlign` flips to the SAME side as the anchor — `end` at the right edge (bubble extends leftward), `start` at the left edge (bubble extends rightward). In every case the bubble's nearest edge stays ≥8 from the viewport edge and the tip stays on the anchor's centreline; visibility is the contract, tail position is the lever.",
76
76
  "positioning": "Bubble is presentational — the *host* owns positioning, and the canonical pattern binds bubble to anchor by VISUAL ALIGNMENT (CSS anchor positioning, or a positioned wrapper around the anchor) rather than DOM proximity, so the tail tracks the anchor through reflows. Four coupled decisions, each detailed in its own rule: (a) `tailSide` — `top` when the bubble sits below the anchor, `bottom` when above; (b) the vertical set-back so the tail tip is flush on the anchor's content edge — see zeroGapToAnchor; (c) horizontal centring so the tip lands on the anchor's centreX — see tailTipCentredOnAnchor; (d) `tailAlign`, `center` by default and flipped only at a viewport edge — see tailAlignSelection. Get any one wrong and the tail points at empty space.",
77
- "colorTuning": "Two CSS custom properties — `--bubble-fill` (background) and `--bubble-ink` (label) — are the supported runtime override surface. Default to `sys.color.primary` / `sys.color.onPrimary`. Operations campaigns set them on inline style; static themes set them on a wrapper class. The tail's `background: inherit` follows automatically."
77
+ "colorTuning": "Two CSS custom properties — `--bubble-fill` (background) and `--bubble-ink` (label) — are the supported runtime override surface. Default to `sys.color.background.primary` / `sys.color.text.onFill`. Operations campaigns set them on inline style; static themes set them on a wrapper class. The tail's `background: inherit` follows automatically."
78
78
  },
79
79
  "forbidden": [
80
80
  "bubble used as a transient hover/focus tooltip — that role is the `tooltip` family; bubble must remain in view at the UI's resting state",
81
81
  "bubble portal-mounted at document root with a fixed z-index that overlays surrounding chrome — bubble must NEVER occlude neighbour elements; that contract is what separates it from Tooltip",
82
- "bubble painted with an elevation shadow (`sys.elevation.raised` / `floating` / `overlay` / `sheet`) — the lift signal is what makes Tooltip 'jump'; Bubble is intentionally flat",
82
+ "bubble painted with an elevation shadow (`sys.elevation.floating` / `overlay` / `sheet`) — the lift signal is what makes Tooltip 'jump'; Bubble is intentionally flat",
83
83
  "bubble copy that wraps to multiple lines — overflow truncates with an ellipsis rather than wrapping; if the message can't fit on one line, it belongs in a Banner or a Tooltip with action",
84
84
  "bubble retinted with hardcoded hex / rgba on `background` or `color` instead of `--bubble-fill` / `--bubble-ink` — campaign tunes must route through the documented custom-property surface so the tail colour tracks the body",
85
85
  "bubble used to convey *required* meaning (error blocking a submit, account-locked notice) — that role is Banner / Dialog; bubble is a soft annotation only",
@@ -20,7 +20,7 @@ Transparent-rest forms ([Icon Button](./icon.md), [Text Button](./text.md), [Ter
20
20
 
21
21
  ### Focus ring
22
22
 
23
- All Button family components draw the same outward two-layer ring as a `position: absolute` pseudo-element — never a layout-affecting border. Suppressed while `disabled`. Trigger: `:focus-visible`. See [DESIGN.md → Focus ring composition](../../DESIGN.md#focus-ring-composition).
23
+ All Button family components draw the same outward single ring as a `position: absolute` pseudo-element — never a layout-affecting border. Suppressed while `disabled`. Trigger: `:focus-visible`. See [DESIGN.md → Focus ring composition](../../DESIGN.md#focus-ring-composition).
24
24
 
25
25
  ## Sub-components
26
26
 
@@ -35,11 +35,9 @@ function Demo() {
35
35
  <Demo />
36
36
  ```
37
37
 
38
- ## Use cases
38
+ ## Accent
39
39
 
40
- ### Accent
41
-
42
- Brand-blue label — `sys.color.primary`. Use sparingly; never two accent Check Buttons in the same row.
40
+ Brand-blue label — `sys.color.background.primary`. Use sparingly; never two accent Check Buttons in the same row.
43
41
 
44
42
  ```preview
45
43
  button/check/accent
@@ -51,9 +49,9 @@ import { Button } from '@teamblind-chorus/ui';
51
49
  </Button>
52
50
  ```
53
51
 
54
- ### Inverse
52
+ ## Inverse
55
53
 
56
- Mirror for inverse hosts (Toast, coach-mark, snackbar). Label paints `sys.color.inverseOnSurface` against the host's `inverseSurface` fill.
54
+ Mirror for inverse hosts (Toast, coach-mark, snackbar). Label paints `sys.color.text.inverse` against the host's `inverseSurface` fill.
57
55
 
58
56
  ```preview
59
57
  button/check/inverse
@@ -65,7 +63,7 @@ import { Button } from '@teamblind-chorus/ui';
65
63
  </Button>
66
64
  ```
67
65
 
68
- ### Checked
66
+ ## Checked
69
67
 
70
68
  Same row with `checked={true}` — checkbox glyph flips to the filled square. State overlays follow the label color.
71
69
 
@@ -79,7 +77,7 @@ import { Button } from '@teamblind-chorus/ui';
79
77
  </Button>
80
78
  ```
81
79
 
82
- ### With middle icon
80
+ ## Middle icon
83
81
 
84
82
  Optional 16px icon between checkbox and label. Use sparingly — most rows don't need it. Canonical case: an item-use row where the middle glyph names the item being consumed.
85
83
 
@@ -100,9 +98,9 @@ Three appearances. `default` is the base neutral toggle; `accent` paints the lab
100
98
 
101
99
  | Appearance | Background (rest) | Label color | When to reach for it |
102
100
  |-----------|-------------------|------------------------------|-----------------------------------------------------------------------|
103
- | `default` | `transparent` | `sys.color.onSurfaceVariant` | The base neutral toggle — option rows next to a primary commit. |
104
- | `accent` | `transparent` | `sys.color.primary` | One option per row that needs commit-rank emphasis. |
105
- | `inverse` | `transparent` | `sys.color.inverseOnSurface` | Inside an inverse host (Toast, coach-mark, snackbar). |
101
+ | `default` | `transparent` | `sys.color.text.subtle` | The base neutral toggle — option rows next to a primary commit. |
102
+ | `accent` | `transparent` | `sys.color.background.primary` | One option per row that needs commit-rank emphasis. |
103
+ | `inverse` | `transparent` | `sys.color.text.inverse` | Inside an inverse host (Toast, coach-mark, snackbar). |
106
104
 
107
105
  ## Slots
108
106
 
@@ -92,19 +92,19 @@
92
92
  "default": {
93
93
  "background": "transparent",
94
94
  "border": null,
95
- "label": "sys.color.onSurfaceVariant",
95
+ "label": "sys.color.text.subtle",
96
96
  "note": "The base neutral inline toggle — the canonical Check Button. Quiet enough to live next to typographic content without claiming commit-rank attention."
97
97
  },
98
98
  "accent": {
99
99
  "background": "transparent",
100
100
  "border": null,
101
- "label": "sys.color.primary",
101
+ "label": "sys.color.text.link",
102
102
  "note": "Brand-blue label for the inline toggle. Use sparingly — never two `accent` Check Buttons in the same row."
103
103
  },
104
104
  "inverse": {
105
105
  "background": "transparent",
106
106
  "border": null,
107
- "label": "sys.color.inverseOnSurface",
107
+ "label": "sys.color.text.inverse",
108
108
  "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."
109
109
  }
110
110
  },
@@ -124,6 +124,25 @@
124
124
  "opacity": "sys.state.pressed"
125
125
  }
126
126
  },
127
+ "focused": {
128
+ "overlay": {
129
+ "color": "label",
130
+ "opacity": "sys.state.focus"
131
+ },
132
+ "focusRing": {
133
+ "composition": "outward",
134
+ "layer": "::after overlay — position:absolute, inset:0, no reflow (DESIGN.md Focus ring composition)",
135
+ "innerCounterRing": {
136
+ "width": "sys.borderWidth.hairline",
137
+ "color": "sys.color.border.focused"
138
+ },
139
+ "outerRing": {
140
+ "width": "sys.borderWidth.thin",
141
+ "color": "sys.color.border.focused"
142
+ }
143
+ },
144
+ "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."
145
+ },
127
146
  "disabled": {
128
147
  "overlay": null,
129
148
  "containerOpacity": "sys.state.disabled",
@@ -134,16 +153,14 @@
134
153
  "focusIndicator": {
135
154
  "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.",
136
155
  "composition": "outward",
137
- "compositionReason": "Action affordance with breathing room around it; the 3px outward extent is reserved by the surrounding layout.",
156
+ "compositionReason": "Action affordance with breathing room around it; the 1px outward ring is reserved by the surrounding layout.",
138
157
  "overlay": {
139
158
  "color": "label",
140
159
  "opacity": "sys.state.focus"
141
160
  },
142
161
  "ring": {
143
- "outerWidth": "sys.borderWidth.thin",
144
- "outerColor": "sys.color.focus",
145
- "insetWidth": "sys.borderWidth.hairline",
146
- "insetColor": "sys.color.focusInset"
162
+ "width": "sys.borderWidth.hairline",
163
+ "color": "sys.color.border.focused"
147
164
  },
148
165
  "trigger": ":focus-visible (keyboard / programmatic focus, never plain mouse click)"
149
166
  },