@shipfox/react-ui 0.10.0 → 0.11.0

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.
Files changed (35) hide show
  1. package/.turbo/turbo-build.log +5 -5
  2. package/.turbo/turbo-check.log +2 -2
  3. package/.turbo/turbo-type.log +1 -1
  4. package/CHANGELOG.md +6 -0
  5. package/dist/components/button/button.js +14 -11
  6. package/dist/components/button/button.js.map +1 -1
  7. package/dist/components/dropdown-menu/dropdown-menu.d.ts +58 -0
  8. package/dist/components/dropdown-menu/dropdown-menu.d.ts.map +1 -0
  9. package/dist/components/dropdown-menu/dropdown-menu.js +280 -0
  10. package/dist/components/dropdown-menu/dropdown-menu.js.map +1 -0
  11. package/dist/components/dropdown-menu/dropdown-menu.stories.js +462 -0
  12. package/dist/components/dropdown-menu/dropdown-menu.stories.js.map +1 -0
  13. package/dist/components/dropdown-menu/index.d.ts +3 -0
  14. package/dist/components/dropdown-menu/index.d.ts.map +1 -0
  15. package/dist/components/dropdown-menu/index.js +3 -0
  16. package/dist/components/dropdown-menu/index.js.map +1 -0
  17. package/dist/components/dynamic-item/dynamic-item.d.ts +1 -1
  18. package/dist/components/dynamic-item/dynamic-item.d.ts.map +1 -1
  19. package/dist/components/dynamic-item/dynamic-item.js +4 -4
  20. package/dist/components/dynamic-item/dynamic-item.js.map +1 -1
  21. package/dist/components/dynamic-item/dynamic-item.stories.js +11 -1
  22. package/dist/components/dynamic-item/dynamic-item.stories.js.map +1 -1
  23. package/dist/components/index.d.ts +1 -0
  24. package/dist/components/index.d.ts.map +1 -1
  25. package/dist/components/index.js +1 -0
  26. package/dist/components/index.js.map +1 -1
  27. package/dist/styles.css +1 -1
  28. package/package.json +3 -3
  29. package/src/components/button/button.tsx +12 -12
  30. package/src/components/dropdown-menu/dropdown-menu.stories.tsx +384 -0
  31. package/src/components/dropdown-menu/dropdown-menu.tsx +416 -0
  32. package/src/components/dropdown-menu/index.ts +29 -0
  33. package/src/components/dynamic-item/dynamic-item.stories.tsx +6 -1
  34. package/src/components/dynamic-item/dynamic-item.tsx +9 -3
  35. package/src/components/index.ts +1 -0
@@ -0,0 +1,384 @@
1
+ import {argosScreenshot} from '@argos-ci/storybook/vitest';
2
+ import type {Meta, StoryObj} from '@storybook/react';
3
+ import {screen, within} from '@testing-library/react';
4
+ import type {UserEvent} from '@testing-library/user-event';
5
+ import userEvent from '@testing-library/user-event';
6
+ import {useState} from 'react';
7
+ import {Avatar} from '../avatar';
8
+ import {Button} from '../button';
9
+ import {
10
+ DropdownMenu,
11
+ DropdownMenuCheckboxItem,
12
+ DropdownMenuContent,
13
+ DropdownMenuGroup,
14
+ DropdownMenuItem,
15
+ DropdownMenuLabel,
16
+ DropdownMenuRadioGroup,
17
+ DropdownMenuRadioItem,
18
+ DropdownMenuSeparator,
19
+ DropdownMenuSub,
20
+ DropdownMenuSubContent,
21
+ DropdownMenuSubTrigger,
22
+ DropdownMenuTrigger,
23
+ } from './dropdown-menu';
24
+
25
+ const OPEN_MENU_REGEX = /open menu/i;
26
+ const ACTIONS_REGEX = /actions/i;
27
+ const ORGANIZATION_REGEX = /organization/i;
28
+ const COMPLETE_MENU_REGEX = /complete menu/i;
29
+ const SWITCH_ORGANIZATION_REGEX = /switch organization/i;
30
+
31
+ const isTestEnvironment = () => typeof navigator !== 'undefined' && navigator.webdriver === true;
32
+
33
+ type StoryContext = Parameters<NonNullable<Story['play']>>[0];
34
+ type AdditionalStepsCallback = (ctx: StoryContext, user: UserEvent) => Promise<void>;
35
+
36
+ async function openMenuAndScreenshot(
37
+ ctx: StoryContext,
38
+ triggerRegex: RegExp,
39
+ screenshotName: string,
40
+ additionalSteps?: AdditionalStepsCallback,
41
+ ): Promise<void> {
42
+ const {canvasElement, step} = ctx;
43
+ const canvas = within(canvasElement);
44
+ const user = userEvent.setup();
45
+
46
+ let triggerButton: HTMLElement | null = null;
47
+
48
+ await step('Open the dropdown menu', async () => {
49
+ triggerButton = canvas.getByRole('button', {name: triggerRegex});
50
+ await user.click(triggerButton);
51
+ });
52
+
53
+ await step('Wait for menu to appear and render', async () => {
54
+ await screen.findByRole('menu');
55
+ await new Promise((resolve) => setTimeout(resolve, 300));
56
+
57
+ if (isTestEnvironment() && triggerButton instanceof HTMLElement) {
58
+ triggerButton.style.display = 'none';
59
+ }
60
+ await new Promise((resolve) => setTimeout(resolve, 100));
61
+ });
62
+
63
+ await argosScreenshot(ctx, screenshotName);
64
+
65
+ if (additionalSteps) {
66
+ await additionalSteps(ctx, user);
67
+ }
68
+ }
69
+
70
+ const meta = {
71
+ title: 'Components/DropdownMenu',
72
+ component: DropdownMenuContent,
73
+ subcomponents: {
74
+ DropdownMenu,
75
+ DropdownMenuTrigger,
76
+ DropdownMenuItem,
77
+ DropdownMenuCheckboxItem,
78
+ DropdownMenuRadioGroup,
79
+ DropdownMenuRadioItem,
80
+ DropdownMenuLabel,
81
+ DropdownMenuSeparator,
82
+ DropdownMenuGroup,
83
+ DropdownMenuSub,
84
+ DropdownMenuSubTrigger,
85
+ DropdownMenuSubContent,
86
+ },
87
+ parameters: {
88
+ layout: 'centered',
89
+ },
90
+ tags: ['autodocs'],
91
+ argTypes: {
92
+ side: {
93
+ control: 'select',
94
+ options: ['top', 'right', 'bottom', 'left'],
95
+ description: 'The preferred side of the trigger to render against',
96
+ table: {defaultValue: {summary: 'bottom'}},
97
+ },
98
+ align: {
99
+ control: 'select',
100
+ options: ['start', 'center', 'end'],
101
+ description: 'The preferred alignment against the trigger',
102
+ table: {defaultValue: {summary: 'start'}},
103
+ },
104
+ sideOffset: {
105
+ control: {type: 'number', min: 0, max: 20, step: 1},
106
+ description: 'Distance in pixels from the trigger',
107
+ table: {defaultValue: {summary: '4'}},
108
+ },
109
+ alignOffset: {
110
+ control: {type: 'number', min: -20, max: 20, step: 1},
111
+ description: 'Offset in pixels from the alignment edge',
112
+ table: {defaultValue: {summary: '0'}},
113
+ },
114
+ size: {
115
+ control: 'select',
116
+ options: ['sm', 'md', 'lg'],
117
+ description: 'Size variant of the dropdown content',
118
+ table: {defaultValue: {summary: 'md'}},
119
+ },
120
+ },
121
+ args: {
122
+ side: 'bottom',
123
+ align: 'center',
124
+ sideOffset: 8,
125
+ alignOffset: 0,
126
+ size: 'md',
127
+ },
128
+ } satisfies Meta<typeof DropdownMenuContent>;
129
+
130
+ export default meta;
131
+ type Story = StoryObj<typeof meta>;
132
+
133
+ export const Default: Story = {
134
+ play: (ctx) => openMenuAndScreenshot(ctx, OPEN_MENU_REGEX, 'DropdownMenu Default Open'),
135
+ render: function DefaultStory(args) {
136
+ const [container, setContainer] = useState<HTMLDivElement | null>(null);
137
+
138
+ return (
139
+ <div
140
+ ref={setContainer}
141
+ className="relative flex h-500 w-500 items-center justify-center rounded-16 bg-background-subtle-base shadow-tooltip overflow-visible"
142
+ >
143
+ {container && (
144
+ <DropdownMenu>
145
+ <DropdownMenuTrigger asChild>
146
+ <Button variant="secondary">Open Menu</Button>
147
+ </DropdownMenuTrigger>
148
+ <DropdownMenuContent {...args} container={container} side="bottom" align="center">
149
+ <DropdownMenuItem icon="editLine">Edit</DropdownMenuItem>
150
+ <DropdownMenuItem icon="addLine">Add</DropdownMenuItem>
151
+ <DropdownMenuSeparator />
152
+ <DropdownMenuItem icon="deleteBinLine">Delete</DropdownMenuItem>
153
+ </DropdownMenuContent>
154
+ </DropdownMenu>
155
+ )}
156
+ </div>
157
+ );
158
+ },
159
+ };
160
+
161
+ export const WithShortcuts: Story = {
162
+ play: (ctx) => openMenuAndScreenshot(ctx, ACTIONS_REGEX, 'DropdownMenu With Shortcuts Open'),
163
+ render: function WithShortcutsStory(args) {
164
+ const [container, setContainer] = useState<HTMLDivElement | null>(null);
165
+
166
+ return (
167
+ <div
168
+ ref={setContainer}
169
+ className="relative flex h-500 w-500 items-center justify-center rounded-16 bg-background-subtle-base shadow-tooltip overflow-visible"
170
+ >
171
+ {container && (
172
+ <DropdownMenu>
173
+ <DropdownMenuTrigger asChild>
174
+ <Button variant="secondary">Actions</Button>
175
+ </DropdownMenuTrigger>
176
+ <DropdownMenuContent {...args} container={container} side="bottom" align="center">
177
+ <DropdownMenuItem icon="fileCopyLine" shortcut="⌘C">
178
+ Copy
179
+ </DropdownMenuItem>
180
+ <DropdownMenuItem icon="clipboardLine" shortcut="⌘V">
181
+ Paste
182
+ </DropdownMenuItem>
183
+ <DropdownMenuSeparator />
184
+ <DropdownMenuItem icon="deleteBinLine" shortcut="⌘⌫">
185
+ Delete
186
+ </DropdownMenuItem>
187
+ </DropdownMenuContent>
188
+ </DropdownMenu>
189
+ )}
190
+ </div>
191
+ );
192
+ },
193
+ };
194
+
195
+ function UserProfileSection() {
196
+ return (
197
+ <div className="flex items-center gap-8 px-8 py-6">
198
+ <Avatar
199
+ size="sm"
200
+ content="image"
201
+ src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=100&h=100&fit=crop&crop=faces"
202
+ fallback="John Doe"
203
+ />
204
+ <div className="flex flex-col">
205
+ <span className="text-sm font-medium leading-20 text-foreground-neutral-base">
206
+ John Doe
207
+ </span>
208
+ <span className="text-xs leading-16 text-foreground-neutral-muted">john@example.com</span>
209
+ </div>
210
+ </div>
211
+ );
212
+ }
213
+
214
+ function OrganizationItem() {
215
+ return (
216
+ <div className="flex items-center gap-8 px-8 py-6">
217
+ <Avatar size="3xs" content="logo" logoName="stripe" radius="rounded" />
218
+ <span className="text-sm leading-20 text-foreground-neutral-subtle">
219
+ Stripe&apos;s organization
220
+ </span>
221
+ </div>
222
+ );
223
+ }
224
+
225
+ export const OrganizationMenu: Story = {
226
+ args: {
227
+ size: 'md',
228
+ },
229
+ play: (ctx) =>
230
+ openMenuAndScreenshot(
231
+ ctx,
232
+ ORGANIZATION_REGEX,
233
+ 'DropdownMenu Organization Menu Open',
234
+ async ({step}, user) => {
235
+ await step('Hover over submenu trigger', async () => {
236
+ const submenuTrigger = screen.getByRole('menuitem', {name: SWITCH_ORGANIZATION_REGEX});
237
+ await user.hover(submenuTrigger);
238
+ await new Promise((resolve) => setTimeout(resolve, 200));
239
+ });
240
+
241
+ await argosScreenshot(ctx, 'DropdownMenu Organization Menu Submenu Open');
242
+ },
243
+ ),
244
+ render: function OrganizationMenuStory(args) {
245
+ const [container, setContainer] = useState<HTMLDivElement | null>(null);
246
+
247
+ return (
248
+ <div
249
+ ref={setContainer}
250
+ className="relative flex h-600 w-600 items-center justify-center rounded-16 bg-background-subtle-base shadow-tooltip overflow-visible"
251
+ >
252
+ {container && (
253
+ <DropdownMenu>
254
+ <DropdownMenuTrigger asChild>
255
+ <Button variant="secondary" iconLeft="buildingLine">
256
+ Organization
257
+ </Button>
258
+ </DropdownMenuTrigger>
259
+ <DropdownMenuContent {...args} container={container} side="bottom" align="start">
260
+ <OrganizationItem />
261
+ <DropdownMenuSeparator />
262
+ <DropdownMenuItem icon="settings3Line">Settings</DropdownMenuItem>
263
+ <DropdownMenuSub>
264
+ <DropdownMenuSubTrigger icon="arrowLeftRightLine">
265
+ Switch organization
266
+ </DropdownMenuSubTrigger>
267
+ <DropdownMenuSubContent>
268
+ <DropdownMenuItem
269
+ icon="shipfox"
270
+ iconStyle="text-foreground-neutral-base"
271
+ className="text-foreground-neutral-base"
272
+ >
273
+ Shipfox
274
+ </DropdownMenuItem>
275
+ <DropdownMenuItem
276
+ icon="github"
277
+ iconStyle="text-foreground-neutral-base"
278
+ className="text-foreground-neutral-base"
279
+ >
280
+ Github
281
+ </DropdownMenuItem>
282
+ <DropdownMenuItem
283
+ icon="google"
284
+ iconStyle="text-foreground-neutral-base"
285
+ className="text-foreground-neutral-base"
286
+ >
287
+ Google
288
+ </DropdownMenuItem>
289
+ </DropdownMenuSubContent>
290
+ </DropdownMenuSub>
291
+ <DropdownMenuItem icon="addLine">New organization</DropdownMenuItem>
292
+ </DropdownMenuContent>
293
+ </DropdownMenu>
294
+ )}
295
+ </div>
296
+ );
297
+ },
298
+ };
299
+
300
+ export const CompleteExample: Story = {
301
+ args: {
302
+ size: 'lg',
303
+ side: 'bottom',
304
+ },
305
+ play: (ctx) =>
306
+ openMenuAndScreenshot(ctx, COMPLETE_MENU_REGEX, 'DropdownMenu Complete Example Open'),
307
+ render: function CompleteExampleStory(args) {
308
+ const [container, setContainer] = useState<HTMLDivElement | null>(null);
309
+ const [showNotifications, setShowNotifications] = useState(true);
310
+ const [showBadges, setShowBadges] = useState(false);
311
+ const [theme, setTheme] = useState('dark');
312
+
313
+ return (
314
+ <div
315
+ ref={setContainer}
316
+ className="relative flex h-600 w-500 items-center justify-center rounded-16 bg-background-subtle-base shadow-tooltip overflow-visible"
317
+ >
318
+ {container && (
319
+ <DropdownMenu>
320
+ <DropdownMenuTrigger asChild>
321
+ <Button variant="secondary" iconLeft="menu3Line">
322
+ Complete Menu
323
+ </Button>
324
+ </DropdownMenuTrigger>
325
+ <DropdownMenuContent
326
+ {...args}
327
+ className="w-260"
328
+ container={container}
329
+ side="bottom"
330
+ align="start"
331
+ >
332
+ <UserProfileSection />
333
+ <DropdownMenuSeparator />
334
+
335
+ <DropdownMenuLabel>Actions</DropdownMenuLabel>
336
+ <DropdownMenuGroup>
337
+ <DropdownMenuItem icon="sparklingLine">Getting started</DropdownMenuItem>
338
+ <DropdownMenuItem icon="userLine">Profile settings</DropdownMenuItem>
339
+ </DropdownMenuGroup>
340
+
341
+ <DropdownMenuSeparator />
342
+
343
+ <DropdownMenuLabel>Preferences</DropdownMenuLabel>
344
+ <DropdownMenuGroup>
345
+ <DropdownMenuCheckboxItem
346
+ checked={showNotifications}
347
+ onCheckedChange={setShowNotifications}
348
+ closeOnSelect={false}
349
+ >
350
+ Show notifications
351
+ </DropdownMenuCheckboxItem>
352
+ <DropdownMenuCheckboxItem
353
+ checked={showBadges}
354
+ onCheckedChange={setShowBadges}
355
+ closeOnSelect={false}
356
+ >
357
+ Show badges
358
+ </DropdownMenuCheckboxItem>
359
+ </DropdownMenuGroup>
360
+
361
+ <DropdownMenuSeparator />
362
+
363
+ <DropdownMenuLabel>Theme</DropdownMenuLabel>
364
+ <DropdownMenuRadioGroup value={theme} onValueChange={setTheme}>
365
+ <DropdownMenuRadioItem value="light" closeOnSelect={false}>
366
+ Light
367
+ </DropdownMenuRadioItem>
368
+ <DropdownMenuRadioItem value="dark" closeOnSelect={false}>
369
+ Dark
370
+ </DropdownMenuRadioItem>
371
+ <DropdownMenuRadioItem value="system" closeOnSelect={false}>
372
+ System
373
+ </DropdownMenuRadioItem>
374
+ </DropdownMenuRadioGroup>
375
+
376
+ <DropdownMenuSeparator />
377
+ <DropdownMenuItem icon="logoutCircleLine">Log out</DropdownMenuItem>
378
+ </DropdownMenuContent>
379
+ </DropdownMenu>
380
+ )}
381
+ </div>
382
+ );
383
+ },
384
+ };