sonance-brand-mcp 1.1.4 → 1.2.2

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 (73) hide show
  1. package/dist/assets/BRAND_GUIDELINES.md +0 -8
  2. package/dist/assets/components/accordion.stories.tsx +310 -0
  3. package/dist/assets/components/accordion.tsx +56 -30
  4. package/dist/assets/components/alert.stories.tsx +199 -0
  5. package/dist/assets/components/autocomplete.stories.tsx +307 -0
  6. package/dist/assets/components/autocomplete.tsx +28 -4
  7. package/dist/assets/components/avatar.stories.tsx +175 -0
  8. package/dist/assets/components/badge.stories.tsx +258 -0
  9. package/dist/assets/components/breadcrumbs.stories.tsx +175 -0
  10. package/dist/assets/components/button.stories.tsx +362 -0
  11. package/dist/assets/components/button.tsx +48 -3
  12. package/dist/assets/components/calendar.stories.tsx +247 -0
  13. package/dist/assets/components/card.stories.tsx +275 -0
  14. package/dist/assets/components/card.tsx +26 -1
  15. package/dist/assets/components/checkbox-group.stories.tsx +281 -0
  16. package/dist/assets/components/checkbox.stories.tsx +160 -0
  17. package/dist/assets/components/checkbox.tsx +32 -4
  18. package/dist/assets/components/code.stories.tsx +265 -0
  19. package/dist/assets/components/date-input.stories.tsx +278 -0
  20. package/dist/assets/components/date-input.tsx +24 -2
  21. package/dist/assets/components/date-picker.stories.tsx +337 -0
  22. package/dist/assets/components/date-picker.tsx +28 -4
  23. package/dist/assets/components/date-range-picker.stories.tsx +340 -0
  24. package/dist/assets/components/dialog.stories.tsx +285 -0
  25. package/dist/assets/components/divider.stories.tsx +176 -0
  26. package/dist/assets/components/drawer.stories.tsx +216 -0
  27. package/dist/assets/components/dropdown.stories.tsx +342 -0
  28. package/dist/assets/components/dropdown.tsx +55 -10
  29. package/dist/assets/components/form.stories.tsx +372 -0
  30. package/dist/assets/components/image.stories.tsx +348 -0
  31. package/dist/assets/components/input-otp.stories.tsx +336 -0
  32. package/dist/assets/components/input-otp.tsx +24 -2
  33. package/dist/assets/components/input.stories.tsx +223 -0
  34. package/dist/assets/components/input.tsx +27 -2
  35. package/dist/assets/components/kbd.stories.tsx +272 -0
  36. package/dist/assets/components/link.stories.tsx +199 -0
  37. package/dist/assets/components/link.tsx +50 -1
  38. package/dist/assets/components/listbox.stories.tsx +287 -0
  39. package/dist/assets/components/listbox.tsx +30 -7
  40. package/dist/assets/components/navbar.stories.tsx +218 -0
  41. package/dist/assets/components/number-input.stories.tsx +295 -0
  42. package/dist/assets/components/number-input.tsx +30 -8
  43. package/dist/assets/components/pagination.stories.tsx +280 -0
  44. package/dist/assets/components/pagination.tsx +45 -21
  45. package/dist/assets/components/popover.stories.tsx +219 -0
  46. package/dist/assets/components/progress.stories.tsx +153 -0
  47. package/dist/assets/components/radio-group.stories.tsx +187 -0
  48. package/dist/assets/components/radio-group.tsx +30 -6
  49. package/dist/assets/components/range-calendar.stories.tsx +334 -0
  50. package/dist/assets/components/scroll-shadow.stories.tsx +335 -0
  51. package/dist/assets/components/select.stories.tsx +192 -0
  52. package/dist/assets/components/select.tsx +54 -7
  53. package/dist/assets/components/skeleton.stories.tsx +166 -0
  54. package/dist/assets/components/slider.stories.tsx +145 -0
  55. package/dist/assets/components/slider.tsx +43 -8
  56. package/dist/assets/components/spacer.stories.tsx +216 -0
  57. package/dist/assets/components/spinner.stories.tsx +149 -0
  58. package/dist/assets/components/switch.stories.tsx +170 -0
  59. package/dist/assets/components/switch.tsx +29 -4
  60. package/dist/assets/components/table.stories.tsx +322 -0
  61. package/dist/assets/components/tabs.stories.tsx +306 -0
  62. package/dist/assets/components/tabs.tsx +25 -4
  63. package/dist/assets/components/textarea.stories.tsx +103 -0
  64. package/dist/assets/components/textarea.tsx +27 -3
  65. package/dist/assets/components/theme-toggle.stories.tsx +248 -0
  66. package/dist/assets/components/time-input.stories.tsx +365 -0
  67. package/dist/assets/components/time-input.tsx +25 -3
  68. package/dist/assets/components/toast.stories.tsx +195 -0
  69. package/dist/assets/components/tooltip.stories.tsx +226 -0
  70. package/dist/assets/components/user.stories.tsx +274 -0
  71. package/dist/assets/logo-manifest.json +0 -18
  72. package/dist/index.js +2142 -85
  73. package/package.json +1 -1
@@ -0,0 +1,248 @@
1
+ import type { Meta, StoryObj } from '@storybook/nextjs-vite';
2
+ import { ThemeToggle, SimpleThemeToggle } from './theme-toggle';
3
+
4
+ const meta: Meta<typeof ThemeToggle> = {
5
+ title: 'Components/Utilities/ThemeToggle',
6
+ component: ThemeToggle,
7
+ tags: ['autodocs'],
8
+ parameters: {
9
+ docs: {
10
+ description: {
11
+ component: 'Theme toggle components for switching between light, dark, and system color modes. Requires next-themes ThemeProvider to be set up.',
12
+ },
13
+ },
14
+ },
15
+ };
16
+
17
+ export default meta;
18
+ type Story = StoryObj<typeof meta>;
19
+
20
+ // Default Three-Way Toggle
21
+ export const Default: Story = {
22
+ render: () => (
23
+ <div className="space-y-4">
24
+ <p className="text-sm text-foreground-muted">
25
+ Three-way toggle: Light, Dark, and System
26
+ </p>
27
+ <ThemeToggle />
28
+ </div>
29
+ ),
30
+ };
31
+
32
+ // Simple Two-Way Toggle
33
+ export const Simple: Story = {
34
+ render: () => (
35
+ <div className="space-y-4">
36
+ <p className="text-sm text-foreground-muted">
37
+ Simple toggle: switches between Light and Dark
38
+ </p>
39
+ <SimpleThemeToggle />
40
+ </div>
41
+ ),
42
+ };
43
+
44
+ // In Navbar Context
45
+ export const InNavbar: Story = {
46
+ render: () => (
47
+ <div className="flex items-center justify-between p-4 border border-border rounded-sm bg-card">
48
+ <div className="flex items-center gap-2">
49
+ <div className="w-8 h-8 bg-primary rounded-sm" />
50
+ <span className="font-medium">Sonance</span>
51
+ </div>
52
+ <div className="flex items-center gap-4">
53
+ <nav className="flex gap-4 text-sm">
54
+ <a href="#" className="hover:text-primary">Products</a>
55
+ <a href="#" className="hover:text-primary">About</a>
56
+ <a href="#" className="hover:text-primary">Contact</a>
57
+ </nav>
58
+ <SimpleThemeToggle />
59
+ </div>
60
+ </div>
61
+ ),
62
+ };
63
+
64
+ // In Settings Panel
65
+ export const InSettingsPanel: Story = {
66
+ render: () => (
67
+ <div className="w-80 p-6 border border-border rounded-sm bg-card space-y-6">
68
+ <h3 className="font-medium">Appearance</h3>
69
+ <div className="space-y-4">
70
+ <div className="space-y-2">
71
+ <label className="text-sm text-foreground-muted">Theme</label>
72
+ <ThemeToggle />
73
+ </div>
74
+ <p className="text-xs text-foreground-muted">
75
+ Choose your preferred color scheme. System will match your device settings.
76
+ </p>
77
+ </div>
78
+ </div>
79
+ ),
80
+ };
81
+
82
+ // With Labels
83
+ export const WithLabels: Story = {
84
+ render: () => (
85
+ <div className="space-y-6">
86
+ <div className="flex items-center justify-between w-64">
87
+ <span className="text-sm">Dark Mode</span>
88
+ <SimpleThemeToggle />
89
+ </div>
90
+ <div className="space-y-2">
91
+ <span className="text-sm">Color Theme</span>
92
+ <ThemeToggle />
93
+ </div>
94
+ </div>
95
+ ),
96
+ };
97
+
98
+ // Size Comparison
99
+ export const SizeComparison: Story = {
100
+ render: () => (
101
+ <div className="space-y-8">
102
+ <div className="space-y-2">
103
+ <p className="text-sm font-medium">Full Toggle (3 options)</p>
104
+ <ThemeToggle />
105
+ </div>
106
+ <div className="space-y-2">
107
+ <p className="text-sm font-medium">Simple Toggle (2 options)</p>
108
+ <SimpleThemeToggle />
109
+ </div>
110
+ </div>
111
+ ),
112
+ };
113
+
114
+ // Dark Background Preview
115
+ export const OnDarkBackground: Story = {
116
+ render: () => (
117
+ <div className="space-y-4">
118
+ <div className="p-6 bg-sonance-charcoal rounded-sm">
119
+ <div className="flex items-center justify-between">
120
+ <span className="text-white text-sm">On dark background</span>
121
+ <SimpleThemeToggle className="[&_button]:text-white [&_button:hover]:bg-white/10" />
122
+ </div>
123
+ </div>
124
+ <div className="p-6 bg-card border border-border rounded-sm">
125
+ <div className="flex items-center justify-between">
126
+ <span className="text-sm">On light background</span>
127
+ <SimpleThemeToggle />
128
+ </div>
129
+ </div>
130
+ </div>
131
+ ),
132
+ };
133
+
134
+ // Footer Example
135
+ export const InFooter: Story = {
136
+ render: () => (
137
+ <div className="p-6 border-t border-border bg-background-secondary">
138
+ <div className="flex items-center justify-between">
139
+ <p className="text-sm text-foreground-muted">
140
+ &copy; 2024 Sonance. All rights reserved.
141
+ </p>
142
+ <div className="flex items-center gap-4">
143
+ <a href="#" className="text-sm text-foreground-muted hover:text-foreground">Privacy</a>
144
+ <a href="#" className="text-sm text-foreground-muted hover:text-foreground">Terms</a>
145
+ <div className="w-px h-4 bg-border" />
146
+ <SimpleThemeToggle />
147
+ </div>
148
+ </div>
149
+ </div>
150
+ ),
151
+ };
152
+
153
+ // Mobile Menu Example
154
+ export const InMobileMenu: Story = {
155
+ render: () => (
156
+ <div className="w-72 p-4 border border-border rounded-sm bg-card space-y-4">
157
+ <div className="flex items-center justify-between pb-4 border-b border-border">
158
+ <span className="font-medium">Menu</span>
159
+ <button className="p-1 hover:bg-secondary-hover rounded-sm">
160
+ <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
161
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
162
+ </svg>
163
+ </button>
164
+ </div>
165
+ <nav className="space-y-1">
166
+ {['Home', 'Products', 'About', 'Contact'].map((item) => (
167
+ <a
168
+ key={item}
169
+ href="#"
170
+ className="block px-3 py-2 rounded-sm hover:bg-secondary-hover"
171
+ >
172
+ {item}
173
+ </a>
174
+ ))}
175
+ </nav>
176
+ <div className="pt-4 border-t border-border">
177
+ <div className="flex items-center justify-between px-3">
178
+ <span className="text-sm text-foreground-muted">Theme</span>
179
+ <ThemeToggle />
180
+ </div>
181
+ </div>
182
+ </div>
183
+ ),
184
+ };
185
+
186
+ // All Variants
187
+ export const AllVariants: Story = {
188
+ render: () => (
189
+ <div className="space-y-8">
190
+ <div>
191
+ <h4 className="text-sm font-medium mb-3">ThemeToggle (Light / Dark / System)</h4>
192
+ <ThemeToggle />
193
+ </div>
194
+ <div>
195
+ <h4 className="text-sm font-medium mb-3">SimpleThemeToggle (Light / Dark)</h4>
196
+ <SimpleThemeToggle />
197
+ </div>
198
+ </div>
199
+ ),
200
+ };
201
+
202
+ // Responsive Matrix - Mobile, Tablet, Desktop
203
+ export const ResponsiveMatrix: Story = {
204
+ render: () => (
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
+ <div className="flex items-center justify-between">
211
+ <span className="text-sm">Dark Mode</span>
212
+ <SimpleThemeToggle />
213
+ </div>
214
+ </div>
215
+ </div>
216
+ {/* Tablet */}
217
+ <div>
218
+ <h4 className="text-xs uppercase text-foreground-muted mb-2">Tablet (768px)</h4>
219
+ <div className="w-[768px] border border-dashed border-border p-4">
220
+ <div className="flex items-center justify-between">
221
+ <span className="text-sm">Theme Preference</span>
222
+ <ThemeToggle />
223
+ </div>
224
+ </div>
225
+ </div>
226
+ {/* Desktop */}
227
+ <div>
228
+ <h4 className="text-xs uppercase text-foreground-muted mb-2">Desktop (1280px)</h4>
229
+ <div className="w-[1280px] border border-dashed border-border p-4">
230
+ <div className="flex items-center justify-between p-4 border border-border rounded-sm bg-card">
231
+ <div className="flex items-center gap-2">
232
+ <div className="w-8 h-8 bg-primary rounded-sm" />
233
+ <span className="font-medium">Sonance</span>
234
+ </div>
235
+ <div className="flex items-center gap-4">
236
+ <nav className="flex gap-4 text-sm">
237
+ <a href="#" className="hover:text-primary">Products</a>
238
+ <a href="#" className="hover:text-primary">About</a>
239
+ <a href="#" className="hover:text-primary">Contact</a>
240
+ </nav>
241
+ <SimpleThemeToggle />
242
+ </div>
243
+ </div>
244
+ </div>
245
+ </div>
246
+ </div>
247
+ ),
248
+ };
@@ -0,0 +1,365 @@
1
+ import type { Meta, StoryObj } from '@storybook/nextjs-vite';
2
+ import { useState } from 'react';
3
+ import { TimeInput, type TimeInputState } from './time-input';
4
+
5
+ interface TimeValue {
6
+ hours: number;
7
+ minutes: number;
8
+ }
9
+
10
+ const meta: Meta<typeof TimeInput> = {
11
+ title: 'Components/Forms/TimeInput',
12
+ component: TimeInput,
13
+ tags: ['autodocs'],
14
+ parameters: {
15
+ docs: {
16
+ description: {
17
+ component: 'A time input component supporting 12-hour and 24-hour formats. Includes AM/PM toggle for 12-hour mode.',
18
+ },
19
+ },
20
+ },
21
+ argTypes: {
22
+ label: {
23
+ control: 'text',
24
+ description: 'Input label',
25
+ },
26
+ use24Hour: {
27
+ control: 'boolean',
28
+ description: 'Use 24-hour format instead of 12-hour',
29
+ table: {
30
+ defaultValue: { summary: 'false' },
31
+ },
32
+ },
33
+ error: {
34
+ control: 'text',
35
+ description: 'Error message',
36
+ },
37
+ disabled: {
38
+ control: 'boolean',
39
+ description: 'Disabled state',
40
+ },
41
+ state: {
42
+ control: 'select',
43
+ options: ['default', 'hover', 'focus', 'error', 'disabled'],
44
+ description: 'Visual state for documentation',
45
+ table: {
46
+ defaultValue: { summary: 'default' },
47
+ },
48
+ },
49
+ },
50
+ };
51
+
52
+ export default meta;
53
+ type Story = StoryObj<typeof meta>;
54
+
55
+ // Default (12-hour)
56
+ export const Default: Story = {
57
+ render: () => {
58
+ const [time, setTime] = useState<TimeValue>({ hours: 12, minutes: 0 });
59
+ return (
60
+ <div className="w-48 space-y-4">
61
+ <TimeInput
62
+ value={time}
63
+ onValueChange={setTime}
64
+ />
65
+ <p className="text-sm text-foreground-muted">
66
+ Selected: {time.hours}:{time.minutes.toString().padStart(2, '0')}
67
+ </p>
68
+ </div>
69
+ );
70
+ },
71
+ };
72
+
73
+ // With Label
74
+ export const WithLabel: Story = {
75
+ render: () => {
76
+ const [time, setTime] = useState<TimeValue>({ hours: 9, minutes: 30 });
77
+ return (
78
+ <div className="w-48">
79
+ <TimeInput
80
+ label="Start Time"
81
+ value={time}
82
+ onValueChange={setTime}
83
+ />
84
+ </div>
85
+ );
86
+ },
87
+ };
88
+
89
+ // 24-Hour Format
90
+ export const Use24Hour: Story = {
91
+ render: () => {
92
+ const [time, setTime] = useState<TimeValue>({ hours: 14, minutes: 30 });
93
+ return (
94
+ <div className="w-40 space-y-4">
95
+ <TimeInput
96
+ label="Time (24h)"
97
+ use24Hour
98
+ value={time}
99
+ onValueChange={setTime}
100
+ />
101
+ <p className="text-sm text-foreground-muted">
102
+ 24-hour format: {time.hours}:{time.minutes.toString().padStart(2, '0')}
103
+ </p>
104
+ </div>
105
+ );
106
+ },
107
+ };
108
+
109
+ // With Error
110
+ export const WithError: Story = {
111
+ render: () => {
112
+ const [time, setTime] = useState<TimeValue>({ hours: 8, minutes: 0 });
113
+ return (
114
+ <div className="w-48">
115
+ <TimeInput
116
+ label="Appointment Time"
117
+ error="Please select a time after 9:00 AM"
118
+ value={time}
119
+ onValueChange={setTime}
120
+ />
121
+ </div>
122
+ );
123
+ },
124
+ };
125
+
126
+ // Disabled
127
+ export const Disabled: Story = {
128
+ render: () => {
129
+ return (
130
+ <div className="w-48">
131
+ <TimeInput
132
+ label="Locked Time"
133
+ defaultValue={{ hours: 10, minutes: 30 }}
134
+ disabled
135
+ />
136
+ </div>
137
+ );
138
+ },
139
+ };
140
+
141
+ // Morning Time
142
+ export const MorningTime: Story = {
143
+ render: () => {
144
+ const [time, setTime] = useState<TimeValue>({ hours: 8, minutes: 0 });
145
+ return (
146
+ <div className="w-48">
147
+ <TimeInput
148
+ label="Wake Up"
149
+ value={time}
150
+ onValueChange={setTime}
151
+ />
152
+ </div>
153
+ );
154
+ },
155
+ };
156
+
157
+ // Evening Time
158
+ export const EveningTime: Story = {
159
+ render: () => {
160
+ const [time, setTime] = useState<TimeValue>({ hours: 20, minutes: 30 });
161
+ return (
162
+ <div className="w-48">
163
+ <TimeInput
164
+ label="Dinner Reservation"
165
+ value={time}
166
+ onValueChange={setTime}
167
+ />
168
+ </div>
169
+ );
170
+ },
171
+ };
172
+
173
+ // Appointment Booking Example
174
+ export const AppointmentBookingExample: Story = {
175
+ render: () => {
176
+ const [startTime, setStartTime] = useState<TimeValue>({ hours: 9, minutes: 0 });
177
+ const [endTime, setEndTime] = useState<TimeValue>({ hours: 10, minutes: 0 });
178
+
179
+ const startMinutes = startTime.hours * 60 + startTime.minutes;
180
+ const endMinutes = endTime.hours * 60 + endTime.minutes;
181
+ const durationMinutes = endMinutes - startMinutes;
182
+
183
+ const hasError = durationMinutes <= 0;
184
+
185
+ return (
186
+ <div className="w-64 space-y-4 p-4 border border-border rounded-sm">
187
+ <h3 className="font-medium">Schedule Appointment</h3>
188
+ <TimeInput
189
+ label="Start Time"
190
+ value={startTime}
191
+ onValueChange={setStartTime}
192
+ />
193
+ <TimeInput
194
+ label="End Time"
195
+ value={endTime}
196
+ onValueChange={setEndTime}
197
+ error={hasError ? "End time must be after start time" : undefined}
198
+ />
199
+ {!hasError && (
200
+ <p className="text-sm text-foreground-muted">
201
+ Duration: {Math.floor(durationMinutes / 60)}h {durationMinutes % 60}m
202
+ </p>
203
+ )}
204
+ </div>
205
+ );
206
+ },
207
+ };
208
+
209
+ // Time Zone Example
210
+ export const TimeZoneExample: Story = {
211
+ render: () => {
212
+ const [time, setTime] = useState<TimeValue>({ hours: 15, minutes: 0 });
213
+
214
+ // Calculate times in different zones
215
+ const utcOffset = -5; // EST
216
+ const localHours = time.hours;
217
+ const utcHours = (localHours - utcOffset + 24) % 24;
218
+ const londonHours = (utcHours + 0) % 24; // GMT
219
+ const tokyoHours = (utcHours + 9) % 24; // JST
220
+
221
+ const formatTime = (h: number) => {
222
+ const period = h >= 12 ? 'PM' : 'AM';
223
+ const hour = h % 12 || 12;
224
+ return `${hour}:00 ${period}`;
225
+ };
226
+
227
+ return (
228
+ <div className="w-64 space-y-4">
229
+ <TimeInput
230
+ label="Your Local Time (EST)"
231
+ value={time}
232
+ onValueChange={setTime}
233
+ />
234
+ <div className="space-y-2 text-sm">
235
+ <div className="flex justify-between">
236
+ <span className="text-foreground-muted">UTC</span>
237
+ <span>{formatTime(utcHours)}</span>
238
+ </div>
239
+ <div className="flex justify-between">
240
+ <span className="text-foreground-muted">London (GMT)</span>
241
+ <span>{formatTime(londonHours)}</span>
242
+ </div>
243
+ <div className="flex justify-between">
244
+ <span className="text-foreground-muted">Tokyo (JST)</span>
245
+ <span>{formatTime(tokyoHours)}</span>
246
+ </div>
247
+ </div>
248
+ </div>
249
+ );
250
+ },
251
+ };
252
+
253
+ // Format Comparison
254
+ export const FormatComparison: Story = {
255
+ render: () => {
256
+ const [time, setTime] = useState<TimeValue>({ hours: 14, minutes: 30 });
257
+ return (
258
+ <div className="flex gap-8">
259
+ <div className="w-48">
260
+ <TimeInput
261
+ label="12-Hour Format"
262
+ value={time}
263
+ onValueChange={setTime}
264
+ />
265
+ </div>
266
+ <div className="w-40">
267
+ <TimeInput
268
+ label="24-Hour Format"
269
+ use24Hour
270
+ value={time}
271
+ onValueChange={setTime}
272
+ />
273
+ </div>
274
+ </div>
275
+ );
276
+ },
277
+ };
278
+
279
+ // All States
280
+ export const AllStates: Story = {
281
+ render: () => (
282
+ <div className="space-y-6">
283
+ <div className="flex gap-8">
284
+ <div className="w-48">
285
+ <TimeInput label="Default (12h)" />
286
+ </div>
287
+ <div className="w-40">
288
+ <TimeInput label="24-Hour" use24Hour />
289
+ </div>
290
+ </div>
291
+ <div className="flex gap-8">
292
+ <div className="w-48">
293
+ <TimeInput label="With Value" defaultValue={{ hours: 14, minutes: 30 }} />
294
+ </div>
295
+ <div className="w-48">
296
+ <TimeInput label="With Error" error="Invalid time" />
297
+ </div>
298
+ </div>
299
+ <div className="w-48">
300
+ <TimeInput label="Disabled" disabled defaultValue={{ hours: 9, minutes: 0 }} />
301
+ </div>
302
+ </div>
303
+ ),
304
+ };
305
+
306
+ // State Matrix - Visual documentation of all states
307
+ export const StateMatrix: Story = {
308
+ render: () => {
309
+ const states: TimeInputState[] = ['default', 'hover', 'focus', 'error', 'disabled'];
310
+ return (
311
+ <div className="space-y-6 w-48">
312
+ <h3 className="text-sm font-medium text-foreground-muted">TimeInput States</h3>
313
+ <div className="space-y-4">
314
+ {states.map((state) => (
315
+ <div key={state}>
316
+ <span className="text-xs font-medium text-foreground-muted uppercase">{state}</span>
317
+ <TimeInput
318
+ state={state}
319
+ defaultValue={{ hours: 9, minutes: 30 }}
320
+ error={state === 'error' ? 'Error message' : undefined}
321
+ />
322
+ </div>
323
+ ))}
324
+ </div>
325
+ </div>
326
+ );
327
+ },
328
+ };
329
+
330
+ // Responsive Matrix - Mobile, Tablet, Desktop
331
+ export const ResponsiveMatrix: Story = {
332
+ render: () => (
333
+ <div className="space-y-8">
334
+ {/* Mobile */}
335
+ <div>
336
+ <h4 className="text-xs uppercase text-foreground-muted mb-2">Mobile (375px)</h4>
337
+ <div className="w-[375px] border border-dashed border-border p-4">
338
+ <TimeInput label="Start Time" defaultValue={{ hours: 9, minutes: 0 }} />
339
+ </div>
340
+ </div>
341
+ {/* Tablet */}
342
+ <div>
343
+ <h4 className="text-xs uppercase text-foreground-muted mb-2">Tablet (768px)</h4>
344
+ <div className="w-[768px] border border-dashed border-border p-4">
345
+ <div className="grid grid-cols-2 gap-4">
346
+ <TimeInput label="Start Time" defaultValue={{ hours: 9, minutes: 0 }} />
347
+ <TimeInput label="End Time" defaultValue={{ hours: 17, minutes: 0 }} />
348
+ </div>
349
+ </div>
350
+ </div>
351
+ {/* Desktop */}
352
+ <div>
353
+ <h4 className="text-xs uppercase text-foreground-muted mb-2">Desktop (1280px)</h4>
354
+ <div className="w-[1280px] border border-dashed border-border p-4">
355
+ <div className="grid grid-cols-4 gap-4">
356
+ <TimeInput label="12-Hour" defaultValue={{ hours: 9, minutes: 30 }} />
357
+ <TimeInput label="24-Hour" use24Hour defaultValue={{ hours: 14, minutes: 30 }} />
358
+ <TimeInput label="With Error" error="Invalid time" />
359
+ <TimeInput label="Disabled" disabled defaultValue={{ hours: 10, minutes: 0 }} />
360
+ </div>
361
+ </div>
362
+ </div>
363
+ </div>
364
+ ),
365
+ };
@@ -4,6 +4,22 @@ import { forwardRef, useState, useEffect, useRef } from "react";
4
4
  import { Clock } from "lucide-react";
5
5
  import { cn } from "@/lib/utils";
6
6
 
7
+ export type TimeInputState = "default" | "hover" | "focus" | "error" | "disabled";
8
+
9
+ // State styles for Storybook/Figma visualization
10
+ const getStateStyles = (state?: TimeInputState) => {
11
+ if (!state || state === "default") return "";
12
+
13
+ const stateMap: Record<string, string> = {
14
+ hover: "border-border-hover",
15
+ focus: "border-input-focus",
16
+ error: "border-error",
17
+ disabled: "opacity-50 cursor-not-allowed",
18
+ };
19
+
20
+ return stateMap[state] || "";
21
+ };
22
+
7
23
  interface TimeValue {
8
24
  hours: number;
9
25
  minutes: number;
@@ -19,6 +35,8 @@ interface TimeInputProps {
19
35
  use24Hour?: boolean;
20
36
  disabled?: boolean;
21
37
  className?: string;
38
+ /** Visual state for Storybook/Figma documentation */
39
+ state?: TimeInputState;
22
40
  }
23
41
 
24
42
  export const TimeInput = forwardRef<HTMLInputElement, TimeInputProps>(
@@ -33,9 +51,12 @@ export const TimeInput = forwardRef<HTMLInputElement, TimeInputProps>(
33
51
  use24Hour = false,
34
52
  disabled = false,
35
53
  className,
54
+ state,
36
55
  },
37
56
  ref
38
57
  ) => {
58
+ const isDisabled = disabled || state === "disabled";
59
+ const hasError = error || state === "error";
39
60
  const [internalValue, setInternalValue] = useState<TimeValue>(defaultValue);
40
61
  const [inputValue, setInputValue] = useState(() => formatTime(defaultValue, use24Hour));
41
62
  const [period, setPeriod] = useState<"AM" | "PM">(defaultValue.hours >= 12 ? "PM" : "AM");
@@ -102,7 +123,7 @@ export const TimeInput = forwardRef<HTMLInputElement, TimeInputProps>(
102
123
  onChange={handleInputChange}
103
124
  onBlur={handleBlur}
104
125
  placeholder={placeholder}
105
- disabled={disabled}
126
+ disabled={isDisabled}
106
127
  className={cn(
107
128
  "w-full border border-input-border bg-input px-4 py-3 pr-10",
108
129
  "text-foreground placeholder:text-input-placeholder",
@@ -110,7 +131,8 @@ export const TimeInput = forwardRef<HTMLInputElement, TimeInputProps>(
110
131
  "focus:border-input-focus focus:outline-none",
111
132
  "disabled:cursor-not-allowed disabled:opacity-50",
112
133
  !use24Hour && "rounded-r-none border-r-0",
113
- error && "border-error"
134
+ hasError && "border-error",
135
+ getStateStyles(state)
114
136
  )}
115
137
  />
116
138
  <Clock className="absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-foreground-muted" />
@@ -119,7 +141,7 @@ export const TimeInput = forwardRef<HTMLInputElement, TimeInputProps>(
119
141
  <button
120
142
  type="button"
121
143
  onClick={handlePeriodChange}
122
- disabled={disabled}
144
+ disabled={isDisabled}
123
145
  className={cn(
124
146
  "px-3 border border-input-border bg-background-secondary",
125
147
  "text-sm font-medium text-foreground",