@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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* steps —
|
|
1
|
+
/* steps — onboarding: one step at a time, centred in a column up to 600 px wide. */
|
|
2
2
|
.mk-steps,
|
|
3
3
|
mk-steps {
|
|
4
4
|
position: relative;
|
|
@@ -11,7 +11,7 @@ mk-steps {
|
|
|
11
11
|
font-family: var(--mk-font);
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
/*
|
|
14
|
+
/* onboarding background runs at 20 %. */
|
|
15
15
|
:is(.mk-steps, mk-steps) > :is(.mk-chat-background, mk-chat-background) {
|
|
16
16
|
--mk-chat-background-opacity: 0.2;
|
|
17
17
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# studio-dock
|
|
2
|
+
|
|
3
|
+
Navegación de estudio que pasa de una píldora a un panel de tarjetas. Inspirada en el comportamiento público de Mubien; código y gráficos propios, sin sus fuentes, marcas o videos.
|
|
4
|
+
|
|
5
|
+
```html
|
|
6
|
+
<div class="mk-studio-dock" data-mk="studio-dock">
|
|
7
|
+
<div class="mk-studio-dock__bar">
|
|
8
|
+
<a href="/" class="mk-studio-dock__brand">ST</a>
|
|
9
|
+
<span class="mk-studio-dock__label">Estudio</span>
|
|
10
|
+
<button type="button" class="mk-studio-dock__toggle" data-mk-studio-toggle aria-label="Abrir navegación"><span aria-hidden="true">+</span></button>
|
|
11
|
+
</div>
|
|
12
|
+
<section class="mk-studio-dock__panel" data-mk-studio-panel hidden>
|
|
13
|
+
<div data-mk-studio-view="menu">
|
|
14
|
+
<div class="mk-studio-dock__grid">
|
|
15
|
+
<a href="/work" class="mk-studio-dock__tile">Proyectos</a>
|
|
16
|
+
<a href="/about" class="mk-studio-dock__tile">Estudio</a>
|
|
17
|
+
</div>
|
|
18
|
+
<button type="button" data-mk-studio-slide>Contacto</button>
|
|
19
|
+
</div>
|
|
20
|
+
<div data-mk-studio-view="contact" hidden>
|
|
21
|
+
<h2>Conversemos</h2><a href="mailto:hello@example.com">hello@example.com</a>
|
|
22
|
+
<button type="button" data-mk-studio-to="menu">Volver</button>
|
|
23
|
+
</div>
|
|
24
|
+
</section>
|
|
25
|
+
</div>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { createStudioDock } from '@yewdiel21/motion-kit';
|
|
30
|
+
import { useStudioDock, StudioDock } from '@yewdiel21/motion-kit/react';
|
|
31
|
+
import '@yewdiel21/motion-kit/css';
|
|
32
|
+
const dock = createStudioDock(root, { duration: 1.3, ease: 'back.out(0.52)' });
|
|
33
|
+
dock.open('contact'); dock.selectPage(1); dock.close(); dock.destroy();
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`StudioDock` es un shell React con los mismos slots y `instanceRef`; `useStudioDock(ref, options, deps)` permite usar tu markup. Web component: `defineStudioDock()` registra `<mk-studio-dock>`. El evento `mk:studio-dock` y `onChange` entregan `{ open, view, page }`.
|
|
37
|
+
|
|
38
|
+
Slots opcionales: `[data-mk-studio-to="menu|contact|reel"]`, `[data-mk-studio-close]`, `[data-mk-studio-pages]`, `[data-mk-studio-page]`, botones `[data-mk-studio-page-to="0"]` y `<time data-mk-studio-timezone="America/Mazatlan">`. Las páginas se ocultan con `hidden`; el paginado tiene flechas izquierda/derecha y botones numerados. Los relojes usan `Intl` y actualizan cada minuto; no consultan ningún servidor.
|
|
39
|
+
|
|
40
|
+
El botón de contacto funciona con clic, Enter y Espacio. Dentro de `.mk-studio-dock__slide-track` también acepta arrastre y abre al superar 70 % del recorrido. Las vistas reciben foco, Escape cierra, clic fuera cierra sin robar foco al destino y el cierre explícito devuelve el foco anterior. `trapFocus` y `focusOnOpen` son `true` por defecto y se pueden desactivar. El panel es un diálogo contenido, sin bloqueo del body ni cambios en otros componentes. `aria-expanded` refleja el estado y el panel cerrado queda fuera del árbol de accesibilidad.
|
|
41
|
+
|
|
42
|
+
El panel se revela/colapsa por `clip-path` y desplazamiento de 16 px; duración configurable de 1.3 s, rebote `.52`. Con movimiento reducido se salta el recorrido. `destroy()` es idempotente: revierte estilos/atributos, listeners, temporizadores y animaciones. Los videos aportados por el consumidor se pausan al salir de su vista. No se envía ningún formulario desde este motor.
|
|
43
|
+
|
|
44
|
+
Por defecto permanece dentro del layout. Usa `.mk-studio-dock--fixed` o `<StudioDock fixed>` para fijarlo abajo de la ventana. Al abrirse, el panel mide el espacio que queda en la ventana: su alto se limita a ese hueco (con scroll interno) y, si arriba no cabe y abajo hay más sitio, se abre bajo la barra (`data-mk-studio-below`). `--mk-studio-dock-offset` (76 px) es la distancia del panel al borde del dock. Variables de color: `--mk-surface`, `--mk-surface-2`, `--mk-accent`, `--mk-on-accent`, `--mk-outline-soft`.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { MkElement, defineElement, type OptionType } from '../../elements/base';
|
|
2
|
+
import { createStudioDock, type StudioDockInstance, type StudioDockView } from './engine';
|
|
3
|
+
export class MkStudioDockElement extends MkElement<StudioDockInstance> {
|
|
4
|
+
static options: Record<string, OptionType> = { duration: 'number', ease: 'string', focusOnOpen: 'boolean', trapFocus: 'boolean' };
|
|
5
|
+
protected create(options: Record<string, unknown>) { return createStudioDock(this, options); }
|
|
6
|
+
open(view?: StudioDockView) { this.instance?.open(view); }
|
|
7
|
+
close() { this.instance?.close(); }
|
|
8
|
+
toggle() { this.instance?.toggle(); }
|
|
9
|
+
selectPage(index: number) { this.instance?.selectPage(index); }
|
|
10
|
+
}
|
|
11
|
+
export const defineStudioDock = () => defineElement('mk-studio-dock', MkStudioDockElement);
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { gsap, registerMotion } from '../../core/gsap';
|
|
2
|
+
import { reduced } from '../../core/reduced';
|
|
3
|
+
import { createScope } from '../../core/scope';
|
|
4
|
+
import { inert, type Instance } from '../../core/types';
|
|
5
|
+
|
|
6
|
+
export type StudioDockView = 'menu' | 'contact' | 'reel';
|
|
7
|
+
export interface StudioDockState { open: boolean; view: StudioDockView; page: number; }
|
|
8
|
+
export interface StudioDockOptions {
|
|
9
|
+
/** Seconds. Reference sheet: 1.3 s, outBack(.52). */
|
|
10
|
+
duration?: number;
|
|
11
|
+
ease?: string;
|
|
12
|
+
/** Move focus into a newly opened view. Defaults to true. */
|
|
13
|
+
focusOnOpen?: boolean;
|
|
14
|
+
/** Contain Tab within the open panel and its toggle. Defaults to true. */
|
|
15
|
+
trapFocus?: boolean;
|
|
16
|
+
onChange?: (state: StudioDockState) => void;
|
|
17
|
+
}
|
|
18
|
+
export interface StudioDockInstance extends Instance {
|
|
19
|
+
open(view?: StudioDockView): void;
|
|
20
|
+
close(): void;
|
|
21
|
+
toggle(): void;
|
|
22
|
+
selectPage(index: number): void;
|
|
23
|
+
readonly state: StudioDockState;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let nextId = 0;
|
|
27
|
+
/** Panel distance from the dock's edge (bar + gap); --mk-studio-dock-offset overrides it. */
|
|
28
|
+
const PANEL_OFFSET = 76;
|
|
29
|
+
const VIEWPORT_GAP = 12;
|
|
30
|
+
const MIN_SPACE = 320;
|
|
31
|
+
const FOCUSABLE = 'a[href],button:not(:disabled),input:not(:disabled),select:not(:disabled),textarea:not(:disabled),video[controls],[tabindex]:not([tabindex="-1"])';
|
|
32
|
+
|
|
33
|
+
/** A contained studio sheet. Markup remains useful without JavaScript; fixed positioning is CSS opt-in. */
|
|
34
|
+
export function createStudioDock(root: HTMLElement | null, options: StudioDockOptions = {}): StudioDockInstance {
|
|
35
|
+
const toggleButton = root?.querySelector<HTMLButtonElement>('[data-mk-studio-toggle]');
|
|
36
|
+
const panel = root?.querySelector<HTMLElement>('[data-mk-studio-panel]');
|
|
37
|
+
if (!root || !toggleButton || !panel) return inert({ open() {}, close() {}, toggle() {}, selectPage() {}, state: { open: false, view: 'menu' as const, page: 0 } });
|
|
38
|
+
registerMotion();
|
|
39
|
+
const scope = createScope();
|
|
40
|
+
const context = gsap.context(() => {}, root);
|
|
41
|
+
scope.add(() => context.revert());
|
|
42
|
+
const views = [...panel.querySelectorAll<HTMLElement>('[data-mk-studio-view]')];
|
|
43
|
+
const pages = [...panel.querySelectorAll<HTMLElement>('[data-mk-studio-page]')];
|
|
44
|
+
const pageButtons = [...root.querySelectorAll<HTMLElement>('[data-mk-studio-page-to]')];
|
|
45
|
+
const slide = root.querySelector<HTMLElement>('[data-mk-studio-slide]');
|
|
46
|
+
const tracked = [root, panel, ...(slide ? [slide] : [])];
|
|
47
|
+
tracked.forEach(el => { const style = el.getAttribute('style'); scope.add(() => style === null ? el.removeAttribute('style') : el.setAttribute('style', style)); });
|
|
48
|
+
scope.attr(panel, 'id', panel.id || `mk-studio-panel-${++nextId}`);
|
|
49
|
+
scope.attr(panel, 'role', panel.getAttribute('role') || 'dialog');
|
|
50
|
+
scope.attr(panel, 'aria-label', panel.getAttribute('aria-label') || 'Studio navigation');
|
|
51
|
+
scope.attr(panel, 'tabindex', '-1');
|
|
52
|
+
scope.attr(toggleButton, 'aria-controls', panel.id);
|
|
53
|
+
scope.attr(toggleButton, 'aria-expanded', 'false');
|
|
54
|
+
scope.attr(panel, 'hidden', '');
|
|
55
|
+
scope.attr(panel, 'aria-hidden', 'true');
|
|
56
|
+
scope.attr(root, 'data-mk-ready', '');
|
|
57
|
+
let state: StudioDockState = { open: false, view: 'menu', page: 0 };
|
|
58
|
+
let previousFocus: HTMLElement | null = null;
|
|
59
|
+
let animation: gsap.core.Tween | undefined;
|
|
60
|
+
let slideStart: number | null = null;
|
|
61
|
+
let slideDistance = 0;
|
|
62
|
+
let slideMoved = false;
|
|
63
|
+
let suppressSlideClick = false;
|
|
64
|
+
|
|
65
|
+
// The sheet opens above the bar. Cap it to the room actually left in the viewport
|
|
66
|
+
// (it scrolls inside) and open it below the bar when above is too tight.
|
|
67
|
+
const place = (decide = false) => {
|
|
68
|
+
const view = root.ownerDocument.defaultView;
|
|
69
|
+
if (!state.open || !view) return;
|
|
70
|
+
const rect = root.getBoundingClientRect();
|
|
71
|
+
const offset = parseFloat(view.getComputedStyle(root).getPropertyValue('--mk-studio-dock-offset')) || PANEL_OFFSET;
|
|
72
|
+
const above = rect.bottom - offset - VIEWPORT_GAP;
|
|
73
|
+
const below = view.innerHeight - rect.top - offset - VIEWPORT_GAP;
|
|
74
|
+
// The direction is chosen on open only, so scrolling never flips an open sheet.
|
|
75
|
+
if (decide) scope.attr(root, 'data-mk-studio-below', above < Math.min(panel.scrollHeight || MIN_SPACE, MIN_SPACE) && below > above ? '' : null);
|
|
76
|
+
const flip = root.hasAttribute('data-mk-studio-below');
|
|
77
|
+
root.style.setProperty('--mk-studio-dock-space', `${Math.max(MIN_SPACE / 2, Math.floor(flip ? below : above))}px`);
|
|
78
|
+
};
|
|
79
|
+
const hidden = () => root.hasAttribute('data-mk-studio-below')
|
|
80
|
+
? { clipPath: 'inset(0% 0% 100% 0% round 18px)', y: -16, opacity: 0 }
|
|
81
|
+
: { clipPath: 'inset(100% 0% 0% 0% round 18px)', y: 16, opacity: 0 };
|
|
82
|
+
const notify = () => {
|
|
83
|
+
const value = { ...state };
|
|
84
|
+
options.onChange?.(value);
|
|
85
|
+
root.dispatchEvent(new CustomEvent('mk:studio-dock', { bubbles: true, detail: value }));
|
|
86
|
+
};
|
|
87
|
+
const isTabbable = (el: HTMLElement) => el.tabIndex >= 0 && !el.closest('[hidden], [inert]') && el.getAttribute('aria-disabled') !== 'true';
|
|
88
|
+
const visibleFocus = () => [...panel.querySelectorAll<HTMLElement>(FOCUSABLE)].filter(isTabbable);
|
|
89
|
+
const focusView = () => { if (options.focusOnOpen !== false) (visibleFocus()[0] ?? panel).focus({ preventScroll: true }); };
|
|
90
|
+
const setView = (view: StudioDockView) => {
|
|
91
|
+
state.view = views.length && !views.some(el => el.dataset.mkStudioView === view) ? 'menu' : view;
|
|
92
|
+
views.forEach(el => scope.attr(el, 'hidden', el.dataset.mkStudioView === state.view ? null : ''));
|
|
93
|
+
scope.attr(root, 'data-mk-studio-active', state.view);
|
|
94
|
+
panel.querySelectorAll<HTMLVideoElement>('video').forEach(video => { if (state.view !== 'reel') video.pause(); });
|
|
95
|
+
};
|
|
96
|
+
const selectPage = (index: number, announce = true) => {
|
|
97
|
+
if (scope.disposed || !pages.length || !Number.isFinite(index)) return;
|
|
98
|
+
state.page = ((Math.round(index) % pages.length) + pages.length) % pages.length;
|
|
99
|
+
pages.forEach((el, i) => scope.attr(el, 'hidden', i === state.page ? null : ''));
|
|
100
|
+
pageButtons.forEach(el => scope.attr(el, 'aria-current', Number(el.dataset.mkStudioPageTo) === state.page ? 'true' : null));
|
|
101
|
+
if (announce) notify();
|
|
102
|
+
};
|
|
103
|
+
const open = (view: StudioDockView = 'menu') => {
|
|
104
|
+
if (scope.disposed) return;
|
|
105
|
+
const wasOpen = state.open;
|
|
106
|
+
if (wasOpen && state.view === view) return;
|
|
107
|
+
if (!wasOpen) {
|
|
108
|
+
const active = root.ownerDocument.activeElement;
|
|
109
|
+
previousFocus = active instanceof HTMLElement && active !== root.ownerDocument.body && !panel.contains(active) ? active : toggleButton;
|
|
110
|
+
}
|
|
111
|
+
animation?.kill();
|
|
112
|
+
state.open = true;
|
|
113
|
+
scope.attr(panel, 'hidden', null);
|
|
114
|
+
scope.attr(panel, 'inert', null);
|
|
115
|
+
scope.attr(panel, 'aria-hidden', 'false');
|
|
116
|
+
scope.attr(root, 'data-mk-studio-open', '');
|
|
117
|
+
scope.attr(toggleButton, 'aria-expanded', 'true');
|
|
118
|
+
setView(view);
|
|
119
|
+
place(!wasOpen);
|
|
120
|
+
if (!reduced()) context.add(() => {
|
|
121
|
+
animation = gsap.fromTo(panel, hidden(), { clipPath: 'inset(0% 0% 0% 0% round 18px)', y: 0, opacity: 1, duration: Math.max(0, options.duration ?? 1.3), ease: options.ease ?? 'back.out(0.52)', overwrite: true });
|
|
122
|
+
});
|
|
123
|
+
else context.add(() => { gsap.set(panel, { clipPath: 'inset(0% round 18px)', y: 0, opacity: 1 }); });
|
|
124
|
+
focusView();
|
|
125
|
+
notify();
|
|
126
|
+
};
|
|
127
|
+
const close = (restoreFocus = true) => {
|
|
128
|
+
if (scope.disposed || !state.open) return;
|
|
129
|
+
animation?.kill();
|
|
130
|
+
state.open = false;
|
|
131
|
+
scope.attr(toggleButton, 'aria-expanded', 'false');
|
|
132
|
+
scope.attr(root, 'data-mk-studio-open', null);
|
|
133
|
+
// Stop focus immediately while the visual sheet collapses.
|
|
134
|
+
scope.attr(panel, 'inert', '');
|
|
135
|
+
scope.attr(panel, 'aria-hidden', 'true');
|
|
136
|
+
if (reduced()) scope.attr(panel, 'hidden', '');
|
|
137
|
+
else context.add(() => {
|
|
138
|
+
animation = gsap.to(panel, { ...hidden(), duration: Math.max(0, options.duration ?? 1.3), ease: options.ease ?? 'back.out(0.52)', overwrite: true, onComplete: () => { if (!scope.disposed && !state.open) scope.attr(panel, 'hidden', ''); } });
|
|
139
|
+
});
|
|
140
|
+
panel.querySelectorAll<HTMLVideoElement>('video').forEach(video => video.pause());
|
|
141
|
+
if (restoreFocus) (previousFocus?.isConnected ? previousFocus : toggleButton).focus({ preventScroll: true });
|
|
142
|
+
notify();
|
|
143
|
+
};
|
|
144
|
+
scope.on(root, 'click', event => {
|
|
145
|
+
const target = event.target instanceof Element ? event.target : null;
|
|
146
|
+
if (!target) return;
|
|
147
|
+
if (target.closest('[data-mk-studio-toggle]') === toggleButton) state.open ? close() : open();
|
|
148
|
+
else if (target.closest('[data-mk-studio-close]')) close();
|
|
149
|
+
else if (target.closest('[data-mk-studio-slide]')) {
|
|
150
|
+
if (suppressSlideClick) { event.preventDefault(); suppressSlideClick = false; }
|
|
151
|
+
else open('contact');
|
|
152
|
+
} else {
|
|
153
|
+
const view = target.closest<HTMLElement>('[data-mk-studio-to]')?.dataset.mkStudioTo;
|
|
154
|
+
if (view === 'menu' || view === 'contact' || view === 'reel') open(view);
|
|
155
|
+
const page = target.closest<HTMLElement>('[data-mk-studio-page-to]');
|
|
156
|
+
if (page) selectPage(Number(page.dataset.mkStudioPageTo));
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
if (root.ownerDocument.defaultView) scope.on(root.ownerDocument.defaultView, 'resize', () => place());
|
|
160
|
+
scope.on(root.ownerDocument, 'scroll', () => place(), { passive: true, capture: true });
|
|
161
|
+
scope.on(root.ownerDocument, 'pointerdown', event => {
|
|
162
|
+
if (state.open && event.target instanceof Node && !root.contains(event.target)) close(false);
|
|
163
|
+
});
|
|
164
|
+
scope.on(root.ownerDocument, 'keydown', event => {
|
|
165
|
+
if (!state.open || event.defaultPrevented) return;
|
|
166
|
+
if (event.key === 'Escape') { event.preventDefault(); close(); return; }
|
|
167
|
+
if (!(event.target instanceof Node) || !root.contains(event.target)) return;
|
|
168
|
+
if ((event.key === 'ArrowLeft' || event.key === 'ArrowRight') && (event.target as Element).closest('[data-mk-studio-pages]')) {
|
|
169
|
+
event.preventDefault(); selectPage(state.page + (event.key === 'ArrowRight' ? 1 : -1));
|
|
170
|
+
pageButtons.find(el => Number(el.dataset.mkStudioPageTo) === state.page)?.focus();
|
|
171
|
+
}
|
|
172
|
+
if (event.key === 'Tab' && options.trapFocus !== false) {
|
|
173
|
+
// querySelectorAll keeps the authored DOM order, including a toggle before OR after the panel.
|
|
174
|
+
const elements = [...root.querySelectorAll<HTMLElement>(FOCUSABLE)].filter(el => (el === toggleButton || panel.contains(el)) && isTabbable(el));
|
|
175
|
+
if (!elements.length) { event.preventDefault(); panel.focus(); return; }
|
|
176
|
+
const first = elements[0]!, last = elements.at(-1)!;
|
|
177
|
+
if (event.shiftKey && (root.ownerDocument.activeElement === first || root.ownerDocument.activeElement === panel)) { event.preventDefault(); last.focus(); }
|
|
178
|
+
else if (!event.shiftKey && root.ownerDocument.activeElement === last) { event.preventDefault(); first.focus(); }
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
if (slide) {
|
|
182
|
+
scope.on(slide, 'pointerdown', event => { if (event.button !== 0) return; slideStart = event.clientX; slideDistance = 0; slideMoved = false; });
|
|
183
|
+
scope.on(root.ownerDocument, 'pointermove', event => {
|
|
184
|
+
if (slideStart === null) return;
|
|
185
|
+
const width = Math.max(0, (slide.parentElement?.clientWidth ?? 0) - slide.offsetWidth);
|
|
186
|
+
slideMoved ||= Math.abs(event.clientX - slideStart) > 5;
|
|
187
|
+
slideDistance = Math.max(0, Math.min(width, event.clientX - slideStart));
|
|
188
|
+
if (!reduced()) slide.style.setProperty('--mk-studio-drag', `${slideDistance}px`);
|
|
189
|
+
});
|
|
190
|
+
const release = (cancel = false) => {
|
|
191
|
+
if (slideStart === null) return;
|
|
192
|
+
const width = Math.max(0, (slide.parentElement?.clientWidth ?? 0) - slide.offsetWidth);
|
|
193
|
+
suppressSlideClick = slideMoved;
|
|
194
|
+
slideStart = null;
|
|
195
|
+
slide.style.removeProperty('--mk-studio-drag');
|
|
196
|
+
if (!cancel && width > 0 && slideDistance / width >= .7) open('contact');
|
|
197
|
+
scope.timeout(() => { suppressSlideClick = false; }, 150);
|
|
198
|
+
};
|
|
199
|
+
scope.on(root.ownerDocument, 'pointerup', () => release());
|
|
200
|
+
scope.on(root.ownerDocument, 'pointercancel', () => release(true));
|
|
201
|
+
}
|
|
202
|
+
const clocks = [...root.querySelectorAll<HTMLElement>('[data-mk-studio-timezone]')];
|
|
203
|
+
const clockOriginals = clocks.map(el => ({ el, text: el.textContent, style: el.getAttribute('style') }));
|
|
204
|
+
const tick = () => clocks.forEach(el => {
|
|
205
|
+
try { el.textContent = new Intl.DateTimeFormat(undefined, { hour: '2-digit', minute: '2-digit', timeZone: el.dataset.mkStudioTimezone }).format(new Date()); } catch { /* Invalid caller time zone leaves the original label intact. */ }
|
|
206
|
+
});
|
|
207
|
+
if (clocks.length) { tick(); const timer = setInterval(tick, 60_000); scope.add(() => clearInterval(timer)); }
|
|
208
|
+
scope.add(() => clockOriginals.forEach(({ el, text, style }) => { el.textContent = text; if (style === null) el.removeAttribute('style'); else el.setAttribute('style', style); }));
|
|
209
|
+
setView('menu');
|
|
210
|
+
selectPage(0, false);
|
|
211
|
+
return { open, close, toggle: () => state.open ? close() : open(), selectPage, get state() { return { ...state }; }, destroy() { if (scope.disposed) return; const focused = root.contains(root.ownerDocument.activeElement); animation?.kill(); panel.querySelectorAll<HTMLVideoElement>('video').forEach(video => video.pause()); scope.dispose(); if (focused) toggleButton.focus({ preventScroll: true }); } };
|
|
212
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useRef, type HTMLAttributes } from 'react';
|
|
3
|
+
import { useIsomorphicLayoutEffect, useLatest } from '../../react/use-recipe';
|
|
4
|
+
import { createStudioDock, type StudioDockInstance, type StudioDockOptions } from './engine';
|
|
5
|
+
|
|
6
|
+
export interface StudioDockProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange'>, StudioDockOptions {
|
|
7
|
+
/** Default stays inside its positioned demo/app panel. */
|
|
8
|
+
fixed?: boolean;
|
|
9
|
+
instanceRef?: { current: StudioDockInstance | null };
|
|
10
|
+
}
|
|
11
|
+
/** Composable light-DOM shell; use the data-mk-studio-* slots shown in the README. */
|
|
12
|
+
export function StudioDock({ duration, ease, focusOnOpen, trapFocus, onChange, fixed = false, instanceRef, className, children, ...props }: StudioDockProps) {
|
|
13
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
14
|
+
const change = useLatest(onChange);
|
|
15
|
+
useIsomorphicLayoutEffect(() => {
|
|
16
|
+
const instance = createStudioDock(ref.current, { duration, ease, focusOnOpen, trapFocus, onChange: state => change.current?.(state) });
|
|
17
|
+
if (instanceRef) instanceRef.current = instance;
|
|
18
|
+
return () => { instance.destroy(); if (instanceRef) instanceRef.current = null; };
|
|
19
|
+
}, [duration, ease, focusOnOpen, trapFocus]);
|
|
20
|
+
return <div {...props} ref={ref} className={['mk-studio-dock', fixed && 'mk-studio-dock--fixed', className].filter(Boolean).join(' ')}>{children}</div>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export { useStudioDock } from '../../react/use-studio-dock';
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
.mk-studio-dock { position: relative; isolation: isolate; width: min(343px, 100%); color: var(--mk-text); font: inherit; }
|
|
2
|
+
.mk-studio-dock--fixed { position: fixed; z-index: 70; inset-block-end: 24px; inset-inline-start: 50%; translate: -50% 0; width: min(343px, calc(100vw - 32px)); }
|
|
3
|
+
.mk-studio-dock__bar { display: flex; align-items: center; gap: 16px; min-height: 64px; padding: 12px; border-radius: 18px; background: var(--mk-surface-2); }
|
|
4
|
+
.mk-studio-dock__brand, .mk-studio-dock__toggle { display: grid; place-items: center; inline-size: 40px; block-size: 40px; flex: none; border: 0; border-radius: 10px; background: var(--mk-surface); color: inherit; text-decoration: none; font: inherit; }
|
|
5
|
+
.mk-studio-dock__label { flex: 1; font-size: .9rem; }
|
|
6
|
+
.mk-studio-dock__toggle { cursor: pointer; }
|
|
7
|
+
.mk-studio-dock__toggle span { transition: transform .4s; }
|
|
8
|
+
.mk-studio-dock[data-mk-studio-open] .mk-studio-dock__toggle span { transform: rotate(45deg); }
|
|
9
|
+
.mk-studio-dock__panel { position: absolute; z-index: 2; inset-inline: 0; inset-block-end: var(--mk-studio-dock-offset, 76px); max-height: min(660px, 75dvh, var(--mk-studio-dock-space, 660px)); overflow: auto; overscroll-behavior: contain; padding: 12px; border: 1px solid var(--mk-outline-soft); border-radius: 18px; background: var(--mk-surface-2); box-shadow: 0 16px 60px #0002; }
|
|
10
|
+
/* Not enough room above the bar: the engine opens the sheet below it. */
|
|
11
|
+
.mk-studio-dock[data-mk-studio-below] .mk-studio-dock__panel { inset-block: var(--mk-studio-dock-offset, 76px) auto; }
|
|
12
|
+
.mk-studio-dock [hidden] { display: none !important; }
|
|
13
|
+
.mk-studio-dock__grid, .mk-studio-dock__page { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
|
|
14
|
+
.mk-studio-dock__tile { min-height: 130px; display: flex; flex-direction: column; justify-content: flex-end; gap: 4px; padding: 12px; border-radius: 10px; background: var(--mk-surface); color: inherit; text-decoration: none; transition: transform .4s cubic-bezier(.2, 1.4, .4, 1); }
|
|
15
|
+
.mk-studio-dock__tile small { color: var(--mk-muted); }
|
|
16
|
+
.mk-studio-dock__tile:is(:hover, :focus-visible) { transform: scale(.94); }
|
|
17
|
+
.mk-studio-dock__reel { display: flex; justify-content: space-between; align-items: end; min-height: 154px; margin-block: 12px; padding: 16px; border-radius: 10px; background: var(--mk-accent); color: var(--mk-on-accent); }
|
|
18
|
+
.mk-studio-dock__reel button { border: 1px solid currentColor; border-radius: 50%; padding: 14px; background: transparent; color: inherit; font: inherit; cursor: pointer; }
|
|
19
|
+
.mk-studio-dock__slide-track { position: relative; display: flex; align-items: center; height: 64px; padding: 4px; margin-block: 12px; border-radius: 48px; background: var(--mk-surface-3, var(--mk-surface)); }
|
|
20
|
+
.mk-studio-dock__slide-track > span { position: absolute; inset-inline: 60px 12px; text-align: center; font-size: .75rem; pointer-events: none; }
|
|
21
|
+
.mk-studio-dock [data-mk-studio-slide] { position: relative; display: grid; place-items: center; width: 56px; height: 56px; border: 0; border-radius: 50%; background: var(--mk-surface); color: inherit; font-size: 20px; cursor: pointer; touch-action: pan-y; transform: translateX(var(--mk-studio-drag, 0px)); }
|
|
22
|
+
.mk-studio-dock__clock { position: relative; aspect-ratio: 1; display: grid; place-content: center; gap: 4px; text-align: center; border: 10px solid var(--mk-surface); border-radius: 50%; background: var(--mk-surface-2); }
|
|
23
|
+
.mk-studio-dock__clock::after { content: ''; position: absolute; inset: 8px; border: 1px solid var(--mk-outline-soft); border-radius: 50%; }
|
|
24
|
+
.mk-studio-dock__clock small { color: var(--mk-muted); }
|
|
25
|
+
.mk-studio-dock__pages { margin-block-start: 12px; }
|
|
26
|
+
.mk-studio-dock__pagination { display: flex; justify-content: center; gap: 8px; margin-block-start: 12px; }
|
|
27
|
+
.mk-studio-dock__pagination button { width: 32px; height: 32px; padding: 0; border: 0; border-radius: 50%; background: var(--mk-surface); color: inherit; font: inherit; cursor: pointer; }
|
|
28
|
+
.mk-studio-dock__pagination [aria-current] { background: var(--mk-accent); color: var(--mk-on-accent); }
|
|
29
|
+
.mk-studio-dock__view { display: grid; gap: 16px; padding: 12px 0; }
|
|
30
|
+
.mk-studio-dock__view input, .mk-studio-dock__view textarea { width: 100%; padding: 12px; border: 1px solid var(--mk-outline-soft); border-radius: 8px; background: var(--mk-surface); color: inherit; font: inherit; }
|
|
31
|
+
.mk-studio-dock__view label { display: grid; gap: 6px; }
|
|
32
|
+
.mk-studio-dock__view video { width: 100%; border-radius: 10px; }
|
|
33
|
+
.mk-studio-dock :is(button,a,input,textarea):focus-visible { outline: 2px solid var(--mk-accent); outline-offset: 3px; }
|
|
34
|
+
@media (prefers-reduced-motion: reduce) { .mk-studio-dock *, .mk-studio-dock *::after { transition: none !important; animation: none !important; } }
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
# theme-picker
|
|
2
2
|
|
|
3
|
-
Las bolas de color del selector de tema
|
|
4
|
-
|
|
5
|
-
Origen: Stepbro — `theme-picker` / `theme-picker-enter` en el kit; CSS de las bolas portada de Kayro (`.accent-swatch`).
|
|
3
|
+
Las bolas de color del selector de tema. Entran escalonadas (desde 50 %, blur 4 px, 10 ms entre bolas); al pasar el ratón la bola se redondea a 32 px y se satura, y la elegida queda como cuadrado redondeado de 16 px (0.5 s, `cubic-bezier(.38,.49,0,1.5)`), con dos luces difuminadas que se mueven dentro (3.5 s). Al elegir una se aplica el tema con `setTheme()`.
|
|
6
4
|
|
|
7
5
|
## HTML / Astro
|
|
8
6
|
|
|
@@ -43,8 +41,8 @@ Con tus propias bolas (no genera ninguna):
|
|
|
43
41
|
|
|
44
42
|
| Opción | Atributo | Por defecto | Qué hace |
|
|
45
43
|
|---|---|---|---|
|
|
46
|
-
| `themes` | `themes="a,b"` | los 10 visibles
|
|
47
|
-
| `showHidden` | `show-hidden` | `false` | Incluye los 9 temas que
|
|
44
|
+
| `themes` | `themes="a,b"` | los 10 visibles | Ids y orden de las bolas |
|
|
45
|
+
| `showHidden` | `show-hidden` | `false` | Incluye los 9 temas que oculta |
|
|
48
46
|
| `labels.group` | `aria-label` | `'Tema'` | Nombre accesible del grupo |
|
|
49
47
|
| `onChange` | evento `mk:theme` en `document` | — | Tema elegido |
|
|
50
48
|
|
|
@@ -7,14 +7,14 @@ import { THEMES } from '../../theme/presets';
|
|
|
7
7
|
import { THEME_EVENT, getTheme, setTheme } from '../../theme/theme';
|
|
8
8
|
|
|
9
9
|
/*
|
|
10
|
-
*
|
|
10
|
+
* theme picker: colour balls pop in (scale .5, blur 4, 10 ms apart);
|
|
11
11
|
* a ball rounds into a squircle on hover and into a rounded square when chosen
|
|
12
12
|
* (0.5 s, cubic(.38,.49,0,1.5)), with two blurred lights drifting inside.
|
|
13
13
|
* Clicking one applies the theme (setTheme); external changes keep it in sync.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
export interface ThemePickerOptions {
|
|
17
|
-
/** Theme ids to show, in order. Default:
|
|
17
|
+
/** Theme ids to show, in order. Default: visible presets, or all with showHidden. */
|
|
18
18
|
themes?: string[];
|
|
19
19
|
showHidden?: boolean;
|
|
20
20
|
labels?: { group?: string };
|
|
@@ -11,7 +11,7 @@ export interface ThemePickerProps extends ThemePickerOptions {
|
|
|
11
11
|
unstyled?: boolean;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
/**
|
|
14
|
+
/** theme picker. Wrap the app in <ThemeProvider> to read the choice. */
|
|
15
15
|
export function ThemePicker({ className, style, unstyled, themes, showHidden, labels, onChange }: ThemePickerProps) {
|
|
16
16
|
const ref = useRef<HTMLDivElement>(null);
|
|
17
17
|
const change = useLatest(onChange);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# toast
|
|
2
2
|
|
|
3
|
-
La pila de confirmaciones
|
|
3
|
+
La pila de confirmaciones, arriba de la pantalla y por encima de los diálogos.
|
|
4
4
|
|
|
5
5
|
- **Pila:** los toasts más viejos asoman 16 px debajo, 5 % más pequeños cada uno, tres visibles; como mucho 5 vivos.
|
|
6
6
|
- **Abrir la pila:** al pasar el ratón se despliega (resorte .55, 60 ms entre tarjetas) y los temporizadores se pausan; al salir se recoge tras 150 ms.
|
|
@@ -8,8 +8,6 @@ La pila de confirmaciones de Stepbro, arriba de la pantalla y por encima de los
|
|
|
8
8
|
- **Letras:** suben una a una (`yPercent 40 → 0`, blur 4, 0.4 s, 20 ms, tras 0.225 s), hasta 64 caracteres.
|
|
9
9
|
- **Salir:** sube 24 px hacia un blur de 4 px (0.3 s); deslizar hacia arriba 48 px o la rueda 80 px lo descarta.
|
|
10
10
|
|
|
11
|
-
Origen: Stepbro `toast-deck`, `toast-morph`, `toast-character-reveal` (kit Money); portado de Kayro.
|
|
12
|
-
|
|
13
11
|
## HTML / Astro
|
|
14
12
|
|
|
15
13
|
```html
|
|
@@ -9,7 +9,7 @@ import { iconSvg, type IconName } from '../icons';
|
|
|
9
9
|
import { dismissToast, getToasts, removeToast, subscribeToasts, toastTimers, type ToastItem, type ToastTone } from './store';
|
|
10
10
|
|
|
11
11
|
/*
|
|
12
|
-
*
|
|
12
|
+
* toast deck:
|
|
13
13
|
* - toasts stack at the top; older ones peek out 16 px below, 5 % smaller each,
|
|
14
14
|
* three drawn; hovering opens the deck (spring .55, 60 ms cascade) and pauses
|
|
15
15
|
* the timers, leaving closes it after 150 ms;
|
|
@@ -112,7 +112,7 @@ function revealLetters(element: HTMLElement) {
|
|
|
112
112
|
);
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
/** Grow a toast's shell out of `origin
|
|
115
|
+
/** Grow a toast's shell out of `origin`. Returns a function that settles it now. */
|
|
116
116
|
function morphFrom(card: HTMLElement, shell: HTMLElement, body: HTMLElement, origin: HTMLElement) {
|
|
117
117
|
const T = TIMING.toast;
|
|
118
118
|
const box = shell.getBoundingClientRect();
|
|
@@ -8,7 +8,7 @@ export interface ToasterProps {
|
|
|
8
8
|
labels?: ToasterLabels;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
/**
|
|
11
|
+
/** toast deck. Mount once near the root; show toasts with `toast()`. */
|
|
12
12
|
export function Toaster({ labels }: ToasterProps) {
|
|
13
13
|
const ref = useRef<HTMLDivElement>(null);
|
|
14
14
|
const dismiss = labels?.dismiss;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* The toasts on screen and their timers
|
|
3
|
-
* Kayro). Framework-agnostic: any number of toasters (vanilla, element, React)
|
|
2
|
+
* The toasts on screen and their timers. Framework-agnostic: any number of toasters (vanilla, element, React)
|
|
4
3
|
* render the same list. A timer pauses while the deck is open and resumes with
|
|
5
4
|
* what was left. Never put a secret in a message: toasts confirm an action.
|
|
6
5
|
*/
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# trailer
|
|
2
|
+
|
|
3
|
+
Modal de video que compone el motor `dialog`: top layer, bloqueo del fondo, Escape y retorno de foco. Medios escritos por el consumidor; no inyecta YouTube, rastreadores ni videos remotos.
|
|
4
|
+
|
|
5
|
+
```html
|
|
6
|
+
<button data-mk-open="preview">Ver trailer</button>
|
|
7
|
+
<dialog class="mk-trailer" id="preview">
|
|
8
|
+
<header class="mk-trailer__header">
|
|
9
|
+
<h2 data-mk-part="title">Trailer</h2>
|
|
10
|
+
<button data-mk-part="close">Cerrar</button>
|
|
11
|
+
</header>
|
|
12
|
+
<video controls playsinline preload="metadata" src="/trailer.webm">
|
|
13
|
+
<track kind="captions" src="/captions.vtt" srclang="es" label="Español" />
|
|
14
|
+
</video>
|
|
15
|
+
</dialog>
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`createTrailer(dialog, { triggers:true, autoplay:false, closeOnBackdrop:true, onOpen, onClose })` devuelve `open(origin?)`, `close()`, `toggle(origin?)`, `play():Promise<boolean>`, `pause()`, `seek(seconds)`, `setVolume(0..1)`, `isOpen`, `destroy()`.
|
|
19
|
+
|
|
20
|
+
React: `<Trailer id="preview" options={…}>…</Trailer>` o `useTrailer(ref, options, deps?)` sobre un `<dialog>`. Custom element: `<mk-trailer><dialog id="preview">…</dialog></mk-trailer>`, `defineTrailer()`. La estructura permanece en light DOM. Los callbacks/options de los hooks se leen al crear; usar `deps` cuando cambien.
|
|
21
|
+
|
|
22
|
+
Controles opcionales: botones `data-mk-trailer-play`, `mute`, `fullscreen`, `captions`; inputs range `data-mk-trailer-seek` y `volume`; output `data-mk-trailer-time`; párrafo `data-mk-trailer-status`. Sólo oculta los controles nativos si existen Play/Mute/Seek y, cuando hay tracks, un botón CC. `aria-label`, `aria-pressed`, duración, posición y mensajes de error se sincronizan. Los sliders conservan teclado nativo; fuera de ellos Space/K alterna, flechas buscan±5s y M cambia mute. Los subtítulos usan el primer track. Pantalla completa depende del soporte del navegador y de un gesto del usuario.
|
|
23
|
+
|
|
24
|
+
Autoplay está desactivado. Cerrar o destruir pausa el video y cancela solicitudes de play pendientes. `play()` absorbe rechazos del navegador y anuncia el error. `destroy()` devuelve los controles/atributos originales y retira listeners. La fuente puede fallar sin bloquear el cierre.
|
|
25
|
+
|
|
26
|
+
Pantalla completa se solicita sobre `[data-mk-trailer-stage]` (un wrapper normal alrededor de video y controles), o sobre el video si falta ese wrapper. Nunca sobre `<dialog>`, que la API no admite. Cerrar/destruir sale de la pantalla completa que pertenece a esta instancia.
|
|
27
|
+
|
|
28
|
+
El modal usa el morph accesible existente de motion-kit. **No copia** la apertura por bandas ni la disolución procedural de A24; el modelo de interacción y los controles se reconstruyeron para ser reutilizables. La demo usa SVG, captions y WebM silencioso originales de 6 s, sin assets de las referencias.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { MkElement, defineElement, type OptionType } from '../../elements/base';
|
|
2
|
+
import { createTrailer, type TrailerInstance, type TrailerOptions } from './engine';
|
|
3
|
+
|
|
4
|
+
/** Wrap a consumer-authored native <dialog> containing <video>. */
|
|
5
|
+
export class MkTrailerElement extends MkElement<TrailerInstance> {
|
|
6
|
+
static options: Record<string, OptionType> = { autoplay: 'boolean', triggers: 'string', closeOnBackdrop: 'boolean' };
|
|
7
|
+
protected create(options: Record<string, unknown>) {
|
|
8
|
+
return createTrailer(this.querySelector('dialog'), options as TrailerOptions);
|
|
9
|
+
}
|
|
10
|
+
open(origin?: HTMLElement | null) { this.instance?.open(origin); }
|
|
11
|
+
close() { this.instance?.close(); }
|
|
12
|
+
}
|
|
13
|
+
export const defineTrailer = () => defineElement('mk-trailer', MkTrailerElement);
|