@yewdiel21/motion-kit 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +3 -35
- package/README.md +93 -81
- package/dist/auto/index.d.ts +22 -3
- package/dist/auto/index.js +43 -5
- package/dist/auto/index.js.map +1 -1
- package/dist/{chunk-KLY5BRRI.js → chunk-34EZ7DF4.js} +1 -1
- package/dist/chunk-34EZ7DF4.js.map +1 -0
- package/dist/{chunk-RFDIZYFX.js → chunk-DOOMULVV.js} +20 -20
- package/dist/chunk-DOOMULVV.js.map +1 -0
- package/dist/{chunk-ABNRDDO7.js → chunk-I3DXF2KQ.js} +2 -2
- package/dist/{chunk-Q4LVFNLN.js → chunk-IKQ5FODN.js} +4 -4
- package/dist/chunk-IKQ5FODN.js.map +1 -0
- package/dist/{chunk-2BQGFOA7.js → chunk-J6OCFNGU.js} +2 -2
- package/dist/chunk-J6OCFNGU.js.map +1 -0
- package/dist/{chunk-6OJAP6ZL.js → chunk-QFBOHPWZ.js} +3 -3
- package/dist/chunk-QFBOHPWZ.js.map +1 -0
- package/dist/{chunk-J27KH2A2.js → chunk-RVSKEBIC.js} +1 -1
- package/dist/chunk-RVSKEBIC.js.map +1 -0
- package/dist/{chunk-34XYOXHG.js → chunk-TFNRIZHU.js} +1615 -12
- package/dist/chunk-TFNRIZHU.js.map +1 -0
- package/dist/{chunk-FS2HJVVY.js → chunk-XAKC2QY7.js} +1204 -5
- package/dist/chunk-XAKC2QY7.js.map +1 -0
- package/dist/css/components/calendar.css +159 -0
- package/dist/css/components/chat-background.css +2 -2
- package/dist/css/components/chat.css +1 -1
- package/dist/css/components/cursor.css +3 -3
- package/dist/css/components/dialog.css +3 -3
- package/dist/css/components/hover-card.css +14 -0
- package/dist/css/components/input-otp.css +10 -0
- package/dist/css/components/island.css +1 -1
- package/dist/css/components/menu.css +1 -1
- package/dist/css/components/nav.css +1 -5
- package/dist/css/components/pagination.css +11 -0
- package/dist/css/components/quantity.css +16 -0
- package/dist/css/components/slot-picker.css +126 -0
- package/dist/css/components/steps.css +2 -2
- package/dist/css/components/studio-dock.css +35 -0
- package/dist/css/components/theme-picker.css +1 -1
- package/dist/css/components/toast.css +1 -1
- package/dist/css/components/trailer.css +37 -0
- package/dist/css/controls.css +78 -13
- package/dist/css/effects.css +8 -8
- package/dist/css/fonts.css +1 -1
- package/dist/css/themes/black-green.css +1 -1
- package/dist/css/themes/black.css +1 -1
- package/dist/css/themes/blue.css +1 -1
- package/dist/css/themes/brown.css +1 -1
- package/dist/css/themes/cold-blue.css +1 -1
- package/dist/css/themes/funny.css +1 -1
- package/dist/css/themes/gray.css +1 -1
- package/dist/css/themes/green.css +1 -1
- package/dist/css/themes/modern-1.css +1 -1
- package/dist/css/themes/neogreen.css +1 -1
- package/dist/css/themes/new.css +1 -1
- package/dist/css/themes/new2.css +1 -1
- package/dist/css/themes/oled.css +1 -1
- package/dist/css/themes/orange.css +1 -1
- package/dist/css/themes/pastel-pink.css +1 -1
- package/dist/css/themes/pink.css +1 -1
- package/dist/css/themes/purple.css +1 -1
- package/dist/css/themes/red.css +1 -1
- package/dist/css/themes/super-blue.css +1 -1
- package/dist/css/themes.css +19 -19
- package/dist/css/tokens.css +9 -9
- package/dist/elements/index.d.ts +75 -7
- package/dist/elements/index.js +172 -6
- package/dist/elements/index.js.map +1 -1
- package/dist/{engine-CUr9QijM.d.ts → engine-GDGwQnJE.d.ts} +2 -2
- package/dist/gl/index.js +3 -3
- package/dist/index.css +1076 -63
- package/dist/index.d.ts +17 -17
- package/dist/index.js +49 -7
- package/dist/index.js.map +1 -1
- package/dist/react/index.d.ts +139 -40
- package/dist/react/index.js +355 -8
- package/dist/react/index.js.map +1 -1
- package/dist/{reduced-BM4Ls0g5.d.ts → reduced-BmBzKH94.d.ts} +215 -18
- package/dist/{reveal-WFUWDrei.d.ts → reveal-jsX-uwUz.d.ts} +233 -24
- package/dist/{store-Bqy_obnD.d.ts → store-DYb3tTsZ.d.ts} +5 -5
- package/dist/theme/generate.d.ts +1 -1
- package/dist/theme/generate.js +2 -2
- package/dist/theme/generate.js.map +1 -1
- package/dist/theme/index.d.ts +2 -2
- package/dist/theme/index.js +2 -2
- package/dist/{theme-BFMoCzi6.d.ts → theme-Bjf9qIIy.d.ts} +1 -1
- package/package.json +2 -2
- package/registry.json +230 -156
- package/skills/motion-kit/SKILL.md +209 -189
- package/skills/motion-kit/reference/components.md +5 -5
- package/skills/motion-kit/reference/design.md +14 -14
- package/skills/motion-kit/reference/recipes.md +48 -49
- package/src/auto/auto-init.ts +38 -0
- package/src/components/calendar/README.md +82 -0
- package/src/components/calendar/calendar.css +158 -0
- package/src/components/calendar/element.ts +27 -0
- package/src/components/calendar/engine.ts +412 -0
- package/src/components/calendar/react.tsx +69 -0
- package/src/components/chat/README.md +1 -3
- package/src/components/chat/chat.css +1 -1
- package/src/components/chat/engine.ts +1 -1
- package/src/components/chat/react.tsx +1 -1
- package/src/components/chat-background/README.md +1 -3
- package/src/components/chat-background/audio.ts +1 -1
- package/src/components/chat-background/chat-background.css +2 -2
- package/src/components/chat-background/engine.ts +1 -1
- package/src/components/chat-background/react.tsx +1 -1
- package/src/components/cursor/README.md +3 -5
- package/src/components/cursor/cursor.css +3 -3
- package/src/components/cursor/element.ts +2 -2
- package/src/components/cursor/engine.ts +5 -5
- package/src/components/cursor/react.tsx +1 -1
- package/src/components/dialog/README.md +4 -8
- package/src/components/dialog/dialog.css +3 -3
- package/src/components/dialog/engine.ts +2 -2
- package/src/components/dialog/react.tsx +1 -1
- package/src/components/hover-card/README.md +16 -0
- package/src/components/hover-card/element.ts +8 -0
- package/src/components/hover-card/engine.ts +69 -0
- package/src/components/hover-card/hover-card.css +13 -0
- package/src/components/hover-card/react.tsx +19 -0
- package/src/components/input-otp/README.md +18 -0
- package/src/components/input-otp/element.ts +14 -0
- package/src/components/input-otp/engine.ts +106 -0
- package/src/components/input-otp/input-otp.css +9 -0
- package/src/components/input-otp/react.tsx +21 -0
- package/src/components/island/README.md +7 -7
- package/src/components/island/engine.ts +3 -3
- package/src/components/island/island.css +1 -1
- package/src/components/island/react.tsx +3 -3
- package/src/components/menu/README.md +2 -4
- package/src/components/menu/engine.ts +1 -1
- package/src/components/menu/menu.css +1 -1
- package/src/components/menu/react.tsx +1 -1
- package/src/components/nav/README.md +12 -14
- package/src/components/nav/engine.ts +5 -5
- package/src/components/nav/nav.css +1 -5
- package/src/components/nav/react.tsx +3 -3
- package/src/components/pagination/README.md +17 -0
- package/src/components/pagination/element.ts +14 -0
- package/src/components/pagination/engine.ts +102 -0
- package/src/components/pagination/pagination.css +10 -0
- package/src/components/pagination/react.tsx +22 -0
- package/src/components/quantity/README.md +20 -0
- package/src/components/quantity/element.ts +14 -0
- package/src/components/quantity/engine.ts +85 -0
- package/src/components/quantity/quantity.css +15 -0
- package/src/components/quantity/react.tsx +21 -0
- package/src/components/slot-picker/README.md +68 -0
- package/src/components/slot-picker/element.ts +27 -0
- package/src/components/slot-picker/engine.ts +412 -0
- package/src/components/slot-picker/react.tsx +76 -0
- package/src/components/slot-picker/slot-picker.css +125 -0
- package/src/components/steps/README.md +1 -3
- package/src/components/steps/engine.ts +3 -3
- package/src/components/steps/react.tsx +2 -2
- package/src/components/steps/steps.css +2 -2
- package/src/components/studio-dock/README.md +44 -0
- package/src/components/studio-dock/element.ts +11 -0
- package/src/components/studio-dock/engine.ts +212 -0
- package/src/components/studio-dock/react.tsx +23 -0
- package/src/components/studio-dock/studio-dock.css +34 -0
- package/src/components/theme-picker/README.md +3 -5
- package/src/components/theme-picker/engine.ts +2 -2
- package/src/components/theme-picker/react.tsx +1 -1
- package/src/components/theme-picker/theme-picker.css +1 -1
- package/src/components/toast/README.md +1 -3
- package/src/components/toast/engine.ts +2 -2
- package/src/components/toast/react.tsx +1 -1
- package/src/components/toast/store.ts +1 -2
- package/src/components/toast/toast.css +1 -1
- package/src/components/trailer/README.md +28 -0
- package/src/components/trailer/element.ts +13 -0
- package/src/components/trailer/engine.ts +189 -0
- package/src/components/trailer/react.tsx +16 -0
- package/src/components/trailer/trailer.css +36 -0
- package/src/core/channels.ts +3 -3
- package/src/core/curves.ts +3 -3
- package/src/core/dates.ts +98 -0
- package/src/core/gsap.ts +2 -2
- package/src/core/loop.ts +1 -1
- package/src/core/math.ts +1 -1
- package/src/core/physics-spring.ts +2 -2
- package/src/core/scroll-track.ts +2 -4
- package/src/core/spring.ts +1 -1
- package/src/core/timing.ts +21 -22
- package/src/css/controls/README.md +24 -20
- package/src/css/controls/burger.css +1 -1
- package/src/css/controls/button.css +4 -4
- package/src/css/controls/checkbox.css +1 -1
- package/src/css/controls/chip.css +1 -1
- package/src/css/controls/field.css +2 -2
- package/src/css/controls/glass.css +4 -4
- package/src/css/controls/status.css +63 -0
- package/src/css/default-theme.css +1 -1
- package/src/css/effects/README.md +19 -21
- package/src/css/effects/anim.css +1 -1
- package/src/css/effects/fade.css +1 -1
- package/src/css/effects/float.css +1 -1
- package/src/css/effects/frost.css +1 -1
- package/src/css/effects/grain.css +1 -1
- package/src/css/effects/loading.css +1 -1
- package/src/css/effects/success.css +1 -1
- package/src/css/effects/text.css +1 -1
- package/src/css/fonts.css +1 -1
- package/src/css/states.css +1 -1
- package/src/css/themes/black-green.css +1 -1
- package/src/css/themes/black.css +1 -1
- package/src/css/themes/blue.css +1 -1
- package/src/css/themes/brown.css +1 -1
- package/src/css/themes/cold-blue.css +1 -1
- package/src/css/themes/funny.css +1 -1
- package/src/css/themes/gray.css +1 -1
- package/src/css/themes/green.css +1 -1
- package/src/css/themes/modern-1.css +1 -1
- package/src/css/themes/neogreen.css +1 -1
- package/src/css/themes/new.css +1 -1
- package/src/css/themes/new2.css +1 -1
- package/src/css/themes/oled.css +1 -1
- package/src/css/themes/orange.css +1 -1
- package/src/css/themes/pastel-pink.css +1 -1
- package/src/css/themes/pink.css +1 -1
- package/src/css/themes/purple.css +1 -1
- package/src/css/themes/red.css +1 -1
- package/src/css/themes/super-blue.css +1 -1
- package/src/css/tokens.css +8 -8
- package/src/elements/index.ts +24 -0
- package/src/gl/liquid-blend/README.md +3 -3
- package/src/gl/matcap/README.md +3 -3
- package/src/gl/particle-hero/README.md +1 -1
- package/src/gl/particle-hero/create-particle-hero.ts +3 -5
- package/src/gl/particle-hero/shaders.ts +1 -1
- package/src/gl/shader-blur/README.md +3 -3
- package/src/gl/shader-grain/README.md +3 -3
- package/src/index.ts +25 -0
- package/src/motion/chat.ts +2 -2
- package/src/motion/confetti.ts +1 -1
- package/src/motion/count.ts +1 -1
- package/src/motion/details.ts +1 -1
- package/src/motion/drag-dismiss.ts +1 -1
- package/src/motion/effects.ts +1 -1
- package/src/motion/entrances.ts +1 -1
- package/src/motion/floating.ts +1 -1
- package/src/motion/flow.ts +2 -2
- package/src/motion/indicator.ts +1 -1
- package/src/motion/layout.ts +1 -1
- package/src/motion/liquid.ts +1 -1
- package/src/motion/morph.ts +2 -2
- package/src/motion/reveal.ts +1 -1
- package/src/motion/sheet.ts +1 -1
- package/src/motion/surface.ts +1 -1
- package/src/react/animated-number.tsx +2 -2
- package/src/react/flip-group.tsx +1 -1
- package/src/react/frost.tsx +1 -1
- package/src/react/index.ts +21 -0
- package/src/react/slot-text.tsx +1 -1
- package/src/react/use-action-button.ts +6 -0
- package/src/react/use-card-dock.ts +1 -1
- package/src/react/use-cascade.ts +1 -1
- package/src/react/use-contrast.ts +1 -1
- package/src/react/use-depth-grid.ts +1 -1
- package/src/react/use-editorial-reveal.ts +7 -0
- package/src/react/use-fill-text.ts +1 -1
- package/src/react/use-film-hero.ts +6 -0
- package/src/react/use-font-cycle.ts +7 -0
- package/src/react/use-frame-zoom.ts +1 -1
- package/src/react/use-image-zoom.ts +5 -0
- package/src/react/use-joined-cta.ts +5 -0
- package/src/react/use-letters.ts +1 -1
- package/src/react/use-local-clock.ts +5 -0
- package/src/react/use-marquee.ts +1 -1
- package/src/react/use-mask-text.ts +1 -1
- package/src/react/use-parallax.ts +1 -1
- package/src/react/use-particle-hero.ts +1 -1
- package/src/react/use-scroll-dock.ts +5 -0
- package/src/react/use-snap-strip.ts +1 -1
- package/src/react/use-sticker.ts +5 -0
- package/src/react/use-studio-dock.ts +8 -0
- package/src/react/use-text-roll.ts +1 -1
- package/src/react/use-thumbnail-nav.ts +7 -0
- package/src/react/use-trailer.ts +6 -0
- package/src/react/use-weight-hover.ts +5 -0
- package/src/recipes/action-button/README.md +31 -0
- package/src/recipes/action-button/action-button.css +113 -0
- package/src/recipes/action-button/create-action-button.ts +56 -0
- package/src/recipes/card-dock/README.md +1 -1
- package/src/recipes/card-dock/card-dock.css +1 -1
- package/src/recipes/card-dock/create-card-dock.ts +2 -2
- package/src/recipes/cascade/README.md +1 -3
- package/src/recipes/cascade/create-cascade.ts +3 -3
- package/src/recipes/contrast/README.md +0 -2
- package/src/recipes/contrast/create-contrast.ts +1 -1
- package/src/recipes/counter/README.md +0 -2
- package/src/recipes/counter/create-counter.ts +1 -1
- package/src/recipes/curved-carousel/README.md +0 -2
- package/src/recipes/curved-carousel/create-curved-carousel.ts +2 -2
- package/src/recipes/curved-carousel/curved-carousel.css +1 -1
- package/src/recipes/depth-grid/README.md +0 -2
- package/src/recipes/depth-grid/create-depth-grid.ts +3 -4
- package/src/recipes/details/README.md +0 -2
- package/src/recipes/details/create-details.ts +1 -1
- package/src/recipes/dock-pill/README.md +3 -3
- package/src/recipes/drag-ruler/README.md +3 -3
- package/src/recipes/editorial-reveal/README.md +35 -0
- package/src/recipes/editorial-reveal/create-editorial-reveal.ts +91 -0
- package/src/recipes/editorial-reveal/editorial-reveal.css +17 -0
- package/src/recipes/fill-text/README.md +1 -1
- package/src/recipes/fill-text/create-fill-text.ts +1 -1
- package/src/recipes/film-hero/README.md +23 -0
- package/src/recipes/film-hero/create-film-hero.ts +65 -0
- package/src/recipes/film-hero/film-hero.css +29 -0
- package/src/recipes/fit-text/README.md +0 -2
- package/src/recipes/flow/README.md +0 -2
- package/src/recipes/flow/create-flow.ts +1 -1
- package/src/recipes/font-cycle/README.md +11 -0
- package/src/recipes/font-cycle/create-font-cycle.ts +126 -0
- package/src/recipes/font-cycle/font-cycle.css +36 -0
- package/src/recipes/frame-zoom/README.md +1 -3
- package/src/recipes/frame-zoom/create-frame-zoom.ts +2 -2
- package/src/recipes/image-zoom/README.md +16 -0
- package/src/recipes/image-zoom/create-image-zoom.ts +27 -0
- package/src/recipes/image-zoom/image-zoom.css +7 -0
- package/src/recipes/infinite-reel/README.md +3 -3
- package/src/recipes/joined-cta/README.md +13 -0
- package/src/recipes/joined-cta/create-joined-cta.ts +22 -0
- package/src/recipes/joined-cta/joined-cta.css +17 -0
- package/src/recipes/letters/README.md +2 -4
- package/src/recipes/letters/create-letters.ts +3 -3
- package/src/recipes/local-clock/README.md +31 -0
- package/src/recipes/local-clock/create-local-clock.ts +61 -0
- package/src/recipes/marquee/README.md +0 -2
- package/src/recipes/marquee/create-marquee.ts +3 -3
- package/src/recipes/marquee/marquee.css +1 -1
- package/src/recipes/mask-text/README.md +3 -5
- package/src/recipes/mask-text/create-mask-text.ts +4 -4
- package/src/recipes/parallax/README.md +1 -3
- package/src/recipes/parallax/create-parallax.ts +1 -1
- package/src/recipes/pile/README.md +0 -2
- package/src/recipes/pile/create-pile.ts +1 -1
- package/src/recipes/pile/pile-math.ts +1 -1
- package/src/recipes/pile/pile.css +1 -1
- package/src/recipes/pill/README.md +0 -2
- package/src/recipes/pill/pill.css +2 -2
- package/src/recipes/pop/README.md +0 -2
- package/src/recipes/pop/create-pop.ts +1 -1
- package/src/recipes/radial-menu/README.md +0 -2
- package/src/recipes/radial-menu/create-radial-menu.ts +1 -1
- package/src/recipes/radial-menu/radial-menu.css +1 -1
- package/src/recipes/reflow/README.md +3 -5
- package/src/recipes/reflow/create-reflow.ts +1 -1
- package/src/recipes/repel-bento/README.md +3 -3
- package/src/recipes/reveal/README.md +0 -2
- package/src/recipes/reveal/create-reveal.ts +1 -1
- package/src/recipes/rotator/README.md +3 -3
- package/src/recipes/scroll-dock/README.md +46 -0
- package/src/recipes/scroll-dock/create-scroll-dock.ts +72 -0
- package/src/recipes/scroll-dock/scroll-dock.css +70 -0
- package/src/recipes/slot-text/README.md +0 -2
- package/src/recipes/slot-text/create-slot-text.ts +1 -1
- package/src/recipes/snap-strip/README.md +1 -1
- package/src/recipes/snap-strip/create-snap-strip.ts +5 -6
- package/src/recipes/snap-strip/snap-strip.css +1 -1
- package/src/recipes/sortable/README.md +3 -5
- package/src/recipes/sortable/create-sortable.ts +4 -4
- package/src/recipes/sortable/sortable.css +1 -1
- package/src/recipes/spring-cards/README.md +0 -2
- package/src/recipes/spring-cards/create-spring-cards.ts +1 -1
- package/src/recipes/spring-cards/spring-cards.css +1 -1
- package/src/recipes/sticker/README.md +59 -0
- package/src/recipes/sticker/create-sticker.ts +490 -0
- package/src/recipes/sticker/sticker.css +112 -0
- package/src/recipes/stretch-footer/README.md +0 -2
- package/src/recipes/stretch-footer/create-stretch-footer.ts +1 -1
- package/src/recipes/stretch-footer/stretch-footer.css +1 -1
- package/src/recipes/tabs/README.md +0 -2
- package/src/recipes/tabs/create-tabs.ts +1 -1
- package/src/recipes/text-roll/README.md +1 -3
- package/src/recipes/text-roll/create-text-roll.ts +3 -3
- package/src/recipes/text-roll/text-roll.css +1 -1
- package/src/recipes/thumbnail-nav/README.md +16 -0
- package/src/recipes/thumbnail-nav/create-thumbnail-nav.ts +165 -0
- package/src/recipes/thumbnail-nav/thumbnail-nav.css +108 -0
- package/src/recipes/ticket-menu/README.md +3 -3
- package/src/recipes/weight-hover/README.md +37 -0
- package/src/recipes/weight-hover/create-weight-hover.ts +136 -0
- package/src/recipes/weight-hover/weight-hover.css +7 -0
- package/src/recipes/words/README.md +0 -2
- package/src/recipes/words/create-words.ts +1 -1
- package/src/theme/generate.ts +1 -1
- package/src/theme/presets.ts +2 -2
- package/src/theme/theme.ts +1 -2
- package/dist/chunk-2BQGFOA7.js.map +0 -1
- package/dist/chunk-34XYOXHG.js.map +0 -1
- package/dist/chunk-6OJAP6ZL.js.map +0 -1
- package/dist/chunk-FS2HJVVY.js.map +0 -1
- package/dist/chunk-J27KH2A2.js.map +0 -1
- package/dist/chunk-KLY5BRRI.js.map +0 -1
- package/dist/chunk-Q4LVFNLN.js.map +0 -1
- package/dist/chunk-RFDIZYFX.js.map +0 -1
- /package/dist/{chunk-ABNRDDO7.js.map → chunk-I3DXF2KQ.js.map} +0 -0
|
@@ -1,20 +1,18 @@
|
|
|
1
1
|
# nav
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Navegación con siete estilos de escritorio y una sola barra de cristal en el móvil (< 680 px), dentro de una cáscara de app (`.mk-shell`).
|
|
4
4
|
|
|
5
|
-
| Estilo |
|
|
6
|
-
|
|
7
|
-
| `clasica` |
|
|
8
|
-
| `bonita` |
|
|
9
|
-
| `dock` |
|
|
10
|
-
| `guapa` |
|
|
11
|
-
| `tasks` |
|
|
12
|
-
| `melon` |
|
|
13
|
-
| `tunnel` |
|
|
5
|
+
| Estilo | Cómo se ve |
|
|
6
|
+
|---|---|
|
|
7
|
+
| `clasica` | Barra lateral con borde; la página actual en una píldora rellena |
|
|
8
|
+
| `bonita` | Píldoras de texto centradas que se ensanchan al pasar y al estar activas (padding con resorte, 0.3 s `cubic(.38,.49,0,1.5)`) |
|
|
9
|
+
| `dock` | Barra flotante de cristal centrada abajo; el botón activo se ensancha a 80 px |
|
|
10
|
+
| `guapa` | La vista es una tarjeta sobre la cáscara tintada; la página actual es un chip blanco |
|
|
11
|
+
| `tasks` | Riel de botones redondos de 56 px; el activo se cuadra (radio 0.3 s `cubic(.38,.49,0,2)`) |
|
|
12
|
+
| `melon` | Barra lateral suave con un lavado primario en la página actual |
|
|
13
|
+
| `tunnel` | Palabras grandes; la que tiene el ratón y sus vecinas crecen como un dock (1.2 / 1.09 / 1.03) |
|
|
14
14
|
|
|
15
|
-
`accent`
|
|
16
|
-
|
|
17
|
-
Origen: Stepbro `navigation-styles.css`, `navigation.json`, componente `Nav` (Tasks).
|
|
15
|
+
`accent` pinta la página actual en primary-container. Al cambiar de página el icono se rellena (`mk-nav-icon`, 125 ms, `FILL 0 → 1`, para fuentes de iconos como Material Symbols). Con `reveal`, al arrancar la cáscara se descubre desde el primer botón (`revealFrom`).
|
|
18
16
|
|
|
19
17
|
## HTML / Astro
|
|
20
18
|
|
|
@@ -73,7 +71,7 @@ const [page, setPage] = useState('home');
|
|
|
73
71
|
|
|
74
72
|
- Al hacer clic se emite `mk:navigate` (cancelable; `detail`: `index`, `value`, `item`, `originalEvent`). Para una SPA, llama `event.detail.originalEvent.preventDefault()` y navega con tu router.
|
|
75
73
|
- `select()` solo marca la página actual (sin evento): úsalo para seguir a tu router.
|
|
76
|
-
- `storageKey` guarda el estilo elegido en `localStorage
|
|
74
|
+
- `storageKey` guarda el estilo elegido en `localStorage`.
|
|
77
75
|
- `NAV_STYLES` lista los siete nombres.
|
|
78
76
|
|
|
79
77
|
## Variables CSS
|
|
@@ -4,11 +4,11 @@ import { inert, warnOnce, type Instance } from '../../core/types';
|
|
|
4
4
|
import { revealFrom, type RevealHandle } from '../../motion/reveal';
|
|
5
5
|
|
|
6
6
|
/*
|
|
7
|
-
*
|
|
7
|
+
* navigation: seven desktop styles (Clásica, Bonita, Dock, Guapa,
|
|
8
8
|
* Tasks, Melon, Tunnel) and one glass bar on phones, all in CSS (nav.css). The
|
|
9
9
|
* engine marks the current page (aria-current="page"), switches styles (and can
|
|
10
|
-
* remember the choice
|
|
11
|
-
* app shell from the first button
|
|
10
|
+
* remember the choice), and on start can reveal the
|
|
11
|
+
* app shell from the first button.
|
|
12
12
|
* Parts: .mk-nav__buttons .mk-nav__button (data-mk-value or href), .mk-nav__button--first.
|
|
13
13
|
*/
|
|
14
14
|
|
|
@@ -18,7 +18,7 @@ export type NavStyle = (typeof NAV_STYLES)[number];
|
|
|
18
18
|
export interface NavOptions {
|
|
19
19
|
/** Default: the saved one (storageKey), else the mk-nav--<style> class in the markup, else 'bonita' (Money's default). */
|
|
20
20
|
style?: NavStyle;
|
|
21
|
-
/** The accent variant
|
|
21
|
+
/** The accent variant: the current page in primary-container. */
|
|
22
22
|
accent?: boolean;
|
|
23
23
|
/** Reveal the shell from the first button when the nav starts (off by default). */
|
|
24
24
|
reveal?: boolean;
|
|
@@ -65,7 +65,7 @@ const write = (key: string, value: string) => {
|
|
|
65
65
|
}
|
|
66
66
|
};
|
|
67
67
|
|
|
68
|
-
/**
|
|
68
|
+
/** nav entrance: the app shell is uncovered from the nav's first button. */
|
|
69
69
|
export function revealShell(nav: HTMLElement): RevealHandle | null {
|
|
70
70
|
if (!entrances()) return null;
|
|
71
71
|
const shell = nav.closest<HTMLElement>('.mk-shell') ?? nav.parentElement;
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* nav —
|
|
3
|
-
* mobile bar under 680 px, inside an app shell. Ported from Stepbro's
|
|
4
|
-
* `.app-nav.style-N` rules: .app-container → .mk-shell, .app-view → .mk-shell__view,
|
|
5
|
-
* .nav-button → .mk-nav__button, [active=true] → [aria-current="page"],
|
|
6
|
-
* variant-1 → .mk-nav--accent.
|
|
2
|
+
* nav — seven desktop styles and one mobile bar under 680 px, inside an app shell.
|
|
7
3
|
*/
|
|
8
4
|
|
|
9
5
|
/* Shell: the view and the nav side by side (the nav first), stacked on phones. */
|
|
@@ -10,7 +10,7 @@ export interface NavItem {
|
|
|
10
10
|
icon?: ReactNode;
|
|
11
11
|
/** Render a link instead of a button. */
|
|
12
12
|
href?: string;
|
|
13
|
-
/** Hidden in the phone bar
|
|
13
|
+
/** Hidden in the phone bar. */
|
|
14
14
|
hideOnMobile?: boolean;
|
|
15
15
|
}
|
|
16
16
|
|
|
@@ -29,7 +29,7 @@ export interface NavProps extends Omit<HTMLAttributes<HTMLElement>, 'onSelect'>
|
|
|
29
29
|
actions?: ReactNode;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
/**
|
|
32
|
+
/** navigation. Put it inside a <div className="mk-shell"> next to <main className="mk-shell__view">. */
|
|
33
33
|
export function Nav({
|
|
34
34
|
items,
|
|
35
35
|
active,
|
|
@@ -47,7 +47,7 @@ export function Nav({
|
|
|
47
47
|
if (!reveal || !ref.current) return;
|
|
48
48
|
const handle = revealShell(ref.current);
|
|
49
49
|
return () => handle?.cancel();
|
|
50
|
-
// Only on mount
|
|
50
|
+
// Only on mount.
|
|
51
51
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
52
52
|
}, []);
|
|
53
53
|
const classes = ['mk-nav', `mk-nav--${navStyle}`, accent && 'mk-nav--accent', className].filter(Boolean).join(' ');
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# pagination
|
|
2
|
+
|
|
3
|
+
Paginación con ventana acotada de números, extremos, elipsis y respuesta de elevación/presión.
|
|
4
|
+
|
|
5
|
+
```html
|
|
6
|
+
<mk-pagination class="mk-pagination" total-pages="30" page="4" role="navigation" aria-label="Artículos">
|
|
7
|
+
<button type="button" data-mk-pagination-previous aria-label="Página anterior">←</button>
|
|
8
|
+
<span data-mk-pagination-pages></span>
|
|
9
|
+
<button type="button" data-mk-pagination-next aria-label="Página siguiente">→</button>
|
|
10
|
+
</mk-pagination>
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`defineElements()`, `createPagination(root, options)`, o `<Pagination totalPages={30} page={page} onChange={setPage} />`.
|
|
14
|
+
|
|
15
|
+
Opciones: `totalPages=10`, `page=1`, `siblings=1` (0…10), `pageLabel(n)`, `onChange(n)`. Total mínimo 1. API: `setPage(n, notify=true)`, `setTotalPages(n)`, `page`, `totalPages`, `destroy()`. `mk:change` lleva `{page,totalPages}`. Los cambios del total acotan la página y conservan el foco.
|
|
16
|
+
|
|
17
|
+
Flechas izquierda/derecha, Home y End cambian la selección; botones nativos y aria-current=page. La aplicación decide cómo cargar los resultados. Sin animación con movimiento reducido.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { MkElement, defineElement, type OptionType } from '../../elements/base';
|
|
2
|
+
import { createPagination, type PaginationInstance } from './engine';
|
|
3
|
+
/** Light DOM: supply the same child markup shown in the README. */
|
|
4
|
+
export class MkPaginationElement extends MkElement<PaginationInstance> {
|
|
5
|
+
static options: Record<string, OptionType> = { totalPages: 'number', page: 'number', siblings: 'number' };
|
|
6
|
+
protected create(options: Record<string, unknown>) { return createPagination(this, options); }
|
|
7
|
+
attributeChangedCallback(name: string, previous: string | null, next: string | null) {
|
|
8
|
+
if (previous === next || !this.isConnected || !this.instance) return;
|
|
9
|
+
const page = this.instance.page;
|
|
10
|
+
this.instance.destroy();
|
|
11
|
+
this.instance = this.create({ ...(!this.hasAttribute('page') ? { page } : {}), ...this.options });
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export const definePagination = () => defineElement('mk-pagination', MkPaginationElement);
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { createScope } from '../../core/scope';
|
|
2
|
+
import { inert, type Instance } from '../../core/types';
|
|
3
|
+
|
|
4
|
+
export interface PaginationOptions {
|
|
5
|
+
totalPages?: number;
|
|
6
|
+
page?: number;
|
|
7
|
+
siblings?: number;
|
|
8
|
+
pageLabel?: (page: number) => string;
|
|
9
|
+
onChange?: (page: number) => void;
|
|
10
|
+
}
|
|
11
|
+
export interface PaginationInstance extends Instance {
|
|
12
|
+
setPage(page: number, notify?: boolean): void;
|
|
13
|
+
setTotalPages(total: number): void;
|
|
14
|
+
readonly page: number;
|
|
15
|
+
readonly totalPages: number;
|
|
16
|
+
}
|
|
17
|
+
// Reserve room for the page-window arithmetic so incrementing always progresses.
|
|
18
|
+
const positive = (n: number | undefined, fallback: number) => Number.isFinite(n) ? Math.max(1, Math.min(Number.MAX_SAFE_INTEGER - 32, Math.floor(n!))) : fallback;
|
|
19
|
+
|
|
20
|
+
/** Renders a bounded window of pages, even when totalPages is very large. */
|
|
21
|
+
export function createPagination(root: HTMLElement | null, options: PaginationOptions = {}): PaginationInstance {
|
|
22
|
+
const pages = root?.querySelector<HTMLElement>('[data-mk-pagination-pages]');
|
|
23
|
+
if (!root || !pages) return inert({ setPage() {}, setTotalPages() {}, page: 1, totalPages: 1 });
|
|
24
|
+
const scope = createScope();
|
|
25
|
+
const original = [...pages.childNodes];
|
|
26
|
+
const previous = root.querySelector<HTMLButtonElement>('[data-mk-pagination-previous]');
|
|
27
|
+
const next = root.querySelector<HTMLButtonElement>('[data-mk-pagination-next]');
|
|
28
|
+
let total = positive(options.totalPages, 10);
|
|
29
|
+
let page = Math.min(total, positive(options.page, 1));
|
|
30
|
+
const siblings = Math.min(10, Math.max(0, Number.isFinite(options.siblings) ? Math.floor(options.siblings!) : 1));
|
|
31
|
+
scope.attr(root, 'data-mk-ready', '');
|
|
32
|
+
const render = () => {
|
|
33
|
+
const hadFocus = pages.contains(document.activeElement);
|
|
34
|
+
const selected = new Set([1, total]);
|
|
35
|
+
for (let n = Math.max(1, page - siblings); n <= Math.min(total, page + siblings); n++) selected.add(n);
|
|
36
|
+
const fragment = document.createDocumentFragment();
|
|
37
|
+
let last = 0;
|
|
38
|
+
for (const n of [...selected].sort((a, b) => a - b)) {
|
|
39
|
+
if (last && n - last > 1) {
|
|
40
|
+
const gap = document.createElement('span');
|
|
41
|
+
gap.className = 'mk-pagination__gap';
|
|
42
|
+
gap.textContent = '…';
|
|
43
|
+
gap.setAttribute('aria-hidden', 'true');
|
|
44
|
+
fragment.append(gap);
|
|
45
|
+
}
|
|
46
|
+
const button = document.createElement('button');
|
|
47
|
+
button.type = 'button';
|
|
48
|
+
button.dataset.mkPage = String(n);
|
|
49
|
+
button.textContent = String(n);
|
|
50
|
+
button.setAttribute('aria-label', options.pageLabel?.(n) ?? `Página ${n}`);
|
|
51
|
+
if (n === page) button.setAttribute('aria-current', 'page');
|
|
52
|
+
fragment.append(button);
|
|
53
|
+
last = n;
|
|
54
|
+
}
|
|
55
|
+
pages.replaceChildren(fragment);
|
|
56
|
+
if (hadFocus) pages.querySelector<HTMLButtonElement>('[aria-current]')?.focus();
|
|
57
|
+
if (previous) scope.attr(previous, 'disabled', page === 1 ? '' : null);
|
|
58
|
+
if (next) scope.attr(next, 'disabled', page === total ? '' : null);
|
|
59
|
+
};
|
|
60
|
+
const setPage = (value: number, notify = true) => {
|
|
61
|
+
if (scope.disposed || !Number.isFinite(value)) return;
|
|
62
|
+
const candidate = Math.max(1, Math.min(total, Math.round(value)));
|
|
63
|
+
if (candidate === page) return;
|
|
64
|
+
const focused = pages.contains(document.activeElement);
|
|
65
|
+
page = candidate;
|
|
66
|
+
render();
|
|
67
|
+
if (focused) pages.querySelector<HTMLButtonElement>('[aria-current]')?.focus();
|
|
68
|
+
if (notify) {
|
|
69
|
+
options.onChange?.(page);
|
|
70
|
+
root.dispatchEvent(new CustomEvent('mk:change', { bubbles: true, detail: { page, totalPages: total } }));
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
scope.on(root, 'click', (event) => {
|
|
74
|
+
const target = (event.target as Element).closest<HTMLButtonElement>('button');
|
|
75
|
+
if (!target || target.disabled) return;
|
|
76
|
+
if (target === previous) setPage(page - 1);
|
|
77
|
+
else if (target === next) setPage(page + 1);
|
|
78
|
+
else if (target.dataset.mkPage) setPage(Number(target.dataset.mkPage));
|
|
79
|
+
});
|
|
80
|
+
scope.on(root, 'keydown', (event) => {
|
|
81
|
+
if (!(event.target instanceof HTMLButtonElement)) return;
|
|
82
|
+
const to = event.key === 'ArrowRight' ? page + 1 : event.key === 'ArrowLeft' ? page - 1 : event.key === 'Home' ? 1 : event.key === 'End' ? total : null;
|
|
83
|
+
if (to === null) return;
|
|
84
|
+
event.preventDefault();
|
|
85
|
+
setPage(to);
|
|
86
|
+
pages.querySelector<HTMLButtonElement>('[aria-current]')?.focus();
|
|
87
|
+
});
|
|
88
|
+
scope.add(() => pages.replaceChildren(...original));
|
|
89
|
+
render();
|
|
90
|
+
return {
|
|
91
|
+
setPage,
|
|
92
|
+
setTotalPages(value) {
|
|
93
|
+
if (scope.disposed || !Number.isFinite(value)) return;
|
|
94
|
+
total = positive(value, 1);
|
|
95
|
+
if (page > total) setPage(total);
|
|
96
|
+
else render();
|
|
97
|
+
},
|
|
98
|
+
get page() { return page; },
|
|
99
|
+
get totalPages() { return total; },
|
|
100
|
+
destroy: () => scope.dispose(),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
.mk-pagination, mk-pagination { display: flex; flex-wrap: wrap; justify-content: center; align-items: center; gap: .35rem; }
|
|
2
|
+
.mk-pagination [data-mk-pagination-pages], mk-pagination [data-mk-pagination-pages] { display: flex; align-items: center; gap: .25rem; }
|
|
3
|
+
.mk-pagination button, mk-pagination button { min-width: 2.5rem; height: 2.5rem; border: 0; border-radius: var(--mk-radius-full); background: var(--mk-surface-2); color: var(--mk-text); font: inherit; cursor: pointer; transition: transform .3s, background .3s, color .3s; }
|
|
4
|
+
.mk-pagination button[aria-current], mk-pagination button[aria-current] { background: var(--mk-accent); color: var(--mk-on-accent); }
|
|
5
|
+
.mk-pagination button:hover:not(:disabled), mk-pagination button:hover:not(:disabled) { transform: translateY(-3px); }
|
|
6
|
+
.mk-pagination button:active:not(:disabled), mk-pagination button:active:not(:disabled) { transform: scale(.9); }
|
|
7
|
+
.mk-pagination button:focus-visible, mk-pagination button:focus-visible { outline: 2px solid var(--mk-accent); outline-offset: 3px; }
|
|
8
|
+
.mk-pagination button:disabled, mk-pagination button:disabled { opacity: .35; cursor: default; }
|
|
9
|
+
.mk-pagination__gap { padding: 0 .15rem; color: var(--mk-muted); }
|
|
10
|
+
@media (prefers-reduced-motion: reduce) { .mk-pagination button, mk-pagination button { transition: none; transform: none !important; } }
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useRef, type HTMLAttributes } from 'react';
|
|
3
|
+
import { useIsomorphicLayoutEffect, useLatest } from '../../react/use-recipe';
|
|
4
|
+
import { createPagination, type PaginationOptions } from './engine';
|
|
5
|
+
export interface PaginationProps extends Omit<HTMLAttributes<HTMLElement>, 'onChange'>, PaginationOptions {
|
|
6
|
+
previousLabel?: string; nextLabel?: string;
|
|
7
|
+
}
|
|
8
|
+
export function Pagination({ totalPages, page, siblings, pageLabel, onChange, previousLabel = 'Página anterior', nextLabel = 'Página siguiente', className, ...props }: PaginationProps) {
|
|
9
|
+
const ref = useRef<HTMLElement>(null);
|
|
10
|
+
const selected = useRef<number | undefined>(undefined);
|
|
11
|
+
const change = useLatest(onChange);
|
|
12
|
+
const label = useLatest(pageLabel);
|
|
13
|
+
useIsomorphicLayoutEffect(() => {
|
|
14
|
+
const instance = createPagination(ref.current, { totalPages, page: page ?? selected.current, siblings, pageLabel: (n) => label.current?.(n) ?? `Página ${n}`, onChange: (n) => change.current?.(n) });
|
|
15
|
+
return () => { selected.current = instance.page; instance.destroy(); };
|
|
16
|
+
}, [totalPages, page, siblings, pageLabel]);
|
|
17
|
+
return <nav aria-label="Paginación" {...props} ref={ref} className={['mk-pagination', className].filter(Boolean).join(' ')}>
|
|
18
|
+
<button type="button" data-mk-pagination-previous aria-label={previousLabel}>←</button>
|
|
19
|
+
<span data-mk-pagination-pages />
|
|
20
|
+
<button type="button" data-mk-pagination-next aria-label={nextLabel}>→</button>
|
|
21
|
+
</nav>;
|
|
22
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# quantity
|
|
2
|
+
|
|
3
|
+
Selector inspirado en [Bernice](https://bernicebakery.com/collections/cakes). Botones nativos, mordida geométrica propia y giro de 15° en .4 s. La forma adapta el original; no usa su SVG.
|
|
4
|
+
|
|
5
|
+
```html
|
|
6
|
+
<mk-quantity class="mk-quantity" min="1" max="9">
|
|
7
|
+
<button type="button" data-mk-quantity-decrease aria-label="Disminuir">−</button>
|
|
8
|
+
<input data-mk-quantity-input type="number" value="1" aria-label="Cantidad" />
|
|
9
|
+
<button type="button" data-mk-quantity-increase aria-label="Aumentar">+</button>
|
|
10
|
+
</mk-quantity>
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Registrar con `defineElements()` de `/elements`, o usar `createQuantity(root, options)`. React: `<Quantity min={1} max={9} value={value} onChange={setValue} />` desde `/react`.
|
|
14
|
+
|
|
15
|
+
Opciones: `min=1`, `max=999`, `step=1`, `value`, `disabled`, `onChange(value)`. API: `setValue(number, notify=true)`, `value`, `destroy()`. El input acepta edición: un número válido y dentro del rango se aplica mientras escribes (sin tocar el texto); uno parcial o fuera de rango se ajusta al paso desde min y se acota al confirmar o salir del campo. Evento `mk:change` con `{value, previous}`. No realiza operaciones de carrito.
|
|
16
|
+
|
|
17
|
+
Variables: `--mk-quantity-size` (3rem), colores semánticos. Movimiento reducido conserva controles sin rotación. React acepta label/decreaseLabel/increaseLabel/name y props HTML. Desmontar llama destroy y restaura marcado/valores originales.
|
|
18
|
+
|
|
19
|
+
El reset nativo del formulario sincroniza el control sin emitir callbacks de edición. Los atributos de custom elements conservan el estado editado si value no está presente; value explícito tiene precedencia.
|
|
20
|
+
Si max no coincide con un paso desde min, el límite efectivo es el mayor min+n×step que no supere max, para mantener validez HTML nativa.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { MkElement, defineElement, type OptionType } from '../../elements/base';
|
|
2
|
+
import { createQuantity, type QuantityInstance } from './engine';
|
|
3
|
+
/** Light DOM: supply the same child markup shown in the README. */
|
|
4
|
+
export class MkQuantityElement extends MkElement<QuantityInstance> {
|
|
5
|
+
static options: Record<string, OptionType> = { min: 'number', max: 'number', step: 'number', value: 'number', disabled: 'boolean' };
|
|
6
|
+
protected create(options: Record<string, unknown>) { return createQuantity(this, options); }
|
|
7
|
+
attributeChangedCallback(name: string, previous: string | null, next: string | null) {
|
|
8
|
+
if (previous === next || !this.isConnected || !this.instance) return;
|
|
9
|
+
const value = this.instance.value;
|
|
10
|
+
this.instance.destroy();
|
|
11
|
+
this.instance = this.create({ ...(!this.hasAttribute('value') ? { value } : {}), ...this.options });
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export const defineQuantity = () => defineElement('mk-quantity', MkQuantityElement);
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { createScope } from '../../core/scope';
|
|
2
|
+
import { inert, type Instance } from '../../core/types';
|
|
3
|
+
|
|
4
|
+
export interface QuantityOptions {
|
|
5
|
+
min?: number;
|
|
6
|
+
max?: number;
|
|
7
|
+
step?: number;
|
|
8
|
+
value?: number;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
onChange?: (value: number) => void;
|
|
11
|
+
}
|
|
12
|
+
export interface QuantityInstance extends Instance {
|
|
13
|
+
setValue(value: number, notify?: boolean): void;
|
|
14
|
+
readonly value: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Bounded numeric control. Bite/rotation is CSS; buttons remain native. */
|
|
18
|
+
export function createQuantity(root: HTMLElement | null, options: QuantityOptions = {}): QuantityInstance {
|
|
19
|
+
const input = root?.querySelector<HTMLInputElement>('[data-mk-quantity-input]');
|
|
20
|
+
if (!root || !input) return inert({ setValue() {}, value: 0 });
|
|
21
|
+
const scope = createScope();
|
|
22
|
+
const min = Number.isFinite(options.min) ? options.min! : 1;
|
|
23
|
+
const max = Number.isFinite(options.max) ? Math.max(min, options.max!) : Math.max(min, 999);
|
|
24
|
+
const step = Number.isFinite(options.step) && options.step! > 0 ? options.step! : 1;
|
|
25
|
+
const steps = Math.floor((max - min) / step + 1e-9);
|
|
26
|
+
const upper = Math.min(max, Number((min + steps * step).toFixed(10)));
|
|
27
|
+
const normalize = (n: number) => Math.max(min, Math.min(upper, Number((min + Math.round((n - min) / step) * step).toFixed(10))));
|
|
28
|
+
const original = input.value;
|
|
29
|
+
const disabled = options.disabled ?? input.disabled;
|
|
30
|
+
const decrease = root.querySelector<HTMLButtonElement>('[data-mk-quantity-decrease]');
|
|
31
|
+
const increase = root.querySelector<HTMLButtonElement>('[data-mk-quantity-increase]');
|
|
32
|
+
scope.attr(input, 'min', String(min));
|
|
33
|
+
scope.attr(input, 'max', String(max));
|
|
34
|
+
scope.attr(input, 'step', String(step));
|
|
35
|
+
scope.attr(input, 'disabled', disabled ? '' : null);
|
|
36
|
+
scope.attr(root, 'data-mk-ready', '');
|
|
37
|
+
const initial = input.value.trim() ? Number(input.value) : NaN;
|
|
38
|
+
let value = normalize(Number.isFinite(options.value) ? options.value! : Number.isFinite(initial) ? initial : min);
|
|
39
|
+
let pendingReset: Event | null = null;
|
|
40
|
+
const typed = () => (input.value.trim() && Number.isFinite(input.valueAsNumber) ? input.valueAsNumber : NaN);
|
|
41
|
+
const render = (syncInput = true) => {
|
|
42
|
+
if (pendingReset) {
|
|
43
|
+
if (pendingReset.eventPhase !== 0) return;
|
|
44
|
+
if (!pendingReset.defaultPrevented) value = normalize(Number.isFinite(typed()) ? typed() : min);
|
|
45
|
+
pendingReset = null;
|
|
46
|
+
}
|
|
47
|
+
if (syncInput) input.value = String(value);
|
|
48
|
+
if (decrease) scope.attr(decrease, 'disabled', disabled || value <= min ? '' : null);
|
|
49
|
+
if (increase) scope.attr(increase, 'disabled', disabled || value >= upper ? '' : null);
|
|
50
|
+
};
|
|
51
|
+
const commit = (next: number, notify: boolean, syncInput: boolean) => {
|
|
52
|
+
if (scope.disposed || !Number.isFinite(next)) return;
|
|
53
|
+
if (pendingReset?.eventPhase === 0) pendingReset = null;
|
|
54
|
+
const previous = value;
|
|
55
|
+
value = normalize(next);
|
|
56
|
+
render(syncInput);
|
|
57
|
+
if (notify && value !== previous) {
|
|
58
|
+
options.onChange?.(value);
|
|
59
|
+
root.dispatchEvent(new CustomEvent('mk:change', { bubbles: true, detail: { value, previous } }));
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
const setValue = (next: number, notify = true) => commit(next, notify, true);
|
|
63
|
+
if (decrease) scope.on(decrease, 'click', () => { if (!decrease.disabled) setValue(value - step); });
|
|
64
|
+
if (increase) scope.on(increase, 'click', () => { if (!increase.disabled) setValue(value + step); });
|
|
65
|
+
// While typing, commit only numbers that are already valid and leave the text alone;
|
|
66
|
+
// partial or out-of-range input settles on change/blur.
|
|
67
|
+
scope.on(input, 'input', () => {
|
|
68
|
+
const n = typed();
|
|
69
|
+
if (Number.isFinite(n) && normalize(n) === n) commit(n, true, false);
|
|
70
|
+
});
|
|
71
|
+
scope.on(input, 'change', () => {
|
|
72
|
+
if (Number.isFinite(typed())) setValue(typed());
|
|
73
|
+
else render();
|
|
74
|
+
});
|
|
75
|
+
scope.on(input, 'blur', () => render());
|
|
76
|
+
if (input.form) scope.on(input.form, 'reset', (event) => {
|
|
77
|
+
// reset's native default action runs after its listeners. Match the final input silently.
|
|
78
|
+
render();
|
|
79
|
+
pendingReset = event;
|
|
80
|
+
scope.timeout(() => { if (pendingReset === event) render(); }, 0);
|
|
81
|
+
});
|
|
82
|
+
scope.add(() => { input.value = original; });
|
|
83
|
+
render();
|
|
84
|
+
return { setValue, get value() { return value; }, destroy: () => scope.dispose() };
|
|
85
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
.mk-quantity, mk-quantity { display: inline-flex; align-items: center; gap: .5rem; --mk-quantity-size: 3rem; }
|
|
2
|
+
.mk-quantity [data-mk-quantity-decrease], .mk-quantity [data-mk-quantity-increase], mk-quantity button {
|
|
3
|
+
width: var(--mk-quantity-size); height: var(--mk-quantity-size); border: 0; border-radius: 50%;
|
|
4
|
+
background: var(--mk-secondary-soft); color: var(--mk-on-secondary-soft); font: inherit; font-size: 1.65rem;
|
|
5
|
+
cursor: pointer; transition: transform .4s cubic-bezier(.28,.71,0,.98), opacity .2s;
|
|
6
|
+
}
|
|
7
|
+
.mk-quantity [data-mk-quantity-increase], mk-quantity [data-mk-quantity-increase] { background: var(--mk-accent-soft); color: var(--mk-on-accent-soft); }
|
|
8
|
+
.mk-quantity button:is(:hover,:focus-visible):not(:disabled), mk-quantity button:is(:hover,:focus-visible):not(:disabled) {
|
|
9
|
+
transform: rotate(15deg); mask-image: radial-gradient(circle at 100% 0, transparent 22%, #000 23%);
|
|
10
|
+
}
|
|
11
|
+
.mk-quantity button:focus-visible, mk-quantity button:focus-visible { outline: 2px solid var(--mk-accent); outline-offset: 4px; }
|
|
12
|
+
.mk-quantity button:disabled, mk-quantity button:disabled { opacity: .4; cursor: default; }
|
|
13
|
+
.mk-quantity input, mk-quantity input { width: 3.5rem; padding: .4rem 0; border: 0; border-radius: var(--mk-radius-sm); text-align: center; font: inherit; font-variant-numeric: tabular-nums; background: transparent; color: var(--mk-text); appearance: textfield; }
|
|
14
|
+
.mk-quantity input::-webkit-inner-spin-button, mk-quantity input::-webkit-inner-spin-button { appearance: none; }
|
|
15
|
+
@media (prefers-reduced-motion: reduce) { .mk-quantity button, mk-quantity button { transition: none; transform: none !important; } }
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useRef, type HTMLAttributes } from 'react';
|
|
3
|
+
import { useIsomorphicLayoutEffect, useLatest } from '../../react/use-recipe';
|
|
4
|
+
import { createQuantity, type QuantityOptions } from './engine';
|
|
5
|
+
export interface QuantityProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange' | 'defaultValue'>, QuantityOptions {
|
|
6
|
+
label?: string; decreaseLabel?: string; increaseLabel?: string; name?: string;
|
|
7
|
+
}
|
|
8
|
+
export function Quantity({ min, max, step, value, disabled, onChange, label = 'Cantidad', decreaseLabel = 'Disminuir', increaseLabel = 'Aumentar', name, className, ...props }: QuantityProps) {
|
|
9
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
10
|
+
const edited = useRef<number | undefined>(undefined);
|
|
11
|
+
const change = useLatest(onChange);
|
|
12
|
+
useIsomorphicLayoutEffect(() => {
|
|
13
|
+
const instance = createQuantity(ref.current, { min, max, step, value: value ?? edited.current, disabled, onChange: (n) => change.current?.(n) });
|
|
14
|
+
return () => { edited.current = instance.value; instance.destroy(); };
|
|
15
|
+
}, [min, max, step, value, disabled]);
|
|
16
|
+
return <div {...props} ref={ref} className={['mk-quantity', className].filter(Boolean).join(' ')} role="group" aria-label={label}>
|
|
17
|
+
<button type="button" data-mk-quantity-decrease aria-label={decreaseLabel}>−</button>
|
|
18
|
+
<input data-mk-quantity-input type="number" aria-label={label} name={name} defaultValue={value ?? min ?? 1} disabled={disabled} />
|
|
19
|
+
<button type="button" data-mk-quantity-increase aria-label={increaseLabel}>+</button>
|
|
20
|
+
</div>;
|
|
21
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# slot-picker
|
|
2
|
+
|
|
3
|
+
El selector de citas de Melon Mind («Agendar cita»): a la izquierda un mes compacto y a la derecha los horarios del día elegido. Sirve para reservas, citas o mesas.
|
|
4
|
+
|
|
5
|
+
- **Cambiar de día:** los horarios salen 30 px hacia un lado y entran 30 px desde el otro (0.25 s `ease-out`), en la dirección del cambio. El original usa una View Transition `time-slot`.
|
|
6
|
+
- **Primera lista:** entra creciendo (escala .7, blur 32 → 0, 0.3 s `cubic-bezier(.38,.49,0,1.1)`), 20 ms entre horarios.
|
|
7
|
+
- **Encabezado del día:** se abre de 0 a 56 px en 0.5 s `cubic-bezier(.38,.49,0,1.4)`.
|
|
8
|
+
- **Horario elegido:** se ilumina en 0.7 s (escala .8 → 1, blur 16 → 0).
|
|
9
|
+
- **Cambiar de mes:** los días se deslizan igual que los horarios.
|
|
10
|
+
|
|
11
|
+
Origen: REA de la app de Melon Mind (`appt-creator.css`, `schedulePicker.css`, `CreateApptManager.js`).
|
|
12
|
+
|
|
13
|
+
## HTML / Astro
|
|
14
|
+
|
|
15
|
+
```html
|
|
16
|
+
<mk-slot-picker class="mk-slot-picker" locale="es" min="2026-10-23" closed-days="[0]"
|
|
17
|
+
slots='["09:00","10:00","11:30","16:00","17:30"]'>
|
|
18
|
+
<div class="mk-slot-picker__calendar">
|
|
19
|
+
<div class="mk-slot-picker__month">
|
|
20
|
+
<h3 class="mk-slot-picker__title" data-mk-slot-title></h3>
|
|
21
|
+
<button type="button" class="mk-slot-picker__nav" data-mk-slot-prev aria-label="Mes anterior">←</button>
|
|
22
|
+
<button type="button" class="mk-slot-picker__nav" data-mk-slot-next aria-label="Mes siguiente">→</button>
|
|
23
|
+
</div>
|
|
24
|
+
<div data-mk-slot-days></div>
|
|
25
|
+
</div>
|
|
26
|
+
<div class="mk-slot-picker__panel">
|
|
27
|
+
<p class="mk-slot-picker__heading" data-mk-slot-heading></p>
|
|
28
|
+
<div data-mk-slot-times></div>
|
|
29
|
+
<input type="hidden" name="cita" data-mk-slot-value>
|
|
30
|
+
</div>
|
|
31
|
+
</mk-slot-picker>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
El input oculto `[data-mk-slot-value]` lleva `YYYY-MM-DDTHH:mm` y emite `input`/`change`, así que funciona dentro de un `<form>`. Si cambias el atributo `slots`, la lista se actualiza sin perder el día. Sin custom element: `createSlotPicker(root, opciones)`.
|
|
35
|
+
|
|
36
|
+
## React
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
<SlotPicker
|
|
40
|
+
name="cita"
|
|
41
|
+
min={new Date()}
|
|
42
|
+
closedDays={[0]}
|
|
43
|
+
slots={(day) => fetch(`/api/horarios?dia=${dayKey(day)}`).then((r) => r.json())}
|
|
44
|
+
onChange={({ value }) => setCita(value)}
|
|
45
|
+
actions={<button className="mk-button">Agendar</button>}
|
|
46
|
+
/>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Opciones
|
|
50
|
+
|
|
51
|
+
| Opción | Atributo | Por defecto | Qué hace |
|
|
52
|
+
|---|---|---|---|
|
|
53
|
+
| `slots` | `slots` (JSON) | `[]` | Lista igual todos los días, mapa `{"2026-10-23": [...]}` o función `(día) => lista \| Promise<lista>`. Horas `HH:mm` |
|
|
54
|
+
| `date` | `date` | hoy | Mes inicial (y día elegido si no hay `value`) |
|
|
55
|
+
| `value` | `value` | — | `"2026-10-23T10:30"` o `{ date, time }` |
|
|
56
|
+
| `min`, `max` | `min`, `max` | — | Días fuera de rango deshabilitados; flechas de mes limitadas |
|
|
57
|
+
| `closedDays` | `closed-days` (JSON) | `[]` | Días de la semana cerrados (0 = domingo) |
|
|
58
|
+
| `isDisabled(date)` | — | — | Regla propia |
|
|
59
|
+
| `firstDay` | `first-day` | `0` | Primer día de la semana |
|
|
60
|
+
| `locale` | `locale` | `'es'` | Nombres de mes y día |
|
|
61
|
+
| `labels` | — | español | `placeholder`, `empty`, `loading`, `heading(date)` |
|
|
62
|
+
| `onChange({date, time, key, value})`, `onDateChange(date)` | — | — | Callbacks |
|
|
63
|
+
|
|
64
|
+
API: `selectDate(d)`, `selectTime(t | null)`, `setSlots(fuente)`, `date`, `time`, `value`, `destroy()`. `mk:change` lleva `{date, time, key, value}`. Si los horarios llegan por promesa, mientras cargan se muestra `loading` con `aria-busy`, y una respuesta vieja nunca pisa a la más reciente.
|
|
65
|
+
|
|
66
|
+
## Accesibilidad
|
|
67
|
+
|
|
68
|
+
Los días forman una rejilla ARIA con foco móvil: flechas, Inicio/Fin y RePág/AvPág para cambiar de mes. Los horarios son un `radiogroup`: las flechas mueven la selección, y el grupo está etiquetado por el encabezado del día. Los días deshabilitados son botones `disabled`. Con movimiento reducido no hay deslizamiento, entradas ni resaltado animado.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { MkElement, defineElement, type OptionType } from '../../elements/base';
|
|
2
|
+
import { createSlotPicker, type SlotPickerInstance, type SlotSource } from './engine';
|
|
3
|
+
|
|
4
|
+
/** Light DOM: supply the [data-mk-slot-days] and [data-mk-slot-times] markup shown in the README. */
|
|
5
|
+
export class MkSlotPickerElement extends MkElement<SlotPickerInstance> {
|
|
6
|
+
static options: Record<string, OptionType> = {
|
|
7
|
+
date: 'string', value: 'string', slots: 'json', min: 'string', max: 'string', closedDays: 'json', firstDay: 'number', locale: 'string',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
protected create(options: Record<string, unknown>) {
|
|
11
|
+
return createSlotPicker(this, options);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
attributeChangedCallback(name: string, previous: string | null, next: string | null) {
|
|
15
|
+
if (previous === next || !this.isConnected || !this.instance) return;
|
|
16
|
+
if (name === 'slots') {
|
|
17
|
+
this.instance.setSlots((this.options.slots as SlotSource | undefined) ?? []);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
// Keep the visitor's pick unless the value itself changed.
|
|
21
|
+
const value = this.instance.value ?? (this.instance.date ? this.instance.date : undefined);
|
|
22
|
+
this.instance.destroy();
|
|
23
|
+
this.instance = this.create({ ...(name !== 'value' && value ? (typeof value === 'string' ? { value } : { date: value }) : {}), ...this.options });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const defineSlotPicker = () => defineElement('mk-slot-picker', MkSlotPickerElement);
|