@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,161 @@
1
+ # FAB
2
+
3
+ Floating action button — surface-elevated commit anchored to the canvas, reachable while content scrolls. Single fixed rung with a pill geometry (`sys.radius.full`) and floating elevation (`sys.elevation.floating`); label and icon both optional, at least one present.
4
+
5
+ **Reach for this when** the canvas needs a single headline action that survives scroll — Compose, Add, Create. **Skip when** the action lives inline with content (use [Standard Button](./standard.md)) or the row is a dense toolbar ([Toolbar Button](./toolbar.md)).
6
+
7
+ **Layout inset.** inline — ships no padding outside its own pill chrome. Sits inside an overlay container anchoring it to the canvas (fixed positioner, bottom-right docked layer) with the host paying the offset to the page rail and safe-area inset. 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
+ Brand-red fill anchoring the canvas's headline action — `appearance="primary"`. One per scrollable canvas.
12
+
13
+ ```preview
14
+ button/fab/primary
15
+ ---
16
+ import { Button } from '@teamblind-chorus/ui';
17
+ import { PlusIcon } from '@teamblind-chorus/ui/icons';
18
+
19
+ <Button
20
+ variant="fab"
21
+ appearance="primary"
22
+ icon={<PlusIcon />}
23
+ >
24
+ Compose
25
+ </Button>
26
+ ```
27
+
28
+ ## Use cases
29
+
30
+ ### Secondary
31
+
32
+ Theme-toned FAB that defers to the canvas's surface tones. Reach for it when a brand-red FAB would over-claim the page hierarchy (filtered map, image-rich feed).
33
+
34
+ ```preview
35
+ button/fab/secondary
36
+ ---
37
+ import { Button } from '@teamblind-chorus/ui';
38
+ import { PlusIcon } from '@teamblind-chorus/ui/icons';
39
+
40
+ <Button
41
+ variant="fab"
42
+ appearance="secondary"
43
+ icon={<PlusIcon />}
44
+ >
45
+ Compose
46
+ </Button>
47
+ ```
48
+
49
+ ### Icon
50
+
51
+ Icon-only — 48 × 48 circle with a 24px glyph. For universally legible actions (`+`, pencil). **Requires `aria-label`.**
52
+
53
+ ```preview
54
+ button/fab/icon
55
+ ---
56
+ import { Button } from '@teamblind-chorus/ui';
57
+ import { PlusIcon } from '@teamblind-chorus/ui/icons';
58
+
59
+ <Button
60
+ variant="fab"
61
+ appearance="primary"
62
+ icon={<PlusIcon />}
63
+ aria-label="Add"
64
+ />
65
+ ```
66
+
67
+ ### Text
68
+
69
+ Label-only pill. For multi-word actions or verbs without an obvious glyph (*Save draft*).
70
+
71
+ ```preview
72
+ button/fab/text
73
+ ---
74
+ import { Button } from '@teamblind-chorus/ui';
75
+
76
+ <Button variant="fab" appearance="primary">
77
+ Compose
78
+ </Button>
79
+ ```
80
+
81
+ ### Extended
82
+
83
+ Icon + label. Default for primary canvas commits where space allows (desktop canvases, mobile sheets).
84
+
85
+ ```preview
86
+ button/fab/extended
87
+ ---
88
+ import { Button } from '@teamblind-chorus/ui';
89
+ import { PlusIcon } from '@teamblind-chorus/ui/icons';
90
+
91
+ <Button
92
+ variant="fab"
93
+ appearance="primary"
94
+ icon={<PlusIcon />}
95
+ >
96
+ Add item
97
+ </Button>
98
+ ```
99
+
100
+ ### Focus indicator
101
+
102
+ Standard ring with the FAB's floating elevation stacked underneath. See [Focus ring composition](../../DESIGN.md#focus-ring-composition).
103
+
104
+ ```preview
105
+ button/fab/focused
106
+ ---
107
+ import { Button } from '@teamblind-chorus/ui';
108
+ import { PlusIcon } from '@teamblind-chorus/ui/icons';
109
+
110
+ <Button variant="fab" appearance="primary" state="focused" icon={<PlusIcon />}>
111
+ Compose
112
+ </Button>
113
+ ```
114
+
115
+ ## Slots
116
+
117
+ - **icon** (optional) — 24px glyph. Inherits foreground via `currentColor` per the [family rule](./button.md#icon-colour-inheritance-family-wide).
118
+ - **label** (optional) — short verb phrase. Internally padded so the glyph never crowds the rounded ends.
119
+
120
+ At least one must be present — combinations: [Icon](#icon), [Text](#text), [Extended](#extended).
121
+
122
+ ## Appearance
123
+
124
+ Two appearances on the standard [Button](./button.md) emphasis ladder — `primary` for the canvas headline commit, `secondary` for the quieter alternative. FAB wears `brand` (red) rather than `color.primary` (blue) — the screen's single high-attention moment.
125
+
126
+ | Appearance | Background | Label / icon color | When to reach for it |
127
+ |-------------|----------------------------------|----------------------|---------------------------------------------------------------------------------------------------|
128
+ | `primary` | `sys.color.brand` | `sys.color.onBrand` | Brand-red commit anchoring the canvas's next step (Compose, Add, Create). |
129
+ | `secondary` | `sys.color.surfaceContainerHigh` | `sys.color.onSurface` | Theme-toned alternative on dense/chromatic canvases where brand-red would over-claim. |
130
+
131
+ ## Sizes
132
+
133
+ Single fixed rung — no size axis; same control on every viewport.
134
+
135
+ | Property | Value | Token |
136
+ |-----------------------------------|----------------------|-------------------------------------|
137
+ | Min-height | 48px | `ref.space.600` ‡ |
138
+ | Padding (all sides) | 12px | `sys.layout.container.sm` |
139
+ | Icon size | 24px | `sys.icon.lg` |
140
+ | Slot gap (icon ↔ label) | 0 | — |
141
+ | Label inset (within label slot) | 4px (all sides) | `sys.layout.container.2xs` |
142
+ | Radius | pill / circle | `sys.radius.full` |
143
+ | Elevation | floating | `sys.elevation.floating` |
144
+
145
+ ‡ **min-height** binds to raw `ref.space.*` — `sys.*` does not expose a 48px step.
146
+
147
+ **Label-slot 4px inset.** Outer 12px padding alone would crowd the rounded ends; the label slot adds another `container.2xs` only when text is present:
148
+
149
+ - **Text-only** — clears 16px each side (12 + 4).
150
+ - **Icon-only** — clean 48 × 48 (12 + 24 + 12).
151
+ - **Icon + text** — icon flush at 12px, label with 4px inset, trailing 12px padding.
152
+
153
+ ## States
154
+
155
+ The focus ring is a `position: absolute` pseudo-element stacked above the FAB's `box-shadow` so the floating elevation survives focus.
156
+
157
+ **No disabled state.** If the action isn't available, hide the FAB rather than render it dimmed.
158
+
159
+ ## Focus indicator
160
+
161
+ Standard outward ring on a `::after` overlay above the FAB's `box-shadow`. Trigger: `:focus-visible`. See [Focus ring composition](../../DESIGN.md#focus-ring-composition).
@@ -0,0 +1,106 @@
1
+ {
2
+ "$schema": "../../spec.schema.json",
3
+ "name": "Fab",
4
+ "family": "button",
5
+ "subcomponent": "fab",
6
+ "description": "Floating action button. Single fixed sizing rung, pill geometry, floating elevation. No disabled state.",
7
+ "element": "button",
8
+ "props": {
9
+ "variant": {
10
+ "type": "literal",
11
+ "value": "fab"
12
+ },
13
+ "appearance": {
14
+ "type": "enum",
15
+ "values": [
16
+ "primary",
17
+ "secondary"
18
+ ],
19
+ "default": "primary"
20
+ },
21
+ "icon": {
22
+ "type": "node",
23
+ "optional": true
24
+ },
25
+ "label": {
26
+ "type": "node",
27
+ "optional": true,
28
+ "note": "Provide as children. At least one of icon/label required."
29
+ }
30
+ },
31
+ "slots": {
32
+ "icon": {
33
+ "required": false,
34
+ "description": "24px (sys.icon.lg) glyph naming the action. Optional but at least one of icon/label required.",
35
+ "accepts": [
36
+ "icon"
37
+ ]
38
+ },
39
+ "label": {
40
+ "required": false,
41
+ "description": "Short verb phrase. When present, padded internally so glyph never crowds the pill ends.",
42
+ "accepts": [
43
+ "text"
44
+ ]
45
+ }
46
+ },
47
+ "sizing": {
48
+ "minHeight": "ref.space.600",
49
+ "padding": "sys.layout.container.sm",
50
+ "iconSize": "sys.icon.lg",
51
+ "slotGap": "0",
52
+ "labelInset": "sys.layout.container.2xs",
53
+ "radius": "sys.radius.full",
54
+ "elevation": "sys.elevation.floating",
55
+ "labelTypo": "sys.typo.label.lg"
56
+ },
57
+ "appearances": {
58
+ "primary": {
59
+ "background": "sys.color.brand",
60
+ "label": "sys.color.onBrand"
61
+ },
62
+ "secondary": {
63
+ "background": "sys.color.surfaceContainerHigh",
64
+ "label": "sys.color.onSurface"
65
+ }
66
+ },
67
+ "states": {
68
+ "default": {
69
+ "overlay": null
70
+ },
71
+ "hovered": {
72
+ "overlay": {
73
+ "color": "label",
74
+ "opacity": "sys.state.hover"
75
+ }
76
+ },
77
+ "pressed": {
78
+ "overlay": {
79
+ "color": "label",
80
+ "opacity": "sys.state.pressed"
81
+ }
82
+ }
83
+ },
84
+ "focusIndicator": {
85
+ "description": "Keyboard-focus visual — an accessibility indicator, not a lifecycle state. Composes over whichever lifecycle state the FAB is in. The ring stacks above the FAB's floating elevation so the lift survives focus. The `states.focused` block above is kept for JSX runtime consumers; this block is the parallel external-reader contract.",
86
+ "composition": "outward",
87
+ "compositionReason": "Action affordance with breathing room around it; the 3px outward extent is reserved by the surrounding layout.",
88
+ "overlay": {
89
+ "color": "label",
90
+ "opacity": "sys.state.focus"
91
+ },
92
+ "ring": {
93
+ "outerWidth": "sys.borderWidth.thin",
94
+ "outerColor": "sys.color.focus",
95
+ "insetWidth": "sys.borderWidth.hairline",
96
+ "insetColor": "sys.color.focusInset"
97
+ },
98
+ "trigger": ":focus-visible (keyboard / programmatic focus, never plain mouse click)"
99
+ },
100
+ "forbidden": [
101
+ "more than one fab per screen — the FAB is the single canonical commit",
102
+ "fab styled with sys.color.primary fill — the FAB carries sys.color.brand by anatomy contract",
103
+ "destructive flavor on a FAB — destructive commits live inside Dialog / BottomSheet, never a floating commit",
104
+ "fab placed inline in flow — it floats over content, anchored bottom-right by the page shell"
105
+ ]
106
+ }
@@ -0,0 +1,141 @@
1
+ # Icon
2
+
3
+ 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
+
5
+ **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.
6
+
7
+ **Layout inset.** inline — ships no padding outside its own circular target. Sits inside a host slot (NavigationBar trailing rail, Dialog header, feed-row action cluster) with the host paying surrounding rhythm. 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
+ Transparent capsule with a single glyph in `onSurface`. `large` is the default; flip to `medium` for inside-control density.
12
+
13
+ ```preview
14
+ button/icon/default
15
+ ---
16
+ import { Button } from '@teamblind-chorus/ui';
17
+ import { SearchIcon } from '@teamblind-chorus/ui/icons';
18
+
19
+ <Button variant="icon" size="large" icon={<SearchIcon />} aria-label="Search" />
20
+ ```
21
+
22
+ ## Use cases
23
+
24
+ ### Inverse
25
+
26
+ Mirror for inverse hosts (Toast dismiss, coach-mark close). Glyph paints in `inverseOnSurface` against the host's `inverseSurface` fill; state overlays mix from the same token.
27
+
28
+ ```preview
29
+ button/icon/inverse
30
+ ---
31
+ import { Button } from '@teamblind-chorus/ui';
32
+ import { XIcon } from '@teamblind-chorus/ui/icons';
33
+
34
+ <Button variant="icon" size="medium" appearance="inverse" icon={<XIcon />} aria-label="Dismiss" />
35
+ ```
36
+
37
+ ### Custom palette colours
38
+
39
+ Outside the named appearances, the glyph inherits `currentColor` — any Chorus icon-paint token works. Reach for a custom colour when the glyph carries semantic weight (favorite star → `sys.color.icon.yellow`, success check → `sys.color.success`, warning bolt, channel-branded glyph in a brand-tinted host). Apply via inline `color` so state overlays still mix from the same token at standard `sys.state.*` opacities — never override `background` or wrap in another element to recolour.
40
+
41
+ ```preview
42
+ button/icon/custom-color
43
+ ---
44
+ import { Button } from '@teamblind-chorus/ui';
45
+ import { StarFillIcon } from '@teamblind-chorus/ui/icons';
46
+
47
+ <div style={{ display: 'inline-flex', gap: 'var(--sys-layout-inline-xl)' }}>
48
+ <Button variant="icon" icon={<StarFillIcon />} aria-label="Favorite — inactive" style={{ color: 'var(--sys-color-icon-muted)' }} />
49
+ <Button variant="icon" icon={<StarFillIcon />} aria-label="Favorite — active" style={{ color: 'var(--sys-color-icon-yellow)' }} />
50
+ </div>
51
+ ```
52
+
53
+ Pick from `sys.color.*` — semantic roles (`primary` / `success` / `error` / …) for status pairs that also carry a background, and the dedicated `sys.color.icon.*` palette (`muted` / `yellow` / `red` / `blue` / `green` / `purple`) for standalone semantic glyphs. Reaching past sys into `ref.palette.*`, hardcoded hex, and Tailwind colour utilities are all forbidden.
54
+
55
+ ### Group
56
+
57
+ Three Icon Buttons in a row — common shape on the [Navigation bar](../navigation-bar/navigation-bar.md) Main trailing slot. Adjacent buttons sit 16px apart (`sys.layout.inline.xl`); with optical alignment on, that gap *is* the visible glyph-to-glyph distance.
58
+
59
+ ```preview
60
+ button/icon/group
61
+ ---
62
+ import { Button } from '@teamblind-chorus/ui';
63
+ import { SearchIcon, ChatIcon, ProfileIcon } from '@teamblind-chorus/ui/icons';
64
+
65
+ <div style={{ display: 'inline-flex', gap: 'var(--sys-layout-inline-xl)' }}>
66
+ <Button variant="icon" icon={<SearchIcon />} aria-label="Search" />
67
+ <Button variant="icon" icon={<ChatIcon />} aria-label="Messages" />
68
+ <Button variant="icon" icon={<ProfileIcon />} aria-label="Profile" />
69
+ </div>
70
+ ```
71
+
72
+ ### Focus indicator
73
+
74
+ Standard ring (see [Focus ring composition](../../DESIGN.md#focus-ring-composition)).
75
+
76
+ ```preview
77
+ button/icon/focused
78
+ ---
79
+ import { Button } from '@teamblind-chorus/ui';
80
+ import { SearchIcon } from '@teamblind-chorus/ui/icons';
81
+
82
+ <Button variant="icon" icon={<SearchIcon />} aria-label="Search" state="focused" />
83
+ ```
84
+
85
+ ## Slots
86
+
87
+ - **icon** — required glyph, sized at the rung's icon token. Inherits foreground via `currentColor` per the [family icon-colour rule](./button.md#icon-colour-inheritance-family-wide).
88
+ - **aria-label** — required accessible name; Icon Button has no visible text.
89
+
90
+ ## Anatomy
91
+
92
+ | Property | Token |
93
+ |-----------------------|----------------------------------------------------------------|
94
+ | Background (rest) | `transparent` |
95
+ | Border | none |
96
+ | Icon color | per appearance (`onSurface` / `inverseOnSurface`) |
97
+ | Hover background | icon color at `sys.state.hover` (8%) opacity |
98
+ | Pressed background | icon color at `sys.state.pressed` (16%) opacity |
99
+
100
+ ## Appearance
101
+
102
+ Two named appearances; geometry identical, only the glyph colour pair flips.
103
+
104
+ | Appearance | Background | Border | Icon color | When to reach for it |
105
+ |-------------|---------------|--------|----------------------------------|----------------------|
106
+ | `default` | `transparent` | none | `sys.color.onSurface` | Every regular page surface. |
107
+ | `inverse` | `transparent` | none | `sys.color.inverseOnSurface` | For use inside an inverse host (Toast dismiss, coach-mark close). |
108
+
109
+ ## Sizes
110
+
111
+ Two rungs. Padding is the single sizing token — `sys.layout.container.xs` (8px) on every edge — so footprint falls out of the icon scale without explicit `width`/`height`. Default is `large`; reach for `medium` only inside another control's chrome, since it falls below the WCAG 24 × 24 floor for top-level commits.
112
+
113
+ | Rung | Capsule footprint | Padding (all sides) | Icon | Radius |
114
+ |-----------|---------------------|----------------------------------|---------------------|-------------------|
115
+ | `large` | 40 × 40 (implicit) | `sys.layout.container.xs` (8) | `sys.icon.lg` (24) | `sys.radius.full` |
116
+ | `medium` | 32 × 32 (implicit) | `sys.layout.container.xs` (8) | `sys.icon.md` (16) | `sys.radius.full` |
117
+
118
+ ## States
119
+
120
+ The overlay paints the **icon color** over the transparent container at the state's opacity.
121
+
122
+ | State | Overlay opacity | Additional |
123
+ |------------|----------------------------|-----------------------------------------------------------------------------|
124
+ | `default` | — | Transparent capsule, glyph at `onSurface`. |
125
+ | `hovered` | `sys.state.hover` (8%) | Pointer-driven via `:hover`. |
126
+ | `pressed` | `sys.state.pressed` (16%) | Pointer-driven via `:active`. |
127
+ | `disabled` | overlay suppressed | Glyph at `sys.state.disabled` (40%) opacity, focus ring suppressed, `cursor: not-allowed`. |
128
+
129
+ ## Focus indicator
130
+
131
+ Standard ring (see [Focus ring composition](../../DESIGN.md#focus-ring-composition)). Trigger: `:focus-visible`.
132
+
133
+ ## Optical alignment
134
+
135
+ Transparent at rest — the eye lands on the glyph. Default rendering applies `margin: calc(-1 × sys.layout.container.xs)` on every side so the **glyph is the layout box**. Not opt-in.
136
+
137
+ | Where it lands | Effect |
138
+ |---|---|
139
+ | At a row / header / toolbar edge | Glyph flush with the content rail; hover capsule bleeds into the gutter. |
140
+ | In a flush row of adjacent Icon Buttons | Capsule edges touch chrome-to-chrome without phantom padding. |
141
+ | Inside another control's chrome | Glyph occupies only its own footprint. |
@@ -0,0 +1,164 @@
1
+ {
2
+ "$schema": "../../spec.schema.json",
3
+ "name": "Button",
4
+ "family": "button",
5
+ "subcomponent": "icon",
6
+ "description": "Icon-only commit surface — a circular, transparent target carrying a single glyph. Reach for this whenever a control's identity is the glyph itself and a text label would be redundant: leading menu / trailing actions on the Navigation Bar, the dismiss '×' on a Dialog header, the '⋯' more affordance on a feed row footer. The capsule is transparent at rest and paints the standard state overlays on hover / pressed.",
7
+ "element": "button",
8
+ "props": {
9
+ "variant": {
10
+ "type": "literal",
11
+ "value": "icon"
12
+ },
13
+ "size": {
14
+ "type": "enum",
15
+ "values": [
16
+ "large",
17
+ "medium"
18
+ ],
19
+ "default": "large"
20
+ },
21
+ "appearance": {
22
+ "type": "enum",
23
+ "values": [
24
+ "default",
25
+ "inverse"
26
+ ],
27
+ "default": "default"
28
+ },
29
+ "icon": {
30
+ "type": "node",
31
+ "required": true,
32
+ "description": "The glyph. Required."
33
+ },
34
+ "aria-label": {
35
+ "type": "string",
36
+ "required": true,
37
+ "description": "Accessible name — required because Icon Button carries no visible text."
38
+ },
39
+ "disabled": {
40
+ "type": "boolean",
41
+ "default": false
42
+ }
43
+ },
44
+ "slots": {
45
+ "icon": {
46
+ "required": true,
47
+ "description": "The glyph. Sized at the rung's icon token — `sys.icon.lg` (24) on `large`, `sys.icon.md` (16) on `medium`.",
48
+ "accepts": [
49
+ "icon"
50
+ ]
51
+ },
52
+ "aria-label": {
53
+ "required": true,
54
+ "description": "Accessible name. Required — screen readers and keyboard users rely on it to identify the action.",
55
+ "accepts": [
56
+ "text"
57
+ ]
58
+ }
59
+ },
60
+ "sizes": {
61
+ "large": {
62
+ "padding": "sys.layout.container.xs",
63
+ "iconSize": "sys.icon.lg",
64
+ "radius": "sys.radius.full",
65
+ "footprint": "40 × 40 (implicit, from 8 + 24 + 8)"
66
+ },
67
+ "medium": {
68
+ "padding": "sys.layout.container.xs",
69
+ "iconSize": "sys.icon.md",
70
+ "radius": "sys.radius.full",
71
+ "footprint": "32 × 32 (implicit, from 8 + 16 + 8)"
72
+ }
73
+ },
74
+ "appearances": {
75
+ "default": {
76
+ "background": "transparent",
77
+ "border": null,
78
+ "icon": "sys.color.onSurface",
79
+ "note": "Transparent capsule with the glyph in `onSurface`. The canonical Icon Button chrome, used on every regular page surface."
80
+ },
81
+ "inverse": {
82
+ "background": "transparent",
83
+ "border": null,
84
+ "icon": "sys.color.inverseOnSurface",
85
+ "note": "Mirror of `default` for use inside an inverse host (Toast, coach-mark, snackbar). Glyph paints in `inverseOnSurface` so it reads against the host's `inverseSurface` fill; state overlays mix from the same token so the recipe carries over without per-host tuning."
86
+ }
87
+ },
88
+ "customGlyphColor": {
89
+ "allowed": true,
90
+ "description": "Outside the named `default` / `inverse` appearances, the glyph colour is open — the icon inherits `currentColor`, so any Chorus icon-paint token can be applied via inline `color` (e.g. `style={{ color: 'var(--sys-color-icon-yellow)' }}` for an active favorite star, `var(--sys-color-success)` for a confirm check, `var(--sys-color-icon-muted)` for the unpressed partner). State overlays still mix from the same token at the standard `hovered`/`pressed`/`focused` state opacities, so hover / pressed / focus carry over without per-host tuning.",
91
+ "constraints": [
92
+ "Pick a Chorus colour token. Two valid sources: (a) a status-pair role (`sys.color.primary`, `sys.color.success`, `sys.color.error`, etc.) when the glyph travels with its own background; (b) the dedicated icon palette (`sys.color.icon.muted`, `sys.color.icon.yellow`, `sys.color.icon.red`, `sys.color.icon.blue`, `sys.color.icon.green`, `sys.color.icon.purple`) for standalone semantic glyphs. Reaching past sys into the raw palette (`ref.palette.yellow.500` etc.), raw hex, and Tailwind colour utilities are all forbidden.",
93
+ "Override the icon's `color` only — never `background`, never wrap in another element to restyle.",
94
+ "Reach for a custom colour when the glyph itself carries semantic weight (favorite, success, warning, brand-tinted host). Otherwise stick to `default` / `inverse`.",
95
+ "The `destructive` flavor (`sys.color.error`) is a named convenience for the same pattern."
96
+ ]
97
+ },
98
+ "flavors": {
99
+ "destructive": {
100
+ "description": "Swaps the glyph color to the error family. Reserved for irreversible commits (a 'Remove' trash glyph, a 'Discard' close on a destructive sheet header).",
101
+ "appearances": {
102
+ "default": {
103
+ "background": "transparent",
104
+ "border": null,
105
+ "icon": "sys.color.error"
106
+ },
107
+ "inverse": {
108
+ "background": "transparent",
109
+ "border": null,
110
+ "icon": "sys.color.error"
111
+ }
112
+ }
113
+ }
114
+ },
115
+ "states": {
116
+ "default": {
117
+ "overlay": null
118
+ },
119
+ "hovered": {
120
+ "overlay": {
121
+ "color": "icon",
122
+ "opacity": "sys.state.hover"
123
+ }
124
+ },
125
+ "pressed": {
126
+ "overlay": {
127
+ "color": "icon",
128
+ "opacity": "sys.state.pressed"
129
+ }
130
+ },
131
+ "disabled": {
132
+ "overlay": null,
133
+ "containerOpacity": "sys.state.disabled",
134
+ "suppressFocusRing": true,
135
+ "cursor": "not-allowed"
136
+ }
137
+ },
138
+ "focusIndicator": {
139
+ "description": "Keyboard-focus visual — an accessibility indicator, not a lifecycle state. Composes over whichever lifecycle state the button is in. The `states.focused` block above is kept for JSX runtime consumers; this block is the parallel external-reader contract.",
140
+ "composition": "outward",
141
+ "compositionReason": "Action affordance with breathing room around it; the 3px outward extent is reserved by the surrounding layout.",
142
+ "overlay": {
143
+ "color": "icon",
144
+ "opacity": "sys.state.focus"
145
+ },
146
+ "ring": {
147
+ "outerWidth": "sys.borderWidth.thin",
148
+ "outerColor": "sys.color.focus",
149
+ "insetWidth": "sys.borderWidth.hairline",
150
+ "insetColor": "sys.color.focusInset"
151
+ },
152
+ "trigger": ":focus-visible (keyboard / programmatic focus, never plain mouse click)"
153
+ },
154
+ "behavior": {
155
+ "footprint": "Content-sized — no explicit width / height. The 40 (large) / 32 (medium) falls out of padding + icon + padding.",
156
+ "group": "Horizontally-grouped Icon Buttons share a row gap of `sys.layout.inline.xl` (16) on both rungs. Because Icon Button applies optical alignment by default (each capsule's chrome bleeds outward by its 8px padding so the visible glyph is the layout box), the chrome-to-chrome gap IS the visible glyph-to-glyph distance — no per-capsule padding to add on top. The hover capsules of adjacent buttons each extend 8px beyond their glyph; with a 16px row gap, two hover capsules touch chrome-to-chrome but never collide.",
157
+ "opticalAlignmentDefault": "Icon Button applies optical alignment as the **default** rendering rule, on all four sides: the capsule is transparent at rest, so the visible glyph (not the padded chrome) defines the button's layout edges. Implemented as a base `margin: calc(-1 × sys.layout.container.xs)` on `.chorus-button--icon`. Hover capsule still paints on every side — the chrome becomes a hover-only affordance, doubling as a hit-zone enlargement. Consumers do not opt in; alignment with the surrounding content rail just works."
158
+ },
159
+ "forbidden": [
160
+ "Icon button without aria-label",
161
+ "raw <button> with a chorus-icon class inside instead of the chorus-button--icon chrome",
162
+ "icon size below sys.icon.md (20px) — small icon buttons fail touch-target minima"
163
+ ]
164
+ }