@yewdiel21/motion-kit 0.0.0-stage → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +38 -0
- package/README.md +80 -2
- package/bin/motion-kit.mjs +242 -0
- package/css.d.ts +2 -0
- package/dist/auto/index.d.ts +63 -0
- package/dist/auto/index.js +207 -0
- package/dist/auto/index.js.map +1 -0
- package/dist/chunk-2BQGFOA7.js +177 -0
- package/dist/chunk-2BQGFOA7.js.map +1 -0
- package/dist/chunk-34XYOXHG.js +2374 -0
- package/dist/chunk-34XYOXHG.js.map +1 -0
- package/dist/chunk-6OJAP6ZL.js +832 -0
- package/dist/chunk-6OJAP6ZL.js.map +1 -0
- package/dist/chunk-ABNRDDO7.js +28 -0
- package/dist/chunk-ABNRDDO7.js.map +1 -0
- package/dist/chunk-FS2HJVVY.js +3320 -0
- package/dist/chunk-FS2HJVVY.js.map +1 -0
- package/dist/chunk-J27KH2A2.js +157 -0
- package/dist/chunk-J27KH2A2.js.map +1 -0
- package/dist/chunk-KLY5BRRI.js +18 -0
- package/dist/chunk-KLY5BRRI.js.map +1 -0
- package/dist/chunk-Q4LVFNLN.js +1617 -0
- package/dist/chunk-Q4LVFNLN.js.map +1 -0
- package/dist/chunk-RFDIZYFX.js +304 -0
- package/dist/chunk-RFDIZYFX.js.map +1 -0
- package/dist/chunk-TGV27ZWM.js +17 -0
- package/dist/chunk-TGV27ZWM.js.map +1 -0
- package/dist/confetti-BSX1CLLV.d.ts +41 -0
- package/dist/css/components/chat-background.css +87 -0
- package/dist/css/components/chat.css +237 -0
- package/dist/css/components/cursor.css +178 -0
- package/dist/css/components/dialog.css +259 -0
- package/dist/css/components/island.css +301 -0
- package/dist/css/components/menu.css +105 -0
- package/dist/css/components/nav.css +1054 -0
- package/dist/css/components/steps.css +50 -0
- package/dist/css/components/theme-picker.css +115 -0
- package/dist/css/components/toast.css +115 -0
- package/dist/css/controls.css +950 -0
- package/dist/css/effects.css +587 -0
- package/dist/css/fonts.css +3 -0
- package/dist/css/themes/black-green.css +126 -0
- package/dist/css/themes/black.css +126 -0
- package/dist/css/themes/blue.css +126 -0
- package/dist/css/themes/brown.css +126 -0
- package/dist/css/themes/cold-blue.css +126 -0
- package/dist/css/themes/funny.css +126 -0
- package/dist/css/themes/gray.css +126 -0
- package/dist/css/themes/green.css +126 -0
- package/dist/css/themes/modern-1.css +126 -0
- package/dist/css/themes/neogreen.css +126 -0
- package/dist/css/themes/new.css +126 -0
- package/dist/css/themes/new2.css +126 -0
- package/dist/css/themes/oled.css +126 -0
- package/dist/css/themes/orange.css +126 -0
- package/dist/css/themes/pastel-pink.css +126 -0
- package/dist/css/themes/pink.css +126 -0
- package/dist/css/themes/purple.css +126 -0
- package/dist/css/themes/red.css +126 -0
- package/dist/css/themes/super-blue.css +126 -0
- package/dist/css/themes.css +2431 -0
- package/dist/css/tokens.css +452 -0
- package/dist/elements/index.d.ts +183 -0
- package/dist/elements/index.js +472 -0
- package/dist/elements/index.js.map +1 -0
- package/dist/engine-CUr9QijM.d.ts +46 -0
- package/dist/gl/index.d.ts +61 -0
- package/dist/gl/index.js +18 -0
- package/dist/gl/index.js.map +1 -0
- package/dist/index.css +5296 -0
- package/dist/index.d.ts +440 -0
- package/dist/index.js +390 -0
- package/dist/index.js.map +1 -0
- package/dist/react/index.d.ts +476 -0
- package/dist/react/index.js +969 -0
- package/dist/react/index.js.map +1 -0
- package/dist/reduced-BM4Ls0g5.d.ts +566 -0
- package/dist/reveal-WFUWDrei.d.ts +416 -0
- package/dist/store-Bqy_obnD.d.ts +195 -0
- package/dist/theme/generate.d.ts +18 -0
- package/dist/theme/generate.js +64 -0
- package/dist/theme/generate.js.map +1 -0
- package/dist/theme/index.d.ts +17 -0
- package/dist/theme/index.js +32 -0
- package/dist/theme/index.js.map +1 -0
- package/dist/theme-BFMoCzi6.d.ts +158 -0
- package/dist/types-BkS_raL8.d.ts +7 -0
- package/package.json +114 -3
- package/registry.json +469 -0
- package/skills/motion-kit/SKILL.md +189 -0
- package/skills/motion-kit/reference/components.md +86 -0
- package/skills/motion-kit/reference/design.md +92 -0
- package/skills/motion-kit/reference/recipes.md +172 -0
- package/src/auto/auto-init.ts +151 -0
- package/src/auto/index.ts +3 -0
- package/src/auto/parse-options.ts +68 -0
- package/src/components/chat/README.md +90 -0
- package/src/components/chat/chat.css +236 -0
- package/src/components/chat/element.ts +98 -0
- package/src/components/chat/engine.ts +295 -0
- package/src/components/chat/react.tsx +196 -0
- package/src/components/chat-background/README.md +50 -0
- package/src/components/chat-background/audio.ts +56 -0
- package/src/components/chat-background/chat-background.css +86 -0
- package/src/components/chat-background/element.ts +31 -0
- package/src/components/chat-background/engine.ts +108 -0
- package/src/components/chat-background/react.tsx +36 -0
- package/src/components/cursor/README.md +53 -0
- package/src/components/cursor/cursor.css +177 -0
- package/src/components/cursor/element.ts +23 -0
- package/src/components/cursor/engine.ts +254 -0
- package/src/components/cursor/react.tsx +26 -0
- package/src/components/dialog/README.md +106 -0
- package/src/components/dialog/dialog.css +258 -0
- package/src/components/dialog/element.ts +118 -0
- package/src/components/dialog/engine.ts +432 -0
- package/src/components/dialog/react.tsx +200 -0
- package/src/components/icon.tsx +10 -0
- package/src/components/icons.ts +24 -0
- package/src/components/island/README.md +91 -0
- package/src/components/island/element.ts +45 -0
- package/src/components/island/engine.ts +200 -0
- package/src/components/island/island.css +300 -0
- package/src/components/island/react.tsx +115 -0
- package/src/components/menu/README.md +67 -0
- package/src/components/menu/element.ts +55 -0
- package/src/components/menu/engine.ts +142 -0
- package/src/components/menu/menu.css +104 -0
- package/src/components/menu/react.tsx +82 -0
- package/src/components/nav/README.md +85 -0
- package/src/components/nav/element.ts +39 -0
- package/src/components/nav/engine.ts +164 -0
- package/src/components/nav/nav.css +1053 -0
- package/src/components/nav/react.tsx +90 -0
- package/src/components/steps/README.md +70 -0
- package/src/components/steps/element.ts +43 -0
- package/src/components/steps/engine.ts +185 -0
- package/src/components/steps/react.tsx +91 -0
- package/src/components/steps/steps.css +49 -0
- package/src/components/theme-picker/README.md +57 -0
- package/src/components/theme-picker/element.ts +20 -0
- package/src/components/theme-picker/engine.ts +91 -0
- package/src/components/theme-picker/react.tsx +31 -0
- package/src/components/theme-picker/theme-picker.css +114 -0
- package/src/components/toast/README.md +60 -0
- package/src/components/toast/element.ts +25 -0
- package/src/components/toast/engine.ts +483 -0
- package/src/components/toast/react.tsx +22 -0
- package/src/components/toast/store.ts +122 -0
- package/src/components/toast/toast.css +114 -0
- package/src/core/channels.ts +165 -0
- package/src/core/curves.ts +35 -0
- package/src/core/frame.ts +12 -0
- package/src/core/gsap.ts +65 -0
- package/src/core/loop.ts +44 -0
- package/src/core/math.ts +9 -0
- package/src/core/physics-spring.ts +59 -0
- package/src/core/pointer.ts +63 -0
- package/src/core/reduced.ts +21 -0
- package/src/core/scope.ts +70 -0
- package/src/core/scroll-track.ts +106 -0
- package/src/core/split.ts +68 -0
- package/src/core/spring.ts +22 -0
- package/src/core/styles.ts +31 -0
- package/src/core/timing.ts +204 -0
- package/src/core/types.ts +20 -0
- package/src/core/visible.ts +19 -0
- package/src/css/base.css +142 -0
- package/src/css/controls/README.md +122 -0
- package/src/css/controls/burger.css +67 -0
- package/src/css/controls/button.css +226 -0
- package/src/css/controls/checkbox.css +53 -0
- package/src/css/controls/chip.css +85 -0
- package/src/css/controls/field.css +124 -0
- package/src/css/controls/glass.css +220 -0
- package/src/css/controls/icon.css +26 -0
- package/src/css/controls/segmented.css +60 -0
- package/src/css/controls/switch.css +72 -0
- package/src/css/default-theme.css +123 -0
- package/src/css/effects/README.md +134 -0
- package/src/css/effects/anim.css +224 -0
- package/src/css/effects/fade.css +15 -0
- package/src/css/effects/float.css +64 -0
- package/src/css/effects/frost.css +62 -0
- package/src/css/effects/grain.css +58 -0
- package/src/css/effects/loading.css +60 -0
- package/src/css/effects/success.css +22 -0
- package/src/css/effects/text.css +67 -0
- package/src/css/fonts.css +3 -0
- package/src/css/states.css +51 -0
- package/src/css/themes/black-green.css +126 -0
- package/src/css/themes/black.css +126 -0
- package/src/css/themes/blue.css +126 -0
- package/src/css/themes/brown.css +126 -0
- package/src/css/themes/cold-blue.css +126 -0
- package/src/css/themes/funny.css +126 -0
- package/src/css/themes/gray.css +126 -0
- package/src/css/themes/green.css +126 -0
- package/src/css/themes/modern-1.css +126 -0
- package/src/css/themes/neogreen.css +126 -0
- package/src/css/themes/new.css +126 -0
- package/src/css/themes/new2.css +126 -0
- package/src/css/themes/oled.css +126 -0
- package/src/css/themes/orange.css +126 -0
- package/src/css/themes/pastel-pink.css +126 -0
- package/src/css/themes/pink.css +126 -0
- package/src/css/themes/purple.css +126 -0
- package/src/css/themes/red.css +126 -0
- package/src/css/themes/super-blue.css +126 -0
- package/src/css/tokens.css +182 -0
- package/src/elements/base.ts +76 -0
- package/src/elements/index.ts +48 -0
- package/src/gl/index.ts +15 -0
- package/src/gl/liquid-blend/README.md +23 -0
- package/src/gl/liquid-blend/create-liquid-blend.ts +7 -0
- package/src/gl/liquid-blend/shader.ts +9 -0
- package/src/gl/matcap/README.md +23 -0
- package/src/gl/matcap/create-matcap.ts +7 -0
- package/src/gl/matcap/shader.ts +16 -0
- package/src/gl/particle-hero/README.md +41 -0
- package/src/gl/particle-hero/create-particle-hero.ts +544 -0
- package/src/gl/particle-hero/particle-hero.css +19 -0
- package/src/gl/particle-hero/shaders.ts +84 -0
- package/src/gl/shader-blur/README.md +23 -0
- package/src/gl/shader-blur/create-shader-blur.ts +7 -0
- package/src/gl/shader-blur/shader.ts +11 -0
- package/src/gl/shader-grain/README.md +23 -0
- package/src/gl/shader-grain/create-shader-grain.ts +7 -0
- package/src/gl/shader-grain/shader.ts +6 -0
- package/src/gl/shared/create-shader-effect.ts +186 -0
- package/src/gl/shared/shader.css +3 -0
- package/src/gl/shared/shaders.ts +30 -0
- package/src/index.ts +177 -0
- package/src/motion/anchor.ts +45 -0
- package/src/motion/chat.ts +97 -0
- package/src/motion/confetti.ts +130 -0
- package/src/motion/count.ts +95 -0
- package/src/motion/details.ts +70 -0
- package/src/motion/drag-dismiss.ts +231 -0
- package/src/motion/effects.ts +81 -0
- package/src/motion/entrances.ts +27 -0
- package/src/motion/floating.ts +389 -0
- package/src/motion/flow.ts +115 -0
- package/src/motion/indicator.ts +44 -0
- package/src/motion/layout.ts +39 -0
- package/src/motion/liquid.ts +227 -0
- package/src/motion/morph.ts +540 -0
- package/src/motion/reveal.ts +112 -0
- package/src/motion/sheet.ts +65 -0
- package/src/motion/surface.ts +108 -0
- package/src/react/animated-number.tsx +30 -0
- package/src/react/flip-group.tsx +44 -0
- package/src/react/frost.tsx +33 -0
- package/src/react/index.ts +80 -0
- package/src/react/slot-text.tsx +44 -0
- package/src/react/theme-provider.tsx +42 -0
- package/src/react/use-card-dock.ts +9 -0
- package/src/react/use-cascade.ts +9 -0
- package/src/react/use-contrast.ts +9 -0
- package/src/react/use-counter.ts +8 -0
- package/src/react/use-curved-carousel.ts +10 -0
- package/src/react/use-depth-grid.ts +9 -0
- package/src/react/use-details.ts +7 -0
- package/src/react/use-dock-pill.ts +10 -0
- package/src/react/use-drag-ruler.ts +10 -0
- package/src/react/use-fill-text.ts +9 -0
- package/src/react/use-fit-text.ts +8 -0
- package/src/react/use-flow.ts +8 -0
- package/src/react/use-frame-zoom.ts +9 -0
- package/src/react/use-infinite-reel.ts +8 -0
- package/src/react/use-letters.ts +9 -0
- package/src/react/use-liquid-blend.ts +8 -0
- package/src/react/use-marquee.ts +9 -0
- package/src/react/use-mask-text.ts +9 -0
- package/src/react/use-matcap.ts +8 -0
- package/src/react/use-parallax.ts +9 -0
- package/src/react/use-particle-hero.ts +9 -0
- package/src/react/use-pile.ts +8 -0
- package/src/react/use-pop.ts +8 -0
- package/src/react/use-radial-menu.ts +10 -0
- package/src/react/use-recipe.ts +38 -0
- package/src/react/use-reflow.ts +9 -0
- package/src/react/use-repel-bento.ts +8 -0
- package/src/react/use-reveal.ts +8 -0
- package/src/react/use-rotator.ts +10 -0
- package/src/react/use-shader-blur.ts +8 -0
- package/src/react/use-shader-grain.ts +8 -0
- package/src/react/use-slot-text.ts +17 -0
- package/src/react/use-snap-strip.ts +9 -0
- package/src/react/use-sortable.ts +14 -0
- package/src/react/use-spring-cards.ts +8 -0
- package/src/react/use-stretch-footer.ts +20 -0
- package/src/react/use-tabs.ts +10 -0
- package/src/react/use-text-roll.ts +9 -0
- package/src/react/use-ticket-menu.ts +15 -0
- package/src/react/use-words.ts +8 -0
- package/src/recipes/card-dock/README.md +36 -0
- package/src/recipes/card-dock/card-dock.css +178 -0
- package/src/recipes/card-dock/create-card-dock.ts +97 -0
- package/src/recipes/cascade/README.md +33 -0
- package/src/recipes/cascade/create-cascade.ts +111 -0
- package/src/recipes/contrast/README.md +29 -0
- package/src/recipes/contrast/create-contrast.ts +68 -0
- package/src/recipes/counter/README.md +42 -0
- package/src/recipes/counter/create-counter.ts +84 -0
- package/src/recipes/curved-carousel/README.md +50 -0
- package/src/recipes/curved-carousel/carousel-math.ts +20 -0
- package/src/recipes/curved-carousel/create-curved-carousel.ts +257 -0
- package/src/recipes/curved-carousel/curved-carousel.css +50 -0
- package/src/recipes/depth-grid/README.md +30 -0
- package/src/recipes/depth-grid/create-depth-grid.ts +120 -0
- package/src/recipes/details/README.md +31 -0
- package/src/recipes/details/create-details.ts +27 -0
- package/src/recipes/dock-pill/README.md +23 -0
- package/src/recipes/dock-pill/create-dock-pill.ts +68 -0
- package/src/recipes/dock-pill/dock-pill.css +5 -0
- package/src/recipes/drag-ruler/README.md +21 -0
- package/src/recipes/drag-ruler/create-drag-ruler.ts +95 -0
- package/src/recipes/drag-ruler/drag-ruler.css +16 -0
- package/src/recipes/fill-text/README.md +27 -0
- package/src/recipes/fill-text/create-fill-text.ts +93 -0
- package/src/recipes/fit-text/README.md +37 -0
- package/src/recipes/fit-text/create-fit-text.ts +55 -0
- package/src/recipes/fit-text/fit-text.css +6 -0
- package/src/recipes/flow/README.md +46 -0
- package/src/recipes/flow/create-flow.ts +55 -0
- package/src/recipes/frame-zoom/README.md +29 -0
- package/src/recipes/frame-zoom/create-frame-zoom.ts +91 -0
- package/src/recipes/infinite-reel/README.md +23 -0
- package/src/recipes/infinite-reel/create-infinite-reel.ts +140 -0
- package/src/recipes/infinite-reel/infinite-reel.css +8 -0
- package/src/recipes/letters/README.md +30 -0
- package/src/recipes/letters/create-letters.ts +113 -0
- package/src/recipes/marquee/README.md +32 -0
- package/src/recipes/marquee/create-marquee.ts +74 -0
- package/src/recipes/marquee/marquee.css +41 -0
- package/src/recipes/mask-text/README.md +39 -0
- package/src/recipes/mask-text/create-mask-text.ts +134 -0
- package/src/recipes/parallax/README.md +31 -0
- package/src/recipes/parallax/create-parallax.ts +74 -0
- package/src/recipes/pile/README.md +48 -0
- package/src/recipes/pile/create-pile.ts +50 -0
- package/src/recipes/pile/pile-math.ts +27 -0
- package/src/recipes/pile/pile.css +63 -0
- package/src/recipes/pill/README.md +30 -0
- package/src/recipes/pill/pill.css +48 -0
- package/src/recipes/pop/README.md +36 -0
- package/src/recipes/pop/create-pop.ts +52 -0
- package/src/recipes/radial-menu/README.md +44 -0
- package/src/recipes/radial-menu/create-radial-menu.ts +96 -0
- package/src/recipes/radial-menu/radial-menu.css +45 -0
- package/src/recipes/reflow/README.md +47 -0
- package/src/recipes/reflow/create-reflow.ts +49 -0
- package/src/recipes/repel-bento/README.md +21 -0
- package/src/recipes/repel-bento/create-repel-bento.ts +65 -0
- package/src/recipes/repel-bento/repel-bento.css +3 -0
- package/src/recipes/reveal/README.md +36 -0
- package/src/recipes/reveal/create-reveal.ts +48 -0
- package/src/recipes/rotator/README.md +21 -0
- package/src/recipes/rotator/create-rotator.ts +66 -0
- package/src/recipes/rotator/rotator.css +3 -0
- package/src/recipes/slot-text/README.md +44 -0
- package/src/recipes/slot-text/create-slot-text.ts +244 -0
- package/src/recipes/slot-text/slot-text.css +42 -0
- package/src/recipes/snap-strip/README.md +31 -0
- package/src/recipes/snap-strip/create-snap-strip.ts +133 -0
- package/src/recipes/snap-strip/snap-strip.css +23 -0
- package/src/recipes/sortable/README.md +88 -0
- package/src/recipes/sortable/create-sortable.ts +863 -0
- package/src/recipes/sortable/sortable.css +40 -0
- package/src/recipes/spring-cards/README.md +48 -0
- package/src/recipes/spring-cards/create-spring-cards.ts +140 -0
- package/src/recipes/spring-cards/spring-cards.css +23 -0
- package/src/recipes/stretch-footer/README.md +44 -0
- package/src/recipes/stretch-footer/create-stretch-footer.ts +69 -0
- package/src/recipes/stretch-footer/stretch-footer.css +28 -0
- package/src/recipes/tabs/README.md +39 -0
- package/src/recipes/tabs/create-tabs.ts +91 -0
- package/src/recipes/tabs/tabs.css +19 -0
- package/src/recipes/text-roll/README.md +35 -0
- package/src/recipes/text-roll/create-text-roll.ts +137 -0
- package/src/recipes/text-roll/text-roll.css +21 -0
- package/src/recipes/ticket-menu/README.md +24 -0
- package/src/recipes/ticket-menu/create-ticket-menu.ts +6 -0
- package/src/recipes/words/README.md +43 -0
- package/src/recipes/words/create-words.ts +114 -0
- package/src/theme/css-format.ts +32 -0
- package/src/theme/generate.ts +72 -0
- package/src/theme/index.ts +15 -0
- package/src/theme/presets.ts +79 -0
- package/src/theme/theme.ts +119 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/* sortable — Stepbro's drag reorder (Tasks): the item lifts into a floating copy that tilts; the others make room. */
|
|
2
|
+
[data-mk-sorting] {
|
|
3
|
+
-webkit-user-select: none;
|
|
4
|
+
user-select: none;
|
|
5
|
+
cursor: grabbing;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/* The slot the item left (its floating copy is on top). */
|
|
9
|
+
[data-mk-item][data-mk-dragging] {
|
|
10
|
+
visibility: hidden;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/* The floating copy: its shadow grows with the lift (0 → 1). */
|
|
14
|
+
[data-mk-sortable-clone] {
|
|
15
|
+
--mk-lift: 0;
|
|
16
|
+
box-shadow: var(
|
|
17
|
+
--mk-sortable-shadow,
|
|
18
|
+
0 calc(12px * var(--mk-lift)) calc(32px * var(--mk-lift)) rgb(0 0 0 / calc(0.22 * var(--mk-lift))),
|
|
19
|
+
0 0 0 1px var(--md-sys-color-outline-variant) inset
|
|
20
|
+
);
|
|
21
|
+
cursor: grabbing;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
[data-mk-sort-handle] {
|
|
25
|
+
cursor: grab;
|
|
26
|
+
touch-action: none;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
[data-mk-sorting] [data-mk-sort-handle] {
|
|
30
|
+
cursor: grabbing;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* Lifted with the keyboard. */
|
|
34
|
+
[data-mk-item][data-mk-sort-grabbed] {
|
|
35
|
+
position: relative;
|
|
36
|
+
z-index: 1;
|
|
37
|
+
box-shadow:
|
|
38
|
+
0 12px 32px rgb(0 0 0 / 0.22),
|
|
39
|
+
0 0 0 2px var(--mk-accent);
|
|
40
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# spring-cards
|
|
2
|
+
|
|
3
|
+
Tarjetas en abanico: entran escalando con rebote, cada una tiene un resorte que arranca desplazado al azar y vuelve a su sitio, y el cursor las empuja al pasar cerca.
|
|
4
|
+
|
|
5
|
+
Origen: spencergabor.com — portada (Hero.vue, `useSpring`).
|
|
6
|
+
|
|
7
|
+
## Vanilla / Astro
|
|
8
|
+
|
|
9
|
+
```html
|
|
10
|
+
<div data-mk="spring-cards" class="cards">
|
|
11
|
+
<div data-mk-card><div class="mk-card__shadow"></div><img src="a.jpg" alt=""></div>
|
|
12
|
+
<div data-mk-card><div class="mk-card__shadow"></div><img src="b.jpg" alt=""></div>
|
|
13
|
+
</div>
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Con `autoInit()` (de `@yewdiel21/motion-kit/auto`) las opciones van como `data-mk-<opción>` en kebab-case. Sin `autoInit`: `createSpringCards(el, opciones)`.
|
|
17
|
+
|
|
18
|
+
## React
|
|
19
|
+
|
|
20
|
+
```tsx
|
|
21
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
22
|
+
useSpringCards(ref);
|
|
23
|
+
return <div ref={ref} data-mk="spring-cards">{/* data-mk-card … */}</div>;
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Opciones
|
|
27
|
+
|
|
28
|
+
| Opción | Por defecto | Qué hace |
|
|
29
|
+
|---|---|---|
|
|
30
|
+
| `selector` | `'[data-mk-card]'` | Tarjetas dentro del root |
|
|
31
|
+
| `initialOffset` | `100` | Desplazamiento inicial aleatorio, ±px |
|
|
32
|
+
| `releaseAfter` | `100` | ms hasta soltar los resortes |
|
|
33
|
+
| `push` | `.4` | Fuerza del empuje del cursor |
|
|
34
|
+
| `falloff` | `6` | Exponente de caída con la distancia: (1 − d)^falloff |
|
|
35
|
+
| `damping` | `.9` | Amortiguación por frame del empuje |
|
|
36
|
+
| `stiffness / friction` | `.05 / .8` | Resorte |
|
|
37
|
+
| `entrance` | `true` | Entrada escalando |
|
|
38
|
+
| `stagger / delay` | `.08 / .4` | Segundos |
|
|
39
|
+
|
|
40
|
+
## Controles
|
|
41
|
+
|
|
42
|
+
`destroy()`
|
|
43
|
+
|
|
44
|
+
## CSS
|
|
45
|
+
|
|
46
|
+
Cada hijo de `[data-mk-card]` se traslada con `--mk-x/--mk-y` y rota `--mk-tilt − --mk-velocity·.25deg`. Las inclinaciones por defecto (2°, −3°, 6°, −2°) y el desfase vertical (`--mk-ty`) se cambian con esas variables.
|
|
47
|
+
|
|
48
|
+
Con `prefers-reduced-motion: reduce` la receta deja el contenido en su estado final, sin animar.
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { gsap, ELASTIC, registerMotion } from '../../core/gsap';
|
|
2
|
+
import { entrances, reduced } from '../../core/reduced';
|
|
3
|
+
import { createSpring, type Spring } from '../../core/spring';
|
|
4
|
+
import { whileVisible } from '../../core/loop';
|
|
5
|
+
import { bindPointer, pointer, takeMovement } from '../../core/pointer';
|
|
6
|
+
import { clamp } from '../../core/math';
|
|
7
|
+
import { createScope } from '../../core/scope';
|
|
8
|
+
import { inert, warnOnce, type Instance } from '../../core/types';
|
|
9
|
+
|
|
10
|
+
/*
|
|
11
|
+
* spencergabor.com hero: each card owns a spring that starts at a random offset
|
|
12
|
+
* and is pulled home shortly after; pointer movement near a card adds velocity
|
|
13
|
+
* with a (1 − distance)^falloff · push weight. CSS turns the variables into
|
|
14
|
+
* translate/rotate (see spring-cards.css).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export interface SpringCardsOptions {
|
|
18
|
+
/** Cards inside the root. */
|
|
19
|
+
selector?: string;
|
|
20
|
+
/** Random start offset range, ±px. */
|
|
21
|
+
initialOffset?: number;
|
|
22
|
+
/** Delay before the springs are pulled to rest, ms. */
|
|
23
|
+
releaseAfter?: number;
|
|
24
|
+
/** Strength of the pointer push. */
|
|
25
|
+
push?: number;
|
|
26
|
+
/** Exponent of the distance falloff. */
|
|
27
|
+
falloff?: number;
|
|
28
|
+
/** Per-frame decay of the pushed velocity. */
|
|
29
|
+
damping?: number;
|
|
30
|
+
stiffness?: number;
|
|
31
|
+
friction?: number;
|
|
32
|
+
/** Scale-in entrance. */
|
|
33
|
+
entrance?: boolean;
|
|
34
|
+
stagger?: number;
|
|
35
|
+
delay?: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type SpringCardsInstance = Instance;
|
|
39
|
+
|
|
40
|
+
const VARS = ['--mk-x', '--mk-y', '--mk-velocity'] as const;
|
|
41
|
+
|
|
42
|
+
interface Card {
|
|
43
|
+
el: HTMLElement;
|
|
44
|
+
spring: Spring;
|
|
45
|
+
push: { x: number; y: number };
|
|
46
|
+
cx: number;
|
|
47
|
+
cy: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const rand = (min: number, max: number) => Math.random() * (max - min) + min;
|
|
51
|
+
|
|
52
|
+
export function createSpringCards(root: HTMLElement | null, options: SpringCardsOptions = {}): SpringCardsInstance {
|
|
53
|
+
const o = {
|
|
54
|
+
selector: '[data-mk-card]',
|
|
55
|
+
initialOffset: 100,
|
|
56
|
+
releaseAfter: 100,
|
|
57
|
+
push: 0.4,
|
|
58
|
+
falloff: 6,
|
|
59
|
+
damping: 0.9,
|
|
60
|
+
stiffness: 0.05,
|
|
61
|
+
friction: 0.8,
|
|
62
|
+
entrance: true,
|
|
63
|
+
stagger: 0.08,
|
|
64
|
+
delay: 0.4,
|
|
65
|
+
...options,
|
|
66
|
+
};
|
|
67
|
+
const elements = root ? [...root.querySelectorAll<HTMLElement>(o.selector)] : [];
|
|
68
|
+
if (!root || !elements.length) {
|
|
69
|
+
warnOnce(`spring-cards: no "${o.selector}" inside the root`);
|
|
70
|
+
return inert();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
registerMotion();
|
|
74
|
+
const scope = createScope();
|
|
75
|
+
const ctx = gsap.context(() => {});
|
|
76
|
+
scope.add(() => ctx.revert());
|
|
77
|
+
|
|
78
|
+
if (o.entrance && entrances()) {
|
|
79
|
+
ctx.add(() => gsap.fromTo(elements, { scale: 0 }, { scale: 1, duration: 1, ease: ELASTIC, stagger: o.stagger, delay: o.delay }));
|
|
80
|
+
}
|
|
81
|
+
root.dataset.mkReady = '';
|
|
82
|
+
scope.add(() => {
|
|
83
|
+
delete root.dataset.mkReady;
|
|
84
|
+
elements.forEach((el) => VARS.forEach((name) => el.style.removeProperty(name)));
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
if (reduced()) return { destroy: () => scope.dispose() };
|
|
88
|
+
|
|
89
|
+
scope.add(bindPointer());
|
|
90
|
+
const cards: Card[] = elements.map((el) => {
|
|
91
|
+
const spring = createSpring(o.stiffness, o.friction);
|
|
92
|
+
spring.current.x = spring.target.x = rand(-o.initialOffset, o.initialOffset);
|
|
93
|
+
spring.current.y = spring.target.y = rand(-o.initialOffset, o.initialOffset);
|
|
94
|
+
return { el, spring, push: { x: 0, y: 0 }, cx: 0, cy: 0 };
|
|
95
|
+
});
|
|
96
|
+
scope.timeout(() => cards.forEach(({ spring }) => (spring.target.x = spring.target.y = 0)), o.releaseAfter);
|
|
97
|
+
|
|
98
|
+
// Centres in page coordinates; the card element is only scaled, never translated.
|
|
99
|
+
const measure = () =>
|
|
100
|
+
cards.forEach((card) => {
|
|
101
|
+
const rect = card.el.getBoundingClientRect();
|
|
102
|
+
card.cx = rect.left + scrollX + rect.width / 2;
|
|
103
|
+
card.cy = rect.top + scrollY + rect.height / 2;
|
|
104
|
+
});
|
|
105
|
+
measure();
|
|
106
|
+
scope.observe(new ResizeObserver(measure)).observe(root);
|
|
107
|
+
|
|
108
|
+
scope.add(
|
|
109
|
+
whileVisible(root, (dt) => {
|
|
110
|
+
const movement = takeMovement();
|
|
111
|
+
const mx = clamp(movement.mx, -80, 80);
|
|
112
|
+
const my = clamp(movement.my, -80, 80);
|
|
113
|
+
const decay = Math.pow(o.damping, dt);
|
|
114
|
+
|
|
115
|
+
for (const card of cards) {
|
|
116
|
+
const { spring, push } = card;
|
|
117
|
+
push.x *= decay;
|
|
118
|
+
push.y *= decay;
|
|
119
|
+
spring.current.x += push.x * dt;
|
|
120
|
+
spring.current.y += push.y * dt;
|
|
121
|
+
spring.update(dt);
|
|
122
|
+
|
|
123
|
+
if (pointer.active) {
|
|
124
|
+
const dx = ((pointer.x + scrollX - card.cx) / innerWidth) * 2;
|
|
125
|
+
const dy = ((pointer.y + scrollY - card.cy) / innerHeight) * 2;
|
|
126
|
+
const k = Math.pow(Math.max(0, 1 - Math.hypot(dx, dy)), o.falloff) * o.push;
|
|
127
|
+
push.x += mx * k;
|
|
128
|
+
push.y += my * k;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const style = card.el.style;
|
|
132
|
+
style.setProperty('--mk-x', spring.current.x.toFixed(2));
|
|
133
|
+
style.setProperty('--mk-y', spring.current.y.toFixed(2));
|
|
134
|
+
style.setProperty('--mk-velocity', (spring.velocity.x - spring.velocity.y).toFixed(3));
|
|
135
|
+
}
|
|
136
|
+
}),
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
return { destroy: () => scope.dispose() };
|
|
140
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/* spring-cards — card geometry and tilt from spencergabor.com (Hero) */
|
|
2
|
+
|
|
3
|
+
[data-mk-card] {
|
|
4
|
+
display: grid;
|
|
5
|
+
border-radius: var(--mk-radius-card);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/* Every layer of the card (shadow, image) follows the spring */
|
|
9
|
+
[data-mk-card] > * {
|
|
10
|
+
grid-area: 1 / 1;
|
|
11
|
+
border-radius: inherit;
|
|
12
|
+
translate: calc(var(--mk-x, 0) * 1px) calc(var(--mk-y, 0) * 1px + var(--mk-ty, 0%));
|
|
13
|
+
rotate: calc(var(--mk-tilt, 0deg) - var(--mk-velocity, 0) * 0.25deg);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
[data-mk-card]:nth-child(4n + 1) { --mk-tilt: 2deg; --mk-ty: 5%; }
|
|
17
|
+
[data-mk-card]:nth-child(4n + 2) { --mk-tilt: -3deg; --mk-ty: -6%; }
|
|
18
|
+
[data-mk-card]:nth-child(4n + 3) { --mk-tilt: 6deg; --mk-ty: 5%; }
|
|
19
|
+
[data-mk-card]:nth-child(4n + 4) { --mk-tilt: -2deg; --mk-ty: -4%; }
|
|
20
|
+
|
|
21
|
+
.mk-card__shadow {
|
|
22
|
+
box-shadow: var(--mk-shadow);
|
|
23
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# stretch-footer
|
|
2
|
+
|
|
3
|
+
Footer fijo detrás de la página que aparece al final del scroll; su título gigante se estira de 0 a 1 en vertical siguiendo el scroll suavizado.
|
|
4
|
+
|
|
5
|
+
Origen: spencergabor.com — Footer.vue.
|
|
6
|
+
|
|
7
|
+
## Vanilla / Astro
|
|
8
|
+
|
|
9
|
+
```html
|
|
10
|
+
<main style="position:relative; z-index:1; background:#fff">…</main>
|
|
11
|
+
<div data-mk-phantom aria-hidden="true"></div>
|
|
12
|
+
<footer data-mk="stretch-footer">
|
|
13
|
+
<p data-mk-footer-title data-mk="fit-text">Contacto</p>
|
|
14
|
+
<ul>…enlaces…</ul>
|
|
15
|
+
</footer>
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Con `autoInit()` (de `@yewdiel21/motion-kit/auto`) las opciones van como `data-mk-<opción>` en kebab-case. Sin `autoInit`: `createStretchFooter(el, opciones)`.
|
|
19
|
+
|
|
20
|
+
## React
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
const footer = useRef<HTMLElement>(null);
|
|
24
|
+
const phantom = useRef<HTMLDivElement>(null);
|
|
25
|
+
useStretchFooter(footer, { phantomRef: phantom });
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Opciones
|
|
29
|
+
|
|
30
|
+
| Opción | Por defecto | Qué hace |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| `phantom` | `'[data-mk-phantom]'` | Espaciador al final del flujo (100dvh) |
|
|
33
|
+
| `title` | `'[data-mk-footer-title]'` | Título que se estira |
|
|
34
|
+
| `lambda` | `.9` | Suavizado del scroll |
|
|
35
|
+
|
|
36
|
+
## Controles
|
|
37
|
+
|
|
38
|
+
`destroy()`
|
|
39
|
+
|
|
40
|
+
## CSS
|
|
41
|
+
|
|
42
|
+
El contenido de la página debe quedar encima: `main { position: relative; z-index: 1; background: … }`.
|
|
43
|
+
|
|
44
|
+
Con `prefers-reduced-motion: reduce` la receta deja el contenido en su estado final, sin animar.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { registerMotion } from '../../core/gsap';
|
|
2
|
+
import { reduced } from '../../core/reduced';
|
|
3
|
+
import { whileVisible } from '../../core/loop';
|
|
4
|
+
import { damp, mapRange } from '../../core/math';
|
|
5
|
+
import { createScope } from '../../core/scope';
|
|
6
|
+
import { inert, warnOnce, type Instance } from '../../core/types';
|
|
7
|
+
|
|
8
|
+
/*
|
|
9
|
+
* spencergabor.com footer: fixed behind the page and revealed by a 100dvh
|
|
10
|
+
* "phantom" at the end of the flow. Smoothed scroll g = damp(g, scrollY, λ)
|
|
11
|
+
* drives the title's scaleY (0 → 1) and lifts the footer from "title on the
|
|
12
|
+
* bottom edge" to its resting place.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export interface StretchFooterOptions {
|
|
16
|
+
/** Spacer at the end of the flow; defaults to `[data-mk-phantom]` in the document. */
|
|
17
|
+
phantom?: HTMLElement | null;
|
|
18
|
+
/** Stretching title; defaults to `[data-mk-footer-title]` inside the footer. */
|
|
19
|
+
title?: HTMLElement | null;
|
|
20
|
+
/** Scroll smoothing (higher follows faster). */
|
|
21
|
+
lambda?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type StretchFooterInstance = Instance;
|
|
25
|
+
|
|
26
|
+
export function createStretchFooter(footer: HTMLElement | null, options: StretchFooterOptions = {}): StretchFooterInstance {
|
|
27
|
+
const phantom = options.phantom ?? document.querySelector<HTMLElement>('[data-mk-phantom]');
|
|
28
|
+
const title = options.title ?? footer?.querySelector<HTMLElement>('[data-mk-footer-title]') ?? null;
|
|
29
|
+
if (!footer || !phantom || !title) {
|
|
30
|
+
warnOnce('stretch-footer: needs the footer, a [data-mk-phantom] spacer and a [data-mk-footer-title]');
|
|
31
|
+
return inert();
|
|
32
|
+
}
|
|
33
|
+
registerMotion();
|
|
34
|
+
const scope = createScope();
|
|
35
|
+
footer.dataset.mkReady = '';
|
|
36
|
+
scope.add(() => {
|
|
37
|
+
delete footer.dataset.mkReady;
|
|
38
|
+
footer.style.removeProperty('--mk-footer-scale');
|
|
39
|
+
footer.style.removeProperty('--mk-footer-offset');
|
|
40
|
+
if (!footer.getAttribute('style')) footer.removeAttribute('style');
|
|
41
|
+
});
|
|
42
|
+
if (reduced()) return { destroy: () => scope.dispose() };
|
|
43
|
+
|
|
44
|
+
const lambda = options.lambda ?? 0.9;
|
|
45
|
+
let phantomTop = 0;
|
|
46
|
+
let titleBottom = 0;
|
|
47
|
+
let viewport = innerHeight;
|
|
48
|
+
let g = scrollY;
|
|
49
|
+
|
|
50
|
+
const measure = () => {
|
|
51
|
+
phantomTop = phantom.offsetTop;
|
|
52
|
+
titleBottom = title.offsetTop + title.offsetHeight;
|
|
53
|
+
viewport = innerHeight;
|
|
54
|
+
};
|
|
55
|
+
measure();
|
|
56
|
+
scope.observe(new ResizeObserver(measure)).observe(document.body);
|
|
57
|
+
scope.on(window, 'resize', measure);
|
|
58
|
+
|
|
59
|
+
const update = (dt: number) => {
|
|
60
|
+
g = damp(g, scrollY, lambda, dt);
|
|
61
|
+
const start = phantomTop - viewport;
|
|
62
|
+
footer.style.setProperty('--mk-footer-scale', mapRange(g, start, phantomTop, 0, 1, true).toFixed(4));
|
|
63
|
+
footer.style.setProperty('--mk-footer-offset', mapRange(g, start, phantomTop, viewport - titleBottom, 0, true).toFixed(2));
|
|
64
|
+
};
|
|
65
|
+
update(100); // settle on the current scroll position before the first frame
|
|
66
|
+
scope.add(whileVisible(phantom, update, '0px'));
|
|
67
|
+
|
|
68
|
+
return { destroy: () => scope.dispose() };
|
|
69
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* stretch-footer — spencergabor.com (Footer). The page content must sit above it:
|
|
3
|
+
* give <main> `position: relative; z-index: 1; background: <page colour>`.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
[data-mk-phantom] {
|
|
7
|
+
height: 100dvh;
|
|
8
|
+
pointer-events: none;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
[data-mk~="stretch-footer"],
|
|
12
|
+
.mk-stretch-footer {
|
|
13
|
+
position: fixed;
|
|
14
|
+
inset: auto 0 0;
|
|
15
|
+
z-index: 0;
|
|
16
|
+
height: 100dvh;
|
|
17
|
+
display: flex;
|
|
18
|
+
flex-direction: column;
|
|
19
|
+
justify-content: flex-end;
|
|
20
|
+
translate: 0 calc(var(--mk-footer-offset, 0) * 1px);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
[data-mk-footer-title] {
|
|
24
|
+
align-self: flex-start;
|
|
25
|
+
line-height: 0.8;
|
|
26
|
+
transform: scaleY(var(--mk-footer-scale, 1));
|
|
27
|
+
transform-origin: 50% 100%;
|
|
28
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# tabs
|
|
2
|
+
|
|
3
|
+
Un indicador se desliza hasta la pestaña elegida en 0.45 s con la curva signature. Si la lista hace scroll, centra la pestaña. Flechas (según orientación), Inicio y Fin cambian la selección.
|
|
4
|
+
|
|
5
|
+
Origen: Stepbro — PrettyTabs (`tabs`, `tabs-indicator` en el kit: `Flip.fit`, 0.45 s, `prettyTabsSlide`).
|
|
6
|
+
|
|
7
|
+
## Vanilla / Astro
|
|
8
|
+
|
|
9
|
+
```html
|
|
10
|
+
<div data-mk="tabs" class="mk-segmented">
|
|
11
|
+
<button data-mk-tab aria-selected="true">Día</button>
|
|
12
|
+
<button data-mk-tab>Semana</button>
|
|
13
|
+
<button data-mk-tab>Mes</button>
|
|
14
|
+
<span data-mk-indicator></span>
|
|
15
|
+
</div>
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Escucha el cambio con `mk:tab` (`event.detail.index`, `event.detail.tab`). Sin `autoInit`: `createTabs(el, opciones)`.
|
|
19
|
+
|
|
20
|
+
## React
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
24
|
+
useTabs(ref, { onSelect: (i) => setView(views[i]) });
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Opciones
|
|
28
|
+
|
|
29
|
+
| Opción | Por defecto | Qué hace |
|
|
30
|
+
|---|---|---|
|
|
31
|
+
| `orientation` | `'horizontal'` | `'vertical'` usa ↑ ↓ y pone `aria-orientation` |
|
|
32
|
+
| `duration` | `.45` | Segundos del deslizamiento |
|
|
33
|
+
| `onSelect` | — | `(index, tab) => void` |
|
|
34
|
+
|
|
35
|
+
## Controles
|
|
36
|
+
|
|
37
|
+
`select(index)`, `index`, `destroy()`
|
|
38
|
+
|
|
39
|
+
La receta pone `role="tablist"`, `role="tab"`, `aria-selected` y `tabindex` y los restaura al destruirse. Con movimiento reducido el indicador salta sin deslizarse.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { gsap, registerMotion } from '../../core/gsap';
|
|
2
|
+
import { createScope } from '../../core/scope';
|
|
3
|
+
import { inert, warnOnce, type Instance } from '../../core/types';
|
|
4
|
+
import { TIMING } from '../../core/timing';
|
|
5
|
+
import { slideIndicator } from '../../motion/indicator';
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
* Stepbro's PrettyTabs: an indicator slides onto the selected tab in 0.45 s on
|
|
9
|
+
* the signature curve; a scrolling tab list centres the selection. Arrow keys
|
|
10
|
+
* (by orientation), Home and End move the selection.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export interface TabsOptions {
|
|
14
|
+
orientation?: 'horizontal' | 'vertical';
|
|
15
|
+
duration?: number;
|
|
16
|
+
onSelect?: (index: number, tab: HTMLElement) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface TabsInstance extends Instance {
|
|
20
|
+
select(index: number): void;
|
|
21
|
+
readonly index: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function createTabs(root: HTMLElement | null, options: TabsOptions = {}): TabsInstance {
|
|
25
|
+
const tabs = root ? [...root.querySelectorAll<HTMLElement>('[data-mk-tab]')] : [];
|
|
26
|
+
if (!root || !tabs.length) {
|
|
27
|
+
warnOnce('tabs: needs at least one [data-mk-tab]');
|
|
28
|
+
return inert({ select() {}, index: 0 });
|
|
29
|
+
}
|
|
30
|
+
registerMotion();
|
|
31
|
+
const { orientation = 'horizontal', duration = TIMING.indicator } = options;
|
|
32
|
+
const indicator = root.querySelector<HTMLElement>('[data-mk-indicator]');
|
|
33
|
+
const scope = createScope();
|
|
34
|
+
const ctx = gsap.context(() => {});
|
|
35
|
+
let index = Math.max(0, tabs.findIndex((tab) => tab.getAttribute('aria-selected') === 'true'));
|
|
36
|
+
|
|
37
|
+
scope.attr(root, 'role', 'tablist');
|
|
38
|
+
if (orientation === 'vertical') scope.attr(root, 'aria-orientation', 'vertical');
|
|
39
|
+
|
|
40
|
+
const render = (immediate: boolean) => {
|
|
41
|
+
tabs.forEach((tab, i) => {
|
|
42
|
+
scope.attr(tab, 'role', 'tab');
|
|
43
|
+
scope.attr(tab, 'aria-selected', String(i === index));
|
|
44
|
+
scope.attr(tab, 'tabindex', i === index ? '0' : '-1');
|
|
45
|
+
});
|
|
46
|
+
if (indicator) ctx.add(() => slideIndicator(indicator, tabs[index], { immediate, duration, scroller: root, orientation }));
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const select = (next: number, focus = false) => {
|
|
50
|
+
const target = Math.min(tabs.length - 1, Math.max(0, next));
|
|
51
|
+
if (focus) tabs[target].focus();
|
|
52
|
+
if (target === index) return;
|
|
53
|
+
index = target;
|
|
54
|
+
render(false);
|
|
55
|
+
root.dispatchEvent(new CustomEvent('mk:tab', { detail: { index, tab: tabs[index] }, bubbles: true }));
|
|
56
|
+
options.onSelect?.(index, tabs[index]);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
scope.on(root, 'click', (event) => {
|
|
60
|
+
const tab = (event.target as Element | null)?.closest<HTMLElement>('[data-mk-tab]');
|
|
61
|
+
const at = tab ? tabs.indexOf(tab) : -1;
|
|
62
|
+
if (at >= 0) select(at);
|
|
63
|
+
});
|
|
64
|
+
scope.on(root, 'keydown', (event) => {
|
|
65
|
+
const key = (event as KeyboardEvent).key;
|
|
66
|
+
const back = orientation === 'vertical' ? 'ArrowUp' : 'ArrowLeft';
|
|
67
|
+
const forward = orientation === 'vertical' ? 'ArrowDown' : 'ArrowRight';
|
|
68
|
+
const moves: Record<string, number> = { [back]: index - 1, [forward]: index + 1, Home: 0, End: tabs.length - 1 };
|
|
69
|
+
if (!(key in moves)) return;
|
|
70
|
+
event.preventDefault();
|
|
71
|
+
select(moves[key], true);
|
|
72
|
+
});
|
|
73
|
+
if (typeof ResizeObserver === 'function') {
|
|
74
|
+
scope.observe(new ResizeObserver(() => render(true))).observe(root);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
render(true);
|
|
78
|
+
root.dataset.mkReady = '';
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
select: (next: number) => select(next),
|
|
82
|
+
get index() {
|
|
83
|
+
return index;
|
|
84
|
+
},
|
|
85
|
+
destroy() {
|
|
86
|
+
scope.dispose();
|
|
87
|
+
ctx.revert();
|
|
88
|
+
delete root.dataset.mkReady;
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/* tabs: the indicator sits under the tabs at 0,0 of the list; the recipe sizes and moves it. */
|
|
2
|
+
[data-mk~='tabs'] {
|
|
3
|
+
position: relative;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
[data-mk~='tabs'] [data-mk-tab] {
|
|
7
|
+
position: relative;
|
|
8
|
+
z-index: 1;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
[data-mk~='tabs'] [data-mk-indicator] {
|
|
12
|
+
position: absolute;
|
|
13
|
+
top: 0;
|
|
14
|
+
left: 0;
|
|
15
|
+
z-index: 0;
|
|
16
|
+
pointer-events: none;
|
|
17
|
+
border-radius: var(--mk-tabs-indicator-radius, var(--mk-radius-full));
|
|
18
|
+
background: var(--mk-tabs-indicator, var(--mk-accent-soft));
|
|
19
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# text-roll
|
|
2
|
+
|
|
3
|
+
Un título que gira como una tragaperras: cada letra es un carrete (la letra y una copia debajo) que da entre 9 y 15 vueltas al azar; todas frenan con `expo.out` en 2 s, 40 ms entre letras en orden aleatorio, y se quedan en la misma letra. Puede repetirse cada 6 s mientras se ve y no hay scroll (espera 1 s de calma).
|
|
4
|
+
|
|
5
|
+
Origen: Siena Film Foundation, títulos de película de la portada (copia `cloneText2`, `L.utils.random(8, 14)` repeticiones).
|
|
6
|
+
|
|
7
|
+
## HTML / Astro
|
|
8
|
+
|
|
9
|
+
```html
|
|
10
|
+
<h1 data-mk="text-roll">My Project X</h1>
|
|
11
|
+
<h2 data-mk="text-roll" data-mk-trigger="hover">Explorar</h2>
|
|
12
|
+
<h1 data-mk="text-roll" data-mk-interval="6000">Savoy</h1>
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## React
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
const ref = useRef<HTMLHeadingElement>(null);
|
|
19
|
+
const roll = useTextRoll(ref, { trigger: 'manual' });
|
|
20
|
+
<button onClick={() => roll.current?.play()}>Girar</button>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## API
|
|
24
|
+
|
|
25
|
+
`createTextRoll(el, { duration, stagger, turns, trigger, delay, interval })` → `{ play(), timelines, destroy }`.
|
|
26
|
+
|
|
27
|
+
| Opción | Por defecto | |
|
|
28
|
+
|---|---|---|
|
|
29
|
+
| `trigger` | `'load'` | `'scroll'`, `'hover'`, `'manual'` |
|
|
30
|
+
| `interval` | `0` | ms entre repeticiones (Siena: 6000) |
|
|
31
|
+
| `turns` | `[8, 14]` | repeticiones de la vuelta de cada letra |
|
|
32
|
+
|
|
33
|
+
## Accesibilidad
|
|
34
|
+
|
|
35
|
+
La raíz recibe `aria-label` y las letras partidas quedan ocultas a lectores. Con movimiento reducido el texto no se parte.
|