sonance-brand-mcp 1.1.3 → 1.1.7
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/assets/components/accordion.stories.tsx +310 -0
- package/dist/assets/components/accordion.tsx +56 -30
- package/dist/assets/components/alert.stories.tsx +199 -0
- package/dist/assets/components/autocomplete.stories.tsx +307 -0
- package/dist/assets/components/autocomplete.tsx +28 -4
- package/dist/assets/components/avatar.stories.tsx +175 -0
- package/dist/assets/components/badge.stories.tsx +258 -0
- package/dist/assets/components/breadcrumbs.stories.tsx +175 -0
- package/dist/assets/components/button.stories.tsx +362 -0
- package/dist/assets/components/button.tsx +48 -3
- package/dist/assets/components/calendar.stories.tsx +247 -0
- package/dist/assets/components/card.stories.tsx +275 -0
- package/dist/assets/components/card.tsx +26 -1
- package/dist/assets/components/checkbox-group.stories.tsx +281 -0
- package/dist/assets/components/checkbox.stories.tsx +160 -0
- package/dist/assets/components/checkbox.tsx +32 -4
- package/dist/assets/components/code.stories.tsx +265 -0
- package/dist/assets/components/date-input.stories.tsx +278 -0
- package/dist/assets/components/date-input.tsx +24 -2
- package/dist/assets/components/date-picker.stories.tsx +337 -0
- package/dist/assets/components/date-picker.tsx +28 -4
- package/dist/assets/components/date-range-picker.stories.tsx +340 -0
- package/dist/assets/components/dialog.stories.tsx +285 -0
- package/dist/assets/components/divider.stories.tsx +176 -0
- package/dist/assets/components/drawer.stories.tsx +216 -0
- package/dist/assets/components/dropdown.stories.tsx +342 -0
- package/dist/assets/components/dropdown.tsx +55 -10
- package/dist/assets/components/form.stories.tsx +372 -0
- package/dist/assets/components/image.stories.tsx +348 -0
- package/dist/assets/components/input-otp.stories.tsx +336 -0
- package/dist/assets/components/input-otp.tsx +24 -2
- package/dist/assets/components/input.stories.tsx +223 -0
- package/dist/assets/components/input.tsx +27 -2
- package/dist/assets/components/kbd.stories.tsx +272 -0
- package/dist/assets/components/link.stories.tsx +199 -0
- package/dist/assets/components/link.tsx +50 -1
- package/dist/assets/components/listbox.stories.tsx +287 -0
- package/dist/assets/components/listbox.tsx +30 -7
- package/dist/assets/components/navbar.stories.tsx +218 -0
- package/dist/assets/components/number-input.stories.tsx +295 -0
- package/dist/assets/components/number-input.tsx +30 -8
- package/dist/assets/components/pagination.stories.tsx +280 -0
- package/dist/assets/components/pagination.tsx +45 -21
- package/dist/assets/components/popover.stories.tsx +219 -0
- package/dist/assets/components/progress.stories.tsx +153 -0
- package/dist/assets/components/radio-group.stories.tsx +187 -0
- package/dist/assets/components/radio-group.tsx +30 -6
- package/dist/assets/components/range-calendar.stories.tsx +334 -0
- package/dist/assets/components/scroll-shadow.stories.tsx +335 -0
- package/dist/assets/components/select.stories.tsx +192 -0
- package/dist/assets/components/select.tsx +54 -7
- package/dist/assets/components/skeleton.stories.tsx +166 -0
- package/dist/assets/components/slider.stories.tsx +145 -0
- package/dist/assets/components/slider.tsx +43 -8
- package/dist/assets/components/spacer.stories.tsx +216 -0
- package/dist/assets/components/spinner.stories.tsx +149 -0
- package/dist/assets/components/switch.stories.tsx +170 -0
- package/dist/assets/components/switch.tsx +29 -4
- package/dist/assets/components/table.stories.tsx +322 -0
- package/dist/assets/components/tabs.stories.tsx +306 -0
- package/dist/assets/components/tabs.tsx +25 -4
- package/dist/assets/components/textarea.stories.tsx +103 -0
- package/dist/assets/components/textarea.tsx +27 -3
- package/dist/assets/components/theme-toggle.stories.tsx +248 -0
- package/dist/assets/components/time-input.stories.tsx +365 -0
- package/dist/assets/components/time-input.tsx +25 -3
- package/dist/assets/components/toast.stories.tsx +195 -0
- package/dist/assets/components/tooltip.stories.tsx +226 -0
- package/dist/assets/components/user.stories.tsx +274 -0
- package/dist/index.js +1732 -13
- package/package.json +1 -1
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/nextjs-vite';
|
|
2
|
+
import { Button, type ButtonState } from './button';
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof Button> = {
|
|
5
|
+
title: 'Components/Forms/Button',
|
|
6
|
+
component: Button,
|
|
7
|
+
tags: ['autodocs'],
|
|
8
|
+
parameters: {
|
|
9
|
+
docs: {
|
|
10
|
+
description: {
|
|
11
|
+
component: 'A versatile button component with multiple variants and sizes. Follows Sonance brand guidelines with uppercase text and clean styling.',
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
argTypes: {
|
|
16
|
+
variant: {
|
|
17
|
+
control: 'select',
|
|
18
|
+
options: ['primary', 'secondary', 'ghost', 'inverted'],
|
|
19
|
+
description: 'Visual style variant',
|
|
20
|
+
table: {
|
|
21
|
+
defaultValue: { summary: 'primary' },
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
size: {
|
|
25
|
+
control: 'select',
|
|
26
|
+
options: ['sm', 'md', 'lg'],
|
|
27
|
+
description: 'Button size',
|
|
28
|
+
table: {
|
|
29
|
+
defaultValue: { summary: 'md' },
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
state: {
|
|
33
|
+
control: 'select',
|
|
34
|
+
options: ['default', 'hover', 'active', 'focus', 'disabled'],
|
|
35
|
+
description: 'Visual state for documentation',
|
|
36
|
+
table: {
|
|
37
|
+
defaultValue: { summary: 'default' },
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
disabled: {
|
|
41
|
+
control: 'boolean',
|
|
42
|
+
description: 'Disable the button',
|
|
43
|
+
},
|
|
44
|
+
children: {
|
|
45
|
+
control: 'text',
|
|
46
|
+
description: 'Button label',
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export default meta;
|
|
52
|
+
type Story = StoryObj<typeof meta>;
|
|
53
|
+
|
|
54
|
+
// Default story
|
|
55
|
+
export const Default: Story = {
|
|
56
|
+
args: {
|
|
57
|
+
children: 'Button',
|
|
58
|
+
variant: 'primary',
|
|
59
|
+
size: 'md',
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// Variants
|
|
64
|
+
export const Primary: Story = {
|
|
65
|
+
args: {
|
|
66
|
+
children: 'Primary Button',
|
|
67
|
+
variant: 'primary',
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const Secondary: Story = {
|
|
72
|
+
args: {
|
|
73
|
+
children: 'Secondary Button',
|
|
74
|
+
variant: 'secondary',
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export const Ghost: Story = {
|
|
79
|
+
args: {
|
|
80
|
+
children: 'Ghost Button',
|
|
81
|
+
variant: 'ghost',
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const Inverted: Story = {
|
|
86
|
+
args: {
|
|
87
|
+
children: 'Inverted Button',
|
|
88
|
+
variant: 'inverted',
|
|
89
|
+
},
|
|
90
|
+
parameters: {
|
|
91
|
+
backgrounds: {
|
|
92
|
+
default: 'dark',
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
decorators: [
|
|
96
|
+
(Story) => (
|
|
97
|
+
<div className="bg-sonance-charcoal p-8 rounded">
|
|
98
|
+
<Story />
|
|
99
|
+
</div>
|
|
100
|
+
),
|
|
101
|
+
],
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// Sizes
|
|
105
|
+
export const Small: Story = {
|
|
106
|
+
args: {
|
|
107
|
+
children: 'Small Button',
|
|
108
|
+
size: 'sm',
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export const Medium: Story = {
|
|
113
|
+
args: {
|
|
114
|
+
children: 'Medium Button',
|
|
115
|
+
size: 'md',
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export const Large: Story = {
|
|
120
|
+
args: {
|
|
121
|
+
children: 'Large Button',
|
|
122
|
+
size: 'lg',
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
// States
|
|
127
|
+
export const Disabled: Story = {
|
|
128
|
+
args: {
|
|
129
|
+
children: 'Disabled',
|
|
130
|
+
disabled: true,
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// All Variants
|
|
135
|
+
export const AllVariants: Story = {
|
|
136
|
+
render: () => (
|
|
137
|
+
<div className="flex flex-wrap gap-4">
|
|
138
|
+
<Button variant="primary">Primary</Button>
|
|
139
|
+
<Button variant="secondary">Secondary</Button>
|
|
140
|
+
<Button variant="ghost">Ghost</Button>
|
|
141
|
+
<div className="bg-sonance-charcoal p-2 rounded">
|
|
142
|
+
<Button variant="inverted">Inverted</Button>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
),
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// All Sizes
|
|
149
|
+
export const AllSizes: Story = {
|
|
150
|
+
render: () => (
|
|
151
|
+
<div className="flex items-center gap-4">
|
|
152
|
+
<Button size="sm">Small</Button>
|
|
153
|
+
<Button size="md">Medium</Button>
|
|
154
|
+
<Button size="lg">Large</Button>
|
|
155
|
+
</div>
|
|
156
|
+
),
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// Variant Matrix
|
|
160
|
+
export const VariantMatrix: Story = {
|
|
161
|
+
render: () => (
|
|
162
|
+
<div className="space-y-6">
|
|
163
|
+
<div>
|
|
164
|
+
<h3 className="text-sm font-medium text-foreground-muted mb-3">Primary</h3>
|
|
165
|
+
<div className="flex items-center gap-4">
|
|
166
|
+
<Button variant="primary" size="sm">Small</Button>
|
|
167
|
+
<Button variant="primary" size="md">Medium</Button>
|
|
168
|
+
<Button variant="primary" size="lg">Large</Button>
|
|
169
|
+
<Button variant="primary" size="md" disabled>Disabled</Button>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
<div>
|
|
173
|
+
<h3 className="text-sm font-medium text-foreground-muted mb-3">Secondary</h3>
|
|
174
|
+
<div className="flex items-center gap-4">
|
|
175
|
+
<Button variant="secondary" size="sm">Small</Button>
|
|
176
|
+
<Button variant="secondary" size="md">Medium</Button>
|
|
177
|
+
<Button variant="secondary" size="lg">Large</Button>
|
|
178
|
+
<Button variant="secondary" size="md" disabled>Disabled</Button>
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
<div>
|
|
182
|
+
<h3 className="text-sm font-medium text-foreground-muted mb-3">Ghost</h3>
|
|
183
|
+
<div className="flex items-center gap-4">
|
|
184
|
+
<Button variant="ghost" size="sm">Small</Button>
|
|
185
|
+
<Button variant="ghost" size="md">Medium</Button>
|
|
186
|
+
<Button variant="ghost" size="lg">Large</Button>
|
|
187
|
+
<Button variant="ghost" size="md" disabled>Disabled</Button>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
),
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
// Interactive States - for Figma documentation
|
|
195
|
+
export const PrimaryStates: Story = {
|
|
196
|
+
render: () => (
|
|
197
|
+
<div className="space-y-4">
|
|
198
|
+
<h3 className="text-sm font-medium text-foreground-muted">Primary Button States</h3>
|
|
199
|
+
<div className="flex items-center gap-4">
|
|
200
|
+
<div className="text-center">
|
|
201
|
+
<Button variant="primary" state="default">Default</Button>
|
|
202
|
+
<p className="text-xs text-foreground-muted mt-2">Default</p>
|
|
203
|
+
</div>
|
|
204
|
+
<div className="text-center">
|
|
205
|
+
<Button variant="primary" state="hover">Hover</Button>
|
|
206
|
+
<p className="text-xs text-foreground-muted mt-2">Hover</p>
|
|
207
|
+
</div>
|
|
208
|
+
<div className="text-center">
|
|
209
|
+
<Button variant="primary" state="active">Active</Button>
|
|
210
|
+
<p className="text-xs text-foreground-muted mt-2">Active</p>
|
|
211
|
+
</div>
|
|
212
|
+
<div className="text-center">
|
|
213
|
+
<Button variant="primary" state="focus">Focus</Button>
|
|
214
|
+
<p className="text-xs text-foreground-muted mt-2">Focus</p>
|
|
215
|
+
</div>
|
|
216
|
+
<div className="text-center">
|
|
217
|
+
<Button variant="primary" state="disabled">Disabled</Button>
|
|
218
|
+
<p className="text-xs text-foreground-muted mt-2">Disabled</p>
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
),
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
export const SecondaryStates: Story = {
|
|
226
|
+
render: () => (
|
|
227
|
+
<div className="space-y-4">
|
|
228
|
+
<h3 className="text-sm font-medium text-foreground-muted">Secondary Button States</h3>
|
|
229
|
+
<div className="flex items-center gap-4">
|
|
230
|
+
<div className="text-center">
|
|
231
|
+
<Button variant="secondary" state="default">Default</Button>
|
|
232
|
+
<p className="text-xs text-foreground-muted mt-2">Default</p>
|
|
233
|
+
</div>
|
|
234
|
+
<div className="text-center">
|
|
235
|
+
<Button variant="secondary" state="hover">Hover</Button>
|
|
236
|
+
<p className="text-xs text-foreground-muted mt-2">Hover</p>
|
|
237
|
+
</div>
|
|
238
|
+
<div className="text-center">
|
|
239
|
+
<Button variant="secondary" state="active">Active</Button>
|
|
240
|
+
<p className="text-xs text-foreground-muted mt-2">Active</p>
|
|
241
|
+
</div>
|
|
242
|
+
<div className="text-center">
|
|
243
|
+
<Button variant="secondary" state="focus">Focus</Button>
|
|
244
|
+
<p className="text-xs text-foreground-muted mt-2">Focus</p>
|
|
245
|
+
</div>
|
|
246
|
+
<div className="text-center">
|
|
247
|
+
<Button variant="secondary" state="disabled">Disabled</Button>
|
|
248
|
+
<p className="text-xs text-foreground-muted mt-2">Disabled</p>
|
|
249
|
+
</div>
|
|
250
|
+
</div>
|
|
251
|
+
</div>
|
|
252
|
+
),
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
export const GhostStates: Story = {
|
|
256
|
+
render: () => (
|
|
257
|
+
<div className="space-y-4">
|
|
258
|
+
<h3 className="text-sm font-medium text-foreground-muted">Ghost Button States</h3>
|
|
259
|
+
<div className="flex items-center gap-4">
|
|
260
|
+
<div className="text-center">
|
|
261
|
+
<Button variant="ghost" state="default">Default</Button>
|
|
262
|
+
<p className="text-xs text-foreground-muted mt-2">Default</p>
|
|
263
|
+
</div>
|
|
264
|
+
<div className="text-center">
|
|
265
|
+
<Button variant="ghost" state="hover">Hover</Button>
|
|
266
|
+
<p className="text-xs text-foreground-muted mt-2">Hover</p>
|
|
267
|
+
</div>
|
|
268
|
+
<div className="text-center">
|
|
269
|
+
<Button variant="ghost" state="active">Active</Button>
|
|
270
|
+
<p className="text-xs text-foreground-muted mt-2">Active</p>
|
|
271
|
+
</div>
|
|
272
|
+
<div className="text-center">
|
|
273
|
+
<Button variant="ghost" state="focus">Focus</Button>
|
|
274
|
+
<p className="text-xs text-foreground-muted mt-2">Focus</p>
|
|
275
|
+
</div>
|
|
276
|
+
<div className="text-center">
|
|
277
|
+
<Button variant="ghost" state="disabled">Disabled</Button>
|
|
278
|
+
<p className="text-xs text-foreground-muted mt-2">Disabled</p>
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
),
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
// Complete State Matrix - All variants × All states
|
|
286
|
+
export const StateMatrix: Story = {
|
|
287
|
+
render: () => {
|
|
288
|
+
const variants = ['primary', 'secondary', 'ghost'] as const;
|
|
289
|
+
const states: ButtonState[] = ['default', 'hover', 'active', 'focus', 'disabled'];
|
|
290
|
+
|
|
291
|
+
return (
|
|
292
|
+
<div className="space-y-8">
|
|
293
|
+
<div className="grid grid-cols-6 gap-4 text-center">
|
|
294
|
+
<div></div>
|
|
295
|
+
{states.map((state) => (
|
|
296
|
+
<div key={state} className="text-xs font-medium text-foreground-muted uppercase">
|
|
297
|
+
{state}
|
|
298
|
+
</div>
|
|
299
|
+
))}
|
|
300
|
+
</div>
|
|
301
|
+
{variants.map((variant) => (
|
|
302
|
+
<div key={variant} className="grid grid-cols-6 gap-4 items-center">
|
|
303
|
+
<div className="text-xs font-medium text-foreground-muted uppercase">{variant}</div>
|
|
304
|
+
{states.map((state) => (
|
|
305
|
+
<Button key={`${variant}-${state}`} variant={variant} state={state}>
|
|
306
|
+
Button
|
|
307
|
+
</Button>
|
|
308
|
+
))}
|
|
309
|
+
</div>
|
|
310
|
+
))}
|
|
311
|
+
</div>
|
|
312
|
+
);
|
|
313
|
+
},
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
// Responsive Matrix - Mobile, Tablet, Desktop
|
|
317
|
+
export const ResponsiveMatrix: Story = {
|
|
318
|
+
render: () => (
|
|
319
|
+
<div className="space-y-8">
|
|
320
|
+
{/* Mobile */}
|
|
321
|
+
<div>
|
|
322
|
+
<h4 className="text-xs uppercase text-foreground-muted mb-2">Mobile (375px)</h4>
|
|
323
|
+
<div className="w-[375px] border border-dashed border-border p-4">
|
|
324
|
+
<div className="flex flex-col gap-2">
|
|
325
|
+
<Button variant="primary" className="w-full">Primary Button</Button>
|
|
326
|
+
<Button variant="secondary" className="w-full">Secondary Button</Button>
|
|
327
|
+
<div className="flex gap-2">
|
|
328
|
+
<Button variant="primary" size="sm">Small</Button>
|
|
329
|
+
<Button variant="secondary" size="sm">Small</Button>
|
|
330
|
+
</div>
|
|
331
|
+
</div>
|
|
332
|
+
</div>
|
|
333
|
+
</div>
|
|
334
|
+
{/* Tablet */}
|
|
335
|
+
<div>
|
|
336
|
+
<h4 className="text-xs uppercase text-foreground-muted mb-2">Tablet (768px)</h4>
|
|
337
|
+
<div className="w-[768px] border border-dashed border-border p-4">
|
|
338
|
+
<div className="flex gap-4">
|
|
339
|
+
<Button variant="primary">Primary Button</Button>
|
|
340
|
+
<Button variant="secondary">Secondary Button</Button>
|
|
341
|
+
<Button variant="ghost">Ghost Button</Button>
|
|
342
|
+
</div>
|
|
343
|
+
</div>
|
|
344
|
+
</div>
|
|
345
|
+
{/* Desktop */}
|
|
346
|
+
<div>
|
|
347
|
+
<h4 className="text-xs uppercase text-foreground-muted mb-2">Desktop (1280px)</h4>
|
|
348
|
+
<div className="w-[1280px] border border-dashed border-border p-4">
|
|
349
|
+
<div className="flex gap-4 items-center">
|
|
350
|
+
<Button variant="primary" size="lg">Large Primary</Button>
|
|
351
|
+
<Button variant="secondary" size="lg">Large Secondary</Button>
|
|
352
|
+
<Button variant="ghost" size="lg">Large Ghost</Button>
|
|
353
|
+
<Button variant="primary" size="md">Medium</Button>
|
|
354
|
+
<Button variant="secondary" size="md">Medium</Button>
|
|
355
|
+
<Button variant="primary" size="sm">Small</Button>
|
|
356
|
+
<Button variant="secondary" size="sm">Small</Button>
|
|
357
|
+
</div>
|
|
358
|
+
</div>
|
|
359
|
+
</div>
|
|
360
|
+
</div>
|
|
361
|
+
),
|
|
362
|
+
};
|
|
@@ -2,6 +2,8 @@ import { cva, type VariantProps } from "class-variance-authority";
|
|
|
2
2
|
import { cn } from "@/lib/utils";
|
|
3
3
|
import { forwardRef } from "react";
|
|
4
4
|
|
|
5
|
+
export type ButtonState = "default" | "hover" | "active" | "focus" | "disabled";
|
|
6
|
+
|
|
5
7
|
const buttonVariants = cva(
|
|
6
8
|
"inline-flex items-center justify-center font-medium uppercase tracking-wide transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50",
|
|
7
9
|
{
|
|
@@ -28,16 +30,59 @@ const buttonVariants = cva(
|
|
|
28
30
|
}
|
|
29
31
|
);
|
|
30
32
|
|
|
33
|
+
// State styles for Storybook/Figma visualization
|
|
34
|
+
const getStateStyles = (variant: string | null | undefined, state?: ButtonState) => {
|
|
35
|
+
if (!state || state === "default") return "";
|
|
36
|
+
|
|
37
|
+
const stateMap: Record<string, Record<string, string>> = {
|
|
38
|
+
primary: {
|
|
39
|
+
hover: "bg-primary-hover",
|
|
40
|
+
active: "bg-primary-active",
|
|
41
|
+
focus: "ring-2 ring-border-focus ring-offset-2 ring-offset-background",
|
|
42
|
+
disabled: "opacity-50 pointer-events-none",
|
|
43
|
+
},
|
|
44
|
+
secondary: {
|
|
45
|
+
hover: "bg-secondary-hover border-border-hover",
|
|
46
|
+
active: "bg-secondary-hover",
|
|
47
|
+
focus: "ring-2 ring-border-focus ring-offset-2 ring-offset-background",
|
|
48
|
+
disabled: "opacity-50 pointer-events-none",
|
|
49
|
+
},
|
|
50
|
+
ghost: {
|
|
51
|
+
hover: "bg-secondary-hover",
|
|
52
|
+
active: "bg-secondary-hover",
|
|
53
|
+
focus: "ring-2 ring-border-focus ring-offset-2 ring-offset-background",
|
|
54
|
+
disabled: "opacity-50 pointer-events-none",
|
|
55
|
+
},
|
|
56
|
+
inverted: {
|
|
57
|
+
hover: "opacity-90",
|
|
58
|
+
active: "opacity-80",
|
|
59
|
+
focus: "ring-2 ring-border-focus ring-offset-2 ring-offset-background",
|
|
60
|
+
disabled: "opacity-50 pointer-events-none",
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
return stateMap[variant || "primary"]?.[state] || "";
|
|
65
|
+
};
|
|
66
|
+
|
|
31
67
|
interface ButtonProps
|
|
32
68
|
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
33
|
-
VariantProps<typeof buttonVariants> {
|
|
69
|
+
VariantProps<typeof buttonVariants> {
|
|
70
|
+
/** Visual state for Storybook/Figma documentation */
|
|
71
|
+
state?: ButtonState;
|
|
72
|
+
}
|
|
34
73
|
|
|
35
74
|
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
|
36
|
-
({ className, variant, size, ...props }, ref) => {
|
|
75
|
+
({ className, variant, size, state, disabled, ...props }, ref) => {
|
|
76
|
+
const isDisabled = disabled || state === "disabled";
|
|
77
|
+
|
|
37
78
|
return (
|
|
38
79
|
<button
|
|
39
|
-
className={cn(
|
|
80
|
+
className={cn(
|
|
81
|
+
buttonVariants({ variant, size, className }),
|
|
82
|
+
getStateStyles(variant, state)
|
|
83
|
+
)}
|
|
40
84
|
ref={ref}
|
|
85
|
+
disabled={isDisabled}
|
|
41
86
|
{...props}
|
|
42
87
|
/>
|
|
43
88
|
);
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/nextjs-vite';
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { addDays, subDays } from 'date-fns';
|
|
4
|
+
import { Calendar, MiniCalendar } from './calendar';
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof Calendar> = {
|
|
7
|
+
title: 'Components/Forms/Calendar',
|
|
8
|
+
component: Calendar,
|
|
9
|
+
tags: ['autodocs'],
|
|
10
|
+
parameters: {
|
|
11
|
+
docs: {
|
|
12
|
+
description: {
|
|
13
|
+
component: 'A date calendar component for selecting single dates. Supports min/max dates, disabled dates, and customizable styling.',
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
argTypes: {
|
|
18
|
+
selected: {
|
|
19
|
+
control: 'date',
|
|
20
|
+
description: 'The currently selected date',
|
|
21
|
+
},
|
|
22
|
+
minDate: {
|
|
23
|
+
control: 'date',
|
|
24
|
+
description: 'Minimum selectable date',
|
|
25
|
+
},
|
|
26
|
+
maxDate: {
|
|
27
|
+
control: 'date',
|
|
28
|
+
description: 'Maximum selectable date',
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export default meta;
|
|
34
|
+
type Story = StoryObj<typeof meta>;
|
|
35
|
+
|
|
36
|
+
// Default
|
|
37
|
+
export const Default: Story = {
|
|
38
|
+
render: () => {
|
|
39
|
+
const [selected, setSelected] = useState<Date | undefined>(undefined);
|
|
40
|
+
return (
|
|
41
|
+
<Calendar
|
|
42
|
+
selected={selected}
|
|
43
|
+
onSelect={setSelected}
|
|
44
|
+
className="w-fit"
|
|
45
|
+
/>
|
|
46
|
+
);
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// With Selected Date
|
|
51
|
+
export const WithSelectedDate: Story = {
|
|
52
|
+
render: () => {
|
|
53
|
+
const [selected, setSelected] = useState<Date>(new Date());
|
|
54
|
+
return (
|
|
55
|
+
<Calendar
|
|
56
|
+
selected={selected}
|
|
57
|
+
onSelect={setSelected}
|
|
58
|
+
className="w-fit"
|
|
59
|
+
/>
|
|
60
|
+
);
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// With Min/Max Dates
|
|
65
|
+
export const WithMinMaxDates: Story = {
|
|
66
|
+
render: () => {
|
|
67
|
+
const [selected, setSelected] = useState<Date | undefined>(undefined);
|
|
68
|
+
const today = new Date();
|
|
69
|
+
return (
|
|
70
|
+
<div className="space-y-4">
|
|
71
|
+
<p className="text-sm text-foreground-muted">
|
|
72
|
+
Only dates within the next 30 days are selectable
|
|
73
|
+
</p>
|
|
74
|
+
<Calendar
|
|
75
|
+
selected={selected}
|
|
76
|
+
onSelect={setSelected}
|
|
77
|
+
minDate={today}
|
|
78
|
+
maxDate={addDays(today, 30)}
|
|
79
|
+
className="w-fit"
|
|
80
|
+
/>
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// With Disabled Dates (weekends)
|
|
87
|
+
export const DisabledWeekends: Story = {
|
|
88
|
+
render: () => {
|
|
89
|
+
const [selected, setSelected] = useState<Date | undefined>(undefined);
|
|
90
|
+
const isWeekend = (date: Date) => {
|
|
91
|
+
const day = date.getDay();
|
|
92
|
+
return day === 0 || day === 6;
|
|
93
|
+
};
|
|
94
|
+
return (
|
|
95
|
+
<div className="space-y-4">
|
|
96
|
+
<p className="text-sm text-foreground-muted">
|
|
97
|
+
Weekends are disabled
|
|
98
|
+
</p>
|
|
99
|
+
<Calendar
|
|
100
|
+
selected={selected}
|
|
101
|
+
onSelect={setSelected}
|
|
102
|
+
disabled={isWeekend}
|
|
103
|
+
className="w-fit"
|
|
104
|
+
/>
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// Past Dates Only
|
|
111
|
+
export const PastDatesOnly: Story = {
|
|
112
|
+
render: () => {
|
|
113
|
+
const [selected, setSelected] = useState<Date | undefined>(undefined);
|
|
114
|
+
const today = new Date();
|
|
115
|
+
return (
|
|
116
|
+
<div className="space-y-4">
|
|
117
|
+
<p className="text-sm text-foreground-muted">
|
|
118
|
+
Only past dates are selectable (useful for birthdays)
|
|
119
|
+
</p>
|
|
120
|
+
<Calendar
|
|
121
|
+
selected={selected}
|
|
122
|
+
onSelect={setSelected}
|
|
123
|
+
maxDate={subDays(today, 1)}
|
|
124
|
+
className="w-fit"
|
|
125
|
+
/>
|
|
126
|
+
</div>
|
|
127
|
+
);
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// Mini Calendar
|
|
132
|
+
export const Mini: Story = {
|
|
133
|
+
render: () => {
|
|
134
|
+
const [selected, setSelected] = useState<Date | undefined>(new Date());
|
|
135
|
+
return (
|
|
136
|
+
<div className="space-y-4">
|
|
137
|
+
<p className="text-sm text-foreground-muted">
|
|
138
|
+
Compact calendar for smaller spaces
|
|
139
|
+
</p>
|
|
140
|
+
<MiniCalendar
|
|
141
|
+
selected={selected}
|
|
142
|
+
onSelect={setSelected}
|
|
143
|
+
className="w-fit"
|
|
144
|
+
/>
|
|
145
|
+
</div>
|
|
146
|
+
);
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
// Booking Example
|
|
151
|
+
export const BookingExample: Story = {
|
|
152
|
+
render: () => {
|
|
153
|
+
const [selected, setSelected] = useState<Date | undefined>(undefined);
|
|
154
|
+
const today = new Date();
|
|
155
|
+
|
|
156
|
+
// Simulate some booked dates
|
|
157
|
+
const bookedDates = [
|
|
158
|
+
addDays(today, 3),
|
|
159
|
+
addDays(today, 4),
|
|
160
|
+
addDays(today, 10),
|
|
161
|
+
addDays(today, 11),
|
|
162
|
+
addDays(today, 12),
|
|
163
|
+
];
|
|
164
|
+
|
|
165
|
+
const isBooked = (date: Date) => {
|
|
166
|
+
return bookedDates.some(
|
|
167
|
+
bookedDate =>
|
|
168
|
+
bookedDate.getDate() === date.getDate() &&
|
|
169
|
+
bookedDate.getMonth() === date.getMonth() &&
|
|
170
|
+
bookedDate.getFullYear() === date.getFullYear()
|
|
171
|
+
);
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
return (
|
|
175
|
+
<div className="space-y-4">
|
|
176
|
+
<p className="text-sm text-foreground-muted">
|
|
177
|
+
Select an available appointment date
|
|
178
|
+
</p>
|
|
179
|
+
<Calendar
|
|
180
|
+
selected={selected}
|
|
181
|
+
onSelect={setSelected}
|
|
182
|
+
minDate={today}
|
|
183
|
+
maxDate={addDays(today, 60)}
|
|
184
|
+
disabled={isBooked}
|
|
185
|
+
className="w-fit"
|
|
186
|
+
/>
|
|
187
|
+
{selected && (
|
|
188
|
+
<p className="text-sm text-foreground">
|
|
189
|
+
Selected: {selected.toLocaleDateString()}
|
|
190
|
+
</p>
|
|
191
|
+
)}
|
|
192
|
+
</div>
|
|
193
|
+
);
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// Responsive Matrix - Mobile, Tablet, Desktop
|
|
198
|
+
export const ResponsiveMatrix: Story = {
|
|
199
|
+
render: () => {
|
|
200
|
+
const [mobile, setMobile] = useState<Date | undefined>(new Date());
|
|
201
|
+
const [tablet, setTablet] = useState<Date | undefined>(new Date());
|
|
202
|
+
const [desktop, setDesktop] = useState<Date | undefined>(new Date());
|
|
203
|
+
|
|
204
|
+
return (
|
|
205
|
+
<div className="space-y-8">
|
|
206
|
+
{/* Mobile */}
|
|
207
|
+
<div>
|
|
208
|
+
<h4 className="text-xs uppercase text-foreground-muted mb-2">Mobile (375px)</h4>
|
|
209
|
+
<div className="w-[375px] border border-dashed border-border p-4">
|
|
210
|
+
<MiniCalendar
|
|
211
|
+
selected={mobile}
|
|
212
|
+
onSelect={setMobile}
|
|
213
|
+
className="w-fit"
|
|
214
|
+
/>
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
{/* Tablet */}
|
|
218
|
+
<div>
|
|
219
|
+
<h4 className="text-xs uppercase text-foreground-muted mb-2">Tablet (768px)</h4>
|
|
220
|
+
<div className="w-[768px] border border-dashed border-border p-4">
|
|
221
|
+
<Calendar
|
|
222
|
+
selected={tablet}
|
|
223
|
+
onSelect={setTablet}
|
|
224
|
+
className="w-fit"
|
|
225
|
+
/>
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
{/* Desktop */}
|
|
229
|
+
<div>
|
|
230
|
+
<h4 className="text-xs uppercase text-foreground-muted mb-2">Desktop (1280px)</h4>
|
|
231
|
+
<div className="w-[1280px] border border-dashed border-border p-4">
|
|
232
|
+
<div className="flex gap-8">
|
|
233
|
+
<Calendar
|
|
234
|
+
selected={desktop}
|
|
235
|
+
onSelect={setDesktop}
|
|
236
|
+
className="w-fit"
|
|
237
|
+
/>
|
|
238
|
+
<div className="text-sm text-foreground-muted">
|
|
239
|
+
{desktop && <p>Selected: {desktop.toLocaleDateString()}</p>}
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
</div>
|
|
244
|
+
</div>
|
|
245
|
+
);
|
|
246
|
+
},
|
|
247
|
+
};
|