@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
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import { gsap, registerMotion } from '../../core/gsap';
|
|
2
|
+
import { reduced } from '../../core/reduced';
|
|
3
|
+
import { createScope } from '../../core/scope';
|
|
4
|
+
import { inert, type Instance } from '../../core/types';
|
|
5
|
+
import {
|
|
6
|
+
addDays, addMonths, capitalize, clampFirstDay, dayKey, monthWeeks, safeFormat, sameDay, sameMonth,
|
|
7
|
+
fromJson, startOfWeek, toDate, toDateTime, weekDays, weekdayNames, type DateInput,
|
|
8
|
+
} from '../../core/dates';
|
|
9
|
+
import { flowIn } from '../../motion/flow';
|
|
10
|
+
|
|
11
|
+
/*
|
|
12
|
+
* Calendar (Melon Mind, section-calendar-test): month and week grids with
|
|
13
|
+
* event chips. Motion from the reference: on first load the day cells flow in
|
|
14
|
+
* (search-result-item-in: y −20, blur 16, .5 s) 5 ms apart and the events
|
|
15
|
+
* 20 ms apart (CalendarComponent eventsSet → flowChilds). Changing month or
|
|
16
|
+
* week slides the grid like Melon Mind's time slots: out −30 px / in +30 px,
|
|
17
|
+
* .25 s ease-out (schedulePicker.css view transitions); the old grid leaves
|
|
18
|
+
* first (.12 s) so two dense months never overlap. Keyboard: an ARIA grid
|
|
19
|
+
* with a roving day button.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
export type CalendarView = 'month' | 'week';
|
|
23
|
+
export type CalendarEventStatus = 'pending' | 'done' | 'cancelled';
|
|
24
|
+
|
|
25
|
+
export interface CalendarEvent {
|
|
26
|
+
id: string | number;
|
|
27
|
+
title: string;
|
|
28
|
+
start: DateInput;
|
|
29
|
+
end?: DateInput;
|
|
30
|
+
allDay?: boolean;
|
|
31
|
+
status?: CalendarEventStatus;
|
|
32
|
+
/** Any CSS colour for the chip. */
|
|
33
|
+
color?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface CalendarRange { start: Date; end: Date; view: CalendarView }
|
|
37
|
+
|
|
38
|
+
export interface CalendarLabels {
|
|
39
|
+
more?: (count: number) => string;
|
|
40
|
+
events?: (count: number) => string;
|
|
41
|
+
status?: Partial<Record<CalendarEventStatus, string>>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface CalendarOptions {
|
|
45
|
+
/** Date the calendar opens on. */
|
|
46
|
+
date?: DateInput;
|
|
47
|
+
view?: CalendarView;
|
|
48
|
+
/** 0 = Sunday … 6 = Saturday. */
|
|
49
|
+
firstDay?: number;
|
|
50
|
+
locale?: string;
|
|
51
|
+
events?: CalendarEvent[];
|
|
52
|
+
/** Chips per day in the month view before "+N". */
|
|
53
|
+
maxEvents?: number;
|
|
54
|
+
selected?: DateInput | null;
|
|
55
|
+
/** Override "today" (tests, demos in another zone). */
|
|
56
|
+
today?: DateInput;
|
|
57
|
+
labels?: CalendarLabels;
|
|
58
|
+
onSelect?: (date: Date) => void;
|
|
59
|
+
onEventClick?: (event: CalendarEvent) => void;
|
|
60
|
+
onNavigate?: (range: CalendarRange) => void;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface CalendarInstance extends Instance {
|
|
64
|
+
next(): void;
|
|
65
|
+
prev(): void;
|
|
66
|
+
goToday(): void;
|
|
67
|
+
setDate(date: DateInput): void;
|
|
68
|
+
setView(view: CalendarView): void;
|
|
69
|
+
setEvents(events: CalendarEvent[]): void;
|
|
70
|
+
select(date: DateInput | null, notify?: boolean): void;
|
|
71
|
+
readonly date: Date;
|
|
72
|
+
readonly view: CalendarView;
|
|
73
|
+
readonly selected: Date | null;
|
|
74
|
+
readonly range: CalendarRange;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const SLIDE = 30;
|
|
78
|
+
const SLIDE_OUT = 0.12;
|
|
79
|
+
const SLIDE_GAP = 0.1;
|
|
80
|
+
const SLIDE_DURATION = 0.25;
|
|
81
|
+
const CELL_STAGGER = 0.005;
|
|
82
|
+
const EVENT_STAGGER = 0.02;
|
|
83
|
+
|
|
84
|
+
export function createCalendar(root: HTMLElement | null, options: CalendarOptions = {}): CalendarInstance {
|
|
85
|
+
const grid = root?.querySelector<HTMLElement>('[data-mk-calendar-grid]');
|
|
86
|
+
if (!root || !grid) {
|
|
87
|
+
const now = toDate(null);
|
|
88
|
+
return inert({
|
|
89
|
+
next() {}, prev() {}, goToday() {}, setDate() {}, setView() {}, setEvents() {}, select() {},
|
|
90
|
+
date: now, view: 'month' as CalendarView, selected: null, range: { start: now, end: now, view: 'month' as CalendarView },
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
registerMotion();
|
|
94
|
+
const scope = createScope();
|
|
95
|
+
const ctx = gsap.context(() => {});
|
|
96
|
+
scope.add(() => ctx.revert());
|
|
97
|
+
const locale = options.locale ?? 'es';
|
|
98
|
+
const firstDay = clampFirstDay(options.firstDay);
|
|
99
|
+
const maxEvents = Number.isFinite(options.maxEvents) ? Math.max(0, Math.floor(options.maxEvents!)) : 3;
|
|
100
|
+
const labels = {
|
|
101
|
+
more: options.labels?.more ?? ((n: number) => `+${n} más`),
|
|
102
|
+
events: options.labels?.events ?? ((n: number) => (n === 1 ? '1 evento' : `${n} eventos`)),
|
|
103
|
+
status: { pending: 'pendiente', done: 'completado', cancelled: 'cancelado', ...options.labels?.status },
|
|
104
|
+
};
|
|
105
|
+
let date = toDate(options.date);
|
|
106
|
+
let view: CalendarView = options.view === 'week' ? 'week' : 'month';
|
|
107
|
+
let selected: Date | null = options.selected ? toDate(options.selected) : null;
|
|
108
|
+
let focusDay: Date = selected ?? date;
|
|
109
|
+
let events: CalendarEvent[] = [];
|
|
110
|
+
let byDay = new Map<string, CalendarEvent[]>();
|
|
111
|
+
let first = true;
|
|
112
|
+
const today = () => toDate(options.today ?? null);
|
|
113
|
+
|
|
114
|
+
const title = root.querySelector<HTMLElement>('[data-mk-calendar-title]');
|
|
115
|
+
const prevButton = root.querySelector<HTMLButtonElement>('[data-mk-calendar-prev]');
|
|
116
|
+
const nextButton = root.querySelector<HTMLButtonElement>('[data-mk-calendar-next]');
|
|
117
|
+
const todayButton = root.querySelector<HTMLButtonElement>('[data-mk-calendar-today]');
|
|
118
|
+
const viewButtons = [...root.querySelectorAll<HTMLButtonElement>('[data-mk-calendar-view]')];
|
|
119
|
+
|
|
120
|
+
const originalGrid = [...grid.childNodes];
|
|
121
|
+
const originalTitle = title ? [...title.childNodes] : [];
|
|
122
|
+
scope.add(() => {
|
|
123
|
+
grid.replaceChildren(...originalGrid);
|
|
124
|
+
title?.replaceChildren(...originalTitle);
|
|
125
|
+
});
|
|
126
|
+
scope.attr(root, 'data-mk-ready', '');
|
|
127
|
+
scope.attr(grid, 'role', 'grid');
|
|
128
|
+
if (title) {
|
|
129
|
+
if (!title.id) scope.attr(title, 'id', `mk-calendar-${Math.random().toString(36).slice(2, 8)}`);
|
|
130
|
+
scope.attr(grid, 'aria-labelledby', title.id);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const monthTitle = safeFormat(locale, { month: 'long', year: 'numeric' });
|
|
134
|
+
const rangeTitle = safeFormat(locale, { day: 'numeric', month: 'short', year: 'numeric' });
|
|
135
|
+
const fullDay = safeFormat(locale, { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' });
|
|
136
|
+
const timeFormat = safeFormat(locale, { hour: 'numeric', minute: '2-digit' });
|
|
137
|
+
|
|
138
|
+
const index = (list: CalendarEvent[]) => {
|
|
139
|
+
events = Array.isArray(list) ? list.filter((e) => e && toDateTime(e.start)) : [];
|
|
140
|
+
byDay = new Map();
|
|
141
|
+
const sorted = [...events].sort((a, b) => toDateTime(a.start)!.getTime() - toDateTime(b.start)!.getTime());
|
|
142
|
+
for (const event of sorted) {
|
|
143
|
+
const start = toDate(toDateTime(event.start)!);
|
|
144
|
+
const endTime = event.end ? toDateTime(event.end) : null;
|
|
145
|
+
const end = endTime && endTime > start ? toDate(endTime) : start;
|
|
146
|
+
// Multi-day events show on every day they cover (at most two months).
|
|
147
|
+
for (let day = start, n = 0; day <= end && n < 62; day = addDays(day, 1), n++) {
|
|
148
|
+
const key = dayKey(day);
|
|
149
|
+
byDay.set(key, [...(byDay.get(key) ?? []), event]);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
index(fromJson<CalendarEvent[]>(options.events) ?? []);
|
|
154
|
+
|
|
155
|
+
const days = () => (view === 'week' ? [weekDays(date, firstDay)] : monthWeeks(date, firstDay));
|
|
156
|
+
const range = (): CalendarRange => {
|
|
157
|
+
const weeks = days();
|
|
158
|
+
return { start: weeks[0][0], end: weeks[weeks.length - 1][6], view };
|
|
159
|
+
};
|
|
160
|
+
const inRange = (day: Date) => {
|
|
161
|
+
const r = range();
|
|
162
|
+
return day >= r.start && day <= r.end;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const header = () => {
|
|
166
|
+
const row = document.createElement('div');
|
|
167
|
+
row.className = 'mk-calendar__head';
|
|
168
|
+
row.setAttribute('role', 'row');
|
|
169
|
+
const long = weekdayNames(locale, firstDay, 'short');
|
|
170
|
+
const narrow = weekdayNames(locale, firstDay, 'narrow');
|
|
171
|
+
long.forEach((name, i) => {
|
|
172
|
+
const cell = document.createElement('div');
|
|
173
|
+
cell.className = 'mk-calendar__weekday';
|
|
174
|
+
cell.setAttribute('role', 'columnheader');
|
|
175
|
+
cell.innerHTML = '<span class="mk-calendar__weekday-long"></span><span class="mk-calendar__weekday-narrow" aria-hidden="true"></span>';
|
|
176
|
+
cell.firstElementChild!.textContent = name;
|
|
177
|
+
cell.lastElementChild!.textContent = narrow[i];
|
|
178
|
+
row.append(cell);
|
|
179
|
+
});
|
|
180
|
+
return row;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const chip = (event: CalendarEvent) => {
|
|
184
|
+
const button = document.createElement('button');
|
|
185
|
+
button.type = 'button';
|
|
186
|
+
button.className = 'mk-calendar__event';
|
|
187
|
+
button.dataset.mkEvent = String(event.id);
|
|
188
|
+
const status = event.status ?? 'pending';
|
|
189
|
+
button.dataset.state = status;
|
|
190
|
+
if (event.color) button.style.setProperty('--mk-calendar-event-color', event.color);
|
|
191
|
+
const start = toDateTime(event.start)!;
|
|
192
|
+
const time = event.allDay ? '' : timeFormat.format(start);
|
|
193
|
+
button.innerHTML = '<span class="mk-calendar__status" aria-hidden="true"></span><span class="mk-calendar__event-title"></span><span class="mk-calendar__event-time"></span>';
|
|
194
|
+
button.children[1].textContent = event.title;
|
|
195
|
+
button.children[2].textContent = time;
|
|
196
|
+
button.setAttribute('aria-label', [event.title, time, labels.status[status]].filter(Boolean).join(', '));
|
|
197
|
+
return button;
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const body = () => {
|
|
201
|
+
const wrap = document.createElement('div');
|
|
202
|
+
wrap.className = 'mk-calendar__body';
|
|
203
|
+
wrap.dataset.view = view;
|
|
204
|
+
wrap.setAttribute('role', 'rowgroup');
|
|
205
|
+
const now = today();
|
|
206
|
+
const focusKey = dayKey(focusDay);
|
|
207
|
+
for (const week of days()) {
|
|
208
|
+
const row = document.createElement('div');
|
|
209
|
+
row.className = 'mk-calendar__week';
|
|
210
|
+
row.setAttribute('role', 'row');
|
|
211
|
+
for (const day of week) {
|
|
212
|
+
const key = dayKey(day);
|
|
213
|
+
const cell = document.createElement('div');
|
|
214
|
+
cell.className = 'mk-calendar__day';
|
|
215
|
+
cell.setAttribute('role', 'gridcell');
|
|
216
|
+
cell.dataset.mkDate = key;
|
|
217
|
+
if (view === 'month' && !sameMonth(day, date)) cell.dataset.outside = '';
|
|
218
|
+
if (sameDay(day, now)) cell.dataset.today = '';
|
|
219
|
+
const isSelected = sameDay(day, selected);
|
|
220
|
+
cell.setAttribute('aria-selected', String(isSelected));
|
|
221
|
+
const list = byDay.get(key) ?? [];
|
|
222
|
+
const number = document.createElement('button');
|
|
223
|
+
number.type = 'button';
|
|
224
|
+
number.className = 'mk-calendar__number';
|
|
225
|
+
number.dataset.mkDay = key;
|
|
226
|
+
number.textContent = view === 'week' ? `${day.getDate()}` : String(day.getDate());
|
|
227
|
+
number.tabIndex = key === focusKey ? 0 : -1;
|
|
228
|
+
number.setAttribute('aria-label', `${capitalize(fullDay.format(day))}${list.length ? `, ${labels.events(list.length)}` : ''}`);
|
|
229
|
+
if (sameDay(day, now)) number.setAttribute('aria-current', 'date');
|
|
230
|
+
cell.append(number);
|
|
231
|
+
if (list.length) {
|
|
232
|
+
const box = document.createElement('div');
|
|
233
|
+
box.className = 'mk-calendar__events';
|
|
234
|
+
const shown = view === 'week' ? list : list.slice(0, maxEvents);
|
|
235
|
+
for (const event of shown) box.append(chip(event));
|
|
236
|
+
if (shown.length < list.length) {
|
|
237
|
+
const more = document.createElement('button');
|
|
238
|
+
more.type = 'button';
|
|
239
|
+
more.className = 'mk-calendar__more';
|
|
240
|
+
more.dataset.mkMore = key;
|
|
241
|
+
more.textContent = labels.more(list.length - shown.length);
|
|
242
|
+
box.append(more);
|
|
243
|
+
}
|
|
244
|
+
cell.append(box);
|
|
245
|
+
}
|
|
246
|
+
row.append(cell);
|
|
247
|
+
}
|
|
248
|
+
wrap.append(row);
|
|
249
|
+
}
|
|
250
|
+
return wrap;
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
let current: HTMLElement | null = null;
|
|
254
|
+
const leaving = new Set<HTMLElement>();
|
|
255
|
+
scope.add(() => leaving.forEach((el) => el.remove()));
|
|
256
|
+
|
|
257
|
+
const syncChrome = () => {
|
|
258
|
+
if (title) {
|
|
259
|
+
const r = range();
|
|
260
|
+
let text = capitalize(monthTitle.format(date));
|
|
261
|
+
if (view === 'week') {
|
|
262
|
+
const format = rangeTitle as Intl.DateTimeFormat & { formatRange?: (a: Date, b: Date) => string };
|
|
263
|
+
text = format.formatRange ? format.formatRange(r.start, r.end) : `${rangeTitle.format(r.start)} – ${rangeTitle.format(r.end)}`;
|
|
264
|
+
}
|
|
265
|
+
title.textContent = text;
|
|
266
|
+
}
|
|
267
|
+
for (const button of viewButtons) {
|
|
268
|
+
scope.attr(button, 'aria-pressed', String(button.dataset.mkCalendarView === view));
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const render = (direction: number) => {
|
|
273
|
+
const next = body();
|
|
274
|
+
const motion = !reduced();
|
|
275
|
+
if (!current) {
|
|
276
|
+
grid.replaceChildren(header(), next);
|
|
277
|
+
} else if (motion && direction !== 0) {
|
|
278
|
+
const old = current;
|
|
279
|
+
old.style.top = `${old.offsetTop}px`;
|
|
280
|
+
old.classList.add('mk-calendar__body--leaving');
|
|
281
|
+
old.setAttribute('aria-hidden', 'true');
|
|
282
|
+
old.querySelectorAll('button').forEach((b) => (b.tabIndex = -1));
|
|
283
|
+
leaving.add(old);
|
|
284
|
+
grid.append(next);
|
|
285
|
+
ctx.add(() => {
|
|
286
|
+
// Out, then in: two dense month grids crossfading read as noise.
|
|
287
|
+
gsap.to(old, { x: -SLIDE * direction, opacity: 0, duration: SLIDE_OUT, ease: 'power1.in', onComplete: () => { old.remove(); leaving.delete(old); } });
|
|
288
|
+
gsap.fromTo(next, { x: SLIDE * direction, opacity: 0 }, { x: 0, opacity: 1, duration: SLIDE_DURATION, delay: SLIDE_GAP, ease: 'power2.out', clearProps: 'transform,opacity' });
|
|
289
|
+
});
|
|
290
|
+
} else {
|
|
291
|
+
current.replaceWith(next);
|
|
292
|
+
}
|
|
293
|
+
current = next;
|
|
294
|
+
syncChrome();
|
|
295
|
+
if (motion) {
|
|
296
|
+
ctx.add(() => {
|
|
297
|
+
if (first) flowIn(next, { target: '.mk-calendar__day', stagger: CELL_STAGGER });
|
|
298
|
+
flowIn(next, { target: '.mk-calendar__event, .mk-calendar__more', stagger: EVENT_STAGGER, delay: first ? 0.1 : direction ? SLIDE_GAP + SLIDE_DURATION * 0.5 : 0 });
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
first = false;
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
const focusButton = () => grid.querySelector<HTMLButtonElement>(`.mk-calendar__body:not(.mk-calendar__body--leaving) [data-mk-day="${dayKey(focusDay)}"]`);
|
|
305
|
+
const navigate = (to: Date, focus = false) => {
|
|
306
|
+
if (scope.disposed) return;
|
|
307
|
+
const from = date;
|
|
308
|
+
const moved = view === 'week' ? !sameDay(startOfWeek(to, firstDay), startOfWeek(from, firstDay)) : !sameMonth(to, from);
|
|
309
|
+
date = to;
|
|
310
|
+
focusDay = to;
|
|
311
|
+
if (moved) {
|
|
312
|
+
render(to > from ? 1 : -1);
|
|
313
|
+
const r = range();
|
|
314
|
+
options.onNavigate?.(r);
|
|
315
|
+
root.dispatchEvent(new CustomEvent('mk:navigate', { bubbles: true, detail: r }));
|
|
316
|
+
} else {
|
|
317
|
+
grid.querySelectorAll<HTMLButtonElement>('[data-mk-day]').forEach((b) => (b.tabIndex = b.dataset.mkDay === dayKey(to) ? 0 : -1));
|
|
318
|
+
}
|
|
319
|
+
if (focus) focusButton()?.focus();
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const select = (value: DateInput | null, notify = true) => {
|
|
323
|
+
if (scope.disposed) return;
|
|
324
|
+
selected = value === null ? null : toDate(value);
|
|
325
|
+
grid.querySelectorAll<HTMLElement>('.mk-calendar__day').forEach((cell) => cell.setAttribute('aria-selected', String(!!selected && cell.dataset.mkDate === dayKey(selected))));
|
|
326
|
+
if (selected && notify) {
|
|
327
|
+
options.onSelect?.(selected);
|
|
328
|
+
root.dispatchEvent(new CustomEvent('mk:select', { bubbles: true, detail: { date: selected, key: dayKey(selected) } }));
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
scope.on(root, 'click', (e) => {
|
|
333
|
+
const target = (e.target as Element).closest<HTMLElement>('button');
|
|
334
|
+
if (!target || !root.contains(target) || (target as HTMLButtonElement).disabled) return;
|
|
335
|
+
if (target === prevButton) navigate(view === 'week' ? addDays(date, -7) : addMonths(date, -1));
|
|
336
|
+
else if (target === nextButton) navigate(view === 'week' ? addDays(date, 7) : addMonths(date, 1));
|
|
337
|
+
else if (target === todayButton) navigate(today(), true);
|
|
338
|
+
else if (target.dataset.mkCalendarView) setView(target.dataset.mkCalendarView as CalendarView);
|
|
339
|
+
else if (target.dataset.mkEvent !== undefined) {
|
|
340
|
+
const event = events.find((ev) => String(ev.id) === target.dataset.mkEvent);
|
|
341
|
+
if (event) {
|
|
342
|
+
options.onEventClick?.(event);
|
|
343
|
+
root.dispatchEvent(new CustomEvent('mk:event', { bubbles: true, detail: { event } }));
|
|
344
|
+
}
|
|
345
|
+
} else if (target.dataset.mkDay || target.dataset.mkMore) {
|
|
346
|
+
const day = toDate(target.dataset.mkDay ?? target.dataset.mkMore);
|
|
347
|
+
focusDay = day;
|
|
348
|
+
grid.querySelectorAll<HTMLButtonElement>('[data-mk-day]').forEach((b) => (b.tabIndex = b.dataset.mkDay === dayKey(day) ? 0 : -1));
|
|
349
|
+
select(day);
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
scope.on(grid, 'keydown', (e) => {
|
|
354
|
+
const event = e as KeyboardEvent;
|
|
355
|
+
const target = event.target as HTMLElement;
|
|
356
|
+
if (!target.dataset?.mkDay) return;
|
|
357
|
+
const day = toDate(target.dataset.mkDay);
|
|
358
|
+
const map: Record<string, () => Date> = {
|
|
359
|
+
ArrowLeft: () => addDays(day, -1),
|
|
360
|
+
ArrowRight: () => addDays(day, 1),
|
|
361
|
+
ArrowUp: () => addDays(day, -7),
|
|
362
|
+
ArrowDown: () => addDays(day, 7),
|
|
363
|
+
Home: () => startOfWeek(day, firstDay),
|
|
364
|
+
End: () => addDays(startOfWeek(day, firstDay), 6),
|
|
365
|
+
PageUp: () => addMonths(day, event.shiftKey ? -12 : -1),
|
|
366
|
+
PageDown: () => addMonths(day, event.shiftKey ? 12 : 1),
|
|
367
|
+
};
|
|
368
|
+
const to = map[event.key]?.();
|
|
369
|
+
if (!to) return;
|
|
370
|
+
event.preventDefault();
|
|
371
|
+
if (!event.key.startsWith('Page') && inRange(to)) {
|
|
372
|
+
focusDay = to;
|
|
373
|
+
grid.querySelectorAll<HTMLButtonElement>('[data-mk-day]').forEach((b) => (b.tabIndex = b.dataset.mkDay === dayKey(to) ? 0 : -1));
|
|
374
|
+
focusButton()?.focus();
|
|
375
|
+
} else navigate(to, true);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
const setView = (next: CalendarView) => {
|
|
379
|
+
if (scope.disposed || (next !== 'month' && next !== 'week') || next === view) return;
|
|
380
|
+
view = next;
|
|
381
|
+
date = focusDay;
|
|
382
|
+
render(0);
|
|
383
|
+
const r = range();
|
|
384
|
+
options.onNavigate?.(r);
|
|
385
|
+
root.dispatchEvent(new CustomEvent('mk:navigate', { bubbles: true, detail: r }));
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
render(0);
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
next: () => navigate(view === 'week' ? addDays(date, 7) : addMonths(date, 1)),
|
|
392
|
+
prev: () => navigate(view === 'week' ? addDays(date, -7) : addMonths(date, -1)),
|
|
393
|
+
goToday: () => navigate(today()),
|
|
394
|
+
setDate: (value) => navigate(toDate(value, date)),
|
|
395
|
+
setView,
|
|
396
|
+
setEvents(list) {
|
|
397
|
+
if (scope.disposed) return;
|
|
398
|
+
const hadFocus = grid.contains(document.activeElement);
|
|
399
|
+
index(fromJson<CalendarEvent[]>(list) ?? []);
|
|
400
|
+
const fresh = body();
|
|
401
|
+
current?.replaceWith(fresh);
|
|
402
|
+
current = fresh;
|
|
403
|
+
if (hadFocus) focusButton()?.focus();
|
|
404
|
+
},
|
|
405
|
+
select,
|
|
406
|
+
get date() { return date; },
|
|
407
|
+
get view() { return view; },
|
|
408
|
+
get selected() { return selected; },
|
|
409
|
+
get range() { return range(); },
|
|
410
|
+
destroy: () => scope.dispose(),
|
|
411
|
+
};
|
|
412
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useRef, type HTMLAttributes } from 'react';
|
|
4
|
+
import { useIsomorphicLayoutEffect, useLatest } from '../../react/use-recipe';
|
|
5
|
+
import { createCalendar, type CalendarInstance, type CalendarOptions, type CalendarView } from './engine';
|
|
6
|
+
import type { DateInput } from '../../core/dates';
|
|
7
|
+
|
|
8
|
+
export interface CalendarProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onSelect'>, CalendarOptions {
|
|
9
|
+
/** Hide the month/week switch. */
|
|
10
|
+
hideViews?: boolean;
|
|
11
|
+
labels?: CalendarOptions['labels'] & { previous?: string; next?: string; today?: string; month?: string; week?: string };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function Calendar({
|
|
15
|
+
date, view, firstDay, locale, events, maxEvents, selected, today, labels, onSelect, onEventClick, onNavigate,
|
|
16
|
+
hideViews, className, ...props
|
|
17
|
+
}: CalendarProps) {
|
|
18
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
19
|
+
const instance = useRef<CalendarInstance | null>(null);
|
|
20
|
+
const kept = useRef<{ date?: DateInput; view?: CalendarView; selected?: DateInput | null }>({});
|
|
21
|
+
const select = useLatest(onSelect);
|
|
22
|
+
const eventClick = useLatest(onEventClick);
|
|
23
|
+
const navigate = useLatest(onNavigate);
|
|
24
|
+
useIsomorphicLayoutEffect(() => {
|
|
25
|
+
const created = createCalendar(ref.current, {
|
|
26
|
+
date: date ?? kept.current.date,
|
|
27
|
+
view: view ?? kept.current.view,
|
|
28
|
+
selected: selected !== undefined ? selected : kept.current.selected,
|
|
29
|
+
firstDay, locale, events, maxEvents, today, labels,
|
|
30
|
+
onSelect: (d) => select.current?.(d),
|
|
31
|
+
onEventClick: (e) => eventClick.current?.(e),
|
|
32
|
+
onNavigate: (r) => navigate.current?.(r),
|
|
33
|
+
});
|
|
34
|
+
instance.current = created;
|
|
35
|
+
return () => {
|
|
36
|
+
kept.current = { date: created.date, view: created.view, selected: created.selected };
|
|
37
|
+
created.destroy();
|
|
38
|
+
instance.current = null;
|
|
39
|
+
};
|
|
40
|
+
// Events and selection update in place; the rest recreates the grid.
|
|
41
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
42
|
+
}, [date, view, firstDay, locale, maxEvents, today]);
|
|
43
|
+
// Re-index only when the events' content changes, not on every new array identity.
|
|
44
|
+
const eventsKey = JSON.stringify(events ?? []);
|
|
45
|
+
const firstEvents = useRef(true);
|
|
46
|
+
useIsomorphicLayoutEffect(() => {
|
|
47
|
+
if (firstEvents.current) { firstEvents.current = false; return; }
|
|
48
|
+
instance.current?.setEvents(events ?? []);
|
|
49
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
50
|
+
}, [eventsKey]);
|
|
51
|
+
useIsomorphicLayoutEffect(() => { if (selected !== undefined) instance.current?.select(selected, false); }, [selected]);
|
|
52
|
+
return (
|
|
53
|
+
<div {...props} ref={ref} className={['mk-calendar', className].filter(Boolean).join(' ')}>
|
|
54
|
+
<div className="mk-calendar__header">
|
|
55
|
+
<h3 className="mk-calendar__title" data-mk-calendar-title />
|
|
56
|
+
{!hideViews && (
|
|
57
|
+
<div className="mk-calendar__views" role="group" aria-label="Vista">
|
|
58
|
+
<button type="button" data-mk-calendar-view="month">{labels?.month ?? 'Mes'}</button>
|
|
59
|
+
<button type="button" data-mk-calendar-view="week">{labels?.week ?? 'Semana'}</button>
|
|
60
|
+
</div>
|
|
61
|
+
)}
|
|
62
|
+
<button type="button" className="mk-calendar__nav" data-mk-calendar-prev aria-label={labels?.previous ?? 'Anterior'}>←</button>
|
|
63
|
+
<button type="button" className="mk-calendar__nav" data-mk-calendar-today>{labels?.today ?? 'Hoy'}</button>
|
|
64
|
+
<button type="button" className="mk-calendar__nav" data-mk-calendar-next aria-label={labels?.next ?? 'Siguiente'}>→</button>
|
|
65
|
+
</div>
|
|
66
|
+
<div data-mk-calendar-grid />
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# chat
|
|
2
2
|
|
|
3
|
-
El chat
|
|
3
|
+
El chat: burbujas de cristal sobre el fondo vivo, desvanecido de 64 px abajo, composer de 64 px que crece con el texto.
|
|
4
4
|
|
|
5
5
|
- **Mensaje nuevo:** la burbuja crece desde el lado de quien habla (`scale .5 → 1`, blur 24 px, 0.7 s curva firma, 60 ms entre burbujas) y sus palabras suben (16 px, blur 6, 0.25 s, 50 ms, `back.out`). La conversación baja al último (0.35 s).
|
|
6
6
|
- **Pensando:** `pending()` muestra "Pensando…" solo si la respuesta tarda más de 500 ms (con brillo que barre en 1.1 s). La respuesta se transforma desde ese hueco (Flip 0.7 s, sin escalar el texto) mientras sus palabras suben.
|
|
@@ -8,8 +8,6 @@ El chat de Stepbro (PrettyChat): burbujas de cristal sobre el fondo vivo, desvan
|
|
|
8
8
|
- **Confirmación:** una barra entra con flow y sus palabras suben (100 ms entre palabras).
|
|
9
9
|
- **Grabando:** el botón de micrófono pulsa (1.5 s).
|
|
10
10
|
|
|
11
|
-
Origen: Stepbro `pretty-chat`, `chat-message-enter`, `chat-pending-to-reply`, `chat-composer-resize`, `chat-confirm` (kit Money).
|
|
12
|
-
|
|
13
11
|
## HTML / Astro
|
|
14
12
|
|
|
15
13
|
```html
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* chat —
|
|
1
|
+
/* chat — Chat: glass bubbles over the live background, a soft fade at the bottom, a 64 px composer. */
|
|
2
2
|
.mk-chat,
|
|
3
3
|
mk-chat {
|
|
4
4
|
--mk-chat-radius: var(--mk-radius-2xl);
|
|
@@ -9,7 +9,7 @@ import { flowIn } from '../../motion/flow';
|
|
|
9
9
|
import { iconSvg } from '../icons';
|
|
10
10
|
|
|
11
11
|
/*
|
|
12
|
-
*
|
|
12
|
+
* Chat: new bubbles grow from their speaker's side while their
|
|
13
13
|
* words rise (enterMessages); the conversation scrolls to the newest one
|
|
14
14
|
* (0.35 s). A "Pensando…" placeholder shows only if the answer takes more than
|
|
15
15
|
* 500 ms, and then morphs (Flip, 0.7 s, no scaling of the text) into the reply.
|
|
@@ -183,7 +183,7 @@ function Confirm({ children, ...props }: HTMLAttributes<HTMLDivElement>) {
|
|
|
183
183
|
}
|
|
184
184
|
|
|
185
185
|
/**
|
|
186
|
-
*
|
|
186
|
+
* chat: <Chat.Root onSend><ChatBackground /><Chat.Messages pending>{messages.map(m =>
|
|
187
187
|
* <Chat.Message role>)}</Chat.Messages><Chat.Composer /></Chat.Root>.
|
|
188
188
|
*/
|
|
189
189
|
export const Chat = { Root, Messages, Message, Composer, Confirm };
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
# chat-background
|
|
2
2
|
|
|
3
|
-
El fondo vivo del chat
|
|
3
|
+
El fondo vivo del chat: dos manchas suaves (primary-container y tertiary-container, degradados radiales de 800 px, saturación 150 %) que orbitan detrás del chat, con grano encima. La energía (0…1, por ejemplo la del micrófono) acelera las órbitas (`10 − 3.5·e` s y `15 − 4.5·e` s), sube su opacidad (`.5 + .15·e`) y las agranda (`(1 + .1·e)·scale`).
|
|
4
4
|
|
|
5
5
|
El estado "ocupado" del onboarding la lleva a energía 1.6 y escala 1.8 en 0.8 s (`power2.out`) y la devuelve en 0.6 s (`power2.inOut`).
|
|
6
6
|
|
|
7
|
-
Origen: Stepbro PrettyBackground (`chat-background`, `background-audio-energy`, `onboarding-busy-background` en el kit). El grano es un SVG `feTurbulence` en lugar del PNG de Stepbro.
|
|
8
|
-
|
|
9
7
|
## HTML / Astro
|
|
10
8
|
|
|
11
9
|
```html
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* Microphone energy for the chat background
|
|
2
|
+
* Microphone energy for the chat background: FFT 256,
|
|
3
3
|
* smoothing .3, −90…−10 dB; the lowest 40 % of the bins, ×3, clamped to 1, with
|
|
4
4
|
* attack .8 and release .15. The caller passes a MediaStream it already has
|
|
5
5
|
* permission for: this never asks for the microphone.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* chat-background —
|
|
1
|
+
/* chat-background — Chat background: two orbiting blobs and a grain, driven by energy. */
|
|
2
2
|
.mk-chat-background,
|
|
3
3
|
mk-chat-background {
|
|
4
4
|
--mk-chat-orbit-1: 10s;
|
|
@@ -46,7 +46,7 @@ mk-chat-background::after {
|
|
|
46
46
|
animation: mk-chat-blob-2 var(--mk-chat-orbit-2) linear infinite;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
/* Grain
|
|
49
|
+
/* Grain overlay using SVG turbulence at 30 %. */
|
|
50
50
|
.mk-chat-background__noise {
|
|
51
51
|
position: absolute;
|
|
52
52
|
inset: 0;
|
|
@@ -5,7 +5,7 @@ import { TIMING } from '../../core/timing';
|
|
|
5
5
|
import { inert, warnOnce, type Instance } from '../../core/types';
|
|
6
6
|
|
|
7
7
|
/*
|
|
8
|
-
*
|
|
8
|
+
* Chat background: two soft blobs (primary-container and
|
|
9
9
|
* tertiary-container, 800 px radial gradients, saturate 150 %) orbit behind the
|
|
10
10
|
* chat and a grain sits on top. Energy (0…1, e.g. the microphone's) speeds the
|
|
11
11
|
* orbits up (10 − 3.5·e s and 15 − 4.5·e s), brightens the blobs (.5 + .15·e) and
|
|
@@ -12,7 +12,7 @@ export interface ChatBackgroundProps {
|
|
|
12
12
|
style?: CSSProperties;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
/**
|
|
15
|
+
/** chat background. Put it first inside a positioned container. */
|
|
16
16
|
export function ChatBackground({ energy = 0, scale = 1, className, style }: ChatBackgroundProps) {
|
|
17
17
|
const ref = useRef<HTMLDivElement>(null);
|
|
18
18
|
const instance = useRef<ChatBackgroundInstance | null>(null);
|
|
@@ -2,13 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
Dos cursores:
|
|
4
4
|
|
|
5
|
-
- **`chip
|
|
6
|
-
- **`dot
|
|
5
|
+
- **`chip`**: el cursor del sistema se queda. Sobre un elemento con `data-mk-cursor="Texto"` aparece un chip de cristal (`.mk-glass`) con el texto que sigue al puntero con un resorte (Motion 800/35) y entra desde `scale .8` (500/30). `data-mk-cursor-shape="circle"` lo convierte en un círculo de 96 px. Se queda a 10 px de los bordes de la ventana. El texto es del color acento sobre fondos claros y blanco sobre oscuros (luminancia `(299R + 587G + 114B) / 1000 > 180`).
|
|
6
|
+
- **`dot`**: dentro de su zona un círculo sustituye al cursor y lo sigue (20 % de la distancia por frame) con una palabra (`text` o el `data-mk-cursor` de lo que hay debajo). Al presionar encoge a .7, la palabra cae y entran dos flechas (0.2–0.5 s, `easeOutQuint`). `data-mk-cursor-hide` lo oculta sobre enlaces o botones.
|
|
7
7
|
|
|
8
8
|
Los dos se apagan en pantallas táctiles y con movimiento reducido.
|
|
9
9
|
|
|
10
|
-
Origen: dousanmiao.com (cursor `kd`), siena.film (`[data-cursor]`, lerp .2).
|
|
11
|
-
|
|
12
10
|
## HTML / Astro
|
|
13
11
|
|
|
14
12
|
```html
|
|
@@ -29,7 +27,7 @@ Sin custom element: `createCursor(capa, { variant, zone, text, arrows })` sobre
|
|
|
29
27
|
## React
|
|
30
28
|
|
|
31
29
|
```tsx
|
|
32
|
-
<Cursor />
|
|
30
|
+
<Cursor /> // en el layout
|
|
33
31
|
<section><Cursor variant="dot" text="Drag" arrows="x" /></section>
|
|
34
32
|
```
|
|
35
33
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* cursor —
|
|
1
|
+
/* cursor — glass chip and dot. The layer follows the pointer; the engine writes its transform. */
|
|
2
2
|
.mk-cursor {
|
|
3
3
|
position: fixed;
|
|
4
4
|
top: 0;
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
will-change: transform;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
/* chip
|
|
11
|
+
/* chip: a .mk-glass label; ink follows the background under it. */
|
|
12
12
|
.mk-cursor__chip {
|
|
13
13
|
--mk-glass-fg: #fff;
|
|
14
14
|
display: inline-flex;
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
display: none;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
/* dot
|
|
49
|
+
/* dot: a disc with a word and two arrows that slide in when pressed. */
|
|
50
50
|
.mk-cursor--dot {
|
|
51
51
|
--mk-cursor-size: 88px;
|
|
52
52
|
transition: opacity 0.15s var(--mk-ease-quint);
|
|
@@ -2,8 +2,8 @@ import { MkElement, defineElement, type OptionType } from '../../elements/base';
|
|
|
2
2
|
import { createCursor, type CursorInstance, type CursorOptions, type CursorVariant } from './engine';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* <mk-cursor></mk-cursor>
|
|
6
|
-
* <mk-cursor variant="dot" text="Scroll" arrows="y"></mk-cursor>
|
|
5
|
+
* <mk-cursor></mk-cursor> glass chip (once per page)
|
|
6
|
+
* <mk-cursor variant="dot" text="Scroll" arrows="y"></mk-cursor> dot, inside its parent
|
|
7
7
|
* zone="#hero" picks another zone.
|
|
8
8
|
*/
|
|
9
9
|
export class MkCursorElement extends MkElement<CursorInstance> {
|