@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,113 @@
1
+ # Dialog
2
+
3
+ A focused, opt-in interruption — a centred card over a scrim that holds the flow until the user makes a single decision.
4
+
5
+ **Reach for this when** the flow must pause for a definitive response — destructive actions, conflicts, consent gates, "Are you sure?". **Skip when** the same nudge can be delivered without halting the flow (use [Bottom sheet](../bottom-sheet/bottom-sheet.md)), the message is contextual to underlying content ([Banner](../banner/banner.md)), or the confirmation is post-action ([Toast](../toast/toast.md)).
6
+
7
+ **Layout inset.** `bounded-surface` — its own modal shell. Owns its outer padding; not a sibling of `full-bleed` page rows. A `full-bleed` child placed inside (List / Feed / Chip group) MUST opt out via `marginInline: 'calc(-1 * var(--sys-layout-container-md))'` (matching `width`, `maxWidth: 'none'`) so its own row padding becomes the visual inset and the dialog title aligns with row leading content. See [`AGENTS.md` § Composition rules](../../../AGENTS.md#composition-rules).
8
+
9
+ ## Default
10
+
11
+ A confirmation-style dialog with title, body, and stacked primary / tertiary actions. No image, so the text left-aligns.
12
+
13
+ ```preview
14
+ dialog/default
15
+ ---
16
+ import { useState } from 'react';
17
+ import { Dialog, Button } from '@teamblind-chorus/ui';
18
+
19
+ const [open, setOpen] = useState(false);
20
+
21
+ <>
22
+ <Button appearance="primary" onClick={() => setOpen(true)}>Open dialog</Button>
23
+ <Dialog
24
+ open={open}
25
+ onClose={() => setOpen(false)}
26
+ title="Pick your major and get tailored job recommendations"
27
+ body="We'll surface the companies and roles your seniors applied to most often, first."
28
+ primaryAction={{ label: 'Pick major', onClick: () => setOpen(false) }}
29
+ secondaryAction={{ label: 'Later', onClick: () => setOpen(false) }}
30
+ />
31
+ </>
32
+ ```
33
+
34
+ ## Use cases
35
+
36
+ ### With image
37
+
38
+ Pass an `image` to add a centred illustration between title and actions. With the image present, the whole stack centre-aligns. `imageFirst` (default `true`) controls whether the image sits under the title or the body.
39
+
40
+ ```preview
41
+ dialog/with-image
42
+ ---
43
+ import { useState } from 'react';
44
+ import { Dialog, Button } from '@teamblind-chorus/ui';
45
+
46
+ const [open, setOpen] = useState(false);
47
+
48
+ <>
49
+ <Button appearance="primary" onClick={() => setOpen(true)}>Open dialog</Button>
50
+ <Dialog
51
+ open={open}
52
+ onClose={() => setOpen(false)}
53
+ title="You earned the Sourdough Starter badge"
54
+ body="Three open-crumb bakes shared this week — your channel just lit up."
55
+ image={{ src: '/badge.png', alt: 'Sourdough Starter badge' }}
56
+ primaryAction={{ label: 'Share to channel', onClick: () => setOpen(false) }}
57
+ secondaryAction={{ label: 'Not now', onClick: () => setOpen(false) }}
58
+ />
59
+ </>
60
+ ```
61
+
62
+ ## Slots
63
+
64
+ - **scrim** — translucent black overlay; clicking fires `onClose`. Holds the card to a 40px inset from the wrapper's left and right edges.
65
+ - **container** — dialog card. `surfaceContainerHigh` fill, `radius.xl`, `elevation.overlay`. Caps at `max-width: 480px`. Vertical stack: title, (optional) image, body, actions.
66
+ - **title** — short headline. `heading.sm` / Semibold / `onSurface`. Always sits at the top.
67
+ - **image** *(optional)* — illustrative image between title and actions. Spans the inner width; preserves intrinsic aspect ratio.
68
+ - **body** *(optional)* — one-paragraph supporting copy. `body.sm` / Regular / `onSurfaceVariant`.
69
+ - **actions** — vertical stack at the bottom; primary on top, secondary below. Both stretch full inner width. Primary = `appearance="primary"`; secondary = `appearance="tertiary"`.
70
+
71
+ ## Anatomy
72
+
73
+ | Slot | Token bindings |
74
+ |--------------|----------------|
75
+ | scrim | Fixed full-viewport overlay, `palette.black.600` (~24% alpha), centres container, fixed 40px inline padding |
76
+ | container | `surfaceContainerHigh` fill, `radius.xl` (16px), `elevation.overlay`, `sys.layout.container.lg` padding, `max-width: 480px` |
77
+ | title | `sys.typo.heading.sm` (16 / Semibold), `onSurface` |
78
+ | image | `width: 100%`, `height: auto`, `radius.lg` corners, 16px above and below |
79
+ | body | `sys.typo.body.sm` (14 / Regular), `onSurfaceVariant`, 16px above |
80
+ | actions | Flex column, 8px between buttons, 16px above the stack |
81
+ | primary CTA | [Button](../button/button.md) `appearance="primary"`, `size="large"`, `fullWidth` |
82
+ | secondary | [Button](../button/button.md) `appearance="tertiary"`, `size="large"`, `fullWidth` |
83
+
84
+ **Inter-slot rhythm.** Every gap between adjacent card children is `sys.layout.stack.md` (16px).
85
+
86
+ ## Alignment
87
+
88
+ - **With image** — title, body, and stack centre-align (`text-align: center`, `align-items: center`).
89
+ - **Text only** — title and body left-align (`text-align: start`, `align-items: stretch`).
90
+
91
+ Actions row is always stretched to the inner width.
92
+
93
+ ## Sizes
94
+
95
+ A single rung. Card holds a fixed `max-width: 480px` and 40px gutter; below that breakpoint it fills available width.
96
+
97
+ ## States
98
+
99
+ Dialog is either **open** or **closed** — closed renders nothing. When open, the underlying surface is non-interactive behind the scrim.
100
+
101
+ The container itself has no interactive state — interaction lives in the action buttons.
102
+
103
+ ## Focus indicator
104
+
105
+ Dialog itself isn't a focus target; action slots inherit [Button → Outward](../button/button.md#focus-indicator) focus composition. On open, focus moves to the primary action. Trigger: `:focus-visible`.
106
+
107
+ ## Behavior
108
+
109
+ - **Scrim click closes.** Card stops propagation.
110
+ - **Escape key closes.** Bound at the document level.
111
+ - **Focus management.** On open, focus moves to the primary action; returns to the previously-focused element on close.
112
+ - **Body scroll lock.** Page underneath does not scroll while the dialog is open.
113
+ - **Portal rendering.** Renders into a portal at `document.body` by default. Pass `inline` to scope to the nearest positioned ancestor (must declare `position: relative` and `overflow: hidden`).
@@ -0,0 +1,156 @@
1
+ {
2
+ "$schema": "../../spec.schema.json",
3
+ "name": "Dialog",
4
+ "family": "dialog",
5
+ "description": "Focused, opt-in interruption — a centred card over a scrim that asks the user to make a single decision before continuing. Reach for Dialog when the surface needs an explicit commit / dismiss pair (onboarding nudge, confirm-before-destructive) and the answer can fit in a short title + one supporting paragraph. Anything longer than two action buttons or a paragraph of supporting copy belongs in a full screen or sheet, not a dialog.",
6
+ "element": "div",
7
+ "props": {
8
+ "open": {
9
+ "type": "boolean",
10
+ "required": true
11
+ },
12
+ "onClose": {
13
+ "type": "function",
14
+ "required": true
15
+ },
16
+ "title": {
17
+ "type": "node",
18
+ "required": true
19
+ },
20
+ "body": {
21
+ "type": "node",
22
+ "optional": true
23
+ },
24
+ "image": {
25
+ "type": "object",
26
+ "optional": true,
27
+ "description": "{ src, alt } — illustrative image between the title and the actions."
28
+ },
29
+ "imageFirst": {
30
+ "type": "boolean",
31
+ "default": true,
32
+ "description": "When true (default), the image sits directly under the title; when false, under the body."
33
+ },
34
+ "primaryAction": {
35
+ "type": "object",
36
+ "optional": true,
37
+ "description": "{ label, onClick } — delegates to Button appearance='primary', size='large', fullWidth."
38
+ },
39
+ "secondaryAction": {
40
+ "type": "object",
41
+ "optional": true,
42
+ "description": "{ label, onClick } — delegates to Button appearance='tertiary', size='large', fullWidth."
43
+ },
44
+ "inline": {
45
+ "type": "boolean",
46
+ "default": false,
47
+ "description": "When true, scopes the scrim and card to the nearest positioned ancestor instead of portaling to document.body."
48
+ }
49
+ },
50
+ "slots": {
51
+ "scrim": {
52
+ "required": true,
53
+ "description": "Translucent black overlay covering the host surface; centres the container with flex. Clicking fires onClose. Holds the card to a 40px inset from the wrapper's left and right edges.",
54
+ "intrinsic": true
55
+ },
56
+ "container": {
57
+ "required": true,
58
+ "description": "The dialog card. surfaceContainerHigh fill, radius.xl, elevation.overlay. Fills the available width up to max-width: 480px.",
59
+ "intrinsic": true
60
+ },
61
+ "title": {
62
+ "required": true,
63
+ "description": "Short headline. heading.sm / Semibold / onSurface. Always sits at the top of the stack.",
64
+ "accepts": [
65
+ "text"
66
+ ]
67
+ },
68
+ "image": {
69
+ "required": false,
70
+ "description": "Single illustrative image between title and actions. Spans the card's inner width and preserves intrinsic aspect ratio.",
71
+ "accepts": [
72
+ "image"
73
+ ]
74
+ },
75
+ "body": {
76
+ "required": false,
77
+ "description": "One-paragraph supporting copy. body.sm / Regular / onSurfaceVariant.",
78
+ "accepts": [
79
+ "text"
80
+ ]
81
+ },
82
+ "actions": {
83
+ "required": false,
84
+ "description": "Vertical stack of action buttons at the bottom of the card. Primary on top, secondary below. Both stretch to inner width.",
85
+ "accepts": [
86
+ "button"
87
+ ]
88
+ }
89
+ },
90
+ "sizing": {
91
+ "scrimTint": "ref.palette.black.600",
92
+ "scrimPaddingInline": "ref.space.500",
93
+ "containerFill": "sys.color.surfaceContainerHigh",
94
+ "containerRadius": "sys.radius.xl",
95
+ "elevation": "sys.elevation.overlay",
96
+ "containerPadding": "sys.layout.container.lg",
97
+ "maxWidth": "480px",
98
+ "titleTypo": "sys.typo.heading.sm",
99
+ "titleColor": "sys.color.onSurface",
100
+ "imageWidth": "100%",
101
+ "imageHeight": "auto",
102
+ "imageRadius": "sys.radius.lg",
103
+ "bodyTypo": "sys.typo.body.sm",
104
+ "bodyColor": "sys.color.onSurfaceVariant",
105
+ "interSlotGap": "sys.layout.stack.md",
106
+ "actionsStackGap": "sys.layout.stack.xs"
107
+ },
108
+ "alignment": {
109
+ "withImage": {
110
+ "textAlign": "center",
111
+ "alignItems": "center",
112
+ "note": "The image's visual mass anchors the composition; centred copy reads as a unified illustrated card."
113
+ },
114
+ "textOnly": {
115
+ "textAlign": "start",
116
+ "alignItems": "stretch",
117
+ "note": "Left alignment matches the reading rhythm of the body copy."
118
+ },
119
+ "actions": "Always stretched to the card's inner width regardless of alignment."
120
+ },
121
+ "states": {
122
+ "open": {
123
+ "description": "Scrim + card both render; underlying surface remains in DOM but pointer events suppressed by the scrim's full-viewport overlay."
124
+ },
125
+ "closed": {
126
+ "description": "Renders nothing — no reserved whitespace, no hidden DOM."
127
+ }
128
+ },
129
+ "focusIndicator": {
130
+ "description": "The dialog itself isn't a focus target. Its action slots (primary / secondary Buttons) inherit Button's outward focus composition; on open, focus is moved to the primary action. See the contained sub-components for the visual contract.",
131
+ "composition": "delegated",
132
+ "delegatesTo": "../button/standard.spec.json#/focusIndicator",
133
+ "trigger": ":focus-visible (keyboard / programmatic focus, never plain mouse click)"
134
+ },
135
+ "behavior": {
136
+ "scrimClickCloses": "Tapping anywhere on the scrim fires onClose. The card stops click propagation so a tap inside never closes the dialog by accident.",
137
+ "escapeKeyCloses": "Pressing Esc fires onClose. Bound at the document level so the dialog doesn't need to own focus to receive the key.",
138
+ "focusManagement": "On open, focus moves into the dialog and is trapped there (see accessibility.focusTrap). For a standard dialog focus lands on the primary action so Enter commits; for a destructive confirmation (role='alertdialog') focus lands on the least-destructive action (the cancel / dismiss control) so the dangerous commit is never one keystroke away. Focus returns to the previously-focused element on close (consumer-managed via onClose).",
139
+ "bodyScrollLock": true,
140
+ "portalRendering": "By default renders into a portal at document.body. Pass inline to scope to the nearest positioned ancestor; that ancestor must declare position: relative and overflow: hidden."
141
+ },
142
+ "accessibility": {
143
+ "role": "container carries role='dialog'. For a destructive / irreversible confirmation it is role='alertdialog' instead, so assistive tech announces it as a decision that requires attention.",
144
+ "ariaModal": "aria-modal='true' on the container — assistive tech treats content outside the dialog as inert while it is open (mirrors the scrim's pointer-event block in the a11y tree).",
145
+ "labelling": "aria-labelledby points at the title slot's id (always present, title is required). aria-describedby points at the body slot's id when a body is rendered.",
146
+ "focusTrap": "While open, Tab / Shift+Tab cycle only through the dialog's focusable descendants and wrap at the ends — focus can never reach the inert page behind the scrim. Required by the APG dialog-modal pattern; pairs with the scrim's pointer-event block so keyboard and pointer are gated identically.",
147
+ "initialFocus": "Standard dialog → primary action. Destructive (alertdialog) → least-destructive action (cancel / dismiss).",
148
+ "escape": "Esc fires onClose (see behavior.escapeKeyCloses)."
149
+ },
150
+ "forbidden": [
151
+ "Dialog without a scrim — dialog is a page-blocking modal and requires the scrim layer",
152
+ "Dialog without a paired triggering action — modal opens are always commit-triggered, never auto-on-mount",
153
+ "Dialog body without a primary action — every dialog has a commit (even informational dialogs carry an Acknowledge / Close action)",
154
+ "Dialog stacked on top of another dialog — pre-overlay flow is sequential, not stacked"
155
+ ]
156
+ }
@@ -0,0 +1,46 @@
1
+ {
2
+ "$schema": "../../family.schema.json",
3
+ "family": "directory-list",
4
+ "name": "DirectoryList",
5
+ "description": "Vertical follow-list — labelled block where each row pairs a 48 thumbnail, identity column (name + followers + description), and a trailing Follow toggle. **Preset wrapper** over `<Header /> + <List variant=\"entry\" size=\"large\" divider={false}>` — no new tokens, no new selection model; reach for it when you want the canonical Follow-able directory shape with item-descriptor sugar (`name → label`, `followers → secondary`, `active/onToggle → trailingIcon`). If you need any divergence from that preset (different rung, different trailing affordance, mixed-thumbnail rows, custom divider behavior), drop down to `<Header /> + <List variant=\"entry\" />` and compose freely. Sibling of SuggestionList: same entity-agnostic row anatomy, but no swipeable pager — the full set is scanned vertically.",
6
+ "useCases": [
7
+ "follow-able directory of entities",
8
+ "new / recommended channels (full list)",
9
+ "people you may know",
10
+ "browse / explore index"
11
+ ],
12
+ "visualReuse": "open",
13
+ "layoutInset": "full-bleed",
14
+ "wrapperGuidance": "Owns its inline padding internally. Place as a direct child of the page-shell <main> (or any host that pays the gutter once). Do NOT wrap in a padding-inline div, className=\"px-*\", or style={{ padding }} — the page rail is paid once at the shell, never on the full-bleed child. Inside a bounded surface (Dialog / BottomSheet / SideSheet), apply the negative-margin opt-out — see AGENTS.md § Composition rules.",
15
+ "compositionModes": {
16
+ "standalone": {
17
+ "default": true,
18
+ "chrome": {
19
+ "background": "sys.color.surface",
20
+ "padding": "sys.layout.container.lg block / sys.layout.container.md inline"
21
+ },
22
+ "context": "Direct child of the page shell or any host that pays the page-rail gutter once."
23
+ },
24
+ "embedded": {
25
+ "trigger": "prop `embedded={true}` on <DirectoryList /> OR direct child of `.chorus-carousel` / `.chorus-feed` (DOM-ancestry safety net).",
26
+ "chrome": {
27
+ "background": "transparent",
28
+ "padding": "0"
29
+ },
30
+ "context": "Composed inside another rail-responsible host (`<Carousel label=\"New channels\"><DirectoryList embedded /></Carousel>`). Row content takes over from the host's content-box edge."
31
+ }
32
+ },
33
+ "usage": {
34
+ "note": "Rows are an `items` array of descriptors (name/followers/description/thumbnail/active/onToggle) — there are NO List or row children.",
35
+ "example": "<DirectoryList label=\"…\" items={[{ value, name, followers, description, thumbnail: { src, alt }, active, onToggle }]} />"
36
+ },
37
+ "spec": "directory-list.md",
38
+ "subcomponents": [
39
+ {
40
+ "slug": "directory-list",
41
+ "spec": "directory-list.spec.json",
42
+ "md": "directory-list.md",
43
+ "default": true
44
+ }
45
+ ]
46
+ }
@@ -0,0 +1,87 @@
1
+ # Directory list
2
+
3
+ A vertical follow-list — labelled block where each row pairs a 48 [Thumbnail](../thumbnail/thumbnail.md), an identity column (name + `secondary` followers + `description`), and a trailing [Toggle Button](../button/toggle.md) flipping between "Follow" and "Following". Anatomy is entity-agnostic — channels, people, companies, topics share one shape.
4
+
5
+ **Preset wrapper.** Internally this is `<Header /> + <List variant="entry" size="large" divider={false}>`. The wrapper exists to (a) map entity-flavored item keys (`name → label`, `followers → secondary`, `active/onToggle → trailingIcon`) and (b) lock the rung + divider preset for the canonical Follow-able directory shape. There is no new visual grammar — just a pinned set of `List` props plus item-key sugar.
6
+
7
+ **Reach for `<DirectoryList />` when** you want the canonical Follow-able directory shape verbatim (48 rung, no inter-row divider, Follow Toggle trailing). **Drop down to `<Header /> + <List variant="entry" />` instead when** you need any divergence — different size rung, mixed label-only and thumbnail rows, a different trailing affordance (chevron / icon button / star), per-row dividers, or a non-toggle commit pattern. **Skip when** the surface is a peek of three with horizontal paging (use [SuggestionList](../suggestion-list/suggestion-list.md)) or a label-only category index ([NavList](../nav-list/nav-list.md)).
8
+
9
+ **Layout inset.** `full-bleed` — sits as a direct child of the page shell (or any host that pays the gutter). Container pays its own `24px block / 16px inline` padding; each row keeps the list/entry native `16px inline padding` for the tap target and pulls its inline margin by `-16` so the visible avatar lines up with the header label at 16 from the surface. Do **not** wrap in another `padding-inline` / `px-*` / `style={{ padding: … }}` div. Inside a bounded surface (Card / Dialog / BottomSheet / Sheet), apply the negative-margin opt-out — see [`AGENTS.md` § Composition rules](../../../AGENTS.md#composition-rules).
10
+
11
+ ## Default
12
+
13
+ Header label only, five follow-able channels at the `large` rung.
14
+
15
+ ```preview
16
+ directory-list/default
17
+ ---
18
+ import { DirectoryList } from '@teamblind-chorus/ui';
19
+
20
+ <DirectoryList
21
+ label="New channels"
22
+ items={[
23
+ { value: 'devsplan', name: 'Devs Plan', followers: '2.1K Followers', description: 'Chat about Devs Plan launches and tips.', thumbnail: { alt: 'Devs Plan' } },
24
+ { value: 'passnotes', name: 'Pass Notes Together', followers: '1.4K Followers', description: 'Ghosting Blind threads on offers and promo packs.', thumbnail: { alt: 'Pass Notes Together' }, active: true },
25
+ { value: 'cars', name: 'Cars', followers: '8.7K Followers', description: 'Daily-driver picks, trims, and ownership notes.', thumbnail: { alt: 'Cars' } },
26
+ { value: 'beauty', name: 'Beauty Bursts', followers: '4.2K Followers', description: 'Routines, cushion drops, scent threads.', thumbnail: { alt: 'Beauty Bursts' } },
27
+ { value: 'health', name: 'Health · Diet', followers: '11.3K Followers', description: 'Programs, macros, and weekday-meal logistics.', thumbnail: { alt: 'Health · Diet' } },
28
+ ]}
29
+ />
30
+ ```
31
+
32
+ ## Use cases
33
+
34
+ ### With header action
35
+
36
+ Extends the header with a trailing `accent` Text Button when the screen has a broader index page to route to.
37
+
38
+ ```preview
39
+ directory-list/with-header-action
40
+ ---
41
+ import { DirectoryList } from '@teamblind-chorus/ui';
42
+
43
+ <DirectoryList
44
+ label="People you may know"
45
+ headerAction={{ label: 'See all', href: '/people' }}
46
+ items={[
47
+ { value: 'jordan', name: 'Jordan Lee', followers: '342 Followers', description: 'PM at a logistics startup. Mostly here for roadmap reviews.', thumbnail: { alt: 'Jordan Lee' } },
48
+ { value: 'taylor', name: 'Taylor Brooks', followers: '1.1K Followers', description: 'Frontend engineer. Writes about the bits between framework and user.', thumbnail: { alt: 'Taylor Brooks' }, active: true },
49
+ { value: 'morgan', name: 'Morgan Park', followers: '512 Followers', description: 'Designer-turned-PM. Notes on the handoff layer.', thumbnail: { alt: 'Morgan Park' } },
50
+ ]}
51
+ />
52
+ ```
53
+
54
+ ## Slots
55
+
56
+ - **container** — `surface` block with 24px block / 16px inline padding. Holds the header above the vertical list.
57
+ - **header** — [Header](../header/header.md) `size="large"`. Section label leading, optional accent Text Button trailing. Anchored above the list.
58
+ - **list** — embedded [List](../list/entry.md) `variant="entry"` `size="large"`. The inter-row hairline divider is suppressed (`divider={false}`); rows tile against the surface fill.
59
+ - **row** — entity entry rendered as a [list/entry](../list/entry.md) row at the `large` rung (48 Thumbnail). Each item descriptor (`name` / `followers` / `description` / `thumbnail` / `active` / `onToggle`) maps to the entry contract (`label` / `secondary` / `description` / `thumbnail` / `trailingIcon`). The row body is presentational — tapping does not route.
60
+ - **trailingAction** — [Toggle Button](../button/toggle.md), `variant="toggle"` — composed into the row's `trailingIcon` slot.
61
+
62
+ ## Anatomy
63
+
64
+ | Slot | Token bindings |
65
+ |----------------|----------------|
66
+ | container | `surface` fill, 24px block / 16px inline padding, vertical stack |
67
+ | header | [Header](../header/header.md) `size="large"`. Container stack (`sys.layout.stack.md` = 16px) separates from list. |
68
+ | label | `heading.md` / Semibold / `onSurface` |
69
+ | headerAction | `xsmall` [Text Button](../button/text.md), `accent` appearance |
70
+ | list | [List](../list/entry.md) `variant="entry"` `size="large"`. `embedded` (wrapper section owns the rail). `divider={false}` (rows tile without an inter-row hairline). |
71
+ | row | [list/entry](../list/entry.md)-shaped row at `large` rung — 48 avatar, `inline.lg` gap, label.md primary / label.sm `secondary` + `description`. Keeps the list/entry native `container.md` inline padding and adds `margin-inline: -container.md` so the avatar lines up at the section rail (16 from the surface). The inter-row hairline divider is suppressed — rows tile against the surface fill with `stack.sm` (12) breathing room from the row's intrinsic block padding. |
72
+ | trailingAction | [Toggle Button](../button/toggle.md), `variant="toggle"` — composed into the row's `trailingIcon` slot. |
73
+
74
+ ## States
75
+
76
+ Container has no interactive state. Each row's only interactive surface is its **trailingAction** — a Toggle Button obeying the [Toggle Button](../button/toggle.md) state contract. Row body is presentational; tapping the row does not route. The **headerAction** is an `xsmall` Text Button (rendered as `<a>` when `href` is set).
77
+
78
+ ## Focus indicator
79
+
80
+ Row body is presentational; the only row-level focus target is the trailing Toggle Button (Outward ring). headerAction also paints its own Outward ring. Composition for any future row-level focus target: Inward — rows tile with a hairline divider.
81
+
82
+ ## Behavior
83
+
84
+ - **Header is required.** Every DirectoryList carries a `label`; the optional `headerAction` extends the header with a trailing `accent` Text Button when there's an index page to route to.
85
+ - **Vertical scroll, no pager.** The full list scrolls in normal document flow — reach for [SuggestionList](../suggestion-list/suggestion-list.md) instead when the surface needs a horizontal peek.
86
+ - **Toggle commits in place.** State is owned by the consumer — `active` and `onToggle` forward per row through `items`.
87
+ - **Entity-agnostic anatomy.** Same row shape carries channels, people, companies, or topics.
@@ -0,0 +1,104 @@
1
+ {
2
+ "$schema": "../../spec.schema.json",
3
+ "name": "DirectoryList",
4
+ "family": "directory-list",
5
+ "description": "Vertical follow-list — labelled block of follow-able entries rendered at the [list/entry](../list/entry.md) `large` rung (48 Thumbnail + identity group of label + stacked `secondary` followers + `description` + trailing [Toggle Button](../button/toggle.md)). **Preset wrapper** — internally this is `<Header /> + <List variant=\"entry\" size=\"large\" divider={false}>`; the wrapper exists to map item descriptors (`name → label`, `followers → secondary`, `active/onToggle → trailingIcon`) and to lock the rung + divider preset for the canonical Follow-able directory shape. **When to reach for the primitive instead:** if you need a different size rung, mix label-only and thumbnail-bearing rows, swap the trailing Toggle for a different affordance (chevron / icon-button / star), or want per-row dividers — drop down to `<Header /> + <List variant=\"entry\" />` directly. Sibling of SuggestionList: same entity-agnostic anatomy but no swipeable pager — the full set is scanned vertically.",
6
+ "element": "section",
7
+ "props": {
8
+ "embedded": {
9
+ "type": "boolean",
10
+ "default": false,
11
+ "description": "Composition mode flag. When `true` (or when DirectoryList is a direct child of `.chorus-carousel` / `.chorus-feed`), the list enters **embedded mode**: zeroes its own `background` + `padding` so chrome defers to the host."
12
+ },
13
+ "label": {
14
+ "type": "string",
15
+ "required": true,
16
+ "description": "Section title."
17
+ },
18
+ "headerAction": {
19
+ "type": "object",
20
+ "optional": true,
21
+ "description": "{ label, href?, onClick? } — trailing accent Text Button in the header. Extend the header when there's an index page to route to."
22
+ },
23
+ "items": {
24
+ "type": "node",
25
+ "required": true,
26
+ "description": "Array of entity descriptors: { value, name, followers, description, thumbnail, active?, onToggle? }."
27
+ }
28
+ },
29
+ "slots": {
30
+ "container": {
31
+ "required": true,
32
+ "description": "Surface block holding the header above the vertical entry list.",
33
+ "intrinsic": true
34
+ },
35
+ "header": {
36
+ "required": true,
37
+ "description": "[Header](../header/header.md) `size=\"large\"` — section label leading, optional `accent` Text Button trailing.",
38
+ "accepts": ["text", "button"]
39
+ },
40
+ "list": {
41
+ "required": true,
42
+ "description": "[List](../list/entry.md) `variant=\"entry\"` `size=\"large\"` rendering each follow-able entity. Embedded mode is forced on the inner list so the wrapper section owns the rail. The inter-row hairline divider is suppressed (`divider={false}`); rows tile against the surface fill.",
43
+ "intrinsic": true
44
+ },
45
+ "row": {
46
+ "required": true,
47
+ "intrinsic": true,
48
+ "description": "Single entity rendered as a [list/entry](../list/entry.md) row at the `large` rung. Item descriptors map to entry slots: `name` → `label`, `followers` → `secondary`, `description` → `description`, `thumbnail` → `thumbnail`, `active` + `onToggle` → `trailingIcon` (a Toggle Button). The row body is presentational — the only commit affordance is the trailing toggle.",
49
+ "accepts": ["row"]
50
+ },
51
+ "trailingAction": {
52
+ "required": true,
53
+ "description": "[Toggle Button](../button/toggle.md) — the row's only commit affordance, composed into the row's `trailingIcon` slot.",
54
+ "accepts": ["button"]
55
+ }
56
+ },
57
+ "sizing": {
58
+ "containerFill": "sys.color.surface",
59
+ "containerPaddingBlock": "sys.layout.container.lg",
60
+ "containerPaddingInline": "sys.layout.container.md",
61
+ "headerToListGap": "sys.layout.stack.md",
62
+ "labelTypo": "sys.typo.heading.md",
63
+ "labelColor": "sys.color.onSurface",
64
+ "headerActionRendersAs": "Button variant='text' size='xsmall' appearance='accent' — link-affordance accent rule.",
65
+ "rowRung": "list/entry size=\"large\" — 48 avatar, inline.lg gap, label.md primary, label.sm secondary + description.",
66
+ "rowInlinePaddingNote": "Each row keeps the list/entry native sys.layout.container.md inline padding (the tap target reaches the surface edge) and adds margin-inline: calc(-1 * sys.layout.container.md) so the visible avatar sits flush at the section's content rail — aligned with the header label at 16 from the surface.",
67
+ "dividerNote": "Inter-row hairline is suppressed via `divider={false}` on the inner List — rows tile against the surface fill rather than carrying a per-row separator. Reach for SuggestionList (xlarge rung) or List/entry directly when the surface needs a separator rule between rows."
68
+ },
69
+ "rowProps": {
70
+ "value": { "type": "string", "required": true },
71
+ "name": { "type": "string", "required": true },
72
+ "followers": { "type": "string", "optional": true },
73
+ "description": { "type": "string", "optional": true },
74
+ "thumbnail": {
75
+ "type": "object",
76
+ "required": true,
77
+ "agentRequired": true,
78
+ "omittedBehavior": "error",
79
+ "description": "Forwarded to Thumbnail verbatim — src, alt, updateDot, logoBadge. Agents MUST pass `src`; fill `/placeholder.png` when no real subject is implied."
80
+ },
81
+ "active": { "type": "boolean", "default": false },
82
+ "onToggle": { "type": "function", "optional": true }
83
+ },
84
+ "states": {
85
+ "note": "Container has no interactive state. Each row's only interactive surface is the trailing Toggle Button (its own state contract). The headerAction is an accent Text Button. The row body is presentational — tapping does not route."
86
+ },
87
+ "focusIndicator": {
88
+ "description": "Row body is presentational; the only row-level focus target is the trailing Toggle Button (Outward ring). headerAction also paints its own Outward ring.",
89
+ "composition": "inward",
90
+ "compositionReason": "Rows tile the column with a hairline divider; an outward ring would overlap divider and neighbour row.",
91
+ "trigger": ":focus-visible (keyboard / programmatic focus, never plain mouse click)"
92
+ },
93
+ "behavior": {
94
+ "headerRequired": "Every DirectoryList carries a label; the optional headerAction extends the header with a trailing accent Text Button.",
95
+ "verticalScroll": "No pager. The full list scrolls vertically in normal document flow — reach for SuggestionList instead when the surface needs a horizontal peek.",
96
+ "toggleCommitsInPlace": "Tapping 'Follow' flips the row's button to 'Following' and stays there. State is owned by the consumer via items[i].active + onToggle.",
97
+ "entityAgnostic": "Same row shape carries channels, people, companies, or topics."
98
+ },
99
+ "forbidden": [
100
+ "row anatomy diverged from the [list/entry](../list/entry.md) visual contract — DirectoryList's row IS an entry-shaped row at the `large` rung; rebuilding the avatar + text column + trailing slot with different typography or geometry is a forbidden divergence",
101
+ "follow action rendered as button/standard — the follow affordance is button/toggle per the spec",
102
+ "swipeable pager — DirectoryList is intentionally vertical-only; reach for SuggestionList when a horizontal peek is needed"
103
+ ]
104
+ }
@@ -0,0 +1,28 @@
1
+ {
2
+ "$schema": "../../family.schema.json",
3
+ "family": "divider",
4
+ "name": "Divider",
5
+ "description": "Region separator — a heavy tonal band that splits adjacent regions whose vertical rhythm alone doesn't read as a boundary. Painted with `sys.color.scrimSubtle` (~8% inverse-tone overlay — black scrim in light mode, white scrim in dark) so the band stays visible on every host surface tier (`surface`, `surfaceContainer`, `surfaceContainerHigh`, hero, …) without colliding with a fixed neutral step. Same scrim used by Banner default, Chip / Tag default, Progress track, StatusTag neutral, and Skeleton — Divider is the section-level member of that family. Reserved for *region*-level separation between groups that don't share an enclosing container (a feed segment ending and a followed-channels list beginning, a recents block ending above a single anchored CTA). Not for row-level separators inside a List (the list's own `divider={true}` paints those as a hairline `outlineVariant` rule). Single-spec family.",
6
+ "useCases": [
7
+ "between page-level content groups",
8
+ "between a feed segment and the section below",
9
+ "between an in-page promo / banner and the list below",
10
+ "above a single anchored CTA at the bottom of a panel",
11
+ "wherever vertical rhythm alone doesn't separate two regions clearly"
12
+ ],
13
+ "visualReuse": "open",
14
+ "layoutInset": "full-bleed",
15
+ "wrapperGuidance": "Owns nothing but its own block thickness (`sys.layout.stack.xs`, 8) and full-bleed inline footprint. Drop as a direct child of the page-shell `<main>` (or any full-width host column) between two regions. Do NOT wrap in a `padding-inline` div, `className=\"px-*\"`, or `style={{ padding }}` — Divider must touch the page edge to read as a region boundary, and the page rail is already paid once at the shell. Do NOT paint outer `margin-block` or wrap to add vertical breathing — the 8 thickness is the breathing. Inside a bounded surface (Dialog / BottomSheet / SideSheet), apply the negative-margin opt-out — see AGENTS.md § Composition rules.",
16
+ "usage": {
17
+ "example": "<Divider />"
18
+ },
19
+ "spec": "divider.md",
20
+ "subcomponents": [
21
+ {
22
+ "slug": "divider",
23
+ "spec": "divider.spec.json",
24
+ "md": "divider.md",
25
+ "default": true
26
+ }
27
+ ]
28
+ }
@@ -0,0 +1,78 @@
1
+ # Divider
2
+
3
+ A section-break band — a single full-bleed block painted with `sys.color.scrimSubtle` (~8% inverse-tone overlay — black in light mode, white in dark) at a fixed block thickness of `sys.layout.stack.xs` (8). Reach for it when two adjacent regions don't share an enclosing container and vertical rhythm alone doesn't read as a boundary — a directory of suggested channels ending and a fresh recommendation list beginning, a feed segment ending and a followed-channels list resuming, a promo strip ending and content picking up below.
4
+
5
+ **Reach for this when** the boundary between two regions is ambiguous because they share the same surface and vertical rhythm — the visitor's eye should *land* on the break, not infer it. **Skip when** the regions already sit on different surfaces (one card, one canvas — the surface change is the break), when one region carries a heading that itself reads as the start of a new block, or when the separation is between *rows of the same list* (List's own `divider={true}` paints those as a hairline `outlineVariant` rule).
6
+
7
+ **Surface-agnostic by design.** The fill is `sys.color.scrimSubtle` — a ~8% inverse-tone overlay (black in light mode, white in dark) — so the band reads as a tint *darker than the host surface* without pinning to a fixed neutral step. The same band visibly separates regions on `surface`, `surfaceContainerHigh`, a hero panel, or a coloured card without retoning per-host. Same scrim tier as Banner default, Chip / Tag default, Progress track, StatusTag neutral, and Skeleton.
8
+
9
+ **Layout inset.** `full-bleed` — Divider stretches edge-to-edge inside the page-shell content box. It ships **no** inline padding, **no** outer margin, **no** corner radius, **no** stroke; it just paints `scrimSubtle` across a `sys.layout.stack.xs` (8) block. Drop it as a direct child of the page-shell `<main>` (or any full-width host column) between two regions — do not wrap in a `padding-inline` div, `className="px-*"`, or `style={{ padding }}`; the band reading edge-to-edge IS what makes it a region boundary.
10
+
11
+ **Safe zone — none.** The parent column pays no `gap`, `padding-block`, or `margin-block` around Divider. The 8 of `scrimSubtle` is the breathing. If two regions feel cramped after dropping Divider between them, the fix is to add internal padding to the regions themselves (`layout.container.*`), never to wrap Divider in a spacer or paint outer margin on it.
12
+
13
+ ## Default
14
+
15
+ The single canonical band. 8 high, full inline width, `scrimSubtle` fill.
16
+
17
+ ```preview
18
+ divider/default
19
+ ---
20
+ import { Divider } from '@teamblind-chorus/ui';
21
+
22
+ <Divider />
23
+ ```
24
+
25
+ ## Use cases
26
+
27
+ ### Between adjacent lists
28
+
29
+ The canonical placement — a region break between two full-bleed lists stacked on the same page. Above: a directory of new channels (DirectoryList, an inert directory). Below: a recommendation set keyed to the visitor (SuggestionList, the pager-flavoured sibling). Both lists own their internal divider hairlines for rows, but the *boundary between the two lists* is a region break — that is the role Divider plays.
30
+
31
+ ```preview
32
+ divider/between-lists
33
+ ---
34
+ import { Divider, DirectoryList, SuggestionList } from '@teamblind-chorus/ui';
35
+
36
+ <div style={{ background: 'var(--sys-color-surface)', display: 'flex', flexDirection: 'column' }}>
37
+ <DirectoryList
38
+ label="New channels"
39
+ items={[
40
+ { value: 'breadclub', name: 'Sourdough Bakers', followers: '12.4K Followers', description: 'Open-crumb obsession and cold-proof timing.', thumbnail: { src: '/placeholder.png', alt: 'Sourdough Bakers' } },
41
+ { value: 'indiedevs', name: 'Indie Game Devs', followers: '8,210 Followers', description: 'Shipping logs, postmortems, marketing on a budget.', thumbnail: { src: '/placeholder.png', alt: 'Indie Game Devs' } },
42
+ ]}
43
+ />
44
+ <Divider />
45
+ <SuggestionList
46
+ label="Recommended channels"
47
+ items={[
48
+ { value: 'plants', name: 'Plant People', followers: '21.7K Followers', description: 'Houseplant troubleshooting and propagation threads.', thumbnail: { src: '/placeholder.png', alt: 'Plant People' } },
49
+ { value: 'movies', name: 'Movie Talk', followers: '34.2K Followers', description: 'Festival coverage, director threads, link shares.', thumbnail: { src: '/placeholder.png', alt: 'Movie Talk' } },
50
+ ]}
51
+ />
52
+ </div>
53
+ ```
54
+
55
+ ## Slots
56
+
57
+ - **container** — the tonal band. `sys.color.scrimSubtle` fill, `sys.layout.stack.xs` (8) block thickness, full inline width, no padding, no border, no corner radius. The native `<hr>` element with all browser defaults reset. `aria-hidden="true"` by default — the band is decorative.
58
+
59
+ ## Anatomy
60
+
61
+ | Slot | Token bindings |
62
+ |------------|----------------|
63
+ | container | `sys.color.scrimSubtle` fill, `sys.layout.stack.xs` (8) block height, 100% inline width, `border: none`, `margin: 0`, `border-radius: 0` |
64
+
65
+ ## Appearance
66
+
67
+ Single appearance — Divider has no emphasis axis, no orientation axis, and no thickness prop.
68
+
69
+ | Appearance | Fill | When to use |
70
+ |------------|----------------------------|-----------------------------------------------------------------------------|
71
+ | `default` | `sys.color.scrimSubtle` | The only mode. Same scrim used by Banner default / Chip-Tag default / Progress track / StatusTag neutral / Skeleton — Divider is the section-level member of that family. |
72
+
73
+ ## Behavior
74
+
75
+ - **aria-hidden by default.** The band is decorative chrome; screen-reader users navigate by headings and landmarks, not visual breaks. Override `aria-hidden={false}` only when the divider genuinely marks a semantic section change (rare — prefer a heading).
76
+ - **Fixed thickness.** Block thickness is `sys.layout.stack.xs` (8) and is not a prop. Heavier bands compete with content; thinner bands collapse to a hairline (use the host's own `outlineVariant` border for row-level breaks, not Divider).
77
+ - **Full-bleed by contract.** The band must touch the page edge to read as a region boundary. Wrapping Divider in a padding-inline container breaks the affordance — see the AGENTS.md "one gutter, paid once" rule.
78
+ - **Not a row separator.** Inside a List, rows already separate via the list's own `divider={true}` hairline `outlineVariant` rule. Reach for Divider only at the *region* level, between groups that don't share an enclosing container.