@nomideusz/svelte-calendar 0.6.3 → 0.6.5

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 (47) hide show
  1. package/README.md +192 -10
  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/assets/favicon.svg +9 -1
  12. package/dist/calendar/Calendar.svelte +161 -90
  13. package/dist/calendar/Calendar.svelte.d.ts +12 -0
  14. package/dist/core/index.d.ts +3 -1
  15. package/dist/core/index.js +2 -0
  16. package/dist/core/measure.d.ts +104 -0
  17. package/dist/core/measure.js +195 -0
  18. package/dist/core/time.js +2 -2
  19. package/dist/core/types.d.ts +32 -0
  20. package/dist/engine/event-store.svelte.js +22 -7
  21. package/dist/headless/create-agenda.svelte.d.ts +67 -0
  22. package/dist/headless/create-agenda.svelte.js +139 -0
  23. package/dist/headless/create-calendar.svelte.d.ts +2 -0
  24. package/dist/headless/create-calendar.svelte.js +380 -0
  25. package/dist/headless/index.d.ts +4 -0
  26. package/dist/headless/index.js +2 -0
  27. package/dist/headless/types.d.ts +219 -0
  28. package/dist/headless/types.js +1 -0
  29. package/dist/index.d.ts +6 -4
  30. package/dist/index.js +4 -2
  31. package/dist/primitives/DayHeader.svelte +6 -6
  32. package/dist/primitives/EmptySlot.svelte +3 -3
  33. package/dist/primitives/EventBlock.svelte +99 -14
  34. package/dist/primitives/NowIndicator.svelte +5 -5
  35. package/dist/views/agenda/AgendaDay.svelte +215 -116
  36. package/dist/views/agenda/AgendaWeek.svelte +166 -113
  37. package/dist/views/mobile/MobileDay.svelte +45 -21
  38. package/dist/views/mobile/MobileWeek.svelte +85 -35
  39. package/dist/views/planner/PlannerDay.svelte +116 -60
  40. package/dist/views/planner/PlannerWeek.svelte +228 -166
  41. package/dist/views/shared/context.svelte.d.ts +47 -0
  42. package/dist/views/shared/context.svelte.js +47 -0
  43. package/dist/views/shared/format.d.ts +19 -0
  44. package/dist/views/shared/format.js +46 -0
  45. package/dist/views/shared/index.d.ts +4 -0
  46. package/dist/views/shared/index.js +2 -0
  47. package/package.json +9 -2
@@ -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}
@@ -493,7 +484,7 @@
493
484
  z-index: 20;
494
485
  display: flex;
495
486
  gap: 2px;
496
- background: color-mix(in srgb, var(--dt-surface, #10141c) 85%, transparent);
487
+ background: color-mix(in srgb, var(--dt-surface, var(--dt-bg, #ffffff)) 85%, transparent);
497
488
  backdrop-filter: blur(6px);
498
489
  -webkit-backdrop-filter: blur(6px);
499
490
  border-radius: 8px;
@@ -505,7 +496,7 @@
505
496
  background: transparent;
506
497
  color: var(--dt-text-2, rgba(148, 163, 184, 0.55));
507
498
  cursor: pointer;
508
- font: 600 11px / 1 var(--dt-sans, 'Outfit', system-ui, sans-serif);
499
+ font: 600 11px / 1 var(--dt-sans, system-ui, sans-serif);
509
500
  padding: 6px 12px;
510
501
  border-radius: 6px;
511
502
  letter-spacing: 0.04em;
@@ -532,7 +523,7 @@
532
523
  pointer-events: none;
533
524
  }
534
525
  .ag-nav-pill:focus-visible {
535
- outline: 2px solid color-mix(in srgb, var(--dt-accent, #ef4444) 55%, transparent);
526
+ outline: 2px solid color-mix(in srgb, var(--dt-accent, #2563eb) 55%, transparent);
536
527
  outline-offset: 2px;
537
528
  }
538
529
 
@@ -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,11 @@
555
549
  /* ═══ Body ═══ */
556
550
  .ag-body {
557
551
  flex: 1;
552
+ min-width: 0;
558
553
  overflow-y: auto;
554
+ overflow-x: hidden;
555
+ box-sizing: border-box;
556
+ padding-top: 44px;
559
557
  scrollbar-width: thin;
560
558
  scrollbar-color: var(--dt-border) transparent;
561
559
  }
@@ -583,22 +581,22 @@
583
581
  gap: 4px;
584
582
  padding: 2px 8px;
585
583
  border-radius: 5px;
586
- background: color-mix(in srgb, var(--ev-color) 12%, var(--dt-surface, #10141c));
584
+ background: color-mix(in srgb, var(--ev-color) 12%, var(--dt-surface, var(--dt-bg, #ffffff)));
587
585
  border: 1px solid color-mix(in srgb, var(--ev-color) 18%, transparent);
588
586
  cursor: pointer;
589
587
  transition: background 0.15s, border-color 0.15s;
590
588
  }
591
589
  .ag-allday-chip:hover {
592
- background: color-mix(in srgb, var(--ev-color) 22%, var(--dt-surface, #10141c));
590
+ background: color-mix(in srgb, var(--ev-color) 22%, var(--dt-surface, var(--dt-bg, #ffffff)));
593
591
  border-color: color-mix(in srgb, var(--ev-color) 30%, transparent);
594
592
  }
595
593
  .ag-allday-chip:focus-visible {
596
- outline: 2px solid var(--dt-accent, #ff6b4a);
594
+ outline: 2px solid var(--dt-accent, #2563eb);
597
595
  outline-offset: 2px;
598
596
  }
599
597
  .ag-allday-chip--selected {
600
598
  border-color: var(--ev-color);
601
- background: color-mix(in srgb, var(--ev-color) 18%, var(--dt-surface, #10141c));
599
+ background: color-mix(in srgb, var(--ev-color) 18%, var(--dt-surface, var(--dt-bg, #ffffff)));
602
600
  }
603
601
  .ag-allday-dot {
604
602
  width: 5px;
@@ -618,23 +616,40 @@
618
616
  display: flex;
619
617
  align-items: stretch;
620
618
  border-radius: 6px;
621
- background: color-mix(in srgb, var(--ev-color) 12%, var(--dt-surface, #191919));
619
+ background: color-mix(in srgb, var(--ev-color) 12%, var(--dt-surface, var(--dt-bg, #ffffff)));
622
620
  border: 1px solid color-mix(in srgb, var(--ev-color) 8%, var(--dt-border, rgba(255, 255, 255, 0.06)));
623
621
  overflow: hidden;
624
622
  cursor: pointer;
625
623
  transition: background 150ms, border-color 150ms;
626
624
  }
627
625
  .ag-card:hover {
628
- background: color-mix(in srgb, var(--ev-color) 20%, var(--dt-surface, #191919));
626
+ background: color-mix(in srgb, var(--ev-color) 20%, var(--dt-surface, var(--dt-bg, #ffffff)));
629
627
  border-color: color-mix(in srgb, var(--ev-color) 30%, transparent);
630
628
  }
631
629
  .ag-card:focus-visible {
632
- outline: 2px solid var(--dt-accent, #ff6b4a);
630
+ outline: 2px solid var(--dt-accent, #2563eb);
633
631
  outline-offset: 2px;
634
632
  }
635
633
  .ag-card--selected {
636
634
  border-color: var(--ev-color);
637
- background: color-mix(in srgb, var(--ev-color) 20%, var(--dt-surface, #191919));
635
+ background: color-mix(in srgb, var(--ev-color) 20%, var(--dt-surface, var(--dt-bg, #ffffff)));
636
+ }
637
+ .ag-card--cancelled {
638
+ opacity: 0.5;
639
+ }
640
+ .ag-card--cancelled .ag-card-title {
641
+ text-decoration: line-through;
642
+ }
643
+ .ag-card--tentative {
644
+ opacity: 0.65;
645
+ border-style: dashed;
646
+ }
647
+ .ag-card--full {
648
+ opacity: 0.55;
649
+ }
650
+ .ag-card--limited {
651
+ opacity: 0.65;
652
+ border-style: dashed;
638
653
  }
639
654
  .ag-card-body {
640
655
  padding: 7px 10px;
@@ -647,11 +662,10 @@
647
662
  .ag-card-title {
648
663
  font-size: 13px;
649
664
  font-weight: 600;
650
- line-height: 1.2;
651
- white-space: nowrap;
652
- overflow: hidden;
653
- text-overflow: ellipsis;
665
+ line-height: 1.3;
666
+ word-break: break-word;
654
667
  flex: 1;
668
+ min-width: 0;
655
669
  }
656
670
  .ag-card-meta {
657
671
  display: flex;
@@ -678,6 +692,11 @@
678
692
  color: var(--dt-text-2, rgba(255, 255, 255, 0.45));
679
693
  line-height: 1;
680
694
  }
695
+ .ag-card-loc {
696
+ font-size: 10px;
697
+ color: var(--dt-text-3, rgba(255, 255, 255, 0.35));
698
+ line-height: 1;
699
+ }
681
700
  .ag-card-tags {
682
701
  display: flex;
683
702
  gap: 4px;
@@ -710,7 +729,7 @@
710
729
  border-bottom: 1px solid var(--dt-border, rgba(255, 255, 255, 0.06));
711
730
  }
712
731
  .ag-wday--today {
713
- background: color-mix(in srgb, var(--dt-accent, #ff6b4a) 2%, transparent);
732
+ background: color-mix(in srgb, var(--dt-accent, #2563eb) 2%, transparent);
714
733
  }
715
734
  .ag-wday--tomorrow .ag-card {
716
735
  opacity: 0.82;
@@ -722,8 +741,6 @@
722
741
  padding: 8px 20px;
723
742
  }
724
743
  .ag-wday--disabled {
725
- opacity: 0.35;
726
- pointer-events: none;
727
744
  position: relative;
728
745
  }
729
746
  .ag-wday--disabled::after {
@@ -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;
@@ -759,8 +776,8 @@
759
776
  font-weight: 600;
760
777
  letter-spacing: 0.08em;
761
778
  text-transform: uppercase;
762
- color: var(--dt-accent, #ff6b4a);
763
- background: color-mix(in srgb, var(--dt-accent, #ff6b4a) 12%, transparent);
779
+ color: var(--dt-accent, #2563eb);
780
+ background: color-mix(in srgb, var(--dt-accent, #2563eb) 12%, transparent);
764
781
  padding: 2px 7px;
765
782
  border-radius: 3px;
766
783
  }
@@ -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;
@@ -807,7 +824,7 @@
807
824
  font-weight: 700;
808
825
  letter-spacing: 0.08em;
809
826
  text-transform: uppercase;
810
- color: var(--dt-accent, #ff6b4a);
827
+ color: var(--dt-accent, #2563eb);
811
828
  }
812
829
  .ag-wslot-cards {
813
830
  display: flex;
@@ -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);
@@ -847,7 +866,7 @@
847
866
  color: var(--dt-text);
848
867
  }
849
868
  .ag-compact:focus-visible {
850
- outline: 2px solid var(--dt-accent, #ff6b4a);
869
+ outline: 2px solid var(--dt-accent, #2563eb);
851
870
  outline-offset: 2px;
852
871
  }
853
872
  .ag-compact-dot {
@@ -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,11 +6,10 @@
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 } 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
- import type { DragState } from '../../engine/drag.svelte.js';
13
- import type { ViewState } from '../../engine/view-state.svelte.js';
14
13
  import { DAY_MS, HOUR_MS, sod, isAllDay, isMultiDay, segmentForDay } from '../../core/time.js';
15
14
  import type { DaySegment } from '../../core/time.js';
16
15
  import { fmtH, fmtTime, getLabels } from '../../core/locale.js';
@@ -45,20 +44,14 @@
45
44
  }: Props = $props();
46
45
 
47
46
  // ── 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())) ?? []));
47
+ const ctx = useCalendarContext();
48
+ const viewState = $derived(ctx.viewState);
49
+ const autoHeight = $derived(ctx.autoHeight);
50
+ const oneventhover = $derived(ctx.oneventhover);
51
+ const disabledSet = $derived(ctx.disabledSet);
52
+ const loadRangeCtx = $derived(ctx.loadRange);
53
+ const minDuration = $derived(ctx.minDuration);
54
+ const blockedSlots = $derived(ctx.blockedSlots);
62
55
 
63
56
  const clock = createClock();
64
57
 
@@ -186,8 +179,8 @@
186
179
  ev: info.ev,
187
180
  top: info.top,
188
181
  height: info.height,
189
- left: `calc(${GUTTER_W}px + ${(info.col / info.totalCols) * 100}% * (1 - ${GUTTER_W} / var(--mb-grid-w, 300)))`,
190
- width: `calc((100% - ${GUTTER_W}px) / ${info.totalCols} - 2px)`,
182
+ left: `calc(${GUTTER_W}px + ${(info.col / info.totalCols) * 100}% - ${(GUTTER_W * info.col) / info.totalCols}px)`,
183
+ width: `calc(${100 / info.totalCols}% - ${GUTTER_W / info.totalCols + 2}px)`,
191
184
  isCurrent: info.isCurrent,
192
185
  isNext: info.isNext,
193
186
  col: info.col,
@@ -321,7 +314,6 @@
321
314
  onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') handleGridClick(e as unknown as MouseEvent); }}
322
315
  role="grid"
323
316
  tabindex="-1"
324
- style:--mb-grid-w="300"
325
317
  >
326
318
  <div class="mb-grid-inner" style:height="{gridHeight}px">
327
319
  <!-- Hour lanes -->
@@ -360,6 +352,10 @@
360
352
  class:mb-event--selected={selectedEventId === p.ev.id}
361
353
  class:mb-event--current={p.isCurrent}
362
354
  class:mb-event--next={p.isNext}
355
+ class:mb-event--cancelled={p.ev.status === 'cancelled'}
356
+ class:mb-event--tentative={p.ev.status === 'tentative'}
357
+ class:mb-event--full={p.ev.status === 'full'}
358
+ class:mb-event--limited={p.ev.status === 'limited'}
363
359
  style:top="{p.top}px"
364
360
  style:height="{p.height}px"
365
361
  style:left={p.left}
@@ -367,7 +363,7 @@
367
363
  style:--ev-color={p.ev.color ?? 'var(--dt-accent)'}
368
364
  onclick={(e) => { e.stopPropagation(); oneventclick?.(p.ev); }}
369
365
  onpointerenter={() => oneventhover?.(p.ev)}
370
- aria-label="{p.ev.title}{p.isCurrent ? `, ${L.inProgress}` : ''}{p.isNext ? `, ${L.upNext}` : ''}"
366
+ 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
367
  >
372
368
  <div class="mb-ev-stripe"></div>
373
369
  <div class="mb-ev-body">
@@ -378,6 +374,9 @@
378
374
  {#if p.ev.subtitle && p.height > 48}
379
375
  <span class="mb-ev-sub">{p.ev.subtitle}</span>
380
376
  {/if}
377
+ {#if p.ev.location && p.height > 56}
378
+ <span class="mb-ev-loc">{p.ev.location}</span>
379
+ {/if}
381
380
  {#if p.ev.tags?.length && p.height > 56}
382
381
  <div class="mb-ev-tags">
383
382
  {#each p.ev.tags as tag}
@@ -611,6 +610,23 @@
611
610
  background: color-mix(in srgb, var(--ev-color) 8%, var(--dt-surface, #f9fafb));
612
611
  border: 1px dashed color-mix(in srgb, var(--ev-color) 35%, transparent);
613
612
  }
613
+ .mb-event--cancelled {
614
+ opacity: 0.5;
615
+ }
616
+ .mb-event--cancelled .mb-ev-title {
617
+ text-decoration: line-through;
618
+ }
619
+ .mb-event--tentative {
620
+ opacity: 0.65;
621
+ border: 1px dashed color-mix(in srgb, var(--ev-color) 35%, transparent);
622
+ }
623
+ .mb-event--full {
624
+ opacity: 0.55;
625
+ }
626
+ .mb-event--limited {
627
+ opacity: 0.65;
628
+ border: 1px dashed color-mix(in srgb, var(--ev-color) 35%, transparent);
629
+ }
614
630
 
615
631
  .mb-ev-stripe {
616
632
  width: 4px;
@@ -650,6 +666,14 @@
650
666
  text-overflow: ellipsis;
651
667
  }
652
668
 
669
+ .mb-ev-loc {
670
+ font: 400 10px/1 var(--dt-sans, system-ui, sans-serif);
671
+ color: var(--dt-text-3, rgba(0, 0, 0, 0.3));
672
+ white-space: nowrap;
673
+ overflow: hidden;
674
+ text-overflow: ellipsis;
675
+ }
676
+
653
677
  .mb-ev-tags {
654
678
  display: flex;
655
679
  gap: 4px;