@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,152 @@
1
+ import type { TreeNode, TreeIndexEntry, SidebarSection, SidebarIndexEntry } from "../types";
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Generic composable — builds index + provides helpers with `getChildren` support
5
+ // ---------------------------------------------------------------------------
6
+
7
+ /**
8
+ * Builds a flat index of all nodes in a tree for O(1) lookup with hierarchy
9
+ * metadata. Returns the index plus `isActive`, `isInActiveChain`, and
10
+ * `isDescendant` helpers that close over the index.
11
+ *
12
+ * Accepts an optional `getChildren` callback so callers with custom tree
13
+ * shapes (e.g. nodes whose children live under a different property name)
14
+ * don't need to conform to `TreeNode.children`.
15
+ */
16
+ export function useTreeIndex<T extends TreeNode>(
17
+ roots: T[],
18
+ options?: { getChildren?: (node: T) => T[] | undefined },
19
+ ) {
20
+ const getChildren =
21
+ options?.getChildren ?? ((n: T) => n.children as T[] | undefined);
22
+ const index = new Map<string, TreeIndexEntry<T>>();
23
+
24
+ function walk(
25
+ list: T[],
26
+ depth: number,
27
+ parentId: string | null,
28
+ rootId: string,
29
+ rootIndex: number,
30
+ ) {
31
+ for (const node of list) {
32
+ const ri = depth === 0 ? roots.indexOf(node) : rootIndex;
33
+ const rid = depth === 0 ? node.id : rootId;
34
+ index.set(node.id, {
35
+ node,
36
+ depth,
37
+ rootId: rid,
38
+ parentId: depth === 0 ? node.id : parentId,
39
+ rootIndex: ri,
40
+ });
41
+ const children = getChildren(node);
42
+ if (children) {
43
+ walk(
44
+ children,
45
+ depth + 1,
46
+ depth === 0 ? node.id : parentId,
47
+ rid,
48
+ ri,
49
+ );
50
+ }
51
+ }
52
+ }
53
+ walk(roots, 0, null, "", 0);
54
+
55
+ /** Check if `id` is the active node. */
56
+ function isActive(id: string, activeId: string | null): boolean {
57
+ return id === activeId;
58
+ }
59
+
60
+ /** Check if `id` is an ancestor of or equal to `activeId`. */
61
+ function isInActiveChain(id: string, activeId: string | null): boolean {
62
+ if (!activeId) return false;
63
+ const entry = index.get(activeId);
64
+ if (!entry) return false;
65
+ if (id === activeId) return true;
66
+ if (id === entry.parentId) return true;
67
+ const target = index.get(id);
68
+ if (!target) return false;
69
+ return isDescendant(activeId, id);
70
+ }
71
+
72
+ /** Check if `childId` is a descendant of `ancestorId`. */
73
+ function isDescendant(childId: string, ancestorId: string): boolean {
74
+ const ancestor = index.get(ancestorId);
75
+ if (!ancestor) return false;
76
+ const children = getChildren(ancestor.node);
77
+ if (!children) return false;
78
+ for (const child of children) {
79
+ if (child.id === childId) return true;
80
+ if (isDescendant(childId, child.id)) return true;
81
+ }
82
+ return false;
83
+ }
84
+
85
+ return { index, isActive, isInActiveChain, isDescendant };
86
+ }
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // Legacy pure-function API — delegates to useTreeIndex under the hood
90
+ // ---------------------------------------------------------------------------
91
+
92
+ /**
93
+ * Builds a flat index of all nodes in a sidebar tree for O(1) lookup
94
+ * with hierarchy metadata. Pure function, no Vue reactivity.
95
+ *
96
+ * @deprecated Prefer `useTreeIndex` which also returns helper functions.
97
+ */
98
+ export function buildTreeIndex(roots: SidebarSection[]): Map<string, SidebarIndexEntry> {
99
+ return useTreeIndex(roots).index;
100
+ }
101
+
102
+ /**
103
+ * Check if `id` is the active section.
104
+ *
105
+ * Standalone variant kept for backward compatibility — the composable
106
+ * returned by `useTreeIndex` includes the same helper.
107
+ */
108
+ export function isActive(id: string, activeId: string | null): boolean {
109
+ return id === activeId;
110
+ }
111
+
112
+ /**
113
+ * Check if `id` is an ancestor of or equal to `activeId`.
114
+ *
115
+ * Standalone variant kept for backward compatibility — the composable
116
+ * returned by `useTreeIndex` includes the same helper.
117
+ */
118
+ export function isInActiveChain(
119
+ id: string,
120
+ activeId: string | null,
121
+ index: Map<string, SidebarIndexEntry>,
122
+ roots: SidebarSection[],
123
+ ): boolean {
124
+ if (!activeId) return false;
125
+ const entry = index.get(activeId);
126
+ if (!entry) return false;
127
+ if (id === activeId) return true;
128
+ if (id === entry.parentId) return true;
129
+ const target = index.get(id);
130
+ if (!target) return false;
131
+
132
+ // Use the generic isDescendant via a one-off useTreeIndex call
133
+ // to avoid duplicating the recursive logic.
134
+ return _isDescendantLegacy(activeId, id, index);
135
+ }
136
+
137
+ /** Legacy isDescendant that operates on an externally-built index. */
138
+ function _isDescendantLegacy(
139
+ childId: string,
140
+ ancestorId: string,
141
+ index: Map<string, SidebarIndexEntry>,
142
+ ): boolean {
143
+ const ancestor = index.get(ancestorId);
144
+ if (!ancestor) return false;
145
+ const children = ancestor.node.children;
146
+ if (!children) return false;
147
+ for (const child of children) {
148
+ if (child.id === childId) return true;
149
+ if (_isDescendantLegacy(childId, child.id, index)) return true;
150
+ }
151
+ return false;
152
+ }
@@ -0,0 +1,15 @@
1
+ export { default as ProgressiveSidebar } from "./ProgressiveSidebar.vue";
2
+ export { useSidebarState } from "./composables/useSidebarState";
3
+ export type { UseSidebarStateOptions } from "./composables/useSidebarState";
4
+ export { useSidebarFollow } from "./composables/useSidebarFollow";
5
+ export type { SidebarFollowOptions } from "./composables/useSidebarFollow";
6
+ export { useScrollTracker } from "./composables/useScrollTracker";
7
+ export { useTreeIndex, buildTreeIndex, isActive, isInActiveChain } from "./composables/useTreeIndex";
8
+ export type {
9
+ TreeNode,
10
+ TreeIndexEntry,
11
+ SidebarSection,
12
+ SidebarIndexEntry,
13
+ SidebarState,
14
+ ScrollTrackerOptions,
15
+ } from "./types";
@@ -0,0 +1,50 @@
1
+ import type { Ref, ComputedRef } from "vue";
2
+
3
+ /** Minimal interface for tree-structured content with scroll targets. */
4
+ export interface TreeNode {
5
+ id: string;
6
+ children?: TreeNode[];
7
+ }
8
+
9
+ /** Generic flat index entry for a tree node. */
10
+ export interface TreeIndexEntry<T extends TreeNode = TreeNode> {
11
+ node: T;
12
+ depth: number;
13
+ /** ID of the root-level ancestor (self.id when depth === 0). */
14
+ rootId: string;
15
+ /** Direct parent ID (self.id for root nodes). */
16
+ parentId: string | null;
17
+ /** Index within root-level nodes. */
18
+ rootIndex: number;
19
+ }
20
+
21
+ /** A section in the sidebar tree. */
22
+ export interface SidebarSection extends TreeNode {
23
+ title: string;
24
+ children?: SidebarSection[];
25
+ level?: number;
26
+ }
27
+
28
+ /** Flat index entry for a sidebar section (backward-compatible alias). */
29
+ export type SidebarIndexEntry = TreeIndexEntry<SidebarSection>;
30
+
31
+ /** Options for scroll tracking. */
32
+ export interface ScrollTrackerOptions {
33
+ /** IntersectionObserver rootMargin. Default: "-20% 0px -60% 0px" */
34
+ rootMargin?: string;
35
+ threshold?: number;
36
+ }
37
+
38
+ /** Reactive state returned by `useSidebarState`. */
39
+ export interface SidebarState {
40
+ sections: SidebarSection[];
41
+ activeId: Ref<string | null>;
42
+ activeRootId: ComputedRef<string | null>;
43
+ treeIndex: Map<string, SidebarIndexEntry>;
44
+ isExpanded(id: string): boolean;
45
+ toggleSection(id: string): void;
46
+ navigateTo(id: string): void;
47
+ scrollToTop(): void;
48
+ isActive(id: string): boolean;
49
+ isInActiveChain(id: string): boolean;
50
+ }
@@ -0,0 +1,39 @@
1
+ <script setup lang="ts">
2
+ import { type HTMLAttributes } from "vue";
3
+ import BouncyToggle from "./BouncyToggle.vue";
4
+ import type { ToggleOption } from "./BouncyToggle.vue";
5
+
6
+ export interface TabOption {
7
+ label: string;
8
+ value: string;
9
+ }
10
+
11
+ const props = withDefaults(defineProps<{
12
+ options: TabOption[];
13
+ modelValue: string;
14
+ /** "default" = subtle muted slider; "pill" = solid foreground pill */
15
+ variant?: "default" | "pill";
16
+ class?: HTMLAttributes["class"];
17
+ }>(), {
18
+ variant: "default",
19
+ });
20
+
21
+ const emit = defineEmits<{
22
+ "update:modelValue": [value: string];
23
+ }>();
24
+
25
+ function onUpdate(value: string | string[]) {
26
+ emit("update:modelValue", value as string);
27
+ }
28
+ </script>
29
+
30
+ <template>
31
+ <BouncyToggle
32
+ :options="(options as ToggleOption[])"
33
+ :model-value="modelValue"
34
+ :variant="variant"
35
+ :class="props.class"
36
+ :multi-select="false"
37
+ @update:model-value="onUpdate"
38
+ />
39
+ </template>
@@ -0,0 +1,352 @@
1
+ <script setup lang="ts">
2
+ import { ref, watch, computed, onMounted, onUnmounted, nextTick, type HTMLAttributes } from "vue";
3
+ import { cn } from "../../../utils";
4
+ import {
5
+ Tooltip,
6
+ TooltipContent,
7
+ TooltipTrigger,
8
+ TooltipProvider,
9
+ } from "../../ui/tooltip";
10
+
11
+ export interface ToggleOption {
12
+ label: string;
13
+ value: string;
14
+ icon?: string;
15
+ disabled?: boolean;
16
+ tooltip?: string;
17
+ }
18
+
19
+ export interface BouncyToggleProps {
20
+ options: ToggleOption[];
21
+ modelValue: string | string[];
22
+ multiSelect?: boolean;
23
+ /** "default" = subtle muted slider; "pill" = solid foreground pill */
24
+ variant?: "default" | "pill";
25
+ class?: HTMLAttributes["class"];
26
+ }
27
+
28
+ const props = withDefaults(defineProps<BouncyToggleProps>(), {
29
+ multiSelect: false,
30
+ variant: "default",
31
+ });
32
+
33
+ const emit = defineEmits<{
34
+ "update:modelValue": [value: string | string[]];
35
+ }>();
36
+
37
+ const containerRef = ref<HTMLElement | null>(null);
38
+ const buttonRefs = ref<HTMLElement[]>([]);
39
+
40
+ // ── Computed state ──
41
+
42
+ const isPill = computed(() => props.variant === "pill");
43
+
44
+ const activeValues = computed<string[]>(() => {
45
+ if (props.multiSelect) {
46
+ return Array.isArray(props.modelValue) ? props.modelValue : [props.modelValue];
47
+ }
48
+ return [props.modelValue as string];
49
+ });
50
+
51
+ const isActive = (value: string) => activeValues.value.includes(value);
52
+
53
+ // ── Single-select slider style ──
54
+
55
+ const singleSliderStyle = ref<Record<string, string>>({
56
+ width: "0px",
57
+ transform: "translateX(0px)",
58
+ opacity: "0",
59
+ });
60
+
61
+ function updateSingleSlider() {
62
+ if (props.multiSelect) return;
63
+ const idx = props.options.findIndex((o) => o.value === (props.modelValue as string));
64
+ if (idx < 0 || !buttonRefs.value[idx]) return;
65
+ const btn = buttonRefs.value[idx];
66
+ singleSliderStyle.value = {
67
+ width: `${btn.offsetWidth}px`,
68
+ transform: `translateX(${btn.offsetLeft}px)`,
69
+ opacity: "1",
70
+ };
71
+ }
72
+
73
+ // ── Multi-select slider styles ──
74
+
75
+ const multiSliderStyles = ref<Record<string, Record<string, string>>>({});
76
+
77
+ function updateMultiSliders() {
78
+ if (!props.multiSelect) return;
79
+ const styles: Record<string, Record<string, string>> = {};
80
+ for (const value of activeValues.value) {
81
+ const optionIdx = props.options.findIndex((o) => o.value === value);
82
+ const btn = buttonRefs.value[optionIdx];
83
+ if (!btn) continue;
84
+ styles[value] = {
85
+ width: `${btn.offsetWidth}px`,
86
+ transform: `translateX(${btn.offsetLeft}px)`,
87
+ opacity: "1",
88
+ };
89
+ }
90
+ multiSliderStyles.value = styles;
91
+ }
92
+
93
+ // ── Unified update ──
94
+
95
+ function updateSliders() {
96
+ if (props.multiSelect) {
97
+ updateMultiSliders();
98
+ } else {
99
+ updateSingleSlider();
100
+ }
101
+ }
102
+
103
+ // ── Button press animation (Web Animations API) ──
104
+
105
+ function animatePress(btn: HTMLElement) {
106
+ // Cancel any in-flight press animations on this button
107
+ btn.getAnimations().forEach((a) => a.cancel());
108
+
109
+ btn.animate(
110
+ [
111
+ { transform: "scale(1)" },
112
+ { transform: "scale(0.93)", offset: 0.25 },
113
+ { transform: "scale(1.02)", offset: 0.7 },
114
+ { transform: "scale(1)" },
115
+ ],
116
+ {
117
+ duration: 200,
118
+ easing: "cubic-bezier(0.175, 0.885, 0.32, 1.275)", // --spring-bouncy (Web Animations API needs literal value)
119
+ },
120
+ );
121
+ }
122
+
123
+ // ── Selection handler ──
124
+
125
+ function select(value: string, idx: number) {
126
+ const option = props.options[idx];
127
+ if (option?.disabled) return;
128
+
129
+ const btn = buttonRefs.value[idx];
130
+ if (btn) {
131
+ animatePress(btn);
132
+ }
133
+
134
+ if (props.multiSelect) {
135
+ const current = [...activeValues.value];
136
+ const existingIdx = current.indexOf(value);
137
+ if (existingIdx > -1) {
138
+ // Don't deselect the last remaining value
139
+ if (current.length > 1) {
140
+ current.splice(existingIdx, 1);
141
+ }
142
+ } else {
143
+ current.push(value);
144
+ }
145
+ emit("update:modelValue", current);
146
+ } else {
147
+ emit("update:modelValue", value);
148
+ }
149
+ }
150
+
151
+ // ── Watchers ──
152
+
153
+ watch(() => props.modelValue, () => nextTick(updateSliders), { deep: true });
154
+ watch(() => props.options, () => nextTick(updateSliders), { deep: true });
155
+
156
+ // ── Lifecycle ──
157
+
158
+ let resizeObserver: ResizeObserver | null = null;
159
+
160
+ onMounted(() => {
161
+ nextTick(updateSliders);
162
+ if (containerRef.value) {
163
+ resizeObserver = new ResizeObserver(() => updateSliders());
164
+ resizeObserver.observe(containerRef.value);
165
+ }
166
+ });
167
+
168
+ onUnmounted(() => {
169
+ resizeObserver?.disconnect();
170
+ });
171
+ </script>
172
+
173
+ <template>
174
+ <div
175
+ ref="containerRef"
176
+ :class="cn(
177
+ isPill ? 'bouncy-toggle bouncy-toggle--pill' : 'bouncy-toggle',
178
+ props.class,
179
+ )"
180
+ >
181
+ <!-- Single-select slider -->
182
+ <div
183
+ v-if="!multiSelect"
184
+ :class="isPill ? 'bouncy-slider bouncy-slider--pill' : 'bouncy-slider'"
185
+ :style="singleSliderStyle"
186
+ />
187
+
188
+ <!-- Multi-select sliders (one per active value) -->
189
+ <template v-if="multiSelect">
190
+ <div
191
+ v-for="value in activeValues"
192
+ :key="'slider-' + value"
193
+ :class="isPill ? 'bouncy-slider bouncy-slider--pill' : 'bouncy-slider'"
194
+ :style="multiSliderStyles[value] ?? { opacity: '0' }"
195
+ />
196
+ </template>
197
+
198
+ <!-- Buttons -->
199
+ <template v-for="(option, idx) in options" :key="option.value">
200
+ <!-- With tooltip -->
201
+ <TooltipProvider v-if="option.tooltip" :delay-duration="200">
202
+ <Tooltip>
203
+ <TooltipTrigger as-child>
204
+ <button
205
+ :ref="(el) => { if (el) buttonRefs[idx] = el as HTMLElement }"
206
+ :class="[
207
+ isPill ? 'bouncy-btn bouncy-btn--pill' : 'bouncy-btn',
208
+ { 'is-active': isActive(option.value) },
209
+ option.disabled && 'is-disabled',
210
+ ]"
211
+ :disabled="option.disabled"
212
+ @click="select(option.value, idx)"
213
+ >
214
+ <slot name="option" :option="option" :active="isActive(option.value)">
215
+ {{ option.label }}
216
+ </slot>
217
+ </button>
218
+ </TooltipTrigger>
219
+ <TooltipContent side="bottom" :side-offset="8">
220
+ {{ option.tooltip }}
221
+ </TooltipContent>
222
+ </Tooltip>
223
+ </TooltipProvider>
224
+
225
+ <!-- Without tooltip -->
226
+ <button
227
+ v-else
228
+ :ref="(el) => { if (el) buttonRefs[idx] = el as HTMLElement }"
229
+ :class="[
230
+ isPill ? 'bouncy-btn bouncy-btn--pill' : 'bouncy-btn',
231
+ { 'is-active': isActive(option.value) },
232
+ option.disabled && 'is-disabled',
233
+ ]"
234
+ :disabled="option.disabled"
235
+ @click="select(option.value, idx)"
236
+ >
237
+ <slot name="option" :option="option" :active="isActive(option.value)">
238
+ {{ option.label }}
239
+ </slot>
240
+ </button>
241
+ </template>
242
+ </div>
243
+ </template>
244
+
245
+ <style scoped>
246
+ /* ── Default variant ── */
247
+ .bouncy-toggle {
248
+ position: relative;
249
+ display: inline-grid;
250
+ grid-auto-flow: column;
251
+ grid-auto-columns: 1fr;
252
+ padding: 0.1875rem;
253
+ border-radius: 0.4375rem;
254
+ background: color-mix(in srgb, var(--muted) 50%, transparent);
255
+ }
256
+
257
+ @media (min-width: 640px) {
258
+ .bouncy-toggle {
259
+ padding: 0.25rem;
260
+ border-radius: 0.5rem;
261
+ }
262
+ }
263
+
264
+ .bouncy-slider {
265
+ position: absolute;
266
+ background: var(--background);
267
+ z-index: 0;
268
+ inset-block: 0.1875rem;
269
+ border-radius: 0.3125rem;
270
+ box-shadow:
271
+ 0 1px 3px rgba(0, 0, 0, 0.08),
272
+ 0 0 0 1px color-mix(in srgb, var(--border) 30%, transparent);
273
+ transition:
274
+ transform var(--duration-normal) var(--spring-snappy),
275
+ width var(--duration-normal) var(--spring-snappy),
276
+ opacity var(--duration-fast) ease;
277
+ }
278
+
279
+ @media (min-width: 640px) {
280
+ .bouncy-slider {
281
+ inset-block: 0.25rem;
282
+ border-radius: 0.375rem;
283
+ }
284
+ }
285
+
286
+ .bouncy-btn {
287
+ position: relative;
288
+ z-index: 10;
289
+ border: none;
290
+ background: none;
291
+ font-weight: 500;
292
+ cursor: pointer;
293
+ white-space: nowrap;
294
+ padding: 0.25rem 0.625rem;
295
+ border-radius: 0.3125rem;
296
+ font: inherit;
297
+ font-size: 0.8125rem;
298
+ color: var(--muted-foreground);
299
+ transition: color var(--duration-fast) ease;
300
+ }
301
+
302
+ @media (min-width: 640px) {
303
+ .bouncy-btn {
304
+ padding: 0.3125rem 0.75rem;
305
+ font-size: 0.875rem;
306
+ }
307
+ }
308
+
309
+ .bouncy-btn.is-active {
310
+ color: var(--foreground);
311
+ }
312
+
313
+ .bouncy-btn.is-disabled {
314
+ opacity: 0.4;
315
+ cursor: not-allowed;
316
+ pointer-events: none;
317
+ filter: blur(0.5px);
318
+ }
319
+
320
+ /* ── Pill variant ── */
321
+ .bouncy-toggle--pill {
322
+ border-radius: var(--radius-pill);
323
+ background: color-mix(in srgb, var(--foreground) 5%, transparent);
324
+ padding: 0.125rem;
325
+ gap: 0.125rem;
326
+ }
327
+
328
+ .bouncy-slider--pill {
329
+ border-radius: var(--radius-pill);
330
+ background: var(--foreground);
331
+ box-shadow: none;
332
+ inset-block: 0.125rem;
333
+ }
334
+
335
+ .bouncy-btn--pill {
336
+ border-radius: var(--radius-pill);
337
+ padding: 0.125rem 0.625rem;
338
+ font-size: 0.75rem;
339
+ font-weight: 500;
340
+ }
341
+
342
+ @media (min-width: 640px) {
343
+ .bouncy-btn--pill {
344
+ padding: 0.125rem 0.625rem;
345
+ font-size: 0.75rem;
346
+ }
347
+ }
348
+
349
+ .bouncy-btn--pill.is-active {
350
+ color: var(--background);
351
+ }
352
+ </style>