@nomideusz/svelte-calendar 0.10.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.
- package/dist/calendar/Calendar.svelte +39 -5
- package/dist/views/agenda/AgendaDay.svelte +68 -16
- package/dist/views/agenda/AgendaWeek.svelte +166 -67
- package/dist/views/mobile/MobileDay.svelte +11 -3
- package/dist/views/mobile/MobileWeek.svelte +6 -0
- package/dist/views/mobile/swipe.d.ts +1 -0
- package/dist/views/mobile/swipe.js +9 -0
- package/dist/views/month/MonthGrid.svelte +35 -2
- package/package.json +1 -1
- package/widget/widget.js +694 -649
|
@@ -97,12 +97,16 @@ function handleEventClick(ev) {
|
|
|
97
97
|
oneventclick?.(ev);
|
|
98
98
|
}
|
|
99
99
|
let containerWidth = $state(
|
|
100
|
-
typeof window !== "undefined" && window.matchMedia?.(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`).matches ?
|
|
100
|
+
typeof window !== "undefined" && window.matchMedia?.(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`).matches ? window.innerWidth : 0
|
|
101
101
|
);
|
|
102
102
|
const isMobileContainer = $derived(containerWidth > 0 && containerWidth < MOBILE_BREAKPOINT);
|
|
103
103
|
const useMobile = $derived(
|
|
104
104
|
mobileProp === "auto" ? isMobileContainer : Boolean(mobileProp)
|
|
105
105
|
);
|
|
106
|
+
const HEADER_STACK_BREAKPOINT = 520;
|
|
107
|
+
const stackHeader = $derived(
|
|
108
|
+
useMobile && containerWidth > 0 && containerWidth < HEADER_STACK_BREAKPOINT
|
|
109
|
+
);
|
|
106
110
|
let calEl = $state();
|
|
107
111
|
let probedTheme = $state("");
|
|
108
112
|
const needsProbe = $derived(theme === auto && autoTheme !== false);
|
|
@@ -480,7 +484,8 @@ const navCtx = $derived({
|
|
|
480
484
|
|
|
481
485
|
<!-- ─── Mobile header (flow layout, no absolute) ─── -->
|
|
482
486
|
{:else if useMobile && (showNavigation || (showModePills && modes.length > 1) || dateLabel)}
|
|
483
|
-
|
|
487
|
+
{@const titleBelow = stackHeader && !!dateLabel}
|
|
488
|
+
<div class="cal-m-hd" class:cal-m-hd--stack={stackHeader} class:cal-m-hd--titled={titleBelow}>
|
|
484
489
|
<div class="cal-m-left">
|
|
485
490
|
{#if showModePills && modes.length > 1}
|
|
486
491
|
<div class="cal-m-pills" role="radiogroup" aria-label={L.viewMode}>
|
|
@@ -500,7 +505,9 @@ const navCtx = $derived({
|
|
|
500
505
|
{/if}
|
|
501
506
|
</div>
|
|
502
507
|
|
|
503
|
-
|
|
508
|
+
{#if !titleBelow}
|
|
509
|
+
<span class="cal-m-title" role="status" aria-live="polite" aria-atomic="true">{dateLabel}</span>
|
|
510
|
+
{/if}
|
|
504
511
|
|
|
505
512
|
<div class="cal-m-right">
|
|
506
513
|
{#if navigationSnippet}
|
|
@@ -525,6 +532,11 @@ const navCtx = $derived({
|
|
|
525
532
|
{/if}
|
|
526
533
|
</div>
|
|
527
534
|
</div>
|
|
535
|
+
{#if titleBelow}
|
|
536
|
+
<div class="cal-m-titlebar">
|
|
537
|
+
<span class="cal-m-title" role="status" aria-live="polite" aria-atomic="true">{dateLabel}</span>
|
|
538
|
+
</div>
|
|
539
|
+
{/if}
|
|
528
540
|
|
|
529
541
|
<!-- ─── Desktop header ─── -->
|
|
530
542
|
{:else if showNavigation || (showModePills && modes.length > 1) || dateLabel}
|
|
@@ -740,7 +752,9 @@ const navCtx = $derived({
|
|
|
740
752
|
transition: background 100ms, color 100ms;
|
|
741
753
|
}
|
|
742
754
|
|
|
743
|
-
|
|
755
|
+
/* :not(--active) — the hover rule otherwise outranks the active color,
|
|
756
|
+
and iOS keeps :hover stuck after a tap (dark text on the accent). */
|
|
757
|
+
.cal-pill:hover:not(.cal-pill--active) {
|
|
744
758
|
color: var(--dt-text, rgba(0, 0, 0, 0.87));
|
|
745
759
|
}
|
|
746
760
|
|
|
@@ -806,6 +820,26 @@ const navCtx = $derived({
|
|
|
806
820
|
min-height: 44px;
|
|
807
821
|
}
|
|
808
822
|
|
|
823
|
+
/* Narrow containers: the date label moves to its own row (.cal-m-titlebar),
|
|
824
|
+
so the controls row spreads pills and nav to the edges. */
|
|
825
|
+
.cal-m-hd--stack {
|
|
826
|
+
justify-content: space-between;
|
|
827
|
+
}
|
|
828
|
+
.cal-m-hd--titled {
|
|
829
|
+
border-bottom: none;
|
|
830
|
+
padding-bottom: 2px;
|
|
831
|
+
}
|
|
832
|
+
.cal-m-titlebar {
|
|
833
|
+
display: flex;
|
|
834
|
+
justify-content: center;
|
|
835
|
+
padding: 0 8px 8px;
|
|
836
|
+
border-bottom: 1px solid var(--dt-border, rgba(0, 0, 0, 0.08));
|
|
837
|
+
flex-shrink: 0;
|
|
838
|
+
}
|
|
839
|
+
.cal-m-titlebar .cal-m-title {
|
|
840
|
+
flex: 0 1 auto;
|
|
841
|
+
}
|
|
842
|
+
|
|
809
843
|
.cal-m-left,
|
|
810
844
|
.cal-m-right {
|
|
811
845
|
display: flex;
|
|
@@ -867,7 +901,7 @@ const navCtx = $derived({
|
|
|
867
901
|
transition: background 100ms, color 100ms;
|
|
868
902
|
-webkit-tap-highlight-color: transparent;
|
|
869
903
|
}
|
|
870
|
-
.cal-m-pill:hover {
|
|
904
|
+
.cal-m-pill:hover:not(.cal-m-pill--active) {
|
|
871
905
|
color: var(--dt-text, rgba(0, 0, 0, 0.87));
|
|
872
906
|
}
|
|
873
907
|
.cal-m-pill--active {
|
|
@@ -162,15 +162,17 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
|
|
|
162
162
|
<EventContent event={ev}>
|
|
163
163
|
<span class="ag-compact-row-dot"></span>
|
|
164
164
|
<span class="ag-compact-row-time">{fmt(ev.start)}</span>
|
|
165
|
-
<
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
{#
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
165
|
+
<div class="ag-compact-row-main">
|
|
166
|
+
<span class="ag-compact-row-title">{ev.title}</span>
|
|
167
|
+
{#if ev.subtitle}
|
|
168
|
+
<span class="ag-compact-row-sub">{ev.subtitle}</span>
|
|
169
|
+
{/if}
|
|
170
|
+
{#if ev.tags?.length}
|
|
171
|
+
{#each ev.tags as tag}
|
|
172
|
+
<span class="ag-compact-row-tag">{tag}</span>
|
|
173
|
+
{/each}
|
|
174
|
+
{/if}
|
|
175
|
+
</div>
|
|
174
176
|
<span class="ag-compact-row-dur">{duration(ev)}</span>
|
|
175
177
|
</EventContent>
|
|
176
178
|
</button>
|
|
@@ -256,10 +258,12 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
|
|
|
256
258
|
<EventContent event={ev}>
|
|
257
259
|
<span class="ag-compact-row-dot"></span>
|
|
258
260
|
<span class="ag-compact-row-time">{fmt(ev.start)}</span>
|
|
259
|
-
<
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
261
|
+
<div class="ag-compact-row-main">
|
|
262
|
+
<span class="ag-compact-row-title">{ev.title}</span>
|
|
263
|
+
{#if ev.subtitle}
|
|
264
|
+
<span class="ag-compact-row-sub">{ev.subtitle}</span>
|
|
265
|
+
{/if}
|
|
266
|
+
</div>
|
|
263
267
|
</EventContent>
|
|
264
268
|
</button>
|
|
265
269
|
{:else}
|
|
@@ -715,7 +719,11 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
|
|
|
715
719
|
font-size: 16px;
|
|
716
720
|
font-weight: 700;
|
|
717
721
|
}
|
|
718
|
-
|
|
722
|
+
/* Everything under the title aligns past the order number — the
|
|
723
|
+
subtitle, location, time and tags share one left edge. */
|
|
724
|
+
.ag-card--plan .ag-card-sub,
|
|
725
|
+
.ag-card--plan .ag-card-loc,
|
|
726
|
+
.ag-card--plan .ag-card-meta {
|
|
719
727
|
padding-left: 22px;
|
|
720
728
|
}
|
|
721
729
|
.ag-card--plan .ag-card-tags {
|
|
@@ -1065,6 +1073,9 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
|
|
|
1065
1073
|
border-radius: 6px;
|
|
1066
1074
|
padding-left: 8px;
|
|
1067
1075
|
padding-right: 8px;
|
|
1076
|
+
margin-left: -8px;
|
|
1077
|
+
margin-right: -8px;
|
|
1078
|
+
width: calc(100% + 16px);
|
|
1068
1079
|
}
|
|
1069
1080
|
.ag-log-check {
|
|
1070
1081
|
font-size: 10px;
|
|
@@ -1126,8 +1137,13 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
|
|
|
1126
1137
|
.ag-compact-row--selected {
|
|
1127
1138
|
background: color-mix(in srgb, var(--ev-color) 10%, transparent);
|
|
1128
1139
|
border-radius: 4px;
|
|
1140
|
+
/* Highlight gutter comes from negative margins so the row's content
|
|
1141
|
+
stays aligned with its unselected siblings (no tap-shift). */
|
|
1129
1142
|
padding-left: 6px;
|
|
1130
1143
|
padding-right: 6px;
|
|
1144
|
+
margin-left: -6px;
|
|
1145
|
+
margin-right: -6px;
|
|
1146
|
+
width: calc(100% + 12px);
|
|
1131
1147
|
}
|
|
1132
1148
|
.ag-compact-row:hover .ag-compact-row-title,
|
|
1133
1149
|
.ag-compact-row:active .ag-compact-row-title { color: var(--dt-text); }
|
|
@@ -1156,11 +1172,43 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
|
|
|
1156
1172
|
flex-shrink: 0;
|
|
1157
1173
|
line-height: 1.4;
|
|
1158
1174
|
}
|
|
1175
|
+
/* Title + subtitle + tags cluster. One line while it fits; on mobile the
|
|
1176
|
+
metadata wraps to a second line under the title instead of crushing it. */
|
|
1177
|
+
.ag-compact-row-main {
|
|
1178
|
+
display: flex;
|
|
1179
|
+
align-items: baseline;
|
|
1180
|
+
gap: 8px;
|
|
1181
|
+
flex: 1;
|
|
1182
|
+
min-width: 0;
|
|
1183
|
+
}
|
|
1184
|
+
.ag--mobile .ag-compact-row-main {
|
|
1185
|
+
flex-wrap: wrap;
|
|
1186
|
+
row-gap: 2px;
|
|
1187
|
+
}
|
|
1188
|
+
/* Mobile: size the title by its content when deciding line breaks — a long
|
|
1189
|
+
title claims the first line whole (ellipsizing only against the full row)
|
|
1190
|
+
and pushes subtitle/tags down instead of truncating at 35%. */
|
|
1191
|
+
.ag--mobile .ag-compact-row-title {
|
|
1192
|
+
flex-basis: auto;
|
|
1193
|
+
}
|
|
1194
|
+
/* On its own wrapped line the subtitle gets the full width */
|
|
1195
|
+
.ag--mobile .ag-compact-row-sub {
|
|
1196
|
+
max-width: 100%;
|
|
1197
|
+
}
|
|
1198
|
+
/* Wrapped rows are two lines tall — center-aligning the dot floats it
|
|
1199
|
+
between lines; pin it optically to the first (title) line instead. */
|
|
1200
|
+
.ag--mobile .ag-compact-row-dot {
|
|
1201
|
+
align-self: flex-start;
|
|
1202
|
+
margin-top: 8px;
|
|
1203
|
+
}
|
|
1159
1204
|
.ag-compact-row-title {
|
|
1160
1205
|
font-size: 12px;
|
|
1161
1206
|
font-weight: 500;
|
|
1162
1207
|
color: color-mix(in srgb, var(--dt-text, rgba(0, 0, 0, 0.87)) 82%, transparent);
|
|
1163
1208
|
flex: 1;
|
|
1209
|
+
/* The title is the row's identity — never let subtitle/tags/duration
|
|
1210
|
+
squeeze it out on narrow screens (flex: 1 alone resolves to 0px). */
|
|
1211
|
+
min-width: 35%;
|
|
1164
1212
|
white-space: nowrap;
|
|
1165
1213
|
overflow: hidden;
|
|
1166
1214
|
text-overflow: ellipsis;
|
|
@@ -1178,7 +1226,8 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
|
|
|
1178
1226
|
.ag-compact-row-sub {
|
|
1179
1227
|
font-size: 10px;
|
|
1180
1228
|
color: var(--dt-text-3, rgba(0, 0, 0, 0.38));
|
|
1181
|
-
flex-shrink:
|
|
1229
|
+
flex-shrink: 3;
|
|
1230
|
+
min-width: 0;
|
|
1182
1231
|
max-width: 45%;
|
|
1183
1232
|
white-space: nowrap;
|
|
1184
1233
|
overflow: hidden;
|
|
@@ -1192,7 +1241,10 @@ const hiddenDoneCount = $derived(showAllDone ? 0 : Math.max(0, dayCat.past.lengt
|
|
|
1192
1241
|
padding: 1px 4px;
|
|
1193
1242
|
border-radius: 3px;
|
|
1194
1243
|
white-space: nowrap;
|
|
1195
|
-
flex-shrink:
|
|
1244
|
+
flex-shrink: 1;
|
|
1245
|
+
min-width: 2.5em;
|
|
1246
|
+
overflow: hidden;
|
|
1247
|
+
text-overflow: ellipsis;
|
|
1196
1248
|
}
|
|
1197
1249
|
.ag-compact-row--cancelled { opacity: 0.5; }
|
|
1198
1250
|
.ag-compact-row--cancelled .ag-compact-row-title { text-decoration: line-through; }
|
|
@@ -54,6 +54,10 @@ let expandedDays = $state([]);
|
|
|
54
54
|
function toggleDayExpand(ms) {
|
|
55
55
|
expandedDays = expandedDays.includes(ms) ? expandedDays.filter((m) => m !== ms) : [...expandedDays, ms];
|
|
56
56
|
}
|
|
57
|
+
let expandedPast = $state([]);
|
|
58
|
+
function togglePastExpand(ms) {
|
|
59
|
+
expandedPast = expandedPast.includes(ms) ? expandedPast.filter((m) => m !== ms) : [...expandedPast, ms];
|
|
60
|
+
}
|
|
57
61
|
const fmt = (d) => fmtTime(d, locale);
|
|
58
62
|
const eta = (ms) => timeUntilMs(ms, clock.tick, L);
|
|
59
63
|
const prog = (ev) => progress(ev, clock.tick);
|
|
@@ -178,6 +182,59 @@ const weekDays = $derived.by(() => {
|
|
|
178
182
|
</button>
|
|
179
183
|
{/snippet}
|
|
180
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>
|
|
236
|
+
{/snippet}
|
|
237
|
+
|
|
181
238
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
182
239
|
<div
|
|
183
240
|
class="ag ag--week"
|
|
@@ -207,7 +264,14 @@ const weekDays = $derived.by(() => {
|
|
|
207
264
|
{/if}
|
|
208
265
|
</div>
|
|
209
266
|
{#if day.timedEvents.length > 0}
|
|
210
|
-
|
|
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}
|
|
211
275
|
{:else if day.events.length === 0}
|
|
212
276
|
<div class="ag-wday-past-line ag-wday-past-line--summary">{L.noEvents}</div>
|
|
213
277
|
{/if}
|
|
@@ -269,34 +333,7 @@ const weekDays = $derived.by(() => {
|
|
|
269
333
|
<!-- Compact: minimal dot + time + title rows for all days -->
|
|
270
334
|
<div class="ag-wday-compact">
|
|
271
335
|
{#each day.timedEvents as ev (ev.id)}
|
|
272
|
-
|
|
273
|
-
type="button"
|
|
274
|
-
class="ag-compact"
|
|
275
|
-
class:ag-compact--selected={selectedEventId === ev.id}
|
|
276
|
-
class:ag-compact--cancelled={ev.status === 'cancelled'}
|
|
277
|
-
class:ag-compact--tentative={ev.status === 'tentative'}
|
|
278
|
-
class:ag-compact--full={ev.status === 'full'}
|
|
279
|
-
class:ag-compact--limited={ev.status === 'limited'}
|
|
280
|
-
style:--ev-color={ev.color || 'var(--dt-accent)'}
|
|
281
|
-
aria-label="{ev.title}, {fmt(ev.start)}, {duration(ev)}"
|
|
282
|
-
onclick={() => handleClick(ev)}
|
|
283
|
-
onpointerenter={() => oneventhover?.(ev)}
|
|
284
|
-
>
|
|
285
|
-
<EventContent event={ev}>
|
|
286
|
-
<span class="ag-compact-dot"></span>
|
|
287
|
-
<span class="ag-compact-time">{fmt(ev.start)}</span>
|
|
288
|
-
<span class="ag-compact-title">{ev.title}</span>
|
|
289
|
-
{#if ev.subtitle}
|
|
290
|
-
<span class="ag-compact-sub">{ev.subtitle}</span>
|
|
291
|
-
{/if}
|
|
292
|
-
{#if ev.tags?.length}
|
|
293
|
-
{#each ev.tags as tag}
|
|
294
|
-
<span class="ag-compact-tag">{tag}</span>
|
|
295
|
-
{/each}
|
|
296
|
-
{/if}
|
|
297
|
-
<span class="ag-compact-dur">{duration(ev)}</span>
|
|
298
|
-
</EventContent>
|
|
299
|
-
</button>
|
|
336
|
+
{@render compactRow(ev, false, false)}
|
|
300
337
|
{/each}
|
|
301
338
|
</div>
|
|
302
339
|
{:else if equalDays}
|
|
@@ -337,7 +374,12 @@ const weekDays = $derived.by(() => {
|
|
|
337
374
|
</div>
|
|
338
375
|
{/each}
|
|
339
376
|
{#if day.pastEvents.length > 0}
|
|
340
|
-
|
|
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}
|
|
341
383
|
{/if}
|
|
342
384
|
</div>
|
|
343
385
|
{:else}
|
|
@@ -345,37 +387,7 @@ const weekDays = $derived.by(() => {
|
|
|
345
387
|
{@const dayExpanded = expandedDays.includes(day.ms)}
|
|
346
388
|
<div class="ag-wday-compact">
|
|
347
389
|
{#each (dayExpanded ? day.timedEvents : day.timedEvents.slice(0, 4)) as ev (ev.id)}
|
|
348
|
-
|
|
349
|
-
type="button"
|
|
350
|
-
class="ag-compact"
|
|
351
|
-
class:ag-compact--selected={selectedEventId === ev.id}
|
|
352
|
-
class:ag-compact--cancelled={ev.status === 'cancelled'}
|
|
353
|
-
class:ag-compact--tentative={ev.status === 'tentative'}
|
|
354
|
-
class:ag-compact--full={ev.status === 'full'}
|
|
355
|
-
class:ag-compact--limited={ev.status === 'limited'}
|
|
356
|
-
style:--ev-color={ev.color || 'var(--dt-accent)'}
|
|
357
|
-
aria-label="{ev.title}, {fmt(ev.start)}, {duration(ev)}"
|
|
358
|
-
onclick={() => handleClick(ev)}
|
|
359
|
-
onpointerenter={() => oneventhover?.(ev)}
|
|
360
|
-
>
|
|
361
|
-
<EventContent event={ev}>
|
|
362
|
-
<span class="ag-compact-dot"></span>
|
|
363
|
-
<span class="ag-compact-time">{fmt(ev.start)}</span>
|
|
364
|
-
<span class="ag-compact-title">{ev.title}</span>
|
|
365
|
-
{#if ev.location}
|
|
366
|
-
<span class="ag-compact-loc">{ev.location}</span>
|
|
367
|
-
{/if}
|
|
368
|
-
{#if ev.subtitle}
|
|
369
|
-
<span class="ag-compact-sub">{ev.subtitle}</span>
|
|
370
|
-
{/if}
|
|
371
|
-
{#if ev.tags?.length}
|
|
372
|
-
{#each ev.tags as tag}
|
|
373
|
-
<span class="ag-compact-tag">{tag}</span>
|
|
374
|
-
{/each}
|
|
375
|
-
{/if}
|
|
376
|
-
<span class="ag-compact-dur">{duration(ev)}</span>
|
|
377
|
-
</EventContent>
|
|
378
|
-
</button>
|
|
390
|
+
{@render compactRow(ev, true, false)}
|
|
379
391
|
{/each}
|
|
380
392
|
{#if day.timedEvents.length > 4}
|
|
381
393
|
<button
|
|
@@ -421,7 +433,8 @@ const weekDays = $derived.by(() => {
|
|
|
421
433
|
.ag-card,
|
|
422
434
|
.ag-allday-chip,
|
|
423
435
|
.ag-compact,
|
|
424
|
-
.ag-compact-more
|
|
436
|
+
.ag-compact-more,
|
|
437
|
+
.ag-past-toggle {
|
|
425
438
|
font: inherit;
|
|
426
439
|
color: inherit;
|
|
427
440
|
text-align: left;
|
|
@@ -439,10 +452,15 @@ const weekDays = $derived.by(() => {
|
|
|
439
452
|
overflow-y: auto;
|
|
440
453
|
overflow-x: hidden;
|
|
441
454
|
box-sizing: border-box;
|
|
442
|
-
padding-top:
|
|
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. */
|
|
443
458
|
scrollbar-width: thin;
|
|
444
459
|
scrollbar-color: var(--dt-border) transparent;
|
|
445
460
|
}
|
|
461
|
+
.ag-wday:first-child .ag-wday-head {
|
|
462
|
+
padding-top: 12px;
|
|
463
|
+
}
|
|
446
464
|
.ag--auto .ag-body {
|
|
447
465
|
overflow-y: visible;
|
|
448
466
|
}
|
|
@@ -678,6 +696,10 @@ const weekDays = $derived.by(() => {
|
|
|
678
696
|
top: 0;
|
|
679
697
|
background: var(--dt-bg, #fff);
|
|
680
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;
|
|
681
703
|
}
|
|
682
704
|
.ag-wday-head-left {
|
|
683
705
|
display: flex;
|
|
@@ -759,6 +781,42 @@ const weekDays = $derived.by(() => {
|
|
|
759
781
|
.ag-wday-past-line--summary {
|
|
760
782
|
padding: 0 20px 8px;
|
|
761
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 {
|
|
818
|
+
opacity: 0.5;
|
|
819
|
+
}
|
|
762
820
|
|
|
763
821
|
/* Compact day events */
|
|
764
822
|
.ag-wday-compact {
|
|
@@ -776,8 +834,13 @@ const weekDays = $derived.by(() => {
|
|
|
776
834
|
.ag-compact--selected {
|
|
777
835
|
background: color-mix(in srgb, var(--ev-color) 10%, transparent);
|
|
778
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). */
|
|
779
839
|
padding-left: 6px;
|
|
780
840
|
padding-right: 6px;
|
|
841
|
+
margin-left: -6px;
|
|
842
|
+
margin-right: -6px;
|
|
843
|
+
width: calc(100% + 12px);
|
|
781
844
|
}
|
|
782
845
|
.ag-compact:hover .ag-compact-title,
|
|
783
846
|
.ag-compact:active .ag-compact-title {
|
|
@@ -809,12 +872,45 @@ const weekDays = $derived.by(() => {
|
|
|
809
872
|
white-space: nowrap;
|
|
810
873
|
line-height: 1.4;
|
|
811
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
|
+
}
|
|
812
906
|
.ag-compact-title {
|
|
813
907
|
font-size: 12px;
|
|
814
908
|
font-weight: 500;
|
|
815
909
|
color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
|
|
816
910
|
flex: 1;
|
|
817
|
-
|
|
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%;
|
|
818
914
|
white-space: nowrap;
|
|
819
915
|
overflow: hidden;
|
|
820
916
|
text-overflow: ellipsis;
|
|
@@ -832,7 +928,8 @@ const weekDays = $derived.by(() => {
|
|
|
832
928
|
.ag-compact-sub {
|
|
833
929
|
font-size: 10px;
|
|
834
930
|
color: var(--dt-text-3, rgba(0, 0, 0, 0.38));
|
|
835
|
-
flex-shrink:
|
|
931
|
+
flex-shrink: 3;
|
|
932
|
+
min-width: 0;
|
|
836
933
|
white-space: nowrap;
|
|
837
934
|
overflow: hidden;
|
|
838
935
|
text-overflow: ellipsis;
|
|
@@ -842,7 +939,8 @@ const weekDays = $derived.by(() => {
|
|
|
842
939
|
.ag-compact-loc {
|
|
843
940
|
font-size: 10px;
|
|
844
941
|
color: var(--dt-text-3, rgba(0, 0, 0, 0.38));
|
|
845
|
-
flex-shrink:
|
|
942
|
+
flex-shrink: 3;
|
|
943
|
+
min-width: 0;
|
|
846
944
|
white-space: nowrap;
|
|
847
945
|
overflow: hidden;
|
|
848
946
|
text-overflow: ellipsis;
|
|
@@ -870,7 +968,8 @@ const weekDays = $derived.by(() => {
|
|
|
870
968
|
padding: 1px 4px;
|
|
871
969
|
border-radius: 3px;
|
|
872
970
|
white-space: nowrap;
|
|
873
|
-
flex-shrink:
|
|
971
|
+
flex-shrink: 1;
|
|
972
|
+
min-width: 2.5em;
|
|
874
973
|
max-width: 80px;
|
|
875
974
|
overflow: hidden;
|
|
876
975
|
text-overflow: ellipsis;
|
|
@@ -398,6 +398,7 @@ $effect(() => {
|
|
|
398
398
|
ontouchstart={swipe.ontouchstart}
|
|
399
399
|
ontouchmove={swipe.ontouchmove}
|
|
400
400
|
ontouchend={swipe.ontouchend}
|
|
401
|
+
ontouchcancel={swipe.ontouchcancel}
|
|
401
402
|
>
|
|
402
403
|
<div
|
|
403
404
|
class="mb-swipe"
|
|
@@ -504,17 +505,20 @@ $effect(() => {
|
|
|
504
505
|
<div class="mb-ev-stripe"></div>
|
|
505
506
|
<div class="mb-ev-body">
|
|
506
507
|
<EventContent event={p.ev}>
|
|
508
|
+
<!-- Line thresholds must track real content height (title 18 +
|
|
509
|
+
time 13 + sub 14 + loc 12 + tags 19 + padding 8) — showing
|
|
510
|
+
a line the block can't hold clips text at both ends. -->
|
|
507
511
|
<span class="mb-ev-title">{p.ev.title}</span>
|
|
508
512
|
{#if p.height > 32}
|
|
509
513
|
<span class="mb-ev-time">{fmtTime(p.ev.start, locale)} – {fmtTime(p.ev.end, locale)}</span>
|
|
510
514
|
{/if}
|
|
511
|
-
{#if p.ev.subtitle && p.height >
|
|
515
|
+
{#if p.ev.subtitle && p.height > 56}
|
|
512
516
|
<span class="mb-ev-sub">{p.ev.subtitle}</span>
|
|
513
517
|
{/if}
|
|
514
|
-
{#if p.ev.location && p.height >
|
|
518
|
+
{#if p.ev.location && p.height > 72}
|
|
515
519
|
<span class="mb-ev-loc">{p.ev.location}</span>
|
|
516
520
|
{/if}
|
|
517
|
-
{#if p.ev.tags?.length && p.height >
|
|
521
|
+
{#if p.ev.tags?.length && p.height > 88}
|
|
518
522
|
<div class="mb-ev-tags">
|
|
519
523
|
{#each p.ev.tags as tag}
|
|
520
524
|
<span class="mb-ev-tag">{tag}</span>
|
|
@@ -751,6 +755,9 @@ $effect(() => {
|
|
|
751
755
|
|
|
752
756
|
.mb-hour-label {
|
|
753
757
|
width: 40px;
|
|
758
|
+
/* border-box keeps the label inside the 40px gutter that events
|
|
759
|
+
start at — content-box pushed digits flush under the event edge */
|
|
760
|
+
box-sizing: border-box;
|
|
754
761
|
flex-shrink: 0;
|
|
755
762
|
font: 500 11px/1 var(--dt-mono, ui-monospace, monospace);
|
|
756
763
|
color: var(--dt-text-3, rgba(0, 0, 0, 0.38));
|
|
@@ -801,6 +808,7 @@ $effect(() => {
|
|
|
801
808
|
|
|
802
809
|
.mb-now-label {
|
|
803
810
|
width: 40px;
|
|
811
|
+
box-sizing: border-box;
|
|
804
812
|
flex-shrink: 0;
|
|
805
813
|
text-align: right;
|
|
806
814
|
padding-right: 6px;
|
|
@@ -143,6 +143,7 @@ function handleDayKeydown(e, dayMs) {
|
|
|
143
143
|
ontouchstart={swipe.ontouchstart}
|
|
144
144
|
ontouchmove={swipe.ontouchmove}
|
|
145
145
|
ontouchend={swipe.ontouchend}
|
|
146
|
+
ontouchcancel={swipe.ontouchcancel}
|
|
146
147
|
>
|
|
147
148
|
<!-- Vertical day list -->
|
|
148
149
|
<div
|
|
@@ -251,6 +252,7 @@ function handleDayKeydown(e, dayMs) {
|
|
|
251
252
|
.mw-list {
|
|
252
253
|
flex: 1;
|
|
253
254
|
overflow-y: auto;
|
|
255
|
+
overflow-x: hidden;
|
|
254
256
|
overscroll-behavior: contain;
|
|
255
257
|
-webkit-overflow-scrolling: touch;
|
|
256
258
|
scrollbar-width: thin;
|
|
@@ -270,6 +272,10 @@ function handleDayKeydown(e, dayMs) {
|
|
|
270
272
|
align-items: center;
|
|
271
273
|
gap: 12px;
|
|
272
274
|
position: relative;
|
|
275
|
+
/* border-box: width 100% + padding otherwise overflows the list by
|
|
276
|
+
24px, which iOS turns into a horizontal pan that clips the date
|
|
277
|
+
column off the left edge */
|
|
278
|
+
box-sizing: border-box;
|
|
273
279
|
padding: 10px 12px;
|
|
274
280
|
background: transparent;
|
|
275
281
|
transition: background 120ms;
|
|
@@ -60,5 +60,14 @@ export function createSwipe(cb) {
|
|
|
60
60
|
dx = 0;
|
|
61
61
|
cb.onend(dir);
|
|
62
62
|
},
|
|
63
|
+
ontouchcancel() {
|
|
64
|
+
// The browser took over the gesture (scroll, edge-swipe, system UI).
|
|
65
|
+
// Without this, the view stays stuck at the last swipe offset.
|
|
66
|
+
if (!tracking)
|
|
67
|
+
return;
|
|
68
|
+
tracking = false;
|
|
69
|
+
dx = 0;
|
|
70
|
+
cb.onend(0);
|
|
71
|
+
},
|
|
63
72
|
};
|
|
64
73
|
}
|