@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
@@ -1,34 +1,34 @@
1
1
  <script setup lang="ts">
2
2
  // Props
3
3
  const props = withDefaults(defineProps<{
4
- tags?: Array<{
5
- name: string;
6
- slug: string;
7
- count: number;
8
- }>;
9
- blogPath?: string;
4
+ tags?: Array<{
5
+ name: string;
6
+ slug: string;
7
+ count: number;
8
+ }>;
9
+ blogPath?: string;
10
10
  }>(), {
11
- tags: () => [],
12
- blogPath: '/blog'
11
+ tags: () => [],
12
+ blogPath: '/blog'
13
13
  });
14
14
  </script>
15
15
 
16
16
  <template>
17
- <div v-if="props.tags.length" class="flex flex-wrap gap-2">
18
- <!-- Tag chip -->
19
- <span v-for="(tag, index) in props.tags" :key="tag.slug" data-aos="blur-up" :data-aos-delay="Math.min(index * 40, 400)" class="inline-flex">
20
- <NuxtLinkLocale :to="`${props.blogPath}/tags/${tag.slug}`" class="group/tag inline-flex items-stretch overflow-hidden rounded border border-border text-xs shadow-elevated-sm outline-none transition-colors duration-150 hover:border-primary/40 focus-visible:ring-[3px] focus-visible:ring-ring/45 sm:text-sm">
21
- <!-- Name -->
22
- <span class="inline-flex items-center gap-1.5 bg-card px-3 py-1.5 font-medium text-foreground transition-colors duration-150 group-hover/tag:bg-primary/10 group-hover/tag:text-primary">
23
- <span aria-hidden="true" class="text-primary">#</span>
24
- {{ tag.name }}
25
- </span>
17
+ <div v-if="props.tags.length" class="flex flex-wrap gap-2">
18
+ <!-- Tag chip -->
19
+ <span v-for="(tag, index) in props.tags" :key="tag.slug" data-aos="blur-up" :data-aos-delay="Math.min(index * 40, 400)" class="inline-flex">
20
+ <NuxtLinkLocale :to="`${props.blogPath}/tags/${tag.slug}`" class="group/tag inline-flex items-stretch overflow-hidden rounded border border-border text-xs shadow-elevated-sm outline-none transition-colors duration-150 hover:border-primary/40 focus-visible:ring-[3px] focus-visible:ring-ring/45 sm:text-sm">
21
+ <!-- Name -->
22
+ <span class="inline-flex items-center gap-1.5 bg-card px-3 py-1.5 font-medium text-foreground transition-colors duration-150 group-hover/tag:bg-primary/10 group-hover/tag:text-primary">
23
+ <span aria-hidden="true" class="text-primary">#</span>
24
+ {{ tag.name }}
25
+ </span>
26
26
 
27
- <!-- Post count -->
28
- <span class="inline-flex items-center border-l border-border bg-surface px-2 text-[11px] font-semibold text-muted-foreground tabular-nums transition-colors duration-150 group-hover/tag:border-primary/40 group-hover/tag:bg-primary/10 group-hover/tag:text-primary">
29
- {{ tag.count }}
30
- </span>
31
- </NuxtLinkLocale>
32
- </span>
33
- </div>
34
- </template>
27
+ <!-- Post count -->
28
+ <span class="inline-flex items-center border-l border-border bg-surface px-2 text-[11px] font-semibold text-muted-foreground tabular-nums transition-colors duration-150 group-hover/tag:border-primary/40 group-hover/tag:bg-primary/10 group-hover/tag:text-primary">
29
+ {{ tag.count }}
30
+ </span>
31
+ </NuxtLinkLocale>
32
+ </span>
33
+ </div>
34
+ </template>
@@ -3,23 +3,23 @@ import CategoryCard from './CategoryCard.vue';
3
3
 
4
4
  // Props
5
5
  const props = withDefaults(defineProps<{
6
- categories?: Array<{
7
- path: string;
8
- slug?: string;
9
- name: string;
10
- image?: string;
11
- count: number;
12
- }>;
6
+ categories?: Array<{
7
+ path: string;
8
+ slug?: string;
9
+ name: string;
10
+ image?: string;
11
+ count: number;
12
+ }>;
13
13
  }>(), {
14
- categories: () => []
14
+ categories: () => []
15
15
  });
16
16
  </script>
17
17
 
18
18
  <template>
19
- <div class="grid grid-cols-1 gap-5 sm:grid-cols-2 md:gap-6 lg:grid-cols-3 xl:grid-cols-4">
20
- <!-- Category card -->
21
- <NuxtLink v-for="(category, index) in props.categories" :key="category.path || category.slug" data-aos="blur-up" :data-aos-delay="(index % 4) * 80" :to="category.path" class="block h-full rounded outline-none focus-visible:ring-[3px] focus-visible:ring-ring/45">
22
- <CategoryCard :name="category.name" :image="category.image" :count="category.count" />
23
- </NuxtLink>
24
- </div>
19
+ <div class="grid grid-cols-1 gap-5 sm:grid-cols-2 md:gap-6 lg:grid-cols-3 xl:grid-cols-4">
20
+ <!-- Category card -->
21
+ <NuxtLink v-for="(category, index) in props.categories" :key="category.path || category.slug" data-aos="blur-up" :data-aos-delay="(index % 4) * 80" :to="category.path" class="block h-full rounded outline-none focus-visible:ring-[3px] focus-visible:ring-ring/45">
22
+ <CategoryCard :name="category.name" :image="category.image" :count="category.count" />
23
+ </NuxtLink>
24
+ </div>
25
25
  </template>
@@ -3,20 +3,20 @@ import CategoriesList from './CategoriesList.vue';
3
3
 
4
4
  // Props
5
5
  const props = withDefaults(defineProps<{
6
- categories?: Array<{
7
- path: string;
8
- slug?: string;
9
- name: string;
10
- image?: string;
11
- count: number;
12
- }>;
6
+ categories?: Array<{
7
+ path: string;
8
+ slug?: string;
9
+ name: string;
10
+ image?: string;
11
+ count: number;
12
+ }>;
13
13
  }>(), {
14
- categories: () => []
14
+ categories: () => []
15
15
  });
16
16
  </script>
17
17
 
18
18
  <template>
19
- <div>
20
- <CategoriesList :categories="props.categories" />
21
- </div>
19
+ <div>
20
+ <CategoriesList :categories="props.categories" />
21
+ </div>
22
22
  </template>
@@ -1,40 +1,41 @@
1
1
  <script setup lang="ts">
2
2
  import { NuxtImg } from '#components';
3
- import { ArrowRight01Icon, Folder01Icon } from '@hugeicons/core-free-icons';
3
+ import ArrowRight01Icon from '@hugeicons/core-free-icons/ArrowRight01Icon';
4
+ import Folder01Icon from '@hugeicons/core-free-icons/Folder01Icon';
4
5
  import { HugeiconsIcon } from '@hugeicons/vue';
5
6
  import { Card } from '../ui/card';
6
7
 
7
8
  // Props
8
9
  const props = withDefaults(defineProps<{
9
- name: string;
10
- image?: string;
11
- count: number;
10
+ name: string;
11
+ image?: string;
12
+ count: number;
12
13
  }>(), {
13
- image: ''
14
+ image: ''
14
15
  });
15
16
  </script>
16
17
 
17
18
  <template>
18
- <Card class="group/category h-full gap-0! overflow-hidden p-0! transition-[translate,box-shadow] duration-300 ease-out-expo hover:-translate-y-0.5 hover:shadow-elevated-md">
19
- <!-- Image -->
20
- <div class="h-32 w-full overflow-hidden border-b border-border bg-surface">
21
- <NuxtImg v-if="props.image" :src="props.image" :alt="props.name" height="225" width="400" format="avif" quality="35" :sizes="{ 480: '480px', 1280: '400px' }" loading="lazy" decoding="async" class="size-full object-cover transition-[scale] duration-700 ease-out-expo group-hover/category:scale-[1.03]" />
19
+ <Card class="group/category h-full gap-0! overflow-hidden p-0! transition-[translate,box-shadow] duration-300 ease-out-expo hover:-translate-y-0.5 hover:shadow-elevated-md">
20
+ <!-- Image -->
21
+ <div class="h-32 w-full overflow-hidden border-b border-border bg-surface">
22
+ <NuxtImg v-if="props.image" :src="props.image" :alt="props.name" height="225" width="400" format="avif" quality="35" :sizes="{ 480: '480px', 1280: '400px' }" loading="lazy" decoding="async" class="size-full object-cover transition-[scale] duration-700 ease-out-expo group-hover/category:scale-[1.03]" />
22
23
 
23
- <!-- Fallback when there is no image -->
24
- <div v-else aria-hidden="true" class="flex size-full items-center justify-center text-primary/60">
25
- <HugeiconsIcon :icon="Folder01Icon" class="size-8" />
26
- </div>
27
- </div>
24
+ <!-- Fallback when there is no image -->
25
+ <div v-else aria-hidden="true" class="flex size-full items-center justify-center text-primary/60">
26
+ <HugeiconsIcon :icon="Folder01Icon" class="size-8" />
27
+ </div>
28
+ </div>
28
29
 
29
- <!-- Name, post count and arrow -->
30
- <div class="flex grow items-center gap-3 bg-surface px-4 py-3 transition-colors duration-200 group-hover/category:bg-primary/10">
31
- <span class="min-w-0 flex-1 truncate text-sm font-semibold text-foreground transition-colors duration-200 group-hover/category:text-primary">
32
- {{ props.name }}
33
- </span>
34
- <span class="shrink-0 rounded border border-primary/30 px-1.5 text-[11px] font-semibold leading-5 text-primary tabular-nums">
35
- {{ props.count }}
36
- </span>
37
- <HugeiconsIcon :icon="ArrowRight01Icon" class="size-4 shrink-0 text-muted-foreground transition-[translate,color] duration-300 ease-spring group-hover/category:translate-x-1 group-hover/category:text-primary" />
38
- </div>
39
- </Card>
40
- </template>
30
+ <!-- Name, post count and arrow -->
31
+ <div class="flex grow items-center gap-3 bg-surface px-4 py-3 transition-colors duration-200 group-hover/category:bg-primary/10">
32
+ <span class="min-w-0 flex-1 truncate text-sm font-semibold text-foreground transition-colors duration-200 group-hover/category:text-primary">
33
+ {{ props.name }}
34
+ </span>
35
+ <span class="shrink-0 rounded border border-primary/30 px-1.5 text-[11px] font-semibold leading-5 text-primary tabular-nums">
36
+ {{ props.count }}
37
+ </span>
38
+ <HugeiconsIcon :icon="ArrowRight01Icon" class="size-4 shrink-0 text-muted-foreground transition-[translate,color] duration-300 ease-spring group-hover/category:translate-x-1 group-hover/category:text-primary" />
39
+ </div>
40
+ </Card>
41
+ </template>
@@ -2,7 +2,8 @@
2
2
  import { NuxtImg } from '#components';
3
3
  import { computed, onMounted, onUnmounted, ref, useId, watch } from 'vue';
4
4
  import { useI18n } from 'vue-i18n';
5
- import { ArrowLeft01Icon, ArrowRight01Icon } from '@hugeicons/core-free-icons';
5
+ import ArrowLeft01Icon from '@hugeicons/core-free-icons/ArrowLeft01Icon';
6
+ import ArrowRight01Icon from '@hugeicons/core-free-icons/ArrowRight01Icon';
6
7
  import { HugeiconsIcon } from '@hugeicons/vue';
7
8
  import { Badge } from '../ui/badge';
8
9
  import { Button } from '../ui/button';
@@ -12,16 +13,16 @@ const { t } = useI18n();
12
13
 
13
14
  // Props
14
15
  const props = withDefaults(defineProps<{
15
- featuredPosts?: Array<{
16
- path: string;
17
- title: string;
18
- description?: string;
19
- image?: string;
20
- categoryText?: string;
21
- categoryPath?: string;
22
- }>;
16
+ featuredPosts?: Array<{
17
+ path: string;
18
+ title: string;
19
+ description?: string;
20
+ image?: string;
21
+ categoryText?: string;
22
+ categoryPath?: string;
23
+ }>;
23
24
  }>(), {
24
- featuredPosts: () => []
25
+ featuredPosts: () => []
25
26
  });
26
27
 
27
28
  const AUTOPLAY_DURATION_MS = 6000;
@@ -37,139 +38,139 @@ const slideIdPrefix = `carousel-${useId()}-slide`;
37
38
 
38
39
  // Keep the active slide in range when the list of posts shrinks
39
40
  watch(slideCount, (count) => {
40
- if (currentSlide.value >= count) {
41
- currentSlide.value = Math.max(0, count - 1);
42
- }
41
+ if (currentSlide.value >= count) {
42
+ currentSlide.value = Math.max(0, count - 1);
43
+ }
43
44
  });
44
45
  let swipeStartX: number | null = null;
45
46
 
46
47
  // Handle next slide press
47
48
  const nextSlide = () => {
48
- currentSlide.value = (currentSlide.value + 1) % slideCount.value;
49
+ currentSlide.value = (currentSlide.value + 1) % slideCount.value;
49
50
  };
50
51
 
51
52
  // Handle previous slide press
52
53
  const prevSlide = () => {
53
- currentSlide.value = (currentSlide.value - 1 + slideCount.value) % slideCount.value;
54
+ currentSlide.value = (currentSlide.value - 1 + slideCount.value) % slideCount.value;
54
55
  };
55
56
 
56
57
  // Go to specific slide
57
58
  const goToSlide = (index: number) => {
58
- currentSlide.value = index;
59
+ currentSlide.value = index;
59
60
  };
60
61
 
61
62
  // Pause autoplay while the user hovers, focuses, or leaves the tab
62
63
  const pause = () => {
63
- isPaused.value = true;
64
+ isPaused.value = true;
64
65
  };
65
66
  const resume = () => {
66
- isPaused.value = false;
67
+ isPaused.value = false;
67
68
  };
68
69
  const handleVisibilityChange = () => {
69
- isPaused.value = document.visibilityState !== 'visible';
70
+ isPaused.value = document.visibilityState !== 'visible';
70
71
  };
71
72
 
72
73
  // Keyboard navigation
73
74
  const handleKeydown = (event: KeyboardEvent) => {
74
- if (event.key === 'ArrowRight') nextSlide();
75
- if (event.key === 'ArrowLeft') prevSlide();
75
+ if (event.key === 'ArrowRight') nextSlide();
76
+ if (event.key === 'ArrowLeft') prevSlide();
76
77
  };
77
78
 
78
79
  // Swipe navigation on touch devices
79
80
  const handlePointerDown = (event: PointerEvent) => {
80
- if (event.pointerType !== 'mouse') swipeStartX = event.clientX;
81
+ if (event.pointerType !== 'mouse') swipeStartX = event.clientX;
81
82
  };
82
83
  const handlePointerUp = (event: PointerEvent) => {
83
- if (swipeStartX === null) return;
84
- const deltaX = event.clientX - swipeStartX;
85
- swipeStartX = null;
86
- if (Math.abs(deltaX) < SWIPE_THRESHOLD_PX) return;
87
- if (deltaX < 0) nextSlide();
88
- else prevSlide();
84
+ if (swipeStartX === null) return;
85
+ const deltaX = event.clientX - swipeStartX;
86
+ swipeStartX = null;
87
+ if (Math.abs(deltaX) < SWIPE_THRESHOLD_PX) return;
88
+ if (deltaX < 0) nextSlide();
89
+ else prevSlide();
89
90
  };
90
91
 
91
92
  // Autoplay is driven by the progress animation of the active indicator, so the bar and the slide change always stay in sync
92
93
  const handleProgressEnd = () => {
93
- if (autoplayEnabled.value) nextSlide();
94
+ if (autoplayEnabled.value) nextSlide();
94
95
  };
95
96
 
96
97
  // Allow autoplay once mounted (never with reduced motion); it starts as soon as there are at least two posts
97
98
  onMounted(() => {
98
- motionAllowed.value = !window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
99
- document.addEventListener('visibilitychange', handleVisibilityChange);
99
+ motionAllowed.value = !window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
100
+ document.addEventListener('visibilitychange', handleVisibilityChange);
100
101
  });
101
102
 
102
103
  // Clean up listeners when component unmounts
103
104
  onUnmounted(() => {
104
- document.removeEventListener('visibilitychange', handleVisibilityChange);
105
+ document.removeEventListener('visibilitychange', handleVisibilityChange);
105
106
  });
106
107
  </script>
107
108
 
108
109
  <template>
109
- <div v-if="props.featuredPosts.length > 0" role="region" aria-roledescription="carousel" :aria-label="t('uiVintage.blog.carouselNavigation')" class="group/carousel relative isolate overflow-hidden rounded border border-border bg-card shadow-elevated-lg outline-none focus-visible:ring-[3px] focus-visible:ring-ring/45" tabindex="0" @mouseenter="pause" @mouseleave="resume" @focusin="pause" @focusout="resume" @keydown="handleKeydown" @pointerdown="handlePointerDown" @pointerup="handlePointerUp">
110
- <!-- Slides (stacked; the active one crossfades in) -->
111
- <div class="relative h-[28rem] w-full md:h-[32rem]">
112
- <div v-for="(post, index) in props.featuredPosts" :id="`${slideIdPrefix}-${index}`" :key="post.path || post.title || index" role="group" aria-roledescription="slide" :aria-label="`${index + 1} / ${slideCount}`" :aria-hidden="currentSlide !== index" :inert="currentSlide !== index || undefined" :class="cn('absolute inset-0 transition-[opacity,scale] duration-700 ease-out-expo', currentSlide === index ? 'z-10 scale-100 opacity-100' : 'z-0 scale-[1.02] opacity-0')">
113
- <!-- Slide image with a slow zoom -->
114
- <NuxtLink :to="post.path" tabindex="-1" aria-hidden="true" class="absolute inset-0 overflow-hidden">
115
- <NuxtImg v-if="post.image" :src="post.image" :alt="post.title" width="1200" height="640" format="avif" quality="45" :sizes="{ 480: '480px', 1536: '1152px' }" :loading="index === 0 ? 'eager' : 'lazy'" :fetchpriority="index === 0 ? 'high' : 'auto'" :preload="index === 0" :class="cn('size-full object-cover', currentSlide === index && 'animate-uv-ken-burns')" />
116
- <div v-else class="size-full bg-[radial-gradient(circle_at_20%_20%,color-mix(in_oklab,var(--primary)_45%,transparent),transparent_60%)] bg-surface" />
117
- </NuxtLink>
118
-
119
- <!-- Legibility shade -->
120
- <div aria-hidden="true" class="pointer-events-none absolute inset-0 bg-linear-to-t from-black/90 via-black/45 to-black/5" />
121
- <div aria-hidden="true" class="pointer-events-none absolute inset-0 bg-linear-to-r from-black/50 via-transparent to-transparent" />
122
-
123
- <!-- Slide content (replays its entrance every time the slide becomes active) -->
124
- <div class="absolute inset-x-0 bottom-0 flex flex-col gap-4 p-6 pb-20 text-white md:max-w-3xl md:p-10 md:pb-24">
125
- <!-- Category badge -->
126
- <NuxtLink v-if="post.categoryText && post.categoryPath" :to="post.categoryPath" :class="cn('w-fit', currentSlide === index && 'animate-uv-fade-up')">
127
- <Badge :text="post.categoryText" pulse class="border-white/25 bg-white/15 text-white backdrop-blur-md transition-colors hover:bg-white/25" />
128
- </NuxtLink>
129
-
130
- <!-- Title -->
131
- <NuxtLink :to="post.path" :class="cn('outline-none', currentSlide === index && 'animate-uv-fade-up [animation-delay:90ms]')">
132
- <h3 class="line-clamp-3 text-2xl leading-tight font-bold tracking-tight text-balance drop-shadow-sm md:text-5xl">
133
- {{ post.title }}
134
- </h3>
135
- </NuxtLink>
136
-
137
- <!-- Description -->
138
- <p v-if="post.description" :class="cn('line-clamp-2 max-w-2xl text-xs leading-relaxed text-white/80 md:text-base', currentSlide === index && 'animate-uv-fade-up [animation-delay:180ms]')">
139
- {{ post.description }}
140
- </p>
141
-
142
- <!-- Read post button -->
143
- <div :class="cn(currentSlide === index && 'animate-uv-fade-up [animation-delay:260ms]')">
144
- <Button as-child size="lg" class="group/cta">
145
- <NuxtLink :to="post.path">
146
- {{ t('uiVintage.blog.goToPost') }}
147
- <HugeiconsIcon :icon="ArrowRight01Icon" class="size-4 transition-[translate] duration-300 ease-spring group-hover/cta:translate-x-1" />
148
- </NuxtLink>
149
- </Button>
150
- </div>
151
- </div>
152
- </div>
153
- </div>
154
-
155
- <!-- Controls bar -->
156
- <div v-if="slideCount > 1" class="absolute inset-x-0 bottom-0 z-20 flex items-center justify-between gap-4 p-6 md:px-10 md:pb-8">
157
- <!-- Slide indicators (the active one fills up until the next slide) -->
158
- <div class="flex items-center gap-2" role="tablist" :aria-label="t('uiVintage.blog.carouselNavigation')">
159
- <button v-for="(_, index) in props.featuredPosts" :key="index" type="button" role="tab" :aria-selected="currentSlide === index" :aria-controls="`${slideIdPrefix}-${index}`" :aria-label="t('uiVintage.blog.goToSlide', { index: index + 1 })" :class="cn('relative h-1.5 cursor-pointer overflow-hidden rounded-full bg-white/30 outline-none transition-[width,background-color] duration-500 ease-spring hover:bg-white/50 focus-visible:ring-2 focus-visible:ring-white', currentSlide === index ? 'w-12' : 'w-4')" @click="goToSlide(index)">
160
- <span v-if="currentSlide === index" :key="`progress-${currentSlide}`" class="absolute inset-0 origin-left rounded-full bg-primary" :style="autoplayEnabled ? { animation: `uv-grow-x ${AUTOPLAY_DURATION_MS}ms linear both`, animationPlayState: isPaused ? 'paused' : 'running' } : undefined" @animationend="handleProgressEnd" />
161
- </button>
162
- </div>
163
-
164
- <!-- Previous and next buttons -->
165
- <div class="flex items-center gap-2">
166
- <button type="button" :aria-label="t('uiVintage.blog.previousSlide')" class="flex size-10 cursor-pointer items-center justify-center rounded border border-white/25 bg-black/30 text-white backdrop-blur-md outline-none [transition:background-color_150ms,scale_300ms_var(--ease-spring)] hover:bg-white/20 active:scale-90 focus-visible:ring-2 focus-visible:ring-white" @click="prevSlide">
167
- <HugeiconsIcon :icon="ArrowLeft01Icon" class="size-4" />
168
- </button>
169
- <button type="button" :aria-label="t('uiVintage.blog.nextSlide')" class="flex size-10 cursor-pointer items-center justify-center rounded border border-white/25 bg-black/30 text-white backdrop-blur-md outline-none [transition:background-color_150ms,scale_300ms_var(--ease-spring)] hover:bg-white/20 active:scale-90 focus-visible:ring-2 focus-visible:ring-white" @click="nextSlide">
170
- <HugeiconsIcon :icon="ArrowRight01Icon" class="size-4" />
171
- </button>
172
- </div>
173
- </div>
174
- </div>
110
+ <div v-if="props.featuredPosts.length > 0" role="region" aria-roledescription="carousel" :aria-label="t('uiVintage.blog.carouselNavigation')" class="group/carousel relative isolate overflow-hidden rounded border border-border bg-card shadow-elevated-lg outline-none focus-visible:ring-[3px] focus-visible:ring-ring/45" tabindex="0" @mouseenter="pause" @mouseleave="resume" @focusin="pause" @focusout="resume" @keydown="handleKeydown" @pointerdown="handlePointerDown" @pointerup="handlePointerUp">
111
+ <!-- Slides (stacked; the active one crossfades in) -->
112
+ <div class="relative h-[28rem] w-full md:h-[32rem]">
113
+ <div v-for="(post, index) in props.featuredPosts" :id="`${slideIdPrefix}-${index}`" :key="post.path || post.title || index" role="group" aria-roledescription="slide" :aria-label="`${index + 1} / ${slideCount}`" :aria-hidden="currentSlide !== index" :inert="currentSlide !== index || undefined" :class="cn('absolute inset-0 transition-[opacity,scale] duration-700 ease-out-expo', currentSlide === index ? 'z-10 scale-100 opacity-100' : 'z-0 scale-[1.02] opacity-0')">
114
+ <!-- Slide image with a slow zoom -->
115
+ <NuxtLink :to="post.path" tabindex="-1" aria-hidden="true" class="absolute inset-0 overflow-hidden">
116
+ <NuxtImg v-if="post.image" :src="post.image" :alt="post.title" width="1200" height="640" format="avif" quality="45" :sizes="{ 480: '480px', 1536: '1152px' }" :loading="index === 0 ? 'eager' : 'lazy'" :fetchpriority="index === 0 ? 'high' : 'auto'" :preload="index === 0" :class="cn('size-full object-cover', currentSlide === index && 'animate-uv-ken-burns')" />
117
+ <div v-else class="size-full bg-[radial-gradient(circle_at_20%_20%,color-mix(in_oklab,var(--primary)_45%,transparent),transparent_60%)] bg-surface" />
118
+ </NuxtLink>
119
+
120
+ <!-- Legibility shade -->
121
+ <div aria-hidden="true" class="pointer-events-none absolute inset-0 bg-linear-to-t from-black/90 via-black/45 to-black/5" />
122
+ <div aria-hidden="true" class="pointer-events-none absolute inset-0 bg-linear-to-r from-black/50 via-transparent to-transparent" />
123
+
124
+ <!-- Slide content (replays its entrance every time the slide becomes active) -->
125
+ <div class="absolute inset-x-0 bottom-0 flex flex-col gap-4 p-6 pb-20 text-white md:max-w-3xl md:p-10 md:pb-24">
126
+ <!-- Category badge -->
127
+ <NuxtLink v-if="post.categoryText && post.categoryPath" :to="post.categoryPath" :class="cn('w-fit', currentSlide === index && 'animate-uv-fade-up')">
128
+ <Badge :text="post.categoryText" pulse class="border-white/25 bg-white/15 text-white backdrop-blur-md transition-colors hover:bg-white/25" />
129
+ </NuxtLink>
130
+
131
+ <!-- Title -->
132
+ <NuxtLink :to="post.path" :class="cn('outline-none', currentSlide === index && 'animate-uv-fade-up [animation-delay:90ms]')">
133
+ <h3 class="line-clamp-3 text-2xl leading-tight font-bold tracking-tight text-balance drop-shadow-sm md:text-5xl">
134
+ {{ post.title }}
135
+ </h3>
136
+ </NuxtLink>
137
+
138
+ <!-- Description -->
139
+ <p v-if="post.description" :class="cn('line-clamp-2 max-w-2xl text-xs leading-relaxed text-white/80 md:text-base', currentSlide === index && 'animate-uv-fade-up [animation-delay:180ms]')">
140
+ {{ post.description }}
141
+ </p>
142
+
143
+ <!-- Read post button -->
144
+ <div :class="cn(currentSlide === index && 'animate-uv-fade-up [animation-delay:260ms]')">
145
+ <Button as-child size="lg" class="group/cta">
146
+ <NuxtLink :to="post.path">
147
+ {{ t('uiVintage.blog.goToPost') }}
148
+ <HugeiconsIcon :icon="ArrowRight01Icon" class="size-4 transition-[translate] duration-300 ease-spring group-hover/cta:translate-x-1" />
149
+ </NuxtLink>
150
+ </Button>
151
+ </div>
152
+ </div>
153
+ </div>
154
+ </div>
155
+
156
+ <!-- Controls bar -->
157
+ <div v-if="slideCount > 1" class="absolute inset-x-0 bottom-0 z-20 flex items-center justify-between gap-4 p-6 md:px-10 md:pb-8">
158
+ <!-- Slide indicators (the active one fills up until the next slide) -->
159
+ <div class="flex items-center gap-2" role="tablist" :aria-label="t('uiVintage.blog.carouselNavigation')">
160
+ <button v-for="(_, index) in props.featuredPosts" :key="index" type="button" role="tab" :aria-selected="currentSlide === index" :aria-controls="`${slideIdPrefix}-${index}`" :aria-label="t('uiVintage.blog.goToSlide', { index: index + 1 })" :class="cn('relative h-1.5 cursor-pointer overflow-hidden rounded-full bg-white/30 outline-none transition-[width,background-color] duration-500 ease-spring hover:bg-white/50 focus-visible:ring-2 focus-visible:ring-white', currentSlide === index ? 'w-12' : 'w-4')" @click="goToSlide(index)">
161
+ <span v-if="currentSlide === index" :key="`progress-${currentSlide}`" class="absolute inset-0 origin-left rounded-full bg-primary" :style="autoplayEnabled ? { animation: `uv-grow-x ${AUTOPLAY_DURATION_MS}ms linear both`, animationPlayState: isPaused ? 'paused' : 'running' } : undefined" @animationend="handleProgressEnd" />
162
+ </button>
163
+ </div>
164
+
165
+ <!-- Previous and next buttons -->
166
+ <div class="flex items-center gap-2">
167
+ <button type="button" :aria-label="t('uiVintage.blog.previousSlide')" class="flex size-10 cursor-pointer items-center justify-center rounded border border-white/25 bg-black/30 text-white backdrop-blur-md outline-none [transition:background-color_150ms,scale_300ms_var(--ease-spring)] hover:bg-white/20 active:scale-90 focus-visible:ring-2 focus-visible:ring-white" @click="prevSlide">
168
+ <HugeiconsIcon :icon="ArrowLeft01Icon" class="size-4" />
169
+ </button>
170
+ <button type="button" :aria-label="t('uiVintage.blog.nextSlide')" class="flex size-10 cursor-pointer items-center justify-center rounded border border-white/25 bg-black/30 text-white backdrop-blur-md outline-none [transition:background-color_150ms,scale_300ms_var(--ease-spring)] hover:bg-white/20 active:scale-90 focus-visible:ring-2 focus-visible:ring-white" @click="nextSlide">
171
+ <HugeiconsIcon :icon="ArrowRight01Icon" class="size-4" />
172
+ </button>
173
+ </div>
174
+ </div>
175
+ </div>
175
176
  </template>
@@ -1,7 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import { NuxtImg } from '#components';
3
3
  import { useI18n } from 'vue-i18n';
4
- import { ArrowRight01Icon } from '@hugeicons/core-free-icons';
4
+ import ArrowRight01Icon from '@hugeicons/core-free-icons/ArrowRight01Icon';
5
5
  import { HugeiconsIcon } from '@hugeicons/vue';
6
6
  import { Card } from '../ui/card';
7
7
 
@@ -9,45 +9,45 @@ const { t } = useI18n();
9
9
 
10
10
  // Props
11
11
  const props = withDefaults(defineProps<{
12
- image?: string;
13
- category?: string;
14
- title?: string;
15
- description?: string;
12
+ image?: string;
13
+ category?: string;
14
+ title?: string;
15
+ description?: string;
16
16
  }>(), {
17
- image: '',
18
- category: '',
19
- title: '',
20
- description: ''
17
+ image: '',
18
+ category: '',
19
+ title: '',
20
+ description: ''
21
21
  });
22
22
  </script>
23
23
 
24
24
  <template>
25
- <Card class="group/post h-full gap-0! overflow-hidden p-0! transition-[translate,box-shadow] duration-300 ease-out-expo hover:-translate-y-0.5 hover:shadow-elevated-md">
26
- <!-- Featured image -->
27
- <div v-if="props.image" class="aspect-video w-full overflow-hidden border-b border-border bg-surface">
28
- <NuxtImg :src="props.image" :alt="props.title" height="225" width="400" format="avif" quality="35" :sizes="{ 480: '480px', 1280: '400px' }" loading="lazy" decoding="async" class="size-full object-cover transition-[scale] duration-700 ease-out-expo group-hover/post:scale-[1.03]" />
29
- </div>
25
+ <Card class="group/post h-full gap-0! overflow-hidden p-0! transition-[translate,box-shadow] duration-300 ease-out-expo hover:-translate-y-0.5 hover:shadow-elevated-md">
26
+ <!-- Featured image -->
27
+ <div v-if="props.image" class="aspect-video w-full overflow-hidden border-b border-border bg-surface">
28
+ <NuxtImg :src="props.image" :alt="props.title" height="225" width="400" format="avif" quality="35" :sizes="{ 480: '480px', 1280: '400px' }" loading="lazy" decoding="async" class="size-full object-cover transition-[scale] duration-700 ease-out-expo group-hover/post:scale-[1.03]" />
29
+ </div>
30
30
 
31
- <!-- Content -->
32
- <div class="flex grow flex-col gap-2 px-5 py-4">
33
- <!-- Category -->
34
- <span v-if="props.category" class="text-[11px] font-semibold uppercase tracking-wider text-primary">{{ props.category }}</span>
31
+ <!-- Content -->
32
+ <div class="flex grow flex-col gap-2 px-5 py-4">
33
+ <!-- Category -->
34
+ <span v-if="props.category" class="text-[11px] font-semibold uppercase tracking-wider text-primary">{{ props.category }}</span>
35
35
 
36
- <!-- Post title -->
37
- <h2 class="line-clamp-2 text-sm leading-snug font-semibold text-foreground transition-colors duration-200 group-hover/post:text-primary md:text-base">
38
- {{ props.title }}
39
- </h2>
36
+ <!-- Post title -->
37
+ <h2 class="line-clamp-2 text-sm leading-snug font-semibold text-foreground transition-colors duration-200 group-hover/post:text-primary md:text-base">
38
+ {{ props.title }}
39
+ </h2>
40
40
 
41
- <!-- Post description -->
42
- <p class="line-clamp-3 grow text-xs leading-relaxed text-muted-foreground md:text-sm">
43
- {{ props.description }}
44
- </p>
45
- </div>
41
+ <!-- Post description -->
42
+ <p class="line-clamp-3 grow text-xs leading-relaxed text-muted-foreground md:text-sm">
43
+ {{ props.description }}
44
+ </p>
45
+ </div>
46
46
 
47
- <!-- Read more -->
48
- <div class="flex items-center justify-between border-t border-border bg-surface px-5 py-2.5 text-xs text-muted-foreground transition-colors duration-200 group-hover/post:bg-primary/10 group-hover/post:text-primary">
49
- <span class="font-medium">{{ t('uiVintage.blog.readMore') }}</span>
50
- <HugeiconsIcon :icon="ArrowRight01Icon" class="size-4 transition-[translate] duration-300 ease-spring group-hover/post:translate-x-1" />
51
- </div>
52
- </Card>
53
- </template>
47
+ <!-- Read more -->
48
+ <div class="flex items-center justify-between border-t border-border bg-surface px-5 py-2.5 text-xs text-muted-foreground transition-colors duration-200 group-hover/post:bg-primary/10 group-hover/post:text-primary">
49
+ <span class="font-medium">{{ t('uiVintage.blog.readMore') }}</span>
50
+ <HugeiconsIcon :icon="ArrowRight01Icon" class="size-4 transition-[translate] duration-300 ease-spring group-hover/post:translate-x-1" />
51
+ </div>
52
+ </Card>
53
+ </template>
@@ -3,24 +3,24 @@ import PostCard from './PostCard.vue';
3
3
 
4
4
  // Props
5
5
  const props = withDefaults(defineProps<{
6
- posts?: Array<{
7
- path: string;
8
- title: string;
9
- description?: string;
10
- image?: string;
11
- categoryText?: string;
12
- categoryPath?: string;
13
- }>;
6
+ posts?: Array<{
7
+ path: string;
8
+ title: string;
9
+ description?: string;
10
+ image?: string;
11
+ categoryText?: string;
12
+ categoryPath?: string;
13
+ }>;
14
14
  }>(), {
15
- posts: () => []
15
+ posts: () => []
16
16
  });
17
17
  </script>
18
18
 
19
19
  <template>
20
- <div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
21
- <!-- Post cards rise in row by row as they scroll into view -->
22
- <NuxtLink v-for="(post, index) in props.posts" :key="post.path" data-aos="blur-up" :data-aos-delay="(index % 3) * 90" :to="post.path" class="block h-full rounded outline-none focus-visible:ring-[3px] focus-visible:ring-ring/45">
23
- <PostCard class="h-full" :image="post.image" :category="post.categoryText" :title="post.title" :description="post.description" />
24
- </NuxtLink>
25
- </div>
20
+ <div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
21
+ <!-- Post cards rise in row by row as they scroll into view -->
22
+ <NuxtLink v-for="(post, index) in props.posts" :key="post.path" data-aos="blur-up" :data-aos-delay="(index % 3) * 90" :to="post.path" class="block h-full rounded outline-none focus-visible:ring-[3px] focus-visible:ring-ring/45">
23
+ <PostCard class="h-full" :image="post.image" :category="post.categoryText" :title="post.title" :description="post.description" />
24
+ </NuxtLink>
25
+ </div>
26
26
  </template>