@rovula/ui 0.0.78 → 0.0.79
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/cjs/bundle.css +15 -3
- package/dist/cjs/bundle.js +3 -3
- package/dist/cjs/bundle.js.map +1 -1
- package/dist/cjs/types/components/Dropdown/Dropdown.d.ts +3 -0
- package/dist/cjs/types/components/Dropdown/Dropdown.stories.d.ts +5 -1
- package/dist/cjs/types/components/Menu/Menu.d.ts +65 -0
- package/dist/cjs/types/components/Menu/Menu.stories.d.ts +31 -0
- package/dist/cjs/types/components/Menu/helpers.d.ts +19 -0
- package/dist/cjs/types/components/Menu/index.d.ts +4 -0
- package/dist/cjs/types/components/Search/Search.d.ts +46 -3
- package/dist/cjs/types/components/Search/Search.stories.d.ts +46 -27
- package/dist/cjs/types/index.d.ts +1 -0
- package/dist/components/Dropdown/Dropdown.js +41 -19
- package/dist/components/Dropdown/Dropdown.stories.js +13 -0
- package/dist/components/Menu/Menu.js +64 -0
- package/dist/components/Menu/Menu.stories.js +406 -0
- package/dist/components/Menu/helpers.js +28 -0
- package/dist/components/Menu/index.js +3 -0
- package/dist/esm/bundle.css +15 -3
- package/dist/esm/bundle.js +3 -3
- package/dist/esm/bundle.js.map +1 -1
- package/dist/esm/types/components/Dropdown/Dropdown.d.ts +3 -0
- package/dist/esm/types/components/Dropdown/Dropdown.stories.d.ts +5 -1
- package/dist/esm/types/components/Menu/Menu.d.ts +65 -0
- package/dist/esm/types/components/Menu/Menu.stories.d.ts +31 -0
- package/dist/esm/types/components/Menu/helpers.d.ts +19 -0
- package/dist/esm/types/components/Menu/index.d.ts +4 -0
- package/dist/esm/types/components/Search/Search.d.ts +46 -3
- package/dist/esm/types/components/Search/Search.stories.d.ts +46 -27
- package/dist/esm/types/index.d.ts +1 -0
- package/dist/index.d.ts +111 -3
- package/dist/index.js +1 -0
- package/dist/src/theme/global.css +20 -4
- package/package.json +1 -1
- package/src/components/Dropdown/Dropdown.stories.tsx +31 -0
- package/src/components/Dropdown/Dropdown.tsx +73 -54
- package/src/components/Menu/Menu.stories.tsx +586 -0
- package/src/components/Menu/Menu.tsx +235 -0
- package/src/components/Menu/helpers.ts +45 -0
- package/src/components/Menu/index.ts +7 -0
- package/src/components/Search/Search.tsx +24 -11
- package/src/index.ts +1 -0
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
|
+
import { Menu, MenuItemType } from "./Menu";
|
|
4
|
+
import { optionsToMenuItems } from "./helpers";
|
|
5
|
+
import Button from "../Button/Button";
|
|
6
|
+
import Icon from "../Icon/Icon";
|
|
7
|
+
import TextInput from "../TextInput/TextInput";
|
|
8
|
+
import { ChevronDownIcon } from "@heroicons/react/16/solid";
|
|
9
|
+
|
|
10
|
+
const meta = {
|
|
11
|
+
title: "Components/Menu",
|
|
12
|
+
component: Menu,
|
|
13
|
+
tags: ["autodocs"],
|
|
14
|
+
parameters: {
|
|
15
|
+
layout: "centered",
|
|
16
|
+
},
|
|
17
|
+
decorators: [
|
|
18
|
+
(Story) => (
|
|
19
|
+
<div className="p-20 flex justify-center">
|
|
20
|
+
<Story />
|
|
21
|
+
</div>
|
|
22
|
+
),
|
|
23
|
+
],
|
|
24
|
+
} satisfies Meta<typeof Menu>;
|
|
25
|
+
|
|
26
|
+
export default meta;
|
|
27
|
+
|
|
28
|
+
// ==================== Basic Menu ====================
|
|
29
|
+
|
|
30
|
+
export const Basic: StoryObj<typeof Menu> = {
|
|
31
|
+
render: () => {
|
|
32
|
+
const items: MenuItemType[] = [
|
|
33
|
+
{ type: "item", item: { value: "1", label: "Option 1" } },
|
|
34
|
+
{ type: "item", item: { value: "2", label: "Option 2" } },
|
|
35
|
+
{ type: "item", item: { value: "3", label: "Option 3" } },
|
|
36
|
+
{ type: "separator" },
|
|
37
|
+
{ type: "item", item: { value: "4", label: "Option 4", disabled: true } },
|
|
38
|
+
{ type: "item", item: { value: "5", label: "Delete", danger: true } },
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<Menu
|
|
43
|
+
items={items}
|
|
44
|
+
onSelect={(value) => console.log("Selected:", value)}
|
|
45
|
+
/>
|
|
46
|
+
);
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// ==================== With Icons ====================
|
|
51
|
+
|
|
52
|
+
export const WithIcons: StoryObj<typeof Menu> = {
|
|
53
|
+
render: () => {
|
|
54
|
+
const items: MenuItemType[] = [
|
|
55
|
+
{
|
|
56
|
+
type: "item",
|
|
57
|
+
item: {
|
|
58
|
+
value: "profile",
|
|
59
|
+
label: "Profile",
|
|
60
|
+
icon: <Icon type="heroicons" name="user" className="size-4" />,
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
type: "item",
|
|
65
|
+
item: {
|
|
66
|
+
value: "settings",
|
|
67
|
+
label: "Settings",
|
|
68
|
+
icon: <Icon type="heroicons" name="cog-6-tooth" className="size-4" />,
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
type: "item",
|
|
73
|
+
item: {
|
|
74
|
+
value: "help",
|
|
75
|
+
label: "Help",
|
|
76
|
+
icon: (
|
|
77
|
+
<Icon
|
|
78
|
+
type="heroicons"
|
|
79
|
+
name="question-mark-circle"
|
|
80
|
+
className="size-4"
|
|
81
|
+
/>
|
|
82
|
+
),
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
{ type: "separator" },
|
|
86
|
+
{
|
|
87
|
+
type: "item",
|
|
88
|
+
item: {
|
|
89
|
+
value: "logout",
|
|
90
|
+
label: "Logout",
|
|
91
|
+
icon: (
|
|
92
|
+
<Icon
|
|
93
|
+
type="heroicons"
|
|
94
|
+
name="arrow-right-on-rectangle"
|
|
95
|
+
className="size-4"
|
|
96
|
+
/>
|
|
97
|
+
),
|
|
98
|
+
danger: true,
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<Menu
|
|
105
|
+
items={items}
|
|
106
|
+
onSelect={(value) => console.log("Selected:", value)}
|
|
107
|
+
/>
|
|
108
|
+
);
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// ==================== With Checkbox ====================
|
|
113
|
+
|
|
114
|
+
export const WithCheckbox: StoryObj<typeof Menu> = {
|
|
115
|
+
render: () => {
|
|
116
|
+
const [checked, setChecked] = useState({
|
|
117
|
+
notifications: true,
|
|
118
|
+
emails: false,
|
|
119
|
+
updates: true,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const items: MenuItemType[] = [
|
|
123
|
+
{ type: "label", label: "Preferences" },
|
|
124
|
+
{
|
|
125
|
+
type: "item",
|
|
126
|
+
item: {
|
|
127
|
+
value: "notifications",
|
|
128
|
+
label: "Notifications",
|
|
129
|
+
type: "checkbox",
|
|
130
|
+
checked: checked.notifications,
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
type: "item",
|
|
135
|
+
item: {
|
|
136
|
+
value: "emails",
|
|
137
|
+
label: "Email Alerts",
|
|
138
|
+
type: "checkbox",
|
|
139
|
+
checked: checked.emails,
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
type: "item",
|
|
144
|
+
item: {
|
|
145
|
+
value: "updates",
|
|
146
|
+
label: "Product Updates",
|
|
147
|
+
type: "checkbox",
|
|
148
|
+
checked: checked.updates,
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<Menu
|
|
155
|
+
items={items}
|
|
156
|
+
onSelect={(value) => {
|
|
157
|
+
setChecked((prev) => ({
|
|
158
|
+
...prev,
|
|
159
|
+
[value]: !prev[value as keyof typeof prev],
|
|
160
|
+
}));
|
|
161
|
+
}}
|
|
162
|
+
/>
|
|
163
|
+
);
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// ==================== With Radio ====================
|
|
168
|
+
|
|
169
|
+
export const WithRadio: StoryObj<typeof Menu> = {
|
|
170
|
+
render: () => {
|
|
171
|
+
const [selected, setSelected] = useState("light");
|
|
172
|
+
|
|
173
|
+
const items: MenuItemType[] = [
|
|
174
|
+
{ type: "label", label: "Theme" },
|
|
175
|
+
{
|
|
176
|
+
type: "item",
|
|
177
|
+
item: {
|
|
178
|
+
value: "light",
|
|
179
|
+
label: "Light",
|
|
180
|
+
type: "radio",
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
type: "item",
|
|
185
|
+
item: {
|
|
186
|
+
value: "dark",
|
|
187
|
+
label: "Dark",
|
|
188
|
+
type: "radio",
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
type: "item",
|
|
193
|
+
item: {
|
|
194
|
+
value: "system",
|
|
195
|
+
label: "System",
|
|
196
|
+
type: "radio",
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
];
|
|
200
|
+
|
|
201
|
+
return (
|
|
202
|
+
<Menu
|
|
203
|
+
items={items}
|
|
204
|
+
selectedValues={[selected]}
|
|
205
|
+
onSelect={(value) => setSelected(value)}
|
|
206
|
+
/>
|
|
207
|
+
);
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// ==================== Complex Menu ====================
|
|
212
|
+
|
|
213
|
+
export const ComplexMenu: StoryObj<typeof Menu> = {
|
|
214
|
+
render: () => {
|
|
215
|
+
const [preferences, setPreferences] = useState({
|
|
216
|
+
notifications: true,
|
|
217
|
+
emails: false,
|
|
218
|
+
});
|
|
219
|
+
const [theme, setTheme] = useState("light");
|
|
220
|
+
|
|
221
|
+
const items: MenuItemType[] = [
|
|
222
|
+
{ type: "label", label: "My Account" },
|
|
223
|
+
{
|
|
224
|
+
type: "item",
|
|
225
|
+
item: {
|
|
226
|
+
value: "profile",
|
|
227
|
+
label: "Profile",
|
|
228
|
+
icon: <Icon type="heroicons" name="user" className="size-4" />,
|
|
229
|
+
onClick: () => console.log("Go to profile"),
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
type: "item",
|
|
234
|
+
item: {
|
|
235
|
+
value: "billing",
|
|
236
|
+
label: "Billing",
|
|
237
|
+
icon: <Icon type="heroicons" name="credit-card" className="size-4" />,
|
|
238
|
+
onClick: () => console.log("Go to billing"),
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
{ type: "separator" },
|
|
242
|
+
{ type: "label", label: "Preferences" },
|
|
243
|
+
{
|
|
244
|
+
type: "item",
|
|
245
|
+
item: {
|
|
246
|
+
value: "notifications",
|
|
247
|
+
label: "Push Notifications",
|
|
248
|
+
type: "checkbox",
|
|
249
|
+
checked: preferences.notifications,
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
type: "item",
|
|
254
|
+
item: {
|
|
255
|
+
value: "emails",
|
|
256
|
+
label: "Email Notifications",
|
|
257
|
+
type: "checkbox",
|
|
258
|
+
checked: preferences.emails,
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
{ type: "separator" },
|
|
262
|
+
{ type: "label", label: "Theme" },
|
|
263
|
+
{
|
|
264
|
+
type: "item",
|
|
265
|
+
item: {
|
|
266
|
+
value: "light",
|
|
267
|
+
label: "Light Mode",
|
|
268
|
+
type: "radio",
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
type: "item",
|
|
273
|
+
item: {
|
|
274
|
+
value: "dark",
|
|
275
|
+
label: "Dark Mode",
|
|
276
|
+
type: "radio",
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
{ type: "separator" },
|
|
280
|
+
{
|
|
281
|
+
type: "item",
|
|
282
|
+
item: {
|
|
283
|
+
value: "logout",
|
|
284
|
+
label: "Logout",
|
|
285
|
+
icon: (
|
|
286
|
+
<Icon
|
|
287
|
+
type="heroicons"
|
|
288
|
+
name="arrow-right-on-rectangle"
|
|
289
|
+
className="size-4"
|
|
290
|
+
/>
|
|
291
|
+
),
|
|
292
|
+
danger: true,
|
|
293
|
+
onClick: () => console.log("Logout"),
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
];
|
|
297
|
+
|
|
298
|
+
return (
|
|
299
|
+
<Menu
|
|
300
|
+
items={items}
|
|
301
|
+
selectedValues={[theme]}
|
|
302
|
+
onSelect={(value, item) => {
|
|
303
|
+
// Handle different types
|
|
304
|
+
if (item.type === "checkbox") {
|
|
305
|
+
// Toggle checkbox
|
|
306
|
+
const key = value as keyof typeof preferences;
|
|
307
|
+
if (key in preferences) {
|
|
308
|
+
setPreferences({ ...preferences, [key]: !preferences[key] });
|
|
309
|
+
}
|
|
310
|
+
} else if (item.type === "radio") {
|
|
311
|
+
// Radio select
|
|
312
|
+
setTheme(value);
|
|
313
|
+
} else {
|
|
314
|
+
// Regular item
|
|
315
|
+
console.log("Item clicked:", value);
|
|
316
|
+
}
|
|
317
|
+
}}
|
|
318
|
+
/>
|
|
319
|
+
);
|
|
320
|
+
},
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
// ==================== With Dropdown Trigger ====================
|
|
324
|
+
|
|
325
|
+
export const WithDropdownTrigger: StoryObj<typeof Menu> = {
|
|
326
|
+
render: () => {
|
|
327
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
328
|
+
|
|
329
|
+
const items: MenuItemType[] = [
|
|
330
|
+
{
|
|
331
|
+
type: "item",
|
|
332
|
+
item: {
|
|
333
|
+
value: "edit",
|
|
334
|
+
label: "Edit",
|
|
335
|
+
icon: <Icon type="heroicons" name="pencil" className="size-4" />,
|
|
336
|
+
onClick: () => {
|
|
337
|
+
console.log("Edit");
|
|
338
|
+
setIsOpen(false);
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
type: "item",
|
|
344
|
+
item: {
|
|
345
|
+
value: "duplicate",
|
|
346
|
+
label: "Duplicate",
|
|
347
|
+
icon: (
|
|
348
|
+
<Icon
|
|
349
|
+
type="heroicons"
|
|
350
|
+
name="document-duplicate"
|
|
351
|
+
className="size-4"
|
|
352
|
+
/>
|
|
353
|
+
),
|
|
354
|
+
onClick: () => {
|
|
355
|
+
console.log("Duplicate");
|
|
356
|
+
setIsOpen(false);
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
},
|
|
360
|
+
{ type: "separator" },
|
|
361
|
+
{
|
|
362
|
+
type: "item",
|
|
363
|
+
item: {
|
|
364
|
+
value: "archive",
|
|
365
|
+
label: "Archive",
|
|
366
|
+
icon: <Icon type="heroicons" name="archive-box" className="size-4" />,
|
|
367
|
+
onClick: () => {
|
|
368
|
+
console.log("Archive");
|
|
369
|
+
setIsOpen(false);
|
|
370
|
+
},
|
|
371
|
+
},
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
type: "item",
|
|
375
|
+
item: {
|
|
376
|
+
value: "delete",
|
|
377
|
+
label: "Delete",
|
|
378
|
+
icon: <Icon type="heroicons" name="trash" className="size-4" />,
|
|
379
|
+
danger: true,
|
|
380
|
+
onClick: () => {
|
|
381
|
+
console.log("Delete");
|
|
382
|
+
setIsOpen(false);
|
|
383
|
+
},
|
|
384
|
+
},
|
|
385
|
+
},
|
|
386
|
+
];
|
|
387
|
+
|
|
388
|
+
return (
|
|
389
|
+
<div className="relative">
|
|
390
|
+
<Button onClick={() => setIsOpen(!isOpen)}>Actions</Button>
|
|
391
|
+
{isOpen && (
|
|
392
|
+
<>
|
|
393
|
+
<div
|
|
394
|
+
className="fixed inset-0 z-40"
|
|
395
|
+
onClick={() => setIsOpen(false)}
|
|
396
|
+
/>
|
|
397
|
+
<div className="absolute top-full mt-2 z-50">
|
|
398
|
+
<Menu items={items} />
|
|
399
|
+
</div>
|
|
400
|
+
</>
|
|
401
|
+
)}
|
|
402
|
+
</div>
|
|
403
|
+
);
|
|
404
|
+
},
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
// ==================== Custom Items ====================
|
|
408
|
+
|
|
409
|
+
export const CustomItems: StoryObj<typeof Menu> = {
|
|
410
|
+
render: () => {
|
|
411
|
+
const items: MenuItemType[] = [
|
|
412
|
+
{ type: "label", label: "Recent Projects" },
|
|
413
|
+
{
|
|
414
|
+
type: "custom",
|
|
415
|
+
render: () => (
|
|
416
|
+
<div className="px-4 py-3 hover:bg-[var(--dropdown-menu-hover-bg)] cursor-pointer">
|
|
417
|
+
<div className="font-semibold">Project Alpha</div>
|
|
418
|
+
<div className="text-xs text-gray-500">Updated 2 hours ago</div>
|
|
419
|
+
</div>
|
|
420
|
+
),
|
|
421
|
+
},
|
|
422
|
+
{
|
|
423
|
+
type: "custom",
|
|
424
|
+
render: () => (
|
|
425
|
+
<div className="px-4 py-3 hover:bg-[var(--dropdown-menu-hover-bg)] cursor-pointer">
|
|
426
|
+
<div className="font-semibold">Project Beta</div>
|
|
427
|
+
<div className="text-xs text-gray-500">Updated yesterday</div>
|
|
428
|
+
</div>
|
|
429
|
+
),
|
|
430
|
+
},
|
|
431
|
+
{ type: "separator" },
|
|
432
|
+
{
|
|
433
|
+
type: "item",
|
|
434
|
+
item: {
|
|
435
|
+
value: "view-all",
|
|
436
|
+
label: "View All Projects",
|
|
437
|
+
icon: <Icon type="heroicons" name="folder-open" className="size-4" />,
|
|
438
|
+
},
|
|
439
|
+
},
|
|
440
|
+
];
|
|
441
|
+
|
|
442
|
+
return (
|
|
443
|
+
<Menu
|
|
444
|
+
items={items}
|
|
445
|
+
onSelect={(value) => console.log("Selected:", value)}
|
|
446
|
+
/>
|
|
447
|
+
);
|
|
448
|
+
},
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
// ==================== Dropdown Pattern (Select with Search) ====================
|
|
452
|
+
|
|
453
|
+
export const DropdownPattern: StoryObj<typeof Menu> = {
|
|
454
|
+
render: () => {
|
|
455
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
456
|
+
const [selectedValue, setSelectedValue] = useState<string>("");
|
|
457
|
+
const [searchText, setSearchText] = useState("");
|
|
458
|
+
|
|
459
|
+
// Simulate options like Dropdown component
|
|
460
|
+
const allOptions = [
|
|
461
|
+
{ value: "apple", label: "Apple" },
|
|
462
|
+
{ value: "banana", label: "Banana" },
|
|
463
|
+
{ value: "cherry", label: "Cherry" },
|
|
464
|
+
{ value: "date", label: "Date" },
|
|
465
|
+
{ value: "elderberry", label: "Elderberry" },
|
|
466
|
+
{ value: "fig", label: "Fig" },
|
|
467
|
+
{ value: "grape", label: "Grape" },
|
|
468
|
+
];
|
|
469
|
+
|
|
470
|
+
// Filter options based on search
|
|
471
|
+
const filteredOptions = allOptions.filter((opt) =>
|
|
472
|
+
opt.label.toLowerCase().includes(searchText.toLowerCase())
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
// Convert to MenuItemType
|
|
476
|
+
const menuItems = optionsToMenuItems(filteredOptions);
|
|
477
|
+
|
|
478
|
+
// Add "not found" message if no results
|
|
479
|
+
if (filteredOptions.length === 0) {
|
|
480
|
+
menuItems.push({
|
|
481
|
+
type: "custom",
|
|
482
|
+
render: () => (
|
|
483
|
+
<div className="px-4 py-14 text-center text-input-text">
|
|
484
|
+
Not found
|
|
485
|
+
</div>
|
|
486
|
+
),
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const selectedLabel =
|
|
491
|
+
allOptions.find((opt) => opt.value === selectedValue)?.label || "";
|
|
492
|
+
|
|
493
|
+
return (
|
|
494
|
+
<div className="relative w-80">
|
|
495
|
+
<TextInput
|
|
496
|
+
value={searchText || selectedLabel}
|
|
497
|
+
onChange={(e) => setSearchText(e.target.value)}
|
|
498
|
+
onFocus={() => setIsOpen(true)}
|
|
499
|
+
onBlur={() => setTimeout(() => setIsOpen(false), 200)}
|
|
500
|
+
placeholder="Select a fruit..."
|
|
501
|
+
endIcon={<ChevronDownIcon className="size-5 text-gray-400" />}
|
|
502
|
+
/>
|
|
503
|
+
{isOpen && (
|
|
504
|
+
<div className="absolute top-full mt-1 w-full z-50">
|
|
505
|
+
<Menu
|
|
506
|
+
items={menuItems}
|
|
507
|
+
selectedValues={[selectedValue]}
|
|
508
|
+
onSelect={(value) => {
|
|
509
|
+
setSelectedValue(value);
|
|
510
|
+
setSearchText("");
|
|
511
|
+
setIsOpen(false);
|
|
512
|
+
}}
|
|
513
|
+
className="max-h-60 overflow-y-auto"
|
|
514
|
+
/>
|
|
515
|
+
</div>
|
|
516
|
+
)}
|
|
517
|
+
</div>
|
|
518
|
+
);
|
|
519
|
+
},
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
// ==================== Multi-Select Pattern ====================
|
|
523
|
+
|
|
524
|
+
export const MultiSelectPattern: StoryObj<typeof Menu> = {
|
|
525
|
+
render: () => {
|
|
526
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
527
|
+
const [selectedValues, setSelectedValues] = useState<string[]>([]);
|
|
528
|
+
|
|
529
|
+
const options = [
|
|
530
|
+
{ value: "react", label: "React" },
|
|
531
|
+
{ value: "vue", label: "Vue" },
|
|
532
|
+
{ value: "angular", label: "Angular" },
|
|
533
|
+
{ value: "svelte", label: "Svelte" },
|
|
534
|
+
{ value: "solid", label: "Solid" },
|
|
535
|
+
];
|
|
536
|
+
|
|
537
|
+
const items: MenuItemType[] = options.map((opt) => ({
|
|
538
|
+
type: "item",
|
|
539
|
+
item: {
|
|
540
|
+
value: opt.value,
|
|
541
|
+
label: opt.label,
|
|
542
|
+
type: "checkbox",
|
|
543
|
+
checked: selectedValues.includes(opt.value),
|
|
544
|
+
},
|
|
545
|
+
}));
|
|
546
|
+
|
|
547
|
+
const displayText =
|
|
548
|
+
selectedValues.length > 0
|
|
549
|
+
? `${selectedValues.length} selected`
|
|
550
|
+
: "Select frameworks...";
|
|
551
|
+
|
|
552
|
+
return (
|
|
553
|
+
<div className="relative w-80">
|
|
554
|
+
<Button
|
|
555
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
556
|
+
className="w-full justify-between"
|
|
557
|
+
>
|
|
558
|
+
{displayText}
|
|
559
|
+
<ChevronDownIcon className="size-5 ml-2" />
|
|
560
|
+
</Button>
|
|
561
|
+
{isOpen && (
|
|
562
|
+
<>
|
|
563
|
+
<div
|
|
564
|
+
className="fixed inset-0 z-40"
|
|
565
|
+
onClick={() => setIsOpen(false)}
|
|
566
|
+
/>
|
|
567
|
+
<div className="absolute top-full mt-2 w-full z-50">
|
|
568
|
+
<Menu
|
|
569
|
+
items={items}
|
|
570
|
+
onSelect={(value) => {
|
|
571
|
+
if (selectedValues.includes(value)) {
|
|
572
|
+
setSelectedValues(
|
|
573
|
+
selectedValues.filter((v) => v !== value)
|
|
574
|
+
);
|
|
575
|
+
} else {
|
|
576
|
+
setSelectedValues([...selectedValues, value]);
|
|
577
|
+
}
|
|
578
|
+
}}
|
|
579
|
+
/>
|
|
580
|
+
</div>
|
|
581
|
+
</>
|
|
582
|
+
)}
|
|
583
|
+
</div>
|
|
584
|
+
);
|
|
585
|
+
},
|
|
586
|
+
};
|