@yewdiel21/motion-kit 0.9.1 → 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/README.md +93 -83
- package/dist/auto/index.d.ts +22 -3
- package/dist/auto/index.js +41 -3
- package/dist/auto/index.js.map +1 -1
- package/dist/{chunk-BFC27F2G.js → chunk-IKQ5FODN.js} +2 -2
- package/dist/{chunk-JQMJQSXD.js → chunk-TFNRIZHU.js} +1614 -11
- package/dist/chunk-TFNRIZHU.js.map +1 -0
- package/dist/{chunk-XDWBVFHQ.js → chunk-XAKC2QY7.js} +1202 -3
- package/dist/chunk-XAKC2QY7.js.map +1 -0
- package/dist/css/components/calendar.css +159 -0
- package/dist/css/components/hover-card.css +14 -0
- package/dist/css/components/input-otp.css +10 -0
- 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/studio-dock.css +35 -0
- package/dist/css/components/trailer.css +37 -0
- package/dist/css/controls.css +65 -0
- package/dist/elements/index.d.ts +72 -4
- package/dist/elements/index.js +170 -4
- package/dist/elements/index.js.map +1 -1
- package/dist/index.css +1017 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +45 -3
- package/dist/index.js.map +1 -1
- package/dist/react/index.d.ts +105 -6
- package/dist/react/index.js +350 -3
- package/dist/react/index.js.map +1 -1
- package/dist/{reduced-B98f8LPs.d.ts → reduced-BmBzKH94.d.ts} +199 -2
- package/dist/{reveal-C0hCDcB_.d.ts → reveal-jsX-uwUz.d.ts} +211 -1
- package/dist/{store-pHiUvxP8.d.ts → store-DYb3tTsZ.d.ts} +1 -1
- package/package.json +1 -1
- package/registry.json +178 -104
- package/skills/motion-kit/SKILL.md +209 -189
- 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/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/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/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/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/dates.ts +98 -0
- package/src/css/controls/README.md +14 -0
- package/src/css/controls/status.css +63 -0
- package/src/elements/index.ts +24 -0
- package/src/index.ts +25 -0
- package/src/react/index.ts +21 -0
- package/src/react/use-action-button.ts +6 -0
- package/src/react/use-editorial-reveal.ts +7 -0
- package/src/react/use-film-hero.ts +6 -0
- package/src/react/use-font-cycle.ts +7 -0
- package/src/react/use-image-zoom.ts +5 -0
- package/src/react/use-joined-cta.ts +5 -0
- package/src/react/use-local-clock.ts +5 -0
- package/src/react/use-scroll-dock.ts +5 -0
- package/src/react/use-sticker.ts +5 -0
- package/src/react/use-studio-dock.ts +8 -0
- 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/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/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/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/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/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/local-clock/README.md +31 -0
- package/src/recipes/local-clock/create-local-clock.ts +61 -0
- 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/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/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/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/dist/chunk-JQMJQSXD.js.map +0 -1
- package/dist/chunk-XDWBVFHQ.js.map +0 -1
- /package/dist/{chunk-BFC27F2G.js.map → chunk-IKQ5FODN.js.map} +0 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/* calendar — Melon Mind's month/week grid on the --mk-* tokens; parts are .mk-calendar__*. */
|
|
2
|
+
.mk-calendar,
|
|
3
|
+
mk-calendar {
|
|
4
|
+
--mk-calendar-gap: 6px;
|
|
5
|
+
--mk-calendar-radius: var(--mk-radius-lg);
|
|
6
|
+
--mk-calendar-day-height: 112px;
|
|
7
|
+
display: flex;
|
|
8
|
+
flex-direction: column;
|
|
9
|
+
gap: var(--mk-space-3);
|
|
10
|
+
color: var(--mk-text);
|
|
11
|
+
}
|
|
12
|
+
.mk-calendar__header {
|
|
13
|
+
display: flex;
|
|
14
|
+
flex-wrap: wrap;
|
|
15
|
+
align-items: center;
|
|
16
|
+
gap: var(--mk-space-2);
|
|
17
|
+
}
|
|
18
|
+
.mk-calendar__title { margin: 0 auto 0 0; font-size: clamp(1.4rem, 2.6vw, 2rem); font-weight: 400; letter-spacing: -0.01em; }
|
|
19
|
+
.mk-calendar__views { display: inline-flex; padding: 4px; gap: 2px; border-radius: var(--mk-radius-full); background: var(--mk-surface-3); }
|
|
20
|
+
.mk-calendar__views button,
|
|
21
|
+
.mk-calendar__nav {
|
|
22
|
+
display: inline-grid;
|
|
23
|
+
place-items: center;
|
|
24
|
+
min-width: 44px;
|
|
25
|
+
height: 44px;
|
|
26
|
+
padding: 0 12px;
|
|
27
|
+
border: 0;
|
|
28
|
+
border-radius: var(--mk-radius-full);
|
|
29
|
+
background: transparent;
|
|
30
|
+
color: var(--mk-muted);
|
|
31
|
+
font: inherit;
|
|
32
|
+
cursor: pointer;
|
|
33
|
+
transition: background 0.2s, color 0.2s, transform 0.3s var(--mk-ease-nav-move);
|
|
34
|
+
}
|
|
35
|
+
.mk-calendar__views button[aria-pressed='true'] { background: var(--mk-surface-5); color: var(--mk-text); }
|
|
36
|
+
.mk-calendar__nav { background: var(--mk-surface-3); color: var(--mk-text); }
|
|
37
|
+
.mk-calendar__nav:active { transform: scale(0.92); }
|
|
38
|
+
:is(.mk-calendar__views button, .mk-calendar__nav):focus-visible { outline: 2px solid var(--mk-accent); outline-offset: 2px; }
|
|
39
|
+
|
|
40
|
+
[data-mk-calendar-grid] { position: relative; display: flex; flex-direction: column; gap: var(--mk-calendar-gap); }
|
|
41
|
+
.mk-calendar__head,
|
|
42
|
+
.mk-calendar__week { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: var(--mk-calendar-gap); }
|
|
43
|
+
.mk-calendar__body { display: flex; flex-direction: column; gap: var(--mk-calendar-gap); }
|
|
44
|
+
.mk-calendar__body--leaving { position: absolute; left: 0; right: 0; pointer-events: none; }
|
|
45
|
+
.mk-calendar__weekday {
|
|
46
|
+
padding: 8px 4px;
|
|
47
|
+
border-radius: var(--mk-radius-md);
|
|
48
|
+
background: var(--mk-surface-2);
|
|
49
|
+
text-align: center;
|
|
50
|
+
font-size: 0.95rem;
|
|
51
|
+
}
|
|
52
|
+
.mk-calendar__weekday-narrow { display: none; }
|
|
53
|
+
|
|
54
|
+
.mk-calendar__day {
|
|
55
|
+
position: relative;
|
|
56
|
+
display: flex;
|
|
57
|
+
flex-direction: column;
|
|
58
|
+
gap: 4px;
|
|
59
|
+
min-width: 0;
|
|
60
|
+
min-height: var(--mk-calendar-day-height);
|
|
61
|
+
padding: 6px;
|
|
62
|
+
border-radius: var(--mk-calendar-radius);
|
|
63
|
+
background: var(--mk-surface-3);
|
|
64
|
+
transition: background 0.3s, box-shadow 0.3s;
|
|
65
|
+
container-type: inline-size;
|
|
66
|
+
}
|
|
67
|
+
.mk-calendar__body[data-view='week'] .mk-calendar__day { min-height: calc(var(--mk-calendar-day-height) * 3); }
|
|
68
|
+
.mk-calendar__day[data-outside] { background: var(--mk-surface-1); color: var(--mk-muted); }
|
|
69
|
+
.mk-calendar__day:hover { background: var(--mk-surface-4); }
|
|
70
|
+
.mk-calendar__day[aria-selected='true'] { box-shadow: inset 0 0 0 2px var(--mk-accent); }
|
|
71
|
+
.mk-calendar__number {
|
|
72
|
+
align-self: flex-end;
|
|
73
|
+
min-width: 2.2rem;
|
|
74
|
+
height: 2.2rem;
|
|
75
|
+
padding: 0 0.4rem;
|
|
76
|
+
border: 0;
|
|
77
|
+
border-radius: var(--mk-radius-full);
|
|
78
|
+
background: transparent;
|
|
79
|
+
color: inherit;
|
|
80
|
+
font: inherit;
|
|
81
|
+
font-size: 1.15rem;
|
|
82
|
+
cursor: pointer;
|
|
83
|
+
transition: background 0.3s, color 0.3s, border-radius 0.3s var(--mk-ease-nav-radius), transform 0.3s var(--mk-ease-nav-radius);
|
|
84
|
+
}
|
|
85
|
+
.mk-calendar__number:hover { background: var(--mk-surface-5); }
|
|
86
|
+
.mk-calendar__number:active { transform: scale(0.9); }
|
|
87
|
+
.mk-calendar__number:focus-visible { outline: 2px solid var(--mk-accent); outline-offset: 2px; }
|
|
88
|
+
.mk-calendar__day[data-today] .mk-calendar__number { background: var(--mk-accent); color: var(--mk-on-accent); }
|
|
89
|
+
|
|
90
|
+
.mk-calendar__events { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
|
|
91
|
+
.mk-calendar__event,
|
|
92
|
+
.mk-calendar__more {
|
|
93
|
+
--mk-calendar-event-color: var(--mk-accent);
|
|
94
|
+
display: flex;
|
|
95
|
+
align-items: center;
|
|
96
|
+
gap: 6px;
|
|
97
|
+
width: 100%;
|
|
98
|
+
min-width: 0;
|
|
99
|
+
padding: 5px 8px;
|
|
100
|
+
border: 0;
|
|
101
|
+
border-radius: 10px;
|
|
102
|
+
background: var(--mk-surface-4);
|
|
103
|
+
color: var(--mk-text);
|
|
104
|
+
font: inherit;
|
|
105
|
+
font-size: 0.85rem;
|
|
106
|
+
text-align: left;
|
|
107
|
+
cursor: pointer;
|
|
108
|
+
transition: background 0.2s, transform 0.3s var(--mk-ease-nav-move);
|
|
109
|
+
}
|
|
110
|
+
.mk-calendar__event[data-state='pending'] {
|
|
111
|
+
background: color-mix(in srgb, var(--mk-calendar-event-color) 30%, var(--mk-surface-3));
|
|
112
|
+
}
|
|
113
|
+
.mk-calendar__event:hover,
|
|
114
|
+
.mk-calendar__more:hover { transform: translateY(-1px); background: var(--mk-surface-5); }
|
|
115
|
+
.mk-calendar__event:focus-visible,
|
|
116
|
+
.mk-calendar__more:focus-visible { outline: 2px solid var(--mk-accent); outline-offset: 1px; }
|
|
117
|
+
.mk-calendar__status {
|
|
118
|
+
flex: none;
|
|
119
|
+
width: 0.8rem;
|
|
120
|
+
height: 0.8rem;
|
|
121
|
+
border: 2px solid currentColor;
|
|
122
|
+
border-radius: 50%;
|
|
123
|
+
}
|
|
124
|
+
.mk-calendar__event[data-state='done'] .mk-calendar__status { background: currentColor; box-shadow: inset 0 0 0 2px var(--mk-surface-4); }
|
|
125
|
+
.mk-calendar__event[data-state='cancelled'] .mk-calendar__event-title { text-decoration: line-through; opacity: 0.6; }
|
|
126
|
+
.mk-calendar__event-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
127
|
+
.mk-calendar__event-time { flex: none; font-variant-numeric: tabular-nums; opacity: 0.85; }
|
|
128
|
+
.mk-calendar__more { justify-content: center; background: transparent; color: var(--mk-muted); }
|
|
129
|
+
|
|
130
|
+
/* Narrow day cells: the month keeps the title (the time stays in the label); the week stacks the time under it. */
|
|
131
|
+
@container (max-width: 150px) {
|
|
132
|
+
.mk-calendar__body[data-view='month'] .mk-calendar__event-time { display: none; }
|
|
133
|
+
.mk-calendar__body[data-view='week'] .mk-calendar__event { flex-wrap: wrap; column-gap: 5px; row-gap: 1px; }
|
|
134
|
+
.mk-calendar__body[data-view='week'] .mk-calendar__event-title { flex: 1 1 0; }
|
|
135
|
+
.mk-calendar__body[data-view='week'] .mk-calendar__event-time { flex-basis: 100%; padding-left: calc(0.8rem + 5px); font-size: 0.72rem; }
|
|
136
|
+
}
|
|
137
|
+
@media (max-width: 680px) {
|
|
138
|
+
.mk-calendar,
|
|
139
|
+
mk-calendar { --mk-calendar-day-height: 64px; --mk-calendar-gap: 4px; --mk-calendar-radius: var(--mk-radius-sm); }
|
|
140
|
+
.mk-calendar__weekday-long { display: none; }
|
|
141
|
+
.mk-calendar__weekday-narrow { display: inline; }
|
|
142
|
+
.mk-calendar__number { align-self: center; font-size: 0.95rem; }
|
|
143
|
+
.mk-calendar__body[data-view='month'] .mk-calendar__events { flex-direction: row; flex-wrap: wrap; justify-content: center; gap: 3px; }
|
|
144
|
+
.mk-calendar__body[data-view='month'] :is(.mk-calendar__event, .mk-calendar__more) {
|
|
145
|
+
width: 7px;
|
|
146
|
+
height: 7px;
|
|
147
|
+
padding: 0;
|
|
148
|
+
border-radius: 50%;
|
|
149
|
+
background: var(--mk-calendar-event-color);
|
|
150
|
+
font-size: 0;
|
|
151
|
+
}
|
|
152
|
+
.mk-calendar__body[data-view='month'] .mk-calendar__status { display: none; }
|
|
153
|
+
.mk-calendar__body[data-view='week'] .mk-calendar__event-time { display: none; }
|
|
154
|
+
}
|
|
155
|
+
@media (prefers-reduced-motion: reduce) {
|
|
156
|
+
.mk-calendar *,
|
|
157
|
+
mk-calendar * { transition: none !important; }
|
|
158
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { MkElement, defineElement, type OptionType } from '../../elements/base';
|
|
2
|
+
import { createCalendar, type CalendarEvent, type CalendarInstance } from './engine';
|
|
3
|
+
|
|
4
|
+
/** Light DOM: supply the header and the [data-mk-calendar-grid] shown in the README. */
|
|
5
|
+
export class MkCalendarElement extends MkElement<CalendarInstance> {
|
|
6
|
+
static options: Record<string, OptionType> = {
|
|
7
|
+
date: 'string', view: 'string', firstDay: 'number', locale: 'string', maxEvents: 'number', events: 'json', selected: 'string', today: 'string',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
protected create(options: Record<string, unknown>) {
|
|
11
|
+
return createCalendar(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 === 'events') {
|
|
17
|
+
this.instance.setEvents((this.options.events as CalendarEvent[] | undefined) ?? []);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
// Keep where the visitor is unless the changed attribute says otherwise.
|
|
21
|
+
const { date, view, selected } = this.instance;
|
|
22
|
+
this.instance.destroy();
|
|
23
|
+
this.instance = this.create({ date, view, selected, ...this.options });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const defineCalendar = () => defineElement('mk-calendar', MkCalendarElement);
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# hover-card
|
|
2
|
+
|
|
3
|
+
Vista previa no modal, accesible al señalar, enfocar o tocar. Los enlaces/botones del panel pueden recibir foco.
|
|
4
|
+
|
|
5
|
+
```html
|
|
6
|
+
<mk-hover-card class="mk-hover-card" placement="bottom">
|
|
7
|
+
<button type="button" data-mk-hover-trigger>Ver estudio</button>
|
|
8
|
+
<div data-mk-hover-panel hidden role="region" aria-label="Vista previa">Contenido</div>
|
|
9
|
+
</mk-hover-card>
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
`defineElements()`, `createHoverCard(root, options)`, o `<HoverCard trigger="Ver estudio" placement="bottom">Contenido</HoverCard>`.
|
|
13
|
+
|
|
14
|
+
Opciones: `openDelay=120` ms, `closeDelay=140` ms, `placement='top'|'bottom'`, `onOpenChange(boolean)`. API `open()`, `close()`, `isOpen`, `destroy()`. Evento `mk:open-change` con `{open}`.
|
|
15
|
+
|
|
16
|
+
Escape cierra y retorna al disparador; salir con foco y puntero fuera o tocar fuera cierra. El panel se posiciona respecto al root: mantener un ancestro que permita overflow; no crea portal. No atrapa el foco. Entrada .28 s, instantánea bajo movimiento reducido. Limpia timers y restaura todos los atributos.
|