@nomideusz/svelte-calendar 0.6.0 → 0.6.3

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