diff --git a/apps/v4/components/demo/DropzoneDemo.vue b/apps/v4/components/demo/DropzoneDemo.vue new file mode 100644 index 000000000..96cba5513 --- /dev/null +++ b/apps/v4/components/demo/DropzoneDemo.vue @@ -0,0 +1,139 @@ + + + diff --git a/apps/v4/components/demo/DropzoneMultiFile.vue b/apps/v4/components/demo/DropzoneMultiFile.vue new file mode 100644 index 000000000..8f5dcee6f --- /dev/null +++ b/apps/v4/components/demo/DropzoneMultiFile.vue @@ -0,0 +1,133 @@ + + + diff --git a/apps/v4/components/demo/DropzoneMultiImage.vue b/apps/v4/components/demo/DropzoneMultiImage.vue new file mode 100644 index 000000000..d6b6ab40c --- /dev/null +++ b/apps/v4/components/demo/DropzoneMultiImage.vue @@ -0,0 +1,121 @@ + + + diff --git a/apps/v4/components/demo/DropzoneSingleFile.vue b/apps/v4/components/demo/DropzoneSingleFile.vue new file mode 100644 index 000000000..92e714841 --- /dev/null +++ b/apps/v4/components/demo/DropzoneSingleFile.vue @@ -0,0 +1,69 @@ + + + diff --git a/apps/v4/components/demo/index.ts b/apps/v4/components/demo/index.ts index 9ea7f730d..6f07fa690 100644 --- a/apps/v4/components/demo/index.ts +++ b/apps/v4/components/demo/index.ts @@ -46,6 +46,12 @@ export { default as ComboboxDemo } from './ComboboxDemo.vue' export { default as DialogDemo } from './DialogDemo.vue' export { default as DialogResponsive } from './DialogResponsive.vue' +// Dropzone demos +export { default as DropzoneDemo } from './DropzoneDemo.vue' +export { default as DropzoneMultiFile } from './DropzoneMultiFile.vue' +export { default as DropzoneMultiImage } from './DropzoneMultiImage.vue' +export { default as DropzoneSingleFile } from './DropzoneSingleFile.vue' + // Hover Card demos export { default as HoverCardDemo } from './HoverCardDemo.vue' diff --git a/apps/v4/content/docs/components/dropzone.md b/apps/v4/content/docs/components/dropzone.md new file mode 100644 index 000000000..68624699c --- /dev/null +++ b/apps/v4/content/docs/components/dropzone.md @@ -0,0 +1,586 @@ +--- +title: Dropzone +description: A drag-and-drop file upload component with support for multiple files, validation, and upload progress. +component: true +--- + +::component-preview +--- +name: DropzoneDemo +description: A dropzone with file upload functionality +--- +:: + +## Installation + +::code-tabs + +::tabs-list + + ::tabs-trigger{value="cli"} + CLI + :: + + ::tabs-trigger{value="manual"} + Manual + :: + +:: + +::tabs-content{value="cli"} + +```bash +npx shadcn-vue@latest add dropzone +``` + +:: + +::tabs-content{value="manual"} + ::steps + ::step + Install the following dependencies: + :: + + ```bash + npm install vue3-dropzone + ``` + + ::step + Copy and paste the [GitHub source code](https://github.com/unovue/shadcn-vue/tree/dev/apps/v4/registry/new-york-v4/ui/dropzone) into your project. + :: + + ::step + Update the import paths to match your project setup. + :: + :: +:: + +:: + +## Usage + +```vue showLineNumbers + + + +``` + +## Examples + +### Single File Upload + +Perfect for profile pictures or single document uploads with automatic replacement when max files is reached. + +::component-preview +--- +name: DropzoneSingleFile +description: Single file upload with avatar preview +--- +:: + +```vue showLineNumbers + + + +``` + +### Multiple Files Upload + +Ideal for document uploads with detailed file management and retry functionality. + +::component-preview +--- +name: DropzoneMultiFile +description: Multiple file upload with progress and retry +--- +:: + +```vue showLineNumbers + + + +``` + +### Multiple Images Upload + +Perfect for image galleries with visual previews in a grid layout. + +::component-preview +--- +name: DropzoneMultiImage +description: Multiple image upload with grid preview +--- +:: + +```vue showLineNumbers + + + +``` + +## API Reference + +### useDropzoneUpload + +The `useDropzoneUpload` composable provides the core functionality for file dropping, validation, and upload handling. + +#### Options + +| Option | Type | Description | +| ------ | ---- | ----------- | +| `onDropFile` | `(file: File) => Promise` | Required. Function to handle file upload. Must return a promise with upload result. | +| `onRemoveFile` | `(id: string) => void \| Promise` | Optional. Function called when a file is removed. | +| `onFileUploaded` | `(result: TUploadRes) => void` | Optional. Callback when a file is successfully uploaded. | +| `onFileUploadError` | `(error: TUploadError) => void` | Optional. Callback when file upload fails. | +| `onAllUploaded` | `() => void` | Optional. Callback when all files are uploaded. | +| `onRootError` | `(error: string \| undefined) => void` | Optional. Callback when validation errors occur. | +| `maxRetryCount` | `number` | Optional. Maximum number of retry attempts. Default: `3` (provided by `useDropzoneUpload`) | +| `autoRetry` | `boolean` | Optional. Whether to automatically retry failed uploads. | +| `shapeUploadError` | `(error: TUploadError) => string \| void` | Optional. Function to transform error messages. | +| `shiftOnMaxFiles` | `boolean` | Optional. Whether to replace oldest file when max files reached. | +| `validation` | `ValidationOptions` | Optional. File validation rules. | + +#### Validation Options + +| Option | Type | Description | +| ------ | ---- | ----------- | +| `accept` | `string \| string[]` | Accepted file types (MIME types or extensions). | +| `minSize` | `number` | Minimum file size in bytes. | +| `maxSize` | `number` | Maximum file size in bytes. | +| `maxFiles` | `number` | Maximum number of files allowed. | + +#### Return Value + +| Property | Type | Description | +| -------- | ---- | ----------- | +| `getRootProps` | `Function` | Props for the root dropzone element. | +| `getInputProps` | `Function` | Props for the hidden file input. | +| `fileStatuses` | `Ref` | Reactive array of file upload statuses. | +| `isInvalid` | `Ref` | Whether the dropzone has validation errors. | +| `isDragActive` | `Ref` | Whether files are being dragged over the dropzone. | +| `rootError` | `Ref` | Current validation error message. | +| `onRemoveFile` | `(id: string) => Promise` | Function to remove a file. | +| `onRetry` | `(id: string) => Promise` | Function to retry a failed upload. | +| `canRetry` | `(id: string) => boolean` | Whether a file can be retried. | + +### Components + +#### Dropzone + +Root container component that provides context to all child components. + +```vue + +``` + +#### DropzoneArea + +Defines the active drop area where files can be dropped. + +#### DropzoneTrigger + +Clickable area that opens the file dialog when clicked. + +#### DropzoneDescription + +Displays help text or instructions for the dropzone. + +#### DropzoneMessage + +Displays validation error messages or status information. + +#### DropzoneFileList + +Container for displaying uploaded files. + +#### DropzoneFileListItem + +Individual file item within the file list. Provides context for file-specific actions. + +**Props:** +- `file: FileStatus` - File status object + +#### DropzoneFileMessage + +Displays messages specific to an individual file (requires DropzoneFileListItem context). + +#### DropzoneRemoveFile + +Button component for removing files (requires DropzoneFileListItem context). + +#### DropzoneRetryFile + +Button component for retrying failed uploads. Only shows when file status is "error" and retries are available. + +#### InfiniteProgress + +Progress indicator that shows upload status with different states for pending, success, and error. + +**Props:** +- `status: "pending" | "success" | "error"` - Current upload status + +### File Status Object + +```typescript +interface FileStatus { + id: string // Unique file identifier + fileName: string // Original file name + file: File // File object + tries: number // Number of upload attempts + status: "pending" | "error" | "success" // Current status + result?: TUploadRes // Upload result (when status is "success") + error?: TUploadError // Error details (when status is "error") +} +``` \ No newline at end of file diff --git a/apps/v4/package.json b/apps/v4/package.json index 028d3cc56..52330a560 100644 --- a/apps/v4/package.json +++ b/apps/v4/package.json @@ -55,6 +55,7 @@ "vue": "catalog:", "vue-input-otp": "^0.3.2", "vue-sonner": "catalog:", + "vue3-dropzone": "^2.2.1", "zod": "catalog:" }, "devDependencies": { diff --git a/apps/v4/public/r/index.json b/apps/v4/public/r/index.json index 480d89079..ced5fbeea 100644 --- a/apps/v4/public/r/index.json +++ b/apps/v4/public/r/index.json @@ -841,6 +841,70 @@ } ] }, + { + "name": "dropzone", + "type": "registry:ui", + "registryDependencies": [ + "button" + ], + "dependencies": [ + "vue3-dropzone" + ], + "files": [ + { + "path": "ui/dropzone/Dropzone.vue", + "type": "registry:ui" + }, + { + "path": "ui/dropzone/DropzoneArea.vue", + "type": "registry:ui" + }, + { + "path": "ui/dropzone/DropzoneDescription.vue", + "type": "registry:ui" + }, + { + "path": "ui/dropzone/DropzoneFileList.vue", + "type": "registry:ui" + }, + { + "path": "ui/dropzone/DropzoneFileListItem.vue", + "type": "registry:ui" + }, + { + "path": "ui/dropzone/DropzoneFileMessage.vue", + "type": "registry:ui" + }, + { + "path": "ui/dropzone/DropzoneMessage.vue", + "type": "registry:ui" + }, + { + "path": "ui/dropzone/DropzoneRemoveFile.vue", + "type": "registry:ui" + }, + { + "path": "ui/dropzone/DropzoneRetryFile.vue", + "type": "registry:ui" + }, + { + "path": "ui/dropzone/DropzoneTrigger.vue", + "type": "registry:ui" + }, + { + "path": "ui/dropzone/index.ts", + "type": "registry:ui" + }, + { + "path": "ui/dropzone/InfiniteProgress.vue", + "type": "registry:ui" + }, + { + "path": "ui/dropzone/useDropzoneUpload.ts", + "type": "registry:ui" + } + ] + }, { "name": "empty", "type": "registry:ui", @@ -1010,11 +1074,11 @@ ], "files": [ { - "path": "ui/input/Input.vue", + "path": "ui/input/index.ts", "type": "registry:ui" }, { - "path": "ui/input/index.ts", + "path": "ui/input/Input.vue", "type": "registry:ui" } ] @@ -1028,6 +1092,10 @@ "textarea" ], "files": [ + { + "path": "ui/input-group/index.ts", + "type": "registry:ui" + }, { "path": "ui/input-group/InputGroup.vue", "type": "registry:ui" @@ -1051,10 +1119,6 @@ { "path": "ui/input-group/InputGroupTextarea.vue", "type": "registry:ui" - }, - { - "path": "ui/input-group/index.ts", - "type": "registry:ui" } ] }, @@ -1067,6 +1131,10 @@ "reka-ui" ], "files": [ + { + "path": "ui/input-otp/index.ts", + "type": "registry:ui" + }, { "path": "ui/input-otp/InputOTP.vue", "type": "registry:ui" @@ -1082,10 +1150,6 @@ { "path": "ui/input-otp/InputOTPSlot.vue", "type": "registry:ui" - }, - { - "path": "ui/input-otp/index.ts", - "type": "registry:ui" } ] }, @@ -1099,6 +1163,10 @@ "separator" ], "files": [ + { + "path": "ui/item/index.ts", + "type": "registry:ui" + }, { "path": "ui/item/Item.vue", "type": "registry:ui" @@ -1138,10 +1206,6 @@ { "path": "ui/item/ItemTitle.vue", "type": "registry:ui" - }, - { - "path": "ui/item/index.ts", - "type": "registry:ui" } ] }, @@ -1150,15 +1214,15 @@ "type": "registry:ui", "files": [ { - "path": "ui/kbd/Kbd.vue", + "path": "ui/kbd/index.ts", "type": "registry:ui" }, { - "path": "ui/kbd/KbdGroup.vue", + "path": "ui/kbd/Kbd.vue", "type": "registry:ui" }, { - "path": "ui/kbd/index.ts", + "path": "ui/kbd/KbdGroup.vue", "type": "registry:ui" } ] @@ -1172,11 +1236,11 @@ ], "files": [ { - "path": "ui/label/Label.vue", + "path": "ui/label/index.ts", "type": "registry:ui" }, { - "path": "ui/label/index.ts", + "path": "ui/label/Label.vue", "type": "registry:ui" } ] @@ -1189,6 +1253,10 @@ "@vueuse/core" ], "files": [ + { + "path": "ui/menubar/index.ts", + "type": "registry:ui" + }, { "path": "ui/menubar/Menubar.vue", "type": "registry:ui" @@ -1248,10 +1316,6 @@ { "path": "ui/menubar/MenubarTrigger.vue", "type": "registry:ui" - }, - { - "path": "ui/menubar/index.ts", - "type": "registry:ui" } ] }, @@ -1264,19 +1328,19 @@ ], "files": [ { - "path": "ui/native-select/NativeSelect.vue", + "path": "ui/native-select/index.ts", "type": "registry:ui" }, { - "path": "ui/native-select/NativeSelectOptGroup.vue", + "path": "ui/native-select/NativeSelect.vue", "type": "registry:ui" }, { - "path": "ui/native-select/NativeSelectOption.vue", + "path": "ui/native-select/NativeSelectOptGroup.vue", "type": "registry:ui" }, { - "path": "ui/native-select/index.ts", + "path": "ui/native-select/NativeSelectOption.vue", "type": "registry:ui" } ] @@ -1289,6 +1353,10 @@ "@vueuse/core" ], "files": [ + { + "path": "ui/navigation-menu/index.ts", + "type": "registry:ui" + }, { "path": "ui/navigation-menu/NavigationMenu.vue", "type": "registry:ui" @@ -1320,10 +1388,6 @@ { "path": "ui/navigation-menu/NavigationMenuViewport.vue", "type": "registry:ui" - }, - { - "path": "ui/navigation-menu/index.ts", - "type": "registry:ui" } ] }, @@ -1335,6 +1399,10 @@ "@vueuse/core" ], "files": [ + { + "path": "ui/number-field/index.ts", + "type": "registry:ui" + }, { "path": "ui/number-field/NumberField.vue", "type": "registry:ui" @@ -1354,10 +1422,6 @@ { "path": "ui/number-field/NumberFieldInput.vue", "type": "registry:ui" - }, - { - "path": "ui/number-field/index.ts", - "type": "registry:ui" } ] }, @@ -1372,6 +1436,10 @@ "button" ], "files": [ + { + "path": "ui/pagination/index.ts", + "type": "registry:ui" + }, { "path": "ui/pagination/Pagination.vue", "type": "registry:ui" @@ -1403,10 +1471,6 @@ { "path": "ui/pagination/PaginationPrevious.vue", "type": "registry:ui" - }, - { - "path": "ui/pagination/index.ts", - "type": "registry:ui" } ] }, @@ -1418,6 +1482,10 @@ "@vueuse/core" ], "files": [ + { + "path": "ui/pin-input/index.ts", + "type": "registry:ui" + }, { "path": "ui/pin-input/PinInput.vue", "type": "registry:ui" @@ -1433,10 +1501,6 @@ { "path": "ui/pin-input/PinInputSlot.vue", "type": "registry:ui" - }, - { - "path": "ui/pin-input/index.ts", - "type": "registry:ui" } ] }, @@ -1448,6 +1512,10 @@ "@vueuse/core" ], "files": [ + { + "path": "ui/popover/index.ts", + "type": "registry:ui" + }, { "path": "ui/popover/Popover.vue", "type": "registry:ui" @@ -1463,10 +1531,6 @@ { "path": "ui/popover/PopoverTrigger.vue", "type": "registry:ui" - }, - { - "path": "ui/popover/index.ts", - "type": "registry:ui" } ] }, @@ -1479,11 +1543,11 @@ ], "files": [ { - "path": "ui/progress/Progress.vue", + "path": "ui/progress/index.ts", "type": "registry:ui" }, { - "path": "ui/progress/index.ts", + "path": "ui/progress/Progress.vue", "type": "registry:ui" } ] @@ -1497,15 +1561,15 @@ ], "files": [ { - "path": "ui/radio-group/RadioGroup.vue", + "path": "ui/radio-group/index.ts", "type": "registry:ui" }, { - "path": "ui/radio-group/RadioGroupItem.vue", + "path": "ui/radio-group/RadioGroup.vue", "type": "registry:ui" }, { - "path": "ui/radio-group/index.ts", + "path": "ui/radio-group/RadioGroupItem.vue", "type": "registry:ui" } ] @@ -1521,6 +1585,10 @@ "button" ], "files": [ + { + "path": "ui/range-calendar/index.ts", + "type": "registry:ui" + }, { "path": "ui/range-calendar/RangeCalendar.vue", "type": "registry:ui" @@ -1568,10 +1636,6 @@ { "path": "ui/range-calendar/RangeCalendarPrevButton.vue", "type": "registry:ui" - }, - { - "path": "ui/range-calendar/index.ts", - "type": "registry:ui" } ] }, @@ -1584,19 +1648,19 @@ ], "files": [ { - "path": "ui/resizable/ResizableHandle.vue", + "path": "ui/resizable/index.ts", "type": "registry:ui" }, { - "path": "ui/resizable/ResizablePanel.vue", + "path": "ui/resizable/ResizableHandle.vue", "type": "registry:ui" }, { - "path": "ui/resizable/ResizablePanelGroup.vue", + "path": "ui/resizable/ResizablePanel.vue", "type": "registry:ui" }, { - "path": "ui/resizable/index.ts", + "path": "ui/resizable/ResizablePanelGroup.vue", "type": "registry:ui" } ] @@ -1610,15 +1674,15 @@ ], "files": [ { - "path": "ui/scroll-area/ScrollArea.vue", + "path": "ui/scroll-area/index.ts", "type": "registry:ui" }, { - "path": "ui/scroll-area/ScrollBar.vue", + "path": "ui/scroll-area/ScrollArea.vue", "type": "registry:ui" }, { - "path": "ui/scroll-area/index.ts", + "path": "ui/scroll-area/ScrollBar.vue", "type": "registry:ui" } ] @@ -1631,6 +1695,10 @@ "@vueuse/core" ], "files": [ + { + "path": "ui/select/index.ts", + "type": "registry:ui" + }, { "path": "ui/select/Select.vue", "type": "registry:ui" @@ -1674,10 +1742,6 @@ { "path": "ui/select/SelectValue.vue", "type": "registry:ui" - }, - { - "path": "ui/select/index.ts", - "type": "registry:ui" } ] }, @@ -1690,11 +1754,11 @@ ], "files": [ { - "path": "ui/separator/Separator.vue", + "path": "ui/separator/index.ts", "type": "registry:ui" }, { - "path": "ui/separator/index.ts", + "path": "ui/separator/Separator.vue", "type": "registry:ui" } ] @@ -1707,6 +1771,10 @@ "@vueuse/core" ], "files": [ + { + "path": "ui/sheet/index.ts", + "type": "registry:ui" + }, { "path": "ui/sheet/Sheet.vue", "type": "registry:ui" @@ -1742,10 +1810,6 @@ { "path": "ui/sheet/SheetTrigger.vue", "type": "registry:ui" - }, - { - "path": "ui/sheet/index.ts", - "type": "registry:ui" } ] }, @@ -1765,6 +1829,10 @@ "button" ], "files": [ + { + "path": "ui/sidebar/index.ts", + "type": "registry:ui" + }, { "path": "ui/sidebar/Sidebar.vue", "type": "registry:ui" @@ -1861,10 +1929,6 @@ "path": "ui/sidebar/SidebarTrigger.vue", "type": "registry:ui" }, - { - "path": "ui/sidebar/index.ts", - "type": "registry:ui" - }, { "path": "ui/sidebar/utils.ts", "type": "registry:ui" @@ -1876,11 +1940,11 @@ "type": "registry:ui", "files": [ { - "path": "ui/skeleton/Skeleton.vue", + "path": "ui/skeleton/index.ts", "type": "registry:ui" }, { - "path": "ui/skeleton/index.ts", + "path": "ui/skeleton/Skeleton.vue", "type": "registry:ui" } ] @@ -1894,11 +1958,11 @@ ], "files": [ { - "path": "ui/slider/Slider.vue", + "path": "ui/slider/index.ts", "type": "registry:ui" }, { - "path": "ui/slider/index.ts", + "path": "ui/slider/Slider.vue", "type": "registry:ui" } ] @@ -1911,11 +1975,11 @@ ], "files": [ { - "path": "ui/sonner/Sonner.vue", + "path": "ui/sonner/index.ts", "type": "registry:ui" }, { - "path": "ui/sonner/index.ts", + "path": "ui/sonner/Sonner.vue", "type": "registry:ui" } ] @@ -1925,11 +1989,11 @@ "type": "registry:ui", "files": [ { - "path": "ui/spinner/Spinner.vue", + "path": "ui/spinner/index.ts", "type": "registry:ui" }, { - "path": "ui/spinner/index.ts", + "path": "ui/spinner/Spinner.vue", "type": "registry:ui" } ] @@ -1942,6 +2006,10 @@ "@vueuse/core" ], "files": [ + { + "path": "ui/stepper/index.ts", + "type": "registry:ui" + }, { "path": "ui/stepper/Stepper.vue", "type": "registry:ui" @@ -1969,10 +2037,6 @@ { "path": "ui/stepper/StepperTrigger.vue", "type": "registry:ui" - }, - { - "path": "ui/stepper/index.ts", - "type": "registry:ui" } ] }, @@ -1985,11 +2049,11 @@ ], "files": [ { - "path": "ui/switch/Switch.vue", + "path": "ui/switch/index.ts", "type": "registry:ui" }, { - "path": "ui/switch/index.ts", + "path": "ui/switch/Switch.vue", "type": "registry:ui" } ] @@ -2002,6 +2066,10 @@ "@tanstack/vue-table" ], "files": [ + { + "path": "ui/table/index.ts", + "type": "registry:ui" + }, { "path": "ui/table/Table.vue", "type": "registry:ui" @@ -2038,10 +2106,6 @@ "path": "ui/table/TableRow.vue", "type": "registry:ui" }, - { - "path": "ui/table/index.ts", - "type": "registry:ui" - }, { "path": "ui/table/utils.ts", "type": "registry:ui" @@ -2056,6 +2120,10 @@ "@vueuse/core" ], "files": [ + { + "path": "ui/tabs/index.ts", + "type": "registry:ui" + }, { "path": "ui/tabs/Tabs.vue", "type": "registry:ui" @@ -2071,10 +2139,6 @@ { "path": "ui/tabs/TabsTrigger.vue", "type": "registry:ui" - }, - { - "path": "ui/tabs/index.ts", - "type": "registry:ui" } ] }, @@ -2086,6 +2150,10 @@ "@vueuse/core" ], "files": [ + { + "path": "ui/tags-input/index.ts", + "type": "registry:ui" + }, { "path": "ui/tags-input/TagsInput.vue", "type": "registry:ui" @@ -2105,10 +2173,6 @@ { "path": "ui/tags-input/TagsInputItemText.vue", "type": "registry:ui" - }, - { - "path": "ui/tags-input/index.ts", - "type": "registry:ui" } ] }, @@ -2120,11 +2184,11 @@ ], "files": [ { - "path": "ui/textarea/Textarea.vue", + "path": "ui/textarea/index.ts", "type": "registry:ui" }, { - "path": "ui/textarea/index.ts", + "path": "ui/textarea/Textarea.vue", "type": "registry:ui" } ] @@ -2138,11 +2202,11 @@ ], "files": [ { - "path": "ui/toggle/Toggle.vue", + "path": "ui/toggle/index.ts", "type": "registry:ui" }, { - "path": "ui/toggle/index.ts", + "path": "ui/toggle/Toggle.vue", "type": "registry:ui" } ] @@ -2159,15 +2223,15 @@ ], "files": [ { - "path": "ui/toggle-group/ToggleGroup.vue", + "path": "ui/toggle-group/index.ts", "type": "registry:ui" }, { - "path": "ui/toggle-group/ToggleGroupItem.vue", + "path": "ui/toggle-group/ToggleGroup.vue", "type": "registry:ui" }, { - "path": "ui/toggle-group/index.ts", + "path": "ui/toggle-group/ToggleGroupItem.vue", "type": "registry:ui" } ] @@ -2180,6 +2244,10 @@ "@vueuse/core" ], "files": [ + { + "path": "ui/tooltip/index.ts", + "type": "registry:ui" + }, { "path": "ui/tooltip/Tooltip.vue", "type": "registry:ui" @@ -2195,10 +2263,6 @@ { "path": "ui/tooltip/TooltipTrigger.vue", "type": "registry:ui" - }, - { - "path": "ui/tooltip/index.ts", - "type": "registry:ui" } ] } diff --git a/apps/v4/public/r/styles/new-york-v4/dropzone.json b/apps/v4/public/r/styles/new-york-v4/dropzone.json new file mode 100644 index 000000000..f43f811c2 --- /dev/null +++ b/apps/v4/public/r/styles/new-york-v4/dropzone.json @@ -0,0 +1,78 @@ +{ + "$schema": "https://shadcn-vue.com/schema/registry-item.json", + "name": "dropzone", + "type": "registry:ui", + "dependencies": [ + "vue3-dropzone" + ], + "registryDependencies": [ + "button" + ], + "files": [ + { + "path": "registry/new-york-v4/ui/dropzone/Dropzone.vue", + "content": "\n\n\n", + "type": "registry:ui" + }, + { + "path": "registry/new-york-v4/ui/dropzone/DropzoneArea.vue", + "content": "\n\n\n", + "type": "registry:ui" + }, + { + "path": "registry/new-york-v4/ui/dropzone/DropzoneDescription.vue", + "content": "\n\n\n", + "type": "registry:ui" + }, + { + "path": "registry/new-york-v4/ui/dropzone/DropzoneFileList.vue", + "content": "\n\n\n", + "type": "registry:ui" + }, + { + "path": "registry/new-york-v4/ui/dropzone/DropzoneFileListItem.vue", + "content": "\n\n\n", + "type": "registry:ui" + }, + { + "path": "registry/new-york-v4/ui/dropzone/DropzoneFileMessage.vue", + "content": "\n\n\n", + "type": "registry:ui" + }, + { + "path": "registry/new-york-v4/ui/dropzone/DropzoneMessage.vue", + "content": "\n\n\n", + "type": "registry:ui" + }, + { + "path": "registry/new-york-v4/ui/dropzone/DropzoneRemoveFile.vue", + "content": "\n\n\n", + "type": "registry:ui" + }, + { + "path": "registry/new-york-v4/ui/dropzone/DropzoneRetryFile.vue", + "content": "\n\n\n", + "type": "registry:ui" + }, + { + "path": "registry/new-york-v4/ui/dropzone/DropzoneTrigger.vue", + "content": "\n\n\n", + "type": "registry:ui" + }, + { + "path": "registry/new-york-v4/ui/dropzone/index.ts", + "content": "export { default as Dropzone } from \"./Dropzone.vue\"\nexport { default as DropzoneArea } from \"./DropzoneArea.vue\"\nexport { default as DropzoneDescription } from \"./DropzoneDescription.vue\"\nexport { default as DropzoneFileList } from \"./DropzoneFileList.vue\"\nexport { default as DropzoneFileListItem } from \"./DropzoneFileListItem.vue\"\nexport { default as DropzoneFileMessage } from \"./DropzoneFileMessage.vue\"\nexport { default as DropzoneMessage } from \"./DropzoneMessage.vue\"\nexport { default as DropzoneRemoveFile } from \"./DropzoneRemoveFile.vue\"\nexport { default as DropzoneRetryFile } from \"./DropzoneRetryFile.vue\"\nexport { default as DropzoneTrigger } from \"./DropzoneTrigger.vue\"\nexport { default as InfiniteProgress } from \"./InfiniteProgress.vue\"\nexport {\n type DropZoneErrorCode,\n type DropzoneResult,\n type FileStatus,\n useDropzoneUpload,\n type UseDropzoneUploadOptions,\n type UseDropzoneUploadReturn,\n} from \"./useDropzoneUpload\"\n", + "type": "registry:ui" + }, + { + "path": "registry/new-york-v4/ui/dropzone/InfiniteProgress.vue", + "content": "\n\n\n\n\n", + "type": "registry:ui" + }, + { + "path": "registry/new-york-v4/ui/dropzone/useDropzoneUpload.ts", + "content": "import type { InjectionKey, Ref } from \"vue\"\nimport type { FileRejectReason, InputFile } from \"vue3-dropzone\"\nimport { computed, ref } from \"vue\"\nimport { useDropzone as useVue3Dropzone } from \"vue3-dropzone\"\n\nexport type DropzoneResult\n = | { status: \"pending\" }\n | { status: \"error\", error: TUploadError }\n | { status: \"success\", result: TUploadRes }\n\nexport interface FileStatus {\n id: string\n fileName: string\n file: File\n tries: number\n status: \"pending\" | \"error\" | \"success\"\n result?: TUploadRes\n error?: TUploadError\n}\n\nexport type DropZoneErrorCode\n = | \"file-invalid-type\"\n | \"file-too-large\"\n | \"file-too-small\"\n | \"too-many-files\"\n\nconst dropZoneErrorCodes: readonly DropZoneErrorCode[] = [\n \"file-invalid-type\",\n \"file-too-large\",\n \"file-too-small\",\n \"too-many-files\",\n] as const\n\nfunction getDropZoneErrorCodes(fileRejections: FileRejectReason[]): DropZoneErrorCode[] {\n const errors = fileRejections.flatMap(rejection =>\n rejection.errors\n .filter((error): error is { code: string, message: string } =>\n error !== null && typeof error === \"object\" && \"code\" in error\n && dropZoneErrorCodes.includes(error.code as DropZoneErrorCode),\n )\n .map(error => error.code as DropZoneErrorCode),\n )\n return Array.from(new Set(errors))\n}\n\nfunction getRootError(\n errorCodes: DropZoneErrorCode[],\n limits: {\n accept?: string | string[]\n maxSize?: number\n minSize?: number\n maxFiles?: number\n },\n): string {\n const errors = errorCodes.map((error) => {\n switch (error) {\n case \"file-invalid-type\": {\n const acceptedTypes = Array.isArray(limits.accept)\n ? limits.accept.join(\", \")\n : limits.accept ?? \"\"\n return `only ${acceptedTypes} are allowed`\n }\n case \"file-too-large\": {\n const maxMb = limits.maxSize\n ? (limits.maxSize / (1024 * 1024)).toFixed(2)\n : \"infinite?\"\n return `max size is ${maxMb}MB`\n }\n case \"file-too-small\": {\n const roundedMinSize = limits.minSize\n ? (limits.minSize / (1024 * 1024)).toFixed(2)\n : \"negative?\"\n return `min size is ${roundedMinSize}MB`\n }\n case \"too-many-files\":\n return `max ${limits.maxFiles} files`\n }\n })\n const joinedErrors = errors.join(\", \")\n return joinedErrors.charAt(0).toUpperCase() + joinedErrors.slice(1)\n}\n\nexport interface UseDropzoneUploadOptions {\n onDropFile: (\n file: File,\n ) => Promise, { status: \"pending\" }>>\n onRemoveFile?: (id: string) => void | Promise\n onFileUploaded?: (result: TUploadRes) => void\n onFileUploadError?: (error: TUploadError) => void\n onAllUploaded?: () => void\n onRootError?: (error: string | undefined) => void\n maxRetryCount?: number\n autoRetry?: boolean\n validation?: {\n accept?: string | string[]\n minSize?: number\n maxSize?: number\n maxFiles?: number\n }\n shiftOnMaxFiles?: boolean\n shapeUploadError?: (error: TUploadError) => string | void\n}\n\nexport interface UseDropzoneUploadReturn {\n getRootProps: ReturnType[\"getRootProps\"]\n getInputProps: ReturnType[\"getInputProps\"]\n onRemoveFile: (id: string) => Promise\n onRetry: (id: string) => Promise\n canRetry: (id: string) => boolean\n fileStatuses: Ref[]>\n isInvalid: Ref\n isDragActive: Ref\n rootError: Ref\n inputId: string\n rootMessageId: string\n rootDescriptionId: string\n getFileMessageId: (id: string) => string\n}\n\n// Injection key for dropzone context\nexport const DropzoneContextKey: InjectionKey> = Symbol(\"dropzone-context\")\n\n// Injection key for file list item context\nexport interface DropzoneFileListItemContext {\n onRemoveFile: () => Promise\n onRetry: () => Promise\n fileStatus: Ref>\n canRetry: Ref\n dropzoneId: string\n messageId: string\n}\n\nexport const DropzoneFileListItemContextKey: InjectionKey> = Symbol(\"dropzone-file-list-item-context\")\n\nexport function useDropzoneUpload(\n options: UseDropzoneUploadOptions,\n): UseDropzoneUploadReturn {\n const {\n onDropFile: pOnDropFile,\n onRemoveFile: pOnRemoveFile,\n shapeUploadError: pShapeUploadError,\n onFileUploaded: pOnFileUploaded,\n onFileUploadError: pOnFileUploadError,\n onAllUploaded: pOnAllUploaded,\n onRootError: pOnRootError,\n maxRetryCount,\n autoRetry,\n validation,\n shiftOnMaxFiles,\n } = options\n\n // Generate unique IDs\n const inputId = `dropzone-${crypto.randomUUID().slice(0, 8)}`\n const rootMessageId = `${inputId}-root-message`\n const rootDescriptionId = `${inputId}-description`\n\n const rootError = ref(undefined)\n const fileStatuses = ref[]>([])\n\n const setRootError = (error: string | undefined) => {\n rootError.value = error\n if (pOnRootError !== undefined) {\n pOnRootError(error)\n }\n }\n\n const isInvalid = computed(() => {\n return (\n fileStatuses.value.filter(file => file.status === \"error\").length > 0\n || rootError.value !== undefined\n )\n })\n\n const uploadFile = async (file: File, id: string, tries = 0) => {\n const result = await pOnDropFile(file)\n\n if (result.status === \"error\") {\n if (autoRetry === true && tries < (maxRetryCount ?? Infinity)) {\n // Update status to pending for retry\n const index = fileStatuses.value.findIndex(f => f.id === id)\n const currentFile = fileStatuses.value[index]\n if (index !== -1 && currentFile) {\n fileStatuses.value = [\n ...fileStatuses.value.slice(0, index),\n { ...currentFile, status: \"pending\" as const, tries: currentFile.tries + 1 },\n ...fileStatuses.value.slice(index + 1),\n ] as FileStatus[]\n }\n return uploadFile(file, id, tries + 1)\n }\n\n // Update status to error\n const index = fileStatuses.value.findIndex(f => f.id === id)\n const currentFile = fileStatuses.value[index]\n if (index !== -1 && currentFile) {\n const shapedError = pShapeUploadError !== undefined\n ? pShapeUploadError(result.error)\n : result.error\n fileStatuses.value = [\n ...fileStatuses.value.slice(0, index),\n { ...currentFile, status: \"error\" as const, error: shapedError as TUploadError },\n ...fileStatuses.value.slice(index + 1),\n ] as FileStatus[]\n }\n if (pOnFileUploadError !== undefined) {\n pOnFileUploadError(result.error)\n }\n return\n }\n\n // Update status to success\n const index = fileStatuses.value.findIndex(f => f.id === id)\n const currentFile = fileStatuses.value[index]\n if (index !== -1 && currentFile) {\n fileStatuses.value = [\n ...fileStatuses.value.slice(0, index),\n { ...currentFile, status: \"success\" as const, result: result.result },\n ...fileStatuses.value.slice(index + 1),\n ] as FileStatus[]\n }\n if (pOnFileUploaded !== undefined) {\n pOnFileUploaded(result.result)\n }\n }\n\n const onRemoveFile = async (id: string) => {\n await pOnRemoveFile?.(id)\n fileStatuses.value = fileStatuses.value.filter(f => f.id !== id)\n }\n\n const canRetry = (id: string): boolean => {\n const fileStatus = fileStatuses.value.find(file => file.id === id)\n return (\n fileStatus?.status === \"error\"\n && fileStatus.tries < (maxRetryCount ?? Infinity)\n )\n }\n\n const onRetry = async (id: string) => {\n if (!canRetry(id)) {\n return\n }\n const fileStatus = fileStatuses.value.find(file => file.id === id)\n if (!fileStatus || fileStatus.status !== \"error\") {\n return\n }\n\n // Update status to pending\n const index = fileStatuses.value.findIndex(f => f.id === id)\n const currentFile = fileStatuses.value[index]\n if (index !== -1 && currentFile) {\n fileStatuses.value = [\n ...fileStatuses.value.slice(0, index),\n { ...currentFile, status: \"pending\" as const, tries: currentFile.tries + 1 },\n ...fileStatuses.value.slice(index + 1),\n ] as FileStatus[]\n }\n\n await uploadFile(fileStatus.file, id)\n }\n\n const getFileMessageId = (id: string) => `${inputId}-${id}-message`\n\n const onDropAccepted = async (acceptedFiles: InputFile[]) => {\n setRootError(undefined)\n\n const newFiles = acceptedFiles.filter((f): f is File => f instanceof File)\n const fileCount = fileStatuses.value.length\n const maxNewFiles\n = validation?.maxFiles === undefined\n ? Infinity\n : validation.maxFiles - fileCount\n\n if (maxNewFiles < newFiles.length) {\n if (!shiftOnMaxFiles) {\n setRootError(getRootError([\"too-many-files\"], validation ?? {}))\n }\n }\n\n const slicedNewFiles\n = shiftOnMaxFiles === true ? newFiles : newFiles.slice(0, maxNewFiles)\n\n const onDropFilePromises = slicedNewFiles.map(async (file, index) => {\n const existingFile = fileStatuses.value[index]\n if (fileCount + 1 > maxNewFiles && shiftOnMaxFiles && existingFile) {\n await onRemoveFile(existingFile.id)\n }\n\n const id = crypto.randomUUID()\n const newFileStatus: FileStatus = {\n id,\n fileName: file.name,\n file,\n status: \"pending\",\n tries: 1,\n }\n fileStatuses.value = [...fileStatuses.value, newFileStatus] as FileStatus[]\n await uploadFile(file, id)\n })\n\n await Promise.all(onDropFilePromises)\n if (pOnAllUploaded !== undefined) {\n pOnAllUploaded()\n }\n }\n\n const onDropRejected = (fileRejections: FileRejectReason[]) => {\n const errorMessage = getRootError(\n getDropZoneErrorCodes(fileRejections),\n validation ?? {},\n )\n setRootError(errorMessage)\n }\n\n const dropzone = useVue3Dropzone({\n accept: validation?.accept,\n minSize: validation?.minSize ?? 0,\n maxSize: validation?.maxSize ?? Infinity,\n onDropAccepted,\n onDropRejected,\n })\n\n return {\n getRootProps: dropzone.getRootProps,\n getInputProps: dropzone.getInputProps,\n inputId,\n rootMessageId,\n rootDescriptionId,\n getFileMessageId,\n onRemoveFile,\n onRetry,\n canRetry,\n fileStatuses: fileStatuses as Ref[]>,\n isInvalid,\n rootError,\n isDragActive: dropzone.isDragActive,\n }\n}\n", + "type": "registry:ui" + } + ] +} diff --git a/apps/v4/public/r/styles/new-york-v4/registry.json b/apps/v4/public/r/styles/new-york-v4/registry.json index 0aa7adeed..445e49672 100644 --- a/apps/v4/public/r/styles/new-york-v4/registry.json +++ b/apps/v4/public/r/styles/new-york-v4/registry.json @@ -876,6 +876,70 @@ ], "type": "registry:ui" }, + { + "name": "dropzone", + "type": "registry:ui", + "dependencies": [ + "vue3-dropzone" + ], + "registryDependencies": [ + "button" + ], + "files": [ + { + "path": "registry/new-york-v4/ui/dropzone/Dropzone.vue", + "type": "registry:ui" + }, + { + "path": "registry/new-york-v4/ui/dropzone/DropzoneArea.vue", + "type": "registry:ui" + }, + { + "path": "registry/new-york-v4/ui/dropzone/DropzoneDescription.vue", + "type": "registry:ui" + }, + { + "path": "registry/new-york-v4/ui/dropzone/DropzoneFileList.vue", + "type": "registry:ui" + }, + { + "path": "registry/new-york-v4/ui/dropzone/DropzoneFileListItem.vue", + "type": "registry:ui" + }, + { + "path": "registry/new-york-v4/ui/dropzone/DropzoneFileMessage.vue", + "type": "registry:ui" + }, + { + "path": "registry/new-york-v4/ui/dropzone/DropzoneMessage.vue", + "type": "registry:ui" + }, + { + "path": "registry/new-york-v4/ui/dropzone/DropzoneRemoveFile.vue", + "type": "registry:ui" + }, + { + "path": "registry/new-york-v4/ui/dropzone/DropzoneRetryFile.vue", + "type": "registry:ui" + }, + { + "path": "registry/new-york-v4/ui/dropzone/DropzoneTrigger.vue", + "type": "registry:ui" + }, + { + "path": "registry/new-york-v4/ui/dropzone/index.ts", + "type": "registry:ui" + }, + { + "path": "registry/new-york-v4/ui/dropzone/InfiniteProgress.vue", + "type": "registry:ui" + }, + { + "path": "registry/new-york-v4/ui/dropzone/useDropzoneUpload.ts", + "type": "registry:ui" + } + ] + }, { "name": "empty", "files": [ @@ -1044,11 +1108,11 @@ ], "files": [ { - "path": "registry/new-york-v4/ui/input/Input.vue", + "path": "registry/new-york-v4/ui/input/index.ts", "type": "registry:ui" }, { - "path": "registry/new-york-v4/ui/input/index.ts", + "path": "registry/new-york-v4/ui/input/Input.vue", "type": "registry:ui" } ], @@ -1062,6 +1126,10 @@ "textarea" ], "files": [ + { + "path": "registry/new-york-v4/ui/input-group/index.ts", + "type": "registry:ui" + }, { "path": "registry/new-york-v4/ui/input-group/InputGroup.vue", "type": "registry:ui" @@ -1085,10 +1153,6 @@ { "path": "registry/new-york-v4/ui/input-group/InputGroupTextarea.vue", "type": "registry:ui" - }, - { - "path": "registry/new-york-v4/ui/input-group/index.ts", - "type": "registry:ui" } ], "type": "registry:ui" @@ -1101,6 +1165,10 @@ "reka-ui" ], "files": [ + { + "path": "registry/new-york-v4/ui/input-otp/index.ts", + "type": "registry:ui" + }, { "path": "registry/new-york-v4/ui/input-otp/InputOTP.vue", "type": "registry:ui" @@ -1116,10 +1184,6 @@ { "path": "registry/new-york-v4/ui/input-otp/InputOTPSlot.vue", "type": "registry:ui" - }, - { - "path": "registry/new-york-v4/ui/input-otp/index.ts", - "type": "registry:ui" } ], "type": "registry:ui" @@ -1133,6 +1197,10 @@ "separator" ], "files": [ + { + "path": "registry/new-york-v4/ui/item/index.ts", + "type": "registry:ui" + }, { "path": "registry/new-york-v4/ui/item/Item.vue", "type": "registry:ui" @@ -1172,10 +1240,6 @@ { "path": "registry/new-york-v4/ui/item/ItemTitle.vue", "type": "registry:ui" - }, - { - "path": "registry/new-york-v4/ui/item/index.ts", - "type": "registry:ui" } ], "type": "registry:ui" @@ -1184,15 +1248,15 @@ "name": "kbd", "files": [ { - "path": "registry/new-york-v4/ui/kbd/Kbd.vue", + "path": "registry/new-york-v4/ui/kbd/index.ts", "type": "registry:ui" }, { - "path": "registry/new-york-v4/ui/kbd/KbdGroup.vue", + "path": "registry/new-york-v4/ui/kbd/Kbd.vue", "type": "registry:ui" }, { - "path": "registry/new-york-v4/ui/kbd/index.ts", + "path": "registry/new-york-v4/ui/kbd/KbdGroup.vue", "type": "registry:ui" } ], @@ -1206,11 +1270,11 @@ ], "files": [ { - "path": "registry/new-york-v4/ui/label/Label.vue", + "path": "registry/new-york-v4/ui/label/index.ts", "type": "registry:ui" }, { - "path": "registry/new-york-v4/ui/label/index.ts", + "path": "registry/new-york-v4/ui/label/Label.vue", "type": "registry:ui" } ], @@ -1223,6 +1287,10 @@ "@vueuse/core" ], "files": [ + { + "path": "registry/new-york-v4/ui/menubar/index.ts", + "type": "registry:ui" + }, { "path": "registry/new-york-v4/ui/menubar/Menubar.vue", "type": "registry:ui" @@ -1282,10 +1350,6 @@ { "path": "registry/new-york-v4/ui/menubar/MenubarTrigger.vue", "type": "registry:ui" - }, - { - "path": "registry/new-york-v4/ui/menubar/index.ts", - "type": "registry:ui" } ], "type": "registry:ui" @@ -1298,19 +1362,19 @@ ], "files": [ { - "path": "registry/new-york-v4/ui/native-select/NativeSelect.vue", + "path": "registry/new-york-v4/ui/native-select/index.ts", "type": "registry:ui" }, { - "path": "registry/new-york-v4/ui/native-select/NativeSelectOptGroup.vue", + "path": "registry/new-york-v4/ui/native-select/NativeSelect.vue", "type": "registry:ui" }, { - "path": "registry/new-york-v4/ui/native-select/NativeSelectOption.vue", + "path": "registry/new-york-v4/ui/native-select/NativeSelectOptGroup.vue", "type": "registry:ui" }, { - "path": "registry/new-york-v4/ui/native-select/index.ts", + "path": "registry/new-york-v4/ui/native-select/NativeSelectOption.vue", "type": "registry:ui" } ], @@ -1323,6 +1387,10 @@ "@vueuse/core" ], "files": [ + { + "path": "registry/new-york-v4/ui/navigation-menu/index.ts", + "type": "registry:ui" + }, { "path": "registry/new-york-v4/ui/navigation-menu/NavigationMenu.vue", "type": "registry:ui" @@ -1354,10 +1422,6 @@ { "path": "registry/new-york-v4/ui/navigation-menu/NavigationMenuViewport.vue", "type": "registry:ui" - }, - { - "path": "registry/new-york-v4/ui/navigation-menu/index.ts", - "type": "registry:ui" } ], "type": "registry:ui" @@ -1369,6 +1433,10 @@ "@vueuse/core" ], "files": [ + { + "path": "registry/new-york-v4/ui/number-field/index.ts", + "type": "registry:ui" + }, { "path": "registry/new-york-v4/ui/number-field/NumberField.vue", "type": "registry:ui" @@ -1388,10 +1456,6 @@ { "path": "registry/new-york-v4/ui/number-field/NumberFieldInput.vue", "type": "registry:ui" - }, - { - "path": "registry/new-york-v4/ui/number-field/index.ts", - "type": "registry:ui" } ], "type": "registry:ui" @@ -1406,6 +1470,10 @@ "button" ], "files": [ + { + "path": "registry/new-york-v4/ui/pagination/index.ts", + "type": "registry:ui" + }, { "path": "registry/new-york-v4/ui/pagination/Pagination.vue", "type": "registry:ui" @@ -1437,10 +1505,6 @@ { "path": "registry/new-york-v4/ui/pagination/PaginationPrevious.vue", "type": "registry:ui" - }, - { - "path": "registry/new-york-v4/ui/pagination/index.ts", - "type": "registry:ui" } ], "type": "registry:ui" @@ -1452,6 +1516,10 @@ "@vueuse/core" ], "files": [ + { + "path": "registry/new-york-v4/ui/pin-input/index.ts", + "type": "registry:ui" + }, { "path": "registry/new-york-v4/ui/pin-input/PinInput.vue", "type": "registry:ui" @@ -1467,10 +1535,6 @@ { "path": "registry/new-york-v4/ui/pin-input/PinInputSlot.vue", "type": "registry:ui" - }, - { - "path": "registry/new-york-v4/ui/pin-input/index.ts", - "type": "registry:ui" } ], "type": "registry:ui" @@ -1482,6 +1546,10 @@ "@vueuse/core" ], "files": [ + { + "path": "registry/new-york-v4/ui/popover/index.ts", + "type": "registry:ui" + }, { "path": "registry/new-york-v4/ui/popover/Popover.vue", "type": "registry:ui" @@ -1497,10 +1565,6 @@ { "path": "registry/new-york-v4/ui/popover/PopoverTrigger.vue", "type": "registry:ui" - }, - { - "path": "registry/new-york-v4/ui/popover/index.ts", - "type": "registry:ui" } ], "type": "registry:ui" @@ -1513,11 +1577,11 @@ ], "files": [ { - "path": "registry/new-york-v4/ui/progress/Progress.vue", + "path": "registry/new-york-v4/ui/progress/index.ts", "type": "registry:ui" }, { - "path": "registry/new-york-v4/ui/progress/index.ts", + "path": "registry/new-york-v4/ui/progress/Progress.vue", "type": "registry:ui" } ], @@ -1531,15 +1595,15 @@ ], "files": [ { - "path": "registry/new-york-v4/ui/radio-group/RadioGroup.vue", + "path": "registry/new-york-v4/ui/radio-group/index.ts", "type": "registry:ui" }, { - "path": "registry/new-york-v4/ui/radio-group/RadioGroupItem.vue", + "path": "registry/new-york-v4/ui/radio-group/RadioGroup.vue", "type": "registry:ui" }, { - "path": "registry/new-york-v4/ui/radio-group/index.ts", + "path": "registry/new-york-v4/ui/radio-group/RadioGroupItem.vue", "type": "registry:ui" } ], @@ -1555,6 +1619,10 @@ "button" ], "files": [ + { + "path": "registry/new-york-v4/ui/range-calendar/index.ts", + "type": "registry:ui" + }, { "path": "registry/new-york-v4/ui/range-calendar/RangeCalendar.vue", "type": "registry:ui" @@ -1602,10 +1670,6 @@ { "path": "registry/new-york-v4/ui/range-calendar/RangeCalendarPrevButton.vue", "type": "registry:ui" - }, - { - "path": "registry/new-york-v4/ui/range-calendar/index.ts", - "type": "registry:ui" } ], "type": "registry:ui" @@ -1618,19 +1682,19 @@ ], "files": [ { - "path": "registry/new-york-v4/ui/resizable/ResizableHandle.vue", + "path": "registry/new-york-v4/ui/resizable/index.ts", "type": "registry:ui" }, { - "path": "registry/new-york-v4/ui/resizable/ResizablePanel.vue", + "path": "registry/new-york-v4/ui/resizable/ResizableHandle.vue", "type": "registry:ui" }, { - "path": "registry/new-york-v4/ui/resizable/ResizablePanelGroup.vue", + "path": "registry/new-york-v4/ui/resizable/ResizablePanel.vue", "type": "registry:ui" }, { - "path": "registry/new-york-v4/ui/resizable/index.ts", + "path": "registry/new-york-v4/ui/resizable/ResizablePanelGroup.vue", "type": "registry:ui" } ], @@ -1644,15 +1708,15 @@ ], "files": [ { - "path": "registry/new-york-v4/ui/scroll-area/ScrollArea.vue", + "path": "registry/new-york-v4/ui/scroll-area/index.ts", "type": "registry:ui" }, { - "path": "registry/new-york-v4/ui/scroll-area/ScrollBar.vue", + "path": "registry/new-york-v4/ui/scroll-area/ScrollArea.vue", "type": "registry:ui" }, { - "path": "registry/new-york-v4/ui/scroll-area/index.ts", + "path": "registry/new-york-v4/ui/scroll-area/ScrollBar.vue", "type": "registry:ui" } ], @@ -1665,6 +1729,10 @@ "@vueuse/core" ], "files": [ + { + "path": "registry/new-york-v4/ui/select/index.ts", + "type": "registry:ui" + }, { "path": "registry/new-york-v4/ui/select/Select.vue", "type": "registry:ui" @@ -1708,10 +1776,6 @@ { "path": "registry/new-york-v4/ui/select/SelectValue.vue", "type": "registry:ui" - }, - { - "path": "registry/new-york-v4/ui/select/index.ts", - "type": "registry:ui" } ], "type": "registry:ui" @@ -1724,11 +1788,11 @@ ], "files": [ { - "path": "registry/new-york-v4/ui/separator/Separator.vue", + "path": "registry/new-york-v4/ui/separator/index.ts", "type": "registry:ui" }, { - "path": "registry/new-york-v4/ui/separator/index.ts", + "path": "registry/new-york-v4/ui/separator/Separator.vue", "type": "registry:ui" } ], @@ -1741,6 +1805,10 @@ "@vueuse/core" ], "files": [ + { + "path": "registry/new-york-v4/ui/sheet/index.ts", + "type": "registry:ui" + }, { "path": "registry/new-york-v4/ui/sheet/Sheet.vue", "type": "registry:ui" @@ -1776,10 +1844,6 @@ { "path": "registry/new-york-v4/ui/sheet/SheetTrigger.vue", "type": "registry:ui" - }, - { - "path": "registry/new-york-v4/ui/sheet/index.ts", - "type": "registry:ui" } ], "type": "registry:ui" @@ -1799,6 +1863,10 @@ "button" ], "files": [ + { + "path": "registry/new-york-v4/ui/sidebar/index.ts", + "type": "registry:ui" + }, { "path": "registry/new-york-v4/ui/sidebar/Sidebar.vue", "type": "registry:ui" @@ -1895,10 +1963,6 @@ "path": "registry/new-york-v4/ui/sidebar/SidebarTrigger.vue", "type": "registry:ui" }, - { - "path": "registry/new-york-v4/ui/sidebar/index.ts", - "type": "registry:ui" - }, { "path": "registry/new-york-v4/ui/sidebar/utils.ts", "type": "registry:ui" @@ -1910,11 +1974,11 @@ "name": "skeleton", "files": [ { - "path": "registry/new-york-v4/ui/skeleton/Skeleton.vue", + "path": "registry/new-york-v4/ui/skeleton/index.ts", "type": "registry:ui" }, { - "path": "registry/new-york-v4/ui/skeleton/index.ts", + "path": "registry/new-york-v4/ui/skeleton/Skeleton.vue", "type": "registry:ui" } ], @@ -1928,11 +1992,11 @@ ], "files": [ { - "path": "registry/new-york-v4/ui/slider/Slider.vue", + "path": "registry/new-york-v4/ui/slider/index.ts", "type": "registry:ui" }, { - "path": "registry/new-york-v4/ui/slider/index.ts", + "path": "registry/new-york-v4/ui/slider/Slider.vue", "type": "registry:ui" } ], @@ -1945,11 +2009,11 @@ ], "files": [ { - "path": "registry/new-york-v4/ui/sonner/Sonner.vue", + "path": "registry/new-york-v4/ui/sonner/index.ts", "type": "registry:ui" }, { - "path": "registry/new-york-v4/ui/sonner/index.ts", + "path": "registry/new-york-v4/ui/sonner/Sonner.vue", "type": "registry:ui" } ], @@ -1959,11 +2023,11 @@ "name": "spinner", "files": [ { - "path": "registry/new-york-v4/ui/spinner/Spinner.vue", + "path": "registry/new-york-v4/ui/spinner/index.ts", "type": "registry:ui" }, { - "path": "registry/new-york-v4/ui/spinner/index.ts", + "path": "registry/new-york-v4/ui/spinner/Spinner.vue", "type": "registry:ui" } ], @@ -1976,6 +2040,10 @@ "@vueuse/core" ], "files": [ + { + "path": "registry/new-york-v4/ui/stepper/index.ts", + "type": "registry:ui" + }, { "path": "registry/new-york-v4/ui/stepper/Stepper.vue", "type": "registry:ui" @@ -2003,10 +2071,6 @@ { "path": "registry/new-york-v4/ui/stepper/StepperTrigger.vue", "type": "registry:ui" - }, - { - "path": "registry/new-york-v4/ui/stepper/index.ts", - "type": "registry:ui" } ], "type": "registry:ui" @@ -2019,11 +2083,11 @@ ], "files": [ { - "path": "registry/new-york-v4/ui/switch/Switch.vue", + "path": "registry/new-york-v4/ui/switch/index.ts", "type": "registry:ui" }, { - "path": "registry/new-york-v4/ui/switch/index.ts", + "path": "registry/new-york-v4/ui/switch/Switch.vue", "type": "registry:ui" } ], @@ -2036,6 +2100,10 @@ "@tanstack/vue-table" ], "files": [ + { + "path": "registry/new-york-v4/ui/table/index.ts", + "type": "registry:ui" + }, { "path": "registry/new-york-v4/ui/table/Table.vue", "type": "registry:ui" @@ -2072,10 +2140,6 @@ "path": "registry/new-york-v4/ui/table/TableRow.vue", "type": "registry:ui" }, - { - "path": "registry/new-york-v4/ui/table/index.ts", - "type": "registry:ui" - }, { "path": "registry/new-york-v4/ui/table/utils.ts", "type": "registry:ui" @@ -2090,6 +2154,10 @@ "@vueuse/core" ], "files": [ + { + "path": "registry/new-york-v4/ui/tabs/index.ts", + "type": "registry:ui" + }, { "path": "registry/new-york-v4/ui/tabs/Tabs.vue", "type": "registry:ui" @@ -2105,10 +2173,6 @@ { "path": "registry/new-york-v4/ui/tabs/TabsTrigger.vue", "type": "registry:ui" - }, - { - "path": "registry/new-york-v4/ui/tabs/index.ts", - "type": "registry:ui" } ], "type": "registry:ui" @@ -2120,6 +2184,10 @@ "@vueuse/core" ], "files": [ + { + "path": "registry/new-york-v4/ui/tags-input/index.ts", + "type": "registry:ui" + }, { "path": "registry/new-york-v4/ui/tags-input/TagsInput.vue", "type": "registry:ui" @@ -2139,10 +2207,6 @@ { "path": "registry/new-york-v4/ui/tags-input/TagsInputItemText.vue", "type": "registry:ui" - }, - { - "path": "registry/new-york-v4/ui/tags-input/index.ts", - "type": "registry:ui" } ], "type": "registry:ui" @@ -2154,11 +2218,11 @@ ], "files": [ { - "path": "registry/new-york-v4/ui/textarea/Textarea.vue", + "path": "registry/new-york-v4/ui/textarea/index.ts", "type": "registry:ui" }, { - "path": "registry/new-york-v4/ui/textarea/index.ts", + "path": "registry/new-york-v4/ui/textarea/Textarea.vue", "type": "registry:ui" } ], @@ -2172,11 +2236,11 @@ ], "files": [ { - "path": "registry/new-york-v4/ui/toggle/Toggle.vue", + "path": "registry/new-york-v4/ui/toggle/index.ts", "type": "registry:ui" }, { - "path": "registry/new-york-v4/ui/toggle/index.ts", + "path": "registry/new-york-v4/ui/toggle/Toggle.vue", "type": "registry:ui" } ], @@ -2193,15 +2257,15 @@ ], "files": [ { - "path": "registry/new-york-v4/ui/toggle-group/ToggleGroup.vue", + "path": "registry/new-york-v4/ui/toggle-group/index.ts", "type": "registry:ui" }, { - "path": "registry/new-york-v4/ui/toggle-group/ToggleGroupItem.vue", + "path": "registry/new-york-v4/ui/toggle-group/ToggleGroup.vue", "type": "registry:ui" }, { - "path": "registry/new-york-v4/ui/toggle-group/index.ts", + "path": "registry/new-york-v4/ui/toggle-group/ToggleGroupItem.vue", "type": "registry:ui" } ], @@ -2214,6 +2278,10 @@ "@vueuse/core" ], "files": [ + { + "path": "registry/new-york-v4/ui/tooltip/index.ts", + "type": "registry:ui" + }, { "path": "registry/new-york-v4/ui/tooltip/Tooltip.vue", "type": "registry:ui" @@ -2229,10 +2297,6 @@ { "path": "registry/new-york-v4/ui/tooltip/TooltipTrigger.vue", "type": "registry:ui" - }, - { - "path": "registry/new-york-v4/ui/tooltip/index.ts", - "type": "registry:ui" } ], "type": "registry:ui" @@ -2280,11 +2344,11 @@ "type": "registry:component" }, { - "path": "registry/new-york-v4/blocks/dashboard-01/components/DragHandle.vue", + "path": "registry/new-york-v4/blocks/dashboard-01/components/DraggableRow.vue", "type": "registry:component" }, { - "path": "registry/new-york-v4/blocks/dashboard-01/components/DraggableRow.vue", + "path": "registry/new-york-v4/blocks/dashboard-01/components/DragHandle.vue", "type": "registry:component" }, { diff --git a/apps/v4/registry/__index__.ts b/apps/v4/registry/__index__.ts index 6f07c1242..04da759d6 100644 --- a/apps/v4/registry/__index__.ts +++ b/apps/v4/registry/__index__.ts @@ -836,6 +836,67 @@ export const Index: Record = { categories: undefined, meta: undefined, }, + "dropzone": { + name: "dropzone", + description: "", + type: "registry:ui", + registryDependencies: ["button"], + files: [{ + path: "registry/new-york-v4/ui/dropzone/Dropzone.vue", + type: "registry:ui", + target: "" + },{ + path: "registry/new-york-v4/ui/dropzone/DropzoneArea.vue", + type: "registry:ui", + target: "" + },{ + path: "registry/new-york-v4/ui/dropzone/DropzoneDescription.vue", + type: "registry:ui", + target: "" + },{ + path: "registry/new-york-v4/ui/dropzone/DropzoneFileList.vue", + type: "registry:ui", + target: "" + },{ + path: "registry/new-york-v4/ui/dropzone/DropzoneFileListItem.vue", + type: "registry:ui", + target: "" + },{ + path: "registry/new-york-v4/ui/dropzone/DropzoneFileMessage.vue", + type: "registry:ui", + target: "" + },{ + path: "registry/new-york-v4/ui/dropzone/DropzoneMessage.vue", + type: "registry:ui", + target: "" + },{ + path: "registry/new-york-v4/ui/dropzone/DropzoneRemoveFile.vue", + type: "registry:ui", + target: "" + },{ + path: "registry/new-york-v4/ui/dropzone/DropzoneRetryFile.vue", + type: "registry:ui", + target: "" + },{ + path: "registry/new-york-v4/ui/dropzone/DropzoneTrigger.vue", + type: "registry:ui", + target: "" + },{ + path: "registry/new-york-v4/ui/dropzone/index.ts", + type: "registry:ui", + target: "" + },{ + path: "registry/new-york-v4/ui/dropzone/InfiniteProgress.vue", + type: "registry:ui", + target: "" + },{ + path: "registry/new-york-v4/ui/dropzone/useDropzoneUpload.ts", + type: "registry:ui", + target: "" + }], + categories: undefined, + meta: undefined, + }, "empty": { name: "empty", description: "", @@ -998,11 +1059,11 @@ export const Index: Record = { type: "registry:ui", registryDependencies: undefined, files: [{ - path: "registry/new-york-v4/ui/input/Input.vue", + path: "registry/new-york-v4/ui/input/index.ts", type: "registry:ui", target: "" },{ - path: "registry/new-york-v4/ui/input/index.ts", + path: "registry/new-york-v4/ui/input/Input.vue", type: "registry:ui", target: "" }], @@ -1015,6 +1076,10 @@ export const Index: Record = { type: "registry:ui", registryDependencies: ["button","input","textarea"], files: [{ + path: "registry/new-york-v4/ui/input-group/index.ts", + type: "registry:ui", + target: "" + },{ path: "registry/new-york-v4/ui/input-group/InputGroup.vue", type: "registry:ui", target: "" @@ -1038,10 +1103,6 @@ export const Index: Record = { path: "registry/new-york-v4/ui/input-group/InputGroupTextarea.vue", type: "registry:ui", target: "" - },{ - path: "registry/new-york-v4/ui/input-group/index.ts", - type: "registry:ui", - target: "" }], categories: undefined, meta: undefined, @@ -1052,6 +1113,10 @@ export const Index: Record = { type: "registry:ui", registryDependencies: undefined, files: [{ + path: "registry/new-york-v4/ui/input-otp/index.ts", + type: "registry:ui", + target: "" + },{ path: "registry/new-york-v4/ui/input-otp/InputOTP.vue", type: "registry:ui", target: "" @@ -1067,10 +1132,6 @@ export const Index: Record = { path: "registry/new-york-v4/ui/input-otp/InputOTPSlot.vue", type: "registry:ui", target: "" - },{ - path: "registry/new-york-v4/ui/input-otp/index.ts", - type: "registry:ui", - target: "" }], categories: undefined, meta: undefined, @@ -1081,6 +1142,10 @@ export const Index: Record = { type: "registry:ui", registryDependencies: ["separator"], files: [{ + path: "registry/new-york-v4/ui/item/index.ts", + type: "registry:ui", + target: "" + },{ path: "registry/new-york-v4/ui/item/Item.vue", type: "registry:ui", target: "" @@ -1120,10 +1185,6 @@ export const Index: Record = { path: "registry/new-york-v4/ui/item/ItemTitle.vue", type: "registry:ui", target: "" - },{ - path: "registry/new-york-v4/ui/item/index.ts", - type: "registry:ui", - target: "" }], categories: undefined, meta: undefined, @@ -1134,15 +1195,15 @@ export const Index: Record = { type: "registry:ui", registryDependencies: undefined, files: [{ - path: "registry/new-york-v4/ui/kbd/Kbd.vue", + path: "registry/new-york-v4/ui/kbd/index.ts", type: "registry:ui", target: "" },{ - path: "registry/new-york-v4/ui/kbd/KbdGroup.vue", + path: "registry/new-york-v4/ui/kbd/Kbd.vue", type: "registry:ui", target: "" },{ - path: "registry/new-york-v4/ui/kbd/index.ts", + path: "registry/new-york-v4/ui/kbd/KbdGroup.vue", type: "registry:ui", target: "" }], @@ -1155,11 +1216,11 @@ export const Index: Record = { type: "registry:ui", registryDependencies: undefined, files: [{ - path: "registry/new-york-v4/ui/label/Label.vue", + path: "registry/new-york-v4/ui/label/index.ts", type: "registry:ui", target: "" },{ - path: "registry/new-york-v4/ui/label/index.ts", + path: "registry/new-york-v4/ui/label/Label.vue", type: "registry:ui", target: "" }], @@ -1172,6 +1233,10 @@ export const Index: Record = { type: "registry:ui", registryDependencies: undefined, files: [{ + path: "registry/new-york-v4/ui/menubar/index.ts", + type: "registry:ui", + target: "" + },{ path: "registry/new-york-v4/ui/menubar/Menubar.vue", type: "registry:ui", target: "" @@ -1231,10 +1296,6 @@ export const Index: Record = { path: "registry/new-york-v4/ui/menubar/MenubarTrigger.vue", type: "registry:ui", target: "" - },{ - path: "registry/new-york-v4/ui/menubar/index.ts", - type: "registry:ui", - target: "" }], categories: undefined, meta: undefined, @@ -1245,19 +1306,19 @@ export const Index: Record = { type: "registry:ui", registryDependencies: undefined, files: [{ - path: "registry/new-york-v4/ui/native-select/NativeSelect.vue", + path: "registry/new-york-v4/ui/native-select/index.ts", type: "registry:ui", target: "" },{ - path: "registry/new-york-v4/ui/native-select/NativeSelectOptGroup.vue", + path: "registry/new-york-v4/ui/native-select/NativeSelect.vue", type: "registry:ui", target: "" },{ - path: "registry/new-york-v4/ui/native-select/NativeSelectOption.vue", + path: "registry/new-york-v4/ui/native-select/NativeSelectOptGroup.vue", type: "registry:ui", target: "" },{ - path: "registry/new-york-v4/ui/native-select/index.ts", + path: "registry/new-york-v4/ui/native-select/NativeSelectOption.vue", type: "registry:ui", target: "" }], @@ -1270,6 +1331,10 @@ export const Index: Record = { type: "registry:ui", registryDependencies: undefined, files: [{ + path: "registry/new-york-v4/ui/navigation-menu/index.ts", + type: "registry:ui", + target: "" + },{ path: "registry/new-york-v4/ui/navigation-menu/NavigationMenu.vue", type: "registry:ui", target: "" @@ -1301,10 +1366,6 @@ export const Index: Record = { path: "registry/new-york-v4/ui/navigation-menu/NavigationMenuViewport.vue", type: "registry:ui", target: "" - },{ - path: "registry/new-york-v4/ui/navigation-menu/index.ts", - type: "registry:ui", - target: "" }], categories: undefined, meta: undefined, @@ -1315,6 +1376,10 @@ export const Index: Record = { type: "registry:ui", registryDependencies: undefined, files: [{ + path: "registry/new-york-v4/ui/number-field/index.ts", + type: "registry:ui", + target: "" + },{ path: "registry/new-york-v4/ui/number-field/NumberField.vue", type: "registry:ui", target: "" @@ -1334,10 +1399,6 @@ export const Index: Record = { path: "registry/new-york-v4/ui/number-field/NumberFieldInput.vue", type: "registry:ui", target: "" - },{ - path: "registry/new-york-v4/ui/number-field/index.ts", - type: "registry:ui", - target: "" }], categories: undefined, meta: undefined, @@ -1348,6 +1409,10 @@ export const Index: Record = { type: "registry:ui", registryDependencies: ["button"], files: [{ + path: "registry/new-york-v4/ui/pagination/index.ts", + type: "registry:ui", + target: "" + },{ path: "registry/new-york-v4/ui/pagination/Pagination.vue", type: "registry:ui", target: "" @@ -1379,10 +1444,6 @@ export const Index: Record = { path: "registry/new-york-v4/ui/pagination/PaginationPrevious.vue", type: "registry:ui", target: "" - },{ - path: "registry/new-york-v4/ui/pagination/index.ts", - type: "registry:ui", - target: "" }], categories: undefined, meta: undefined, @@ -1393,6 +1454,10 @@ export const Index: Record = { type: "registry:ui", registryDependencies: undefined, files: [{ + path: "registry/new-york-v4/ui/pin-input/index.ts", + type: "registry:ui", + target: "" + },{ path: "registry/new-york-v4/ui/pin-input/PinInput.vue", type: "registry:ui", target: "" @@ -1408,10 +1473,6 @@ export const Index: Record = { path: "registry/new-york-v4/ui/pin-input/PinInputSlot.vue", type: "registry:ui", target: "" - },{ - path: "registry/new-york-v4/ui/pin-input/index.ts", - type: "registry:ui", - target: "" }], categories: undefined, meta: undefined, @@ -1422,6 +1483,10 @@ export const Index: Record = { type: "registry:ui", registryDependencies: undefined, files: [{ + path: "registry/new-york-v4/ui/popover/index.ts", + type: "registry:ui", + target: "" + },{ path: "registry/new-york-v4/ui/popover/Popover.vue", type: "registry:ui", target: "" @@ -1437,10 +1502,6 @@ export const Index: Record = { path: "registry/new-york-v4/ui/popover/PopoverTrigger.vue", type: "registry:ui", target: "" - },{ - path: "registry/new-york-v4/ui/popover/index.ts", - type: "registry:ui", - target: "" }], categories: undefined, meta: undefined, @@ -1451,11 +1512,11 @@ export const Index: Record = { type: "registry:ui", registryDependencies: undefined, files: [{ - path: "registry/new-york-v4/ui/progress/Progress.vue", + path: "registry/new-york-v4/ui/progress/index.ts", type: "registry:ui", target: "" },{ - path: "registry/new-york-v4/ui/progress/index.ts", + path: "registry/new-york-v4/ui/progress/Progress.vue", type: "registry:ui", target: "" }], @@ -1468,15 +1529,15 @@ export const Index: Record = { type: "registry:ui", registryDependencies: undefined, files: [{ - path: "registry/new-york-v4/ui/radio-group/RadioGroup.vue", + path: "registry/new-york-v4/ui/radio-group/index.ts", type: "registry:ui", target: "" },{ - path: "registry/new-york-v4/ui/radio-group/RadioGroupItem.vue", + path: "registry/new-york-v4/ui/radio-group/RadioGroup.vue", type: "registry:ui", target: "" },{ - path: "registry/new-york-v4/ui/radio-group/index.ts", + path: "registry/new-york-v4/ui/radio-group/RadioGroupItem.vue", type: "registry:ui", target: "" }], @@ -1489,6 +1550,10 @@ export const Index: Record = { type: "registry:ui", registryDependencies: ["button"], files: [{ + path: "registry/new-york-v4/ui/range-calendar/index.ts", + type: "registry:ui", + target: "" + },{ path: "registry/new-york-v4/ui/range-calendar/RangeCalendar.vue", type: "registry:ui", target: "" @@ -1536,10 +1601,6 @@ export const Index: Record = { path: "registry/new-york-v4/ui/range-calendar/RangeCalendarPrevButton.vue", type: "registry:ui", target: "" - },{ - path: "registry/new-york-v4/ui/range-calendar/index.ts", - type: "registry:ui", - target: "" }], categories: undefined, meta: undefined, @@ -1550,19 +1611,19 @@ export const Index: Record = { type: "registry:ui", registryDependencies: undefined, files: [{ - path: "registry/new-york-v4/ui/resizable/ResizableHandle.vue", + path: "registry/new-york-v4/ui/resizable/index.ts", type: "registry:ui", target: "" },{ - path: "registry/new-york-v4/ui/resizable/ResizablePanel.vue", + path: "registry/new-york-v4/ui/resizable/ResizableHandle.vue", type: "registry:ui", target: "" },{ - path: "registry/new-york-v4/ui/resizable/ResizablePanelGroup.vue", + path: "registry/new-york-v4/ui/resizable/ResizablePanel.vue", type: "registry:ui", target: "" },{ - path: "registry/new-york-v4/ui/resizable/index.ts", + path: "registry/new-york-v4/ui/resizable/ResizablePanelGroup.vue", type: "registry:ui", target: "" }], @@ -1575,15 +1636,15 @@ export const Index: Record = { type: "registry:ui", registryDependencies: undefined, files: [{ - path: "registry/new-york-v4/ui/scroll-area/ScrollArea.vue", + path: "registry/new-york-v4/ui/scroll-area/index.ts", type: "registry:ui", target: "" },{ - path: "registry/new-york-v4/ui/scroll-area/ScrollBar.vue", + path: "registry/new-york-v4/ui/scroll-area/ScrollArea.vue", type: "registry:ui", target: "" },{ - path: "registry/new-york-v4/ui/scroll-area/index.ts", + path: "registry/new-york-v4/ui/scroll-area/ScrollBar.vue", type: "registry:ui", target: "" }], @@ -1596,6 +1657,10 @@ export const Index: Record = { type: "registry:ui", registryDependencies: undefined, files: [{ + path: "registry/new-york-v4/ui/select/index.ts", + type: "registry:ui", + target: "" + },{ path: "registry/new-york-v4/ui/select/Select.vue", type: "registry:ui", target: "" @@ -1639,10 +1704,6 @@ export const Index: Record = { path: "registry/new-york-v4/ui/select/SelectValue.vue", type: "registry:ui", target: "" - },{ - path: "registry/new-york-v4/ui/select/index.ts", - type: "registry:ui", - target: "" }], categories: undefined, meta: undefined, @@ -1653,11 +1714,11 @@ export const Index: Record = { type: "registry:ui", registryDependencies: undefined, files: [{ - path: "registry/new-york-v4/ui/separator/Separator.vue", + path: "registry/new-york-v4/ui/separator/index.ts", type: "registry:ui", target: "" },{ - path: "registry/new-york-v4/ui/separator/index.ts", + path: "registry/new-york-v4/ui/separator/Separator.vue", type: "registry:ui", target: "" }], @@ -1670,6 +1731,10 @@ export const Index: Record = { type: "registry:ui", registryDependencies: undefined, files: [{ + path: "registry/new-york-v4/ui/sheet/index.ts", + type: "registry:ui", + target: "" + },{ path: "registry/new-york-v4/ui/sheet/Sheet.vue", type: "registry:ui", target: "" @@ -1705,10 +1770,6 @@ export const Index: Record = { path: "registry/new-york-v4/ui/sheet/SheetTrigger.vue", type: "registry:ui", target: "" - },{ - path: "registry/new-york-v4/ui/sheet/index.ts", - type: "registry:ui", - target: "" }], categories: undefined, meta: undefined, @@ -1719,6 +1780,10 @@ export const Index: Record = { type: "registry:ui", registryDependencies: ["sheet","input","tooltip","skeleton","separator","button"], files: [{ + path: "registry/new-york-v4/ui/sidebar/index.ts", + type: "registry:ui", + target: "" + },{ path: "registry/new-york-v4/ui/sidebar/Sidebar.vue", type: "registry:ui", target: "" @@ -1814,10 +1879,6 @@ export const Index: Record = { path: "registry/new-york-v4/ui/sidebar/SidebarTrigger.vue", type: "registry:ui", target: "" - },{ - path: "registry/new-york-v4/ui/sidebar/index.ts", - type: "registry:ui", - target: "" },{ path: "registry/new-york-v4/ui/sidebar/utils.ts", type: "registry:ui", @@ -1832,11 +1893,11 @@ export const Index: Record = { type: "registry:ui", registryDependencies: undefined, files: [{ - path: "registry/new-york-v4/ui/skeleton/Skeleton.vue", + path: "registry/new-york-v4/ui/skeleton/index.ts", type: "registry:ui", target: "" },{ - path: "registry/new-york-v4/ui/skeleton/index.ts", + path: "registry/new-york-v4/ui/skeleton/Skeleton.vue", type: "registry:ui", target: "" }], @@ -1849,11 +1910,11 @@ export const Index: Record = { type: "registry:ui", registryDependencies: undefined, files: [{ - path: "registry/new-york-v4/ui/slider/Slider.vue", + path: "registry/new-york-v4/ui/slider/index.ts", type: "registry:ui", target: "" },{ - path: "registry/new-york-v4/ui/slider/index.ts", + path: "registry/new-york-v4/ui/slider/Slider.vue", type: "registry:ui", target: "" }], @@ -1866,11 +1927,11 @@ export const Index: Record = { type: "registry:ui", registryDependencies: undefined, files: [{ - path: "registry/new-york-v4/ui/sonner/Sonner.vue", + path: "registry/new-york-v4/ui/sonner/index.ts", type: "registry:ui", target: "" },{ - path: "registry/new-york-v4/ui/sonner/index.ts", + path: "registry/new-york-v4/ui/sonner/Sonner.vue", type: "registry:ui", target: "" }], @@ -1883,11 +1944,11 @@ export const Index: Record = { type: "registry:ui", registryDependencies: undefined, files: [{ - path: "registry/new-york-v4/ui/spinner/Spinner.vue", + path: "registry/new-york-v4/ui/spinner/index.ts", type: "registry:ui", target: "" },{ - path: "registry/new-york-v4/ui/spinner/index.ts", + path: "registry/new-york-v4/ui/spinner/Spinner.vue", type: "registry:ui", target: "" }], @@ -1900,6 +1961,10 @@ export const Index: Record = { type: "registry:ui", registryDependencies: undefined, files: [{ + path: "registry/new-york-v4/ui/stepper/index.ts", + type: "registry:ui", + target: "" + },{ path: "registry/new-york-v4/ui/stepper/Stepper.vue", type: "registry:ui", target: "" @@ -1927,10 +1992,6 @@ export const Index: Record = { path: "registry/new-york-v4/ui/stepper/StepperTrigger.vue", type: "registry:ui", target: "" - },{ - path: "registry/new-york-v4/ui/stepper/index.ts", - type: "registry:ui", - target: "" }], categories: undefined, meta: undefined, @@ -1941,11 +2002,11 @@ export const Index: Record = { type: "registry:ui", registryDependencies: undefined, files: [{ - path: "registry/new-york-v4/ui/switch/Switch.vue", + path: "registry/new-york-v4/ui/switch/index.ts", type: "registry:ui", target: "" },{ - path: "registry/new-york-v4/ui/switch/index.ts", + path: "registry/new-york-v4/ui/switch/Switch.vue", type: "registry:ui", target: "" }], @@ -1958,6 +2019,10 @@ export const Index: Record = { type: "registry:ui", registryDependencies: undefined, files: [{ + path: "registry/new-york-v4/ui/table/index.ts", + type: "registry:ui", + target: "" + },{ path: "registry/new-york-v4/ui/table/Table.vue", type: "registry:ui", target: "" @@ -1993,10 +2058,6 @@ export const Index: Record = { path: "registry/new-york-v4/ui/table/TableRow.vue", type: "registry:ui", target: "" - },{ - path: "registry/new-york-v4/ui/table/index.ts", - type: "registry:ui", - target: "" },{ path: "registry/new-york-v4/ui/table/utils.ts", type: "registry:ui", @@ -2011,6 +2072,10 @@ export const Index: Record = { type: "registry:ui", registryDependencies: undefined, files: [{ + path: "registry/new-york-v4/ui/tabs/index.ts", + type: "registry:ui", + target: "" + },{ path: "registry/new-york-v4/ui/tabs/Tabs.vue", type: "registry:ui", target: "" @@ -2026,10 +2091,6 @@ export const Index: Record = { path: "registry/new-york-v4/ui/tabs/TabsTrigger.vue", type: "registry:ui", target: "" - },{ - path: "registry/new-york-v4/ui/tabs/index.ts", - type: "registry:ui", - target: "" }], categories: undefined, meta: undefined, @@ -2040,6 +2101,10 @@ export const Index: Record = { type: "registry:ui", registryDependencies: undefined, files: [{ + path: "registry/new-york-v4/ui/tags-input/index.ts", + type: "registry:ui", + target: "" + },{ path: "registry/new-york-v4/ui/tags-input/TagsInput.vue", type: "registry:ui", target: "" @@ -2059,10 +2124,6 @@ export const Index: Record = { path: "registry/new-york-v4/ui/tags-input/TagsInputItemText.vue", type: "registry:ui", target: "" - },{ - path: "registry/new-york-v4/ui/tags-input/index.ts", - type: "registry:ui", - target: "" }], categories: undefined, meta: undefined, @@ -2073,11 +2134,11 @@ export const Index: Record = { type: "registry:ui", registryDependencies: undefined, files: [{ - path: "registry/new-york-v4/ui/textarea/Textarea.vue", + path: "registry/new-york-v4/ui/textarea/index.ts", type: "registry:ui", target: "" },{ - path: "registry/new-york-v4/ui/textarea/index.ts", + path: "registry/new-york-v4/ui/textarea/Textarea.vue", type: "registry:ui", target: "" }], @@ -2090,11 +2151,11 @@ export const Index: Record = { type: "registry:ui", registryDependencies: undefined, files: [{ - path: "registry/new-york-v4/ui/toggle/Toggle.vue", + path: "registry/new-york-v4/ui/toggle/index.ts", type: "registry:ui", target: "" },{ - path: "registry/new-york-v4/ui/toggle/index.ts", + path: "registry/new-york-v4/ui/toggle/Toggle.vue", type: "registry:ui", target: "" }], @@ -2107,15 +2168,15 @@ export const Index: Record = { type: "registry:ui", registryDependencies: ["toggle"], files: [{ - path: "registry/new-york-v4/ui/toggle-group/ToggleGroup.vue", + path: "registry/new-york-v4/ui/toggle-group/index.ts", type: "registry:ui", target: "" },{ - path: "registry/new-york-v4/ui/toggle-group/ToggleGroupItem.vue", + path: "registry/new-york-v4/ui/toggle-group/ToggleGroup.vue", type: "registry:ui", target: "" },{ - path: "registry/new-york-v4/ui/toggle-group/index.ts", + path: "registry/new-york-v4/ui/toggle-group/ToggleGroupItem.vue", type: "registry:ui", target: "" }], @@ -2128,6 +2189,10 @@ export const Index: Record = { type: "registry:ui", registryDependencies: undefined, files: [{ + path: "registry/new-york-v4/ui/tooltip/index.ts", + type: "registry:ui", + target: "" + },{ path: "registry/new-york-v4/ui/tooltip/Tooltip.vue", type: "registry:ui", target: "" @@ -2143,10 +2208,6 @@ export const Index: Record = { path: "registry/new-york-v4/ui/tooltip/TooltipTrigger.vue", type: "registry:ui", target: "" - },{ - path: "registry/new-york-v4/ui/tooltip/index.ts", - type: "registry:ui", - target: "" }], categories: undefined, meta: undefined, @@ -2173,11 +2234,11 @@ export const Index: Record = { type: "registry:component", target: "" },{ - path: "registry/new-york-v4/blocks/dashboard-01/components/DragHandle.vue", + path: "registry/new-york-v4/blocks/dashboard-01/components/DraggableRow.vue", type: "registry:component", target: "" },{ - path: "registry/new-york-v4/blocks/dashboard-01/components/DraggableRow.vue", + path: "registry/new-york-v4/blocks/dashboard-01/components/DragHandle.vue", type: "registry:component", target: "" },{ diff --git a/apps/v4/registry/new-york-v4/ui/_registry.ts b/apps/v4/registry/new-york-v4/ui/_registry.ts index 141ada99a..45190e5da 100644 --- a/apps/v4/registry/new-york-v4/ui/_registry.ts +++ b/apps/v4/registry/new-york-v4/ui/_registry.ts @@ -843,6 +843,70 @@ export const ui: Registry["items"] = [ }, ], }, + { + name: "dropzone", + type: "registry:ui", + registryDependencies: [ + "button", + ], + dependencies: [ + "vue3-dropzone", + ], + files: [ + { + path: "ui/dropzone/Dropzone.vue", + type: "registry:ui", + }, + { + path: "ui/dropzone/DropzoneArea.vue", + type: "registry:ui", + }, + { + path: "ui/dropzone/DropzoneDescription.vue", + type: "registry:ui", + }, + { + path: "ui/dropzone/DropzoneFileList.vue", + type: "registry:ui", + }, + { + path: "ui/dropzone/DropzoneFileListItem.vue", + type: "registry:ui", + }, + { + path: "ui/dropzone/DropzoneFileMessage.vue", + type: "registry:ui", + }, + { + path: "ui/dropzone/DropzoneMessage.vue", + type: "registry:ui", + }, + { + path: "ui/dropzone/DropzoneRemoveFile.vue", + type: "registry:ui", + }, + { + path: "ui/dropzone/DropzoneRetryFile.vue", + type: "registry:ui", + }, + { + path: "ui/dropzone/DropzoneTrigger.vue", + type: "registry:ui", + }, + { + path: "ui/dropzone/index.ts", + type: "registry:ui", + }, + { + path: "ui/dropzone/InfiniteProgress.vue", + type: "registry:ui", + }, + { + path: "ui/dropzone/useDropzoneUpload.ts", + type: "registry:ui", + }, + ], + }, { name: "empty", type: "registry:ui", diff --git a/apps/v4/registry/new-york-v4/ui/dropzone/Dropzone.vue b/apps/v4/registry/new-york-v4/ui/dropzone/Dropzone.vue new file mode 100644 index 000000000..5fb6db5ed --- /dev/null +++ b/apps/v4/registry/new-york-v4/ui/dropzone/Dropzone.vue @@ -0,0 +1,13 @@ + + + diff --git a/apps/v4/registry/new-york-v4/ui/dropzone/DropzoneArea.vue b/apps/v4/registry/new-york-v4/ui/dropzone/DropzoneArea.vue new file mode 100644 index 000000000..1b80eedb1 --- /dev/null +++ b/apps/v4/registry/new-york-v4/ui/dropzone/DropzoneArea.vue @@ -0,0 +1,32 @@ + + + diff --git a/apps/v4/registry/new-york-v4/ui/dropzone/DropzoneDescription.vue b/apps/v4/registry/new-york-v4/ui/dropzone/DropzoneDescription.vue new file mode 100644 index 000000000..2f5dbfa20 --- /dev/null +++ b/apps/v4/registry/new-york-v4/ui/dropzone/DropzoneDescription.vue @@ -0,0 +1,24 @@ + + + diff --git a/apps/v4/registry/new-york-v4/ui/dropzone/DropzoneFileList.vue b/apps/v4/registry/new-york-v4/ui/dropzone/DropzoneFileList.vue new file mode 100644 index 000000000..9b9785284 --- /dev/null +++ b/apps/v4/registry/new-york-v4/ui/dropzone/DropzoneFileList.vue @@ -0,0 +1,24 @@ + + + diff --git a/apps/v4/registry/new-york-v4/ui/dropzone/DropzoneFileListItem.vue b/apps/v4/registry/new-york-v4/ui/dropzone/DropzoneFileListItem.vue new file mode 100644 index 000000000..41ddad0da --- /dev/null +++ b/apps/v4/registry/new-york-v4/ui/dropzone/DropzoneFileListItem.vue @@ -0,0 +1,52 @@ + + + diff --git a/apps/v4/registry/new-york-v4/ui/dropzone/DropzoneFileMessage.vue b/apps/v4/registry/new-york-v4/ui/dropzone/DropzoneFileMessage.vue new file mode 100644 index 000000000..689a61702 --- /dev/null +++ b/apps/v4/registry/new-york-v4/ui/dropzone/DropzoneFileMessage.vue @@ -0,0 +1,27 @@ + + + diff --git a/apps/v4/registry/new-york-v4/ui/dropzone/DropzoneMessage.vue b/apps/v4/registry/new-york-v4/ui/dropzone/DropzoneMessage.vue new file mode 100644 index 000000000..0fbf7e5d9 --- /dev/null +++ b/apps/v4/registry/new-york-v4/ui/dropzone/DropzoneMessage.vue @@ -0,0 +1,29 @@ + + + diff --git a/apps/v4/registry/new-york-v4/ui/dropzone/DropzoneRemoveFile.vue b/apps/v4/registry/new-york-v4/ui/dropzone/DropzoneRemoveFile.vue new file mode 100644 index 000000000..44debc8e3 --- /dev/null +++ b/apps/v4/registry/new-york-v4/ui/dropzone/DropzoneRemoveFile.vue @@ -0,0 +1,34 @@ + + + diff --git a/apps/v4/registry/new-york-v4/ui/dropzone/DropzoneRetryFile.vue b/apps/v4/registry/new-york-v4/ui/dropzone/DropzoneRetryFile.vue new file mode 100644 index 000000000..5cf2c9ab6 --- /dev/null +++ b/apps/v4/registry/new-york-v4/ui/dropzone/DropzoneRetryFile.vue @@ -0,0 +1,37 @@ + + + diff --git a/apps/v4/registry/new-york-v4/ui/dropzone/DropzoneTrigger.vue b/apps/v4/registry/new-york-v4/ui/dropzone/DropzoneTrigger.vue new file mode 100644 index 000000000..7f4afd5e1 --- /dev/null +++ b/apps/v4/registry/new-york-v4/ui/dropzone/DropzoneTrigger.vue @@ -0,0 +1,60 @@ + + + diff --git a/apps/v4/registry/new-york-v4/ui/dropzone/InfiniteProgress.vue b/apps/v4/registry/new-york-v4/ui/dropzone/InfiniteProgress.vue new file mode 100644 index 000000000..e96d36ebb --- /dev/null +++ b/apps/v4/registry/new-york-v4/ui/dropzone/InfiniteProgress.vue @@ -0,0 +1,54 @@ + + + + + diff --git a/apps/v4/registry/new-york-v4/ui/dropzone/index.ts b/apps/v4/registry/new-york-v4/ui/dropzone/index.ts new file mode 100644 index 000000000..a63bb7d85 --- /dev/null +++ b/apps/v4/registry/new-york-v4/ui/dropzone/index.ts @@ -0,0 +1,19 @@ +export { default as Dropzone } from "./Dropzone.vue" +export { default as DropzoneArea } from "./DropzoneArea.vue" +export { default as DropzoneDescription } from "./DropzoneDescription.vue" +export { default as DropzoneFileList } from "./DropzoneFileList.vue" +export { default as DropzoneFileListItem } from "./DropzoneFileListItem.vue" +export { default as DropzoneFileMessage } from "./DropzoneFileMessage.vue" +export { default as DropzoneMessage } from "./DropzoneMessage.vue" +export { default as DropzoneRemoveFile } from "./DropzoneRemoveFile.vue" +export { default as DropzoneRetryFile } from "./DropzoneRetryFile.vue" +export { default as DropzoneTrigger } from "./DropzoneTrigger.vue" +export { default as InfiniteProgress } from "./InfiniteProgress.vue" +export { + type DropZoneErrorCode, + type DropzoneResult, + type FileStatus, + useDropzoneUpload, + type UseDropzoneUploadOptions, + type UseDropzoneUploadReturn, +} from "./useDropzoneUpload" diff --git a/apps/v4/registry/new-york-v4/ui/dropzone/useDropzoneUpload.ts b/apps/v4/registry/new-york-v4/ui/dropzone/useDropzoneUpload.ts new file mode 100644 index 000000000..019ab49d4 --- /dev/null +++ b/apps/v4/registry/new-york-v4/ui/dropzone/useDropzoneUpload.ts @@ -0,0 +1,387 @@ +import type { InjectionKey, Ref } from "vue" +import type { FileRejectReason, InputFile } from "vue3-dropzone" +import { computed, ref } from "vue" +import { useDropzone as useVue3Dropzone } from "vue3-dropzone" + +// Helper function for cross-environment UUID generation +function getUniqueId(): string { + try { + return crypto.randomUUID().slice(0, 8) + } + catch { + // Fallback for older browsers + return Math.random().toString(36).substring(2, 10) + } +} + +export type DropzoneResult + = | { status: "pending" } + | { status: "error", error: TUploadError } + | { status: "success", result: TUploadRes } + +export interface FileStatus { + id: string + fileName: string + file: File + tries: number + status: "pending" | "error" | "success" + result?: TUploadRes + error?: TUploadError + shapedError?: string +} + +export type DropZoneErrorCode + = | "file-invalid-type" + | "file-too-large" + | "file-too-small" + | "too-many-files" + +const dropZoneErrorCodes: readonly DropZoneErrorCode[] = [ + "file-invalid-type", + "file-too-large", + "file-too-small", + "too-many-files", +] as const + +function getDropZoneErrorCodes(fileRejections: FileRejectReason[]): DropZoneErrorCode[] { + const errors = fileRejections.flatMap(rejection => + rejection.errors + .filter((error): error is { code: string, message: string } => + error !== null && typeof error === "object" && "code" in error + && dropZoneErrorCodes.includes(error.code as DropZoneErrorCode), + ) + .map(error => error.code as DropZoneErrorCode), + ) + return Array.from(new Set(errors)) +} + +function getRootError( + errorCodes: DropZoneErrorCode[], + limits: { + accept?: string | string[] + maxSize?: number + minSize?: number + maxFiles?: number + }, +): string { + const errors = errorCodes.map((error) => { + switch (error) { + case "file-invalid-type": { + const acceptedTypes = Array.isArray(limits.accept) + ? limits.accept.join(", ") + : limits.accept ?? "" + return `only ${acceptedTypes} are allowed` + } + case "file-too-large": { + const maxMb = limits.maxSize + ? (limits.maxSize / (1024 * 1024)).toFixed(2) + : "unlimited" + return `max size is ${maxMb}MB` + } + case "file-too-small": { + const roundedMinSize = limits.minSize + ? (limits.minSize / (1024 * 1024)).toFixed(2) + : "not specified" + return `min size is ${roundedMinSize}MB` + } + case "too-many-files": + return `max ${limits.maxFiles} files` + default: + return error + } + }) + const joinedErrors = errors.join(", ") + return joinedErrors.charAt(0).toUpperCase() + joinedErrors.slice(1) +} + +export interface UseDropzoneUploadOptions { + onDropFile: ( + file: File, + ) => Promise, { status: "pending" }>> + onRemoveFile?: (id: string) => void | Promise + onFileUploaded?: (result: TUploadRes) => void + onFileUploadError?: (error: TUploadError) => void + onAllUploaded?: () => void + onRootError?: (error: string | undefined) => void + maxRetryCount?: number + autoRetry?: boolean + validation?: { + accept?: string | string[] + minSize?: number + maxSize?: number + maxFiles?: number + } + shiftOnMaxFiles?: boolean + shapeUploadError?: (error: TUploadError) => string | void +} + +export interface UseDropzoneUploadReturn { + getRootProps: ReturnType["getRootProps"] + getInputProps: ReturnType["getInputProps"] + onRemoveFile: (id: string) => Promise + onRetry: (id: string) => Promise + canRetry: (id: string) => boolean + fileStatuses: Ref[]> + isInvalid: Ref + isDragActive: Ref + rootError: Ref + inputId: string + rootMessageId: string + rootDescriptionId: string + getFileMessageId: (id: string) => string +} + +// Injection key for dropzone context +export const DropzoneContextKey: InjectionKey> = Symbol("dropzone-context") + +// Injection key for file list item context +export interface DropzoneFileListItemContext { + onRemoveFile: () => Promise + onRetry: () => Promise + fileStatus: Ref> + canRetry: Ref + dropzoneId: string + messageId: string +} + +export const DropzoneFileListItemContextKey: InjectionKey> = Symbol("dropzone-file-list-item-context") + +export function useDropzoneUpload( + options: UseDropzoneUploadOptions, +): UseDropzoneUploadReturn { + const { + onDropFile: pOnDropFile, + onRemoveFile: pOnRemoveFile, + shapeUploadError: pShapeUploadError, + onFileUploaded: pOnFileUploaded, + onFileUploadError: pOnFileUploadError, + onAllUploaded: pOnAllUploaded, + onRootError: pOnRootError, + maxRetryCount, + autoRetry, + validation, + shiftOnMaxFiles, + } = options + + // Generate unique IDs + const inputId = `dropzone-${getUniqueId()}` + const rootMessageId = `${inputId}-root-message` + const rootDescriptionId = `${inputId}-description` + + const rootError = ref(undefined) + const fileStatuses = ref[]>([]) + + const setRootError = (error: string | undefined) => { + rootError.value = error + if (pOnRootError !== undefined) { + pOnRootError(error) + } + } + + const isInvalid = computed(() => { + return ( + fileStatuses.value.filter(file => file.status === "error").length > 0 + || rootError.value !== undefined + ) + }) + + const uploadFile = async (file: File, id: string, tries = 0) => { + let result: Exclude, { status: "pending" }> + + try { + result = await pOnDropFile(file) + } + catch (error) { + // Treat thrown exceptions as errors + result = { status: "error" as const, error: error as TUploadError } + } + + if (result.status === "error") { + const effectiveMax = maxRetryCount ?? 3 + if (autoRetry === true && tries < effectiveMax) { + // Update status to pending for retry + const index = fileStatuses.value.findIndex(f => f.id === id) + const currentFile = fileStatuses.value[index] + if (index !== -1 && currentFile) { + fileStatuses.value = [ + ...fileStatuses.value.slice(0, index), + { ...currentFile, status: "pending" as const, tries: currentFile.tries + 1 }, + ...fileStatuses.value.slice(index + 1), + ] as FileStatus[] + } + return uploadFile(file, id, tries + 1) + } + + // Update status to error + const index = fileStatuses.value.findIndex(f => f.id === id) + const currentFile = fileStatuses.value[index] + if (index !== -1 && currentFile) { + const shapedError = pShapeUploadError !== undefined + ? pShapeUploadError(result.error) + : undefined + fileStatuses.value = [ + ...fileStatuses.value.slice(0, index), + { ...currentFile, status: "error" as const, error: result.error, shapedError }, + ...fileStatuses.value.slice(index + 1), + ] as FileStatus[] + + if (pOnFileUploadError !== undefined) { + pOnFileUploadError(result.error) + } + } + return + } + + // Update status to success + const index = fileStatuses.value.findIndex(f => f.id === id) + const currentFile = fileStatuses.value[index] + if (index !== -1 && currentFile) { + fileStatuses.value = [ + ...fileStatuses.value.slice(0, index), + { ...currentFile, status: "success" as const, result: result.result }, + ...fileStatuses.value.slice(index + 1), + ] as FileStatus[] + + if (pOnFileUploaded !== undefined) { + pOnFileUploaded(result.result) + } + } + } + + const onRemoveFile = async (id: string) => { + await pOnRemoveFile?.(id) + fileStatuses.value = fileStatuses.value.filter(f => f.id !== id) + } + + const canRetry = (id: string): boolean => { + const fileStatus = fileStatuses.value.find(file => file.id === id) + const effectiveMax = maxRetryCount ?? 3 + return ( + fileStatus?.status === "error" + && fileStatus.tries <= effectiveMax + ) + } + + const onRetry = async (id: string) => { + if (!canRetry(id)) { + return + } + const fileStatus = fileStatuses.value.find(file => file.id === id) + if (!fileStatus || fileStatus.status !== "error") { + return + } + + // Update status to pending + const index = fileStatuses.value.findIndex(f => f.id === id) + const currentFile = fileStatuses.value[index] + if (index !== -1 && currentFile) { + fileStatuses.value = [ + ...fileStatuses.value.slice(0, index), + { ...currentFile, status: "pending" as const, tries: currentFile.tries + 1 }, + ...fileStatuses.value.slice(index + 1), + ] as FileStatus[] + } + + await uploadFile(fileStatus.file, id) + } + + const getFileMessageId = (id: string) => `${inputId}-${id}-message` + + const onDropAccepted = async (acceptedFiles: InputFile[]) => { + setRootError(undefined) + + const newFiles = acceptedFiles.filter((f): f is File => f instanceof File) + const fileCount = fileStatuses.value.length + const maxNewFiles + = validation?.maxFiles === undefined + ? Infinity + : validation.maxFiles - fileCount + + if (maxNewFiles < newFiles.length) { + if (!shiftOnMaxFiles) { + setRootError(getRootError(["too-many-files"], validation ?? {})) + } + } + + // When shiftOnMaxFiles is true, clamp to maxFiles capacity upfront + let slicedNewFiles + = shiftOnMaxFiles === true && validation?.maxFiles !== undefined + ? newFiles.slice(0, validation.maxFiles) + : (shiftOnMaxFiles === true ? newFiles : newFiles.slice(0, maxNewFiles)) + + if (shiftOnMaxFiles === true && validation?.maxFiles !== undefined) { + // Calculate how many files need to be removed + const removalsNeeded = Math.max(0, fileStatuses.value.length + slicedNewFiles.length - validation.maxFiles) + + // Remove oldest files sequentially + for (let i = 0; i < removalsNeeded; i++) { + const oldestFile = fileStatuses.value[0] + if (oldestFile) { + await onRemoveFile(oldestFile.id) + } + } + + // Recalculate remaining capacity after removals + const remainingCapacity = Math.max(0, validation.maxFiles - fileStatuses.value.length) + slicedNewFiles = newFiles.slice(0, remainingCapacity) + } + + // Process files sequentially to avoid race conditions + const batchIds: string[] = [] + for (const file of slicedNewFiles) { + const id = `file-${getUniqueId()}` + batchIds.push(id) + const newFileStatus: FileStatus = { + id, + fileName: file.name, + file, + status: "pending", + tries: 1, + } + fileStatuses.value = [...fileStatuses.value, newFileStatus] as FileStatus[] + await uploadFile(file, id) + } + + // Only call pOnAllUploaded if batch had files and all succeeded + if (pOnAllUploaded !== undefined && batchIds.length > 0) { + const allSuccessful = batchIds.every(id => + fileStatuses.value.find(f => f.id === id)?.status === "success", + ) + if (allSuccessful) { + pOnAllUploaded() + } + } + } + + const onDropRejected = (fileRejections: FileRejectReason[]) => { + const errorMessage = getRootError( + getDropZoneErrorCodes(fileRejections), + validation ?? {}, + ) + setRootError(errorMessage) + } + + const dropzone = useVue3Dropzone({ + accept: validation?.accept, + minSize: validation?.minSize ?? 0, + maxSize: validation?.maxSize ?? Infinity, + onDropAccepted, + onDropRejected, + }) + + return { + getRootProps: dropzone.getRootProps, + getInputProps: dropzone.getInputProps, + inputId, + rootMessageId, + rootDescriptionId, + getFileMessageId, + onRemoveFile, + onRetry, + canRetry, + fileStatuses: fileStatuses as Ref[]>, + isInvalid, + rootError, + isDragActive: dropzone.isDragActive, + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index da727303f..270a5b6ba 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -280,6 +280,9 @@ importers: vue-sonner: specifier: 'catalog:' version: 2.0.9(@nuxt/kit@4.2.1(magicast@0.5.1))(@nuxt/schema@4.2.1)(nuxt@4.2.1(@parcel/watcher@2.5.1)(@types/node@24.10.0)(@vue/compiler-sfc@3.5.25)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.1(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.32.0)(magicast@0.5.1)(optionator@0.9.4)(rolldown@1.0.0-beta.53)(rollup@4.52.5)(stylus@0.57.0)(terser@5.44.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.2.2(@types/node@24.10.0)(jiti@2.6.1)(lightningcss@1.32.0)(stylus@0.57.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.2))(vue-tsc@3.1.8(typescript@5.9.3))(yaml@2.8.2)) + vue3-dropzone: + specifier: ^2.2.1 + version: 2.2.1(vue@3.5.25(typescript@5.9.3)) zod: specifier: 'catalog:' version: 3.25.76 @@ -4449,6 +4452,10 @@ packages: engines: {node: '>= 4.5.0'} hasBin: true + attr-accept@2.2.5: + resolution: {integrity: sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==} + engines: {node: '>=4'} + autoprefixer@10.4.21: resolution: {integrity: sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==} engines: {node: ^10 || ^12 || >=14} @@ -5883,6 +5890,10 @@ packages: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} + file-selector@0.2.4: + resolution: {integrity: sha512-ZDsQNbrv6qRi1YTDOEWzf5J2KjZ9KMI1Q2SGeTkCJmNNW25Jg4TW4UMcmoqcg4WrAyKRcpBXdbWRxkfrOzVRbA==} + engines: {node: '>= 10'} + file-uri-to-path@1.0.0: resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} @@ -9469,6 +9480,11 @@ packages: peerDependencies: typescript: '>=5.0.0' + vue3-dropzone@2.2.1: + resolution: {integrity: sha512-TWV/BWTMHePoAcHVn+S5a+a69S1Hwkpdn1LlcBkzvesGZTBqL0TDnKuXWMrF+aWlPLVBUfRJO0uIy9+n2jkDxA==} + peerDependencies: + vue: '>=3' + vue@3.5.24: resolution: {integrity: sha512-uTHDOpVQTMjcGgrqFPSb8iO2m1DUvo+WbGqoXQz8Y1CeBYQ0FXf2z1gLRaBtHjlRz7zZUBHxjVB5VTLzYkvftg==} peerDependencies: @@ -14117,6 +14133,8 @@ snapshots: atob@2.1.2: {} + attr-accept@2.2.5: {} + autoprefixer@10.4.21(postcss@8.5.6): dependencies: browserslist: 4.27.0 @@ -15682,6 +15700,10 @@ snapshots: dependencies: flat-cache: 4.0.1 + file-selector@0.2.4: + dependencies: + tslib: 2.8.1 + file-uri-to-path@1.0.0: {} fill-range@7.1.1: @@ -20397,6 +20419,12 @@ snapshots: '@vue/language-core': 3.1.8(typescript@5.9.3) typescript: 5.9.3 + vue3-dropzone@2.2.1(vue@3.5.25(typescript@5.9.3)): + dependencies: + attr-accept: 2.2.5 + file-selector: 0.2.4 + vue: 3.5.25(typescript@5.9.3) + vue@3.5.24(typescript@5.9.3): dependencies: '@vue/compiler-dom': 3.5.24