@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.
@@ -832,14 +832,16 @@ function pct(n) {
832
832
 
833
833
  // src/dom/format.ts
834
834
  var Formatter = class {
835
- cache = /* @__PURE__ */ new Map();
836
- locale;
837
- hour12;
838
- constructor(locale, hour12) {
835
+ constructor(locale, hour12, formatTime) {
836
+ this.formatTime = formatTime;
839
837
  this.locale = locale;
840
838
  const probe = new Intl.DateTimeFormat(locale, { hour: "numeric" }).resolvedOptions();
841
839
  this.hour12 = hour12 ?? (probe.hour12 === true || probe.hourCycle === "h12" || probe.hourCycle === "h11");
842
840
  }
841
+ formatTime;
842
+ cache = /* @__PURE__ */ new Map();
843
+ locale;
844
+ hour12;
843
845
  fmt(key, opts) {
844
846
  let f = this.cache.get(key);
845
847
  if (!f) {
@@ -897,6 +899,7 @@ var Formatter = class {
897
899
  /** Wall-clock minutes to a label. `compact` drops ":00" in 12-hour formats ("9am"). */
898
900
  time(minute, compact = false) {
899
901
  const m = (Math.round(minute) % 1440 + 1440) % 1440;
902
+ if (this.formatTime) return this.formatTime(m);
900
903
  const date = new Date(Date.UTC(2e3, 0, 1, Math.floor(m / 60), m % 60));
901
904
  if (this.hour12) {
902
905
  const parts = this.fmt("t12", { hour: "numeric", minute: "2-digit", hour12: true }).formatToParts(date);
@@ -988,6 +991,7 @@ function defaultOptions() {
988
991
  snapDuration: void 0,
989
992
  scrollTime: "08:00",
990
993
  slotHeight: 24,
994
+ pinchZoom: true,
991
995
  expandRows: false,
992
996
  nowIndicator: true,
993
997
  selectMirror: true,
@@ -1029,6 +1033,7 @@ var LAYOUT_KEYS = /* @__PURE__ */ new Set([
1029
1033
  "locale",
1030
1034
  "firstDay",
1031
1035
  "hour12",
1036
+ "formatTime",
1032
1037
  "weekNumbers",
1033
1038
  "weekends",
1034
1039
  "hiddenDays",
@@ -1041,9 +1046,11 @@ var LAYOUT_KEYS = /* @__PURE__ */ new Set([
1041
1046
  "slotDuration",
1042
1047
  "snapDuration",
1043
1048
  "slotHeight",
1049
+ "pinchZoom",
1044
1050
  "expandRows",
1045
1051
  "nowIndicator",
1046
1052
  "slotEventOverlap",
1053
+ "overlapStackThreshold",
1047
1054
  "eventMinHeight",
1048
1055
  "eventMaxStack",
1049
1056
  "dayMaxEvents",
@@ -1074,8 +1081,9 @@ var DayGridView = class {
1074
1081
  constructor(ctx) {
1075
1082
  this.ctx = ctx;
1076
1083
  const { doc, opts, days } = ctx;
1077
- this.dotsMode = opts.monthDisplay === "dots";
1084
+ this.dotsMode = opts.monthDisplay === "dots" || opts.monthDisplay === "chips";
1078
1085
  this.el = h(doc, "div", this.dotsMode ? "tnc-dg tnc-dg--dots" : "tnc-dg");
1086
+ this.el.classList.toggle("tnc-dg--chips", opts.monthDisplay === "chips");
1079
1087
  this.scroller = h(doc, "div", "tnc-dg-scroll");
1080
1088
  this.grid = h(doc, "div", "tnc-dg-grid", { role: "grid", "aria-label": ctx.view.title });
1081
1089
  const perWeek = this.daysPerWeek();
@@ -1105,14 +1113,14 @@ var DayGridView = class {
1105
1113
  const cell = h(doc, "div", cls.join(" "), { role: "gridcell", "aria-label": ctx.fmt.longDate(d), "data-date": arg.dateStr });
1106
1114
  if (this.dotsMode) cell.tabIndex = 0;
1107
1115
  const num2 = ctx.fmt.dayNumber(d);
1108
- const label = ymdOfDay(d).day === 1 ? `${ctx.fmt.monthShort(d)} ${num2}` : num2;
1116
+ const label = !this.dotsMode && ymdOfDay(d).day === 1 ? `${ctx.fmt.monthShort(d)} ${num2}` : num2;
1109
1117
  cell.append(
1110
1118
  h(
1111
1119
  doc,
1112
1120
  "div",
1113
1121
  "tnc-dg-top",
1114
1122
  null,
1115
- opts.navLinks ? ctx.navLink(d, label, "tnc-dg-num") : h(doc, "span", "tnc-dg-num", null, label)
1123
+ opts.navLinks && !this.dotsMode ? ctx.navLink(d, label, "tnc-dg-num") : h(doc, "span", "tnc-dg-num", null, label)
1116
1124
  )
1117
1125
  );
1118
1126
  dates.append(cell);
@@ -1126,20 +1134,20 @@ var DayGridView = class {
1126
1134
  layer.append(content);
1127
1135
  row.append(dates, layer);
1128
1136
  this.grid.append(row);
1129
- const dots = this.dotsMode ? cells.map((cell) => {
1137
+ const dots = cells.map((cell) => {
1130
1138
  const box = h(doc, "div", "tnc-dg-dots", { "aria-hidden": "true" });
1131
1139
  if (opts.dayMaxEvents === false) box.classList.add("is-unlimited");
1132
1140
  const count = h(doc, "span", "tnc-dg-dots-more");
1133
1141
  cell.append(box);
1134
1142
  return { box, pool: new ItemPool(ctx, box), count };
1135
- }) : null;
1143
+ });
1136
1144
  this.weeks.push({ days: wdays, row, cells, layer, sel, more, pool: new ItemPool(ctx, content), dots });
1137
1145
  }
1138
1146
  this.scroller.append(this.grid);
1139
1147
  this.el.append(this.scroller);
1140
1148
  this.el.classList.toggle("tnc-dg--auto", opts.height === "auto");
1141
1149
  const RO = ctx.win.ResizeObserver;
1142
- if (RO && (opts.dayMaxEvents === true || this.dotsMode && opts.dayMaxEvents !== false)) {
1150
+ if (RO) {
1143
1151
  this.resizeObs = new RO(() => {
1144
1152
  if (this.sizeKey() !== this.lastHeights) this.render(null);
1145
1153
  });
@@ -1157,6 +1165,7 @@ var DayGridView = class {
1157
1165
  topPx = 24;
1158
1166
  lastHeights = "";
1159
1167
  dotsMode;
1168
+ dragPreviews = [];
1160
1169
  daysPerWeek() {
1161
1170
  const { days } = this.ctx;
1162
1171
  const first = days[0];
@@ -1170,16 +1179,71 @@ var DayGridView = class {
1170
1179
  }
1171
1180
  render(_change) {
1172
1181
  const { ctx } = this;
1182
+ this.clearDragPreviews();
1183
+ const dragged = ctx.draggingId ? ctx.store.get(ctx.draggingId) : void 0;
1184
+ const source = dragged ? this.elementsFor(dragged.id)[0] : void 0;
1185
+ if (dragged && source && this.el.closest(".tnc-dragging-move")) {
1186
+ const proposals = [dragged, ...[...ctx.store.all()].filter((item) => item.id !== dragged.id && item.item.preview)];
1187
+ for (const proposal of proposals) {
1188
+ const original = this.elementsFor(proposal.id)[0] ?? source;
1189
+ const bounds = original.getBoundingClientRect();
1190
+ const sourceWeek = original.closest(".tnc-dg-week");
1191
+ const offset = bounds.top - sourceWeek.getBoundingClientRect().top;
1192
+ for (const week of this.weeks) {
1193
+ const start = week.days.findIndex((day) => day >= proposal.startDay && day < proposal.endDay);
1194
+ if (start < 0) continue;
1195
+ let end = start + 1;
1196
+ while (!this.dotsMode && end < week.days.length && week.days[end] < proposal.endDay) end++;
1197
+ 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 });
1198
+ preview.classList.add("tnc-month-preview");
1199
+ preview.inert = true;
1200
+ setStyle(preview, {
1201
+ left: `calc(${pct(start / week.days.length)} + 2px)`,
1202
+ width: `calc(${pct((end - start) / week.days.length)} - 4px)`,
1203
+ top: `${Math.max(this.topPx, Math.min(offset, week.row.clientHeight - bounds.height - 4))}px`,
1204
+ height: `${bounds.height}px`
1205
+ });
1206
+ week.layer.append(preview);
1207
+ this.dragPreviews.push(preview);
1208
+ }
1209
+ }
1210
+ return;
1211
+ }
1212
+ this.dotsMode = ctx.opts.monthDisplay !== "full" || this.grid.clientWidth / this.daysPerWeek() < 72;
1213
+ this.el.classList.toggle("tnc-dg--dots", this.dotsMode);
1214
+ this.el.classList.toggle("tnc-dg--compact", this.dotsMode && ctx.opts.monthDisplay === "full");
1215
+ this.el.classList.toggle("tnc-dg--chips", ctx.opts.monthDisplay === "chips" && this.weeks[0].dots[0].box.clientHeight >= 44);
1173
1216
  const cs = ctx.win.getComputedStyle(this.el);
1174
1217
  this.barPx = px(cs, "--tnc-bar-h", 20);
1175
1218
  this.topPx = this.weeks[0]?.cells[0]?.querySelector(".tnc-dg-top")?.offsetHeight || px(cs, "--tnc-dg-top-h", 24);
1176
1219
  this.morePx = this.measureMore(cs);
1177
1220
  this.lastHeights = this.sizeKey();
1178
1221
  for (const w of this.weeks) {
1179
- if (w.dots) this.renderDots(w);
1180
- else this.renderWeek(w);
1222
+ for (const cell of w.cells) {
1223
+ if (this.dotsMode) cell.tabIndex = 0;
1224
+ else cell.removeAttribute("tabindex");
1225
+ if (this.dotsMode && ctx.opts.monthDisplay === "full") cell.setAttribute("aria-haspopup", "dialog");
1226
+ else cell.removeAttribute("aria-haspopup");
1227
+ }
1228
+ for (const cell of w.dots) cell.box.hidden = !this.dotsMode;
1229
+ if (this.dotsMode) {
1230
+ w.pool.begin();
1231
+ w.pool.end();
1232
+ w.more.replaceChildren();
1233
+ this.renderDots(w);
1234
+ } else {
1235
+ for (const cell of w.dots) {
1236
+ cell.pool.begin();
1237
+ cell.pool.end();
1238
+ }
1239
+ this.renderWeek(w);
1240
+ }
1181
1241
  }
1182
1242
  }
1243
+ clearDragPreviews() {
1244
+ for (const preview of this.dragPreviews) this.ctx.destroyItemEl(preview);
1245
+ this.dragPreviews = [];
1246
+ }
1183
1247
  /** Height of a "+N more" link as the host styles it (`--tnc-more-h`, or a taller min-height). */
1184
1248
  measureMore(cs) {
1185
1249
  const fallback = px(cs, "--tnc-more-h", this.barPx);
@@ -1276,7 +1340,8 @@ var DayGridView = class {
1276
1340
  w.more.append(link);
1277
1341
  }
1278
1342
  existing.delete(key);
1279
- link.textContent = ctx.text.more(items.length);
1343
+ link.setAttribute("aria-label", ctx.text.more(items.length));
1344
+ link.textContent = w.row.clientWidth / n < 72 ? `+${new Intl.NumberFormat(ctx.opts.locale).format(items.length)}` : ctx.text.more(items.length);
1280
1345
  setStyle(link, {
1281
1346
  left: `calc(${pct(col / n)} + 2px)`,
1282
1347
  width: `calc(${pct(1 / n)} - 4px)`,
@@ -1306,10 +1371,11 @@ var DayGridView = class {
1306
1371
  const { dayMaxEvents } = ctx.opts;
1307
1372
  const first = w.dots[0].box;
1308
1373
  const cs = ctx.win.getComputedStyle(first);
1309
- const size = px(cs, "--tnc-dot-size", 8);
1374
+ const chips = this.el.classList.contains("tnc-dg--chips");
1375
+ const size = chips ? px(cs, "--tnc-chip-h", 14) : px(cs, "--tnc-dot-size", 8);
1310
1376
  const gap = px(cs, "--tnc-dot-gap", 3);
1311
1377
  const step = size + gap;
1312
- const perRow = Math.max(1, Math.floor((first.clientWidth + gap) / step));
1378
+ const perRow = chips ? 1 : Math.max(1, Math.floor((first.clientWidth + gap) / step));
1313
1379
  const rows = Math.max(1, Math.floor((first.clientHeight + gap) / step));
1314
1380
  const fits = dayMaxEvents === false ? Infinity : perRow * rows;
1315
1381
  const cap = typeof dayMaxEvents === "number" && dayMaxEvents > 0 ? Math.min(dayMaxEvents, fits) : fits;
@@ -1318,7 +1384,7 @@ var DayGridView = class {
1318
1384
  w.days.forEach((day, i) => {
1319
1385
  const cell = w.dots[i];
1320
1386
  const items = this.itemsOn(day);
1321
- const shown = items.length <= cap ? items.length : cap >= fits ? Math.max(0, cap - 2) : cap;
1387
+ const shown = items.length <= cap ? items.length : cap >= fits ? Math.max(1, cap - (chips ? 1 : 2)) : Math.max(1, cap);
1322
1388
  cell.pool.begin();
1323
1389
  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 }));
1324
1390
  cell.pool.end();
@@ -1369,6 +1435,7 @@ var DayGridView = class {
1369
1435
  return this.weeks.flatMap((w) => [...w.pool.elementsFor(id), ...(w.dots ?? []).flatMap((c) => c.pool.elementsFor(id))]);
1370
1436
  }
1371
1437
  destroy() {
1438
+ this.clearDragPreviews();
1372
1439
  this.resizeObs?.disconnect();
1373
1440
  for (const w of this.weeks) {
1374
1441
  w.pool.clear();
@@ -1421,6 +1488,7 @@ var ListView = class {
1421
1488
  frame = null;
1422
1489
  /** A picked day stays marked until the list is scrolled by hand. */
1423
1490
  pinned = false;
1491
+ pinnedTop = 0;
1424
1492
  itemsOn(day) {
1425
1493
  const { store, opts } = this.ctx;
1426
1494
  const out = store.spanningIn(day, day + 1, true);
@@ -1435,6 +1503,7 @@ var ListView = class {
1435
1503
  const leadItems = [];
1436
1504
  const inLead = /* @__PURE__ */ new Set();
1437
1505
  const keep = /* @__PURE__ */ new Set();
1506
+ let itemCount = 0;
1438
1507
  let prev = null;
1439
1508
  for (const day of days) {
1440
1509
  let items = this.itemsOn(day);
@@ -1448,7 +1517,7 @@ var ListView = class {
1448
1517
  return false;
1449
1518
  });
1450
1519
  }
1451
- if (!items.length && !agenda) continue;
1520
+ if (!items.length && !agenda && opts.listDayCount !== 1) continue;
1452
1521
  keep.add(day);
1453
1522
  let section = this.sections.get(day);
1454
1523
  let pool = this.pools.get(day);
@@ -1467,6 +1536,7 @@ var ListView = class {
1467
1536
  this.pools.set(day, pool);
1468
1537
  }
1469
1538
  section.classList.toggle("is-empty", !items.length);
1539
+ itemCount += items.length;
1470
1540
  this.fill(pool, items, () => day);
1471
1541
  if (prev ? prev.nextSibling !== section : this.body.firstChild !== section) {
1472
1542
  if (prev) prev.after(section);
@@ -1482,13 +1552,14 @@ var ListView = class {
1482
1552
  this.sections.delete(day);
1483
1553
  }
1484
1554
  this.renderLead(leadItems);
1485
- let empty = this.body.querySelector(":scope > .tnc-list-empty");
1486
- if (!keep.size && !leadItems.length) {
1555
+ let empty = this.body.querySelector(".tnc-list-empty");
1556
+ if (!itemCount && !leadItems.length && (!agenda || opts.listEmptyContent)) {
1487
1557
  if (!empty) {
1488
1558
  empty = h(doc, "div", "tnc-list-empty", { role: "listitem" }, ctx.text.noItems);
1489
1559
  const content = ctx.opts.listEmptyContent?.();
1490
1560
  if (content) empty.replaceChildren(content);
1491
- this.body.append(empty);
1561
+ if (this.sections.size === 1) this.sections.values().next().value.append(empty);
1562
+ else this.body.prepend(empty);
1492
1563
  }
1493
1564
  } else empty?.remove();
1494
1565
  this.ctx.strip?.draw();
@@ -1546,9 +1617,10 @@ var ListView = class {
1546
1617
  const section = day === this.ctx.days[0] && this.lead ? this.lead.section : this.sections.get(day);
1547
1618
  if (!section) return;
1548
1619
  this.ctx.strip?.measure();
1549
- section.scrollIntoView({ block: "start" });
1620
+ this.scroller.scrollTop += section.getBoundingClientRect().top - this.scroller.getBoundingClientRect().top;
1550
1621
  this.ctx.strip?.setFocus(day);
1551
1622
  this.pinned = true;
1623
+ this.pinnedTop = this.scroller.scrollTop;
1552
1624
  }
1553
1625
  fold = () => {
1554
1626
  this.pinned = false;
@@ -1556,6 +1628,7 @@ var ListView = class {
1556
1628
  if (strip?.expanded) strip.setExpanded(false);
1557
1629
  };
1558
1630
  onScroll = () => {
1631
+ if (this.pinned && Math.abs(this.scroller.scrollTop - this.pinnedTop) > 1) this.pinned = false;
1559
1632
  if (this.frame !== null) return;
1560
1633
  this.frame = this.ctx.win.requestAnimationFrame(() => {
1561
1634
  this.frame = null;
@@ -1566,7 +1639,7 @@ var ListView = class {
1566
1639
  followTop() {
1567
1640
  const strip = this.ctx.strip;
1568
1641
  if (!strip || this.pinned) return;
1569
- const edge = strip.el.getBoundingClientRect().bottom + 1;
1642
+ const edge = Math.max(strip.el.getBoundingClientRect().bottom, this.scroller.getBoundingClientRect().top) + 1;
1570
1643
  for (const day of this.ctx.days) {
1571
1644
  const section = this.sections.get(day);
1572
1645
  if (section && section.getBoundingClientRect().bottom > edge) {
@@ -1614,10 +1687,11 @@ var ListView = class {
1614
1687
  // src/dom/views/strip.ts
1615
1688
  var SWIPE_PX = 40;
1616
1689
  var DateStrip = class {
1617
- constructor(ctx, pick, focus, onShown) {
1690
+ constructor(ctx, pick, focus, onShown, onPage) {
1618
1691
  this.ctx = ctx;
1619
1692
  this.pick = pick;
1620
1693
  this.onShown = onShown;
1694
+ this.onPage = onPage;
1621
1695
  const { doc, text } = ctx;
1622
1696
  this.focus = focus;
1623
1697
  this.shown = startOfWeek(focus, ctx.firstDay);
@@ -1635,13 +1709,16 @@ var DateStrip = class {
1635
1709
  head.append(this.title, this.todayBtn, prev, next, this.slot);
1636
1710
  this.dows = h(doc, "div", "tnc-strip-dows", { "aria-hidden": "true" });
1637
1711
  this.grid = h(doc, "div", "tnc-strip-grid", { role: "group" });
1638
- const handle = h(doc, "button", "tnc-strip-handle", { type: "button", tabindex: "-1", "aria-hidden": "true" });
1639
- handle.addEventListener("click", () => this.setExpanded(!this.expanded));
1712
+ const handle = h(doc, "div", "tnc-strip-handle", { "aria-hidden": "true" });
1640
1713
  this.el.append(head, this.dows, this.grid, handle);
1641
1714
  this.grid.addEventListener("click", this.onClick);
1642
1715
  this.grid.addEventListener("keydown", this.onKeyDown);
1643
1716
  this.el.addEventListener("pointerdown", this.onPointerDown);
1644
1717
  this.el.addEventListener("pointerup", this.onPointerUp);
1718
+ this.el.addEventListener("touchstart", this.ownTouch, { passive: true });
1719
+ this.el.addEventListener("touchmove", this.ownTouch, { passive: true });
1720
+ this.el.addEventListener("touchend", this.ownTouch, { passive: true });
1721
+ this.el.addEventListener("touchcancel", this.ownTouch, { passive: true });
1645
1722
  this.el.addEventListener("pointercancel", this.onPointerCancel);
1646
1723
  this.el.addEventListener("click", this.onClickCapture, true);
1647
1724
  this.draw();
@@ -1650,6 +1727,7 @@ var DateStrip = class {
1650
1727
  ctx;
1651
1728
  pick;
1652
1729
  onShown;
1730
+ onPage;
1653
1731
  el;
1654
1732
  title;
1655
1733
  dows;
@@ -1708,6 +1786,7 @@ var DateStrip = class {
1708
1786
  step(dir) {
1709
1787
  this.shown = this.expanded ? addMonths(this.shown, dir) : this.shown + 7 * dir;
1710
1788
  this.draw();
1789
+ this.onPage?.(this.expanded ? this.shown : this.focus + 7 * dir);
1711
1790
  }
1712
1791
  days() {
1713
1792
  if (!this.expanded) return Array.from({ length: 7 }, (_, i) => this.shown + i);
@@ -1732,7 +1811,8 @@ var DateStrip = class {
1732
1811
  const tabDay = days.includes(this.focus) ? this.focus : days[0];
1733
1812
  const cells = days.map((d) => {
1734
1813
  const arg = ctx.dayArg(d);
1735
- const count = store.inRange(d, d + 1).filter((n) => n.item.display !== "background" && n.item.display !== "none").length;
1814
+ const items = store.inRange(d, d + 1).filter((n) => n.item.display !== "background" && n.item.display !== "none");
1815
+ const count = items.length;
1736
1816
  const first = ymdOfDay(d).day === 1 && d !== days[0] && !this.expanded;
1737
1817
  const label = fmt.longDate(d);
1738
1818
  const cell = h(
@@ -1756,7 +1836,15 @@ var DateStrip = class {
1756
1836
  }
1757
1837
  );
1758
1838
  if (first) cell.append(h(doc, "span", "tnc-strip-month", { "aria-hidden": "true" }, fmt.monthShort(d)));
1759
- cell.append(h(doc, "span", "tnc-strip-num", null, fmt.dayNumber(d)), h(doc, "span", "tnc-strip-dot", { "aria-hidden": "true" }));
1839
+ cell.append(h(doc, "span", "tnc-strip-num", null, fmt.dayNumber(d)));
1840
+ const dots = h(doc, "span", "tnc-strip-dots", { "aria-hidden": "true" });
1841
+ for (const item of items.slice(0, 3)) {
1842
+ const dot = h(doc, "span", "tnc-strip-dot");
1843
+ const color = item.item.color || item.item.borderColor || item.item.backgroundColor;
1844
+ if (color) dot.style.backgroundColor = color;
1845
+ dots.append(dot);
1846
+ }
1847
+ cell.append(dots);
1760
1848
  return cell;
1761
1849
  });
1762
1850
  this.grid.replaceChildren(...cells);
@@ -1796,6 +1884,9 @@ var DateStrip = class {
1796
1884
  if (ev.button !== 0 || this.ctx.draggingId) return;
1797
1885
  this.swipe = { id: ev.pointerId, x: ev.clientX, y: ev.clientY };
1798
1886
  };
1887
+ ownTouch = (event) => {
1888
+ event.stopPropagation();
1889
+ };
1799
1890
  onPointerUp = (ev) => {
1800
1891
  const s = this.swipe;
1801
1892
  this.swipe = null;
@@ -1835,6 +1926,7 @@ var TimeGridView = class {
1835
1926
  const showGrid = opts.timeGrid;
1836
1927
  this.el = h(doc, "div", "tnc-tg", { role: "table", "aria-label": ctx.view.title });
1837
1928
  this.el.classList.toggle("tnc-tg--no-grid", !showGrid);
1929
+ this.el.classList.toggle("tnc-tg--pinch", showGrid && opts.pinchZoom);
1838
1930
  this.scroller = h(doc, "div", "tnc-tg-scroll");
1839
1931
  this.dayNavigation = h(doc, "div", "tnc-tg-day-navigation", { role: "group", "aria-label": ctx.view.title });
1840
1932
  this.scroller.addEventListener("scroll", () => this.updateDayNavigation());
@@ -1984,6 +2076,11 @@ var TimeGridView = class {
1984
2076
  }
1985
2077
  if (this.nowGutter) gutter.append(this.nowGutter);
1986
2078
  }
2079
+ /** Slot height as drawn, which `expandRows` can stretch past the option. */
2080
+ get drawnSlotHeight() {
2081
+ return this.ctx.slot.duration * this.ppm;
2082
+ }
2083
+ /** Applies `slotHeight`. Items are placed in percentages, so they follow without a render. */
1987
2084
  applyScale() {
1988
2085
  const { opts, slot } = this.ctx;
1989
2086
  this.ppm = opts.slotHeight / slot.duration;
@@ -2164,15 +2261,17 @@ var TimeGridView = class {
2164
2261
  const { opts } = ctx;
2165
2262
  const readableWidth = Number.parseFloat(ctx.win.getComputedStyle(this.el).getPropertyValue("--tnc-event-min-width")) || 128;
2166
2263
  const laneWidth = c.col.clientWidth * (lane === "actual" && placements.some((placement) => placement.split) ? 0.22 : 1);
2167
- const collapse = placements.some((placement) => {
2264
+ const overlapThreshold = opts.overlapStackThreshold ?? 0;
2265
+ const collapsedClusters = new Set(placements.filter((placement) => {
2168
2266
  const columns = opts.eventMaxStack > 0 ? Math.min(placement.cols, opts.eventMaxStack) : placement.cols;
2169
- return columns > 1 && columns * readableWidth > laneWidth;
2170
- });
2171
- const withMore = lane === "plan" || collapse;
2172
- const maxStack = collapse ? 1 : opts.eventMaxStack;
2267
+ const denseOverlap = overlapThreshold > 1 && placement.cols >= overlapThreshold;
2268
+ return columns > 1 && (columns * readableWidth > laneWidth || denseOverlap);
2269
+ }).map((placement) => placement.cluster));
2173
2270
  const hiddenByCluster = /* @__PURE__ */ new Map();
2174
2271
  pool.begin();
2175
2272
  for (const p of placements) {
2273
+ const withMore = lane === "plan" || collapsedClusters.has(p.cluster);
2274
+ const maxStack = collapsedClusters.has(p.cluster) ? 1 : opts.eventMaxStack;
2176
2275
  const preview = p.item.item.preview || p.item.id === ctx.draggingId;
2177
2276
  if (!preview && withMore && maxStack > 0 && p.cols > maxStack && p.col >= maxStack - 1) {
2178
2277
  const rec = hiddenByCluster.get(p.cluster) ?? { count: 0, start: p.startMin, end: p.endMin, split: p.split, items: [] };
@@ -2185,7 +2284,7 @@ var TimeGridView = class {
2185
2284
  }
2186
2285
  const cols = !preview && withMore && maxStack > 0 ? Math.min(p.cols, maxStack) : p.cols;
2187
2286
  const span = Math.min(p.span, cols - p.col);
2188
- const left = p.col / cols;
2287
+ const left = p.item.id === ctx.draggingId ? 0 : p.col / cols;
2189
2288
  let width = span / cols;
2190
2289
  if (opts.slotEventOverlap && cols > 1 && p.col + span < cols) width = Math.min(1 - left, width * 1.7);
2191
2290
  const el = pool.place(p.item.id, this.meta(p, false));
@@ -2200,8 +2299,9 @@ var TimeGridView = class {
2200
2299
  });
2201
2300
  }
2202
2301
  pool.end();
2203
- if (!withMore) return;
2204
2302
  for (const [cluster, rec] of hiddenByCluster) {
2303
+ const collapse = collapsedClusters.has(cluster);
2304
+ const maxStack = collapse ? 1 : opts.eventMaxStack;
2205
2305
  const key = `${lane}-${cluster}`;
2206
2306
  let link = c.more.querySelector(`[data-cluster="${key}"]`);
2207
2307
  if (!link) {
@@ -2215,8 +2315,15 @@ var TimeGridView = class {
2215
2315
  link.classList.toggle("tnc-more--collision", collapse);
2216
2316
  link.textContent = `+${rec.count}`;
2217
2317
  if (collapse) {
2218
- link.textContent = ctx.text.appointments(rec.count);
2219
- link.setAttribute("aria-label", link.textContent);
2318
+ const items = placements.filter((placement) => placement.cluster === cluster).map((placement) => placement.item.item.title);
2319
+ const count = ctx.text.appointments(rec.count);
2320
+ const interval = `${ctx.fmt.time(rec.start)} \u2013 ${ctx.fmt.time(rec.end)}`;
2321
+ link.replaceChildren(
2322
+ h(ctx.doc, "span", "tnc-collision-count", null, String(rec.count)),
2323
+ h(ctx.doc, "span", "tnc-collision-range", null, interval),
2324
+ h(ctx.doc, "span", "tnc-collision-title", { "aria-hidden": "true" }, items.join(" \xB7 "))
2325
+ );
2326
+ link.setAttribute("aria-label", `${count}, ${interval}: ${items.join(", ")}`);
2220
2327
  link.title = link.getAttribute("aria-label");
2221
2328
  } else {
2222
2329
  link.removeAttribute("aria-label");
@@ -2225,6 +2332,7 @@ var TimeGridView = class {
2225
2332
  const cols = maxStack;
2226
2333
  setStyle(link, {
2227
2334
  top: top(rec.start),
2335
+ height: collapse ? pct((rec.end - rec.start) / this.minutes) : "",
2228
2336
  ...inLane(lane, rec.split, (cols - 1) / cols, 1 / cols)
2229
2337
  });
2230
2338
  }
@@ -2291,12 +2399,10 @@ var TimeGridView = class {
2291
2399
  const r2 = cells[i2].getBoundingClientRect();
2292
2400
  if (x >= r2.left && x < r2.right) return i2;
2293
2401
  }
2294
- const firstR = cells[0]?.getBoundingClientRect();
2295
- const lastR = cells[cells.length - 1]?.getBoundingClientRect();
2296
- if (firstR && x < firstR.left) return 0;
2297
- if (lastR && x >= lastR.right) return cells.length - 1;
2298
2402
  return -1;
2299
2403
  };
2404
+ const bounds = this.scroller.getBoundingClientRect();
2405
+ if (x < bounds.left || x >= bounds.right || y < bounds.top || y >= bounds.bottom) return null;
2300
2406
  if (this.allDayRow) {
2301
2407
  const r2 = this.allDayRow.getBoundingClientRect();
2302
2408
  if (y >= r2.top && y < r2.bottom || !this.body && y >= r2.top) {
@@ -2361,7 +2467,9 @@ var TimeGridView = class {
2361
2467
  scrollToTime(minute) {
2362
2468
  if (!this.body) return;
2363
2469
  const y = (minute - this.ctx.slot.min) * this.ppm;
2364
- this.scroller.scrollTop = Math.max(0, y);
2470
+ const label = this.gutter?.querySelector(".tnc-slot-label");
2471
+ const inset = label ? Math.ceil(label.getBoundingClientRect().height / 2) + 2 : 0;
2472
+ this.scroller.scrollTop = Math.max(0, y - inset);
2365
2473
  }
2366
2474
  getScroll() {
2367
2475
  return { top: this.scroller.scrollTop, left: this.scroller.scrollLeft };
@@ -3019,6 +3127,14 @@ ${this.ctx.text.more(items.length - 8)}` : ""}` : label;
3019
3127
  setScroll(s) {
3020
3128
  this.scroller.scrollTop = s.top;
3021
3129
  }
3130
+ revealDay(day) {
3131
+ const month = this.cells.find((cell) => cell.day === day)?.el.closest(".tnc-ym");
3132
+ if (!month) return;
3133
+ const bounds = month.getBoundingClientRect();
3134
+ const viewport = this.scroller.getBoundingClientRect();
3135
+ if (bounds.top < viewport.top) this.scroller.scrollTop += bounds.top - viewport.top;
3136
+ else if (bounds.bottom > viewport.bottom) this.scroller.scrollTop += bounds.top - viewport.top;
3137
+ }
3022
3138
  elementsFor(_id) {
3023
3139
  return [];
3024
3140
  }
@@ -3031,6 +3147,8 @@ ${this.ctx.text.more(items.length - 8)}` : ""}` : label;
3031
3147
  var EXTERNAL_ID = "__tnc_external__";
3032
3148
  var SVG_NS = "http://www.w3.org/2000/svg";
3033
3149
  var MAX_AGENDA_DAYS = 366;
3150
+ var PINCH_MIN_SLOT = 16;
3151
+ var PINCH_MAX_SLOT = 96;
3034
3152
  function sameOption(a, b, depth = 0) {
3035
3153
  if (a === b) return true;
3036
3154
  if (typeof a === "function" && typeof b === "function") return true;
@@ -3058,7 +3176,7 @@ var Calendar = class {
3058
3176
  this.win = this.doc.defaultView ?? globalThis;
3059
3177
  this.opts = { ...defaultOptions(), ...options };
3060
3178
  this.zone = getTimeZone(this.opts.timeZone);
3061
- this.fmt = new Formatter(this.opts.locale, this.opts.hour12);
3179
+ this.fmt = new Formatter(this.opts.locale, this.opts.hour12, this.opts.formatTime);
3062
3180
  this.text = this.resolveText();
3063
3181
  this.order = parseOrder(this.opts.eventOrder);
3064
3182
  this.store = new ItemStore(this.zone, {
@@ -3098,10 +3216,18 @@ var Calendar = class {
3098
3216
  conflicts = /* @__PURE__ */ new Map();
3099
3217
  slot = { min: 0, max: MINUTES_PER_DAY, duration: 30, snap: 30, scroll: 480 };
3100
3218
  draggingId = null;
3219
+ listDragPreview = null;
3220
+ dragSources = [];
3221
+ dragWidth = null;
3222
+ dragScrollFrame = null;
3223
+ dragScrollOrigins = /* @__PURE__ */ new Map();
3224
+ dragScrollEdges = /* @__PURE__ */ new Map();
3225
+ itemWriteStates = /* @__PURE__ */ new Map();
3101
3226
  dragRow = null;
3102
3227
  strip = null;
3103
3228
  stripSize = null;
3104
3229
  rebuilding = false;
3230
+ pinch = null;
3105
3231
  toolbarEl;
3106
3232
  viewHost;
3107
3233
  current = null;
@@ -3111,6 +3237,7 @@ var Calendar = class {
3111
3237
  session = null;
3112
3238
  selection = null;
3113
3239
  suppressClick = false;
3240
+ cancelledPointerId = null;
3114
3241
  selectedIds = /* @__PURE__ */ new Set();
3115
3242
  popover = null;
3116
3243
  timer = null;
@@ -3128,6 +3255,7 @@ var Calendar = class {
3128
3255
  this.positionMore();
3129
3256
  });
3130
3257
  this.resizeObs.observe(this.root);
3258
+ this.resizeObs.observe(this.toolbarEl);
3131
3259
  }
3132
3260
  }
3133
3261
  onAdoptCheck = () => {
@@ -3208,7 +3336,7 @@ var Calendar = class {
3208
3336
  defaultAllDayDays: this.opts.defaultAllDayDays
3209
3337
  });
3210
3338
  }
3211
- if (has("locale") || has("hour12")) this.fmt = new Formatter(this.opts.locale, this.opts.hour12);
3339
+ if (has("locale") || has("hour12") || has("formatTime")) this.fmt = new Formatter(this.opts.locale, this.opts.hour12, this.opts.formatTime);
3212
3340
  if (has("text") || has("locale")) this.text = this.resolveText();
3213
3341
  if (has("eventOrder")) this.order = parseOrder(this.opts.eventOrder);
3214
3342
  if (has("date")) this.anchor = this.toDay(this.opts.date) ?? this.anchor;
@@ -3254,7 +3382,12 @@ var Calendar = class {
3254
3382
  gotoDate(date) {
3255
3383
  const day = this.toDay(date);
3256
3384
  if (day === null) return;
3385
+ const sameMonth = this.opts.view === "month" && startOfMonth(day) === startOfMonth(this.anchor);
3257
3386
  this.anchor = day;
3387
+ if (sameMonth) {
3388
+ this.view = { ...this.view, anchor: day };
3389
+ return;
3390
+ }
3258
3391
  this.rebuild(true);
3259
3392
  }
3260
3393
  prev() {
@@ -3400,7 +3533,8 @@ var Calendar = class {
3400
3533
  out.push(...classList(this.opts.dayClassNames?.(this.dayArg(day))));
3401
3534
  return out;
3402
3535
  }
3403
- isEditable(n, what) {
3536
+ isEditable(n, what, presentation = false) {
3537
+ if (!presentation && this.itemWriteStates.get(n.id) === "pending") return false;
3404
3538
  if (n.id === EXTERNAL_ID || n.item.preview || this.opts.view === "year") return false;
3405
3539
  if (this.opts.view === "list" && (!this.opts.listAgenda || what === "duration")) return false;
3406
3540
  const it = n.item;
@@ -3440,13 +3574,17 @@ var Calendar = class {
3440
3574
  if (preview || n.id === EXTERNAL_ID) el.classList.add("is-mirror");
3441
3575
  if (this.conflicts.has(n.id)) el.classList.add("is-conflict");
3442
3576
  const indicator = variant === "indicator";
3443
- if (!preview && !indicator && (this.isEditable(n, "start") || this.isEditable(n, "duration"))) el.classList.add("is-draggable");
3444
- if (this.draggingId === n.id) el.classList.add("is-dragging");
3577
+ if (!preview && !indicator && (this.isEditable(n, "start", true) || this.isEditable(n, "duration", true))) el.classList.add("is-draggable");
3578
+ if (this.draggingId === n.id) {
3579
+ el.classList.add("is-dragging");
3580
+ if (variant === "timed" && this.dragWidth !== null) el.style.setProperty("--tnc-drag-width", `${this.dragWidth}px`);
3581
+ }
3445
3582
  if (this.selectedIds.has(n.id) && !preview) el.classList.add("is-selected");
3446
3583
  el.classList.add(...classList(it.className));
3447
3584
  const arg = this.itemArg(el, meta);
3448
3585
  el.classList.add(...classList(this.opts.itemClassNames?.(arg)));
3449
3586
  el.dataset.id = n.id;
3587
+ this.applyItemWriteState(el, n.id);
3450
3588
  if (it.kind) el.dataset.kind = it.kind;
3451
3589
  const color = it.color ?? it.borderColor;
3452
3590
  if (color) el.style.setProperty("--tnc-c", color);
@@ -3463,6 +3601,7 @@ var Calendar = class {
3463
3601
  const custom = variant === "list" ? this.opts.listItemContent?.(arg) : null;
3464
3602
  const content = indicator ? null : custom ?? this.opts.itemContent?.(arg);
3465
3603
  if (indicator) {
3604
+ if (this.opts.monthDisplay === "chips") el.append(h(this.doc, "span", "tnc-item-title", null, it.title));
3466
3605
  } else if (content != null) {
3467
3606
  const wrap = h(this.doc, "div", "tnc-item-main");
3468
3607
  wrap.append(content);
@@ -3475,6 +3614,7 @@ var Calendar = class {
3475
3614
  return el;
3476
3615
  }
3477
3616
  refreshItemEl(el, meta) {
3617
+ this.applyItemWriteState(el, meta.n.id);
3478
3618
  const time = el.querySelector(":scope .tnc-item-time");
3479
3619
  if (time) this.fillTime(time, meta.n, meta.variant, meta.day);
3480
3620
  el.classList.toggle("is-dragging", this.draggingId === meta.n.id);
@@ -3657,6 +3797,7 @@ var Calendar = class {
3657
3797
  this.current.scrollToTime(this.slot.scroll);
3658
3798
  this.current.revealDay(this.anchor);
3659
3799
  } else if (this.current instanceof TimelineView) this.current.scrollToDay(this.anchor);
3800
+ else if (this.current instanceof YearView) this.current.revealDay(this.anchor);
3660
3801
  if (rangeChanged) this.opts.onRangeChange?.(this.view);
3661
3802
  }
3662
3803
  /** Sets the visible days and view info from the anchor. */
@@ -3682,9 +3823,11 @@ var Calendar = class {
3682
3823
  }
3683
3824
  /** The range, run on to the end of the strip's days so their dots are right. */
3684
3825
  dataRangeFor(range) {
3685
- const end = this.strip ? Math.max(range.endDay, this.strip.window().end) : range.endDay;
3686
- if (end === range.endDay) return range;
3687
- return { ...range, endDay: end, end: new Date(toInstant(end, 0, this.zone)) };
3826
+ const window = this.strip?.window();
3827
+ const start = window ? Math.min(range.startDay, window.start) : range.startDay;
3828
+ const end = window ? Math.max(range.endDay, window.end) : range.endDay;
3829
+ if (start === range.startDay && end === range.endDay) return range;
3830
+ return { ...range, startDay: start, start: new Date(toInstant(start, 0, this.zone)), endDay: end, end: new Date(toInstant(end, 0, this.zone)) };
3688
3831
  }
3689
3832
  /** Adds, updates or removes the date strip for the current view. */
3690
3833
  syncStrip(navigated) {
@@ -3703,7 +3846,9 @@ var Calendar = class {
3703
3846
  this.root.classList.add("tnc--has-strip");
3704
3847
  const { startDay, endDay } = this.view.range;
3705
3848
  if (!this.strip) {
3706
- const strip = new DateStrip(this, (day, ev) => this.pickStripDay(day, ev), startDay, () => this.stripShown());
3849
+ const strip = new DateStrip(this, (day, ev) => this.pickStripDay(day, ev), startDay, () => this.stripShown(), (day) => {
3850
+ if (this.agendaList()) this.showListDay(day);
3851
+ });
3707
3852
  this.strip = strip;
3708
3853
  this.root.insertBefore(strip.el, this.viewHost);
3709
3854
  const RO = this.win.ResizeObserver;
@@ -3731,20 +3876,24 @@ var Calendar = class {
3731
3876
  stripShown() {
3732
3877
  if (!this.strip || this.rebuilding || !this.view) return;
3733
3878
  const next = this.dataRangeFor(this.view.range);
3734
- if (next.endDay === this.view.dataRange.endDay) return;
3879
+ if (next.startDay === this.view.dataRange.startDay && next.endDay === this.view.dataRange.endDay) return;
3735
3880
  this.view = { ...this.view, dataRange: next };
3736
3881
  this.opts.onRangeChange?.(this.view);
3737
3882
  }
3738
3883
  /** What is under a point, the date strip included. */
3739
3884
  hitAt(x, y) {
3885
+ const target = this.doc.elementFromPoint(x, y);
3886
+ if (!target || !this.root.contains(target)) {
3887
+ this.strip?.markTarget(null);
3888
+ return null;
3889
+ }
3740
3890
  const strip = this.strip;
3741
3891
  if (strip) {
3742
- const el = this.doc.elementFromPoint(x, y);
3743
- const day = el ? strip.dayAt(el) : null;
3892
+ const day = strip.dayAt(target);
3744
3893
  strip.markTarget(day !== null && this.draggingId ? day : null);
3745
3894
  if (day !== null) return { kind: "day", day };
3746
3895
  }
3747
- return this.current?.hit(x, y) ?? null;
3896
+ return this.current?.el.contains(target) ? this.current.hit(x, y) : null;
3748
3897
  }
3749
3898
  agendaList() {
3750
3899
  return this.opts.view === "list" && this.opts.listAgenda && this.current instanceof ListView ? this.current : null;
@@ -3858,11 +4007,13 @@ var Calendar = class {
3858
4007
  buttons.style.display = "flex";
3859
4008
  select.style.display = "none";
3860
4009
  const startWidth = Array.from(start.children).reduce((width, child) => width + child.scrollWidth, 0);
3861
- const compact = mode === "select" || mode === "auto" && startWidth + buttons.scrollWidth + 32 > this.root.clientWidth;
4010
+ const availableWidth = Math.min(this.root.clientWidth, this.toolbarEl.clientWidth);
4011
+ const compact = mode === "select" || mode === "auto" && startWidth + buttons.scrollWidth + 32 > availableWidth;
3862
4012
  buttons.hidden = compact;
3863
4013
  select.hidden = !compact;
3864
4014
  buttons.style.display = compact ? "none" : "flex";
3865
4015
  select.style.display = compact ? "" : "none";
4016
+ select.parentElement?.classList.toggle("tnc-toolbar-end--compact", compact);
3866
4017
  }
3867
4018
  applyChange(change) {
3868
4019
  if (!change.added.length && !change.updated.length && !change.removed.length && !change.days.size && !change.spans) return;
@@ -3927,7 +4078,7 @@ var Calendar = class {
3927
4078
  const s = wallTime(n.startMs, this.zone);
3928
4079
  const start = this.fmt.time(s.minute, true);
3929
4080
  if (variant === "dot" || variant === "bar" || variant === "indicator") return { start: s.day === day ? start : "", end: "" };
3930
- if (n.startMs === n.endMs) return { start, end: "" };
4081
+ if (n.startMs === n.endMs || n.item.timeDisplay === "start") return { start, end: "" };
3931
4082
  return { start, end: this.fmt.time(wallTime(n.endMs, this.zone).minute, true) };
3932
4083
  }
3933
4084
  fillTime(el, n, variant, day) {
@@ -3943,7 +4094,8 @@ var Calendar = class {
3943
4094
  }
3944
4095
  const s = wallTime(n.startMs, this.zone);
3945
4096
  const e = wallTime(n.endMs, this.zone);
3946
- return `${n.item.title}, ${this.fmt.longDate(s.day)} ${this.fmt.time(s.minute)} \u2013 ${this.fmt.time(e.minute)}`;
4097
+ const start = `${n.item.title}, ${this.fmt.longDate(s.day)} ${this.fmt.time(s.minute)}`;
4098
+ return n.item.timeDisplay === "start" || n.startMs === n.endMs ? start : `${start} \u2013 ${this.fmt.time(e.minute)}`;
3947
4099
  }
3948
4100
  defaultContent(meta) {
3949
4101
  const { n, variant, day } = meta;
@@ -3971,7 +4123,7 @@ var Calendar = class {
3971
4123
  addHandles(el, meta) {
3972
4124
  const { n, variant } = meta;
3973
4125
  const ports = variant === "timeline" && !meta.mirror && n.id !== EXTERNAL_ID && !!this.opts.timeline?.onLink;
3974
- if (variant === "dot" || !this.isEditable(n, "duration")) {
4126
+ if (variant === "dot" || !this.isEditable(n, "duration", true)) {
3975
4127
  if (ports) this.addPorts(el, n);
3976
4128
  return;
3977
4129
  }
@@ -4060,7 +4212,10 @@ var Calendar = class {
4060
4212
  r.addEventListener("dragover", this.onNativeDragOver);
4061
4213
  r.addEventListener("dragleave", this.onNativeDragLeave);
4062
4214
  r.addEventListener("drop", this.onNativeDrop);
4215
+ r.addEventListener("touchstart", this.onTouchStart, { passive: true });
4063
4216
  r.addEventListener("touchmove", this.onTouchMove, { passive: false });
4217
+ r.addEventListener("touchend", this.onTouchEnd);
4218
+ r.addEventListener("touchcancel", this.onTouchEnd);
4064
4219
  r.addEventListener("pointerover", this.onAdoptCheck);
4065
4220
  r.addEventListener("focusin", this.onAdoptCheck);
4066
4221
  this.doc.addEventListener("pointerdown", this.onDocPointerDown, true);
@@ -4079,7 +4234,10 @@ var Calendar = class {
4079
4234
  r.removeEventListener("dragover", this.onNativeDragOver);
4080
4235
  r.removeEventListener("dragleave", this.onNativeDragLeave);
4081
4236
  r.removeEventListener("drop", this.onNativeDrop);
4237
+ r.removeEventListener("touchstart", this.onTouchStart);
4082
4238
  r.removeEventListener("touchmove", this.onTouchMove);
4239
+ r.removeEventListener("touchend", this.onTouchEnd);
4240
+ r.removeEventListener("touchcancel", this.onTouchEnd);
4083
4241
  r.removeEventListener("pointerover", this.onAdoptCheck);
4084
4242
  r.removeEventListener("focusin", this.onAdoptCheck);
4085
4243
  this.doc.removeEventListener("pointerdown", this.onDocPointerDown, true);
@@ -4099,6 +4257,10 @@ var Calendar = class {
4099
4257
  return el && this.root.contains(el) && this.meta.has(el) && !this.meta.get(el)?.n.item.preview ? el : null;
4100
4258
  }
4101
4259
  onDocPointerDown = (ev) => {
4260
+ if (this.cancelledPointerId !== null) {
4261
+ this.cancelledPointerId = null;
4262
+ this.listenDoc(false);
4263
+ }
4102
4264
  const t = ev.target;
4103
4265
  if (this.popover && !this.popover.el.contains(t) && !this.popover.anchor.contains(t)) this.closeMore();
4104
4266
  if (this.selection && !this.root.contains(t)) this.unselect();
@@ -4106,6 +4268,7 @@ var Calendar = class {
4106
4268
  onPointerDown = (ev) => {
4107
4269
  if (ev.button !== 0 || this.session) return;
4108
4270
  const target = ev.target;
4271
+ if (target.closest(".tnc-dg--compact .tnc-dg-day")) return;
4109
4272
  if (target.closest("button, a, input, select, textarea, .tnc-toolbar, .tnc-port, [data-tnc-no-drag]")) {
4110
4273
  if (!target.closest(".tnc-item")) return;
4111
4274
  if (target.closest(".tnc-port, [data-tnc-no-drag], input, select, textarea")) return;
@@ -4138,7 +4301,7 @@ var Calendar = class {
4138
4301
  y0: ev.clientY,
4139
4302
  active: false,
4140
4303
  origin,
4141
- grab: overflowOrigin && this.current instanceof TimeGridView && !origin?.allDay ? { kind: "time", day: overflowOrigin.day, minute: overflowOrigin.minute } : this.current?.hit(ev.clientX, ev.clientY) ?? null,
4304
+ 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,
4142
4305
  proposal: null,
4143
4306
  denied: false,
4144
4307
  timer: null,
@@ -4163,20 +4326,159 @@ var Calendar = class {
4163
4326
  };
4164
4327
  activate(s) {
4165
4328
  s.active = true;
4329
+ if (s.lastEvent) this.leaveHover(s.lastEvent);
4166
4330
  if (s.timer !== null) this.win.clearTimeout(s.timer);
4167
4331
  s.timer = null;
4168
4332
  if (s.origin) {
4169
4333
  this.draggingId = s.origin.id;
4170
- for (const el of this.elementsFor(s.origin.id)) el.classList.add("is-dragging");
4334
+ for (const el of this.elementsFor(s.origin.id)) {
4335
+ if (this.meta.get(el)?.variant === "timed" && s.mode === "move") {
4336
+ this.dragWidth = el.getBoundingClientRect().width;
4337
+ el.style.setProperty("--tnc-drag-width", `${this.dragWidth}px`);
4338
+ }
4339
+ if (this.opts.view !== "list" && !(this.current instanceof DayGridView) && s.mode === "move") {
4340
+ const source = el.cloneNode(true);
4341
+ source.classList.remove("is-selected");
4342
+ source.classList.add("tnc-drag-source");
4343
+ source.setAttribute("aria-hidden", "true");
4344
+ source.inert = true;
4345
+ for (const node of [source, ...source.querySelectorAll("[id], [data-id], [data-task-path]")]) {
4346
+ node.removeAttribute("id");
4347
+ node.removeAttribute("data-id");
4348
+ node.removeAttribute("data-task-path");
4349
+ }
4350
+ el.before(source);
4351
+ this.dragSources.push(source);
4352
+ }
4353
+ if (this.opts.view === "list" && !this.listDragPreview && s.mode === "move") {
4354
+ const bounds = el.getBoundingClientRect();
4355
+ const preview = el.cloneNode(true);
4356
+ preview.classList.add("tnc-drag-preview", "is-dragging");
4357
+ preview.removeAttribute("data-id");
4358
+ preview.removeAttribute("data-task-path");
4359
+ preview.setAttribute("aria-hidden", "true");
4360
+ preview.inert = true;
4361
+ preview.style.width = `${bounds.width}px`;
4362
+ this.root.append(preview);
4363
+ this.listDragPreview = { element: preview, offsetX: s.x0 - bounds.left, offsetY: s.y0 - bounds.top, width: bounds.width, height: bounds.height };
4364
+ }
4365
+ el.classList.add("is-dragging");
4366
+ }
4171
4367
  }
4172
4368
  this.closeMore();
4173
4369
  if (s.mode !== "select") this.unselectSilently();
4174
4370
  this.root.classList.add("tnc-dragging", `tnc-dragging-${s.mode}`);
4175
4371
  if (s.lastEvent) this.track(s, s.lastEvent);
4372
+ this.startDragScroll();
4373
+ }
4374
+ startDragScroll() {
4375
+ if (this.dragScrollFrame !== null) return;
4376
+ const tick = () => {
4377
+ this.dragScrollFrame = null;
4378
+ const session = this.session;
4379
+ const pointer = session?.lastEvent;
4380
+ if (!session?.active || !pointer) return;
4381
+ const origin = session.origin ? this.elementsFor(session.origin.id)[0] : this.viewHost;
4382
+ const underPointer = this.doc.elementFromPoint(pointer.clientX, pointer.clientY);
4383
+ let moved = false;
4384
+ if (origin && underPointer && this.root.contains(underPointer)) {
4385
+ for (const axis of ["x", "y"]) {
4386
+ const scroller = scrollerOf(origin, axis);
4387
+ if (!scroller) continue;
4388
+ const viewport = scroller === this.doc.scrollingElement ? { left: 0, top: 0, right: this.win.innerWidth, bottom: this.win.innerHeight } : scroller.getBoundingClientRect();
4389
+ const coordinate = axis === "x" ? pointer.clientX : pointer.clientY;
4390
+ const lower = Math.max(0, axis === "x" ? viewport.left : viewport.top);
4391
+ const upper = Math.min(axis === "x" ? this.win.innerWidth : this.win.innerHeight, axis === "x" ? viewport.right : viewport.bottom);
4392
+ if (coordinate < lower || coordinate > upper) {
4393
+ this.dragScrollEdges.delete(axis);
4394
+ continue;
4395
+ }
4396
+ const delta = coordinate < lower + 32 ? -Math.min(12, (lower + 32 - coordinate) / 2) : coordinate > upper - 32 ? Math.min(12, (coordinate - upper + 32) / 2) : 0;
4397
+ if (!delta) {
4398
+ this.dragScrollEdges.delete(axis);
4399
+ continue;
4400
+ }
4401
+ const edge = this.dragScrollEdges.get(axis);
4402
+ const direction = Math.sign(delta);
4403
+ const now = this.win.performance.now();
4404
+ if (!edge || edge.scroller !== scroller || edge.direction !== direction) {
4405
+ this.dragScrollEdges.set(axis, { scroller, direction, since: now });
4406
+ continue;
4407
+ }
4408
+ if (now - edge.since < 180) continue;
4409
+ if (!this.dragScrollOrigins.has(scroller)) this.dragScrollOrigins.set(scroller, { top: scroller.scrollTop, left: scroller.scrollLeft });
4410
+ const before = axis === "x" ? scroller.scrollLeft : scroller.scrollTop;
4411
+ if (axis === "x") scroller.scrollLeft += delta;
4412
+ else scroller.scrollTop += delta;
4413
+ moved ||= before !== (axis === "x" ? scroller.scrollLeft : scroller.scrollTop);
4414
+ }
4415
+ } else this.dragScrollEdges.clear();
4416
+ if (moved) this.track(session, pointer);
4417
+ this.dragScrollFrame = this.win.requestAnimationFrame(tick);
4418
+ };
4419
+ this.dragScrollFrame = this.win.requestAnimationFrame(tick);
4176
4420
  }
4421
+ onTouchStart = (ev) => {
4422
+ const first = ev.touches[0];
4423
+ const second = ev.touches[1];
4424
+ if (!first || !second) {
4425
+ this.endPinch();
4426
+ return;
4427
+ }
4428
+ const view = this.current;
4429
+ if (this.session?.active || !this.opts.pinchZoom || !(view instanceof TimeGridView)) return;
4430
+ const body = first.target.closest(".tnc-tg-body");
4431
+ if (!body?.contains(second.target)) return;
4432
+ this.endSession(null, true);
4433
+ if (this.pinch) return;
4434
+ const { top, height } = body.getBoundingClientRect();
4435
+ const y = (first.clientY + second.clientY) / 2;
4436
+ this.pinch = {
4437
+ body,
4438
+ distance: Math.max(1, Math.hypot(first.clientX - second.clientX, first.clientY - second.clientY)),
4439
+ y,
4440
+ anchor: (y - top) / height,
4441
+ drawn: view.drawnSlotHeight,
4442
+ option: this.opts.slotHeight
4443
+ };
4444
+ };
4177
4445
  onTouchMove = (ev) => {
4178
4446
  if (this.session?.active) ev.preventDefault();
4447
+ const p = this.pinch;
4448
+ const first = ev.touches[0];
4449
+ const second = ev.touches[1];
4450
+ if (!p || !first || !second) return;
4451
+ if (!ev.cancelable) {
4452
+ this.endPinch();
4453
+ return;
4454
+ }
4455
+ ev.preventDefault();
4456
+ const height = p.drawn * Math.hypot(first.clientX - second.clientX, first.clientY - second.clientY) / p.distance;
4457
+ p.y = (first.clientY + second.clientY) / 2;
4458
+ this.pinchTo(p, Math.min(Math.max(p.drawn, PINCH_MAX_SLOT), Math.max(Math.min(p.drawn, PINCH_MIN_SLOT), height)));
4459
+ };
4460
+ onTouchEnd = (ev) => {
4461
+ if (ev.touches.length < 2) this.endPinch();
4179
4462
  };
4463
+ /** Rescales the grid, keeping the anchored time under the fingers. */
4464
+ pinchTo(p, slotHeight) {
4465
+ this.opts = { ...this.opts, slotHeight };
4466
+ if (!(this.current instanceof TimeGridView)) return;
4467
+ this.current.applyScale();
4468
+ const { top, height } = p.body.getBoundingClientRect();
4469
+ const scroller = scrollerOf(p.body, "y");
4470
+ if (scroller) scroller.scrollTop += top + height * p.anchor - p.y;
4471
+ }
4472
+ endPinch() {
4473
+ const p = this.pinch;
4474
+ this.pinch = null;
4475
+ if (!p?.body.isConnected) return;
4476
+ const settled = Math.round(this.opts.slotHeight);
4477
+ const slotHeight = settled === Math.round(p.drawn) ? p.option : settled;
4478
+ this.pinchTo(p, slotHeight);
4479
+ this.current?.render(null);
4480
+ if (slotHeight !== p.option) this.opts.onSlotHeightChange?.(slotHeight);
4481
+ }
4180
4482
  onPointerMove = (ev) => {
4181
4483
  const s = this.session;
4182
4484
  if (!s || ev.pointerId !== s.pointerId) return;
@@ -4202,6 +4504,12 @@ var Calendar = class {
4202
4504
  this.track(s, ev);
4203
4505
  };
4204
4506
  onPointerUp = (ev) => {
4507
+ if (ev.pointerId === this.cancelledPointerId) {
4508
+ this.cancelledPointerId = null;
4509
+ this.suppressNextClick();
4510
+ this.listenDoc(false);
4511
+ return;
4512
+ }
4205
4513
  const s = this.session;
4206
4514
  if (!s || ev.pointerId !== s.pointerId) return;
4207
4515
  if (!s.active) {
@@ -4214,28 +4522,49 @@ var Calendar = class {
4214
4522
  return;
4215
4523
  }
4216
4524
  this.suppressNextClick();
4525
+ this.track(s, ev);
4217
4526
  this.endSession(ev, false);
4218
4527
  };
4219
4528
  onPointerCancel = (ev) => {
4529
+ if (ev.pointerId === this.cancelledPointerId) {
4530
+ this.cancelledPointerId = null;
4531
+ this.listenDoc(false);
4532
+ }
4220
4533
  if (this.session && ev.pointerId === this.session.pointerId) this.endSession(null, true);
4221
4534
  };
4222
4535
  onDocKeyDown = (ev) => {
4223
4536
  if (ev.key === "Escape" && this.session) {
4224
4537
  ev.preventDefault();
4225
4538
  ev.stopPropagation();
4539
+ const pointerId = this.session.active ? this.session.pointerId : null;
4226
4540
  this.endSession(null, true);
4541
+ if (pointerId !== null) {
4542
+ this.cancelledPointerId = pointerId;
4543
+ this.listenDoc(true);
4544
+ }
4227
4545
  }
4228
4546
  };
4229
4547
  /** Proposal for the current pointer position. */
4230
4548
  track(s, ev) {
4549
+ if (this.listDragPreview) {
4550
+ const preview = this.listDragPreview;
4551
+ const bounds = this.root.getBoundingClientRect();
4552
+ preview.element.style.left = `${Math.max(8, Math.min(this.win.innerWidth - preview.width - 8, ev.clientX - preview.offsetX)) - bounds.left}px`;
4553
+ preview.element.style.top = `${Math.max(8, Math.min(this.win.innerHeight - preview.height - 8, ev.clientY - preview.offsetY)) - bounds.top}px`;
4554
+ }
4231
4555
  const hit = this.hitAt(ev.clientX, ev.clientY);
4232
4556
  if (s.mode === "external") {
4233
4557
  this.trackExternal(s, hit);
4234
4558
  return;
4235
4559
  }
4236
4560
  if (!s.grab) s.grab = hit;
4237
- if (!hit || !s.grab) return;
4561
+ if (!hit || !s.grab) {
4562
+ this.invalidateProposal(s);
4563
+ return;
4564
+ }
4238
4565
  if (s.mode === "select") {
4566
+ s.denied = false;
4567
+ this.root.classList.remove("tnc-drop-denied");
4239
4568
  const sel = this.selectionFor(s.grab, hit);
4240
4569
  if (!sel) return;
4241
4570
  this.selection = sel;
@@ -4244,25 +4573,40 @@ var Calendar = class {
4244
4573
  }
4245
4574
  const origin = s.origin;
4246
4575
  const p = s.mode === "move" ? this.moveProposal(origin, s.grab, hit) : this.resizeProposal(origin, hit, s.mode === "resize-start");
4247
- if (!p) return;
4576
+ if (!p) {
4577
+ this.invalidateProposal(s);
4578
+ return;
4579
+ }
4248
4580
  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;
4249
4581
  s.denied = !allowed;
4250
4582
  this.root.classList.toggle("tnc-drop-denied", !allowed);
4251
- if (!allowed || sameProposal(p, s.proposal)) return;
4583
+ if (!allowed) {
4584
+ this.invalidateProposal(s);
4585
+ return;
4586
+ }
4587
+ if (sameProposal(p, s.proposal)) return;
4252
4588
  s.proposal = p;
4253
4589
  this.dragRow = p.rowId !== void 0 ? { id: origin.id, rowId: p.rowId } : null;
4254
- this.applyChange(this.store.upsert([this.withProposal(origin, p)]));
4590
+ if (this.opts.view !== "list") this.applyChange(this.store.upsert([this.withProposal(origin, p)]));
4591
+ }
4592
+ invalidateProposal(s) {
4593
+ const hadProposal = s.proposal !== null;
4594
+ s.proposal = null;
4595
+ s.hit = null;
4596
+ s.denied = true;
4597
+ this.dragRow = null;
4598
+ this.root.classList.add("tnc-drop-denied");
4599
+ this.strip?.markTarget(null);
4600
+ this.unselectSilently();
4601
+ if (s.mode === "external") this.applyChange(this.store.remove([EXTERNAL_ID]));
4602
+ else if (hadProposal && s.origin) this.applyChange(this.store.upsert([s.origin.item]));
4255
4603
  }
4256
4604
  trackExternal(s, raw) {
4257
4605
  const src = s.source;
4258
4606
  const hit = raw?.kind === "timeline" && raw.area !== "track" && raw.area !== "header" ? null : raw;
4259
4607
  s.hit = hit;
4260
4608
  if (!hit) {
4261
- if (s.proposal) {
4262
- s.proposal = null;
4263
- this.applyChange(this.store.remove([EXTERNAL_ID]));
4264
- }
4265
- this.unselectSilently();
4609
+ this.invalidateProposal(s);
4266
4610
  return;
4267
4611
  }
4268
4612
  const dur = src.durationMinutes ?? this.opts.defaultTimedMinutes;
@@ -4281,6 +4625,10 @@ var Calendar = class {
4281
4625
  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;
4282
4626
  s.denied = !allowed;
4283
4627
  this.root.classList.toggle("tnc-drop-denied", !allowed);
4628
+ if (!allowed) {
4629
+ this.invalidateProposal(s);
4630
+ return;
4631
+ }
4284
4632
  if (sameProposal(p, s.proposal)) return;
4285
4633
  s.proposal = p;
4286
4634
  if (hit.kind === "timeline" && p.rowId === void 0) {
@@ -4473,7 +4821,19 @@ var Calendar = class {
4473
4821
  endSession(ev, cancel) {
4474
4822
  const s = this.session;
4475
4823
  if (!s) return;
4824
+ if (this.dragScrollFrame !== null) this.win.cancelAnimationFrame(this.dragScrollFrame);
4825
+ this.dragScrollFrame = null;
4826
+ if (cancel || s.denied) for (const [scroller, position] of this.dragScrollOrigins) {
4827
+ scroller.scrollTop = position.top;
4828
+ scroller.scrollLeft = position.left;
4829
+ }
4830
+ this.dragScrollOrigins.clear();
4831
+ this.dragScrollEdges.clear();
4476
4832
  this.session = null;
4833
+ this.listDragPreview?.element.remove();
4834
+ this.listDragPreview = null;
4835
+ for (const source of this.dragSources) source.remove();
4836
+ this.dragSources = [];
4477
4837
  if (s.timer !== null) this.win.clearTimeout(s.timer);
4478
4838
  this.listenDoc(false);
4479
4839
  this.root.classList.remove(
@@ -4487,10 +4847,15 @@ var Calendar = class {
4487
4847
  );
4488
4848
  const draggedId = this.draggingId;
4489
4849
  this.draggingId = null;
4850
+ this.dragWidth = null;
4490
4851
  this.dragRow = null;
4491
- if (draggedId) for (const el of this.elementsFor(draggedId)) el.classList.remove("is-dragging");
4852
+ if (draggedId) for (const el of this.elementsFor(draggedId)) {
4853
+ el.classList.remove("is-dragging");
4854
+ el.style.removeProperty("--tnc-drag-width");
4855
+ }
4492
4856
  this.strip?.markTarget(null);
4493
4857
  if (!s.active) return;
4858
+ if (this.current instanceof DayGridView) this.current.render(null);
4494
4859
  if (s.mode === "select") {
4495
4860
  if (cancel) this.unselectSilently();
4496
4861
  else if (this.selection) this.fireSelect(this.selection, ev);
@@ -4542,10 +4907,16 @@ var Calendar = class {
4542
4907
  revert();
4543
4908
  return;
4544
4909
  }
4910
+ if (this.opts.view === "list") this.applyChange(this.store.upsert([this.withProposal(origin, p)]));
4545
4911
  this.flush();
4546
4912
  this.commit(s.mode === "move" ? "drop" : "resize", origin, p, ev, revert);
4547
4913
  }
4548
4914
  commit(kind, origin, p, ev, revert) {
4915
+ let reverted = false;
4916
+ const mark = (state) => {
4917
+ this.itemWriteStates.set(origin.id, state);
4918
+ for (const element of this.elementsFor(origin.id)) this.applyItemWriteState(element, origin.id);
4919
+ };
4549
4920
  const arg = {
4550
4921
  item: origin.item,
4551
4922
  start: new Date(p.startMs),
@@ -4561,19 +4932,32 @@ var Calendar = class {
4561
4932
  jsEvent: ev,
4562
4933
  source: ev && "key" in ev ? "keyboard" : "pointer",
4563
4934
  view: this.view,
4564
- revert
4935
+ revert: () => {
4936
+ reverted = true;
4937
+ revert();
4938
+ mark("failed");
4939
+ }
4565
4940
  };
4566
4941
  const handler = kind === "drop" ? this.opts.onItemDrop : this.opts.onItemResize;
4567
4942
  try {
4943
+ mark("pending");
4568
4944
  const res = handler?.(arg);
4569
4945
  if (res && typeof res.then === "function") {
4570
- Promise.resolve(res).catch(() => revert());
4571
- }
4946
+ void Promise.resolve(res).then(() => {
4947
+ if (!reverted) mark("changed");
4948
+ }, () => arg.revert());
4949
+ } else if (!reverted) mark("changed");
4572
4950
  } catch (err) {
4573
- revert();
4951
+ arg.revert();
4574
4952
  throw err;
4575
4953
  }
4576
4954
  }
4955
+ applyItemWriteState(element, id) {
4956
+ const state = this.itemWriteStates.get(id);
4957
+ for (const name of ["pending", "failed", "changed"]) element.classList.toggle(`is-write-${name}`, state === name);
4958
+ if (state === "pending") element.setAttribute("aria-busy", "true");
4959
+ else element.removeAttribute("aria-busy");
4960
+ }
4577
4961
  /* --------------------------------------------------------- click / keys */
4578
4962
  /** Runs in the capture phase so the click ending a drag never reaches content inside the calendar, React portals included. */
4579
4963
  onClickCapture = (ev) => {
@@ -4583,6 +4967,12 @@ var Calendar = class {
4583
4967
  ev.preventDefault();
4584
4968
  };
4585
4969
  onClick = (ev) => {
4970
+ const compactDay = ev.target.closest?.(".tnc-dg--compact .tnc-dg-day");
4971
+ if (compactDay && this.root.contains(compactDay) && !ev.target.closest(".tnc-navlink")) {
4972
+ const day = parseDay(compactDay.dataset.date ?? "");
4973
+ if (day !== null) this.openMore(day, compactDay, ev);
4974
+ return;
4975
+ }
4586
4976
  const yearDay = ev.target.closest?.(".tnc-ym-day");
4587
4977
  if (yearDay && this.root.contains(yearDay)) {
4588
4978
  const day = parseDay(yearDay.dataset.date ?? "");
@@ -4671,7 +5061,8 @@ var Calendar = class {
4671
5061
  const day = parseDay(target.dataset.date ?? "");
4672
5062
  if (day !== null) {
4673
5063
  ev.preventDefault();
4674
- this.clickSlot({ kind: "day", day }, ev, true);
5064
+ if (target.closest(".tnc-dg--compact")) this.openMore(day, target, ev);
5065
+ else this.clickSlot({ kind: "day", day }, ev, true);
4675
5066
  }
4676
5067
  return;
4677
5068
  }
@@ -4759,9 +5150,13 @@ var Calendar = class {
4759
5150
  const dt = ev.dataTransfer;
4760
5151
  if (!dt || !this.opts.acceptNativeDrop?.(dt) || !this.current) return;
4761
5152
  ev.preventDefault();
5153
+ const hit = this.hitAt(ev.clientX, ev.clientY);
5154
+ if (!hit || hit.kind === "timeline" && hit.area !== "track" && hit.area !== "header") {
5155
+ dt.dropEffect = "none";
5156
+ this.unselectSilently();
5157
+ return;
5158
+ }
4762
5159
  dt.dropEffect = "copy";
4763
- const hit = this.current.hit(ev.clientX, ev.clientY);
4764
- if (!hit || hit.kind === "timeline" && hit.area !== "track" && hit.area !== "header") return;
4765
5160
  const sel = this.selectionFor(hit, hit);
4766
5161
  if (!sel) return;
4767
5162
  this.selection = sel;
@@ -4775,7 +5170,7 @@ var Calendar = class {
4775
5170
  const dt = ev.dataTransfer;
4776
5171
  if (!dt || !this.opts.acceptNativeDrop?.(dt) || !this.current) return;
4777
5172
  ev.preventDefault();
4778
- const hit = this.current.hit(ev.clientX, ev.clientY);
5173
+ const hit = this.hitAt(ev.clientX, ev.clientY);
4779
5174
  this.unselectSilently();
4780
5175
  if (!hit || hit.kind === "timeline" && hit.area !== "track" && hit.area !== "header") return;
4781
5176
  const sel = this.selectionFor(hit, hit);
@@ -4806,7 +5201,7 @@ function scrollerOf(el, axis) {
4806
5201
  return doc.scrollingElement;
4807
5202
  }
4808
5203
  function rangeMoved(prev, next) {
4809
- return !prev || prev.type !== next.type || prev.range.startDay !== next.range.startDay || prev.range.endDay !== next.range.endDay || prev.dataRange.endDay !== next.dataRange.endDay;
5204
+ 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;
4810
5205
  }
4811
5206
 
4812
5207
  // src/tasknotes/bases.ts
@@ -4849,6 +5244,8 @@ var BASES_CALENDAR_DEFAULTS = {
4849
5244
  slotDuration: "00:30:00",
4850
5245
  snapDuration: "00:30:00",
4851
5246
  scrollTime: "08:00:00",
5247
+ slotHeight: 24,
5248
+ pinchZoom: true,
4852
5249
  nowIndicator: true,
4853
5250
  showTimeGrid: true,
4854
5251
  selectMirror: true,
@@ -4913,6 +5310,8 @@ function basesLayoutOptions(s, context = {}) {
4913
5310
  slotDuration,
4914
5311
  snapDuration: time("snapDuration") ?? slotDuration,
4915
5312
  scrollTime: time("scrollTime", 48) ?? d.scrollTime,
5313
+ slotHeight: clamp(Math.round(num(s.slotHeight, d.slotHeight)), 5, 96),
5314
+ pinchZoom: pick("pinchZoom") !== false,
4916
5315
  nowIndicator: pick("nowIndicator") !== false,
4917
5316
  timeGrid: showTimeGrid,
4918
5317
  selectMirror: pick("selectMirror") !== false,