@nomideusz/svelte-calendar 0.6.3 → 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.
Files changed (43) hide show
  1. package/README.md +183 -5
  2. package/dist/adapters/composite.d.ts +15 -0
  3. package/dist/adapters/composite.js +65 -0
  4. package/dist/adapters/index.d.ts +5 -1
  5. package/dist/adapters/index.js +2 -0
  6. package/dist/adapters/mapped.d.ts +170 -0
  7. package/dist/adapters/mapped.js +343 -0
  8. package/dist/adapters/recurring.d.ts +4 -0
  9. package/dist/adapters/recurring.js +5 -10
  10. package/dist/adapters/types.d.ts +11 -2
  11. package/dist/calendar/Calendar.svelte +147 -76
  12. package/dist/calendar/Calendar.svelte.d.ts +12 -0
  13. package/dist/core/index.d.ts +3 -1
  14. package/dist/core/index.js +2 -0
  15. package/dist/core/measure.d.ts +104 -0
  16. package/dist/core/measure.js +195 -0
  17. package/dist/core/time.js +2 -2
  18. package/dist/core/types.d.ts +32 -0
  19. package/dist/engine/event-store.svelte.js +22 -7
  20. package/dist/headless/create-agenda.svelte.d.ts +67 -0
  21. package/dist/headless/create-agenda.svelte.js +139 -0
  22. package/dist/headless/create-calendar.svelte.d.ts +2 -0
  23. package/dist/headless/create-calendar.svelte.js +380 -0
  24. package/dist/headless/index.d.ts +4 -0
  25. package/dist/headless/index.js +2 -0
  26. package/dist/headless/types.d.ts +219 -0
  27. package/dist/headless/types.js +1 -0
  28. package/dist/index.d.ts +6 -4
  29. package/dist/index.js +4 -2
  30. package/dist/primitives/EventBlock.svelte +87 -2
  31. package/dist/views/agenda/AgendaDay.svelte +183 -89
  32. package/dist/views/agenda/AgendaWeek.svelte +148 -95
  33. package/dist/views/mobile/MobileDay.svelte +43 -16
  34. package/dist/views/mobile/MobileWeek.svelte +32 -17
  35. package/dist/views/planner/PlannerDay.svelte +91 -36
  36. package/dist/views/planner/PlannerWeek.svelte +107 -132
  37. package/dist/views/shared/context.svelte.d.ts +47 -0
  38. package/dist/views/shared/context.svelte.js +47 -0
  39. package/dist/views/shared/format.d.ts +19 -0
  40. package/dist/views/shared/format.js +46 -0
  41. package/dist/views/shared/index.d.ts +4 -0
  42. package/dist/views/shared/index.js +2 -0
  43. package/package.json +76 -71
@@ -9,14 +9,15 @@
9
9
  *
10
10
  * Answers: "What's coming up and when do I need to be ready?"
11
11
  */
12
- import { getContext, type Snippet } from 'svelte';
13
12
  import { createClock } from '../../core/clock.svelte.js';
14
13
  import type { TimelineEvent } from '../../core/types.js';
15
14
  import { sod, DAY_MS, startOfWeek, dayNum, isAllDay, isMultiDay } from '../../core/time.js';
16
- import { weekdayLong, monthLong, fmtTime as _fmtTime, fmtDuration, getLabels } from '../../core/locale.js';
17
- import type { ViewState } from '../../engine/view-state.svelte.js';
15
+ import { weekdayLong, monthLong, getLabels } from '../../core/locale.js';
16
+ import { useCalendarContext } from '../shared/context.svelte.js';
17
+ import { fmtTime, duration, timeUntilMs, progress, groupIntoSlots } from '../shared/format.js';
18
18
 
19
19
  const L = $derived(getLabels());
20
+ const ctx = useCalendarContext();
20
21
 
21
22
  interface Props {
22
23
  mondayStart?: boolean;
@@ -42,19 +43,17 @@
42
43
  }: Props = $props();
43
44
 
44
45
  const clock = createClock();
45
- const viewState = getContext<ViewState>('calendar:viewState') as ViewState | undefined;
46
- const showNavCtx = getContext<{ current: boolean }>('calendar:showNavigation') as { current: boolean } | undefined;
47
- const equalDaysCtx = getContext<{ current: boolean }>('calendar:equalDays') as { current: boolean } | undefined;
48
- const showDatesCtx = getContext<{ current: boolean }>('calendar:showDates') as { current: boolean } | undefined;
49
- const hideDaysCtx = getContext<{ current: number[] | undefined }>('calendar:hideDays') as { current: number[] | undefined } | undefined;
50
- const mobileCtx = getContext<{ current: boolean }>('calendar:mobile') as { current: boolean } | undefined;
51
- const autoHeightCtx = getContext<{ current: boolean }>('calendar:autoHeight') as { current: boolean } | undefined;
52
- const showNav = $derived(showNavCtx?.current ?? true);
53
- const equalDays = $derived(equalDaysCtx?.current ?? false);
54
- const showDates = $derived(showDatesCtx?.current ?? true);
55
- const hideDays = $derived(hideDaysCtx?.current);
56
- const isMobile = $derived(mobileCtx?.current ?? false);
57
- const autoHeight = $derived(autoHeightCtx?.current ?? false);
46
+ const viewState = $derived(ctx.viewState);
47
+ const showNav = $derived(ctx.showNav);
48
+ const equalDays = $derived(ctx.equalDays);
49
+ const showDates = $derived(ctx.showDates);
50
+ const hideDays = $derived(ctx.hideDays);
51
+ const isMobile = $derived(ctx.isMobile);
52
+ const autoHeight = $derived(ctx.autoHeight);
53
+ const compact = $derived(ctx.compact);
54
+ const dayHeaderSnippet = $derived(ctx.dayHeaderSnippet);
55
+ const oneventhover = $derived(ctx.oneventhover);
56
+ const disabledSet = $derived(ctx.disabledSet);
58
57
 
59
58
  // ── Swipe navigation (mobile) ──────────────────────
60
59
  let swipeStartX = 0;
@@ -77,68 +76,12 @@
77
76
  }
78
77
  }
79
78
 
80
- // ── New feature contexts ──
81
- const dayHeaderSnippetCtx = getContext<{ current: Snippet<[{ date: Date; isToday: boolean; dayName: string }]> | undefined }>('calendar:dayHeaderSnippet') as { current: Snippet<[{ date: Date; isToday: boolean; dayName: string }]> | undefined } | undefined;
82
- const callbacksCtx = getContext<{ oneventhover?: (event: TimelineEvent) => void }>('calendar:callbacks') as { oneventhover?: (event: TimelineEvent) => void } | undefined;
83
- const disabledDatesCtx = getContext<{ current: Date[] | undefined }>('calendar:disabledDates') as { current: Date[] | undefined } | undefined;
84
-
85
- const dayHeaderSnippet = $derived(dayHeaderSnippetCtx?.current);
86
- const oneventhover = $derived(callbacksCtx?.oneventhover);
87
- const disabledDates = $derived(disabledDatesCtx?.current);
88
- const disabledSet = $derived(new Set(disabledDates?.map(d => sod(d.getTime())) ?? []));
89
-
90
- // ── Format helpers ──────────────────────────────────
91
- function fmtTime(d: Date): string {
92
- return _fmtTime(d, locale);
93
- }
94
-
95
- function duration(ev: TimelineEvent): string {
96
- return fmtDuration(ev.start, ev.end);
97
- }
98
-
99
- function timeUntilMs(ms: number): string {
100
- const diff = ms - clock.tick;
101
- if (diff <= 0) return L.now;
102
- const tMins = Math.floor(diff / 60000);
103
- if (tMins < 60) return `in ${tMins}m`;
104
- const hrs = Math.floor(tMins / 60);
105
- const rm = tMins % 60;
106
- if (hrs < 24) return rm > 0 ? `in ${hrs}h ${rm}m` : `in ${hrs}h`;
107
- const days = Math.floor(hrs / 24);
108
- return `in ${days}d`;
109
- }
110
-
111
- function progress(ev: TimelineEvent): number {
112
- const s = ev.start.getTime();
113
- const e = ev.end.getTime();
114
- return Math.min(1, Math.max(0, (clock.tick - s) / (e - s)));
115
- }
116
-
117
- // ── Overlap grouping ────────────────────────────────
118
- interface TimeSlot {
119
- startMs: number;
120
- endMs: number;
121
- events: TimelineEvent[];
122
- }
123
-
124
- function groupIntoSlots(evts: TimelineEvent[]): TimeSlot[] {
125
- const sorted = [...evts].sort((a, b) => a.start.getTime() - b.start.getTime());
126
- const slots: TimeSlot[] = [];
127
- for (const ev of sorted) {
128
- const last = slots[slots.length - 1];
129
- if (last && ev.start.getTime() < last.endMs) {
130
- last.events.push(ev);
131
- last.endMs = Math.max(last.endMs, ev.end.getTime());
132
- } else {
133
- slots.push({
134
- startMs: ev.start.getTime(),
135
- endMs: ev.end.getTime(),
136
- events: [ev],
137
- });
138
- }
139
- }
140
- return slots;
141
- }
79
+ // ── Format helpers (delegated to shared/format.ts) ──
80
+ // fmtTime, duration, groupIntoSlots imported at top
81
+ // Thin wrappers that bind locale / clock.tick:
82
+ const fmt = (d: Date) => fmtTime(d, locale);
83
+ const eta = (ms: number) => timeUntilMs(ms, clock.tick);
84
+ const prog = (ev: TimelineEvent) => progress(ev, clock.tick);
142
85
 
143
86
  // ── Event handlers ──────────────────────────────────
144
87
  function handleClick(ev: TimelineEvent): void {
@@ -259,10 +202,14 @@
259
202
  <div
260
203
  class="ag-card"
261
204
  class:ag-card--selected={selectedEventId === ev.id}
205
+ class:ag-card--cancelled={ev.status === 'cancelled'}
206
+ class:ag-card--tentative={ev.status === 'tentative'}
207
+ class:ag-card--full={ev.status === 'full'}
208
+ class:ag-card--limited={ev.status === 'limited'}
262
209
  style:--ev-color={ev.color || 'var(--dt-accent)'}
263
210
  role="button"
264
211
  tabindex="0"
265
- aria-label="{ev.title}, {fmtTime(ev.start)} to {fmtTime(ev.end)}, {duration(ev)}"
212
+ aria-label="{ev.title}{ev.status === 'cancelled' ? ' (cancelled)' : ''}{ev.status === 'tentative' ? ' (tentative)' : ''}{ev.status === 'full' ? ' (full)' : ''}{ev.status === 'limited' ? ' (limited)' : ''}, {fmt(ev.start)} to {fmt(ev.end)}, {duration(ev)}"
266
213
  onclick={() => handleClick(ev)}
267
214
  onpointerenter={() => oneventhover?.(ev)}
268
215
  onkeydown={(e) => handleKeydown(e, ev)}
@@ -272,11 +219,14 @@
272
219
  {#if ev.subtitle}
273
220
  <span class="ag-card-sub">{ev.subtitle}</span>
274
221
  {/if}
222
+ {#if ev.location}
223
+ <span class="ag-card-loc">{ev.location}</span>
224
+ {/if}
275
225
  <span class="ag-card-meta">
276
226
  {#if isNow}
277
- {L.until} {fmtTime(ev.end)}
227
+ {L.until} {fmt(ev.end)}
278
228
  {:else}
279
- {fmtTime(ev.start)} – {fmtTime(ev.end)}
229
+ {fmt(ev.start)} – {fmt(ev.end)}
280
230
  {/if}
281
231
  <span class="ag-card-dur">{duration(ev)}</span>
282
232
  {#if eta}
@@ -292,7 +242,7 @@
292
242
  {/if}
293
243
  {#if isNow}
294
244
  <div class="ag-card-progress">
295
- <div class="ag-card-progress-fill" style:width="{progress(ev) * 100}%"></div>
245
+ <div class="ag-card-progress-fill" style:width="{prog(ev) * 100}%"></div>
296
246
  </div>
297
247
  {/if}
298
248
  </div>
@@ -376,6 +326,40 @@
376
326
 
377
327
  {#if day.events.length === 0}
378
328
  <div class="ag-wday-empty">{L.noEvents}</div>
329
+ {:else if compact}
330
+ <!-- Compact: minimal dot + time + title rows for all days -->
331
+ <div class="ag-wday-compact">
332
+ {#each day.timedEvents as ev (ev.id)}
333
+ <div
334
+ class="ag-compact"
335
+ class:ag-compact--selected={selectedEventId === ev.id}
336
+ class:ag-compact--cancelled={ev.status === 'cancelled'}
337
+ class:ag-compact--tentative={ev.status === 'tentative'}
338
+ class:ag-compact--full={ev.status === 'full'}
339
+ class:ag-compact--limited={ev.status === 'limited'}
340
+ style:--ev-color={ev.color || 'var(--dt-accent)'}
341
+ role="button"
342
+ tabindex="0"
343
+ aria-label="{ev.title}, {fmt(ev.start)}, {duration(ev)}"
344
+ onclick={() => handleClick(ev)}
345
+ onpointerenter={() => oneventhover?.(ev)}
346
+ onkeydown={(e) => handleKeydown(e, ev)}
347
+ >
348
+ <span class="ag-compact-dot"></span>
349
+ <span class="ag-compact-time">{fmt(ev.start)}</span>
350
+ <span class="ag-compact-title">{ev.title}</span>
351
+ {#if ev.subtitle}
352
+ <span class="ag-compact-sub">{ev.subtitle}</span>
353
+ {/if}
354
+ {#if ev.tags?.length}
355
+ {#each ev.tags as tag}
356
+ <span class="ag-compact-tag">{tag}</span>
357
+ {/each}
358
+ {/if}
359
+ <span class="ag-compact-dur">{duration(ev)}</span>
360
+ </div>
361
+ {/each}
362
+ </div>
379
363
  {:else if equalDays}
380
364
  <!-- Equal days: card layout for all days, no time-relative badges -->
381
365
  <div class="ag-wday-expanded">
@@ -408,7 +392,7 @@
408
392
 
409
393
  <div class="ag-wslot-cards" class:ag-wslot-cards--multi={slot.events.length > 1}>
410
394
  {#each slot.events as ev (ev.id)}
411
- {@render eventCard(ev, false, day.tier === 'today' ? timeUntilMs(ev.start.getTime()) : undefined)}
395
+ {@render eventCard(ev, false, day.tier === 'today' ? eta(ev.start.getTime()) : undefined)}
412
396
  {/each}
413
397
  </div>
414
398
  </div>
@@ -424,15 +408,22 @@
424
408
  <div
425
409
  class="ag-compact"
426
410
  class:ag-compact--selected={selectedEventId === ev.id}
411
+ class:ag-compact--cancelled={ev.status === 'cancelled'}
412
+ class:ag-compact--tentative={ev.status === 'tentative'}
413
+ class:ag-compact--full={ev.status === 'full'}
414
+ class:ag-compact--limited={ev.status === 'limited'}
427
415
  style:--ev-color={ev.color || 'var(--dt-accent)'}
428
416
  role="button"
429
417
  tabindex="0"
430
- aria-label="{ev.title}, {fmtTime(ev.start)}, {duration(ev)}"
418
+ aria-label="{ev.title}, {fmt(ev.start)}, {duration(ev)}"
431
419
  onclick={() => handleClick(ev)} onpointerenter={() => oneventhover?.(ev)} onkeydown={(e) => handleKeydown(e, ev)}
432
420
  >
433
421
  <span class="ag-compact-dot"></span>
434
- <span class="ag-compact-time">{fmtTime(ev.start)}</span>
422
+ <span class="ag-compact-time">{fmt(ev.start)}</span>
435
423
  <span class="ag-compact-title">{ev.title}</span>
424
+ {#if ev.location}
425
+ <span class="ag-compact-loc">{ev.location}</span>
426
+ {/if}
436
427
  {#if ev.subtitle}
437
428
  <span class="ag-compact-sub">{ev.subtitle}</span>
438
429
  {/if}
@@ -544,6 +535,9 @@
544
535
  display: flex;
545
536
  flex-direction: column;
546
537
  height: 100%;
538
+ width: 100%;
539
+ min-width: 0;
540
+ box-sizing: border-box;
547
541
  color: var(--dt-text, rgba(255, 255, 255, 0.92));
548
542
  font-family: var(--dt-sans, system-ui, sans-serif);
549
543
  }
@@ -555,7 +549,9 @@
555
549
  /* ═══ Body ═══ */
556
550
  .ag-body {
557
551
  flex: 1;
552
+ min-width: 0;
558
553
  overflow-y: auto;
554
+ overflow-x: hidden;
559
555
  scrollbar-width: thin;
560
556
  scrollbar-color: var(--dt-border) transparent;
561
557
  }
@@ -636,6 +632,23 @@
636
632
  border-color: var(--ev-color);
637
633
  background: color-mix(in srgb, var(--ev-color) 20%, var(--dt-surface, #191919));
638
634
  }
635
+ .ag-card--cancelled {
636
+ opacity: 0.5;
637
+ }
638
+ .ag-card--cancelled .ag-card-title {
639
+ text-decoration: line-through;
640
+ }
641
+ .ag-card--tentative {
642
+ opacity: 0.65;
643
+ border-style: dashed;
644
+ }
645
+ .ag-card--full {
646
+ opacity: 0.55;
647
+ }
648
+ .ag-card--limited {
649
+ opacity: 0.65;
650
+ border-style: dashed;
651
+ }
639
652
  .ag-card-body {
640
653
  padding: 7px 10px;
641
654
  display: flex;
@@ -647,11 +660,10 @@
647
660
  .ag-card-title {
648
661
  font-size: 13px;
649
662
  font-weight: 600;
650
- line-height: 1.2;
651
- white-space: nowrap;
652
- overflow: hidden;
653
- text-overflow: ellipsis;
663
+ line-height: 1.3;
664
+ word-break: break-word;
654
665
  flex: 1;
666
+ min-width: 0;
655
667
  }
656
668
  .ag-card-meta {
657
669
  display: flex;
@@ -678,6 +690,11 @@
678
690
  color: var(--dt-text-2, rgba(255, 255, 255, 0.45));
679
691
  line-height: 1;
680
692
  }
693
+ .ag-card-loc {
694
+ font-size: 10px;
695
+ color: var(--dt-text-3, rgba(255, 255, 255, 0.35));
696
+ line-height: 1;
697
+ }
681
698
  .ag-card-tags {
682
699
  display: flex;
683
700
  gap: 4px;
@@ -747,7 +764,7 @@
747
764
  display: flex;
748
765
  justify-content: space-between;
749
766
  align-items: center;
750
- padding: 10px 20px;
767
+ padding: 8px 20px;
751
768
  }
752
769
  .ag-wday-head-left {
753
770
  display: flex;
@@ -783,7 +800,7 @@
783
800
  }
784
801
 
785
802
  .ag-wday-empty {
786
- padding: 2px 20px 10px;
803
+ padding: 2px 20px 6px;
787
804
  font-size: 11px;
788
805
  color: var(--dt-text-3, rgba(255, 255, 255, 0.2));
789
806
  font-style: italic;
@@ -828,14 +845,16 @@
828
845
 
829
846
  /* Compact day events */
830
847
  .ag-wday-compact {
831
- padding: 0 20px 10px;
848
+ padding: 0 20px 6px;
832
849
  }
833
850
  .ag-compact {
834
851
  display: flex;
835
852
  align-items: center;
836
- gap: 8px;
837
- padding: 4px 0;
853
+ gap: 6px;
854
+ padding: 3px 0;
838
855
  cursor: pointer;
856
+ min-width: 0;
857
+ overflow: hidden;
839
858
  }
840
859
  .ag-compact--selected {
841
860
  background: color-mix(in srgb, var(--ev-color) 10%, transparent);
@@ -861,14 +880,16 @@
861
880
  font-size: 11px;
862
881
  font-family: var(--dt-mono, monospace);
863
882
  color: var(--dt-text-2, rgba(255, 255, 255, 0.5));
864
- min-width: 64px;
883
+ min-width: 40px;
865
884
  flex-shrink: 0;
885
+ white-space: nowrap;
866
886
  }
867
887
  .ag-compact-title {
868
888
  font-size: 12px;
869
889
  font-weight: 500;
870
890
  color: var(--dt-text-2, rgba(255, 255, 255, 0.5));
871
891
  flex: 1;
892
+ min-width: 0;
872
893
  white-space: nowrap;
873
894
  overflow: hidden;
874
895
  text-overflow: ellipsis;
@@ -879,11 +900,40 @@
879
900
  font-family: var(--dt-mono, monospace);
880
901
  color: var(--dt-text-3, rgba(255, 255, 255, 0.3));
881
902
  flex-shrink: 0;
903
+ white-space: nowrap;
882
904
  }
883
905
  .ag-compact-sub {
884
906
  font-size: 10px;
885
907
  color: var(--dt-text-3, rgba(255, 255, 255, 0.35));
886
908
  flex-shrink: 0;
909
+ white-space: nowrap;
910
+ overflow: hidden;
911
+ text-overflow: ellipsis;
912
+ max-width: 120px;
913
+ }
914
+ .ag-compact-loc {
915
+ font-size: 9px;
916
+ color: var(--dt-text-3, rgba(255, 255, 255, 0.3));
917
+ flex-shrink: 0;
918
+ white-space: nowrap;
919
+ overflow: hidden;
920
+ text-overflow: ellipsis;
921
+ max-width: 100px;
922
+ }
923
+ .ag-compact--cancelled {
924
+ opacity: 0.5;
925
+ }
926
+ .ag-compact--cancelled .ag-compact-title {
927
+ text-decoration: line-through;
928
+ }
929
+ .ag-compact--tentative {
930
+ opacity: 0.65;
931
+ }
932
+ .ag-compact--full {
933
+ opacity: 0.55;
934
+ }
935
+ .ag-compact--limited {
936
+ opacity: 0.65;
887
937
  }
888
938
  .ag-compact-tag {
889
939
  font: 500 8px / 1 var(--dt-sans, system-ui, sans-serif);
@@ -893,6 +943,9 @@
893
943
  border-radius: 3px;
894
944
  white-space: nowrap;
895
945
  flex-shrink: 0;
946
+ max-width: 80px;
947
+ overflow: hidden;
948
+ text-overflow: ellipsis;
896
949
  }
897
950
  .ag-compact-more {
898
951
  font-size: 11px;
@@ -6,7 +6,8 @@
6
6
  Events positioned absolutely within hour lanes.
7
7
  -->
8
8
  <script lang="ts">
9
- import { getContext, onMount, tick, untrack } from 'svelte';
9
+ import { onMount, tick, untrack } from 'svelte';
10
+ import { useCalendarContext } from '../shared/context.svelte.js';
10
11
  import { createClock } from '../../core/clock.svelte.js';
11
12
  import type { TimelineEvent, BlockedSlot } from '../../core/types.js';
12
13
  import type { DragState } from '../../engine/drag.svelte.js';
@@ -45,20 +46,14 @@
45
46
  }: Props = $props();
46
47
 
47
48
  // ── Context ────────────────────────────────────────
48
- const viewState = getContext<ViewState>('calendar:viewState') as ViewState | undefined;
49
- const loadRangeCtx = getContext<{ current: { start: Date; end: Date } | null; set: (r: { start: Date; end: Date } | null) => void }>('calendar:loadRange') as { current: { start: Date; end: Date } | null; set: (r: { start: Date; end: Date } | null) => void } | undefined;
50
- const blockedSlotsCtx = getContext<{ current: BlockedSlot[] | undefined }>('calendar:blockedSlots') as { current: BlockedSlot[] | undefined } | undefined;
51
- const minDurationCtx = getContext<{ current: number | undefined }>('calendar:minDuration') as { current: number | undefined } | undefined;
52
- const callbacksCtx = getContext<{ oneventhover?: (event: TimelineEvent) => void }>('calendar:callbacks') as { oneventhover?: (event: TimelineEvent) => void } | undefined;
53
- const disabledDatesCtx = getContext<{ current: Date[] | undefined }>('calendar:disabledDates') as { current: Date[] | undefined } | undefined;
54
- const autoHeightCtx = getContext<{ current: boolean }>('calendar:autoHeight') as { current: boolean } | undefined;
55
-
56
- const blockedSlots = $derived(blockedSlotsCtx?.current);
57
- const minDuration = $derived(minDurationCtx?.current);
58
- const autoHeight = $derived(autoHeightCtx?.current ?? false);
59
- const oneventhover = $derived(callbacksCtx?.oneventhover);
60
- const disabledDates = $derived(disabledDatesCtx?.current);
61
- const disabledSet = $derived(new Set(disabledDates?.map(d => sod(d.getTime())) ?? []));
49
+ const ctx = useCalendarContext();
50
+ const viewState = $derived(ctx.viewState);
51
+ const autoHeight = $derived(ctx.autoHeight);
52
+ const oneventhover = $derived(ctx.oneventhover);
53
+ const disabledSet = $derived(ctx.disabledSet);
54
+ const loadRangeCtx = $derived(ctx.loadRange);
55
+ const minDuration = $derived(ctx.minDuration);
56
+ const blockedSlots = $derived(ctx.blockedSlots);
62
57
 
63
58
  const clock = createClock();
64
59
 
@@ -360,6 +355,10 @@
360
355
  class:mb-event--selected={selectedEventId === p.ev.id}
361
356
  class:mb-event--current={p.isCurrent}
362
357
  class:mb-event--next={p.isNext}
358
+ class:mb-event--cancelled={p.ev.status === 'cancelled'}
359
+ class:mb-event--tentative={p.ev.status === 'tentative'}
360
+ class:mb-event--full={p.ev.status === 'full'}
361
+ class:mb-event--limited={p.ev.status === 'limited'}
363
362
  style:top="{p.top}px"
364
363
  style:height="{p.height}px"
365
364
  style:left={p.left}
@@ -367,7 +366,7 @@
367
366
  style:--ev-color={p.ev.color ?? 'var(--dt-accent)'}
368
367
  onclick={(e) => { e.stopPropagation(); oneventclick?.(p.ev); }}
369
368
  onpointerenter={() => oneventhover?.(p.ev)}
370
- aria-label="{p.ev.title}{p.isCurrent ? `, ${L.inProgress}` : ''}{p.isNext ? `, ${L.upNext}` : ''}"
369
+ aria-label="{p.ev.title}{p.ev.status === 'cancelled' ? ' (cancelled)' : ''}{p.ev.status === 'tentative' ? ' (tentative)' : ''}{p.ev.status === 'full' ? ' (full)' : ''}{p.ev.status === 'limited' ? ' (limited)' : ''}{p.isCurrent ? `, ${L.inProgress}` : ''}{p.isNext ? `, ${L.upNext}` : ''}"
371
370
  >
372
371
  <div class="mb-ev-stripe"></div>
373
372
  <div class="mb-ev-body">
@@ -378,6 +377,9 @@
378
377
  {#if p.ev.subtitle && p.height > 48}
379
378
  <span class="mb-ev-sub">{p.ev.subtitle}</span>
380
379
  {/if}
380
+ {#if p.ev.location && p.height > 56}
381
+ <span class="mb-ev-loc">{p.ev.location}</span>
382
+ {/if}
381
383
  {#if p.ev.tags?.length && p.height > 56}
382
384
  <div class="mb-ev-tags">
383
385
  {#each p.ev.tags as tag}
@@ -611,6 +613,23 @@
611
613
  background: color-mix(in srgb, var(--ev-color) 8%, var(--dt-surface, #f9fafb));
612
614
  border: 1px dashed color-mix(in srgb, var(--ev-color) 35%, transparent);
613
615
  }
616
+ .mb-event--cancelled {
617
+ opacity: 0.5;
618
+ }
619
+ .mb-event--cancelled .mb-ev-title {
620
+ text-decoration: line-through;
621
+ }
622
+ .mb-event--tentative {
623
+ opacity: 0.65;
624
+ border: 1px dashed color-mix(in srgb, var(--ev-color) 35%, transparent);
625
+ }
626
+ .mb-event--full {
627
+ opacity: 0.55;
628
+ }
629
+ .mb-event--limited {
630
+ opacity: 0.65;
631
+ border: 1px dashed color-mix(in srgb, var(--ev-color) 35%, transparent);
632
+ }
614
633
 
615
634
  .mb-ev-stripe {
616
635
  width: 4px;
@@ -650,6 +669,14 @@
650
669
  text-overflow: ellipsis;
651
670
  }
652
671
 
672
+ .mb-ev-loc {
673
+ font: 400 10px/1 var(--dt-sans, system-ui, sans-serif);
674
+ color: var(--dt-text-3, rgba(0, 0, 0, 0.3));
675
+ white-space: nowrap;
676
+ overflow: hidden;
677
+ text-overflow: ellipsis;
678
+ }
679
+
653
680
  .mb-ev-tags {
654
681
  display: flex;
655
682
  gap: 4px;
@@ -6,7 +6,7 @@
6
6
  left/right to navigate weeks.
7
7
  -->
8
8
  <script lang="ts">
9
- import { getContext } from 'svelte';
9
+ import { useCalendarContext } from '../shared/context.svelte.js';
10
10
  import { createClock } from '../../core/clock.svelte.js';
11
11
  import type { TimelineEvent } from '../../core/types.js';
12
12
  import type { ViewState } from '../../engine/view-state.svelte.js';
@@ -44,22 +44,16 @@
44
44
  }: Props = $props();
45
45
 
46
46
  // ── Context ────────────────────────────────────────
47
- const viewState = getContext<ViewState>('calendar:viewState') as ViewState | undefined;
48
- const loadRangeCtx = getContext<{ current: { start: Date; end: Date } | null; set: (r: { start: Date; end: Date } | null) => void }>('calendar:loadRange') as { current: { start: Date; end: Date } | null; set: (r: { start: Date; end: Date } | null) => void } | undefined;
49
- const equalDaysCtx = getContext<{ current: boolean }>('calendar:equalDays') as { current: boolean } | undefined;
50
- const showDatesCtx = getContext<{ current: boolean }>('calendar:showDates') as { current: boolean } | undefined;
51
- const hideDaysCtx = getContext<{ current: number[] | undefined }>('calendar:hideDays') as { current: number[] | undefined } | undefined;
52
- const callbacksCtx = getContext<{ oneventhover?: (event: TimelineEvent) => void }>('calendar:callbacks') as { oneventhover?: (event: TimelineEvent) => void } | undefined;
53
- const disabledDatesCtx = getContext<{ current: Date[] | undefined }>('calendar:disabledDates') as { current: Date[] | undefined } | undefined;
54
- const autoHeightCtx = getContext<{ current: boolean }>('calendar:autoHeight') as { current: boolean } | undefined;
55
-
56
- const equalDays = $derived(equalDaysCtx?.current ?? false);
57
- const autoHeight = $derived(autoHeightCtx?.current ?? false);
58
- const showDates = $derived(showDatesCtx?.current ?? true);
59
- const hideDays = $derived(hideDaysCtx?.current);
60
- const oneventhover = $derived(callbacksCtx?.oneventhover);
61
- const disabledDates = $derived(disabledDatesCtx?.current);
62
- const disabledSet = $derived(new Set(disabledDates?.map(d => sod(d.getTime())) ?? []));
47
+ const ctx = useCalendarContext();
48
+ const viewState = $derived(ctx.viewState);
49
+ const equalDays = $derived(ctx.equalDays);
50
+ const showDates = $derived(ctx.showDates);
51
+ const hideDays = $derived(ctx.hideDays);
52
+ const autoHeight = $derived(ctx.autoHeight);
53
+ const oneventhover = $derived(ctx.oneventhover);
54
+ const disabledSet = $derived(ctx.disabledSet);
55
+ const loadRangeCtx = $derived(ctx.loadRange);
56
+ const minDuration = $derived(ctx.minDuration);
63
57
 
64
58
  const clock = createClock();
65
59
 
@@ -236,6 +230,10 @@
236
230
  class:mw-ev--selected={selectedEventId === ev.id}
237
231
  class:mw-ev--allday={isAllDay(ev) || isMultiDay(ev)}
238
232
  class:mw-ev--current={!isAllDay(ev) && !isMultiDay(ev) && ev.start.getTime() <= clock.tick && ev.end.getTime() > clock.tick}
233
+ class:mw-ev--cancelled={ev.status === 'cancelled'}
234
+ class:mw-ev--tentative={ev.status === 'tentative'}
235
+ class:mw-ev--full={ev.status === 'full'}
236
+ class:mw-ev--limited={ev.status === 'limited'}
239
237
  style:--ev-color={ev.color ?? 'var(--dt-accent)'}
240
238
  role="button"
241
239
  tabindex="0"
@@ -402,6 +400,23 @@
402
400
  .mw-ev--allday {
403
401
  background: color-mix(in srgb, var(--ev-color) 14%, var(--dt-surface, #f9fafb));
404
402
  }
403
+ .mw-ev--cancelled {
404
+ opacity: 0.5;
405
+ }
406
+ .mw-ev--cancelled .mw-ev-title {
407
+ text-decoration: line-through;
408
+ }
409
+ .mw-ev--tentative {
410
+ opacity: 0.65;
411
+ border: 1px dashed color-mix(in srgb, var(--ev-color) 35%, transparent);
412
+ }
413
+ .mw-ev--full {
414
+ opacity: 0.55;
415
+ }
416
+ .mw-ev--limited {
417
+ opacity: 0.65;
418
+ border: 1px dashed color-mix(in srgb, var(--ev-color) 35%, transparent);
419
+ }
405
420
 
406
421
  .mw-ev-stripe {
407
422
  width: 3px;