@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.
Files changed (50) 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 +11 -0
  9. package/dist/adapters/recurring.js +9 -13
  10. package/dist/adapters/types.d.ts +11 -2
  11. package/dist/calendar/Calendar.svelte +847 -767
  12. package/dist/calendar/Calendar.svelte.d.ts +14 -2
  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/DayHeader.svelte +103 -103
  31. package/dist/primitives/EmptySlot.svelte +105 -105
  32. package/dist/primitives/EventBlock.svelte +397 -312
  33. package/dist/primitives/NowIndicator.svelte +178 -178
  34. package/dist/primitives/TimeGutter.svelte +104 -104
  35. package/dist/theme/presets.js +36 -36
  36. package/dist/views/agenda/AgendaDay.svelte +1167 -1055
  37. package/dist/views/agenda/AgendaWeek.svelte +987 -924
  38. package/dist/views/mobile/Mobile.svelte +17 -17
  39. package/dist/views/mobile/MobileDay.svelte +729 -665
  40. package/dist/views/mobile/MobileWeek.svelte +476 -456
  41. package/dist/views/planner/PlannerDay.svelte +1184 -1091
  42. package/dist/views/planner/PlannerWeek.svelte +924 -945
  43. package/dist/views/shared/context.svelte.d.ts +47 -0
  44. package/dist/views/shared/context.svelte.js +47 -0
  45. package/dist/views/shared/format.d.ts +19 -0
  46. package/dist/views/shared/format.js +46 -0
  47. package/dist/views/shared/index.d.ts +4 -0
  48. package/dist/views/shared/index.js +2 -0
  49. package/dist/widget/CalendarWidget.svelte +142 -142
  50. package/package.json +76 -71
@@ -1,924 +1,987 @@
1
- <script lang="ts">
2
- /**
3
- * AgendaWeek — rolling N-day agenda view.
4
- *
5
- * "The Week Ahead":
6
- * Today + tomorrow expanded with time slots/countdowns.
7
- * Future days compact (dot + time + title).
8
- * Past days dimmed.
9
- *
10
- * Answers: "What's coming up and when do I need to be ready?"
11
- */
12
- import { getContext, type Snippet } from 'svelte';
13
- import { createClock } from '../../core/clock.svelte.js';
14
- import type { TimelineEvent } from '../../core/types.js';
15
- 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';
18
-
19
- const L = $derived(getLabels());
20
-
21
- interface Props {
22
- mondayStart?: boolean;
23
- locale?: string;
24
- height?: number;
25
- events?: TimelineEvent[];
26
- style?: string;
27
- focusDate?: Date;
28
- oneventclick?: (event: TimelineEvent) => void;
29
- selectedEventId?: string | null;
30
- [key: string]: unknown;
31
- }
32
-
33
- let {
34
- mondayStart = true,
35
- locale,
36
- height = 520,
37
- events = [],
38
- style = '',
39
- focusDate,
40
- oneventclick,
41
- selectedEventId = null,
42
- }: Props = $props();
43
-
44
- 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 showNav = $derived(showNavCtx?.current ?? true);
52
- const equalDays = $derived(equalDaysCtx?.current ?? false);
53
- const showDates = $derived(showDatesCtx?.current ?? true);
54
- const hideDays = $derived(hideDaysCtx?.current);
55
- const isMobile = $derived(mobileCtx?.current ?? false);
56
-
57
- // ── Swipe navigation (mobile) ──────────────────────
58
- let swipeStartX = 0;
59
- let swipeStartY = 0;
60
- const SWIPE_THRESHOLD = 50;
61
-
62
- function onPointerDown(e: PointerEvent) {
63
- if (!isMobile) return;
64
- swipeStartX = e.clientX;
65
- swipeStartY = e.clientY;
66
- }
67
-
68
- function onPointerUp(e: PointerEvent) {
69
- if (!isMobile) return;
70
- const dx = e.clientX - swipeStartX;
71
- const dy = e.clientY - swipeStartY;
72
- if (Math.abs(dx) > SWIPE_THRESHOLD && Math.abs(dx) > Math.abs(dy) * 1.4) {
73
- if (dx > 0) viewState?.prev();
74
- else viewState?.next();
75
- }
76
- }
77
-
78
- // ── New feature contexts ──
79
- 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;
80
- const callbacksCtx = getContext<{ oneventhover?: (event: TimelineEvent) => void }>('calendar:callbacks') as { oneventhover?: (event: TimelineEvent) => void } | undefined;
81
- const disabledDatesCtx = getContext<{ current: Date[] | undefined }>('calendar:disabledDates') as { current: Date[] | undefined } | undefined;
82
-
83
- const dayHeaderSnippet = $derived(dayHeaderSnippetCtx?.current);
84
- const oneventhover = $derived(callbacksCtx?.oneventhover);
85
- const disabledDates = $derived(disabledDatesCtx?.current);
86
- const disabledSet = $derived(new Set(disabledDates?.map(d => sod(d.getTime())) ?? []));
87
-
88
- // ── Format helpers ──────────────────────────────────
89
- function fmtTime(d: Date): string {
90
- return _fmtTime(d, locale);
91
- }
92
-
93
- function duration(ev: TimelineEvent): string {
94
- return fmtDuration(ev.start, ev.end);
95
- }
96
-
97
- function timeUntilMs(ms: number): string {
98
- const diff = ms - clock.tick;
99
- if (diff <= 0) return L.now;
100
- const tMins = Math.floor(diff / 60000);
101
- if (tMins < 60) return `in ${tMins}m`;
102
- const hrs = Math.floor(tMins / 60);
103
- const rm = tMins % 60;
104
- if (hrs < 24) return rm > 0 ? `in ${hrs}h ${rm}m` : `in ${hrs}h`;
105
- const days = Math.floor(hrs / 24);
106
- return `in ${days}d`;
107
- }
108
-
109
- function progress(ev: TimelineEvent): number {
110
- const s = ev.start.getTime();
111
- const e = ev.end.getTime();
112
- return Math.min(1, Math.max(0, (clock.tick - s) / (e - s)));
113
- }
114
-
115
- // ── Overlap grouping ────────────────────────────────
116
- interface TimeSlot {
117
- startMs: number;
118
- endMs: number;
119
- events: TimelineEvent[];
120
- }
121
-
122
- function groupIntoSlots(evts: TimelineEvent[]): TimeSlot[] {
123
- const sorted = [...evts].sort((a, b) => a.start.getTime() - b.start.getTime());
124
- const slots: TimeSlot[] = [];
125
- for (const ev of sorted) {
126
- const last = slots[slots.length - 1];
127
- if (last && ev.start.getTime() < last.endMs) {
128
- last.events.push(ev);
129
- last.endMs = Math.max(last.endMs, ev.end.getTime());
130
- } else {
131
- slots.push({
132
- startMs: ev.start.getTime(),
133
- endMs: ev.end.getTime(),
134
- events: [ev],
135
- });
136
- }
137
- }
138
- return slots;
139
- }
140
-
141
- // ── Event handlers ──────────────────────────────────
142
- function handleClick(ev: TimelineEvent): void {
143
- oneventclick?.(ev);
144
- }
145
-
146
- function handleKeydown(e: KeyboardEvent, ev: TimelineEvent): void {
147
- if (e.key === 'Enter' || e.key === ' ') {
148
- e.preventDefault();
149
- oneventclick?.(ev);
150
- }
151
- }
152
-
153
- // ── Week derivations ────────────────────────────────
154
- type DayTier = 'today' | 'tomorrow' | 'upcoming' | 'past';
155
-
156
- interface DayGroup {
157
- ms: number;
158
- dayName: string;
159
- dateLabel: string;
160
- tier: DayTier;
161
- events: TimelineEvent[];
162
- allDayEvents: TimelineEvent[];
163
- timedEvents: TimelineEvent[];
164
- pastEvents: TimelineEvent[];
165
- currentEvents: TimelineEvent[];
166
- upcomingEvents: TimelineEvent[];
167
- totalHours: number;
168
- }
169
-
170
- const weekStartMs = $derived(
171
- focusDate
172
- ? (viewState?.dayCount === 7
173
- ? startOfWeek(sod(focusDate.getTime()), mondayStart)
174
- : sod(focusDate.getTime()))
175
- : (viewState?.dayCount === 7
176
- ? startOfWeek(clock.today, mondayStart)
177
- : clock.today),
178
- );
179
-
180
- const customDays = $derived(viewState?.dayCount ?? 7);
181
-
182
- const isThisWeek = $derived(
183
- customDays === 7
184
- ? weekStartMs === startOfWeek(clock.today, mondayStart)
185
- : clock.today >= weekStartMs && clock.today < weekStartMs + customDays * DAY_MS,
186
- );
187
-
188
- const weekDays = $derived.by((): DayGroup[] => {
189
- const now = clock.tick;
190
- const todayMs = clock.today;
191
- const tomorrowMs = todayMs + DAY_MS;
192
- const out: DayGroup[] = [];
193
- for (let i = 0; i < customDays; i++) {
194
- const ms = weekStartMs + i * DAY_MS;
195
- const dEnd = ms + DAY_MS;
196
- const dayEvts = events
197
- .filter((ev) => ev.start.getTime() < dEnd && ev.end.getTime() > ms)
198
- .sort((a, b) => a.start.getTime() - b.start.getTime());
199
- const allDayEvts = dayEvts.filter((ev) => isAllDay(ev) || isMultiDay(ev));
200
- const timedEvts = dayEvts.filter((ev) => !isAllDay(ev) && !isMultiDay(ev));
201
- const totalMinutes = timedEvts.reduce((sum, ev) => {
202
- const s = Math.max(ev.start.getTime(), ms);
203
- const e = Math.min(ev.end.getTime(), dEnd);
204
- return sum + (e - s) / 60000;
205
- }, 0);
206
- const pastEvents: TimelineEvent[] = [];
207
- const currentEvents: TimelineEvent[] = [];
208
- const upcomingEvents: TimelineEvent[] = [];
209
- for (const ev of timedEvts) {
210
- if (ev.end.getTime() <= now) pastEvents.push(ev);
211
- else if (ev.start.getTime() <= now && ev.end.getTime() > now) currentEvents.push(ev);
212
- else upcomingEvents.push(ev);
213
- }
214
- let tier: DayTier;
215
- if (equalDays) {
216
- tier = 'upcoming';
217
- } else if (ms === todayMs) {
218
- tier = 'today';
219
- } else if (ms === tomorrowMs) {
220
- tier = 'tomorrow';
221
- } else if (ms < todayMs) {
222
- tier = 'past';
223
- } else {
224
- tier = 'upcoming';
225
- }
226
-
227
- out.push({
228
- ms,
229
- dayName: weekdayLong(ms, locale),
230
- dateLabel: `${monthLong(ms, locale)} ${dayNum(ms)}`,
231
- tier,
232
- events: dayEvts,
233
- allDayEvents: allDayEvts,
234
- timedEvents: timedEvts,
235
- pastEvents,
236
- currentEvents,
237
- upcomingEvents,
238
- totalHours: Math.round((totalMinutes / 60) * 10) / 10,
239
- });
240
- }
241
-
242
- // Filter hidden days if hideDays is set
243
- if (hideDays?.length) {
244
- return out.filter((d) => {
245
- const jsDay = new Date(d.ms).getDay();
246
- const iso = jsDay === 0 ? 7 : jsDay;
247
- return !hideDays.includes(iso);
248
- });
249
- }
250
-
251
- return out;
252
- });
253
- </script>
254
-
255
- <!-- ═══ Shared event card snippet ═══ -->
256
- {#snippet eventCard(ev: TimelineEvent, isNow: boolean, eta?: string)}
257
- <div
258
- class="ag-card"
259
- class:ag-card--selected={selectedEventId === ev.id}
260
- style:--ev-color={ev.color || 'var(--dt-accent)'}
261
- role="button"
262
- tabindex="0"
263
- aria-label="{ev.title}, {fmtTime(ev.start)} to {fmtTime(ev.end)}, {duration(ev)}"
264
- onclick={() => handleClick(ev)}
265
- onpointerenter={() => oneventhover?.(ev)}
266
- onkeydown={(e) => handleKeydown(e, ev)}
267
- >
268
- <div class="ag-card-body">
269
- <span class="ag-card-title">{ev.title}</span>
270
- {#if ev.subtitle}
271
- <span class="ag-card-sub">{ev.subtitle}</span>
272
- {/if}
273
- <span class="ag-card-meta">
274
- {#if isNow}
275
- {L.until} {fmtTime(ev.end)}
276
- {:else}
277
- {fmtTime(ev.start)} – {fmtTime(ev.end)}
278
- {/if}
279
- <span class="ag-card-dur">{duration(ev)}</span>
280
- {#if eta}
281
- <span class="ag-card-eta">{eta}</span>
282
- {/if}
283
- </span>
284
- {#if ev.tags?.length}
285
- <div class="ag-card-tags">
286
- {#each ev.tags as tag}
287
- <span class="ag-card-tag">{tag}</span>
288
- {/each}
289
- </div>
290
- {/if}
291
- {#if isNow}
292
- <div class="ag-card-progress">
293
- <div class="ag-card-progress-fill" style:width="{progress(ev) * 100}%"></div>
294
- </div>
295
- {/if}
296
- </div>
297
- </div>
298
- {/snippet}
299
-
300
- <!-- svelte-ignore a11y_no_static_element_interactions -->
301
- <div
302
- class="ag ag--week"
303
- class:ag--mobile={isMobile}
304
- style={style || undefined}
305
- onpointerdown={onPointerDown}
306
- onpointerup={onPointerUp}
307
- >
308
- <div class="ag-body" role="list" aria-label={L.weekAhead}>
309
- {#each weekDays as day (day.ms)}
310
- {@const expanded = day.tier === 'today' || day.tier === 'tomorrow'}
311
- {#if day.tier === 'past'}
312
- <!-- Past day: single collapsed line -->
313
- <div class="ag-wday ag-wday--past" class:ag-wday--disabled={disabledSet.has(day.ms)} role="listitem">
314
- <div class="ag-wday-head">
315
- <div class="ag-wday-head-left">
316
- <span class="ag-wday-name">{day.dayName}</span>
317
- {#if showDates}<span class="ag-wday-date">{day.dateLabel}</span>{/if}
318
- </div>
319
- {#if dayHeaderSnippet}
320
- <div class="ag-wday-custom-header">
321
- {@render dayHeaderSnippet({ date: new Date(day.ms), isToday: false, dayName: day.dayName })}
322
- </div>
323
- {/if}
324
- </div>
325
- </div>
326
- {:else}
327
- <div
328
- class="ag-wday"
329
- class:ag-wday--today={day.tier === 'today'}
330
- class:ag-wday--tomorrow={day.tier === 'tomorrow'}
331
- class:ag-wday--equal={equalDays}
332
- class:ag-wday--disabled={disabledSet.has(day.ms)}
333
- role="listitem"
334
- >
335
- <!-- Day header -->
336
- <div class="ag-wday-head">
337
- <div class="ag-wday-head-left">
338
- {#if day.tier === 'today'}
339
- <span class="ag-wday-badge">{L.today}</span>
340
- {:else if day.tier === 'tomorrow'}
341
- <span class="ag-wday-badge ag-wday-badge--muted">{L.tomorrow}</span>
342
- {/if}
343
- <span class="ag-wday-name">{day.dayName}</span>
344
- {#if showDates}<span class="ag-wday-date">{day.dateLabel}</span>{/if}
345
- </div>
346
- {#if dayHeaderSnippet}
347
- <div class="ag-wday-custom-header">
348
- {@render dayHeaderSnippet({ date: new Date(day.ms), isToday: day.tier === 'today', dayName: day.dayName })}
349
- </div>
350
- {/if}
351
- </div>
352
-
353
- {#if day.allDayEvents.length > 0}
354
- <div class="ag-allday">
355
- {#each day.allDayEvents as ev (ev.id)}
356
- <div
357
- class="ag-allday-chip"
358
- class:ag-allday-chip--selected={selectedEventId === ev.id}
359
- style:--ev-color={ev.color || 'var(--dt-accent)'}
360
- role="button"
361
- tabindex="0"
362
- aria-label="{ev.title}, {L.allDay}"
363
- onclick={() => handleClick(ev)}
364
- onpointerenter={() => oneventhover?.(ev)}
365
- onkeydown={(e) => handleKeydown(e, ev)}
366
- >
367
- <span class="ag-allday-dot"></span>
368
- <span class="ag-allday-title">{ev.title}</span>
369
- </div>
370
- {/each}
371
- </div>
372
- {/if}
373
-
374
- {#if day.events.length === 0}
375
- <div class="ag-wday-empty">{L.noEvents}</div>
376
- {:else if equalDays}
377
- <!-- Equal days: card layout for all days, no time-relative badges -->
378
- <div class="ag-wday-expanded">
379
- {#each groupIntoSlots(day.timedEvents) as slot (slot.startMs)}
380
- <div class="ag-wslot">
381
-
382
- <div class="ag-wslot-cards" class:ag-wslot-cards--multi={slot.events.length > 1}>
383
- {#each slot.events as ev (ev.id)}
384
- {@render eventCard(ev, false)}
385
- {/each}
386
- </div>
387
- </div>
388
- {/each}
389
- </div>
390
- {:else if expanded}
391
- <!-- Expanded: today/tomorrow get full slot treatment -->
392
- <div class="ag-wday-expanded">
393
- {#if day.currentEvents.length > 0}
394
- {#each day.currentEvents as ev (ev.id)}
395
- <div class="ag-wslot">
396
- <div class="ag-wslot-header">
397
- <span class="ag-wslot-now">{L.now}</span>
398
- </div>
399
- {@render eventCard(ev, true)}
400
- </div>
401
- {/each}
402
- {/if}
403
- {#each groupIntoSlots(day.upcomingEvents) as slot (slot.startMs)}
404
- <div class="ag-wslot">
405
-
406
- <div class="ag-wslot-cards" class:ag-wslot-cards--multi={slot.events.length > 1}>
407
- {#each slot.events as ev (ev.id)}
408
- {@render eventCard(ev, false, day.tier === 'today' ? timeUntilMs(ev.start.getTime()) : undefined)}
409
- {/each}
410
- </div>
411
- </div>
412
- {/each}
413
- {#if day.pastEvents.length > 0}
414
- <div class="ag-wday-past-line">✓ {L.nCompleted(day.pastEvents.length)}</div>
415
- {/if}
416
- </div>
417
- {:else}
418
- <!-- Compact: future days get minimal rows -->
419
- <div class="ag-wday-compact">
420
- {#each day.timedEvents.slice(0, 4) as ev (ev.id)}
421
- <div
422
- class="ag-compact"
423
- class:ag-compact--selected={selectedEventId === ev.id}
424
- style:--ev-color={ev.color || 'var(--dt-accent)'}
425
- role="button"
426
- tabindex="0"
427
- aria-label="{ev.title}, {fmtTime(ev.start)}, {duration(ev)}"
428
- onclick={() => handleClick(ev)} onpointerenter={() => oneventhover?.(ev)} onkeydown={(e) => handleKeydown(e, ev)}
429
- >
430
- <span class="ag-compact-dot"></span>
431
- <span class="ag-compact-time">{fmtTime(ev.start)}</span>
432
- <span class="ag-compact-title">{ev.title}</span>
433
- {#if ev.subtitle}
434
- <span class="ag-compact-sub">{ev.subtitle}</span>
435
- {/if}
436
- {#if ev.tags?.length}
437
- {#each ev.tags as tag}
438
- <span class="ag-compact-tag">{tag}</span>
439
- {/each}
440
- {/if}
441
- <span class="ag-compact-dur">{duration(ev)}</span>
442
- </div>
443
- {/each}
444
- {#if day.timedEvents.length > 4}
445
- <div class="ag-compact-more">{L.nMore(day.timedEvents.length - 4)}</div>
446
- {/if}
447
- </div>
448
- {/if}
449
- </div>
450
- {/if}
451
- {/each}
452
- </div>
453
-
454
- <!-- ── Floating nav pills (desktop only — mobile uses Calendar header) ── -->
455
- {#if showNav && !isMobile}
456
- <nav class="ag-nav" aria-label={L.weekNavigation}>
457
- <button
458
- class="ag-nav-pill ag-nav-today"
459
- class:ag-nav-today--hidden={isThisWeek}
460
- onclick={() => viewState?.goToday()}
461
- aria-label={L.goToToday}
462
- tabindex={isThisWeek ? -1 : 0}
463
- >
464
- {L.today}
465
- </button>
466
- <button
467
- class="ag-nav-pill"
468
- onclick={() => viewState?.prev()}
469
- aria-label={L.previousWeek}
470
- >
471
- <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="12" height="12" aria-hidden="true"><path d="M10 3 5 8l5 5"/></svg>
472
- </button>
473
- <button
474
- class="ag-nav-pill"
475
- onclick={() => viewState?.next()}
476
- aria-label={L.nextWeek}
477
- >
478
- <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="12" height="12" aria-hidden="true"><path d="M6 3l5 5-5 5"/></svg>
479
- </button>
480
- </nav>
481
- {/if}
482
- </div>
483
-
484
- <style>
485
- /* ═══ Floating nav pills ═══ */
486
- .ag-nav {
487
- position: absolute;
488
- top: 10px;
489
- right: 14px;
490
- z-index: 20;
491
- display: flex;
492
- gap: 2px;
493
- background: color-mix(in srgb, var(--dt-surface, #10141c) 85%, transparent);
494
- backdrop-filter: blur(6px);
495
- -webkit-backdrop-filter: blur(6px);
496
- border-radius: 8px;
497
- padding: 2px;
498
- border: 1px solid var(--dt-border, rgba(148, 163, 184, 0.07));
499
- }
500
- .ag-nav-pill {
501
- border: none;
502
- background: transparent;
503
- color: var(--dt-text-2, rgba(148, 163, 184, 0.55));
504
- cursor: pointer;
505
- font: 600 11px / 1 var(--dt-sans, 'Outfit', system-ui, sans-serif);
506
- padding: 6px 12px;
507
- border-radius: 6px;
508
- letter-spacing: 0.04em;
509
- text-transform: uppercase;
510
- transition: background 100ms, color 100ms;
511
- display: inline-flex;
512
- align-items: center;
513
- justify-content: center;
514
- }
515
- .ag-nav-pill:hover {
516
- color: var(--dt-text, rgba(226, 232, 240, 0.85));
517
- }
518
- .ag-nav-today {
519
- max-width: 60px;
520
- overflow: hidden;
521
- white-space: nowrap;
522
- transition: max-width 250ms ease, padding 250ms ease, opacity 200ms ease;
523
- }
524
- .ag-nav-today--hidden {
525
- max-width: 0;
526
- padding-left: 0;
527
- padding-right: 0;
528
- opacity: 0;
529
- pointer-events: none;
530
- }
531
- .ag-nav-pill:focus-visible {
532
- outline: 2px solid color-mix(in srgb, var(--dt-accent, #ef4444) 55%, transparent);
533
- outline-offset: 2px;
534
- }
535
-
536
- /* ═══ Container ═══ */
537
- .ag {
538
- position: relative;
539
- overflow: hidden;
540
- user-select: none;
541
- display: flex;
542
- flex-direction: column;
543
- height: 100%;
544
- color: var(--dt-text, rgba(255, 255, 255, 0.92));
545
- font-family: var(--dt-sans, system-ui, sans-serif);
546
- }
547
-
548
- /* ═══ Body ═══ */
549
- .ag-body {
550
- flex: 1;
551
- overflow-y: auto;
552
- scrollbar-width: thin;
553
- scrollbar-color: var(--dt-border) transparent;
554
- }
555
- .ag-body::-webkit-scrollbar {
556
- width: 4px;
557
- }
558
- .ag-body::-webkit-scrollbar-thumb {
559
- background: var(--dt-border);
560
- border-radius: 2px;
561
- }
562
-
563
- /* ═══ All-day chips ═══ */
564
- .ag-allday {
565
- display: flex;
566
- flex-wrap: wrap;
567
- gap: 4px;
568
- padding: 4px 14px 6px;
569
- }
570
- .ag-allday-chip {
571
- display: flex;
572
- align-items: center;
573
- gap: 4px;
574
- padding: 2px 8px;
575
- border-radius: 5px;
576
- background: color-mix(in srgb, var(--ev-color) 12%, var(--dt-surface, #10141c));
577
- border: 1px solid color-mix(in srgb, var(--ev-color) 18%, transparent);
578
- cursor: pointer;
579
- transition: background 0.15s, border-color 0.15s;
580
- }
581
- .ag-allday-chip:hover {
582
- background: color-mix(in srgb, var(--ev-color) 22%, var(--dt-surface, #10141c));
583
- border-color: color-mix(in srgb, var(--ev-color) 30%, transparent);
584
- }
585
- .ag-allday-chip:focus-visible {
586
- outline: 2px solid var(--dt-accent, #ff6b4a);
587
- outline-offset: 2px;
588
- }
589
- .ag-allday-chip--selected {
590
- border-color: var(--ev-color);
591
- background: color-mix(in srgb, var(--ev-color) 18%, var(--dt-surface, #10141c));
592
- }
593
- .ag-allday-dot {
594
- width: 5px;
595
- height: 5px;
596
- border-radius: 50%;
597
- background: var(--ev-color);
598
- flex-shrink: 0;
599
- }
600
- .ag-allday-title {
601
- font: 500 0.7rem/1.2 var(--dt-sans, system-ui, sans-serif);
602
- color: var(--dt-text, rgba(226, 232, 240, 0.85));
603
- white-space: nowrap;
604
- }
605
-
606
- /* ═══ Shared: event card ═══ */
607
- .ag-card {
608
- display: flex;
609
- align-items: stretch;
610
- border-radius: 6px;
611
- background: color-mix(in srgb, var(--ev-color) 12%, var(--dt-surface, #191919));
612
- border: 1px solid color-mix(in srgb, var(--ev-color) 8%, var(--dt-border, rgba(255, 255, 255, 0.06)));
613
- overflow: hidden;
614
- cursor: pointer;
615
- transition: background 150ms, border-color 150ms;
616
- }
617
- .ag-card:hover {
618
- background: color-mix(in srgb, var(--ev-color) 20%, var(--dt-surface, #191919));
619
- border-color: color-mix(in srgb, var(--ev-color) 30%, transparent);
620
- }
621
- .ag-card:focus-visible {
622
- outline: 2px solid var(--dt-accent, #ff6b4a);
623
- outline-offset: 2px;
624
- }
625
- .ag-card--selected {
626
- border-color: var(--ev-color);
627
- background: color-mix(in srgb, var(--ev-color) 20%, var(--dt-surface, #191919));
628
- }
629
- .ag-card-body {
630
- padding: 7px 10px;
631
- display: flex;
632
- flex-direction: column;
633
- gap: 2px;
634
- min-width: 0;
635
- flex: 1;
636
- }
637
- .ag-card-title {
638
- font-size: 13px;
639
- font-weight: 600;
640
- line-height: 1.2;
641
- white-space: nowrap;
642
- overflow: hidden;
643
- text-overflow: ellipsis;
644
- flex: 1;
645
- }
646
- .ag-card-meta {
647
- display: flex;
648
- align-items: center;
649
- font-size: 11px;
650
- color: var(--dt-text-2, rgba(255, 255, 255, 0.5));
651
- font-family: var(--dt-mono, monospace);
652
- line-height: 1;
653
- }
654
- .ag-card-dur {
655
- margin-left: 6px;
656
- color: var(--dt-text-3, rgba(255, 255, 255, 0.3));
657
- }
658
- .ag-card-eta {
659
- margin-left: auto;
660
- font-size: 10px;
661
- font-weight: 600;
662
- color: color-mix(in srgb, var(--ev-color) 80%, var(--dt-text));
663
- opacity: 0.85;
664
- letter-spacing: 0.02em;
665
- }
666
- .ag-card-sub {
667
- font-size: 11px;
668
- color: var(--dt-text-2, rgba(255, 255, 255, 0.45));
669
- line-height: 1;
670
- }
671
- .ag-card-tags {
672
- display: flex;
673
- gap: 4px;
674
- flex-wrap: wrap;
675
- }
676
- .ag-card-tag {
677
- font: 500 9px / 1 var(--dt-sans, system-ui, sans-serif);
678
- color: var(--ev-color, var(--dt-accent));
679
- background: color-mix(in srgb, var(--ev-color, var(--dt-accent)) 15%, transparent);
680
- padding: 2px 5px;
681
- border-radius: 3px;
682
- white-space: nowrap;
683
- }
684
- .ag-card-progress {
685
- height: 3px;
686
- background: var(--dt-border, rgba(255, 255, 255, 0.06));
687
- border-radius: 2px;
688
- overflow: hidden;
689
- margin-top: 2px;
690
- }
691
- .ag-card-progress-fill {
692
- height: 100%;
693
- background: var(--ev-color, var(--dt-accent));
694
- border-radius: 2px;
695
- transition: width 1s linear;
696
- }
697
-
698
- /* ═══ Week day groups ═══ */
699
- .ag-wday {
700
- border-bottom: 1px solid var(--dt-border, rgba(255, 255, 255, 0.06));
701
- }
702
- .ag-wday--today {
703
- background: color-mix(in srgb, var(--dt-accent, #ff6b4a) 2%, transparent);
704
- }
705
- .ag-wday--tomorrow .ag-card {
706
- opacity: 0.82;
707
- }
708
- .ag-wday--past {
709
- opacity: 0.4;
710
- }
711
- .ag-wday--past .ag-wday-head {
712
- padding: 8px 20px;
713
- }
714
- .ag-wday--disabled {
715
- opacity: 0.35;
716
- pointer-events: none;
717
- position: relative;
718
- }
719
- .ag-wday--disabled::after {
720
- content: '';
721
- position: absolute;
722
- inset: 0;
723
- background: repeating-linear-gradient(
724
- 135deg,
725
- transparent,
726
- transparent 4px,
727
- rgba(128, 128, 128, 0.08) 4px,
728
- rgba(128, 128, 128, 0.08) 8px
729
- );
730
- pointer-events: none;
731
- }
732
- .ag-wday-custom-header {
733
- padding: 2px 0 4px;
734
- }
735
-
736
- .ag-wday-head {
737
- display: flex;
738
- justify-content: space-between;
739
- align-items: center;
740
- padding: 10px 20px;
741
- }
742
- .ag-wday-head-left {
743
- display: flex;
744
- align-items: center;
745
- gap: 8px;
746
- }
747
- .ag-wday-badge {
748
- font-size: 10px;
749
- font-weight: 600;
750
- letter-spacing: 0.08em;
751
- text-transform: uppercase;
752
- color: var(--dt-accent, #ff6b4a);
753
- background: color-mix(in srgb, var(--dt-accent, #ff6b4a) 12%, transparent);
754
- padding: 2px 7px;
755
- border-radius: 3px;
756
- }
757
- .ag-wday-badge--muted {
758
- color: var(--dt-text-2, rgba(255, 255, 255, 0.5));
759
- background: color-mix(
760
- in srgb,
761
- var(--dt-text-2, rgba(255, 255, 255, 0.5)) 10%,
762
- transparent
763
- );
764
- }
765
- .ag-wday-name {
766
- font-size: 13px;
767
- font-weight: 600;
768
- }
769
- .ag-wday-date {
770
- font-size: 11px;
771
- font-family: var(--dt-mono, monospace);
772
- color: var(--dt-text-3, rgba(255, 255, 255, 0.3));
773
- }
774
-
775
- .ag-wday-empty {
776
- padding: 2px 20px 10px;
777
- font-size: 11px;
778
- color: var(--dt-text-3, rgba(255, 255, 255, 0.2));
779
- font-style: italic;
780
- }
781
-
782
- /* Expanded day */
783
- .ag-wday-expanded {
784
- padding: 0 20px 10px;
785
- }
786
- .ag-wslot {
787
- margin-bottom: 4px;
788
- }
789
- .ag-wslot-header {
790
- display: flex;
791
- align-items: baseline;
792
- gap: 8px;
793
- padding: 2px 0;
794
- }
795
- .ag-wslot-now {
796
- font-size: 9px;
797
- font-weight: 700;
798
- letter-spacing: 0.08em;
799
- text-transform: uppercase;
800
- color: var(--dt-accent, #ff6b4a);
801
- }
802
- .ag-wslot-cards {
803
- display: flex;
804
- flex-direction: column;
805
- gap: 4px;
806
- }
807
- .ag-wslot-cards--multi {
808
- display: grid;
809
- grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
810
- gap: 4px;
811
- }
812
- .ag-wday-past-line {
813
- font-size: 10px;
814
- color: var(--dt-text-3, rgba(255, 255, 255, 0.3));
815
- padding: 6px 0 0;
816
- opacity: 0.5;
817
- }
818
-
819
- /* Compact day events */
820
- .ag-wday-compact {
821
- padding: 0 20px 10px;
822
- }
823
- .ag-compact {
824
- display: flex;
825
- align-items: center;
826
- gap: 8px;
827
- padding: 4px 0;
828
- cursor: pointer;
829
- }
830
- .ag-compact--selected {
831
- background: color-mix(in srgb, var(--ev-color) 10%, transparent);
832
- border-radius: 4px;
833
- padding-left: 6px;
834
- padding-right: 6px;
835
- }
836
- .ag-compact:hover .ag-compact-title {
837
- color: var(--dt-text);
838
- }
839
- .ag-compact:focus-visible {
840
- outline: 2px solid var(--dt-accent, #ff6b4a);
841
- outline-offset: 2px;
842
- }
843
- .ag-compact-dot {
844
- width: 5px;
845
- height: 5px;
846
- border-radius: 50%;
847
- background: var(--ev-color, var(--dt-accent));
848
- flex-shrink: 0;
849
- }
850
- .ag-compact-time {
851
- font-size: 11px;
852
- font-family: var(--dt-mono, monospace);
853
- color: var(--dt-text-2, rgba(255, 255, 255, 0.5));
854
- min-width: 64px;
855
- flex-shrink: 0;
856
- }
857
- .ag-compact-title {
858
- font-size: 12px;
859
- font-weight: 500;
860
- color: var(--dt-text-2, rgba(255, 255, 255, 0.5));
861
- flex: 1;
862
- white-space: nowrap;
863
- overflow: hidden;
864
- text-overflow: ellipsis;
865
- transition: color 150ms;
866
- }
867
- .ag-compact-dur {
868
- font-size: 10px;
869
- font-family: var(--dt-mono, monospace);
870
- color: var(--dt-text-3, rgba(255, 255, 255, 0.3));
871
- flex-shrink: 0;
872
- }
873
- .ag-compact-sub {
874
- font-size: 10px;
875
- color: var(--dt-text-3, rgba(255, 255, 255, 0.35));
876
- flex-shrink: 0;
877
- }
878
- .ag-compact-tag {
879
- font: 500 8px / 1 var(--dt-sans, system-ui, sans-serif);
880
- color: var(--ev-color, var(--dt-accent));
881
- background: color-mix(in srgb, var(--ev-color, var(--dt-accent)) 12%, transparent);
882
- padding: 1px 4px;
883
- border-radius: 3px;
884
- white-space: nowrap;
885
- flex-shrink: 0;
886
- }
887
- .ag-compact-more {
888
- font-size: 11px;
889
- color: var(--dt-text-3);
890
- padding: 2px 0 0 13px;
891
- }
892
-
893
- /* ═══ Mobile adaptations ═══ */
894
- .ag--mobile .ag-wday-head {
895
- padding: 12px 16px;
896
- }
897
- .ag--mobile .ag-wday-expanded {
898
- padding: 0 16px 12px;
899
- }
900
- .ag--mobile .ag-wday-compact {
901
- padding: 0 16px 12px;
902
- }
903
- .ag--mobile .ag-card-body {
904
- padding: 12px 14px;
905
- }
906
- .ag--mobile .ag-card-title {
907
- font-size: 14px;
908
- }
909
- .ag--mobile .ag-compact {
910
- padding: 8px 0;
911
- }
912
- .ag--mobile .ag-compact-title {
913
- font-size: 14px;
914
- }
915
- .ag--mobile .ag-compact-time {
916
- font-size: 12px;
917
- }
918
- .ag--mobile .ag-nav-pill {
919
- padding: 10px 16px;
920
- }
921
- .ag--mobile .ag-wslot-cards--multi {
922
- grid-template-columns: 1fr;
923
- }
924
- </style>
1
+ <script lang="ts">
2
+ /**
3
+ * AgendaWeek — rolling N-day agenda view.
4
+ *
5
+ * "The Week Ahead":
6
+ * Today + tomorrow expanded with time slots/countdowns.
7
+ * Future days compact (dot + time + title).
8
+ * Past days dimmed.
9
+ *
10
+ * Answers: "What's coming up and when do I need to be ready?"
11
+ */
12
+ import { createClock } from '../../core/clock.svelte.js';
13
+ import type { TimelineEvent } from '../../core/types.js';
14
+ import { sod, DAY_MS, startOfWeek, dayNum, isAllDay, isMultiDay } from '../../core/time.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
+
19
+ const L = $derived(getLabels());
20
+ const ctx = useCalendarContext();
21
+
22
+ interface Props {
23
+ mondayStart?: boolean;
24
+ locale?: string;
25
+ height?: number;
26
+ events?: TimelineEvent[];
27
+ style?: string;
28
+ focusDate?: Date;
29
+ oneventclick?: (event: TimelineEvent) => void;
30
+ selectedEventId?: string | null;
31
+ [key: string]: unknown;
32
+ }
33
+
34
+ let {
35
+ mondayStart = true,
36
+ locale,
37
+ height = 520,
38
+ events = [],
39
+ style = '',
40
+ focusDate,
41
+ oneventclick,
42
+ selectedEventId = null,
43
+ }: Props = $props();
44
+
45
+ const clock = createClock();
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);
57
+
58
+ // ── Swipe navigation (mobile) ──────────────────────
59
+ let swipeStartX = 0;
60
+ let swipeStartY = 0;
61
+ const SWIPE_THRESHOLD = 50;
62
+
63
+ function onPointerDown(e: PointerEvent) {
64
+ if (!isMobile) return;
65
+ swipeStartX = e.clientX;
66
+ swipeStartY = e.clientY;
67
+ }
68
+
69
+ function onPointerUp(e: PointerEvent) {
70
+ if (!isMobile) return;
71
+ const dx = e.clientX - swipeStartX;
72
+ const dy = e.clientY - swipeStartY;
73
+ if (Math.abs(dx) > SWIPE_THRESHOLD && Math.abs(dx) > Math.abs(dy) * 1.4) {
74
+ if (dx > 0) viewState?.prev();
75
+ else viewState?.next();
76
+ }
77
+ }
78
+
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);
85
+
86
+ // ── Event handlers ──────────────────────────────────
87
+ function handleClick(ev: TimelineEvent): void {
88
+ oneventclick?.(ev);
89
+ }
90
+
91
+ function handleKeydown(e: KeyboardEvent, ev: TimelineEvent): void {
92
+ if (e.key === 'Enter' || e.key === ' ') {
93
+ e.preventDefault();
94
+ oneventclick?.(ev);
95
+ }
96
+ }
97
+
98
+ // ── Week derivations ────────────────────────────────
99
+ type DayTier = 'today' | 'tomorrow' | 'upcoming' | 'past';
100
+
101
+ interface DayGroup {
102
+ ms: number;
103
+ dayName: string;
104
+ dateLabel: string;
105
+ tier: DayTier;
106
+ events: TimelineEvent[];
107
+ allDayEvents: TimelineEvent[];
108
+ timedEvents: TimelineEvent[];
109
+ pastEvents: TimelineEvent[];
110
+ currentEvents: TimelineEvent[];
111
+ upcomingEvents: TimelineEvent[];
112
+ totalHours: number;
113
+ }
114
+
115
+ const weekStartMs = $derived(
116
+ focusDate
117
+ ? (viewState?.dayCount === 7
118
+ ? startOfWeek(sod(focusDate.getTime()), mondayStart)
119
+ : sod(focusDate.getTime()))
120
+ : (viewState?.dayCount === 7
121
+ ? startOfWeek(clock.today, mondayStart)
122
+ : clock.today),
123
+ );
124
+
125
+ const customDays = $derived(viewState?.dayCount ?? 7);
126
+
127
+ const isThisWeek = $derived(
128
+ customDays === 7
129
+ ? weekStartMs === startOfWeek(clock.today, mondayStart)
130
+ : clock.today >= weekStartMs && clock.today < weekStartMs + customDays * DAY_MS,
131
+ );
132
+
133
+ const weekDays = $derived.by((): DayGroup[] => {
134
+ const now = clock.tick;
135
+ const todayMs = clock.today;
136
+ const tomorrowMs = todayMs + DAY_MS;
137
+ const out: DayGroup[] = [];
138
+ for (let i = 0; i < customDays; i++) {
139
+ const ms = weekStartMs + i * DAY_MS;
140
+ const dEnd = ms + DAY_MS;
141
+ const dayEvts = events
142
+ .filter((ev) => ev.start.getTime() < dEnd && ev.end.getTime() > ms)
143
+ .sort((a, b) => a.start.getTime() - b.start.getTime());
144
+ const allDayEvts = dayEvts.filter((ev) => isAllDay(ev) || isMultiDay(ev));
145
+ const timedEvts = dayEvts.filter((ev) => !isAllDay(ev) && !isMultiDay(ev));
146
+ const totalMinutes = timedEvts.reduce((sum, ev) => {
147
+ const s = Math.max(ev.start.getTime(), ms);
148
+ const e = Math.min(ev.end.getTime(), dEnd);
149
+ return sum + (e - s) / 60000;
150
+ }, 0);
151
+ const pastEvents: TimelineEvent[] = [];
152
+ const currentEvents: TimelineEvent[] = [];
153
+ const upcomingEvents: TimelineEvent[] = [];
154
+ for (const ev of timedEvts) {
155
+ if (ev.end.getTime() <= now) pastEvents.push(ev);
156
+ else if (ev.start.getTime() <= now && ev.end.getTime() > now) currentEvents.push(ev);
157
+ else upcomingEvents.push(ev);
158
+ }
159
+ let tier: DayTier;
160
+ if (equalDays) {
161
+ tier = 'upcoming';
162
+ } else if (ms === todayMs) {
163
+ tier = 'today';
164
+ } else if (ms === tomorrowMs) {
165
+ tier = 'tomorrow';
166
+ } else if (ms < todayMs) {
167
+ tier = 'past';
168
+ } else {
169
+ tier = 'upcoming';
170
+ }
171
+
172
+ out.push({
173
+ ms,
174
+ dayName: weekdayLong(ms, locale),
175
+ dateLabel: `${monthLong(ms, locale)} ${dayNum(ms)}`,
176
+ tier,
177
+ events: dayEvts,
178
+ allDayEvents: allDayEvts,
179
+ timedEvents: timedEvts,
180
+ pastEvents,
181
+ currentEvents,
182
+ upcomingEvents,
183
+ totalHours: Math.round((totalMinutes / 60) * 10) / 10,
184
+ });
185
+ }
186
+
187
+ // Filter hidden days if hideDays is set
188
+ if (hideDays?.length) {
189
+ return out.filter((d) => {
190
+ const jsDay = new Date(d.ms).getDay();
191
+ const iso = jsDay === 0 ? 7 : jsDay;
192
+ return !hideDays.includes(iso);
193
+ });
194
+ }
195
+
196
+ return out;
197
+ });
198
+ </script>
199
+
200
+ <!-- ═══ Shared event card snippet ═══ -->
201
+ {#snippet eventCard(ev: TimelineEvent, isNow: boolean, eta?: string)}
202
+ <div
203
+ class="ag-card"
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'}
209
+ style:--ev-color={ev.color || 'var(--dt-accent)'}
210
+ role="button"
211
+ tabindex="0"
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)}"
213
+ onclick={() => handleClick(ev)}
214
+ onpointerenter={() => oneventhover?.(ev)}
215
+ onkeydown={(e) => handleKeydown(e, ev)}
216
+ >
217
+ <div class="ag-card-body">
218
+ <span class="ag-card-title">{ev.title}</span>
219
+ {#if ev.subtitle}
220
+ <span class="ag-card-sub">{ev.subtitle}</span>
221
+ {/if}
222
+ {#if ev.location}
223
+ <span class="ag-card-loc">{ev.location}</span>
224
+ {/if}
225
+ <span class="ag-card-meta">
226
+ {#if isNow}
227
+ {L.until} {fmt(ev.end)}
228
+ {:else}
229
+ {fmt(ev.start)} – {fmt(ev.end)}
230
+ {/if}
231
+ <span class="ag-card-dur">{duration(ev)}</span>
232
+ {#if eta}
233
+ <span class="ag-card-eta">{eta}</span>
234
+ {/if}
235
+ </span>
236
+ {#if ev.tags?.length}
237
+ <div class="ag-card-tags">
238
+ {#each ev.tags as tag}
239
+ <span class="ag-card-tag">{tag}</span>
240
+ {/each}
241
+ </div>
242
+ {/if}
243
+ {#if isNow}
244
+ <div class="ag-card-progress">
245
+ <div class="ag-card-progress-fill" style:width="{prog(ev) * 100}%"></div>
246
+ </div>
247
+ {/if}
248
+ </div>
249
+ </div>
250
+ {/snippet}
251
+
252
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
253
+ <div
254
+ class="ag ag--week"
255
+ class:ag--mobile={isMobile}
256
+ class:ag--auto={autoHeight}
257
+ style={style || undefined}
258
+ onpointerdown={onPointerDown}
259
+ onpointerup={onPointerUp}
260
+ >
261
+ <div class="ag-body" role="list" aria-label={L.weekAhead}>
262
+ {#each weekDays as day (day.ms)}
263
+ {@const expanded = day.tier === 'today' || day.tier === 'tomorrow'}
264
+ {#if day.tier === 'past'}
265
+ <!-- Past day: single collapsed line -->
266
+ <div class="ag-wday ag-wday--past" class:ag-wday--disabled={disabledSet.has(day.ms)} role="listitem">
267
+ <div class="ag-wday-head">
268
+ <div class="ag-wday-head-left">
269
+ <span class="ag-wday-name">{day.dayName}</span>
270
+ {#if showDates}<span class="ag-wday-date">{day.dateLabel}</span>{/if}
271
+ </div>
272
+ {#if dayHeaderSnippet}
273
+ <div class="ag-wday-custom-header">
274
+ {@render dayHeaderSnippet({ date: new Date(day.ms), isToday: false, dayName: day.dayName })}
275
+ </div>
276
+ {/if}
277
+ </div>
278
+ </div>
279
+ {:else}
280
+ <div
281
+ class="ag-wday"
282
+ class:ag-wday--today={day.tier === 'today'}
283
+ class:ag-wday--tomorrow={day.tier === 'tomorrow'}
284
+ class:ag-wday--equal={equalDays}
285
+ class:ag-wday--disabled={disabledSet.has(day.ms)}
286
+ role="listitem"
287
+ >
288
+ <!-- Day header -->
289
+ <div class="ag-wday-head">
290
+ <div class="ag-wday-head-left">
291
+ {#if day.tier === 'today'}
292
+ <span class="ag-wday-badge">{L.today}</span>
293
+ {:else if day.tier === 'tomorrow'}
294
+ <span class="ag-wday-badge ag-wday-badge--muted">{L.tomorrow}</span>
295
+ {/if}
296
+ <span class="ag-wday-name">{day.dayName}</span>
297
+ {#if showDates}<span class="ag-wday-date">{day.dateLabel}</span>{/if}
298
+ </div>
299
+ {#if dayHeaderSnippet}
300
+ <div class="ag-wday-custom-header">
301
+ {@render dayHeaderSnippet({ date: new Date(day.ms), isToday: day.tier === 'today', dayName: day.dayName })}
302
+ </div>
303
+ {/if}
304
+ </div>
305
+
306
+ {#if day.allDayEvents.length > 0}
307
+ <div class="ag-allday">
308
+ {#each day.allDayEvents as ev (ev.id)}
309
+ <div
310
+ class="ag-allday-chip"
311
+ class:ag-allday-chip--selected={selectedEventId === ev.id}
312
+ style:--ev-color={ev.color || 'var(--dt-accent)'}
313
+ role="button"
314
+ tabindex="0"
315
+ aria-label="{ev.title}, {L.allDay}"
316
+ onclick={() => handleClick(ev)}
317
+ onpointerenter={() => oneventhover?.(ev)}
318
+ onkeydown={(e) => handleKeydown(e, ev)}
319
+ >
320
+ <span class="ag-allday-dot"></span>
321
+ <span class="ag-allday-title">{ev.title}</span>
322
+ </div>
323
+ {/each}
324
+ </div>
325
+ {/if}
326
+
327
+ {#if day.events.length === 0}
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>
363
+ {:else if equalDays}
364
+ <!-- Equal days: card layout for all days, no time-relative badges -->
365
+ <div class="ag-wday-expanded">
366
+ {#each groupIntoSlots(day.timedEvents) as slot (slot.startMs)}
367
+ <div class="ag-wslot">
368
+
369
+ <div class="ag-wslot-cards" class:ag-wslot-cards--multi={slot.events.length > 1}>
370
+ {#each slot.events as ev (ev.id)}
371
+ {@render eventCard(ev, false)}
372
+ {/each}
373
+ </div>
374
+ </div>
375
+ {/each}
376
+ </div>
377
+ {:else if expanded}
378
+ <!-- Expanded: today/tomorrow get full slot treatment -->
379
+ <div class="ag-wday-expanded">
380
+ {#if day.currentEvents.length > 0}
381
+ {#each day.currentEvents as ev (ev.id)}
382
+ <div class="ag-wslot">
383
+ <div class="ag-wslot-header">
384
+ <span class="ag-wslot-now">{L.now}</span>
385
+ </div>
386
+ {@render eventCard(ev, true)}
387
+ </div>
388
+ {/each}
389
+ {/if}
390
+ {#each groupIntoSlots(day.upcomingEvents) as slot (slot.startMs)}
391
+ <div class="ag-wslot">
392
+
393
+ <div class="ag-wslot-cards" class:ag-wslot-cards--multi={slot.events.length > 1}>
394
+ {#each slot.events as ev (ev.id)}
395
+ {@render eventCard(ev, false, day.tier === 'today' ? eta(ev.start.getTime()) : undefined)}
396
+ {/each}
397
+ </div>
398
+ </div>
399
+ {/each}
400
+ {#if day.pastEvents.length > 0}
401
+ <div class="ag-wday-past-line">✓ {L.nCompleted(day.pastEvents.length)}</div>
402
+ {/if}
403
+ </div>
404
+ {:else}
405
+ <!-- Compact: future days get minimal rows -->
406
+ <div class="ag-wday-compact">
407
+ {#each day.timedEvents.slice(0, 4) as ev (ev.id)}
408
+ <div
409
+ class="ag-compact"
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'}
415
+ style:--ev-color={ev.color || 'var(--dt-accent)'}
416
+ role="button"
417
+ tabindex="0"
418
+ aria-label="{ev.title}, {fmt(ev.start)}, {duration(ev)}"
419
+ onclick={() => handleClick(ev)} onpointerenter={() => oneventhover?.(ev)} onkeydown={(e) => handleKeydown(e, ev)}
420
+ >
421
+ <span class="ag-compact-dot"></span>
422
+ <span class="ag-compact-time">{fmt(ev.start)}</span>
423
+ <span class="ag-compact-title">{ev.title}</span>
424
+ {#if ev.location}
425
+ <span class="ag-compact-loc">{ev.location}</span>
426
+ {/if}
427
+ {#if ev.subtitle}
428
+ <span class="ag-compact-sub">{ev.subtitle}</span>
429
+ {/if}
430
+ {#if ev.tags?.length}
431
+ {#each ev.tags as tag}
432
+ <span class="ag-compact-tag">{tag}</span>
433
+ {/each}
434
+ {/if}
435
+ <span class="ag-compact-dur">{duration(ev)}</span>
436
+ </div>
437
+ {/each}
438
+ {#if day.timedEvents.length > 4}
439
+ <div class="ag-compact-more">{L.nMore(day.timedEvents.length - 4)}</div>
440
+ {/if}
441
+ </div>
442
+ {/if}
443
+ </div>
444
+ {/if}
445
+ {/each}
446
+ </div>
447
+
448
+ <!-- ── Floating nav pills (desktop only — mobile uses Calendar header) ── -->
449
+ {#if showNav && !isMobile}
450
+ <nav class="ag-nav" aria-label={L.weekNavigation}>
451
+ <button
452
+ class="ag-nav-pill ag-nav-today"
453
+ class:ag-nav-today--hidden={isThisWeek}
454
+ onclick={() => viewState?.goToday()}
455
+ aria-label={L.goToToday}
456
+ tabindex={isThisWeek ? -1 : 0}
457
+ >
458
+ {L.today}
459
+ </button>
460
+ <button
461
+ class="ag-nav-pill"
462
+ onclick={() => viewState?.prev()}
463
+ aria-label={L.previousWeek}
464
+ >
465
+ <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="12" height="12" aria-hidden="true"><path d="M10 3 5 8l5 5"/></svg>
466
+ </button>
467
+ <button
468
+ class="ag-nav-pill"
469
+ onclick={() => viewState?.next()}
470
+ aria-label={L.nextWeek}
471
+ >
472
+ <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="12" height="12" aria-hidden="true"><path d="M6 3l5 5-5 5"/></svg>
473
+ </button>
474
+ </nav>
475
+ {/if}
476
+ </div>
477
+
478
+ <style>
479
+ /* ═══ Floating nav pills ═══ */
480
+ .ag-nav {
481
+ position: absolute;
482
+ top: 10px;
483
+ right: 14px;
484
+ z-index: 20;
485
+ display: flex;
486
+ gap: 2px;
487
+ background: color-mix(in srgb, var(--dt-surface, #10141c) 85%, transparent);
488
+ backdrop-filter: blur(6px);
489
+ -webkit-backdrop-filter: blur(6px);
490
+ border-radius: 8px;
491
+ padding: 2px;
492
+ border: 1px solid var(--dt-border, rgba(148, 163, 184, 0.07));
493
+ }
494
+ .ag-nav-pill {
495
+ border: none;
496
+ background: transparent;
497
+ color: var(--dt-text-2, rgba(148, 163, 184, 0.55));
498
+ cursor: pointer;
499
+ font: 600 11px / 1 var(--dt-sans, 'Outfit', system-ui, sans-serif);
500
+ padding: 6px 12px;
501
+ border-radius: 6px;
502
+ letter-spacing: 0.04em;
503
+ text-transform: uppercase;
504
+ transition: background 100ms, color 100ms;
505
+ display: inline-flex;
506
+ align-items: center;
507
+ justify-content: center;
508
+ }
509
+ .ag-nav-pill:hover {
510
+ color: var(--dt-text, rgba(226, 232, 240, 0.85));
511
+ }
512
+ .ag-nav-today {
513
+ max-width: 60px;
514
+ overflow: hidden;
515
+ white-space: nowrap;
516
+ transition: max-width 250ms ease, padding 250ms ease, opacity 200ms ease;
517
+ }
518
+ .ag-nav-today--hidden {
519
+ max-width: 0;
520
+ padding-left: 0;
521
+ padding-right: 0;
522
+ opacity: 0;
523
+ pointer-events: none;
524
+ }
525
+ .ag-nav-pill:focus-visible {
526
+ outline: 2px solid color-mix(in srgb, var(--dt-accent, #ef4444) 55%, transparent);
527
+ outline-offset: 2px;
528
+ }
529
+
530
+ /* ═══ Container ═══ */
531
+ .ag {
532
+ position: relative;
533
+ overflow: hidden;
534
+ user-select: none;
535
+ display: flex;
536
+ flex-direction: column;
537
+ height: 100%;
538
+ width: 100%;
539
+ min-width: 0;
540
+ box-sizing: border-box;
541
+ color: var(--dt-text, rgba(255, 255, 255, 0.92));
542
+ font-family: var(--dt-sans, system-ui, sans-serif);
543
+ }
544
+ .ag--auto {
545
+ height: auto;
546
+ overflow: visible;
547
+ }
548
+
549
+ /* ═══ Body ═══ */
550
+ .ag-body {
551
+ flex: 1;
552
+ min-width: 0;
553
+ overflow-y: auto;
554
+ overflow-x: hidden;
555
+ scrollbar-width: thin;
556
+ scrollbar-color: var(--dt-border) transparent;
557
+ }
558
+ .ag--auto .ag-body {
559
+ overflow-y: visible;
560
+ }
561
+ .ag-body::-webkit-scrollbar {
562
+ width: 4px;
563
+ }
564
+ .ag-body::-webkit-scrollbar-thumb {
565
+ background: var(--dt-border);
566
+ border-radius: 2px;
567
+ }
568
+
569
+ /* ═══ All-day chips ═══ */
570
+ .ag-allday {
571
+ display: flex;
572
+ flex-wrap: wrap;
573
+ gap: 4px;
574
+ padding: 4px 14px 6px;
575
+ }
576
+ .ag-allday-chip {
577
+ display: flex;
578
+ align-items: center;
579
+ gap: 4px;
580
+ padding: 2px 8px;
581
+ border-radius: 5px;
582
+ background: color-mix(in srgb, var(--ev-color) 12%, var(--dt-surface, #10141c));
583
+ border: 1px solid color-mix(in srgb, var(--ev-color) 18%, transparent);
584
+ cursor: pointer;
585
+ transition: background 0.15s, border-color 0.15s;
586
+ }
587
+ .ag-allday-chip:hover {
588
+ background: color-mix(in srgb, var(--ev-color) 22%, var(--dt-surface, #10141c));
589
+ border-color: color-mix(in srgb, var(--ev-color) 30%, transparent);
590
+ }
591
+ .ag-allday-chip:focus-visible {
592
+ outline: 2px solid var(--dt-accent, #ff6b4a);
593
+ outline-offset: 2px;
594
+ }
595
+ .ag-allday-chip--selected {
596
+ border-color: var(--ev-color);
597
+ background: color-mix(in srgb, var(--ev-color) 18%, var(--dt-surface, #10141c));
598
+ }
599
+ .ag-allday-dot {
600
+ width: 5px;
601
+ height: 5px;
602
+ border-radius: 50%;
603
+ background: var(--ev-color);
604
+ flex-shrink: 0;
605
+ }
606
+ .ag-allday-title {
607
+ font: 500 0.7rem/1.2 var(--dt-sans, system-ui, sans-serif);
608
+ color: var(--dt-text, rgba(226, 232, 240, 0.85));
609
+ white-space: nowrap;
610
+ }
611
+
612
+ /* ═══ Shared: event card ═══ */
613
+ .ag-card {
614
+ display: flex;
615
+ align-items: stretch;
616
+ border-radius: 6px;
617
+ background: color-mix(in srgb, var(--ev-color) 12%, var(--dt-surface, #191919));
618
+ border: 1px solid color-mix(in srgb, var(--ev-color) 8%, var(--dt-border, rgba(255, 255, 255, 0.06)));
619
+ overflow: hidden;
620
+ cursor: pointer;
621
+ transition: background 150ms, border-color 150ms;
622
+ }
623
+ .ag-card:hover {
624
+ background: color-mix(in srgb, var(--ev-color) 20%, var(--dt-surface, #191919));
625
+ border-color: color-mix(in srgb, var(--ev-color) 30%, transparent);
626
+ }
627
+ .ag-card:focus-visible {
628
+ outline: 2px solid var(--dt-accent, #ff6b4a);
629
+ outline-offset: 2px;
630
+ }
631
+ .ag-card--selected {
632
+ border-color: var(--ev-color);
633
+ background: color-mix(in srgb, var(--ev-color) 20%, var(--dt-surface, #191919));
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
+ }
652
+ .ag-card-body {
653
+ padding: 7px 10px;
654
+ display: flex;
655
+ flex-direction: column;
656
+ gap: 2px;
657
+ min-width: 0;
658
+ flex: 1;
659
+ }
660
+ .ag-card-title {
661
+ font-size: 13px;
662
+ font-weight: 600;
663
+ line-height: 1.3;
664
+ word-break: break-word;
665
+ flex: 1;
666
+ min-width: 0;
667
+ }
668
+ .ag-card-meta {
669
+ display: flex;
670
+ align-items: center;
671
+ font-size: 11px;
672
+ color: var(--dt-text-2, rgba(255, 255, 255, 0.5));
673
+ font-family: var(--dt-mono, monospace);
674
+ line-height: 1;
675
+ }
676
+ .ag-card-dur {
677
+ margin-left: 6px;
678
+ color: var(--dt-text-3, rgba(255, 255, 255, 0.3));
679
+ }
680
+ .ag-card-eta {
681
+ margin-left: auto;
682
+ font-size: 10px;
683
+ font-weight: 600;
684
+ color: color-mix(in srgb, var(--ev-color) 80%, var(--dt-text));
685
+ opacity: 0.85;
686
+ letter-spacing: 0.02em;
687
+ }
688
+ .ag-card-sub {
689
+ font-size: 11px;
690
+ color: var(--dt-text-2, rgba(255, 255, 255, 0.45));
691
+ line-height: 1;
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
+ }
698
+ .ag-card-tags {
699
+ display: flex;
700
+ gap: 4px;
701
+ flex-wrap: wrap;
702
+ }
703
+ .ag-card-tag {
704
+ font: 500 9px / 1 var(--dt-sans, system-ui, sans-serif);
705
+ color: var(--ev-color, var(--dt-accent));
706
+ background: color-mix(in srgb, var(--ev-color, var(--dt-accent)) 15%, transparent);
707
+ padding: 2px 5px;
708
+ border-radius: 3px;
709
+ white-space: nowrap;
710
+ }
711
+ .ag-card-progress {
712
+ height: 3px;
713
+ background: var(--dt-border, rgba(255, 255, 255, 0.06));
714
+ border-radius: 2px;
715
+ overflow: hidden;
716
+ margin-top: 2px;
717
+ }
718
+ .ag-card-progress-fill {
719
+ height: 100%;
720
+ background: var(--ev-color, var(--dt-accent));
721
+ border-radius: 2px;
722
+ transition: width 1s linear;
723
+ }
724
+
725
+ /* ═══ Week day groups ═══ */
726
+ .ag-wday {
727
+ border-bottom: 1px solid var(--dt-border, rgba(255, 255, 255, 0.06));
728
+ }
729
+ .ag-wday--today {
730
+ background: color-mix(in srgb, var(--dt-accent, #ff6b4a) 2%, transparent);
731
+ }
732
+ .ag-wday--tomorrow .ag-card {
733
+ opacity: 0.82;
734
+ }
735
+ .ag-wday--past {
736
+ opacity: 0.4;
737
+ }
738
+ .ag-wday--past .ag-wday-head {
739
+ padding: 8px 20px;
740
+ }
741
+ .ag-wday--disabled {
742
+ opacity: 0.35;
743
+ pointer-events: none;
744
+ position: relative;
745
+ }
746
+ .ag-wday--disabled::after {
747
+ content: '';
748
+ position: absolute;
749
+ inset: 0;
750
+ background: repeating-linear-gradient(
751
+ 135deg,
752
+ transparent,
753
+ transparent 4px,
754
+ rgba(128, 128, 128, 0.08) 4px,
755
+ rgba(128, 128, 128, 0.08) 8px
756
+ );
757
+ pointer-events: none;
758
+ }
759
+ .ag-wday-custom-header {
760
+ padding: 2px 0 4px;
761
+ }
762
+
763
+ .ag-wday-head {
764
+ display: flex;
765
+ justify-content: space-between;
766
+ align-items: center;
767
+ padding: 8px 20px;
768
+ }
769
+ .ag-wday-head-left {
770
+ display: flex;
771
+ align-items: center;
772
+ gap: 8px;
773
+ }
774
+ .ag-wday-badge {
775
+ font-size: 10px;
776
+ font-weight: 600;
777
+ letter-spacing: 0.08em;
778
+ text-transform: uppercase;
779
+ color: var(--dt-accent, #ff6b4a);
780
+ background: color-mix(in srgb, var(--dt-accent, #ff6b4a) 12%, transparent);
781
+ padding: 2px 7px;
782
+ border-radius: 3px;
783
+ }
784
+ .ag-wday-badge--muted {
785
+ color: var(--dt-text-2, rgba(255, 255, 255, 0.5));
786
+ background: color-mix(
787
+ in srgb,
788
+ var(--dt-text-2, rgba(255, 255, 255, 0.5)) 10%,
789
+ transparent
790
+ );
791
+ }
792
+ .ag-wday-name {
793
+ font-size: 13px;
794
+ font-weight: 600;
795
+ }
796
+ .ag-wday-date {
797
+ font-size: 11px;
798
+ font-family: var(--dt-mono, monospace);
799
+ color: var(--dt-text-3, rgba(255, 255, 255, 0.3));
800
+ }
801
+
802
+ .ag-wday-empty {
803
+ padding: 2px 20px 6px;
804
+ font-size: 11px;
805
+ color: var(--dt-text-3, rgba(255, 255, 255, 0.2));
806
+ font-style: italic;
807
+ }
808
+
809
+ /* Expanded day */
810
+ .ag-wday-expanded {
811
+ padding: 0 20px 10px;
812
+ }
813
+ .ag-wslot {
814
+ margin-bottom: 4px;
815
+ }
816
+ .ag-wslot-header {
817
+ display: flex;
818
+ align-items: baseline;
819
+ gap: 8px;
820
+ padding: 2px 0;
821
+ }
822
+ .ag-wslot-now {
823
+ font-size: 9px;
824
+ font-weight: 700;
825
+ letter-spacing: 0.08em;
826
+ text-transform: uppercase;
827
+ color: var(--dt-accent, #ff6b4a);
828
+ }
829
+ .ag-wslot-cards {
830
+ display: flex;
831
+ flex-direction: column;
832
+ gap: 4px;
833
+ }
834
+ .ag-wslot-cards--multi {
835
+ display: grid;
836
+ grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
837
+ gap: 4px;
838
+ }
839
+ .ag-wday-past-line {
840
+ font-size: 10px;
841
+ color: var(--dt-text-3, rgba(255, 255, 255, 0.3));
842
+ padding: 6px 0 0;
843
+ opacity: 0.5;
844
+ }
845
+
846
+ /* Compact day events */
847
+ .ag-wday-compact {
848
+ padding: 0 20px 6px;
849
+ }
850
+ .ag-compact {
851
+ display: flex;
852
+ align-items: center;
853
+ gap: 6px;
854
+ padding: 3px 0;
855
+ cursor: pointer;
856
+ min-width: 0;
857
+ overflow: hidden;
858
+ }
859
+ .ag-compact--selected {
860
+ background: color-mix(in srgb, var(--ev-color) 10%, transparent);
861
+ border-radius: 4px;
862
+ padding-left: 6px;
863
+ padding-right: 6px;
864
+ }
865
+ .ag-compact:hover .ag-compact-title {
866
+ color: var(--dt-text);
867
+ }
868
+ .ag-compact:focus-visible {
869
+ outline: 2px solid var(--dt-accent, #ff6b4a);
870
+ outline-offset: 2px;
871
+ }
872
+ .ag-compact-dot {
873
+ width: 5px;
874
+ height: 5px;
875
+ border-radius: 50%;
876
+ background: var(--ev-color, var(--dt-accent));
877
+ flex-shrink: 0;
878
+ }
879
+ .ag-compact-time {
880
+ font-size: 11px;
881
+ font-family: var(--dt-mono, monospace);
882
+ color: var(--dt-text-2, rgba(255, 255, 255, 0.5));
883
+ min-width: 40px;
884
+ flex-shrink: 0;
885
+ white-space: nowrap;
886
+ }
887
+ .ag-compact-title {
888
+ font-size: 12px;
889
+ font-weight: 500;
890
+ color: var(--dt-text-2, rgba(255, 255, 255, 0.5));
891
+ flex: 1;
892
+ min-width: 0;
893
+ white-space: nowrap;
894
+ overflow: hidden;
895
+ text-overflow: ellipsis;
896
+ transition: color 150ms;
897
+ }
898
+ .ag-compact-dur {
899
+ font-size: 10px;
900
+ font-family: var(--dt-mono, monospace);
901
+ color: var(--dt-text-3, rgba(255, 255, 255, 0.3));
902
+ flex-shrink: 0;
903
+ white-space: nowrap;
904
+ }
905
+ .ag-compact-sub {
906
+ font-size: 10px;
907
+ color: var(--dt-text-3, rgba(255, 255, 255, 0.35));
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;
937
+ }
938
+ .ag-compact-tag {
939
+ font: 500 8px / 1 var(--dt-sans, system-ui, sans-serif);
940
+ color: var(--ev-color, var(--dt-accent));
941
+ background: color-mix(in srgb, var(--ev-color, var(--dt-accent)) 12%, transparent);
942
+ padding: 1px 4px;
943
+ border-radius: 3px;
944
+ white-space: nowrap;
945
+ flex-shrink: 0;
946
+ max-width: 80px;
947
+ overflow: hidden;
948
+ text-overflow: ellipsis;
949
+ }
950
+ .ag-compact-more {
951
+ font-size: 11px;
952
+ color: var(--dt-text-3);
953
+ padding: 2px 0 0 13px;
954
+ }
955
+
956
+ /* ═══ Mobile adaptations ═══ */
957
+ .ag--mobile .ag-wday-head {
958
+ padding: 12px 16px;
959
+ }
960
+ .ag--mobile .ag-wday-expanded {
961
+ padding: 0 16px 12px;
962
+ }
963
+ .ag--mobile .ag-wday-compact {
964
+ padding: 0 16px 12px;
965
+ }
966
+ .ag--mobile .ag-card-body {
967
+ padding: 12px 14px;
968
+ }
969
+ .ag--mobile .ag-card-title {
970
+ font-size: 14px;
971
+ }
972
+ .ag--mobile .ag-compact {
973
+ padding: 8px 0;
974
+ }
975
+ .ag--mobile .ag-compact-title {
976
+ font-size: 14px;
977
+ }
978
+ .ag--mobile .ag-compact-time {
979
+ font-size: 12px;
980
+ }
981
+ .ag--mobile .ag-nav-pill {
982
+ padding: 10px 16px;
983
+ }
984
+ .ag--mobile .ag-wslot-cards--multi {
985
+ grid-template-columns: 1fr;
986
+ }
987
+ </style>