@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.
@@ -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
- mondayStart = true,
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
- customDays === 7 ? sowFn(focusMs, mondayStart) : sod(focusMs)
42
- );
34
+ const weekStart = $derived(customDays === 7 ? sowFn(focusMs, mondayStart) : sod(focusMs));
35
+ // ── Load range ─────────────────────────────────────
43
36
  $effect(() => {
44
- if (!loadRangeCtx) return;
45
- const rangeStart = new Date(weekStart - 7 * DAY_MS);
46
- const rangeEnd = new Date(weekStart + (customDays + 7) * DAY_MS);
47
- loadRangeCtx.set({ start: rangeStart, end: rangeEnd });
48
- return () => loadRangeCtx.set(null);
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
- const result = [];
52
- const hideSet = new Set(hideDays ?? []);
53
- for (let i = 0; i < customDays; i++) {
54
- const ms = weekStart + i * DAY_MS;
55
- const d = new Date(ms);
56
- const jsDay = d.getDay();
57
- const isoDay = jsDay === 0 ? 7 : jsDay;
58
- if (hideSet.has(isoDay)) continue;
59
- const isToday = ms === todayMs;
60
- const isPast = equalDays ? false : ms < todayMs;
61
- const isWeekend = jsDay === 0 || jsDay === 6;
62
- const isDisabled = disabledSet.has(ms);
63
- const dayEnd = ms + DAY_MS;
64
- const dayEvents = events.filter((ev) => ev.start.getTime() < dayEnd && ev.end.getTime() > ms).sort((a, b) => a.start.getTime() - b.start.getTime());
65
- const allDayCount = dayEvents.filter((ev) => isAllDay(ev) || isMultiDay(ev)).length;
66
- result.push({
67
- ms,
68
- dayNum: d.getDate(),
69
- dayName: weekdayShort(ms, locale),
70
- isToday,
71
- isPast,
72
- isDisabled,
73
- isWeekend,
74
- events: dayEvents,
75
- allDayCount,
76
- totalCount: dayEvents.length
77
- });
78
- }
79
- return result;
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
- let expandedDays = $state(/* @__PURE__ */ new Set());
77
+ // ── Expanded rows ("+N more") ──────────────────────
78
+ let expandedDays = $state(new Set());
82
79
  function toggleExpand(ms) {
83
- const next = new Set(expandedDays);
84
- if (next.has(ms)) next.delete(ms);
85
- else next.add(ms);
86
- expandedDays = next;
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
- return _fmtTime(d, locale);
87
+ return _fmtTime(d, locale);
90
88
  }
91
89
  function evTimeLabel(ev) {
92
- if (isAllDay(ev) || isMultiDay(ev)) return L.allDay;
93
- return `${fmtTime(ev.start)} \u2013 ${fmtTime(ev.end)}`;
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
- if (ev.status === "cancelled") return ` (${L.cancelled})`;
97
- if (ev.status === "tentative") return ` (${L.tentative})`;
98
- if (ev.status === "full") return ` (${L.full})`;
99
- if (ev.status === "limited") return ` (${L.limited})`;
100
- return "";
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
- onmove: (dx) => {
106
- swipeAnimate = false;
107
- swipeOffset = dx;
108
- },
109
- onend: (dir) => {
110
- if (dir !== 0) {
111
- swipeAnimate = false;
112
- swipeOffset = 0;
113
- if (dir > 0) viewState?.prev();
114
- else viewState?.next();
115
- } else {
116
- swipeAnimate = true;
117
- swipeOffset = 0;
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
- if (!viewState) return;
123
- viewState.setFocusDate(new Date(dayMs));
124
- const currentView = viewState.view;
125
- if (!currentView.split("-").includes("week")) return;
126
- const dayView = currentView.split("-").map((seg) => seg === "week" ? "day" : seg).join("-");
127
- if (dayView !== currentView) viewState.setView(dayView);
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
- if (e.key !== "Enter" && e.key !== " ") return;
131
- e.preventDefault();
132
- handleDayTap(dayMs);
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 ?? /* @__PURE__ */ new Date()).getMonth());
32
+ const focusMonth = $derived((focusDate ?? new Date()).getMonth());
38
33
  $effect(() => {
39
- if (!loadRangeCtx || !range) return;
40
- loadRangeCtx.set({
41
- start: new Date(range.start.getTime() - 7 * DAY_MS),
42
- end: new Date(range.end.getTime() + 7 * DAY_MS)
43
- });
44
- return () => loadRangeCtx.set(null);
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
- if (!range) return null;
52
- const start = sod(range.start.getTime());
53
- const end = range.end.getTime();
54
- if (focusMs !== null && focusMs >= start && focusMs < end) return focusMs;
55
- if (todayMs >= start && todayMs < end) return todayMs;
56
- return start;
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
- if (!range) return;
60
- const target = fromMs + deltaDays * DAY_MS;
61
- if (target < sod(range.start.getTime()) || target >= range.end.getTime()) return;
62
- focusMs = target;
63
- const el = bodyEl?.querySelector(`[data-ms="${target}"]`);
64
- el?.focus();
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
- switch (e.key) {
68
- case "ArrowRight":
69
- e.preventDefault();
70
- moveFocus(cell.ms, 1);
71
- break;
72
- case "ArrowLeft":
73
- e.preventDefault();
74
- moveFocus(cell.ms, -1);
75
- break;
76
- case "ArrowDown":
77
- e.preventDefault();
78
- moveFocus(cell.ms, 7);
79
- break;
80
- case "ArrowUp":
81
- e.preventDefault();
82
- moveFocus(cell.ms, -7);
83
- break;
84
- case "Escape":
85
- if (expandedMs !== null) {
86
- e.preventDefault();
87
- expandedMs = null;
88
- }
89
- break;
90
- case "Enter":
91
- case " ":
92
- if (!cell.isDisabled) {
93
- e.preventDefault();
94
- ondayclick?.(cell.date);
95
- }
96
- break;
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
- e.stopPropagation();
101
- if (ondayclick) ondayclick(cell.date);
102
- else expandedMs = expandedMs === cell.ms ? null : cell.ms;
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
- const date = cell.date.toLocaleDateString(locale, {
106
- weekday: "long",
107
- day: "numeric",
108
- month: "long"
109
- });
110
- return `${date}, ${L.nEvents(cell.all.length)}`;
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
- const dayStart = ms;
114
- const dayEnd = ms + DAY_MS;
115
- return events.filter((e) => e.start.getTime() < dayEnd && e.end.getTime() > dayStart).sort((a, b) => {
116
- const aAll = isAllDay(a) ? 0 : 1;
117
- const bAll = isAllDay(b) ? 0 : 1;
118
- return aAll - bAll || a.start.getTime() - b.start.getTime();
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
- if (!range) return [];
123
- const rows = [];
124
- for (let ms = sod(range.start.getTime()); ms < range.end.getTime(); ms += 7 * DAY_MS) {
125
- const row = [];
126
- for (let i = 0; i < 7; i++) {
127
- const cellMs = ms + i * DAY_MS;
128
- const date = new Date(cellMs);
129
- const jsDay = date.getDay();
130
- const dayEvents = eventsForDay(cellMs);
131
- row.push({
132
- ms: cellMs,
133
- date,
134
- dayNum: date.getDate(),
135
- inMonth: date.getMonth() === focusMonth,
136
- isToday: cellMs === todayMs,
137
- isWeekend: jsDay === 0 || jsDay === 6,
138
- isDisabled: disabledSet.has(cellMs),
139
- chips: dayEvents.slice(0, MAX_CHIPS),
140
- all: dayEvents,
141
- overflow: Math.max(0, dayEvents.length - MAX_CHIPS)
142
- });
143
- }
144
- rows.push(row);
145
- }
146
- return rows;
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
- const first = weeks[0];
150
- if (!first) return [];
151
- return first.map((c) => weekdayShort(c.ms, locale));
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
- return isAllDay(e) ? "" : fmtTime(e.start, locale);
151
+ return isAllDay(e) ? "" : fmtTime(e.start, locale);
155
152
  }
156
153
  </script>
157
154