@nomideusz/svelte-calendar 0.14.1 → 0.15.2
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/dist/calendar/Calendar.svelte +450 -372
- package/dist/calendar/Calendar.svelte.d.ts +2 -1
- package/dist/headless/types.d.ts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/primitives/DayHeader.svelte +1 -7
- package/dist/primitives/EmptySlot.svelte +8 -10
- package/dist/primitives/EventBlock.svelte +12 -22
- package/dist/primitives/FloatingPanel.svelte +191 -0
- package/dist/primitives/FloatingPanel.svelte.d.ts +23 -0
- package/dist/primitives/NowIndicator.svelte +2 -13
- package/dist/primitives/TimeGutter.svelte +1 -7
- package/dist/primitives/index.d.ts +1 -0
- package/dist/primitives/index.js +1 -0
- package/dist/views/agenda/AgendaDay.svelte +68 -45
- package/dist/views/agenda/AgendaWeek.svelte +120 -105
- package/dist/views/mobile/MobileDay.svelte +313 -276
- package/dist/views/mobile/MobileWeek.svelte +91 -84
- package/dist/views/month/MonthGrid.svelte +101 -104
- package/dist/views/planner/PlannerWeek.svelte +475 -407
- package/dist/views/planner/PlannerWeek.svelte.d.ts +1 -1
- package/dist/widget/CalendarWidget.svelte +64 -83
- package/package.json +17 -10
- package/widget/widget.js +12723 -12203
|
@@ -12,16 +12,8 @@ import { DAY_MS, sod, isAllDay, isMultiDay } from "../../core/time.js";
|
|
|
12
12
|
import { startOfWeek as sowFn } from "../../core/time.js";
|
|
13
13
|
import { fmtTime as _fmtTime, weekdayShort } from "../../core/locale.js";
|
|
14
14
|
import { createSwipe } from "./swipe.js";
|
|
15
|
-
let {
|
|
16
|
-
|
|
17
|
-
locale,
|
|
18
|
-
height = null,
|
|
19
|
-
events = [],
|
|
20
|
-
style = "",
|
|
21
|
-
focusDate,
|
|
22
|
-
oneventclick,
|
|
23
|
-
selectedEventId = null
|
|
24
|
-
} = $props();
|
|
15
|
+
let { mondayStart = true, locale, height = null, events = [], style = "", focusDate, oneventclick, selectedEventId = null } = $props();
|
|
16
|
+
// ── Context ────────────────────────────────────────
|
|
25
17
|
const ctx = useCalendarContext();
|
|
26
18
|
const L = $derived(ctx.labels);
|
|
27
19
|
const viewState = $derived(ctx.viewState);
|
|
@@ -33,103 +25,118 @@ const oneventhover = $derived(ctx.oneventhover);
|
|
|
33
25
|
const disabledSet = $derived(ctx.disabledSet);
|
|
34
26
|
const loadRangeCtx = $derived(ctx.loadRange);
|
|
35
27
|
const clock = createClock(ctx.timezone);
|
|
28
|
+
// ── Config ─────────────────────────────────────────
|
|
36
29
|
const MAX_EVENTS = 3;
|
|
37
30
|
const customDays = $derived(viewState?.dayCount ?? 7);
|
|
31
|
+
// ── Derived week data ──────────────────────────────
|
|
38
32
|
const todayMs = $derived(clock.today);
|
|
39
33
|
const focusMs = $derived(focusDate ? sod(focusDate.getTime()) : todayMs);
|
|
40
|
-
const weekStart = $derived(
|
|
41
|
-
|
|
42
|
-
);
|
|
34
|
+
const weekStart = $derived(customDays === 7 ? sowFn(focusMs, mondayStart) : sod(focusMs));
|
|
35
|
+
// ── Load range ─────────────────────────────────────
|
|
43
36
|
$effect(() => {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
37
|
+
if (!loadRangeCtx) return;
|
|
38
|
+
const rangeStart = new Date(weekStart - 7 * DAY_MS);
|
|
39
|
+
const rangeEnd = new Date(weekStart + (customDays + 7) * DAY_MS);
|
|
40
|
+
loadRangeCtx.set({
|
|
41
|
+
start: rangeStart,
|
|
42
|
+
end: rangeEnd
|
|
43
|
+
});
|
|
44
|
+
return () => loadRangeCtx.set(null);
|
|
49
45
|
});
|
|
50
46
|
const dayCells = $derived.by(() => {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
47
|
+
const result = [];
|
|
48
|
+
const hideSet = new Set(hideDays ?? []);
|
|
49
|
+
for (let i = 0; i < customDays; i++) {
|
|
50
|
+
const ms = weekStart + i * DAY_MS;
|
|
51
|
+
const d = new Date(ms);
|
|
52
|
+
const jsDay = d.getDay();
|
|
53
|
+
const isoDay = jsDay === 0 ? 7 : jsDay;
|
|
54
|
+
if (hideSet.has(isoDay)) continue;
|
|
55
|
+
const isToday = ms === todayMs;
|
|
56
|
+
const isPast = equalDays ? false : ms < todayMs;
|
|
57
|
+
const isWeekend = jsDay === 0 || jsDay === 6;
|
|
58
|
+
const isDisabled = disabledSet.has(ms);
|
|
59
|
+
const dayEnd = ms + DAY_MS;
|
|
60
|
+
const dayEvents = events.filter((ev) => ev.start.getTime() < dayEnd && ev.end.getTime() > ms).sort((a, b) => a.start.getTime() - b.start.getTime());
|
|
61
|
+
const allDayCount = dayEvents.filter((ev) => isAllDay(ev) || isMultiDay(ev)).length;
|
|
62
|
+
result.push({
|
|
63
|
+
ms,
|
|
64
|
+
dayNum: d.getDate(),
|
|
65
|
+
dayName: weekdayShort(ms, locale),
|
|
66
|
+
isToday,
|
|
67
|
+
isPast,
|
|
68
|
+
isDisabled,
|
|
69
|
+
isWeekend,
|
|
70
|
+
events: dayEvents,
|
|
71
|
+
allDayCount,
|
|
72
|
+
totalCount: dayEvents.length
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return result;
|
|
80
76
|
});
|
|
81
|
-
|
|
77
|
+
// ── Expanded rows ("+N more") ──────────────────────
|
|
78
|
+
let expandedDays = $state(new Set());
|
|
82
79
|
function toggleExpand(ms) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
80
|
+
const next = new Set(expandedDays);
|
|
81
|
+
if (next.has(ms)) next.delete(ms);
|
|
82
|
+
else next.add(ms);
|
|
83
|
+
expandedDays = next;
|
|
87
84
|
}
|
|
85
|
+
// ── Format helpers ─────────────────────────────────
|
|
88
86
|
function fmtTime(d) {
|
|
89
|
-
|
|
87
|
+
return _fmtTime(d, locale);
|
|
90
88
|
}
|
|
91
89
|
function evTimeLabel(ev) {
|
|
92
|
-
|
|
93
|
-
|
|
90
|
+
if (isAllDay(ev) || isMultiDay(ev)) return L.allDay;
|
|
91
|
+
return `${fmtTime(ev.start)} – ${fmtTime(ev.end)}`;
|
|
94
92
|
}
|
|
95
93
|
function statusText(ev) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
94
|
+
if (ev.status === "cancelled") return ` (${L.cancelled})`;
|
|
95
|
+
if (ev.status === "tentative") return ` (${L.tentative})`;
|
|
96
|
+
if (ev.status === "full") return ` (${L.full})`;
|
|
97
|
+
if (ev.status === "limited") return ` (${L.limited})`;
|
|
98
|
+
return "";
|
|
101
99
|
}
|
|
100
|
+
// ── Touch swipe ────────────────────────────────────
|
|
102
101
|
let swipeOffset = $state(0);
|
|
103
102
|
let swipeAnimate = $state(false);
|
|
104
103
|
const swipe = createSwipe({
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
104
|
+
onmove: (dx) => {
|
|
105
|
+
swipeAnimate = false;
|
|
106
|
+
swipeOffset = dx;
|
|
107
|
+
},
|
|
108
|
+
onend: (dir) => {
|
|
109
|
+
if (dir !== 0) {
|
|
110
|
+
// Committed — the week changes, so jump rather than slide new content.
|
|
111
|
+
swipeAnimate = false;
|
|
112
|
+
swipeOffset = 0;
|
|
113
|
+
if (dir > 0) viewState?.prev();
|
|
114
|
+
else viewState?.next();
|
|
115
|
+
} else {
|
|
116
|
+
// Snap back (CSS drops the transition under prefers-reduced-motion).
|
|
117
|
+
swipeAnimate = true;
|
|
118
|
+
swipeOffset = 0;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
120
121
|
});
|
|
122
|
+
// ── Day tap → switch to day mode ───────────────────
|
|
121
123
|
function handleDayTap(dayMs) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
124
|
+
if (!viewState) return;
|
|
125
|
+
viewState.setFocusDate(new Date(dayMs));
|
|
126
|
+
// The registered view list isn't reachable from the view context, so we
|
|
127
|
+
// can only map conventionally-named ids ("week-mobile" → "day-mobile",
|
|
128
|
+
// "week-planner" → "day-planner", …). For custom ids that don't follow
|
|
129
|
+
// the "week-*" segment convention, we keep the current view (the focus
|
|
130
|
+
// date still moves) instead of switching to a possibly-unregistered id.
|
|
131
|
+
const currentView = viewState.view;
|
|
132
|
+
if (!currentView.split("-").includes("week")) return;
|
|
133
|
+
const dayView = currentView.split("-").map((seg) => seg === "week" ? "day" : seg).join("-");
|
|
134
|
+
if (dayView !== currentView) viewState.setView(dayView);
|
|
128
135
|
}
|
|
129
136
|
function handleDayKeydown(e, dayMs) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
137
|
+
if (e.key !== "Enter" && e.key !== " ") return;
|
|
138
|
+
e.preventDefault();
|
|
139
|
+
handleDayTap(dayMs);
|
|
133
140
|
}
|
|
134
141
|
</script>
|
|
135
142
|
|
|
@@ -10,15 +10,7 @@
|
|
|
10
10
|
import { createClock } from "../../core/clock.svelte.js";
|
|
11
11
|
import { DAY_MS, sod, isAllDay } from "../../core/time.js";
|
|
12
12
|
import { fmtTime, weekdayShort } from "../../core/locale.js";
|
|
13
|
-
let {
|
|
14
|
-
events = [],
|
|
15
|
-
style = "",
|
|
16
|
-
height = null,
|
|
17
|
-
locale,
|
|
18
|
-
focusDate,
|
|
19
|
-
oneventclick,
|
|
20
|
-
selectedEventId = null
|
|
21
|
-
} = $props();
|
|
13
|
+
let { events = [], style = "", height = null, locale, focusDate, oneventclick, selectedEventId = null } = $props();
|
|
22
14
|
const ctx = useCalendarContext();
|
|
23
15
|
const L = $derived(ctx.labels);
|
|
24
16
|
const viewState = $derived(ctx.viewState);
|
|
@@ -31,127 +23,132 @@ const eventSnippet = $derived(ctx.eventSnippet);
|
|
|
31
23
|
const loadRangeCtx = $derived(ctx.loadRange);
|
|
32
24
|
const clock = createClock(ctx.timezone);
|
|
33
25
|
const todayMs = $derived(clock.today);
|
|
26
|
+
// Mobile cells are too narrow for text chips (a 55px cell fits ~1 character)
|
|
27
|
+
// and too short for stacked rows — render color dots instead, and let the
|
|
28
|
+
// cell tap (day drill-down) carry the interaction.
|
|
34
29
|
const dotsMode = $derived(isMobile);
|
|
35
30
|
const MAX_CHIPS = 3;
|
|
36
31
|
const range = $derived(viewState?.range);
|
|
37
|
-
const focusMonth = $derived((focusDate ??
|
|
32
|
+
const focusMonth = $derived((focusDate ?? new Date()).getMonth());
|
|
38
33
|
$effect(() => {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
34
|
+
if (!loadRangeCtx || !range) return;
|
|
35
|
+
loadRangeCtx.set({
|
|
36
|
+
start: new Date(range.start.getTime() - 7 * DAY_MS),
|
|
37
|
+
end: new Date(range.end.getTime() + 7 * DAY_MS)
|
|
38
|
+
});
|
|
39
|
+
return () => loadRangeCtx.set(null);
|
|
45
40
|
});
|
|
41
|
+
/** Day (ms) whose cell is inline-expanded to show all events (no-ondayclick fallback). */
|
|
46
42
|
let expandedMs = $state(null);
|
|
43
|
+
/** Roving-tabindex anchor: the one cell reachable via Tab. */
|
|
47
44
|
let focusMs = $state(null);
|
|
48
45
|
let bodyEl = $state(null);
|
|
49
46
|
const cellsInteractive = $derived(!!ondayclick);
|
|
50
47
|
const rovingMs = $derived.by(() => {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
48
|
+
if (!range) return null;
|
|
49
|
+
const start = sod(range.start.getTime());
|
|
50
|
+
const end = range.end.getTime();
|
|
51
|
+
if (focusMs !== null && focusMs >= start && focusMs < end) return focusMs;
|
|
52
|
+
if (todayMs >= start && todayMs < end) return todayMs;
|
|
53
|
+
return start;
|
|
57
54
|
});
|
|
58
55
|
function moveFocus(fromMs, deltaDays) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
56
|
+
if (!range) return;
|
|
57
|
+
const target = fromMs + deltaDays * DAY_MS;
|
|
58
|
+
if (target < sod(range.start.getTime()) || target >= range.end.getTime()) return;
|
|
59
|
+
focusMs = target;
|
|
60
|
+
const el = bodyEl?.querySelector(`[data-ms="${target}"]`);
|
|
61
|
+
el?.focus();
|
|
65
62
|
}
|
|
66
63
|
function cellKeydown(e, cell) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
64
|
+
switch (e.key) {
|
|
65
|
+
case "ArrowRight":
|
|
66
|
+
e.preventDefault();
|
|
67
|
+
moveFocus(cell.ms, 1);
|
|
68
|
+
break;
|
|
69
|
+
case "ArrowLeft":
|
|
70
|
+
e.preventDefault();
|
|
71
|
+
moveFocus(cell.ms, -1);
|
|
72
|
+
break;
|
|
73
|
+
case "ArrowDown":
|
|
74
|
+
e.preventDefault();
|
|
75
|
+
moveFocus(cell.ms, 7);
|
|
76
|
+
break;
|
|
77
|
+
case "ArrowUp":
|
|
78
|
+
e.preventDefault();
|
|
79
|
+
moveFocus(cell.ms, -7);
|
|
80
|
+
break;
|
|
81
|
+
case "Escape":
|
|
82
|
+
if (expandedMs !== null) {
|
|
83
|
+
e.preventDefault();
|
|
84
|
+
expandedMs = null;
|
|
85
|
+
}
|
|
86
|
+
break;
|
|
87
|
+
case "Enter":
|
|
88
|
+
case " ":
|
|
89
|
+
if (!cell.isDisabled) {
|
|
90
|
+
e.preventDefault();
|
|
91
|
+
ondayclick?.(cell.date);
|
|
92
|
+
}
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
98
95
|
}
|
|
99
96
|
function overflowClick(e, cell) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
97
|
+
e.stopPropagation();
|
|
98
|
+
if (ondayclick) ondayclick(cell.date);
|
|
99
|
+
else expandedMs = expandedMs === cell.ms ? null : cell.ms;
|
|
103
100
|
}
|
|
104
101
|
function cellLabel(cell) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
102
|
+
const date = cell.date.toLocaleDateString(locale, {
|
|
103
|
+
weekday: "long",
|
|
104
|
+
day: "numeric",
|
|
105
|
+
month: "long"
|
|
106
|
+
});
|
|
107
|
+
return `${date}, ${L.nEvents(cell.all.length)}`;
|
|
111
108
|
}
|
|
112
109
|
function eventsForDay(ms) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
110
|
+
const dayStart = ms;
|
|
111
|
+
const dayEnd = ms + DAY_MS;
|
|
112
|
+
return events.filter((e) => e.start.getTime() < dayEnd && e.end.getTime() > dayStart).sort((a, b) => {
|
|
113
|
+
const aAll = isAllDay(a) ? 0 : 1;
|
|
114
|
+
const bAll = isAllDay(b) ? 0 : 1;
|
|
115
|
+
return aAll - bAll || a.start.getTime() - b.start.getTime();
|
|
116
|
+
});
|
|
120
117
|
}
|
|
121
118
|
const weeks = $derived.by(() => {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
119
|
+
if (!range) return [];
|
|
120
|
+
const rows = [];
|
|
121
|
+
for (let ms = sod(range.start.getTime()); ms < range.end.getTime(); ms += 7 * DAY_MS) {
|
|
122
|
+
const row = [];
|
|
123
|
+
for (let i = 0; i < 7; i++) {
|
|
124
|
+
const cellMs = ms + i * DAY_MS;
|
|
125
|
+
const date = new Date(cellMs);
|
|
126
|
+
const jsDay = date.getDay();
|
|
127
|
+
const dayEvents = eventsForDay(cellMs);
|
|
128
|
+
row.push({
|
|
129
|
+
ms: cellMs,
|
|
130
|
+
date,
|
|
131
|
+
dayNum: date.getDate(),
|
|
132
|
+
inMonth: date.getMonth() === focusMonth,
|
|
133
|
+
isToday: cellMs === todayMs,
|
|
134
|
+
isWeekend: jsDay === 0 || jsDay === 6,
|
|
135
|
+
isDisabled: disabledSet.has(cellMs),
|
|
136
|
+
chips: dayEvents.slice(0, MAX_CHIPS),
|
|
137
|
+
all: dayEvents,
|
|
138
|
+
overflow: Math.max(0, dayEvents.length - MAX_CHIPS)
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
rows.push(row);
|
|
142
|
+
}
|
|
143
|
+
return rows;
|
|
147
144
|
});
|
|
148
145
|
const weekdayLabels = $derived.by(() => {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
146
|
+
const first = weeks[0];
|
|
147
|
+
if (!first) return [];
|
|
148
|
+
return first.map((c) => weekdayShort(c.ms, locale));
|
|
152
149
|
});
|
|
153
150
|
function chipTime(e) {
|
|
154
|
-
|
|
151
|
+
return isAllDay(e) ? "" : fmtTime(e.start, locale);
|
|
155
152
|
}
|
|
156
153
|
</script>
|
|
157
154
|
|