@proteos/ui 0.55.0 → 0.57.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.
- package/dist/{chunk-IKAX76MB.js → chunk-NSE4VW3Z.js} +45 -21
- package/dist/chunk-NSE4VW3Z.js.map +1 -0
- package/dist/{chunk-MWAXK66H.cjs → chunk-NWGG5WFL.cjs} +45 -21
- package/dist/chunk-NWGG5WFL.cjs.map +1 -0
- package/dist/index.cjs +377 -361
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +46 -14
- package/dist/index.d.ts +46 -14
- package/dist/index.js +124 -108
- package/dist/index.js.map +1 -1
- package/dist/{pdf-view-MH75RVQP.js → pdf-view-RQ2OZYHJ.js} +3 -3
- package/dist/{pdf-view-MH75RVQP.js.map → pdf-view-RQ2OZYHJ.js.map} +1 -1
- package/dist/{pdf-view-K4MMVHVY.cjs → pdf-view-RRMAUK3L.cjs} +7 -7
- package/dist/{pdf-view-K4MMVHVY.cjs.map → pdf-view-RRMAUK3L.cjs.map} +1 -1
- package/package.json +2 -2
- package/src/components/avatar/avatar.stories.tsx +13 -0
- package/src/components/avatar/avatar.tsx +16 -9
- package/src/components/badge-multi-select/badge-multi-select.tsx +2 -2
- package/src/components/button/button.stories.tsx +202 -97
- package/src/components/button/button.test.tsx +40 -7
- package/src/components/button/button.tsx +71 -18
- package/src/components/combobox/combobox.tsx +2 -2
- package/src/components/currency-input/currency-input.tsx +2 -2
- package/src/components/date-picker/calendar-internals.tsx +1 -1
- package/src/components/date-picker/date-picker.tsx +3 -3
- package/src/components/date-time-picker/date-time-picker.tsx +3 -3
- package/src/components/icon-picker/icon-picker.tsx +2 -2
- package/src/components/image-lightbox/image-lightbox.tsx +2 -2
- package/src/components/menu/menu.tsx +2 -2
- package/src/components/modal/modal.tsx +4 -4
- package/src/components/multi-select/multi-select.tsx +2 -2
- package/src/components/popover/popover.tsx +2 -2
- package/src/components/record-filter/chip-value.tsx +6 -5
- package/src/components/record-filter/filter-value-input.tsx +11 -9
- package/src/components/record-filter/nested-group.tsx +2 -2
- package/src/components/record-filter/operators.test.ts +67 -12
- package/src/components/record-filter/operators.ts +43 -2
- package/src/components/record-filter/record-filter-builder-panel.tsx +11 -23
- package/src/components/record-filter/record-filter-chips.tsx +3 -10
- package/src/components/record-filter/types.ts +6 -1
- package/src/components/segmented-control/segmented-control.stories.tsx +134 -8
- package/src/components/segmented-control/segmented-control.test.tsx +81 -8
- package/src/components/segmented-control/segmented-control.tsx +38 -19
- package/src/components/select/select.tsx +2 -2
- package/src/components/time-field/time-field.tsx +1 -1
- package/src/components/toast/toast.tsx +2 -2
- package/src/components/tooltip/tooltip.tsx +2 -2
- package/src/styles.css +58 -0
- package/dist/chunk-IKAX76MB.js.map +0 -1
- package/dist/chunk-MWAXK66H.cjs.map +0 -1
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
2
|
|
|
3
|
-
import { Button } from './button'
|
|
3
|
+
import { Button, buttonStyles, type ButtonVariants } from './button'
|
|
4
4
|
|
|
5
5
|
const meta: Meta<typeof Button> = {
|
|
6
6
|
title: 'Components/Button',
|
|
7
7
|
component: Button,
|
|
8
8
|
tags: ['autodocs'],
|
|
9
9
|
args: {
|
|
10
|
-
children: '
|
|
10
|
+
children: 'Save changes',
|
|
11
11
|
variant: 'primary',
|
|
12
12
|
size: 'md',
|
|
13
13
|
shape: 'rounded',
|
|
@@ -15,7 +15,7 @@ const meta: Meta<typeof Button> = {
|
|
|
15
15
|
argTypes: {
|
|
16
16
|
variant: {
|
|
17
17
|
control: { type: 'inline-radio' },
|
|
18
|
-
options: ['
|
|
18
|
+
options: ['cta', 'primary', 'secondary', 'ghost', 'subtle', 'link', 'danger'],
|
|
19
19
|
},
|
|
20
20
|
size: {
|
|
21
21
|
control: { type: 'inline-radio' },
|
|
@@ -34,35 +34,55 @@ type Story = StoryObj<typeof Button>
|
|
|
34
34
|
|
|
35
35
|
export const Playground: Story = {}
|
|
36
36
|
|
|
37
|
-
const
|
|
38
|
-
<svg viewBox="0 0
|
|
39
|
-
<path d="
|
|
37
|
+
const Plus = () => (
|
|
38
|
+
<svg viewBox="0 0 16 16" fill="none" aria-hidden>
|
|
39
|
+
<path d="M8 3v10M3 8h10" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" />
|
|
40
40
|
</svg>
|
|
41
41
|
)
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
const ArrowRight = () => (
|
|
44
|
+
<svg viewBox="0 0 16 16" fill="none" aria-hidden>
|
|
45
|
+
<path
|
|
46
|
+
d="M3 8h10m-4-4 4 4-4 4"
|
|
47
|
+
stroke="currentColor"
|
|
48
|
+
strokeWidth="1.4"
|
|
49
|
+
strokeLinecap="round"
|
|
50
|
+
strokeLinejoin="round"
|
|
51
|
+
/>
|
|
52
|
+
</svg>
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
/* =========================================================================
|
|
56
|
+
The ladder. One `cta` per view; `primary` is the everyday button; the rest
|
|
57
|
+
step down. `accent` (not shown) is a frozen alias of `cta`.
|
|
58
|
+
========================================================================= */
|
|
59
|
+
|
|
60
|
+
export const Cta: Story = {
|
|
44
61
|
render: () => (
|
|
45
62
|
<div className="flex flex-wrap items-center gap-3">
|
|
46
|
-
<Button
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
<Button
|
|
63
|
+
<Button variant="cta" size="sm">
|
|
64
|
+
New record
|
|
65
|
+
</Button>
|
|
66
|
+
<Button variant="cta">
|
|
67
|
+
Publish changes <ArrowRight />
|
|
68
|
+
</Button>
|
|
69
|
+
<Button variant="cta" size="lg">
|
|
70
|
+
Sign in
|
|
71
|
+
</Button>
|
|
72
|
+
<Button variant="cta" isDisabled>
|
|
73
|
+
Disabled
|
|
74
|
+
</Button>
|
|
50
75
|
</div>
|
|
51
76
|
),
|
|
52
77
|
}
|
|
53
78
|
|
|
54
|
-
export const
|
|
79
|
+
export const Primary: Story = {
|
|
55
80
|
render: () => (
|
|
56
81
|
<div className="flex flex-wrap items-center gap-3">
|
|
57
|
-
<Button
|
|
58
|
-
|
|
59
|
-
</Button>
|
|
60
|
-
<Button
|
|
61
|
-
Run
|
|
62
|
-
</Button>
|
|
63
|
-
<Button variant="accent" size="lg">
|
|
64
|
-
Publish changes
|
|
65
|
-
</Button>
|
|
82
|
+
<Button size="sm">Save</Button>
|
|
83
|
+
<Button>Save changes</Button>
|
|
84
|
+
<Button size="lg">Duplicate</Button>
|
|
85
|
+
<Button isDisabled>Disabled</Button>
|
|
66
86
|
</div>
|
|
67
87
|
),
|
|
68
88
|
}
|
|
@@ -70,12 +90,12 @@ export const Accent: Story = {
|
|
|
70
90
|
export const Secondary: Story = {
|
|
71
91
|
render: () => (
|
|
72
92
|
<div className="flex flex-wrap items-center gap-3">
|
|
73
|
-
<Button variant="secondary">Read manifesto</Button>
|
|
74
93
|
<Button variant="secondary" size="sm">
|
|
75
94
|
Cancel
|
|
76
95
|
</Button>
|
|
96
|
+
<Button variant="secondary">Cancel</Button>
|
|
77
97
|
<Button variant="secondary" size="lg">
|
|
78
|
-
|
|
98
|
+
Back
|
|
79
99
|
</Button>
|
|
80
100
|
</div>
|
|
81
101
|
),
|
|
@@ -89,7 +109,20 @@ export const Ghost: Story = {
|
|
|
89
109
|
Group
|
|
90
110
|
</Button>
|
|
91
111
|
<Button variant="ghost">Export</Button>
|
|
92
|
-
|
|
112
|
+
</div>
|
|
113
|
+
),
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export const Subtle: Story = {
|
|
117
|
+
render: () => (
|
|
118
|
+
<div className="flex flex-wrap items-center gap-3">
|
|
119
|
+
<Button variant="subtle">More</Button>
|
|
120
|
+
<Button variant="subtle" size="sm">
|
|
121
|
+
Edit
|
|
122
|
+
</Button>
|
|
123
|
+
<Button variant="subtle" size="icon-md" aria-label="Add">
|
|
124
|
+
<Plus />
|
|
125
|
+
</Button>
|
|
93
126
|
</div>
|
|
94
127
|
),
|
|
95
128
|
}
|
|
@@ -99,7 +132,21 @@ export const Link: Story = {
|
|
|
99
132
|
<div className="flex flex-wrap items-center gap-6">
|
|
100
133
|
<Button variant="link">Read the docs</Button>
|
|
101
134
|
<Button variant="link">View architecture</Button>
|
|
102
|
-
|
|
135
|
+
</div>
|
|
136
|
+
),
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export const Danger: Story = {
|
|
140
|
+
render: () => (
|
|
141
|
+
<div className="flex flex-wrap items-center gap-3">
|
|
142
|
+
<Button variant="secondary">Cancel</Button>
|
|
143
|
+
<Button variant="danger">Delete record</Button>
|
|
144
|
+
<Button variant="danger" size="sm">
|
|
145
|
+
Remove
|
|
146
|
+
</Button>
|
|
147
|
+
<Button variant="danger" isDisabled>
|
|
148
|
+
Disabled
|
|
149
|
+
</Button>
|
|
103
150
|
</div>
|
|
104
151
|
),
|
|
105
152
|
}
|
|
@@ -111,107 +158,176 @@ export const AllVariants: Story = {
|
|
|
111
158
|
parameters: { controls: { hideNoControlsWarning: true } },
|
|
112
159
|
render: () => (
|
|
113
160
|
<div className={columns}>
|
|
114
|
-
<div className={label}>
|
|
161
|
+
<div className={label}>cta</div>
|
|
115
162
|
<div className="flex flex-wrap items-center gap-3">
|
|
116
|
-
<Button size="sm">
|
|
117
|
-
|
|
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
|
|
163
|
+
<Button variant="cta" size="sm">
|
|
164
|
+
New record
|
|
126
165
|
</Button>
|
|
127
|
-
<Button variant="
|
|
128
|
-
|
|
166
|
+
<Button variant="cta">
|
|
167
|
+
Publish changes <ArrowRight />
|
|
129
168
|
</Button>
|
|
130
|
-
<Button variant="
|
|
131
|
-
|
|
169
|
+
<Button variant="cta" size="lg">
|
|
170
|
+
Sign in
|
|
132
171
|
</Button>
|
|
133
172
|
</div>
|
|
134
173
|
|
|
135
|
-
<div className={label}>
|
|
174
|
+
<div className={label}>primary</div>
|
|
175
|
+
<div className="flex flex-wrap items-center gap-3">
|
|
176
|
+
<Button size="sm">Save</Button>
|
|
177
|
+
<Button>Save changes</Button>
|
|
178
|
+
<Button size="lg">Duplicate</Button>
|
|
179
|
+
<Button isDisabled>Disabled</Button>
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
<div className={label}>secondary</div>
|
|
136
183
|
<div className="flex flex-wrap items-center gap-3">
|
|
137
184
|
<Button variant="secondary" size="sm">
|
|
138
185
|
Cancel
|
|
139
186
|
</Button>
|
|
140
|
-
<Button variant="secondary">
|
|
187
|
+
<Button variant="secondary">Cancel</Button>
|
|
141
188
|
<Button variant="secondary" size="lg">
|
|
142
|
-
|
|
189
|
+
Back
|
|
143
190
|
</Button>
|
|
144
191
|
</div>
|
|
145
192
|
|
|
146
|
-
<div className={label}>
|
|
193
|
+
<div className={label}>ghost</div>
|
|
147
194
|
<div className="flex flex-wrap items-center gap-3">
|
|
148
195
|
<Button variant="ghost" size="sm">
|
|
149
196
|
Group
|
|
150
197
|
</Button>
|
|
151
198
|
<Button variant="ghost">Filter</Button>
|
|
152
|
-
<Button variant="ghost"
|
|
199
|
+
<Button variant="ghost">Export</Button>
|
|
153
200
|
</div>
|
|
154
201
|
|
|
155
|
-
<div className={label}>
|
|
202
|
+
<div className={label}>subtle</div>
|
|
203
|
+
<div className="flex flex-wrap items-center gap-3">
|
|
204
|
+
<Button variant="subtle" size="sm">
|
|
205
|
+
Edit
|
|
206
|
+
</Button>
|
|
207
|
+
<Button variant="subtle">More</Button>
|
|
208
|
+
</div>
|
|
209
|
+
|
|
210
|
+
<div className={label}>link</div>
|
|
156
211
|
<div className="flex flex-wrap items-center gap-6">
|
|
157
212
|
<Button variant="link">Read the docs</Button>
|
|
158
213
|
<Button variant="link">View architecture</Button>
|
|
159
214
|
</div>
|
|
215
|
+
|
|
216
|
+
<div className={label}>danger</div>
|
|
217
|
+
<div className="flex flex-wrap items-center gap-3">
|
|
218
|
+
<Button variant="danger" size="sm">
|
|
219
|
+
Remove
|
|
220
|
+
</Button>
|
|
221
|
+
<Button variant="danger">Delete record</Button>
|
|
222
|
+
</div>
|
|
160
223
|
</div>
|
|
161
224
|
),
|
|
162
225
|
}
|
|
163
226
|
|
|
164
227
|
/* =========================================================================
|
|
165
|
-
|
|
166
|
-
|
|
228
|
+
ControlsLadder — every variant × every interaction state, pinned. RAC
|
|
229
|
+
overwrites user-supplied data-* state on the live <Button>, so the pinned
|
|
230
|
+
columns are static spans wearing `buttonStyles(...)` — which is exactly why
|
|
231
|
+
the style function is exported. Rendered on the page ground AND on a white
|
|
232
|
+
surface so raised-vs-muted stays reviewable in every theme.
|
|
167
233
|
========================================================================= */
|
|
168
234
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
235
|
+
const LADDER: ReadonlyArray<NonNullable<ButtonVariants['variant']>> = [
|
|
236
|
+
'cta',
|
|
237
|
+
'primary',
|
|
238
|
+
'secondary',
|
|
239
|
+
'ghost',
|
|
240
|
+
'subtle',
|
|
241
|
+
'link',
|
|
242
|
+
'danger',
|
|
243
|
+
]
|
|
244
|
+
|
|
245
|
+
const STATES = [
|
|
246
|
+
{ key: 'rest', title: 'rest', attrs: {} },
|
|
247
|
+
{ key: 'hovered', title: 'hover', attrs: { 'data-hovered': '' } },
|
|
248
|
+
{ key: 'pressed', title: 'press', attrs: { 'data-pressed': '' } },
|
|
249
|
+
{ key: 'focus', title: 'focus', attrs: {}, className: 'shadow-[var(--shadow-focus)]' },
|
|
250
|
+
{ key: 'disabled', title: 'disabled', attrs: {}, className: 'opacity-45' },
|
|
251
|
+
] as const
|
|
252
|
+
|
|
253
|
+
function Ladder({ ground }: { ground: 'bg-bg' | 'bg-surface' }) {
|
|
254
|
+
return (
|
|
255
|
+
<div className={`${ground} rounded-lg border border-rule p-5`}>
|
|
256
|
+
<div className="grid grid-cols-[96px_repeat(5,minmax(0,1fr))] items-center gap-x-4 gap-y-3">
|
|
257
|
+
<div className={label}>{ground === 'bg-bg' ? 'on bg' : 'on surface'}</div>
|
|
258
|
+
{STATES.map((state) => (
|
|
259
|
+
<div key={state.key} className={`${label} text-center`}>
|
|
260
|
+
{state.title}
|
|
261
|
+
</div>
|
|
262
|
+
))}
|
|
263
|
+
{LADDER.map((variant) => (
|
|
264
|
+
<div key={variant} className="contents">
|
|
265
|
+
<div className="font-mono text-[11px] text-ink-3">{variant}</div>
|
|
266
|
+
<div className="flex justify-center">
|
|
267
|
+
<Button variant={variant} size="sm">
|
|
268
|
+
Label
|
|
269
|
+
</Button>
|
|
270
|
+
</div>
|
|
271
|
+
{STATES.slice(1).map((state) => (
|
|
272
|
+
<div key={state.key} className="flex justify-center">
|
|
273
|
+
<span
|
|
274
|
+
{...state.attrs}
|
|
275
|
+
className={buttonStyles({
|
|
276
|
+
variant,
|
|
277
|
+
size: 'sm',
|
|
278
|
+
className: 'className' in state ? state.className : undefined,
|
|
279
|
+
})}
|
|
280
|
+
>
|
|
281
|
+
Label
|
|
282
|
+
</span>
|
|
283
|
+
</div>
|
|
284
|
+
))}
|
|
285
|
+
</div>
|
|
286
|
+
))}
|
|
287
|
+
</div>
|
|
175
288
|
</div>
|
|
176
|
-
)
|
|
289
|
+
)
|
|
177
290
|
}
|
|
178
291
|
|
|
179
|
-
export const
|
|
292
|
+
export const ControlsLadder: Story = {
|
|
293
|
+
parameters: { controls: { hideNoControlsWarning: true }, layout: 'padded' },
|
|
180
294
|
render: () => (
|
|
181
|
-
<div className="
|
|
182
|
-
<
|
|
183
|
-
|
|
184
|
-
</Button>
|
|
185
|
-
<Button shape="pill" variant="ghost">
|
|
186
|
-
Group
|
|
187
|
-
</Button>
|
|
188
|
-
<Button shape="pill">+ New shipment</Button>
|
|
295
|
+
<div className="grid gap-4">
|
|
296
|
+
<Ladder ground="bg-bg" />
|
|
297
|
+
<Ladder ground="bg-surface" />
|
|
189
298
|
</div>
|
|
190
299
|
),
|
|
191
300
|
}
|
|
192
301
|
|
|
302
|
+
/* =========================================================================
|
|
303
|
+
Shape — default `rounded` (soft 10px corners) vs `pill` (fully-rounded,
|
|
304
|
+
used sparingly for prominent CTAs / filters).
|
|
305
|
+
========================================================================= */
|
|
306
|
+
|
|
193
307
|
export const Shapes: Story = {
|
|
194
308
|
parameters: { controls: { hideNoControlsWarning: true } },
|
|
195
309
|
render: () => (
|
|
196
310
|
<div className={columns}>
|
|
197
311
|
<div className={label}>Rounded</div>
|
|
198
312
|
<div className="flex flex-wrap items-center gap-3">
|
|
199
|
-
<Button variant="
|
|
313
|
+
<Button variant="secondary" size="sm">
|
|
200
314
|
Filter
|
|
201
315
|
</Button>
|
|
202
|
-
<Button
|
|
203
|
-
<Button
|
|
316
|
+
<Button>Group</Button>
|
|
317
|
+
<Button variant="cta">
|
|
318
|
+
<Plus /> New shipment
|
|
319
|
+
</Button>
|
|
204
320
|
</div>
|
|
205
321
|
|
|
206
322
|
<div className={label}>Pill</div>
|
|
207
323
|
<div className="flex flex-wrap items-center gap-3">
|
|
208
|
-
<Button shape="pill" variant="
|
|
324
|
+
<Button shape="pill" variant="secondary" size="sm">
|
|
209
325
|
Filter
|
|
210
326
|
</Button>
|
|
211
|
-
<Button shape="pill"
|
|
212
|
-
|
|
327
|
+
<Button shape="pill">Group</Button>
|
|
328
|
+
<Button shape="pill" variant="cta">
|
|
329
|
+
<Plus /> New shipment
|
|
213
330
|
</Button>
|
|
214
|
-
<Button shape="pill">+ New shipment</Button>
|
|
215
331
|
</div>
|
|
216
332
|
</div>
|
|
217
333
|
),
|
|
@@ -220,68 +336,57 @@ export const Shapes: Story = {
|
|
|
220
336
|
/* =========================================================================
|
|
221
337
|
Icon-only — square dimensions via `size="icon-sm | icon-md | icon-lg"`.
|
|
222
338
|
`aria-label` is required (not enforced at the type level — add it manually).
|
|
339
|
+
Raised `primary` is the toolbar register: a lighter inner on the page ground.
|
|
223
340
|
========================================================================= */
|
|
224
341
|
|
|
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
342
|
export const IconOnly: Story = {
|
|
244
343
|
parameters: { controls: { hideNoControlsWarning: true } },
|
|
245
344
|
render: () => (
|
|
246
345
|
<div className={columns}>
|
|
247
346
|
<div className={label}>Sizes</div>
|
|
248
347
|
<div className="flex items-center gap-3">
|
|
249
|
-
<Button size="icon-sm"
|
|
348
|
+
<Button size="icon-sm" aria-label="Add">
|
|
250
349
|
<Plus />
|
|
251
350
|
</Button>
|
|
252
|
-
<Button size="icon-md"
|
|
351
|
+
<Button size="icon-md" aria-label="Add">
|
|
253
352
|
<Plus />
|
|
254
353
|
</Button>
|
|
255
|
-
<Button size="icon-lg"
|
|
354
|
+
<Button size="icon-lg" aria-label="Add">
|
|
256
355
|
<Plus />
|
|
257
356
|
</Button>
|
|
258
357
|
</div>
|
|
259
358
|
|
|
260
359
|
<div className={label}>Variants</div>
|
|
261
360
|
<div className="flex items-center gap-3">
|
|
361
|
+
<Button size="icon-md" variant="cta" aria-label="Next">
|
|
362
|
+
<ArrowRight />
|
|
363
|
+
</Button>
|
|
262
364
|
<Button size="icon-md" variant="primary" aria-label="Add">
|
|
263
365
|
<Plus />
|
|
264
366
|
</Button>
|
|
265
|
-
<Button size="icon-md" variant="accent" aria-label="Next">
|
|
266
|
-
<ArrowRight />
|
|
267
|
-
</Button>
|
|
268
367
|
<Button size="icon-md" variant="secondary" aria-label="Add">
|
|
269
368
|
<Plus />
|
|
270
369
|
</Button>
|
|
271
370
|
<Button size="icon-md" variant="ghost" aria-label="Add">
|
|
272
371
|
<Plus />
|
|
273
372
|
</Button>
|
|
373
|
+
<Button size="icon-md" variant="subtle" aria-label="Add">
|
|
374
|
+
<Plus />
|
|
375
|
+
</Button>
|
|
376
|
+
<Button size="icon-md" variant="danger" aria-label="Remove">
|
|
377
|
+
<Plus />
|
|
378
|
+
</Button>
|
|
274
379
|
</div>
|
|
275
380
|
|
|
276
381
|
<div className={label}>Pill shape</div>
|
|
277
382
|
<div className="flex items-center gap-3">
|
|
278
|
-
<Button size="icon-sm" variant="
|
|
383
|
+
<Button size="icon-sm" variant="secondary" shape="pill" aria-label="Add">
|
|
279
384
|
<Plus />
|
|
280
385
|
</Button>
|
|
281
386
|
<Button size="icon-md" variant="primary" shape="pill" aria-label="Add">
|
|
282
387
|
<Plus />
|
|
283
388
|
</Button>
|
|
284
|
-
<Button size="icon-lg" variant="
|
|
389
|
+
<Button size="icon-lg" variant="cta" shape="pill" aria-label="Next">
|
|
285
390
|
<ArrowRight />
|
|
286
391
|
</Button>
|
|
287
392
|
</div>
|
|
@@ -2,7 +2,7 @@ import { render, screen } from '@testing-library/react'
|
|
|
2
2
|
import userEvent from '@testing-library/user-event'
|
|
3
3
|
import { describe, expect, it, vi } from 'vitest'
|
|
4
4
|
|
|
5
|
-
import { Button } from './button'
|
|
5
|
+
import { Button, buttonStyles } from './button'
|
|
6
6
|
|
|
7
7
|
describe('Button', () => {
|
|
8
8
|
it('renders its label', () => {
|
|
@@ -46,14 +46,47 @@ describe('Button', () => {
|
|
|
46
46
|
expect(onPress).not.toHaveBeenCalled()
|
|
47
47
|
})
|
|
48
48
|
|
|
49
|
-
it('
|
|
49
|
+
it('renders every variant as a button', () => {
|
|
50
|
+
const variants = ['cta', 'accent', 'primary', 'secondary', 'ghost', 'subtle', 'link', 'danger'] as const
|
|
50
51
|
render(
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
<>
|
|
53
|
+
{variants.map((variant) => (
|
|
54
|
+
<Button key={variant} variant={variant}>
|
|
55
|
+
{variant}
|
|
56
|
+
</Button>
|
|
57
|
+
))}
|
|
58
|
+
</>,
|
|
59
|
+
)
|
|
60
|
+
for (const variant of variants) {
|
|
61
|
+
expect(screen.getByRole('button', { name: variant })).toBeInTheDocument()
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('danger fires onPress like any other variant', async () => {
|
|
66
|
+
const onPress = vi.fn()
|
|
67
|
+
const user = userEvent.setup()
|
|
68
|
+
|
|
69
|
+
render(
|
|
70
|
+
<Button variant="danger" onPress={onPress}>
|
|
71
|
+
Delete record
|
|
53
72
|
</Button>,
|
|
54
73
|
)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
expect(
|
|
74
|
+
await user.click(screen.getByRole('button', { name: 'Delete record' }))
|
|
75
|
+
|
|
76
|
+
expect(onPress).toHaveBeenCalledOnce()
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
// Contract: `accent` is a frozen alias of `cta`. Variant names are part of
|
|
80
|
+
// the runtime contract for module component bundles, so the alias must keep
|
|
81
|
+
// producing the identical chrome rather than silently diverging.
|
|
82
|
+
it('keeps accent as an exact alias of cta', () => {
|
|
83
|
+
expect(buttonStyles({ variant: 'accent' })).toBe(buttonStyles({ variant: 'cta' }))
|
|
84
|
+
expect(buttonStyles({ variant: 'accent', size: 'sm', shape: 'pill' })).toBe(
|
|
85
|
+
buttonStyles({ variant: 'cta', size: 'sm', shape: 'pill' }),
|
|
86
|
+
)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('defaults to the primary variant', () => {
|
|
90
|
+
expect(buttonStyles({})).toBe(buttonStyles({ variant: 'primary' }))
|
|
58
91
|
})
|
|
59
92
|
})
|
|
@@ -3,10 +3,56 @@ import { tv, type VariantProps } from 'tailwind-variants'
|
|
|
3
3
|
|
|
4
4
|
import { cn } from '../../lib/cn'
|
|
5
5
|
|
|
6
|
+
/* =========================================================================
|
|
7
|
+
Button — emphasis ladder, top to bottom:
|
|
8
|
+
|
|
9
|
+
cta ink fill. The one strong action per view (never two visible).
|
|
10
|
+
primary raised white: `surface` fill, `rule-strong` hairline, rest
|
|
11
|
+
shadow. The everyday button; the default variant.
|
|
12
|
+
secondary muted: `surface-3` fill, no border. Cancel / supporting actions.
|
|
13
|
+
ghost secondary's chrome, only on hover. Invisible at rest.
|
|
14
|
+
subtle borderless text button (toolbars, icon buttons).
|
|
15
|
+
link inline underline.
|
|
16
|
+
danger solid terracotta for destructive confirms.
|
|
17
|
+
|
|
18
|
+
`accent` is a frozen alias of `cta` — variant names are part of the runtime
|
|
19
|
+
contract for module component bundles, so nothing is ever removed.
|
|
20
|
+
|
|
21
|
+
Interaction state comes from React Aria's data attributes (`hovered:`,
|
|
22
|
+
`pressed:` — see the @custom-variant block in styles.css); `focus-visible:`
|
|
23
|
+
and `disabled:` stay on the native pseudo-classes because the RAC root IS
|
|
24
|
+
the focusable <button>. Hover is always a colour step on the surface ramp,
|
|
25
|
+
never opacity; press is translateY(1px) plus one more ramp step.
|
|
26
|
+
========================================================================= */
|
|
27
|
+
|
|
28
|
+
// Ink fill with the indigo-tinted hover. Kept verbatim from the old primary.
|
|
29
|
+
const ctaClasses = [
|
|
30
|
+
'bg-ink text-surface border-ink',
|
|
31
|
+
'hovered:bg-accent-ink hovered:border-accent-ink',
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
// Raised white: a lighter inner on the darker page ground. On a white card the
|
|
35
|
+
// hairline + rest shadow carry it. Press sinks one step and drops the shadow.
|
|
36
|
+
const raisedClasses = [
|
|
37
|
+
'bg-surface text-ink border-rule shadow-[var(--shadow-rest)]',
|
|
38
|
+
'hovered:bg-surface-2 hovered:border-rule-strong',
|
|
39
|
+
'pressed:bg-surface-3 pressed:shadow-none',
|
|
40
|
+
'focus-visible:shadow-[var(--shadow-focus),var(--shadow-rest)]',
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
// Muted gray fill, no border. One step quieter than primary.
|
|
44
|
+
const mutedClasses = [
|
|
45
|
+
'bg-surface-3 text-ink-2 border-transparent',
|
|
46
|
+
'hovered:bg-surface-4 hovered:text-ink',
|
|
47
|
+
'pressed:bg-[color-mix(in_oklab,var(--color-ink)_8%,var(--color-surface-4))]',
|
|
48
|
+
]
|
|
49
|
+
|
|
6
50
|
export const buttonStyles = tv({
|
|
7
51
|
base: [
|
|
8
52
|
'inline-flex items-center justify-center gap-2 whitespace-nowrap cursor-pointer',
|
|
9
|
-
|
|
53
|
+
// Button labels are human copy, not machine fact — Inter 500 (DESIGN.md
|
|
54
|
+
// typography.body), not the mono reserved for ids, timestamps and badges.
|
|
55
|
+
'font-sans font-medium tracking-[-0.005em] border border-transparent',
|
|
10
56
|
'transition-[background-color,color,border-color,box-shadow,transform] duration-150',
|
|
11
57
|
'outline-none pressed:translate-y-px',
|
|
12
58
|
'focus-visible:shadow-[var(--shadow-focus)]',
|
|
@@ -14,26 +60,31 @@ export const buttonStyles = tv({
|
|
|
14
60
|
],
|
|
15
61
|
variants: {
|
|
16
62
|
variant: {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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'],
|
|
63
|
+
cta: ctaClasses,
|
|
64
|
+
accent: ctaClasses,
|
|
65
|
+
primary: raisedClasses,
|
|
66
|
+
secondary: mutedClasses,
|
|
26
67
|
ghost: [
|
|
27
|
-
'bg-
|
|
28
|
-
'
|
|
68
|
+
'bg-transparent text-ink-2 border-transparent',
|
|
69
|
+
'hovered:bg-surface-3 hovered:text-ink',
|
|
70
|
+
'pressed:bg-surface-4',
|
|
71
|
+
],
|
|
72
|
+
subtle: [
|
|
73
|
+
'bg-transparent text-ink-3 border-transparent',
|
|
74
|
+
'hovered:bg-surface-2 hovered:text-ink',
|
|
75
|
+
'pressed:bg-surface-3',
|
|
29
76
|
],
|
|
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
77
|
link: [
|
|
34
78
|
'bg-transparent text-ink-2 border-transparent',
|
|
35
79
|
'px-0.5 border-b border-b-ink-4',
|
|
36
|
-
'
|
|
80
|
+
'hovered:text-ink hovered:border-b-ink',
|
|
81
|
+
],
|
|
82
|
+
// `text-surface`, not white: dark's --color-danger is a light salmon, and
|
|
83
|
+
// the surface token flips dark with it while white would fail contrast.
|
|
84
|
+
danger: [
|
|
85
|
+
'bg-danger text-surface border-danger',
|
|
86
|
+
'hovered:bg-[color-mix(in_oklab,var(--color-danger)_88%,var(--color-ink))]',
|
|
87
|
+
'hovered:border-[color-mix(in_oklab,var(--color-danger)_88%,var(--color-ink))]',
|
|
37
88
|
],
|
|
38
89
|
},
|
|
39
90
|
size: {
|
|
@@ -46,8 +97,8 @@ export const buttonStyles = tv({
|
|
|
46
97
|
'icon-lg': 'size-[42px] p-0 [&_svg]:size-[18px]',
|
|
47
98
|
},
|
|
48
99
|
// Corner shape. Default is rounded (soft 10px radius — the standard
|
|
49
|
-
// Proteos button), `pill` is the fully-rounded
|
|
50
|
-
// The `link` variant overrides both to a flat underline.
|
|
100
|
+
// Proteos button), `pill` is the fully-rounded treatment for a small set
|
|
101
|
+
// of prominent CTAs. The `link` variant overrides both to a flat underline.
|
|
51
102
|
shape: {
|
|
52
103
|
rounded: 'rounded-md',
|
|
53
104
|
pill: 'rounded-pill',
|
|
@@ -60,6 +111,8 @@ export const buttonStyles = tv({
|
|
|
60
111
|
// Link never takes a corner radius — it's an underline, not a box.
|
|
61
112
|
{ variant: 'link', shape: 'rounded', class: 'rounded-none' },
|
|
62
113
|
{ variant: 'link', shape: 'pill', class: 'rounded-none' },
|
|
114
|
+
// Destructive confirms get the terracotta halo instead of the indigo one.
|
|
115
|
+
{ variant: 'danger', class: 'focus-visible:shadow-[var(--shadow-focus-danger)]' },
|
|
63
116
|
],
|
|
64
117
|
defaultVariants: {
|
|
65
118
|
variant: 'primary',
|
|
@@ -142,8 +142,8 @@ export function Combobox<T extends Key>({
|
|
|
142
142
|
'min-w-[var(--trigger-width)]',
|
|
143
143
|
'rounded-md border border-rule bg-surface p-1',
|
|
144
144
|
'shadow-[var(--shadow-raised)]',
|
|
145
|
-
'entering:animate-
|
|
146
|
-
'exiting:animate-
|
|
145
|
+
'entering:animate-enter-slide-down',
|
|
146
|
+
'exiting:animate-exit-slide-up',
|
|
147
147
|
)}
|
|
148
148
|
>
|
|
149
149
|
<ListBox
|