@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,212 @@
1
+ # Filter
2
+
3
+ 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
+
5
+ **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.
6
+
7
+ **Layout inset.** inline — content-sized, inherits its surface's padding; rails compose via gap, not chrome.
8
+
9
+ ## Default
10
+
11
+ At-rest — transparent fill with a hairline `outlineVariant` stroke so the chip sits on any surface without colliding with the surface ladder.
12
+
13
+ ```preview
14
+ chip/filter/unselected
15
+ ---
16
+ import { Chip } from '@teamblind-chorus/ui';
17
+
18
+ <Chip variant="filter">
19
+ All
20
+ </Chip>
21
+ ```
22
+
23
+ ## Use cases
24
+
25
+ ### Selected
26
+
27
+ Active — inverse-toned fill. Toggle the `selected` flag on the same chip element.
28
+
29
+ ```preview
30
+ chip/filter/selected
31
+ ---
32
+ import { Chip } from '@teamblind-chorus/ui';
33
+
34
+ <Chip
35
+ variant="filter"
36
+ selected
37
+ >
38
+ All
39
+ </Chip>
40
+ ```
41
+
42
+ ### With icon
43
+
44
+ Facet glyph before the label — tag for category, magnifier for search, check on selection.
45
+
46
+ ```preview
47
+ chip/filter/leading-icon
48
+ ---
49
+ import { Chip } from '@teamblind-chorus/ui';
50
+ import { CheckedIcon } from '@teamblind-chorus/ui/icons';
51
+
52
+ <Chip
53
+ variant="filter"
54
+ leadingIcon={<CheckedIcon />}
55
+ >
56
+ Selected
57
+ </Chip>
58
+ ```
59
+
60
+ ### With trailing icon
61
+
62
+ Directional/dismiss glyph after the label — chevron-down to expand, *×* to clear.
63
+
64
+ ```preview
65
+ chip/filter/trailing-icon
66
+ ---
67
+ import { Chip } from '@teamblind-chorus/ui';
68
+ import { XIcon } from '@teamblind-chorus/ui/icons';
69
+
70
+ <Chip
71
+ variant="filter"
72
+ trailingIcon={<XIcon />}
73
+ >
74
+ Today
75
+ </Chip>
76
+ ```
77
+
78
+ ### With trailing action
79
+
80
+ Pair the chip rail with a trailing accent [Text Button](../button/text.md) (`size='small'`, `appearance='accent'`) for a destination outside the filter axis — managing the set, opening keyword settings. The button is *not* a filter toggle; the chip track scrolls horizontally with a trailing 48px `mask-image` fade painted only while overflowing, and the button stays pinned outside the scroll viewport at `sys.layout.inline.xl` gap.
81
+
82
+ ```preview
83
+ chip/filter/with-trailing-action
84
+ ---
85
+ import { Chip, Button } from '@teamblind-chorus/ui';
86
+ import { ArrowDownIcon } from '@teamblind-chorus/ui/icons';
87
+
88
+ <div
89
+ style={{
90
+ display: 'flex',
91
+ alignItems: 'center',
92
+ gap: 'var(--sys-layout-inline-xl)',
93
+ width: '100%',
94
+ boxSizing: 'border-box',
95
+ }}
96
+ >
97
+ <div
98
+ style={{
99
+ display: 'flex',
100
+ alignItems: 'center',
101
+ gap: 'var(--sys-layout-inline-sm)',
102
+ flex: '1 1 auto',
103
+ minWidth: 0,
104
+ overflowX: 'auto',
105
+ scrollbarWidth: 'none',
106
+ WebkitMaskImage: 'linear-gradient(to right, black 0, black calc(100% - 48px), transparent 100%)',
107
+ maskImage: 'linear-gradient(to right, black 0, black calc(100% - 48px), transparent 100%)',
108
+ }}
109
+ >
110
+ <Chip variant="filter" selected trailingIcon={<ArrowDownIcon />}>
111
+ All keywords
112
+ </Chip>
113
+ <Chip variant="filter" selected trailingIcon={<ArrowDownIcon />}>
114
+ All channels
115
+ </Chip>
116
+ <Chip variant="filter">
117
+ Label
118
+ </Chip>
119
+ <Chip variant="filter">
120
+ Saved
121
+ </Chip>
122
+ </div>
123
+ <Button variant="text" size="small" appearance="accent" style={{ flex: '0 0 auto' }}>
124
+ Manage
125
+ </Button>
126
+ </div>
127
+ ```
128
+
129
+ ### Group
130
+
131
+ Adjacent filter chips share a 4px gap (`sys.layout.inline.sm`), left-to-right; selection is independent per chip — Filter does not enforce single-select.
132
+
133
+ ```preview
134
+ chip/filter/group
135
+ ---
136
+ import { Chip } from '@teamblind-chorus/ui';
137
+
138
+ <div style={{ display: 'flex', gap: 4 }}>
139
+ <Chip variant="filter" selected>
140
+ All
141
+ </Chip>
142
+ <Chip variant="filter">
143
+ Open
144
+ </Chip>
145
+ <Chip variant="filter">
146
+ Closed
147
+ </Chip>
148
+ <Chip variant="filter">
149
+ Archived
150
+ </Chip>
151
+ </div>
152
+ ```
153
+
154
+ ### Focus indicator
155
+
156
+ Both selection states take the same standard ring; the case below shows unselected.
157
+
158
+ ```preview
159
+ chip/filter/focused
160
+ ---
161
+ import { Chip } from '@teamblind-chorus/ui';
162
+
163
+ <Chip variant="filter" state="focused">
164
+ All
165
+ </Chip>
166
+ ```
167
+
168
+ ## Slots
169
+
170
+ - **label** — accessible name. Required, single line.
171
+ - **leadingIcon** (optional) — facet glyph before the label.
172
+ - **trailingIcon** (optional) — directional/dismiss glyph after the label.
173
+
174
+ ## Sizes
175
+
176
+ Single fixed footprint, consistent across breakpoints.
177
+
178
+ | Property | Value | Token |
179
+ |-----------------------------------|----------------------|-------------------------------------|
180
+ | Min-height | 32px | `ref.space.400` ‡ |
181
+ | Padding (block × inline) | 4 × 12 | `sys.layout.container.2xs` × `sys.layout.container.sm` |
182
+ | Label inset (within label slot) | 4px (horizontal) | `sys.layout.container.2xs` |
183
+ | Slot gap (icon ↔ label) | 0 | — † |
184
+ | Radius | pill | `sys.radius.full` |
185
+ | Label | 12 / Semibold | `sys.typo.label.sm` |
186
+ | Icon | 16px | `sys.icon.md` |
187
+
188
+ ‡ Footprint shared with [Toolbar button](../button/toolbar.md) and [Tabs segmented](../tabs/segmented.md).
189
+
190
+ † Visible icon-to-label rhythm comes from the label-slot inset.
191
+
192
+ ## Variants
193
+
194
+ Single visual variant; the selected/unselected toggle swaps the container/label pair wholesale. On `selected`, the border colour goes transparent but its 1px width is held, so footprint never changes.
195
+
196
+ | State | Background | Border (1px `sys.borderWidth.hairline`) | Label / icon color |
197
+ |--------------|-------------------------------------|---------------------------------------------------------|-----------------------------------|
198
+ | unselected | `transparent` | `sys.color.outlineVariant` | `sys.color.onSurface` |
199
+ | selected | `sys.color.inverseSurface` | `transparent` | `sys.color.inverseOnSurface` |
200
+
201
+ ## States
202
+
203
+ | State | Overlay opacity | Additional |
204
+ |------------|----------------------------|-----------------------------------------------------------------------------|
205
+ | `default` | — | Container + label at rest. |
206
+ | `hovered` | `sys.state.hover` (8%) | Pointer-driven via `:hover`. |
207
+ | `pressed` | `sys.state.pressed` (16%) | Pointer-driven via `:active`. |
208
+ | `disabled` | overlay suppressed | Container at `sys.state.disabled` (40%) opacity, focus ring suppressed, `cursor: not-allowed`. |
209
+
210
+ ## Focus indicator
211
+
212
+ Standard outward ring on a `position: absolute` pseudo-element so it never affects layout; inside a [Tabs](../tabs/segmented.md) row the same layer is re-anchored inward. Trigger: `:focus-visible`. See [Focus ring composition](../../DESIGN.md#focus-ring-composition).
@@ -0,0 +1,124 @@
1
+ {
2
+ "$schema": "../../spec.schema.json",
3
+ "name": "Chip",
4
+ "family": "chip",
5
+ "subcomponent": "filter",
6
+ "description": "Selectable chip. Capsule-shaped toggle. Unselected = transparent fill + hairline outline so the chip adopts whatever surface it sits on; selected = inverse fill.",
7
+ "element": "button",
8
+ "props": {
9
+ "variant": {
10
+ "type": "literal",
11
+ "value": "filter"
12
+ },
13
+ "selected": {
14
+ "type": "boolean",
15
+ "default": false
16
+ },
17
+ "leadingIcon": {
18
+ "type": "node",
19
+ "optional": true
20
+ },
21
+ "trailingIcon": {
22
+ "type": "node",
23
+ "optional": true
24
+ },
25
+ "disabled": {
26
+ "type": "boolean",
27
+ "default": false
28
+ }
29
+ },
30
+ "slots": {
31
+ "label": {
32
+ "required": true,
33
+ "description": "Required, single line.",
34
+ "accepts": [
35
+ "text"
36
+ ]
37
+ },
38
+ "leadingIcon": {
39
+ "required": false,
40
+ "description": "Facet glyph before label.",
41
+ "accepts": [
42
+ "icon"
43
+ ]
44
+ },
45
+ "trailingIcon": {
46
+ "required": false,
47
+ "description": "Directional / dismiss glyph after label.",
48
+ "accepts": [
49
+ "icon"
50
+ ]
51
+ }
52
+ },
53
+ "sizing": {
54
+ "minHeight": "ref.space.400",
55
+ "paddingBlock": "sys.layout.container.2xs",
56
+ "paddingInline": "sys.layout.container.sm",
57
+ "labelInset": "sys.layout.container.2xs",
58
+ "slotGap": "0",
59
+ "radius": "sys.radius.full",
60
+ "labelTypo": "sys.typo.label.sm",
61
+ "iconSize": "sys.icon.md"
62
+ },
63
+ "selectionStates": {
64
+ "unselected": {
65
+ "background": "transparent",
66
+ "label": "sys.color.onSurface",
67
+ "border": {
68
+ "width": "sys.borderWidth.hairline",
69
+ "color": "sys.color.outlineVariant"
70
+ },
71
+ "note": "Transparent fill so the chip adopts whatever surface sits behind it (page, raised card, sheet) without pinning to a fixed neutral step."
72
+ },
73
+ "selected": {
74
+ "background": "sys.color.inverseSurface",
75
+ "label": "sys.color.inverseOnSurface",
76
+ "border": null
77
+ }
78
+ },
79
+ "states": {
80
+ "default": {
81
+ "overlay": null
82
+ },
83
+ "hovered": {
84
+ "overlay": {
85
+ "color": "label",
86
+ "opacity": "sys.state.hover"
87
+ }
88
+ },
89
+ "pressed": {
90
+ "overlay": {
91
+ "color": "label",
92
+ "opacity": "sys.state.pressed"
93
+ }
94
+ },
95
+ "disabled": {
96
+ "overlay": null,
97
+ "containerOpacity": "sys.state.disabled",
98
+ "suppressFocusRing": true,
99
+ "cursor": "not-allowed"
100
+ }
101
+ },
102
+ "focusIndicator": {
103
+ "description": "Keyboard-focus visual — an accessibility indicator, not a lifecycle state. Composes over whichever lifecycle state the chip is in. The `states.focused` block above is kept for JSX runtime consumers; this block is the parallel external-reader contract.",
104
+ "composition": "outward",
105
+ "compositionReason": "Action affordance with breathing room around it; the 3px outward extent is reserved by the surrounding layout.",
106
+ "overlay": {
107
+ "color": "label",
108
+ "opacity": "sys.state.focus"
109
+ },
110
+ "ring": {
111
+ "outerWidth": "sys.borderWidth.thin",
112
+ "outerColor": "sys.color.focus",
113
+ "insetWidth": "sys.borderWidth.hairline",
114
+ "insetColor": "sys.color.focusInset"
115
+ },
116
+ "trigger": ":focus-visible (keyboard / programmatic focus, never plain mouse click)"
117
+ },
118
+ "forbidden": [
119
+ "radius below sys.radius.full — filter chip is always a fully-rounded pill",
120
+ "filled gray rest state with no `selected` semantics — rest carries hairline outline + onSurface label; selected carries inverseSurface fill + inverseOnSurface label",
121
+ "border declared via `border:` instead of inset box-shadow stroke",
122
+ "trailing chevron / icon as a separate hit area — the entire chip is the click target"
123
+ ]
124
+ }
@@ -0,0 +1,137 @@
1
+ # Tag
2
+
3
+ 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
+
5
+ **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.
6
+
7
+ **Layout inset.** inline — Tag ships no padding outside its own pill chrome; the host row pays the surrounding gap and column padding. Inside a bounded surface (Card / Dialog / BottomSheet / Sheet), the host already owns the inset — see [`AGENTS.md` § Composition rules](../../../AGENTS.md#composition-rules).
8
+
9
+ ## Default
10
+
11
+ Translucent overlay fill, label-only — the overlay tints the surface one step darker (light) or lighter (dark) rather than locking to a single container tone. Omit `appearance` or pass `appearance="default"`.
12
+
13
+ ```preview
14
+ chip/tag/default
15
+ ---
16
+ import { Chip } from '@teamblind-chorus/ui';
17
+
18
+ <Chip variant="tag">
19
+ Design
20
+ </Chip>
21
+ ```
22
+
23
+ ## Use cases
24
+
25
+ ### Accent
26
+
27
+ Tonal pale-primary fill — `sys.color.primaryContainer` background, `sys.color.primary` label. Use when the tag should pop against the surface (Popular Tags in compose, highlighted hashtags); the default overlay is too quiet there.
28
+
29
+ ```preview
30
+ chip/tag/accent
31
+ ---
32
+ import { Chip } from '@teamblind-chorus/ui';
33
+
34
+ <Chip variant="tag" appearance="accent">
35
+ #sellside
36
+ </Chip>
37
+ ```
38
+
39
+ ### Dismissable
40
+
41
+ Opt-out — same chip with a trailing *×* to remove the tag. Trailing icon inherits label color via `currentColor`.
42
+
43
+ ```preview
44
+ chip/tag/dismissable
45
+ ---
46
+ import { Chip } from '@teamblind-chorus/ui';
47
+ import { XIcon } from '@teamblind-chorus/ui/icons';
48
+
49
+ <Chip
50
+ variant="tag"
51
+ trailingIcon={<XIcon />}
52
+ >
53
+ Newsletter
54
+ </Chip>
55
+ ```
56
+
57
+ ### Group
58
+
59
+ Adjacent tag chips share a 4px gap on both axes — `sys.layout.inline.sm` between siblings, `sys.layout.stack.2xs` between rows on wrap. Mixing with Filter is allowed — Tag's square + sunken tone vs Filter's pill + raised tone keeps roles legible. Tags are passive metadata, so collections exceeding the container's width **wrap** rather than scroll or truncate (set `display: flex; flex-wrap: wrap` on the container; do not use `overflow-x: auto` — horizontal scrolling belongs to tappable affordances).
60
+
61
+ ```preview
62
+ chip/tag/group
63
+ ---
64
+ import { Chip } from '@teamblind-chorus/ui';
65
+
66
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: 4, maxWidth: 320 }}>
67
+ <Chip variant="tag">Design</Chip>
68
+ <Chip variant="tag">Engineering</Chip>
69
+ <Chip variant="tag">Research</Chip>
70
+ <Chip variant="tag">Product</Chip>
71
+ <Chip variant="tag">Marketing</Chip>
72
+ <Chip variant="tag">Operations</Chip>
73
+ <Chip variant="tag">Customer success</Chip>
74
+ </div>
75
+ ```
76
+
77
+ ### Focus indicator
78
+
79
+ Only the dismissable tag is focusable; the case below shows that form. See [Focus ring composition](../../DESIGN.md#focus-ring-composition).
80
+
81
+ ```preview
82
+ chip/tag/focused
83
+ ---
84
+ import { Chip } from '@teamblind-chorus/ui';
85
+ import { XIcon } from '@teamblind-chorus/ui/icons';
86
+
87
+ <Chip variant="tag" state="focused" trailingIcon={<XIcon />}>
88
+ Newsletter
89
+ </Chip>
90
+ ```
91
+
92
+ ## Slots
93
+
94
+ - **label** — accessible name. Required, single line.
95
+ - **trailingIcon** (optional) — dismiss/opt-out glyph after the label (typically *×*). Tag does not carry a leading icon.
96
+
97
+ ## Sizes
98
+
99
+ Same min-height, vertical padding, label rung, and label-slot inset as Filter, but horizontal outer padding tightens from 12 to 8 (label-only Tag clears 12px each side; dismissable Tag reads 4px label-to-glyph then 8px to edge).
100
+
101
+ | Property | Value | Token |
102
+ |-----------------------------------|----------------------|-------------------------------------|
103
+ | Min-height | 24px | `ref.space.300` ‡ |
104
+ | Padding (block × inline) | 4 × 8 | `sys.layout.container.2xs` × `sys.layout.container.xs` |
105
+ | Label inset (within label slot) | 4px (horizontal) | `sys.layout.container.2xs` |
106
+ | Slot gap (icon ↔ label) | 0 | — † |
107
+ | Radius | 4px | `sys.radius.sm` |
108
+ | Label | 12 / Semibold | `sys.typo.label.sm` |
109
+ | Icon | 16px | `sys.icon.md` |
110
+
111
+ ‡ `min-height` binds to raw `ref.space.*` — `sys.*` does not expose a 24px step.
112
+
113
+ † Slot gap is `0` — see [Filter → Sizes](./filter.md#sizes).
114
+
115
+ ## Appearance
116
+
117
+ Two appearances; Tag never toggles.
118
+
119
+ | Appearance | Background | Label / icon color |
120
+ |------------|---------------------------------------------------------------------------------------------|------------------------------|
121
+ | `default` | `sys.color.scrimSubtle` (translucent inverse-tone overlay — black 8% light / white 8% dark) | `sys.color.onSurface` |
122
+ | `accent` | `sys.color.primaryContainer` (theme-aware) | `sys.color.primary` |
123
+
124
+ ## States
125
+
126
+ Tag is interactive only when `trailingIcon` carries a click target; otherwise the chip is presentational and the chip body takes no state.
127
+
128
+ | State | Overlay opacity | Additional |
129
+ |------------|----------------------------|-----------------------------------------------------------------------------|
130
+ | `default` | — | Container + label at rest. |
131
+ | `hovered` | `sys.state.hover` (8%) | Pointer-driven via `:hover` when the chip is interactive. |
132
+ | `pressed` | `sys.state.pressed` (16%) | Pointer-driven via `:active` when the chip is interactive. |
133
+ | `disabled` | overlay suppressed | Container at `sys.state.disabled` (40%) opacity, focus ring suppressed, `cursor: not-allowed`. |
134
+
135
+ ## Focus indicator
136
+
137
+ Standard ring on the dismissable form only. Trigger: `:focus-visible`. See [Focus ring composition](../../DESIGN.md#focus-ring-composition).
@@ -0,0 +1,104 @@
1
+ {
2
+ "$schema": "../../spec.schema.json",
3
+ "name": "Chip",
4
+ "family": "chip",
5
+ "subcomponent": "tag",
6
+ "description": "Informational chip. Square-cornered metadata label. Passive (or dismissable via trailing ×). No leading icon. Two appearances: `default` paints the translucent `sys.color.scrimSubtle` scrim (~8% inverse-tone overlay — black in light, white in dark) so the tag adopts whatever surface sits behind it; `accent` paints a tonal pale-primary container (`sys.color.primaryContainer`) with primary label for tags that need to pop against the surface.",
7
+ "element": "span",
8
+ "props": {
9
+ "variant": {
10
+ "type": "literal",
11
+ "value": "tag"
12
+ },
13
+ "trailingIcon": {
14
+ "type": "node",
15
+ "optional": true,
16
+ "description": "Dismiss glyph (×); when present, chip becomes interactive."
17
+ }
18
+ },
19
+ "slots": {
20
+ "label": {
21
+ "required": true,
22
+ "description": "Required, single line.",
23
+ "accepts": [
24
+ "text"
25
+ ]
26
+ },
27
+ "trailingIcon": {
28
+ "required": false,
29
+ "description": "Dismiss / opt-out glyph after the label.",
30
+ "accepts": [
31
+ "icon"
32
+ ]
33
+ }
34
+ },
35
+ "sizing": {
36
+ "minHeight": "ref.space.300",
37
+ "paddingBlock": "sys.layout.container.2xs",
38
+ "paddingInline": "sys.layout.container.xs",
39
+ "labelInset": "sys.layout.container.2xs",
40
+ "slotGap": "0",
41
+ "radius": "sys.radius.sm",
42
+ "labelTypo": "sys.typo.label.sm",
43
+ "iconSize": "sys.icon.md"
44
+ },
45
+ "appearances": {
46
+ "default": {
47
+ "background": "sys.color.scrimSubtle",
48
+ "label": "sys.color.onSurface",
49
+ "border": null,
50
+ "default": true,
51
+ "note": "Background is the translucent inverse-tone scrim (`sys.color.scrimSubtle` — black 8% in light mode, white 8% in dark) so the tag harmonises with whatever surface sits behind it — body, raised card, BottomSheet — instead of pinning to a fixed neutral step. Same Banner-style fill used by Progress track, StatusTag neutral, and Skeleton; sys-color is theme-aware so a single token resolves correctly in both modes."
52
+ },
53
+ "accent": {
54
+ "background": "sys.color.primaryContainer",
55
+ "label": "sys.color.primary",
56
+ "border": null,
57
+ "note": "Tonal accent: pale primary container background with primary label. Sys-color tokens are theme-aware, so no separate dark binding is needed — the resolved tokens pick the right values per theme. Use for tags that should pop against the surface (e.g. Popular Tags in compose, highlighted hashtags) where the default overlay is too quiet."
58
+ }
59
+ },
60
+ "states": {
61
+ "default": {
62
+ "overlay": null
63
+ },
64
+ "hovered": {
65
+ "overlay": {
66
+ "color": "label",
67
+ "opacity": "sys.state.hover"
68
+ }
69
+ },
70
+ "pressed": {
71
+ "overlay": {
72
+ "color": "label",
73
+ "opacity": "sys.state.pressed"
74
+ }
75
+ },
76
+ "disabled": {
77
+ "overlay": null,
78
+ "containerOpacity": "sys.state.disabled",
79
+ "suppressFocusRing": true,
80
+ "cursor": "not-allowed"
81
+ }
82
+ },
83
+ "focusIndicator": {
84
+ "description": "Keyboard-focus visual — an accessibility indicator, not a lifecycle state. Composes over whichever lifecycle state the chip is in. Applies only when the chip is interactive (a dismiss trailingIcon is present). The `states.focused` block above is kept for JSX runtime consumers; this block is the parallel external-reader contract.",
85
+ "composition": "outward",
86
+ "compositionReason": "Action affordance with breathing room around it; the 3px outward extent is reserved by the surrounding layout.",
87
+ "overlay": {
88
+ "color": "label",
89
+ "opacity": "sys.state.focus"
90
+ },
91
+ "ring": {
92
+ "outerWidth": "sys.borderWidth.thin",
93
+ "outerColor": "sys.color.focus",
94
+ "insetWidth": "sys.borderWidth.hairline",
95
+ "insetColor": "sys.color.focusInset"
96
+ },
97
+ "trigger": ":focus-visible (keyboard / programmatic focus, never plain mouse click)"
98
+ },
99
+ "forbidden": [
100
+ "radius set to sys.radius.full (pill) — tag is a sys.radius.sm square-cornered metadata pill, not a capsule chip",
101
+ "tag rendered as a clickable commit — tag is a metadata pill, not an action; the click target lives on the surrounding card / row",
102
+ "raw color override on a tag — tone is the surrounding container's secondary text color"
103
+ ]
104
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "$schema": "../../family.schema.json",
3
+ "family": "dialog",
4
+ "name": "Dialog",
5
+ "description": "Centered modal surface used for confirmations, short forms, and image-led prompts that demand a focused commit. Single-spec family.",
6
+ "useCases": [
7
+ "confirmation",
8
+ "destructive confirmation",
9
+ "image-led prompt",
10
+ "centred modal",
11
+ "short form"
12
+ ],
13
+ "visualReuse": "locked",
14
+ "layoutInset": "bounded-surface",
15
+ "wrapperGuidance": "Renders into a body portal (or owns its own surface chrome). Place the call as a sibling of the page shell \u2014 do NOT wrap in a layout container, padding div, or className=\"px-*\". Full-bleed children inside the surface body get the negative-margin opt-out \u2014 see AGENTS.md \u00a7 Composition rules.",
16
+ "usage": {
17
+ "note": "Actions are `primaryAction` / `secondaryAction` descriptor objects ({ label, onClick }) — there are NO Button children.",
18
+ "example": "<Dialog open={open} onClose={() => setOpen(false)} title=\"…\" body=\"…\" primaryAction={{ label, onClick }} secondaryAction={{ label, onClick }} />"
19
+ },
20
+ "spec": "dialog.md",
21
+ "subcomponents": [
22
+ {
23
+ "slug": "dialog",
24
+ "spec": "dialog.spec.json",
25
+ "md": "dialog.md",
26
+ "default": true
27
+ }
28
+ ]
29
+ }