@teamblind-chorus/ui 1.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 (191) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +112 -0
  3. package/agents/AGENTS.md +143 -0
  4. package/agents/DESIGN.md +1311 -0
  5. package/agents/LOVABLE.md +472 -0
  6. package/agents/anti-patterns.md +533 -0
  7. package/agents/catalog.md +232 -0
  8. package/agents/components/avatar-rail/avatar-rail.family.json +46 -0
  9. package/agents/components/avatar-rail/avatar-rail.md +103 -0
  10. package/agents/components/avatar-rail/avatar-rail.spec.json +160 -0
  11. package/agents/components/badge/badge.family.json +45 -0
  12. package/agents/components/badge/badge.md +10 -0
  13. package/agents/components/badge/role.md +100 -0
  14. package/agents/components/badge/role.spec.json +75 -0
  15. package/agents/components/badge/update.md +132 -0
  16. package/agents/components/badge/update.spec.json +114 -0
  17. package/agents/components/banner/banner.family.json +28 -0
  18. package/agents/components/banner/banner.md +136 -0
  19. package/agents/components/banner/banner.spec.json +136 -0
  20. package/agents/components/bottom-sheet/bottom-sheet.family.json +29 -0
  21. package/agents/components/bottom-sheet/bottom-sheet.md +176 -0
  22. package/agents/components/bottom-sheet/bottom-sheet.spec.json +168 -0
  23. package/agents/components/bubble/bubble.family.json +29 -0
  24. package/agents/components/bubble/bubble.md +134 -0
  25. package/agents/components/bubble/bubble.spec.json +91 -0
  26. package/agents/components/button/button.family.json +76 -0
  27. package/agents/components/button/button.md +31 -0
  28. package/agents/components/button/check.md +138 -0
  29. package/agents/components/button/check.spec.json +161 -0
  30. package/agents/components/button/fab.md +161 -0
  31. package/agents/components/button/fab.spec.json +106 -0
  32. package/agents/components/button/icon.md +141 -0
  33. package/agents/components/button/icon.spec.json +164 -0
  34. package/agents/components/button/standard.md +219 -0
  35. package/agents/components/button/standard.spec.json +205 -0
  36. package/agents/components/button/text.md +186 -0
  37. package/agents/components/button/text.spec.json +215 -0
  38. package/agents/components/button/toggle.md +108 -0
  39. package/agents/components/button/toggle.spec.json +124 -0
  40. package/agents/components/button/toolbar.md +189 -0
  41. package/agents/components/button/toolbar.spec.json +109 -0
  42. package/agents/components/carousel/carousel.family.json +41 -0
  43. package/agents/components/carousel/carousel.md +40 -0
  44. package/agents/components/carousel/post.md +148 -0
  45. package/agents/components/carousel/post.spec.json +229 -0
  46. package/agents/components/carousel/profile.md +184 -0
  47. package/agents/components/carousel/profile.spec.json +219 -0
  48. package/agents/components/chip/chip.family.json +37 -0
  49. package/agents/components/chip/chip.md +10 -0
  50. package/agents/components/chip/filter.md +212 -0
  51. package/agents/components/chip/filter.spec.json +124 -0
  52. package/agents/components/chip/tag.md +137 -0
  53. package/agents/components/chip/tag.spec.json +104 -0
  54. package/agents/components/dialog/dialog.family.json +29 -0
  55. package/agents/components/dialog/dialog.md +113 -0
  56. package/agents/components/dialog/dialog.spec.json +156 -0
  57. package/agents/components/directory-list/directory-list.family.json +46 -0
  58. package/agents/components/directory-list/directory-list.md +87 -0
  59. package/agents/components/directory-list/directory-list.spec.json +104 -0
  60. package/agents/components/divider/divider.family.json +28 -0
  61. package/agents/components/divider/divider.md +78 -0
  62. package/agents/components/divider/divider.spec.json +51 -0
  63. package/agents/components/feed/ad.md +108 -0
  64. package/agents/components/feed/ad.spec.json +187 -0
  65. package/agents/components/feed/feed.family.json +48 -0
  66. package/agents/components/feed/feed.md +30 -0
  67. package/agents/components/feed/post.md +240 -0
  68. package/agents/components/feed/post.spec.json +361 -0
  69. package/agents/components/form-field/form-field.family.json +50 -0
  70. package/agents/components/form-field/form-field.md +11 -0
  71. package/agents/components/form-field/input.md +198 -0
  72. package/agents/components/form-field/input.spec.json +202 -0
  73. package/agents/components/form-field/search.md +81 -0
  74. package/agents/components/form-field/search.spec.json +135 -0
  75. package/agents/components/form-field/select.md +101 -0
  76. package/agents/components/form-field/select.spec.json +194 -0
  77. package/agents/components/form-field/textarea.md +89 -0
  78. package/agents/components/form-field/textarea.spec.json +176 -0
  79. package/agents/components/header/header.family.json +43 -0
  80. package/agents/components/header/header.md +18 -0
  81. package/agents/components/header/main.md +101 -0
  82. package/agents/components/header/main.spec.json +117 -0
  83. package/agents/components/header/sub.md +129 -0
  84. package/agents/components/header/sub.spec.json +81 -0
  85. package/agents/components/list/accordion.md +183 -0
  86. package/agents/components/list/accordion.spec.json +201 -0
  87. package/agents/components/list/entry.md +280 -0
  88. package/agents/components/list/entry.spec.json +237 -0
  89. package/agents/components/list/list.family.json +75 -0
  90. package/agents/components/list/list.md +24 -0
  91. package/agents/components/list/radio.md +144 -0
  92. package/agents/components/list/radio.spec.json +186 -0
  93. package/agents/components/list/standard.md +262 -0
  94. package/agents/components/list/standard.spec.json +221 -0
  95. package/agents/components/metadata/compact.md +69 -0
  96. package/agents/components/metadata/compact.spec.json +69 -0
  97. package/agents/components/metadata/metadata.family.json +42 -0
  98. package/agents/components/metadata/metadata.md +26 -0
  99. package/agents/components/metadata/standard.md +104 -0
  100. package/agents/components/metadata/standard.spec.json +152 -0
  101. package/agents/components/nav-card/nav-card.family.json +29 -0
  102. package/agents/components/nav-card/nav-card.md +179 -0
  103. package/agents/components/nav-card/nav-card.spec.json +161 -0
  104. package/agents/components/nav-list/nav-list.family.json +46 -0
  105. package/agents/components/nav-list/nav-list.md +91 -0
  106. package/agents/components/nav-list/nav-list.spec.json +107 -0
  107. package/agents/components/navigation-bar/main.md +201 -0
  108. package/agents/components/navigation-bar/main.spec.json +109 -0
  109. package/agents/components/navigation-bar/navigation-bar.family.json +44 -0
  110. package/agents/components/navigation-bar/navigation-bar.md +21 -0
  111. package/agents/components/navigation-bar/search.md +96 -0
  112. package/agents/components/navigation-bar/search.spec.json +142 -0
  113. package/agents/components/navigation-bar/sub.md +174 -0
  114. package/agents/components/navigation-bar/sub.spec.json +123 -0
  115. package/agents/components/page-shell/page-shell.family.json +22 -0
  116. package/agents/components/page-shell/page-shell.md +51 -0
  117. package/agents/components/profile-header/profile-header.family.json +29 -0
  118. package/agents/components/profile-header/profile-header.md +149 -0
  119. package/agents/components/profile-header/profile-header.spec.json +200 -0
  120. package/agents/components/progress/progress.family.json +27 -0
  121. package/agents/components/progress/progress.md +38 -0
  122. package/agents/components/progress/progress.spec.json +67 -0
  123. package/agents/components/side-sheet/side-sheet.family.json +30 -0
  124. package/agents/components/side-sheet/side-sheet.md +154 -0
  125. package/agents/components/side-sheet/side-sheet.spec.json +109 -0
  126. package/agents/components/skeleton/skeleton.family.json +28 -0
  127. package/agents/components/skeleton/skeleton.md +123 -0
  128. package/agents/components/skeleton/skeleton.spec.json +73 -0
  129. package/agents/components/status-tag/status-tag.family.json +26 -0
  130. package/agents/components/status-tag/status-tag.md +114 -0
  131. package/agents/components/status-tag/status-tag.spec.json +69 -0
  132. package/agents/components/suggestion-list/suggestion-list.family.json +46 -0
  133. package/agents/components/suggestion-list/suggestion-list.md +91 -0
  134. package/agents/components/suggestion-list/suggestion-list.spec.json +178 -0
  135. package/agents/components/switch/switch.family.json +27 -0
  136. package/agents/components/switch/switch.md +114 -0
  137. package/agents/components/switch/switch.spec.json +123 -0
  138. package/agents/components/tab-bar/tab-bar.family.json +27 -0
  139. package/agents/components/tab-bar/tab-bar.md +178 -0
  140. package/agents/components/tab-bar/tab-bar.spec.json +184 -0
  141. package/agents/components/tabs/rounded.md +150 -0
  142. package/agents/components/tabs/rounded.spec.json +140 -0
  143. package/agents/components/tabs/segmented.md +114 -0
  144. package/agents/components/tabs/segmented.spec.json +100 -0
  145. package/agents/components/tabs/tabs.family.json +59 -0
  146. package/agents/components/tabs/tabs.md +18 -0
  147. package/agents/components/tabs/underline.md +147 -0
  148. package/agents/components/tabs/underline.spec.json +139 -0
  149. package/agents/components/thumbnail/thumbnail.family.json +28 -0
  150. package/agents/components/thumbnail/thumbnail.md +152 -0
  151. package/agents/components/thumbnail/thumbnail.spec.json +172 -0
  152. package/agents/components/toast/toast.family.json +28 -0
  153. package/agents/components/toast/toast.md +133 -0
  154. package/agents/components/toast/toast.spec.json +89 -0
  155. package/agents/components/tooltip/tooltip.family.json +29 -0
  156. package/agents/components/tooltip/tooltip.md +139 -0
  157. package/agents/components/tooltip/tooltip.spec.json +110 -0
  158. package/agents/compose.md +240 -0
  159. package/agents/icons.json +831 -0
  160. package/agents/images.md +66 -0
  161. package/agents/manifest.json +87 -0
  162. package/agents/patterns/README.md +59 -0
  163. package/agents/patterns/actions.md +50 -0
  164. package/agents/patterns/browsing.md +52 -0
  165. package/agents/patterns/communications.md +56 -0
  166. package/agents/patterns/layout.md +72 -0
  167. package/agents/patterns/modals.md +50 -0
  168. package/agents/patterns/visual.md +55 -0
  169. package/agents/reconstruct.md +55 -0
  170. package/agents/scoped-adoption.md +111 -0
  171. package/agents/tokens.usage.json +1657 -0
  172. package/agents/usage.json +422 -0
  173. package/dist/icons/index.cjs +1332 -0
  174. package/dist/icons/index.cjs.map +1 -0
  175. package/dist/icons/index.d.cts +228 -0
  176. package/dist/icons/index.d.ts +228 -0
  177. package/dist/icons/index.js +1114 -0
  178. package/dist/icons/index.js.map +1 -0
  179. package/dist/index.cjs +5905 -0
  180. package/dist/index.cjs.map +1 -0
  181. package/dist/index.d.cts +896 -0
  182. package/dist/index.d.ts +896 -0
  183. package/dist/index.js +5847 -0
  184. package/dist/index.js.map +1 -0
  185. package/dist/styles.css +5765 -0
  186. package/eslint/README.md +79 -0
  187. package/eslint/index.js +78 -0
  188. package/eslint/rules.js +472 -0
  189. package/eslint/test.mjs +135 -0
  190. package/package.json +96 -0
  191. package/placeholder.png +0 -0
@@ -0,0 +1,101 @@
1
+ # Main
2
+
3
+ The louder member of the [Header](./header.md) family — a labelled `onSurface` heading + an optional trailing affordance, reached as `<Header>`. The composable header anatomy reused across [Carousel](../carousel/carousel.md), in-sheet sub-sections, bounded cards, [SideSheet](../side-sheet/side-sheet.md) drawer columns, and any host that needs a leading title + one trailing commit.
4
+
5
+ **Reach for this when** a labelled block needs an `onSurface` heading and at most one trailing commit (See-all link, drill-in chevron, or sort dropdown). **Skip when** the block needs only a quiet muted group label — that is the family's [Sub](./sub.md) (`<SubHeader>`); when the block has multiple actions; the trailing affordance is a destructive commit; or the surrounding host already owns a heading at the same rung.
6
+
7
+ **Layout inset.** `full-bleed` — transparent background, owns its inline padding internally (the same model as the family's [Sub](./sub.md) member). The inline inset is `container.md` (16) — the same content rail the List rows / Feed items it heads pay — so a bare `<Header>` and the content beneath it land on one shared rail with no host help; asymmetric block padding `stack.lg` (24) above / `stack.md` (16) below carries the heading's own vertical rhythm. Drop it directly above the labelled region as a sibling, never inside a `padding-inline` wrapper, and don't pay the rail again on the host. A bundling host (Carousel, DirectoryList, NavList, SideSheet column) stays full-bleed — it adds no inline padding of its own — and absorbs only Header's block padding into its stack rhythm: Header's 24-above replaces the host's leading inset, its 16-below is the header↔body gap (one gutter, paid once).
8
+
9
+ ## Default
10
+
11
+ Section-style heading with a trailing Text Button "See all" link. Two sizes diverge on label typography: `large` = `sys.typo.heading.md` (20 / Semibold), `medium` = `sys.typo.heading.sm` (16 / Semibold).
12
+
13
+ ```preview
14
+ header/main/default
15
+ ---
16
+ import { Header } from '@teamblind-chorus/ui';
17
+
18
+ <Header
19
+ label="Recommended channels"
20
+ headerAction={{ label: 'See all', href: '#all' }}
21
+ />
22
+ ```
23
+
24
+ ## Use cases
25
+
26
+ ### With drill-in chevron
27
+
28
+ `trailingIcon` mode — the chevron is a [Button `variant="icon"`](../button/icon.md) at `size="medium"` (32 × 32 capsule, 16-glyph) that owns its own tap target. The chevron paints in `onSurfaceVariant` and rotates -90° to read as chevron-right. The surrounding `<header>` element stays non-interactive — clicks land on the Icon Button, not on the row chrome. Use when the trailing affordance is "open this surface", not "commit a labelled action". `headerAction` and `trailingIcon` are mutually exclusive.
29
+
30
+ ```preview
31
+ header/main/with-icon-action
32
+ ---
33
+ import { Header } from '@teamblind-chorus/ui';
34
+
35
+ <Header
36
+ size="medium"
37
+ label="My channels"
38
+ trailingIcon
39
+ onClick={() => {}}
40
+ />
41
+ ```
42
+
43
+ ### With dropdown
44
+
45
+ `headerDropdown` mode — the trailing affordance is a [Text Button dropdown](../button/text.md#dropdown) (`size="xsmall"`, default appearance) whose **label is the current value** ("Top", "Newest", "All time") and whose trailing chevron flips with `open` as a state signal. Use when the heading row needs an inline sort / filter / range disclosure — "Recommended channels [Top ▾]", "Posts [Last 7 days ▾]". Consumer owns the menu surface (Menu, popover, ListBox) and the `open` state; Header only renders the trigger.
46
+
47
+ `headerAction`, `trailingIcon`, and `headerDropdown` are mutually exclusive — priority is `trailingIcon` > `headerAction` > `headerDropdown` when more than one is set.
48
+
49
+ ```preview
50
+ header/main/with-dropdown
51
+ ---
52
+ import { useState } from 'react';
53
+ import { Header } from '@teamblind-chorus/ui';
54
+
55
+ function Example() {
56
+ const [open, setOpen] = useState(false);
57
+ return (
58
+ <Header
59
+ label="Recommended channels"
60
+ headerDropdown={{
61
+ label: 'Top',
62
+ open,
63
+ onClick: () => setOpen((v) => !v),
64
+ }}
65
+ />
66
+ );
67
+ }
68
+ ```
69
+
70
+ ### Label only
71
+
72
+ Set only `label` (omit `headerAction`, `trailingIcon`, and `headerDropdown`) for a labelled region that needs a heading without a trailing affordance. The row collapses to a single heading at the requested size — no spacer, no empty container.
73
+
74
+ ```preview
75
+ header/main/label-only
76
+ ---
77
+ import { Header } from '@teamblind-chorus/ui';
78
+
79
+ <Header label="Recommended channels" />
80
+ ```
81
+
82
+ ## Slots
83
+
84
+ - **container** — outer row. Flex with `space-between`; label leads, action / icon / dropdown trails, 8px (`layout.inline.md`) gap. Full-bleed — transparent background and its own padding: `container.md` (16) inline (the shared content rail), `stack.lg` (24) block-start, `stack.md` (16) block-end — so the label lands on the same rail as the rows beneath it; bundling hosts add no inline padding of their own and absorb only the block padding. Stays a non-interactive `<header>` / `<div>` — the trailing affordance owns its own hit target.
85
+ - **label** *(optional)* — heading text. `<h3>` by default; override the wrapper with `as="div"` when the surrounding host already owns the heading semantics. Color `sys.color.onSurface`. Typo per size.
86
+ - **action** *(optional, headerAction mode)* — trailing Text Button. Fixed at `size="xsmall"`, `appearance="accent"`.
87
+ - **icon** *(optional, trailingIcon mode)* — trailing [Icon Button](../button/icon.md) (`variant="icon"` `size="medium"`) hosting a 16px glyph (canonical: chevron-right). Its own tap target — clicks land on the Icon Button, not the surrounding header. Supply `aria-label` for the icon-only button (defaults to `"Open <label>"` when `label` is a string).
88
+ - **dropdown** *(optional, headerDropdown mode)* — trailing [Text Button dropdown](../button/text.md#dropdown) (`size="xsmall"`, default appearance). Label is the current value; trailing chevron flips between `ChevronDownIcon` (closed) and `ChevronUpIcon` (open) tied to `headerDropdown.open`. Owns its own tap target — clicks land on the dropdown trigger, not the surrounding header.
89
+
90
+ ## Behavior
91
+
92
+ - **Empty render** — when `label`, `headerAction`, `trailingIcon`, and `headerDropdown` are all omitted, Header renders nothing.
93
+ - **Alignment** — label and trailing affordance share the row's centre line. The label never wraps; the trailing keeps intrinsic width and never grows.
94
+ - **Mode exclusivity** — `headerAction`, `trailingIcon`, and `headerDropdown` are mutually exclusive. Priority when more than one is set: `trailingIcon` > `headerAction` > `headerDropdown`.
95
+
96
+ ## Composition
97
+
98
+ - Inside [Carousel](../carousel/carousel.md) — rendered automatically as `size="large"` + `headerAction` mode.
99
+ - Standalone — drop above any labelled region (`List`, `Feed`, `PostCarousel`, `Banner`, a bounded card body).
100
+ - Inside a [SideSheet](../side-sheet/side-sheet.md) drawer column — `size="medium"` is the canonical rung; pair with `List` compact below.
101
+ - Inside a [BottomSheet](../bottom-sheet/bottom-sheet.md) content slot — `size="medium"` so the heading does not compete with the sheet's own 20-rung title.
@@ -0,0 +1,117 @@
1
+ {
2
+ "$schema": "../../spec.schema.json",
3
+ "name": "Header",
4
+ "family": "header",
5
+ "subcomponent": "main",
6
+ "description": "The loud member of the Header family — a labelled `onSurface` heading + optional trailing 'See all' Text Button. The leading label is rendered as a semantic `<h3>` by default; consumers may override the wrapping element via the `as` prop (e.g. `as=\"div\"` when the host already owns the heading semantics). Two sizes diverge only on the label typography — anatomy (slot grammar, trailing-button binding, alignment) is identical. Reached as `<Header>` (the family's primary export); its quieter sibling is [Sub](./sub.md) (`<SubHeader>`), a muted 14 group label. Full-bleed (mirrors Sub): a transparent background and its own inline padding — `container.md` (16), the same rail the List rows / Feed items it heads pay — so a bare `<Header>` and the content beneath it land on one shared rail with no host help. Asymmetric block padding (`stack.lg` 24 above, `stack.md` 16 below) carries the heading's own vertical rhythm. Hosts that bundle Header above their content (Carousel, DirectoryList, NavList, SideSheet column) stay full-bleed (they add no inline padding of their own) and absorb only the block padding into their stack rhythm — the 'one gutter, paid once' rule.",
7
+ "element": "header",
8
+ "props": {
9
+ "size": {
10
+ "type": "literal",
11
+ "values": ["large", "medium"],
12
+ "default": "large",
13
+ "description": "Label typography rung. `large` (default) — `sys.typo.heading.md` (20 / Semibold) — the canonical section heading used by Section and other top-level page regions. `medium` — `sys.typo.heading.sm` (16 / Semibold) — denser host surfaces (in-sheet sub-section, bounded card, drawer column heading, secondary regions inside an already-headed page)."
14
+ },
15
+ "label": {
16
+ "type": "string",
17
+ "optional": true,
18
+ "description": "Leading heading text. Single line; the consumer truncates upstream if the host width is too narrow."
19
+ },
20
+ "headerAction": {
21
+ "type": "object",
22
+ "optional": true,
23
+ "description": "Trailing Text Button binding. Object: `{ label, href, onClick }`. Rendered as a Text Button at `size=\"xsmall\"`, `appearance=\"accent\"`. The link-affordance rule keeps the navigational intent in the accent tone across hosts. Mutually exclusive with `trailingIcon`."
24
+ },
25
+ "trailingIcon": {
26
+ "type": "node",
27
+ "optional": true,
28
+ "description": "Drill-in [Icon Button](../button/icon.md) mode. Pass `true` to render the canonical chevron-right glyph inside a Button `variant=\"icon\"` `size=\"medium\"` (32 × 32 capsule, 16-glyph), or pass any 16px icon node to override the glyph. The trailing Icon Button owns its own tap target via `onClick` / `href`; the surrounding `<header>` element stays non-interactive. Supply an `aria-label` for the icon-only button (defaults to `\"Open <label>\"` when `label` is a string). Mutually exclusive with `headerAction` and `headerDropdown`."
29
+ },
30
+ "headerDropdown": {
31
+ "type": "object",
32
+ "optional": true,
33
+ "description": "Trailing [Text Button dropdown](../button/text.md#dropdown) binding. Object: `{ label, onClick, open, 'aria-haspopup'?, 'aria-controls'? }`. Rendered as a Text Button at `size=\"xsmall\"` with the default appearance — label IS the current selected value ('Top', 'Last 7 days', 'All time'), and the trailing chevron flips between `ChevronDownIcon` (when `open` is falsy) and `ChevronUpIcon` (when `open` is true) as a state signal. Consumer owns the menu surface and the `open` state. Mutually exclusive with `headerAction` and `trailingIcon` — priority `trailingIcon` > `headerAction` > `headerDropdown` when more than one is set."
34
+ },
35
+ "onClick": {
36
+ "type": "function",
37
+ "optional": true,
38
+ "description": "Click handler. Honored when `trailingIcon` is set — wired to the trailing Icon Button's `onClick`."
39
+ },
40
+ "href": {
41
+ "type": "string",
42
+ "optional": true,
43
+ "description": "Link href. Honored when `trailingIcon` is set — wired to the trailing Icon Button."
44
+ },
45
+ "as": {
46
+ "type": "literal",
47
+ "values": ["header", "div"],
48
+ "default": "header",
49
+ "description": "Wrapping element. Defaults to `<header>` for top-level / labelled-region hosts; use `as=\"div\"` when the surrounding host already carries the section semantics. The wrapper itself is never interactive — in `trailingIcon` mode the click target sits on the trailing Icon Button, not the wrapper."
50
+ }
51
+ },
52
+ "slots": {
53
+ "container": {
54
+ "required": true,
55
+ "description": "Outer row. Flex with `space-between` so the label and the trailing action sit on opposite edges, separated by an 8px (`layout.inline.md`) gap. Full-bleed — transparent background and its own padding: `container.md` (16) inline (the shared content rail), `stack.lg` (24) block-start, `stack.md` (16) block-end — so the label lands on the same rail as the rows beneath it with no host help; bundling hosts add no inline padding of their own and absorb only the block padding."
56
+ },
57
+ "label": {
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`."
60
+ },
61
+ "action": {
62
+ "required": false,
63
+ "description": "Trailing Text Button. Fixed at `size=\"xsmall\"`, `appearance=\"accent\"`. The label string comes from `headerAction.label`. Mutually exclusive with the icon slot below."
64
+ },
65
+ "icon": {
66
+ "required": false,
67
+ "description": "Trailing [Icon Button](../button/icon.md) (Button `variant=\"icon\"` `size=\"medium\"`) hosting a 16px glyph (canonical: chevron-right). Its own tap target — clicks land on the Icon Button, not the surrounding header. Mutually exclusive with the action and dropdown slots.",
68
+ "accepts": ["button"],
69
+ "rendersAs": "button:icon"
70
+ },
71
+ "dropdown": {
72
+ "required": false,
73
+ "description": "Trailing [Text Button dropdown](../button/text.md#dropdown) (`size=\"xsmall\"`, default appearance) when `headerDropdown` is set. Label is `headerDropdown.label`; trailing chevron flips between `ChevronDownIcon` (closed) and `ChevronUpIcon` (open) tied to `headerDropdown.open`. Owns its own tap target — clicks land on the dropdown trigger, not the surrounding header. Mutually exclusive with the action and icon slots.",
74
+ "accepts": ["button"],
75
+ "rendersAs": "button:text (dropdown)"
76
+ }
77
+ },
78
+ "sizing": {
79
+ "containerGap": "sys.layout.inline.md",
80
+ "containerAlign": "center",
81
+ "containerJustify": "space-between",
82
+ "large": {
83
+ "labelTypo": "sys.typo.heading.md (20 / Semibold)"
84
+ },
85
+ "medium": {
86
+ "labelTypo": "sys.typo.heading.sm (16 / Semibold)"
87
+ },
88
+ "labelColor": "sys.color.onSurface",
89
+ "actionSize": "Text Button xsmall, appearance accent",
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)"
92
+ },
93
+ "appearance": {
94
+ "background": "transparent",
95
+ "paddingInline": "sys.layout.container.md",
96
+ "paddingBlockStart": "sys.layout.stack.lg",
97
+ "paddingBlockEnd": "sys.layout.stack.md",
98
+ "note": "Full-bleed: transparent background (Header paints on whatever surface tier hosts it) + its own block & inline padding. Inline `container.md` (16) IS the shared content rail — the same inset the List rows / Feed items beneath the Header pay — so a bare `<Header>` and its content align with no host help. Block is asymmetric — `stack.lg` (24) above to break from the previous region, `stack.md` (16) below to bind the heading to the block it labels. Bundling hosts (Carousel, DirectoryList, NavList, SideSheet column) stay full-bleed — they add no inline padding of their own — and absorb only the block padding into their stack rhythm (Header's 24-above replaces the host's leading inset, its 16-below is the header↔body gap) — the 'one gutter, paid once' rule. NEVER give Header an opaque fill or wrap it in a `padding-inline` div."
99
+ },
100
+ "states": {
101
+ "description": "Header itself has no lifecycle states. The trailing action carries Text Button's hover / pressed / focus."
102
+ },
103
+ "behavior": {
104
+ "rendering": "When `label`, `headerAction`, `trailingIcon`, and `headerDropdown` are all omitted, the component renders nothing (no empty container). When only `label` is set the row collapses to a heading-only Header — the canonical 'labelled region without affordance' case.",
105
+ "alignment": "Label and trailing affordance share the row's centre line; the label never wraps, the affordance keeps intrinsic width and never grows.",
106
+ "linkAffordance": "In `headerAction` mode, the trailing action is always a Text Button — never a Standard Button, never a raw `<a>`, never an icon. In `trailingIcon` mode, the chevron is rendered as a Button `variant=\"icon\"` `size=\"medium\"` that owns its own tap target — the surrounding `<header>` element is decorative and never interactive. In `headerDropdown` mode, the trigger is rendered as a Text Button `size=\"xsmall\"` with a state-signalling chevron, and the consumer owns the menu surface and the `open` state.",
107
+ "modeExclusivity": "`headerAction`, `trailingIcon`, and `headerDropdown` are mutually exclusive — priority when more than one is set: `trailingIcon` > `headerAction` > `headerDropdown`. Pick one mode per host."
108
+ },
109
+ "forbidden": [
110
+ "non-Text-Button trailing action in `headerAction` mode (Standard Button, Icon Button, raw <a>)",
111
+ "label larger than heading-md or smaller than heading-sm — pick the size dial, do not custom-tune the typo",
112
+ "headerAction without an href or onClick — the link affordance must commit somewhere",
113
+ "trailingIcon without an onClick or href — an icon-only header that is non-interactive is decorative noise; reach for a labelled Section header instead",
114
+ "headerDropdown without `label` (the trigger's label IS the current value — empty label is meaningless) or without `onClick` (the trigger must commit somewhere)",
115
+ "multiple trailing modes together (headerAction + trailingIcon, headerAction + headerDropdown, trailingIcon + headerDropdown) — pick one"
116
+ ]
117
+ }
@@ -0,0 +1,129 @@
1
+ # Sub
2
+
3
+ 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.
4
+
5
+ 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.
6
+
7
+ **Reach for this when** a stack of rows splits into named groups and the boundary needs a quiet word, not a loud heading — a followed set above a suggested set, settings rows clustered by theme — optionally with a single quiet "See all" / "Edit" commit on the trailing edge. **Skip when** the region needs a louder section *heading* (`onSurface`, 16 / 20) or a drill-in chevron / sort dropdown — that is [Main](./main.md); when the break between two regions is purely visual with no label — that is [Divider](../divider/divider.md); or when the group label belongs *inside* a bounded surface that already carries its own heading rung. The split from Main is the tone/weight axis (muted label vs heading), not whether a trailing commit exists.
8
+
9
+ **Layout inset.** `full-bleed` — SubHeader pays its own `16px` (`sys.layout.container.md`) inline inset so the label aligns with the full-bleed [List](../list/list.md) rows it heads, plus `24px` block padding above (to break from the previous region) and `8px` below (to bind the label to its group). Drop it as a direct child of the page shell directly above the group it labels; do **not** wrap it in another `padding-inline` / `px-*` / `style={{ padding }}` div, or the label lands at a different inset than the rows beneath it. Inside a bounded surface (Card / Dialog / BottomSheet / SideSheet), apply the negative-margin opt-out — see [`AGENTS.md` § Composition rules](../../../AGENTS.md#composition-rules).
10
+
11
+ ## Default
12
+
13
+ A single muted group label. Pair it directly above the rows it names.
14
+
15
+ ```preview
16
+ header/sub/default
17
+ ---
18
+ import { SubHeader } from '@teamblind-chorus/ui';
19
+
20
+ <SubHeader label="Following" />
21
+ ```
22
+
23
+ ## Use cases
24
+
25
+ ### Grouping a list
26
+
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".
28
+
29
+ ```preview
30
+ header/sub/grouping-a-list
31
+ ---
32
+ import { useState } from 'react';
33
+ import { SubHeader, List } from '@teamblind-chorus/ui';
34
+
35
+ function Example() {
36
+ const [value, setValue] = useState('product');
37
+ return (
38
+ <div style={{ background: 'var(--sys-color-surface)', display: 'flex', flexDirection: 'column' }}>
39
+ <SubHeader label="Following" />
40
+ <List
41
+ variant="radio"
42
+ value={value}
43
+ onChange={setValue}
44
+ aria-label="Following"
45
+ items={[
46
+ { value: 'tech', label: 'Tech Industry' },
47
+ { value: 'swe', label: 'Software Engineering Career' },
48
+ { value: 'product', label: 'Product Management' },
49
+ { value: 'design', label: 'Design Career' },
50
+ ]}
51
+ />
52
+ <SubHeader label="More Topics to follow" />
53
+ <List
54
+ variant="radio"
55
+ value={value}
56
+ onChange={setValue}
57
+ aria-label="More topics to follow"
58
+ items={[
59
+ { value: 'misc', label: 'Misc.' },
60
+ { value: 'visa', label: 'Work Visa' },
61
+ { value: 'jobs', label: 'Job Opening' },
62
+ { value: 'startups', label: 'Startups' },
63
+ ]}
64
+ />
65
+ </div>
66
+ );
67
+ }
68
+ ```
69
+
70
+ ### With a trailing action
71
+
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.
73
+
74
+ ```preview
75
+ header/sub/with-action
76
+ ---
77
+ import { SubHeader, List } from '@teamblind-chorus/ui';
78
+
79
+ <div style={{ background: 'var(--sys-color-surface)', display: 'flex', flexDirection: 'column' }}>
80
+ <SubHeader label="Following" action={{ label: 'See all', href: '#all' }} />
81
+ <List
82
+ aria-label="Following"
83
+ items={[
84
+ { label: 'Tech Industry' },
85
+ { label: 'Software Engineering Career' },
86
+ { label: 'Product Management' },
87
+ ]}
88
+ />
89
+ </div>
90
+ ```
91
+
92
+ ### Host owns the heading
93
+
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.
95
+
96
+ ```preview
97
+ header/sub/as-div
98
+ ---
99
+ import { SubHeader } from '@teamblind-chorus/ui';
100
+
101
+ <SubHeader as="div" label="More Topics to follow" />
102
+ ```
103
+
104
+ ## Slots
105
+
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.
107
+ - **label** — required. Section label text. `<h3>` by default (override via `as`). Typo `sys.typo.label.md` (14 / Semibold); color `sys.color.onSurfaceVariant`. Single line.
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.
109
+
110
+ ## Anatomy
111
+
112
+ | Slot | Token bindings |
113
+ |-----------|----------------|
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 |
115
+ | label | `sys.typo.label.md` (14 / Semibold) typo, `sys.color.onSurfaceVariant` color |
116
+ | action | [Text Button](../button/text.md) `size="xsmall"`, `appearance="accent"`; intrinsic width, never grows |
117
+
118
+ ## Appearance
119
+
120
+ Single appearance — SubHeader has no size axis and no emphasis axis. The muted `onSurfaceVariant` tone at the 14 / Semibold `label.md` rung is the dividing device; a tone down from Header's `onSurface` heading so the label reads as a region marker, not a competing title. When an `action` is present the trailing Text Button supplies the one pop of accent; the label tone is unchanged.
121
+
122
+ **Transparent background.** The container paints no fill — it is `transparent` — so the label composes directly on whatever surface tier hosts it (`surface`, `surfaceContainer`, a tonal band, a Sheet body) and stays harmonious on any background. The dividing device is the muted tone, never a painted fill; do not give SubHeader an opaque background.
123
+
124
+ ## Behavior
125
+
126
+ - **Empty render.** When neither `label` nor `children` is set, SubHeader renders nothing — no empty container, even if `action` is set (a trailing button with no label is meaningless).
127
+ - **Single action only.** SubHeader carries at most one trailing affordance, and only a Text Button (`action`). It supports no icon drill-in and no sort dropdown — those stay [Main](./main.md)'s, where the louder `onSurface` heading earns the heavier affordance set. The split from Main is the tone/weight axis (muted label at 14 vs heading at 16 / 20), not whether a trailing commit exists.
128
+ - **Action semantics.** In `action` mode the wrapper is a non-semantic flex `<div>` and the label keeps its heading element (`as`) nested on the leading edge, so the trailing button never folds into the heading's accessible name. The button owns its own tap target; the row chrome stays non-interactive.
129
+ - **Heading by default.** The label renders as `<h3>` so screen-reader heading navigation lands on the group label. Override `as` to fit the host's outline, or `as="div"` when the host already owns the section semantics.
@@ -0,0 +1,81 @@
1
+ {
2
+ "$schema": "../../spec.schema.json",
3
+ "name": "SubHeader",
4
+ "family": "header",
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).",
7
+ "element": "h3",
8
+ "props": {
9
+ "label": {
10
+ "type": "string",
11
+ "optional": true,
12
+ "description": "The section label text (\"Following\", \"More Topics to follow\"). Single line; the consumer keeps it short. `children` may be passed instead and takes precedence — `label` is the canonical string form."
13
+ },
14
+ "action": {
15
+ "type": "object",
16
+ "optional": true,
17
+ "description": "Trailing Text Button binding. Object: `{ label, href, onClick }`. Rendered as a Text Button at `size=\"xsmall\"`, `appearance=\"accent\"` — the one trailing affordance SubHeader carries, for a quiet region-level commit (\"See all\", \"Edit\", \"Manage\"). When set, the wrapper becomes a full-bleed flex row and the label is nested as the heading (`as`) on the leading edge so the heading's accessible name never absorbs the button. Mirrors Header's `headerAction` binding; SubHeader supports no icon or dropdown mode (those stay Header's, where the louder heading tone belongs)."
18
+ },
19
+ "as": {
20
+ "type": "literal",
21
+ "values": ["h2", "h3", "h4", "div"],
22
+ "default": "h3",
23
+ "description": "Wrapping element for the label. Defaults to `<h3>` so the muted label still registers as a heading for the group beneath it; use a different heading level to fit the host's document outline, or `as=\"div\"` when the surrounding host already carries the section semantics. The label element is never interactive — in `action` mode the click target sits on the trailing Text Button, and the flex-row wrapper that holds both is a non-semantic `<div>`."
24
+ },
25
+ "children": {
26
+ "type": "node",
27
+ "optional": true,
28
+ "description": "Label content — overrides `label` when both are set. Use the `label` string for the canonical case; `children` is the escape hatch for inline composition."
29
+ },
30
+ "className": {
31
+ "type": "string",
32
+ "optional": true,
33
+ "description": "Composes with the component's own class. Use sparingly — SubHeader exposes its tone and inset through the spec; overriding them breaks the alignment-with-rows and muted-divider contracts."
34
+ }
35
+ },
36
+ "slots": {
37
+ "container": {
38
+ "required": true,
39
+ "description": "The full-bleed label line. Pays `sys.layout.container.md` (16) inline inset to align with the List rows beneath, `sys.layout.stack.lg` (24) block padding above and `sys.layout.stack.xs` (8) below. Non-interactive. Without `action` the container IS the heading element (`as`); with `action` it is a non-semantic flex `<div>` (`space-between`, `sys.layout.inline.md` gap) holding the nested label and the trailing action on opposite edges."
40
+ },
41
+ "label": {
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.",
44
+ "accepts": ["text"]
45
+ },
46
+ "action": {
47
+ "required": false,
48
+ "description": "Optional trailing Text Button. Fixed at `size=\"xsmall\"`, `appearance=\"accent\"`; label comes from `action.label`. Owns its own tap target — clicks land on the button, not the surrounding row. Present only when `action` is set.",
49
+ "accepts": ["button"],
50
+ "rendersAs": "button:text"
51
+ }
52
+ },
53
+ "appearance": {
54
+ "containerFill": "transparent",
55
+ "labelTypo": "sys.typo.label.md",
56
+ "labelColor": "sys.color.onSurfaceVariant",
57
+ "paddingInline": "sys.layout.container.md",
58
+ "paddingBlockStart": "sys.layout.stack.lg",
59
+ "paddingBlockEnd": "sys.layout.stack.xs",
60
+ "note": "The container paints NO fill — `transparent` — so the muted label composes directly on whatever surface tier it sits on (surface, surfaceContainer, a tonal band, a Sheet body) and stays harmonious on any host. The dividing device is the tone, not a painted background. The muted `onSurfaceVariant` tone is a tone down from Header's `onSurface` heading so the label reads as a region marker, not a competing title. The 14 / Semibold `label.md` rung is the section-label weight: smaller than any Header heading (16 / 20) but emphatic enough to read as a label over the rows beneath it. Asymmetric block padding (24 above / 8 below) breaks the label from the previous region and binds it to the group below."
61
+ },
62
+ "behavior": {
63
+ "rendering": "When neither `label` nor `children` is set, SubHeader renders nothing (no empty container) — even when `action` is set, since a trailing button with no label is meaningless.",
64
+ "singleAction": "SubHeader carries at most one trailing affordance, and only a Text Button (`action`). It supports no icon drill-in and no dropdown disclosure — those stay Header's, where the louder `onSurface` heading tone reads as a heading-with-affordance rather than a quiet labelled region. The distinction from Header is the tone/weight axis (muted `onSurfaceVariant` label at 14 vs `onSurface` heading at 16/20), not the presence of a trailing commit.",
65
+ "actionSemantics": "In `action` mode the wrapper is a non-semantic flex `<div>` and the label keeps its heading element (`as`) nested on the leading edge, so the trailing Text Button never folds into the heading's accessible name. The button owns its own tap target; the row chrome is non-interactive.",
66
+ "fullBleed": "Pays its own 16 (`container.md`) inline inset so the label aligns with the full-bleed List rows it heads. Must not be wrapped in a padding-inline container, or the label drifts off the row inset."
67
+ },
68
+ "states": {
69
+ "description": "SubHeader itself has no lifecycle states — the label is a static, non-interactive marker. When `action` is set, the trailing Text Button carries its own hover / pressed / focus."
70
+ },
71
+ "forbidden": [
72
+ "SubHeader painted in `onSurface` or any non-muted tone — the muted `onSurfaceVariant` IS the dividing device; a full-strength tone makes it compete with the page heading (reach for Header instead)",
73
+ "SubHeader given an opaque container fill (`surface`, `surfaceContainer`, any background) — the container is `transparent` so the label harmonizes on any host surface tier; a painted fill clashes the moment it sits on a non-surface band",
74
+ "SubHeader given a trailing icon drill-in or dropdown disclosure — only a single Text Button `action` is allowed; an icon/dropdown affordance belongs on Header",
75
+ "SubHeader given more than one trailing action — the row carries a single quiet commit, not a button cluster",
76
+ "SubHeader `action` rendered as anything but a Text Button (Standard Button, Icon Button, raw <a>) — the link affordance keeps the navigational intent in the accent tone, mirroring Header",
77
+ "SubHeader typo dialled above `label.md` (14 / Semibold) — a larger or louder rung is a Header heading, not a quiet group label",
78
+ "SubHeader wrapped in a padding-inline div or `style={{ paddingInline }}` — it is full-bleed and pays its own 16 inset to align with the rows beneath",
79
+ "SubHeader rendered as a raw <p> / <span> with Tailwind — the label owns its tone, inset, and block rhythm through the spec"
80
+ ]
81
+ }
@@ -0,0 +1,183 @@
1
+ # Accordion
2
+
3
+ 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.
4
+
5
+ **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).
6
+
7
+ Row geometry, label typography, divider, state overlays, and inward focus ring all delegate to the [family-wide rules](./list.md); this sub documents only the expand/collapse contract and the top group-divider between trigger and child rows.
8
+
9
+ **Layout inset.** `full-bleed` — direct child of the page shell, edge-to-edge. Each row pays its own `16px inline / 8px block` padding via `layout.container.*`. Do **not** wrap in another `padding-inline` / `px-*` / `style={{ padding: … }}` div — the page rail will double-pay. Inside a bounded surface, apply the negative-margin opt-out — see [`AGENTS.md` § Composition rules](../../../AGENTS.md#composition-rules).
10
+
11
+ ## Default
12
+
13
+ Single-mode form. One item open at a time; clicking an open item collapses it (`collapsible={true}` by default).
14
+
15
+ ```preview
16
+ accordion/default
17
+ ---
18
+ import { Accordion } from '@teamblind-chorus/ui';
19
+
20
+ <Accordion type="single" defaultValue="why" aria-label="Frequently asked questions">
21
+ <Accordion.Item value="why" label="Why does Blind anonymise posts?">
22
+ 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.
23
+ </Accordion.Item>
24
+ <Accordion.Item value="verify" label="How is my company verified?">
25
+ 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).
26
+ </Accordion.Item>
27
+ <Accordion.Item value="data" label="What can other users see?">
28
+ Other users see your company badge, your career stage, and the content you post. Your email, name, and exact title are never exposed.
29
+ </Accordion.Item>
30
+ </Accordion>
31
+ ```
32
+
33
+ ## Use cases
34
+
35
+ ### Multiple
36
+
37
+ `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.
38
+
39
+ ```preview
40
+ accordion/multiple
41
+ ---
42
+ import { Accordion } from '@teamblind-chorus/ui';
43
+
44
+ <Accordion type="multiple" defaultValue={['salary', 'tenure']} aria-label="Active filters">
45
+ <Accordion.Item value="salary" label="Compensation">
46
+ Filter posts by salary range, equity, sign-on bonus.
47
+ </Accordion.Item>
48
+ <Accordion.Item value="tenure" label="Tenure">
49
+ Filter authors by years at current company.
50
+ </Accordion.Item>
51
+ <Accordion.Item value="role" label="Role">
52
+ Filter by IC level, manager track, or specialty.
53
+ </Accordion.Item>
54
+ </Accordion>
55
+ ```
56
+
57
+ ### Nested list
58
+
59
+ 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.
60
+
61
+ ```preview
62
+ accordion/nested-list
63
+ ---
64
+ import { Accordion, List } from '@teamblind-chorus/ui';
65
+
66
+ <Accordion type="single" defaultValue="invest-cast" aria-label="Company directory">
67
+ <Accordion.Item value="invest-fund" label="Invest Fund">
68
+ <List
69
+ variant="standard"
70
+ embedded
71
+ aria-label="Invest Fund roles"
72
+ items={[
73
+ { value: 'invest-fund-analyst', label: 'Invest Fund + Analyst' },
74
+ { value: 'invest-fund-associate', label: 'Invest Fund + Associate' },
75
+ { value: 'invest-fund-partner', label: 'Invest Fund + Partner' },
76
+ ]}
77
+ />
78
+ </Accordion.Item>
79
+ <Accordion.Item value="invest-cast" label="Invest Cast">
80
+ <List
81
+ variant="standard"
82
+ embedded
83
+ aria-label="Invest Cast roles"
84
+ items={[
85
+ { value: 'invest-cast-acctg', label: 'Invest Cast + Acctg' },
86
+ { value: 'invest-cast-admin', label: 'Invest Cast + Admin' },
87
+ { value: 'invest-cast-biz-dev', label: 'Invest Cast + Biz Dev' },
88
+ { value: 'invest-cast-consultant', label: 'Invest Cast + Consultant' },
89
+ { value: 'invest-cast-creative', label: 'Invest Cast + Creative' },
90
+ ]}
91
+ />
92
+ </Accordion.Item>
93
+ <Accordion.Item value="invest-financial" label="INVEST Financial Corporation">
94
+ <List
95
+ variant="standard"
96
+ embedded
97
+ aria-label="INVEST Financial Corporation roles"
98
+ items={[
99
+ { value: 'invest-fin-advisor', label: 'INVEST Financial + Advisor' },
100
+ { value: 'invest-fin-compliance',label: 'INVEST Financial + Compliance' },
101
+ { value: 'invest-fin-ops', label: 'INVEST Financial + Ops' },
102
+ ]}
103
+ />
104
+ </Accordion.Item>
105
+ </Accordion>
106
+ ```
107
+
108
+ ### Disabled item
109
+
110
+ 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.
111
+
112
+ ```preview
113
+ accordion/disabled-item
114
+ ---
115
+ import { Accordion } from '@teamblind-chorus/ui';
116
+
117
+ <Accordion type="single" aria-label="Account settings">
118
+ <Accordion.Item value="profile" label="Profile">
119
+ Display name, avatar, bio.
120
+ </Accordion.Item>
121
+ <Accordion.Item value="billing" label="Billing" disabled>
122
+ Available for verified enterprise accounts only.
123
+ </Accordion.Item>
124
+ <Accordion.Item value="notifications" label="Notifications">
125
+ Email, push, and in-app notification preferences.
126
+ </Accordion.Item>
127
+ </Accordion>
128
+ ```
129
+
130
+ ## Slots
131
+
132
+ - **container** — outer stack. Transparent fill so the host surface tone reads through. `role="region"` carries the accordion's accessible name (`aria-label`).
133
+ - **item** — single expandable row. Hairline `outlineVariant` divider inset 16px on both inline edges, painted as an `::after` overlay on every row except the last.
134
+ - **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.
135
+ - **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.
136
+ - **chevron** — auto-rendered 16px `ChevronDownIcon`. Rotates `0°` → `180°` over 120ms `ease-out` on expand. Decorative.
137
+ - **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.
138
+
139
+ ## Anatomy
140
+
141
+ | Slot | Token bindings |
142
+ |---------------|----------------|
143
+ | container | Transparent fill, no padding (full-bleed, edge-to-edge) |
144
+ | item | Hairline `outlineVariant` divider inset 16px on both inline edges, omitted on the last row — family-wide List divider |
145
+ | trigger | 48px min-height, 8px block / 16px inline padding, full-row click target |
146
+ | label | `16 / Regular`, `onSurface` — matches the List `label` spec |
147
+ | chevron | 16 × 16, `onSurfaceVariant`, rotates 0° → 180° over 120ms `ease-out` |
148
+ | content (prose) | 8px block padding, `32 leading / 16 trailing` inline padding, `min-height: 40px`, `sys.typo.body.sm` (14 / Regular) at `onSurfaceVariant` |
149
+ | 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 |
150
+ | divider | `sys.borderWidth.hairline` × `sys.color.outlineVariant`, inset 16px on both inline edges via `::after` overlay |
151
+ | 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 |
152
+
153
+ ## Appearance
154
+
155
+ A single appearance — Accordion paints no fill of its own and offers no emphasis axis. The host surface tone reads through every row. Wrap the Accordion in a [Carousel](../carousel/carousel.md) when it needs its own labelled region.
156
+
157
+ ## States
158
+
159
+ | State | Overlay | Additional |
160
+ |------------|-------------------------------|------------|
161
+ | `default` | — | Trigger paints at rest. |
162
+ | `hovered` | label tone at `sys.state.hover` | Overlay paints across the trigger row only (not the open content). |
163
+ | `pressed` | label tone at `sys.state.pressed` | Overlay deepens; no other shift. |
164
+ | `disabled` | overlay suppressed | Whole item at `sys.state.disabled` opacity; `pointer-events: none`. |
165
+
166
+ ## Focus indicator
167
+
168
+ 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).
169
+
170
+ ## Behavior
171
+
172
+ - **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.
173
+ - **Inset divider.** 1px `outlineVariant` rule inset 16px on both inline edges — same as every other List sub.
174
+ - **Indented prose.** Expanded body sits one extra 16px in from the trigger's label edge for parent ↔ child hierarchy.
175
+ - **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.
176
+ - **Whole trigger is the click target.** Chevron is decorative.
177
+ - **Element swap.** Trigger is `<button>`; content region is `<div role="region">` with `hidden` toggled.
178
+ - **Keyboard.** Space / Enter toggle. Arrow up/down moves focus between triggers.
179
+ - **Single mode.** Opening one item closes the previously open one. With `collapsible={true}` (default), clicking the open item closes it.
180
+ - **Multiple mode.** Each trigger toggles independently. `value` is a `string[]`.
181
+ - **Wrap, not truncate.** Trigger labels wrap to a second line; the row grows.
182
+ - **Content min-height.** Open content carries a `40px` floor so short bodies read as a distinct expanded row.
183
+ - **Reduced motion.** Chevron rotation snaps instantly under `prefers-reduced-motion: reduce`.