@nomideusz/svelte-calendar 0.7.5 → 0.9.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.
- package/README.md +33 -5
- package/dist/calendar/Calendar.svelte +379 -497
- package/dist/calendar/Calendar.svelte.d.ts +13 -2
- package/dist/core/clock.svelte.d.ts +1 -1
- package/dist/core/clock.svelte.js +20 -9
- package/dist/core/locale.d.ts +4 -0
- package/dist/core/locale.js +4 -0
- package/dist/core/timezone.d.ts +12 -0
- package/dist/core/timezone.js +31 -0
- package/dist/engine/view-state.svelte.d.ts +2 -2
- package/dist/engine/view-state.svelte.js +20 -0
- package/dist/headless/create-calendar.svelte.js +29 -7
- package/dist/headless/types.d.ts +8 -6
- package/dist/index.d.ts +3 -0
- package/dist/index.js +4 -0
- package/dist/primitives/DayHeader.svelte +9 -25
- package/dist/primitives/EmptySlot.svelte +15 -31
- package/dist/primitives/EventBlock.svelte +33 -61
- package/dist/primitives/NowIndicator.svelte +16 -42
- package/dist/primitives/TimeGutter.svelte +9 -23
- package/dist/views/agenda/Agenda.svelte +3 -10
- package/dist/views/agenda/AgendaDay.svelte +195 -167
- package/dist/views/agenda/AgendaWeek.svelte +141 -195
- package/dist/views/mobile/Mobile.svelte +3 -10
- package/dist/views/mobile/MobileDay.svelte +422 -266
- package/dist/views/mobile/MobileWeek.svelte +131 -186
- package/dist/views/month/MonthGrid.svelte +334 -0
- package/dist/views/month/MonthGrid.svelte.d.ts +15 -0
- package/dist/views/planner/Planner.svelte +3 -10
- package/dist/views/planner/PlannerDay.svelte +636 -546
- package/dist/views/planner/PlannerWeek.svelte +299 -412
- package/dist/views/shared/EventContent.svelte +16 -0
- package/dist/views/shared/EventContent.svelte.d.ts +9 -0
- package/dist/views/shared/context.svelte.d.ts +2 -0
- package/dist/views/shared/context.svelte.js +2 -0
- package/dist/widget/CalendarWidget.svelte +79 -116
- package/package.json +3 -2
- package/widget/svelte-calendar.css +321 -10
- package/widget/widget.js +2508 -1056
|
@@ -7,8 +7,8 @@ import type { AutoThemeOptions } from '../theme/auto.js';
|
|
|
7
7
|
export interface CalendarView {
|
|
8
8
|
id: CalendarViewId;
|
|
9
9
|
label: string;
|
|
10
|
-
/** day or
|
|
11
|
-
mode: 'day' | 'week';
|
|
10
|
+
/** day, week or month */
|
|
11
|
+
mode: 'day' | 'week' | 'month';
|
|
12
12
|
/** The Svelte component to render */
|
|
13
13
|
component: Component<Record<string, unknown>>;
|
|
14
14
|
/** Extra props to pass through (e.g. hourHeight, specialized settings) */
|
|
@@ -112,6 +112,17 @@ interface Props {
|
|
|
112
112
|
ondatechange?: (date: Date) => void;
|
|
113
113
|
/** Called when the pointer enters an event (hover). */
|
|
114
114
|
oneventhover?: (event: TimelineEvent) => void;
|
|
115
|
+
/** Called when a day cell is clicked (month grid; more views over time). */
|
|
116
|
+
ondayclick?: (date: Date) => void;
|
|
117
|
+
/** Surfaced instead of silent console output when loading or mutations fail. */
|
|
118
|
+
onerror?: (error: Error) => void;
|
|
119
|
+
/**
|
|
120
|
+
* Render the calendar in an IANA timezone (e.g. 'Europe/Warsaw').
|
|
121
|
+
* Events, "now" and day boundaries all shift; ranges passed to
|
|
122
|
+
* oneventcreate/oneventmove convert back to real instants.
|
|
123
|
+
* Default: the viewer's local time.
|
|
124
|
+
*/
|
|
125
|
+
timezone?: string;
|
|
115
126
|
}
|
|
116
127
|
declare const Calendar: Component<Props, {}, "">;
|
|
117
128
|
type Calendar = ReturnType<typeof Calendar>;
|
|
@@ -18,4 +18,4 @@ export interface Clock {
|
|
|
18
18
|
* Must be called during component initialisation (before first await).
|
|
19
19
|
* Automatically cleans up on unmount via onMount return.
|
|
20
20
|
*/
|
|
21
|
-
export declare function createClock(): Clock;
|
|
21
|
+
export declare function createClock(timezone?: string): Clock;
|
|
@@ -13,19 +13,23 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { onMount } from 'svelte';
|
|
15
15
|
import { sod, fmtHM, fmtS, fractionalHour } from './time.js';
|
|
16
|
+
import { toZonedTime } from './timezone.js';
|
|
16
17
|
/**
|
|
17
18
|
* Create a shared reactive clock.
|
|
18
19
|
*
|
|
19
20
|
* Must be called during component initialisation (before first await).
|
|
20
21
|
* Automatically cleans up on unmount via onMount return.
|
|
21
22
|
*/
|
|
22
|
-
export function createClock() {
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
export function createClock(timezone) {
|
|
24
|
+
// With a timezone, ticks are "zoned wall-clock" epoch ms — the same plane
|
|
25
|
+
// the wrapped adapter shifts event Dates into. Views stay zone-agnostic.
|
|
26
|
+
const now = () => (timezone ? toZonedTime(Date.now(), timezone).getTime() : Date.now());
|
|
27
|
+
let tick = $state(now());
|
|
28
|
+
let today = $state(sod(tick));
|
|
25
29
|
let intervalId = null;
|
|
26
30
|
function start() {
|
|
27
31
|
intervalId = setInterval(() => {
|
|
28
|
-
tick =
|
|
32
|
+
tick = now();
|
|
29
33
|
const sd = sod(tick);
|
|
30
34
|
if (sd !== today)
|
|
31
35
|
today = sd;
|
|
@@ -37,11 +41,18 @@ export function createClock() {
|
|
|
37
41
|
intervalId = null;
|
|
38
42
|
}
|
|
39
43
|
}
|
|
40
|
-
// Auto-start and auto-cleanup
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
// Auto-start and auto-cleanup.
|
|
45
|
+
// Outside component initialisation (headless usage, tests) onMount throws —
|
|
46
|
+
// fall back to a static clock; callers may still call destroy().
|
|
47
|
+
try {
|
|
48
|
+
onMount(() => {
|
|
49
|
+
start();
|
|
50
|
+
return destroy;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// lifecycle_outside_component — no interval, clock stays at creation time
|
|
55
|
+
}
|
|
45
56
|
return {
|
|
46
57
|
get tick() { return tick; },
|
|
47
58
|
get today() { return today; },
|
package/dist/core/locale.d.ts
CHANGED
|
@@ -17,6 +17,7 @@ export interface CalendarLabels {
|
|
|
17
17
|
tomorrow: string;
|
|
18
18
|
day: string;
|
|
19
19
|
week: string;
|
|
20
|
+
month: string;
|
|
20
21
|
planner: string;
|
|
21
22
|
agenda: string;
|
|
22
23
|
now: string;
|
|
@@ -34,7 +35,9 @@ export interface CalendarLabels {
|
|
|
34
35
|
previousDay: string;
|
|
35
36
|
nextDay: string;
|
|
36
37
|
previousWeek: string;
|
|
38
|
+
previousMonth: string;
|
|
37
39
|
nextWeek: string;
|
|
40
|
+
nextMonth: string;
|
|
38
41
|
calendar: string;
|
|
39
42
|
viewMode: string;
|
|
40
43
|
dayNavigation: string;
|
|
@@ -56,6 +59,7 @@ export interface CalendarLabels {
|
|
|
56
59
|
nEvents: (n: number) => string;
|
|
57
60
|
/** e.g. "3 completed" */
|
|
58
61
|
nCompleted: (n: number) => string;
|
|
62
|
+
showLess: string;
|
|
59
63
|
/** e.g. "day 2 of 4" */
|
|
60
64
|
dayNOfTotal: (current: number, total: number) => string;
|
|
61
65
|
/** e.g. "75% complete" */
|
package/dist/core/locale.js
CHANGED
|
@@ -15,6 +15,7 @@ export const defaultLabels = {
|
|
|
15
15
|
tomorrow: 'Tomorrow',
|
|
16
16
|
day: 'Day',
|
|
17
17
|
week: 'Week',
|
|
18
|
+
month: 'Month',
|
|
18
19
|
planner: 'Planner',
|
|
19
20
|
agenda: 'Agenda',
|
|
20
21
|
now: 'now',
|
|
@@ -32,7 +33,9 @@ export const defaultLabels = {
|
|
|
32
33
|
previousDay: 'Previous day',
|
|
33
34
|
nextDay: 'Next day',
|
|
34
35
|
previousWeek: 'Previous week',
|
|
36
|
+
previousMonth: 'Previous month',
|
|
35
37
|
nextWeek: 'Next week',
|
|
38
|
+
nextMonth: 'Next month',
|
|
36
39
|
calendar: 'Calendar',
|
|
37
40
|
viewMode: 'View mode',
|
|
38
41
|
dayNavigation: 'Day navigation',
|
|
@@ -51,6 +54,7 @@ export const defaultLabels = {
|
|
|
51
54
|
nMore: (n) => `+${n} more`,
|
|
52
55
|
nEvents: (n) => `${n} event${n === 1 ? '' : 's'}`,
|
|
53
56
|
nCompleted: (n) => `${n} completed`,
|
|
57
|
+
showLess: 'Show less',
|
|
54
58
|
dayNOfTotal: (current, total) => `day ${current} of ${total}`,
|
|
55
59
|
percentComplete: (pct) => `${pct}% complete`,
|
|
56
60
|
};
|
package/dist/core/timezone.d.ts
CHANGED
|
@@ -20,3 +20,15 @@ export declare function nowInZone(timezone: string): Date;
|
|
|
20
20
|
* Returns a locale-aware string.
|
|
21
21
|
*/
|
|
22
22
|
export declare function formatInTimeZone(date: Date | number, timezone: string, options?: Intl.DateTimeFormatOptions, locale?: string): string;
|
|
23
|
+
/**
|
|
24
|
+
* Wrap a CalendarAdapter so everything it emits is expressed as wall-clock
|
|
25
|
+
* Dates in `timezone`, and everything written through it is converted back to
|
|
26
|
+
* real instants. This is how the Calendar's `timezone` prop works: views keep
|
|
27
|
+
* doing plain local-time math on an already-shifted plane.
|
|
28
|
+
*
|
|
29
|
+
* Known limit shared by every wall-clock calendar UI: during a DST fall-back
|
|
30
|
+
* the repeated hour is ambiguous on the wall clock, so writes made inside it
|
|
31
|
+
* resolve to one of the two instants (date-fns-tz picks the offset).
|
|
32
|
+
*/
|
|
33
|
+
import type { CalendarAdapter } from '../adapters/types.js';
|
|
34
|
+
export declare function wrapAdapterWithTimezone(adapter: CalendarAdapter, timezone: string): CalendarAdapter;
|
package/dist/core/timezone.js
CHANGED
|
@@ -51,3 +51,34 @@ export function formatInTimeZone(date, timezone, options = {}, locale) {
|
|
|
51
51
|
timeZone: timezone,
|
|
52
52
|
}).format(d);
|
|
53
53
|
}
|
|
54
|
+
export function wrapAdapterWithTimezone(adapter, timezone) {
|
|
55
|
+
const zoneEvent = (ev) => ({
|
|
56
|
+
...ev,
|
|
57
|
+
start: toZonedTime(ev.start, timezone),
|
|
58
|
+
end: toZonedTime(ev.end, timezone),
|
|
59
|
+
});
|
|
60
|
+
const unzonePartial = (obj) => ({
|
|
61
|
+
...obj,
|
|
62
|
+
...(obj.start instanceof Date ? { start: fromZonedTime(obj.start, timezone) } : {}),
|
|
63
|
+
...(obj.end instanceof Date ? { end: fromZonedTime(obj.end, timezone) } : {}),
|
|
64
|
+
});
|
|
65
|
+
const wrapped = {
|
|
66
|
+
async fetchEvents(range) {
|
|
67
|
+
const events = await adapter.fetchEvents({
|
|
68
|
+
start: fromZonedTime(range.start, timezone),
|
|
69
|
+
end: fromZonedTime(range.end, timezone),
|
|
70
|
+
});
|
|
71
|
+
return events.map(zoneEvent);
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
if (adapter.createEvent) {
|
|
75
|
+
wrapped.createEvent = async (event) => zoneEvent(await adapter.createEvent(unzonePartial(event)));
|
|
76
|
+
}
|
|
77
|
+
if (adapter.updateEvent) {
|
|
78
|
+
wrapped.updateEvent = async (id, patch) => zoneEvent(await adapter.updateEvent(id, unzonePartial(patch)));
|
|
79
|
+
}
|
|
80
|
+
if (adapter.deleteEvent) {
|
|
81
|
+
wrapped.deleteEvent = (id) => adapter.deleteEvent(id);
|
|
82
|
+
}
|
|
83
|
+
return wrapped;
|
|
84
|
+
}
|
|
@@ -4,13 +4,13 @@ export type { DateRange };
|
|
|
4
4
|
* Built-in view IDs. Custom view IDs are also supported — CalendarViewId
|
|
5
5
|
* is typed as `string` so consumers can register any ID.
|
|
6
6
|
*/
|
|
7
|
-
export type BuiltInViewId = 'day-planner' | 'day-agenda' | 'week-planner' | 'week-agenda';
|
|
7
|
+
export type BuiltInViewId = 'day-planner' | 'day-agenda' | 'week-planner' | 'week-agenda' | 'month-grid';
|
|
8
8
|
/**
|
|
9
9
|
* Any view identifier. Use built-in strings like 'day-planner' or your own
|
|
10
10
|
* custom IDs like 'day-kanban', 'week-resource', etc.
|
|
11
11
|
*/
|
|
12
12
|
export type CalendarViewId = string;
|
|
13
|
-
export type ViewMode = 'day' | 'week';
|
|
13
|
+
export type ViewMode = 'day' | 'week' | 'month';
|
|
14
14
|
export interface ViewStateOptions {
|
|
15
15
|
view?: CalendarViewId;
|
|
16
16
|
mondayStart?: boolean;
|
|
@@ -15,6 +15,8 @@ import { startOfWeek as calcStartOfWeek, addDaysMs, DAY_MS } from '../core/time.
|
|
|
15
15
|
function inferMode(view) {
|
|
16
16
|
if (view.startsWith('day'))
|
|
17
17
|
return 'day';
|
|
18
|
+
if (view.startsWith('month'))
|
|
19
|
+
return 'month';
|
|
18
20
|
return 'week';
|
|
19
21
|
}
|
|
20
22
|
function computeRange(focus, mode, mondayStart, dayCount = 7) {
|
|
@@ -24,6 +26,15 @@ function computeRange(focus, mode, mondayStart, dayCount = 7) {
|
|
|
24
26
|
const end = new Date(start.getTime() + DAY_MS);
|
|
25
27
|
return { start, end };
|
|
26
28
|
}
|
|
29
|
+
if (mode === 'month') {
|
|
30
|
+
// Week-aligned grid covering the focus month: 4–6 rows depending on
|
|
31
|
+
// where the month's edges fall.
|
|
32
|
+
const first = new Date(focus.getFullYear(), focus.getMonth(), 1);
|
|
33
|
+
const last = new Date(focus.getFullYear(), focus.getMonth() + 1, 0);
|
|
34
|
+
const gridStart = calcStartOfWeek(first.getTime(), mondayStart);
|
|
35
|
+
const gridEnd = addDaysMs(calcStartOfWeek(last.getTime(), mondayStart), 7);
|
|
36
|
+
return { start: new Date(gridStart), end: new Date(gridEnd) };
|
|
37
|
+
}
|
|
27
38
|
// week / custom period
|
|
28
39
|
if (dayCount === 7) {
|
|
29
40
|
const ws = calcStartOfWeek(focus.getTime(), mondayStart);
|
|
@@ -84,10 +95,19 @@ export function createViewState(options = {}) {
|
|
|
84
95
|
dayCount = n;
|
|
85
96
|
},
|
|
86
97
|
next() {
|
|
98
|
+
if (mode === 'month') {
|
|
99
|
+
// Anchor to the 1st so a Jan 31 focus can't skip February.
|
|
100
|
+
focusDate = new Date(focusDate.getFullYear(), focusDate.getMonth() + 1, 1);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
87
103
|
const days = mode === 'day' ? 1 : dayCount;
|
|
88
104
|
focusDate = new Date(addDaysMs(focusDate.getTime(), days));
|
|
89
105
|
},
|
|
90
106
|
prev() {
|
|
107
|
+
if (mode === 'month') {
|
|
108
|
+
focusDate = new Date(focusDate.getFullYear(), focusDate.getMonth() - 1, 1);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
91
111
|
const days = mode === 'day' ? -1 : -dayCount;
|
|
92
112
|
focusDate = new Date(addDaysMs(focusDate.getTime(), days));
|
|
93
113
|
},
|
|
@@ -42,15 +42,18 @@
|
|
|
42
42
|
import { untrack } from 'svelte';
|
|
43
43
|
import { createEventStore } from '../engine/event-store.svelte.js';
|
|
44
44
|
import { createViewState } from '../engine/view-state.svelte.js';
|
|
45
|
+
import { toZonedTime, fromZonedTime, wrapAdapterWithTimezone } from '../core/timezone.js';
|
|
45
46
|
import { createSelection } from '../engine/selection.svelte.js';
|
|
46
47
|
import { createDragState } from '../engine/drag.svelte.js';
|
|
47
48
|
import { createClock } from '../core/clock.svelte.js';
|
|
48
49
|
import { sod, DAY_MS, HOUR_MS, startOfWeek as sowFn, isAllDay, isMultiDay, segmentForDay, } from '../core/time.js';
|
|
49
50
|
import { monthLong, weekdayLong, weekdayShort } from '../core/locale.js';
|
|
50
51
|
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
|
+
const { adapter, mondayStart: initialMondayStart = true, initialDate: rawInitialDate, locale, visibleHours, snapInterval = 15, equalDays = false, hideDays, blockedSlots, disabledDates, days: initialDayCount = 7, readOnly = false, minDuration, maxDuration, oneventclick, oneventcreate, oneventmove, } = options;
|
|
52
53
|
// ── Create engine ────────────────────────────────────
|
|
53
|
-
const
|
|
54
|
+
const timezone = options.timezone;
|
|
55
|
+
const initialDate = rawInitialDate && timezone ? toZonedTime(rawInitialDate, timezone) : rawInitialDate;
|
|
56
|
+
const store = createEventStore(timezone ? wrapAdapterWithTimezone(adapter, timezone) : adapter);
|
|
54
57
|
const viewState = createViewState({
|
|
55
58
|
view: options.view ?? 'week-planner',
|
|
56
59
|
mondayStart: initialMondayStart,
|
|
@@ -60,7 +63,8 @@ export function createCalendar(options) {
|
|
|
60
63
|
});
|
|
61
64
|
const selection = createSelection();
|
|
62
65
|
const drag = createDragState();
|
|
63
|
-
const clock = createClock();
|
|
66
|
+
const clock = createClock(timezone);
|
|
67
|
+
const unzoneDate = (d) => (timezone ? fromZonedTime(d, timezone) : d);
|
|
64
68
|
// ── Computed ──────────────────────────────────────────
|
|
65
69
|
const disabledSet = $derived(new Set(disabledDates?.map((d) => sod(d.getTime())) ?? []));
|
|
66
70
|
const startHour = visibleHours?.[0] ?? 0;
|
|
@@ -182,7 +186,7 @@ export function createCalendar(options) {
|
|
|
182
186
|
return {
|
|
183
187
|
dateLabel,
|
|
184
188
|
mode,
|
|
185
|
-
modes: ['day', 'week'],
|
|
189
|
+
modes: ['day', 'week', 'month'],
|
|
186
190
|
switchMode: (m) => {
|
|
187
191
|
const currentView = viewState.view;
|
|
188
192
|
const currentLabel = currentView.replace(/^(day|week)-/, '');
|
|
@@ -220,6 +224,15 @@ export function createCalendar(options) {
|
|
|
220
224
|
let { start, end } = payload;
|
|
221
225
|
// Enforce min/max duration
|
|
222
226
|
if (mode === 'create' || mode === 'resize-start' || mode === 'resize-end') {
|
|
227
|
+
// Defensive floor: never accept a zero/negative duration, even when
|
|
228
|
+
// minDuration is unset (views clamp, but the engine must hold alone).
|
|
229
|
+
if (end.getTime() <= start.getTime()) {
|
|
230
|
+
const floorMs = Math.max(1, snapInterval) * 60_000;
|
|
231
|
+
if (mode === 'resize-start')
|
|
232
|
+
start = new Date(end.getTime() - floorMs);
|
|
233
|
+
else
|
|
234
|
+
end = new Date(start.getTime() + floorMs);
|
|
235
|
+
}
|
|
223
236
|
const durationMs = end.getTime() - start.getTime();
|
|
224
237
|
const durationMin = durationMs / 60_000;
|
|
225
238
|
if (minDuration && durationMin < minDuration) {
|
|
@@ -272,18 +285,27 @@ export function createCalendar(options) {
|
|
|
272
285
|
await store.move(payload.eventId, start, end);
|
|
273
286
|
const ev = store.byId(payload.eventId);
|
|
274
287
|
if (ev)
|
|
275
|
-
oneventmove?.(ev, start, end);
|
|
288
|
+
oneventmove?.(ev, unzoneDate(start), unzoneDate(end));
|
|
276
289
|
}
|
|
277
290
|
catch (e) {
|
|
278
291
|
const msg = e instanceof Error ? e.message : '';
|
|
279
|
-
|
|
292
|
+
// Read-only adapter: the host still gets the callback — it owns
|
|
293
|
+
// persistence and refetches.
|
|
294
|
+
if (msg.includes('read-only')) {
|
|
295
|
+
const ev = store.byId(payload.eventId);
|
|
296
|
+
if (ev)
|
|
297
|
+
oneventmove?.(ev, unzoneDate(start), unzoneDate(end));
|
|
298
|
+
}
|
|
299
|
+
else if (!msg.includes('not found')) {
|
|
280
300
|
console.warn('[calendar] drag commit failed:', e);
|
|
281
301
|
}
|
|
282
302
|
return null;
|
|
283
303
|
}
|
|
284
304
|
}
|
|
285
305
|
else if (mode === 'create') {
|
|
286
|
-
oneventcreate?.(
|
|
306
|
+
oneventcreate?.(timezone
|
|
307
|
+
? { start: fromZonedTime(start, timezone), end: fromZonedTime(end, timezone) }
|
|
308
|
+
: { start, end });
|
|
287
309
|
}
|
|
288
310
|
return result;
|
|
289
311
|
}
|
package/dist/headless/types.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import type { CalendarAdapter } from '../adapters/types.js';
|
|
8
8
|
import type { TimelineEvent, BlockedSlot } from '../core/types.js';
|
|
9
9
|
import type { DaySegment } from '../core/time.js';
|
|
10
|
-
import type { CalendarViewId } from '../engine/view-state.svelte.js';
|
|
10
|
+
import type { CalendarViewId, ViewMode } from '../engine/view-state.svelte.js';
|
|
11
11
|
import type { EventStore } from '../engine/event-store.svelte.js';
|
|
12
12
|
import type { ViewState } from '../engine/view-state.svelte.js';
|
|
13
13
|
import type { Selection } from '../engine/selection.svelte.js';
|
|
@@ -22,6 +22,8 @@ export interface HeadlessCalendarOptions {
|
|
|
22
22
|
mondayStart?: boolean;
|
|
23
23
|
/** Initial date to focus on (default: today) */
|
|
24
24
|
initialDate?: Date;
|
|
25
|
+
/** Render in an IANA timezone; events, "now" and day boundaries shift. */
|
|
26
|
+
timezone?: string;
|
|
25
27
|
/** BCP 47 locale tag (e.g. 'en-US', 'pl-PL') */
|
|
26
28
|
locale?: string;
|
|
27
29
|
/** Visible hour range [startHour, endHour). Default: [0, 24] */
|
|
@@ -96,11 +98,11 @@ export interface HeaderContext {
|
|
|
96
98
|
/** Formatted date label for current view */
|
|
97
99
|
dateLabel: string;
|
|
98
100
|
/** Current mode */
|
|
99
|
-
mode:
|
|
101
|
+
mode: ViewMode;
|
|
100
102
|
/** Available modes */
|
|
101
|
-
modes:
|
|
103
|
+
modes: ViewMode[];
|
|
102
104
|
/** Switch to a different mode */
|
|
103
|
-
switchMode: (mode:
|
|
105
|
+
switchMode: (mode: ViewMode) => void;
|
|
104
106
|
/** Go to previous period */
|
|
105
107
|
prev: () => void;
|
|
106
108
|
/** Go to next period */
|
|
@@ -118,7 +120,7 @@ export interface NavigationContext {
|
|
|
118
120
|
goToday: () => void;
|
|
119
121
|
isViewOnToday: boolean;
|
|
120
122
|
focusDate: Date;
|
|
121
|
-
mode:
|
|
123
|
+
mode: ViewMode;
|
|
122
124
|
}
|
|
123
125
|
export interface HeadlessCalendar {
|
|
124
126
|
/** All loaded events for the current range */
|
|
@@ -128,7 +130,7 @@ export interface HeadlessCalendar {
|
|
|
128
130
|
/** The currently focused date */
|
|
129
131
|
readonly focusDate: Date;
|
|
130
132
|
/** Current mode: 'day' or 'week' */
|
|
131
|
-
readonly mode:
|
|
133
|
+
readonly mode: ViewMode;
|
|
132
134
|
/** Current view ID */
|
|
133
135
|
readonly view: CalendarViewId;
|
|
134
136
|
/** Visible date range */
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export { NowIndicator, EventBlock, TimeGutter, DayHeader, EmptySlot, } from './primitives/index.js';
|
|
2
2
|
export { Calendar } from './calendar/index.js';
|
|
3
|
+
export { Planner, Agenda, Mobile } from './views/index.js';
|
|
4
|
+
export { default as MonthGrid } from './views/month/MonthGrid.svelte';
|
|
3
5
|
export type { CalendarView } from './calendar/index.js';
|
|
4
6
|
export { createEventStore, createViewState, createSelection, createDragState, } from './engine/index.js';
|
|
5
7
|
export type { EventStore, ViewState, ViewStateOptions, CalendarViewId, BuiltInViewId, ViewMode, Selection, DragState, DragMode, DragPayload, } from './engine/index.js';
|
|
@@ -9,6 +11,7 @@ export { createClock, startOfWeek, fmtH, fmtTime, fmtDuration, weekdayShort, wee
|
|
|
9
11
|
export type { Clock, TimelineEvent, BlockedSlot, DaySegment, CalendarLabels, EventStatus, TextMeasure, TextMeasureOptions, ContentFit, } from './core/index.js';
|
|
10
12
|
export { auto, neutral, midnight, presets } from './theme/index.js';
|
|
11
13
|
export { probeHostTheme, observeHostTheme } from './theme/index.js';
|
|
14
|
+
export { wrapAdapterWithTimezone } from './core/timezone.js';
|
|
12
15
|
export type { PresetName, AutoThemeOptions } from './theme/index.js';
|
|
13
16
|
export { createCalendar, createAgenda } from './headless/index.js';
|
|
14
17
|
export type { HeadlessCalendarOptions, HeadlessCalendar, HeadlessDay, HeadlessWeek, TodayQueue, HeaderContext, NavigationContext, AgendaOptions, HeadlessAgenda, } from './headless/index.js';
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
export { NowIndicator, EventBlock, TimeGutter, DayHeader, EmptySlot, } from './primitives/index.js';
|
|
3
3
|
// ─── Calendar shell ─────────────────────────────────────
|
|
4
4
|
export { Calendar } from './calendar/index.js';
|
|
5
|
+
// Raw view components — compose your own shell around the engine if needed
|
|
6
|
+
export { Planner, Agenda, Mobile } from './views/index.js';
|
|
7
|
+
export { default as MonthGrid } from './views/month/MonthGrid.svelte';
|
|
5
8
|
// ─── Engine (reactive state) ────────────────────────────
|
|
6
9
|
export { createEventStore, createViewState, createSelection, createDragState, } from './engine/index.js';
|
|
7
10
|
// ─── Adapters ───────────────────────────────────────────
|
|
@@ -11,5 +14,6 @@ export { createClock, startOfWeek, fmtH, fmtTime, fmtDuration, weekdayShort, wee
|
|
|
11
14
|
// ─── Themes ─────────────────────────────────────────────
|
|
12
15
|
export { auto, neutral, midnight, presets } from './theme/index.js';
|
|
13
16
|
export { probeHostTheme, observeHostTheme } from './theme/index.js';
|
|
17
|
+
export { wrapAdapterWithTimezone } from './core/timezone.js';
|
|
14
18
|
// ─── Headless API ───────────────────────────────────────
|
|
15
19
|
export { createCalendar, createAgenda } from './headless/index.js';
|
|
@@ -5,31 +5,15 @@
|
|
|
5
5
|
- Relative labels ("Today", "Tomorrow") for day views via fmtDay
|
|
6
6
|
- Short/long weekday + date number for week views
|
|
7
7
|
-->
|
|
8
|
-
<script lang="ts">
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
format?: 'relative' | 'short' | 'long';
|
|
18
|
-
/** Whether this day is "today" */
|
|
19
|
-
isToday?: boolean;
|
|
20
|
-
/** Whether this day is in the past */
|
|
21
|
-
isPast?: boolean;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
let {
|
|
25
|
-
dayMs,
|
|
26
|
-
todayMs = Date.now(),
|
|
27
|
-
format = 'short',
|
|
28
|
-
isToday = false,
|
|
29
|
-
isPast = false,
|
|
30
|
-
}: Props = $props();
|
|
31
|
-
|
|
32
|
-
const dayNum = $derived(new Date(dayMs).getDate());
|
|
8
|
+
<script lang="ts">import { weekdayShort, weekdayLong, monthShort, fmtDay } from "../core/locale.js";
|
|
9
|
+
let {
|
|
10
|
+
dayMs,
|
|
11
|
+
todayMs = Date.now(),
|
|
12
|
+
format = "short",
|
|
13
|
+
isToday = false,
|
|
14
|
+
isPast = false
|
|
15
|
+
} = $props();
|
|
16
|
+
const dayNum = $derived(new Date(dayMs).getDate());
|
|
33
17
|
</script>
|
|
34
18
|
|
|
35
19
|
<div
|
|
@@ -5,37 +5,21 @@
|
|
|
5
5
|
the time range when clicked. View components place these in gaps
|
|
6
6
|
between events.
|
|
7
7
|
-->
|
|
8
|
-
<script lang="ts">
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
let {
|
|
25
|
-
start,
|
|
26
|
-
end,
|
|
27
|
-
onclick,
|
|
28
|
-
orientation = 'vertical',
|
|
29
|
-
}: Props = $props();
|
|
30
|
-
|
|
31
|
-
const dur = $derived(`${fmtDuration(start, end)} ${L.free}`);
|
|
32
|
-
|
|
33
|
-
function handleKeydown(e: KeyboardEvent) {
|
|
34
|
-
if (e.key === 'Enter' || e.key === ' ') {
|
|
35
|
-
e.preventDefault();
|
|
36
|
-
onclick?.({ start, end });
|
|
37
|
-
}
|
|
38
|
-
}
|
|
8
|
+
<script lang="ts">import { fmtTime, fmtDuration, getLabels } from "../core/locale.js";
|
|
9
|
+
const L = $derived(getLabels());
|
|
10
|
+
let {
|
|
11
|
+
start,
|
|
12
|
+
end,
|
|
13
|
+
onclick,
|
|
14
|
+
orientation = "vertical"
|
|
15
|
+
} = $props();
|
|
16
|
+
const dur = $derived(`${fmtDuration(start, end)} ${L.free}`);
|
|
17
|
+
function handleKeydown(e) {
|
|
18
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
19
|
+
e.preventDefault();
|
|
20
|
+
onclick?.({ start, end });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
39
23
|
</script>
|
|
40
24
|
|
|
41
25
|
<div
|
|
@@ -8,67 +8,39 @@
|
|
|
8
8
|
|
|
9
9
|
Emits: onclick, ondragstart, onresize (for future interaction wiring)
|
|
10
10
|
-->
|
|
11
|
-
<script lang="ts">
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
showDuration = false,
|
|
45
|
-
editable = false,
|
|
46
|
-
onclick,
|
|
47
|
-
children,
|
|
48
|
-
}: Props = $props();
|
|
49
|
-
|
|
50
|
-
const accentColor = $derived(event.color || 'var(--dt-accent, #2563eb)');
|
|
51
|
-
const isCancelled = $derived(event.status === 'cancelled');
|
|
52
|
-
const isTentative = $derived(event.status === 'tentative');
|
|
53
|
-
const isFull = $derived(event.status === 'full');
|
|
54
|
-
const isLimited = $derived(event.status === 'limited');
|
|
55
|
-
|
|
56
|
-
const ariaLabel = $derived.by(() => {
|
|
57
|
-
const t = event.title;
|
|
58
|
-
const time = `${fmtTime(event.start)} to ${fmtTime(event.end)}`;
|
|
59
|
-
const dur = fmtDuration(event.start, event.end);
|
|
60
|
-
const loc = event.location ? `, ${event.location}` : '';
|
|
61
|
-
const statusStr = isCancelled ? ', cancelled' : isTentative ? ', tentative' : isFull ? ', full' : isLimited ? ', limited' : '';
|
|
62
|
-
const activeStr = active ? `, ${L.happeningNow}` : past ? `, ${L.past}` : '';
|
|
63
|
-
return `${t}${loc}, ${time}, ${dur}${statusStr}${activeStr}`;
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
function handleKeydown(e: KeyboardEvent) {
|
|
67
|
-
if (e.key === 'Enter' || e.key === ' ') {
|
|
68
|
-
e.preventDefault();
|
|
69
|
-
onclick?.(event);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
11
|
+
<script lang="ts">import { fmtTime, fmtDuration, getLabels } from "../core/locale.js";
|
|
12
|
+
const L = $derived(getLabels());
|
|
13
|
+
let {
|
|
14
|
+
event,
|
|
15
|
+
variant = "chip",
|
|
16
|
+
active = false,
|
|
17
|
+
past = false,
|
|
18
|
+
showTime = false,
|
|
19
|
+
showDuration = false,
|
|
20
|
+
editable = false,
|
|
21
|
+
onclick,
|
|
22
|
+
children
|
|
23
|
+
} = $props();
|
|
24
|
+
const accentColor = $derived(event.color || "var(--dt-accent, #2563eb)");
|
|
25
|
+
const isCancelled = $derived(event.status === "cancelled");
|
|
26
|
+
const isTentative = $derived(event.status === "tentative");
|
|
27
|
+
const isFull = $derived(event.status === "full");
|
|
28
|
+
const isLimited = $derived(event.status === "limited");
|
|
29
|
+
const ariaLabel = $derived.by(() => {
|
|
30
|
+
const t = event.title;
|
|
31
|
+
const time = `${fmtTime(event.start)} to ${fmtTime(event.end)}`;
|
|
32
|
+
const dur = fmtDuration(event.start, event.end);
|
|
33
|
+
const loc = event.location ? `, ${event.location}` : "";
|
|
34
|
+
const statusStr = isCancelled ? ", cancelled" : isTentative ? ", tentative" : isFull ? ", full" : isLimited ? ", limited" : "";
|
|
35
|
+
const activeStr = active ? `, ${L.happeningNow}` : past ? `, ${L.past}` : "";
|
|
36
|
+
return `${t}${loc}, ${time}, ${dur}${statusStr}${activeStr}`;
|
|
37
|
+
});
|
|
38
|
+
function handleKeydown(e) {
|
|
39
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
40
|
+
e.preventDefault();
|
|
41
|
+
onclick?.(event);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
72
44
|
</script>
|
|
73
45
|
|
|
74
46
|
{#snippet content()}
|