@nomideusz/svelte-calendar 0.5.2 → 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.
Files changed (49) hide show
  1. package/README.md +408 -178
  2. package/dist/adapters/memory.d.ts +7 -9
  3. package/dist/adapters/memory.js +8 -23
  4. package/dist/adapters/recurring.d.ts +8 -6
  5. package/dist/adapters/recurring.js +28 -30
  6. package/dist/calendar/Calendar.svelte +776 -356
  7. package/dist/calendar/Calendar.svelte.d.ts +57 -6
  8. package/dist/core/index.d.ts +2 -2
  9. package/dist/core/index.js +1 -1
  10. package/dist/core/locale.js +2 -2
  11. package/dist/core/palette.d.ts +5 -0
  12. package/dist/core/palette.js +8 -0
  13. package/dist/core/types.d.ts +14 -0
  14. package/dist/engine/event-store.svelte.d.ts +0 -17
  15. package/dist/engine/event-store.svelte.js +30 -17
  16. package/dist/engine/index.d.ts +1 -1
  17. package/dist/engine/view-state.svelte.d.ts +10 -5
  18. package/dist/engine/view-state.svelte.js +32 -16
  19. package/dist/index.d.ts +7 -7
  20. package/dist/index.js +3 -4
  21. package/dist/primitives/DayHeader.svelte +103 -103
  22. package/dist/primitives/EmptySlot.svelte +105 -105
  23. package/dist/primitives/EventBlock.svelte +312 -312
  24. package/dist/primitives/NowIndicator.svelte +178 -178
  25. package/dist/primitives/TimeGutter.svelte +104 -104
  26. package/dist/theme/auto.d.ts +53 -0
  27. package/dist/theme/auto.js +534 -0
  28. package/dist/theme/index.d.ts +3 -1
  29. package/dist/theme/index.js +3 -1
  30. package/dist/theme/presets.d.ts +20 -6
  31. package/dist/theme/presets.js +55 -42
  32. package/dist/views/agenda/AgendaDay.svelte +1073 -976
  33. package/dist/views/agenda/AgendaWeek.svelte +934 -775
  34. package/dist/views/agenda/index.d.ts +0 -2
  35. package/dist/views/agenda/index.js +0 -2
  36. package/dist/views/index.d.ts +3 -2
  37. package/dist/views/index.js +3 -2
  38. package/dist/views/mobile/Mobile.svelte +17 -0
  39. package/dist/views/mobile/Mobile.svelte.d.ts +7 -0
  40. package/dist/views/mobile/MobileDay.svelte +702 -0
  41. package/dist/views/mobile/MobileDay.svelte.d.ts +20 -0
  42. package/dist/views/mobile/MobileWeek.svelte +461 -0
  43. package/dist/views/mobile/MobileWeek.svelte.d.ts +20 -0
  44. package/dist/views/mobile/index.d.ts +1 -0
  45. package/dist/views/mobile/index.js +1 -0
  46. package/dist/views/planner/PlannerDay.svelte +1129 -957
  47. package/dist/views/planner/PlannerWeek.svelte +949 -840
  48. package/dist/widget/CalendarWidget.svelte +142 -157
  49. package/package.json +2 -1
@@ -1,976 +1,1073 @@
1
- <script lang="ts">
2
- /**
3
- * AgendaDay — single-day agenda view.
4
- *
5
- * Today ("The Queue"):
6
- * 3-column layout: Done | Now | Up next (hero).
7
- * Answers: "What's coming up next?"
8
- *
9
- * Past day ("The Log"):
10
- * Quiet chronological record of completed events.
11
- *
12
- * Future day ("The Plan"):
13
- * Clean numbered schedule list.
14
- */
15
- import { getContext } from 'svelte';
16
- import { createClock } from '../../core/clock.svelte.js';
17
- import type { TimelineEvent } from '../../core/types.js';
18
- import { sod, DAY_MS, isAllDay, isMultiDay } from '../../core/time.js';
19
- import { fmtTime as _fmtTime, fmtDuration, getLabels } from '../../core/locale.js';
20
- import type { ViewState } from '../../engine/view-state.svelte.js';
21
-
22
- const L = $derived(getLabels());
23
-
24
- interface Props {
25
- locale?: string;
26
- height?: number;
27
- events?: TimelineEvent[];
28
- style?: string;
29
- focusDate?: Date;
30
- oneventclick?: (event: TimelineEvent) => void;
31
- selectedEventId?: string | null;
32
- [key: string]: unknown;
33
- }
34
-
35
- let {
36
- locale,
37
- height,
38
- events = [],
39
- style = '',
40
- focusDate,
41
- oneventclick,
42
- selectedEventId = null,
43
- }: Props = $props();
44
-
45
- const clock = createClock();
46
- const viewState = getContext<ViewState>('calendar:viewState') as ViewState | undefined;
47
-
48
- // ── Format helpers ──────────────────────────────────
49
- function fmtTime(d: Date): string {
50
- return _fmtTime(d, locale);
51
- }
52
-
53
- function duration(ev: TimelineEvent): string {
54
- return fmtDuration(ev.start, ev.end);
55
- }
56
-
57
- function timeUntilMs(ms: number): string {
58
- const diff = ms - clock.tick;
59
- if (diff <= 0) return L.now;
60
- const tMins = Math.floor(diff / 60000);
61
- if (tMins < 60) return `in ${tMins}m`;
62
- const hrs = Math.floor(tMins / 60);
63
- const rm = tMins % 60;
64
- if (hrs < 24) return rm > 0 ? `in ${hrs}h ${rm}m` : `in ${hrs}h`;
65
- const days = Math.floor(hrs / 24);
66
- return `in ${days}d`;
67
- }
68
-
69
- function progress(ev: TimelineEvent): number {
70
- const s = ev.start.getTime();
71
- const e = ev.end.getTime();
72
- return Math.min(1, Math.max(0, (clock.tick - s) / (e - s)));
73
- }
74
-
75
- // ── Overlap grouping ────────────────────────────────
76
- interface TimeSlot {
77
- startMs: number;
78
- endMs: number;
79
- events: TimelineEvent[];
80
- }
81
-
82
- function groupIntoSlots(evts: TimelineEvent[]): TimeSlot[] {
83
- const sorted = [...evts].sort((a, b) => a.start.getTime() - b.start.getTime());
84
- const slots: TimeSlot[] = [];
85
- for (const ev of sorted) {
86
- const last = slots[slots.length - 1];
87
- if (last && ev.start.getTime() < last.endMs) {
88
- last.events.push(ev);
89
- last.endMs = Math.max(last.endMs, ev.end.getTime());
90
- } else {
91
- slots.push({
92
- startMs: ev.start.getTime(),
93
- endMs: ev.end.getTime(),
94
- events: [ev],
95
- });
96
- }
97
- }
98
- return slots;
99
- }
100
-
101
- // ── Event handlers ──────────────────────────────────
102
- function handleClick(ev: TimelineEvent): void {
103
- oneventclick?.(ev);
104
- }
105
-
106
- function handleKeydown(e: KeyboardEvent, ev: TimelineEvent): void {
107
- if (e.key === 'Enter' || e.key === ' ') {
108
- e.preventDefault();
109
- oneventclick?.(ev);
110
- }
111
- }
112
-
113
- // ── Day derivations ─────────────────────────────────
114
- const dayMs = $derived(focusDate ? sod(focusDate.getTime()) : clock.today);
115
- const dayEnd = $derived(dayMs + DAY_MS);
116
- const isToday = $derived(dayMs === clock.today);
117
- const isPastDay = $derived(dayMs < clock.today);
118
-
119
- const dateLabel = $derived(
120
- new Date(dayMs).toLocaleDateString(locale ?? 'en-US', {
121
- weekday: 'long',
122
- month: 'long',
123
- day: 'numeric',
124
- })
125
- );
126
-
127
- /** All events for this day, sorted chronologically */
128
- const dayEvents = $derived.by((): TimelineEvent[] => {
129
- return events
130
- .filter((ev) => ev.start.getTime() < dayEnd && ev.end.getTime() > dayMs)
131
- .sort((a, b) => a.start.getTime() - b.start.getTime());
132
- });
133
-
134
- /** All-day / multi-day events shown in a separate strip */
135
- const allDayBanner = $derived(dayEvents.filter((ev) => isAllDay(ev) || isMultiDay(ev)));
136
-
137
- /** Timed events (non-all-day) for normal slot rendering */
138
- const timedDayEvents = $derived(dayEvents.filter((ev) => !isAllDay(ev) && !isMultiDay(ev)));
139
-
140
- const dayCat = $derived.by(() => {
141
- const now = clock.tick;
142
- const past: TimelineEvent[] = [];
143
- const current: TimelineEvent[] = [];
144
- const upcoming: TimelineEvent[] = [];
145
- for (const ev of timedDayEvents) {
146
- const s = ev.start.getTime();
147
- const e = ev.end.getTime();
148
- if (e <= now) past.push(ev);
149
- else if (s <= now && e > now) current.push(ev);
150
- else upcoming.push(ev);
151
- }
152
- return { past, current, upcomingSlots: groupIntoSlots(upcoming), totalUp: upcoming.length };
153
- });
154
-
155
- /** Flat list of next upcoming events (max 5) for the "Up next" column */
156
- const upcomingNext = $derived.by((): TimelineEvent[] => {
157
- const all: TimelineEvent[] = [];
158
- for (const slot of dayCat.upcomingSlots) {
159
- for (const ev of slot.events) {
160
- all.push(ev);
161
- if (all.length >= 5) return all;
162
- }
163
- }
164
- return all;
165
- });
166
- </script>
167
-
168
- <div
169
- class="ag ag--day"
170
- style={style || undefined}
171
- style:height={height ? `${height}px` : undefined}
172
- >
173
- <div class="ag-body" role="list" aria-label={L.todaysLineup}>
174
- {#if allDayBanner.length > 0}
175
- <!-- ─── All-day / multi-day events ─── -->
176
- <div class="ag-allday">
177
- <div class="ag-allday-label">{L.allDay}</div>
178
- <div class="ag-allday-items">
179
- {#each allDayBanner as ev (ev.id)}
180
- <div
181
- class="ag-allday-chip"
182
- class:ag-allday-chip--selected={selectedEventId === ev.id}
183
- style:--ev-color={ev.color || 'var(--dt-accent)'}
184
- role="button"
185
- tabindex="0"
186
- aria-label="{ev.title}, {L.allDay}"
187
- onclick={() => handleClick(ev)}
188
- onkeydown={(e) => handleKeydown(e, ev)}
189
- >
190
- <span class="ag-allday-dot"></span>
191
- <span class="ag-allday-title">{ev.title}</span>
192
- </div>
193
- {/each}
194
- </div>
195
- </div>
196
- {/if}
197
-
198
- {#if isToday}
199
- <!-- ─── Today: "The Queue" — upcoming is the hero ─── -->
200
- <div class="ag-q">
201
- <!-- NOW column: past events stacked above NOW strip -->
202
- <div class="ag-q-status">
203
- {#if dayCat.past.length > 0}
204
- <div class="ag-q-done-section">
205
- <div class="ag-q-label">{L.done}</div>
206
- {#each dayCat.past as ev (ev.id)}
207
- <div
208
- class="ag-q-done-item"
209
- class:ag-q-done-item--selected={selectedEventId === ev.id}
210
- role="button"
211
- tabindex="0"
212
- aria-label="{ev.title}, {L.completed}, {fmtTime(ev.start)}"
213
- onclick={() => handleClick(ev)}
214
- onkeydown={(e) => handleKeydown(e, ev)}
215
- >
216
- <span class="ag-q-done-check">✓</span>
217
- <span class="ag-q-done-title">{ev.title}</span>
218
- </div>
219
- {/each}
220
- </div>
221
- {/if}
222
-
223
- <div class="ag-q-label">{L.now} <span class="ag-q-clock">{clock.hm}</span></div>
224
- {#if dayCat.current.length > 0}
225
- {#each dayCat.current as ev (ev.id)}
226
- <div
227
- class="ag-q-now"
228
- class:ag-q-now--selected={selectedEventId === ev.id}
229
- style:--ev-color={ev.color || 'var(--dt-accent)'}
230
- role="button"
231
- tabindex="0"
232
- aria-label="{ev.title}, {L.happeningNow}, {L.percentComplete(Math.round(progress(ev) * 100))}"
233
- onclick={() => handleClick(ev)}
234
- onkeydown={(e) => handleKeydown(e, ev)}
235
- >
236
- <div class="ag-q-now-dot"></div>
237
- <div class="ag-q-now-title">{ev.title}</div>
238
- <div class="ag-q-now-time">{L.until} {fmtTime(ev.end)}</div>
239
- <div class="ag-q-now-track">
240
- <div class="ag-q-now-fill" style:width="{progress(ev) * 100}%"></div>
241
- </div>
242
- </div>
243
- {/each}
244
- {:else}
245
- <div class="ag-q-free">
246
- <div class="ag-q-free-label">{L.free}</div>
247
- </div>
248
- {/if}
249
- </div>
250
-
251
- <!-- NEXT: the hero center column -->
252
- <div class="ag-q-queue">
253
- <div class="ag-q-label">{L.upNext}</div>
254
- {#if upcomingNext.length === 0}
255
- <div class="ag-q-empty">
256
- {dayCat.past.length > 0 ? L.allDoneForToday : L.nothingScheduled}
257
- </div>
258
- {:else}
259
- {#each upcomingNext as ev, i (ev.id)}
260
- <div
261
- class="ag-card ag-card--q"
262
- class:ag-card--hero={i === 0}
263
- class:ag-card--selected={selectedEventId === ev.id}
264
- style:--ev-color={ev.color || 'var(--dt-accent)'}
265
- role="button"
266
- tabindex="0"
267
- aria-label="{ev.title}, {fmtTime(ev.start)}, {duration(ev)}"
268
- onclick={() => handleClick(ev)}
269
- onkeydown={(e) => handleKeydown(e, ev)}
270
- >
271
- <div class="ag-card-stripe"></div>
272
- <div class="ag-card-body">
273
- <div class="ag-card-top">
274
- <span class="ag-card-title">{ev.title}</span>
275
- <span class="ag-card-eta">{timeUntilMs(ev.start.getTime())}</span>
276
- </div>
277
- {#if ev.subtitle}
278
- <span class="ag-card-sub">{ev.subtitle}</span>
279
- {/if}
280
- <div class="ag-card-meta">
281
- {fmtTime(ev.start)} – {fmtTime(ev.end)}
282
- <span class="ag-card-dur">{duration(ev)}</span>
283
- </div>
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
- </div>
292
- </div>
293
- {/each}
294
- {/if}
295
- </div>
296
- </div>
297
-
298
- {:else if isPastDay}
299
- <!-- ─── Past day: "The Log" — everything happened ─── -->
300
- <div class="ag-log">
301
- {#if timedDayEvents.length === 0 && allDayBanner.length === 0}
302
- <div class="ag-q-empty">{L.nothingWasScheduled}</div>
303
- {:else}
304
- {#each timedDayEvents as ev (ev.id)}
305
- <div
306
- class="ag-log-row"
307
- class:ag-log-row--selected={selectedEventId === ev.id}
308
- style:--ev-color={ev.color || 'var(--dt-accent)'}
309
- role="button"
310
- tabindex="0"
311
- aria-label="{ev.title}, {fmtTime(ev.start)} to {fmtTime(ev.end)}"
312
- onclick={() => handleClick(ev)}
313
- onkeydown={(e) => handleKeydown(e, ev)}
314
- >
315
- <span class="ag-log-check">✓</span>
316
- <span class="ag-log-time">{fmtTime(ev.start)}</span>
317
- <span class="ag-log-dot" style:background={ev.color || 'var(--dt-accent)'}></span>
318
- <span class="ag-log-title">{ev.title}</span>
319
- <span class="ag-log-dur">{duration(ev)}</span>
320
- </div>
321
- {/each}
322
- {/if}
323
- </div>
324
-
325
- {:else}
326
- <!-- ─── Future day: "The Plan" — everything is ahead ─── -->
327
- <div class="ag-plan">
328
- {#if timedDayEvents.length === 0 && allDayBanner.length === 0}
329
- <div class="ag-q-empty">{L.nothingScheduledYet}</div>
330
- {:else}
331
- {#each timedDayEvents as ev, i (ev.id)}
332
- <div
333
- class="ag-card ag-card--plan"
334
- class:ag-card--first={i === 0}
335
- class:ag-card--selected={selectedEventId === ev.id}
336
- style:--ev-color={ev.color || 'var(--dt-accent)'}
337
- role="button"
338
- tabindex="0"
339
- aria-label="{ev.title}, {fmtTime(ev.start)} to {fmtTime(ev.end)}, {duration(ev)}"
340
- onclick={() => handleClick(ev)}
341
- onkeydown={(e) => handleKeydown(e, ev)}
342
- >
343
- <div class="ag-card-stripe"></div>
344
- <div class="ag-card-body">
345
- <div class="ag-card-top">
346
- <span class="ag-card-order">{i + 1}</span>
347
- <span class="ag-card-title">{ev.title}</span>
348
- </div>
349
- {#if ev.subtitle}
350
- <span class="ag-card-sub">{ev.subtitle}</span>
351
- {/if}
352
- <div class="ag-card-meta">
353
- {fmtTime(ev.start)} – {fmtTime(ev.end)}
354
- <span class="ag-card-dur">{duration(ev)}</span>
355
- </div>
356
- {#if ev.tags?.length}
357
- <div class="ag-card-tags">
358
- {#each ev.tags as tag}
359
- <span class="ag-card-tag">{tag}</span>
360
- {/each}
361
- </div>
362
- {/if}
363
- </div>
364
- </div>
365
- {/each}
366
- {/if}
367
- </div>
368
- {/if}
369
- </div>
370
-
371
- <div class="ag-date-label">{dateLabel}</div>
372
-
373
- <!-- ── Floating nav pills ── -->
374
- <nav class="ag-nav" aria-label={L.dayNavigation}>
375
- <button
376
- class="ag-nav-pill ag-nav-today"
377
- class:ag-nav-today--hidden={isToday}
378
- onclick={() => viewState?.goToday()}
379
- aria-label={L.goToToday}
380
- tabindex={isToday ? -1 : 0}
381
- >
382
- {L.today}
383
- </button>
384
- <button
385
- class="ag-nav-pill"
386
- onclick={() => viewState?.prev()}
387
- aria-label={L.previousDay}
388
- >
389
- <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>
390
- </button>
391
- <button
392
- class="ag-nav-pill"
393
- onclick={() => viewState?.next()}
394
- aria-label={L.nextDay}
395
- >
396
- <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>
397
- </button>
398
- </nav>
399
- </div>
400
-
401
- <style>
402
- /* ═══ Floating date label ═══ */
403
- .ag-date-label {
404
- position: absolute;
405
- top: 10px;
406
- left: 50%;
407
- transform: translateX(-50%);
408
- z-index: 20;
409
- font: 600 11px/1 var(--dt-sans, 'Outfit', system-ui, sans-serif);
410
- letter-spacing: 0.04em;
411
- text-transform: uppercase;
412
- color: var(--dt-text, rgba(226, 232, 240, 0.85));
413
- background: color-mix(in srgb, var(--dt-surface, #10141c) 85%, transparent);
414
- backdrop-filter: blur(6px);
415
- -webkit-backdrop-filter: blur(6px);
416
- padding: 8px 16px;
417
- border-radius: 8px;
418
- border: 1px solid var(--dt-border, rgba(148, 163, 184, 0.07));
419
- pointer-events: none;
420
- white-space: nowrap;
421
- }
422
-
423
- /* ═══ Floating nav pills ═══ */
424
- .ag-nav {
425
- position: absolute;
426
- top: 10px;
427
- right: 14px;
428
- z-index: 20;
429
- display: flex;
430
- gap: 2px;
431
- background: color-mix(in srgb, var(--dt-surface, #10141c) 85%, transparent);
432
- backdrop-filter: blur(6px);
433
- -webkit-backdrop-filter: blur(6px);
434
- border-radius: 8px;
435
- padding: 2px;
436
- border: 1px solid var(--dt-border, rgba(148, 163, 184, 0.07));
437
- }
438
- .ag-nav-pill {
439
- border: none;
440
- background: transparent;
441
- color: var(--dt-text-2, rgba(148, 163, 184, 0.55));
442
- cursor: pointer;
443
- font: 600 11px / 1 var(--dt-sans, 'Outfit', system-ui, sans-serif);
444
- padding: 6px 12px;
445
- border-radius: 6px;
446
- letter-spacing: 0.04em;
447
- text-transform: uppercase;
448
- transition: background 100ms, color 100ms;
449
- display: inline-flex;
450
- align-items: center;
451
- justify-content: center;
452
- }
453
- .ag-nav-pill:hover {
454
- color: var(--dt-text, rgba(226, 232, 240, 0.85));
455
- }
456
- .ag-nav-today {
457
- max-width: 60px;
458
- overflow: hidden;
459
- white-space: nowrap;
460
- transition: max-width 250ms ease, padding 250ms ease, opacity 200ms ease;
461
- }
462
- .ag-nav-today--hidden {
463
- max-width: 0;
464
- padding-left: 0;
465
- padding-right: 0;
466
- opacity: 0;
467
- pointer-events: none;
468
- }
469
- .ag-nav-pill:focus-visible {
470
- outline: 2px solid color-mix(in srgb, var(--dt-accent, #ef4444) 55%, transparent);
471
- outline-offset: 2px;
472
- }
473
-
474
- /* ═══ Container ═══ */
475
- .ag {
476
- position: relative;
477
- overflow: hidden;
478
- user-select: none;
479
- display: flex;
480
- flex-direction: column;
481
- height: 100%;
482
- color: var(--dt-text, rgba(255, 255, 255, 0.92));
483
- font-family: var(--dt-sans, system-ui, sans-serif);
484
- }
485
-
486
- /* ═══ Body ═══ */
487
- .ag-body {
488
- flex: 1;
489
- display: flex;
490
- flex-direction: column;
491
- overflow-y: auto;
492
- padding-top: 44px;
493
- scrollbar-width: thin;
494
- scrollbar-color: var(--dt-border) transparent;
495
- }
496
- .ag-body::-webkit-scrollbar {
497
- width: 4px;
498
- }
499
- .ag-body::-webkit-scrollbar-thumb {
500
- background: var(--dt-border);
501
- border-radius: 2px;
502
- }
503
-
504
- /* ═══ All-day strip ═══ */
505
- .ag-allday {
506
- display: flex;
507
- align-items: center;
508
- gap: 8px;
509
- padding: 6px 16px;
510
- border-bottom: 1px solid var(--dt-border, rgba(148, 163, 184, 0.07));
511
- }
512
- .ag-allday-label {
513
- font: 600 10px/1 var(--dt-sans, system-ui, sans-serif);
514
- text-transform: uppercase;
515
- letter-spacing: 0.06em;
516
- color: var(--dt-text-2, rgba(148, 163, 184, 0.55));
517
- white-space: nowrap;
518
- flex-shrink: 0;
519
- }
520
- .ag-allday-items {
521
- display: flex;
522
- flex-wrap: wrap;
523
- gap: 6px;
524
- }
525
- .ag-allday-chip {
526
- display: flex;
527
- align-items: center;
528
- gap: 5px;
529
- padding: 3px 10px;
530
- border-radius: 6px;
531
- background: color-mix(in srgb, var(--ev-color) 12%, var(--dt-surface, #10141c));
532
- border: 1px solid color-mix(in srgb, var(--ev-color) 20%, transparent);
533
- cursor: pointer;
534
- transition: background 0.15s, border-color 0.15s;
535
- }
536
- .ag-allday-chip:hover {
537
- background: color-mix(in srgb, var(--ev-color) 22%, var(--dt-surface, #10141c));
538
- border-color: color-mix(in srgb, var(--ev-color) 35%, transparent);
539
- }
540
- .ag-allday-chip:focus-visible {
541
- outline: 2px solid var(--dt-accent, #ff6b4a);
542
- outline-offset: 2px;
543
- }
544
- .ag-allday-chip--selected {
545
- border-color: var(--ev-color);
546
- background: color-mix(in srgb, var(--ev-color) 18%, var(--dt-surface, #10141c));
547
- }
548
- .ag-allday-dot {
549
- width: 6px;
550
- height: 6px;
551
- border-radius: 50%;
552
- background: var(--ev-color);
553
- flex-shrink: 0;
554
- }
555
- .ag-allday-title {
556
- font: 500 0.75rem/1.2 var(--dt-sans, system-ui, sans-serif);
557
- color: var(--dt-text, rgba(226, 232, 240, 0.85));
558
- white-space: nowrap;
559
- }
560
-
561
- /* ═══ Shared: event card ═══ */
562
- .ag-card {
563
- display: flex;
564
- align-items: stretch;
565
- border-radius: 10px;
566
- background: var(--dt-surface, #191919);
567
- border: 1px solid var(--dt-border, rgba(255, 255, 255, 0.06));
568
- overflow: hidden;
569
- cursor: pointer;
570
- transition: border-color 150ms;
571
- }
572
- .ag-card:hover {
573
- border-color: color-mix(in srgb, var(--ev-color) 40%, transparent);
574
- }
575
- .ag-card:focus-visible {
576
- outline: 2px solid var(--dt-accent, #ff6b4a);
577
- outline-offset: 2px;
578
- }
579
- .ag-card--selected {
580
- border-color: var(--ev-color);
581
- background: color-mix(in srgb, var(--ev-color) 6%, var(--dt-surface, #191919));
582
- }
583
- .ag-card-stripe {
584
- width: 3px;
585
- background: var(--ev-color, var(--dt-accent));
586
- flex-shrink: 0;
587
- }
588
- .ag-card-body {
589
- padding: 10px 12px;
590
- display: flex;
591
- flex-direction: column;
592
- gap: 4px;
593
- min-width: 0;
594
- flex: 1;
595
- }
596
- .ag-card-top {
597
- display: flex;
598
- justify-content: space-between;
599
- align-items: flex-start;
600
- gap: 8px;
601
- }
602
- .ag-card-title {
603
- font-size: 13px;
604
- font-weight: 600;
605
- line-height: 1.2;
606
- white-space: nowrap;
607
- overflow: hidden;
608
- text-overflow: ellipsis;
609
- flex: 1;
610
- }
611
- .ag-card-meta {
612
- font-size: 11px;
613
- color: var(--dt-text-2, rgba(255, 255, 255, 0.5));
614
- font-family: var(--dt-mono, monospace);
615
- line-height: 1;
616
- }
617
- .ag-card-dur {
618
- margin-left: 6px;
619
- color: var(--dt-text-3, rgba(255, 255, 255, 0.3));
620
- }
621
- .ag-card-sub {
622
- font-size: 11px;
623
- color: var(--dt-text-2, rgba(255, 255, 255, 0.45));
624
- line-height: 1;
625
- }
626
- .ag-card-tags {
627
- display: flex;
628
- gap: 4px;
629
- flex-wrap: wrap;
630
- }
631
- .ag-card-tag {
632
- font: 500 9px / 1 var(--dt-sans, system-ui, sans-serif);
633
- color: var(--ev-color, var(--dt-accent));
634
- background: color-mix(in srgb, var(--ev-color, var(--dt-accent)) 15%, transparent);
635
- padding: 2px 5px;
636
- border-radius: 3px;
637
- white-space: nowrap;
638
- }
639
-
640
- /* ── Queue card variant ── */
641
- .ag-card--q {
642
- margin-bottom: 6px;
643
- transition: border-color 150ms, transform 100ms;
644
- }
645
- .ag-card--q .ag-card-stripe {
646
- width: 3.5px;
647
- }
648
- .ag-card--q .ag-card-body {
649
- gap: 3px;
650
- }
651
- .ag-card--q .ag-card-tags {
652
- margin-top: 2px;
653
- }
654
- .ag-card-eta {
655
- font-size: 9px;
656
- font-weight: 600;
657
- letter-spacing: 0.04em;
658
- color: var(--dt-accent, #ff6b4a);
659
- flex-shrink: 0;
660
- white-space: nowrap;
661
- }
662
- .ag-card--hero {
663
- background: color-mix(in srgb, var(--ev-color) 8%, var(--dt-surface, #191919));
664
- border-color: color-mix(in srgb, var(--ev-color) 25%, transparent);
665
- }
666
- .ag-card--hero .ag-card-title {
667
- font-size: 16px;
668
- font-weight: 700;
669
- }
670
- .ag-card--hero .ag-card-eta {
671
- font-size: 11px;
672
- background: color-mix(in srgb, var(--dt-accent, #ff6b4a) 18%, transparent);
673
- padding: 2px 7px;
674
- border-radius: 4px;
675
- }
676
- .ag-card--hero .ag-card-body {
677
- padding: 14px 16px;
678
- }
679
-
680
- /* ── Plan card variant ── */
681
- .ag-card--plan .ag-card-stripe {
682
- width: 3.5px;
683
- }
684
- .ag-card--plan .ag-card-body {
685
- padding: 12px 14px;
686
- gap: 3px;
687
- }
688
- .ag-card--plan .ag-card-top {
689
- align-items: baseline;
690
- }
691
- .ag-card-order {
692
- font-size: 10px;
693
- font-weight: 700;
694
- color: var(--dt-text-3, rgba(255, 255, 255, 0.2));
695
- font-family: var(--dt-mono, monospace);
696
- flex-shrink: 0;
697
- }
698
- .ag-card--plan .ag-card-title {
699
- font-size: 14px;
700
- }
701
- .ag-card--first {
702
- background: color-mix(in srgb, var(--ev-color) 5%, var(--dt-surface, #191919));
703
- border-color: color-mix(in srgb, var(--ev-color) 15%, transparent);
704
- }
705
- .ag-card--first .ag-card-title {
706
- font-size: 16px;
707
- font-weight: 700;
708
- }
709
- .ag-card--plan .ag-card-sub {
710
- padding-left: 22px;
711
- }
712
- .ag-card--plan .ag-card-tags {
713
- padding-left: 22px;
714
- margin-top: 2px;
715
- }
716
-
717
- /* ═══ The Queue: 2-column grid ═══ */
718
- .ag-q {
719
- display: grid;
720
- grid-template-columns: 1fr 1.8fr;
721
- gap: 0;
722
- flex: 1;
723
- padding: 8px 0 10px;
724
- min-height: 0;
725
- }
726
- .ag-q-label {
727
- font-size: 8px;
728
- font-weight: 600;
729
- letter-spacing: 0.14em;
730
- text-transform: uppercase;
731
- color: var(--dt-text-3, rgba(255, 255, 255, 0.25));
732
- margin-bottom: 8px;
733
- padding: 0 12px;
734
- font-family: var(--dt-sans, system-ui, sans-serif);
735
- }
736
- .ag-q-empty {
737
- display: flex;
738
- align-items: center;
739
- justify-content: center;
740
- flex: 1;
741
- font-size: 13px;
742
- font-weight: 300;
743
- color: var(--dt-text-3, rgba(255, 255, 255, 0.25));
744
- }
745
-
746
- /* ── NOW column (includes Done above) ── */
747
- .ag-q-status {
748
- padding: 0 10px 0 14px;
749
- border-right: 1px solid var(--dt-border, rgba(255, 255, 255, 0.06));
750
- display: flex;
751
- flex-direction: column;
752
- overflow-y: auto;
753
- scrollbar-width: none;
754
- }
755
- .ag-q-status::-webkit-scrollbar {
756
- display: none;
757
- }
758
- .ag-q-done-section {
759
- margin-bottom: 10px;
760
- padding-bottom: 8px;
761
- border-bottom: 1px solid var(--dt-border, rgba(255, 255, 255, 0.06));
762
- }
763
- .ag-q-clock {
764
- font-size: 10px;
765
- font-weight: 600;
766
- font-family: var(--dt-mono, monospace);
767
- color: var(--dt-accent, #ff6b4a);
768
- margin-left: 4px;
769
- }
770
- .ag-q-now {
771
- padding: 8px 10px;
772
- border-radius: 8px;
773
- background: var(--dt-surface, #191919);
774
- border: 1px solid color-mix(in srgb, var(--ev-color, var(--dt-accent)) 15%, transparent);
775
- cursor: pointer;
776
- transition: border-color 150ms;
777
- margin-right: 10px;
778
- }
779
- .ag-q-now:hover {
780
- border-color: color-mix(in srgb, var(--ev-color, var(--dt-accent)) 35%, transparent);
781
- }
782
- .ag-q-now:focus-visible {
783
- outline: 2px solid var(--dt-accent, #ff6b4a);
784
- outline-offset: 2px;
785
- }
786
- .ag-q-now--selected {
787
- border-color: var(--ev-color, var(--dt-accent));
788
- }
789
- .ag-q-now-dot {
790
- width: 6px;
791
- height: 6px;
792
- border-radius: 50%;
793
- background: var(--ev-color, var(--dt-accent, #ff6b4a));
794
- margin-bottom: 6px;
795
- animation: ag-pulse 2.5s ease-in-out infinite;
796
- }
797
- @keyframes ag-pulse {
798
- 0%, 100% { opacity: 1; }
799
- 50% { opacity: 0.4; }
800
- }
801
- .ag-q-now-title {
802
- font-size: 11px;
803
- font-weight: 600;
804
- line-height: 1.2;
805
- color: var(--dt-text, rgba(255, 255, 255, 0.92));
806
- white-space: nowrap;
807
- overflow: hidden;
808
- text-overflow: ellipsis;
809
- margin-bottom: 3px;
810
- }
811
- .ag-q-now-time {
812
- font-size: 9px;
813
- font-family: var(--dt-mono, monospace);
814
- color: var(--dt-text-3, rgba(255, 255, 255, 0.35));
815
- margin-bottom: 6px;
816
- }
817
- .ag-q-now-track {
818
- height: 2px;
819
- background: var(--dt-border, rgba(255, 255, 255, 0.06));
820
- border-radius: 1px;
821
- overflow: hidden;
822
- }
823
- .ag-q-now-fill {
824
- height: 100%;
825
- background: var(--ev-color, var(--dt-accent, #ff6b4a));
826
- border-radius: 1px;
827
- transition: width 1s linear;
828
- }
829
- .ag-q-free {
830
- padding: 8px 10px;
831
- margin-right: 10px;
832
- }
833
- .ag-q-free-label {
834
- font-size: 12px;
835
- font-weight: 300;
836
- color: var(--dt-text-3, rgba(255, 255, 255, 0.25));
837
- margin-bottom: 2px;
838
- }
839
-
840
- /* ── NEXT: hero center column ── */
841
- .ag-q-queue {
842
- padding: 0 16px;
843
- overflow-y: auto;
844
- scrollbar-width: none;
845
- display: flex;
846
- flex-direction: column;
847
- }
848
- .ag-q-queue::-webkit-scrollbar {
849
- display: none;
850
- }
851
-
852
-
853
- .ag-q-done-item {
854
- display: flex;
855
- align-items: center;
856
- gap: 5px;
857
- padding: 3px 0;
858
- opacity: 0.35;
859
- cursor: pointer;
860
- }
861
- .ag-q-done-item:hover {
862
- opacity: 0.6;
863
- }
864
- .ag-q-done-item:focus-visible {
865
- outline: 2px solid var(--dt-accent, #ff6b4a);
866
- outline-offset: 2px;
867
- opacity: 0.6;
868
- }
869
- .ag-q-done-item--selected {
870
- opacity: 0.7;
871
- }
872
- .ag-q-done-check {
873
- font-size: 9px;
874
- color: var(--dt-success, rgba(120, 200, 140, 0.7));
875
- flex-shrink: 0;
876
- }
877
- .ag-q-done-title {
878
- font-size: 10px;
879
- line-height: 1.2;
880
- color: var(--dt-text-3, rgba(255, 255, 255, 0.35));
881
- white-space: nowrap;
882
- overflow: hidden;
883
- text-overflow: ellipsis;
884
- text-decoration: line-through;
885
- text-decoration-color: var(--dt-text-3, rgba(255, 255, 255, 0.15));
886
- }
887
-
888
- /* ═══ Past Day: "The Log" ═══ */
889
- .ag-log {
890
- flex: 1;
891
- padding: 8px 20px 12px;
892
- overflow-y: auto;
893
- scrollbar-width: none;
894
- opacity: 0.7;
895
- }
896
- .ag-log::-webkit-scrollbar {
897
- display: none;
898
- }
899
- .ag-log-row {
900
- display: flex;
901
- align-items: center;
902
- gap: 10px;
903
- padding: 8px 0;
904
- border-bottom: 1px solid var(--dt-border, rgba(255, 255, 255, 0.04));
905
- cursor: pointer;
906
- transition: opacity 150ms;
907
- }
908
- .ag-log-row:last-child {
909
- border-bottom: none;
910
- }
911
- .ag-log-row:hover {
912
- opacity: 1;
913
- }
914
- .ag-log-row:focus-visible {
915
- outline: 2px solid var(--dt-accent, #ff6b4a);
916
- outline-offset: 2px;
917
- }
918
- .ag-log-row--selected {
919
- opacity: 1;
920
- background: color-mix(in srgb, var(--ev-color) 6%, transparent);
921
- border-radius: 6px;
922
- padding-left: 8px;
923
- padding-right: 8px;
924
- }
925
- .ag-log-check {
926
- font-size: 10px;
927
- color: var(--dt-success, rgba(120, 200, 140, 0.5));
928
- flex-shrink: 0;
929
- }
930
- .ag-log-time {
931
- font-size: 11px;
932
- font-family: var(--dt-mono, monospace);
933
- color: var(--dt-text-3, rgba(255, 255, 255, 0.3));
934
- width: 64px;
935
- flex-shrink: 0;
936
- }
937
- .ag-log-dot {
938
- width: 5px;
939
- height: 5px;
940
- border-radius: 50%;
941
- flex-shrink: 0;
942
- opacity: 0.6;
943
- }
944
- .ag-log-title {
945
- font-size: 13px;
946
- font-weight: 500;
947
- line-height: 1.2;
948
- color: var(--dt-text-2, rgba(255, 255, 255, 0.55));
949
- flex: 1;
950
- white-space: nowrap;
951
- overflow: hidden;
952
- text-overflow: ellipsis;
953
- text-decoration: line-through;
954
- text-decoration-color: var(--dt-border, rgba(255, 255, 255, 0.08));
955
- }
956
- .ag-log-dur {
957
- font-size: 10px;
958
- font-family: var(--dt-mono, monospace);
959
- color: var(--dt-text-3, rgba(255, 255, 255, 0.2));
960
- flex-shrink: 0;
961
- }
962
-
963
- /* ═══ Future Day: "The Plan" ═══ */
964
- .ag-plan {
965
- flex: 1;
966
- padding: 8px 20px 12px;
967
- overflow-y: auto;
968
- scrollbar-width: none;
969
- display: flex;
970
- flex-direction: column;
971
- gap: 6px;
972
- }
973
- .ag-plan::-webkit-scrollbar {
974
- display: none;
975
- }
976
- </style>
1
+ <script lang="ts">
2
+ /**
3
+ * AgendaDay — single-day agenda view.
4
+ *
5
+ * Today ("The Queue"):
6
+ * 3-column layout: Done | Now | Up next (hero).
7
+ * Answers: "What's coming up next?"
8
+ *
9
+ * Past day ("The Log"):
10
+ * Quiet chronological record of completed events.
11
+ *
12
+ * Future day ("The Plan"):
13
+ * Clean numbered schedule list.
14
+ */
15
+ import { getContext } from 'svelte';
16
+ import { createClock } from '../../core/clock.svelte.js';
17
+ import type { TimelineEvent } from '../../core/types.js';
18
+ import { sod, DAY_MS, isAllDay, isMultiDay } from '../../core/time.js';
19
+ import { fmtTime as _fmtTime, fmtDuration, getLabels } from '../../core/locale.js';
20
+ import type { ViewState } from '../../engine/view-state.svelte.js';
21
+
22
+ const L = $derived(getLabels());
23
+
24
+ interface Props {
25
+ locale?: string;
26
+ height?: number;
27
+ events?: TimelineEvent[];
28
+ style?: string;
29
+ focusDate?: Date;
30
+ oneventclick?: (event: TimelineEvent) => void;
31
+ selectedEventId?: string | null;
32
+ [key: string]: unknown;
33
+ }
34
+
35
+ let {
36
+ locale,
37
+ height,
38
+ events = [],
39
+ style = '',
40
+ focusDate,
41
+ oneventclick,
42
+ selectedEventId = null,
43
+ }: Props = $props();
44
+
45
+ const clock = createClock();
46
+ const viewState = getContext<ViewState>('calendar:viewState') as ViewState | undefined;
47
+ const showNavCtx = getContext<{ current: boolean }>('calendar:showNavigation') as { current: boolean } | undefined;
48
+ const equalDaysCtx = getContext<{ current: boolean }>('calendar:equalDays') as { current: boolean } | undefined;
49
+ const showDatesCtx = getContext<{ current: boolean }>('calendar:showDates') as { current: boolean } | 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 isMobile = $derived(mobileCtx?.current ?? false);
56
+ const autoHeight = $derived(autoHeightCtx?.current ?? false);
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
+ // ── New feature contexts ──
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
+ const oneventhover = $derived(callbacksCtx?.oneventhover);
83
+ const disabledDates = $derived(disabledDatesCtx?.current);
84
+ const disabledSet = $derived(new Set(disabledDates?.map(d => sod(d.getTime())) ?? []));
85
+
86
+ // ── Format helpers ──────────────────────────────────
87
+ function fmtTime(d: Date): string {
88
+ return _fmtTime(d, locale);
89
+ }
90
+
91
+ function duration(ev: TimelineEvent): string {
92
+ return fmtDuration(ev.start, ev.end);
93
+ }
94
+
95
+ function timeUntilMs(ms: number): string {
96
+ const diff = ms - clock.tick;
97
+ if (diff <= 0) return L.now;
98
+ const tMins = Math.floor(diff / 60000);
99
+ if (tMins < 60) return `in ${tMins}m`;
100
+ const hrs = Math.floor(tMins / 60);
101
+ const rm = tMins % 60;
102
+ if (hrs < 24) return rm > 0 ? `in ${hrs}h ${rm}m` : `in ${hrs}h`;
103
+ const days = Math.floor(hrs / 24);
104
+ return `in ${days}d`;
105
+ }
106
+
107
+ function progress(ev: TimelineEvent): number {
108
+ const s = ev.start.getTime();
109
+ const e = ev.end.getTime();
110
+ return Math.min(1, Math.max(0, (clock.tick - s) / (e - s)));
111
+ }
112
+
113
+ // ── Overlap grouping ────────────────────────────────
114
+ interface TimeSlot {
115
+ startMs: number;
116
+ endMs: number;
117
+ events: TimelineEvent[];
118
+ }
119
+
120
+ function groupIntoSlots(evts: TimelineEvent[]): TimeSlot[] {
121
+ const sorted = [...evts].sort((a, b) => a.start.getTime() - b.start.getTime());
122
+ const slots: TimeSlot[] = [];
123
+ for (const ev of sorted) {
124
+ const last = slots[slots.length - 1];
125
+ if (last && ev.start.getTime() < last.endMs) {
126
+ last.events.push(ev);
127
+ last.endMs = Math.max(last.endMs, ev.end.getTime());
128
+ } else {
129
+ slots.push({
130
+ startMs: ev.start.getTime(),
131
+ endMs: ev.end.getTime(),
132
+ events: [ev],
133
+ });
134
+ }
135
+ }
136
+ return slots;
137
+ }
138
+
139
+ // ── Event handlers ──────────────────────────────────
140
+ function handleClick(ev: TimelineEvent): void {
141
+ oneventclick?.(ev);
142
+ }
143
+
144
+ function handleKeydown(e: KeyboardEvent, ev: TimelineEvent): void {
145
+ if (e.key === 'Enter' || e.key === ' ') {
146
+ e.preventDefault();
147
+ oneventclick?.(ev);
148
+ }
149
+ }
150
+
151
+ // ── Day derivations ─────────────────────────────────
152
+ const dayMs = $derived(focusDate ? sod(focusDate.getTime()) : clock.today);
153
+ const dayEnd = $derived(dayMs + DAY_MS);
154
+ const isToday = $derived(dayMs === clock.today);
155
+ const isPastDay = $derived(equalDays ? false : dayMs < clock.today);
156
+
157
+ const dateLabel = $derived(
158
+ showDates
159
+ ? new Date(dayMs).toLocaleDateString(locale ?? 'en-US', {
160
+ weekday: 'long',
161
+ month: 'long',
162
+ day: 'numeric',
163
+ })
164
+ : new Date(dayMs).toLocaleDateString(locale ?? 'en-US', {
165
+ weekday: 'long',
166
+ })
167
+ );
168
+
169
+ /** All events for this day, sorted chronologically */
170
+ const dayEvents = $derived.by((): TimelineEvent[] => {
171
+ return events
172
+ .filter((ev) => ev.start.getTime() < dayEnd && ev.end.getTime() > dayMs)
173
+ .sort((a, b) => a.start.getTime() - b.start.getTime());
174
+ });
175
+
176
+ /** All-day / multi-day events shown in a separate strip */
177
+ const allDayBanner = $derived(dayEvents.filter((ev) => isAllDay(ev) || isMultiDay(ev)));
178
+
179
+ /** Timed events (non-all-day) for normal slot rendering */
180
+ const timedDayEvents = $derived(dayEvents.filter((ev) => !isAllDay(ev) && !isMultiDay(ev)));
181
+
182
+ const dayCat = $derived.by(() => {
183
+ const now = clock.tick;
184
+ const past: TimelineEvent[] = [];
185
+ const current: TimelineEvent[] = [];
186
+ const upcoming: TimelineEvent[] = [];
187
+ for (const ev of timedDayEvents) {
188
+ const s = ev.start.getTime();
189
+ const e = ev.end.getTime();
190
+ if (e <= now) past.push(ev);
191
+ else if (s <= now && e > now) current.push(ev);
192
+ else upcoming.push(ev);
193
+ }
194
+ return { past, current, upcomingSlots: groupIntoSlots(upcoming), totalUp: upcoming.length };
195
+ });
196
+
197
+ /** Flat list of next upcoming events (max 5) for the "Up next" column */
198
+ const upcomingNext = $derived.by((): TimelineEvent[] => {
199
+ const all: TimelineEvent[] = [];
200
+ for (const slot of dayCat.upcomingSlots) {
201
+ for (const ev of slot.events) {
202
+ all.push(ev);
203
+ if (all.length >= 5) return all;
204
+ }
205
+ }
206
+ return all;
207
+ });
208
+ </script>
209
+
210
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
211
+ <div
212
+ class="ag ag--day"
213
+ class:ag--disabled={disabledSet.has(dayMs)}
214
+ class:ag--mobile={isMobile}
215
+ class:ag--auto={autoHeight}
216
+ style={style || undefined}
217
+ style:height={height ? `${height}px` : undefined}
218
+ onpointerdown={onPointerDown}
219
+ onpointerup={onPointerUp}
220
+ >
221
+ <div class="ag-body" role="list" aria-label={L.todaysLineup}>
222
+ {#if allDayBanner.length > 0}
223
+ <!-- ─── All-day / multi-day events ─── -->
224
+ <div class="ag-allday">
225
+ <div class="ag-allday-label">{L.allDay}</div>
226
+ <div class="ag-allday-items">
227
+ {#each allDayBanner as ev (ev.id)}
228
+ <div
229
+ class="ag-allday-chip"
230
+ class:ag-allday-chip--selected={selectedEventId === ev.id}
231
+ style:--ev-color={ev.color || 'var(--dt-accent)'}
232
+ role="button"
233
+ tabindex="0"
234
+ aria-label="{ev.title}, {L.allDay}"
235
+ onclick={() => handleClick(ev)}
236
+ onpointerenter={() => oneventhover?.(ev)}
237
+ onkeydown={(e) => handleKeydown(e, ev)}
238
+ >
239
+ <span class="ag-allday-dot"></span>
240
+ <span class="ag-allday-title">{ev.title}</span>
241
+ </div>
242
+ {/each}
243
+ </div>
244
+ </div>
245
+ {/if}
246
+
247
+ {#if isToday}
248
+ <!-- ─── Today: "The Queue" — upcoming is the hero ─── -->
249
+ <div class="ag-q">
250
+ <!-- NOW column: past events stacked above NOW strip -->
251
+ <div class="ag-q-status">
252
+ {#if dayCat.past.length > 0}
253
+ <div class="ag-q-done-section">
254
+ <div class="ag-q-label">{L.done}</div>
255
+ {#each dayCat.past as ev (ev.id)}
256
+ <div
257
+ class="ag-q-done-item"
258
+ class:ag-q-done-item--selected={selectedEventId === ev.id}
259
+ role="button"
260
+ tabindex="0"
261
+ aria-label="{ev.title}, {L.completed}, {fmtTime(ev.start)}"
262
+ onclick={() => handleClick(ev)}
263
+ onkeydown={(e) => handleKeydown(e, ev)}
264
+ >
265
+ <span class="ag-q-done-check">✓</span>
266
+ <span class="ag-q-done-title">{ev.title}</span>
267
+ </div>
268
+ {/each}
269
+ </div>
270
+ {/if}
271
+
272
+ <div class="ag-q-label">{L.now} <span class="ag-q-clock">{clock.hm}</span></div>
273
+ {#if dayCat.current.length > 0}
274
+ {#each dayCat.current as ev (ev.id)}
275
+ <div
276
+ class="ag-q-now"
277
+ class:ag-q-now--selected={selectedEventId === ev.id}
278
+ style:--ev-color={ev.color || 'var(--dt-accent)'}
279
+ role="button"
280
+ tabindex="0"
281
+ aria-label="{ev.title}, {L.happeningNow}, {L.percentComplete(Math.round(progress(ev) * 100))}"
282
+ onclick={() => handleClick(ev)} onpointerenter={() => oneventhover?.(ev)} onkeydown={(e) => handleKeydown(e, ev)}
283
+ >
284
+ <div class="ag-q-now-dot"></div>
285
+ <div class="ag-q-now-title">{ev.title}</div>
286
+ <div class="ag-q-now-time">{L.until} {fmtTime(ev.end)}</div>
287
+ <div class="ag-q-now-track">
288
+ <div class="ag-q-now-fill" style:width="{progress(ev) * 100}%"></div>
289
+ </div>
290
+ </div>
291
+ {/each}
292
+ {:else}
293
+ <div class="ag-q-free">
294
+ <div class="ag-q-free-label">{L.free}</div>
295
+ </div>
296
+ {/if}
297
+ </div>
298
+
299
+ <!-- NEXT: the hero center column -->
300
+ <div class="ag-q-queue">
301
+ <div class="ag-q-label">{L.upNext}</div>
302
+ {#if upcomingNext.length === 0}
303
+ <div class="ag-q-empty">
304
+ {dayCat.past.length > 0 ? L.allDoneForToday : L.nothingScheduled}
305
+ </div>
306
+ {:else}
307
+ {#each upcomingNext as ev, i (ev.id)}
308
+ <div
309
+ class="ag-card ag-card--q"
310
+ class:ag-card--hero={i === 0}
311
+ class:ag-card--selected={selectedEventId === ev.id}
312
+ style:--ev-color={ev.color || 'var(--dt-accent)'}
313
+ role="button"
314
+ tabindex="0"
315
+ aria-label="{ev.title}, {fmtTime(ev.start)}, {duration(ev)}"
316
+ onclick={() => handleClick(ev)}
317
+ onpointerenter={() => oneventhover?.(ev)}
318
+ onkeydown={(e) => handleKeydown(e, ev)}
319
+ >
320
+ <div class="ag-card-body">
321
+ <div class="ag-card-top">
322
+ <span class="ag-card-title">{ev.title}</span>
323
+ <span class="ag-card-eta">{timeUntilMs(ev.start.getTime())}</span>
324
+ </div>
325
+ {#if ev.subtitle}
326
+ <span class="ag-card-sub">{ev.subtitle}</span>
327
+ {/if}
328
+ <div class="ag-card-meta">
329
+ {fmtTime(ev.start)} – {fmtTime(ev.end)}
330
+ <span class="ag-card-dur">{duration(ev)}</span>
331
+ </div>
332
+ {#if ev.tags?.length}
333
+ <div class="ag-card-tags">
334
+ {#each ev.tags as tag}
335
+ <span class="ag-card-tag">{tag}</span>
336
+ {/each}
337
+ </div>
338
+ {/if}
339
+ </div>
340
+ </div>
341
+ {/each}
342
+ {/if}
343
+ </div>
344
+ </div>
345
+
346
+ {:else if isPastDay}
347
+ <!-- ─── Past day: "The Log" — everything happened ─── -->
348
+ <div class="ag-log">
349
+ {#if timedDayEvents.length === 0 && allDayBanner.length === 0}
350
+ <div class="ag-q-empty">{L.nothingWasScheduled}</div>
351
+ {:else}
352
+ {#each timedDayEvents as ev (ev.id)}
353
+ <div
354
+ class="ag-log-row"
355
+ class:ag-log-row--selected={selectedEventId === ev.id}
356
+ style:--ev-color={ev.color || 'var(--dt-accent)'}
357
+ role="button"
358
+ tabindex="0"
359
+ aria-label="{ev.title}, {fmtTime(ev.start)} to {fmtTime(ev.end)}"
360
+ onclick={() => handleClick(ev)} onpointerenter={() => oneventhover?.(ev)} onkeydown={(e) => handleKeydown(e, ev)}
361
+ >
362
+ <span class="ag-log-check">✓</span>
363
+ <span class="ag-log-time">{fmtTime(ev.start)}</span>
364
+ <span class="ag-log-dot" style:background={ev.color || 'var(--dt-accent)'}></span>
365
+ <span class="ag-log-title">{ev.title}</span>
366
+ <span class="ag-log-dur">{duration(ev)}</span>
367
+ </div>
368
+ {/each}
369
+ {/if}
370
+ </div>
371
+
372
+ {:else}
373
+ <!-- ─── Future day: "The Plan" — everything is ahead ─── -->
374
+ <div class="ag-plan">
375
+ {#if timedDayEvents.length === 0 && allDayBanner.length === 0}
376
+ <div class="ag-q-empty">{L.nothingScheduledYet}</div>
377
+ {:else}
378
+ {#each timedDayEvents as ev, i (ev.id)}
379
+ <div
380
+ class="ag-card ag-card--plan"
381
+ class:ag-card--first={i === 0}
382
+ class:ag-card--selected={selectedEventId === ev.id}
383
+ style:--ev-color={ev.color || 'var(--dt-accent)'}
384
+ role="button"
385
+ tabindex="0"
386
+ aria-label="{ev.title}, {fmtTime(ev.start)} to {fmtTime(ev.end)}, {duration(ev)}"
387
+ onclick={() => handleClick(ev)} onpointerenter={() => oneventhover?.(ev)} onkeydown={(e) => handleKeydown(e, ev)}
388
+ >
389
+ <div class="ag-card-body">
390
+ <div class="ag-card-top">
391
+ <span class="ag-card-order">{i + 1}</span>
392
+ <span class="ag-card-title">{ev.title}</span>
393
+ </div>
394
+ {#if ev.subtitle}
395
+ <span class="ag-card-sub">{ev.subtitle}</span>
396
+ {/if}
397
+ <div class="ag-card-meta">
398
+ {fmtTime(ev.start)} – {fmtTime(ev.end)}
399
+ <span class="ag-card-dur">{duration(ev)}</span>
400
+ </div>
401
+ {#if ev.tags?.length}
402
+ <div class="ag-card-tags">
403
+ {#each ev.tags as tag}
404
+ <span class="ag-card-tag">{tag}</span>
405
+ {/each}
406
+ </div>
407
+ {/if}
408
+ </div>
409
+ </div>
410
+ {/each}
411
+ {/if}
412
+ </div>
413
+ {/if}
414
+ </div>
415
+
416
+ {#if !isMobile}
417
+ <div class="ag-date-label">{dateLabel}</div>
418
+ {/if}
419
+
420
+ <!-- ── Floating nav pills (desktop only — mobile uses Calendar header) ── -->
421
+ {#if showNav && !isMobile}
422
+ <nav class="ag-nav" aria-label={L.dayNavigation}>
423
+ <button
424
+ class="ag-nav-pill ag-nav-today"
425
+ class:ag-nav-today--hidden={isToday}
426
+ onclick={() => viewState?.goToday()}
427
+ aria-label={L.goToToday}
428
+ tabindex={isToday ? -1 : 0}
429
+ >
430
+ {L.today}
431
+ </button>
432
+ <button
433
+ class="ag-nav-pill"
434
+ onclick={() => viewState?.prev()}
435
+ aria-label={L.previousDay}
436
+ >
437
+ <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>
438
+ </button>
439
+ <button
440
+ class="ag-nav-pill"
441
+ onclick={() => viewState?.next()}
442
+ aria-label={L.nextDay}
443
+ >
444
+ <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>
445
+ </button>
446
+ </nav>
447
+ {/if}
448
+ </div>
449
+
450
+ <style>
451
+ /* ═══ Floating date label ═══ */
452
+ .ag-date-label {
453
+ position: absolute;
454
+ top: 10px;
455
+ left: 50%;
456
+ transform: translateX(-50%);
457
+ z-index: 20;
458
+ font: 600 11px/1 var(--dt-sans, 'Outfit', system-ui, sans-serif);
459
+ letter-spacing: 0.04em;
460
+ text-transform: uppercase;
461
+ color: var(--dt-text, rgba(226, 232, 240, 0.85));
462
+ background: color-mix(in srgb, var(--dt-surface, #10141c) 85%, transparent);
463
+ backdrop-filter: blur(6px);
464
+ -webkit-backdrop-filter: blur(6px);
465
+ padding: 8px 16px;
466
+ border-radius: 8px;
467
+ border: 1px solid var(--dt-border, rgba(148, 163, 184, 0.07));
468
+ pointer-events: none;
469
+ white-space: nowrap;
470
+ }
471
+
472
+ /* ═══ Floating nav pills ═══ */
473
+ .ag-nav {
474
+ position: absolute;
475
+ top: 10px;
476
+ right: 14px;
477
+ z-index: 20;
478
+ display: flex;
479
+ gap: 2px;
480
+ background: color-mix(in srgb, var(--dt-surface, #10141c) 85%, transparent);
481
+ backdrop-filter: blur(6px);
482
+ -webkit-backdrop-filter: blur(6px);
483
+ border-radius: 8px;
484
+ padding: 2px;
485
+ border: 1px solid var(--dt-border, rgba(148, 163, 184, 0.07));
486
+ }
487
+ .ag-nav-pill {
488
+ border: none;
489
+ background: transparent;
490
+ color: var(--dt-text-2, rgba(148, 163, 184, 0.55));
491
+ cursor: pointer;
492
+ font: 600 11px / 1 var(--dt-sans, 'Outfit', system-ui, sans-serif);
493
+ padding: 6px 12px;
494
+ border-radius: 6px;
495
+ letter-spacing: 0.04em;
496
+ text-transform: uppercase;
497
+ transition: background 100ms, color 100ms;
498
+ display: inline-flex;
499
+ align-items: center;
500
+ justify-content: center;
501
+ }
502
+ .ag-nav-pill:hover {
503
+ color: var(--dt-text, rgba(226, 232, 240, 0.85));
504
+ }
505
+ .ag-nav-today {
506
+ max-width: 60px;
507
+ overflow: hidden;
508
+ white-space: nowrap;
509
+ transition: max-width 250ms ease, padding 250ms ease, opacity 200ms ease;
510
+ }
511
+ .ag-nav-today--hidden {
512
+ max-width: 0;
513
+ padding-left: 0;
514
+ padding-right: 0;
515
+ opacity: 0;
516
+ pointer-events: none;
517
+ }
518
+ .ag-nav-pill:focus-visible {
519
+ outline: 2px solid color-mix(in srgb, var(--dt-accent, #ef4444) 55%, transparent);
520
+ outline-offset: 2px;
521
+ }
522
+
523
+ /* ═══ Container ═══ */
524
+ .ag {
525
+ position: relative;
526
+ overflow: hidden;
527
+ user-select: none;
528
+ display: flex;
529
+ flex-direction: column;
530
+ height: 100%;
531
+ color: var(--dt-text, rgba(255, 255, 255, 0.92));
532
+ font-family: var(--dt-sans, system-ui, sans-serif);
533
+ }
534
+
535
+ .ag--auto { height: auto; overflow: visible; }
536
+
537
+ .ag--disabled {
538
+ opacity: 0.4;
539
+ pointer-events: none;
540
+ }
541
+
542
+ /* ═══ Body ═══ */
543
+ .ag-body {
544
+ flex: 1;
545
+ min-height: 0;
546
+ display: flex;
547
+ flex-direction: column;
548
+ overflow-y: auto;
549
+ padding-top: 44px;
550
+ scrollbar-width: thin;
551
+ scrollbar-color: var(--dt-border) transparent;
552
+ }
553
+ .ag--auto .ag-body { overflow-y: visible; min-height: auto; }
554
+ .ag--mobile .ag-body { padding-top: 8px; }
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 strip ═══ */
564
+ .ag-allday {
565
+ display: flex;
566
+ align-items: center;
567
+ gap: 8px;
568
+ padding: 6px 16px;
569
+ border-bottom: 1px solid var(--dt-border, rgba(148, 163, 184, 0.07));
570
+ }
571
+ .ag-allday-label {
572
+ font: 600 10px/1 var(--dt-sans, system-ui, sans-serif);
573
+ text-transform: uppercase;
574
+ letter-spacing: 0.06em;
575
+ color: var(--dt-text-2, rgba(148, 163, 184, 0.55));
576
+ white-space: nowrap;
577
+ flex-shrink: 0;
578
+ }
579
+ .ag-allday-items {
580
+ display: flex;
581
+ flex-wrap: wrap;
582
+ gap: 6px;
583
+ }
584
+ .ag-allday-chip {
585
+ display: flex;
586
+ align-items: center;
587
+ gap: 5px;
588
+ padding: 3px 10px;
589
+ border-radius: 6px;
590
+ background: color-mix(in srgb, var(--ev-color) 12%, var(--dt-surface, #10141c));
591
+ border: 1px solid color-mix(in srgb, var(--ev-color) 20%, transparent);
592
+ cursor: pointer;
593
+ transition: background 0.15s, border-color 0.15s;
594
+ }
595
+ .ag-allday-chip:hover {
596
+ background: color-mix(in srgb, var(--ev-color) 22%, var(--dt-surface, #10141c));
597
+ border-color: color-mix(in srgb, var(--ev-color) 35%, transparent);
598
+ }
599
+ .ag-allday-chip:focus-visible {
600
+ outline: 2px solid var(--dt-accent, #ff6b4a);
601
+ outline-offset: 2px;
602
+ }
603
+ .ag-allday-chip--selected {
604
+ border-color: var(--ev-color);
605
+ background: color-mix(in srgb, var(--ev-color) 18%, var(--dt-surface, #10141c));
606
+ }
607
+ .ag-allday-dot {
608
+ width: 6px;
609
+ height: 6px;
610
+ border-radius: 50%;
611
+ background: var(--ev-color);
612
+ flex-shrink: 0;
613
+ }
614
+ .ag-allday-title {
615
+ font: 500 0.75rem/1.2 var(--dt-sans, system-ui, sans-serif);
616
+ color: var(--dt-text, rgba(226, 232, 240, 0.85));
617
+ white-space: nowrap;
618
+ }
619
+
620
+ /* ═══ Shared: event card ═══ */
621
+ .ag-card {
622
+ display: flex;
623
+ align-items: stretch;
624
+ border-radius: 10px;
625
+ background: color-mix(in srgb, var(--ev-color) 15%, var(--dt-surface, #191919));
626
+ border: 1px solid color-mix(in srgb, var(--ev-color) 10%, var(--dt-border, rgba(255, 255, 255, 0.06)));
627
+ overflow: hidden;
628
+ cursor: pointer;
629
+ transition: background 150ms, border-color 150ms;
630
+ }
631
+ .ag-card:hover {
632
+ background: color-mix(in srgb, var(--ev-color) 25%, var(--dt-surface, #191919));
633
+ border-color: color-mix(in srgb, var(--ev-color) 40%, transparent);
634
+ }
635
+ .ag-card:focus-visible {
636
+ outline: 2px solid var(--dt-accent, #ff6b4a);
637
+ outline-offset: 2px;
638
+ }
639
+ .ag-card--selected {
640
+ border-color: var(--ev-color);
641
+ background: color-mix(in srgb, var(--ev-color) 22%, var(--dt-surface, #191919));
642
+ }
643
+ .ag-card-body {
644
+ padding: 10px 12px;
645
+ display: flex;
646
+ flex-direction: column;
647
+ gap: 4px;
648
+ min-width: 0;
649
+ flex: 1;
650
+ }
651
+ .ag-card-top {
652
+ display: flex;
653
+ justify-content: space-between;
654
+ align-items: flex-start;
655
+ gap: 8px;
656
+ }
657
+ .ag-card-title {
658
+ font-size: 13px;
659
+ font-weight: 600;
660
+ line-height: 1.2;
661
+ white-space: nowrap;
662
+ overflow: hidden;
663
+ text-overflow: ellipsis;
664
+ flex: 1;
665
+ }
666
+ .ag-card-meta {
667
+ font-size: 11px;
668
+ color: var(--dt-text-2, rgba(255, 255, 255, 0.5));
669
+ font-family: var(--dt-mono, monospace);
670
+ line-height: 1;
671
+ }
672
+ .ag-card-dur {
673
+ margin-left: 6px;
674
+ color: var(--dt-text-3, rgba(255, 255, 255, 0.3));
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
+
695
+ /* ── Queue card variant ── */
696
+ .ag-card--q {
697
+ margin-bottom: 6px;
698
+ transition: border-color 150ms, transform 100ms;
699
+ }
700
+
701
+ .ag-card--q .ag-card-body {
702
+ gap: 3px;
703
+ }
704
+ .ag-card--q .ag-card-tags {
705
+ margin-top: 2px;
706
+ }
707
+ .ag-card-eta {
708
+ font-size: 9px;
709
+ font-weight: 600;
710
+ letter-spacing: 0.04em;
711
+ color: var(--dt-accent, #ff6b4a);
712
+ flex-shrink: 0;
713
+ white-space: nowrap;
714
+ }
715
+ .ag-card--hero {
716
+ background: color-mix(in srgb, var(--ev-color) 22%, var(--dt-surface, #191919));
717
+ border-color: color-mix(in srgb, var(--ev-color) 30%, transparent);
718
+ }
719
+ .ag-card--hero .ag-card-title {
720
+ font-size: 16px;
721
+ font-weight: 700;
722
+ }
723
+ .ag-card--hero .ag-card-eta {
724
+ font-size: 11px;
725
+ background: color-mix(in srgb, var(--dt-accent, #ff6b4a) 18%, transparent);
726
+ padding: 2px 7px;
727
+ border-radius: 4px;
728
+ }
729
+ .ag-card--hero .ag-card-body {
730
+ padding: 14px 16px;
731
+ }
732
+
733
+ /* ── Plan card variant ── */
734
+
735
+ .ag-card--plan .ag-card-body {
736
+ padding: 12px 14px;
737
+ gap: 3px;
738
+ }
739
+ .ag-card--plan .ag-card-top {
740
+ align-items: baseline;
741
+ }
742
+ .ag-card-order {
743
+ font-size: 10px;
744
+ font-weight: 700;
745
+ color: var(--dt-text-3, rgba(255, 255, 255, 0.2));
746
+ font-family: var(--dt-mono, monospace);
747
+ flex-shrink: 0;
748
+ }
749
+ .ag-card--plan .ag-card-title {
750
+ font-size: 14px;
751
+ }
752
+ .ag-card--first {
753
+ background: color-mix(in srgb, var(--ev-color) 20%, var(--dt-surface, #191919));
754
+ border-color: color-mix(in srgb, var(--ev-color) 25%, transparent);
755
+ }
756
+ .ag-card--first .ag-card-title {
757
+ font-size: 16px;
758
+ font-weight: 700;
759
+ }
760
+ .ag-card--plan .ag-card-sub {
761
+ padding-left: 22px;
762
+ }
763
+ .ag-card--plan .ag-card-tags {
764
+ padding-left: 22px;
765
+ margin-top: 2px;
766
+ }
767
+
768
+ /* ═══ The Queue: 2-column grid ═══ */
769
+ .ag-q {
770
+ display: grid;
771
+ grid-template-columns: 1fr 1.8fr;
772
+ gap: 0;
773
+ flex: 1;
774
+ padding: 8px 0 10px;
775
+ min-height: 0;
776
+ }
777
+ /* Mobile: stack queue columns vertically */
778
+ .ag--mobile .ag-q {
779
+ grid-template-columns: 1fr;
780
+ min-height: auto;
781
+ }
782
+ .ag--mobile .ag-q-status {
783
+ border-right: none;
784
+ border-bottom: 1px solid var(--dt-border, rgba(255, 255, 255, 0.06));
785
+ padding-bottom: 10px;
786
+ margin-bottom: 8px;
787
+ overflow-y: visible;
788
+ }
789
+ .ag--mobile .ag-q-queue {
790
+ overflow-y: visible;
791
+ padding-bottom: 16px;
792
+ }
793
+ .ag--mobile .ag-card-meta {
794
+ line-height: 1.3;
795
+ padding-bottom: 1px;
796
+ }
797
+ /* Mobile: larger touch targets */
798
+ .ag--mobile .ag-card-body {
799
+ padding: 14px 16px;
800
+ }
801
+ .ag--mobile .ag-card-title {
802
+ font-size: 15px;
803
+ }
804
+ .ag--mobile .ag-card--hero .ag-card-title {
805
+ font-size: 18px;
806
+ }
807
+ .ag--mobile .ag-card--hero .ag-card-body {
808
+ padding: 16px 18px;
809
+ }
810
+ .ag--mobile .ag-log-row {
811
+ padding: 12px 0;
812
+ }
813
+ .ag--mobile .ag-card--plan .ag-card-body {
814
+ padding: 14px 16px;
815
+ }
816
+ .ag--mobile .ag-card--plan .ag-card-title {
817
+ font-size: 15px;
818
+ }
819
+ .ag--mobile .ag-nav-pill {
820
+ padding: 10px 16px;
821
+ }
822
+ .ag-q-label {
823
+ font-size: 8px;
824
+ font-weight: 600;
825
+ letter-spacing: 0.14em;
826
+ text-transform: uppercase;
827
+ color: var(--dt-text-3, rgba(255, 255, 255, 0.25));
828
+ margin-bottom: 8px;
829
+ padding: 0 12px;
830
+ font-family: var(--dt-sans, system-ui, sans-serif);
831
+ }
832
+ .ag-q-empty {
833
+ display: flex;
834
+ align-items: center;
835
+ justify-content: center;
836
+ flex: 1;
837
+ font-size: 13px;
838
+ font-weight: 300;
839
+ color: var(--dt-text-3, rgba(255, 255, 255, 0.25));
840
+ }
841
+
842
+ /* ── NOW column (includes Done above) ── */
843
+ .ag-q-status {
844
+ padding: 0 10px 0 14px;
845
+ border-right: 1px solid var(--dt-border, rgba(255, 255, 255, 0.06));
846
+ display: flex;
847
+ flex-direction: column;
848
+ overflow-y: auto;
849
+ scrollbar-width: none;
850
+ }
851
+ .ag-q-status::-webkit-scrollbar {
852
+ display: none;
853
+ }
854
+ .ag-q-done-section {
855
+ margin-bottom: 10px;
856
+ padding-bottom: 8px;
857
+ border-bottom: 1px solid var(--dt-border, rgba(255, 255, 255, 0.06));
858
+ }
859
+ .ag-q-clock {
860
+ font-size: 10px;
861
+ font-weight: 600;
862
+ font-family: var(--dt-mono, monospace);
863
+ color: var(--dt-accent, #ff6b4a);
864
+ margin-left: 4px;
865
+ }
866
+ .ag-q-now {
867
+ padding: 8px 10px;
868
+ border-radius: 8px;
869
+ background: color-mix(in srgb, var(--ev-color, var(--dt-accent)) 15%, var(--dt-surface, #191919));
870
+ border: 1px solid color-mix(in srgb, var(--ev-color, var(--dt-accent)) 15%, transparent);
871
+ cursor: pointer;
872
+ transition: background 150ms, border-color 150ms;
873
+ margin-right: 10px;
874
+ }
875
+ .ag-q-now:hover {
876
+ background: color-mix(in srgb, var(--ev-color, var(--dt-accent)) 25%, var(--dt-surface, #191919));
877
+ border-color: color-mix(in srgb, var(--ev-color, var(--dt-accent)) 35%, transparent);
878
+ }
879
+ .ag-q-now:focus-visible {
880
+ outline: 2px solid var(--dt-accent, #ff6b4a);
881
+ outline-offset: 2px;
882
+ }
883
+ .ag-q-now--selected {
884
+ border-color: var(--ev-color, var(--dt-accent));
885
+ }
886
+ .ag-q-now-dot {
887
+ width: 6px;
888
+ height: 6px;
889
+ border-radius: 50%;
890
+ background: var(--ev-color, var(--dt-accent, #ff6b4a));
891
+ margin-bottom: 6px;
892
+ animation: ag-pulse 2.5s ease-in-out infinite;
893
+ }
894
+ @keyframes ag-pulse {
895
+ 0%, 100% { opacity: 1; }
896
+ 50% { opacity: 0.4; }
897
+ }
898
+ .ag-q-now-title {
899
+ font-size: 11px;
900
+ font-weight: 600;
901
+ line-height: 1.2;
902
+ color: var(--dt-text, rgba(255, 255, 255, 0.92));
903
+ white-space: nowrap;
904
+ overflow: hidden;
905
+ text-overflow: ellipsis;
906
+ margin-bottom: 3px;
907
+ }
908
+ .ag-q-now-time {
909
+ font-size: 9px;
910
+ font-family: var(--dt-mono, monospace);
911
+ color: var(--dt-text-3, rgba(255, 255, 255, 0.35));
912
+ margin-bottom: 6px;
913
+ }
914
+ .ag-q-now-track {
915
+ height: 2px;
916
+ background: var(--dt-border, rgba(255, 255, 255, 0.06));
917
+ border-radius: 1px;
918
+ overflow: hidden;
919
+ }
920
+ .ag-q-now-fill {
921
+ height: 100%;
922
+ background: var(--ev-color, var(--dt-accent, #ff6b4a));
923
+ border-radius: 1px;
924
+ transition: width 1s linear;
925
+ }
926
+ .ag-q-free {
927
+ padding: 8px 10px;
928
+ margin-right: 10px;
929
+ }
930
+ .ag-q-free-label {
931
+ font-size: 12px;
932
+ font-weight: 300;
933
+ color: var(--dt-text-3, rgba(255, 255, 255, 0.25));
934
+ margin-bottom: 2px;
935
+ }
936
+
937
+ /* ── NEXT: hero center column ── */
938
+ .ag-q-queue {
939
+ padding: 0 16px;
940
+ overflow-y: auto;
941
+ scrollbar-width: none;
942
+ display: flex;
943
+ flex-direction: column;
944
+ }
945
+ .ag-q-queue::-webkit-scrollbar {
946
+ display: none;
947
+ }
948
+
949
+
950
+ .ag-q-done-item {
951
+ display: flex;
952
+ align-items: center;
953
+ gap: 5px;
954
+ padding: 3px 0;
955
+ opacity: 0.35;
956
+ cursor: pointer;
957
+ }
958
+ .ag-q-done-item:hover {
959
+ opacity: 0.6;
960
+ }
961
+ .ag-q-done-item:focus-visible {
962
+ outline: 2px solid var(--dt-accent, #ff6b4a);
963
+ outline-offset: 2px;
964
+ opacity: 0.6;
965
+ }
966
+ .ag-q-done-item--selected {
967
+ opacity: 0.7;
968
+ }
969
+ .ag-q-done-check {
970
+ font-size: 9px;
971
+ color: var(--dt-success, rgba(120, 200, 140, 0.7));
972
+ flex-shrink: 0;
973
+ }
974
+ .ag-q-done-title {
975
+ font-size: 10px;
976
+ line-height: 1.2;
977
+ color: var(--dt-text-3, rgba(255, 255, 255, 0.35));
978
+ white-space: nowrap;
979
+ overflow: hidden;
980
+ text-overflow: ellipsis;
981
+ text-decoration: line-through;
982
+ text-decoration-color: var(--dt-text-3, rgba(255, 255, 255, 0.15));
983
+ }
984
+
985
+ /* ═══ Past Day: "The Log" ═══ */
986
+ .ag-log {
987
+ flex: 1;
988
+ padding: 8px 20px 12px;
989
+ overflow-y: auto;
990
+ scrollbar-width: none;
991
+ opacity: 0.7;
992
+ }
993
+ .ag-log::-webkit-scrollbar {
994
+ display: none;
995
+ }
996
+ .ag-log-row {
997
+ display: flex;
998
+ align-items: center;
999
+ gap: 10px;
1000
+ padding: 8px 0;
1001
+ border-bottom: 1px solid var(--dt-border, rgba(255, 255, 255, 0.04));
1002
+ cursor: pointer;
1003
+ transition: opacity 150ms;
1004
+ }
1005
+ .ag-log-row:last-child {
1006
+ border-bottom: none;
1007
+ }
1008
+ .ag-log-row:hover {
1009
+ opacity: 1;
1010
+ }
1011
+ .ag-log-row:focus-visible {
1012
+ outline: 2px solid var(--dt-accent, #ff6b4a);
1013
+ outline-offset: 2px;
1014
+ }
1015
+ .ag-log-row--selected {
1016
+ opacity: 1;
1017
+ background: color-mix(in srgb, var(--ev-color) 6%, transparent);
1018
+ border-radius: 6px;
1019
+ padding-left: 8px;
1020
+ padding-right: 8px;
1021
+ }
1022
+ .ag-log-check {
1023
+ font-size: 10px;
1024
+ color: var(--dt-success, rgba(120, 200, 140, 0.5));
1025
+ flex-shrink: 0;
1026
+ }
1027
+ .ag-log-time {
1028
+ font-size: 11px;
1029
+ font-family: var(--dt-mono, monospace);
1030
+ color: var(--dt-text-3, rgba(255, 255, 255, 0.3));
1031
+ width: 64px;
1032
+ flex-shrink: 0;
1033
+ }
1034
+ .ag-log-dot {
1035
+ width: 5px;
1036
+ height: 5px;
1037
+ border-radius: 50%;
1038
+ flex-shrink: 0;
1039
+ opacity: 0.6;
1040
+ }
1041
+ .ag-log-title {
1042
+ font-size: 13px;
1043
+ font-weight: 500;
1044
+ line-height: 1.2;
1045
+ color: var(--dt-text-2, rgba(255, 255, 255, 0.55));
1046
+ flex: 1;
1047
+ white-space: nowrap;
1048
+ overflow: hidden;
1049
+ text-overflow: ellipsis;
1050
+ text-decoration: line-through;
1051
+ text-decoration-color: var(--dt-border, rgba(255, 255, 255, 0.08));
1052
+ }
1053
+ .ag-log-dur {
1054
+ font-size: 10px;
1055
+ font-family: var(--dt-mono, monospace);
1056
+ color: var(--dt-text-3, rgba(255, 255, 255, 0.2));
1057
+ flex-shrink: 0;
1058
+ }
1059
+
1060
+ /* ═══ Future Day: "The Plan" ═══ */
1061
+ .ag-plan {
1062
+ flex: 1;
1063
+ padding: 8px 20px 12px;
1064
+ overflow-y: auto;
1065
+ scrollbar-width: none;
1066
+ display: flex;
1067
+ flex-direction: column;
1068
+ gap: 6px;
1069
+ }
1070
+ .ag-plan::-webkit-scrollbar {
1071
+ display: none;
1072
+ }
1073
+ </style>