@brumaombra/ui-vintage 0.9.0 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (337) hide show
  1. package/AGENTS.md +1 -1
  2. package/COMPONENTS.md +1 -1
  3. package/README.md +3 -3
  4. package/module.mjs +43 -43
  5. package/package.json +175 -175
  6. package/src/components/background-grid/BackgroundGrid.vue +4 -4
  7. package/src/components/blog/AllPostsSection.vue +33 -33
  8. package/src/components/blog/BlogContentRenderer.vue +111 -111
  9. package/src/components/blog/BlogFAQSection.vue +20 -20
  10. package/src/components/blog/BlogHeaderSection.vue +81 -81
  11. package/src/components/blog/BlogInfoSection.vue +50 -48
  12. package/src/components/blog/BlogPostHeader.vue +135 -135
  13. package/src/components/blog/BlogProgressBar.vue +33 -33
  14. package/src/components/blog/BlogSectionTitle.vue +22 -22
  15. package/src/components/blog/BlogTagsSection.vue +25 -25
  16. package/src/components/blog/CategoriesList.vue +14 -14
  17. package/src/components/blog/CategoriesSection.vue +11 -11
  18. package/src/components/blog/CategoryCard.vue +27 -26
  19. package/src/components/blog/HeaderCarousel.vue +99 -98
  20. package/src/components/blog/PostCard.vue +34 -34
  21. package/src/components/blog/PostsList.vue +15 -15
  22. package/src/components/blog/SocialShareSidebar.vue +92 -85
  23. package/src/components/blog/TableOfContents.vue +310 -308
  24. package/src/components/busy-indicator/BusyIndicator.vue +7 -7
  25. package/src/components/busy-indicator/busy-indicator-state.ts +56 -56
  26. package/src/components/card-grid/CardGrid.vue +35 -35
  27. package/src/components/chip/Chip.vue +16 -16
  28. package/src/components/confirm-dialog/ConfirmDialog.vue +51 -51
  29. package/src/components/confirm-dialog/confirm-dialog-state.ts +131 -131
  30. package/src/components/content/BlogList.vue +32 -31
  31. package/src/components/content/BlogTable.vue +36 -36
  32. package/src/components/content/CodeBlock.vue +139 -137
  33. package/src/components/content/Flow.vue +60 -60
  34. package/src/components/content/ProseHr.vue +16 -16
  35. package/src/components/content/ProsePre.vue +12 -12
  36. package/src/components/content/Terminal.vue +62 -60
  37. package/src/components/cookie-consent/CookieConsent.vue +36 -36
  38. package/src/components/cookie-consent/useCookieConsent.ts +37 -37
  39. package/src/components/dashboard-shell/DashboardShell.vue +171 -171
  40. package/src/components/data-list/DataList.vue +6 -6
  41. package/src/components/data-list/DataListItem.vue +4 -4
  42. package/src/components/data-list/DataListLabel.vue +4 -4
  43. package/src/components/data-list/DataListText.vue +4 -4
  44. package/src/components/data-list/DataListValue.vue +4 -4
  45. package/src/components/empty-state-card/EmptyStateCard.vue +41 -41
  46. package/src/components/error-page/ErrorPage.vue +73 -73
  47. package/src/components/info-card/InfoCard.vue +31 -31
  48. package/src/components/landing/landing-content/LandingContent.vue +4 -4
  49. package/src/components/landing/landing-footer/LandingFooter.vue +129 -129
  50. package/src/components/landing/landing-navbar/LandingNavbar.vue +50 -50
  51. package/src/components/landing/landing-shell/LandingShell.vue +36 -36
  52. package/src/components/language-flag/LanguageFlag.vue +25 -25
  53. package/src/components/language-flag/flags/ChineseFlag.vue +8 -8
  54. package/src/components/language-flag/flags/EnglishFlag.vue +7 -7
  55. package/src/components/language-flag/flags/FrenchFlag.vue +5 -5
  56. package/src/components/language-flag/flags/GermanFlag.vue +5 -5
  57. package/src/components/language-flag/flags/ItalianFlag.vue +7 -7
  58. package/src/components/language-flag/flags/JapaneseFlag.vue +4 -4
  59. package/src/components/language-flag/flags/PortugueseFlag.vue +57 -57
  60. package/src/components/language-flag/flags/RussianFlag.vue +5 -5
  61. package/src/components/language-flag/flags/SpanishFlag.vue +4 -4
  62. package/src/components/language-selector/LanguageSelector.vue +77 -77
  63. package/src/components/load-more-button/LoadMoreButton.vue +19 -18
  64. package/src/components/loading-state-card/LoadingStateCard.vue +29 -29
  65. package/src/components/message-dialog/MessageDialog.vue +62 -60
  66. package/src/components/message-dialog/message-dialog-state.ts +105 -105
  67. package/src/components/message-toast/MessageToast.vue +153 -150
  68. package/src/components/message-toast/message-toast-state.ts +146 -146
  69. package/src/components/page-header/PageHeader.vue +34 -34
  70. package/src/components/progress-component/ProgressComponent.vue +33 -33
  71. package/src/components/single-value-card/SingleValueCard.vue +59 -58
  72. package/src/components/slider-form-component/SliderFormComponent.vue +36 -36
  73. package/src/components/stat-strip/StatStrip.vue +22 -22
  74. package/src/components/switch-form-component/SwitchFormComponent.vue +20 -20
  75. package/src/components/text-link/TextLink.vue +14 -14
  76. package/src/components/theme-selector/ThemeSelector.vue +65 -62
  77. package/src/components/theme-selector/theme-transition.ts +63 -63
  78. package/src/components/ui/accordion/Accordion.vue +39 -39
  79. package/src/components/ui/alert/Alert.vue +5 -5
  80. package/src/components/ui/alert/AlertDescription.vue +4 -4
  81. package/src/components/ui/alert/AlertTitle.vue +4 -4
  82. package/src/components/ui/alert/index.ts +2 -2
  83. package/src/components/ui/alert-dialog/AlertDialog.vue +3 -3
  84. package/src/components/ui/alert-dialog/AlertDialogAction.vue +3 -3
  85. package/src/components/ui/alert-dialog/AlertDialogCancel.vue +3 -3
  86. package/src/components/ui/alert-dialog/AlertDialogContent.vue +7 -7
  87. package/src/components/ui/alert-dialog/AlertDialogDescription.vue +3 -3
  88. package/src/components/ui/alert-dialog/AlertDialogFooter.vue +4 -4
  89. package/src/components/ui/alert-dialog/AlertDialogHeader.vue +4 -4
  90. package/src/components/ui/alert-dialog/AlertDialogTitle.vue +3 -3
  91. package/src/components/ui/alert-dialog/AlertDialogTrigger.vue +3 -3
  92. package/src/components/ui/animated-number/AnimatedNumber.vue +49 -48
  93. package/src/components/ui/avatar/Avatar.vue +28 -28
  94. package/src/components/ui/avatar/AvatarFallback.vue +6 -6
  95. package/src/components/ui/avatar/AvatarGroup.vue +40 -40
  96. package/src/components/ui/avatar/AvatarImage.vue +3 -3
  97. package/src/components/ui/avatar/index.ts +17 -17
  98. package/src/components/ui/badge/Badge.vue +20 -20
  99. package/src/components/ui/breadcrumb/Breadcrumb.vue +4 -4
  100. package/src/components/ui/breadcrumb/BreadcrumbEllipsis.vue +10 -10
  101. package/src/components/ui/breadcrumb/BreadcrumbItem.vue +4 -4
  102. package/src/components/ui/breadcrumb/BreadcrumbLink.vue +4 -4
  103. package/src/components/ui/breadcrumb/BreadcrumbList.vue +4 -4
  104. package/src/components/ui/breadcrumb/BreadcrumbPage.vue +4 -4
  105. package/src/components/ui/breadcrumb/BreadcrumbSeparator.vue +4 -4
  106. package/src/components/ui/busy/Busy.vue +33 -33
  107. package/src/components/ui/button/Button.vue +23 -23
  108. package/src/components/ui/button/index.ts +56 -56
  109. package/src/components/ui/calendar/Calendar.vue +138 -138
  110. package/src/components/ui/calendar/CalendarCell.vue +7 -7
  111. package/src/components/ui/calendar/CalendarCellTrigger.vue +18 -18
  112. package/src/components/ui/calendar/CalendarGrid.vue +3 -3
  113. package/src/components/ui/calendar/CalendarGridBody.vue +3 -3
  114. package/src/components/ui/calendar/CalendarGridHead.vue +3 -3
  115. package/src/components/ui/calendar/CalendarGridRow.vue +3 -3
  116. package/src/components/ui/calendar/CalendarHeadCell.vue +7 -7
  117. package/src/components/ui/calendar/CalendarHeader.vue +7 -7
  118. package/src/components/ui/calendar/CalendarHeading.vue +6 -6
  119. package/src/components/ui/calendar/CalendarNextButton.vue +16 -16
  120. package/src/components/ui/calendar/CalendarPrevButton.vue +16 -16
  121. package/src/components/ui/calendar/index.ts +4 -4
  122. package/src/components/ui/card/Card.vue +20 -20
  123. package/src/components/ui/card/CardAction.vue +8 -8
  124. package/src/components/ui/card/CardContent.vue +4 -4
  125. package/src/components/ui/card/CardDescription.vue +12 -12
  126. package/src/components/ui/card/CardFooter.vue +4 -4
  127. package/src/components/ui/card/CardHeader.vue +8 -8
  128. package/src/components/ui/card/CardText.vue +8 -8
  129. package/src/components/ui/card/CardTitle.vue +8 -8
  130. package/src/components/ui/checkbox/Checkbox.vue +27 -27
  131. package/src/components/ui/collapsible/Collapsible.vue +3 -3
  132. package/src/components/ui/collapsible/CollapsibleContent.vue +3 -3
  133. package/src/components/ui/collapsible/CollapsibleTrigger.vue +3 -3
  134. package/src/components/ui/combobox/Combobox.vue +4 -4
  135. package/src/components/ui/combobox/ComboboxAnchor.vue +16 -16
  136. package/src/components/ui/combobox/ComboboxEmpty.vue +3 -3
  137. package/src/components/ui/combobox/ComboboxGroup.vue +9 -9
  138. package/src/components/ui/combobox/ComboboxInput.vue +6 -6
  139. package/src/components/ui/combobox/ComboboxItem.vue +18 -18
  140. package/src/components/ui/combobox/ComboboxList.vue +19 -19
  141. package/src/components/ui/combobox/ComboboxSelect.vue +79 -78
  142. package/src/components/ui/combobox/ComboboxSeparator.vue +1 -1
  143. package/src/components/ui/combobox/ComboboxTrigger.vue +11 -11
  144. package/src/components/ui/combobox/types.ts +5 -5
  145. package/src/components/ui/command/Command.vue +48 -48
  146. package/src/components/ui/command/CommandDialog.vue +14 -14
  147. package/src/components/ui/command/CommandEmpty.vue +4 -4
  148. package/src/components/ui/command/CommandGroup.vue +23 -23
  149. package/src/components/ui/command/CommandInput.vue +10 -10
  150. package/src/components/ui/command/CommandItem.vue +40 -40
  151. package/src/components/ui/command/CommandList.vue +9 -9
  152. package/src/components/ui/command/CommandSeparator.vue +3 -3
  153. package/src/components/ui/command/CommandShortcut.vue +8 -8
  154. package/src/components/ui/command/index.ts +11 -11
  155. package/src/components/ui/data-table/DataTable.vue +147 -146
  156. package/src/components/ui/data-table/index.ts +11 -11
  157. package/src/components/ui/date-picker/DatePicker.vue +32 -32
  158. package/src/components/ui/date-time-picker/DateTimePicker.vue +97 -97
  159. package/src/components/ui/dialog/Dialog.vue +3 -3
  160. package/src/components/ui/dialog/DialogClose.vue +3 -3
  161. package/src/components/ui/dialog/DialogContent.vue +22 -22
  162. package/src/components/ui/dialog/DialogDescription.vue +7 -7
  163. package/src/components/ui/dialog/DialogFooter.vue +8 -8
  164. package/src/components/ui/dialog/DialogHeader.vue +4 -4
  165. package/src/components/ui/dialog/DialogOverlay.vue +7 -7
  166. package/src/components/ui/dialog/DialogScrollContent.vue +33 -33
  167. package/src/components/ui/dialog/DialogTitle.vue +3 -3
  168. package/src/components/ui/dialog/DialogTrigger.vue +3 -3
  169. package/src/components/ui/dropdown-menu/DropdownMenu.vue +3 -3
  170. package/src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue +12 -12
  171. package/src/components/ui/dropdown-menu/DropdownMenuContent.vue +15 -15
  172. package/src/components/ui/dropdown-menu/DropdownMenuGroup.vue +3 -3
  173. package/src/components/ui/dropdown-menu/DropdownMenuItem.vue +12 -12
  174. package/src/components/ui/dropdown-menu/DropdownMenuLabel.vue +9 -9
  175. package/src/components/ui/dropdown-menu/DropdownMenuPortal.vue +3 -3
  176. package/src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue +3 -3
  177. package/src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue +11 -11
  178. package/src/components/ui/dropdown-menu/DropdownMenuSeparator.vue +1 -1
  179. package/src/components/ui/dropdown-menu/DropdownMenuShortcut.vue +4 -4
  180. package/src/components/ui/dropdown-menu/DropdownMenuSub.vue +3 -3
  181. package/src/components/ui/dropdown-menu/DropdownMenuSubContent.vue +15 -15
  182. package/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +16 -16
  183. package/src/components/ui/dropdown-menu/DropdownMenuTrigger.vue +3 -3
  184. package/src/components/ui/dropdown-menu/classes.ts +16 -16
  185. package/src/components/ui/field/Field.vue +5 -5
  186. package/src/components/ui/field/FieldContent.vue +8 -8
  187. package/src/components/ui/field/FieldDescription.vue +10 -10
  188. package/src/components/ui/field/FieldError.vue +39 -39
  189. package/src/components/ui/field/FieldGroup.vue +8 -8
  190. package/src/components/ui/field/FieldLabel.vue +10 -10
  191. package/src/components/ui/field/FieldLegend.vue +11 -11
  192. package/src/components/ui/field/FieldSeparator.vue +11 -11
  193. package/src/components/ui/field/FieldSet.vue +9 -9
  194. package/src/components/ui/field/FieldTitle.vue +8 -8
  195. package/src/components/ui/field/field-context.ts +36 -36
  196. package/src/components/ui/field/index.ts +22 -22
  197. package/src/components/ui/file-dropzone/FileDropzone.vue +163 -154
  198. package/src/components/ui/file-dropzone/index.ts +19 -19
  199. package/src/components/ui/hover-card/HoverCard.vue +5 -5
  200. package/src/components/ui/hover-card/HoverCardContent.vue +13 -13
  201. package/src/components/ui/hover-card/HoverCardTrigger.vue +3 -3
  202. package/src/components/ui/input/Input.vue +13 -13
  203. package/src/components/ui/kbd/Kbd.vue +10 -10
  204. package/src/components/ui/kbd/KbdGroup.vue +49 -49
  205. package/src/components/ui/label/Label.vue +7 -7
  206. package/src/components/ui/native-select/NativeSelect.vue +17 -17
  207. package/src/components/ui/native-select/NativeSelectOptGroup.vue +3 -3
  208. package/src/components/ui/native-select/NativeSelectOption.vue +3 -3
  209. package/src/components/ui/number-field/NumberField.vue +90 -89
  210. package/src/components/ui/pagination/Pagination.vue +4 -4
  211. package/src/components/ui/pagination/PaginationContent.vue +4 -4
  212. package/src/components/ui/pagination/PaginationEllipsis.vue +10 -10
  213. package/src/components/ui/pagination/PaginationFirst.vue +9 -9
  214. package/src/components/ui/pagination/PaginationItem.vue +14 -14
  215. package/src/components/ui/pagination/PaginationLast.vue +9 -9
  216. package/src/components/ui/pagination/PaginationNext.vue +9 -9
  217. package/src/components/ui/pagination/PaginationPrevious.vue +9 -9
  218. package/src/components/ui/pagination/SimplePagination.vue +25 -25
  219. package/src/components/ui/pin-input/PinInput.vue +20 -20
  220. package/src/components/ui/pin-input/PinInputInput.vue +17 -17
  221. package/src/components/ui/pin-input/context.ts +1 -1
  222. package/src/components/ui/popover/Popover.vue +3 -3
  223. package/src/components/ui/popover/PopoverAnchor.vue +3 -3
  224. package/src/components/ui/popover/PopoverContent.vue +12 -12
  225. package/src/components/ui/popover/PopoverTrigger.vue +3 -3
  226. package/src/components/ui/progress/Progress.vue +16 -16
  227. package/src/components/ui/radio-group/RadioGroup.vue +7 -7
  228. package/src/components/ui/radio-group/RadioGroupCard.vue +39 -39
  229. package/src/components/ui/radio-group/RadioGroupItem.vue +20 -20
  230. package/src/components/ui/scroll-area/ScrollArea.vue +14 -14
  231. package/src/components/ui/scroll-area/ScrollBar.vue +12 -12
  232. package/src/components/ui/select/Select.vue +3 -3
  233. package/src/components/ui/select/SelectContent.vue +25 -25
  234. package/src/components/ui/select/SelectGroup.vue +3 -3
  235. package/src/components/ui/select/SelectItem.vue +25 -25
  236. package/src/components/ui/select/SelectItemContent.vue +32 -32
  237. package/src/components/ui/select/SelectItemText.vue +3 -3
  238. package/src/components/ui/select/SelectLabel.vue +3 -3
  239. package/src/components/ui/select/SelectScrollDownButton.vue +10 -10
  240. package/src/components/ui/select/SelectScrollUpButton.vue +10 -10
  241. package/src/components/ui/select/SelectSeparator.vue +2 -2
  242. package/src/components/ui/select/SelectTrigger.vue +15 -15
  243. package/src/components/ui/select/SelectValue.vue +3 -3
  244. package/src/components/ui/select/SelectValueContent.vue +39 -39
  245. package/src/components/ui/separator/Separator.vue +7 -7
  246. package/src/components/ui/sheet/Sheet.vue +3 -3
  247. package/src/components/ui/sheet/SheetClose.vue +3 -3
  248. package/src/components/ui/sheet/SheetContent.vue +22 -22
  249. package/src/components/ui/sheet/SheetDescription.vue +3 -3
  250. package/src/components/ui/sheet/SheetFooter.vue +3 -3
  251. package/src/components/ui/sheet/SheetHeader.vue +3 -3
  252. package/src/components/ui/sheet/SheetOverlay.vue +7 -7
  253. package/src/components/ui/sheet/SheetTitle.vue +3 -3
  254. package/src/components/ui/sheet/SheetTrigger.vue +3 -3
  255. package/src/components/ui/sidebar/Sidebar.vue +57 -57
  256. package/src/components/ui/sidebar/SidebarContent.vue +8 -8
  257. package/src/components/ui/sidebar/SidebarFooter.vue +8 -8
  258. package/src/components/ui/sidebar/SidebarGroup.vue +8 -8
  259. package/src/components/ui/sidebar/SidebarGroupAction.vue +9 -9
  260. package/src/components/ui/sidebar/SidebarGroupContent.vue +4 -4
  261. package/src/components/ui/sidebar/SidebarGroupLabel.vue +8 -8
  262. package/src/components/ui/sidebar/SidebarHeader.vue +8 -8
  263. package/src/components/ui/sidebar/SidebarInput.vue +8 -8
  264. package/src/components/ui/sidebar/SidebarInset.vue +9 -9
  265. package/src/components/ui/sidebar/SidebarMenu.vue +4 -4
  266. package/src/components/ui/sidebar/SidebarMenuAction.vue +18 -18
  267. package/src/components/ui/sidebar/SidebarMenuBadge.vue +14 -14
  268. package/src/components/ui/sidebar/SidebarMenuButton.vue +21 -21
  269. package/src/components/ui/sidebar/SidebarMenuButtonChild.vue +10 -10
  270. package/src/components/ui/sidebar/SidebarMenuItem.vue +4 -4
  271. package/src/components/ui/sidebar/SidebarMenuSkeleton.vue +10 -10
  272. package/src/components/ui/sidebar/SidebarMenuSub.vue +9 -9
  273. package/src/components/ui/sidebar/SidebarMenuSubButton.vue +17 -17
  274. package/src/components/ui/sidebar/SidebarMenuSubItem.vue +4 -4
  275. package/src/components/ui/sidebar/SidebarProvider.vue +46 -46
  276. package/src/components/ui/sidebar/SidebarRail.vue +13 -13
  277. package/src/components/ui/sidebar/SidebarSeparator.vue +4 -4
  278. package/src/components/ui/sidebar/SidebarTrigger.vue +6 -6
  279. package/src/components/ui/sidebar/index.ts +29 -29
  280. package/src/components/ui/sidebar/utils.ts +7 -7
  281. package/src/components/ui/skeleton/Skeleton.vue +6 -6
  282. package/src/components/ui/slider/Slider.vue +14 -14
  283. package/src/components/ui/spinner/Spinner.vue +21 -21
  284. package/src/components/ui/stepper/SimpleStepper.vue +27 -27
  285. package/src/components/ui/stepper/Stepper.vue +6 -6
  286. package/src/components/ui/stepper/StepperDescription.vue +5 -5
  287. package/src/components/ui/stepper/StepperIndicator.vue +18 -18
  288. package/src/components/ui/stepper/StepperItem.vue +4 -4
  289. package/src/components/ui/stepper/StepperSeparator.vue +7 -7
  290. package/src/components/ui/stepper/StepperTitle.vue +5 -5
  291. package/src/components/ui/stepper/StepperTrigger.vue +4 -4
  292. package/src/components/ui/stepper/index.ts +3 -3
  293. package/src/components/ui/switch/Switch.vue +15 -15
  294. package/src/components/ui/table/Table.vue +6 -6
  295. package/src/components/ui/table/TableBody.vue +4 -4
  296. package/src/components/ui/table/TableCaption.vue +4 -4
  297. package/src/components/ui/table/TableCell.vue +8 -8
  298. package/src/components/ui/table/TableEmpty.vue +14 -14
  299. package/src/components/ui/table/TableFooter.vue +8 -8
  300. package/src/components/ui/table/TableHead.vue +8 -8
  301. package/src/components/ui/table/TableHeader.vue +4 -4
  302. package/src/components/ui/table/TableRow.vue +8 -8
  303. package/src/components/ui/tabs/Tabs.vue +4 -4
  304. package/src/components/ui/tabs/TabsContent.vue +4 -4
  305. package/src/components/ui/tabs/TabsList.vue +15 -15
  306. package/src/components/ui/tabs/TabsTrigger.vue +10 -10
  307. package/src/components/ui/tags-input/TagsInput.vue +17 -17
  308. package/src/components/ui/tags-input/TagsInputInput.vue +6 -6
  309. package/src/components/ui/tags-input/TagsInputItem.vue +15 -15
  310. package/src/components/ui/tags-input/TagsInputItemDelete.vue +13 -13
  311. package/src/components/ui/tags-input/TagsInputItemText.vue +4 -4
  312. package/src/components/ui/textarea/Textarea.vue +12 -12
  313. package/src/components/ui/time-picker/TimePicker.vue +25 -25
  314. package/src/components/ui/toggle-group/Toggle.vue +10 -10
  315. package/src/components/ui/toggle-group/ToggleGroup.vue +38 -38
  316. package/src/components/ui/toggle-group/ToggleGroupItem.vue +9 -9
  317. package/src/components/ui/toggle-group/index.ts +46 -46
  318. package/src/components/ui/tooltip/Tooltip.vue +3 -3
  319. package/src/components/ui/tooltip/TooltipContent.vue +13 -13
  320. package/src/components/ui/tooltip/TooltipProvider.vue +4 -4
  321. package/src/components/ui/tooltip/TooltipTrigger.vue +3 -3
  322. package/src/i18n/de.json +185 -185
  323. package/src/i18n/en.json +185 -185
  324. package/src/i18n/es.json +185 -185
  325. package/src/i18n/fr.json +185 -185
  326. package/src/i18n/it.json +185 -185
  327. package/src/i18n/ja.json +185 -185
  328. package/src/i18n/pt.json +185 -185
  329. package/src/i18n/ru.json +185 -185
  330. package/src/i18n/zh.json +185 -185
  331. package/src/lib/color-tokens.ts +24 -24
  332. package/src/lib/i18n.ts +65 -65
  333. package/src/lib/utils.ts +33 -33
  334. package/src/nuxt-components.d.ts +2 -2
  335. package/src/runtime/ui-vintage-i18n-plugin.ts +49 -49
  336. package/src/shims-vue.d.ts +3 -3
  337. package/src/styles.css +377 -377
@@ -4,17 +4,17 @@ import { cn } from '../../../lib/utils';
4
4
 
5
5
  // Props
6
6
  const props = defineProps<{
7
- class?: HTMLAttributes['class'];
7
+ class?: HTMLAttributes['class'];
8
8
  }>();
9
9
  </script>
10
10
 
11
11
  <template>
12
- <fieldset data-slot="field-set" :class="cn(
13
- 'flex flex-col gap-6',
14
- 'has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3',
15
- props.class
16
- )
17
- ">
18
- <slot />
19
- </fieldset>
12
+ <fieldset data-slot="field-set" :class="cn(
13
+ 'flex flex-col gap-6',
14
+ 'has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3',
15
+ props.class
16
+ )
17
+ ">
18
+ <slot />
19
+ </fieldset>
20
20
  </template>
@@ -4,16 +4,16 @@ import { cn } from '../../../lib/utils';
4
4
 
5
5
  // Props
6
6
  const props = defineProps<{
7
- class?: HTMLAttributes['class'];
7
+ class?: HTMLAttributes['class'];
8
8
  }>();
9
9
  </script>
10
10
 
11
11
  <template>
12
- <div data-slot="field-label" :class="cn(
13
- 'flex w-fit items-center gap-2 text-sm leading-snug font-semibold group-data-[disabled=true]/field:opacity-50',
14
- props.class
15
- )
16
- ">
17
- <slot />
18
- </div>
12
+ <div data-slot="field-label" :class="cn(
13
+ 'flex w-fit items-center gap-2 text-sm leading-snug font-semibold group-data-[disabled=true]/field:opacity-50',
14
+ props.class
15
+ )
16
+ ">
17
+ <slot />
18
+ </div>
19
19
  </template>
@@ -2,61 +2,61 @@ import type { InjectionKey, Ref } from 'vue';
2
2
  import { computed, inject, onBeforeUnmount, provide, ref, useAttrs, useId } from 'vue';
3
3
 
4
4
  interface FieldContext {
5
- // Id of the control the field label points to (undefined until a control registers)
6
- controlId: Ref<string | undefined>;
7
- // Id of the field label, for controls that combine it with their own text
8
- labelId: Ref<string | undefined>;
5
+ // Id of the control the field label points to (undefined until a control registers)
6
+ controlId: Ref<string | undefined>;
7
+ // Id of the field label, for controls that combine it with their own text
8
+ labelId: Ref<string | undefined>;
9
9
  }
10
10
 
11
11
  const fieldContextKey: InjectionKey<FieldContext> = Symbol('uv-field');
12
12
 
13
13
  // Share the label target with the label and the control inside a Field
14
14
  export const provideFieldContext = () => {
15
- const context: FieldContext = { controlId: ref(undefined), labelId: ref(undefined) };
16
- provide(fieldContextKey, context);
17
- return context;
15
+ const context: FieldContext = { controlId: ref(undefined), labelId: ref(undefined) };
16
+ provide(fieldContextKey, context);
17
+ return context;
18
18
  };
19
19
 
20
20
  // Id the FieldLabel should point to, if the field has a control, and the id the label registers for itself
21
21
  export const useFieldLabel = () => {
22
- const field = inject(fieldContextKey, null);
23
- const labelId = `uv-field-label-${useId()}`;
24
- if (field && !field.labelId.value) {
25
- field.labelId.value = labelId;
26
- onBeforeUnmount(() => {
27
- if (field.labelId.value === labelId) field.labelId.value = undefined;
28
- });
29
- }
30
- return {
31
- labelId: field ? labelId : undefined,
32
- target: computed(() => field?.controlId.value)
33
- };
22
+ const field = inject(fieldContextKey, null);
23
+ const labelId = `uv-field-label-${useId()}`;
24
+ if (field && !field.labelId.value) {
25
+ field.labelId.value = labelId;
26
+ onBeforeUnmount(() => {
27
+ if (field.labelId.value === labelId) field.labelId.value = undefined;
28
+ });
29
+ }
30
+ return {
31
+ labelId: field ? labelId : undefined,
32
+ target: computed(() => field?.controlId.value)
33
+ };
34
34
  };
35
35
 
36
36
  // Id of the surrounding Field label, if any
37
37
  export const useFieldLabelId = () => {
38
- const field = inject(fieldContextKey, null);
39
- return computed(() => field?.labelId.value);
38
+ const field = inject(fieldContextKey, null);
39
+ return computed(() => field?.labelId.value);
40
40
  };
41
41
 
42
42
  // Give the first control inside a Field an id its FieldLabel points to (an explicit id on the control wins)
43
43
  export const useFieldControlId = (explicitId?: () => string | undefined) => {
44
- const field = inject(fieldContextKey, null);
45
- const attrs = useAttrs();
46
- const generatedId = `uv-field-control-${useId()}`;
44
+ const field = inject(fieldContextKey, null);
45
+ const attrs = useAttrs();
46
+ const generatedId = `uv-field-control-${useId()}`;
47
47
 
48
- // Only the first control claims the label, so composite controls (like DateTimePicker) don't fight over it
49
- if (!field || field.controlId.value) {
50
- return computed(() => explicitId?.());
51
- }
48
+ // Only the first control claims the label, so composite controls (like DateTimePicker) don't fight over it
49
+ if (!field || field.controlId.value) {
50
+ return computed(() => explicitId?.());
51
+ }
52
52
 
53
- const id = (explicitId?.() ?? (attrs.id as string | undefined)) || generatedId;
54
- field.controlId.value = id;
53
+ const id = (explicitId?.() ?? (attrs.id as string | undefined)) || generatedId;
54
+ field.controlId.value = id;
55
55
 
56
- // Release the label when the control goes away
57
- onBeforeUnmount(() => {
58
- if (field.controlId.value === id) field.controlId.value = undefined;
59
- });
56
+ // Release the label when the control goes away
57
+ onBeforeUnmount(() => {
58
+ if (field.controlId.value === id) field.controlId.value = undefined;
59
+ });
60
60
 
61
- return computed(() => id);
62
- };
61
+ return computed(() => id);
62
+ };
@@ -2,31 +2,31 @@ import type { ClassProp } from 'class-variance-authority/types';
2
2
  import { cva } from 'class-variance-authority';
3
3
 
4
4
  export type FieldVariants = {
5
- orientation?: 'vertical' | 'horizontal' | 'responsive' | null | undefined;
5
+ orientation?: 'vertical' | 'horizontal' | 'responsive' | null | undefined;
6
6
  };
7
7
 
8
8
  export const fieldVariants: (props?: FieldVariants & ClassProp) => string = cva(
9
- 'group/field flex w-full gap-3 data-[invalid=true]:text-destructive',
10
- {
11
- variants: {
12
- orientation: {
13
- vertical: ['flex-col [&>*]:w-full [&>.sr-only]:w-auto'],
14
- horizontal: [
15
- 'flex-row items-center',
16
- '[&>[data-slot=field-label]]:flex-auto',
17
- 'has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px'
18
- ],
19
- responsive: [
20
- 'flex-col [&>*]:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto',
21
- '@md/field-group:[&>[data-slot=field-label]]:flex-auto',
22
- '@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px'
23
- ]
24
- }
25
- },
26
- defaultVariants: {
27
- orientation: 'vertical'
28
- }
29
- }
9
+ 'group/field flex w-full gap-3 data-[invalid=true]:text-destructive',
10
+ {
11
+ variants: {
12
+ orientation: {
13
+ vertical: ['flex-col [&>*]:w-full [&>.sr-only]:w-auto'],
14
+ horizontal: [
15
+ 'flex-row items-center',
16
+ '[&>[data-slot=field-label]]:flex-auto',
17
+ 'has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px'
18
+ ],
19
+ responsive: [
20
+ 'flex-col [&>*]:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto',
21
+ '@md/field-group:[&>[data-slot=field-label]]:flex-auto',
22
+ '@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px'
23
+ ]
24
+ }
25
+ },
26
+ defaultVariants: {
27
+ orientation: 'vertical'
28
+ }
29
+ }
30
30
  );
31
31
 
32
32
  export { useFieldControlId } from './field-context';
@@ -2,7 +2,16 @@
2
2
  import type { HTMLAttributes } from 'vue';
3
3
  import type { HugeiconsIconDefinition } from '../../../lib/common-types';
4
4
  import type { FileRejection } from '.';
5
- import { Cancel01Icon, CloudUploadIcon, Doc01Icon, File01Icon, FileZipIcon, Image01Icon, MusicNote01Icon, Pdf01Icon, Video01Icon, Xls01Icon } from '@hugeicons/core-free-icons';
5
+ import Cancel01Icon from '@hugeicons/core-free-icons/Cancel01Icon';
6
+ import CloudUploadIcon from '@hugeicons/core-free-icons/CloudUploadIcon';
7
+ import Doc01Icon from '@hugeicons/core-free-icons/Doc01Icon';
8
+ import File01Icon from '@hugeicons/core-free-icons/File01Icon';
9
+ import FileZipIcon from '@hugeicons/core-free-icons/FileZipIcon';
10
+ import Image01Icon from '@hugeicons/core-free-icons/Image01Icon';
11
+ import MusicNote01Icon from '@hugeicons/core-free-icons/MusicNote01Icon';
12
+ import Pdf01Icon from '@hugeicons/core-free-icons/Pdf01Icon';
13
+ import Video01Icon from '@hugeicons/core-free-icons/Video01Icon';
14
+ import Xls01Icon from '@hugeicons/core-free-icons/Xls01Icon';
6
15
  import { HugeiconsIcon } from '@hugeicons/vue';
7
16
  import { computed, onBeforeUnmount, ref } from 'vue';
8
17
  import { useI18n } from 'vue-i18n';
@@ -12,22 +21,22 @@ import { formatFileSize, isFileAccepted } from '.';
12
21
 
13
22
  // Props
14
23
  const props = withDefaults(defineProps<{
15
- accept?: string;
16
- multiple?: boolean;
17
- maxSize?: number;
18
- maxFiles?: number;
19
- disabled?: boolean;
20
- title?: string;
21
- description?: string;
22
- class?: HTMLAttributes['class'];
24
+ accept?: string;
25
+ multiple?: boolean;
26
+ maxSize?: number;
27
+ maxFiles?: number;
28
+ disabled?: boolean;
29
+ title?: string;
30
+ description?: string;
31
+ class?: HTMLAttributes['class'];
23
32
  }>(), {
24
- multiple: true,
25
- disabled: false,
33
+ multiple: true,
34
+ disabled: false,
26
35
  });
27
36
 
28
37
  // Emits
29
38
  const emits = defineEmits<{
30
- reject: [rejections: FileRejection[]];
39
+ reject: [rejections: FileRejection[]];
31
40
  }>();
32
41
 
33
42
  // Selected files (v-model)
@@ -47,204 +56,204 @@ const resolvedDescription = computed(() => props.description || t('uiVintage.fil
47
56
 
48
57
  // Constraint hints shown under the description
49
58
  const hints = computed(() => {
50
- const list: string[] = [];
51
- if (props.maxSize) list.push(t('uiVintage.fileDropzone.maxSize', { size: formatFileSize(props.maxSize) }));
52
- if (props.multiple && props.maxFiles) list.push(t('uiVintage.fileDropzone.maxFiles', { count: props.maxFiles }));
53
- return list;
59
+ const list: string[] = [];
60
+ if (props.maxSize) list.push(t('uiVintage.fileDropzone.maxSize', { size: formatFileSize(props.maxSize) }));
61
+ if (props.multiple && props.maxFiles) list.push(t('uiVintage.fileDropzone.maxFiles', { count: props.maxFiles }));
62
+ return list;
54
63
  });
55
64
 
56
65
  // Stable keys for the file list
57
66
  const fileIds = new WeakMap<File, number>();
58
67
  let nextFileId = 0;
59
68
  function getFileId(file: File) {
60
- if (!fileIds.has(file)) fileIds.set(file, nextFileId++);
61
- return fileIds.get(file)!;
69
+ if (!fileIds.has(file)) fileIds.set(file, nextFileId++);
70
+ return fileIds.get(file)!;
62
71
  }
63
72
 
64
73
  // Pick an icon from the file type or extension
65
74
  function getFileIcon(file: File): HugeiconsIconDefinition {
66
- const type = file.type.toLowerCase();
67
- const extension = file.name.split('.').pop()?.toLowerCase() ?? '';
68
- if (type.startsWith('image/')) return Image01Icon;
69
- if (type.startsWith('video/')) return Video01Icon;
70
- if (type.startsWith('audio/')) return MusicNote01Icon;
71
- if (type === 'application/pdf' || extension === 'pdf') return Pdf01Icon;
72
- if (['zip', 'rar', '7z', 'tar', 'gz'].includes(extension)) return FileZipIcon;
73
- if (['doc', 'docx', 'odt', 'rtf', 'txt', 'md'].includes(extension)) return Doc01Icon;
74
- if (['xls', 'xlsx', 'ods', 'csv'].includes(extension)) return Xls01Icon;
75
- return File01Icon;
75
+ const type = file.type.toLowerCase();
76
+ const extension = file.name.split('.').pop()?.toLowerCase() ?? '';
77
+ if (type.startsWith('image/')) return Image01Icon;
78
+ if (type.startsWith('video/')) return Video01Icon;
79
+ if (type.startsWith('audio/')) return MusicNote01Icon;
80
+ if (type === 'application/pdf' || extension === 'pdf') return Pdf01Icon;
81
+ if (['zip', 'rar', '7z', 'tar', 'gz'].includes(extension)) return FileZipIcon;
82
+ if (['doc', 'docx', 'odt', 'rtf', 'txt', 'md'].includes(extension)) return Doc01Icon;
83
+ if (['xls', 'xlsx', 'ods', 'csv'].includes(extension)) return Xls01Icon;
84
+ return File01Icon;
76
85
  }
77
86
 
78
87
  // Briefly shake the drop zone when something is refused
79
88
  function flashRejected() {
80
- isRejected.value = false;
81
- clearTimeout(rejectTimer);
82
- requestAnimationFrame(() => {
83
- isRejected.value = true;
84
- rejectTimer = setTimeout(() => (isRejected.value = false), 450);
85
- });
89
+ isRejected.value = false;
90
+ clearTimeout(rejectTimer);
91
+ requestAnimationFrame(() => {
92
+ isRejected.value = true;
93
+ rejectTimer = setTimeout(() => (isRejected.value = false), 450);
94
+ });
86
95
  }
87
96
 
88
97
  // Validate incoming files and merge them into the model
89
98
  function addFiles(incoming: File[]) {
90
- if (props.disabled || incoming.length === 0) return;
91
- const rejections: FileRejection[] = [];
92
- const accepted: File[] = [];
93
-
94
- // Type and size checks
95
- for (const file of incoming) {
96
- if (!isFileAccepted(file, props.accept)) rejections.push({ file, reason: 'type' });
97
- else if (props.maxSize !== undefined && file.size > props.maxSize) rejections.push({ file, reason: 'size' });
98
- else accepted.push(file);
99
- }
100
-
101
- let next: File[];
102
- if (!props.multiple) {
103
- // Single mode keeps only the first valid file
104
- next = accepted.slice(0, 1);
105
- for (const file of accepted.slice(1)) rejections.push({ file, reason: 'count' });
106
- if (next.length === 0) next = [...files.value];
107
- } else {
108
- // Skip exact duplicates already in the list
109
- const isDuplicate = (file: File) => files.value.some(existing => existing.name === file.name && existing.size === file.size && existing.lastModified === file.lastModified);
110
- const fresh = accepted.filter(file => !isDuplicate(file));
111
-
112
- // Enforce the max file count
113
- const room = props.maxFiles !== undefined ? Math.max(0, props.maxFiles - files.value.length) : fresh.length;
114
- for (const file of fresh.slice(room)) rejections.push({ file, reason: 'count' });
115
- next = [...files.value, ...fresh.slice(0, room)];
116
- }
117
-
118
- if (next.length !== files.value.length || next.some((file, index) => file !== files.value[index])) files.value = next;
119
- if (rejections.length > 0) {
120
- emits('reject', rejections);
121
- flashRejected();
122
- }
99
+ if (props.disabled || incoming.length === 0) return;
100
+ const rejections: FileRejection[] = [];
101
+ const accepted: File[] = [];
102
+
103
+ // Type and size checks
104
+ for (const file of incoming) {
105
+ if (!isFileAccepted(file, props.accept)) rejections.push({ file, reason: 'type' });
106
+ else if (props.maxSize !== undefined && file.size > props.maxSize) rejections.push({ file, reason: 'size' });
107
+ else accepted.push(file);
108
+ }
109
+
110
+ let next: File[];
111
+ if (!props.multiple) {
112
+ // Single mode keeps only the first valid file
113
+ next = accepted.slice(0, 1);
114
+ for (const file of accepted.slice(1)) rejections.push({ file, reason: 'count' });
115
+ if (next.length === 0) next = [...files.value];
116
+ } else {
117
+ // Skip exact duplicates already in the list
118
+ const isDuplicate = (file: File) => files.value.some(existing => existing.name === file.name && existing.size === file.size && existing.lastModified === file.lastModified);
119
+ const fresh = accepted.filter(file => !isDuplicate(file));
120
+
121
+ // Enforce the max file count
122
+ const room = props.maxFiles !== undefined ? Math.max(0, props.maxFiles - files.value.length) : fresh.length;
123
+ for (const file of fresh.slice(room)) rejections.push({ file, reason: 'count' });
124
+ next = [...files.value, ...fresh.slice(0, room)];
125
+ }
126
+
127
+ if (next.length !== files.value.length || next.some((file, index) => file !== files.value[index])) files.value = next;
128
+ if (rejections.length > 0) {
129
+ emits('reject', rejections);
130
+ flashRejected();
131
+ }
123
132
  }
124
133
 
125
134
  // Remove a file from the list
126
135
  function removeFile(file: File) {
127
- files.value = files.value.filter(existing => existing !== file);
136
+ files.value = files.value.filter(existing => existing !== file);
128
137
  }
129
138
 
130
139
  // Open the native file picker
131
140
  function openPicker() {
132
- if (props.disabled) return;
133
- inputRef.value?.click();
141
+ if (props.disabled) return;
142
+ inputRef.value?.click();
134
143
  }
135
144
 
136
145
  // Native picker selection
137
146
  function onInputChange(event: Event) {
138
- const input = event.target as HTMLInputElement;
139
- addFiles(Array.from(input.files ?? []));
140
- input.value = '';
147
+ const input = event.target as HTMLInputElement;
148
+ addFiles(Array.from(input.files ?? []));
149
+ input.value = '';
141
150
  }
142
151
 
143
152
  // Drag tracking (depth counter avoids flicker when crossing child elements)
144
153
  function onDragEnter(event: DragEvent) {
145
- if (props.disabled || !event.dataTransfer?.types.includes('Files')) return;
146
- dragDepth++;
147
- isDragging.value = true;
154
+ if (props.disabled || !event.dataTransfer?.types.includes('Files')) return;
155
+ dragDepth++;
156
+ isDragging.value = true;
148
157
  }
149
158
 
150
159
  function onDragOver(event: DragEvent) {
151
- if (props.disabled || !event.dataTransfer) return;
152
- event.dataTransfer.dropEffect = 'copy';
160
+ if (props.disabled || !event.dataTransfer) return;
161
+ event.dataTransfer.dropEffect = 'copy';
153
162
  }
154
163
 
155
164
  function onDragLeave() {
156
- if (!isDragging.value) return;
157
- dragDepth = Math.max(0, dragDepth - 1);
158
- if (dragDepth === 0) isDragging.value = false;
165
+ if (!isDragging.value) return;
166
+ dragDepth = Math.max(0, dragDepth - 1);
167
+ if (dragDepth === 0) isDragging.value = false;
159
168
  }
160
169
 
161
170
  function onDrop(event: DragEvent) {
162
- dragDepth = 0;
163
- isDragging.value = false;
164
- if (props.disabled) return;
165
- addFiles(Array.from(event.dataTransfer?.files ?? []));
171
+ dragDepth = 0;
172
+ isDragging.value = false;
173
+ if (props.disabled) return;
174
+ addFiles(Array.from(event.dataTransfer?.files ?? []));
166
175
  }
167
176
 
168
177
  onBeforeUnmount(() => clearTimeout(rejectTimer));
169
178
  </script>
170
179
 
171
180
  <template>
172
- <div data-slot="file-dropzone" :class="cn('flex w-full flex-col gap-3', props.class)">
173
- <!-- Drop area -->
174
- <div role="button" :tabindex="props.disabled ? -1 : 0" :aria-disabled="props.disabled || undefined" :aria-label="resolvedTitle" :data-dragging="isDragging ? '' : undefined" :data-disabled="props.disabled ? '' : undefined" :class="cn(
175
- 'group/dropzone relative isolate flex min-h-44 cursor-pointer select-none flex-col items-center justify-center gap-3 rounded border-2 border-dashed border-border bg-surface/50 px-6 py-8 text-center outline-none',
176
- '[transition:border-color_150ms,background-color_220ms,box-shadow_220ms,transform_420ms_var(--ease-spring)]',
177
- 'hover:border-border-strong hover:bg-surface focus-visible:ring-[3px] focus-visible:ring-ring/45 active:scale-[0.99] active:duration-75',
178
- 'data-dragging:scale-[1.01] data-dragging:border-transparent data-dragging:bg-primary/5 data-dragging:hover:border-transparent data-dragging:hover:bg-primary/5',
179
- 'data-disabled:cursor-not-allowed data-disabled:opacity-50 data-disabled:hover:border-border data-disabled:hover:bg-surface/50 data-disabled:active:scale-100',
180
- isRejected && 'animate-uv-shake border-destructive/70'
181
- )" @click="openPicker" @keydown.enter.prevent="openPicker" @keydown.space.prevent="openPicker" @dragenter.prevent="onDragEnter" @dragover.prevent="onDragOver" @dragleave="onDragLeave" @drop.prevent="onDrop">
182
- <!-- Marching dashed border while dragging -->
183
- <span aria-hidden="true" class="uv-dropzone-ants pointer-events-none absolute -inset-0.5 rounded opacity-0 transition-opacity duration-150 group-data-dragging/dropzone:opacity-100" />
184
-
185
- <!-- Upload icon (lifts and grows while dragging) -->
186
- <span aria-hidden="true" class="relative flex size-14 items-center justify-center rounded border border-border bg-card text-muted-foreground shadow-elevated-sm transition-[translate,scale,color,border-color,box-shadow] duration-420 ease-bounce group-hover/dropzone:-translate-y-0.5 group-hover/dropzone:text-foreground group-data-dragging/dropzone:-translate-y-2 group-data-dragging/dropzone:scale-115 group-data-dragging/dropzone:border-primary/50 group-data-dragging/dropzone:text-primary group-data-dragging/dropzone:shadow-elevated-md">
187
- <HugeiconsIcon :icon="CloudUploadIcon" class="size-6" />
188
- </span>
189
-
190
- <!-- Texts -->
191
- <div class="flex flex-col items-center gap-1">
192
- <p class="text-xs font-semibold text-foreground sm:text-sm">
193
- {{ isDragging ? t('uiVintage.fileDropzone.dropActive') : resolvedTitle }}
194
- </p>
195
- <p class="text-xs text-muted-foreground">{{ resolvedDescription }}</p>
196
- <p v-if="hints.length" class="text-xs text-muted-foreground/80">{{ hints.join(' · ') }}</p>
197
- </div>
198
-
199
- <!-- Browse hint (decorative, the whole zone is the button) -->
200
- <span aria-hidden="true" :class="cn(buttonVariants({ variant: 'secondary', size: 'sm' }), 'pointer-events-none transition-opacity group-data-dragging/dropzone:opacity-0')">
201
- {{ t('uiVintage.fileDropzone.browse') }}
202
- </span>
203
-
204
- <!-- Hidden native input -->
205
- <input ref="inputRef" type="file" class="hidden" tabindex="-1" :accept="props.accept" :multiple="props.multiple" :disabled="props.disabled" @change="onInputChange" @click.stop>
206
- </div>
207
-
208
- <!-- Selected files -->
209
- <TransitionGroup tag="ul" data-slot="file-dropzone-list" :aria-label="t('uiVintage.fileDropzone.files')" class="flex flex-col" enter-active-class="transition-[opacity,translate,filter] duration-420 ease-out-expo" enter-from-class="translate-y-2.5 opacity-0 blur-[3px]" leave-active-class="transition-[grid-template-rows,opacity,scale] duration-220 ease-snappy" leave-to-class="grid-rows-[0fr] scale-[0.98] opacity-0">
210
- <li v-for="file in files" :key="getFileId(file)" class="grid grid-rows-[1fr]">
211
- <div class="min-h-0 overflow-hidden">
212
- <div class="pb-2">
213
- <!-- File row -->
214
- <div data-slot="file-dropzone-item" class="flex items-center gap-3 rounded border border-border bg-card py-2 pr-2 pl-3 shadow-elevated-sm">
215
- <span class="flex size-9 shrink-0 items-center justify-center rounded-sm bg-surface text-muted-foreground">
216
- <HugeiconsIcon :icon="getFileIcon(file)" class="size-4.5" />
217
- </span>
218
- <div class="flex min-w-0 flex-1 flex-col">
219
- <span class="truncate text-xs font-semibold text-foreground sm:text-sm" :title="file.name">{{ file.name }}</span>
220
- <span class="text-xs text-muted-foreground tabular-nums">{{ formatFileSize(file.size) }}</span>
221
- </div>
222
- <Button variant="ghost" size="icon-sm" :disabled="props.disabled" :aria-label="t('uiVintage.fileDropzone.remove', { name: file.name })" class="hover:text-destructive" @click="removeFile(file)">
223
- <HugeiconsIcon :icon="Cancel01Icon" class="size-4" />
224
- </Button>
225
- </div>
226
- </div>
227
- </div>
228
- </li>
229
- </TransitionGroup>
230
- </div>
181
+ <div data-slot="file-dropzone" :class="cn('flex w-full flex-col gap-3', props.class)">
182
+ <!-- Drop area -->
183
+ <div role="button" :tabindex="props.disabled ? -1 : 0" :aria-disabled="props.disabled || undefined" :aria-label="resolvedTitle" :data-dragging="isDragging ? '' : undefined" :data-disabled="props.disabled ? '' : undefined" :class="cn(
184
+ 'group/dropzone relative isolate flex min-h-44 cursor-pointer select-none flex-col items-center justify-center gap-3 rounded border-2 border-dashed border-border bg-surface/50 px-6 py-8 text-center outline-none',
185
+ '[transition:border-color_150ms,background-color_220ms,box-shadow_220ms,transform_420ms_var(--ease-spring)]',
186
+ 'hover:border-border-strong hover:bg-surface focus-visible:ring-[3px] focus-visible:ring-ring/45 active:scale-[0.99] active:duration-75',
187
+ 'data-dragging:scale-[1.01] data-dragging:border-transparent data-dragging:bg-primary/5 data-dragging:hover:border-transparent data-dragging:hover:bg-primary/5',
188
+ 'data-disabled:cursor-not-allowed data-disabled:opacity-50 data-disabled:hover:border-border data-disabled:hover:bg-surface/50 data-disabled:active:scale-100',
189
+ isRejected && 'animate-uv-shake border-destructive/70'
190
+ )" @click="openPicker" @keydown.enter.prevent="openPicker" @keydown.space.prevent="openPicker" @dragenter.prevent="onDragEnter" @dragover.prevent="onDragOver" @dragleave="onDragLeave" @drop.prevent="onDrop">
191
+ <!-- Marching dashed border while dragging -->
192
+ <span aria-hidden="true" class="uv-dropzone-ants pointer-events-none absolute -inset-0.5 rounded opacity-0 transition-opacity duration-150 group-data-dragging/dropzone:opacity-100" />
193
+
194
+ <!-- Upload icon (lifts and grows while dragging) -->
195
+ <span aria-hidden="true" class="relative flex size-14 items-center justify-center rounded border border-border bg-card text-muted-foreground shadow-elevated-sm transition-[translate,scale,color,border-color,box-shadow] duration-420 ease-bounce group-hover/dropzone:-translate-y-0.5 group-hover/dropzone:text-foreground group-data-dragging/dropzone:-translate-y-2 group-data-dragging/dropzone:scale-115 group-data-dragging/dropzone:border-primary/50 group-data-dragging/dropzone:text-primary group-data-dragging/dropzone:shadow-elevated-md">
196
+ <HugeiconsIcon :icon="CloudUploadIcon" class="size-6" />
197
+ </span>
198
+
199
+ <!-- Texts -->
200
+ <div class="flex flex-col items-center gap-1">
201
+ <p class="text-xs font-semibold text-foreground sm:text-sm">
202
+ {{ isDragging ? t('uiVintage.fileDropzone.dropActive') : resolvedTitle }}
203
+ </p>
204
+ <p class="text-xs text-muted-foreground">{{ resolvedDescription }}</p>
205
+ <p v-if="hints.length" class="text-xs text-muted-foreground/80">{{ hints.join(' · ') }}</p>
206
+ </div>
207
+
208
+ <!-- Browse hint (decorative, the whole zone is the button) -->
209
+ <span aria-hidden="true" :class="cn(buttonVariants({ variant: 'secondary', size: 'sm' }), 'pointer-events-none transition-opacity group-data-dragging/dropzone:opacity-0')">
210
+ {{ t('uiVintage.fileDropzone.browse') }}
211
+ </span>
212
+
213
+ <!-- Hidden native input -->
214
+ <input ref="inputRef" type="file" class="hidden" tabindex="-1" :accept="props.accept" :multiple="props.multiple" :disabled="props.disabled" @change="onInputChange" @click.stop>
215
+ </div>
216
+
217
+ <!-- Selected files -->
218
+ <TransitionGroup tag="ul" data-slot="file-dropzone-list" :aria-label="t('uiVintage.fileDropzone.files')" class="flex flex-col" enter-active-class="transition-[opacity,translate,filter] duration-420 ease-out-expo" enter-from-class="translate-y-2.5 opacity-0 blur-[3px]" leave-active-class="transition-[grid-template-rows,opacity,scale] duration-220 ease-snappy" leave-to-class="grid-rows-[0fr] scale-[0.98] opacity-0">
219
+ <li v-for="file in files" :key="getFileId(file)" class="grid grid-rows-[1fr]">
220
+ <div class="min-h-0 overflow-hidden">
221
+ <div class="pb-2">
222
+ <!-- File row -->
223
+ <div data-slot="file-dropzone-item" class="flex items-center gap-3 rounded border border-border bg-card py-2 pr-2 pl-3 shadow-elevated-sm">
224
+ <span class="flex size-9 shrink-0 items-center justify-center rounded-sm bg-surface text-muted-foreground">
225
+ <HugeiconsIcon :icon="getFileIcon(file)" class="size-4.5" />
226
+ </span>
227
+ <div class="flex min-w-0 flex-1 flex-col">
228
+ <span class="truncate text-xs font-semibold text-foreground sm:text-sm" :title="file.name">{{ file.name }}</span>
229
+ <span class="text-xs text-muted-foreground tabular-nums">{{ formatFileSize(file.size) }}</span>
230
+ </div>
231
+ <Button variant="ghost" size="icon-sm" :disabled="props.disabled" :aria-label="t('uiVintage.fileDropzone.remove', { name: file.name })" class="hover:text-destructive" @click="removeFile(file)">
232
+ <HugeiconsIcon :icon="Cancel01Icon" class="size-4" />
233
+ </Button>
234
+ </div>
235
+ </div>
236
+ </div>
237
+ </li>
238
+ </TransitionGroup>
239
+ </div>
231
240
  </template>
232
241
 
233
242
  <style>
234
243
  /* Animated dashed border shown while files are dragged over the drop zone */
235
244
  .uv-dropzone-ants {
236
- background-image:
237
- linear-gradient(90deg, var(--primary) 55%, transparent 0),
238
- linear-gradient(90deg, var(--primary) 55%, transparent 0),
239
- linear-gradient(0deg, var(--primary) 55%, transparent 0),
240
- linear-gradient(0deg, var(--primary) 55%, transparent 0);
241
- background-size: 16px 2px, 16px 2px, 2px 16px, 2px 16px;
242
- background-position: 0 0, 0 100%, 0 0, 100% 0;
243
- background-repeat: repeat-x, repeat-x, repeat-y, repeat-y;
244
- animation: uv-dropzone-march 0.7s linear infinite;
245
+ background-image:
246
+ linear-gradient(90deg, var(--primary) 55%, transparent 0),
247
+ linear-gradient(90deg, var(--primary) 55%, transparent 0),
248
+ linear-gradient(0deg, var(--primary) 55%, transparent 0),
249
+ linear-gradient(0deg, var(--primary) 55%, transparent 0);
250
+ background-size: 16px 2px, 16px 2px, 2px 16px, 2px 16px;
251
+ background-position: 0 0, 0 100%, 0 0, 100% 0;
252
+ background-repeat: repeat-x, repeat-x, repeat-y, repeat-y;
253
+ animation: uv-dropzone-march 0.7s linear infinite;
245
254
  }
246
255
 
247
256
  @keyframes uv-dropzone-march {
248
- to { background-position: 16px 0, -16px 100%, 0 -16px, 100% 16px; }
257
+ to { background-position: 16px 0, -16px 100%, 0 -16px, 100% 16px; }
249
258
  }
250
259
  </style>