@nomideusz/svelte-calendar 1.2.0 → 1.4.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.
@@ -21,7 +21,8 @@ import { createClock } from "../../core/clock.svelte.js";
21
21
  import { fitParts } from "../../text-fit.js";
22
22
  import { HOUR_MS, sod, addDaysMs, diffDays } from "../../core/time.js";
23
23
  import { startOfWeek as sowFn, isAllDay, segmentForDay } from "../../core/time.js";
24
- import { fmtHParts, fmtTime, fmtDuration, weekdayShort, weekdayLong } from "../../core/locale.js";
24
+ import { fmtHParts, fmtTime, fmtDuration, fmtWeekRange, weekdayShort, weekdayLong } from "../../core/locale.js";
25
+ import { dateLong } from "../shared/format.js";
25
26
  let { mode = "week", mondayStart = true, locale, height = 520, events = [], style = "", focusDate, oneventclick, oneventcreate, onexternaldrop, selectedEventId = null, readOnly = false, visibleHours } = $props();
26
27
  // ── Context ────────────────────────────────────────
27
28
  const ctx = useCalendarContext();
@@ -122,6 +123,7 @@ const dayCols = $derived.by(() => {
122
123
  cols.push({
123
124
  ms,
124
125
  isToday: ms === todayMs,
126
+ markToday: showDates && ms === todayMs,
125
127
  isPast: equalDays ? false : ms < todayMs,
126
128
  isWeekend: dow === 0 || dow === 6,
127
129
  isDisabled: disabledSet.has(ms),
@@ -301,8 +303,12 @@ const weekIsEmpty = $derived(!events.some((ev) => ev.start.getTime() < weekEndMs
301
303
  // Week with today → current time minus ~1h of context; otherwise the
302
304
  // visibleHours start or 08:00, whichever is later. Lands on a whole hour,
303
305
  // with room for its label (centred on the line) below the sticky top.
306
+ // An event running across that hour line would be sliced under the sticky
307
+ // top, so the scroll starts at that event's hour instead.
304
308
  $effect(() => {
305
309
  void weekStartMs;
310
+ void startHour;
311
+ void endHour;
306
312
  const el = scrollEl;
307
313
  if (!el) return;
308
314
  untrack(() => {
@@ -313,7 +319,26 @@ $effect(() => {
313
319
  } else {
314
320
  targetHour = Math.max(startHour, Math.min(8, endHour - 1));
315
321
  }
316
- el.scrollTop = Math.max(0, (Math.floor(targetHour) - startHour) * HOUR_H - LABEL_ROOM);
322
+ let hour = Math.floor(targetHour);
323
+ // Moving up can slice an earlier event on another day, so repeat
324
+ // until no block crosses the line.
325
+ // ponytail: looks back at most 2 h, so a long block can't drag the view to dawn.
326
+ const floorY = (hour - 2 - startHour) * HOUR_H;
327
+ for (let moved = true; moved;) {
328
+ moved = false;
329
+ const line = (hour - startHour) * HOUR_H;
330
+ for (const day of layoutByDay.values()) {
331
+ for (const p of day) {
332
+ if (p.top < line && p.top + p.height > line && p.top >= floorY) {
333
+ hour = Math.floor(startHour + p.top / HOUR_H);
334
+ moved = true;
335
+ break;
336
+ }
337
+ }
338
+ if (moved) break;
339
+ }
340
+ }
341
+ el.scrollTop = Math.max(0, (hour - startHour) * HOUR_H - LABEL_ROOM);
317
342
  });
318
343
  });
319
344
  // ── Pointer coalescing ─────────────────────────────
@@ -389,11 +414,12 @@ function clampToDayBand(ms, dayMs) {
389
414
  return Math.max(atHour(dayMs, startHour), Math.min(atHour(dayMs, endHour), ms));
390
415
  }
391
416
  // ── Blocked slot helpers ───────────────────────────
417
+ /** The blocked slot covering `hour` on the day, if any (truthy = blocked). */
392
418
  function isBlockedAt(dayMs, hour) {
393
- if (!blockedSlots?.length) return false;
419
+ if (!blockedSlots?.length) return undefined;
394
420
  const jsDay = new Date(dayMs).getDay();
395
421
  const isoDay = jsDay === 0 ? 7 : jsDay;
396
- return blockedSlots.some((slot) => {
422
+ return blockedSlots.find((slot) => {
397
423
  if (slot.day && slot.day !== isoDay) return false;
398
424
  return hour >= slot.start && hour < slot.end;
399
425
  });
@@ -640,11 +666,14 @@ function onColKeydown(e, i) {
640
666
  const day = dayCols[i];
641
667
  if (e.key === "Enter" || e.key === " ") {
642
668
  e.preventDefault();
643
- if (!oneventcreate || readOnly || day.isDisabled) return;
669
+ if (!oneventcreate || readOnly) return;
670
+ // Say why nothing was created, through the calendar's status line.
671
+ if (day.isDisabled) return ctx.announce(`${dateLong(day.ms, locale)}, ${L.unavailable}`);
644
672
  const step = Math.max(1, ctx.snapInterval) / 60;
645
673
  const want = day.isToday ? snapHour(nowFracHour, "floor") + step : 9;
646
674
  const hour = Math.min(Math.max(want, startHour), endHour - step);
647
- if (isBlockedAt(day.ms, hour)) return;
675
+ const slot = isBlockedAt(day.ms, hour);
676
+ if (slot) return ctx.announce(`${slot.label || L.unavailable}, ${blockedRangeLabel(day.ms, slot.start, slot.end)}`);
648
677
  const startMs = atHour(day.ms, hour);
649
678
  oneventcreate({
650
679
  start: new Date(startMs),
@@ -899,7 +928,9 @@ function onWindowKeydown(e) {
899
928
  style:--tw-col-min="{MIN_COL_W}px"
900
929
  style:height={autoHeight ? undefined : (height ? `${height}px` : '100%')}
901
930
  role="region"
902
- aria-label={L.weekAhead}
931
+ aria-label="{singleDay ? L.dayPlanner : L.weekAhead}{dayCols.length
932
+ ? `, ${singleDay ? dateLong(dayCols[0].ms, locale) : fmtWeekRange(dayCols[0].ms, locale, dayCols[dayCols.length - 1].ms)}`
933
+ : ''}"
903
934
  >
904
935
  <div class="tw-scroll" bind:this={scrollEl}>
905
936
  <div class="tw-inner" style:min-width="{innerMinWidth}px">
@@ -912,12 +943,12 @@ function onWindowKeydown(e) {
912
943
  {#each dayCols as day (day.ms)}
913
944
  <div
914
945
  class="tw-hd"
915
- class:tw-hd--today={day.isToday}
916
- aria-current={day.isToday ? 'date' : undefined}
946
+ class:tw-hd--today={day.markToday}
947
+ aria-current={day.markToday ? 'date' : undefined}
917
948
  >
918
949
  <span class="tw-hd-wd">{singleDay ? weekdayLong(day.ms, locale) : weekdayShort(day.ms, locale)}</span>
919
950
  {#if showDates}
920
- <span class="tw-hd-num" class:tw-hd-num--today={day.isToday}>{day.dayNum}</span>
951
+ <span class="tw-hd-num" class:tw-hd-num--today={day.markToday}>{day.dayNum}</span>
921
952
  {/if}
922
953
  {#if dayHeaderSnippet}
923
954
  <div class="tw-hd-custom">
@@ -938,7 +969,7 @@ function onWindowKeydown(e) {
938
969
  {@const segs = allDayByDay.get(day.ms) ?? []}
939
970
  {@const isExpanded = adExpanded[day.ms] ?? false}
940
971
  {@const shown = isExpanded ? segs : segs.slice(0, ALLDAY_MAX)}
941
- <div class="tw-ad-cell" class:tw-ad-cell--today={day.isToday}>
972
+ <div class="tw-ad-cell" class:tw-ad-cell--today={day.markToday}>
942
973
  {#each shown as seg (seg.ev.id)}
943
974
  {@render allDayChip(seg)}
944
975
  {/each}
@@ -1011,14 +1042,14 @@ function onWindowKeydown(e) {
1011
1042
  <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
1012
1043
  <div
1013
1044
  class="tw-col"
1014
- class:tw-col--today={day.isToday}
1045
+ class:tw-col--today={day.markToday}
1015
1046
  class:tw-col--past={day.isPast}
1016
1047
  class:tw-col--weekend={day.isWeekend}
1017
1048
  class:tw-col--disabled={day.isDisabled}
1018
1049
  data-day={day.ms}
1019
1050
  role={colsCreate ? 'group' : undefined}
1020
- aria-label={colsCreate ? `${weekdayLong(day.ms, locale)} ${day.dayNum}, ${L.createEvent}` : undefined}
1021
- aria-keyshortcuts={colsCreate ? 'Enter' : undefined}
1051
+ aria-label={colsCreate ? `${dateLong(day.ms, locale)}, ${day.isDisabled ? L.unavailable : L.createEvent}` : undefined}
1052
+ aria-keyshortcuts={colsCreate && !day.isDisabled ? 'Enter' : undefined}
1022
1053
  tabindex={colsCreate ? (day.ms === tabbableColMs ? 0 : -1) : undefined}
1023
1054
  onfocusin={() => (focusedColMs = day.ms)}
1024
1055
  onkeydown={colsCreate ? (e) => onColKeydown(e, di) : undefined}
@@ -1064,6 +1095,7 @@ function onWindowKeydown(e) {
1064
1095
  class:tw-ev--past={p.ev.end.getTime() < clock.tick}
1065
1096
  class:tw-ev--short={p.height < 44}
1066
1097
  class:tw-ev--compact={p.height < 34}
1098
+ class:tw-ev--on-now={weekHasToday && nowY !== null && p.top < nowY && p.top + p.height > nowY}
1067
1099
  style:top="{p.top}px"
1068
1100
  style:height="{p.height}px"
1069
1101
  style:left="calc({(p.col / p.totalCols) * 100}% + 1px)"
@@ -1121,8 +1153,8 @@ function onWindowKeydown(e) {
1121
1153
  style:top="{ghost.top}px"
1122
1154
  style:height="{ghost.height}px"
1123
1155
  style:--ev-color={ghost.create
1124
- ? 'var(--dt-accent, #2563eb)'
1125
- : (movingEvent?.color ?? 'var(--dt-accent, #2563eb)')}
1156
+ ? 'var(--dt-accent, #1d4fd7)'
1157
+ : (movingEvent?.color ?? 'var(--dt-event, var(--dt-accent))')}
1126
1158
  aria-hidden="true"
1127
1159
  >
1128
1160
  {#if ghost.showTime}
@@ -1138,9 +1170,8 @@ function onWindowKeydown(e) {
1138
1170
 
1139
1171
  <!-- Now line (today's column only) -->
1140
1172
  {#if day.isToday && nowY !== null}
1141
- <div class="tw-now" style:top="{nowY}px" aria-label={L.currentTime}>
1142
- <span class="tw-now-dot" aria-hidden="true"></span>
1143
- </div>
1173
+ <div class="tw-now" style:top="{nowY}px" aria-label={L.currentTime}></div>
1174
+ <span class="tw-now-dot" style:top="{nowY}px" aria-hidden="true"></span>
1144
1175
  {/if}
1145
1176
  </div>
1146
1177
  {/each}
@@ -1166,7 +1197,7 @@ function onWindowKeydown(e) {
1166
1197
  overflow: hidden;
1167
1198
  user-select: none;
1168
1199
  font-variant-numeric: tabular-nums;
1169
- background: var(--dt-paper, none), var(--dt-bg, #ffffff);
1200
+ background: var(--dt-paper, none), var(--dt-bg, #efebe9);
1170
1201
  -webkit-tap-highlight-color: transparent;
1171
1202
  }
1172
1203
  .tw--auto { overflow: visible; }
@@ -1183,12 +1214,12 @@ function onWindowKeydown(e) {
1183
1214
  overscroll-behavior-x: contain;
1184
1215
  overscroll-behavior-y: auto;
1185
1216
  scrollbar-width: thin;
1186
- scrollbar-color: var(--dt-scrollbar, rgba(0, 0, 0, 0.1)) transparent;
1217
+ scrollbar-color: var(--dt-scrollbar, rgba(15, 15, 15, 0.1)) transparent;
1187
1218
  }
1188
1219
  .tw--auto .tw-scroll { overflow-y: visible; }
1189
1220
  .tw-scroll::-webkit-scrollbar { width: 5px; height: 5px; }
1190
1221
  .tw-scroll::-webkit-scrollbar-thumb {
1191
- background: var(--dt-scrollbar, rgba(0, 0, 0, 0.1));
1222
+ background: var(--dt-scrollbar, rgba(15, 15, 15, 0.1));
1192
1223
  border-radius: 4px;
1193
1224
  }
1194
1225
  .tw-scroll::-webkit-scrollbar-track { background: transparent; }
@@ -1204,8 +1235,8 @@ function onWindowKeydown(e) {
1204
1235
  position: sticky;
1205
1236
  top: 0;
1206
1237
  z-index: 30;
1207
- background: var(--dt-paper, none), var(--dt-bg, #ffffff);
1208
- border-bottom: 1px solid var(--dt-border, rgba(0, 0, 0, 0.08));
1238
+ background: var(--dt-paper, none), var(--dt-bg, #efebe9);
1239
+ border-bottom: 1px solid var(--dt-border, rgba(15, 15, 15, 0.08));
1209
1240
  }
1210
1241
 
1211
1242
  /* ─── Day header row ─────────────────────────────── */
@@ -1218,7 +1249,7 @@ function onWindowKeydown(e) {
1218
1249
  position: sticky;
1219
1250
  left: 0;
1220
1251
  z-index: 2;
1221
- background: var(--dt-paper, none), var(--dt-bg, #ffffff);
1252
+ background: var(--dt-paper, none), var(--dt-bg, #efebe9);
1222
1253
  }
1223
1254
 
1224
1255
  .tw-hd {
@@ -1229,17 +1260,17 @@ function onWindowKeydown(e) {
1229
1260
  align-items: center;
1230
1261
  gap: 4px;
1231
1262
  padding: 14px 4px 12px;
1232
- border-left: 1px solid var(--dt-border, rgba(0, 0, 0, 0.08));
1263
+ border-left: 1px solid var(--dt-border, rgba(15, 15, 15, 0.08));
1233
1264
  }
1234
1265
 
1235
1266
  .tw-hd-wd {
1236
1267
  font: 500 12px/1 var(--dt-sans, system-ui, sans-serif);
1237
1268
  letter-spacing: 0.08em;
1238
1269
  text-transform: uppercase;
1239
- color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
1270
+ color: var(--dt-text-2, rgba(15, 15, 15, 0.54));
1240
1271
  }
1241
1272
  .tw-hd--today .tw-hd-wd {
1242
- color: var(--dt-accent, #2563eb);
1273
+ color: var(--dt-accent, #1d4fd7);
1243
1274
  }
1244
1275
 
1245
1276
  /* The day number is the typography of the grid: large, regular weight. */
@@ -1250,11 +1281,11 @@ function onWindowKeydown(e) {
1250
1281
  font: 400 40px/1 var(--dt-sans, system-ui, sans-serif);
1251
1282
  font-stretch: 90%;
1252
1283
  letter-spacing: -0.02em;
1253
- color: var(--dt-text, rgba(0, 0, 0, 0.87));
1284
+ color: var(--dt-text, rgba(15, 15, 15, 0.87));
1254
1285
  }
1255
1286
  /* Today: the numeral in the accent, nothing else. */
1256
1287
  .tw-hd-num--today {
1257
- color: var(--dt-accent, #2563eb);
1288
+ color: var(--dt-accent, #1d4fd7);
1258
1289
  }
1259
1290
 
1260
1291
  .tw-hd-custom {
@@ -1265,7 +1296,7 @@ function onWindowKeydown(e) {
1265
1296
  /* ─── All-day strip ──────────────────────────────── */
1266
1297
  .tw-allday {
1267
1298
  display: flex;
1268
- border-top: 1px solid var(--dt-border, rgba(0, 0, 0, 0.08));
1299
+ border-top: 1px solid var(--dt-border, rgba(15, 15, 15, 0.08));
1269
1300
  }
1270
1301
 
1271
1302
  .tw-ad-gutter {
@@ -1276,15 +1307,16 @@ function onWindowKeydown(e) {
1276
1307
  position: sticky;
1277
1308
  left: 0;
1278
1309
  z-index: 2;
1279
- background: var(--dt-paper, none), var(--dt-bg, #ffffff);
1310
+ background: var(--dt-paper, none), var(--dt-bg, #efebe9);
1280
1311
  display: flex;
1281
1312
  align-items: flex-start;
1282
1313
  justify-content: flex-end;
1283
1314
  padding: 6px 8px 6px 0;
1284
1315
  }
1285
1316
  .tw-ad-gutter-lb {
1286
- font: 500 10px/1.2 var(--dt-sans, system-ui, sans-serif);
1287
- color: var(--dt-text-3, rgba(0, 0, 0, 0.38));
1317
+ font: 500 12px/1.2 var(--dt-sans, system-ui, sans-serif);
1318
+ letter-spacing: 0.08em;
1319
+ color: var(--dt-text-2, rgba(15, 15, 15, 0.66));
1288
1320
  text-align: right;
1289
1321
  }
1290
1322
 
@@ -1295,9 +1327,9 @@ function onWindowKeydown(e) {
1295
1327
  flex-direction: column;
1296
1328
  gap: 2px;
1297
1329
  padding: 4px 0 5px;
1298
- border-left: 1px solid var(--dt-border, rgba(0, 0, 0, 0.08));
1330
+ border-left: 1px solid var(--dt-border, rgba(15, 15, 15, 0.08));
1299
1331
  }
1300
- .tw-ad-cell--today { background: var(--dt-today-bg, rgba(37, 99, 235, 0.04)); }
1332
+ .tw-ad-cell--today { background: var(--dt-today-bg, rgba(29, 79, 215, 0.04)); }
1301
1333
 
1302
1334
  .tw-ad {
1303
1335
  appearance: none;
@@ -1333,11 +1365,11 @@ function onWindowKeydown(e) {
1333
1365
  text-decoration: line-through;
1334
1366
  }
1335
1367
  .tw-ad--cancelled {
1336
- background: color-mix(in srgb, var(--ev-color) 55%, var(--dt-bg, #fff));
1368
+ background: color-mix(in srgb, var(--ev-color) 55%, var(--dt-bg, #efebe9));
1337
1369
  }
1338
1370
  .tw-ad:focus-visible {
1339
1371
  outline: none;
1340
- box-shadow: inset 0 0 0 2px var(--dt-btn-text, #fff), 0 0 0 2px var(--dt-accent, #2563eb);
1372
+ box-shadow: inset 0 0 0 2px var(--dt-btn-text, #fff), 0 0 0 2px var(--dt-accent, #1d4fd7);
1341
1373
  }
1342
1374
 
1343
1375
  .tw-ad-title {
@@ -1350,7 +1382,7 @@ function onWindowKeydown(e) {
1350
1382
  min-width: 0;
1351
1383
  }
1352
1384
  .tw-ad-span {
1353
- font: 400 11px/1 var(--dt-sans, system-ui, sans-serif);
1385
+ font: 400 12.5px/1 var(--dt-sans, system-ui, sans-serif);
1354
1386
  font-variant-numeric: tabular-nums;
1355
1387
  color: inherit;
1356
1388
  opacity: 0.75;
@@ -1380,19 +1412,19 @@ function onWindowKeydown(e) {
1380
1412
  appearance: none;
1381
1413
  background: none;
1382
1414
  border: none;
1383
- border-radius: 3px;
1415
+ border-radius: 0;
1384
1416
  text-align: left;
1385
1417
  align-self: flex-start;
1386
- font: 500 10px/1 var(--dt-sans, system-ui, sans-serif);
1387
- color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
1418
+ font: 400 12.5px/1 var(--dt-sans, system-ui, sans-serif);
1419
+ color: var(--dt-text-2, rgba(15, 15, 15, 0.54));
1388
1420
  padding: 2px 6px;
1389
1421
  cursor: pointer;
1390
1422
  -webkit-tap-highlight-color: transparent;
1391
1423
  }
1392
- .tw-ad-more:hover { color: var(--dt-text, rgba(0, 0, 0, 0.87)); }
1424
+ .tw-ad-more:hover { color: var(--dt-text, rgba(15, 15, 15, 0.87)); }
1393
1425
  .tw-ad-more:focus-visible {
1394
1426
  outline: none;
1395
- box-shadow: 0 0 0 2px var(--dt-accent, #2563eb);
1427
+ box-shadow: 0 0 0 2px var(--dt-accent, #1d4fd7);
1396
1428
  }
1397
1429
 
1398
1430
  /* ─── Grid body ──────────────────────────────────── */
@@ -1407,8 +1439,8 @@ function onWindowKeydown(e) {
1407
1439
  position: sticky;
1408
1440
  left: 0;
1409
1441
  z-index: 20;
1410
- background: var(--dt-paper, none), var(--dt-bg, #ffffff);
1411
- border-right: 1px solid var(--dt-border, rgba(0, 0, 0, 0.08));
1442
+ background: var(--dt-paper, none), var(--dt-bg, #efebe9);
1443
+ border-right: 1px solid var(--dt-border, rgba(15, 15, 15, 0.08));
1412
1444
  }
1413
1445
 
1414
1446
  /* Timetable hour: the numeral carries the rank, the suffix sits raised
@@ -1425,7 +1457,7 @@ function onWindowKeydown(e) {
1425
1457
  font-stretch: 85%;
1426
1458
  font-variant-numeric: tabular-nums;
1427
1459
  letter-spacing: -0.02em;
1428
- color: var(--dt-text, rgba(0, 0, 0, 0.87));
1460
+ color: var(--dt-text, rgba(15, 15, 15, 0.87));
1429
1461
  white-space: nowrap;
1430
1462
  }
1431
1463
  .tw-gutter-lb::after {
@@ -1435,7 +1467,7 @@ function onWindowKeydown(e) {
1435
1467
  height: 1px;
1436
1468
  margin-left: 5px;
1437
1469
  align-self: center;
1438
- background: var(--dt-border-day, rgba(0, 0, 0, 0.3));
1470
+ background: var(--dt-border-day, rgba(15, 15, 15, 0.3));
1439
1471
  }
1440
1472
  .tw-gutter-lb--under {
1441
1473
  visibility: hidden;
@@ -1445,7 +1477,7 @@ function onWindowKeydown(e) {
1445
1477
  font-weight: 500;
1446
1478
  line-height: 1;
1447
1479
  letter-spacing: 0;
1448
- color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
1480
+ color: var(--dt-text-2, rgba(15, 15, 15, 0.54));
1449
1481
  }
1450
1482
 
1451
1483
  /* The clock beside the now line, and the drag's edges while it lasts —
@@ -1457,16 +1489,16 @@ function onWindowKeydown(e) {
1457
1489
  transform: translateY(-50%);
1458
1490
  padding: 2px 4px;
1459
1491
  border-radius: 0;
1460
- background: var(--dt-surface, var(--dt-bg, #fff));
1492
+ background: var(--dt-surface, var(--dt-bg, #efebe9));
1461
1493
  font: 500 11px/1.2 var(--dt-sans, system-ui, sans-serif);
1462
1494
  font-variant-numeric: tabular-nums;
1463
- color: var(--dt-accent, #2563eb);
1495
+ color: var(--dt-accent, #1d4fd7);
1464
1496
  white-space: nowrap;
1465
1497
  z-index: 2;
1466
1498
  }
1467
1499
  /* The clock is a reversed plate on the accent, like a board's live time. */
1468
1500
  .tw-gutter-now {
1469
- background: var(--dt-accent, #2563eb);
1501
+ background: var(--dt-accent, #1d4fd7);
1470
1502
  color: var(--dt-btn-text, #ffffff);
1471
1503
  }
1472
1504
 
@@ -1491,7 +1523,7 @@ function onWindowKeydown(e) {
1491
1523
  left: 0;
1492
1524
  right: 0;
1493
1525
  height: 1px;
1494
- background: var(--dt-border, rgba(0, 0, 0, 0.08));
1526
+ background: var(--dt-border, rgba(15, 15, 15, 0.08));
1495
1527
  }
1496
1528
  .tw-line--half { opacity: 0.4; }
1497
1529
  .tw-now-week {
@@ -1499,7 +1531,7 @@ function onWindowKeydown(e) {
1499
1531
  left: 0;
1500
1532
  right: 0;
1501
1533
  height: 0;
1502
- border-top: 2px solid var(--dt-accent, #2563eb);
1534
+ border-top: 2px solid var(--dt-accent, #1d4fd7);
1503
1535
  opacity: 0.85;
1504
1536
  transform: translateY(-1px);
1505
1537
  }
@@ -1509,35 +1541,38 @@ function onWindowKeydown(e) {
1509
1541
  flex: 1 1 0;
1510
1542
  min-width: var(--tw-col-min, 110px);
1511
1543
  position: relative;
1512
- border-left: 1px solid var(--dt-border, rgba(0, 0, 0, 0.08));
1544
+ border-left: 1px solid var(--dt-border, rgba(15, 15, 15, 0.08));
1513
1545
  box-sizing: border-box;
1514
1546
  }
1515
1547
  .tw-col:focus-visible {
1516
- outline: 2px solid var(--dt-accent, #2563eb);
1548
+ outline: 2px solid var(--dt-accent, #1d4fd7);
1517
1549
  outline-offset: -2px;
1518
1550
  }
1519
1551
  /* The gutter's right border already bounds the first column */
1520
1552
  .tw-lines + .tw-col { border-left: none; }
1521
1553
 
1522
- .tw-col--today { background: var(--dt-today-bg, rgba(37, 99, 235, 0.04)); }
1554
+ /* --tw-col-wash: the column's ground, repeated under an event that the
1555
+ now line crosses (that event is opaque, so the line runs beneath it). */
1556
+ .tw-col { background: var(--tw-col-wash, transparent); }
1557
+ .tw-col--today { --tw-col-wash: var(--dt-today-bg, rgba(29, 79, 215, 0.04)); }
1523
1558
  /* Dim past days with a wash, never a subtree opacity (event contrast) */
1524
1559
  .tw-col--past {
1525
- background: color-mix(in srgb, var(--dt-text, rgba(0, 0, 0, 0.87)) 4%, transparent);
1560
+ --tw-col-wash: color-mix(in srgb, var(--dt-text, rgba(15, 15, 15, 0.87)) 4%, transparent);
1526
1561
  }
1527
1562
  /* What has already happened steps back; a cancelled block keeps its own look. */
1528
1563
  .tw-ev--past:not(.tw-ev--cancelled) {
1529
1564
  opacity: 0.78;
1530
1565
  }
1531
1566
  .tw-col--weekend:not(.tw-col--today):not(.tw-col--past) {
1532
- background: var(--dt-weekend-bg, rgba(0, 0, 0, 0.012));
1567
+ --tw-col-wash: var(--dt-weekend-bg, rgba(15, 15, 15, 0.012));
1533
1568
  }
1534
1569
  .tw-col--disabled {
1535
1570
  background: repeating-linear-gradient(
1536
1571
  45deg,
1537
1572
  transparent,
1538
1573
  transparent 6px,
1539
- var(--dt-border, rgba(0, 0, 0, 0.08)) 6px,
1540
- var(--dt-border, rgba(0, 0, 0, 0.08)) 7px
1574
+ var(--dt-border, rgba(15, 15, 15, 0.08)) 6px,
1575
+ var(--dt-border, rgba(15, 15, 15, 0.08)) 7px
1541
1576
  ) !important;
1542
1577
  }
1543
1578
 
@@ -1549,8 +1584,8 @@ function onWindowKeydown(e) {
1549
1584
  z-index: 2;
1550
1585
  background: repeating-linear-gradient(
1551
1586
  -45deg,
1552
- color-mix(in srgb, var(--dt-text, rgba(0, 0, 0, 0.87)) 4%, transparent),
1553
- color-mix(in srgb, var(--dt-text, rgba(0, 0, 0, 0.87)) 4%, transparent) 4px,
1587
+ color-mix(in srgb, var(--dt-text, rgba(15, 15, 15, 0.87)) 4%, transparent),
1588
+ color-mix(in srgb, var(--dt-text, rgba(15, 15, 15, 0.87)) 4%, transparent) 4px,
1554
1589
  transparent 4px,
1555
1590
  transparent 8px
1556
1591
  );
@@ -1561,8 +1596,8 @@ function onWindowKeydown(e) {
1561
1596
  overflow: hidden;
1562
1597
  }
1563
1598
  .tw-blocked-lb {
1564
- font: 500 10px/1 var(--dt-sans, system-ui, sans-serif);
1565
- color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
1599
+ font: 500 11px/1 var(--dt-sans, system-ui, sans-serif);
1600
+ color: var(--dt-text-2, rgba(15, 15, 15, 0.54));
1566
1601
  text-transform: uppercase;
1567
1602
  letter-spacing: 0.04em;
1568
1603
  white-space: nowrap;
@@ -1570,24 +1605,28 @@ function onWindowKeydown(e) {
1570
1605
  }
1571
1606
 
1572
1607
  /* ─── Now line ───────────────────────────────────── */
1608
+ /* Under the events (z 6): the one it crosses is opaque (.tw-ev--on-now),
1609
+ so the line never strikes through a title. The dot stays on top. */
1573
1610
  .tw-now {
1574
1611
  position: absolute;
1575
1612
  left: 0;
1576
1613
  right: 0;
1577
1614
  height: 0;
1578
- border-top: 2px solid var(--dt-accent, #2563eb);
1579
- z-index: 12;
1615
+ border-top: 2px solid var(--dt-accent, #1d4fd7);
1616
+ z-index: 5;
1580
1617
  pointer-events: none;
1581
1618
  transform: translateY(-1px);
1582
1619
  }
1583
1620
  .tw-now-dot {
1584
1621
  position: absolute;
1585
1622
  left: -4px;
1586
- top: -5px;
1587
1623
  width: 8px;
1588
1624
  height: 8px;
1625
+ margin-top: -4px;
1589
1626
  border-radius: 50%;
1590
- background: var(--dt-accent, #2563eb);
1627
+ background: var(--dt-accent, #1d4fd7);
1628
+ z-index: 12;
1629
+ pointer-events: none;
1591
1630
  }
1592
1631
 
1593
1632
  /* ─── Events ─────────────────────────────────────── */
@@ -1608,7 +1647,7 @@ function onWindowKeydown(e) {
1608
1647
  -webkit-tap-highlight-color: transparent;
1609
1648
  }
1610
1649
  .tw-ev:hover {
1611
- background: var(--dt-hover, rgba(0, 0, 0, 0.04));
1650
+ background: var(--dt-hover, rgba(15, 15, 15, 0.04));
1612
1651
  z-index: 8;
1613
1652
  }
1614
1653
  /* Short blocks keep duration-proportional height, but get a 44px
@@ -1628,8 +1667,19 @@ function onWindowKeydown(e) {
1628
1667
  z-index: 9;
1629
1668
  }
1630
1669
  .tw-ev--current {
1631
- background: var(--dt-accent-dim, rgba(37, 99, 235, 0.12));
1632
- }
1670
+ background: var(--dt-accent-dim, rgba(29, 79, 215, 0.12));
1671
+ }
1672
+ /* An event the now line crosses lays its column's ground (and its own
1673
+ wash) over the paper, so the line passes beneath its text. */
1674
+ .tw-ev.tw-ev--on-now {
1675
+ background:
1676
+ linear-gradient(var(--tw-ev-wash, transparent), var(--tw-ev-wash, transparent)),
1677
+ linear-gradient(var(--tw-col-wash, transparent), var(--tw-col-wash, transparent)),
1678
+ var(--dt-paper, none),
1679
+ var(--dt-bg, #efebe9);
1680
+ }
1681
+ .tw-ev--on-now.tw-ev--current { --tw-ev-wash: var(--dt-accent-dim, rgba(29, 79, 215, 0.12)); }
1682
+ .tw-ev--on-now:hover { --tw-ev-wash: var(--dt-hover, rgba(15, 15, 15, 0.04)); }
1633
1683
  .tw-ev--resizing {
1634
1684
  z-index: 50;
1635
1685
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25);
@@ -1647,12 +1697,12 @@ function onWindowKeydown(e) {
1647
1697
  grid; a tentative one is set in grey with a dashed bar. */
1648
1698
  .tw-ev--cancelled .tw-ev-title {
1649
1699
  text-decoration: line-through;
1650
- color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
1700
+ color: var(--dt-text-2, rgba(15, 15, 15, 0.54));
1651
1701
  }
1652
1702
  .tw-ev--cancelled .tw-ev-stripe { opacity: 0.45; /* decorative bar only */ }
1653
1703
  .tw-ev--tentative .tw-ev-title,
1654
1704
  .tw-ev--full .tw-ev-title {
1655
- color: var(--dt-text-2, rgba(0, 0, 0, 0.54));
1705
+ color: var(--dt-text-2, rgba(15, 15, 15, 0.54));
1656
1706
  }
1657
1707
  .tw-ev--tentative .tw-ev-stripe,
1658
1708
  .tw-ev--limited .tw-ev-stripe {
@@ -1681,25 +1731,30 @@ function onWindowKeydown(e) {
1681
1731
  gap: 1px;
1682
1732
  overflow: hidden;
1683
1733
  }
1684
- /* Compact (< ~35min at default zoom): single inline line "9:00 Title" */
1734
+ /* Compact (< ~35min at default zoom): one line, "Title 9:00". The title
1735
+ wins the room: the time wraps onto a second line, which the block
1736
+ clips, before the title is cut; a title too long alone ellipsizes. */
1685
1737
  .tw-ev--compact .tw-ev-body {
1686
1738
  flex-direction: row;
1687
- align-items: center;
1688
- gap: 4px;
1689
- padding-top: 1px;
1690
- padding-bottom: 1px;
1691
- }
1739
+ flex-wrap: wrap;
1740
+ align-content: flex-start;
1741
+ align-items: baseline;
1742
+ gap: 24px 5px;
1743
+ padding-top: 2px;
1744
+ padding-bottom: 0;
1745
+ }
1746
+ .tw-ev--compact .tw-ev-title { flex: 0 1 auto; min-width: 0; }
1747
+ .tw-ev--compact .tw-ev-end { display: none; }
1692
1748
 
1693
1749
  /* Title first, time under it; both one size, rank by weight and ink. */
1694
1750
  .tw-ev-time {
1695
1751
  order: 1;
1696
1752
  font: 400 14px/1.2 var(--dt-sans, system-ui, sans-serif);
1697
1753
  font-variant-numeric: tabular-nums;
1698
- color: var(--dt-text, rgba(0, 0, 0, 0.87));
1754
+ color: var(--dt-text, rgba(15, 15, 15, 0.87));
1699
1755
  white-space: nowrap;
1700
1756
  flex-shrink: 0;
1701
1757
  }
1702
- .tw-ev--compact .tw-ev-time { order: 0; }
1703
1758
  /* A narrow overlap column keeps the start time whole and drops the end. */
1704
1759
  @container tw-ev (max-width: 104px) {
1705
1760
  .tw-ev-end { display: none; }
@@ -1708,7 +1763,7 @@ function onWindowKeydown(e) {
1708
1763
  .tw-ev-title {
1709
1764
  order: 0;
1710
1765
  font: 500 15px/1.25 var(--dt-sans, system-ui, sans-serif);
1711
- color: var(--dt-text, rgba(0, 0, 0, 0.87));
1766
+ color: var(--dt-text, rgba(15, 15, 15, 0.87));
1712
1767
  white-space: nowrap;
1713
1768
  overflow: hidden;
1714
1769
  text-overflow: ellipsis;
@@ -1717,7 +1772,7 @@ function onWindowKeydown(e) {
1717
1772
  .tw-ev-loc {
1718
1773
  order: 2;
1719
1774
  font: 400 12.5px/1.2 var(--dt-sans, system-ui, sans-serif);
1720
- color: var(--dt-text, rgba(0, 0, 0, 0.87));
1775
+ color: var(--dt-text, rgba(15, 15, 15, 0.87));
1721
1776
  white-space: nowrap;
1722
1777
  overflow: hidden;
1723
1778
  text-overflow: ellipsis;
@@ -1802,7 +1857,7 @@ function onWindowKeydown(e) {
1802
1857
  right: 3px;
1803
1858
  z-index: 40;
1804
1859
  border-radius: 0;
1805
- background: color-mix(in srgb, var(--ev-color) 12%, var(--dt-surface, var(--dt-bg, #ffffff)));
1860
+ background: color-mix(in srgb, var(--ev-color) 12%, var(--dt-surface, var(--dt-bg, #efebe9)));
1806
1861
  outline: 1px solid color-mix(in srgb, var(--ev-color) 45%, transparent);
1807
1862
  box-shadow: 0 6px 18px color-mix(in srgb, var(--ev-color) 24%, rgba(0, 0, 0, 0.22));
1808
1863
  display: flex;
@@ -1820,14 +1875,14 @@ function onWindowKeydown(e) {
1820
1875
  box-shadow: none;
1821
1876
  }
1822
1877
  .tw-ghost-time {
1823
- font: 700 12px/1.15 var(--dt-sans, system-ui, sans-serif);
1878
+ font: 500 12.5px/1.15 var(--dt-sans, system-ui, sans-serif);
1824
1879
  font-variant-numeric: tabular-nums;
1825
- color: var(--ev-color, var(--dt-accent, #2563eb));
1880
+ color: var(--ev-color, var(--dt-accent, #1d4fd7));
1826
1881
  white-space: nowrap;
1827
1882
  }
1828
1883
  .tw-ghost-title {
1829
- font: 600 12px/1.2 var(--dt-sans, system-ui, sans-serif);
1830
- color: var(--dt-text, rgba(0, 0, 0, 0.87));
1884
+ font: 500 12.5px/1.2 var(--dt-sans, system-ui, sans-serif);
1885
+ color: var(--dt-text, rgba(15, 15, 15, 0.87));
1831
1886
  white-space: nowrap;
1832
1887
  overflow: hidden;
1833
1888
  text-overflow: ellipsis;
@@ -1849,7 +1904,7 @@ function onWindowKeydown(e) {
1849
1904
  overflow: hidden scroll container. */
1850
1905
  .tw-ev:focus-visible {
1851
1906
  outline: none;
1852
- box-shadow: 0 0 0 2px var(--dt-accent, #2563eb);
1907
+ box-shadow: 0 0 0 2px var(--dt-accent, #1d4fd7);
1853
1908
  z-index: 9;
1854
1909
  }
1855
1910
 
@@ -48,6 +48,8 @@ export interface CalendarContext {
48
48
  readonly emptySnippet: Snippet | undefined;
49
49
  /** Per-instance labels (Calendar's `labels` prop merged over globals); global labels when headless. */
50
50
  readonly labels: CalendarLabels;
51
+ /** Say something through the calendar's polite status line (no-op when headless). */
52
+ announce(text: string): void;
51
53
  }
52
54
  /**
53
55
  * Read the calendar context.
@@ -51,5 +51,6 @@ export function useCalendarContext() {
51
51
  get eventSnippet() { return raw?.eventSnippet; },
52
52
  get emptySnippet() { return raw?.emptySnippet; },
53
53
  get labels() { return raw?.labels ?? getLabels(); },
54
+ announce: (text) => raw?.announce(text),
54
55
  };
55
56
  }
@@ -18,3 +18,6 @@ export interface TimeSlot {
18
18
  events: TimelineEvent[];
19
19
  }
20
20
  export declare function groupIntoSlots(evts: TimelineEvent[]): TimeSlot[];
21
+ /** A day in full, in the locale's own order: "Monday, October 19" (en-US),
22
+ * "poniedziałek, 19 października" (pl-PL). For screen-reader names. */
23
+ export declare function dateLong(ms: number, locale?: string): string;