@nomideusz/svelte-calendar 0.3.1 → 0.5.1

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 (75) hide show
  1. package/README.md +193 -406
  2. package/dist/adapters/recurring.d.ts +45 -8
  3. package/dist/adapters/recurring.js +227 -49
  4. package/dist/adapters/rest.js +6 -1
  5. package/dist/calendar/Calendar.svelte +137 -29
  6. package/dist/calendar/Calendar.svelte.d.ts +10 -8
  7. package/dist/calendar/index.d.ts +0 -1
  8. package/dist/calendar/index.js +0 -1
  9. package/dist/core/index.d.ts +5 -4
  10. package/dist/core/index.js +2 -3
  11. package/dist/core/locale.d.ts +74 -0
  12. package/dist/core/locale.js +96 -4
  13. package/dist/core/time.d.ts +30 -0
  14. package/dist/core/time.js +42 -0
  15. package/dist/core/types.d.ts +1 -72
  16. package/dist/core/types.js +1 -8
  17. package/dist/engine/event-store.svelte.js +2 -2
  18. package/dist/engine/index.d.ts +1 -1
  19. package/dist/engine/view-state.svelte.d.ts +12 -6
  20. package/dist/engine/view-state.svelte.js +8 -4
  21. package/dist/index.d.ts +6 -7
  22. package/dist/index.js +4 -4
  23. package/dist/primitives/EmptySlot.svelte +6 -16
  24. package/dist/primitives/EventBlock.svelte +6 -19
  25. package/dist/primitives/NowIndicator.svelte +7 -4
  26. package/dist/primitives/TimeGutter.svelte +1 -1
  27. package/dist/theme/index.d.ts +1 -1
  28. package/dist/theme/index.js +1 -1
  29. package/dist/theme/presets.d.ts +11 -19
  30. package/dist/theme/presets.js +25 -111
  31. package/dist/views/agenda/Agenda.svelte +17 -1514
  32. package/dist/views/agenda/Agenda.svelte.d.ts +0 -13
  33. package/dist/views/agenda/AgendaDay.svelte +976 -0
  34. package/dist/views/agenda/AgendaDay.svelte.d.ts +14 -0
  35. package/dist/views/agenda/AgendaWeek.svelte +775 -0
  36. package/dist/views/agenda/AgendaWeek.svelte.d.ts +15 -0
  37. package/dist/views/agenda/index.d.ts +2 -0
  38. package/dist/views/agenda/index.js +2 -0
  39. package/dist/views/index.d.ts +2 -6
  40. package/dist/views/index.js +2 -6
  41. package/dist/views/planner/Planner.svelte +17 -0
  42. package/dist/views/planner/Planner.svelte.d.ts +7 -0
  43. package/dist/views/planner/PlannerDay.svelte +957 -0
  44. package/dist/views/{day/DayGrid.svelte.d.ts → planner/PlannerDay.svelte.d.ts} +8 -16
  45. package/dist/views/planner/PlannerWeek.svelte +826 -0
  46. package/dist/views/{week/WeekGrid.svelte.d.ts → planner/PlannerWeek.svelte.d.ts} +5 -9
  47. package/dist/views/planner/index.d.ts +1 -0
  48. package/dist/views/planner/index.js +1 -0
  49. package/dist/widget/CalendarWidget.svelte +54 -35
  50. package/dist/widget/CalendarWidget.svelte.d.ts +1 -1
  51. package/dist/widget/widget.d.ts +1 -7
  52. package/dist/widget/widget.js +44 -3
  53. package/package.json +5 -2
  54. package/widget/svelte-calendar.css +1 -0
  55. package/widget/widget.js +6 -144
  56. package/dist/calendar/Toolbar.svelte +0 -241
  57. package/dist/calendar/Toolbar.svelte.d.ts +0 -19
  58. package/dist/views/day/DayGrid.svelte +0 -1101
  59. package/dist/views/day/DayTimeline.svelte +0 -532
  60. package/dist/views/day/DayTimeline.svelte.d.ts +0 -4
  61. package/dist/views/day/index.d.ts +0 -2
  62. package/dist/views/day/index.js +0 -2
  63. package/dist/views/schedule/WeekSchedule.svelte +0 -133
  64. package/dist/views/schedule/WeekSchedule.svelte.d.ts +0 -38
  65. package/dist/views/schedule/index.d.ts +0 -1
  66. package/dist/views/schedule/index.js +0 -2
  67. package/dist/views/settings/Settings.svelte +0 -307
  68. package/dist/views/settings/Settings.svelte.d.ts +0 -23
  69. package/dist/views/settings/index.d.ts +0 -2
  70. package/dist/views/settings/index.js +0 -1
  71. package/dist/views/week/WeekGrid.svelte +0 -631
  72. package/dist/views/week/WeekHeatmap.svelte +0 -460
  73. package/dist/views/week/WeekHeatmap.svelte.d.ts +0 -8
  74. package/dist/views/week/index.d.ts +0 -2
  75. package/dist/views/week/index.js +0 -2
@@ -1,18 +1,18 @@
1
1
  <!--
2
2
  Calendar — the unified orchestrator.
3
3
 
4
- Brings together: adapter → event store → view state + selection → toolbar + active view.
4
+ Brings together: adapter → event store → view state + selection → active view.
5
5
  Provides context so any descendant view can read the store/state via getContext().
6
6
 
7
7
  Usage:
8
8
  <Calendar
9
9
  adapter={myAdapter}
10
10
  views={[
11
- { id: 'week-terrain', label: 'Terrain', component: WeekTimelineV4 },
12
- { id: 'week-agenda', label: 'Agenda', component: WeekTimelineV5 },
13
- { id: 'day', label: 'Day', component: DayTimeline },
11
+ { id: 'week-planner', label: 'Planner', component: Planner, props: { mode: 'week' } },
12
+ { id: 'week-agenda', label: 'Agenda', component: Agenda },
13
+ { id: 'day-planner', label: 'Planner', component: Planner, props: { mode: 'day' } },
14
14
  ]}
15
- defaultView="week-terrain"
15
+ defaultView="week-planner"
16
16
  theme={presets.midnight}
17
17
  height={600}
18
18
  oneventclick={handleClick}
@@ -20,7 +20,7 @@
20
20
  />
21
21
  -->
22
22
  <script lang="ts">
23
- import { setContext, untrack, type Component } from 'svelte';
23
+ import { setContext, untrack, type Component, type Snippet } from 'svelte';
24
24
  import type { CalendarAdapter } from '../adapters/types.js';
25
25
  import type { CalendarViewId } from '../engine/view-state.svelte.js';
26
26
  import { createEventStore, type EventStore } from '../engine/event-store.svelte.js';
@@ -28,7 +28,7 @@
28
28
  import { createSelection, type Selection } from '../engine/selection.svelte.js';
29
29
  import { createDragState, type DragState } from '../engine/drag.svelte.js';
30
30
  import type { TimelineEvent } from '../core/types.js';
31
- import Toolbar from './Toolbar.svelte';
31
+ import { getLabels } from '../core/locale.js';
32
32
 
33
33
  /** One view registration */
34
34
  export interface CalendarView {
@@ -55,10 +55,6 @@
55
55
  mondayStart?: boolean;
56
56
  /** Total height */
57
57
  height?: number;
58
- /** Show toolbar */
59
- showToolbar?: boolean;
60
- /** Links to display in toolbar */
61
- links?: { href: string; label: string }[];
62
58
  /** Text direction: 'ltr' (default), 'rtl', or 'auto' */
63
59
  dir?: 'ltr' | 'rtl' | 'auto';
64
60
  /** BCP 47 locale tag (e.g. 'en-US', 'ar-SA') — sets lang and locale for formatting */
@@ -67,47 +63,56 @@
67
63
  readOnly?: boolean;
68
64
  /** Visible hour range: [startHour, endHour). Crops the grid to these hours. */
69
65
  visibleHours?: [number, number];
66
+ /** Initial date to focus on (defaults to today) */
67
+ initialDate?: Date;
68
+ /** Drag snap interval in minutes (default: 15) */
69
+ snapInterval?: number;
70
+
71
+ // ── Snippets ──
72
+ /** Custom event rendering snippet */
73
+ event?: Snippet<[TimelineEvent]>;
74
+ /** Content to show when no events are loaded */
75
+ empty?: Snippet;
70
76
 
71
77
  // ── Callbacks ──
72
78
  oneventclick?: (event: TimelineEvent) => void;
73
79
  oneventcreate?: (range: { start: Date; end: Date }) => void;
74
80
  oneventmove?: (event: TimelineEvent, newStart: Date, newEnd: Date) => void;
81
+ onviewchange?: (viewId: CalendarViewId) => void;
75
82
  }
76
83
 
77
84
  let {
78
85
  adapter,
79
86
  views = [],
80
- defaultView = 'week-grid',
87
+ defaultView = 'week-planner',
81
88
  theme = '',
82
89
  mondayStart = true,
83
90
  height = 600,
84
- showToolbar = true,
85
- links = [],
86
91
  dir,
87
92
  locale,
88
93
  readOnly = false,
89
94
  visibleHours,
95
+ initialDate,
96
+ snapInterval = 15,
97
+ event: eventSnippet,
98
+ empty: emptySnippet,
90
99
  oneventclick,
91
100
  oneventcreate,
92
101
  oneventmove,
102
+ onviewchange,
93
103
  }: Props = $props();
94
104
 
95
105
  // In readOnly mode, suppress mutation callbacks
96
106
  const effectiveCreate = $derived(readOnly ? undefined : oneventcreate);
97
107
  const effectiveMove = $derived(readOnly ? undefined : oneventmove);
98
108
 
99
- import { setDefaultLocale } from '../core/locale.js';
100
-
101
- // ── Set locale when provided ──
102
- $effect(() => {
103
- if (locale) setDefaultLocale(locale);
104
- });
105
-
106
109
  // ── Create reactive state ──
107
110
  const store: EventStore = $derived(createEventStore(adapter));
108
111
  const viewState: ViewState = createViewState(untrack(() => ({
109
112
  defaultView,
110
113
  mondayStart,
114
+ initialDate,
115
+ granularityForView: (viewId) => views.find((v) => v.id === viewId)?.granularity,
111
116
  })));
112
117
  const selection: Selection = createSelection();
113
118
  const drag: DragState = createDragState();
@@ -146,6 +151,9 @@
146
151
  });
147
152
  setContext('calendar:readOnly', { get current() { return readOnly; } });
148
153
  setContext('calendar:visibleHours', { get current() { return visibleHours; } });
154
+ setContext('calendar:snapInterval', { get current() { return snapInterval; } });
155
+ setContext('calendar:eventSnippet', { get current() { return eventSnippet; } });
156
+ setContext('calendar:emptySnippet', { get current() { return emptySnippet; } });
149
157
 
150
158
  // ── Load events when range changes ──
151
159
  $effect(() => {
@@ -153,24 +161,78 @@
153
161
  store.load({ start, end });
154
162
  });
155
163
 
164
+ // Keep active view in sync when external defaultView changes after mount.
165
+ $effect(() => {
166
+ viewState.setView(defaultView);
167
+ });
168
+
169
+ // Keep view state's week-start rule in sync with incoming prop changes.
170
+ $effect(() => {
171
+ if (viewState.mondayStart !== mondayStart) {
172
+ viewState.setMondayStart(mondayStart);
173
+ }
174
+ });
175
+
176
+ // Notify host when active view changes (e.g. via granularity toggles).
177
+ $effect(() => {
178
+ onviewchange?.(viewState.view);
179
+ });
180
+
156
181
  // ── Resolve active view ──
157
182
  const activeView = $derived(views.find((v) => v.id === viewState.view) ?? views[0]);
158
183
 
159
- const toolbarViews = $derived(
160
- views.map((v) => ({ id: v.id, label: v.label, granularity: v.granularity })),
161
- );
184
+ // ── Date label (always visible, centered over views) ──
185
+ const dateLabel = $derived.by(() => {
186
+ if (viewState.granularity === 'day') {
187
+ return viewState.focusDate.toLocaleDateString(locale, {
188
+ weekday: 'long',
189
+ month: 'short',
190
+ day: 'numeric',
191
+ });
192
+ }
193
+ return viewState.focusDate.toLocaleDateString(locale, {
194
+ month: 'long',
195
+ year: 'numeric',
196
+ });
197
+ });
198
+
199
+ // Which granularities are available?
200
+ const granularities = $derived.by(() => {
201
+ const g = new Set(views.map((v) => v.granularity));
202
+ return (['day', 'week'] as const).filter((key) => g.has(key));
203
+ });
204
+
205
+ const L = $derived(getLabels());
162
206
  </script>
163
207
 
164
208
  <div
165
209
  class="cal"
166
210
  style="{theme}; --cal-h: {height}px"
167
211
  role="region"
168
- aria-label="Calendar"
212
+ aria-label={L.calendar}
169
213
  dir={dir}
170
214
  lang={locale}
171
215
  >
172
- {#if showToolbar}
173
- <Toolbar {viewState} views={toolbarViews} {links} />
216
+ <!-- Floating granularity pills (hidden for Agenda views) -->
217
+ {#if granularities.length > 1 && activeView?.label !== 'Agenda'}
218
+ <div class="cal-pills" role="group" aria-label={L.viewMode}>
219
+ {#each granularities as g}
220
+ <button
221
+ class="cal-pill"
222
+ class:cal-pill--active={viewState.granularity === g}
223
+ aria-pressed={viewState.granularity === g}
224
+ onclick={() => {
225
+ const currentLabel = views.find((v) => v.id === viewState.view)?.label;
226
+ const match = views.find((v) => v.granularity === g && v.label === currentLabel);
227
+ const fallback = views.find((v) => v.granularity === g);
228
+ const target = match ?? fallback;
229
+ if (target) viewState.setView(target.id);
230
+ }}
231
+ >
232
+ {g === 'day' ? L.day : L.week}
233
+ </button>
234
+ {/each}
235
+ </div>
174
236
  {/if}
175
237
 
176
238
  <div class="cal-body">
@@ -179,8 +241,9 @@
179
241
  <Comp
180
242
  events={store.events}
181
243
  style={theme}
182
- height={height - (showToolbar ? 48 : 0)}
244
+ height={null}
183
245
  mondayStart={viewState.mondayStart}
246
+ {locale}
184
247
  focusDate={viewState.focusDate}
185
248
  oneventclick={oneventclick}
186
249
  oneventcreate={effectiveCreate}
@@ -205,15 +268,60 @@
205
268
  height: var(--cal-h, 600px);
206
269
  background: var(--dt-bg, #0b0e14);
207
270
  border-radius: 12px;
208
- overflow: hidden;
271
+ overflow: clip;
209
272
  display: flex;
210
273
  flex-direction: column;
211
274
  border: 1px solid var(--dt-border, rgba(148, 163, 184, 0.07));
212
275
  }
213
276
 
277
+ /* ── Floating pills ── */
278
+ .cal-pills {
279
+ position: absolute;
280
+ top: 22px;
281
+ bottom: auto;
282
+ left: 10px;
283
+ z-index: 20;
284
+ display: flex;
285
+ gap: 2px;
286
+ background: color-mix(in srgb, var(--dt-surface, #10141c) 85%, transparent);
287
+ backdrop-filter: blur(6px);
288
+ -webkit-backdrop-filter: blur(6px);
289
+ border-radius: 8px;
290
+ padding: 2px;
291
+ border: 1px solid var(--dt-border, rgba(148, 163, 184, 0.07));
292
+ }
293
+
294
+ .cal-pill {
295
+ border: none;
296
+ background: transparent;
297
+ color: var(--dt-text-2, rgba(148, 163, 184, 0.55));
298
+ cursor: pointer;
299
+ font: 600 11px / 1 var(--dt-sans, 'Outfit', system-ui, sans-serif);
300
+ padding: 6px 12px;
301
+ border-radius: 6px;
302
+ letter-spacing: 0.04em;
303
+ text-transform: uppercase;
304
+ transition: background 100ms, color 100ms;
305
+ }
306
+
307
+ .cal-pill:hover {
308
+ color: var(--dt-text, rgba(226, 232, 240, 0.85));
309
+ }
310
+
311
+ .cal-pill--active {
312
+ background: var(--dt-accent, #ef4444);
313
+ color: var(--dt-btn-text, #fff);
314
+ }
315
+
316
+ .cal-pill:focus-visible {
317
+ outline: 2px solid color-mix(in srgb, var(--dt-accent, #ef4444) 55%, transparent);
318
+ outline-offset: 2px;
319
+ }
320
+
214
321
  .cal-body {
215
322
  flex: 1;
216
323
  min-height: 0;
324
+ position: relative;
217
325
  overflow: hidden;
218
326
  }
219
327
 
@@ -1,4 +1,4 @@
1
- import { type Component } from 'svelte';
1
+ import { type Component, type Snippet } from 'svelte';
2
2
  import type { CalendarAdapter } from '../adapters/types.js';
3
3
  import type { CalendarViewId } from '../engine/view-state.svelte.js';
4
4
  import type { TimelineEvent } from '../core/types.js';
@@ -26,13 +26,6 @@ interface Props {
26
26
  mondayStart?: boolean;
27
27
  /** Total height */
28
28
  height?: number;
29
- /** Show toolbar */
30
- showToolbar?: boolean;
31
- /** Links to display in toolbar */
32
- links?: {
33
- href: string;
34
- label: string;
35
- }[];
36
29
  /** Text direction: 'ltr' (default), 'rtl', or 'auto' */
37
30
  dir?: 'ltr' | 'rtl' | 'auto';
38
31
  /** BCP 47 locale tag (e.g. 'en-US', 'ar-SA') — sets lang and locale for formatting */
@@ -41,12 +34,21 @@ interface Props {
41
34
  readOnly?: boolean;
42
35
  /** Visible hour range: [startHour, endHour). Crops the grid to these hours. */
43
36
  visibleHours?: [number, number];
37
+ /** Initial date to focus on (defaults to today) */
38
+ initialDate?: Date;
39
+ /** Drag snap interval in minutes (default: 15) */
40
+ snapInterval?: number;
41
+ /** Custom event rendering snippet */
42
+ event?: Snippet<[TimelineEvent]>;
43
+ /** Content to show when no events are loaded */
44
+ empty?: Snippet;
44
45
  oneventclick?: (event: TimelineEvent) => void;
45
46
  oneventcreate?: (range: {
46
47
  start: Date;
47
48
  end: Date;
48
49
  }) => void;
49
50
  oneventmove?: (event: TimelineEvent, newStart: Date, newEnd: Date) => void;
51
+ onviewchange?: (viewId: CalendarViewId) => void;
50
52
  }
51
53
  declare const Calendar: Component<Props, {}, "">;
52
54
  type Calendar = ReturnType<typeof Calendar>;
@@ -1,3 +1,2 @@
1
1
  export { default as Calendar } from './Calendar.svelte';
2
- export { default as Toolbar } from './Toolbar.svelte';
3
2
  export type { CalendarView } from './Calendar.svelte';
@@ -1,3 +1,2 @@
1
1
  // ─── Calendar shell barrel export ───────────────────────
2
2
  export { default as Calendar } from './Calendar.svelte';
3
- export { default as Toolbar } from './Toolbar.svelte';
@@ -1,8 +1,9 @@
1
1
  export { createClock } from './clock.svelte.js';
2
2
  export type { Clock } from './clock.svelte.js';
3
- export { DAY_MS, HOUR_MS, HOURS, sod, startOfWeek, addDaysMs, diffDays, pad, fractionalHour, fmtHM, fmtS, dayNum, dayOfWeek, } from './time.js';
4
- export { setDefaultLocale, getDefaultLocale, is24HourLocale, fmtH, weekdayShort, weekdayLong, monthShort, monthLong, dateShort, dateWithWeekday, fmtDay, fmtWeekRange, } from './locale.js';
3
+ export { DAY_MS, HOUR_MS, HOURS, sod, startOfWeek, addDaysMs, diffDays, pad, fractionalHour, fmtHM, fmtS, dayNum, dayOfWeek, isMultiDay, isAllDay, segmentForDay, } from './time.js';
4
+ export type { DaySegment } from './time.js';
5
+ export { setDefaultLocale, getDefaultLocale, is24HourLocale, fmtH, fmtTime, fmtDuration, weekdayShort, weekdayLong, monthShort, monthLong, dateShort, dateWithWeekday, fmtDay, fmtWeekRange, defaultLabels, setLabels, resetLabels, getLabels, } from './locale.js';
6
+ export type { CalendarLabels } from './locale.js';
5
7
  export { toZonedTime, fromZonedTime, nowInZone, formatInTimeZone, } from './timezone.js';
6
- export type { TimelineEvent, WeekTimelineProps, DayTimelineProps, } from './types.js';
7
- export { timeToX } from './types.js';
8
+ export type { TimelineEvent, } from './types.js';
8
9
  export { generatePalette, VIVID_PALETTE } from './palette.js';
@@ -2,11 +2,10 @@
2
2
  // Clock
3
3
  export { createClock } from './clock.svelte.js';
4
4
  // Time constants & math
5
- export { DAY_MS, HOUR_MS, HOURS, sod, startOfWeek, addDaysMs, diffDays, pad, fractionalHour, fmtHM, fmtS, dayNum, dayOfWeek, } from './time.js';
5
+ export { DAY_MS, HOUR_MS, HOURS, sod, startOfWeek, addDaysMs, diffDays, pad, fractionalHour, fmtHM, fmtS, dayNum, dayOfWeek, isMultiDay, isAllDay, segmentForDay, } from './time.js';
6
6
  // Locale-aware formatting
7
- export { setDefaultLocale, getDefaultLocale, is24HourLocale, fmtH, weekdayShort, weekdayLong, monthShort, monthLong, dateShort, dateWithWeekday, fmtDay, fmtWeekRange, } from './locale.js';
7
+ export { setDefaultLocale, getDefaultLocale, is24HourLocale, fmtH, fmtTime, fmtDuration, weekdayShort, weekdayLong, monthShort, monthLong, dateShort, dateWithWeekday, fmtDay, fmtWeekRange, defaultLabels, setLabels, resetLabels, getLabels, } from './locale.js';
8
8
  // Timezone utilities
9
9
  export { toZonedTime, fromZonedTime, nowInZone, formatInTimeZone, } from './timezone.js';
10
- export { timeToX } from './types.js';
11
10
  // Palette
12
11
  export { generatePalette, VIVID_PALETTE } from './palette.js';
@@ -7,6 +7,68 @@
7
7
  * For date-fns locale integration later, this module can import from
8
8
  * `date-fns/locale/*` and pass to `format()`.
9
9
  */
10
+ /**
11
+ * All user-visible text in the calendar UI.
12
+ * Override via `setLabels()` for full localisation.
13
+ */
14
+ export interface CalendarLabels {
15
+ today: string;
16
+ yesterday: string;
17
+ tomorrow: string;
18
+ day: string;
19
+ week: string;
20
+ planner: string;
21
+ agenda: string;
22
+ now: string;
23
+ free: string;
24
+ allDay: string;
25
+ done: string;
26
+ upNext: string;
27
+ until: string;
28
+ noEvents: string;
29
+ nothingScheduled: string;
30
+ nothingScheduledYet: string;
31
+ nothingWasScheduled: string;
32
+ allDoneForToday: string;
33
+ goToToday: string;
34
+ previousDay: string;
35
+ nextDay: string;
36
+ previousWeek: string;
37
+ nextWeek: string;
38
+ calendar: string;
39
+ viewMode: string;
40
+ dayNavigation: string;
41
+ weekNavigation: string;
42
+ dayPlanner: string;
43
+ scrollableDayPlanner: string;
44
+ todaysLineup: string;
45
+ weekAhead: string;
46
+ multiWeekGrid: string;
47
+ currentTime: string;
48
+ createEvent: string;
49
+ happeningNow: string;
50
+ past: string;
51
+ completed: string;
52
+ inProgress: string;
53
+ /** e.g. "+3 more" */
54
+ nMore: (n: number) => string;
55
+ /** e.g. "5 events" */
56
+ nEvents: (n: number) => string;
57
+ /** e.g. "3 completed" */
58
+ nCompleted: (n: number) => string;
59
+ /** e.g. "day 2 of 4" */
60
+ dayNOfTotal: (current: number, total: number) => string;
61
+ /** e.g. "75% complete" */
62
+ percentComplete: (pct: number) => string;
63
+ }
64
+ /** English defaults — used unless overridden via `setLabels()`. */
65
+ export declare const defaultLabels: CalendarLabels;
66
+ /** Replace one or more UI labels. Merges with current labels. */
67
+ export declare function setLabels(overrides: Partial<CalendarLabels>): void;
68
+ /** Reset all labels to English defaults. */
69
+ export declare function resetLabels(): void;
70
+ /** Get the currently active labels. */
71
+ export declare function getLabels(): Readonly<CalendarLabels>;
10
72
  /** Change the default locale for all formatting functions */
11
73
  export declare function setDefaultLocale(tag: string): void;
12
74
  /** Get the current default locale */
@@ -37,3 +99,15 @@ export declare function fmtDay(ms: number, todayMs: number, opts?: {
37
99
  * Format a week range label: "Feb 17 – 23, 2026" or "Jan 27 – Feb 2, 2026"
38
100
  */
39
101
  export declare function fmtWeekRange(weekStartMs: number, locale?: string): string;
102
+ /**
103
+ * Format a Date as a compact time string.
104
+ *
105
+ * 12-hour locales → "9a", "12:30p"
106
+ * 24-hour locales → "9:00", "14:30"
107
+ */
108
+ export declare function fmtTime(d: Date, locale?: string): string;
109
+ /**
110
+ * Format the duration between two Dates as a compact string.
111
+ * e.g. "45m", "1h", "1h 30m"
112
+ */
113
+ export declare function fmtDuration(start: Date, end: Date): string;
@@ -7,6 +7,67 @@
7
7
  * For date-fns locale integration later, this module can import from
8
8
  * `date-fns/locale/*` and pass to `format()`.
9
9
  */
10
+ import { DAY_MS } from './time.js';
11
+ /** English defaults — used unless overridden via `setLabels()`. */
12
+ export const defaultLabels = {
13
+ today: 'Today',
14
+ yesterday: 'Yesterday',
15
+ tomorrow: 'Tomorrow',
16
+ day: 'Day',
17
+ week: 'Week',
18
+ planner: 'Planner',
19
+ agenda: 'Agenda',
20
+ now: 'now',
21
+ free: 'free',
22
+ allDay: 'All day',
23
+ done: 'Done',
24
+ upNext: 'Up next',
25
+ until: 'until',
26
+ noEvents: 'No events',
27
+ nothingScheduled: 'Nothing scheduled',
28
+ nothingScheduledYet: 'Nothing scheduled yet',
29
+ nothingWasScheduled: 'Nothing was scheduled',
30
+ allDoneForToday: 'All done for today',
31
+ goToToday: 'Go to today',
32
+ previousDay: 'Previous day',
33
+ nextDay: 'Next day',
34
+ previousWeek: 'Previous week',
35
+ nextWeek: 'Next week',
36
+ calendar: 'Calendar',
37
+ viewMode: 'View mode',
38
+ dayNavigation: 'Day navigation',
39
+ weekNavigation: 'Week navigation',
40
+ dayPlanner: 'Day planner',
41
+ scrollableDayPlanner: 'Scrollable day planner',
42
+ todaysLineup: "Today's lineup",
43
+ weekAhead: 'Week ahead',
44
+ multiWeekGrid: 'Multi-week calendar grid',
45
+ currentTime: 'Current time',
46
+ createEvent: 'Create event',
47
+ happeningNow: 'happening now',
48
+ past: 'past',
49
+ completed: 'completed',
50
+ inProgress: 'in progress',
51
+ nMore: (n) => `+${n} more`,
52
+ nEvents: (n) => `${n} event${n === 1 ? '' : 's'}`,
53
+ nCompleted: (n) => `${n} completed`,
54
+ dayNOfTotal: (current, total) => `day ${current} of ${total}`,
55
+ percentComplete: (pct) => `${pct}% complete`,
56
+ };
57
+ let _labels = { ...defaultLabels };
58
+ /** Replace one or more UI labels. Merges with current labels. */
59
+ export function setLabels(overrides) {
60
+ _labels = { ..._labels, ...overrides };
61
+ }
62
+ /** Reset all labels to English defaults. */
63
+ export function resetLabels() {
64
+ _labels = { ...defaultLabels };
65
+ }
66
+ /** Get the currently active labels. */
67
+ export function getLabels() {
68
+ return _labels;
69
+ }
70
+ // ─── Default locale ─────────────────────────────────────
10
71
  /** Module-level default locale — consumers can override via setDefaultLocale() */
11
72
  let defaultLocale = 'en-US';
12
73
  /** Change the default locale for all formatting functions */
@@ -73,7 +134,6 @@ export function dateWithWeekday(ms, locale) {
73
134
  day: 'numeric',
74
135
  });
75
136
  }
76
- import { DAY_MS } from './time.js';
77
137
  /**
78
138
  * Format a timestamp as a smart day label:
79
139
  * "Today · Feb 21", "Yesterday · Feb 20", "Mon, Feb 17", etc.
@@ -81,12 +141,13 @@ import { DAY_MS } from './time.js';
81
141
  export function fmtDay(ms, todayMs, opts, locale) {
82
142
  const loc = locale ?? defaultLocale;
83
143
  const short = new Date(ms).toLocaleDateString(loc, { month: 'short', day: 'numeric' });
144
+ const L = _labels;
84
145
  if (ms === todayMs)
85
- return opts?.short ? 'Today' : `Today · ${short}`;
146
+ return opts?.short ? L.today : `${L.today} · ${short}`;
86
147
  if (ms === todayMs - DAY_MS)
87
- return opts?.short ? 'Yesterday' : `Yesterday · ${short}`;
148
+ return opts?.short ? L.yesterday : `${L.yesterday} · ${short}`;
88
149
  if (ms === todayMs + DAY_MS)
89
- return opts?.short ? 'Tomorrow' : `Tomorrow · ${short}`;
150
+ return opts?.short ? L.tomorrow : `${L.tomorrow} · ${short}`;
90
151
  if (opts?.short) {
91
152
  return new Date(ms).toLocaleDateString(loc, { weekday: 'short', day: 'numeric' });
92
153
  }
@@ -111,3 +172,34 @@ export function fmtWeekRange(weekStartMs, locale) {
111
172
  }
112
173
  return `${sm} ${s.getDate()} – ${e.getDate()}, ${ey}`;
113
174
  }
175
+ // ─── Shared time / duration formatting ──────────────────
176
+ /**
177
+ * Format a Date as a compact time string.
178
+ *
179
+ * 12-hour locales → "9a", "12:30p"
180
+ * 24-hour locales → "9:00", "14:30"
181
+ */
182
+ export function fmtTime(d, locale) {
183
+ if (is24HourLocale(locale)) {
184
+ const h = d.getHours();
185
+ const m = d.getMinutes();
186
+ return m === 0 ? String(h) : `${h}:${String(m).padStart(2, '0')}`;
187
+ }
188
+ const h = d.getHours();
189
+ const m = d.getMinutes();
190
+ const suffix = h >= 12 ? 'p' : 'a';
191
+ const h12 = h === 0 ? 12 : h > 12 ? h - 12 : h;
192
+ return m === 0 ? `${h12}${suffix}` : `${h12}:${String(m).padStart(2, '0')}${suffix}`;
193
+ }
194
+ /**
195
+ * Format the duration between two Dates as a compact string.
196
+ * e.g. "45m", "1h", "1h 30m"
197
+ */
198
+ export function fmtDuration(start, end) {
199
+ const mins = Math.round((end.getTime() - start.getTime()) / 60_000);
200
+ if (mins < 60)
201
+ return `${mins}m`;
202
+ const h = Math.floor(mins / 60);
203
+ const m = mins % 60;
204
+ return m > 0 ? `${h}h ${m}m` : `${h}h`;
205
+ }
@@ -35,3 +35,33 @@ export declare function fmtS(ms: number): string;
35
35
  export declare function dayNum(ms: number): number;
36
36
  /** Day-of-week index (0 = Sun … 6 = Sat) */
37
37
  export declare function dayOfWeek(ms: number): number;
38
+ import type { TimelineEvent } from './types.js';
39
+ /** Does an event span more than one calendar day? */
40
+ export declare function isMultiDay(ev: TimelineEvent): boolean;
41
+ /** Is an event effectively all-day? (allDay flag, or spans ≥24h with midnight boundaries) */
42
+ export declare function isAllDay(ev: TimelineEvent): boolean;
43
+ /**
44
+ * Describes how an event appears on a specific day.
45
+ */
46
+ export interface DaySegment {
47
+ ev: TimelineEvent;
48
+ /** Effective start for this day (clamped to day start) */
49
+ start: Date;
50
+ /** Effective end for this day (clamped to day end) */
51
+ end: Date;
52
+ /** Is this the first day of the event? */
53
+ isStart: boolean;
54
+ /** Is this the last day of the event? */
55
+ isEnd: boolean;
56
+ /** 1-based day index within the span */
57
+ dayIndex: number;
58
+ /** Total number of days the event spans */
59
+ totalDays: number;
60
+ /** Is this an all-day event? */
61
+ allDay: boolean;
62
+ }
63
+ /**
64
+ * Compute how an event appears on a specific day.
65
+ * Returns null if the event doesn't overlap the day.
66
+ */
67
+ export declare function segmentForDay(ev: TimelineEvent, dayMs: number): DaySegment | null;
package/dist/core/time.js CHANGED
@@ -66,3 +66,45 @@ export function dayNum(ms) {
66
66
  export function dayOfWeek(ms) {
67
67
  return getDay(new Date(ms));
68
68
  }
69
+ /** Does an event span more than one calendar day? */
70
+ export function isMultiDay(ev) {
71
+ return sod(ev.start.getTime()) !== sod(ev.end.getTime() - 1);
72
+ }
73
+ /** Is an event effectively all-day? (allDay flag, or spans ≥24h with midnight boundaries) */
74
+ export function isAllDay(ev) {
75
+ if (ev.allDay)
76
+ return true;
77
+ const duration = ev.end.getTime() - ev.start.getTime();
78
+ if (duration < DAY_MS)
79
+ return false;
80
+ const s = ev.start;
81
+ return s.getHours() === 0 && s.getMinutes() === 0 && s.getSeconds() === 0;
82
+ }
83
+ /**
84
+ * Compute how an event appears on a specific day.
85
+ * Returns null if the event doesn't overlap the day.
86
+ */
87
+ export function segmentForDay(ev, dayMs) {
88
+ const dayStart = sod(dayMs);
89
+ const dayEnd = dayStart + DAY_MS;
90
+ const evStart = ev.start.getTime();
91
+ const evEnd = ev.end.getTime();
92
+ // No overlap
93
+ if (evStart >= dayEnd || evEnd <= dayStart)
94
+ return null;
95
+ const firstDayMs = sod(evStart);
96
+ // For end time: if event ends exactly at midnight, last day is the day before
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;
100
+ return {
101
+ ev,
102
+ start: new Date(Math.max(evStart, dayStart)),
103
+ end: new Date(Math.min(evEnd, dayEnd)),
104
+ isStart: dayStart === firstDayMs,
105
+ isEnd: dayStart === lastDayMs,
106
+ dayIndex,
107
+ totalDays,
108
+ allDay: isAllDay(ev),
109
+ };
110
+ }