@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,50 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* curved-carousel — spencergabor.com (Carousel). Layout rules apply only while the
|
|
3
|
+
* recipe runs (`[data-mk-carousel]`); without JS the root keeps whatever fallback
|
|
4
|
+
* the page gives it (e.g. a horizontal scroll row).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
[data-mk-carousel] {
|
|
8
|
+
--mk-slide-w: min(30rem, 78vw);
|
|
9
|
+
position: relative;
|
|
10
|
+
display: block;
|
|
11
|
+
height: var(--mk-carousel-h, calc(var(--mk-slide-w) * 0.625 + 6.5rem));
|
|
12
|
+
overflow-x: clip;
|
|
13
|
+
overflow-y: visible;
|
|
14
|
+
column-gap: var(--mk-gap, 1rem);
|
|
15
|
+
touch-action: pan-y;
|
|
16
|
+
cursor: grab;
|
|
17
|
+
user-select: none;
|
|
18
|
+
-webkit-user-select: none;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
[data-mk-carousel]:active {
|
|
22
|
+
cursor: grabbing;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
[data-mk-carousel] [data-mk-slide] {
|
|
26
|
+
position: absolute;
|
|
27
|
+
top: 0;
|
|
28
|
+
left: 0;
|
|
29
|
+
width: var(--mk-slide-w);
|
|
30
|
+
transform-origin: 50% 70vw;
|
|
31
|
+
translate: calc(var(--mk-x, -9999) * 1px) calc(cos(var(--mk-t, 0) * 3.14 / 1000) * -3rem + 3rem);
|
|
32
|
+
rotate: calc(var(--mk-t, 0) * 0.03deg);
|
|
33
|
+
will-change: translate, rotate;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
[data-mk-carousel] .mk-carousel__media {
|
|
37
|
+
display: block;
|
|
38
|
+
transition:
|
|
39
|
+
scale 1.4s var(--mk-ease-elastic),
|
|
40
|
+
translate 1.4s var(--mk-ease-elastic);
|
|
41
|
+
transition-delay: calc(sin(var(--mk-k, 0) * 1.5708) * 0.12s);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
[data-mk-carousel] .mk-carousel__media img {
|
|
45
|
+
pointer-events: none;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
[data-mk-carousel] .mk-carousel__title {
|
|
49
|
+
opacity: max(0, cos(var(--mk-t, 0) * 3.14 / 600));
|
|
50
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# depth-grid
|
|
2
|
+
|
|
3
|
+
Un grid de imágenes que llega desde la profundidad: cada imagen parte empujada hacia fuera del centro (900 px), hundida en la pantalla (hasta −3000 px), girada (hasta 160°), casi transparente (.05) y pequeña (.4), y se coloca en su sitio mientras el grid baja desde que su borde superior toca el centro de la pantalla hasta que su borde inferior toca el borde inferior. Primero las del centro; 0.15 s repartidos hacia los bordes. Sigue al scroll sin retraso (`lerp: .1` para suavizarlo).
|
|
4
|
+
|
|
5
|
+
Origen: Siena Film Foundation, página About (`Rv`, `Un`). Siena busca el timeline en segundos y las del borde se quedan al 85 %; aquí se usa el progreso y el grid termina montado.
|
|
6
|
+
|
|
7
|
+
## HTML / Astro
|
|
8
|
+
|
|
9
|
+
```html
|
|
10
|
+
<div data-mk="depth-grid" class="grid">
|
|
11
|
+
<img src="1.jpg" alt=""> <img src="2.jpg" alt=""> …
|
|
12
|
+
</div>
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Dale espacio de scroll alrededor: el recorrido va de `top center` a `bottom bottom`.
|
|
16
|
+
|
|
17
|
+
## React
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
21
|
+
useDepthGrid(ref);
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## API
|
|
25
|
+
|
|
26
|
+
`createDepthGrid(root, { start, end, lerp, scroll })` → `{ seek(p), destroy }`.
|
|
27
|
+
|
|
28
|
+
## Accesibilidad
|
|
29
|
+
|
|
30
|
+
Con movimiento reducido el grid aparece montado. Las imágenes no cambian de orden en el DOM.
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { gsap, registerMotion } from '../../core/gsap';
|
|
2
|
+
import { entrances } from '../../core/reduced';
|
|
3
|
+
import { trackScroll, type ScrollTrack } from '../../core/scroll-track';
|
|
4
|
+
import { clearStyles } from '../../core/styles';
|
|
5
|
+
import { TIMING } from '../../core/timing';
|
|
6
|
+
import { inert, warnOnce, type Instance } from '../../core/types';
|
|
7
|
+
|
|
8
|
+
/*
|
|
9
|
+
* Siena's About grid: the pictures come from far away — pushed out from the
|
|
10
|
+
* grid's centre (900 px), deep into the screen (up to −3000 px), tilted (up to
|
|
11
|
+
* 160°), nearly transparent (.05) and small (.4) — and gather into their places
|
|
12
|
+
* as the grid scrolls from its top at the screen's centre to its bottom at the
|
|
13
|
+
* screen's bottom (eased .1 a frame). Centre first, 0.15 s spread to the edges.
|
|
14
|
+
* Siena seeks the timeline in seconds (edges stop at 85 %); this uses its
|
|
15
|
+
* progress, so the grid ends fully assembled.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export interface DepthGridOptions {
|
|
19
|
+
start?: string;
|
|
20
|
+
end?: string;
|
|
21
|
+
lerp?: number;
|
|
22
|
+
/** Follow the scroll (default). Off: only seek() moves it. */
|
|
23
|
+
scroll?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface DepthGridInstance extends Instance {
|
|
27
|
+
seek(progress: number): void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const STYLES = ['transform', 'translate', 'rotate', 'scale', 'opacity', 'visibility'];
|
|
31
|
+
|
|
32
|
+
/** Siena's Un(): where a child starts, from its centre relative to the grid's. */
|
|
33
|
+
function origin(child: Element, box: DOMRect) {
|
|
34
|
+
const t = TIMING.depthGrid;
|
|
35
|
+
const own = child.getBoundingClientRect();
|
|
36
|
+
const centre = { x: box.width / 2, y: box.height / 2 };
|
|
37
|
+
const point = { x: own.left - box.left + own.width / 2, y: own.top - box.top + own.height / 2 };
|
|
38
|
+
const angle = Math.atan2(Math.abs(centre.y - point.y), Math.abs(centre.x - point.x));
|
|
39
|
+
const cos = Math.abs(Math.cos(angle));
|
|
40
|
+
const sin = Math.abs(Math.sin(angle));
|
|
41
|
+
const reach = Math.hypot(centre.x, centre.y) || 1;
|
|
42
|
+
const h = Math.hypot(centre.x - point.x, centre.y - point.y) / reach;
|
|
43
|
+
const left = point.x < centre.x;
|
|
44
|
+
const above = point.y < centre.y;
|
|
45
|
+
return {
|
|
46
|
+
x: (left ? -1 : 1) * cos * t.distance,
|
|
47
|
+
y: (above ? -1 : 1) * sin * t.distance,
|
|
48
|
+
z: t.depth * h,
|
|
49
|
+
rotateX: (above ? -1 : 1) * sin * t.rotate * h,
|
|
50
|
+
rotateY: (left ? 1 : -1) * cos * t.rotate * h,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function createDepthGrid(root: HTMLElement | null, options: DepthGridOptions = {}): DepthGridInstance {
|
|
55
|
+
const items = root ? ([...root.children] as HTMLElement[]) : [];
|
|
56
|
+
if (!root || !items.length) {
|
|
57
|
+
warnOnce('depth-grid: needs a grid with children');
|
|
58
|
+
return inert({ seek() {} });
|
|
59
|
+
}
|
|
60
|
+
const t = TIMING.depthGrid;
|
|
61
|
+
const { start = 'top center', end = 'bottom bottom', lerp = t.lerp, scroll = true } = options;
|
|
62
|
+
if (!entrances()) {
|
|
63
|
+
root.dataset.mkReady = '';
|
|
64
|
+
return {
|
|
65
|
+
seek() {},
|
|
66
|
+
destroy() {
|
|
67
|
+
delete root.dataset.mkReady;
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
registerMotion();
|
|
72
|
+
const perspective = root.style.perspective;
|
|
73
|
+
root.style.perspective = `${t.perspective}px`;
|
|
74
|
+
const box = root.getBoundingClientRect();
|
|
75
|
+
const starts = items.map((item) => origin(item, box));
|
|
76
|
+
const timeline = gsap.timeline({ paused: true, defaults: { ease: 'none' } });
|
|
77
|
+
timeline.fromTo(
|
|
78
|
+
items,
|
|
79
|
+
{
|
|
80
|
+
x: (i: number) => starts[i]!.x,
|
|
81
|
+
y: (i: number) => starts[i]!.y,
|
|
82
|
+
z: (i: number) => starts[i]!.z,
|
|
83
|
+
rotateX: (i: number) => starts[i]!.rotateX,
|
|
84
|
+
rotateY: (i: number) => starts[i]!.rotateY,
|
|
85
|
+
autoAlpha: t.opacity,
|
|
86
|
+
scale: t.scale,
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
x: 0,
|
|
90
|
+
y: 0,
|
|
91
|
+
z: 0,
|
|
92
|
+
rotateX: 0,
|
|
93
|
+
rotateY: 0,
|
|
94
|
+
autoAlpha: 1,
|
|
95
|
+
scale: 1,
|
|
96
|
+
duration: 1,
|
|
97
|
+
stagger: { amount: t.stagger, from: 'center', grid: 'auto' },
|
|
98
|
+
},
|
|
99
|
+
);
|
|
100
|
+
const render = (progress: number) => timeline.progress(progress);
|
|
101
|
+
let track: ScrollTrack | null = null;
|
|
102
|
+
if (scroll) track = trackScroll(root, { start, end, lerp, onUpdate: render });
|
|
103
|
+
else render(0);
|
|
104
|
+
root.dataset.mkReady = '';
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
seek(progress: number) {
|
|
108
|
+
if (track) track.set(progress);
|
|
109
|
+
else render(progress);
|
|
110
|
+
},
|
|
111
|
+
destroy() {
|
|
112
|
+
track?.destroy();
|
|
113
|
+
timeline.kill();
|
|
114
|
+
clearStyles(items, STYLES);
|
|
115
|
+
root.style.perspective = perspective;
|
|
116
|
+
if (root.getAttribute('style') === '') root.removeAttribute('style');
|
|
117
|
+
delete root.dataset.mkReady;
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# details
|
|
2
|
+
|
|
3
|
+
Los `<details>` crecen hasta la altura de su contenido mientras este aparece, y se pliegan de vuelta (0.32 s, curva signature). Un `[data-mk-chevron]` dentro del `<summary>` gira 180° en 0.25 s.
|
|
4
|
+
|
|
5
|
+
Origen: Stepbro — PrettyDetails (`details` en el kit).
|
|
6
|
+
|
|
7
|
+
## Vanilla / Astro
|
|
8
|
+
|
|
9
|
+
```html
|
|
10
|
+
<section data-mk="details">
|
|
11
|
+
<details>
|
|
12
|
+
<summary>¿Qué incluye? <span data-mk-chevron class="mk-icon">expand_more</span></summary>
|
|
13
|
+
<p>…</p>
|
|
14
|
+
</details>
|
|
15
|
+
</section>
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Un solo listener en el contenedor anima todos sus `<details>`. Los controles dentro del `<summary>` conservan su clic. `data-mk-motion="none"` en un `<details>` lo excluye. Sin `autoInit`: `createDetails(el)`; para todo el documento, `installDetailsMotion()`.
|
|
19
|
+
|
|
20
|
+
## React
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
const ref = useRef<HTMLElement>(null);
|
|
24
|
+
useDetails(ref);
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Controles
|
|
28
|
+
|
|
29
|
+
`destroy()`
|
|
30
|
+
|
|
31
|
+
Con movimiento reducido se abren y cierran al instante.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { inert, warnOnce, type Instance } from '../../core/types';
|
|
2
|
+
import { installDetailsMotion } from '../../motion/details';
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Stepbro's PrettyDetails for every <details> under `root` (or `root` itself):
|
|
6
|
+
* height and content fade in 0.32 s on the signature curve, chevron 180° in 0.25 s.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type DetailsInstance = Instance;
|
|
10
|
+
|
|
11
|
+
export function createDetails(root: HTMLElement | null): DetailsInstance {
|
|
12
|
+
if (!root) {
|
|
13
|
+
warnOnce('details: missing root');
|
|
14
|
+
return inert();
|
|
15
|
+
}
|
|
16
|
+
const uninstall = installDetailsMotion(root);
|
|
17
|
+
root.dataset.mkReady = '';
|
|
18
|
+
let destroyed = false;
|
|
19
|
+
return {
|
|
20
|
+
destroy() {
|
|
21
|
+
if (destroyed) return;
|
|
22
|
+
destroyed = true;
|
|
23
|
+
uninstall();
|
|
24
|
+
delete root.dataset.mkReady;
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# dock-pill
|
|
2
|
+
|
|
3
|
+
Indicador spring que sigue la opción activa de un dock (Dousan).
|
|
4
|
+
|
|
5
|
+
```html
|
|
6
|
+
<nav class="mk-dock-pill" data-mk="dock-pill" aria-label="Secciones">
|
|
7
|
+
<button data-mk-dock-item>Inicio</button><button data-mk-dock-item>Archivo</button>
|
|
8
|
+
</nav>
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { createDockPill } from '@yewdiel21/motion-kit';
|
|
13
|
+
import { useDockPill } from '@yewdiel21/motion-kit/react';
|
|
14
|
+
import '@yewdiel21/motion-kit/css';
|
|
15
|
+
const instance = createDockPill(root);
|
|
16
|
+
instance.destroy();
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`index` (o aria-current="page" existente), `onSelect(index,item)`. `select(index)`, `index`; evento `mk:select`. Medición de la opción activa con ResizeObserver, indicador spring 380/30. Flechas, Home/End; evita opciones deshabilitadas. Conserva los enlaces y la navegación del consumidor. Origen: `activeTabPill` del dock móvil de Dousan.
|
|
20
|
+
|
|
21
|
+
Vanilla declarativo: `autoInit(root)`. React: `useDockPill(ref, options, deps)` devuelve una ref a la instancia; se destruye al desmontar. Con movimiento reducido no se ejecuta movimiento decorativo. `destroy()` restaura atributos y estilos y quita listeners, observers y nodos generados.
|
|
22
|
+
|
|
23
|
+
Desde 0.8.0. Parámetros investigados en `REA - Dousan OFFFICE Siena/`; implementación propia, sin código ni assets de esos sitios.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { gsap, registerMotion } from '../../core/gsap';
|
|
2
|
+
import { motionSpring } from '../../core/physics-spring';
|
|
3
|
+
import { reduced } from '../../core/reduced';
|
|
4
|
+
import { wrap } from '../../core/math';
|
|
5
|
+
import { createScope } from '../../core/scope';
|
|
6
|
+
import { TIMING } from '../../core/timing';
|
|
7
|
+
import { inert, type Instance } from '../../core/types';
|
|
8
|
+
|
|
9
|
+
export interface DockPillOptions { index?: number; onSelect?: (index: number, item: HTMLElement) => void; }
|
|
10
|
+
export interface DockPillInstance extends Instance { select(index: number): void; readonly index: number; }
|
|
11
|
+
|
|
12
|
+
export function createDockPill(root: HTMLElement | null, options: DockPillOptions = {}): DockPillInstance {
|
|
13
|
+
const items = root ? [...root.querySelectorAll<HTMLElement>('[data-mk-dock-item]')] : [];
|
|
14
|
+
if (!root || !items.length) return inert({ select() {}, index: 0 });
|
|
15
|
+
registerMotion();
|
|
16
|
+
const scope = createScope();
|
|
17
|
+
const indicator = document.createElement('span');
|
|
18
|
+
indicator.className = 'mk-dock-pill__indicator';
|
|
19
|
+
indicator.setAttribute('aria-hidden', 'true');
|
|
20
|
+
root.prepend(indicator);
|
|
21
|
+
scope.add(() => indicator.remove());
|
|
22
|
+
let index = Math.max(0, items.findIndex((item) => item.getAttribute('aria-current') === 'page'));
|
|
23
|
+
let animation: gsap.core.Tween | undefined;
|
|
24
|
+
const measure = (animate = true) => {
|
|
25
|
+
if (scope.disposed) return;
|
|
26
|
+
const item = items[index]!;
|
|
27
|
+
const target = { x: item.offsetLeft, y: item.offsetTop, width: item.offsetWidth, height: item.offsetHeight };
|
|
28
|
+
animation?.kill();
|
|
29
|
+
if (animate && !reduced()) animation = gsap.to(indicator, { ...target, ...motionSpring(...TIMING.springs.dockPill), overwrite: true });
|
|
30
|
+
else gsap.set(indicator, target);
|
|
31
|
+
};
|
|
32
|
+
const select = (next: number, notify = true) => {
|
|
33
|
+
if (scope.disposed || !Number.isFinite(next)) return;
|
|
34
|
+
const candidate = wrap(Math.round(next), items.length);
|
|
35
|
+
if (items[candidate]!.matches(':disabled, [aria-disabled="true"]')) return;
|
|
36
|
+
index = candidate;
|
|
37
|
+
items.forEach((item, position) => scope.attr(item, 'aria-current', position === index ? 'page' : null));
|
|
38
|
+
measure(notify);
|
|
39
|
+
if (notify) {
|
|
40
|
+
options.onSelect?.(index, items[index]!);
|
|
41
|
+
root.dispatchEvent(new CustomEvent('mk:select', { bubbles: true, detail: { index, item: items[index] } }));
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
scope.on(root, 'click', (event) => {
|
|
45
|
+
const item = (event.target as Element).closest<HTMLElement>('[data-mk-dock-item]');
|
|
46
|
+
if (item && items.includes(item)) select(items.indexOf(item));
|
|
47
|
+
});
|
|
48
|
+
scope.on(root, 'keydown', (event) => {
|
|
49
|
+
const current = items.indexOf(document.activeElement as HTMLElement);
|
|
50
|
+
if (current < 0) return;
|
|
51
|
+
const direction = event.key === 'ArrowRight' ? 1 : event.key === 'ArrowLeft' ? -1 : 0;
|
|
52
|
+
if (!direction && event.key !== 'Home' && event.key !== 'End') return;
|
|
53
|
+
event.preventDefault();
|
|
54
|
+
let next = event.key === 'Home' ? 0 : event.key === 'End' ? items.length - 1 : wrap(current + direction, items.length);
|
|
55
|
+
for (let tries = 0; tries < items.length && items[next]!.matches(':disabled, [aria-disabled="true"]'); tries++) next = wrap(next + (direction || 1), items.length);
|
|
56
|
+
select(next);
|
|
57
|
+
items[index]!.focus();
|
|
58
|
+
});
|
|
59
|
+
if (typeof ResizeObserver === 'function') {
|
|
60
|
+
const observer = scope.observe(new ResizeObserver(() => measure(false)));
|
|
61
|
+
observer.observe(root);
|
|
62
|
+
items.forEach((item) => observer.observe(item));
|
|
63
|
+
}
|
|
64
|
+
scope.on(window, 'resize', () => measure(false));
|
|
65
|
+
scope.add(() => animation?.kill());
|
|
66
|
+
select(options.index ?? index, false);
|
|
67
|
+
return { select, get index() { return index; }, destroy: () => scope.dispose() };
|
|
68
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
.mk-dock-pill { position: relative; isolation: isolate; display: inline-flex; gap: 4px; padding: 8px; border-radius: 64px; background: var(--mk-surface-2); }
|
|
2
|
+
.mk-dock-pill [data-mk-dock-item] { position: relative; display: grid; place-items: center; min-width: 48px; min-height: 48px; padding: 10px 16px; border: 0; border-radius: 48px; background: transparent; color: inherit; font: inherit; text-decoration: none; cursor: pointer; }
|
|
3
|
+
.mk-dock-pill [aria-current='page'] { color: var(--mk-on-accent); }
|
|
4
|
+
.mk-dock-pill [data-mk-dock-item]:focus-visible { outline: 2px solid var(--mk-accent); outline-offset: 2px; }
|
|
5
|
+
.mk-dock-pill__indicator { position: absolute; top: 0; left: 0; z-index: -1; border-radius: 48px; background: var(--mk-accent); pointer-events: none; }
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# drag-ruler
|
|
2
|
+
|
|
3
|
+
Regla numérica arrastrable de 60 px por unidad, paso y teclado (Dousan).
|
|
4
|
+
|
|
5
|
+
```html
|
|
6
|
+
<div class="mk-drag-ruler" data-mk="drag-ruler" aria-label="Años de experiencia" data-mk-value="8"></div>
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { createDragRuler } from '@yewdiel21/motion-kit';
|
|
11
|
+
import { useDragRuler } from '@yewdiel21/motion-kit/react';
|
|
12
|
+
import '@yewdiel21/motion-kit/css';
|
|
13
|
+
const instance = createDragRuler(root);
|
|
14
|
+
instance.destroy();
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`min` 0, `max` 8, `step` .1, `value` max, `pixelsPerUnit` 60, `onChange(value)`. `set(value)`, `value`; evento `mk:change` con detail.value. Role slider, aria-valuemin/max/now y nombre accesible provisto por el consumidor. Flechas, Home/End y PageUp/PageDown. Drag usa pointer capture, respeta pointercancel. Origen: regla de años de Dousan; marcas generadas según el rango, sin su formulario.
|
|
18
|
+
|
|
19
|
+
Vanilla declarativo: `autoInit(root)`. React: `useDragRuler(ref, options, deps)` devuelve una ref a la instancia; se destruye al desmontar. Con movimiento reducido no se ejecuta movimiento decorativo. `destroy()` restaura atributos y estilos y quita listeners, observers y nodos generados.
|
|
20
|
+
|
|
21
|
+
Desde 0.8.0. Parámetros investigados en `REA - Dousan OFFFICE Siena/`; implementación propia, sin código ni assets de esos sitios.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { clamp } from '../../core/math';
|
|
2
|
+
import { createScope } from '../../core/scope';
|
|
3
|
+
import { TIMING } from '../../core/timing';
|
|
4
|
+
import { inert, type Instance } from '../../core/types';
|
|
5
|
+
|
|
6
|
+
export interface DragRulerOptions {
|
|
7
|
+
min?: number;
|
|
8
|
+
max?: number;
|
|
9
|
+
step?: number;
|
|
10
|
+
value?: number;
|
|
11
|
+
pixelsPerUnit?: number;
|
|
12
|
+
onChange?: (value: number) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface DragRulerInstance extends Instance {
|
|
16
|
+
set(value: number): void;
|
|
17
|
+
readonly value: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function createDragRuler(root: HTMLElement | null, options: DragRulerOptions = {}): DragRulerInstance {
|
|
21
|
+
if (!root) return inert({ set() {}, value: 0 });
|
|
22
|
+
const scope = createScope();
|
|
23
|
+
const defaults = TIMING.dragRuler;
|
|
24
|
+
const min = Number.isFinite(options.min) ? options.min! : defaults.min;
|
|
25
|
+
const max = Math.max(min, Number.isFinite(options.max) ? options.max! : defaults.max);
|
|
26
|
+
const step = Number.isFinite(options.step) && options.step! > 0 ? options.step! : defaults.step;
|
|
27
|
+
const pixels = Number.isFinite(options.pixelsPerUnit) && options.pixelsPerUnit! > 0 ? options.pixelsPerUnit! : defaults.pixelsPerUnit;
|
|
28
|
+
let value = min;
|
|
29
|
+
let drag: { x: number; value: number; id: number } | null = null;
|
|
30
|
+
const track = document.createElement('span');
|
|
31
|
+
track.className = 'mk-drag-ruler__track';
|
|
32
|
+
track.setAttribute('aria-hidden', 'true');
|
|
33
|
+
const count = Math.min(500, Math.ceil((max - min) / step));
|
|
34
|
+
for (let index = 0; index <= count; index++) {
|
|
35
|
+
const tick = document.createElement('span');
|
|
36
|
+
const tickValue = min + index * ((max - min) / Math.max(1, count));
|
|
37
|
+
tick.className = 'mk-drag-ruler__tick';
|
|
38
|
+
tick.style.left = `${(tickValue - min) * pixels}px`;
|
|
39
|
+
if (index % 10 === 0) tick.textContent = String(Number(tickValue.toFixed(2)));
|
|
40
|
+
track.append(tick);
|
|
41
|
+
}
|
|
42
|
+
root.append(track);
|
|
43
|
+
scope.add(() => track.remove());
|
|
44
|
+
scope.attr(root, 'role', 'slider');
|
|
45
|
+
scope.attr(root, 'tabindex', root.getAttribute('tabindex') ?? '0');
|
|
46
|
+
scope.attr(root, 'aria-orientation', 'horizontal');
|
|
47
|
+
scope.attr(root, 'aria-valuemin', String(min));
|
|
48
|
+
scope.attr(root, 'aria-valuemax', String(max));
|
|
49
|
+
const set = (next: number, notify = true) => {
|
|
50
|
+
if (scope.disposed || !Number.isFinite(next)) return;
|
|
51
|
+
const snapped = clamp(Number((min + Math.round((next - min) / step) * step).toFixed(8)), min, max);
|
|
52
|
+
const changed = snapped !== value;
|
|
53
|
+
value = snapped;
|
|
54
|
+
scope.attr(root, 'aria-valuenow', String(value));
|
|
55
|
+
track.style.transform = `translateX(${-(value - min) * pixels - 3}px)`;
|
|
56
|
+
if (changed && notify) {
|
|
57
|
+
options.onChange?.(value);
|
|
58
|
+
root.dispatchEvent(new CustomEvent('mk:change', { bubbles: true, detail: { value } }));
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
scope.on(root, 'keydown', (event) => {
|
|
62
|
+
const keys: Record<string, number> = {
|
|
63
|
+
ArrowLeft: value - step, ArrowDown: value - step, ArrowRight: value + step, ArrowUp: value + step,
|
|
64
|
+
PageDown: value - step * 10, PageUp: value + step * 10, Home: min, End: max,
|
|
65
|
+
};
|
|
66
|
+
if (!(event.key in keys)) return;
|
|
67
|
+
event.preventDefault();
|
|
68
|
+
set(keys[event.key]!);
|
|
69
|
+
});
|
|
70
|
+
scope.on(root, 'pointerdown', (event) => {
|
|
71
|
+
if (event.button !== 0 || drag) return;
|
|
72
|
+
drag = { x: event.clientX, value, id: event.pointerId };
|
|
73
|
+
root.setPointerCapture?.(event.pointerId);
|
|
74
|
+
scope.attr(root, 'data-mk-dragging', '');
|
|
75
|
+
root.focus({ preventScroll: true });
|
|
76
|
+
});
|
|
77
|
+
scope.on(window, 'pointermove', (event) => {
|
|
78
|
+
if (!drag || event.pointerId !== drag.id) return;
|
|
79
|
+
set(drag.value - (event.clientX - drag.x) / pixels);
|
|
80
|
+
});
|
|
81
|
+
const release = (event: PointerEvent) => {
|
|
82
|
+
if (!drag || event.pointerId !== drag.id) return;
|
|
83
|
+
if (root.hasPointerCapture?.(drag.id)) root.releasePointerCapture(drag.id);
|
|
84
|
+
drag = null;
|
|
85
|
+
scope.attr(root, 'data-mk-dragging', null);
|
|
86
|
+
};
|
|
87
|
+
scope.on(window, 'pointerup', release);
|
|
88
|
+
scope.on(window, 'pointercancel', release);
|
|
89
|
+
scope.on(root, 'lostpointercapture', release);
|
|
90
|
+
scope.add(() => {
|
|
91
|
+
if (drag && root.hasPointerCapture?.(drag.id)) root.releasePointerCapture(drag.id);
|
|
92
|
+
});
|
|
93
|
+
set(options.value ?? max, false);
|
|
94
|
+
return { set, get value() { return value; }, destroy: () => scope.dispose() };
|
|
95
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
.mk-drag-ruler {
|
|
2
|
+
position: relative;
|
|
3
|
+
height: 100px;
|
|
4
|
+
overflow: hidden;
|
|
5
|
+
cursor: ew-resize;
|
|
6
|
+
touch-action: pan-y;
|
|
7
|
+
user-select: none;
|
|
8
|
+
border-block: 1px solid var(--mk-outline-soft);
|
|
9
|
+
}
|
|
10
|
+
.mk-drag-ruler:focus-visible { outline: 2px solid var(--mk-accent); outline-offset: 4px; }
|
|
11
|
+
.mk-drag-ruler::after { content: ''; position: absolute; left: 50%; top: 8px; width: 3px; height: 58px; background: var(--mk-accent); }
|
|
12
|
+
.mk-drag-ruler__track { position: absolute; left: 50%; top: 20px; transition: transform .15s ease-out; }
|
|
13
|
+
.mk-drag-ruler[data-mk-dragging] .mk-drag-ruler__track { transition: none; }
|
|
14
|
+
.mk-drag-ruler__tick { position: absolute; border-left: 1px solid var(--mk-muted); height: 16px; padding-top: 28px; font: 11px monospace; }
|
|
15
|
+
.mk-drag-ruler__tick:not(:empty) { height: 26px; }
|
|
16
|
+
@media (prefers-reduced-motion: reduce) { .mk-drag-ruler__track { transition: none; } }
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# fill-text
|
|
2
|
+
|
|
3
|
+
Un texto cuyas letras empiezan al 20 % y se rellenan una tras otra ligadas al scroll: desde que el borde superior del texto llega al 90 % de la pantalla hasta que su centro llega al 40 % (cada letra 0.5 s de una línea de tiempo lineal, 0.1 s entre letras).
|
|
4
|
+
|
|
5
|
+
Origen: iyo.ai, módulo `fill-text` (`GM`): SplitText `words, chars`, `autoAlpha .2 → 1`, `stagger .1`, `ease: linear`, ScrollTrigger con `scrub` de `top 90%` a `center 40%`. Aquí sigue al scroll con `trackScroll` y lee el texto completo a los lectores de pantalla (iyo usaba `aria: none`).
|
|
6
|
+
|
|
7
|
+
## HTML / Astro
|
|
8
|
+
|
|
9
|
+
```html
|
|
10
|
+
<p data-mk="fill-text" class="statement">Diseñamos objetos que escuchan.</p>
|
|
11
|
+
<h2 data-mk="fill-text" data-mk-start="top 80%" data-mk-end="center center">…</h2>
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## React
|
|
15
|
+
|
|
16
|
+
```tsx
|
|
17
|
+
const ref = useRef<HTMLParagraphElement>(null);
|
|
18
|
+
useFillText(ref);
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## API
|
|
22
|
+
|
|
23
|
+
`createFillText(el, { from, start, end, scroll })` → `{ seek(p), progress, destroy }`. Clases: `.mk-fill-word`, `.mk-fill-char`.
|
|
24
|
+
|
|
25
|
+
## Accesibilidad
|
|
26
|
+
|
|
27
|
+
El texto entero queda en `aria-label` y las letras ocultas a los lectores. No es una entrada (no esconde el texto, lo atenúa), así que solo lo detiene el movimiento reducido: el texto queda entero y sin partir.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { gsap, SplitText, registerMotion } from '../../core/gsap';
|
|
2
|
+
import { reduced } from '../../core/reduced';
|
|
3
|
+
import { trackScroll, type ScrollTrack } from '../../core/scroll-track';
|
|
4
|
+
import { TIMING } from '../../core/timing';
|
|
5
|
+
import { inert, warnOnce, type Instance } from '../../core/types';
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
* iyo.ai's fill-text: the letters start at 20 % and fill in one after another,
|
|
9
|
+
* tied to the scroll, from when the text's top reaches 90 % of the screen until
|
|
10
|
+
* its centre reaches 40 % (each letter 0.5 s of a linear timeline, 0.1 s apart).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export interface FillTextOptions {
|
|
14
|
+
/** Opacity the letters start at. */
|
|
15
|
+
from?: number;
|
|
16
|
+
/** ScrollTrigger-style positions, as in trackScroll. */
|
|
17
|
+
start?: string;
|
|
18
|
+
end?: string;
|
|
19
|
+
/** Follow the scroll (default). Off: only seek() moves it. */
|
|
20
|
+
scroll?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface FillTextInstance extends Instance {
|
|
24
|
+
/** Show the state at progress 0…1. */
|
|
25
|
+
seek(progress: number): void;
|
|
26
|
+
readonly progress: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function createFillText(el: HTMLElement | null, options: FillTextOptions = {}): FillTextInstance {
|
|
30
|
+
if (!el) {
|
|
31
|
+
warnOnce('fill-text: missing element');
|
|
32
|
+
return inert({ seek() {}, progress: 1 });
|
|
33
|
+
}
|
|
34
|
+
const t = TIMING.fillText;
|
|
35
|
+
const { from = t.from, start = t.start, end = t.end, scroll = true } = options;
|
|
36
|
+
registerMotion();
|
|
37
|
+
// Not an entrance: the text stays readable, so only reduced motion stills it.
|
|
38
|
+
if (reduced()) {
|
|
39
|
+
el.dataset.mkReady = '';
|
|
40
|
+
return {
|
|
41
|
+
seek() {},
|
|
42
|
+
progress: 1,
|
|
43
|
+
destroy() {
|
|
44
|
+
delete el.dataset.mkReady;
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let timeline: gsap.core.Timeline | null = null;
|
|
50
|
+
let progress = 0;
|
|
51
|
+
const build = (chars: Element[]) => {
|
|
52
|
+
timeline?.kill();
|
|
53
|
+
timeline = gsap
|
|
54
|
+
.timeline({ paused: true })
|
|
55
|
+
.fromTo(chars, { opacity: from }, { opacity: 1, ease: 'none', duration: t.duration, stagger: t.stagger });
|
|
56
|
+
timeline.progress(progress);
|
|
57
|
+
};
|
|
58
|
+
const split = SplitText.create(el, {
|
|
59
|
+
type: 'words,chars',
|
|
60
|
+
aria: 'auto',
|
|
61
|
+
autoSplit: true,
|
|
62
|
+
wordsClass: 'mk-fill-word',
|
|
63
|
+
charsClass: 'mk-fill-char',
|
|
64
|
+
// A new split (resize, fonts) keeps the place it was at.
|
|
65
|
+
onSplit: (self: SplitText) => build(self.chars),
|
|
66
|
+
});
|
|
67
|
+
if (!timeline) build(split.chars);
|
|
68
|
+
|
|
69
|
+
const render = (value: number) => {
|
|
70
|
+
progress = value;
|
|
71
|
+
timeline?.progress(value);
|
|
72
|
+
};
|
|
73
|
+
let track: ScrollTrack | null = scroll ? trackScroll(el, { start, end, onUpdate: render }) : null;
|
|
74
|
+
if (!track) render(0);
|
|
75
|
+
el.dataset.mkReady = '';
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
seek(value: number) {
|
|
79
|
+
if (track) track.set(value);
|
|
80
|
+
else render(Math.min(1, Math.max(0, value)));
|
|
81
|
+
},
|
|
82
|
+
get progress() {
|
|
83
|
+
return progress;
|
|
84
|
+
},
|
|
85
|
+
destroy() {
|
|
86
|
+
track?.destroy();
|
|
87
|
+
track = null;
|
|
88
|
+
timeline?.kill();
|
|
89
|
+
split.revert();
|
|
90
|
+
delete el.dataset.mkReady;
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# fit-text
|
|
2
|
+
|
|
3
|
+
Ajusta el tamaño de una línea de texto para que ocupe exactamente el ancho de contenido de su padre; se reajusta al cambiar el ancho.
|
|
4
|
+
|
|
5
|
+
Origen: portfolio — "Portfolio" y "Contacto" a ancho completo.
|
|
6
|
+
|
|
7
|
+
## Vanilla / Astro
|
|
8
|
+
|
|
9
|
+
```html
|
|
10
|
+
<h1><span data-mk="fit-text">Portfolio</span></h1>
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Con `autoInit()` (de `@yewdiel21/motion-kit/auto`) las opciones van como `data-mk-<opción>` en kebab-case. Sin `autoInit`: `createFitText(el, opciones)`.
|
|
14
|
+
|
|
15
|
+
## React
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
const ref = useRef<HTMLSpanElement>(null);
|
|
19
|
+
const fit = useFitText(ref);
|
|
20
|
+
// tras cambiar el texto: fit.current?.refit()
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Opciones
|
|
24
|
+
|
|
25
|
+
| Opción | Por defecto | Qué hace |
|
|
26
|
+
|---|---|---|
|
|
27
|
+
| `safety` | `.995` | Fracción del ancho disponible (margen para el ojo de las letras) |
|
|
28
|
+
|
|
29
|
+
## Controles
|
|
30
|
+
|
|
31
|
+
`refit()`, `destroy()`
|
|
32
|
+
|
|
33
|
+
## CSS
|
|
34
|
+
|
|
35
|
+
El elemento es `inline-block` y `nowrap`. Con `autoInit`, espera a las fuentes web antes de medir.
|
|
36
|
+
|
|
37
|
+
Con `prefers-reduced-motion: reduce` la receta deja el contenido en su estado final, sin animar.
|