fancy-ui-svelte 0.11.0 → 0.12.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 (539) hide show
  1. package/README.md +4 -4
  2. package/dist/fancy-ui/_internals/ScrollLockHarness.test.svelte +29 -0
  3. package/dist/fancy-ui/_internals/ScrollLockHarness.test.svelte.d.ts +3 -0
  4. package/dist/fancy-ui/_internals/ScrollLockPanel.test.svelte +43 -0
  5. package/dist/fancy-ui/_internals/ScrollLockPanel.test.svelte.d.ts +8 -0
  6. package/dist/fancy-ui/_internals/anchor-position.d.ts +18 -7
  7. package/dist/fancy-ui/_internals/anchor-position.js +45 -10
  8. package/dist/fancy-ui/_internals/anchor-position.test.js +48 -1
  9. package/dist/fancy-ui/_internals/dismissable.d.ts +16 -0
  10. package/dist/fancy-ui/_internals/dismissable.js +23 -6
  11. package/dist/fancy-ui/_internals/dismissable.test.js +115 -6
  12. package/dist/fancy-ui/_internals/focus-trap.d.ts +36 -0
  13. package/dist/fancy-ui/_internals/focus-trap.js +106 -39
  14. package/dist/fancy-ui/_internals/focus-trap.test.js +356 -1
  15. package/dist/fancy-ui/_internals/motion/AnchoredHarness.test.svelte +42 -0
  16. package/dist/fancy-ui/_internals/motion/AnchoredHarness.test.svelte.d.ts +11 -0
  17. package/dist/fancy-ui/_internals/motion/TransitionsHarness.test.svelte +32 -0
  18. package/dist/fancy-ui/_internals/motion/TransitionsHarness.test.svelte.d.ts +10 -0
  19. package/dist/fancy-ui/_internals/motion/anchored.d.ts +104 -0
  20. package/dist/fancy-ui/_internals/motion/anchored.js +141 -0
  21. package/dist/fancy-ui/_internals/motion/anchored.test.d.ts +1 -0
  22. package/dist/fancy-ui/_internals/motion/anchored.test.js +250 -0
  23. package/dist/fancy-ui/_internals/motion/haptics.d.ts +38 -0
  24. package/dist/fancy-ui/_internals/motion/haptics.js +59 -0
  25. package/dist/fancy-ui/_internals/motion/haptics.test.d.ts +1 -0
  26. package/dist/fancy-ui/_internals/motion/haptics.test.js +70 -0
  27. package/dist/fancy-ui/_internals/motion/in-view.d.ts +24 -0
  28. package/dist/fancy-ui/_internals/motion/in-view.js +99 -0
  29. package/dist/fancy-ui/_internals/motion/in-view.test.d.ts +1 -0
  30. package/dist/fancy-ui/_internals/motion/in-view.test.js +149 -0
  31. package/dist/fancy-ui/_internals/motion/media-query.svelte.d.ts +26 -0
  32. package/dist/fancy-ui/_internals/motion/media-query.svelte.js +69 -0
  33. package/dist/fancy-ui/_internals/motion/media-query.test.d.ts +1 -0
  34. package/dist/fancy-ui/_internals/motion/media-query.test.js +175 -0
  35. package/dist/fancy-ui/_internals/motion/presets.d.ts +34 -0
  36. package/dist/fancy-ui/_internals/motion/presets.js +33 -0
  37. package/dist/fancy-ui/_internals/motion/presets.test.d.ts +1 -0
  38. package/dist/fancy-ui/_internals/motion/presets.test.js +48 -0
  39. package/dist/fancy-ui/_internals/motion/raf.d.ts +35 -0
  40. package/dist/fancy-ui/_internals/motion/raf.js +54 -0
  41. package/dist/fancy-ui/_internals/motion/raf.test.d.ts +1 -0
  42. package/dist/fancy-ui/_internals/motion/raf.test.js +104 -0
  43. package/dist/fancy-ui/_internals/motion/stagger.d.ts +32 -0
  44. package/dist/fancy-ui/_internals/motion/stagger.js +51 -0
  45. package/dist/fancy-ui/_internals/motion/stagger.test.d.ts +1 -0
  46. package/dist/fancy-ui/_internals/motion/stagger.test.js +58 -0
  47. package/dist/fancy-ui/_internals/motion/tokens.d.ts +97 -0
  48. package/dist/fancy-ui/_internals/motion/tokens.js +83 -0
  49. package/dist/fancy-ui/_internals/motion/tokens.test.d.ts +1 -0
  50. package/dist/fancy-ui/_internals/motion/tokens.test.js +28 -0
  51. package/dist/fancy-ui/_internals/motion/transitions.d.ts +52 -0
  52. package/dist/fancy-ui/_internals/motion/transitions.js +91 -0
  53. package/dist/fancy-ui/_internals/motion/transitions.test.d.ts +1 -0
  54. package/dist/fancy-ui/_internals/motion/transitions.test.js +143 -0
  55. package/dist/fancy-ui/_internals/motion/types.d.ts +17 -0
  56. package/dist/fancy-ui/_internals/motion/types.js +15 -0
  57. package/dist/fancy-ui/_internals/motion/types.test.d.ts +1 -0
  58. package/dist/fancy-ui/_internals/motion/types.test.js +19 -0
  59. package/dist/fancy-ui/_internals/scroll-lock.d.ts +21 -0
  60. package/dist/fancy-ui/_internals/scroll-lock.js +20 -0
  61. package/dist/fancy-ui/_internals/scroll-lock.test.js +82 -1
  62. package/dist/fancy-ui/agent-plan/AgentPlan.svelte +16 -1
  63. package/dist/fancy-ui/agent-plan/AgentPlan.svelte.d.ts +5 -0
  64. package/dist/fancy-ui/agent-plan/AgentPlan.test.js +55 -1
  65. package/dist/fancy-ui/agent-plan/README.md +11 -0
  66. package/dist/fancy-ui/alert-dialog/AlertDialog.svelte +20 -0
  67. package/dist/fancy-ui/alert-dialog/AlertDialog.svelte.d.ts +5 -0
  68. package/dist/fancy-ui/alert-dialog/AlertDialog.test.js +145 -2
  69. package/dist/fancy-ui/alert-dialog/README.md +45 -14
  70. package/dist/fancy-ui/animated-beam/AnimatedBeam.svelte +4 -0
  71. package/dist/fancy-ui/animated-beam/AnimatedBeam.test.js +16 -0
  72. package/dist/fancy-ui/animated-beam/README.md +1 -0
  73. package/dist/fancy-ui/animated-testimonials/AnimatedTestimonials.svelte +13 -3
  74. package/dist/fancy-ui/animated-testimonials/AnimatedTestimonials.svelte.d.ts +5 -0
  75. package/dist/fancy-ui/animated-testimonials/AnimatedTestimonials.test.js +70 -0
  76. package/dist/fancy-ui/animated-testimonials/README.md +17 -6
  77. package/dist/fancy-ui/animated-tooltip/AnimatedTooltip.svelte +21 -1
  78. package/dist/fancy-ui/animated-tooltip/AnimatedTooltip.test.js +25 -1
  79. package/dist/fancy-ui/animated-tooltip/README.md +1 -0
  80. package/dist/fancy-ui/apple-card-carousel/AppleCard.svelte +20 -0
  81. package/dist/fancy-ui/apple-card-carousel/AppleCard.svelte.d.ts +5 -0
  82. package/dist/fancy-ui/apple-card-carousel/AppleCardCarousel.svelte +7 -1
  83. package/dist/fancy-ui/apple-card-carousel/AppleCardCarousel.svelte.d.ts +5 -0
  84. package/dist/fancy-ui/apple-card-carousel/AppleCardCarousel.test.js +111 -1
  85. package/dist/fancy-ui/apple-card-carousel/README.md +16 -5
  86. package/dist/fancy-ui/approval-card/ApprovalCard.svelte +10 -0
  87. package/dist/fancy-ui/approval-card/ApprovalCard.svelte.d.ts +5 -0
  88. package/dist/fancy-ui/approval-card/ApprovalCard.test.js +51 -1
  89. package/dist/fancy-ui/approval-card/README.md +30 -14
  90. package/dist/fancy-ui/artifact-card/ArtifactCard.svelte +19 -1
  91. package/dist/fancy-ui/artifact-card/ArtifactCard.svelte.d.ts +5 -0
  92. package/dist/fancy-ui/artifact-card/ArtifactCard.test.js +80 -1
  93. package/dist/fancy-ui/artifact-card/README.md +24 -13
  94. package/dist/fancy-ui/autocomplete/Autocomplete.svelte +16 -0
  95. package/dist/fancy-ui/autocomplete/Autocomplete.svelte.d.ts +5 -0
  96. package/dist/fancy-ui/autocomplete/Autocomplete.test.js +245 -1
  97. package/dist/fancy-ui/autocomplete/AutocompletePanel.svelte +60 -3
  98. package/dist/fancy-ui/autocomplete/AutocompletePanel.svelte.d.ts +2 -17
  99. package/dist/fancy-ui/autocomplete/README.md +74 -17
  100. package/dist/fancy-ui/autocomplete/types.d.ts +8 -0
  101. package/dist/fancy-ui/breadcrumb/Breadcrumb.svelte +16 -0
  102. package/dist/fancy-ui/breadcrumb/Breadcrumb.svelte.d.ts +6 -0
  103. package/dist/fancy-ui/breadcrumb/Breadcrumb.test.js +43 -2
  104. package/dist/fancy-ui/breadcrumb/README.md +11 -0
  105. package/dist/fancy-ui/chat-error/ChatError.svelte +19 -1
  106. package/dist/fancy-ui/chat-error/ChatError.svelte.d.ts +6 -0
  107. package/dist/fancy-ui/chat-error/ChatError.test.js +40 -0
  108. package/dist/fancy-ui/chat-error/README.md +11 -0
  109. package/dist/fancy-ui/chat-message/ChatMessage.svelte +13 -0
  110. package/dist/fancy-ui/chat-message/ChatMessage.svelte.d.ts +9 -0
  111. package/dist/fancy-ui/chat-message/ChatMessageAction.svelte +10 -0
  112. package/dist/fancy-ui/chat-message/ChatMessageBranches.svelte +14 -3
  113. package/dist/fancy-ui/chat-message/ChatMessageHarness.test.svelte +3 -1
  114. package/dist/fancy-ui/chat-message/ChatMessageHarness.test.svelte.d.ts +1 -0
  115. package/dist/fancy-ui/chat-message/ChatMessageParts.test.js +68 -0
  116. package/dist/fancy-ui/chat-message/README.md +25 -0
  117. package/dist/fancy-ui/chat-message/types.d.ts +7 -0
  118. package/dist/fancy-ui/chat-panel/ChatPanel.svelte +9 -0
  119. package/dist/fancy-ui/chat-panel/ChatPanel.svelte.d.ts +6 -0
  120. package/dist/fancy-ui/chat-panel/ChatPanel.test.js +46 -0
  121. package/dist/fancy-ui/chat-panel/README.md +15 -0
  122. package/dist/fancy-ui/checkbox/Checkbox.svelte +139 -42
  123. package/dist/fancy-ui/checkbox/Checkbox.test.js +108 -0
  124. package/dist/fancy-ui/checkbox/README.md +70 -11
  125. package/dist/fancy-ui/code-diff/CodeDiff.svelte +23 -2
  126. package/dist/fancy-ui/code-diff/CodeDiff.svelte.d.ts +5 -0
  127. package/dist/fancy-ui/code-diff/CodeDiff.test.js +61 -1
  128. package/dist/fancy-ui/code-diff/README.md +21 -10
  129. package/dist/fancy-ui/combobox/Combobox.svelte +16 -0
  130. package/dist/fancy-ui/combobox/Combobox.svelte.d.ts +5 -0
  131. package/dist/fancy-ui/combobox/Combobox.test.js +210 -0
  132. package/dist/fancy-ui/combobox/ComboboxPanel.svelte +57 -3
  133. package/dist/fancy-ui/combobox/ComboboxPanel.svelte.d.ts +2 -17
  134. package/dist/fancy-ui/combobox/README.md +52 -0
  135. package/dist/fancy-ui/combobox/types.d.ts +8 -0
  136. package/dist/fancy-ui/command-menu/CommandMenu.svelte +122 -47
  137. package/dist/fancy-ui/command-menu/CommandMenu.svelte.d.ts +5 -0
  138. package/dist/fancy-ui/command-menu/CommandMenu.test.js +242 -4
  139. package/dist/fancy-ui/command-menu/README.md +43 -2
  140. package/dist/fancy-ui/composer/Composer.svelte +14 -0
  141. package/dist/fancy-ui/composer/Composer.svelte.d.ts +5 -0
  142. package/dist/fancy-ui/composer/Composer.test.js +107 -1
  143. package/dist/fancy-ui/composer/ComposerAttachment.svelte +7 -0
  144. package/dist/fancy-ui/composer/ComposerAttachments.test.js +49 -1
  145. package/dist/fancy-ui/composer/ComposerCommandMenu.svelte +5 -0
  146. package/dist/fancy-ui/composer/ComposerCommandMenu.test.js +57 -2
  147. package/dist/fancy-ui/composer/ComposerHarness.test.svelte +3 -0
  148. package/dist/fancy-ui/composer/ComposerHarness.test.svelte.d.ts +1 -0
  149. package/dist/fancy-ui/composer/ComposerModelPicker.svelte +14 -2
  150. package/dist/fancy-ui/composer/ComposerToolbar.test.js +64 -2
  151. package/dist/fancy-ui/composer/README.md +31 -0
  152. package/dist/fancy-ui/composer/types.d.ts +6 -0
  153. package/dist/fancy-ui/confetti/Confetti.test.js +55 -11
  154. package/dist/fancy-ui/confetti/ConfettiButton.svelte +8 -1
  155. package/dist/fancy-ui/confetti/ConfettiButton.svelte.d.ts +5 -0
  156. package/dist/fancy-ui/confetti/README.md +22 -9
  157. package/dist/fancy-ui/context-menu/ContextMenu.svelte +27 -3
  158. package/dist/fancy-ui/context-menu/ContextMenu.svelte.d.ts +5 -0
  159. package/dist/fancy-ui/context-menu/ContextMenu.test.js +324 -0
  160. package/dist/fancy-ui/context-menu/ContextMenuContent.svelte +77 -22
  161. package/dist/fancy-ui/context-menu/ContextMenuHarness.test.svelte +3 -1
  162. package/dist/fancy-ui/context-menu/ContextMenuHarness.test.svelte.d.ts +1 -0
  163. package/dist/fancy-ui/context-menu/README.md +86 -2
  164. package/dist/fancy-ui/context-menu/types.d.ts +5 -3
  165. package/dist/fancy-ui/context-ring/ContextRing.svelte +24 -3
  166. package/dist/fancy-ui/context-ring/ContextRing.svelte.d.ts +5 -0
  167. package/dist/fancy-ui/context-ring/ContextRing.test.js +75 -1
  168. package/dist/fancy-ui/context-ring/README.md +11 -0
  169. package/dist/fancy-ui/copy-button/CopyButton.svelte +171 -52
  170. package/dist/fancy-ui/copy-button/CopyButton.svelte.d.ts +6 -4
  171. package/dist/fancy-ui/copy-button/CopyButton.test.js +237 -16
  172. package/dist/fancy-ui/copy-button/README.md +111 -44
  173. package/dist/fancy-ui/date-picker/DatePicker.svelte +74 -22
  174. package/dist/fancy-ui/date-picker/DatePicker.svelte.d.ts +5 -0
  175. package/dist/fancy-ui/date-picker/DatePicker.test.js +230 -0
  176. package/dist/fancy-ui/date-picker/README.md +50 -0
  177. package/dist/fancy-ui/dialog/Dialog.svelte +16 -0
  178. package/dist/fancy-ui/dialog/Dialog.svelte.d.ts +5 -0
  179. package/dist/fancy-ui/dialog/Dialog.test.js +234 -3
  180. package/dist/fancy-ui/dialog/DialogSurface.svelte +101 -48
  181. package/dist/fancy-ui/dialog/README.md +43 -16
  182. package/dist/fancy-ui/dim-siblings/DimSiblings.svelte +184 -0
  183. package/dist/fancy-ui/dim-siblings/DimSiblings.svelte.d.ts +35 -0
  184. package/dist/fancy-ui/dim-siblings/DimSiblings.test.d.ts +1 -0
  185. package/dist/fancy-ui/dim-siblings/DimSiblings.test.js +127 -0
  186. package/dist/fancy-ui/dim-siblings/DimSiblingsHarness.test.svelte +18 -0
  187. package/dist/fancy-ui/dim-siblings/DimSiblingsHarness.test.svelte.d.ts +18 -0
  188. package/dist/fancy-ui/dim-siblings/README.md +77 -0
  189. package/dist/fancy-ui/dim-siblings/index.d.ts +3 -0
  190. package/dist/fancy-ui/dim-siblings/index.js +2 -0
  191. package/dist/fancy-ui/displacement-text/DisplacementText.svelte +6 -1
  192. package/dist/fancy-ui/displacement-text/DisplacementText.test.js +5 -1
  193. package/dist/fancy-ui/displacement-text/README.md +1 -0
  194. package/dist/fancy-ui/dock/Dock.svelte +44 -4
  195. package/dist/fancy-ui/dock/Dock.test.js +133 -2
  196. package/dist/fancy-ui/dock/DockHarness.test.svelte +36 -0
  197. package/dist/fancy-ui/dock/DockHarness.test.svelte.d.ts +8 -0
  198. package/dist/fancy-ui/dock/DockIcon.svelte +6 -0
  199. package/dist/fancy-ui/dock/README.md +49 -22
  200. package/dist/fancy-ui/dock/types.d.ts +8 -0
  201. package/dist/fancy-ui/drawer/Drawer.svelte +176 -50
  202. package/dist/fancy-ui/drawer/Drawer.svelte.d.ts +12 -0
  203. package/dist/fancy-ui/drawer/Drawer.test.js +228 -10
  204. package/dist/fancy-ui/drawer/DrawerHarness.test.svelte +6 -2
  205. package/dist/fancy-ui/drawer/DrawerHarness.test.svelte.d.ts +1 -0
  206. package/dist/fancy-ui/drawer/README.md +70 -26
  207. package/dist/fancy-ui/dropdown-menu/DropdownMenu.test.js +295 -8
  208. package/dist/fancy-ui/dropdown-menu/DropdownMenuContent.svelte +67 -21
  209. package/dist/fancy-ui/dropdown-menu/DropdownMenuSub.svelte +14 -3
  210. package/dist/fancy-ui/dropdown-menu/DropdownMenuSubContent.svelte +68 -20
  211. package/dist/fancy-ui/dropdown-menu/DropdownMenuSubTrigger.svelte +1 -0
  212. package/dist/fancy-ui/dropdown-menu/README.md +80 -5
  213. package/dist/fancy-ui/dropdown-menu/types.d.ts +26 -5
  214. package/dist/fancy-ui/editorial-engine/EditorialEngine.svelte +37 -15
  215. package/dist/fancy-ui/editorial-engine/EditorialEngine.test.js +34 -2
  216. package/dist/fancy-ui/editorial-engine/README.md +9 -0
  217. package/dist/fancy-ui/file-upload/FileUpload.svelte +137 -15
  218. package/dist/fancy-ui/file-upload/FileUpload.svelte.d.ts +5 -0
  219. package/dist/fancy-ui/file-upload/FileUpload.test.js +302 -1
  220. package/dist/fancy-ui/file-upload/README.md +69 -5
  221. package/dist/fancy-ui/fireworks-hdr/FireworksHdr.svelte +12 -2
  222. package/dist/fancy-ui/fireworks-hdr/FireworksHdr.test.js +70 -0
  223. package/dist/fancy-ui/fireworks-hdr/README.md +4 -2
  224. package/dist/fancy-ui/form-field/FormField.svelte +38 -2
  225. package/dist/fancy-ui/form-field/FormField.test.js +118 -1
  226. package/dist/fancy-ui/form-field/README.md +26 -0
  227. package/dist/fancy-ui/gradient-button/GradientButton.svelte +14 -0
  228. package/dist/fancy-ui/gradient-button/GradientButton.svelte.d.ts +5 -0
  229. package/dist/fancy-ui/gradient-button/GradientButton.test.js +40 -2
  230. package/dist/fancy-ui/gradient-button/README.md +10 -0
  231. package/dist/fancy-ui/hover-card/HoverCard.svelte +76 -29
  232. package/dist/fancy-ui/hover-card/HoverCard.test.js +227 -8
  233. package/dist/fancy-ui/hover-card/README.md +47 -1
  234. package/dist/fancy-ui/icon-button/IconButton.svelte +7 -0
  235. package/dist/fancy-ui/icon-button/IconButton.svelte.d.ts +5 -0
  236. package/dist/fancy-ui/icon-button/IconButton.test.js +40 -1
  237. package/dist/fancy-ui/icon-button/README.md +13 -0
  238. package/dist/fancy-ui/image-generation/ImageGeneration.svelte +18 -1
  239. package/dist/fancy-ui/image-generation/ImageGeneration.svelte.d.ts +6 -0
  240. package/dist/fancy-ui/image-generation/ImageGeneration.test.js +46 -0
  241. package/dist/fancy-ui/image-generation/README.md +11 -0
  242. package/dist/fancy-ui/index.d.ts +13 -0
  243. package/dist/fancy-ui/index.js +14 -0
  244. package/dist/fancy-ui/interactive-hover-button/InteractiveHoverButton.svelte +26 -3
  245. package/dist/fancy-ui/interactive-hover-button/InteractiveHoverButton.svelte.d.ts +5 -0
  246. package/dist/fancy-ui/interactive-hover-button/InteractiveHoverButton.test.js +69 -2
  247. package/dist/fancy-ui/interactive-hover-button/README.md +30 -5
  248. package/dist/fancy-ui/line-hover-link/LineHoverLink.svelte +181 -51
  249. package/dist/fancy-ui/line-hover-link/LineHoverLink.svelte.d.ts +5 -0
  250. package/dist/fancy-ui/line-hover-link/LineHoverLink.test.js +69 -2
  251. package/dist/fancy-ui/line-hover-link/README.md +42 -8
  252. package/dist/fancy-ui/line-reveal/LineReveal.svelte +10 -6
  253. package/dist/fancy-ui/line-reveal/LineReveal.test.js +20 -16
  254. package/dist/fancy-ui/link/Link.svelte +54 -1
  255. package/dist/fancy-ui/link/Link.svelte.d.ts +5 -0
  256. package/dist/fancy-ui/link/Link.test.js +67 -1
  257. package/dist/fancy-ui/link/README.md +35 -0
  258. package/dist/fancy-ui/magnetic/Magnetic.svelte +337 -0
  259. package/dist/fancy-ui/magnetic/Magnetic.svelte.d.ts +40 -0
  260. package/dist/fancy-ui/magnetic/Magnetic.test.d.ts +1 -0
  261. package/dist/fancy-ui/magnetic/Magnetic.test.js +354 -0
  262. package/dist/fancy-ui/magnetic/README.md +175 -0
  263. package/dist/fancy-ui/magnetic/index.d.ts +2 -0
  264. package/dist/fancy-ui/magnetic/index.js +2 -0
  265. package/dist/fancy-ui/mosaic-glow/MosaicGlow.ssr.test.d.ts +1 -0
  266. package/dist/fancy-ui/mosaic-glow/MosaicGlow.ssr.test.js +22 -0
  267. package/dist/fancy-ui/mosaic-glow/MosaicGlow.svelte +528 -0
  268. package/dist/fancy-ui/mosaic-glow/MosaicGlow.svelte.d.ts +53 -0
  269. package/dist/fancy-ui/mosaic-glow/MosaicGlow.test.d.ts +1 -0
  270. package/dist/fancy-ui/mosaic-glow/MosaicGlow.test.js +319 -0
  271. package/dist/fancy-ui/mosaic-glow/README.md +60 -0
  272. package/dist/fancy-ui/mosaic-glow/index.d.ts +2 -0
  273. package/dist/fancy-ui/mosaic-glow/index.js +2 -0
  274. package/dist/fancy-ui/mosaic-glow/mosaic-glow-core.d.ts +87 -0
  275. package/dist/fancy-ui/mosaic-glow/mosaic-glow-core.js +221 -0
  276. package/dist/fancy-ui/mosaic-glow/mosaic-glow-core.test.d.ts +1 -0
  277. package/dist/fancy-ui/mosaic-glow/mosaic-glow-core.test.js +201 -0
  278. package/dist/fancy-ui/navbar/Navbar.test.js +76 -1
  279. package/dist/fancy-ui/navbar/NavbarLink.svelte +55 -1
  280. package/dist/fancy-ui/navbar/NavbarLink.svelte.d.ts +5 -0
  281. package/dist/fancy-ui/navbar/README.md +36 -0
  282. package/dist/fancy-ui/navigation-menu/NavigationMenu.test.js +151 -12
  283. package/dist/fancy-ui/navigation-menu/NavigationMenuContent.svelte +74 -26
  284. package/dist/fancy-ui/navigation-menu/README.md +58 -0
  285. package/dist/fancy-ui/number-input/NumberInput.svelte +110 -3
  286. package/dist/fancy-ui/number-input/NumberInput.svelte.d.ts +5 -0
  287. package/dist/fancy-ui/number-input/NumberInput.test.js +234 -0
  288. package/dist/fancy-ui/number-input/README.md +65 -0
  289. package/dist/fancy-ui/number-ticker/NumberTicker.svelte +18 -3
  290. package/dist/fancy-ui/number-ticker/NumberTicker.test.js +94 -0
  291. package/dist/fancy-ui/pagination/Pagination.svelte +63 -1
  292. package/dist/fancy-ui/pagination/Pagination.svelte.d.ts +5 -0
  293. package/dist/fancy-ui/pagination/Pagination.test.js +90 -2
  294. package/dist/fancy-ui/pagination/PaginationHarness.test.svelte +3 -0
  295. package/dist/fancy-ui/pagination/PaginationHarness.test.svelte.d.ts +1 -0
  296. package/dist/fancy-ui/pagination/README.md +38 -0
  297. package/dist/fancy-ui/password-input/PasswordInput.svelte +109 -9
  298. package/dist/fancy-ui/password-input/PasswordInput.svelte.d.ts +5 -0
  299. package/dist/fancy-ui/password-input/PasswordInput.test.js +187 -0
  300. package/dist/fancy-ui/password-input/README.md +52 -0
  301. package/dist/fancy-ui/popover/Popover.svelte +17 -0
  302. package/dist/fancy-ui/popover/Popover.svelte.d.ts +5 -0
  303. package/dist/fancy-ui/popover/Popover.test.js +361 -2
  304. package/dist/fancy-ui/popover/PopoverContent.svelte +86 -22
  305. package/dist/fancy-ui/popover/README.md +70 -12
  306. package/dist/fancy-ui/popover/types.d.ts +11 -0
  307. package/dist/fancy-ui/presence/Presence.svelte +162 -0
  308. package/dist/fancy-ui/presence/Presence.svelte.d.ts +29 -0
  309. package/dist/fancy-ui/presence/Presence.test.d.ts +1 -0
  310. package/dist/fancy-ui/presence/Presence.test.js +274 -0
  311. package/dist/fancy-ui/presence/PresenceHarness.test.svelte +23 -0
  312. package/dist/fancy-ui/presence/PresenceHarness.test.svelte.d.ts +7 -0
  313. package/dist/fancy-ui/presence/README.md +121 -0
  314. package/dist/fancy-ui/presence/index.d.ts +2 -0
  315. package/dist/fancy-ui/presence/index.js +1 -0
  316. package/dist/fancy-ui/pressable/Pressable.svelte +217 -0
  317. package/dist/fancy-ui/pressable/Pressable.svelte.d.ts +32 -0
  318. package/dist/fancy-ui/pressable/Pressable.test.d.ts +1 -0
  319. package/dist/fancy-ui/pressable/Pressable.test.js +285 -0
  320. package/dist/fancy-ui/pressable/README.md +80 -0
  321. package/dist/fancy-ui/pressable/index.d.ts +3 -0
  322. package/dist/fancy-ui/pressable/index.js +2 -0
  323. package/dist/fancy-ui/prompt-suggestions/PromptSuggestions.svelte +14 -1
  324. package/dist/fancy-ui/prompt-suggestions/PromptSuggestions.svelte.d.ts +5 -0
  325. package/dist/fancy-ui/prompt-suggestions/PromptSuggestions.test.js +41 -1
  326. package/dist/fancy-ui/prompt-suggestions/README.md +11 -0
  327. package/dist/fancy-ui/pulse-beam/PulseBeam.ssr.test.d.ts +1 -0
  328. package/dist/fancy-ui/pulse-beam/PulseBeam.ssr.test.js +15 -0
  329. package/dist/fancy-ui/pulse-beam/PulseBeam.svelte +373 -0
  330. package/dist/fancy-ui/pulse-beam/PulseBeam.svelte.d.ts +50 -0
  331. package/dist/fancy-ui/pulse-beam/PulseBeam.test.d.ts +1 -0
  332. package/dist/fancy-ui/pulse-beam/PulseBeam.test.js +254 -0
  333. package/dist/fancy-ui/pulse-beam/README.md +72 -0
  334. package/dist/fancy-ui/pulse-beam/index.d.ts +2 -0
  335. package/dist/fancy-ui/pulse-beam/index.js +2 -0
  336. package/dist/fancy-ui/pulse-beam/pulse-beam-data.d.ts +81 -0
  337. package/dist/fancy-ui/pulse-beam/pulse-beam-data.js +331 -0
  338. package/dist/fancy-ui/pulse-beam/pulse-beam-data.test.d.ts +1 -0
  339. package/dist/fancy-ui/pulse-beam/pulse-beam-data.test.js +155 -0
  340. package/dist/fancy-ui/pulse-beam/pulse-beam-loop.d.ts +26 -0
  341. package/dist/fancy-ui/pulse-beam/pulse-beam-loop.js +84 -0
  342. package/dist/fancy-ui/radio-group/README.md +27 -1
  343. package/dist/fancy-ui/radio-group/RadioGroup.test.js +43 -0
  344. package/dist/fancy-ui/radio-group/RadioGroupItem.svelte +31 -2
  345. package/dist/fancy-ui/rainbow-button/README.md +23 -9
  346. package/dist/fancy-ui/rainbow-button/RainbowButton.svelte +25 -1
  347. package/dist/fancy-ui/rainbow-button/RainbowButton.svelte.d.ts +5 -0
  348. package/dist/fancy-ui/rainbow-button/RainbowButton.test.js +49 -2
  349. package/dist/fancy-ui/reasoning-panel/README.md +11 -0
  350. package/dist/fancy-ui/reasoning-panel/ReasoningPanel.svelte +13 -1
  351. package/dist/fancy-ui/reasoning-panel/ReasoningPanel.svelte.d.ts +5 -0
  352. package/dist/fancy-ui/reasoning-panel/ReasoningPanel.test.js +47 -1
  353. package/dist/fancy-ui/recommendation-card/README.md +25 -14
  354. package/dist/fancy-ui/recommendation-card/RecommendationCard.svelte +10 -0
  355. package/dist/fancy-ui/recommendation-card/RecommendationCard.svelte.d.ts +5 -0
  356. package/dist/fancy-ui/recommendation-card/RecommendationCard.test.js +39 -1
  357. package/dist/fancy-ui/registry.d.ts +7 -0
  358. package/dist/fancy-ui/registry.js +1278 -99
  359. package/dist/fancy-ui/registry.test.d.ts +1 -0
  360. package/dist/fancy-ui/registry.test.js +32 -0
  361. package/dist/fancy-ui/reveal/README.md +186 -0
  362. package/dist/fancy-ui/reveal/Reveal.svelte +353 -0
  363. package/dist/fancy-ui/reveal/Reveal.svelte.d.ts +41 -0
  364. package/dist/fancy-ui/reveal/Reveal.test.d.ts +1 -0
  365. package/dist/fancy-ui/reveal/Reveal.test.js +421 -0
  366. package/dist/fancy-ui/reveal/RevealHarness.test.svelte +49 -0
  367. package/dist/fancy-ui/reveal/RevealHarness.test.svelte.d.ts +9 -0
  368. package/dist/fancy-ui/reveal/index.d.ts +2 -0
  369. package/dist/fancy-ui/reveal/index.js +1 -0
  370. package/dist/fancy-ui/ripple-button/README.md +17 -6
  371. package/dist/fancy-ui/ripple-button/RippleButton.svelte +10 -1
  372. package/dist/fancy-ui/ripple-button/RippleButton.svelte.d.ts +5 -0
  373. package/dist/fancy-ui/ripple-button/RippleButton.test.js +81 -2
  374. package/dist/fancy-ui/scroll-anchor/README.md +11 -0
  375. package/dist/fancy-ui/scroll-anchor/ScrollAnchor.svelte +9 -0
  376. package/dist/fancy-ui/scroll-anchor/ScrollAnchor.svelte.d.ts +6 -0
  377. package/dist/fancy-ui/scroll-anchor/ScrollAnchor.test.js +39 -0
  378. package/dist/fancy-ui/scroll-progress/README.md +73 -0
  379. package/dist/fancy-ui/scroll-progress/ScrollProgress.svelte +291 -0
  380. package/dist/fancy-ui/scroll-progress/ScrollProgress.svelte.d.ts +38 -0
  381. package/dist/fancy-ui/scroll-progress/ScrollProgress.test.d.ts +1 -0
  382. package/dist/fancy-ui/scroll-progress/ScrollProgress.test.js +275 -0
  383. package/dist/fancy-ui/scroll-progress/index.d.ts +3 -0
  384. package/dist/fancy-ui/scroll-progress/index.js +2 -0
  385. package/dist/fancy-ui/search-input/README.md +44 -0
  386. package/dist/fancy-ui/search-input/SearchInput.svelte +31 -0
  387. package/dist/fancy-ui/search-input/SearchInput.svelte.d.ts +5 -0
  388. package/dist/fancy-ui/search-input/SearchInput.test.js +131 -0
  389. package/dist/fancy-ui/select/README.md +32 -0
  390. package/dist/fancy-ui/select/Select.svelte +6 -0
  391. package/dist/fancy-ui/select/Select.test.js +162 -4
  392. package/dist/fancy-ui/select/SelectPanel.svelte +56 -18
  393. package/dist/fancy-ui/select/types.d.ts +14 -0
  394. package/dist/fancy-ui/sheet/README.md +63 -27
  395. package/dist/fancy-ui/sheet/Sheet.svelte +150 -85
  396. package/dist/fancy-ui/sheet/Sheet.svelte.d.ts +12 -0
  397. package/dist/fancy-ui/sheet/Sheet.test.js +192 -8
  398. package/dist/fancy-ui/sheet/SheetHarness.test.svelte +3 -2
  399. package/dist/fancy-ui/sheet/SheetHarness.test.svelte.d.ts +1 -0
  400. package/dist/fancy-ui/shimmer-button/README.md +10 -0
  401. package/dist/fancy-ui/shimmer-button/ShimmerButton.svelte +14 -0
  402. package/dist/fancy-ui/shimmer-button/ShimmerButton.svelte.d.ts +5 -0
  403. package/dist/fancy-ui/shimmer-button/ShimmerButton.test.js +39 -2
  404. package/dist/fancy-ui/sidebar/README.md +42 -0
  405. package/dist/fancy-ui/sidebar/Sidebar.svelte +14 -1
  406. package/dist/fancy-ui/sidebar/Sidebar.test.js +94 -1
  407. package/dist/fancy-ui/sidebar/SidebarItem.svelte +51 -1
  408. package/dist/fancy-ui/sidebar/SidebarItem.svelte.d.ts +5 -0
  409. package/dist/fancy-ui/skeleton/README.md +166 -0
  410. package/dist/fancy-ui/skeleton/Skeleton.svelte +390 -0
  411. package/dist/fancy-ui/skeleton/Skeleton.svelte.d.ts +38 -0
  412. package/dist/fancy-ui/skeleton/Skeleton.test.d.ts +1 -0
  413. package/dist/fancy-ui/skeleton/Skeleton.test.js +347 -0
  414. package/dist/fancy-ui/skeleton/SkeletonRevealHarness.test.svelte +48 -0
  415. package/dist/fancy-ui/skeleton/SkeletonRevealHarness.test.svelte.d.ts +11 -0
  416. package/dist/fancy-ui/skeleton/index.d.ts +3 -0
  417. package/dist/fancy-ui/skeleton/index.js +2 -0
  418. package/dist/fancy-ui/slider/README.md +19 -0
  419. package/dist/fancy-ui/slider/Slider.svelte +19 -0
  420. package/dist/fancy-ui/slider/Slider.svelte.d.ts +5 -0
  421. package/dist/fancy-ui/slider/Slider.test.js +37 -0
  422. package/dist/fancy-ui/smooth-cursor/SmoothCursor.svelte +2 -1
  423. package/dist/fancy-ui/smooth-cursor/SmoothCursor.test.js +6 -0
  424. package/dist/fancy-ui/sound/README.md +72 -13
  425. package/dist/fancy-ui/sources/README.md +12 -1
  426. package/dist/fancy-ui/sources/Sources.svelte +9 -0
  427. package/dist/fancy-ui/sources/Sources.svelte.d.ts +6 -0
  428. package/dist/fancy-ui/sources/Sources.test.js +34 -0
  429. package/dist/fancy-ui/sources/SourcesHarness.test.svelte +10 -2
  430. package/dist/fancy-ui/sources/SourcesHarness.test.svelte.d.ts +1 -0
  431. package/dist/fancy-ui/sparkles/Sparkles.svelte +6 -3
  432. package/dist/fancy-ui/sparkles/Sparkles.test.js +51 -0
  433. package/dist/fancy-ui/status-morph/README.md +175 -0
  434. package/dist/fancy-ui/status-morph/StatusMorph.svelte +449 -0
  435. package/dist/fancy-ui/status-morph/StatusMorph.svelte.d.ts +45 -0
  436. package/dist/fancy-ui/status-morph/StatusMorph.test.d.ts +1 -0
  437. package/dist/fancy-ui/status-morph/StatusMorph.test.js +243 -0
  438. package/dist/fancy-ui/status-morph/StatusMorphHarness.test.svelte +33 -0
  439. package/dist/fancy-ui/status-morph/StatusMorphHarness.test.svelte.d.ts +7 -0
  440. package/dist/fancy-ui/status-morph/index.d.ts +3 -0
  441. package/dist/fancy-ui/status-morph/index.js +2 -0
  442. package/dist/fancy-ui/stepper/README.md +40 -0
  443. package/dist/fancy-ui/stepper/Step.svelte +26 -0
  444. package/dist/fancy-ui/stepper/Stepper.svelte +9 -0
  445. package/dist/fancy-ui/stepper/Stepper.svelte.d.ts +5 -0
  446. package/dist/fancy-ui/stepper/Stepper.test.js +98 -1
  447. package/dist/fancy-ui/stepper/StepperHarness.test.svelte +11 -1
  448. package/dist/fancy-ui/stepper/StepperHarness.test.svelte.d.ts +1 -0
  449. package/dist/fancy-ui/sticky-scroll/README.md +91 -0
  450. package/dist/fancy-ui/sticky-scroll/StickyScroll.svelte +255 -0
  451. package/dist/fancy-ui/sticky-scroll/StickyScroll.svelte.d.ts +62 -0
  452. package/dist/fancy-ui/sticky-scroll/StickyScroll.test.d.ts +1 -0
  453. package/dist/fancy-ui/sticky-scroll/StickyScroll.test.js +267 -0
  454. package/dist/fancy-ui/sticky-scroll/StickyScrollHarness.test.svelte +73 -0
  455. package/dist/fancy-ui/sticky-scroll/StickyScrollHarness.test.svelte.d.ts +12 -0
  456. package/dist/fancy-ui/sticky-scroll/index.d.ts +3 -0
  457. package/dist/fancy-ui/sticky-scroll/index.js +2 -0
  458. package/dist/fancy-ui/subagent-list/README.md +11 -0
  459. package/dist/fancy-ui/subagent-list/SubagentList.svelte +15 -1
  460. package/dist/fancy-ui/subagent-list/SubagentList.svelte.d.ts +5 -0
  461. package/dist/fancy-ui/subagent-list/SubagentList.test.js +50 -1
  462. package/dist/fancy-ui/switch/README.md +27 -6
  463. package/dist/fancy-ui/switch/Switch.svelte +18 -4
  464. package/dist/fancy-ui/switch/Switch.test.js +41 -0
  465. package/dist/fancy-ui/tabs/README.md +141 -11
  466. package/dist/fancy-ui/tabs/Tabs.svelte +9 -0
  467. package/dist/fancy-ui/tabs/Tabs.svelte.d.ts +5 -0
  468. package/dist/fancy-ui/tabs/Tabs.test.js +407 -2
  469. package/dist/fancy-ui/tabs/TabsContent.svelte +34 -0
  470. package/dist/fancy-ui/tabs/TabsHarness.test.svelte +3 -1
  471. package/dist/fancy-ui/tabs/TabsHarness.test.svelte.d.ts +1 -0
  472. package/dist/fancy-ui/tabs/TabsList.svelte +330 -0
  473. package/dist/fancy-ui/tabs/TabsTrigger.svelte +46 -23
  474. package/dist/fancy-ui/terminal-text/README.md +1 -1
  475. package/dist/fancy-ui/terminal-text/TerminalText.svelte +31 -2
  476. package/dist/fancy-ui/terminal-text/TerminalText.test.js +70 -0
  477. package/dist/fancy-ui/terminal-text/TerminalTextHarness.test.svelte +36 -0
  478. package/dist/fancy-ui/terminal-text/TerminalTextHarness.test.svelte.d.ts +6 -0
  479. package/dist/fancy-ui/text-roll/README.md +152 -0
  480. package/dist/fancy-ui/text-roll/TextRoll.svelte +455 -0
  481. package/dist/fancy-ui/text-roll/TextRoll.svelte.d.ts +53 -0
  482. package/dist/fancy-ui/text-roll/TextRoll.test.d.ts +1 -0
  483. package/dist/fancy-ui/text-roll/TextRoll.test.js +574 -0
  484. package/dist/fancy-ui/text-roll/TextRollHarness.test.svelte +49 -0
  485. package/dist/fancy-ui/text-roll/TextRollHarness.test.svelte.d.ts +7 -0
  486. package/dist/fancy-ui/text-roll/index.d.ts +2 -0
  487. package/dist/fancy-ui/text-roll/index.js +2 -0
  488. package/dist/fancy-ui/text-roll/roll-transitions.d.ts +74 -0
  489. package/dist/fancy-ui/text-roll/roll-transitions.js +66 -0
  490. package/dist/fancy-ui/thread-list/README.md +11 -0
  491. package/dist/fancy-ui/thread-list/ThreadList.svelte +15 -1
  492. package/dist/fancy-ui/thread-list/ThreadList.svelte.d.ts +6 -0
  493. package/dist/fancy-ui/thread-list/ThreadList.test.js +43 -0
  494. package/dist/fancy-ui/thread-list/ThreadListHarness.test.svelte +3 -0
  495. package/dist/fancy-ui/thread-list/ThreadListHarness.test.svelte.d.ts +1 -0
  496. package/dist/fancy-ui/time-picker/README.md +51 -1
  497. package/dist/fancy-ui/time-picker/TimePicker.svelte +47 -10
  498. package/dist/fancy-ui/time-picker/TimePicker.svelte.d.ts +5 -0
  499. package/dist/fancy-ui/time-picker/TimePicker.test.js +254 -1
  500. package/dist/fancy-ui/time-picker/TimePickerPanel.svelte +55 -20
  501. package/dist/fancy-ui/time-picker/types.d.ts +8 -0
  502. package/dist/fancy-ui/toast/README.md +62 -6
  503. package/dist/fancy-ui/toast/Toast.svelte +32 -15
  504. package/dist/fancy-ui/toast/Toast.test.js +243 -4
  505. package/dist/fancy-ui/toast/Toaster.svelte +48 -0
  506. package/dist/fancy-ui/toast/Toaster.svelte.d.ts +6 -0
  507. package/dist/fancy-ui/toggle/README.md +48 -11
  508. package/dist/fancy-ui/toggle/Toggle.svelte +89 -10
  509. package/dist/fancy-ui/toggle/Toggle.svelte.d.ts +5 -0
  510. package/dist/fancy-ui/toggle/Toggle.test.js +78 -1
  511. package/dist/fancy-ui/toggle-group/README.md +50 -12
  512. package/dist/fancy-ui/toggle-group/ToggleGroup.svelte +8 -0
  513. package/dist/fancy-ui/toggle-group/ToggleGroup.svelte.d.ts +5 -0
  514. package/dist/fancy-ui/toggle-group/ToggleGroup.test.js +97 -1
  515. package/dist/fancy-ui/toggle-group/ToggleGroupHarness.test.svelte +3 -1
  516. package/dist/fancy-ui/toggle-group/ToggleGroupHarness.test.svelte.d.ts +1 -0
  517. package/dist/fancy-ui/toggle-group/ToggleGroupItem.svelte +53 -1
  518. package/dist/fancy-ui/tool-call/README.md +21 -10
  519. package/dist/fancy-ui/tool-call/ToolCall.svelte +13 -1
  520. package/dist/fancy-ui/tool-call/ToolCall.svelte.d.ts +5 -0
  521. package/dist/fancy-ui/tool-call/ToolCall.test.js +50 -1
  522. package/dist/fancy-ui/tool-timeline/README.md +11 -0
  523. package/dist/fancy-ui/tool-timeline/ToolTimeline.svelte +15 -1
  524. package/dist/fancy-ui/tool-timeline/ToolTimeline.svelte.d.ts +5 -0
  525. package/dist/fancy-ui/tool-timeline/ToolTimeline.test.js +56 -2
  526. package/dist/fancy-ui/tooltip/README.md +26 -0
  527. package/dist/fancy-ui/tooltip/Tooltip.svelte +48 -20
  528. package/dist/fancy-ui/tooltip/Tooltip.test.js +118 -0
  529. package/dist/fancy-ui/voice-input/README.md +25 -13
  530. package/dist/fancy-ui/voice-input/VoiceInput.svelte +42 -1
  531. package/dist/fancy-ui/voice-input/VoiceInput.svelte.d.ts +5 -0
  532. package/dist/fancy-ui/voice-input/VoiceInput.test.js +99 -1
  533. package/dist/fancy-ui/voice-input/VoiceInputHarness.test.svelte +3 -1
  534. package/dist/fancy-ui/voice-input/VoiceInputHarness.test.svelte.d.ts +1 -0
  535. package/dist/fancy-ui/web-search/README.md +11 -0
  536. package/dist/fancy-ui/web-search/WebSearch.svelte +22 -2
  537. package/dist/fancy-ui/web-search/WebSearch.svelte.d.ts +5 -0
  538. package/dist/fancy-ui/web-search/WebSearch.test.js +60 -1
  539. package/package.json +3 -2
package/README.md CHANGED
@@ -9,7 +9,7 @@ Beautiful animation and UI components for **Svelte 5**.
9
9
  ![Svelte 5](https://img.shields.io/badge/Svelte-5-FF3E00?logo=svelte&logoColor=white)
10
10
  ![Tailwind CSS](https://img.shields.io/badge/Tailwind_CSS-4-06B6D4?logo=tailwindcss&logoColor=white)
11
11
  ![TypeScript](https://img.shields.io/badge/TypeScript-5-3178C6?logo=typescript&logoColor=white)
12
- ![Components](https://img.shields.io/badge/Components-133-8B5CF6)
12
+ ![Components](https://img.shields.io/badge/Components-145-8B5CF6)
13
13
  ![MIT License](https://img.shields.io/badge/License-MIT-green)
14
14
 
15
15
  <p align="center">
@@ -36,10 +36,10 @@ Beautiful animation and UI components for **Svelte 5**.
36
36
  - **Svelte 5 Runes** &mdash; Built with `$state`, `$derived`, `$effect`, and `$props`
37
37
  - **Tailwind CSS 4** &mdash; Utility-first styling with theme tokens
38
38
  - **TypeScript** &mdash; Fully typed props and events
39
- - **133 Components** &mdash; Core primitives (forms, navigation, overlays) alongside buttons, text animations, backgrounds and effects
39
+ - **145 Components** &mdash; Core primitives (forms, navigation, overlays) alongside buttons, text animations, backgrounds, effects and a micro-interactions collection
40
40
  - **Dark Mode** &mdash; All components support light and dark themes
41
41
  - **Opt-in Sound** &mdash; Eleven synthesised interface cues, a `SoundToggle` switch and a `soundFeedback` action; silent until the user turns it on
42
- - **Tested** &mdash; 2,900+ unit tests with Vitest and Testing Library
42
+ - **Tested** &mdash; 3,700+ unit tests with Vitest and Testing Library
43
43
 
44
44
  ## Quick Start
45
45
 
@@ -152,7 +152,7 @@ Component tests are colocated with their component (see `RainbowButton.test.ts`
152
152
 
153
153
  ## Contributing
154
154
 
155
- Contributions are welcome! 133 components and counting — PRs for new components, bug fixes, and improvements are appreciated.
155
+ Contributions are welcome! 145 components and counting — PRs for new components, bug fixes, and improvements are appreciated.
156
156
 
157
157
  ### Adding a new component
158
158
 
@@ -0,0 +1,29 @@
1
+ <!--
2
+ Test-only rig, half one of two. The PARENT: it owns the `{#if}`, and the
3
+ element that transitions lives one component down, in
4
+ `ScrollLockPanel.test.svelte`. See that file for what this pair proves.
5
+
6
+ `open` is held as local `$state` and flipped from a button inside this
7
+ component, deliberately, rather than driven from the test through
8
+ `rerender()`: `rerender` awaits a tick of its own, so a test using it can
9
+ never observe the ONE flush the proof is about — by the time it returns, the
10
+ stubbed animation has already resolved and the panel has left either way.
11
+ A click flips the state synchronously, so a single `await tick()` after it
12
+ lands exactly inside the exit window.
13
+
14
+ Not exported from index.ts, and not collected by Vitest directly (the run
15
+ includes `*.test.ts` only) — imported and rendered from scroll-lock.test.ts.
16
+ -->
17
+ <script lang="ts">
18
+ import ScrollLockPanel from "./ScrollLockPanel.test.svelte";
19
+
20
+ // Always starts closed, so the first click is a genuine (non-first-mount)
21
+ // intro and the second a genuine exit — the pair the proof needs.
22
+ let isOpen = $state(false);
23
+ </script>
24
+
25
+ <button type="button" data-testid="toggle" onclick={() => (isOpen = !isOpen)}>toggle</button>
26
+
27
+ {#if isOpen}
28
+ <ScrollLockPanel open={isOpen} />
29
+ {/if}
@@ -0,0 +1,3 @@
1
+ declare const ScrollLockHarness: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type ScrollLockHarness = ReturnType<typeof ScrollLockHarness>;
3
+ export default ScrollLockHarness;
@@ -0,0 +1,43 @@
1
+ <!--
2
+ Test-only rig, half two of two. The CHILD component: its root element
3
+ carries the exit transition and `use:scrollLock`, exactly as a real panel
4
+ does, while the `{#if}` that mounts and unmounts it lives in its PARENT
5
+ (`ScrollLockHarness.test.svelte`).
6
+
7
+ That split is the whole point. Five Tier-2 panels are shaped this way, and
8
+ the campaign's design rests on a claim about Svelte's internals: a LOCAL
9
+ transition on a child component's root element is still collected by the
10
+ parent's `{#if}` when that branch closes, because the child compiles to a
11
+ transparent effect the collector walks through. If the claim is wrong the
12
+ transition is skipped, the branch is destroyed synchronously, and the scroll
13
+ lock releases the instant `open` flips rather than when the surface is
14
+ actually gone. `scroll-lock.test.ts` proves it rather than assuming it.
15
+
16
+ Not exported from index.ts, and not collected by Vitest directly (the run
17
+ includes `*.test.ts` only) — imported and rendered from scroll-lock.test.ts.
18
+ -->
19
+ <script lang="ts">
20
+ import { scrollLock } from "./scroll-lock.js";
21
+ import { anchored } from "./motion/anchored.js";
22
+ import { DURATIONS } from "./motion/tokens.js";
23
+
24
+ interface Props {
25
+ /** The parent's own `{#if}` condition, forwarded so the one
26
+ * bidirectional directive can tell an entrance from an exit. */
27
+ open: boolean;
28
+ }
29
+
30
+ let { open }: Props = $props();
31
+ </script>
32
+
33
+ <div
34
+ data-testid="panel"
35
+ use:scrollLock
36
+ transition:anchored={{
37
+ entering: open,
38
+ duration: DURATIONS.base,
39
+ exitDuration: DURATIONS.exit,
40
+ }}
41
+ >
42
+ panel
43
+ </div>
@@ -0,0 +1,8 @@
1
+ interface Props {
2
+ /** The parent's own `{#if}` condition, forwarded so the one
3
+ * bidirectional directive can tell an entrance from an exit. */
4
+ open: boolean;
5
+ }
6
+ declare const ScrollLockPanel: import("svelte").Component<Props, {}, "">;
7
+ type ScrollLockPanel = ReturnType<typeof ScrollLockPanel>;
8
+ export default ScrollLockPanel;
@@ -21,6 +21,13 @@ export interface ComputePositionResult {
21
21
  y: number;
22
22
  /** The side actually used, which may differ from the requested one when flipped. */
23
23
  side: Side;
24
+ /**
25
+ * The cross-axis alignment the element actually ended up with, which
26
+ * differs from the requested one whenever clamping slid it along that
27
+ * axis. Read it — not the requested `align` — for anything that has to
28
+ * point back at the anchor, such as a transform origin or a caret.
29
+ */
30
+ align: Align;
24
31
  }
25
32
  /**
26
33
  * Computes where to place a floating element relative to an anchor rect.
@@ -40,15 +47,19 @@ export interface AnchorPositionOptions {
40
47
  offset?: number;
41
48
  /**
42
49
  * Called with the side the element was actually placed on, which differs
43
- * from the requested `side` whenever a flip occurred. Fires once on the
44
- * initial placement and thereafter only when the resolved side changes,
45
- * so a consumer can react (move a caret, mirror a submenu's open
46
- * direction) without re-rendering on every scroll frame.
50
+ * from the requested `side` whenever a flip occurred, and with the
51
+ * cross-axis alignment it actually ended up with, which differs from the
52
+ * requested `align` whenever clamping slid it along that axis. Fires once
53
+ * on the initial placement and thereafter only when either resolved value
54
+ * changes, so a consumer can react (move a caret, set a transform origin,
55
+ * mirror a submenu's open direction) without re-rendering on every scroll
56
+ * frame.
47
57
  *
48
- * Without this, `computePosition`'s resolved side is computed and
49
- * discarded here, and a consumer has no way to learn a flip happened.
58
+ * Without this, `computePosition`'s resolved placement is computed and
59
+ * discarded here, and a consumer has no way to learn a flip or a clamp
60
+ * happened.
50
61
  */
51
- onPlacement?: (side: Side) => void;
62
+ onPlacement?: (side: Side, align: Align) => void;
52
63
  }
53
64
  /**
54
65
  * Svelte action that positions `node` with `position: fixed` against a live
@@ -75,6 +75,34 @@ function placeAt(side, anchor, floating, align, offset) {
75
75
  }
76
76
  return { x, y };
77
77
  }
78
+ /**
79
+ * Where the anchor's cross-axis centre falls inside the element as finally
80
+ * placed, expressed in the same vocabulary as the requested `align`.
81
+ *
82
+ * The requested alignment cannot answer this: clamping moves x/y without
83
+ * touching either input, so a menu asked for `align: "start"` next to the
84
+ * viewport's right edge is reported as `start` while the anchor now sits by
85
+ * its right edge. Deriving from the clamped coordinates keeps that honest.
86
+ * The thirds are the natural split of the three-value vocabulary.
87
+ */
88
+ function resolveAlign(side, anchor, floating, x, y, requested) {
89
+ const horizontal = isHorizontalSide(side);
90
+ const extent = horizontal ? floating.width : floating.height;
91
+ // A zero-size element has no inside for the anchor to fall in — an
92
+ // unlaid-out first frame, a `display: none` ancestor, or jsdom, where
93
+ // every element measures zero. Nothing was clamped in that state either,
94
+ // so the requested alignment is both the honest answer and the one that
95
+ // keeps a never-measured panel behaving exactly as it did before.
96
+ if (extent <= 0)
97
+ return requested;
98
+ const anchorCentre = horizontal ? anchor.left + anchor.width / 2 : anchor.top + anchor.height / 2;
99
+ const ratio = (anchorCentre - (horizontal ? x : y)) / extent;
100
+ if (ratio < 1 / 3)
101
+ return "start";
102
+ if (ratio > 2 / 3)
103
+ return "end";
104
+ return "center";
105
+ }
78
106
  /**
79
107
  * Computes where to place a floating element relative to an anchor rect.
80
108
  *
@@ -88,10 +116,13 @@ export function computePosition(anchor, floating, opts = {}) {
88
116
  const { x, y } = placeAt(resolvedSide, anchor, floating, align, offset);
89
117
  const maxX = Math.max(0, viewport.width - floating.width);
90
118
  const maxY = Math.max(0, viewport.height - floating.height);
119
+ const clampedX = clamp(x, 0, maxX);
120
+ const clampedY = clamp(y, 0, maxY);
91
121
  return {
92
- x: clamp(x, 0, maxX),
93
- y: clamp(y, 0, maxY),
122
+ x: clampedX,
123
+ y: clampedY,
94
124
  side: resolvedSide,
125
+ align: resolveAlign(resolvedSide, anchor, floating, clampedX, clampedY, align),
95
126
  };
96
127
  }
97
128
  /**
@@ -107,18 +138,19 @@ export const anchorPosition = (node, opts) => {
107
138
  return {};
108
139
  }
109
140
  let options = opts;
110
- // The last side reported through `onPlacement`, so a scroll or resize that
111
- // recomputes to the same side stays silent. `null` means "nothing reported
112
- // yet", which is distinct from any real side and makes the first placement
113
- // always fire.
141
+ // The last placement reported through `onPlacement`, so a scroll or resize
142
+ // that recomputes to the same one stays silent. `null` means "nothing
143
+ // reported yet", which is distinct from any real value and makes the first
144
+ // placement always fire.
114
145
  let reportedSide = null;
146
+ let reportedAlign = null;
115
147
  function update() {
116
148
  const anchorEl = options.anchor();
117
149
  if (!anchorEl)
118
150
  return;
119
151
  const anchorRect = anchorEl.getBoundingClientRect();
120
152
  const floatingRect = node.getBoundingClientRect();
121
- const { x, y, side } = computePosition(anchorRect, { width: floatingRect.width, height: floatingRect.height }, {
153
+ const { x, y, side, align } = computePosition(anchorRect, { width: floatingRect.width, height: floatingRect.height }, {
122
154
  side: options.side,
123
155
  align: options.align,
124
156
  offset: options.offset,
@@ -127,9 +159,10 @@ export const anchorPosition = (node, opts) => {
127
159
  node.style.position = "fixed";
128
160
  node.style.left = `${x}px`;
129
161
  node.style.top = `${y}px`;
130
- if (side !== reportedSide) {
162
+ if (side !== reportedSide || align !== reportedAlign) {
131
163
  reportedSide = side;
132
- options.onPlacement?.(side);
164
+ reportedAlign = align;
165
+ options.onPlacement?.(side, align);
133
166
  }
134
167
  }
135
168
  update();
@@ -145,8 +178,10 @@ export const anchorPosition = (node, opts) => {
145
178
  // scoped to the node, not to the callback, so the gate that exists
146
179
  // to suppress duplicate reports to the *same* listener ends up
147
180
  // suppressing the first report to a *different* one.
148
- if (newOpts.onPlacement !== options.onPlacement)
181
+ if (newOpts.onPlacement !== options.onPlacement) {
149
182
  reportedSide = null;
183
+ reportedAlign = null;
184
+ }
150
185
  options = newOpts;
151
186
  update();
152
187
  },
@@ -89,6 +89,53 @@ describe("computePosition", () => {
89
89
  expect(() => computePosition(anchor, { width: 10, height: 10 }, { viewport: { width: 100, height: 100 } })).not.toThrow();
90
90
  });
91
91
  });
92
+ // The requested alignment says which corner the caller ASKED for; only the
93
+ // clamped coordinates say which corner the anchor actually ends up near. A
94
+ // transform origin taken from the request grows the panel out of the corner
95
+ // farthest from the click whenever a clamp moved it.
96
+ describe("computePosition — resolved cross-axis align", () => {
97
+ it("reports the requested align back when nothing clamped it", () => {
98
+ // Mid-viewport on purpose: at x=100 the end-aligned panel would start at
99
+ // -50 and be clamped to 0, which is the very case the tests below cover.
100
+ const anchor = rect({ x: 400, y: 100, width: 50, height: 20 });
101
+ expect(computePosition(anchor, { width: 200, height: 40 }, { align: "start", viewport }).align).toBe("start");
102
+ expect(computePosition(anchor, { width: 200, height: 40 }, { align: "center", viewport }).align).toBe("center");
103
+ expect(computePosition(anchor, { width: 200, height: 40 }, { align: "end", viewport }).align).toBe("end");
104
+ });
105
+ // The reported case: a right-click near the viewport's right edge, opened
106
+ // bottom/start. The panel is clamped leftward until its RIGHT edge is the
107
+ // one at the pointer, so the origin has to be "end", not the requested
108
+ // "start".
109
+ it("reports 'end' when a start-aligned panel is clamped off the right edge", () => {
110
+ const anchor = rect({ x: 1010, y: 100, width: 0, height: 0 });
111
+ const result = computePosition(anchor, { width: 200, height: 40 }, { side: "bottom", align: "start", viewport });
112
+ expect(result.x).toBe(viewport.width - 200); // clamped, not anchor.left
113
+ expect(result.align).toBe("end");
114
+ });
115
+ it("reports 'start' when an end-aligned panel is clamped off the left edge", () => {
116
+ const anchor = rect({ x: 4, y: 100, width: 0, height: 0 });
117
+ const result = computePosition(anchor, { width: 200, height: 40 }, { side: "bottom", align: "end", viewport });
118
+ expect(result.x).toBe(0);
119
+ expect(result.align).toBe("start");
120
+ });
121
+ // Same failure on the other axis: a side-placed panel clamped against the
122
+ // bottom of the viewport.
123
+ it("resolves against the vertical axis for left/right sides", () => {
124
+ const anchor = rect({ x: 100, y: 760, width: 20, height: 0 });
125
+ const result = computePosition(anchor, { width: 200, height: 300 }, { side: "right", align: "start", viewport });
126
+ expect(result.y).toBe(viewport.height - 300);
127
+ expect(result.align).toBe("end");
128
+ });
129
+ // Every element measures zero in jsdom, so this is the path the component
130
+ // tests take. With no measurable box nothing was clamped either, so the
131
+ // requested alignment is the honest answer — and the one that keeps an
132
+ // unlaid-out first frame pointing where the caller asked.
133
+ it("falls back to the requested align for a zero-size element", () => {
134
+ const anchor = rect({ x: 100, y: 100, width: 50, height: 20 });
135
+ expect(computePosition(anchor, { width: 0, height: 0 }, { align: "start", viewport }).align).toBe("start");
136
+ expect(computePosition(anchor, { width: 0, height: 0 }, { align: "end", viewport }).align).toBe("end");
137
+ });
138
+ });
92
139
  describe("anchorPosition — onPlacement", () => {
93
140
  afterEach(() => {
94
141
  document.body.innerHTML = "";
@@ -189,7 +236,7 @@ describe("anchorPosition — onPlacement", () => {
189
236
  onPlacement: second,
190
237
  });
191
238
  expect(second).toHaveBeenCalledTimes(1);
192
- expect(second).toHaveBeenCalledWith("bottom");
239
+ expect(second).toHaveBeenCalledWith("bottom", "center");
193
240
  expect(first).toHaveBeenCalledTimes(1);
194
241
  action?.destroy?.();
195
242
  });
@@ -8,5 +8,21 @@ export interface DismissableOptions {
8
8
  outsideClick?: boolean;
9
9
  /** Elements to ignore when detecting outside clicks (e.g. a trigger button). */
10
10
  exclude?: () => (HTMLElement | null)[];
11
+ /**
12
+ * Whether this layer is still the live one. A GETTER, not a boolean, and
13
+ * read fresh on every Escape and every outside pointerdown — because an
14
+ * action's `update()` never runs again once its owning `{#if}` branch
15
+ * starts closing (Svelte marks the branch INERT before it plays the outro,
16
+ * and the scheduler skips inert effects). A changed param would never
17
+ * arrive; a getter captured at creation always reads the current value.
18
+ *
19
+ * Every overlay that animates its exit passes `() => open`. The moment
20
+ * `open` flips false the layer stops answering Escape AND stops counting
21
+ * as the top layer, so a second Escape during the fade is a no-op and the
22
+ * dialog underneath gets it instead of having it swallowed by a panel that
23
+ * is already leaving. Defaults to always-active, which is exactly the
24
+ * behaviour every caller had before this option existed.
25
+ */
26
+ active?: () => boolean;
11
27
  }
12
28
  export declare const dismissable: Action<HTMLElement, DismissableOptions>;
@@ -3,9 +3,6 @@
3
3
  // disabled individually. `exclude` lets callers ignore extra elements that
4
4
  // live outside the node but should not count as "outside" (e.g. a trigger
5
5
  // button that toggles the dismissable element).
6
- // Stack of currently mounted dismissable layers. With nested overlays (e.g. a
7
- // popover inside a dialog) only the top-most layer reacts to Escape or an
8
- // outside pointerdown, so one interaction closes one layer at a time.
9
6
  const layers = [];
10
7
  export const dismissable = (node, opts) => {
11
8
  // Actions only run in the browser, but stay defensive for SSR contexts.
@@ -16,10 +13,23 @@ export const dismissable = (node, opts) => {
16
13
  let escape = opts.escape ?? true;
17
14
  let outsideClick = opts.outsideClick ?? true;
18
15
  let exclude = opts.exclude;
16
+ let isActive = opts.active ?? (() => true);
17
+ // Scans DOWN past inactive layers rather than testing only the last entry:
18
+ // a closing panel is still ON the stack (its `destroy()` is delayed by the
19
+ // outro), it just must not be TOP of it. The first live layer found from
20
+ // the top is the one that owns the interaction.
19
21
  function isTopLayer() {
20
- return layers[layers.length - 1] === node;
22
+ for (let i = layers.length - 1; i >= 0; i -= 1) {
23
+ if (layers[i].isActive())
24
+ return layers[i].node === node;
25
+ }
26
+ return false;
21
27
  }
22
28
  function handleKeydown(event) {
29
+ // Before `stopImmediatePropagation()`, deliberately: an inactive layer
30
+ // must not swallow the key on its way to whatever is underneath.
31
+ if (!isActive())
32
+ return;
23
33
  if (!escape)
24
34
  return;
25
35
  if (event.key !== "Escape")
@@ -30,6 +40,8 @@ export const dismissable = (node, opts) => {
30
40
  onDismiss();
31
41
  }
32
42
  function handlePointerdown(event) {
43
+ if (!isActive())
44
+ return;
33
45
  if (!outsideClick)
34
46
  return;
35
47
  if (!isTopLayer())
@@ -42,7 +54,8 @@ export const dismissable = (node, opts) => {
42
54
  return;
43
55
  onDismiss();
44
56
  }
45
- layers.push(node);
57
+ const layer = { node, isActive: () => isActive() };
58
+ layers.push(layer);
46
59
  document.addEventListener("keydown", handleKeydown);
47
60
  document.addEventListener("pointerdown", handlePointerdown);
48
61
  return {
@@ -51,9 +64,13 @@ export const dismissable = (node, opts) => {
51
64
  escape = newOpts.escape ?? true;
52
65
  outsideClick = newOpts.outsideClick ?? true;
53
66
  exclude = newOpts.exclude;
67
+ // The update path still matters for a panel whose `active` source
68
+ // changes while it is genuinely open. It is the CLOSE that never
69
+ // reaches here — hence the getter above, not this line.
70
+ isActive = newOpts.active ?? (() => true);
54
71
  },
55
72
  destroy() {
56
- const i = layers.indexOf(node);
73
+ const i = layers.indexOf(layer);
57
74
  if (i !== -1)
58
75
  layers.splice(i, 1);
59
76
  document.removeEventListener("keydown", handleKeydown);
@@ -21,41 +21,46 @@ describe("dismissable", () => {
21
21
  it("calls onDismiss on Escape by default", () => {
22
22
  const onDismiss = vi.fn();
23
23
  setup();
24
- dismissable(node, { onDismiss });
24
+ const action = dismissable(node, { onDismiss });
25
25
  pressEscape();
26
26
  expect(onDismiss).toHaveBeenCalledTimes(1);
27
+ action?.destroy?.();
27
28
  });
28
29
  it("does not call onDismiss on Escape when escape is false", () => {
29
30
  const onDismiss = vi.fn();
30
31
  setup();
31
- dismissable(node, { onDismiss, escape: false });
32
+ const action = dismissable(node, { onDismiss, escape: false });
32
33
  pressEscape();
33
34
  expect(onDismiss).not.toHaveBeenCalled();
35
+ action?.destroy?.();
34
36
  });
35
37
  it("calls onDismiss on pointerdown outside the node by default", () => {
36
38
  const onDismiss = vi.fn();
37
39
  setup();
38
40
  const outside = document.createElement("button");
39
41
  document.body.appendChild(outside);
40
- dismissable(node, { onDismiss });
42
+ const action = dismissable(node, { onDismiss });
41
43
  pointerDownOn(outside);
42
44
  expect(onDismiss).toHaveBeenCalledTimes(1);
45
+ action?.destroy?.();
43
46
  });
44
47
  it("does not call onDismiss on pointerdown inside the node", () => {
45
48
  const onDismiss = vi.fn();
46
49
  setup();
47
- dismissable(node, { onDismiss });
50
+ const action = dismissable(node, { onDismiss });
48
51
  pointerDownOn(node.querySelector("#inner"));
49
52
  expect(onDismiss).not.toHaveBeenCalled();
53
+ action?.destroy?.();
50
54
  });
51
55
  it("does not call onDismiss on outside pointerdown when outsideClick is false", () => {
52
56
  const onDismiss = vi.fn();
53
57
  setup();
54
58
  const outside = document.createElement("button");
55
59
  document.body.appendChild(outside);
56
- dismissable(node, { onDismiss, outsideClick: false });
60
+ const action = dismissable(node, { onDismiss, outsideClick: false });
57
61
  pointerDownOn(outside);
58
62
  expect(onDismiss).not.toHaveBeenCalled();
63
+ action?.destroy?.();
59
64
  });
60
65
  it("does not call onDismiss on pointerdown on an excluded element", () => {
61
66
  const onDismiss = vi.fn();
@@ -63,9 +68,10 @@ describe("dismissable", () => {
63
68
  const trigger = document.createElement("button");
64
69
  trigger.id = "trigger";
65
70
  document.body.appendChild(trigger);
66
- dismissable(node, { onDismiss, exclude: () => [trigger] });
71
+ const action = dismissable(node, { onDismiss, exclude: () => [trigger] });
67
72
  pointerDownOn(trigger);
68
73
  expect(onDismiss).not.toHaveBeenCalled();
74
+ action?.destroy?.();
69
75
  });
70
76
  it("removes listeners on destroy", () => {
71
77
  const onDismiss = vi.fn();
@@ -87,6 +93,7 @@ describe("dismissable", () => {
87
93
  pressEscape();
88
94
  expect(onDismiss).not.toHaveBeenCalled();
89
95
  expect(onDismissUpdated).toHaveBeenCalledTimes(1);
96
+ action?.destroy?.();
90
97
  });
91
98
  });
92
99
  describe("dismissable — stacked layers", () => {
@@ -126,3 +133,105 @@ describe("dismissable — stacked layers", () => {
126
133
  a?.destroy?.();
127
134
  });
128
135
  });
136
+ // `active` is what makes an ANIMATED close safe. A panel that is fading out
137
+ // still holds its layer — its `destroy()` is delayed by the outro — so
138
+ // without a liveness signal it would keep swallowing Escape for the whole
139
+ // fade. It is a getter, not a boolean, because an action's `update()` never
140
+ // runs again once its owning branch starts closing.
141
+ describe("dismissable — the active gate", () => {
142
+ afterEach(() => {
143
+ document.body.innerHTML = "";
144
+ });
145
+ function layer(active) {
146
+ const node = document.createElement("div");
147
+ document.body.appendChild(node);
148
+ const onDismiss = vi.fn();
149
+ const action = dismissable(node, { onDismiss, active });
150
+ return { node, onDismiss, action };
151
+ }
152
+ it("an inactive layer ignores Escape — and does not swallow it on the way past", () => {
153
+ const { onDismiss, action } = layer(() => false);
154
+ const stopped = vi.fn();
155
+ const event = new KeyboardEvent("keydown", {
156
+ key: "Escape",
157
+ bubbles: true,
158
+ cancelable: true,
159
+ });
160
+ event.stopImmediatePropagation = stopped;
161
+ document.dispatchEvent(event);
162
+ expect(onDismiss).not.toHaveBeenCalled();
163
+ // The guard runs BEFORE stopImmediatePropagation, deliberately: a
164
+ // layer on its way out must let the key through to whatever is
165
+ // underneath rather than eating it.
166
+ expect(stopped).not.toHaveBeenCalled();
167
+ action?.destroy?.();
168
+ });
169
+ it("Escape falls through an inactive top layer to the live one beneath it", () => {
170
+ const bottom = layer();
171
+ const top = layer(() => false);
172
+ pressEscape();
173
+ expect(bottom.onDismiss).toHaveBeenCalledTimes(1);
174
+ expect(top.onDismiss).not.toHaveBeenCalled();
175
+ top.action?.destroy?.();
176
+ bottom.action?.destroy?.();
177
+ });
178
+ it("an outside pointerdown falls through an inactive top layer the same way", () => {
179
+ const bottom = layer();
180
+ const top = layer(() => false);
181
+ const outside = document.createElement("div");
182
+ document.body.appendChild(outside);
183
+ pointerDownOn(outside);
184
+ expect(bottom.onDismiss).toHaveBeenCalledTimes(1);
185
+ expect(top.onDismiss).not.toHaveBeenCalled();
186
+ top.action?.destroy?.();
187
+ bottom.action?.destroy?.();
188
+ });
189
+ it("reads the getter fresh on every event, so a layer going inactive stops answering", () => {
190
+ let live = true;
191
+ const { onDismiss, action } = layer(() => live);
192
+ pressEscape();
193
+ expect(onDismiss).toHaveBeenCalledTimes(1);
194
+ // The close instant: `open` flips, the action is never updated again
195
+ // (its branch is inert), and the captured getter is what notices.
196
+ live = false;
197
+ pressEscape();
198
+ pressEscape();
199
+ expect(onDismiss).toHaveBeenCalledTimes(1);
200
+ action?.destroy?.();
201
+ });
202
+ it("update() swaps active in place: active → inactive → active behaves at each step", () => {
203
+ const node = document.createElement("div");
204
+ document.body.appendChild(node);
205
+ const onDismiss = vi.fn();
206
+ const action = dismissable(node, { onDismiss });
207
+ pressEscape();
208
+ expect(onDismiss).toHaveBeenCalledTimes(1);
209
+ action?.update?.({ onDismiss, active: () => false });
210
+ pressEscape();
211
+ expect(onDismiss).toHaveBeenCalledTimes(1);
212
+ action?.update?.({ onDismiss, active: () => true });
213
+ pressEscape();
214
+ expect(onDismiss).toHaveBeenCalledTimes(2);
215
+ action?.destroy?.();
216
+ });
217
+ it("omitting active is byte-identical to always-active, at the top and underneath", () => {
218
+ const bottom = layer();
219
+ const top = layer();
220
+ pressEscape();
221
+ expect(top.onDismiss).toHaveBeenCalledTimes(1);
222
+ expect(bottom.onDismiss).not.toHaveBeenCalled();
223
+ top.action?.destroy?.();
224
+ pressEscape();
225
+ expect(bottom.onDismiss).toHaveBeenCalledTimes(1);
226
+ bottom.action?.destroy?.();
227
+ });
228
+ it("an inactive layer is not top even when every layer above it is gone", () => {
229
+ const { onDismiss, action } = layer(() => false);
230
+ const outside = document.createElement("div");
231
+ document.body.appendChild(outside);
232
+ pressEscape();
233
+ pointerDownOn(outside);
234
+ expect(onDismiss).not.toHaveBeenCalled();
235
+ action?.destroy?.();
236
+ });
237
+ });
@@ -14,5 +14,41 @@ export interface FocusTrapOptions {
14
14
  * still connected — that element always wins first, unconditionally.
15
15
  */
16
16
  fallbackFocus?: () => HTMLElement | null | undefined;
17
+ /**
18
+ * Called once, synchronously, as the trap activates, with two functions:
19
+ *
20
+ * 1. `returnFocusNow` — runs the return chain IMMEDIATELY and disarms
21
+ * `destroy()`'s own return. Idempotent: calling it twice, or letting
22
+ * `destroy()` follow it, returns focus exactly once. Overlays call it
23
+ * from `onoutrostart`.
24
+ * 2. `rearm` — undoes that latch and puts focus back inside the trap.
25
+ * Overlays call it from `onintrostart`.
26
+ *
27
+ * The first exists because an animated exit delays `destroy()` by the
28
+ * length of the fade. Without it a keyboard user pressing Escape waits
29
+ * 200 ms for focus to come back — and worse, Svelte sets `inert` on the
30
+ * panel the instant the exit starts, which drops focus to `<body>` for
31
+ * that whole window. `outrostart` is the one event that fires on every
32
+ * close path (Escape, outside click, close button, a caller's own
33
+ * `bind:open` write) and fires before the node is removed.
34
+ *
35
+ * The second exists because a surface reopened DURING its exit is not a
36
+ * new mount. Svelte reverses the outro and resumes the same branch
37
+ * (`reactivity/effects.js`'s `resume_children` clears INERT and calls
38
+ * `transition.in()`; the action factory in `dom/elements/actions.js` runs
39
+ * once per mount, untracked), so this action is never re-created, the
40
+ * initial focus move never re-runs, and the latch below stays set. Without
41
+ * `rearm` the reopened panel would be `aria-modal` and interactive again
42
+ * with focus sitting on the trigger *behind* it — untrapped, because the
43
+ * Tab handler is bound to the panel — and its next dismiss would return
44
+ * focus nowhere at all, the handle and `destroy()` both being permanently
45
+ * disarmed for the life of the instance.
46
+ *
47
+ * A caller that passes both `onActivate` and `returnFocus: false` is
48
+ * contradicting itself, and the handle wins: asking for the handle IS
49
+ * asking for the return. `returnFocus: false` governs the *unmount* path
50
+ * only.
51
+ */
52
+ onActivate?: (returnFocusNow: () => void, rearm: () => void) => void;
17
53
  }
18
54
  export declare const focusTrap: Action<HTMLElement, FocusTrapOptions | undefined>;