@nomideusz/svelte-calendar 1.4.0 → 1.4.2

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.
@@ -10,7 +10,7 @@ import EventContent from "../shared/EventContent.svelte";
10
10
  import { createClock } from "../../core/clock.svelte.js";
11
11
  import { sod, addDaysMs, isAllDay } from "../../core/time.js";
12
12
  import { startOfWeek as sowFn } from "../../core/time.js";
13
- import { fmtTime as _fmtTime, fmtWeekRange, weekdayShort } from "../../core/locale.js";
13
+ import { fmtTime as _fmtTime, fmtDayNum, fmtWeekRange, weekdayShort } from "../../core/locale.js";
14
14
  import { dateLong } from "../shared/format.js";
15
15
  import { createSwipe } from "./swipe.js";
16
16
  let { mondayStart = true, locale, height = null, events = [], style = "", focusDate, oneventclick, selectedEventId = null } = $props();
@@ -207,7 +207,7 @@ function handleDayKeydown(e, dayMs) {
207
207
  <div class="mw-date">
208
208
  <span class="mw-day-name" class:mw-day-name--today={cell.isToday}>{cell.dayName}</span>
209
209
  {#if showDates}
210
- <span class="mw-day-num" class:mw-day-num--today={cell.isToday}>{cell.dayNum}</span>
210
+ <span class="mw-day-num" class:mw-day-num--today={cell.isToday}>{fmtDayNum(cell.ms, locale)}</span>
211
211
  {/if}
212
212
  </div>
213
213
 
@@ -231,7 +231,7 @@ function handleDayKeydown(e, dayMs) {
231
231
  onclick={(e) => { e.stopPropagation(); oneventclick?.(ev); }}
232
232
  onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.stopPropagation(); oneventclick?.(ev); } }}
233
233
  onpointerenter={() => oneventhover?.(ev)}
234
- aria-label="{ev.title}{statusText(ev)}, {evTimeLabel(ev)}"
234
+ aria-label="{ev.title}{statusText(ev)}, {dateLong(cell.ms, locale)}, {evTimeLabel(ev)}"
235
235
  >
236
236
  <span class="mw-ev-stripe"></span>
237
237
  <div class="mw-ev-body">
@@ -247,6 +247,7 @@ function handleDayKeydown(e, dayMs) {
247
247
  type="button"
248
248
  class="mw-ev-more"
249
249
  aria-expanded={expandedDays.has(cell.ms)}
250
+ aria-label="{expandedDays.has(cell.ms) ? L.showLess : L.nMore(cell.totalCount - MAX_EVENTS)}, {dateLong(cell.ms, locale)}"
250
251
  onclick={() => toggleExpand(cell.ms)}
251
252
  >
252
253
  {expandedDays.has(cell.ms) ? L.showLess : L.nMore(cell.totalCount - MAX_EVENTS)}
@@ -356,7 +357,7 @@ function handleDayKeydown(e, dayMs) {
356
357
  }
357
358
  .mw-row-target:focus-visible {
358
359
  outline: none;
359
- box-shadow: inset 0 0 0 2px var(--dt-accent, #1d4fd7);
360
+ box-shadow: inset 0 0 0 2px var(--dt-text, #0f0f0f);
360
361
  }
361
362
 
362
363
  /* ─── Date column ────────────────────────────────── */
@@ -533,19 +534,15 @@ function handleDayKeydown(e, dayMs) {
533
534
  pointer-events: auto;
534
535
  -webkit-tap-highlight-color: transparent;
535
536
  }
536
- /* Hit-slop: 44px effective touch target */
537
- .mw-ev-more::before {
538
- content: '';
539
- position: absolute;
540
- left: 0;
541
- right: 0;
542
- top: 50%;
543
- transform: translateY(-50%);
544
- height: 44px;
537
+ @media (pointer: coarse) {
538
+ .mw-ev-more {
539
+ min-height: 44px;
540
+ min-width: 44px;
541
+ }
545
542
  }
546
543
  .mw-ev-more:focus-visible {
547
544
  outline: none;
548
- box-shadow: 0 0 0 2px var(--dt-accent, #1d4fd7);
545
+ box-shadow: 0 0 0 2px var(--dt-text, #0f0f0f);
549
546
  }
550
547
 
551
548
  /* ─── Chevron ────────────────────────────────────── */
@@ -9,7 +9,8 @@
9
9
  <script lang="ts">import { useCalendarContext } from "../shared/context.svelte.js";
10
10
  import { createClock } from "../../core/clock.svelte.js";
11
11
  import { DAY_MS, sod, addDaysMs, isAllDay } from "../../core/time.js";
12
- import { fmtTime, weekdayShort } from "../../core/locale.js";
12
+ import { fmtTime, fmtDayNum, weekdayShort } from "../../core/locale.js";
13
+ import { dateLong } from "../shared/format.js";
13
14
  let { events = [], style = "", height = null, locale, focusDate, oneventclick, selectedEventId = null } = $props();
14
15
  const ctx = useCalendarContext();
15
16
  const L = $derived(ctx.labels);
@@ -248,7 +249,7 @@ function chipTime(e) {
248
249
  onkeydown={(e) => cellKeydown(e, cell)}
249
250
  >
250
251
  <span class="mg-daynum" class:mg-daynum--today={cell.isToday}>
251
- {cell.dayNum}
252
+ {fmtDayNum(cell.ms, locale)}
252
253
  </span>
253
254
  <!-- Phones: dots are a picture of the day; the cell (its label counts
254
255
  the events) is the one tap and tab target. -->
@@ -282,7 +283,7 @@ function chipTime(e) {
282
283
  class:mg-chip--cancelled={ev.status === 'cancelled'}
283
284
  style:--mg-chip-color={ev.color ?? (isAllDay(ev) ? 'var(--dt-accent)' : 'var(--dt-event, var(--dt-accent))')}
284
285
  title={ev.title}
285
- aria-label="{ev.title}{chipTime(ev) ? `, ${chipTime(ev)}` : ''}"
286
+ aria-label="{ev.title}, {dateLong(cell.ms, locale)}{chipTime(ev) ? `, ${chipTime(ev)}` : ''}"
286
287
  onclick={(e) => {
287
288
  e.stopPropagation();
288
289
  oneventclick?.(ev, e.currentTarget.getBoundingClientRect());
@@ -304,7 +305,7 @@ function chipTime(e) {
304
305
  type="button"
305
306
  class="mg-more"
306
307
  aria-expanded={ondayclick ? undefined : false}
307
- aria-label={L.nMore(cell.overflow)}
308
+ aria-label="{L.nMore(cell.overflow)}, {dateLong(cell.ms, locale)}"
308
309
  onclick={(e) => overflowClick(e, cell)}
309
310
  >
310
311
  {L.nMore(cell.overflow)}
@@ -417,7 +418,7 @@ function chipTime(e) {
417
418
  background: var(--dt-hover, rgba(15, 15, 15, 0.04));
418
419
  }
419
420
  .mg-cell:focus-visible {
420
- outline: 2px solid var(--dt-accent, #1d4fd7);
421
+ outline: 2px solid var(--dt-text, #0f0f0f);
421
422
  outline-offset: -2px;
422
423
  }
423
424
 
@@ -498,7 +499,12 @@ function chipTime(e) {
498
499
  overflow: hidden;
499
500
  text-overflow: ellipsis;
500
501
  white-space: nowrap;
502
+ /* A Latin title in RTL keeps its own direction: the ellipsis cuts its end */
503
+ unicode-bidi: plaintext;
504
+ text-align: left;
501
505
  }
506
+ /* …but it sits on the calendar's side, by the bar */
507
+ .mg-chip-title:dir(rtl) { text-align: right; }
502
508
  .mg-more {
503
509
  align-self: flex-start;
504
510
  border: none;
@@ -515,7 +521,7 @@ function chipTime(e) {
515
521
  color: var(--dt-text);
516
522
  }
517
523
  .mg-more:focus-visible {
518
- box-shadow: 0 0 0 2px var(--dt-accent, #1d4fd7);
524
+ box-shadow: 0 0 0 2px var(--dt-text, #0f0f0f);
519
525
  outline: none;
520
526
  }
521
527
 
@@ -22,7 +22,7 @@ import EventContent from "../shared/EventContent.svelte";
22
22
  import { createClock } from "../../core/clock.svelte.js";
23
23
  import { HOUR_MS, sod, addDaysMs, diffDays } from "../../core/time.js";
24
24
  import { startOfWeek as sowFn, isAllDay, segmentForDay } from "../../core/time.js";
25
- import { weekdayShort, monthLong, fmtTime as _fmtTime } from "../../core/locale.js";
25
+ import { weekdayShort, monthLong, fmtDayNum, fmtTime as _fmtTime } from "../../core/locale.js";
26
26
  import { createChipFit } from "../shared/chip-fit.svelte.js";
27
27
  let { mondayStart = true, locale, height = 520, events = [], style = "", focusDate, oneventclick, oneventcreate, onexternaldrop, selectedEventId = null, readOnly = false, visibleHours } = $props();
28
28
  const ctx = useCalendarContext();
@@ -873,7 +873,7 @@ function onCellKeydown(e, ms) {
873
873
  <div class="wg-cell-hd" class:wg-cell-hd--today={day.isToday}>
874
874
  <span class="wg-day-wd">{weekdayShort(day.ms, locale)}</span>
875
875
  {#if showDates}
876
- <span class="wg-day-num">{day.dayNum}</span>
876
+ <span class="wg-day-num">{fmtDayNum(day.ms, locale)}</span>
877
877
  {/if}
878
878
  </div>
879
879
 
@@ -1446,7 +1446,12 @@ function onCellKeydown(e, ms) {
1446
1446
  text-overflow: ellipsis;
1447
1447
  min-width: 0;
1448
1448
  flex: 1 1 auto;
1449
+ /* A Latin title in RTL keeps its own direction: the ellipsis cuts its end */
1450
+ unicode-bidi: plaintext;
1451
+ text-align: left;
1449
1452
  }
1453
+ /* …but it sits on the calendar's side, by the bar */
1454
+ .wg-ev-title:dir(rtl) { text-align: right; }
1450
1455
 
1451
1456
  .wg-ev-loc {
1452
1457
  font: 400 12.5px / 1 var(--dt-sans, system-ui, sans-serif);
@@ -1476,12 +1481,12 @@ function onCellKeydown(e, ms) {
1476
1481
 
1477
1482
  .wg-ev-more:focus-visible {
1478
1483
  outline: none;
1479
- box-shadow: 0 0 0 2px var(--dt-accent, #1d4fd7);
1484
+ box-shadow: 0 0 0 2px var(--dt-text, #0f0f0f);
1480
1485
  }
1481
1486
 
1482
1487
  /* ─── Focus-visible ──────────────────────────────── */
1483
1488
  .wg-cell:focus-visible {
1484
- outline: 2px solid var(--dt-accent, #1d4fd7);
1489
+ outline: 2px solid var(--dt-text, #0f0f0f);
1485
1490
  outline-offset: -2px;
1486
1491
  }
1487
1492
 
@@ -21,7 +21,7 @@ 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, fmtWeekRange, weekdayShort, weekdayLong } from "../../core/locale.js";
24
+ import { fmtHParts, fmtTime, fmtDayNum, fmtDuration, fmtWeekRange, weekdayShort, weekdayLong } from "../../core/locale.js";
25
25
  import { dateLong } from "../shared/format.js";
26
26
  let { mode = "week", mondayStart = true, locale, height = 520, events = [], style = "", focusDate, oneventclick, oneventcreate, onexternaldrop, selectedEventId = null, readOnly = false, visibleHours } = $props();
27
27
  // ── Context ────────────────────────────────────────
@@ -305,6 +305,9 @@ const weekIsEmpty = $derived(!events.some((ev) => ev.start.getTime() < weekEndMs
305
305
  // with room for its label (centred on the line) below the sticky top.
306
306
  // An event running across that hour line would be sliced under the sticky
307
307
  // top, so the scroll starts at that event's hour instead.
308
+ // A server-rendered grid sits at midnight until this runs: it stays
309
+ // hidden until placed (.tw-body--unplaced), and shows anyway after 1.5s.
310
+ let placed = $state(false);
308
311
  $effect(() => {
309
312
  void weekStartMs;
310
313
  void startHour;
@@ -339,6 +342,7 @@ $effect(() => {
339
342
  }
340
343
  }
341
344
  el.scrollTop = Math.max(0, (hour - startHour) * HOUR_H - LABEL_ROOM);
345
+ placed = true;
342
346
  });
343
347
  });
344
348
  // ── Pointer coalescing ─────────────────────────────
@@ -442,6 +446,9 @@ const TIME_LINE = 11 * 1.1;
442
446
  const TITLE_LINE = 12 * 1.2;
443
447
  const ROOM_LINE = 10 * 1.2;
444
448
  const ROOM_AIR = 9;
449
+ /** Tall enough for two title lines over the time (.tw-ev--tall), in the
450
+ * CSS's own line boxes: title 15px/1.25, time 14px/1.2. */
451
+ const TWO_LINE_H = BODY_PAD_Y + 2 * 15 * 1.25 + LINE_GAP + 14 * 1.2;
445
452
  function blockParts(ev, height) {
446
453
  return fitParts([
447
454
  {
@@ -661,6 +668,20 @@ function handleColsClick(e) {
661
668
  let focusedColMs = $state(null);
662
669
  const tabbableColMs = $derived((dayCols.find((d) => d.ms === focusedColMs) ?? dayCols.find((d) => d.isToday) ?? dayCols[0])?.ms);
663
670
  const colsCreate = $derived(!!oneventcreate && !readOnly);
671
+ // After Enter creates, the keyboard follows the new block once the host
672
+ // adds it: the first unseen event at that start, within two updates.
673
+ let createFocus = null;
674
+ $effect(() => {
675
+ const list = events;
676
+ untrack(() => {
677
+ if (!createFocus) return;
678
+ const { startMs, known } = createFocus;
679
+ const ev = list.find((e) => !known.has(e.id) && e.start.getTime() === startMs);
680
+ const block = ev && colsEl?.querySelector(`.tw-ev[data-event-id="${CSS.escape(ev.id)}"]`);
681
+ if (block) block.focus();
682
+ if (block || --createFocus.tries <= 0) createFocus = null;
683
+ });
684
+ });
664
685
  function onColKeydown(e, i) {
665
686
  if (e.target !== e.currentTarget) return;
666
687
  const day = dayCols[i];
@@ -675,6 +696,11 @@ function onColKeydown(e, i) {
675
696
  const slot = isBlockedAt(day.ms, hour);
676
697
  if (slot) return ctx.announce(`${slot.label || L.unavailable}, ${blockedRangeLabel(day.ms, slot.start, slot.end)}`);
677
698
  const startMs = atHour(day.ms, hour);
699
+ createFocus = {
700
+ startMs,
701
+ known: new Set(events.map((ev) => ev.id)),
702
+ tries: 2
703
+ };
678
704
  oneventcreate({
679
705
  start: new Date(startMs),
680
706
  end: new Date(startMs + Math.max(60, minDuration ?? 0) * 6e4)
@@ -948,7 +974,7 @@ function onWindowKeydown(e) {
948
974
  >
949
975
  <span class="tw-hd-wd">{singleDay ? weekdayLong(day.ms, locale) : weekdayShort(day.ms, locale)}</span>
950
976
  {#if showDates}
951
- <span class="tw-hd-num" class:tw-hd-num--today={day.markToday}>{day.dayNum}</span>
977
+ <span class="tw-hd-num" class:tw-hd-num--today={day.markToday}>{fmtDayNum(day.ms, locale)}</span>
952
978
  {/if}
953
979
  {#if dayHeaderSnippet}
954
980
  <div class="tw-hd-custom">
@@ -989,18 +1015,16 @@ function onWindowKeydown(e) {
989
1015
  {/if}
990
1016
 
991
1017
  <!-- Grid body -->
992
- <div class="tw-body" style:height="{gridHeight}px">
1018
+ <div class="tw-body" class:tw-body--unplaced={!placed} style:height="{gridHeight}px">
993
1019
  <!-- Time gutter -->
994
1020
  <div class="tw-gutter" style:width="{GUTTER_W}px" aria-hidden="true">
995
1021
  {#each { length: hourCount } as _, i}
996
- {#if i > 0}
997
- {@const [hNum, hSuf] = fmtHParts(startHour + i, locale)}
998
- <!-- an hour the now plate would half-cover gives way to it -->
999
- <span
1000
- class="tw-gutter-lb"
1001
- class:tw-gutter-lb--under={nowY !== null && weekHasToday && Math.abs(i * HOUR_H - nowY) < 18}
1002
- style:top="{i * HOUR_H}px">{hNum}<small>{hSuf}</small></span>
1003
- {/if}
1022
+ {@const [hNum, hSuf] = fmtHParts(startHour + i, locale)}
1023
+ <!-- an hour the now plate would half-cover gives way to it -->
1024
+ <span
1025
+ class="tw-gutter-lb"
1026
+ class:tw-gutter-lb--under={nowY !== null && weekHasToday && Math.abs(i * HOUR_H - nowY) < 18}
1027
+ style:top="{i * HOUR_H}px">{hNum}<small>{hSuf}</small></span>
1004
1028
  {/each}
1005
1029
  {#if axisGhost}
1006
1030
  <!-- The axis reads the drag to the minute, at both edges. -->
@@ -1095,6 +1119,7 @@ function onWindowKeydown(e) {
1095
1119
  class:tw-ev--past={p.ev.end.getTime() < clock.tick}
1096
1120
  class:tw-ev--short={p.height < 44}
1097
1121
  class:tw-ev--compact={p.height < 34}
1122
+ class:tw-ev--tall={p.height >= TWO_LINE_H}
1098
1123
  class:tw-ev--on-now={weekHasToday && nowY !== null && p.top < nowY && p.top + p.height > nowY}
1099
1124
  style:top="{p.top}px"
1100
1125
  style:height="{p.height}px"
@@ -1378,9 +1403,13 @@ function onWindowKeydown(e) {
1378
1403
  white-space: nowrap;
1379
1404
  overflow: hidden;
1380
1405
  text-overflow: ellipsis;
1406
+ unicode-bidi: plaintext;
1407
+ text-align: left;
1381
1408
  flex: 1;
1382
1409
  min-width: 0;
1383
1410
  }
1411
+ /* …but it sits on the calendar's side, by the bar */
1412
+ .tw-ad-title:dir(rtl) { text-align: right; }
1384
1413
  .tw-ad-span {
1385
1414
  font: 400 12.5px/1 var(--dt-sans, system-ui, sans-serif);
1386
1415
  font-variant-numeric: tabular-nums;
@@ -1424,7 +1453,7 @@ function onWindowKeydown(e) {
1424
1453
  .tw-ad-more:hover { color: var(--dt-text, rgba(15, 15, 15, 0.87)); }
1425
1454
  .tw-ad-more:focus-visible {
1426
1455
  outline: none;
1427
- box-shadow: 0 0 0 2px var(--dt-accent, #1d4fd7);
1456
+ box-shadow: 0 0 0 2px var(--dt-text, #0f0f0f);
1428
1457
  }
1429
1458
 
1430
1459
  /* ─── Grid body ──────────────────────────────────── */
@@ -1432,6 +1461,11 @@ function onWindowKeydown(e) {
1432
1461
  display: flex;
1433
1462
  position: relative;
1434
1463
  }
1464
+ .tw-body--unplaced {
1465
+ visibility: hidden;
1466
+ animation: tw-reveal 0s 1.5s forwards;
1467
+ }
1468
+ @keyframes tw-reveal { to { visibility: visible; } }
1435
1469
 
1436
1470
  /* ─── Time gutter ────────────────────────────────── */
1437
1471
  .tw-gutter {
@@ -1469,6 +1503,10 @@ function onWindowKeydown(e) {
1469
1503
  align-self: center;
1470
1504
  background: var(--dt-border-day, rgba(15, 15, 15, 0.3));
1471
1505
  }
1506
+ /* The first hour has no room above its line: it hangs below it, the
1507
+ tick still on the line. */
1508
+ .tw-gutter-lb:first-child { transform: none; }
1509
+ .tw-gutter-lb:first-child::after { align-self: flex-start; }
1472
1510
  .tw-gutter-lb--under {
1473
1511
  visibility: hidden;
1474
1512
  }
@@ -1545,7 +1583,7 @@ function onWindowKeydown(e) {
1545
1583
  box-sizing: border-box;
1546
1584
  }
1547
1585
  .tw-col:focus-visible {
1548
- outline: 2px solid var(--dt-accent, #1d4fd7);
1586
+ outline: 2px solid var(--dt-text, #0f0f0f);
1549
1587
  outline-offset: -2px;
1550
1588
  }
1551
1589
  /* The gutter's right border already bounds the first column */
@@ -1767,6 +1805,30 @@ function onWindowKeydown(e) {
1767
1805
  white-space: nowrap;
1768
1806
  overflow: hidden;
1769
1807
  text-overflow: ellipsis;
1808
+ /* A Latin title in an RTL calendar keeps its own direction, so the
1809
+ ellipsis cuts its end, not its start. */
1810
+ unicode-bidi: plaintext;
1811
+ text-align: left;
1812
+ }
1813
+ /* …but it sits on the calendar's side, by the bar */
1814
+ :is(.tw-ev-title, .tw-ev-loc):dir(rtl) { text-align: right; }
1815
+ /* A block tall enough for two title lines over the time wraps the title
1816
+ to two. The parts flow in a wrapping column, so a room that no longer
1817
+ fits under them moves into a second column the block clips: it gives
1818
+ way whole, before the title is cut, instead of being sliced. */
1819
+ .tw-ev--tall .tw-ev-body {
1820
+ flex-wrap: wrap;
1821
+ column-gap: 16px;
1822
+ }
1823
+ .tw-ev--tall :is(.tw-ev-title, .tw-ev-time, .tw-ev-loc) { width: 100%; }
1824
+ .tw-ev--tall .tw-ev-title {
1825
+ display: -webkit-box;
1826
+ -webkit-box-orient: vertical;
1827
+ -webkit-line-clamp: 2;
1828
+ line-clamp: 2;
1829
+ white-space: normal;
1830
+ overflow-wrap: break-word; /* a word wider than an overlap lane breaks, not clips */
1831
+ hyphens: auto; /* …and hyphenates where the host page declares a lang */
1770
1832
  }
1771
1833
 
1772
1834
  .tw-ev-loc {
@@ -1776,6 +1838,8 @@ function onWindowKeydown(e) {
1776
1838
  white-space: nowrap;
1777
1839
  overflow: hidden;
1778
1840
  text-overflow: ellipsis;
1841
+ unicode-bidi: plaintext;
1842
+ text-align: left;
1779
1843
  }
1780
1844
 
1781
1845
  .tw-ev-live {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nomideusz/svelte-calendar",
3
- "version": "1.4.0",
3
+ "version": "1.4.2",
4
4
  "description": "A themeable Svelte 5 calendar: day, week, scrolling-week and month views, agenda lists, touch-first mobile layouts, drag-and-drop, timezones, a headless API and an embeddable web component.",
5
5
  "type": "module",
6
6
  "sideEffects": [