@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,224 @@
1
+ /**
2
+ * Generic fuzzy search index — builds a flat searchable index from items
3
+ * and scores queries with VSCode-style fuzzy matching.
4
+ * Pure functions, no Vue reactivity.
5
+ */
6
+ import type { SearchableItem, SearchResult } from "./types";
7
+
8
+ // ── Internal index entry ────────────────────────────────────
9
+
10
+ interface IndexEntry<T extends SearchableItem> {
11
+ item: T;
12
+ /** Pre-lowercased fields, built once at index time. */
13
+ _lc: {
14
+ label: string;
15
+ text: string;
16
+ type: string;
17
+ };
18
+ }
19
+
20
+ /** Opaque index handle. */
21
+ export type SearchIndex<T extends SearchableItem = SearchableItem> = IndexEntry<T>[];
22
+
23
+ // ── Fuzzy matching (VSCode-style subsequence scorer) ─────────
24
+
25
+ /**
26
+ * Scores a fuzzy subsequence match of `pattern` against `text`.
27
+ * Every character in `pattern` must appear in `text` in order.
28
+ * Returns null if no match, otherwise { score, matches }.
29
+ *
30
+ * Scoring bonuses:
31
+ * +8 match at index 0 (start of string)
32
+ * +7 match after a word separator (space, -, _, ., /, \, :)
33
+ * +6 camelCase boundary (lowercase -> uppercase)
34
+ * +5 consecutive characters in a run
35
+ * +3 pattern index === text index (prefix alignment)
36
+ * +1 base per-character match
37
+ * -0.1 per excess character in text (prefer tighter matches)
38
+ */
39
+ export function fuzzyMatch(
40
+ pattern: string,
41
+ text: string,
42
+ ): { score: number; matches: number[] } | null {
43
+ const pLen = pattern.length;
44
+ const tLen = text.length;
45
+ if (pLen === 0) return { score: 0, matches: [] };
46
+ if (pLen > tLen) return null;
47
+
48
+ const matches: number[] = [];
49
+ let pi = 0;
50
+ let score = 0;
51
+ let prevIdx = -2;
52
+
53
+ for (let ti = 0; ti < tLen && pi < pLen; ti++) {
54
+ if (pattern[pi] !== text[ti]) continue;
55
+
56
+ matches.push(ti);
57
+ let cs = 1;
58
+
59
+ if (prevIdx === ti - 1) cs += 5;
60
+
61
+ if (ti === 0) {
62
+ cs += 8;
63
+ } else {
64
+ const prev = text[ti - 1];
65
+ if (" -_./\\:()".includes(prev)) {
66
+ cs += 7;
67
+ } else if (
68
+ text[ti] >= "A" &&
69
+ text[ti] <= "Z" &&
70
+ prev >= "a" &&
71
+ prev <= "z"
72
+ ) {
73
+ cs += 6;
74
+ }
75
+ }
76
+
77
+ if (pi === ti) cs += 3;
78
+
79
+ score += cs;
80
+ prevIdx = ti;
81
+ pi++;
82
+ }
83
+
84
+ if (pi < pLen) return null;
85
+
86
+ score -= Math.max(0, (tLen - pLen) * 0.1);
87
+
88
+ return { score, matches };
89
+ }
90
+
91
+ // ── Multi-token fuzzy search ─────────────────────────────────
92
+
93
+ interface TokenMatch {
94
+ score: number;
95
+ matches: number[];
96
+ }
97
+
98
+ /**
99
+ * Matches a query against a single text field. The query is split into
100
+ * whitespace tokens; every token must fuzzy-match independently (AND logic).
101
+ */
102
+ function multiTokenFuzzy(
103
+ queryTokens: string[],
104
+ text: string,
105
+ ): TokenMatch | null {
106
+ if (!text) return null;
107
+ let total = 0;
108
+ const allMatches: number[] = [];
109
+
110
+ for (const token of queryTokens) {
111
+ const m = fuzzyMatch(token, text);
112
+ if (!m) return null;
113
+ total += m.score;
114
+ for (const idx of m.matches) allMatches.push(idx);
115
+ }
116
+
117
+ return { score: total, matches: allMatches };
118
+ }
119
+
120
+ /**
121
+ * Scores one query against one entry. Tries each field with different
122
+ * weights and returns the best composite score.
123
+ */
124
+ function scoreEntry<T extends SearchableItem>(
125
+ tokens: string[],
126
+ entry: IndexEntry<T>,
127
+ ): { score: number; matches: number[] } | null {
128
+ let best = 0;
129
+ let bestMatches: number[] = [];
130
+
131
+ const fields: [string, number][] = [
132
+ [entry._lc.label, 12],
133
+ [entry._lc.type, 10],
134
+ [entry._lc.text, 3],
135
+ ];
136
+
137
+ for (const [text, weight] of fields) {
138
+ if (!text) continue;
139
+ const m = multiTokenFuzzy(tokens, text);
140
+ if (m && m.score * weight > best) {
141
+ best = m.score * weight;
142
+ bestMatches = text === entry._lc.label ? m.matches : [];
143
+ }
144
+ }
145
+
146
+ if (best <= 0) return null;
147
+ return { score: best, matches: bestMatches };
148
+ }
149
+
150
+ // ── Index construction ───────────────────────────────────────
151
+
152
+ /** Build a search index from a list of items. */
153
+ export function buildIndex<T extends SearchableItem>(items: T[]): SearchIndex<T> {
154
+ return items.map((item) => ({
155
+ item,
156
+ _lc: {
157
+ label: item.label.toLowerCase(),
158
+ text: item.text.toLowerCase(),
159
+ type: item.type?.toLowerCase() ?? "",
160
+ },
161
+ }));
162
+ }
163
+
164
+ // ── Search function ──────────────────────────────────────────
165
+
166
+ /** Query result cache — cleared when prefix diverges. */
167
+ let _cache: Map<string, SearchResult<any>[]> = new Map();
168
+
169
+ /** Search the index for items matching `query`. */
170
+ export function searchIndex<T extends SearchableItem>(
171
+ index: SearchIndex<T>,
172
+ query: string,
173
+ maxResults = 30,
174
+ ): SearchResult<T>[] {
175
+ const q = query.toLowerCase().trim();
176
+ if (!q) {
177
+ _cache.clear();
178
+ return [];
179
+ }
180
+
181
+ const cached = _cache.get(q);
182
+ if (cached) return cached;
183
+
184
+ if (_cache.size > 200) _cache.clear();
185
+
186
+ const tokens = q.split(/\s+/).filter(Boolean);
187
+ if (tokens.length === 0) return [];
188
+
189
+ // Narrow from prefix cache if available
190
+ let candidates: IndexEntry<T>[] = index;
191
+ if (q.length > 1) {
192
+ const prefix = q.slice(0, -1);
193
+ const prefixResults = _cache.get(prefix);
194
+ if (prefixResults) {
195
+ // Re-wrap previous results back into IndexEntry form
196
+ candidates = prefixResults.map((r: SearchResult<T>) => ({
197
+ item: r.item,
198
+ _lc: {
199
+ label: r.item.label.toLowerCase(),
200
+ text: r.item.text.toLowerCase(),
201
+ type: r.item.type?.toLowerCase() ?? "",
202
+ },
203
+ }));
204
+ }
205
+ }
206
+
207
+ const scored: SearchResult<T>[] = [];
208
+ for (const entry of candidates) {
209
+ const m = scoreEntry(tokens, entry);
210
+ if (m) {
211
+ scored.push({ item: entry.item, score: m.score, matchIndices: m.matches });
212
+ }
213
+ }
214
+
215
+ scored.sort((a, b) => b.score - a.score);
216
+ const results = scored.slice(0, maxResults);
217
+ _cache.set(q, results);
218
+ return results;
219
+ }
220
+
221
+ /** Clear the search cache (call when index changes). */
222
+ export function clearSearchCache(): void {
223
+ _cache.clear();
224
+ }
@@ -0,0 +1,5 @@
1
+ export { useFuzzySearch } from "./useFuzzySearch";
2
+ export type { UseFuzzySearchOptions } from "./useFuzzySearch";
3
+ export { buildIndex, searchIndex, fuzzyMatch, clearSearchCache } from "./fuzzySearchIndex";
4
+ export type { SearchIndex } from "./fuzzySearchIndex";
5
+ export type { SearchableItem, SearchResult, FuzzySearchState } from "./types";
@@ -0,0 +1,34 @@
1
+ import type { Ref, ComputedRef } from "vue";
2
+
3
+ /** A searchable item — the generic unit of the fuzzy search index. */
4
+ export interface SearchableItem {
5
+ id: string;
6
+ /** Primary display label (title, heading, name). */
7
+ label: string;
8
+ /** Full-text content to search against. */
9
+ text: string;
10
+ /** Optional type tag for badge display (e.g. "section", "code"). */
11
+ type?: string;
12
+ }
13
+
14
+ /** A single fuzzy match result with score and highlight positions. */
15
+ export interface SearchResult<T extends SearchableItem = SearchableItem> {
16
+ item: T;
17
+ score: number;
18
+ /** Character positions in `item.label` that matched the query. */
19
+ matchIndices: number[];
20
+ }
21
+
22
+ /** Reactive state returned by `useFuzzySearch`. */
23
+ export interface FuzzySearchState<T extends SearchableItem = SearchableItem> {
24
+ query: Ref<string>;
25
+ results: ComputedRef<SearchResult<T>[]>;
26
+ selectedIndex: Ref<number>;
27
+ isOpen: Ref<boolean>;
28
+ isExpanded: Ref<boolean>;
29
+ onKeydown(e: KeyboardEvent): void;
30
+ selectResult(result: SearchResult<T>): void;
31
+ toggleExpanded(): void;
32
+ close(): void;
33
+ open(): void;
34
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Vue composable for fuzzy search — manages reactive state, debouncing,
3
+ * keyboard navigation, modal expand, and result selection.
4
+ */
5
+ import { ref, watch, computed } from "vue";
6
+ import type { SearchableItem, SearchResult, FuzzySearchState } from "./types";
7
+ import { buildIndex, searchIndex, clearSearchCache } from "./fuzzySearchIndex";
8
+
9
+ export interface UseFuzzySearchOptions<T extends SearchableItem = SearchableItem> {
10
+ /** Reactive or static list of searchable items. */
11
+ items: T[] | (() => T[]);
12
+ /** Debounce delay in ms (default: 120). */
13
+ debounceMs?: number;
14
+ /** Maximum results to return (default: 30). */
15
+ maxResults?: number;
16
+ /** Callback when a result is selected (Enter or click). */
17
+ onSelect?: (result: SearchResult<T>) => void;
18
+ }
19
+
20
+ export function useFuzzySearch<T extends SearchableItem = SearchableItem>(
21
+ options: UseFuzzySearchOptions<T>,
22
+ ): FuzzySearchState<T> {
23
+ const getItems = typeof options.items === "function" ? options.items : () => options.items as T[];
24
+ const debounceMs = options.debounceMs ?? 120;
25
+ const maxResults = options.maxResults ?? 30;
26
+
27
+ const query = ref("");
28
+ const isOpen = ref(false);
29
+ const isExpanded = ref(false);
30
+ const selectedIndex = ref(0);
31
+ const debouncedQuery = ref("");
32
+
33
+ // Inline debounce
34
+ let debounceTimer: ReturnType<typeof setTimeout> | undefined;
35
+ watch(query, (val) => {
36
+ clearTimeout(debounceTimer);
37
+ debounceTimer = setTimeout(() => {
38
+ debouncedQuery.value = val;
39
+ }, debounceMs);
40
+ });
41
+
42
+ const results = computed(() => {
43
+ const index = buildIndex(getItems());
44
+ return searchIndex<T>(index, debouncedQuery.value, maxResults);
45
+ });
46
+
47
+ watch(results, () => {
48
+ selectedIndex.value = 0;
49
+ });
50
+
51
+ function selectResult(r: SearchResult<T>) {
52
+ options.onSelect?.(r);
53
+ close();
54
+ }
55
+
56
+ function close() {
57
+ isOpen.value = false;
58
+ isExpanded.value = false;
59
+ query.value = "";
60
+ selectedIndex.value = 0;
61
+ clearSearchCache();
62
+ }
63
+
64
+ function open() {
65
+ isOpen.value = true;
66
+ }
67
+
68
+ function toggleExpanded() {
69
+ isExpanded.value = !isExpanded.value;
70
+ }
71
+
72
+ function onKeydown(e: KeyboardEvent) {
73
+ switch (e.key) {
74
+ case "ArrowDown":
75
+ e.preventDefault();
76
+ if (selectedIndex.value < results.value.length - 1) {
77
+ selectedIndex.value++;
78
+ }
79
+ break;
80
+ case "ArrowUp":
81
+ e.preventDefault();
82
+ if (selectedIndex.value > 0) {
83
+ selectedIndex.value--;
84
+ }
85
+ break;
86
+ case "Enter":
87
+ e.preventDefault();
88
+ if (results.value[selectedIndex.value]) {
89
+ selectResult(results.value[selectedIndex.value]);
90
+ }
91
+ break;
92
+ case "Escape":
93
+ e.preventDefault();
94
+ if (isExpanded.value) {
95
+ isExpanded.value = false;
96
+ } else {
97
+ close();
98
+ }
99
+ break;
100
+ }
101
+ }
102
+
103
+ return {
104
+ query,
105
+ results,
106
+ isOpen,
107
+ isExpanded,
108
+ selectedIndex,
109
+ selectResult,
110
+ close,
111
+ open,
112
+ toggleExpanded,
113
+ onKeydown,
114
+ } as FuzzySearchState<T>;
115
+ }
@@ -0,0 +1,7 @@
1
+ export { default as FuzzySearch } from "./FuzzySearch.vue";
2
+ export { default as SearchBar } from "./SearchBar.vue";
3
+ export { useFuzzySearch } from "./composables/useFuzzySearch";
4
+ export type { UseFuzzySearchOptions } from "./composables/useFuzzySearch";
5
+ export { buildIndex, searchIndex, fuzzyMatch, clearSearchCache } from "./composables/fuzzySearchIndex";
6
+ export type { SearchIndex } from "./composables/fuzzySearchIndex";
7
+ export type { SearchableItem, SearchResult, FuzzySearchState } from "./composables/types";
@@ -0,0 +1,256 @@
1
+ <script setup lang="ts">
2
+ import { ref } from "vue";
3
+ import { ChevronUp } from "lucide-vue-next";
4
+ import type { SidebarState, SidebarSection } from "./types";
5
+
6
+ const props = withDefaults(
7
+ defineProps<{
8
+ state: SidebarState;
9
+ mode?: "sticky" | "drawer";
10
+ renderTitle?: (title: string) => string;
11
+ }>(),
12
+ {
13
+ mode: "sticky",
14
+ renderTitle: (title: string) => title,
15
+ },
16
+ );
17
+
18
+ defineSlots<{
19
+ search?: () => any;
20
+ }>();
21
+
22
+ const sidebarNav = ref<HTMLElement | null>(null);
23
+ defineExpose({ sidebarNav });
24
+ </script>
25
+
26
+ <template>
27
+ <aside class="progressive-sidebar" :class="`progressive-sidebar--${mode}`">
28
+ <nav ref="sidebarNav" class="sidebar-nav scrollbar-thin">
29
+ <slot name="search" />
30
+ <div class="sidebar-header">
31
+ <p class="sidebar-label">Contents</p>
32
+ <button
33
+ class="sidebar-top-btn"
34
+ @click="state.scrollToTop()"
35
+ title="Scroll to top"
36
+ >
37
+ <ChevronUp class="h-3 w-3" />
38
+ </button>
39
+ </div>
40
+ <ol class="sidebar-list">
41
+ <li v-for="(section, si) in state.sections" :key="section.id">
42
+ <button
43
+ :data-toc-id="section.id"
44
+ @click="state.toggleSection(section.id)"
45
+ class="sidebar-link"
46
+ :class="{ 'is-active': state.activeRootId.value === section.id }"
47
+ >
48
+ <span v-html="renderTitle(section.title)" />
49
+ </button>
50
+ <!-- Subsections (animated expand) -->
51
+ <div
52
+ v-if="section.children"
53
+ class="sidebar-sublist-wrapper"
54
+ :class="{ 'is-expanded': state.isExpanded(section.id) }"
55
+ >
56
+ <ol class="sidebar-sublist">
57
+ <li v-for="sub in section.children" :key="sub.id">
58
+ <button
59
+ :data-toc-id="sub.id"
60
+ @click="state.navigateTo(sub.id)"
61
+ class="sidebar-link sidebar-sublink"
62
+ :class="{ 'is-active-sub': state.isActive(sub.id) || state.isInActiveChain(sub.id) }"
63
+ >
64
+ <span v-html="renderTitle(sub.title)" />
65
+ </button>
66
+ <!-- Sub-subsections -->
67
+ <ol
68
+ v-if="sub.children && state.isInActiveChain(sub.id)"
69
+ class="sidebar-subsublist"
70
+ >
71
+ <li v-for="subsub in sub.children" :key="subsub.id">
72
+ <button
73
+ :data-toc-id="subsub.id"
74
+ @click="state.navigateTo(subsub.id)"
75
+ class="sidebar-link sidebar-subsublink"
76
+ :class="{ 'is-active-sub': state.isActive(subsub.id) }"
77
+ >
78
+ <span v-html="renderTitle(subsub.title)" />
79
+ </button>
80
+ </li>
81
+ </ol>
82
+ </li>
83
+ </ol>
84
+ </div>
85
+ </li>
86
+ </ol>
87
+ </nav>
88
+ </aside>
89
+ </template>
90
+
91
+ <style scoped>
92
+ .progressive-sidebar {
93
+ --sidebar-top-inset: 1rem;
94
+ --sidebar-bottom-inset: 1.5rem;
95
+ }
96
+
97
+ .progressive-sidebar--sticky {
98
+ display: none;
99
+ }
100
+
101
+ @media (min-width: 1024px) {
102
+ .progressive-sidebar--sticky {
103
+ display: block;
104
+ position: sticky;
105
+ top: var(--sidebar-top-inset);
106
+ align-self: start;
107
+ min-height: 0;
108
+ max-height: calc(100dvh - var(--sidebar-top-inset) - var(--sidebar-bottom-inset));
109
+ }
110
+ }
111
+
112
+ .sidebar-nav {
113
+ overflow-y: auto;
114
+ overscroll-behavior: contain;
115
+ border-radius: var(--radius-xl);
116
+ background: var(--card);
117
+ max-height: calc(100dvh - var(--sidebar-top-inset) - var(--sidebar-bottom-inset));
118
+ scrollbar-gutter: stable;
119
+ scroll-padding-bottom: var(--sidebar-bottom-inset);
120
+ touch-action: pan-y;
121
+ padding: 0.625rem 0.625rem var(--sidebar-bottom-inset);
122
+ border: 2px solid color-mix(in srgb, var(--foreground) 15%, transparent);
123
+ }
124
+
125
+ .sidebar-header {
126
+ display: flex;
127
+ align-items: center;
128
+ justify-content: space-between;
129
+ margin-bottom: 0.5rem;
130
+ padding: 0 0.625rem;
131
+ }
132
+
133
+ .sidebar-label {
134
+ font-size: var(--type-small);
135
+ font-weight: 700;
136
+ text-transform: uppercase;
137
+ margin: 0;
138
+ letter-spacing: 0.08em;
139
+ color: color-mix(in srgb, var(--muted-foreground) 60%, transparent);
140
+ }
141
+
142
+ .sidebar-top-btn {
143
+ flex-shrink: 0;
144
+ display: flex;
145
+ align-items: center;
146
+ justify-content: center;
147
+ cursor: pointer;
148
+ width: 1.25rem;
149
+ height: 1.25rem;
150
+ border-radius: var(--radius-sm);
151
+ border: 1px solid color-mix(in srgb, var(--border) 40%, transparent);
152
+ background: none;
153
+ color: color-mix(in srgb, var(--muted-foreground) 45%, transparent);
154
+ transition: all var(--duration-fast) var(--ease-standard);
155
+ }
156
+
157
+ .sidebar-top-btn:hover {
158
+ color: var(--foreground);
159
+ border-color: var(--border);
160
+ background: color-mix(in srgb, var(--muted) 50%, transparent);
161
+ }
162
+
163
+ .sidebar-list {
164
+ list-style: none;
165
+ padding: 0;
166
+ margin: 0;
167
+ display: flex;
168
+ flex-direction: column;
169
+ gap: 1px;
170
+ }
171
+
172
+ .sidebar-link {
173
+ display: block;
174
+ width: 100%;
175
+ text-align: left;
176
+ background: none;
177
+ border: none;
178
+ cursor: pointer;
179
+ font-weight: 500;
180
+ font-size: 1rem;
181
+ line-height: 1.35;
182
+ padding: 0.28rem 0.625rem;
183
+ border-radius: var(--radius-md);
184
+ color: var(--muted-foreground);
185
+ transition:
186
+ color var(--duration-normal) var(--ease-out-expo),
187
+ background-color var(--duration-normal) var(--ease-out-expo),
188
+ font-weight var(--duration-fast) var(--ease-standard);
189
+ }
190
+
191
+ .sidebar-link:hover {
192
+ color: var(--foreground);
193
+ background: color-mix(in srgb, var(--muted) 50%, transparent);
194
+ }
195
+
196
+ .sidebar-link.is-active {
197
+ background: none;
198
+ font-weight: 600;
199
+ color: var(--primary);
200
+ }
201
+
202
+ .sidebar-link.is-active-sub {
203
+ font-weight: 600;
204
+ background: color-mix(in srgb, var(--muted) 40%, transparent);
205
+ color: var(--primary);
206
+ }
207
+
208
+ /* Animated subsection expand/collapse */
209
+ .sidebar-sublist-wrapper {
210
+ display: grid;
211
+ opacity: 0;
212
+ grid-template-rows: 0fr;
213
+ transition:
214
+ grid-template-rows 0.4s var(--ease-out-expo),
215
+ opacity 0.3s var(--ease-out-expo);
216
+ }
217
+
218
+ .sidebar-sublist-wrapper.is-expanded {
219
+ opacity: 1;
220
+ grid-template-rows: 1fr;
221
+ }
222
+
223
+ .sidebar-sublist-wrapper > .sidebar-sublist {
224
+ overflow: hidden;
225
+ }
226
+
227
+ .sidebar-sublist {
228
+ list-style: none;
229
+ padding: 0 0 0 0.625rem;
230
+ margin: 0.0625rem 0 0.125rem;
231
+ }
232
+
233
+ .sidebar-sublink {
234
+ font-size: 0.78rem;
235
+ padding: 0.2rem 0.45rem;
236
+ }
237
+
238
+ .sidebar-subsublist {
239
+ list-style: none;
240
+ padding: 0 0 0 0.5rem;
241
+ margin: 0.03125rem 0 0.0625rem;
242
+ }
243
+
244
+ .sidebar-subsublink {
245
+ font-size: 0.72rem;
246
+ padding: 0.15rem 0.32rem;
247
+ }
248
+
249
+ /* Drawer mode */
250
+ .progressive-sidebar--drawer .sidebar-nav {
251
+ max-height: 100%;
252
+ border: none;
253
+ border-radius: 0;
254
+ background: transparent;
255
+ }
256
+ </style>
@@ -0,0 +1,6 @@
1
+ export { useScrollTracker } from "./useScrollTracker";
2
+ export { useSidebarFollow } from "./useSidebarFollow";
3
+ export type { SidebarFollowOptions } from "./useSidebarFollow";
4
+ export { useSidebarState } from "./useSidebarState";
5
+ export type { UseSidebarStateOptions } from "./useSidebarState";
6
+ export { useTreeIndex, buildTreeIndex, isActive, isInActiveChain } from "./useTreeIndex";