@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.
@@ -575,14 +575,16 @@ function pct(n) {
575
575
 
576
576
  // src/dom/format.ts
577
577
  var Formatter = class {
578
- cache = /* @__PURE__ */ new Map();
579
- locale;
580
- hour12;
581
- constructor(locale, hour12) {
578
+ constructor(locale, hour12, formatTime) {
579
+ this.formatTime = formatTime;
582
580
  this.locale = locale;
583
581
  const probe = new Intl.DateTimeFormat(locale, { hour: "numeric" }).resolvedOptions();
584
582
  this.hour12 = hour12 ?? (probe.hour12 === true || probe.hourCycle === "h12" || probe.hourCycle === "h11");
585
583
  }
584
+ formatTime;
585
+ cache = /* @__PURE__ */ new Map();
586
+ locale;
587
+ hour12;
586
588
  fmt(key, opts) {
587
589
  let f = this.cache.get(key);
588
590
  if (!f) {
@@ -640,6 +642,7 @@ var Formatter = class {
640
642
  /** Wall-clock minutes to a label. `compact` drops ":00" in 12-hour formats ("9am"). */
641
643
  time(minute, compact = false) {
642
644
  const m = (Math.round(minute) % 1440 + 1440) % 1440;
645
+ if (this.formatTime) return this.formatTime(m);
643
646
  const date = new Date(Date.UTC(2e3, 0, 1, Math.floor(m / 60), m % 60));
644
647
  if (this.hour12) {
645
648
  const parts = this.fmt("t12", { hour: "numeric", minute: "2-digit", hour12: true }).formatToParts(date);
@@ -731,6 +734,7 @@ function defaultOptions() {
731
734
  snapDuration: void 0,
732
735
  scrollTime: "08:00",
733
736
  slotHeight: 24,
737
+ pinchZoom: true,
734
738
  expandRows: false,
735
739
  nowIndicator: true,
736
740
  selectMirror: true,
@@ -772,6 +776,7 @@ var LAYOUT_KEYS = /* @__PURE__ */ new Set([
772
776
  "locale",
773
777
  "firstDay",
774
778
  "hour12",
779
+ "formatTime",
775
780
  "weekNumbers",
776
781
  "weekends",
777
782
  "hiddenDays",
@@ -784,9 +789,11 @@ var LAYOUT_KEYS = /* @__PURE__ */ new Set([
784
789
  "slotDuration",
785
790
  "snapDuration",
786
791
  "slotHeight",
792
+ "pinchZoom",
787
793
  "expandRows",
788
794
  "nowIndicator",
789
795
  "slotEventOverlap",
796
+ "overlapStackThreshold",
790
797
  "eventMinHeight",
791
798
  "eventMaxStack",
792
799
  "dayMaxEvents",
@@ -831,43 +838,44 @@ function segmentInWindow(n, day, minMinute, maxMinute, zone) {
831
838
  };
832
839
  }
833
840
  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);
841
+ function collides(a, b, minMinutes) {
842
+ if (b.startMin < a.endMin) return true;
843
+ if (b.startMin === a.endMin && a.endMin > a.startMin) return false;
844
+ return b.startMin < a.startMin + minMinutes;
845
+ }
846
+ function overlaps(a, b, minMinutes) {
847
+ return a.startMin <= b.startMin ? collides(a, b, minMinutes) : collides(b, a, minMinutes);
848
+ }
834
849
  function packTimed(segs, minMinutes, order = defaultOrder) {
835
- 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));
850
+ 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));
836
851
  const out = [];
837
852
  let cluster = [];
838
- let colEnds = [];
839
- let clusterEnd = -Infinity;
853
+ let colLast = [];
840
854
  let clusterIndex = 0;
841
855
  const flush = () => {
842
856
  if (cluster.length) clusterIndex++;
843
- const cols = colEnds.length;
857
+ const cols = colLast.length;
844
858
  for (const p of cluster) {
845
859
  p.cols = cols;
846
860
  let span = 1;
847
- while (p.col + span < cols && !cluster.some((o) => o.col === p.col + span && o.startMin < p.end && p.startMin < o.end)) span++;
861
+ while (p.col + span < cols && !cluster.some((o) => o.col === p.col + span && overlaps(o, p, minMinutes))) span++;
848
862
  p.span = span;
849
863
  }
850
864
  cluster = [];
851
- colEnds = [];
865
+ colLast = [];
852
866
  };
853
867
  for (const s of sorted) {
854
- if (s.startMin >= clusterEnd) {
855
- flush();
856
- clusterEnd = -Infinity;
857
- }
858
- let col = colEnds.findIndex((end) => end <= s.startMin);
859
- if (col < 0) {
860
- col = colEnds.length;
861
- colEnds.push(0);
862
- }
863
- colEnds[col] = s.end;
868
+ if (!cluster.some((o) => collides(o, s, minMinutes))) flush();
869
+ let col = colLast.findIndex((last) => !collides(last, s, minMinutes));
870
+ if (col < 0) col = colLast.length;
864
871
  const p = { ...s, col, cols: 1, span: 1, cluster: clusterIndex, split: false };
872
+ colLast[col] = p;
865
873
  cluster.push(p);
866
874
  out.push(p);
867
- clusterEnd = Math.max(clusterEnd, s.end);
868
875
  }
869
876
  flush();
870
- for (const p of out) delete p.end;
877
+ const starts = new Set(out.map((p) => p.startMin));
878
+ for (const p of out) if (p.endMin > p.startMin && p.startMin + minMinutes > p.endMin && starts.has(p.endMin)) p.abutted = true;
871
879
  return out;
872
880
  }
873
881
  function layoutTimedDay(items, day, zone, minMinutes, order, window = [0, MINUTES_PER_DAY]) {
@@ -889,15 +897,8 @@ function layoutTimedDay(items, day, zone, minMinutes, order, window = [0, MINUTE
889
897
  }
890
898
  function markSplit(lane, other, minMinutes) {
891
899
  if (!lane.length || !other.length) return;
892
- const end = (p) => Math.max(p.endMin, p.startMin + minMinutes);
893
- const ranges = /* @__PURE__ */ new Map();
894
- for (const p of lane) {
895
- const r = ranges.get(p.cluster);
896
- if (r) r[1] = Math.max(r[1], end(p));
897
- else ranges.set(p.cluster, [p.startMin, end(p)]);
898
- }
899
900
  const split = /* @__PURE__ */ new Set();
900
- for (const [cluster, [s, e]] of ranges) if (other.some((o) => o.startMin < e && s < end(o))) split.add(cluster);
901
+ for (const p of lane) if (!split.has(p.cluster) && other.some((o) => overlaps(p, o, minMinutes))) split.add(p.cluster);
901
902
  for (const p of lane) p.split = split.has(p.cluster);
902
903
  }
903
904
  function layoutRow(items, days, order = defaultOrder) {
@@ -1066,6 +1067,7 @@ var DayGridView = class {
1066
1067
  topPx = 24;
1067
1068
  lastHeights = "";
1068
1069
  dotsMode;
1070
+ dragPreviews = [];
1069
1071
  daysPerWeek() {
1070
1072
  const { days } = this.ctx;
1071
1073
  const first = days[0];
@@ -1079,6 +1081,36 @@ var DayGridView = class {
1079
1081
  }
1080
1082
  render(_change) {
1081
1083
  const { ctx } = this;
1084
+ this.clearDragPreviews();
1085
+ const dragged = ctx.draggingId ? ctx.store.get(ctx.draggingId) : void 0;
1086
+ const source = dragged ? this.elementsFor(dragged.id)[0] : void 0;
1087
+ if (dragged && source && this.el.closest(".tnc-dragging-move")) {
1088
+ const proposals = [dragged, ...[...ctx.store.all()].filter((item) => item.id !== dragged.id && item.item.preview)];
1089
+ for (const proposal of proposals) {
1090
+ const original = this.elementsFor(proposal.id)[0] ?? source;
1091
+ const bounds = original.getBoundingClientRect();
1092
+ const sourceWeek = original.closest(".tnc-dg-week");
1093
+ const offset = bounds.top - sourceWeek.getBoundingClientRect().top;
1094
+ for (const week of this.weeks) {
1095
+ const start = week.days.findIndex((day) => day >= proposal.startDay && day < proposal.endDay);
1096
+ if (start < 0) continue;
1097
+ let end = start + 1;
1098
+ while (!this.dotsMode && end < week.days.length && week.days[end] < proposal.endDay) end++;
1099
+ 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 });
1100
+ preview.classList.add("tnc-month-preview");
1101
+ preview.inert = true;
1102
+ setStyle(preview, {
1103
+ left: `calc(${pct(start / week.days.length)} + 2px)`,
1104
+ width: `calc(${pct((end - start) / week.days.length)} - 4px)`,
1105
+ top: `${Math.max(this.topPx, Math.min(offset, week.row.clientHeight - bounds.height - 4))}px`,
1106
+ height: `${bounds.height}px`
1107
+ });
1108
+ week.layer.append(preview);
1109
+ this.dragPreviews.push(preview);
1110
+ }
1111
+ }
1112
+ return;
1113
+ }
1082
1114
  this.dotsMode = ctx.opts.monthDisplay !== "full" || this.grid.clientWidth / this.daysPerWeek() < 72;
1083
1115
  this.el.classList.toggle("tnc-dg--dots", this.dotsMode);
1084
1116
  this.el.classList.toggle("tnc-dg--compact", this.dotsMode && ctx.opts.monthDisplay === "full");
@@ -1110,6 +1142,10 @@ var DayGridView = class {
1110
1142
  }
1111
1143
  }
1112
1144
  }
1145
+ clearDragPreviews() {
1146
+ for (const preview of this.dragPreviews) this.ctx.destroyItemEl(preview);
1147
+ this.dragPreviews = [];
1148
+ }
1113
1149
  /** Height of a "+N more" link as the host styles it (`--tnc-more-h`, or a taller min-height). */
1114
1150
  measureMore(cs) {
1115
1151
  const fallback = px(cs, "--tnc-more-h", this.barPx);
@@ -1301,6 +1337,7 @@ var DayGridView = class {
1301
1337
  return this.weeks.flatMap((w) => [...w.pool.elementsFor(id), ...(w.dots ?? []).flatMap((c) => c.pool.elementsFor(id))]);
1302
1338
  }
1303
1339
  destroy() {
1340
+ this.clearDragPreviews();
1304
1341
  this.resizeObs?.disconnect();
1305
1342
  for (const w of this.weeks) {
1306
1343
  w.pool.clear();
@@ -1353,6 +1390,7 @@ var ListView = class {
1353
1390
  frame = null;
1354
1391
  /** A picked day stays marked until the list is scrolled by hand. */
1355
1392
  pinned = false;
1393
+ pinnedTop = 0;
1356
1394
  itemsOn(day) {
1357
1395
  const { store, opts } = this.ctx;
1358
1396
  const out = store.spanningIn(day, day + 1, true);
@@ -1484,6 +1522,7 @@ var ListView = class {
1484
1522
  this.scroller.scrollTop += section.getBoundingClientRect().top - this.scroller.getBoundingClientRect().top;
1485
1523
  this.ctx.strip?.setFocus(day);
1486
1524
  this.pinned = true;
1525
+ this.pinnedTop = this.scroller.scrollTop;
1487
1526
  }
1488
1527
  fold = () => {
1489
1528
  this.pinned = false;
@@ -1491,6 +1530,7 @@ var ListView = class {
1491
1530
  if (strip?.expanded) strip.setExpanded(false);
1492
1531
  };
1493
1532
  onScroll = () => {
1533
+ if (this.pinned && Math.abs(this.scroller.scrollTop - this.pinnedTop) > 1) this.pinned = false;
1494
1534
  if (this.frame !== null) return;
1495
1535
  this.frame = this.ctx.win.requestAnimationFrame(() => {
1496
1536
  this.frame = null;
@@ -1501,7 +1541,7 @@ var ListView = class {
1501
1541
  followTop() {
1502
1542
  const strip = this.ctx.strip;
1503
1543
  if (!strip || this.pinned) return;
1504
- const edge = strip.el.getBoundingClientRect().bottom + 1;
1544
+ const edge = Math.max(strip.el.getBoundingClientRect().bottom, this.scroller.getBoundingClientRect().top) + 1;
1505
1545
  for (const day of this.ctx.days) {
1506
1546
  const section = this.sections.get(day);
1507
1547
  if (section && section.getBoundingClientRect().bottom > edge) {
@@ -1549,10 +1589,11 @@ var ListView = class {
1549
1589
  // src/dom/views/strip.ts
1550
1590
  var SWIPE_PX = 40;
1551
1591
  var DateStrip = class {
1552
- constructor(ctx, pick, focus, onShown) {
1592
+ constructor(ctx, pick, focus, onShown, onPage) {
1553
1593
  this.ctx = ctx;
1554
1594
  this.pick = pick;
1555
1595
  this.onShown = onShown;
1596
+ this.onPage = onPage;
1556
1597
  const { doc, text } = ctx;
1557
1598
  this.focus = focus;
1558
1599
  this.shown = startOfWeek(focus, ctx.firstDay);
@@ -1576,6 +1617,10 @@ var DateStrip = class {
1576
1617
  this.grid.addEventListener("keydown", this.onKeyDown);
1577
1618
  this.el.addEventListener("pointerdown", this.onPointerDown);
1578
1619
  this.el.addEventListener("pointerup", this.onPointerUp);
1620
+ this.el.addEventListener("touchstart", this.ownTouch, { passive: true });
1621
+ this.el.addEventListener("touchmove", this.ownTouch, { passive: true });
1622
+ this.el.addEventListener("touchend", this.ownTouch, { passive: true });
1623
+ this.el.addEventListener("touchcancel", this.ownTouch, { passive: true });
1579
1624
  this.el.addEventListener("pointercancel", this.onPointerCancel);
1580
1625
  this.el.addEventListener("click", this.onClickCapture, true);
1581
1626
  this.draw();
@@ -1584,6 +1629,7 @@ var DateStrip = class {
1584
1629
  ctx;
1585
1630
  pick;
1586
1631
  onShown;
1632
+ onPage;
1587
1633
  el;
1588
1634
  title;
1589
1635
  dows;
@@ -1642,6 +1688,7 @@ var DateStrip = class {
1642
1688
  step(dir) {
1643
1689
  this.shown = this.expanded ? addMonths(this.shown, dir) : this.shown + 7 * dir;
1644
1690
  this.draw();
1691
+ this.onPage?.(this.expanded ? this.shown : this.focus + 7 * dir);
1645
1692
  }
1646
1693
  days() {
1647
1694
  if (!this.expanded) return Array.from({ length: 7 }, (_, i) => this.shown + i);
@@ -1666,7 +1713,8 @@ var DateStrip = class {
1666
1713
  const tabDay = days.includes(this.focus) ? this.focus : days[0];
1667
1714
  const cells = days.map((d) => {
1668
1715
  const arg = ctx.dayArg(d);
1669
- const count = store.inRange(d, d + 1).filter((n) => n.item.display !== "background" && n.item.display !== "none").length;
1716
+ const items = store.inRange(d, d + 1).filter((n) => n.item.display !== "background" && n.item.display !== "none");
1717
+ const count = items.length;
1670
1718
  const first = ymdOfDay(d).day === 1 && d !== days[0] && !this.expanded;
1671
1719
  const label = fmt.longDate(d);
1672
1720
  const cell = h(
@@ -1690,7 +1738,15 @@ var DateStrip = class {
1690
1738
  }
1691
1739
  );
1692
1740
  if (first) cell.append(h(doc, "span", "tnc-strip-month", { "aria-hidden": "true" }, fmt.monthShort(d)));
1693
- cell.append(h(doc, "span", "tnc-strip-num", null, fmt.dayNumber(d)), h(doc, "span", "tnc-strip-dot", { "aria-hidden": "true" }));
1741
+ cell.append(h(doc, "span", "tnc-strip-num", null, fmt.dayNumber(d)));
1742
+ const dots = h(doc, "span", "tnc-strip-dots", { "aria-hidden": "true" });
1743
+ for (const item of items.slice(0, 3)) {
1744
+ const dot = h(doc, "span", "tnc-strip-dot");
1745
+ const color = item.item.color || item.item.borderColor || item.item.backgroundColor;
1746
+ if (color) dot.style.backgroundColor = color;
1747
+ dots.append(dot);
1748
+ }
1749
+ cell.append(dots);
1694
1750
  return cell;
1695
1751
  });
1696
1752
  this.grid.replaceChildren(...cells);
@@ -1730,6 +1786,9 @@ var DateStrip = class {
1730
1786
  if (ev.button !== 0 || this.ctx.draggingId) return;
1731
1787
  this.swipe = { id: ev.pointerId, x: ev.clientX, y: ev.clientY };
1732
1788
  };
1789
+ ownTouch = (event) => {
1790
+ event.stopPropagation();
1791
+ };
1733
1792
  onPointerUp = (ev) => {
1734
1793
  const s = this.swipe;
1735
1794
  this.swipe = null;
@@ -1769,6 +1828,7 @@ var TimeGridView = class {
1769
1828
  const showGrid = opts.timeGrid;
1770
1829
  this.el = h(doc, "div", "tnc-tg", { role: "table", "aria-label": ctx.view.title });
1771
1830
  this.el.classList.toggle("tnc-tg--no-grid", !showGrid);
1831
+ this.el.classList.toggle("tnc-tg--pinch", showGrid && opts.pinchZoom);
1772
1832
  this.scroller = h(doc, "div", "tnc-tg-scroll");
1773
1833
  this.dayNavigation = h(doc, "div", "tnc-tg-day-navigation", { role: "group", "aria-label": ctx.view.title });
1774
1834
  this.scroller.addEventListener("scroll", () => this.updateDayNavigation());
@@ -1918,6 +1978,11 @@ var TimeGridView = class {
1918
1978
  }
1919
1979
  if (this.nowGutter) gutter.append(this.nowGutter);
1920
1980
  }
1981
+ /** Slot height as drawn, which `expandRows` can stretch past the option. */
1982
+ get drawnSlotHeight() {
1983
+ return this.ctx.slot.duration * this.ppm;
1984
+ }
1985
+ /** Applies `slotHeight`. Items are placed in percentages, so they follow without a render. */
1921
1986
  applyScale() {
1922
1987
  const { opts, slot } = this.ctx;
1923
1988
  this.ppm = opts.slotHeight / slot.duration;
@@ -2098,15 +2163,17 @@ var TimeGridView = class {
2098
2163
  const { opts } = ctx;
2099
2164
  const readableWidth = Number.parseFloat(ctx.win.getComputedStyle(this.el).getPropertyValue("--tnc-event-min-width")) || 128;
2100
2165
  const laneWidth = c.col.clientWidth * (lane === "actual" && placements.some((placement) => placement.split) ? 0.22 : 1);
2101
- const collapse = placements.some((placement) => {
2166
+ const overlapThreshold = opts.overlapStackThreshold ?? 0;
2167
+ const collapsedClusters = new Set(placements.filter((placement) => {
2102
2168
  const columns = opts.eventMaxStack > 0 ? Math.min(placement.cols, opts.eventMaxStack) : placement.cols;
2103
- return columns > 1 && columns * readableWidth > laneWidth;
2104
- });
2105
- const withMore = lane === "plan" || collapse;
2106
- const maxStack = collapse ? 1 : opts.eventMaxStack;
2169
+ const denseOverlap = overlapThreshold > 1 && placement.cols >= overlapThreshold;
2170
+ return columns > 1 && (columns * readableWidth > laneWidth || denseOverlap);
2171
+ }).map((placement) => placement.cluster));
2107
2172
  const hiddenByCluster = /* @__PURE__ */ new Map();
2108
2173
  pool.begin();
2109
2174
  for (const p of placements) {
2175
+ const withMore = lane === "plan" || collapsedClusters.has(p.cluster);
2176
+ const maxStack = collapsedClusters.has(p.cluster) ? 1 : opts.eventMaxStack;
2110
2177
  const preview = p.item.item.preview || p.item.id === ctx.draggingId;
2111
2178
  if (!preview && withMore && maxStack > 0 && p.cols > maxStack && p.col >= maxStack - 1) {
2112
2179
  const rec = hiddenByCluster.get(p.cluster) ?? { count: 0, start: p.startMin, end: p.endMin, split: p.split, items: [] };
@@ -2119,7 +2186,7 @@ var TimeGridView = class {
2119
2186
  }
2120
2187
  const cols = !preview && withMore && maxStack > 0 ? Math.min(p.cols, maxStack) : p.cols;
2121
2188
  const span = Math.min(p.span, cols - p.col);
2122
- const left = p.col / cols;
2189
+ const left = p.item.id === ctx.draggingId ? 0 : p.col / cols;
2123
2190
  let width = span / cols;
2124
2191
  if (opts.slotEventOverlap && cols > 1 && p.col + span < cols) width = Math.min(1 - left, width * 1.7);
2125
2192
  const el = pool.place(p.item.id, this.meta(p, false));
@@ -2127,15 +2194,16 @@ var TimeGridView = class {
2127
2194
  el.classList.toggle("is-short", short);
2128
2195
  setStyle(el, {
2129
2196
  top: top(p.startMin),
2130
- height: `max(${height(p)}, ${opts.eventMinHeight}px)`,
2197
+ height: p.abutted ? height(p) : `max(${height(p)}, ${opts.eventMinHeight}px)`,
2131
2198
  ...inLane(lane, p.split, left, width),
2132
2199
  "--tnc-event-readable-width": pct(span / cols / width),
2133
2200
  "z-index": String(p.col + 1)
2134
2201
  });
2135
2202
  }
2136
2203
  pool.end();
2137
- if (!withMore) return;
2138
2204
  for (const [cluster, rec] of hiddenByCluster) {
2205
+ const collapse = collapsedClusters.has(cluster);
2206
+ const maxStack = collapse ? 1 : opts.eventMaxStack;
2139
2207
  const key = `${lane}-${cluster}`;
2140
2208
  let link = c.more.querySelector(`[data-cluster="${key}"]`);
2141
2209
  if (!link) {
@@ -2301,7 +2369,9 @@ var TimeGridView = class {
2301
2369
  scrollToTime(minute) {
2302
2370
  if (!this.body) return;
2303
2371
  const y = (minute - this.ctx.slot.min) * this.ppm;
2304
- this.scroller.scrollTop = Math.max(0, y);
2372
+ const label = this.gutter?.querySelector(".tnc-slot-label");
2373
+ const inset = label ? Math.ceil(label.getBoundingClientRect().height / 2) + 2 : 0;
2374
+ this.scroller.scrollTop = Math.max(0, y - inset);
2305
2375
  }
2306
2376
  getScroll() {
2307
2377
  return { top: this.scroller.scrollTop, left: this.scroller.scrollLeft };
@@ -2965,7 +3035,7 @@ ${this.ctx.text.more(items.length - 8)}` : ""}` : label;
2965
3035
  const bounds = month.getBoundingClientRect();
2966
3036
  const viewport = this.scroller.getBoundingClientRect();
2967
3037
  if (bounds.top < viewport.top) this.scroller.scrollTop += bounds.top - viewport.top;
2968
- else if (bounds.bottom > viewport.bottom) this.scroller.scrollTop += bounds.bottom - viewport.bottom;
3038
+ else if (bounds.bottom > viewport.bottom) this.scroller.scrollTop += bounds.top - viewport.top;
2969
3039
  }
2970
3040
  elementsFor(_id) {
2971
3041
  return [];
@@ -2979,6 +3049,8 @@ ${this.ctx.text.more(items.length - 8)}` : ""}` : label;
2979
3049
  var EXTERNAL_ID = "__tnc_external__";
2980
3050
  var SVG_NS = "http://www.w3.org/2000/svg";
2981
3051
  var MAX_AGENDA_DAYS = 366;
3052
+ var PINCH_MIN_SLOT = 16;
3053
+ var PINCH_MAX_SLOT = 96;
2982
3054
  function sameOption(a, b, depth = 0) {
2983
3055
  if (a === b) return true;
2984
3056
  if (typeof a === "function" && typeof b === "function") return true;
@@ -3006,7 +3078,7 @@ var Calendar = class {
3006
3078
  this.win = this.doc.defaultView ?? globalThis;
3007
3079
  this.opts = { ...defaultOptions(), ...options };
3008
3080
  this.zone = getTimeZone(this.opts.timeZone);
3009
- this.fmt = new Formatter(this.opts.locale, this.opts.hour12);
3081
+ this.fmt = new Formatter(this.opts.locale, this.opts.hour12, this.opts.formatTime);
3010
3082
  this.text = this.resolveText();
3011
3083
  this.order = parseOrder(this.opts.eventOrder);
3012
3084
  this.store = new ItemStore(this.zone, {
@@ -3048,13 +3120,16 @@ var Calendar = class {
3048
3120
  draggingId = null;
3049
3121
  listDragPreview = null;
3050
3122
  dragSources = [];
3123
+ dragWidth = null;
3051
3124
  dragScrollFrame = null;
3052
3125
  dragScrollOrigins = /* @__PURE__ */ new Map();
3126
+ dragScrollEdges = /* @__PURE__ */ new Map();
3053
3127
  itemWriteStates = /* @__PURE__ */ new Map();
3054
3128
  dragRow = null;
3055
3129
  strip = null;
3056
3130
  stripSize = null;
3057
3131
  rebuilding = false;
3132
+ pinch = null;
3058
3133
  toolbarEl;
3059
3134
  viewHost;
3060
3135
  current = null;
@@ -3163,7 +3238,7 @@ var Calendar = class {
3163
3238
  defaultAllDayDays: this.opts.defaultAllDayDays
3164
3239
  });
3165
3240
  }
3166
- if (has("locale") || has("hour12")) this.fmt = new Formatter(this.opts.locale, this.opts.hour12);
3241
+ if (has("locale") || has("hour12") || has("formatTime")) this.fmt = new Formatter(this.opts.locale, this.opts.hour12, this.opts.formatTime);
3167
3242
  if (has("text") || has("locale")) this.text = this.resolveText();
3168
3243
  if (has("eventOrder")) this.order = parseOrder(this.opts.eventOrder);
3169
3244
  if (has("date")) this.anchor = this.toDay(this.opts.date) ?? this.anchor;
@@ -3402,7 +3477,10 @@ var Calendar = class {
3402
3477
  if (this.conflicts.has(n.id)) el.classList.add("is-conflict");
3403
3478
  const indicator = variant === "indicator";
3404
3479
  if (!preview && !indicator && (this.isEditable(n, "start", true) || this.isEditable(n, "duration", true))) el.classList.add("is-draggable");
3405
- if (this.draggingId === n.id) el.classList.add("is-dragging");
3480
+ if (this.draggingId === n.id) {
3481
+ el.classList.add("is-dragging");
3482
+ if (variant === "timed" && this.dragWidth !== null) el.style.setProperty("--tnc-drag-width", `${this.dragWidth}px`);
3483
+ }
3406
3484
  if (this.selectedIds.has(n.id) && !preview) el.classList.add("is-selected");
3407
3485
  el.classList.add(...classList(it.className));
3408
3486
  const arg = this.itemArg(el, meta);
@@ -3670,7 +3748,9 @@ var Calendar = class {
3670
3748
  this.root.classList.add("tnc--has-strip");
3671
3749
  const { startDay, endDay } = this.view.range;
3672
3750
  if (!this.strip) {
3673
- const strip = new DateStrip(this, (day, ev) => this.pickStripDay(day, ev), startDay, () => this.stripShown());
3751
+ const strip = new DateStrip(this, (day, ev) => this.pickStripDay(day, ev), startDay, () => this.stripShown(), (day) => {
3752
+ if (this.agendaList()) this.showListDay(day);
3753
+ });
3674
3754
  this.strip = strip;
3675
3755
  this.root.insertBefore(strip.el, this.viewHost);
3676
3756
  const RO = this.win.ResizeObserver;
@@ -4034,7 +4114,10 @@ var Calendar = class {
4034
4114
  r.addEventListener("dragover", this.onNativeDragOver);
4035
4115
  r.addEventListener("dragleave", this.onNativeDragLeave);
4036
4116
  r.addEventListener("drop", this.onNativeDrop);
4117
+ r.addEventListener("touchstart", this.onTouchStart, { passive: true });
4037
4118
  r.addEventListener("touchmove", this.onTouchMove, { passive: false });
4119
+ r.addEventListener("touchend", this.onTouchEnd);
4120
+ r.addEventListener("touchcancel", this.onTouchEnd);
4038
4121
  r.addEventListener("pointerover", this.onAdoptCheck);
4039
4122
  r.addEventListener("focusin", this.onAdoptCheck);
4040
4123
  this.doc.addEventListener("pointerdown", this.onDocPointerDown, true);
@@ -4053,7 +4136,10 @@ var Calendar = class {
4053
4136
  r.removeEventListener("dragover", this.onNativeDragOver);
4054
4137
  r.removeEventListener("dragleave", this.onNativeDragLeave);
4055
4138
  r.removeEventListener("drop", this.onNativeDrop);
4139
+ r.removeEventListener("touchstart", this.onTouchStart);
4056
4140
  r.removeEventListener("touchmove", this.onTouchMove);
4141
+ r.removeEventListener("touchend", this.onTouchEnd);
4142
+ r.removeEventListener("touchcancel", this.onTouchEnd);
4057
4143
  r.removeEventListener("pointerover", this.onAdoptCheck);
4058
4144
  r.removeEventListener("focusin", this.onAdoptCheck);
4059
4145
  this.doc.removeEventListener("pointerdown", this.onDocPointerDown, true);
@@ -4148,7 +4234,11 @@ var Calendar = class {
4148
4234
  if (s.origin) {
4149
4235
  this.draggingId = s.origin.id;
4150
4236
  for (const el of this.elementsFor(s.origin.id)) {
4151
- if (this.opts.view !== "list" && s.mode === "move") {
4237
+ if (this.meta.get(el)?.variant === "timed" && s.mode === "move") {
4238
+ this.dragWidth = el.getBoundingClientRect().width;
4239
+ el.style.setProperty("--tnc-drag-width", `${this.dragWidth}px`);
4240
+ }
4241
+ if (this.opts.view !== "list" && !(this.current instanceof DayGridView) && s.mode === "move") {
4152
4242
  const source = el.cloneNode(true);
4153
4243
  source.classList.remove("is-selected");
4154
4244
  source.classList.add("tnc-drag-source");
@@ -4201,24 +4291,96 @@ var Calendar = class {
4201
4291
  const coordinate = axis === "x" ? pointer.clientX : pointer.clientY;
4202
4292
  const lower = Math.max(0, axis === "x" ? viewport.left : viewport.top);
4203
4293
  const upper = Math.min(axis === "x" ? this.win.innerWidth : this.win.innerHeight, axis === "x" ? viewport.right : viewport.bottom);
4204
- if (coordinate < lower || coordinate > upper) continue;
4294
+ if (coordinate < lower || coordinate > upper) {
4295
+ this.dragScrollEdges.delete(axis);
4296
+ continue;
4297
+ }
4205
4298
  const delta = coordinate < lower + 32 ? -Math.min(12, (lower + 32 - coordinate) / 2) : coordinate > upper - 32 ? Math.min(12, (coordinate - upper + 32) / 2) : 0;
4206
- if (!delta) continue;
4299
+ if (!delta) {
4300
+ this.dragScrollEdges.delete(axis);
4301
+ continue;
4302
+ }
4303
+ const edge = this.dragScrollEdges.get(axis);
4304
+ const direction = Math.sign(delta);
4305
+ const now = this.win.performance.now();
4306
+ if (!edge || edge.scroller !== scroller || edge.direction !== direction) {
4307
+ this.dragScrollEdges.set(axis, { scroller, direction, since: now });
4308
+ continue;
4309
+ }
4310
+ if (now - edge.since < 180) continue;
4207
4311
  if (!this.dragScrollOrigins.has(scroller)) this.dragScrollOrigins.set(scroller, { top: scroller.scrollTop, left: scroller.scrollLeft });
4208
4312
  const before = axis === "x" ? scroller.scrollLeft : scroller.scrollTop;
4209
4313
  if (axis === "x") scroller.scrollLeft += delta;
4210
4314
  else scroller.scrollTop += delta;
4211
4315
  moved ||= before !== (axis === "x" ? scroller.scrollLeft : scroller.scrollTop);
4212
4316
  }
4213
- }
4317
+ } else this.dragScrollEdges.clear();
4214
4318
  if (moved) this.track(session, pointer);
4215
4319
  this.dragScrollFrame = this.win.requestAnimationFrame(tick);
4216
4320
  };
4217
4321
  this.dragScrollFrame = this.win.requestAnimationFrame(tick);
4218
4322
  }
4323
+ onTouchStart = (ev) => {
4324
+ const first = ev.touches[0];
4325
+ const second = ev.touches[1];
4326
+ if (!first || !second) {
4327
+ this.endPinch();
4328
+ return;
4329
+ }
4330
+ const view = this.current;
4331
+ if (this.session?.active || !this.opts.pinchZoom || !(view instanceof TimeGridView)) return;
4332
+ const body = first.target.closest(".tnc-tg-body");
4333
+ if (!body?.contains(second.target)) return;
4334
+ this.endSession(null, true);
4335
+ if (this.pinch) return;
4336
+ const { top, height } = body.getBoundingClientRect();
4337
+ const y = (first.clientY + second.clientY) / 2;
4338
+ this.pinch = {
4339
+ body,
4340
+ distance: Math.max(1, Math.hypot(first.clientX - second.clientX, first.clientY - second.clientY)),
4341
+ y,
4342
+ anchor: (y - top) / height,
4343
+ drawn: view.drawnSlotHeight,
4344
+ option: this.opts.slotHeight
4345
+ };
4346
+ };
4219
4347
  onTouchMove = (ev) => {
4220
4348
  if (this.session?.active) ev.preventDefault();
4349
+ const p = this.pinch;
4350
+ const first = ev.touches[0];
4351
+ const second = ev.touches[1];
4352
+ if (!p || !first || !second) return;
4353
+ if (!ev.cancelable) {
4354
+ this.endPinch();
4355
+ return;
4356
+ }
4357
+ ev.preventDefault();
4358
+ const height = p.drawn * Math.hypot(first.clientX - second.clientX, first.clientY - second.clientY) / p.distance;
4359
+ p.y = (first.clientY + second.clientY) / 2;
4360
+ this.pinchTo(p, Math.min(Math.max(p.drawn, PINCH_MAX_SLOT), Math.max(Math.min(p.drawn, PINCH_MIN_SLOT), height)));
4361
+ };
4362
+ onTouchEnd = (ev) => {
4363
+ if (ev.touches.length < 2) this.endPinch();
4221
4364
  };
4365
+ /** Rescales the grid, keeping the anchored time under the fingers. */
4366
+ pinchTo(p, slotHeight) {
4367
+ this.opts = { ...this.opts, slotHeight };
4368
+ if (!(this.current instanceof TimeGridView)) return;
4369
+ this.current.applyScale();
4370
+ const { top, height } = p.body.getBoundingClientRect();
4371
+ const scroller = scrollerOf(p.body, "y");
4372
+ if (scroller) scroller.scrollTop += top + height * p.anchor - p.y;
4373
+ }
4374
+ endPinch() {
4375
+ const p = this.pinch;
4376
+ this.pinch = null;
4377
+ if (!p?.body.isConnected) return;
4378
+ const settled = Math.round(this.opts.slotHeight);
4379
+ const slotHeight = settled === Math.round(p.drawn) ? p.option : settled;
4380
+ this.pinchTo(p, slotHeight);
4381
+ this.current?.render(null);
4382
+ if (slotHeight !== p.option) this.opts.onSlotHeightChange?.(slotHeight);
4383
+ }
4222
4384
  onPointerMove = (ev) => {
4223
4385
  const s = this.session;
4224
4386
  if (!s || ev.pointerId !== s.pointerId) return;
@@ -4568,6 +4730,7 @@ var Calendar = class {
4568
4730
  scroller.scrollLeft = position.left;
4569
4731
  }
4570
4732
  this.dragScrollOrigins.clear();
4733
+ this.dragScrollEdges.clear();
4571
4734
  this.session = null;
4572
4735
  this.listDragPreview?.element.remove();
4573
4736
  this.listDragPreview = null;
@@ -4586,10 +4749,15 @@ var Calendar = class {
4586
4749
  );
4587
4750
  const draggedId = this.draggingId;
4588
4751
  this.draggingId = null;
4752
+ this.dragWidth = null;
4589
4753
  this.dragRow = null;
4590
- if (draggedId) for (const el of this.elementsFor(draggedId)) el.classList.remove("is-dragging");
4754
+ if (draggedId) for (const el of this.elementsFor(draggedId)) {
4755
+ el.classList.remove("is-dragging");
4756
+ el.style.removeProperty("--tnc-drag-width");
4757
+ }
4591
4758
  this.strip?.markTarget(null);
4592
4759
  if (!s.active) return;
4760
+ if (this.current instanceof DayGridView) this.current.render(null);
4593
4761
  if (s.mode === "select") {
4594
4762
  if (cancel) this.unselectSilently();
4595
4763
  else if (this.selection) this.fireSelect(this.selection, ev);