@brumaombra/ui-vintage 0.9.1 → 0.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/COMPONENTS.md +102 -2
- package/module.mjs +43 -43
- package/package.json +175 -175
- package/src/components/background-grid/BackgroundGrid.vue +4 -4
- package/src/components/blog/AllPostsSection.vue +33 -33
- package/src/components/blog/BlogCategoriesPage.vue +46 -0
- package/src/components/blog/BlogContentRenderer.vue +111 -111
- package/src/components/blog/BlogFAQSection.vue +19 -19
- package/src/components/blog/BlogHeaderSection.vue +80 -80
- package/src/components/blog/BlogIndexPage.vue +118 -0
- package/src/components/blog/BlogInfoSection.vue +47 -47
- package/src/components/blog/BlogPageBreadcrumbs.vue +43 -0
- package/src/components/blog/BlogPostHeader.vue +135 -135
- package/src/components/blog/BlogPostPage.vue +116 -0
- package/src/components/blog/BlogPostsPage.vue +74 -0
- package/src/components/blog/BlogProgressBar.vue +33 -33
- package/src/components/blog/BlogSectionTitle.vue +22 -22
- package/src/components/blog/BlogTagsPage.vue +46 -0
- package/src/components/blog/BlogTagsSection.vue +25 -25
- package/src/components/blog/BlogViewAllLink.vue +18 -0
- package/src/components/blog/CategoriesList.vue +14 -14
- package/src/components/blog/CategoriesSection.vue +11 -11
- package/src/components/blog/CategoryCard.vue +25 -25
- package/src/components/blog/HeaderCarousel.vue +97 -97
- package/src/components/blog/PostCard.vue +33 -33
- package/src/components/blog/PostsList.vue +15 -15
- package/src/components/blog/SocialShareSidebar.vue +84 -84
- package/src/components/blog/TableOfContents.vue +307 -307
- package/src/components/blog/blogPageUtils.ts +98 -0
- package/src/components/blog/index.ts +12 -1
- package/src/components/busy-indicator/BusyIndicator.vue +7 -7
- package/src/components/busy-indicator/busy-indicator-state.ts +56 -56
- package/src/components/card-grid/CardGrid.vue +34 -34
- package/src/components/chip/Chip.vue +15 -15
- package/src/components/confirm-dialog/ConfirmDialog.vue +50 -50
- package/src/components/confirm-dialog/confirm-dialog-state.ts +131 -131
- package/src/components/content/BlogList.vue +30 -30
- package/src/components/content/BlogTable.vue +36 -36
- package/src/components/content/CodeBlock.vue +136 -136
- package/src/components/content/Flow.vue +60 -60
- package/src/components/content/ProseHr.vue +15 -15
- package/src/components/content/ProsePre.vue +12 -12
- package/src/components/content/Terminal.vue +59 -59
- package/src/components/cookie-consent/CookieConsent.vue +36 -36
- package/src/components/cookie-consent/useCookieConsent.ts +37 -37
- package/src/components/dashboard-shell/DashboardShell.vue +171 -171
- package/src/components/data-list/DataList.vue +6 -6
- package/src/components/data-list/DataListItem.vue +4 -4
- package/src/components/data-list/DataListLabel.vue +4 -4
- package/src/components/data-list/DataListText.vue +4 -4
- package/src/components/data-list/DataListValue.vue +4 -4
- package/src/components/empty-state-card/EmptyStateCard.vue +40 -40
- package/src/components/error-page/ErrorPage.vue +72 -72
- package/src/components/info-card/InfoCard.vue +31 -31
- package/src/components/landing/landing-content/LandingContent.vue +4 -4
- package/src/components/landing/landing-footer/LandingFooter.vue +128 -128
- package/src/components/landing/landing-navbar/LandingNavbar.vue +50 -50
- package/src/components/landing/landing-shell/LandingShell.vue +36 -36
- package/src/components/language-flag/LanguageFlag.vue +25 -25
- package/src/components/language-flag/flags/ChineseFlag.vue +8 -8
- package/src/components/language-flag/flags/EnglishFlag.vue +7 -7
- package/src/components/language-flag/flags/FrenchFlag.vue +5 -5
- package/src/components/language-flag/flags/GermanFlag.vue +5 -5
- package/src/components/language-flag/flags/ItalianFlag.vue +7 -7
- package/src/components/language-flag/flags/JapaneseFlag.vue +4 -4
- package/src/components/language-flag/flags/PortugueseFlag.vue +57 -57
- package/src/components/language-flag/flags/RussianFlag.vue +5 -5
- package/src/components/language-flag/flags/SpanishFlag.vue +4 -4
- package/src/components/language-selector/LanguageSelector.vue +76 -76
- package/src/components/load-more-button/LoadMoreButton.vue +17 -17
- package/src/components/loading-state-card/LoadingStateCard.vue +28 -28
- package/src/components/message-dialog/MessageDialog.vue +59 -59
- package/src/components/message-dialog/message-dialog-state.ts +105 -105
- package/src/components/message-toast/MessageToast.vue +149 -149
- package/src/components/message-toast/message-toast-state.ts +146 -146
- package/src/components/page-header/PageHeader.vue +34 -34
- package/src/components/progress-component/ProgressComponent.vue +33 -33
- package/src/components/single-value-card/SingleValueCard.vue +57 -57
- package/src/components/slider-form-component/SliderFormComponent.vue +36 -36
- package/src/components/stat-strip/StatStrip.vue +22 -22
- package/src/components/switch-form-component/SwitchFormComponent.vue +20 -20
- package/src/components/text-link/TextLink.vue +14 -14
- package/src/components/theme-selector/ThemeSelector.vue +61 -61
- package/src/components/theme-selector/theme-transition.ts +63 -63
- package/src/components/ui/accordion/Accordion.vue +38 -38
- package/src/components/ui/alert/Alert.vue +5 -5
- package/src/components/ui/alert/AlertDescription.vue +4 -4
- package/src/components/ui/alert/AlertTitle.vue +4 -4
- package/src/components/ui/alert/index.ts +2 -2
- package/src/components/ui/alert-dialog/AlertDialog.vue +3 -3
- package/src/components/ui/alert-dialog/AlertDialogAction.vue +3 -3
- package/src/components/ui/alert-dialog/AlertDialogCancel.vue +3 -3
- package/src/components/ui/alert-dialog/AlertDialogContent.vue +7 -7
- package/src/components/ui/alert-dialog/AlertDialogDescription.vue +3 -3
- package/src/components/ui/alert-dialog/AlertDialogFooter.vue +4 -4
- package/src/components/ui/alert-dialog/AlertDialogHeader.vue +4 -4
- package/src/components/ui/alert-dialog/AlertDialogTitle.vue +3 -3
- package/src/components/ui/alert-dialog/AlertDialogTrigger.vue +3 -3
- package/src/components/ui/animated-number/AnimatedNumber.vue +49 -48
- package/src/components/ui/avatar/Avatar.vue +28 -28
- package/src/components/ui/avatar/AvatarFallback.vue +6 -6
- package/src/components/ui/avatar/AvatarGroup.vue +40 -40
- package/src/components/ui/avatar/AvatarImage.vue +3 -3
- package/src/components/ui/avatar/index.ts +17 -17
- package/src/components/ui/badge/Badge.vue +20 -20
- package/src/components/ui/breadcrumb/Breadcrumb.vue +4 -4
- package/src/components/ui/breadcrumb/BreadcrumbEllipsis.vue +9 -9
- package/src/components/ui/breadcrumb/BreadcrumbItem.vue +4 -4
- package/src/components/ui/breadcrumb/BreadcrumbLink.vue +4 -4
- package/src/components/ui/breadcrumb/BreadcrumbList.vue +4 -4
- package/src/components/ui/breadcrumb/BreadcrumbPage.vue +4 -4
- package/src/components/ui/breadcrumb/BreadcrumbSeparator.vue +4 -4
- package/src/components/ui/busy/Busy.vue +33 -33
- package/src/components/ui/button/Button.vue +23 -23
- package/src/components/ui/button/index.ts +56 -56
- package/src/components/ui/calendar/Calendar.vue +138 -138
- package/src/components/ui/calendar/CalendarCell.vue +7 -7
- package/src/components/ui/calendar/CalendarCellTrigger.vue +18 -18
- package/src/components/ui/calendar/CalendarGrid.vue +3 -3
- package/src/components/ui/calendar/CalendarGridBody.vue +3 -3
- package/src/components/ui/calendar/CalendarGridHead.vue +3 -3
- package/src/components/ui/calendar/CalendarGridRow.vue +3 -3
- package/src/components/ui/calendar/CalendarHeadCell.vue +7 -7
- package/src/components/ui/calendar/CalendarHeader.vue +7 -7
- package/src/components/ui/calendar/CalendarHeading.vue +6 -6
- package/src/components/ui/calendar/CalendarNextButton.vue +15 -15
- package/src/components/ui/calendar/CalendarPrevButton.vue +15 -15
- package/src/components/ui/calendar/index.ts +4 -4
- package/src/components/ui/card/Card.vue +20 -20
- package/src/components/ui/card/CardAction.vue +8 -8
- package/src/components/ui/card/CardContent.vue +4 -4
- package/src/components/ui/card/CardDescription.vue +12 -12
- package/src/components/ui/card/CardFooter.vue +4 -4
- package/src/components/ui/card/CardHeader.vue +8 -8
- package/src/components/ui/card/CardText.vue +8 -8
- package/src/components/ui/card/CardTitle.vue +8 -8
- package/src/components/ui/checkbox/Checkbox.vue +27 -27
- package/src/components/ui/collapsible/Collapsible.vue +3 -3
- package/src/components/ui/collapsible/CollapsibleContent.vue +3 -3
- package/src/components/ui/collapsible/CollapsibleTrigger.vue +3 -3
- package/src/components/ui/combobox/Combobox.vue +4 -4
- package/src/components/ui/combobox/ComboboxAnchor.vue +16 -16
- package/src/components/ui/combobox/ComboboxEmpty.vue +3 -3
- package/src/components/ui/combobox/ComboboxGroup.vue +9 -9
- package/src/components/ui/combobox/ComboboxInput.vue +6 -6
- package/src/components/ui/combobox/ComboboxItem.vue +17 -17
- package/src/components/ui/combobox/ComboboxList.vue +19 -19
- package/src/components/ui/combobox/ComboboxSelect.vue +77 -77
- package/src/components/ui/combobox/ComboboxSeparator.vue +1 -1
- package/src/components/ui/combobox/ComboboxTrigger.vue +10 -10
- package/src/components/ui/combobox/types.ts +5 -5
- package/src/components/ui/command/Command.vue +48 -48
- package/src/components/ui/command/CommandDialog.vue +14 -14
- package/src/components/ui/command/CommandEmpty.vue +4 -4
- package/src/components/ui/command/CommandGroup.vue +23 -23
- package/src/components/ui/command/CommandInput.vue +9 -9
- package/src/components/ui/command/CommandItem.vue +40 -40
- package/src/components/ui/command/CommandList.vue +9 -9
- package/src/components/ui/command/CommandSeparator.vue +3 -3
- package/src/components/ui/command/CommandShortcut.vue +8 -8
- package/src/components/ui/command/index.ts +11 -11
- package/src/components/ui/data-table/DataTable.vue +145 -145
- package/src/components/ui/data-table/index.ts +11 -11
- package/src/components/ui/date-picker/DatePicker.vue +31 -31
- package/src/components/ui/date-time-picker/DateTimePicker.vue +97 -97
- package/src/components/ui/dialog/Dialog.vue +3 -3
- package/src/components/ui/dialog/DialogClose.vue +3 -3
- package/src/components/ui/dialog/DialogContent.vue +21 -21
- package/src/components/ui/dialog/DialogDescription.vue +7 -7
- package/src/components/ui/dialog/DialogFooter.vue +8 -8
- package/src/components/ui/dialog/DialogHeader.vue +4 -4
- package/src/components/ui/dialog/DialogOverlay.vue +7 -7
- package/src/components/ui/dialog/DialogScrollContent.vue +32 -32
- package/src/components/ui/dialog/DialogTitle.vue +3 -3
- package/src/components/ui/dialog/DialogTrigger.vue +3 -3
- package/src/components/ui/dropdown-menu/DropdownMenu.vue +3 -3
- package/src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue +11 -11
- package/src/components/ui/dropdown-menu/DropdownMenuContent.vue +15 -15
- package/src/components/ui/dropdown-menu/DropdownMenuGroup.vue +3 -3
- package/src/components/ui/dropdown-menu/DropdownMenuItem.vue +12 -12
- package/src/components/ui/dropdown-menu/DropdownMenuLabel.vue +9 -9
- package/src/components/ui/dropdown-menu/DropdownMenuPortal.vue +3 -3
- package/src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue +3 -3
- package/src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue +11 -11
- package/src/components/ui/dropdown-menu/DropdownMenuSeparator.vue +1 -1
- package/src/components/ui/dropdown-menu/DropdownMenuShortcut.vue +4 -4
- package/src/components/ui/dropdown-menu/DropdownMenuSub.vue +3 -3
- package/src/components/ui/dropdown-menu/DropdownMenuSubContent.vue +15 -15
- package/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +15 -15
- package/src/components/ui/dropdown-menu/DropdownMenuTrigger.vue +3 -3
- package/src/components/ui/dropdown-menu/classes.ts +16 -16
- package/src/components/ui/field/Field.vue +5 -5
- package/src/components/ui/field/FieldContent.vue +8 -8
- package/src/components/ui/field/FieldDescription.vue +10 -10
- package/src/components/ui/field/FieldError.vue +39 -39
- package/src/components/ui/field/FieldGroup.vue +8 -8
- package/src/components/ui/field/FieldLabel.vue +10 -10
- package/src/components/ui/field/FieldLegend.vue +11 -11
- package/src/components/ui/field/FieldSeparator.vue +11 -11
- package/src/components/ui/field/FieldSet.vue +9 -9
- package/src/components/ui/field/FieldTitle.vue +8 -8
- package/src/components/ui/field/field-context.ts +36 -36
- package/src/components/ui/field/index.ts +22 -22
- package/src/components/ui/file-dropzone/FileDropzone.vue +144 -144
- package/src/components/ui/file-dropzone/index.ts +19 -19
- package/src/components/ui/hover-card/HoverCard.vue +5 -5
- package/src/components/ui/hover-card/HoverCardContent.vue +13 -13
- package/src/components/ui/hover-card/HoverCardTrigger.vue +3 -3
- package/src/components/ui/input/Input.vue +13 -13
- package/src/components/ui/kbd/Kbd.vue +10 -10
- package/src/components/ui/kbd/KbdGroup.vue +49 -49
- package/src/components/ui/label/Label.vue +7 -7
- package/src/components/ui/native-select/NativeSelect.vue +16 -16
- package/src/components/ui/native-select/NativeSelectOptGroup.vue +3 -3
- package/src/components/ui/native-select/NativeSelectOption.vue +3 -3
- package/src/components/ui/number-field/NumberField.vue +88 -88
- package/src/components/ui/pagination/Pagination.vue +4 -4
- package/src/components/ui/pagination/PaginationContent.vue +4 -4
- package/src/components/ui/pagination/PaginationEllipsis.vue +9 -9
- package/src/components/ui/pagination/PaginationFirst.vue +8 -8
- package/src/components/ui/pagination/PaginationItem.vue +14 -14
- package/src/components/ui/pagination/PaginationLast.vue +8 -8
- package/src/components/ui/pagination/PaginationNext.vue +8 -8
- package/src/components/ui/pagination/PaginationPrevious.vue +8 -8
- package/src/components/ui/pagination/SimplePagination.vue +25 -25
- package/src/components/ui/pin-input/PinInput.vue +20 -20
- package/src/components/ui/pin-input/PinInputInput.vue +17 -17
- package/src/components/ui/pin-input/context.ts +1 -1
- package/src/components/ui/popover/Popover.vue +3 -3
- package/src/components/ui/popover/PopoverAnchor.vue +3 -3
- package/src/components/ui/popover/PopoverContent.vue +12 -12
- package/src/components/ui/popover/PopoverTrigger.vue +3 -3
- package/src/components/ui/progress/Progress.vue +16 -16
- package/src/components/ui/radio-group/RadioGroup.vue +7 -7
- package/src/components/ui/radio-group/RadioGroupCard.vue +39 -39
- package/src/components/ui/radio-group/RadioGroupItem.vue +20 -20
- package/src/components/ui/scroll-area/ScrollArea.vue +14 -14
- package/src/components/ui/scroll-area/ScrollBar.vue +12 -12
- package/src/components/ui/select/Select.vue +3 -3
- package/src/components/ui/select/SelectContent.vue +25 -25
- package/src/components/ui/select/SelectGroup.vue +3 -3
- package/src/components/ui/select/SelectItem.vue +24 -24
- package/src/components/ui/select/SelectItemContent.vue +32 -32
- package/src/components/ui/select/SelectItemText.vue +3 -3
- package/src/components/ui/select/SelectLabel.vue +3 -3
- package/src/components/ui/select/SelectScrollDownButton.vue +9 -9
- package/src/components/ui/select/SelectScrollUpButton.vue +9 -9
- package/src/components/ui/select/SelectSeparator.vue +2 -2
- package/src/components/ui/select/SelectTrigger.vue +14 -14
- package/src/components/ui/select/SelectValue.vue +3 -3
- package/src/components/ui/select/SelectValueContent.vue +39 -39
- package/src/components/ui/separator/Separator.vue +7 -7
- package/src/components/ui/sheet/Sheet.vue +3 -3
- package/src/components/ui/sheet/SheetClose.vue +3 -3
- package/src/components/ui/sheet/SheetContent.vue +22 -22
- package/src/components/ui/sheet/SheetDescription.vue +3 -3
- package/src/components/ui/sheet/SheetFooter.vue +3 -3
- package/src/components/ui/sheet/SheetHeader.vue +3 -3
- package/src/components/ui/sheet/SheetOverlay.vue +7 -7
- package/src/components/ui/sheet/SheetTitle.vue +3 -3
- package/src/components/ui/sheet/SheetTrigger.vue +3 -3
- package/src/components/ui/sidebar/Sidebar.vue +57 -57
- package/src/components/ui/sidebar/SidebarContent.vue +8 -8
- package/src/components/ui/sidebar/SidebarFooter.vue +8 -8
- package/src/components/ui/sidebar/SidebarGroup.vue +8 -8
- package/src/components/ui/sidebar/SidebarGroupAction.vue +9 -9
- package/src/components/ui/sidebar/SidebarGroupContent.vue +4 -4
- package/src/components/ui/sidebar/SidebarGroupLabel.vue +8 -8
- package/src/components/ui/sidebar/SidebarHeader.vue +8 -8
- package/src/components/ui/sidebar/SidebarInput.vue +8 -8
- package/src/components/ui/sidebar/SidebarInset.vue +9 -9
- package/src/components/ui/sidebar/SidebarMenu.vue +4 -4
- package/src/components/ui/sidebar/SidebarMenuAction.vue +18 -18
- package/src/components/ui/sidebar/SidebarMenuBadge.vue +14 -14
- package/src/components/ui/sidebar/SidebarMenuButton.vue +21 -21
- package/src/components/ui/sidebar/SidebarMenuButtonChild.vue +10 -10
- package/src/components/ui/sidebar/SidebarMenuItem.vue +4 -4
- package/src/components/ui/sidebar/SidebarMenuSkeleton.vue +10 -10
- package/src/components/ui/sidebar/SidebarMenuSub.vue +9 -9
- package/src/components/ui/sidebar/SidebarMenuSubButton.vue +17 -17
- package/src/components/ui/sidebar/SidebarMenuSubItem.vue +4 -4
- package/src/components/ui/sidebar/SidebarProvider.vue +46 -46
- package/src/components/ui/sidebar/SidebarRail.vue +13 -13
- package/src/components/ui/sidebar/SidebarSeparator.vue +4 -4
- package/src/components/ui/sidebar/SidebarTrigger.vue +5 -5
- package/src/components/ui/sidebar/index.ts +29 -29
- package/src/components/ui/sidebar/utils.ts +7 -7
- package/src/components/ui/skeleton/Skeleton.vue +6 -6
- package/src/components/ui/slider/Slider.vue +14 -14
- package/src/components/ui/spinner/Spinner.vue +21 -21
- package/src/components/ui/stepper/SimpleStepper.vue +26 -26
- package/src/components/ui/stepper/Stepper.vue +6 -6
- package/src/components/ui/stepper/StepperDescription.vue +5 -5
- package/src/components/ui/stepper/StepperIndicator.vue +17 -17
- package/src/components/ui/stepper/StepperItem.vue +4 -4
- package/src/components/ui/stepper/StepperSeparator.vue +7 -7
- package/src/components/ui/stepper/StepperTitle.vue +5 -5
- package/src/components/ui/stepper/StepperTrigger.vue +4 -4
- package/src/components/ui/stepper/index.ts +3 -3
- package/src/components/ui/switch/Switch.vue +15 -15
- package/src/components/ui/table/Table.vue +6 -6
- package/src/components/ui/table/TableBody.vue +4 -4
- package/src/components/ui/table/TableCaption.vue +4 -4
- package/src/components/ui/table/TableCell.vue +8 -8
- package/src/components/ui/table/TableEmpty.vue +14 -14
- package/src/components/ui/table/TableFooter.vue +8 -8
- package/src/components/ui/table/TableHead.vue +8 -8
- package/src/components/ui/table/TableHeader.vue +4 -4
- package/src/components/ui/table/TableRow.vue +8 -8
- package/src/components/ui/tabs/Tabs.vue +4 -4
- package/src/components/ui/tabs/TabsContent.vue +4 -4
- package/src/components/ui/tabs/TabsList.vue +15 -15
- package/src/components/ui/tabs/TabsTrigger.vue +10 -10
- package/src/components/ui/tags-input/TagsInput.vue +17 -17
- package/src/components/ui/tags-input/TagsInputInput.vue +6 -6
- package/src/components/ui/tags-input/TagsInputItem.vue +15 -15
- package/src/components/ui/tags-input/TagsInputItemDelete.vue +12 -12
- package/src/components/ui/tags-input/TagsInputItemText.vue +4 -4
- package/src/components/ui/textarea/Textarea.vue +12 -12
- package/src/components/ui/time-picker/TimePicker.vue +25 -25
- package/src/components/ui/toggle-group/Toggle.vue +10 -10
- package/src/components/ui/toggle-group/ToggleGroup.vue +38 -38
- package/src/components/ui/toggle-group/ToggleGroupItem.vue +9 -9
- package/src/components/ui/toggle-group/index.ts +46 -46
- package/src/components/ui/tooltip/Tooltip.vue +3 -3
- package/src/components/ui/tooltip/TooltipContent.vue +13 -13
- package/src/components/ui/tooltip/TooltipProvider.vue +4 -4
- package/src/components/ui/tooltip/TooltipTrigger.vue +3 -3
- package/src/i18n/de.json +206 -185
- package/src/i18n/en.json +206 -185
- package/src/i18n/es.json +206 -185
- package/src/i18n/fr.json +206 -185
- package/src/i18n/it.json +206 -185
- package/src/i18n/ja.json +206 -185
- package/src/i18n/pt.json +206 -185
- package/src/i18n/ru.json +206 -185
- package/src/i18n/zh.json +206 -185
- package/src/lib/color-tokens.ts +24 -24
- package/src/lib/i18n.ts +65 -65
- package/src/lib/utils.ts +33 -33
- package/src/nuxt-components.d.ts +2 -2
- package/src/runtime/ui-vintage-i18n-plugin.ts +49 -49
- package/src/shims-vue.d.ts +3 -3
- package/src/styles.css +377 -377
|
@@ -10,31 +10,31 @@ import { Button } from '../ui/button';
|
|
|
10
10
|
import { Sheet, SheetClose, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '../ui/sheet';
|
|
11
11
|
|
|
12
12
|
interface BlogTocLink {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
id?: string;
|
|
14
|
+
text?: string;
|
|
15
|
+
depth?: number;
|
|
16
|
+
children?: BlogTocLink[];
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
interface TocHeading {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
id: string;
|
|
21
|
+
text: string;
|
|
22
|
+
level: number;
|
|
23
|
+
number: string;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
// Props
|
|
27
27
|
const props = defineProps<{
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
28
|
+
content: {
|
|
29
|
+
body?: {
|
|
30
|
+
toc?: {
|
|
31
|
+
links?: BlogTocLink[];
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
toc?: {
|
|
35
|
+
links?: BlogTocLink[];
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
38
|
}>();
|
|
39
39
|
|
|
40
40
|
const { t } = useI18n();
|
|
@@ -62,33 +62,33 @@ let restoreMobileTriggerFocus = true;
|
|
|
62
62
|
|
|
63
63
|
// Create the headings list
|
|
64
64
|
const headings = computed(() => {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
+
}));
|
|
92
92
|
});
|
|
93
93
|
|
|
94
94
|
// Number of main sections shown in the header
|
|
@@ -101,363 +101,363 @@ const tocSections = computed(() => t('uiVintage.blog.tableOfContentsSections', {
|
|
|
101
101
|
|
|
102
102
|
// Prefer reduced motion for heading scroll
|
|
103
103
|
const prefersReducedMotion = () => {
|
|
104
|
-
|
|
104
|
+
return Boolean(window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches);
|
|
105
105
|
};
|
|
106
106
|
|
|
107
107
|
// Resolve the heading that should be marked as the current chapter
|
|
108
108
|
const resolveActiveHeadingId = () => {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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;
|
|
128
128
|
};
|
|
129
129
|
|
|
130
130
|
// Update the active heading based on the current scroll position
|
|
131
131
|
const updateActiveHeading = () => {
|
|
132
|
-
|
|
132
|
+
activeHeadingId.value = resolveActiveHeadingId();
|
|
133
133
|
};
|
|
134
134
|
|
|
135
135
|
// Track which headings currently intersect the reading window
|
|
136
136
|
const handleHeadingIntersect = (entries: IntersectionObserverEntry[]) => {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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();
|
|
151
151
|
};
|
|
152
152
|
|
|
153
153
|
// Disconnect the heading observer and clear any pending retries
|
|
154
154
|
const disconnectHeadingObserver = () => {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
155
|
+
headingObserver?.disconnect();
|
|
156
|
+
headingObserver = null;
|
|
157
|
+
window.clearTimeout(headingRetryTimeout);
|
|
158
158
|
};
|
|
159
159
|
|
|
160
160
|
// Bind the heading observer to track which headings are visible in the viewport
|
|
161
161
|
const bindHeadingObserver = () => {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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;
|
|
195
195
|
};
|
|
196
196
|
|
|
197
197
|
// Retry binding until the content renderer has mounted its headings
|
|
198
198
|
const scheduleHeadingObserver = (attempt = 0) => {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
+
});
|
|
207
207
|
};
|
|
208
208
|
|
|
209
209
|
// Disconnect the observer that controls the mobile trigger visibility
|
|
210
210
|
const disconnectSentinelObserver = () => {
|
|
211
|
-
|
|
212
|
-
|
|
211
|
+
sentinelObserver?.disconnect();
|
|
212
|
+
sentinelObserver = null;
|
|
213
213
|
};
|
|
214
214
|
|
|
215
215
|
// Show the mobile trigger after the in-flow accordion leaves the viewport
|
|
216
216
|
const bindSentinelObserver = () => {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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);
|
|
233
233
|
};
|
|
234
234
|
|
|
235
235
|
// Keep the mobile trigger state in sync with the viewport breakpoint
|
|
236
236
|
const handleDesktopMediaChange = (event: MediaQueryList | MediaQueryListEvent) => {
|
|
237
|
-
|
|
237
|
+
isDesktop.value = event.matches;
|
|
238
238
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
239
|
+
if (event.matches) {
|
|
240
|
+
isMobileOpen.value = false;
|
|
241
|
+
}
|
|
242
242
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
243
|
+
nextTick(() => {
|
|
244
|
+
bindSentinelObserver();
|
|
245
|
+
});
|
|
246
246
|
};
|
|
247
247
|
|
|
248
248
|
// Synchronize the mobile sheet state and restore focus after dismissal
|
|
249
249
|
const handleMobileOpenChange = (open: boolean) => {
|
|
250
|
-
|
|
250
|
+
isMobileOpen.value = open;
|
|
251
251
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
252
|
+
// Return focus to the persistent control when the overlay is dismissed
|
|
253
|
+
if (!open && restoreMobileTriggerFocus) {
|
|
254
|
+
mobileTriggerRef.value?.focus();
|
|
255
|
+
}
|
|
256
256
|
|
|
257
|
-
|
|
257
|
+
restoreMobileTriggerFocus = true;
|
|
258
258
|
};
|
|
259
259
|
|
|
260
260
|
// Scroll to a heading and optionally close the mobile sheet first
|
|
261
261
|
const scrollToHeading = (id: string, options: { closeMobile?: boolean } = {}) => {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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();
|
|
296
296
|
};
|
|
297
297
|
|
|
298
298
|
// Open the mobile table of contents sheet
|
|
299
299
|
const openMobileToc = () => {
|
|
300
|
-
|
|
300
|
+
isMobileOpen.value = true;
|
|
301
301
|
};
|
|
302
302
|
|
|
303
303
|
// Get classes for heading button
|
|
304
304
|
const getHeadingButtonClasses = (level: number, isActive: boolean) => {
|
|
305
|
-
|
|
306
|
-
|
|
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';
|
|
307
307
|
|
|
308
|
-
|
|
309
|
-
|
|
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';
|
|
310
310
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
+
}
|
|
316
316
|
|
|
317
|
-
|
|
318
|
-
|
|
317
|
+
// Return combined classes
|
|
318
|
+
return [baseClasses, levelClasses, stateClasses];
|
|
319
319
|
};
|
|
320
320
|
|
|
321
321
|
// Watch for changes in the headings and update the visible heading IDs and active heading ID accordingly
|
|
322
322
|
watch(headings, () => {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
323
|
+
visibleHeadingIds.value = new Set();
|
|
324
|
+
activeHeadingId.value = headings.value[0]?.id ?? '';
|
|
325
|
+
scheduleHeadingObserver();
|
|
326
326
|
});
|
|
327
327
|
|
|
328
328
|
// Watch for changes in the inline table of contents reference and bind the sentinel observer accordingly
|
|
329
329
|
watch(inlineRef, () => {
|
|
330
|
-
|
|
330
|
+
bindSentinelObserver();
|
|
331
331
|
}, { flush: 'post' });
|
|
332
332
|
|
|
333
333
|
// On component mounted
|
|
334
334
|
onMounted(() => {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
335
|
+
desktopMediaQuery = window.matchMedia(DESKTOP_MEDIA_QUERY);
|
|
336
|
+
handleDesktopMediaChange(desktopMediaQuery);
|
|
337
|
+
desktopMediaQuery.addEventListener('change', handleDesktopMediaChange);
|
|
338
338
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
339
|
+
const hashId = window.location.hash.replace(/^#/, '');
|
|
340
|
+
if (hashId && headings.value.some(heading => heading.id === hashId)) {
|
|
341
|
+
activeHeadingId.value = hashId;
|
|
342
|
+
}
|
|
343
343
|
|
|
344
|
-
|
|
344
|
+
scheduleHeadingObserver();
|
|
345
345
|
});
|
|
346
346
|
|
|
347
347
|
// On component unmounted
|
|
348
348
|
onUnmounted(() => {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
349
|
+
desktopMediaQuery?.removeEventListener('change', handleDesktopMediaChange);
|
|
350
|
+
disconnectSentinelObserver();
|
|
351
|
+
disconnectHeadingObserver();
|
|
352
|
+
window.clearTimeout(mobileScrollTimeout);
|
|
353
353
|
});
|
|
354
354
|
</script>
|
|
355
355
|
|
|
356
356
|
<template>
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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>
|
|
439
439
|
</template>
|
|
440
440
|
|
|
441
441
|
<style scoped>
|
|
442
442
|
.toc-tab-enter-active {
|
|
443
|
-
|
|
443
|
+
transition: opacity 0.3s var(--ease-out-expo), transform 0.32s var(--ease-out-expo);
|
|
444
444
|
}
|
|
445
445
|
|
|
446
446
|
.toc-tab-leave-active {
|
|
447
|
-
|
|
447
|
+
transition: opacity 0.15s var(--ease-snappy), transform 0.15s var(--ease-snappy);
|
|
448
448
|
}
|
|
449
449
|
|
|
450
450
|
.toc-tab-enter-from,
|
|
451
451
|
.toc-tab-leave-to {
|
|
452
|
-
|
|
453
|
-
|
|
452
|
+
opacity: 0;
|
|
453
|
+
transform: translateX(-100%);
|
|
454
454
|
}
|
|
455
455
|
|
|
456
456
|
@media (prefers-reduced-motion: reduce) {
|
|
457
457
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
458
|
+
.toc-tab-enter-active,
|
|
459
|
+
.toc-tab-leave-active {
|
|
460
|
+
transition: none;
|
|
461
|
+
}
|
|
462
462
|
}
|
|
463
463
|
</style>
|