@questpie/admin 0.0.1 → 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.
- package/README.md +439 -424
- package/dist/auth-layout-M8K8_q5R.mjs +181 -0
- package/dist/auth-layout-M8K8_q5R.mjs.map +1 -0
- package/dist/bulk-upload-dialog-h7zXD78Y.mjs +274 -0
- package/dist/bulk-upload-dialog-h7zXD78Y.mjs.map +1 -0
- package/dist/{components/ui/card.mjs → card-BKHjBQfw.mjs} +8 -8
- package/dist/card-BKHjBQfw.mjs.map +1 -0
- package/dist/client/styles/index.css +434 -0
- package/dist/client-BCGpkAz6.mjs +22635 -0
- package/dist/client-BCGpkAz6.mjs.map +1 -0
- package/dist/client-CcWZbkBP.d.mts +13585 -0
- package/dist/client-CcWZbkBP.d.mts.map +1 -0
- package/dist/client.d.mts +3 -0
- package/dist/client.mjs +14 -0
- package/dist/content-locales-provider-BXvuIgfg.mjs +1650 -0
- package/dist/content-locales-provider-BXvuIgfg.mjs.map +1 -0
- package/dist/dashboard-page-B4PGEdc2.mjs +2500 -0
- package/dist/dashboard-page-B4PGEdc2.mjs.map +1 -0
- package/dist/dashboard-page-CVlyR40m.mjs +6 -0
- package/dist/dropzone-Do3awXKd.mjs +634 -0
- package/dist/dropzone-Do3awXKd.mjs.map +1 -0
- package/dist/{views/auth/forgot-password-form.mjs → forgot-password-page-Bcp-An4Y.mjs} +87 -14
- package/dist/forgot-password-page-Bcp-An4Y.mjs.map +1 -0
- package/dist/forgot-password-page-CIILVhfo.mjs +7 -0
- package/dist/index-B9Xwk4hi.d.mts +2753 -0
- package/dist/index-B9Xwk4hi.d.mts.map +1 -0
- package/dist/index.d.mts +3 -0
- package/dist/index.mjs +14 -0
- package/dist/login-page-8K7fo0qK.mjs +7 -0
- package/dist/login-page-CP4gA-dl.mjs +298 -0
- package/dist/login-page-CP4gA-dl.mjs.map +1 -0
- package/dist/preview-utils-BKQ9-TMa.mjs +65 -0
- package/dist/preview-utils-BKQ9-TMa.mjs.map +1 -0
- package/dist/{views/auth/reset-password-form.mjs → reset-password-page-BqfDmLxA.mjs} +111 -14
- package/dist/reset-password-page-BqfDmLxA.mjs.map +1 -0
- package/dist/reset-password-page-DLATv0xQ.mjs +7 -0
- package/dist/runtime-6VZM878K.mjs +69 -0
- package/dist/runtime-6VZM878K.mjs.map +1 -0
- package/dist/saved-views.types-BMsz5mCy.d.mts +42 -0
- package/dist/saved-views.types-BMsz5mCy.d.mts.map +1 -0
- package/dist/server.d.mts +250 -0
- package/dist/server.d.mts.map +1 -0
- package/dist/server.mjs +832 -0
- package/dist/server.mjs.map +1 -0
- package/dist/setup-page-CMZ5P_OE.mjs +6 -0
- package/dist/setup-page-YAP_fzqh.mjs +264 -0
- package/dist/setup-page-YAP_fzqh.mjs.map +1 -0
- package/dist/shared.d.mts +57 -0
- package/dist/shared.d.mts.map +1 -0
- package/dist/shared.mjs +3 -0
- package/dist/{hooks/use-auth.mjs → use-auth-BoLmWtmU.mjs} +42 -30
- package/dist/use-auth-BoLmWtmU.mjs.map +1 -0
- package/package.json +48 -197
- package/.turbo/turbo-build.log +0 -108
- package/CHANGELOG.md +0 -10
- package/STATUS.md +0 -917
- package/VALIDATION.md +0 -602
- package/components.json +0 -24
- package/dist/__tests__/setup.mjs +0 -38
- package/dist/__tests__/test-utils.mjs +0 -45
- package/dist/__tests__/vitest.d.mjs +0 -3
- package/dist/components/admin-app.mjs +0 -69
- package/dist/components/fields/array-field.mjs +0 -190
- package/dist/components/fields/checkbox-field.mjs +0 -34
- package/dist/components/fields/custom-field.mjs +0 -32
- package/dist/components/fields/date-field.mjs +0 -41
- package/dist/components/fields/datetime-field.mjs +0 -42
- package/dist/components/fields/email-field.mjs +0 -37
- package/dist/components/fields/embedded-collection.mjs +0 -253
- package/dist/components/fields/field-types.mjs +0 -1
- package/dist/components/fields/field-utils.mjs +0 -10
- package/dist/components/fields/field-wrapper.mjs +0 -34
- package/dist/components/fields/index.mjs +0 -23
- package/dist/components/fields/json-field.mjs +0 -243
- package/dist/components/fields/locale-badge.mjs +0 -16
- package/dist/components/fields/number-field.mjs +0 -39
- package/dist/components/fields/password-field.mjs +0 -37
- package/dist/components/fields/relation-field.mjs +0 -104
- package/dist/components/fields/relation-picker.mjs +0 -229
- package/dist/components/fields/relation-select.mjs +0 -188
- package/dist/components/fields/rich-text-editor/index.mjs +0 -897
- package/dist/components/fields/select-field.mjs +0 -41
- package/dist/components/fields/switch-field.mjs +0 -34
- package/dist/components/fields/text-field.mjs +0 -38
- package/dist/components/fields/textarea-field.mjs +0 -38
- package/dist/components/index.mjs +0 -59
- package/dist/components/primitives/checkbox-input.mjs +0 -127
- package/dist/components/primitives/date-input.mjs +0 -303
- package/dist/components/primitives/index.mjs +0 -12
- package/dist/components/primitives/number-input.mjs +0 -104
- package/dist/components/primitives/select-input.mjs +0 -177
- package/dist/components/primitives/tag-input.mjs +0 -135
- package/dist/components/primitives/text-input.mjs +0 -39
- package/dist/components/primitives/textarea-input.mjs +0 -37
- package/dist/components/primitives/toggle-input.mjs +0 -31
- package/dist/components/primitives/types.mjs +0 -12
- package/dist/components/ui/accordion.mjs +0 -55
- package/dist/components/ui/avatar.mjs +0 -54
- package/dist/components/ui/badge.mjs +0 -34
- package/dist/components/ui/button.mjs +0 -48
- package/dist/components/ui/checkbox.mjs +0 -21
- package/dist/components/ui/combobox.mjs +0 -163
- package/dist/components/ui/dialog.mjs +0 -95
- package/dist/components/ui/dropdown-menu.mjs +0 -138
- package/dist/components/ui/field.mjs +0 -113
- package/dist/components/ui/input-group.mjs +0 -82
- package/dist/components/ui/input.mjs +0 -17
- package/dist/components/ui/label.mjs +0 -15
- package/dist/components/ui/popover.mjs +0 -56
- package/dist/components/ui/scroll-area.mjs +0 -38
- package/dist/components/ui/select.mjs +0 -100
- package/dist/components/ui/separator.mjs +0 -16
- package/dist/components/ui/sheet.mjs +0 -90
- package/dist/components/ui/sidebar.mjs +0 -387
- package/dist/components/ui/skeleton.mjs +0 -14
- package/dist/components/ui/spinner.mjs +0 -16
- package/dist/components/ui/switch.mjs +0 -22
- package/dist/components/ui/table.mjs +0 -68
- package/dist/components/ui/tabs.mjs +0 -48
- package/dist/components/ui/textarea.mjs +0 -15
- package/dist/components/ui/tooltip.mjs +0 -44
- package/dist/config/component-registry.mjs +0 -38
- package/dist/config/index.mjs +0 -129
- package/dist/hooks/admin-provider.mjs +0 -70
- package/dist/hooks/index.mjs +0 -7
- package/dist/hooks/store.mjs +0 -178
- package/dist/hooks/use-collection-db.mjs +0 -146
- package/dist/hooks/use-collection.mjs +0 -112
- package/dist/hooks/use-global.mjs +0 -46
- package/dist/hooks/use-mobile.mjs +0 -20
- package/dist/lib/utils.mjs +0 -10
- package/dist/styles/index.css +0 -336
- package/dist/styles/index.mjs +0 -1
- package/dist/utils/index.mjs +0 -9
- package/dist/views/auth/auth-layout.mjs +0 -52
- package/dist/views/auth/index.mjs +0 -6
- package/dist/views/auth/login-form.mjs +0 -156
- package/dist/views/collection/auto-form-fields.mjs +0 -525
- package/dist/views/collection/collection-form.mjs +0 -91
- package/dist/views/collection/collection-list.mjs +0 -76
- package/dist/views/collection/form-field.mjs +0 -42
- package/dist/views/collection/index.mjs +0 -6
- package/dist/views/common/index.mjs +0 -4
- package/dist/views/common/locale-switcher.mjs +0 -39
- package/dist/views/common/version-history.mjs +0 -272
- package/dist/views/index.mjs +0 -9
- package/dist/views/layout/admin-layout.mjs +0 -40
- package/dist/views/layout/admin-router.mjs +0 -95
- package/dist/views/layout/admin-sidebar.mjs +0 -63
- package/dist/views/layout/index.mjs +0 -5
- package/src/__tests__/setup.ts +0 -44
- package/src/__tests__/test-utils.tsx +0 -49
- package/src/__tests__/vitest.d.ts +0 -9
- package/src/components/admin-app.tsx +0 -221
- package/src/components/fields/array-field.tsx +0 -237
- package/src/components/fields/checkbox-field.tsx +0 -47
- package/src/components/fields/custom-field.tsx +0 -50
- package/src/components/fields/date-field.tsx +0 -65
- package/src/components/fields/datetime-field.tsx +0 -67
- package/src/components/fields/email-field.tsx +0 -51
- package/src/components/fields/embedded-collection.tsx +0 -315
- package/src/components/fields/field-types.ts +0 -162
- package/src/components/fields/field-utils.ts +0 -6
- package/src/components/fields/field-wrapper.tsx +0 -52
- package/src/components/fields/index.ts +0 -66
- package/src/components/fields/json-field.tsx +0 -440
- package/src/components/fields/locale-badge.tsx +0 -15
- package/src/components/fields/number-field.tsx +0 -57
- package/src/components/fields/password-field.tsx +0 -51
- package/src/components/fields/relation-field.tsx +0 -243
- package/src/components/fields/relation-picker.tsx +0 -402
- package/src/components/fields/relation-select.tsx +0 -327
- package/src/components/fields/rich-text-editor/index.tsx +0 -1337
- package/src/components/fields/select-field.tsx +0 -61
- package/src/components/fields/switch-field.tsx +0 -47
- package/src/components/fields/text-field.tsx +0 -55
- package/src/components/fields/textarea-field.tsx +0 -55
- package/src/components/index.ts +0 -40
- package/src/components/primitives/checkbox-input.tsx +0 -193
- package/src/components/primitives/date-input.tsx +0 -401
- package/src/components/primitives/index.ts +0 -24
- package/src/components/primitives/number-input.tsx +0 -132
- package/src/components/primitives/select-input.tsx +0 -296
- package/src/components/primitives/tag-input.tsx +0 -200
- package/src/components/primitives/text-input.tsx +0 -49
- package/src/components/primitives/textarea-input.tsx +0 -46
- package/src/components/primitives/toggle-input.tsx +0 -36
- package/src/components/primitives/types.ts +0 -235
- package/src/components/ui/accordion.tsx +0 -72
- package/src/components/ui/avatar.tsx +0 -106
- package/src/components/ui/badge.tsx +0 -48
- package/src/components/ui/button.tsx +0 -53
- package/src/components/ui/card.tsx +0 -94
- package/src/components/ui/checkbox.tsx +0 -27
- package/src/components/ui/combobox.tsx +0 -290
- package/src/components/ui/dialog.tsx +0 -151
- package/src/components/ui/dropdown-menu.tsx +0 -254
- package/src/components/ui/field.tsx +0 -227
- package/src/components/ui/input-group.tsx +0 -149
- package/src/components/ui/input.tsx +0 -20
- package/src/components/ui/label.tsx +0 -18
- package/src/components/ui/popover.tsx +0 -88
- package/src/components/ui/scroll-area.tsx +0 -53
- package/src/components/ui/select.tsx +0 -192
- package/src/components/ui/separator.tsx +0 -23
- package/src/components/ui/sheet.tsx +0 -127
- package/src/components/ui/sidebar.tsx +0 -723
- package/src/components/ui/skeleton.tsx +0 -13
- package/src/components/ui/spinner.tsx +0 -10
- package/src/components/ui/switch.tsx +0 -32
- package/src/components/ui/table.tsx +0 -99
- package/src/components/ui/tabs.tsx +0 -82
- package/src/components/ui/textarea.tsx +0 -18
- package/src/components/ui/tooltip.tsx +0 -70
- package/src/config/component-registry.ts +0 -190
- package/src/config/index.ts +0 -1099
- package/src/hooks/README.md +0 -269
- package/src/hooks/admin-provider.tsx +0 -110
- package/src/hooks/index.ts +0 -41
- package/src/hooks/store.ts +0 -248
- package/src/hooks/use-auth.ts +0 -168
- package/src/hooks/use-collection-db.ts +0 -209
- package/src/hooks/use-collection.ts +0 -156
- package/src/hooks/use-global.ts +0 -69
- package/src/hooks/use-mobile.ts +0 -21
- package/src/lib/utils.ts +0 -6
- package/src/styles/index.css +0 -340
- package/src/utils/index.ts +0 -6
- package/src/views/auth/auth-layout.tsx +0 -77
- package/src/views/auth/forgot-password-form.tsx +0 -192
- package/src/views/auth/index.ts +0 -21
- package/src/views/auth/login-form.tsx +0 -229
- package/src/views/auth/reset-password-form.tsx +0 -232
- package/src/views/collection/auto-form-fields.tsx +0 -982
- package/src/views/collection/collection-form.tsx +0 -186
- package/src/views/collection/collection-list.tsx +0 -223
- package/src/views/collection/form-field.tsx +0 -52
- package/src/views/collection/index.ts +0 -15
- package/src/views/common/index.ts +0 -8
- package/src/views/common/locale-switcher.tsx +0 -45
- package/src/views/common/version-history.tsx +0 -406
- package/src/views/index.ts +0 -25
- package/src/views/layout/admin-layout.tsx +0 -117
- package/src/views/layout/admin-router.tsx +0 -206
- package/src/views/layout/admin-sidebar.tsx +0 -185
- package/src/views/layout/index.ts +0 -12
- package/tsconfig.json +0 -13
- package/tsconfig.tsbuildinfo +0 -1
- package/tsdown.config.ts +0 -13
- package/vitest.config.ts +0 -29
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dropzone-Do3awXKd.mjs","names":["React","Dialog","DialogPrimitive","Drawer","DrawerPrimitive","React","Drawer","DialogPrimitive","React","valid: File[]","errors: ValidationError[]","parts: string[]"],"sources":["../src/client/hooks/use-media-query.ts","../src/client/components/ui/dialog.tsx","../src/client/components/ui/drawer.tsx","../src/client/components/ui/responsive-dialog.tsx","../src/client/hooks/use-upload.ts","../src/client/components/primitives/dropzone.tsx"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\n\n/**\n * Hook to detect media query matches\n *\n * @example\n * ```tsx\n * const isMobile = useMediaQuery(\"(max-width: 768px)\");\n * const isDesktop = useMediaQuery(\"(min-width: 769px)\");\n * const prefersDark = useMediaQuery(\"(prefers-color-scheme: dark)\");\n * ```\n */\nexport function useMediaQuery(query: string): boolean {\n const [matches, setMatches] = React.useState<boolean>(() => {\n // Check if we're on the server\n if (typeof window === \"undefined\") {\n return false;\n }\n return window.matchMedia(query).matches;\n });\n\n React.useEffect(() => {\n if (typeof window === \"undefined\") {\n return;\n }\n\n const mediaQuery = window.matchMedia(query);\n\n // Set initial value\n setMatches(mediaQuery.matches);\n\n // Create listener\n const listener = (event: MediaQueryListEvent) => {\n setMatches(event.matches);\n };\n\n // Add listener (using modern API with fallback)\n if (mediaQuery.addEventListener) {\n mediaQuery.addEventListener(\"change\", listener);\n } else {\n // Fallback for older browsers\n mediaQuery.addListener(listener);\n }\n\n // Cleanup\n return () => {\n if (mediaQuery.removeEventListener) {\n mediaQuery.removeEventListener(\"change\", listener);\n } else {\n mediaQuery.removeListener(listener);\n }\n };\n }, [query]);\n\n return matches;\n}\n\n/**\n * Predefined breakpoints matching Tailwind CSS defaults\n */\nexport const breakpoints = {\n sm: \"(min-width: 640px)\",\n md: \"(min-width: 768px)\",\n lg: \"(min-width: 1024px)\",\n xl: \"(min-width: 1280px)\",\n \"2xl\": \"(min-width: 1536px)\",\n} as const;\n\n/**\n * Hook to check if viewport is mobile (below md breakpoint)\n */\nexport function useIsMobile(): boolean {\n return !useMediaQuery(breakpoints.md);\n}\n\n/**\n * Hook to check if viewport is desktop (md breakpoint and above)\n */\nexport function useIsDesktop(): boolean {\n return useMediaQuery(breakpoints.md);\n}\n","\"use client\";\n\nimport { Dialog as DialogPrimitive } from \"@base-ui/react/dialog\";\nimport { XIcon } from \"@phosphor-icons/react\";\nimport type * as React from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport { Button } from \"./button\";\n\nfunction Dialog({ ...props }: DialogPrimitive.Root.Props) {\n return <DialogPrimitive.Root data-slot=\"dialog\" {...props} />;\n}\n\nfunction DialogTrigger({ ...props }: DialogPrimitive.Trigger.Props) {\n return <DialogPrimitive.Trigger data-slot=\"dialog-trigger\" {...props} />;\n}\n\nfunction DialogPortal({ ...props }: DialogPrimitive.Portal.Props) {\n return <DialogPrimitive.Portal data-slot=\"dialog-portal\" {...props} />;\n}\n\nfunction DialogClose({ ...props }: DialogPrimitive.Close.Props) {\n return <DialogPrimitive.Close data-slot=\"dialog-close\" {...props} />;\n}\n\nfunction DialogOverlay({\n className,\n ...props\n}: DialogPrimitive.Backdrop.Props) {\n return (\n <DialogPrimitive.Backdrop\n data-slot=\"dialog-overlay\"\n className={cn(\n \"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/80 duration-100 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 isolate z-50\",\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction DialogContent({\n className,\n children,\n showCloseButton = true,\n ...props\n}: DialogPrimitive.Popup.Props & {\n showCloseButton?: boolean;\n}) {\n return (\n <DialogPortal>\n <DialogOverlay />\n <DialogPrimitive.Popup\n data-slot=\"dialog-content\"\n className={cn(\n \"bg-background data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 ring-foreground/10 grid max-w-[calc(100%-2rem)] gap-4 p-4 text-sm ring-1 duration-100 sm:max-w-sm fixed top-1/2 left-1/2 z-50 w-full -translate-x-1/2 -translate-y-1/2 outline-none\",\n className,\n )}\n {...props}\n >\n {children}\n {showCloseButton && (\n <DialogPrimitive.Close\n data-slot=\"dialog-close\"\n render={\n <Button\n variant=\"ghost\"\n className=\"absolute top-2 right-2\"\n size=\"icon-sm\"\n />\n }\n >\n <XIcon />\n <span className=\"sr-only\">Close</span>\n </DialogPrimitive.Close>\n )}\n </DialogPrimitive.Popup>\n </DialogPortal>\n );\n}\n\nfunction DialogHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"dialog-header\"\n className={cn(\"gap-1 flex flex-col\", className)}\n {...props}\n />\n );\n}\n\nfunction DialogFooter({\n className,\n showCloseButton = false,\n children,\n ...props\n}: React.ComponentProps<\"div\"> & {\n showCloseButton?: boolean;\n}) {\n return (\n <div\n data-slot=\"dialog-footer\"\n className={cn(\n \"gap-2 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end\",\n className,\n )}\n {...props}\n >\n {children}\n {showCloseButton && (\n <DialogPrimitive.Close render={<Button variant=\"outline\" />}>\n Close\n </DialogPrimitive.Close>\n )}\n </div>\n );\n}\n\nfunction DialogTitle({ className, ...props }: DialogPrimitive.Title.Props) {\n return (\n <DialogPrimitive.Title\n data-slot=\"dialog-title\"\n className={cn(\"text-sm font-medium\", className)}\n {...props}\n />\n );\n}\n\nfunction DialogDescription({\n className,\n ...props\n}: DialogPrimitive.Description.Props) {\n return (\n <DialogPrimitive.Description\n data-slot=\"dialog-description\"\n className={cn(\n \"text-muted-foreground *:[a]:hover:text-foreground text-xs/relaxed *:[a]:underline *:[a]:underline-offset-3\",\n className,\n )}\n {...props}\n />\n );\n}\n\nexport {\n Dialog,\n DialogClose,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogOverlay,\n DialogPortal,\n DialogTitle,\n DialogTrigger,\n};\n","\"use client\";\n\nimport type * as React from \"react\";\nimport { Drawer as DrawerPrimitive } from \"vaul\";\n\nimport { cn } from \"#questpie/admin/client/lib/utils\";\n\nfunction Drawer({\n\t...props\n}: React.ComponentProps<typeof DrawerPrimitive.Root>) {\n\treturn <DrawerPrimitive.Root data-slot=\"drawer\" {...props} />;\n}\n\nfunction DrawerTrigger({\n\t...props\n}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {\n\treturn <DrawerPrimitive.Trigger data-slot=\"drawer-trigger\" {...props} />;\n}\n\nfunction DrawerPortal({\n\t...props\n}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {\n\treturn <DrawerPrimitive.Portal data-slot=\"drawer-portal\" {...props} />;\n}\n\nfunction DrawerClose({\n\t...props\n}: React.ComponentProps<typeof DrawerPrimitive.Close>) {\n\treturn <DrawerPrimitive.Close data-slot=\"drawer-close\" {...props} />;\n}\n\nfunction DrawerOverlay({\n\tclassName,\n\t...props\n}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {\n\treturn (\n\t\t<DrawerPrimitive.Overlay\n\t\t\tdata-slot=\"drawer-overlay\"\n\t\t\tclassName={cn(\n\t\t\t\t\"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/80 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 z-50\",\n\t\t\t\tclassName,\n\t\t\t)}\n\t\t\t{...props}\n\t\t/>\n\t);\n}\n\nfunction DrawerContent({\n\tclassName,\n\tchildren,\n\t...props\n}: React.ComponentProps<typeof DrawerPrimitive.Content>) {\n\treturn (\n\t\t<DrawerPortal data-slot=\"drawer-portal\">\n\t\t\t<DrawerOverlay />\n\t\t\t<DrawerPrimitive.Content\n\t\t\t\tdata-slot=\"drawer-content\"\n\t\t\t\tclassName={cn(\n\t\t\t\t\t\"before:bg-background px-4 pb-4 relative flex h-auto flex-col bg-transparent text-xs/relaxed before:absolute before:inset-2 before:-z-10 before:rounded-xl data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=left]:sm:max-w-sm data-[vaul-drawer-direction=right]:sm:max-w-sm group/drawer-content fixed z-50\",\n\t\t\t\t\tclassName,\n\t\t\t\t)}\n\t\t\t\t{...props}\n\t\t\t>\n\t\t\t\t<div className=\"bg-muted mx-auto mt-4 hidden h-1.5 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block\" />\n\t\t\t\t{children}\n\t\t\t</DrawerPrimitive.Content>\n\t\t</DrawerPortal>\n\t);\n}\n\nfunction DrawerHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n\treturn (\n\t\t<div\n\t\t\tdata-slot=\"drawer-header\"\n\t\t\tclassName={cn(\n\t\t\t\t\"gap-1 pt-4 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:text-left flex flex-col\",\n\t\t\t\tclassName,\n\t\t\t)}\n\t\t\t{...props}\n\t\t/>\n\t);\n}\n\nfunction DrawerFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n\treturn (\n\t\t<div\n\t\t\tdata-slot=\"drawer-footer\"\n\t\t\tclassName={cn(\"gap-2 py-4 mt-auto flex flex-col\", className)}\n\t\t\t{...props}\n\t\t/>\n\t);\n}\n\nfunction DrawerTitle({\n\tclassName,\n\t...props\n}: React.ComponentProps<typeof DrawerPrimitive.Title>) {\n\treturn (\n\t\t<DrawerPrimitive.Title\n\t\t\tdata-slot=\"drawer-title\"\n\t\t\tclassName={cn(\"text-foreground text-sm font-medium\", className)}\n\t\t\t{...props}\n\t\t/>\n\t);\n}\n\nfunction DrawerDescription({\n\tclassName,\n\t...props\n}: React.ComponentProps<typeof DrawerPrimitive.Description>) {\n\treturn (\n\t\t<DrawerPrimitive.Description\n\t\t\tdata-slot=\"drawer-description\"\n\t\t\tclassName={cn(\"text-muted-foreground text-xs/relaxed\", className)}\n\t\t\t{...props}\n\t\t/>\n\t);\n}\n\nexport {\n\tDrawer,\n\tDrawerPortal,\n\tDrawerOverlay,\n\tDrawerTrigger,\n\tDrawerClose,\n\tDrawerContent,\n\tDrawerHeader,\n\tDrawerFooter,\n\tDrawerTitle,\n\tDrawerDescription,\n};\n","\"use client\";\n\nimport * as React from \"react\";\nimport { Dialog as DialogPrimitive } from \"@base-ui/react/dialog\";\nimport { cn } from \"#questpie/admin/client/lib/utils\";\nimport { useIsMobile } from \"#questpie/admin/client/hooks/use-media-query\";\nimport { DialogContent, DialogFooter, DialogHeader } from \"./dialog\";\nimport {\n\tDrawer,\n\tDrawerClose,\n\tDrawerContent,\n\tDrawerDescription,\n\tDrawerFooter,\n\tDrawerHeader,\n\tDrawerTitle,\n\tDrawerTrigger,\n} from \"./drawer\";\n\n/**\n * ResponsiveDialog - Uses Dialog on desktop, fullscreen Drawer on mobile\n *\n * @example\n * ```tsx\n * <ResponsiveDialog>\n * <ResponsiveDialogTrigger asChild>\n * <Button>Open</Button>\n * </ResponsiveDialogTrigger>\n * <ResponsiveDialogContent>\n * <ResponsiveDialogHeader>\n * <ResponsiveDialogTitle>Title</ResponsiveDialogTitle>\n * <ResponsiveDialogDescription>Description</ResponsiveDialogDescription>\n * </ResponsiveDialogHeader>\n * <div>Content here</div>\n * <ResponsiveDialogFooter>\n * <ResponsiveDialogClose asChild>\n * <Button variant=\"outline\">Cancel</Button>\n * </ResponsiveDialogClose>\n * <Button>Save</Button>\n * </ResponsiveDialogFooter>\n * </ResponsiveDialogContent>\n * </ResponsiveDialog>\n * ```\n */\n\ninterface ResponsiveDialogContextValue {\n\tisMobile: boolean;\n}\n\nconst ResponsiveDialogContext =\n\tReact.createContext<ResponsiveDialogContextValue | null>(null);\n\nfunction useResponsiveDialog() {\n\tconst context = React.useContext(ResponsiveDialogContext);\n\tif (!context) {\n\t\tthrow new Error(\n\t\t\t\"ResponsiveDialog components must be used within ResponsiveDialog\",\n\t\t);\n\t}\n\treturn context;\n}\n\nexport interface ResponsiveDialogProps {\n\tchildren: React.ReactNode;\n\topen?: boolean;\n\tonOpenChange?: (open: boolean) => void;\n}\n\nfunction ResponsiveDialog({\n\tchildren,\n\topen,\n\tonOpenChange,\n}: ResponsiveDialogProps) {\n\tconst isMobile = useIsMobile();\n\n\tconst contextValue = React.useMemo(() => ({ isMobile }), [isMobile]);\n\n\tif (isMobile) {\n\t\treturn (\n\t\t\t<ResponsiveDialogContext.Provider value={contextValue}>\n\t\t\t\t<Drawer open={open} onOpenChange={onOpenChange}>\n\t\t\t\t\t{children}\n\t\t\t\t</Drawer>\n\t\t\t</ResponsiveDialogContext.Provider>\n\t\t);\n\t}\n\n\treturn (\n\t\t<ResponsiveDialogContext.Provider value={contextValue}>\n\t\t\t<DialogPrimitive.Root open={open} onOpenChange={onOpenChange}>\n\t\t\t\t{children}\n\t\t\t</DialogPrimitive.Root>\n\t\t</ResponsiveDialogContext.Provider>\n\t);\n}\n\nfunction ResponsiveDialogTrigger({\n\tchildren,\n\tasChild = false,\n\tclassName,\n\t...props\n}: {\n\tchildren: React.ReactNode;\n\tasChild?: boolean;\n\tclassName?: string;\n} & Omit<React.ComponentProps<\"button\">, \"className\">) {\n\tconst { isMobile } = useResponsiveDialog();\n\n\tif (isMobile) {\n\t\treturn (\n\t\t\t<DrawerTrigger asChild={asChild} className={className} {...props}>\n\t\t\t\t{children}\n\t\t\t</DrawerTrigger>\n\t\t);\n\t}\n\n\treturn (\n\t\t<DialogPrimitive.Trigger\n\t\t\tclassName={className}\n\t\t\trender={asChild ? (children as React.ReactElement) : undefined}\n\t\t\t{...props}\n\t\t>\n\t\t\t{!asChild ? children : undefined}\n\t\t</DialogPrimitive.Trigger>\n\t);\n}\n\nexport interface ResponsiveDialogContentProps {\n\tchildren: React.ReactNode;\n\tclassName?: string;\n}\n\nfunction ResponsiveDialogContent({\n\tchildren,\n\tclassName,\n}: ResponsiveDialogContentProps) {\n\tconst { isMobile } = useResponsiveDialog();\n\n\tif (isMobile) {\n\t\treturn (\n\t\t\t<DrawerContent className={cn(\"max-h-[96vh]\", className)}>\n\t\t\t\t{children}\n\t\t\t</DrawerContent>\n\t\t);\n\t}\n\n\treturn <DialogContent className={className}>{children}</DialogContent>;\n}\n\nfunction ResponsiveDialogHeader({\n\tclassName,\n\t...props\n}: React.ComponentProps<\"div\">) {\n\tconst { isMobile } = useResponsiveDialog();\n\n\tif (isMobile) {\n\t\treturn <DrawerHeader className={className} {...props} />;\n\t}\n\n\treturn <DialogHeader className={className} {...props} />;\n}\n\nfunction ResponsiveDialogTitle({\n\tclassName,\n\tchildren,\n\t...props\n}: React.ComponentProps<\"h2\">) {\n\tconst { isMobile } = useResponsiveDialog();\n\n\tif (isMobile) {\n\t\treturn (\n\t\t\t<DrawerTitle className={className} {...props}>\n\t\t\t\t{children}\n\t\t\t</DrawerTitle>\n\t\t);\n\t}\n\n\treturn (\n\t\t<DialogPrimitive.Title\n\t\t\tdata-slot=\"responsive-dialog-title\"\n\t\t\tclassName={cn(\"text-sm font-medium\", className)}\n\t\t\t{...props}\n\t\t>\n\t\t\t{children}\n\t\t</DialogPrimitive.Title>\n\t);\n}\n\nfunction ResponsiveDialogDescription({\n\tclassName,\n\tchildren,\n\t...props\n}: React.ComponentProps<\"p\">) {\n\tconst { isMobile } = useResponsiveDialog();\n\n\tif (isMobile) {\n\t\treturn (\n\t\t\t<DrawerDescription className={className} {...props}>\n\t\t\t\t{children}\n\t\t\t</DrawerDescription>\n\t\t);\n\t}\n\n\treturn (\n\t\t<DialogPrimitive.Description\n\t\t\tdata-slot=\"responsive-dialog-description\"\n\t\t\tclassName={cn(\"text-muted-foreground text-xs/relaxed\", className)}\n\t\t\t{...props}\n\t\t>\n\t\t\t{children}\n\t\t</DialogPrimitive.Description>\n\t);\n}\n\nfunction ResponsiveDialogFooter({\n\tclassName,\n\t...props\n}: React.ComponentProps<\"div\">) {\n\tconst { isMobile } = useResponsiveDialog();\n\n\tif (isMobile) {\n\t\treturn <DrawerFooter className={className} {...props} />;\n\t}\n\n\treturn <DialogFooter className={className} {...props} />;\n}\n\nfunction ResponsiveDialogClose({\n\tchildren,\n\tasChild = false,\n\tclassName,\n\t...props\n}: {\n\tchildren: React.ReactNode;\n\tasChild?: boolean;\n\tclassName?: string;\n} & Omit<React.ComponentProps<\"button\">, \"className\">) {\n\tconst { isMobile } = useResponsiveDialog();\n\n\tif (isMobile) {\n\t\treturn (\n\t\t\t<DrawerClose asChild={asChild} className={className} {...props}>\n\t\t\t\t{children}\n\t\t\t</DrawerClose>\n\t\t);\n\t}\n\n\treturn (\n\t\t<DialogPrimitive.Close\n\t\t\tclassName={className}\n\t\t\trender={asChild ? (children as React.ReactElement) : undefined}\n\t\t\t{...props}\n\t\t>\n\t\t\t{!asChild ? children : undefined}\n\t\t</DialogPrimitive.Close>\n\t);\n}\n\nexport {\n\tResponsiveDialog,\n\tResponsiveDialogTrigger,\n\tResponsiveDialogContent,\n\tResponsiveDialogHeader,\n\tResponsiveDialogTitle,\n\tResponsiveDialogDescription,\n\tResponsiveDialogFooter,\n\tResponsiveDialogClose,\n\tuseResponsiveDialog,\n};\n","/**\n * useUpload Hook\n *\n * Handles file uploads to the CMS with progress tracking.\n * Uses the QuestpieClient's upload method which uses XMLHttpRequest for progress.\n *\n * @example\n * ```tsx\n * const { upload, uploadMany, isUploading, progress } = useUpload();\n *\n * // Single file upload\n * const asset = await upload(file);\n *\n * // Multiple files upload\n * const assets = await uploadMany(files, {\n * onProgress: (p) => console.log(`${p}%`),\n * });\n * ```\n */\n\nimport { useCallback, useState } from \"react\";\nimport { useQueryClient } from \"@tanstack/react-query\";\nimport { selectClient, useAdminStore } from \"../runtime\";\n\n/**\n * Upload error with additional context\n */\nexport class UploadError extends Error {\n constructor(\n message: string,\n public readonly status?: number,\n public readonly response?: unknown,\n ) {\n super(message);\n this.name = \"UploadError\";\n }\n}\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * Asset record returned from upload\n */\nexport interface Asset {\n id: string;\n key: string;\n filename: string;\n mimeType: string;\n size: number;\n visibility: \"public\" | \"private\";\n url?: string;\n width?: number | null;\n height?: number | null;\n alt?: string | null;\n caption?: string | null;\n createdAt?: string;\n updatedAt?: string;\n}\n\n/**\n * Options for upload operations\n */\nexport interface UploadOptions {\n /**\n * Target collection for upload (must have .upload() enabled)\n * @default \"assets\"\n */\n collection?: string;\n\n /**\n * Progress callback (0-100)\n */\n onProgress?: (progress: number) => void;\n\n /**\n * Abort signal for cancellation\n */\n signal?: AbortSignal;\n}\n\n/**\n * Options for uploadMany operation\n */\nexport interface UploadManyOptions extends UploadOptions {\n /**\n * Progress callback receives overall progress (0-100)\n * and optionally individual file progress\n */\n onProgress?: (progress: number, fileIndex?: number) => void;\n}\n\n/**\n * Return type for useUpload hook\n */\nexport interface UseUploadReturn {\n /**\n * Upload a single file\n */\n upload: (file: File, options?: UploadOptions) => Promise<Asset>;\n\n /**\n * Upload multiple files sequentially\n */\n uploadMany: (files: File[], options?: UploadManyOptions) => Promise<Asset[]>;\n\n /**\n * Whether an upload is currently in progress\n */\n isUploading: boolean;\n\n /**\n * Current upload progress (0-100)\n */\n progress: number;\n\n /**\n * Current error, if any\n */\n error: Error | null;\n\n /**\n * Reset state (clear error, progress)\n */\n reset: () => void;\n}\n\n// ============================================================================\n// Hook Implementation\n// ============================================================================\n\n/**\n * Hook for uploading files to the CMS\n *\n * Uses the QuestpieClient's built-in upload method which provides\n * progress tracking via XMLHttpRequest.\n */\nexport function useUpload(): UseUploadReturn {\n const client = useAdminStore(selectClient);\n const queryClient = useQueryClient();\n\n const [isUploading, setIsUploading] = useState(false);\n const [progress, setProgress] = useState(0);\n const [error, setError] = useState<Error | null>(null);\n\n /**\n * Upload a single file\n */\n const upload = useCallback(\n async (file: File, options: UploadOptions = {}): Promise<Asset> => {\n const { collection = \"assets\", onProgress, signal } = options;\n\n setIsUploading(true);\n setProgress(0);\n setError(null);\n\n try {\n // Get the collection API from client\n const collectionApi = (client.collections as any)[collection];\n\n if (!collectionApi?.upload) {\n throw new Error(\n `Collection \"${collection}\" does not support uploads. Make sure .upload() is enabled on the collection.`,\n );\n }\n\n const result = await collectionApi.upload(file, {\n signal,\n onProgress: (p: number) => {\n setProgress(p);\n onProgress?.(p);\n },\n });\n\n // Invalidate collection queries to refresh lists\n queryClient.invalidateQueries({\n queryKey: [\"questpie\", \"collections\", collection],\n });\n\n return result as Asset;\n } catch (err) {\n const uploadError =\n err instanceof Error ? err : new Error(\"Upload failed\");\n setError(uploadError);\n throw uploadError;\n } finally {\n setIsUploading(false);\n }\n },\n [client, queryClient],\n );\n\n /**\n * Upload multiple files sequentially\n */\n const uploadMany = useCallback(\n async (\n files: File[],\n options: UploadManyOptions = {},\n ): Promise<Asset[]> => {\n const { collection = \"assets\", onProgress, signal } = options;\n\n if (files.length === 0) {\n return [];\n }\n\n setIsUploading(true);\n setProgress(0);\n setError(null);\n\n try {\n // Get the collection API from client\n const collectionApi = (client.collections as any)[collection];\n\n if (!collectionApi?.uploadMany) {\n throw new Error(\n `Collection \"${collection}\" does not support uploads. Make sure .upload() is enabled on the collection.`,\n );\n }\n\n const results = await collectionApi.uploadMany(files, {\n signal,\n onProgress: (p: number, fileIndex?: number) => {\n setProgress(p);\n onProgress?.(p, fileIndex);\n },\n });\n\n // Invalidate collection queries\n queryClient.invalidateQueries({\n queryKey: [\"questpie\", \"collections\", collection],\n });\n\n setProgress(100);\n return results as Asset[];\n } catch (err) {\n const uploadError =\n err instanceof Error ? err : new Error(\"Upload failed\");\n setError(uploadError);\n throw uploadError;\n } finally {\n setIsUploading(false);\n }\n },\n [client, queryClient],\n );\n\n /**\n * Reset hook state\n */\n const reset = useCallback(() => {\n setIsUploading(false);\n setProgress(0);\n setError(null);\n }, []);\n\n return {\n upload,\n uploadMany,\n isUploading,\n progress,\n error,\n reset,\n };\n}\n","/**\n * Dropzone Primitive\n *\n * A reusable drag-and-drop area for file uploads.\n * Supports file type filtering, size validation, and visual feedback.\n *\n * @example\n * ```tsx\n * <Dropzone\n * onDrop={(files) => handleUpload(files)}\n * accept={[\"image/*\", \"application/pdf\"]}\n * maxSize={5_000_000}\n * />\n * ```\n */\n\nimport * as React from \"react\";\nimport { CloudArrowUp, SpinnerGap } from \"@phosphor-icons/react\";\nimport { cn } from \"../../lib/utils\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface DropzoneProps {\n /**\n * Called when files are dropped or selected\n */\n onDrop: (files: File[]) => void;\n\n /**\n * Accepted file types (MIME types or extensions)\n * @example [\"image/*\", \"application/pdf\", \".doc\"]\n */\n accept?: string[];\n\n /**\n * Maximum file size in bytes\n */\n maxSize?: number;\n\n /**\n * Whether multiple files can be selected\n * @default false\n */\n multiple?: boolean;\n\n /**\n * Disable the dropzone\n */\n disabled?: boolean;\n\n /**\n * Show loading state (e.g., during upload)\n */\n loading?: boolean;\n\n /**\n * Current upload progress (0-100)\n */\n progress?: number;\n\n /**\n * Custom label text\n * @default \"Drop files here or click to browse\"\n */\n label?: string;\n\n /**\n * Helper text shown below the label\n */\n hint?: string;\n\n /**\n * Error message to display\n */\n error?: string;\n\n /**\n * Additional className\n */\n className?: string;\n\n /**\n * Children to render inside the dropzone (custom content)\n */\n children?: React.ReactNode;\n\n /**\n * Callback when validation fails\n */\n onValidationError?: (errors: ValidationError[]) => void;\n}\n\nexport interface ValidationError {\n file: File;\n type: \"type\" | \"size\";\n message: string;\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\n/**\n * Check if a file matches accepted types\n */\nfunction matchesAccept(file: File, accept?: string[]): boolean {\n if (!accept || accept.length === 0) return true;\n\n const mimeType = file.type.toLowerCase();\n const fileName = file.name.toLowerCase();\n\n return accept.some((pattern) => {\n const normalizedPattern = pattern.toLowerCase();\n\n // Handle wildcard MIME types like \"image/*\"\n if (normalizedPattern.endsWith(\"/*\")) {\n const baseType = normalizedPattern.slice(0, -2);\n return mimeType.startsWith(`${baseType}/`);\n }\n\n // Handle extensions like \".pdf\"\n if (normalizedPattern.startsWith(\".\")) {\n return fileName.endsWith(normalizedPattern);\n }\n\n // Handle exact MIME types\n return mimeType === normalizedPattern;\n });\n}\n\n/**\n * Format file size for display\n */\nfunction formatFileSize(bytes: number): string {\n if (bytes < 1024) return `${bytes} B`;\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;\n return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport function Dropzone({\n onDrop,\n accept,\n maxSize,\n multiple = false,\n disabled = false,\n loading = false,\n progress,\n label = \"Drop files here or click to browse\",\n hint,\n error,\n className,\n children,\n onValidationError,\n}: DropzoneProps) {\n const [isDragging, setIsDragging] = React.useState(false);\n const inputRef = React.useRef<HTMLInputElement>(null);\n const dragCounterRef = React.useRef(0);\n\n /**\n * Validate files and return valid ones + errors\n */\n const validateFiles = React.useCallback(\n (files: File[]): { valid: File[]; errors: ValidationError[] } => {\n const valid: File[] = [];\n const errors: ValidationError[] = [];\n\n for (const file of files) {\n // Check type\n if (!matchesAccept(file, accept)) {\n errors.push({\n file,\n type: \"type\",\n message: `\"${file.name}\" is not an accepted file type`,\n });\n continue;\n }\n\n // Check size\n if (maxSize && file.size > maxSize) {\n errors.push({\n file,\n type: \"size\",\n message: `\"${file.name}\" exceeds maximum size of ${formatFileSize(maxSize)}`,\n });\n continue;\n }\n\n valid.push(file);\n }\n\n return { valid, errors };\n },\n [accept, maxSize],\n );\n\n /**\n * Handle file selection\n */\n const handleFiles = React.useCallback(\n (files: FileList | File[]) => {\n if (disabled || loading) return;\n\n const fileArray = Array.from(files);\n const filesToProcess = multiple ? fileArray : fileArray.slice(0, 1);\n\n const { valid, errors } = validateFiles(filesToProcess);\n\n if (errors.length > 0) {\n onValidationError?.(errors);\n }\n\n if (valid.length > 0) {\n onDrop(valid);\n }\n },\n [disabled, loading, multiple, validateFiles, onDrop, onValidationError],\n );\n\n /**\n * Handle drag events\n */\n const handleDragEnter = React.useCallback(\n (e: React.DragEvent) => {\n e.preventDefault();\n e.stopPropagation();\n dragCounterRef.current += 1;\n if (e.dataTransfer.items.length > 0 && !disabled && !loading) {\n setIsDragging(true);\n }\n },\n [disabled, loading],\n );\n\n const handleDragLeave = React.useCallback((e: React.DragEvent) => {\n e.preventDefault();\n e.stopPropagation();\n dragCounterRef.current -= 1;\n if (dragCounterRef.current === 0) {\n setIsDragging(false);\n }\n }, []);\n\n const handleDragOver = React.useCallback((e: React.DragEvent) => {\n e.preventDefault();\n e.stopPropagation();\n }, []);\n\n const handleDrop = React.useCallback(\n (e: React.DragEvent) => {\n e.preventDefault();\n e.stopPropagation();\n dragCounterRef.current = 0;\n setIsDragging(false);\n\n if (e.dataTransfer.files.length > 0) {\n handleFiles(e.dataTransfer.files);\n }\n },\n [handleFiles],\n );\n\n /**\n * Handle click to open file dialog\n */\n const handleClick = React.useCallback(() => {\n if (disabled || loading) return;\n inputRef.current?.click();\n }, [disabled, loading]);\n\n /**\n * Handle file input change\n */\n const handleInputChange = React.useCallback(\n (e: React.ChangeEvent<HTMLInputElement>) => {\n if (e.target.files && e.target.files.length > 0) {\n handleFiles(e.target.files);\n }\n // Reset input so same file can be selected again\n e.target.value = \"\";\n },\n [handleFiles],\n );\n\n /**\n * Build accept string for input\n */\n const acceptString = accept?.join(\",\") || undefined;\n\n /**\n * Build hint text\n */\n const hintText = React.useMemo(() => {\n if (hint) return hint;\n\n const parts: string[] = [];\n\n if (accept && accept.length > 0) {\n // Simplify accept types for display\n const types = accept\n .map((t) => {\n if (t.startsWith(\"image/\")) return \"Images\";\n if (t.startsWith(\"video/\")) return \"Videos\";\n if (t.startsWith(\"audio/\")) return \"Audio\";\n if (t === \"application/pdf\") return \"PDF\";\n if (t.startsWith(\".\")) return t.toUpperCase();\n return t;\n })\n .filter((v, i, a) => a.indexOf(v) === i); // unique\n parts.push(types.join(\", \"));\n }\n\n if (maxSize) {\n parts.push(`Max ${formatFileSize(maxSize)}`);\n }\n\n return parts.length > 0 ? parts.join(\" • \") : undefined;\n }, [hint, accept, maxSize]);\n\n return (\n <div\n role=\"button\"\n tabIndex={disabled || loading ? -1 : 0}\n onClick={handleClick}\n onKeyDown={(e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n handleClick();\n }\n }}\n onDragEnter={handleDragEnter}\n onDragLeave={handleDragLeave}\n onDragOver={handleDragOver}\n onDrop={handleDrop}\n className={cn(\n \"relative flex min-h-[120px] cursor-pointer flex-col items-center justify-center gap-2 rounded-lg border-2 border-dashed p-6 text-center transition-colors\",\n \"border-border/60 bg-muted/30 hover:border-border hover:bg-muted/50\",\n isDragging && \"border-primary bg-primary/5\",\n error && \"border-destructive/50 bg-destructive/5\",\n (disabled || loading) && \"pointer-events-none opacity-60\",\n className,\n )}\n aria-disabled={disabled || loading}\n data-dragging={isDragging || undefined}\n >\n {/* Hidden file input */}\n <input\n ref={inputRef}\n type=\"file\"\n accept={acceptString}\n multiple={multiple}\n onChange={handleInputChange}\n className=\"sr-only\"\n disabled={disabled || loading}\n tabIndex={-1}\n />\n\n {/* Custom children or default content */}\n {children || (\n <>\n {/* Icon / Loading */}\n <div className=\"flex items-center justify-center\">\n {loading ? (\n <div className=\"relative\">\n <SpinnerGap\n className=\"text-muted-foreground size-10 animate-spin\"\n weight=\"regular\"\n />\n {typeof progress === \"number\" && (\n <span className=\"text-muted-foreground absolute inset-0 flex items-center justify-center text-xs font-medium\">\n {progress}%\n </span>\n )}\n </div>\n ) : (\n <CloudArrowUp\n className={cn(\n \"size-10 transition-colors\",\n isDragging ? \"text-primary\" : \"text-muted-foreground\",\n )}\n weight=\"regular\"\n />\n )}\n </div>\n\n {/* Label */}\n <div className=\"space-y-1\">\n <p\n className={cn(\n \"text-sm font-medium\",\n isDragging ? \"text-primary\" : \"text-foreground\",\n )}\n >\n {loading ? \"Uploading...\" : label}\n </p>\n\n {/* Hint */}\n {hintText && !loading && (\n <p className=\"text-muted-foreground text-xs\">{hintText}</p>\n )}\n\n {/* Progress bar */}\n {loading && typeof progress === \"number\" && (\n <div className=\"bg-muted mx-auto mt-2 h-1.5 w-32 overflow-hidden rounded-full\">\n <div\n className=\"bg-primary h-full rounded-full transition-all duration-300\"\n style={{ width: `${progress}%` }}\n />\n </div>\n )}\n </div>\n </>\n )}\n\n {/* Error message */}\n {error && (\n <p className=\"text-destructive absolute bottom-2 left-0 right-0 text-center text-xs\">\n {error}\n </p>\n )}\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAcA,SAAgB,cAAc,OAAwB;CACpD,MAAM,CAAC,SAAS,cAAcA,QAAM,eAAwB;AAE1D,MAAI,OAAO,WAAW,YACpB,QAAO;AAET,SAAO,OAAO,WAAW,MAAM,CAAC;GAChC;AAEF,SAAM,gBAAgB;AACpB,MAAI,OAAO,WAAW,YACpB;EAGF,MAAM,aAAa,OAAO,WAAW,MAAM;AAG3C,aAAW,WAAW,QAAQ;EAG9B,MAAM,YAAY,UAA+B;AAC/C,cAAW,MAAM,QAAQ;;AAI3B,MAAI,WAAW,iBACb,YAAW,iBAAiB,UAAU,SAAS;MAG/C,YAAW,YAAY,SAAS;AAIlC,eAAa;AACX,OAAI,WAAW,oBACb,YAAW,oBAAoB,UAAU,SAAS;OAElD,YAAW,eAAe,SAAS;;IAGtC,CAAC,MAAM,CAAC;AAEX,QAAO;;;;;AAMT,MAAa,cAAc;CACzB,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,OAAO;CACR;;;;AAKD,SAAgB,cAAuB;AACrC,QAAO,CAAC,cAAc,YAAY,GAAG;;;;;AAMvC,SAAgB,eAAwB;AACtC,QAAO,cAAc,YAAY,GAAG;;;;;ACzEtC,SAASC,SAAO,EAAE,GAAG,SAAqC;AACxD,QAAO,oBAACC,OAAgB;EAAK,aAAU;EAAS,GAAI;GAAS;;AAO/D,SAAS,aAAa,EAAE,GAAG,SAAuC;AAChE,QAAO,oBAACA,OAAgB;EAAO,aAAU;EAAgB,GAAI;GAAS;;AAOxE,SAAS,cAAc,EACrB,WACA,GAAG,SAC8B;AACjC,QACE,oBAACA,OAAgB;EACf,aAAU;EACV,WAAW,GACT,yLACA,UACD;EACD,GAAI;GACJ;;AAIN,SAAS,cAAc,EACrB,WACA,UACA,kBAAkB,MAClB,GAAG,SAGF;AACD,QACE,qBAAC,2BACC,oBAAC,kBAAgB,EACjB,qBAACA,OAAgB;EACf,aAAU;EACV,WAAW,GACT,0UACA,UACD;EACD,GAAI;aAEH,UACA,mBACC,qBAACA,OAAgB;GACf,aAAU;GACV,QACE,oBAAC;IACC,SAAQ;IACR,WAAU;IACV,MAAK;KACL;cAGJ,oBAAC,UAAQ,EACT,oBAAC;IAAK,WAAU;cAAU;KAAY;IAChB;GAEJ,IACX;;AAInB,SAAS,aAAa,EAAE,WAAW,GAAG,SAAsC;AAC1E,QACE,oBAAC;EACC,aAAU;EACV,WAAW,GAAG,uBAAuB,UAAU;EAC/C,GAAI;GACJ;;AAIN,SAAS,aAAa,EACpB,WACA,kBAAkB,OAClB,UACA,GAAG,SAGF;AACD,QACE,qBAAC;EACC,aAAU;EACV,WAAW,GACT,gEACA,UACD;EACD,GAAI;aAEH,UACA,mBACC,oBAACA,OAAgB;GAAM,QAAQ,oBAAC,UAAO,SAAQ,YAAY;aAAE;IAErC;GAEtB;;AAIV,SAAS,YAAY,EAAE,WAAW,GAAG,SAAsC;AACzE,QACE,oBAACA,OAAgB;EACf,aAAU;EACV,WAAW,GAAG,uBAAuB,UAAU;EAC/C,GAAI;GACJ;;AAIN,SAAS,kBAAkB,EACzB,WACA,GAAG,SACiC;AACpC,QACE,oBAACA,OAAgB;EACf,aAAU;EACV,WAAW,GACT,8GACA,UACD;EACD,GAAI;GACJ;;;;;ACpIN,SAASC,SAAO,EACf,GAAG,SACkD;AACrD,QAAO,oBAACC,OAAgB;EAAK,aAAU;EAAS,GAAI;GAAS;;AAG9D,SAAS,cAAc,EACtB,GAAG,SACqD;AACxD,QAAO,oBAACA,OAAgB;EAAQ,aAAU;EAAiB,GAAI;GAAS;;AAGzE,SAAS,aAAa,EACrB,GAAG,SACoD;AACvD,QAAO,oBAACA,OAAgB;EAAO,aAAU;EAAgB,GAAI;GAAS;;AASvE,SAAS,cAAc,EACtB,WACA,GAAG,SACqD;AACxD,QACC,oBAACA,OAAgB;EAChB,aAAU;EACV,WAAW,GACV,oKACA,UACA;EACD,GAAI;GACH;;AAIJ,SAAS,cAAc,EACtB,WACA,UACA,GAAG,SACqD;AACxD,QACC,qBAAC;EAAa,aAAU;aACvB,oBAAC,kBAAgB,EACjB,qBAACA,OAAgB;GAChB,aAAU;GACV,WAAW,GACV,s3BACA,UACA;GACD,GAAI;cAEJ,oBAAC,SAAI,WAAU,sIAAsI,EACpJ;IACwB;GACZ;;AAIjB,SAAS,aAAa,EAAE,WAAW,GAAG,SAAsC;AAC3E,QACC,oBAAC;EACA,aAAU;EACV,WAAW,GACV,gLACA,UACA;EACD,GAAI;GACH;;AAIJ,SAAS,aAAa,EAAE,WAAW,GAAG,SAAsC;AAC3E,QACC,oBAAC;EACA,aAAU;EACV,WAAW,GAAG,oCAAoC,UAAU;EAC5D,GAAI;GACH;;AAIJ,SAAS,YAAY,EACpB,WACA,GAAG,SACmD;AACtD,QACC,oBAACA,OAAgB;EAChB,aAAU;EACV,WAAW,GAAG,uCAAuC,UAAU;EAC/D,GAAI;GACH;;AAIJ,SAAS,kBAAkB,EAC1B,WACA,GAAG,SACyD;AAC5D,QACC,oBAACA,OAAgB;EAChB,aAAU;EACV,WAAW,GAAG,yCAAyC,UAAU;EACjE,GAAI;GACH;;;;;ACnEJ,MAAM,0BACLC,QAAM,cAAmD,KAAK;AAE/D,SAAS,sBAAsB;CAC9B,MAAM,UAAUA,QAAM,WAAW,wBAAwB;AACzD,KAAI,CAAC,QACJ,OAAM,IAAI,MACT,mEACA;AAEF,QAAO;;AASR,SAAS,iBAAiB,EACzB,UACA,MACA,gBACyB;CACzB,MAAM,WAAW,aAAa;CAE9B,MAAM,eAAeA,QAAM,eAAe,EAAE,UAAU,GAAG,CAAC,SAAS,CAAC;AAEpE,KAAI,SACH,QACC,oBAAC,wBAAwB;EAAS,OAAO;YACxC,oBAACC;GAAa;GAAoB;GAChC;IACO;GACyB;AAIrC,QACC,oBAAC,wBAAwB;EAAS,OAAO;YACxC,oBAACC,OAAgB;GAAW;GAAoB;GAC9C;IACqB;GACW;;AAwCrC,SAAS,wBAAwB,EAChC,UACA,aACgC;CAChC,MAAM,EAAE,aAAa,qBAAqB;AAE1C,KAAI,SACH,QACC,oBAAC;EAAc,WAAW,GAAG,gBAAgB,UAAU;EACrD;GACc;AAIlB,QAAO,oBAAC;EAAyB;EAAY;GAAyB;;AAGvE,SAAS,uBAAuB,EAC/B,WACA,GAAG,SAC4B;CAC/B,MAAM,EAAE,aAAa,qBAAqB;AAE1C,KAAI,SACH,QAAO,oBAAC;EAAwB;EAAW,GAAI;GAAS;AAGzD,QAAO,oBAAC;EAAwB;EAAW,GAAI;GAAS;;AAGzD,SAAS,sBAAsB,EAC9B,WACA,UACA,GAAG,SAC2B;CAC9B,MAAM,EAAE,aAAa,qBAAqB;AAE1C,KAAI,SACH,QACC,oBAAC;EAAuB;EAAW,GAAI;EACrC;GACY;AAIhB,QACC,oBAACA,OAAgB;EAChB,aAAU;EACV,WAAW,GAAG,uBAAuB,UAAU;EAC/C,GAAI;EAEH;GACsB;;AAI1B,SAAS,4BAA4B,EACpC,WACA,UACA,GAAG,SAC0B;CAC7B,MAAM,EAAE,aAAa,qBAAqB;AAE1C,KAAI,SACH,QACC,oBAAC;EAA6B;EAAW,GAAI;EAC3C;GACkB;AAItB,QACC,oBAACA,OAAgB;EAChB,aAAU;EACV,WAAW,GAAG,yCAAyC,UAAU;EACjE,GAAI;EAEH;GAC4B;;AAIhC,SAAS,uBAAuB,EAC/B,WACA,GAAG,SAC4B;CAC/B,MAAM,EAAE,aAAa,qBAAqB;AAE1C,KAAI,SACH,QAAO,oBAAC;EAAwB;EAAW,GAAI;GAAS;AAGzD,QAAO,oBAAC;EAAwB;EAAW,GAAI;GAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACrFzD,SAAgB,YAA6B;CAC3C,MAAM,SAAS,cAAc,aAAa;CAC1C,MAAM,cAAc,gBAAgB;CAEpC,MAAM,CAAC,aAAa,kBAAkB,SAAS,MAAM;CACrD,MAAM,CAAC,UAAU,eAAe,SAAS,EAAE;CAC3C,MAAM,CAAC,OAAO,YAAY,SAAuB,KAAK;AAiHtD,QAAO;EACL,QA7Ga,YACb,OAAO,MAAY,UAAyB,EAAE,KAAqB;GACjE,MAAM,EAAE,aAAa,UAAU,YAAY,WAAW;AAEtD,kBAAe,KAAK;AACpB,eAAY,EAAE;AACd,YAAS,KAAK;AAEd,OAAI;IAEF,MAAM,gBAAiB,OAAO,YAAoB;AAElD,QAAI,CAAC,eAAe,OAClB,OAAM,IAAI,MACR,eAAe,WAAW,+EAC3B;IAGH,MAAM,SAAS,MAAM,cAAc,OAAO,MAAM;KAC9C;KACA,aAAa,MAAc;AACzB,kBAAY,EAAE;AACd,mBAAa,EAAE;;KAElB,CAAC;AAGF,gBAAY,kBAAkB,EAC5B,UAAU;KAAC;KAAY;KAAe;KAAW,EAClD,CAAC;AAEF,WAAO;YACA,KAAK;IACZ,MAAM,cACJ,eAAe,QAAQ,sBAAM,IAAI,MAAM,gBAAgB;AACzD,aAAS,YAAY;AACrB,UAAM;aACE;AACR,mBAAe,MAAM;;KAGzB,CAAC,QAAQ,YAAY,CACtB;EAoEC,YA/DiB,YACjB,OACE,OACA,UAA6B,EAAE,KACV;GACrB,MAAM,EAAE,aAAa,UAAU,YAAY,WAAW;AAEtD,OAAI,MAAM,WAAW,EACnB,QAAO,EAAE;AAGX,kBAAe,KAAK;AACpB,eAAY,EAAE;AACd,YAAS,KAAK;AAEd,OAAI;IAEF,MAAM,gBAAiB,OAAO,YAAoB;AAElD,QAAI,CAAC,eAAe,WAClB,OAAM,IAAI,MACR,eAAe,WAAW,+EAC3B;IAGH,MAAM,UAAU,MAAM,cAAc,WAAW,OAAO;KACpD;KACA,aAAa,GAAW,cAAuB;AAC7C,kBAAY,EAAE;AACd,mBAAa,GAAG,UAAU;;KAE7B,CAAC;AAGF,gBAAY,kBAAkB,EAC5B,UAAU;KAAC;KAAY;KAAe;KAAW,EAClD,CAAC;AAEF,gBAAY,IAAI;AAChB,WAAO;YACA,KAAK;IACZ,MAAM,cACJ,eAAe,QAAQ,sBAAM,IAAI,MAAM,gBAAgB;AACzD,aAAS,YAAY;AACrB,UAAM;aACE;AACR,mBAAe,MAAM;;KAGzB,CAAC,QAAQ,YAAY,CACtB;EAcC;EACA;EACA;EACA,OAZY,kBAAkB;AAC9B,kBAAe,MAAM;AACrB,eAAY,EAAE;AACd,YAAS,KAAK;KACb,EAAE,CAAC;EASL;;;;;;;;;;;;;;;;;;;;;;;AC7JH,SAAS,cAAc,MAAY,QAA4B;AAC7D,KAAI,CAAC,UAAU,OAAO,WAAW,EAAG,QAAO;CAE3C,MAAM,WAAW,KAAK,KAAK,aAAa;CACxC,MAAM,WAAW,KAAK,KAAK,aAAa;AAExC,QAAO,OAAO,MAAM,YAAY;EAC9B,MAAM,oBAAoB,QAAQ,aAAa;AAG/C,MAAI,kBAAkB,SAAS,KAAK,EAAE;GACpC,MAAM,WAAW,kBAAkB,MAAM,GAAG,GAAG;AAC/C,UAAO,SAAS,WAAW,GAAG,SAAS,GAAG;;AAI5C,MAAI,kBAAkB,WAAW,IAAI,CACnC,QAAO,SAAS,SAAS,kBAAkB;AAI7C,SAAO,aAAa;GACpB;;;;;AAMJ,SAAS,eAAe,OAAuB;AAC7C,KAAI,QAAQ,KAAM,QAAO,GAAG,MAAM;AAClC,KAAI,QAAQ,OAAO,KAAM,QAAO,IAAI,QAAQ,MAAM,QAAQ,EAAE,CAAC;AAC7D,QAAO,IAAI,SAAS,OAAO,OAAO,QAAQ,EAAE,CAAC;;AAO/C,SAAgB,SAAS,EACvB,QACA,QACA,SACA,WAAW,OACX,WAAW,OACX,UAAU,OACV,UACA,QAAQ,sCACR,MACA,OACA,WACA,UACA,qBACgB;CAChB,MAAM,CAAC,YAAY,iBAAiBC,QAAM,SAAS,MAAM;CACzD,MAAM,WAAWA,QAAM,OAAyB,KAAK;CACrD,MAAM,iBAAiBA,QAAM,OAAO,EAAE;;;;CAKtC,MAAM,gBAAgBA,QAAM,aACzB,UAAgE;EAC/D,MAAMC,QAAgB,EAAE;EACxB,MAAMC,SAA4B,EAAE;AAEpC,OAAK,MAAM,QAAQ,OAAO;AAExB,OAAI,CAAC,cAAc,MAAM,OAAO,EAAE;AAChC,WAAO,KAAK;KACV;KACA,MAAM;KACN,SAAS,IAAI,KAAK,KAAK;KACxB,CAAC;AACF;;AAIF,OAAI,WAAW,KAAK,OAAO,SAAS;AAClC,WAAO,KAAK;KACV;KACA,MAAM;KACN,SAAS,IAAI,KAAK,KAAK,4BAA4B,eAAe,QAAQ;KAC3E,CAAC;AACF;;AAGF,SAAM,KAAK,KAAK;;AAGlB,SAAO;GAAE;GAAO;GAAQ;IAE1B,CAAC,QAAQ,QAAQ,CAClB;;;;CAKD,MAAM,cAAcF,QAAM,aACvB,UAA6B;AAC5B,MAAI,YAAY,QAAS;EAEzB,MAAM,YAAY,MAAM,KAAK,MAAM;EAGnC,MAAM,EAAE,OAAO,WAAW,cAFH,WAAW,YAAY,UAAU,MAAM,GAAG,EAAE,CAEZ;AAEvD,MAAI,OAAO,SAAS,EAClB,qBAAoB,OAAO;AAG7B,MAAI,MAAM,SAAS,EACjB,QAAO,MAAM;IAGjB;EAAC;EAAU;EAAS;EAAU;EAAe;EAAQ;EAAkB,CACxE;;;;CAKD,MAAM,kBAAkBA,QAAM,aAC3B,MAAuB;AACtB,IAAE,gBAAgB;AAClB,IAAE,iBAAiB;AACnB,iBAAe,WAAW;AAC1B,MAAI,EAAE,aAAa,MAAM,SAAS,KAAK,CAAC,YAAY,CAAC,QACnD,eAAc,KAAK;IAGvB,CAAC,UAAU,QAAQ,CACpB;CAED,MAAM,kBAAkBA,QAAM,aAAa,MAAuB;AAChE,IAAE,gBAAgB;AAClB,IAAE,iBAAiB;AACnB,iBAAe,WAAW;AAC1B,MAAI,eAAe,YAAY,EAC7B,eAAc,MAAM;IAErB,EAAE,CAAC;CAEN,MAAM,iBAAiBA,QAAM,aAAa,MAAuB;AAC/D,IAAE,gBAAgB;AAClB,IAAE,iBAAiB;IAClB,EAAE,CAAC;CAEN,MAAM,aAAaA,QAAM,aACtB,MAAuB;AACtB,IAAE,gBAAgB;AAClB,IAAE,iBAAiB;AACnB,iBAAe,UAAU;AACzB,gBAAc,MAAM;AAEpB,MAAI,EAAE,aAAa,MAAM,SAAS,EAChC,aAAY,EAAE,aAAa,MAAM;IAGrC,CAAC,YAAY,CACd;;;;CAKD,MAAM,cAAcA,QAAM,kBAAkB;AAC1C,MAAI,YAAY,QAAS;AACzB,WAAS,SAAS,OAAO;IACxB,CAAC,UAAU,QAAQ,CAAC;;;;CAKvB,MAAM,oBAAoBA,QAAM,aAC7B,MAA2C;AAC1C,MAAI,EAAE,OAAO,SAAS,EAAE,OAAO,MAAM,SAAS,EAC5C,aAAY,EAAE,OAAO,MAAM;AAG7B,IAAE,OAAO,QAAQ;IAEnB,CAAC,YAAY,CACd;;;;CAKD,MAAM,eAAe,QAAQ,KAAK,IAAI,IAAI;;;;CAK1C,MAAM,WAAWA,QAAM,cAAc;AACnC,MAAI,KAAM,QAAO;EAEjB,MAAMG,QAAkB,EAAE;AAE1B,MAAI,UAAU,OAAO,SAAS,GAAG;GAE/B,MAAM,QAAQ,OACX,KAAK,MAAM;AACV,QAAI,EAAE,WAAW,SAAS,CAAE,QAAO;AACnC,QAAI,EAAE,WAAW,SAAS,CAAE,QAAO;AACnC,QAAI,EAAE,WAAW,SAAS,CAAE,QAAO;AACnC,QAAI,MAAM,kBAAmB,QAAO;AACpC,QAAI,EAAE,WAAW,IAAI,CAAE,QAAO,EAAE,aAAa;AAC7C,WAAO;KACP,CACD,QAAQ,GAAG,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE;AAC1C,SAAM,KAAK,MAAM,KAAK,KAAK,CAAC;;AAG9B,MAAI,QACF,OAAM,KAAK,OAAO,eAAe,QAAQ,GAAG;AAG9C,SAAO,MAAM,SAAS,IAAI,MAAM,KAAK,MAAM,GAAG;IAC7C;EAAC;EAAM;EAAQ;EAAQ,CAAC;AAE3B,QACE,qBAAC;EACC,MAAK;EACL,UAAU,YAAY,UAAU,KAAK;EACrC,SAAS;EACT,YAAY,MAAM;AAChB,OAAI,EAAE,QAAQ,WAAW,EAAE,QAAQ,KAAK;AACtC,MAAE,gBAAgB;AAClB,iBAAa;;;EAGjB,aAAa;EACb,aAAa;EACb,YAAY;EACZ,QAAQ;EACR,WAAW,GACT,6JACA,sEACA,cAAc,+BACd,SAAS,2CACR,YAAY,YAAY,kCACzB,UACD;EACD,iBAAe,YAAY;EAC3B,iBAAe,cAAc;;GAG7B,oBAAC;IACC,KAAK;IACL,MAAK;IACL,QAAQ;IACE;IACV,UAAU;IACV,WAAU;IACV,UAAU,YAAY;IACtB,UAAU;KACV;GAGD,YACC,4CAEE,oBAAC;IAAI,WAAU;cACZ,UACC,qBAAC;KAAI,WAAU;gBACb,oBAAC;MACC,WAAU;MACV,QAAO;OACP,EACD,OAAO,aAAa,YACnB,qBAAC;MAAK,WAAU;iBACb,UAAS;OACL;MAEL,GAEN,oBAAC;KACC,WAAW,GACT,6BACA,aAAa,iBAAiB,wBAC/B;KACD,QAAO;MACP;KAEA,EAGN,qBAAC;IAAI,WAAU;;KACb,oBAAC;MACC,WAAW,GACT,uBACA,aAAa,iBAAiB,kBAC/B;gBAEA,UAAU,iBAAiB;OAC1B;KAGH,YAAY,CAAC,WACZ,oBAAC;MAAE,WAAU;gBAAiC;OAAa;KAI5D,WAAW,OAAO,aAAa,YAC9B,oBAAC;MAAI,WAAU;gBACb,oBAAC;OACC,WAAU;OACV,OAAO,EAAE,OAAO,GAAG,SAAS,IAAI;QAChC;OACE;;KAEJ,IACL;GAIJ,SACC,oBAAC;IAAE,WAAU;cACV;KACC;;GAEF"}
|
|
@@ -1,13 +1,12 @@
|
|
|
1
|
+
import { a as selectBasePath, f as useAdminStore, g as cn, h as Button, l as selectNavigate, o as selectBrandName } from "./content-locales-provider-BXvuIgfg.mjs";
|
|
2
|
+
import { a as FieldContent, c as FieldGroup, f as Input, i as Field, l as FieldLabel, n as Alert, r as AlertDescription, s as FieldError, t as AuthLayout } from "./auth-layout-M8K8_q5R.mjs";
|
|
3
|
+
import { n as useAuthClient } from "./use-auth-BoLmWtmU.mjs";
|
|
4
|
+
import { CheckCircle, Envelope, SpinnerGap, WarningCircle } from "@phosphor-icons/react";
|
|
1
5
|
import * as React$1 from "react";
|
|
2
|
-
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { Button } from "../../components/ui/button";
|
|
4
|
-
import { Field, FieldContent, FieldError, FieldGroup, FieldLabel } from "../../components/ui/field";
|
|
5
|
-
import { Input } from "../../components/ui/input";
|
|
6
|
-
import { cn } from "../../lib/utils";
|
|
7
|
-
import { CheckCircle, Envelope, SpinnerGap } from "@phosphor-icons/react";
|
|
8
6
|
import { useForm } from "react-hook-form";
|
|
7
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
9
8
|
|
|
10
|
-
//#region src/views/auth/forgot-password-form.tsx
|
|
9
|
+
//#region src/client/views/auth/forgot-password-form.tsx
|
|
11
10
|
/**
|
|
12
11
|
* Forgot Password Form - request password reset email
|
|
13
12
|
*/
|
|
@@ -53,7 +52,7 @@ function ForgotPasswordForm({ onSubmit, onBackToLoginClick, defaultValues, class
|
|
|
53
52
|
className: cn("space-y-4 text-center", className),
|
|
54
53
|
children: [
|
|
55
54
|
/* @__PURE__ */ jsx("div", {
|
|
56
|
-
className: "bg-primary/10 mx-auto flex size-12 items-center justify-center
|
|
55
|
+
className: "bg-primary/10 mx-auto flex size-12 items-center justify-center",
|
|
57
56
|
children: /* @__PURE__ */ jsx(CheckCircle, {
|
|
58
57
|
className: "text-primary size-6",
|
|
59
58
|
weight: "duotone"
|
|
@@ -113,9 +112,9 @@ function ForgotPasswordForm({ onSubmit, onBackToLoginClick, defaultValues, class
|
|
|
113
112
|
})]
|
|
114
113
|
}), /* @__PURE__ */ jsx(FieldError, { children: errors.email?.message })] })]
|
|
115
114
|
}) }),
|
|
116
|
-
error && /* @__PURE__ */
|
|
117
|
-
|
|
118
|
-
children: error
|
|
115
|
+
error && /* @__PURE__ */ jsxs(Alert, {
|
|
116
|
+
variant: "destructive",
|
|
117
|
+
children: [/* @__PURE__ */ jsx(WarningCircle, {}), /* @__PURE__ */ jsx(AlertDescription, { children: error })]
|
|
119
118
|
}),
|
|
120
119
|
/* @__PURE__ */ jsx(Button, {
|
|
121
120
|
type: "submit",
|
|
@@ -132,10 +131,12 @@ function ForgotPasswordForm({ onSubmit, onBackToLoginClick, defaultValues, class
|
|
|
132
131
|
children: [
|
|
133
132
|
"Remember your password?",
|
|
134
133
|
" ",
|
|
135
|
-
/* @__PURE__ */ jsx(
|
|
134
|
+
/* @__PURE__ */ jsx(Button, {
|
|
136
135
|
type: "button",
|
|
136
|
+
variant: "link",
|
|
137
|
+
size: "sm",
|
|
137
138
|
onClick: onBackToLoginClick,
|
|
138
|
-
className: "text-
|
|
139
|
+
className: "h-auto p-0 text-xs",
|
|
139
140
|
children: "Back to login"
|
|
140
141
|
})
|
|
141
142
|
]
|
|
@@ -145,4 +146,76 @@ function ForgotPasswordForm({ onSubmit, onBackToLoginClick, defaultValues, class
|
|
|
145
146
|
}
|
|
146
147
|
|
|
147
148
|
//#endregion
|
|
148
|
-
|
|
149
|
+
//#region src/client/views/pages/forgot-password-page.tsx
|
|
150
|
+
/**
|
|
151
|
+
* Forgot Password Page
|
|
152
|
+
*
|
|
153
|
+
* Default forgot password page that uses AuthLayout and ForgotPasswordForm.
|
|
154
|
+
* Integrates with authClient from AdminProvider context.
|
|
155
|
+
*/
|
|
156
|
+
/**
|
|
157
|
+
* Default forgot password page component.
|
|
158
|
+
*
|
|
159
|
+
* Uses authClient from AdminProvider to handle password reset requests.
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* ```tsx
|
|
163
|
+
* // In your admin config
|
|
164
|
+
* const admin = qa<AppCMS>()
|
|
165
|
+
* .use(coreAdminModule)
|
|
166
|
+
* .pages({
|
|
167
|
+
* forgotPassword: page("forgot-password", { component: ForgotPasswordPage })
|
|
168
|
+
* .path("/forgot-password"),
|
|
169
|
+
* })
|
|
170
|
+
* ```
|
|
171
|
+
*/
|
|
172
|
+
function ForgotPasswordPage({ title = "Forgot password", description = "Enter your email to receive a password reset link", logo, loginPath, resetPasswordRedirectUrl }) {
|
|
173
|
+
const authClient = useAuthClient();
|
|
174
|
+
const navigate = useAdminStore(selectNavigate);
|
|
175
|
+
const basePath = useAdminStore(selectBasePath);
|
|
176
|
+
const brandName = useAdminStore(selectBrandName);
|
|
177
|
+
const [error, setError] = React$1.useState(null);
|
|
178
|
+
const handleSubmit = async (values) => {
|
|
179
|
+
setError(null);
|
|
180
|
+
try {
|
|
181
|
+
const redirectUrl = resetPasswordRedirectUrl ?? `${typeof window !== "undefined" ? window.location.origin : ""}${basePath}/reset-password`;
|
|
182
|
+
const result = await authClient.forgetPassword({
|
|
183
|
+
email: values.email,
|
|
184
|
+
redirectTo: redirectUrl
|
|
185
|
+
});
|
|
186
|
+
if (result.error) {
|
|
187
|
+
setError(result.error.message || "Failed to send reset email");
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
} catch (err) {
|
|
191
|
+
setError(err instanceof Error ? err.message : "An error occurred");
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
const handleBackToLoginClick = () => {
|
|
195
|
+
navigate(loginPath ?? `${basePath}/login`);
|
|
196
|
+
};
|
|
197
|
+
return /* @__PURE__ */ jsx(AuthLayout, {
|
|
198
|
+
title,
|
|
199
|
+
description,
|
|
200
|
+
logo: logo ?? /* @__PURE__ */ jsx(DefaultLogo, { brandName }),
|
|
201
|
+
children: /* @__PURE__ */ jsx(ForgotPasswordForm, {
|
|
202
|
+
onSubmit: handleSubmit,
|
|
203
|
+
onBackToLoginClick: handleBackToLoginClick,
|
|
204
|
+
error
|
|
205
|
+
})
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
function DefaultLogo({ brandName }) {
|
|
209
|
+
return /* @__PURE__ */ jsx("div", {
|
|
210
|
+
className: "text-center",
|
|
211
|
+
children: /* @__PURE__ */ jsx("h1", {
|
|
212
|
+
className: "text-xl font-bold",
|
|
213
|
+
children: brandName
|
|
214
|
+
})
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
var forgot_password_page_default = ForgotPasswordPage;
|
|
218
|
+
|
|
219
|
+
//#endregion
|
|
220
|
+
export { forgot_password_page_default as n, ForgotPasswordForm as r, ForgotPasswordPage as t };
|
|
221
|
+
//# sourceMappingURL=forgot-password-page-Bcp-An4Y.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"forgot-password-page-Bcp-An4Y.mjs","names":["React","React"],"sources":["../src/client/views/auth/forgot-password-form.tsx","../src/client/views/pages/forgot-password-page.tsx"],"sourcesContent":["/**\n * Forgot Password Form - request password reset email\n */\n\nimport {\n CheckCircle,\n Envelope,\n SpinnerGap,\n WarningCircle,\n} from \"@phosphor-icons/react\";\nimport * as React from \"react\";\nimport { useForm } from \"react-hook-form\";\nimport { Alert, AlertDescription } from \"../../components/ui/alert\";\nimport { Button } from \"../../components/ui/button\";\nimport {\n Field,\n FieldContent,\n FieldError,\n FieldGroup,\n FieldLabel,\n} from \"../../components/ui/field\";\nimport { Input } from \"../../components/ui/input\";\nimport { cn } from \"../../lib/utils\";\n\nexport type ForgotPasswordFormValues = {\n email: string;\n};\n\nexport type ForgotPasswordFormProps = {\n /** Called when form is submitted with valid data */\n onSubmit: (values: ForgotPasswordFormValues) => Promise<void>;\n /** Called when back to login link is clicked */\n onBackToLoginClick?: () => void;\n /** Default values */\n defaultValues?: Partial<ForgotPasswordFormValues>;\n /** Additional class name */\n className?: string;\n /** Error message from auth */\n error?: string | null;\n};\n\n/**\n * Forgot password form with email field\n *\n * @example\n * ```tsx\n * const authClient = createAdminAuthClient<typeof cms>({ baseURL: '...' })\n *\n * function ForgotPasswordPage() {\n * const [error, setError] = useState<string | null>(null)\n *\n * const handleSubmit = async (values: ForgotPasswordFormValues) => {\n * const result = await authClient.forgetPassword({\n * email: values.email,\n * redirectTo: '/reset-password',\n * })\n * if (result.error) {\n * setError(result.error.message)\n * }\n * }\n *\n * return (\n * <AuthLayout title=\"Forgot password\">\n * <ForgotPasswordForm onSubmit={handleSubmit} error={error} />\n * </AuthLayout>\n * )\n * }\n * ```\n */\nexport function ForgotPasswordForm({\n onSubmit,\n onBackToLoginClick,\n defaultValues,\n className,\n error,\n}: ForgotPasswordFormProps) {\n const [isSuccess, setIsSuccess] = React.useState(false);\n\n const {\n register,\n handleSubmit,\n formState: { errors, isSubmitting },\n } = useForm<ForgotPasswordFormValues>({\n defaultValues: {\n email: \"\",\n ...defaultValues,\n },\n });\n\n const handleFormSubmit = handleSubmit(async (values) => {\n await onSubmit(values);\n if (!error) {\n setIsSuccess(true);\n }\n });\n\n // Success state\n if (isSuccess) {\n return (\n <div className={cn(\"space-y-4 text-center\", className)}>\n <div className=\"bg-primary/10 mx-auto flex size-12 items-center justify-center\">\n <CheckCircle className=\"text-primary size-6\" weight=\"duotone\" />\n </div>\n <div className=\"space-y-2\">\n <h3 className=\"text-sm font-medium\">Check your email</h3>\n <p className=\"text-muted-foreground text-xs\">\n We've sent a password reset link to your email address. Please check\n your inbox and follow the instructions.\n </p>\n </div>\n <Button\n type=\"button\"\n variant=\"outline\"\n className=\"w-full\"\n onClick={onBackToLoginClick}\n >\n Back to login\n </Button>\n </div>\n );\n }\n\n return (\n <form onSubmit={handleFormSubmit} className={cn(\"space-y-4\", className)}>\n <p className=\"text-muted-foreground text-xs\">\n Enter your email address and we'll send you a link to reset your\n password.\n </p>\n\n <FieldGroup>\n {/* Email Field */}\n <Field data-invalid={!!errors.email}>\n <FieldLabel htmlFor=\"email\">Email</FieldLabel>\n <FieldContent>\n <div className=\"relative\">\n <Envelope\n className=\"text-muted-foreground absolute left-2 top-1/2 size-4 -translate-y-1/2\"\n weight=\"duotone\"\n />\n <Input\n id=\"email\"\n type=\"email\"\n placeholder=\"you@example.com\"\n className=\"pl-8\"\n autoComplete=\"email\"\n aria-invalid={!!errors.email}\n {...register(\"email\", {\n required: \"Email is required\",\n pattern: {\n value: /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/,\n message: \"Invalid email address\",\n },\n })}\n />\n </div>\n <FieldError>{errors.email?.message}</FieldError>\n </FieldContent>\n </Field>\n </FieldGroup>\n\n {/* Error Message */}\n {error && (\n <Alert variant=\"destructive\">\n <WarningCircle />\n <AlertDescription>{error}</AlertDescription>\n </Alert>\n )}\n\n {/* Submit Button */}\n <Button\n type=\"submit\"\n className=\"w-full\"\n size=\"lg\"\n disabled={isSubmitting}\n >\n {isSubmitting ? (\n <>\n <SpinnerGap className=\"animate-spin\" weight=\"bold\" />\n Sending...\n </>\n ) : (\n \"Send reset link\"\n )}\n </Button>\n\n {/* Back to Login Link */}\n <p className=\"text-muted-foreground text-center text-xs\">\n Remember your password?{\" \"}\n <Button\n type=\"button\"\n variant=\"link\"\n size=\"sm\"\n onClick={onBackToLoginClick}\n className=\"h-auto p-0 text-xs\"\n >\n Back to login\n </Button>\n </p>\n </form>\n );\n}\n","/**\n * Forgot Password Page\n *\n * Default forgot password page that uses AuthLayout and ForgotPasswordForm.\n * Integrates with authClient from AdminProvider context.\n */\n\nimport * as React from \"react\";\nimport { useAuthClient } from \"../../hooks/use-auth\";\nimport {\n selectBasePath,\n selectBrandName,\n selectNavigate,\n useAdminStore,\n} from \"../../runtime/provider\";\nimport { AuthLayout } from \"../auth/auth-layout\";\nimport {\n ForgotPasswordForm,\n type ForgotPasswordFormValues,\n} from \"../auth/forgot-password-form\";\n\nexport interface ForgotPasswordPageProps {\n /**\n * Title shown on the page\n * @default \"Forgot password\"\n */\n title?: string;\n\n /**\n * Description shown below the title\n * @default \"Enter your email to receive a password reset link\"\n */\n description?: string;\n\n /**\n * Logo component to show above the form\n */\n logo?: React.ReactNode;\n\n /**\n * Path to login page\n * @default \"{basePath}/login\"\n */\n loginPath?: string;\n\n /**\n * URL to redirect to after password reset (included in email)\n * @default \"{window.origin}{basePath}/reset-password\"\n */\n resetPasswordRedirectUrl?: string;\n}\n\n/**\n * Default forgot password page component.\n *\n * Uses authClient from AdminProvider to handle password reset requests.\n *\n * @example\n * ```tsx\n * // In your admin config\n * const admin = qa<AppCMS>()\n * .use(coreAdminModule)\n * .pages({\n * forgotPassword: page(\"forgot-password\", { component: ForgotPasswordPage })\n * .path(\"/forgot-password\"),\n * })\n * ```\n */\nexport function ForgotPasswordPage({\n title = \"Forgot password\",\n description = \"Enter your email to receive a password reset link\",\n logo,\n loginPath,\n resetPasswordRedirectUrl,\n}: ForgotPasswordPageProps) {\n const authClient = useAuthClient();\n const navigate = useAdminStore(selectNavigate);\n const basePath = useAdminStore(selectBasePath);\n const brandName = useAdminStore(selectBrandName);\n\n const [error, setError] = React.useState<string | null>(null);\n\n const handleSubmit = async (values: ForgotPasswordFormValues) => {\n setError(null);\n\n try {\n const redirectUrl =\n resetPasswordRedirectUrl ??\n `${typeof window !== \"undefined\" ? window.location.origin : \"\"}${basePath}/reset-password`;\n\n const result = await authClient.forgetPassword({\n email: values.email,\n redirectTo: redirectUrl,\n });\n\n if (result.error) {\n setError(result.error.message || \"Failed to send reset email\");\n return;\n }\n\n // Success is handled by the form (shows success message)\n } catch (err) {\n setError(err instanceof Error ? err.message : \"An error occurred\");\n }\n };\n\n const handleBackToLoginClick = () => {\n navigate(loginPath ?? `${basePath}/login`);\n };\n\n return (\n <AuthLayout\n title={title}\n description={description}\n logo={logo ?? <DefaultLogo brandName={brandName} />}\n >\n <ForgotPasswordForm\n onSubmit={handleSubmit}\n onBackToLoginClick={handleBackToLoginClick}\n error={error}\n />\n </AuthLayout>\n );\n}\n\nfunction DefaultLogo({ brandName }: { brandName: string }) {\n return (\n <div className=\"text-center\">\n <h1 className=\"text-xl font-bold\">{brandName}</h1>\n </div>\n );\n}\n\nexport default ForgotPasswordPage;\n"],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import "./content-locales-provider-BXvuIgfg.mjs";
|
|
2
|
+
import "./auth-layout-M8K8_q5R.mjs";
|
|
3
|
+
import "./use-auth-BoLmWtmU.mjs";
|
|
4
|
+
import "./card-BKHjBQfw.mjs";
|
|
5
|
+
import { n as forgot_password_page_default, t as ForgotPasswordPage } from "./forgot-password-page-Bcp-An4Y.mjs";
|
|
6
|
+
|
|
7
|
+
export { ForgotPasswordPage, forgot_password_page_default as default };
|