@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,173 @@
|
|
|
1
|
+
// glass-ui WebGPU glass shader
|
|
2
|
+
// Features: variable blur, real-time refraction, caustics, dynamic specular
|
|
3
|
+
//
|
|
4
|
+
// This shader is designed to be used with a full-screen quad.
|
|
5
|
+
// It reads from a background texture and applies glass effects
|
|
6
|
+
// for each registered glass panel.
|
|
7
|
+
|
|
8
|
+
struct Uniforms {
|
|
9
|
+
resolution: vec2f, // Canvas resolution
|
|
10
|
+
glass_bounds: vec4f, // Glass panel rect (x, y, w, h) in UV space
|
|
11
|
+
blur_radius: f32, // Base blur radius
|
|
12
|
+
refraction_strength: f32, // Snell's law displacement strength
|
|
13
|
+
chromatic_aberration: f32, // Edge chromatic aberration strength
|
|
14
|
+
caustic_intensity: f32, // Caustic light pattern intensity
|
|
15
|
+
light_pos: vec2f, // Virtual light source position (UV)
|
|
16
|
+
time: f32, // Animation time (seconds)
|
|
17
|
+
_pad: f32, // Alignment padding
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
@group(0) @binding(0) var<uniform> u: Uniforms;
|
|
21
|
+
@group(0) @binding(1) var background_texture: texture_2d<f32>;
|
|
22
|
+
@group(0) @binding(2) var background_sampler: sampler;
|
|
23
|
+
|
|
24
|
+
// Pseudo-random hash for noise generation
|
|
25
|
+
fn hash21(p: vec2f) -> f32 {
|
|
26
|
+
var p3 = fract(vec3f(p.xyx) * 0.1031);
|
|
27
|
+
p3 += dot(p3, p3.yzx + 33.33);
|
|
28
|
+
return fract((p3.x + p3.y) * p3.z);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Simplex-style noise for caustics
|
|
32
|
+
fn noise(p: vec2f) -> f32 {
|
|
33
|
+
let i = floor(p);
|
|
34
|
+
let f = fract(p);
|
|
35
|
+
let u_curve = f * f * (3.0 - 2.0 * f);
|
|
36
|
+
|
|
37
|
+
return mix(
|
|
38
|
+
mix(hash21(i), hash21(i + vec2f(1.0, 0.0)), u_curve.x),
|
|
39
|
+
mix(hash21(i + vec2f(0.0, 1.0)), hash21(i + vec2f(1.0, 1.0)), u_curve.x),
|
|
40
|
+
u_curve.y
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Fractal Brownian Motion for caustic patterns
|
|
45
|
+
fn fbm(p: vec2f) -> f32 {
|
|
46
|
+
var value = 0.0;
|
|
47
|
+
var amplitude = 0.5;
|
|
48
|
+
var frequency = 1.0;
|
|
49
|
+
var pos = p;
|
|
50
|
+
|
|
51
|
+
for (var i = 0; i < 5; i++) {
|
|
52
|
+
value += amplitude * noise(pos * frequency);
|
|
53
|
+
pos += vec2f(1.7, 9.2);
|
|
54
|
+
amplitude *= 0.5;
|
|
55
|
+
frequency *= 2.0;
|
|
56
|
+
}
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Caustic light pattern — animated voronoi-like cells
|
|
61
|
+
fn caustics(uv: vec2f, time: f32) -> f32 {
|
|
62
|
+
let p = uv * 8.0 + vec2f(time * 0.3, time * 0.2);
|
|
63
|
+
let n1 = fbm(p);
|
|
64
|
+
let n2 = fbm(p + vec2f(1.3, 2.7) + time * 0.1);
|
|
65
|
+
let pattern = fbm(p + vec2f(n1, n2) * 2.0);
|
|
66
|
+
return smoothstep(0.3, 0.7, pattern);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Fresnel approximation (Schlick)
|
|
70
|
+
fn fresnel(cos_theta: f32, ior: f32) -> f32 {
|
|
71
|
+
let r0 = pow((1.0 - ior) / (1.0 + ior), 2.0);
|
|
72
|
+
return r0 + (1.0 - r0) * pow(1.0 - cos_theta, 5.0);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Distance from UV to nearest edge of glass panel
|
|
76
|
+
fn edge_distance(uv: vec2f) -> f32 {
|
|
77
|
+
let glass_min = u.glass_bounds.xy;
|
|
78
|
+
let glass_max = u.glass_bounds.xy + u.glass_bounds.zw;
|
|
79
|
+
let d = min(uv - glass_min, glass_max - uv);
|
|
80
|
+
return min(d.x, d.y);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@fragment
|
|
84
|
+
fn fs_main(@builtin(position) frag_coord: vec4f) -> @location(0) vec4f {
|
|
85
|
+
let uv = frag_coord.xy / u.resolution;
|
|
86
|
+
let pixel_size = 1.0 / u.resolution;
|
|
87
|
+
|
|
88
|
+
// Check if fragment is inside the glass panel
|
|
89
|
+
let glass_min = u.glass_bounds.xy;
|
|
90
|
+
let glass_max = u.glass_bounds.xy + u.glass_bounds.zw;
|
|
91
|
+
|
|
92
|
+
if (uv.x < glass_min.x || uv.x > glass_max.x ||
|
|
93
|
+
uv.y < glass_min.y || uv.y > glass_max.y) {
|
|
94
|
+
return textureSample(background_texture, background_sampler, uv);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Progressive blur — variable radius based on edge distance
|
|
98
|
+
let edge_dist = edge_distance(uv);
|
|
99
|
+
let max_edge_dist = min(u.glass_bounds.z, u.glass_bounds.w) * 0.5;
|
|
100
|
+
let blur_factor = smoothstep(0.0, max_edge_dist * 0.3, edge_dist);
|
|
101
|
+
let effective_blur = u.blur_radius * (0.4 + 0.6 * blur_factor);
|
|
102
|
+
|
|
103
|
+
// Refraction displacement
|
|
104
|
+
let center = u.glass_bounds.xy + u.glass_bounds.zw * 0.5;
|
|
105
|
+
let from_center = uv - center;
|
|
106
|
+
let refraction_offset = from_center * u.refraction_strength * 0.02;
|
|
107
|
+
|
|
108
|
+
// Depth-aware blur kernel
|
|
109
|
+
var blur_color = vec3f(0.0);
|
|
110
|
+
var total_weight = 0.0;
|
|
111
|
+
let samples = i32(min(effective_blur * 2.0, 16.0));
|
|
112
|
+
let sigma = max(effective_blur * 0.5, 0.1);
|
|
113
|
+
|
|
114
|
+
for (var x = -8; x <= 8; x++) {
|
|
115
|
+
for (var y = -8; y <= 8; y++) {
|
|
116
|
+
if (abs(x) > samples || abs(y) > samples) { continue; }
|
|
117
|
+
|
|
118
|
+
let offset = vec2f(f32(x), f32(y)) * pixel_size * effective_blur * 0.1;
|
|
119
|
+
let dist_sq = f32(x * x + y * y);
|
|
120
|
+
let weight = exp(-dist_sq / (2.0 * sigma * sigma));
|
|
121
|
+
|
|
122
|
+
let sample_uv = uv + offset + refraction_offset;
|
|
123
|
+
blur_color += textureSample(background_texture, background_sampler, sample_uv).rgb * weight;
|
|
124
|
+
total_weight += weight;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
blur_color /= total_weight;
|
|
128
|
+
|
|
129
|
+
// Chromatic aberration at edges
|
|
130
|
+
if (u.chromatic_aberration > 0.0) {
|
|
131
|
+
let edge_strength = 1.0 - smoothstep(0.0, max_edge_dist * 0.15, edge_dist);
|
|
132
|
+
let aberration_dir = normalize(from_center) * edge_strength * u.chromatic_aberration * 0.003;
|
|
133
|
+
|
|
134
|
+
let r = textureSample(background_texture, background_sampler, uv + refraction_offset + aberration_dir).r;
|
|
135
|
+
let b = textureSample(background_texture, background_sampler, uv + refraction_offset - aberration_dir).b;
|
|
136
|
+
blur_color.x = mix(blur_color.x, r, edge_strength * 0.5);
|
|
137
|
+
blur_color.z = mix(blur_color.z, b, edge_strength * 0.5);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Caustic light pattern
|
|
141
|
+
var caustic_light = 0.0;
|
|
142
|
+
if (u.caustic_intensity > 0.0) {
|
|
143
|
+
let local_uv = (uv - glass_min) / u.glass_bounds.zw;
|
|
144
|
+
caustic_light = caustics(local_uv, u.time) * u.caustic_intensity;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Specular highlight with cursor-tracking light
|
|
148
|
+
let normal_xy = from_center / (u.glass_bounds.zw * 0.5);
|
|
149
|
+
let normal = normalize(vec3f(normal_xy * 0.3, 1.0));
|
|
150
|
+
let view_dir = vec3f(0.0, 0.0, 1.0);
|
|
151
|
+
let cos_theta = max(dot(view_dir, normal), 0.0);
|
|
152
|
+
var spec = fresnel(cos_theta, 1.5);
|
|
153
|
+
|
|
154
|
+
// Light-source-aware specular
|
|
155
|
+
let light_dir = normalize(u.light_pos - uv);
|
|
156
|
+
let light_dot = max(dot(normal_xy, light_dir), 0.0);
|
|
157
|
+
spec *= 0.3 + 0.7 * light_dot;
|
|
158
|
+
|
|
159
|
+
// Combine
|
|
160
|
+
let result = blur_color + vec3f(spec * 0.15 + caustic_light * 0.08);
|
|
161
|
+
let alpha = 0.85 + spec * 0.1;
|
|
162
|
+
|
|
163
|
+
return vec4f(result, alpha);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Vertex shader — full-screen triangle
|
|
167
|
+
@vertex
|
|
168
|
+
fn vs_main(@builtin(vertex_index) vertex_index: u32) -> @builtin(position) vec4f {
|
|
169
|
+
// Full-screen triangle trick: 3 vertices cover the entire viewport
|
|
170
|
+
let x = f32(i32(vertex_index) / 2) * 4.0 - 1.0;
|
|
171
|
+
let y = f32(i32(vertex_index) % 2) * 4.0 - 1.0;
|
|
172
|
+
return vec4f(x, y, 0.0, 1.0);
|
|
173
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export * from "./interaction";
|
|
2
|
+
export { copyToClipboard } from "./useClipboard";
|
|
3
|
+
export { useGlobalDark, type UseGlobalDarkOptions } from "./useGlobalDark";
|
|
4
|
+
export * from "./useKeyboardShortcuts";
|
|
5
|
+
export * from "./useWatercolorBlob";
|
|
6
|
+
export { useCharSplit } from "./useCharSplit";
|
|
7
|
+
export * from "./glass";
|
|
8
|
+
export * from "./pagination";
|
|
9
|
+
export * from "./prng";
|
|
10
|
+
export * from "./virtual";
|
|
11
|
+
export * from "../components/custom/infinite-scroll/composables";
|
|
12
|
+
|
|
13
|
+
// Sidebar composables and types
|
|
14
|
+
export {
|
|
15
|
+
useTreeIndex,
|
|
16
|
+
useScrollTracker,
|
|
17
|
+
useSidebarFollow,
|
|
18
|
+
useSidebarState,
|
|
19
|
+
buildTreeIndex,
|
|
20
|
+
isActive,
|
|
21
|
+
isInActiveChain,
|
|
22
|
+
} from "../components/custom/sidebar";
|
|
23
|
+
export type {
|
|
24
|
+
TreeNode,
|
|
25
|
+
TreeIndexEntry,
|
|
26
|
+
SidebarSection,
|
|
27
|
+
SidebarIndexEntry,
|
|
28
|
+
SidebarState,
|
|
29
|
+
ScrollTrackerOptions,
|
|
30
|
+
SidebarFollowOptions,
|
|
31
|
+
UseSidebarStateOptions,
|
|
32
|
+
} from "../components/custom/sidebar";
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Ref, MaybeRefOrGetter } from "vue";
|
|
2
|
+
|
|
3
|
+
export interface InfiniteScrollOptions {
|
|
4
|
+
/** The scrollable container element. Defaults to window if not provided. */
|
|
5
|
+
scrollContainer?: Ref<HTMLElement | null>;
|
|
6
|
+
/** Distance in pixels from the bottom to trigger loading (default: 200) */
|
|
7
|
+
threshold?: number;
|
|
8
|
+
/** Whether more data is available */
|
|
9
|
+
hasMore: MaybeRefOrGetter<boolean>;
|
|
10
|
+
/** Whether data is currently loading */
|
|
11
|
+
isLoading: MaybeRefOrGetter<boolean>;
|
|
12
|
+
/** Callback invoked when the sentinel enters the viewport */
|
|
13
|
+
onLoadMore: () => void | Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface InfiniteScrollReturn {
|
|
17
|
+
/** Ref to bind to the sentinel element */
|
|
18
|
+
sentinelRef: Ref<HTMLElement | null>;
|
|
19
|
+
/** Error message from the last failed load, or null */
|
|
20
|
+
error: Ref<string | null>;
|
|
21
|
+
/** Manually trigger a check (e.g., after DOM updates) */
|
|
22
|
+
check: () => void;
|
|
23
|
+
/** Stop observing and reset error state */
|
|
24
|
+
stop: () => void;
|
|
25
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { ref, watch, onScopeDispose, toValue, type MaybeRefOrGetter } from "vue";
|
|
2
|
+
import type { InfiniteScrollOptions, InfiniteScrollReturn } from "./types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Composable for infinite scroll with IntersectionObserver.
|
|
6
|
+
*
|
|
7
|
+
* Watches a sentinel element at the bottom of the scrollable area.
|
|
8
|
+
* When it enters the viewport and `hasMore` is true / `isLoading` is false,
|
|
9
|
+
* the `onLoadMore` callback fires.
|
|
10
|
+
*/
|
|
11
|
+
export function useInfiniteScroll(options: InfiniteScrollOptions): InfiniteScrollReturn {
|
|
12
|
+
const { threshold = 200, hasMore, isLoading, onLoadMore } = options;
|
|
13
|
+
const sentinelRef = ref<HTMLElement | null>(null);
|
|
14
|
+
const error = ref<string | null>(null);
|
|
15
|
+
let observer: IntersectionObserver | null = null;
|
|
16
|
+
|
|
17
|
+
function shouldLoad(): boolean {
|
|
18
|
+
return toValue(hasMore) && !toValue(isLoading);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function handleIntersect(entries: IntersectionObserverEntry[]) {
|
|
22
|
+
for (const entry of entries) {
|
|
23
|
+
if (entry.isIntersecting && shouldLoad()) {
|
|
24
|
+
error.value = null;
|
|
25
|
+
try {
|
|
26
|
+
const result = onLoadMore();
|
|
27
|
+
// Handle async onLoadMore callbacks
|
|
28
|
+
if (result && typeof (result as any).catch === "function") {
|
|
29
|
+
(result as Promise<unknown>).catch((e: unknown) => {
|
|
30
|
+
error.value =
|
|
31
|
+
e instanceof Error ? e.message : "Failed to load more";
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
} catch (e) {
|
|
35
|
+
error.value =
|
|
36
|
+
e instanceof Error ? e.message : "Failed to load more";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function setupObserver(el: HTMLElement) {
|
|
43
|
+
teardown();
|
|
44
|
+
observer = new IntersectionObserver(handleIntersect, {
|
|
45
|
+
root: options.scrollContainer?.value ?? null,
|
|
46
|
+
rootMargin: `0px 0px ${threshold}px 0px`,
|
|
47
|
+
});
|
|
48
|
+
observer.observe(el);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function teardown() {
|
|
52
|
+
if (observer) {
|
|
53
|
+
observer.disconnect();
|
|
54
|
+
observer = null;
|
|
55
|
+
}
|
|
56
|
+
error.value = null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function check() {
|
|
60
|
+
if (sentinelRef.value && shouldLoad()) {
|
|
61
|
+
error.value = null;
|
|
62
|
+
try {
|
|
63
|
+
const result = onLoadMore();
|
|
64
|
+
if (result && typeof (result as any).catch === "function") {
|
|
65
|
+
(result as Promise<unknown>).catch((e: unknown) => {
|
|
66
|
+
error.value =
|
|
67
|
+
e instanceof Error ? e.message : "Failed to load more";
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
} catch (e) {
|
|
71
|
+
error.value =
|
|
72
|
+
e instanceof Error ? e.message : "Failed to load more";
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
watch(sentinelRef, (el) => {
|
|
78
|
+
if (el) setupObserver(el);
|
|
79
|
+
else teardown();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Re-check when loading finishes (new content may be short enough to need another load)
|
|
83
|
+
watch(
|
|
84
|
+
() => toValue(isLoading),
|
|
85
|
+
(loading) => {
|
|
86
|
+
if (!loading && sentinelRef.value) {
|
|
87
|
+
// Defer to next tick so DOM updates first
|
|
88
|
+
requestAnimationFrame(check);
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
onScopeDispose(teardown);
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
sentinelRef,
|
|
97
|
+
error,
|
|
98
|
+
check,
|
|
99
|
+
stop: teardown,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
const DEFAULT_EXPAND_DURATION = "var(--duration-normal, 0.3s)";
|
|
2
|
+
const DEFAULT_COLLAPSE_DURATION = "var(--duration-fast, 0.2s)";
|
|
3
|
+
const EXPAND_EASING = "var(--spring-smooth, cubic-bezier(0.16, 1, 0.3, 1))";
|
|
4
|
+
const COLLAPSE_EASING = "var(--ease-out, cubic-bezier(0, 0, 0.2, 1))";
|
|
5
|
+
|
|
6
|
+
export function useHeightTransition(options?: {
|
|
7
|
+
expandDuration?: string;
|
|
8
|
+
collapseDuration?: string;
|
|
9
|
+
onBeforeCollapse?: () => void;
|
|
10
|
+
onAfterExpand?: () => void;
|
|
11
|
+
}) {
|
|
12
|
+
const expandDuration = options?.expandDuration ?? DEFAULT_EXPAND_DURATION;
|
|
13
|
+
const collapseDuration = options?.collapseDuration ?? DEFAULT_COLLAPSE_DURATION;
|
|
14
|
+
|
|
15
|
+
function onBeforeEnter(el: Element) {
|
|
16
|
+
const htmlEl = el as HTMLElement;
|
|
17
|
+
htmlEl.style.height = "0";
|
|
18
|
+
htmlEl.style.opacity = "0";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function onEnter(el: Element, done: () => void) {
|
|
22
|
+
const htmlEl = el as HTMLElement;
|
|
23
|
+
const targetHeight = htmlEl.scrollHeight;
|
|
24
|
+
htmlEl.style.transition = `height ${expandDuration} ${EXPAND_EASING}, opacity ${expandDuration} ease`;
|
|
25
|
+
// Force reflow
|
|
26
|
+
void htmlEl.offsetHeight;
|
|
27
|
+
htmlEl.style.height = `${targetHeight}px`;
|
|
28
|
+
htmlEl.style.opacity = "1";
|
|
29
|
+
htmlEl.addEventListener("transitionend", function handler(e) {
|
|
30
|
+
if (e.propertyName !== "height") return;
|
|
31
|
+
htmlEl.removeEventListener("transitionend", handler);
|
|
32
|
+
done();
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function onAfterEnter(el: Element) {
|
|
37
|
+
const htmlEl = el as HTMLElement;
|
|
38
|
+
htmlEl.style.height = "";
|
|
39
|
+
htmlEl.style.transition = "";
|
|
40
|
+
htmlEl.style.opacity = "";
|
|
41
|
+
options?.onAfterExpand?.();
|
|
42
|
+
htmlEl.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function onBeforeLeave(el: Element) {
|
|
46
|
+
const htmlEl = el as HTMLElement;
|
|
47
|
+
options?.onBeforeCollapse?.();
|
|
48
|
+
htmlEl.style.height = `${htmlEl.scrollHeight}px`;
|
|
49
|
+
// Force reflow
|
|
50
|
+
void htmlEl.offsetHeight;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function onLeave(el: Element, done: () => void) {
|
|
54
|
+
const htmlEl = el as HTMLElement;
|
|
55
|
+
htmlEl.style.transition = `height ${collapseDuration} ${COLLAPSE_EASING}, opacity ${collapseDuration} ease`;
|
|
56
|
+
// Force reflow
|
|
57
|
+
void htmlEl.offsetHeight;
|
|
58
|
+
htmlEl.style.height = "0";
|
|
59
|
+
htmlEl.style.opacity = "0";
|
|
60
|
+
htmlEl.addEventListener("transitionend", function handler(e) {
|
|
61
|
+
if (e.propertyName !== "height") return;
|
|
62
|
+
htmlEl.removeEventListener("transitionend", handler);
|
|
63
|
+
done();
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function onAfterLeave(el: Element) {
|
|
68
|
+
const htmlEl = el as HTMLElement;
|
|
69
|
+
htmlEl.style.height = "";
|
|
70
|
+
htmlEl.style.transition = "";
|
|
71
|
+
htmlEl.style.opacity = "";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
onBeforeEnter,
|
|
76
|
+
onEnter,
|
|
77
|
+
onAfterEnter,
|
|
78
|
+
onBeforeLeave,
|
|
79
|
+
onLeave,
|
|
80
|
+
onAfterLeave,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { ref, reactive, nextTick } from "vue";
|
|
2
|
+
import { useLeaveTimer } from "./useLeaveTimer";
|
|
3
|
+
|
|
4
|
+
const CAN_HOVER = typeof window !== "undefined" && window.matchMedia("(hover: hover)").matches;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Shared hover-timer + floating-panel positioning pattern.
|
|
8
|
+
* Used by components with hover-activated popover panels (e.g. swatch grids).
|
|
9
|
+
*/
|
|
10
|
+
export function useHoverPopover(options?: { canHover?: boolean }) {
|
|
11
|
+
const canHover = options?.canHover ?? CAN_HOVER;
|
|
12
|
+
|
|
13
|
+
const openIndex = ref<number | null>(null);
|
|
14
|
+
const style = reactive({ top: "0px", left: "0px" });
|
|
15
|
+
const leaveTimer = useLeaveTimer(250);
|
|
16
|
+
|
|
17
|
+
function positionPanel(swatchEl: Element, offsetY = -42) {
|
|
18
|
+
const rect = swatchEl.getBoundingClientRect();
|
|
19
|
+
style.top = `${rect.top + offsetY}px`;
|
|
20
|
+
style.left = `${rect.left + rect.width / 2}px`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function onHover(index: number, e: PointerEvent) {
|
|
24
|
+
if (!canHover || e.pointerType === "touch") return;
|
|
25
|
+
cancelLeave();
|
|
26
|
+
openIndex.value = index;
|
|
27
|
+
nextTick(() => positionPanel(e.currentTarget as Element));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function onLeave() {
|
|
31
|
+
if (!canHover) return;
|
|
32
|
+
leaveTimer.schedule(() => { openIndex.value = null; });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function cancelLeave() {
|
|
36
|
+
leaveTimer.cancel();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function close() {
|
|
40
|
+
cancelLeave();
|
|
41
|
+
openIndex.value = null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function onPopoverUpdateTouch(open: boolean, index: number) {
|
|
45
|
+
openIndex.value = open ? index : null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function onSwatchClick(index: number) {
|
|
49
|
+
cancelLeave();
|
|
50
|
+
openIndex.value = openIndex.value === index ? null : index;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
canHover,
|
|
55
|
+
openIndex,
|
|
56
|
+
style,
|
|
57
|
+
onHover,
|
|
58
|
+
onLeave,
|
|
59
|
+
cancelLeave,
|
|
60
|
+
close,
|
|
61
|
+
onPopoverUpdateTouch,
|
|
62
|
+
onSwatchClick,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { ref, computed, onBeforeUnmount } from "vue";
|
|
2
|
+
import type { Ref, ComputedRef } from "vue";
|
|
3
|
+
|
|
4
|
+
export interface UseHoverToggleOptions {
|
|
5
|
+
/** Delay before auto-collapse after mouse leaves (ms) */
|
|
6
|
+
collapseDelay?: number;
|
|
7
|
+
/** If true, clicking toggles persistent open state; clicking elsewhere collapses */
|
|
8
|
+
persistOnClick?: boolean;
|
|
9
|
+
/** If true, starts expanded */
|
|
10
|
+
startExpanded?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface UseHoverToggleReturn {
|
|
14
|
+
isExpanded: Ref<boolean>;
|
|
15
|
+
isPinned: Ref<boolean>;
|
|
16
|
+
onMouseEnter: () => void;
|
|
17
|
+
onMouseLeave: () => void;
|
|
18
|
+
onClickToggle: () => void;
|
|
19
|
+
onClickOutside: () => void;
|
|
20
|
+
/** Bind all listeners to a container element */
|
|
21
|
+
containerProps: ComputedRef<Record<string, Function>>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function useHoverToggle(options: UseHoverToggleOptions = {}): UseHoverToggleReturn {
|
|
25
|
+
const {
|
|
26
|
+
collapseDelay = 2500,
|
|
27
|
+
persistOnClick = true,
|
|
28
|
+
startExpanded = false,
|
|
29
|
+
} = options;
|
|
30
|
+
|
|
31
|
+
const isExpanded = ref(startExpanded);
|
|
32
|
+
const isPinned = ref(false);
|
|
33
|
+
let collapseTimer: ReturnType<typeof setTimeout> | null = null;
|
|
34
|
+
|
|
35
|
+
function clearTimer() {
|
|
36
|
+
if (collapseTimer) {
|
|
37
|
+
clearTimeout(collapseTimer);
|
|
38
|
+
collapseTimer = null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function scheduleCollapse() {
|
|
43
|
+
clearTimer();
|
|
44
|
+
collapseTimer = setTimeout(() => {
|
|
45
|
+
isExpanded.value = false;
|
|
46
|
+
}, collapseDelay);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function onMouseEnter() {
|
|
50
|
+
clearTimer();
|
|
51
|
+
isExpanded.value = true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function onMouseLeave() {
|
|
55
|
+
if (!isPinned.value) {
|
|
56
|
+
scheduleCollapse();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function onClickToggle() {
|
|
61
|
+
if (persistOnClick) {
|
|
62
|
+
if (isPinned.value) {
|
|
63
|
+
isPinned.value = false;
|
|
64
|
+
scheduleCollapse();
|
|
65
|
+
} else {
|
|
66
|
+
isPinned.value = true;
|
|
67
|
+
isExpanded.value = true;
|
|
68
|
+
clearTimer();
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
clearTimer();
|
|
72
|
+
isExpanded.value = !isExpanded.value;
|
|
73
|
+
if (isExpanded.value) {
|
|
74
|
+
scheduleCollapse();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function onClickOutside() {
|
|
80
|
+
if (isPinned.value) {
|
|
81
|
+
isPinned.value = false;
|
|
82
|
+
}
|
|
83
|
+
isExpanded.value = false;
|
|
84
|
+
clearTimer();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const containerProps = computed(() => ({
|
|
88
|
+
onMouseenter: onMouseEnter,
|
|
89
|
+
onMouseleave: onMouseLeave,
|
|
90
|
+
}));
|
|
91
|
+
|
|
92
|
+
onBeforeUnmount(clearTimer);
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
isExpanded,
|
|
96
|
+
isPinned,
|
|
97
|
+
onMouseEnter,
|
|
98
|
+
onMouseLeave,
|
|
99
|
+
onClickToggle,
|
|
100
|
+
onClickOutside,
|
|
101
|
+
containerProps,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function useLeaveTimer(delay = 250) {
|
|
2
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
3
|
+
|
|
4
|
+
function schedule(callback: () => void) {
|
|
5
|
+
cancel();
|
|
6
|
+
timer = setTimeout(callback, delay);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function cancel() {
|
|
10
|
+
if (timer) {
|
|
11
|
+
clearTimeout(timer);
|
|
12
|
+
timer = null;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return { schedule, cancel };
|
|
17
|
+
}
|