@teamblind-chorus/ui 1.0.1 → 1.2.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 (131) hide show
  1. package/agents/AGENTS.md +4 -6
  2. package/agents/DESIGN.md +2 -0
  3. package/agents/LOVABLE.md +167 -373
  4. package/agents/anti-patterns.md +2 -2
  5. package/agents/catalog.md +12 -6
  6. package/agents/components/avatar-rail/avatar-rail.md +2 -0
  7. package/agents/components/avatar-rail/avatar-rail.spec.json +19 -0
  8. package/agents/components/badge/badge.md +2 -0
  9. package/agents/components/badge/role.md +2 -0
  10. package/agents/components/badge/update.md +2 -0
  11. package/agents/components/banner/banner.family.json +3 -1
  12. package/agents/components/banner/banner.md +125 -9
  13. package/agents/components/banner/banner.spec.json +64 -3
  14. package/agents/components/bottom-sheet/bottom-sheet.md +2 -0
  15. package/agents/components/bubble/bubble.md +2 -0
  16. package/agents/components/button/button.family.json +8 -2
  17. package/agents/components/button/button.md +2 -0
  18. package/agents/components/button/check.md +2 -0
  19. package/agents/components/button/check.spec.json +19 -0
  20. package/agents/components/button/fab.md +2 -0
  21. package/agents/components/button/fab.spec.json +19 -0
  22. package/agents/components/button/group.spec.json +65 -0
  23. package/agents/components/button/icon.md +2 -0
  24. package/agents/components/button/icon.spec.json +19 -0
  25. package/agents/components/button/standard.md +45 -19
  26. package/agents/components/button/standard.spec.json +19 -0
  27. package/agents/components/button/text.md +2 -0
  28. package/agents/components/button/text.spec.json +19 -0
  29. package/agents/components/button/toggle.md +2 -0
  30. package/agents/components/button/toggle.spec.json +19 -0
  31. package/agents/components/button/toolbar.md +2 -0
  32. package/agents/components/carousel/carousel.md +2 -0
  33. package/agents/components/carousel/post.md +5 -3
  34. package/agents/components/carousel/post.spec.json +4 -6
  35. package/agents/components/carousel/profile.md +4 -2
  36. package/agents/components/carousel/profile.spec.json +4 -6
  37. package/agents/components/chip/chip.md +2 -0
  38. package/agents/components/chip/filter.md +2 -0
  39. package/agents/components/chip/filter.spec.json +19 -0
  40. package/agents/components/chip/tag.md +2 -0
  41. package/agents/components/chip/tag.spec.json +19 -0
  42. package/agents/components/dialog/dialog.md +2 -0
  43. package/agents/components/directory-list/directory-list.md +2 -0
  44. package/agents/components/divider/divider.md +2 -0
  45. package/agents/components/empty-state/empty-state.family.json +28 -0
  46. package/agents/components/empty-state/empty-state.md +69 -0
  47. package/agents/components/empty-state/empty-state.spec.json +87 -0
  48. package/agents/components/feed/ad.md +2 -0
  49. package/agents/components/feed/feed.md +2 -0
  50. package/agents/components/feed/post.md +2 -0
  51. package/agents/components/form-field/form-field.md +3 -1
  52. package/agents/components/form-field/input.md +2 -0
  53. package/agents/components/form-field/input.spec.json +10 -2
  54. package/agents/components/form-field/search.md +2 -0
  55. package/agents/components/form-field/search.spec.json +10 -2
  56. package/agents/components/form-field/select.md +2 -0
  57. package/agents/components/form-field/select.spec.json +9 -1
  58. package/agents/components/form-field/textarea.md +2 -0
  59. package/agents/components/form-field/textarea.spec.json +10 -2
  60. package/agents/components/header/header.md +2 -0
  61. package/agents/components/header/main.md +2 -0
  62. package/agents/components/header/sub.md +2 -0
  63. package/agents/components/list/accordion.md +2 -0
  64. package/agents/components/list/accordion.spec.json +9 -0
  65. package/agents/components/list/entry.md +2 -0
  66. package/agents/components/list/entry.spec.json +21 -1
  67. package/agents/components/list/list.md +3 -1
  68. package/agents/components/list/radio.md +2 -0
  69. package/agents/components/list/radio.spec.json +19 -0
  70. package/agents/components/list/standard.md +48 -0
  71. package/agents/components/list/standard.spec.json +39 -3
  72. package/agents/components/metadata/compact.md +13 -7
  73. package/agents/components/metadata/compact.spec.json +19 -6
  74. package/agents/components/metadata/metadata.family.json +3 -3
  75. package/agents/components/metadata/metadata.md +4 -2
  76. package/agents/components/metadata/standard.md +24 -0
  77. package/agents/components/nav-card/nav-card.md +2 -0
  78. package/agents/components/nav-card/nav-card.spec.json +9 -0
  79. package/agents/components/nav-list/nav-list.md +2 -0
  80. package/agents/components/navigation-bar/main.md +2 -0
  81. package/agents/components/navigation-bar/navigation-bar.md +2 -0
  82. package/agents/components/navigation-bar/search.md +2 -0
  83. package/agents/components/navigation-bar/sub.md +2 -0
  84. package/agents/components/page-shell/page-shell.family.json +1 -1
  85. package/agents/components/page-shell/page-shell.md +35 -0
  86. package/agents/components/page-shell/page-shell.spec.json +85 -0
  87. package/agents/components/pagination/pagination.family.json +26 -0
  88. package/agents/components/pagination/pagination.md +40 -0
  89. package/agents/components/pagination/pagination.spec.json +54 -0
  90. package/agents/components/profile-header/profile-header.md +2 -0
  91. package/agents/components/progress/progress.md +2 -0
  92. package/agents/components/side-sheet/side-sheet.md +2 -0
  93. package/agents/components/skeleton/skeleton.md +2 -0
  94. package/agents/components/spinner/spinner.family.json +27 -0
  95. package/agents/components/spinner/spinner.md +98 -0
  96. package/agents/components/spinner/spinner.spec.json +82 -0
  97. package/agents/components/status-tag/status-tag.md +2 -0
  98. package/agents/components/suggestion-list/suggestion-list.md +2 -0
  99. package/agents/components/switch/switch.md +2 -0
  100. package/agents/components/switch/switch.spec.json +9 -0
  101. package/agents/components/tab-bar/tab-bar.md +2 -0
  102. package/agents/components/tab-bar/tab-bar.spec.json +16 -0
  103. package/agents/components/tabs/rounded.md +2 -0
  104. package/agents/components/tabs/rounded.spec.json +19 -0
  105. package/agents/components/tabs/segmented.md +2 -0
  106. package/agents/components/tabs/tabs.md +2 -0
  107. package/agents/components/tabs/underline.md +2 -0
  108. package/agents/components/tabs/underline.spec.json +19 -0
  109. package/agents/components/thumbnail/thumbnail.md +2 -0
  110. package/agents/components/toast/toast.md +2 -0
  111. package/agents/components/tooltip/tooltip.md +2 -0
  112. package/agents/compose.md +3 -3
  113. package/agents/manifest.json +9 -6
  114. package/agents/patterns/README.md +2 -0
  115. package/agents/patterns/actions.md +2 -0
  116. package/agents/patterns/browsing.md +2 -0
  117. package/agents/patterns/communications.md +2 -0
  118. package/agents/patterns/layout.md +2 -0
  119. package/agents/patterns/modals.md +2 -0
  120. package/agents/patterns/visual.md +2 -0
  121. package/agents/usage.json +27 -3
  122. package/dist/index.cjs +433 -97
  123. package/dist/index.cjs.map +1 -1
  124. package/dist/index.d.cts +74 -3
  125. package/dist/index.d.ts +74 -3
  126. package/dist/index.js +430 -98
  127. package/dist/index.js.map +1 -1
  128. package/dist/styles.css +365 -41
  129. package/package.json +1 -2
  130. package/agents/reconstruct.md +0 -55
  131. package/agents/scoped-adoption.md +0 -111
@@ -79,6 +79,25 @@
79
79
  "color": "label",
80
80
  "opacity": "sys.state.pressed"
81
81
  }
82
+ },
83
+ "focused": {
84
+ "overlay": {
85
+ "color": "label",
86
+ "opacity": "sys.state.focus"
87
+ },
88
+ "focusRing": {
89
+ "composition": "outward",
90
+ "layer": "::after overlay — position:absolute, inset:0, no reflow (DESIGN.md Focus ring composition)",
91
+ "innerCounterRing": {
92
+ "width": "sys.borderWidth.hairline",
93
+ "color": "sys.color.focusInset"
94
+ },
95
+ "outerRing": {
96
+ "width": "sys.borderWidth.thin",
97
+ "color": "sys.color.focus"
98
+ }
99
+ },
100
+ "note": "Keyboard-focus (:focus-visible) visual. Mirrors the `focusIndicator` block (the external-reader contract); kept here so spec-only renderers see focus in the states map. Composes over the lifecycle state the FAB is in; never via plain mouse click."
82
101
  }
83
102
  },
84
103
  "focusIndicator": {
@@ -0,0 +1,65 @@
1
+ {
2
+ "$schema": "../../spec.schema.json",
3
+ "name": "ButtonGroup",
4
+ "family": "button",
5
+ "subcomponent": "group",
6
+ "description": "Lays out two or more Buttons as one composition so consumers never hand-roll the wrapper div. A NAMED export (`import { ButtonGroup }`) — NOT a Button variant. Three forms share the family's 8px (`sys.layout.inline.md`) gap: inline horizontal (default), inline vertical (`orientation=\"vertical\"`), and docked (`variant=\"docked\"`) — a footer bar pinned to the bottom of the app. The docked form adds full-bleed `sys.color.surface`, a 16px (`sys.layout.container.md`) inset, an upward `sys.elevation.sheet` shadow (no top stroke), and an optional `label` above the row.",
7
+ "element": "div",
8
+ "props": {
9
+ "variant": {
10
+ "type": "enum",
11
+ "values": [
12
+ "inline",
13
+ "docked"
14
+ ],
15
+ "default": "inline"
16
+ },
17
+ "orientation": {
18
+ "type": "enum",
19
+ "values": [
20
+ "horizontal",
21
+ "vertical"
22
+ ],
23
+ "default": "horizontal",
24
+ "note": "Applies to the `inline` variant. `docked` is always a single horizontal row."
25
+ },
26
+ "label": {
27
+ "type": "node",
28
+ "optional": true,
29
+ "note": "Optional caption above the row. Rendered in `sys.typo.body.md` (16px) / `sys.color.onSurfaceVariant`, centered. An inline <strong> reads as the emphasized value in the full-strength on-surface tone."
30
+ }
31
+ },
32
+ "slots": {
33
+ "children": {
34
+ "required": true,
35
+ "description": "The Buttons to group. In the `docked` variant they split the row equally (each `flex: 1`), so no per-Button `fullWidth` is needed.",
36
+ "accepts": [
37
+ "button"
38
+ ]
39
+ },
40
+ "label": {
41
+ "required": false,
42
+ "description": "Optional caption above the row (`label` prop). `sys.typo.body.md` (16px), `sys.color.onSurfaceVariant`, centered, 16px (`sys.layout.stack.md`) above the row.",
43
+ "accepts": [
44
+ "text"
45
+ ]
46
+ }
47
+ },
48
+ "anatomy": {
49
+ "gap": "sys.layout.inline.md (8) — between Buttons, both axes",
50
+ "labelStackGap": "sys.layout.stack.md (16) — label → row",
51
+ "docked": {
52
+ "background": "sys.color.surface",
53
+ "padding": "sys.layout.container.md (16) — all four sides",
54
+ "elevation": "sys.elevation.sheet — upward shadow, the same one Bottom Sheet / Side Sheet cast",
55
+ "border": null,
56
+ "split": "each Button flex: 1 1 0 — they share the row equally"
57
+ }
58
+ },
59
+ "forbidden": [
60
+ "raw wrapper <div> with hand-rolled flex/gap instead of <ButtonGroup> — the gap, surface, and shadow are the component's contract",
61
+ "`variant=\"docked\"` self-pinned with position: sticky/fixed — like the other bars it renders in flow; PageShell owns the pinning",
62
+ "top `border` / hairline on the docked bar — separation from the scrolling body is the upward `sys.elevation.sheet` shadow, not a stroke",
63
+ "reaching for ButtonGroup as `<Button variant=\"group\">` — it is its own named export, not a Button variant"
64
+ ]
65
+ }
@@ -1,5 +1,7 @@
1
1
  # Icon
2
2
 
3
+ > 🇰🇷 한국어: [`i18n/ko/schema/components/button/icon.md`](../../../i18n/ko/schema/components/button/icon.md)
4
+
3
5
  The icon-only commit surface — circular transparent target carrying a single glyph. Two rungs: `large` (40 × 40 / 24-glyph) for page chrome, `medium` (32 × 32 / 16-glyph) for inside-control density.
4
6
 
5
7
  **Reach for this when** the control's identity is the glyph and the action is reversible — [Navigation bar](../navigation-bar/navigation-bar.md) search/chat, [Dialog](../dialog/dialog.md) dismiss, feed-row "⋯". **Skip when** the action is destructive or one-shot — use standard [Button](./button.md) with a visible verb.
@@ -128,6 +128,25 @@
128
128
  "opacity": "sys.state.pressed"
129
129
  }
130
130
  },
131
+ "focused": {
132
+ "overlay": {
133
+ "color": "icon",
134
+ "opacity": "sys.state.focus"
135
+ },
136
+ "focusRing": {
137
+ "composition": "outward",
138
+ "layer": "::after overlay — position:absolute, inset:0, no reflow (DESIGN.md Focus ring composition)",
139
+ "innerCounterRing": {
140
+ "width": "sys.borderWidth.hairline",
141
+ "color": "sys.color.focusInset"
142
+ },
143
+ "outerRing": {
144
+ "width": "sys.borderWidth.thin",
145
+ "color": "sys.color.focus"
146
+ }
147
+ },
148
+ "note": "Keyboard-focus (:focus-visible) visual. Mirrors the `focusIndicator` block (the external-reader contract); kept here so spec-only renderers see focus in the states map. Composes over the lifecycle state the button is in; never via plain mouse click."
149
+ },
131
150
  "disabled": {
132
151
  "overlay": null,
133
152
  "containerOpacity": "sys.state.disabled",
@@ -1,5 +1,7 @@
1
1
  # Standard
2
2
 
3
+ > 🇰🇷 한국어: [`i18n/ko/schema/components/button/standard.md`](../../../i18n/ko/schema/components/button/standard.md)
4
+
3
5
  The default inline action surface — a labelled commit (form submit, dialog confirm, row action). Two axes: **size** (`large` / `medium` / `small`), **appearance** (`primary` / `secondary` / `outlined` / `tertiary`).
4
6
 
5
7
  **Reach for this when** you need a labelled commit inline with content — Save, Continue, Confirm, Cancel. **Skip when** the commit must float above scrolling content ([FAB](./fab.md)), the rung is body-text-sized ([Text Button](./text.md)), or the row is a dense toolbar ([Toolbar Button](./toolbar.md)).
@@ -103,36 +105,60 @@ import { Button } from '@teamblind-chorus/ui';
103
105
 
104
106
  ### Group
105
107
 
106
- Adjacent Buttons share an **8px** gap (`sys.layout.inline.md` horizontal / `sys.layout.stack.xs` vertical). Horizontal: outlined left, primary right. Vertical: primary top, secondary below.
108
+ Compose adjacent Buttons with **`ButtonGroup`** instead of a hand-rolled wrapper — it owns the family's **8px** gap (`sys.layout.inline.md`). Horizontal (default): outlined left, primary right. Vertical (`orientation="vertical"`): primary top, secondary below.
107
109
 
108
110
  ```preview
109
111
  button/standard/group
110
112
  ---
111
- import { Button } from '@teamblind-chorus/ui';
113
+ import { Button, ButtonGroup } from '@teamblind-chorus/ui';
112
114
 
113
- <div style={{ display: 'flex', gap: 8 }}>
114
- <Button appearance="outlined" size="large">
115
- See more
116
- </Button>
117
- <Button appearance="primary" size="large">
118
- Confirm
119
- </Button>
120
- </div>
115
+ <ButtonGroup aria-label="Group example">
116
+ <Button appearance="outlined" size="large">See more</Button>
117
+ <Button appearance="primary" size="large">Confirm</Button>
118
+ </ButtonGroup>
121
119
  ```
122
120
 
123
121
  ```preview
124
122
  button/standard/group-vertical
125
123
  ---
126
- import { Button } from '@teamblind-chorus/ui';
124
+ import { Button, ButtonGroup } from '@teamblind-chorus/ui';
125
+
126
+ <ButtonGroup orientation="vertical" aria-label="Group example">
127
+ <Button appearance="primary" size="large" fullWidth>Save</Button>
128
+ <Button appearance="secondary" size="large" fullWidth>Cancel</Button>
129
+ </ButtonGroup>
130
+ ```
127
131
 
128
- <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
129
- <Button appearance="primary" size="large">
130
- Save
131
- </Button>
132
- <Button appearance="secondary" size="large">
133
- Cancel
134
- </Button>
135
- </div>
132
+ ### Docked action bar
133
+
134
+ `ButtonGroup` with **`variant="docked"`** — two **Large** Buttons combined into a footer bar pinned to the bottom of the app, the screen-level commit row for a flow (a picker, a form, a detail view). Full-bleed `sys.color.surface` with a **16px** inset (`sys.layout.container.md`); the two Buttons split the row equally at the family's 8px gap (`sys.layout.inline.md`). No top stroke — instead an upward `sys.elevation.sheet` shadow (the same one [Bottom Sheet](../bottom-sheet/bottom-sheet.md) / [Side Sheet](../side-sheet/side-sheet.md) cast) lifts it off the scrolling body, so content passing behind reads as a separate region. Supplementary **outlined** left, **primary** commit right. Renders in flow — like the other bars it must **not** self-pin (`position: sticky`/`fixed`); [Page Shell](../page-shell/page-shell.md) owns the pinning.
135
+
136
+ ```preview
137
+ button/standard/docked-bar
138
+ ---
139
+ import { Button, ButtonGroup } from '@teamblind-chorus/ui';
140
+
141
+ <ButtonGroup variant="docked" aria-label="Page actions">
142
+ <Button appearance="outlined" size="large">Salary info</Button>
143
+ <Button appearance="primary" size="large">View jobs</Button>
144
+ </ButtonGroup>
145
+ ```
146
+
147
+ An **optional text label** sits above the row — pass it via the `label` prop. It's a caption echoing what the Buttons act on (the current selection, a running total), rendered in `sys.typo.body.md` (**16px**) / `sys.color.onSurfaceVariant`, centered, separated from the row by `sys.layout.stack.md` (16px); an inline `<strong>` reads as the emphasized value in the full-strength on-surface tone.
148
+
149
+ ```preview
150
+ button/standard/docked-bar-labeled
151
+ ---
152
+ import { Button, ButtonGroup } from '@teamblind-chorus/ui';
153
+
154
+ <ButtonGroup
155
+ variant="docked"
156
+ aria-label="Page actions"
157
+ label={<>Selected role: <strong>SW Engineer</strong></>}
158
+ >
159
+ <Button appearance="outlined" size="large">Salary info</Button>
160
+ <Button appearance="primary" size="large">View jobs</Button>
161
+ </ButtonGroup>
136
162
  ```
137
163
 
138
164
  ### Truncation
@@ -161,6 +161,25 @@
161
161
  "opacity": "sys.state.pressed"
162
162
  }
163
163
  },
164
+ "focused": {
165
+ "overlay": {
166
+ "color": "label",
167
+ "opacity": "sys.state.focus"
168
+ },
169
+ "focusRing": {
170
+ "composition": "outward",
171
+ "layer": "::after overlay — position:absolute, inset:0, no reflow (DESIGN.md Focus ring composition)",
172
+ "innerCounterRing": {
173
+ "width": "sys.borderWidth.hairline",
174
+ "color": "sys.color.focusInset"
175
+ },
176
+ "outerRing": {
177
+ "width": "sys.borderWidth.thin",
178
+ "color": "sys.color.focus"
179
+ }
180
+ },
181
+ "note": "Keyboard-focus (:focus-visible) visual. Mirrors the `focusIndicator` block (the external-reader contract); kept here so spec-only renderers see focus in the states map. Composes over the lifecycle state the button is in; never via plain mouse click."
182
+ },
164
183
  "disabled": {
165
184
  "overlay": null,
166
185
  "containerOpacity": "sys.state.disabled",
@@ -1,5 +1,7 @@
1
1
  # Text
2
2
 
3
+ > 🇰🇷 한국어: [`i18n/ko/schema/components/button/text.md`](../../../i18n/ko/schema/components/button/text.md)
4
+
3
5
  The link-shaped commit surface — reads as text at rest, paints a button-like hover overlay and focus ring on interaction. Two axes: **appearance** (`default` / `accent` / `onPrimary` / `inverse`), **size** (`medium` / `small` / `xsmall`).
4
6
 
5
7
  **Reach for this when** the action is inline next to typographic content and commits — *Skip*, *Edit*, *Resend*, a section's trailing *See all*. **Skip when** the affordance navigates — use [Text link](../../DESIGN.md#text-links).
@@ -166,6 +166,25 @@
166
166
  "opacity": "sys.state.pressed"
167
167
  }
168
168
  },
169
+ "focused": {
170
+ "overlay": {
171
+ "color": "label",
172
+ "opacity": "sys.state.focus"
173
+ },
174
+ "focusRing": {
175
+ "composition": "outward",
176
+ "layer": "::after overlay — position:absolute, inset:0, no reflow (DESIGN.md Focus ring composition)",
177
+ "innerCounterRing": {
178
+ "width": "sys.borderWidth.hairline",
179
+ "color": "sys.color.focusInset"
180
+ },
181
+ "outerRing": {
182
+ "width": "sys.borderWidth.thin",
183
+ "color": "sys.color.focus"
184
+ }
185
+ },
186
+ "note": "Keyboard-focus (:focus-visible) visual. Mirrors the `focusIndicator` block (the external-reader contract); kept here so spec-only renderers see focus in the states map. Composes over the lifecycle state the button is in; never via plain mouse click."
187
+ },
169
188
  "disabled": {
170
189
  "overlay": null,
171
190
  "containerOpacity": "sys.state.disabled",
@@ -1,5 +1,7 @@
1
1
  # Toggle
2
2
 
3
+ > 🇰🇷 한국어: [`i18n/ko/schema/components/button/toggle.md`](../../../i18n/ko/schema/components/button/toggle.md)
4
+
3
5
  Commit-and-record action — a Toolbar-footprint button with two states. **Inactive** invites the commit (`primary` fill); **active** records it (`transparent` fill + hairline `outlineVariant` outline).
4
6
 
5
7
  **Reach for this when** you need a reversible commit that persists across views — *Follow / Following*, *Subscribe / Subscribed*, *Join / Joined*. **Skip when** the action is one-shot ([Standard Button](./standard.md)), the row is a dense toolbar ([Toolbar Button](./toolbar.md)), or the toggle belongs to a filter set ([Filter Chip](../chip/filter.md)).
@@ -92,6 +92,25 @@
92
92
  "opacity": "sys.state.pressed"
93
93
  }
94
94
  },
95
+ "focused": {
96
+ "overlay": {
97
+ "color": "label",
98
+ "opacity": "sys.state.focus"
99
+ },
100
+ "focusRing": {
101
+ "composition": "outward",
102
+ "layer": "::after overlay — position:absolute, inset:0, no reflow (DESIGN.md Focus ring composition)",
103
+ "innerCounterRing": {
104
+ "width": "sys.borderWidth.hairline",
105
+ "color": "sys.color.focusInset"
106
+ },
107
+ "outerRing": {
108
+ "width": "sys.borderWidth.thin",
109
+ "color": "sys.color.focus"
110
+ }
111
+ },
112
+ "note": "Keyboard-focus (:focus-visible) visual. Mirrors the `focusIndicator` block (the external-reader contract); kept here so spec-only renderers see focus in the states map. Composes over the lifecycle state the button is in; never via plain mouse click."
113
+ },
95
114
  "disabled": {
96
115
  "overlay": null,
97
116
  "containerOpacity": "sys.state.disabled",
@@ -1,5 +1,7 @@
1
1
  # Toolbar
2
2
 
3
+ > 🇰🇷 한국어: [`i18n/ko/schema/components/button/toolbar.md`](../../../i18n/ko/schema/components/button/toolbar.md)
4
+
3
5
  Dense inline action — a 32-tall capsule for toolbars, table-row actions, and inline menu triggers. Chrome shared with [Filter chip](../chip/filter.md) and [Tabs Segmented](../tabs/segmented.md) so mixed rows read at one density; divergence is intent (Toolbar fires, Filter toggles, Segmented enforces single-select).
4
6
 
5
7
  **Reach for this when** a dense row needs an inline action — toolbar opener, table-row action, inline menu trigger. **Skip when** the standard inline shape fits ([Button](./button.md)), the affordance floats above content ([FAB](./fab.md)), or the row is body-text density ([Text Button](./text.md)).
@@ -1,5 +1,7 @@
1
1
  # Carousel
2
2
 
3
+ > 🇰🇷 한국어: [`i18n/ko/schema/components/carousel/carousel.md`](../../../i18n/ko/schema/components/carousel/carousel.md)
4
+
3
5
  Page-region wrapper for editorial collections — a labelled block with a leading heading and an optional trailing `See all` link, hosting a horizontal swipeable rail underneath. Carousel owns the chrome (surface, padding, header anatomy); each sub owns only its pager + cards.
4
6
 
5
7
  - **[Post](./post.md)** — a swipeable rail of compact post cards. Surfaces a curated set of popular posts or gives paid / verified accounts priority placement inside the feed column.
@@ -1,5 +1,7 @@
1
1
  # Post
2
2
 
3
+ > 🇰🇷 한국어: [`i18n/ko/schema/components/carousel/post.md`](../../../i18n/ko/schema/components/carousel/post.md)
4
+
3
5
  Sub-component of the [Carousel](./carousel.md) family. Horizontally-scrolling pager of up to 5 compact post cards — surfaces curated popular posts or gives paid / verified accounts priority placement inside the [Post](../feed/post.md) feed column. The section heading and `See all` link live on the [Carousel](./carousel.md) wrapper — PostCarousel is the *content* only.
4
6
 
5
7
  **Reach for this when** spotlighting a small set (≤5) of curated or sponsored posts inside a feed column. **Skip when** the surface is a single feed stream ([Feed · Post](../feed/post.md)) or an entity directory ([List/entry](../list/entry.md)).
@@ -108,7 +110,7 @@ import { Carousel, PostCarousel } from '@teamblind-chorus/ui';
108
110
  - **body** — `sys.typo.body.sm` / `sys.color.onSurfaceVariant`. Three-line clamp.
109
111
  - **mention** *(optional)* — `sys.typo.body.sm` / `sys.color.primary` (not italic).
110
112
  - **footer** — leading 'See more' [Text Button](../button/text.md) (`xsmall` / `secondary`) + trailing view count (`EyeIcon` + count `<span>`).
111
- - **pagination** — one dot per card; active `sys.color.onSurface`, rest `sys.color.outlineVariant`. Decorative (`aria-hidden`).
113
+ - **pagination** — [Pagination](../pagination/pagination.md) component, one dot per card (`count` = card count, `activeIndex` from scroll position). Decorative — dot tokens and the `aria-hidden` contract live on its spec; the carousel centers the intrinsic-width row (`align-self: center`).
112
114
 
113
115
  ## Anatomy
114
116
 
@@ -123,7 +125,7 @@ import { Carousel, PostCarousel } from '@teamblind-chorus/ui';
123
125
  | body / mention | `sys.typo.body.sm` / `sys.color.onSurfaceVariant` (three-line clamp) · mention in `sys.color.primary` |
124
126
  | followAction | [Text Button](../button/text.md) `size={'xsmall'}`, `appearance={'accent'}` inactive → `appearance={'default'}` active; state tokens delegate to Text Button |
125
127
  | footer | Row: leading 'See more' Text Button (`xsmall` / `secondary`) + trailing view count `<span>`; `justify-content: space-between` |
126
- | pagination dot | 6 × 6, `sys.radius.full`; active `sys.color.onSurface`, inactive `sys.color.outlineVariant`; `sys.layout.inline.sm` row gap |
128
+ | pagination dot | [Pagination](../pagination/pagination.md) component delegated verbatim — 6 × 6 dots, active/inactive colors, and row gap bind on its spec |
127
129
 
128
130
  ## Sizes
129
131
 
@@ -144,5 +146,5 @@ Inward — cards tile inside a horizontal scroll viewport with hairline outlines
144
146
  - **Card header renders the shared `EntryRow`.** The avatar + optional verified mark + channel label + trailing follow [Text Button](../button/text.md) row pinned to the top of each *card* is **the same `EntryRow` atom that [List/entry](../list/entry.md) rows render** — not a look-alike, the literal shared component. The carousel passes `verified` (the inline `VerifiedFillIcon`, left of the label), a 40-rung `thumbnail`, and a `xsmall` follow Text Button (`accent` inactive → `default` active) into `EntryRow`'s trailing slot; the leading→label gap (12), identity column, and trailing-action pin all live on `EntryRow`. Only the surrounding card chrome (`chorus-post-carousel__card-header` wrapper) is carousel-specific. Retune the attribution shape on `EntryRow` / at [List/entry](../list/entry.md), not here.
145
147
  - **One card per page, scroll-snap.** `scroll-snap-type: x mandatory`; each card declares `scroll-snap-align: start`. The pager re-pays the 16 left rail via `padding-left` and, as a full-bleed child, reaches the trailing edge intrinsically so the peek isn't clipped.
146
148
  - **Guaranteed 40px peek.** Trailing-edge visibility of the next card pins to `ref.space.500` — raw ref step so the floor is fixed-pixel across breakpoints. Card basis is `calc(100% - sys.layout.inline.md - ref.space.500)`. After every swipe, the snapped card aligns flush with the container's left padding; the trailing edge always holds the 40px peek.
147
- - **Pagination dots are decorative.** Dots reflect scroll position via `IntersectionObserver`; tapping a dot does not scroll. Active dot paints `sys.color.onSurface`, the rest `sys.color.outlineVariant`.
149
+ - **Pagination dots are decorative.** The dot row is the shared [Pagination](../pagination/pagination.md) component. The carousel derives the active index via `IntersectionObserver` on the pager's snap targets and passes it down; tapping a dot does not scroll.
148
150
  - **Cards route via `onClick`.** When an item carries `onClick`, the card surface becomes the click target; header (follow action) and footer (more link) intercept the tap so each routes independently.
@@ -94,7 +94,7 @@
94
94
  },
95
95
  "pagination": {
96
96
  "required": true,
97
- "description": "Row of dots below the pager — one per card. Active dot paints `sys.color.onSurface`, the rest paint `sys.color.outlineVariant`. Decorative (`aria-hidden`); the active index updates from the pager's scroll position via IntersectionObserver.",
97
+ "description": "Row of dots below the pager — rendered by the [Pagination](../pagination/pagination.md) component (`count` = card count, `activeIndex` from the pager's scroll position via IntersectionObserver). Dot size / gap / colors and the `aria-hidden` decorative contract all come from the Pagination spec; Pagination renders nothing below two cards. Pagination is an intrinsic-width inline element — the carousel centers it (`align-self: center` on the carousel's own selector).",
98
98
  "intrinsic": true
99
99
  }
100
100
  },
@@ -134,10 +134,8 @@
134
134
  "cardFooterColor": "sys.color.onSurfaceVariant",
135
135
  "cardFooterIcon": "EyeIcon",
136
136
  "cardFooterIconSize": "sys.icon.md",
137
- "paginationDotSize": "ref.space.75",
138
- "paginationDotGap": "sys.layout.inline.sm",
139
- "paginationActiveColor": "sys.color.onSurface",
140
- "paginationInactiveColor": "sys.color.outlineVariant"
137
+ "paginationRendersAs": "Pagination component — dot size / gap / radius and active / inactive colors all delegate to the [Pagination](../pagination/pagination.md) spec.",
138
+ "paginationAlign": "center — Pagination is an intrinsic-width inline element; the carousel binds `align-self: center` on its own selector to center the row under the pager."
141
139
  },
142
140
  "itemProps": {
143
141
  "id": {
@@ -210,7 +208,7 @@
210
208
  "horizontalScrollSnap": "scroll-snap-type: x mandatory; each card declares scroll-snap-align: start.",
211
209
  "nextCardPeek": "The Carousel host is full-bleed, so the pager spans the full surface width and reaches the trailing edge intrinsically (no negative margin); it re-pays the 16 left rail via `padding-left: sys.layout.container.md`. Card basis composes the inter-card gap (`sys.layout.inline.md`) plus the visible peek (`ref.space.500`, 40px) into one calc — a minimum of 40px of the next card is guaranteed to remain visible at the trailing edge whenever a card is snapped to the leading edge. The peek is pinned to a raw ref step rather than a responsive sys-layout rung so the visibility floor is fixed-pixel and does not shift at the responsive breakpoint.",
212
210
  "stickyToLeadingPadding": "After every swipe, the snapped card aligns flush with the pager's left padding (the 16 rail) — it 'sticks' to the leading edge of the pager. `scroll-snap-align: start` plus the pager's `scroll-padding-left: sys.layout.container.md` together produce this behavior. The trailing edge of the same snap state always holds the 40px peek of the next card; the two are one geometry contract, not two independent rules.",
213
- "paginationDecorative": "Dots are decorative (`aria-hidden`). They reflect the scroll position only; tapping a dot does not scroll. Active state updates via IntersectionObserver on the pager's snap targets.",
211
+ "paginationDecorative": "Dots render via the [Pagination](../pagination/pagination.md) component and are decorative (`aria-hidden`). They reflect the scroll position only; tapping a dot does not scroll. The carousel derives the active index via IntersectionObserver on the pager's snap targets and passes it down as `activeIndex`.",
214
212
  "cardClickRoutes": "When an item carries `onClick`, the card surface becomes the click target. Header affordances (follow action) and footer affordances (more link) intercept the tap so each can route independently.",
215
213
  "insertionPolicy": "Editorial / ops control inserts the carousel between regular Feed cards according to placement policy — the component does not own that decision."
216
214
  },
@@ -1,5 +1,7 @@
1
1
  # Profile
2
2
 
3
+ > 🇰🇷 한국어: [`i18n/ko/schema/components/carousel/profile.md`](../../../i18n/ko/schema/components/carousel/profile.md)
4
+
3
5
  Sub-component of the [Carousel](./carousel.md) family. Horizontally-scrolling rail of fixed-width (176px) profile cards — channels, user profiles, or company channels grouped under a single editorial heading. Each card carries a cover band, an overlapping 64-rung avatar, entity name + follower count, a metrics row or two-line description, and a trailing follow [Toggle Button](../button/text.md).
4
6
 
5
7
  **Reach for this when** an editorial collection groups follow-able entities under a single heading — hot companies, recommended channels, suggested people. **Skip when** the rail carries content posts ([Post carousel](./post.md)), the surface needs the full list scanned vertically ([SuggestionList](../suggestion-list/suggestion-list.md) / [DirectoryList](../directory-list/directory-list.md)), or the rung is a label-only nav strip ([AvatarRail](../avatar-rail/avatar-rail.md)).
@@ -152,7 +154,7 @@ import { Carousel, ProfileCarousel } from '@teamblind-chorus/ui';
152
154
  - **metrics** *(optional)* — row of `icon + value` chips: `star → StarFillIcon (sys.color.icon.yellow)`, `pulse → PulseFillIcon (sys.color.success)`, `heart → HeartFillIcon (sys.color.icon.red)`. Mutually exclusive with `description`.
153
155
  - **description** *(optional)* — two-line clamped paragraph that replaces the metrics row when present. Block height fixed to two lines of `sys.typo.label.sm` regardless of copy length, so card height stays consistent across metrics-carrying and copy-carrying cards.
154
156
  - **followAction** — full-width [Toggle Button](../button/text.md) (`variant={'toggle'}`); `Follow` (inactive) / `Following` (active).
155
- - **pagination** — one dot per card. Active dot paints `sys.color.onSurface`; rest paint `sys.color.outlineVariant`. Decorative.
157
+ - **pagination** — [Pagination](../pagination/pagination.md) component, one dot per card (`count` = card count, `activeIndex` from scroll position). Decorative dot tokens and the `aria-hidden` contract live on its spec; the carousel centers the intrinsic-width row (`align-self: center`).
156
158
 
157
159
  ## Anatomy
158
160
 
@@ -168,7 +170,7 @@ import { Carousel, ProfileCarousel } from '@teamblind-chorus/ui';
168
170
  | metric chip | `sys.icon.md` glyph + `sys.typo.label.sm` value; star → `StarFillIcon` (`sys.color.icon.yellow`), pulse → `PulseFillIcon` (`sys.color.success`), heart → `HeartFillIcon` (`sys.color.icon.red`) |
169
171
  | description | `sys.typo.label.sm` / `sys.color.onSurfaceVariant`, centered, two-line clamp with trailing ellipsis. Two-layer DOM — outer container owns the same fixed-height slot as `metrics row` (min/max-height = 2 label.sm lines); inner `<p>` owns the `-webkit-line-clamp: 2` truncation. Split sidesteps a Chrome quirk where `display: -webkit-box` and explicit `height` on one element break the third-line clip. |
170
172
  | followAction | [Toggle Button](../button/text.md) (Chip-toggle anatomy), stretched to full card width |
171
- | pagination dot | 6×6, `sys.radius.full`; active `sys.color.onSurface`, inactive `sys.color.outlineVariant` |
173
+ | pagination dot | [Pagination](../pagination/pagination.md) component delegated verbatim — 6 × 6 dots, active/inactive colors, and row gap bind on its spec |
172
174
 
173
175
  ## States
174
176
 
@@ -80,7 +80,7 @@
80
80
  },
81
81
  "pagination": {
82
82
  "required": true,
83
- "description": "Row of dots below the pager — one per card. Active dot paints `sys.color.onSurface`; the rest paint `sys.color.outlineVariant`. Decorative (`aria-hidden`); the active index updates from the pager's scroll position via IntersectionObserver.",
83
+ "description": "Row of dots below the pager — rendered by the [Pagination](../pagination/pagination.md) component (`count` = card count, `activeIndex` from the pager's scroll position via IntersectionObserver). Dot size / gap / colors and the `aria-hidden` decorative contract all come from the Pagination spec; Pagination renders nothing below two cards. Pagination is an intrinsic-width inline element — the carousel centers it (`align-self: center` on the carousel's own selector).",
84
84
  "intrinsic": true
85
85
  }
86
86
  },
@@ -123,10 +123,8 @@
123
123
  "followActionRendersAs": "Button variant='toggle' — Chip-toggle anatomy. Stretched to full card width via a wrapper rule. All state tokens delegate to the Toggle Button (Chip) family.",
124
124
  "cardPaddingBlock": "sys.layout.stack.sm",
125
125
  "cardPaddingInline": "sys.layout.container.md",
126
- "paginationDotSize": "ref.space.75",
127
- "paginationDotGap": "sys.layout.inline.sm",
128
- "paginationActiveColor": "sys.color.onSurface",
129
- "paginationInactiveColor": "sys.color.outlineVariant"
126
+ "paginationRendersAs": "Pagination component — dot size / gap / radius and active / inactive colors all delegate to the [Pagination](../pagination/pagination.md) spec.",
127
+ "paginationAlign": "center — Pagination is an intrinsic-width inline element; the carousel binds `align-self: center` on its own selector to center the row under the pager."
130
128
  },
131
129
  "itemProps": {
132
130
  "id": {
@@ -198,7 +196,7 @@
198
196
  "fixedCardWidth": "Every profile card paints a fixed 176px footprint — the carousel never reflows the card to fit the available inline width. The trailing peek (`ref.space.500`, 40px) of the next card is always visible regardless of viewport.",
199
197
  "horizontalScrollSnap": "scroll-snap-type: x mandatory; each card declares scroll-snap-align: start.",
200
198
  "stickyToLeadingPadding": "After every swipe, the snapped card aligns flush with the pager's left padding (the 16 rail) — same geometry contract as PostCarousel.",
201
- "paginationDecorative": "Dots are decorative; tapping a dot does not scroll. Active state updates via IntersectionObserver on the snap targets.",
199
+ "paginationDecorative": "Dots render via the [Pagination](../pagination/pagination.md) component and are decorative; tapping a dot does not scroll. The carousel derives the active index via IntersectionObserver on the snap targets and passes it down as `activeIndex`.",
202
200
  "followToggleCommitsInPlace": "Tapping Follow flips the card's Toggle Button to Following and stays there. State is owned by the consumer via items[i].followed + onFollowChange.",
203
201
  "cardClickRoutes": "When an item carries `onClick`, the card surface becomes the click target. The follow toggle intercepts the tap so it routes independently."
204
202
  },
@@ -1,5 +1,7 @@
1
1
  # Chip
2
2
 
3
+ > 🇰🇷 한국어: [`i18n/ko/schema/components/chip/chip.md`](../../../i18n/ko/schema/components/chip/chip.md)
4
+
3
5
  A small, content-shaped control or label — chip-shaped affordance for two anchoring roles. **Filter** is the selectable, capsule-shaped choice the user toggles to refine a set (search filters, taxonomy switches, faceted lists). **Tag** is the static, square-cornered label naming attached metadata (categories on a card, attributes on a profile, opt-in/out tokens on a settings row). Each family carries its own slot vocabulary, geometry, and tone; the cross-family contract is limited to the shared label rung (12px Semibold, 4px block padding, 4px label-slot inset producing the visible icon-to-label rhythm) and the Chorus-wide color / state / focus tokens in [`DESIGN.md`](../../DESIGN.md). The two diverge on min-height and horizontal padding by role — **Filter** sits at 32 / 12 (tappable affordance, shared verbatim with [Toolbar Button](../button/toolbar.md) and [Tabs Segmented](../tabs/segmented.md)); **Tag** tightens to 24 / 8 (passive metadata that should not invite touch).
4
6
 
5
7
  **Layout inset.** `inline` — slot atom. A single Chip has no page-rail responsibility; the surrounding container places it (List row leading, Feed footer, FormField suffix). A Chip *group* — multiple Filter chips arranged as a filter rail — is rail-responsible: place the group as a direct child of the page shell (no `padding-inline` / `px-*` wrapper) and let its outer container pay the gutter once, so the first chip's leading edge aligns with section H2s and list-row leading content. See [`AGENTS.md` § Composition rules](../../../AGENTS.md#composition-rules).
@@ -1,5 +1,7 @@
1
1
  # Filter
2
2
 
3
+ > 🇰🇷 한국어: [`i18n/ko/schema/components/chip/filter.md`](../../../i18n/ko/schema/components/chip/filter.md)
4
+
3
5
  The selectable chip — a capsule-shaped toggle for refining a set. Unselected is a transparent hairline-outlined pill that adopts whatever surface sits behind it; selected swaps to an inverse fill. Optional leading/trailing icons compose without changing footprint.
4
6
 
5
7
  **Reach for this when** the user narrows a set by toggling one or more independent criteria — multiple chips can be on at once. **Skip when** the choices are mutually-exclusive view modes — use [Segmented](../tabs/segmented.md) instead.
@@ -92,6 +92,25 @@
92
92
  "opacity": "sys.state.pressed"
93
93
  }
94
94
  },
95
+ "focused": {
96
+ "overlay": {
97
+ "color": "label",
98
+ "opacity": "sys.state.focus"
99
+ },
100
+ "focusRing": {
101
+ "composition": "outward",
102
+ "layer": "::after overlay — position:absolute, inset:0, no reflow (DESIGN.md Focus ring composition)",
103
+ "innerCounterRing": {
104
+ "width": "sys.borderWidth.hairline",
105
+ "color": "sys.color.focusInset"
106
+ },
107
+ "outerRing": {
108
+ "width": "sys.borderWidth.thin",
109
+ "color": "sys.color.focus"
110
+ }
111
+ },
112
+ "note": "Keyboard-focus (:focus-visible) visual. Mirrors the `focusIndicator` block (the external-reader contract); kept here so spec-only renderers see focus in the states map. Composes over the lifecycle state the chip is in; never via plain mouse click."
113
+ },
95
114
  "disabled": {
96
115
  "overlay": null,
97
116
  "containerOpacity": "sys.state.disabled",
@@ -1,5 +1,7 @@
1
1
  # Tag
2
2
 
3
+ > 🇰🇷 한국어: [`i18n/ko/schema/components/chip/tag.md`](../../../i18n/ko/schema/components/chip/tag.md)
4
+
3
5
  The informational chip — square-cornered label naming attached metadata (categories, statuses, content labels). Shorter than Filter (24 vs 32 min-height) with `sys.radius.sm` corners. Two appearances: `default` paints a translucent `sys.color.scrimSubtle` overlay (~8% inverse-tone, adopts whatever surface sits behind it); `accent` paints a tonal pale-primary container with primary label.
4
6
 
5
7
  **Reach for this when** you're naming attached metadata on rows, cards, or profiles. **Skip when** the marker signals unread / new activity on a host rather than describing it — use [Badge](../badge/badge.md) instead.
@@ -73,6 +73,25 @@
73
73
  "opacity": "sys.state.pressed"
74
74
  }
75
75
  },
76
+ "focused": {
77
+ "overlay": {
78
+ "color": "label",
79
+ "opacity": "sys.state.focus"
80
+ },
81
+ "focusRing": {
82
+ "composition": "outward",
83
+ "layer": "::after overlay — position:absolute, inset:0, no reflow (DESIGN.md Focus ring composition)",
84
+ "innerCounterRing": {
85
+ "width": "sys.borderWidth.hairline",
86
+ "color": "sys.color.focusInset"
87
+ },
88
+ "outerRing": {
89
+ "width": "sys.borderWidth.thin",
90
+ "color": "sys.color.focus"
91
+ }
92
+ },
93
+ "note": "Keyboard-focus (:focus-visible) visual. Mirrors the `focusIndicator` block (the external-reader contract); kept here so spec-only renderers see focus in the states map. Composes over the lifecycle state the chip is in; never via plain mouse click."
94
+ },
76
95
  "disabled": {
77
96
  "overlay": null,
78
97
  "containerOpacity": "sys.state.disabled",
@@ -1,5 +1,7 @@
1
1
  # Dialog
2
2
 
3
+ > 🇰🇷 한국어: [`i18n/ko/schema/components/dialog/dialog.md`](../../../i18n/ko/schema/components/dialog/dialog.md)
4
+
3
5
  A focused, opt-in interruption — a centred card over a scrim that holds the flow until the user makes a single decision.
4
6
 
5
7
  **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)).
@@ -1,5 +1,7 @@
1
1
  # Directory list
2
2
 
3
+ > 🇰🇷 한국어: [`i18n/ko/schema/components/directory-list/directory-list.md`](../../../i18n/ko/schema/components/directory-list/directory-list.md)
4
+
3
5
  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
6
 
5
7
  **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.
@@ -1,5 +1,7 @@
1
1
  # Divider
2
2
 
3
+ > 🇰🇷 한국어: [`i18n/ko/schema/components/divider/divider.md`](../../../i18n/ko/schema/components/divider/divider.md)
4
+
3
5
  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
6
 
5
7
  **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).
@@ -0,0 +1,28 @@
1
+ {
2
+ "$schema": "../../family.schema.json",
3
+ "family": "empty-state",
4
+ "name": "EmptyState",
5
+ "description": "No-data placeholder — the centered composition a surface paints when it has no content yet (an empty feed, a search with no results, a fresh inbox). Fills the space the real data would occupy with an optional monochrome illustration, a required headline, optional body copy, and an optional primary CTA that performs the one action that would fill the surface. Single-spec family. Distinct from `skeleton` (in-flight tonal placeholder for data that is loading) — EmptyState is the durable 'there is nothing here yet, here is how to start' surface.",
6
+ "useCases": [
7
+ "surface with no data yet",
8
+ "empty feed / inbox / list",
9
+ "search with no results",
10
+ "first-run / zero state",
11
+ "cleared / dismissed-everything state"
12
+ ],
13
+ "visualReuse": "open",
14
+ "layoutInset": "inline",
15
+ "spec": "empty-state.md",
16
+ "usage": {
17
+ "note": "Headline is the `headline` prop (required); `body` is the `body` prop; the CTA is the `action` object ({ label, href?/onClick? }) which renders a default-size primary Button — there is NO `cta` slot to fill with your own button. Pass an icon node to `illustration`.",
18
+ "example": "<EmptyState illustration={<ChatIcon />} headline=\"No posts yet\" body=\"Conversations you start or join will appear here.\" action={{ label: 'Start a post', onClick: () => {} }} />"
19
+ },
20
+ "subcomponents": [
21
+ {
22
+ "slug": "empty-state",
23
+ "spec": "empty-state.spec.json",
24
+ "md": "empty-state.md",
25
+ "default": true
26
+ }
27
+ ]
28
+ }