@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
@@ -27,9 +27,7 @@ import { PlusIcon } from '@teamblind-chorus/ui/icons';
27
27
  </Button>
28
28
  ```
29
29
 
30
- ## Use cases
31
-
32
- ### Secondary
30
+ ## Secondary
33
31
 
34
32
  Theme-toned FAB that defers to the canvas's surface tones. Reach for it when a brand-red FAB would over-claim the page hierarchy (filtered map, image-rich feed).
35
33
 
@@ -48,7 +46,7 @@ import { PlusIcon } from '@teamblind-chorus/ui/icons';
48
46
  </Button>
49
47
  ```
50
48
 
51
- ### Icon
49
+ ## Icon
52
50
 
53
51
  Icon-only — 48 × 48 circle with a 24px glyph. For universally legible actions (`+`, pencil). **Requires `aria-label`.**
54
52
 
@@ -66,7 +64,7 @@ import { PlusIcon } from '@teamblind-chorus/ui/icons';
66
64
  />
67
65
  ```
68
66
 
69
- ### Text
67
+ ## Text
70
68
 
71
69
  Label-only pill. For multi-word actions or verbs without an obvious glyph (*Save draft*).
72
70
 
@@ -80,7 +78,7 @@ import { Button } from '@teamblind-chorus/ui';
80
78
  </Button>
81
79
  ```
82
80
 
83
- ### Extended
81
+ ## Extended
84
82
 
85
83
  Icon + label. Default for primary canvas commits where space allows (desktop canvases, mobile sheets).
86
84
 
@@ -99,7 +97,7 @@ import { PlusIcon } from '@teamblind-chorus/ui/icons';
99
97
  </Button>
100
98
  ```
101
99
 
102
- ### Focus indicator
100
+ ## Focus ring
103
101
 
104
102
  Standard ring with the FAB's floating elevation stacked underneath. See [Focus ring composition](../../DESIGN.md#focus-ring-composition).
105
103
 
@@ -127,8 +125,8 @@ Two appearances on the standard [Button](./button.md) emphasis ladder — `prima
127
125
 
128
126
  | Appearance | Background | Label / icon color | When to reach for it |
129
127
  |-------------|----------------------------------|----------------------|---------------------------------------------------------------------------------------------------|
130
- | `primary` | `sys.color.brand` | `sys.color.onBrand` | Brand-red commit anchoring the canvas's next step (Compose, Add, Create). |
131
- | `secondary` | `sys.color.surfaceContainerHigh` | `sys.color.onSurface` | Theme-toned alternative on dense/chromatic canvases where brand-red would over-claim. |
128
+ | `primary` | `sys.color.text.brand` | `sys.color.text.onFill` | Brand-red commit anchoring the canvas's next step (Compose, Add, Create). |
129
+ | `secondary` | `sys.color.surfaceContainerHigh` | `sys.color.text.default` | Theme-toned alternative on dense/chromatic canvases where brand-red would over-claim. |
132
130
 
133
131
  ## Sizes
134
132
 
@@ -56,12 +56,12 @@
56
56
  },
57
57
  "appearances": {
58
58
  "primary": {
59
- "background": "sys.color.brand",
60
- "label": "sys.color.onBrand"
59
+ "background": "sys.color.text.brand",
60
+ "label": "sys.color.text.onFill"
61
61
  },
62
62
  "secondary": {
63
- "background": "sys.color.surfaceContainerHigh",
64
- "label": "sys.color.onSurface"
63
+ "background": "sys.color.surface.default",
64
+ "label": "sys.color.text.default"
65
65
  }
66
66
  },
67
67
  "states": {
@@ -79,27 +79,44 @@
79
79
  "color": "label",
80
80
  "opacity": "sys.state.pressed"
81
81
  }
82
+ },
83
+ "focused": {
84
+ "overlay": {
85
+ "color": "label",
86
+ "opacity": "sys.state.focus"
87
+ },
88
+ "focusRing": {
89
+ "composition": "outward",
90
+ "layer": "::after overlay — position:absolute, inset:0, no reflow (DESIGN.md Focus ring composition)",
91
+ "innerCounterRing": {
92
+ "width": "sys.borderWidth.hairline",
93
+ "color": "sys.color.border.focused"
94
+ },
95
+ "outerRing": {
96
+ "width": "sys.borderWidth.thin",
97
+ "color": "sys.color.border.focused"
98
+ }
99
+ },
100
+ "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 FAB is in; never via plain mouse click."
82
101
  }
83
102
  },
84
103
  "focusIndicator": {
85
104
  "description": "Keyboard-focus visual — an accessibility indicator, not a lifecycle state. Composes over whichever lifecycle state the FAB is in. The ring stacks above the FAB's floating elevation so the lift survives focus. The `states.focused` block above is kept for JSX runtime consumers; this block is the parallel external-reader contract.",
86
105
  "composition": "outward",
87
- "compositionReason": "Action affordance with breathing room around it; the 3px outward extent is reserved by the surrounding layout.",
106
+ "compositionReason": "Action affordance with breathing room around it; the 1px outward ring is reserved by the surrounding layout.",
88
107
  "overlay": {
89
108
  "color": "label",
90
109
  "opacity": "sys.state.focus"
91
110
  },
92
111
  "ring": {
93
- "outerWidth": "sys.borderWidth.thin",
94
- "outerColor": "sys.color.focus",
95
- "insetWidth": "sys.borderWidth.hairline",
96
- "insetColor": "sys.color.focusInset"
112
+ "width": "sys.borderWidth.hairline",
113
+ "color": "sys.color.border.focused"
97
114
  },
98
115
  "trigger": ":focus-visible (keyboard / programmatic focus, never plain mouse click)"
99
116
  },
100
117
  "forbidden": [
101
118
  "more than one fab per screen — the FAB is the single canonical commit",
102
- "fab styled with sys.color.primary fill — the FAB carries sys.color.brand by anatomy contract",
119
+ "fab styled with sys.color.background.primary fill — the FAB carries sys.color.text.brand by anatomy contract",
103
120
  "destructive flavor on a FAB — destructive commits live inside Dialog / BottomSheet, never a floating commit",
104
121
  "fab placed inline in flow — it floats over content, anchored bottom-right by the page shell"
105
122
  ]
@@ -3,7 +3,7 @@
3
3
  "name": "ButtonGroup",
4
4
  "family": "button",
5
5
  "subcomponent": "group",
6
- "description": "Lays out two or more Buttons as one composition so consumers never hand-roll the wrapper div. A NAMED export (`import { ButtonGroup }`) — NOT a Button variant. Three forms share the family's 8px (`sys.layout.inline.md`) gap: inline horizontal (default), inline vertical (`orientation=\"vertical\"`), and docked (`variant=\"docked\"`) — a footer bar pinned to the bottom of the app. The docked form adds full-bleed `sys.color.surface`, a 16px (`sys.layout.container.md`) inset, an upward `sys.elevation.sheet` shadow (no top stroke), and an optional `label` above the row.",
6
+ "description": "Lays out two or more Buttons as one composition so consumers never hand-roll the wrapper div. A NAMED export (`import { ButtonGroup }`) — NOT a Button variant. Three forms share the family's 8px (`sys.layout.inline.md`) gap: inline horizontal (default), inline vertical (`orientation=\"vertical\"`), and docked (`variant=\"docked\"`) — a footer bar pinned to the bottom of the app. The docked form adds full-bleed `sys.color.surface.default`, a 16px (`sys.layout.container.md`) inset, an upward `sys.elevation.sheet` shadow (no top stroke), and an optional `label` above the row.",
7
7
  "element": "div",
8
8
  "props": {
9
9
  "variant": {
@@ -26,7 +26,7 @@
26
26
  "label": {
27
27
  "type": "node",
28
28
  "optional": true,
29
- "note": "Optional caption above the row. Rendered in `sys.typo.body.md` (16px) / `sys.color.onSurfaceVariant`, centered. An inline <strong> reads as the emphasized value in the full-strength on-surface tone."
29
+ "note": "Optional caption above the row. Rendered in `sys.typo.body.md` (16px) / `sys.color.text.subtle`, centered. An inline <strong> reads as the emphasized value in the full-strength on-surface tone."
30
30
  }
31
31
  },
32
32
  "slots": {
@@ -39,7 +39,7 @@
39
39
  },
40
40
  "label": {
41
41
  "required": false,
42
- "description": "Optional caption above the row (`label` prop). `sys.typo.body.md` (16px), `sys.color.onSurfaceVariant`, centered, 16px (`sys.layout.stack.md`) above the row.",
42
+ "description": "Optional caption above the row (`label` prop). `sys.typo.body.md` (16px), `sys.color.text.subtle`, centered, 16px (`sys.layout.stack.md`) above the row.",
43
43
  "accepts": [
44
44
  "text"
45
45
  ]
@@ -49,7 +49,7 @@
49
49
  "gap": "sys.layout.inline.md (8) — between Buttons, both axes",
50
50
  "labelStackGap": "sys.layout.stack.md (16) — label → row",
51
51
  "docked": {
52
- "background": "sys.color.surface",
52
+ "background": "sys.color.surface.default",
53
53
  "padding": "sys.layout.container.md (16) — all four sides",
54
54
  "elevation": "sys.elevation.sheet — upward shadow, the same one Bottom Sheet / Side Sheet cast",
55
55
  "border": null,
@@ -21,9 +21,7 @@ import { SearchIcon } from '@teamblind-chorus/ui/icons';
21
21
  <Button variant="icon" size="large" icon={<SearchIcon />} aria-label="Search" />
22
22
  ```
23
23
 
24
- ## Use cases
25
-
26
- ### Inverse
24
+ ## Inverse
27
25
 
28
26
  Mirror for inverse hosts (Toast dismiss, coach-mark close). Glyph paints in `inverseOnSurface` against the host's `inverseSurface` fill; state overlays mix from the same token.
29
27
 
@@ -36,9 +34,9 @@ import { XIcon } from '@teamblind-chorus/ui/icons';
36
34
  <Button variant="icon" size="medium" appearance="inverse" icon={<XIcon />} aria-label="Dismiss" />
37
35
  ```
38
36
 
39
- ### Custom palette colours
37
+ ## Custom palette
40
38
 
41
- Outside the named appearances, the glyph inherits `currentColor` — any Chorus icon-paint token works. Reach for a custom colour when the glyph carries semantic weight (favorite star → `sys.color.icon.yellow`, success check → `sys.color.success`, warning bolt, channel-branded glyph in a brand-tinted host). Apply via inline `color` so state overlays still mix from the same token at standard `sys.state.*` opacities — never override `background` or wrap in another element to recolour.
39
+ Outside the named appearances, the glyph inherits `currentColor` — any Chorus icon-paint token works. Reach for a custom colour when the glyph carries semantic weight (favorite star → `sys.color.icon.accent.yellow.default`, success check → `sys.color.text.success`, warning bolt, channel-branded glyph in a brand-tinted host). Apply via inline `color` so state overlays still mix from the same token at standard `sys.state.*` opacities — never override `background` or wrap in another element to recolour.
42
40
 
43
41
  ```preview
44
42
  button/icon/custom-color
@@ -47,41 +45,41 @@ import { Button } from '@teamblind-chorus/ui';
47
45
  import { StarFillIcon } from '@teamblind-chorus/ui/icons';
48
46
 
49
47
  <div style={{ display: 'inline-flex', gap: 'var(--sys-layout-inline-xl)' }}>
50
- <Button variant="icon" icon={<StarFillIcon />} aria-label="Favorite — inactive" style={{ color: 'var(--sys-color-icon-muted)' }} />
51
- <Button variant="icon" icon={<StarFillIcon />} aria-label="Favorite — active" style={{ color: 'var(--sys-color-icon-yellow)' }} />
48
+ <Button variant="icon" icon={<StarFillIcon />} aria-label="Favorite — inactive" style={{ color: 'var(--sys-color-icon-subtle)' }} />
49
+ <Button variant="icon" icon={<StarFillIcon />} aria-label="Favorite — active" style={{ color: 'var(--sys-color-icon-accent-yellow-default)' }} />
52
50
  </div>
53
51
  ```
54
52
 
55
53
  Pick from `sys.color.*` — semantic roles (`primary` / `success` / `error` / …) for status pairs that also carry a background, and the dedicated `sys.color.icon.*` palette (`muted` / `yellow` / `red` / `blue` / `green` / `purple`) for standalone semantic glyphs. Reaching past sys into `ref.palette.*`, hardcoded hex, and Tailwind colour utilities are all forbidden.
56
54
 
57
- ### Group
55
+ ## Focus ring
58
56
 
59
- Three Icon Buttons in a row — common shape on the [Navigation bar](../navigation-bar/navigation-bar.md) Main trailing slot. Adjacent buttons sit 16px apart (`sys.layout.inline.xl`); with optical alignment on, that gap *is* the visible glyph-to-glyph distance.
57
+ Standard ring (see [Focus ring composition](../../DESIGN.md#focus-ring-composition)).
60
58
 
61
59
  ```preview
62
- button/icon/group
60
+ button/icon/focused
63
61
  ---
64
62
  import { Button } from '@teamblind-chorus/ui';
65
- import { SearchIcon, ChatIcon, ProfileIcon } from '@teamblind-chorus/ui/icons';
63
+ import { SearchIcon } from '@teamblind-chorus/ui/icons';
66
64
 
67
- <div style={{ display: 'inline-flex', gap: 'var(--sys-layout-inline-xl)' }}>
68
- <Button variant="icon" icon={<SearchIcon />} aria-label="Search" />
69
- <Button variant="icon" icon={<ChatIcon />} aria-label="Messages" />
70
- <Button variant="icon" icon={<ProfileIcon />} aria-label="Profile" />
71
- </div>
65
+ <Button variant="icon" icon={<SearchIcon />} aria-label="Search" state="focused" />
72
66
  ```
73
67
 
74
- ### Focus indicator
68
+ ## Group
75
69
 
76
- Standard ring (see [Focus ring composition](../../DESIGN.md#focus-ring-composition)).
70
+ Three Icon Buttons in a row — common shape on the [Navigation bar](../navigation-bar/navigation-bar.md) Main trailing slot. Adjacent buttons sit 16px apart (`sys.layout.inline.xl`); with optical alignment on, that gap *is* the visible glyph-to-glyph distance.
77
71
 
78
72
  ```preview
79
- button/icon/focused
73
+ button/icon/group
80
74
  ---
81
75
  import { Button } from '@teamblind-chorus/ui';
82
- import { SearchIcon } from '@teamblind-chorus/ui/icons';
76
+ import { SearchIcon, ChatIcon, ProfileIcon } from '@teamblind-chorus/ui/icons';
83
77
 
84
- <Button variant="icon" icon={<SearchIcon />} aria-label="Search" state="focused" />
78
+ <div style={{ display: 'inline-flex', gap: 'var(--sys-layout-inline-xl)' }}>
79
+ <Button variant="icon" icon={<SearchIcon />} aria-label="Search" />
80
+ <Button variant="icon" icon={<ChatIcon />} aria-label="Messages" />
81
+ <Button variant="icon" icon={<ProfileIcon />} aria-label="Profile" />
82
+ </div>
85
83
  ```
86
84
 
87
85
  ## Slots
@@ -105,8 +103,8 @@ Two named appearances; geometry identical, only the glyph colour pair flips.
105
103
 
106
104
  | Appearance | Background | Border | Icon color | When to reach for it |
107
105
  |-------------|---------------|--------|----------------------------------|----------------------|
108
- | `default` | `transparent` | none | `sys.color.onSurface` | Every regular page surface. |
109
- | `inverse` | `transparent` | none | `sys.color.inverseOnSurface` | For use inside an inverse host (Toast dismiss, coach-mark close). |
106
+ | `default` | `transparent` | none | `sys.color.text.default` | Every regular page surface. |
107
+ | `inverse` | `transparent` | none | `sys.color.text.inverse` | For use inside an inverse host (Toast dismiss, coach-mark close). |
110
108
 
111
109
  ## Sizes
112
110
 
@@ -75,24 +75,24 @@
75
75
  "default": {
76
76
  "background": "transparent",
77
77
  "border": null,
78
- "icon": "sys.color.onSurface",
78
+ "icon": "sys.color.icon.default",
79
79
  "note": "Transparent capsule with the glyph in `onSurface`. The canonical Icon Button chrome, used on every regular page surface."
80
80
  },
81
81
  "inverse": {
82
82
  "background": "transparent",
83
83
  "border": null,
84
- "icon": "sys.color.inverseOnSurface",
84
+ "icon": "sys.color.text.inverse",
85
85
  "note": "Mirror of `default` for use inside an inverse host (Toast, coach-mark, snackbar). Glyph 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."
86
86
  }
87
87
  },
88
88
  "customGlyphColor": {
89
89
  "allowed": true,
90
- "description": "Outside the named `default` / `inverse` appearances, the glyph colour is open — the icon inherits `currentColor`, so any Chorus icon-paint token can be applied via inline `color` (e.g. `style={{ color: 'var(--sys-color-icon-yellow)' }}` for an active favorite star, `var(--sys-color-success)` for a confirm check, `var(--sys-color-icon-muted)` for the unpressed partner). State overlays still mix from the same token at the standard `hovered`/`pressed`/`focused` state opacities, so hover / pressed / focus carry over without per-host tuning.",
90
+ "description": "Outside the named `default` / `inverse` appearances, the glyph colour is open — the icon inherits `currentColor`, so any Chorus icon-paint token can be applied via inline `color` (e.g. `style={{ color: 'var(--sys-color-icon-accent-yellow-default)' }}` for an active favorite star, `var(--sys-color-icon-success)` for a confirm check, `var(--sys-color-icon-subtle)` for the unpressed partner). State overlays still mix from the same token at the standard `hovered`/`pressed`/`focused` state opacities, so hover / pressed / focus carry over without per-host tuning.",
91
91
  "constraints": [
92
- "Pick a Chorus colour token. Two valid sources: (a) a status-pair role (`sys.color.primary`, `sys.color.success`, `sys.color.error`, etc.) when the glyph travels with its own background; (b) the dedicated icon palette (`sys.color.icon.muted`, `sys.color.icon.yellow`, `sys.color.icon.red`, `sys.color.icon.blue`, `sys.color.icon.green`, `sys.color.icon.purple`) for standalone semantic glyphs. Reaching past sys into the raw palette (`ref.palette.yellow.500` etc.), raw hex, and Tailwind colour utilities are all forbidden.",
92
+ "Pick a Chorus colour token. Two valid sources: (a) a status-pair role (`sys.color.background.primary`, `sys.color.text.success`, `sys.color.text.danger`, etc.) when the glyph travels with its own background; (b) the dedicated icon palette (`sys.color.icon.subtle`, `sys.color.icon.accent.yellow.default`, `sys.color.icon.accent.red.default`, `sys.color.icon.accent.blue.default`, `sys.color.icon.accent.green.default`, `sys.color.icon.accent.purple.default`) for standalone semantic glyphs. Reaching past sys into the raw palette (`ref.palette.yellow.500` etc.), raw hex, and Tailwind colour utilities are all forbidden.",
93
93
  "Override the icon's `color` only — never `background`, never wrap in another element to restyle.",
94
94
  "Reach for a custom colour when the glyph itself carries semantic weight (favorite, success, warning, brand-tinted host). Otherwise stick to `default` / `inverse`.",
95
- "The `destructive` flavor (`sys.color.error`) is a named convenience for the same pattern."
95
+ "The `destructive` flavor (`sys.color.text.danger`) is a named convenience for the same pattern."
96
96
  ]
97
97
  },
98
98
  "flavors": {
@@ -102,12 +102,12 @@
102
102
  "default": {
103
103
  "background": "transparent",
104
104
  "border": null,
105
- "icon": "sys.color.error"
105
+ "icon": "sys.color.icon.danger"
106
106
  },
107
107
  "inverse": {
108
108
  "background": "transparent",
109
109
  "border": null,
110
- "icon": "sys.color.error"
110
+ "icon": "sys.color.icon.danger"
111
111
  }
112
112
  }
113
113
  }
@@ -128,6 +128,25 @@
128
128
  "opacity": "sys.state.pressed"
129
129
  }
130
130
  },
131
+ "focused": {
132
+ "overlay": {
133
+ "color": "icon",
134
+ "opacity": "sys.state.focus"
135
+ },
136
+ "focusRing": {
137
+ "composition": "outward",
138
+ "layer": "::after overlay — position:absolute, inset:0, no reflow (DESIGN.md Focus ring composition)",
139
+ "innerCounterRing": {
140
+ "width": "sys.borderWidth.hairline",
141
+ "color": "sys.color.border.focused"
142
+ },
143
+ "outerRing": {
144
+ "width": "sys.borderWidth.thin",
145
+ "color": "sys.color.border.focused"
146
+ }
147
+ },
148
+ "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."
149
+ },
131
150
  "disabled": {
132
151
  "overlay": null,
133
152
  "containerOpacity": "sys.state.disabled",
@@ -138,16 +157,14 @@
138
157
  "focusIndicator": {
139
158
  "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.",
140
159
  "composition": "outward",
141
- "compositionReason": "Action affordance with breathing room around it; the 3px outward extent is reserved by the surrounding layout.",
160
+ "compositionReason": "Action affordance with breathing room around it; the 1px outward ring is reserved by the surrounding layout.",
142
161
  "overlay": {
143
162
  "color": "icon",
144
163
  "opacity": "sys.state.focus"
145
164
  },
146
165
  "ring": {
147
- "outerWidth": "sys.borderWidth.thin",
148
- "outerColor": "sys.color.focus",
149
- "insetWidth": "sys.borderWidth.hairline",
150
- "insetColor": "sys.color.focusInset"
166
+ "width": "sys.borderWidth.hairline",
167
+ "color": "sys.color.border.focused"
151
168
  },
152
169
  "trigger": ":focus-visible (keyboard / programmatic focus, never plain mouse click)"
153
170
  },
@@ -22,9 +22,7 @@ import { Button } from '@teamblind-chorus/ui';
22
22
  </Button>
23
23
  ```
24
24
 
25
- ## Use cases
26
-
27
- ### Secondary
25
+ ## Secondary
28
26
 
29
27
  Lower-emphasis tier paired against `primary` — opposing actions (Cancel beside Save) or quieter alternatives. Safe to repeat on a single view.
30
28
 
@@ -38,7 +36,7 @@ import { Button } from '@teamblind-chorus/ui';
38
36
  </Button>
39
37
  ```
40
38
 
41
- ### Outlined
39
+ ## Outlined
42
40
 
43
41
  Bordered blue-on-transparent — supplementary option beside `primary` (*See more*, *Learn more*, *Skip for now*). For opposing paths use `secondary`.
44
42
 
@@ -52,9 +50,9 @@ import { Button } from '@teamblind-chorus/ui';
52
50
  </Button>
53
51
  ```
54
52
 
55
- ### Tertiary
53
+ ## Tertiary
56
54
 
57
- Neutral grey ghost — transparent at rest, label in `sys.color.onSurfaceVariant`. Reads as a button only on hover.
55
+ Neutral grey ghost — transparent at rest, label in `sys.color.text.subtle`. Reads as a button only on hover.
58
56
 
59
57
  ```preview
60
58
  button/standard/tertiary
@@ -66,7 +64,7 @@ import { Button } from '@teamblind-chorus/ui';
66
64
  </Button>
67
65
  ```
68
66
 
69
- ### With leading icon
67
+ ## Leading icon
70
68
 
71
69
  Optional context glyph before the label. Inherits label color via `currentColor` (`sys.icon.lg` on `large`, `sys.icon.md` on `medium`/`small`).
72
70
 
@@ -85,7 +83,7 @@ import { PlusIcon } from '@teamblind-chorus/ui/icons';
85
83
  </Button>
86
84
  ```
87
85
 
88
- ### Full width
86
+ ## Full width
89
87
 
90
88
  Stretched to fill the column (`width: 100%`). Default mobile shape for hero surfaces, empty states, onboarding, login. On wider surfaces, fall back to content-sized.
91
89
 
@@ -103,33 +101,7 @@ import { Button } from '@teamblind-chorus/ui';
103
101
  </Button>
104
102
  ```
105
103
 
106
- ### Group
107
-
108
- Compose adjacent Buttons with **`ButtonGroup`** instead of a hand-rolled wrapper — it owns the family's **8px** gap (`sys.layout.inline.md`). Horizontal (default): outlined left, primary right. Vertical (`orientation="vertical"`): primary top, secondary below.
109
-
110
- ```preview
111
- button/standard/group
112
- ---
113
- import { Button, ButtonGroup } from '@teamblind-chorus/ui';
114
-
115
- <ButtonGroup aria-label="Group example">
116
- <Button appearance="outlined" size="large">See more</Button>
117
- <Button appearance="primary" size="large">Confirm</Button>
118
- </ButtonGroup>
119
- ```
120
-
121
- ```preview
122
- button/standard/group-vertical
123
- ---
124
- import { Button, ButtonGroup } from '@teamblind-chorus/ui';
125
-
126
- <ButtonGroup orientation="vertical" aria-label="Group example">
127
- <Button appearance="primary" size="large" fullWidth>Save</Button>
128
- <Button appearance="secondary" size="large" fullWidth>Cancel</Button>
129
- </ButtonGroup>
130
- ```
131
-
132
- ### Docked action bar
104
+ ## Docked action bar
133
105
 
134
106
  `ButtonGroup` with **`variant="docked"`** — two **Large** Buttons combined into a footer bar pinned to the bottom of the app, the screen-level commit row for a flow (a picker, a form, a detail view). Full-bleed `sys.color.surface` with a **16px** inset (`sys.layout.container.md`); the two Buttons split the row equally at the family's 8px gap (`sys.layout.inline.md`). No top stroke — instead an upward `sys.elevation.sheet` shadow (the same one [Bottom Sheet](../bottom-sheet/bottom-sheet.md) / [Side Sheet](../side-sheet/side-sheet.md) cast) lifts it off the scrolling body, so content passing behind reads as a separate region. Supplementary **outlined** left, **primary** commit right. Renders in flow — like the other bars it must **not** self-pin (`position: sticky`/`fixed`); [Page Shell](../page-shell/page-shell.md) owns the pinning.
135
107
 
@@ -144,7 +116,7 @@ import { Button, ButtonGroup } from '@teamblind-chorus/ui';
144
116
  </ButtonGroup>
145
117
  ```
146
118
 
147
- An **optional text label** sits above the row — pass it via the `label` prop. It's a caption echoing what the Buttons act on (the current selection, a running total), rendered in `sys.typo.body.md` (**16px**) / `sys.color.onSurfaceVariant`, centered, separated from the row by `sys.layout.stack.md` (16px); an inline `<strong>` reads as the emphasized value in the full-strength on-surface tone.
119
+ An **optional text label** sits above the row — pass it via the `label` prop. It's a caption echoing what the Buttons act on (the current selection, a running total), rendered in `sys.typo.body.md` (**16px**) / `sys.color.text.subtle`, centered, separated from the row by `sys.layout.stack.md` (16px); an inline `<strong>` reads as the emphasized value in the full-strength on-surface tone.
148
120
 
149
121
  ```preview
150
122
  button/standard/docked-bar-labeled
@@ -161,7 +133,7 @@ import { Button, ButtonGroup } from '@teamblind-chorus/ui';
161
133
  </ButtonGroup>
162
134
  ```
163
135
 
164
- ### Truncation
136
+ ## Truncation
165
137
 
166
138
  When the column is narrower than the label, it clips with an ellipsis — Buttons are single-line by contract.
167
139
 
@@ -180,7 +152,7 @@ import { Button } from '@teamblind-chorus/ui';
180
152
  </Button>
181
153
  ```
182
154
 
183
- ### Focus indicator
155
+ ## Focus ring
184
156
 
185
157
  Standard keyboard-focus ring (see [Focus ring composition](../../DESIGN.md#focus-ring-composition)).
186
158
 
@@ -194,6 +166,32 @@ import { Button } from '@teamblind-chorus/ui';
194
166
  </Button>
195
167
  ```
196
168
 
169
+ ## Group
170
+
171
+ Compose adjacent Buttons with **`ButtonGroup`** instead of a hand-rolled wrapper — it owns the family's **8px** gap (`sys.layout.inline.md`). Horizontal (default): outlined left, primary right. Vertical (`orientation="vertical"`): primary top, secondary below.
172
+
173
+ ```preview
174
+ button/standard/group
175
+ ---
176
+ import { Button, ButtonGroup } from '@teamblind-chorus/ui';
177
+
178
+ <ButtonGroup aria-label="Group example">
179
+ <Button appearance="outlined" size="large">See more</Button>
180
+ <Button appearance="primary" size="large">Confirm</Button>
181
+ </ButtonGroup>
182
+ ```
183
+
184
+ ```preview
185
+ button/standard/group-vertical
186
+ ---
187
+ import { Button, ButtonGroup } from '@teamblind-chorus/ui';
188
+
189
+ <ButtonGroup orientation="vertical" aria-label="Group example">
190
+ <Button appearance="primary" size="large" fullWidth>Save</Button>
191
+ <Button appearance="secondary" size="large" fullWidth>Cancel</Button>
192
+ </ButtonGroup>
193
+ ```
194
+
197
195
  ## Slots
198
196
 
199
197
  - **label** — accessible name. Required, single line; long labels truncate.
@@ -207,10 +205,10 @@ A **destructive** flavor swaps `primary` → `error` across any appearance; rese
207
205
 
208
206
  | Appearance | Background | Border (1px) | Label color | Notes |
209
207
  |-------------|---------------|--------------------------|-----------------------------------|-----------------------------------------------------------------------|
210
- | `primary` | `sys.color.primary` | — | `sys.color.onPrimary` | Single highest-emphasis action; one per view. |
211
- | `secondary` | `sys.color.secondaryContainer` | — | `sys.color.onSecondaryContainer` | Lower-emphasis tier; opposing-action and quieter-alternative roles. |
212
- | `outlined` | `transparent` | `sys.color.primary` (`sys.borderWidth.hairline`) | `sys.color.primary` | Supplementary option beside `primary`. |
213
- | `tertiary` | `transparent` | — | `sys.color.onSurfaceVariant` | Lowest-emphasis neutral ghost. |
208
+ | `primary` | `sys.color.background.primary` | — | `sys.color.text.onFill` | Single highest-emphasis action; one per view. |
209
+ | `secondary` | `sys.color.background.neutral` | — | `sys.color.text.default` | Lower-emphasis tier; opposing-action and quieter-alternative roles. |
210
+ | `outlined` | `transparent` | `sys.color.background.primary` (`sys.borderWidth.hairline`) | `sys.color.background.primary` | Supplementary option beside `primary`. |
211
+ | `tertiary` | `transparent` | — | `sys.color.text.subtle` | Lowest-emphasis neutral ghost. |
214
212
 
215
213
  ## Sizes
216
214
 
@@ -92,55 +92,55 @@
92
92
  },
93
93
  "appearances": {
94
94
  "primary": {
95
- "background": "sys.color.primary",
95
+ "background": "sys.color.background.primary",
96
96
  "border": null,
97
- "label": "sys.color.onPrimary"
97
+ "label": "sys.color.text.onFill"
98
98
  },
99
99
  "secondary": {
100
- "background": "sys.color.secondaryContainer",
100
+ "background": "sys.color.background.neutral",
101
101
  "border": null,
102
- "label": "sys.color.onSecondaryContainer"
102
+ "label": "sys.color.text.default"
103
103
  },
104
104
  "outlined": {
105
105
  "background": "transparent",
106
106
  "border": {
107
107
  "width": "sys.borderWidth.hairline",
108
- "color": "sys.color.primary"
108
+ "color": "sys.color.text.link"
109
109
  },
110
- "label": "sys.color.primary"
110
+ "label": "sys.color.text.link"
111
111
  },
112
112
  "tertiary": {
113
113
  "background": "transparent",
114
114
  "border": null,
115
- "label": "sys.color.onSurfaceVariant"
115
+ "label": "sys.color.text.subtle"
116
116
  }
117
117
  },
118
118
  "flavors": {
119
119
  "destructive": {
120
- "description": "Swaps the primary family error family across every appearance. Reserved for irreversible commits (Delete, Remove, Discard).",
120
+ "description": "Error-family appearances for irreversible commits (Delete, Remove, Discard). Destructive is a TONAL form, not a solid red fill or an outline — a soft danger-toned fill with a danger label and NO border, matching Blind's tonal button system (a tonal red sits beside a tonal neutral). It reads as destructive without a loud red block dominating the (monochrome) UI. A solid red fill is reserved for a future high-stakes escalation only.",
121
121
  "appearances": {
122
122
  "primary": {
123
- "background": "sys.color.error",
123
+ "background": "sys.color.background.danger",
124
124
  "border": null,
125
- "label": "sys.color.onError"
125
+ "label": "sys.color.text.danger"
126
126
  },
127
127
  "secondary": {
128
- "background": "sys.color.errorContainer",
128
+ "background": "sys.color.background.danger",
129
129
  "border": null,
130
- "label": "sys.color.onErrorContainer"
130
+ "label": "sys.color.text.danger"
131
131
  },
132
132
  "outlined": {
133
133
  "background": "transparent",
134
134
  "border": {
135
135
  "width": "sys.borderWidth.hairline",
136
- "color": "sys.color.error"
136
+ "color": "sys.color.text.danger"
137
137
  },
138
- "label": "sys.color.error"
138
+ "label": "sys.color.text.danger"
139
139
  },
140
140
  "tertiary": {
141
141
  "background": "transparent",
142
142
  "border": null,
143
- "label": "sys.color.error"
143
+ "label": "sys.color.text.danger"
144
144
  }
145
145
  }
146
146
  }
@@ -161,6 +161,25 @@
161
161
  "opacity": "sys.state.pressed"
162
162
  }
163
163
  },
164
+ "focused": {
165
+ "overlay": {
166
+ "color": "label",
167
+ "opacity": "sys.state.focus"
168
+ },
169
+ "focusRing": {
170
+ "composition": "outward",
171
+ "layer": "::after overlay — position:absolute, inset:0, no reflow (DESIGN.md Focus ring composition)",
172
+ "innerCounterRing": {
173
+ "width": "sys.borderWidth.hairline",
174
+ "color": "sys.color.border.focused"
175
+ },
176
+ "outerRing": {
177
+ "width": "sys.borderWidth.thin",
178
+ "color": "sys.color.border.focused"
179
+ }
180
+ },
181
+ "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."
182
+ },
164
183
  "disabled": {
165
184
  "overlay": null,
166
185
  "containerOpacity": "sys.state.disabled",
@@ -171,16 +190,14 @@
171
190
  "focusIndicator": {
172
191
  "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.",
173
192
  "composition": "outward",
174
- "compositionReason": "Action affordance with breathing room around it; the 3px outward extent is reserved by the surrounding layout.",
193
+ "compositionReason": "Action affordance with breathing room around it; the 1px outward ring is reserved by the surrounding layout.",
175
194
  "overlay": {
176
195
  "color": "label",
177
196
  "opacity": "sys.state.focus"
178
197
  },
179
198
  "ring": {
180
- "outerWidth": "sys.borderWidth.thin",
181
- "outerColor": "sys.color.focus",
182
- "insetWidth": "sys.borderWidth.hairline",
183
- "insetColor": "sys.color.focusInset"
199
+ "width": "sys.borderWidth.hairline",
200
+ "color": "sys.color.border.focused"
184
201
  },
185
202
  "trigger": ":focus-visible (keyboard / programmatic focus, never plain mouse click)"
186
203
  },