@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
@@ -85,7 +85,7 @@
85
85
  },
86
86
  "item": {
87
87
  "required": true,
88
- "description": "Single expandable row. Hairline `outlineVariant` divider inset 16px (`layout.container.md`) on BOTH the leading and trailing edges, painted as an absolutely-positioned `::after` overlay on every row except the last — matching the family-wide List row divider rule. `data-state='open' | 'closed'` reflects the current open-state.",
88
+ "description": "Single expandable row. Hairline `border.default` divider inset 16px (`layout.container.md`) on BOTH the leading and trailing edges, painted as an absolutely-positioned `::after` overlay on every row except the last — matching the family-wide List row divider rule. `data-state='open' | 'closed'` reflects the current open-state.",
89
89
  "intrinsic": true
90
90
  },
91
91
  "trigger": {
@@ -105,12 +105,12 @@
105
105
  },
106
106
  "content": {
107
107
  "required": true,
108
- "description": "Body region. Paints below the trigger when open; uses `hidden` attribute when closed.\n\nTwo padding modes by content kind:\n\n- **Prose body** (text, icon, button, form-field): inline padding is `32px leading / 16px trailing` — one extra `layout.container.md` of indent on the leading edge so the prose reads as nested INSIDE the trigger's label column. `min-height: 40px` keeps short single-line bodies on a touch-target rhythm.\n- **Embedded row group** (`<List embedded>` or another `<Accordion embedded>`): inline padding is `16px leading / 0 trailing` — the leading indent is paid once by the body (sub-list rows align one container.md inside the trigger's label column) and the trailing rail is paid by the row's own inline padding, so the sub-list stretches flush to the accordion's right edge without a double-paid gutter. The body also paints a hairline `outlineVariant` divider at its TOP via a `::before` overlay (inset 16px on both inline edges, matching the inter-item divider rule) so the parent trigger and the child rows read as a parent ↔ child hierarchy.\n\nThe two modes are selected automatically via `:has([data-embedded='true'])` — call sites do not pass a mode prop; dropping a `<List embedded>` (or nested `<Accordion embedded>`) into the content body switches the body to compact-host geometry.",
108
+ "description": "Body region. Paints below the trigger when open; uses `hidden` attribute when closed.\n\nTwo padding modes by content kind:\n\n- **Prose body** (text, icon, button, form-field): inline padding is `32px leading / 16px trailing` — one extra `layout.container.md` of indent on the leading edge so the prose reads as nested INSIDE the trigger's label column. `min-height: 40px` keeps short single-line bodies on a touch-target rhythm.\n- **Embedded row group** (`<List embedded>` or another `<Accordion embedded>`): inline padding is `16px leading / 0 trailing` — the leading indent is paid once by the body (sub-list rows align one container.md inside the trigger's label column) and the trailing rail is paid by the row's own inline padding, so the sub-list stretches flush to the accordion's right edge without a double-paid gutter. The body also paints a hairline `border.default` divider at its TOP via a `::before` overlay (inset 16px on both inline edges, matching the inter-item divider rule) so the parent trigger and the child rows read as a parent ↔ child hierarchy.\n\nThe two modes are selected automatically via `:has([data-embedded='true'])` — call sites do not pass a mode prop; dropping a `<List embedded>` (or nested `<Accordion embedded>`) into the content body switches the body to compact-host geometry.",
109
109
  "accepts": ["text", "icon", "button", "list", "form-field"]
110
110
  },
111
111
  "groupDivider": {
112
112
  "required": false,
113
- "description": "Hairline `outlineVariant` rule painted at the TOP edge of the open content body via a `::before` overlay when the body hosts a `<List embedded>` (or any same-kind row group). Inset 16px (`layout.container.md`) on both inline edges, matching the inter-item divider. Distinguishes the parent trigger from the child group so the hierarchy reads visually. Omitted for prose bodies.",
113
+ "description": "Hairline `border.default` rule painted at the TOP edge of the open content body via a `::before` overlay when the body hosts a `<List embedded>` (or any same-kind row group). Inset 16px (`layout.container.md`) on both inline edges, matching the inter-item divider. Distinguishes the parent trigger from the child group so the hierarchy reads visually. Omitted for prose bodies.",
114
114
  "intrinsic": true
115
115
  }
116
116
  },
@@ -119,9 +119,9 @@
119
119
  "triggerPaddingBlock": "sys.layout.container.xs",
120
120
  "triggerPaddingInline": "sys.layout.container.md",
121
121
  "triggerLabelTypo": "sys.typo.body.md",
122
- "triggerLabelColor": "sys.color.onSurface",
122
+ "triggerLabelColor": "sys.color.text.default",
123
123
  "chevronSize": "sys.icon.md",
124
- "chevronColor": "sys.color.onSurfaceVariant",
124
+ "chevronColor": "sys.color.text.subtle",
125
125
  "chevronGap": "sys.layout.inline.md",
126
126
  "chevronRotationDuration": "120ms",
127
127
  "chevronRotationTiming": "ease-out",
@@ -133,15 +133,15 @@
133
133
  "contentIndent": "sys.layout.container.md",
134
134
  "contentMinHeight": "ref.space.500",
135
135
  "contentBodyTypo": "sys.typo.body.sm",
136
- "contentBodyColor": "sys.color.onSurfaceVariant",
136
+ "contentBodyColor": "sys.color.text.subtle",
137
137
  "embeddedRowLabelTypo": "sys.typo.body.sm",
138
138
  "embeddedRowMinHeight": "ref.space.500",
139
139
  "embeddedRowDivider": "none",
140
140
  "dividerWidth": "sys.borderWidth.hairline",
141
- "dividerColor": "sys.color.outlineVariant",
141
+ "dividerColor": "sys.color.border.default",
142
142
  "dividerInsetInline": "sys.layout.container.md",
143
143
  "groupDividerWidth": "sys.borderWidth.hairline",
144
- "groupDividerColor": "sys.color.outlineVariant",
144
+ "groupDividerColor": "sys.color.border.default",
145
145
  "groupDividerInsetInline": "sys.layout.container.md"
146
146
  },
147
147
  "appearances": {
@@ -162,25 +162,25 @@
162
162
  "focusRing": {
163
163
  "composition": "inward",
164
164
  "layer": "::before overlay — position:absolute, inset:0, inset box-shadow, no reflow (DESIGN.md Focus ring composition)",
165
- "innerCounterRing": { "width": "sys.borderWidth.hairline", "color": "sys.color.focusInset" },
166
- "outerRing": { "width": "sys.borderWidth.thin", "color": "sys.color.focus" }
165
+ "innerCounterRing": { "width": "sys.borderWidth.hairline", "color": "sys.color.border.focused" },
166
+ "outerRing": { "width": "sys.borderWidth.thin", "color": "sys.color.border.focused" }
167
167
  },
168
- "note": "Keyboard-focus (:focus-visible) visual — a three-layer inward ring inside the trigger's footprint, with no state-overlay tint (the ring alone carries focus here). Mirrors the `focusIndicator` block for spec-only renderers. Composes over the lifecycle state the trigger is in."
168
+ "note": "Keyboard-focus (:focus-visible) visual — a single inward ring inside the trigger's footprint, with no state-overlay tint (the ring alone carries focus here). Mirrors the `focusIndicator` block for spec-only renderers. Composes over the lifecycle state the trigger is in."
169
169
  },
170
170
  "disabled": {
171
- "containerOpacity": "sys.state.disabled",
172
- "pointerEvents": "none"
171
+ "text": "sys.color.text.disabled",
172
+ "icon": "sys.color.icon.disabled",
173
+ "pointerEvents": "none",
174
+ "note": "Explicit disabled (no opacity): trigger text to text.disabled, chevron/icons to icon.disabled."
173
175
  }
174
176
  },
175
177
  "focusIndicator": {
176
- "description": "Keyboard-focus visual painted as an inward 3-layer ring inside the trigger's footprint. Composes over whichever lifecycle state the trigger is in.",
178
+ "description": "Keyboard-focus visual painted as an inward single ring inside the trigger's footprint. Composes over whichever lifecycle state the trigger is in.",
177
179
  "composition": "inward",
178
- "compositionReason": "Items tile flush with only a hairline `outlineVariant` divider between them; an outward ring would overlap the divider and the neighbouring row.",
180
+ "compositionReason": "Items tile flush with only a hairline `border.default` divider between them; an outward ring would overlap the divider and the neighbouring row.",
179
181
  "ring": {
180
- "outerWidth": "sys.borderWidth.thin",
181
- "outerColor": "sys.color.focus",
182
- "insetWidth": "sys.borderWidth.hairline",
183
- "insetColor": "sys.color.focusInset",
182
+ "width": "sys.borderWidth.hairline",
183
+ "color": "sys.color.border.focused",
184
184
  "implementation": "inset box-shadow on the trigger's `::before` overlay."
185
185
  },
186
186
  "trigger": ":focus-visible (keyboard / programmatic focus, never plain mouse click)"
@@ -203,7 +203,7 @@
203
203
  "content body painted with a chromatic fill (primaryContainer, etc.) — the body inherits the host surface tone; chromatic emphasis on an accordion body reads as a Banner aside, not a content section",
204
204
  "destructive commit fired inline inside the content body without a Button wrapper — destructive commits open a Dialog / BottomSheet, never fire from a raw content link",
205
205
  "child `<List>` dropped into the content body WITHOUT `embedded={true}` — the list keeps its own surface chrome + inline padding and double-pays the body's 32px leading indent, leaving child rows at a deeper inset than the trigger's label column. Use `embedded` so the list inherits the body's edges.",
206
- "child list group rendered WITHOUT the top `outlineVariant` divider — the parent trigger and the first child row tile flush, collapsing the parent ↔ child hierarchy into one stack. The top divider is the visual contract that the group is nested, not co-equal.",
206
+ "child list group rendered WITHOUT the top `border.default` divider — the parent trigger and the first child row tile flush, collapsing the parent ↔ child hierarchy into one stack. The top divider is the visual contract that the group is nested, not co-equal.",
207
207
  "top group divider replaced with a `border-top:` on the list container — `border` reflows the box and breaks the inward focus ring on the first list row. Paint via the body's `::before` overlay (no-layout stroke), never a layout border.",
208
208
  "trigger label sized below 16px or set to Semibold — Accordion is a List sub and inherits the family `label` spec (`16 / Regular / onSurface`); a 14/Semibold trigger reads as a different family from the rows it nests"
209
209
  ]
@@ -10,7 +10,7 @@ Directory-entry [List](./list.md) sub — an entity row pairing an optional lead
10
10
 
11
11
  ## Default
12
12
 
13
- Directory entry rowspick the Thumbnail rung via the **Size** dropdown (`xlarge` 56 → `small` 32). Row payload (label + stacked secondary + single-line description + trailing follow toggle) stays constant; only the leading avatar footprint changes. At `xlarge` the inter-row divider anchors to the text column (16 + 56 + 12 = 84) so the rule reads as separating identity columns under the wider avatar.
13
+ A single directory entry row — the atomic Entry component: leading Thumbnail + identity group (label + stacked `secondary` + single-line `description`) + a trailing follow toggle. Pick the Thumbnail rung via the **Size** dropdown (`xlarge` 56 `small` 32); the row payload stays constant, only the leading avatar footprint changes.
14
14
 
15
15
  ```preview
16
16
  list/entry
@@ -31,23 +31,11 @@ import { Button, List } from '@teamblind-chorus/ui';
31
31
  <Button variant="toggle" onClick={() => {}}>Follow</Button>
32
32
  ),
33
33
  },
34
- {
35
- value: 'indie-game-devs',
36
- label: 'Indie Game Devs',
37
- secondary: '8,210 Followers',
38
- description: 'Solo dev diaries, first-release postmortems, jam recaps.',
39
- thumbnail: { src: '/placeholder.png', alt: 'Indie Game Devs' },
40
- trailingIcon: (
41
- <Button variant="toggle" onClick={() => {}}>Follow</Button>
42
- ),
43
- },
44
34
  ]}
45
35
  />
46
36
  ```
47
37
 
48
- ## Use cases
49
-
50
- ### With trailing Text Button (compact attribution row)
38
+ ## Trailing Text Button
51
39
 
52
40
  Pairs a leading Thumbnail + label with a trailing [Text Button](../button/text.md) (`size="xsmall"`) and nothing else — the most compact *image + label + trailing-text-button* identity-plus-commit row. The button carries the **link-affordance follow / invite** shape: `appearance="accent"` while inactive (`Follow`) so the commit reads as the loudest call, flipping to `appearance="default"` once active (`Following`) so the followed state recedes. Distinct from the [Default](#default)'s `variant="toggle"` Follow chip — reach for the text button when the row reads as an attribution line (channel / author + follow) rather than a directory entry with a pill control. This is the exact combo the [Post carousel](../carousel/post.md) card pins to the top of each post as its attached attribution element.
53
41
 
@@ -68,21 +56,13 @@ import { Button, List } from '@teamblind-chorus/ui';
68
56
  <Button variant="text" size="xsmall" appearance="accent" onClick={() => {}}>Follow</Button>
69
57
  ),
70
58
  },
71
- {
72
- value: 'plant-people',
73
- label: 'Plant People',
74
- thumbnail: { src: '/placeholder.png', alt: 'Plant People' },
75
- trailingIcon: (
76
- <Button variant="text" size="xsmall" onClick={() => {}}>Following</Button>
77
- ),
78
- },
79
59
  ]}
80
60
  />
81
61
  ```
82
62
 
83
- ### With trailing star toggle
63
+ ## Trailing star toggle
84
64
 
85
- Uses the **single-shape fill-only contract**: always `<StarFillIcon>`, color flips by state (active = `var(--sys-color-icon-yellow)`, inactive = `var(--sys-color-icon-muted)`). Shape stays constant so the trailing rail keeps a stable hit-target footprint — never swap between outline (`StarIcon`) and fill (`StarFillIcon`) for the same affordance. Rows with a trailing affordance default to `size="small"` (32) — the smaller leading footprint keeps the trailing rail visually balanced.
65
+ Uses the **single-shape fill-only contract**: always `<StarFillIcon>`, color flips by state (active = `var(--sys-color-icon-accent-yellow-default)`, inactive = `var(--sys-color-icon-subtle)`). Shape stays constant so the trailing rail keeps a stable hit-target footprint — never swap between outline (`StarIcon`) and fill (`StarFillIcon`) for the same affordance. Rows with a trailing affordance default to `size="small"` (32) — the smaller leading footprint keeps the trailing rail visually balanced.
86
66
 
87
67
  ```preview
88
68
  list/entry-with-star
@@ -106,30 +86,7 @@ import { StarFillIcon } from '@teamblind-chorus/ui/icons';
106
86
  aria-label="Favorited"
107
87
  aria-pressed="true"
108
88
  icon={<StarFillIcon />}
109
- style={{ color: 'var(--sys-color-icon-yellow)' }}
110
- onClick={() => {}}
111
- />
112
- ),
113
- },
114
- {
115
- value: 'stocks',
116
- label: 'Stocks & Investing',
117
- count: <Badge count={142} />,
118
- thumbnail: { src: '/placeholder.png', alt: 'Stocks & Investing' },
119
- },
120
- {
121
- value: 'movie-talk',
122
- label: 'Movie Talk',
123
- count: <Badge count={24} />,
124
- thumbnail: { src: '/placeholder.png', alt: 'Movie Talk' },
125
- trailingIcon: (
126
- <Button
127
- variant="icon"
128
- size="medium"
129
- aria-label="Favorite"
130
- aria-pressed="false"
131
- icon={<StarFillIcon />}
132
- style={{ color: 'var(--sys-color-icon-muted)' }}
89
+ style={{ color: 'var(--sys-color-icon-accent-yellow-default)' }}
133
90
  onClick={() => {}}
134
91
  />
135
92
  ),
@@ -138,9 +95,9 @@ import { StarFillIcon } from '@teamblind-chorus/ui/icons';
138
95
  />
139
96
  ```
140
97
 
141
- ### As nav option (trailing chevron Icon Button)
98
+ ## Nav option
142
99
 
143
- The trailing slot carries a default [Icon Button](../button/icon.md) (`variant="icon"`, `size="medium"`) filled with a right-pointing chevron — the canonical nav-option drill-in affordance. Reach for it when the row routes to a sub-page and you still want to expose an identity-bearing thumbnail (workspace switch, channel directory drill-in). For pure label-only nav stacks, omit `thumbnail` instead — see [the label-only case below](#label-only-no-thumbnail).
100
+ The trailing slot carries a default [Icon Button](../button/icon.md) (`variant="icon"`, `size="medium"`) filled with a right-pointing chevron — the canonical nav-option drill-in affordance. Reach for it when the row routes to a sub-page and you still want to expose an identity-bearing thumbnail (workspace switch, channel directory drill-in). For pure label-only nav stacks, omit `thumbnail` instead — see [the label-only case below](#label-only).
144
101
 
145
102
  ```preview
146
103
  list/entry-as-nav-option
@@ -167,26 +124,11 @@ import { ChevronRightIcon } from '@teamblind-chorus/ui/icons';
167
124
  />
168
125
  ),
169
126
  },
170
- {
171
- value: 'indie-game-devs',
172
- label: 'Indie Game Devs',
173
- secondary: '8,210 Followers',
174
- thumbnail: { src: '/placeholder.png', alt: 'Indie Game Devs' },
175
- trailingIcon: (
176
- <Button
177
- variant="icon"
178
- size="medium"
179
- aria-label="Open Indie Game Devs"
180
- icon={<ChevronRightIcon />}
181
- onClick={() => {}}
182
- />
183
- ),
184
- },
185
127
  ]}
186
128
  />
187
129
  ```
188
130
 
189
- ### Label only (no thumbnail)
131
+ ## Label only
190
132
 
191
133
  Omit `thumbnail` on a row to collapse the leading column — the label sits flush at the 16 inline rail and the row reads as a label-only entry. Reach for it on settings menus, category indexes, and *pick a sub-page* stacks. Pair with a trailing chevron Icon Button to assemble the canonical nav-option row; this is the shape [NavList](../nav-list/nav-list.md) bundles under its header.
192
134
 
@@ -206,25 +148,61 @@ import { ChevronRightIcon } from '@teamblind-chorus/ui/icons';
206
148
  <Button variant="icon" size="medium" aria-label="Open Location" icon={<ChevronRightIcon />} onClick={() => {}} />
207
149
  ),
208
150
  },
151
+ ]}
152
+ />
153
+ ```
154
+
155
+ ## Group
156
+
157
+ Several directory rows bundled into one `<List>`, each with the leading Thumbnail and a different trailing affordance — a Follow toggle, a star favorite, a drill-in chevron — the realistic directory composition the single-variant sections below each isolate one facet of. Rows tile with a hairline divider between them; the last row drops its divider automatically. At `xlarge` the inter-row divider anchors to the text column (16 + 56 + 12 = 84) so the rule reads as separating identity columns under the wider avatar.
158
+
159
+ ```preview
160
+ list/entry-group
161
+ ---
162
+ import { Button, List } from '@teamblind-chorus/ui';
163
+ import { ChevronRightIcon, StarFillIcon } from '@teamblind-chorus/ui/icons';
164
+
165
+ <List
166
+ variant="entry"
167
+ size="medium"
168
+ aria-label="Channels"
169
+ items={[
209
170
  {
210
- value: 'job',
211
- label: 'Job Function',
171
+ value: 'sourdough',
172
+ label: 'Sourdough Bakers',
173
+ secondary: '12.4K Followers',
174
+ thumbnail: { src: '/placeholder.png', alt: 'Sourdough Bakers' },
212
175
  trailingIcon: (
213
- <Button variant="icon" size="medium" aria-label="Open Job Function" icon={<ChevronRightIcon />} onClick={() => {}} />
176
+ <Button variant="toggle" onClick={() => {}}>Follow</Button>
214
177
  ),
215
178
  },
216
179
  {
217
- value: 'learning',
218
- label: 'Learning & Advising',
180
+ value: 'stocks',
181
+ label: 'Stocks & Investing',
182
+ secondary: '8,210 Followers',
183
+ thumbnail: { src: '/placeholder.png', alt: 'Stocks & Investing' },
219
184
  trailingIcon: (
220
- <Button variant="icon" size="medium" aria-label="Open Learning & Advising" icon={<ChevronRightIcon />} onClick={() => {}} />
185
+ <Button
186
+ variant="icon" size="medium"
187
+ aria-label="Favorited" aria-pressed="true"
188
+ icon={<StarFillIcon />}
189
+ style={{ color: 'var(--sys-color-icon-accent-yellow-default)' }}
190
+ onClick={() => {}}
191
+ />
221
192
  ),
222
193
  },
223
194
  {
224
- value: 'money',
225
- label: 'Money',
195
+ value: 'indiedev',
196
+ label: 'Indie Game Devs',
197
+ secondary: '3 new this week',
198
+ thumbnail: { src: '/placeholder.png', alt: 'Indie Game Devs' },
226
199
  trailingIcon: (
227
- <Button variant="icon" size="medium" aria-label="Open Money" icon={<ChevronRightIcon />} onClick={() => {}} />
200
+ <Button
201
+ variant="icon" size="medium"
202
+ aria-label="Open Indie Game Devs"
203
+ icon={<ChevronRightIcon />}
204
+ onClick={() => {}}
205
+ />
228
206
  ),
229
207
  },
230
208
  ]}
@@ -236,11 +214,11 @@ import { ChevronRightIcon } from '@teamblind-chorus/ui/icons';
236
214
  - **container** — outer vertical stack (delegates to family).
237
215
  - **row** — single list item; whole row is the click target.
238
216
  - **leading** *(optional)* — [Thumbnail](../thumbnail/thumbnail.md) at the list's `size` rung (32 / 40 / 48 / 56). `thumbnail` props forward verbatim. Omit per row to collapse the leading column entirely — the leading→text gap (12) also drops, and the label sits flush at the 16 inline rail. Mix-and-match per row is supported.
239
- - **label** — primary row text. `sys.typo.label.md` (14 / Semibold) / `sys.color.onSurface`. Pairs flush with `count` on the primary line.
217
+ - **label** — primary row text. `sys.typo.label.md` (14 / Semibold) / `sys.color.text.default`. Pairs flush with `count` on the primary line.
240
218
  - **count** *(optional)* — inline node painted to the right of the label on the same line (canonical: `<Badge count={n} />`). Separated by `sys.layout.inline.sm` (4); label shrinks first so a long name truncates against the count.
241
- - **secondary** *(optional)* — stacked meta line below the label inside the identity group (follower count, location). `sys.typo.label.sm` (12 / Semibold) / `sys.color.onSurface`. Tiles flush with the label — line-height-only spacing, no margin — so the two lines read as one tight identity block.
242
- - **description** *(optional)* — single-line caption-tone supporting line below the identity group. `sys.typo.label.sm` (12 / Semibold) / `sys.color.onSurfaceVariant`. Separated from the identity group by `ref.space.25` (2). Truncates with ellipsis; never wraps.
243
- - **trailingIcon** *(optional, per-row)* — consumer-supplied node at the trailing edge. Canonical fills: `<Button variant="toggle">` (Follow chip), `<Button variant="text" size="xsmall" appearance="accent">` (Follow / Invite link-affordance — the [attribution-row case](#with-trailing-text-button-compact-attribution-row)), `<Button variant="icon">` (favorite / overflow), `<Badge>`. Its own hit target — clicks stop propagating before reaching the row.
219
+ - **secondary** *(optional)* — stacked meta line below the label inside the identity group (follower count, location). `sys.typo.label.sm` (12 / Semibold) / `sys.color.text.default`. Tiles flush with the label — line-height-only spacing, no margin — so the two lines read as one tight identity block.
220
+ - **description** *(optional)* — single-line caption-tone supporting line below the identity group. `sys.typo.label.sm` (12 / Semibold) / `sys.color.text.subtle`. Separated from the identity group by `ref.space.25` (2). Truncates with ellipsis; never wraps.
221
+ - **trailingIcon** *(optional, per-row)* — consumer-supplied node at the trailing edge. Canonical fills: `<Button variant="toggle">` (Follow chip), `<Button variant="text" size="xsmall" appearance="accent">` (Follow / Invite link-affordance — the [attribution-row case](#trailing-text-button)), `<Button variant="icon">` (favorite / overflow), `<Badge>`. Its own hit target — clicks stop propagating before reaching the row.
244
222
  - **divider** *(optional, per-row)* — pass `divider: false` to suppress the row's bottom hairline. Use when a visual group ends mid-stack and the divider would visually fence off the next group from its label.
245
223
 
246
224
  ## Anatomy
@@ -259,7 +237,7 @@ import { ChevronRightIcon } from '@teamblind-chorus/ui/icons';
259
237
  | description | `sys.typo.label.sm` (12 / Semibold) / `onSurfaceVariant`, single-line ellipsis |
260
238
  | text column → trailing | `sys.layout.inline.sm` (4) |
261
239
  | trailingIcon | `<Button variant="icon">`, `<Button variant="toggle">`, `<Button variant="text" appearance="accent">`, or `<Badge>` |
262
- | inter-row divider | `1px` `outlineVariant`. Default (`small` / `medium` / `large`): `16` inset from both edges. `xlarge`: leading inset anchors to the text column (`16 + 56 + 12 = 84`) so the rule reads as separating identity columns; trailing inset stays at `16`. **Label-only rows** (no thumbnail): leading inset falls back to the default `16` regardless of `size`. |
240
+ | inter-row divider | `1px` `border.default`. Default (`small` / `medium` / `large`): `16` inset from both edges. `xlarge`: leading inset anchors to the text column (`16 + 56 + 12 = 84`) so the rule reads as separating identity columns; trailing inset stays at `16`. **Label-only rows** (no thumbnail): leading inset falls back to the default `16` regardless of `size`. |
263
241
 
264
242
  ## States
265
243
 
@@ -267,7 +245,7 @@ No `selected` state. State overlays (hover / pressed / disabled) delegate to the
267
245
 
268
246
  ## Focus indicator
269
247
 
270
- Inward 3-layer ring inside the row's bounds — see the family-wide [Focus indicator](./list.md#cross-sub-contract). The whole row is the keyboard target; a trailing toggle / icon button carries its own focus ring per its component spec.
248
+ Inward single ring inside the row's bounds — see the family-wide [Focus indicator](./list.md#cross-sub-contract). The whole row is the keyboard target; a trailing toggle / icon button carries its own focus ring per its component spec.
271
249
 
272
250
  ## Behavior
273
251
 
@@ -31,7 +31,7 @@
31
31
  "slots": {
32
32
  "container": {
33
33
  "required": true,
34
- "description": "Outer scroll surface. Vertical stack with a transparent fill (inherits parent container tone); rows separated by a 1px outlineVariant divider, not a gap."
34
+ "description": "Outer scroll surface. Vertical stack with a transparent fill (inherits parent container tone); rows separated by a 1px border.default divider, not a gap."
35
35
  },
36
36
  "row": {
37
37
  "required": true,
@@ -40,7 +40,7 @@
40
40
  "leading": {
41
41
  "required": false,
42
42
  "omittedBehavior": "collapsed",
43
- "fallbackOnMissingSrc": "sys.color.surfaceContainerHigh",
43
+ "fallbackOnMissingSrc": "sys.color.surface.sunken",
44
44
  "description": "Optional Thumbnail at the leading edge at the list's `size` rung (32 / 40 / 48 / 56), vertically centred against the text column. Forwarded verbatim from item.thumbnail. When `thumbnail` is omitted on a row descriptor the leading column collapses, the `leading → text` (12) gap drops to 0, and the label sits flush at the 16 inline rail — reach for it on label-only nav rows (settings menu, category index) and mix-and-match identity rows. `fallbackOnMissingSrc` is the dim-tone fill the Thumbnail paints when `src` is present but empty / fails to load — at scaffold time, agents fill `src` with `/placeholder.png` rather than relying on the fallback. To intentionally render a label-only row, omit `thumbnail` entirely rather than passing an empty `src`.",
45
45
  "accepts": [
46
46
  "thumbnail"
@@ -48,7 +48,7 @@
48
48
  },
49
49
  "label": {
50
50
  "required": true,
51
- "description": "Primary row text. `sys.typo.label.md` (14 / Semibold) / `sys.color.onSurface`. Single line; truncates with ellipsis. Pairs flush with the inline `count` slot — no gap between them, only `sys.layout.inline.sm` (4) horizontal separation on the primary row.",
51
+ "description": "Primary row text. `sys.typo.label.md` (14 / Semibold) / `sys.color.text.default`. Single line; truncates with ellipsis. Pairs flush with the inline `count` slot — no gap between them, only `sys.layout.inline.sm` (4) horizontal separation on the primary row.",
52
52
  "accepts": [
53
53
  "text"
54
54
  ]
@@ -63,14 +63,14 @@
63
63
  },
64
64
  "secondary": {
65
65
  "required": false,
66
- "description": "Optional stacked meta line painted below the label inside the identity group (e.g. `'12.4K Followers'`, `'Brooklyn, NY · Home baker'`). `sys.typo.label.sm` (12 / Semibold) / `sys.color.onSurface`. Tiles flush with the label — line-height-only spacing, no margin — so the two lines read as one tight identity block.",
66
+ "description": "Optional stacked meta line painted below the label inside the identity group (e.g. `'12.4K Followers'`, `'Brooklyn, NY · Home baker'`). `sys.typo.label.sm` (12 / Semibold) / `sys.color.text.default`. Tiles flush with the label — line-height-only spacing, no margin — so the two lines read as one tight identity block.",
67
67
  "accepts": [
68
68
  "text"
69
69
  ]
70
70
  },
71
71
  "description": {
72
72
  "required": false,
73
- "description": "Optional secondary line under the identity group. `sys.typo.label.sm` (12 / Semibold) / `sys.color.onSurfaceVariant`. Single line; truncates with ellipsis. Separated from the identity group by `ref.space.25` (2) so the description reads as a tight supporting layer below the name rather than as a co-equal line.",
73
+ "description": "Optional secondary line under the identity group. `sys.typo.label.sm` (12 / Semibold) / `sys.color.text.subtle`. Single line; truncates with ellipsis. Separated from the identity group by `ref.space.25` (2) so the description reads as a tight supporting layer below the name rather than as a co-equal line.",
74
74
  "accepts": [
75
75
  "text"
76
76
  ]
@@ -126,7 +126,7 @@
126
126
  "divider": {
127
127
  "type": "boolean",
128
128
  "default": true,
129
- "description": "Per-row bottom-divider opt-out. Pass `divider: false` to suppress the hairline `outlineVariant` rule beneath the row; the row's footprint and inline padding stay unchanged. Reach for it when a visual group ends mid-stack and the divider would visually fence off the next group from its label."
129
+ "description": "Per-row bottom-divider opt-out. Pass `divider: false` to suppress the hairline `border.default` rule beneath the row; the row's footprint and inline padding stay unchanged. Reach for it when a visual group ends mid-stack and the divider would visually fence off the next group from its label."
130
130
  },
131
131
  "strong": {
132
132
  "type": "boolean",
@@ -156,17 +156,17 @@
156
156
  "trailingActionGap": "sys.layout.inline.md",
157
157
  "trailingActionGapNote": "Fixed 8px between the text group and a trailing action — the family-wide trailing gap.",
158
158
  "dividerWidth": "sys.borderWidth.hairline",
159
- "dividerColor": "sys.color.outlineVariant",
159
+ "dividerColor": "sys.color.border.default",
160
160
  "dividerPerRowOptOut": "Pass `divider: false` on a row to suppress its bottom divider.",
161
161
  "labelTypo": "sys.typo.label.md",
162
- "labelColor": "sys.color.onSurface",
162
+ "labelColor": "sys.color.text.default",
163
163
  "labelToCountGap": "sys.layout.inline.sm",
164
164
  "secondaryTypo": "sys.typo.label.sm",
165
- "secondaryColor": "sys.color.onSurface",
165
+ "secondaryColor": "sys.color.text.default",
166
166
  "secondaryToLabelGap": "0",
167
167
  "identityToDescriptionGap": "ref.space.25",
168
168
  "descriptionTypo": "sys.typo.label.sm",
169
- "descriptionColor": "sys.color.onSurfaceVariant",
169
+ "descriptionColor": "sys.color.text.subtle",
170
170
  "descriptionMaxLines": 1,
171
171
  "leadingThumbnailSize": {
172
172
  "small": 32,
@@ -176,7 +176,7 @@
176
176
  },
177
177
  "dividerInset": "left 16 + right 16 from the row's edges (overrides the family-wide `right: 0` rule). At `size=\"xlarge\"`, the divider's left edge anchors to the text column instead (16 + 56 + 12 = 84 from the row's leading edge) so the rule reads as separating the text columns of two suggestion rows. Label-only rows (no thumbnail) always fall back to the default 16 inset regardless of `size` — there is no avatar column to anchor against.",
178
178
  "trailingIconSize": "16 × 16",
179
- "trailingIconColor": "sys.color.onSurfaceVariant"
179
+ "trailingIconColor": "sys.color.text.subtle"
180
180
  },
181
181
  "states": {
182
182
  "default": {
@@ -204,37 +204,34 @@
204
204
  "layer": "::after/::before overlay — position:absolute, inset:0, inset box-shadow, no reflow (DESIGN.md Focus ring composition)",
205
205
  "innerCounterRing": {
206
206
  "width": "sys.borderWidth.hairline",
207
- "color": "sys.color.focusInset"
207
+ "color": "sys.color.border.focused"
208
208
  },
209
209
  "outerRing": {
210
210
  "width": "sys.borderWidth.thin",
211
- "color": "sys.color.focus"
211
+ "color": "sys.color.border.focused"
212
212
  }
213
213
  },
214
214
  "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 row is in; never via plain mouse click."
215
215
  },
216
216
  "nestedActionScope": "The hover / pressed overlay is suppressed while the pointer sits on the independent trailing action (a favorite / follow / overflow control). The small control owns the state; the large row does NOT also read as hovered / pressed. The visual-state boundary matches the event boundary (the trailing action already stops propagation).",
217
217
  "disabled": {
218
- "containerOpacity": "sys.state.disabled",
219
- "containerOpacityScope": "Dims the row content only — the inter-row divider and the focus overlay keep full opacity, so a disabled row never fades the hairline rule between it and the next row.",
220
- "pointerEvents": "none"
218
+ "text": "sys.color.text.disabled",
219
+ "icon": "sys.color.icon.disabled",
220
+ "pointerEvents": "none",
221
+ "note": "Explicit disabled (no opacity): row text to text.disabled, icons to icon.disabled. Divider/focus overlay unaffected."
221
222
  }
222
223
  },
223
224
  "focusIndicator": {
224
225
  "description": "Keyboard-focus visual — an accessibility indicator, not a lifecycle state. Composes over whichever lifecycle state the row is in.",
225
226
  "composition": "inward",
226
- "compositionReason": "Rows tile the column flush with only a hairline `outlineVariant` divider between them; an outward ring would overlap the divider and the neighbouring row.",
227
+ "compositionReason": "Rows tile the column flush with only a hairline `border.default` divider between them; an outward ring would overlap the divider and the neighbouring row.",
227
228
  "overlay": {
228
229
  "color": "label",
229
230
  "opacity": "sys.state.focus"
230
231
  },
231
232
  "ring": {
232
- "outerWidth": "sys.borderWidth.thin",
233
- "outerColor": "sys.color.focus",
234
- "outerLayerPosition": "depth 0..2px from the row edge (the outer stroke)",
235
- "insetWidth": "sys.borderWidth.hairline",
236
- "insetColor": "sys.color.focusInset",
237
- "insetLayerPosition": "depth 2..3px from the row edge (the counter-ring just inside the outer stroke)",
233
+ "width": "sys.borderWidth.hairline",
234
+ "color": "sys.color.border.focused",
238
235
  "implementation": "inset box-shadow on the row's `::before` overlay (the `::after` carries the inter-row divider). Constrained strictly inside the row's footprint and never exceeds it."
239
236
  },
240
237
  "trigger": ":focus-visible (keyboard / programmatic focus, never plain mouse click)"
@@ -10,7 +10,7 @@ A vertically-stacked sequence of rows for menus, settings panels, picker sheets,
10
10
 
11
11
  ## Cross-sub contract
12
12
 
13
- - **Container.** Vertical stack, transparent fill (inherits the parent surface). Rows separated by a 1px `outlineVariant` divider inset 16px (`layout.container.md`) from **both** the leading and trailing edges so the rule reads as separating *content*, not the container. The Entry sub overrides the leading inset at `size="xlarge"` only — see [entry.md](./entry.md). No outer radius — corner shape belongs to the wrapping container.
13
+ - **Container.** Vertical stack, transparent fill (inherits the parent surface). Rows separated by a 1px `border.default` divider inset 16px (`layout.container.md`) from **both** the leading and trailing edges so the rule reads as separating *content*, not the container. The Entry sub overrides the leading inset at `size="xlarge"` only — see [entry.md](./entry.md). No outer radius — corner shape belongs to the wrapping container.
14
14
  - **Row geometry.** 8px block / 16px inline padding (`layout.container.xs` / `layout.container.md`); min-height 48px. Row spacing is **role-based**, not a single flex gap: the **text group → trailing action** gap is a fixed `layout.inline.md` (8px) in every sub, while the **leading → text group** gap depends on the leading *type* — `layout.inline.md` (8px) for an icon leading (Radio's indicator), `layout.inline.lg` (12px) for an image leading (a Standard row's `thumbnail` image type / Entry Thumbnail). A label-only Entry row (no `thumbnail`) drops the leading gap to 0. Row grows when `supportingText` is present.
15
15
  - **Label column.** Label: 16px / Regular / `onSurface` (sub-list rows compressed inside an accordion render at 14px / Regular — see [accordion.md](./accordion.md) § Nested list; Entry rows promote the label to 14px / Semibold so the inline `count` reads as part of the identity group). SupportingText: 14px / Regular / `onSurfaceVariant`, sits directly under the label with no extra gap — the two lines stack on the label-column's intrinsic line-box rhythm. The Entry sub replaces the second line with a single-line `description` (12px / Regular / `onSurfaceVariant`, separated from the identity group by `ref.space.25` (2) — see [entry.md](./entry.md)). All secondary lines truncate with ellipsis.
16
16
  - **Strong-label opt-in.** Pass `strong={true}` on a row (`<Accordion.Item strong>` on the accordion sub) to promote the label's weight from Regular (`body.*-weight`, 400) to Semibold (`label.*-weight`, 600) at the same size and line-height — `body.md → label.lg` at the 16 rung, `body.sm → label.md` at the 14 rung. The row's geometry (height, dividers, slot positions) is unchanged; only the label glyphs gain stroke weight. Reach for it when one row needs to read as the primary entry within a denser scan — the active company in a directory, the canonical answer in an FAQ, the user's own row in a member list. Use sparingly — a stack where every row is strong reads as the default again, defeating the marker.
@@ -23,4 +23,4 @@ A vertically-stacked sequence of rows for menus, settings panels, picker sheets,
23
23
  - **[Standard](./standard.md)** — Display / navigation rows; the default sub for menu lists that route or fire without a selection model. Text-only by default (no leading slot, whole row is the click target); a row opts into a 40px leading [Thumbnail](../thumbnail/thumbnail.md) — the image type — by passing `thumbnail` (the former Image sub, now a per-row case), or a 24px (`sys.icon.lg`) leading glyph — the icon type — by passing `icon` (8px from the text group, mutually exclusive with `thumbnail`). A row opts into an inline `count` Badge to the right of the label (the unread / status-count case); for the avatar-anchored identity group with an inline count, reach for [Entry](./entry.md) instead. Set `nav: true` on a row for the drill-in chevron (the former Nav sub, now a per-row case).
24
24
  - **[Radio](./radio.md)** — Single-select picker with a leading 16px radio indicator; clicking a row commits its value via `onChange(value)`.
25
25
  - **[Entry](./entry.md)** — Directory-entry rows with a selectable 32 / 40 / 48 / 56 leading [Thumbnail](../thumbnail/thumbnail.md) (`size="small|medium|large|xlarge"`). Identity group of label + optional inline `count` Badge + optional stacked `secondary` line, plus an optional single-line `description` separated by `ref.space.25` (2). Same click semantics as Standard. The single home for every entity-row case (follow suggestion, member directory, subscription / channel / topic / playlist directory).
26
- - **[Accordion](./accordion.md)** — Expandable rows. Trailing edge auto-renders a `ChevronDownIcon` that rotates `0° → 180°` on expand; the open trigger hosts a content body (prose or another `<List embedded>`) indented one extra `layout.container.md` so the body reads as nested inside the trigger's label column. When the body holds a `<List embedded>`, a hairline `outlineVariant` divider paints at the top of the body so parent ↔ child hierarchy reads.
26
+ - **[Accordion](./accordion.md)** — Expandable rows. Trailing edge auto-renders a `ChevronDownIcon` that rotates `0° → 180°` on expand; the open trigger hosts a content body (prose or another `<List embedded>`) indented one extra `layout.container.md` so the body reads as nested inside the trigger's label column. When the body holds a `<List embedded>`, a hairline `border.default` divider paints at the top of the body so parent ↔ child hierarchy reads.
@@ -4,7 +4,7 @@
4
4
 
5
5
  Single-select picker List sub-component. Each row carries a leading 24px (`sys.icon.lg`) radio indicator; clicking commits that row's value via `onChange(value)`. Exactly one row is selected at a time. Row geometry, typography, divider, state overlays, and inward focus ring all delegate to the [family-wide rules](./list.md); this sub documents the Radio-specific leading indicator and selection contract.
6
6
 
7
- **Reach for this when** the user picks exactly one value from a short, fully-visible set — sort order, range filter, equity tier. **Skip when** multiple values may be selected (use [`Chip variant="filter"`](../chip/filter.md) or [`Button variant="check"`](../button/check.md)), the set is long enough to demand a sheet-driven picker ([Select](../form-field/select.md)), or the row *only* navigates without selecting (use a [Standard](./standard.md) row with `nav: true`). When a value both selects *and* opens a deeper screen — a major category — keep Radio and add `nav: true` (see [Major category with a second screen](#major-category-with-a-second-screen)).
7
+ **Reach for this when** the user picks exactly one value from a short, fully-visible set — sort order, range filter, equity tier. **Skip when** multiple values may be selected (use [`Chip variant="filter"`](../chip/filter.md) or [`Button variant="check"`](../button/check.md)), the set is long enough to demand a sheet-driven picker ([Select](../form-field/select.md)), or the row *only* navigates without selecting (use a [Standard](./standard.md) row with `nav: true`). When a value both selects *and* opens a deeper screen — a major category — keep Radio and add `nav: true` (see [Major category](#major-category)).
8
8
 
9
9
  **Layout inset.** `full-bleed` — sits as a direct child of the page shell. Each row pays its own `16px inline / 8px block` padding via `layout.container.*`; do **not** wrap the list in another `padding-inline` / `px-*` / `style={{ padding: … }}` div, or the radio indicator lands at a different inset than the section headings around it. Inside a bounded surface (Card / Dialog / BottomSheet / Sheet), apply the negative-margin opt-out — see [`AGENTS.md` § Composition rules](../../../AGENTS.md#composition-rules).
10
10
 
@@ -18,25 +18,22 @@ list/radio
18
18
  import { useState } from 'react';
19
19
  import { List } from '@teamblind-chorus/ui';
20
20
 
21
- const [value, setValue] = useState('week');
21
+ const [value, setValue] = useState('trending');
22
22
 
23
23
  <List
24
24
  variant="radio"
25
25
  value={value}
26
26
  onChange={setValue}
27
+ aria-label="Sort posts by"
27
28
  items={[
28
- { value: 'day', label: 'Day' },
29
- { value: 'week', label: 'Week' },
30
- { value: 'month', label: 'Month' },
31
- { value: 'quarter', label: 'Quarter' },
32
- { value: 'year', label: 'Year' },
29
+ { value: 'newest', label: 'Newest first' },
30
+ { value: 'trending', label: 'Trending' },
31
+ { value: 'most-liked', label: 'Most liked' },
33
32
  ]}
34
33
  />
35
34
  ```
36
35
 
37
- ## Use cases
38
-
39
- ### With supporting text
36
+ ## Supporting text
40
37
 
41
38
  Pairs each label with a secondary line — for when the label alone doesn't carry enough context (*sort orders explained in copy*, *equity types with one-line definitions*).
42
39
 
@@ -57,12 +54,11 @@ const [value, setValue] = useState('trending');
57
54
  { value: 'newest', label: 'Newest first', supportingText: 'Most recent posts at the top' },
58
55
  { value: 'trending', label: 'Trending', supportingText: 'Active threads from the last 24h' },
59
56
  { value: 'most-liked', label: 'Most liked', supportingText: 'Highest like count this week' },
60
- { value: 'oldest', label: 'Oldest first', supportingText: 'Earliest posts at the top' },
61
57
  ]}
62
58
  />
63
59
  ```
64
60
 
65
- ### Disabled item
61
+ ## Disabled item
66
62
 
67
63
  A row pinned to `disabled: true` — pointer-events suppressed, indicator dims with the row at `sys.state.disabled` opacity. For options contextually unavailable but still belonging in the set (*paywalled tier*, *region-locked option*).
68
64
 
@@ -79,16 +75,14 @@ const [value, setValue] = useState('week');
79
75
  value={value}
80
76
  onChange={setValue}
81
77
  items={[
82
- { value: 'day', label: 'Day' },
83
- { value: 'week', label: 'Week' },
84
- { value: 'month', label: 'Month' },
85
- { value: 'quarter', label: 'Quarter', disabled: true },
86
- { value: 'year', label: 'Year' },
78
+ { value: 'day', label: 'Day' },
79
+ { value: 'week', label: 'Week' },
80
+ { value: 'month', label: 'Month', disabled: true },
87
81
  ]}
88
82
  />
89
83
  ```
90
84
 
91
- ### Major category with a second screen
85
+ ## Major category
92
86
 
93
87
  A `nav: true` row adds a trailing right-pointing chevron alongside the radio indicator — for a major category that both commits a value *and* opens a deeper screen of sub-options. Selecting the row fires `onChange`; the row's `onClick` routes to the second screen. The chevron is decorative; the whole row is the single click target.
94
88
 
@@ -109,7 +103,6 @@ const [value, setValue] = useState('apparel');
109
103
  { value: 'all', label: 'All categories' },
110
104
  { value: 'apparel', label: 'Apparel', supportingText: 'Tops, outerwear, footwear', nav: true, onClick: () => {} },
111
105
  { value: 'home', label: 'Home & living', supportingText: 'Furniture, decor, kitchen', nav: true, onClick: () => {} },
112
- { value: 'beauty', label: 'Beauty' },
113
106
  ]}
114
107
  />
115
108
  ```
@@ -137,7 +130,7 @@ const [value, setValue] = useState('apparel');
137
130
 
138
131
  ## Focus indicator
139
132
 
140
- Inward 3-layer ring inside the row's bounds — see [Focus indicator](./list.md#cross-sub-contract). The row is the keyboard target, not the indicator.
133
+ Inward single ring inside the row's bounds — see [Focus indicator](./list.md#cross-sub-contract). The row is the keyboard target, not the indicator.
141
134
 
142
135
  ## Behavior
143
136