@teamblind-chorus/ui 1.2.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 (141) 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 +4 -4
  6. package/agents/components/avatar-rail/avatar-rail.md +2 -4
  7. package/agents/components/avatar-rail/avatar-rail.spec.json +10 -14
  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.md +16 -18
  13. package/agents/components/banner/banner.spec.json +14 -14
  14. package/agents/components/bottom-sheet/bottom-sheet.md +4 -6
  15. package/agents/components/bottom-sheet/bottom-sheet.spec.json +5 -5
  16. package/agents/components/bubble/bubble.md +8 -10
  17. package/agents/components/bubble/bubble.spec.json +11 -11
  18. package/agents/components/button/button.md +1 -1
  19. package/agents/components/button/check.md +9 -11
  20. package/agents/components/button/check.spec.json +8 -10
  21. package/agents/components/button/fab.md +7 -9
  22. package/agents/components/button/fab.spec.json +10 -12
  23. package/agents/components/button/group.spec.json +4 -4
  24. package/agents/components/button/icon.md +21 -23
  25. package/agents/components/button/icon.spec.json +12 -14
  26. package/agents/components/button/standard.md +40 -42
  27. package/agents/components/button/standard.spec.json +20 -22
  28. package/agents/components/button/text.md +21 -23
  29. package/agents/components/button/text.spec.json +13 -15
  30. package/agents/components/button/toggle.md +7 -9
  31. package/agents/components/button/toggle.spec.json +10 -12
  32. package/agents/components/button/toolbar.md +24 -26
  33. package/agents/components/button/toolbar.spec.json +10 -12
  34. package/agents/components/carousel/carousel.md +1 -1
  35. package/agents/components/carousel/post.md +15 -21
  36. package/agents/components/carousel/post.spec.json +17 -17
  37. package/agents/components/carousel/profile.md +9 -45
  38. package/agents/components/carousel/profile.spec.json +17 -17
  39. package/agents/components/chip/chip.md +1 -1
  40. package/agents/components/chip/filter.md +22 -24
  41. package/agents/components/chip/filter.spec.json +17 -13
  42. package/agents/components/chip/tag.md +22 -24
  43. package/agents/components/chip/tag.spec.json +19 -15
  44. package/agents/components/dialog/dialog.md +1 -3
  45. package/agents/components/dialog/dialog.spec.json +3 -3
  46. package/agents/components/directory-list/directory-list.md +1 -3
  47. package/agents/components/directory-list/directory-list.spec.json +2 -2
  48. package/agents/components/divider/divider.family.json +1 -1
  49. package/agents/components/divider/divider.md +12 -14
  50. package/agents/components/divider/divider.spec.json +8 -8
  51. package/agents/components/empty-state/empty-state.md +9 -9
  52. package/agents/components/empty-state/empty-state.spec.json +14 -14
  53. package/agents/components/feed/ad.md +2 -4
  54. package/agents/components/feed/ad.spec.json +10 -10
  55. package/agents/components/feed/post.md +41 -43
  56. package/agents/components/feed/post.spec.json +35 -39
  57. package/agents/components/form-field/form-field.md +1 -1
  58. package/agents/components/form-field/input.md +32 -34
  59. package/agents/components/form-field/input.spec.json +34 -33
  60. package/agents/components/form-field/search.md +2 -4
  61. package/agents/components/form-field/search.spec.json +19 -18
  62. package/agents/components/form-field/select.md +18 -20
  63. package/agents/components/form-field/select.spec.json +30 -29
  64. package/agents/components/form-field/textarea.md +3 -5
  65. package/agents/components/form-field/textarea.spec.json +32 -31
  66. package/agents/components/header/main.md +4 -6
  67. package/agents/components/header/main.spec.json +3 -3
  68. package/agents/components/header/sub.md +6 -8
  69. package/agents/components/header/sub.spec.json +3 -3
  70. package/agents/components/list/accordion.md +34 -45
  71. package/agents/components/list/accordion.spec.json +20 -20
  72. package/agents/components/list/entry.md +59 -81
  73. package/agents/components/list/entry.spec.json +20 -23
  74. package/agents/components/list/list.md +2 -2
  75. package/agents/components/list/radio.md +13 -20
  76. package/agents/components/list/radio.spec.json +16 -20
  77. package/agents/components/list/standard.md +50 -72
  78. package/agents/components/list/standard.spec.json +18 -21
  79. package/agents/components/metadata/compact.md +4 -6
  80. package/agents/components/metadata/compact.spec.json +6 -6
  81. package/agents/components/metadata/metadata.md +1 -1
  82. package/agents/components/metadata/standard.md +12 -14
  83. package/agents/components/metadata/standard.spec.json +10 -10
  84. package/agents/components/nav-card/nav-card.md +25 -27
  85. package/agents/components/nav-card/nav-card.spec.json +19 -19
  86. package/agents/components/nav-list/nav-list.md +2 -8
  87. package/agents/components/nav-list/nav-list.spec.json +3 -3
  88. package/agents/components/navigation-bar/main.md +9 -11
  89. package/agents/components/navigation-bar/main.spec.json +6 -6
  90. package/agents/components/navigation-bar/search.md +6 -8
  91. package/agents/components/navigation-bar/search.spec.json +9 -9
  92. package/agents/components/navigation-bar/sub.md +9 -11
  93. package/agents/components/navigation-bar/sub.spec.json +7 -7
  94. package/agents/components/pagination/pagination.family.json +1 -1
  95. package/agents/components/pagination/pagination.md +3 -3
  96. package/agents/components/pagination/pagination.spec.json +5 -5
  97. package/agents/components/profile-header/profile-header.md +9 -11
  98. package/agents/components/profile-header/profile-header.spec.json +9 -9
  99. package/agents/components/progress/progress.family.json +1 -1
  100. package/agents/components/progress/progress.md +5 -5
  101. package/agents/components/progress/progress.spec.json +8 -8
  102. package/agents/components/side-sheet/side-sheet.md +11 -13
  103. package/agents/components/side-sheet/side-sheet.spec.json +3 -3
  104. package/agents/components/skeleton/skeleton.md +7 -9
  105. package/agents/components/skeleton/skeleton.spec.json +5 -5
  106. package/agents/components/spinner/spinner.family.json +1 -1
  107. package/agents/components/spinner/spinner.md +8 -10
  108. package/agents/components/spinner/spinner.spec.json +9 -9
  109. package/agents/components/status-tag/status-tag.md +7 -9
  110. package/agents/components/status-tag/status-tag.spec.json +5 -5
  111. package/agents/components/suggestion-list/suggestion-list.md +3 -7
  112. package/agents/components/suggestion-list/suggestion-list.spec.json +8 -12
  113. package/agents/components/switch/switch.md +12 -14
  114. package/agents/components/switch/switch.spec.json +17 -18
  115. package/agents/components/tab-bar/tab-bar.md +9 -11
  116. package/agents/components/tab-bar/tab-bar.spec.json +25 -27
  117. package/agents/components/tabs/rounded.md +6 -8
  118. package/agents/components/tabs/rounded.spec.json +17 -15
  119. package/agents/components/tabs/segmented.md +4 -6
  120. package/agents/components/tabs/segmented.spec.json +4 -8
  121. package/agents/components/tabs/underline.md +9 -11
  122. package/agents/components/tabs/underline.spec.json +14 -16
  123. package/agents/components/thumbnail/thumbnail.md +5 -7
  124. package/agents/components/thumbnail/thumbnail.spec.json +8 -8
  125. package/agents/components/toast/toast.md +5 -7
  126. package/agents/components/toast/toast.spec.json +3 -3
  127. package/agents/components/tooltip/tooltip.md +6 -8
  128. package/agents/components/tooltip/tooltip.spec.json +4 -4
  129. package/agents/tokens.usage.json +71 -226
  130. package/dist/index.cjs +212 -223
  131. package/dist/index.cjs.map +1 -1
  132. package/dist/index.d.cts +16 -16
  133. package/dist/index.d.ts +16 -16
  134. package/dist/index.js +212 -223
  135. package/dist/index.js.map +1 -1
  136. package/dist/styles.css +386 -387
  137. package/eslint/rules.js +7 -7
  138. package/package.json +2 -3
  139. package/agents/anti-patterns.md +0 -533
  140. package/agents/compose.md +0 -240
  141. package/agents/images.md +0 -66
@@ -24,9 +24,7 @@ import { Tabs, Tab } from '@teamblind-chorus/ui';
24
24
  </Tabs>
25
25
  ```
26
26
 
27
- ## Use cases
28
-
29
- ### With icon
27
+ ## Leading icon
30
28
 
31
29
  Canonical sort/filter row — each tab pairs a leading glyph (`sys.icon.md`, 16px) with its label. All glyphs draw from `@teamblind-chorus/ui/icons` so the row carries no inline SVG.
32
30
 
@@ -44,7 +42,7 @@ import { PulseIcon, StarIcon, HeartIcon, BookmarkIcon } from '@teamblind-chorus/
44
42
  </Tabs>
45
43
  ```
46
44
 
47
- ### Icon only
45
+ ## Icon only
48
46
 
49
47
  Glyph-only tab — collapses to a clean 32×32 square (inline padding 12 → 8). Requires `aria-label`.
50
48
 
@@ -61,7 +59,7 @@ import { StarIcon, BookmarkIcon, HeartIcon } from '@teamblind-chorus/ui/icons';
61
59
  </Tabs>
62
60
  ```
63
61
 
64
- ### Overflow
62
+ ## Overflow
65
63
 
66
64
  When natural width exceeds the column, the row scrolls horizontally. Trailing edge fade (48px / `ref.space.600`) paints via `mask-image` only while overflow is present.
67
65
 
@@ -82,7 +80,7 @@ import { PulseIcon, StarIcon, HeartIcon, BookmarkIcon, TagIcon, ProfileIcon, Men
82
80
  </Tabs>
83
81
  ```
84
82
 
85
- ### Focus indicator
83
+ ## Focused tab
86
84
 
87
85
  Static specimen — pins the focus ring to the selected tab. See top-level [Focus indicator](#focus-indicator).
88
86
 
@@ -111,8 +109,8 @@ Selected/unselected pairs are inherited verbatim from [Filter chip's variants](.
111
109
 
112
110
  | Prop / state | Container | Label color | Border (always 1px `sys.borderWidth.hairline`) |
113
111
  |------------------------|------------------------------------|-----------------------------------|-------------------------------------------------------------------------|
114
- | **Tab — unselected** | `transparent` | `sys.color.onSurface` | `sys.color.outlineVariant` |
115
- | **Tab — selected** | `sys.color.inverseSurface` | `sys.color.inverseOnSurface` | `transparent` — 1px width held so footprint never changes between states |
112
+ | **Tab — unselected** | `transparent` | `sys.color.text.default` | `sys.color.border.default` |
113
+ | **Tab — selected** | `sys.color.background.inverse` | `sys.color.text.inverse` | `transparent` — 1px width held so footprint never changes between states |
116
114
 
117
115
  ## Sizes
118
116
 
@@ -77,16 +77,16 @@
77
77
  "selectionStates": {
78
78
  "unselected": {
79
79
  "background": "transparent",
80
- "label": "sys.color.onSurface",
80
+ "label": "sys.color.text.default",
81
81
  "border": {
82
82
  "width": "sys.borderWidth.hairline",
83
- "color": "sys.color.outlineVariant"
83
+ "color": "sys.color.border.default"
84
84
  },
85
85
  "note": "Transparent fill so the tab adopts whatever surface sits behind it — page, raised card, sheet — without pinning to a fixed neutral step. Inherited from Filter chip's unselected recipe."
86
86
  },
87
87
  "selected": {
88
- "background": "sys.color.inverseSurface",
89
- "label": "sys.color.inverseOnSurface",
88
+ "background": "sys.color.background.inverse",
89
+ "label": "sys.color.text.inverse",
90
90
  "border": null
91
91
  }
92
92
  },
@@ -116,20 +116,26 @@
116
116
  "layer": "::after/::before overlay — position:absolute, inset:0, inset box-shadow, no reflow (DESIGN.md Focus ring composition)",
117
117
  "innerCounterRing": {
118
118
  "width": "sys.borderWidth.hairline",
119
- "color": "sys.color.focusInset"
119
+ "color": "sys.color.border.focused"
120
120
  },
121
121
  "outerRing": {
122
122
  "width": "sys.borderWidth.thin",
123
- "color": "sys.color.focus"
123
+ "color": "sys.color.border.focused"
124
124
  }
125
125
  },
126
126
  "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 tab is in; never via plain mouse click."
127
127
  },
128
128
  "disabled": {
129
129
  "overlay": null,
130
- "containerOpacity": "sys.state.disabled",
130
+ "background": "sys.color.background.disabled",
131
+ "label": "sys.color.text.disabled",
132
+ "border": {
133
+ "width": "sys.borderWidth.hairline",
134
+ "color": "sys.color.border.bold"
135
+ },
131
136
  "suppressFocusRing": true,
132
- "cursor": "not-allowed"
137
+ "cursor": "not-allowed",
138
+ "note": "Explicit disabled (no opacity): neutral disabled fill + bold border so the shape reads on any surface, plus disabled label. Overrides the rest/selected appearance."
133
139
  }
134
140
  },
135
141
  "focusIndicator": {
@@ -141,12 +147,8 @@
141
147
  "opacity": "sys.state.focus"
142
148
  },
143
149
  "ring": {
144
- "outerWidth": "sys.borderWidth.thin",
145
- "outerColor": "sys.color.focus",
146
- "outerLayerPosition": "depth 0..2px from the tab edge (the outer stroke)",
147
- "insetWidth": "sys.borderWidth.hairline",
148
- "insetColor": "sys.color.focusInset",
149
- "insetLayerPosition": "depth 2..3px from the tab edge (the counter-ring just inside the outer stroke)",
150
+ "width": "sys.borderWidth.hairline",
151
+ "color": "sys.color.border.focused",
150
152
  "implementation": "inset box-shadow on the tab's `::after` overlay. Constrained strictly inside the tab's footprint and never exceeds it."
151
153
  },
152
154
  "trigger": ":focus-visible (keyboard / programmatic focus, never plain mouse click)"
@@ -154,6 +156,6 @@
154
156
  "forbidden": [
155
157
  "rounded tabs given raw <button>/<a> or bare-text children instead of <Tab value=…> elements — the chip chrome (chorus-chip--filter + chorus-chip--rounded), the selected state, and the aria-selected/data-value binding all live on <Tab>; raw children render unstyled with no selected state",
156
158
  "rounded tab radius set to sys.radius.full (pill) — the rounded variant is the sys.radius.md (8) soft-rectangle; reach for Segmented when a capsule row is needed",
157
- "active state painted with sys.color.brand — rounded tabs use sys.color.inverseSurface fill + sys.color.inverseOnSurface label in selected state"
159
+ "active state painted with sys.color.text.brand — rounded tabs use sys.color.background.inverse fill + sys.color.text.inverse label in selected state"
158
160
  ]
159
161
  }
@@ -23,9 +23,7 @@ import { Tabs, Tab } from '@teamblind-chorus/ui';
23
23
  </Tabs>
24
24
  ```
25
25
 
26
- ## Use cases
27
-
28
- ### With icon
26
+ ## Leading icon
29
27
 
30
28
  Leading glyph in each segment — useful when the verb alone could read as anything.
31
29
 
@@ -41,7 +39,7 @@ import { CheckedIcon, PlusIcon } from '@teamblind-chorus/ui/icons';
41
39
  </Tabs>
42
40
  ```
43
41
 
44
- ### Overflow
42
+ ## Overflow
45
43
 
46
44
  When natural width exceeds the column, the row scrolls horizontally — no `fullWidth` (equal-width segments would break the shared-density contract with Filter chips). Trailing **Edge fade** (rightmost **48px** / `ref.space.600`) paints via `mask-image` only while overflow is present.
47
45
 
@@ -61,7 +59,7 @@ import { Tabs, Tab } from '@teamblind-chorus/ui';
61
59
  </Tabs>
62
60
  ```
63
61
 
64
- ### Focus indicator
62
+ ## Focused segment
65
63
 
66
64
  Static specimen — pins the focus ring to a selected segment. See top-level [Focus indicator](#focus-indicator).
67
65
 
@@ -83,7 +81,7 @@ import { Tabs, Tab } from '@teamblind-chorus/ui';
83
81
 
84
82
  ## Anatomy
85
83
 
86
- Each segment renders with `chorus-chip chorus-chip--filter` — see [Filter chip](../chip/filter.md). Selected swaps from unselected (`transparent` fill + `outlineVariant` border + `onSurface` label) to selected (`inverseSurface` fill + `inverseOnSurface` label, border `transparent` with 1px width held).
84
+ Each segment renders with `chorus-chip chorus-chip--filter` — see [Filter chip](../chip/filter.md). Selected swaps from unselected (`transparent` fill + `border.default` border + `onSurface` label) to selected (`inverseSurface` fill + `inverseOnSurface` label, border `transparent` with 1px width held).
87
85
 
88
86
  Chip behaviour inherited verbatim — except the focus ring, re-anchored as an inset overlay on a `::after` layer (segmented row is a horizontal scroller; the chip's default outward ring would clip).
89
87
 
@@ -67,7 +67,7 @@
67
67
  },
68
68
  "selectionStates": {
69
69
  "$ref": "../chip/filter.spec.json#/selectionStates",
70
- "note": "Each segment swaps between Filter chip's unselected (surfaceContainerHigh + outlineVariant border) and selected (inverseSurface + inverseOnSurface) recipes."
70
+ "note": "Each segment swaps between Filter chip's unselected (surfaceContainerHigh + border.default border) and selected (inverseSurface + inverseOnSurface) recipes."
71
71
  },
72
72
  "states": {
73
73
  "$ref": "../chip/filter.spec.json#/states",
@@ -82,19 +82,15 @@
82
82
  "opacity": "sys.state.focus"
83
83
  },
84
84
  "ring": {
85
- "outerWidth": "sys.borderWidth.thin",
86
- "outerColor": "sys.color.focus",
87
- "outerLayerPosition": "depth 0..2px from the segment edge (the outer stroke)",
88
- "insetWidth": "sys.borderWidth.hairline",
89
- "insetColor": "sys.color.focusInset",
90
- "insetLayerPosition": "depth 2..3px from the segment edge (the counter-ring just inside the outer stroke)",
85
+ "width": "sys.borderWidth.hairline",
86
+ "color": "sys.color.border.focused",
91
87
  "implementation": "inset box-shadow on the segment's `::after` overlay. Constrained strictly inside the segment's footprint and never exceeds it."
92
88
  },
93
89
  "trigger": ":focus-visible (keyboard / programmatic focus, never plain mouse click)"
94
90
  },
95
91
  "forbidden": [
96
92
  "segmented control given raw <button>/<a> or bare-text children instead of <Tab value=…> elements — the chip chrome, the selected pill state, and the aria-selected/data-value binding all live on <Tab>; raw children render unstyled with no selected state",
97
- "active item painted with sys.color.primary fill — segmented active uses sys.color.surface fill on the selected pill (the rest of the row is surfaceContainer)",
93
+ "active item painted with sys.color.background.primary fill — segmented active uses sys.color.surface.default fill on the selected pill (the rest of the row is surfaceContainer)",
98
94
  "segmented row reflowing on selection — anatomy is no-layout"
99
95
  ]
100
96
  }
@@ -24,9 +24,7 @@ import { Tabs, Tab } from '@teamblind-chorus/ui';
24
24
  </Tabs>
25
25
  ```
26
26
 
27
- ## Use cases
28
-
29
- ### With icon
27
+ ## Leading icon
30
28
 
31
29
  A leading glyph before the label.
32
30
 
@@ -43,7 +41,7 @@ import { PlusIcon, CheckedIcon } from '@teamblind-chorus/ui/icons';
43
41
  </Tabs>
44
42
  ```
45
43
 
46
- ### Auto-fit
44
+ ## Auto-fit
47
45
 
48
46
  Wider terminal layout of Adaptive width — tabs share row width equally; indicator widens to match.
49
47
 
@@ -59,7 +57,7 @@ import { Tabs, Tab } from '@teamblind-chorus/ui';
59
57
  </Tabs>
60
58
  ```
61
59
 
62
- ### Overflow
60
+ ## Overflow
63
61
 
64
62
  Narrower terminal layout — tabs hold content width and the row scrolls. The trailing 48px (`ref.space.600`) paints as a transparent `mask-image` edge fade; clears when scrolled to the last tab.
65
63
 
@@ -79,7 +77,7 @@ import { Tabs, Tab } from '@teamblind-chorus/ui';
79
77
  </Tabs>
80
78
  ```
81
79
 
82
- ### Focus indicator
80
+ ## Focused tab
83
81
 
84
82
  Static specimen — pins the keyboard-focus ring to the selected tab. See top-level [Focus indicator](#focus-indicator).
85
83
 
@@ -104,9 +102,9 @@ import { Tabs, Tab } from '@teamblind-chorus/ui';
104
102
 
105
103
  | Prop / state | Container | Label color | Indicator |
106
104
  |------------------------|--------------------|---------------------------------------|-----------------------------------|
107
- | **Tab — unselected** | transparent | `sys.color.outline` (muted foreground) | none |
108
- | **Tab — selected** | transparent | `sys.color.onSurface` (strong foreground) | 2px `sys.borderWidth.thin` × `sys.color.onSurface` along the bottom edge |
109
- | **Container row** | transparent, 16px inline padding, 1px `sys.color.outlineVariant` bottom divider running the full row width. Selected indicator paints over this divider. Edge fade (rightmost 48px / `ref.space.600`) paints via `mask-image` only while overflow is present. | — | — |
105
+ | **Tab — unselected** | transparent | `sys.color.border.boldest` (muted foreground) | none |
106
+ | **Tab — selected** | transparent | `sys.color.text.default` (strong foreground) | 2px `sys.borderWidth.thin` × `sys.color.text.default` along the bottom edge |
107
+ | **Container row** | transparent, 16px inline padding, 1px `sys.color.border.default` bottom divider running the full row width. Selected indicator paints over this divider. Edge fade (rightmost 48px / `ref.space.600`) paints via `mask-image` only while overflow is present. | — | — |
110
108
 
111
109
  ## Sizes
112
110
 
@@ -120,7 +118,7 @@ A single fixed rung — the 40px footprint stays constant across breakpoints.
120
118
  | Inter-tab gap | 0 | — † |
121
119
  | Slot gap (icon ↔ label) | 4px | `sys.layout.inline.sm` |
122
120
  | Indicator height | 2px | `sys.borderWidth.thin` |
123
- | Container bottom divider | 1px | `sys.borderWidth.hairline` × `sys.color.outlineVariant` |
121
+ | Container bottom divider | 1px | `sys.borderWidth.hairline` × `sys.color.border.default` |
124
122
  | Label | 14 / Semibold | `sys.typo.label.md` |
125
123
  | Icon | 16px | `sys.icon.md` |
126
124
  | Edge fade width | 48px | `ref.space.600` — trailing `mask-image`, on overflow only |
@@ -136,7 +134,7 @@ A single fixed rung — the 40px footprint stays constant across breakpoints.
136
134
  | `default` | — | Container + label at rest. |
137
135
  | `hovered` | `sys.state.hover` (8%) | Pointer-driven via `:hover`. |
138
136
  | `pressed` | `sys.state.pressed` (16%) | Pointer-driven via `:active`. |
139
- | `selected` | — | Label flips to `sys.color.onSurface`; 2px indicator slides to the new tab. |
137
+ | `selected` | — | Label flips to `sys.color.text.default`; 2px indicator slides to the new tab. |
140
138
  | `disabled` | overlay suppressed | Label at `sys.state.disabled` (40%) opacity, focus ring suppressed, `cursor: not-allowed`. |
141
139
 
142
140
  ## Focus indicator
@@ -3,7 +3,7 @@
3
3
  "name": "Tabs",
4
4
  "family": "tabs",
5
5
  "subcomponent": "underline",
6
- "description": "Horizontal tab row with a single 2px (`sys.borderWidth.thin`) `sys.color.onSurface` indicator that slides between the active tab's bottom edge on selection. Default content-section switcher.",
6
+ "description": "Horizontal tab row with a single 2px (`sys.borderWidth.thin`) `sys.color.border.selected` indicator that slides between the active tab's bottom edge on selection. Default content-section switcher.",
7
7
  "element": "div",
8
8
  "props": {
9
9
  "variant": {
@@ -72,19 +72,19 @@
72
72
  "slotGap": "sys.layout.inline.sm",
73
73
  "indicatorHeight": "sys.borderWidth.thin",
74
74
  "dividerWidth": "sys.borderWidth.hairline",
75
- "dividerColor": "sys.color.outlineVariant",
75
+ "dividerColor": "sys.color.border.default",
76
76
  "labelTypo": "sys.typo.label.md",
77
77
  "iconSize": "sys.icon.md",
78
78
  "fadeWidth": "ref.space.600"
79
79
  },
80
80
  "selectionStates": {
81
81
  "unselected": {
82
- "label": "sys.color.outline",
82
+ "label": "sys.color.text.subtle",
83
83
  "indicator": null
84
84
  },
85
85
  "selected": {
86
- "label": "sys.color.onSurface",
87
- "indicator": "sys.color.onSurface"
86
+ "label": "sys.color.text.default",
87
+ "indicator": "sys.color.border.selected"
88
88
  }
89
89
  },
90
90
  "states": {
@@ -113,20 +113,22 @@
113
113
  "layer": "::after/::before overlay — position:absolute, inset:0, inset box-shadow, no reflow (DESIGN.md Focus ring composition)",
114
114
  "innerCounterRing": {
115
115
  "width": "sys.borderWidth.hairline",
116
- "color": "sys.color.focusInset"
116
+ "color": "sys.color.border.focused"
117
117
  },
118
118
  "outerRing": {
119
119
  "width": "sys.borderWidth.thin",
120
- "color": "sys.color.focus"
120
+ "color": "sys.color.border.focused"
121
121
  }
122
122
  },
123
123
  "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 tab is in; never via plain mouse click."
124
124
  },
125
125
  "disabled": {
126
126
  "overlay": null,
127
- "containerOpacity": "sys.state.disabled",
127
+ "label": "sys.color.text.disabled",
128
+ "indicator": null,
128
129
  "suppressFocusRing": true,
129
- "cursor": "not-allowed"
130
+ "cursor": "not-allowed",
131
+ "note": "Explicit disabled (no opacity): label drops to text.disabled, no indicator. Sits on a neutral surface so the disabled text token reads directly."
130
132
  }
131
133
  },
132
134
  "focusIndicator": {
@@ -138,19 +140,15 @@
138
140
  "opacity": "sys.state.focus"
139
141
  },
140
142
  "ring": {
141
- "outerWidth": "sys.borderWidth.thin",
142
- "outerColor": "sys.color.focus",
143
- "outerLayerPosition": "depth 0..2px from the tab edge (the outer stroke)",
144
- "insetWidth": "sys.borderWidth.hairline",
145
- "insetColor": "sys.color.focusInset",
146
- "insetLayerPosition": "depth 2..3px from the tab edge (the counter-ring just inside the outer stroke)",
143
+ "width": "sys.borderWidth.hairline",
144
+ "color": "sys.color.border.focused",
147
145
  "implementation": "inset box-shadow on the tab's `::after` overlay (sits above the state-overlay `::before`, the label/icon, and the underline indicator). Constrained strictly inside the tab's footprint and never exceeds it."
148
146
  },
149
147
  "trigger": ":focus-visible (keyboard / programmatic focus, never plain mouse click)"
150
148
  },
151
149
  "forbidden": [
152
150
  "underline tabs given raw <button>/<a> or bare-text children instead of <Tab value=…> elements — the sliding indicator (.chorus-tabs__indicator), the useAdaptiveFit measurement that sets data-fit, the aria-selected/data-value binding, and the active label styling all live on <Tab>; raw children render as unstyled run-together text with no indicator and a broken scroll/stretch layout",
153
- "active indicator painted with sys.color.brand — underline uses sys.color.onSurface for the indicator",
151
+ "active indicator painted with sys.color.text.brand — underline uses sys.color.border.selected for the indicator",
154
152
  "tabs wrapped in extra horizontal padding — tabs is full-bleed by family declaration",
155
153
  "tab label sizing below sys.typo.label.md (14px) — smaller text breaks Korean / CJK hierarchy",
156
154
  "manual underline drawn via `border-bottom:` — indicator is the `::after` overlay, not a real border"
@@ -20,9 +20,7 @@ import { Thumbnail } from '@teamblind-chorus/ui';
20
20
  <Thumbnail size={48} alt="Channel" src="/placeholder.png" />
21
21
  ```
22
22
 
23
- ## Use cases
24
-
25
- ### With update dot
23
+ ## Update dot
26
24
 
27
25
  A `brand`-tone dot at the top-right flags new activity. Decorative; the row carries the count in a sibling text slot.
28
26
 
@@ -34,7 +32,7 @@ import { Thumbnail } from '@teamblind-chorus/ui';
34
32
  <Thumbnail size={48} alt="Channel" src="/placeholder.png" updateDot />
35
33
  ```
36
34
 
37
- ### With logo badge
35
+ ## Logo badge
38
36
 
39
37
  A 16 × 16 sub-brand mark at the bottom-right on its own surface halo.
40
38
 
@@ -51,7 +49,7 @@ import { Thumbnail } from '@teamblind-chorus/ui';
51
49
  />
52
50
  ```
53
51
 
54
- ### With both badges
52
+ ## Both badges
55
53
 
56
54
  Top-right and bottom-right corners are independent and never collide.
57
55
 
@@ -69,7 +67,7 @@ import { Thumbnail } from '@teamblind-chorus/ui';
69
67
  />
70
68
  ```
71
69
 
72
- ### With surface outline
70
+ ## Surface outline
73
71
 
74
72
  `outlined` paints a 2-token (`sys.borderWidth.thin`) `sys.color.surface` halo as an outset `box-shadow` around the container. The ring blends into the host's `surface*` tier and separates the circle's edge from anything visually noisy underneath. Painted as a shadow, not a `border:` — the rung's diameter never reflows.
75
73
 
@@ -87,7 +85,7 @@ import { Thumbnail } from '@teamblind-chorus/ui';
87
85
 
88
86
  The two corner badges (`updateDot`, `logoBadge`) carry their own 1-token surface halos and compose cleanly over the outlined ring — order is image → outlined ring → badge halos → badge fills, all painted as `box-shadow` so footprint never changes.
89
87
 
90
- ### Size ladder
88
+ ## Size ladder
91
89
 
92
90
  The full ladder side-by-side; update-dot steps down at the 32-rung boundary.
93
91
 
@@ -43,7 +43,7 @@
43
43
  "outlined": {
44
44
  "type": "boolean",
45
45
  "default": false,
46
- "description": "When `true`, paints a 2-token-wide `sys.color.surface` outline around the Thumbnail container as an outset halo. The outline reads as an isolation ring that separates the Thumbnail's circular edge from anything visually noisy underneath it. Painted as `box-shadow: 0 0 0 sys.borderWidth.thin sys.color.surface` so it never reflows the slot's intrinsic diameter — same no-layout-stroke idiom the rest of the system uses. Pair with hosts whose chrome reads as a `surface*` tier (so the halo blends in) and whose backdrop differs from that tier (so the halo actually separates).",
46
+ "description": "When `true`, paints a 2-token-wide `sys.color.surface.default` outline around the Thumbnail container as an outset halo. The outline reads as an isolation ring that separates the Thumbnail's circular edge from anything visually noisy underneath it. Painted as `box-shadow: 0 0 0 sys.borderWidth.thin sys.color.surface.default` so it never reflows the slot's intrinsic diameter — same no-layout-stroke idiom the rest of the system uses. Pair with hosts whose chrome reads as a `surface*` tier (so the halo blends in) and whose backdrop differs from that tier (so the halo actually separates).",
47
47
  "whenToReachForIt": [
48
48
  "The Thumbnail half-overlaps or sits over an image — [ProfileHeader](../profile-header/profile-header.md) avatar (56-rung on cover band), [Profile carousel](../carousel/profile.md) avatar (64-rung on card cover), any avatar pulled onto a Hero / Cover photo.",
49
49
  "The backdrop is a brand-tonal strip, a `*Container` fill, or a gradient band ([Banner](../banner/banner.md) inside a colour-tinted host, a Section painted with a `successContainer` / `errorContainer` / `brandContainer` fill).",
@@ -90,24 +90,24 @@
90
90
  "containerRadius": "sys.radius.full",
91
91
  "containerOutline": {
92
92
  "appliesWhen": "props.outlined === true",
93
- "color": "sys.color.surface",
93
+ "color": "sys.color.surface.default",
94
94
  "width": "sys.borderWidth.thin",
95
95
  "rendering": "box-shadow (outset, 0 0 0 width color) so the halo never reflows the slot's intrinsic diameter — same no-layout-stroke contract every other Chorus surface follows. Pairs the halo with the host's `surface*` tier so the ring blends into the chrome around it while separating the Thumbnail from a contrasting backdrop (cover image, brand tonal strip, gradient band)."
96
96
  },
97
- "imageFallbackFill": "sys.color.surfaceContainerHigh",
97
+ "imageFallbackFill": "sys.color.surface.sunken",
98
98
  "imageFallbackImage": "/placeholder.png",
99
99
  "imageFallbackImageRendering": "background-image, cover, center — paints under the runtime <img>; visible only when the inline image is missing or fails to load, so the slot still resolves to an image rather than an empty surface tone.",
100
- "imageFallbackGlyphColor": "sys.color.onSurfaceVariant",
101
- "updateDotFill": "sys.color.brand",
100
+ "imageFallbackGlyphColor": "sys.color.text.subtle",
101
+ "updateDotFill": "sys.color.text.brand",
102
102
  "updateDotRadius": "sys.radius.full",
103
103
  "updateDotHalo": {
104
- "color": "sys.color.surface",
104
+ "color": "sys.color.surface.default",
105
105
  "width": "sys.borderWidth.hairline",
106
106
  "rendering": "box-shadow"
107
107
  },
108
108
  "logoBadgeRadius": "sys.radius.full",
109
109
  "logoBadgeHalo": {
110
- "color": "sys.color.surface",
110
+ "color": "sys.color.surface.default",
111
111
  "width": "sys.borderWidth.hairline",
112
112
  "rendering": "box-shadow"
113
113
  }
@@ -155,7 +155,7 @@
155
155
  "behavior": {
156
156
  "slotOmissionCollapses": "Both badges (updateDot, logoBadge) drop out of the layout entirely when absent — no reserved corner whitespace.",
157
157
  "badgesOverlay": "Both badges are absolutely positioned over the image; the container's overall footprint is the image's diameter regardless of whether the badges are present. The 1px surface halo is rendered as a box-shadow.",
158
- "outlinedHalo": "When `outlined={true}`, the container paints a 2-token (`sys.borderWidth.thin`) `sys.color.surface` halo via `box-shadow` — outset of the diameter, never reflowing the slot. The corner badges' own 1-token halos are layered above it. Composes cleanly with `updateDot` and `logoBadge`.",
158
+ "outlinedHalo": "When `outlined={true}`, the container paints a 2-token (`sys.borderWidth.thin`) `sys.color.surface.default` halo via `box-shadow` — outset of the diameter, never reflowing the slot. The corner badges' own 1-token halos are layered above it. Composes cleanly with `updateDot` and `logoBadge`.",
159
159
  "imageClip": "Container's radius.full plus overflow: hidden clips the image to a perfect circle even when the source is rectangular.",
160
160
  "noTextFallback": "Slot only renders <img> content — there is no text fallback. When `src` is omitted or the server fails to deliver the image, the slot's background paints the bundled `/placeholder.png` over a `surfaceContainerHigh` base. The placeholder is the image-area's runtime safety net for load failures; design-time scaffolds should pass the same URL through `src` explicitly so the contract is visible in the composition rather than hidden in the CSS layer.",
161
161
  "updateDotBreakpoint": "Dot drops from 8×8 (at and above rung 32, including 56) to 4×4 (rungs 24 / 20 / 16) so it reads as highlight, not occluder, on the smaller diameters.",
@@ -22,9 +22,7 @@ import { Toast } from '@teamblind-chorus/ui';
22
22
  <Toast>Token copied to clipboard</Toast>
23
23
  ```
24
24
 
25
- ## Use cases
26
-
27
- ### With action
25
+ ## Action
28
26
 
29
27
  A small Text Button (`appearance="inverse"`) on the trailing edge for follow-through (Undo, Retry, View). With the trailing slot present, the toast stays on screen longer (~6s). The Button node is passed directly so the call site spells out the sub-component.
30
28
 
@@ -42,7 +40,7 @@ import { Toast, Button } from '@teamblind-chorus/ui';
42
40
  </Toast>
43
41
  ```
44
42
 
45
- ### With dismiss
43
+ ## Dismiss
46
44
 
47
45
  A medium Icon Button (`appearance="inverse"`) on the trailing edge for explicit dismissal — used when the toast carries information the user may want to read at their own pace. Composed at the call site so the `appearance="inverse"` binding stays visible.
48
46
 
@@ -66,7 +64,7 @@ import { XIcon } from '@teamblind-chorus/ui/icons';
66
64
  </Toast>
67
65
  ```
68
66
 
69
- ### Max width
67
+ ## Max width
70
68
 
71
69
  Strip grows until it hits the 400 cap (or viewport-minus-safe-area on narrow screens). Past that, the body wraps onto a second line rather than letting the strip stretch into a banner.
72
70
 
@@ -78,7 +76,7 @@ import { Toast } from '@teamblind-chorus/ui';
78
76
  <Toast>Saved your draft to every workspace you joined this month</Toast>
79
77
  ```
80
78
 
81
- ### Truncation
79
+ ## Truncation
82
80
 
83
81
  Body wraps up to two lines and truncates with an ellipsis past that — body, trailing button, and any leading glyph stay vertically centred. Pair with a trailing dismiss when the message is status the user may want to read at their own pace.
84
82
 
@@ -108,7 +106,7 @@ A single appearance — inverse. The inverse pair (`inverseSurface` / `inverseOn
108
106
 
109
107
  | Appearance | Container fill | Foreground | When to use |
110
108
  |------------|-----------------------------|----------------------------------|-------------|
111
- | `default` | `sys.color.inverseSurface` | `sys.color.inverseOnSurface` | Every toast. Status messages that must read against any surface tier in the stack. |
109
+ | `default` | `sys.color.background.inverse` | `sys.color.text.inverse` | Every toast. Status messages that must read against any surface tier in the stack. |
112
110
 
113
111
  ## Slots
114
112
 
@@ -66,8 +66,8 @@
66
66
  },
67
67
  "appearances": {
68
68
  "default": {
69
- "background": "sys.color.inverseSurface",
70
- "foreground": "sys.color.inverseOnSurface",
69
+ "background": "sys.color.background.inverse",
70
+ "foreground": "sys.color.text.inverse",
71
71
  "note": "The inverse pair is the only appearance — toasts always read as a contrasting strip against the page. Action buttons inside an inverse surface fall back to the regular primary family per the inverse-cluster contract."
72
72
  }
73
73
  },
@@ -76,7 +76,7 @@
76
76
  "role": "Container carries role='status' and aria-live='polite' so screen readers announce the confirmation without interrupting the user's current focus."
77
77
  },
78
78
  "forbidden": [
79
- "Toast painted with sys.color.surface — toast uses sys.color.inverseSurface fill + inverseOnSurface text",
79
+ "Toast painted with sys.color.surface.default — toast uses sys.color.background.inverse fill + inverseOnSurface text",
80
80
  "Toast as a blocking commit prompt — destructive prompts use Dialog or BottomSheet; toast is for non-blocking feedback",
81
81
  "Toast persistent (no auto-dismiss)",
82
82
  "More than one toast stacked — toast queue is single-at-a-time",
@@ -18,9 +18,7 @@ import { Tooltip } from '@teamblind-chorus/ui';
18
18
  <Tooltip placement="top">Tooltip text</Tooltip>
19
19
  ```
20
20
 
21
- ## Use cases
22
-
23
- ### Inverse
21
+ ## Inverse
24
22
 
25
23
  The dark-cluster bubble. Reach for it when the host screen is already saturated with `primary` tone — the inverse cluster (`inverseSurface` / `inverseOnSurface`) reads as a distinct floating note above the page.
26
24
 
@@ -32,7 +30,7 @@ import { Tooltip } from '@teamblind-chorus/ui';
32
30
  <Tooltip placement="top" appearance="inverse">Tooltip text</Tooltip>
33
31
  ```
34
32
 
35
- ### With action
33
+ ## Action
36
34
 
37
35
  A small Text Button for follow-through ("Learn more", "Got it"). Bind the button's `appearance` to match the tooltip — `onPrimary` for the default (brand-blue) tooltip; `inverse` for the inverse tooltip.
38
36
 
@@ -50,7 +48,7 @@ import { Tooltip, Button } from '@teamblind-chorus/ui';
50
48
  </Tooltip>
51
49
  ```
52
50
 
53
- ### Multi-line with action
51
+ ## Multi-line action
54
52
 
55
53
  When the body grows past the 300 cap, the body wraps onto a second line and the action slot drops below the body. The body-to-action gap goes from 12 (inline) to 6 (block) so the stacked action reads as part of the same group.
56
54
 
@@ -68,7 +66,7 @@ import { Tooltip, Button } from '@teamblind-chorus/ui';
68
66
  </Tooltip>
69
67
  ```
70
68
 
71
- ### Placements
69
+ ## Placements
72
70
 
73
71
  Six placements, named `<edge>` or `<edge>-<align>`. The `<edge>` axis (top / bottom) places the bubble above or below the trigger and chooses the caret edge; the `<align>` axis (start / center / end) shifts the caret along the parallel axis.
74
72
 
@@ -93,8 +91,8 @@ Two appearances — `default` (the canonical brand-blue tooltip) and `inverse` (
93
91
 
94
92
  | Appearance | Container fill | Foreground | Pair the action button with | When to use |
95
93
  |------------|-----------------------------|----------------------------------|---------------------------------------------------------------|-------------|
96
- | `default` | `sys.color.primary` | `sys.color.onPrimary` | `<Button variant="text" appearance="onPrimary">` | The canonical tooltip — brand-blue bubble, always-white label. |
97
- | `inverse` | `sys.color.inverseSurface` | `sys.color.inverseOnSurface` | `<Button variant="text" appearance="inverse">` | When the host screen is saturated with `primary` tone — the dark inverse bubble reads as distinct floating chrome. |
94
+ | `default` | `sys.color.background.primary` | `sys.color.text.onFill` | `<Button variant="text" appearance="onPrimary">` | The canonical tooltip — brand-blue bubble, always-white label. |
95
+ | `inverse` | `sys.color.background.inverse` | `sys.color.text.inverse` | `<Button variant="text" appearance="inverse">` | When the host screen is saturated with `primary` tone — the dark inverse bubble reads as distinct floating chrome. |
98
96
 
99
97
  `default` is the right reach in most cases; switch to `inverse` only when the surrounding surface gives the brand-blue bubble no breathing room.
100
98
 
@@ -80,14 +80,14 @@
80
80
  },
81
81
  "appearances": {
82
82
  "default": {
83
- "background": "sys.color.primary",
84
- "foreground": "sys.color.onPrimary",
83
+ "background": "sys.color.background.primary",
84
+ "foreground": "sys.color.text.onFill",
85
85
  "elevation": "sys.elevation.overlay",
86
86
  "note": "Brand-blue bubble with an always-white label. Both `primary` and `onPrimary` are theme-stable, so the bubble reads the same in light and dark mode. Action buttons inside this appearance MUST use `Button variant=\"text\" appearance=\"onPrimary\"` so the label stays white in either theme — `appearance=\"inverse\"` flips with the theme and would render the label dark in dark mode."
87
87
  },
88
88
  "inverse": {
89
- "background": "sys.color.inverseSurface",
90
- "foreground": "sys.color.inverseOnSurface",
89
+ "background": "sys.color.background.inverse",
90
+ "foreground": "sys.color.text.inverse",
91
91
  "elevation": "sys.elevation.overlay",
92
92
  "note": "Inverse-cluster bubble for screens already saturated with primary tone, where the `default` brand-blue tooltip would compete with the surrounding chrome. Action buttons inside this appearance use `Button variant=\"text\" appearance=\"inverse\"` so the label flips with the host fill."
93
93
  }