vinta-schedule-design-system 0.1.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 (107) hide show
  1. package/components.json +21 -0
  2. package/dist/styles.css +3452 -0
  3. package/package.json +103 -0
  4. package/src/layout/app-shell.tsx +115 -0
  5. package/src/layout/app-topbar.stories.tsx +74 -0
  6. package/src/layout/app-topbar.tsx +145 -0
  7. package/src/layout/auth-layout.tsx +45 -0
  8. package/src/layout/box.stories.tsx +86 -0
  9. package/src/layout/box.tsx +34 -0
  10. package/src/layout/center.tsx +24 -0
  11. package/src/layout/container.stories.tsx +44 -0
  12. package/src/layout/container.tsx +39 -0
  13. package/src/layout/divider.tsx +64 -0
  14. package/src/layout/flex.stories.tsx +109 -0
  15. package/src/layout/flex.test.tsx +71 -0
  16. package/src/layout/flex.tsx +112 -0
  17. package/src/layout/grid.stories.tsx +91 -0
  18. package/src/layout/grid.tsx +132 -0
  19. package/src/layout/heading.tsx +53 -0
  20. package/src/layout/index.ts +47 -0
  21. package/src/layout/layout-style.ts +291 -0
  22. package/src/layout/link.stories.tsx +54 -0
  23. package/src/layout/link.test.tsx +75 -0
  24. package/src/layout/link.tsx +100 -0
  25. package/src/layout/navbar.stories.tsx +70 -0
  26. package/src/layout/navbar.tsx +128 -0
  27. package/src/layout/page-header.stories.tsx +80 -0
  28. package/src/layout/page-header.tsx +59 -0
  29. package/src/layout/section.tsx +22 -0
  30. package/src/layout/spacer.tsx +21 -0
  31. package/src/layout/stack.stories.tsx +58 -0
  32. package/src/layout/stack.tsx +19 -0
  33. package/src/layout/text.tsx +139 -0
  34. package/src/layout/typography.stories.tsx +112 -0
  35. package/src/lib/utils.ts +6 -0
  36. package/src/story-types.ts +80 -0
  37. package/src/styles/build.css +33 -0
  38. package/src/styles/globals.css +9 -0
  39. package/src/styles/tokens.css +281 -0
  40. package/src/ui/accordion.stories.tsx +84 -0
  41. package/src/ui/accordion.tsx +57 -0
  42. package/src/ui/alert-dialog.stories.tsx +57 -0
  43. package/src/ui/alert-dialog.tsx +141 -0
  44. package/src/ui/alert.stories.tsx +46 -0
  45. package/src/ui/alert.tsx +59 -0
  46. package/src/ui/avatar.stories.tsx +56 -0
  47. package/src/ui/avatar.tsx +50 -0
  48. package/src/ui/badge.stories.tsx +105 -0
  49. package/src/ui/badge.tsx +57 -0
  50. package/src/ui/breadcrumb.stories.tsx +69 -0
  51. package/src/ui/breadcrumb.tsx +140 -0
  52. package/src/ui/button.stories.tsx +85 -0
  53. package/src/ui/button.tsx +60 -0
  54. package/src/ui/card.stories.tsx +62 -0
  55. package/src/ui/card.tsx +96 -0
  56. package/src/ui/carousel.stories.tsx +55 -0
  57. package/src/ui/carousel.tsx +262 -0
  58. package/src/ui/checkbox.stories.tsx +52 -0
  59. package/src/ui/checkbox.tsx +30 -0
  60. package/src/ui/combobox.stories.tsx +138 -0
  61. package/src/ui/combobox.tsx +217 -0
  62. package/src/ui/command.tsx +184 -0
  63. package/src/ui/dialog.stories.tsx +66 -0
  64. package/src/ui/dialog.tsx +163 -0
  65. package/src/ui/drawer.stories.tsx +80 -0
  66. package/src/ui/drawer.tsx +118 -0
  67. package/src/ui/dropdown-menu.stories.tsx +57 -0
  68. package/src/ui/dropdown-menu.tsx +201 -0
  69. package/src/ui/form.stories.tsx +70 -0
  70. package/src/ui/form.tsx +181 -0
  71. package/src/ui/input-otp.stories.tsx +52 -0
  72. package/src/ui/input-otp.tsx +71 -0
  73. package/src/ui/input.stories.tsx +68 -0
  74. package/src/ui/input.tsx +22 -0
  75. package/src/ui/label.stories.tsx +35 -0
  76. package/src/ui/label.tsx +26 -0
  77. package/src/ui/navigation-menu.stories.tsx +79 -0
  78. package/src/ui/navigation-menu.tsx +128 -0
  79. package/src/ui/pagination.stories.tsx +62 -0
  80. package/src/ui/pagination.tsx +137 -0
  81. package/src/ui/popover.tsx +89 -0
  82. package/src/ui/progress.stories.tsx +33 -0
  83. package/src/ui/progress.tsx +28 -0
  84. package/src/ui/radio-group.stories.tsx +52 -0
  85. package/src/ui/radio-group.tsx +44 -0
  86. package/src/ui/scheduling-chip.stories.tsx +117 -0
  87. package/src/ui/scheduling-chip.tsx +62 -0
  88. package/src/ui/scroll-area.stories.tsx +52 -0
  89. package/src/ui/scroll-area.tsx +48 -0
  90. package/src/ui/select.stories.tsx +56 -0
  91. package/src/ui/select.tsx +159 -0
  92. package/src/ui/sheet.stories.tsx +63 -0
  93. package/src/ui/sheet.tsx +141 -0
  94. package/src/ui/skeleton.stories.tsx +40 -0
  95. package/src/ui/skeleton.tsx +15 -0
  96. package/src/ui/sonner.stories.tsx +62 -0
  97. package/src/ui/sonner.tsx +34 -0
  98. package/src/ui/switch.stories.tsx +41 -0
  99. package/src/ui/switch.tsx +29 -0
  100. package/src/ui/table.stories.tsx +88 -0
  101. package/src/ui/table.tsx +132 -0
  102. package/src/ui/tabs.stories.tsx +63 -0
  103. package/src/ui/tabs.tsx +55 -0
  104. package/src/ui/textarea.stories.tsx +45 -0
  105. package/src/ui/textarea.tsx +22 -0
  106. package/src/ui/tooltip.stories.tsx +48 -0
  107. package/src/ui/tooltip.tsx +32 -0
@@ -0,0 +1,184 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import { Command as CommandPrimitive } from 'cmdk';
5
+ import { SearchIcon } from 'lucide-react';
6
+
7
+ import { cn } from '../lib/utils';
8
+ import {
9
+ Dialog,
10
+ DialogContent,
11
+ DialogDescription,
12
+ DialogHeader,
13
+ DialogTitle,
14
+ } from './dialog';
15
+
16
+ function Command({
17
+ className,
18
+ ...props
19
+ }: React.ComponentProps<typeof CommandPrimitive>) {
20
+ return (
21
+ <CommandPrimitive
22
+ data-slot='command'
23
+ className={cn(
24
+ 'bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md',
25
+ className
26
+ )}
27
+ {...props}
28
+ />
29
+ );
30
+ }
31
+
32
+ function CommandDialog({
33
+ title = 'Command Palette',
34
+ description = 'Search for a command to run...',
35
+ children,
36
+ className,
37
+ showCloseButton = true,
38
+ ...props
39
+ }: React.ComponentProps<typeof Dialog> & {
40
+ title?: string;
41
+ description?: string;
42
+ className?: string;
43
+ showCloseButton?: boolean;
44
+ }) {
45
+ return (
46
+ <Dialog {...props}>
47
+ <DialogHeader className='sr-only'>
48
+ <DialogTitle>{title}</DialogTitle>
49
+ <DialogDescription>{description}</DialogDescription>
50
+ </DialogHeader>
51
+ <DialogContent
52
+ className={cn('overflow-hidden p-0', className)}
53
+ showCloseButton={showCloseButton}
54
+ >
55
+ <Command className='[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5'>
56
+ {children}
57
+ </Command>
58
+ </DialogContent>
59
+ </Dialog>
60
+ );
61
+ }
62
+
63
+ function CommandInput({
64
+ className,
65
+ ...props
66
+ }: React.ComponentProps<typeof CommandPrimitive.Input>) {
67
+ return (
68
+ <div
69
+ data-slot='command-input-wrapper'
70
+ className='flex h-9 items-center gap-2 border-b px-3'
71
+ >
72
+ <SearchIcon className='size-4 shrink-0 opacity-50' />
73
+ <CommandPrimitive.Input
74
+ data-slot='command-input'
75
+ className={cn(
76
+ 'placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50',
77
+ className
78
+ )}
79
+ {...props}
80
+ />
81
+ </div>
82
+ );
83
+ }
84
+
85
+ function CommandList({
86
+ className,
87
+ ...props
88
+ }: React.ComponentProps<typeof CommandPrimitive.List>) {
89
+ return (
90
+ <CommandPrimitive.List
91
+ data-slot='command-list'
92
+ className={cn(
93
+ 'max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto',
94
+ className
95
+ )}
96
+ {...props}
97
+ />
98
+ );
99
+ }
100
+
101
+ function CommandEmpty({
102
+ ...props
103
+ }: React.ComponentProps<typeof CommandPrimitive.Empty>) {
104
+ return (
105
+ <CommandPrimitive.Empty
106
+ data-slot='command-empty'
107
+ className='py-6 text-center text-sm'
108
+ {...props}
109
+ />
110
+ );
111
+ }
112
+
113
+ function CommandGroup({
114
+ className,
115
+ ...props
116
+ }: React.ComponentProps<typeof CommandPrimitive.Group>) {
117
+ return (
118
+ <CommandPrimitive.Group
119
+ data-slot='command-group'
120
+ className={cn(
121
+ 'text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium',
122
+ className
123
+ )}
124
+ {...props}
125
+ />
126
+ );
127
+ }
128
+
129
+ function CommandSeparator({
130
+ className,
131
+ ...props
132
+ }: React.ComponentProps<typeof CommandPrimitive.Separator>) {
133
+ return (
134
+ <CommandPrimitive.Separator
135
+ data-slot='command-separator'
136
+ className={cn('bg-border -mx-1 h-px', className)}
137
+ {...props}
138
+ />
139
+ );
140
+ }
141
+
142
+ function CommandItem({
143
+ className,
144
+ ...props
145
+ }: React.ComponentProps<typeof CommandPrimitive.Item>) {
146
+ return (
147
+ <CommandPrimitive.Item
148
+ data-slot='command-item'
149
+ className={cn(
150
+ "data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
151
+ className
152
+ )}
153
+ {...props}
154
+ />
155
+ );
156
+ }
157
+
158
+ function CommandShortcut({
159
+ className,
160
+ ...props
161
+ }: React.ComponentProps<'span'>) {
162
+ return (
163
+ <span
164
+ data-slot='command-shortcut'
165
+ className={cn(
166
+ 'text-muted-foreground ml-auto text-xs tracking-widest',
167
+ className
168
+ )}
169
+ {...props}
170
+ />
171
+ );
172
+ }
173
+
174
+ export {
175
+ Command,
176
+ CommandDialog,
177
+ CommandInput,
178
+ CommandList,
179
+ CommandEmpty,
180
+ CommandGroup,
181
+ CommandItem,
182
+ CommandShortcut,
183
+ CommandSeparator,
184
+ };
@@ -0,0 +1,66 @@
1
+ import type { Meta, StoryObj } from '../story-types';
2
+
3
+ import {
4
+ Dialog,
5
+ DialogClose,
6
+ DialogContent,
7
+ DialogDescription,
8
+ DialogFooter,
9
+ DialogHeader,
10
+ DialogTitle,
11
+ DialogTrigger,
12
+ } from './dialog';
13
+ import { Button } from './button';
14
+ import { Input } from './input';
15
+ import { Label } from './label';
16
+
17
+ const meta = {
18
+ title: 'Components/Dialog',
19
+ component: Dialog,
20
+ tags: ['autodocs'],
21
+ // Radix Dialog.Root scalars (`open` / `onOpenChange` are controlled-mode only,
22
+ // so they stay out). `children` (trigger + content) is composed → a slot.
23
+ argTypes: {
24
+ defaultOpen: {
25
+ control: 'boolean',
26
+ description: 'Open on first render (uncontrolled)',
27
+ },
28
+ modal: {
29
+ control: 'boolean',
30
+ description: 'Trap focus and block interaction outside the dialog',
31
+ },
32
+ },
33
+ args: { defaultOpen: false, modal: true },
34
+ parameters: { puck: { slots: ['children'] } },
35
+ } satisfies Meta<typeof Dialog>;
36
+
37
+ export default meta;
38
+ type Story = StoryObj<typeof meta>;
39
+
40
+ export const Default: Story = {
41
+ render: (args) => (
42
+ <Dialog {...args}>
43
+ <DialogTrigger asChild>
44
+ <Button>New booking</Button>
45
+ </DialogTrigger>
46
+ <DialogContent className='sm:max-w-md'>
47
+ <DialogHeader>
48
+ <DialogTitle>Create a booking</DialogTitle>
49
+ <DialogDescription>
50
+ Add a one-off appointment to your schedule.
51
+ </DialogDescription>
52
+ </DialogHeader>
53
+ <div className='grid gap-3 py-2'>
54
+ <Label htmlFor='title'>Title</Label>
55
+ <Input id='title' placeholder='Follow-up visit' />
56
+ </div>
57
+ <DialogFooter>
58
+ <DialogClose asChild>
59
+ <Button variant='outline'>Cancel</Button>
60
+ </DialogClose>
61
+ <Button>Save</Button>
62
+ </DialogFooter>
63
+ </DialogContent>
64
+ </Dialog>
65
+ ),
66
+ };
@@ -0,0 +1,163 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import { XIcon } from 'lucide-react';
5
+ import { Dialog as DialogPrimitive } from 'radix-ui';
6
+
7
+ import { cn } from '../lib/utils';
8
+ import { Button } from './button';
9
+
10
+ function Dialog({
11
+ ...props
12
+ }: React.ComponentProps<typeof DialogPrimitive.Root>) {
13
+ return <DialogPrimitive.Root data-slot='dialog' {...props} />;
14
+ }
15
+ // Pinned: bundlers rename this function (it collides with radix-ui's own
16
+ // `Dialog` export once inlined), and tooling that resolves the component by
17
+ // name — Storybook docgen, the composer's contract extractor — would see the
18
+ // mangled name instead.
19
+ Dialog.displayName = 'Dialog';
20
+
21
+ function DialogTrigger({
22
+ ...props
23
+ }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
24
+ return <DialogPrimitive.Trigger data-slot='dialog-trigger' {...props} />;
25
+ }
26
+
27
+ function DialogPortal({
28
+ ...props
29
+ }: React.ComponentProps<typeof DialogPrimitive.Portal>) {
30
+ return <DialogPrimitive.Portal data-slot='dialog-portal' {...props} />;
31
+ }
32
+
33
+ function DialogClose({
34
+ ...props
35
+ }: React.ComponentProps<typeof DialogPrimitive.Close>) {
36
+ return <DialogPrimitive.Close data-slot='dialog-close' {...props} />;
37
+ }
38
+
39
+ function DialogOverlay({
40
+ className,
41
+ ...props
42
+ }: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
43
+ return (
44
+ <DialogPrimitive.Overlay
45
+ data-slot='dialog-overlay'
46
+ className={cn(
47
+ 'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50',
48
+ className
49
+ )}
50
+ {...props}
51
+ />
52
+ );
53
+ }
54
+
55
+ function DialogContent({
56
+ className,
57
+ children,
58
+ showCloseButton = true,
59
+ ...props
60
+ }: React.ComponentProps<typeof DialogPrimitive.Content> & {
61
+ showCloseButton?: boolean;
62
+ }) {
63
+ return (
64
+ <DialogPortal data-slot='dialog-portal'>
65
+ <DialogOverlay />
66
+ <DialogPrimitive.Content
67
+ data-slot='dialog-content'
68
+ className={cn(
69
+ 'bg-background data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg',
70
+ className
71
+ )}
72
+ {...props}
73
+ >
74
+ {children}
75
+ {showCloseButton && (
76
+ <DialogPrimitive.Close
77
+ data-slot='dialog-close'
78
+ className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
79
+ >
80
+ <XIcon />
81
+ <span className='sr-only'>Close</span>
82
+ </DialogPrimitive.Close>
83
+ )}
84
+ </DialogPrimitive.Content>
85
+ </DialogPortal>
86
+ );
87
+ }
88
+
89
+ function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) {
90
+ return (
91
+ <div
92
+ data-slot='dialog-header'
93
+ className={cn('flex flex-col gap-2 text-center sm:text-left', className)}
94
+ {...props}
95
+ />
96
+ );
97
+ }
98
+
99
+ function DialogFooter({
100
+ className,
101
+ showCloseButton = false,
102
+ children,
103
+ ...props
104
+ }: React.ComponentProps<'div'> & {
105
+ showCloseButton?: boolean;
106
+ }) {
107
+ return (
108
+ <div
109
+ data-slot='dialog-footer'
110
+ className={cn(
111
+ 'flex flex-col-reverse gap-2 sm:flex-row sm:justify-end',
112
+ className
113
+ )}
114
+ {...props}
115
+ >
116
+ {children}
117
+ {showCloseButton && (
118
+ <DialogPrimitive.Close asChild>
119
+ <Button variant='outline'>Close</Button>
120
+ </DialogPrimitive.Close>
121
+ )}
122
+ </div>
123
+ );
124
+ }
125
+
126
+ function DialogTitle({
127
+ className,
128
+ ...props
129
+ }: React.ComponentProps<typeof DialogPrimitive.Title>) {
130
+ return (
131
+ <DialogPrimitive.Title
132
+ data-slot='dialog-title'
133
+ className={cn('text-lg leading-none font-semibold', className)}
134
+ {...props}
135
+ />
136
+ );
137
+ }
138
+
139
+ function DialogDescription({
140
+ className,
141
+ ...props
142
+ }: React.ComponentProps<typeof DialogPrimitive.Description>) {
143
+ return (
144
+ <DialogPrimitive.Description
145
+ data-slot='dialog-description'
146
+ className={cn('text-muted-foreground text-sm', className)}
147
+ {...props}
148
+ />
149
+ );
150
+ }
151
+
152
+ export {
153
+ Dialog,
154
+ DialogClose,
155
+ DialogContent,
156
+ DialogDescription,
157
+ DialogFooter,
158
+ DialogHeader,
159
+ DialogOverlay,
160
+ DialogPortal,
161
+ DialogTitle,
162
+ DialogTrigger,
163
+ };
@@ -0,0 +1,80 @@
1
+ import type { Meta, StoryObj } from '../story-types';
2
+
3
+ import {
4
+ Drawer,
5
+ DrawerClose,
6
+ DrawerContent,
7
+ DrawerDescription,
8
+ DrawerFooter,
9
+ DrawerHeader,
10
+ DrawerTitle,
11
+ DrawerTrigger,
12
+ } from './drawer';
13
+ import { Button } from './button';
14
+
15
+ const meta = {
16
+ title: 'Components/Drawer',
17
+ component: Drawer,
18
+ tags: ['autodocs'],
19
+ // vaul Drawer.Root scalars (drawer.tsx forwards every prop to it, and owns
20
+ // `shouldScaleBackground`). `children` (trigger + content) is composed → slot.
21
+ argTypes: {
22
+ direction: {
23
+ control: 'select',
24
+ options: ['bottom', 'top', 'left', 'right'],
25
+ description:
26
+ 'Edge the drawer slides from. DrawerContent is styled for `bottom`; other directions need content classes to match.',
27
+ },
28
+ modal: {
29
+ control: 'boolean',
30
+ description: 'Block interaction with the page behind the drawer',
31
+ },
32
+ dismissible: {
33
+ control: 'boolean',
34
+ description: 'Allow closing by dragging or clicking the overlay',
35
+ },
36
+ defaultOpen: {
37
+ control: 'boolean',
38
+ description: 'Open on first render (uncontrolled)',
39
+ },
40
+ shouldScaleBackground: {
41
+ control: 'boolean',
42
+ description: 'Scale the page behind the drawer while it is open',
43
+ },
44
+ },
45
+ args: {
46
+ direction: 'bottom',
47
+ modal: true,
48
+ dismissible: true,
49
+ defaultOpen: false,
50
+ shouldScaleBackground: true,
51
+ },
52
+ parameters: { puck: { slots: ['children'] } },
53
+ } satisfies Meta<typeof Drawer>;
54
+
55
+ export default meta;
56
+ type Story = StoryObj<typeof meta>;
57
+
58
+ export const Default: Story = {
59
+ render: (args) => (
60
+ <Drawer {...args}>
61
+ <DrawerTrigger asChild>
62
+ <Button variant='outline'>Open drawer</Button>
63
+ </DrawerTrigger>
64
+ <DrawerContent>
65
+ <div className='mx-auto w-full max-w-sm'>
66
+ <DrawerHeader>
67
+ <DrawerTitle>Confirm time</DrawerTitle>
68
+ <DrawerDescription>Mon, 9:00–9:30 AM</DrawerDescription>
69
+ </DrawerHeader>
70
+ <DrawerFooter>
71
+ <Button>Confirm</Button>
72
+ <DrawerClose asChild>
73
+ <Button variant='outline'>Cancel</Button>
74
+ </DrawerClose>
75
+ </DrawerFooter>
76
+ </div>
77
+ </DrawerContent>
78
+ </Drawer>
79
+ ),
80
+ };
@@ -0,0 +1,118 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import { Drawer as DrawerPrimitive } from 'vaul';
5
+
6
+ import { cn } from '../lib/utils';
7
+
8
+ const Drawer = ({
9
+ shouldScaleBackground = true,
10
+ ...props
11
+ }: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
12
+ <DrawerPrimitive.Root
13
+ shouldScaleBackground={shouldScaleBackground}
14
+ {...props}
15
+ />
16
+ );
17
+ Drawer.displayName = 'Drawer';
18
+
19
+ const DrawerTrigger = DrawerPrimitive.Trigger;
20
+
21
+ const DrawerPortal = DrawerPrimitive.Portal;
22
+
23
+ const DrawerClose = DrawerPrimitive.Close;
24
+
25
+ const DrawerOverlay = React.forwardRef<
26
+ React.ElementRef<typeof DrawerPrimitive.Overlay>,
27
+ React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
28
+ >(({ className, ...props }, ref) => (
29
+ <DrawerPrimitive.Overlay
30
+ ref={ref}
31
+ className={cn('fixed inset-0 z-50 bg-black/80', className)}
32
+ {...props}
33
+ />
34
+ ));
35
+ DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;
36
+
37
+ const DrawerContent = React.forwardRef<
38
+ React.ElementRef<typeof DrawerPrimitive.Content>,
39
+ React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
40
+ >(({ className, children, ...props }, ref) => (
41
+ <DrawerPortal>
42
+ <DrawerOverlay />
43
+ <DrawerPrimitive.Content
44
+ ref={ref}
45
+ className={cn(
46
+ 'bg-background fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border',
47
+ className
48
+ )}
49
+ {...props}
50
+ >
51
+ <div className='bg-muted mx-auto mt-4 h-2 w-[100px] rounded-full' />
52
+ {children}
53
+ </DrawerPrimitive.Content>
54
+ </DrawerPortal>
55
+ ));
56
+ DrawerContent.displayName = 'DrawerContent';
57
+
58
+ const DrawerHeader = ({
59
+ className,
60
+ ...props
61
+ }: React.HTMLAttributes<HTMLDivElement>) => (
62
+ <div
63
+ className={cn('grid gap-1.5 p-4 text-center sm:text-left', className)}
64
+ {...props}
65
+ />
66
+ );
67
+ DrawerHeader.displayName = 'DrawerHeader';
68
+
69
+ const DrawerFooter = ({
70
+ className,
71
+ ...props
72
+ }: React.HTMLAttributes<HTMLDivElement>) => (
73
+ <div
74
+ className={cn('mt-auto flex flex-col gap-2 p-4', className)}
75
+ {...props}
76
+ />
77
+ );
78
+ DrawerFooter.displayName = 'DrawerFooter';
79
+
80
+ const DrawerTitle = React.forwardRef<
81
+ React.ElementRef<typeof DrawerPrimitive.Title>,
82
+ React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
83
+ >(({ className, ...props }, ref) => (
84
+ <DrawerPrimitive.Title
85
+ ref={ref}
86
+ className={cn(
87
+ 'text-lg leading-none font-semibold tracking-tight',
88
+ className
89
+ )}
90
+ {...props}
91
+ />
92
+ ));
93
+ DrawerTitle.displayName = DrawerPrimitive.Title.displayName;
94
+
95
+ const DrawerDescription = React.forwardRef<
96
+ React.ElementRef<typeof DrawerPrimitive.Description>,
97
+ React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
98
+ >(({ className, ...props }, ref) => (
99
+ <DrawerPrimitive.Description
100
+ ref={ref}
101
+ className={cn('text-muted-foreground text-sm', className)}
102
+ {...props}
103
+ />
104
+ ));
105
+ DrawerDescription.displayName = DrawerPrimitive.Description.displayName;
106
+
107
+ export {
108
+ Drawer,
109
+ DrawerPortal,
110
+ DrawerOverlay,
111
+ DrawerTrigger,
112
+ DrawerClose,
113
+ DrawerContent,
114
+ DrawerHeader,
115
+ DrawerFooter,
116
+ DrawerTitle,
117
+ DrawerDescription,
118
+ };
@@ -0,0 +1,57 @@
1
+ import type { Meta, StoryObj } from '../story-types';
2
+
3
+ import {
4
+ DropdownMenu,
5
+ DropdownMenuContent,
6
+ DropdownMenuItem,
7
+ DropdownMenuLabel,
8
+ DropdownMenuSeparator,
9
+ DropdownMenuShortcut,
10
+ DropdownMenuTrigger,
11
+ } from './dropdown-menu';
12
+ import { Button } from './button';
13
+
14
+ const meta = {
15
+ title: 'Components/DropdownMenu',
16
+ component: DropdownMenu,
17
+ tags: ['autodocs'],
18
+ // Radix DropdownMenu.Root scalars. `children` (trigger + content) is composed
19
+ // content → a slot, never an argTypes key.
20
+ argTypes: {
21
+ defaultOpen: {
22
+ control: 'boolean',
23
+ description: 'Open on first render (uncontrolled)',
24
+ },
25
+ modal: {
26
+ control: 'boolean',
27
+ description: 'Block interaction with the page while the menu is open',
28
+ },
29
+ dir: { control: 'inline-radio', options: ['ltr', 'rtl'] },
30
+ },
31
+ args: { defaultOpen: false, modal: true },
32
+ parameters: { puck: { slots: ['children'] } },
33
+ } satisfies Meta<typeof DropdownMenu>;
34
+
35
+ export default meta;
36
+ type Story = StoryObj<typeof meta>;
37
+
38
+ export const Default: Story = {
39
+ render: (args) => (
40
+ <DropdownMenu {...args}>
41
+ <DropdownMenuTrigger asChild>
42
+ <Button variant='outline'>Actions</Button>
43
+ </DropdownMenuTrigger>
44
+ <DropdownMenuContent align='start' className='w-52'>
45
+ <DropdownMenuLabel>Appointment</DropdownMenuLabel>
46
+ <DropdownMenuSeparator />
47
+ <DropdownMenuItem>
48
+ Reschedule
49
+ <DropdownMenuShortcut>⌘R</DropdownMenuShortcut>
50
+ </DropdownMenuItem>
51
+ <DropdownMenuItem>Copy link</DropdownMenuItem>
52
+ <DropdownMenuSeparator />
53
+ <DropdownMenuItem className='text-destructive'>Cancel</DropdownMenuItem>
54
+ </DropdownMenuContent>
55
+ </DropdownMenu>
56
+ ),
57
+ };