@yewdiel21/motion-kit 0.9.0 → 0.10.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 +3 -35
- package/README.md +93 -81
- package/dist/auto/index.d.ts +22 -3
- package/dist/auto/index.js +43 -5
- package/dist/auto/index.js.map +1 -1
- package/dist/{chunk-KLY5BRRI.js → chunk-34EZ7DF4.js} +1 -1
- package/dist/chunk-34EZ7DF4.js.map +1 -0
- package/dist/{chunk-RFDIZYFX.js → chunk-DOOMULVV.js} +20 -20
- package/dist/chunk-DOOMULVV.js.map +1 -0
- package/dist/{chunk-ABNRDDO7.js → chunk-I3DXF2KQ.js} +2 -2
- package/dist/{chunk-Q4LVFNLN.js → chunk-IKQ5FODN.js} +4 -4
- package/dist/chunk-IKQ5FODN.js.map +1 -0
- package/dist/{chunk-2BQGFOA7.js → chunk-J6OCFNGU.js} +2 -2
- package/dist/chunk-J6OCFNGU.js.map +1 -0
- package/dist/{chunk-6OJAP6ZL.js → chunk-QFBOHPWZ.js} +3 -3
- package/dist/chunk-QFBOHPWZ.js.map +1 -0
- package/dist/{chunk-J27KH2A2.js → chunk-RVSKEBIC.js} +1 -1
- package/dist/chunk-RVSKEBIC.js.map +1 -0
- package/dist/{chunk-34XYOXHG.js → chunk-TFNRIZHU.js} +1615 -12
- package/dist/chunk-TFNRIZHU.js.map +1 -0
- package/dist/{chunk-FS2HJVVY.js → chunk-XAKC2QY7.js} +1204 -5
- package/dist/chunk-XAKC2QY7.js.map +1 -0
- package/dist/css/components/calendar.css +159 -0
- package/dist/css/components/chat-background.css +2 -2
- package/dist/css/components/chat.css +1 -1
- package/dist/css/components/cursor.css +3 -3
- package/dist/css/components/dialog.css +3 -3
- package/dist/css/components/hover-card.css +14 -0
- package/dist/css/components/input-otp.css +10 -0
- package/dist/css/components/island.css +1 -1
- package/dist/css/components/menu.css +1 -1
- package/dist/css/components/nav.css +1 -5
- package/dist/css/components/pagination.css +11 -0
- package/dist/css/components/quantity.css +16 -0
- package/dist/css/components/slot-picker.css +126 -0
- package/dist/css/components/steps.css +2 -2
- package/dist/css/components/studio-dock.css +35 -0
- package/dist/css/components/theme-picker.css +1 -1
- package/dist/css/components/toast.css +1 -1
- package/dist/css/components/trailer.css +37 -0
- package/dist/css/controls.css +78 -13
- package/dist/css/effects.css +8 -8
- package/dist/css/fonts.css +1 -1
- package/dist/css/themes/black-green.css +1 -1
- package/dist/css/themes/black.css +1 -1
- package/dist/css/themes/blue.css +1 -1
- package/dist/css/themes/brown.css +1 -1
- package/dist/css/themes/cold-blue.css +1 -1
- package/dist/css/themes/funny.css +1 -1
- package/dist/css/themes/gray.css +1 -1
- package/dist/css/themes/green.css +1 -1
- package/dist/css/themes/modern-1.css +1 -1
- package/dist/css/themes/neogreen.css +1 -1
- package/dist/css/themes/new.css +1 -1
- package/dist/css/themes/new2.css +1 -1
- package/dist/css/themes/oled.css +1 -1
- package/dist/css/themes/orange.css +1 -1
- package/dist/css/themes/pastel-pink.css +1 -1
- package/dist/css/themes/pink.css +1 -1
- package/dist/css/themes/purple.css +1 -1
- package/dist/css/themes/red.css +1 -1
- package/dist/css/themes/super-blue.css +1 -1
- package/dist/css/themes.css +19 -19
- package/dist/css/tokens.css +9 -9
- package/dist/elements/index.d.ts +75 -7
- package/dist/elements/index.js +172 -6
- package/dist/elements/index.js.map +1 -1
- package/dist/{engine-CUr9QijM.d.ts → engine-GDGwQnJE.d.ts} +2 -2
- package/dist/gl/index.js +3 -3
- package/dist/index.css +1076 -63
- package/dist/index.d.ts +17 -17
- package/dist/index.js +49 -7
- package/dist/index.js.map +1 -1
- package/dist/react/index.d.ts +139 -40
- package/dist/react/index.js +355 -8
- package/dist/react/index.js.map +1 -1
- package/dist/{reduced-BM4Ls0g5.d.ts → reduced-BmBzKH94.d.ts} +215 -18
- package/dist/{reveal-WFUWDrei.d.ts → reveal-jsX-uwUz.d.ts} +233 -24
- package/dist/{store-Bqy_obnD.d.ts → store-DYb3tTsZ.d.ts} +5 -5
- package/dist/theme/generate.d.ts +1 -1
- package/dist/theme/generate.js +2 -2
- package/dist/theme/generate.js.map +1 -1
- package/dist/theme/index.d.ts +2 -2
- package/dist/theme/index.js +2 -2
- package/dist/{theme-BFMoCzi6.d.ts → theme-Bjf9qIIy.d.ts} +1 -1
- package/package.json +2 -2
- package/registry.json +230 -156
- package/skills/motion-kit/SKILL.md +209 -189
- package/skills/motion-kit/reference/components.md +5 -5
- package/skills/motion-kit/reference/design.md +14 -14
- package/skills/motion-kit/reference/recipes.md +48 -49
- package/src/auto/auto-init.ts +38 -0
- package/src/components/calendar/README.md +82 -0
- package/src/components/calendar/calendar.css +158 -0
- package/src/components/calendar/element.ts +27 -0
- package/src/components/calendar/engine.ts +412 -0
- package/src/components/calendar/react.tsx +69 -0
- package/src/components/chat/README.md +1 -3
- package/src/components/chat/chat.css +1 -1
- package/src/components/chat/engine.ts +1 -1
- package/src/components/chat/react.tsx +1 -1
- package/src/components/chat-background/README.md +1 -3
- package/src/components/chat-background/audio.ts +1 -1
- package/src/components/chat-background/chat-background.css +2 -2
- package/src/components/chat-background/engine.ts +1 -1
- package/src/components/chat-background/react.tsx +1 -1
- package/src/components/cursor/README.md +3 -5
- package/src/components/cursor/cursor.css +3 -3
- package/src/components/cursor/element.ts +2 -2
- package/src/components/cursor/engine.ts +5 -5
- package/src/components/cursor/react.tsx +1 -1
- package/src/components/dialog/README.md +4 -8
- package/src/components/dialog/dialog.css +3 -3
- package/src/components/dialog/engine.ts +2 -2
- package/src/components/dialog/react.tsx +1 -1
- package/src/components/hover-card/README.md +16 -0
- package/src/components/hover-card/element.ts +8 -0
- package/src/components/hover-card/engine.ts +69 -0
- package/src/components/hover-card/hover-card.css +13 -0
- package/src/components/hover-card/react.tsx +19 -0
- package/src/components/input-otp/README.md +18 -0
- package/src/components/input-otp/element.ts +14 -0
- package/src/components/input-otp/engine.ts +106 -0
- package/src/components/input-otp/input-otp.css +9 -0
- package/src/components/input-otp/react.tsx +21 -0
- package/src/components/island/README.md +7 -7
- package/src/components/island/engine.ts +3 -3
- package/src/components/island/island.css +1 -1
- package/src/components/island/react.tsx +3 -3
- package/src/components/menu/README.md +2 -4
- package/src/components/menu/engine.ts +1 -1
- package/src/components/menu/menu.css +1 -1
- package/src/components/menu/react.tsx +1 -1
- package/src/components/nav/README.md +12 -14
- package/src/components/nav/engine.ts +5 -5
- package/src/components/nav/nav.css +1 -5
- package/src/components/nav/react.tsx +3 -3
- package/src/components/pagination/README.md +17 -0
- package/src/components/pagination/element.ts +14 -0
- package/src/components/pagination/engine.ts +102 -0
- package/src/components/pagination/pagination.css +10 -0
- package/src/components/pagination/react.tsx +22 -0
- package/src/components/quantity/README.md +20 -0
- package/src/components/quantity/element.ts +14 -0
- package/src/components/quantity/engine.ts +85 -0
- package/src/components/quantity/quantity.css +15 -0
- package/src/components/quantity/react.tsx +21 -0
- package/src/components/slot-picker/README.md +68 -0
- package/src/components/slot-picker/element.ts +27 -0
- package/src/components/slot-picker/engine.ts +412 -0
- package/src/components/slot-picker/react.tsx +76 -0
- package/src/components/slot-picker/slot-picker.css +125 -0
- package/src/components/steps/README.md +1 -3
- package/src/components/steps/engine.ts +3 -3
- package/src/components/steps/react.tsx +2 -2
- package/src/components/steps/steps.css +2 -2
- package/src/components/studio-dock/README.md +44 -0
- package/src/components/studio-dock/element.ts +11 -0
- package/src/components/studio-dock/engine.ts +212 -0
- package/src/components/studio-dock/react.tsx +23 -0
- package/src/components/studio-dock/studio-dock.css +34 -0
- package/src/components/theme-picker/README.md +3 -5
- package/src/components/theme-picker/engine.ts +2 -2
- package/src/components/theme-picker/react.tsx +1 -1
- package/src/components/theme-picker/theme-picker.css +1 -1
- package/src/components/toast/README.md +1 -3
- package/src/components/toast/engine.ts +2 -2
- package/src/components/toast/react.tsx +1 -1
- package/src/components/toast/store.ts +1 -2
- package/src/components/toast/toast.css +1 -1
- package/src/components/trailer/README.md +28 -0
- package/src/components/trailer/element.ts +13 -0
- package/src/components/trailer/engine.ts +189 -0
- package/src/components/trailer/react.tsx +16 -0
- package/src/components/trailer/trailer.css +36 -0
- package/src/core/channels.ts +3 -3
- package/src/core/curves.ts +3 -3
- package/src/core/dates.ts +98 -0
- package/src/core/gsap.ts +2 -2
- package/src/core/loop.ts +1 -1
- package/src/core/math.ts +1 -1
- package/src/core/physics-spring.ts +2 -2
- package/src/core/scroll-track.ts +2 -4
- package/src/core/spring.ts +1 -1
- package/src/core/timing.ts +21 -22
- package/src/css/controls/README.md +24 -20
- package/src/css/controls/burger.css +1 -1
- package/src/css/controls/button.css +4 -4
- package/src/css/controls/checkbox.css +1 -1
- package/src/css/controls/chip.css +1 -1
- package/src/css/controls/field.css +2 -2
- package/src/css/controls/glass.css +4 -4
- package/src/css/controls/status.css +63 -0
- package/src/css/default-theme.css +1 -1
- package/src/css/effects/README.md +19 -21
- package/src/css/effects/anim.css +1 -1
- package/src/css/effects/fade.css +1 -1
- package/src/css/effects/float.css +1 -1
- package/src/css/effects/frost.css +1 -1
- package/src/css/effects/grain.css +1 -1
- package/src/css/effects/loading.css +1 -1
- package/src/css/effects/success.css +1 -1
- package/src/css/effects/text.css +1 -1
- package/src/css/fonts.css +1 -1
- package/src/css/states.css +1 -1
- package/src/css/themes/black-green.css +1 -1
- package/src/css/themes/black.css +1 -1
- package/src/css/themes/blue.css +1 -1
- package/src/css/themes/brown.css +1 -1
- package/src/css/themes/cold-blue.css +1 -1
- package/src/css/themes/funny.css +1 -1
- package/src/css/themes/gray.css +1 -1
- package/src/css/themes/green.css +1 -1
- package/src/css/themes/modern-1.css +1 -1
- package/src/css/themes/neogreen.css +1 -1
- package/src/css/themes/new.css +1 -1
- package/src/css/themes/new2.css +1 -1
- package/src/css/themes/oled.css +1 -1
- package/src/css/themes/orange.css +1 -1
- package/src/css/themes/pastel-pink.css +1 -1
- package/src/css/themes/pink.css +1 -1
- package/src/css/themes/purple.css +1 -1
- package/src/css/themes/red.css +1 -1
- package/src/css/themes/super-blue.css +1 -1
- package/src/css/tokens.css +8 -8
- package/src/elements/index.ts +24 -0
- package/src/gl/liquid-blend/README.md +3 -3
- package/src/gl/matcap/README.md +3 -3
- package/src/gl/particle-hero/README.md +1 -1
- package/src/gl/particle-hero/create-particle-hero.ts +3 -5
- package/src/gl/particle-hero/shaders.ts +1 -1
- package/src/gl/shader-blur/README.md +3 -3
- package/src/gl/shader-grain/README.md +3 -3
- package/src/index.ts +25 -0
- package/src/motion/chat.ts +2 -2
- package/src/motion/confetti.ts +1 -1
- package/src/motion/count.ts +1 -1
- package/src/motion/details.ts +1 -1
- package/src/motion/drag-dismiss.ts +1 -1
- package/src/motion/effects.ts +1 -1
- package/src/motion/entrances.ts +1 -1
- package/src/motion/floating.ts +1 -1
- package/src/motion/flow.ts +2 -2
- package/src/motion/indicator.ts +1 -1
- package/src/motion/layout.ts +1 -1
- package/src/motion/liquid.ts +1 -1
- package/src/motion/morph.ts +2 -2
- package/src/motion/reveal.ts +1 -1
- package/src/motion/sheet.ts +1 -1
- package/src/motion/surface.ts +1 -1
- package/src/react/animated-number.tsx +2 -2
- package/src/react/flip-group.tsx +1 -1
- package/src/react/frost.tsx +1 -1
- package/src/react/index.ts +21 -0
- package/src/react/slot-text.tsx +1 -1
- package/src/react/use-action-button.ts +6 -0
- package/src/react/use-card-dock.ts +1 -1
- package/src/react/use-cascade.ts +1 -1
- package/src/react/use-contrast.ts +1 -1
- package/src/react/use-depth-grid.ts +1 -1
- package/src/react/use-editorial-reveal.ts +7 -0
- package/src/react/use-fill-text.ts +1 -1
- package/src/react/use-film-hero.ts +6 -0
- package/src/react/use-font-cycle.ts +7 -0
- package/src/react/use-frame-zoom.ts +1 -1
- package/src/react/use-image-zoom.ts +5 -0
- package/src/react/use-joined-cta.ts +5 -0
- package/src/react/use-letters.ts +1 -1
- package/src/react/use-local-clock.ts +5 -0
- package/src/react/use-marquee.ts +1 -1
- package/src/react/use-mask-text.ts +1 -1
- package/src/react/use-parallax.ts +1 -1
- package/src/react/use-particle-hero.ts +1 -1
- package/src/react/use-scroll-dock.ts +5 -0
- package/src/react/use-snap-strip.ts +1 -1
- package/src/react/use-sticker.ts +5 -0
- package/src/react/use-studio-dock.ts +8 -0
- package/src/react/use-text-roll.ts +1 -1
- package/src/react/use-thumbnail-nav.ts +7 -0
- package/src/react/use-trailer.ts +6 -0
- package/src/react/use-weight-hover.ts +5 -0
- package/src/recipes/action-button/README.md +31 -0
- package/src/recipes/action-button/action-button.css +113 -0
- package/src/recipes/action-button/create-action-button.ts +56 -0
- package/src/recipes/card-dock/README.md +1 -1
- package/src/recipes/card-dock/card-dock.css +1 -1
- package/src/recipes/card-dock/create-card-dock.ts +2 -2
- package/src/recipes/cascade/README.md +1 -3
- package/src/recipes/cascade/create-cascade.ts +3 -3
- package/src/recipes/contrast/README.md +0 -2
- package/src/recipes/contrast/create-contrast.ts +1 -1
- package/src/recipes/counter/README.md +0 -2
- package/src/recipes/counter/create-counter.ts +1 -1
- package/src/recipes/curved-carousel/README.md +0 -2
- package/src/recipes/curved-carousel/create-curved-carousel.ts +2 -2
- package/src/recipes/curved-carousel/curved-carousel.css +1 -1
- package/src/recipes/depth-grid/README.md +0 -2
- package/src/recipes/depth-grid/create-depth-grid.ts +3 -4
- package/src/recipes/details/README.md +0 -2
- package/src/recipes/details/create-details.ts +1 -1
- package/src/recipes/dock-pill/README.md +3 -3
- package/src/recipes/drag-ruler/README.md +3 -3
- package/src/recipes/editorial-reveal/README.md +35 -0
- package/src/recipes/editorial-reveal/create-editorial-reveal.ts +91 -0
- package/src/recipes/editorial-reveal/editorial-reveal.css +17 -0
- package/src/recipes/fill-text/README.md +1 -1
- package/src/recipes/fill-text/create-fill-text.ts +1 -1
- package/src/recipes/film-hero/README.md +23 -0
- package/src/recipes/film-hero/create-film-hero.ts +65 -0
- package/src/recipes/film-hero/film-hero.css +29 -0
- package/src/recipes/fit-text/README.md +0 -2
- package/src/recipes/flow/README.md +0 -2
- package/src/recipes/flow/create-flow.ts +1 -1
- package/src/recipes/font-cycle/README.md +11 -0
- package/src/recipes/font-cycle/create-font-cycle.ts +126 -0
- package/src/recipes/font-cycle/font-cycle.css +36 -0
- package/src/recipes/frame-zoom/README.md +1 -3
- package/src/recipes/frame-zoom/create-frame-zoom.ts +2 -2
- package/src/recipes/image-zoom/README.md +16 -0
- package/src/recipes/image-zoom/create-image-zoom.ts +27 -0
- package/src/recipes/image-zoom/image-zoom.css +7 -0
- package/src/recipes/infinite-reel/README.md +3 -3
- package/src/recipes/joined-cta/README.md +13 -0
- package/src/recipes/joined-cta/create-joined-cta.ts +22 -0
- package/src/recipes/joined-cta/joined-cta.css +17 -0
- package/src/recipes/letters/README.md +2 -4
- package/src/recipes/letters/create-letters.ts +3 -3
- package/src/recipes/local-clock/README.md +31 -0
- package/src/recipes/local-clock/create-local-clock.ts +61 -0
- package/src/recipes/marquee/README.md +0 -2
- package/src/recipes/marquee/create-marquee.ts +3 -3
- package/src/recipes/marquee/marquee.css +1 -1
- package/src/recipes/mask-text/README.md +3 -5
- package/src/recipes/mask-text/create-mask-text.ts +4 -4
- package/src/recipes/parallax/README.md +1 -3
- package/src/recipes/parallax/create-parallax.ts +1 -1
- package/src/recipes/pile/README.md +0 -2
- package/src/recipes/pile/create-pile.ts +1 -1
- package/src/recipes/pile/pile-math.ts +1 -1
- package/src/recipes/pile/pile.css +1 -1
- package/src/recipes/pill/README.md +0 -2
- package/src/recipes/pill/pill.css +2 -2
- package/src/recipes/pop/README.md +0 -2
- package/src/recipes/pop/create-pop.ts +1 -1
- package/src/recipes/radial-menu/README.md +0 -2
- package/src/recipes/radial-menu/create-radial-menu.ts +1 -1
- package/src/recipes/radial-menu/radial-menu.css +1 -1
- package/src/recipes/reflow/README.md +3 -5
- package/src/recipes/reflow/create-reflow.ts +1 -1
- package/src/recipes/repel-bento/README.md +3 -3
- package/src/recipes/reveal/README.md +0 -2
- package/src/recipes/reveal/create-reveal.ts +1 -1
- package/src/recipes/rotator/README.md +3 -3
- package/src/recipes/scroll-dock/README.md +46 -0
- package/src/recipes/scroll-dock/create-scroll-dock.ts +72 -0
- package/src/recipes/scroll-dock/scroll-dock.css +70 -0
- package/src/recipes/slot-text/README.md +0 -2
- package/src/recipes/slot-text/create-slot-text.ts +1 -1
- package/src/recipes/snap-strip/README.md +1 -1
- package/src/recipes/snap-strip/create-snap-strip.ts +5 -6
- package/src/recipes/snap-strip/snap-strip.css +1 -1
- package/src/recipes/sortable/README.md +3 -5
- package/src/recipes/sortable/create-sortable.ts +4 -4
- package/src/recipes/sortable/sortable.css +1 -1
- package/src/recipes/spring-cards/README.md +0 -2
- package/src/recipes/spring-cards/create-spring-cards.ts +1 -1
- package/src/recipes/spring-cards/spring-cards.css +1 -1
- package/src/recipes/sticker/README.md +59 -0
- package/src/recipes/sticker/create-sticker.ts +490 -0
- package/src/recipes/sticker/sticker.css +112 -0
- package/src/recipes/stretch-footer/README.md +0 -2
- package/src/recipes/stretch-footer/create-stretch-footer.ts +1 -1
- package/src/recipes/stretch-footer/stretch-footer.css +1 -1
- package/src/recipes/tabs/README.md +0 -2
- package/src/recipes/tabs/create-tabs.ts +1 -1
- package/src/recipes/text-roll/README.md +1 -3
- package/src/recipes/text-roll/create-text-roll.ts +3 -3
- package/src/recipes/text-roll/text-roll.css +1 -1
- package/src/recipes/thumbnail-nav/README.md +16 -0
- package/src/recipes/thumbnail-nav/create-thumbnail-nav.ts +165 -0
- package/src/recipes/thumbnail-nav/thumbnail-nav.css +108 -0
- package/src/recipes/ticket-menu/README.md +3 -3
- package/src/recipes/weight-hover/README.md +37 -0
- package/src/recipes/weight-hover/create-weight-hover.ts +136 -0
- package/src/recipes/weight-hover/weight-hover.css +7 -0
- package/src/recipes/words/README.md +0 -2
- package/src/recipes/words/create-words.ts +1 -1
- package/src/theme/generate.ts +1 -1
- package/src/theme/presets.ts +2 -2
- package/src/theme/theme.ts +1 -2
- package/dist/chunk-2BQGFOA7.js.map +0 -1
- package/dist/chunk-34XYOXHG.js.map +0 -1
- package/dist/chunk-6OJAP6ZL.js.map +0 -1
- package/dist/chunk-FS2HJVVY.js.map +0 -1
- package/dist/chunk-J27KH2A2.js.map +0 -1
- package/dist/chunk-KLY5BRRI.js.map +0 -1
- package/dist/chunk-Q4LVFNLN.js.map +0 -1
- package/dist/chunk-RFDIZYFX.js.map +0 -1
- /package/dist/{chunk-ABNRDDO7.js.map → chunk-I3DXF2KQ.js.map} +0 -0
|
@@ -9,12 +9,12 @@ import { ICON_PATHS } from '../icons';
|
|
|
9
9
|
|
|
10
10
|
/*
|
|
11
11
|
* Two cursors.
|
|
12
|
-
* chip
|
|
12
|
+
* chip: the system cursor stays; over [data-mk-cursor="Label"] a glass
|
|
13
13
|
* chip with the label follows the pointer on a Motion 800/35 spring, popping in
|
|
14
14
|
* from scale .8 (500/30); data-mk-cursor-shape="circle" makes it a 96 px disc;
|
|
15
15
|
* it stays 10 px inside the window; its ink is the accent on light backgrounds
|
|
16
|
-
* and white on dark ones
|
|
17
|
-
* dot
|
|
16
|
+
* and white on dark ones.
|
|
17
|
+
* dot: inside its zone a disc replaces the cursor and eases after it
|
|
18
18
|
* (lerp .2 a frame) with a word (data-mk-cursor or `text`); pressing shrinks it
|
|
19
19
|
* to .7, drops the word and slides two arrows in; [data-mk-cursor-hide] hides it.
|
|
20
20
|
* Off on touch screens and with reduced motion.
|
|
@@ -44,7 +44,7 @@ export interface CursorInstance extends Instance {
|
|
|
44
44
|
const svg = (extra = '') =>
|
|
45
45
|
`<svg aria-hidden="true" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"${extra}><path d="${ICON_PATHS.send}"/></svg>`;
|
|
46
46
|
|
|
47
|
-
/**
|
|
47
|
+
/** the first background with alpha > .1 up the tree; light when (299R + 587G + 114B) / 1000 > 180. */
|
|
48
48
|
function backgroundTone(el: Element | null): 'light' | 'dark' {
|
|
49
49
|
if (!el) return 'light';
|
|
50
50
|
if (el.closest('img, video, picture, canvas')) return 'dark';
|
|
@@ -199,7 +199,7 @@ export function createCursor(host: HTMLElement | null, options: CursorOptions =
|
|
|
199
199
|
const [stiffness, damping] = TIMING.springs.cursorFollow;
|
|
200
200
|
const update = (dt = 1) => {
|
|
201
201
|
if (variant === 'chip') {
|
|
202
|
-
//
|
|
202
|
+
// Keeps the chip inside the window, 10 px from its edges.
|
|
203
203
|
const halfW = size.width / 2 + t.gutter;
|
|
204
204
|
const halfH = size.height / 2 + t.gutter;
|
|
205
205
|
const tx = size.width ? Math.min(Math.max(pointer.x, halfW), innerWidth - halfW) : pointer.x;
|
|
@@ -5,7 +5,7 @@ import { useIsomorphicLayoutEffect } from '../../react/use-recipe';
|
|
|
5
5
|
import { createCursor, type CursorOptions, type CursorVariant } from './engine';
|
|
6
6
|
|
|
7
7
|
export interface CursorProps {
|
|
8
|
-
/** 'chip' (
|
|
8
|
+
/** 'chip' (default) or 'dot'. */
|
|
9
9
|
variant?: CursorVariant;
|
|
10
10
|
/** dot: the word when nothing under the pointer gives one. */
|
|
11
11
|
text?: string;
|
|
@@ -1,21 +1,19 @@
|
|
|
1
1
|
# dialog
|
|
2
2
|
|
|
3
|
-
El diálogo
|
|
3
|
+
El diálogo sobre `<dialog>` nativo (foco atrapado, Escape, capa superior). El botón que lo abre se convierte en el diálogo y al cerrar vuelve a él.
|
|
4
4
|
|
|
5
5
|
- **Abrir desde el botón (morph):** la superficie viaja recortada desde el rect del botón en 0.7 s con la curva signature y toma su color en 0.3 s; el contenido crece como grupo desde la caja del botón con blur 8 → 0. El botón se oculta en 80 ms mientras está "dentro".
|
|
6
6
|
- **Elementos compartidos:** `data-mk-shared="titulo"` en una pieza del botón y otra del diálogo: el texto vuela de una a otra (0.7 s al abrir, 0.5 s al cerrar).
|
|
7
7
|
- **Sin botón en pantalla:** escala .92 → 1 en 0.35 s (`power2.out`).
|
|
8
8
|
- **Cerrar:** el diálogo se cierra al instante (el foco vuelve y la página responde) mientras una copia inerte se pliega al botón: forma 0.5 s, contenido con blur → 32 px (`.37,.35,0,1`) y fundido (`.56,.27,0,1`), velo `power3.in`; el botón reaparece 0.3 s tras 0.2 s. Sin botón: escala .9 + blur 32.
|
|
9
9
|
- **Líquido** (`variant="liquid"`): dos blobs bajo filtro goo; posición con resorte 144/14 y patada de 2400 px/s, tamaño 0.3 s `cubic(.8,.3,.5,.8)`, radio 0.7 s, contenido `scale 2 → 1` y blur 8 → 0, color 0.4 s tras 0.15 s. Cierre con resorte 130/18.
|
|
10
|
-
- **Backdrop:** tinte 0.5 s con la curva signature (`linear()`
|
|
10
|
+
- **Backdrop:** tinte 0.5 s con la curva signature (`linear()`).
|
|
11
11
|
- **Auto-resize:** si el contenido crece o se encoge, la altura se ajusta en 0.45 s (signature).
|
|
12
12
|
- **Arrastrar para cerrar:** sigue al puntero a la mitad y encoge (`1 − y/400`); pasados 100 px se cierra donde quedó, si no vuelve con `elastic.out(.5,.5)` 0.5 s. También con la rueda. Ignora controles, áreas con scroll y `[data-mk-no-drag]`.
|
|
13
13
|
- **Ventana flotante** (`float="clave"`, escritorio ≥ 680 px): el asa la levanta (la página sigue usable), se mueve con resorte 1100/38 e inercia, se queda dentro de la pantalla, se redimensiona por las esquinas (mín. 320 × 240) y recuerda su sitio. Un clic en el asa la devuelve.
|
|
14
14
|
- **Móvil (< 680 px):** pantalla completa, radio 0.
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
- **Sheet** (`variant="sheet"`, de Dousan Miao): la hoja sube desde abajo (`y 100% → 0`, 0.72 s `cubic-bezier(.16,1,.3,1)`) y lo marcado con `[data-mk-underlay]` se hunde a escala .98 y opacidad .4 con un resorte 300/30. Al cerrar, una copia inerte cae por debajo de la pantalla y el foco vuelve al instante. No sale del botón. Mientras está abierta, la página no hace scroll.
|
|
16
|
+
- **Sheet** (`variant="sheet"`): la hoja sube desde abajo (`y 100% → 0`, 0.72 s `cubic-bezier(.16,1,.3,1)`) y lo marcado con `[data-mk-underlay]` se hunde a escala .98 y opacidad .4 con un resorte 300/30. Al cerrar, una copia inerte cae por debajo de la pantalla y el foco vuelve al instante. No sale del botón. Mientras está abierta, la página no hace scroll.
|
|
19
17
|
|
|
20
18
|
## HTML / Astro
|
|
21
19
|
|
|
@@ -60,7 +58,7 @@ Las opciones van en `Dialog.Root` (`variant`, `placement`, `float`…). Controla
|
|
|
60
58
|
|
|
61
59
|
| Opción | Atributo | Por defecto | Qué hace |
|
|
62
60
|
|---|---|---|---|
|
|
63
|
-
| `variant` | `variant` | `'morph'` | `'liquid'` para la versión líquida; `'sheet'` para la hoja que sube desde abajo
|
|
61
|
+
| `variant` | `variant` | `'morph'` | `'liquid'` para la versión líquida; `'sheet'` para la hoja que sube desde abajo |
|
|
64
62
|
| `placement` | `placement` | `'center'` | `'anchor'` junto al botón, `'side'` panel lateral |
|
|
65
63
|
| `backdrop` | `no-backdrop` | `true` | Velo tintado |
|
|
66
64
|
| `closeOnBackdrop` | `persistent` | `true` | Clic fuera cierra |
|
|
@@ -89,8 +87,6 @@ Las opciones van en `Dialog.Root` (`variant`, `placement`, `float`…). Controla
|
|
|
89
87
|
|
|
90
88
|
Ancho hasta `--mk-sheet-max-width` (720px), alto hasta `100dvh − 24px`, radio solo arriba; en el móvil ocupa todo el ancho. Se cierra con Escape, con el fondo, arrastrando hacia abajo o con `[data-mk-close]`. Con movimiento reducido aparece y desaparece sin animar y la página de detrás no cambia. Funciones sueltas: `sheetOpen`, `sheetClose`, `pushUnderlay`, `releaseUnderlay`.
|
|
91
89
|
|
|
92
|
-
Origen: dousanmiao.com, panel de proyecto a pantalla completa (`modal-project-fullscreen`).
|
|
93
|
-
|
|
94
90
|
## Controles y eventos
|
|
95
91
|
|
|
96
92
|
`open(origen?)`, `close()`, `toggle(origen?)`, `float()`, `dock()`, `isOpen`, `isFloating`, `destroy()`.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* dialog —
|
|
1
|
+
/* dialog — modal surface: 400 px, radius 40, tonal surface; full screen under 680 px. */
|
|
2
2
|
.mk-dialog {
|
|
3
3
|
--mk-dialog-bg: var(--mk-surface-2);
|
|
4
4
|
--mk-dialog-radius: var(--mk-radius-dialog);
|
|
@@ -218,7 +218,7 @@ mk-dialog:not(:defined) {
|
|
|
218
218
|
display: none;
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
-
/* Phones: dialogs take the whole screen with square corners
|
|
221
|
+
/* Phones: dialogs take the whole screen with square corners. */
|
|
222
222
|
@media (max-width: 679px) {
|
|
223
223
|
.mk-dialog:not([data-mk-placement='anchor']) {
|
|
224
224
|
width: 100vw;
|
|
@@ -238,7 +238,7 @@ mk-dialog:not(:defined) {
|
|
|
238
238
|
}
|
|
239
239
|
}
|
|
240
240
|
|
|
241
|
-
/* Sheet
|
|
241
|
+
/* Sheet: pinned to the bottom, up to 720 px wide, rises from below. Also on phones. */
|
|
242
242
|
.mk-dialog[data-mk-variant='sheet'] {
|
|
243
243
|
--mk-dialog-width: var(--mk-sheet-max-width, 720px);
|
|
244
244
|
/* A modal <dialog> is inset: 0 — anchored only to the bottom, it takes its content's height. */
|
|
@@ -20,7 +20,7 @@ import { pushUnderlay, releaseUnderlay, sheetClose, sheetOpen } from '../../moti
|
|
|
20
20
|
import { closeModal, fadeBackdrop, openModal } from '../../motion/surface';
|
|
21
21
|
|
|
22
22
|
/*
|
|
23
|
-
*
|
|
23
|
+
* Dialog on a native <dialog> (focus trap, Escape, top layer): the
|
|
24
24
|
* control that opened it turns into it and it folds back into that control
|
|
25
25
|
* (morph or liquid) over a backdrop tinted on the signature curve. It eases to
|
|
26
26
|
* new heights (auto-resize), can be dragged away (> 100 px) and, on desktop,
|
|
@@ -38,7 +38,7 @@ export interface DialogLabels {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
export interface DialogOptions {
|
|
41
|
-
/** morph (
|
|
41
|
+
/** morph (default), liquid, or sheet: rises from the bottom and pushes [data-mk-underlay] back. */
|
|
42
42
|
variant?: DialogVariant;
|
|
43
43
|
placement?: DialogPlacement;
|
|
44
44
|
/** Tinted backdrop; false leaves it transparent (menus). */
|
|
@@ -173,7 +173,7 @@ function Handle({ children, ...props }: ButtonHTMLAttributes<HTMLButtonElement>)
|
|
|
173
173
|
}
|
|
174
174
|
|
|
175
175
|
/**
|
|
176
|
-
*
|
|
176
|
+
* dialog for React:
|
|
177
177
|
* <Dialog.Root><Dialog.Trigger>Crear</Dialog.Trigger><Dialog.Content>
|
|
178
178
|
* <Dialog.Header><Dialog.Title>…</Dialog.Title><Dialog.Close /></Dialog.Header>
|
|
179
179
|
* <Dialog.Body>…</Dialog.Body><Dialog.Footer>…</Dialog.Footer>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# hover-card
|
|
2
|
+
|
|
3
|
+
Vista previa no modal, accesible al señalar, enfocar o tocar. Los enlaces/botones del panel pueden recibir foco.
|
|
4
|
+
|
|
5
|
+
```html
|
|
6
|
+
<mk-hover-card class="mk-hover-card" placement="bottom">
|
|
7
|
+
<button type="button" data-mk-hover-trigger>Ver estudio</button>
|
|
8
|
+
<div data-mk-hover-panel hidden role="region" aria-label="Vista previa">Contenido</div>
|
|
9
|
+
</mk-hover-card>
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
`defineElements()`, `createHoverCard(root, options)`, o `<HoverCard trigger="Ver estudio" placement="bottom">Contenido</HoverCard>`.
|
|
13
|
+
|
|
14
|
+
Opciones: `openDelay=120` ms, `closeDelay=140` ms, `placement='top'|'bottom'`, `onOpenChange(boolean)`. API `open()`, `close()`, `isOpen`, `destroy()`. Evento `mk:open-change` con `{open}`.
|
|
15
|
+
|
|
16
|
+
Escape cierra y retorna al disparador; salir con foco y puntero fuera o tocar fuera cierra. El panel se posiciona respecto al root: mantener un ancestro que permita overflow; no crea portal. No atrapa el foco. Entrada .28 s, instantánea bajo movimiento reducido. Limpia timers y restaura todos los atributos.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { MkElement, defineElement, type OptionType } from '../../elements/base';
|
|
2
|
+
import { createHoverCard, type HoverCardInstance } from './engine';
|
|
3
|
+
/** Light DOM: supply the same child markup shown in the README. */
|
|
4
|
+
export class MkHoverCardElement extends MkElement<HoverCardInstance> {
|
|
5
|
+
static options: Record<string, OptionType> = { openDelay: 'number', closeDelay: 'number', placement: 'string' };
|
|
6
|
+
protected create(options: Record<string, unknown>) { return createHoverCard(this, options); }
|
|
7
|
+
}
|
|
8
|
+
export const defineHoverCard = () => defineElement('mk-hover-card', MkHoverCardElement);
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { createScope } from '../../core/scope';
|
|
2
|
+
import { inert, type Instance } from '../../core/types';
|
|
3
|
+
|
|
4
|
+
export interface HoverCardOptions {
|
|
5
|
+
openDelay?: number;
|
|
6
|
+
closeDelay?: number;
|
|
7
|
+
placement?: 'top' | 'bottom';
|
|
8
|
+
onOpenChange?: (open: boolean) => void;
|
|
9
|
+
}
|
|
10
|
+
export interface HoverCardInstance extends Instance {
|
|
11
|
+
open(): void;
|
|
12
|
+
close(): void;
|
|
13
|
+
readonly isOpen: boolean;
|
|
14
|
+
}
|
|
15
|
+
let sequence = 0;
|
|
16
|
+
|
|
17
|
+
/** Nonmodal preview: pointer, keyboard focus and tap all reveal the same panel. */
|
|
18
|
+
export function createHoverCard(root: HTMLElement | null, options: HoverCardOptions = {}): HoverCardInstance {
|
|
19
|
+
const trigger = root?.querySelector<HTMLElement>('[data-mk-hover-trigger]');
|
|
20
|
+
const panel = root?.querySelector<HTMLElement>('[data-mk-hover-panel]');
|
|
21
|
+
if (!root || !trigger || !panel) return inert({ open() {}, close() {}, isOpen: false });
|
|
22
|
+
const scope = createScope();
|
|
23
|
+
const openDelay = Number.isFinite(options.openDelay) ? Math.max(0, options.openDelay!) : 120;
|
|
24
|
+
const closeDelay = Number.isFinite(options.closeDelay) ? Math.max(0, options.closeDelay!) : 140;
|
|
25
|
+
let opened = false;
|
|
26
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
27
|
+
let pointerInside = false;
|
|
28
|
+
let id = panel.id;
|
|
29
|
+
if (!id) { do { id = `mk-hover-${++sequence}`; } while (document.getElementById(id)); }
|
|
30
|
+
scope.attr(panel, 'id', id);
|
|
31
|
+
scope.attr(trigger, 'aria-controls', id);
|
|
32
|
+
scope.attr(root, 'data-mk-placement', options.placement ?? 'top');
|
|
33
|
+
scope.attr(root, 'data-mk-ready', '');
|
|
34
|
+
const cancel = () => { clearTimeout(timer); timer = undefined; };
|
|
35
|
+
const set = (value: boolean) => {
|
|
36
|
+
cancel();
|
|
37
|
+
if (scope.disposed) return;
|
|
38
|
+
const changed = value !== opened;
|
|
39
|
+
opened = value;
|
|
40
|
+
scope.attr(trigger, 'aria-expanded', String(value));
|
|
41
|
+
scope.attr(panel, 'hidden', value ? null : '');
|
|
42
|
+
scope.attr(root, 'data-mk-open', value ? '' : null);
|
|
43
|
+
if (changed) {
|
|
44
|
+
options.onOpenChange?.(value);
|
|
45
|
+
root.dispatchEvent(new CustomEvent('mk:open-change', { bubbles: true, detail: { open: value } }));
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
const schedule = (value: boolean, delay: number) => { cancel(); timer = setTimeout(() => set(value), delay); };
|
|
49
|
+
scope.on(root, 'pointerenter', () => { pointerInside = true; schedule(true, openDelay); });
|
|
50
|
+
scope.on(root, 'pointerleave', () => {
|
|
51
|
+
pointerInside = false;
|
|
52
|
+
if (!root.contains(document.activeElement)) schedule(false, closeDelay);
|
|
53
|
+
});
|
|
54
|
+
scope.on(root, 'focusin', () => set(true));
|
|
55
|
+
scope.on(root, 'focusout', (event) => {
|
|
56
|
+
if (!pointerInside && !root.contains((event as FocusEvent).relatedTarget as Node | null)) schedule(false, closeDelay);
|
|
57
|
+
});
|
|
58
|
+
scope.on(trigger, 'click', () => set(true));
|
|
59
|
+
scope.on(document, 'pointerdown', (event) => { if (!root.contains(event.target as Node)) set(false); });
|
|
60
|
+
scope.on(root, 'keydown', (event) => {
|
|
61
|
+
if (event.key !== 'Escape' || !opened) return;
|
|
62
|
+
event.preventDefault();
|
|
63
|
+
trigger.focus();
|
|
64
|
+
set(false);
|
|
65
|
+
});
|
|
66
|
+
scope.add(cancel);
|
|
67
|
+
set(false);
|
|
68
|
+
return { open: () => set(true), close: () => set(false), get isOpen() { return opened; }, destroy: () => scope.dispose() };
|
|
69
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
.mk-hover-card, mk-hover-card { position: relative; display: inline-flex; }
|
|
2
|
+
.mk-hover-card [data-mk-hover-panel], mk-hover-card [data-mk-hover-panel] {
|
|
3
|
+
position: absolute; z-index: 10; bottom: calc(100% + .75rem); left: 0;
|
|
4
|
+
width: min(19rem, calc(100vw - 3rem)); padding: 1.25rem; border-radius: var(--mk-radius-lg);
|
|
5
|
+
color: var(--mk-text); background: var(--mk-surface-3); border: 1px solid var(--mk-outline-soft);
|
|
6
|
+
box-shadow: 0 12px 40px color-mix(in srgb, var(--mk-shadow-color) 20%, transparent);
|
|
7
|
+
animation: mk-hover-card-in .28s cubic-bezier(.2,.8,.2,1) both;
|
|
8
|
+
}
|
|
9
|
+
.mk-hover-card [data-mk-hover-panel]::after, mk-hover-card [data-mk-hover-panel]::after { content: ''; position: absolute; inset: -1rem 0; z-index: -1; }
|
|
10
|
+
.mk-hover-card[data-mk-placement=bottom] [data-mk-hover-panel], mk-hover-card[data-mk-placement=bottom] [data-mk-hover-panel] { top: calc(100% + .75rem); bottom: auto; }
|
|
11
|
+
.mk-hover-card [hidden], mk-hover-card [hidden] { display: none !important; }
|
|
12
|
+
@keyframes mk-hover-card-in { from { opacity: 0; transform: translateY(7px) scale(.97); } to { opacity: 1; transform: none; } }
|
|
13
|
+
@media (prefers-reduced-motion: reduce) { .mk-hover-card [data-mk-hover-panel], mk-hover-card [data-mk-hover-panel] { animation: none; } }
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useRef, type HTMLAttributes, type ReactNode } from 'react';
|
|
3
|
+
import { useIsomorphicLayoutEffect, useLatest } from '../../react/use-recipe';
|
|
4
|
+
import { createHoverCard, type HoverCardOptions } from './engine';
|
|
5
|
+
export interface HoverCardProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange'>, HoverCardOptions {
|
|
6
|
+
trigger: ReactNode; children: ReactNode; triggerLabel?: string; panelLabel?: string;
|
|
7
|
+
}
|
|
8
|
+
export function HoverCard({ trigger, children, triggerLabel, panelLabel = 'Vista previa', openDelay, closeDelay, placement, onOpenChange, className, ...props }: HoverCardProps) {
|
|
9
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
10
|
+
const change = useLatest(onOpenChange);
|
|
11
|
+
useIsomorphicLayoutEffect(() => {
|
|
12
|
+
const instance = createHoverCard(ref.current, { openDelay, closeDelay, placement, onOpenChange: (n) => change.current?.(n) });
|
|
13
|
+
return () => instance.destroy();
|
|
14
|
+
}, [openDelay, closeDelay, placement]);
|
|
15
|
+
return <div {...props} ref={ref} className={['mk-hover-card', className].filter(Boolean).join(' ')}>
|
|
16
|
+
<button type="button" className="mk-button mk-button--tonal" data-mk-hover-trigger aria-label={triggerLabel} aria-expanded="false">{trigger}</button>
|
|
17
|
+
<div data-mk-hover-panel role="region" aria-label={panelLabel} hidden>{children}</div>
|
|
18
|
+
</div>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# input-otp
|
|
2
|
+
|
|
3
|
+
OTP numérico con un único input nativo y slots decorativos. Conserva edición, selección, pegado, ceros iniciales y autocomplete=one-time-code.
|
|
4
|
+
|
|
5
|
+
```html
|
|
6
|
+
<mk-input-otp class="mk-input-otp" length="6">
|
|
7
|
+
<span data-mk-otp-slots aria-hidden="true"></span>
|
|
8
|
+
<input data-mk-otp-input type="text" name="code" aria-label="Código de verificación" />
|
|
9
|
+
</mk-input-otp>
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
`defineElements()`, `createInputOtp(root, options)`, o `<InputOtp length={6} onChange={setCode} onComplete={verify} />`.
|
|
13
|
+
|
|
14
|
+
Opciones: `length=6` (1…16), `value`, `mask=false`, `onChange(value)`, `onComplete(value)`. API `setValue(text, notify=true)`, `focus()`, `value`, `destroy()`. Limpia caracteres no ASCII numéricos y limita longitud. Eventos `mk:change {value,complete}`, `mk:complete {value}`; escribir el mismo valor no repite callbacks. No envía ni valida códigos contra un servidor.
|
|
15
|
+
|
|
16
|
+
React acepta `defaultValue`, `label`, `name`, `disabled`, `required`, `invalid`. Variables `--mk-otp-size`, `--mk-otp-gap`; foco y error usan tokens. Sin movimiento con reduced-motion. Al destruir restaura input y nodos previos.
|
|
17
|
+
|
|
18
|
+
El reset nativo del formulario sincroniza el control sin emitir callbacks de edición. Los atributos de custom elements conservan el estado editado si value no está presente; value explícito tiene precedencia.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { MkElement, defineElement, type OptionType } from '../../elements/base';
|
|
2
|
+
import { createInputOtp, type InputOtpInstance } from './engine';
|
|
3
|
+
/** Light DOM: supply the same child markup shown in the README. */
|
|
4
|
+
export class MkInputOtpElement extends MkElement<InputOtpInstance> {
|
|
5
|
+
static options: Record<string, OptionType> = { length: 'number', value: 'string', mask: 'boolean' };
|
|
6
|
+
protected create(options: Record<string, unknown>) { return createInputOtp(this, options); }
|
|
7
|
+
attributeChangedCallback(name: string, previous: string | null, next: string | null) {
|
|
8
|
+
if (previous === next || !this.isConnected || !this.instance) return;
|
|
9
|
+
const value = this.instance.value;
|
|
10
|
+
this.instance.destroy();
|
|
11
|
+
this.instance = this.create({ ...(!this.hasAttribute('value') ? { value } : {}), ...this.options });
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export const defineInputOtp = () => defineElement('mk-input-otp', MkInputOtpElement);
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { createScope } from '../../core/scope';
|
|
2
|
+
import { inert, type Instance } from '../../core/types';
|
|
3
|
+
|
|
4
|
+
export interface InputOtpOptions {
|
|
5
|
+
length?: number;
|
|
6
|
+
value?: string;
|
|
7
|
+
mask?: boolean;
|
|
8
|
+
onChange?: (value: string) => void;
|
|
9
|
+
onComplete?: (value: string) => void;
|
|
10
|
+
}
|
|
11
|
+
export interface InputOtpInstance extends Instance {
|
|
12
|
+
setValue(value: string, notify?: boolean): void;
|
|
13
|
+
focus(): void;
|
|
14
|
+
readonly value: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** One real input preserves native editing, mobile autofill and password managers. */
|
|
18
|
+
export function createInputOtp(root: HTMLElement | null, options: InputOtpOptions = {}): InputOtpInstance {
|
|
19
|
+
const input = root?.querySelector<HTMLInputElement>('[data-mk-otp-input]');
|
|
20
|
+
const slots = root?.querySelector<HTMLElement>('[data-mk-otp-slots]');
|
|
21
|
+
if (!root || !input || !slots) return inert({ setValue() {}, focus() {}, value: '' });
|
|
22
|
+
const scope = createScope();
|
|
23
|
+
const length = Number.isFinite(options.length) ? Math.min(16, Math.max(1, Math.round(options.length!))) : 6;
|
|
24
|
+
const clean = (text: string) => text.replace(/[^0-9]/g, '').slice(0, length);
|
|
25
|
+
const original = input.value;
|
|
26
|
+
const originalNodes = [...slots.childNodes];
|
|
27
|
+
let value = clean(options.value ?? input.value);
|
|
28
|
+
let pendingReset: Event | null = null;
|
|
29
|
+
const cells = Array.from({ length }, () => {
|
|
30
|
+
const cell = document.createElement('span');
|
|
31
|
+
cell.className = 'mk-input-otp__slot';
|
|
32
|
+
return cell;
|
|
33
|
+
});
|
|
34
|
+
slots.replaceChildren(...cells);
|
|
35
|
+
scope.attr(slots, 'aria-hidden', 'true');
|
|
36
|
+
scope.attr(input, 'type', 'text');
|
|
37
|
+
scope.attr(input, 'inputmode', 'numeric');
|
|
38
|
+
scope.attr(input, 'autocomplete', 'one-time-code');
|
|
39
|
+
scope.attr(input, 'maxlength', String(length));
|
|
40
|
+
scope.attr(input, 'pattern', `[0-9]{${length}}`);
|
|
41
|
+
scope.attr(root, 'data-mk-ready', '');
|
|
42
|
+
const render = () => {
|
|
43
|
+
if (pendingReset) {
|
|
44
|
+
if (pendingReset.eventPhase !== 0) return;
|
|
45
|
+
if (!pendingReset.defaultPrevented) value = clean(input.value);
|
|
46
|
+
pendingReset = null;
|
|
47
|
+
}
|
|
48
|
+
input.value = value;
|
|
49
|
+
const active = document.activeElement === input ? Math.min(length - 1, input.selectionStart ?? value.length) : -1;
|
|
50
|
+
cells.forEach((cell, i) => {
|
|
51
|
+
const text = value[i] ? options.mask ? '•' : value[i]! : '';
|
|
52
|
+
if (cell.textContent !== text) cell.textContent = text;
|
|
53
|
+
cell.toggleAttribute('data-mk-active', i === active);
|
|
54
|
+
cell.toggleAttribute('data-mk-filled', i < value.length);
|
|
55
|
+
});
|
|
56
|
+
scope.attr(root, 'data-mk-complete', value.length === length ? '' : null);
|
|
57
|
+
};
|
|
58
|
+
const setValue = (next: string, notify = true) => {
|
|
59
|
+
if (scope.disposed) return;
|
|
60
|
+
if (pendingReset?.eventPhase === 0) pendingReset = null;
|
|
61
|
+
const previous = value;
|
|
62
|
+
value = clean(String(next));
|
|
63
|
+
render();
|
|
64
|
+
if (notify && value !== previous) {
|
|
65
|
+
options.onChange?.(value);
|
|
66
|
+
root.dispatchEvent(new CustomEvent('mk:change', { bubbles: true, detail: { value, complete: value.length === length } }));
|
|
67
|
+
if (value.length === length) {
|
|
68
|
+
options.onComplete?.(value);
|
|
69
|
+
root.dispatchEvent(new CustomEvent('mk:complete', { bubbles: true, detail: { value } }));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
scope.on(input, 'input', (event) => {
|
|
74
|
+
if ((event as InputEvent).isComposing) return;
|
|
75
|
+
const caret = clean(input.value.slice(0, input.selectionStart ?? input.value.length)).length;
|
|
76
|
+
setValue(input.value);
|
|
77
|
+
input.setSelectionRange(caret, caret);
|
|
78
|
+
render();
|
|
79
|
+
});
|
|
80
|
+
scope.on(input, 'compositionend', () => setValue(input.value));
|
|
81
|
+
scope.on(input, 'paste', (event) => {
|
|
82
|
+
const text = (event as ClipboardEvent).clipboardData?.getData('text');
|
|
83
|
+
if (text === undefined) return;
|
|
84
|
+
event.preventDefault();
|
|
85
|
+
const start = input.selectionStart ?? value.length;
|
|
86
|
+
const end = input.selectionEnd ?? start;
|
|
87
|
+
const inserted = clean(text);
|
|
88
|
+
setValue(value.slice(0, start) + inserted + value.slice(end));
|
|
89
|
+
const caret = Math.min(length, start + inserted.length);
|
|
90
|
+
input.setSelectionRange(caret, caret);
|
|
91
|
+
render();
|
|
92
|
+
});
|
|
93
|
+
scope.on(input, 'focus', render);
|
|
94
|
+
scope.on(input, 'blur', render);
|
|
95
|
+
scope.on(input, 'keyup', render);
|
|
96
|
+
scope.on(input, 'click', render);
|
|
97
|
+
scope.on(document, 'selectionchange', () => { if (document.activeElement === input) render(); });
|
|
98
|
+
if (input.form) scope.on(input.form, 'reset', (event) => {
|
|
99
|
+
render();
|
|
100
|
+
pendingReset = event;
|
|
101
|
+
scope.timeout(() => { if (pendingReset === event) render(); }, 0);
|
|
102
|
+
});
|
|
103
|
+
scope.add(() => { slots.replaceChildren(...originalNodes); input.value = original; });
|
|
104
|
+
render();
|
|
105
|
+
return { setValue, focus: () => { if (!scope.disposed) input.focus(); }, get value() { return value; }, destroy: () => scope.dispose() };
|
|
106
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
.mk-input-otp, mk-input-otp { --mk-otp-size: 2.75rem; --mk-otp-gap: .4rem; display: inline-block; position: relative; max-width: 100%; }
|
|
2
|
+
.mk-input-otp [data-mk-otp-slots], mk-input-otp [data-mk-otp-slots] { display: flex; flex-wrap: wrap; gap: var(--mk-otp-gap); }
|
|
3
|
+
.mk-input-otp__slot { box-sizing: border-box; width: var(--mk-otp-size); height: 3.5rem; display: grid; place-items: center; border: 1px solid var(--mk-outline-soft); border-radius: var(--mk-radius-sm); color: var(--mk-text); background: var(--mk-surface-1); font: 1.5rem var(--mk-font-mono, monospace); transition: border-color .2s, background .2s, transform .2s; }
|
|
4
|
+
.mk-input-otp__slot[data-mk-active] { outline: 2px solid var(--mk-accent); outline-offset: 2px; background: var(--mk-accent-soft); color: var(--mk-on-accent-soft); transform: translateY(-2px); }
|
|
5
|
+
.mk-input-otp [data-mk-otp-input], mk-input-otp [data-mk-otp-input] { position: absolute; inset: 0; box-sizing: border-box; width: 100%; height: 100%; border: 0; background: transparent; color: transparent; caret-color: transparent; outline: 0; font: 1.5rem var(--mk-font-mono, monospace); letter-spacing: calc(var(--mk-otp-size) + var(--mk-otp-gap) - 1ch); padding: 0 0 0 calc((var(--mk-otp-size) - 1ch) / 2); }
|
|
6
|
+
.mk-input-otp:has([aria-invalid=true]) .mk-input-otp__slot, mk-input-otp:has([aria-invalid=true]) .mk-input-otp__slot { border-color: var(--mk-error); }
|
|
7
|
+
.mk-input-otp:has(:disabled), mk-input-otp:has(:disabled) { opacity: .45; }
|
|
8
|
+
@media (max-width: 420px) { .mk-input-otp, mk-input-otp { --mk-otp-size: 2.25rem; --mk-otp-gap: .3rem; } }
|
|
9
|
+
@media (prefers-reduced-motion: reduce) { .mk-input-otp__slot { transition: none; transform: none !important; } }
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useRef, type HTMLAttributes } from 'react';
|
|
3
|
+
import { useIsomorphicLayoutEffect, useLatest } from '../../react/use-recipe';
|
|
4
|
+
import { createInputOtp, type InputOtpOptions } from './engine';
|
|
5
|
+
export interface InputOtpProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange' | 'onComplete' | 'defaultValue'>, InputOtpOptions {
|
|
6
|
+
label?: string; name?: string; disabled?: boolean; required?: boolean; invalid?: boolean; defaultValue?: string;
|
|
7
|
+
}
|
|
8
|
+
export function InputOtp({ length, value, defaultValue, mask, onChange, onComplete, label = 'Código de verificación', name, disabled, required, invalid, className, ...props }: InputOtpProps) {
|
|
9
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
10
|
+
const edited = useRef<string | undefined>(undefined);
|
|
11
|
+
const change = useLatest(onChange);
|
|
12
|
+
const complete = useLatest(onComplete);
|
|
13
|
+
useIsomorphicLayoutEffect(() => {
|
|
14
|
+
const instance = createInputOtp(ref.current, { length, value: value ?? edited.current, mask, onChange: (n) => change.current?.(n), onComplete: (n) => complete.current?.(n) });
|
|
15
|
+
return () => { edited.current = instance.value; instance.destroy(); };
|
|
16
|
+
}, [length, value, mask]);
|
|
17
|
+
return <div {...props} ref={ref} className={['mk-input-otp', className].filter(Boolean).join(' ')}>
|
|
18
|
+
<span data-mk-otp-slots aria-hidden="true" />
|
|
19
|
+
<input data-mk-otp-input type="text" aria-label={label} name={name} defaultValue={value ?? defaultValue ?? ''} disabled={disabled} required={required} aria-invalid={invalid || undefined} />
|
|
20
|
+
</div>;
|
|
21
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Una isla de cristal esmerilado fija arriba y centrada que crece hasta ser un panel: la fila del contenido pasa de `0fr` a `1fr` (0.6 s `cubic-bezier(.625,.05,0,1)`, 0.125 s después), el radio pasa de 3em a 2em, la página de detrás se oscurece a .4 (0.7 s, 0.1 s después) y las filas del panel suben 3em desde 0.15 s, 25 ms una tras otra. Varios botones comparten la isla: el mismo cierra, otro cambia directamente a su panel. Escape, el fondo, un enlace del panel y hacer scroll después de una rueda o un toque la cierran. Al cargar cae desde −300 % en 1.8 s. En las tarjetas, pasar sobre una baja las demás al 20 % y su imagen crece a 1.04.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Superficie de cristal con blur de 20 px, ancho adaptable y botones de 2.5rem. La entrada usa `power1.out`; el icono de nueve puntos es estático.
|
|
6
6
|
|
|
7
7
|
## HTML / Astro
|
|
8
8
|
|
|
@@ -72,13 +72,13 @@ Con la clase `mk-island` sirve cualquier etiqueta (`<nav class="mk-island">` + `
|
|
|
72
72
|
|
|
73
73
|
## Opciones
|
|
74
74
|
|
|
75
|
-
| Opción
|
|
75
|
+
| Opción | Atributo | Por defecto | Qué hace |
|
|
76
76
|
| ----------------- | -------------------------- | ----------- | ---------------------------------------------------------- |
|
|
77
|
-
| `entrance`
|
|
78
|
-
| `closeOnScroll`
|
|
79
|
-
| `closeOnNavigate` | `close-on-navigate`
|
|
80
|
-
| `focusOnOpen`
|
|
81
|
-
| `onChange`
|
|
77
|
+
| `entrance` | `entrance="false\|manual"` | `true` | Caer desde −300 % al cargar; `'manual'` espera a `ready()` |
|
|
78
|
+
| `closeOnScroll` | `close-on-scroll` | `true` | Cerrar al moverse la página tras una rueda o un toque |
|
|
79
|
+
| `closeOnNavigate` | `close-on-navigate` | `true` | Cerrar al seguir un enlace del panel |
|
|
80
|
+
| `focusOnOpen` | `focus-on-open` | `true` | Enfocar el primer control del panel (150 ms) |
|
|
81
|
+
| `onChange` | evento `mk:island` | — | Panel abierto o `null` |
|
|
82
82
|
|
|
83
83
|
API: `createIsland(root, options)` → `{ open(name), close(), toggle(name), ready(), active, destroy }`.
|
|
84
84
|
|
|
@@ -5,7 +5,7 @@ import { TIMING } from '../../core/timing';
|
|
|
5
5
|
import { inert, warnOnce, type Instance } from '../../core/types';
|
|
6
6
|
|
|
7
7
|
/*
|
|
8
|
-
*
|
|
8
|
+
* floating navbar: a frosted glass pill centred at the top that grows
|
|
9
9
|
* into a rounded panel (CSS grid rows 0fr → 1fr, 0.6 s cubic-bezier(.625,.05,0,1),
|
|
10
10
|
* 0.125 s late; radius 3em → 2em) while the page behind darkens to .4 and the
|
|
11
11
|
* panel's rows rise in 0.15 s + 0.025 s apart. Several triggers share the shell:
|
|
@@ -21,7 +21,7 @@ import { inert, warnOnce, type Instance } from '../../core/types';
|
|
|
21
21
|
export interface IslandOptions {
|
|
22
22
|
/** Drop in from above on load (true), wait for ready() ('manual') or not at all. */
|
|
23
23
|
entrance?: boolean | 'manual';
|
|
24
|
-
/** Close when the page scrolls after a wheel or touch
|
|
24
|
+
/** Close when the page scrolls after a wheel or touch. */
|
|
25
25
|
closeOnScroll?: boolean;
|
|
26
26
|
/** Close when a link inside a panel is followed. */
|
|
27
27
|
closeOnNavigate?: boolean;
|
|
@@ -169,7 +169,7 @@ export function createIsland(root: HTMLElement | null, options: IslandOptions =
|
|
|
169
169
|
}
|
|
170
170
|
scope.add(() => clearTimeout(focusTimer));
|
|
171
171
|
|
|
172
|
-
//
|
|
172
|
+
// The entrance uses GSAP's default ease (power1.out).
|
|
173
173
|
const moving = entrance !== false && entrances();
|
|
174
174
|
if (moving) gsap.set(shell, { yPercent: t.offset });
|
|
175
175
|
const ready = () => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
*
|
|
2
|
+
* floating navbar: frosted glass pill at the top that grows into a
|
|
3
3
|
* panel. Fixed over the page by default; .mk-island--contained keeps it (and its
|
|
4
4
|
* backdrop) inside a positioned parent, for previews.
|
|
5
5
|
*/
|
|
@@ -10,7 +10,7 @@ export interface IslandPanel {
|
|
|
10
10
|
label: string;
|
|
11
11
|
/** Button content; the menu's nine dots by default. */
|
|
12
12
|
icon?: ReactNode;
|
|
13
|
-
/** Light button
|
|
13
|
+
/** Light button. */
|
|
14
14
|
light?: boolean;
|
|
15
15
|
content: ReactNode;
|
|
16
16
|
}
|
|
@@ -27,7 +27,7 @@ export interface IslandProps extends Omit<HTMLAttributes<HTMLElement>, 'onChange
|
|
|
27
27
|
instanceRef?: { current: IslandInstance | null };
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
/** The nine static dots of
|
|
30
|
+
/** The nine static dots of the island menu button. */
|
|
31
31
|
export function IslandDots() {
|
|
32
32
|
return (
|
|
33
33
|
<span className="mk-island__dots" aria-hidden="true">
|
|
@@ -38,7 +38,7 @@ export function IslandDots() {
|
|
|
38
38
|
);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
/**
|
|
41
|
+
/** floating navbar. */
|
|
42
42
|
export function Island({
|
|
43
43
|
brand,
|
|
44
44
|
cta,
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
# menu
|
|
2
2
|
|
|
3
|
-
El menú "Opciones"
|
|
4
|
-
|
|
5
|
-
Origen: Stepbro NavMenu (abre con ModalHost `absolute`, sin backdrop, superficie translúcida; `nav-menu-enter` en el kit). Su código usa el mismo plegado de 0.5 s que los diálogos; el screencast en vivo de Stepbro midió menos (abre 175 → 364 ms, cierra 66 → 182 ms) que `pnpm timings` aquí (≈ 585 / 510 ms); lo más probable es que su comparación de frames no registrara la cascada pequeña de las opciones ni la cola del plegado.
|
|
3
|
+
El menú "Opciones": un panel translúcido que crece desde su botón (el mismo motor del diálogo, anclado y sin velo) y cuyas opciones entran escalonadas desde 90 % con blur 4 px, de la última a la primera, 0.35 s, 40 ms entre opciones, tras 0.1 s. Un clic fuera o Escape lo cierra; flechas, Inicio y Fin mueven el foco.
|
|
6
4
|
|
|
7
5
|
## HTML / Astro
|
|
8
6
|
|
|
@@ -63,5 +61,5 @@ Sobre tu propio `<dialog class="mk-dialog mk-menu">`: `createMenu(dialog, opcion
|
|
|
63
61
|
|
|
64
62
|
## Presentación boleto (v0.8)
|
|
65
63
|
|
|
66
|
-
`createMenu(dialog, { ticket: true })`, `createTicketMenu(dialog)`, `<mk-menu ticket>` o `<Menu.Root ticket>` aplican la presentación
|
|
64
|
+
`createMenu(dialog, { ticket: true })`, `createTicketMenu(dialog)`, `<mk-menu ticket>` o `<Menu.Root ticket>` aplican la presentación. El motor conserva foco, teclado, Escape y selección. Apertura recortada .9 s; contenido desde 150%, 1.1 s con stagger .02. La receta `ticket-menu` tiene su propia página en el playground.
|
|
67
65
|
|
|
@@ -7,7 +7,7 @@ import { flowInMenu } from '../../motion/entrances';
|
|
|
7
7
|
import { createDialog, type DialogInstance, type DialogOptions } from '../dialog/engine';
|
|
8
8
|
|
|
9
9
|
/*
|
|
10
|
-
*
|
|
10
|
+
* Options menu: the same surface engine as a dialog, anchored to its
|
|
11
11
|
* button, without a tinted backdrop, on a translucent surface that grows out of
|
|
12
12
|
* the button; its options flow in from 90 % out of a 4 px blur, last first,
|
|
13
13
|
* 40 ms apart, after 0.1 s. Arrow keys, Home and End move between options.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* menu —
|
|
1
|
+
/* menu — translucent options panel that grows out of its button (an anchored .mk-dialog). */
|
|
2
2
|
.mk-menu {
|
|
3
3
|
--mk-menu-bg: color-mix(in srgb, var(--mk-surface-3) 78%, transparent);
|
|
4
4
|
--mk-menu-radius: 28px;
|
|
@@ -78,5 +78,5 @@ function Separator() {
|
|
|
78
78
|
return <hr data-mk-separator="" />;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
/**
|
|
81
|
+
/** options menu: <Menu.Root onSelect><Menu.Trigger/><Menu.Content><Menu.Item value/>…</Menu.Content></Menu.Root>. */
|
|
82
82
|
export const Menu = { Root, Trigger, Content, Item, Separator };
|