@proteos/ui 0.31.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 (226) hide show
  1. package/LICENSE +105 -0
  2. package/dist/chunk-IKAX76MB.js +113 -0
  3. package/dist/chunk-IKAX76MB.js.map +1 -0
  4. package/dist/chunk-MWAXK66H.cjs +119 -0
  5. package/dist/chunk-MWAXK66H.cjs.map +1 -0
  6. package/dist/index.cjs +6246 -0
  7. package/dist/index.cjs.map +1 -0
  8. package/dist/index.d.cts +2126 -0
  9. package/dist/index.d.ts +2126 -0
  10. package/dist/index.js +6069 -0
  11. package/dist/index.js.map +1 -0
  12. package/dist/pdf-view-3ZMMS6ML.cjs +95 -0
  13. package/dist/pdf-view-3ZMMS6ML.cjs.map +1 -0
  14. package/dist/pdf-view-BV5ZD6OC.js +93 -0
  15. package/dist/pdf-view-BV5ZD6OC.js.map +1 -0
  16. package/package.json +96 -0
  17. package/src/components/attachment-chip/attachment-chip.tsx +129 -0
  18. package/src/components/attachment-chip/index.ts +2 -0
  19. package/src/components/attribute-type-icon/attribute-type-icon.stories.tsx +70 -0
  20. package/src/components/attribute-type-icon/attribute-type-icon.tsx +125 -0
  21. package/src/components/attribute-type-icon/index.ts +8 -0
  22. package/src/components/avatar/avatar.stories.tsx +42 -0
  23. package/src/components/avatar/avatar.tsx +48 -0
  24. package/src/components/avatar/index.ts +2 -0
  25. package/src/components/avatar-group/avatar-group.test.tsx +38 -0
  26. package/src/components/avatar-group/avatar-group.tsx +60 -0
  27. package/src/components/avatar-group/index.ts +2 -0
  28. package/src/components/badge/badge.stories.tsx +62 -0
  29. package/src/components/badge/badge.test.tsx +36 -0
  30. package/src/components/badge/badge.tsx +54 -0
  31. package/src/components/badge/index.ts +2 -0
  32. package/src/components/badge-multi-select/badge-multi-select.stories.tsx +177 -0
  33. package/src/components/badge-multi-select/badge-multi-select.test.tsx +155 -0
  34. package/src/components/badge-multi-select/badge-multi-select.tsx +272 -0
  35. package/src/components/badge-multi-select/index.ts +2 -0
  36. package/src/components/breadcrumbs/breadcrumbs.stories.tsx +43 -0
  37. package/src/components/breadcrumbs/breadcrumbs.tsx +86 -0
  38. package/src/components/breadcrumbs/index.ts +2 -0
  39. package/src/components/button/button.stories.tsx +290 -0
  40. package/src/components/button/button.test.tsx +59 -0
  41. package/src/components/button/button.tsx +86 -0
  42. package/src/components/button/index.ts +2 -0
  43. package/src/components/card/card.stories.tsx +351 -0
  44. package/src/components/card/card.test.tsx +141 -0
  45. package/src/components/card/card.tsx +173 -0
  46. package/src/components/card/index.ts +13 -0
  47. package/src/components/checkbox/checkbox-group.tsx +52 -0
  48. package/src/components/checkbox/checkbox.stories.tsx +48 -0
  49. package/src/components/checkbox/checkbox.test.tsx +50 -0
  50. package/src/components/checkbox/checkbox.tsx +71 -0
  51. package/src/components/checkbox/index.ts +4 -0
  52. package/src/components/combobox/combobox.stories.tsx +97 -0
  53. package/src/components/combobox/combobox.test.tsx +123 -0
  54. package/src/components/combobox/combobox.tsx +239 -0
  55. package/src/components/combobox/index.ts +2 -0
  56. package/src/components/command/command-hint.tsx +46 -0
  57. package/src/components/command/command.stories.tsx +88 -0
  58. package/src/components/command/command.test.tsx +102 -0
  59. package/src/components/command/command.tsx +76 -0
  60. package/src/components/command/index.ts +16 -0
  61. package/src/components/currency-input/currency-input.tsx +192 -0
  62. package/src/components/currency-input/index.ts +6 -0
  63. package/src/components/data-table/cells.stories.tsx +120 -0
  64. package/src/components/data-table/cells.tsx +165 -0
  65. package/src/components/data-table/data-table.stories.tsx +371 -0
  66. package/src/components/data-table/data-table.tsx +556 -0
  67. package/src/components/data-table/index.ts +22 -0
  68. package/src/components/data-table/make-actions-column.ts +30 -0
  69. package/src/components/data-table/states/data-table-empty.tsx +40 -0
  70. package/src/components/data-table/states/data-table-error.tsx +30 -0
  71. package/src/components/data-table/states/data-table-skeleton.tsx +42 -0
  72. package/src/components/data-table/use-infinite-scroll-sentinel.ts +51 -0
  73. package/src/components/date-picker/calendar-internals.tsx +138 -0
  74. package/src/components/date-picker/date-picker.stories.tsx +78 -0
  75. package/src/components/date-picker/date-picker.test.tsx +71 -0
  76. package/src/components/date-picker/date-picker.tsx +140 -0
  77. package/src/components/date-picker/index.ts +2 -0
  78. package/src/components/date-time-picker/date-time-picker.stories.tsx +96 -0
  79. package/src/components/date-time-picker/date-time-picker.test.tsx +87 -0
  80. package/src/components/date-time-picker/date-time-picker.tsx +203 -0
  81. package/src/components/date-time-picker/index.ts +2 -0
  82. package/src/components/empty-state/empty-state.tsx +63 -0
  83. package/src/components/empty-state/index.ts +2 -0
  84. package/src/components/error-animation/error-animation.stories.tsx +50 -0
  85. package/src/components/error-animation/error-animation.test.tsx +22 -0
  86. package/src/components/error-animation/error-animation.tsx +26 -0
  87. package/src/components/error-animation/index.ts +2 -0
  88. package/src/components/field/field.tsx +142 -0
  89. package/src/components/field/index.ts +8 -0
  90. package/src/components/file-field/file-field.tsx +285 -0
  91. package/src/components/file-field/index.ts +2 -0
  92. package/src/components/file-viewer/fallback-card.tsx +68 -0
  93. package/src/components/file-viewer/file-viewer.stories.tsx +67 -0
  94. package/src/components/file-viewer/file-viewer.test.tsx +81 -0
  95. package/src/components/file-viewer/file-viewer.tsx +116 -0
  96. package/src/components/file-viewer/image-view.tsx +38 -0
  97. package/src/components/file-viewer/index.ts +7 -0
  98. package/src/components/file-viewer/pdf-view.tsx +122 -0
  99. package/src/components/file-viewer/video-view.tsx +16 -0
  100. package/src/components/image-attachment/image-attachment.stories.tsx +39 -0
  101. package/src/components/image-attachment/image-attachment.test.tsx +54 -0
  102. package/src/components/image-attachment/image-attachment.tsx +77 -0
  103. package/src/components/image-attachment/index.ts +3 -0
  104. package/src/components/image-attachment/is-previewable-image-name.ts +14 -0
  105. package/src/components/image-lightbox/image-lightbox.stories.tsx +38 -0
  106. package/src/components/image-lightbox/image-lightbox.test.tsx +72 -0
  107. package/src/components/image-lightbox/image-lightbox.tsx +92 -0
  108. package/src/components/image-lightbox/index.ts +2 -0
  109. package/src/components/input/index.ts +2 -0
  110. package/src/components/input/input.stories.tsx +71 -0
  111. package/src/components/input/input.test.tsx +36 -0
  112. package/src/components/input/input.tsx +76 -0
  113. package/src/components/kbd/index.ts +2 -0
  114. package/src/components/kbd/kbd.stories.tsx +34 -0
  115. package/src/components/kbd/kbd.tsx +27 -0
  116. package/src/components/menu/index.ts +2 -0
  117. package/src/components/menu/menu.tsx +121 -0
  118. package/src/components/modal/index.ts +10 -0
  119. package/src/components/modal/modal.stories.tsx +153 -0
  120. package/src/components/modal/modal.test.tsx +93 -0
  121. package/src/components/modal/modal.tsx +149 -0
  122. package/src/components/multi-select/index.ts +2 -0
  123. package/src/components/multi-select/multi-select.stories.tsx +211 -0
  124. package/src/components/multi-select/multi-select.test.tsx +151 -0
  125. package/src/components/multi-select/multi-select.tsx +339 -0
  126. package/src/components/nav/index.ts +7 -0
  127. package/src/components/nav/nav.stories.tsx +139 -0
  128. package/src/components/nav/nav.tsx +246 -0
  129. package/src/components/popover/index.ts +8 -0
  130. package/src/components/popover/popover.stories.tsx +92 -0
  131. package/src/components/popover/popover.tsx +135 -0
  132. package/src/components/progress-bar/index.ts +2 -0
  133. package/src/components/progress-bar/progress-bar.stories.tsx +75 -0
  134. package/src/components/progress-bar/progress-bar.test.tsx +29 -0
  135. package/src/components/progress-bar/progress-bar.tsx +57 -0
  136. package/src/components/radio-group/index.ts +2 -0
  137. package/src/components/radio-group/radio-group.stories.tsx +113 -0
  138. package/src/components/radio-group/radio-group.test.tsx +87 -0
  139. package/src/components/radio-group/radio-group.tsx +104 -0
  140. package/src/components/record-filter/attribute-picker.tsx +74 -0
  141. package/src/components/record-filter/chip-value.tsx +171 -0
  142. package/src/components/record-filter/filter-row.tsx +262 -0
  143. package/src/components/record-filter/filter-value-input.tsx +351 -0
  144. package/src/components/record-filter/group-add-controls.tsx +77 -0
  145. package/src/components/record-filter/group-body.tsx +173 -0
  146. package/src/components/record-filter/group-mutations.test.ts +104 -0
  147. package/src/components/record-filter/group-mutations.ts +116 -0
  148. package/src/components/record-filter/index.ts +58 -0
  149. package/src/components/record-filter/nested-group.tsx +138 -0
  150. package/src/components/record-filter/operator-picker.tsx +62 -0
  151. package/src/components/record-filter/operators.test.ts +105 -0
  152. package/src/components/record-filter/operators.ts +97 -0
  153. package/src/components/record-filter/record-filter-builder-panel.tsx +236 -0
  154. package/src/components/record-filter/record-filter-button.tsx +126 -0
  155. package/src/components/record-filter/record-filter-chips.tsx +527 -0
  156. package/src/components/record-filter/record-filter-editor.tsx +91 -0
  157. package/src/components/record-filter/record-filter.stories.tsx +196 -0
  158. package/src/components/record-filter/record-filter.test.tsx +437 -0
  159. package/src/components/record-filter/show-details-store.ts +72 -0
  160. package/src/components/record-filter/types.ts +150 -0
  161. package/src/components/segmented-control/index.ts +6 -0
  162. package/src/components/segmented-control/segmented-control.stories.tsx +67 -0
  163. package/src/components/segmented-control/segmented-control.test.tsx +47 -0
  164. package/src/components/segmented-control/segmented-control.tsx +105 -0
  165. package/src/components/select/index.ts +2 -0
  166. package/src/components/select/select.stories.tsx +76 -0
  167. package/src/components/select/select.test.tsx +56 -0
  168. package/src/components/select/select.tsx +159 -0
  169. package/src/components/separator/index.ts +2 -0
  170. package/src/components/separator/separator.stories.tsx +73 -0
  171. package/src/components/separator/separator.test.tsx +29 -0
  172. package/src/components/separator/separator.tsx +51 -0
  173. package/src/components/skeleton/index.ts +2 -0
  174. package/src/components/skeleton/skeleton.stories.tsx +68 -0
  175. package/src/components/skeleton/skeleton.test.tsx +38 -0
  176. package/src/components/skeleton/skeleton.tsx +43 -0
  177. package/src/components/success-animation/index.ts +2 -0
  178. package/src/components/success-animation/success-animation.stories.tsx +50 -0
  179. package/src/components/success-animation/success-animation.test.tsx +22 -0
  180. package/src/components/success-animation/success-animation.tsx +26 -0
  181. package/src/components/switch/index.ts +2 -0
  182. package/src/components/switch/switch.stories.tsx +60 -0
  183. package/src/components/switch/switch.test.tsx +50 -0
  184. package/src/components/switch/switch.tsx +49 -0
  185. package/src/components/table/index.ts +25 -0
  186. package/src/components/table/table.stories.tsx +306 -0
  187. package/src/components/table/table.test.tsx +115 -0
  188. package/src/components/table/table.tsx +543 -0
  189. package/src/components/tabs/index.ts +8 -0
  190. package/src/components/tabs/tabs.tsx +141 -0
  191. package/src/components/tag-input/index.ts +2 -0
  192. package/src/components/tag-input/tag-input.stories.tsx +73 -0
  193. package/src/components/tag-input/tag-input.test.tsx +80 -0
  194. package/src/components/tag-input/tag-input.tsx +193 -0
  195. package/src/components/textarea/index.ts +2 -0
  196. package/src/components/textarea/textarea.stories.tsx +75 -0
  197. package/src/components/textarea/textarea.test.tsx +41 -0
  198. package/src/components/textarea/textarea.tsx +86 -0
  199. package/src/components/time-field/index.ts +2 -0
  200. package/src/components/time-field/time-field.stories.tsx +76 -0
  201. package/src/components/time-field/time-field.test.tsx +51 -0
  202. package/src/components/time-field/time-field.tsx +98 -0
  203. package/src/components/tooltip/index.ts +2 -0
  204. package/src/components/tooltip/tooltip.stories.tsx +176 -0
  205. package/src/components/tooltip/tooltip.test.tsx +52 -0
  206. package/src/components/tooltip/tooltip.tsx +56 -0
  207. package/src/index.ts +47 -0
  208. package/src/lib/cn.ts +6 -0
  209. package/src/lib/commands/discovery-store.test.tsx +76 -0
  210. package/src/lib/commands/discovery-store.ts +110 -0
  211. package/src/lib/commands/index.ts +9 -0
  212. package/src/lib/commands/match-event.test.ts +99 -0
  213. package/src/lib/commands/match-event.ts +31 -0
  214. package/src/lib/commands/parse-combo.test.ts +65 -0
  215. package/src/lib/commands/parse-combo.ts +48 -0
  216. package/src/lib/commands/platform.ts +27 -0
  217. package/src/lib/commands/protected-combos.test.ts +18 -0
  218. package/src/lib/commands/protected-combos.ts +38 -0
  219. package/src/lib/commands/registry.test.ts +97 -0
  220. package/src/lib/commands/registry.ts +59 -0
  221. package/src/lib/commands/use-command.test.tsx +91 -0
  222. package/src/lib/commands/use-command.ts +32 -0
  223. package/src/lib/commands/use-escape-key.test.tsx +97 -0
  224. package/src/lib/commands/use-escape-key.ts +62 -0
  225. package/src/styles.css +321 -0
  226. package/src/test/setup.ts +9 -0
@@ -0,0 +1,290 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite'
2
+
3
+ import { Button } from './button'
4
+
5
+ const meta: Meta<typeof Button> = {
6
+ title: 'Components/Button',
7
+ component: Button,
8
+ tags: ['autodocs'],
9
+ args: {
10
+ children: 'Start building →',
11
+ variant: 'primary',
12
+ size: 'md',
13
+ shape: 'rounded',
14
+ },
15
+ argTypes: {
16
+ variant: {
17
+ control: { type: 'inline-radio' },
18
+ options: ['primary', 'accent', 'secondary', 'ghost', 'link'],
19
+ },
20
+ size: {
21
+ control: { type: 'inline-radio' },
22
+ options: ['sm', 'md', 'lg', 'icon-sm', 'icon-md', 'icon-lg'],
23
+ },
24
+ shape: {
25
+ control: { type: 'inline-radio' },
26
+ options: ['rounded', 'pill'],
27
+ },
28
+ isDisabled: { control: { type: 'boolean' } },
29
+ },
30
+ }
31
+
32
+ export default meta
33
+ type Story = StoryObj<typeof Button>
34
+
35
+ export const Playground: Story = {}
36
+
37
+ const Arrow = () => (
38
+ <svg viewBox="0 0 12 12" fill="none" aria-hidden>
39
+ <path d="M2 6h8m-3-3 3 3-3 3" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round" />
40
+ </svg>
41
+ )
42
+
43
+ export const Primary: Story = {
44
+ render: () => (
45
+ <div className="flex flex-wrap items-center gap-3">
46
+ <Button>Start building →</Button>
47
+ <Button size="sm">Save</Button>
48
+ <Button size="lg">Compose system</Button>
49
+ <Button isDisabled>Disabled</Button>
50
+ </div>
51
+ ),
52
+ }
53
+
54
+ export const Accent: Story = {
55
+ render: () => (
56
+ <div className="flex flex-wrap items-center gap-3">
57
+ <Button variant="accent">
58
+ Compose <Arrow />
59
+ </Button>
60
+ <Button variant="accent" size="sm">
61
+ Run
62
+ </Button>
63
+ <Button variant="accent" size="lg">
64
+ Publish changes
65
+ </Button>
66
+ </div>
67
+ ),
68
+ }
69
+
70
+ export const Secondary: Story = {
71
+ render: () => (
72
+ <div className="flex flex-wrap items-center gap-3">
73
+ <Button variant="secondary">Read manifesto</Button>
74
+ <Button variant="secondary" size="sm">
75
+ Cancel
76
+ </Button>
77
+ <Button variant="secondary" size="lg">
78
+ Book a demo
79
+ </Button>
80
+ </div>
81
+ ),
82
+ }
83
+
84
+ export const Ghost: Story = {
85
+ render: () => (
86
+ <div className="flex flex-wrap items-center gap-3">
87
+ <Button variant="ghost">Filter</Button>
88
+ <Button variant="ghost" size="sm">
89
+ Group
90
+ </Button>
91
+ <Button variant="ghost">Export</Button>
92
+ <Button variant="ghost">+ New shipment</Button>
93
+ </div>
94
+ ),
95
+ }
96
+
97
+ export const Link: Story = {
98
+ render: () => (
99
+ <div className="flex flex-wrap items-center gap-6">
100
+ <Button variant="link">Read the docs</Button>
101
+ <Button variant="link">View architecture</Button>
102
+ <Button variant="link">Schedule an architect</Button>
103
+ </div>
104
+ ),
105
+ }
106
+
107
+ const columns = 'grid grid-cols-[160px_1fr] items-center gap-y-5 gap-x-6'
108
+ const label = 'font-mono text-[11px] uppercase tracking-[0.08em] text-ink-4'
109
+
110
+ export const AllVariants: Story = {
111
+ parameters: { controls: { hideNoControlsWarning: true } },
112
+ render: () => (
113
+ <div className={columns}>
114
+ <div className={label}>Primary</div>
115
+ <div className="flex flex-wrap items-center gap-3">
116
+ <Button size="sm">Save</Button>
117
+ <Button>Start building →</Button>
118
+ <Button size="lg">Compose system</Button>
119
+ <Button isDisabled>Disabled</Button>
120
+ </div>
121
+
122
+ <div className={label}>Accent</div>
123
+ <div className="flex flex-wrap items-center gap-3">
124
+ <Button variant="accent" size="sm">
125
+ Run
126
+ </Button>
127
+ <Button variant="accent">
128
+ Compose <Arrow />
129
+ </Button>
130
+ <Button variant="accent" size="lg">
131
+ Publish changes
132
+ </Button>
133
+ </div>
134
+
135
+ <div className={label}>Secondary</div>
136
+ <div className="flex flex-wrap items-center gap-3">
137
+ <Button variant="secondary" size="sm">
138
+ Cancel
139
+ </Button>
140
+ <Button variant="secondary">Read manifesto</Button>
141
+ <Button variant="secondary" size="lg">
142
+ Book a demo
143
+ </Button>
144
+ </div>
145
+
146
+ <div className={label}>Ghost</div>
147
+ <div className="flex flex-wrap items-center gap-3">
148
+ <Button variant="ghost" size="sm">
149
+ Group
150
+ </Button>
151
+ <Button variant="ghost">Filter</Button>
152
+ <Button variant="ghost">+ New shipment</Button>
153
+ </div>
154
+
155
+ <div className={label}>Link</div>
156
+ <div className="flex flex-wrap items-center gap-6">
157
+ <Button variant="link">Read the docs</Button>
158
+ <Button variant="link">View architecture</Button>
159
+ </div>
160
+ </div>
161
+ ),
162
+ }
163
+
164
+ /* =========================================================================
165
+ Shape — default `rounded` (soft 10px corners, the new standard) vs `pill`
166
+ (fully-rounded, used sparingly for prominent CTAs / filters).
167
+ ========================================================================= */
168
+
169
+ export const Rounded: Story = {
170
+ render: () => (
171
+ <div className="flex flex-wrap items-center gap-3">
172
+ <Button variant="ghost">Filter</Button>
173
+ <Button variant="ghost">Group</Button>
174
+ <Button>+ New shipment</Button>
175
+ </div>
176
+ ),
177
+ }
178
+
179
+ export const Pill: Story = {
180
+ render: () => (
181
+ <div className="flex flex-wrap items-center gap-3">
182
+ <Button shape="pill" variant="ghost">
183
+ Filter
184
+ </Button>
185
+ <Button shape="pill" variant="ghost">
186
+ Group
187
+ </Button>
188
+ <Button shape="pill">+ New shipment</Button>
189
+ </div>
190
+ ),
191
+ }
192
+
193
+ export const Shapes: Story = {
194
+ parameters: { controls: { hideNoControlsWarning: true } },
195
+ render: () => (
196
+ <div className={columns}>
197
+ <div className={label}>Rounded</div>
198
+ <div className="flex flex-wrap items-center gap-3">
199
+ <Button variant="ghost" size="sm">
200
+ Filter
201
+ </Button>
202
+ <Button variant="ghost">Group</Button>
203
+ <Button>+ New shipment</Button>
204
+ </div>
205
+
206
+ <div className={label}>Pill</div>
207
+ <div className="flex flex-wrap items-center gap-3">
208
+ <Button shape="pill" variant="ghost" size="sm">
209
+ Filter
210
+ </Button>
211
+ <Button shape="pill" variant="ghost">
212
+ Group
213
+ </Button>
214
+ <Button shape="pill">+ New shipment</Button>
215
+ </div>
216
+ </div>
217
+ ),
218
+ }
219
+
220
+ /* =========================================================================
221
+ Icon-only — square dimensions via `size="icon-sm | icon-md | icon-lg"`.
222
+ `aria-label` is required (not enforced at the type level — add it manually).
223
+ ========================================================================= */
224
+
225
+ const Plus = () => (
226
+ <svg viewBox="0 0 16 16" fill="none" aria-hidden>
227
+ <path d="M8 3v10M3 8h10" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" />
228
+ </svg>
229
+ )
230
+
231
+ const ArrowRight = () => (
232
+ <svg viewBox="0 0 16 16" fill="none" aria-hidden>
233
+ <path
234
+ d="M3 8h10m-4-4 4 4-4 4"
235
+ stroke="currentColor"
236
+ strokeWidth="1.4"
237
+ strokeLinecap="round"
238
+ strokeLinejoin="round"
239
+ />
240
+ </svg>
241
+ )
242
+
243
+ export const IconOnly: Story = {
244
+ parameters: { controls: { hideNoControlsWarning: true } },
245
+ render: () => (
246
+ <div className={columns}>
247
+ <div className={label}>Sizes</div>
248
+ <div className="flex items-center gap-3">
249
+ <Button size="icon-sm" variant="ghost" aria-label="Add">
250
+ <Plus />
251
+ </Button>
252
+ <Button size="icon-md" variant="ghost" aria-label="Add">
253
+ <Plus />
254
+ </Button>
255
+ <Button size="icon-lg" variant="ghost" aria-label="Add">
256
+ <Plus />
257
+ </Button>
258
+ </div>
259
+
260
+ <div className={label}>Variants</div>
261
+ <div className="flex items-center gap-3">
262
+ <Button size="icon-md" variant="primary" aria-label="Add">
263
+ <Plus />
264
+ </Button>
265
+ <Button size="icon-md" variant="accent" aria-label="Next">
266
+ <ArrowRight />
267
+ </Button>
268
+ <Button size="icon-md" variant="secondary" aria-label="Add">
269
+ <Plus />
270
+ </Button>
271
+ <Button size="icon-md" variant="ghost" aria-label="Add">
272
+ <Plus />
273
+ </Button>
274
+ </div>
275
+
276
+ <div className={label}>Pill shape</div>
277
+ <div className="flex items-center gap-3">
278
+ <Button size="icon-sm" variant="ghost" shape="pill" aria-label="Add">
279
+ <Plus />
280
+ </Button>
281
+ <Button size="icon-md" variant="primary" shape="pill" aria-label="Add">
282
+ <Plus />
283
+ </Button>
284
+ <Button size="icon-lg" variant="accent" shape="pill" aria-label="Next">
285
+ <ArrowRight />
286
+ </Button>
287
+ </div>
288
+ </div>
289
+ ),
290
+ }
@@ -0,0 +1,59 @@
1
+ import { render, screen } from '@testing-library/react'
2
+ import userEvent from '@testing-library/user-event'
3
+ import { describe, expect, it, vi } from 'vitest'
4
+
5
+ import { Button } from './button'
6
+
7
+ describe('Button', () => {
8
+ it('renders its label', () => {
9
+ render(<Button>Compose</Button>)
10
+ expect(screen.getByRole('button', { name: 'Compose' })).toBeInTheDocument()
11
+ })
12
+
13
+ it('fires onPress when clicked', async () => {
14
+ const onPress = vi.fn()
15
+ const user = userEvent.setup()
16
+
17
+ render(<Button onPress={onPress}>Save</Button>)
18
+ await user.click(screen.getByRole('button', { name: 'Save' }))
19
+
20
+ expect(onPress).toHaveBeenCalledOnce()
21
+ })
22
+
23
+ it('is keyboard focusable and activates on Enter', async () => {
24
+ const onPress = vi.fn()
25
+ const user = userEvent.setup()
26
+
27
+ render(<Button onPress={onPress}>Run</Button>)
28
+ await user.tab()
29
+ expect(screen.getByRole('button', { name: 'Run' })).toHaveFocus()
30
+
31
+ await user.keyboard('{Enter}')
32
+ expect(onPress).toHaveBeenCalledOnce()
33
+ })
34
+
35
+ it('does not fire onPress when disabled', async () => {
36
+ const onPress = vi.fn()
37
+ const user = userEvent.setup()
38
+
39
+ render(
40
+ <Button onPress={onPress} isDisabled>
41
+ Disabled
42
+ </Button>,
43
+ )
44
+ await user.click(screen.getByRole('button', { name: 'Disabled' }))
45
+
46
+ expect(onPress).not.toHaveBeenCalled()
47
+ })
48
+
49
+ it('applies the requested variant and size classes', () => {
50
+ render(
51
+ <Button variant="accent" size="lg">
52
+ Publish
53
+ </Button>,
54
+ )
55
+ const button = screen.getByRole('button', { name: 'Publish' })
56
+ expect(button.className).toContain('bg-accent')
57
+ expect(button.className).toContain('px-[22px]')
58
+ })
59
+ })
@@ -0,0 +1,86 @@
1
+ import { Button as AriaButton, type ButtonProps as AriaButtonProps } from 'react-aria-components'
2
+ import { tv, type VariantProps } from 'tailwind-variants'
3
+
4
+ import { cn } from '../../lib/cn'
5
+
6
+ export const buttonStyles = tv({
7
+ base: [
8
+ 'inline-flex items-center justify-center gap-2 whitespace-nowrap cursor-pointer',
9
+ 'font-mono tracking-[-0.005em] border border-transparent',
10
+ 'transition-[background-color,color,border-color,box-shadow,transform] duration-150',
11
+ 'outline-none pressed:translate-y-px',
12
+ 'focus-visible:shadow-[var(--shadow-focus)]',
13
+ 'disabled:opacity-45 disabled:cursor-not-allowed disabled:pointer-events-none',
14
+ ],
15
+ variants: {
16
+ variant: {
17
+ primary: ['bg-ink text-surface border-ink', 'hover:bg-accent-ink hover:border-accent-ink'],
18
+ // Hover uses accent-2 — the token DESIGN.md reserves for "hover on
19
+ // accent". accent-ink is a text-on-tinted-surface color and flips
20
+ // light in dark mode, so it can't double as a hover fill here.
21
+ accent: [
22
+ 'bg-accent text-[#f0efe8] border-accent',
23
+ 'hover:bg-accent-2 hover:border-accent-2',
24
+ ],
25
+ secondary: ['bg-transparent text-ink border-ink', 'hover:bg-ink hover:text-surface'],
26
+ ghost: [
27
+ 'bg-bg text-ink-2 border-rule-strong',
28
+ 'hover:bg-surface-2 hover:text-ink hover:border-ink-4',
29
+ ],
30
+ // Transparent at rest, paper-fill on hover. The toolbar / sidebar
31
+ // register — pairs with `size="icon-md"` for top-bar icon buttons.
32
+ subtle: ['bg-transparent text-ink-3 border-transparent', 'hover:bg-surface-2 hover:text-ink'],
33
+ link: [
34
+ 'bg-transparent text-ink-2 border-transparent',
35
+ 'px-0.5 border-b border-b-ink-4',
36
+ 'hover:text-ink hover:border-b-ink',
37
+ ],
38
+ },
39
+ size: {
40
+ sm: 'text-xs px-3 py-1.5 [&_svg]:size-3',
41
+ md: 'text-[13px] px-4 py-[9px] [&_svg]:size-3',
42
+ lg: 'text-sm px-[22px] py-3 [&_svg]:size-3.5',
43
+ // Square dimensions for icon-only buttons. Requires an `aria-label`.
44
+ 'icon-sm': 'size-7 p-0 [&_svg]:size-3.5',
45
+ 'icon-md': 'size-[34px] p-0 [&_svg]:size-4',
46
+ 'icon-lg': 'size-[42px] p-0 [&_svg]:size-[18px]',
47
+ },
48
+ // Corner shape. Default is rounded (soft 10px radius — the standard
49
+ // Proteos button), `pill` is the fully-rounded legacy treatment.
50
+ // The `link` variant overrides both to a flat underline.
51
+ shape: {
52
+ rounded: 'rounded-md',
53
+ pill: 'rounded-pill',
54
+ },
55
+ },
56
+ compoundVariants: [
57
+ { variant: 'link', size: 'sm', class: 'px-0.5 py-0' },
58
+ { variant: 'link', size: 'md', class: 'px-0.5 py-0' },
59
+ { variant: 'link', size: 'lg', class: 'px-0.5 py-0' },
60
+ // Link never takes a corner radius — it's an underline, not a box.
61
+ { variant: 'link', shape: 'rounded', class: 'rounded-none' },
62
+ { variant: 'link', shape: 'pill', class: 'rounded-none' },
63
+ ],
64
+ defaultVariants: {
65
+ variant: 'primary',
66
+ size: 'md',
67
+ shape: 'rounded',
68
+ },
69
+ })
70
+
71
+ export type ButtonVariants = VariantProps<typeof buttonStyles>
72
+ export type ButtonProps = AriaButtonProps & ButtonVariants
73
+
74
+ export function Button({ variant, size, shape, className, ...props }: ButtonProps) {
75
+ return (
76
+ <AriaButton
77
+ {...props}
78
+ className={(renderProps) =>
79
+ cn(
80
+ buttonStyles({ variant, size, shape }),
81
+ typeof className === 'function' ? className(renderProps) : className,
82
+ )
83
+ }
84
+ />
85
+ )
86
+ }
@@ -0,0 +1,2 @@
1
+ export type { ButtonProps, ButtonVariants } from './button'
2
+ export { Button, buttonStyles } from './button'