@nomideusz/svelte-calendar 0.6.3 → 0.6.5
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 +192 -10
- package/dist/adapters/composite.d.ts +15 -0
- package/dist/adapters/composite.js +65 -0
- package/dist/adapters/index.d.ts +5 -1
- package/dist/adapters/index.js +2 -0
- package/dist/adapters/mapped.d.ts +170 -0
- package/dist/adapters/mapped.js +343 -0
- package/dist/adapters/recurring.d.ts +4 -0
- package/dist/adapters/recurring.js +5 -10
- package/dist/adapters/types.d.ts +11 -2
- package/dist/assets/favicon.svg +9 -1
- package/dist/calendar/Calendar.svelte +161 -90
- package/dist/calendar/Calendar.svelte.d.ts +12 -0
- package/dist/core/index.d.ts +3 -1
- package/dist/core/index.js +2 -0
- package/dist/core/measure.d.ts +104 -0
- package/dist/core/measure.js +195 -0
- package/dist/core/time.js +2 -2
- package/dist/core/types.d.ts +32 -0
- package/dist/engine/event-store.svelte.js +22 -7
- package/dist/headless/create-agenda.svelte.d.ts +67 -0
- package/dist/headless/create-agenda.svelte.js +139 -0
- package/dist/headless/create-calendar.svelte.d.ts +2 -0
- package/dist/headless/create-calendar.svelte.js +380 -0
- package/dist/headless/index.d.ts +4 -0
- package/dist/headless/index.js +2 -0
- package/dist/headless/types.d.ts +219 -0
- package/dist/headless/types.js +1 -0
- package/dist/index.d.ts +6 -4
- package/dist/index.js +4 -2
- package/dist/primitives/DayHeader.svelte +6 -6
- package/dist/primitives/EmptySlot.svelte +3 -3
- package/dist/primitives/EventBlock.svelte +99 -14
- package/dist/primitives/NowIndicator.svelte +5 -5
- package/dist/views/agenda/AgendaDay.svelte +215 -116
- package/dist/views/agenda/AgendaWeek.svelte +166 -113
- package/dist/views/mobile/MobileDay.svelte +45 -21
- package/dist/views/mobile/MobileWeek.svelte +85 -35
- package/dist/views/planner/PlannerDay.svelte +116 -60
- package/dist/views/planner/PlannerWeek.svelte +228 -166
- package/dist/views/shared/context.svelte.d.ts +47 -0
- package/dist/views/shared/context.svelte.js +47 -0
- package/dist/views/shared/format.d.ts +19 -0
- package/dist/views/shared/format.js +46 -0
- package/dist/views/shared/index.d.ts +4 -0
- package/dist/views/shared/index.js +2 -0
- package/package.json +9 -2
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createCalendar() — headless calendar state.
|
|
3
|
+
*
|
|
4
|
+
* Returns reactive state, computed layouts, and actions with zero DOM.
|
|
5
|
+
* Users render their own UI using the returned data.
|
|
6
|
+
*
|
|
7
|
+
* Must be called during component initialisation (Svelte 5 rune scope).
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```svelte
|
|
11
|
+
* <script>
|
|
12
|
+
* import { createCalendar, createMemoryAdapter } from '@nomideusz/svelte-calendar';
|
|
13
|
+
*
|
|
14
|
+
* const adapter = createMemoryAdapter([
|
|
15
|
+
* { id: '1', title: 'Standup', start: new Date(), end: new Date() }
|
|
16
|
+
* ]);
|
|
17
|
+
*
|
|
18
|
+
* const cal = createCalendar({ adapter });
|
|
19
|
+
* </script>
|
|
20
|
+
*
|
|
21
|
+
* <!-- Render your own UI -->
|
|
22
|
+
* <div>
|
|
23
|
+
* <button onclick={cal.prev}>←</button>
|
|
24
|
+
* <span>{cal.headerContext.dateLabel}</span>
|
|
25
|
+
* <button onclick={cal.next}>→</button>
|
|
26
|
+
* </div>
|
|
27
|
+
*
|
|
28
|
+
* {#each cal.weeks as week}
|
|
29
|
+
* <div class="week-row">
|
|
30
|
+
* {#each week.days as day}
|
|
31
|
+
* <div class:today={day.isToday}>
|
|
32
|
+
* <span>{day.dayNum}</span>
|
|
33
|
+
* {#each day.events as event}
|
|
34
|
+
* <div style:background={event.color}>{event.title}</div>
|
|
35
|
+
* {/each}
|
|
36
|
+
* </div>
|
|
37
|
+
* {/each}
|
|
38
|
+
* </div>
|
|
39
|
+
* {/each}
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
import { untrack } from 'svelte';
|
|
43
|
+
import { createEventStore } from '../engine/event-store.svelte.js';
|
|
44
|
+
import { createViewState } from '../engine/view-state.svelte.js';
|
|
45
|
+
import { createSelection } from '../engine/selection.svelte.js';
|
|
46
|
+
import { createDragState } from '../engine/drag.svelte.js';
|
|
47
|
+
import { createClock } from '../core/clock.svelte.js';
|
|
48
|
+
import { sod, DAY_MS, HOUR_MS, startOfWeek as sowFn, isAllDay, isMultiDay, segmentForDay, } from '../core/time.js';
|
|
49
|
+
import { monthLong, weekdayLong, weekdayShort } from '../core/locale.js';
|
|
50
|
+
export function createCalendar(options) {
|
|
51
|
+
const { adapter, mondayStart: initialMondayStart = true, initialDate, locale, visibleHours, snapInterval = 15, equalDays = false, hideDays, blockedSlots, disabledDates, days: initialDayCount = 7, readOnly = false, minDuration, maxDuration, oneventclick, oneventcreate, oneventmove, } = options;
|
|
52
|
+
// ── Create engine ────────────────────────────────────
|
|
53
|
+
const store = createEventStore(adapter);
|
|
54
|
+
const viewState = createViewState({
|
|
55
|
+
view: options.view ?? 'week-planner',
|
|
56
|
+
mondayStart: initialMondayStart,
|
|
57
|
+
initialDate,
|
|
58
|
+
dayCount: initialDayCount,
|
|
59
|
+
modeForView: (id) => id.startsWith('day-') ? 'day' : 'week',
|
|
60
|
+
});
|
|
61
|
+
const selection = createSelection();
|
|
62
|
+
const drag = createDragState();
|
|
63
|
+
const clock = createClock();
|
|
64
|
+
// ── Computed ──────────────────────────────────────────
|
|
65
|
+
const disabledSet = $derived(new Set(disabledDates?.map((d) => sod(d.getTime())) ?? []));
|
|
66
|
+
const startHour = visibleHours?.[0] ?? 0;
|
|
67
|
+
const endHour = visibleHours?.[1] ?? 24;
|
|
68
|
+
const hours = Array.from({ length: endHour - startHour }, (_, i) => startHour + i);
|
|
69
|
+
// ── Load events reactively ───────────────────────────
|
|
70
|
+
$effect(() => {
|
|
71
|
+
const { start, end } = viewState.range;
|
|
72
|
+
store.load({ start, end });
|
|
73
|
+
});
|
|
74
|
+
// Eager initial load
|
|
75
|
+
untrack(() => store.load({ start: viewState.range.start, end: viewState.range.end }));
|
|
76
|
+
// ── Compute day cells ────────────────────────────────
|
|
77
|
+
const days = $derived.by(() => {
|
|
78
|
+
const { start, end } = viewState.range;
|
|
79
|
+
const startMs = sod(start.getTime());
|
|
80
|
+
const endMs = end.getTime();
|
|
81
|
+
const events = store.events;
|
|
82
|
+
const todayMs = clock.today;
|
|
83
|
+
const result = [];
|
|
84
|
+
for (let ms = startMs; ms < endMs; ms += DAY_MS) {
|
|
85
|
+
const date = new Date(ms);
|
|
86
|
+
const jsDay = date.getDay();
|
|
87
|
+
const isoDay = jsDay === 0 ? 7 : jsDay;
|
|
88
|
+
// Skip hidden days
|
|
89
|
+
if (hideDays?.includes(isoDay))
|
|
90
|
+
continue;
|
|
91
|
+
const dayEnd = ms + DAY_MS;
|
|
92
|
+
const dayEventsAll = events
|
|
93
|
+
.filter((ev) => ev.start.getTime() < dayEnd && ev.end.getTime() > ms)
|
|
94
|
+
.sort((a, b) => a.start.getTime() - b.start.getTime());
|
|
95
|
+
const timedEvents = [];
|
|
96
|
+
const allDaySegments = [];
|
|
97
|
+
for (const ev of dayEventsAll) {
|
|
98
|
+
if (isAllDay(ev) || isMultiDay(ev)) {
|
|
99
|
+
const seg = segmentForDay(ev, ms);
|
|
100
|
+
if (seg)
|
|
101
|
+
allDaySegments.push(seg);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
timedEvents.push(ev);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
result.push({
|
|
108
|
+
ms,
|
|
109
|
+
date,
|
|
110
|
+
dayNum: date.getDate(),
|
|
111
|
+
weekday: isoDay,
|
|
112
|
+
isToday: ms === todayMs,
|
|
113
|
+
isPast: equalDays ? false : ms < todayMs,
|
|
114
|
+
isWeekend: jsDay === 0 || jsDay === 6,
|
|
115
|
+
isDisabled: disabledSet.has(ms),
|
|
116
|
+
isFirstOfMonth: date.getDate() === 1,
|
|
117
|
+
events: timedEvents,
|
|
118
|
+
allDaySegments,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return result;
|
|
122
|
+
});
|
|
123
|
+
// ── Group days into weeks ────────────────────────────
|
|
124
|
+
const weeks = $derived.by(() => {
|
|
125
|
+
const dayCount = viewState.dayCount;
|
|
126
|
+
const todayMs = clock.today;
|
|
127
|
+
const result = [];
|
|
128
|
+
for (let i = 0; i < days.length; i += dayCount) {
|
|
129
|
+
const chunk = days.slice(i, i + dayCount);
|
|
130
|
+
if (chunk.length === 0)
|
|
131
|
+
continue;
|
|
132
|
+
const periodStart = chunk[0].ms;
|
|
133
|
+
const periodEnd = periodStart + dayCount * DAY_MS;
|
|
134
|
+
result.push({
|
|
135
|
+
periodStart,
|
|
136
|
+
isCurrent: todayMs >= periodStart && todayMs < periodEnd,
|
|
137
|
+
days: chunk,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
return result;
|
|
141
|
+
});
|
|
142
|
+
// ── Today queue ──────────────────────────────────────
|
|
143
|
+
const todayQueue = $derived.by(() => {
|
|
144
|
+
const todayMs = clock.today;
|
|
145
|
+
const now = clock.tick;
|
|
146
|
+
const todayDay = days.find((d) => d.ms === todayMs);
|
|
147
|
+
if (!todayDay)
|
|
148
|
+
return { past: [], current: [], upcoming: [] };
|
|
149
|
+
const past = [];
|
|
150
|
+
const current = [];
|
|
151
|
+
const upcoming = [];
|
|
152
|
+
for (const ev of todayDay.events) {
|
|
153
|
+
const s = ev.start.getTime();
|
|
154
|
+
const e = ev.end.getTime();
|
|
155
|
+
if (e <= now)
|
|
156
|
+
past.push(ev);
|
|
157
|
+
else if (s <= now && e > now)
|
|
158
|
+
current.push(ev);
|
|
159
|
+
else
|
|
160
|
+
upcoming.push(ev);
|
|
161
|
+
}
|
|
162
|
+
return { past, current, upcoming };
|
|
163
|
+
});
|
|
164
|
+
// ── Header context ───────────────────────────────────
|
|
165
|
+
const headerContext = $derived.by(() => {
|
|
166
|
+
const mode = viewState.mode;
|
|
167
|
+
const focus = viewState.focusDate;
|
|
168
|
+
const now = Date.now();
|
|
169
|
+
const { start, end } = viewState.range;
|
|
170
|
+
const isViewOnToday = now >= start.getTime() && now < end.getTime();
|
|
171
|
+
let dateLabel;
|
|
172
|
+
if (mode === 'day') {
|
|
173
|
+
dateLabel = focus.toLocaleDateString(locale, {
|
|
174
|
+
weekday: 'long', month: 'short', day: 'numeric',
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
dateLabel = focus.toLocaleDateString(locale, {
|
|
179
|
+
month: 'long', year: 'numeric',
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
dateLabel,
|
|
184
|
+
mode,
|
|
185
|
+
modes: ['day', 'week'],
|
|
186
|
+
switchMode: (m) => {
|
|
187
|
+
const currentView = viewState.view;
|
|
188
|
+
const currentLabel = currentView.replace(/^(day|week)-/, '');
|
|
189
|
+
viewState.setView(`${m}-${currentLabel}`);
|
|
190
|
+
},
|
|
191
|
+
prev: () => viewState.prev(),
|
|
192
|
+
next: () => viewState.next(),
|
|
193
|
+
goToday: () => viewState.goToday(),
|
|
194
|
+
isViewOnToday,
|
|
195
|
+
focusDate: focus,
|
|
196
|
+
};
|
|
197
|
+
});
|
|
198
|
+
const navigationContext = $derived.by(() => {
|
|
199
|
+
const { start, end } = viewState.range;
|
|
200
|
+
const now = Date.now();
|
|
201
|
+
return {
|
|
202
|
+
prev: () => viewState.prev(),
|
|
203
|
+
next: () => viewState.next(),
|
|
204
|
+
goToday: () => viewState.goToday(),
|
|
205
|
+
isViewOnToday: now >= start.getTime() && now < end.getTime(),
|
|
206
|
+
focusDate: viewState.focusDate,
|
|
207
|
+
mode: viewState.mode,
|
|
208
|
+
};
|
|
209
|
+
});
|
|
210
|
+
// ── Drag validation & commit ─────────────────────────
|
|
211
|
+
async function commitDrag() {
|
|
212
|
+
if (readOnly) {
|
|
213
|
+
drag.cancel();
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
const mode = drag.mode;
|
|
217
|
+
const payload = drag.commit();
|
|
218
|
+
if (!payload)
|
|
219
|
+
return null;
|
|
220
|
+
let { start, end } = payload;
|
|
221
|
+
// Enforce min/max duration
|
|
222
|
+
if (mode === 'create' || mode === 'resize-start' || mode === 'resize-end') {
|
|
223
|
+
const durationMs = end.getTime() - start.getTime();
|
|
224
|
+
const durationMin = durationMs / 60_000;
|
|
225
|
+
if (minDuration && durationMin < minDuration) {
|
|
226
|
+
if (mode === 'resize-start') {
|
|
227
|
+
start = new Date(end.getTime() - minDuration * 60_000);
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
end = new Date(start.getTime() + minDuration * 60_000);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (maxDuration && durationMin > maxDuration) {
|
|
234
|
+
if (mode === 'resize-start') {
|
|
235
|
+
start = new Date(end.getTime() - maxDuration * 60_000);
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
end = new Date(start.getTime() + maxDuration * 60_000);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
// Reject if lands on disabled date
|
|
243
|
+
if (disabledDates?.length) {
|
|
244
|
+
const startDay = sod(start.getTime());
|
|
245
|
+
const endDay = sod(end.getTime() - 1);
|
|
246
|
+
for (const dd of disabledDates) {
|
|
247
|
+
const ts = sod(dd.getTime());
|
|
248
|
+
if (ts >= startDay && ts <= endDay)
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
// Reject if overlaps blocked slot
|
|
253
|
+
if (blockedSlots?.length) {
|
|
254
|
+
const startH = start.getHours() + start.getMinutes() / 60;
|
|
255
|
+
const endH = end.getHours() + end.getMinutes() / 60 + (end.getDate() !== start.getDate() ? 24 : 0);
|
|
256
|
+
const jsDay = start.getDay();
|
|
257
|
+
const isoDay = jsDay === 0 ? 7 : jsDay;
|
|
258
|
+
for (const slot of blockedSlots) {
|
|
259
|
+
if (slot.day && slot.day !== isoDay)
|
|
260
|
+
continue;
|
|
261
|
+
if (startH < slot.end && endH > slot.start)
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
const result = {
|
|
266
|
+
start,
|
|
267
|
+
end,
|
|
268
|
+
...(payload.eventId ? { eventId: payload.eventId } : {}),
|
|
269
|
+
};
|
|
270
|
+
if ((mode === 'move' || mode === 'resize-start' || mode === 'resize-end') && payload.eventId) {
|
|
271
|
+
try {
|
|
272
|
+
await store.move(payload.eventId, start, end);
|
|
273
|
+
const ev = store.byId(payload.eventId);
|
|
274
|
+
if (ev)
|
|
275
|
+
oneventmove?.(ev, start, end);
|
|
276
|
+
}
|
|
277
|
+
catch (e) {
|
|
278
|
+
const msg = e instanceof Error ? e.message : '';
|
|
279
|
+
if (!msg.includes('read-only') && !msg.includes('not found')) {
|
|
280
|
+
console.warn('[calendar] drag commit failed:', e);
|
|
281
|
+
}
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
else if (mode === 'create') {
|
|
286
|
+
oneventcreate?.({ start, end });
|
|
287
|
+
}
|
|
288
|
+
return result;
|
|
289
|
+
}
|
|
290
|
+
// ── Helpers ──────────────────────────────────────────
|
|
291
|
+
function isBlocked(dayMs, hour) {
|
|
292
|
+
if (!blockedSlots?.length)
|
|
293
|
+
return false;
|
|
294
|
+
const jsDay = new Date(dayMs).getDay();
|
|
295
|
+
const isoDay = jsDay === 0 ? 7 : jsDay;
|
|
296
|
+
return blockedSlots.some((slot) => {
|
|
297
|
+
if (slot.day && slot.day !== isoDay)
|
|
298
|
+
return false;
|
|
299
|
+
return hour >= slot.start && hour < slot.end;
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
function isDisabledFn(dayMs) {
|
|
303
|
+
return disabledSet.has(sod(dayMs));
|
|
304
|
+
}
|
|
305
|
+
function isTodayFn(dayMs) {
|
|
306
|
+
return sod(dayMs) === clock.today;
|
|
307
|
+
}
|
|
308
|
+
function isPastFn(dayMs) {
|
|
309
|
+
if (equalDays)
|
|
310
|
+
return false;
|
|
311
|
+
return sod(dayMs) < clock.today;
|
|
312
|
+
}
|
|
313
|
+
// ── Return headless calendar ─────────────────────────
|
|
314
|
+
return {
|
|
315
|
+
// Reactive state
|
|
316
|
+
get events() { return store.events; },
|
|
317
|
+
get loading() { return store.loading; },
|
|
318
|
+
get focusDate() { return viewState.focusDate; },
|
|
319
|
+
get mode() { return viewState.mode; },
|
|
320
|
+
get view() { return viewState.view; },
|
|
321
|
+
get range() { return viewState.range; },
|
|
322
|
+
get today() { return clock.today; },
|
|
323
|
+
get now() { return clock.tick; },
|
|
324
|
+
get timeLabel() { return clock.hm; },
|
|
325
|
+
get secondsLabel() { return clock.s; },
|
|
326
|
+
get selectedEventId() { return selection.selectedId; },
|
|
327
|
+
// Computed layouts
|
|
328
|
+
get days() { return days; },
|
|
329
|
+
get weeks() { return weeks; },
|
|
330
|
+
get hours() { return hours; },
|
|
331
|
+
get todayQueue() { return todayQueue; },
|
|
332
|
+
// Configuration
|
|
333
|
+
get mondayStart() { return viewState.mondayStart; },
|
|
334
|
+
get locale() { return locale; },
|
|
335
|
+
get readOnly() { return readOnly; },
|
|
336
|
+
get dayCount() { return viewState.dayCount; },
|
|
337
|
+
// Navigation
|
|
338
|
+
prev: () => viewState.prev(),
|
|
339
|
+
next: () => viewState.next(),
|
|
340
|
+
goToday: () => viewState.goToday(),
|
|
341
|
+
setView: (id) => viewState.setView(id),
|
|
342
|
+
setFocusDate: (d) => viewState.setFocusDate(d),
|
|
343
|
+
setMondayStart: (v) => viewState.setMondayStart(v),
|
|
344
|
+
setDayCount: (n) => viewState.setDayCount(n),
|
|
345
|
+
// Selection
|
|
346
|
+
selectEvent: (id) => id ? selection.select(id) : selection.deselect(),
|
|
347
|
+
clearSelection: () => selection.clear(),
|
|
348
|
+
// Drag
|
|
349
|
+
beginDragMove: (eventId, start, end) => drag.beginMove(eventId, start, end),
|
|
350
|
+
beginDragCreate: (start, end) => drag.beginCreate(start, end),
|
|
351
|
+
updateDrag: (start, end) => drag.updatePointer(start, end),
|
|
352
|
+
commitDrag,
|
|
353
|
+
cancelDrag: () => drag.cancel(),
|
|
354
|
+
get isDragging() { return drag.active; },
|
|
355
|
+
get dragPayload() {
|
|
356
|
+
if (!drag.payload)
|
|
357
|
+
return null;
|
|
358
|
+
return {
|
|
359
|
+
start: drag.payload.start,
|
|
360
|
+
end: drag.payload.end,
|
|
361
|
+
...(drag.payload.eventId ? { eventId: drag.payload.eventId } : {}),
|
|
362
|
+
};
|
|
363
|
+
},
|
|
364
|
+
get dragMode() { return drag.mode === 'none' ? null : drag.mode; },
|
|
365
|
+
// Helpers
|
|
366
|
+
isBlocked,
|
|
367
|
+
isDisabled: isDisabledFn,
|
|
368
|
+
isToday: isTodayFn,
|
|
369
|
+
isPast: isPastFn,
|
|
370
|
+
eventById: (id) => store.byId(id),
|
|
371
|
+
get headerContext() { return headerContext; },
|
|
372
|
+
get navigationContext() { return navigationContext; },
|
|
373
|
+
// Raw engine access
|
|
374
|
+
get store() { return store; },
|
|
375
|
+
get viewState() { return viewState; },
|
|
376
|
+
get selection() { return selection; },
|
|
377
|
+
get dragState() { return drag; },
|
|
378
|
+
get clock() { return clock; },
|
|
379
|
+
};
|
|
380
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { createCalendar } from './create-calendar.svelte.js';
|
|
2
|
+
export { createAgenda } from './create-agenda.svelte.js';
|
|
3
|
+
export type { AgendaOptions, HeadlessAgenda } from './create-agenda.svelte.js';
|
|
4
|
+
export type { HeadlessCalendarOptions, HeadlessCalendar, HeadlessDay, HeadlessWeek, TodayQueue, HeaderContext, NavigationContext, } from './types.js';
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for the headless calendar API.
|
|
3
|
+
*
|
|
4
|
+
* The headless API gives users full control over rendering.
|
|
5
|
+
* They get reactive state, computed layouts, and actions — zero DOM.
|
|
6
|
+
*/
|
|
7
|
+
import type { CalendarAdapter } from '../adapters/types.js';
|
|
8
|
+
import type { TimelineEvent, BlockedSlot } from '../core/types.js';
|
|
9
|
+
import type { DaySegment } from '../core/time.js';
|
|
10
|
+
import type { CalendarViewId } from '../engine/view-state.svelte.js';
|
|
11
|
+
import type { EventStore } from '../engine/event-store.svelte.js';
|
|
12
|
+
import type { ViewState } from '../engine/view-state.svelte.js';
|
|
13
|
+
import type { Selection } from '../engine/selection.svelte.js';
|
|
14
|
+
import type { DragState } from '../engine/drag.svelte.js';
|
|
15
|
+
import type { Clock } from '../core/clock.svelte.js';
|
|
16
|
+
export interface HeadlessCalendarOptions {
|
|
17
|
+
/** Data source (required) */
|
|
18
|
+
adapter: CalendarAdapter;
|
|
19
|
+
/** Initial view ID (default: 'week-planner') */
|
|
20
|
+
view?: CalendarViewId;
|
|
21
|
+
/** Start week on Monday (default: true) */
|
|
22
|
+
mondayStart?: boolean;
|
|
23
|
+
/** Initial date to focus on (default: today) */
|
|
24
|
+
initialDate?: Date;
|
|
25
|
+
/** BCP 47 locale tag (e.g. 'en-US', 'pl-PL') */
|
|
26
|
+
locale?: string;
|
|
27
|
+
/** Visible hour range [startHour, endHour). Default: [0, 24] */
|
|
28
|
+
visibleHours?: [number, number];
|
|
29
|
+
/** Drag snap interval in minutes (default: 15) */
|
|
30
|
+
snapInterval?: number;
|
|
31
|
+
/** Treat all days equally — no past dimming (default: false) */
|
|
32
|
+
equalDays?: boolean;
|
|
33
|
+
/** ISO weekdays to hide (1=Mon … 7=Sun) */
|
|
34
|
+
hideDays?: number[];
|
|
35
|
+
/** Blocked/unavailable time slots */
|
|
36
|
+
blockedSlots?: BlockedSlot[];
|
|
37
|
+
/** Specific dates to disable */
|
|
38
|
+
disabledDates?: Date[];
|
|
39
|
+
/** Days per period in week mode (default: 7) */
|
|
40
|
+
days?: number;
|
|
41
|
+
/** Read-only mode: disables create/move (default: false) */
|
|
42
|
+
readOnly?: boolean;
|
|
43
|
+
/** Minimum event duration in minutes */
|
|
44
|
+
minDuration?: number;
|
|
45
|
+
/** Maximum event duration in minutes */
|
|
46
|
+
maxDuration?: number;
|
|
47
|
+
oneventclick?: (event: TimelineEvent) => void;
|
|
48
|
+
oneventcreate?: (range: {
|
|
49
|
+
start: Date;
|
|
50
|
+
end: Date;
|
|
51
|
+
}) => void;
|
|
52
|
+
oneventmove?: (event: TimelineEvent, newStart: Date, newEnd: Date) => void;
|
|
53
|
+
}
|
|
54
|
+
export interface HeadlessDay {
|
|
55
|
+
/** Start-of-day timestamp (ms) */
|
|
56
|
+
ms: number;
|
|
57
|
+
/** Date object for this day */
|
|
58
|
+
date: Date;
|
|
59
|
+
/** Day of month (1–31) */
|
|
60
|
+
dayNum: number;
|
|
61
|
+
/** ISO weekday (1=Mon … 7=Sun) */
|
|
62
|
+
weekday: number;
|
|
63
|
+
/** Is this today? */
|
|
64
|
+
isToday: boolean;
|
|
65
|
+
/** Is this day in the past? (respects equalDays) */
|
|
66
|
+
isPast: boolean;
|
|
67
|
+
/** Is this a weekend day (Sat/Sun)? */
|
|
68
|
+
isWeekend: boolean;
|
|
69
|
+
/** Is this day disabled? */
|
|
70
|
+
isDisabled: boolean;
|
|
71
|
+
/** Is this the 1st of the month? */
|
|
72
|
+
isFirstOfMonth: boolean;
|
|
73
|
+
/** Timed events for this day, sorted by start */
|
|
74
|
+
events: TimelineEvent[];
|
|
75
|
+
/** All-day / multi-day event segments */
|
|
76
|
+
allDaySegments: DaySegment[];
|
|
77
|
+
}
|
|
78
|
+
export interface HeadlessWeek {
|
|
79
|
+
/** Start of this period (ms) */
|
|
80
|
+
periodStart: number;
|
|
81
|
+
/** Does this period contain today? */
|
|
82
|
+
isCurrent: boolean;
|
|
83
|
+
/** Days in this period */
|
|
84
|
+
days: HeadlessDay[];
|
|
85
|
+
}
|
|
86
|
+
/** Categorized events for today (Queue layout data) */
|
|
87
|
+
export interface TodayQueue {
|
|
88
|
+
/** Events that have already ended */
|
|
89
|
+
past: TimelineEvent[];
|
|
90
|
+
/** Events currently in progress */
|
|
91
|
+
current: TimelineEvent[];
|
|
92
|
+
/** Events coming up, sorted by start */
|
|
93
|
+
upcoming: TimelineEvent[];
|
|
94
|
+
}
|
|
95
|
+
export interface HeaderContext {
|
|
96
|
+
/** Formatted date label for current view */
|
|
97
|
+
dateLabel: string;
|
|
98
|
+
/** Current mode */
|
|
99
|
+
mode: 'day' | 'week';
|
|
100
|
+
/** Available modes */
|
|
101
|
+
modes: ('day' | 'week')[];
|
|
102
|
+
/** Switch to a different mode */
|
|
103
|
+
switchMode: (mode: 'day' | 'week') => void;
|
|
104
|
+
/** Go to previous period */
|
|
105
|
+
prev: () => void;
|
|
106
|
+
/** Go to next period */
|
|
107
|
+
next: () => void;
|
|
108
|
+
/** Go to today */
|
|
109
|
+
goToday: () => void;
|
|
110
|
+
/** Whether the current view includes today */
|
|
111
|
+
isViewOnToday: boolean;
|
|
112
|
+
/** Current focus date */
|
|
113
|
+
focusDate: Date;
|
|
114
|
+
}
|
|
115
|
+
export interface NavigationContext {
|
|
116
|
+
prev: () => void;
|
|
117
|
+
next: () => void;
|
|
118
|
+
goToday: () => void;
|
|
119
|
+
isViewOnToday: boolean;
|
|
120
|
+
focusDate: Date;
|
|
121
|
+
mode: 'day' | 'week';
|
|
122
|
+
}
|
|
123
|
+
export interface HeadlessCalendar {
|
|
124
|
+
/** All loaded events for the current range */
|
|
125
|
+
readonly events: TimelineEvent[];
|
|
126
|
+
/** Whether the adapter is currently loading */
|
|
127
|
+
readonly loading: boolean;
|
|
128
|
+
/** The currently focused date */
|
|
129
|
+
readonly focusDate: Date;
|
|
130
|
+
/** Current mode: 'day' or 'week' */
|
|
131
|
+
readonly mode: 'day' | 'week';
|
|
132
|
+
/** Current view ID */
|
|
133
|
+
readonly view: CalendarViewId;
|
|
134
|
+
/** Visible date range */
|
|
135
|
+
readonly range: {
|
|
136
|
+
start: Date;
|
|
137
|
+
end: Date;
|
|
138
|
+
};
|
|
139
|
+
/** Today's start-of-day (ms) */
|
|
140
|
+
readonly today: number;
|
|
141
|
+
/** Current timestamp (ms), ticks every second */
|
|
142
|
+
readonly now: number;
|
|
143
|
+
/** Formatted current time "HH:MM" */
|
|
144
|
+
readonly timeLabel: string;
|
|
145
|
+
/** Formatted seconds ":SS" */
|
|
146
|
+
readonly secondsLabel: string;
|
|
147
|
+
/** Currently selected event ID */
|
|
148
|
+
readonly selectedEventId: string | null;
|
|
149
|
+
/** Days in the current view range, each with events attached */
|
|
150
|
+
readonly days: HeadlessDay[];
|
|
151
|
+
/** Same days grouped into week-sized periods */
|
|
152
|
+
readonly weeks: HeadlessWeek[];
|
|
153
|
+
/** Visible hour numbers (e.g. [0,1,...,23] or [9,10,...,17]) */
|
|
154
|
+
readonly hours: number[];
|
|
155
|
+
/** Categorized events for today (past/current/upcoming) */
|
|
156
|
+
readonly todayQueue: TodayQueue;
|
|
157
|
+
readonly mondayStart: boolean;
|
|
158
|
+
readonly locale: string | undefined;
|
|
159
|
+
readonly readOnly: boolean;
|
|
160
|
+
readonly dayCount: number;
|
|
161
|
+
prev(): void;
|
|
162
|
+
next(): void;
|
|
163
|
+
goToday(): void;
|
|
164
|
+
setView(id: CalendarViewId): void;
|
|
165
|
+
setFocusDate(date: Date): void;
|
|
166
|
+
setMondayStart(v: boolean): void;
|
|
167
|
+
setDayCount(n: number): void;
|
|
168
|
+
selectEvent(id: string | null): void;
|
|
169
|
+
clearSelection(): void;
|
|
170
|
+
/** Start dragging an existing event to move it */
|
|
171
|
+
beginDragMove(eventId: string, start: Date, end: Date): void;
|
|
172
|
+
/** Start drag-creating a new event */
|
|
173
|
+
beginDragCreate(start: Date, end: Date): void;
|
|
174
|
+
/** Update drag pointer position */
|
|
175
|
+
updateDrag(start: Date, end: Date): void;
|
|
176
|
+
/**
|
|
177
|
+
* Commit the current drag operation.
|
|
178
|
+
* Validates against min/max duration, blocked slots, disabled dates.
|
|
179
|
+
* For moves: calls store.move() + oneventmove callback.
|
|
180
|
+
* For creates: calls oneventcreate callback.
|
|
181
|
+
* Returns the validated payload, or null if rejected.
|
|
182
|
+
*/
|
|
183
|
+
commitDrag(): Promise<{
|
|
184
|
+
start: Date;
|
|
185
|
+
end: Date;
|
|
186
|
+
eventId?: string;
|
|
187
|
+
} | null>;
|
|
188
|
+
/** Cancel the current drag */
|
|
189
|
+
cancelDrag(): void;
|
|
190
|
+
/** Whether a drag is in progress */
|
|
191
|
+
readonly isDragging: boolean;
|
|
192
|
+
/** Current drag payload (snapped times) */
|
|
193
|
+
readonly dragPayload: {
|
|
194
|
+
start: Date;
|
|
195
|
+
end: Date;
|
|
196
|
+
eventId?: string;
|
|
197
|
+
} | null;
|
|
198
|
+
/** Current drag mode */
|
|
199
|
+
readonly dragMode: 'move' | 'create' | 'resize-start' | 'resize-end' | null;
|
|
200
|
+
/** Check if a specific hour on a day is blocked */
|
|
201
|
+
isBlocked(dayMs: number, hour: number): boolean;
|
|
202
|
+
/** Check if a date is disabled */
|
|
203
|
+
isDisabled(dayMs: number): boolean;
|
|
204
|
+
/** Check if a timestamp is today */
|
|
205
|
+
isToday(dayMs: number): boolean;
|
|
206
|
+
/** Check if a timestamp is in the past */
|
|
207
|
+
isPast(dayMs: number): boolean;
|
|
208
|
+
/** Find an event by ID */
|
|
209
|
+
eventById(id: string): TimelineEvent | undefined;
|
|
210
|
+
/** Get header context (for building custom headers) */
|
|
211
|
+
readonly headerContext: HeaderContext;
|
|
212
|
+
/** Get navigation context (for building custom nav) */
|
|
213
|
+
readonly navigationContext: NavigationContext;
|
|
214
|
+
readonly store: EventStore;
|
|
215
|
+
readonly viewState: ViewState;
|
|
216
|
+
readonly selection: Selection;
|
|
217
|
+
readonly dragState: DragState;
|
|
218
|
+
readonly clock: Clock;
|
|
219
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.d.ts
CHANGED
|
@@ -3,10 +3,12 @@ export { Calendar } from './calendar/index.js';
|
|
|
3
3
|
export type { CalendarView } from './calendar/index.js';
|
|
4
4
|
export { createEventStore, createViewState, createSelection, createDragState, } from './engine/index.js';
|
|
5
5
|
export type { EventStore, ViewState, ViewStateOptions, CalendarViewId, BuiltInViewId, ViewMode, Selection, DragState, DragMode, DragPayload, } from './engine/index.js';
|
|
6
|
-
export { createMemoryAdapter, createRestAdapter, createRecurringAdapter } from './adapters/index.js';
|
|
7
|
-
export type { CalendarAdapter, DateRange, RestAdapterOptions, RecurringEvent, RecurringAdapterOptions, } from './adapters/index.js';
|
|
8
|
-
export { createClock, startOfWeek, fmtH, fmtTime, fmtDuration, weekdayShort, weekdayLong, monthShort, monthLong, dateShort, dateWithWeekday, fmtDay, fmtWeekRange, setDefaultLocale, getDefaultLocale, is24HourLocale, defaultLabels, setLabels, resetLabels, getLabels, toZonedTime, fromZonedTime, nowInZone, formatInTimeZone, generatePalette, extractAccent, VIVID_PALETTE, isMultiDay, isAllDay, segmentForDay, } from './core/index.js';
|
|
9
|
-
export type { Clock, TimelineEvent, BlockedSlot, DaySegment, CalendarLabels, } from './core/index.js';
|
|
6
|
+
export { createMemoryAdapter, createRestAdapter, createRecurringAdapter, createMappedAdapter, createCompositeAdapter } from './adapters/index.js';
|
|
7
|
+
export type { CalendarAdapter, WritableCalendarAdapter, DateRange, RestAdapterOptions, RecurringEvent, RecurringAdapterOptions, FieldMapping, MappedAdapterOptions, MutationHandler, CompositeAdapterOptions, } from './adapters/index.js';
|
|
8
|
+
export { createClock, startOfWeek, fmtH, fmtTime, fmtDuration, weekdayShort, weekdayLong, monthShort, monthLong, dateShort, dateWithWeekday, fmtDay, fmtWeekRange, setDefaultLocale, getDefaultLocale, is24HourLocale, defaultLabels, setLabels, resetLabels, getLabels, toZonedTime, fromZonedTime, nowInZone, formatInTimeZone, generatePalette, extractAccent, VIVID_PALETTE, isMultiDay, isAllDay, segmentForDay, createTextMeasure, initTextMeasure, } from './core/index.js';
|
|
9
|
+
export type { Clock, TimelineEvent, BlockedSlot, DaySegment, CalendarLabels, EventStatus, TextMeasure, TextMeasureOptions, ContentFit, } from './core/index.js';
|
|
10
10
|
export { auto, neutral, midnight, presets } from './theme/index.js';
|
|
11
11
|
export { probeHostTheme, observeHostTheme } from './theme/index.js';
|
|
12
12
|
export type { PresetName, AutoThemeOptions } from './theme/index.js';
|
|
13
|
+
export { createCalendar, createAgenda } from './headless/index.js';
|
|
14
|
+
export type { HeadlessCalendarOptions, HeadlessCalendar, HeadlessDay, HeadlessWeek, TodayQueue, HeaderContext, NavigationContext, AgendaOptions, HeadlessAgenda, } from './headless/index.js';
|
package/dist/index.js
CHANGED
|
@@ -5,9 +5,11 @@ export { Calendar } from './calendar/index.js';
|
|
|
5
5
|
// ─── Engine (reactive state) ────────────────────────────
|
|
6
6
|
export { createEventStore, createViewState, createSelection, createDragState, } from './engine/index.js';
|
|
7
7
|
// ─── Adapters ───────────────────────────────────────────
|
|
8
|
-
export { createMemoryAdapter, createRestAdapter, createRecurringAdapter } from './adapters/index.js';
|
|
8
|
+
export { createMemoryAdapter, createRestAdapter, createRecurringAdapter, createMappedAdapter, createCompositeAdapter } from './adapters/index.js';
|
|
9
9
|
// ─── Core: clock, time, locale, types ───────────────────
|
|
10
|
-
export { createClock, startOfWeek, fmtH, fmtTime, fmtDuration, weekdayShort, weekdayLong, monthShort, monthLong, dateShort, dateWithWeekday, fmtDay, fmtWeekRange, setDefaultLocale, getDefaultLocale, is24HourLocale, defaultLabels, setLabels, resetLabels, getLabels, toZonedTime, fromZonedTime, nowInZone, formatInTimeZone, generatePalette, extractAccent, VIVID_PALETTE, isMultiDay, isAllDay, segmentForDay, } from './core/index.js';
|
|
10
|
+
export { createClock, startOfWeek, fmtH, fmtTime, fmtDuration, weekdayShort, weekdayLong, monthShort, monthLong, dateShort, dateWithWeekday, fmtDay, fmtWeekRange, setDefaultLocale, getDefaultLocale, is24HourLocale, defaultLabels, setLabels, resetLabels, getLabels, toZonedTime, fromZonedTime, nowInZone, formatInTimeZone, generatePalette, extractAccent, VIVID_PALETTE, isMultiDay, isAllDay, segmentForDay, createTextMeasure, initTextMeasure, } from './core/index.js';
|
|
11
11
|
// ─── Themes ─────────────────────────────────────────────
|
|
12
12
|
export { auto, neutral, midnight, presets } from './theme/index.js';
|
|
13
13
|
export { probeHostTheme, observeHostTheme } from './theme/index.js';
|
|
14
|
+
// ─── Headless API ───────────────────────────────────────
|
|
15
|
+
export { createCalendar, createAgenda } from './headless/index.js';
|