@tasknotes/calendar 0.1.8 → 0.1.11

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.
@@ -502,43 +502,44 @@ function segmentInWindow(n, day, minMinute, maxMinute, zone) {
502
502
  };
503
503
  }
504
504
  var defaultOrder = (a, b) => a.startMs - b.startMs || b.endMs - b.startMs - (a.endMs - a.startMs) || Number(b.allDay) - Number(a.allDay) || a.item.title.localeCompare(b.item.title);
505
+ function collides(a, b, minMinutes) {
506
+ if (b.startMin < a.endMin) return true;
507
+ if (b.startMin === a.endMin && a.endMin > a.startMin) return false;
508
+ return b.startMin < a.startMin + minMinutes;
509
+ }
510
+ function overlaps(a, b, minMinutes) {
511
+ return a.startMin <= b.startMin ? collides(a, b, minMinutes) : collides(b, a, minMinutes);
512
+ }
505
513
  function packTimed(segs, minMinutes, order = defaultOrder) {
506
- const sorted = segs.map((s) => ({ ...s, end: Math.max(s.endMin, s.startMin + minMinutes) })).sort((a, b) => a.startMin - b.startMin || b.end - a.end || order(a.item, b.item));
514
+ const sorted = [...segs].sort((a, b) => a.startMin - b.startMin || Math.max(b.endMin, b.startMin + minMinutes) - Math.max(a.endMin, a.startMin + minMinutes) || order(a.item, b.item));
507
515
  const out = [];
508
516
  let cluster = [];
509
- let colEnds = [];
510
- let clusterEnd = -Infinity;
517
+ let colLast = [];
511
518
  let clusterIndex = 0;
512
519
  const flush = () => {
513
520
  if (cluster.length) clusterIndex++;
514
- const cols = colEnds.length;
521
+ const cols = colLast.length;
515
522
  for (const p of cluster) {
516
523
  p.cols = cols;
517
524
  let span = 1;
518
- while (p.col + span < cols && !cluster.some((o) => o.col === p.col + span && o.startMin < p.end && p.startMin < o.end)) span++;
525
+ while (p.col + span < cols && !cluster.some((o) => o.col === p.col + span && overlaps(o, p, minMinutes))) span++;
519
526
  p.span = span;
520
527
  }
521
528
  cluster = [];
522
- colEnds = [];
529
+ colLast = [];
523
530
  };
524
531
  for (const s of sorted) {
525
- if (s.startMin >= clusterEnd) {
526
- flush();
527
- clusterEnd = -Infinity;
528
- }
529
- let col = colEnds.findIndex((end) => end <= s.startMin);
530
- if (col < 0) {
531
- col = colEnds.length;
532
- colEnds.push(0);
533
- }
534
- colEnds[col] = s.end;
532
+ if (!cluster.some((o) => collides(o, s, minMinutes))) flush();
533
+ let col = colLast.findIndex((last) => !collides(last, s, minMinutes));
534
+ if (col < 0) col = colLast.length;
535
535
  const p = { ...s, col, cols: 1, span: 1, cluster: clusterIndex, split: false };
536
+ colLast[col] = p;
536
537
  cluster.push(p);
537
538
  out.push(p);
538
- clusterEnd = Math.max(clusterEnd, s.end);
539
539
  }
540
540
  flush();
541
- for (const p of out) delete p.end;
541
+ const starts = new Set(out.map((p) => p.startMin));
542
+ for (const p of out) if (p.endMin > p.startMin && p.startMin + minMinutes > p.endMin && starts.has(p.endMin)) p.abutted = true;
542
543
  return out;
543
544
  }
544
545
  function layoutTimedDay(items, day, zone, minMinutes, order, window = [0, MINUTES_PER_DAY]) {
@@ -560,15 +561,8 @@ function layoutTimedDay(items, day, zone, minMinutes, order, window = [0, MINUTE
560
561
  }
561
562
  function markSplit(lane, other, minMinutes) {
562
563
  if (!lane.length || !other.length) return;
563
- const end = (p) => Math.max(p.endMin, p.startMin + minMinutes);
564
- const ranges = /* @__PURE__ */ new Map();
565
- for (const p of lane) {
566
- const r = ranges.get(p.cluster);
567
- if (r) r[1] = Math.max(r[1], end(p));
568
- else ranges.set(p.cluster, [p.startMin, end(p)]);
569
- }
570
564
  const split = /* @__PURE__ */ new Set();
571
- for (const [cluster, [s, e]] of ranges) if (other.some((o) => o.startMin < e && s < end(o))) split.add(cluster);
565
+ for (const p of lane) if (!split.has(p.cluster) && other.some((o) => overlaps(p, o, minMinutes))) split.add(p.cluster);
572
566
  for (const p of lane) p.split = split.has(p.cluster);
573
567
  }
574
568
  function layoutRow(items, days, order = defaultOrder) {
@@ -832,14 +826,16 @@ function pct(n) {
832
826
 
833
827
  // src/dom/format.ts
834
828
  var Formatter = class {
835
- cache = /* @__PURE__ */ new Map();
836
- locale;
837
- hour12;
838
- constructor(locale, hour12) {
829
+ constructor(locale, hour12, formatTime) {
830
+ this.formatTime = formatTime;
839
831
  this.locale = locale;
840
832
  const probe = new Intl.DateTimeFormat(locale, { hour: "numeric" }).resolvedOptions();
841
833
  this.hour12 = hour12 ?? (probe.hour12 === true || probe.hourCycle === "h12" || probe.hourCycle === "h11");
842
834
  }
835
+ formatTime;
836
+ cache = /* @__PURE__ */ new Map();
837
+ locale;
838
+ hour12;
843
839
  fmt(key, opts) {
844
840
  let f = this.cache.get(key);
845
841
  if (!f) {
@@ -897,6 +893,7 @@ var Formatter = class {
897
893
  /** Wall-clock minutes to a label. `compact` drops ":00" in 12-hour formats ("9am"). */
898
894
  time(minute, compact = false) {
899
895
  const m = (Math.round(minute) % 1440 + 1440) % 1440;
896
+ if (this.formatTime) return this.formatTime(m);
900
897
  const date = new Date(Date.UTC(2e3, 0, 1, Math.floor(m / 60), m % 60));
901
898
  if (this.hour12) {
902
899
  const parts = this.fmt("t12", { hour: "numeric", minute: "2-digit", hour12: true }).formatToParts(date);
@@ -988,6 +985,7 @@ function defaultOptions() {
988
985
  snapDuration: void 0,
989
986
  scrollTime: "08:00",
990
987
  slotHeight: 24,
988
+ pinchZoom: true,
991
989
  expandRows: false,
992
990
  nowIndicator: true,
993
991
  selectMirror: true,
@@ -1029,6 +1027,7 @@ var LAYOUT_KEYS = /* @__PURE__ */ new Set([
1029
1027
  "locale",
1030
1028
  "firstDay",
1031
1029
  "hour12",
1030
+ "formatTime",
1032
1031
  "weekNumbers",
1033
1032
  "weekends",
1034
1033
  "hiddenDays",
@@ -1041,9 +1040,11 @@ var LAYOUT_KEYS = /* @__PURE__ */ new Set([
1041
1040
  "slotDuration",
1042
1041
  "snapDuration",
1043
1042
  "slotHeight",
1043
+ "pinchZoom",
1044
1044
  "expandRows",
1045
1045
  "nowIndicator",
1046
1046
  "slotEventOverlap",
1047
+ "overlapStackThreshold",
1047
1048
  "eventMinHeight",
1048
1049
  "eventMaxStack",
1049
1050
  "dayMaxEvents",
@@ -1158,6 +1159,7 @@ var DayGridView = class {
1158
1159
  topPx = 24;
1159
1160
  lastHeights = "";
1160
1161
  dotsMode;
1162
+ dragPreviews = [];
1161
1163
  daysPerWeek() {
1162
1164
  const { days } = this.ctx;
1163
1165
  const first = days[0];
@@ -1171,6 +1173,36 @@ var DayGridView = class {
1171
1173
  }
1172
1174
  render(_change) {
1173
1175
  const { ctx } = this;
1176
+ this.clearDragPreviews();
1177
+ const dragged = ctx.draggingId ? ctx.store.get(ctx.draggingId) : void 0;
1178
+ const source = dragged ? this.elementsFor(dragged.id)[0] : void 0;
1179
+ if (dragged && source && this.el.closest(".tnc-dragging-move")) {
1180
+ const proposals = [dragged, ...[...ctx.store.all()].filter((item) => item.id !== dragged.id && item.item.preview)];
1181
+ for (const proposal of proposals) {
1182
+ const original = this.elementsFor(proposal.id)[0] ?? source;
1183
+ const bounds = original.getBoundingClientRect();
1184
+ const sourceWeek = original.closest(".tnc-dg-week");
1185
+ const offset = bounds.top - sourceWeek.getBoundingClientRect().top;
1186
+ for (const week of this.weeks) {
1187
+ const start = week.days.findIndex((day) => day >= proposal.startDay && day < proposal.endDay);
1188
+ if (start < 0) continue;
1189
+ let end = start + 1;
1190
+ while (!this.dotsMode && end < week.days.length && week.days[end] < proposal.endDay) end++;
1191
+ 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 });
1192
+ preview.classList.add("tnc-month-preview");
1193
+ preview.inert = true;
1194
+ setStyle(preview, {
1195
+ left: `calc(${pct(start / week.days.length)} + 2px)`,
1196
+ width: `calc(${pct((end - start) / week.days.length)} - 4px)`,
1197
+ top: `${Math.max(this.topPx, Math.min(offset, week.row.clientHeight - bounds.height - 4))}px`,
1198
+ height: `${bounds.height}px`
1199
+ });
1200
+ week.layer.append(preview);
1201
+ this.dragPreviews.push(preview);
1202
+ }
1203
+ }
1204
+ return;
1205
+ }
1174
1206
  this.dotsMode = ctx.opts.monthDisplay !== "full" || this.grid.clientWidth / this.daysPerWeek() < 72;
1175
1207
  this.el.classList.toggle("tnc-dg--dots", this.dotsMode);
1176
1208
  this.el.classList.toggle("tnc-dg--compact", this.dotsMode && ctx.opts.monthDisplay === "full");
@@ -1202,6 +1234,10 @@ var DayGridView = class {
1202
1234
  }
1203
1235
  }
1204
1236
  }
1237
+ clearDragPreviews() {
1238
+ for (const preview of this.dragPreviews) this.ctx.destroyItemEl(preview);
1239
+ this.dragPreviews = [];
1240
+ }
1205
1241
  /** Height of a "+N more" link as the host styles it (`--tnc-more-h`, or a taller min-height). */
1206
1242
  measureMore(cs) {
1207
1243
  const fallback = px(cs, "--tnc-more-h", this.barPx);
@@ -1393,6 +1429,7 @@ var DayGridView = class {
1393
1429
  return this.weeks.flatMap((w) => [...w.pool.elementsFor(id), ...(w.dots ?? []).flatMap((c) => c.pool.elementsFor(id))]);
1394
1430
  }
1395
1431
  destroy() {
1432
+ this.clearDragPreviews();
1396
1433
  this.resizeObs?.disconnect();
1397
1434
  for (const w of this.weeks) {
1398
1435
  w.pool.clear();
@@ -1445,6 +1482,7 @@ var ListView = class {
1445
1482
  frame = null;
1446
1483
  /** A picked day stays marked until the list is scrolled by hand. */
1447
1484
  pinned = false;
1485
+ pinnedTop = 0;
1448
1486
  itemsOn(day) {
1449
1487
  const { store, opts } = this.ctx;
1450
1488
  const out = store.spanningIn(day, day + 1, true);
@@ -1576,6 +1614,7 @@ var ListView = class {
1576
1614
  this.scroller.scrollTop += section.getBoundingClientRect().top - this.scroller.getBoundingClientRect().top;
1577
1615
  this.ctx.strip?.setFocus(day);
1578
1616
  this.pinned = true;
1617
+ this.pinnedTop = this.scroller.scrollTop;
1579
1618
  }
1580
1619
  fold = () => {
1581
1620
  this.pinned = false;
@@ -1583,6 +1622,7 @@ var ListView = class {
1583
1622
  if (strip?.expanded) strip.setExpanded(false);
1584
1623
  };
1585
1624
  onScroll = () => {
1625
+ if (this.pinned && Math.abs(this.scroller.scrollTop - this.pinnedTop) > 1) this.pinned = false;
1586
1626
  if (this.frame !== null) return;
1587
1627
  this.frame = this.ctx.win.requestAnimationFrame(() => {
1588
1628
  this.frame = null;
@@ -1593,7 +1633,7 @@ var ListView = class {
1593
1633
  followTop() {
1594
1634
  const strip = this.ctx.strip;
1595
1635
  if (!strip || this.pinned) return;
1596
- const edge = strip.el.getBoundingClientRect().bottom + 1;
1636
+ const edge = Math.max(strip.el.getBoundingClientRect().bottom, this.scroller.getBoundingClientRect().top) + 1;
1597
1637
  for (const day of this.ctx.days) {
1598
1638
  const section = this.sections.get(day);
1599
1639
  if (section && section.getBoundingClientRect().bottom > edge) {
@@ -1641,10 +1681,11 @@ var ListView = class {
1641
1681
  // src/dom/views/strip.ts
1642
1682
  var SWIPE_PX = 40;
1643
1683
  var DateStrip = class {
1644
- constructor(ctx, pick, focus, onShown) {
1684
+ constructor(ctx, pick, focus, onShown, onPage) {
1645
1685
  this.ctx = ctx;
1646
1686
  this.pick = pick;
1647
1687
  this.onShown = onShown;
1688
+ this.onPage = onPage;
1648
1689
  const { doc, text } = ctx;
1649
1690
  this.focus = focus;
1650
1691
  this.shown = startOfWeek(focus, ctx.firstDay);
@@ -1668,6 +1709,10 @@ var DateStrip = class {
1668
1709
  this.grid.addEventListener("keydown", this.onKeyDown);
1669
1710
  this.el.addEventListener("pointerdown", this.onPointerDown);
1670
1711
  this.el.addEventListener("pointerup", this.onPointerUp);
1712
+ this.el.addEventListener("touchstart", this.ownTouch, { passive: true });
1713
+ this.el.addEventListener("touchmove", this.ownTouch, { passive: true });
1714
+ this.el.addEventListener("touchend", this.ownTouch, { passive: true });
1715
+ this.el.addEventListener("touchcancel", this.ownTouch, { passive: true });
1671
1716
  this.el.addEventListener("pointercancel", this.onPointerCancel);
1672
1717
  this.el.addEventListener("click", this.onClickCapture, true);
1673
1718
  this.draw();
@@ -1676,6 +1721,7 @@ var DateStrip = class {
1676
1721
  ctx;
1677
1722
  pick;
1678
1723
  onShown;
1724
+ onPage;
1679
1725
  el;
1680
1726
  title;
1681
1727
  dows;
@@ -1734,6 +1780,7 @@ var DateStrip = class {
1734
1780
  step(dir) {
1735
1781
  this.shown = this.expanded ? addMonths(this.shown, dir) : this.shown + 7 * dir;
1736
1782
  this.draw();
1783
+ this.onPage?.(this.expanded ? this.shown : this.focus + 7 * dir);
1737
1784
  }
1738
1785
  days() {
1739
1786
  if (!this.expanded) return Array.from({ length: 7 }, (_, i) => this.shown + i);
@@ -1758,7 +1805,8 @@ var DateStrip = class {
1758
1805
  const tabDay = days.includes(this.focus) ? this.focus : days[0];
1759
1806
  const cells = days.map((d) => {
1760
1807
  const arg = ctx.dayArg(d);
1761
- const count = store.inRange(d, d + 1).filter((n) => n.item.display !== "background" && n.item.display !== "none").length;
1808
+ const items = store.inRange(d, d + 1).filter((n) => n.item.display !== "background" && n.item.display !== "none");
1809
+ const count = items.length;
1762
1810
  const first = ymdOfDay(d).day === 1 && d !== days[0] && !this.expanded;
1763
1811
  const label = fmt.longDate(d);
1764
1812
  const cell = h(
@@ -1782,7 +1830,15 @@ var DateStrip = class {
1782
1830
  }
1783
1831
  );
1784
1832
  if (first) cell.append(h(doc, "span", "tnc-strip-month", { "aria-hidden": "true" }, fmt.monthShort(d)));
1785
- cell.append(h(doc, "span", "tnc-strip-num", null, fmt.dayNumber(d)), h(doc, "span", "tnc-strip-dot", { "aria-hidden": "true" }));
1833
+ cell.append(h(doc, "span", "tnc-strip-num", null, fmt.dayNumber(d)));
1834
+ const dots = h(doc, "span", "tnc-strip-dots", { "aria-hidden": "true" });
1835
+ for (const item of items.slice(0, 3)) {
1836
+ const dot = h(doc, "span", "tnc-strip-dot");
1837
+ const color = item.item.color || item.item.borderColor || item.item.backgroundColor;
1838
+ if (color) dot.style.backgroundColor = color;
1839
+ dots.append(dot);
1840
+ }
1841
+ cell.append(dots);
1786
1842
  return cell;
1787
1843
  });
1788
1844
  this.grid.replaceChildren(...cells);
@@ -1822,6 +1878,9 @@ var DateStrip = class {
1822
1878
  if (ev.button !== 0 || this.ctx.draggingId) return;
1823
1879
  this.swipe = { id: ev.pointerId, x: ev.clientX, y: ev.clientY };
1824
1880
  };
1881
+ ownTouch = (event) => {
1882
+ event.stopPropagation();
1883
+ };
1825
1884
  onPointerUp = (ev) => {
1826
1885
  const s = this.swipe;
1827
1886
  this.swipe = null;
@@ -1861,6 +1920,7 @@ var TimeGridView = class {
1861
1920
  const showGrid = opts.timeGrid;
1862
1921
  this.el = h(doc, "div", "tnc-tg", { role: "table", "aria-label": ctx.view.title });
1863
1922
  this.el.classList.toggle("tnc-tg--no-grid", !showGrid);
1923
+ this.el.classList.toggle("tnc-tg--pinch", showGrid && opts.pinchZoom);
1864
1924
  this.scroller = h(doc, "div", "tnc-tg-scroll");
1865
1925
  this.dayNavigation = h(doc, "div", "tnc-tg-day-navigation", { role: "group", "aria-label": ctx.view.title });
1866
1926
  this.scroller.addEventListener("scroll", () => this.updateDayNavigation());
@@ -2010,6 +2070,11 @@ var TimeGridView = class {
2010
2070
  }
2011
2071
  if (this.nowGutter) gutter.append(this.nowGutter);
2012
2072
  }
2073
+ /** Slot height as drawn, which `expandRows` can stretch past the option. */
2074
+ get drawnSlotHeight() {
2075
+ return this.ctx.slot.duration * this.ppm;
2076
+ }
2077
+ /** Applies `slotHeight`. Items are placed in percentages, so they follow without a render. */
2013
2078
  applyScale() {
2014
2079
  const { opts, slot } = this.ctx;
2015
2080
  this.ppm = opts.slotHeight / slot.duration;
@@ -2190,15 +2255,17 @@ var TimeGridView = class {
2190
2255
  const { opts } = ctx;
2191
2256
  const readableWidth = Number.parseFloat(ctx.win.getComputedStyle(this.el).getPropertyValue("--tnc-event-min-width")) || 128;
2192
2257
  const laneWidth = c.col.clientWidth * (lane === "actual" && placements.some((placement) => placement.split) ? 0.22 : 1);
2193
- const collapse = placements.some((placement) => {
2258
+ const overlapThreshold = opts.overlapStackThreshold ?? 0;
2259
+ const collapsedClusters = new Set(placements.filter((placement) => {
2194
2260
  const columns = opts.eventMaxStack > 0 ? Math.min(placement.cols, opts.eventMaxStack) : placement.cols;
2195
- return columns > 1 && columns * readableWidth > laneWidth;
2196
- });
2197
- const withMore = lane === "plan" || collapse;
2198
- const maxStack = collapse ? 1 : opts.eventMaxStack;
2261
+ const denseOverlap = overlapThreshold > 1 && placement.cols >= overlapThreshold;
2262
+ return columns > 1 && (columns * readableWidth > laneWidth || denseOverlap);
2263
+ }).map((placement) => placement.cluster));
2199
2264
  const hiddenByCluster = /* @__PURE__ */ new Map();
2200
2265
  pool.begin();
2201
2266
  for (const p of placements) {
2267
+ const withMore = lane === "plan" || collapsedClusters.has(p.cluster);
2268
+ const maxStack = collapsedClusters.has(p.cluster) ? 1 : opts.eventMaxStack;
2202
2269
  const preview = p.item.item.preview || p.item.id === ctx.draggingId;
2203
2270
  if (!preview && withMore && maxStack > 0 && p.cols > maxStack && p.col >= maxStack - 1) {
2204
2271
  const rec = hiddenByCluster.get(p.cluster) ?? { count: 0, start: p.startMin, end: p.endMin, split: p.split, items: [] };
@@ -2211,7 +2278,7 @@ var TimeGridView = class {
2211
2278
  }
2212
2279
  const cols = !preview && withMore && maxStack > 0 ? Math.min(p.cols, maxStack) : p.cols;
2213
2280
  const span = Math.min(p.span, cols - p.col);
2214
- const left = p.col / cols;
2281
+ const left = p.item.id === ctx.draggingId ? 0 : p.col / cols;
2215
2282
  let width = span / cols;
2216
2283
  if (opts.slotEventOverlap && cols > 1 && p.col + span < cols) width = Math.min(1 - left, width * 1.7);
2217
2284
  const el = pool.place(p.item.id, this.meta(p, false));
@@ -2219,15 +2286,16 @@ var TimeGridView = class {
2219
2286
  el.classList.toggle("is-short", short);
2220
2287
  setStyle(el, {
2221
2288
  top: top(p.startMin),
2222
- height: `max(${height(p)}, ${opts.eventMinHeight}px)`,
2289
+ height: p.abutted ? height(p) : `max(${height(p)}, ${opts.eventMinHeight}px)`,
2223
2290
  ...inLane(lane, p.split, left, width),
2224
2291
  "--tnc-event-readable-width": pct(span / cols / width),
2225
2292
  "z-index": String(p.col + 1)
2226
2293
  });
2227
2294
  }
2228
2295
  pool.end();
2229
- if (!withMore) return;
2230
2296
  for (const [cluster, rec] of hiddenByCluster) {
2297
+ const collapse = collapsedClusters.has(cluster);
2298
+ const maxStack = collapse ? 1 : opts.eventMaxStack;
2231
2299
  const key = `${lane}-${cluster}`;
2232
2300
  let link = c.more.querySelector(`[data-cluster="${key}"]`);
2233
2301
  if (!link) {
@@ -2393,7 +2461,9 @@ var TimeGridView = class {
2393
2461
  scrollToTime(minute) {
2394
2462
  if (!this.body) return;
2395
2463
  const y = (minute - this.ctx.slot.min) * this.ppm;
2396
- this.scroller.scrollTop = Math.max(0, y);
2464
+ const label = this.gutter?.querySelector(".tnc-slot-label");
2465
+ const inset = label ? Math.ceil(label.getBoundingClientRect().height / 2) + 2 : 0;
2466
+ this.scroller.scrollTop = Math.max(0, y - inset);
2397
2467
  }
2398
2468
  getScroll() {
2399
2469
  return { top: this.scroller.scrollTop, left: this.scroller.scrollLeft };
@@ -3057,7 +3127,7 @@ ${this.ctx.text.more(items.length - 8)}` : ""}` : label;
3057
3127
  const bounds = month.getBoundingClientRect();
3058
3128
  const viewport = this.scroller.getBoundingClientRect();
3059
3129
  if (bounds.top < viewport.top) this.scroller.scrollTop += bounds.top - viewport.top;
3060
- else if (bounds.bottom > viewport.bottom) this.scroller.scrollTop += bounds.bottom - viewport.bottom;
3130
+ else if (bounds.bottom > viewport.bottom) this.scroller.scrollTop += bounds.top - viewport.top;
3061
3131
  }
3062
3132
  elementsFor(_id) {
3063
3133
  return [];
@@ -3071,6 +3141,8 @@ ${this.ctx.text.more(items.length - 8)}` : ""}` : label;
3071
3141
  var EXTERNAL_ID = "__tnc_external__";
3072
3142
  var SVG_NS = "http://www.w3.org/2000/svg";
3073
3143
  var MAX_AGENDA_DAYS = 366;
3144
+ var PINCH_MIN_SLOT = 16;
3145
+ var PINCH_MAX_SLOT = 96;
3074
3146
  function sameOption(a, b, depth = 0) {
3075
3147
  if (a === b) return true;
3076
3148
  if (typeof a === "function" && typeof b === "function") return true;
@@ -3098,7 +3170,7 @@ var Calendar = class {
3098
3170
  this.win = this.doc.defaultView ?? globalThis;
3099
3171
  this.opts = { ...defaultOptions(), ...options };
3100
3172
  this.zone = getTimeZone(this.opts.timeZone);
3101
- this.fmt = new Formatter(this.opts.locale, this.opts.hour12);
3173
+ this.fmt = new Formatter(this.opts.locale, this.opts.hour12, this.opts.formatTime);
3102
3174
  this.text = this.resolveText();
3103
3175
  this.order = parseOrder(this.opts.eventOrder);
3104
3176
  this.store = new ItemStore(this.zone, {
@@ -3140,13 +3212,16 @@ var Calendar = class {
3140
3212
  draggingId = null;
3141
3213
  listDragPreview = null;
3142
3214
  dragSources = [];
3215
+ dragWidth = null;
3143
3216
  dragScrollFrame = null;
3144
3217
  dragScrollOrigins = /* @__PURE__ */ new Map();
3218
+ dragScrollEdges = /* @__PURE__ */ new Map();
3145
3219
  itemWriteStates = /* @__PURE__ */ new Map();
3146
3220
  dragRow = null;
3147
3221
  strip = null;
3148
3222
  stripSize = null;
3149
3223
  rebuilding = false;
3224
+ pinch = null;
3150
3225
  toolbarEl;
3151
3226
  viewHost;
3152
3227
  current = null;
@@ -3255,7 +3330,7 @@ var Calendar = class {
3255
3330
  defaultAllDayDays: this.opts.defaultAllDayDays
3256
3331
  });
3257
3332
  }
3258
- if (has("locale") || has("hour12")) this.fmt = new Formatter(this.opts.locale, this.opts.hour12);
3333
+ if (has("locale") || has("hour12") || has("formatTime")) this.fmt = new Formatter(this.opts.locale, this.opts.hour12, this.opts.formatTime);
3259
3334
  if (has("text") || has("locale")) this.text = this.resolveText();
3260
3335
  if (has("eventOrder")) this.order = parseOrder(this.opts.eventOrder);
3261
3336
  if (has("date")) this.anchor = this.toDay(this.opts.date) ?? this.anchor;
@@ -3494,7 +3569,10 @@ var Calendar = class {
3494
3569
  if (this.conflicts.has(n.id)) el.classList.add("is-conflict");
3495
3570
  const indicator = variant === "indicator";
3496
3571
  if (!preview && !indicator && (this.isEditable(n, "start", true) || this.isEditable(n, "duration", true))) el.classList.add("is-draggable");
3497
- if (this.draggingId === n.id) el.classList.add("is-dragging");
3572
+ if (this.draggingId === n.id) {
3573
+ el.classList.add("is-dragging");
3574
+ if (variant === "timed" && this.dragWidth !== null) el.style.setProperty("--tnc-drag-width", `${this.dragWidth}px`);
3575
+ }
3498
3576
  if (this.selectedIds.has(n.id) && !preview) el.classList.add("is-selected");
3499
3577
  el.classList.add(...classList(it.className));
3500
3578
  const arg = this.itemArg(el, meta);
@@ -3762,7 +3840,9 @@ var Calendar = class {
3762
3840
  this.root.classList.add("tnc--has-strip");
3763
3841
  const { startDay, endDay } = this.view.range;
3764
3842
  if (!this.strip) {
3765
- const strip = new DateStrip(this, (day, ev) => this.pickStripDay(day, ev), startDay, () => this.stripShown());
3843
+ const strip = new DateStrip(this, (day, ev) => this.pickStripDay(day, ev), startDay, () => this.stripShown(), (day) => {
3844
+ if (this.agendaList()) this.showListDay(day);
3845
+ });
3766
3846
  this.strip = strip;
3767
3847
  this.root.insertBefore(strip.el, this.viewHost);
3768
3848
  const RO = this.win.ResizeObserver;
@@ -4126,7 +4206,10 @@ var Calendar = class {
4126
4206
  r.addEventListener("dragover", this.onNativeDragOver);
4127
4207
  r.addEventListener("dragleave", this.onNativeDragLeave);
4128
4208
  r.addEventListener("drop", this.onNativeDrop);
4209
+ r.addEventListener("touchstart", this.onTouchStart, { passive: true });
4129
4210
  r.addEventListener("touchmove", this.onTouchMove, { passive: false });
4211
+ r.addEventListener("touchend", this.onTouchEnd);
4212
+ r.addEventListener("touchcancel", this.onTouchEnd);
4130
4213
  r.addEventListener("pointerover", this.onAdoptCheck);
4131
4214
  r.addEventListener("focusin", this.onAdoptCheck);
4132
4215
  this.doc.addEventListener("pointerdown", this.onDocPointerDown, true);
@@ -4145,7 +4228,10 @@ var Calendar = class {
4145
4228
  r.removeEventListener("dragover", this.onNativeDragOver);
4146
4229
  r.removeEventListener("dragleave", this.onNativeDragLeave);
4147
4230
  r.removeEventListener("drop", this.onNativeDrop);
4231
+ r.removeEventListener("touchstart", this.onTouchStart);
4148
4232
  r.removeEventListener("touchmove", this.onTouchMove);
4233
+ r.removeEventListener("touchend", this.onTouchEnd);
4234
+ r.removeEventListener("touchcancel", this.onTouchEnd);
4149
4235
  r.removeEventListener("pointerover", this.onAdoptCheck);
4150
4236
  r.removeEventListener("focusin", this.onAdoptCheck);
4151
4237
  this.doc.removeEventListener("pointerdown", this.onDocPointerDown, true);
@@ -4240,7 +4326,11 @@ var Calendar = class {
4240
4326
  if (s.origin) {
4241
4327
  this.draggingId = s.origin.id;
4242
4328
  for (const el of this.elementsFor(s.origin.id)) {
4243
- if (this.opts.view !== "list" && s.mode === "move") {
4329
+ if (this.meta.get(el)?.variant === "timed" && s.mode === "move") {
4330
+ this.dragWidth = el.getBoundingClientRect().width;
4331
+ el.style.setProperty("--tnc-drag-width", `${this.dragWidth}px`);
4332
+ }
4333
+ if (this.opts.view !== "list" && !(this.current instanceof DayGridView) && s.mode === "move") {
4244
4334
  const source = el.cloneNode(true);
4245
4335
  source.classList.remove("is-selected");
4246
4336
  source.classList.add("tnc-drag-source");
@@ -4293,24 +4383,96 @@ var Calendar = class {
4293
4383
  const coordinate = axis === "x" ? pointer.clientX : pointer.clientY;
4294
4384
  const lower = Math.max(0, axis === "x" ? viewport.left : viewport.top);
4295
4385
  const upper = Math.min(axis === "x" ? this.win.innerWidth : this.win.innerHeight, axis === "x" ? viewport.right : viewport.bottom);
4296
- if (coordinate < lower || coordinate > upper) continue;
4386
+ if (coordinate < lower || coordinate > upper) {
4387
+ this.dragScrollEdges.delete(axis);
4388
+ continue;
4389
+ }
4297
4390
  const delta = coordinate < lower + 32 ? -Math.min(12, (lower + 32 - coordinate) / 2) : coordinate > upper - 32 ? Math.min(12, (coordinate - upper + 32) / 2) : 0;
4298
- if (!delta) continue;
4391
+ if (!delta) {
4392
+ this.dragScrollEdges.delete(axis);
4393
+ continue;
4394
+ }
4395
+ const edge = this.dragScrollEdges.get(axis);
4396
+ const direction = Math.sign(delta);
4397
+ const now = this.win.performance.now();
4398
+ if (!edge || edge.scroller !== scroller || edge.direction !== direction) {
4399
+ this.dragScrollEdges.set(axis, { scroller, direction, since: now });
4400
+ continue;
4401
+ }
4402
+ if (now - edge.since < 180) continue;
4299
4403
  if (!this.dragScrollOrigins.has(scroller)) this.dragScrollOrigins.set(scroller, { top: scroller.scrollTop, left: scroller.scrollLeft });
4300
4404
  const before = axis === "x" ? scroller.scrollLeft : scroller.scrollTop;
4301
4405
  if (axis === "x") scroller.scrollLeft += delta;
4302
4406
  else scroller.scrollTop += delta;
4303
4407
  moved ||= before !== (axis === "x" ? scroller.scrollLeft : scroller.scrollTop);
4304
4408
  }
4305
- }
4409
+ } else this.dragScrollEdges.clear();
4306
4410
  if (moved) this.track(session, pointer);
4307
4411
  this.dragScrollFrame = this.win.requestAnimationFrame(tick);
4308
4412
  };
4309
4413
  this.dragScrollFrame = this.win.requestAnimationFrame(tick);
4310
4414
  }
4415
+ onTouchStart = (ev) => {
4416
+ const first = ev.touches[0];
4417
+ const second = ev.touches[1];
4418
+ if (!first || !second) {
4419
+ this.endPinch();
4420
+ return;
4421
+ }
4422
+ const view = this.current;
4423
+ if (this.session?.active || !this.opts.pinchZoom || !(view instanceof TimeGridView)) return;
4424
+ const body = first.target.closest(".tnc-tg-body");
4425
+ if (!body?.contains(second.target)) return;
4426
+ this.endSession(null, true);
4427
+ if (this.pinch) return;
4428
+ const { top, height } = body.getBoundingClientRect();
4429
+ const y = (first.clientY + second.clientY) / 2;
4430
+ this.pinch = {
4431
+ body,
4432
+ distance: Math.max(1, Math.hypot(first.clientX - second.clientX, first.clientY - second.clientY)),
4433
+ y,
4434
+ anchor: (y - top) / height,
4435
+ drawn: view.drawnSlotHeight,
4436
+ option: this.opts.slotHeight
4437
+ };
4438
+ };
4311
4439
  onTouchMove = (ev) => {
4312
4440
  if (this.session?.active) ev.preventDefault();
4441
+ const p = this.pinch;
4442
+ const first = ev.touches[0];
4443
+ const second = ev.touches[1];
4444
+ if (!p || !first || !second) return;
4445
+ if (!ev.cancelable) {
4446
+ this.endPinch();
4447
+ return;
4448
+ }
4449
+ ev.preventDefault();
4450
+ const height = p.drawn * Math.hypot(first.clientX - second.clientX, first.clientY - second.clientY) / p.distance;
4451
+ p.y = (first.clientY + second.clientY) / 2;
4452
+ this.pinchTo(p, Math.min(Math.max(p.drawn, PINCH_MAX_SLOT), Math.max(Math.min(p.drawn, PINCH_MIN_SLOT), height)));
4453
+ };
4454
+ onTouchEnd = (ev) => {
4455
+ if (ev.touches.length < 2) this.endPinch();
4313
4456
  };
4457
+ /** Rescales the grid, keeping the anchored time under the fingers. */
4458
+ pinchTo(p, slotHeight) {
4459
+ this.opts = { ...this.opts, slotHeight };
4460
+ if (!(this.current instanceof TimeGridView)) return;
4461
+ this.current.applyScale();
4462
+ const { top, height } = p.body.getBoundingClientRect();
4463
+ const scroller = scrollerOf(p.body, "y");
4464
+ if (scroller) scroller.scrollTop += top + height * p.anchor - p.y;
4465
+ }
4466
+ endPinch() {
4467
+ const p = this.pinch;
4468
+ this.pinch = null;
4469
+ if (!p?.body.isConnected) return;
4470
+ const settled = Math.round(this.opts.slotHeight);
4471
+ const slotHeight = settled === Math.round(p.drawn) ? p.option : settled;
4472
+ this.pinchTo(p, slotHeight);
4473
+ this.current?.render(null);
4474
+ if (slotHeight !== p.option) this.opts.onSlotHeightChange?.(slotHeight);
4475
+ }
4314
4476
  onPointerMove = (ev) => {
4315
4477
  const s = this.session;
4316
4478
  if (!s || ev.pointerId !== s.pointerId) return;
@@ -4660,6 +4822,7 @@ var Calendar = class {
4660
4822
  scroller.scrollLeft = position.left;
4661
4823
  }
4662
4824
  this.dragScrollOrigins.clear();
4825
+ this.dragScrollEdges.clear();
4663
4826
  this.session = null;
4664
4827
  this.listDragPreview?.element.remove();
4665
4828
  this.listDragPreview = null;
@@ -4678,10 +4841,15 @@ var Calendar = class {
4678
4841
  );
4679
4842
  const draggedId = this.draggingId;
4680
4843
  this.draggingId = null;
4844
+ this.dragWidth = null;
4681
4845
  this.dragRow = null;
4682
- if (draggedId) for (const el of this.elementsFor(draggedId)) el.classList.remove("is-dragging");
4846
+ if (draggedId) for (const el of this.elementsFor(draggedId)) {
4847
+ el.classList.remove("is-dragging");
4848
+ el.style.removeProperty("--tnc-drag-width");
4849
+ }
4683
4850
  this.strip?.markTarget(null);
4684
4851
  if (!s.active) return;
4852
+ if (this.current instanceof DayGridView) this.current.render(null);
4685
4853
  if (s.mode === "select") {
4686
4854
  if (cancel) this.unselectSilently();
4687
4855
  else if (this.selection) this.fireSelect(this.selection, ev);
@@ -5070,6 +5238,8 @@ var BASES_CALENDAR_DEFAULTS = {
5070
5238
  slotDuration: "00:30:00",
5071
5239
  snapDuration: "00:30:00",
5072
5240
  scrollTime: "08:00:00",
5241
+ slotHeight: 24,
5242
+ pinchZoom: true,
5073
5243
  nowIndicator: true,
5074
5244
  showTimeGrid: true,
5075
5245
  selectMirror: true,
@@ -5134,6 +5304,8 @@ function basesLayoutOptions(s, context = {}) {
5134
5304
  slotDuration,
5135
5305
  snapDuration: time("snapDuration") ?? slotDuration,
5136
5306
  scrollTime: time("scrollTime", 48) ?? d.scrollTime,
5307
+ slotHeight: clamp(Math.round(num(s.slotHeight, d.slotHeight)), 5, 96),
5308
+ pinchZoom: pick("pinchZoom") !== false,
5137
5309
  nowIndicator: pick("nowIndicator") !== false,
5138
5310
  timeGrid: showTimeGrid,
5139
5311
  selectMirror: pick("selectMirror") !== false,