@nomideusz/svelte-calendar 0.9.0 → 0.11.0

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,15 +1,15 @@
1
1
  <script lang="ts">import { createClock } from "../../core/clock.svelte.js";
2
- import { sod, DAY_MS, startOfWeek, dayNum, isAllDay, isMultiDay } from "../../core/time.js";
3
- import { weekdayLong, monthLong, getLabels } from "../../core/locale.js";
2
+ import { sod, DAY_MS, startOfWeek, dayNum, isAllDay, isMultiDay, segmentForDay } from "../../core/time.js";
3
+ import { weekdayLong, monthLong } from "../../core/locale.js";
4
4
  import { useCalendarContext } from "../shared/context.svelte.js";
5
5
  import EventContent from "../shared/EventContent.svelte";
6
6
  import { fmtTime, duration, timeUntilMs, progress, groupIntoSlots } from "../shared/format.js";
7
- const L = $derived(getLabels());
8
7
  const ctx = useCalendarContext();
8
+ const L = $derived(ctx.labels);
9
9
  let {
10
10
  mondayStart = true,
11
11
  locale,
12
- height = 520,
12
+ height,
13
13
  events = [],
14
14
  style = "",
15
15
  focusDate,
@@ -29,14 +29,17 @@ const oneventhover = $derived(ctx.oneventhover);
29
29
  const disabledSet = $derived(ctx.disabledSet);
30
30
  let swipeStartX = 0;
31
31
  let swipeStartY = 0;
32
+ let swipeActive = false;
32
33
  const SWIPE_THRESHOLD = 50;
33
34
  function onPointerDown(e) {
34
- if (!isMobile) return;
35
+ if (!isMobile || e.pointerType !== "touch") return;
36
+ swipeActive = true;
35
37
  swipeStartX = e.clientX;
36
38
  swipeStartY = e.clientY;
37
39
  }
38
40
  function onPointerUp(e) {
39
- if (!isMobile) return;
41
+ if (!swipeActive || e.pointerType !== "touch") return;
42
+ swipeActive = false;
40
43
  const dx = e.clientX - swipeStartX;
41
44
  const dy = e.clientY - swipeStartY;
42
45
  if (Math.abs(dx) > SWIPE_THRESHOLD && Math.abs(dx) > Math.abs(dy) * 1.4) {
@@ -44,18 +47,23 @@ function onPointerUp(e) {
44
47
  else viewState?.next();
45
48
  }
46
49
  }
50
+ function onPointerCancel() {
51
+ swipeActive = false;
52
+ }
53
+ let expandedDays = $state([]);
54
+ function toggleDayExpand(ms) {
55
+ expandedDays = expandedDays.includes(ms) ? expandedDays.filter((m) => m !== ms) : [...expandedDays, ms];
56
+ }
57
+ let expandedPast = $state([]);
58
+ function togglePastExpand(ms) {
59
+ expandedPast = expandedPast.includes(ms) ? expandedPast.filter((m) => m !== ms) : [...expandedPast, ms];
60
+ }
47
61
  const fmt = (d) => fmtTime(d, locale);
48
- const eta = (ms) => timeUntilMs(ms, clock.tick);
62
+ const eta = (ms) => timeUntilMs(ms, clock.tick, L);
49
63
  const prog = (ev) => progress(ev, clock.tick);
50
64
  function handleClick(ev) {
51
65
  oneventclick?.(ev);
52
66
  }
53
- function handleKeydown(e, ev) {
54
- if (e.key === "Enter" || e.key === " ") {
55
- e.preventDefault();
56
- oneventclick?.(ev);
57
- }
58
- }
59
67
  const weekStartMs = $derived(
60
68
  focusDate ? viewState?.dayCount === 7 ? startOfWeek(sod(focusDate.getTime()), mondayStart) : sod(focusDate.getTime()) : viewState?.dayCount === 7 ? startOfWeek(clock.today, mondayStart) : clock.today
61
69
  );
@@ -101,6 +109,7 @@ const weekDays = $derived.by(() => {
101
109
  dayName: weekdayLong(ms, locale),
102
110
  dateLabel: `${monthLong(ms, locale)} ${dayNum(ms)}`,
103
111
  tier,
112
+ isToday: ms === todayMs,
104
113
  events: dayEvts,
105
114
  allDayEvents: allDayEvts,
106
115
  timedEvents: timedEvts,
@@ -123,7 +132,8 @@ const weekDays = $derived.by(() => {
123
132
 
124
133
  <!-- ═══ Shared event card snippet ═══ -->
125
134
  {#snippet eventCard(ev: TimelineEvent, isNow: boolean, eta?: string)}
126
- <div
135
+ <button
136
+ type="button"
127
137
  class="ag-card"
128
138
  class:ag-card--selected={selectedEventId === ev.id}
129
139
  class:ag-card--cancelled={ev.status === 'cancelled'}
@@ -131,12 +141,9 @@ const weekDays = $derived.by(() => {
131
141
  class:ag-card--full={ev.status === 'full'}
132
142
  class:ag-card--limited={ev.status === 'limited'}
133
143
  style:--ev-color={ev.color || 'var(--dt-accent)'}
134
- role="button"
135
- tabindex="0"
136
144
  aria-label="{ev.title}{ev.status === 'cancelled' ? ' (cancelled)' : ''}{ev.status === 'tentative' ? ' (tentative)' : ''}{ev.status === 'full' ? ' (full)' : ''}{ev.status === 'limited' ? ' (limited)' : ''}, {fmt(ev.start)} to {fmt(ev.end)}, {duration(ev)}"
137
145
  onclick={() => handleClick(ev)}
138
146
  onpointerenter={() => oneventhover?.(ev)}
139
- onkeydown={(e) => handleKeydown(e, ev)}
140
147
  >
141
148
  <div class="ag-card-body">
142
149
  <EventContent event={ev}>
@@ -168,11 +175,64 @@ const weekDays = $derived.by(() => {
168
175
  </EventContent>
169
176
  {#if isNow}
170
177
  <div class="ag-card-progress">
171
- <div class="ag-card-progress-fill" style:width="{prog(ev) * 100}%"></div>
178
+ <div class="ag-card-progress-fill" style:transform="scaleX({prog(ev)})"></div>
172
179
  </div>
173
180
  {/if}
174
181
  </div>
175
- </div>
182
+ </button>
183
+ {/snippet}
184
+
185
+ <!-- ═══ Shared compact row snippet ═══ -->
186
+ {#snippet compactRow(ev: TimelineEvent, showLoc: boolean, done: boolean)}
187
+ <button
188
+ type="button"
189
+ class="ag-compact"
190
+ class:ag-compact--selected={selectedEventId === ev.id}
191
+ class:ag-compact--done={done}
192
+ class:ag-compact--cancelled={ev.status === 'cancelled'}
193
+ class:ag-compact--tentative={ev.status === 'tentative'}
194
+ class:ag-compact--full={ev.status === 'full'}
195
+ class:ag-compact--limited={ev.status === 'limited'}
196
+ style:--ev-color={ev.color || 'var(--dt-accent)'}
197
+ aria-label="{ev.title}{done ? `, ${L.completed}` : ''}, {fmt(ev.start)}, {duration(ev)}"
198
+ onclick={() => handleClick(ev)}
199
+ onpointerenter={() => oneventhover?.(ev)}
200
+ >
201
+ <EventContent event={ev}>
202
+ <span class="ag-compact-dot"></span>
203
+ <span class="ag-compact-time">{fmt(ev.start)}</span>
204
+ <div class="ag-compact-main">
205
+ <span class="ag-compact-title">{ev.title}</span>
206
+ {#if showLoc && ev.location}
207
+ <span class="ag-compact-loc">{ev.location}</span>
208
+ {/if}
209
+ {#if ev.subtitle}
210
+ <span class="ag-compact-sub">{ev.subtitle}</span>
211
+ {/if}
212
+ {#if ev.tags?.length}
213
+ {#each ev.tags as tag}
214
+ <span class="ag-compact-tag">{tag}</span>
215
+ {/each}
216
+ {/if}
217
+ </div>
218
+ <span class="ag-compact-dur">{duration(ev)}</span>
219
+ </EventContent>
220
+ </button>
221
+ {/snippet}
222
+
223
+ <!-- ═══ "✓ N completed" disclosure toggle ═══ -->
224
+ {#snippet pastToggle(ms: number, count: number, summary: boolean)}
225
+ {@const open = expandedPast.includes(ms)}
226
+ <button
227
+ type="button"
228
+ class="ag-wday-past-line ag-past-toggle"
229
+ class:ag-wday-past-line--summary={summary}
230
+ aria-expanded={open}
231
+ onclick={() => togglePastExpand(ms)}
232
+ >
233
+ ✓ {L.nCompleted(count)}
234
+ <svg class="ag-past-chevron" class:ag-past-chevron--open={open} viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" width="12" height="12" aria-hidden="true"><path d="M4 6l4 4 4-4"/></svg>
235
+ </button>
176
236
  {/snippet}
177
237
 
178
238
  <!-- svelte-ignore a11y_no_static_element_interactions -->
@@ -181,8 +241,10 @@ const weekDays = $derived.by(() => {
181
241
  class:ag--mobile={isMobile}
182
242
  class:ag--auto={autoHeight}
183
243
  style={style || undefined}
244
+ style:height={height ? `${height}px` : undefined}
184
245
  onpointerdown={onPointerDown}
185
246
  onpointerup={onPointerUp}
247
+ onpointercancel={onPointerCancel}
186
248
  >
187
249
  <div class="ag-body" role="list" aria-label={L.weekAhead}>
188
250
  {#each weekDays as day (day.ms)}
@@ -201,6 +263,18 @@ const weekDays = $derived.by(() => {
201
263
  </div>
202
264
  {/if}
203
265
  </div>
266
+ {#if day.timedEvents.length > 0}
267
+ {@render pastToggle(day.ms, day.timedEvents.length, true)}
268
+ {#if expandedPast.includes(day.ms)}
269
+ <div class="ag-wday-compact">
270
+ {#each day.timedEvents as ev (ev.id)}
271
+ {@render compactRow(ev, false, true)}
272
+ {/each}
273
+ </div>
274
+ {/if}
275
+ {:else if day.events.length === 0}
276
+ <div class="ag-wday-past-line ag-wday-past-line--summary">{L.noEvents}</div>
277
+ {/if}
204
278
  </div>
205
279
  {:else}
206
280
  <div
@@ -214,7 +288,7 @@ const weekDays = $derived.by(() => {
214
288
  <!-- Day header -->
215
289
  <div class="ag-wday-head">
216
290
  <div class="ag-wday-head-left">
217
- {#if day.tier === 'today'}
291
+ {#if day.isToday}
218
292
  <span class="ag-wday-badge">{L.today}</span>
219
293
  {:else if day.tier === 'tomorrow'}
220
294
  <span class="ag-wday-badge ag-wday-badge--muted">{L.tomorrow}</span>
@@ -232,20 +306,23 @@ const weekDays = $derived.by(() => {
232
306
  {#if day.allDayEvents.length > 0}
233
307
  <div class="ag-allday">
234
308
  {#each day.allDayEvents as ev (ev.id)}
235
- <div
309
+ {@const seg = segmentForDay(ev, day.ms)}
310
+ {@const isCont = seg !== null && seg.totalDays > 1}
311
+ <button
312
+ type="button"
236
313
  class="ag-allday-chip"
237
314
  class:ag-allday-chip--selected={selectedEventId === ev.id}
238
315
  style:--ev-color={ev.color || 'var(--dt-accent)'}
239
- role="button"
240
- tabindex="0"
241
- aria-label="{ev.title}, {L.allDay}"
316
+ aria-label="{ev.title}, {isCont && seg ? L.dayNOfTotal(seg.dayIndex, seg.totalDays) : L.allDay}"
242
317
  onclick={() => handleClick(ev)}
243
318
  onpointerenter={() => oneventhover?.(ev)}
244
- onkeydown={(e) => handleKeydown(e, ev)}
245
319
  >
246
320
  <span class="ag-allday-dot"></span>
247
321
  <span class="ag-allday-title">{ev.title}</span>
248
- </div>
322
+ {#if isCont && seg}
323
+ <span class="ag-allday-span">{seg.dayIndex}/{seg.totalDays}</span>
324
+ {/if}
325
+ </button>
249
326
  {/each}
250
327
  </div>
251
328
  {/if}
@@ -256,36 +333,7 @@ const weekDays = $derived.by(() => {
256
333
  <!-- Compact: minimal dot + time + title rows for all days -->
257
334
  <div class="ag-wday-compact">
258
335
  {#each day.timedEvents as ev (ev.id)}
259
- <div
260
- class="ag-compact"
261
- class:ag-compact--selected={selectedEventId === ev.id}
262
- class:ag-compact--cancelled={ev.status === 'cancelled'}
263
- class:ag-compact--tentative={ev.status === 'tentative'}
264
- class:ag-compact--full={ev.status === 'full'}
265
- class:ag-compact--limited={ev.status === 'limited'}
266
- style:--ev-color={ev.color || 'var(--dt-accent)'}
267
- role="button"
268
- tabindex="0"
269
- aria-label="{ev.title}, {fmt(ev.start)}, {duration(ev)}"
270
- onclick={() => handleClick(ev)}
271
- onpointerenter={() => oneventhover?.(ev)}
272
- onkeydown={(e) => handleKeydown(e, ev)}
273
- >
274
- <EventContent event={ev}>
275
- <span class="ag-compact-dot"></span>
276
- <span class="ag-compact-time">{fmt(ev.start)}</span>
277
- <span class="ag-compact-title">{ev.title}</span>
278
- {#if ev.subtitle}
279
- <span class="ag-compact-sub">{ev.subtitle}</span>
280
- {/if}
281
- {#if ev.tags?.length}
282
- {#each ev.tags as tag}
283
- <span class="ag-compact-tag">{tag}</span>
284
- {/each}
285
- {/if}
286
- <span class="ag-compact-dur">{duration(ev)}</span>
287
- </EventContent>
288
- </div>
336
+ {@render compactRow(ev, false, false)}
289
337
  {/each}
290
338
  </div>
291
339
  {:else if equalDays}
@@ -326,47 +374,30 @@ const weekDays = $derived.by(() => {
326
374
  </div>
327
375
  {/each}
328
376
  {#if day.pastEvents.length > 0}
329
- <div class="ag-wday-past-line">✓ {L.nCompleted(day.pastEvents.length)}</div>
377
+ {@render pastToggle(day.ms, day.pastEvents.length, false)}
378
+ {#if expandedPast.includes(day.ms)}
379
+ {#each day.pastEvents as ev (ev.id)}
380
+ {@render compactRow(ev, false, true)}
381
+ {/each}
382
+ {/if}
330
383
  {/if}
331
384
  </div>
332
385
  {:else}
333
386
  <!-- Compact: future days get minimal rows -->
387
+ {@const dayExpanded = expandedDays.includes(day.ms)}
334
388
  <div class="ag-wday-compact">
335
- {#each day.timedEvents.slice(0, 4) as ev (ev.id)}
336
- <div
337
- class="ag-compact"
338
- class:ag-compact--selected={selectedEventId === ev.id}
339
- class:ag-compact--cancelled={ev.status === 'cancelled'}
340
- class:ag-compact--tentative={ev.status === 'tentative'}
341
- class:ag-compact--full={ev.status === 'full'}
342
- class:ag-compact--limited={ev.status === 'limited'}
343
- style:--ev-color={ev.color || 'var(--dt-accent)'}
344
- role="button"
345
- tabindex="0"
346
- aria-label="{ev.title}, {fmt(ev.start)}, {duration(ev)}"
347
- onclick={() => handleClick(ev)} onpointerenter={() => oneventhover?.(ev)} onkeydown={(e) => handleKeydown(e, ev)}
348
- >
349
- <EventContent event={ev}>
350
- <span class="ag-compact-dot"></span>
351
- <span class="ag-compact-time">{fmt(ev.start)}</span>
352
- <span class="ag-compact-title">{ev.title}</span>
353
- {#if ev.location}
354
- <span class="ag-compact-loc">{ev.location}</span>
355
- {/if}
356
- {#if ev.subtitle}
357
- <span class="ag-compact-sub">{ev.subtitle}</span>
358
- {/if}
359
- {#if ev.tags?.length}
360
- {#each ev.tags as tag}
361
- <span class="ag-compact-tag">{tag}</span>
362
- {/each}
363
- {/if}
364
- <span class="ag-compact-dur">{duration(ev)}</span>
365
- </EventContent>
366
- </div>
389
+ {#each (dayExpanded ? day.timedEvents : day.timedEvents.slice(0, 4)) as ev (ev.id)}
390
+ {@render compactRow(ev, true, false)}
367
391
  {/each}
368
392
  {#if day.timedEvents.length > 4}
369
- <div class="ag-compact-more">{L.nMore(day.timedEvents.length - 4)}</div>
393
+ <button
394
+ type="button"
395
+ class="ag-compact-more"
396
+ aria-expanded={dayExpanded}
397
+ onclick={() => toggleDayExpand(day.ms)}
398
+ >
399
+ {dayExpanded ? L.showLess : L.nMore(day.timedEvents.length - 4)}
400
+ </button>
370
401
  {/if}
371
402
  </div>
372
403
  {/if}
@@ -397,6 +428,23 @@ const weekDays = $derived.by(() => {
397
428
  overflow: visible;
398
429
  }
399
430
 
431
+ /* Button UA reset for interactive cards/rows (real <button>s for a11y).
432
+ Placed first so later component rules override it. */
433
+ .ag-card,
434
+ .ag-allday-chip,
435
+ .ag-compact,
436
+ .ag-compact-more,
437
+ .ag-past-toggle {
438
+ font: inherit;
439
+ color: inherit;
440
+ text-align: left;
441
+ background: none;
442
+ border: none;
443
+ padding: 0;
444
+ margin: 0;
445
+ box-sizing: border-box;
446
+ }
447
+
400
448
  /* ═══ Body ═══ */
401
449
  .ag-body {
402
450
  flex: 1;
@@ -404,10 +452,15 @@ const weekDays = $derived.by(() => {
404
452
  overflow-y: auto;
405
453
  overflow-x: hidden;
406
454
  box-sizing: border-box;
407
- padding-top: 8px;
455
+ /* No padding-top here: the sticky day headers pin at the scrollport
456
+ edge, and container padding would leave a see-through band above
457
+ them where scrolled cards bleed out. */
408
458
  scrollbar-width: thin;
409
459
  scrollbar-color: var(--dt-border) transparent;
410
460
  }
461
+ .ag-wday:first-child .ag-wday-head {
462
+ padding-top: 12px;
463
+ }
411
464
  .ag--auto .ag-body {
412
465
  overflow-y: visible;
413
466
  }
@@ -437,13 +490,14 @@ const weekDays = $derived.by(() => {
437
490
  cursor: pointer;
438
491
  transition: background 0.15s, border-color 0.15s;
439
492
  }
440
- .ag-allday-chip:hover {
493
+ .ag-allday-chip:hover,
494
+ .ag-allday-chip:active {
441
495
  background: color-mix(in srgb, var(--ev-color) 22%, var(--dt-surface, var(--dt-bg, #ffffff)));
442
496
  border-color: color-mix(in srgb, var(--ev-color) 30%, transparent);
443
497
  }
444
498
  .ag-allday-chip:focus-visible {
445
- outline: 2px solid var(--dt-accent, #2563eb);
446
- outline-offset: 2px;
499
+ outline: none;
500
+ box-shadow: 0 0 0 2px var(--dt-accent, #2563eb);
447
501
  }
448
502
  .ag-allday-chip--selected {
449
503
  border-color: var(--ev-color);
@@ -461,6 +515,11 @@ const weekDays = $derived.by(() => {
461
515
  color: var(--dt-text, rgba(0, 0, 0, 0.87));
462
516
  white-space: nowrap;
463
517
  }
518
+ .ag-allday-span {
519
+ font: 500 10px/1.2 var(--dt-mono, monospace);
520
+ color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
521
+ white-space: nowrap;
522
+ }
464
523
 
465
524
  /* ═══ Shared: event card ═══ */
466
525
  .ag-card {
@@ -473,13 +532,14 @@ const weekDays = $derived.by(() => {
473
532
  cursor: pointer;
474
533
  transition: background 150ms, border-color 150ms;
475
534
  }
476
- .ag-card:hover {
535
+ .ag-card:hover,
536
+ .ag-card:active {
477
537
  background: color-mix(in srgb, var(--ev-color) 20%, var(--dt-surface, var(--dt-bg, #ffffff)));
478
538
  border-color: color-mix(in srgb, var(--ev-color) 30%, transparent);
479
539
  }
480
540
  .ag-card:focus-visible {
481
- outline: 2px solid var(--dt-accent, #2563eb);
482
- outline-offset: 2px;
541
+ outline: none;
542
+ box-shadow: 0 0 0 2px var(--dt-accent, #2563eb);
483
543
  }
484
544
  .ag-card--selected {
485
545
  border-color: var(--ev-color);
@@ -517,6 +577,11 @@ const weekDays = $derived.by(() => {
517
577
  word-break: break-word;
518
578
  flex: 1;
519
579
  min-width: 0;
580
+ display: -webkit-box;
581
+ -webkit-box-orient: vertical;
582
+ -webkit-line-clamp: 2;
583
+ line-clamp: 2;
584
+ overflow: hidden;
520
585
  }
521
586
  .ag-card-meta {
522
587
  display: flex;
@@ -532,10 +597,9 @@ const weekDays = $derived.by(() => {
532
597
  }
533
598
  .ag-card-eta {
534
599
  margin-left: auto;
535
- font-size: 10px;
600
+ font-size: 11px;
536
601
  font-weight: 600;
537
- color: color-mix(in srgb, var(--ev-color) 80%, var(--dt-text));
538
- opacity: 0.85;
602
+ color: color-mix(in srgb, var(--dt-text, rgba(0, 0, 0, 0.87)) 60%, var(--ev-color));
539
603
  letter-spacing: 0.02em;
540
604
  }
541
605
  .ag-card-sub {
@@ -554,7 +618,7 @@ const weekDays = $derived.by(() => {
554
618
  flex-wrap: wrap;
555
619
  }
556
620
  .ag-card-tag {
557
- font: 500 9px / 1 var(--dt-sans, system-ui, sans-serif);
621
+ font: 500 10px / 1 var(--dt-sans, system-ui, sans-serif);
558
622
  color: var(--ev-color, var(--dt-accent));
559
623
  background: color-mix(in srgb, var(--ev-color, var(--dt-accent)) 15%, transparent);
560
624
  padding: 2px 5px;
@@ -570,9 +634,16 @@ const weekDays = $derived.by(() => {
570
634
  }
571
635
  .ag-card-progress-fill {
572
636
  height: 100%;
637
+ width: 100%;
573
638
  background: var(--ev-color, var(--dt-accent));
574
639
  border-radius: 2px;
575
- transition: width 1s linear;
640
+ transform-origin: left;
641
+ transition: transform 1s linear;
642
+ }
643
+ @media (prefers-reduced-motion: reduce) {
644
+ .ag-card-progress-fill {
645
+ transition: none;
646
+ }
576
647
  }
577
648
 
578
649
  /* ═══ Week day groups ═══ */
@@ -588,11 +659,13 @@ const weekDays = $derived.by(() => {
588
659
  .ag-wday--tomorrow .ag-card {
589
660
  opacity: 0.82;
590
661
  }
591
- .ag-wday--past {
592
- opacity: 0.4;
662
+ /* Past days: token-based text dim instead of subtree opacity (readability) */
663
+ .ag-wday--past .ag-wday-name {
664
+ color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
665
+ font-weight: 500;
593
666
  }
594
667
  .ag-wday--past .ag-wday-head {
595
- padding: 8px 20px;
668
+ padding: 8px 20px 2px;
596
669
  }
597
670
  .ag-wday--disabled {
598
671
  position: relative;
@@ -605,8 +678,8 @@ const weekDays = $derived.by(() => {
605
678
  135deg,
606
679
  transparent,
607
680
  transparent 4px,
608
- rgba(128, 128, 128, 0.08) 4px,
609
- rgba(128, 128, 128, 0.08) 8px
681
+ color-mix(in srgb, var(--dt-text, rgba(0, 0, 0, 0.87)) 8%, transparent) 4px,
682
+ color-mix(in srgb, var(--dt-text, rgba(0, 0, 0, 0.87)) 8%, transparent) 8px
610
683
  );
611
684
  pointer-events: none;
612
685
  }
@@ -619,6 +692,14 @@ const weekDays = $derived.by(() => {
619
692
  justify-content: space-between;
620
693
  align-items: center;
621
694
  padding: 8px 20px;
695
+ position: sticky;
696
+ top: 0;
697
+ background: var(--dt-bg, #fff);
698
+ z-index: 1;
699
+ /* Own compositor layer: without it, fast (async) scrolling repaints
700
+ the pinned header a frame late and a gap flashes above it. */
701
+ transform: translateZ(0);
702
+ will-change: transform;
622
703
  }
623
704
  .ag-wday-head-left {
624
705
  display: flex;
@@ -676,7 +757,7 @@ const weekDays = $derived.by(() => {
676
757
  padding: 2px 0;
677
758
  }
678
759
  .ag-wslot-now {
679
- font-size: 9px;
760
+ font-size: 10px;
680
761
  font-weight: 700;
681
762
  letter-spacing: 0.08em;
682
763
  text-transform: uppercase;
@@ -693,9 +774,47 @@ const weekDays = $derived.by(() => {
693
774
  gap: 4px;
694
775
  }
695
776
  .ag-wday-past-line {
696
- font-size: 10px;
777
+ font-size: 11px;
697
778
  color: var(--dt-text-3, rgba(0, 0, 0, 0.38));
698
779
  padding: 6px 0 0;
780
+ }
781
+ .ag-wday-past-line--summary {
782
+ padding: 0 20px 8px;
783
+ }
784
+ /* "✓ N completed" is a disclosure — tap to reveal the finished events */
785
+ .ag-past-toggle {
786
+ display: inline-flex;
787
+ align-items: center;
788
+ gap: 5px;
789
+ cursor: pointer;
790
+ min-height: 32px;
791
+ transition: color 150ms;
792
+ -webkit-tap-highlight-color: transparent;
793
+ }
794
+ .ag-past-toggle:hover,
795
+ .ag-past-toggle:active {
796
+ color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
797
+ }
798
+ .ag-past-toggle:focus-visible {
799
+ outline: none;
800
+ box-shadow: 0 0 0 2px var(--dt-accent, #2563eb);
801
+ border-radius: 4px;
802
+ }
803
+ .ag-past-chevron {
804
+ transition: transform 120ms;
805
+ }
806
+ .ag-past-chevron--open {
807
+ transform: rotate(180deg);
808
+ }
809
+ @media (prefers-reduced-motion: reduce) {
810
+ .ag-past-chevron { transition: none; }
811
+ }
812
+ /* Revealed completed events: dim + strike, single token layer */
813
+ .ag-compact--done .ag-compact-title {
814
+ text-decoration: line-through;
815
+ text-decoration-color: var(--dt-text-3, rgba(0, 0, 0, 0.38));
816
+ }
817
+ .ag-compact--done .ag-compact-dot {
699
818
  opacity: 0.5;
700
819
  }
701
820
 
@@ -710,19 +829,31 @@ const weekDays = $derived.by(() => {
710
829
  padding: 3px 0;
711
830
  cursor: pointer;
712
831
  min-width: 0;
832
+ width: 100%;
713
833
  }
714
834
  .ag-compact--selected {
715
835
  background: color-mix(in srgb, var(--ev-color) 10%, transparent);
716
836
  border-radius: 4px;
837
+ /* Highlight gutter comes from negative margins so the row's content
838
+ stays aligned with its unselected siblings (no tap-shift). */
717
839
  padding-left: 6px;
718
840
  padding-right: 6px;
841
+ margin-left: -6px;
842
+ margin-right: -6px;
843
+ width: calc(100% + 12px);
719
844
  }
720
- .ag-compact:hover .ag-compact-title {
845
+ .ag-compact:hover .ag-compact-title,
846
+ .ag-compact:active .ag-compact-title {
721
847
  color: var(--dt-text);
722
848
  }
849
+ .ag-compact:active {
850
+ background: color-mix(in srgb, var(--ev-color) 8%, transparent);
851
+ border-radius: 4px;
852
+ }
723
853
  .ag-compact:focus-visible {
724
- outline: 2px solid var(--dt-accent, #2563eb);
725
- outline-offset: 2px;
854
+ outline: none;
855
+ box-shadow: 0 0 0 2px var(--dt-accent, #2563eb);
856
+ border-radius: 4px;
726
857
  }
727
858
  .ag-compact-dot {
728
859
  width: 5px;
@@ -741,12 +872,45 @@ const weekDays = $derived.by(() => {
741
872
  white-space: nowrap;
742
873
  line-height: 1.4;
743
874
  }
875
+ /* Title + location + subtitle + tags cluster. One line while it fits; on
876
+ mobile the metadata wraps to a second line instead of crushing the title. */
877
+ .ag-compact-main {
878
+ display: flex;
879
+ align-items: baseline;
880
+ gap: 6px;
881
+ flex: 1;
882
+ min-width: 0;
883
+ }
884
+ .ag--mobile .ag-compact-main {
885
+ flex-wrap: wrap;
886
+ row-gap: 2px;
887
+ }
888
+ /* Mobile: size the title by its content when deciding line breaks — a long
889
+ title claims the first line whole (ellipsizing only against the full row)
890
+ and pushes location/subtitle/tags down instead of truncating at 35%. */
891
+ .ag--mobile .ag-compact-title {
892
+ flex-basis: auto;
893
+ }
894
+ /* On their own wrapped line the metadata gets the full width — the tight
895
+ desktop caps would truncate it beside empty space. */
896
+ .ag--mobile .ag-compact-loc,
897
+ .ag--mobile .ag-compact-sub {
898
+ max-width: 100%;
899
+ }
900
+ /* Wrapped rows are two/three lines tall — center-aligning the dot floats
901
+ it between lines; pin it optically to the first (title) line instead. */
902
+ .ag--mobile .ag-compact-dot {
903
+ align-self: flex-start;
904
+ margin-top: 8px;
905
+ }
744
906
  .ag-compact-title {
745
907
  font-size: 12px;
746
908
  font-weight: 500;
747
909
  color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
748
910
  flex: 1;
749
- min-width: 0;
911
+ /* The title is the row's identity — never let subtitle/tags/duration
912
+ squeeze it out on narrow screens (min-width: 0 resolves to 0px). */
913
+ min-width: 35%;
750
914
  white-space: nowrap;
751
915
  overflow: hidden;
752
916
  text-overflow: ellipsis;
@@ -764,7 +928,8 @@ const weekDays = $derived.by(() => {
764
928
  .ag-compact-sub {
765
929
  font-size: 10px;
766
930
  color: var(--dt-text-3, rgba(0, 0, 0, 0.38));
767
- flex-shrink: 0;
931
+ flex-shrink: 3;
932
+ min-width: 0;
768
933
  white-space: nowrap;
769
934
  overflow: hidden;
770
935
  text-overflow: ellipsis;
@@ -772,9 +937,10 @@ const weekDays = $derived.by(() => {
772
937
  line-height: 1.4;
773
938
  }
774
939
  .ag-compact-loc {
775
- font-size: 9px;
940
+ font-size: 10px;
776
941
  color: var(--dt-text-3, rgba(0, 0, 0, 0.38));
777
- flex-shrink: 0;
942
+ flex-shrink: 3;
943
+ min-width: 0;
778
944
  white-space: nowrap;
779
945
  overflow: hidden;
780
946
  text-overflow: ellipsis;
@@ -796,21 +962,33 @@ const weekDays = $derived.by(() => {
796
962
  opacity: 0.65;
797
963
  }
798
964
  .ag-compact-tag {
799
- font: 500 8px / 1 var(--dt-sans, system-ui, sans-serif);
965
+ font: 500 10px / 1 var(--dt-sans, system-ui, sans-serif);
800
966
  color: var(--ev-color, var(--dt-accent));
801
967
  background: color-mix(in srgb, var(--ev-color, var(--dt-accent)) 12%, transparent);
802
968
  padding: 1px 4px;
803
969
  border-radius: 3px;
804
970
  white-space: nowrap;
805
- flex-shrink: 0;
971
+ flex-shrink: 1;
972
+ min-width: 2.5em;
806
973
  max-width: 80px;
807
974
  overflow: hidden;
808
975
  text-overflow: ellipsis;
809
976
  }
810
977
  .ag-compact-more {
811
978
  font-size: 11px;
812
- color: var(--dt-text-3);
979
+ color: color-mix(in srgb, var(--dt-text, rgba(0, 0, 0, 0.87)) 72%, transparent);
813
980
  padding: 2px 0 0 13px;
981
+ cursor: pointer;
982
+ display: block;
983
+ }
984
+ .ag-compact-more:hover,
985
+ .ag-compact-more:active {
986
+ color: var(--dt-text, rgba(0, 0, 0, 0.87));
987
+ }
988
+ .ag-compact-more:focus-visible {
989
+ outline: none;
990
+ box-shadow: 0 0 0 2px var(--dt-accent, #2563eb);
991
+ border-radius: 4px;
814
992
  }
815
993
 
816
994
  /* ═══ Mobile adaptations ═══ */
@@ -827,17 +1005,63 @@ const weekDays = $derived.by(() => {
827
1005
  padding: 12px 14px;
828
1006
  }
829
1007
  .ag--mobile .ag-card-title {
830
- font-size: 14px;
1008
+ font-size: 15px;
1009
+ }
1010
+ .ag--mobile .ag-card-meta {
1011
+ font-size: 12px;
1012
+ }
1013
+ .ag--mobile .ag-card-sub {
1014
+ font-size: 12px;
1015
+ }
1016
+ .ag--mobile .ag-card-loc {
1017
+ font-size: 12px;
1018
+ }
1019
+ .ag--mobile .ag-card-eta {
1020
+ font-size: 12px;
1021
+ }
1022
+ .ag--mobile .ag-card-tag {
1023
+ font-size: 11px;
831
1024
  }
832
1025
  .ag--mobile .ag-compact {
833
1026
  padding: 8px 0;
834
1027
  }
835
1028
  .ag--mobile .ag-compact-title {
836
- font-size: 14px;
1029
+ font-size: 15px;
837
1030
  }
838
1031
  .ag--mobile .ag-compact-time {
839
1032
  font-size: 12px;
840
1033
  }
1034
+ .ag--mobile .ag-compact-dur {
1035
+ font-size: 12px;
1036
+ }
1037
+ .ag--mobile .ag-compact-sub {
1038
+ font-size: 12px;
1039
+ }
1040
+ .ag--mobile .ag-compact-loc {
1041
+ font-size: 11px;
1042
+ }
1043
+ .ag--mobile .ag-compact-tag {
1044
+ font-size: 11px;
1045
+ }
1046
+ .ag--mobile .ag-compact-more {
1047
+ font-size: 12px;
1048
+ padding-top: 6px;
1049
+ }
1050
+ .ag--mobile .ag-allday-span {
1051
+ font-size: 11px;
1052
+ }
1053
+ .ag--mobile .ag-wday-badge {
1054
+ font-size: 11px;
1055
+ }
1056
+ .ag--mobile .ag-wslot-now {
1057
+ font-size: 11px;
1058
+ }
1059
+ .ag--mobile .ag-wday-empty {
1060
+ font-size: 12px;
1061
+ }
1062
+ .ag--mobile .ag-wday-past-line {
1063
+ font-size: 12px;
1064
+ }
841
1065
  .ag--mobile .ag-wslot-cards--multi {
842
1066
  grid-template-columns: 1fr;
843
1067
  }