@nomideusz/svelte-calendar 0.9.0 → 0.10.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,11 +1,11 @@
1
1
  <script lang="ts">import { createClock } from "../../core/clock.svelte.js";
2
- import { sod, DAY_MS, isAllDay, isMultiDay } from "../../core/time.js";
3
- import { getLabels } from "../../core/locale.js";
2
+ import { sod, DAY_MS, dayNum, isAllDay, isMultiDay } 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
  const emptySnippet = $derived(ctx.emptySnippet);
10
10
  let {
11
11
  locale,
@@ -26,14 +26,17 @@ const oneventhover = $derived(ctx.oneventhover);
26
26
  const disabledSet = $derived(ctx.disabledSet);
27
27
  let swipeStartX = 0;
28
28
  let swipeStartY = 0;
29
+ let swipeActive = false;
29
30
  const SWIPE_THRESHOLD = 50;
30
31
  function onPointerDown(e) {
31
- if (!isMobile) return;
32
+ if (!isMobile || e.pointerType !== "touch") return;
33
+ swipeActive = true;
32
34
  swipeStartX = e.clientX;
33
35
  swipeStartY = e.clientY;
34
36
  }
35
37
  function onPointerUp(e) {
36
- if (!isMobile) return;
38
+ if (!swipeActive || e.pointerType !== "touch") return;
39
+ swipeActive = false;
37
40
  const dx = e.clientX - swipeStartX;
38
41
  const dy = e.clientY - swipeStartY;
39
42
  if (Math.abs(dx) > SWIPE_THRESHOLD && Math.abs(dx) > Math.abs(dy) * 1.4) {
@@ -41,21 +44,19 @@ function onPointerUp(e) {
41
44
  else viewState?.next();
42
45
  }
43
46
  }
47
+ function onPointerCancel() {
48
+ swipeActive = false;
49
+ }
44
50
  const fmt = (d) => fmtTime(d, locale);
45
- const eta = (ms) => timeUntilMs(ms, clock.tick);
51
+ const eta = (ms) => timeUntilMs(ms, clock.tick, L);
46
52
  const prog = (ev) => progress(ev, clock.tick);
47
53
  function handleClick(ev) {
48
54
  oneventclick?.(ev);
49
55
  }
50
- function handleKeydown(e, ev) {
51
- if (e.key === "Enter" || e.key === " ") {
52
- e.preventDefault();
53
- oneventclick?.(ev);
54
- }
55
- }
56
56
  const dayMs = $derived(focusDate ? sod(focusDate.getTime()) : clock.today);
57
57
  const dayEnd = $derived(dayMs + DAY_MS);
58
58
  const isToday = $derived(dayMs === clock.today);
59
+ const isTomorrow = $derived(dayMs === clock.today + DAY_MS);
59
60
  const isPastDay = $derived(equalDays ? false : dayMs < clock.today);
60
61
  const dayEvents = $derived.by(() => {
61
62
  return events.filter((ev) => ev.start.getTime() < dayEnd && ev.end.getTime() > dayMs).sort((a, b) => a.start.getTime() - b.start.getTime());
@@ -102,28 +103,37 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
102
103
  style:height={height ? `${height}px` : undefined}
103
104
  onpointerdown={onPointerDown}
104
105
  onpointerup={onPointerUp}
106
+ onpointercancel={onPointerCancel}
105
107
  >
106
- <div class="ag-body" role="list" aria-label={L.todaysLineup}>
108
+ <div class="ag-body" role="group" aria-label={L.todaysLineup}>
109
+ <!-- ─── In-view date header (swipe nav is otherwise unlabelled) ─── -->
110
+ <div class="ag-day-head">
111
+ {#if !equalDays && isToday}
112
+ <span class="ag-day-head-badge">{L.today}</span>
113
+ {:else if !equalDays && isTomorrow}
114
+ <span class="ag-day-head-badge ag-day-head-badge--muted">{L.tomorrow}</span>
115
+ {/if}
116
+ <span class="ag-day-head-name">{weekdayLong(dayMs, locale)}</span>
117
+ <span class="ag-day-head-date">{monthLong(dayMs, locale)} {dayNum(dayMs)}</span>
118
+ </div>
107
119
  {#if allDayBanner.length > 0}
108
120
  <!-- ─── All-day / multi-day events ─── -->
109
121
  <div class="ag-allday">
110
122
  <div class="ag-allday-label">{L.allDay}</div>
111
123
  <div class="ag-allday-items">
112
124
  {#each allDayBanner as ev (ev.id)}
113
- <div
125
+ <button
126
+ type="button"
114
127
  class="ag-allday-chip"
115
128
  class:ag-allday-chip--selected={selectedEventId === ev.id}
116
129
  style:--ev-color={ev.color || 'var(--dt-accent)'}
117
- role="button"
118
- tabindex="0"
119
130
  aria-label="{ev.title}, {L.allDay}"
120
131
  onclick={() => handleClick(ev)}
121
132
  onpointerenter={() => oneventhover?.(ev)}
122
- onkeydown={(e) => handleKeydown(e, ev)}
123
133
  >
124
134
  <span class="ag-allday-dot"></span>
125
135
  <span class="ag-allday-title">{ev.title}</span>
126
- </div>
136
+ </button>
127
137
  {/each}
128
138
  </div>
129
139
  </div>
@@ -138,18 +148,16 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
138
148
  </div>
139
149
  {:else}
140
150
  {#each timedDayEvents as ev (ev.id)}
141
- <div
151
+ <button
152
+ type="button"
142
153
  class="ag-compact-row"
143
154
  class:ag-compact-row--selected={selectedEventId === ev.id}
144
155
  class:ag-compact-row--cancelled={ev.status === 'cancelled'}
145
156
  class:ag-compact-row--tentative={ev.status === 'tentative'}
146
157
  style:--ev-color={ev.color || 'var(--dt-accent)'}
147
- role="button"
148
- tabindex="0"
149
158
  aria-label="{ev.title}, {fmt(ev.start)}, {duration(ev)}"
150
159
  onclick={() => handleClick(ev)}
151
160
  onpointerenter={() => oneventhover?.(ev)}
152
- onkeydown={(e) => handleKeydown(e, ev)}
153
161
  >
154
162
  <EventContent event={ev}>
155
163
  <span class="ag-compact-row-dot"></span>
@@ -165,7 +173,7 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
165
173
  {/if}
166
174
  <span class="ag-compact-row-dur">{duration(ev)}</span>
167
175
  </EventContent>
168
- </div>
176
+ </button>
169
177
  {/each}
170
178
  {/if}
171
179
  </div>
@@ -178,23 +186,23 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
178
186
  <div class="ag-q-label">{L.now} <span class="ag-q-clock">{clock.hm}</span></div>
179
187
  {#if dayCat.current.length > 0}
180
188
  {#each dayCat.current as ev (ev.id)}
181
- <div
189
+ <button
190
+ type="button"
182
191
  class="ag-q-now"
183
192
  class:ag-q-now--selected={selectedEventId === ev.id}
184
193
  style:--ev-color={ev.color || 'var(--dt-accent)'}
185
- role="button"
186
- tabindex="0"
187
194
  aria-label="{ev.title}, {L.happeningNow}, {L.percentComplete(Math.round(prog(ev) * 100))}"
188
- onclick={() => handleClick(ev)} onpointerenter={() => oneventhover?.(ev)} onkeydown={(e) => handleKeydown(e, ev)}
195
+ onclick={() => handleClick(ev)}
196
+ onpointerenter={() => oneventhover?.(ev)}
189
197
  >
190
198
  <div class="ag-q-now-dot"></div>
191
199
  <div class="ag-q-now-title">{ev.title}</div>
192
200
  {#if ev.subtitle}<div class="ag-q-now-sub">{ev.subtitle}</div>{/if}
193
201
  <div class="ag-q-now-time">{L.until} {fmt(ev.end)}</div>
194
202
  <div class="ag-q-now-track">
195
- <div class="ag-q-now-fill" style:width="{prog(ev) * 100}%"></div>
203
+ <div class="ag-q-now-fill" style:transform="scaleX({prog(ev)})"></div>
196
204
  </div>
197
- </div>
205
+ </button>
198
206
  {/each}
199
207
  {:else}
200
208
  <div class="ag-q-free">
@@ -206,21 +214,19 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
206
214
  <div class="ag-q-done-section">
207
215
  <div class="ag-q-label">{L.done}</div>
208
216
  {#each visibleDone as ev (ev.id)}
209
- <div
217
+ <button
218
+ type="button"
210
219
  class="ag-q-done-item"
211
220
  class:ag-q-done-item--selected={selectedEventId === ev.id}
212
- role="button"
213
- tabindex="0"
214
221
  aria-label="{ev.title}, {L.completed}, {fmt(ev.start)}"
215
222
  onclick={() => handleClick(ev)}
216
- onkeydown={(e) => handleKeydown(e, ev)}
217
223
  >
218
224
  <span class="ag-q-done-check">✓</span>
219
225
  <span class="ag-q-done-title">{ev.title}</span>
220
- </div>
226
+ </button>
221
227
  {/each}
222
228
  {#if hiddenDoneCount > 0}
223
- <button class="ag-q-done-toggle" onclick={() => (showAllDone = !showAllDone)}>
229
+ <button type="button" class="ag-q-done-toggle" onclick={() => (showAllDone = !showAllDone)}>
224
230
  {showAllDone ? L.showLess : L.nCompleted(hiddenDoneCount)}
225
231
  </button>
226
232
  {/if}
@@ -233,21 +239,19 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
233
239
  <div class="ag-q-label">{L.upNext}</div>
234
240
  {#if upcomingNext.length === 0}
235
241
  <div class="ag-q-empty">
236
- {dayCat.past.length > 0 ? L.allDoneForToday : L.nothingScheduled}
242
+ {#if emptySnippet}{@render emptySnippet()}{:else}{dayCat.past.length > 0 ? L.allDoneForToday : L.nothingScheduled}{/if}
237
243
  </div>
238
244
  {:else}
239
245
  {#each upcomingNext as ev, i (ev.id)}
240
246
  {#if i >= UPCOMING_CARDS}
241
- <div
247
+ <button
248
+ type="button"
242
249
  class="ag-compact-row ag-compact-row--queue"
243
250
  class:ag-compact-row--selected={selectedEventId === ev.id}
244
251
  style:--ev-color={ev.color || 'var(--dt-accent)'}
245
- role="button"
246
- tabindex="0"
247
252
  aria-label="{ev.title}, {fmt(ev.start)}, {duration(ev)}"
248
253
  onclick={() => handleClick(ev)}
249
254
  onpointerenter={() => oneventhover?.(ev)}
250
- onkeydown={(e) => handleKeydown(e, ev)}
251
255
  >
252
256
  <EventContent event={ev}>
253
257
  <span class="ag-compact-row-dot"></span>
@@ -257,19 +261,17 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
257
261
  <span class="ag-compact-row-sub">{ev.subtitle}</span>
258
262
  {/if}
259
263
  </EventContent>
260
- </div>
264
+ </button>
261
265
  {:else}
262
- <div
266
+ <button
267
+ type="button"
263
268
  class="ag-card ag-card--q"
264
269
  class:ag-card--hero={i === 0}
265
270
  class:ag-card--selected={selectedEventId === ev.id}
266
271
  style:--ev-color={ev.color || 'var(--dt-accent)'}
267
- role="button"
268
- tabindex="0"
269
272
  aria-label="{ev.title}, {fmt(ev.start)}, {duration(ev)}"
270
273
  onclick={() => handleClick(ev)}
271
274
  onpointerenter={() => oneventhover?.(ev)}
272
- onkeydown={(e) => handleKeydown(e, ev)}
273
275
  >
274
276
  <div class="ag-card-body">
275
277
  <EventContent event={ev}>
@@ -293,7 +295,7 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
293
295
  {/if}
294
296
  </EventContent>
295
297
  </div>
296
- </div>
298
+ </button>
297
299
  {/if}
298
300
  {/each}
299
301
  {/if}
@@ -304,24 +306,26 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
304
306
  <!-- ─── Past day: "The Log" — everything happened ─── -->
305
307
  <div class="ag-log">
306
308
  {#if timedDayEvents.length === 0 && allDayBanner.length === 0}
307
- <div class="ag-q-empty">{L.nothingWasScheduled}</div>
309
+ <div class="ag-q-empty">
310
+ {#if emptySnippet}{@render emptySnippet()}{:else}{L.nothingWasScheduled}{/if}
311
+ </div>
308
312
  {:else}
309
313
  {#each timedDayEvents as ev (ev.id)}
310
- <div
314
+ <button
315
+ type="button"
311
316
  class="ag-log-row"
312
317
  class:ag-log-row--selected={selectedEventId === ev.id}
313
318
  style:--ev-color={ev.color || 'var(--dt-accent)'}
314
- role="button"
315
- tabindex="0"
316
319
  aria-label="{ev.title}, {fmt(ev.start)} to {fmt(ev.end)}"
317
- onclick={() => handleClick(ev)} onpointerenter={() => oneventhover?.(ev)} onkeydown={(e) => handleKeydown(e, ev)}
320
+ onclick={() => handleClick(ev)}
321
+ onpointerenter={() => oneventhover?.(ev)}
318
322
  >
319
323
  <span class="ag-log-check">✓</span>
320
324
  <span class="ag-log-time">{fmt(ev.start)}</span>
321
325
  <span class="ag-log-dot" style:background={ev.color || 'var(--dt-accent)'}></span>
322
326
  <span class="ag-log-title">{ev.title}</span>
323
327
  <span class="ag-log-dur">{duration(ev)}</span>
324
- </div>
328
+ </button>
325
329
  {/each}
326
330
  {/if}
327
331
  </div>
@@ -335,7 +339,8 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
335
339
  </div>
336
340
  {:else}
337
341
  {#each timedDayEvents as ev, i (ev.id)}
338
- <div
342
+ <button
343
+ type="button"
339
344
  class="ag-card ag-card--plan"
340
345
  class:ag-card--first={i === 0}
341
346
  class:ag-card--selected={selectedEventId === ev.id}
@@ -344,10 +349,9 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
344
349
  class:ag-card--full={ev.status === 'full'}
345
350
  class:ag-card--limited={ev.status === 'limited'}
346
351
  style:--ev-color={ev.color || 'var(--dt-accent)'}
347
- role="button"
348
- tabindex="0"
349
352
  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)}"
350
- onclick={() => handleClick(ev)} onpointerenter={() => oneventhover?.(ev)} onkeydown={(e) => handleKeydown(e, ev)}
353
+ onclick={() => handleClick(ev)}
354
+ onpointerenter={() => oneventhover?.(ev)}
351
355
  >
352
356
  <div class="ag-card-body">
353
357
  <EventContent event={ev}>
@@ -374,7 +378,7 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
374
378
  {/if}
375
379
  </EventContent>
376
380
  </div>
377
- </div>
381
+ </button>
378
382
  {/each}
379
383
  {/if}
380
384
  </div>
@@ -388,7 +392,6 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
388
392
  .ag {
389
393
  position: relative;
390
394
  overflow: hidden;
391
- user-select: none;
392
395
  display: flex;
393
396
  flex-direction: column;
394
397
  height: 100%;
@@ -401,6 +404,27 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
401
404
 
402
405
  .ag--auto { height: auto; overflow: visible; }
403
406
 
407
+ /* Button UA reset for interactive cards/rows (real <button>s for a11y).
408
+ Placed first so later component rules override it.
409
+ user-select is scoped here (not on .ag) so event text stays copyable. */
410
+ .ag-card,
411
+ .ag-allday-chip,
412
+ .ag-compact-row,
413
+ .ag-q-now,
414
+ .ag-q-done-item,
415
+ .ag-log-row,
416
+ .ag-q-done-toggle {
417
+ font: inherit;
418
+ color: inherit;
419
+ text-align: left;
420
+ background: none;
421
+ border: none;
422
+ padding: 0;
423
+ margin: 0;
424
+ box-sizing: border-box;
425
+ user-select: none;
426
+ }
427
+
404
428
  .ag--disabled {
405
429
  background-image: repeating-linear-gradient(
406
430
  135deg,
@@ -420,10 +444,45 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
420
444
  flex-direction: column;
421
445
  overflow-y: auto;
422
446
  overflow-x: hidden;
447
+ overscroll-behavior: contain;
423
448
  padding-top: 8px;
424
449
  scrollbar-width: thin;
425
450
  scrollbar-color: var(--dt-border) transparent;
426
451
  }
452
+
453
+ /* ═══ In-view date header ═══ */
454
+ .ag-day-head {
455
+ display: flex;
456
+ align-items: baseline;
457
+ gap: 8px;
458
+ padding: 0 16px 6px;
459
+ flex-shrink: 0;
460
+ }
461
+ .ag-day-head-badge {
462
+ font-size: 10px;
463
+ font-weight: 600;
464
+ letter-spacing: 0.08em;
465
+ text-transform: uppercase;
466
+ color: var(--dt-accent, #2563eb);
467
+ background: color-mix(in srgb, var(--dt-accent, #2563eb) 12%, transparent);
468
+ padding: 2px 7px;
469
+ border-radius: 3px;
470
+ }
471
+ .ag-day-head-badge--muted {
472
+ color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
473
+ background: color-mix(in srgb, var(--dt-text-2, rgba(0, 0, 0, 0.54)) 10%, transparent);
474
+ }
475
+ .ag-day-head-name {
476
+ font-size: 13px;
477
+ font-weight: 600;
478
+ line-height: 1.2;
479
+ }
480
+ .ag-day-head-date {
481
+ font-size: 11px;
482
+ font-family: var(--dt-mono, monospace);
483
+ color: var(--dt-text-3, rgba(0, 0, 0, 0.38));
484
+ line-height: 1.2;
485
+ }
427
486
  .ag--auto .ag-body { overflow-y: visible; min-height: auto; }
428
487
  .ag-body::-webkit-scrollbar {
429
488
  width: 4px;
@@ -465,13 +524,14 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
465
524
  cursor: pointer;
466
525
  transition: background 0.15s, border-color 0.15s;
467
526
  }
468
- .ag-allday-chip:hover {
527
+ .ag-allday-chip:hover,
528
+ .ag-allday-chip:active {
469
529
  background: color-mix(in srgb, var(--ev-color) 22%, var(--dt-surface, var(--dt-bg, #ffffff)));
470
530
  border-color: color-mix(in srgb, var(--ev-color) 35%, transparent);
471
531
  }
472
532
  .ag-allday-chip:focus-visible {
473
- outline: 2px solid var(--dt-accent, #2563eb);
474
- outline-offset: 2px;
533
+ outline: none;
534
+ box-shadow: 0 0 0 2px var(--dt-accent, #2563eb);
475
535
  }
476
536
  .ag-allday-chip--selected {
477
537
  border-color: var(--ev-color);
@@ -501,13 +561,14 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
501
561
  cursor: pointer;
502
562
  transition: background 150ms, border-color 150ms;
503
563
  }
504
- .ag-card:hover {
564
+ .ag-card:hover,
565
+ .ag-card:active {
505
566
  background: color-mix(in srgb, var(--ev-color) 25%, var(--dt-surface, var(--dt-bg, #ffffff)));
506
567
  border-color: color-mix(in srgb, var(--ev-color) 40%, transparent);
507
568
  }
508
569
  .ag-card:focus-visible {
509
- outline: 2px solid var(--dt-accent, #2563eb);
510
- outline-offset: 2px;
570
+ outline: none;
571
+ box-shadow: 0 0 0 2px var(--dt-accent, #2563eb);
511
572
  }
512
573
  .ag-card--selected {
513
574
  border-color: var(--ev-color);
@@ -579,7 +640,7 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
579
640
  flex-wrap: wrap;
580
641
  }
581
642
  .ag-card-tag {
582
- font: 500 9px / 1 var(--dt-sans, system-ui, sans-serif);
643
+ font: 500 10px / 1 var(--dt-sans, system-ui, sans-serif);
583
644
  color: var(--ev-color, var(--dt-accent));
584
645
  background: color-mix(in srgb, var(--ev-color, var(--dt-accent)) 15%, transparent);
585
646
  padding: 2px 5px;
@@ -602,7 +663,7 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
602
663
  margin-top: 2px;
603
664
  }
604
665
  .ag-card-eta {
605
- font-size: 9px;
666
+ font-size: 11px;
606
667
  font-weight: 600;
607
668
  letter-spacing: 0.04em;
608
669
  color: var(--dt-accent, #2563eb);
@@ -671,19 +732,22 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
671
732
  padding: 8px 0 10px;
672
733
  min-height: 0;
673
734
  }
674
- /* Mobile: stack queue columns vertically */
735
+ /* Mobile: stack queue columns vertically — "Up next" (hero) first,
736
+ Now/Done status column second */
675
737
  .ag--mobile .ag-q {
676
738
  grid-template-columns: 1fr;
677
739
  min-height: auto;
678
740
  }
679
741
  .ag--mobile .ag-q-status {
742
+ order: 2;
680
743
  border-right: none;
681
- border-bottom: 1px solid var(--dt-border, rgba(0, 0, 0, 0.08));
682
- padding-bottom: 10px;
683
- margin-bottom: 8px;
744
+ border-top: 1px solid var(--dt-border, rgba(0, 0, 0, 0.08));
745
+ padding-top: 10px;
746
+ margin-top: 8px;
684
747
  overflow-y: visible;
685
748
  }
686
749
  .ag--mobile .ag-q-queue {
750
+ order: 1;
687
751
  overflow-y: visible;
688
752
  padding-bottom: 16px;
689
753
  }
@@ -713,8 +777,52 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
713
777
  .ag--mobile .ag-card--plan .ag-card-title {
714
778
  font-size: 15px;
715
779
  }
780
+ /* Mobile: Now/Done status subtree type scale */
781
+ .ag--mobile .ag-q-label {
782
+ font-size: 11px;
783
+ }
784
+ .ag--mobile .ag-q-clock {
785
+ font-size: 12px;
786
+ }
787
+ .ag--mobile .ag-q-now-title {
788
+ font-size: 16px;
789
+ }
790
+ .ag--mobile .ag-q-now-sub {
791
+ font-size: 13px;
792
+ }
793
+ .ag--mobile .ag-q-now-time {
794
+ font-size: 12px;
795
+ }
796
+ .ag--mobile .ag-q-free-label {
797
+ font-size: 13px;
798
+ }
799
+ .ag--mobile .ag-q-done-title {
800
+ font-size: 13px;
801
+ }
802
+ .ag--mobile .ag-q-done-check {
803
+ font-size: 12px;
804
+ }
805
+ .ag--mobile .ag-card-eta {
806
+ font-size: 12px;
807
+ }
808
+ .ag--mobile .ag-card-sub {
809
+ font-size: 12px;
810
+ }
811
+ .ag--mobile .ag-card-loc {
812
+ font-size: 12px;
813
+ }
814
+ .ag--mobile .ag-card-tag {
815
+ font-size: 11px;
816
+ }
817
+ .ag--mobile .ag-log-time,
818
+ .ag--mobile .ag-log-dur {
819
+ font-size: 12px;
820
+ }
821
+ .ag--mobile .ag-log-title {
822
+ font-size: 15px;
823
+ }
716
824
  .ag-q-label {
717
- font-size: 8px;
825
+ font-size: 10px;
718
826
  font-weight: 600;
719
827
  letter-spacing: 0.14em;
720
828
  text-transform: uppercase;
@@ -757,10 +865,15 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
757
865
  color: var(--dt-text-3);
758
866
  cursor: pointer;
759
867
  }
760
- .ag-q-done-toggle:hover {
868
+ .ag-q-done-toggle:hover,
869
+ .ag-q-done-toggle:active {
761
870
  color: var(--dt-text);
762
871
  border-color: var(--dt-text-3);
763
872
  }
873
+ .ag-q-done-toggle:focus-visible {
874
+ outline: none;
875
+ box-shadow: 0 0 0 2px var(--dt-accent, #2563eb);
876
+ }
764
877
  .ag-q-now-sub {
765
878
  font-size: 12px;
766
879
  color: var(--dt-text-2);
@@ -772,13 +885,15 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
772
885
  border-top: 1px solid var(--dt-border, rgba(0, 0, 0, 0.08));
773
886
  }
774
887
  .ag-q-clock {
775
- font-size: 10px;
888
+ font-size: 11px;
776
889
  font-weight: 600;
777
890
  font-family: var(--dt-mono, monospace);
778
891
  color: var(--dt-accent, #2563eb);
779
892
  margin-left: 4px;
780
893
  }
781
894
  .ag-q-now {
895
+ display: block;
896
+ width: 100%;
782
897
  padding: 8px 10px;
783
898
  margin-bottom: 8px;
784
899
  border-radius: 8px;
@@ -786,15 +901,15 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
786
901
  border: 1px solid color-mix(in srgb, var(--ev-color, var(--dt-accent)) 15%, transparent);
787
902
  cursor: pointer;
788
903
  transition: background 150ms, border-color 150ms;
789
- margin-right: 10px;
790
904
  }
791
- .ag-q-now:hover {
905
+ .ag-q-now:hover,
906
+ .ag-q-now:active {
792
907
  background: color-mix(in srgb, var(--ev-color, var(--dt-accent)) 25%, var(--dt-surface, var(--dt-bg, #ffffff)));
793
908
  border-color: color-mix(in srgb, var(--ev-color, var(--dt-accent)) 35%, transparent);
794
909
  }
795
910
  .ag-q-now:focus-visible {
796
- outline: 2px solid var(--dt-accent, #2563eb);
797
- outline-offset: 2px;
911
+ outline: none;
912
+ box-shadow: 0 0 0 2px var(--dt-accent, #2563eb);
798
913
  }
799
914
  .ag-q-now--selected {
800
915
  border-color: var(--ev-color, var(--dt-accent));
@@ -811,18 +926,29 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
811
926
  0%, 100% { opacity: 1; }
812
927
  50% { opacity: 0.4; }
813
928
  }
929
+ @media (prefers-reduced-motion: reduce) {
930
+ .ag-q-now-dot {
931
+ animation: none;
932
+ }
933
+ .ag-q-now-fill {
934
+ transition: none;
935
+ }
936
+ }
814
937
  .ag-q-now-title {
815
- font-size: 11px;
938
+ font-size: 12px;
816
939
  font-weight: 600;
817
- line-height: 1.2;
940
+ line-height: 1.25;
818
941
  color: var(--dt-text, rgba(0, 0, 0, 0.87));
819
- white-space: nowrap;
942
+ display: -webkit-box;
943
+ -webkit-box-orient: vertical;
944
+ -webkit-line-clamp: 2;
945
+ line-clamp: 2;
820
946
  overflow: hidden;
821
- text-overflow: ellipsis;
947
+ word-break: break-word;
822
948
  margin-bottom: 3px;
823
949
  }
824
950
  .ag-q-now-time {
825
- font-size: 9px;
951
+ font-size: 11px;
826
952
  font-family: var(--dt-mono, monospace);
827
953
  color: var(--dt-text-3, rgba(0, 0, 0, 0.38));
828
954
  margin-bottom: 6px;
@@ -835,9 +961,11 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
835
961
  }
836
962
  .ag-q-now-fill {
837
963
  height: 100%;
964
+ width: 100%;
838
965
  background: var(--ev-color, var(--dt-accent, #2563eb));
839
966
  border-radius: 1px;
840
- transition: width 1s linear;
967
+ transform-origin: left;
968
+ transition: transform 1s linear;
841
969
  }
842
970
  .ag-q-free {
843
971
  padding: 8px 10px;
@@ -869,43 +997,43 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
869
997
  align-items: center;
870
998
  gap: 5px;
871
999
  padding: 3px 0;
872
- opacity: 0.35;
1000
+ width: 100%;
873
1001
  cursor: pointer;
874
1002
  }
875
- .ag-q-done-item:hover {
876
- opacity: 0.6;
1003
+ .ag-q-done-item:hover .ag-q-done-title,
1004
+ .ag-q-done-item:active .ag-q-done-title,
1005
+ .ag-q-done-item--selected .ag-q-done-title {
1006
+ color: var(--dt-text, rgba(0, 0, 0, 0.87));
877
1007
  }
878
1008
  .ag-q-done-item:focus-visible {
879
- outline: 2px solid var(--dt-accent, #2563eb);
880
- outline-offset: 2px;
881
- opacity: 0.6;
882
- }
883
- .ag-q-done-item--selected {
884
- opacity: 0.7;
1009
+ outline: none;
1010
+ box-shadow: 0 0 0 2px var(--dt-accent, #2563eb);
1011
+ border-radius: 4px;
885
1012
  }
886
1013
  .ag-q-done-check {
887
- font-size: 9px;
1014
+ font-size: 11px;
888
1015
  color: var(--dt-success, rgba(22, 163, 74, 0.7));
889
1016
  flex-shrink: 0;
890
1017
  }
891
1018
  .ag-q-done-title {
892
- font-size: 10px;
1019
+ font-size: 12px;
893
1020
  line-height: 1.2;
894
- color: var(--dt-text-3, rgba(0, 0, 0, 0.38));
1021
+ color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
895
1022
  white-space: nowrap;
896
1023
  overflow: hidden;
897
1024
  text-overflow: ellipsis;
898
1025
  text-decoration: line-through;
899
1026
  text-decoration-color: var(--dt-text-3, rgba(0, 0, 0, 0.38));
1027
+ transition: color 150ms;
900
1028
  }
901
1029
 
902
- /* ═══ Past Day: "The Log" ═══ */
1030
+ /* ═══ Past Day: "The Log" ═══
1031
+ Dim comes from text tokens only (single layer) — no subtree opacity. */
903
1032
  .ag-log {
904
1033
  flex: 1;
905
1034
  padding: 8px 20px 12px;
906
1035
  overflow-y: auto;
907
1036
  scrollbar-width: none;
908
- opacity: 0.7;
909
1037
  }
910
1038
  .ag-log::-webkit-scrollbar {
911
1039
  display: none;
@@ -915,22 +1043,24 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
915
1043
  align-items: center;
916
1044
  gap: 10px;
917
1045
  padding: 8px 0;
1046
+ width: 100%;
918
1047
  border-bottom: 1px solid var(--dt-border, rgba(0, 0, 0, 0.08));
919
1048
  cursor: pointer;
920
- transition: opacity 150ms;
921
1049
  }
922
1050
  .ag-log-row:last-child {
923
1051
  border-bottom: none;
924
1052
  }
925
- .ag-log-row:hover {
926
- opacity: 1;
1053
+ .ag-log-row:hover .ag-log-title,
1054
+ .ag-log-row:active .ag-log-title,
1055
+ .ag-log-row--selected .ag-log-title {
1056
+ color: var(--dt-text, rgba(0, 0, 0, 0.87));
927
1057
  }
928
1058
  .ag-log-row:focus-visible {
929
- outline: 2px solid var(--dt-accent, #2563eb);
930
- outline-offset: 2px;
1059
+ outline: none;
1060
+ box-shadow: 0 0 0 2px var(--dt-accent, #2563eb);
1061
+ border-radius: 6px;
931
1062
  }
932
1063
  .ag-log-row--selected {
933
- opacity: 1;
934
1064
  background: color-mix(in srgb, var(--ev-color) 6%, transparent);
935
1065
  border-radius: 6px;
936
1066
  padding-left: 8px;
@@ -966,9 +1096,11 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
966
1096
  text-overflow: ellipsis;
967
1097
  text-decoration: line-through;
968
1098
  text-decoration-color: var(--dt-border, rgba(0, 0, 0, 0.08));
1099
+ transition: color 150ms;
1100
+ text-align: left;
969
1101
  }
970
1102
  .ag-log-dur {
971
- font-size: 10px;
1103
+ font-size: 11px;
972
1104
  font-family: var(--dt-mono, monospace);
973
1105
  color: var(--dt-text-3, rgba(0, 0, 0, 0.38));
974
1106
  flex-shrink: 0;
@@ -989,6 +1121,7 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
989
1121
  padding: 4px 0;
990
1122
  cursor: pointer;
991
1123
  min-width: 0;
1124
+ width: 100%;
992
1125
  }
993
1126
  .ag-compact-row--selected {
994
1127
  background: color-mix(in srgb, var(--ev-color) 10%, transparent);
@@ -996,10 +1129,16 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
996
1129
  padding-left: 6px;
997
1130
  padding-right: 6px;
998
1131
  }
999
- .ag-compact-row:hover .ag-compact-row-title { color: var(--dt-text); }
1132
+ .ag-compact-row:hover .ag-compact-row-title,
1133
+ .ag-compact-row:active .ag-compact-row-title { color: var(--dt-text); }
1134
+ .ag-compact-row:active {
1135
+ background: color-mix(in srgb, var(--ev-color) 8%, transparent);
1136
+ border-radius: 4px;
1137
+ }
1000
1138
  .ag-compact-row:focus-visible {
1001
- outline: 2px solid var(--dt-accent, #2563eb);
1002
- outline-offset: 2px;
1139
+ outline: none;
1140
+ box-shadow: 0 0 0 2px var(--dt-accent, #2563eb);
1141
+ border-radius: 4px;
1003
1142
  }
1004
1143
  .ag-compact-row-dot {
1005
1144
  width: 5px;
@@ -1020,13 +1159,14 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
1020
1159
  .ag-compact-row-title {
1021
1160
  font-size: 12px;
1022
1161
  font-weight: 500;
1023
- color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
1162
+ color: color-mix(in srgb, var(--dt-text, rgba(0, 0, 0, 0.87)) 82%, transparent);
1024
1163
  flex: 1;
1025
1164
  white-space: nowrap;
1026
1165
  overflow: hidden;
1027
1166
  text-overflow: ellipsis;
1028
1167
  transition: color 150ms;
1029
1168
  line-height: 1.4;
1169
+ text-align: left;
1030
1170
  }
1031
1171
  .ag-compact-row-dur {
1032
1172
  font-size: 10px;
@@ -1046,7 +1186,7 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
1046
1186
  line-height: 1.4;
1047
1187
  }
1048
1188
  .ag-compact-row-tag {
1049
- font: 500 8px / 1 var(--dt-sans, system-ui, sans-serif);
1189
+ font: 500 10px / 1 var(--dt-sans, system-ui, sans-serif);
1050
1190
  color: var(--ev-color, var(--dt-accent));
1051
1191
  background: color-mix(in srgb, var(--ev-color, var(--dt-accent)) 12%, transparent);
1052
1192
  padding: 1px 4px;
@@ -1059,8 +1199,17 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
1059
1199
  .ag-compact-row--tentative { opacity: 0.65; }
1060
1200
  /* Mobile: larger touch targets for compact rows */
1061
1201
  .ag--mobile .ag-compact-row { padding: 8px 0; }
1062
- .ag--mobile .ag-compact-row-title { font-size: 14px; }
1202
+ .ag--mobile .ag-compact-row-title { font-size: 15px; }
1063
1203
  .ag--mobile .ag-compact-row-time { font-size: 12px; }
1204
+ .ag--mobile .ag-compact-row-dur { font-size: 12px; }
1205
+ .ag--mobile .ag-compact-row-sub { font-size: 12px; }
1206
+ .ag--mobile .ag-compact-row-tag { font-size: 11px; }
1207
+ .ag--mobile .ag-day-head { padding: 0 16px 8px; }
1208
+ .ag--mobile .ag-day-head-name { font-size: 15px; }
1209
+ .ag--mobile .ag-day-head-date { font-size: 12px; }
1210
+ .ag--mobile .ag-day-head-badge { font-size: 11px; }
1211
+ .ag--mobile .ag-allday-title { font-size: 0.85rem; }
1212
+ .ag--mobile .ag-allday-label { font-size: 11px; }
1064
1213
 
1065
1214
  /* ═══ Future Day: "The Plan" ═══ */
1066
1215
  .ag-plan {