@nomideusz/svelte-calendar 0.6.3 → 0.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +183 -5
  2. package/dist/adapters/composite.d.ts +15 -0
  3. package/dist/adapters/composite.js +65 -0
  4. package/dist/adapters/index.d.ts +5 -1
  5. package/dist/adapters/index.js +2 -0
  6. package/dist/adapters/mapped.d.ts +170 -0
  7. package/dist/adapters/mapped.js +343 -0
  8. package/dist/adapters/recurring.d.ts +4 -0
  9. package/dist/adapters/recurring.js +5 -10
  10. package/dist/adapters/types.d.ts +11 -2
  11. package/dist/calendar/Calendar.svelte +147 -76
  12. package/dist/calendar/Calendar.svelte.d.ts +12 -0
  13. package/dist/core/index.d.ts +3 -1
  14. package/dist/core/index.js +2 -0
  15. package/dist/core/measure.d.ts +104 -0
  16. package/dist/core/measure.js +195 -0
  17. package/dist/core/time.js +2 -2
  18. package/dist/core/types.d.ts +32 -0
  19. package/dist/engine/event-store.svelte.js +22 -7
  20. package/dist/headless/create-agenda.svelte.d.ts +67 -0
  21. package/dist/headless/create-agenda.svelte.js +139 -0
  22. package/dist/headless/create-calendar.svelte.d.ts +2 -0
  23. package/dist/headless/create-calendar.svelte.js +380 -0
  24. package/dist/headless/index.d.ts +4 -0
  25. package/dist/headless/index.js +2 -0
  26. package/dist/headless/types.d.ts +219 -0
  27. package/dist/headless/types.js +1 -0
  28. package/dist/index.d.ts +6 -4
  29. package/dist/index.js +4 -2
  30. package/dist/primitives/EventBlock.svelte +87 -2
  31. package/dist/views/agenda/AgendaDay.svelte +183 -89
  32. package/dist/views/agenda/AgendaWeek.svelte +148 -95
  33. package/dist/views/mobile/MobileDay.svelte +43 -16
  34. package/dist/views/mobile/MobileWeek.svelte +32 -17
  35. package/dist/views/planner/PlannerDay.svelte +91 -36
  36. package/dist/views/planner/PlannerWeek.svelte +107 -132
  37. package/dist/views/shared/context.svelte.d.ts +47 -0
  38. package/dist/views/shared/context.svelte.js +47 -0
  39. package/dist/views/shared/format.d.ts +19 -0
  40. package/dist/views/shared/format.js +46 -0
  41. package/dist/views/shared/index.d.ts +4 -0
  42. package/dist/views/shared/index.js +2 -0
  43. package/package.json +76 -71
@@ -48,13 +48,19 @@
48
48
  }: Props = $props();
49
49
 
50
50
  const accentColor = $derived(event.color || 'var(--dt-accent, #ef4444)');
51
+ const isCancelled = $derived(event.status === 'cancelled');
52
+ const isTentative = $derived(event.status === 'tentative');
53
+ const isFull = $derived(event.status === 'full');
54
+ const isLimited = $derived(event.status === 'limited');
51
55
 
52
56
  const ariaLabel = $derived.by(() => {
53
57
  const t = event.title;
54
58
  const time = `${fmtTime(event.start)} to ${fmtTime(event.end)}`;
55
59
  const dur = fmtDuration(event.start, event.end);
56
- const status = active ? `, ${L.happeningNow}` : past ? `, ${L.past}` : '';
57
- return `${t}, ${time}, ${dur}${status}`;
60
+ const loc = event.location ? `, ${event.location}` : '';
61
+ const statusStr = isCancelled ? ', cancelled' : isTentative ? ', tentative' : isFull ? ', full' : isLimited ? ', limited' : '';
62
+ const activeStr = active ? `, ${L.happeningNow}` : past ? `, ${L.past}` : '';
63
+ return `${t}${loc}, ${time}, ${dur}${statusStr}${activeStr}`;
58
64
  });
59
65
 
60
66
  function handleKeydown(e: KeyboardEvent) {
@@ -79,6 +85,9 @@
79
85
  {#if event.subtitle}
80
86
  <span class="eb-subtitle">{event.subtitle}</span>
81
87
  {/if}
88
+ {#if event.location}
89
+ <span class="eb-location">📍 {event.location}</span>
90
+ {/if}
82
91
  {#if showTime}
83
92
  <span class="eb-time">{fmtTime(event.start)} – {fmtTime(event.end)}</span>
84
93
  {/if}
@@ -92,6 +101,10 @@
92
101
  {/each}
93
102
  </div>
94
103
  {/if}
104
+ {#if isCancelled}<span class="eb-status-badge eb-cancelled-badge">Cancelled</span>{/if}
105
+ {#if isTentative}<span class="eb-status-badge eb-tentative-badge">Tentative</span>{/if}
106
+ {#if isFull}<span class="eb-status-badge eb-full-badge">Full</span>{/if}
107
+ {#if isLimited}<span class="eb-status-badge eb-limited-badge">Limited</span>{/if}
95
108
  {#if active}<span class="eb-live-badge">{L.now}</span>{/if}
96
109
  </div>
97
110
  {:else}
@@ -102,6 +115,9 @@
102
115
  {#if event.subtitle}
103
116
  <span class="eb-subtitle">{event.subtitle}</span>
104
117
  {/if}
118
+ {#if event.location}
119
+ <span class="eb-location">📍 {event.location}</span>
120
+ {/if}
105
121
  {#if showDuration}
106
122
  <span class="eb-dur">{fmtDuration(event.start, event.end)}</span>
107
123
  {/if}
@@ -112,6 +128,9 @@
112
128
  {/each}
113
129
  </span>
114
130
  {/if}
131
+ {#if isCancelled}<span class="eb-status-badge eb-cancelled-badge">Cancelled</span>{/if}
132
+ {#if isFull}<span class="eb-status-badge eb-full-badge">Full</span>{/if}
133
+ {#if isLimited}<span class="eb-status-badge eb-limited-badge">Limited</span>{/if}
115
134
  {#if active}<span class="eb-live-badge">{L.now}</span>{/if}
116
135
  {/if}
117
136
  {/snippet}
@@ -121,6 +140,10 @@
121
140
  class="eb eb-{variant}"
122
141
  class:eb-active={active}
123
142
  class:eb-past={past}
143
+ class:eb-cancelled={isCancelled}
144
+ class:eb-tentative={isTentative}
145
+ class:eb-full={isFull}
146
+ class:eb-limited={isLimited}
124
147
  class:eb-editable={editable}
125
148
  style="--eb-color: {accentColor}"
126
149
  role="button"
@@ -137,6 +160,10 @@
137
160
  class="eb eb-{variant}"
138
161
  class:eb-active={active}
139
162
  class:eb-past={past}
163
+ class:eb-cancelled={isCancelled}
164
+ class:eb-tentative={isTentative}
165
+ class:eb-full={isFull}
166
+ class:eb-limited={isLimited}
140
167
  class:eb-editable={editable}
141
168
  style="--eb-color: {accentColor}"
142
169
  role="article"
@@ -167,6 +194,31 @@
167
194
  .eb-past {
168
195
  opacity: 0.5;
169
196
  }
197
+ .eb-cancelled {
198
+ opacity: 0.45;
199
+ }
200
+ .eb-cancelled .eb-title {
201
+ text-decoration: line-through;
202
+ }
203
+ .eb-tentative {
204
+ opacity: 0.7;
205
+ }
206
+ .eb-tentative .eb-stripe {
207
+ background: repeating-linear-gradient(
208
+ 45deg,
209
+ var(--_color),
210
+ var(--_color) 2px,
211
+ transparent 2px,
212
+ transparent 4px
213
+ );
214
+ }
215
+ .eb-full {
216
+ opacity: 0.55;
217
+ }
218
+ .eb-limited .eb-stripe {
219
+ border-left: 2px dashed var(--_color);
220
+ background: none;
221
+ }
170
222
 
171
223
  /* ── Chip ── */
172
224
  .eb-chip {
@@ -291,6 +343,39 @@
291
343
  background: color-mix(in srgb, var(--_color) 18%, transparent);
292
344
  color: var(--_color);
293
345
  }
346
+ .eb-location {
347
+ font: 400 9px / 1.2 var(--dt-sans, 'Outfit', system-ui, sans-serif);
348
+ color: var(--dt-text-3, rgba(100, 116, 139, 0.55));
349
+ overflow: hidden;
350
+ text-overflow: ellipsis;
351
+ white-space: nowrap;
352
+ }
353
+ .eb-status-badge {
354
+ display: inline-flex;
355
+ align-items: center;
356
+ font: 700 8px / 1 var(--dt-sans, 'Outfit', system-ui, sans-serif);
357
+ letter-spacing: 0.06em;
358
+ text-transform: uppercase;
359
+ padding: 2px 6px;
360
+ border-radius: 3px;
361
+ width: fit-content;
362
+ }
363
+ .eb-cancelled-badge {
364
+ color: var(--dt-text-2, rgba(148, 163, 184, 0.65));
365
+ background: color-mix(in srgb, var(--dt-text-2, rgba(148, 163, 184, 0.3)) 15%, transparent);
366
+ }
367
+ .eb-tentative-badge {
368
+ color: var(--_color);
369
+ background: color-mix(in srgb, var(--_color) 12%, transparent);
370
+ }
371
+ .eb-full-badge {
372
+ color: #ef4444;
373
+ background: color-mix(in srgb, #ef4444 15%, transparent);
374
+ }
375
+ .eb-limited-badge {
376
+ color: var(--_color);
377
+ background: color-mix(in srgb, var(--_color) 12%, transparent);
378
+ }
294
379
  .eb-live-badge {
295
380
  display: inline-flex;
296
381
  align-items: center;
@@ -12,14 +12,16 @@
12
12
  * Future day ("The Plan"):
13
13
  * Clean numbered schedule list.
14
14
  */
15
- import { getContext } from 'svelte';
16
15
  import { createClock } from '../../core/clock.svelte.js';
17
16
  import type { TimelineEvent } from '../../core/types.js';
18
17
  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';
18
+ import { getLabels } from '../../core/locale.js';
19
+ import { useCalendarContext } from '../shared/context.svelte.js';
20
+ import { fmtTime, duration, timeUntilMs, progress, groupIntoSlots } from '../shared/format.js';
21
+ import type { TimeSlot } from '../shared/format.js';
21
22
 
22
23
  const L = $derived(getLabels());
24
+ const ctx = useCalendarContext();
23
25
 
24
26
  interface Props {
25
27
  locale?: string;
@@ -43,17 +45,15 @@
43
45
  }: Props = $props();
44
46
 
45
47
  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);
48
+ const viewState = $derived(ctx.viewState);
49
+ const showNav = $derived(ctx.showNav);
50
+ const equalDays = $derived(ctx.equalDays);
51
+ const showDates = $derived(ctx.showDates);
52
+ const isMobile = $derived(ctx.isMobile);
53
+ const autoHeight = $derived(ctx.autoHeight);
54
+ const compact = $derived(ctx.compact);
55
+ const oneventhover = $derived(ctx.oneventhover);
56
+ const disabledSet = $derived(ctx.disabledSet);
57
57
 
58
58
  // ── Swipe navigation (mobile) ──────────────────────
59
59
  let swipeStartX = 0;
@@ -76,65 +76,10 @@
76
76
  }
77
77
  }
78
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
- }
79
+ // ── Format helpers (delegated to shared/format.ts) ──
80
+ const fmt = (d: Date) => fmtTime(d, locale);
81
+ const eta = (ms: number) => timeUntilMs(ms, clock.tick);
82
+ const prog = (ev: TimelineEvent) => progress(ev, clock.tick);
138
83
 
139
84
  // ── Event handlers ──────────────────────────────────
140
85
  function handleClick(ev: TimelineEvent): void {
@@ -244,7 +189,44 @@
244
189
  </div>
245
190
  {/if}
246
191
 
247
- {#if isToday}
192
+ {#if compact}
193
+ <!-- ─── Compact: minimal text rows ─── -->
194
+ <div class="ag-compact-list">
195
+ {#if timedDayEvents.length === 0 && allDayBanner.length === 0}
196
+ <div class="ag-q-empty">{L.nothingScheduledYet}</div>
197
+ {:else}
198
+ {#each timedDayEvents as ev (ev.id)}
199
+ <div
200
+ class="ag-compact-row"
201
+ class:ag-compact-row--selected={selectedEventId === ev.id}
202
+ class:ag-compact-row--cancelled={ev.status === 'cancelled'}
203
+ class:ag-compact-row--tentative={ev.status === 'tentative'}
204
+ style:--ev-color={ev.color || 'var(--dt-accent)'}
205
+ role="button"
206
+ tabindex="0"
207
+ aria-label="{ev.title}, {fmt(ev.start)}, {duration(ev)}"
208
+ onclick={() => handleClick(ev)}
209
+ onpointerenter={() => oneventhover?.(ev)}
210
+ onkeydown={(e) => handleKeydown(e, ev)}
211
+ >
212
+ <span class="ag-compact-row-dot"></span>
213
+ <span class="ag-compact-row-time">{fmt(ev.start)}</span>
214
+ <span class="ag-compact-row-title">{ev.title}</span>
215
+ {#if ev.subtitle}
216
+ <span class="ag-compact-row-sub">{ev.subtitle}</span>
217
+ {/if}
218
+ {#if ev.tags?.length}
219
+ {#each ev.tags as tag}
220
+ <span class="ag-compact-row-tag">{tag}</span>
221
+ {/each}
222
+ {/if}
223
+ <span class="ag-compact-row-dur">{duration(ev)}</span>
224
+ </div>
225
+ {/each}
226
+ {/if}
227
+ </div>
228
+
229
+ {:else if isToday}
248
230
  <!-- ─── Today: "The Queue" — upcoming is the hero ─── -->
249
231
  <div class="ag-q">
250
232
  <!-- NOW column: past events stacked above NOW strip -->
@@ -258,7 +240,7 @@
258
240
  class:ag-q-done-item--selected={selectedEventId === ev.id}
259
241
  role="button"
260
242
  tabindex="0"
261
- aria-label="{ev.title}, {L.completed}, {fmtTime(ev.start)}"
243
+ aria-label="{ev.title}, {L.completed}, {fmt(ev.start)}"
262
244
  onclick={() => handleClick(ev)}
263
245
  onkeydown={(e) => handleKeydown(e, ev)}
264
246
  >
@@ -278,14 +260,14 @@
278
260
  style:--ev-color={ev.color || 'var(--dt-accent)'}
279
261
  role="button"
280
262
  tabindex="0"
281
- aria-label="{ev.title}, {L.happeningNow}, {L.percentComplete(Math.round(progress(ev) * 100))}"
263
+ aria-label="{ev.title}, {L.happeningNow}, {L.percentComplete(Math.round(prog(ev) * 100))}"
282
264
  onclick={() => handleClick(ev)} onpointerenter={() => oneventhover?.(ev)} onkeydown={(e) => handleKeydown(e, ev)}
283
265
  >
284
266
  <div class="ag-q-now-dot"></div>
285
267
  <div class="ag-q-now-title">{ev.title}</div>
286
- <div class="ag-q-now-time">{L.until} {fmtTime(ev.end)}</div>
268
+ <div class="ag-q-now-time">{L.until} {fmt(ev.end)}</div>
287
269
  <div class="ag-q-now-track">
288
- <div class="ag-q-now-fill" style:width="{progress(ev) * 100}%"></div>
270
+ <div class="ag-q-now-fill" style:width="{prog(ev) * 100}%"></div>
289
271
  </div>
290
272
  </div>
291
273
  {/each}
@@ -312,7 +294,7 @@
312
294
  style:--ev-color={ev.color || 'var(--dt-accent)'}
313
295
  role="button"
314
296
  tabindex="0"
315
- aria-label="{ev.title}, {fmtTime(ev.start)}, {duration(ev)}"
297
+ aria-label="{ev.title}, {fmt(ev.start)}, {duration(ev)}"
316
298
  onclick={() => handleClick(ev)}
317
299
  onpointerenter={() => oneventhover?.(ev)}
318
300
  onkeydown={(e) => handleKeydown(e, ev)}
@@ -320,13 +302,13 @@
320
302
  <div class="ag-card-body">
321
303
  <div class="ag-card-top">
322
304
  <span class="ag-card-title">{ev.title}</span>
323
- <span class="ag-card-eta">{timeUntilMs(ev.start.getTime())}</span>
305
+ <span class="ag-card-eta">{eta(ev.start.getTime())}</span>
324
306
  </div>
325
307
  {#if ev.subtitle}
326
308
  <span class="ag-card-sub">{ev.subtitle}</span>
327
309
  {/if}
328
310
  <div class="ag-card-meta">
329
- {fmtTime(ev.start)} – {fmtTime(ev.end)}
311
+ {fmt(ev.start)} – {fmt(ev.end)}
330
312
  <span class="ag-card-dur">{duration(ev)}</span>
331
313
  </div>
332
314
  {#if ev.tags?.length}
@@ -356,11 +338,11 @@
356
338
  style:--ev-color={ev.color || 'var(--dt-accent)'}
357
339
  role="button"
358
340
  tabindex="0"
359
- aria-label="{ev.title}, {fmtTime(ev.start)} to {fmtTime(ev.end)}"
341
+ aria-label="{ev.title}, {fmt(ev.start)} to {fmt(ev.end)}"
360
342
  onclick={() => handleClick(ev)} onpointerenter={() => oneventhover?.(ev)} onkeydown={(e) => handleKeydown(e, ev)}
361
343
  >
362
344
  <span class="ag-log-check">✓</span>
363
- <span class="ag-log-time">{fmtTime(ev.start)}</span>
345
+ <span class="ag-log-time">{fmt(ev.start)}</span>
364
346
  <span class="ag-log-dot" style:background={ev.color || 'var(--dt-accent)'}></span>
365
347
  <span class="ag-log-title">{ev.title}</span>
366
348
  <span class="ag-log-dur">{duration(ev)}</span>
@@ -380,10 +362,14 @@
380
362
  class="ag-card ag-card--plan"
381
363
  class:ag-card--first={i === 0}
382
364
  class:ag-card--selected={selectedEventId === ev.id}
365
+ class:ag-card--cancelled={ev.status === 'cancelled'}
366
+ class:ag-card--tentative={ev.status === 'tentative'}
367
+ class:ag-card--full={ev.status === 'full'}
368
+ class:ag-card--limited={ev.status === 'limited'}
383
369
  style:--ev-color={ev.color || 'var(--dt-accent)'}
384
370
  role="button"
385
371
  tabindex="0"
386
- aria-label="{ev.title}, {fmtTime(ev.start)} to {fmtTime(ev.end)}, {duration(ev)}"
372
+ 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)}"
387
373
  onclick={() => handleClick(ev)} onpointerenter={() => oneventhover?.(ev)} onkeydown={(e) => handleKeydown(e, ev)}
388
374
  >
389
375
  <div class="ag-card-body">
@@ -394,8 +380,11 @@
394
380
  {#if ev.subtitle}
395
381
  <span class="ag-card-sub">{ev.subtitle}</span>
396
382
  {/if}
383
+ {#if ev.location}
384
+ <span class="ag-card-loc">{ev.location}</span>
385
+ {/if}
397
386
  <div class="ag-card-meta">
398
- {fmtTime(ev.start)} – {fmtTime(ev.end)}
387
+ {fmt(ev.start)} – {fmt(ev.end)}
399
388
  <span class="ag-card-dur">{duration(ev)}</span>
400
389
  </div>
401
390
  {#if ev.tags?.length}
@@ -528,6 +517,9 @@
528
517
  display: flex;
529
518
  flex-direction: column;
530
519
  height: 100%;
520
+ width: 100%;
521
+ min-width: 0;
522
+ box-sizing: border-box;
531
523
  color: var(--dt-text, rgba(255, 255, 255, 0.92));
532
524
  font-family: var(--dt-sans, system-ui, sans-serif);
533
525
  }
@@ -543,9 +535,11 @@
543
535
  .ag-body {
544
536
  flex: 1;
545
537
  min-height: 0;
538
+ min-width: 0;
546
539
  display: flex;
547
540
  flex-direction: column;
548
541
  overflow-y: auto;
542
+ overflow-x: hidden;
549
543
  padding-top: 44px;
550
544
  scrollbar-width: thin;
551
545
  scrollbar-color: var(--dt-border) transparent;
@@ -640,6 +634,23 @@
640
634
  border-color: var(--ev-color);
641
635
  background: color-mix(in srgb, var(--ev-color) 22%, var(--dt-surface, #191919));
642
636
  }
637
+ .ag-card--cancelled {
638
+ opacity: 0.5;
639
+ }
640
+ .ag-card--cancelled .ag-card-title {
641
+ text-decoration: line-through;
642
+ }
643
+ .ag-card--tentative {
644
+ opacity: 0.65;
645
+ border-style: dashed;
646
+ }
647
+ .ag-card--full {
648
+ opacity: 0.55;
649
+ }
650
+ .ag-card--limited {
651
+ opacity: 0.65;
652
+ border-style: dashed;
653
+ }
643
654
  .ag-card-body {
644
655
  padding: 10px 12px;
645
656
  display: flex;
@@ -653,15 +664,15 @@
653
664
  justify-content: space-between;
654
665
  align-items: flex-start;
655
666
  gap: 8px;
667
+ min-width: 0;
656
668
  }
657
669
  .ag-card-title {
658
670
  font-size: 13px;
659
671
  font-weight: 600;
660
- line-height: 1.2;
661
- white-space: nowrap;
662
- overflow: hidden;
663
- text-overflow: ellipsis;
672
+ line-height: 1.3;
673
+ word-break: break-word;
664
674
  flex: 1;
675
+ min-width: 0;
665
676
  }
666
677
  .ag-card-meta {
667
678
  font-size: 11px;
@@ -678,6 +689,11 @@
678
689
  color: var(--dt-text-2, rgba(255, 255, 255, 0.45));
679
690
  line-height: 1;
680
691
  }
692
+ .ag-card-loc {
693
+ font-size: 10px;
694
+ color: var(--dt-text-3, rgba(255, 255, 255, 0.35));
695
+ line-height: 1;
696
+ }
681
697
  .ag-card-tags {
682
698
  display: flex;
683
699
  gap: 4px;
@@ -1057,6 +1073,84 @@
1057
1073
  flex-shrink: 0;
1058
1074
  }
1059
1075
 
1076
+ /* ═══ Compact Day ═══ */
1077
+ .ag-compact-list {
1078
+ flex: 1;
1079
+ padding: 8px 20px 12px;
1080
+ overflow-y: auto;
1081
+ scrollbar-width: none;
1082
+ }
1083
+ .ag-compact-list::-webkit-scrollbar { display: none; }
1084
+ .ag-compact-row {
1085
+ display: flex;
1086
+ align-items: center;
1087
+ gap: 8px;
1088
+ padding: 4px 0;
1089
+ cursor: pointer;
1090
+ }
1091
+ .ag-compact-row--selected {
1092
+ background: color-mix(in srgb, var(--ev-color) 10%, transparent);
1093
+ border-radius: 4px;
1094
+ padding-left: 6px;
1095
+ padding-right: 6px;
1096
+ }
1097
+ .ag-compact-row:hover .ag-compact-row-title { color: var(--dt-text); }
1098
+ .ag-compact-row:focus-visible {
1099
+ outline: 2px solid var(--dt-accent, #ff6b4a);
1100
+ outline-offset: 2px;
1101
+ }
1102
+ .ag-compact-row-dot {
1103
+ width: 5px;
1104
+ height: 5px;
1105
+ border-radius: 50%;
1106
+ background: var(--ev-color, var(--dt-accent));
1107
+ flex-shrink: 0;
1108
+ }
1109
+ .ag-compact-row-time {
1110
+ font-size: 11px;
1111
+ font-family: var(--dt-mono, monospace);
1112
+ color: var(--dt-text-2, rgba(255, 255, 255, 0.5));
1113
+ min-width: 64px;
1114
+ flex-shrink: 0;
1115
+ }
1116
+ .ag-compact-row-title {
1117
+ font-size: 12px;
1118
+ font-weight: 500;
1119
+ color: var(--dt-text-2, rgba(255, 255, 255, 0.5));
1120
+ flex: 1;
1121
+ white-space: nowrap;
1122
+ overflow: hidden;
1123
+ text-overflow: ellipsis;
1124
+ transition: color 150ms;
1125
+ }
1126
+ .ag-compact-row-dur {
1127
+ font-size: 10px;
1128
+ font-family: var(--dt-mono, monospace);
1129
+ color: var(--dt-text-3, rgba(255, 255, 255, 0.3));
1130
+ flex-shrink: 0;
1131
+ }
1132
+ .ag-compact-row-sub {
1133
+ font-size: 10px;
1134
+ color: var(--dt-text-3, rgba(255, 255, 255, 0.35));
1135
+ flex-shrink: 0;
1136
+ }
1137
+ .ag-compact-row-tag {
1138
+ font: 500 8px / 1 var(--dt-sans, system-ui, sans-serif);
1139
+ color: var(--ev-color, var(--dt-accent));
1140
+ background: color-mix(in srgb, var(--ev-color, var(--dt-accent)) 12%, transparent);
1141
+ padding: 1px 4px;
1142
+ border-radius: 3px;
1143
+ white-space: nowrap;
1144
+ flex-shrink: 0;
1145
+ }
1146
+ .ag-compact-row--cancelled { opacity: 0.5; }
1147
+ .ag-compact-row--cancelled .ag-compact-row-title { text-decoration: line-through; }
1148
+ .ag-compact-row--tentative { opacity: 0.65; }
1149
+ /* Mobile: larger touch targets for compact rows */
1150
+ .ag--mobile .ag-compact-row { padding: 8px 0; }
1151
+ .ag--mobile .ag-compact-row-title { font-size: 14px; }
1152
+ .ag--mobile .ag-compact-row-time { font-size: 12px; }
1153
+
1060
1154
  /* ═══ Future Day: "The Plan" ═══ */
1061
1155
  .ag-plan {
1062
1156
  flex: 1;