@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,242 @@
1
+ /**
2
+ * Tracks which tree node is currently visible via IntersectionObserver,
3
+ * with a scroll-event fallback for fast scrollbar drags.
4
+ * Deepest visible node wins.
5
+ *
6
+ * Generic over `T extends TreeNode` — pass a custom `getChildren` callback
7
+ * for tree structures whose children are stored under a different property.
8
+ */
9
+ import { ref, computed, watch, onMounted, onUnmounted, nextTick, toValue } from "vue";
10
+ import type { MaybeRefOrGetter, Ref } from "vue";
11
+ import type { TreeNode, TreeIndexEntry, ScrollTrackerOptions } from "../types";
12
+
13
+ export function useScrollTracker<T extends TreeNode>(
14
+ roots: MaybeRefOrGetter<T[]>,
15
+ index: MaybeRefOrGetter<Map<string, TreeIndexEntry<T>>>,
16
+ options?: ScrollTrackerOptions & {
17
+ getChildren?: (node: T) => T[] | undefined;
18
+ scrollContainer?: Ref<HTMLElement | null>;
19
+ },
20
+ ) {
21
+ const getChildren =
22
+ options?.getChildren ?? ((n: T) => n.children as T[] | undefined);
23
+ const rootMargin = options?.rootMargin ?? "-20% 0px -60% 0px";
24
+ const threshold = options?.threshold ?? 0;
25
+
26
+ const activeId = ref<string | null>(null);
27
+ const sectionVisibility = new Map<string, boolean>();
28
+ let observer: IntersectionObserver | null = null;
29
+ const observedIds = new Set<string>();
30
+ let locked = false;
31
+ let mounted = false;
32
+
33
+ function lockTracking() {
34
+ locked = true;
35
+ }
36
+ function unlockTracking() {
37
+ locked = false;
38
+ }
39
+
40
+ const activeRootId = computed(() => {
41
+ if (!activeId.value) return null;
42
+ return toValue(index).get(activeId.value)?.parentId ?? null;
43
+ });
44
+
45
+ function findDeepestVisible(list: T[]): string | null {
46
+ for (const node of list) {
47
+ const children = getChildren(node);
48
+ if (children) {
49
+ const deep = findDeepestVisible(children);
50
+ if (deep) return deep;
51
+ }
52
+ if (sectionVisibility.get(node.id)) return node.id;
53
+ }
54
+ return null;
55
+ }
56
+
57
+ function updateActive() {
58
+ const found = findDeepestVisible(toValue(roots));
59
+ if (found) activeId.value = found;
60
+ }
61
+
62
+ let cachedIds: string[] | null = null;
63
+ function collectIds(): string[] {
64
+ if (cachedIds) return cachedIds;
65
+ const out: string[] = [];
66
+ function walk(nodes: T[]) {
67
+ for (const node of nodes) {
68
+ out.push(node.id);
69
+ const children = getChildren(node);
70
+ if (children) walk(children);
71
+ }
72
+ }
73
+ walk(toValue(roots));
74
+ cachedIds = out;
75
+ return out;
76
+ }
77
+
78
+ function invalidateIdCache() {
79
+ cachedIds = null;
80
+ }
81
+
82
+ let rafId = 0;
83
+ function onScroll() {
84
+ if (locked || rafId) return;
85
+ rafId = requestAnimationFrame(() => {
86
+ rafId = 0;
87
+ const container = options?.scrollContainer?.value;
88
+ const topPct = parseFloat(rootMargin.split(" ")[0]) / 100;
89
+ const viewportH = container
90
+ ? container.clientHeight
91
+ : window.innerHeight;
92
+ const activeZoneTop = Math.abs(topPct) * viewportH;
93
+ const containerTop = container
94
+ ? container.getBoundingClientRect().top
95
+ : 0;
96
+
97
+ const allIds = collectIds();
98
+ let bestId: string | null = null;
99
+ let bestDist = Infinity;
100
+ let closestBelowId: string | null = null;
101
+ let closestBelowDist = Infinity;
102
+
103
+ for (const id of allIds) {
104
+ const el = document.getElementById(id);
105
+ if (!el) continue;
106
+ const rect = el.getBoundingClientRect();
107
+ const dist = rect.top - containerTop - activeZoneTop;
108
+ if (dist <= 0 && Math.abs(dist) < bestDist) {
109
+ bestDist = Math.abs(dist);
110
+ bestId = id;
111
+ }
112
+ if (dist > 0 && dist < closestBelowDist) {
113
+ closestBelowDist = dist;
114
+ closestBelowId = id;
115
+ }
116
+ }
117
+
118
+ const resolvedId = bestId ?? closestBelowId;
119
+ if (resolvedId && resolvedId !== activeId.value) {
120
+ sectionVisibility.clear();
121
+ sectionVisibility.set(resolvedId, true);
122
+ activeId.value = resolvedId;
123
+ }
124
+ });
125
+ }
126
+
127
+ function observeTree(list: T[]) {
128
+ for (const node of list) {
129
+ if (!observedIds.has(node.id)) {
130
+ const el = document.getElementById(node.id);
131
+ if (el) {
132
+ observer?.observe(el);
133
+ observedIds.add(node.id);
134
+ }
135
+ }
136
+ const children = getChildren(node);
137
+ if (children) observeTree(children);
138
+ }
139
+ }
140
+
141
+ function setupObserver() {
142
+ observer?.disconnect();
143
+ observedIds.clear();
144
+ sectionVisibility.clear();
145
+ invalidateIdCache();
146
+
147
+ const container = options?.scrollContainer?.value;
148
+ observer = new IntersectionObserver(
149
+ (entries) => {
150
+ if (locked) return;
151
+ for (const entry of entries) {
152
+ sectionVisibility.set(
153
+ (entry.target as HTMLElement).id,
154
+ entry.isIntersecting,
155
+ );
156
+ }
157
+ updateActive();
158
+ },
159
+ { root: container ?? undefined, rootMargin, threshold },
160
+ );
161
+
162
+ nextTick(() => {
163
+ observeTree(toValue(roots));
164
+ const currentRoots = toValue(roots);
165
+ if (!activeId.value && currentRoots.length > 0) {
166
+ activeId.value = currentRoots[0].id;
167
+ }
168
+ });
169
+ }
170
+
171
+ watch(
172
+ () => toValue(roots),
173
+ (newRoots) => {
174
+ if (!mounted) return;
175
+ activeId.value = newRoots[0]?.id ?? null;
176
+ setupObserver();
177
+ },
178
+ );
179
+
180
+ let scrollTarget: EventTarget | null = null;
181
+
182
+ onMounted(() => {
183
+ mounted = true;
184
+ setupObserver();
185
+
186
+ const container = options?.scrollContainer?.value;
187
+ scrollTarget = container ?? document;
188
+ scrollTarget.addEventListener("scroll", onScroll, { passive: true });
189
+ });
190
+
191
+ onUnmounted(() => {
192
+ mounted = false;
193
+ observer?.disconnect();
194
+ if (rafId) cancelAnimationFrame(rafId);
195
+ scrollTarget?.removeEventListener("scroll", onScroll);
196
+ });
197
+
198
+ function forceRecalculate() {
199
+ sectionVisibility.clear();
200
+ if (rafId) cancelAnimationFrame(rafId);
201
+ rafId = 0;
202
+
203
+ const container = options?.scrollContainer?.value;
204
+ const topPct = parseFloat(rootMargin.split(" ")[0]) / 100;
205
+ const viewportH = container
206
+ ? container.clientHeight
207
+ : window.innerHeight;
208
+ const activeZoneTop = Math.abs(topPct) * viewportH;
209
+ const containerTop = container
210
+ ? container.getBoundingClientRect().top
211
+ : 0;
212
+
213
+ const allIds = collectIds();
214
+ let bestId: string | null = null;
215
+ let bestDist = Infinity;
216
+ let closestBelowId: string | null = null;
217
+ let closestBelowDist = Infinity;
218
+
219
+ for (const id of allIds) {
220
+ const el = document.getElementById(id);
221
+ if (!el) continue;
222
+ const rect = el.getBoundingClientRect();
223
+ const dist = rect.top - containerTop - activeZoneTop;
224
+ if (dist <= 0 && Math.abs(dist) < bestDist) {
225
+ bestDist = Math.abs(dist);
226
+ bestId = id;
227
+ }
228
+ if (dist > 0 && dist < closestBelowDist) {
229
+ closestBelowDist = dist;
230
+ closestBelowId = id;
231
+ }
232
+ }
233
+
234
+ const resolvedId = bestId ?? closestBelowId;
235
+ if (resolvedId) {
236
+ sectionVisibility.set(resolvedId, true);
237
+ activeId.value = resolvedId;
238
+ }
239
+ }
240
+
241
+ return { activeId, activeRootId, forceRecalculate, lockTracking, unlockTracking };
242
+ }
@@ -0,0 +1,247 @@
1
+ /**
2
+ * Damped scroll-following for sidebar navigation.
3
+ * Keeps the active item in view without hijacking manual scrolling.
4
+ *
5
+ * - Deadzone: no scroll if active item is already visible
6
+ * - Manual override: stops auto-scrolling when user scrolls the sidebar
7
+ * - Damped animation: smooth approach via requestAnimationFrame
8
+ */
9
+ import { nextTick, onMounted, onUnmounted, watch } from "vue";
10
+ import type { Ref } from "vue";
11
+
12
+ export interface SidebarFollowOptions {
13
+ /** The sidebar nav element to auto-scroll. */
14
+ sidebarEl: Ref<HTMLElement | null>;
15
+ /** Currently active section ID. */
16
+ activeId: Ref<string | null>;
17
+ /** Currently active root section ID. */
18
+ activeRootId?: Ref<string | null>;
19
+ /** External scroll source (page container) to listen to. */
20
+ scrollSource?: Ref<HTMLElement | null>;
21
+ /** Damping factor (0-1, default 0.22). Lower = slower approach. */
22
+ damping?: number;
23
+ }
24
+
25
+ export function useSidebarFollow(options: SidebarFollowOptions) {
26
+ const damping = options.damping ?? 0.22;
27
+ let followRaf = 0;
28
+ let syncRaf = 0;
29
+ let currentScrollSource: HTMLElement | null = null;
30
+ let currentSidebar: HTMLElement | null = null;
31
+ let targetScrollTop: number | null = null;
32
+ let manualOverride = false;
33
+ let programmaticScrollDepth = 0;
34
+
35
+ function suspendForManualInteraction() {
36
+ manualOverride = true;
37
+ targetScrollTop = null;
38
+ if (followRaf) {
39
+ cancelAnimationFrame(followRaf);
40
+ followRaf = 0;
41
+ }
42
+ if (syncRaf) {
43
+ cancelAnimationFrame(syncRaf);
44
+ syncRaf = 0;
45
+ }
46
+ }
47
+
48
+ function escapeSelector(value: string): string {
49
+ if (typeof CSS !== "undefined" && typeof CSS.escape === "function") {
50
+ return CSS.escape(value);
51
+ }
52
+ return value.replace(/["\\]/g, "\\$&");
53
+ }
54
+
55
+ function getActiveElement(): HTMLElement | null {
56
+ const nav = options.sidebarEl.value;
57
+ const id = options.activeId.value ?? options.activeRootId?.value ?? null;
58
+ if (!nav || !id) return null;
59
+ return nav.querySelector(
60
+ `[data-toc-id="${escapeSelector(id)}"]`,
61
+ ) as HTMLElement | null;
62
+ }
63
+
64
+ function resolveTarget(nav: HTMLElement, activeEl: HTMLElement): number {
65
+ const navHeight = nav.clientHeight;
66
+ const elementCenter = activeEl.offsetTop + activeEl.offsetHeight / 2;
67
+ const maxScrollTop = Math.max(0, nav.scrollHeight - navHeight);
68
+ const deadzone = navHeight * 0.18;
69
+ const currentCenter = nav.scrollTop + navHeight / 2;
70
+ if (Math.abs(elementCenter - currentCenter) <= deadzone) {
71
+ return nav.scrollTop;
72
+ }
73
+ return Math.max(0, Math.min(maxScrollTop, elementCenter - navHeight / 2));
74
+ }
75
+
76
+ function withProgrammaticScroll(fn: () => void) {
77
+ programmaticScrollDepth += 1;
78
+ try {
79
+ fn();
80
+ } finally {
81
+ requestAnimationFrame(() => {
82
+ programmaticScrollDepth = Math.max(0, programmaticScrollDepth - 1);
83
+ });
84
+ }
85
+ }
86
+
87
+ function follow() {
88
+ followRaf = 0;
89
+ if (manualOverride) return;
90
+ const nav = options.sidebarEl.value;
91
+ const target = targetScrollTop;
92
+ if (!nav || target == null) return;
93
+
94
+ const delta = target - nav.scrollTop;
95
+ if (Math.abs(delta) < 1) {
96
+ withProgrammaticScroll(() => {
97
+ nav.scrollTop = target;
98
+ });
99
+ targetScrollTop = null;
100
+ return;
101
+ }
102
+
103
+ withProgrammaticScroll(() => {
104
+ nav.scrollTop += delta * damping;
105
+ });
106
+ followRaf = requestAnimationFrame(follow);
107
+ }
108
+
109
+ function queue(immediate = false) {
110
+ if (!immediate && manualOverride) return;
111
+ const nav = options.sidebarEl.value;
112
+ const activeEl = getActiveElement();
113
+ if (!nav || !activeEl) return;
114
+
115
+ targetScrollTop = resolveTarget(nav, activeEl);
116
+
117
+ if (immediate) {
118
+ if (followRaf) {
119
+ cancelAnimationFrame(followRaf);
120
+ followRaf = 0;
121
+ }
122
+ withProgrammaticScroll(() => {
123
+ nav.scrollTop = targetScrollTop!;
124
+ });
125
+ targetScrollTop = null;
126
+ return;
127
+ }
128
+
129
+ if (!followRaf) {
130
+ followRaf = requestAnimationFrame(follow);
131
+ }
132
+ }
133
+
134
+ function scheduleFromScroll() {
135
+ if (manualOverride) {
136
+ manualOverride = false;
137
+ targetScrollTop = null;
138
+ }
139
+ if (syncRaf) return;
140
+ syncRaf = requestAnimationFrame(() => {
141
+ syncRaf = 0;
142
+ queue();
143
+ });
144
+ }
145
+
146
+ function handleSidebarWheel() {
147
+ suspendForManualInteraction();
148
+ }
149
+
150
+ function handleSidebarTouch() {
151
+ suspendForManualInteraction();
152
+ }
153
+
154
+ function handleSidebarPointer(event: PointerEvent) {
155
+ const target = event.target as HTMLElement | null;
156
+ if (target?.closest("[data-toc-id], .sidebar-top-btn")) return;
157
+ suspendForManualInteraction();
158
+ }
159
+
160
+ function handleSidebarKeydown(event: KeyboardEvent) {
161
+ if (
162
+ event.key === "ArrowUp" ||
163
+ event.key === "ArrowDown" ||
164
+ event.key === "PageUp" ||
165
+ event.key === "PageDown" ||
166
+ event.key === "Home" ||
167
+ event.key === "End" ||
168
+ event.key === " "
169
+ ) {
170
+ suspendForManualInteraction();
171
+ }
172
+ }
173
+
174
+ function handleSidebarScroll() {
175
+ if (programmaticScrollDepth > 0) return;
176
+ suspendForManualInteraction();
177
+ }
178
+
179
+ function bindSidebar(nav: HTMLElement | null) {
180
+ if (currentSidebar === nav) return;
181
+ currentSidebar?.removeEventListener("wheel", handleSidebarWheel);
182
+ currentSidebar?.removeEventListener("scroll", handleSidebarScroll);
183
+ currentSidebar?.removeEventListener("touchstart", handleSidebarTouch);
184
+ currentSidebar?.removeEventListener("pointerdown", handleSidebarPointer as EventListener);
185
+ currentSidebar?.removeEventListener("keydown", handleSidebarKeydown);
186
+ currentSidebar = nav;
187
+ currentSidebar?.addEventListener("wheel", handleSidebarWheel, { passive: true });
188
+ currentSidebar?.addEventListener("scroll", handleSidebarScroll, { passive: true });
189
+ currentSidebar?.addEventListener("touchstart", handleSidebarTouch, { passive: true });
190
+ currentSidebar?.addEventListener("pointerdown", handleSidebarPointer as EventListener, { passive: true });
191
+ currentSidebar?.addEventListener("keydown", handleSidebarKeydown);
192
+ }
193
+
194
+ function bindScrollSource(source: HTMLElement | null) {
195
+ if (currentScrollSource === source) return;
196
+ currentScrollSource?.removeEventListener("scroll", scheduleFromScroll);
197
+ currentScrollSource = source;
198
+ currentScrollSource?.addEventListener("scroll", scheduleFromScroll, {
199
+ passive: true,
200
+ });
201
+ }
202
+
203
+ onMounted(() => {
204
+ bindScrollSource(options.scrollSource?.value ?? null);
205
+ bindSidebar(options.sidebarEl.value ?? null);
206
+ nextTick(() => queue(true));
207
+ window.addEventListener("resize", scheduleFromScroll);
208
+ });
209
+
210
+ watch(
211
+ [options.activeId, options.activeRootId ?? { value: null }],
212
+ () => {
213
+ nextTick(() => queue());
214
+ },
215
+ { flush: "post" },
216
+ );
217
+
218
+ watch(
219
+ options.sidebarEl,
220
+ (sidebar) => {
221
+ bindSidebar(sidebar);
222
+ },
223
+ { immediate: true },
224
+ );
225
+
226
+ watch(
227
+ () => options.scrollSource?.value ?? null,
228
+ (source) => {
229
+ bindScrollSource(source);
230
+ },
231
+ { immediate: true },
232
+ );
233
+
234
+ onUnmounted(() => {
235
+ if (followRaf) cancelAnimationFrame(followRaf);
236
+ if (syncRaf) cancelAnimationFrame(syncRaf);
237
+ currentScrollSource?.removeEventListener("scroll", scheduleFromScroll);
238
+ currentSidebar?.removeEventListener("wheel", handleSidebarWheel);
239
+ currentSidebar?.removeEventListener("scroll", handleSidebarScroll);
240
+ currentSidebar?.removeEventListener("touchstart", handleSidebarTouch);
241
+ currentSidebar?.removeEventListener("pointerdown", handleSidebarPointer as EventListener);
242
+ currentSidebar?.removeEventListener("keydown", handleSidebarKeydown);
243
+ window.removeEventListener("resize", scheduleFromScroll);
244
+ });
245
+
246
+ return { queueSidebarFollow: queue };
247
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Manages sidebar section expand/collapse state with user overrides.
3
+ * Combines tree indexing, scroll tracking, and manual toggle tracking
4
+ * into a unified SidebarState.
5
+ */
6
+ import { reactive, computed } from "vue";
7
+ import type { Ref } from "vue";
8
+ import type { SidebarSection, SidebarState } from "../types";
9
+ import { useTreeIndex } from "./useTreeIndex";
10
+
11
+ export interface UseSidebarStateOptions {
12
+ sections: SidebarSection[];
13
+ activeId: Ref<string | null>;
14
+ activeRootId: Ref<string | null>;
15
+ scrollTo: (id: string) => void;
16
+ scrollToTop: () => void;
17
+ }
18
+
19
+ export function useSidebarState(options: UseSidebarStateOptions): SidebarState {
20
+ const {
21
+ index: treeIndex,
22
+ isActive: checkActive,
23
+ isInActiveChain: checkChain,
24
+ } = useTreeIndex(options.sections);
25
+
26
+ // Tracks user overrides for section expand/collapse state
27
+ const userExpanded = reactive(new Set<string>());
28
+ const userCollapsed = reactive(new Set<string>());
29
+
30
+ function isExpanded(sectionId: string): boolean {
31
+ if (userCollapsed.has(sectionId)) return false;
32
+ if (userExpanded.has(sectionId)) return true;
33
+ return options.activeRootId.value === sectionId;
34
+ }
35
+
36
+ function toggleSection(sectionId: string) {
37
+ if (isExpanded(sectionId)) {
38
+ userExpanded.delete(sectionId);
39
+ userCollapsed.add(sectionId);
40
+ } else {
41
+ userCollapsed.delete(sectionId);
42
+ userExpanded.add(sectionId);
43
+ }
44
+ }
45
+
46
+ function navigateTo(id: string) {
47
+ options.scrollTo(id);
48
+ }
49
+
50
+ function isActive(id: string): boolean {
51
+ return checkActive(id, options.activeId.value);
52
+ }
53
+
54
+ function isInActiveChain(id: string): boolean {
55
+ return checkChain(id, options.activeId.value);
56
+ }
57
+
58
+ const activeRootId = computed(() => options.activeRootId.value);
59
+
60
+ return {
61
+ sections: options.sections,
62
+ activeId: options.activeId,
63
+ activeRootId,
64
+ treeIndex,
65
+ isExpanded,
66
+ toggleSection,
67
+ navigateTo,
68
+ scrollToTop: options.scrollToTop,
69
+ isActive,
70
+ isInActiveChain,
71
+ };
72
+ }