@nomideusz/svelte-calendar 0.12.0 → 0.13.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.
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { TimelineEvent } from '../core/types.js';
|
|
2
|
+
import type { CalendarAdapter } from '../adapters/types.js';
|
|
3
|
+
export interface RangeAgendaOptions {
|
|
4
|
+
/** Data source (required). Can be a getter for reactive adapters. */
|
|
5
|
+
adapter: CalendarAdapter | (() => CalendarAdapter);
|
|
6
|
+
/** Days per page — prev()/next() move by this many (default: 7) */
|
|
7
|
+
days?: number;
|
|
8
|
+
/**
|
|
9
|
+
* First visible day (default: today). Not auto-aligned: pass the Monday
|
|
10
|
+
* of the week yourself if you want calendar-week pages.
|
|
11
|
+
*/
|
|
12
|
+
initialDate?: Date;
|
|
13
|
+
/** BCP 47 locale tag (e.g. 'en-US', 'pl-PL') for the format helpers */
|
|
14
|
+
locale?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface RangeAgendaDay {
|
|
17
|
+
/** Start-of-day timestamp (ms) */
|
|
18
|
+
ms: number;
|
|
19
|
+
/** Date object for this day */
|
|
20
|
+
date: Date;
|
|
21
|
+
/** ISO weekday (1=Mon … 7=Sun) */
|
|
22
|
+
weekday: number;
|
|
23
|
+
/** Is this today? (evaluated when the window derives — does not tick) */
|
|
24
|
+
isToday: boolean;
|
|
25
|
+
/** Is this day fully in the past? */
|
|
26
|
+
isPast: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Events overlapping this day, sorted by start. Multi-day events appear
|
|
29
|
+
* on every day they touch; all-day events sort first (00:00 start).
|
|
30
|
+
*/
|
|
31
|
+
events: TimelineEvent[];
|
|
32
|
+
}
|
|
33
|
+
export interface HeadlessRangeAgenda {
|
|
34
|
+
/** Every day in the current window, in order, with events attached */
|
|
35
|
+
readonly days: RangeAgendaDay[];
|
|
36
|
+
/** The current window */
|
|
37
|
+
readonly range: {
|
|
38
|
+
start: Date;
|
|
39
|
+
end: Date;
|
|
40
|
+
};
|
|
41
|
+
/** Total event count in the window */
|
|
42
|
+
readonly count: number;
|
|
43
|
+
/** Whether the adapter is loading */
|
|
44
|
+
readonly loading: boolean;
|
|
45
|
+
/** Last adapter error, if any */
|
|
46
|
+
readonly error: string | null;
|
|
47
|
+
/** Page back one window */
|
|
48
|
+
prev(): void;
|
|
49
|
+
/** Page forward one window */
|
|
50
|
+
next(): void;
|
|
51
|
+
/** Move the window start to today (not re-aligned; use setDate for aligned jumps) */
|
|
52
|
+
goToday(): void;
|
|
53
|
+
/** Set the window start to a specific date */
|
|
54
|
+
setDate(date: Date): void;
|
|
55
|
+
/** Format a Date to locale time string (e.g. "14:30") */
|
|
56
|
+
fmtTime(date: Date): string;
|
|
57
|
+
/** Format event duration (e.g. "1h 30m") */
|
|
58
|
+
fmtDuration(event: TimelineEvent): string;
|
|
59
|
+
/** Format time range (e.g. "14:00 – 15:30") */
|
|
60
|
+
fmtRange(event: TimelineEvent): string;
|
|
61
|
+
}
|
|
62
|
+
export declare function createRangeAgenda(options: RangeAgendaOptions): HeadlessRangeAgenda;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createRangeAgenda() — headless multi-day agenda state.
|
|
3
|
+
*
|
|
4
|
+
* Returns reactive state for rendering a window of days (default: one week)
|
|
5
|
+
* with events grouped per day — zero DOM, zero presentation opinions.
|
|
6
|
+
* Sits between createAgenda (live single day) and createCalendar (full grid
|
|
7
|
+
* engine): no clock, no drag, no selection — just "these days, these events",
|
|
8
|
+
* plus paging. Built for read-only schedule surfaces: public timetables,
|
|
9
|
+
* class listings, embeds.
|
|
10
|
+
*
|
|
11
|
+
* Must be called during component initialisation (Svelte 5 rune scope).
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```svelte
|
|
15
|
+
* <script>
|
|
16
|
+
* import { createRangeAgenda, createMemoryAdapter } from '@nomideusz/svelte-calendar';
|
|
17
|
+
*
|
|
18
|
+
* const adapter = createMemoryAdapter([...]);
|
|
19
|
+
* const agenda = createRangeAgenda({ adapter, days: 7 });
|
|
20
|
+
* </script>
|
|
21
|
+
*
|
|
22
|
+
* <button onclick={agenda.prev}>←</button>
|
|
23
|
+
* <button onclick={agenda.next}>→</button>
|
|
24
|
+
*
|
|
25
|
+
* {#each agenda.days.filter((d) => d.events.length > 0) as day}
|
|
26
|
+
* <h3>{day.date.toLocaleDateString()}</h3>
|
|
27
|
+
* {#each day.events as ev}
|
|
28
|
+
* <a href="/book?slot={ev.id}">{agenda.fmtTime(ev.start)} {ev.title}</a>
|
|
29
|
+
* {/each}
|
|
30
|
+
* {/each}
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
import { untrack } from 'svelte';
|
|
34
|
+
import { createEventStore } from '../engine/event-store.svelte.js';
|
|
35
|
+
import { sod, DAY_MS } from '../core/time.js';
|
|
36
|
+
import { fmtTime as _fmtTime, fmtDuration } from '../core/locale.js';
|
|
37
|
+
// ─── Implementation ─────────────────────────────────────
|
|
38
|
+
export function createRangeAgenda(options) {
|
|
39
|
+
const { initialDate, locale, days: dayCount = 7 } = options;
|
|
40
|
+
const resolveAdapter = typeof options.adapter === 'function'
|
|
41
|
+
? options.adapter
|
|
42
|
+
: () => options.adapter;
|
|
43
|
+
const store = $derived(createEventStore(resolveAdapter()));
|
|
44
|
+
// ── Window start (reactive, writable) ──
|
|
45
|
+
let startMs = $state(sod(initialDate?.getTime() ?? Date.now()));
|
|
46
|
+
const endMs = $derived(startMs + dayCount * DAY_MS);
|
|
47
|
+
// ── Load events for the window (re-runs on paging / adapter swap) ──
|
|
48
|
+
$effect(() => {
|
|
49
|
+
store.load({ start: new Date(startMs), end: new Date(endMs) });
|
|
50
|
+
});
|
|
51
|
+
// Eager initial load
|
|
52
|
+
untrack(() => {
|
|
53
|
+
store.load({ start: new Date(startMs), end: new Date(endMs) });
|
|
54
|
+
});
|
|
55
|
+
// ── Day derivations ──
|
|
56
|
+
const days = $derived.by(() => {
|
|
57
|
+
const todayMs = sod(Date.now());
|
|
58
|
+
const events = store.events;
|
|
59
|
+
return Array.from({ length: dayCount }, (_, i) => {
|
|
60
|
+
const ms = startMs + i * DAY_MS;
|
|
61
|
+
const dayEnd = ms + DAY_MS;
|
|
62
|
+
const date = new Date(ms);
|
|
63
|
+
// JS getDay(): 0=Sun … 6=Sat → ISO 1=Mon … 7=Sun
|
|
64
|
+
const weekday = ((date.getDay() + 6) % 7) + 1;
|
|
65
|
+
return {
|
|
66
|
+
ms,
|
|
67
|
+
date,
|
|
68
|
+
weekday,
|
|
69
|
+
isToday: ms === todayMs,
|
|
70
|
+
isPast: dayEnd <= todayMs,
|
|
71
|
+
events: events
|
|
72
|
+
.filter((ev) => ev.start.getTime() < dayEnd && ev.end.getTime() > ms)
|
|
73
|
+
.sort((a, b) => a.start.getTime() - b.start.getTime()),
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
const count = $derived(days.reduce((n, d) => n + d.events.length, 0));
|
|
78
|
+
return {
|
|
79
|
+
get days() {
|
|
80
|
+
return days;
|
|
81
|
+
},
|
|
82
|
+
get range() {
|
|
83
|
+
return { start: new Date(startMs), end: new Date(endMs) };
|
|
84
|
+
},
|
|
85
|
+
get count() {
|
|
86
|
+
return count;
|
|
87
|
+
},
|
|
88
|
+
get loading() {
|
|
89
|
+
return store.loading;
|
|
90
|
+
},
|
|
91
|
+
get error() {
|
|
92
|
+
return store.error;
|
|
93
|
+
},
|
|
94
|
+
prev() {
|
|
95
|
+
startMs -= dayCount * DAY_MS;
|
|
96
|
+
},
|
|
97
|
+
next() {
|
|
98
|
+
startMs += dayCount * DAY_MS;
|
|
99
|
+
},
|
|
100
|
+
goToday() {
|
|
101
|
+
startMs = sod(Date.now());
|
|
102
|
+
},
|
|
103
|
+
setDate(date) {
|
|
104
|
+
startMs = sod(date.getTime());
|
|
105
|
+
},
|
|
106
|
+
fmtTime: (d) => _fmtTime(d, locale),
|
|
107
|
+
fmtDuration: (ev) => fmtDuration(ev.start, ev.end),
|
|
108
|
+
fmtRange: (ev) => `${_fmtTime(ev.start, locale)} – ${_fmtTime(ev.end, locale)}`,
|
|
109
|
+
};
|
|
110
|
+
}
|
package/dist/headless/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { createCalendar } from './create-calendar.svelte.js';
|
|
2
2
|
export { createAgenda } from './create-agenda.svelte.js';
|
|
3
3
|
export type { AgendaOptions, HeadlessAgenda } from './create-agenda.svelte.js';
|
|
4
|
+
export { createRangeAgenda } from './create-range-agenda.svelte.js';
|
|
5
|
+
export type { RangeAgendaOptions, RangeAgendaDay, HeadlessRangeAgenda, } from './create-range-agenda.svelte.js';
|
|
4
6
|
export type { HeadlessCalendarOptions, HeadlessCalendar, HeadlessDay, HeadlessWeek, TodayQueue, HeaderContext, NavigationContext, } from './types.js';
|
package/dist/headless/index.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -13,5 +13,5 @@ export { auto, neutral, midnight, presets } from './theme/index.js';
|
|
|
13
13
|
export { probeHostTheme, observeHostTheme } from './theme/index.js';
|
|
14
14
|
export { wrapAdapterWithTimezone } from './core/timezone.js';
|
|
15
15
|
export type { PresetName, AutoThemeOptions } from './theme/index.js';
|
|
16
|
-
export { createCalendar, createAgenda } from './headless/index.js';
|
|
17
|
-
export type { HeadlessCalendarOptions, HeadlessCalendar, HeadlessDay, HeadlessWeek, TodayQueue, HeaderContext, NavigationContext, AgendaOptions, HeadlessAgenda, } from './headless/index.js';
|
|
16
|
+
export { createCalendar, createAgenda, createRangeAgenda } from './headless/index.js';
|
|
17
|
+
export type { HeadlessCalendarOptions, HeadlessCalendar, HeadlessDay, HeadlessWeek, TodayQueue, HeaderContext, NavigationContext, AgendaOptions, HeadlessAgenda, RangeAgendaOptions, RangeAgendaDay, HeadlessRangeAgenda, } from './headless/index.js';
|
package/dist/index.js
CHANGED
|
@@ -16,4 +16,4 @@ export { auto, neutral, midnight, presets } from './theme/index.js';
|
|
|
16
16
|
export { probeHostTheme, observeHostTheme } from './theme/index.js';
|
|
17
17
|
export { wrapAdapterWithTimezone } from './core/timezone.js';
|
|
18
18
|
// ─── Headless API ───────────────────────────────────────
|
|
19
|
-
export { createCalendar, createAgenda } from './headless/index.js';
|
|
19
|
+
export { createCalendar, createAgenda, createRangeAgenda } from './headless/index.js';
|