@rovula/ui 0.1.21 → 0.1.22
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 +175 -26
- package/dist/cjs/bundle.js +675 -675
- package/dist/cjs/bundle.js.map +1 -1
- package/dist/cjs/types/components/Badge/Badge.d.ts +40 -0
- package/dist/cjs/types/components/Badge/Badge.stories.d.ts +295 -0
- package/dist/cjs/types/components/Badge/Badge.styles.d.ts +7 -0
- package/dist/cjs/types/components/Badge/index.d.ts +2 -0
- package/dist/cjs/types/components/Dropdown/Dropdown.d.ts +4 -8
- package/dist/cjs/types/components/Dropdown/Dropdown.stories.d.ts +1 -6
- package/dist/cjs/types/components/DropdownMenu/DropdownMenu.d.ts +5 -1
- package/dist/cjs/types/components/DropdownMenu/DropdownMenu.stories.d.ts +16 -0
- package/dist/cjs/types/index.d.ts +3 -1
- package/dist/cjs/types/patterns/menu/Menu.d.ts +70 -0
- package/dist/cjs/types/{components/Menu → patterns/menu}/Menu.stories.d.ts +17 -10
- package/dist/cjs/types/utils/mergeRefs.d.ts +20 -0
- package/dist/components/Avatar/Avatar.styles.js +2 -2
- package/dist/components/Badge/Badge.js +36 -0
- package/dist/components/Badge/Badge.stories.js +51 -0
- package/dist/components/Badge/Badge.styles.js +62 -0
- package/dist/components/Badge/index.js +2 -0
- package/dist/components/Dropdown/Dropdown.js +54 -163
- package/dist/components/Dropdown/Dropdown.stories.js +29 -0
- package/dist/components/DropdownMenu/DropdownMenu.js +22 -9
- package/dist/components/DropdownMenu/DropdownMenu.stories.js +54 -10
- package/dist/components/TextInput/TextInput.js +6 -3
- package/dist/esm/bundle.css +175 -26
- package/dist/esm/bundle.js +1545 -1545
- package/dist/esm/bundle.js.map +1 -1
- package/dist/esm/types/components/Badge/Badge.d.ts +40 -0
- package/dist/esm/types/components/Badge/Badge.stories.d.ts +295 -0
- package/dist/esm/types/components/Badge/Badge.styles.d.ts +7 -0
- package/dist/esm/types/components/Badge/index.d.ts +2 -0
- package/dist/esm/types/components/Dropdown/Dropdown.d.ts +4 -8
- package/dist/esm/types/components/Dropdown/Dropdown.stories.d.ts +1 -6
- package/dist/esm/types/components/DropdownMenu/DropdownMenu.d.ts +5 -1
- package/dist/esm/types/components/DropdownMenu/DropdownMenu.stories.d.ts +16 -0
- package/dist/esm/types/index.d.ts +3 -1
- package/dist/esm/types/patterns/menu/Menu.d.ts +70 -0
- package/dist/esm/types/{components/Menu → patterns/menu}/Menu.stories.d.ts +17 -10
- package/dist/esm/types/utils/mergeRefs.d.ts +20 -0
- package/dist/index.d.ts +116 -73
- package/dist/index.js +2 -1
- package/dist/patterns/menu/Menu.js +95 -0
- package/dist/patterns/menu/Menu.stories.js +611 -0
- package/dist/src/theme/global.css +289 -37
- package/dist/utils/mergeRefs.js +42 -0
- package/package.json +1 -1
- package/src/components/Avatar/Avatar.styles.ts +2 -2
- package/src/components/Badge/Badge.stories.tsx +128 -0
- package/src/components/Badge/Badge.styles.ts +70 -0
- package/src/components/Badge/Badge.tsx +103 -0
- package/src/components/Badge/index.ts +3 -0
- package/src/components/Dropdown/Dropdown.stories.tsx +170 -1
- package/src/components/Dropdown/Dropdown.tsx +186 -276
- package/src/components/DropdownMenu/DropdownMenu.stories.tsx +1050 -113
- package/src/components/DropdownMenu/DropdownMenu.tsx +116 -52
- package/src/components/TextInput/TextInput.tsx +6 -3
- package/src/index.ts +3 -1
- package/src/patterns/menu/Menu.stories.tsx +1100 -0
- package/src/patterns/menu/Menu.tsx +282 -0
- package/src/theme/themes/xspector/baseline.css +0 -1
- package/src/theme/tokens/baseline.css +2 -1
- package/src/theme/tokens/components/badge.css +54 -0
- package/src/theme/tokens/components/dropdown-menu.css +15 -4
- package/src/utils/mergeRefs.ts +46 -0
- package/dist/cjs/types/components/Menu/Menu.d.ts +0 -65
- package/dist/cjs/types/components/Menu/helpers.d.ts +0 -19
- package/dist/cjs/types/components/Menu/index.d.ts +0 -4
- package/dist/components/Menu/Menu.js +0 -64
- package/dist/components/Menu/Menu.stories.js +0 -406
- package/dist/components/Menu/helpers.js +0 -28
- package/dist/components/Menu/index.js +0 -3
- package/dist/esm/types/components/Menu/Menu.d.ts +0 -65
- package/dist/esm/types/components/Menu/helpers.d.ts +0 -19
- package/dist/esm/types/components/Menu/index.d.ts +0 -4
- package/src/components/Menu/Menu.stories.tsx +0 -586
- package/src/components/Menu/Menu.tsx +0 -235
- package/src/components/Menu/helpers.ts +0 -45
- package/src/components/Menu/index.ts +0 -7
- package/src/theme/themes/xspector/components/dropdown-menu.css +0 -28
|
@@ -0,0 +1,1100 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
|
+
import { Menu, MenuItemType } from "./Menu";
|
|
4
|
+
import Button from "../../components/Button/Button";
|
|
5
|
+
import ActionButton from "../../components/ActionButton/ActionButton";
|
|
6
|
+
import Icon from "../../components/Icon/Icon";
|
|
7
|
+
import { ChevronDownIcon } from "@heroicons/react/16/solid";
|
|
8
|
+
import { Switch } from "../../components/Switch/Switch";
|
|
9
|
+
|
|
10
|
+
const meta = {
|
|
11
|
+
title: "Patterns/Menu",
|
|
12
|
+
component: Menu,
|
|
13
|
+
parameters: {
|
|
14
|
+
layout: "fullscreen",
|
|
15
|
+
},
|
|
16
|
+
decorators: [
|
|
17
|
+
(Story) => (
|
|
18
|
+
<div className="p-10 flex gap-8 flex-wrap bg-workspace-surface min-h-screen">
|
|
19
|
+
<Story />
|
|
20
|
+
</div>
|
|
21
|
+
),
|
|
22
|
+
],
|
|
23
|
+
} satisfies Meta<typeof Menu>;
|
|
24
|
+
|
|
25
|
+
export default meta;
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Basic — default items, separator, disabled, danger
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
export const Basic: StoryObj<typeof Menu> = {
|
|
31
|
+
name: "Basic (Default Items)",
|
|
32
|
+
render: () => (
|
|
33
|
+
<div className="flex gap-8 items-start flex-wrap">
|
|
34
|
+
<div>
|
|
35
|
+
<p className="typography-small4 text-text-g-contrast-medium mb-2">
|
|
36
|
+
Icon button trigger
|
|
37
|
+
</p>
|
|
38
|
+
<Menu
|
|
39
|
+
trigger={
|
|
40
|
+
<ActionButton variant="icon">
|
|
41
|
+
<Icon type="heroicons" name="ellipsis-vertical" />
|
|
42
|
+
</ActionButton>
|
|
43
|
+
}
|
|
44
|
+
items={[
|
|
45
|
+
{ type: "item", item: { value: "1", label: "Option 1" } },
|
|
46
|
+
{ type: "item", item: { value: "2", label: "Option 2" } },
|
|
47
|
+
{
|
|
48
|
+
type: "item",
|
|
49
|
+
item: { value: "3", label: "Option 3 (Selected)" },
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
type: "item",
|
|
53
|
+
item: {
|
|
54
|
+
value: "4",
|
|
55
|
+
label: "Option 4 (Disabled)",
|
|
56
|
+
disabled: true,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
{ type: "separator" },
|
|
60
|
+
{
|
|
61
|
+
type: "item",
|
|
62
|
+
item: { value: "delete", label: "Delete", danger: true },
|
|
63
|
+
},
|
|
64
|
+
]}
|
|
65
|
+
selectedValues={["3"]}
|
|
66
|
+
onSelect={(v) => console.log("selected:", v)}
|
|
67
|
+
/>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<div>
|
|
71
|
+
<p className="typography-small4 text-text-g-contrast-medium mb-2">
|
|
72
|
+
Button trigger + label section
|
|
73
|
+
</p>
|
|
74
|
+
<Menu
|
|
75
|
+
trigger={<Button variant="outline">My Account</Button>}
|
|
76
|
+
items={[
|
|
77
|
+
{ type: "label", label: "My Account" },
|
|
78
|
+
{ type: "item", item: { value: "profile", label: "Profile" } },
|
|
79
|
+
{ type: "item", item: { value: "settings", label: "Settings" } },
|
|
80
|
+
{ type: "separator" },
|
|
81
|
+
{ type: "item", item: { value: "logout", label: "Logout" } },
|
|
82
|
+
]}
|
|
83
|
+
onSelect={(v) => console.log("selected:", v)}
|
|
84
|
+
/>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
),
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// With Icons
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
export const WithIcons: StoryObj<typeof Menu> = {
|
|
94
|
+
name: "With Icons",
|
|
95
|
+
render: () => (
|
|
96
|
+
<div className="flex gap-8 items-start flex-wrap">
|
|
97
|
+
<div>
|
|
98
|
+
<p className="typography-small4 text-text-g-contrast-medium mb-2">
|
|
99
|
+
Items with leading icon
|
|
100
|
+
</p>
|
|
101
|
+
<Menu
|
|
102
|
+
trigger={
|
|
103
|
+
<ActionButton variant="icon">
|
|
104
|
+
<Icon type="heroicons" name="ellipsis-vertical" />
|
|
105
|
+
</ActionButton>
|
|
106
|
+
}
|
|
107
|
+
align="end"
|
|
108
|
+
items={[
|
|
109
|
+
{
|
|
110
|
+
type: "item",
|
|
111
|
+
item: {
|
|
112
|
+
value: "profile",
|
|
113
|
+
label: "Profile",
|
|
114
|
+
icon: <Icon type="heroicons" name="user" className="size-4" />,
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
type: "item",
|
|
119
|
+
item: {
|
|
120
|
+
value: "settings",
|
|
121
|
+
label: "Settings",
|
|
122
|
+
icon: (
|
|
123
|
+
<Icon
|
|
124
|
+
type="heroicons"
|
|
125
|
+
name="cog-6-tooth"
|
|
126
|
+
className="size-4"
|
|
127
|
+
/>
|
|
128
|
+
),
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
type: "item",
|
|
133
|
+
item: {
|
|
134
|
+
value: "billing",
|
|
135
|
+
label: "Billing",
|
|
136
|
+
icon: (
|
|
137
|
+
<Icon
|
|
138
|
+
type="heroicons"
|
|
139
|
+
name="credit-card"
|
|
140
|
+
className="size-4"
|
|
141
|
+
/>
|
|
142
|
+
),
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
{ type: "separator" },
|
|
146
|
+
{
|
|
147
|
+
type: "item",
|
|
148
|
+
item: {
|
|
149
|
+
value: "logout",
|
|
150
|
+
label: "Logout",
|
|
151
|
+
icon: (
|
|
152
|
+
<Icon
|
|
153
|
+
type="heroicons"
|
|
154
|
+
name="arrow-right-on-rectangle"
|
|
155
|
+
className="size-4"
|
|
156
|
+
/>
|
|
157
|
+
),
|
|
158
|
+
danger: true,
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
]}
|
|
162
|
+
onSelect={(v) => console.log("selected:", v)}
|
|
163
|
+
/>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
<div>
|
|
167
|
+
<p className="typography-small4 text-text-g-contrast-medium mb-2">
|
|
168
|
+
With icon + disabled
|
|
169
|
+
</p>
|
|
170
|
+
<Menu
|
|
171
|
+
trigger={
|
|
172
|
+
<Button variant="outline">
|
|
173
|
+
Actions <ChevronDownIcon className="size-4 ml-2" />
|
|
174
|
+
</Button>
|
|
175
|
+
}
|
|
176
|
+
items={[
|
|
177
|
+
{
|
|
178
|
+
type: "item",
|
|
179
|
+
item: {
|
|
180
|
+
value: "edit",
|
|
181
|
+
label: "Edit",
|
|
182
|
+
icon: (
|
|
183
|
+
<Icon type="heroicons" name="pencil" className="size-4" />
|
|
184
|
+
),
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
type: "item",
|
|
189
|
+
item: {
|
|
190
|
+
value: "duplicate",
|
|
191
|
+
label: "Duplicate",
|
|
192
|
+
icon: (
|
|
193
|
+
<Icon
|
|
194
|
+
type="heroicons"
|
|
195
|
+
name="document-duplicate"
|
|
196
|
+
className="size-4"
|
|
197
|
+
/>
|
|
198
|
+
),
|
|
199
|
+
disabled: true,
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
type: "item",
|
|
204
|
+
item: {
|
|
205
|
+
value: "archive",
|
|
206
|
+
label: "Archive",
|
|
207
|
+
icon: (
|
|
208
|
+
<Icon
|
|
209
|
+
type="heroicons"
|
|
210
|
+
name="archive-box"
|
|
211
|
+
className="size-4"
|
|
212
|
+
/>
|
|
213
|
+
),
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
{ type: "separator" },
|
|
217
|
+
{
|
|
218
|
+
type: "item",
|
|
219
|
+
item: {
|
|
220
|
+
value: "delete",
|
|
221
|
+
label: "Delete",
|
|
222
|
+
icon: <Icon type="heroicons" name="trash" className="size-4" />,
|
|
223
|
+
danger: true,
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
]}
|
|
227
|
+
onSelect={(v) => console.log("selected:", v)}
|
|
228
|
+
/>
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
),
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
// With Checkbox — multi-select toggle
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
export const WithCheckbox: StoryObj<typeof Menu> = {
|
|
238
|
+
name: "With Checkbox",
|
|
239
|
+
render: () => {
|
|
240
|
+
const [checked, setChecked] = useState({
|
|
241
|
+
notifications: true,
|
|
242
|
+
emails: false,
|
|
243
|
+
updates: true,
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
const items: MenuItemType[] = [
|
|
247
|
+
{ type: "label", label: "Preferences" },
|
|
248
|
+
{
|
|
249
|
+
type: "item",
|
|
250
|
+
item: {
|
|
251
|
+
value: "notifications",
|
|
252
|
+
label: "Notifications",
|
|
253
|
+
type: "checkbox",
|
|
254
|
+
checked: checked.notifications,
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
type: "item",
|
|
259
|
+
item: {
|
|
260
|
+
value: "emails",
|
|
261
|
+
label: "Email Alerts",
|
|
262
|
+
type: "checkbox",
|
|
263
|
+
checked: checked.emails,
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
type: "item",
|
|
268
|
+
item: {
|
|
269
|
+
value: "updates",
|
|
270
|
+
label: "Product Updates",
|
|
271
|
+
type: "checkbox",
|
|
272
|
+
checked: checked.updates,
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
type: "item",
|
|
277
|
+
item: {
|
|
278
|
+
value: "disabled-feature",
|
|
279
|
+
label: "Disabled Feature",
|
|
280
|
+
type: "checkbox",
|
|
281
|
+
checked: false,
|
|
282
|
+
disabled: true,
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
];
|
|
286
|
+
|
|
287
|
+
return (
|
|
288
|
+
<div className="flex gap-8 items-start">
|
|
289
|
+
<div>
|
|
290
|
+
<p className="typography-small4 text-text-g-contrast-medium mb-2">
|
|
291
|
+
Click to toggle (menu stays open for checkboxes)
|
|
292
|
+
</p>
|
|
293
|
+
<Menu
|
|
294
|
+
trigger={
|
|
295
|
+
<Button variant="outline">
|
|
296
|
+
Preferences <ChevronDownIcon className="size-4 ml-2" />
|
|
297
|
+
</Button>
|
|
298
|
+
}
|
|
299
|
+
items={items}
|
|
300
|
+
onSelect={(value) => {
|
|
301
|
+
setChecked((prev) => ({
|
|
302
|
+
...prev,
|
|
303
|
+
[value]: !prev[value as keyof typeof prev],
|
|
304
|
+
}));
|
|
305
|
+
}}
|
|
306
|
+
/>
|
|
307
|
+
</div>
|
|
308
|
+
<div className="text-sm text-text-g-contrast-medium">
|
|
309
|
+
<p className="font-semibold mb-1">State:</p>
|
|
310
|
+
<pre className="text-xs">{JSON.stringify(checked, null, 2)}</pre>
|
|
311
|
+
</div>
|
|
312
|
+
</div>
|
|
313
|
+
);
|
|
314
|
+
},
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
// ---------------------------------------------------------------------------
|
|
318
|
+
// With Radio — single-select group
|
|
319
|
+
// ---------------------------------------------------------------------------
|
|
320
|
+
export const WithRadio: StoryObj<typeof Menu> = {
|
|
321
|
+
name: "With Radio",
|
|
322
|
+
render: () => {
|
|
323
|
+
const [theme, setTheme] = useState("light");
|
|
324
|
+
const [density, setDensity] = useState("comfortable");
|
|
325
|
+
|
|
326
|
+
const themeItems: MenuItemType[] = [
|
|
327
|
+
{ type: "label", label: "Theme" },
|
|
328
|
+
{ type: "item", item: { value: "light", label: "Light", type: "radio" } },
|
|
329
|
+
{ type: "item", item: { value: "dark", label: "Dark", type: "radio" } },
|
|
330
|
+
{
|
|
331
|
+
type: "item",
|
|
332
|
+
item: { value: "system", label: "System", type: "radio" },
|
|
333
|
+
},
|
|
334
|
+
];
|
|
335
|
+
|
|
336
|
+
const densityItems: MenuItemType[] = [
|
|
337
|
+
{ type: "label", label: "Density" },
|
|
338
|
+
{
|
|
339
|
+
type: "item",
|
|
340
|
+
item: { value: "compact", label: "Compact", type: "radio" },
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
type: "item",
|
|
344
|
+
item: { value: "comfortable", label: "Comfortable", type: "radio" },
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
type: "item",
|
|
348
|
+
item: { value: "spacious", label: "Spacious", type: "radio" },
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
type: "item",
|
|
352
|
+
item: {
|
|
353
|
+
value: "disabled-opt",
|
|
354
|
+
label: "Disabled Option",
|
|
355
|
+
type: "radio",
|
|
356
|
+
disabled: true,
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
];
|
|
360
|
+
|
|
361
|
+
return (
|
|
362
|
+
<div className="flex gap-8 items-start">
|
|
363
|
+
<div>
|
|
364
|
+
<p className="typography-small4 text-text-g-contrast-medium mb-2">
|
|
365
|
+
Theme selector
|
|
366
|
+
</p>
|
|
367
|
+
<Menu
|
|
368
|
+
trigger={
|
|
369
|
+
<Button variant="outline">
|
|
370
|
+
Theme: {theme} <ChevronDownIcon className="size-4 ml-2" />
|
|
371
|
+
</Button>
|
|
372
|
+
}
|
|
373
|
+
items={themeItems}
|
|
374
|
+
selectedValues={[theme]}
|
|
375
|
+
onSelect={(value) => setTheme(value)}
|
|
376
|
+
/>
|
|
377
|
+
</div>
|
|
378
|
+
|
|
379
|
+
<div>
|
|
380
|
+
<p className="typography-small4 text-text-g-contrast-medium mb-2">
|
|
381
|
+
Density selector + disabled
|
|
382
|
+
</p>
|
|
383
|
+
<Menu
|
|
384
|
+
trigger={
|
|
385
|
+
<Button variant="outline">
|
|
386
|
+
Density: {density} <ChevronDownIcon className="size-4 ml-2" />
|
|
387
|
+
</Button>
|
|
388
|
+
}
|
|
389
|
+
items={densityItems}
|
|
390
|
+
selectedValues={[density]}
|
|
391
|
+
onSelect={(value) => setDensity(value)}
|
|
392
|
+
/>
|
|
393
|
+
</div>
|
|
394
|
+
</div>
|
|
395
|
+
);
|
|
396
|
+
},
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
// ---------------------------------------------------------------------------
|
|
400
|
+
// Multiple Radio Groups — separated by separator
|
|
401
|
+
// ---------------------------------------------------------------------------
|
|
402
|
+
export const MultipleRadioGroups: StoryObj<typeof Menu> = {
|
|
403
|
+
name: "Multiple Radio Groups",
|
|
404
|
+
render: () => {
|
|
405
|
+
const [theme, setTheme] = useState("light");
|
|
406
|
+
const [language, setLanguage] = useState("en");
|
|
407
|
+
|
|
408
|
+
const items: MenuItemType[] = [
|
|
409
|
+
{ type: "label", label: "Theme" },
|
|
410
|
+
{ type: "item", item: { value: "light", label: "Light", type: "radio" } },
|
|
411
|
+
{ type: "item", item: { value: "dark", label: "Dark", type: "radio" } },
|
|
412
|
+
{ type: "separator" },
|
|
413
|
+
{ type: "label", label: "Language" },
|
|
414
|
+
{ type: "item", item: { value: "en", label: "English", type: "radio" } },
|
|
415
|
+
{ type: "item", item: { value: "th", label: "ภาษาไทย", type: "radio" } },
|
|
416
|
+
{ type: "item", item: { value: "ja", label: "日本語", type: "radio" } },
|
|
417
|
+
];
|
|
418
|
+
|
|
419
|
+
const handleSelect = (value: string) => {
|
|
420
|
+
if (["light", "dark"].includes(value)) setTheme(value);
|
|
421
|
+
else if (["en", "th", "ja"].includes(value)) setLanguage(value);
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
return (
|
|
425
|
+
<div className="flex gap-8 items-start">
|
|
426
|
+
<div>
|
|
427
|
+
<p className="typography-small4 text-text-g-contrast-medium mb-2">
|
|
428
|
+
Two independent radio groups
|
|
429
|
+
</p>
|
|
430
|
+
<Menu
|
|
431
|
+
trigger={<Button variant="outline">Settings</Button>}
|
|
432
|
+
items={items}
|
|
433
|
+
selectedValues={[theme, language]}
|
|
434
|
+
onSelect={handleSelect}
|
|
435
|
+
/>
|
|
436
|
+
</div>
|
|
437
|
+
<div className="text-sm text-text-g-contrast-medium">
|
|
438
|
+
<p className="font-semibold mb-1">State:</p>
|
|
439
|
+
<pre className="text-xs">
|
|
440
|
+
{JSON.stringify({ theme, language }, null, 2)}
|
|
441
|
+
</pre>
|
|
442
|
+
</div>
|
|
443
|
+
</div>
|
|
444
|
+
);
|
|
445
|
+
},
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
// ---------------------------------------------------------------------------
|
|
449
|
+
// Custom Items — type="custom" render prop
|
|
450
|
+
// ---------------------------------------------------------------------------
|
|
451
|
+
export const CustomItems: StoryObj<typeof Menu> = {
|
|
452
|
+
name: "Custom Items",
|
|
453
|
+
render: () => {
|
|
454
|
+
const items: MenuItemType[] = [
|
|
455
|
+
{ type: "label", label: "Recent Projects" },
|
|
456
|
+
{
|
|
457
|
+
type: "custom",
|
|
458
|
+
render: () => (
|
|
459
|
+
<div className="px-4 py-3 hover:bg-[var(--dropdown-menu-hover-bg)] cursor-pointer transition-colors">
|
|
460
|
+
<div className="typography-subtitle4 text-text-g-contrast-high">
|
|
461
|
+
Project Alpha
|
|
462
|
+
</div>
|
|
463
|
+
<div className="typography-small4 text-text-g-contrast-medium">
|
|
464
|
+
Updated 2 hours ago
|
|
465
|
+
</div>
|
|
466
|
+
</div>
|
|
467
|
+
),
|
|
468
|
+
},
|
|
469
|
+
{
|
|
470
|
+
type: "custom",
|
|
471
|
+
render: () => (
|
|
472
|
+
<div className="px-4 py-3 hover:bg-[var(--dropdown-menu-hover-bg)] cursor-pointer transition-colors">
|
|
473
|
+
<div className="typography-subtitle4 text-text-g-contrast-high">
|
|
474
|
+
Project Beta
|
|
475
|
+
</div>
|
|
476
|
+
<div className="typography-small4 text-text-g-contrast-medium">
|
|
477
|
+
Updated yesterday
|
|
478
|
+
</div>
|
|
479
|
+
</div>
|
|
480
|
+
),
|
|
481
|
+
},
|
|
482
|
+
{ type: "separator" },
|
|
483
|
+
{
|
|
484
|
+
type: "item",
|
|
485
|
+
item: {
|
|
486
|
+
value: "view-all",
|
|
487
|
+
label: "View All Projects",
|
|
488
|
+
icon: <Icon type="heroicons" name="folder-open" className="size-4" />,
|
|
489
|
+
},
|
|
490
|
+
},
|
|
491
|
+
];
|
|
492
|
+
|
|
493
|
+
return (
|
|
494
|
+
<div>
|
|
495
|
+
<p className="typography-small4 text-text-g-contrast-medium mb-2">
|
|
496
|
+
Mix of custom render + regular items
|
|
497
|
+
</p>
|
|
498
|
+
<Menu
|
|
499
|
+
trigger={
|
|
500
|
+
<Button variant="outline">
|
|
501
|
+
Projects <ChevronDownIcon className="size-4 ml-2" />
|
|
502
|
+
</Button>
|
|
503
|
+
}
|
|
504
|
+
items={items}
|
|
505
|
+
onSelect={(v) => console.log("selected:", v)}
|
|
506
|
+
/>
|
|
507
|
+
</div>
|
|
508
|
+
);
|
|
509
|
+
},
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
// ---------------------------------------------------------------------------
|
|
513
|
+
// With Submenu — type="submenu" uses DropdownMenuSub natively
|
|
514
|
+
// ---------------------------------------------------------------------------
|
|
515
|
+
export const WithSubmenu: StoryObj<typeof Menu> = {
|
|
516
|
+
name: "With Submenu",
|
|
517
|
+
render: () => {
|
|
518
|
+
const [selected, setSelected] = useState<string | null>(null);
|
|
519
|
+
const [permission, setPermission] = useState("perm-view");
|
|
520
|
+
|
|
521
|
+
const items: MenuItemType[] = [
|
|
522
|
+
{
|
|
523
|
+
type: "item",
|
|
524
|
+
item: {
|
|
525
|
+
value: "new-file",
|
|
526
|
+
label: "New File",
|
|
527
|
+
icon: (
|
|
528
|
+
<Icon type="heroicons" name="document-plus" className="size-4" />
|
|
529
|
+
),
|
|
530
|
+
},
|
|
531
|
+
},
|
|
532
|
+
{
|
|
533
|
+
type: "submenu",
|
|
534
|
+
label: "New From Template",
|
|
535
|
+
icon: (
|
|
536
|
+
<Icon type="heroicons" name="document-duplicate" className="size-4" />
|
|
537
|
+
),
|
|
538
|
+
items: [
|
|
539
|
+
{
|
|
540
|
+
type: "item",
|
|
541
|
+
item: { value: "tpl-blank", label: "Blank Document" },
|
|
542
|
+
},
|
|
543
|
+
{ type: "item", item: { value: "tpl-report", label: "Report" } },
|
|
544
|
+
{
|
|
545
|
+
type: "item",
|
|
546
|
+
item: { value: "tpl-presentation", label: "Presentation" },
|
|
547
|
+
},
|
|
548
|
+
{ type: "separator" },
|
|
549
|
+
{
|
|
550
|
+
type: "item",
|
|
551
|
+
item: {
|
|
552
|
+
value: "tpl-browse",
|
|
553
|
+
label: "Browse Templates…",
|
|
554
|
+
icon: (
|
|
555
|
+
<Icon
|
|
556
|
+
type="heroicons"
|
|
557
|
+
name="arrow-top-right-on-square"
|
|
558
|
+
className="size-4"
|
|
559
|
+
/>
|
|
560
|
+
),
|
|
561
|
+
},
|
|
562
|
+
},
|
|
563
|
+
],
|
|
564
|
+
},
|
|
565
|
+
{ type: "separator" },
|
|
566
|
+
{
|
|
567
|
+
type: "submenu",
|
|
568
|
+
label: "Share",
|
|
569
|
+
icon: <Icon type="heroicons" name="share" className="size-4" />,
|
|
570
|
+
items: [
|
|
571
|
+
{
|
|
572
|
+
type: "item",
|
|
573
|
+
item: {
|
|
574
|
+
value: "share-link",
|
|
575
|
+
label: "Copy Link",
|
|
576
|
+
icon: <Icon type="heroicons" name="link" className="size-4" />,
|
|
577
|
+
},
|
|
578
|
+
},
|
|
579
|
+
{
|
|
580
|
+
type: "item",
|
|
581
|
+
item: {
|
|
582
|
+
value: "share-email",
|
|
583
|
+
label: "Send via Email",
|
|
584
|
+
icon: (
|
|
585
|
+
<Icon type="heroicons" name="envelope" className="size-4" />
|
|
586
|
+
),
|
|
587
|
+
},
|
|
588
|
+
},
|
|
589
|
+
{ type: "separator" },
|
|
590
|
+
{
|
|
591
|
+
// Nested submenu inside submenu
|
|
592
|
+
type: "submenu",
|
|
593
|
+
label: "Permissions",
|
|
594
|
+
items: [
|
|
595
|
+
{
|
|
596
|
+
type: "item",
|
|
597
|
+
item: {
|
|
598
|
+
value: "perm-view",
|
|
599
|
+
label: "Viewer",
|
|
600
|
+
type: "radio",
|
|
601
|
+
},
|
|
602
|
+
},
|
|
603
|
+
{
|
|
604
|
+
type: "item",
|
|
605
|
+
item: {
|
|
606
|
+
value: "perm-comment",
|
|
607
|
+
label: "Commenter",
|
|
608
|
+
type: "radio",
|
|
609
|
+
},
|
|
610
|
+
},
|
|
611
|
+
{
|
|
612
|
+
type: "item",
|
|
613
|
+
item: {
|
|
614
|
+
value: "perm-edit",
|
|
615
|
+
label: "Editor",
|
|
616
|
+
type: "radio",
|
|
617
|
+
},
|
|
618
|
+
},
|
|
619
|
+
],
|
|
620
|
+
},
|
|
621
|
+
],
|
|
622
|
+
},
|
|
623
|
+
{ type: "separator" },
|
|
624
|
+
{
|
|
625
|
+
type: "item",
|
|
626
|
+
item: {
|
|
627
|
+
value: "delete",
|
|
628
|
+
label: "Move to Trash",
|
|
629
|
+
icon: <Icon type="heroicons" name="trash" className="size-4" />,
|
|
630
|
+
danger: true,
|
|
631
|
+
},
|
|
632
|
+
},
|
|
633
|
+
];
|
|
634
|
+
|
|
635
|
+
const handleSelect = (value: string) => {
|
|
636
|
+
if (value.startsWith("perm-")) setPermission(value);
|
|
637
|
+
else setSelected(value);
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
return (
|
|
641
|
+
<div className="flex gap-8 items-start">
|
|
642
|
+
<div>
|
|
643
|
+
<p className="typography-small4 text-text-g-contrast-medium mb-2">
|
|
644
|
+
Submenu (hover to reveal) — 3 levels deep
|
|
645
|
+
</p>
|
|
646
|
+
<Menu
|
|
647
|
+
trigger={<Button variant="outline">File</Button>}
|
|
648
|
+
items={items}
|
|
649
|
+
selectedValues={[permission]}
|
|
650
|
+
onSelect={handleSelect}
|
|
651
|
+
/>
|
|
652
|
+
</div>
|
|
653
|
+
<div className="text-sm text-text-g-contrast-medium">
|
|
654
|
+
<p className="font-semibold mb-1">State:</p>
|
|
655
|
+
<pre className="text-xs">
|
|
656
|
+
{JSON.stringify({ selected, permission }, null, 2)}
|
|
657
|
+
</pre>
|
|
658
|
+
</div>
|
|
659
|
+
</div>
|
|
660
|
+
);
|
|
661
|
+
},
|
|
662
|
+
};
|
|
663
|
+
|
|
664
|
+
// ---------------------------------------------------------------------------
|
|
665
|
+
// Complex — mixed types, multi-section
|
|
666
|
+
// ---------------------------------------------------------------------------
|
|
667
|
+
export const ComplexMenu: StoryObj<typeof Menu> = {
|
|
668
|
+
name: "Complex (Mixed Types)",
|
|
669
|
+
render: () => {
|
|
670
|
+
const [preferences, setPreferences] = useState({
|
|
671
|
+
notifications: true,
|
|
672
|
+
emails: false,
|
|
673
|
+
});
|
|
674
|
+
const [theme, setTheme] = useState("light");
|
|
675
|
+
|
|
676
|
+
const items: MenuItemType[] = [
|
|
677
|
+
{ type: "label", label: "My Account" },
|
|
678
|
+
{
|
|
679
|
+
type: "item",
|
|
680
|
+
item: {
|
|
681
|
+
value: "profile",
|
|
682
|
+
label: "Profile",
|
|
683
|
+
icon: <Icon type="heroicons" name="user" className="size-4" />,
|
|
684
|
+
onClick: () => console.log("Go to profile"),
|
|
685
|
+
},
|
|
686
|
+
},
|
|
687
|
+
{
|
|
688
|
+
type: "item",
|
|
689
|
+
item: {
|
|
690
|
+
value: "billing",
|
|
691
|
+
label: "Billing",
|
|
692
|
+
icon: <Icon type="heroicons" name="credit-card" className="size-4" />,
|
|
693
|
+
onClick: () => console.log("Go to billing"),
|
|
694
|
+
},
|
|
695
|
+
},
|
|
696
|
+
{ type: "separator" },
|
|
697
|
+
{ type: "label", label: "Notifications" },
|
|
698
|
+
{
|
|
699
|
+
type: "item",
|
|
700
|
+
item: {
|
|
701
|
+
value: "notifications",
|
|
702
|
+
label: "Push Notifications",
|
|
703
|
+
type: "checkbox",
|
|
704
|
+
checked: preferences.notifications,
|
|
705
|
+
},
|
|
706
|
+
},
|
|
707
|
+
{
|
|
708
|
+
type: "item",
|
|
709
|
+
item: {
|
|
710
|
+
value: "emails",
|
|
711
|
+
label: "Email Notifications",
|
|
712
|
+
type: "checkbox",
|
|
713
|
+
checked: preferences.emails,
|
|
714
|
+
},
|
|
715
|
+
},
|
|
716
|
+
{ type: "separator" },
|
|
717
|
+
{ type: "label", label: "Theme" },
|
|
718
|
+
{
|
|
719
|
+
type: "item",
|
|
720
|
+
item: { value: "light", label: "Light Mode", type: "radio" },
|
|
721
|
+
},
|
|
722
|
+
{
|
|
723
|
+
type: "item",
|
|
724
|
+
item: { value: "dark", label: "Dark Mode", type: "radio" },
|
|
725
|
+
},
|
|
726
|
+
{
|
|
727
|
+
type: "item",
|
|
728
|
+
item: { value: "system", label: "System", type: "radio" },
|
|
729
|
+
},
|
|
730
|
+
{ type: "separator" },
|
|
731
|
+
{
|
|
732
|
+
type: "item",
|
|
733
|
+
item: {
|
|
734
|
+
value: "logout",
|
|
735
|
+
label: "Logout",
|
|
736
|
+
icon: (
|
|
737
|
+
<Icon
|
|
738
|
+
type="heroicons"
|
|
739
|
+
name="arrow-right-on-rectangle"
|
|
740
|
+
className="size-4"
|
|
741
|
+
/>
|
|
742
|
+
),
|
|
743
|
+
danger: true,
|
|
744
|
+
onClick: () => console.log("Logout"),
|
|
745
|
+
},
|
|
746
|
+
},
|
|
747
|
+
];
|
|
748
|
+
|
|
749
|
+
const handleSelect = (value: string, item: { type?: string }) => {
|
|
750
|
+
if (item.type === "checkbox") {
|
|
751
|
+
const key = value as keyof typeof preferences;
|
|
752
|
+
if (key in preferences)
|
|
753
|
+
setPreferences((prev) => ({ ...prev, [key]: !prev[key] }));
|
|
754
|
+
} else if (item.type === "radio") {
|
|
755
|
+
setTheme(value);
|
|
756
|
+
}
|
|
757
|
+
};
|
|
758
|
+
|
|
759
|
+
return (
|
|
760
|
+
<div className="flex gap-8 items-start">
|
|
761
|
+
<div>
|
|
762
|
+
<p className="typography-small4 text-text-g-contrast-medium mb-2">
|
|
763
|
+
Account menu — all item types combined
|
|
764
|
+
</p>
|
|
765
|
+
<Menu
|
|
766
|
+
trigger={
|
|
767
|
+
<Button variant="outline">
|
|
768
|
+
Account <ChevronDownIcon className="size-4 ml-2" />
|
|
769
|
+
</Button>
|
|
770
|
+
}
|
|
771
|
+
items={items}
|
|
772
|
+
selectedValues={[theme]}
|
|
773
|
+
onSelect={handleSelect}
|
|
774
|
+
/>
|
|
775
|
+
</div>
|
|
776
|
+
<div className="text-sm text-text-g-contrast-medium">
|
|
777
|
+
<p className="font-semibold mb-1">State:</p>
|
|
778
|
+
<pre className="text-xs">
|
|
779
|
+
{JSON.stringify({ preferences, theme }, null, 2)}
|
|
780
|
+
</pre>
|
|
781
|
+
</div>
|
|
782
|
+
</div>
|
|
783
|
+
);
|
|
784
|
+
},
|
|
785
|
+
};
|
|
786
|
+
|
|
787
|
+
// ---------------------------------------------------------------------------
|
|
788
|
+
// Multi-Select via Checkbox
|
|
789
|
+
// ---------------------------------------------------------------------------
|
|
790
|
+
export const MultiSelectPattern: StoryObj<typeof Menu> = {
|
|
791
|
+
name: "Multi-Select Pattern",
|
|
792
|
+
render: () => {
|
|
793
|
+
const [selectedValues, setSelectedValues] = useState<string[]>(["react"]);
|
|
794
|
+
|
|
795
|
+
const options = [
|
|
796
|
+
{ value: "react", label: "React" },
|
|
797
|
+
{ value: "vue", label: "Vue" },
|
|
798
|
+
{ value: "angular", label: "Angular" },
|
|
799
|
+
{ value: "svelte", label: "Svelte" },
|
|
800
|
+
{ value: "solid", label: "Solid" },
|
|
801
|
+
];
|
|
802
|
+
|
|
803
|
+
const items: MenuItemType[] = options.map((opt) => ({
|
|
804
|
+
type: "item",
|
|
805
|
+
item: {
|
|
806
|
+
value: opt.value,
|
|
807
|
+
label: opt.label,
|
|
808
|
+
type: "checkbox" as const,
|
|
809
|
+
checked: selectedValues.includes(opt.value),
|
|
810
|
+
},
|
|
811
|
+
}));
|
|
812
|
+
|
|
813
|
+
const displayText =
|
|
814
|
+
selectedValues.length > 0
|
|
815
|
+
? selectedValues
|
|
816
|
+
.map((v) => options.find((o) => o.value === v)?.label)
|
|
817
|
+
.join(", ")
|
|
818
|
+
: "Select frameworks…";
|
|
819
|
+
|
|
820
|
+
return (
|
|
821
|
+
<div className="flex gap-8 items-start">
|
|
822
|
+
<Menu
|
|
823
|
+
trigger={
|
|
824
|
+
<Button variant="outline" className="w-64 justify-between">
|
|
825
|
+
<span className="truncate">{displayText}</span>
|
|
826
|
+
<ChevronDownIcon className="size-4 ml-2 shrink-0" />
|
|
827
|
+
</Button>
|
|
828
|
+
}
|
|
829
|
+
items={items}
|
|
830
|
+
onSelect={(value) => {
|
|
831
|
+
setSelectedValues((prev) =>
|
|
832
|
+
prev.includes(value)
|
|
833
|
+
? prev.filter((v) => v !== value)
|
|
834
|
+
: [...prev, value],
|
|
835
|
+
);
|
|
836
|
+
}}
|
|
837
|
+
/>
|
|
838
|
+
<div className="text-sm text-text-g-contrast-medium">
|
|
839
|
+
<p className="font-semibold mb-1">Selected:</p>
|
|
840
|
+
<pre className="text-xs">
|
|
841
|
+
{JSON.stringify(selectedValues, null, 2)}
|
|
842
|
+
</pre>
|
|
843
|
+
</div>
|
|
844
|
+
</div>
|
|
845
|
+
);
|
|
846
|
+
},
|
|
847
|
+
};
|
|
848
|
+
|
|
849
|
+
// ---------------------------------------------------------------------------
|
|
850
|
+
// Figma: "Change Status" — header slot + section labels + custom badge items
|
|
851
|
+
// ---------------------------------------------------------------------------
|
|
852
|
+
export const ChangeStatus: StoryObj<typeof Menu> = {
|
|
853
|
+
name: "Figma: Change Status",
|
|
854
|
+
render: () => {
|
|
855
|
+
const [status, setStatus] = useState("todo");
|
|
856
|
+
const [open, setOpen] = useState(false);
|
|
857
|
+
|
|
858
|
+
const statusItems: MenuItemType[] = [
|
|
859
|
+
{ type: "label", label: "To do" },
|
|
860
|
+
{
|
|
861
|
+
type: "item",
|
|
862
|
+
item: { value: "todo", label: "To do", type: "radio" },
|
|
863
|
+
},
|
|
864
|
+
{ type: "separator" },
|
|
865
|
+
{ type: "label", label: "In progress" },
|
|
866
|
+
{
|
|
867
|
+
type: "item",
|
|
868
|
+
item: {
|
|
869
|
+
value: "in-progress",
|
|
870
|
+
label: "In Progress",
|
|
871
|
+
type: "radio",
|
|
872
|
+
icon: (
|
|
873
|
+
<span className="inline-flex items-center px-3 py-0.5 rounded-lg text-xs font-medium bg-yellow-400/10 text-yellow-400">
|
|
874
|
+
In Progress
|
|
875
|
+
</span>
|
|
876
|
+
),
|
|
877
|
+
},
|
|
878
|
+
},
|
|
879
|
+
{
|
|
880
|
+
type: "item",
|
|
881
|
+
item: {
|
|
882
|
+
value: "ready-review",
|
|
883
|
+
label: "Ready to review",
|
|
884
|
+
type: "radio",
|
|
885
|
+
icon: (
|
|
886
|
+
<span className="inline-flex items-center px-3 py-0.5 rounded-lg text-xs font-medium bg-blue-400/10 text-blue-400">
|
|
887
|
+
Ready to review
|
|
888
|
+
</span>
|
|
889
|
+
),
|
|
890
|
+
},
|
|
891
|
+
},
|
|
892
|
+
{
|
|
893
|
+
type: "item",
|
|
894
|
+
item: {
|
|
895
|
+
value: "in-review",
|
|
896
|
+
label: "In review",
|
|
897
|
+
type: "radio",
|
|
898
|
+
icon: (
|
|
899
|
+
<span className="inline-flex items-center px-3 py-0.5 rounded-lg text-xs font-medium bg-red-400/10 text-red-400">
|
|
900
|
+
In review
|
|
901
|
+
</span>
|
|
902
|
+
),
|
|
903
|
+
},
|
|
904
|
+
},
|
|
905
|
+
{ type: "separator" },
|
|
906
|
+
{ type: "label", label: "Completed" },
|
|
907
|
+
{
|
|
908
|
+
type: "item",
|
|
909
|
+
item: {
|
|
910
|
+
value: "completed",
|
|
911
|
+
label: "Completed",
|
|
912
|
+
type: "radio",
|
|
913
|
+
icon: (
|
|
914
|
+
<span className="inline-flex items-center px-3 py-0.5 rounded-lg text-xs font-medium bg-green-400/10 text-green-400">
|
|
915
|
+
Completed
|
|
916
|
+
</span>
|
|
917
|
+
),
|
|
918
|
+
},
|
|
919
|
+
},
|
|
920
|
+
];
|
|
921
|
+
|
|
922
|
+
return (
|
|
923
|
+
<div className="flex gap-8 items-start">
|
|
924
|
+
<div>
|
|
925
|
+
<p className="typography-small4 text-text-g-contrast-medium mb-2">
|
|
926
|
+
With header (title + settings + close)
|
|
927
|
+
</p>
|
|
928
|
+
<Menu
|
|
929
|
+
trigger={<Button variant="outline">Change Status</Button>}
|
|
930
|
+
open={open}
|
|
931
|
+
onOpenChange={setOpen}
|
|
932
|
+
header={
|
|
933
|
+
<div className="flex items-center justify-between px-6 py-3">
|
|
934
|
+
<span className="typography-subtitle4 text-text-g-contrast-high">
|
|
935
|
+
Status
|
|
936
|
+
</span>
|
|
937
|
+
<div className="flex items-center gap-1">
|
|
938
|
+
<ActionButton variant="icon" size="sm">
|
|
939
|
+
<Icon
|
|
940
|
+
type="heroicons"
|
|
941
|
+
name="cog6-tooth"
|
|
942
|
+
className="size-4"
|
|
943
|
+
/>
|
|
944
|
+
</ActionButton>
|
|
945
|
+
{/* X ปิด menu ได้ผ่าน onOpenChange */}
|
|
946
|
+
<ActionButton
|
|
947
|
+
variant="icon"
|
|
948
|
+
size="sm"
|
|
949
|
+
onClick={() => setOpen(false)}
|
|
950
|
+
>
|
|
951
|
+
<Icon type="heroicons" name="xmark" className="size-4" />
|
|
952
|
+
</ActionButton>
|
|
953
|
+
</div>
|
|
954
|
+
</div>
|
|
955
|
+
}
|
|
956
|
+
items={statusItems}
|
|
957
|
+
selectedValues={[status]}
|
|
958
|
+
onSelect={(v) => setStatus(v)}
|
|
959
|
+
/>
|
|
960
|
+
</div>
|
|
961
|
+
|
|
962
|
+
<div>
|
|
963
|
+
<p className="typography-small4 text-text-g-contrast-medium mb-2">
|
|
964
|
+
No header (simple list)
|
|
965
|
+
</p>
|
|
966
|
+
<Menu
|
|
967
|
+
trigger={<Button variant="outline">Status</Button>}
|
|
968
|
+
items={statusItems}
|
|
969
|
+
selectedValues={[status]}
|
|
970
|
+
onSelect={(v) => setStatus(v)}
|
|
971
|
+
/>
|
|
972
|
+
</div>
|
|
973
|
+
|
|
974
|
+
<div className="text-sm text-text-g-contrast-medium">
|
|
975
|
+
<p className="font-semibold mb-1">Selected:</p>
|
|
976
|
+
<pre className="text-xs">{status}</pre>
|
|
977
|
+
</div>
|
|
978
|
+
</div>
|
|
979
|
+
);
|
|
980
|
+
},
|
|
981
|
+
};
|
|
982
|
+
|
|
983
|
+
// ---------------------------------------------------------------------------
|
|
984
|
+
// Figma: "Manage Column" — header with actions + toggle rows + drag handle
|
|
985
|
+
// ---------------------------------------------------------------------------
|
|
986
|
+
export const ManageColumn: StoryObj<typeof Menu> = {
|
|
987
|
+
name: "Figma: Manage Column",
|
|
988
|
+
render: () => {
|
|
989
|
+
const [open, setOpen] = useState(false);
|
|
990
|
+
const [columns, setColumns] = useState([
|
|
991
|
+
{ id: "name", label: "Name", visible: true },
|
|
992
|
+
{ id: "status", label: "Status", visible: true },
|
|
993
|
+
{ id: "assignee", label: "Assignee", visible: false },
|
|
994
|
+
{ id: "due-date", label: "Due date", visible: true },
|
|
995
|
+
{ id: "priority", label: "Priority", visible: false },
|
|
996
|
+
]);
|
|
997
|
+
|
|
998
|
+
const toggleColumn = (id: string) =>
|
|
999
|
+
setColumns((prev) =>
|
|
1000
|
+
prev.map((col) =>
|
|
1001
|
+
col.id === id ? { ...col, visible: !col.visible } : col,
|
|
1002
|
+
),
|
|
1003
|
+
);
|
|
1004
|
+
|
|
1005
|
+
const showAll = () =>
|
|
1006
|
+
setColumns((prev) => prev.map((col) => ({ ...col, visible: true })));
|
|
1007
|
+
|
|
1008
|
+
const hideAll = () =>
|
|
1009
|
+
setColumns((prev) => prev.map((col) => ({ ...col, visible: false })));
|
|
1010
|
+
|
|
1011
|
+
const items: MenuItemType[] = columns.map((col) => ({
|
|
1012
|
+
type: "custom",
|
|
1013
|
+
render: () => (
|
|
1014
|
+
<div
|
|
1015
|
+
key={col.id}
|
|
1016
|
+
className="flex items-center gap-3 px-4 py-3 hover:bg-[var(--dropdown-menu-hover-bg)] cursor-default transition-colors"
|
|
1017
|
+
>
|
|
1018
|
+
<Icon
|
|
1019
|
+
type="heroicons"
|
|
1020
|
+
name="bars-2"
|
|
1021
|
+
className="size-4 text-text-g-contrast-medium shrink-0 cursor-grab"
|
|
1022
|
+
/>
|
|
1023
|
+
<Switch
|
|
1024
|
+
checked={col.visible}
|
|
1025
|
+
onCheckedChange={() => toggleColumn(col.id)}
|
|
1026
|
+
/>
|
|
1027
|
+
<span className="typography-subtitle4 text-text-g-contrast-high flex-1">
|
|
1028
|
+
{col.label}
|
|
1029
|
+
</span>
|
|
1030
|
+
</div>
|
|
1031
|
+
),
|
|
1032
|
+
}));
|
|
1033
|
+
|
|
1034
|
+
return (
|
|
1035
|
+
<div className="flex gap-8 items-start">
|
|
1036
|
+
<div>
|
|
1037
|
+
<p className="typography-small4 text-text-g-contrast-medium mb-2">
|
|
1038
|
+
Manage Column panel
|
|
1039
|
+
</p>
|
|
1040
|
+
<Menu
|
|
1041
|
+
trigger={
|
|
1042
|
+
<Button variant="outline">
|
|
1043
|
+
<Icon
|
|
1044
|
+
type="heroicons"
|
|
1045
|
+
name="view-columns"
|
|
1046
|
+
className="size-4 mr-2"
|
|
1047
|
+
/>
|
|
1048
|
+
Manage Columns
|
|
1049
|
+
</Button>
|
|
1050
|
+
}
|
|
1051
|
+
open={open}
|
|
1052
|
+
onOpenChange={setOpen}
|
|
1053
|
+
header={
|
|
1054
|
+
<div className="flex items-center justify-between px-4 py-3">
|
|
1055
|
+
<span className="typography-subtitle4 text-text-g-contrast-high">
|
|
1056
|
+
Manage column
|
|
1057
|
+
</span>
|
|
1058
|
+
<div className="flex items-center gap-2">
|
|
1059
|
+
<button
|
|
1060
|
+
className="typography-small4 text-text-g-contrast-medium hover:text-text-g-contrast-high transition-colors"
|
|
1061
|
+
onClick={hideAll}
|
|
1062
|
+
>
|
|
1063
|
+
Hide all
|
|
1064
|
+
</button>
|
|
1065
|
+
<button
|
|
1066
|
+
className="typography-small4 text-[var(--dropdown-menu-checkbox-checked-bg)] hover:opacity-80 transition-opacity"
|
|
1067
|
+
onClick={showAll}
|
|
1068
|
+
>
|
|
1069
|
+
Show all
|
|
1070
|
+
</button>
|
|
1071
|
+
{/* Done ปิด menu ได้ผ่าน onOpenChange */}
|
|
1072
|
+
<Button
|
|
1073
|
+
size="sm"
|
|
1074
|
+
variant="outline"
|
|
1075
|
+
onClick={() => setOpen(false)}
|
|
1076
|
+
>
|
|
1077
|
+
Done
|
|
1078
|
+
</Button>
|
|
1079
|
+
</div>
|
|
1080
|
+
</div>
|
|
1081
|
+
}
|
|
1082
|
+
items={items}
|
|
1083
|
+
contentClassName="w-80"
|
|
1084
|
+
/>
|
|
1085
|
+
</div>
|
|
1086
|
+
|
|
1087
|
+
<div className="text-sm text-text-g-contrast-medium">
|
|
1088
|
+
<p className="font-semibold mb-1">Columns:</p>
|
|
1089
|
+
<pre className="text-xs">
|
|
1090
|
+
{JSON.stringify(
|
|
1091
|
+
columns.map((c) => ({ [c.label]: c.visible })),
|
|
1092
|
+
null,
|
|
1093
|
+
2,
|
|
1094
|
+
)}
|
|
1095
|
+
</pre>
|
|
1096
|
+
</div>
|
|
1097
|
+
</div>
|
|
1098
|
+
);
|
|
1099
|
+
},
|
|
1100
|
+
};
|