@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.
package/CHANGELOG.md CHANGED
@@ -1,6 +1,23 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 0.1.10
4
+
5
+ - Freeze Month source layout during a move and draw an inert target placeholder; preserve timed drag width across collision layouts.
6
+ - Contain date-strip touch gestures, reveal the complete target month in Year view, and allow dense collision stacks independently of the overlap preference.
7
+ - Require a short pause at a drag edge before scrolling, so passing through the edge does not shift the grid. Cancellation still restores the original scroll position.
8
+ - Keep the whole requested hour label visible below the sticky day and all-day headers when scrolling to a time.
9
+ - Agenda strip paging moves the list to the focused day; scrollbar and keyboard scrolling update the date highlight after a day is picked. Activity dots show up to three appointments in their configured colours.
10
+ - Hosts can supply one wall-clock `formatTime` callback for event, stack, selection, accessibility and slot labels.
11
+ - Hosts can set `overlapStackThreshold` to group dense overlapping appointments into the existing collision stack even in wide columns. Explicit side-by-side layouts remain unchanged.
12
+
13
+ ## 0.1.9
14
+
15
+ Not published to npm (the release check failed); these changes ship in 0.1.10.
16
+
17
+ ### Added
18
+
19
+ - Pinching a time grid on touch rescales its slots around the time under the fingers. `onSlotHeightChange` reports the settled height for hosts to store, and `pinchZoom: false` turns the gesture off.
20
+ - `basesLayoutOptions` reads `slotHeight` and `pinchZoom` from Bases calendar settings.
4
21
 
5
22
  ## 0.1.8
6
23
 
package/README.md CHANGED
@@ -46,6 +46,7 @@ The host persists changes; the calendar supplies optimistic updates and a
46
46
  - **Changes** (`onItemDrop`, `onItemResize`, `onSelect`, `onExternalDrop`) report TaskNotes storage strings (`startStr`/`endStr`), `revert()`, and `source: "pointer" | "keyboard"`. Returning a rejected promise reverts.
47
47
  - **Clicks**: `onDateClick` reports `minute` (the slot's wall-clock minute, snapped to `slotDuration`; null for day cells and the all-day row), `allDay` and `startStr` (`"YYYY-MM-DD"` or `"YYYY-MM-DDTHH:mm"`), so a tap on a slot can prefill a time.
48
48
  - **Input**: mouse drag after 4px, touch long press (`longPressDelay`), keyboard (Enter/Space, Shift+F10, Alt+Arrow to move, Alt+Shift+Arrow to resize), external drags via `beginExternalDrag` or native HTML5 drops.
49
+ - **Pinch**: two fingers on a time grid rescale `slotHeight` between 16 and 96px, keeping the time under the fingers in place. `onSlotHeightChange` reports the settled height for the host to store; `pinchZoom: false` leaves the gesture to the browser.
49
50
  - **Popouts**: all listeners, timers and observers use the host element's `ownerDocument`; bare `window`/`document` are lint errors in `src`.
50
51
 
51
52
  ### Views and options
package/dist/calendar.css CHANGED
@@ -707,6 +707,10 @@
707
707
  .tnc-tg-body {
708
708
  position: relative;
709
709
  }
710
+ .tnc-tg--pinch .tnc-tg-body {
711
+ /* Pinches rescale the grid rather than zoom the page. */
712
+ touch-action: pan-x pan-y;
713
+ }
710
714
  .tnc-gutter {
711
715
  border-right: 1px solid var(--tnc-line);
712
716
  }
@@ -1196,6 +1200,12 @@
1196
1200
  font-weight: 650;
1197
1201
  line-height: 1;
1198
1202
  }
1203
+ .tnc-strip-dots {
1204
+ display: flex;
1205
+ justify-content: center;
1206
+ gap: 2px;
1207
+ min-height: 5px;
1208
+ }
1199
1209
  .tnc-strip-dot {
1200
1210
  width: 5px;
1201
1211
  height: 5px;
@@ -1906,6 +1916,22 @@
1906
1916
  outline: 1px solid var(--tnc-focus);
1907
1917
  outline-offset: -1px;
1908
1918
  }
1919
+ .tnc .tnc-item--timed.is-dragging {
1920
+ min-width: var(--tnc-drag-width);
1921
+ max-width: var(--tnc-drag-width);
1922
+ }
1923
+ .tnc .tnc-dg .tnc-item.is-dragging:not(.is-mirror) {
1924
+ opacity: var(--tnc-drag-opacity);
1925
+ box-shadow: none;
1926
+ outline: none;
1927
+ }
1928
+ .tnc .tnc-month-preview {
1929
+ position: absolute;
1930
+ pointer-events: none;
1931
+ outline: 2px dashed var(--tnc-accent);
1932
+ outline-offset: -2px;
1933
+ z-index: 10;
1934
+ }
1909
1935
  .tnc .tnc-item--list.is-dragging:not(.tnc-drag-preview) {
1910
1936
  opacity: var(--tnc-drag-opacity);
1911
1937
  box-shadow: none;
@@ -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",
@@ -1158,6 +1165,7 @@ var DayGridView = class {
1158
1165
  topPx = 24;
1159
1166
  lastHeights = "";
1160
1167
  dotsMode;
1168
+ dragPreviews = [];
1161
1169
  daysPerWeek() {
1162
1170
  const { days } = this.ctx;
1163
1171
  const first = days[0];
@@ -1171,6 +1179,36 @@ var DayGridView = class {
1171
1179
  }
1172
1180
  render(_change) {
1173
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
+ }
1174
1212
  this.dotsMode = ctx.opts.monthDisplay !== "full" || this.grid.clientWidth / this.daysPerWeek() < 72;
1175
1213
  this.el.classList.toggle("tnc-dg--dots", this.dotsMode);
1176
1214
  this.el.classList.toggle("tnc-dg--compact", this.dotsMode && ctx.opts.monthDisplay === "full");
@@ -1202,6 +1240,10 @@ var DayGridView = class {
1202
1240
  }
1203
1241
  }
1204
1242
  }
1243
+ clearDragPreviews() {
1244
+ for (const preview of this.dragPreviews) this.ctx.destroyItemEl(preview);
1245
+ this.dragPreviews = [];
1246
+ }
1205
1247
  /** Height of a "+N more" link as the host styles it (`--tnc-more-h`, or a taller min-height). */
1206
1248
  measureMore(cs) {
1207
1249
  const fallback = px(cs, "--tnc-more-h", this.barPx);
@@ -1393,6 +1435,7 @@ var DayGridView = class {
1393
1435
  return this.weeks.flatMap((w) => [...w.pool.elementsFor(id), ...(w.dots ?? []).flatMap((c) => c.pool.elementsFor(id))]);
1394
1436
  }
1395
1437
  destroy() {
1438
+ this.clearDragPreviews();
1396
1439
  this.resizeObs?.disconnect();
1397
1440
  for (const w of this.weeks) {
1398
1441
  w.pool.clear();
@@ -1445,6 +1488,7 @@ var ListView = class {
1445
1488
  frame = null;
1446
1489
  /** A picked day stays marked until the list is scrolled by hand. */
1447
1490
  pinned = false;
1491
+ pinnedTop = 0;
1448
1492
  itemsOn(day) {
1449
1493
  const { store, opts } = this.ctx;
1450
1494
  const out = store.spanningIn(day, day + 1, true);
@@ -1576,6 +1620,7 @@ var ListView = class {
1576
1620
  this.scroller.scrollTop += section.getBoundingClientRect().top - this.scroller.getBoundingClientRect().top;
1577
1621
  this.ctx.strip?.setFocus(day);
1578
1622
  this.pinned = true;
1623
+ this.pinnedTop = this.scroller.scrollTop;
1579
1624
  }
1580
1625
  fold = () => {
1581
1626
  this.pinned = false;
@@ -1583,6 +1628,7 @@ var ListView = class {
1583
1628
  if (strip?.expanded) strip.setExpanded(false);
1584
1629
  };
1585
1630
  onScroll = () => {
1631
+ if (this.pinned && Math.abs(this.scroller.scrollTop - this.pinnedTop) > 1) this.pinned = false;
1586
1632
  if (this.frame !== null) return;
1587
1633
  this.frame = this.ctx.win.requestAnimationFrame(() => {
1588
1634
  this.frame = null;
@@ -1593,7 +1639,7 @@ var ListView = class {
1593
1639
  followTop() {
1594
1640
  const strip = this.ctx.strip;
1595
1641
  if (!strip || this.pinned) return;
1596
- const edge = strip.el.getBoundingClientRect().bottom + 1;
1642
+ const edge = Math.max(strip.el.getBoundingClientRect().bottom, this.scroller.getBoundingClientRect().top) + 1;
1597
1643
  for (const day of this.ctx.days) {
1598
1644
  const section = this.sections.get(day);
1599
1645
  if (section && section.getBoundingClientRect().bottom > edge) {
@@ -1641,10 +1687,11 @@ var ListView = class {
1641
1687
  // src/dom/views/strip.ts
1642
1688
  var SWIPE_PX = 40;
1643
1689
  var DateStrip = class {
1644
- constructor(ctx, pick, focus, onShown) {
1690
+ constructor(ctx, pick, focus, onShown, onPage) {
1645
1691
  this.ctx = ctx;
1646
1692
  this.pick = pick;
1647
1693
  this.onShown = onShown;
1694
+ this.onPage = onPage;
1648
1695
  const { doc, text } = ctx;
1649
1696
  this.focus = focus;
1650
1697
  this.shown = startOfWeek(focus, ctx.firstDay);
@@ -1668,6 +1715,10 @@ var DateStrip = class {
1668
1715
  this.grid.addEventListener("keydown", this.onKeyDown);
1669
1716
  this.el.addEventListener("pointerdown", this.onPointerDown);
1670
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 });
1671
1722
  this.el.addEventListener("pointercancel", this.onPointerCancel);
1672
1723
  this.el.addEventListener("click", this.onClickCapture, true);
1673
1724
  this.draw();
@@ -1676,6 +1727,7 @@ var DateStrip = class {
1676
1727
  ctx;
1677
1728
  pick;
1678
1729
  onShown;
1730
+ onPage;
1679
1731
  el;
1680
1732
  title;
1681
1733
  dows;
@@ -1734,6 +1786,7 @@ var DateStrip = class {
1734
1786
  step(dir) {
1735
1787
  this.shown = this.expanded ? addMonths(this.shown, dir) : this.shown + 7 * dir;
1736
1788
  this.draw();
1789
+ this.onPage?.(this.expanded ? this.shown : this.focus + 7 * dir);
1737
1790
  }
1738
1791
  days() {
1739
1792
  if (!this.expanded) return Array.from({ length: 7 }, (_, i) => this.shown + i);
@@ -1758,7 +1811,8 @@ var DateStrip = class {
1758
1811
  const tabDay = days.includes(this.focus) ? this.focus : days[0];
1759
1812
  const cells = days.map((d) => {
1760
1813
  const arg = ctx.dayArg(d);
1761
- 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;
1762
1816
  const first = ymdOfDay(d).day === 1 && d !== days[0] && !this.expanded;
1763
1817
  const label = fmt.longDate(d);
1764
1818
  const cell = h(
@@ -1782,7 +1836,15 @@ var DateStrip = class {
1782
1836
  }
1783
1837
  );
1784
1838
  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" }));
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);
1786
1848
  return cell;
1787
1849
  });
1788
1850
  this.grid.replaceChildren(...cells);
@@ -1822,6 +1884,9 @@ var DateStrip = class {
1822
1884
  if (ev.button !== 0 || this.ctx.draggingId) return;
1823
1885
  this.swipe = { id: ev.pointerId, x: ev.clientX, y: ev.clientY };
1824
1886
  };
1887
+ ownTouch = (event) => {
1888
+ event.stopPropagation();
1889
+ };
1825
1890
  onPointerUp = (ev) => {
1826
1891
  const s = this.swipe;
1827
1892
  this.swipe = null;
@@ -1861,6 +1926,7 @@ var TimeGridView = class {
1861
1926
  const showGrid = opts.timeGrid;
1862
1927
  this.el = h(doc, "div", "tnc-tg", { role: "table", "aria-label": ctx.view.title });
1863
1928
  this.el.classList.toggle("tnc-tg--no-grid", !showGrid);
1929
+ this.el.classList.toggle("tnc-tg--pinch", showGrid && opts.pinchZoom);
1864
1930
  this.scroller = h(doc, "div", "tnc-tg-scroll");
1865
1931
  this.dayNavigation = h(doc, "div", "tnc-tg-day-navigation", { role: "group", "aria-label": ctx.view.title });
1866
1932
  this.scroller.addEventListener("scroll", () => this.updateDayNavigation());
@@ -2010,6 +2076,11 @@ var TimeGridView = class {
2010
2076
  }
2011
2077
  if (this.nowGutter) gutter.append(this.nowGutter);
2012
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. */
2013
2084
  applyScale() {
2014
2085
  const { opts, slot } = this.ctx;
2015
2086
  this.ppm = opts.slotHeight / slot.duration;
@@ -2190,15 +2261,17 @@ var TimeGridView = class {
2190
2261
  const { opts } = ctx;
2191
2262
  const readableWidth = Number.parseFloat(ctx.win.getComputedStyle(this.el).getPropertyValue("--tnc-event-min-width")) || 128;
2192
2263
  const laneWidth = c.col.clientWidth * (lane === "actual" && placements.some((placement) => placement.split) ? 0.22 : 1);
2193
- const collapse = placements.some((placement) => {
2264
+ const overlapThreshold = opts.overlapStackThreshold ?? 0;
2265
+ const collapsedClusters = new Set(placements.filter((placement) => {
2194
2266
  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;
2267
+ const denseOverlap = overlapThreshold > 1 && placement.cols >= overlapThreshold;
2268
+ return columns > 1 && (columns * readableWidth > laneWidth || denseOverlap);
2269
+ }).map((placement) => placement.cluster));
2199
2270
  const hiddenByCluster = /* @__PURE__ */ new Map();
2200
2271
  pool.begin();
2201
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;
2202
2275
  const preview = p.item.item.preview || p.item.id === ctx.draggingId;
2203
2276
  if (!preview && withMore && maxStack > 0 && p.cols > maxStack && p.col >= maxStack - 1) {
2204
2277
  const rec = hiddenByCluster.get(p.cluster) ?? { count: 0, start: p.startMin, end: p.endMin, split: p.split, items: [] };
@@ -2211,7 +2284,7 @@ var TimeGridView = class {
2211
2284
  }
2212
2285
  const cols = !preview && withMore && maxStack > 0 ? Math.min(p.cols, maxStack) : p.cols;
2213
2286
  const span = Math.min(p.span, cols - p.col);
2214
- const left = p.col / cols;
2287
+ const left = p.item.id === ctx.draggingId ? 0 : p.col / cols;
2215
2288
  let width = span / cols;
2216
2289
  if (opts.slotEventOverlap && cols > 1 && p.col + span < cols) width = Math.min(1 - left, width * 1.7);
2217
2290
  const el = pool.place(p.item.id, this.meta(p, false));
@@ -2226,8 +2299,9 @@ var TimeGridView = class {
2226
2299
  });
2227
2300
  }
2228
2301
  pool.end();
2229
- if (!withMore) return;
2230
2302
  for (const [cluster, rec] of hiddenByCluster) {
2303
+ const collapse = collapsedClusters.has(cluster);
2304
+ const maxStack = collapse ? 1 : opts.eventMaxStack;
2231
2305
  const key = `${lane}-${cluster}`;
2232
2306
  let link = c.more.querySelector(`[data-cluster="${key}"]`);
2233
2307
  if (!link) {
@@ -2393,7 +2467,9 @@ var TimeGridView = class {
2393
2467
  scrollToTime(minute) {
2394
2468
  if (!this.body) return;
2395
2469
  const y = (minute - this.ctx.slot.min) * this.ppm;
2396
- 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);
2397
2473
  }
2398
2474
  getScroll() {
2399
2475
  return { top: this.scroller.scrollTop, left: this.scroller.scrollLeft };
@@ -3057,7 +3133,7 @@ ${this.ctx.text.more(items.length - 8)}` : ""}` : label;
3057
3133
  const bounds = month.getBoundingClientRect();
3058
3134
  const viewport = this.scroller.getBoundingClientRect();
3059
3135
  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;
3136
+ else if (bounds.bottom > viewport.bottom) this.scroller.scrollTop += bounds.top - viewport.top;
3061
3137
  }
3062
3138
  elementsFor(_id) {
3063
3139
  return [];
@@ -3071,6 +3147,8 @@ ${this.ctx.text.more(items.length - 8)}` : ""}` : label;
3071
3147
  var EXTERNAL_ID = "__tnc_external__";
3072
3148
  var SVG_NS = "http://www.w3.org/2000/svg";
3073
3149
  var MAX_AGENDA_DAYS = 366;
3150
+ var PINCH_MIN_SLOT = 16;
3151
+ var PINCH_MAX_SLOT = 96;
3074
3152
  function sameOption(a, b, depth = 0) {
3075
3153
  if (a === b) return true;
3076
3154
  if (typeof a === "function" && typeof b === "function") return true;
@@ -3098,7 +3176,7 @@ var Calendar = class {
3098
3176
  this.win = this.doc.defaultView ?? globalThis;
3099
3177
  this.opts = { ...defaultOptions(), ...options };
3100
3178
  this.zone = getTimeZone(this.opts.timeZone);
3101
- this.fmt = new Formatter(this.opts.locale, this.opts.hour12);
3179
+ this.fmt = new Formatter(this.opts.locale, this.opts.hour12, this.opts.formatTime);
3102
3180
  this.text = this.resolveText();
3103
3181
  this.order = parseOrder(this.opts.eventOrder);
3104
3182
  this.store = new ItemStore(this.zone, {
@@ -3140,13 +3218,16 @@ var Calendar = class {
3140
3218
  draggingId = null;
3141
3219
  listDragPreview = null;
3142
3220
  dragSources = [];
3221
+ dragWidth = null;
3143
3222
  dragScrollFrame = null;
3144
3223
  dragScrollOrigins = /* @__PURE__ */ new Map();
3224
+ dragScrollEdges = /* @__PURE__ */ new Map();
3145
3225
  itemWriteStates = /* @__PURE__ */ new Map();
3146
3226
  dragRow = null;
3147
3227
  strip = null;
3148
3228
  stripSize = null;
3149
3229
  rebuilding = false;
3230
+ pinch = null;
3150
3231
  toolbarEl;
3151
3232
  viewHost;
3152
3233
  current = null;
@@ -3255,7 +3336,7 @@ var Calendar = class {
3255
3336
  defaultAllDayDays: this.opts.defaultAllDayDays
3256
3337
  });
3257
3338
  }
3258
- 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);
3259
3340
  if (has("text") || has("locale")) this.text = this.resolveText();
3260
3341
  if (has("eventOrder")) this.order = parseOrder(this.opts.eventOrder);
3261
3342
  if (has("date")) this.anchor = this.toDay(this.opts.date) ?? this.anchor;
@@ -3494,7 +3575,10 @@ var Calendar = class {
3494
3575
  if (this.conflicts.has(n.id)) el.classList.add("is-conflict");
3495
3576
  const indicator = variant === "indicator";
3496
3577
  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");
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
+ }
3498
3582
  if (this.selectedIds.has(n.id) && !preview) el.classList.add("is-selected");
3499
3583
  el.classList.add(...classList(it.className));
3500
3584
  const arg = this.itemArg(el, meta);
@@ -3762,7 +3846,9 @@ var Calendar = class {
3762
3846
  this.root.classList.add("tnc--has-strip");
3763
3847
  const { startDay, endDay } = this.view.range;
3764
3848
  if (!this.strip) {
3765
- 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
+ });
3766
3852
  this.strip = strip;
3767
3853
  this.root.insertBefore(strip.el, this.viewHost);
3768
3854
  const RO = this.win.ResizeObserver;
@@ -4126,7 +4212,10 @@ var Calendar = class {
4126
4212
  r.addEventListener("dragover", this.onNativeDragOver);
4127
4213
  r.addEventListener("dragleave", this.onNativeDragLeave);
4128
4214
  r.addEventListener("drop", this.onNativeDrop);
4215
+ r.addEventListener("touchstart", this.onTouchStart, { passive: true });
4129
4216
  r.addEventListener("touchmove", this.onTouchMove, { passive: false });
4217
+ r.addEventListener("touchend", this.onTouchEnd);
4218
+ r.addEventListener("touchcancel", this.onTouchEnd);
4130
4219
  r.addEventListener("pointerover", this.onAdoptCheck);
4131
4220
  r.addEventListener("focusin", this.onAdoptCheck);
4132
4221
  this.doc.addEventListener("pointerdown", this.onDocPointerDown, true);
@@ -4145,7 +4234,10 @@ var Calendar = class {
4145
4234
  r.removeEventListener("dragover", this.onNativeDragOver);
4146
4235
  r.removeEventListener("dragleave", this.onNativeDragLeave);
4147
4236
  r.removeEventListener("drop", this.onNativeDrop);
4237
+ r.removeEventListener("touchstart", this.onTouchStart);
4148
4238
  r.removeEventListener("touchmove", this.onTouchMove);
4239
+ r.removeEventListener("touchend", this.onTouchEnd);
4240
+ r.removeEventListener("touchcancel", this.onTouchEnd);
4149
4241
  r.removeEventListener("pointerover", this.onAdoptCheck);
4150
4242
  r.removeEventListener("focusin", this.onAdoptCheck);
4151
4243
  this.doc.removeEventListener("pointerdown", this.onDocPointerDown, true);
@@ -4240,7 +4332,11 @@ var Calendar = class {
4240
4332
  if (s.origin) {
4241
4333
  this.draggingId = s.origin.id;
4242
4334
  for (const el of this.elementsFor(s.origin.id)) {
4243
- if (this.opts.view !== "list" && s.mode === "move") {
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") {
4244
4340
  const source = el.cloneNode(true);
4245
4341
  source.classList.remove("is-selected");
4246
4342
  source.classList.add("tnc-drag-source");
@@ -4293,24 +4389,96 @@ var Calendar = class {
4293
4389
  const coordinate = axis === "x" ? pointer.clientX : pointer.clientY;
4294
4390
  const lower = Math.max(0, axis === "x" ? viewport.left : viewport.top);
4295
4391
  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;
4392
+ if (coordinate < lower || coordinate > upper) {
4393
+ this.dragScrollEdges.delete(axis);
4394
+ continue;
4395
+ }
4297
4396
  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;
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;
4299
4409
  if (!this.dragScrollOrigins.has(scroller)) this.dragScrollOrigins.set(scroller, { top: scroller.scrollTop, left: scroller.scrollLeft });
4300
4410
  const before = axis === "x" ? scroller.scrollLeft : scroller.scrollTop;
4301
4411
  if (axis === "x") scroller.scrollLeft += delta;
4302
4412
  else scroller.scrollTop += delta;
4303
4413
  moved ||= before !== (axis === "x" ? scroller.scrollLeft : scroller.scrollTop);
4304
4414
  }
4305
- }
4415
+ } else this.dragScrollEdges.clear();
4306
4416
  if (moved) this.track(session, pointer);
4307
4417
  this.dragScrollFrame = this.win.requestAnimationFrame(tick);
4308
4418
  };
4309
4419
  this.dragScrollFrame = this.win.requestAnimationFrame(tick);
4310
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
+ };
4311
4445
  onTouchMove = (ev) => {
4312
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)));
4313
4459
  };
4460
+ onTouchEnd = (ev) => {
4461
+ if (ev.touches.length < 2) this.endPinch();
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
+ }
4314
4482
  onPointerMove = (ev) => {
4315
4483
  const s = this.session;
4316
4484
  if (!s || ev.pointerId !== s.pointerId) return;
@@ -4660,6 +4828,7 @@ var Calendar = class {
4660
4828
  scroller.scrollLeft = position.left;
4661
4829
  }
4662
4830
  this.dragScrollOrigins.clear();
4831
+ this.dragScrollEdges.clear();
4663
4832
  this.session = null;
4664
4833
  this.listDragPreview?.element.remove();
4665
4834
  this.listDragPreview = null;
@@ -4678,10 +4847,15 @@ var Calendar = class {
4678
4847
  );
4679
4848
  const draggedId = this.draggingId;
4680
4849
  this.draggingId = null;
4850
+ this.dragWidth = null;
4681
4851
  this.dragRow = null;
4682
- 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
+ }
4683
4856
  this.strip?.markTarget(null);
4684
4857
  if (!s.active) return;
4858
+ if (this.current instanceof DayGridView) this.current.render(null);
4685
4859
  if (s.mode === "select") {
4686
4860
  if (cancel) this.unselectSilently();
4687
4861
  else if (this.selection) this.fireSelect(this.selection, ev);
@@ -5070,6 +5244,8 @@ var BASES_CALENDAR_DEFAULTS = {
5070
5244
  slotDuration: "00:30:00",
5071
5245
  snapDuration: "00:30:00",
5072
5246
  scrollTime: "08:00:00",
5247
+ slotHeight: 24,
5248
+ pinchZoom: true,
5073
5249
  nowIndicator: true,
5074
5250
  showTimeGrid: true,
5075
5251
  selectMirror: true,
@@ -5134,6 +5310,8 @@ function basesLayoutOptions(s, context = {}) {
5134
5310
  slotDuration,
5135
5311
  snapDuration: time("snapDuration") ?? slotDuration,
5136
5312
  scrollTime: time("scrollTime", 48) ?? d.scrollTime,
5313
+ slotHeight: clamp(Math.round(num(s.slotHeight, d.slotHeight)), 5, 96),
5314
+ pinchZoom: pick("pinchZoom") !== false,
5137
5315
  nowIndicator: pick("nowIndicator") !== false,
5138
5316
  timeGrid: showTimeGrid,
5139
5317
  selectMirror: pick("selectMirror") !== false,