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.
- package/dist/assets/BRAND_GUIDELINES.md +0 -8
- 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/assets/logo-manifest.json +0 -18
- package/dist/index.js +2142 -85
- 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
|
+
© 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={
|
|
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
|
-
|
|
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={
|
|
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",
|