@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.
Files changed (335) hide show
  1. package/README.md +172 -0
  2. package/dist/glass-ui.css +1 -0
  3. package/dist/glass-ui.js +10019 -0
  4. package/dist/index.d.ts +6619 -0
  5. package/package.json +65 -0
  6. package/src/components/custom/aurora/Aurora.vue +34 -0
  7. package/src/components/custom/aurora/composables/color.ts +122 -0
  8. package/src/components/custom/aurora/composables/useAurora.ts +355 -0
  9. package/src/components/custom/aurora/index.ts +8 -0
  10. package/src/components/custom/confirm-dialog/ConfirmDialog.vue +88 -0
  11. package/src/components/custom/confirm-dialog/index.ts +1 -0
  12. package/src/components/custom/controls/DarkModeToggle.vue +96 -0
  13. package/src/components/custom/controls/index.ts +1 -0
  14. package/src/components/custom/dock/DockLayerGroup.vue +21 -0
  15. package/src/components/custom/dock/DockPopover.vue +263 -0
  16. package/src/components/custom/dock/GlassDock.vue +276 -0
  17. package/src/components/custom/dock/composables/index.ts +16 -0
  18. package/src/components/custom/dock/composables/isTeleportedTarget.ts +19 -0
  19. package/src/components/custom/dock/composables/useDockActionBar.ts +33 -0
  20. package/src/components/custom/dock/composables/useDockState.ts +301 -0
  21. package/src/components/custom/dock/composables/useDockTransition.ts +146 -0
  22. package/src/components/custom/dock/composables/useLayerTransition.ts +135 -0
  23. package/src/components/custom/dock/composables/usePopupMutex.ts +83 -0
  24. package/src/components/custom/dock/index.ts +9 -0
  25. package/src/components/custom/expandable-container/ExpandableContainer.vue +64 -0
  26. package/src/components/custom/expandable-container/index.ts +1 -0
  27. package/src/components/custom/glass-panel/GlassPanel.vue +98 -0
  28. package/src/components/custom/glass-panel/index.ts +2 -0
  29. package/src/components/custom/icon-tooltip/IconTooltip.vue +20 -0
  30. package/src/components/custom/icon-tooltip/index.ts +1 -0
  31. package/src/components/custom/index.ts +15 -0
  32. package/src/components/custom/infinite-scroll/InfiniteScroll.vue +55 -0
  33. package/src/components/custom/infinite-scroll/composables/index.ts +2 -0
  34. package/src/components/custom/infinite-scroll/composables/types.ts +23 -0
  35. package/src/components/custom/infinite-scroll/composables/useInfiniteScroll.ts +73 -0
  36. package/src/components/custom/infinite-scroll/index.ts +1 -0
  37. package/src/components/custom/labeled-field/LabeledInput.vue +29 -0
  38. package/src/components/custom/labeled-field/LabeledSelect.vue +59 -0
  39. package/src/components/custom/labeled-field/LabeledSlider.vue +32 -0
  40. package/src/components/custom/labeled-field/LabeledSwitch.vue +27 -0
  41. package/src/components/custom/labeled-field/index.ts +4 -0
  42. package/src/components/custom/metaballs/MetaballCanvas.vue +23 -0
  43. package/src/components/custom/metaballs/index.ts +4 -0
  44. package/src/components/custom/metaballs/shaders.ts +63 -0
  45. package/src/components/custom/metaballs/types.ts +29 -0
  46. package/src/components/custom/metaballs/useMetaballs.ts +252 -0
  47. package/src/components/custom/search/FuzzySearch.vue +589 -0
  48. package/src/components/custom/search/SearchBar.vue +44 -0
  49. package/src/components/custom/search/composables/fuzzySearchIndex.ts +224 -0
  50. package/src/components/custom/search/composables/index.ts +5 -0
  51. package/src/components/custom/search/composables/types.ts +34 -0
  52. package/src/components/custom/search/composables/useFuzzySearch.ts +115 -0
  53. package/src/components/custom/search/index.ts +7 -0
  54. package/src/components/custom/sidebar/ProgressiveSidebar.vue +256 -0
  55. package/src/components/custom/sidebar/composables/index.ts +6 -0
  56. package/src/components/custom/sidebar/composables/useScrollTracker.ts +242 -0
  57. package/src/components/custom/sidebar/composables/useSidebarFollow.ts +247 -0
  58. package/src/components/custom/sidebar/composables/useSidebarState.ts +72 -0
  59. package/src/components/custom/sidebar/composables/useTreeIndex.ts +152 -0
  60. package/src/components/custom/sidebar/index.ts +15 -0
  61. package/src/components/custom/sidebar/types.ts +50 -0
  62. package/src/components/custom/tabs/BouncyTabs.vue +39 -0
  63. package/src/components/custom/tabs/BouncyToggle.vue +352 -0
  64. package/src/components/custom/tabs/UnderlineTabs.vue +115 -0
  65. package/src/components/custom/tabs/index.ts +5 -0
  66. package/src/components/custom/timeline/GlassTimeline.vue +174 -0
  67. package/src/components/custom/timeline/index.ts +1 -0
  68. package/src/components/custom/typewriter/TypewriterText.vue +239 -0
  69. package/src/components/custom/typewriter/composables/index.ts +1 -0
  70. package/src/components/custom/typewriter/composables/useTypewriter.ts +413 -0
  71. package/src/components/custom/typewriter/index.ts +7 -0
  72. package/src/components/custom/typewriter/types.ts +159 -0
  73. package/src/components/custom/typewriter/utils/keyboard.ts +213 -0
  74. package/src/components/custom/typewriter/utils/pausePatterns.ts +55 -0
  75. package/src/components/custom/typewriter/utils/timing.ts +104 -0
  76. package/src/components/custom/typewriter/utils/typoStateMachine.ts +197 -0
  77. package/src/components/index.ts +2 -0
  78. package/src/components/ui/accordion/Accordion.vue +19 -0
  79. package/src/components/ui/accordion/AccordionContent.vue +24 -0
  80. package/src/components/ui/accordion/AccordionItem.vue +24 -0
  81. package/src/components/ui/accordion/AccordionTrigger.vue +39 -0
  82. package/src/components/ui/accordion/index.ts +4 -0
  83. package/src/components/ui/alert/Alert.vue +20 -0
  84. package/src/components/ui/alert/AlertDescription.vue +17 -0
  85. package/src/components/ui/alert/AlertTitle.vue +17 -0
  86. package/src/components/ui/alert/index.ts +23 -0
  87. package/src/components/ui/avatar/Avatar.vue +21 -0
  88. package/src/components/ui/avatar/AvatarFallback.vue +11 -0
  89. package/src/components/ui/avatar/AvatarImage.vue +9 -0
  90. package/src/components/ui/avatar/index.ts +24 -0
  91. package/src/components/ui/badge/Badge.vue +16 -0
  92. package/src/components/ui/badge/index.ts +25 -0
  93. package/src/components/ui/button/Button.vue +26 -0
  94. package/src/components/ui/button/index.ts +43 -0
  95. package/src/components/ui/card/Card.vue +28 -0
  96. package/src/components/ui/card/CardContent.vue +14 -0
  97. package/src/components/ui/card/CardDescription.vue +14 -0
  98. package/src/components/ui/card/CardFooter.vue +14 -0
  99. package/src/components/ui/card/CardHeader.vue +14 -0
  100. package/src/components/ui/card/CardTitle.vue +21 -0
  101. package/src/components/ui/card/index.ts +6 -0
  102. package/src/components/ui/carousel/Carousel.vue +53 -0
  103. package/src/components/ui/carousel/CarouselContent.vue +35 -0
  104. package/src/components/ui/carousel/CarouselItem.vue +24 -0
  105. package/src/components/ui/carousel/CarouselNext.vue +40 -0
  106. package/src/components/ui/carousel/CarouselPrevious.vue +40 -0
  107. package/src/components/ui/carousel/index.ts +10 -0
  108. package/src/components/ui/carousel/interface.ts +26 -0
  109. package/src/components/ui/carousel/useCarousel.ts +56 -0
  110. package/src/components/ui/checkbox/Checkbox.vue +33 -0
  111. package/src/components/ui/checkbox/index.ts +1 -0
  112. package/src/components/ui/collapsible/Collapsible.vue +15 -0
  113. package/src/components/ui/collapsible/CollapsibleContent.vue +11 -0
  114. package/src/components/ui/collapsible/CollapsibleTrigger.vue +11 -0
  115. package/src/components/ui/collapsible/index.ts +3 -0
  116. package/src/components/ui/combobox/Combobox.vue +17 -0
  117. package/src/components/ui/combobox/ComboboxAnchor.vue +23 -0
  118. package/src/components/ui/combobox/ComboboxEmpty.vue +21 -0
  119. package/src/components/ui/combobox/ComboboxGroup.vue +27 -0
  120. package/src/components/ui/combobox/ComboboxInput.vue +41 -0
  121. package/src/components/ui/combobox/ComboboxItem.vue +24 -0
  122. package/src/components/ui/combobox/ComboboxItemIndicator.vue +23 -0
  123. package/src/components/ui/combobox/ComboboxList.vue +29 -0
  124. package/src/components/ui/combobox/ComboboxSeparator.vue +21 -0
  125. package/src/components/ui/combobox/ComboboxViewport.vue +23 -0
  126. package/src/components/ui/combobox/index.ts +12 -0
  127. package/src/components/ui/command/Command.vue +30 -0
  128. package/src/components/ui/command/CommandDialog.vue +21 -0
  129. package/src/components/ui/command/CommandEmpty.vue +20 -0
  130. package/src/components/ui/command/CommandGroup.vue +29 -0
  131. package/src/components/ui/command/CommandInput.vue +33 -0
  132. package/src/components/ui/command/CommandItem.vue +26 -0
  133. package/src/components/ui/command/CommandList.vue +27 -0
  134. package/src/components/ui/command/CommandSeparator.vue +23 -0
  135. package/src/components/ui/command/CommandShortcut.vue +14 -0
  136. package/src/components/ui/command/index.ts +9 -0
  137. package/src/components/ui/context-menu/ContextMenu.vue +15 -0
  138. package/src/components/ui/context-menu/ContextMenuCheckboxItem.vue +40 -0
  139. package/src/components/ui/context-menu/ContextMenuContent.vue +36 -0
  140. package/src/components/ui/context-menu/ContextMenuGroup.vue +11 -0
  141. package/src/components/ui/context-menu/ContextMenuItem.vue +34 -0
  142. package/src/components/ui/context-menu/ContextMenuLabel.vue +25 -0
  143. package/src/components/ui/context-menu/ContextMenuPortal.vue +11 -0
  144. package/src/components/ui/context-menu/ContextMenuRadioGroup.vue +19 -0
  145. package/src/components/ui/context-menu/ContextMenuRadioItem.vue +40 -0
  146. package/src/components/ui/context-menu/ContextMenuSeparator.vue +20 -0
  147. package/src/components/ui/context-menu/ContextMenuShortcut.vue +14 -0
  148. package/src/components/ui/context-menu/ContextMenuSub.vue +19 -0
  149. package/src/components/ui/context-menu/ContextMenuSubContent.vue +35 -0
  150. package/src/components/ui/context-menu/ContextMenuSubTrigger.vue +34 -0
  151. package/src/components/ui/context-menu/ContextMenuTrigger.vue +13 -0
  152. package/src/components/ui/context-menu/index.ts +14 -0
  153. package/src/components/ui/data-table/DataTable.vue +167 -0
  154. package/src/components/ui/data-table/DataTablePagination.vue +112 -0
  155. package/src/components/ui/data-table/index.ts +3 -0
  156. package/src/components/ui/data-table/types.ts +48 -0
  157. package/src/components/ui/dialog/Dialog.vue +14 -0
  158. package/src/components/ui/dialog/DialogClose.vue +11 -0
  159. package/src/components/ui/dialog/DialogContent.vue +61 -0
  160. package/src/components/ui/dialog/DialogDescription.vue +24 -0
  161. package/src/components/ui/dialog/DialogFooter.vue +19 -0
  162. package/src/components/ui/dialog/DialogHeader.vue +16 -0
  163. package/src/components/ui/dialog/DialogScrollContent.vue +65 -0
  164. package/src/components/ui/dialog/DialogTitle.vue +29 -0
  165. package/src/components/ui/dialog/DialogTrigger.vue +11 -0
  166. package/src/components/ui/dialog/index.ts +9 -0
  167. package/src/components/ui/drawer/Drawer.vue +19 -0
  168. package/src/components/ui/drawer/DrawerContent.vue +28 -0
  169. package/src/components/ui/drawer/DrawerDescription.vue +20 -0
  170. package/src/components/ui/drawer/DrawerFooter.vue +14 -0
  171. package/src/components/ui/drawer/DrawerHeader.vue +14 -0
  172. package/src/components/ui/drawer/DrawerOverlay.vue +18 -0
  173. package/src/components/ui/drawer/DrawerTitle.vue +20 -0
  174. package/src/components/ui/drawer/index.ts +8 -0
  175. package/src/components/ui/dropdown-menu/DropdownMenu.vue +14 -0
  176. package/src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue +40 -0
  177. package/src/components/ui/dropdown-menu/DropdownMenuContent.vue +44 -0
  178. package/src/components/ui/dropdown-menu/DropdownMenuGroup.vue +11 -0
  179. package/src/components/ui/dropdown-menu/DropdownMenuItem.vue +28 -0
  180. package/src/components/ui/dropdown-menu/DropdownMenuLabel.vue +24 -0
  181. package/src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue +19 -0
  182. package/src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue +40 -0
  183. package/src/components/ui/dropdown-menu/DropdownMenuSeparator.vue +22 -0
  184. package/src/components/ui/dropdown-menu/DropdownMenuShortcut.vue +14 -0
  185. package/src/components/ui/dropdown-menu/DropdownMenuSub.vue +19 -0
  186. package/src/components/ui/dropdown-menu/DropdownMenuSubContent.vue +36 -0
  187. package/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +33 -0
  188. package/src/components/ui/dropdown-menu/DropdownMenuTrigger.vue +13 -0
  189. package/src/components/ui/dropdown-menu/index.ts +16 -0
  190. package/src/components/ui/hover-card/HoverCard.vue +14 -0
  191. package/src/components/ui/hover-card/HoverCardContent.vue +41 -0
  192. package/src/components/ui/hover-card/HoverCardTrigger.vue +11 -0
  193. package/src/components/ui/hover-card/index.ts +3 -0
  194. package/src/components/ui/index.ts +41 -0
  195. package/src/components/ui/input/Input.vue +24 -0
  196. package/src/components/ui/input/index.ts +1 -0
  197. package/src/components/ui/label/Label.vue +27 -0
  198. package/src/components/ui/label/index.ts +1 -0
  199. package/src/components/ui/multi-select/MultiSelect.vue +141 -0
  200. package/src/components/ui/multi-select/index.ts +7 -0
  201. package/src/components/ui/notification/Notification.vue +85 -0
  202. package/src/components/ui/notification/index.ts +1 -0
  203. package/src/components/ui/number-field/NumberField.vue +23 -0
  204. package/src/components/ui/number-field/NumberFieldContent.vue +14 -0
  205. package/src/components/ui/number-field/NumberFieldDecrement.vue +25 -0
  206. package/src/components/ui/number-field/NumberFieldIncrement.vue +25 -0
  207. package/src/components/ui/number-field/NumberFieldInput.vue +8 -0
  208. package/src/components/ui/number-field/index.ts +5 -0
  209. package/src/components/ui/popover/Popover.vue +15 -0
  210. package/src/components/ui/popover/PopoverContent.vue +61 -0
  211. package/src/components/ui/popover/PopoverTrigger.vue +11 -0
  212. package/src/components/ui/popover/index.ts +3 -0
  213. package/src/components/ui/progress/Progress.vue +39 -0
  214. package/src/components/ui/progress/index.ts +1 -0
  215. package/src/components/ui/radio-group/RadioGroup.vue +25 -0
  216. package/src/components/ui/radio-group/RadioGroupItem.vue +39 -0
  217. package/src/components/ui/radio-group/index.ts +2 -0
  218. package/src/components/ui/scroll-area/ScrollArea.vue +29 -0
  219. package/src/components/ui/scroll-area/ScrollBar.vue +30 -0
  220. package/src/components/ui/scroll-area/index.ts +2 -0
  221. package/src/components/ui/scroll-pane/ScrollPane.vue +25 -0
  222. package/src/components/ui/scroll-pane/ScrollPaneHeader.vue +75 -0
  223. package/src/components/ui/scroll-pane/index.ts +2 -0
  224. package/src/components/ui/select/Select.vue +15 -0
  225. package/src/components/ui/select/SelectContent.vue +57 -0
  226. package/src/components/ui/select/SelectGroup.vue +19 -0
  227. package/src/components/ui/select/SelectItem.vue +47 -0
  228. package/src/components/ui/select/SelectItemText.vue +11 -0
  229. package/src/components/ui/select/SelectLabel.vue +13 -0
  230. package/src/components/ui/select/SelectScrollDownButton.vue +24 -0
  231. package/src/components/ui/select/SelectScrollUpButton.vue +24 -0
  232. package/src/components/ui/select/SelectSeparator.vue +17 -0
  233. package/src/components/ui/select/SelectTrigger.vue +45 -0
  234. package/src/components/ui/select/SelectValue.vue +11 -0
  235. package/src/components/ui/select/index.ts +11 -0
  236. package/src/components/ui/separator/Separator.vue +35 -0
  237. package/src/components/ui/separator/index.ts +1 -0
  238. package/src/components/ui/sheet/Sheet.vue +14 -0
  239. package/src/components/ui/sheet/SheetClose.vue +11 -0
  240. package/src/components/ui/sheet/SheetContent.vue +56 -0
  241. package/src/components/ui/sheet/SheetDescription.vue +22 -0
  242. package/src/components/ui/sheet/SheetFooter.vue +19 -0
  243. package/src/components/ui/sheet/SheetHeader.vue +16 -0
  244. package/src/components/ui/sheet/SheetTitle.vue +22 -0
  245. package/src/components/ui/sheet/SheetTrigger.vue +11 -0
  246. package/src/components/ui/sheet/index.ts +31 -0
  247. package/src/components/ui/skeleton/Skeleton.vue +14 -0
  248. package/src/components/ui/skeleton/index.ts +1 -0
  249. package/src/components/ui/slider/Slider.vue +66 -0
  250. package/src/components/ui/slider/index.ts +1 -0
  251. package/src/components/ui/switch/Switch.vue +37 -0
  252. package/src/components/ui/switch/index.ts +1 -0
  253. package/src/components/ui/table/Table.vue +16 -0
  254. package/src/components/ui/table/TableBody.vue +14 -0
  255. package/src/components/ui/table/TableCaption.vue +14 -0
  256. package/src/components/ui/table/TableCell.vue +14 -0
  257. package/src/components/ui/table/TableEmpty.vue +39 -0
  258. package/src/components/ui/table/TableFooter.vue +16 -0
  259. package/src/components/ui/table/TableHead.vue +21 -0
  260. package/src/components/ui/table/TableHeader.vue +14 -0
  261. package/src/components/ui/table/TableRow.vue +21 -0
  262. package/src/components/ui/table/index.ts +9 -0
  263. package/src/components/ui/tabs/Tabs.vue +15 -0
  264. package/src/components/ui/tabs/TabsContent.vue +22 -0
  265. package/src/components/ui/tabs/TabsIndicator.vue +22 -0
  266. package/src/components/ui/tabs/TabsList.vue +25 -0
  267. package/src/components/ui/tabs/TabsTrigger.vue +29 -0
  268. package/src/components/ui/tabs/index.ts +5 -0
  269. package/src/components/ui/tags-input/TagsInput.vue +22 -0
  270. package/src/components/ui/tags-input/TagsInputInput.vue +19 -0
  271. package/src/components/ui/tags-input/TagsInputItem.vue +22 -0
  272. package/src/components/ui/tags-input/TagsInputItemDelete.vue +24 -0
  273. package/src/components/ui/tags-input/TagsInputItemText.vue +19 -0
  274. package/src/components/ui/tags-input/index.ts +5 -0
  275. package/src/components/ui/textarea/Textarea.vue +24 -0
  276. package/src/components/ui/textarea/index.ts +1 -0
  277. package/src/components/ui/toast/Toast.vue +57 -0
  278. package/src/components/ui/toast/ToastAction.vue +30 -0
  279. package/src/components/ui/toast/ToastClose.vue +31 -0
  280. package/src/components/ui/toast/ToastDescription.vue +25 -0
  281. package/src/components/ui/toast/ToastTitle.vue +25 -0
  282. package/src/components/ui/toast/Toaster.vue +31 -0
  283. package/src/components/ui/toast/index.ts +8 -0
  284. package/src/components/ui/toast/use-toast.ts +136 -0
  285. package/src/components/ui/toggle/Toggle.vue +35 -0
  286. package/src/components/ui/toggle/index.ts +27 -0
  287. package/src/components/ui/toggle-group/ToggleGroup.vue +34 -0
  288. package/src/components/ui/toggle-group/ToggleGroupItem.vue +35 -0
  289. package/src/components/ui/toggle-group/index.ts +2 -0
  290. package/src/components/ui/tooltip/Tooltip.vue +14 -0
  291. package/src/components/ui/tooltip/TooltipContent.vue +31 -0
  292. package/src/components/ui/tooltip/TooltipProvider.vue +11 -0
  293. package/src/components/ui/tooltip/TooltipTrigger.vue +11 -0
  294. package/src/components/ui/tooltip/index.ts +4 -0
  295. package/src/composables/glass/index.ts +8 -0
  296. package/src/composables/glass/useGlassRenderer.ts +252 -0
  297. package/src/composables/glass/webgl/frostShader.ts +221 -0
  298. package/src/composables/glass/webgpu/glassShader.wgsl +173 -0
  299. package/src/composables/index.ts +32 -0
  300. package/src/composables/infinite-scroll/index.ts +2 -0
  301. package/src/composables/infinite-scroll/types.ts +25 -0
  302. package/src/composables/infinite-scroll/useInfiniteScroll.ts +101 -0
  303. package/src/composables/interaction/index.ts +5 -0
  304. package/src/composables/interaction/useHeightTransition.ts +82 -0
  305. package/src/composables/interaction/useHoverPopover.ts +64 -0
  306. package/src/composables/interaction/useHoverToggle.ts +103 -0
  307. package/src/composables/interaction/useLeaveTimer.ts +17 -0
  308. package/src/composables/interaction/useTouchGate.ts +207 -0
  309. package/src/composables/pagination/index.ts +2 -0
  310. package/src/composables/pagination/useOffsetPagination.ts +70 -0
  311. package/src/composables/prng.ts +32 -0
  312. package/src/composables/useCharSplit.ts +31 -0
  313. package/src/composables/useClipboard.ts +46 -0
  314. package/src/composables/useGlobalDark.ts +61 -0
  315. package/src/composables/useKeyboardShortcuts.ts +205 -0
  316. package/src/composables/useWatercolorBlob.ts +136 -0
  317. package/src/composables/virtual/index.ts +22 -0
  318. package/src/composables/virtual/useVirtualSectionWindow.ts +338 -0
  319. package/src/composables/virtual/useWindowedStore.ts +86 -0
  320. package/src/composables/virtual/virtualSectionLayout.ts +212 -0
  321. package/src/index.ts +9 -0
  322. package/src/styles/animations.css +233 -0
  323. package/src/styles/cards.css +66 -0
  324. package/src/styles/dock.css +221 -0
  325. package/src/styles/floating-panel.css +49 -0
  326. package/src/styles/glass.css +266 -0
  327. package/src/styles/index.css +26 -0
  328. package/src/styles/scroll-pane.css +10 -0
  329. package/src/styles/theme.css +138 -0
  330. package/src/styles/tokens.css +333 -0
  331. package/src/styles/transitions.css +226 -0
  332. package/src/styles/typography.css +277 -0
  333. package/src/styles/utilities.css +697 -0
  334. package/src/utils/cn.ts +6 -0
  335. package/src/utils/index.ts +1 -0
@@ -0,0 +1,136 @@
1
+ import { ref, onUnmounted, watch, type Ref } from "vue";
2
+ import { mulberry32, hashString, randomRadii, radiiToCSS } from "./prng";
3
+
4
+ export interface UseWatercolorBlobOptions {
5
+ /** Enable rAF animation loop */
6
+ animate?: boolean;
7
+ /** Base cycle duration in ms (default 4000) */
8
+ cycleDuration?: number;
9
+ /** Border-radius range [lo, hi] as percentages (default [20, 80]) */
10
+ range?: [number, number];
11
+ /** Extra seed string mixed into the hash for unique shapes */
12
+ seed?: string;
13
+ }
14
+
15
+ /**
16
+ * Per-vertex animation state — each of the 8 border-radius values
17
+ * animates independently with its own timing, producing organic motion.
18
+ */
19
+ interface VertexState {
20
+ from: number;
21
+ to: number;
22
+ startTime: number;
23
+ duration: number;
24
+ }
25
+
26
+ export function useWatercolorBlob(
27
+ color: Ref<string> | (() => string),
28
+ options: UseWatercolorBlobOptions = {},
29
+ ) {
30
+ const {
31
+ animate = false,
32
+ cycleDuration = 4000,
33
+ range = [20, 80],
34
+ seed = "",
35
+ } = options;
36
+
37
+ const borderRadius = ref("");
38
+ const hoverBorderRadius = ref("");
39
+
40
+ const getColor = typeof color === "function" ? color : () => color.value;
41
+
42
+ // Deterministic initial shape from color string + optional seed
43
+ const rng = mulberry32(hashString(getColor() + seed));
44
+ const initial = randomRadii(rng, range[0], range[1]);
45
+ borderRadius.value = radiiToCSS(initial);
46
+
47
+ // Generate a second deterministic shape for hover state
48
+ const hoverShape = randomRadii(rng, range[0], range[1]);
49
+ hoverBorderRadius.value = radiiToCSS(hoverShape);
50
+
51
+ // No-op nudge for non-animated blobs
52
+ let nudge = () => {};
53
+
54
+ // Re-seed when color changes (non-animated mode)
55
+ if (!animate) {
56
+ if (typeof color !== "function") {
57
+ watch(color, (c) => {
58
+ const r = mulberry32(hashString(c + seed));
59
+ borderRadius.value = radiiToCSS(randomRadii(r, range[0], range[1]));
60
+ hoverBorderRadius.value = radiiToCSS(randomRadii(r, range[0], range[1]));
61
+ });
62
+ }
63
+ return { borderRadius, hoverBorderRadius, nudge };
64
+ }
65
+
66
+ // --- Per-vertex independent animation ---
67
+
68
+ const [lo, hi] = range;
69
+ const current = [...initial];
70
+
71
+ // Each vertex gets its own timing with wide variance
72
+ const vertices: VertexState[] = [];
73
+ for (let i = 0; i < 8; i++) {
74
+ const durationMul = 0.5 + rng() * 1.3;
75
+ const phaseOffset = rng();
76
+ vertices.push({
77
+ from: initial[i],
78
+ to: lo + rng() * (hi - lo),
79
+ startTime: -phaseOffset * cycleDuration * durationMul,
80
+ duration: cycleDuration * durationMul,
81
+ });
82
+ }
83
+
84
+ let rafId: number | null = null;
85
+ let lastNow = 0;
86
+
87
+ function tick(now: number) {
88
+ lastNow = now;
89
+ for (let i = 0; i < 8; i++) {
90
+ const v = vertices[i];
91
+ let t = (now - v.startTime) / v.duration;
92
+
93
+ if (t >= 1) {
94
+ v.from = v.to;
95
+ v.to = lo + rng() * (hi - lo);
96
+ v.duration = cycleDuration * (0.5 + rng() * 1.3);
97
+ v.startTime = now;
98
+ t = 0;
99
+ }
100
+
101
+ // Sinusoidal ease — smoother and more organic than quadratic
102
+ const ease = 0.5 - 0.5 * Math.cos(Math.PI * t);
103
+ current[i] = v.from + ease * (v.to - v.from);
104
+ }
105
+
106
+ borderRadius.value = radiiToCSS(current);
107
+ rafId = requestAnimationFrame(tick);
108
+ }
109
+
110
+ /**
111
+ * Nudge: immediately retarget all vertices to new random positions
112
+ * with short durations, creating a visible "jiggle" effect.
113
+ */
114
+ nudge = () => {
115
+ const now = lastNow || performance.now();
116
+ for (let i = 0; i < 8; i++) {
117
+ const v = vertices[i];
118
+ v.from = current[i];
119
+ v.to = lo + rng() * (hi - lo);
120
+ // Fast transition (25-50% of normal cycle)
121
+ v.duration = cycleDuration * (0.25 + rng() * 0.25);
122
+ v.startTime = now;
123
+ }
124
+ };
125
+
126
+ rafId = requestAnimationFrame(tick);
127
+
128
+ onUnmounted(() => {
129
+ if (rafId !== null) {
130
+ cancelAnimationFrame(rafId);
131
+ rafId = null;
132
+ }
133
+ });
134
+
135
+ return { borderRadius, hoverBorderRadius, nudge };
136
+ }
@@ -0,0 +1,22 @@
1
+ export {
2
+ useVirtualSectionWindow,
3
+ type VirtualSectionWindowOptions,
4
+ } from "./useVirtualSectionWindow";
5
+
6
+ export {
7
+ useWindowedStore,
8
+ type WindowedStore,
9
+ type UseWindowedStoreOptions,
10
+ } from "./useWindowedStore";
11
+
12
+ export {
13
+ buildSectionLayout,
14
+ findSectionOffset,
15
+ resolveActiveSection,
16
+ resolveSectionWindow,
17
+ type FlatSection,
18
+ type ForcedSectionWindowRange,
19
+ type SectionLayout,
20
+ type SectionLayoutEntry,
21
+ type SectionWindowRange,
22
+ } from "./virtualSectionLayout";
@@ -0,0 +1,338 @@
1
+ import {
2
+ computed,
3
+ onMounted,
4
+ onUnmounted,
5
+ ref,
6
+ shallowRef,
7
+ toValue,
8
+ watch,
9
+ type MaybeRefOrGetter,
10
+ type Ref,
11
+ } from "vue";
12
+ import type { FlatSection } from "./virtualSectionLayout";
13
+ import {
14
+ buildSectionLayout,
15
+ findSectionOffset,
16
+ resolveActiveSection,
17
+ resolveSectionWindow,
18
+ type ForcedSectionWindowRange,
19
+ type SectionLayout,
20
+ type SectionWindowRange,
21
+ } from "./virtualSectionLayout";
22
+
23
+ export interface VirtualSectionWindowOptions<T extends FlatSection = FlatSection> {
24
+ /** The flat list of section items to virtualize. */
25
+ items: MaybeRefOrGetter<readonly T[]>;
26
+ /**
27
+ * The element whose scroll position drives the window.
28
+ * `null` = use `window` / document scroll.
29
+ */
30
+ scrollContainer: Ref<HTMLElement | null>;
31
+ /**
32
+ * The wrapper element around the virtualized content.
33
+ * When provided, scroll offset is computed relative to this element's
34
+ * position in the page, so content above it (headers, images, etc.)
35
+ * is correctly excluded from the layout calculation.
36
+ */
37
+ contentEl?: Ref<HTMLElement | null>;
38
+ /** Overscan before the viewport in pixels. Default: viewport height. */
39
+ overscanBeforePx?: number;
40
+ /** Overscan after the viewport in pixels. Default: 2x viewport height. */
41
+ overscanAfterPx?: number;
42
+ /** Number of items to warm before a target when using `ensureTargetWindow`. Default: 2. */
43
+ warmTargetBefore?: number;
44
+ /** Number of items to warm after a target when using `ensureTargetWindow`. Default: 3. */
45
+ warmTargetAfter?: number;
46
+ }
47
+
48
+ /** Session-level height cache shared across all instances. */
49
+ const SESSION_HEIGHT_CACHE = new Map<string, number>();
50
+
51
+ /**
52
+ * Scroll-based virtual windowing composable for section lists.
53
+ *
54
+ * Renders only the items visible in (or near) the scroll viewport,
55
+ * using spacer divs to maintain correct scroll height. Supports:
56
+ *
57
+ * - Measured heights: call `measureSection(id, el)` from a template ref
58
+ * to replace estimated heights with real DOM measurements.
59
+ * - Warm targeting: call `ensureTargetWindow(id)` to force an item into
60
+ * the render window before scrolling to it.
61
+ * - Active tracking: `activeId` / `activeRootId` report which section
62
+ * is currently at the 20% viewport mark.
63
+ * - Content offset: provide `contentEl` to correctly handle non-virtualized
64
+ * content (headers, banners) above the virtual list.
65
+ */
66
+ export function useVirtualSectionWindow<T extends FlatSection>(
67
+ options: VirtualSectionWindowOptions<T>,
68
+ ) {
69
+ const items = computed(() => Array.from(toValue(options.items)));
70
+ const measuredHeights = new Map<string, number>();
71
+ const itemIndex = new Map<string, number>();
72
+ const layout = shallowRef<SectionLayout<T>>({
73
+ entries: [],
74
+ totalHeight: 0,
75
+ });
76
+ const range = ref<SectionWindowRange>({
77
+ startIndex: 0,
78
+ endIndex: -1,
79
+ topSpacerPx: 0,
80
+ bottomSpacerPx: 0,
81
+ });
82
+ const activeItem = ref<T | null>(null);
83
+ const warmRange = ref<ForcedSectionWindowRange | null>(null);
84
+
85
+ const elementMap = new Map<string, HTMLElement>();
86
+ let scrollRaf = 0;
87
+ let recalcRaf = 0;
88
+ let warmTimer = 0;
89
+ let containerResizeObserver: ResizeObserver | null = null;
90
+
91
+ function getViewportHeight(): number {
92
+ return Math.max(
93
+ 1,
94
+ options.scrollContainer.value?.clientHeight ?? window.innerHeight ?? 900,
95
+ );
96
+ }
97
+
98
+ /**
99
+ * Compute how far the user has scrolled into the virtualized content.
100
+ * When `contentEl` is provided, we measure relative to the content
101
+ * wrapper's position — so headers/images above it are excluded.
102
+ */
103
+ function getContentScrollTop(): number {
104
+ const contentEl = options.contentEl?.value;
105
+ if (contentEl) {
106
+ // Negative rect.top = user has scrolled past the content's top edge
107
+ return Math.max(0, -contentEl.getBoundingClientRect().top);
108
+ }
109
+ const container = options.scrollContainer.value;
110
+ return container
111
+ ? container.scrollTop
112
+ : (document.documentElement.scrollTop || document.body.scrollTop || 0);
113
+ }
114
+
115
+ function getHeight(item: T): number {
116
+ return (
117
+ measuredHeights.get(item.id) ??
118
+ SESSION_HEIGHT_CACHE.get(item.id) ??
119
+ item.estimatedHeight
120
+ );
121
+ }
122
+
123
+ function rebuildLayout() {
124
+ layout.value = buildSectionLayout(items.value, getHeight);
125
+ }
126
+
127
+ function computeWindowState() {
128
+ const viewportHeight = getViewportHeight();
129
+ const overscanBeforePx = options.overscanBeforePx ?? viewportHeight;
130
+ const overscanAfterPx = options.overscanAfterPx ?? viewportHeight * 2;
131
+ const normalizedScrollTop = getContentScrollTop();
132
+
133
+ range.value = resolveSectionWindow(
134
+ layout.value,
135
+ normalizedScrollTop,
136
+ viewportHeight,
137
+ overscanBeforePx,
138
+ overscanAfterPx,
139
+ warmRange.value,
140
+ );
141
+ activeItem.value = resolveActiveSection(
142
+ layout.value,
143
+ normalizedScrollTop + viewportHeight * 0.2,
144
+ );
145
+ }
146
+
147
+ /** Force a full layout rebuild and window recomputation. */
148
+ function recalculate() {
149
+ rebuildLayout();
150
+ computeWindowState();
151
+ }
152
+
153
+ function scheduleRecalculate() {
154
+ if (recalcRaf) return;
155
+ recalcRaf = requestAnimationFrame(() => {
156
+ recalcRaf = 0;
157
+ recalculate();
158
+ });
159
+ }
160
+
161
+ function scheduleWarmRangeRelease() {
162
+ if (warmTimer) window.clearTimeout(warmTimer);
163
+ warmTimer = window.setTimeout(() => {
164
+ warmRange.value = null;
165
+ scheduleRecalculate();
166
+ }, 320);
167
+ }
168
+
169
+ function syncMeasuredHeight(id: string, height: number) {
170
+ const normalized = Math.max(1, Math.round(height));
171
+ if (measuredHeights.get(id) === normalized) return;
172
+ measuredHeights.set(id, normalized);
173
+ SESSION_HEIGHT_CACHE.set(id, normalized);
174
+ scheduleRecalculate();
175
+ }
176
+
177
+ function disconnectSection(id: string) {
178
+ elementMap.delete(id);
179
+ }
180
+
181
+ /**
182
+ * Register (or update) a section's DOM element for height measurement.
183
+ * Call from a template ref callback: `:ref="(el) => measureSection(item.id, el)"`.
184
+ * Pass `null` to disconnect.
185
+ */
186
+ function measureSection(id: string, el: HTMLElement | null) {
187
+ if (!el) {
188
+ disconnectSection(id);
189
+ return;
190
+ }
191
+
192
+ const current = elementMap.get(id);
193
+ if (current === el) {
194
+ syncMeasuredHeight(id, el.offsetHeight);
195
+ return;
196
+ }
197
+
198
+ disconnectSection(id);
199
+ elementMap.set(id, el);
200
+ requestAnimationFrame(() => {
201
+ const target = elementMap.get(id);
202
+ if (target) syncMeasuredHeight(id, target.offsetHeight);
203
+ });
204
+ }
205
+
206
+ /**
207
+ * Force an item into the render window so it can be scrolled to.
208
+ * The warm range auto-releases after 320ms.
209
+ */
210
+ function ensureTargetWindow(id: string) {
211
+ const index = itemIndex.get(id);
212
+ if (index == null) return;
213
+ const warmBefore = options.warmTargetBefore ?? 2;
214
+ const warmAfter = options.warmTargetAfter ?? 3;
215
+ warmRange.value = {
216
+ startIndex: Math.max(0, index - warmBefore),
217
+ endIndex: Math.min(items.value.length - 1, index + warmAfter),
218
+ };
219
+ scheduleWarmRangeRelease();
220
+ recalculate();
221
+ }
222
+
223
+ /** Get the pixel offset of a section by id, or `null` if not found. */
224
+ function getOffsetFor(id: string): number | null {
225
+ return findSectionOffset(layout.value, id);
226
+ }
227
+
228
+ function attachContainerObserver(container: HTMLElement | null) {
229
+ containerResizeObserver?.disconnect();
230
+ containerResizeObserver = null;
231
+ if (!container || typeof ResizeObserver === "undefined") return;
232
+ containerResizeObserver = new ResizeObserver(() => {
233
+ scheduleRecalculate();
234
+ });
235
+ containerResizeObserver.observe(container);
236
+ }
237
+
238
+ function handleScroll() {
239
+ if (scrollRaf) return;
240
+ scrollRaf = requestAnimationFrame(() => {
241
+ scrollRaf = 0;
242
+ computeWindowState();
243
+ });
244
+ }
245
+
246
+ let currentContainer: EventTarget | null = null;
247
+ function bindContainer(container: HTMLElement | null) {
248
+ const scrollTarget: EventTarget = container ?? window;
249
+ if (currentContainer === scrollTarget) return;
250
+ if (currentContainer) {
251
+ currentContainer.removeEventListener("scroll", handleScroll);
252
+ }
253
+ currentContainer = scrollTarget;
254
+ currentContainer.addEventListener("scroll", handleScroll, { passive: true });
255
+ attachContainerObserver(container);
256
+ scheduleRecalculate();
257
+ }
258
+
259
+ watch(
260
+ items,
261
+ (nextItems) => {
262
+ itemIndex.clear();
263
+ for (const item of nextItems) {
264
+ itemIndex.set(item.id, item.index);
265
+ const cached = SESSION_HEIGHT_CACHE.get(item.id);
266
+ if (cached != null) measuredHeights.set(item.id, cached);
267
+ }
268
+ for (const id of [...measuredHeights.keys()]) {
269
+ if (!itemIndex.has(id)) measuredHeights.delete(id);
270
+ }
271
+ recalculate();
272
+ },
273
+ { immediate: true },
274
+ );
275
+
276
+ watch(
277
+ options.scrollContainer,
278
+ (container) => bindContainer(container),
279
+ { immediate: true },
280
+ );
281
+
282
+ // Also observe the content element for resize (e.g. images loading above)
283
+ let contentResizeObserver: ResizeObserver | null = null;
284
+ if (options.contentEl) {
285
+ watch(
286
+ options.contentEl,
287
+ (el) => {
288
+ contentResizeObserver?.disconnect();
289
+ contentResizeObserver = null;
290
+ if (el && typeof ResizeObserver !== "undefined") {
291
+ contentResizeObserver = new ResizeObserver(() => scheduleRecalculate());
292
+ contentResizeObserver.observe(el);
293
+ }
294
+ },
295
+ { immediate: true },
296
+ );
297
+ }
298
+
299
+ onMounted(() => {
300
+ // Ensure initial calculation after DOM is ready
301
+ scheduleRecalculate();
302
+ });
303
+
304
+ onUnmounted(() => {
305
+ if (scrollRaf) cancelAnimationFrame(scrollRaf);
306
+ if (recalcRaf) cancelAnimationFrame(recalcRaf);
307
+ if (warmTimer) window.clearTimeout(warmTimer);
308
+ if (currentContainer) {
309
+ currentContainer.removeEventListener("scroll", handleScroll);
310
+ }
311
+ containerResizeObserver?.disconnect();
312
+ contentResizeObserver?.disconnect();
313
+ elementMap.clear();
314
+ });
315
+
316
+ /** The subset of items currently in the render window. */
317
+ const visibleItems = computed(() => {
318
+ if (range.value.endIndex < range.value.startIndex) return [] as T[];
319
+ return items.value.slice(range.value.startIndex, range.value.endIndex + 1);
320
+ });
321
+
322
+ /** The `id` of the currently active (at 20% viewport) section. */
323
+ const activeId = computed(() => activeItem.value?.id ?? null);
324
+ /** The `rootId` of the currently active section. */
325
+ const activeRootId = computed(() => activeItem.value?.rootId ?? null);
326
+
327
+ return {
328
+ visibleItems,
329
+ topSpacerPx: computed(() => range.value.topSpacerPx),
330
+ bottomSpacerPx: computed(() => range.value.bottomSpacerPx),
331
+ measureSection,
332
+ ensureTargetWindow,
333
+ getOffsetFor,
334
+ activeId,
335
+ activeRootId,
336
+ recalculate,
337
+ };
338
+ }
@@ -0,0 +1,86 @@
1
+ import { ref, shallowRef, type Ref, type ShallowRef } from "vue";
2
+
3
+ export interface UseWindowedStoreOptions {
4
+ /** Maximum items to keep in memory. Default: 200. */
5
+ maxResident?: number;
6
+ }
7
+
8
+ export interface WindowedStore<T> {
9
+ /** Currently loaded items (a sliding window of the full list). */
10
+ items: ShallowRef<T[]>;
11
+ /** Logical offset of `items[0]` in the full list. */
12
+ windowStart: Ref<number>;
13
+ /** Generation counter -- increments on each reset. Used to reject stale appends. */
14
+ generation: Ref<number>;
15
+ /** Append or replace items. `replace=true` resets window; `replace=false` appends with eviction from front. */
16
+ set(newItems: T[], replace?: boolean): void;
17
+ /** Prepend items before the current window. Evicts from the end if over `maxResident`. */
18
+ prepend(newItems: T[], newWindowStart: number): void;
19
+ /** Clear all items and reset window. */
20
+ clear(): void;
21
+ /**
22
+ * Append items only if the generation matches (i.e., no reset happened since the fetch started).
23
+ * Returns `true` if the append was accepted, `false` if rejected (stale).
24
+ */
25
+ appendIfCurrent(newItems: T[], expectedGeneration: number): boolean;
26
+ }
27
+
28
+ /**
29
+ * Sliding-window store primitive for paginated / infinite-scroll data.
30
+ *
31
+ * Supports both forward (append) and backward (prepend) loading.
32
+ * When items exceed `maxResident`, trims from the opposite end.
33
+ *
34
+ * Uses a generation counter to reject stale appends after a backward reset,
35
+ * preventing race conditions between concurrent page fetches.
36
+ */
37
+ export function useWindowedStore<T>(
38
+ options?: UseWindowedStoreOptions,
39
+ ): WindowedStore<T> {
40
+ const maxResident = options?.maxResident ?? 200;
41
+ const items = shallowRef<T[]>([]);
42
+ const windowStart = ref(0);
43
+ const generation = ref(0);
44
+
45
+ function set(newItems: T[], replace = true) {
46
+ if (replace) {
47
+ items.value = [...newItems];
48
+ windowStart.value = 0;
49
+ generation.value++;
50
+ } else {
51
+ const merged = [...items.value, ...newItems];
52
+ if (merged.length > maxResident) {
53
+ const trimCount = merged.length - maxResident;
54
+ items.value = merged.slice(trimCount);
55
+ windowStart.value += trimCount;
56
+ } else {
57
+ items.value = merged;
58
+ }
59
+ }
60
+ }
61
+
62
+ function appendIfCurrent(newItems: T[], expectedGeneration: number): boolean {
63
+ if (generation.value !== expectedGeneration) return false;
64
+ set(newItems, false);
65
+ return true;
66
+ }
67
+
68
+ function prepend(newItems: T[], newWindowStart: number) {
69
+ const merged = [...newItems, ...items.value];
70
+ windowStart.value = newWindowStart;
71
+ generation.value++;
72
+ if (merged.length > maxResident) {
73
+ items.value = merged.slice(0, maxResident);
74
+ } else {
75
+ items.value = merged;
76
+ }
77
+ }
78
+
79
+ function clear() {
80
+ items.value = [];
81
+ windowStart.value = 0;
82
+ generation.value++;
83
+ }
84
+
85
+ return { items, windowStart, generation, set, prepend, appendIfCurrent, clear };
86
+ }