@tasknotes/calendar 0.1.8 → 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.
@@ -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",
@@ -1066,6 +1073,7 @@ var DayGridView = class {
1066
1073
  topPx = 24;
1067
1074
  lastHeights = "";
1068
1075
  dotsMode;
1076
+ dragPreviews = [];
1069
1077
  daysPerWeek() {
1070
1078
  const { days } = this.ctx;
1071
1079
  const first = days[0];
@@ -1079,6 +1087,36 @@ var DayGridView = class {
1079
1087
  }
1080
1088
  render(_change) {
1081
1089
  const { ctx } = this;
1090
+ this.clearDragPreviews();
1091
+ const dragged = ctx.draggingId ? ctx.store.get(ctx.draggingId) : void 0;
1092
+ const source = dragged ? this.elementsFor(dragged.id)[0] : void 0;
1093
+ if (dragged && source && this.el.closest(".tnc-dragging-move")) {
1094
+ const proposals = [dragged, ...[...ctx.store.all()].filter((item) => item.id !== dragged.id && item.item.preview)];
1095
+ for (const proposal of proposals) {
1096
+ const original = this.elementsFor(proposal.id)[0] ?? source;
1097
+ const bounds = original.getBoundingClientRect();
1098
+ const sourceWeek = original.closest(".tnc-dg-week");
1099
+ const offset = bounds.top - sourceWeek.getBoundingClientRect().top;
1100
+ for (const week of this.weeks) {
1101
+ const start = week.days.findIndex((day) => day >= proposal.startDay && day < proposal.endDay);
1102
+ if (start < 0) continue;
1103
+ let end = start + 1;
1104
+ while (!this.dotsMode && end < week.days.length && week.days[end] < proposal.endDay) end++;
1105
+ 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 });
1106
+ preview.classList.add("tnc-month-preview");
1107
+ preview.inert = true;
1108
+ setStyle(preview, {
1109
+ left: `calc(${pct(start / week.days.length)} + 2px)`,
1110
+ width: `calc(${pct((end - start) / week.days.length)} - 4px)`,
1111
+ top: `${Math.max(this.topPx, Math.min(offset, week.row.clientHeight - bounds.height - 4))}px`,
1112
+ height: `${bounds.height}px`
1113
+ });
1114
+ week.layer.append(preview);
1115
+ this.dragPreviews.push(preview);
1116
+ }
1117
+ }
1118
+ return;
1119
+ }
1082
1120
  this.dotsMode = ctx.opts.monthDisplay !== "full" || this.grid.clientWidth / this.daysPerWeek() < 72;
1083
1121
  this.el.classList.toggle("tnc-dg--dots", this.dotsMode);
1084
1122
  this.el.classList.toggle("tnc-dg--compact", this.dotsMode && ctx.opts.monthDisplay === "full");
@@ -1110,6 +1148,10 @@ var DayGridView = class {
1110
1148
  }
1111
1149
  }
1112
1150
  }
1151
+ clearDragPreviews() {
1152
+ for (const preview of this.dragPreviews) this.ctx.destroyItemEl(preview);
1153
+ this.dragPreviews = [];
1154
+ }
1113
1155
  /** Height of a "+N more" link as the host styles it (`--tnc-more-h`, or a taller min-height). */
1114
1156
  measureMore(cs) {
1115
1157
  const fallback = px(cs, "--tnc-more-h", this.barPx);
@@ -1301,6 +1343,7 @@ var DayGridView = class {
1301
1343
  return this.weeks.flatMap((w) => [...w.pool.elementsFor(id), ...(w.dots ?? []).flatMap((c) => c.pool.elementsFor(id))]);
1302
1344
  }
1303
1345
  destroy() {
1346
+ this.clearDragPreviews();
1304
1347
  this.resizeObs?.disconnect();
1305
1348
  for (const w of this.weeks) {
1306
1349
  w.pool.clear();
@@ -1353,6 +1396,7 @@ var ListView = class {
1353
1396
  frame = null;
1354
1397
  /** A picked day stays marked until the list is scrolled by hand. */
1355
1398
  pinned = false;
1399
+ pinnedTop = 0;
1356
1400
  itemsOn(day) {
1357
1401
  const { store, opts } = this.ctx;
1358
1402
  const out = store.spanningIn(day, day + 1, true);
@@ -1484,6 +1528,7 @@ var ListView = class {
1484
1528
  this.scroller.scrollTop += section.getBoundingClientRect().top - this.scroller.getBoundingClientRect().top;
1485
1529
  this.ctx.strip?.setFocus(day);
1486
1530
  this.pinned = true;
1531
+ this.pinnedTop = this.scroller.scrollTop;
1487
1532
  }
1488
1533
  fold = () => {
1489
1534
  this.pinned = false;
@@ -1491,6 +1536,7 @@ var ListView = class {
1491
1536
  if (strip?.expanded) strip.setExpanded(false);
1492
1537
  };
1493
1538
  onScroll = () => {
1539
+ if (this.pinned && Math.abs(this.scroller.scrollTop - this.pinnedTop) > 1) this.pinned = false;
1494
1540
  if (this.frame !== null) return;
1495
1541
  this.frame = this.ctx.win.requestAnimationFrame(() => {
1496
1542
  this.frame = null;
@@ -1501,7 +1547,7 @@ var ListView = class {
1501
1547
  followTop() {
1502
1548
  const strip = this.ctx.strip;
1503
1549
  if (!strip || this.pinned) return;
1504
- const edge = strip.el.getBoundingClientRect().bottom + 1;
1550
+ const edge = Math.max(strip.el.getBoundingClientRect().bottom, this.scroller.getBoundingClientRect().top) + 1;
1505
1551
  for (const day of this.ctx.days) {
1506
1552
  const section = this.sections.get(day);
1507
1553
  if (section && section.getBoundingClientRect().bottom > edge) {
@@ -1549,10 +1595,11 @@ var ListView = class {
1549
1595
  // src/dom/views/strip.ts
1550
1596
  var SWIPE_PX = 40;
1551
1597
  var DateStrip = class {
1552
- constructor(ctx, pick, focus, onShown) {
1598
+ constructor(ctx, pick, focus, onShown, onPage) {
1553
1599
  this.ctx = ctx;
1554
1600
  this.pick = pick;
1555
1601
  this.onShown = onShown;
1602
+ this.onPage = onPage;
1556
1603
  const { doc, text } = ctx;
1557
1604
  this.focus = focus;
1558
1605
  this.shown = startOfWeek(focus, ctx.firstDay);
@@ -1576,6 +1623,10 @@ var DateStrip = class {
1576
1623
  this.grid.addEventListener("keydown", this.onKeyDown);
1577
1624
  this.el.addEventListener("pointerdown", this.onPointerDown);
1578
1625
  this.el.addEventListener("pointerup", this.onPointerUp);
1626
+ this.el.addEventListener("touchstart", this.ownTouch, { passive: true });
1627
+ this.el.addEventListener("touchmove", this.ownTouch, { passive: true });
1628
+ this.el.addEventListener("touchend", this.ownTouch, { passive: true });
1629
+ this.el.addEventListener("touchcancel", this.ownTouch, { passive: true });
1579
1630
  this.el.addEventListener("pointercancel", this.onPointerCancel);
1580
1631
  this.el.addEventListener("click", this.onClickCapture, true);
1581
1632
  this.draw();
@@ -1584,6 +1635,7 @@ var DateStrip = class {
1584
1635
  ctx;
1585
1636
  pick;
1586
1637
  onShown;
1638
+ onPage;
1587
1639
  el;
1588
1640
  title;
1589
1641
  dows;
@@ -1642,6 +1694,7 @@ var DateStrip = class {
1642
1694
  step(dir) {
1643
1695
  this.shown = this.expanded ? addMonths(this.shown, dir) : this.shown + 7 * dir;
1644
1696
  this.draw();
1697
+ this.onPage?.(this.expanded ? this.shown : this.focus + 7 * dir);
1645
1698
  }
1646
1699
  days() {
1647
1700
  if (!this.expanded) return Array.from({ length: 7 }, (_, i) => this.shown + i);
@@ -1666,7 +1719,8 @@ var DateStrip = class {
1666
1719
  const tabDay = days.includes(this.focus) ? this.focus : days[0];
1667
1720
  const cells = days.map((d) => {
1668
1721
  const arg = ctx.dayArg(d);
1669
- const count = store.inRange(d, d + 1).filter((n) => n.item.display !== "background" && n.item.display !== "none").length;
1722
+ const items = store.inRange(d, d + 1).filter((n) => n.item.display !== "background" && n.item.display !== "none");
1723
+ const count = items.length;
1670
1724
  const first = ymdOfDay(d).day === 1 && d !== days[0] && !this.expanded;
1671
1725
  const label = fmt.longDate(d);
1672
1726
  const cell = h(
@@ -1690,7 +1744,15 @@ var DateStrip = class {
1690
1744
  }
1691
1745
  );
1692
1746
  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" }));
1747
+ cell.append(h(doc, "span", "tnc-strip-num", null, fmt.dayNumber(d)));
1748
+ const dots = h(doc, "span", "tnc-strip-dots", { "aria-hidden": "true" });
1749
+ for (const item of items.slice(0, 3)) {
1750
+ const dot = h(doc, "span", "tnc-strip-dot");
1751
+ const color = item.item.color || item.item.borderColor || item.item.backgroundColor;
1752
+ if (color) dot.style.backgroundColor = color;
1753
+ dots.append(dot);
1754
+ }
1755
+ cell.append(dots);
1694
1756
  return cell;
1695
1757
  });
1696
1758
  this.grid.replaceChildren(...cells);
@@ -1730,6 +1792,9 @@ var DateStrip = class {
1730
1792
  if (ev.button !== 0 || this.ctx.draggingId) return;
1731
1793
  this.swipe = { id: ev.pointerId, x: ev.clientX, y: ev.clientY };
1732
1794
  };
1795
+ ownTouch = (event) => {
1796
+ event.stopPropagation();
1797
+ };
1733
1798
  onPointerUp = (ev) => {
1734
1799
  const s = this.swipe;
1735
1800
  this.swipe = null;
@@ -1769,6 +1834,7 @@ var TimeGridView = class {
1769
1834
  const showGrid = opts.timeGrid;
1770
1835
  this.el = h(doc, "div", "tnc-tg", { role: "table", "aria-label": ctx.view.title });
1771
1836
  this.el.classList.toggle("tnc-tg--no-grid", !showGrid);
1837
+ this.el.classList.toggle("tnc-tg--pinch", showGrid && opts.pinchZoom);
1772
1838
  this.scroller = h(doc, "div", "tnc-tg-scroll");
1773
1839
  this.dayNavigation = h(doc, "div", "tnc-tg-day-navigation", { role: "group", "aria-label": ctx.view.title });
1774
1840
  this.scroller.addEventListener("scroll", () => this.updateDayNavigation());
@@ -1918,6 +1984,11 @@ var TimeGridView = class {
1918
1984
  }
1919
1985
  if (this.nowGutter) gutter.append(this.nowGutter);
1920
1986
  }
1987
+ /** Slot height as drawn, which `expandRows` can stretch past the option. */
1988
+ get drawnSlotHeight() {
1989
+ return this.ctx.slot.duration * this.ppm;
1990
+ }
1991
+ /** Applies `slotHeight`. Items are placed in percentages, so they follow without a render. */
1921
1992
  applyScale() {
1922
1993
  const { opts, slot } = this.ctx;
1923
1994
  this.ppm = opts.slotHeight / slot.duration;
@@ -2098,15 +2169,17 @@ var TimeGridView = class {
2098
2169
  const { opts } = ctx;
2099
2170
  const readableWidth = Number.parseFloat(ctx.win.getComputedStyle(this.el).getPropertyValue("--tnc-event-min-width")) || 128;
2100
2171
  const laneWidth = c.col.clientWidth * (lane === "actual" && placements.some((placement) => placement.split) ? 0.22 : 1);
2101
- const collapse = placements.some((placement) => {
2172
+ const overlapThreshold = opts.overlapStackThreshold ?? 0;
2173
+ const collapsedClusters = new Set(placements.filter((placement) => {
2102
2174
  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;
2175
+ const denseOverlap = overlapThreshold > 1 && placement.cols >= overlapThreshold;
2176
+ return columns > 1 && (columns * readableWidth > laneWidth || denseOverlap);
2177
+ }).map((placement) => placement.cluster));
2107
2178
  const hiddenByCluster = /* @__PURE__ */ new Map();
2108
2179
  pool.begin();
2109
2180
  for (const p of placements) {
2181
+ const withMore = lane === "plan" || collapsedClusters.has(p.cluster);
2182
+ const maxStack = collapsedClusters.has(p.cluster) ? 1 : opts.eventMaxStack;
2110
2183
  const preview = p.item.item.preview || p.item.id === ctx.draggingId;
2111
2184
  if (!preview && withMore && maxStack > 0 && p.cols > maxStack && p.col >= maxStack - 1) {
2112
2185
  const rec = hiddenByCluster.get(p.cluster) ?? { count: 0, start: p.startMin, end: p.endMin, split: p.split, items: [] };
@@ -2119,7 +2192,7 @@ var TimeGridView = class {
2119
2192
  }
2120
2193
  const cols = !preview && withMore && maxStack > 0 ? Math.min(p.cols, maxStack) : p.cols;
2121
2194
  const span = Math.min(p.span, cols - p.col);
2122
- const left = p.col / cols;
2195
+ const left = p.item.id === ctx.draggingId ? 0 : p.col / cols;
2123
2196
  let width = span / cols;
2124
2197
  if (opts.slotEventOverlap && cols > 1 && p.col + span < cols) width = Math.min(1 - left, width * 1.7);
2125
2198
  const el = pool.place(p.item.id, this.meta(p, false));
@@ -2134,8 +2207,9 @@ var TimeGridView = class {
2134
2207
  });
2135
2208
  }
2136
2209
  pool.end();
2137
- if (!withMore) return;
2138
2210
  for (const [cluster, rec] of hiddenByCluster) {
2211
+ const collapse = collapsedClusters.has(cluster);
2212
+ const maxStack = collapse ? 1 : opts.eventMaxStack;
2139
2213
  const key = `${lane}-${cluster}`;
2140
2214
  let link = c.more.querySelector(`[data-cluster="${key}"]`);
2141
2215
  if (!link) {
@@ -2301,7 +2375,9 @@ var TimeGridView = class {
2301
2375
  scrollToTime(minute) {
2302
2376
  if (!this.body) return;
2303
2377
  const y = (minute - this.ctx.slot.min) * this.ppm;
2304
- this.scroller.scrollTop = Math.max(0, y);
2378
+ const label = this.gutter?.querySelector(".tnc-slot-label");
2379
+ const inset = label ? Math.ceil(label.getBoundingClientRect().height / 2) + 2 : 0;
2380
+ this.scroller.scrollTop = Math.max(0, y - inset);
2305
2381
  }
2306
2382
  getScroll() {
2307
2383
  return { top: this.scroller.scrollTop, left: this.scroller.scrollLeft };
@@ -2965,7 +3041,7 @@ ${this.ctx.text.more(items.length - 8)}` : ""}` : label;
2965
3041
  const bounds = month.getBoundingClientRect();
2966
3042
  const viewport = this.scroller.getBoundingClientRect();
2967
3043
  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;
3044
+ else if (bounds.bottom > viewport.bottom) this.scroller.scrollTop += bounds.top - viewport.top;
2969
3045
  }
2970
3046
  elementsFor(_id) {
2971
3047
  return [];
@@ -2979,6 +3055,8 @@ ${this.ctx.text.more(items.length - 8)}` : ""}` : label;
2979
3055
  var EXTERNAL_ID = "__tnc_external__";
2980
3056
  var SVG_NS = "http://www.w3.org/2000/svg";
2981
3057
  var MAX_AGENDA_DAYS = 366;
3058
+ var PINCH_MIN_SLOT = 16;
3059
+ var PINCH_MAX_SLOT = 96;
2982
3060
  function sameOption(a, b, depth = 0) {
2983
3061
  if (a === b) return true;
2984
3062
  if (typeof a === "function" && typeof b === "function") return true;
@@ -3006,7 +3084,7 @@ var Calendar = class {
3006
3084
  this.win = this.doc.defaultView ?? globalThis;
3007
3085
  this.opts = { ...defaultOptions(), ...options };
3008
3086
  this.zone = getTimeZone(this.opts.timeZone);
3009
- this.fmt = new Formatter(this.opts.locale, this.opts.hour12);
3087
+ this.fmt = new Formatter(this.opts.locale, this.opts.hour12, this.opts.formatTime);
3010
3088
  this.text = this.resolveText();
3011
3089
  this.order = parseOrder(this.opts.eventOrder);
3012
3090
  this.store = new ItemStore(this.zone, {
@@ -3048,13 +3126,16 @@ var Calendar = class {
3048
3126
  draggingId = null;
3049
3127
  listDragPreview = null;
3050
3128
  dragSources = [];
3129
+ dragWidth = null;
3051
3130
  dragScrollFrame = null;
3052
3131
  dragScrollOrigins = /* @__PURE__ */ new Map();
3132
+ dragScrollEdges = /* @__PURE__ */ new Map();
3053
3133
  itemWriteStates = /* @__PURE__ */ new Map();
3054
3134
  dragRow = null;
3055
3135
  strip = null;
3056
3136
  stripSize = null;
3057
3137
  rebuilding = false;
3138
+ pinch = null;
3058
3139
  toolbarEl;
3059
3140
  viewHost;
3060
3141
  current = null;
@@ -3163,7 +3244,7 @@ var Calendar = class {
3163
3244
  defaultAllDayDays: this.opts.defaultAllDayDays
3164
3245
  });
3165
3246
  }
3166
- if (has("locale") || has("hour12")) this.fmt = new Formatter(this.opts.locale, this.opts.hour12);
3247
+ if (has("locale") || has("hour12") || has("formatTime")) this.fmt = new Formatter(this.opts.locale, this.opts.hour12, this.opts.formatTime);
3167
3248
  if (has("text") || has("locale")) this.text = this.resolveText();
3168
3249
  if (has("eventOrder")) this.order = parseOrder(this.opts.eventOrder);
3169
3250
  if (has("date")) this.anchor = this.toDay(this.opts.date) ?? this.anchor;
@@ -3402,7 +3483,10 @@ var Calendar = class {
3402
3483
  if (this.conflicts.has(n.id)) el.classList.add("is-conflict");
3403
3484
  const indicator = variant === "indicator";
3404
3485
  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");
3486
+ if (this.draggingId === n.id) {
3487
+ el.classList.add("is-dragging");
3488
+ if (variant === "timed" && this.dragWidth !== null) el.style.setProperty("--tnc-drag-width", `${this.dragWidth}px`);
3489
+ }
3406
3490
  if (this.selectedIds.has(n.id) && !preview) el.classList.add("is-selected");
3407
3491
  el.classList.add(...classList(it.className));
3408
3492
  const arg = this.itemArg(el, meta);
@@ -3670,7 +3754,9 @@ var Calendar = class {
3670
3754
  this.root.classList.add("tnc--has-strip");
3671
3755
  const { startDay, endDay } = this.view.range;
3672
3756
  if (!this.strip) {
3673
- const strip = new DateStrip(this, (day, ev) => this.pickStripDay(day, ev), startDay, () => this.stripShown());
3757
+ const strip = new DateStrip(this, (day, ev) => this.pickStripDay(day, ev), startDay, () => this.stripShown(), (day) => {
3758
+ if (this.agendaList()) this.showListDay(day);
3759
+ });
3674
3760
  this.strip = strip;
3675
3761
  this.root.insertBefore(strip.el, this.viewHost);
3676
3762
  const RO = this.win.ResizeObserver;
@@ -4034,7 +4120,10 @@ var Calendar = class {
4034
4120
  r.addEventListener("dragover", this.onNativeDragOver);
4035
4121
  r.addEventListener("dragleave", this.onNativeDragLeave);
4036
4122
  r.addEventListener("drop", this.onNativeDrop);
4123
+ r.addEventListener("touchstart", this.onTouchStart, { passive: true });
4037
4124
  r.addEventListener("touchmove", this.onTouchMove, { passive: false });
4125
+ r.addEventListener("touchend", this.onTouchEnd);
4126
+ r.addEventListener("touchcancel", this.onTouchEnd);
4038
4127
  r.addEventListener("pointerover", this.onAdoptCheck);
4039
4128
  r.addEventListener("focusin", this.onAdoptCheck);
4040
4129
  this.doc.addEventListener("pointerdown", this.onDocPointerDown, true);
@@ -4053,7 +4142,10 @@ var Calendar = class {
4053
4142
  r.removeEventListener("dragover", this.onNativeDragOver);
4054
4143
  r.removeEventListener("dragleave", this.onNativeDragLeave);
4055
4144
  r.removeEventListener("drop", this.onNativeDrop);
4145
+ r.removeEventListener("touchstart", this.onTouchStart);
4056
4146
  r.removeEventListener("touchmove", this.onTouchMove);
4147
+ r.removeEventListener("touchend", this.onTouchEnd);
4148
+ r.removeEventListener("touchcancel", this.onTouchEnd);
4057
4149
  r.removeEventListener("pointerover", this.onAdoptCheck);
4058
4150
  r.removeEventListener("focusin", this.onAdoptCheck);
4059
4151
  this.doc.removeEventListener("pointerdown", this.onDocPointerDown, true);
@@ -4148,7 +4240,11 @@ var Calendar = class {
4148
4240
  if (s.origin) {
4149
4241
  this.draggingId = s.origin.id;
4150
4242
  for (const el of this.elementsFor(s.origin.id)) {
4151
- if (this.opts.view !== "list" && s.mode === "move") {
4243
+ if (this.meta.get(el)?.variant === "timed" && s.mode === "move") {
4244
+ this.dragWidth = el.getBoundingClientRect().width;
4245
+ el.style.setProperty("--tnc-drag-width", `${this.dragWidth}px`);
4246
+ }
4247
+ if (this.opts.view !== "list" && !(this.current instanceof DayGridView) && s.mode === "move") {
4152
4248
  const source = el.cloneNode(true);
4153
4249
  source.classList.remove("is-selected");
4154
4250
  source.classList.add("tnc-drag-source");
@@ -4201,24 +4297,96 @@ var Calendar = class {
4201
4297
  const coordinate = axis === "x" ? pointer.clientX : pointer.clientY;
4202
4298
  const lower = Math.max(0, axis === "x" ? viewport.left : viewport.top);
4203
4299
  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;
4300
+ if (coordinate < lower || coordinate > upper) {
4301
+ this.dragScrollEdges.delete(axis);
4302
+ continue;
4303
+ }
4205
4304
  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;
4305
+ if (!delta) {
4306
+ this.dragScrollEdges.delete(axis);
4307
+ continue;
4308
+ }
4309
+ const edge = this.dragScrollEdges.get(axis);
4310
+ const direction = Math.sign(delta);
4311
+ const now = this.win.performance.now();
4312
+ if (!edge || edge.scroller !== scroller || edge.direction !== direction) {
4313
+ this.dragScrollEdges.set(axis, { scroller, direction, since: now });
4314
+ continue;
4315
+ }
4316
+ if (now - edge.since < 180) continue;
4207
4317
  if (!this.dragScrollOrigins.has(scroller)) this.dragScrollOrigins.set(scroller, { top: scroller.scrollTop, left: scroller.scrollLeft });
4208
4318
  const before = axis === "x" ? scroller.scrollLeft : scroller.scrollTop;
4209
4319
  if (axis === "x") scroller.scrollLeft += delta;
4210
4320
  else scroller.scrollTop += delta;
4211
4321
  moved ||= before !== (axis === "x" ? scroller.scrollLeft : scroller.scrollTop);
4212
4322
  }
4213
- }
4323
+ } else this.dragScrollEdges.clear();
4214
4324
  if (moved) this.track(session, pointer);
4215
4325
  this.dragScrollFrame = this.win.requestAnimationFrame(tick);
4216
4326
  };
4217
4327
  this.dragScrollFrame = this.win.requestAnimationFrame(tick);
4218
4328
  }
4329
+ onTouchStart = (ev) => {
4330
+ const first = ev.touches[0];
4331
+ const second = ev.touches[1];
4332
+ if (!first || !second) {
4333
+ this.endPinch();
4334
+ return;
4335
+ }
4336
+ const view = this.current;
4337
+ if (this.session?.active || !this.opts.pinchZoom || !(view instanceof TimeGridView)) return;
4338
+ const body = first.target.closest(".tnc-tg-body");
4339
+ if (!body?.contains(second.target)) return;
4340
+ this.endSession(null, true);
4341
+ if (this.pinch) return;
4342
+ const { top, height } = body.getBoundingClientRect();
4343
+ const y = (first.clientY + second.clientY) / 2;
4344
+ this.pinch = {
4345
+ body,
4346
+ distance: Math.max(1, Math.hypot(first.clientX - second.clientX, first.clientY - second.clientY)),
4347
+ y,
4348
+ anchor: (y - top) / height,
4349
+ drawn: view.drawnSlotHeight,
4350
+ option: this.opts.slotHeight
4351
+ };
4352
+ };
4219
4353
  onTouchMove = (ev) => {
4220
4354
  if (this.session?.active) ev.preventDefault();
4355
+ const p = this.pinch;
4356
+ const first = ev.touches[0];
4357
+ const second = ev.touches[1];
4358
+ if (!p || !first || !second) return;
4359
+ if (!ev.cancelable) {
4360
+ this.endPinch();
4361
+ return;
4362
+ }
4363
+ ev.preventDefault();
4364
+ const height = p.drawn * Math.hypot(first.clientX - second.clientX, first.clientY - second.clientY) / p.distance;
4365
+ p.y = (first.clientY + second.clientY) / 2;
4366
+ this.pinchTo(p, Math.min(Math.max(p.drawn, PINCH_MAX_SLOT), Math.max(Math.min(p.drawn, PINCH_MIN_SLOT), height)));
4221
4367
  };
4368
+ onTouchEnd = (ev) => {
4369
+ if (ev.touches.length < 2) this.endPinch();
4370
+ };
4371
+ /** Rescales the grid, keeping the anchored time under the fingers. */
4372
+ pinchTo(p, slotHeight) {
4373
+ this.opts = { ...this.opts, slotHeight };
4374
+ if (!(this.current instanceof TimeGridView)) return;
4375
+ this.current.applyScale();
4376
+ const { top, height } = p.body.getBoundingClientRect();
4377
+ const scroller = scrollerOf(p.body, "y");
4378
+ if (scroller) scroller.scrollTop += top + height * p.anchor - p.y;
4379
+ }
4380
+ endPinch() {
4381
+ const p = this.pinch;
4382
+ this.pinch = null;
4383
+ if (!p?.body.isConnected) return;
4384
+ const settled = Math.round(this.opts.slotHeight);
4385
+ const slotHeight = settled === Math.round(p.drawn) ? p.option : settled;
4386
+ this.pinchTo(p, slotHeight);
4387
+ this.current?.render(null);
4388
+ if (slotHeight !== p.option) this.opts.onSlotHeightChange?.(slotHeight);
4389
+ }
4222
4390
  onPointerMove = (ev) => {
4223
4391
  const s = this.session;
4224
4392
  if (!s || ev.pointerId !== s.pointerId) return;
@@ -4568,6 +4736,7 @@ var Calendar = class {
4568
4736
  scroller.scrollLeft = position.left;
4569
4737
  }
4570
4738
  this.dragScrollOrigins.clear();
4739
+ this.dragScrollEdges.clear();
4571
4740
  this.session = null;
4572
4741
  this.listDragPreview?.element.remove();
4573
4742
  this.listDragPreview = null;
@@ -4586,10 +4755,15 @@ var Calendar = class {
4586
4755
  );
4587
4756
  const draggedId = this.draggingId;
4588
4757
  this.draggingId = null;
4758
+ this.dragWidth = null;
4589
4759
  this.dragRow = null;
4590
- if (draggedId) for (const el of this.elementsFor(draggedId)) el.classList.remove("is-dragging");
4760
+ if (draggedId) for (const el of this.elementsFor(draggedId)) {
4761
+ el.classList.remove("is-dragging");
4762
+ el.style.removeProperty("--tnc-drag-width");
4763
+ }
4591
4764
  this.strip?.markTarget(null);
4592
4765
  if (!s.active) return;
4766
+ if (this.current instanceof DayGridView) this.current.render(null);
4593
4767
  if (s.mode === "select") {
4594
4768
  if (cancel) this.unselectSilently();
4595
4769
  else if (this.selection) this.fireSelect(this.selection, ev);