@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,44 @@
|
|
|
1
|
+
# slot-text
|
|
2
|
+
|
|
3
|
+
Cuando el texto cambia, cada letra distinta sale rodando y entra la nueva (300 ms, curva con rebote `cubic-bezier(.34,1.56,.64,1)`), con una inclinación determinista de hasta 5° y 45 ms entre letras; el ancho de cada letra se ajusta. Las letras se agrupan por palabra para que el texto parta línea entre palabras.
|
|
4
|
+
|
|
5
|
+
Origen: Stepbro — SlotText (`slot-text` en el kit), portado desde Kayro.
|
|
6
|
+
|
|
7
|
+
## Vanilla / Astro
|
|
8
|
+
|
|
9
|
+
```html
|
|
10
|
+
<h1><span data-mk="slot-text" id="saludo">Hola</span></h1>
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
const slot = createSlotText(document.getElementById('saludo'));
|
|
15
|
+
slot.set('Hello');
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Los lectores de pantalla leen una copia en texto plano (`.mk-sr-only`); las letras animadas van con `aria-hidden`. Importa la CSS del paquete (o `slot-text.css`).
|
|
19
|
+
|
|
20
|
+
## React
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
<SlotText text={idioma === 'es' ? 'Hola' : 'Hello'} />
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Con el hook, el elemento no debe tener hijos propios: `useSlotText(ref, texto)` sobre `<span ref={ref} />`.
|
|
27
|
+
|
|
28
|
+
## Opciones
|
|
29
|
+
|
|
30
|
+
| Opción | Por defecto | Qué hace |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| `direction` | `'down'` | Hacia dónde ruedan las letras |
|
|
33
|
+
| `duration` | `300` | ms por letra |
|
|
34
|
+
| `stagger` | `45` | ms entre letras |
|
|
35
|
+
| `exitOffset` | `50` | ms entre la salida de la letra vieja y la entrada de la nueva |
|
|
36
|
+
| `colorFade` | `280` | ms del fundido de cada letra |
|
|
37
|
+
| `maxTilt` | `5` | Inclinación máxima, grados |
|
|
38
|
+
| `text` | texto del elemento | Texto inicial (para frameworks) |
|
|
39
|
+
|
|
40
|
+
## Controles
|
|
41
|
+
|
|
42
|
+
`set(texto)`, `text`, `destroy()` (deja el texto actual como texto plano)
|
|
43
|
+
|
|
44
|
+
Con movimiento reducido escribe el texto nuevo sin animar.
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { reduced } from '../../core/reduced';
|
|
2
|
+
import { inert, warnOnce, type Instance } from '../../core/types';
|
|
3
|
+
import { TIMING } from '../../core/timing';
|
|
4
|
+
import { splitGraphemes } from '../../core/split';
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
* Stepbro's SlotText (ported from Kayro): when the text changes, each letter
|
|
8
|
+
* that differs rolls out and the new one rolls in (300 ms, a springy curve),
|
|
9
|
+
* with a slight deterministic tilt (≤ 5°) and a 45 ms cascade; letter boxes ease
|
|
10
|
+
* to their new width. Letters are grouped by word so lines only break between
|
|
11
|
+
* words. Screen readers get a plain copy; the rolling letters are aria-hidden.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export interface SlotTextOptions {
|
|
15
|
+
direction?: 'down' | 'up';
|
|
16
|
+
/** ms per letter */
|
|
17
|
+
duration?: number;
|
|
18
|
+
/** ms between letters */
|
|
19
|
+
stagger?: number;
|
|
20
|
+
/** ms between the old letter leaving and the new one entering */
|
|
21
|
+
exitOffset?: number;
|
|
22
|
+
/** ms the letters take to fade */
|
|
23
|
+
colorFade?: number;
|
|
24
|
+
maxTilt?: number;
|
|
25
|
+
/** Initial text. Default: the element's text. Pass it when a framework owns the element (it must render no children). */
|
|
26
|
+
text?: string;
|
|
27
|
+
/** Add a visually hidden plain copy for screen readers (default). Turn off when you render your own. */
|
|
28
|
+
accessibleCopy?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface SlotTextInstance extends Instance {
|
|
32
|
+
set(text: string): void;
|
|
33
|
+
readonly text: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const EASING = 'cubic-bezier(0.34, 1.56, 0.64, 1)';
|
|
37
|
+
const RESIZE_EASING = 'cubic-bezier(0.2, 0, 0, 1)';
|
|
38
|
+
const BOUNCE = 0.6;
|
|
39
|
+
const VARIATION = { index: 12.9898, channel: 78.233, seed: 43758.5453, duration: 1, stagger: 2, tilt: 3 };
|
|
40
|
+
const GLYPH = { tailDuration: 0.75, durationStrength: 0.45, tailStaggerStart: 0.5, tailStaggerStep: 0.25, staggerStrength: 0.25 };
|
|
41
|
+
const WIDTH = { tailCollapseDelay: 0.55, tailCollapseDuration: 0.6, newSlotExpansion: 0.45, minimumChange: 0.5 };
|
|
42
|
+
const COMPLETION_BUFFER = 80;
|
|
43
|
+
const NBSP = ' ';
|
|
44
|
+
|
|
45
|
+
const glyph = (letter: string) => (letter === ' ' ? NBSP : letter);
|
|
46
|
+
const letters = (text: string) => splitGraphemes(text, Number.MAX_SAFE_INTEGER);
|
|
47
|
+
|
|
48
|
+
/** A stable pseudo-random value in [-1, 1] for a letter and a channel. */
|
|
49
|
+
const noise = (index: number, channel: number) => {
|
|
50
|
+
const value = Math.sin((index + 1) * VARIATION.index + channel * VARIATION.channel) * VARIATION.seed;
|
|
51
|
+
return (value - Math.floor(value)) * 2 - 1;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const face = (letter: string) => {
|
|
55
|
+
const element = document.createElement('span');
|
|
56
|
+
element.className = 'mk-slot__face';
|
|
57
|
+
element.textContent = glyph(letter);
|
|
58
|
+
return element;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const slot = (letter: string) => {
|
|
62
|
+
const element = document.createElement('span');
|
|
63
|
+
element.className = 'mk-slot__char';
|
|
64
|
+
element.dataset.char = letter;
|
|
65
|
+
const sizer = document.createElement('span');
|
|
66
|
+
sizer.className = 'mk-slot__sizer';
|
|
67
|
+
sizer.textContent = glyph(letter);
|
|
68
|
+
element.append(sizer, face(letter));
|
|
69
|
+
return element;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/** Letters grouped by word; a space is the last slot of the word before it. */
|
|
73
|
+
function build(target: HTMLElement, text: string) {
|
|
74
|
+
const nodes: HTMLElement[] = [];
|
|
75
|
+
let word: HTMLElement | null = null;
|
|
76
|
+
for (const letter of letters(text)) {
|
|
77
|
+
if (letter === ' ') {
|
|
78
|
+
if (word) word.append(slot(letter));
|
|
79
|
+
else nodes.push(slot(letter));
|
|
80
|
+
word = null;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (!word) {
|
|
84
|
+
word = document.createElement('span');
|
|
85
|
+
word.className = 'mk-slot__word';
|
|
86
|
+
nodes.push(word);
|
|
87
|
+
}
|
|
88
|
+
word.append(slot(letter));
|
|
89
|
+
}
|
|
90
|
+
target.replaceChildren(...nodes);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function createSlotText(el: HTMLElement | null, options: SlotTextOptions = {}): SlotTextInstance {
|
|
94
|
+
if (!el) {
|
|
95
|
+
warnOnce('slot-text: missing element');
|
|
96
|
+
return inert({ set() {}, text: '' });
|
|
97
|
+
}
|
|
98
|
+
const {
|
|
99
|
+
direction = 'down',
|
|
100
|
+
duration: baseDuration = TIMING.slot.durationMs,
|
|
101
|
+
stagger = TIMING.slot.staggerMs,
|
|
102
|
+
exitOffset = TIMING.slot.exitOffsetMs,
|
|
103
|
+
colorFade = TIMING.slot.colorFadeMs,
|
|
104
|
+
maxTilt = TIMING.slot.maxTilt,
|
|
105
|
+
} = options;
|
|
106
|
+
const original = [...el.childNodes];
|
|
107
|
+
const originalText = el.textContent ?? '';
|
|
108
|
+
let text = options.text ?? originalText;
|
|
109
|
+
let timers: number[] = [];
|
|
110
|
+
let destroyed = false;
|
|
111
|
+
|
|
112
|
+
const copy = document.createElement('span');
|
|
113
|
+
copy.className = 'mk-sr-only';
|
|
114
|
+
copy.textContent = text;
|
|
115
|
+
const track = document.createElement('span');
|
|
116
|
+
track.className = 'mk-slot';
|
|
117
|
+
track.setAttribute('aria-hidden', 'true');
|
|
118
|
+
build(track, text);
|
|
119
|
+
if (options.accessibleCopy === false) {
|
|
120
|
+
track.removeAttribute('aria-hidden');
|
|
121
|
+
el.replaceChildren(track);
|
|
122
|
+
} else el.replaceChildren(copy, track);
|
|
123
|
+
el.dataset.mkReady = '';
|
|
124
|
+
|
|
125
|
+
const stop = () => {
|
|
126
|
+
for (const timer of timers) clearTimeout(timer);
|
|
127
|
+
timers = [];
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const roll = (next: string) => {
|
|
131
|
+
const interrupted = timers.length > 0;
|
|
132
|
+
stop();
|
|
133
|
+
if (interrupted) build(track, text);
|
|
134
|
+
let slots = [...track.querySelectorAll<HTMLElement>('.mk-slot__char')];
|
|
135
|
+
if (!slots.length || reduced()) {
|
|
136
|
+
build(track, next);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const current = slots.map((entry) => entry.dataset.char ?? '');
|
|
140
|
+
const target = letters(next);
|
|
141
|
+
for (let index = slots.length; index < Math.max(current.length, target.length); index += 1) {
|
|
142
|
+
const extra = slot('');
|
|
143
|
+
track.append(extra);
|
|
144
|
+
slots = [...slots, extra];
|
|
145
|
+
}
|
|
146
|
+
const firstFilled = slots.find((entry) => (entry.dataset.char ?? '') !== '') ?? slots[0];
|
|
147
|
+
const height = Math.ceil(firstFilled?.getBoundingClientRect().height ?? 0) || 18;
|
|
148
|
+
const changes = slots
|
|
149
|
+
.map((entry, index) => ({
|
|
150
|
+
index,
|
|
151
|
+
slot: entry,
|
|
152
|
+
sizer: entry.querySelector<HTMLElement>('.mk-slot__sizer'),
|
|
153
|
+
previous: entry.querySelector<HTMLElement>('.mk-slot__face'),
|
|
154
|
+
from: current[index] ?? '',
|
|
155
|
+
to: target[index] ?? '',
|
|
156
|
+
width: entry.getBoundingClientRect().width,
|
|
157
|
+
}))
|
|
158
|
+
.filter(({ from, to }) => from !== to);
|
|
159
|
+
if (!changes.length) {
|
|
160
|
+
build(track, next);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
for (const change of changes) if (change.sizer) change.sizer.textContent = glyph(change.to);
|
|
164
|
+
const measured = changes.map((change) => {
|
|
165
|
+
const width = change.sizer?.getBoundingClientRect().width ?? change.width;
|
|
166
|
+
return { ...change, targetWidth: width, resize: Math.abs(width - change.width) > WIDTH.minimumChange };
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const later = (run: () => void, ms: number) => timers.push(window.setTimeout(run, ms));
|
|
170
|
+
const enterFrom = direction === 'down' ? -height : height;
|
|
171
|
+
const exitTo = -enterFrom;
|
|
172
|
+
let completion = 0;
|
|
173
|
+
const planned = measured.map((change) => {
|
|
174
|
+
const tail = change.to === '';
|
|
175
|
+
const vary = (strength: number, channel: number) => 1 + BOUNCE * strength * noise(change.index, channel);
|
|
176
|
+
const duration = Math.round(baseDuration * (tail ? GLYPH.tailDuration : 1) * vary(GLYPH.durationStrength, VARIATION.duration));
|
|
177
|
+
const order = tail ? target.length * GLYPH.tailStaggerStart + (change.index - target.length) * GLYPH.tailStaggerStep : change.index;
|
|
178
|
+
const delay = Math.round(order * stagger * vary(GLYPH.staggerStrength, VARIATION.stagger));
|
|
179
|
+
const tilt = Number((BOUNCE * maxTilt * noise(change.index, VARIATION.tilt)).toFixed(2));
|
|
180
|
+
const minimum = (factor: number) => Math.max(TIMING.slot.minResizeMs, Math.round(duration * factor));
|
|
181
|
+
const widthTiming =
|
|
182
|
+
change.to === ''
|
|
183
|
+
? { delay: delay + Math.round(duration * WIDTH.tailCollapseDelay), duration: minimum(WIDTH.tailCollapseDuration) }
|
|
184
|
+
: change.from === ''
|
|
185
|
+
? { delay, duration: minimum(WIDTH.newSlotExpansion) }
|
|
186
|
+
: { delay, duration };
|
|
187
|
+
if (change.resize) change.slot.style.width = `${change.width}px`;
|
|
188
|
+
if (change.from === '' || change.to === '') change.slot.classList.add('is-resizing');
|
|
189
|
+
const incoming = face(change.to);
|
|
190
|
+
incoming.style.transform = `translateY(${enterFrom}px) rotate(${tilt}deg)`;
|
|
191
|
+
incoming.style.opacity = '0';
|
|
192
|
+
change.slot.append(incoming);
|
|
193
|
+
completion = Math.max(completion, change.resize ? widthTiming.delay + widthTiming.duration : 0, delay + exitOffset + duration);
|
|
194
|
+
return { change, duration, delay, tilt, widthTiming, incoming };
|
|
195
|
+
});
|
|
196
|
+
void track.offsetWidth;
|
|
197
|
+
for (const { change, duration, delay, tilt, widthTiming, incoming } of planned) {
|
|
198
|
+
const transition = `transform ${duration}ms ${EASING}, opacity ${colorFade}ms linear`;
|
|
199
|
+
if (change.resize) {
|
|
200
|
+
later(() => {
|
|
201
|
+
change.slot.style.transition = `width ${widthTiming.duration}ms ${RESIZE_EASING}`;
|
|
202
|
+
change.slot.style.width = `${change.targetWidth}px`;
|
|
203
|
+
}, widthTiming.delay);
|
|
204
|
+
}
|
|
205
|
+
if (change.previous) {
|
|
206
|
+
const previous = change.previous;
|
|
207
|
+
later(() => {
|
|
208
|
+
previous.style.transition = transition;
|
|
209
|
+
previous.style.transform = `translateY(${exitTo}px) rotate(${-tilt}deg)`;
|
|
210
|
+
previous.style.opacity = '0';
|
|
211
|
+
}, delay);
|
|
212
|
+
}
|
|
213
|
+
later(() => {
|
|
214
|
+
incoming.style.transition = transition;
|
|
215
|
+
incoming.style.transform = 'translateY(0) rotate(0deg)';
|
|
216
|
+
incoming.style.opacity = '1';
|
|
217
|
+
}, delay + exitOffset);
|
|
218
|
+
}
|
|
219
|
+
later(() => {
|
|
220
|
+
timers = [];
|
|
221
|
+
build(track, next);
|
|
222
|
+
}, completion + COMPLETION_BUFFER);
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
set(next: string) {
|
|
227
|
+
if (destroyed || next === text) return;
|
|
228
|
+
roll(next);
|
|
229
|
+
text = next;
|
|
230
|
+
copy.textContent = next;
|
|
231
|
+
},
|
|
232
|
+
get text() {
|
|
233
|
+
return text;
|
|
234
|
+
},
|
|
235
|
+
destroy() {
|
|
236
|
+
if (destroyed) return;
|
|
237
|
+
destroyed = true;
|
|
238
|
+
stop();
|
|
239
|
+
if (text === originalText) el.replaceChildren(...original);
|
|
240
|
+
else el.textContent = text;
|
|
241
|
+
delete el.dataset.mkReady;
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/* slot-text: each letter sits in a slot whose face rolls out and in; the hidden sizer gives the slot its width. */
|
|
2
|
+
.mk-slot {
|
|
3
|
+
display: inline;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.mk-slot__word {
|
|
7
|
+
display: inline-flex;
|
|
8
|
+
align-items: baseline;
|
|
9
|
+
white-space: pre;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.mk-slot__char {
|
|
13
|
+
position: relative;
|
|
14
|
+
display: inline-flex;
|
|
15
|
+
flex: none;
|
|
16
|
+
justify-content: center;
|
|
17
|
+
align-items: baseline;
|
|
18
|
+
overflow-x: visible;
|
|
19
|
+
overflow-y: clip;
|
|
20
|
+
/* Descenders stay inside the clip. */
|
|
21
|
+
overflow-clip-margin: 0.15em;
|
|
22
|
+
line-height: inherit;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.mk-slot__char.is-resizing {
|
|
26
|
+
overflow-x: clip;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.mk-slot__sizer {
|
|
30
|
+
visibility: hidden;
|
|
31
|
+
white-space: pre;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.mk-slot__face {
|
|
35
|
+
position: absolute;
|
|
36
|
+
inset: 0;
|
|
37
|
+
display: flex;
|
|
38
|
+
align-items: center;
|
|
39
|
+
justify-content: center;
|
|
40
|
+
white-space: pre;
|
|
41
|
+
will-change: transform;
|
|
42
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# snap-strip
|
|
2
|
+
|
|
3
|
+
Una tira horizontal de tarjetas con scroll-snap nativo que pasa a la siguiente cada 5 s mientras se ve (empieza medio segundo después de aparecer), espera 2 s después de un toque, arrastre o rueda antes de seguir y vuelve de la última a la primera.
|
|
4
|
+
|
|
5
|
+
Origen: iyo.ai, tarjetas de producto en móvil (`product-card-mobile`, `CM`): cadencia 5000 ms, espera 2000 ms, `data-delay="500"`, tarjetas de 68vw. iyo usa el slider físico Smooothy con bucle clonado; aquí desliza el navegador con su propio snap.
|
|
6
|
+
|
|
7
|
+
## HTML / Astro
|
|
8
|
+
|
|
9
|
+
```html
|
|
10
|
+
<div data-mk="snap-strip" data-mk-delay="500" class="mk-snap-strip" aria-label="Productos">
|
|
11
|
+
<article class="card">…</article>
|
|
12
|
+
<article class="card">…</article>
|
|
13
|
+
<article class="card">…</article>
|
|
14
|
+
</div>
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## React
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
21
|
+
const strip = useSnapStrip(ref, { interval: 5000 });
|
|
22
|
+
<button onClick={() => strip.current?.next()}>Siguiente</button>;
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## API
|
|
26
|
+
|
|
27
|
+
`createSnapStrip(root, { autoplay, interval, resume, delay })` → `{ next(), previous(), index, destroy }`. Variables: `--mk-snap-strip-item` (68vw), `--mk-snap-strip-gap`.
|
|
28
|
+
|
|
29
|
+
## Accesibilidad
|
|
30
|
+
|
|
31
|
+
Se para fuera de pantalla, con la pestaña oculta y mientras se toca. Con movimiento reducido no avanza sola y `next()` / `previous()` saltan sin animación. Si lo usas con autoplay en contenido importante, añade botones de anterior / siguiente.
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { reduced } from '../../core/reduced';
|
|
2
|
+
import { createScope } from '../../core/scope';
|
|
3
|
+
import { TIMING } from '../../core/timing';
|
|
4
|
+
import { inert, warnOnce, type Instance } from '../../core/types';
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
* iyo.ai's phone cards: a native scroll-snap strip that moves on by one card
|
|
8
|
+
* every 5 s while it is on screen (starting `delay` ms after it shows), waits
|
|
9
|
+
* 2 s after a touch, drag or wheel before carrying on, and wraps from the last
|
|
10
|
+
* card to the first. iyo drives it with Smooothy; this keeps the browser's own
|
|
11
|
+
* scrolling and snapping instead of a physics slider.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export interface SnapStripOptions {
|
|
15
|
+
autoplay?: boolean;
|
|
16
|
+
/** Milliseconds between cards (iyo: 5000). */
|
|
17
|
+
interval?: number;
|
|
18
|
+
/** Milliseconds to wait after the reader touches it (iyo: 2000). */
|
|
19
|
+
resume?: number;
|
|
20
|
+
/** Milliseconds after it shows before autoplay starts (iyo's data-delay: 500). */
|
|
21
|
+
delay?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface SnapStripInstance extends Instance {
|
|
25
|
+
next(): void;
|
|
26
|
+
previous(): void;
|
|
27
|
+
/** Index of the card nearest the start edge. */
|
|
28
|
+
readonly index: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function createSnapStrip(root: HTMLElement | null, options: SnapStripOptions = {}): SnapStripInstance {
|
|
32
|
+
if (!root || root.children.length < 2) {
|
|
33
|
+
warnOnce('snap-strip: needs a strip with at least two cards');
|
|
34
|
+
return inert({ next() {}, previous() {}, index: 0 });
|
|
35
|
+
}
|
|
36
|
+
const t = TIMING.snapStrip;
|
|
37
|
+
const { autoplay = true, interval = t.interval, resume = t.resume, delay = t.delay } = options;
|
|
38
|
+
const scope = createScope();
|
|
39
|
+
const items = () => [...root.children] as HTMLElement[];
|
|
40
|
+
const startScroll = root.scrollLeft;
|
|
41
|
+
let timer: ReturnType<typeof setInterval> | undefined;
|
|
42
|
+
let waiting: ReturnType<typeof setTimeout> | undefined;
|
|
43
|
+
let visible = typeof IntersectionObserver !== 'function';
|
|
44
|
+
let touching = false;
|
|
45
|
+
|
|
46
|
+
const offsetOf = (item: HTMLElement) => item.getBoundingClientRect().left - root.getBoundingClientRect().left;
|
|
47
|
+
const nearest = () => {
|
|
48
|
+
let best = 0;
|
|
49
|
+
items().forEach((item, i, all) => {
|
|
50
|
+
if (Math.abs(offsetOf(item)) < Math.abs(offsetOf(all[best]!))) best = i;
|
|
51
|
+
});
|
|
52
|
+
return best;
|
|
53
|
+
};
|
|
54
|
+
const move = (step: number) => {
|
|
55
|
+
if (scope.disposed) return;
|
|
56
|
+
const all = items();
|
|
57
|
+
const target = all[(nearest() + step + all.length) % all.length]!;
|
|
58
|
+
root.scrollTo({ left: offsetOf(target) + root.scrollLeft, behavior: reduced() ? 'instant' : 'smooth' });
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const stop = () => {
|
|
62
|
+
clearInterval(timer);
|
|
63
|
+
timer = undefined;
|
|
64
|
+
};
|
|
65
|
+
const start = () => {
|
|
66
|
+
if (scope.disposed || !autoplay || reduced() || !visible || touching || document.hidden || timer) return;
|
|
67
|
+
timer = setInterval(() => move(1), interval);
|
|
68
|
+
};
|
|
69
|
+
const later = (fn: () => void, ms: number) => {
|
|
70
|
+
clearTimeout(waiting);
|
|
71
|
+
waiting = setTimeout(fn, ms);
|
|
72
|
+
};
|
|
73
|
+
scope.add(() => {
|
|
74
|
+
stop();
|
|
75
|
+
clearTimeout(waiting);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const hold = () => {
|
|
79
|
+
touching = true;
|
|
80
|
+
stop();
|
|
81
|
+
};
|
|
82
|
+
const release = () => {
|
|
83
|
+
if (!touching) return;
|
|
84
|
+
later(() => {
|
|
85
|
+
touching = false;
|
|
86
|
+
start();
|
|
87
|
+
}, resume);
|
|
88
|
+
};
|
|
89
|
+
scope.on(root, 'pointerdown', hold, { passive: true });
|
|
90
|
+
scope.on(root, 'touchstart', hold, { passive: true });
|
|
91
|
+
scope.on(window, 'pointerup', release, { passive: true });
|
|
92
|
+
scope.on(window, 'pointercancel', release, { passive: true });
|
|
93
|
+
scope.on(window, 'touchend', release, { passive: true });
|
|
94
|
+
scope.on(
|
|
95
|
+
root,
|
|
96
|
+
'wheel',
|
|
97
|
+
() => {
|
|
98
|
+
hold();
|
|
99
|
+
release();
|
|
100
|
+
},
|
|
101
|
+
{ passive: true },
|
|
102
|
+
);
|
|
103
|
+
scope.on(document, 'visibilitychange', () => {
|
|
104
|
+
stop();
|
|
105
|
+
start();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
if (typeof IntersectionObserver === 'function') {
|
|
109
|
+
const observer = scope.observe(
|
|
110
|
+
new IntersectionObserver(([entry]) => {
|
|
111
|
+
visible = Boolean(entry?.isIntersecting);
|
|
112
|
+
stop();
|
|
113
|
+
if (visible) later(start, delay);
|
|
114
|
+
else clearTimeout(waiting);
|
|
115
|
+
}),
|
|
116
|
+
);
|
|
117
|
+
observer.observe(root);
|
|
118
|
+
} else later(start, delay);
|
|
119
|
+
|
|
120
|
+
scope.add(() => {
|
|
121
|
+
root.scrollLeft = startScroll;
|
|
122
|
+
});
|
|
123
|
+
scope.attr(root, 'data-mk-ready', '');
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
next: () => move(1),
|
|
127
|
+
previous: () => move(-1),
|
|
128
|
+
get index() {
|
|
129
|
+
return nearest();
|
|
130
|
+
},
|
|
131
|
+
destroy: () => scope.dispose(),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/* iyo.ai's phone cards: a strip of cards that snaps to each one. */
|
|
2
|
+
.mk-snap-strip {
|
|
3
|
+
--mk-snap-strip-item: 68vw;
|
|
4
|
+
--mk-snap-strip-gap: 0.75rem;
|
|
5
|
+
display: flex;
|
|
6
|
+
gap: var(--mk-snap-strip-gap);
|
|
7
|
+
overflow-x: auto;
|
|
8
|
+
overscroll-behavior-inline: contain;
|
|
9
|
+
scroll-snap-type: x mandatory;
|
|
10
|
+
scrollbar-width: none;
|
|
11
|
+
}
|
|
12
|
+
.mk-snap-strip::-webkit-scrollbar {
|
|
13
|
+
display: none;
|
|
14
|
+
}
|
|
15
|
+
.mk-snap-strip > * {
|
|
16
|
+
flex: 0 0 var(--mk-snap-strip-item);
|
|
17
|
+
scroll-snap-align: start;
|
|
18
|
+
}
|
|
19
|
+
@media (prefers-reduced-motion: reduce) {
|
|
20
|
+
.mk-snap-strip {
|
|
21
|
+
scroll-behavior: auto;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# sortable
|
|
2
|
+
|
|
3
|
+
El arrastre para reordenar de Stepbro Tasks (`useDragReorder`, modo "transform"):
|
|
4
|
+
|
|
5
|
+
- **Levantar:** pasados 6 px el elemento se convierte en una copia flotante (0.18 s, `power2.out`) que crece un 3 % y gana sombra. El hueco que deja queda invisible.
|
|
6
|
+
- **Seguir:** la copia sigue al puntero con un resorte (rigidez 1100, amortiguación 38; más pesada cuanto más grande) y se inclina según la velocidad y la aceleración, como si colgara del punto por donde la agarraste (resorte 170/13, hasta 14°).
|
|
7
|
+
- **Hacer sitio:** los demás no cambian de sitio en el DOM mientras arrastras: se deslizan (0.62 s, resorte .52) para abrir el hueco donde caería.
|
|
8
|
+
- **Soltar:** la copia aterriza en su sitio nuevo (0.42 s, resorte .62) y, si cambia de lista, se funde con una copia con el estilo de la lista nueva (0.28 s).
|
|
9
|
+
- **Bordes:** cerca del borde (56 px) el contenedor que hace scroll se desplaza solo, hasta 16 px por frame.
|
|
10
|
+
- **Cancelar:** Escape (o soltar donde estaba) la devuelve a su sitio.
|
|
11
|
+
|
|
12
|
+
En pantallas táctiles solo se arrastra desde `[data-mk-sort-handle]`, para que la lista siga haciendo scroll. Teclado (añadido): Espacio o Enter levanta, flechas arriba/abajo mueven, izquierda/derecha cambian de lista, Espacio o Enter suelta, Escape lo devuelve; cada paso se anuncia.
|
|
13
|
+
|
|
14
|
+
Origen: Stepbro Tasks `useDragReorder` (`stepbro-tasks.js`, líneas 39021–39330).
|
|
15
|
+
|
|
16
|
+
## HTML / Astro
|
|
17
|
+
|
|
18
|
+
```html
|
|
19
|
+
<ul data-mk="sortable" class="lista">
|
|
20
|
+
<li data-mk-item data-mk-id="1">Comprar pan</li>
|
|
21
|
+
<li data-mk-item data-mk-id="2">Llamar a Ana</li>
|
|
22
|
+
<li data-mk-item data-mk-id="3">Pagar la luz <button data-mk-sort-handle aria-label="Mover">⋮⋮</button></li>
|
|
23
|
+
</ul>
|
|
24
|
+
|
|
25
|
+
<script>
|
|
26
|
+
import { autoInit } from '@yewdiel21/motion-kit/auto';
|
|
27
|
+
autoInit();
|
|
28
|
+
document.querySelector('.lista').addEventListener('mk:reorder', (event) => {
|
|
29
|
+
const { id, toIndex, afterId } = event.detail;
|
|
30
|
+
guardarOrden(id, afterId); // el DOM ya está reordenado
|
|
31
|
+
});
|
|
32
|
+
</script>
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Varias listas (kanban):
|
|
36
|
+
|
|
37
|
+
```html
|
|
38
|
+
<div data-mk="sortable" class="tablero">
|
|
39
|
+
<ul data-mk-sortable-group="pendiente" aria-label="Pendiente">…</ul>
|
|
40
|
+
<ul data-mk-sortable-group="hecho" aria-label="Hecho">…</ul>
|
|
41
|
+
<ul data-mk-sortable-group="archivo" data-mk-sortable-locked>…</ul>
|
|
42
|
+
</div>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Con JS: `createSortable(el, opciones)`. Atributos de opciones: `data-mk-handle`, `data-mk-mutate="false"`, `data-mk-auto-scroll="false"`.
|
|
46
|
+
|
|
47
|
+
## React
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
const ref = useRef<HTMLUListElement>(null);
|
|
51
|
+
const [tareas, setTareas] = useState(lista);
|
|
52
|
+
useSortable(ref, {
|
|
53
|
+
onReorder: ({ fromIndex, toIndex }) => setTareas((t) => moveItem(t, fromIndex, toIndex)),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
<ul ref={ref}>
|
|
57
|
+
{tareas.map((t) => (
|
|
58
|
+
<li key={t.id} data-mk-item data-mk-id={t.id}>{t.titulo}</li>
|
|
59
|
+
))}
|
|
60
|
+
</ul>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
En React el motor no toca el orden del DOM (`mutate: false`): llama `onReorder`, espera a que React pinte el nuevo orden y aterriza ahí. Si tu estado no cambia (por ejemplo, el servidor lo rechaza), todo vuelve a su sitio.
|
|
64
|
+
|
|
65
|
+
## API
|
|
66
|
+
|
|
67
|
+
`createSortable(root, opciones)` → `{ cancel(), dragging, destroy }`.
|
|
68
|
+
|
|
69
|
+
| Opción | Por defecto | Qué hace |
|
|
70
|
+
|---|---|---|
|
|
71
|
+
| `mutate` | `true` (`false` en `useSortable`) | El motor mueve el elemento en el DOM al soltar |
|
|
72
|
+
| `handle` | `false` | Solo se arrastra desde `[data-mk-sort-handle]` (siempre en táctil) |
|
|
73
|
+
| `autoScroll` | `true` | Desplaza el contenedor cerca de los bordes |
|
|
74
|
+
| `follow` | `{ stiffness: 1100, damping: 38 }` | Resorte de la copia flotante |
|
|
75
|
+
| `labels` | español | `lifted`, `moved`, `dropped` (funciones de posición y total), `cancelled` |
|
|
76
|
+
| `onReorder` | — | `(detalle) => void` |
|
|
77
|
+
|
|
78
|
+
`detalle` (también en el evento `mk:reorder`, cancelable): `id`, `item`, `fromGroup`, `toGroup`, `fromIndex`, `toIndex`, `groupChanged`, `afterId`, `beforeId` (el vecino en el destino, para guardar el orden en un servidor como hace Stepbro). `moveItem(lista, desde, hasta)` mueve una entrada de un array.
|
|
79
|
+
|
|
80
|
+
Partes: `[data-mk-item]` con `data-mk-id`, `[data-mk-sort-handle]`, `[data-mk-sortable-group]` (opcional; `data-mk-sortable-locked` no acepta elementos). Durante el arrastre: `[data-mk-sorting]` en la raíz, `[data-mk-dragging]` en el hueco, `[data-mk-sortable-clone]` en la copia; levantado con teclado: `[data-mk-sort-grabbed]`.
|
|
81
|
+
|
|
82
|
+
## Variables CSS
|
|
83
|
+
|
|
84
|
+
`--mk-sortable-shadow` (sombra de la copia; usa `--mk-lift`, de 0 a 1).
|
|
85
|
+
|
|
86
|
+
## Accesibilidad
|
|
87
|
+
|
|
88
|
+
Los elementos sin asa reciben `tabindex="0"` para poder levantarlos con el teclado; los anuncios van a una región `aria-live`. Un clic corto (menos de 6 px) sigue siendo un clic; tras arrastrar, el clic se descarta. Los controles dentro de un elemento (botones, enlaces, campos) no inician el arrastre. Con movimiento reducido la copia sigue al puntero sin resorte ni inclinación y aterriza al instante.
|