fancy-ui-svelte 0.11.0 → 0.13.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.
- package/README.md +4 -4
- package/dist/fancy-ui/_internals/ScrollLockHarness.test.svelte +29 -0
- package/dist/fancy-ui/_internals/ScrollLockHarness.test.svelte.d.ts +3 -0
- package/dist/fancy-ui/_internals/ScrollLockPanel.test.svelte +43 -0
- package/dist/fancy-ui/_internals/ScrollLockPanel.test.svelte.d.ts +8 -0
- package/dist/fancy-ui/_internals/anchor-position.d.ts +18 -7
- package/dist/fancy-ui/_internals/anchor-position.js +45 -10
- package/dist/fancy-ui/_internals/anchor-position.test.js +48 -1
- package/dist/fancy-ui/_internals/dismissable.d.ts +16 -0
- package/dist/fancy-ui/_internals/dismissable.js +23 -6
- package/dist/fancy-ui/_internals/dismissable.test.js +115 -6
- package/dist/fancy-ui/_internals/focus-trap.d.ts +36 -0
- package/dist/fancy-ui/_internals/focus-trap.js +106 -39
- package/dist/fancy-ui/_internals/focus-trap.test.js +356 -1
- package/dist/fancy-ui/_internals/motion/AnchoredHarness.test.svelte +42 -0
- package/dist/fancy-ui/_internals/motion/AnchoredHarness.test.svelte.d.ts +11 -0
- package/dist/fancy-ui/_internals/motion/TransitionsHarness.test.svelte +32 -0
- package/dist/fancy-ui/_internals/motion/TransitionsHarness.test.svelte.d.ts +10 -0
- package/dist/fancy-ui/_internals/motion/anchored.d.ts +104 -0
- package/dist/fancy-ui/_internals/motion/anchored.js +141 -0
- package/dist/fancy-ui/_internals/motion/anchored.test.d.ts +1 -0
- package/dist/fancy-ui/_internals/motion/anchored.test.js +250 -0
- package/dist/fancy-ui/_internals/motion/haptics.d.ts +38 -0
- package/dist/fancy-ui/_internals/motion/haptics.js +59 -0
- package/dist/fancy-ui/_internals/motion/haptics.test.d.ts +1 -0
- package/dist/fancy-ui/_internals/motion/haptics.test.js +70 -0
- package/dist/fancy-ui/_internals/motion/in-view.d.ts +24 -0
- package/dist/fancy-ui/_internals/motion/in-view.js +99 -0
- package/dist/fancy-ui/_internals/motion/in-view.test.d.ts +1 -0
- package/dist/fancy-ui/_internals/motion/in-view.test.js +149 -0
- package/dist/fancy-ui/_internals/motion/media-query.svelte.d.ts +26 -0
- package/dist/fancy-ui/_internals/motion/media-query.svelte.js +69 -0
- package/dist/fancy-ui/_internals/motion/media-query.test.d.ts +1 -0
- package/dist/fancy-ui/_internals/motion/media-query.test.js +175 -0
- package/dist/fancy-ui/_internals/motion/presets.d.ts +34 -0
- package/dist/fancy-ui/_internals/motion/presets.js +33 -0
- package/dist/fancy-ui/_internals/motion/presets.test.d.ts +1 -0
- package/dist/fancy-ui/_internals/motion/presets.test.js +48 -0
- package/dist/fancy-ui/_internals/motion/raf.d.ts +35 -0
- package/dist/fancy-ui/_internals/motion/raf.js +54 -0
- package/dist/fancy-ui/_internals/motion/raf.test.d.ts +1 -0
- package/dist/fancy-ui/_internals/motion/raf.test.js +104 -0
- package/dist/fancy-ui/_internals/motion/stagger.d.ts +32 -0
- package/dist/fancy-ui/_internals/motion/stagger.js +51 -0
- package/dist/fancy-ui/_internals/motion/stagger.test.d.ts +1 -0
- package/dist/fancy-ui/_internals/motion/stagger.test.js +58 -0
- package/dist/fancy-ui/_internals/motion/tokens.d.ts +97 -0
- package/dist/fancy-ui/_internals/motion/tokens.js +83 -0
- package/dist/fancy-ui/_internals/motion/tokens.test.d.ts +1 -0
- package/dist/fancy-ui/_internals/motion/tokens.test.js +28 -0
- package/dist/fancy-ui/_internals/motion/transitions.d.ts +52 -0
- package/dist/fancy-ui/_internals/motion/transitions.js +91 -0
- package/dist/fancy-ui/_internals/motion/transitions.test.d.ts +1 -0
- package/dist/fancy-ui/_internals/motion/transitions.test.js +143 -0
- package/dist/fancy-ui/_internals/motion/types.d.ts +17 -0
- package/dist/fancy-ui/_internals/motion/types.js +15 -0
- package/dist/fancy-ui/_internals/motion/types.test.d.ts +1 -0
- package/dist/fancy-ui/_internals/motion/types.test.js +19 -0
- package/dist/fancy-ui/_internals/scroll-lock.d.ts +21 -0
- package/dist/fancy-ui/_internals/scroll-lock.js +20 -0
- package/dist/fancy-ui/_internals/scroll-lock.test.js +82 -1
- package/dist/fancy-ui/agent-plan/AgentPlan.svelte +16 -1
- package/dist/fancy-ui/agent-plan/AgentPlan.svelte.d.ts +5 -0
- package/dist/fancy-ui/agent-plan/AgentPlan.test.js +55 -1
- package/dist/fancy-ui/agent-plan/README.md +11 -0
- package/dist/fancy-ui/alert-dialog/AlertDialog.svelte +20 -0
- package/dist/fancy-ui/alert-dialog/AlertDialog.svelte.d.ts +5 -0
- package/dist/fancy-ui/alert-dialog/AlertDialog.test.js +145 -2
- package/dist/fancy-ui/alert-dialog/README.md +45 -14
- package/dist/fancy-ui/animated-beam/AnimatedBeam.svelte +4 -0
- package/dist/fancy-ui/animated-beam/AnimatedBeam.test.js +16 -0
- package/dist/fancy-ui/animated-beam/README.md +1 -0
- package/dist/fancy-ui/animated-testimonials/AnimatedTestimonials.svelte +13 -3
- package/dist/fancy-ui/animated-testimonials/AnimatedTestimonials.svelte.d.ts +5 -0
- package/dist/fancy-ui/animated-testimonials/AnimatedTestimonials.test.js +70 -0
- package/dist/fancy-ui/animated-testimonials/README.md +17 -6
- package/dist/fancy-ui/animated-tooltip/AnimatedTooltip.svelte +21 -1
- package/dist/fancy-ui/animated-tooltip/AnimatedTooltip.test.js +25 -1
- package/dist/fancy-ui/animated-tooltip/README.md +1 -0
- package/dist/fancy-ui/apple-card-carousel/AppleCard.svelte +20 -0
- package/dist/fancy-ui/apple-card-carousel/AppleCard.svelte.d.ts +5 -0
- package/dist/fancy-ui/apple-card-carousel/AppleCardCarousel.svelte +7 -1
- package/dist/fancy-ui/apple-card-carousel/AppleCardCarousel.svelte.d.ts +5 -0
- package/dist/fancy-ui/apple-card-carousel/AppleCardCarousel.test.js +111 -1
- package/dist/fancy-ui/apple-card-carousel/README.md +16 -5
- package/dist/fancy-ui/approval-card/ApprovalCard.svelte +10 -0
- package/dist/fancy-ui/approval-card/ApprovalCard.svelte.d.ts +5 -0
- package/dist/fancy-ui/approval-card/ApprovalCard.test.js +51 -1
- package/dist/fancy-ui/approval-card/README.md +30 -14
- package/dist/fancy-ui/artifact-card/ArtifactCard.svelte +19 -1
- package/dist/fancy-ui/artifact-card/ArtifactCard.svelte.d.ts +5 -0
- package/dist/fancy-ui/artifact-card/ArtifactCard.test.js +80 -1
- package/dist/fancy-ui/artifact-card/README.md +24 -13
- package/dist/fancy-ui/autocomplete/Autocomplete.svelte +16 -0
- package/dist/fancy-ui/autocomplete/Autocomplete.svelte.d.ts +5 -0
- package/dist/fancy-ui/autocomplete/Autocomplete.test.js +245 -1
- package/dist/fancy-ui/autocomplete/AutocompletePanel.svelte +60 -3
- package/dist/fancy-ui/autocomplete/AutocompletePanel.svelte.d.ts +2 -17
- package/dist/fancy-ui/autocomplete/README.md +74 -17
- package/dist/fancy-ui/autocomplete/types.d.ts +8 -0
- package/dist/fancy-ui/breadcrumb/Breadcrumb.svelte +16 -0
- package/dist/fancy-ui/breadcrumb/Breadcrumb.svelte.d.ts +6 -0
- package/dist/fancy-ui/breadcrumb/Breadcrumb.test.js +43 -2
- package/dist/fancy-ui/breadcrumb/README.md +11 -0
- package/dist/fancy-ui/button/Button.svelte +174 -9
- package/dist/fancy-ui/button/Button.test.js +117 -3
- package/dist/fancy-ui/button/ButtonLeadHarness.test.svelte +32 -0
- package/dist/fancy-ui/button/ButtonLeadHarness.test.svelte.d.ts +3 -0
- package/dist/fancy-ui/button/README.md +13 -2
- package/dist/fancy-ui/chat-error/ChatError.svelte +19 -1
- package/dist/fancy-ui/chat-error/ChatError.svelte.d.ts +6 -0
- package/dist/fancy-ui/chat-error/ChatError.test.js +40 -0
- package/dist/fancy-ui/chat-error/README.md +11 -0
- package/dist/fancy-ui/chat-message/ChatMessage.svelte +13 -0
- package/dist/fancy-ui/chat-message/ChatMessage.svelte.d.ts +9 -0
- package/dist/fancy-ui/chat-message/ChatMessageAction.svelte +10 -0
- package/dist/fancy-ui/chat-message/ChatMessageBranches.svelte +14 -3
- package/dist/fancy-ui/chat-message/ChatMessageHarness.test.svelte +3 -1
- package/dist/fancy-ui/chat-message/ChatMessageHarness.test.svelte.d.ts +1 -0
- package/dist/fancy-ui/chat-message/ChatMessageParts.test.js +68 -0
- package/dist/fancy-ui/chat-message/README.md +25 -0
- package/dist/fancy-ui/chat-message/types.d.ts +7 -0
- package/dist/fancy-ui/chat-panel/ChatPanel.svelte +9 -0
- package/dist/fancy-ui/chat-panel/ChatPanel.svelte.d.ts +6 -0
- package/dist/fancy-ui/chat-panel/ChatPanel.test.js +46 -0
- package/dist/fancy-ui/chat-panel/README.md +15 -0
- package/dist/fancy-ui/checkbox/Checkbox.svelte +139 -42
- package/dist/fancy-ui/checkbox/Checkbox.test.js +108 -0
- package/dist/fancy-ui/checkbox/README.md +70 -11
- package/dist/fancy-ui/code-diff/CodeDiff.svelte +23 -2
- package/dist/fancy-ui/code-diff/CodeDiff.svelte.d.ts +5 -0
- package/dist/fancy-ui/code-diff/CodeDiff.test.js +61 -1
- package/dist/fancy-ui/code-diff/README.md +21 -10
- package/dist/fancy-ui/combobox/Combobox.svelte +16 -0
- package/dist/fancy-ui/combobox/Combobox.svelte.d.ts +5 -0
- package/dist/fancy-ui/combobox/Combobox.test.js +210 -0
- package/dist/fancy-ui/combobox/ComboboxPanel.svelte +57 -3
- package/dist/fancy-ui/combobox/ComboboxPanel.svelte.d.ts +2 -17
- package/dist/fancy-ui/combobox/README.md +52 -0
- package/dist/fancy-ui/combobox/types.d.ts +8 -0
- package/dist/fancy-ui/command-menu/CommandMenu.svelte +122 -47
- package/dist/fancy-ui/command-menu/CommandMenu.svelte.d.ts +5 -0
- package/dist/fancy-ui/command-menu/CommandMenu.test.js +242 -4
- package/dist/fancy-ui/command-menu/README.md +43 -2
- package/dist/fancy-ui/composer/Composer.svelte +14 -0
- package/dist/fancy-ui/composer/Composer.svelte.d.ts +5 -0
- package/dist/fancy-ui/composer/Composer.test.js +107 -1
- package/dist/fancy-ui/composer/ComposerAttachment.svelte +7 -0
- package/dist/fancy-ui/composer/ComposerAttachments.test.js +49 -1
- package/dist/fancy-ui/composer/ComposerCommandMenu.svelte +5 -0
- package/dist/fancy-ui/composer/ComposerCommandMenu.test.js +57 -2
- package/dist/fancy-ui/composer/ComposerHarness.test.svelte +3 -0
- package/dist/fancy-ui/composer/ComposerHarness.test.svelte.d.ts +1 -0
- package/dist/fancy-ui/composer/ComposerModelPicker.svelte +14 -2
- package/dist/fancy-ui/composer/ComposerToolbar.test.js +64 -2
- package/dist/fancy-ui/composer/README.md +31 -0
- package/dist/fancy-ui/composer/types.d.ts +6 -0
- package/dist/fancy-ui/confetti/Confetti.test.js +55 -11
- package/dist/fancy-ui/confetti/ConfettiButton.svelte +8 -1
- package/dist/fancy-ui/confetti/ConfettiButton.svelte.d.ts +5 -0
- package/dist/fancy-ui/confetti/README.md +22 -9
- package/dist/fancy-ui/context-menu/ContextMenu.svelte +27 -3
- package/dist/fancy-ui/context-menu/ContextMenu.svelte.d.ts +5 -0
- package/dist/fancy-ui/context-menu/ContextMenu.test.js +324 -0
- package/dist/fancy-ui/context-menu/ContextMenuContent.svelte +77 -22
- package/dist/fancy-ui/context-menu/ContextMenuHarness.test.svelte +3 -1
- package/dist/fancy-ui/context-menu/ContextMenuHarness.test.svelte.d.ts +1 -0
- package/dist/fancy-ui/context-menu/README.md +86 -2
- package/dist/fancy-ui/context-menu/types.d.ts +5 -3
- package/dist/fancy-ui/context-ring/ContextRing.svelte +24 -3
- package/dist/fancy-ui/context-ring/ContextRing.svelte.d.ts +5 -0
- package/dist/fancy-ui/context-ring/ContextRing.test.js +75 -1
- package/dist/fancy-ui/context-ring/README.md +11 -0
- package/dist/fancy-ui/copy-button/CopyButton.svelte +171 -52
- package/dist/fancy-ui/copy-button/CopyButton.svelte.d.ts +6 -4
- package/dist/fancy-ui/copy-button/CopyButton.test.js +237 -16
- package/dist/fancy-ui/copy-button/README.md +111 -44
- package/dist/fancy-ui/date-picker/DatePicker.svelte +74 -22
- package/dist/fancy-ui/date-picker/DatePicker.svelte.d.ts +5 -0
- package/dist/fancy-ui/date-picker/DatePicker.test.js +230 -0
- package/dist/fancy-ui/date-picker/README.md +50 -0
- package/dist/fancy-ui/dialog/Dialog.svelte +16 -0
- package/dist/fancy-ui/dialog/Dialog.svelte.d.ts +5 -0
- package/dist/fancy-ui/dialog/Dialog.test.js +234 -3
- package/dist/fancy-ui/dialog/DialogSurface.svelte +101 -48
- package/dist/fancy-ui/dialog/README.md +43 -16
- package/dist/fancy-ui/dim-siblings/DimSiblings.svelte +184 -0
- package/dist/fancy-ui/dim-siblings/DimSiblings.svelte.d.ts +35 -0
- package/dist/fancy-ui/dim-siblings/DimSiblings.test.d.ts +1 -0
- package/dist/fancy-ui/dim-siblings/DimSiblings.test.js +127 -0
- package/dist/fancy-ui/dim-siblings/DimSiblingsHarness.test.svelte +18 -0
- package/dist/fancy-ui/dim-siblings/DimSiblingsHarness.test.svelte.d.ts +18 -0
- package/dist/fancy-ui/dim-siblings/README.md +77 -0
- package/dist/fancy-ui/dim-siblings/index.d.ts +3 -0
- package/dist/fancy-ui/dim-siblings/index.js +2 -0
- package/dist/fancy-ui/displacement-text/DisplacementText.svelte +6 -1
- package/dist/fancy-ui/displacement-text/DisplacementText.test.js +5 -1
- package/dist/fancy-ui/displacement-text/README.md +1 -0
- package/dist/fancy-ui/dock/Dock.svelte +44 -4
- package/dist/fancy-ui/dock/Dock.test.js +133 -2
- package/dist/fancy-ui/dock/DockHarness.test.svelte +36 -0
- package/dist/fancy-ui/dock/DockHarness.test.svelte.d.ts +8 -0
- package/dist/fancy-ui/dock/DockIcon.svelte +6 -0
- package/dist/fancy-ui/dock/README.md +49 -22
- package/dist/fancy-ui/dock/types.d.ts +8 -0
- package/dist/fancy-ui/drawer/Drawer.svelte +176 -50
- package/dist/fancy-ui/drawer/Drawer.svelte.d.ts +12 -0
- package/dist/fancy-ui/drawer/Drawer.test.js +228 -10
- package/dist/fancy-ui/drawer/DrawerHarness.test.svelte +6 -2
- package/dist/fancy-ui/drawer/DrawerHarness.test.svelte.d.ts +1 -0
- package/dist/fancy-ui/drawer/README.md +70 -26
- package/dist/fancy-ui/dropdown-menu/DropdownMenu.test.js +295 -8
- package/dist/fancy-ui/dropdown-menu/DropdownMenuContent.svelte +67 -21
- package/dist/fancy-ui/dropdown-menu/DropdownMenuSub.svelte +14 -3
- package/dist/fancy-ui/dropdown-menu/DropdownMenuSubContent.svelte +68 -20
- package/dist/fancy-ui/dropdown-menu/DropdownMenuSubTrigger.svelte +1 -0
- package/dist/fancy-ui/dropdown-menu/README.md +80 -5
- package/dist/fancy-ui/dropdown-menu/types.d.ts +26 -5
- package/dist/fancy-ui/editorial-engine/EditorialEngine.svelte +37 -15
- package/dist/fancy-ui/editorial-engine/EditorialEngine.test.js +34 -2
- package/dist/fancy-ui/editorial-engine/README.md +9 -0
- package/dist/fancy-ui/file-upload/FileUpload.svelte +137 -15
- package/dist/fancy-ui/file-upload/FileUpload.svelte.d.ts +5 -0
- package/dist/fancy-ui/file-upload/FileUpload.test.js +302 -1
- package/dist/fancy-ui/file-upload/README.md +69 -5
- package/dist/fancy-ui/fireworks-hdr/FireworksHdr.svelte +12 -2
- package/dist/fancy-ui/fireworks-hdr/FireworksHdr.test.js +70 -0
- package/dist/fancy-ui/fireworks-hdr/README.md +4 -2
- package/dist/fancy-ui/fluid-cursor/FluidCursor.svelte +97 -11
- package/dist/fancy-ui/fluid-cursor/README.md +3 -1
- package/dist/fancy-ui/fluid-cursor/fluid-shared.d.ts +18 -0
- package/dist/fancy-ui/fluid-cursor/fluid-shared.js +22 -0
- package/dist/fancy-ui/fluid-cursor/fluid-shared.test.js +17 -1
- package/dist/fancy-ui/fluid-cursor/webgpu-engine.d.ts +6 -0
- package/dist/fancy-ui/fluid-cursor/webgpu-engine.js +32 -9
- package/dist/fancy-ui/form-field/FormField.svelte +38 -2
- package/dist/fancy-ui/form-field/FormField.test.js +118 -1
- package/dist/fancy-ui/form-field/README.md +26 -0
- package/dist/fancy-ui/gradient-button/GradientButton.svelte +14 -0
- package/dist/fancy-ui/gradient-button/GradientButton.svelte.d.ts +5 -0
- package/dist/fancy-ui/gradient-button/GradientButton.test.js +40 -2
- package/dist/fancy-ui/gradient-button/README.md +10 -0
- package/dist/fancy-ui/hover-card/HoverCard.svelte +76 -29
- package/dist/fancy-ui/hover-card/HoverCard.test.js +227 -8
- package/dist/fancy-ui/hover-card/README.md +47 -1
- package/dist/fancy-ui/icon-button/IconButton.svelte +7 -0
- package/dist/fancy-ui/icon-button/IconButton.svelte.d.ts +5 -0
- package/dist/fancy-ui/icon-button/IconButton.test.js +40 -1
- package/dist/fancy-ui/icon-button/README.md +13 -0
- package/dist/fancy-ui/image-generation/ImageGeneration.svelte +18 -1
- package/dist/fancy-ui/image-generation/ImageGeneration.svelte.d.ts +6 -0
- package/dist/fancy-ui/image-generation/ImageGeneration.test.js +46 -0
- package/dist/fancy-ui/image-generation/README.md +11 -0
- package/dist/fancy-ui/index.d.ts +13 -0
- package/dist/fancy-ui/index.js +14 -0
- package/dist/fancy-ui/interactive-hover-button/InteractiveHoverButton.svelte +26 -3
- package/dist/fancy-ui/interactive-hover-button/InteractiveHoverButton.svelte.d.ts +5 -0
- package/dist/fancy-ui/interactive-hover-button/InteractiveHoverButton.test.js +69 -2
- package/dist/fancy-ui/interactive-hover-button/README.md +30 -5
- package/dist/fancy-ui/line-hover-link/LineHoverLink.svelte +181 -51
- package/dist/fancy-ui/line-hover-link/LineHoverLink.svelte.d.ts +5 -0
- package/dist/fancy-ui/line-hover-link/LineHoverLink.test.js +69 -2
- package/dist/fancy-ui/line-hover-link/README.md +42 -8
- package/dist/fancy-ui/line-reveal/LineReveal.svelte +10 -6
- package/dist/fancy-ui/line-reveal/LineReveal.test.js +20 -16
- package/dist/fancy-ui/link/Link.svelte +54 -1
- package/dist/fancy-ui/link/Link.svelte.d.ts +5 -0
- package/dist/fancy-ui/link/Link.test.js +67 -1
- package/dist/fancy-ui/link/README.md +35 -0
- package/dist/fancy-ui/magnetic/Magnetic.svelte +337 -0
- package/dist/fancy-ui/magnetic/Magnetic.svelte.d.ts +40 -0
- package/dist/fancy-ui/magnetic/Magnetic.test.d.ts +1 -0
- package/dist/fancy-ui/magnetic/Magnetic.test.js +354 -0
- package/dist/fancy-ui/magnetic/README.md +175 -0
- package/dist/fancy-ui/magnetic/index.d.ts +2 -0
- package/dist/fancy-ui/magnetic/index.js +2 -0
- package/dist/fancy-ui/mosaic-glow/MosaicGlow.ssr.test.d.ts +1 -0
- package/dist/fancy-ui/mosaic-glow/MosaicGlow.ssr.test.js +22 -0
- package/dist/fancy-ui/mosaic-glow/MosaicGlow.svelte +528 -0
- package/dist/fancy-ui/mosaic-glow/MosaicGlow.svelte.d.ts +53 -0
- package/dist/fancy-ui/mosaic-glow/MosaicGlow.test.d.ts +1 -0
- package/dist/fancy-ui/mosaic-glow/MosaicGlow.test.js +319 -0
- package/dist/fancy-ui/mosaic-glow/README.md +60 -0
- package/dist/fancy-ui/mosaic-glow/index.d.ts +2 -0
- package/dist/fancy-ui/mosaic-glow/index.js +2 -0
- package/dist/fancy-ui/mosaic-glow/mosaic-glow-core.d.ts +87 -0
- package/dist/fancy-ui/mosaic-glow/mosaic-glow-core.js +221 -0
- package/dist/fancy-ui/mosaic-glow/mosaic-glow-core.test.d.ts +1 -0
- package/dist/fancy-ui/mosaic-glow/mosaic-glow-core.test.js +201 -0
- package/dist/fancy-ui/navbar/Navbar.test.js +76 -1
- package/dist/fancy-ui/navbar/NavbarLink.svelte +55 -1
- package/dist/fancy-ui/navbar/NavbarLink.svelte.d.ts +5 -0
- package/dist/fancy-ui/navbar/README.md +36 -0
- package/dist/fancy-ui/navigation-menu/NavigationMenu.test.js +151 -12
- package/dist/fancy-ui/navigation-menu/NavigationMenuContent.svelte +74 -26
- package/dist/fancy-ui/navigation-menu/README.md +58 -0
- package/dist/fancy-ui/number-input/NumberInput.svelte +110 -3
- package/dist/fancy-ui/number-input/NumberInput.svelte.d.ts +5 -0
- package/dist/fancy-ui/number-input/NumberInput.test.js +234 -0
- package/dist/fancy-ui/number-input/README.md +65 -0
- package/dist/fancy-ui/number-ticker/NumberTicker.svelte +18 -3
- package/dist/fancy-ui/number-ticker/NumberTicker.test.js +94 -0
- package/dist/fancy-ui/pagination/Pagination.svelte +63 -1
- package/dist/fancy-ui/pagination/Pagination.svelte.d.ts +5 -0
- package/dist/fancy-ui/pagination/Pagination.test.js +90 -2
- package/dist/fancy-ui/pagination/PaginationHarness.test.svelte +3 -0
- package/dist/fancy-ui/pagination/PaginationHarness.test.svelte.d.ts +1 -0
- package/dist/fancy-ui/pagination/README.md +38 -0
- package/dist/fancy-ui/password-input/PasswordInput.svelte +109 -9
- package/dist/fancy-ui/password-input/PasswordInput.svelte.d.ts +5 -0
- package/dist/fancy-ui/password-input/PasswordInput.test.js +187 -0
- package/dist/fancy-ui/password-input/README.md +52 -0
- package/dist/fancy-ui/popover/Popover.svelte +17 -0
- package/dist/fancy-ui/popover/Popover.svelte.d.ts +5 -0
- package/dist/fancy-ui/popover/Popover.test.js +361 -2
- package/dist/fancy-ui/popover/PopoverContent.svelte +86 -22
- package/dist/fancy-ui/popover/README.md +70 -12
- package/dist/fancy-ui/popover/types.d.ts +11 -0
- package/dist/fancy-ui/presence/Presence.svelte +162 -0
- package/dist/fancy-ui/presence/Presence.svelte.d.ts +29 -0
- package/dist/fancy-ui/presence/Presence.test.d.ts +1 -0
- package/dist/fancy-ui/presence/Presence.test.js +274 -0
- package/dist/fancy-ui/presence/PresenceHarness.test.svelte +23 -0
- package/dist/fancy-ui/presence/PresenceHarness.test.svelte.d.ts +7 -0
- package/dist/fancy-ui/presence/README.md +121 -0
- package/dist/fancy-ui/presence/index.d.ts +2 -0
- package/dist/fancy-ui/presence/index.js +1 -0
- package/dist/fancy-ui/pressable/Pressable.svelte +217 -0
- package/dist/fancy-ui/pressable/Pressable.svelte.d.ts +32 -0
- package/dist/fancy-ui/pressable/Pressable.test.d.ts +1 -0
- package/dist/fancy-ui/pressable/Pressable.test.js +285 -0
- package/dist/fancy-ui/pressable/README.md +80 -0
- package/dist/fancy-ui/pressable/index.d.ts +3 -0
- package/dist/fancy-ui/pressable/index.js +2 -0
- package/dist/fancy-ui/prompt-suggestions/PromptSuggestions.svelte +14 -1
- package/dist/fancy-ui/prompt-suggestions/PromptSuggestions.svelte.d.ts +5 -0
- package/dist/fancy-ui/prompt-suggestions/PromptSuggestions.test.js +41 -1
- package/dist/fancy-ui/prompt-suggestions/README.md +11 -0
- package/dist/fancy-ui/pulse-beam/PulseBeam.ssr.test.d.ts +1 -0
- package/dist/fancy-ui/pulse-beam/PulseBeam.ssr.test.js +15 -0
- package/dist/fancy-ui/pulse-beam/PulseBeam.svelte +373 -0
- package/dist/fancy-ui/pulse-beam/PulseBeam.svelte.d.ts +50 -0
- package/dist/fancy-ui/pulse-beam/PulseBeam.test.d.ts +1 -0
- package/dist/fancy-ui/pulse-beam/PulseBeam.test.js +254 -0
- package/dist/fancy-ui/pulse-beam/README.md +72 -0
- package/dist/fancy-ui/pulse-beam/index.d.ts +2 -0
- package/dist/fancy-ui/pulse-beam/index.js +2 -0
- package/dist/fancy-ui/pulse-beam/pulse-beam-data.d.ts +81 -0
- package/dist/fancy-ui/pulse-beam/pulse-beam-data.js +331 -0
- package/dist/fancy-ui/pulse-beam/pulse-beam-data.test.d.ts +1 -0
- package/dist/fancy-ui/pulse-beam/pulse-beam-data.test.js +155 -0
- package/dist/fancy-ui/pulse-beam/pulse-beam-loop.d.ts +26 -0
- package/dist/fancy-ui/pulse-beam/pulse-beam-loop.js +84 -0
- package/dist/fancy-ui/radio-group/README.md +27 -1
- package/dist/fancy-ui/radio-group/RadioGroup.test.js +43 -0
- package/dist/fancy-ui/radio-group/RadioGroupItem.svelte +31 -2
- package/dist/fancy-ui/rainbow-button/README.md +23 -9
- package/dist/fancy-ui/rainbow-button/RainbowButton.svelte +25 -1
- package/dist/fancy-ui/rainbow-button/RainbowButton.svelte.d.ts +5 -0
- package/dist/fancy-ui/rainbow-button/RainbowButton.test.js +49 -2
- package/dist/fancy-ui/reasoning-panel/README.md +11 -0
- package/dist/fancy-ui/reasoning-panel/ReasoningPanel.svelte +13 -1
- package/dist/fancy-ui/reasoning-panel/ReasoningPanel.svelte.d.ts +5 -0
- package/dist/fancy-ui/reasoning-panel/ReasoningPanel.test.js +47 -1
- package/dist/fancy-ui/recommendation-card/README.md +25 -14
- package/dist/fancy-ui/recommendation-card/RecommendationCard.svelte +10 -0
- package/dist/fancy-ui/recommendation-card/RecommendationCard.svelte.d.ts +5 -0
- package/dist/fancy-ui/recommendation-card/RecommendationCard.test.js +39 -1
- package/dist/fancy-ui/registry.d.ts +7 -0
- package/dist/fancy-ui/registry.js +1278 -99
- package/dist/fancy-ui/registry.test.d.ts +1 -0
- package/dist/fancy-ui/registry.test.js +32 -0
- package/dist/fancy-ui/reveal/README.md +186 -0
- package/dist/fancy-ui/reveal/Reveal.svelte +353 -0
- package/dist/fancy-ui/reveal/Reveal.svelte.d.ts +41 -0
- package/dist/fancy-ui/reveal/Reveal.test.d.ts +1 -0
- package/dist/fancy-ui/reveal/Reveal.test.js +421 -0
- package/dist/fancy-ui/reveal/RevealHarness.test.svelte +49 -0
- package/dist/fancy-ui/reveal/RevealHarness.test.svelte.d.ts +9 -0
- package/dist/fancy-ui/reveal/index.d.ts +2 -0
- package/dist/fancy-ui/reveal/index.js +1 -0
- package/dist/fancy-ui/ripple-button/README.md +17 -6
- package/dist/fancy-ui/ripple-button/RippleButton.svelte +10 -1
- package/dist/fancy-ui/ripple-button/RippleButton.svelte.d.ts +5 -0
- package/dist/fancy-ui/ripple-button/RippleButton.test.js +81 -2
- package/dist/fancy-ui/scroll-anchor/README.md +11 -0
- package/dist/fancy-ui/scroll-anchor/ScrollAnchor.svelte +9 -0
- package/dist/fancy-ui/scroll-anchor/ScrollAnchor.svelte.d.ts +6 -0
- package/dist/fancy-ui/scroll-anchor/ScrollAnchor.test.js +39 -0
- package/dist/fancy-ui/scroll-progress/README.md +73 -0
- package/dist/fancy-ui/scroll-progress/ScrollProgress.svelte +291 -0
- package/dist/fancy-ui/scroll-progress/ScrollProgress.svelte.d.ts +38 -0
- package/dist/fancy-ui/scroll-progress/ScrollProgress.test.d.ts +1 -0
- package/dist/fancy-ui/scroll-progress/ScrollProgress.test.js +275 -0
- package/dist/fancy-ui/scroll-progress/index.d.ts +3 -0
- package/dist/fancy-ui/scroll-progress/index.js +2 -0
- package/dist/fancy-ui/search-input/README.md +44 -0
- package/dist/fancy-ui/search-input/SearchInput.svelte +31 -0
- package/dist/fancy-ui/search-input/SearchInput.svelte.d.ts +5 -0
- package/dist/fancy-ui/search-input/SearchInput.test.js +131 -0
- package/dist/fancy-ui/select/README.md +32 -0
- package/dist/fancy-ui/select/Select.svelte +6 -0
- package/dist/fancy-ui/select/Select.test.js +162 -4
- package/dist/fancy-ui/select/SelectPanel.svelte +56 -18
- package/dist/fancy-ui/select/types.d.ts +14 -0
- package/dist/fancy-ui/sheet/README.md +63 -27
- package/dist/fancy-ui/sheet/Sheet.svelte +150 -85
- package/dist/fancy-ui/sheet/Sheet.svelte.d.ts +12 -0
- package/dist/fancy-ui/sheet/Sheet.test.js +192 -8
- package/dist/fancy-ui/sheet/SheetHarness.test.svelte +3 -2
- package/dist/fancy-ui/sheet/SheetHarness.test.svelte.d.ts +1 -0
- package/dist/fancy-ui/shimmer-button/README.md +10 -0
- package/dist/fancy-ui/shimmer-button/ShimmerButton.svelte +14 -0
- package/dist/fancy-ui/shimmer-button/ShimmerButton.svelte.d.ts +5 -0
- package/dist/fancy-ui/shimmer-button/ShimmerButton.test.js +39 -2
- package/dist/fancy-ui/sidebar/README.md +42 -0
- package/dist/fancy-ui/sidebar/Sidebar.svelte +14 -1
- package/dist/fancy-ui/sidebar/Sidebar.test.js +94 -1
- package/dist/fancy-ui/sidebar/SidebarItem.svelte +51 -1
- package/dist/fancy-ui/sidebar/SidebarItem.svelte.d.ts +5 -0
- package/dist/fancy-ui/skeleton/README.md +166 -0
- package/dist/fancy-ui/skeleton/Skeleton.svelte +390 -0
- package/dist/fancy-ui/skeleton/Skeleton.svelte.d.ts +38 -0
- package/dist/fancy-ui/skeleton/Skeleton.test.d.ts +1 -0
- package/dist/fancy-ui/skeleton/Skeleton.test.js +347 -0
- package/dist/fancy-ui/skeleton/SkeletonRevealHarness.test.svelte +48 -0
- package/dist/fancy-ui/skeleton/SkeletonRevealHarness.test.svelte.d.ts +11 -0
- package/dist/fancy-ui/skeleton/index.d.ts +3 -0
- package/dist/fancy-ui/skeleton/index.js +2 -0
- package/dist/fancy-ui/slider/README.md +19 -0
- package/dist/fancy-ui/slider/Slider.svelte +19 -0
- package/dist/fancy-ui/slider/Slider.svelte.d.ts +5 -0
- package/dist/fancy-ui/slider/Slider.test.js +37 -0
- package/dist/fancy-ui/smooth-cursor/SmoothCursor.svelte +2 -1
- package/dist/fancy-ui/smooth-cursor/SmoothCursor.test.js +6 -0
- package/dist/fancy-ui/sound/README.md +72 -13
- package/dist/fancy-ui/sources/README.md +12 -1
- package/dist/fancy-ui/sources/Sources.svelte +9 -0
- package/dist/fancy-ui/sources/Sources.svelte.d.ts +6 -0
- package/dist/fancy-ui/sources/Sources.test.js +34 -0
- package/dist/fancy-ui/sources/SourcesHarness.test.svelte +10 -2
- package/dist/fancy-ui/sources/SourcesHarness.test.svelte.d.ts +1 -0
- package/dist/fancy-ui/sparkles/Sparkles.svelte +6 -3
- package/dist/fancy-ui/sparkles/Sparkles.test.js +51 -0
- package/dist/fancy-ui/status-morph/README.md +175 -0
- package/dist/fancy-ui/status-morph/StatusMorph.svelte +449 -0
- package/dist/fancy-ui/status-morph/StatusMorph.svelte.d.ts +45 -0
- package/dist/fancy-ui/status-morph/StatusMorph.test.d.ts +1 -0
- package/dist/fancy-ui/status-morph/StatusMorph.test.js +243 -0
- package/dist/fancy-ui/status-morph/StatusMorphHarness.test.svelte +33 -0
- package/dist/fancy-ui/status-morph/StatusMorphHarness.test.svelte.d.ts +7 -0
- package/dist/fancy-ui/status-morph/index.d.ts +3 -0
- package/dist/fancy-ui/status-morph/index.js +2 -0
- package/dist/fancy-ui/stepper/README.md +40 -0
- package/dist/fancy-ui/stepper/Step.svelte +26 -0
- package/dist/fancy-ui/stepper/Stepper.svelte +9 -0
- package/dist/fancy-ui/stepper/Stepper.svelte.d.ts +5 -0
- package/dist/fancy-ui/stepper/Stepper.test.js +98 -1
- package/dist/fancy-ui/stepper/StepperHarness.test.svelte +11 -1
- package/dist/fancy-ui/stepper/StepperHarness.test.svelte.d.ts +1 -0
- package/dist/fancy-ui/sticky-scroll/README.md +91 -0
- package/dist/fancy-ui/sticky-scroll/StickyScroll.svelte +255 -0
- package/dist/fancy-ui/sticky-scroll/StickyScroll.svelte.d.ts +62 -0
- package/dist/fancy-ui/sticky-scroll/StickyScroll.test.d.ts +1 -0
- package/dist/fancy-ui/sticky-scroll/StickyScroll.test.js +267 -0
- package/dist/fancy-ui/sticky-scroll/StickyScrollHarness.test.svelte +73 -0
- package/dist/fancy-ui/sticky-scroll/StickyScrollHarness.test.svelte.d.ts +12 -0
- package/dist/fancy-ui/sticky-scroll/index.d.ts +3 -0
- package/dist/fancy-ui/sticky-scroll/index.js +2 -0
- package/dist/fancy-ui/subagent-list/README.md +11 -0
- package/dist/fancy-ui/subagent-list/SubagentList.svelte +15 -1
- package/dist/fancy-ui/subagent-list/SubagentList.svelte.d.ts +5 -0
- package/dist/fancy-ui/subagent-list/SubagentList.test.js +50 -1
- package/dist/fancy-ui/switch/README.md +27 -6
- package/dist/fancy-ui/switch/Switch.svelte +18 -4
- package/dist/fancy-ui/switch/Switch.test.js +41 -0
- package/dist/fancy-ui/tabs/README.md +141 -11
- package/dist/fancy-ui/tabs/Tabs.svelte +9 -0
- package/dist/fancy-ui/tabs/Tabs.svelte.d.ts +5 -0
- package/dist/fancy-ui/tabs/Tabs.test.js +407 -2
- package/dist/fancy-ui/tabs/TabsContent.svelte +34 -0
- package/dist/fancy-ui/tabs/TabsHarness.test.svelte +3 -1
- package/dist/fancy-ui/tabs/TabsHarness.test.svelte.d.ts +1 -0
- package/dist/fancy-ui/tabs/TabsList.svelte +330 -0
- package/dist/fancy-ui/tabs/TabsTrigger.svelte +46 -23
- package/dist/fancy-ui/terminal-text/README.md +1 -1
- package/dist/fancy-ui/terminal-text/TerminalText.svelte +31 -2
- package/dist/fancy-ui/terminal-text/TerminalText.test.js +70 -0
- package/dist/fancy-ui/terminal-text/TerminalTextHarness.test.svelte +36 -0
- package/dist/fancy-ui/terminal-text/TerminalTextHarness.test.svelte.d.ts +6 -0
- package/dist/fancy-ui/text-roll/README.md +152 -0
- package/dist/fancy-ui/text-roll/TextRoll.svelte +455 -0
- package/dist/fancy-ui/text-roll/TextRoll.svelte.d.ts +53 -0
- package/dist/fancy-ui/text-roll/TextRoll.test.d.ts +1 -0
- package/dist/fancy-ui/text-roll/TextRoll.test.js +574 -0
- package/dist/fancy-ui/text-roll/TextRollHarness.test.svelte +49 -0
- package/dist/fancy-ui/text-roll/TextRollHarness.test.svelte.d.ts +7 -0
- package/dist/fancy-ui/text-roll/index.d.ts +2 -0
- package/dist/fancy-ui/text-roll/index.js +2 -0
- package/dist/fancy-ui/text-roll/roll-transitions.d.ts +74 -0
- package/dist/fancy-ui/text-roll/roll-transitions.js +66 -0
- package/dist/fancy-ui/thread-list/README.md +11 -0
- package/dist/fancy-ui/thread-list/ThreadList.svelte +15 -1
- package/dist/fancy-ui/thread-list/ThreadList.svelte.d.ts +6 -0
- package/dist/fancy-ui/thread-list/ThreadList.test.js +43 -0
- package/dist/fancy-ui/thread-list/ThreadListHarness.test.svelte +3 -0
- package/dist/fancy-ui/thread-list/ThreadListHarness.test.svelte.d.ts +1 -0
- package/dist/fancy-ui/time-picker/README.md +51 -1
- package/dist/fancy-ui/time-picker/TimePicker.svelte +47 -10
- package/dist/fancy-ui/time-picker/TimePicker.svelte.d.ts +5 -0
- package/dist/fancy-ui/time-picker/TimePicker.test.js +254 -1
- package/dist/fancy-ui/time-picker/TimePickerPanel.svelte +55 -20
- package/dist/fancy-ui/time-picker/types.d.ts +8 -0
- package/dist/fancy-ui/toast/README.md +62 -6
- package/dist/fancy-ui/toast/Toast.svelte +32 -15
- package/dist/fancy-ui/toast/Toast.test.js +243 -4
- package/dist/fancy-ui/toast/Toaster.svelte +48 -0
- package/dist/fancy-ui/toast/Toaster.svelte.d.ts +6 -0
- package/dist/fancy-ui/toggle/README.md +48 -11
- package/dist/fancy-ui/toggle/Toggle.svelte +89 -10
- package/dist/fancy-ui/toggle/Toggle.svelte.d.ts +5 -0
- package/dist/fancy-ui/toggle/Toggle.test.js +78 -1
- package/dist/fancy-ui/toggle-group/README.md +50 -12
- package/dist/fancy-ui/toggle-group/ToggleGroup.svelte +8 -0
- package/dist/fancy-ui/toggle-group/ToggleGroup.svelte.d.ts +5 -0
- package/dist/fancy-ui/toggle-group/ToggleGroup.test.js +97 -1
- package/dist/fancy-ui/toggle-group/ToggleGroupHarness.test.svelte +3 -1
- package/dist/fancy-ui/toggle-group/ToggleGroupHarness.test.svelte.d.ts +1 -0
- package/dist/fancy-ui/toggle-group/ToggleGroupItem.svelte +53 -1
- package/dist/fancy-ui/tool-call/README.md +21 -10
- package/dist/fancy-ui/tool-call/ToolCall.svelte +13 -1
- package/dist/fancy-ui/tool-call/ToolCall.svelte.d.ts +5 -0
- package/dist/fancy-ui/tool-call/ToolCall.test.js +50 -1
- package/dist/fancy-ui/tool-timeline/README.md +11 -0
- package/dist/fancy-ui/tool-timeline/ToolTimeline.svelte +15 -1
- package/dist/fancy-ui/tool-timeline/ToolTimeline.svelte.d.ts +5 -0
- package/dist/fancy-ui/tool-timeline/ToolTimeline.test.js +56 -2
- package/dist/fancy-ui/tooltip/README.md +26 -0
- package/dist/fancy-ui/tooltip/Tooltip.svelte +48 -20
- package/dist/fancy-ui/tooltip/Tooltip.test.js +118 -0
- package/dist/fancy-ui/voice-input/README.md +25 -13
- package/dist/fancy-ui/voice-input/VoiceInput.svelte +42 -1
- package/dist/fancy-ui/voice-input/VoiceInput.svelte.d.ts +5 -0
- package/dist/fancy-ui/voice-input/VoiceInput.test.js +99 -1
- package/dist/fancy-ui/voice-input/VoiceInputHarness.test.svelte +3 -1
- package/dist/fancy-ui/voice-input/VoiceInputHarness.test.svelte.d.ts +1 -0
- package/dist/fancy-ui/web-search/README.md +11 -0
- package/dist/fancy-ui/web-search/WebSearch.svelte +22 -2
- package/dist/fancy-ui/web-search/WebSearch.svelte.d.ts +5 -0
- package/dist/fancy-ui/web-search/WebSearch.test.js +60 -1
- package/package.json +3 -2
- package/dist/fancy-ui/themes.d.ts +0 -38
- package/dist/fancy-ui/themes.js +0 -356
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ Beautiful animation and UI components for **Svelte 5**.
|
|
|
9
9
|

|
|
10
10
|

|
|
11
11
|

|
|
12
|
-

|
|
13
13
|

|
|
14
14
|
|
|
15
15
|
<p align="center">
|
|
@@ -36,10 +36,10 @@ Beautiful animation and UI components for **Svelte 5**.
|
|
|
36
36
|
- **Svelte 5 Runes** — Built with `$state`, `$derived`, `$effect`, and `$props`
|
|
37
37
|
- **Tailwind CSS 4** — Utility-first styling with theme tokens
|
|
38
38
|
- **TypeScript** — Fully typed props and events
|
|
39
|
-
- **
|
|
39
|
+
- **145 Components** — Core primitives (forms, navigation, overlays) alongside buttons, text animations, backgrounds, effects and a micro-interactions collection
|
|
40
40
|
- **Dark Mode** — All components support light and dark themes
|
|
41
41
|
- **Opt-in Sound** — Eleven synthesised interface cues, a `SoundToggle` switch and a `soundFeedback` action; silent until the user turns it on
|
|
42
|
-
- **Tested** —
|
|
42
|
+
- **Tested** — 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!
|
|
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,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
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
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
|
|
49
|
-
* discarded here, and a consumer has no way to learn a flip
|
|
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:
|
|
93
|
-
y:
|
|
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
|
|
111
|
-
// recomputes to the same
|
|
112
|
-
// yet", which is distinct from any real
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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>;
|