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,218 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/nextjs-vite';
|
|
2
|
+
import {
|
|
3
|
+
Navbar,
|
|
4
|
+
NavbarContent,
|
|
5
|
+
NavbarBrand,
|
|
6
|
+
NavbarItems,
|
|
7
|
+
NavbarItem,
|
|
8
|
+
ResponsiveNavbar,
|
|
9
|
+
} from './navbar';
|
|
10
|
+
import { Button } from './button';
|
|
11
|
+
|
|
12
|
+
const meta: Meta<typeof Navbar> = {
|
|
13
|
+
title: 'Components/Navigation/Navbar',
|
|
14
|
+
component: Navbar,
|
|
15
|
+
tags: ['autodocs'],
|
|
16
|
+
parameters: {
|
|
17
|
+
docs: {
|
|
18
|
+
description: {
|
|
19
|
+
component: 'A responsive navigation bar component with multiple variants and mobile support.',
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
layout: 'fullscreen',
|
|
23
|
+
},
|
|
24
|
+
argTypes: {
|
|
25
|
+
variant: {
|
|
26
|
+
control: 'select',
|
|
27
|
+
options: ['default', 'dark', 'transparent', 'blur'],
|
|
28
|
+
},
|
|
29
|
+
sticky: { control: 'boolean' },
|
|
30
|
+
bordered: { control: 'boolean' },
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export default meta;
|
|
35
|
+
type Story = StoryObj<typeof meta>;
|
|
36
|
+
|
|
37
|
+
export const Default: Story = {
|
|
38
|
+
render: () => (
|
|
39
|
+
<Navbar>
|
|
40
|
+
<NavbarContent>
|
|
41
|
+
<NavbarBrand>
|
|
42
|
+
<span className="text-xl font-semibold">Sonance</span>
|
|
43
|
+
</NavbarBrand>
|
|
44
|
+
<NavbarItems>
|
|
45
|
+
<NavbarItem href="#" active>Home</NavbarItem>
|
|
46
|
+
<NavbarItem href="#">Products</NavbarItem>
|
|
47
|
+
<NavbarItem href="#">About</NavbarItem>
|
|
48
|
+
<NavbarItem href="#">Contact</NavbarItem>
|
|
49
|
+
</NavbarItems>
|
|
50
|
+
<Button size="sm">Sign In</Button>
|
|
51
|
+
</NavbarContent>
|
|
52
|
+
</Navbar>
|
|
53
|
+
),
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const Dark: Story = {
|
|
57
|
+
render: () => (
|
|
58
|
+
<Navbar variant="dark">
|
|
59
|
+
<NavbarContent>
|
|
60
|
+
<NavbarBrand>
|
|
61
|
+
<span className="text-xl font-semibold">Sonance</span>
|
|
62
|
+
</NavbarBrand>
|
|
63
|
+
<NavbarItems>
|
|
64
|
+
<NavbarItem href="#" active>Home</NavbarItem>
|
|
65
|
+
<NavbarItem href="#">Products</NavbarItem>
|
|
66
|
+
<NavbarItem href="#">About</NavbarItem>
|
|
67
|
+
</NavbarItems>
|
|
68
|
+
<Button size="sm" variant="inverted">Sign In</Button>
|
|
69
|
+
</NavbarContent>
|
|
70
|
+
</Navbar>
|
|
71
|
+
),
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const Transparent: Story = {
|
|
75
|
+
render: () => (
|
|
76
|
+
<div className="relative h-64 bg-gradient-to-br from-sonance-charcoal to-sonance-gray-700">
|
|
77
|
+
<Navbar variant="transparent" className="text-white">
|
|
78
|
+
<NavbarContent>
|
|
79
|
+
<NavbarBrand>
|
|
80
|
+
<span className="text-xl font-semibold">Sonance</span>
|
|
81
|
+
</NavbarBrand>
|
|
82
|
+
<NavbarItems>
|
|
83
|
+
<NavbarItem href="#" active>Home</NavbarItem>
|
|
84
|
+
<NavbarItem href="#">Products</NavbarItem>
|
|
85
|
+
<NavbarItem href="#">About</NavbarItem>
|
|
86
|
+
</NavbarItems>
|
|
87
|
+
<Button size="sm" variant="inverted">Sign In</Button>
|
|
88
|
+
</NavbarContent>
|
|
89
|
+
</Navbar>
|
|
90
|
+
</div>
|
|
91
|
+
),
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export const Blur: Story = {
|
|
95
|
+
render: () => (
|
|
96
|
+
<div className="relative h-64 bg-gradient-to-br from-sonance-blue to-foundation-green">
|
|
97
|
+
<Navbar variant="blur">
|
|
98
|
+
<NavbarContent>
|
|
99
|
+
<NavbarBrand>
|
|
100
|
+
<span className="text-xl font-semibold">Sonance</span>
|
|
101
|
+
</NavbarBrand>
|
|
102
|
+
<NavbarItems>
|
|
103
|
+
<NavbarItem href="#" active>Home</NavbarItem>
|
|
104
|
+
<NavbarItem href="#">Products</NavbarItem>
|
|
105
|
+
<NavbarItem href="#">About</NavbarItem>
|
|
106
|
+
</NavbarItems>
|
|
107
|
+
<Button size="sm">Sign In</Button>
|
|
108
|
+
</NavbarContent>
|
|
109
|
+
</Navbar>
|
|
110
|
+
<div className="p-8 text-white">
|
|
111
|
+
<p>Content behind the blurred navbar</p>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
),
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export const Responsive: Story = {
|
|
118
|
+
render: () => (
|
|
119
|
+
<ResponsiveNavbar
|
|
120
|
+
brand={<span className="text-xl font-semibold">Sonance</span>}
|
|
121
|
+
items={[
|
|
122
|
+
{ label: 'Home', href: '#', active: true },
|
|
123
|
+
{ label: 'Products', href: '#' },
|
|
124
|
+
{ label: 'About', href: '#' },
|
|
125
|
+
{ label: 'Contact', href: '#' },
|
|
126
|
+
]}
|
|
127
|
+
actions={<Button size="sm">Sign In</Button>}
|
|
128
|
+
/>
|
|
129
|
+
),
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export const WithLogo: Story = {
|
|
133
|
+
render: () => (
|
|
134
|
+
<Navbar>
|
|
135
|
+
<NavbarContent>
|
|
136
|
+
<NavbarBrand>
|
|
137
|
+
<div className="w-8 h-8 bg-primary rounded" />
|
|
138
|
+
<span className="text-xl font-semibold">Sonance</span>
|
|
139
|
+
</NavbarBrand>
|
|
140
|
+
<NavbarItems>
|
|
141
|
+
<NavbarItem href="#" active>Home</NavbarItem>
|
|
142
|
+
<NavbarItem href="#">Products</NavbarItem>
|
|
143
|
+
<NavbarItem href="#">Support</NavbarItem>
|
|
144
|
+
</NavbarItems>
|
|
145
|
+
<div className="flex items-center gap-2">
|
|
146
|
+
<Button size="sm" variant="ghost">Log In</Button>
|
|
147
|
+
<Button size="sm">Sign Up</Button>
|
|
148
|
+
</div>
|
|
149
|
+
</NavbarContent>
|
|
150
|
+
</Navbar>
|
|
151
|
+
),
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// Responsive Matrix - Mobile, Tablet, Desktop
|
|
155
|
+
export const ResponsiveMatrix: Story = {
|
|
156
|
+
render: () => (
|
|
157
|
+
<div className="space-y-8">
|
|
158
|
+
{/* Mobile */}
|
|
159
|
+
<div>
|
|
160
|
+
<h4 className="text-xs uppercase text-foreground-muted mb-2">Mobile (375px)</h4>
|
|
161
|
+
<div className="w-[375px] border border-dashed border-border overflow-hidden">
|
|
162
|
+
<ResponsiveNavbar
|
|
163
|
+
brand={<span className="text-lg font-semibold">Sonance</span>}
|
|
164
|
+
items={[
|
|
165
|
+
{ label: 'Home', href: '#', active: true },
|
|
166
|
+
{ label: 'Products', href: '#' },
|
|
167
|
+
{ label: 'About', href: '#' },
|
|
168
|
+
]}
|
|
169
|
+
actions={<Button size="sm">Sign In</Button>}
|
|
170
|
+
/>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
{/* Tablet */}
|
|
174
|
+
<div>
|
|
175
|
+
<h4 className="text-xs uppercase text-foreground-muted mb-2">Tablet (768px)</h4>
|
|
176
|
+
<div className="w-[768px] border border-dashed border-border overflow-hidden">
|
|
177
|
+
<Navbar>
|
|
178
|
+
<NavbarContent>
|
|
179
|
+
<NavbarBrand>
|
|
180
|
+
<span className="text-xl font-semibold">Sonance</span>
|
|
181
|
+
</NavbarBrand>
|
|
182
|
+
<NavbarItems>
|
|
183
|
+
<NavbarItem href="#" active>Home</NavbarItem>
|
|
184
|
+
<NavbarItem href="#">Products</NavbarItem>
|
|
185
|
+
<NavbarItem href="#">About</NavbarItem>
|
|
186
|
+
</NavbarItems>
|
|
187
|
+
<Button size="sm">Sign In</Button>
|
|
188
|
+
</NavbarContent>
|
|
189
|
+
</Navbar>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
{/* Desktop */}
|
|
193
|
+
<div>
|
|
194
|
+
<h4 className="text-xs uppercase text-foreground-muted mb-2">Desktop (1280px)</h4>
|
|
195
|
+
<div className="w-[1280px] border border-dashed border-border overflow-hidden">
|
|
196
|
+
<Navbar>
|
|
197
|
+
<NavbarContent>
|
|
198
|
+
<NavbarBrand>
|
|
199
|
+
<span className="text-xl font-semibold">Sonance</span>
|
|
200
|
+
</NavbarBrand>
|
|
201
|
+
<NavbarItems>
|
|
202
|
+
<NavbarItem href="#" active>Home</NavbarItem>
|
|
203
|
+
<NavbarItem href="#">Products</NavbarItem>
|
|
204
|
+
<NavbarItem href="#">About</NavbarItem>
|
|
205
|
+
<NavbarItem href="#">Support</NavbarItem>
|
|
206
|
+
<NavbarItem href="#">Contact</NavbarItem>
|
|
207
|
+
</NavbarItems>
|
|
208
|
+
<div className="flex items-center gap-2">
|
|
209
|
+
<Button size="sm" variant="ghost">Log In</Button>
|
|
210
|
+
<Button size="sm">Sign Up</Button>
|
|
211
|
+
</div>
|
|
212
|
+
</NavbarContent>
|
|
213
|
+
</Navbar>
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
),
|
|
218
|
+
};
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/nextjs-vite';
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { NumberInput, Stepper, type NumberInputState } from './number-input';
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof NumberInput> = {
|
|
6
|
+
title: 'Components/Forms/NumberInput',
|
|
7
|
+
component: NumberInput,
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
parameters: {
|
|
10
|
+
docs: {
|
|
11
|
+
description: {
|
|
12
|
+
component: 'A number input component with increment/decrement controls.',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
argTypes: {
|
|
17
|
+
state: {
|
|
18
|
+
control: 'select',
|
|
19
|
+
options: ['default', 'hover', 'focus', 'error', 'disabled'],
|
|
20
|
+
description: 'Visual state for documentation',
|
|
21
|
+
table: {
|
|
22
|
+
defaultValue: { summary: 'default' },
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export default meta;
|
|
29
|
+
type Story = StoryObj<typeof meta>;
|
|
30
|
+
|
|
31
|
+
export const Default: Story = {
|
|
32
|
+
render: () => (
|
|
33
|
+
<div className="w-48">
|
|
34
|
+
<NumberInput defaultValue={5} />
|
|
35
|
+
</div>
|
|
36
|
+
),
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const WithLabel: Story = {
|
|
40
|
+
render: () => (
|
|
41
|
+
<div className="w-48">
|
|
42
|
+
<NumberInput label="Quantity" defaultValue={1} />
|
|
43
|
+
</div>
|
|
44
|
+
),
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const WithMinMax: Story = {
|
|
48
|
+
render: () => (
|
|
49
|
+
<div className="w-48">
|
|
50
|
+
<NumberInput
|
|
51
|
+
label="Volume Level"
|
|
52
|
+
defaultValue={50}
|
|
53
|
+
min={0}
|
|
54
|
+
max={100}
|
|
55
|
+
/>
|
|
56
|
+
</div>
|
|
57
|
+
),
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const CustomStep: Story = {
|
|
61
|
+
render: () => (
|
|
62
|
+
<div className="w-48">
|
|
63
|
+
<NumberInput
|
|
64
|
+
label="Price (in cents)"
|
|
65
|
+
defaultValue={500}
|
|
66
|
+
step={25}
|
|
67
|
+
min={0}
|
|
68
|
+
/>
|
|
69
|
+
</div>
|
|
70
|
+
),
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const DecimalStep: Story = {
|
|
74
|
+
render: () => (
|
|
75
|
+
<div className="w-48">
|
|
76
|
+
<NumberInput
|
|
77
|
+
label="Temperature"
|
|
78
|
+
defaultValue={72.5}
|
|
79
|
+
step={0.5}
|
|
80
|
+
min={60}
|
|
81
|
+
max={85}
|
|
82
|
+
/>
|
|
83
|
+
</div>
|
|
84
|
+
),
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export const WithError: Story = {
|
|
88
|
+
render: () => (
|
|
89
|
+
<div className="w-48">
|
|
90
|
+
<NumberInput
|
|
91
|
+
label="Quantity"
|
|
92
|
+
defaultValue={0}
|
|
93
|
+
min={1}
|
|
94
|
+
error="Minimum quantity is 1"
|
|
95
|
+
/>
|
|
96
|
+
</div>
|
|
97
|
+
),
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export const Disabled: Story = {
|
|
101
|
+
render: () => (
|
|
102
|
+
<div className="w-48">
|
|
103
|
+
<NumberInput
|
|
104
|
+
label="Locked Value"
|
|
105
|
+
defaultValue={42}
|
|
106
|
+
disabled
|
|
107
|
+
/>
|
|
108
|
+
</div>
|
|
109
|
+
),
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export const HiddenControls: Story = {
|
|
113
|
+
render: () => (
|
|
114
|
+
<div className="w-48">
|
|
115
|
+
<NumberInput
|
|
116
|
+
label="Direct Entry"
|
|
117
|
+
defaultValue={100}
|
|
118
|
+
hideControls
|
|
119
|
+
/>
|
|
120
|
+
</div>
|
|
121
|
+
),
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export const CurrencyFormat: Story = {
|
|
125
|
+
render: () => (
|
|
126
|
+
<div className="w-48">
|
|
127
|
+
<NumberInput
|
|
128
|
+
label="Price"
|
|
129
|
+
defaultValue={1299}
|
|
130
|
+
formatOptions={{
|
|
131
|
+
style: 'currency',
|
|
132
|
+
currency: 'USD',
|
|
133
|
+
}}
|
|
134
|
+
/>
|
|
135
|
+
</div>
|
|
136
|
+
),
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
export const PercentageFormat: Story = {
|
|
140
|
+
render: () => (
|
|
141
|
+
<div className="w-48">
|
|
142
|
+
<NumberInput
|
|
143
|
+
label="Discount"
|
|
144
|
+
defaultValue={15}
|
|
145
|
+
min={0}
|
|
146
|
+
max={100}
|
|
147
|
+
formatOptions={{
|
|
148
|
+
style: 'percent',
|
|
149
|
+
maximumFractionDigits: 0,
|
|
150
|
+
}}
|
|
151
|
+
/>
|
|
152
|
+
</div>
|
|
153
|
+
),
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
export const Controlled: Story = {
|
|
157
|
+
render: () => {
|
|
158
|
+
const [value, setValue] = useState(10);
|
|
159
|
+
return (
|
|
160
|
+
<div className="w-48 space-y-4">
|
|
161
|
+
<NumberInput
|
|
162
|
+
label="Controlled Input"
|
|
163
|
+
value={value}
|
|
164
|
+
onValueChange={setValue}
|
|
165
|
+
min={0}
|
|
166
|
+
max={20}
|
|
167
|
+
/>
|
|
168
|
+
<p className="text-sm text-foreground-muted">
|
|
169
|
+
Current value: {value}
|
|
170
|
+
</p>
|
|
171
|
+
<div className="flex gap-2">
|
|
172
|
+
<button
|
|
173
|
+
onClick={() => setValue(0)}
|
|
174
|
+
className="text-sm text-primary hover:text-primary-hover"
|
|
175
|
+
>
|
|
176
|
+
Reset to 0
|
|
177
|
+
</button>
|
|
178
|
+
<button
|
|
179
|
+
onClick={() => setValue(20)}
|
|
180
|
+
className="text-sm text-primary hover:text-primary-hover"
|
|
181
|
+
>
|
|
182
|
+
Set to Max
|
|
183
|
+
</button>
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
);
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
export const StepperVariant: Story = {
|
|
191
|
+
render: () => (
|
|
192
|
+
<div className="space-y-4">
|
|
193
|
+
<div className="w-48">
|
|
194
|
+
<Stepper label="Small" size="sm" defaultValue={1} />
|
|
195
|
+
</div>
|
|
196
|
+
<div className="w-48">
|
|
197
|
+
<Stepper label="Medium" size="md" defaultValue={1} />
|
|
198
|
+
</div>
|
|
199
|
+
<div className="w-48">
|
|
200
|
+
<Stepper label="Large" size="lg" defaultValue={1} />
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
),
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
export const QuantitySelector: Story = {
|
|
207
|
+
render: () => {
|
|
208
|
+
const [quantity, setQuantity] = useState(1);
|
|
209
|
+
const pricePerUnit = 1299;
|
|
210
|
+
const total = quantity * pricePerUnit;
|
|
211
|
+
|
|
212
|
+
return (
|
|
213
|
+
<div className="p-4 border border-border w-80">
|
|
214
|
+
<h3 className="font-medium text-foreground">VP66 TL In-Wall Speaker</h3>
|
|
215
|
+
<p className="text-sm text-foreground-muted mt-1">${pricePerUnit.toFixed(2)} each</p>
|
|
216
|
+
<div className="mt-4">
|
|
217
|
+
<NumberInput
|
|
218
|
+
label="Quantity"
|
|
219
|
+
value={quantity}
|
|
220
|
+
onValueChange={setQuantity}
|
|
221
|
+
min={1}
|
|
222
|
+
max={10}
|
|
223
|
+
/>
|
|
224
|
+
</div>
|
|
225
|
+
<div className="mt-4 pt-4 border-t border-border flex justify-between items-center">
|
|
226
|
+
<span className="text-sm text-foreground-muted">Total:</span>
|
|
227
|
+
<span className="text-lg font-medium text-foreground">
|
|
228
|
+
${total.toFixed(2)}
|
|
229
|
+
</span>
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
);
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
// State Matrix - Visual documentation of all states
|
|
237
|
+
export const StateMatrix: Story = {
|
|
238
|
+
render: () => {
|
|
239
|
+
const states: NumberInputState[] = ['default', 'hover', 'focus', 'error', 'disabled'];
|
|
240
|
+
return (
|
|
241
|
+
<div className="space-y-6 w-48">
|
|
242
|
+
<h3 className="text-sm font-medium text-foreground-muted">NumberInput States</h3>
|
|
243
|
+
<div className="space-y-4">
|
|
244
|
+
{states.map((state) => (
|
|
245
|
+
<div key={state}>
|
|
246
|
+
<span className="text-xs font-medium text-foreground-muted uppercase">{state}</span>
|
|
247
|
+
<NumberInput
|
|
248
|
+
state={state}
|
|
249
|
+
defaultValue={5}
|
|
250
|
+
error={state === 'error' ? 'Error message' : undefined}
|
|
251
|
+
/>
|
|
252
|
+
</div>
|
|
253
|
+
))}
|
|
254
|
+
</div>
|
|
255
|
+
</div>
|
|
256
|
+
);
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
// Responsive Matrix - Mobile, Tablet, Desktop
|
|
261
|
+
export const ResponsiveMatrix: Story = {
|
|
262
|
+
render: () => (
|
|
263
|
+
<div className="space-y-8">
|
|
264
|
+
{/* Mobile */}
|
|
265
|
+
<div>
|
|
266
|
+
<h4 className="text-xs uppercase text-foreground-muted mb-2">Mobile (375px)</h4>
|
|
267
|
+
<div className="w-[375px] border border-dashed border-border p-4">
|
|
268
|
+
<NumberInput label="Quantity" defaultValue={1} min={1} max={10} />
|
|
269
|
+
</div>
|
|
270
|
+
</div>
|
|
271
|
+
{/* Tablet */}
|
|
272
|
+
<div>
|
|
273
|
+
<h4 className="text-xs uppercase text-foreground-muted mb-2">Tablet (768px)</h4>
|
|
274
|
+
<div className="w-[768px] border border-dashed border-border p-4">
|
|
275
|
+
<div className="grid grid-cols-2 gap-4">
|
|
276
|
+
<NumberInput label="Quantity" defaultValue={5} />
|
|
277
|
+
<NumberInput label="Volume" defaultValue={50} min={0} max={100} />
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
{/* Desktop */}
|
|
282
|
+
<div>
|
|
283
|
+
<h4 className="text-xs uppercase text-foreground-muted mb-2">Desktop (1280px)</h4>
|
|
284
|
+
<div className="w-[1280px] border border-dashed border-border p-4">
|
|
285
|
+
<div className="grid grid-cols-4 gap-4">
|
|
286
|
+
<NumberInput label="Default" defaultValue={5} />
|
|
287
|
+
<NumberInput label="With Min/Max" defaultValue={50} min={0} max={100} />
|
|
288
|
+
<NumberInput label="With Error" defaultValue={0} min={1} error="Min 1" />
|
|
289
|
+
<NumberInput label="Disabled" defaultValue={42} disabled />
|
|
290
|
+
</div>
|
|
291
|
+
</div>
|
|
292
|
+
</div>
|
|
293
|
+
</div>
|
|
294
|
+
),
|
|
295
|
+
};
|
|
@@ -4,6 +4,22 @@ import { forwardRef, useState, useCallback } from "react";
|
|
|
4
4
|
import { Plus, Minus } from "lucide-react";
|
|
5
5
|
import { cn } from "@/lib/utils";
|
|
6
6
|
|
|
7
|
+
export type NumberInputState = "default" | "hover" | "focus" | "error" | "disabled";
|
|
8
|
+
|
|
9
|
+
// State styles for Storybook/Figma visualization
|
|
10
|
+
const getStateStyles = (state?: NumberInputState) => {
|
|
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 NumberInputProps {
|
|
8
24
|
value?: number;
|
|
9
25
|
defaultValue?: number;
|
|
@@ -17,6 +33,8 @@ interface NumberInputProps {
|
|
|
17
33
|
hideControls?: boolean;
|
|
18
34
|
formatOptions?: Intl.NumberFormatOptions;
|
|
19
35
|
className?: string;
|
|
36
|
+
/** Visual state for Storybook/Figma documentation */
|
|
37
|
+
state?: NumberInputState;
|
|
20
38
|
}
|
|
21
39
|
|
|
22
40
|
export const NumberInput = forwardRef<HTMLInputElement, NumberInputProps>(
|
|
@@ -34,9 +52,12 @@ export const NumberInput = forwardRef<HTMLInputElement, NumberInputProps>(
|
|
|
34
52
|
hideControls = false,
|
|
35
53
|
formatOptions,
|
|
36
54
|
className,
|
|
55
|
+
state,
|
|
37
56
|
},
|
|
38
57
|
ref
|
|
39
58
|
) => {
|
|
59
|
+
const isDisabled = disabled || state === "disabled";
|
|
60
|
+
const hasError = error || state === "error";
|
|
40
61
|
const [internalValue, setInternalValue] = useState(defaultValue);
|
|
41
62
|
const [inputValue, setInputValue] = useState(String(defaultValue));
|
|
42
63
|
|
|
@@ -58,13 +79,13 @@ export const NumberInput = forwardRef<HTMLInputElement, NumberInputProps>(
|
|
|
58
79
|
);
|
|
59
80
|
|
|
60
81
|
const increment = () => {
|
|
61
|
-
if (!
|
|
82
|
+
if (!isDisabled) {
|
|
62
83
|
updateValue(value + step);
|
|
63
84
|
}
|
|
64
85
|
};
|
|
65
86
|
|
|
66
87
|
const decrement = () => {
|
|
67
|
-
if (!
|
|
88
|
+
if (!isDisabled) {
|
|
68
89
|
updateValue(value - step);
|
|
69
90
|
}
|
|
70
91
|
};
|
|
@@ -90,7 +111,7 @@ export const NumberInput = forwardRef<HTMLInputElement, NumberInputProps>(
|
|
|
90
111
|
};
|
|
91
112
|
|
|
92
113
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
93
|
-
if (
|
|
114
|
+
if (isDisabled) return;
|
|
94
115
|
|
|
95
116
|
switch (e.key) {
|
|
96
117
|
case "ArrowUp":
|
|
@@ -124,7 +145,7 @@ export const NumberInput = forwardRef<HTMLInputElement, NumberInputProps>(
|
|
|
124
145
|
<button
|
|
125
146
|
type="button"
|
|
126
147
|
onClick={decrement}
|
|
127
|
-
disabled={
|
|
148
|
+
disabled={isDisabled || !canDecrement}
|
|
128
149
|
className={cn(
|
|
129
150
|
"flex items-center justify-center w-10",
|
|
130
151
|
"border border-r-0 border-input-border bg-background-secondary",
|
|
@@ -145,22 +166,23 @@ export const NumberInput = forwardRef<HTMLInputElement, NumberInputProps>(
|
|
|
145
166
|
onChange={handleInputChange}
|
|
146
167
|
onBlur={handleBlur}
|
|
147
168
|
onKeyDown={handleKeyDown}
|
|
148
|
-
disabled={
|
|
169
|
+
disabled={isDisabled}
|
|
149
170
|
className={cn(
|
|
150
171
|
"flex-1 min-w-0 border border-input-border bg-input px-4 py-3",
|
|
151
172
|
"text-center text-foreground",
|
|
152
173
|
"transition-colors duration-200",
|
|
153
174
|
"focus:border-input-focus focus:outline-none focus:z-10",
|
|
154
175
|
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
155
|
-
|
|
156
|
-
hideControls ? "" : "border-x-0"
|
|
176
|
+
hasError && "border-error",
|
|
177
|
+
hideControls ? "" : "border-x-0",
|
|
178
|
+
getStateStyles(state)
|
|
157
179
|
)}
|
|
158
180
|
/>
|
|
159
181
|
{!hideControls && (
|
|
160
182
|
<button
|
|
161
183
|
type="button"
|
|
162
184
|
onClick={increment}
|
|
163
|
-
disabled={
|
|
185
|
+
disabled={isDisabled || !canIncrement}
|
|
164
186
|
className={cn(
|
|
165
187
|
"flex items-center justify-center w-10",
|
|
166
188
|
"border border-l-0 border-input-border bg-background-secondary",
|