@shipfox/react-ui 0.5.0 → 0.6.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 (63) hide show
  1. package/.storybook/preview.tsx +11 -0
  2. package/.turbo/turbo-build.log +16 -3
  3. package/.turbo/turbo-check.log +2 -2
  4. package/.turbo/turbo-type.log +1 -1
  5. package/CHANGELOG.md +8 -0
  6. package/README.md +16 -0
  7. package/dist/build-css-entry.js +5 -0
  8. package/dist/build-css-entry.js.map +1 -0
  9. package/dist/components/code-block/code-block-footer.d.ts.map +1 -1
  10. package/dist/components/code-block/code-block-footer.js +29 -15
  11. package/dist/components/code-block/code-block-footer.js.map +1 -1
  12. package/dist/components/dynamic-item/dynamic-item.stories.js +1 -1
  13. package/dist/components/dynamic-item/dynamic-item.stories.js.map +1 -1
  14. package/dist/components/icon/icon.d.ts +1 -0
  15. package/dist/components/icon/icon.d.ts.map +1 -1
  16. package/dist/components/icon/icon.js +3 -2
  17. package/dist/components/icon/icon.js.map +1 -1
  18. package/dist/components/index.d.ts +1 -0
  19. package/dist/components/index.d.ts.map +1 -1
  20. package/dist/components/index.js +1 -0
  21. package/dist/components/index.js.map +1 -1
  22. package/dist/components/modal/index.d.ts +3 -0
  23. package/dist/components/modal/index.d.ts.map +1 -0
  24. package/dist/components/modal/index.js +3 -0
  25. package/dist/components/modal/index.js.map +1 -0
  26. package/dist/components/modal/modal.d.ts +37 -0
  27. package/dist/components/modal/modal.d.ts.map +1 -0
  28. package/dist/components/modal/modal.js +262 -0
  29. package/dist/components/modal/modal.js.map +1 -0
  30. package/dist/components/modal/modal.stories.js +497 -0
  31. package/dist/components/modal/modal.stories.js.map +1 -0
  32. package/dist/components/moving-border/index.d.ts +2 -0
  33. package/dist/components/moving-border/index.d.ts.map +1 -0
  34. package/dist/components/moving-border/index.js +3 -0
  35. package/dist/components/moving-border/index.js.map +1 -0
  36. package/dist/components/typography/text.d.ts.map +1 -1
  37. package/dist/components/typography/text.js +1 -1
  38. package/dist/components/typography/text.js.map +1 -1
  39. package/dist/hooks/index.d.ts +1 -0
  40. package/dist/hooks/index.d.ts.map +1 -1
  41. package/dist/hooks/index.js +1 -0
  42. package/dist/hooks/index.js.map +1 -1
  43. package/dist/hooks/useMediaQuery.d.ts +2 -0
  44. package/dist/hooks/useMediaQuery.d.ts.map +1 -0
  45. package/dist/hooks/useMediaQuery.js +74 -0
  46. package/dist/hooks/useMediaQuery.js.map +1 -0
  47. package/dist/styles.css +1 -0
  48. package/index.css +1 -1
  49. package/package.json +11 -7
  50. package/src/build-css-entry.ts +3 -0
  51. package/src/components/code-block/code-block-footer.tsx +37 -30
  52. package/src/components/dynamic-item/dynamic-item.stories.tsx +1 -1
  53. package/src/components/icon/icon.tsx +2 -0
  54. package/src/components/index.ts +1 -0
  55. package/src/components/modal/index.ts +23 -0
  56. package/src/components/modal/modal.stories.tsx +384 -0
  57. package/src/components/modal/modal.tsx +309 -0
  58. package/src/components/moving-border/index.ts +1 -0
  59. package/src/components/typography/text.tsx +9 -1
  60. package/src/hooks/index.ts +1 -0
  61. package/src/hooks/useMediaQuery.ts +87 -0
  62. package/tsconfig.build.json +7 -1
  63. package/vite.css.config.ts +30 -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 userEvent from '@testing-library/user-event';
5
+ import {Button, ButtonLink} from 'components/button';
6
+ import {
7
+ CodeBlock,
8
+ CodeBlockBody,
9
+ CodeBlockContent,
10
+ CodeBlockCopyButton,
11
+ CodeBlockFilename,
12
+ CodeBlockFiles,
13
+ CodeBlockFooter,
14
+ CodeBlockHeader,
15
+ CodeBlockItem,
16
+ } from 'components/code-block';
17
+ import {DynamicItem} from 'components/dynamic-item';
18
+ import {Icon} from 'components/icon';
19
+ import {Input} from 'components/input';
20
+ import {ItemTitle} from 'components/item';
21
+ import {Label} from 'components/label';
22
+ import {MovingBorder} from 'components/moving-border';
23
+ import {Text} from 'components/typography';
24
+ import {useState} from 'react';
25
+ import {cn} from 'utils/cn';
26
+ import illustration2 from '../../assets/illustration-2.svg';
27
+ import illustrationBg from '../../assets/illustration-gradient.svg';
28
+ import {
29
+ Modal,
30
+ ModalBody,
31
+ ModalContent,
32
+ ModalFooter,
33
+ ModalHeader,
34
+ ModalTitle,
35
+ ModalTrigger,
36
+ } from './modal';
37
+
38
+ const OPEN_MODAL_REGEX = /open modal/i;
39
+ const IMPORT_JOBS_REGEX = /import past jobs from github/i;
40
+ const GITHUB_ACTIONS_REGEX = /run github actions on shipfox/i;
41
+
42
+ const meta = {
43
+ title: 'Components/Modal',
44
+ component: Modal,
45
+ tags: ['autodocs'],
46
+ parameters: {
47
+ layout: 'centered',
48
+ },
49
+ } satisfies Meta<typeof Modal>;
50
+
51
+ export default meta;
52
+ type Story = StoryObj<typeof meta>;
53
+
54
+ export const Default: Story = {
55
+ play: async (ctx) => {
56
+ const {canvasElement, step} = ctx;
57
+ const canvas = within(canvasElement);
58
+ const user = userEvent.setup();
59
+
60
+ await step('Open the modal', async () => {
61
+ const triggerButton = canvas.getByRole('button', {name: OPEN_MODAL_REGEX});
62
+ await user.click(triggerButton);
63
+ });
64
+
65
+ await step('Wait for dialog to appear and render', async () => {
66
+ await screen.findByRole('dialog');
67
+ await new Promise((resolve) => setTimeout(resolve, 100));
68
+ });
69
+
70
+ await argosScreenshot(ctx, 'Default Modal Open');
71
+ },
72
+ render: () => {
73
+ const [open, setOpen] = useState(false);
74
+
75
+ return (
76
+ <div className="flex h-[calc(100vh/2)] w-[calc(100vw/2)] items-center justify-center rounded-16 bg-background-subtle-base shadow-tooltip">
77
+ <Modal open={open} onOpenChange={setOpen}>
78
+ <ModalTrigger asChild>
79
+ <Button>Open Modal</Button>
80
+ </ModalTrigger>
81
+ <ModalContent aria-describedby={undefined}>
82
+ <ModalTitle className="sr-only">Modal Title</ModalTitle>
83
+ <ModalHeader>
84
+ <Text
85
+ size="lg"
86
+ className="flex-1 overflow-ellipsis overflow-hidden whitespace-nowrap"
87
+ >
88
+ Modal Title
89
+ </Text>
90
+ </ModalHeader>
91
+ <ModalBody>
92
+ <Text size="sm" className="text-foreground-neutral-subtle w-full">
93
+ This modal automatically adapts between dialog (desktop) and drawer (mobile) based
94
+ on screen size. Try resizing your browser window!
95
+ </Text>
96
+ </ModalBody>
97
+ <ModalFooter>
98
+ <Button variant="transparent" onClick={() => setOpen(false)}>
99
+ Cancel
100
+ </Button>
101
+ <Button variant="primary" onClick={() => setOpen(false)}>
102
+ Confirm
103
+ </Button>
104
+ </ModalFooter>
105
+ </ModalContent>
106
+ </Modal>
107
+ </div>
108
+ );
109
+ },
110
+ };
111
+
112
+ export const ImportForm: Story = {
113
+ play: async (ctx) => {
114
+ const {canvasElement, step} = ctx;
115
+ const canvas = within(canvasElement);
116
+ const user = userEvent.setup();
117
+
118
+ await step('Open the modal', async () => {
119
+ const triggerButton = canvas.getByRole('button', {name: IMPORT_JOBS_REGEX});
120
+ await user.click(triggerButton);
121
+ });
122
+
123
+ await step('Wait for dialog to appear and render', async () => {
124
+ await screen.findByRole('dialog');
125
+ await new Promise((resolve) => setTimeout(resolve, 100));
126
+ });
127
+
128
+ await argosScreenshot(ctx, 'Import Form Modal Open');
129
+ },
130
+ render: () => {
131
+ const [open, setOpen] = useState(false);
132
+
133
+ return (
134
+ <div className="flex h-[calc(100vh/2)] w-[calc(100vw/2)] items-center justify-center rounded-16 bg-background-subtle-base shadow-tooltip">
135
+ <Modal open={open} onOpenChange={setOpen}>
136
+ <ModalTrigger asChild>
137
+ <Button>Import past jobs from Github</Button>
138
+ </ModalTrigger>
139
+ <ModalContent aria-describedby={undefined}>
140
+ <ModalTitle className="sr-only">Import past jobs from Github</ModalTitle>
141
+ <ModalHeader title="Import past jobs from Github" />
142
+ <ModalBody className="gap-20">
143
+ <Text size="sm" className="text-foreground-neutral-subtle w-full">
144
+ Backfill your CI history by importing past runs from your Github repo. We&apos;ll
145
+ handle the rest by creating a background task to import the data for you.
146
+ </Text>
147
+ <div className="flex flex-col gap-20 w-full">
148
+ <div className="flex flex-col gap-8 w-full">
149
+ <Label>Repository owner</Label>
150
+ <Input placeholder="apache" />
151
+ </div>
152
+ <div className="flex flex-col gap-8 w-full">
153
+ <Label>Repository name</Label>
154
+ <Input placeholder="kafka" />
155
+ </div>
156
+ <div className="flex flex-col gap-8 w-full">
157
+ <Label>Start date</Label>
158
+ <Input placeholder="September 5th, 2025" />
159
+ </div>
160
+ </div>
161
+ </ModalBody>
162
+ <ModalFooter>
163
+ <Button variant="transparent" onClick={() => setOpen(false)}>
164
+ Cancel
165
+ </Button>
166
+ <Button variant="primary" onClick={() => setOpen(false)}>
167
+ Import
168
+ </Button>
169
+ </ModalFooter>
170
+ </ModalContent>
171
+ </Modal>
172
+ </div>
173
+ );
174
+ },
175
+ };
176
+
177
+ const diffCode = `jobs:
178
+ build:
179
+ - runs-on: ubuntu-latest
180
+ + runs-on: shipfox-2vcpu-ubuntu-2404`;
181
+
182
+ export const GithubActions: Story = {
183
+ parameters: {
184
+ viewport: {
185
+ defaultViewport: 'large',
186
+ },
187
+ },
188
+ play: async (ctx) => {
189
+ const {canvasElement, step} = ctx;
190
+ const canvas = within(canvasElement);
191
+ const user = userEvent.setup();
192
+
193
+ await step('Open the modal', async () => {
194
+ const triggerButton = canvas.getByRole('button', {name: GITHUB_ACTIONS_REGEX});
195
+ await user.click(triggerButton);
196
+ });
197
+
198
+ await step('Wait for dialog to appear and render', async () => {
199
+ await screen.findByRole('dialog');
200
+ await new Promise((resolve) => setTimeout(resolve, 100));
201
+ });
202
+
203
+ await argosScreenshot(ctx, 'Github Actions Modal Open');
204
+ },
205
+ render: () => {
206
+ const [open, setOpen] = useState(false);
207
+
208
+ return (
209
+ <div className="flex h-[50vh] w-[calc(100vw/2)] items-center justify-center rounded-16 bg-background-subtle-base shadow-tooltip">
210
+ <Modal open={open} onOpenChange={setOpen}>
211
+ <ModalTrigger asChild>
212
+ <Button>Run GitHub Actions on Shipfox</Button>
213
+ </ModalTrigger>
214
+ <ModalContent aria-describedby={undefined}>
215
+ <ModalTitle className="sr-only">Run GitHub Actions on Shipfox</ModalTitle>
216
+ <ModalHeader title="Run GitHub Actions on Shipfox" />
217
+ <ModalBody className="gap-32">
218
+ <div className="flex flex-col gap-20 w-full">
219
+ <Text size="sm" className="text-foreground-neutral-subtle w-full">
220
+ This will run your jobs on Shipfox&apos;s optimized infrastructure. Giving you
221
+ faster builds, and dedicated resources.
222
+ </Text>
223
+ <div className="relative">
224
+ <img
225
+ src={illustration2}
226
+ alt="illustration-2"
227
+ className="hidden sm:block absolute overflow-clip right-2 top-1/2 -translate-y-1/2 translate-x-8 w-fit object-contain z-50"
228
+ />
229
+ <div className={cn('relative overflow-hidden bg-transparent p-1 rounded-8')}>
230
+ <div className="absolute inset-0" style={{borderRadius: 'calc(0.5rem * 0.96)'}}>
231
+ <MovingBorder duration={6000} rx="30%" ry="30%">
232
+ <div className="h-100 w-200 bg-[radial-gradient(#ff9e7a_40%,transparent_60%)]" />
233
+ </MovingBorder>
234
+ </div>
235
+ <div
236
+ className="relative"
237
+ style={{
238
+ borderRadius: 'calc(0.5rem * 0.96)',
239
+ }}
240
+ >
241
+ <DynamicItem
242
+ variant="default"
243
+ title={
244
+ <div className="flex items-center gap-6">
245
+ <span className="flex shrink-0 items-center justify-center text-tag-success-icon w-16 h-16">
246
+ <Icon
247
+ name="money"
248
+ size="sm"
249
+ color="var(--foreground-neutral-subtle, #a1a1aa)"
250
+ />
251
+ </span>
252
+ <ItemTitle>6000 free credits/month to run your jobs</ItemTitle>
253
+ </div>
254
+ }
255
+ description="~500 builds/month. No payment required."
256
+ rightElement={
257
+ <img
258
+ src={illustrationBg}
259
+ alt="illustration-bg"
260
+ className="hidden sm:block absolute overflow-clip right-4 w-fit object-contain scale-105"
261
+ />
262
+ }
263
+ />
264
+ </div>
265
+ </div>
266
+ </div>
267
+ </div>
268
+ <div className="flex flex-col gap-20 w-full">
269
+ <div className="flex flex-col gap-6">
270
+ <div className="flex items-center justify-center w-full">
271
+ <Text className="flex-1 font-semibold text-foreground-neutral-base overflow-ellipsis overflow-hidden whitespace-nowrap">
272
+ Update your GitHub Actions workflow
273
+ </Text>
274
+ <ButtonLink variant="base" size="sm" href="#" iconRight="bookOpen">
275
+ See docs
276
+ </ButtonLink>
277
+ </div>
278
+ <Text size="sm" className="text-foreground-neutral-subtle w-full">
279
+ Replace the runs-on line in your workflow file to use Shipfox runners.
280
+ </Text>
281
+ </div>
282
+
283
+ <CodeBlock
284
+ data={[
285
+ {
286
+ language: 'yaml',
287
+ filename: '.github/workflows/<workflow-name>.yml',
288
+ code: diffCode,
289
+ },
290
+ ]}
291
+ defaultValue="yaml"
292
+ >
293
+ <CodeBlockHeader>
294
+ <CodeBlockFiles>
295
+ {(item) => (
296
+ <CodeBlockFilename value={item.language}>{item.filename}</CodeBlockFilename>
297
+ )}
298
+ </CodeBlockFiles>
299
+ <CodeBlockCopyButton />
300
+ </CodeBlockHeader>
301
+ <CodeBlockBody>
302
+ {(item) => (
303
+ <CodeBlockItem value={item.language}>
304
+ <CodeBlockContent language={item.language}>{item.code}</CodeBlockContent>
305
+ </CodeBlockItem>
306
+ )}
307
+ </CodeBlockBody>
308
+ <CodeBlockFooter
309
+ state="running"
310
+ message="Waiting for Shipfox runner event…"
311
+ description="This usually takes 30-60 seconds after you commit the workflow file."
312
+ />
313
+ </CodeBlock>
314
+ </div>
315
+ </ModalBody>
316
+ <ModalFooter>
317
+ <Button variant="primary" onClick={() => setOpen(false)}>
318
+ Got it
319
+ </Button>
320
+ </ModalFooter>
321
+ </ModalContent>
322
+ </Modal>
323
+ </div>
324
+ );
325
+ },
326
+ };
327
+
328
+ export const OpenedModal: Story = {
329
+ play: async (ctx) => {
330
+ const {canvasElement, step} = ctx;
331
+ const canvas = within(canvasElement);
332
+ const user = userEvent.setup();
333
+
334
+ await step('Open the modal', async () => {
335
+ const triggerButton = canvas.getByRole('button', {name: OPEN_MODAL_REGEX});
336
+ await user.click(triggerButton);
337
+ });
338
+
339
+ await step('Wait for dialog to appear and render', async () => {
340
+ await screen.findByRole('dialog');
341
+ await new Promise((resolve) => setTimeout(resolve, 100));
342
+ });
343
+
344
+ await argosScreenshot(ctx, 'Opened Modal State');
345
+ },
346
+ render: () => {
347
+ const [open, setOpen] = useState(false);
348
+
349
+ return (
350
+ <div className="flex h-[calc(100vh/2)] w-[calc(100vw/2)] items-center justify-center rounded-16 bg-background-subtle-base shadow-tooltip">
351
+ <Modal open={open} onOpenChange={setOpen}>
352
+ <ModalTrigger asChild>
353
+ <Button>Open Modal</Button>
354
+ </ModalTrigger>
355
+ <ModalContent aria-describedby={undefined}>
356
+ <ModalTitle className="sr-only">Modal Title</ModalTitle>
357
+ <ModalHeader>
358
+ <Text
359
+ size="lg"
360
+ className="flex-1 overflow-ellipsis overflow-hidden whitespace-nowrap"
361
+ >
362
+ Modal Title
363
+ </Text>
364
+ </ModalHeader>
365
+ <ModalBody>
366
+ <Text size="sm" className="text-foreground-neutral-subtle w-full">
367
+ This modal automatically adapts between dialog (desktop) and drawer (mobile) based
368
+ on screen size. Try resizing your browser window!
369
+ </Text>
370
+ </ModalBody>
371
+ <ModalFooter>
372
+ <Button variant="transparent" onClick={() => setOpen(false)}>
373
+ Cancel
374
+ </Button>
375
+ <Button variant="primary" onClick={() => setOpen(false)}>
376
+ Confirm
377
+ </Button>
378
+ </ModalFooter>
379
+ </ModalContent>
380
+ </Modal>
381
+ </div>
382
+ );
383
+ },
384
+ };
@@ -0,0 +1,309 @@
1
+ import * as DialogPrimitive from '@radix-ui/react-dialog';
2
+ import {cva} from 'class-variance-authority';
3
+ import {Button} from 'components/button';
4
+ import {Icon} from 'components/icon';
5
+ import {Text} from 'components/typography';
6
+ import {motion, type Transition} from 'framer-motion';
7
+ import {useMediaQuery} from 'hooks/useMediaQuery';
8
+ import {type ComponentProps, createContext, useContext} from 'react';
9
+ import {cn} from 'utils/cn';
10
+ import {Drawer as VaulDrawer} from 'vaul';
11
+
12
+ const modalDefaultTransition: Transition = {
13
+ type: 'spring',
14
+ stiffness: 300,
15
+ damping: 30,
16
+ };
17
+
18
+ type ModalContextValue = {
19
+ breakpoint: string;
20
+ isDesktop: boolean;
21
+ };
22
+
23
+ const ModalContext = createContext<ModalContextValue | null>(null);
24
+
25
+ function useModalContext() {
26
+ const context = useContext(ModalContext);
27
+ if (!context) {
28
+ throw new Error('Modal components must be used within a Modal component');
29
+ }
30
+ return context;
31
+ }
32
+
33
+ const modalOverlayVariants = cva(
34
+ 'fixed inset-0 z-40 bg-background-backdrop-backdrop data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
35
+ );
36
+
37
+ const modalContentVariants = cva(
38
+ 'fixed left-1/2 top-1/2 z-50 flex flex-col overflow-clip bg-background-neutral-base rounded-16 w-full max-w-[576px] -translate-x-1/2 -translate-y-1/2 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 shadow-tooltip',
39
+ );
40
+
41
+ function Modal({
42
+ breakpoint = '(min-width: 768px)',
43
+ children,
44
+ ...props
45
+ }: ComponentProps<typeof DialogPrimitive.Root> & {breakpoint?: string}) {
46
+ const isDesktop = useMediaQuery(breakpoint);
47
+
48
+ const contextValue: ModalContextValue = {
49
+ breakpoint,
50
+ isDesktop,
51
+ };
52
+
53
+ const Root = isDesktop ? DialogPrimitive.Root : VaulDrawer.Root;
54
+
55
+ return (
56
+ <ModalContext.Provider value={contextValue}>
57
+ <Root {...props}>{children}</Root>
58
+ </ModalContext.Provider>
59
+ );
60
+ }
61
+
62
+ function ModalTrigger(props: ComponentProps<typeof DialogPrimitive.Trigger>) {
63
+ const {isDesktop} = useModalContext();
64
+
65
+ if (isDesktop) {
66
+ return <DialogPrimitive.Trigger {...props} />;
67
+ }
68
+
69
+ return <VaulDrawer.Trigger {...props} />;
70
+ }
71
+
72
+ function ModalPortal(props: ComponentProps<typeof DialogPrimitive.Portal>) {
73
+ const {isDesktop} = useModalContext();
74
+
75
+ if (isDesktop) {
76
+ return <DialogPrimitive.Portal {...props} />;
77
+ }
78
+
79
+ return <VaulDrawer.Portal {...props} />;
80
+ }
81
+
82
+ function ModalClose(props: ComponentProps<typeof DialogPrimitive.Close>) {
83
+ const {isDesktop} = useModalContext();
84
+
85
+ if (isDesktop) {
86
+ return <DialogPrimitive.Close {...props} />;
87
+ }
88
+
89
+ return <VaulDrawer.Close {...props} />;
90
+ }
91
+
92
+ type ModalOverlayProps = ComponentProps<typeof DialogPrimitive.Overlay> & {
93
+ animated?: boolean;
94
+ transition?: Transition;
95
+ };
96
+
97
+ function ModalOverlay({
98
+ className,
99
+ animated = true,
100
+ transition = modalDefaultTransition,
101
+ ...props
102
+ }: ModalOverlayProps) {
103
+ const {isDesktop} = useModalContext();
104
+
105
+ if (!isDesktop) {
106
+ return <VaulDrawer.Overlay className={cn(modalOverlayVariants(), className)} {...props} />;
107
+ }
108
+
109
+ if (animated) {
110
+ return (
111
+ <DialogPrimitive.Overlay className={cn(modalOverlayVariants(), className)} asChild {...props}>
112
+ <motion.div
113
+ initial={{opacity: 0}}
114
+ animate={{opacity: 1}}
115
+ exit={{opacity: 0}}
116
+ transition={transition}
117
+ />
118
+ </DialogPrimitive.Overlay>
119
+ );
120
+ }
121
+
122
+ return <DialogPrimitive.Overlay className={cn(modalOverlayVariants(), className)} {...props} />;
123
+ }
124
+
125
+ type ModalContentProps = ComponentProps<typeof DialogPrimitive.Content> & {
126
+ animated?: boolean;
127
+ transition?: Transition;
128
+ };
129
+
130
+ function ModalContent({
131
+ className,
132
+ children,
133
+ animated = true,
134
+ transition = modalDefaultTransition,
135
+ ...props
136
+ }: ModalContentProps) {
137
+ const {isDesktop} = useModalContext();
138
+
139
+ if (!isDesktop) {
140
+ return (
141
+ <ModalPortal>
142
+ <ModalOverlay animated={animated} transition={transition} />
143
+ <VaulDrawer.Content
144
+ className={cn(
145
+ 'fixed bottom-0 left-0 right-0 z-50 flex flex-col bg-background-neutral-base rounded-t-16 max-h-[85vh] shadow-tooltip',
146
+ className,
147
+ )}
148
+ {...props}
149
+ >
150
+ <div className="relative w-full h-full flex flex-col min-h-0">
151
+ <div className="pointer-events-none absolute inset-0 shadow-separator-inset rounded-t-16" />
152
+ <div className="flex items-center justify-center pt-8 pb-8 shrink-0">
153
+ <div className="bg-foreground-neutral-subtle w-32 h-4 rounded-full opacity-40" />
154
+ </div>
155
+ {children}
156
+ </div>
157
+ </VaulDrawer.Content>
158
+ </ModalPortal>
159
+ );
160
+ }
161
+
162
+ const baseClasses = cn(modalContentVariants(), className);
163
+
164
+ return (
165
+ <ModalPortal>
166
+ <ModalOverlay animated={animated} transition={transition} />
167
+ <DialogPrimitive.Content className={baseClasses} {...props}>
168
+ <div className="relative size-full">
169
+ <div className="pointer-events-none absolute inset-0 shadow-separator-inset rounded-16" />
170
+ {children}
171
+ </div>
172
+ </DialogPrimitive.Content>
173
+ </ModalPortal>
174
+ );
175
+ }
176
+
177
+ type ModalHeaderProps = ComponentProps<'div'> & {
178
+ title?: string;
179
+ showEscIndicator?: boolean;
180
+ showClose?: boolean;
181
+ };
182
+
183
+ function ModalHeader({
184
+ className,
185
+ title,
186
+ showEscIndicator = true,
187
+ showClose = true,
188
+ children,
189
+ ...props
190
+ }: ModalHeaderProps) {
191
+ const {isDesktop} = useModalContext();
192
+
193
+ return (
194
+ <div className="flex flex-col w-full shrink-0" {...props}>
195
+ <div className="bg-background-neutral-base flex items-center justify-center gap-20 overflow-clip px-24 py-16 w-full">
196
+ {title ? (
197
+ <Text size="lg" className="flex-1 overflow-ellipsis overflow-hidden whitespace-nowrap">
198
+ {title}
199
+ </Text>
200
+ ) : (
201
+ <div className="flex-1">{children}</div>
202
+ )}
203
+ <div className="flex items-center gap-8">
204
+ {isDesktop && showEscIndicator && (
205
+ <kbd className="flex items-center justify-center rounded-8 border border-border-neutral-base shadow-button-neutral bg-background-field-base text-xs text-foreground-neutral-subtle px-4">
206
+ esc
207
+ </kbd>
208
+ )}
209
+ {showClose && (
210
+ <ModalClose asChild>
211
+ <Button
212
+ variant="transparent"
213
+ size="xs"
214
+ className="rounded-4 p-2 cursor-pointer bg-transparent border-none text-foreground-neutral-muted hover:text-foreground-neutral-base hover:bg-background-components-hover transition-colors duration-150 outline-none focus-visible:ring-2 focus-visible:ring-background-accent-blue-base focus-visible:ring-offset-2 w-24 h-24"
215
+ >
216
+ <Icon name="close" />
217
+ </Button>
218
+ </ModalClose>
219
+ )}
220
+ </div>
221
+ </div>
222
+ <div className="bg-border-neutral-strong h-[1px] w-full" />
223
+ </div>
224
+ );
225
+ }
226
+
227
+ function ModalBody({className, children, ...props}: ComponentProps<'div'>) {
228
+ const {isDesktop} = useModalContext();
229
+
230
+ return (
231
+ <div
232
+ className={cn(
233
+ 'bg-background-neutral-base flex flex-col items-start px-24 pb-24 pt-16 w-full',
234
+ isDesktop ? 'overflow-clip' : 'overflow-y-auto overflow-x-clip flex-1',
235
+ className,
236
+ )}
237
+ {...props}
238
+ >
239
+ {children}
240
+ </div>
241
+ );
242
+ }
243
+
244
+ function ModalFooter({className, children, ...props}: ComponentProps<'div'>) {
245
+ return (
246
+ <div className="flex flex-col w-full shrink-0" {...props}>
247
+ <div className="bg-border-neutral-strong h-[1px] w-full" />
248
+ <div className="bg-background-neutral-base flex items-end justify-end gap-20 overflow-clip px-24 py-16 w-full">
249
+ <div className={cn('flex items-center gap-16', className)}>{children}</div>
250
+ </div>
251
+ </div>
252
+ );
253
+ }
254
+
255
+ type ModalTitleProps = ComponentProps<typeof DialogPrimitive.Title>;
256
+
257
+ function ModalTitle({className, ...props}: ModalTitleProps) {
258
+ const {isDesktop} = useModalContext();
259
+
260
+ const titleClassName = cn(
261
+ 'font-medium text-lg leading-20 overflow-ellipsis overflow-hidden text-foreground-neutral-base',
262
+ className,
263
+ );
264
+
265
+ if (!isDesktop) {
266
+ return <VaulDrawer.Title className={titleClassName} {...props} />;
267
+ }
268
+
269
+ return <DialogPrimitive.Title className={titleClassName} {...props} />;
270
+ }
271
+
272
+ type ModalDescriptionProps = ComponentProps<typeof DialogPrimitive.Description>;
273
+
274
+ function ModalDescription({className, ...props}: ModalDescriptionProps) {
275
+ const {isDesktop} = useModalContext();
276
+
277
+ const descClassName = cn('text-sm leading-20 text-foreground-neutral-subtle', className);
278
+
279
+ if (!isDesktop) {
280
+ return <VaulDrawer.Description className={descClassName} {...props} />;
281
+ }
282
+
283
+ return <DialogPrimitive.Description className={descClassName} {...props} />;
284
+ }
285
+
286
+ export {
287
+ Modal,
288
+ ModalPortal,
289
+ ModalOverlay,
290
+ ModalTrigger,
291
+ ModalClose,
292
+ ModalContent,
293
+ ModalHeader,
294
+ ModalBody,
295
+ ModalFooter,
296
+ ModalTitle,
297
+ ModalDescription,
298
+ modalContentVariants,
299
+ modalOverlayVariants,
300
+ modalDefaultTransition,
301
+ };
302
+
303
+ export type {
304
+ ModalContentProps,
305
+ ModalHeaderProps,
306
+ ModalOverlayProps,
307
+ ModalTitleProps,
308
+ ModalDescriptionProps,
309
+ };
@@ -0,0 +1 @@
1
+ export * from './moving-border';