@teamblind-chorus/ui 1.1.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/README.md +3 -3
  2. package/agents/AGENTS.md +6 -6
  3. package/agents/DESIGN.md +245 -244
  4. package/agents/LOVABLE.md +40 -11
  5. package/agents/catalog.md +10 -8
  6. package/agents/components/avatar-rail/avatar-rail.md +2 -4
  7. package/agents/components/avatar-rail/avatar-rail.spec.json +27 -12
  8. package/agents/components/badge/role.md +7 -9
  9. package/agents/components/badge/role.spec.json +6 -6
  10. package/agents/components/badge/update.md +6 -8
  11. package/agents/components/badge/update.spec.json +5 -5
  12. package/agents/components/banner/banner.family.json +3 -1
  13. package/agents/components/banner/banner.md +66 -15
  14. package/agents/components/banner/banner.spec.json +37 -14
  15. package/agents/components/bottom-sheet/bottom-sheet.md +4 -6
  16. package/agents/components/bottom-sheet/bottom-sheet.spec.json +5 -5
  17. package/agents/components/bubble/bubble.md +8 -10
  18. package/agents/components/bubble/bubble.spec.json +11 -11
  19. package/agents/components/button/button.md +1 -1
  20. package/agents/components/button/check.md +9 -11
  21. package/agents/components/button/check.spec.json +25 -8
  22. package/agents/components/button/fab.md +7 -9
  23. package/agents/components/button/fab.spec.json +27 -10
  24. package/agents/components/button/group.spec.json +4 -4
  25. package/agents/components/button/icon.md +21 -23
  26. package/agents/components/button/icon.spec.json +29 -12
  27. package/agents/components/button/standard.md +40 -42
  28. package/agents/components/button/standard.spec.json +37 -20
  29. package/agents/components/button/text.md +21 -23
  30. package/agents/components/button/text.spec.json +30 -13
  31. package/agents/components/button/toggle.md +7 -9
  32. package/agents/components/button/toggle.spec.json +27 -10
  33. package/agents/components/button/toolbar.md +24 -26
  34. package/agents/components/button/toolbar.spec.json +10 -12
  35. package/agents/components/carousel/carousel.md +1 -1
  36. package/agents/components/carousel/post.md +15 -21
  37. package/agents/components/carousel/post.spec.json +17 -17
  38. package/agents/components/carousel/profile.md +9 -45
  39. package/agents/components/carousel/profile.spec.json +17 -17
  40. package/agents/components/chip/chip.md +1 -1
  41. package/agents/components/chip/filter.md +22 -24
  42. package/agents/components/chip/filter.spec.json +34 -11
  43. package/agents/components/chip/tag.md +22 -24
  44. package/agents/components/chip/tag.spec.json +36 -13
  45. package/agents/components/dialog/dialog.md +1 -3
  46. package/agents/components/dialog/dialog.spec.json +3 -3
  47. package/agents/components/directory-list/directory-list.md +1 -3
  48. package/agents/components/directory-list/directory-list.spec.json +2 -2
  49. package/agents/components/divider/divider.family.json +1 -1
  50. package/agents/components/divider/divider.md +12 -14
  51. package/agents/components/divider/divider.spec.json +8 -8
  52. package/agents/components/empty-state/empty-state.family.json +28 -0
  53. package/agents/components/empty-state/empty-state.md +69 -0
  54. package/agents/components/empty-state/empty-state.spec.json +87 -0
  55. package/agents/components/feed/ad.md +2 -4
  56. package/agents/components/feed/ad.spec.json +10 -10
  57. package/agents/components/feed/post.md +41 -43
  58. package/agents/components/feed/post.spec.json +35 -39
  59. package/agents/components/form-field/form-field.md +1 -1
  60. package/agents/components/form-field/input.md +32 -34
  61. package/agents/components/form-field/input.spec.json +39 -31
  62. package/agents/components/form-field/search.md +2 -4
  63. package/agents/components/form-field/search.spec.json +24 -16
  64. package/agents/components/form-field/select.md +18 -20
  65. package/agents/components/form-field/select.spec.json +36 -27
  66. package/agents/components/form-field/textarea.md +3 -5
  67. package/agents/components/form-field/textarea.spec.json +37 -29
  68. package/agents/components/header/main.md +4 -6
  69. package/agents/components/header/main.spec.json +3 -3
  70. package/agents/components/header/sub.md +6 -8
  71. package/agents/components/header/sub.spec.json +3 -3
  72. package/agents/components/list/accordion.md +34 -45
  73. package/agents/components/list/accordion.spec.json +26 -17
  74. package/agents/components/list/entry.md +59 -81
  75. package/agents/components/list/entry.spec.json +37 -21
  76. package/agents/components/list/list.md +2 -2
  77. package/agents/components/list/radio.md +13 -20
  78. package/agents/components/list/radio.spec.json +33 -18
  79. package/agents/components/list/standard.md +88 -64
  80. package/agents/components/list/standard.spec.json +52 -20
  81. package/agents/components/metadata/compact.md +4 -6
  82. package/agents/components/metadata/compact.spec.json +6 -6
  83. package/agents/components/metadata/metadata.md +1 -1
  84. package/agents/components/metadata/standard.md +12 -14
  85. package/agents/components/metadata/standard.spec.json +10 -10
  86. package/agents/components/nav-card/nav-card.md +25 -27
  87. package/agents/components/nav-card/nav-card.spec.json +25 -16
  88. package/agents/components/nav-list/nav-list.md +2 -8
  89. package/agents/components/nav-list/nav-list.spec.json +3 -3
  90. package/agents/components/navigation-bar/main.md +9 -11
  91. package/agents/components/navigation-bar/main.spec.json +6 -6
  92. package/agents/components/navigation-bar/search.md +6 -8
  93. package/agents/components/navigation-bar/search.spec.json +9 -9
  94. package/agents/components/navigation-bar/sub.md +9 -11
  95. package/agents/components/navigation-bar/sub.spec.json +7 -7
  96. package/agents/components/page-shell/page-shell.family.json +1 -1
  97. package/agents/components/page-shell/page-shell.md +33 -0
  98. package/agents/components/page-shell/page-shell.spec.json +85 -0
  99. package/agents/components/pagination/pagination.family.json +1 -1
  100. package/agents/components/pagination/pagination.md +3 -3
  101. package/agents/components/pagination/pagination.spec.json +5 -5
  102. package/agents/components/profile-header/profile-header.md +9 -11
  103. package/agents/components/profile-header/profile-header.spec.json +9 -9
  104. package/agents/components/progress/progress.family.json +1 -1
  105. package/agents/components/progress/progress.md +5 -5
  106. package/agents/components/progress/progress.spec.json +8 -8
  107. package/agents/components/side-sheet/side-sheet.md +11 -13
  108. package/agents/components/side-sheet/side-sheet.spec.json +3 -3
  109. package/agents/components/skeleton/skeleton.md +7 -9
  110. package/agents/components/skeleton/skeleton.spec.json +5 -5
  111. package/agents/components/spinner/spinner.family.json +27 -0
  112. package/agents/components/spinner/spinner.md +96 -0
  113. package/agents/components/spinner/spinner.spec.json +82 -0
  114. package/agents/components/status-tag/status-tag.md +7 -9
  115. package/agents/components/status-tag/status-tag.spec.json +5 -5
  116. package/agents/components/suggestion-list/suggestion-list.md +3 -7
  117. package/agents/components/suggestion-list/suggestion-list.spec.json +8 -12
  118. package/agents/components/switch/switch.md +12 -14
  119. package/agents/components/switch/switch.spec.json +23 -15
  120. package/agents/components/tab-bar/tab-bar.md +9 -11
  121. package/agents/components/tab-bar/tab-bar.spec.json +37 -23
  122. package/agents/components/tabs/rounded.md +6 -8
  123. package/agents/components/tabs/rounded.spec.json +34 -13
  124. package/agents/components/tabs/segmented.md +4 -6
  125. package/agents/components/tabs/segmented.spec.json +4 -8
  126. package/agents/components/tabs/underline.md +9 -11
  127. package/agents/components/tabs/underline.spec.json +31 -14
  128. package/agents/components/thumbnail/thumbnail.md +5 -7
  129. package/agents/components/thumbnail/thumbnail.spec.json +8 -8
  130. package/agents/components/toast/toast.md +5 -7
  131. package/agents/components/toast/toast.spec.json +3 -3
  132. package/agents/components/tooltip/tooltip.md +6 -8
  133. package/agents/components/tooltip/tooltip.spec.json +4 -4
  134. package/agents/manifest.json +8 -6
  135. package/agents/tokens.usage.json +71 -226
  136. package/agents/usage.json +12 -0
  137. package/dist/index.cjs +531 -262
  138. package/dist/index.cjs.map +1 -1
  139. package/dist/index.d.cts +57 -13
  140. package/dist/index.d.ts +57 -13
  141. package/dist/index.js +530 -263
  142. package/dist/index.js.map +1 -1
  143. package/dist/styles.css +560 -379
  144. package/eslint/rules.js +7 -7
  145. package/package.json +2 -3
  146. package/agents/anti-patterns.md +0 -533
  147. package/agents/compose.md +0 -240
  148. package/agents/images.md +0 -66
@@ -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": {
@@ -194,28 +194,44 @@
194
194
  "opacity": "sys.state.pressed"
195
195
  }
196
196
  },
197
+ "focused": {
198
+ "overlay": {
199
+ "color": "label",
200
+ "opacity": "sys.state.focus"
201
+ },
202
+ "focusRing": {
203
+ "composition": "inward",
204
+ "layer": "::after/::before overlay — position:absolute, inset:0, inset box-shadow, no reflow (DESIGN.md Focus ring composition)",
205
+ "innerCounterRing": {
206
+ "width": "sys.borderWidth.hairline",
207
+ "color": "sys.color.border.focused"
208
+ },
209
+ "outerRing": {
210
+ "width": "sys.borderWidth.thin",
211
+ "color": "sys.color.border.focused"
212
+ }
213
+ },
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
+ },
197
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).",
198
217
  "disabled": {
199
- "containerOpacity": "sys.state.disabled",
200
- "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.",
201
- "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."
202
222
  }
203
223
  },
204
224
  "focusIndicator": {
205
225
  "description": "Keyboard-focus visual — an accessibility indicator, not a lifecycle state. Composes over whichever lifecycle state the row is in.",
206
226
  "composition": "inward",
207
- "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.",
208
228
  "overlay": {
209
229
  "color": "label",
210
230
  "opacity": "sys.state.focus"
211
231
  },
212
232
  "ring": {
213
- "outerWidth": "sys.borderWidth.thin",
214
- "outerColor": "sys.color.focus",
215
- "outerLayerPosition": "depth 0..2px from the row edge (the outer stroke)",
216
- "insetWidth": "sys.borderWidth.hairline",
217
- "insetColor": "sys.color.focusInset",
218
- "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",
219
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."
220
236
  },
221
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
 
@@ -30,7 +30,7 @@
30
30
  "slots": {
31
31
  "container": {
32
32
  "required": true,
33
- "description": "Outer scroll surface. Vertical stack with a transparent fill (inherits parent container tone); rows separated by a 1px outlineVariant divider, not a gap."
33
+ "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."
34
34
  },
35
35
  "row": {
36
36
  "required": true,
@@ -114,17 +114,17 @@
114
114
  "trailingActionGap": "sys.layout.inline.md",
115
115
  "trailingActionGapNote": "Fixed 8px between the text group and a trailing nav chevron (the major-category `nav: true` case) — the family-wide trailing gap.",
116
116
  "dividerWidth": "sys.borderWidth.hairline",
117
- "dividerColor": "sys.color.outlineVariant",
117
+ "dividerColor": "sys.color.border.default",
118
118
  "labelTypo": "sys.typo.body.md",
119
- "labelColor": "sys.color.onSurface",
119
+ "labelColor": "sys.color.text.default",
120
120
  "supportingTypo": "sys.typo.body.sm",
121
- "supportingColor": "sys.color.onSurfaceVariant",
121
+ "supportingColor": "sys.color.text.subtle",
122
122
  "supportingOffset": "0",
123
123
  "leadingRadioSize": "24 × 24",
124
- "leadingRadioColorRest": "sys.color.outline",
125
- "leadingRadioColorSelected": "sys.color.primary",
124
+ "leadingRadioColorRest": "sys.color.border.boldest",
125
+ "leadingRadioColorSelected": "sys.color.background.primary",
126
126
  "navChevronSize": "16 × 16",
127
- "navChevronColor": "sys.color.onSurfaceVariant"
127
+ "navChevronColor": "sys.color.text.subtle"
128
128
  },
129
129
  "states": {
130
130
  "default": {
@@ -142,31 +142,46 @@
142
142
  "opacity": "sys.state.pressed"
143
143
  }
144
144
  },
145
+ "focused": {
146
+ "overlay": {
147
+ "color": "label",
148
+ "opacity": "sys.state.focus"
149
+ },
150
+ "focusRing": {
151
+ "composition": "inward",
152
+ "layer": "::after/::before overlay — position:absolute, inset:0, inset box-shadow, no reflow (DESIGN.md Focus ring composition)",
153
+ "innerCounterRing": {
154
+ "width": "sys.borderWidth.hairline",
155
+ "color": "sys.color.border.focused"
156
+ },
157
+ "outerRing": {
158
+ "width": "sys.borderWidth.thin",
159
+ "color": "sys.color.border.focused"
160
+ }
161
+ },
162
+ "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."
163
+ },
145
164
  "selected": {
146
165
  "leading": "Filled primary indicator; row foreground stays at onSurface. No fill change on the row itself."
147
166
  },
148
167
  "disabled": {
149
- "containerOpacity": "sys.state.disabled",
150
- "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.",
168
+ "text": "sys.color.text.disabled",
169
+ "icon": "sys.color.icon.disabled",
151
170
  "pointerEvents": "none",
152
- "note": "Radio indicator dims with the row."
171
+ "note": "Explicit disabled (no opacity): row text to text.disabled, radio indicator + icons to icon.disabled. Divider/focus overlay unaffected."
153
172
  }
154
173
  },
155
174
  "focusIndicator": {
156
175
  "description": "Keyboard-focus visual — an accessibility indicator, not a lifecycle state. The ring sits on the row, not on the leading indicator — the row is the keyboard target.",
157
176
  "composition": "inward",
158
- "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.",
177
+ "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.",
159
178
  "overlay": {
160
179
  "color": "label",
161
180
  "opacity": "sys.state.focus"
162
181
  },
163
182
  "ring": {
164
- "outerWidth": "sys.borderWidth.thin",
165
- "outerColor": "sys.color.focus",
166
- "outerLayerPosition": "depth 0..2px from the row edge (the outer stroke)",
167
- "insetWidth": "sys.borderWidth.hairline",
168
- "insetColor": "sys.color.focusInset",
169
- "insetLayerPosition": "depth 2..3px from the row edge (the counter-ring just inside the outer stroke)",
183
+ "width": "sys.borderWidth.hairline",
184
+ "color": "sys.color.border.focused",
170
185
  "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."
171
186
  },
172
187
  "trigger": ":focus-visible (keyboard / programmatic focus, never plain mouse click)"
@@ -181,6 +196,6 @@
181
196
  "forbidden": [
182
197
  "radio glyph as a separate hit area — the entire row is the click target",
183
198
  "multi-select painted as radio — radio variant is single-select; use checkbox or chip/filter for multi-select",
184
- "selected state painted with sys.color.primaryContainer fill — radio selected paints the inner dot, not the row fill"
199
+ "selected state painted with sys.color.background.selected fill — radio selected paints the inner dot, not the row fill"
185
200
  ]
186
201
  }
@@ -10,7 +10,7 @@ A row opts into an inline `count` Badge to the right of the label (4px / `sys.la
10
10
 
11
11
  ## Default
12
12
 
13
- A plain text listfive menu rows with optional supporting text.
13
+ A single text rowthe atomic List component. A row is a `value` + `label`, with an optional `supportingText` second line. The whole row is the click target.
14
14
 
15
15
  ```preview
16
16
  list/standard
@@ -19,18 +19,12 @@ import { List } from '@teamblind-chorus/ui';
19
19
 
20
20
  <List
21
21
  items={[
22
- { value: 'profile', label: 'Profile', supportingText: 'Name, photo, bio' },
23
- { value: 'notif', label: 'Notifications', supportingText: 'Email, push, in-app' },
24
- { value: 'privacy', label: 'Privacy', supportingText: 'Who can see your activity' },
25
- { value: 'language', label: 'Language' },
26
- { value: 'about', label: 'About' },
22
+ { value: 'profile', label: 'Profile', supportingText: 'Display name, avatar, bio' },
27
23
  ]}
28
24
  />
29
25
  ```
30
26
 
31
- ## Use cases
32
-
33
- ### With trailing action
27
+ ## Trailing action
34
28
 
35
29
  A Text Button in the row's `trailingIcon` slot turns a display row into row + action — the row label stays informational (no `onClick` on the row), the trailing button is the only commit target. Reach for it on settings rows that pair a value with a small "change / edit / view" action.
36
30
 
@@ -52,21 +46,11 @@ import { Button, List } from '@teamblind-chorus/ui';
52
46
  </Button>
53
47
  ),
54
48
  },
55
- {
56
- value: 'sms',
57
- label: 'SMS',
58
- supportingText: '+1 (415) ***-2487',
59
- trailingIcon: (
60
- <Button variant="text" size="small" appearance="accent" onClick={() => {}}>
61
- Edit
62
- </Button>
63
- ),
64
- },
65
49
  ]}
66
50
  />
67
51
  ```
68
52
 
69
- ### With an inline count
53
+ ## Inline count
70
54
 
71
55
  Pass a `count` on a row and a Badge renders to the right of the label on the same line, separated by `4px` (`sys.layout.inline.sm`) — the unread / status-count case. The label shrinks first so a long label truncates against the count, which stays pinned at its intrinsic width. Unlike `trailingIcon`, the count tiles tight to the label inside the text group, so it **composes with the drill-in chevron**: a `nav: true` row carries the count by the label *and* keeps its trailing chevron — one row with both an icon and a badge.
72
56
 
@@ -77,16 +61,12 @@ import { List, Badge } from '@teamblind-chorus/ui';
77
61
 
78
62
  <List
79
63
  items={[
80
- { value: 'profile', label: 'Profile', supportingText: 'Display name, avatar, bio', nav: true },
81
- { value: 'channels', label: 'My channels', supportingText: '12 joined · 3 muted', nav: true },
82
- { value: 'notif', label: 'Notifications', count: <Badge>3</Badge>, nav: true },
83
- { value: 'privacy', label: 'Privacy', nav: true },
84
- { value: 'account', label: 'Account', nav: true },
64
+ { value: 'notif', label: 'Notifications', count: <Badge>3</Badge>, nav: true },
85
65
  ]}
86
66
  />
87
67
  ```
88
68
 
89
- ### Drill-in rows
69
+ ## Drill-in
90
70
 
91
71
  Set `nav: true` on a row to auto-render a trailing right-pointing chevron — the drill-in affordance signalling the row routes to another surface. The whole row is the click target; the chevron is decorative. This is the canonical settings / menu navigation shape (it replaces the former `nav` variant). A per-item `trailingIcon` overrides the chevron for that row.
92
72
 
@@ -97,16 +77,12 @@ import { List } from '@teamblind-chorus/ui';
97
77
 
98
78
  <List
99
79
  items={[
100
- { value: 'profile', label: 'Profile', supportingText: 'Display name, avatar, bio', nav: true },
101
- { value: 'channels', label: 'My channels', supportingText: '12 joined · 3 muted', nav: true },
102
- { value: 'notif', label: 'Notifications', nav: true },
103
- { value: 'privacy', label: 'Privacy', nav: true },
104
- { value: 'account', label: 'Account', nav: true },
80
+ { value: 'profile', label: 'Profile', supportingText: 'Display name, avatar, bio', nav: true },
105
81
  ]}
106
82
  />
107
83
  ```
108
84
 
109
- ### With a leading icon
85
+ ## Leading icon
110
86
 
111
87
  Pass an `icon` on a row and a 24px (`sys.icon.lg`) glyph renders at the leading edge in `onSurfaceVariant`, `8px` (`sys.layout.inline.md`) from the text group — the category-mark shape for settings / menu rows, lighter than a 40px leading image (and mutually exclusive with `thumbnail`). The glyph is decorative (the label carries the meaning) and the slot enforces the 24 rung regardless of the glyph's own `size`. Every other slot stays optional, so the icon composes with `supportingText`, an inline `count`, and the drill-in chevron.
112
88
 
@@ -114,19 +90,16 @@ Pass an `icon` on a row and a 24px (`sys.icon.lg`) glyph renders at the leading
114
90
  list/standard-icon-leading
115
91
  ---
116
92
  import { List, Badge } from '@teamblind-chorus/ui';
117
- import { ProfileIcon, BellIcon, BookmarkIcon, PulseIcon } from '@teamblind-chorus/ui/icons';
93
+ import { BellIcon } from '@teamblind-chorus/ui/icons';
118
94
 
119
95
  <List
120
96
  items={[
121
- { value: 'profile', label: 'Profile', supportingText: 'Display name, avatar, bio', icon: <ProfileIcon />, nav: true },
122
- { value: 'notif', label: 'Notifications', icon: <BellIcon />, count: <Badge>3</Badge>, nav: true },
123
- { value: 'saved', label: 'Saved', icon: <BookmarkIcon />, nav: true },
124
- { value: 'activity', label: 'Activity', icon: <PulseIcon />, nav: true },
97
+ { value: 'notif', label: 'Notifications', icon: <BellIcon />, count: <Badge>3</Badge>, nav: true },
125
98
  ]}
126
99
  />
127
100
  ```
128
101
 
129
- ### Leading image
102
+ ## Leading image
130
103
 
131
104
  Pass a `thumbnail` on a row and a 40px [Thumbnail](../thumbnail/thumbnail.md) renders at the leading edge, vertically centred against the label column. `thumbnail` props (`src`, `alt`, `updateDot`, `logoBadge`) forward verbatim. The gap to the text group steps up to `12px` (`sys.layout.inline.lg`) so the avatar and the label column read as two distinct blocks. This is the channel / source / author row shape — same click semantics as a text row, no selection model.
132
105
 
@@ -137,14 +110,12 @@ import { List } from '@teamblind-chorus/ui';
137
110
 
138
111
  <List
139
112
  items={[
140
- { value: 'design-weekly', label: 'Design Weekly', supportingText: 'Updated 2h ago', thumbnail: { alt: 'Design Weekly' } },
141
- { value: 'frontend', label: 'Frontend Friday', supportingText: 'Updated 1d ago', thumbnail: { alt: 'Frontend Friday' } },
142
- { value: 'changelog', label: 'Changelog', supportingText: 'Updated 3d ago', thumbnail: { alt: 'Changelog' } },
113
+ { value: 'sourdough', label: 'Sourdough Bakers', supportingText: '3 new posts today', thumbnail: { alt: 'Sourdough Bakers' } },
143
114
  ]}
144
115
  />
145
116
  ```
146
117
 
147
- ### Leading image with trailing action
118
+ ## Leading image + trailing action
148
119
 
149
120
  A Text Button in the row's `trailingIcon` slot — the canonical "directory row + small commit" composition. Reach for it on follow / join / invite rows where the leading Thumbnail anchors the entity and the trailing button is the only commit. Row body stays informational.
150
121
 
@@ -167,22 +138,11 @@ import { Button, List } from '@teamblind-chorus/ui';
167
138
  </Button>
168
139
  ),
169
140
  },
170
- {
171
- value: 'frontend',
172
- label: 'Frontend',
173
- supportingText: '892 colleagues following',
174
- thumbnail: { alt: 'Frontend' },
175
- trailingIcon: (
176
- <Button variant="text" size="small" appearance="accent" onClick={() => {}}>
177
- Follow
178
- </Button>
179
- ),
180
- },
181
141
  ]}
182
142
  />
183
143
  ```
184
144
 
185
- ### Leading image drill-in rows
145
+ ## Leading image + drill-in
186
146
 
187
147
  Set `nav: true` on a leading-image row for an avatar-anchored row that routes to another surface (channel → channel detail, person → profile). The whole row is the click target; the chevron is decorative. A per-item `trailingIcon` overrides the chevron.
188
148
 
@@ -193,14 +153,12 @@ import { List } from '@teamblind-chorus/ui';
193
153
 
194
154
  <List
195
155
  items={[
196
- { value: 'design-weekly', label: 'Design Weekly', supportingText: '2.3k members', thumbnail: { alt: 'Design Weekly' }, nav: true },
197
- { value: 'frontend', label: 'Frontend Friday', supportingText: '1.1k members', thumbnail: { alt: 'Frontend Friday' }, nav: true },
198
- { value: 'changelog', label: 'Changelog', supportingText: '840 members', thumbnail: { alt: 'Changelog' }, nav: true },
156
+ { value: 'design-weekly', label: 'Design Weekly', supportingText: '2.3k members', thumbnail: { alt: 'Design Weekly' }, nav: true },
199
157
  ]}
200
158
  />
201
159
  ```
202
160
 
203
- ### Leading image without divider
161
+ ## Leading image, no divider
204
162
 
205
163
  `divider: false` on a row suppresses its bottom hairline rule — useful when a visual group ends mid-stack and the divider would visually fence off the next group from its label. The row's footprint and inline padding stay unchanged.
206
164
 
@@ -218,6 +176,75 @@ import { List } from '@teamblind-chorus/ui';
218
176
  />
219
177
  ```
220
178
 
179
+ ## Embedded Banner
180
+
181
+ Pass a `banner` on a row and a [Banner](../banner/banner.md) renders **below** the row's text group, `8px` (`sys.layout.stack.xs`) down, spanning the row's full content width (aligned to the same 16px inline inset as the label above it). The row flips from a single line to a vertical stack — text group on top, Banner underneath. Reach for it when a row needs an in-row call-out tied to *that row's* subject (a follow-up prompt, a capability nudge, a single-line CTA), rather than a separate full-width Banner detached from the row.
182
+
183
+ The Banner keeps its full prop surface — here `appearance="accent"` + `neutralBody` for the quiet-tint shape, a blue `CheckCircleFillIcon` leading the single-line body, and a `trailingAction` Text Button with a trailing chevron. The Banner is a nested-action region: its button never commits the row, and the row's hover / pressed overlay is suppressed over it. The row leads with a fill-type category glyph (`BriefcaseFillIcon`) that the leading slot tones to `onSurfaceVariant`.
184
+
185
+ ```preview
186
+ list/standard-embedded-banner
187
+ ---
188
+ import { Banner, Button, List } from '@teamblind-chorus/ui';
189
+ import { BriefcaseFillIcon, CheckCircleFillIcon, ChevronRightIcon } from '@teamblind-chorus/ui/icons';
190
+
191
+ <List
192
+ aria-label="Career"
193
+ items={[
194
+ {
195
+ value: 'major',
196
+ label: 'My major: Computer Science',
197
+ strong: true,
198
+ icon: <BriefcaseFillIcon />,
199
+ banner: (
200
+ <Banner
201
+ appearance="accent"
202
+ neutralBody
203
+ icon={<CheckCircleFillIcon size={16} style={{ color: 'var(--sys-color-background-primary)' }} />}
204
+ trailingAction={(
205
+ <Button variant="text" appearance="accent" size="small" trailingIcon={<ChevronRightIcon />}>
206
+ Expert Q&A
207
+ </Button>
208
+ )}
209
+ >
210
+ Ask a professional in this field
211
+ </Banner>
212
+ ),
213
+ },
214
+ ]}
215
+ />
216
+ ```
217
+
218
+ ## Group
219
+
220
+ Several rows bundled into one `<List>`. Every row carries a leading icon and reaches for a different per-row variant — a drill-in row, a row with an inline `count` + drill-in chevron, a row with a trailing action — so the group reads as the realistic 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 (no `divider: false` needed).
221
+
222
+ ```preview
223
+ list/standard-group
224
+ ---
225
+ import { Badge, Button, List } from '@teamblind-chorus/ui';
226
+ import { BellIcon, InvitationIcon, ProfileIcon } from '@teamblind-chorus/ui/icons';
227
+
228
+ <List
229
+ aria-label="Account"
230
+ items={[
231
+ { value: 'profile', label: 'Profile', supportingText: 'Display name, avatar, bio', icon: <ProfileIcon />, nav: true },
232
+ { value: 'notif', label: 'Notifications', icon: <BellIcon />, count: <Badge>3</Badge>, nav: true },
233
+ {
234
+ value: 'email',
235
+ label: 'Email',
236
+ supportingText: 'work@example.com',
237
+ icon: <InvitationIcon />,
238
+ trailingIcon: (
239
+ <Button variant="text" size="small" appearance="accent" onClick={() => {}}>
240
+ Edit
241
+ </Button>
242
+ ),
243
+ },
244
+ ]}
245
+ />
246
+ ```
247
+
221
248
  ## Slots
222
249
 
223
250
  - **container** — outer vertical stack (delegates to family).
@@ -231,6 +258,7 @@ import { List } from '@teamblind-chorus/ui';
231
258
  - **trailingIcon** *(optional, per-row)* — consumer-supplied node at the trailing edge. Each row decides independently. Canonical fills: a 16px icon (e.g. an external-link mark), or `<Button variant="text" appearance="accent">` (Follow / Invite) on leading-image rows. A status badge does **not** go here — it tiles next to the label via `count`. Its own hit target: a tap on this slot stops propagating before it reaches the row's `onClick`. Overrides the nav chevron on the same row.
232
259
  - **navChevron** *(optional, per-row)* — auto-rendered 16px right-pointing chevron, painted when the row sets `nav: true`. `onSurfaceVariant`, decorative; never a separate hit target.
233
260
  - **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.
261
+ - **banner** *(optional, per-row)* — an embedded [Banner](../banner/banner.md) below the row's text group, `8px` (`sys.layout.stack.xs`) down, spanning the row's full content width. The row stacks (text group over Banner). A nested-action region: its own controls never commit the row, and the row's hover / pressed overlay is suppressed over it. Canonical fill: `<Banner appearance="accent" neutralBody icon={…} trailingAction={…}>` — an in-row call-out tied to the row's subject.
234
262
 
235
263
  ## States
236
264
 
@@ -238,7 +266,7 @@ No `selected` state — selection belongs to the [Radio sub](./radio.md).
238
266
 
239
267
  ## Focus indicator
240
268
 
241
- Inward 3-layer ring inside the row's bounds — see the family-wide [Focus indicator](./list.md#cross-sub-contract). The preview pins **My channels** to its focused state via `forcedState: 'focused'` for static inspection.
269
+ Inward single ring inside the row's bounds — see the family-wide [Focus indicator](./list.md#cross-sub-contract). The preview pins **My channels** to its focused state via `forcedState: 'focused'` for static inspection.
242
270
 
243
271
  ```preview
244
272
  list/focus-indicator
@@ -247,11 +275,7 @@ import { List } from '@teamblind-chorus/ui';
247
275
 
248
276
  <List
249
277
  items={[
250
- { value: 'profile', label: 'Profile', supportingText: 'Display name, avatar, bio', nav: true },
251
- { value: 'channels', label: 'My channels', supportingText: '12 joined · 3 muted', nav: true, forcedState: 'focused' },
252
- { value: 'notif', label: 'Notifications', nav: true },
253
- { value: 'privacy', label: 'Privacy', nav: true },
254
- { value: 'account', label: 'Account', nav: true },
278
+ { value: 'channels', label: 'My channels', supportingText: '12 joined · 3 muted', nav: true, forcedState: 'focused' },
255
279
  ]}
256
280
  />
257
281
  ```