@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.
@@ -31,14 +31,16 @@ import {
31
31
 
32
32
  // src/dom/format.ts
33
33
  var Formatter = class {
34
- cache = /* @__PURE__ */ new Map();
35
- locale;
36
- hour12;
37
- constructor(locale, hour12) {
34
+ constructor(locale, hour12, formatTime) {
35
+ this.formatTime = formatTime;
38
36
  this.locale = locale;
39
37
  const probe = new Intl.DateTimeFormat(locale, { hour: "numeric" }).resolvedOptions();
40
38
  this.hour12 = hour12 ?? (probe.hour12 === true || probe.hourCycle === "h12" || probe.hourCycle === "h11");
41
39
  }
40
+ formatTime;
41
+ cache = /* @__PURE__ */ new Map();
42
+ locale;
43
+ hour12;
42
44
  fmt(key, opts) {
43
45
  let f = this.cache.get(key);
44
46
  if (!f) {
@@ -96,6 +98,7 @@ var Formatter = class {
96
98
  /** Wall-clock minutes to a label. `compact` drops ":00" in 12-hour formats ("9am"). */
97
99
  time(minute, compact = false) {
98
100
  const m = (Math.round(minute) % 1440 + 1440) % 1440;
101
+ if (this.formatTime) return this.formatTime(m);
99
102
  const date = new Date(Date.UTC(2e3, 0, 1, Math.floor(m / 60), m % 60));
100
103
  if (this.hour12) {
101
104
  const parts = this.fmt("t12", { hour: "numeric", minute: "2-digit", hour12: true }).formatToParts(date);
@@ -187,6 +190,7 @@ function defaultOptions() {
187
190
  snapDuration: void 0,
188
191
  scrollTime: "08:00",
189
192
  slotHeight: 24,
193
+ pinchZoom: true,
190
194
  expandRows: false,
191
195
  nowIndicator: true,
192
196
  selectMirror: true,
@@ -228,6 +232,7 @@ var LAYOUT_KEYS = /* @__PURE__ */ new Set([
228
232
  "locale",
229
233
  "firstDay",
230
234
  "hour12",
235
+ "formatTime",
231
236
  "weekNumbers",
232
237
  "weekends",
233
238
  "hiddenDays",
@@ -240,9 +245,11 @@ var LAYOUT_KEYS = /* @__PURE__ */ new Set([
240
245
  "slotDuration",
241
246
  "snapDuration",
242
247
  "slotHeight",
248
+ "pinchZoom",
243
249
  "expandRows",
244
250
  "nowIndicator",
245
251
  "slotEventOverlap",
252
+ "overlapStackThreshold",
246
253
  "eventMinHeight",
247
254
  "eventMaxStack",
248
255
  "dayMaxEvents",
@@ -443,6 +450,7 @@ var DayGridView = class {
443
450
  topPx = 24;
444
451
  lastHeights = "";
445
452
  dotsMode;
453
+ dragPreviews = [];
446
454
  daysPerWeek() {
447
455
  const { days } = this.ctx;
448
456
  const first = days[0];
@@ -456,6 +464,36 @@ var DayGridView = class {
456
464
  }
457
465
  render(_change) {
458
466
  const { ctx } = this;
467
+ this.clearDragPreviews();
468
+ const dragged = ctx.draggingId ? ctx.store.get(ctx.draggingId) : void 0;
469
+ const source = dragged ? this.elementsFor(dragged.id)[0] : void 0;
470
+ if (dragged && source && this.el.closest(".tnc-dragging-move")) {
471
+ const proposals = [dragged, ...[...ctx.store.all()].filter((item) => item.id !== dragged.id && item.item.preview)];
472
+ for (const proposal of proposals) {
473
+ const original = this.elementsFor(proposal.id)[0] ?? source;
474
+ const bounds = original.getBoundingClientRect();
475
+ const sourceWeek = original.closest(".tnc-dg-week");
476
+ const offset = bounds.top - sourceWeek.getBoundingClientRect().top;
477
+ for (const week of this.weeks) {
478
+ const start = week.days.findIndex((day) => day >= proposal.startDay && day < proposal.endDay);
479
+ if (start < 0) continue;
480
+ let end = start + 1;
481
+ while (!this.dotsMode && end < week.days.length && week.days[end] < proposal.endDay) end++;
482
+ 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 });
483
+ preview.classList.add("tnc-month-preview");
484
+ preview.inert = true;
485
+ setStyle(preview, {
486
+ left: `calc(${pct(start / week.days.length)} + 2px)`,
487
+ width: `calc(${pct((end - start) / week.days.length)} - 4px)`,
488
+ top: `${Math.max(this.topPx, Math.min(offset, week.row.clientHeight - bounds.height - 4))}px`,
489
+ height: `${bounds.height}px`
490
+ });
491
+ week.layer.append(preview);
492
+ this.dragPreviews.push(preview);
493
+ }
494
+ }
495
+ return;
496
+ }
459
497
  this.dotsMode = ctx.opts.monthDisplay !== "full" || this.grid.clientWidth / this.daysPerWeek() < 72;
460
498
  this.el.classList.toggle("tnc-dg--dots", this.dotsMode);
461
499
  this.el.classList.toggle("tnc-dg--compact", this.dotsMode && ctx.opts.monthDisplay === "full");
@@ -487,6 +525,10 @@ var DayGridView = class {
487
525
  }
488
526
  }
489
527
  }
528
+ clearDragPreviews() {
529
+ for (const preview of this.dragPreviews) this.ctx.destroyItemEl(preview);
530
+ this.dragPreviews = [];
531
+ }
490
532
  /** Height of a "+N more" link as the host styles it (`--tnc-more-h`, or a taller min-height). */
491
533
  measureMore(cs) {
492
534
  const fallback = px(cs, "--tnc-more-h", this.barPx);
@@ -678,6 +720,7 @@ var DayGridView = class {
678
720
  return this.weeks.flatMap((w) => [...w.pool.elementsFor(id), ...(w.dots ?? []).flatMap((c) => c.pool.elementsFor(id))]);
679
721
  }
680
722
  destroy() {
723
+ this.clearDragPreviews();
681
724
  this.resizeObs?.disconnect();
682
725
  for (const w of this.weeks) {
683
726
  w.pool.clear();
@@ -730,6 +773,7 @@ var ListView = class {
730
773
  frame = null;
731
774
  /** A picked day stays marked until the list is scrolled by hand. */
732
775
  pinned = false;
776
+ pinnedTop = 0;
733
777
  itemsOn(day) {
734
778
  const { store, opts } = this.ctx;
735
779
  const out = store.spanningIn(day, day + 1, true);
@@ -861,6 +905,7 @@ var ListView = class {
861
905
  this.scroller.scrollTop += section.getBoundingClientRect().top - this.scroller.getBoundingClientRect().top;
862
906
  this.ctx.strip?.setFocus(day);
863
907
  this.pinned = true;
908
+ this.pinnedTop = this.scroller.scrollTop;
864
909
  }
865
910
  fold = () => {
866
911
  this.pinned = false;
@@ -868,6 +913,7 @@ var ListView = class {
868
913
  if (strip?.expanded) strip.setExpanded(false);
869
914
  };
870
915
  onScroll = () => {
916
+ if (this.pinned && Math.abs(this.scroller.scrollTop - this.pinnedTop) > 1) this.pinned = false;
871
917
  if (this.frame !== null) return;
872
918
  this.frame = this.ctx.win.requestAnimationFrame(() => {
873
919
  this.frame = null;
@@ -878,7 +924,7 @@ var ListView = class {
878
924
  followTop() {
879
925
  const strip = this.ctx.strip;
880
926
  if (!strip || this.pinned) return;
881
- const edge = strip.el.getBoundingClientRect().bottom + 1;
927
+ const edge = Math.max(strip.el.getBoundingClientRect().bottom, this.scroller.getBoundingClientRect().top) + 1;
882
928
  for (const day of this.ctx.days) {
883
929
  const section = this.sections.get(day);
884
930
  if (section && section.getBoundingClientRect().bottom > edge) {
@@ -926,10 +972,11 @@ var ListView = class {
926
972
  // src/dom/views/strip.ts
927
973
  var SWIPE_PX = 40;
928
974
  var DateStrip = class {
929
- constructor(ctx, pick, focus, onShown) {
975
+ constructor(ctx, pick, focus, onShown, onPage) {
930
976
  this.ctx = ctx;
931
977
  this.pick = pick;
932
978
  this.onShown = onShown;
979
+ this.onPage = onPage;
933
980
  const { doc, text } = ctx;
934
981
  this.focus = focus;
935
982
  this.shown = startOfWeek(focus, ctx.firstDay);
@@ -953,6 +1000,10 @@ var DateStrip = class {
953
1000
  this.grid.addEventListener("keydown", this.onKeyDown);
954
1001
  this.el.addEventListener("pointerdown", this.onPointerDown);
955
1002
  this.el.addEventListener("pointerup", this.onPointerUp);
1003
+ this.el.addEventListener("touchstart", this.ownTouch, { passive: true });
1004
+ this.el.addEventListener("touchmove", this.ownTouch, { passive: true });
1005
+ this.el.addEventListener("touchend", this.ownTouch, { passive: true });
1006
+ this.el.addEventListener("touchcancel", this.ownTouch, { passive: true });
956
1007
  this.el.addEventListener("pointercancel", this.onPointerCancel);
957
1008
  this.el.addEventListener("click", this.onClickCapture, true);
958
1009
  this.draw();
@@ -961,6 +1012,7 @@ var DateStrip = class {
961
1012
  ctx;
962
1013
  pick;
963
1014
  onShown;
1015
+ onPage;
964
1016
  el;
965
1017
  title;
966
1018
  dows;
@@ -1019,6 +1071,7 @@ var DateStrip = class {
1019
1071
  step(dir) {
1020
1072
  this.shown = this.expanded ? addMonths(this.shown, dir) : this.shown + 7 * dir;
1021
1073
  this.draw();
1074
+ this.onPage?.(this.expanded ? this.shown : this.focus + 7 * dir);
1022
1075
  }
1023
1076
  days() {
1024
1077
  if (!this.expanded) return Array.from({ length: 7 }, (_, i) => this.shown + i);
@@ -1043,7 +1096,8 @@ var DateStrip = class {
1043
1096
  const tabDay = days.includes(this.focus) ? this.focus : days[0];
1044
1097
  const cells = days.map((d) => {
1045
1098
  const arg = ctx.dayArg(d);
1046
- const count = store.inRange(d, d + 1).filter((n) => n.item.display !== "background" && n.item.display !== "none").length;
1099
+ const items = store.inRange(d, d + 1).filter((n) => n.item.display !== "background" && n.item.display !== "none");
1100
+ const count = items.length;
1047
1101
  const first = ymdOfDay(d).day === 1 && d !== days[0] && !this.expanded;
1048
1102
  const label = fmt.longDate(d);
1049
1103
  const cell = h(
@@ -1067,7 +1121,15 @@ var DateStrip = class {
1067
1121
  }
1068
1122
  );
1069
1123
  if (first) cell.append(h(doc, "span", "tnc-strip-month", { "aria-hidden": "true" }, fmt.monthShort(d)));
1070
- cell.append(h(doc, "span", "tnc-strip-num", null, fmt.dayNumber(d)), h(doc, "span", "tnc-strip-dot", { "aria-hidden": "true" }));
1124
+ cell.append(h(doc, "span", "tnc-strip-num", null, fmt.dayNumber(d)));
1125
+ const dots = h(doc, "span", "tnc-strip-dots", { "aria-hidden": "true" });
1126
+ for (const item of items.slice(0, 3)) {
1127
+ const dot = h(doc, "span", "tnc-strip-dot");
1128
+ const color = item.item.color || item.item.borderColor || item.item.backgroundColor;
1129
+ if (color) dot.style.backgroundColor = color;
1130
+ dots.append(dot);
1131
+ }
1132
+ cell.append(dots);
1071
1133
  return cell;
1072
1134
  });
1073
1135
  this.grid.replaceChildren(...cells);
@@ -1107,6 +1169,9 @@ var DateStrip = class {
1107
1169
  if (ev.button !== 0 || this.ctx.draggingId) return;
1108
1170
  this.swipe = { id: ev.pointerId, x: ev.clientX, y: ev.clientY };
1109
1171
  };
1172
+ ownTouch = (event) => {
1173
+ event.stopPropagation();
1174
+ };
1110
1175
  onPointerUp = (ev) => {
1111
1176
  const s = this.swipe;
1112
1177
  this.swipe = null;
@@ -1146,6 +1211,7 @@ var TimeGridView = class {
1146
1211
  const showGrid = opts.timeGrid;
1147
1212
  this.el = h(doc, "div", "tnc-tg", { role: "table", "aria-label": ctx.view.title });
1148
1213
  this.el.classList.toggle("tnc-tg--no-grid", !showGrid);
1214
+ this.el.classList.toggle("tnc-tg--pinch", showGrid && opts.pinchZoom);
1149
1215
  this.scroller = h(doc, "div", "tnc-tg-scroll");
1150
1216
  this.dayNavigation = h(doc, "div", "tnc-tg-day-navigation", { role: "group", "aria-label": ctx.view.title });
1151
1217
  this.scroller.addEventListener("scroll", () => this.updateDayNavigation());
@@ -1295,6 +1361,11 @@ var TimeGridView = class {
1295
1361
  }
1296
1362
  if (this.nowGutter) gutter.append(this.nowGutter);
1297
1363
  }
1364
+ /** Slot height as drawn, which `expandRows` can stretch past the option. */
1365
+ get drawnSlotHeight() {
1366
+ return this.ctx.slot.duration * this.ppm;
1367
+ }
1368
+ /** Applies `slotHeight`. Items are placed in percentages, so they follow without a render. */
1298
1369
  applyScale() {
1299
1370
  const { opts, slot } = this.ctx;
1300
1371
  this.ppm = opts.slotHeight / slot.duration;
@@ -1475,15 +1546,17 @@ var TimeGridView = class {
1475
1546
  const { opts } = ctx;
1476
1547
  const readableWidth = Number.parseFloat(ctx.win.getComputedStyle(this.el).getPropertyValue("--tnc-event-min-width")) || 128;
1477
1548
  const laneWidth = c.col.clientWidth * (lane === "actual" && placements.some((placement) => placement.split) ? 0.22 : 1);
1478
- const collapse = placements.some((placement) => {
1549
+ const overlapThreshold = opts.overlapStackThreshold ?? 0;
1550
+ const collapsedClusters = new Set(placements.filter((placement) => {
1479
1551
  const columns = opts.eventMaxStack > 0 ? Math.min(placement.cols, opts.eventMaxStack) : placement.cols;
1480
- return columns > 1 && columns * readableWidth > laneWidth;
1481
- });
1482
- const withMore = lane === "plan" || collapse;
1483
- const maxStack = collapse ? 1 : opts.eventMaxStack;
1552
+ const denseOverlap = overlapThreshold > 1 && placement.cols >= overlapThreshold;
1553
+ return columns > 1 && (columns * readableWidth > laneWidth || denseOverlap);
1554
+ }).map((placement) => placement.cluster));
1484
1555
  const hiddenByCluster = /* @__PURE__ */ new Map();
1485
1556
  pool.begin();
1486
1557
  for (const p of placements) {
1558
+ const withMore = lane === "plan" || collapsedClusters.has(p.cluster);
1559
+ const maxStack = collapsedClusters.has(p.cluster) ? 1 : opts.eventMaxStack;
1487
1560
  const preview = p.item.item.preview || p.item.id === ctx.draggingId;
1488
1561
  if (!preview && withMore && maxStack > 0 && p.cols > maxStack && p.col >= maxStack - 1) {
1489
1562
  const rec = hiddenByCluster.get(p.cluster) ?? { count: 0, start: p.startMin, end: p.endMin, split: p.split, items: [] };
@@ -1496,7 +1569,7 @@ var TimeGridView = class {
1496
1569
  }
1497
1570
  const cols = !preview && withMore && maxStack > 0 ? Math.min(p.cols, maxStack) : p.cols;
1498
1571
  const span = Math.min(p.span, cols - p.col);
1499
- const left = p.col / cols;
1572
+ const left = p.item.id === ctx.draggingId ? 0 : p.col / cols;
1500
1573
  let width = span / cols;
1501
1574
  if (opts.slotEventOverlap && cols > 1 && p.col + span < cols) width = Math.min(1 - left, width * 1.7);
1502
1575
  const el = pool.place(p.item.id, this.meta(p, false));
@@ -1511,8 +1584,9 @@ var TimeGridView = class {
1511
1584
  });
1512
1585
  }
1513
1586
  pool.end();
1514
- if (!withMore) return;
1515
1587
  for (const [cluster, rec] of hiddenByCluster) {
1588
+ const collapse = collapsedClusters.has(cluster);
1589
+ const maxStack = collapse ? 1 : opts.eventMaxStack;
1516
1590
  const key = `${lane}-${cluster}`;
1517
1591
  let link = c.more.querySelector(`[data-cluster="${key}"]`);
1518
1592
  if (!link) {
@@ -1678,7 +1752,9 @@ var TimeGridView = class {
1678
1752
  scrollToTime(minute) {
1679
1753
  if (!this.body) return;
1680
1754
  const y = (minute - this.ctx.slot.min) * this.ppm;
1681
- this.scroller.scrollTop = Math.max(0, y);
1755
+ const label = this.gutter?.querySelector(".tnc-slot-label");
1756
+ const inset = label ? Math.ceil(label.getBoundingClientRect().height / 2) + 2 : 0;
1757
+ this.scroller.scrollTop = Math.max(0, y - inset);
1682
1758
  }
1683
1759
  getScroll() {
1684
1760
  return { top: this.scroller.scrollTop, left: this.scroller.scrollLeft };
@@ -2342,7 +2418,7 @@ ${this.ctx.text.more(items.length - 8)}` : ""}` : label;
2342
2418
  const bounds = month.getBoundingClientRect();
2343
2419
  const viewport = this.scroller.getBoundingClientRect();
2344
2420
  if (bounds.top < viewport.top) this.scroller.scrollTop += bounds.top - viewport.top;
2345
- else if (bounds.bottom > viewport.bottom) this.scroller.scrollTop += bounds.bottom - viewport.bottom;
2421
+ else if (bounds.bottom > viewport.bottom) this.scroller.scrollTop += bounds.top - viewport.top;
2346
2422
  }
2347
2423
  elementsFor(_id) {
2348
2424
  return [];
@@ -2356,6 +2432,8 @@ ${this.ctx.text.more(items.length - 8)}` : ""}` : label;
2356
2432
  var EXTERNAL_ID = "__tnc_external__";
2357
2433
  var SVG_NS = "http://www.w3.org/2000/svg";
2358
2434
  var MAX_AGENDA_DAYS = 366;
2435
+ var PINCH_MIN_SLOT = 16;
2436
+ var PINCH_MAX_SLOT = 96;
2359
2437
  function sameOption(a, b, depth = 0) {
2360
2438
  if (a === b) return true;
2361
2439
  if (typeof a === "function" && typeof b === "function") return true;
@@ -2383,7 +2461,7 @@ var Calendar = class {
2383
2461
  this.win = this.doc.defaultView ?? globalThis;
2384
2462
  this.opts = { ...defaultOptions(), ...options };
2385
2463
  this.zone = getTimeZone(this.opts.timeZone);
2386
- this.fmt = new Formatter(this.opts.locale, this.opts.hour12);
2464
+ this.fmt = new Formatter(this.opts.locale, this.opts.hour12, this.opts.formatTime);
2387
2465
  this.text = this.resolveText();
2388
2466
  this.order = parseOrder(this.opts.eventOrder);
2389
2467
  this.store = new ItemStore(this.zone, {
@@ -2425,13 +2503,16 @@ var Calendar = class {
2425
2503
  draggingId = null;
2426
2504
  listDragPreview = null;
2427
2505
  dragSources = [];
2506
+ dragWidth = null;
2428
2507
  dragScrollFrame = null;
2429
2508
  dragScrollOrigins = /* @__PURE__ */ new Map();
2509
+ dragScrollEdges = /* @__PURE__ */ new Map();
2430
2510
  itemWriteStates = /* @__PURE__ */ new Map();
2431
2511
  dragRow = null;
2432
2512
  strip = null;
2433
2513
  stripSize = null;
2434
2514
  rebuilding = false;
2515
+ pinch = null;
2435
2516
  toolbarEl;
2436
2517
  viewHost;
2437
2518
  current = null;
@@ -2540,7 +2621,7 @@ var Calendar = class {
2540
2621
  defaultAllDayDays: this.opts.defaultAllDayDays
2541
2622
  });
2542
2623
  }
2543
- if (has("locale") || has("hour12")) this.fmt = new Formatter(this.opts.locale, this.opts.hour12);
2624
+ if (has("locale") || has("hour12") || has("formatTime")) this.fmt = new Formatter(this.opts.locale, this.opts.hour12, this.opts.formatTime);
2544
2625
  if (has("text") || has("locale")) this.text = this.resolveText();
2545
2626
  if (has("eventOrder")) this.order = parseOrder(this.opts.eventOrder);
2546
2627
  if (has("date")) this.anchor = this.toDay(this.opts.date) ?? this.anchor;
@@ -2779,7 +2860,10 @@ var Calendar = class {
2779
2860
  if (this.conflicts.has(n.id)) el.classList.add("is-conflict");
2780
2861
  const indicator = variant === "indicator";
2781
2862
  if (!preview && !indicator && (this.isEditable(n, "start", true) || this.isEditable(n, "duration", true))) el.classList.add("is-draggable");
2782
- if (this.draggingId === n.id) el.classList.add("is-dragging");
2863
+ if (this.draggingId === n.id) {
2864
+ el.classList.add("is-dragging");
2865
+ if (variant === "timed" && this.dragWidth !== null) el.style.setProperty("--tnc-drag-width", `${this.dragWidth}px`);
2866
+ }
2783
2867
  if (this.selectedIds.has(n.id) && !preview) el.classList.add("is-selected");
2784
2868
  el.classList.add(...classList(it.className));
2785
2869
  const arg = this.itemArg(el, meta);
@@ -3047,7 +3131,9 @@ var Calendar = class {
3047
3131
  this.root.classList.add("tnc--has-strip");
3048
3132
  const { startDay, endDay } = this.view.range;
3049
3133
  if (!this.strip) {
3050
- const strip = new DateStrip(this, (day, ev) => this.pickStripDay(day, ev), startDay, () => this.stripShown());
3134
+ const strip = new DateStrip(this, (day, ev) => this.pickStripDay(day, ev), startDay, () => this.stripShown(), (day) => {
3135
+ if (this.agendaList()) this.showListDay(day);
3136
+ });
3051
3137
  this.strip = strip;
3052
3138
  this.root.insertBefore(strip.el, this.viewHost);
3053
3139
  const RO = this.win.ResizeObserver;
@@ -3411,7 +3497,10 @@ var Calendar = class {
3411
3497
  r.addEventListener("dragover", this.onNativeDragOver);
3412
3498
  r.addEventListener("dragleave", this.onNativeDragLeave);
3413
3499
  r.addEventListener("drop", this.onNativeDrop);
3500
+ r.addEventListener("touchstart", this.onTouchStart, { passive: true });
3414
3501
  r.addEventListener("touchmove", this.onTouchMove, { passive: false });
3502
+ r.addEventListener("touchend", this.onTouchEnd);
3503
+ r.addEventListener("touchcancel", this.onTouchEnd);
3415
3504
  r.addEventListener("pointerover", this.onAdoptCheck);
3416
3505
  r.addEventListener("focusin", this.onAdoptCheck);
3417
3506
  this.doc.addEventListener("pointerdown", this.onDocPointerDown, true);
@@ -3430,7 +3519,10 @@ var Calendar = class {
3430
3519
  r.removeEventListener("dragover", this.onNativeDragOver);
3431
3520
  r.removeEventListener("dragleave", this.onNativeDragLeave);
3432
3521
  r.removeEventListener("drop", this.onNativeDrop);
3522
+ r.removeEventListener("touchstart", this.onTouchStart);
3433
3523
  r.removeEventListener("touchmove", this.onTouchMove);
3524
+ r.removeEventListener("touchend", this.onTouchEnd);
3525
+ r.removeEventListener("touchcancel", this.onTouchEnd);
3434
3526
  r.removeEventListener("pointerover", this.onAdoptCheck);
3435
3527
  r.removeEventListener("focusin", this.onAdoptCheck);
3436
3528
  this.doc.removeEventListener("pointerdown", this.onDocPointerDown, true);
@@ -3525,7 +3617,11 @@ var Calendar = class {
3525
3617
  if (s.origin) {
3526
3618
  this.draggingId = s.origin.id;
3527
3619
  for (const el of this.elementsFor(s.origin.id)) {
3528
- if (this.opts.view !== "list" && s.mode === "move") {
3620
+ if (this.meta.get(el)?.variant === "timed" && s.mode === "move") {
3621
+ this.dragWidth = el.getBoundingClientRect().width;
3622
+ el.style.setProperty("--tnc-drag-width", `${this.dragWidth}px`);
3623
+ }
3624
+ if (this.opts.view !== "list" && !(this.current instanceof DayGridView) && s.mode === "move") {
3529
3625
  const source = el.cloneNode(true);
3530
3626
  source.classList.remove("is-selected");
3531
3627
  source.classList.add("tnc-drag-source");
@@ -3578,24 +3674,96 @@ var Calendar = class {
3578
3674
  const coordinate = axis === "x" ? pointer.clientX : pointer.clientY;
3579
3675
  const lower = Math.max(0, axis === "x" ? viewport.left : viewport.top);
3580
3676
  const upper = Math.min(axis === "x" ? this.win.innerWidth : this.win.innerHeight, axis === "x" ? viewport.right : viewport.bottom);
3581
- if (coordinate < lower || coordinate > upper) continue;
3677
+ if (coordinate < lower || coordinate > upper) {
3678
+ this.dragScrollEdges.delete(axis);
3679
+ continue;
3680
+ }
3582
3681
  const delta = coordinate < lower + 32 ? -Math.min(12, (lower + 32 - coordinate) / 2) : coordinate > upper - 32 ? Math.min(12, (coordinate - upper + 32) / 2) : 0;
3583
- if (!delta) continue;
3682
+ if (!delta) {
3683
+ this.dragScrollEdges.delete(axis);
3684
+ continue;
3685
+ }
3686
+ const edge = this.dragScrollEdges.get(axis);
3687
+ const direction = Math.sign(delta);
3688
+ const now = this.win.performance.now();
3689
+ if (!edge || edge.scroller !== scroller || edge.direction !== direction) {
3690
+ this.dragScrollEdges.set(axis, { scroller, direction, since: now });
3691
+ continue;
3692
+ }
3693
+ if (now - edge.since < 180) continue;
3584
3694
  if (!this.dragScrollOrigins.has(scroller)) this.dragScrollOrigins.set(scroller, { top: scroller.scrollTop, left: scroller.scrollLeft });
3585
3695
  const before = axis === "x" ? scroller.scrollLeft : scroller.scrollTop;
3586
3696
  if (axis === "x") scroller.scrollLeft += delta;
3587
3697
  else scroller.scrollTop += delta;
3588
3698
  moved ||= before !== (axis === "x" ? scroller.scrollLeft : scroller.scrollTop);
3589
3699
  }
3590
- }
3700
+ } else this.dragScrollEdges.clear();
3591
3701
  if (moved) this.track(session, pointer);
3592
3702
  this.dragScrollFrame = this.win.requestAnimationFrame(tick);
3593
3703
  };
3594
3704
  this.dragScrollFrame = this.win.requestAnimationFrame(tick);
3595
3705
  }
3706
+ onTouchStart = (ev) => {
3707
+ const first = ev.touches[0];
3708
+ const second = ev.touches[1];
3709
+ if (!first || !second) {
3710
+ this.endPinch();
3711
+ return;
3712
+ }
3713
+ const view = this.current;
3714
+ if (this.session?.active || !this.opts.pinchZoom || !(view instanceof TimeGridView)) return;
3715
+ const body = first.target.closest(".tnc-tg-body");
3716
+ if (!body?.contains(second.target)) return;
3717
+ this.endSession(null, true);
3718
+ if (this.pinch) return;
3719
+ const { top, height } = body.getBoundingClientRect();
3720
+ const y = (first.clientY + second.clientY) / 2;
3721
+ this.pinch = {
3722
+ body,
3723
+ distance: Math.max(1, Math.hypot(first.clientX - second.clientX, first.clientY - second.clientY)),
3724
+ y,
3725
+ anchor: (y - top) / height,
3726
+ drawn: view.drawnSlotHeight,
3727
+ option: this.opts.slotHeight
3728
+ };
3729
+ };
3596
3730
  onTouchMove = (ev) => {
3597
3731
  if (this.session?.active) ev.preventDefault();
3732
+ const p = this.pinch;
3733
+ const first = ev.touches[0];
3734
+ const second = ev.touches[1];
3735
+ if (!p || !first || !second) return;
3736
+ if (!ev.cancelable) {
3737
+ this.endPinch();
3738
+ return;
3739
+ }
3740
+ ev.preventDefault();
3741
+ const height = p.drawn * Math.hypot(first.clientX - second.clientX, first.clientY - second.clientY) / p.distance;
3742
+ p.y = (first.clientY + second.clientY) / 2;
3743
+ this.pinchTo(p, Math.min(Math.max(p.drawn, PINCH_MAX_SLOT), Math.max(Math.min(p.drawn, PINCH_MIN_SLOT), height)));
3598
3744
  };
3745
+ onTouchEnd = (ev) => {
3746
+ if (ev.touches.length < 2) this.endPinch();
3747
+ };
3748
+ /** Rescales the grid, keeping the anchored time under the fingers. */
3749
+ pinchTo(p, slotHeight) {
3750
+ this.opts = { ...this.opts, slotHeight };
3751
+ if (!(this.current instanceof TimeGridView)) return;
3752
+ this.current.applyScale();
3753
+ const { top, height } = p.body.getBoundingClientRect();
3754
+ const scroller = scrollerOf(p.body, "y");
3755
+ if (scroller) scroller.scrollTop += top + height * p.anchor - p.y;
3756
+ }
3757
+ endPinch() {
3758
+ const p = this.pinch;
3759
+ this.pinch = null;
3760
+ if (!p?.body.isConnected) return;
3761
+ const settled = Math.round(this.opts.slotHeight);
3762
+ const slotHeight = settled === Math.round(p.drawn) ? p.option : settled;
3763
+ this.pinchTo(p, slotHeight);
3764
+ this.current?.render(null);
3765
+ if (slotHeight !== p.option) this.opts.onSlotHeightChange?.(slotHeight);
3766
+ }
3599
3767
  onPointerMove = (ev) => {
3600
3768
  const s = this.session;
3601
3769
  if (!s || ev.pointerId !== s.pointerId) return;
@@ -3945,6 +4113,7 @@ var Calendar = class {
3945
4113
  scroller.scrollLeft = position.left;
3946
4114
  }
3947
4115
  this.dragScrollOrigins.clear();
4116
+ this.dragScrollEdges.clear();
3948
4117
  this.session = null;
3949
4118
  this.listDragPreview?.element.remove();
3950
4119
  this.listDragPreview = null;
@@ -3963,10 +4132,15 @@ var Calendar = class {
3963
4132
  );
3964
4133
  const draggedId = this.draggingId;
3965
4134
  this.draggingId = null;
4135
+ this.dragWidth = null;
3966
4136
  this.dragRow = null;
3967
- if (draggedId) for (const el of this.elementsFor(draggedId)) el.classList.remove("is-dragging");
4137
+ if (draggedId) for (const el of this.elementsFor(draggedId)) {
4138
+ el.classList.remove("is-dragging");
4139
+ el.style.removeProperty("--tnc-drag-width");
4140
+ }
3968
4141
  this.strip?.markTarget(null);
3969
4142
  if (!s.active) return;
4143
+ if (this.current instanceof DayGridView) this.current.render(null);
3970
4144
  if (s.mode === "select") {
3971
4145
  if (cancel) this.unselectSilently();
3972
4146
  else if (this.selection) this.fireSelect(this.selection, ev);
package/dist/esm/index.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  TIMELINE_UNIT_DAYS,
8
8
  defaultOptions,
9
9
  localeFirstDay
10
- } from "./chunk-EXKFHPW2.js";
10
+ } from "./chunk-5PHSJK36.js";
11
11
  import {
12
12
  DAY_MS,
13
13
  ItemStore,
@@ -91,6 +91,8 @@ var BASES_CALENDAR_DEFAULTS = {
91
91
  slotDuration: "00:30:00",
92
92
  snapDuration: "00:30:00",
93
93
  scrollTime: "08:00:00",
94
+ slotHeight: 24,
95
+ pinchZoom: true,
94
96
  nowIndicator: true,
95
97
  showTimeGrid: true,
96
98
  selectMirror: true,
@@ -155,6 +157,8 @@ function basesLayoutOptions(s, context = {}) {
155
157
  slotDuration,
156
158
  snapDuration: time("snapDuration") ?? slotDuration,
157
159
  scrollTime: time("scrollTime", 48) ?? d.scrollTime,
160
+ slotHeight: clamp(Math.round(num(s.slotHeight, d.slotHeight)), 5, 96),
161
+ pinchZoom: pick("pinchZoom") !== false,
158
162
  nowIndicator: pick("nowIndicator") !== false,
159
163
  timeGrid: showTimeGrid,
160
164
  selectMirror: pick("selectMirror") !== false,
package/dist/esm/react.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  Calendar
3
- } from "./chunk-EXKFHPW2.js";
3
+ } from "./chunk-5PHSJK36.js";
4
4
  import "./chunk-66A6EMCU.js";
5
5
 
6
6
  // src/react/index.tsx
@@ -39,8 +39,10 @@ export declare class Calendar<D = unknown> implements ViewContext<D> {
39
39
  draggingId: string | null;
40
40
  private listDragPreview;
41
41
  private dragSources;
42
+ private dragWidth;
42
43
  private dragScrollFrame;
43
44
  private readonly dragScrollOrigins;
45
+ private readonly dragScrollEdges;
44
46
  private readonly itemWriteStates;
45
47
  dragRow: {
46
48
  id: string;
@@ -49,6 +51,7 @@ export declare class Calendar<D = unknown> implements ViewContext<D> {
49
51
  strip: DateStrip<D> | null;
50
52
  private stripSize;
51
53
  private rebuilding;
54
+ private pinch;
52
55
  private readonly toolbarEl;
53
56
  private readonly viewHost;
54
57
  private current;
@@ -204,7 +207,12 @@ export declare class Calendar<D = unknown> implements ViewContext<D> {
204
207
  private readonly onPointerDown;
205
208
  private activate;
206
209
  private startDragScroll;
210
+ private readonly onTouchStart;
207
211
  private readonly onTouchMove;
212
+ private readonly onTouchEnd;
213
+ /** Rescales the grid, keeping the anchored time under the fingers. */
214
+ private pinchTo;
215
+ private endPinch;
208
216
  private readonly onPointerMove;
209
217
  private readonly onPointerUp;
210
218
  private readonly onPointerCancel;
@@ -39,8 +39,10 @@ export declare class Calendar<D = unknown> implements ViewContext<D> {
39
39
  draggingId: string | null;
40
40
  private listDragPreview;
41
41
  private dragSources;
42
+ private dragWidth;
42
43
  private dragScrollFrame;
43
44
  private readonly dragScrollOrigins;
45
+ private readonly dragScrollEdges;
44
46
  private readonly itemWriteStates;
45
47
  dragRow: {
46
48
  id: string;
@@ -49,6 +51,7 @@ export declare class Calendar<D = unknown> implements ViewContext<D> {
49
51
  strip: DateStrip<D> | null;
50
52
  private stripSize;
51
53
  private rebuilding;
54
+ private pinch;
52
55
  private readonly toolbarEl;
53
56
  private readonly viewHost;
54
57
  private current;
@@ -204,7 +207,12 @@ export declare class Calendar<D = unknown> implements ViewContext<D> {
204
207
  private readonly onPointerDown;
205
208
  private activate;
206
209
  private startDragScroll;
210
+ private readonly onTouchStart;
207
211
  private readonly onTouchMove;
212
+ private readonly onTouchEnd;
213
+ /** Rescales the grid, keeping the anchored time under the fingers. */
214
+ private pinchTo;
215
+ private endPinch;
208
216
  private readonly onPointerMove;
209
217
  private readonly onPointerUp;
210
218
  private readonly onPointerCancel;
@@ -1,10 +1,11 @@
1
1
  import { type Day } from "../core/date.cjs";
2
2
  /** Locale-aware labels. Formatting happens on UTC dates built from civil days, so zones never shift them. */
3
3
  export declare class Formatter {
4
+ private readonly formatTime?;
4
5
  private readonly cache;
5
6
  readonly locale: string | undefined;
6
7
  readonly hour12: boolean;
7
- constructor(locale: string | undefined, hour12: boolean | undefined);
8
+ constructor(locale: string | undefined, hour12: boolean | undefined, formatTime?: ((minute: number) => string) | undefined);
8
9
  private fmt;
9
10
  private d;
10
11
  weekdayShort(day: Day): string;
@@ -1,10 +1,11 @@
1
1
  import { type Day } from "../core/date.js";
2
2
  /** Locale-aware labels. Formatting happens on UTC dates built from civil days, so zones never shift them. */
3
3
  export declare class Formatter {
4
+ private readonly formatTime?;
4
5
  private readonly cache;
5
6
  readonly locale: string | undefined;
6
7
  readonly hour12: boolean;
7
- constructor(locale: string | undefined, hour12: boolean | undefined);
8
+ constructor(locale: string | undefined, hour12: boolean | undefined, formatTime?: ((minute: number) => string) | undefined);
8
9
  private fmt;
9
10
  private d;
10
11
  weekdayShort(day: Day): string;