@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,38 +1,40 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
3
3
  import { useI18n } from 'vue-i18n';
4
- import { ArrowRight01Icon, Bookmark01Icon, Cancel01Icon } from '@hugeicons/core-free-icons';
4
+ import ArrowRight01Icon from '@hugeicons/core-free-icons/ArrowRight01Icon';
5
+ import Bookmark01Icon from '@hugeicons/core-free-icons/Bookmark01Icon';
6
+ import Cancel01Icon from '@hugeicons/core-free-icons/Cancel01Icon';
5
7
  import { HugeiconsIcon } from '@hugeicons/vue';
6
8
  import { Accordion } from '../ui/accordion';
7
9
  import { Button } from '../ui/button';
8
10
  import { Sheet, SheetClose, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '../ui/sheet';
9
11
 
10
12
  interface BlogTocLink {
11
- id?: string;
12
- text?: string;
13
- depth?: number;
14
- children?: BlogTocLink[];
13
+ id?: string;
14
+ text?: string;
15
+ depth?: number;
16
+ children?: BlogTocLink[];
15
17
  }
16
18
 
17
19
  interface TocHeading {
18
- id: string;
19
- text: string;
20
- level: number;
21
- number: string;
20
+ id: string;
21
+ text: string;
22
+ level: number;
23
+ number: string;
22
24
  }
23
25
 
24
26
  // Props
25
27
  const props = defineProps<{
26
- content: {
27
- body?: {
28
- toc?: {
29
- links?: BlogTocLink[];
30
- };
31
- };
32
- toc?: {
33
- links?: BlogTocLink[];
34
- };
35
- };
28
+ content: {
29
+ body?: {
30
+ toc?: {
31
+ links?: BlogTocLink[];
32
+ };
33
+ };
34
+ toc?: {
35
+ links?: BlogTocLink[];
36
+ };
37
+ };
36
38
  }>();
37
39
 
38
40
  const { t } = useI18n();
@@ -60,33 +62,33 @@ let restoreMobileTriggerFocus = true;
60
62
 
61
63
  // Create the headings list
62
64
  const headings = computed(() => {
63
- const tocLinks = props.content?.body?.toc?.links || props.content?.toc?.links || [];
64
-
65
- // Recursive function to flatten the nested links
66
- const flattenLinks = (links: BlogTocLink[]): TocHeading[] => {
67
- return links.flatMap(link => {
68
- // Extract the current heading
69
- const currentHeading = {
70
- id: link.id ?? '',
71
- text: link.text ?? '',
72
- level: link.depth ?? 0,
73
- number: ''
74
- };
75
-
76
- // Recursively flatten the children links
77
- const children = link.children?.length ? flattenLinks(link.children) : [];
78
- return [currentHeading, ...children];
79
- });
80
- };
81
-
82
- // Keep h2 and h3 headings and number the main sections
83
- let sectionNumber = 0;
84
- return flattenLinks(tocLinks)
85
- .filter(heading => heading.id && heading.text && heading.level >= 2 && heading.level <= 3)
86
- .map(heading => ({
87
- ...heading,
88
- number: heading.level === 2 ? String(++sectionNumber).padStart(2, '0') : ''
89
- }));
65
+ const tocLinks = props.content?.body?.toc?.links || props.content?.toc?.links || [];
66
+
67
+ // Recursive function to flatten the nested links
68
+ const flattenLinks = (links: BlogTocLink[]): TocHeading[] => {
69
+ return links.flatMap(link => {
70
+ // Extract the current heading
71
+ const currentHeading = {
72
+ id: link.id ?? '',
73
+ text: link.text ?? '',
74
+ level: link.depth ?? 0,
75
+ number: ''
76
+ };
77
+
78
+ // Recursively flatten the children links
79
+ const children = link.children?.length ? flattenLinks(link.children) : [];
80
+ return [currentHeading, ...children];
81
+ });
82
+ };
83
+
84
+ // Keep h2 and h3 headings and number the main sections
85
+ let sectionNumber = 0;
86
+ return flattenLinks(tocLinks)
87
+ .filter(heading => heading.id && heading.text && heading.level >= 2 && heading.level <= 3)
88
+ .map(heading => ({
89
+ ...heading,
90
+ number: heading.level === 2 ? String(++sectionNumber).padStart(2, '0') : ''
91
+ }));
90
92
  });
91
93
 
92
94
  // Number of main sections shown in the header
@@ -99,363 +101,363 @@ const tocSections = computed(() => t('uiVintage.blog.tableOfContentsSections', {
99
101
 
100
102
  // Prefer reduced motion for heading scroll
101
103
  const prefersReducedMotion = () => {
102
- return Boolean(window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches);
104
+ return Boolean(window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches);
103
105
  };
104
106
 
105
107
  // Resolve the heading that should be marked as the current chapter
106
108
  const resolveActiveHeadingId = () => {
107
- if (!headings.value.length) return '';
108
-
109
- // Filter the headings to only those currently visible in the viewport
110
- const visibleHeadings = headings.value.filter(heading => visibleHeadingIds.value.has(heading.id));
111
- if (visibleHeadings.length > 0) {
112
- return visibleHeadings[0].id;
113
- }
114
-
115
- // If no headings are currently visible, fall back to the first heading in the list
116
- let currentId = headings.value[0].id;
117
- for (const heading of headings.value) {
118
- const element = document.getElementById(heading.id);
119
- if (element && element.getBoundingClientRect().top <= ACTIVE_HEADING_OFFSET_PX) {
120
- currentId = heading.id;
121
- }
122
- }
123
-
124
- // Return the resolved current heading ID
125
- return currentId;
109
+ if (!headings.value.length) return '';
110
+
111
+ // Filter the headings to only those currently visible in the viewport
112
+ const visibleHeadings = headings.value.filter(heading => visibleHeadingIds.value.has(heading.id));
113
+ if (visibleHeadings.length > 0) {
114
+ return visibleHeadings[0].id;
115
+ }
116
+
117
+ // If no headings are currently visible, fall back to the first heading in the list
118
+ let currentId = headings.value[0].id;
119
+ for (const heading of headings.value) {
120
+ const element = document.getElementById(heading.id);
121
+ if (element && element.getBoundingClientRect().top <= ACTIVE_HEADING_OFFSET_PX) {
122
+ currentId = heading.id;
123
+ }
124
+ }
125
+
126
+ // Return the resolved current heading ID
127
+ return currentId;
126
128
  };
127
129
 
128
130
  // Update the active heading based on the current scroll position
129
131
  const updateActiveHeading = () => {
130
- activeHeadingId.value = resolveActiveHeadingId();
132
+ activeHeadingId.value = resolveActiveHeadingId();
131
133
  };
132
134
 
133
135
  // Track which headings currently intersect the reading window
134
136
  const handleHeadingIntersect = (entries: IntersectionObserverEntry[]) => {
135
- const nextVisibleIds = new Set(visibleHeadingIds.value);
136
-
137
- // Update the set of visible heading IDs
138
- for (const entry of entries) {
139
- if (entry.isIntersecting) {
140
- nextVisibleIds.add(entry.target.id);
141
- } else {
142
- nextVisibleIds.delete(entry.target.id);
143
- }
144
- }
145
-
146
- // Apply the updated set
147
- visibleHeadingIds.value = nextVisibleIds;
148
- updateActiveHeading();
137
+ const nextVisibleIds = new Set(visibleHeadingIds.value);
138
+
139
+ // Update the set of visible heading IDs
140
+ for (const entry of entries) {
141
+ if (entry.isIntersecting) {
142
+ nextVisibleIds.add(entry.target.id);
143
+ } else {
144
+ nextVisibleIds.delete(entry.target.id);
145
+ }
146
+ }
147
+
148
+ // Apply the updated set
149
+ visibleHeadingIds.value = nextVisibleIds;
150
+ updateActiveHeading();
149
151
  };
150
152
 
151
153
  // Disconnect the heading observer and clear any pending retries
152
154
  const disconnectHeadingObserver = () => {
153
- headingObserver?.disconnect();
154
- headingObserver = null;
155
- window.clearTimeout(headingRetryTimeout);
155
+ headingObserver?.disconnect();
156
+ headingObserver = null;
157
+ window.clearTimeout(headingRetryTimeout);
156
158
  };
157
159
 
158
160
  // Bind the heading observer to track which headings are visible in the viewport
159
161
  const bindHeadingObserver = () => {
160
- disconnectHeadingObserver();
161
-
162
- // If there are no headings, clear the active heading and visible heading IDs
163
- if (!headings.value.length) {
164
- activeHeadingId.value = '';
165
- visibleHeadingIds.value = new Set();
166
- return true;
167
- }
168
-
169
- // Get the DOM elements corresponding to the headings
170
- const elements = headings.value
171
- .map(heading => document.getElementById(heading.id))
172
- .filter((element): element is HTMLElement => Boolean(element));
173
-
174
- // Content renderer mounts after this component, so headings may not exist yet
175
- if (!elements.length) return false;
176
-
177
- headingObserver = new IntersectionObserver(handleHeadingIntersect, {
178
- root: null,
179
- rootMargin: '-20% 0px -65% 0px',
180
- threshold: [0, 1]
181
- });
182
-
183
- for (const element of elements) {
184
- headingObserver.observe(element);
185
- }
186
-
187
- if (!activeHeadingId.value) {
188
- activeHeadingId.value = headings.value[0].id;
189
- }
190
-
191
- updateActiveHeading();
192
- return true;
162
+ disconnectHeadingObserver();
163
+
164
+ // If there are no headings, clear the active heading and visible heading IDs
165
+ if (!headings.value.length) {
166
+ activeHeadingId.value = '';
167
+ visibleHeadingIds.value = new Set();
168
+ return true;
169
+ }
170
+
171
+ // Get the DOM elements corresponding to the headings
172
+ const elements = headings.value
173
+ .map(heading => document.getElementById(heading.id))
174
+ .filter((element): element is HTMLElement => Boolean(element));
175
+
176
+ // Content renderer mounts after this component, so headings may not exist yet
177
+ if (!elements.length) return false;
178
+
179
+ headingObserver = new IntersectionObserver(handleHeadingIntersect, {
180
+ root: null,
181
+ rootMargin: '-20% 0px -65% 0px',
182
+ threshold: [0, 1]
183
+ });
184
+
185
+ for (const element of elements) {
186
+ headingObserver.observe(element);
187
+ }
188
+
189
+ if (!activeHeadingId.value) {
190
+ activeHeadingId.value = headings.value[0].id;
191
+ }
192
+
193
+ updateActiveHeading();
194
+ return true;
193
195
  };
194
196
 
195
197
  // Retry binding until the content renderer has mounted its headings
196
198
  const scheduleHeadingObserver = (attempt = 0) => {
197
- nextTick(() => {
198
- if (bindHeadingObserver()) return;
199
- if (attempt >= MAX_HEADING_OBSERVER_RETRIES) return;
200
-
201
- headingRetryTimeout = window.setTimeout(() => {
202
- scheduleHeadingObserver(attempt + 1);
203
- }, HEADING_OBSERVER_RETRY_MS);
204
- });
199
+ nextTick(() => {
200
+ if (bindHeadingObserver()) return;
201
+ if (attempt >= MAX_HEADING_OBSERVER_RETRIES) return;
202
+
203
+ headingRetryTimeout = window.setTimeout(() => {
204
+ scheduleHeadingObserver(attempt + 1);
205
+ }, HEADING_OBSERVER_RETRY_MS);
206
+ });
205
207
  };
206
208
 
207
209
  // Disconnect the observer that controls the mobile trigger visibility
208
210
  const disconnectSentinelObserver = () => {
209
- sentinelObserver?.disconnect();
210
- sentinelObserver = null;
211
+ sentinelObserver?.disconnect();
212
+ sentinelObserver = null;
211
213
  };
212
214
 
213
215
  // Show the mobile trigger after the in-flow accordion leaves the viewport
214
216
  const bindSentinelObserver = () => {
215
- disconnectSentinelObserver();
216
-
217
- if (!inlineRef.value) {
218
- isDocked.value = false;
219
- return;
220
- }
221
-
222
- // Show the mobile control once the in-flow accordion has scrolled completely out of view
223
- sentinelObserver = new IntersectionObserver(([entry]) => {
224
- const hasScrolledPast = !entry.isIntersecting && entry.boundingClientRect.top < 0;
225
- isDocked.value = hasScrolledPast;
226
- }, {
227
- threshold: 0
228
- });
229
-
230
- sentinelObserver.observe(inlineRef.value);
217
+ disconnectSentinelObserver();
218
+
219
+ if (!inlineRef.value) {
220
+ isDocked.value = false;
221
+ return;
222
+ }
223
+
224
+ // Show the mobile control once the in-flow accordion has scrolled completely out of view
225
+ sentinelObserver = new IntersectionObserver(([entry]) => {
226
+ const hasScrolledPast = !entry.isIntersecting && entry.boundingClientRect.top < 0;
227
+ isDocked.value = hasScrolledPast;
228
+ }, {
229
+ threshold: 0
230
+ });
231
+
232
+ sentinelObserver.observe(inlineRef.value);
231
233
  };
232
234
 
233
235
  // Keep the mobile trigger state in sync with the viewport breakpoint
234
236
  const handleDesktopMediaChange = (event: MediaQueryList | MediaQueryListEvent) => {
235
- isDesktop.value = event.matches;
237
+ isDesktop.value = event.matches;
236
238
 
237
- if (event.matches) {
238
- isMobileOpen.value = false;
239
- }
239
+ if (event.matches) {
240
+ isMobileOpen.value = false;
241
+ }
240
242
 
241
- nextTick(() => {
242
- bindSentinelObserver();
243
- });
243
+ nextTick(() => {
244
+ bindSentinelObserver();
245
+ });
244
246
  };
245
247
 
246
248
  // Synchronize the mobile sheet state and restore focus after dismissal
247
249
  const handleMobileOpenChange = (open: boolean) => {
248
- isMobileOpen.value = open;
250
+ isMobileOpen.value = open;
249
251
 
250
- // Return focus to the persistent control when the overlay is dismissed
251
- if (!open && restoreMobileTriggerFocus) {
252
- mobileTriggerRef.value?.focus();
253
- }
252
+ // Return focus to the persistent control when the overlay is dismissed
253
+ if (!open && restoreMobileTriggerFocus) {
254
+ mobileTriggerRef.value?.focus();
255
+ }
254
256
 
255
- restoreMobileTriggerFocus = true;
257
+ restoreMobileTriggerFocus = true;
256
258
  };
257
259
 
258
260
  // Scroll to a heading and optionally close the mobile sheet first
259
261
  const scrollToHeading = (id: string, options: { closeMobile?: boolean } = {}) => {
260
- const runScroll = () => {
261
- const element = document.getElementById(id);
262
- if (!element) return;
263
-
264
- if (!element.hasAttribute('tabindex')) {
265
- element.setAttribute('tabindex', '-1');
266
- }
267
-
268
- element.scrollIntoView({
269
- behavior: prefersReducedMotion() ? 'auto' : 'smooth',
270
- block: 'start',
271
- inline: 'nearest'
272
- });
273
- element.focus({ preventScroll: true });
274
- activeHeadingId.value = id;
275
-
276
- if (window.location.hash !== `#${id}`) {
277
- history.replaceState(null, '', `#${id}`);
278
- }
279
- };
280
- if (options.closeMobile && isMobileOpen.value) {
281
- // The heading takes focus after the scroll, so skip the trigger focus restore for this close only
282
- restoreMobileTriggerFocus = false;
283
- mobileTriggerRef.value?.focus();
284
- isMobileOpen.value = false;
285
- nextTick(() => {
286
- restoreMobileTriggerFocus = true;
287
- });
288
- window.clearTimeout(mobileScrollTimeout);
289
- mobileScrollTimeout = window.setTimeout(runScroll, MOBILE_CLOSE_SCROLL_DELAY_MS);
290
- return;
291
- }
292
-
293
- runScroll();
262
+ const runScroll = () => {
263
+ const element = document.getElementById(id);
264
+ if (!element) return;
265
+
266
+ if (!element.hasAttribute('tabindex')) {
267
+ element.setAttribute('tabindex', '-1');
268
+ }
269
+
270
+ element.scrollIntoView({
271
+ behavior: prefersReducedMotion() ? 'auto' : 'smooth',
272
+ block: 'start',
273
+ inline: 'nearest'
274
+ });
275
+ element.focus({ preventScroll: true });
276
+ activeHeadingId.value = id;
277
+
278
+ if (window.location.hash !== `#${id}`) {
279
+ history.replaceState(null, '', `#${id}`);
280
+ }
281
+ };
282
+ if (options.closeMobile && isMobileOpen.value) {
283
+ // The heading takes focus after the scroll, so skip the trigger focus restore for this close only
284
+ restoreMobileTriggerFocus = false;
285
+ mobileTriggerRef.value?.focus();
286
+ isMobileOpen.value = false;
287
+ nextTick(() => {
288
+ restoreMobileTriggerFocus = true;
289
+ });
290
+ window.clearTimeout(mobileScrollTimeout);
291
+ mobileScrollTimeout = window.setTimeout(runScroll, MOBILE_CLOSE_SCROLL_DELAY_MS);
292
+ return;
293
+ }
294
+
295
+ runScroll();
294
296
  };
295
297
 
296
298
  // Open the mobile table of contents sheet
297
299
  const openMobileToc = () => {
298
- isMobileOpen.value = true;
300
+ isMobileOpen.value = true;
299
301
  };
300
302
 
301
303
  // Get classes for heading button
302
304
  const getHeadingButtonClasses = (level: number, isActive: boolean) => {
303
- // Base classes for all titles
304
- const baseClasses = 'flex w-full cursor-pointer gap-3 rounded-r py-1.5 pr-2 text-left text-xs outline-none transition-colors duration-200 hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/45 md:text-sm';
305
+ // Base classes for all titles
306
+ const baseClasses = 'flex w-full cursor-pointer gap-3 rounded-r py-1.5 pr-2 text-left text-xs outline-none transition-colors duration-200 hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/45 md:text-sm';
305
307
 
306
- // Main sections carry a number, subsections align with their text
307
- const levelClasses = level === 2 ? 'pl-4 font-medium' : 'pl-12 md:text-xs';
308
+ // Main sections carry a number, subsections align with their text
309
+ const levelClasses = level === 2 ? 'pl-4 font-medium' : 'pl-12 md:text-xs';
308
310
 
309
- // Add classes based on the active state
310
- let stateClasses = level === 2 ? 'text-foreground/80' : 'text-muted-foreground';
311
- if (isActive) {
312
- stateClasses = 'font-semibold text-foreground';
313
- }
311
+ // Add classes based on the active state
312
+ let stateClasses = level === 2 ? 'text-foreground/80' : 'text-muted-foreground';
313
+ if (isActive) {
314
+ stateClasses = 'font-semibold text-foreground';
315
+ }
314
316
 
315
- // Return combined classes
316
- return [baseClasses, levelClasses, stateClasses];
317
+ // Return combined classes
318
+ return [baseClasses, levelClasses, stateClasses];
317
319
  };
318
320
 
319
321
  // Watch for changes in the headings and update the visible heading IDs and active heading ID accordingly
320
322
  watch(headings, () => {
321
- visibleHeadingIds.value = new Set();
322
- activeHeadingId.value = headings.value[0]?.id ?? '';
323
- scheduleHeadingObserver();
323
+ visibleHeadingIds.value = new Set();
324
+ activeHeadingId.value = headings.value[0]?.id ?? '';
325
+ scheduleHeadingObserver();
324
326
  });
325
327
 
326
328
  // Watch for changes in the inline table of contents reference and bind the sentinel observer accordingly
327
329
  watch(inlineRef, () => {
328
- bindSentinelObserver();
330
+ bindSentinelObserver();
329
331
  }, { flush: 'post' });
330
332
 
331
333
  // On component mounted
332
334
  onMounted(() => {
333
- desktopMediaQuery = window.matchMedia(DESKTOP_MEDIA_QUERY);
334
- handleDesktopMediaChange(desktopMediaQuery);
335
- desktopMediaQuery.addEventListener('change', handleDesktopMediaChange);
335
+ desktopMediaQuery = window.matchMedia(DESKTOP_MEDIA_QUERY);
336
+ handleDesktopMediaChange(desktopMediaQuery);
337
+ desktopMediaQuery.addEventListener('change', handleDesktopMediaChange);
336
338
 
337
- const hashId = window.location.hash.replace(/^#/, '');
338
- if (hashId && headings.value.some(heading => heading.id === hashId)) {
339
- activeHeadingId.value = hashId;
340
- }
339
+ const hashId = window.location.hash.replace(/^#/, '');
340
+ if (hashId && headings.value.some(heading => heading.id === hashId)) {
341
+ activeHeadingId.value = hashId;
342
+ }
341
343
 
342
- scheduleHeadingObserver();
344
+ scheduleHeadingObserver();
343
345
  });
344
346
 
345
347
  // On component unmounted
346
348
  onUnmounted(() => {
347
- desktopMediaQuery?.removeEventListener('change', handleDesktopMediaChange);
348
- disconnectSentinelObserver();
349
- disconnectHeadingObserver();
350
- window.clearTimeout(mobileScrollTimeout);
349
+ desktopMediaQuery?.removeEventListener('change', handleDesktopMediaChange);
350
+ disconnectSentinelObserver();
351
+ disconnectHeadingObserver();
352
+ window.clearTimeout(mobileScrollTimeout);
351
353
  });
352
354
  </script>
353
355
 
354
356
  <template>
355
- <div v-if="headings.length > 0">
356
- <!-- In-flow accordion; replaced by the mobile control after it scrolls away -->
357
- <div ref="inlineRef" class="mb-6" :aria-hidden="isDocked" :inert="isDocked">
358
- <Accordion :title="tocTitle" :icon="Bookmark01Icon">
359
- <!-- Section count -->
360
- <template #trailing>
361
- <span class="shrink-0 text-xs text-muted-foreground">{{ tocSections }}</span>
362
- </template>
363
-
364
- <nav :aria-label="tocTitle">
365
- <ol>
366
- <li v-for="heading in headings" :key="`inline-${heading.id}`" class="relative border-l border-border">
367
- <!-- Active marker on the track -->
368
- <span aria-hidden="true" :class="['absolute inset-y-1 -left-px w-0.5 origin-center bg-primary transition-transform duration-300 ease-spring', heading.id === activeHeadingId ? 'scale-y-100' : 'scale-y-0']" />
369
- <button type="button" :class="getHeadingButtonClasses(heading.level, heading.id === activeHeadingId)" :aria-current="heading.id === activeHeadingId ? 'location' : undefined" @click="scrollToHeading(heading.id)">
370
- <span v-if="heading.number" :class="['w-5 shrink-0 text-[11px] leading-5 tabular-nums transition-colors', heading.id === activeHeadingId ? 'text-primary' : 'text-muted-foreground']">{{ heading.number }}</span>
371
- <span>{{ heading.text }}</span>
372
- </button>
373
- </li>
374
- </ol>
375
- </nav>
376
- </Accordion>
377
- </div>
378
-
379
- <Teleport to="body">
380
- <!-- Mobile persistent control -->
381
- <Transition name="toc-tab">
382
- <button v-if="!isDesktop && isDocked" ref="mobileTriggerRef" type="button" class="group/toc-tab fixed top-4/5 left-0 z-20 flex h-16 w-7 -translate-y-1/2 items-center justify-center rounded-r border border-l-0 border-border bg-card text-primary shadow-elevated-sm transition-[width] duration-300 ease-out-expo hover:w-8 focus-visible:z-30 focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/45" :aria-label="t('uiVintage.blog.openTableOfContents')" :aria-expanded="isMobileOpen" aria-haspopup="dialog" aria-controls="toc-mobile-overlay" @click="openMobileToc">
383
- <HugeiconsIcon :icon="ArrowRight01Icon" class="size-3.5 transition-[translate] duration-300 ease-spring group-hover/toc-tab:translate-x-0.5" aria-hidden="true" />
384
- </button>
385
- </Transition>
386
-
387
- <!-- Mobile full-screen overlay -->
388
- <Sheet :open="isMobileOpen" @update:open="handleMobileOpenChange">
389
- <SheetContent id="toc-mobile-overlay" side="left" class="flex h-full w-full max-w-none flex-col gap-0 border-0 bg-card p-0 data-[state=open]:[animation-duration:320ms] data-[state=open]:[animation-timing-function:var(--ease-out-expo)] sm:max-w-none">
390
- <SheetHeader class="flex-row items-center justify-between gap-4 border-b border-border p-5">
391
- <!-- Mobile table of contents header -->
392
- <div class="flex min-w-0 items-center gap-3">
393
- <!-- Mobile table of contents icon -->
394
- <div class="flex size-7 shrink-0 items-center justify-center rounded border border-primary/30 text-primary">
395
- <HugeiconsIcon :icon="Bookmark01Icon" class="size-3.5" />
396
- </div>
397
-
398
- <!-- Mobile table of contents title and description -->
399
- <div class="min-w-0">
400
- <SheetTitle class="text-sm md:text-base! font-semibold text-foreground">
401
- {{ tocTitle }}
402
- </SheetTitle>
403
- <SheetDescription class="sr-only">
404
- {{ tocDescription }}
405
- </SheetDescription>
406
- <p aria-hidden="true" class="text-xs text-muted-foreground">
407
- {{ tocSections }}
408
- </p>
409
- </div>
410
- </div>
411
-
412
- <!-- Close button for the mobile table of contents -->
413
- <SheetClose as-child>
414
- <Button type="button" variant="ghost" size="icon-sm" :aria-label="t('uiVintage.blog.closeTableOfContents')">
415
- <HugeiconsIcon :icon="Cancel01Icon" class="size-4" />
416
- </Button>
417
- </SheetClose>
418
- </SheetHeader>
419
-
420
- <!-- Mobile table of contents navigation -->
421
- <nav :aria-label="tocTitle" class="flex-1 overflow-y-auto p-5">
422
- <ol>
423
- <li v-for="heading in headings" :key="`mobile-${heading.id}`" class="relative border-l border-border">
424
- <!-- Active marker on the track -->
425
- <span aria-hidden="true" :class="['absolute inset-y-1 -left-px w-0.5 bg-primary', heading.id === activeHeadingId ? 'opacity-100' : 'opacity-0']" />
426
- <button type="button" :class="getHeadingButtonClasses(heading.level, heading.id === activeHeadingId)" :aria-current="heading.id === activeHeadingId ? 'location' : undefined" @click="scrollToHeading(heading.id, { closeMobile: true })">
427
- <span v-if="heading.number" :class="['w-5 shrink-0 text-[11px] leading-5 tabular-nums', heading.id === activeHeadingId ? 'text-primary' : 'text-muted-foreground']">{{ heading.number }}</span>
428
- <span>{{ heading.text }}</span>
429
- </button>
430
- </li>
431
- </ol>
432
- </nav>
433
- </SheetContent>
434
- </Sheet>
435
- </Teleport>
436
- </div>
357
+ <div v-if="headings.length > 0">
358
+ <!-- In-flow accordion; replaced by the mobile control after it scrolls away -->
359
+ <div ref="inlineRef" class="mb-6" :aria-hidden="isDocked" :inert="isDocked">
360
+ <Accordion :title="tocTitle" :icon="Bookmark01Icon">
361
+ <!-- Section count -->
362
+ <template #trailing>
363
+ <span class="shrink-0 text-xs text-muted-foreground">{{ tocSections }}</span>
364
+ </template>
365
+
366
+ <nav :aria-label="tocTitle">
367
+ <ol>
368
+ <li v-for="heading in headings" :key="`inline-${heading.id}`" class="relative border-l border-border">
369
+ <!-- Active marker on the track -->
370
+ <span aria-hidden="true" :class="['absolute inset-y-1 -left-px w-0.5 origin-center bg-primary transition-transform duration-300 ease-spring', heading.id === activeHeadingId ? 'scale-y-100' : 'scale-y-0']" />
371
+ <button type="button" :class="getHeadingButtonClasses(heading.level, heading.id === activeHeadingId)" :aria-current="heading.id === activeHeadingId ? 'location' : undefined" @click="scrollToHeading(heading.id)">
372
+ <span v-if="heading.number" :class="['w-5 shrink-0 text-[11px] leading-5 tabular-nums transition-colors', heading.id === activeHeadingId ? 'text-primary' : 'text-muted-foreground']">{{ heading.number }}</span>
373
+ <span>{{ heading.text }}</span>
374
+ </button>
375
+ </li>
376
+ </ol>
377
+ </nav>
378
+ </Accordion>
379
+ </div>
380
+
381
+ <Teleport to="body">
382
+ <!-- Mobile persistent control -->
383
+ <Transition name="toc-tab">
384
+ <button v-if="!isDesktop && isDocked" ref="mobileTriggerRef" type="button" class="group/toc-tab fixed top-4/5 left-0 z-20 flex h-16 w-7 -translate-y-1/2 items-center justify-center rounded-r border border-l-0 border-border bg-card text-primary shadow-elevated-sm transition-[width] duration-300 ease-out-expo hover:w-8 focus-visible:z-30 focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/45" :aria-label="t('uiVintage.blog.openTableOfContents')" :aria-expanded="isMobileOpen" aria-haspopup="dialog" aria-controls="toc-mobile-overlay" @click="openMobileToc">
385
+ <HugeiconsIcon :icon="ArrowRight01Icon" class="size-3.5 transition-[translate] duration-300 ease-spring group-hover/toc-tab:translate-x-0.5" aria-hidden="true" />
386
+ </button>
387
+ </Transition>
388
+
389
+ <!-- Mobile full-screen overlay -->
390
+ <Sheet :open="isMobileOpen" @update:open="handleMobileOpenChange">
391
+ <SheetContent id="toc-mobile-overlay" side="left" class="flex h-full w-full max-w-none flex-col gap-0 border-0 bg-card p-0 data-[state=open]:[animation-duration:320ms] data-[state=open]:[animation-timing-function:var(--ease-out-expo)] sm:max-w-none">
392
+ <SheetHeader class="flex-row items-center justify-between gap-4 border-b border-border p-5">
393
+ <!-- Mobile table of contents header -->
394
+ <div class="flex min-w-0 items-center gap-3">
395
+ <!-- Mobile table of contents icon -->
396
+ <div class="flex size-7 shrink-0 items-center justify-center rounded border border-primary/30 text-primary">
397
+ <HugeiconsIcon :icon="Bookmark01Icon" class="size-3.5" />
398
+ </div>
399
+
400
+ <!-- Mobile table of contents title and description -->
401
+ <div class="min-w-0">
402
+ <SheetTitle class="text-sm md:text-base! font-semibold text-foreground">
403
+ {{ tocTitle }}
404
+ </SheetTitle>
405
+ <SheetDescription class="sr-only">
406
+ {{ tocDescription }}
407
+ </SheetDescription>
408
+ <p aria-hidden="true" class="text-xs text-muted-foreground">
409
+ {{ tocSections }}
410
+ </p>
411
+ </div>
412
+ </div>
413
+
414
+ <!-- Close button for the mobile table of contents -->
415
+ <SheetClose as-child>
416
+ <Button type="button" variant="ghost" size="icon-sm" :aria-label="t('uiVintage.blog.closeTableOfContents')">
417
+ <HugeiconsIcon :icon="Cancel01Icon" class="size-4" />
418
+ </Button>
419
+ </SheetClose>
420
+ </SheetHeader>
421
+
422
+ <!-- Mobile table of contents navigation -->
423
+ <nav :aria-label="tocTitle" class="flex-1 overflow-y-auto p-5">
424
+ <ol>
425
+ <li v-for="heading in headings" :key="`mobile-${heading.id}`" class="relative border-l border-border">
426
+ <!-- Active marker on the track -->
427
+ <span aria-hidden="true" :class="['absolute inset-y-1 -left-px w-0.5 bg-primary', heading.id === activeHeadingId ? 'opacity-100' : 'opacity-0']" />
428
+ <button type="button" :class="getHeadingButtonClasses(heading.level, heading.id === activeHeadingId)" :aria-current="heading.id === activeHeadingId ? 'location' : undefined" @click="scrollToHeading(heading.id, { closeMobile: true })">
429
+ <span v-if="heading.number" :class="['w-5 shrink-0 text-[11px] leading-5 tabular-nums', heading.id === activeHeadingId ? 'text-primary' : 'text-muted-foreground']">{{ heading.number }}</span>
430
+ <span>{{ heading.text }}</span>
431
+ </button>
432
+ </li>
433
+ </ol>
434
+ </nav>
435
+ </SheetContent>
436
+ </Sheet>
437
+ </Teleport>
438
+ </div>
437
439
  </template>
438
440
 
439
441
  <style scoped>
440
442
  .toc-tab-enter-active {
441
- transition: opacity 0.3s var(--ease-out-expo), transform 0.32s var(--ease-out-expo);
443
+ transition: opacity 0.3s var(--ease-out-expo), transform 0.32s var(--ease-out-expo);
442
444
  }
443
445
 
444
446
  .toc-tab-leave-active {
445
- transition: opacity 0.15s var(--ease-snappy), transform 0.15s var(--ease-snappy);
447
+ transition: opacity 0.15s var(--ease-snappy), transform 0.15s var(--ease-snappy);
446
448
  }
447
449
 
448
450
  .toc-tab-enter-from,
449
451
  .toc-tab-leave-to {
450
- opacity: 0;
451
- transform: translateX(-100%);
452
+ opacity: 0;
453
+ transform: translateX(-100%);
452
454
  }
453
455
 
454
456
  @media (prefers-reduced-motion: reduce) {
455
457
 
456
- .toc-tab-enter-active,
457
- .toc-tab-leave-active {
458
- transition: none;
459
- }
458
+ .toc-tab-enter-active,
459
+ .toc-tab-leave-active {
460
+ transition: none;
461
+ }
460
462
  }
461
463
  </style>