@nomideusz/svelte-calendar 0.6.0 → 0.6.4
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 +183 -5
- package/dist/adapters/composite.d.ts +15 -0
- package/dist/adapters/composite.js +65 -0
- package/dist/adapters/index.d.ts +5 -1
- package/dist/adapters/index.js +2 -0
- package/dist/adapters/mapped.d.ts +170 -0
- package/dist/adapters/mapped.js +343 -0
- package/dist/adapters/recurring.d.ts +11 -0
- package/dist/adapters/recurring.js +9 -13
- package/dist/adapters/types.d.ts +11 -2
- package/dist/calendar/Calendar.svelte +847 -767
- package/dist/calendar/Calendar.svelte.d.ts +14 -2
- package/dist/core/index.d.ts +3 -1
- package/dist/core/index.js +2 -0
- package/dist/core/measure.d.ts +104 -0
- package/dist/core/measure.js +195 -0
- package/dist/core/time.js +2 -2
- package/dist/core/types.d.ts +32 -0
- package/dist/engine/event-store.svelte.js +22 -7
- package/dist/headless/create-agenda.svelte.d.ts +67 -0
- package/dist/headless/create-agenda.svelte.js +139 -0
- package/dist/headless/create-calendar.svelte.d.ts +2 -0
- package/dist/headless/create-calendar.svelte.js +380 -0
- package/dist/headless/index.d.ts +4 -0
- package/dist/headless/index.js +2 -0
- package/dist/headless/types.d.ts +219 -0
- package/dist/headless/types.js +1 -0
- package/dist/index.d.ts +6 -4
- package/dist/index.js +4 -2
- package/dist/primitives/DayHeader.svelte +103 -103
- package/dist/primitives/EmptySlot.svelte +105 -105
- package/dist/primitives/EventBlock.svelte +397 -312
- package/dist/primitives/NowIndicator.svelte +178 -178
- package/dist/primitives/TimeGutter.svelte +104 -104
- package/dist/theme/presets.js +36 -36
- package/dist/views/agenda/AgendaDay.svelte +1167 -1055
- package/dist/views/agenda/AgendaWeek.svelte +987 -924
- package/dist/views/mobile/Mobile.svelte +17 -17
- package/dist/views/mobile/MobileDay.svelte +729 -665
- package/dist/views/mobile/MobileWeek.svelte +476 -456
- package/dist/views/planner/PlannerDay.svelte +1184 -1091
- package/dist/views/planner/PlannerWeek.svelte +924 -945
- package/dist/views/shared/context.svelte.d.ts +47 -0
- package/dist/views/shared/context.svelte.js +47 -0
- package/dist/views/shared/format.d.ts +19 -0
- package/dist/views/shared/format.js +46 -0
- package/dist/views/shared/index.d.ts +4 -0
- package/dist/views/shared/index.js +2 -0
- package/dist/widget/CalendarWidget.svelte +142 -142
- package/package.json +76 -71
|
@@ -1,767 +1,847 @@
|
|
|
1
|
-
<!--
|
|
2
|
-
Calendar — the unified orchestrator.
|
|
3
|
-
|
|
4
|
-
Brings together: adapter → event store → view state + selection → active view.
|
|
5
|
-
Provides context so any descendant view can read the store/state via getContext().
|
|
6
|
-
|
|
7
|
-
Usage (minimal):
|
|
8
|
-
<Calendar adapter={myAdapter} theme={neutral} />
|
|
9
|
-
|
|
10
|
-
Usage (full control):
|
|
11
|
-
<Calendar
|
|
12
|
-
adapter={myAdapter}
|
|
13
|
-
view="week-planner"
|
|
14
|
-
theme={midnight}
|
|
15
|
-
height={600}
|
|
16
|
-
oneventclick={handleClick}
|
|
17
|
-
oneventcreate={handleCreate}
|
|
18
|
-
/>
|
|
19
|
-
-->
|
|
20
|
-
<script lang="ts">
|
|
21
|
-
import { setContext, untrack, type Component, type Snippet } from 'svelte';
|
|
22
|
-
import type { CalendarAdapter } from '../adapters/types.js';
|
|
23
|
-
import type { CalendarViewId } from '../engine/view-state.svelte.js';
|
|
24
|
-
import { createEventStore, type EventStore } from '../engine/event-store.svelte.js';
|
|
25
|
-
import { createViewState, type ViewState } from '../engine/view-state.svelte.js';
|
|
26
|
-
import { createSelection, type Selection } from '../engine/selection.svelte.js';
|
|
27
|
-
import { createDragState, type DragState } from '../engine/drag.svelte.js';
|
|
28
|
-
import { onMount } from 'svelte';
|
|
29
|
-
import type { TimelineEvent, BlockedSlot } from '../core/types.js';
|
|
30
|
-
import { getLabels } from '../core/locale.js';
|
|
31
|
-
import { auto } from '../theme/presets.js';
|
|
32
|
-
import { probeHostTheme, observeHostTheme } from '../theme/auto.js';
|
|
33
|
-
import type { AutoThemeOptions } from '../theme/auto.js';
|
|
34
|
-
import Planner from '../views/planner/Planner.svelte';
|
|
35
|
-
import Agenda from '../views/agenda/Agenda.svelte';
|
|
36
|
-
import Mobile from '../views/mobile/Mobile.svelte';
|
|
37
|
-
|
|
38
|
-
/** Breakpoint (px) at which auto-mobile activates */
|
|
39
|
-
const MOBILE_BREAKPOINT = 768;
|
|
40
|
-
|
|
41
|
-
/** One view registration */
|
|
42
|
-
export interface CalendarView {
|
|
43
|
-
id: CalendarViewId;
|
|
44
|
-
label: string;
|
|
45
|
-
/** day or week */
|
|
46
|
-
mode: 'day' | 'week';
|
|
47
|
-
/** The Svelte component to render */
|
|
48
|
-
component: Component<Record<string, unknown>>;
|
|
49
|
-
/** Extra props to pass through (e.g. hourHeight, specialized settings) */
|
|
50
|
-
props?: Record<string, unknown>;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
interface Props {
|
|
54
|
-
/** Data adapter (required) */
|
|
55
|
-
adapter: CalendarAdapter;
|
|
56
|
-
/** Registered views */
|
|
57
|
-
views?: CalendarView[];
|
|
58
|
-
/** Active view ID (defaults to first registered view) */
|
|
59
|
-
view?: CalendarViewId;
|
|
60
|
-
/** CSS theme string (--dt-* inline style) */
|
|
61
|
-
theme?: string;
|
|
62
|
-
/**
|
|
63
|
-
* Options for the smart auto-theme.
|
|
64
|
-
* When theme is `auto` (empty string), the calendar probes the host page
|
|
65
|
-
* and generates matching --dt-* vars automatically.
|
|
66
|
-
*
|
|
67
|
-
* Pass `{ mode, accent, font }` to override individual aspects.
|
|
68
|
-
* Set `autoTheme: false` to disable probing entirely.
|
|
69
|
-
*/
|
|
70
|
-
autoTheme?: AutoThemeOptions | false;
|
|
71
|
-
/** Start week on Monday */
|
|
72
|
-
mondayStart?: boolean;
|
|
73
|
-
/** Total height */
|
|
74
|
-
height?: number;
|
|
75
|
-
/** Border radius in px (default: 12). Set to 0 for no rounding. */
|
|
76
|
-
borderRadius?: number;
|
|
77
|
-
/** Text direction: 'ltr' (default), 'rtl', or 'auto' */
|
|
78
|
-
dir?: 'ltr' | 'rtl' | 'auto';
|
|
79
|
-
/** BCP 47 locale tag (e.g. 'en-US', 'ar-SA') — sets lang and locale for formatting */
|
|
80
|
-
locale?: string;
|
|
81
|
-
/** Read-only mode: disables drag, resize, empty-slot creation */
|
|
82
|
-
readOnly?: boolean;
|
|
83
|
-
/** Visible hour range: [startHour, endHour). Crops the grid to these hours. */
|
|
84
|
-
visibleHours?: [number, number];
|
|
85
|
-
/** Initial date to focus on (defaults to today) */
|
|
86
|
-
initialDate?: Date;
|
|
87
|
-
/** Drag snap interval in minutes (default: 15) */
|
|
88
|
-
snapInterval?: number;
|
|
89
|
-
/** Show the Day/Week mode pills (default: true) */
|
|
90
|
-
showModePills?: boolean;
|
|
91
|
-
/** Show prev/next/today navigation controls (default: true) */
|
|
92
|
-
showNavigation?: boolean;
|
|
93
|
-
/** Treat all days equally — no past-day dimming or collapsing (default: false) */
|
|
94
|
-
equalDays?: boolean;
|
|
95
|
-
/** Hide date numbers — headers show only day names (Mon, Tue, …). Useful for template/recurring schedules. */
|
|
96
|
-
showDates?: boolean;
|
|
97
|
-
/** ISO weekdays to hide (1=Mon … 7=Sun). E.g. [6, 7] hides weekends. */
|
|
98
|
-
hideDays?: number[];
|
|
99
|
-
/** Controlled current date — drives which date the calendar focuses on. */
|
|
100
|
-
currentDate?: Date;
|
|
101
|
-
/** Blocked/unavailable time slots — rendered as hatched regions, prevent event creation. */
|
|
102
|
-
blockedSlots?: BlockedSlot[];
|
|
103
|
-
/** Number of days shown in week mode (default: 7). E.g. 3 for a 3-day view, 5 for workweek. */
|
|
104
|
-
days?: number;
|
|
105
|
-
/** Minimum event duration in minutes (enforced during drag-create and click-to-create). */
|
|
106
|
-
minDuration?: number;
|
|
107
|
-
/** Maximum event duration in minutes (enforced during drag-create). */
|
|
108
|
-
maxDuration?: number;
|
|
109
|
-
/** Specific dates to disable (greyed-out, no event creation). */
|
|
110
|
-
disabledDates?: Date[];
|
|
111
|
-
/**
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
*
|
|
115
|
-
* - `
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
//
|
|
334
|
-
//
|
|
335
|
-
|
|
336
|
-
setContext('calendar
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
});
|
|
379
|
-
|
|
380
|
-
//
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
//
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
if (
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
});
|
|
397
|
-
|
|
398
|
-
//
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
//
|
|
405
|
-
|
|
406
|
-
if (
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
</div>
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
min-
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
.cal-m-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
1
|
+
<!--
|
|
2
|
+
Calendar — the unified orchestrator.
|
|
3
|
+
|
|
4
|
+
Brings together: adapter → event store → view state + selection → active view.
|
|
5
|
+
Provides context so any descendant view can read the store/state via getContext().
|
|
6
|
+
|
|
7
|
+
Usage (minimal):
|
|
8
|
+
<Calendar adapter={myAdapter} theme={neutral} />
|
|
9
|
+
|
|
10
|
+
Usage (full control):
|
|
11
|
+
<Calendar
|
|
12
|
+
adapter={myAdapter}
|
|
13
|
+
view="week-planner"
|
|
14
|
+
theme={midnight}
|
|
15
|
+
height={600}
|
|
16
|
+
oneventclick={handleClick}
|
|
17
|
+
oneventcreate={handleCreate}
|
|
18
|
+
/>
|
|
19
|
+
-->
|
|
20
|
+
<script lang="ts">
|
|
21
|
+
import { setContext, untrack, type Component, type Snippet } from 'svelte';
|
|
22
|
+
import type { CalendarAdapter } from '../adapters/types.js';
|
|
23
|
+
import type { CalendarViewId } from '../engine/view-state.svelte.js';
|
|
24
|
+
import { createEventStore, type EventStore } from '../engine/event-store.svelte.js';
|
|
25
|
+
import { createViewState, type ViewState } from '../engine/view-state.svelte.js';
|
|
26
|
+
import { createSelection, type Selection } from '../engine/selection.svelte.js';
|
|
27
|
+
import { createDragState, type DragState } from '../engine/drag.svelte.js';
|
|
28
|
+
import { onMount } from 'svelte';
|
|
29
|
+
import type { TimelineEvent, BlockedSlot } from '../core/types.js';
|
|
30
|
+
import { getLabels } from '../core/locale.js';
|
|
31
|
+
import { auto } from '../theme/presets.js';
|
|
32
|
+
import { probeHostTheme, observeHostTheme } from '../theme/auto.js';
|
|
33
|
+
import type { AutoThemeOptions } from '../theme/auto.js';
|
|
34
|
+
import Planner from '../views/planner/Planner.svelte';
|
|
35
|
+
import Agenda from '../views/agenda/Agenda.svelte';
|
|
36
|
+
import Mobile from '../views/mobile/Mobile.svelte';
|
|
37
|
+
|
|
38
|
+
/** Breakpoint (px) at which auto-mobile activates */
|
|
39
|
+
const MOBILE_BREAKPOINT = 768;
|
|
40
|
+
|
|
41
|
+
/** One view registration */
|
|
42
|
+
export interface CalendarView {
|
|
43
|
+
id: CalendarViewId;
|
|
44
|
+
label: string;
|
|
45
|
+
/** day or week */
|
|
46
|
+
mode: 'day' | 'week';
|
|
47
|
+
/** The Svelte component to render */
|
|
48
|
+
component: Component<Record<string, unknown>>;
|
|
49
|
+
/** Extra props to pass through (e.g. hourHeight, specialized settings) */
|
|
50
|
+
props?: Record<string, unknown>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface Props {
|
|
54
|
+
/** Data adapter (required) */
|
|
55
|
+
adapter: CalendarAdapter;
|
|
56
|
+
/** Registered views */
|
|
57
|
+
views?: CalendarView[];
|
|
58
|
+
/** Active view ID (defaults to first registered view) */
|
|
59
|
+
view?: CalendarViewId;
|
|
60
|
+
/** CSS theme string (--dt-* inline style) */
|
|
61
|
+
theme?: string;
|
|
62
|
+
/**
|
|
63
|
+
* Options for the smart auto-theme.
|
|
64
|
+
* When theme is `auto` (empty string), the calendar probes the host page
|
|
65
|
+
* and generates matching --dt-* vars automatically.
|
|
66
|
+
*
|
|
67
|
+
* Pass `{ mode, accent, font }` to override individual aspects.
|
|
68
|
+
* Set `autoTheme: false` to disable probing entirely.
|
|
69
|
+
*/
|
|
70
|
+
autoTheme?: AutoThemeOptions | false;
|
|
71
|
+
/** Start week on Monday */
|
|
72
|
+
mondayStart?: boolean;
|
|
73
|
+
/** Total height. Use `'auto'` to let content determine height (ideal for Agenda views). */
|
|
74
|
+
height?: number | 'auto';
|
|
75
|
+
/** Border radius in px (default: 12). Set to 0 for no rounding. */
|
|
76
|
+
borderRadius?: number;
|
|
77
|
+
/** Text direction: 'ltr' (default), 'rtl', or 'auto' */
|
|
78
|
+
dir?: 'ltr' | 'rtl' | 'auto';
|
|
79
|
+
/** BCP 47 locale tag (e.g. 'en-US', 'ar-SA') — sets lang and locale for formatting */
|
|
80
|
+
locale?: string;
|
|
81
|
+
/** Read-only mode: disables drag, resize, empty-slot creation */
|
|
82
|
+
readOnly?: boolean;
|
|
83
|
+
/** Visible hour range: [startHour, endHour). Crops the grid to these hours. */
|
|
84
|
+
visibleHours?: [number, number];
|
|
85
|
+
/** Initial date to focus on (defaults to today) */
|
|
86
|
+
initialDate?: Date;
|
|
87
|
+
/** Drag snap interval in minutes (default: 15) */
|
|
88
|
+
snapInterval?: number;
|
|
89
|
+
/** Show the Day/Week mode pills (default: true) */
|
|
90
|
+
showModePills?: boolean;
|
|
91
|
+
/** Show prev/next/today navigation controls (default: true) */
|
|
92
|
+
showNavigation?: boolean;
|
|
93
|
+
/** Treat all days equally — no past-day dimming or collapsing (default: false) */
|
|
94
|
+
equalDays?: boolean;
|
|
95
|
+
/** Hide date numbers — headers show only day names (Mon, Tue, …). Useful for template/recurring schedules. */
|
|
96
|
+
showDates?: boolean;
|
|
97
|
+
/** ISO weekdays to hide (1=Mon … 7=Sun). E.g. [6, 7] hides weekends. */
|
|
98
|
+
hideDays?: number[];
|
|
99
|
+
/** Controlled current date — drives which date the calendar focuses on. */
|
|
100
|
+
currentDate?: Date;
|
|
101
|
+
/** Blocked/unavailable time slots — rendered as hatched regions, prevent event creation. */
|
|
102
|
+
blockedSlots?: BlockedSlot[];
|
|
103
|
+
/** Number of days shown in week mode (default: 7). E.g. 3 for a 3-day view, 5 for workweek. */
|
|
104
|
+
days?: number;
|
|
105
|
+
/** Minimum event duration in minutes (enforced during drag-create and click-to-create). */
|
|
106
|
+
minDuration?: number;
|
|
107
|
+
/** Maximum event duration in minutes (enforced during drag-create). */
|
|
108
|
+
maxDuration?: number;
|
|
109
|
+
/** Specific dates to disable (greyed-out, no event creation). */
|
|
110
|
+
disabledDates?: Date[];
|
|
111
|
+
/** Compact mode: use minimal text-row rendering in Agenda views (dot + time + title). */
|
|
112
|
+
compact?: boolean;
|
|
113
|
+
/**
|
|
114
|
+
* Mobile mode.
|
|
115
|
+
* - `'auto'` (default): detect via viewport width (< 768 px)
|
|
116
|
+
* - `true`: always use mobile views
|
|
117
|
+
* - `false`: never use mobile views
|
|
118
|
+
*/
|
|
119
|
+
mobile?: 'auto' | boolean;
|
|
120
|
+
|
|
121
|
+
// ── Snippets ──
|
|
122
|
+
/** Custom event rendering snippet */
|
|
123
|
+
event?: Snippet<[TimelineEvent]>;
|
|
124
|
+
/** Content to show when no events are loaded */
|
|
125
|
+
empty?: Snippet;
|
|
126
|
+
/** Custom day header snippet. Receives { date, isToday, dayName }. */
|
|
127
|
+
dayHeader?: Snippet<[{ date: Date; isToday: boolean; dayName: string }]>;
|
|
128
|
+
/**
|
|
129
|
+
* Replace the entire header chrome (date label + mode pills + nav arrows).
|
|
130
|
+
* Receives context: { dateLabel, mode, modes, switchMode, prev, next, goToday, isViewOnToday, focusDate }.
|
|
131
|
+
*/
|
|
132
|
+
header?: Snippet<[import('../headless/types.js').HeaderContext]>;
|
|
133
|
+
/**
|
|
134
|
+
* Replace just the navigation controls (arrows + today button).
|
|
135
|
+
* Receives context: { prev, next, goToday, isViewOnToday, focusDate, mode }.
|
|
136
|
+
*/
|
|
137
|
+
navigation?: Snippet<[import('../headless/types.js').NavigationContext]>;
|
|
138
|
+
|
|
139
|
+
// ── Callbacks ──
|
|
140
|
+
oneventclick?: (event: TimelineEvent) => void;
|
|
141
|
+
oneventcreate?: (range: { start: Date; end: Date }) => void;
|
|
142
|
+
oneventmove?: (event: TimelineEvent, newStart: Date, newEnd: Date) => void;
|
|
143
|
+
onviewchange?: (viewId: CalendarViewId) => void;
|
|
144
|
+
/** Called when the focused date changes (navigation, drag-scroll, etc.) */
|
|
145
|
+
ondatechange?: (date: Date) => void;
|
|
146
|
+
/** Called when the pointer enters an event (hover). */
|
|
147
|
+
oneventhover?: (event: TimelineEvent) => void;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ── Built-in views (used when no custom views are provided) ──
|
|
151
|
+
const DEFAULT_VIEWS: CalendarView[] = [
|
|
152
|
+
{ id: 'day-planner', label: 'Planner', mode: 'day', component: Planner },
|
|
153
|
+
{ id: 'week-planner', label: 'Planner', mode: 'week', component: Planner },
|
|
154
|
+
{ id: 'day-agenda', label: 'Agenda', mode: 'day', component: Agenda },
|
|
155
|
+
{ id: 'week-agenda', label: 'Agenda', mode: 'week', component: Agenda },
|
|
156
|
+
{ id: 'day-mobile', label: 'Mobile', mode: 'day', component: Mobile },
|
|
157
|
+
{ id: 'week-mobile', label: 'Mobile', mode: 'week', component: Mobile },
|
|
158
|
+
];
|
|
159
|
+
|
|
160
|
+
let {
|
|
161
|
+
adapter,
|
|
162
|
+
views = DEFAULT_VIEWS,
|
|
163
|
+
view: activeViewId,
|
|
164
|
+
theme = auto,
|
|
165
|
+
autoTheme,
|
|
166
|
+
mondayStart = true,
|
|
167
|
+
height: heightProp = 600,
|
|
168
|
+
borderRadius = 12,
|
|
169
|
+
dir,
|
|
170
|
+
locale,
|
|
171
|
+
readOnly = false,
|
|
172
|
+
visibleHours,
|
|
173
|
+
initialDate,
|
|
174
|
+
snapInterval = 15,
|
|
175
|
+
showModePills = true,
|
|
176
|
+
showNavigation = true,
|
|
177
|
+
equalDays = false,
|
|
178
|
+
showDates = true,
|
|
179
|
+
hideDays,
|
|
180
|
+
currentDate,
|
|
181
|
+
blockedSlots,
|
|
182
|
+
days,
|
|
183
|
+
minDuration,
|
|
184
|
+
maxDuration,
|
|
185
|
+
disabledDates,
|
|
186
|
+
compact = false,
|
|
187
|
+
mobile: mobileProp = 'auto',
|
|
188
|
+
event: eventSnippet,
|
|
189
|
+
empty: emptySnippet,
|
|
190
|
+
dayHeader: dayHeaderSnippet,
|
|
191
|
+
header: headerSnippet,
|
|
192
|
+
navigation: navigationSnippet,
|
|
193
|
+
oneventclick,
|
|
194
|
+
oneventcreate,
|
|
195
|
+
oneventmove,
|
|
196
|
+
onviewchange,
|
|
197
|
+
ondatechange,
|
|
198
|
+
oneventhover,
|
|
199
|
+
}: Props = $props();
|
|
200
|
+
|
|
201
|
+
// In readOnly mode, suppress mutation callbacks
|
|
202
|
+
const effectiveCreate = $derived(readOnly ? undefined : oneventcreate);
|
|
203
|
+
const effectiveMove = $derived(readOnly ? undefined : oneventmove);
|
|
204
|
+
|
|
205
|
+
// ── Mobile detection (container-based, not viewport) ──
|
|
206
|
+
let containerWidth = $state(0);
|
|
207
|
+
const isMobileContainer = $derived(containerWidth > 0 && containerWidth < MOBILE_BREAKPOINT);
|
|
208
|
+
|
|
209
|
+
const useMobile = $derived(
|
|
210
|
+
mobileProp === 'auto' ? isMobileContainer : Boolean(mobileProp)
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
// ── Smart auto-theme ──
|
|
214
|
+
// When theme is empty (auto preset) and autoTheme is not false,
|
|
215
|
+
// probe the host page on mount and reactively watch for host theme changes.
|
|
216
|
+
let calEl: HTMLElement | undefined = $state();
|
|
217
|
+
let probedTheme = $state('');
|
|
218
|
+
// Only delay rendering when auto-probe is actually needed
|
|
219
|
+
const needsProbe = $derived(theme === auto && autoTheme !== false);
|
|
220
|
+
|
|
221
|
+
onMount(() => {
|
|
222
|
+
if (!calEl) return;
|
|
223
|
+
|
|
224
|
+
// Measure container width for mobile detection
|
|
225
|
+
containerWidth = calEl.clientWidth;
|
|
226
|
+
const ro = new ResizeObserver((entries) => {
|
|
227
|
+
containerWidth = Math.round(entries[0].contentRect.width);
|
|
228
|
+
});
|
|
229
|
+
ro.observe(calEl);
|
|
230
|
+
|
|
231
|
+
// Only probe theme when using the auto preset
|
|
232
|
+
if (!needsProbe) return () => ro.disconnect();
|
|
233
|
+
|
|
234
|
+
const opts: AutoThemeOptions = typeof autoTheme === 'object' ? autoTheme : {};
|
|
235
|
+
const stopTheme = observeHostTheme(calEl, (vars) => {
|
|
236
|
+
probedTheme = vars;
|
|
237
|
+
}, opts);
|
|
238
|
+
return () => { ro.disconnect(); stopTheme?.(); };
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
/** Effective theme: user-provided takes priority, otherwise probed auto. */
|
|
242
|
+
const effectiveTheme = $derived(theme === auto && autoTheme !== false ? probedTheme : theme);
|
|
243
|
+
|
|
244
|
+
// ── Create reactive state ──
|
|
245
|
+
const store: EventStore = $derived(createEventStore(adapter));
|
|
246
|
+
const viewState: ViewState = createViewState(untrack(() => ({
|
|
247
|
+
view: activeViewId ?? views[0]?.id,
|
|
248
|
+
mondayStart,
|
|
249
|
+
initialDate,
|
|
250
|
+
dayCount: days,
|
|
251
|
+
modeForView: (viewId) => views.find((v) => v.id === viewId)?.mode,
|
|
252
|
+
})));
|
|
253
|
+
const selection: Selection = createSelection();
|
|
254
|
+
const drag: DragState = createDragState();
|
|
255
|
+
|
|
256
|
+
// ── Drag commit handler ──
|
|
257
|
+
// Views call this on pointer-up to process drag results.
|
|
258
|
+
async function commitDrag(): Promise<void> {
|
|
259
|
+
if (readOnly) { drag.cancel(); return; }
|
|
260
|
+
const mode = drag.mode;
|
|
261
|
+
const payload = drag.commit();
|
|
262
|
+
if (!payload) return;
|
|
263
|
+
|
|
264
|
+
let { start, end } = payload;
|
|
265
|
+
|
|
266
|
+
// Enforce min/max duration for create and resize
|
|
267
|
+
if (mode === 'create' || mode === 'resize-start' || mode === 'resize-end') {
|
|
268
|
+
const durationMs = end.getTime() - start.getTime();
|
|
269
|
+
const durationMin = durationMs / 60_000;
|
|
270
|
+
if (minDuration && durationMin < minDuration) {
|
|
271
|
+
if (mode === 'resize-start') {
|
|
272
|
+
start = new Date(end.getTime() - minDuration * 60_000);
|
|
273
|
+
} else {
|
|
274
|
+
end = new Date(start.getTime() + minDuration * 60_000);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (maxDuration && durationMin > maxDuration) {
|
|
278
|
+
if (mode === 'resize-start') {
|
|
279
|
+
start = new Date(end.getTime() - maxDuration * 60_000);
|
|
280
|
+
} else {
|
|
281
|
+
end = new Date(start.getTime() + maxDuration * 60_000);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Reject if target lands on a disabled date
|
|
287
|
+
if (disabledDates?.length) {
|
|
288
|
+
const startDay = new Date(start); startDay.setHours(0, 0, 0, 0);
|
|
289
|
+
const endDay = new Date(end.getTime() - 1); endDay.setHours(0, 0, 0, 0);
|
|
290
|
+
for (const dd of disabledDates) {
|
|
291
|
+
const dt = new Date(dd); dt.setHours(0, 0, 0, 0);
|
|
292
|
+
const ts = dt.getTime();
|
|
293
|
+
if (ts >= startDay.getTime() && ts <= endDay.getTime()) return;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Reject if target overlaps a blocked slot
|
|
298
|
+
if (blockedSlots?.length) {
|
|
299
|
+
const startH = start.getHours() + start.getMinutes() / 60;
|
|
300
|
+
const endH = end.getHours() + end.getMinutes() / 60 + (end.getDate() !== start.getDate() ? 24 : 0);
|
|
301
|
+
const jsDay = start.getDay();
|
|
302
|
+
const isoDay = jsDay === 0 ? 7 : jsDay;
|
|
303
|
+
for (const slot of blockedSlots) {
|
|
304
|
+
if (slot.day && slot.day !== isoDay) continue;
|
|
305
|
+
if (startH < slot.end && endH > slot.start) return;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if ((mode === 'move' || mode === 'resize-start' || mode === 'resize-end') && payload.eventId) {
|
|
310
|
+
try {
|
|
311
|
+
await store.move(payload.eventId, start, end);
|
|
312
|
+
const ev = store.byId(payload.eventId);
|
|
313
|
+
if (ev) effectiveMove?.(ev, start, end);
|
|
314
|
+
} catch (e) {
|
|
315
|
+
// Silently handle read-only / missing event errors.
|
|
316
|
+
// The optimistic update in store.move() already reverted.
|
|
317
|
+
const msg = e instanceof Error ? e.message : '';
|
|
318
|
+
if (!msg.includes('read-only') && !msg.includes('not found')) {
|
|
319
|
+
console.warn('[calendar] drag commit failed:', e);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
} else if (mode === 'create') {
|
|
323
|
+
effectiveCreate?.({ start, end });
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ── Load range signal ──
|
|
328
|
+
// Views can write a wider range here to override the default viewState.range.
|
|
329
|
+
// This lets infinite-scroll views (PlannerWeek, PlannerDay) declare their
|
|
330
|
+
// buffer needs without directly calling store.load().
|
|
331
|
+
let viewLoadRange = $state<{ start: Date; end: Date } | null>(null);
|
|
332
|
+
|
|
333
|
+
// ── Single context object ──
|
|
334
|
+
// All view state is exposed through one context key with reactive getters.
|
|
335
|
+
// Views read this via useCalendarContext() from views/shared/context.svelte.ts.
|
|
336
|
+
setContext('calendar', {
|
|
337
|
+
// Engine objects (hold $state internally)
|
|
338
|
+
get store() { return store; },
|
|
339
|
+
viewState,
|
|
340
|
+
selection,
|
|
341
|
+
drag,
|
|
342
|
+
commitDrag,
|
|
343
|
+
|
|
344
|
+
// Callbacks
|
|
345
|
+
get oneventclick() { return oneventclick; },
|
|
346
|
+
get oneventcreate() { return effectiveCreate; },
|
|
347
|
+
get oneventmove() { return effectiveMove; },
|
|
348
|
+
get oneventhover() { return oneventhover; },
|
|
349
|
+
|
|
350
|
+
// Config (reactive via getters)
|
|
351
|
+
get readOnly() { return readOnly; },
|
|
352
|
+
get visibleHours() { return visibleHours; },
|
|
353
|
+
get snapInterval() { return snapInterval; },
|
|
354
|
+
get eventSnippet() { return eventSnippet; },
|
|
355
|
+
get emptySnippet() { return emptySnippet; },
|
|
356
|
+
get showNavigation() { return showNavigation; },
|
|
357
|
+
get equalDays() { return equalDays; },
|
|
358
|
+
get showDates() { return showDates; },
|
|
359
|
+
get hideDays() { return hideDays; },
|
|
360
|
+
get blockedSlots() { return blockedSlots; },
|
|
361
|
+
get dayHeaderSnippet() { return dayHeaderSnippet; },
|
|
362
|
+
get minDuration() { return minDuration; },
|
|
363
|
+
get maxDuration() { return maxDuration; },
|
|
364
|
+
get disabledDates() { return disabledDates; },
|
|
365
|
+
get mobile() { return useMobile; },
|
|
366
|
+
get autoHeight() { return heightProp === 'auto'; },
|
|
367
|
+
get compact() { return compact; },
|
|
368
|
+
|
|
369
|
+
// Load range (read/write)
|
|
370
|
+
get loadRange() { return viewLoadRange; },
|
|
371
|
+
setLoadRange(range: { start: Date; end: Date } | null) { viewLoadRange = range; },
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
// ── Load events when effective range changes ──
|
|
375
|
+
$effect(() => {
|
|
376
|
+
const range = viewLoadRange ?? viewState.range;
|
|
377
|
+
store.load({ start: range.start, end: range.end });
|
|
378
|
+
});
|
|
379
|
+
// Eager initial load — $effect runs after paint, but for sync adapters
|
|
380
|
+
// (memory, recurring) this resolves in the same microtask.
|
|
381
|
+
untrack(() => store.load({ start: viewState.range.start, end: viewState.range.end }));
|
|
382
|
+
|
|
383
|
+
// Keep active view in sync when external view prop changes after mount.
|
|
384
|
+
$effect(() => {
|
|
385
|
+
if (activeViewId) viewState.setView(activeViewId);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
// Sync controlled currentDate prop → viewState
|
|
389
|
+
$effect(() => {
|
|
390
|
+
if (currentDate) viewState.setFocusDate(currentDate);
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
// Sync days prop → viewState.dayCount
|
|
394
|
+
$effect(() => {
|
|
395
|
+
if (days !== undefined && viewState.dayCount !== days) viewState.setDayCount(days);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
// Notify host when focusDate changes
|
|
399
|
+
$effect(() => {
|
|
400
|
+
const d = viewState.focusDate;
|
|
401
|
+
ondatechange?.(d);
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
// Keep view state's week-start rule in sync with incoming prop changes.
|
|
405
|
+
$effect(() => {
|
|
406
|
+
if (viewState.mondayStart !== mondayStart) {
|
|
407
|
+
viewState.setMondayStart(mondayStart);
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
// Notify host when active view changes (e.g. via mode toggles).
|
|
412
|
+
$effect(() => {
|
|
413
|
+
onviewchange?.(viewState.view);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
// ── Resolve active view ──
|
|
418
|
+
// When mobile is active, Planner views get remapped to Mobile variants.
|
|
419
|
+
// Agenda views stay as Agenda — they're already list-based and will adapt
|
|
420
|
+
// via the 'calendar:mobile' context flag.
|
|
421
|
+
const resolvedView = $derived.by(() => {
|
|
422
|
+
const requested = views.find((v) => v.id === viewState.view) ?? views[0];
|
|
423
|
+
if (!useMobile || !requested) return requested;
|
|
424
|
+
// Already a mobile view? Keep it.
|
|
425
|
+
if (requested.id.endsWith('-mobile')) return requested;
|
|
426
|
+
// Agenda views: keep as-is (they adapt internally via mobile context).
|
|
427
|
+
if (requested.label === 'Agenda') return requested;
|
|
428
|
+
// Planner / other views: remap to mobile variant with the same mode.
|
|
429
|
+
const mobileVariant = views.find(
|
|
430
|
+
(v) => v.id === `${requested.mode}-mobile`
|
|
431
|
+
);
|
|
432
|
+
return mobileVariant ?? requested;
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
// Backward-compat alias used in the template.
|
|
436
|
+
const activeView = $derived(resolvedView);
|
|
437
|
+
|
|
438
|
+
// Non-mobile views for mode pills (exclude mobile entries).
|
|
439
|
+
const desktopViews = $derived(views.filter((v) => !v.id.endsWith('-mobile')));
|
|
440
|
+
|
|
441
|
+
// ── Date label (always visible, centered over views) ──
|
|
442
|
+
const dateLabel = $derived.by(() => {
|
|
443
|
+
if (!showDates) {
|
|
444
|
+
// Template week mode: just show weekday name (day) or nothing (week)
|
|
445
|
+
if (viewState.mode === 'day') {
|
|
446
|
+
return viewState.focusDate.toLocaleDateString(locale, { weekday: 'long' });
|
|
447
|
+
}
|
|
448
|
+
return ''; // week views have their own day headers
|
|
449
|
+
}
|
|
450
|
+
if (viewState.mode === 'day') {
|
|
451
|
+
return viewState.focusDate.toLocaleDateString(locale, {
|
|
452
|
+
weekday: 'long',
|
|
453
|
+
month: 'short',
|
|
454
|
+
day: 'numeric',
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
return viewState.focusDate.toLocaleDateString(locale, {
|
|
458
|
+
month: 'long',
|
|
459
|
+
year: 'numeric',
|
|
460
|
+
});
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
// Which modes are available?
|
|
464
|
+
const modes = $derived.by(() => {
|
|
465
|
+
const g = new Set(desktopViews.map((v) => v.mode));
|
|
466
|
+
return (['day', 'week'] as const).filter((key) => g.has(key));
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
const L = $derived(getLabels());
|
|
470
|
+
|
|
471
|
+
/** Switch to a different mode (day/week), preserving the current view label. */
|
|
472
|
+
function switchMode(g: 'day' | 'week') {
|
|
473
|
+
const currentLabel = desktopViews.find((v) => v.id === viewState.view)?.label
|
|
474
|
+
?? activeView?.label;
|
|
475
|
+
const match = desktopViews.find((v) => v.mode === g && v.label === currentLabel);
|
|
476
|
+
const fallback = desktopViews.find((v) => v.mode === g);
|
|
477
|
+
const target = match ?? fallback;
|
|
478
|
+
if (target) viewState.setView(target.id);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** True when the current view range already includes today. */
|
|
482
|
+
const viewIncludesToday = $derived.by(() => {
|
|
483
|
+
const now = Date.now();
|
|
484
|
+
const { start, end } = viewState.range;
|
|
485
|
+
return now >= start.getTime() && now < end.getTime();
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
/** Header context for custom header snippet */
|
|
489
|
+
const headerCtx = $derived({
|
|
490
|
+
dateLabel,
|
|
491
|
+
mode: viewState.mode,
|
|
492
|
+
modes,
|
|
493
|
+
switchMode,
|
|
494
|
+
prev: () => viewState.prev(),
|
|
495
|
+
next: () => viewState.next(),
|
|
496
|
+
goToday: () => viewState.goToday(),
|
|
497
|
+
isViewOnToday: viewIncludesToday,
|
|
498
|
+
focusDate: viewState.focusDate,
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
/** Navigation context for custom navigation snippet */
|
|
502
|
+
const navCtx = $derived({
|
|
503
|
+
prev: () => viewState.prev(),
|
|
504
|
+
next: () => viewState.next(),
|
|
505
|
+
goToday: () => viewState.goToday(),
|
|
506
|
+
isViewOnToday: viewIncludesToday,
|
|
507
|
+
focusDate: viewState.focusDate,
|
|
508
|
+
mode: viewState.mode,
|
|
509
|
+
});
|
|
510
|
+
</script>
|
|
511
|
+
|
|
512
|
+
<div
|
|
513
|
+
class="cal"
|
|
514
|
+
bind:this={calEl}
|
|
515
|
+
style="{effectiveTheme}; {heightProp === 'auto' ? '' : `--cal-h: ${heightProp}px;`} --cal-r: {borderRadius}px"
|
|
516
|
+
class:cal--auto={heightProp === 'auto'}
|
|
517
|
+
role="region"
|
|
518
|
+
aria-label={L.calendar}
|
|
519
|
+
dir={dir}
|
|
520
|
+
lang={locale}
|
|
521
|
+
>
|
|
522
|
+
<!-- ─── Custom header snippet (replaces all chrome) ─── -->
|
|
523
|
+
{#if headerSnippet}
|
|
524
|
+
{@render headerSnippet(headerCtx)}
|
|
525
|
+
|
|
526
|
+
<!-- ─── Mobile header (flow layout, no absolute) ─── -->
|
|
527
|
+
{:else if useMobile}
|
|
528
|
+
<div class="cal-m-hd">
|
|
529
|
+
<div class="cal-m-left">
|
|
530
|
+
{#if navigationSnippet}
|
|
531
|
+
{@render navigationSnippet(navCtx)}
|
|
532
|
+
{:else if showNavigation}
|
|
533
|
+
<button class="cal-m-nav" onclick={() => viewState.prev()} aria-label={viewState.mode === 'day' ? L.previousDay : L.previousWeek}>
|
|
534
|
+
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" width="16" height="16" aria-hidden="true"><path d="M10 3 5 8l5 5"/></svg>
|
|
535
|
+
</button>
|
|
536
|
+
{/if}
|
|
537
|
+
|
|
538
|
+
{#if showModePills && modes.length > 1}
|
|
539
|
+
<div class="cal-m-pills" role="group" aria-label={L.viewMode}>
|
|
540
|
+
{#each modes as g}
|
|
541
|
+
<button
|
|
542
|
+
class="cal-m-pill"
|
|
543
|
+
class:cal-m-pill--active={viewState.mode === g}
|
|
544
|
+
aria-pressed={viewState.mode === g}
|
|
545
|
+
onclick={() => switchMode(g)}
|
|
546
|
+
>
|
|
547
|
+
{g === 'day' ? L.day : L.week}
|
|
548
|
+
</button>
|
|
549
|
+
{/each}
|
|
550
|
+
</div>
|
|
551
|
+
{/if}
|
|
552
|
+
</div>
|
|
553
|
+
|
|
554
|
+
<span class="cal-m-title">{dateLabel}</span>
|
|
555
|
+
|
|
556
|
+
<div class="cal-m-right">
|
|
557
|
+
{#if !navigationSnippet && showNavigation}
|
|
558
|
+
<button class="cal-m-nav" onclick={() => viewState.next()} aria-label={viewState.mode === 'day' ? L.nextDay : L.nextWeek}>
|
|
559
|
+
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" width="16" height="16" aria-hidden="true"><path d="M6 3l5 5-5 5"/></svg>
|
|
560
|
+
</button>
|
|
561
|
+
{/if}
|
|
562
|
+
</div>
|
|
563
|
+
</div>
|
|
564
|
+
|
|
565
|
+
<!-- Today pill — floats below header, doesn't affect header flow -->
|
|
566
|
+
{#if !navigationSnippet && showNavigation && !viewIncludesToday}
|
|
567
|
+
<div class="cal-m-today-bar">
|
|
568
|
+
<button class="cal-m-today" onclick={() => viewState.goToday()}>
|
|
569
|
+
{L.today}
|
|
570
|
+
</button>
|
|
571
|
+
</div>
|
|
572
|
+
{/if}
|
|
573
|
+
|
|
574
|
+
<!-- ─── Desktop chrome ─── -->
|
|
575
|
+
{:else}
|
|
576
|
+
<!-- Floating mode pills (hidden for Agenda views) -->
|
|
577
|
+
{#if showModePills && modes.length > 1 && activeView?.label !== 'Agenda'}
|
|
578
|
+
<div class="cal-pills" role="group" aria-label={L.viewMode}>
|
|
579
|
+
{#each modes as g}
|
|
580
|
+
<button
|
|
581
|
+
class="cal-pill"
|
|
582
|
+
class:cal-pill--active={viewState.mode === g}
|
|
583
|
+
aria-pressed={viewState.mode === g}
|
|
584
|
+
onclick={() => switchMode(g)}
|
|
585
|
+
>
|
|
586
|
+
{g === 'day' ? L.day : L.week}
|
|
587
|
+
</button>
|
|
588
|
+
{/each}
|
|
589
|
+
</div>
|
|
590
|
+
{/if}
|
|
591
|
+
{/if}
|
|
592
|
+
|
|
593
|
+
<div class="cal-body">
|
|
594
|
+
{#if activeView}
|
|
595
|
+
{@const Comp = activeView.component}
|
|
596
|
+
<Comp
|
|
597
|
+
events={store.events}
|
|
598
|
+
style={effectiveTheme}
|
|
599
|
+
height={null}
|
|
600
|
+
mode={activeView.mode}
|
|
601
|
+
mondayStart={viewState.mondayStart}
|
|
602
|
+
{locale}
|
|
603
|
+
focusDate={viewState.focusDate}
|
|
604
|
+
oneventclick={oneventclick}
|
|
605
|
+
oneventcreate={effectiveCreate}
|
|
606
|
+
readOnly={readOnly}
|
|
607
|
+
visibleHours={visibleHours}
|
|
608
|
+
selectedEventId={selection.selectedId}
|
|
609
|
+
{...activeView.props ?? {}}
|
|
610
|
+
/>
|
|
611
|
+
{:else}
|
|
612
|
+
<div class="cal-empty">No views registered.</div>
|
|
613
|
+
{/if}
|
|
614
|
+
</div>
|
|
615
|
+
|
|
616
|
+
{#if store.loading}
|
|
617
|
+
<div class="cal-loading"></div>
|
|
618
|
+
{/if}
|
|
619
|
+
</div>
|
|
620
|
+
|
|
621
|
+
<style>
|
|
622
|
+
.cal {
|
|
623
|
+
position: relative;
|
|
624
|
+
width: 100%;
|
|
625
|
+
min-width: 0;
|
|
626
|
+
height: var(--cal-h, 600px);
|
|
627
|
+
background: var(--dt-bg, inherit);
|
|
628
|
+
border-radius: var(--cal-r, 12px);
|
|
629
|
+
overflow: clip;
|
|
630
|
+
display: flex;
|
|
631
|
+
flex-direction: column;
|
|
632
|
+
border: 1px solid var(--dt-border, rgba(148, 163, 184, 0.07));
|
|
633
|
+
box-sizing: border-box;
|
|
634
|
+
}
|
|
635
|
+
.cal--auto {
|
|
636
|
+
height: auto;
|
|
637
|
+
overflow: visible;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
/* ── Floating pills ── */
|
|
642
|
+
.cal-pills {
|
|
643
|
+
position: absolute;
|
|
644
|
+
top: 22px;
|
|
645
|
+
bottom: auto;
|
|
646
|
+
left: 10px;
|
|
647
|
+
z-index: 20;
|
|
648
|
+
display: flex;
|
|
649
|
+
gap: 2px;
|
|
650
|
+
background: color-mix(in srgb, var(--dt-surface, #10141c) 85%, transparent);
|
|
651
|
+
backdrop-filter: blur(6px);
|
|
652
|
+
-webkit-backdrop-filter: blur(6px);
|
|
653
|
+
border-radius: 8px;
|
|
654
|
+
padding: 2px;
|
|
655
|
+
border: 1px solid var(--dt-border, rgba(148, 163, 184, 0.07));
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
.cal-pill {
|
|
659
|
+
border: none;
|
|
660
|
+
background: transparent;
|
|
661
|
+
color: var(--dt-text-2, rgba(148, 163, 184, 0.55));
|
|
662
|
+
cursor: pointer;
|
|
663
|
+
font: 600 11px / 1 var(--dt-sans, 'Outfit', system-ui, sans-serif);
|
|
664
|
+
padding: 6px 12px;
|
|
665
|
+
border-radius: 6px;
|
|
666
|
+
letter-spacing: 0.04em;
|
|
667
|
+
text-transform: uppercase;
|
|
668
|
+
transition: background 100ms, color 100ms;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
.cal-pill:hover {
|
|
672
|
+
color: var(--dt-text, rgba(226, 232, 240, 0.85));
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
.cal-pill--active {
|
|
676
|
+
background: var(--dt-accent, #ef4444);
|
|
677
|
+
color: var(--dt-btn-text, #fff);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
.cal-pill:focus-visible {
|
|
681
|
+
outline: 2px solid color-mix(in srgb, var(--dt-accent, #ef4444) 55%, transparent);
|
|
682
|
+
outline-offset: 2px;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
.cal-body {
|
|
686
|
+
flex: 1;
|
|
687
|
+
min-height: 0;
|
|
688
|
+
position: relative;
|
|
689
|
+
overflow: hidden;
|
|
690
|
+
}
|
|
691
|
+
.cal--auto .cal-body {
|
|
692
|
+
overflow: visible;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
.cal-empty {
|
|
696
|
+
display: flex;
|
|
697
|
+
align-items: center;
|
|
698
|
+
justify-content: center;
|
|
699
|
+
height: 100%;
|
|
700
|
+
font: 400 13px / 1 var(--dt-sans, 'Outfit', system-ui, sans-serif);
|
|
701
|
+
color: var(--dt-text-3, rgba(100, 116, 139, 0.55));
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
.cal-loading {
|
|
705
|
+
position: absolute;
|
|
706
|
+
top: 0;
|
|
707
|
+
left: 0;
|
|
708
|
+
right: 0;
|
|
709
|
+
height: 2px;
|
|
710
|
+
background: linear-gradient(
|
|
711
|
+
90deg,
|
|
712
|
+
transparent 0%,
|
|
713
|
+
var(--dt-accent, #ef4444) 50%,
|
|
714
|
+
transparent 100%
|
|
715
|
+
);
|
|
716
|
+
animation: cal-slide 1.2s ease-in-out infinite;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
@keyframes cal-slide {
|
|
720
|
+
0% { transform: translateX(-100%); }
|
|
721
|
+
100% { transform: translateX(100%); }
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/* ── Mobile header (flow layout) ── */
|
|
725
|
+
.cal-m-hd {
|
|
726
|
+
display: flex;
|
|
727
|
+
align-items: center;
|
|
728
|
+
gap: 4px;
|
|
729
|
+
padding: 8px 8px 6px;
|
|
730
|
+
border-bottom: 1px solid var(--dt-border, rgba(148, 163, 184, 0.07));
|
|
731
|
+
flex-shrink: 0;
|
|
732
|
+
min-height: 44px;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
.cal-m-left,
|
|
736
|
+
.cal-m-right {
|
|
737
|
+
display: flex;
|
|
738
|
+
align-items: center;
|
|
739
|
+
gap: 2px;
|
|
740
|
+
flex-shrink: 0;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
.cal-m-right {
|
|
744
|
+
justify-content: flex-end;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
.cal-m-nav {
|
|
748
|
+
display: flex;
|
|
749
|
+
align-items: center;
|
|
750
|
+
justify-content: center;
|
|
751
|
+
width: 32px;
|
|
752
|
+
height: 32px;
|
|
753
|
+
border: none;
|
|
754
|
+
background: transparent;
|
|
755
|
+
color: var(--dt-text-2, rgba(148, 163, 184, 0.55));
|
|
756
|
+
border-radius: 50%;
|
|
757
|
+
cursor: pointer;
|
|
758
|
+
transition: background 120ms, color 120ms;
|
|
759
|
+
-webkit-tap-highlight-color: transparent;
|
|
760
|
+
flex-shrink: 0;
|
|
761
|
+
}
|
|
762
|
+
.cal-m-nav:hover {
|
|
763
|
+
color: var(--dt-text, rgba(226, 232, 240, 0.85));
|
|
764
|
+
background: color-mix(in srgb, var(--dt-text, rgba(226, 232, 240, 0.85)) 8%, transparent);
|
|
765
|
+
}
|
|
766
|
+
.cal-m-nav:active {
|
|
767
|
+
background: var(--dt-accent-dim, rgba(239, 68, 68, 0.12));
|
|
768
|
+
}
|
|
769
|
+
.cal-m-nav:focus-visible {
|
|
770
|
+
outline: 2px solid color-mix(in srgb, var(--dt-accent, #ef4444) 55%, transparent);
|
|
771
|
+
outline-offset: 2px;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
.cal-m-pills {
|
|
775
|
+
display: flex;
|
|
776
|
+
gap: 2px;
|
|
777
|
+
background: color-mix(in srgb, var(--dt-surface, #10141c) 85%, transparent);
|
|
778
|
+
border-radius: 8px;
|
|
779
|
+
padding: 2px;
|
|
780
|
+
border: 1px solid var(--dt-border, rgba(148, 163, 184, 0.07));
|
|
781
|
+
flex-shrink: 0;
|
|
782
|
+
}
|
|
783
|
+
.cal-m-pill {
|
|
784
|
+
border: none;
|
|
785
|
+
background: transparent;
|
|
786
|
+
color: var(--dt-text-2, rgba(148, 163, 184, 0.55));
|
|
787
|
+
cursor: pointer;
|
|
788
|
+
font: 600 11px / 1 var(--dt-sans, system-ui, sans-serif);
|
|
789
|
+
padding: 5px 10px;
|
|
790
|
+
border-radius: 6px;
|
|
791
|
+
letter-spacing: 0.04em;
|
|
792
|
+
text-transform: uppercase;
|
|
793
|
+
transition: background 100ms, color 100ms;
|
|
794
|
+
-webkit-tap-highlight-color: transparent;
|
|
795
|
+
}
|
|
796
|
+
.cal-m-pill:hover {
|
|
797
|
+
color: var(--dt-text, rgba(226, 232, 240, 0.85));
|
|
798
|
+
}
|
|
799
|
+
.cal-m-pill--active {
|
|
800
|
+
background: var(--dt-accent, #ef4444);
|
|
801
|
+
color: var(--dt-btn-text, #fff);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
.cal-m-title {
|
|
805
|
+
flex: 1;
|
|
806
|
+
text-align: center;
|
|
807
|
+
font: 600 14px / 1.2 var(--dt-sans, system-ui, sans-serif);
|
|
808
|
+
color: var(--dt-text, rgba(226, 232, 240, 0.85));
|
|
809
|
+
white-space: nowrap;
|
|
810
|
+
overflow: hidden;
|
|
811
|
+
text-overflow: ellipsis;
|
|
812
|
+
min-width: 0;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
.cal-m-today-bar {
|
|
816
|
+
display: flex;
|
|
817
|
+
justify-content: center;
|
|
818
|
+
padding: 12px 8px 6px;
|
|
819
|
+
flex-shrink: 0;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
.cal-m-today {
|
|
823
|
+
font: 600 11px / 1 var(--dt-sans, system-ui, sans-serif);
|
|
824
|
+
color: var(--dt-accent, #ef4444);
|
|
825
|
+
background: color-mix(in srgb, var(--dt-accent, #ef4444) 10%, transparent);
|
|
826
|
+
border: none;
|
|
827
|
+
padding: 5px 10px;
|
|
828
|
+
border-radius: 6px;
|
|
829
|
+
cursor: pointer;
|
|
830
|
+
white-space: nowrap;
|
|
831
|
+
letter-spacing: 0.04em;
|
|
832
|
+
text-transform: uppercase;
|
|
833
|
+
transition: background 120ms, color 120ms;
|
|
834
|
+
-webkit-tap-highlight-color: transparent;
|
|
835
|
+
flex-shrink: 0;
|
|
836
|
+
}
|
|
837
|
+
.cal-m-today:hover {
|
|
838
|
+
background: color-mix(in srgb, var(--dt-accent, #ef4444) 18%, transparent);
|
|
839
|
+
}
|
|
840
|
+
.cal-m-today:active {
|
|
841
|
+
background: color-mix(in srgb, var(--dt-accent, #ef4444) 25%, transparent);
|
|
842
|
+
}
|
|
843
|
+
.cal-m-today:focus-visible {
|
|
844
|
+
outline: 2px solid color-mix(in srgb, var(--dt-accent, #ef4444) 55%, transparent);
|
|
845
|
+
outline-offset: 2px;
|
|
846
|
+
}
|
|
847
|
+
</style>
|