@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.
- package/README.md +193 -406
- package/dist/adapters/recurring.d.ts +45 -8
- package/dist/adapters/recurring.js +227 -49
- package/dist/adapters/rest.js +6 -1
- package/dist/calendar/Calendar.svelte +137 -29
- package/dist/calendar/Calendar.svelte.d.ts +10 -8
- package/dist/calendar/index.d.ts +0 -1
- package/dist/calendar/index.js +0 -1
- package/dist/core/index.d.ts +5 -4
- package/dist/core/index.js +2 -3
- package/dist/core/locale.d.ts +74 -0
- package/dist/core/locale.js +96 -4
- package/dist/core/time.d.ts +30 -0
- package/dist/core/time.js +42 -0
- package/dist/core/types.d.ts +1 -72
- package/dist/core/types.js +1 -8
- package/dist/engine/event-store.svelte.js +2 -2
- package/dist/engine/index.d.ts +1 -1
- package/dist/engine/view-state.svelte.d.ts +12 -6
- package/dist/engine/view-state.svelte.js +8 -4
- package/dist/index.d.ts +6 -7
- package/dist/index.js +4 -4
- package/dist/primitives/EmptySlot.svelte +6 -16
- package/dist/primitives/EventBlock.svelte +6 -19
- package/dist/primitives/NowIndicator.svelte +7 -4
- package/dist/primitives/TimeGutter.svelte +1 -1
- package/dist/theme/index.d.ts +1 -1
- package/dist/theme/index.js +1 -1
- package/dist/theme/presets.d.ts +11 -19
- package/dist/theme/presets.js +25 -111
- package/dist/views/agenda/Agenda.svelte +17 -1514
- package/dist/views/agenda/Agenda.svelte.d.ts +0 -13
- package/dist/views/agenda/AgendaDay.svelte +976 -0
- package/dist/views/agenda/AgendaDay.svelte.d.ts +14 -0
- package/dist/views/agenda/AgendaWeek.svelte +775 -0
- package/dist/views/agenda/AgendaWeek.svelte.d.ts +15 -0
- package/dist/views/agenda/index.d.ts +2 -0
- package/dist/views/agenda/index.js +2 -0
- package/dist/views/index.d.ts +2 -6
- package/dist/views/index.js +2 -6
- package/dist/views/planner/Planner.svelte +17 -0
- package/dist/views/planner/Planner.svelte.d.ts +7 -0
- package/dist/views/planner/PlannerDay.svelte +957 -0
- package/dist/views/{day/DayGrid.svelte.d.ts → planner/PlannerDay.svelte.d.ts} +8 -16
- package/dist/views/planner/PlannerWeek.svelte +826 -0
- package/dist/views/{week/WeekGrid.svelte.d.ts → planner/PlannerWeek.svelte.d.ts} +5 -9
- package/dist/views/planner/index.d.ts +1 -0
- package/dist/views/planner/index.js +1 -0
- package/dist/widget/CalendarWidget.svelte +54 -35
- package/dist/widget/CalendarWidget.svelte.d.ts +1 -1
- package/dist/widget/widget.d.ts +1 -7
- package/dist/widget/widget.js +44 -3
- package/package.json +5 -2
- package/widget/svelte-calendar.css +1 -0
- package/widget/widget.js +6 -144
- package/dist/calendar/Toolbar.svelte +0 -241
- package/dist/calendar/Toolbar.svelte.d.ts +0 -19
- package/dist/views/day/DayGrid.svelte +0 -1101
- package/dist/views/day/DayTimeline.svelte +0 -532
- package/dist/views/day/DayTimeline.svelte.d.ts +0 -4
- package/dist/views/day/index.d.ts +0 -2
- package/dist/views/day/index.js +0 -2
- package/dist/views/schedule/WeekSchedule.svelte +0 -133
- package/dist/views/schedule/WeekSchedule.svelte.d.ts +0 -38
- package/dist/views/schedule/index.d.ts +0 -1
- package/dist/views/schedule/index.js +0 -2
- package/dist/views/settings/Settings.svelte +0 -307
- package/dist/views/settings/Settings.svelte.d.ts +0 -23
- package/dist/views/settings/index.d.ts +0 -2
- package/dist/views/settings/index.js +0 -1
- package/dist/views/week/WeekGrid.svelte +0 -631
- package/dist/views/week/WeekHeatmap.svelte +0 -460
- package/dist/views/week/WeekHeatmap.svelte.d.ts +0 -8
- package/dist/views/week/index.d.ts +0 -2
- 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 →
|
|
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-
|
|
12
|
-
{ id: 'week-agenda',
|
|
13
|
-
{ id: 'day',
|
|
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-
|
|
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
|
|
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-
|
|
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
|
-
|
|
160
|
-
|
|
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=
|
|
212
|
+
aria-label={L.calendar}
|
|
169
213
|
dir={dir}
|
|
170
214
|
lang={locale}
|
|
171
215
|
>
|
|
172
|
-
|
|
173
|
-
|
|
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={
|
|
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:
|
|
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>;
|
package/dist/calendar/index.d.ts
CHANGED
package/dist/calendar/index.js
CHANGED
package/dist/core/index.d.ts
CHANGED
|
@@ -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 {
|
|
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,
|
|
7
|
-
export { timeToX } from './types.js';
|
|
8
|
+
export type { TimelineEvent, } from './types.js';
|
|
8
9
|
export { generatePalette, VIVID_PALETTE } from './palette.js';
|
package/dist/core/index.js
CHANGED
|
@@ -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';
|
package/dist/core/locale.d.ts
CHANGED
|
@@ -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;
|
package/dist/core/locale.js
CHANGED
|
@@ -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 ?
|
|
146
|
+
return opts?.short ? L.today : `${L.today} · ${short}`;
|
|
86
147
|
if (ms === todayMs - DAY_MS)
|
|
87
|
-
return opts?.short ?
|
|
148
|
+
return opts?.short ? L.yesterday : `${L.yesterday} · ${short}`;
|
|
88
149
|
if (ms === todayMs + DAY_MS)
|
|
89
|
-
return opts?.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
|
+
}
|
package/dist/core/time.d.ts
CHANGED
|
@@ -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
|
+
}
|