@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.
@@ -28,6 +28,39 @@ const todayMs = $derived(clock.today);
28
28
  // cell tap (day drill-down) carry the interaction.
29
29
  const dotsMode = $derived(isMobile);
30
30
  const MAX_CHIPS = 3;
31
+ // How many chips a cell has room for, measured, never more than MAX_CHIPS.
32
+ // A cell that overflows must also show its "+N more", so it gets one row
33
+ // fewer when that row would not fit beside its chips: a hidden event is
34
+ // always counted, never clipped away. Auto height and dots grow instead.
35
+ let fitAll = $state(MAX_CHIPS);
36
+ let fitMore = $state(MAX_CHIPS);
37
+ function measureFit() {
38
+ const cell = bodyEl?.querySelector(".mg-cell");
39
+ const chip = bodyEl?.querySelector(".mg-chip");
40
+ if (!cell || !chip) return;
41
+ const num = cell.querySelector(".mg-daynum");
42
+ const chips = cell.querySelector(".mg-chips");
43
+ const cs = getComputedStyle(cell);
44
+ const gap = chips ? parseFloat(getComputedStyle(chips).rowGap) || 0 : 0;
45
+ const room = cell.clientHeight - parseFloat(cs.paddingTop) - parseFloat(cs.paddingBottom) - (num?.offsetHeight ?? 0) - (parseFloat(cs.rowGap) || 0);
46
+ const chipH = chip.offsetHeight;
47
+ const moreH = bodyEl?.querySelector(".mg-more")?.offsetHeight ?? chipH;
48
+ if (!chipH) return;
49
+ fitAll = Math.min(MAX_CHIPS, Math.floor((room + gap) / (chipH + gap)));
50
+ fitMore = Math.max(0, Math.min(MAX_CHIPS, Math.floor((room - moreH) / (chipH + gap))));
51
+ }
52
+ $effect(() => {
53
+ if (!bodyEl || autoHeight || dotsMode) {
54
+ fitAll = fitMore = MAX_CHIPS;
55
+ return;
56
+ }
57
+ void events;
58
+ measureFit();
59
+ const ro = new ResizeObserver(measureFit);
60
+ ro.observe(bodyEl);
61
+ document.fonts?.ready.then(measureFit);
62
+ return () => ro.disconnect();
63
+ });
31
64
  const range = $derived(viewState?.range);
32
65
  const focusMonth = $derived((focusDate ?? new Date()).getMonth());
33
66
  $effect(() => {
@@ -106,11 +139,18 @@ function cellKeydown(e, cell) {
106
139
  case " ":
107
140
  if (!cell.isDisabled) {
108
141
  e.preventDefault();
109
- ondayclick?.(cell.date);
142
+ activateCell(cell);
110
143
  }
111
144
  break;
112
145
  }
113
146
  }
147
+ /** The cell's own action: drill into the day; on phones (dots, no
148
+ * ondayclick) the whole cell opens its overflow instead of a tiny "+N". */
149
+ function activateCell(cell) {
150
+ if (cell.isDisabled) return;
151
+ if (ondayclick) ondayclick(cell.date);
152
+ else if (dotsMode && cell.overflow > 0) expandedMs = expandedMs === cell.ms ? null : cell.ms;
153
+ }
114
154
  function overflowClick(e, cell) {
115
155
  e.stopPropagation();
116
156
  if (ondayclick) ondayclick(cell.date);
@@ -143,6 +183,7 @@ const weeks = $derived.by(() => {
143
183
  const date = new Date(cellMs);
144
184
  const jsDay = date.getDay();
145
185
  const dayEvents = eventsForDay(cellMs);
186
+ const shown = dayEvents.length <= fitAll ? dayEvents.length : fitMore;
146
187
  row.push({
147
188
  ms: cellMs,
148
189
  date,
@@ -151,9 +192,9 @@ const weeks = $derived.by(() => {
151
192
  isToday: cellMs === todayMs,
152
193
  isWeekend: jsDay === 0 || jsDay === 6,
153
194
  isDisabled: disabledSet.has(cellMs),
154
- chips: dayEvents.slice(0, MAX_CHIPS),
195
+ chips: dayEvents.slice(0, shown),
155
196
  all: dayEvents,
156
- overflow: Math.max(0, dayEvents.length - MAX_CHIPS)
197
+ overflow: dayEvents.length - shown
157
198
  });
158
199
  }
159
200
  rows.push(row);
@@ -202,18 +243,25 @@ function chipTime(e) {
202
243
  aria-current={cell.isToday ? 'date' : undefined}
203
244
  data-ms={cell.ms}
204
245
  tabindex={isFocusable(cell) ? (cell.ms === rovingMs ? 0 : -1) : undefined}
205
- onclick={() => {
206
- if (!cell.isDisabled) ondayclick?.(cell.date);
207
- }}
246
+ onclick={() => activateCell(cell)}
208
247
  onfocusin={() => (focusMs = cell.ms)}
209
248
  onkeydown={(e) => cellKeydown(e, cell)}
210
249
  >
211
250
  <span class="mg-daynum" class:mg-daynum--today={cell.isToday}>
212
251
  {cell.dayNum}
213
252
  </span>
214
- <div class="mg-chips">
253
+ <!-- Phones: dots are a picture of the day; the cell (its label counts
254
+ the events) is the one tap and tab target. -->
255
+ <div class="mg-chips" aria-hidden={dotsMode || undefined}>
215
256
  {#each expandedMs === cell.ms ? cell.all : cell.chips as ev (ev.id + cell.ms)}
216
- {#if eventSnippet}
257
+ {#if dotsMode}
258
+ <span
259
+ class="mg-chip"
260
+ class:mg-chip--selected={ev.id === selectedEventId}
261
+ class:mg-chip--cancelled={ev.status === 'cancelled'}
262
+ style:--mg-chip-color={ev.color ?? (isAllDay(ev) ? 'var(--dt-accent)' : 'var(--dt-event, var(--dt-accent))')}
263
+ ><span class="mg-chip-dot"></span></span>
264
+ {:else if eventSnippet}
217
265
  <button
218
266
  type="button"
219
267
  class="mg-chip mg-chip--custom"
@@ -232,7 +280,7 @@ function chipTime(e) {
232
280
  class="mg-chip"
233
281
  class:mg-chip--selected={ev.id === selectedEventId}
234
282
  class:mg-chip--cancelled={ev.status === 'cancelled'}
235
- style:--mg-chip-color={ev.color ?? 'var(--dt-accent)'}
283
+ style:--mg-chip-color={ev.color ?? (isAllDay(ev) ? 'var(--dt-accent)' : 'var(--dt-event, var(--dt-accent))')}
236
284
  title={ev.title}
237
285
  aria-label="{ev.title}{chipTime(ev) ? `, ${chipTime(ev)}` : ''}"
238
286
  onclick={(e) => {
@@ -247,7 +295,11 @@ function chipTime(e) {
247
295
  </button>
248
296
  {/if}
249
297
  {/each}
250
- {#if cell.overflow > 0 && expandedMs !== cell.ms}
298
+ {#if dotsMode}
299
+ {#if cell.overflow > 0 && expandedMs !== cell.ms}
300
+ <span class="mg-more">+{cell.overflow}</span>
301
+ {/if}
302
+ {:else if cell.overflow > 0 && expandedMs !== cell.ms}
251
303
  <button
252
304
  type="button"
253
305
  class="mg-more"
@@ -255,7 +307,7 @@ function chipTime(e) {
255
307
  aria-label={L.nMore(cell.overflow)}
256
308
  onclick={(e) => overflowClick(e, cell)}
257
309
  >
258
- {dotsMode ? `+${cell.overflow}` : L.nMore(cell.overflow)}
310
+ {L.nMore(cell.overflow)}
259
311
  </button>
260
312
  {:else if expandedMs === cell.ms}
261
313
  <button
@@ -300,13 +352,11 @@ function chipTime(e) {
300
352
  }
301
353
  .mg-head-cell {
302
354
  padding: 6px 8px;
303
- font-family: var(--dt-sans, system-ui, sans-serif);
355
+ font: 500 12px/1.2 var(--dt-sans, system-ui, sans-serif);
304
356
  font-variant-numeric: tabular-nums;
305
- font-size: 11px;
306
- font-weight: 600;
307
357
  text-transform: uppercase;
308
- letter-spacing: 0.06em;
309
- color: var(--dt-text-3);
358
+ letter-spacing: 0.08em;
359
+ color: var(--dt-text-2, rgba(15, 15, 15, 0.66));
310
360
  }
311
361
 
312
362
  .mg-body {
@@ -340,14 +390,15 @@ function chipTime(e) {
340
390
  border-right: none;
341
391
  }
342
392
  .mg-cell--weekend {
343
- background: var(--dt-weekend-bg, rgba(0, 0, 0, 0.02));
393
+ background: var(--dt-weekend-bg, rgba(15, 15, 15, 0.02));
344
394
  }
345
- /* Dim only the day number for adjacent-month cells — their events stay legible */
395
+ /* Dim only the day number for adjacent-month cells — their events stay
396
+ legible. text-2, not text-3: a 22px numeral still needs 4.5:1. */
346
397
  .mg-cell--out {
347
398
  background: var(--dt-surface, transparent);
348
399
  }
349
400
  .mg-cell--out .mg-daynum {
350
- color: var(--dt-text-3);
401
+ color: var(--dt-text-2, rgba(15, 15, 15, 0.66));
351
402
  }
352
403
  .mg-cell--today {
353
404
  background: var(--dt-today-bg);
@@ -363,10 +414,10 @@ function chipTime(e) {
363
414
  cursor: pointer;
364
415
  }
365
416
  .mg-cell--clickable:hover {
366
- background: var(--dt-hover, rgba(0, 0, 0, 0.04));
417
+ background: var(--dt-hover, rgba(15, 15, 15, 0.04));
367
418
  }
368
419
  .mg-cell:focus-visible {
369
- outline: 2px solid var(--dt-accent, #2563eb);
420
+ outline: 2px solid var(--dt-accent, #1d4fd7);
370
421
  outline-offset: -2px;
371
422
  }
372
423
 
@@ -406,19 +457,17 @@ function chipTime(e) {
406
457
  background: none;
407
458
  padding: 2px 4px;
408
459
  border-radius: 0;
409
- font-family: var(--dt-sans);
410
- font-size: 12px;
411
- line-height: 1.3;
460
+ font: 400 12.5px/1.3 var(--dt-sans, system-ui, sans-serif);
412
461
  color: var(--dt-text);
413
462
  cursor: pointer;
414
463
  min-width: 0;
415
464
  text-align: left;
416
465
  }
417
466
  .mg-chip:hover {
418
- background: var(--dt-hover, rgba(0, 0, 0, 0.04));
467
+ background: var(--dt-hover, rgba(15, 15, 15, 0.04));
419
468
  }
420
469
  .mg-chip:focus-visible {
421
- box-shadow: 0 0 0 2px var(--dt-accent, #2563eb);
470
+ box-shadow: 0 0 0 2px var(--dt-accent, #1d4fd7);
422
471
  outline: none;
423
472
  }
424
473
  .mg-chip--selected {
@@ -442,9 +491,7 @@ function chipTime(e) {
442
491
  }
443
492
  .mg-chip-time {
444
493
  flex: none;
445
- font-family: var(--dt-sans, system-ui, sans-serif);
446
494
  font-variant-numeric: tabular-nums;
447
- font-size: 11px;
448
495
  color: var(--dt-text-2);
449
496
  }
450
497
  .mg-chip-title {
@@ -458,18 +505,17 @@ function chipTime(e) {
458
505
  background: none;
459
506
  padding: 1px 4px;
460
507
  border-radius: 0;
461
- font-family: var(--dt-sans, system-ui, sans-serif);
508
+ font: 400 12.5px/1.3 var(--dt-sans, system-ui, sans-serif);
462
509
  font-variant-numeric: tabular-nums;
463
- font-size: 11px;
464
510
  color: var(--dt-text-2);
465
511
  cursor: pointer;
466
512
  }
467
513
  .mg-more:hover {
468
- background: var(--dt-hover, rgba(0, 0, 0, 0.04));
514
+ background: var(--dt-hover, rgba(15, 15, 15, 0.04));
469
515
  color: var(--dt-text);
470
516
  }
471
517
  .mg-more:focus-visible {
472
- box-shadow: 0 0 0 2px var(--dt-accent, #2563eb);
518
+ box-shadow: 0 0 0 2px var(--dt-accent, #1d4fd7);
473
519
  outline: none;
474
520
  }
475
521
 
@@ -490,13 +536,20 @@ function chipTime(e) {
490
536
  /* ── Dots mode (mobile) ─────────────────────────────
491
537
  Cells are too narrow for text chips, so events render as colored
492
538
  dots in a wrapping row. The cell itself stays the tap target
493
- (day drill-down); dots keep their title/aria-label for a11y. */
539
+ (day drill-down) and its label counts the events; dots are aria-hidden. */
494
540
  .mg--dots .mg-chips {
495
541
  flex-direction: row;
496
542
  flex-wrap: wrap;
497
543
  align-items: center;
498
544
  gap: 3px;
499
545
  }
546
+ /* The cell is the target: at least a 44px tap, dots never take the hit. */
547
+ .mg--dots .mg-cell {
548
+ min-height: 44px;
549
+ }
550
+ .mg--dots .mg-chips {
551
+ pointer-events: none;
552
+ }
500
553
  .mg--dots .mg-chip {
501
554
  padding: 3px;
502
555
  min-height: 0;
@@ -505,14 +558,17 @@ function chipTime(e) {
505
558
  .mg--dots .mg-chip-time {
506
559
  display: none;
507
560
  }
561
+ /* Marks are circles, like every dot in the calendar */
508
562
  .mg--dots .mg-chip-dot {
509
- width: 8px;
510
- height: 8px;
563
+ align-self: center;
564
+ width: 7px;
565
+ height: 7px;
566
+ border-radius: 50%;
511
567
  }
512
568
  .mg--dots .mg-more {
513
569
  padding: 0 3px;
514
570
  min-height: 0;
515
571
  align-self: center;
516
- font-size: 10px;
572
+ font-size: 11px;
517
573
  }
518
574
  </style>