@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
@@ -56,7 +56,7 @@
56
56
  },
57
57
  "label": {
58
58
  "required": false,
59
- "description": "Heading text. `<h3>` by default. Size `large` → `sys.typo.heading.md`; size `medium` → `sys.typo.heading.sm`. Color `sys.color.onSurface`."
59
+ "description": "Heading text. `<h3>` by default. Size `large` → `sys.typo.heading.md`; size `medium` → `sys.typo.heading.sm`. Color `sys.color.text.default`."
60
60
  },
61
61
  "action": {
62
62
  "required": false,
@@ -85,10 +85,10 @@
85
85
  "medium": {
86
86
  "labelTypo": "sys.typo.heading.sm (16 / Semibold)"
87
87
  },
88
- "labelColor": "sys.color.onSurface",
88
+ "labelColor": "sys.color.text.default",
89
89
  "actionSize": "Text Button xsmall, appearance accent",
90
90
  "iconButtonSize": "Button variant=\"icon\" size=\"medium\" (32 × 32 capsule, 16-glyph)",
91
- "iconColor": "sys.color.onSurfaceVariant (glyph inherits via `currentColor` from the Icon Button)"
91
+ "iconColor": "sys.color.text.subtle (glyph inherits via `currentColor` from the Icon Button)"
92
92
  },
93
93
  "appearance": {
94
94
  "background": "transparent",
@@ -2,7 +2,7 @@
2
2
 
3
3
  > 🇰🇷 한국어: [`i18n/ko/schema/components/header/sub.md`](../../../i18n/ko/schema/components/header/sub.md)
4
4
 
5
- Quiet section-dividing label — a 14px (`sys.typo.label.md`, 14 / Semibold) line in the muted `sys.color.onSurfaceVariant` tone that names the group of rows beneath it ("Following", "More Topics to follow"). The muted tone is the dividing device: it reads as a region label rather than a heading that competes with the page title, while the label-weight typo keeps it from dissolving into body copy. Rendered as a semantic `<h3>` by default so screen-reader heading navigation lands on the group label.
5
+ Quiet section-dividing label — a 14px (`sys.typo.label.md`, 14 / Semibold) line in the muted `sys.color.text.subtle` tone that names the group of rows beneath it ("Following", "More Topics to follow"). The muted tone is the dividing device: it reads as a region label rather than a heading that competes with the page title, while the label-weight typo keeps it from dissolving into body copy. Rendered as a semantic `<h3>` by default so screen-reader heading navigation lands on the group label.
6
6
 
7
7
  It may carry one quiet trailing affordance — a single [Text Button](../button/text.md) `action` ("See all", "Edit", "Manage") — for a region-level commit that stays subordinate to the muted label. Icon drill-in and dropdown disclosure are *not* SubHeader's: those belong on [Main](./main.md), whose louder `onSurface` heading earns the heavier affordance set.
8
8
 
@@ -22,9 +22,7 @@ import { SubHeader } from '@teamblind-chorus/ui';
22
22
  <SubHeader label="Following" />
23
23
  ```
24
24
 
25
- ## Use cases
26
-
27
- ### Grouping a list
25
+ ## Grouping a list
28
26
 
29
27
  The canonical placement — a SubHeader labels each group of rows, splitting one stacked list into named sections. The muted label sits at the same `16px` inset as the rows; the block rhythm (more above, less below) reads as "new group starts here".
30
28
 
@@ -69,7 +67,7 @@ function Example() {
69
67
  }
70
68
  ```
71
69
 
72
- ### With a trailing action
70
+ ## Trailing action
73
71
 
74
72
  Pass `action={{ label, href, onClick }}` for a single quiet commit on the trailing edge — "See all" above a previewed group, "Edit" above an editable cluster. The wrapper becomes a full-bleed flex row: the muted label holds the leading edge, the [Text Button](../button/text.md) (`size="xsmall"`, `appearance="accent"`) hugs the trailing edge at the same 16 inset. The action stays subordinate to the label — this is still a quiet region marker, not a [Main](./main.md) heading row. Use Main instead when the region needs the louder `onSurface` heading, an icon drill-in, or a sort dropdown.
75
73
 
@@ -91,7 +89,7 @@ import { SubHeader, List } from '@teamblind-chorus/ui';
91
89
  </div>
92
90
  ```
93
91
 
94
- ### Host owns the heading
92
+ ## Host owns the heading
95
93
 
96
94
  When the surrounding host already carries the section semantics, override the wrapping element with `as="div"` so the document outline doesn't gain a stray heading — the label keeps its muted tone and inset.
97
95
 
@@ -106,7 +104,7 @@ import { SubHeader } from '@teamblind-chorus/ui';
106
104
  ## Slots
107
105
 
108
106
  - **container** — the full-bleed label line. `16px` (`layout.container.md`) inline inset to align with the rows beneath, `24px` (`layout.stack.lg`) block padding above and `8px` (`layout.stack.xs`) below. Non-interactive. Without `action` the container *is* the heading element (`as`); with `action` it is a non-semantic flex `<div>` (`space-between`, `8px` `layout.inline.md` gap) holding the nested label and the trailing action on opposite edges.
109
- - **label** — required. Section label text. `<h3>` by default (override via `as`). Typo `sys.typo.label.md` (14 / Semibold); color `sys.color.onSurfaceVariant`. Single line.
107
+ - **label** — required. Section label text. `<h3>` by default (override via `as`). Typo `sys.typo.label.md` (14 / Semibold); color `sys.color.text.subtle`. Single line.
110
108
  - **action** — optional. Trailing [Text Button](../button/text.md) (`size="xsmall"`, `appearance="accent"`) from `action={{ label, href, onClick }}`. The one trailing affordance SubHeader carries; owns its own tap target. Present only when `action` is set.
111
109
 
112
110
  ## Anatomy
@@ -114,7 +112,7 @@ import { SubHeader } from '@teamblind-chorus/ui';
114
112
  | Slot | Token bindings |
115
113
  |-----------|----------------|
116
114
  | container | `sys.layout.container.md` (16) inline padding, `sys.layout.stack.lg` (24) block-start, `sys.layout.stack.xs` (8) block-end, full inline width, no border, no radius; in `action` mode flex `space-between` with `sys.layout.inline.md` (8) gap |
117
- | label | `sys.typo.label.md` (14 / Semibold) typo, `sys.color.onSurfaceVariant` color |
115
+ | label | `sys.typo.label.md` (14 / Semibold) typo, `sys.color.text.subtle` color |
118
116
  | action | [Text Button](../button/text.md) `size="xsmall"`, `appearance="accent"`; intrinsic width, never grows |
119
117
 
120
118
  ## Appearance
@@ -3,7 +3,7 @@
3
3
  "name": "SubHeader",
4
4
  "family": "header",
5
5
  "subcomponent": "sub",
6
- "description": "The quiet member of the Header family — a section-dividing label. A single line of 14px (`sys.typo.label.md`, 14 / Semibold) text in the muted `sys.color.onSurfaceVariant` tone that names the group of rows beneath it (\"Following\", \"More Topics to follow\"). No size axis — the muted tone is the dividing device, and the label-weight typo keeps it reading as a section label rather than body copy. Reached as `<SubHeader>`; its louder sibling is [Main](./main.md) (`<Header>`), an `onSurface` heading at 16 / 20. Optionally carries a single trailing `action` Text Button (`size=\"xsmall\"`, `appearance=\"accent\"`) for a quiet region-level commit (\"See all\", \"Edit\", \"Manage\"); icon drill-in and dropdown disclosure stay Main's territory, where the louder `onSurface` heading tone belongs. Rendered as a semantic `<h3>` by default so screen-reader heading navigation lands on the group label; consumers may override the wrapping element via the `as` prop (e.g. `as=\"div\"` when the host already owns the heading semantics). Full-bleed: pays the same 16 (`sys.layout.container.md`) inline inset as the List rows it labels, with asymmetric block padding (24 above to break from the previous region, 8 below to bind the label to its group).",
6
+ "description": "The quiet member of the Header family — a section-dividing label. A single line of 14px (`sys.typo.label.md`, 14 / Semibold) text in the muted `sys.color.text.subtle` tone that names the group of rows beneath it (\"Following\", \"More Topics to follow\"). No size axis — the muted tone is the dividing device, and the label-weight typo keeps it reading as a section label rather than body copy. Reached as `<SubHeader>`; its louder sibling is [Main](./main.md) (`<Header>`), an `onSurface` heading at 16 / 20. Optionally carries a single trailing `action` Text Button (`size=\"xsmall\"`, `appearance=\"accent\"`) for a quiet region-level commit (\"See all\", \"Edit\", \"Manage\"); icon drill-in and dropdown disclosure stay Main's territory, where the louder `onSurface` heading tone belongs. Rendered as a semantic `<h3>` by default so screen-reader heading navigation lands on the group label; consumers may override the wrapping element via the `as` prop (e.g. `as=\"div\"` when the host already owns the heading semantics). Full-bleed: pays the same 16 (`sys.layout.container.md`) inline inset as the List rows it labels, with asymmetric block padding (24 above to break from the previous region, 8 below to bind the label to its group).",
7
7
  "element": "h3",
8
8
  "props": {
9
9
  "label": {
@@ -40,7 +40,7 @@
40
40
  },
41
41
  "label": {
42
42
  "required": false,
43
- "description": "Section label text. `<h3>` by default (overridable via `as`). Typo `sys.typo.label.md` (14 / Semibold); color `sys.color.onSurfaceVariant`. Single line.",
43
+ "description": "Section label text. `<h3>` by default (overridable via `as`). Typo `sys.typo.label.md` (14 / Semibold); color `sys.color.text.subtle`. Single line.",
44
44
  "accepts": ["text"]
45
45
  },
46
46
  "action": {
@@ -53,7 +53,7 @@
53
53
  "appearance": {
54
54
  "containerFill": "transparent",
55
55
  "labelTypo": "sys.typo.label.md",
56
- "labelColor": "sys.color.onSurfaceVariant",
56
+ "labelColor": "sys.color.text.subtle",
57
57
  "paddingInline": "sys.layout.container.md",
58
58
  "paddingBlockStart": "sys.layout.stack.lg",
59
59
  "paddingBlockEnd": "sys.layout.stack.xs",
@@ -2,7 +2,7 @@
2
2
 
3
3
  > 🇰🇷 한국어: [`i18n/ko/schema/components/list/accordion.md`](../../../i18n/ko/schema/components/list/accordion.md)
4
4
 
5
- Expandable-row List sub. Each item exposes a List-row trigger (label + auto-rendered trailing chevron that rotates `180°` on expand) and a body that paints below it when open. Rows tile flush with the family hairline `outlineVariant` divider between them; an extra rule paints between the open trigger and its child row group.
5
+ Expandable-row List sub. Each item exposes a List-row trigger (label + auto-rendered trailing chevron that rotates `180°` on expand) and a body that paints below it when open. Rows tile flush with the family hairline `border.default` divider between them; an extra rule paints between the open trigger and its child row group.
6
6
 
7
7
  **Reach for this when** a list of titled sections is too long to keep open at once — FAQs, T&C sections, expandable filter groups, settings groups with infrequent edits, hierarchical menus (companies → channels, regions → cities). **Skip when** bodies are short enough to read inline (use [Carousel](../carousel/carousel.md) per group), the user needs to act on labels (use a [List/standard](./standard.md) drill-in row or [List/radio](./radio.md)), or every item should be visible at once (stack [Carousel](../carousel/carousel.md)s).
8
8
 
@@ -12,7 +12,7 @@ Row geometry, label typography, divider, state overlays, and inward focus ring a
12
12
 
13
13
  ## Default
14
14
 
15
- Single-mode form. One item open at a time; clicking an open item collapses it (`collapsible={true}` by default).
15
+ A single expandable row — the atomic Accordion item. Click the trigger to expand its body; click again to collapse (`collapsible` by default). The Group / Multiple sections below stack several. `type="single"` keeps one item open at a time.
16
16
 
17
17
  ```preview
18
18
  accordion/default
@@ -23,42 +23,12 @@ import { Accordion } from '@teamblind-chorus/ui';
23
23
  <Accordion.Item value="why" label="Why does Blind anonymise posts?">
24
24
  Anonymity is the only way employees can compare salaries, escalate misconduct, or ask candid culture questions without retaliation. Verified company badges keep the channel trustworthy without unmasking the author.
25
25
  </Accordion.Item>
26
- <Accordion.Item value="verify" label="How is my company verified?">
27
- Sign up with your corporate email — the verification code lands in your inbox, never on a public profile. Once verified, the badge persists across job changes (we re-verify when you update your employer).
28
- </Accordion.Item>
29
- <Accordion.Item value="data" label="What can other users see?">
30
- Other users see your company badge, your career stage, and the content you post. Your email, name, and exact title are never exposed.
31
- </Accordion.Item>
32
- </Accordion>
33
- ```
34
-
35
- ## Use cases
36
-
37
- ### Multiple
38
-
39
- `type="multiple"` lets the user open any number of items at once. Use when rows are independent and the user reads across several — comparing T&C clauses, expanding filter groups, reviewing policy sections.
40
-
41
- ```preview
42
- accordion/multiple
43
- ---
44
- import { Accordion } from '@teamblind-chorus/ui';
45
-
46
- <Accordion type="multiple" defaultValue={['salary', 'tenure']} aria-label="Active filters">
47
- <Accordion.Item value="salary" label="Compensation">
48
- Filter posts by salary range, equity, sign-on bonus.
49
- </Accordion.Item>
50
- <Accordion.Item value="tenure" label="Tenure">
51
- Filter authors by years at current company.
52
- </Accordion.Item>
53
- <Accordion.Item value="role" label="Role">
54
- Filter by IC level, manager track, or specialty.
55
- </Accordion.Item>
56
26
  </Accordion>
57
27
  ```
58
28
 
59
- ### Nested list
29
+ ## Nested list
60
30
 
61
- When the body holds a same-kind row group rather than prose — directory sub-entries, settings sub-options, filter children, menu sub-items — drop a `<List>` into the content slot with `embedded={true}` so the list defers its own chrome to the host. The body recognises the embedded child via `:has([data-embedded='true'])` and switches to compact-host geometry: body inline padding flips from prose's `32 leading / 16 trailing` to `16 / 0` (sub-list stretches flush right), a hairline `outlineVariant` top group-divider paints via the body's `::before`, and sub-list rows compress to `body.sm` at `40px` min-height with inter-row dividers suppressed. Call sites pass no extra mode prop — dropping `<List embedded>` (or a nested `<Accordion embedded>`) activates it automatically.
31
+ When the body holds a same-kind row group rather than prose — directory sub-entries, settings sub-options, filter children, menu sub-items — drop a `<List>` into the content slot with `embedded={true}` so the list defers its own chrome to the host. The body recognises the embedded child via `:has([data-embedded='true'])` and switches to compact-host geometry: body inline padding flips from prose's `32 leading / 16 trailing` to `16 / 0` (sub-list stretches flush right), a hairline `border.default` top group-divider paints via the body's `::before`, and sub-list rows compress to `body.sm` at `40px` min-height with inter-row dividers suppressed. Call sites pass no extra mode prop — dropping `<List embedded>` (or a nested `<Accordion embedded>`) activates it automatically.
62
32
 
63
33
  ```preview
64
34
  accordion/nested-list
@@ -107,7 +77,7 @@ import { Accordion, List } from '@teamblind-chorus/ui';
107
77
  </Accordion>
108
78
  ```
109
79
 
110
- ### Disabled item
80
+ ## Disabled item
111
81
 
112
82
  A `disabled` row fades to `sys.state.disabled` opacity and ignores click / keyboard activation. Stays in the DOM so surrounding items keep their stable index.
113
83
 
@@ -123,8 +93,27 @@ import { Accordion } from '@teamblind-chorus/ui';
123
93
  <Accordion.Item value="billing" label="Billing" disabled>
124
94
  Available for verified enterprise accounts only.
125
95
  </Accordion.Item>
126
- <Accordion.Item value="notifications" label="Notifications">
127
- Email, push, and in-app notification preferences.
96
+ </Accordion>
97
+ ```
98
+
99
+ ## Group
100
+
101
+ `type="multiple"` lets the user open any number of items at once. Use when rows are independent and the user reads across several — comparing T&C clauses, expanding filter groups, reviewing policy sections.
102
+
103
+ ```preview
104
+ accordion/multiple
105
+ ---
106
+ import { Accordion } from '@teamblind-chorus/ui';
107
+
108
+ <Accordion type="multiple" defaultValue={['salary', 'tenure']} aria-label="Active filters">
109
+ <Accordion.Item value="salary" label="Compensation">
110
+ Filter posts by salary range, equity, sign-on bonus.
111
+ </Accordion.Item>
112
+ <Accordion.Item value="tenure" label="Tenure">
113
+ Filter authors by years at current company.
114
+ </Accordion.Item>
115
+ <Accordion.Item value="role" label="Role">
116
+ Filter by IC level, manager track, or specialty.
128
117
  </Accordion.Item>
129
118
  </Accordion>
130
119
  ```
@@ -132,25 +121,25 @@ import { Accordion } from '@teamblind-chorus/ui';
132
121
  ## Slots
133
122
 
134
123
  - **container** — outer stack. Transparent fill so the host surface tone reads through. `role="region"` carries the accordion's accessible name (`aria-label`).
135
- - **item** — single expandable row. Hairline `outlineVariant` divider inset 16px on both inline edges, painted as an `::after` overlay on every row except the last.
124
+ - **item** — single expandable row. Hairline `border.default` divider inset 16px on both inline edges, painted as an `::after` overlay on every row except the last.
136
125
  - **trigger** — header button. Holds the label and the auto-rendered trailing chevron. Same geometry as a List row (48px min-height, 8 × 16 padding). `aria-expanded` reflects open-state, `aria-controls` references the content region.
137
126
  - **label** — trigger label. `16 / Regular / onSurface` — matches the List family `label` spec so the trigger reads as a List row that happens to expand. Wraps to a second line; no truncation.
138
127
  - **chevron** — auto-rendered 16px `ChevronDownIcon`. Rotates `0°` → `180°` over 120ms `ease-out` on expand. Decorative.
139
- - **content** — body region. Paints below the trigger when open; toggled via the `hidden` attribute when closed. `min-height: 40px` keeps short single-line bodies on a touch-target rhythm. Two padding modes by content kind: *prose body* (text, icon, button, form-field) uses `32 leading / 16 trailing` inline padding so prose reads as nested inside the trigger's label column, body text at `body.sm` (one rung below the trigger label), no top group-divider. *Embedded row group* (`<List embedded>` or nested `<Accordion embedded>`, detected via `:has([data-embedded='true'])`) uses `16 / 0` inline padding with a hairline `outlineVariant` top divider via `::before`; sub-list rows compress to `body.sm` at `40px` min-height with no inter-row dividers.
128
+ - **content** — body region. Paints below the trigger when open; toggled via the `hidden` attribute when closed. `min-height: 40px` keeps short single-line bodies on a touch-target rhythm. Two padding modes by content kind: *prose body* (text, icon, button, form-field) uses `32 leading / 16 trailing` inline padding so prose reads as nested inside the trigger's label column, body text at `body.sm` (one rung below the trigger label), no top group-divider. *Embedded row group* (`<List embedded>` or nested `<Accordion embedded>`, detected via `:has([data-embedded='true'])`) uses `16 / 0` inline padding with a hairline `border.default` top divider via `::before`; sub-list rows compress to `body.sm` at `40px` min-height with no inter-row dividers.
140
129
 
141
130
  ## Anatomy
142
131
 
143
132
  | Slot | Token bindings |
144
133
  |---------------|----------------|
145
134
  | container | Transparent fill, no padding (full-bleed, edge-to-edge) |
146
- | item | Hairline `outlineVariant` divider inset 16px on both inline edges, omitted on the last row — family-wide List divider |
135
+ | item | Hairline `border.default` divider inset 16px on both inline edges, omitted on the last row — family-wide List divider |
147
136
  | trigger | 48px min-height, 8px block / 16px inline padding, full-row click target |
148
137
  | label | `16 / Regular`, `onSurface` — matches the List `label` spec |
149
138
  | chevron | 16 × 16, `onSurfaceVariant`, rotates 0° → 180° over 120ms `ease-out` |
150
139
  | content (prose) | 8px block padding, `32 leading / 16 trailing` inline padding, `min-height: 40px`, `sys.typo.body.sm` (14 / Regular) at `onSurfaceVariant` |
151
140
  | content (embedded group) | 8px block padding, `16 leading / 0 trailing` inline padding; sub-list rows render at `body.sm` (14 / Regular), `min-height: 40px`, with `::after` row dividers suppressed |
152
- | divider | `sys.borderWidth.hairline` × `sys.color.outlineVariant`, inset 16px on both inline edges via `::after` overlay |
153
- | groupDivider | `sys.borderWidth.hairline` × `sys.color.outlineVariant`, inset 16px on both inline edges via `::before` overlay on the content body, painted ONLY when the body hosts a `<List embedded>` child group |
141
+ | divider | `sys.borderWidth.hairline` × `sys.color.border.default`, inset 16px on both inline edges via `::after` overlay |
142
+ | groupDivider | `sys.borderWidth.hairline` × `sys.color.border.default`, inset 16px on both inline edges via `::before` overlay on the content body, painted ONLY when the body hosts a `<List embedded>` child group |
154
143
 
155
144
  ## Appearance
156
145
 
@@ -167,14 +156,14 @@ A single appearance — Accordion paints no fill of its own and offers no emphas
167
156
 
168
157
  ## Focus indicator
169
158
 
170
- Inward 3-layer ring painted inside the trigger's footprint via a `::before` overlay. Trigger: `:focus-visible`. Items tile flush with only a hairline divider between them, so an outward ring would overlap the divider and the neighbouring row — see [Focus ring composition](../../DESIGN.md#focus-ring-composition).
159
+ Inward single ring painted inside the trigger's footprint via a `::before` overlay. Trigger: `:focus-visible`. Items tile flush with only a hairline divider between them, so an outward ring would overlap the divider and the neighbouring row — see [Focus ring composition](../../DESIGN.md#focus-ring-composition).
171
160
 
172
161
  ## Behavior
173
162
 
174
163
  - **Edge-to-edge composition.** `layoutInset: full-bleed` — direct child of the page shell. Wrapping in another `padding-inline` / `px-*` div double-pays the rail. Use the negative-margin opt-out inside a bounded surface.
175
- - **Inset divider.** 1px `outlineVariant` rule inset 16px on both inline edges — same as every other List sub.
164
+ - **Inset divider.** 1px `border.default` rule inset 16px on both inline edges — same as every other List sub.
176
165
  - **Indented prose.** Expanded body sits one extra 16px in from the trigger's label edge for parent ↔ child hierarchy.
177
- - **Embedded groups switch the body to compact-host geometry.** When the body hosts a `<List embedded>`, three things change at once: body inline padding flips from `32 / 16` to `16 / 0` (sub-list flush right); a hairline `outlineVariant` rule paints at the body's top edge so parent and child read as a hierarchy; sub-list rows compress to `body.sm` at `40px` min-height with no inter-row dividers.
166
+ - **Embedded groups switch the body to compact-host geometry.** When the body hosts a `<List embedded>`, three things change at once: body inline padding flips from `32 / 16` to `16 / 0` (sub-list flush right); a hairline `border.default` rule paints at the body's top edge so parent and child read as a hierarchy; sub-list rows compress to `body.sm` at `40px` min-height with no inter-row dividers.
178
167
  - **Whole trigger is the click target.** Chevron is decorative.
179
168
  - **Element swap.** Trigger is `<button>`; content region is `<div role="region">` with `hidden` toggled.
180
169
  - **Keyboard.** Space / Enter toggle. Arrow up/down moves focus between triggers.
@@ -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": {
@@ -158,20 +158,29 @@
158
158
  "pressed": {
159
159
  "overlay": { "color": "label", "opacity": "sys.state.pressed" }
160
160
  },
161
+ "focused": {
162
+ "focusRing": {
163
+ "composition": "inward",
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.border.focused" },
166
+ "outerRing": { "width": "sys.borderWidth.thin", "color": "sys.color.border.focused" }
167
+ },
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
+ },
161
170
  "disabled": {
162
- "containerOpacity": "sys.state.disabled",
163
- "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."
164
175
  }
165
176
  },
166
177
  "focusIndicator": {
167
- "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.",
168
179
  "composition": "inward",
169
- "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.",
170
181
  "ring": {
171
- "outerWidth": "sys.borderWidth.thin",
172
- "outerColor": "sys.color.focus",
173
- "insetWidth": "sys.borderWidth.hairline",
174
- "insetColor": "sys.color.focusInset",
182
+ "width": "sys.borderWidth.hairline",
183
+ "color": "sys.color.border.focused",
175
184
  "implementation": "inset box-shadow on the trigger's `::before` overlay."
176
185
  },
177
186
  "trigger": ":focus-visible (keyboard / programmatic focus, never plain mouse click)"
@@ -194,7 +203,7 @@
194
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",
195
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",
196
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.",
197
- "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.",
198
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.",
199
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"
200
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