@nocobase/portal-template-default 1.0.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 (123) hide show
  1. package/.env.example +10 -0
  2. package/Dockerfile +36 -0
  3. package/README.MD +50 -0
  4. package/components.json +25 -0
  5. package/eslint.config.js +28 -0
  6. package/index.html +39 -0
  7. package/package.json +84 -0
  8. package/pnpm-workspace.yaml +2 -0
  9. package/public/favicon.ico +0 -0
  10. package/public/logo-dark.png +0 -0
  11. package/public/logo-mark-dark.png +0 -0
  12. package/public/logo-mark.png +0 -0
  13. package/public/logo.png +0 -0
  14. package/src/App.css +158 -0
  15. package/src/App.tsx +151 -0
  16. package/src/components/app-shell/brand.tsx +70 -0
  17. package/src/components/app-shell/breadcrumb.tsx +81 -0
  18. package/src/components/app-shell/document-title-handler.tsx +169 -0
  19. package/src/components/app-shell/error-component.tsx +134 -0
  20. package/src/components/app-shell/header.tsx +127 -0
  21. package/src/components/app-shell/layout.tsx +39 -0
  22. package/src/components/app-shell/loading-overlay.tsx +36 -0
  23. package/src/components/app-shell/sidebar.tsx +351 -0
  24. package/src/components/app-shell/user-avatar.tsx +42 -0
  25. package/src/components/app-shell/user-info.tsx +53 -0
  26. package/src/components/auth/auth-layout.tsx +82 -0
  27. package/src/components/auth/forgot-password-form.tsx +58 -0
  28. package/src/components/auth/input-password.tsx +38 -0
  29. package/src/components/auth/sign-in-form.tsx +111 -0
  30. package/src/components/auth/sign-up-form.tsx +127 -0
  31. package/src/components/data-table/data-table-filter.tsx +994 -0
  32. package/src/components/data-table/data-table-pagination.tsx +146 -0
  33. package/src/components/data-table/data-table-sorter.tsx +47 -0
  34. package/src/components/data-table/data-table.tsx +321 -0
  35. package/src/components/notifications/toaster.tsx +23 -0
  36. package/src/components/notifications/undoable-notification.tsx +84 -0
  37. package/src/components/notifications/use-notification-provider.tsx +62 -0
  38. package/src/components/resources/buttons/clone.tsx +81 -0
  39. package/src/components/resources/buttons/create.tsx +70 -0
  40. package/src/components/resources/buttons/delete.tsx +113 -0
  41. package/src/components/resources/buttons/edit.tsx +81 -0
  42. package/src/components/resources/buttons/list.tsx +70 -0
  43. package/src/components/resources/buttons/refresh.tsx +81 -0
  44. package/src/components/resources/buttons/show.tsx +81 -0
  45. package/src/components/resources/status-badge.tsx +37 -0
  46. package/src/components/resources/views/create-view.tsx +81 -0
  47. package/src/components/resources/views/edit-view.tsx +104 -0
  48. package/src/components/resources/views/list-view.tsx +86 -0
  49. package/src/components/resources/views/show-view.tsx +108 -0
  50. package/src/components/theme/theme-provider.tsx +71 -0
  51. package/src/components/theme/theme-select.tsx +97 -0
  52. package/src/components/theme/theme-toggle.tsx +66 -0
  53. package/src/components/ui/accordion.tsx +72 -0
  54. package/src/components/ui/alert-dialog.tsx +185 -0
  55. package/src/components/ui/alert.tsx +76 -0
  56. package/src/components/ui/aspect-ratio.tsx +22 -0
  57. package/src/components/ui/avatar.tsx +107 -0
  58. package/src/components/ui/badge.tsx +52 -0
  59. package/src/components/ui/breadcrumb.tsx +125 -0
  60. package/src/components/ui/button.tsx +58 -0
  61. package/src/components/ui/calendar.tsx +221 -0
  62. package/src/components/ui/card.tsx +103 -0
  63. package/src/components/ui/carousel.tsx +240 -0
  64. package/src/components/ui/chart.tsx +373 -0
  65. package/src/components/ui/checkbox.tsx +29 -0
  66. package/src/components/ui/collapsible.tsx +19 -0
  67. package/src/components/ui/command.tsx +192 -0
  68. package/src/components/ui/context-menu.tsx +271 -0
  69. package/src/components/ui/dialog.tsx +160 -0
  70. package/src/components/ui/drawer.tsx +226 -0
  71. package/src/components/ui/dropdown-menu.tsx +268 -0
  72. package/src/components/ui/form.tsx +174 -0
  73. package/src/components/ui/hover-card.tsx +49 -0
  74. package/src/components/ui/input-group.tsx +158 -0
  75. package/src/components/ui/input-otp.tsx +87 -0
  76. package/src/components/ui/input.tsx +20 -0
  77. package/src/components/ui/label.tsx +18 -0
  78. package/src/components/ui/menubar.tsx +278 -0
  79. package/src/components/ui/navigation-menu.tsx +168 -0
  80. package/src/components/ui/pagination.tsx +130 -0
  81. package/src/components/ui/popover.tsx +91 -0
  82. package/src/components/ui/progress.tsx +81 -0
  83. package/src/components/ui/radio-group.tsx +38 -0
  84. package/src/components/ui/resizable.tsx +48 -0
  85. package/src/components/ui/scroll-area.tsx +55 -0
  86. package/src/components/ui/select.tsx +199 -0
  87. package/src/components/ui/separator.tsx +25 -0
  88. package/src/components/ui/sheet.tsx +136 -0
  89. package/src/components/ui/sidebar.tsx +723 -0
  90. package/src/components/ui/skeleton.tsx +13 -0
  91. package/src/components/ui/slider.tsx +52 -0
  92. package/src/components/ui/sonner.tsx +47 -0
  93. package/src/components/ui/switch.tsx +32 -0
  94. package/src/components/ui/table.tsx +114 -0
  95. package/src/components/ui/tabs.tsx +82 -0
  96. package/src/components/ui/textarea.tsx +18 -0
  97. package/src/components/ui/toggle-group.tsx +87 -0
  98. package/src/components/ui/toggle.tsx +43 -0
  99. package/src/components/ui/tooltip.tsx +66 -0
  100. package/src/hooks/use-mobile.ts +19 -0
  101. package/src/index.tsx +17 -0
  102. package/src/lib/utils.ts +19 -0
  103. package/src/pages/blog-posts/create.tsx +177 -0
  104. package/src/pages/blog-posts/edit.tsx +197 -0
  105. package/src/pages/blog-posts/index.ts +4 -0
  106. package/src/pages/blog-posts/list.tsx +131 -0
  107. package/src/pages/blog-posts/show.tsx +81 -0
  108. package/src/pages/categories/create.tsx +73 -0
  109. package/src/pages/categories/edit.tsx +73 -0
  110. package/src/pages/categories/index.ts +4 -0
  111. package/src/pages/categories/list.tsx +86 -0
  112. package/src/pages/categories/show.tsx +37 -0
  113. package/src/pages/forgot-password/index.tsx +6 -0
  114. package/src/pages/forgotPassword/index.tsx +5 -0
  115. package/src/pages/login/index.tsx +5 -0
  116. package/src/pages/register/index.tsx +5 -0
  117. package/src/providers/auth.ts +204 -0
  118. package/src/providers/constants.ts +75 -0
  119. package/src/providers/data.ts +232 -0
  120. package/src/vite-env.d.ts +1 -0
  121. package/tsconfig.json +25 -0
  122. package/tsconfig.node.json +21 -0
  123. package/vite.config.ts +74 -0
@@ -0,0 +1,271 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { ContextMenu as ContextMenuPrimitive } from "@base-ui/react/context-menu"
5
+
6
+ import { cn } from "@/lib/utils"
7
+ import { ChevronRightIcon, CheckIcon } from "lucide-react"
8
+
9
+ function ContextMenu({ ...props }: ContextMenuPrimitive.Root.Props) {
10
+ return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />
11
+ }
12
+
13
+ function ContextMenuPortal({ ...props }: ContextMenuPrimitive.Portal.Props) {
14
+ return (
15
+ <ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
16
+ )
17
+ }
18
+
19
+ function ContextMenuTrigger({
20
+ className,
21
+ ...props
22
+ }: ContextMenuPrimitive.Trigger.Props) {
23
+ return (
24
+ <ContextMenuPrimitive.Trigger
25
+ data-slot="context-menu-trigger"
26
+ className={cn("select-none", className)}
27
+ {...props}
28
+ />
29
+ )
30
+ }
31
+
32
+ function ContextMenuContent({
33
+ className,
34
+ align = "start",
35
+ alignOffset = 4,
36
+ side = "right",
37
+ sideOffset = 0,
38
+ ...props
39
+ }: ContextMenuPrimitive.Popup.Props &
40
+ Pick<
41
+ ContextMenuPrimitive.Positioner.Props,
42
+ "align" | "alignOffset" | "side" | "sideOffset"
43
+ >) {
44
+ return (
45
+ <ContextMenuPrimitive.Portal>
46
+ <ContextMenuPrimitive.Positioner
47
+ className="isolate z-50 outline-none"
48
+ align={align}
49
+ alignOffset={alignOffset}
50
+ side={side}
51
+ sideOffset={sideOffset}
52
+ >
53
+ <ContextMenuPrimitive.Popup
54
+ data-slot="context-menu-content"
55
+ className={cn("z-50 max-h-(--available-height) min-w-36 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 outline-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className )}
56
+ {...props}
57
+ />
58
+ </ContextMenuPrimitive.Positioner>
59
+ </ContextMenuPrimitive.Portal>
60
+ )
61
+ }
62
+
63
+ function ContextMenuGroup({ ...props }: ContextMenuPrimitive.Group.Props) {
64
+ return (
65
+ <ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
66
+ )
67
+ }
68
+
69
+ function ContextMenuLabel({
70
+ className,
71
+ inset,
72
+ ...props
73
+ }: ContextMenuPrimitive.GroupLabel.Props & {
74
+ inset?: boolean
75
+ }) {
76
+ return (
77
+ <ContextMenuPrimitive.GroupLabel
78
+ data-slot="context-menu-label"
79
+ data-inset={inset}
80
+ className={cn(
81
+ "px-1.5 py-1 text-xs font-medium text-muted-foreground data-inset:pl-7",
82
+ className
83
+ )}
84
+ {...props}
85
+ />
86
+ )
87
+ }
88
+
89
+ function ContextMenuItem({
90
+ className,
91
+ inset,
92
+ variant = "default",
93
+ ...props
94
+ }: ContextMenuPrimitive.Item.Props & {
95
+ inset?: boolean
96
+ variant?: "default" | "destructive"
97
+ }) {
98
+ return (
99
+ <ContextMenuPrimitive.Item
100
+ data-slot="context-menu-item"
101
+ data-inset={inset}
102
+ data-variant={variant}
103
+ className={cn(
104
+ "group/context-menu-item relative flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-inset:pl-7 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 focus:*:[svg]:text-accent-foreground data-[variant=destructive]:*:[svg]:text-destructive",
105
+ className
106
+ )}
107
+ {...props}
108
+ />
109
+ )
110
+ }
111
+
112
+ function ContextMenuSub({ ...props }: ContextMenuPrimitive.SubmenuRoot.Props) {
113
+ return (
114
+ <ContextMenuPrimitive.SubmenuRoot data-slot="context-menu-sub" {...props} />
115
+ )
116
+ }
117
+
118
+ function ContextMenuSubTrigger({
119
+ className,
120
+ inset,
121
+ children,
122
+ ...props
123
+ }: ContextMenuPrimitive.SubmenuTrigger.Props & {
124
+ inset?: boolean
125
+ }) {
126
+ return (
127
+ <ContextMenuPrimitive.SubmenuTrigger
128
+ data-slot="context-menu-sub-trigger"
129
+ data-inset={inset}
130
+ className={cn(
131
+ "flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-inset:pl-7 data-open:bg-accent data-open:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
132
+ className
133
+ )}
134
+ {...props}
135
+ >
136
+ {children}
137
+ <ChevronRightIcon className="ml-auto" />
138
+ </ContextMenuPrimitive.SubmenuTrigger>
139
+ )
140
+ }
141
+
142
+ function ContextMenuSubContent({
143
+ ...props
144
+ }: React.ComponentProps<typeof ContextMenuContent>) {
145
+ return (
146
+ <ContextMenuContent
147
+ data-slot="context-menu-sub-content"
148
+ className="shadow-lg"
149
+ side="right"
150
+ {...props}
151
+ />
152
+ )
153
+ }
154
+
155
+ function ContextMenuCheckboxItem({
156
+ className,
157
+ children,
158
+ checked,
159
+ inset,
160
+ ...props
161
+ }: ContextMenuPrimitive.CheckboxItem.Props & {
162
+ inset?: boolean
163
+ }) {
164
+ return (
165
+ <ContextMenuPrimitive.CheckboxItem
166
+ data-slot="context-menu-checkbox-item"
167
+ data-inset={inset}
168
+ className={cn(
169
+ "relative flex cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-inset:pl-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
170
+ className
171
+ )}
172
+ checked={checked}
173
+ {...props}
174
+ >
175
+ <span className="pointer-events-none absolute right-2">
176
+ <ContextMenuPrimitive.CheckboxItemIndicator>
177
+ <CheckIcon
178
+ />
179
+ </ContextMenuPrimitive.CheckboxItemIndicator>
180
+ </span>
181
+ {children}
182
+ </ContextMenuPrimitive.CheckboxItem>
183
+ )
184
+ }
185
+
186
+ function ContextMenuRadioGroup({
187
+ ...props
188
+ }: ContextMenuPrimitive.RadioGroup.Props) {
189
+ return (
190
+ <ContextMenuPrimitive.RadioGroup
191
+ data-slot="context-menu-radio-group"
192
+ {...props}
193
+ />
194
+ )
195
+ }
196
+
197
+ function ContextMenuRadioItem({
198
+ className,
199
+ children,
200
+ inset,
201
+ ...props
202
+ }: ContextMenuPrimitive.RadioItem.Props & {
203
+ inset?: boolean
204
+ }) {
205
+ return (
206
+ <ContextMenuPrimitive.RadioItem
207
+ data-slot="context-menu-radio-item"
208
+ data-inset={inset}
209
+ className={cn(
210
+ "relative flex cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-inset:pl-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
211
+ className
212
+ )}
213
+ {...props}
214
+ >
215
+ <span className="pointer-events-none absolute right-2">
216
+ <ContextMenuPrimitive.RadioItemIndicator>
217
+ <CheckIcon
218
+ />
219
+ </ContextMenuPrimitive.RadioItemIndicator>
220
+ </span>
221
+ {children}
222
+ </ContextMenuPrimitive.RadioItem>
223
+ )
224
+ }
225
+
226
+ function ContextMenuSeparator({
227
+ className,
228
+ ...props
229
+ }: ContextMenuPrimitive.Separator.Props) {
230
+ return (
231
+ <ContextMenuPrimitive.Separator
232
+ data-slot="context-menu-separator"
233
+ className={cn("-mx-1 my-1 h-px bg-border", className)}
234
+ {...props}
235
+ />
236
+ )
237
+ }
238
+
239
+ function ContextMenuShortcut({
240
+ className,
241
+ ...props
242
+ }: React.ComponentProps<"span">) {
243
+ return (
244
+ <span
245
+ data-slot="context-menu-shortcut"
246
+ className={cn(
247
+ "ml-auto text-xs tracking-widest text-muted-foreground group-focus/context-menu-item:text-accent-foreground",
248
+ className
249
+ )}
250
+ {...props}
251
+ />
252
+ )
253
+ }
254
+
255
+ export {
256
+ ContextMenu,
257
+ ContextMenuTrigger,
258
+ ContextMenuContent,
259
+ ContextMenuItem,
260
+ ContextMenuCheckboxItem,
261
+ ContextMenuRadioItem,
262
+ ContextMenuLabel,
263
+ ContextMenuSeparator,
264
+ ContextMenuShortcut,
265
+ ContextMenuGroup,
266
+ ContextMenuPortal,
267
+ ContextMenuSub,
268
+ ContextMenuSubContent,
269
+ ContextMenuSubTrigger,
270
+ ContextMenuRadioGroup,
271
+ }
@@ -0,0 +1,160 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Dialog as DialogPrimitive } from "@base-ui/react/dialog"
5
+
6
+ import { cn } from "@/lib/utils"
7
+ import { Button } from "@/components/ui/button"
8
+ import { XIcon } from "lucide-react"
9
+
10
+ function Dialog({ ...props }: DialogPrimitive.Root.Props) {
11
+ return <DialogPrimitive.Root data-slot="dialog" {...props} />
12
+ }
13
+
14
+ function DialogTrigger({ ...props }: DialogPrimitive.Trigger.Props) {
15
+ return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
16
+ }
17
+
18
+ function DialogPortal({ ...props }: DialogPrimitive.Portal.Props) {
19
+ return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
20
+ }
21
+
22
+ function DialogClose({ ...props }: DialogPrimitive.Close.Props) {
23
+ return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
24
+ }
25
+
26
+ function DialogOverlay({
27
+ className,
28
+ ...props
29
+ }: DialogPrimitive.Backdrop.Props) {
30
+ return (
31
+ <DialogPrimitive.Backdrop
32
+ data-slot="dialog-overlay"
33
+ className={cn(
34
+ "fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
35
+ className
36
+ )}
37
+ {...props}
38
+ />
39
+ )
40
+ }
41
+
42
+ function DialogContent({
43
+ className,
44
+ children,
45
+ showCloseButton = true,
46
+ ...props
47
+ }: DialogPrimitive.Popup.Props & {
48
+ showCloseButton?: boolean
49
+ }) {
50
+ return (
51
+ <DialogPortal>
52
+ <DialogOverlay />
53
+ <DialogPrimitive.Popup
54
+ data-slot="dialog-content"
55
+ className={cn(
56
+ "fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-4 text-sm text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
57
+ className
58
+ )}
59
+ {...props}
60
+ >
61
+ {children}
62
+ {showCloseButton && (
63
+ <DialogPrimitive.Close
64
+ data-slot="dialog-close"
65
+ render={
66
+ <Button
67
+ variant="ghost"
68
+ className="absolute top-2 right-2"
69
+ size="icon-sm"
70
+ />
71
+ }
72
+ >
73
+ <XIcon
74
+ />
75
+ <span className="sr-only">Close</span>
76
+ </DialogPrimitive.Close>
77
+ )}
78
+ </DialogPrimitive.Popup>
79
+ </DialogPortal>
80
+ )
81
+ }
82
+
83
+ function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
84
+ return (
85
+ <div
86
+ data-slot="dialog-header"
87
+ className={cn("flex flex-col gap-2", className)}
88
+ {...props}
89
+ />
90
+ )
91
+ }
92
+
93
+ function DialogFooter({
94
+ className,
95
+ showCloseButton = false,
96
+ children,
97
+ ...props
98
+ }: React.ComponentProps<"div"> & {
99
+ showCloseButton?: boolean
100
+ }) {
101
+ return (
102
+ <div
103
+ data-slot="dialog-footer"
104
+ className={cn(
105
+ "-mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t bg-muted/50 p-4 sm:flex-row sm:justify-end",
106
+ className
107
+ )}
108
+ {...props}
109
+ >
110
+ {children}
111
+ {showCloseButton && (
112
+ <DialogPrimitive.Close render={<Button variant="outline" />}>
113
+ Close
114
+ </DialogPrimitive.Close>
115
+ )}
116
+ </div>
117
+ )
118
+ }
119
+
120
+ function DialogTitle({ className, ...props }: DialogPrimitive.Title.Props) {
121
+ return (
122
+ <DialogPrimitive.Title
123
+ data-slot="dialog-title"
124
+ className={cn(
125
+ "font-heading text-base leading-none font-medium",
126
+ className
127
+ )}
128
+ {...props}
129
+ />
130
+ )
131
+ }
132
+
133
+ function DialogDescription({
134
+ className,
135
+ ...props
136
+ }: DialogPrimitive.Description.Props) {
137
+ return (
138
+ <DialogPrimitive.Description
139
+ data-slot="dialog-description"
140
+ className={cn(
141
+ "text-sm text-muted-foreground *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",
142
+ className
143
+ )}
144
+ {...props}
145
+ />
146
+ )
147
+ }
148
+
149
+ export {
150
+ Dialog,
151
+ DialogClose,
152
+ DialogContent,
153
+ DialogDescription,
154
+ DialogFooter,
155
+ DialogHeader,
156
+ DialogOverlay,
157
+ DialogPortal,
158
+ DialogTitle,
159
+ DialogTrigger,
160
+ }
@@ -0,0 +1,226 @@
1
+ import * as React from "react"
2
+ import { Drawer as DrawerPrimitive } from "@base-ui/react/drawer"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ type DrawerContextProps = {
7
+ hasSnapPoints: boolean
8
+ modal: DrawerPrimitive.Root.Props["modal"]
9
+ showSwipeHandle: boolean
10
+ swipeDirection: NonNullable<DrawerPrimitive.Root.Props["swipeDirection"]>
11
+ }
12
+
13
+ const DrawerContext = React.createContext<DrawerContextProps | null>(null)
14
+
15
+ function useDrawer() {
16
+ const context = React.useContext(DrawerContext)
17
+
18
+ if (!context) {
19
+ throw new Error("useDrawer must be used within a Drawer.")
20
+ }
21
+
22
+ return context
23
+ }
24
+
25
+ function Drawer({
26
+ modal = true,
27
+ showSwipeHandle = false,
28
+ snapPoints,
29
+ swipeDirection = "down",
30
+ ...props
31
+ }: DrawerPrimitive.Root.Props & {
32
+ showSwipeHandle?: boolean
33
+ }) {
34
+ const hasSnapPoints = snapPoints != null && snapPoints.length > 0
35
+ const contextValue = React.useMemo(
36
+ () => ({ hasSnapPoints, modal, showSwipeHandle, swipeDirection }),
37
+ [hasSnapPoints, modal, showSwipeHandle, swipeDirection]
38
+ )
39
+
40
+ return (
41
+ <DrawerContext.Provider value={contextValue}>
42
+ <DrawerPrimitive.Root
43
+ data-slot="drawer"
44
+ modal={modal}
45
+ snapPoints={snapPoints}
46
+ swipeDirection={swipeDirection}
47
+ {...props}
48
+ />
49
+ </DrawerContext.Provider>
50
+ )
51
+ }
52
+
53
+ function DrawerTrigger({ ...props }: DrawerPrimitive.Trigger.Props) {
54
+ return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />
55
+ }
56
+
57
+ function DrawerPortal({ ...props }: DrawerPrimitive.Portal.Props) {
58
+ return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />
59
+ }
60
+
61
+ function DrawerClose({ ...props }: DrawerPrimitive.Close.Props) {
62
+ return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />
63
+ }
64
+
65
+ function DrawerOverlay({
66
+ className,
67
+ ...props
68
+ }: DrawerPrimitive.Backdrop.Props) {
69
+ return (
70
+ <DrawerPrimitive.Backdrop
71
+ data-slot="drawer-overlay"
72
+ className={cn(
73
+ "fixed inset-0 z-50 min-h-dvh bg-black/10 opacity-[max(var(--drawer-overlay-min-opacity,0),calc(1-var(--drawer-swipe-progress)))] transition-opacity duration-450 ease-[cubic-bezier(0.32,0.72,0,1)] select-none data-ending-style:pointer-events-none data-ending-style:opacity-0 data-ending-style:duration-[calc(var(--drawer-swipe-strength)*400ms)] data-snap-points:[--drawer-overlay-min-opacity:0.5] data-starting-style:opacity-0 data-swiping:duration-0 supports-backdrop-filter:backdrop-blur-xs supports-[-webkit-touch-callout:none]:absolute",
74
+ className
75
+ )}
76
+ {...props}
77
+ />
78
+ )
79
+ }
80
+
81
+ function DrawerSwipeHandle({
82
+ className,
83
+ ...props
84
+ }: React.ComponentProps<"div">) {
85
+ return (
86
+ <div
87
+ data-slot="drawer-swipe-handle"
88
+ aria-hidden="true"
89
+ className={cn(
90
+ "relative z-10 flex shrink-0 cursor-grab transition-opacity duration-200 group-data-nested-drawer-open/drawer-popup:opacity-0 group-data-nested-drawer-swiping/drawer-popup:opacity-100 group-data-[swipe-axis=x]/drawer-popup:h-full group-data-[swipe-axis=x]/drawer-popup:w-3 group-data-[swipe-axis=x]/drawer-popup:items-center group-data-[swipe-axis=y]/drawer-popup:h-3 group-data-[swipe-axis=y]/drawer-popup:w-full group-data-[swipe-axis=y]/drawer-popup:justify-center group-data-[swipe-direction=down]/drawer-popup:items-end group-data-[swipe-direction=left]/drawer-popup:order-last group-data-[swipe-direction=left]/drawer-popup:justify-start group-data-[swipe-direction=right]/drawer-popup:justify-end group-data-[swipe-direction=up]/drawer-popup:order-last group-data-[swipe-direction=up]/drawer-popup:items-start after:block after:shrink-0 after:rounded-full after:bg-muted group-data-[swipe-axis=x]/drawer-popup:after:h-24 group-data-[swipe-axis=x]/drawer-popup:after:w-1 group-data-[swipe-axis=y]/drawer-popup:after:h-1 group-data-[swipe-axis=y]/drawer-popup:after:w-24 active:cursor-grabbing",
91
+ className
92
+ )}
93
+ {...props}
94
+ />
95
+ )
96
+ }
97
+
98
+ function DrawerContent({
99
+ className,
100
+ children,
101
+ ...props
102
+ }: DrawerPrimitive.Popup.Props) {
103
+ const { hasSnapPoints, modal, showSwipeHandle, swipeDirection } = useDrawer()
104
+ const swipeAxis =
105
+ swipeDirection === "down" || swipeDirection === "up" ? "y" : "x"
106
+
107
+ return (
108
+ <DrawerPortal data-slot="drawer-portal">
109
+ {modal === true && (
110
+ <DrawerOverlay data-snap-points={hasSnapPoints ? "" : undefined} />
111
+ )}
112
+ <DrawerPrimitive.Viewport
113
+ data-slot="drawer-viewport"
114
+ data-modal={modal}
115
+ className="pointer-events-none fixed inset-0 z-50 select-none data-[modal=true]:pointer-events-auto"
116
+ >
117
+ <DrawerPrimitive.Popup
118
+ data-slot="drawer-popup"
119
+ data-swipe-axis={swipeAxis}
120
+ data-snap-points={hasSnapPoints ? "" : undefined}
121
+ className={cn(
122
+ // Base.
123
+ "group/drawer-popup pointer-events-auto fixed z-50 m-(--drawer-inset,0px) flex h-(--drawer-content-height) max-h-(--drawer-content-max-height,none) min-h-0 w-(--drawer-content-width,auto) transform-[translate3d(var(--translate-x,0px),var(--translate-y,0px),0)_scale(var(--stack-scale))] flex-col bg-popover text-sm text-popover-foreground transition-[transform,height,opacity,filter] duration-450 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-transform outline-none select-none [interpolate-size:allow-keywords] data-[swipe-direction=down]:rounded-t-xl data-[swipe-direction=down]:border-t data-[swipe-direction=left]:rounded-r-xl data-[swipe-direction=left]:border-r data-[swipe-direction=right]:rounded-l-xl data-[swipe-direction=right]:border-l data-[swipe-direction=up]:rounded-b-xl data-[swipe-direction=up]:border-b",
124
+ // Nested.
125
+ "data-nested-drawer-open:overflow-hidden data-nested-drawer-open:brightness-95",
126
+ // Bleed.
127
+ "after:pointer-events-none after:absolute after:bg-(--drawer-bleed-background,var(--color-popover)) data-[swipe-axis=x]:after:inset-y-0 data-[swipe-axis=x]:after:w-(--bleed) data-[swipe-axis=y]:after:inset-x-0 data-[swipe-axis=y]:after:h-(--bleed) data-[swipe-direction=down]:after:top-full data-[swipe-direction=left]:after:right-full data-[swipe-direction=right]:after:left-full data-[swipe-direction=up]:after:bottom-full",
128
+ // Sizing.
129
+ "[--drawer-content-height:var(--drawer-height,auto)] data-[swipe-axis=x]:[--drawer-content-width:75%] data-[swipe-axis=y]:[--drawer-content-max-height:calc(100dvh-6rem)] data-[swipe-axis=y]:data-snap-points:[--drawer-content-height:100dvh] data-[swipe-axis=x]:sm:[--drawer-content-width:24rem]",
130
+ // Stack.
131
+ "[--bleed:3rem] [--peek:1rem] [--stack-height:var(--drawer-frontmost-height,var(--drawer-height,0px))] [--stack-peek-offset:max(0px,calc((var(--nested-drawers)-var(--stack-progress))*var(--peek)))] [--stack-progress:clamp(0,var(--drawer-swipe-progress),1)] [--stack-scale-base:max(0,calc(1-(var(--nested-drawers)*var(--stack-step))))] [--stack-scale:clamp(0,calc(var(--stack-scale-base)+(var(--stack-step)*var(--stack-progress))),1)] [--stack-shrink:calc(1-var(--stack-scale))] [--stack-step:0.05]",
132
+ // Transitions.
133
+ "data-ending-style:transform-(--closed-transform) data-ending-style:opacity-[0.9999] data-ending-style:duration-[calc(var(--drawer-swipe-strength)*400ms)] data-nested-drawer-swiping:duration-0 data-ending-style:data-nested-drawer-swiping:duration-[calc(var(--drawer-swipe-strength)*400ms)] data-starting-style:transform-(--closed-transform) data-swiping:duration-0 data-ending-style:data-swiping:duration-[calc(var(--drawer-swipe-strength)*400ms)]",
134
+ // Axis: y.
135
+ "data-[swipe-axis=y]:inset-x-0 data-[swipe-axis=y]:data-nested-drawer-open:h-(--stack-height)",
136
+ // Axis: x.
137
+ "data-[swipe-axis=x]:inset-y-0 data-[swipe-axis=x]:flex-row",
138
+ // Direction: down.
139
+ "data-[swipe-direction=down]:bottom-0 data-[swipe-direction=down]:origin-bottom data-[swipe-direction=down]:[--closed-transform:translate3d(0,calc(100%+var(--drawer-inset,0px)+2px),0)] data-[swipe-direction=down]:[--translate-y:calc(var(--drawer-snap-point-offset,0px)+var(--drawer-swipe-movement-y)-var(--stack-peek-offset)-(var(--stack-shrink)*var(--stack-height)))]",
140
+ // Direction: up.
141
+ "data-[swipe-direction=up]:top-0 data-[swipe-direction=up]:origin-top data-[swipe-direction=up]:[--closed-transform:translate3d(0,calc(-100%-var(--drawer-inset,0px)-2px),0)] data-[swipe-direction=up]:[--translate-y:calc(var(--drawer-snap-point-offset,0px)+var(--drawer-swipe-movement-y)+var(--stack-peek-offset)+(var(--stack-shrink)*var(--stack-height)))]",
142
+ // Direction: left.
143
+ "data-[swipe-direction=left]:left-0 data-[swipe-direction=left]:origin-left data-[swipe-direction=left]:[--closed-transform:translate3d(calc(-100%-var(--drawer-inset,0px)-2px),0,0)] data-[swipe-direction=left]:[--translate-x:calc(var(--drawer-swipe-movement-x)+var(--stack-peek-offset)+(var(--stack-shrink)*100%))]",
144
+ // Direction: right.
145
+ "data-[swipe-direction=right]:right-0 data-[swipe-direction=right]:origin-right data-[swipe-direction=right]:[--closed-transform:translate3d(calc(100%+var(--drawer-inset,0px)+2px),0,0)] data-[swipe-direction=right]:[--translate-x:calc(var(--drawer-swipe-movement-x)-var(--stack-peek-offset)-(var(--stack-shrink)*100%))]",
146
+ className
147
+ )}
148
+ {...props}
149
+ >
150
+ {showSwipeHandle && <DrawerSwipeHandle />}
151
+ <DrawerPrimitive.Content
152
+ data-slot="drawer-content"
153
+ className={cn(
154
+ "flex min-h-0 flex-1 flex-col overflow-hidden overscroll-contain rounded-[inherit] transition-opacity duration-300 ease-[cubic-bezier(0.45,1.005,0,1.005)] select-text group-data-nested-drawer-open/drawer-popup:opacity-0 group-data-nested-drawer-swiping/drawer-popup:opacity-100 group-data-swiping/drawer-popup:select-none"
155
+ )}
156
+ >
157
+ {children}
158
+ </DrawerPrimitive.Content>
159
+ </DrawerPrimitive.Popup>
160
+ </DrawerPrimitive.Viewport>
161
+ </DrawerPortal>
162
+ )
163
+ }
164
+
165
+ function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
166
+ return (
167
+ <div
168
+ data-slot="drawer-header"
169
+ className={cn(
170
+ "flex shrink-0 flex-col gap-0.5 p-4 pb-0 group-data-[swipe-axis=y]/drawer-popup:text-center md:gap-0.5 md:text-left",
171
+ className
172
+ )}
173
+ {...props}
174
+ />
175
+ )
176
+ }
177
+
178
+ function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
179
+ return (
180
+ <div
181
+ data-slot="drawer-footer"
182
+ className={cn("mt-auto flex shrink-0 flex-col gap-2 p-4 pt-0", className)}
183
+ {...props}
184
+ />
185
+ )
186
+ }
187
+
188
+ function DrawerTitle({ className, ...props }: DrawerPrimitive.Title.Props) {
189
+ return (
190
+ <DrawerPrimitive.Title
191
+ data-slot="drawer-title"
192
+ className={cn(
193
+ "font-heading text-base font-medium text-foreground",
194
+ className
195
+ )}
196
+ {...props}
197
+ />
198
+ )
199
+ }
200
+
201
+ function DrawerDescription({
202
+ className,
203
+ ...props
204
+ }: DrawerPrimitive.Description.Props) {
205
+ return (
206
+ <DrawerPrimitive.Description
207
+ data-slot="drawer-description"
208
+ className={cn("text-sm text-balance text-muted-foreground", className)}
209
+ {...props}
210
+ />
211
+ )
212
+ }
213
+
214
+ export {
215
+ Drawer,
216
+ DrawerPortal,
217
+ DrawerOverlay,
218
+ DrawerSwipeHandle,
219
+ DrawerTrigger,
220
+ DrawerClose,
221
+ DrawerContent,
222
+ DrawerHeader,
223
+ DrawerFooter,
224
+ DrawerTitle,
225
+ DrawerDescription,
226
+ }