@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.
Files changed (39) hide show
  1. package/README.md +33 -5
  2. package/dist/calendar/Calendar.svelte +379 -497
  3. package/dist/calendar/Calendar.svelte.d.ts +13 -2
  4. package/dist/core/clock.svelte.d.ts +1 -1
  5. package/dist/core/clock.svelte.js +20 -9
  6. package/dist/core/locale.d.ts +4 -0
  7. package/dist/core/locale.js +4 -0
  8. package/dist/core/timezone.d.ts +12 -0
  9. package/dist/core/timezone.js +31 -0
  10. package/dist/engine/view-state.svelte.d.ts +2 -2
  11. package/dist/engine/view-state.svelte.js +20 -0
  12. package/dist/headless/create-calendar.svelte.js +29 -7
  13. package/dist/headless/types.d.ts +8 -6
  14. package/dist/index.d.ts +3 -0
  15. package/dist/index.js +4 -0
  16. package/dist/primitives/DayHeader.svelte +9 -25
  17. package/dist/primitives/EmptySlot.svelte +15 -31
  18. package/dist/primitives/EventBlock.svelte +33 -61
  19. package/dist/primitives/NowIndicator.svelte +16 -42
  20. package/dist/primitives/TimeGutter.svelte +9 -23
  21. package/dist/views/agenda/Agenda.svelte +3 -10
  22. package/dist/views/agenda/AgendaDay.svelte +195 -167
  23. package/dist/views/agenda/AgendaWeek.svelte +141 -195
  24. package/dist/views/mobile/Mobile.svelte +3 -10
  25. package/dist/views/mobile/MobileDay.svelte +422 -266
  26. package/dist/views/mobile/MobileWeek.svelte +131 -186
  27. package/dist/views/month/MonthGrid.svelte +334 -0
  28. package/dist/views/month/MonthGrid.svelte.d.ts +15 -0
  29. package/dist/views/planner/Planner.svelte +3 -10
  30. package/dist/views/planner/PlannerDay.svelte +636 -546
  31. package/dist/views/planner/PlannerWeek.svelte +299 -412
  32. package/dist/views/shared/EventContent.svelte +16 -0
  33. package/dist/views/shared/EventContent.svelte.d.ts +9 -0
  34. package/dist/views/shared/context.svelte.d.ts +2 -0
  35. package/dist/views/shared/context.svelte.js +2 -0
  36. package/dist/widget/CalendarWidget.svelte +79 -116
  37. package/package.json +3 -2
  38. package/widget/svelte-calendar.css +321 -10
  39. 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 week */
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
- let tick = $state(Date.now());
24
- let today = $state(sod(Date.now()));
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 = Date.now();
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
- onMount(() => {
42
- start();
43
- return destroy;
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; },
@@ -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" */
@@ -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
  };
@@ -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;
@@ -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 store = createEventStore(adapter);
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
- if (!msg.includes('read-only') && !msg.includes('not found')) {
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?.({ start, end });
306
+ oneventcreate?.(timezone
307
+ ? { start: fromZonedTime(start, timezone), end: fromZonedTime(end, timezone) }
308
+ : { start, end });
287
309
  }
288
310
  return result;
289
311
  }
@@ -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: 'day' | 'week';
101
+ mode: ViewMode;
100
102
  /** Available modes */
101
- modes: ('day' | 'week')[];
103
+ modes: ViewMode[];
102
104
  /** Switch to a different mode */
103
- switchMode: (mode: 'day' | 'week') => void;
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: 'day' | 'week';
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: 'day' | 'week';
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
- import { weekdayShort, weekdayLong, monthShort, fmtDay } from '../core/locale.js';
10
-
11
- interface Props {
12
- /** Timestamp (ms) of the day to label */
13
- dayMs: number;
14
- /** Timestamp (ms) of today (for relative labels) */
15
- todayMs?: number;
16
- /** Display format */
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
- import { fmtTime, fmtDuration, getLabels } from '../core/locale.js';
10
-
11
- const L = $derived(getLabels());
12
-
13
- interface Props {
14
- /** Start time of the empty slot */
15
- start: Date;
16
- /** End time of the empty slot */
17
- end: Date;
18
- /** Click handler create event in this range */
19
- onclick?: (range: { start: Date; end: Date }) => void;
20
- /** Orientation for cursor hint */
21
- orientation?: 'horizontal' | 'vertical';
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
- import type { TimelineEvent } from '../core/types.js';
13
- import { fmtTime, fmtDuration, getLabels } from '../core/locale.js';
14
- import type { Snippet } from 'svelte';
15
-
16
- const L = $derived(getLabels());
17
-
18
- interface Props {
19
- event: TimelineEvent;
20
- /** Display variant */
21
- variant?: 'chip' | 'card' | 'row';
22
- /** Is this event currently in-progress? */
23
- active?: boolean;
24
- /** Is this event in the past? */
25
- past?: boolean;
26
- /** Show the time range */
27
- showTime?: boolean;
28
- /** Show the duration */
29
- showDuration?: boolean;
30
- /** Editable (shows resize handles in future) */
31
- editable?: boolean;
32
- /** Click handler */
33
- onclick?: (event: TimelineEvent) => void;
34
- /** Custom content slot */
35
- children?: Snippet<[TimelineEvent]>;
36
- }
37
-
38
- let {
39
- event,
40
- variant = 'chip',
41
- active = false,
42
- past = false,
43
- showTime = false,
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()}