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,342 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/nextjs-vite';
|
|
2
|
+
import { ComponentProps } from 'react';
|
|
3
|
+
import { Settings, User, LogOut, Edit, Copy, Trash2 } from 'lucide-react';
|
|
4
|
+
import {
|
|
5
|
+
Dropdown,
|
|
6
|
+
DropdownTrigger,
|
|
7
|
+
DropdownMenu,
|
|
8
|
+
DropdownItem,
|
|
9
|
+
DropdownSeparator,
|
|
10
|
+
DropdownLabel,
|
|
11
|
+
type DropdownTriggerState,
|
|
12
|
+
type DropdownItemState,
|
|
13
|
+
} from './dropdown';
|
|
14
|
+
import { Button } from './button';
|
|
15
|
+
|
|
16
|
+
type DropdownStoryProps = ComponentProps<typeof Dropdown> & {
|
|
17
|
+
triggerState?: DropdownTriggerState;
|
|
18
|
+
itemState?: DropdownItemState;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const meta: Meta<DropdownStoryProps> = {
|
|
22
|
+
title: 'Components/Navigation/Dropdown',
|
|
23
|
+
component: Dropdown,
|
|
24
|
+
tags: ['autodocs'],
|
|
25
|
+
parameters: {
|
|
26
|
+
docs: {
|
|
27
|
+
description: {
|
|
28
|
+
component: 'A dropdown menu component with support for sections, icons, shortcuts, and selection states.',
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
layout: 'centered',
|
|
32
|
+
},
|
|
33
|
+
argTypes: {
|
|
34
|
+
triggerState: {
|
|
35
|
+
control: 'select',
|
|
36
|
+
options: ['default', 'hover', 'focus', 'open', 'disabled'],
|
|
37
|
+
description: 'Visual state for DropdownTrigger documentation',
|
|
38
|
+
table: {
|
|
39
|
+
defaultValue: { summary: 'default' },
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
itemState: {
|
|
43
|
+
control: 'select',
|
|
44
|
+
options: ['default', 'hover', 'focus', 'selected', 'disabled'],
|
|
45
|
+
description: 'Visual state for DropdownItem documentation',
|
|
46
|
+
table: {
|
|
47
|
+
defaultValue: { summary: 'default' },
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export default meta;
|
|
54
|
+
type Story = StoryObj<DropdownStoryProps>;
|
|
55
|
+
|
|
56
|
+
export const Default: Story = {
|
|
57
|
+
render: () => (
|
|
58
|
+
<Dropdown>
|
|
59
|
+
<DropdownTrigger>Options</DropdownTrigger>
|
|
60
|
+
<DropdownMenu>
|
|
61
|
+
<DropdownItem>New File</DropdownItem>
|
|
62
|
+
<DropdownItem>New Folder</DropdownItem>
|
|
63
|
+
<DropdownSeparator />
|
|
64
|
+
<DropdownItem>Settings</DropdownItem>
|
|
65
|
+
</DropdownMenu>
|
|
66
|
+
</Dropdown>
|
|
67
|
+
),
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export const WithIcons: Story = {
|
|
71
|
+
render: () => (
|
|
72
|
+
<Dropdown>
|
|
73
|
+
<DropdownTrigger>Actions</DropdownTrigger>
|
|
74
|
+
<DropdownMenu>
|
|
75
|
+
<DropdownItem icon={<Edit className="h-4 w-4" />}>Edit</DropdownItem>
|
|
76
|
+
<DropdownItem icon={<Copy className="h-4 w-4" />}>Duplicate</DropdownItem>
|
|
77
|
+
<DropdownSeparator />
|
|
78
|
+
<DropdownItem icon={<Trash2 className="h-4 w-4" />} destructive>
|
|
79
|
+
Delete
|
|
80
|
+
</DropdownItem>
|
|
81
|
+
</DropdownMenu>
|
|
82
|
+
</Dropdown>
|
|
83
|
+
),
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export const WithShortcuts: Story = {
|
|
87
|
+
render: () => (
|
|
88
|
+
<Dropdown>
|
|
89
|
+
<DropdownTrigger>Edit</DropdownTrigger>
|
|
90
|
+
<DropdownMenu>
|
|
91
|
+
<DropdownItem shortcut="⌘X">Cut</DropdownItem>
|
|
92
|
+
<DropdownItem shortcut="⌘C">Copy</DropdownItem>
|
|
93
|
+
<DropdownItem shortcut="⌘V">Paste</DropdownItem>
|
|
94
|
+
<DropdownSeparator />
|
|
95
|
+
<DropdownItem shortcut="⌘Z">Undo</DropdownItem>
|
|
96
|
+
<DropdownItem shortcut="⌘⇧Z">Redo</DropdownItem>
|
|
97
|
+
</DropdownMenu>
|
|
98
|
+
</Dropdown>
|
|
99
|
+
),
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export const WithSections: Story = {
|
|
103
|
+
render: () => (
|
|
104
|
+
<Dropdown>
|
|
105
|
+
<DropdownTrigger>Menu</DropdownTrigger>
|
|
106
|
+
<DropdownMenu>
|
|
107
|
+
<DropdownLabel>Account</DropdownLabel>
|
|
108
|
+
<DropdownItem icon={<User className="h-4 w-4" />}>Profile</DropdownItem>
|
|
109
|
+
<DropdownItem icon={<Settings className="h-4 w-4" />}>Settings</DropdownItem>
|
|
110
|
+
<DropdownSeparator />
|
|
111
|
+
<DropdownLabel>Danger Zone</DropdownLabel>
|
|
112
|
+
<DropdownItem icon={<LogOut className="h-4 w-4" />} destructive>
|
|
113
|
+
Sign Out
|
|
114
|
+
</DropdownItem>
|
|
115
|
+
</DropdownMenu>
|
|
116
|
+
</Dropdown>
|
|
117
|
+
),
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export const WithSelectedItem: Story = {
|
|
121
|
+
render: () => (
|
|
122
|
+
<Dropdown>
|
|
123
|
+
<DropdownTrigger>Sort By</DropdownTrigger>
|
|
124
|
+
<DropdownMenu>
|
|
125
|
+
<DropdownItem selected>Name</DropdownItem>
|
|
126
|
+
<DropdownItem>Date Modified</DropdownItem>
|
|
127
|
+
<DropdownItem>Date Created</DropdownItem>
|
|
128
|
+
<DropdownItem>Size</DropdownItem>
|
|
129
|
+
</DropdownMenu>
|
|
130
|
+
</Dropdown>
|
|
131
|
+
),
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export const WithDisabledItems: Story = {
|
|
135
|
+
render: () => (
|
|
136
|
+
<Dropdown>
|
|
137
|
+
<DropdownTrigger>File</DropdownTrigger>
|
|
138
|
+
<DropdownMenu>
|
|
139
|
+
<DropdownItem>New</DropdownItem>
|
|
140
|
+
<DropdownItem>Open</DropdownItem>
|
|
141
|
+
<DropdownSeparator />
|
|
142
|
+
<DropdownItem disabled>Save (no changes)</DropdownItem>
|
|
143
|
+
<DropdownItem>Save As...</DropdownItem>
|
|
144
|
+
</DropdownMenu>
|
|
145
|
+
</Dropdown>
|
|
146
|
+
),
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
export const AlignCenter: Story = {
|
|
150
|
+
render: () => (
|
|
151
|
+
<Dropdown>
|
|
152
|
+
<DropdownTrigger>Centered</DropdownTrigger>
|
|
153
|
+
<DropdownMenu align="center">
|
|
154
|
+
<DropdownItem>Option 1</DropdownItem>
|
|
155
|
+
<DropdownItem>Option 2</DropdownItem>
|
|
156
|
+
<DropdownItem>Option 3</DropdownItem>
|
|
157
|
+
</DropdownMenu>
|
|
158
|
+
</Dropdown>
|
|
159
|
+
),
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
export const AlignEnd: Story = {
|
|
163
|
+
render: () => (
|
|
164
|
+
<div className="flex justify-end w-64">
|
|
165
|
+
<Dropdown>
|
|
166
|
+
<DropdownTrigger>Right Aligned</DropdownTrigger>
|
|
167
|
+
<DropdownMenu align="end">
|
|
168
|
+
<DropdownItem>Option 1</DropdownItem>
|
|
169
|
+
<DropdownItem>Option 2</DropdownItem>
|
|
170
|
+
<DropdownItem>Option 3</DropdownItem>
|
|
171
|
+
</DropdownMenu>
|
|
172
|
+
</Dropdown>
|
|
173
|
+
</div>
|
|
174
|
+
),
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
export const CustomTrigger: Story = {
|
|
178
|
+
render: () => (
|
|
179
|
+
<Dropdown>
|
|
180
|
+
<DropdownTrigger asChild>
|
|
181
|
+
<Button variant="secondary">Custom Button Trigger</Button>
|
|
182
|
+
</DropdownTrigger>
|
|
183
|
+
<DropdownMenu>
|
|
184
|
+
<DropdownItem>Action 1</DropdownItem>
|
|
185
|
+
<DropdownItem>Action 2</DropdownItem>
|
|
186
|
+
<DropdownItem>Action 3</DropdownItem>
|
|
187
|
+
</DropdownMenu>
|
|
188
|
+
</Dropdown>
|
|
189
|
+
),
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
export const UserMenu: Story = {
|
|
193
|
+
render: () => (
|
|
194
|
+
<Dropdown>
|
|
195
|
+
<DropdownTrigger asChild>
|
|
196
|
+
<button className="flex items-center gap-2 p-2 hover:bg-secondary-hover rounded transition-colors">
|
|
197
|
+
<div className="w-8 h-8 rounded-full bg-primary flex items-center justify-center text-primary-foreground text-sm font-medium">
|
|
198
|
+
JD
|
|
199
|
+
</div>
|
|
200
|
+
<span className="text-sm font-medium text-foreground">John Doe</span>
|
|
201
|
+
</button>
|
|
202
|
+
</DropdownTrigger>
|
|
203
|
+
<DropdownMenu align="end">
|
|
204
|
+
<div className="px-3 py-2 border-b border-border">
|
|
205
|
+
<p className="text-sm font-medium text-foreground">John Doe</p>
|
|
206
|
+
<p className="text-xs text-foreground-muted">john@sonance.com</p>
|
|
207
|
+
</div>
|
|
208
|
+
<DropdownItem icon={<User className="h-4 w-4" />}>Profile</DropdownItem>
|
|
209
|
+
<DropdownItem icon={<Settings className="h-4 w-4" />}>Settings</DropdownItem>
|
|
210
|
+
<DropdownSeparator />
|
|
211
|
+
<DropdownItem icon={<LogOut className="h-4 w-4" />} destructive>
|
|
212
|
+
Sign Out
|
|
213
|
+
</DropdownItem>
|
|
214
|
+
</DropdownMenu>
|
|
215
|
+
</Dropdown>
|
|
216
|
+
),
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
// State Matrix - Visual documentation of all states
|
|
220
|
+
export const StateMatrix: Story = {
|
|
221
|
+
render: () => {
|
|
222
|
+
const triggerStates: DropdownTriggerState[] = ['default', 'hover', 'focus', 'open', 'disabled'];
|
|
223
|
+
const itemStates: DropdownItemState[] = ['default', 'hover', 'focus', 'selected', 'disabled'];
|
|
224
|
+
return (
|
|
225
|
+
<div className="space-y-8">
|
|
226
|
+
<div>
|
|
227
|
+
<h3 className="text-sm font-medium text-foreground-muted mb-4">DropdownTrigger States</h3>
|
|
228
|
+
<div className="flex flex-wrap gap-4">
|
|
229
|
+
{triggerStates.map((state) => (
|
|
230
|
+
<div key={state} className="flex flex-col items-center gap-2">
|
|
231
|
+
<span className="text-xs font-medium text-foreground-muted uppercase">{state}</span>
|
|
232
|
+
<Dropdown>
|
|
233
|
+
<DropdownTrigger state={state}>{state}</DropdownTrigger>
|
|
234
|
+
<DropdownMenu>
|
|
235
|
+
<DropdownItem>Option</DropdownItem>
|
|
236
|
+
</DropdownMenu>
|
|
237
|
+
</Dropdown>
|
|
238
|
+
</div>
|
|
239
|
+
))}
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
<div>
|
|
243
|
+
<h3 className="text-sm font-medium text-foreground-muted mb-4">DropdownItem States (Open menu to see)</h3>
|
|
244
|
+
<Dropdown>
|
|
245
|
+
<DropdownTrigger state="open">View Item States</DropdownTrigger>
|
|
246
|
+
<DropdownMenu>
|
|
247
|
+
{itemStates.map((state) => (
|
|
248
|
+
<DropdownItem key={state} state={state}>
|
|
249
|
+
{state} item
|
|
250
|
+
</DropdownItem>
|
|
251
|
+
))}
|
|
252
|
+
</DropdownMenu>
|
|
253
|
+
</Dropdown>
|
|
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 flex justify-center">
|
|
268
|
+
<Dropdown>
|
|
269
|
+
<DropdownTrigger>Actions</DropdownTrigger>
|
|
270
|
+
<DropdownMenu>
|
|
271
|
+
<DropdownItem icon={<Edit className="h-4 w-4" />}>Edit</DropdownItem>
|
|
272
|
+
<DropdownItem icon={<Copy className="h-4 w-4" />}>Copy</DropdownItem>
|
|
273
|
+
<DropdownSeparator />
|
|
274
|
+
<DropdownItem icon={<Trash2 className="h-4 w-4" />} destructive>Delete</DropdownItem>
|
|
275
|
+
</DropdownMenu>
|
|
276
|
+
</Dropdown>
|
|
277
|
+
</div>
|
|
278
|
+
</div>
|
|
279
|
+
{/* Tablet */}
|
|
280
|
+
<div>
|
|
281
|
+
<h4 className="text-xs uppercase text-foreground-muted mb-2">Tablet (768px)</h4>
|
|
282
|
+
<div className="w-[768px] border border-dashed border-border p-4 flex justify-between">
|
|
283
|
+
<Dropdown>
|
|
284
|
+
<DropdownTrigger>File</DropdownTrigger>
|
|
285
|
+
<DropdownMenu>
|
|
286
|
+
<DropdownItem shortcut="⌘N">New</DropdownItem>
|
|
287
|
+
<DropdownItem shortcut="⌘O">Open</DropdownItem>
|
|
288
|
+
<DropdownSeparator />
|
|
289
|
+
<DropdownItem shortcut="⌘S">Save</DropdownItem>
|
|
290
|
+
</DropdownMenu>
|
|
291
|
+
</Dropdown>
|
|
292
|
+
<Dropdown>
|
|
293
|
+
<DropdownTrigger>Edit</DropdownTrigger>
|
|
294
|
+
<DropdownMenu>
|
|
295
|
+
<DropdownItem shortcut="⌘X">Cut</DropdownItem>
|
|
296
|
+
<DropdownItem shortcut="⌘C">Copy</DropdownItem>
|
|
297
|
+
<DropdownItem shortcut="⌘V">Paste</DropdownItem>
|
|
298
|
+
</DropdownMenu>
|
|
299
|
+
</Dropdown>
|
|
300
|
+
</div>
|
|
301
|
+
</div>
|
|
302
|
+
{/* Desktop */}
|
|
303
|
+
<div>
|
|
304
|
+
<h4 className="text-xs uppercase text-foreground-muted mb-2">Desktop (1280px)</h4>
|
|
305
|
+
<div className="w-[1280px] border border-dashed border-border p-4 flex justify-between items-center">
|
|
306
|
+
<div className="flex gap-4">
|
|
307
|
+
<Dropdown>
|
|
308
|
+
<DropdownTrigger>File</DropdownTrigger>
|
|
309
|
+
<DropdownMenu>
|
|
310
|
+
<DropdownItem>New File</DropdownItem>
|
|
311
|
+
<DropdownItem>New Folder</DropdownItem>
|
|
312
|
+
<DropdownSeparator />
|
|
313
|
+
<DropdownItem>Settings</DropdownItem>
|
|
314
|
+
</DropdownMenu>
|
|
315
|
+
</Dropdown>
|
|
316
|
+
<Dropdown>
|
|
317
|
+
<DropdownTrigger>Edit</DropdownTrigger>
|
|
318
|
+
<DropdownMenu>
|
|
319
|
+
<DropdownItem shortcut="⌘Z">Undo</DropdownItem>
|
|
320
|
+
<DropdownItem shortcut="⌘⇧Z">Redo</DropdownItem>
|
|
321
|
+
</DropdownMenu>
|
|
322
|
+
</Dropdown>
|
|
323
|
+
</div>
|
|
324
|
+
<Dropdown>
|
|
325
|
+
<DropdownTrigger asChild>
|
|
326
|
+
<button className="flex items-center gap-2 p-2 hover:bg-secondary-hover rounded">
|
|
327
|
+
<div className="w-8 h-8 rounded-full bg-primary flex items-center justify-center text-primary-foreground text-sm font-medium">JD</div>
|
|
328
|
+
<span className="text-sm font-medium">John Doe</span>
|
|
329
|
+
</button>
|
|
330
|
+
</DropdownTrigger>
|
|
331
|
+
<DropdownMenu align="end">
|
|
332
|
+
<DropdownItem icon={<User className="h-4 w-4" />}>Profile</DropdownItem>
|
|
333
|
+
<DropdownItem icon={<Settings className="h-4 w-4" />}>Settings</DropdownItem>
|
|
334
|
+
<DropdownSeparator />
|
|
335
|
+
<DropdownItem icon={<LogOut className="h-4 w-4" />} destructive>Sign Out</DropdownItem>
|
|
336
|
+
</DropdownMenu>
|
|
337
|
+
</Dropdown>
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
</div>
|
|
341
|
+
),
|
|
342
|
+
};
|
|
@@ -4,6 +4,37 @@ import { useState, useRef, useEffect, createContext, useContext } from "react";
|
|
|
4
4
|
import { ChevronDown, Check } from "lucide-react";
|
|
5
5
|
import { cn } from "@/lib/utils";
|
|
6
6
|
|
|
7
|
+
export type DropdownTriggerState = "default" | "hover" | "focus" | "open" | "disabled";
|
|
8
|
+
export type DropdownItemState = "default" | "hover" | "focus" | "selected" | "disabled";
|
|
9
|
+
|
|
10
|
+
// State styles for DropdownTrigger Storybook/Figma visualization
|
|
11
|
+
const getTriggerStateStyles = (state?: DropdownTriggerState) => {
|
|
12
|
+
if (!state || state === "default") return "";
|
|
13
|
+
|
|
14
|
+
const stateMap: Record<string, string> = {
|
|
15
|
+
hover: "bg-secondary-hover",
|
|
16
|
+
focus: "ring-2 ring-border-focus",
|
|
17
|
+
open: "",
|
|
18
|
+
disabled: "opacity-50 cursor-not-allowed",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
return stateMap[state] || "";
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// State styles for DropdownItem Storybook/Figma visualization
|
|
25
|
+
const getItemStateStyles = (state?: DropdownItemState) => {
|
|
26
|
+
if (!state || state === "default") return "";
|
|
27
|
+
|
|
28
|
+
const stateMap: Record<string, string> = {
|
|
29
|
+
hover: "bg-secondary-hover",
|
|
30
|
+
focus: "ring-2 ring-border-focus ring-inset",
|
|
31
|
+
selected: "bg-primary text-primary-foreground",
|
|
32
|
+
disabled: "opacity-50 cursor-not-allowed",
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
return stateMap[state] || "";
|
|
36
|
+
};
|
|
37
|
+
|
|
7
38
|
interface DropdownContextValue {
|
|
8
39
|
isOpen: boolean;
|
|
9
40
|
setIsOpen: (open: boolean) => void;
|
|
@@ -60,17 +91,22 @@ interface DropdownTriggerProps {
|
|
|
60
91
|
children: React.ReactNode;
|
|
61
92
|
className?: string;
|
|
62
93
|
asChild?: boolean;
|
|
94
|
+
disabled?: boolean;
|
|
95
|
+
/** Visual state for Storybook/Figma documentation */
|
|
96
|
+
state?: DropdownTriggerState;
|
|
63
97
|
}
|
|
64
98
|
|
|
65
|
-
export function DropdownTrigger({ children, className, asChild }: DropdownTriggerProps) {
|
|
99
|
+
export function DropdownTrigger({ children, className, asChild, disabled, state }: DropdownTriggerProps) {
|
|
66
100
|
const context = useContext(DropdownContext);
|
|
67
101
|
if (!context) throw new Error("DropdownTrigger must be used within Dropdown");
|
|
68
102
|
|
|
69
103
|
const { isOpen, setIsOpen } = context;
|
|
104
|
+
const isDisabled = disabled || state === "disabled";
|
|
105
|
+
const isOpenState = state === "open";
|
|
70
106
|
|
|
71
107
|
if (asChild) {
|
|
72
108
|
return (
|
|
73
|
-
<span onClick={() => setIsOpen(!isOpen)} className={className}>
|
|
109
|
+
<span onClick={() => !isDisabled && setIsOpen(!isOpen)} className={className}>
|
|
74
110
|
{children}
|
|
75
111
|
</span>
|
|
76
112
|
);
|
|
@@ -79,22 +115,25 @@ export function DropdownTrigger({ children, className, asChild }: DropdownTrigge
|
|
|
79
115
|
return (
|
|
80
116
|
<button
|
|
81
117
|
type="button"
|
|
82
|
-
onClick={() => setIsOpen(!isOpen)}
|
|
118
|
+
onClick={() => !isDisabled && setIsOpen(!isOpen)}
|
|
119
|
+
disabled={isDisabled}
|
|
83
120
|
className={cn(
|
|
84
121
|
"inline-flex items-center justify-center gap-2 px-4 py-2",
|
|
85
122
|
"text-sm font-medium text-foreground",
|
|
86
123
|
"border border-border bg-background hover:bg-secondary-hover",
|
|
87
124
|
"transition-colors focus:outline-none focus:ring-2 focus:ring-border-focus",
|
|
125
|
+
"disabled:opacity-50 disabled:cursor-not-allowed",
|
|
126
|
+
getTriggerStateStyles(state),
|
|
88
127
|
className
|
|
89
128
|
)}
|
|
90
|
-
aria-expanded={isOpen}
|
|
129
|
+
aria-expanded={isOpen || isOpenState}
|
|
91
130
|
aria-haspopup="menu"
|
|
92
131
|
>
|
|
93
132
|
{children}
|
|
94
133
|
<ChevronDown
|
|
95
134
|
className={cn(
|
|
96
135
|
"h-4 w-4 transition-transform duration-200",
|
|
97
|
-
isOpen && "rotate-180"
|
|
136
|
+
(isOpen || isOpenState) && "rotate-180"
|
|
98
137
|
)}
|
|
99
138
|
/>
|
|
100
139
|
</button>
|
|
@@ -146,6 +185,8 @@ interface DropdownItemProps {
|
|
|
146
185
|
icon?: React.ReactNode;
|
|
147
186
|
shortcut?: string;
|
|
148
187
|
className?: string;
|
|
188
|
+
/** Visual state for Storybook/Figma documentation */
|
|
189
|
+
state?: DropdownItemState;
|
|
149
190
|
}
|
|
150
191
|
|
|
151
192
|
export function DropdownItem({
|
|
@@ -157,14 +198,17 @@ export function DropdownItem({
|
|
|
157
198
|
icon,
|
|
158
199
|
shortcut,
|
|
159
200
|
className,
|
|
201
|
+
state,
|
|
160
202
|
}: DropdownItemProps) {
|
|
161
203
|
const context = useContext(DropdownContext);
|
|
162
204
|
if (!context) throw new Error("DropdownItem must be used within Dropdown");
|
|
163
205
|
|
|
164
206
|
const { closeDropdown } = context;
|
|
207
|
+
const isDisabled = disabled || state === "disabled";
|
|
208
|
+
const isSelected = selected || state === "selected";
|
|
165
209
|
|
|
166
210
|
const handleClick = () => {
|
|
167
|
-
if (!
|
|
211
|
+
if (!isDisabled) {
|
|
168
212
|
onClick?.();
|
|
169
213
|
closeDropdown();
|
|
170
214
|
}
|
|
@@ -175,23 +219,24 @@ export function DropdownItem({
|
|
|
175
219
|
type="button"
|
|
176
220
|
role="menuitem"
|
|
177
221
|
onClick={handleClick}
|
|
178
|
-
disabled={
|
|
222
|
+
disabled={isDisabled}
|
|
179
223
|
className={cn(
|
|
180
224
|
"flex w-full items-center gap-2 px-3 py-2 text-left text-sm",
|
|
181
225
|
"transition-colors",
|
|
182
|
-
|
|
226
|
+
isDisabled
|
|
183
227
|
? "cursor-not-allowed opacity-50"
|
|
184
228
|
: destructive
|
|
185
229
|
? "text-error hover:bg-error-light"
|
|
186
|
-
:
|
|
230
|
+
: isSelected
|
|
187
231
|
? "bg-primary text-primary-foreground"
|
|
188
232
|
: "text-foreground hover:bg-secondary-hover",
|
|
233
|
+
getItemStateStyles(state),
|
|
189
234
|
className
|
|
190
235
|
)}
|
|
191
236
|
>
|
|
192
237
|
{icon && <span className="w-4 h-4 shrink-0">{icon}</span>}
|
|
193
238
|
<span className="flex-1">{children}</span>
|
|
194
|
-
{
|
|
239
|
+
{isSelected && !icon && <Check className="h-4 w-4 ml-auto" />}
|
|
195
240
|
{shortcut && (
|
|
196
241
|
<span className="ml-auto text-xs text-foreground-muted">{shortcut}</span>
|
|
197
242
|
)}
|