@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,9 @@
|
|
|
1
|
+
export { default as GlassDock } from "./GlassDock.vue";
|
|
2
|
+
export { default as DockPopover } from "./DockPopover.vue";
|
|
3
|
+
export { default as DockLayerGroup } from "./DockLayerGroup.vue";
|
|
4
|
+
export { useDockState, useDockTransition, useLayerTransition, usePopupMutex, DOCK_ACTION_BAR_KEY, isTeleportedTarget } from "./composables";
|
|
5
|
+
export type { UseDockStateOptions, DockState } from "./composables";
|
|
6
|
+
export type { UseDockTransitionOptions } from "./composables";
|
|
7
|
+
export type { UseLayerTransitionOptions, UseLayerTransitionReturn } from "./composables";
|
|
8
|
+
export type { UsePopupMutexOptions, UsePopupMutexReturn } from "./composables";
|
|
9
|
+
export type { DockAction, DockActionBar } from "./composables";
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<!-- Normal mode: render inline -->
|
|
3
|
+
<div v-if="!isFullscreen" class="relative" v-bind="$attrs">
|
|
4
|
+
<button
|
|
5
|
+
class="absolute z-10 rounded-lg bg-card/70 [backdrop-filter:var(--glass-blur-subtle)] p-1.5 text-muted-foreground hover:text-foreground transition-colors shadow-sm border border-border/40"
|
|
6
|
+
:class="buttonPosition === 'left' ? 'left-2 top-2' : 'right-2 top-2'"
|
|
7
|
+
title="Fullscreen"
|
|
8
|
+
@click="isFullscreen = true"
|
|
9
|
+
>
|
|
10
|
+
<Maximize2 class="h-4 w-4" />
|
|
11
|
+
</button>
|
|
12
|
+
<slot :fullscreen="false" />
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
<!-- Fullscreen mode: teleport to body -->
|
|
16
|
+
<Teleport to="body">
|
|
17
|
+
<div
|
|
18
|
+
v-if="isFullscreen"
|
|
19
|
+
class="fixed inset-0 z-modal flex flex-col bg-background"
|
|
20
|
+
>
|
|
21
|
+
<button
|
|
22
|
+
class="absolute z-10 rounded-lg bg-card/70 [backdrop-filter:var(--glass-blur-subtle)] p-2 text-muted-foreground hover:text-foreground transition-colors shadow-sm border border-border/40"
|
|
23
|
+
:class="buttonPosition === 'left' ? 'left-3 top-3' : 'right-3 top-3'"
|
|
24
|
+
title="Exit fullscreen"
|
|
25
|
+
@click="isFullscreen = false"
|
|
26
|
+
>
|
|
27
|
+
<Minimize2 class="h-4 w-4" />
|
|
28
|
+
</button>
|
|
29
|
+
<div class="h-full w-full">
|
|
30
|
+
<slot :fullscreen="true" />
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
</Teleport>
|
|
34
|
+
</template>
|
|
35
|
+
|
|
36
|
+
<script setup lang="ts">
|
|
37
|
+
import { ref, watch, onUnmounted, defineOptions } from "vue";
|
|
38
|
+
|
|
39
|
+
defineOptions({ inheritAttrs: false });
|
|
40
|
+
import { Maximize2, Minimize2 } from "lucide-vue-next";
|
|
41
|
+
|
|
42
|
+
withDefaults(defineProps<{
|
|
43
|
+
buttonPosition?: "left" | "right";
|
|
44
|
+
}>(), {
|
|
45
|
+
buttonPosition: "right",
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const isFullscreen = ref(false);
|
|
49
|
+
|
|
50
|
+
watch(isFullscreen, (fs) => {
|
|
51
|
+
document.body.style.overflow = fs ? "hidden" : "";
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
function onKeydown(e: KeyboardEvent) {
|
|
55
|
+
if (e.key === "Escape" && isFullscreen.value) {
|
|
56
|
+
isFullscreen.value = false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
document.addEventListener("keydown", onKeydown);
|
|
60
|
+
onUnmounted(() => {
|
|
61
|
+
document.removeEventListener("keydown", onKeydown);
|
|
62
|
+
document.body.style.overflow = "";
|
|
63
|
+
});
|
|
64
|
+
</script>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as ExpandableContainer } from "./ExpandableContainer.vue";
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, computed, onMounted, onBeforeUnmount } from "vue";
|
|
3
|
+
import {
|
|
4
|
+
useGlassRenderer,
|
|
5
|
+
createGlassFilter,
|
|
6
|
+
destroyGlassFilter,
|
|
7
|
+
type GlassTier,
|
|
8
|
+
type GlassFilterState,
|
|
9
|
+
} from "../../../composables/glass/useGlassRenderer";
|
|
10
|
+
import { cn } from "../../../utils/cn";
|
|
11
|
+
|
|
12
|
+
export interface GlassPanelProps {
|
|
13
|
+
/** Force a specific rendering tier */
|
|
14
|
+
tier?: GlassTier;
|
|
15
|
+
/** Blur radius (default: 16) */
|
|
16
|
+
blur?: number;
|
|
17
|
+
/** Refraction strength 0-1 (default: 0.3) */
|
|
18
|
+
refraction?: number;
|
|
19
|
+
/** Chromatic aberration strength 0-1 (default: 0) */
|
|
20
|
+
chromaticAberration?: boolean;
|
|
21
|
+
/** Glass variant for CSS tier */
|
|
22
|
+
variant?: "default" | "medium" | "elevated";
|
|
23
|
+
/** Additional classes */
|
|
24
|
+
class?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const props = withDefaults(defineProps<GlassPanelProps>(), {
|
|
28
|
+
blur: 16,
|
|
29
|
+
refraction: 0.3,
|
|
30
|
+
chromaticAberration: false,
|
|
31
|
+
variant: "default",
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const panelRef = ref<HTMLElement | null>(null);
|
|
35
|
+
const renderer = useGlassRenderer({ preferredTier: props.tier });
|
|
36
|
+
const activeTier = computed(() => props.tier ?? renderer.tier.value);
|
|
37
|
+
|
|
38
|
+
const cssClass = computed(() => {
|
|
39
|
+
// SVG-filter tier: no glass CSS class (filter applied directly via JS)
|
|
40
|
+
if (activeTier.value === "svg-filter") {
|
|
41
|
+
return cn("glass-panel glass-panel--svg", props.class);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (activeTier.value === "fallback") {
|
|
45
|
+
return cn("glass-panel glass-panel--fallback", props.class);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// CSS tier
|
|
49
|
+
const variantClass =
|
|
50
|
+
props.variant === "elevated"
|
|
51
|
+
? "glass-elevated"
|
|
52
|
+
: props.variant === "medium"
|
|
53
|
+
? "glass-medium"
|
|
54
|
+
: "glass-default";
|
|
55
|
+
|
|
56
|
+
return cn("glass-panel", variantClass, props.class);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
let filterState: GlassFilterState | null = null;
|
|
60
|
+
|
|
61
|
+
onMounted(() => {
|
|
62
|
+
if (panelRef.value && activeTier.value === "svg-filter") {
|
|
63
|
+
filterState = createGlassFilter(panelRef.value, {
|
|
64
|
+
blur: props.blur,
|
|
65
|
+
refraction: props.refraction,
|
|
66
|
+
chromaticAberration: props.chromaticAberration ? 0.5 : 0,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
onBeforeUnmount(() => {
|
|
72
|
+
if (filterState) {
|
|
73
|
+
destroyGlassFilter(filterState);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
</script>
|
|
77
|
+
|
|
78
|
+
<template>
|
|
79
|
+
<div ref="panelRef" :class="cssClass">
|
|
80
|
+
<slot />
|
|
81
|
+
</div>
|
|
82
|
+
</template>
|
|
83
|
+
|
|
84
|
+
<style scoped>
|
|
85
|
+
.glass-panel {
|
|
86
|
+
position: relative;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.glass-panel--svg {
|
|
90
|
+
/* Background tint for SVG filter tier */
|
|
91
|
+
background: color-mix(in srgb, var(--card) 15%, transparent);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.glass-panel--fallback {
|
|
95
|
+
background: color-mix(in srgb, var(--card) 92%, transparent);
|
|
96
|
+
border: 1px solid color-mix(in srgb, var(--border) 35%, transparent);
|
|
97
|
+
}
|
|
98
|
+
</style>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Tooltip>
|
|
3
|
+
<TooltipTrigger as-child>
|
|
4
|
+
<slot />
|
|
5
|
+
</TooltipTrigger>
|
|
6
|
+
<TooltipContent class="font-display text-base">{{ text }}</TooltipContent>
|
|
7
|
+
</Tooltip>
|
|
8
|
+
</template>
|
|
9
|
+
|
|
10
|
+
<script setup lang="ts">
|
|
11
|
+
import {
|
|
12
|
+
Tooltip,
|
|
13
|
+
TooltipContent,
|
|
14
|
+
TooltipTrigger,
|
|
15
|
+
} from "../../ui/tooltip";
|
|
16
|
+
|
|
17
|
+
defineProps<{
|
|
18
|
+
text: string;
|
|
19
|
+
}>();
|
|
20
|
+
</script>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as IconTooltip } from "./IconTooltip.vue";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export * from "./confirm-dialog";
|
|
2
|
+
export * from "./controls";
|
|
3
|
+
export * from "./dock";
|
|
4
|
+
export * from "./expandable-container";
|
|
5
|
+
export * from "./icon-tooltip";
|
|
6
|
+
export * from "./infinite-scroll";
|
|
7
|
+
export * from "./labeled-field";
|
|
8
|
+
export * from "./metaballs";
|
|
9
|
+
export * from "./search";
|
|
10
|
+
export * from "./sidebar";
|
|
11
|
+
export * from "./tabs";
|
|
12
|
+
export * from "./timeline";
|
|
13
|
+
export * from "./typewriter";
|
|
14
|
+
export * from "./glass-panel";
|
|
15
|
+
export * from "./aurora";
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, toRef } from "vue";
|
|
3
|
+
import { useInfiniteScroll } from "./composables";
|
|
4
|
+
|
|
5
|
+
const props = withDefaults(
|
|
6
|
+
defineProps<{
|
|
7
|
+
/** Whether more data is available */
|
|
8
|
+
hasMore: boolean;
|
|
9
|
+
/** Whether data is currently loading */
|
|
10
|
+
isLoading: boolean;
|
|
11
|
+
/** Distance from bottom to trigger (px) */
|
|
12
|
+
threshold?: number;
|
|
13
|
+
}>(),
|
|
14
|
+
{
|
|
15
|
+
threshold: 200,
|
|
16
|
+
},
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
const emit = defineEmits<{
|
|
20
|
+
"load-more": [];
|
|
21
|
+
}>();
|
|
22
|
+
|
|
23
|
+
const scrollContainer = ref<HTMLElement | null>(null);
|
|
24
|
+
|
|
25
|
+
const { sentinelRef } = useInfiniteScroll({
|
|
26
|
+
scrollContainer,
|
|
27
|
+
threshold: props.threshold,
|
|
28
|
+
hasMore: toRef(() => props.hasMore),
|
|
29
|
+
isLoading: toRef(() => props.isLoading),
|
|
30
|
+
onLoadMore: () => emit("load-more"),
|
|
31
|
+
});
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<template>
|
|
35
|
+
<div ref="scrollContainer">
|
|
36
|
+
<slot />
|
|
37
|
+
|
|
38
|
+
<!-- Sentinel observed by IntersectionObserver -->
|
|
39
|
+
<div ref="sentinelRef" class="h-px w-full" aria-hidden="true" />
|
|
40
|
+
|
|
41
|
+
<!-- Loading indicator -->
|
|
42
|
+
<div v-if="isLoading" class="flex justify-center py-4">
|
|
43
|
+
<slot name="loading">
|
|
44
|
+
<div
|
|
45
|
+
class="h-5 w-5 animate-spin rounded-full border-2 border-muted-foreground border-t-transparent"
|
|
46
|
+
/>
|
|
47
|
+
</slot>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<!-- End of list -->
|
|
51
|
+
<div v-else-if="!hasMore" class="py-4 text-center text-sm text-muted-foreground">
|
|
52
|
+
<slot name="end" />
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
</template>
|
|
@@ -0,0 +1,23 @@
|
|
|
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;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface InfiniteScrollReturn {
|
|
17
|
+
/** Ref to bind to the sentinel element */
|
|
18
|
+
sentinelRef: Ref<HTMLElement | null>;
|
|
19
|
+
/** Manually trigger a check (e.g., after DOM updates) */
|
|
20
|
+
check: () => void;
|
|
21
|
+
/** Stop observing */
|
|
22
|
+
stop: () => void;
|
|
23
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
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
|
+
let observer: IntersectionObserver | null = null;
|
|
15
|
+
|
|
16
|
+
function shouldLoad(): boolean {
|
|
17
|
+
return toValue(hasMore) && !toValue(isLoading);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function handleIntersect(entries: IntersectionObserverEntry[]) {
|
|
21
|
+
for (const entry of entries) {
|
|
22
|
+
if (entry.isIntersecting && shouldLoad()) {
|
|
23
|
+
onLoadMore();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function setupObserver(el: HTMLElement) {
|
|
29
|
+
teardown();
|
|
30
|
+
observer = new IntersectionObserver(handleIntersect, {
|
|
31
|
+
root: options.scrollContainer?.value ?? null,
|
|
32
|
+
rootMargin: `0px 0px ${threshold}px 0px`,
|
|
33
|
+
});
|
|
34
|
+
observer.observe(el);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function teardown() {
|
|
38
|
+
if (observer) {
|
|
39
|
+
observer.disconnect();
|
|
40
|
+
observer = null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function check() {
|
|
45
|
+
if (sentinelRef.value && shouldLoad()) {
|
|
46
|
+
onLoadMore();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
watch(sentinelRef, (el) => {
|
|
51
|
+
if (el) setupObserver(el);
|
|
52
|
+
else teardown();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Re-check when loading finishes (new content may be short enough to need another load)
|
|
56
|
+
watch(
|
|
57
|
+
() => toValue(isLoading),
|
|
58
|
+
(loading) => {
|
|
59
|
+
if (!loading && sentinelRef.value) {
|
|
60
|
+
// Defer to next tick so DOM updates first
|
|
61
|
+
requestAnimationFrame(check);
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
onScopeDispose(teardown);
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
sentinelRef,
|
|
70
|
+
check,
|
|
71
|
+
stop: teardown,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as InfiniteScroll } from "./InfiniteScroll.vue";
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<IconTooltip :text="tooltip">
|
|
3
|
+
<label :class="labelClass ?? 'font-display text-lg text-muted-foreground cursor-help'">{{ label }}</label>
|
|
4
|
+
</IconTooltip>
|
|
5
|
+
<Input
|
|
6
|
+
:type="type ?? 'string'"
|
|
7
|
+
:class="inputClass ?? 'font-mono-code'"
|
|
8
|
+
:model-value="modelValue"
|
|
9
|
+
@change="(e: Event) => emit('update:modelValue', (e.target as HTMLInputElement).value)"
|
|
10
|
+
/>
|
|
11
|
+
</template>
|
|
12
|
+
|
|
13
|
+
<script setup lang="ts">
|
|
14
|
+
import { IconTooltip } from "../icon-tooltip";
|
|
15
|
+
import { Input } from "../../ui/input";
|
|
16
|
+
|
|
17
|
+
defineProps<{
|
|
18
|
+
modelValue: string | number;
|
|
19
|
+
label: string;
|
|
20
|
+
tooltip: string;
|
|
21
|
+
labelClass?: string;
|
|
22
|
+
inputClass?: string;
|
|
23
|
+
type?: string;
|
|
24
|
+
}>();
|
|
25
|
+
|
|
26
|
+
const emit = defineEmits<{
|
|
27
|
+
"update:modelValue": [value: string];
|
|
28
|
+
}>();
|
|
29
|
+
</script>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<IconTooltip :text="tooltip">
|
|
3
|
+
<label :class="labelClass ?? 'font-display text-lg text-muted-foreground cursor-help'">{{ label }}</label>
|
|
4
|
+
</IconTooltip>
|
|
5
|
+
<Select
|
|
6
|
+
:model-value="modelValue"
|
|
7
|
+
:open="isOpen"
|
|
8
|
+
@update:open="(v: boolean) => emit('update:open', v)"
|
|
9
|
+
@update:model-value="(v: any) => emit('update:modelValue', v)"
|
|
10
|
+
>
|
|
11
|
+
<SelectTrigger class="font-mono-code">
|
|
12
|
+
<SelectValue />
|
|
13
|
+
</SelectTrigger>
|
|
14
|
+
<SelectContent>
|
|
15
|
+
<SelectGroup class="font-mono-code">
|
|
16
|
+
<SelectItem
|
|
17
|
+
v-for="item in items"
|
|
18
|
+
:key="item"
|
|
19
|
+
:value="item"
|
|
20
|
+
>
|
|
21
|
+
{{ item }}
|
|
22
|
+
<template #description>
|
|
23
|
+
<span
|
|
24
|
+
v-if="descriptions?.[item]"
|
|
25
|
+
class="ml-auto pl-2 text-2xs text-muted-foreground whitespace-nowrap"
|
|
26
|
+
>{{ descriptions[item] }}</span>
|
|
27
|
+
</template>
|
|
28
|
+
</SelectItem>
|
|
29
|
+
</SelectGroup>
|
|
30
|
+
</SelectContent>
|
|
31
|
+
</Select>
|
|
32
|
+
</template>
|
|
33
|
+
|
|
34
|
+
<script setup lang="ts">
|
|
35
|
+
import { IconTooltip } from "../icon-tooltip";
|
|
36
|
+
import {
|
|
37
|
+
Select,
|
|
38
|
+
SelectContent,
|
|
39
|
+
SelectGroup,
|
|
40
|
+
SelectItem,
|
|
41
|
+
SelectTrigger,
|
|
42
|
+
SelectValue,
|
|
43
|
+
} from "../../ui/select";
|
|
44
|
+
|
|
45
|
+
defineProps<{
|
|
46
|
+
modelValue: string;
|
|
47
|
+
isOpen: boolean;
|
|
48
|
+
items: readonly string[];
|
|
49
|
+
descriptions?: Record<string, string>;
|
|
50
|
+
label: string;
|
|
51
|
+
tooltip: string;
|
|
52
|
+
labelClass?: string;
|
|
53
|
+
}>();
|
|
54
|
+
|
|
55
|
+
const emit = defineEmits<{
|
|
56
|
+
"update:modelValue": [value: string];
|
|
57
|
+
"update:open": [value: boolean];
|
|
58
|
+
}>();
|
|
59
|
+
</script>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<IconTooltip :text="tooltip">
|
|
3
|
+
<label :class="labelClass ?? 'font-display text-base text-muted-foreground cursor-help'">{{ label }}</label>
|
|
4
|
+
</IconTooltip>
|
|
5
|
+
<Slider
|
|
6
|
+
class="py-2"
|
|
7
|
+
:min="min"
|
|
8
|
+
:max="max"
|
|
9
|
+
:step="step"
|
|
10
|
+
:model-value="[modelValue]"
|
|
11
|
+
@update:model-value="(v: number[] | undefined) => { if (v) emit('update:modelValue', v[0]!) }"
|
|
12
|
+
/>
|
|
13
|
+
</template>
|
|
14
|
+
|
|
15
|
+
<script setup lang="ts">
|
|
16
|
+
import { IconTooltip } from "../icon-tooltip";
|
|
17
|
+
import { Slider } from "../../ui/slider";
|
|
18
|
+
|
|
19
|
+
defineProps<{
|
|
20
|
+
modelValue: number;
|
|
21
|
+
label: string;
|
|
22
|
+
tooltip: string;
|
|
23
|
+
labelClass?: string;
|
|
24
|
+
min: number;
|
|
25
|
+
max: number;
|
|
26
|
+
step: number;
|
|
27
|
+
}>();
|
|
28
|
+
|
|
29
|
+
const emit = defineEmits<{
|
|
30
|
+
"update:modelValue": [value: number];
|
|
31
|
+
}>();
|
|
32
|
+
</script>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<IconTooltip :text="tooltip">
|
|
3
|
+
<label :class="labelClass ?? 'font-display text-base text-muted-foreground cursor-help'">{{ label }}</label>
|
|
4
|
+
</IconTooltip>
|
|
5
|
+
<div class="flex items-center">
|
|
6
|
+
<Switch
|
|
7
|
+
:checked="checked"
|
|
8
|
+
@update:checked="(v: boolean) => emit('update:checked', v)"
|
|
9
|
+
/>
|
|
10
|
+
</div>
|
|
11
|
+
</template>
|
|
12
|
+
|
|
13
|
+
<script setup lang="ts">
|
|
14
|
+
import { IconTooltip } from "../icon-tooltip";
|
|
15
|
+
import { Switch } from "../../ui/switch";
|
|
16
|
+
|
|
17
|
+
defineProps<{
|
|
18
|
+
checked: boolean;
|
|
19
|
+
label: string;
|
|
20
|
+
tooltip: string;
|
|
21
|
+
labelClass?: string;
|
|
22
|
+
}>();
|
|
23
|
+
|
|
24
|
+
const emit = defineEmits<{
|
|
25
|
+
"update:checked": [value: boolean];
|
|
26
|
+
}>();
|
|
27
|
+
</script>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref } from "vue";
|
|
3
|
+
import { useMetaballs } from "./useMetaballs";
|
|
4
|
+
import type { MetaballConfig } from "./types";
|
|
5
|
+
|
|
6
|
+
const props = defineProps<{
|
|
7
|
+
config?: MetaballConfig;
|
|
8
|
+
}>();
|
|
9
|
+
|
|
10
|
+
const canvasRef = ref<HTMLCanvasElement | null>(null);
|
|
11
|
+
const { isSupported } = useMetaballs(canvasRef, props.config);
|
|
12
|
+
|
|
13
|
+
defineExpose({ isSupported });
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<template>
|
|
17
|
+
<canvas
|
|
18
|
+
v-if="isSupported"
|
|
19
|
+
ref="canvasRef"
|
|
20
|
+
class="pointer-events-none fixed inset-0 -z-10 h-full w-full"
|
|
21
|
+
/>
|
|
22
|
+
<slot v-else name="fallback" />
|
|
23
|
+
</template>
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export const VERTEX_SHADER = `
|
|
2
|
+
attribute vec2 a_position;
|
|
3
|
+
void main() {
|
|
4
|
+
gl_Position = vec4(a_position, 0.0, 1.0);
|
|
5
|
+
}
|
|
6
|
+
`;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Metaball fragment shader.
|
|
10
|
+
*
|
|
11
|
+
* Computes a density field per pixel by summing inverse-square
|
|
12
|
+
* contributions from all blobs. Pixels above the threshold
|
|
13
|
+
* render as colored goo with smooth edges.
|
|
14
|
+
*/
|
|
15
|
+
export const FRAGMENT_SHADER = `
|
|
16
|
+
precision mediump float;
|
|
17
|
+
|
|
18
|
+
#define MAX_BLOBS 16
|
|
19
|
+
|
|
20
|
+
uniform vec2 u_resolution;
|
|
21
|
+
uniform int u_blobCount;
|
|
22
|
+
uniform vec3 u_positions[MAX_BLOBS];
|
|
23
|
+
uniform vec3 u_colors[MAX_BLOBS];
|
|
24
|
+
uniform float u_threshold;
|
|
25
|
+
uniform float u_edgeSoftness;
|
|
26
|
+
uniform float u_bgAlpha;
|
|
27
|
+
|
|
28
|
+
void main() {
|
|
29
|
+
vec2 uv = gl_FragCoord.xy / u_resolution;
|
|
30
|
+
float aspect = u_resolution.x / u_resolution.y;
|
|
31
|
+
vec2 coord = vec2(uv.x * aspect, uv.y);
|
|
32
|
+
|
|
33
|
+
float density = 0.0;
|
|
34
|
+
vec3 color = vec3(0.0);
|
|
35
|
+
|
|
36
|
+
for (int i = 0; i < MAX_BLOBS; i++) {
|
|
37
|
+
if (i >= u_blobCount) break;
|
|
38
|
+
|
|
39
|
+
vec2 pos = vec2(u_positions[i].x * aspect, u_positions[i].y);
|
|
40
|
+
float radius = u_positions[i].z;
|
|
41
|
+
float dist = distance(coord, pos);
|
|
42
|
+
float contrib = (radius * radius) / (dist * dist + 0.0001);
|
|
43
|
+
|
|
44
|
+
density += contrib;
|
|
45
|
+
color += contrib * u_colors[i];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (density > u_threshold * 0.4) {
|
|
49
|
+
color /= density;
|
|
50
|
+
// Gooey edge: slightly wider smoothstep for organic merge zone
|
|
51
|
+
float edge = smoothstep(
|
|
52
|
+
u_threshold * (1.0 - u_edgeSoftness),
|
|
53
|
+
u_threshold * (1.0 + u_edgeSoftness * 0.3),
|
|
54
|
+
density
|
|
55
|
+
);
|
|
56
|
+
// Solid core with soft goo boundary — full color at core
|
|
57
|
+
gl_FragColor = vec4(color, edge * (1.0 - u_bgAlpha * 0.3));
|
|
58
|
+
} else {
|
|
59
|
+
// Background: transparent (let page bg show through) with optional tint
|
|
60
|
+
gl_FragColor = vec4(0.0, 0.0, 0.0, u_bgAlpha * 0.15);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
`;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface MetaballConfig {
|
|
2
|
+
/** Number of blobs (default 8, max 16 for shader uniform limit) */
|
|
3
|
+
blobCount?: number;
|
|
4
|
+
/** Animation speed multiplier (default 0.08) */
|
|
5
|
+
speed?: number;
|
|
6
|
+
/** Density threshold for surface rendering (default 1.0, higher = sharper edges) */
|
|
7
|
+
threshold?: number;
|
|
8
|
+
/** Base blob radius as fraction of viewport (default 0.12) */
|
|
9
|
+
baseRadius?: number;
|
|
10
|
+
/** Orbital drift amplitude as fraction of viewport (default 0.3) */
|
|
11
|
+
orbitAmplitude?: number;
|
|
12
|
+
/** CSS color strings for blob colors (cycles if fewer than blobCount) */
|
|
13
|
+
colors?: string[];
|
|
14
|
+
/** Background alpha (0 = fully transparent, default 0) */
|
|
15
|
+
bgAlpha?: number;
|
|
16
|
+
/** Edge softness — smoothstep range as fraction of threshold (default 0.3) */
|
|
17
|
+
edgeSoftness?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const DEFAULT_METABALL_CONFIG: Required<MetaballConfig> = {
|
|
21
|
+
blobCount: 8,
|
|
22
|
+
speed: 0.08,
|
|
23
|
+
threshold: 1.0,
|
|
24
|
+
baseRadius: 0.12,
|
|
25
|
+
orbitAmplitude: 0.3,
|
|
26
|
+
colors: ["#E31937", "#FF6B35", "#FFA726", "#EF5350"],
|
|
27
|
+
bgAlpha: 0,
|
|
28
|
+
edgeSoftness: 0.3,
|
|
29
|
+
};
|