@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.
- package/AGENTS.md +1 -1
- package/COMPONENTS.md +1 -1
- package/README.md +3 -3
- 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/BlogContentRenderer.vue +111 -111
- package/src/components/blog/BlogFAQSection.vue +20 -20
- package/src/components/blog/BlogHeaderSection.vue +81 -81
- package/src/components/blog/BlogInfoSection.vue +50 -48
- package/src/components/blog/BlogPostHeader.vue +135 -135
- package/src/components/blog/BlogProgressBar.vue +33 -33
- package/src/components/blog/BlogSectionTitle.vue +22 -22
- package/src/components/blog/BlogTagsSection.vue +25 -25
- package/src/components/blog/CategoriesList.vue +14 -14
- package/src/components/blog/CategoriesSection.vue +11 -11
- package/src/components/blog/CategoryCard.vue +27 -26
- package/src/components/blog/HeaderCarousel.vue +99 -98
- package/src/components/blog/PostCard.vue +34 -34
- package/src/components/blog/PostsList.vue +15 -15
- package/src/components/blog/SocialShareSidebar.vue +92 -85
- package/src/components/blog/TableOfContents.vue +310 -308
- 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 +35 -35
- package/src/components/chip/Chip.vue +16 -16
- package/src/components/confirm-dialog/ConfirmDialog.vue +51 -51
- package/src/components/confirm-dialog/confirm-dialog-state.ts +131 -131
- package/src/components/content/BlogList.vue +32 -31
- package/src/components/content/BlogTable.vue +36 -36
- package/src/components/content/CodeBlock.vue +139 -137
- package/src/components/content/Flow.vue +60 -60
- package/src/components/content/ProseHr.vue +16 -16
- package/src/components/content/ProsePre.vue +12 -12
- package/src/components/content/Terminal.vue +62 -60
- 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 +41 -41
- package/src/components/error-page/ErrorPage.vue +73 -73
- 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 +129 -129
- 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 +77 -77
- package/src/components/load-more-button/LoadMoreButton.vue +19 -18
- package/src/components/loading-state-card/LoadingStateCard.vue +29 -29
- package/src/components/message-dialog/MessageDialog.vue +62 -60
- package/src/components/message-dialog/message-dialog-state.ts +105 -105
- package/src/components/message-toast/MessageToast.vue +153 -150
- 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 +59 -58
- 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 +65 -62
- package/src/components/theme-selector/theme-transition.ts +63 -63
- package/src/components/ui/accordion/Accordion.vue +39 -39
- 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 +10 -10
- 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 +16 -16
- package/src/components/ui/calendar/CalendarPrevButton.vue +16 -16
- 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 +18 -18
- package/src/components/ui/combobox/ComboboxList.vue +19 -19
- package/src/components/ui/combobox/ComboboxSelect.vue +79 -78
- package/src/components/ui/combobox/ComboboxSeparator.vue +1 -1
- package/src/components/ui/combobox/ComboboxTrigger.vue +11 -11
- 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 +10 -10
- 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 +147 -146
- package/src/components/ui/data-table/index.ts +11 -11
- package/src/components/ui/date-picker/DatePicker.vue +32 -32
- 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 +22 -22
- 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 +33 -33
- 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 +12 -12
- 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 +16 -16
- 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 +163 -154
- 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 +17 -17
- 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 +90 -89
- 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 +10 -10
- package/src/components/ui/pagination/PaginationFirst.vue +9 -9
- package/src/components/ui/pagination/PaginationItem.vue +14 -14
- package/src/components/ui/pagination/PaginationLast.vue +9 -9
- package/src/components/ui/pagination/PaginationNext.vue +9 -9
- package/src/components/ui/pagination/PaginationPrevious.vue +9 -9
- 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 +25 -25
- 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 +10 -10
- package/src/components/ui/select/SelectScrollUpButton.vue +10 -10
- package/src/components/ui/select/SelectSeparator.vue +2 -2
- package/src/components/ui/select/SelectTrigger.vue +15 -15
- 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 +6 -6
- 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 +27 -27
- 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 +18 -18
- 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 +13 -13
- 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 +185 -185
- package/src/i18n/en.json +185 -185
- package/src/i18n/es.json +185 -185
- package/src/i18n/fr.json +185 -185
- package/src/i18n/it.json +185 -185
- package/src/i18n/ja.json +185 -185
- package/src/i18n/pt.json +185 -185
- package/src/i18n/ru.json +185 -185
- package/src/i18n/zh.json +185 -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
|
@@ -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
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
id?: string;
|
|
14
|
+
text?: string;
|
|
15
|
+
depth?: number;
|
|
16
|
+
children?: BlogTocLink[];
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
interface TocHeading {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
161
|
-
|
|
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
|
-
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
210
|
-
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
237
|
+
isDesktop.value = event.matches;
|
|
236
238
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
239
|
+
if (event.matches) {
|
|
240
|
+
isMobileOpen.value = false;
|
|
241
|
+
}
|
|
240
242
|
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
250
|
+
isMobileOpen.value = open;
|
|
249
251
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
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
|
-
|
|
261
|
-
|
|
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
|
-
|
|
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
|
-
|
|
300
|
+
isMobileOpen.value = true;
|
|
299
301
|
};
|
|
300
302
|
|
|
301
303
|
// Get classes for heading button
|
|
302
304
|
const getHeadingButtonClasses = (level: number, isActive: boolean) => {
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
|
|
307
|
-
|
|
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
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
-
|
|
316
|
-
|
|
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
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
-
|
|
330
|
+
bindSentinelObserver();
|
|
329
331
|
}, { flush: 'post' });
|
|
330
332
|
|
|
331
333
|
// On component mounted
|
|
332
334
|
onMounted(() => {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
335
|
+
desktopMediaQuery = window.matchMedia(DESKTOP_MEDIA_QUERY);
|
|
336
|
+
handleDesktopMediaChange(desktopMediaQuery);
|
|
337
|
+
desktopMediaQuery.addEventListener('change', handleDesktopMediaChange);
|
|
336
338
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
-
|
|
344
|
+
scheduleHeadingObserver();
|
|
343
345
|
});
|
|
344
346
|
|
|
345
347
|
// On component unmounted
|
|
346
348
|
onUnmounted(() => {
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
349
|
+
desktopMediaQuery?.removeEventListener('change', handleDesktopMediaChange);
|
|
350
|
+
disconnectSentinelObserver();
|
|
351
|
+
disconnectHeadingObserver();
|
|
352
|
+
window.clearTimeout(mobileScrollTimeout);
|
|
351
353
|
});
|
|
352
354
|
</script>
|
|
353
355
|
|
|
354
356
|
<template>
|
|
355
|
-
|
|
356
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
451
|
-
|
|
452
|
+
opacity: 0;
|
|
453
|
+
transform: translateX(-100%);
|
|
452
454
|
}
|
|
453
455
|
|
|
454
456
|
@media (prefers-reduced-motion: reduce) {
|
|
455
457
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
458
|
+
.toc-tab-enter-active,
|
|
459
|
+
.toc-tab-leave-active {
|
|
460
|
+
transition: none;
|
|
461
|
+
}
|
|
460
462
|
}
|
|
461
463
|
</style>
|