@yewdiel21/motion-kit 0.0.0-stage → 0.9.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/NOTICE +38 -0
- package/README.md +80 -2
- package/bin/motion-kit.mjs +242 -0
- package/css.d.ts +2 -0
- package/dist/auto/index.d.ts +63 -0
- package/dist/auto/index.js +207 -0
- package/dist/auto/index.js.map +1 -0
- package/dist/chunk-2BQGFOA7.js +177 -0
- package/dist/chunk-2BQGFOA7.js.map +1 -0
- package/dist/chunk-34XYOXHG.js +2374 -0
- package/dist/chunk-34XYOXHG.js.map +1 -0
- package/dist/chunk-6OJAP6ZL.js +832 -0
- package/dist/chunk-6OJAP6ZL.js.map +1 -0
- package/dist/chunk-ABNRDDO7.js +28 -0
- package/dist/chunk-ABNRDDO7.js.map +1 -0
- package/dist/chunk-FS2HJVVY.js +3320 -0
- package/dist/chunk-FS2HJVVY.js.map +1 -0
- package/dist/chunk-J27KH2A2.js +157 -0
- package/dist/chunk-J27KH2A2.js.map +1 -0
- package/dist/chunk-KLY5BRRI.js +18 -0
- package/dist/chunk-KLY5BRRI.js.map +1 -0
- package/dist/chunk-Q4LVFNLN.js +1617 -0
- package/dist/chunk-Q4LVFNLN.js.map +1 -0
- package/dist/chunk-RFDIZYFX.js +304 -0
- package/dist/chunk-RFDIZYFX.js.map +1 -0
- package/dist/chunk-TGV27ZWM.js +17 -0
- package/dist/chunk-TGV27ZWM.js.map +1 -0
- package/dist/confetti-BSX1CLLV.d.ts +41 -0
- package/dist/css/components/chat-background.css +87 -0
- package/dist/css/components/chat.css +237 -0
- package/dist/css/components/cursor.css +178 -0
- package/dist/css/components/dialog.css +259 -0
- package/dist/css/components/island.css +301 -0
- package/dist/css/components/menu.css +105 -0
- package/dist/css/components/nav.css +1054 -0
- package/dist/css/components/steps.css +50 -0
- package/dist/css/components/theme-picker.css +115 -0
- package/dist/css/components/toast.css +115 -0
- package/dist/css/controls.css +950 -0
- package/dist/css/effects.css +587 -0
- package/dist/css/fonts.css +3 -0
- package/dist/css/themes/black-green.css +126 -0
- package/dist/css/themes/black.css +126 -0
- package/dist/css/themes/blue.css +126 -0
- package/dist/css/themes/brown.css +126 -0
- package/dist/css/themes/cold-blue.css +126 -0
- package/dist/css/themes/funny.css +126 -0
- package/dist/css/themes/gray.css +126 -0
- package/dist/css/themes/green.css +126 -0
- package/dist/css/themes/modern-1.css +126 -0
- package/dist/css/themes/neogreen.css +126 -0
- package/dist/css/themes/new.css +126 -0
- package/dist/css/themes/new2.css +126 -0
- package/dist/css/themes/oled.css +126 -0
- package/dist/css/themes/orange.css +126 -0
- package/dist/css/themes/pastel-pink.css +126 -0
- package/dist/css/themes/pink.css +126 -0
- package/dist/css/themes/purple.css +126 -0
- package/dist/css/themes/red.css +126 -0
- package/dist/css/themes/super-blue.css +126 -0
- package/dist/css/themes.css +2431 -0
- package/dist/css/tokens.css +452 -0
- package/dist/elements/index.d.ts +183 -0
- package/dist/elements/index.js +472 -0
- package/dist/elements/index.js.map +1 -0
- package/dist/engine-CUr9QijM.d.ts +46 -0
- package/dist/gl/index.d.ts +61 -0
- package/dist/gl/index.js +18 -0
- package/dist/gl/index.js.map +1 -0
- package/dist/index.css +5296 -0
- package/dist/index.d.ts +440 -0
- package/dist/index.js +390 -0
- package/dist/index.js.map +1 -0
- package/dist/react/index.d.ts +476 -0
- package/dist/react/index.js +969 -0
- package/dist/react/index.js.map +1 -0
- package/dist/reduced-BM4Ls0g5.d.ts +566 -0
- package/dist/reveal-WFUWDrei.d.ts +416 -0
- package/dist/store-Bqy_obnD.d.ts +195 -0
- package/dist/theme/generate.d.ts +18 -0
- package/dist/theme/generate.js +64 -0
- package/dist/theme/generate.js.map +1 -0
- package/dist/theme/index.d.ts +17 -0
- package/dist/theme/index.js +32 -0
- package/dist/theme/index.js.map +1 -0
- package/dist/theme-BFMoCzi6.d.ts +158 -0
- package/dist/types-BkS_raL8.d.ts +7 -0
- package/package.json +114 -3
- package/registry.json +469 -0
- package/skills/motion-kit/SKILL.md +189 -0
- package/skills/motion-kit/reference/components.md +86 -0
- package/skills/motion-kit/reference/design.md +92 -0
- package/skills/motion-kit/reference/recipes.md +172 -0
- package/src/auto/auto-init.ts +151 -0
- package/src/auto/index.ts +3 -0
- package/src/auto/parse-options.ts +68 -0
- package/src/components/chat/README.md +90 -0
- package/src/components/chat/chat.css +236 -0
- package/src/components/chat/element.ts +98 -0
- package/src/components/chat/engine.ts +295 -0
- package/src/components/chat/react.tsx +196 -0
- package/src/components/chat-background/README.md +50 -0
- package/src/components/chat-background/audio.ts +56 -0
- package/src/components/chat-background/chat-background.css +86 -0
- package/src/components/chat-background/element.ts +31 -0
- package/src/components/chat-background/engine.ts +108 -0
- package/src/components/chat-background/react.tsx +36 -0
- package/src/components/cursor/README.md +53 -0
- package/src/components/cursor/cursor.css +177 -0
- package/src/components/cursor/element.ts +23 -0
- package/src/components/cursor/engine.ts +254 -0
- package/src/components/cursor/react.tsx +26 -0
- package/src/components/dialog/README.md +106 -0
- package/src/components/dialog/dialog.css +258 -0
- package/src/components/dialog/element.ts +118 -0
- package/src/components/dialog/engine.ts +432 -0
- package/src/components/dialog/react.tsx +200 -0
- package/src/components/icon.tsx +10 -0
- package/src/components/icons.ts +24 -0
- package/src/components/island/README.md +91 -0
- package/src/components/island/element.ts +45 -0
- package/src/components/island/engine.ts +200 -0
- package/src/components/island/island.css +300 -0
- package/src/components/island/react.tsx +115 -0
- package/src/components/menu/README.md +67 -0
- package/src/components/menu/element.ts +55 -0
- package/src/components/menu/engine.ts +142 -0
- package/src/components/menu/menu.css +104 -0
- package/src/components/menu/react.tsx +82 -0
- package/src/components/nav/README.md +85 -0
- package/src/components/nav/element.ts +39 -0
- package/src/components/nav/engine.ts +164 -0
- package/src/components/nav/nav.css +1053 -0
- package/src/components/nav/react.tsx +90 -0
- package/src/components/steps/README.md +70 -0
- package/src/components/steps/element.ts +43 -0
- package/src/components/steps/engine.ts +185 -0
- package/src/components/steps/react.tsx +91 -0
- package/src/components/steps/steps.css +49 -0
- package/src/components/theme-picker/README.md +57 -0
- package/src/components/theme-picker/element.ts +20 -0
- package/src/components/theme-picker/engine.ts +91 -0
- package/src/components/theme-picker/react.tsx +31 -0
- package/src/components/theme-picker/theme-picker.css +114 -0
- package/src/components/toast/README.md +60 -0
- package/src/components/toast/element.ts +25 -0
- package/src/components/toast/engine.ts +483 -0
- package/src/components/toast/react.tsx +22 -0
- package/src/components/toast/store.ts +122 -0
- package/src/components/toast/toast.css +114 -0
- package/src/core/channels.ts +165 -0
- package/src/core/curves.ts +35 -0
- package/src/core/frame.ts +12 -0
- package/src/core/gsap.ts +65 -0
- package/src/core/loop.ts +44 -0
- package/src/core/math.ts +9 -0
- package/src/core/physics-spring.ts +59 -0
- package/src/core/pointer.ts +63 -0
- package/src/core/reduced.ts +21 -0
- package/src/core/scope.ts +70 -0
- package/src/core/scroll-track.ts +106 -0
- package/src/core/split.ts +68 -0
- package/src/core/spring.ts +22 -0
- package/src/core/styles.ts +31 -0
- package/src/core/timing.ts +204 -0
- package/src/core/types.ts +20 -0
- package/src/core/visible.ts +19 -0
- package/src/css/base.css +142 -0
- package/src/css/controls/README.md +122 -0
- package/src/css/controls/burger.css +67 -0
- package/src/css/controls/button.css +226 -0
- package/src/css/controls/checkbox.css +53 -0
- package/src/css/controls/chip.css +85 -0
- package/src/css/controls/field.css +124 -0
- package/src/css/controls/glass.css +220 -0
- package/src/css/controls/icon.css +26 -0
- package/src/css/controls/segmented.css +60 -0
- package/src/css/controls/switch.css +72 -0
- package/src/css/default-theme.css +123 -0
- package/src/css/effects/README.md +134 -0
- package/src/css/effects/anim.css +224 -0
- package/src/css/effects/fade.css +15 -0
- package/src/css/effects/float.css +64 -0
- package/src/css/effects/frost.css +62 -0
- package/src/css/effects/grain.css +58 -0
- package/src/css/effects/loading.css +60 -0
- package/src/css/effects/success.css +22 -0
- package/src/css/effects/text.css +67 -0
- package/src/css/fonts.css +3 -0
- package/src/css/states.css +51 -0
- package/src/css/themes/black-green.css +126 -0
- package/src/css/themes/black.css +126 -0
- package/src/css/themes/blue.css +126 -0
- package/src/css/themes/brown.css +126 -0
- package/src/css/themes/cold-blue.css +126 -0
- package/src/css/themes/funny.css +126 -0
- package/src/css/themes/gray.css +126 -0
- package/src/css/themes/green.css +126 -0
- package/src/css/themes/modern-1.css +126 -0
- package/src/css/themes/neogreen.css +126 -0
- package/src/css/themes/new.css +126 -0
- package/src/css/themes/new2.css +126 -0
- package/src/css/themes/oled.css +126 -0
- package/src/css/themes/orange.css +126 -0
- package/src/css/themes/pastel-pink.css +126 -0
- package/src/css/themes/pink.css +126 -0
- package/src/css/themes/purple.css +126 -0
- package/src/css/themes/red.css +126 -0
- package/src/css/themes/super-blue.css +126 -0
- package/src/css/tokens.css +182 -0
- package/src/elements/base.ts +76 -0
- package/src/elements/index.ts +48 -0
- package/src/gl/index.ts +15 -0
- package/src/gl/liquid-blend/README.md +23 -0
- package/src/gl/liquid-blend/create-liquid-blend.ts +7 -0
- package/src/gl/liquid-blend/shader.ts +9 -0
- package/src/gl/matcap/README.md +23 -0
- package/src/gl/matcap/create-matcap.ts +7 -0
- package/src/gl/matcap/shader.ts +16 -0
- package/src/gl/particle-hero/README.md +41 -0
- package/src/gl/particle-hero/create-particle-hero.ts +544 -0
- package/src/gl/particle-hero/particle-hero.css +19 -0
- package/src/gl/particle-hero/shaders.ts +84 -0
- package/src/gl/shader-blur/README.md +23 -0
- package/src/gl/shader-blur/create-shader-blur.ts +7 -0
- package/src/gl/shader-blur/shader.ts +11 -0
- package/src/gl/shader-grain/README.md +23 -0
- package/src/gl/shader-grain/create-shader-grain.ts +7 -0
- package/src/gl/shader-grain/shader.ts +6 -0
- package/src/gl/shared/create-shader-effect.ts +186 -0
- package/src/gl/shared/shader.css +3 -0
- package/src/gl/shared/shaders.ts +30 -0
- package/src/index.ts +177 -0
- package/src/motion/anchor.ts +45 -0
- package/src/motion/chat.ts +97 -0
- package/src/motion/confetti.ts +130 -0
- package/src/motion/count.ts +95 -0
- package/src/motion/details.ts +70 -0
- package/src/motion/drag-dismiss.ts +231 -0
- package/src/motion/effects.ts +81 -0
- package/src/motion/entrances.ts +27 -0
- package/src/motion/floating.ts +389 -0
- package/src/motion/flow.ts +115 -0
- package/src/motion/indicator.ts +44 -0
- package/src/motion/layout.ts +39 -0
- package/src/motion/liquid.ts +227 -0
- package/src/motion/morph.ts +540 -0
- package/src/motion/reveal.ts +112 -0
- package/src/motion/sheet.ts +65 -0
- package/src/motion/surface.ts +108 -0
- package/src/react/animated-number.tsx +30 -0
- package/src/react/flip-group.tsx +44 -0
- package/src/react/frost.tsx +33 -0
- package/src/react/index.ts +80 -0
- package/src/react/slot-text.tsx +44 -0
- package/src/react/theme-provider.tsx +42 -0
- package/src/react/use-card-dock.ts +9 -0
- package/src/react/use-cascade.ts +9 -0
- package/src/react/use-contrast.ts +9 -0
- package/src/react/use-counter.ts +8 -0
- package/src/react/use-curved-carousel.ts +10 -0
- package/src/react/use-depth-grid.ts +9 -0
- package/src/react/use-details.ts +7 -0
- package/src/react/use-dock-pill.ts +10 -0
- package/src/react/use-drag-ruler.ts +10 -0
- package/src/react/use-fill-text.ts +9 -0
- package/src/react/use-fit-text.ts +8 -0
- package/src/react/use-flow.ts +8 -0
- package/src/react/use-frame-zoom.ts +9 -0
- package/src/react/use-infinite-reel.ts +8 -0
- package/src/react/use-letters.ts +9 -0
- package/src/react/use-liquid-blend.ts +8 -0
- package/src/react/use-marquee.ts +9 -0
- package/src/react/use-mask-text.ts +9 -0
- package/src/react/use-matcap.ts +8 -0
- package/src/react/use-parallax.ts +9 -0
- package/src/react/use-particle-hero.ts +9 -0
- package/src/react/use-pile.ts +8 -0
- package/src/react/use-pop.ts +8 -0
- package/src/react/use-radial-menu.ts +10 -0
- package/src/react/use-recipe.ts +38 -0
- package/src/react/use-reflow.ts +9 -0
- package/src/react/use-repel-bento.ts +8 -0
- package/src/react/use-reveal.ts +8 -0
- package/src/react/use-rotator.ts +10 -0
- package/src/react/use-shader-blur.ts +8 -0
- package/src/react/use-shader-grain.ts +8 -0
- package/src/react/use-slot-text.ts +17 -0
- package/src/react/use-snap-strip.ts +9 -0
- package/src/react/use-sortable.ts +14 -0
- package/src/react/use-spring-cards.ts +8 -0
- package/src/react/use-stretch-footer.ts +20 -0
- package/src/react/use-tabs.ts +10 -0
- package/src/react/use-text-roll.ts +9 -0
- package/src/react/use-ticket-menu.ts +15 -0
- package/src/react/use-words.ts +8 -0
- package/src/recipes/card-dock/README.md +36 -0
- package/src/recipes/card-dock/card-dock.css +178 -0
- package/src/recipes/card-dock/create-card-dock.ts +97 -0
- package/src/recipes/cascade/README.md +33 -0
- package/src/recipes/cascade/create-cascade.ts +111 -0
- package/src/recipes/contrast/README.md +29 -0
- package/src/recipes/contrast/create-contrast.ts +68 -0
- package/src/recipes/counter/README.md +42 -0
- package/src/recipes/counter/create-counter.ts +84 -0
- package/src/recipes/curved-carousel/README.md +50 -0
- package/src/recipes/curved-carousel/carousel-math.ts +20 -0
- package/src/recipes/curved-carousel/create-curved-carousel.ts +257 -0
- package/src/recipes/curved-carousel/curved-carousel.css +50 -0
- package/src/recipes/depth-grid/README.md +30 -0
- package/src/recipes/depth-grid/create-depth-grid.ts +120 -0
- package/src/recipes/details/README.md +31 -0
- package/src/recipes/details/create-details.ts +27 -0
- package/src/recipes/dock-pill/README.md +23 -0
- package/src/recipes/dock-pill/create-dock-pill.ts +68 -0
- package/src/recipes/dock-pill/dock-pill.css +5 -0
- package/src/recipes/drag-ruler/README.md +21 -0
- package/src/recipes/drag-ruler/create-drag-ruler.ts +95 -0
- package/src/recipes/drag-ruler/drag-ruler.css +16 -0
- package/src/recipes/fill-text/README.md +27 -0
- package/src/recipes/fill-text/create-fill-text.ts +93 -0
- package/src/recipes/fit-text/README.md +37 -0
- package/src/recipes/fit-text/create-fit-text.ts +55 -0
- package/src/recipes/fit-text/fit-text.css +6 -0
- package/src/recipes/flow/README.md +46 -0
- package/src/recipes/flow/create-flow.ts +55 -0
- package/src/recipes/frame-zoom/README.md +29 -0
- package/src/recipes/frame-zoom/create-frame-zoom.ts +91 -0
- package/src/recipes/infinite-reel/README.md +23 -0
- package/src/recipes/infinite-reel/create-infinite-reel.ts +140 -0
- package/src/recipes/infinite-reel/infinite-reel.css +8 -0
- package/src/recipes/letters/README.md +30 -0
- package/src/recipes/letters/create-letters.ts +113 -0
- package/src/recipes/marquee/README.md +32 -0
- package/src/recipes/marquee/create-marquee.ts +74 -0
- package/src/recipes/marquee/marquee.css +41 -0
- package/src/recipes/mask-text/README.md +39 -0
- package/src/recipes/mask-text/create-mask-text.ts +134 -0
- package/src/recipes/parallax/README.md +31 -0
- package/src/recipes/parallax/create-parallax.ts +74 -0
- package/src/recipes/pile/README.md +48 -0
- package/src/recipes/pile/create-pile.ts +50 -0
- package/src/recipes/pile/pile-math.ts +27 -0
- package/src/recipes/pile/pile.css +63 -0
- package/src/recipes/pill/README.md +30 -0
- package/src/recipes/pill/pill.css +48 -0
- package/src/recipes/pop/README.md +36 -0
- package/src/recipes/pop/create-pop.ts +52 -0
- package/src/recipes/radial-menu/README.md +44 -0
- package/src/recipes/radial-menu/create-radial-menu.ts +96 -0
- package/src/recipes/radial-menu/radial-menu.css +45 -0
- package/src/recipes/reflow/README.md +47 -0
- package/src/recipes/reflow/create-reflow.ts +49 -0
- package/src/recipes/repel-bento/README.md +21 -0
- package/src/recipes/repel-bento/create-repel-bento.ts +65 -0
- package/src/recipes/repel-bento/repel-bento.css +3 -0
- package/src/recipes/reveal/README.md +36 -0
- package/src/recipes/reveal/create-reveal.ts +48 -0
- package/src/recipes/rotator/README.md +21 -0
- package/src/recipes/rotator/create-rotator.ts +66 -0
- package/src/recipes/rotator/rotator.css +3 -0
- package/src/recipes/slot-text/README.md +44 -0
- package/src/recipes/slot-text/create-slot-text.ts +244 -0
- package/src/recipes/slot-text/slot-text.css +42 -0
- package/src/recipes/snap-strip/README.md +31 -0
- package/src/recipes/snap-strip/create-snap-strip.ts +133 -0
- package/src/recipes/snap-strip/snap-strip.css +23 -0
- package/src/recipes/sortable/README.md +88 -0
- package/src/recipes/sortable/create-sortable.ts +863 -0
- package/src/recipes/sortable/sortable.css +40 -0
- package/src/recipes/spring-cards/README.md +48 -0
- package/src/recipes/spring-cards/create-spring-cards.ts +140 -0
- package/src/recipes/spring-cards/spring-cards.css +23 -0
- package/src/recipes/stretch-footer/README.md +44 -0
- package/src/recipes/stretch-footer/create-stretch-footer.ts +69 -0
- package/src/recipes/stretch-footer/stretch-footer.css +28 -0
- package/src/recipes/tabs/README.md +39 -0
- package/src/recipes/tabs/create-tabs.ts +91 -0
- package/src/recipes/tabs/tabs.css +19 -0
- package/src/recipes/text-roll/README.md +35 -0
- package/src/recipes/text-roll/create-text-roll.ts +137 -0
- package/src/recipes/text-roll/text-roll.css +21 -0
- package/src/recipes/ticket-menu/README.md +24 -0
- package/src/recipes/ticket-menu/create-ticket-menu.ts +6 -0
- package/src/recipes/words/README.md +43 -0
- package/src/recipes/words/create-words.ts +114 -0
- package/src/theme/css-format.ts +32 -0
- package/src/theme/generate.ts +72 -0
- package/src/theme/index.ts +15 -0
- package/src/theme/presets.ts +79 -0
- package/src/theme/theme.ts +119 -0
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
import { gsap, EASE, registerMotion } from '../../core/gsap';
|
|
2
|
+
import { reduced } from '../../core/reduced';
|
|
3
|
+
import { createScope } from '../../core/scope';
|
|
4
|
+
import { TIMING } from '../../core/timing';
|
|
5
|
+
import { inert, warnOnce, type Cleanup, type Instance } from '../../core/types';
|
|
6
|
+
import { MORPH_STYLES, clearStyles } from '../../core/styles';
|
|
7
|
+
import { anchorSurface, clearAnchor } from '../../motion/anchor';
|
|
8
|
+
import { attachDragDismiss } from '../../motion/drag-dismiss';
|
|
9
|
+
import {
|
|
10
|
+
attachFloating,
|
|
11
|
+
dockWindow,
|
|
12
|
+
floatingAvailable,
|
|
13
|
+
forgetFloat,
|
|
14
|
+
liftWindow,
|
|
15
|
+
savedFloat,
|
|
16
|
+
unplaceWindow,
|
|
17
|
+
} from '../../motion/floating';
|
|
18
|
+
import { isMorphing, morphCloseWithGhost, morphOpen, releaseShared, showOrigin, stopMorph } from '../../motion/morph';
|
|
19
|
+
import { pushUnderlay, releaseUnderlay, sheetClose, sheetOpen } from '../../motion/sheet';
|
|
20
|
+
import { closeModal, fadeBackdrop, openModal } from '../../motion/surface';
|
|
21
|
+
|
|
22
|
+
/*
|
|
23
|
+
* Stepbro's ModalHost on a native <dialog> (focus trap, Escape, top layer): the
|
|
24
|
+
* control that opened it turns into it and it folds back into that control
|
|
25
|
+
* (morph or liquid) over a backdrop tinted on the signature curve. It eases to
|
|
26
|
+
* new heights (auto-resize), can be dragged away (> 100 px) and, on desktop,
|
|
27
|
+
* lifted into a floating window by its handle. Parts: [data-mk-part] header,
|
|
28
|
+
* title, description, body, footer, close, handle.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
export type DialogVariant = 'morph' | 'liquid' | 'sheet';
|
|
32
|
+
export type DialogPlacement = 'center' | 'anchor' | 'side';
|
|
33
|
+
|
|
34
|
+
export interface DialogLabels {
|
|
35
|
+
close?: string;
|
|
36
|
+
float?: string;
|
|
37
|
+
dock?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface DialogOptions {
|
|
41
|
+
/** morph (Stepbro, default), liquid (Stepbro), or sheet: rises from the bottom and pushes [data-mk-underlay] back (Dousan). */
|
|
42
|
+
variant?: DialogVariant;
|
|
43
|
+
placement?: DialogPlacement;
|
|
44
|
+
/** Tinted backdrop; false leaves it transparent (menus). */
|
|
45
|
+
backdrop?: boolean;
|
|
46
|
+
closeOnBackdrop?: boolean;
|
|
47
|
+
/** Drag (or wheel) down to dismiss. */
|
|
48
|
+
drag?: boolean;
|
|
49
|
+
/** A key turns on the floating window (desktop) and remembers its place. */
|
|
50
|
+
float?: string | false;
|
|
51
|
+
autoResize?: boolean;
|
|
52
|
+
/** `[data-mk-open="<id>"]` buttons open it: true uses the dialog's id; a string names another id; false: none. */
|
|
53
|
+
triggers?: boolean | string;
|
|
54
|
+
/** Modal (default) or a non-modal dialog. */
|
|
55
|
+
modal?: boolean;
|
|
56
|
+
labels?: DialogLabels;
|
|
57
|
+
/** Grow the content with the surface (default). Menus turn it off and cascade their items instead. */
|
|
58
|
+
animateContent?: boolean;
|
|
59
|
+
onOpen?: () => void;
|
|
60
|
+
/** After it closed, whatever closed it. */
|
|
61
|
+
onClose?: () => void;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface DialogInstance extends Instance {
|
|
65
|
+
/** `origin`: the control it grows from; omitted, the focused control; null, none (scale-in). */
|
|
66
|
+
open(origin?: HTMLElement | null): void;
|
|
67
|
+
close(): void;
|
|
68
|
+
toggle(origin?: HTMLElement | null): void;
|
|
69
|
+
float(): void;
|
|
70
|
+
dock(): void;
|
|
71
|
+
readonly isOpen: boolean;
|
|
72
|
+
readonly isFloating: boolean;
|
|
73
|
+
readonly element: HTMLElement;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const DEFAULT_LABELS: Required<DialogLabels> = { close: 'Cerrar', float: 'Ventana flotante', dock: 'Volver a su sitio' };
|
|
77
|
+
const FIELDS = '[autofocus], input:not([type="hidden"]):not(:disabled), textarea:not(:disabled), select:not(:disabled)';
|
|
78
|
+
const CORNERS = ['tl', 'tr', 'bl', 'br'] as const;
|
|
79
|
+
let uid = 0;
|
|
80
|
+
|
|
81
|
+
const part = (dialog: HTMLElement, name: string) => dialog.querySelector<HTMLElement>(`[data-mk-part="${name}"]`);
|
|
82
|
+
|
|
83
|
+
export function createDialog(dialog: HTMLElement | null, options: DialogOptions = {}): DialogInstance {
|
|
84
|
+
if (!dialog) {
|
|
85
|
+
warnOnce('dialog: missing <dialog> element');
|
|
86
|
+
return inert({ open() {}, close() {}, toggle() {}, float() {}, dock() {}, isOpen: false, isFloating: false, element: null as unknown as HTMLElement });
|
|
87
|
+
}
|
|
88
|
+
registerMotion();
|
|
89
|
+
const {
|
|
90
|
+
variant = 'morph',
|
|
91
|
+
placement = 'center',
|
|
92
|
+
backdrop = true,
|
|
93
|
+
closeOnBackdrop = true,
|
|
94
|
+
drag = true,
|
|
95
|
+
float: floatKey = false,
|
|
96
|
+
autoResize = true,
|
|
97
|
+
triggers = true,
|
|
98
|
+
modal = true,
|
|
99
|
+
} = options;
|
|
100
|
+
const labels = { ...DEFAULT_LABELS, ...options.labels };
|
|
101
|
+
const liquid = variant === 'liquid';
|
|
102
|
+
const sheet = variant === 'sheet';
|
|
103
|
+
const scope = createScope();
|
|
104
|
+
/** Effects that live while it is open. */
|
|
105
|
+
let session: Cleanup[] = [];
|
|
106
|
+
let opener: HTMLElement | null = null;
|
|
107
|
+
let isOpen = false;
|
|
108
|
+
let closing = false;
|
|
109
|
+
let floating = false;
|
|
110
|
+
/**
|
|
111
|
+
* Lifting and docking swap modal ↔ popover: the native `close` events they cause
|
|
112
|
+
* (queued, so they arrive later) are not the person closing it.
|
|
113
|
+
*/
|
|
114
|
+
let ownCloses = 0;
|
|
115
|
+
const closeNatively = () => {
|
|
116
|
+
if (!dialog.hasAttribute('open')) return;
|
|
117
|
+
ownCloses += 1;
|
|
118
|
+
closeModal(dialog as HTMLDialogElement);
|
|
119
|
+
};
|
|
120
|
+
let destroyed = false;
|
|
121
|
+
/** destroy() is under way: the page behind comes back at once. */
|
|
122
|
+
let tearingDown = false;
|
|
123
|
+
|
|
124
|
+
if (placement !== 'center') scope.attr(dialog, 'data-mk-placement', placement);
|
|
125
|
+
if (!backdrop) scope.attr(dialog, 'data-mk-backdrop', 'none');
|
|
126
|
+
scope.attr(dialog, 'data-mk-variant', variant);
|
|
127
|
+
const title = part(dialog, 'title');
|
|
128
|
+
if (title && !dialog.hasAttribute('aria-label') && !dialog.hasAttribute('aria-labelledby')) {
|
|
129
|
+
if (!title.id) scope.attr(title, 'id', `mk-dialog-title-${++uid}`);
|
|
130
|
+
scope.attr(dialog, 'aria-labelledby', title.id);
|
|
131
|
+
}
|
|
132
|
+
const description = part(dialog, 'description');
|
|
133
|
+
if (description && !dialog.hasAttribute('aria-describedby')) {
|
|
134
|
+
if (!description.id) scope.attr(description, 'id', `mk-dialog-description-${++uid}`);
|
|
135
|
+
scope.attr(dialog, 'aria-describedby', description.id);
|
|
136
|
+
}
|
|
137
|
+
const closeButton = part(dialog, 'close');
|
|
138
|
+
if (closeButton && !closeButton.hasAttribute('aria-label') && !closeButton.textContent?.trim()) {
|
|
139
|
+
scope.attr(closeButton, 'aria-label', labels.close);
|
|
140
|
+
}
|
|
141
|
+
const handle = floatKey ? part(dialog, 'handle') : null;
|
|
142
|
+
if (handle) {
|
|
143
|
+
scope.attr(handle, 'data-mk-no-drag', '');
|
|
144
|
+
scope.attr(handle, 'aria-pressed', 'false');
|
|
145
|
+
scope.attr(handle, 'aria-label', labels.float);
|
|
146
|
+
}
|
|
147
|
+
const corners: HTMLElement[] = [];
|
|
148
|
+
if (floatKey) {
|
|
149
|
+
for (const corner of CORNERS) {
|
|
150
|
+
const element = document.createElement('span');
|
|
151
|
+
element.className = 'mk-float-corner';
|
|
152
|
+
element.dataset.mkFloatCorner = corner;
|
|
153
|
+
element.dataset.mkNoDrag = '';
|
|
154
|
+
element.setAttribute('aria-hidden', 'true');
|
|
155
|
+
dialog.append(element);
|
|
156
|
+
corners.push(element);
|
|
157
|
+
}
|
|
158
|
+
scope.add(() => corners.forEach((corner) => corner.remove()));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const emit = (type: string, cancelable = false, detail: Record<string, unknown> = {}) =>
|
|
162
|
+
dialog.dispatchEvent(new CustomEvent(type, { bubbles: true, cancelable, detail: { dialog, ...detail } }));
|
|
163
|
+
|
|
164
|
+
const endSession = () => {
|
|
165
|
+
for (const cleanup of session.splice(0).reverse()) cleanup();
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const settleFloating = () => {
|
|
169
|
+
if (!floating) return;
|
|
170
|
+
floating = false;
|
|
171
|
+
if (typeof dialog.hidePopover === 'function' && dialog.hasAttribute('popover')) {
|
|
172
|
+
try {
|
|
173
|
+
dialog.hidePopover();
|
|
174
|
+
} catch {
|
|
175
|
+
// Already hidden.
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
dialog.removeAttribute('popover');
|
|
179
|
+
unplaceWindow(dialog);
|
|
180
|
+
if (handle) {
|
|
181
|
+
handle.setAttribute('aria-pressed', 'false');
|
|
182
|
+
handle.setAttribute('aria-label', labels.float);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
/** Closed without our animation (owner, form[method=dialog], destroy). */
|
|
187
|
+
const closedQuietly = () => {
|
|
188
|
+
stopMorph(dialog);
|
|
189
|
+
endSession();
|
|
190
|
+
isOpen = false;
|
|
191
|
+
closing = false;
|
|
192
|
+
showOrigin(opener);
|
|
193
|
+
releaseShared(opener);
|
|
194
|
+
emit('mk:closed');
|
|
195
|
+
options.onClose?.();
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const lift = () => {
|
|
199
|
+
if (!floatKey || floating || !isOpen || !floatingAvailable()) return;
|
|
200
|
+
const box = dialog.getBoundingClientRect();
|
|
201
|
+
const rect = savedFloat(floatKey) ?? { top: box.top, left: box.left, width: box.width, height: box.height };
|
|
202
|
+
const focused = document.activeElement;
|
|
203
|
+
// Its backdrop leaves as the window lifts off the page.
|
|
204
|
+
const backdropStyle = getComputedStyle(dialog, '::backdrop');
|
|
205
|
+
const veil = document.createElement('div');
|
|
206
|
+
veil.setAttribute('aria-hidden', 'true');
|
|
207
|
+
Object.assign(veil.style, {
|
|
208
|
+
position: 'fixed',
|
|
209
|
+
inset: '0',
|
|
210
|
+
background: backdropStyle.background || backdropStyle.backgroundColor,
|
|
211
|
+
backdropFilter: backdropStyle.backdropFilter,
|
|
212
|
+
pointerEvents: 'none',
|
|
213
|
+
});
|
|
214
|
+
closeNatively();
|
|
215
|
+
document.body.append(veil);
|
|
216
|
+
dialog.setAttribute('popover', 'manual');
|
|
217
|
+
if (typeof dialog.showPopover === 'function') dialog.showPopover();
|
|
218
|
+
else openModal(dialog, false);
|
|
219
|
+
liftWindow(dialog, rect);
|
|
220
|
+
gsap.to(veil, { opacity: 0, duration: reduced() ? 0 : TIMING.float.shadow, ease: EASE.signature, onComplete: () => veil.remove() });
|
|
221
|
+
floating = true;
|
|
222
|
+
if (handle) {
|
|
223
|
+
handle.setAttribute('aria-pressed', 'true');
|
|
224
|
+
handle.setAttribute('aria-label', labels.dock);
|
|
225
|
+
}
|
|
226
|
+
showOrigin(opener, { fade: true });
|
|
227
|
+
if (focused instanceof HTMLElement && dialog.contains(focused)) focused.focus();
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const dock = () => {
|
|
231
|
+
if (!floatKey || !floating) return;
|
|
232
|
+
const box = dialog.getBoundingClientRect();
|
|
233
|
+
const focused = document.activeElement;
|
|
234
|
+
floating = false;
|
|
235
|
+
if (typeof dialog.hidePopover === 'function') dialog.hidePopover();
|
|
236
|
+
dialog.removeAttribute('popover');
|
|
237
|
+
closeNatively();
|
|
238
|
+
openModal(dialog, modal);
|
|
239
|
+
dockWindow(dialog, { top: box.top, left: box.left, width: box.width, height: box.height });
|
|
240
|
+
if (backdrop) void fadeBackdrop(dialog, 'in');
|
|
241
|
+
forgetFloat(floatKey);
|
|
242
|
+
if (handle) {
|
|
243
|
+
handle.setAttribute('aria-pressed', 'false');
|
|
244
|
+
handle.setAttribute('aria-label', labels.float);
|
|
245
|
+
}
|
|
246
|
+
if (focused instanceof HTMLElement && dialog.contains(focused)) focused.focus();
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const startSession = () => {
|
|
250
|
+
if (sheet) {
|
|
251
|
+
const behind = [...document.querySelectorAll<HTMLElement>('[data-mk-underlay]')];
|
|
252
|
+
pushUnderlay(behind);
|
|
253
|
+
const root = document.documentElement;
|
|
254
|
+
const overflow = root.style.overflow;
|
|
255
|
+
root.style.overflow = 'hidden';
|
|
256
|
+
session.push(() => {
|
|
257
|
+
root.style.overflow = overflow;
|
|
258
|
+
if (root.getAttribute('style') === '') root.removeAttribute('style');
|
|
259
|
+
releaseUnderlay(behind, !tearingDown);
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
if (autoResize && typeof ResizeObserver === 'function') {
|
|
263
|
+
let last = dialog.offsetHeight;
|
|
264
|
+
let resizing: gsap.core.Tween | null = null;
|
|
265
|
+
const observer = new ResizeObserver(() => {
|
|
266
|
+
if (resizing?.isActive()) return;
|
|
267
|
+
const next = dialog.offsetHeight;
|
|
268
|
+
const previous = last;
|
|
269
|
+
last = next;
|
|
270
|
+
// A floating window's size is the person's, not its content's.
|
|
271
|
+
if (floating || Math.abs(next - previous) < 2 || !previous || isMorphing(dialog) || reduced()) return;
|
|
272
|
+
resizing = gsap.fromTo(
|
|
273
|
+
dialog,
|
|
274
|
+
{ height: previous },
|
|
275
|
+
{
|
|
276
|
+
height: next,
|
|
277
|
+
duration: TIMING.dialog.autoResize,
|
|
278
|
+
ease: EASE.signature,
|
|
279
|
+
clearProps: 'height',
|
|
280
|
+
onComplete: () => {
|
|
281
|
+
last = dialog.offsetHeight;
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
);
|
|
285
|
+
});
|
|
286
|
+
observer.observe(dialog);
|
|
287
|
+
session.push(() => {
|
|
288
|
+
observer.disconnect();
|
|
289
|
+
resizing?.kill();
|
|
290
|
+
clearStyles([dialog], ['height']);
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
if (drag) {
|
|
294
|
+
session.push(
|
|
295
|
+
attachDragDismiss(dialog, {
|
|
296
|
+
canDrag: () => !isMorphing(dialog) && !floating && !closing && isOpen,
|
|
297
|
+
onDismiss: () => close(true),
|
|
298
|
+
}),
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
if (placement === 'anchor') {
|
|
302
|
+
const onResize = () => {
|
|
303
|
+
if (isOpen && !floating) anchorSurface(dialog, opener?.isConnected ? opener.getBoundingClientRect() : null);
|
|
304
|
+
};
|
|
305
|
+
addEventListener('resize', onResize);
|
|
306
|
+
session.push(() => {
|
|
307
|
+
removeEventListener('resize', onResize);
|
|
308
|
+
clearAnchor(dialog);
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
if (floatKey && handle) {
|
|
312
|
+
session.push(attachFloating(dialog, { key: floatKey, handle, corners, isFloating: () => floating, float: lift, dock }));
|
|
313
|
+
const onResize = () => {
|
|
314
|
+
if (floating && !floatingAvailable()) dock();
|
|
315
|
+
};
|
|
316
|
+
addEventListener('resize', onResize);
|
|
317
|
+
session.push(() => removeEventListener('resize', onResize));
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
const open = (origin?: HTMLElement | null) => {
|
|
322
|
+
if (destroyed || isOpen) return;
|
|
323
|
+
const active = document.activeElement;
|
|
324
|
+
// undefined: the focused control (usually the one clicked); null: no opener at all.
|
|
325
|
+
opener = origin !== undefined ? origin : active instanceof HTMLElement && active !== document.body && !dialog.contains(active) ? active : null;
|
|
326
|
+
closing = false;
|
|
327
|
+
isOpen = true;
|
|
328
|
+
openModal(dialog, modal);
|
|
329
|
+
if (placement === 'anchor') anchorSurface(dialog, opener?.getBoundingClientRect());
|
|
330
|
+
if (backdrop && modal) void fadeBackdrop(dialog, 'in');
|
|
331
|
+
if (sheet) sheetOpen(dialog);
|
|
332
|
+
else morphOpen(dialog, opener, { hideOrigin: true, liquid, content: options.animateContent === false ? ':scope > :not(*)' : undefined });
|
|
333
|
+
startSession();
|
|
334
|
+
// Forms start at their first field (after showModal: a closed dialog cannot take focus).
|
|
335
|
+
dialog.querySelector<HTMLElement>(FIELDS)?.focus({ preventScroll: true });
|
|
336
|
+
emit('mk:open', false, { origin: opener });
|
|
337
|
+
options.onOpen?.();
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
/** Animated close: the dialog closes at once while an inert copy folds into the opener. */
|
|
341
|
+
const close = (inPlace = false) => {
|
|
342
|
+
if (destroyed || !isOpen || closing) return;
|
|
343
|
+
if (!emit('mk:close', true, { inPlace })) return;
|
|
344
|
+
closing = true;
|
|
345
|
+
// The copy is measured first: the session cleanup clears an anchored place.
|
|
346
|
+
if (sheet) sheetClose(dialog, { inPlace });
|
|
347
|
+
else morphCloseWithGhost(dialog, opener, { inPlace, liquid });
|
|
348
|
+
endSession();
|
|
349
|
+
if (floating) settleFloating();
|
|
350
|
+
if (dialog.hasAttribute('open')) closeModal(dialog as HTMLDialogElement);
|
|
351
|
+
isOpen = false;
|
|
352
|
+
closing = false;
|
|
353
|
+
if (opener?.isConnected) opener.focus();
|
|
354
|
+
emit('mk:closed');
|
|
355
|
+
options.onClose?.();
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
scope.on(dialog, 'cancel', (event) => {
|
|
359
|
+
event.preventDefault();
|
|
360
|
+
close();
|
|
361
|
+
});
|
|
362
|
+
scope.on(dialog, 'close', () => {
|
|
363
|
+
if (ownCloses > 0) {
|
|
364
|
+
ownCloses -= 1;
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
// A native close we did not start (form method="dialog", dialog.close()).
|
|
368
|
+
if (!isOpen || closing) return;
|
|
369
|
+
closedQuietly();
|
|
370
|
+
});
|
|
371
|
+
scope.on(dialog, 'keydown', (event) => {
|
|
372
|
+
// A floating (non-modal) window still closes with Escape.
|
|
373
|
+
if ((event as KeyboardEvent).key === 'Escape' && floating) {
|
|
374
|
+
event.preventDefault();
|
|
375
|
+
close();
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
scope.on(dialog, 'click', (event) => {
|
|
379
|
+
const target = event.target as Element | null;
|
|
380
|
+
if (target?.closest('[data-mk-part="close"], [data-mk-close]') && dialog.contains(target)) {
|
|
381
|
+
close();
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
if (target !== dialog || !closeOnBackdrop || floating) return;
|
|
385
|
+
const mouse = event as MouseEvent;
|
|
386
|
+
const box = dialog.getBoundingClientRect();
|
|
387
|
+
if (mouse.clientX < box.left || mouse.clientX > box.right || mouse.clientY < box.top || mouse.clientY > box.bottom) close();
|
|
388
|
+
});
|
|
389
|
+
const triggerId = triggers === true ? dialog.id : triggers || '';
|
|
390
|
+
if (triggerId) {
|
|
391
|
+
const selector = `[data-mk-open="${typeof CSS !== 'undefined' && CSS.escape ? CSS.escape(triggerId) : triggerId}"]`;
|
|
392
|
+
scope.on(document, 'click', (event) => {
|
|
393
|
+
const trigger = (event.target as Element | null)?.closest<HTMLElement>(selector);
|
|
394
|
+
if (!trigger || dialog.contains(trigger)) return;
|
|
395
|
+
event.preventDefault();
|
|
396
|
+
if (isOpen) close();
|
|
397
|
+
else open(trigger);
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
dialog.dataset.mkReady = '';
|
|
401
|
+
|
|
402
|
+
return {
|
|
403
|
+
open,
|
|
404
|
+
close: () => close(),
|
|
405
|
+
toggle: (origin?: HTMLElement | null) => (isOpen ? close() : open(origin)),
|
|
406
|
+
float: lift,
|
|
407
|
+
dock,
|
|
408
|
+
get isOpen() {
|
|
409
|
+
return isOpen;
|
|
410
|
+
},
|
|
411
|
+
get isFloating() {
|
|
412
|
+
return floating;
|
|
413
|
+
},
|
|
414
|
+
element: dialog,
|
|
415
|
+
destroy() {
|
|
416
|
+
if (destroyed) return;
|
|
417
|
+
tearingDown = true;
|
|
418
|
+
if (isOpen) {
|
|
419
|
+
settleFloating();
|
|
420
|
+
closing = true;
|
|
421
|
+
if (dialog.hasAttribute('open')) closeModal(dialog as HTMLDialogElement);
|
|
422
|
+
closedQuietly();
|
|
423
|
+
}
|
|
424
|
+
destroyed = true;
|
|
425
|
+
endSession();
|
|
426
|
+
gsap.killTweensOf(dialog);
|
|
427
|
+
clearStyles([dialog], [...MORPH_STYLES, 'height']);
|
|
428
|
+
scope.dispose();
|
|
429
|
+
delete dialog.dataset.mkReady;
|
|
430
|
+
},
|
|
431
|
+
};
|
|
432
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createContext,
|
|
5
|
+
useCallback,
|
|
6
|
+
useContext,
|
|
7
|
+
useMemo,
|
|
8
|
+
useRef,
|
|
9
|
+
useState,
|
|
10
|
+
type ButtonHTMLAttributes,
|
|
11
|
+
type DialogHTMLAttributes,
|
|
12
|
+
type HTMLAttributes,
|
|
13
|
+
type MutableRefObject,
|
|
14
|
+
type ReactNode,
|
|
15
|
+
} from 'react';
|
|
16
|
+
import { useIsomorphicLayoutEffect, useLatest } from '../../react/use-recipe';
|
|
17
|
+
import { MkIcon } from '../icon';
|
|
18
|
+
import { createDialog, type DialogInstance, type DialogOptions } from './engine';
|
|
19
|
+
|
|
20
|
+
export { MkIcon };
|
|
21
|
+
|
|
22
|
+
export type SurfaceOptions = Omit<DialogOptions, 'onOpen' | 'onClose' | 'triggers'>;
|
|
23
|
+
|
|
24
|
+
export interface SurfaceContextValue<I> {
|
|
25
|
+
open: boolean;
|
|
26
|
+
setOpen: (open: boolean) => void;
|
|
27
|
+
/** The control the next open grows from (set by the Trigger). */
|
|
28
|
+
origin: MutableRefObject<HTMLElement | null>;
|
|
29
|
+
instance: MutableRefObject<I | null>;
|
|
30
|
+
options: SurfaceOptions;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface SurfaceRootProps extends SurfaceOptions {
|
|
34
|
+
open?: boolean;
|
|
35
|
+
defaultOpen?: boolean;
|
|
36
|
+
onOpenChange?: (open: boolean) => void;
|
|
37
|
+
children?: ReactNode;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Open state (controlled or not) shared by a surface's parts. Used by Dialog and Menu. */
|
|
41
|
+
export function useSurfaceRoot<I>({ open: controlled, defaultOpen = false, onOpenChange, children: _children, ...options }: SurfaceRootProps): SurfaceContextValue<I> {
|
|
42
|
+
const [uncontrolled, setUncontrolled] = useState(defaultOpen);
|
|
43
|
+
const open = controlled ?? uncontrolled;
|
|
44
|
+
const current = useLatest(open);
|
|
45
|
+
const change = useLatest(onOpenChange);
|
|
46
|
+
const isControlled = controlled !== undefined;
|
|
47
|
+
const setOpen = useCallback(
|
|
48
|
+
(next: boolean) => {
|
|
49
|
+
if (next === current.current) return;
|
|
50
|
+
current.current = next;
|
|
51
|
+
if (!isControlled) setUncontrolled(next);
|
|
52
|
+
change.current?.(next);
|
|
53
|
+
},
|
|
54
|
+
[isControlled, current, change],
|
|
55
|
+
);
|
|
56
|
+
const origin = useRef<HTMLElement | null>(null);
|
|
57
|
+
const instance = useRef<I | null>(null);
|
|
58
|
+
const optionKey = JSON.stringify(options);
|
|
59
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
60
|
+
const stable = useMemo(() => options, [optionKey]);
|
|
61
|
+
return useMemo(() => ({ open, setOpen, origin, instance, options: stable }), [open, setOpen, stable]);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const DialogContext = createContext<SurfaceContextValue<DialogInstance> | null>(null);
|
|
65
|
+
|
|
66
|
+
function useDialogContext(part: string) {
|
|
67
|
+
const value = useContext(DialogContext);
|
|
68
|
+
if (!value) throw new Error(`<Dialog.${part}> must be inside <Dialog.Root>`);
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function Root(props: SurfaceRootProps) {
|
|
73
|
+
const value = useSurfaceRoot<DialogInstance>(props);
|
|
74
|
+
return <DialogContext.Provider value={value}>{props.children}</DialogContext.Provider>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** A button that opens the dialog and is the shape it grows from. */
|
|
78
|
+
function Trigger({ onClick, children, ...props }: ButtonHTMLAttributes<HTMLButtonElement>) {
|
|
79
|
+
const context = useDialogContext('Trigger');
|
|
80
|
+
return (
|
|
81
|
+
<button
|
|
82
|
+
type="button"
|
|
83
|
+
aria-haspopup="dialog"
|
|
84
|
+
aria-expanded={context.open}
|
|
85
|
+
{...props}
|
|
86
|
+
onClick={(event) => {
|
|
87
|
+
onClick?.(event);
|
|
88
|
+
if (event.defaultPrevented) return;
|
|
89
|
+
context.origin.current = event.currentTarget;
|
|
90
|
+
context.setOpen(!context.open);
|
|
91
|
+
}}
|
|
92
|
+
>
|
|
93
|
+
{children}
|
|
94
|
+
</button>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface DialogContentProps extends Omit<DialogHTMLAttributes<HTMLDialogElement>, 'open'> {
|
|
99
|
+
size?: 'sm' | 'md' | 'lg' | 'xl';
|
|
100
|
+
/** Skip the base class (style it yourself). */
|
|
101
|
+
unstyled?: boolean;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Runs a surface engine on a <dialog> and follows the root's open state.
|
|
106
|
+
* Shared with Menu (which passes its own engine).
|
|
107
|
+
*/
|
|
108
|
+
export function useSurfaceEngine<I extends DialogInstance>(
|
|
109
|
+
context: SurfaceContextValue<I>,
|
|
110
|
+
create: (dialog: HTMLDialogElement, options: DialogOptions) => I,
|
|
111
|
+
) {
|
|
112
|
+
const ref = useRef<HTMLDialogElement>(null);
|
|
113
|
+
const setOpen = useLatest(context.setOpen);
|
|
114
|
+
useIsomorphicLayoutEffect(() => {
|
|
115
|
+
const dialog = ref.current;
|
|
116
|
+
if (!dialog) return;
|
|
117
|
+
const created = create(dialog, { ...context.options, triggers: false, onClose: () => setOpen.current(false) });
|
|
118
|
+
context.instance.current = created;
|
|
119
|
+
if (context.open) created.open(context.origin.current ?? undefined);
|
|
120
|
+
return () => {
|
|
121
|
+
created.destroy();
|
|
122
|
+
context.instance.current = null;
|
|
123
|
+
};
|
|
124
|
+
// The engine is rebuilt only when its options change; open state is followed below.
|
|
125
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
126
|
+
}, [context.options]);
|
|
127
|
+
useIsomorphicLayoutEffect(() => {
|
|
128
|
+
const instance = context.instance.current;
|
|
129
|
+
if (!instance) return;
|
|
130
|
+
if (context.open && !instance.isOpen) {
|
|
131
|
+
instance.open(context.origin.current ?? undefined);
|
|
132
|
+
context.origin.current = null;
|
|
133
|
+
} else if (!context.open && instance.isOpen) instance.close();
|
|
134
|
+
}, [context.open]);
|
|
135
|
+
return ref;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function Content({ size = 'md', unstyled, className, children, ...props }: DialogContentProps) {
|
|
139
|
+
const context = useDialogContext('Content');
|
|
140
|
+
const ref = useSurfaceEngine(context, createDialog);
|
|
141
|
+
const classes = [unstyled ? null : 'mk-dialog', !unstyled && size !== 'md' ? `mk-dialog--${size}` : null, className].filter(Boolean).join(' ');
|
|
142
|
+
return (
|
|
143
|
+
<dialog ref={ref} className={classes || undefined} {...props}>
|
|
144
|
+
{children}
|
|
145
|
+
</dialog>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const partOf = (name: string) =>
|
|
150
|
+
function Part({ children, ...props }: HTMLAttributes<HTMLElement>) {
|
|
151
|
+
const Tag = (name === 'header' ? 'header' : name === 'footer' ? 'footer' : name === 'title' ? 'h2' : name === 'description' ? 'p' : 'div') as 'div';
|
|
152
|
+
return (
|
|
153
|
+
<Tag data-mk-part={name} {...props}>
|
|
154
|
+
{children}
|
|
155
|
+
</Tag>
|
|
156
|
+
);
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
function Close({ children, label = 'Cerrar', ...props }: ButtonHTMLAttributes<HTMLButtonElement> & { label?: string }) {
|
|
160
|
+
return (
|
|
161
|
+
<button type="button" data-mk-part="close" aria-label={children ? undefined : label} {...props}>
|
|
162
|
+
{children ?? <MkIcon name="close" />}
|
|
163
|
+
</button>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function Handle({ children, ...props }: ButtonHTMLAttributes<HTMLButtonElement>) {
|
|
168
|
+
return (
|
|
169
|
+
<button type="button" data-mk-part="handle" {...props}>
|
|
170
|
+
{children ?? <MkIcon name="expand" size={18} />}
|
|
171
|
+
</button>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Stepbro's dialog for React:
|
|
177
|
+
* <Dialog.Root><Dialog.Trigger>Crear</Dialog.Trigger><Dialog.Content>
|
|
178
|
+
* <Dialog.Header><Dialog.Title>…</Dialog.Title><Dialog.Close /></Dialog.Header>
|
|
179
|
+
* <Dialog.Body>…</Dialog.Body><Dialog.Footer>…</Dialog.Footer>
|
|
180
|
+
* </Dialog.Content></Dialog.Root>
|
|
181
|
+
*/
|
|
182
|
+
export const Dialog = {
|
|
183
|
+
Root,
|
|
184
|
+
Trigger,
|
|
185
|
+
Content,
|
|
186
|
+
Header: partOf('header'),
|
|
187
|
+
Title: partOf('title'),
|
|
188
|
+
Description: partOf('description'),
|
|
189
|
+
Actions: partOf('actions'),
|
|
190
|
+
Body: partOf('body'),
|
|
191
|
+
Footer: partOf('footer'),
|
|
192
|
+
Close,
|
|
193
|
+
Handle,
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
/** The dialog instance of the surrounding <Dialog.Root> (open, close, float…). */
|
|
197
|
+
export function useDialog() {
|
|
198
|
+
const context = useDialogContext('useDialog');
|
|
199
|
+
return { open: context.open, setOpen: context.setOpen, instance: context.instance };
|
|
200
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ICON_PATHS, type IconName } from './icons';
|
|
2
|
+
|
|
3
|
+
/** The same stroke icons the vanilla components use (aria-hidden). */
|
|
4
|
+
export function MkIcon({ name, size = 20 }: { name: IconName; size?: number }) {
|
|
5
|
+
return (
|
|
6
|
+
<svg aria-hidden="true" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
|
|
7
|
+
<path d={ICON_PATHS[name]} />
|
|
8
|
+
</svg>
|
|
9
|
+
);
|
|
10
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The few icons the components draw themselves (close, float, dock, tones), as
|
|
3
|
+
* 24 × 24 stroke paths, so no component depends on the Material Symbols font.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const ICON_PATHS = {
|
|
7
|
+
close: 'M6 6l12 12M18 6L6 18',
|
|
8
|
+
expand: 'M14 4h6v6M10 20H4v-6M20 4l-7 7M4 20l7-7',
|
|
9
|
+
shrink: 'M4 14h6v6M20 10h-6V4M10 14l-7 7M14 10l7-7',
|
|
10
|
+
check: 'M5 12.5l4.5 4.5L19 7.5',
|
|
11
|
+
alert: 'M12 8v5M12 16.5v.01M10.3 3.9L2.6 17.3a2 2 0 001.7 3h15.4a2 2 0 001.7-3L13.7 3.9a2 2 0 00-3.4 0z',
|
|
12
|
+
info: 'M12 11v6M12 7.5v.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z',
|
|
13
|
+
send: 'M12 19V5M5.5 11.5L12 5l6.5 6.5',
|
|
14
|
+
mic: 'M12 3a3 3 0 00-3 3v6a3 3 0 006 0V6a3 3 0 00-3-3zM5.5 11a6.5 6.5 0 0013 0M12 17.5V21',
|
|
15
|
+
sparkle: 'M12 3l1.9 5.1L19 10l-5.1 1.9L12 17l-1.9-5.1L5 10l5.1-1.9z',
|
|
16
|
+
grip: 'M9 6h.01M15 6h.01M9 12h.01M15 12h.01M9 18h.01M15 18h.01',
|
|
17
|
+
} as const;
|
|
18
|
+
|
|
19
|
+
export type IconName = keyof typeof ICON_PATHS;
|
|
20
|
+
|
|
21
|
+
/** An inline SVG string (aria-hidden), for vanilla engines and custom elements. */
|
|
22
|
+
export function iconSvg(name: IconName, size = 20) {
|
|
23
|
+
return `<svg aria-hidden="true" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="${ICON_PATHS[name]}"/></svg>`;
|
|
24
|
+
}
|