@tasknotes/calendar 0.1.7 → 0.1.10

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.
@@ -31,14 +31,16 @@ import {
31
31
 
32
32
  // src/dom/format.ts
33
33
  var Formatter = class {
34
- cache = /* @__PURE__ */ new Map();
35
- locale;
36
- hour12;
37
- constructor(locale, hour12) {
34
+ constructor(locale, hour12, formatTime) {
35
+ this.formatTime = formatTime;
38
36
  this.locale = locale;
39
37
  const probe = new Intl.DateTimeFormat(locale, { hour: "numeric" }).resolvedOptions();
40
38
  this.hour12 = hour12 ?? (probe.hour12 === true || probe.hourCycle === "h12" || probe.hourCycle === "h11");
41
39
  }
40
+ formatTime;
41
+ cache = /* @__PURE__ */ new Map();
42
+ locale;
43
+ hour12;
42
44
  fmt(key, opts) {
43
45
  let f = this.cache.get(key);
44
46
  if (!f) {
@@ -96,6 +98,7 @@ var Formatter = class {
96
98
  /** Wall-clock minutes to a label. `compact` drops ":00" in 12-hour formats ("9am"). */
97
99
  time(minute, compact = false) {
98
100
  const m = (Math.round(minute) % 1440 + 1440) % 1440;
101
+ if (this.formatTime) return this.formatTime(m);
99
102
  const date = new Date(Date.UTC(2e3, 0, 1, Math.floor(m / 60), m % 60));
100
103
  if (this.hour12) {
101
104
  const parts = this.fmt("t12", { hour: "numeric", minute: "2-digit", hour12: true }).formatToParts(date);
@@ -187,6 +190,7 @@ function defaultOptions() {
187
190
  snapDuration: void 0,
188
191
  scrollTime: "08:00",
189
192
  slotHeight: 24,
193
+ pinchZoom: true,
190
194
  expandRows: false,
191
195
  nowIndicator: true,
192
196
  selectMirror: true,
@@ -228,6 +232,7 @@ var LAYOUT_KEYS = /* @__PURE__ */ new Set([
228
232
  "locale",
229
233
  "firstDay",
230
234
  "hour12",
235
+ "formatTime",
231
236
  "weekNumbers",
232
237
  "weekends",
233
238
  "hiddenDays",
@@ -240,9 +245,11 @@ var LAYOUT_KEYS = /* @__PURE__ */ new Set([
240
245
  "slotDuration",
241
246
  "snapDuration",
242
247
  "slotHeight",
248
+ "pinchZoom",
243
249
  "expandRows",
244
250
  "nowIndicator",
245
251
  "slotEventOverlap",
252
+ "overlapStackThreshold",
246
253
  "eventMinHeight",
247
254
  "eventMaxStack",
248
255
  "dayMaxEvents",
@@ -359,8 +366,9 @@ var DayGridView = class {
359
366
  constructor(ctx) {
360
367
  this.ctx = ctx;
361
368
  const { doc, opts, days } = ctx;
362
- this.dotsMode = opts.monthDisplay === "dots";
369
+ this.dotsMode = opts.monthDisplay === "dots" || opts.monthDisplay === "chips";
363
370
  this.el = h(doc, "div", this.dotsMode ? "tnc-dg tnc-dg--dots" : "tnc-dg");
371
+ this.el.classList.toggle("tnc-dg--chips", opts.monthDisplay === "chips");
364
372
  this.scroller = h(doc, "div", "tnc-dg-scroll");
365
373
  this.grid = h(doc, "div", "tnc-dg-grid", { role: "grid", "aria-label": ctx.view.title });
366
374
  const perWeek = this.daysPerWeek();
@@ -390,14 +398,14 @@ var DayGridView = class {
390
398
  const cell = h(doc, "div", cls.join(" "), { role: "gridcell", "aria-label": ctx.fmt.longDate(d), "data-date": arg.dateStr });
391
399
  if (this.dotsMode) cell.tabIndex = 0;
392
400
  const num = ctx.fmt.dayNumber(d);
393
- const label = ymdOfDay(d).day === 1 ? `${ctx.fmt.monthShort(d)} ${num}` : num;
401
+ const label = !this.dotsMode && ymdOfDay(d).day === 1 ? `${ctx.fmt.monthShort(d)} ${num}` : num;
394
402
  cell.append(
395
403
  h(
396
404
  doc,
397
405
  "div",
398
406
  "tnc-dg-top",
399
407
  null,
400
- opts.navLinks ? ctx.navLink(d, label, "tnc-dg-num") : h(doc, "span", "tnc-dg-num", null, label)
408
+ opts.navLinks && !this.dotsMode ? ctx.navLink(d, label, "tnc-dg-num") : h(doc, "span", "tnc-dg-num", null, label)
401
409
  )
402
410
  );
403
411
  dates.append(cell);
@@ -411,20 +419,20 @@ var DayGridView = class {
411
419
  layer.append(content);
412
420
  row.append(dates, layer);
413
421
  this.grid.append(row);
414
- const dots = this.dotsMode ? cells.map((cell) => {
422
+ const dots = cells.map((cell) => {
415
423
  const box = h(doc, "div", "tnc-dg-dots", { "aria-hidden": "true" });
416
424
  if (opts.dayMaxEvents === false) box.classList.add("is-unlimited");
417
425
  const count = h(doc, "span", "tnc-dg-dots-more");
418
426
  cell.append(box);
419
427
  return { box, pool: new ItemPool(ctx, box), count };
420
- }) : null;
428
+ });
421
429
  this.weeks.push({ days: wdays, row, cells, layer, sel, more, pool: new ItemPool(ctx, content), dots });
422
430
  }
423
431
  this.scroller.append(this.grid);
424
432
  this.el.append(this.scroller);
425
433
  this.el.classList.toggle("tnc-dg--auto", opts.height === "auto");
426
434
  const RO = ctx.win.ResizeObserver;
427
- if (RO && (opts.dayMaxEvents === true || this.dotsMode && opts.dayMaxEvents !== false)) {
435
+ if (RO) {
428
436
  this.resizeObs = new RO(() => {
429
437
  if (this.sizeKey() !== this.lastHeights) this.render(null);
430
438
  });
@@ -442,6 +450,7 @@ var DayGridView = class {
442
450
  topPx = 24;
443
451
  lastHeights = "";
444
452
  dotsMode;
453
+ dragPreviews = [];
445
454
  daysPerWeek() {
446
455
  const { days } = this.ctx;
447
456
  const first = days[0];
@@ -455,16 +464,71 @@ var DayGridView = class {
455
464
  }
456
465
  render(_change) {
457
466
  const { ctx } = this;
467
+ this.clearDragPreviews();
468
+ const dragged = ctx.draggingId ? ctx.store.get(ctx.draggingId) : void 0;
469
+ const source = dragged ? this.elementsFor(dragged.id)[0] : void 0;
470
+ if (dragged && source && this.el.closest(".tnc-dragging-move")) {
471
+ const proposals = [dragged, ...[...ctx.store.all()].filter((item) => item.id !== dragged.id && item.item.preview)];
472
+ for (const proposal of proposals) {
473
+ const original = this.elementsFor(proposal.id)[0] ?? source;
474
+ const bounds = original.getBoundingClientRect();
475
+ const sourceWeek = original.closest(".tnc-dg-week");
476
+ const offset = bounds.top - sourceWeek.getBoundingClientRect().top;
477
+ for (const week of this.weeks) {
478
+ const start = week.days.findIndex((day) => day >= proposal.startDay && day < proposal.endDay);
479
+ if (start < 0) continue;
480
+ let end = start + 1;
481
+ while (!this.dotsMode && end < week.days.length && week.days[end] < proposal.endDay) end++;
482
+ const preview = ctx.createItemEl({ n: proposal, day: week.days[start], isStart: proposal.startDay >= week.days[start], isEnd: proposal.endDay <= week.days[end - 1] + 1, variant: ctx.meta.get(original).variant, mirror: true });
483
+ preview.classList.add("tnc-month-preview");
484
+ preview.inert = true;
485
+ setStyle(preview, {
486
+ left: `calc(${pct(start / week.days.length)} + 2px)`,
487
+ width: `calc(${pct((end - start) / week.days.length)} - 4px)`,
488
+ top: `${Math.max(this.topPx, Math.min(offset, week.row.clientHeight - bounds.height - 4))}px`,
489
+ height: `${bounds.height}px`
490
+ });
491
+ week.layer.append(preview);
492
+ this.dragPreviews.push(preview);
493
+ }
494
+ }
495
+ return;
496
+ }
497
+ this.dotsMode = ctx.opts.monthDisplay !== "full" || this.grid.clientWidth / this.daysPerWeek() < 72;
498
+ this.el.classList.toggle("tnc-dg--dots", this.dotsMode);
499
+ this.el.classList.toggle("tnc-dg--compact", this.dotsMode && ctx.opts.monthDisplay === "full");
500
+ this.el.classList.toggle("tnc-dg--chips", ctx.opts.monthDisplay === "chips" && this.weeks[0].dots[0].box.clientHeight >= 44);
458
501
  const cs = ctx.win.getComputedStyle(this.el);
459
502
  this.barPx = px(cs, "--tnc-bar-h", 20);
460
503
  this.topPx = this.weeks[0]?.cells[0]?.querySelector(".tnc-dg-top")?.offsetHeight || px(cs, "--tnc-dg-top-h", 24);
461
504
  this.morePx = this.measureMore(cs);
462
505
  this.lastHeights = this.sizeKey();
463
506
  for (const w of this.weeks) {
464
- if (w.dots) this.renderDots(w);
465
- else this.renderWeek(w);
507
+ for (const cell of w.cells) {
508
+ if (this.dotsMode) cell.tabIndex = 0;
509
+ else cell.removeAttribute("tabindex");
510
+ if (this.dotsMode && ctx.opts.monthDisplay === "full") cell.setAttribute("aria-haspopup", "dialog");
511
+ else cell.removeAttribute("aria-haspopup");
512
+ }
513
+ for (const cell of w.dots) cell.box.hidden = !this.dotsMode;
514
+ if (this.dotsMode) {
515
+ w.pool.begin();
516
+ w.pool.end();
517
+ w.more.replaceChildren();
518
+ this.renderDots(w);
519
+ } else {
520
+ for (const cell of w.dots) {
521
+ cell.pool.begin();
522
+ cell.pool.end();
523
+ }
524
+ this.renderWeek(w);
525
+ }
466
526
  }
467
527
  }
528
+ clearDragPreviews() {
529
+ for (const preview of this.dragPreviews) this.ctx.destroyItemEl(preview);
530
+ this.dragPreviews = [];
531
+ }
468
532
  /** Height of a "+N more" link as the host styles it (`--tnc-more-h`, or a taller min-height). */
469
533
  measureMore(cs) {
470
534
  const fallback = px(cs, "--tnc-more-h", this.barPx);
@@ -561,7 +625,8 @@ var DayGridView = class {
561
625
  w.more.append(link);
562
626
  }
563
627
  existing.delete(key);
564
- link.textContent = ctx.text.more(items.length);
628
+ link.setAttribute("aria-label", ctx.text.more(items.length));
629
+ link.textContent = w.row.clientWidth / n < 72 ? `+${new Intl.NumberFormat(ctx.opts.locale).format(items.length)}` : ctx.text.more(items.length);
565
630
  setStyle(link, {
566
631
  left: `calc(${pct(col / n)} + 2px)`,
567
632
  width: `calc(${pct(1 / n)} - 4px)`,
@@ -591,10 +656,11 @@ var DayGridView = class {
591
656
  const { dayMaxEvents } = ctx.opts;
592
657
  const first = w.dots[0].box;
593
658
  const cs = ctx.win.getComputedStyle(first);
594
- const size = px(cs, "--tnc-dot-size", 8);
659
+ const chips = this.el.classList.contains("tnc-dg--chips");
660
+ const size = chips ? px(cs, "--tnc-chip-h", 14) : px(cs, "--tnc-dot-size", 8);
595
661
  const gap = px(cs, "--tnc-dot-gap", 3);
596
662
  const step = size + gap;
597
- const perRow = Math.max(1, Math.floor((first.clientWidth + gap) / step));
663
+ const perRow = chips ? 1 : Math.max(1, Math.floor((first.clientWidth + gap) / step));
598
664
  const rows = Math.max(1, Math.floor((first.clientHeight + gap) / step));
599
665
  const fits = dayMaxEvents === false ? Infinity : perRow * rows;
600
666
  const cap = typeof dayMaxEvents === "number" && dayMaxEvents > 0 ? Math.min(dayMaxEvents, fits) : fits;
@@ -603,7 +669,7 @@ var DayGridView = class {
603
669
  w.days.forEach((day, i) => {
604
670
  const cell = w.dots[i];
605
671
  const items = this.itemsOn(day);
606
- const shown = items.length <= cap ? items.length : cap >= fits ? Math.max(0, cap - 2) : cap;
672
+ const shown = items.length <= cap ? items.length : cap >= fits ? Math.max(1, cap - (chips ? 1 : 2)) : Math.max(1, cap);
607
673
  cell.pool.begin();
608
674
  const els = items.slice(0, shown).map((n) => cell.pool.place(n.id, { n, day, isStart: n.startDay >= day, isEnd: n.endDay <= day + 1, variant: "indicator", mirror: false }));
609
675
  cell.pool.end();
@@ -654,6 +720,7 @@ var DayGridView = class {
654
720
  return this.weeks.flatMap((w) => [...w.pool.elementsFor(id), ...(w.dots ?? []).flatMap((c) => c.pool.elementsFor(id))]);
655
721
  }
656
722
  destroy() {
723
+ this.clearDragPreviews();
657
724
  this.resizeObs?.disconnect();
658
725
  for (const w of this.weeks) {
659
726
  w.pool.clear();
@@ -706,6 +773,7 @@ var ListView = class {
706
773
  frame = null;
707
774
  /** A picked day stays marked until the list is scrolled by hand. */
708
775
  pinned = false;
776
+ pinnedTop = 0;
709
777
  itemsOn(day) {
710
778
  const { store, opts } = this.ctx;
711
779
  const out = store.spanningIn(day, day + 1, true);
@@ -720,6 +788,7 @@ var ListView = class {
720
788
  const leadItems = [];
721
789
  const inLead = /* @__PURE__ */ new Set();
722
790
  const keep = /* @__PURE__ */ new Set();
791
+ let itemCount = 0;
723
792
  let prev = null;
724
793
  for (const day of days) {
725
794
  let items = this.itemsOn(day);
@@ -733,7 +802,7 @@ var ListView = class {
733
802
  return false;
734
803
  });
735
804
  }
736
- if (!items.length && !agenda) continue;
805
+ if (!items.length && !agenda && opts.listDayCount !== 1) continue;
737
806
  keep.add(day);
738
807
  let section = this.sections.get(day);
739
808
  let pool = this.pools.get(day);
@@ -752,6 +821,7 @@ var ListView = class {
752
821
  this.pools.set(day, pool);
753
822
  }
754
823
  section.classList.toggle("is-empty", !items.length);
824
+ itemCount += items.length;
755
825
  this.fill(pool, items, () => day);
756
826
  if (prev ? prev.nextSibling !== section : this.body.firstChild !== section) {
757
827
  if (prev) prev.after(section);
@@ -767,13 +837,14 @@ var ListView = class {
767
837
  this.sections.delete(day);
768
838
  }
769
839
  this.renderLead(leadItems);
770
- let empty = this.body.querySelector(":scope > .tnc-list-empty");
771
- if (!keep.size && !leadItems.length) {
840
+ let empty = this.body.querySelector(".tnc-list-empty");
841
+ if (!itemCount && !leadItems.length && (!agenda || opts.listEmptyContent)) {
772
842
  if (!empty) {
773
843
  empty = h(doc, "div", "tnc-list-empty", { role: "listitem" }, ctx.text.noItems);
774
844
  const content = ctx.opts.listEmptyContent?.();
775
845
  if (content) empty.replaceChildren(content);
776
- this.body.append(empty);
846
+ if (this.sections.size === 1) this.sections.values().next().value.append(empty);
847
+ else this.body.prepend(empty);
777
848
  }
778
849
  } else empty?.remove();
779
850
  this.ctx.strip?.draw();
@@ -831,9 +902,10 @@ var ListView = class {
831
902
  const section = day === this.ctx.days[0] && this.lead ? this.lead.section : this.sections.get(day);
832
903
  if (!section) return;
833
904
  this.ctx.strip?.measure();
834
- section.scrollIntoView({ block: "start" });
905
+ this.scroller.scrollTop += section.getBoundingClientRect().top - this.scroller.getBoundingClientRect().top;
835
906
  this.ctx.strip?.setFocus(day);
836
907
  this.pinned = true;
908
+ this.pinnedTop = this.scroller.scrollTop;
837
909
  }
838
910
  fold = () => {
839
911
  this.pinned = false;
@@ -841,6 +913,7 @@ var ListView = class {
841
913
  if (strip?.expanded) strip.setExpanded(false);
842
914
  };
843
915
  onScroll = () => {
916
+ if (this.pinned && Math.abs(this.scroller.scrollTop - this.pinnedTop) > 1) this.pinned = false;
844
917
  if (this.frame !== null) return;
845
918
  this.frame = this.ctx.win.requestAnimationFrame(() => {
846
919
  this.frame = null;
@@ -851,7 +924,7 @@ var ListView = class {
851
924
  followTop() {
852
925
  const strip = this.ctx.strip;
853
926
  if (!strip || this.pinned) return;
854
- const edge = strip.el.getBoundingClientRect().bottom + 1;
927
+ const edge = Math.max(strip.el.getBoundingClientRect().bottom, this.scroller.getBoundingClientRect().top) + 1;
855
928
  for (const day of this.ctx.days) {
856
929
  const section = this.sections.get(day);
857
930
  if (section && section.getBoundingClientRect().bottom > edge) {
@@ -899,10 +972,11 @@ var ListView = class {
899
972
  // src/dom/views/strip.ts
900
973
  var SWIPE_PX = 40;
901
974
  var DateStrip = class {
902
- constructor(ctx, pick, focus, onShown) {
975
+ constructor(ctx, pick, focus, onShown, onPage) {
903
976
  this.ctx = ctx;
904
977
  this.pick = pick;
905
978
  this.onShown = onShown;
979
+ this.onPage = onPage;
906
980
  const { doc, text } = ctx;
907
981
  this.focus = focus;
908
982
  this.shown = startOfWeek(focus, ctx.firstDay);
@@ -920,13 +994,16 @@ var DateStrip = class {
920
994
  head.append(this.title, this.todayBtn, prev, next, this.slot);
921
995
  this.dows = h(doc, "div", "tnc-strip-dows", { "aria-hidden": "true" });
922
996
  this.grid = h(doc, "div", "tnc-strip-grid", { role: "group" });
923
- const handle = h(doc, "button", "tnc-strip-handle", { type: "button", tabindex: "-1", "aria-hidden": "true" });
924
- handle.addEventListener("click", () => this.setExpanded(!this.expanded));
997
+ const handle = h(doc, "div", "tnc-strip-handle", { "aria-hidden": "true" });
925
998
  this.el.append(head, this.dows, this.grid, handle);
926
999
  this.grid.addEventListener("click", this.onClick);
927
1000
  this.grid.addEventListener("keydown", this.onKeyDown);
928
1001
  this.el.addEventListener("pointerdown", this.onPointerDown);
929
1002
  this.el.addEventListener("pointerup", this.onPointerUp);
1003
+ this.el.addEventListener("touchstart", this.ownTouch, { passive: true });
1004
+ this.el.addEventListener("touchmove", this.ownTouch, { passive: true });
1005
+ this.el.addEventListener("touchend", this.ownTouch, { passive: true });
1006
+ this.el.addEventListener("touchcancel", this.ownTouch, { passive: true });
930
1007
  this.el.addEventListener("pointercancel", this.onPointerCancel);
931
1008
  this.el.addEventListener("click", this.onClickCapture, true);
932
1009
  this.draw();
@@ -935,6 +1012,7 @@ var DateStrip = class {
935
1012
  ctx;
936
1013
  pick;
937
1014
  onShown;
1015
+ onPage;
938
1016
  el;
939
1017
  title;
940
1018
  dows;
@@ -993,6 +1071,7 @@ var DateStrip = class {
993
1071
  step(dir) {
994
1072
  this.shown = this.expanded ? addMonths(this.shown, dir) : this.shown + 7 * dir;
995
1073
  this.draw();
1074
+ this.onPage?.(this.expanded ? this.shown : this.focus + 7 * dir);
996
1075
  }
997
1076
  days() {
998
1077
  if (!this.expanded) return Array.from({ length: 7 }, (_, i) => this.shown + i);
@@ -1017,7 +1096,8 @@ var DateStrip = class {
1017
1096
  const tabDay = days.includes(this.focus) ? this.focus : days[0];
1018
1097
  const cells = days.map((d) => {
1019
1098
  const arg = ctx.dayArg(d);
1020
- const count = store.inRange(d, d + 1).filter((n) => n.item.display !== "background" && n.item.display !== "none").length;
1099
+ const items = store.inRange(d, d + 1).filter((n) => n.item.display !== "background" && n.item.display !== "none");
1100
+ const count = items.length;
1021
1101
  const first = ymdOfDay(d).day === 1 && d !== days[0] && !this.expanded;
1022
1102
  const label = fmt.longDate(d);
1023
1103
  const cell = h(
@@ -1041,7 +1121,15 @@ var DateStrip = class {
1041
1121
  }
1042
1122
  );
1043
1123
  if (first) cell.append(h(doc, "span", "tnc-strip-month", { "aria-hidden": "true" }, fmt.monthShort(d)));
1044
- cell.append(h(doc, "span", "tnc-strip-num", null, fmt.dayNumber(d)), h(doc, "span", "tnc-strip-dot", { "aria-hidden": "true" }));
1124
+ cell.append(h(doc, "span", "tnc-strip-num", null, fmt.dayNumber(d)));
1125
+ const dots = h(doc, "span", "tnc-strip-dots", { "aria-hidden": "true" });
1126
+ for (const item of items.slice(0, 3)) {
1127
+ const dot = h(doc, "span", "tnc-strip-dot");
1128
+ const color = item.item.color || item.item.borderColor || item.item.backgroundColor;
1129
+ if (color) dot.style.backgroundColor = color;
1130
+ dots.append(dot);
1131
+ }
1132
+ cell.append(dots);
1045
1133
  return cell;
1046
1134
  });
1047
1135
  this.grid.replaceChildren(...cells);
@@ -1081,6 +1169,9 @@ var DateStrip = class {
1081
1169
  if (ev.button !== 0 || this.ctx.draggingId) return;
1082
1170
  this.swipe = { id: ev.pointerId, x: ev.clientX, y: ev.clientY };
1083
1171
  };
1172
+ ownTouch = (event) => {
1173
+ event.stopPropagation();
1174
+ };
1084
1175
  onPointerUp = (ev) => {
1085
1176
  const s = this.swipe;
1086
1177
  this.swipe = null;
@@ -1120,6 +1211,7 @@ var TimeGridView = class {
1120
1211
  const showGrid = opts.timeGrid;
1121
1212
  this.el = h(doc, "div", "tnc-tg", { role: "table", "aria-label": ctx.view.title });
1122
1213
  this.el.classList.toggle("tnc-tg--no-grid", !showGrid);
1214
+ this.el.classList.toggle("tnc-tg--pinch", showGrid && opts.pinchZoom);
1123
1215
  this.scroller = h(doc, "div", "tnc-tg-scroll");
1124
1216
  this.dayNavigation = h(doc, "div", "tnc-tg-day-navigation", { role: "group", "aria-label": ctx.view.title });
1125
1217
  this.scroller.addEventListener("scroll", () => this.updateDayNavigation());
@@ -1269,6 +1361,11 @@ var TimeGridView = class {
1269
1361
  }
1270
1362
  if (this.nowGutter) gutter.append(this.nowGutter);
1271
1363
  }
1364
+ /** Slot height as drawn, which `expandRows` can stretch past the option. */
1365
+ get drawnSlotHeight() {
1366
+ return this.ctx.slot.duration * this.ppm;
1367
+ }
1368
+ /** Applies `slotHeight`. Items are placed in percentages, so they follow without a render. */
1272
1369
  applyScale() {
1273
1370
  const { opts, slot } = this.ctx;
1274
1371
  this.ppm = opts.slotHeight / slot.duration;
@@ -1449,15 +1546,17 @@ var TimeGridView = class {
1449
1546
  const { opts } = ctx;
1450
1547
  const readableWidth = Number.parseFloat(ctx.win.getComputedStyle(this.el).getPropertyValue("--tnc-event-min-width")) || 128;
1451
1548
  const laneWidth = c.col.clientWidth * (lane === "actual" && placements.some((placement) => placement.split) ? 0.22 : 1);
1452
- const collapse = placements.some((placement) => {
1549
+ const overlapThreshold = opts.overlapStackThreshold ?? 0;
1550
+ const collapsedClusters = new Set(placements.filter((placement) => {
1453
1551
  const columns = opts.eventMaxStack > 0 ? Math.min(placement.cols, opts.eventMaxStack) : placement.cols;
1454
- return columns > 1 && columns * readableWidth > laneWidth;
1455
- });
1456
- const withMore = lane === "plan" || collapse;
1457
- const maxStack = collapse ? 1 : opts.eventMaxStack;
1552
+ const denseOverlap = overlapThreshold > 1 && placement.cols >= overlapThreshold;
1553
+ return columns > 1 && (columns * readableWidth > laneWidth || denseOverlap);
1554
+ }).map((placement) => placement.cluster));
1458
1555
  const hiddenByCluster = /* @__PURE__ */ new Map();
1459
1556
  pool.begin();
1460
1557
  for (const p of placements) {
1558
+ const withMore = lane === "plan" || collapsedClusters.has(p.cluster);
1559
+ const maxStack = collapsedClusters.has(p.cluster) ? 1 : opts.eventMaxStack;
1461
1560
  const preview = p.item.item.preview || p.item.id === ctx.draggingId;
1462
1561
  if (!preview && withMore && maxStack > 0 && p.cols > maxStack && p.col >= maxStack - 1) {
1463
1562
  const rec = hiddenByCluster.get(p.cluster) ?? { count: 0, start: p.startMin, end: p.endMin, split: p.split, items: [] };
@@ -1470,7 +1569,7 @@ var TimeGridView = class {
1470
1569
  }
1471
1570
  const cols = !preview && withMore && maxStack > 0 ? Math.min(p.cols, maxStack) : p.cols;
1472
1571
  const span = Math.min(p.span, cols - p.col);
1473
- const left = p.col / cols;
1572
+ const left = p.item.id === ctx.draggingId ? 0 : p.col / cols;
1474
1573
  let width = span / cols;
1475
1574
  if (opts.slotEventOverlap && cols > 1 && p.col + span < cols) width = Math.min(1 - left, width * 1.7);
1476
1575
  const el = pool.place(p.item.id, this.meta(p, false));
@@ -1485,8 +1584,9 @@ var TimeGridView = class {
1485
1584
  });
1486
1585
  }
1487
1586
  pool.end();
1488
- if (!withMore) return;
1489
1587
  for (const [cluster, rec] of hiddenByCluster) {
1588
+ const collapse = collapsedClusters.has(cluster);
1589
+ const maxStack = collapse ? 1 : opts.eventMaxStack;
1490
1590
  const key = `${lane}-${cluster}`;
1491
1591
  let link = c.more.querySelector(`[data-cluster="${key}"]`);
1492
1592
  if (!link) {
@@ -1500,8 +1600,15 @@ var TimeGridView = class {
1500
1600
  link.classList.toggle("tnc-more--collision", collapse);
1501
1601
  link.textContent = `+${rec.count}`;
1502
1602
  if (collapse) {
1503
- link.textContent = ctx.text.appointments(rec.count);
1504
- link.setAttribute("aria-label", link.textContent);
1603
+ const items = placements.filter((placement) => placement.cluster === cluster).map((placement) => placement.item.item.title);
1604
+ const count = ctx.text.appointments(rec.count);
1605
+ const interval = `${ctx.fmt.time(rec.start)} \u2013 ${ctx.fmt.time(rec.end)}`;
1606
+ link.replaceChildren(
1607
+ h(ctx.doc, "span", "tnc-collision-count", null, String(rec.count)),
1608
+ h(ctx.doc, "span", "tnc-collision-range", null, interval),
1609
+ h(ctx.doc, "span", "tnc-collision-title", { "aria-hidden": "true" }, items.join(" \xB7 "))
1610
+ );
1611
+ link.setAttribute("aria-label", `${count}, ${interval}: ${items.join(", ")}`);
1505
1612
  link.title = link.getAttribute("aria-label");
1506
1613
  } else {
1507
1614
  link.removeAttribute("aria-label");
@@ -1510,6 +1617,7 @@ var TimeGridView = class {
1510
1617
  const cols = maxStack;
1511
1618
  setStyle(link, {
1512
1619
  top: top(rec.start),
1620
+ height: collapse ? pct((rec.end - rec.start) / this.minutes) : "",
1513
1621
  ...inLane(lane, rec.split, (cols - 1) / cols, 1 / cols)
1514
1622
  });
1515
1623
  }
@@ -1576,12 +1684,10 @@ var TimeGridView = class {
1576
1684
  const r2 = cells[i2].getBoundingClientRect();
1577
1685
  if (x >= r2.left && x < r2.right) return i2;
1578
1686
  }
1579
- const firstR = cells[0]?.getBoundingClientRect();
1580
- const lastR = cells[cells.length - 1]?.getBoundingClientRect();
1581
- if (firstR && x < firstR.left) return 0;
1582
- if (lastR && x >= lastR.right) return cells.length - 1;
1583
1687
  return -1;
1584
1688
  };
1689
+ const bounds = this.scroller.getBoundingClientRect();
1690
+ if (x < bounds.left || x >= bounds.right || y < bounds.top || y >= bounds.bottom) return null;
1585
1691
  if (this.allDayRow) {
1586
1692
  const r2 = this.allDayRow.getBoundingClientRect();
1587
1693
  if (y >= r2.top && y < r2.bottom || !this.body && y >= r2.top) {
@@ -1646,7 +1752,9 @@ var TimeGridView = class {
1646
1752
  scrollToTime(minute) {
1647
1753
  if (!this.body) return;
1648
1754
  const y = (minute - this.ctx.slot.min) * this.ppm;
1649
- this.scroller.scrollTop = Math.max(0, y);
1755
+ const label = this.gutter?.querySelector(".tnc-slot-label");
1756
+ const inset = label ? Math.ceil(label.getBoundingClientRect().height / 2) + 2 : 0;
1757
+ this.scroller.scrollTop = Math.max(0, y - inset);
1650
1758
  }
1651
1759
  getScroll() {
1652
1760
  return { top: this.scroller.scrollTop, left: this.scroller.scrollLeft };
@@ -2304,6 +2412,14 @@ ${this.ctx.text.more(items.length - 8)}` : ""}` : label;
2304
2412
  setScroll(s) {
2305
2413
  this.scroller.scrollTop = s.top;
2306
2414
  }
2415
+ revealDay(day) {
2416
+ const month = this.cells.find((cell) => cell.day === day)?.el.closest(".tnc-ym");
2417
+ if (!month) return;
2418
+ const bounds = month.getBoundingClientRect();
2419
+ const viewport = this.scroller.getBoundingClientRect();
2420
+ if (bounds.top < viewport.top) this.scroller.scrollTop += bounds.top - viewport.top;
2421
+ else if (bounds.bottom > viewport.bottom) this.scroller.scrollTop += bounds.top - viewport.top;
2422
+ }
2307
2423
  elementsFor(_id) {
2308
2424
  return [];
2309
2425
  }
@@ -2316,6 +2432,8 @@ ${this.ctx.text.more(items.length - 8)}` : ""}` : label;
2316
2432
  var EXTERNAL_ID = "__tnc_external__";
2317
2433
  var SVG_NS = "http://www.w3.org/2000/svg";
2318
2434
  var MAX_AGENDA_DAYS = 366;
2435
+ var PINCH_MIN_SLOT = 16;
2436
+ var PINCH_MAX_SLOT = 96;
2319
2437
  function sameOption(a, b, depth = 0) {
2320
2438
  if (a === b) return true;
2321
2439
  if (typeof a === "function" && typeof b === "function") return true;
@@ -2343,7 +2461,7 @@ var Calendar = class {
2343
2461
  this.win = this.doc.defaultView ?? globalThis;
2344
2462
  this.opts = { ...defaultOptions(), ...options };
2345
2463
  this.zone = getTimeZone(this.opts.timeZone);
2346
- this.fmt = new Formatter(this.opts.locale, this.opts.hour12);
2464
+ this.fmt = new Formatter(this.opts.locale, this.opts.hour12, this.opts.formatTime);
2347
2465
  this.text = this.resolveText();
2348
2466
  this.order = parseOrder(this.opts.eventOrder);
2349
2467
  this.store = new ItemStore(this.zone, {
@@ -2383,10 +2501,18 @@ var Calendar = class {
2383
2501
  conflicts = /* @__PURE__ */ new Map();
2384
2502
  slot = { min: 0, max: MINUTES_PER_DAY, duration: 30, snap: 30, scroll: 480 };
2385
2503
  draggingId = null;
2504
+ listDragPreview = null;
2505
+ dragSources = [];
2506
+ dragWidth = null;
2507
+ dragScrollFrame = null;
2508
+ dragScrollOrigins = /* @__PURE__ */ new Map();
2509
+ dragScrollEdges = /* @__PURE__ */ new Map();
2510
+ itemWriteStates = /* @__PURE__ */ new Map();
2386
2511
  dragRow = null;
2387
2512
  strip = null;
2388
2513
  stripSize = null;
2389
2514
  rebuilding = false;
2515
+ pinch = null;
2390
2516
  toolbarEl;
2391
2517
  viewHost;
2392
2518
  current = null;
@@ -2396,6 +2522,7 @@ var Calendar = class {
2396
2522
  session = null;
2397
2523
  selection = null;
2398
2524
  suppressClick = false;
2525
+ cancelledPointerId = null;
2399
2526
  selectedIds = /* @__PURE__ */ new Set();
2400
2527
  popover = null;
2401
2528
  timer = null;
@@ -2413,6 +2540,7 @@ var Calendar = class {
2413
2540
  this.positionMore();
2414
2541
  });
2415
2542
  this.resizeObs.observe(this.root);
2543
+ this.resizeObs.observe(this.toolbarEl);
2416
2544
  }
2417
2545
  }
2418
2546
  onAdoptCheck = () => {
@@ -2493,7 +2621,7 @@ var Calendar = class {
2493
2621
  defaultAllDayDays: this.opts.defaultAllDayDays
2494
2622
  });
2495
2623
  }
2496
- if (has("locale") || has("hour12")) this.fmt = new Formatter(this.opts.locale, this.opts.hour12);
2624
+ if (has("locale") || has("hour12") || has("formatTime")) this.fmt = new Formatter(this.opts.locale, this.opts.hour12, this.opts.formatTime);
2497
2625
  if (has("text") || has("locale")) this.text = this.resolveText();
2498
2626
  if (has("eventOrder")) this.order = parseOrder(this.opts.eventOrder);
2499
2627
  if (has("date")) this.anchor = this.toDay(this.opts.date) ?? this.anchor;
@@ -2539,7 +2667,12 @@ var Calendar = class {
2539
2667
  gotoDate(date) {
2540
2668
  const day = this.toDay(date);
2541
2669
  if (day === null) return;
2670
+ const sameMonth = this.opts.view === "month" && startOfMonth(day) === startOfMonth(this.anchor);
2542
2671
  this.anchor = day;
2672
+ if (sameMonth) {
2673
+ this.view = { ...this.view, anchor: day };
2674
+ return;
2675
+ }
2543
2676
  this.rebuild(true);
2544
2677
  }
2545
2678
  prev() {
@@ -2685,7 +2818,8 @@ var Calendar = class {
2685
2818
  out.push(...classList(this.opts.dayClassNames?.(this.dayArg(day))));
2686
2819
  return out;
2687
2820
  }
2688
- isEditable(n, what) {
2821
+ isEditable(n, what, presentation = false) {
2822
+ if (!presentation && this.itemWriteStates.get(n.id) === "pending") return false;
2689
2823
  if (n.id === EXTERNAL_ID || n.item.preview || this.opts.view === "year") return false;
2690
2824
  if (this.opts.view === "list" && (!this.opts.listAgenda || what === "duration")) return false;
2691
2825
  const it = n.item;
@@ -2725,13 +2859,17 @@ var Calendar = class {
2725
2859
  if (preview || n.id === EXTERNAL_ID) el.classList.add("is-mirror");
2726
2860
  if (this.conflicts.has(n.id)) el.classList.add("is-conflict");
2727
2861
  const indicator = variant === "indicator";
2728
- if (!preview && !indicator && (this.isEditable(n, "start") || this.isEditable(n, "duration"))) el.classList.add("is-draggable");
2729
- if (this.draggingId === n.id) el.classList.add("is-dragging");
2862
+ if (!preview && !indicator && (this.isEditable(n, "start", true) || this.isEditable(n, "duration", true))) el.classList.add("is-draggable");
2863
+ if (this.draggingId === n.id) {
2864
+ el.classList.add("is-dragging");
2865
+ if (variant === "timed" && this.dragWidth !== null) el.style.setProperty("--tnc-drag-width", `${this.dragWidth}px`);
2866
+ }
2730
2867
  if (this.selectedIds.has(n.id) && !preview) el.classList.add("is-selected");
2731
2868
  el.classList.add(...classList(it.className));
2732
2869
  const arg = this.itemArg(el, meta);
2733
2870
  el.classList.add(...classList(this.opts.itemClassNames?.(arg)));
2734
2871
  el.dataset.id = n.id;
2872
+ this.applyItemWriteState(el, n.id);
2735
2873
  if (it.kind) el.dataset.kind = it.kind;
2736
2874
  const color = it.color ?? it.borderColor;
2737
2875
  if (color) el.style.setProperty("--tnc-c", color);
@@ -2748,6 +2886,7 @@ var Calendar = class {
2748
2886
  const custom = variant === "list" ? this.opts.listItemContent?.(arg) : null;
2749
2887
  const content = indicator ? null : custom ?? this.opts.itemContent?.(arg);
2750
2888
  if (indicator) {
2889
+ if (this.opts.monthDisplay === "chips") el.append(h(this.doc, "span", "tnc-item-title", null, it.title));
2751
2890
  } else if (content != null) {
2752
2891
  const wrap = h(this.doc, "div", "tnc-item-main");
2753
2892
  wrap.append(content);
@@ -2760,6 +2899,7 @@ var Calendar = class {
2760
2899
  return el;
2761
2900
  }
2762
2901
  refreshItemEl(el, meta) {
2902
+ this.applyItemWriteState(el, meta.n.id);
2763
2903
  const time = el.querySelector(":scope .tnc-item-time");
2764
2904
  if (time) this.fillTime(time, meta.n, meta.variant, meta.day);
2765
2905
  el.classList.toggle("is-dragging", this.draggingId === meta.n.id);
@@ -2942,6 +3082,7 @@ var Calendar = class {
2942
3082
  this.current.scrollToTime(this.slot.scroll);
2943
3083
  this.current.revealDay(this.anchor);
2944
3084
  } else if (this.current instanceof TimelineView) this.current.scrollToDay(this.anchor);
3085
+ else if (this.current instanceof YearView) this.current.revealDay(this.anchor);
2945
3086
  if (rangeChanged) this.opts.onRangeChange?.(this.view);
2946
3087
  }
2947
3088
  /** Sets the visible days and view info from the anchor. */
@@ -2967,9 +3108,11 @@ var Calendar = class {
2967
3108
  }
2968
3109
  /** The range, run on to the end of the strip's days so their dots are right. */
2969
3110
  dataRangeFor(range) {
2970
- const end = this.strip ? Math.max(range.endDay, this.strip.window().end) : range.endDay;
2971
- if (end === range.endDay) return range;
2972
- return { ...range, endDay: end, end: new Date(toInstant(end, 0, this.zone)) };
3111
+ const window = this.strip?.window();
3112
+ const start = window ? Math.min(range.startDay, window.start) : range.startDay;
3113
+ const end = window ? Math.max(range.endDay, window.end) : range.endDay;
3114
+ if (start === range.startDay && end === range.endDay) return range;
3115
+ return { ...range, startDay: start, start: new Date(toInstant(start, 0, this.zone)), endDay: end, end: new Date(toInstant(end, 0, this.zone)) };
2973
3116
  }
2974
3117
  /** Adds, updates or removes the date strip for the current view. */
2975
3118
  syncStrip(navigated) {
@@ -2988,7 +3131,9 @@ var Calendar = class {
2988
3131
  this.root.classList.add("tnc--has-strip");
2989
3132
  const { startDay, endDay } = this.view.range;
2990
3133
  if (!this.strip) {
2991
- const strip = new DateStrip(this, (day, ev) => this.pickStripDay(day, ev), startDay, () => this.stripShown());
3134
+ const strip = new DateStrip(this, (day, ev) => this.pickStripDay(day, ev), startDay, () => this.stripShown(), (day) => {
3135
+ if (this.agendaList()) this.showListDay(day);
3136
+ });
2992
3137
  this.strip = strip;
2993
3138
  this.root.insertBefore(strip.el, this.viewHost);
2994
3139
  const RO = this.win.ResizeObserver;
@@ -3016,20 +3161,24 @@ var Calendar = class {
3016
3161
  stripShown() {
3017
3162
  if (!this.strip || this.rebuilding || !this.view) return;
3018
3163
  const next = this.dataRangeFor(this.view.range);
3019
- if (next.endDay === this.view.dataRange.endDay) return;
3164
+ if (next.startDay === this.view.dataRange.startDay && next.endDay === this.view.dataRange.endDay) return;
3020
3165
  this.view = { ...this.view, dataRange: next };
3021
3166
  this.opts.onRangeChange?.(this.view);
3022
3167
  }
3023
3168
  /** What is under a point, the date strip included. */
3024
3169
  hitAt(x, y) {
3170
+ const target = this.doc.elementFromPoint(x, y);
3171
+ if (!target || !this.root.contains(target)) {
3172
+ this.strip?.markTarget(null);
3173
+ return null;
3174
+ }
3025
3175
  const strip = this.strip;
3026
3176
  if (strip) {
3027
- const el = this.doc.elementFromPoint(x, y);
3028
- const day = el ? strip.dayAt(el) : null;
3177
+ const day = strip.dayAt(target);
3029
3178
  strip.markTarget(day !== null && this.draggingId ? day : null);
3030
3179
  if (day !== null) return { kind: "day", day };
3031
3180
  }
3032
- return this.current?.hit(x, y) ?? null;
3181
+ return this.current?.el.contains(target) ? this.current.hit(x, y) : null;
3033
3182
  }
3034
3183
  agendaList() {
3035
3184
  return this.opts.view === "list" && this.opts.listAgenda && this.current instanceof ListView ? this.current : null;
@@ -3143,11 +3292,13 @@ var Calendar = class {
3143
3292
  buttons.style.display = "flex";
3144
3293
  select.style.display = "none";
3145
3294
  const startWidth = Array.from(start.children).reduce((width, child) => width + child.scrollWidth, 0);
3146
- const compact = mode === "select" || mode === "auto" && startWidth + buttons.scrollWidth + 32 > this.root.clientWidth;
3295
+ const availableWidth = Math.min(this.root.clientWidth, this.toolbarEl.clientWidth);
3296
+ const compact = mode === "select" || mode === "auto" && startWidth + buttons.scrollWidth + 32 > availableWidth;
3147
3297
  buttons.hidden = compact;
3148
3298
  select.hidden = !compact;
3149
3299
  buttons.style.display = compact ? "none" : "flex";
3150
3300
  select.style.display = compact ? "" : "none";
3301
+ select.parentElement?.classList.toggle("tnc-toolbar-end--compact", compact);
3151
3302
  }
3152
3303
  applyChange(change) {
3153
3304
  if (!change.added.length && !change.updated.length && !change.removed.length && !change.days.size && !change.spans) return;
@@ -3212,7 +3363,7 @@ var Calendar = class {
3212
3363
  const s = wallTime(n.startMs, this.zone);
3213
3364
  const start = this.fmt.time(s.minute, true);
3214
3365
  if (variant === "dot" || variant === "bar" || variant === "indicator") return { start: s.day === day ? start : "", end: "" };
3215
- if (n.startMs === n.endMs) return { start, end: "" };
3366
+ if (n.startMs === n.endMs || n.item.timeDisplay === "start") return { start, end: "" };
3216
3367
  return { start, end: this.fmt.time(wallTime(n.endMs, this.zone).minute, true) };
3217
3368
  }
3218
3369
  fillTime(el, n, variant, day) {
@@ -3228,7 +3379,8 @@ var Calendar = class {
3228
3379
  }
3229
3380
  const s = wallTime(n.startMs, this.zone);
3230
3381
  const e = wallTime(n.endMs, this.zone);
3231
- return `${n.item.title}, ${this.fmt.longDate(s.day)} ${this.fmt.time(s.minute)} \u2013 ${this.fmt.time(e.minute)}`;
3382
+ const start = `${n.item.title}, ${this.fmt.longDate(s.day)} ${this.fmt.time(s.minute)}`;
3383
+ return n.item.timeDisplay === "start" || n.startMs === n.endMs ? start : `${start} \u2013 ${this.fmt.time(e.minute)}`;
3232
3384
  }
3233
3385
  defaultContent(meta) {
3234
3386
  const { n, variant, day } = meta;
@@ -3256,7 +3408,7 @@ var Calendar = class {
3256
3408
  addHandles(el, meta) {
3257
3409
  const { n, variant } = meta;
3258
3410
  const ports = variant === "timeline" && !meta.mirror && n.id !== EXTERNAL_ID && !!this.opts.timeline?.onLink;
3259
- if (variant === "dot" || !this.isEditable(n, "duration")) {
3411
+ if (variant === "dot" || !this.isEditable(n, "duration", true)) {
3260
3412
  if (ports) this.addPorts(el, n);
3261
3413
  return;
3262
3414
  }
@@ -3345,7 +3497,10 @@ var Calendar = class {
3345
3497
  r.addEventListener("dragover", this.onNativeDragOver);
3346
3498
  r.addEventListener("dragleave", this.onNativeDragLeave);
3347
3499
  r.addEventListener("drop", this.onNativeDrop);
3500
+ r.addEventListener("touchstart", this.onTouchStart, { passive: true });
3348
3501
  r.addEventListener("touchmove", this.onTouchMove, { passive: false });
3502
+ r.addEventListener("touchend", this.onTouchEnd);
3503
+ r.addEventListener("touchcancel", this.onTouchEnd);
3349
3504
  r.addEventListener("pointerover", this.onAdoptCheck);
3350
3505
  r.addEventListener("focusin", this.onAdoptCheck);
3351
3506
  this.doc.addEventListener("pointerdown", this.onDocPointerDown, true);
@@ -3364,7 +3519,10 @@ var Calendar = class {
3364
3519
  r.removeEventListener("dragover", this.onNativeDragOver);
3365
3520
  r.removeEventListener("dragleave", this.onNativeDragLeave);
3366
3521
  r.removeEventListener("drop", this.onNativeDrop);
3522
+ r.removeEventListener("touchstart", this.onTouchStart);
3367
3523
  r.removeEventListener("touchmove", this.onTouchMove);
3524
+ r.removeEventListener("touchend", this.onTouchEnd);
3525
+ r.removeEventListener("touchcancel", this.onTouchEnd);
3368
3526
  r.removeEventListener("pointerover", this.onAdoptCheck);
3369
3527
  r.removeEventListener("focusin", this.onAdoptCheck);
3370
3528
  this.doc.removeEventListener("pointerdown", this.onDocPointerDown, true);
@@ -3384,6 +3542,10 @@ var Calendar = class {
3384
3542
  return el && this.root.contains(el) && this.meta.has(el) && !this.meta.get(el)?.n.item.preview ? el : null;
3385
3543
  }
3386
3544
  onDocPointerDown = (ev) => {
3545
+ if (this.cancelledPointerId !== null) {
3546
+ this.cancelledPointerId = null;
3547
+ this.listenDoc(false);
3548
+ }
3387
3549
  const t = ev.target;
3388
3550
  if (this.popover && !this.popover.el.contains(t) && !this.popover.anchor.contains(t)) this.closeMore();
3389
3551
  if (this.selection && !this.root.contains(t)) this.unselect();
@@ -3391,6 +3553,7 @@ var Calendar = class {
3391
3553
  onPointerDown = (ev) => {
3392
3554
  if (ev.button !== 0 || this.session) return;
3393
3555
  const target = ev.target;
3556
+ if (target.closest(".tnc-dg--compact .tnc-dg-day")) return;
3394
3557
  if (target.closest("button, a, input, select, textarea, .tnc-toolbar, .tnc-port, [data-tnc-no-drag]")) {
3395
3558
  if (!target.closest(".tnc-item")) return;
3396
3559
  if (target.closest(".tnc-port, [data-tnc-no-drag], input, select, textarea")) return;
@@ -3423,7 +3586,7 @@ var Calendar = class {
3423
3586
  y0: ev.clientY,
3424
3587
  active: false,
3425
3588
  origin,
3426
- grab: overflowOrigin && this.current instanceof TimeGridView && !origin?.allDay ? { kind: "time", day: overflowOrigin.day, minute: overflowOrigin.minute } : this.current?.hit(ev.clientX, ev.clientY) ?? null,
3589
+ grab: overflowOrigin ? this.current instanceof TimeGridView && !origin?.allDay ? { kind: "time", day: overflowOrigin.day, minute: overflowOrigin.minute } : { kind: "day", day: this.meta.get(itemEl).day } : this.current?.hit(ev.clientX, ev.clientY) ?? null,
3427
3590
  proposal: null,
3428
3591
  denied: false,
3429
3592
  timer: null,
@@ -3448,20 +3611,159 @@ var Calendar = class {
3448
3611
  };
3449
3612
  activate(s) {
3450
3613
  s.active = true;
3614
+ if (s.lastEvent) this.leaveHover(s.lastEvent);
3451
3615
  if (s.timer !== null) this.win.clearTimeout(s.timer);
3452
3616
  s.timer = null;
3453
3617
  if (s.origin) {
3454
3618
  this.draggingId = s.origin.id;
3455
- for (const el of this.elementsFor(s.origin.id)) el.classList.add("is-dragging");
3619
+ for (const el of this.elementsFor(s.origin.id)) {
3620
+ if (this.meta.get(el)?.variant === "timed" && s.mode === "move") {
3621
+ this.dragWidth = el.getBoundingClientRect().width;
3622
+ el.style.setProperty("--tnc-drag-width", `${this.dragWidth}px`);
3623
+ }
3624
+ if (this.opts.view !== "list" && !(this.current instanceof DayGridView) && s.mode === "move") {
3625
+ const source = el.cloneNode(true);
3626
+ source.classList.remove("is-selected");
3627
+ source.classList.add("tnc-drag-source");
3628
+ source.setAttribute("aria-hidden", "true");
3629
+ source.inert = true;
3630
+ for (const node of [source, ...source.querySelectorAll("[id], [data-id], [data-task-path]")]) {
3631
+ node.removeAttribute("id");
3632
+ node.removeAttribute("data-id");
3633
+ node.removeAttribute("data-task-path");
3634
+ }
3635
+ el.before(source);
3636
+ this.dragSources.push(source);
3637
+ }
3638
+ if (this.opts.view === "list" && !this.listDragPreview && s.mode === "move") {
3639
+ const bounds = el.getBoundingClientRect();
3640
+ const preview = el.cloneNode(true);
3641
+ preview.classList.add("tnc-drag-preview", "is-dragging");
3642
+ preview.removeAttribute("data-id");
3643
+ preview.removeAttribute("data-task-path");
3644
+ preview.setAttribute("aria-hidden", "true");
3645
+ preview.inert = true;
3646
+ preview.style.width = `${bounds.width}px`;
3647
+ this.root.append(preview);
3648
+ this.listDragPreview = { element: preview, offsetX: s.x0 - bounds.left, offsetY: s.y0 - bounds.top, width: bounds.width, height: bounds.height };
3649
+ }
3650
+ el.classList.add("is-dragging");
3651
+ }
3456
3652
  }
3457
3653
  this.closeMore();
3458
3654
  if (s.mode !== "select") this.unselectSilently();
3459
3655
  this.root.classList.add("tnc-dragging", `tnc-dragging-${s.mode}`);
3460
3656
  if (s.lastEvent) this.track(s, s.lastEvent);
3657
+ this.startDragScroll();
3658
+ }
3659
+ startDragScroll() {
3660
+ if (this.dragScrollFrame !== null) return;
3661
+ const tick = () => {
3662
+ this.dragScrollFrame = null;
3663
+ const session = this.session;
3664
+ const pointer = session?.lastEvent;
3665
+ if (!session?.active || !pointer) return;
3666
+ const origin = session.origin ? this.elementsFor(session.origin.id)[0] : this.viewHost;
3667
+ const underPointer = this.doc.elementFromPoint(pointer.clientX, pointer.clientY);
3668
+ let moved = false;
3669
+ if (origin && underPointer && this.root.contains(underPointer)) {
3670
+ for (const axis of ["x", "y"]) {
3671
+ const scroller = scrollerOf(origin, axis);
3672
+ if (!scroller) continue;
3673
+ const viewport = scroller === this.doc.scrollingElement ? { left: 0, top: 0, right: this.win.innerWidth, bottom: this.win.innerHeight } : scroller.getBoundingClientRect();
3674
+ const coordinate = axis === "x" ? pointer.clientX : pointer.clientY;
3675
+ const lower = Math.max(0, axis === "x" ? viewport.left : viewport.top);
3676
+ const upper = Math.min(axis === "x" ? this.win.innerWidth : this.win.innerHeight, axis === "x" ? viewport.right : viewport.bottom);
3677
+ if (coordinate < lower || coordinate > upper) {
3678
+ this.dragScrollEdges.delete(axis);
3679
+ continue;
3680
+ }
3681
+ const delta = coordinate < lower + 32 ? -Math.min(12, (lower + 32 - coordinate) / 2) : coordinate > upper - 32 ? Math.min(12, (coordinate - upper + 32) / 2) : 0;
3682
+ if (!delta) {
3683
+ this.dragScrollEdges.delete(axis);
3684
+ continue;
3685
+ }
3686
+ const edge = this.dragScrollEdges.get(axis);
3687
+ const direction = Math.sign(delta);
3688
+ const now = this.win.performance.now();
3689
+ if (!edge || edge.scroller !== scroller || edge.direction !== direction) {
3690
+ this.dragScrollEdges.set(axis, { scroller, direction, since: now });
3691
+ continue;
3692
+ }
3693
+ if (now - edge.since < 180) continue;
3694
+ if (!this.dragScrollOrigins.has(scroller)) this.dragScrollOrigins.set(scroller, { top: scroller.scrollTop, left: scroller.scrollLeft });
3695
+ const before = axis === "x" ? scroller.scrollLeft : scroller.scrollTop;
3696
+ if (axis === "x") scroller.scrollLeft += delta;
3697
+ else scroller.scrollTop += delta;
3698
+ moved ||= before !== (axis === "x" ? scroller.scrollLeft : scroller.scrollTop);
3699
+ }
3700
+ } else this.dragScrollEdges.clear();
3701
+ if (moved) this.track(session, pointer);
3702
+ this.dragScrollFrame = this.win.requestAnimationFrame(tick);
3703
+ };
3704
+ this.dragScrollFrame = this.win.requestAnimationFrame(tick);
3461
3705
  }
3706
+ onTouchStart = (ev) => {
3707
+ const first = ev.touches[0];
3708
+ const second = ev.touches[1];
3709
+ if (!first || !second) {
3710
+ this.endPinch();
3711
+ return;
3712
+ }
3713
+ const view = this.current;
3714
+ if (this.session?.active || !this.opts.pinchZoom || !(view instanceof TimeGridView)) return;
3715
+ const body = first.target.closest(".tnc-tg-body");
3716
+ if (!body?.contains(second.target)) return;
3717
+ this.endSession(null, true);
3718
+ if (this.pinch) return;
3719
+ const { top, height } = body.getBoundingClientRect();
3720
+ const y = (first.clientY + second.clientY) / 2;
3721
+ this.pinch = {
3722
+ body,
3723
+ distance: Math.max(1, Math.hypot(first.clientX - second.clientX, first.clientY - second.clientY)),
3724
+ y,
3725
+ anchor: (y - top) / height,
3726
+ drawn: view.drawnSlotHeight,
3727
+ option: this.opts.slotHeight
3728
+ };
3729
+ };
3462
3730
  onTouchMove = (ev) => {
3463
3731
  if (this.session?.active) ev.preventDefault();
3732
+ const p = this.pinch;
3733
+ const first = ev.touches[0];
3734
+ const second = ev.touches[1];
3735
+ if (!p || !first || !second) return;
3736
+ if (!ev.cancelable) {
3737
+ this.endPinch();
3738
+ return;
3739
+ }
3740
+ ev.preventDefault();
3741
+ const height = p.drawn * Math.hypot(first.clientX - second.clientX, first.clientY - second.clientY) / p.distance;
3742
+ p.y = (first.clientY + second.clientY) / 2;
3743
+ this.pinchTo(p, Math.min(Math.max(p.drawn, PINCH_MAX_SLOT), Math.max(Math.min(p.drawn, PINCH_MIN_SLOT), height)));
3744
+ };
3745
+ onTouchEnd = (ev) => {
3746
+ if (ev.touches.length < 2) this.endPinch();
3464
3747
  };
3748
+ /** Rescales the grid, keeping the anchored time under the fingers. */
3749
+ pinchTo(p, slotHeight) {
3750
+ this.opts = { ...this.opts, slotHeight };
3751
+ if (!(this.current instanceof TimeGridView)) return;
3752
+ this.current.applyScale();
3753
+ const { top, height } = p.body.getBoundingClientRect();
3754
+ const scroller = scrollerOf(p.body, "y");
3755
+ if (scroller) scroller.scrollTop += top + height * p.anchor - p.y;
3756
+ }
3757
+ endPinch() {
3758
+ const p = this.pinch;
3759
+ this.pinch = null;
3760
+ if (!p?.body.isConnected) return;
3761
+ const settled = Math.round(this.opts.slotHeight);
3762
+ const slotHeight = settled === Math.round(p.drawn) ? p.option : settled;
3763
+ this.pinchTo(p, slotHeight);
3764
+ this.current?.render(null);
3765
+ if (slotHeight !== p.option) this.opts.onSlotHeightChange?.(slotHeight);
3766
+ }
3465
3767
  onPointerMove = (ev) => {
3466
3768
  const s = this.session;
3467
3769
  if (!s || ev.pointerId !== s.pointerId) return;
@@ -3487,6 +3789,12 @@ var Calendar = class {
3487
3789
  this.track(s, ev);
3488
3790
  };
3489
3791
  onPointerUp = (ev) => {
3792
+ if (ev.pointerId === this.cancelledPointerId) {
3793
+ this.cancelledPointerId = null;
3794
+ this.suppressNextClick();
3795
+ this.listenDoc(false);
3796
+ return;
3797
+ }
3490
3798
  const s = this.session;
3491
3799
  if (!s || ev.pointerId !== s.pointerId) return;
3492
3800
  if (!s.active) {
@@ -3499,28 +3807,49 @@ var Calendar = class {
3499
3807
  return;
3500
3808
  }
3501
3809
  this.suppressNextClick();
3810
+ this.track(s, ev);
3502
3811
  this.endSession(ev, false);
3503
3812
  };
3504
3813
  onPointerCancel = (ev) => {
3814
+ if (ev.pointerId === this.cancelledPointerId) {
3815
+ this.cancelledPointerId = null;
3816
+ this.listenDoc(false);
3817
+ }
3505
3818
  if (this.session && ev.pointerId === this.session.pointerId) this.endSession(null, true);
3506
3819
  };
3507
3820
  onDocKeyDown = (ev) => {
3508
3821
  if (ev.key === "Escape" && this.session) {
3509
3822
  ev.preventDefault();
3510
3823
  ev.stopPropagation();
3824
+ const pointerId = this.session.active ? this.session.pointerId : null;
3511
3825
  this.endSession(null, true);
3826
+ if (pointerId !== null) {
3827
+ this.cancelledPointerId = pointerId;
3828
+ this.listenDoc(true);
3829
+ }
3512
3830
  }
3513
3831
  };
3514
3832
  /** Proposal for the current pointer position. */
3515
3833
  track(s, ev) {
3834
+ if (this.listDragPreview) {
3835
+ const preview = this.listDragPreview;
3836
+ const bounds = this.root.getBoundingClientRect();
3837
+ preview.element.style.left = `${Math.max(8, Math.min(this.win.innerWidth - preview.width - 8, ev.clientX - preview.offsetX)) - bounds.left}px`;
3838
+ preview.element.style.top = `${Math.max(8, Math.min(this.win.innerHeight - preview.height - 8, ev.clientY - preview.offsetY)) - bounds.top}px`;
3839
+ }
3516
3840
  const hit = this.hitAt(ev.clientX, ev.clientY);
3517
3841
  if (s.mode === "external") {
3518
3842
  this.trackExternal(s, hit);
3519
3843
  return;
3520
3844
  }
3521
3845
  if (!s.grab) s.grab = hit;
3522
- if (!hit || !s.grab) return;
3846
+ if (!hit || !s.grab) {
3847
+ this.invalidateProposal(s);
3848
+ return;
3849
+ }
3523
3850
  if (s.mode === "select") {
3851
+ s.denied = false;
3852
+ this.root.classList.remove("tnc-drop-denied");
3524
3853
  const sel = this.selectionFor(s.grab, hit);
3525
3854
  if (!sel) return;
3526
3855
  this.selection = sel;
@@ -3529,25 +3858,40 @@ var Calendar = class {
3529
3858
  }
3530
3859
  const origin = s.origin;
3531
3860
  const p = s.mode === "move" ? this.moveProposal(origin, s.grab, hit) : this.resizeProposal(origin, hit, s.mode === "resize-start");
3532
- if (!p) return;
3861
+ if (!p) {
3862
+ this.invalidateProposal(s);
3863
+ return;
3864
+ }
3533
3865
  const allowed = this.opts.canDrop?.({ item: origin.item, start: new Date(p.startMs), end: new Date(p.endMs), allDay: p.allDay, rowId: p.rowId }) ?? true;
3534
3866
  s.denied = !allowed;
3535
3867
  this.root.classList.toggle("tnc-drop-denied", !allowed);
3536
- if (!allowed || sameProposal(p, s.proposal)) return;
3868
+ if (!allowed) {
3869
+ this.invalidateProposal(s);
3870
+ return;
3871
+ }
3872
+ if (sameProposal(p, s.proposal)) return;
3537
3873
  s.proposal = p;
3538
3874
  this.dragRow = p.rowId !== void 0 ? { id: origin.id, rowId: p.rowId } : null;
3539
- this.applyChange(this.store.upsert([this.withProposal(origin, p)]));
3875
+ if (this.opts.view !== "list") this.applyChange(this.store.upsert([this.withProposal(origin, p)]));
3876
+ }
3877
+ invalidateProposal(s) {
3878
+ const hadProposal = s.proposal !== null;
3879
+ s.proposal = null;
3880
+ s.hit = null;
3881
+ s.denied = true;
3882
+ this.dragRow = null;
3883
+ this.root.classList.add("tnc-drop-denied");
3884
+ this.strip?.markTarget(null);
3885
+ this.unselectSilently();
3886
+ if (s.mode === "external") this.applyChange(this.store.remove([EXTERNAL_ID]));
3887
+ else if (hadProposal && s.origin) this.applyChange(this.store.upsert([s.origin.item]));
3540
3888
  }
3541
3889
  trackExternal(s, raw) {
3542
3890
  const src = s.source;
3543
3891
  const hit = raw?.kind === "timeline" && raw.area !== "track" && raw.area !== "header" ? null : raw;
3544
3892
  s.hit = hit;
3545
3893
  if (!hit) {
3546
- if (s.proposal) {
3547
- s.proposal = null;
3548
- this.applyChange(this.store.remove([EXTERNAL_ID]));
3549
- }
3550
- this.unselectSilently();
3894
+ this.invalidateProposal(s);
3551
3895
  return;
3552
3896
  }
3553
3897
  const dur = src.durationMinutes ?? this.opts.defaultTimedMinutes;
@@ -3566,6 +3910,10 @@ var Calendar = class {
3566
3910
  const allowed = this.opts.canDrop?.({ item: this.externalItem(src, p), start: new Date(p.startMs), end: new Date(p.endMs), allDay: p.allDay, rowId: p.rowId }) ?? true;
3567
3911
  s.denied = !allowed;
3568
3912
  this.root.classList.toggle("tnc-drop-denied", !allowed);
3913
+ if (!allowed) {
3914
+ this.invalidateProposal(s);
3915
+ return;
3916
+ }
3569
3917
  if (sameProposal(p, s.proposal)) return;
3570
3918
  s.proposal = p;
3571
3919
  if (hit.kind === "timeline" && p.rowId === void 0) {
@@ -3758,7 +4106,19 @@ var Calendar = class {
3758
4106
  endSession(ev, cancel) {
3759
4107
  const s = this.session;
3760
4108
  if (!s) return;
4109
+ if (this.dragScrollFrame !== null) this.win.cancelAnimationFrame(this.dragScrollFrame);
4110
+ this.dragScrollFrame = null;
4111
+ if (cancel || s.denied) for (const [scroller, position] of this.dragScrollOrigins) {
4112
+ scroller.scrollTop = position.top;
4113
+ scroller.scrollLeft = position.left;
4114
+ }
4115
+ this.dragScrollOrigins.clear();
4116
+ this.dragScrollEdges.clear();
3761
4117
  this.session = null;
4118
+ this.listDragPreview?.element.remove();
4119
+ this.listDragPreview = null;
4120
+ for (const source of this.dragSources) source.remove();
4121
+ this.dragSources = [];
3762
4122
  if (s.timer !== null) this.win.clearTimeout(s.timer);
3763
4123
  this.listenDoc(false);
3764
4124
  this.root.classList.remove(
@@ -3772,10 +4132,15 @@ var Calendar = class {
3772
4132
  );
3773
4133
  const draggedId = this.draggingId;
3774
4134
  this.draggingId = null;
4135
+ this.dragWidth = null;
3775
4136
  this.dragRow = null;
3776
- if (draggedId) for (const el of this.elementsFor(draggedId)) el.classList.remove("is-dragging");
4137
+ if (draggedId) for (const el of this.elementsFor(draggedId)) {
4138
+ el.classList.remove("is-dragging");
4139
+ el.style.removeProperty("--tnc-drag-width");
4140
+ }
3777
4141
  this.strip?.markTarget(null);
3778
4142
  if (!s.active) return;
4143
+ if (this.current instanceof DayGridView) this.current.render(null);
3779
4144
  if (s.mode === "select") {
3780
4145
  if (cancel) this.unselectSilently();
3781
4146
  else if (this.selection) this.fireSelect(this.selection, ev);
@@ -3827,10 +4192,16 @@ var Calendar = class {
3827
4192
  revert();
3828
4193
  return;
3829
4194
  }
4195
+ if (this.opts.view === "list") this.applyChange(this.store.upsert([this.withProposal(origin, p)]));
3830
4196
  this.flush();
3831
4197
  this.commit(s.mode === "move" ? "drop" : "resize", origin, p, ev, revert);
3832
4198
  }
3833
4199
  commit(kind, origin, p, ev, revert) {
4200
+ let reverted = false;
4201
+ const mark = (state) => {
4202
+ this.itemWriteStates.set(origin.id, state);
4203
+ for (const element of this.elementsFor(origin.id)) this.applyItemWriteState(element, origin.id);
4204
+ };
3834
4205
  const arg = {
3835
4206
  item: origin.item,
3836
4207
  start: new Date(p.startMs),
@@ -3846,19 +4217,32 @@ var Calendar = class {
3846
4217
  jsEvent: ev,
3847
4218
  source: ev && "key" in ev ? "keyboard" : "pointer",
3848
4219
  view: this.view,
3849
- revert
4220
+ revert: () => {
4221
+ reverted = true;
4222
+ revert();
4223
+ mark("failed");
4224
+ }
3850
4225
  };
3851
4226
  const handler = kind === "drop" ? this.opts.onItemDrop : this.opts.onItemResize;
3852
4227
  try {
4228
+ mark("pending");
3853
4229
  const res = handler?.(arg);
3854
4230
  if (res && typeof res.then === "function") {
3855
- Promise.resolve(res).catch(() => revert());
3856
- }
4231
+ void Promise.resolve(res).then(() => {
4232
+ if (!reverted) mark("changed");
4233
+ }, () => arg.revert());
4234
+ } else if (!reverted) mark("changed");
3857
4235
  } catch (err) {
3858
- revert();
4236
+ arg.revert();
3859
4237
  throw err;
3860
4238
  }
3861
4239
  }
4240
+ applyItemWriteState(element, id) {
4241
+ const state = this.itemWriteStates.get(id);
4242
+ for (const name of ["pending", "failed", "changed"]) element.classList.toggle(`is-write-${name}`, state === name);
4243
+ if (state === "pending") element.setAttribute("aria-busy", "true");
4244
+ else element.removeAttribute("aria-busy");
4245
+ }
3862
4246
  /* --------------------------------------------------------- click / keys */
3863
4247
  /** Runs in the capture phase so the click ending a drag never reaches content inside the calendar, React portals included. */
3864
4248
  onClickCapture = (ev) => {
@@ -3868,6 +4252,12 @@ var Calendar = class {
3868
4252
  ev.preventDefault();
3869
4253
  };
3870
4254
  onClick = (ev) => {
4255
+ const compactDay = ev.target.closest?.(".tnc-dg--compact .tnc-dg-day");
4256
+ if (compactDay && this.root.contains(compactDay) && !ev.target.closest(".tnc-navlink")) {
4257
+ const day = parseDay(compactDay.dataset.date ?? "");
4258
+ if (day !== null) this.openMore(day, compactDay, ev);
4259
+ return;
4260
+ }
3871
4261
  const yearDay = ev.target.closest?.(".tnc-ym-day");
3872
4262
  if (yearDay && this.root.contains(yearDay)) {
3873
4263
  const day = parseDay(yearDay.dataset.date ?? "");
@@ -3956,7 +4346,8 @@ var Calendar = class {
3956
4346
  const day = parseDay(target.dataset.date ?? "");
3957
4347
  if (day !== null) {
3958
4348
  ev.preventDefault();
3959
- this.clickSlot({ kind: "day", day }, ev, true);
4349
+ if (target.closest(".tnc-dg--compact")) this.openMore(day, target, ev);
4350
+ else this.clickSlot({ kind: "day", day }, ev, true);
3960
4351
  }
3961
4352
  return;
3962
4353
  }
@@ -4044,9 +4435,13 @@ var Calendar = class {
4044
4435
  const dt = ev.dataTransfer;
4045
4436
  if (!dt || !this.opts.acceptNativeDrop?.(dt) || !this.current) return;
4046
4437
  ev.preventDefault();
4438
+ const hit = this.hitAt(ev.clientX, ev.clientY);
4439
+ if (!hit || hit.kind === "timeline" && hit.area !== "track" && hit.area !== "header") {
4440
+ dt.dropEffect = "none";
4441
+ this.unselectSilently();
4442
+ return;
4443
+ }
4047
4444
  dt.dropEffect = "copy";
4048
- const hit = this.current.hit(ev.clientX, ev.clientY);
4049
- if (!hit || hit.kind === "timeline" && hit.area !== "track" && hit.area !== "header") return;
4050
4445
  const sel = this.selectionFor(hit, hit);
4051
4446
  if (!sel) return;
4052
4447
  this.selection = sel;
@@ -4060,7 +4455,7 @@ var Calendar = class {
4060
4455
  const dt = ev.dataTransfer;
4061
4456
  if (!dt || !this.opts.acceptNativeDrop?.(dt) || !this.current) return;
4062
4457
  ev.preventDefault();
4063
- const hit = this.current.hit(ev.clientX, ev.clientY);
4458
+ const hit = this.hitAt(ev.clientX, ev.clientY);
4064
4459
  this.unselectSilently();
4065
4460
  if (!hit || hit.kind === "timeline" && hit.area !== "track" && hit.area !== "header") return;
4066
4461
  const sel = this.selectionFor(hit, hit);
@@ -4091,7 +4486,7 @@ function scrollerOf(el, axis) {
4091
4486
  return doc.scrollingElement;
4092
4487
  }
4093
4488
  function rangeMoved(prev, next) {
4094
- return !prev || prev.type !== next.type || prev.range.startDay !== next.range.startDay || prev.range.endDay !== next.range.endDay || prev.dataRange.endDay !== next.dataRange.endDay;
4489
+ return !prev || prev.type !== next.type || prev.range.startDay !== next.range.startDay || prev.range.endDay !== next.range.endDay || prev.dataRange.startDay !== next.dataRange.startDay || prev.dataRange.endDay !== next.dataRange.endDay;
4095
4490
  }
4096
4491
 
4097
4492
  export {