@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.
Files changed (47) hide show
  1. package/README.md +192 -10
  2. package/dist/adapters/composite.d.ts +15 -0
  3. package/dist/adapters/composite.js +65 -0
  4. package/dist/adapters/index.d.ts +5 -1
  5. package/dist/adapters/index.js +2 -0
  6. package/dist/adapters/mapped.d.ts +170 -0
  7. package/dist/adapters/mapped.js +343 -0
  8. package/dist/adapters/recurring.d.ts +4 -0
  9. package/dist/adapters/recurring.js +5 -10
  10. package/dist/adapters/types.d.ts +11 -2
  11. package/dist/assets/favicon.svg +9 -1
  12. package/dist/calendar/Calendar.svelte +161 -90
  13. package/dist/calendar/Calendar.svelte.d.ts +12 -0
  14. package/dist/core/index.d.ts +3 -1
  15. package/dist/core/index.js +2 -0
  16. package/dist/core/measure.d.ts +104 -0
  17. package/dist/core/measure.js +195 -0
  18. package/dist/core/time.js +2 -2
  19. package/dist/core/types.d.ts +32 -0
  20. package/dist/engine/event-store.svelte.js +22 -7
  21. package/dist/headless/create-agenda.svelte.d.ts +67 -0
  22. package/dist/headless/create-agenda.svelte.js +139 -0
  23. package/dist/headless/create-calendar.svelte.d.ts +2 -0
  24. package/dist/headless/create-calendar.svelte.js +380 -0
  25. package/dist/headless/index.d.ts +4 -0
  26. package/dist/headless/index.js +2 -0
  27. package/dist/headless/types.d.ts +219 -0
  28. package/dist/headless/types.js +1 -0
  29. package/dist/index.d.ts +6 -4
  30. package/dist/index.js +4 -2
  31. package/dist/primitives/DayHeader.svelte +6 -6
  32. package/dist/primitives/EmptySlot.svelte +3 -3
  33. package/dist/primitives/EventBlock.svelte +99 -14
  34. package/dist/primitives/NowIndicator.svelte +5 -5
  35. package/dist/views/agenda/AgendaDay.svelte +215 -116
  36. package/dist/views/agenda/AgendaWeek.svelte +166 -113
  37. package/dist/views/mobile/MobileDay.svelte +45 -21
  38. package/dist/views/mobile/MobileWeek.svelte +85 -35
  39. package/dist/views/planner/PlannerDay.svelte +116 -60
  40. package/dist/views/planner/PlannerWeek.svelte +228 -166
  41. package/dist/views/shared/context.svelte.d.ts +47 -0
  42. package/dist/views/shared/context.svelte.js +47 -0
  43. package/dist/views/shared/format.d.ts +19 -0
  44. package/dist/views/shared/format.js +46 -0
  45. package/dist/views/shared/index.d.ts +4 -0
  46. package/dist/views/shared/index.js +2 -0
  47. package/package.json +9 -2
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Text measurement utilities powered by Pretext.
3
+ *
4
+ * Provides DOM-free text measurement for calendar event blocks.
5
+ * Pretext is an optional peer dependency — if not installed,
6
+ * these functions degrade gracefully to simple heuristics.
7
+ *
8
+ * Usage:
9
+ * const m = createTextMeasure({ font: '500 12px Inter' });
10
+ * const fits = m.fits('Meeting with team', 120, 16); // width, lineHeight
11
+ * const { height, lineCount } = m.measure('Long event title...', 120, 16);
12
+ * m.clear(); // release caches
13
+ */
14
+ const DEFAULTS = {
15
+ titleFont: '500 12px system-ui, sans-serif',
16
+ secondaryFont: '400 10px system-ui, sans-serif',
17
+ tagFont: '500 8px system-ui, sans-serif',
18
+ titleLineHeight: 16,
19
+ secondaryLineHeight: 13,
20
+ contentGap: 3,
21
+ };
22
+ /**
23
+ * Create a text measure instance.
24
+ * Lazily loads Pretext on first use. Falls back to char-width heuristics if unavailable.
25
+ */
26
+ export function createTextMeasure(opts = {}) {
27
+ const config = { ...DEFAULTS, ...opts };
28
+ // Pretext module — loaded lazily
29
+ let pretext = null;
30
+ let loadAttempted = false;
31
+ let pretextAvailable = false;
32
+ // Cache prepared texts (font+text → prepared handle)
33
+ const cache = new Map();
34
+ function tryLoadPretext() {
35
+ if (loadAttempted)
36
+ return pretextAvailable;
37
+ loadAttempted = true;
38
+ try {
39
+ // Dynamic require/import for optional dependency
40
+ // This works because Pretext is a synchronous API
41
+ const mod = globalThis.__pretextModule;
42
+ if (mod) {
43
+ pretext = mod;
44
+ pretextAvailable = true;
45
+ }
46
+ }
47
+ catch {
48
+ // Not available — that's fine
49
+ }
50
+ return pretextAvailable;
51
+ }
52
+ function getPrepared(text, font) {
53
+ const key = `${font}\0${text}`;
54
+ let prepared = cache.get(key);
55
+ if (!prepared) {
56
+ prepared = pretext.prepare(text, font);
57
+ cache.set(key, prepared);
58
+ }
59
+ return prepared;
60
+ }
61
+ // Heuristic fallback: estimate chars per line from average char width
62
+ function heuristicMeasure(text, maxWidth, lineHeight, font) {
63
+ // Rough average char widths by font size
64
+ const sizeMatch = font.match(/(\d+)px/);
65
+ const fontSize = sizeMatch ? parseInt(sizeMatch[1]) : 12;
66
+ const avgCharWidth = fontSize * 0.55; // rough average for Latin text
67
+ const charsPerLine = Math.max(1, Math.floor(maxWidth / avgCharWidth));
68
+ const lineCount = Math.max(1, Math.ceil(text.length / charsPerLine));
69
+ return { height: lineCount * lineHeight, lineCount };
70
+ }
71
+ function measureOne(text, maxWidth, lineHeight, font) {
72
+ if (!text)
73
+ return { height: 0, lineCount: 0 };
74
+ if (pretextAvailable && pretext) {
75
+ const prepared = getPrepared(text, font);
76
+ return pretext.layout(prepared, maxWidth, lineHeight);
77
+ }
78
+ return heuristicMeasure(text, maxWidth, lineHeight, font);
79
+ }
80
+ return {
81
+ get available() {
82
+ tryLoadPretext();
83
+ return pretextAvailable;
84
+ },
85
+ measure(text, maxWidth, lineHeight) {
86
+ tryLoadPretext();
87
+ return measureOne(text, maxWidth, lineHeight, config.titleFont);
88
+ },
89
+ fits(text, maxWidth, lineHeight) {
90
+ tryLoadPretext();
91
+ const { lineCount } = measureOne(text, maxWidth, lineHeight, config.titleFont);
92
+ return lineCount <= 1;
93
+ },
94
+ measureStack(items, maxWidth, gap = config.contentGap) {
95
+ tryLoadPretext();
96
+ const breakdown = [];
97
+ let totalHeight = 0;
98
+ for (const item of items) {
99
+ if (!item.text) {
100
+ breakdown.push({ height: 0, lineCount: 0 });
101
+ continue;
102
+ }
103
+ const font = item.font ?? config.secondaryFont;
104
+ const lh = item.lineHeight ?? config.secondaryLineHeight;
105
+ const result = measureOne(item.text, maxWidth, lh, font);
106
+ breakdown.push(result);
107
+ if (result.height > 0) {
108
+ totalHeight += (totalHeight > 0 ? gap : 0) + result.height;
109
+ }
110
+ }
111
+ return { height: totalHeight, breakdown };
112
+ },
113
+ fitContent(opts) {
114
+ tryLoadPretext();
115
+ const { title, subtitle, location, time, tags, maxWidth, maxHeight } = opts;
116
+ const gap = config.contentGap;
117
+ // Title always measured
118
+ const titleResult = measureOne(title, maxWidth, config.titleLineHeight, config.titleFont);
119
+ let used = titleResult.height;
120
+ const result = {
121
+ title: true,
122
+ titleLines: titleResult.lineCount,
123
+ subtitle: false,
124
+ location: false,
125
+ time: false,
126
+ tags: false,
127
+ totalHeight: used,
128
+ };
129
+ // Try adding time
130
+ if (time) {
131
+ const h = measureOne(time, maxWidth, config.secondaryLineHeight, config.secondaryFont);
132
+ if (used + gap + h.height <= maxHeight) {
133
+ result.time = true;
134
+ used += gap + h.height;
135
+ }
136
+ }
137
+ // Try adding subtitle
138
+ if (subtitle) {
139
+ const h = measureOne(subtitle, maxWidth, config.secondaryLineHeight, config.secondaryFont);
140
+ if (used + gap + h.height <= maxHeight) {
141
+ result.subtitle = true;
142
+ used += gap + h.height;
143
+ }
144
+ }
145
+ // Try adding location
146
+ if (location) {
147
+ const h = measureOne(location, maxWidth, config.secondaryLineHeight, config.secondaryFont);
148
+ if (used + gap + h.height <= maxHeight) {
149
+ result.location = true;
150
+ used += gap + h.height;
151
+ }
152
+ }
153
+ // Try adding tags
154
+ if (tags?.length) {
155
+ const tagText = tags.join(' ');
156
+ const h = measureOne(tagText, maxWidth, config.secondaryLineHeight, config.tagFont);
157
+ if (used + gap + h.height <= maxHeight) {
158
+ result.tags = true;
159
+ used += gap + h.height;
160
+ }
161
+ }
162
+ result.totalHeight = used;
163
+ return result;
164
+ },
165
+ clear() {
166
+ cache.clear();
167
+ if (pretextAvailable && pretext) {
168
+ pretext.clearCache();
169
+ }
170
+ },
171
+ };
172
+ }
173
+ /**
174
+ * Initialize Pretext for text measurement.
175
+ * Call this once in your app's entry point (e.g., +layout.svelte onMount).
176
+ * If not called, measurement falls back to character-width heuristics.
177
+ *
178
+ * Usage:
179
+ * import { initTextMeasure } from '@nomideusz/svelte-calendar';
180
+ * onMount(() => initTextMeasure());
181
+ */
182
+ export async function initTextMeasure() {
183
+ try {
184
+ const mod = await import('@chenglou/pretext');
185
+ globalThis.__pretextModule = {
186
+ prepare: mod.prepare,
187
+ layout: mod.layout,
188
+ clearCache: mod.clearCache,
189
+ };
190
+ return true;
191
+ }
192
+ catch {
193
+ return false;
194
+ }
195
+ }
package/dist/core/time.js CHANGED
@@ -95,8 +95,8 @@ export function segmentForDay(ev, dayMs) {
95
95
  const firstDayMs = sod(evStart);
96
96
  // For end time: if event ends exactly at midnight, last day is the day before
97
97
  const lastDayMs = sod(evEnd - 1);
98
- const totalDays = Math.round((lastDayMs - firstDayMs) / DAY_MS) + 1;
99
- const dayIndex = Math.round((dayStart - firstDayMs) / DAY_MS) + 1;
98
+ const totalDays = Math.floor((lastDayMs - firstDayMs) / DAY_MS) + 1;
99
+ const dayIndex = Math.floor((dayStart - firstDayMs) / DAY_MS) + 1;
100
100
  return {
101
101
  ev,
102
102
  start: new Date(Math.max(evStart, dayStart)),
@@ -5,6 +5,16 @@
5
5
  * Represents an event on the timeline / scheduler.
6
6
  * Core interface for all views, adapters, and the event store.
7
7
  */
8
+ /**
9
+ * Event status — universal across scheduling domains.
10
+ *
11
+ * 'confirmed' — default, normal event
12
+ * 'cancelled' — visually struck through, still shown on grid
13
+ * 'tentative' — e.g. awaiting confirmation
14
+ * 'full' — no remaining capacity
15
+ * 'limited' — low remaining capacity
16
+ */
17
+ export type EventStatus = 'confirmed' | 'cancelled' | 'tentative' | 'full' | 'limited';
8
18
  export interface TimelineEvent {
9
19
  id: string;
10
20
  title: string;
@@ -19,6 +29,28 @@ export interface TimelineEvent {
19
29
  subtitle?: string;
20
30
  /** Tags displayed as small pills (e.g. ["Beginner", "Yoga"]) */
21
31
  tags?: string[];
32
+ /**
33
+ * Location or room where the event takes place.
34
+ * Universal across domains: yoga studio, meeting room, classroom, venue.
35
+ */
36
+ location?: string;
37
+ /**
38
+ * Event status. Defaults to `'confirmed'` when omitted.
39
+ * Cancelled events are visually distinct but still shown on the grid.
40
+ */
41
+ status?: EventStatus;
42
+ /**
43
+ * ID from the external / upstream system (e.g. booking platform,
44
+ * CRM, LMS). Preserved during mapping so the consumer can
45
+ * cross-reference back to the source.
46
+ */
47
+ externalId?: string;
48
+ /**
49
+ * Resource this event belongs to — for multi-resource views.
50
+ * Could be a room ID, instructor ID, court number, etc.
51
+ * The calendar can group columns/lanes by this value.
52
+ */
53
+ resourceId?: string;
22
54
  /** Arbitrary payload from the source app (bookings, attendees, etc.) */
23
55
  data?: Record<string, unknown>;
24
56
  }
@@ -30,11 +30,6 @@ export function createEventStore(adapter) {
30
30
  function overlaps(ev, start, end) {
31
31
  return ev.start < end && ev.end > start;
32
32
  }
33
- function replaceEvent(id, updated) {
34
- if (eventMap.has(id)) {
35
- eventMap.set(id, updated);
36
- }
37
- }
38
33
  function removeEvent(id) {
39
34
  eventMap.delete(id);
40
35
  }
@@ -81,6 +76,8 @@ export function createEventStore(adapter) {
81
76
  return eventMap.get(id);
82
77
  },
83
78
  async add(eventData) {
79
+ if (!adapter.createEvent)
80
+ throw new Error('Adapter is read-only: createEvent not implemented');
84
81
  loading = true;
85
82
  error = null;
86
83
  try {
@@ -97,11 +94,13 @@ export function createEventStore(adapter) {
97
94
  }
98
95
  },
99
96
  async update(id, patch) {
97
+ if (!adapter.updateEvent)
98
+ throw new Error('Adapter is read-only: updateEvent not implemented');
100
99
  loading = true;
101
100
  error = null;
102
101
  try {
103
102
  const updated = await adapter.updateEvent(id, patch);
104
- replaceEvent(id, updated);
103
+ upsertEvent(updated);
105
104
  }
106
105
  catch (e) {
107
106
  error = e instanceof Error ? e.message : String(e);
@@ -112,6 +111,8 @@ export function createEventStore(adapter) {
112
111
  }
113
112
  },
114
113
  async remove(id) {
114
+ if (!adapter.deleteEvent)
115
+ throw new Error('Adapter is read-only: deleteEvent not implemented');
115
116
  loading = true;
116
117
  error = null;
117
118
  try {
@@ -127,7 +128,21 @@ export function createEventStore(adapter) {
127
128
  }
128
129
  },
129
130
  async move(id, newStart, newEnd) {
130
- return this.update(id, { start: newStart, end: newEnd });
131
+ // Optimistic update: apply locally first so the UI doesn't flash
132
+ // back to the old position between drag.commit() and adapter response.
133
+ const existing = eventMap.get(id);
134
+ if (existing) {
135
+ upsertEvent({ ...existing, start: newStart, end: newEnd });
136
+ }
137
+ try {
138
+ await this.update(id, { start: newStart, end: newEnd });
139
+ }
140
+ catch (e) {
141
+ // Revert optimistic update on failure
142
+ if (existing)
143
+ upsertEvent(existing);
144
+ throw e;
145
+ }
131
146
  },
132
147
  };
133
148
  }
@@ -0,0 +1,67 @@
1
+ import type { TimelineEvent } from '../core/types.js';
2
+ import type { CalendarAdapter } from '../adapters/types.js';
3
+ export interface AgendaOptions {
4
+ /** Data source (required). Can be a getter for reactive adapters. */
5
+ adapter: CalendarAdapter | (() => CalendarAdapter);
6
+ /** Initial date to focus on (default: today) */
7
+ initialDate?: Date;
8
+ /** BCP 47 locale tag (e.g. 'en-US', 'pl-PL') */
9
+ locale?: string;
10
+ /** Number of days to load ahead of focus date for the upcoming list (default: 7) */
11
+ lookahead?: number;
12
+ }
13
+ export interface HeadlessAgenda {
14
+ /** The focused date */
15
+ readonly focusDate: Date;
16
+ /** Start-of-day ms for focus date */
17
+ readonly focusDayMs: number;
18
+ /** Is the focus date today? */
19
+ readonly isToday: boolean;
20
+ /** Is the focus date in the past? */
21
+ readonly isPast: boolean;
22
+ /** Is the focus date in the future? */
23
+ readonly isFuture: boolean;
24
+ /** Formatted date label (e.g. "Monday, April 14") */
25
+ readonly dateLabel: string;
26
+ /** Whether the adapter is loading */
27
+ readonly loading: boolean;
28
+ /** All events for the focus day, sorted by start */
29
+ readonly dayEvents: TimelineEvent[];
30
+ /** All-day or multi-day events */
31
+ readonly allDay: TimelineEvent[];
32
+ /** Timed events that have ended (today only; empty for other days) */
33
+ readonly past: TimelineEvent[];
34
+ /** Timed events currently in progress */
35
+ readonly current: TimelineEvent[];
36
+ /** Timed events coming up */
37
+ readonly upcoming: TimelineEvent[];
38
+ /** Grouped upcoming slots (overlapping events merged) */
39
+ readonly upcomingSlots: import('../views/shared/format.js').TimeSlot[];
40
+ /** Total event count for the day */
41
+ readonly count: number;
42
+ /** Format a Date to locale time string (e.g. "2:30 PM" or "14:30") */
43
+ fmtTime(date: Date): string;
44
+ /** Format event duration (e.g. "1h 30m") */
45
+ fmtDuration(event: TimelineEvent): string;
46
+ /** Format time range (e.g. "14:00 – 15:30") */
47
+ fmtRange(event: TimelineEvent): string;
48
+ /** Time until event starts (e.g. "in 2h 15m") — live, updates every second */
49
+ eta(event: TimelineEvent): string;
50
+ /** Progress of an in-progress event (0–1) — live */
51
+ progress(event: TimelineEvent): number;
52
+ /** Go to previous day */
53
+ prev(): void;
54
+ /** Go to next day */
55
+ next(): void;
56
+ /** Go to today */
57
+ goToday(): void;
58
+ /** Set focus to a specific date */
59
+ setDate(date: Date): void;
60
+ /** Current epoch ms, ticks every second */
61
+ readonly now: number;
62
+ /** Formatted current time "HH:MM" */
63
+ readonly timeLabel: string;
64
+ /** Today's start-of-day ms */
65
+ readonly todayMs: number;
66
+ }
67
+ export declare function createAgenda(options: AgendaOptions): HeadlessAgenda;
@@ -0,0 +1,139 @@
1
+ /**
2
+ * createAgenda() — headless single-day agenda state.
3
+ *
4
+ * Returns reactive state for rendering a day's events with zero DOM.
5
+ * Lighter than createCalendar: no view-state, drag, or selection engines.
6
+ * Manages its own focus date, loads events from the adapter, and categorises
7
+ * them into past / current / upcoming buckets that update every second.
8
+ *
9
+ * Must be called during component initialisation (Svelte 5 rune scope).
10
+ *
11
+ * @example
12
+ * ```svelte
13
+ * <script>
14
+ * import { createAgenda, createMemoryAdapter } from '@nomideusz/svelte-calendar';
15
+ *
16
+ * const adapter = createMemoryAdapter([...]);
17
+ * const agenda = createAgenda({ adapter });
18
+ * </script>
19
+ *
20
+ * <h2>{agenda.dateLabel}</h2>
21
+ * <button onclick={agenda.prev}>←</button>
22
+ * <button onclick={agenda.next}>→</button>
23
+ *
24
+ * {#each agenda.upcoming as ev}
25
+ * <div>
26
+ * <span>{agenda.fmtTime(ev.start)}</span>
27
+ * <span>{ev.title}</span>
28
+ * <span>{agenda.eta(ev)}</span>
29
+ * </div>
30
+ * {/each}
31
+ * ```
32
+ */
33
+ import { untrack } from 'svelte';
34
+ import { createEventStore } from '../engine/event-store.svelte.js';
35
+ import { createClock } from '../core/clock.svelte.js';
36
+ import { sod, DAY_MS, isAllDay, isMultiDay } from '../core/time.js';
37
+ import { fmtTime as _fmtTime, fmtDuration } from '../core/locale.js';
38
+ import { timeUntilMs, progress as _progress, groupIntoSlots } from '../views/shared/format.js';
39
+ // ─── Implementation ─────────────────────────────────────
40
+ export function createAgenda(options) {
41
+ const { initialDate, locale, lookahead = 7, } = options;
42
+ const resolveAdapter = typeof options.adapter === 'function'
43
+ ? options.adapter
44
+ : () => options.adapter;
45
+ const store = $derived(createEventStore(resolveAdapter()));
46
+ const clock = createClock();
47
+ // ── Focus date (reactive, writable) ──
48
+ let focusDayMs = $state(sod(initialDate?.getTime() ?? Date.now()));
49
+ // ── Load events for focus date range ──
50
+ $effect(() => {
51
+ const start = new Date(focusDayMs);
52
+ const end = new Date(focusDayMs + lookahead * DAY_MS);
53
+ store.load({ start, end });
54
+ });
55
+ // Eager initial load
56
+ untrack(() => {
57
+ const start = new Date(focusDayMs);
58
+ const end = new Date(focusDayMs + lookahead * DAY_MS);
59
+ store.load({ start, end });
60
+ });
61
+ // ── Day derivations ──
62
+ const dayEnd = $derived(focusDayMs + DAY_MS);
63
+ const isToday = $derived(focusDayMs === clock.today);
64
+ const isPast = $derived(focusDayMs < clock.today);
65
+ const isFuture = $derived(focusDayMs > clock.today);
66
+ const dateLabel = $derived(new Date(focusDayMs).toLocaleDateString(locale ?? undefined, {
67
+ weekday: 'long',
68
+ month: 'long',
69
+ day: 'numeric',
70
+ }));
71
+ const dayEvents = $derived.by(() => {
72
+ return store.events
73
+ .filter((ev) => ev.start.getTime() < dayEnd && ev.end.getTime() > focusDayMs)
74
+ .sort((a, b) => a.start.getTime() - b.start.getTime());
75
+ });
76
+ const allDay = $derived(dayEvents.filter((ev) => isAllDay(ev) || isMultiDay(ev)));
77
+ const timed = $derived(dayEvents.filter((ev) => !isAllDay(ev) && !isMultiDay(ev)));
78
+ const categorised = $derived.by(() => {
79
+ const now = clock.tick;
80
+ const past = [];
81
+ const current = [];
82
+ const upcoming = [];
83
+ for (const ev of timed) {
84
+ const s = ev.start.getTime();
85
+ const e = ev.end.getTime();
86
+ if (e <= now)
87
+ past.push(ev);
88
+ else if (s <= now && e > now)
89
+ current.push(ev);
90
+ else
91
+ upcoming.push(ev);
92
+ }
93
+ return { past, current, upcoming, upcomingSlots: groupIntoSlots(upcoming) };
94
+ });
95
+ // ── Navigation ──
96
+ function prev() { focusDayMs = focusDayMs - DAY_MS; }
97
+ function next() { focusDayMs = focusDayMs + DAY_MS; }
98
+ function goToday() { focusDayMs = clock.today; }
99
+ function setDate(date) { focusDayMs = sod(date.getTime()); }
100
+ // ── Format helpers ──
101
+ const fmtTime = (d) => _fmtTime(d, locale);
102
+ const fmtDur = (ev) => fmtDuration(ev.start, ev.end);
103
+ const fmtRange = (ev) => `${_fmtTime(ev.start, locale)} – ${_fmtTime(ev.end, locale)}`;
104
+ const eta = (ev) => timeUntilMs(ev.start.getTime(), clock.tick);
105
+ const progress = (ev) => _progress(ev, clock.tick);
106
+ return {
107
+ // State
108
+ get focusDate() { return new Date(focusDayMs); },
109
+ get focusDayMs() { return focusDayMs; },
110
+ get isToday() { return isToday; },
111
+ get isPast() { return isPast; },
112
+ get isFuture() { return isFuture; },
113
+ get dateLabel() { return dateLabel; },
114
+ get loading() { return store.loading; },
115
+ // Events
116
+ get dayEvents() { return dayEvents; },
117
+ get allDay() { return allDay; },
118
+ get past() { return categorised.past; },
119
+ get current() { return categorised.current; },
120
+ get upcoming() { return categorised.upcoming; },
121
+ get upcomingSlots() { return categorised.upcomingSlots; },
122
+ get count() { return dayEvents.length; },
123
+ // Format helpers
124
+ fmtTime,
125
+ fmtDuration: fmtDur,
126
+ fmtRange,
127
+ eta,
128
+ progress,
129
+ // Navigation
130
+ prev,
131
+ next,
132
+ goToday,
133
+ setDate,
134
+ // Clock
135
+ get now() { return clock.tick; },
136
+ get timeLabel() { return clock.hm; },
137
+ get todayMs() { return clock.today; },
138
+ };
139
+ }
@@ -0,0 +1,2 @@
1
+ import type { HeadlessCalendarOptions, HeadlessCalendar } from './types.js';
2
+ export declare function createCalendar(options: HeadlessCalendarOptions): HeadlessCalendar;