@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
@@ -6,10 +6,9 @@
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
- import type { ViewState } from '../../engine/view-state.svelte.js';
13
12
  import { DAY_MS, sod, isAllDay, isMultiDay } from '../../core/time.js';
14
13
  import { startOfWeek as sowFn } from '../../core/time.js';
15
14
  import { fmtTime as _fmtTime, weekdayShort, getLabels } from '../../core/locale.js';
@@ -44,22 +43,16 @@
44
43
  }: Props = $props();
45
44
 
46
45
  // ── 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())) ?? []));
46
+ const ctx = useCalendarContext();
47
+ const viewState = $derived(ctx.viewState);
48
+ const equalDays = $derived(ctx.equalDays);
49
+ const showDates = $derived(ctx.showDates);
50
+ const hideDays = $derived(ctx.hideDays);
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);
63
56
 
64
57
  const clock = createClock();
65
58
 
@@ -192,6 +185,12 @@
192
185
  viewState.setView(dayView);
193
186
  }
194
187
  }
188
+
189
+ function handleDayKeydown(e: KeyboardEvent, dayMs: number) {
190
+ if (e.key !== 'Enter' && e.key !== ' ') return;
191
+ e.preventDefault();
192
+ handleDayTap(dayMs);
193
+ }
195
194
  </script>
196
195
 
197
196
  <div
@@ -208,15 +207,21 @@
208
207
  <!-- Vertical day list -->
209
208
  <div class="mw-list" role="list">
210
209
  {#each dayCells as cell (cell.ms)}
211
- <button
210
+ <div
212
211
  class="mw-row"
213
212
  class:mw-row--today={cell.isToday}
214
213
  class:mw-row--past={cell.isPast}
215
214
  class:mw-row--weekend={cell.isWeekend}
216
215
  class:mw-row--disabled={cell.isDisabled}
217
- onclick={() => handleDayTap(cell.ms)}
218
- aria-label="{cell.dayName} {cell.dayNum}"
216
+ role="listitem"
219
217
  >
218
+ <button
219
+ class="mw-row-target"
220
+ disabled={cell.isDisabled}
221
+ onclick={() => handleDayTap(cell.ms)}
222
+ onkeydown={(e) => handleDayKeydown(e, cell.ms)}
223
+ aria-label="{cell.dayName} {cell.dayNum}"
224
+ ></button>
220
225
  <!-- Date column -->
221
226
  <div class="mw-date">
222
227
  <span class="mw-day-name" class:mw-day-name--today={cell.isToday}>{cell.dayName}</span>
@@ -231,14 +236,17 @@
231
236
  <span class="mw-empty">{L.noEvents}</span>
232
237
  {:else}
233
238
  {#each cell.events.slice(0, MAX_EVENTS) as ev (ev.id)}
234
- <div
239
+ <button
240
+ type="button"
235
241
  class="mw-ev"
236
242
  class:mw-ev--selected={selectedEventId === ev.id}
237
243
  class:mw-ev--allday={isAllDay(ev) || isMultiDay(ev)}
238
244
  class:mw-ev--current={!isAllDay(ev) && !isMultiDay(ev) && ev.start.getTime() <= clock.tick && ev.end.getTime() > clock.tick}
245
+ class:mw-ev--cancelled={ev.status === 'cancelled'}
246
+ class:mw-ev--tentative={ev.status === 'tentative'}
247
+ class:mw-ev--full={ev.status === 'full'}
248
+ class:mw-ev--limited={ev.status === 'limited'}
239
249
  style:--ev-color={ev.color ?? 'var(--dt-accent)'}
240
- role="button"
241
- tabindex="0"
242
250
  onclick={(e) => { e.stopPropagation(); oneventclick?.(ev); }}
243
251
  onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.stopPropagation(); oneventclick?.(ev); } }}
244
252
  onpointerenter={() => oneventhover?.(ev)}
@@ -252,7 +260,7 @@
252
260
  <span class="mw-ev-time">{fmtTime(ev.start)}</span>
253
261
  {/if}
254
262
  </div>
255
- </div>
263
+ </button>
256
264
  {/each}
257
265
  {#if cell.totalCount > MAX_EVENTS}
258
266
  <span class="mw-ev-more">{L.nMore(cell.totalCount - MAX_EVENTS)}</span>
@@ -262,7 +270,7 @@
262
270
 
263
271
  <!-- Chevron -->
264
272
  <svg class="mw-chevron" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="14" height="14" aria-hidden="true"><path d="M6 3l5 5-5 5"/></svg>
265
- </button>
273
+ </div>
266
274
  {/each}
267
275
  </div>
268
276
  </div>
@@ -296,10 +304,9 @@
296
304
  display: flex;
297
305
  align-items: center;
298
306
  gap: 12px;
307
+ position: relative;
299
308
  padding: 10px 12px;
300
- border: none;
301
309
  background: transparent;
302
- cursor: pointer;
303
310
  transition: background 120ms;
304
311
  text-align: left;
305
312
  width: 100%;
@@ -310,7 +317,7 @@
310
317
  .mw-row:last-child {
311
318
  border-bottom: none;
312
319
  }
313
- .mw-row:active {
320
+ .mw-row:has(.mw-row-target:active) {
314
321
  background: color-mix(in srgb, var(--dt-accent, #2563eb) 6%, transparent);
315
322
  }
316
323
  .mw-row--today {
@@ -320,8 +327,30 @@
320
327
  opacity: 0.5;
321
328
  }
322
329
  .mw-row--disabled {
323
- opacity: 0.3;
324
- pointer-events: none;
330
+ background-image: repeating-linear-gradient(
331
+ 135deg,
332
+ transparent,
333
+ transparent 6px,
334
+ color-mix(in srgb, var(--dt-text, rgba(0, 0, 0, 0.87)) 4%, transparent) 6px,
335
+ color-mix(in srgb, var(--dt-text, rgba(0, 0, 0, 0.87)) 4%, transparent) 12px
336
+ );
337
+ }
338
+ .mw-row-target {
339
+ position: absolute;
340
+ inset: 0;
341
+ z-index: 0;
342
+ border: none;
343
+ background: transparent;
344
+ cursor: pointer;
345
+ padding: 0;
346
+ -webkit-tap-highlight-color: transparent;
347
+ }
348
+ .mw-row-target:disabled {
349
+ cursor: default;
350
+ }
351
+ .mw-row-target:focus-visible {
352
+ outline: 2px solid var(--dt-accent, #2563eb);
353
+ outline-offset: -2px;
325
354
  }
326
355
 
327
356
  /* ─── Date column ────────────────────────────────── */
@@ -332,6 +361,9 @@
332
361
  width: 40px;
333
362
  flex-shrink: 0;
334
363
  gap: 2px;
364
+ position: relative;
365
+ z-index: 1;
366
+ pointer-events: none;
335
367
  }
336
368
 
337
369
  .mw-day-name {
@@ -367,6 +399,8 @@
367
399
  display: flex;
368
400
  flex-direction: column;
369
401
  gap: 4px;
402
+ position: relative;
403
+ z-index: 2;
370
404
  }
371
405
 
372
406
  .mw-empty {
@@ -402,6 +436,23 @@
402
436
  .mw-ev--allday {
403
437
  background: color-mix(in srgb, var(--ev-color) 14%, var(--dt-surface, #f9fafb));
404
438
  }
439
+ .mw-ev--cancelled {
440
+ opacity: 0.5;
441
+ }
442
+ .mw-ev--cancelled .mw-ev-title {
443
+ text-decoration: line-through;
444
+ }
445
+ .mw-ev--tentative {
446
+ opacity: 0.65;
447
+ border: 1px dashed color-mix(in srgb, var(--ev-color) 35%, transparent);
448
+ }
449
+ .mw-ev--full {
450
+ opacity: 0.55;
451
+ }
452
+ .mw-ev--limited {
453
+ opacity: 0.65;
454
+ border: 1px dashed color-mix(in srgb, var(--ev-color) 35%, transparent);
455
+ }
405
456
 
406
457
  .mw-ev-stripe {
407
458
  width: 3px;
@@ -447,13 +498,12 @@
447
498
  .mw-chevron {
448
499
  flex-shrink: 0;
449
500
  color: var(--dt-text-3, rgba(0, 0, 0, 0.2));
501
+ position: relative;
502
+ z-index: 1;
503
+ pointer-events: none;
450
504
  }
451
505
 
452
506
  /* ─── Focus ──────────────────────────────────────── */
453
- .mw-row:focus-visible {
454
- outline: 2px solid var(--dt-accent, #2563eb);
455
- outline-offset: -2px;
456
- }
457
507
  .mw-ev:focus-visible {
458
508
  outline: 2px solid var(--ev-color, var(--dt-accent));
459
509
  outline-offset: 1px;
@@ -6,9 +6,11 @@
6
6
  Events are positioned as horizontal cards with overlap support.
7
7
  -->
8
8
  <script lang="ts">
9
- import { getContext, onMount, tick, untrack, type Snippet } from 'svelte';
9
+ import { onMount, tick, untrack } from 'svelte';
10
+ import { useCalendarContext } from '../shared/context.svelte.js';
10
11
  import { fly } from 'svelte/transition';
11
12
  import { createClock } from '../../core/clock.svelte.js';
13
+ import { createTextMeasure, type ContentFit } from '../../core/measure.js';
12
14
  import type { TimelineEvent, BlockedSlot } from '../../core/types.js';
13
15
  import type { DragState } from '../../engine/drag.svelte.js';
14
16
  import type { ViewState } from '../../engine/view-state.svelte.js';
@@ -58,33 +60,21 @@
58
60
  }: Props = $props();
59
61
 
60
62
  // ── Context (available when inside Calendar) ──
61
- const drag = getContext<DragState>('calendar:drag') as DragState | undefined;
62
- const commitDragCtx = getContext<() => void>('calendar:commitDrag') as (() => void) | undefined;
63
- const snapIntervalCtx = getContext<{ current: number }>('calendar:snapInterval');
64
- const viewState = getContext<ViewState>('calendar:viewState') as ViewState | undefined;
65
- 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;
66
-
63
+ const ctx = useCalendarContext();
67
64
  const clock = createClock();
68
- const showNavCtx = getContext<{ current: boolean }>('calendar:showNavigation') as { current: boolean } | undefined;
69
- const showDatesCtx = getContext<{ current: boolean }>('calendar:showDates') as { current: boolean } | undefined;
70
- const showNav = $derived(showNavCtx?.current ?? true);
71
- const showDates = $derived(showDatesCtx?.current ?? true);
72
-
73
- // ── New feature contexts ──
74
- const blockedSlotsCtx = getContext<{ current: BlockedSlot[] | undefined }>('calendar:blockedSlots') as { current: BlockedSlot[] | undefined } | undefined;
75
- 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;
76
- const minDurationCtx = getContext<{ current: number | undefined }>('calendar:minDuration') as { current: number | undefined } | undefined;
77
- const callbacksCtx = getContext<{ oneventhover?: (event: TimelineEvent) => void }>('calendar:callbacks') as { oneventhover?: (event: TimelineEvent) => void } | undefined;
78
- const disabledDatesCtx = getContext<{ current: Date[] | undefined }>('calendar:disabledDates') as { current: Date[] | undefined } | undefined;
79
- const autoHeightCtx = getContext<{ current: boolean }>('calendar:autoHeight') as { current: boolean } | undefined;
80
-
81
- const blockedSlots = $derived(blockedSlotsCtx?.current);
82
- const dayHeaderSnippet = $derived(dayHeaderSnippetCtx?.current);
83
- const minDuration = $derived(minDurationCtx?.current);
84
- const autoHeight = $derived(autoHeightCtx?.current ?? false);
85
- const oneventhover = $derived(callbacksCtx?.oneventhover);
86
- const disabledDates = $derived(disabledDatesCtx?.current);
87
- const disabledSet = $derived(new Set(disabledDates?.map(d => sod(d.getTime())) ?? []));
65
+ const drag = $derived(ctx.drag);
66
+ const commitDragCtx = $derived(ctx.commitDrag);
67
+ const viewState = $derived(ctx.viewState);
68
+ const loadRangeCtx = $derived(ctx.loadRange);
69
+ const showNav = $derived(ctx.showNav);
70
+ const showDates = $derived(ctx.showDates);
71
+ const blockedSlots = $derived(ctx.blockedSlots);
72
+ const dayHeaderSnippet = $derived(ctx.dayHeaderSnippet);
73
+ const minDuration = $derived(ctx.minDuration);
74
+ const autoHeight = $derived(ctx.autoHeight);
75
+ const oneventhover = $derived(ctx.oneventhover);
76
+ const disabledSet = $derived(ctx.disabledSet);
77
+ const SNAP_MS = $derived(ctx.snapInterval * 60_000);
88
78
 
89
79
  // ─── State ──────────────────────────────────────────
90
80
  let following = $state(true);
@@ -113,7 +103,6 @@
113
103
  const startHour = $derived(visibleHours?.[0] ?? 0);
114
104
  const endHour = $derived(visibleHours?.[1] ?? 24);
115
105
  const hourCount = $derived(Math.max(1, endHour - startHour));
116
- const SNAP_MS = $derived((snapIntervalCtx?.current ?? 15) * 60_000);
117
106
  const DAY_GAP = 2;
118
107
 
119
108
  const dateLabel = $derived(
@@ -207,6 +196,16 @@
207
196
  const EVENT_GAP = 5;
208
197
  const MIN_EVENT_H = 32;
209
198
 
199
+ // Text measure for smart content fitting
200
+ const measure = createTextMeasure({
201
+ titleFont: '600 13px system-ui, sans-serif',
202
+ secondaryFont: '400 10px system-ui, sans-serif',
203
+ tagFont: '500 8px system-ui, sans-serif',
204
+ titleLineHeight: 16,
205
+ secondaryLineHeight: 13,
206
+ contentGap: 6,
207
+ });
208
+
210
209
  interface PositionedEvent {
211
210
  ev: TimelineEvent;
212
211
  x: number;
@@ -218,6 +217,7 @@
218
217
  isCurrent: boolean;
219
218
  isNext: boolean;
220
219
  isDragged: boolean;
220
+ fit: ContentFit;
221
221
  }
222
222
 
223
223
  const positionedEvents = $derived.by(() => {
@@ -297,20 +297,39 @@
297
297
  const result: PositionedEvent[] = infos.map(({ startMs: _s, endMs: _e, ...info }) => {
298
298
  const laneH = Math.max(MIN_EVENT_H, availH / info.groupMaxRow - EVENT_GAP);
299
299
  const topPx = contentTop + info.row * (availH / info.groupMaxRow);
300
- return { ...info, topPx, heightPx: laneH, isNext: info.isNext };
300
+ const fit = measure.fitContent({
301
+ title: info.ev.title,
302
+ subtitle: info.ev.subtitle,
303
+ location: info.ev.location,
304
+ time: `${fmtTime(info.ev.start, locale)} – ${fmtTime(info.ev.end, locale)}`,
305
+ tags: info.ev.tags,
306
+ maxWidth: info.width - 16,
307
+ maxHeight: laneH - 16,
308
+ });
309
+ return { ...info, topPx, heightPx: laneH, isNext: info.isNext, fit };
301
310
  });
302
311
 
303
312
  if (draggedEv && dragP) {
304
313
  const x = timeToPx(dragP.start.getTime());
305
314
  const xEnd = timeToPx(dragP.end.getTime());
315
+ const dragH = Math.max(MIN_EVENT_H, availH - EVENT_GAP);
316
+ const dragW = Math.max(xEnd - x, 28);
306
317
  result.push({
307
- ev: draggedEv, x, width: Math.max(xEnd - x, 28),
318
+ ev: draggedEv, x, width: dragW,
308
319
  row: 0, groupMaxRow: 1,
309
320
  topPx: contentTop,
310
- heightPx: Math.max(MIN_EVENT_H, availH - EVENT_GAP),
321
+ heightPx: dragH,
311
322
  isCurrent: draggedEv.start.getTime() <= now && draggedEv.end.getTime() > now,
312
323
  isNext: false,
313
324
  isDragged: true,
325
+ fit: measure.fitContent({
326
+ title: draggedEv.title,
327
+ subtitle: draggedEv.subtitle,
328
+ location: draggedEv.location,
329
+ tags: draggedEv.tags,
330
+ maxWidth: dragW - 16,
331
+ maxHeight: dragH - 16,
332
+ }),
314
333
  });
315
334
  }
316
335
 
@@ -493,7 +512,7 @@
493
512
  let evDragEvent: TimelineEvent | null = null;
494
513
 
495
514
  function onEventPointerDown(e: PointerEvent, ev: TimelineEvent) {
496
- if (e.button !== 0 || !drag || readOnly) return;
515
+ if (e.button !== 0 || !drag || readOnly || ev.data?.readOnly) return;
497
516
  e.stopPropagation();
498
517
  evDragStartX = e.clientX;
499
518
  evDragOriginPx = timeToPx(ev.start.getTime());
@@ -623,6 +642,11 @@
623
642
  class:fs-event--current={p.isCurrent}
624
643
  class:fs-event--next={p.isNext}
625
644
  class:fs-event--dragging={p.isDragged}
645
+ class:fs-event--readonly={p.ev.data?.readOnly}
646
+ class:fs-event--cancelled={p.ev.status === 'cancelled'}
647
+ class:fs-event--tentative={p.ev.status === 'tentative'}
648
+ class:fs-event--full={p.ev.status === 'full'}
649
+ class:fs-event--limited={p.ev.status === 'limited'}
626
650
  style:left="{p.x}px"
627
651
  style:width="{p.width}px"
628
652
  style:top="{p.topPx}px"
@@ -630,7 +654,7 @@
630
654
  style:--ev-color={p.ev.color ?? 'var(--dt-accent)'}
631
655
  role="button"
632
656
  tabindex="0"
633
- aria-label="{p.ev.title}{p.isCurrent ? ` (${L.inProgress})` : ''}{p.isNext ? ` (${L.upNext})` : ''}"
657
+ 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})` : ''}"
634
658
  onpointerdown={(e) => onEventPointerDown(e, p.ev)}
635
659
  onpointerenter={() => oneventhover?.(p.ev)}
636
660
  onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); oneventclick?.(p.ev); } }}
@@ -641,14 +665,17 @@
641
665
  {:else if p.isNext}
642
666
  <span class="fs-ev-next-badge" aria-hidden="true">{L.upNext}</span>
643
667
  {/if}
644
- {#if p.heightPx > 64}
668
+ {#if p.fit.time}
645
669
  <span class="fs-ev-time">{fmtTime(p.ev.start, locale)} – {fmtTime(p.ev.end, locale)}</span>
646
670
  {/if}
647
671
  <span class="fs-ev-title">{p.ev.title}</span>
648
- {#if p.ev.subtitle && p.heightPx > 48}
672
+ {#if p.ev.subtitle && p.fit.subtitle}
649
673
  <span class="fs-ev-sub">{p.ev.subtitle}</span>
650
674
  {/if}
651
- {#if p.ev.tags?.length && p.heightPx > 72}
675
+ {#if p.ev.location && p.fit.location}
676
+ <span class="fs-ev-loc">{p.ev.location}</span>
677
+ {/if}
678
+ {#if p.ev.tags?.length && p.fit.tags}
652
679
  <span class="fs-ev-tags">
653
680
  {#each p.ev.tags as tag}
654
681
  <span class="fs-ev-tag">{tag}</span>
@@ -866,7 +893,7 @@
866
893
  bottom: 0;
867
894
  left: 0;
868
895
  width: 2px;
869
- background: var(--dt-accent, #ef4444);
896
+ background: var(--dt-accent, #2563eb);
870
897
  box-shadow: 0 0 8px var(--dt-glow, rgba(239, 68, 68, 0.35));
871
898
  }
872
899
  .fs-now-tag {
@@ -874,8 +901,8 @@
874
901
  top: 8px;
875
902
  left: 8px;
876
903
  font: 700 11px/1 var(--dt-mono, ui-monospace, monospace);
877
- color: var(--dt-accent, #ef4444);
878
- background: color-mix(in srgb, var(--dt-bg, #0b0e14) 92%, var(--dt-accent, #ef4444));
904
+ color: var(--dt-accent, #2563eb);
905
+ background: color-mix(in srgb, var(--dt-bg, #ffffff) 92%, var(--dt-accent, #2563eb));
879
906
  border: 1px solid var(--dt-accent-dim, rgba(239, 68, 68, 0.18));
880
907
  padding: 3px 6px;
881
908
  border-radius: 4px;
@@ -899,7 +926,7 @@
899
926
  letter-spacing: 0.04em;
900
927
  text-transform: uppercase;
901
928
  color: var(--dt-text, rgba(226, 232, 240, 0.85));
902
- background: color-mix(in srgb, var(--dt-surface, #10141c) 85%, transparent);
929
+ background: color-mix(in srgb, var(--dt-surface, var(--dt-bg, #ffffff)) 85%, transparent);
903
930
  backdrop-filter: blur(6px);
904
931
  -webkit-backdrop-filter: blur(6px);
905
932
  padding: 8px 16px;
@@ -917,7 +944,7 @@
917
944
  z-index: 20;
918
945
  display: flex;
919
946
  gap: 2px;
920
- background: color-mix(in srgb, var(--dt-surface, #10141c) 85%, transparent);
947
+ background: color-mix(in srgb, var(--dt-surface, var(--dt-bg, #ffffff)) 85%, transparent);
921
948
  backdrop-filter: blur(6px);
922
949
  -webkit-backdrop-filter: blur(6px);
923
950
  border-radius: 8px;
@@ -929,7 +956,7 @@
929
956
  background: transparent;
930
957
  color: var(--dt-text-2, rgba(148, 163, 184, 0.55));
931
958
  cursor: pointer;
932
- font: 600 11px / 1 var(--dt-sans, 'Outfit', system-ui, sans-serif);
959
+ font: 600 11px / 1 var(--dt-sans, system-ui, sans-serif);
933
960
  padding: 6px 12px;
934
961
  border-radius: 6px;
935
962
  letter-spacing: 0.04em;
@@ -956,7 +983,7 @@
956
983
  pointer-events: none;
957
984
  }
958
985
  .fs-nav-pill:focus-visible {
959
- outline: 2px solid color-mix(in srgb, var(--dt-accent, #ef4444) 55%, transparent);
986
+ outline: 2px solid color-mix(in srgb, var(--dt-accent, #2563eb) 55%, transparent);
960
987
  outline-offset: 2px;
961
988
  }
962
989
 
@@ -981,7 +1008,7 @@
981
1008
  gap: 4px;
982
1009
  padding: 2px 8px;
983
1010
  border-radius: 4px;
984
- background: color-mix(in srgb, var(--ev-color) 18%, var(--dt-surface, #10141c));
1011
+ background: color-mix(in srgb, var(--ev-color) 18%, var(--dt-surface, var(--dt-bg, #ffffff)));
985
1012
  border-left: 3px solid var(--ev-color);
986
1013
  white-space: nowrap;
987
1014
  flex-shrink: 0;
@@ -989,14 +1016,14 @@
989
1016
  transition: background 0.15s;
990
1017
  }
991
1018
  .fs-ad:hover {
992
- background: color-mix(in srgb, var(--ev-color) 28%, var(--dt-surface, #10141c));
1019
+ background: color-mix(in srgb, var(--ev-color) 28%, var(--dt-surface, var(--dt-bg, #ffffff)));
993
1020
  }
994
1021
  .fs-ad:focus-visible {
995
- outline: 2px solid color-mix(in srgb, var(--dt-accent, #ef4444) 55%, transparent);
1022
+ outline: 2px solid color-mix(in srgb, var(--dt-accent, #2563eb) 55%, transparent);
996
1023
  outline-offset: 2px;
997
1024
  }
998
1025
  .fs-ad--selected {
999
- background: color-mix(in srgb, var(--ev-color) 30%, var(--dt-surface, #10141c));
1026
+ background: color-mix(in srgb, var(--ev-color) 30%, var(--dt-surface, var(--dt-bg, #ffffff)));
1000
1027
  border-left-width: 4px;
1001
1028
  }
1002
1029
 
@@ -1029,7 +1056,7 @@
1029
1056
  z-index: 6;
1030
1057
  border-radius: 6px;
1031
1058
  cursor: pointer;
1032
- background: color-mix(in srgb, var(--ev-color) 15%, var(--dt-surface, #10141c));
1059
+ background: color-mix(in srgb, var(--ev-color) 15%, var(--dt-surface, var(--dt-bg, #ffffff)));
1033
1060
  overflow: hidden;
1034
1061
  display: flex;
1035
1062
  align-items: center;
@@ -1037,18 +1064,18 @@
1037
1064
  transition: box-shadow 120ms, background 120ms;
1038
1065
  }
1039
1066
  .fs-event:hover {
1040
- background: color-mix(in srgb, var(--ev-color) 25%, var(--dt-surface, #10141c));
1067
+ background: color-mix(in srgb, var(--ev-color) 25%, var(--dt-surface, var(--dt-bg, #ffffff)));
1041
1068
  box-shadow: 0 2px 12px color-mix(in srgb, var(--ev-color) 25%, transparent);
1042
1069
  }
1043
1070
  .fs-event--selected {
1044
1071
  box-shadow: 0 0 0 2px var(--ev-color), 0 2px 14px color-mix(in srgb, var(--ev-color) 35%, transparent);
1045
1072
  }
1046
1073
  .fs-event--current {
1047
- background: color-mix(in srgb, var(--ev-color) 22%, var(--dt-surface, #10141c));
1074
+ background: color-mix(in srgb, var(--ev-color) 22%, var(--dt-surface, var(--dt-bg, #ffffff)));
1048
1075
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--ev-color) 20%, transparent);
1049
1076
  }
1050
1077
  .fs-event--next {
1051
- background: color-mix(in srgb, var(--ev-color) 12%, var(--dt-surface, #10141c));
1078
+ background: color-mix(in srgb, var(--ev-color) 12%, var(--dt-surface, var(--dt-bg, #ffffff)));
1052
1079
  border: 1px dashed color-mix(in srgb, var(--ev-color) 35%, transparent);
1053
1080
  }
1054
1081
  .fs-event--dragging {
@@ -1058,17 +1085,39 @@
1058
1085
  cursor: grabbing;
1059
1086
  transition: none;
1060
1087
  }
1088
+ .fs-event--cancelled {
1089
+ opacity: 0.5;
1090
+ }
1091
+ .fs-event--cancelled .fs-ev-title {
1092
+ text-decoration: line-through;
1093
+ }
1094
+ .fs-event--tentative {
1095
+ opacity: 0.65;
1096
+ border: 1px dashed color-mix(in srgb, var(--ev-color) 40%, transparent);
1097
+ }
1098
+ .fs-event--full {
1099
+ opacity: 0.55;
1100
+ }
1101
+ .fs-event--limited {
1102
+ opacity: 0.65;
1103
+ border: 1px dashed color-mix(in srgb, var(--ev-color) 40%, transparent);
1104
+ }
1105
+ .fs-event--readonly {
1106
+ cursor: default;
1107
+ }
1061
1108
 
1062
- /* Event inner (vertical text, bottom-to-top reading) */
1109
+ /* Event inner */
1063
1110
  .fs-ev-inner {
1064
- writing-mode: vertical-rl;
1065
- transform: rotate(180deg);
1066
1111
  display: flex;
1067
- align-items: center;
1068
- gap: 6px;
1069
- max-height: 100%;
1112
+ flex-direction: column;
1113
+ align-items: flex-start;
1114
+ justify-content: center;
1115
+ gap: 3px;
1116
+ width: 100%;
1117
+ height: 100%;
1070
1118
  overflow: hidden;
1071
- padding: 8px 4px;
1119
+ box-sizing: border-box;
1120
+ padding: 6px 8px;
1072
1121
  }
1073
1122
  .fs-ev-live {
1074
1123
  flex-shrink: 0;
@@ -1109,7 +1158,14 @@
1109
1158
  overflow: hidden;
1110
1159
  text-overflow: ellipsis;
1111
1160
  }
1112
- .fs-ev-tags { display: flex; gap: 4px; }
1161
+ .fs-ev-loc {
1162
+ font: 400 10px/1 var(--dt-sans, system-ui, sans-serif);
1163
+ color: var(--dt-text-3, rgba(148, 163, 184, 0.5));
1164
+ white-space: nowrap;
1165
+ overflow: hidden;
1166
+ text-overflow: ellipsis;
1167
+ }
1168
+ .fs-ev-tags { display: flex; gap: 4px; flex-wrap: wrap; }
1113
1169
  .fs-ev-tag {
1114
1170
  font: 500 8px/1 var(--dt-sans, system-ui, sans-serif);
1115
1171
  color: var(--ev-color, var(--dt-accent));
@@ -1121,7 +1177,7 @@
1121
1177
 
1122
1178
  /* ─── Focus-visible ──────────────────────────────── */
1123
1179
  .fs-event:focus-visible {
1124
- outline: 2px solid var(--dt-accent, #ef4444);
1180
+ outline: 2px solid var(--dt-accent, #2563eb);
1125
1181
  outline-offset: 2px;
1126
1182
  }
1127
1183
  </style>