@mkbabb/glass-ui 0.2.0
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/README.md +172 -0
- package/dist/glass-ui.css +1 -0
- package/dist/glass-ui.js +10019 -0
- package/dist/index.d.ts +6619 -0
- package/package.json +65 -0
- package/src/components/custom/aurora/Aurora.vue +34 -0
- package/src/components/custom/aurora/composables/color.ts +122 -0
- package/src/components/custom/aurora/composables/useAurora.ts +355 -0
- package/src/components/custom/aurora/index.ts +8 -0
- package/src/components/custom/confirm-dialog/ConfirmDialog.vue +88 -0
- package/src/components/custom/confirm-dialog/index.ts +1 -0
- package/src/components/custom/controls/DarkModeToggle.vue +96 -0
- package/src/components/custom/controls/index.ts +1 -0
- package/src/components/custom/dock/DockLayerGroup.vue +21 -0
- package/src/components/custom/dock/DockPopover.vue +263 -0
- package/src/components/custom/dock/GlassDock.vue +276 -0
- package/src/components/custom/dock/composables/index.ts +16 -0
- package/src/components/custom/dock/composables/isTeleportedTarget.ts +19 -0
- package/src/components/custom/dock/composables/useDockActionBar.ts +33 -0
- package/src/components/custom/dock/composables/useDockState.ts +301 -0
- package/src/components/custom/dock/composables/useDockTransition.ts +146 -0
- package/src/components/custom/dock/composables/useLayerTransition.ts +135 -0
- package/src/components/custom/dock/composables/usePopupMutex.ts +83 -0
- package/src/components/custom/dock/index.ts +9 -0
- package/src/components/custom/expandable-container/ExpandableContainer.vue +64 -0
- package/src/components/custom/expandable-container/index.ts +1 -0
- package/src/components/custom/glass-panel/GlassPanel.vue +98 -0
- package/src/components/custom/glass-panel/index.ts +2 -0
- package/src/components/custom/icon-tooltip/IconTooltip.vue +20 -0
- package/src/components/custom/icon-tooltip/index.ts +1 -0
- package/src/components/custom/index.ts +15 -0
- package/src/components/custom/infinite-scroll/InfiniteScroll.vue +55 -0
- package/src/components/custom/infinite-scroll/composables/index.ts +2 -0
- package/src/components/custom/infinite-scroll/composables/types.ts +23 -0
- package/src/components/custom/infinite-scroll/composables/useInfiniteScroll.ts +73 -0
- package/src/components/custom/infinite-scroll/index.ts +1 -0
- package/src/components/custom/labeled-field/LabeledInput.vue +29 -0
- package/src/components/custom/labeled-field/LabeledSelect.vue +59 -0
- package/src/components/custom/labeled-field/LabeledSlider.vue +32 -0
- package/src/components/custom/labeled-field/LabeledSwitch.vue +27 -0
- package/src/components/custom/labeled-field/index.ts +4 -0
- package/src/components/custom/metaballs/MetaballCanvas.vue +23 -0
- package/src/components/custom/metaballs/index.ts +4 -0
- package/src/components/custom/metaballs/shaders.ts +63 -0
- package/src/components/custom/metaballs/types.ts +29 -0
- package/src/components/custom/metaballs/useMetaballs.ts +252 -0
- package/src/components/custom/search/FuzzySearch.vue +589 -0
- package/src/components/custom/search/SearchBar.vue +44 -0
- package/src/components/custom/search/composables/fuzzySearchIndex.ts +224 -0
- package/src/components/custom/search/composables/index.ts +5 -0
- package/src/components/custom/search/composables/types.ts +34 -0
- package/src/components/custom/search/composables/useFuzzySearch.ts +115 -0
- package/src/components/custom/search/index.ts +7 -0
- package/src/components/custom/sidebar/ProgressiveSidebar.vue +256 -0
- package/src/components/custom/sidebar/composables/index.ts +6 -0
- package/src/components/custom/sidebar/composables/useScrollTracker.ts +242 -0
- package/src/components/custom/sidebar/composables/useSidebarFollow.ts +247 -0
- package/src/components/custom/sidebar/composables/useSidebarState.ts +72 -0
- package/src/components/custom/sidebar/composables/useTreeIndex.ts +152 -0
- package/src/components/custom/sidebar/index.ts +15 -0
- package/src/components/custom/sidebar/types.ts +50 -0
- package/src/components/custom/tabs/BouncyTabs.vue +39 -0
- package/src/components/custom/tabs/BouncyToggle.vue +352 -0
- package/src/components/custom/tabs/UnderlineTabs.vue +115 -0
- package/src/components/custom/tabs/index.ts +5 -0
- package/src/components/custom/timeline/GlassTimeline.vue +174 -0
- package/src/components/custom/timeline/index.ts +1 -0
- package/src/components/custom/typewriter/TypewriterText.vue +239 -0
- package/src/components/custom/typewriter/composables/index.ts +1 -0
- package/src/components/custom/typewriter/composables/useTypewriter.ts +413 -0
- package/src/components/custom/typewriter/index.ts +7 -0
- package/src/components/custom/typewriter/types.ts +159 -0
- package/src/components/custom/typewriter/utils/keyboard.ts +213 -0
- package/src/components/custom/typewriter/utils/pausePatterns.ts +55 -0
- package/src/components/custom/typewriter/utils/timing.ts +104 -0
- package/src/components/custom/typewriter/utils/typoStateMachine.ts +197 -0
- package/src/components/index.ts +2 -0
- package/src/components/ui/accordion/Accordion.vue +19 -0
- package/src/components/ui/accordion/AccordionContent.vue +24 -0
- package/src/components/ui/accordion/AccordionItem.vue +24 -0
- package/src/components/ui/accordion/AccordionTrigger.vue +39 -0
- package/src/components/ui/accordion/index.ts +4 -0
- package/src/components/ui/alert/Alert.vue +20 -0
- package/src/components/ui/alert/AlertDescription.vue +17 -0
- package/src/components/ui/alert/AlertTitle.vue +17 -0
- package/src/components/ui/alert/index.ts +23 -0
- package/src/components/ui/avatar/Avatar.vue +21 -0
- package/src/components/ui/avatar/AvatarFallback.vue +11 -0
- package/src/components/ui/avatar/AvatarImage.vue +9 -0
- package/src/components/ui/avatar/index.ts +24 -0
- package/src/components/ui/badge/Badge.vue +16 -0
- package/src/components/ui/badge/index.ts +25 -0
- package/src/components/ui/button/Button.vue +26 -0
- package/src/components/ui/button/index.ts +43 -0
- package/src/components/ui/card/Card.vue +28 -0
- package/src/components/ui/card/CardContent.vue +14 -0
- package/src/components/ui/card/CardDescription.vue +14 -0
- package/src/components/ui/card/CardFooter.vue +14 -0
- package/src/components/ui/card/CardHeader.vue +14 -0
- package/src/components/ui/card/CardTitle.vue +21 -0
- package/src/components/ui/card/index.ts +6 -0
- package/src/components/ui/carousel/Carousel.vue +53 -0
- package/src/components/ui/carousel/CarouselContent.vue +35 -0
- package/src/components/ui/carousel/CarouselItem.vue +24 -0
- package/src/components/ui/carousel/CarouselNext.vue +40 -0
- package/src/components/ui/carousel/CarouselPrevious.vue +40 -0
- package/src/components/ui/carousel/index.ts +10 -0
- package/src/components/ui/carousel/interface.ts +26 -0
- package/src/components/ui/carousel/useCarousel.ts +56 -0
- package/src/components/ui/checkbox/Checkbox.vue +33 -0
- package/src/components/ui/checkbox/index.ts +1 -0
- package/src/components/ui/collapsible/Collapsible.vue +15 -0
- package/src/components/ui/collapsible/CollapsibleContent.vue +11 -0
- package/src/components/ui/collapsible/CollapsibleTrigger.vue +11 -0
- package/src/components/ui/collapsible/index.ts +3 -0
- package/src/components/ui/combobox/Combobox.vue +17 -0
- package/src/components/ui/combobox/ComboboxAnchor.vue +23 -0
- package/src/components/ui/combobox/ComboboxEmpty.vue +21 -0
- package/src/components/ui/combobox/ComboboxGroup.vue +27 -0
- package/src/components/ui/combobox/ComboboxInput.vue +41 -0
- package/src/components/ui/combobox/ComboboxItem.vue +24 -0
- package/src/components/ui/combobox/ComboboxItemIndicator.vue +23 -0
- package/src/components/ui/combobox/ComboboxList.vue +29 -0
- package/src/components/ui/combobox/ComboboxSeparator.vue +21 -0
- package/src/components/ui/combobox/ComboboxViewport.vue +23 -0
- package/src/components/ui/combobox/index.ts +12 -0
- package/src/components/ui/command/Command.vue +30 -0
- package/src/components/ui/command/CommandDialog.vue +21 -0
- package/src/components/ui/command/CommandEmpty.vue +20 -0
- package/src/components/ui/command/CommandGroup.vue +29 -0
- package/src/components/ui/command/CommandInput.vue +33 -0
- package/src/components/ui/command/CommandItem.vue +26 -0
- package/src/components/ui/command/CommandList.vue +27 -0
- package/src/components/ui/command/CommandSeparator.vue +23 -0
- package/src/components/ui/command/CommandShortcut.vue +14 -0
- package/src/components/ui/command/index.ts +9 -0
- package/src/components/ui/context-menu/ContextMenu.vue +15 -0
- package/src/components/ui/context-menu/ContextMenuCheckboxItem.vue +40 -0
- package/src/components/ui/context-menu/ContextMenuContent.vue +36 -0
- package/src/components/ui/context-menu/ContextMenuGroup.vue +11 -0
- package/src/components/ui/context-menu/ContextMenuItem.vue +34 -0
- package/src/components/ui/context-menu/ContextMenuLabel.vue +25 -0
- package/src/components/ui/context-menu/ContextMenuPortal.vue +11 -0
- package/src/components/ui/context-menu/ContextMenuRadioGroup.vue +19 -0
- package/src/components/ui/context-menu/ContextMenuRadioItem.vue +40 -0
- package/src/components/ui/context-menu/ContextMenuSeparator.vue +20 -0
- package/src/components/ui/context-menu/ContextMenuShortcut.vue +14 -0
- package/src/components/ui/context-menu/ContextMenuSub.vue +19 -0
- package/src/components/ui/context-menu/ContextMenuSubContent.vue +35 -0
- package/src/components/ui/context-menu/ContextMenuSubTrigger.vue +34 -0
- package/src/components/ui/context-menu/ContextMenuTrigger.vue +13 -0
- package/src/components/ui/context-menu/index.ts +14 -0
- package/src/components/ui/data-table/DataTable.vue +167 -0
- package/src/components/ui/data-table/DataTablePagination.vue +112 -0
- package/src/components/ui/data-table/index.ts +3 -0
- package/src/components/ui/data-table/types.ts +48 -0
- package/src/components/ui/dialog/Dialog.vue +14 -0
- package/src/components/ui/dialog/DialogClose.vue +11 -0
- package/src/components/ui/dialog/DialogContent.vue +61 -0
- package/src/components/ui/dialog/DialogDescription.vue +24 -0
- package/src/components/ui/dialog/DialogFooter.vue +19 -0
- package/src/components/ui/dialog/DialogHeader.vue +16 -0
- package/src/components/ui/dialog/DialogScrollContent.vue +65 -0
- package/src/components/ui/dialog/DialogTitle.vue +29 -0
- package/src/components/ui/dialog/DialogTrigger.vue +11 -0
- package/src/components/ui/dialog/index.ts +9 -0
- package/src/components/ui/drawer/Drawer.vue +19 -0
- package/src/components/ui/drawer/DrawerContent.vue +28 -0
- package/src/components/ui/drawer/DrawerDescription.vue +20 -0
- package/src/components/ui/drawer/DrawerFooter.vue +14 -0
- package/src/components/ui/drawer/DrawerHeader.vue +14 -0
- package/src/components/ui/drawer/DrawerOverlay.vue +18 -0
- package/src/components/ui/drawer/DrawerTitle.vue +20 -0
- package/src/components/ui/drawer/index.ts +8 -0
- package/src/components/ui/dropdown-menu/DropdownMenu.vue +14 -0
- package/src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue +40 -0
- package/src/components/ui/dropdown-menu/DropdownMenuContent.vue +44 -0
- package/src/components/ui/dropdown-menu/DropdownMenuGroup.vue +11 -0
- package/src/components/ui/dropdown-menu/DropdownMenuItem.vue +28 -0
- package/src/components/ui/dropdown-menu/DropdownMenuLabel.vue +24 -0
- package/src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue +19 -0
- package/src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue +40 -0
- package/src/components/ui/dropdown-menu/DropdownMenuSeparator.vue +22 -0
- package/src/components/ui/dropdown-menu/DropdownMenuShortcut.vue +14 -0
- package/src/components/ui/dropdown-menu/DropdownMenuSub.vue +19 -0
- package/src/components/ui/dropdown-menu/DropdownMenuSubContent.vue +36 -0
- package/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +33 -0
- package/src/components/ui/dropdown-menu/DropdownMenuTrigger.vue +13 -0
- package/src/components/ui/dropdown-menu/index.ts +16 -0
- package/src/components/ui/hover-card/HoverCard.vue +14 -0
- package/src/components/ui/hover-card/HoverCardContent.vue +41 -0
- package/src/components/ui/hover-card/HoverCardTrigger.vue +11 -0
- package/src/components/ui/hover-card/index.ts +3 -0
- package/src/components/ui/index.ts +41 -0
- package/src/components/ui/input/Input.vue +24 -0
- package/src/components/ui/input/index.ts +1 -0
- package/src/components/ui/label/Label.vue +27 -0
- package/src/components/ui/label/index.ts +1 -0
- package/src/components/ui/multi-select/MultiSelect.vue +141 -0
- package/src/components/ui/multi-select/index.ts +7 -0
- package/src/components/ui/notification/Notification.vue +85 -0
- package/src/components/ui/notification/index.ts +1 -0
- package/src/components/ui/number-field/NumberField.vue +23 -0
- package/src/components/ui/number-field/NumberFieldContent.vue +14 -0
- package/src/components/ui/number-field/NumberFieldDecrement.vue +25 -0
- package/src/components/ui/number-field/NumberFieldIncrement.vue +25 -0
- package/src/components/ui/number-field/NumberFieldInput.vue +8 -0
- package/src/components/ui/number-field/index.ts +5 -0
- package/src/components/ui/popover/Popover.vue +15 -0
- package/src/components/ui/popover/PopoverContent.vue +61 -0
- package/src/components/ui/popover/PopoverTrigger.vue +11 -0
- package/src/components/ui/popover/index.ts +3 -0
- package/src/components/ui/progress/Progress.vue +39 -0
- package/src/components/ui/progress/index.ts +1 -0
- package/src/components/ui/radio-group/RadioGroup.vue +25 -0
- package/src/components/ui/radio-group/RadioGroupItem.vue +39 -0
- package/src/components/ui/radio-group/index.ts +2 -0
- package/src/components/ui/scroll-area/ScrollArea.vue +29 -0
- package/src/components/ui/scroll-area/ScrollBar.vue +30 -0
- package/src/components/ui/scroll-area/index.ts +2 -0
- package/src/components/ui/scroll-pane/ScrollPane.vue +25 -0
- package/src/components/ui/scroll-pane/ScrollPaneHeader.vue +75 -0
- package/src/components/ui/scroll-pane/index.ts +2 -0
- package/src/components/ui/select/Select.vue +15 -0
- package/src/components/ui/select/SelectContent.vue +57 -0
- package/src/components/ui/select/SelectGroup.vue +19 -0
- package/src/components/ui/select/SelectItem.vue +47 -0
- package/src/components/ui/select/SelectItemText.vue +11 -0
- package/src/components/ui/select/SelectLabel.vue +13 -0
- package/src/components/ui/select/SelectScrollDownButton.vue +24 -0
- package/src/components/ui/select/SelectScrollUpButton.vue +24 -0
- package/src/components/ui/select/SelectSeparator.vue +17 -0
- package/src/components/ui/select/SelectTrigger.vue +45 -0
- package/src/components/ui/select/SelectValue.vue +11 -0
- package/src/components/ui/select/index.ts +11 -0
- package/src/components/ui/separator/Separator.vue +35 -0
- package/src/components/ui/separator/index.ts +1 -0
- package/src/components/ui/sheet/Sheet.vue +14 -0
- package/src/components/ui/sheet/SheetClose.vue +11 -0
- package/src/components/ui/sheet/SheetContent.vue +56 -0
- package/src/components/ui/sheet/SheetDescription.vue +22 -0
- package/src/components/ui/sheet/SheetFooter.vue +19 -0
- package/src/components/ui/sheet/SheetHeader.vue +16 -0
- package/src/components/ui/sheet/SheetTitle.vue +22 -0
- package/src/components/ui/sheet/SheetTrigger.vue +11 -0
- package/src/components/ui/sheet/index.ts +31 -0
- package/src/components/ui/skeleton/Skeleton.vue +14 -0
- package/src/components/ui/skeleton/index.ts +1 -0
- package/src/components/ui/slider/Slider.vue +66 -0
- package/src/components/ui/slider/index.ts +1 -0
- package/src/components/ui/switch/Switch.vue +37 -0
- package/src/components/ui/switch/index.ts +1 -0
- package/src/components/ui/table/Table.vue +16 -0
- package/src/components/ui/table/TableBody.vue +14 -0
- package/src/components/ui/table/TableCaption.vue +14 -0
- package/src/components/ui/table/TableCell.vue +14 -0
- package/src/components/ui/table/TableEmpty.vue +39 -0
- package/src/components/ui/table/TableFooter.vue +16 -0
- package/src/components/ui/table/TableHead.vue +21 -0
- package/src/components/ui/table/TableHeader.vue +14 -0
- package/src/components/ui/table/TableRow.vue +21 -0
- package/src/components/ui/table/index.ts +9 -0
- package/src/components/ui/tabs/Tabs.vue +15 -0
- package/src/components/ui/tabs/TabsContent.vue +22 -0
- package/src/components/ui/tabs/TabsIndicator.vue +22 -0
- package/src/components/ui/tabs/TabsList.vue +25 -0
- package/src/components/ui/tabs/TabsTrigger.vue +29 -0
- package/src/components/ui/tabs/index.ts +5 -0
- package/src/components/ui/tags-input/TagsInput.vue +22 -0
- package/src/components/ui/tags-input/TagsInputInput.vue +19 -0
- package/src/components/ui/tags-input/TagsInputItem.vue +22 -0
- package/src/components/ui/tags-input/TagsInputItemDelete.vue +24 -0
- package/src/components/ui/tags-input/TagsInputItemText.vue +19 -0
- package/src/components/ui/tags-input/index.ts +5 -0
- package/src/components/ui/textarea/Textarea.vue +24 -0
- package/src/components/ui/textarea/index.ts +1 -0
- package/src/components/ui/toast/Toast.vue +57 -0
- package/src/components/ui/toast/ToastAction.vue +30 -0
- package/src/components/ui/toast/ToastClose.vue +31 -0
- package/src/components/ui/toast/ToastDescription.vue +25 -0
- package/src/components/ui/toast/ToastTitle.vue +25 -0
- package/src/components/ui/toast/Toaster.vue +31 -0
- package/src/components/ui/toast/index.ts +8 -0
- package/src/components/ui/toast/use-toast.ts +136 -0
- package/src/components/ui/toggle/Toggle.vue +35 -0
- package/src/components/ui/toggle/index.ts +27 -0
- package/src/components/ui/toggle-group/ToggleGroup.vue +34 -0
- package/src/components/ui/toggle-group/ToggleGroupItem.vue +35 -0
- package/src/components/ui/toggle-group/index.ts +2 -0
- package/src/components/ui/tooltip/Tooltip.vue +14 -0
- package/src/components/ui/tooltip/TooltipContent.vue +31 -0
- package/src/components/ui/tooltip/TooltipProvider.vue +11 -0
- package/src/components/ui/tooltip/TooltipTrigger.vue +11 -0
- package/src/components/ui/tooltip/index.ts +4 -0
- package/src/composables/glass/index.ts +8 -0
- package/src/composables/glass/useGlassRenderer.ts +252 -0
- package/src/composables/glass/webgl/frostShader.ts +221 -0
- package/src/composables/glass/webgpu/glassShader.wgsl +173 -0
- package/src/composables/index.ts +32 -0
- package/src/composables/infinite-scroll/index.ts +2 -0
- package/src/composables/infinite-scroll/types.ts +25 -0
- package/src/composables/infinite-scroll/useInfiniteScroll.ts +101 -0
- package/src/composables/interaction/index.ts +5 -0
- package/src/composables/interaction/useHeightTransition.ts +82 -0
- package/src/composables/interaction/useHoverPopover.ts +64 -0
- package/src/composables/interaction/useHoverToggle.ts +103 -0
- package/src/composables/interaction/useLeaveTimer.ts +17 -0
- package/src/composables/interaction/useTouchGate.ts +207 -0
- package/src/composables/pagination/index.ts +2 -0
- package/src/composables/pagination/useOffsetPagination.ts +70 -0
- package/src/composables/prng.ts +32 -0
- package/src/composables/useCharSplit.ts +31 -0
- package/src/composables/useClipboard.ts +46 -0
- package/src/composables/useGlobalDark.ts +61 -0
- package/src/composables/useKeyboardShortcuts.ts +205 -0
- package/src/composables/useWatercolorBlob.ts +136 -0
- package/src/composables/virtual/index.ts +22 -0
- package/src/composables/virtual/useVirtualSectionWindow.ts +338 -0
- package/src/composables/virtual/useWindowedStore.ts +86 -0
- package/src/composables/virtual/virtualSectionLayout.ts +212 -0
- package/src/index.ts +9 -0
- package/src/styles/animations.css +233 -0
- package/src/styles/cards.css +66 -0
- package/src/styles/dock.css +221 -0
- package/src/styles/floating-panel.css +49 -0
- package/src/styles/glass.css +266 -0
- package/src/styles/index.css +26 -0
- package/src/styles/scroll-pane.css +10 -0
- package/src/styles/theme.css +138 -0
- package/src/styles/tokens.css +333 -0
- package/src/styles/transitions.css +226 -0
- package/src/styles/typography.css +277 -0
- package/src/styles/utilities.css +697 -0
- package/src/utils/cn.ts +6 -0
- package/src/utils/index.ts +1 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import type { TreeNode, TreeIndexEntry, SidebarSection, SidebarIndexEntry } from "../types";
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Generic composable — builds index + provides helpers with `getChildren` support
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Builds a flat index of all nodes in a tree for O(1) lookup with hierarchy
|
|
9
|
+
* metadata. Returns the index plus `isActive`, `isInActiveChain`, and
|
|
10
|
+
* `isDescendant` helpers that close over the index.
|
|
11
|
+
*
|
|
12
|
+
* Accepts an optional `getChildren` callback so callers with custom tree
|
|
13
|
+
* shapes (e.g. nodes whose children live under a different property name)
|
|
14
|
+
* don't need to conform to `TreeNode.children`.
|
|
15
|
+
*/
|
|
16
|
+
export function useTreeIndex<T extends TreeNode>(
|
|
17
|
+
roots: T[],
|
|
18
|
+
options?: { getChildren?: (node: T) => T[] | undefined },
|
|
19
|
+
) {
|
|
20
|
+
const getChildren =
|
|
21
|
+
options?.getChildren ?? ((n: T) => n.children as T[] | undefined);
|
|
22
|
+
const index = new Map<string, TreeIndexEntry<T>>();
|
|
23
|
+
|
|
24
|
+
function walk(
|
|
25
|
+
list: T[],
|
|
26
|
+
depth: number,
|
|
27
|
+
parentId: string | null,
|
|
28
|
+
rootId: string,
|
|
29
|
+
rootIndex: number,
|
|
30
|
+
) {
|
|
31
|
+
for (const node of list) {
|
|
32
|
+
const ri = depth === 0 ? roots.indexOf(node) : rootIndex;
|
|
33
|
+
const rid = depth === 0 ? node.id : rootId;
|
|
34
|
+
index.set(node.id, {
|
|
35
|
+
node,
|
|
36
|
+
depth,
|
|
37
|
+
rootId: rid,
|
|
38
|
+
parentId: depth === 0 ? node.id : parentId,
|
|
39
|
+
rootIndex: ri,
|
|
40
|
+
});
|
|
41
|
+
const children = getChildren(node);
|
|
42
|
+
if (children) {
|
|
43
|
+
walk(
|
|
44
|
+
children,
|
|
45
|
+
depth + 1,
|
|
46
|
+
depth === 0 ? node.id : parentId,
|
|
47
|
+
rid,
|
|
48
|
+
ri,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
walk(roots, 0, null, "", 0);
|
|
54
|
+
|
|
55
|
+
/** Check if `id` is the active node. */
|
|
56
|
+
function isActive(id: string, activeId: string | null): boolean {
|
|
57
|
+
return id === activeId;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Check if `id` is an ancestor of or equal to `activeId`. */
|
|
61
|
+
function isInActiveChain(id: string, activeId: string | null): boolean {
|
|
62
|
+
if (!activeId) return false;
|
|
63
|
+
const entry = index.get(activeId);
|
|
64
|
+
if (!entry) return false;
|
|
65
|
+
if (id === activeId) return true;
|
|
66
|
+
if (id === entry.parentId) return true;
|
|
67
|
+
const target = index.get(id);
|
|
68
|
+
if (!target) return false;
|
|
69
|
+
return isDescendant(activeId, id);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Check if `childId` is a descendant of `ancestorId`. */
|
|
73
|
+
function isDescendant(childId: string, ancestorId: string): boolean {
|
|
74
|
+
const ancestor = index.get(ancestorId);
|
|
75
|
+
if (!ancestor) return false;
|
|
76
|
+
const children = getChildren(ancestor.node);
|
|
77
|
+
if (!children) return false;
|
|
78
|
+
for (const child of children) {
|
|
79
|
+
if (child.id === childId) return true;
|
|
80
|
+
if (isDescendant(childId, child.id)) return true;
|
|
81
|
+
}
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return { index, isActive, isInActiveChain, isDescendant };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
// Legacy pure-function API — delegates to useTreeIndex under the hood
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Builds a flat index of all nodes in a sidebar tree for O(1) lookup
|
|
94
|
+
* with hierarchy metadata. Pure function, no Vue reactivity.
|
|
95
|
+
*
|
|
96
|
+
* @deprecated Prefer `useTreeIndex` which also returns helper functions.
|
|
97
|
+
*/
|
|
98
|
+
export function buildTreeIndex(roots: SidebarSection[]): Map<string, SidebarIndexEntry> {
|
|
99
|
+
return useTreeIndex(roots).index;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Check if `id` is the active section.
|
|
104
|
+
*
|
|
105
|
+
* Standalone variant kept for backward compatibility — the composable
|
|
106
|
+
* returned by `useTreeIndex` includes the same helper.
|
|
107
|
+
*/
|
|
108
|
+
export function isActive(id: string, activeId: string | null): boolean {
|
|
109
|
+
return id === activeId;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Check if `id` is an ancestor of or equal to `activeId`.
|
|
114
|
+
*
|
|
115
|
+
* Standalone variant kept for backward compatibility — the composable
|
|
116
|
+
* returned by `useTreeIndex` includes the same helper.
|
|
117
|
+
*/
|
|
118
|
+
export function isInActiveChain(
|
|
119
|
+
id: string,
|
|
120
|
+
activeId: string | null,
|
|
121
|
+
index: Map<string, SidebarIndexEntry>,
|
|
122
|
+
roots: SidebarSection[],
|
|
123
|
+
): boolean {
|
|
124
|
+
if (!activeId) return false;
|
|
125
|
+
const entry = index.get(activeId);
|
|
126
|
+
if (!entry) return false;
|
|
127
|
+
if (id === activeId) return true;
|
|
128
|
+
if (id === entry.parentId) return true;
|
|
129
|
+
const target = index.get(id);
|
|
130
|
+
if (!target) return false;
|
|
131
|
+
|
|
132
|
+
// Use the generic isDescendant via a one-off useTreeIndex call
|
|
133
|
+
// to avoid duplicating the recursive logic.
|
|
134
|
+
return _isDescendantLegacy(activeId, id, index);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Legacy isDescendant that operates on an externally-built index. */
|
|
138
|
+
function _isDescendantLegacy(
|
|
139
|
+
childId: string,
|
|
140
|
+
ancestorId: string,
|
|
141
|
+
index: Map<string, SidebarIndexEntry>,
|
|
142
|
+
): boolean {
|
|
143
|
+
const ancestor = index.get(ancestorId);
|
|
144
|
+
if (!ancestor) return false;
|
|
145
|
+
const children = ancestor.node.children;
|
|
146
|
+
if (!children) return false;
|
|
147
|
+
for (const child of children) {
|
|
148
|
+
if (child.id === childId) return true;
|
|
149
|
+
if (_isDescendantLegacy(childId, child.id, index)) return true;
|
|
150
|
+
}
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { default as ProgressiveSidebar } from "./ProgressiveSidebar.vue";
|
|
2
|
+
export { useSidebarState } from "./composables/useSidebarState";
|
|
3
|
+
export type { UseSidebarStateOptions } from "./composables/useSidebarState";
|
|
4
|
+
export { useSidebarFollow } from "./composables/useSidebarFollow";
|
|
5
|
+
export type { SidebarFollowOptions } from "./composables/useSidebarFollow";
|
|
6
|
+
export { useScrollTracker } from "./composables/useScrollTracker";
|
|
7
|
+
export { useTreeIndex, buildTreeIndex, isActive, isInActiveChain } from "./composables/useTreeIndex";
|
|
8
|
+
export type {
|
|
9
|
+
TreeNode,
|
|
10
|
+
TreeIndexEntry,
|
|
11
|
+
SidebarSection,
|
|
12
|
+
SidebarIndexEntry,
|
|
13
|
+
SidebarState,
|
|
14
|
+
ScrollTrackerOptions,
|
|
15
|
+
} from "./types";
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { Ref, ComputedRef } from "vue";
|
|
2
|
+
|
|
3
|
+
/** Minimal interface for tree-structured content with scroll targets. */
|
|
4
|
+
export interface TreeNode {
|
|
5
|
+
id: string;
|
|
6
|
+
children?: TreeNode[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Generic flat index entry for a tree node. */
|
|
10
|
+
export interface TreeIndexEntry<T extends TreeNode = TreeNode> {
|
|
11
|
+
node: T;
|
|
12
|
+
depth: number;
|
|
13
|
+
/** ID of the root-level ancestor (self.id when depth === 0). */
|
|
14
|
+
rootId: string;
|
|
15
|
+
/** Direct parent ID (self.id for root nodes). */
|
|
16
|
+
parentId: string | null;
|
|
17
|
+
/** Index within root-level nodes. */
|
|
18
|
+
rootIndex: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** A section in the sidebar tree. */
|
|
22
|
+
export interface SidebarSection extends TreeNode {
|
|
23
|
+
title: string;
|
|
24
|
+
children?: SidebarSection[];
|
|
25
|
+
level?: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Flat index entry for a sidebar section (backward-compatible alias). */
|
|
29
|
+
export type SidebarIndexEntry = TreeIndexEntry<SidebarSection>;
|
|
30
|
+
|
|
31
|
+
/** Options for scroll tracking. */
|
|
32
|
+
export interface ScrollTrackerOptions {
|
|
33
|
+
/** IntersectionObserver rootMargin. Default: "-20% 0px -60% 0px" */
|
|
34
|
+
rootMargin?: string;
|
|
35
|
+
threshold?: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Reactive state returned by `useSidebarState`. */
|
|
39
|
+
export interface SidebarState {
|
|
40
|
+
sections: SidebarSection[];
|
|
41
|
+
activeId: Ref<string | null>;
|
|
42
|
+
activeRootId: ComputedRef<string | null>;
|
|
43
|
+
treeIndex: Map<string, SidebarIndexEntry>;
|
|
44
|
+
isExpanded(id: string): boolean;
|
|
45
|
+
toggleSection(id: string): void;
|
|
46
|
+
navigateTo(id: string): void;
|
|
47
|
+
scrollToTop(): void;
|
|
48
|
+
isActive(id: string): boolean;
|
|
49
|
+
isInActiveChain(id: string): boolean;
|
|
50
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { type HTMLAttributes } from "vue";
|
|
3
|
+
import BouncyToggle from "./BouncyToggle.vue";
|
|
4
|
+
import type { ToggleOption } from "./BouncyToggle.vue";
|
|
5
|
+
|
|
6
|
+
export interface TabOption {
|
|
7
|
+
label: string;
|
|
8
|
+
value: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const props = withDefaults(defineProps<{
|
|
12
|
+
options: TabOption[];
|
|
13
|
+
modelValue: string;
|
|
14
|
+
/** "default" = subtle muted slider; "pill" = solid foreground pill */
|
|
15
|
+
variant?: "default" | "pill";
|
|
16
|
+
class?: HTMLAttributes["class"];
|
|
17
|
+
}>(), {
|
|
18
|
+
variant: "default",
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const emit = defineEmits<{
|
|
22
|
+
"update:modelValue": [value: string];
|
|
23
|
+
}>();
|
|
24
|
+
|
|
25
|
+
function onUpdate(value: string | string[]) {
|
|
26
|
+
emit("update:modelValue", value as string);
|
|
27
|
+
}
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<template>
|
|
31
|
+
<BouncyToggle
|
|
32
|
+
:options="(options as ToggleOption[])"
|
|
33
|
+
:model-value="modelValue"
|
|
34
|
+
:variant="variant"
|
|
35
|
+
:class="props.class"
|
|
36
|
+
:multi-select="false"
|
|
37
|
+
@update:model-value="onUpdate"
|
|
38
|
+
/>
|
|
39
|
+
</template>
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, watch, computed, onMounted, onUnmounted, nextTick, type HTMLAttributes } from "vue";
|
|
3
|
+
import { cn } from "../../../utils";
|
|
4
|
+
import {
|
|
5
|
+
Tooltip,
|
|
6
|
+
TooltipContent,
|
|
7
|
+
TooltipTrigger,
|
|
8
|
+
TooltipProvider,
|
|
9
|
+
} from "../../ui/tooltip";
|
|
10
|
+
|
|
11
|
+
export interface ToggleOption {
|
|
12
|
+
label: string;
|
|
13
|
+
value: string;
|
|
14
|
+
icon?: string;
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
tooltip?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface BouncyToggleProps {
|
|
20
|
+
options: ToggleOption[];
|
|
21
|
+
modelValue: string | string[];
|
|
22
|
+
multiSelect?: boolean;
|
|
23
|
+
/** "default" = subtle muted slider; "pill" = solid foreground pill */
|
|
24
|
+
variant?: "default" | "pill";
|
|
25
|
+
class?: HTMLAttributes["class"];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const props = withDefaults(defineProps<BouncyToggleProps>(), {
|
|
29
|
+
multiSelect: false,
|
|
30
|
+
variant: "default",
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const emit = defineEmits<{
|
|
34
|
+
"update:modelValue": [value: string | string[]];
|
|
35
|
+
}>();
|
|
36
|
+
|
|
37
|
+
const containerRef = ref<HTMLElement | null>(null);
|
|
38
|
+
const buttonRefs = ref<HTMLElement[]>([]);
|
|
39
|
+
|
|
40
|
+
// ── Computed state ──
|
|
41
|
+
|
|
42
|
+
const isPill = computed(() => props.variant === "pill");
|
|
43
|
+
|
|
44
|
+
const activeValues = computed<string[]>(() => {
|
|
45
|
+
if (props.multiSelect) {
|
|
46
|
+
return Array.isArray(props.modelValue) ? props.modelValue : [props.modelValue];
|
|
47
|
+
}
|
|
48
|
+
return [props.modelValue as string];
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const isActive = (value: string) => activeValues.value.includes(value);
|
|
52
|
+
|
|
53
|
+
// ── Single-select slider style ──
|
|
54
|
+
|
|
55
|
+
const singleSliderStyle = ref<Record<string, string>>({
|
|
56
|
+
width: "0px",
|
|
57
|
+
transform: "translateX(0px)",
|
|
58
|
+
opacity: "0",
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
function updateSingleSlider() {
|
|
62
|
+
if (props.multiSelect) return;
|
|
63
|
+
const idx = props.options.findIndex((o) => o.value === (props.modelValue as string));
|
|
64
|
+
if (idx < 0 || !buttonRefs.value[idx]) return;
|
|
65
|
+
const btn = buttonRefs.value[idx];
|
|
66
|
+
singleSliderStyle.value = {
|
|
67
|
+
width: `${btn.offsetWidth}px`,
|
|
68
|
+
transform: `translateX(${btn.offsetLeft}px)`,
|
|
69
|
+
opacity: "1",
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ── Multi-select slider styles ──
|
|
74
|
+
|
|
75
|
+
const multiSliderStyles = ref<Record<string, Record<string, string>>>({});
|
|
76
|
+
|
|
77
|
+
function updateMultiSliders() {
|
|
78
|
+
if (!props.multiSelect) return;
|
|
79
|
+
const styles: Record<string, Record<string, string>> = {};
|
|
80
|
+
for (const value of activeValues.value) {
|
|
81
|
+
const optionIdx = props.options.findIndex((o) => o.value === value);
|
|
82
|
+
const btn = buttonRefs.value[optionIdx];
|
|
83
|
+
if (!btn) continue;
|
|
84
|
+
styles[value] = {
|
|
85
|
+
width: `${btn.offsetWidth}px`,
|
|
86
|
+
transform: `translateX(${btn.offsetLeft}px)`,
|
|
87
|
+
opacity: "1",
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
multiSliderStyles.value = styles;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── Unified update ──
|
|
94
|
+
|
|
95
|
+
function updateSliders() {
|
|
96
|
+
if (props.multiSelect) {
|
|
97
|
+
updateMultiSliders();
|
|
98
|
+
} else {
|
|
99
|
+
updateSingleSlider();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ── Button press animation (Web Animations API) ──
|
|
104
|
+
|
|
105
|
+
function animatePress(btn: HTMLElement) {
|
|
106
|
+
// Cancel any in-flight press animations on this button
|
|
107
|
+
btn.getAnimations().forEach((a) => a.cancel());
|
|
108
|
+
|
|
109
|
+
btn.animate(
|
|
110
|
+
[
|
|
111
|
+
{ transform: "scale(1)" },
|
|
112
|
+
{ transform: "scale(0.93)", offset: 0.25 },
|
|
113
|
+
{ transform: "scale(1.02)", offset: 0.7 },
|
|
114
|
+
{ transform: "scale(1)" },
|
|
115
|
+
],
|
|
116
|
+
{
|
|
117
|
+
duration: 200,
|
|
118
|
+
easing: "cubic-bezier(0.175, 0.885, 0.32, 1.275)", // --spring-bouncy (Web Animations API needs literal value)
|
|
119
|
+
},
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ── Selection handler ──
|
|
124
|
+
|
|
125
|
+
function select(value: string, idx: number) {
|
|
126
|
+
const option = props.options[idx];
|
|
127
|
+
if (option?.disabled) return;
|
|
128
|
+
|
|
129
|
+
const btn = buttonRefs.value[idx];
|
|
130
|
+
if (btn) {
|
|
131
|
+
animatePress(btn);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (props.multiSelect) {
|
|
135
|
+
const current = [...activeValues.value];
|
|
136
|
+
const existingIdx = current.indexOf(value);
|
|
137
|
+
if (existingIdx > -1) {
|
|
138
|
+
// Don't deselect the last remaining value
|
|
139
|
+
if (current.length > 1) {
|
|
140
|
+
current.splice(existingIdx, 1);
|
|
141
|
+
}
|
|
142
|
+
} else {
|
|
143
|
+
current.push(value);
|
|
144
|
+
}
|
|
145
|
+
emit("update:modelValue", current);
|
|
146
|
+
} else {
|
|
147
|
+
emit("update:modelValue", value);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ── Watchers ──
|
|
152
|
+
|
|
153
|
+
watch(() => props.modelValue, () => nextTick(updateSliders), { deep: true });
|
|
154
|
+
watch(() => props.options, () => nextTick(updateSliders), { deep: true });
|
|
155
|
+
|
|
156
|
+
// ── Lifecycle ──
|
|
157
|
+
|
|
158
|
+
let resizeObserver: ResizeObserver | null = null;
|
|
159
|
+
|
|
160
|
+
onMounted(() => {
|
|
161
|
+
nextTick(updateSliders);
|
|
162
|
+
if (containerRef.value) {
|
|
163
|
+
resizeObserver = new ResizeObserver(() => updateSliders());
|
|
164
|
+
resizeObserver.observe(containerRef.value);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
onUnmounted(() => {
|
|
169
|
+
resizeObserver?.disconnect();
|
|
170
|
+
});
|
|
171
|
+
</script>
|
|
172
|
+
|
|
173
|
+
<template>
|
|
174
|
+
<div
|
|
175
|
+
ref="containerRef"
|
|
176
|
+
:class="cn(
|
|
177
|
+
isPill ? 'bouncy-toggle bouncy-toggle--pill' : 'bouncy-toggle',
|
|
178
|
+
props.class,
|
|
179
|
+
)"
|
|
180
|
+
>
|
|
181
|
+
<!-- Single-select slider -->
|
|
182
|
+
<div
|
|
183
|
+
v-if="!multiSelect"
|
|
184
|
+
:class="isPill ? 'bouncy-slider bouncy-slider--pill' : 'bouncy-slider'"
|
|
185
|
+
:style="singleSliderStyle"
|
|
186
|
+
/>
|
|
187
|
+
|
|
188
|
+
<!-- Multi-select sliders (one per active value) -->
|
|
189
|
+
<template v-if="multiSelect">
|
|
190
|
+
<div
|
|
191
|
+
v-for="value in activeValues"
|
|
192
|
+
:key="'slider-' + value"
|
|
193
|
+
:class="isPill ? 'bouncy-slider bouncy-slider--pill' : 'bouncy-slider'"
|
|
194
|
+
:style="multiSliderStyles[value] ?? { opacity: '0' }"
|
|
195
|
+
/>
|
|
196
|
+
</template>
|
|
197
|
+
|
|
198
|
+
<!-- Buttons -->
|
|
199
|
+
<template v-for="(option, idx) in options" :key="option.value">
|
|
200
|
+
<!-- With tooltip -->
|
|
201
|
+
<TooltipProvider v-if="option.tooltip" :delay-duration="200">
|
|
202
|
+
<Tooltip>
|
|
203
|
+
<TooltipTrigger as-child>
|
|
204
|
+
<button
|
|
205
|
+
:ref="(el) => { if (el) buttonRefs[idx] = el as HTMLElement }"
|
|
206
|
+
:class="[
|
|
207
|
+
isPill ? 'bouncy-btn bouncy-btn--pill' : 'bouncy-btn',
|
|
208
|
+
{ 'is-active': isActive(option.value) },
|
|
209
|
+
option.disabled && 'is-disabled',
|
|
210
|
+
]"
|
|
211
|
+
:disabled="option.disabled"
|
|
212
|
+
@click="select(option.value, idx)"
|
|
213
|
+
>
|
|
214
|
+
<slot name="option" :option="option" :active="isActive(option.value)">
|
|
215
|
+
{{ option.label }}
|
|
216
|
+
</slot>
|
|
217
|
+
</button>
|
|
218
|
+
</TooltipTrigger>
|
|
219
|
+
<TooltipContent side="bottom" :side-offset="8">
|
|
220
|
+
{{ option.tooltip }}
|
|
221
|
+
</TooltipContent>
|
|
222
|
+
</Tooltip>
|
|
223
|
+
</TooltipProvider>
|
|
224
|
+
|
|
225
|
+
<!-- Without tooltip -->
|
|
226
|
+
<button
|
|
227
|
+
v-else
|
|
228
|
+
:ref="(el) => { if (el) buttonRefs[idx] = el as HTMLElement }"
|
|
229
|
+
:class="[
|
|
230
|
+
isPill ? 'bouncy-btn bouncy-btn--pill' : 'bouncy-btn',
|
|
231
|
+
{ 'is-active': isActive(option.value) },
|
|
232
|
+
option.disabled && 'is-disabled',
|
|
233
|
+
]"
|
|
234
|
+
:disabled="option.disabled"
|
|
235
|
+
@click="select(option.value, idx)"
|
|
236
|
+
>
|
|
237
|
+
<slot name="option" :option="option" :active="isActive(option.value)">
|
|
238
|
+
{{ option.label }}
|
|
239
|
+
</slot>
|
|
240
|
+
</button>
|
|
241
|
+
</template>
|
|
242
|
+
</div>
|
|
243
|
+
</template>
|
|
244
|
+
|
|
245
|
+
<style scoped>
|
|
246
|
+
/* ── Default variant ── */
|
|
247
|
+
.bouncy-toggle {
|
|
248
|
+
position: relative;
|
|
249
|
+
display: inline-grid;
|
|
250
|
+
grid-auto-flow: column;
|
|
251
|
+
grid-auto-columns: 1fr;
|
|
252
|
+
padding: 0.1875rem;
|
|
253
|
+
border-radius: 0.4375rem;
|
|
254
|
+
background: color-mix(in srgb, var(--muted) 50%, transparent);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
@media (min-width: 640px) {
|
|
258
|
+
.bouncy-toggle {
|
|
259
|
+
padding: 0.25rem;
|
|
260
|
+
border-radius: 0.5rem;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.bouncy-slider {
|
|
265
|
+
position: absolute;
|
|
266
|
+
background: var(--background);
|
|
267
|
+
z-index: 0;
|
|
268
|
+
inset-block: 0.1875rem;
|
|
269
|
+
border-radius: 0.3125rem;
|
|
270
|
+
box-shadow:
|
|
271
|
+
0 1px 3px rgba(0, 0, 0, 0.08),
|
|
272
|
+
0 0 0 1px color-mix(in srgb, var(--border) 30%, transparent);
|
|
273
|
+
transition:
|
|
274
|
+
transform var(--duration-normal) var(--spring-snappy),
|
|
275
|
+
width var(--duration-normal) var(--spring-snappy),
|
|
276
|
+
opacity var(--duration-fast) ease;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
@media (min-width: 640px) {
|
|
280
|
+
.bouncy-slider {
|
|
281
|
+
inset-block: 0.25rem;
|
|
282
|
+
border-radius: 0.375rem;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.bouncy-btn {
|
|
287
|
+
position: relative;
|
|
288
|
+
z-index: 10;
|
|
289
|
+
border: none;
|
|
290
|
+
background: none;
|
|
291
|
+
font-weight: 500;
|
|
292
|
+
cursor: pointer;
|
|
293
|
+
white-space: nowrap;
|
|
294
|
+
padding: 0.25rem 0.625rem;
|
|
295
|
+
border-radius: 0.3125rem;
|
|
296
|
+
font: inherit;
|
|
297
|
+
font-size: 0.8125rem;
|
|
298
|
+
color: var(--muted-foreground);
|
|
299
|
+
transition: color var(--duration-fast) ease;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
@media (min-width: 640px) {
|
|
303
|
+
.bouncy-btn {
|
|
304
|
+
padding: 0.3125rem 0.75rem;
|
|
305
|
+
font-size: 0.875rem;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
.bouncy-btn.is-active {
|
|
310
|
+
color: var(--foreground);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.bouncy-btn.is-disabled {
|
|
314
|
+
opacity: 0.4;
|
|
315
|
+
cursor: not-allowed;
|
|
316
|
+
pointer-events: none;
|
|
317
|
+
filter: blur(0.5px);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/* ── Pill variant ── */
|
|
321
|
+
.bouncy-toggle--pill {
|
|
322
|
+
border-radius: var(--radius-pill);
|
|
323
|
+
background: color-mix(in srgb, var(--foreground) 5%, transparent);
|
|
324
|
+
padding: 0.125rem;
|
|
325
|
+
gap: 0.125rem;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.bouncy-slider--pill {
|
|
329
|
+
border-radius: var(--radius-pill);
|
|
330
|
+
background: var(--foreground);
|
|
331
|
+
box-shadow: none;
|
|
332
|
+
inset-block: 0.125rem;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.bouncy-btn--pill {
|
|
336
|
+
border-radius: var(--radius-pill);
|
|
337
|
+
padding: 0.125rem 0.625rem;
|
|
338
|
+
font-size: 0.75rem;
|
|
339
|
+
font-weight: 500;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
@media (min-width: 640px) {
|
|
343
|
+
.bouncy-btn--pill {
|
|
344
|
+
padding: 0.125rem 0.625rem;
|
|
345
|
+
font-size: 0.75rem;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.bouncy-btn--pill.is-active {
|
|
350
|
+
color: var(--background);
|
|
351
|
+
}
|
|
352
|
+
</style>
|