@tasknotes/calendar 0.1.7 → 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",
@@ -982,8 +989,9 @@ var DayGridView = class {
982
989
  constructor(ctx) {
983
990
  this.ctx = ctx;
984
991
  const { doc, opts, days } = ctx;
985
- this.dotsMode = opts.monthDisplay === "dots";
992
+ this.dotsMode = opts.monthDisplay === "dots" || opts.monthDisplay === "chips";
986
993
  this.el = h(doc, "div", this.dotsMode ? "tnc-dg tnc-dg--dots" : "tnc-dg");
994
+ this.el.classList.toggle("tnc-dg--chips", opts.monthDisplay === "chips");
987
995
  this.scroller = h(doc, "div", "tnc-dg-scroll");
988
996
  this.grid = h(doc, "div", "tnc-dg-grid", { role: "grid", "aria-label": ctx.view.title });
989
997
  const perWeek = this.daysPerWeek();
@@ -1013,14 +1021,14 @@ var DayGridView = class {
1013
1021
  const cell = h(doc, "div", cls.join(" "), { role: "gridcell", "aria-label": ctx.fmt.longDate(d), "data-date": arg.dateStr });
1014
1022
  if (this.dotsMode) cell.tabIndex = 0;
1015
1023
  const num = ctx.fmt.dayNumber(d);
1016
- const label = ymdOfDay(d).day === 1 ? `${ctx.fmt.monthShort(d)} ${num}` : num;
1024
+ const label = !this.dotsMode && ymdOfDay(d).day === 1 ? `${ctx.fmt.monthShort(d)} ${num}` : num;
1017
1025
  cell.append(
1018
1026
  h(
1019
1027
  doc,
1020
1028
  "div",
1021
1029
  "tnc-dg-top",
1022
1030
  null,
1023
- opts.navLinks ? ctx.navLink(d, label, "tnc-dg-num") : h(doc, "span", "tnc-dg-num", null, label)
1031
+ opts.navLinks && !this.dotsMode ? ctx.navLink(d, label, "tnc-dg-num") : h(doc, "span", "tnc-dg-num", null, label)
1024
1032
  )
1025
1033
  );
1026
1034
  dates.append(cell);
@@ -1034,20 +1042,20 @@ var DayGridView = class {
1034
1042
  layer.append(content);
1035
1043
  row.append(dates, layer);
1036
1044
  this.grid.append(row);
1037
- const dots = this.dotsMode ? cells.map((cell) => {
1045
+ const dots = cells.map((cell) => {
1038
1046
  const box = h(doc, "div", "tnc-dg-dots", { "aria-hidden": "true" });
1039
1047
  if (opts.dayMaxEvents === false) box.classList.add("is-unlimited");
1040
1048
  const count = h(doc, "span", "tnc-dg-dots-more");
1041
1049
  cell.append(box);
1042
1050
  return { box, pool: new ItemPool(ctx, box), count };
1043
- }) : null;
1051
+ });
1044
1052
  this.weeks.push({ days: wdays, row, cells, layer, sel, more, pool: new ItemPool(ctx, content), dots });
1045
1053
  }
1046
1054
  this.scroller.append(this.grid);
1047
1055
  this.el.append(this.scroller);
1048
1056
  this.el.classList.toggle("tnc-dg--auto", opts.height === "auto");
1049
1057
  const RO = ctx.win.ResizeObserver;
1050
- if (RO && (opts.dayMaxEvents === true || this.dotsMode && opts.dayMaxEvents !== false)) {
1058
+ if (RO) {
1051
1059
  this.resizeObs = new RO(() => {
1052
1060
  if (this.sizeKey() !== this.lastHeights) this.render(null);
1053
1061
  });
@@ -1065,6 +1073,7 @@ var DayGridView = class {
1065
1073
  topPx = 24;
1066
1074
  lastHeights = "";
1067
1075
  dotsMode;
1076
+ dragPreviews = [];
1068
1077
  daysPerWeek() {
1069
1078
  const { days } = this.ctx;
1070
1079
  const first = days[0];
@@ -1078,16 +1087,71 @@ var DayGridView = class {
1078
1087
  }
1079
1088
  render(_change) {
1080
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
+ }
1120
+ this.dotsMode = ctx.opts.monthDisplay !== "full" || this.grid.clientWidth / this.daysPerWeek() < 72;
1121
+ this.el.classList.toggle("tnc-dg--dots", this.dotsMode);
1122
+ this.el.classList.toggle("tnc-dg--compact", this.dotsMode && ctx.opts.monthDisplay === "full");
1123
+ this.el.classList.toggle("tnc-dg--chips", ctx.opts.monthDisplay === "chips" && this.weeks[0].dots[0].box.clientHeight >= 44);
1081
1124
  const cs = ctx.win.getComputedStyle(this.el);
1082
1125
  this.barPx = px(cs, "--tnc-bar-h", 20);
1083
1126
  this.topPx = this.weeks[0]?.cells[0]?.querySelector(".tnc-dg-top")?.offsetHeight || px(cs, "--tnc-dg-top-h", 24);
1084
1127
  this.morePx = this.measureMore(cs);
1085
1128
  this.lastHeights = this.sizeKey();
1086
1129
  for (const w of this.weeks) {
1087
- if (w.dots) this.renderDots(w);
1088
- else this.renderWeek(w);
1130
+ for (const cell of w.cells) {
1131
+ if (this.dotsMode) cell.tabIndex = 0;
1132
+ else cell.removeAttribute("tabindex");
1133
+ if (this.dotsMode && ctx.opts.monthDisplay === "full") cell.setAttribute("aria-haspopup", "dialog");
1134
+ else cell.removeAttribute("aria-haspopup");
1135
+ }
1136
+ for (const cell of w.dots) cell.box.hidden = !this.dotsMode;
1137
+ if (this.dotsMode) {
1138
+ w.pool.begin();
1139
+ w.pool.end();
1140
+ w.more.replaceChildren();
1141
+ this.renderDots(w);
1142
+ } else {
1143
+ for (const cell of w.dots) {
1144
+ cell.pool.begin();
1145
+ cell.pool.end();
1146
+ }
1147
+ this.renderWeek(w);
1148
+ }
1089
1149
  }
1090
1150
  }
1151
+ clearDragPreviews() {
1152
+ for (const preview of this.dragPreviews) this.ctx.destroyItemEl(preview);
1153
+ this.dragPreviews = [];
1154
+ }
1091
1155
  /** Height of a "+N more" link as the host styles it (`--tnc-more-h`, or a taller min-height). */
1092
1156
  measureMore(cs) {
1093
1157
  const fallback = px(cs, "--tnc-more-h", this.barPx);
@@ -1184,7 +1248,8 @@ var DayGridView = class {
1184
1248
  w.more.append(link);
1185
1249
  }
1186
1250
  existing.delete(key);
1187
- link.textContent = ctx.text.more(items.length);
1251
+ link.setAttribute("aria-label", ctx.text.more(items.length));
1252
+ link.textContent = w.row.clientWidth / n < 72 ? `+${new Intl.NumberFormat(ctx.opts.locale).format(items.length)}` : ctx.text.more(items.length);
1188
1253
  setStyle(link, {
1189
1254
  left: `calc(${pct(col / n)} + 2px)`,
1190
1255
  width: `calc(${pct(1 / n)} - 4px)`,
@@ -1214,10 +1279,11 @@ var DayGridView = class {
1214
1279
  const { dayMaxEvents } = ctx.opts;
1215
1280
  const first = w.dots[0].box;
1216
1281
  const cs = ctx.win.getComputedStyle(first);
1217
- const size = px(cs, "--tnc-dot-size", 8);
1282
+ const chips = this.el.classList.contains("tnc-dg--chips");
1283
+ const size = chips ? px(cs, "--tnc-chip-h", 14) : px(cs, "--tnc-dot-size", 8);
1218
1284
  const gap = px(cs, "--tnc-dot-gap", 3);
1219
1285
  const step = size + gap;
1220
- const perRow = Math.max(1, Math.floor((first.clientWidth + gap) / step));
1286
+ const perRow = chips ? 1 : Math.max(1, Math.floor((first.clientWidth + gap) / step));
1221
1287
  const rows = Math.max(1, Math.floor((first.clientHeight + gap) / step));
1222
1288
  const fits = dayMaxEvents === false ? Infinity : perRow * rows;
1223
1289
  const cap = typeof dayMaxEvents === "number" && dayMaxEvents > 0 ? Math.min(dayMaxEvents, fits) : fits;
@@ -1226,7 +1292,7 @@ var DayGridView = class {
1226
1292
  w.days.forEach((day, i) => {
1227
1293
  const cell = w.dots[i];
1228
1294
  const items = this.itemsOn(day);
1229
- const shown = items.length <= cap ? items.length : cap >= fits ? Math.max(0, cap - 2) : cap;
1295
+ const shown = items.length <= cap ? items.length : cap >= fits ? Math.max(1, cap - (chips ? 1 : 2)) : Math.max(1, cap);
1230
1296
  cell.pool.begin();
1231
1297
  const els = items.slice(0, shown).map((n) => cell.pool.place(n.id, { n, day, isStart: n.startDay >= day, isEnd: n.endDay <= day + 1, variant: "indicator", mirror: false }));
1232
1298
  cell.pool.end();
@@ -1277,6 +1343,7 @@ var DayGridView = class {
1277
1343
  return this.weeks.flatMap((w) => [...w.pool.elementsFor(id), ...(w.dots ?? []).flatMap((c) => c.pool.elementsFor(id))]);
1278
1344
  }
1279
1345
  destroy() {
1346
+ this.clearDragPreviews();
1280
1347
  this.resizeObs?.disconnect();
1281
1348
  for (const w of this.weeks) {
1282
1349
  w.pool.clear();
@@ -1329,6 +1396,7 @@ var ListView = class {
1329
1396
  frame = null;
1330
1397
  /** A picked day stays marked until the list is scrolled by hand. */
1331
1398
  pinned = false;
1399
+ pinnedTop = 0;
1332
1400
  itemsOn(day) {
1333
1401
  const { store, opts } = this.ctx;
1334
1402
  const out = store.spanningIn(day, day + 1, true);
@@ -1343,6 +1411,7 @@ var ListView = class {
1343
1411
  const leadItems = [];
1344
1412
  const inLead = /* @__PURE__ */ new Set();
1345
1413
  const keep = /* @__PURE__ */ new Set();
1414
+ let itemCount = 0;
1346
1415
  let prev = null;
1347
1416
  for (const day of days) {
1348
1417
  let items = this.itemsOn(day);
@@ -1356,7 +1425,7 @@ var ListView = class {
1356
1425
  return false;
1357
1426
  });
1358
1427
  }
1359
- if (!items.length && !agenda) continue;
1428
+ if (!items.length && !agenda && opts.listDayCount !== 1) continue;
1360
1429
  keep.add(day);
1361
1430
  let section = this.sections.get(day);
1362
1431
  let pool = this.pools.get(day);
@@ -1375,6 +1444,7 @@ var ListView = class {
1375
1444
  this.pools.set(day, pool);
1376
1445
  }
1377
1446
  section.classList.toggle("is-empty", !items.length);
1447
+ itemCount += items.length;
1378
1448
  this.fill(pool, items, () => day);
1379
1449
  if (prev ? prev.nextSibling !== section : this.body.firstChild !== section) {
1380
1450
  if (prev) prev.after(section);
@@ -1390,13 +1460,14 @@ var ListView = class {
1390
1460
  this.sections.delete(day);
1391
1461
  }
1392
1462
  this.renderLead(leadItems);
1393
- let empty = this.body.querySelector(":scope > .tnc-list-empty");
1394
- if (!keep.size && !leadItems.length) {
1463
+ let empty = this.body.querySelector(".tnc-list-empty");
1464
+ if (!itemCount && !leadItems.length && (!agenda || opts.listEmptyContent)) {
1395
1465
  if (!empty) {
1396
1466
  empty = h(doc, "div", "tnc-list-empty", { role: "listitem" }, ctx.text.noItems);
1397
1467
  const content = ctx.opts.listEmptyContent?.();
1398
1468
  if (content) empty.replaceChildren(content);
1399
- this.body.append(empty);
1469
+ if (this.sections.size === 1) this.sections.values().next().value.append(empty);
1470
+ else this.body.prepend(empty);
1400
1471
  }
1401
1472
  } else empty?.remove();
1402
1473
  this.ctx.strip?.draw();
@@ -1454,9 +1525,10 @@ var ListView = class {
1454
1525
  const section = day === this.ctx.days[0] && this.lead ? this.lead.section : this.sections.get(day);
1455
1526
  if (!section) return;
1456
1527
  this.ctx.strip?.measure();
1457
- section.scrollIntoView({ block: "start" });
1528
+ this.scroller.scrollTop += section.getBoundingClientRect().top - this.scroller.getBoundingClientRect().top;
1458
1529
  this.ctx.strip?.setFocus(day);
1459
1530
  this.pinned = true;
1531
+ this.pinnedTop = this.scroller.scrollTop;
1460
1532
  }
1461
1533
  fold = () => {
1462
1534
  this.pinned = false;
@@ -1464,6 +1536,7 @@ var ListView = class {
1464
1536
  if (strip?.expanded) strip.setExpanded(false);
1465
1537
  };
1466
1538
  onScroll = () => {
1539
+ if (this.pinned && Math.abs(this.scroller.scrollTop - this.pinnedTop) > 1) this.pinned = false;
1467
1540
  if (this.frame !== null) return;
1468
1541
  this.frame = this.ctx.win.requestAnimationFrame(() => {
1469
1542
  this.frame = null;
@@ -1474,7 +1547,7 @@ var ListView = class {
1474
1547
  followTop() {
1475
1548
  const strip = this.ctx.strip;
1476
1549
  if (!strip || this.pinned) return;
1477
- const edge = strip.el.getBoundingClientRect().bottom + 1;
1550
+ const edge = Math.max(strip.el.getBoundingClientRect().bottom, this.scroller.getBoundingClientRect().top) + 1;
1478
1551
  for (const day of this.ctx.days) {
1479
1552
  const section = this.sections.get(day);
1480
1553
  if (section && section.getBoundingClientRect().bottom > edge) {
@@ -1522,10 +1595,11 @@ var ListView = class {
1522
1595
  // src/dom/views/strip.ts
1523
1596
  var SWIPE_PX = 40;
1524
1597
  var DateStrip = class {
1525
- constructor(ctx, pick, focus, onShown) {
1598
+ constructor(ctx, pick, focus, onShown, onPage) {
1526
1599
  this.ctx = ctx;
1527
1600
  this.pick = pick;
1528
1601
  this.onShown = onShown;
1602
+ this.onPage = onPage;
1529
1603
  const { doc, text } = ctx;
1530
1604
  this.focus = focus;
1531
1605
  this.shown = startOfWeek(focus, ctx.firstDay);
@@ -1543,13 +1617,16 @@ var DateStrip = class {
1543
1617
  head.append(this.title, this.todayBtn, prev, next, this.slot);
1544
1618
  this.dows = h(doc, "div", "tnc-strip-dows", { "aria-hidden": "true" });
1545
1619
  this.grid = h(doc, "div", "tnc-strip-grid", { role: "group" });
1546
- const handle = h(doc, "button", "tnc-strip-handle", { type: "button", tabindex: "-1", "aria-hidden": "true" });
1547
- handle.addEventListener("click", () => this.setExpanded(!this.expanded));
1620
+ const handle = h(doc, "div", "tnc-strip-handle", { "aria-hidden": "true" });
1548
1621
  this.el.append(head, this.dows, this.grid, handle);
1549
1622
  this.grid.addEventListener("click", this.onClick);
1550
1623
  this.grid.addEventListener("keydown", this.onKeyDown);
1551
1624
  this.el.addEventListener("pointerdown", this.onPointerDown);
1552
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 });
1553
1630
  this.el.addEventListener("pointercancel", this.onPointerCancel);
1554
1631
  this.el.addEventListener("click", this.onClickCapture, true);
1555
1632
  this.draw();
@@ -1558,6 +1635,7 @@ var DateStrip = class {
1558
1635
  ctx;
1559
1636
  pick;
1560
1637
  onShown;
1638
+ onPage;
1561
1639
  el;
1562
1640
  title;
1563
1641
  dows;
@@ -1616,6 +1694,7 @@ var DateStrip = class {
1616
1694
  step(dir) {
1617
1695
  this.shown = this.expanded ? addMonths(this.shown, dir) : this.shown + 7 * dir;
1618
1696
  this.draw();
1697
+ this.onPage?.(this.expanded ? this.shown : this.focus + 7 * dir);
1619
1698
  }
1620
1699
  days() {
1621
1700
  if (!this.expanded) return Array.from({ length: 7 }, (_, i) => this.shown + i);
@@ -1640,7 +1719,8 @@ var DateStrip = class {
1640
1719
  const tabDay = days.includes(this.focus) ? this.focus : days[0];
1641
1720
  const cells = days.map((d) => {
1642
1721
  const arg = ctx.dayArg(d);
1643
- 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;
1644
1724
  const first = ymdOfDay(d).day === 1 && d !== days[0] && !this.expanded;
1645
1725
  const label = fmt.longDate(d);
1646
1726
  const cell = h(
@@ -1664,7 +1744,15 @@ var DateStrip = class {
1664
1744
  }
1665
1745
  );
1666
1746
  if (first) cell.append(h(doc, "span", "tnc-strip-month", { "aria-hidden": "true" }, fmt.monthShort(d)));
1667
- 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);
1668
1756
  return cell;
1669
1757
  });
1670
1758
  this.grid.replaceChildren(...cells);
@@ -1704,6 +1792,9 @@ var DateStrip = class {
1704
1792
  if (ev.button !== 0 || this.ctx.draggingId) return;
1705
1793
  this.swipe = { id: ev.pointerId, x: ev.clientX, y: ev.clientY };
1706
1794
  };
1795
+ ownTouch = (event) => {
1796
+ event.stopPropagation();
1797
+ };
1707
1798
  onPointerUp = (ev) => {
1708
1799
  const s = this.swipe;
1709
1800
  this.swipe = null;
@@ -1743,6 +1834,7 @@ var TimeGridView = class {
1743
1834
  const showGrid = opts.timeGrid;
1744
1835
  this.el = h(doc, "div", "tnc-tg", { role: "table", "aria-label": ctx.view.title });
1745
1836
  this.el.classList.toggle("tnc-tg--no-grid", !showGrid);
1837
+ this.el.classList.toggle("tnc-tg--pinch", showGrid && opts.pinchZoom);
1746
1838
  this.scroller = h(doc, "div", "tnc-tg-scroll");
1747
1839
  this.dayNavigation = h(doc, "div", "tnc-tg-day-navigation", { role: "group", "aria-label": ctx.view.title });
1748
1840
  this.scroller.addEventListener("scroll", () => this.updateDayNavigation());
@@ -1892,6 +1984,11 @@ var TimeGridView = class {
1892
1984
  }
1893
1985
  if (this.nowGutter) gutter.append(this.nowGutter);
1894
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. */
1895
1992
  applyScale() {
1896
1993
  const { opts, slot } = this.ctx;
1897
1994
  this.ppm = opts.slotHeight / slot.duration;
@@ -2072,15 +2169,17 @@ var TimeGridView = class {
2072
2169
  const { opts } = ctx;
2073
2170
  const readableWidth = Number.parseFloat(ctx.win.getComputedStyle(this.el).getPropertyValue("--tnc-event-min-width")) || 128;
2074
2171
  const laneWidth = c.col.clientWidth * (lane === "actual" && placements.some((placement) => placement.split) ? 0.22 : 1);
2075
- const collapse = placements.some((placement) => {
2172
+ const overlapThreshold = opts.overlapStackThreshold ?? 0;
2173
+ const collapsedClusters = new Set(placements.filter((placement) => {
2076
2174
  const columns = opts.eventMaxStack > 0 ? Math.min(placement.cols, opts.eventMaxStack) : placement.cols;
2077
- return columns > 1 && columns * readableWidth > laneWidth;
2078
- });
2079
- const withMore = lane === "plan" || collapse;
2080
- 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));
2081
2178
  const hiddenByCluster = /* @__PURE__ */ new Map();
2082
2179
  pool.begin();
2083
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;
2084
2183
  const preview = p.item.item.preview || p.item.id === ctx.draggingId;
2085
2184
  if (!preview && withMore && maxStack > 0 && p.cols > maxStack && p.col >= maxStack - 1) {
2086
2185
  const rec = hiddenByCluster.get(p.cluster) ?? { count: 0, start: p.startMin, end: p.endMin, split: p.split, items: [] };
@@ -2093,7 +2192,7 @@ var TimeGridView = class {
2093
2192
  }
2094
2193
  const cols = !preview && withMore && maxStack > 0 ? Math.min(p.cols, maxStack) : p.cols;
2095
2194
  const span = Math.min(p.span, cols - p.col);
2096
- const left = p.col / cols;
2195
+ const left = p.item.id === ctx.draggingId ? 0 : p.col / cols;
2097
2196
  let width = span / cols;
2098
2197
  if (opts.slotEventOverlap && cols > 1 && p.col + span < cols) width = Math.min(1 - left, width * 1.7);
2099
2198
  const el = pool.place(p.item.id, this.meta(p, false));
@@ -2108,8 +2207,9 @@ var TimeGridView = class {
2108
2207
  });
2109
2208
  }
2110
2209
  pool.end();
2111
- if (!withMore) return;
2112
2210
  for (const [cluster, rec] of hiddenByCluster) {
2211
+ const collapse = collapsedClusters.has(cluster);
2212
+ const maxStack = collapse ? 1 : opts.eventMaxStack;
2113
2213
  const key = `${lane}-${cluster}`;
2114
2214
  let link = c.more.querySelector(`[data-cluster="${key}"]`);
2115
2215
  if (!link) {
@@ -2123,8 +2223,15 @@ var TimeGridView = class {
2123
2223
  link.classList.toggle("tnc-more--collision", collapse);
2124
2224
  link.textContent = `+${rec.count}`;
2125
2225
  if (collapse) {
2126
- link.textContent = ctx.text.appointments(rec.count);
2127
- link.setAttribute("aria-label", link.textContent);
2226
+ const items = placements.filter((placement) => placement.cluster === cluster).map((placement) => placement.item.item.title);
2227
+ const count = ctx.text.appointments(rec.count);
2228
+ const interval = `${ctx.fmt.time(rec.start)} \u2013 ${ctx.fmt.time(rec.end)}`;
2229
+ link.replaceChildren(
2230
+ h(ctx.doc, "span", "tnc-collision-count", null, String(rec.count)),
2231
+ h(ctx.doc, "span", "tnc-collision-range", null, interval),
2232
+ h(ctx.doc, "span", "tnc-collision-title", { "aria-hidden": "true" }, items.join(" \xB7 "))
2233
+ );
2234
+ link.setAttribute("aria-label", `${count}, ${interval}: ${items.join(", ")}`);
2128
2235
  link.title = link.getAttribute("aria-label");
2129
2236
  } else {
2130
2237
  link.removeAttribute("aria-label");
@@ -2133,6 +2240,7 @@ var TimeGridView = class {
2133
2240
  const cols = maxStack;
2134
2241
  setStyle(link, {
2135
2242
  top: top(rec.start),
2243
+ height: collapse ? pct((rec.end - rec.start) / this.minutes) : "",
2136
2244
  ...inLane(lane, rec.split, (cols - 1) / cols, 1 / cols)
2137
2245
  });
2138
2246
  }
@@ -2199,12 +2307,10 @@ var TimeGridView = class {
2199
2307
  const r2 = cells[i2].getBoundingClientRect();
2200
2308
  if (x >= r2.left && x < r2.right) return i2;
2201
2309
  }
2202
- const firstR = cells[0]?.getBoundingClientRect();
2203
- const lastR = cells[cells.length - 1]?.getBoundingClientRect();
2204
- if (firstR && x < firstR.left) return 0;
2205
- if (lastR && x >= lastR.right) return cells.length - 1;
2206
2310
  return -1;
2207
2311
  };
2312
+ const bounds = this.scroller.getBoundingClientRect();
2313
+ if (x < bounds.left || x >= bounds.right || y < bounds.top || y >= bounds.bottom) return null;
2208
2314
  if (this.allDayRow) {
2209
2315
  const r2 = this.allDayRow.getBoundingClientRect();
2210
2316
  if (y >= r2.top && y < r2.bottom || !this.body && y >= r2.top) {
@@ -2269,7 +2375,9 @@ var TimeGridView = class {
2269
2375
  scrollToTime(minute) {
2270
2376
  if (!this.body) return;
2271
2377
  const y = (minute - this.ctx.slot.min) * this.ppm;
2272
- 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);
2273
2381
  }
2274
2382
  getScroll() {
2275
2383
  return { top: this.scroller.scrollTop, left: this.scroller.scrollLeft };
@@ -2927,6 +3035,14 @@ ${this.ctx.text.more(items.length - 8)}` : ""}` : label;
2927
3035
  setScroll(s) {
2928
3036
  this.scroller.scrollTop = s.top;
2929
3037
  }
3038
+ revealDay(day) {
3039
+ const month = this.cells.find((cell) => cell.day === day)?.el.closest(".tnc-ym");
3040
+ if (!month) return;
3041
+ const bounds = month.getBoundingClientRect();
3042
+ const viewport = this.scroller.getBoundingClientRect();
3043
+ if (bounds.top < viewport.top) this.scroller.scrollTop += bounds.top - viewport.top;
3044
+ else if (bounds.bottom > viewport.bottom) this.scroller.scrollTop += bounds.top - viewport.top;
3045
+ }
2930
3046
  elementsFor(_id) {
2931
3047
  return [];
2932
3048
  }
@@ -2939,6 +3055,8 @@ ${this.ctx.text.more(items.length - 8)}` : ""}` : label;
2939
3055
  var EXTERNAL_ID = "__tnc_external__";
2940
3056
  var SVG_NS = "http://www.w3.org/2000/svg";
2941
3057
  var MAX_AGENDA_DAYS = 366;
3058
+ var PINCH_MIN_SLOT = 16;
3059
+ var PINCH_MAX_SLOT = 96;
2942
3060
  function sameOption(a, b, depth = 0) {
2943
3061
  if (a === b) return true;
2944
3062
  if (typeof a === "function" && typeof b === "function") return true;
@@ -2966,7 +3084,7 @@ var Calendar = class {
2966
3084
  this.win = this.doc.defaultView ?? globalThis;
2967
3085
  this.opts = { ...defaultOptions(), ...options };
2968
3086
  this.zone = getTimeZone(this.opts.timeZone);
2969
- this.fmt = new Formatter(this.opts.locale, this.opts.hour12);
3087
+ this.fmt = new Formatter(this.opts.locale, this.opts.hour12, this.opts.formatTime);
2970
3088
  this.text = this.resolveText();
2971
3089
  this.order = parseOrder(this.opts.eventOrder);
2972
3090
  this.store = new ItemStore(this.zone, {
@@ -3006,10 +3124,18 @@ var Calendar = class {
3006
3124
  conflicts = /* @__PURE__ */ new Map();
3007
3125
  slot = { min: 0, max: MINUTES_PER_DAY, duration: 30, snap: 30, scroll: 480 };
3008
3126
  draggingId = null;
3127
+ listDragPreview = null;
3128
+ dragSources = [];
3129
+ dragWidth = null;
3130
+ dragScrollFrame = null;
3131
+ dragScrollOrigins = /* @__PURE__ */ new Map();
3132
+ dragScrollEdges = /* @__PURE__ */ new Map();
3133
+ itemWriteStates = /* @__PURE__ */ new Map();
3009
3134
  dragRow = null;
3010
3135
  strip = null;
3011
3136
  stripSize = null;
3012
3137
  rebuilding = false;
3138
+ pinch = null;
3013
3139
  toolbarEl;
3014
3140
  viewHost;
3015
3141
  current = null;
@@ -3019,6 +3145,7 @@ var Calendar = class {
3019
3145
  session = null;
3020
3146
  selection = null;
3021
3147
  suppressClick = false;
3148
+ cancelledPointerId = null;
3022
3149
  selectedIds = /* @__PURE__ */ new Set();
3023
3150
  popover = null;
3024
3151
  timer = null;
@@ -3036,6 +3163,7 @@ var Calendar = class {
3036
3163
  this.positionMore();
3037
3164
  });
3038
3165
  this.resizeObs.observe(this.root);
3166
+ this.resizeObs.observe(this.toolbarEl);
3039
3167
  }
3040
3168
  }
3041
3169
  onAdoptCheck = () => {
@@ -3116,7 +3244,7 @@ var Calendar = class {
3116
3244
  defaultAllDayDays: this.opts.defaultAllDayDays
3117
3245
  });
3118
3246
  }
3119
- 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);
3120
3248
  if (has("text") || has("locale")) this.text = this.resolveText();
3121
3249
  if (has("eventOrder")) this.order = parseOrder(this.opts.eventOrder);
3122
3250
  if (has("date")) this.anchor = this.toDay(this.opts.date) ?? this.anchor;
@@ -3162,7 +3290,12 @@ var Calendar = class {
3162
3290
  gotoDate(date) {
3163
3291
  const day = this.toDay(date);
3164
3292
  if (day === null) return;
3293
+ const sameMonth = this.opts.view === "month" && startOfMonth(day) === startOfMonth(this.anchor);
3165
3294
  this.anchor = day;
3295
+ if (sameMonth) {
3296
+ this.view = { ...this.view, anchor: day };
3297
+ return;
3298
+ }
3166
3299
  this.rebuild(true);
3167
3300
  }
3168
3301
  prev() {
@@ -3308,7 +3441,8 @@ var Calendar = class {
3308
3441
  out.push(...classList(this.opts.dayClassNames?.(this.dayArg(day))));
3309
3442
  return out;
3310
3443
  }
3311
- isEditable(n, what) {
3444
+ isEditable(n, what, presentation = false) {
3445
+ if (!presentation && this.itemWriteStates.get(n.id) === "pending") return false;
3312
3446
  if (n.id === EXTERNAL_ID || n.item.preview || this.opts.view === "year") return false;
3313
3447
  if (this.opts.view === "list" && (!this.opts.listAgenda || what === "duration")) return false;
3314
3448
  const it = n.item;
@@ -3348,13 +3482,17 @@ var Calendar = class {
3348
3482
  if (preview || n.id === EXTERNAL_ID) el.classList.add("is-mirror");
3349
3483
  if (this.conflicts.has(n.id)) el.classList.add("is-conflict");
3350
3484
  const indicator = variant === "indicator";
3351
- if (!preview && !indicator && (this.isEditable(n, "start") || this.isEditable(n, "duration"))) el.classList.add("is-draggable");
3352
- if (this.draggingId === n.id) el.classList.add("is-dragging");
3485
+ if (!preview && !indicator && (this.isEditable(n, "start", true) || this.isEditable(n, "duration", true))) el.classList.add("is-draggable");
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
+ }
3353
3490
  if (this.selectedIds.has(n.id) && !preview) el.classList.add("is-selected");
3354
3491
  el.classList.add(...classList(it.className));
3355
3492
  const arg = this.itemArg(el, meta);
3356
3493
  el.classList.add(...classList(this.opts.itemClassNames?.(arg)));
3357
3494
  el.dataset.id = n.id;
3495
+ this.applyItemWriteState(el, n.id);
3358
3496
  if (it.kind) el.dataset.kind = it.kind;
3359
3497
  const color = it.color ?? it.borderColor;
3360
3498
  if (color) el.style.setProperty("--tnc-c", color);
@@ -3371,6 +3509,7 @@ var Calendar = class {
3371
3509
  const custom = variant === "list" ? this.opts.listItemContent?.(arg) : null;
3372
3510
  const content = indicator ? null : custom ?? this.opts.itemContent?.(arg);
3373
3511
  if (indicator) {
3512
+ if (this.opts.monthDisplay === "chips") el.append(h(this.doc, "span", "tnc-item-title", null, it.title));
3374
3513
  } else if (content != null) {
3375
3514
  const wrap = h(this.doc, "div", "tnc-item-main");
3376
3515
  wrap.append(content);
@@ -3383,6 +3522,7 @@ var Calendar = class {
3383
3522
  return el;
3384
3523
  }
3385
3524
  refreshItemEl(el, meta) {
3525
+ this.applyItemWriteState(el, meta.n.id);
3386
3526
  const time = el.querySelector(":scope .tnc-item-time");
3387
3527
  if (time) this.fillTime(time, meta.n, meta.variant, meta.day);
3388
3528
  el.classList.toggle("is-dragging", this.draggingId === meta.n.id);
@@ -3565,6 +3705,7 @@ var Calendar = class {
3565
3705
  this.current.scrollToTime(this.slot.scroll);
3566
3706
  this.current.revealDay(this.anchor);
3567
3707
  } else if (this.current instanceof TimelineView) this.current.scrollToDay(this.anchor);
3708
+ else if (this.current instanceof YearView) this.current.revealDay(this.anchor);
3568
3709
  if (rangeChanged) this.opts.onRangeChange?.(this.view);
3569
3710
  }
3570
3711
  /** Sets the visible days and view info from the anchor. */
@@ -3590,9 +3731,11 @@ var Calendar = class {
3590
3731
  }
3591
3732
  /** The range, run on to the end of the strip's days so their dots are right. */
3592
3733
  dataRangeFor(range) {
3593
- const end = this.strip ? Math.max(range.endDay, this.strip.window().end) : range.endDay;
3594
- if (end === range.endDay) return range;
3595
- return { ...range, endDay: end, end: new Date(toInstant(end, 0, this.zone)) };
3734
+ const window = this.strip?.window();
3735
+ const start = window ? Math.min(range.startDay, window.start) : range.startDay;
3736
+ const end = window ? Math.max(range.endDay, window.end) : range.endDay;
3737
+ if (start === range.startDay && end === range.endDay) return range;
3738
+ return { ...range, startDay: start, start: new Date(toInstant(start, 0, this.zone)), endDay: end, end: new Date(toInstant(end, 0, this.zone)) };
3596
3739
  }
3597
3740
  /** Adds, updates or removes the date strip for the current view. */
3598
3741
  syncStrip(navigated) {
@@ -3611,7 +3754,9 @@ var Calendar = class {
3611
3754
  this.root.classList.add("tnc--has-strip");
3612
3755
  const { startDay, endDay } = this.view.range;
3613
3756
  if (!this.strip) {
3614
- 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
+ });
3615
3760
  this.strip = strip;
3616
3761
  this.root.insertBefore(strip.el, this.viewHost);
3617
3762
  const RO = this.win.ResizeObserver;
@@ -3639,20 +3784,24 @@ var Calendar = class {
3639
3784
  stripShown() {
3640
3785
  if (!this.strip || this.rebuilding || !this.view) return;
3641
3786
  const next = this.dataRangeFor(this.view.range);
3642
- if (next.endDay === this.view.dataRange.endDay) return;
3787
+ if (next.startDay === this.view.dataRange.startDay && next.endDay === this.view.dataRange.endDay) return;
3643
3788
  this.view = { ...this.view, dataRange: next };
3644
3789
  this.opts.onRangeChange?.(this.view);
3645
3790
  }
3646
3791
  /** What is under a point, the date strip included. */
3647
3792
  hitAt(x, y) {
3793
+ const target = this.doc.elementFromPoint(x, y);
3794
+ if (!target || !this.root.contains(target)) {
3795
+ this.strip?.markTarget(null);
3796
+ return null;
3797
+ }
3648
3798
  const strip = this.strip;
3649
3799
  if (strip) {
3650
- const el = this.doc.elementFromPoint(x, y);
3651
- const day = el ? strip.dayAt(el) : null;
3800
+ const day = strip.dayAt(target);
3652
3801
  strip.markTarget(day !== null && this.draggingId ? day : null);
3653
3802
  if (day !== null) return { kind: "day", day };
3654
3803
  }
3655
- return this.current?.hit(x, y) ?? null;
3804
+ return this.current?.el.contains(target) ? this.current.hit(x, y) : null;
3656
3805
  }
3657
3806
  agendaList() {
3658
3807
  return this.opts.view === "list" && this.opts.listAgenda && this.current instanceof ListView ? this.current : null;
@@ -3766,11 +3915,13 @@ var Calendar = class {
3766
3915
  buttons.style.display = "flex";
3767
3916
  select.style.display = "none";
3768
3917
  const startWidth = Array.from(start.children).reduce((width, child) => width + child.scrollWidth, 0);
3769
- const compact = mode === "select" || mode === "auto" && startWidth + buttons.scrollWidth + 32 > this.root.clientWidth;
3918
+ const availableWidth = Math.min(this.root.clientWidth, this.toolbarEl.clientWidth);
3919
+ const compact = mode === "select" || mode === "auto" && startWidth + buttons.scrollWidth + 32 > availableWidth;
3770
3920
  buttons.hidden = compact;
3771
3921
  select.hidden = !compact;
3772
3922
  buttons.style.display = compact ? "none" : "flex";
3773
3923
  select.style.display = compact ? "" : "none";
3924
+ select.parentElement?.classList.toggle("tnc-toolbar-end--compact", compact);
3774
3925
  }
3775
3926
  applyChange(change) {
3776
3927
  if (!change.added.length && !change.updated.length && !change.removed.length && !change.days.size && !change.spans) return;
@@ -3835,7 +3986,7 @@ var Calendar = class {
3835
3986
  const s = wallTime(n.startMs, this.zone);
3836
3987
  const start = this.fmt.time(s.minute, true);
3837
3988
  if (variant === "dot" || variant === "bar" || variant === "indicator") return { start: s.day === day ? start : "", end: "" };
3838
- if (n.startMs === n.endMs) return { start, end: "" };
3989
+ if (n.startMs === n.endMs || n.item.timeDisplay === "start") return { start, end: "" };
3839
3990
  return { start, end: this.fmt.time(wallTime(n.endMs, this.zone).minute, true) };
3840
3991
  }
3841
3992
  fillTime(el, n, variant, day) {
@@ -3851,7 +4002,8 @@ var Calendar = class {
3851
4002
  }
3852
4003
  const s = wallTime(n.startMs, this.zone);
3853
4004
  const e = wallTime(n.endMs, this.zone);
3854
- return `${n.item.title}, ${this.fmt.longDate(s.day)} ${this.fmt.time(s.minute)} \u2013 ${this.fmt.time(e.minute)}`;
4005
+ const start = `${n.item.title}, ${this.fmt.longDate(s.day)} ${this.fmt.time(s.minute)}`;
4006
+ return n.item.timeDisplay === "start" || n.startMs === n.endMs ? start : `${start} \u2013 ${this.fmt.time(e.minute)}`;
3855
4007
  }
3856
4008
  defaultContent(meta) {
3857
4009
  const { n, variant, day } = meta;
@@ -3879,7 +4031,7 @@ var Calendar = class {
3879
4031
  addHandles(el, meta) {
3880
4032
  const { n, variant } = meta;
3881
4033
  const ports = variant === "timeline" && !meta.mirror && n.id !== EXTERNAL_ID && !!this.opts.timeline?.onLink;
3882
- if (variant === "dot" || !this.isEditable(n, "duration")) {
4034
+ if (variant === "dot" || !this.isEditable(n, "duration", true)) {
3883
4035
  if (ports) this.addPorts(el, n);
3884
4036
  return;
3885
4037
  }
@@ -3968,7 +4120,10 @@ var Calendar = class {
3968
4120
  r.addEventListener("dragover", this.onNativeDragOver);
3969
4121
  r.addEventListener("dragleave", this.onNativeDragLeave);
3970
4122
  r.addEventListener("drop", this.onNativeDrop);
4123
+ r.addEventListener("touchstart", this.onTouchStart, { passive: true });
3971
4124
  r.addEventListener("touchmove", this.onTouchMove, { passive: false });
4125
+ r.addEventListener("touchend", this.onTouchEnd);
4126
+ r.addEventListener("touchcancel", this.onTouchEnd);
3972
4127
  r.addEventListener("pointerover", this.onAdoptCheck);
3973
4128
  r.addEventListener("focusin", this.onAdoptCheck);
3974
4129
  this.doc.addEventListener("pointerdown", this.onDocPointerDown, true);
@@ -3987,7 +4142,10 @@ var Calendar = class {
3987
4142
  r.removeEventListener("dragover", this.onNativeDragOver);
3988
4143
  r.removeEventListener("dragleave", this.onNativeDragLeave);
3989
4144
  r.removeEventListener("drop", this.onNativeDrop);
4145
+ r.removeEventListener("touchstart", this.onTouchStart);
3990
4146
  r.removeEventListener("touchmove", this.onTouchMove);
4147
+ r.removeEventListener("touchend", this.onTouchEnd);
4148
+ r.removeEventListener("touchcancel", this.onTouchEnd);
3991
4149
  r.removeEventListener("pointerover", this.onAdoptCheck);
3992
4150
  r.removeEventListener("focusin", this.onAdoptCheck);
3993
4151
  this.doc.removeEventListener("pointerdown", this.onDocPointerDown, true);
@@ -4007,6 +4165,10 @@ var Calendar = class {
4007
4165
  return el && this.root.contains(el) && this.meta.has(el) && !this.meta.get(el)?.n.item.preview ? el : null;
4008
4166
  }
4009
4167
  onDocPointerDown = (ev) => {
4168
+ if (this.cancelledPointerId !== null) {
4169
+ this.cancelledPointerId = null;
4170
+ this.listenDoc(false);
4171
+ }
4010
4172
  const t = ev.target;
4011
4173
  if (this.popover && !this.popover.el.contains(t) && !this.popover.anchor.contains(t)) this.closeMore();
4012
4174
  if (this.selection && !this.root.contains(t)) this.unselect();
@@ -4014,6 +4176,7 @@ var Calendar = class {
4014
4176
  onPointerDown = (ev) => {
4015
4177
  if (ev.button !== 0 || this.session) return;
4016
4178
  const target = ev.target;
4179
+ if (target.closest(".tnc-dg--compact .tnc-dg-day")) return;
4017
4180
  if (target.closest("button, a, input, select, textarea, .tnc-toolbar, .tnc-port, [data-tnc-no-drag]")) {
4018
4181
  if (!target.closest(".tnc-item")) return;
4019
4182
  if (target.closest(".tnc-port, [data-tnc-no-drag], input, select, textarea")) return;
@@ -4046,7 +4209,7 @@ var Calendar = class {
4046
4209
  y0: ev.clientY,
4047
4210
  active: false,
4048
4211
  origin,
4049
- grab: overflowOrigin && this.current instanceof TimeGridView && !origin?.allDay ? { kind: "time", day: overflowOrigin.day, minute: overflowOrigin.minute } : this.current?.hit(ev.clientX, ev.clientY) ?? null,
4212
+ grab: overflowOrigin ? this.current instanceof TimeGridView && !origin?.allDay ? { kind: "time", day: overflowOrigin.day, minute: overflowOrigin.minute } : { kind: "day", day: this.meta.get(itemEl).day } : this.current?.hit(ev.clientX, ev.clientY) ?? null,
4050
4213
  proposal: null,
4051
4214
  denied: false,
4052
4215
  timer: null,
@@ -4071,20 +4234,159 @@ var Calendar = class {
4071
4234
  };
4072
4235
  activate(s) {
4073
4236
  s.active = true;
4237
+ if (s.lastEvent) this.leaveHover(s.lastEvent);
4074
4238
  if (s.timer !== null) this.win.clearTimeout(s.timer);
4075
4239
  s.timer = null;
4076
4240
  if (s.origin) {
4077
4241
  this.draggingId = s.origin.id;
4078
- for (const el of this.elementsFor(s.origin.id)) el.classList.add("is-dragging");
4242
+ for (const el of this.elementsFor(s.origin.id)) {
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") {
4248
+ const source = el.cloneNode(true);
4249
+ source.classList.remove("is-selected");
4250
+ source.classList.add("tnc-drag-source");
4251
+ source.setAttribute("aria-hidden", "true");
4252
+ source.inert = true;
4253
+ for (const node of [source, ...source.querySelectorAll("[id], [data-id], [data-task-path]")]) {
4254
+ node.removeAttribute("id");
4255
+ node.removeAttribute("data-id");
4256
+ node.removeAttribute("data-task-path");
4257
+ }
4258
+ el.before(source);
4259
+ this.dragSources.push(source);
4260
+ }
4261
+ if (this.opts.view === "list" && !this.listDragPreview && s.mode === "move") {
4262
+ const bounds = el.getBoundingClientRect();
4263
+ const preview = el.cloneNode(true);
4264
+ preview.classList.add("tnc-drag-preview", "is-dragging");
4265
+ preview.removeAttribute("data-id");
4266
+ preview.removeAttribute("data-task-path");
4267
+ preview.setAttribute("aria-hidden", "true");
4268
+ preview.inert = true;
4269
+ preview.style.width = `${bounds.width}px`;
4270
+ this.root.append(preview);
4271
+ this.listDragPreview = { element: preview, offsetX: s.x0 - bounds.left, offsetY: s.y0 - bounds.top, width: bounds.width, height: bounds.height };
4272
+ }
4273
+ el.classList.add("is-dragging");
4274
+ }
4079
4275
  }
4080
4276
  this.closeMore();
4081
4277
  if (s.mode !== "select") this.unselectSilently();
4082
4278
  this.root.classList.add("tnc-dragging", `tnc-dragging-${s.mode}`);
4083
4279
  if (s.lastEvent) this.track(s, s.lastEvent);
4280
+ this.startDragScroll();
4281
+ }
4282
+ startDragScroll() {
4283
+ if (this.dragScrollFrame !== null) return;
4284
+ const tick = () => {
4285
+ this.dragScrollFrame = null;
4286
+ const session = this.session;
4287
+ const pointer = session?.lastEvent;
4288
+ if (!session?.active || !pointer) return;
4289
+ const origin = session.origin ? this.elementsFor(session.origin.id)[0] : this.viewHost;
4290
+ const underPointer = this.doc.elementFromPoint(pointer.clientX, pointer.clientY);
4291
+ let moved = false;
4292
+ if (origin && underPointer && this.root.contains(underPointer)) {
4293
+ for (const axis of ["x", "y"]) {
4294
+ const scroller = scrollerOf(origin, axis);
4295
+ if (!scroller) continue;
4296
+ const viewport = scroller === this.doc.scrollingElement ? { left: 0, top: 0, right: this.win.innerWidth, bottom: this.win.innerHeight } : scroller.getBoundingClientRect();
4297
+ const coordinate = axis === "x" ? pointer.clientX : pointer.clientY;
4298
+ const lower = Math.max(0, axis === "x" ? viewport.left : viewport.top);
4299
+ const upper = Math.min(axis === "x" ? this.win.innerWidth : this.win.innerHeight, axis === "x" ? viewport.right : viewport.bottom);
4300
+ if (coordinate < lower || coordinate > upper) {
4301
+ this.dragScrollEdges.delete(axis);
4302
+ continue;
4303
+ }
4304
+ const delta = coordinate < lower + 32 ? -Math.min(12, (lower + 32 - coordinate) / 2) : coordinate > upper - 32 ? Math.min(12, (coordinate - upper + 32) / 2) : 0;
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;
4317
+ if (!this.dragScrollOrigins.has(scroller)) this.dragScrollOrigins.set(scroller, { top: scroller.scrollTop, left: scroller.scrollLeft });
4318
+ const before = axis === "x" ? scroller.scrollLeft : scroller.scrollTop;
4319
+ if (axis === "x") scroller.scrollLeft += delta;
4320
+ else scroller.scrollTop += delta;
4321
+ moved ||= before !== (axis === "x" ? scroller.scrollLeft : scroller.scrollTop);
4322
+ }
4323
+ } else this.dragScrollEdges.clear();
4324
+ if (moved) this.track(session, pointer);
4325
+ this.dragScrollFrame = this.win.requestAnimationFrame(tick);
4326
+ };
4327
+ this.dragScrollFrame = this.win.requestAnimationFrame(tick);
4084
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
+ };
4085
4353
  onTouchMove = (ev) => {
4086
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)));
4367
+ };
4368
+ onTouchEnd = (ev) => {
4369
+ if (ev.touches.length < 2) this.endPinch();
4087
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
+ }
4088
4390
  onPointerMove = (ev) => {
4089
4391
  const s = this.session;
4090
4392
  if (!s || ev.pointerId !== s.pointerId) return;
@@ -4110,6 +4412,12 @@ var Calendar = class {
4110
4412
  this.track(s, ev);
4111
4413
  };
4112
4414
  onPointerUp = (ev) => {
4415
+ if (ev.pointerId === this.cancelledPointerId) {
4416
+ this.cancelledPointerId = null;
4417
+ this.suppressNextClick();
4418
+ this.listenDoc(false);
4419
+ return;
4420
+ }
4113
4421
  const s = this.session;
4114
4422
  if (!s || ev.pointerId !== s.pointerId) return;
4115
4423
  if (!s.active) {
@@ -4122,28 +4430,49 @@ var Calendar = class {
4122
4430
  return;
4123
4431
  }
4124
4432
  this.suppressNextClick();
4433
+ this.track(s, ev);
4125
4434
  this.endSession(ev, false);
4126
4435
  };
4127
4436
  onPointerCancel = (ev) => {
4437
+ if (ev.pointerId === this.cancelledPointerId) {
4438
+ this.cancelledPointerId = null;
4439
+ this.listenDoc(false);
4440
+ }
4128
4441
  if (this.session && ev.pointerId === this.session.pointerId) this.endSession(null, true);
4129
4442
  };
4130
4443
  onDocKeyDown = (ev) => {
4131
4444
  if (ev.key === "Escape" && this.session) {
4132
4445
  ev.preventDefault();
4133
4446
  ev.stopPropagation();
4447
+ const pointerId = this.session.active ? this.session.pointerId : null;
4134
4448
  this.endSession(null, true);
4449
+ if (pointerId !== null) {
4450
+ this.cancelledPointerId = pointerId;
4451
+ this.listenDoc(true);
4452
+ }
4135
4453
  }
4136
4454
  };
4137
4455
  /** Proposal for the current pointer position. */
4138
4456
  track(s, ev) {
4457
+ if (this.listDragPreview) {
4458
+ const preview = this.listDragPreview;
4459
+ const bounds = this.root.getBoundingClientRect();
4460
+ preview.element.style.left = `${Math.max(8, Math.min(this.win.innerWidth - preview.width - 8, ev.clientX - preview.offsetX)) - bounds.left}px`;
4461
+ preview.element.style.top = `${Math.max(8, Math.min(this.win.innerHeight - preview.height - 8, ev.clientY - preview.offsetY)) - bounds.top}px`;
4462
+ }
4139
4463
  const hit = this.hitAt(ev.clientX, ev.clientY);
4140
4464
  if (s.mode === "external") {
4141
4465
  this.trackExternal(s, hit);
4142
4466
  return;
4143
4467
  }
4144
4468
  if (!s.grab) s.grab = hit;
4145
- if (!hit || !s.grab) return;
4469
+ if (!hit || !s.grab) {
4470
+ this.invalidateProposal(s);
4471
+ return;
4472
+ }
4146
4473
  if (s.mode === "select") {
4474
+ s.denied = false;
4475
+ this.root.classList.remove("tnc-drop-denied");
4147
4476
  const sel = this.selectionFor(s.grab, hit);
4148
4477
  if (!sel) return;
4149
4478
  this.selection = sel;
@@ -4152,25 +4481,40 @@ var Calendar = class {
4152
4481
  }
4153
4482
  const origin = s.origin;
4154
4483
  const p = s.mode === "move" ? this.moveProposal(origin, s.grab, hit) : this.resizeProposal(origin, hit, s.mode === "resize-start");
4155
- if (!p) return;
4484
+ if (!p) {
4485
+ this.invalidateProposal(s);
4486
+ return;
4487
+ }
4156
4488
  const allowed = this.opts.canDrop?.({ item: origin.item, start: new Date(p.startMs), end: new Date(p.endMs), allDay: p.allDay, rowId: p.rowId }) ?? true;
4157
4489
  s.denied = !allowed;
4158
4490
  this.root.classList.toggle("tnc-drop-denied", !allowed);
4159
- if (!allowed || sameProposal(p, s.proposal)) return;
4491
+ if (!allowed) {
4492
+ this.invalidateProposal(s);
4493
+ return;
4494
+ }
4495
+ if (sameProposal(p, s.proposal)) return;
4160
4496
  s.proposal = p;
4161
4497
  this.dragRow = p.rowId !== void 0 ? { id: origin.id, rowId: p.rowId } : null;
4162
- this.applyChange(this.store.upsert([this.withProposal(origin, p)]));
4498
+ if (this.opts.view !== "list") this.applyChange(this.store.upsert([this.withProposal(origin, p)]));
4499
+ }
4500
+ invalidateProposal(s) {
4501
+ const hadProposal = s.proposal !== null;
4502
+ s.proposal = null;
4503
+ s.hit = null;
4504
+ s.denied = true;
4505
+ this.dragRow = null;
4506
+ this.root.classList.add("tnc-drop-denied");
4507
+ this.strip?.markTarget(null);
4508
+ this.unselectSilently();
4509
+ if (s.mode === "external") this.applyChange(this.store.remove([EXTERNAL_ID]));
4510
+ else if (hadProposal && s.origin) this.applyChange(this.store.upsert([s.origin.item]));
4163
4511
  }
4164
4512
  trackExternal(s, raw) {
4165
4513
  const src = s.source;
4166
4514
  const hit = raw?.kind === "timeline" && raw.area !== "track" && raw.area !== "header" ? null : raw;
4167
4515
  s.hit = hit;
4168
4516
  if (!hit) {
4169
- if (s.proposal) {
4170
- s.proposal = null;
4171
- this.applyChange(this.store.remove([EXTERNAL_ID]));
4172
- }
4173
- this.unselectSilently();
4517
+ this.invalidateProposal(s);
4174
4518
  return;
4175
4519
  }
4176
4520
  const dur = src.durationMinutes ?? this.opts.defaultTimedMinutes;
@@ -4189,6 +4533,10 @@ var Calendar = class {
4189
4533
  const allowed = this.opts.canDrop?.({ item: this.externalItem(src, p), start: new Date(p.startMs), end: new Date(p.endMs), allDay: p.allDay, rowId: p.rowId }) ?? true;
4190
4534
  s.denied = !allowed;
4191
4535
  this.root.classList.toggle("tnc-drop-denied", !allowed);
4536
+ if (!allowed) {
4537
+ this.invalidateProposal(s);
4538
+ return;
4539
+ }
4192
4540
  if (sameProposal(p, s.proposal)) return;
4193
4541
  s.proposal = p;
4194
4542
  if (hit.kind === "timeline" && p.rowId === void 0) {
@@ -4381,7 +4729,19 @@ var Calendar = class {
4381
4729
  endSession(ev, cancel) {
4382
4730
  const s = this.session;
4383
4731
  if (!s) return;
4732
+ if (this.dragScrollFrame !== null) this.win.cancelAnimationFrame(this.dragScrollFrame);
4733
+ this.dragScrollFrame = null;
4734
+ if (cancel || s.denied) for (const [scroller, position] of this.dragScrollOrigins) {
4735
+ scroller.scrollTop = position.top;
4736
+ scroller.scrollLeft = position.left;
4737
+ }
4738
+ this.dragScrollOrigins.clear();
4739
+ this.dragScrollEdges.clear();
4384
4740
  this.session = null;
4741
+ this.listDragPreview?.element.remove();
4742
+ this.listDragPreview = null;
4743
+ for (const source of this.dragSources) source.remove();
4744
+ this.dragSources = [];
4385
4745
  if (s.timer !== null) this.win.clearTimeout(s.timer);
4386
4746
  this.listenDoc(false);
4387
4747
  this.root.classList.remove(
@@ -4395,10 +4755,15 @@ var Calendar = class {
4395
4755
  );
4396
4756
  const draggedId = this.draggingId;
4397
4757
  this.draggingId = null;
4758
+ this.dragWidth = null;
4398
4759
  this.dragRow = null;
4399
- 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
+ }
4400
4764
  this.strip?.markTarget(null);
4401
4765
  if (!s.active) return;
4766
+ if (this.current instanceof DayGridView) this.current.render(null);
4402
4767
  if (s.mode === "select") {
4403
4768
  if (cancel) this.unselectSilently();
4404
4769
  else if (this.selection) this.fireSelect(this.selection, ev);
@@ -4450,10 +4815,16 @@ var Calendar = class {
4450
4815
  revert();
4451
4816
  return;
4452
4817
  }
4818
+ if (this.opts.view === "list") this.applyChange(this.store.upsert([this.withProposal(origin, p)]));
4453
4819
  this.flush();
4454
4820
  this.commit(s.mode === "move" ? "drop" : "resize", origin, p, ev, revert);
4455
4821
  }
4456
4822
  commit(kind, origin, p, ev, revert) {
4823
+ let reverted = false;
4824
+ const mark = (state) => {
4825
+ this.itemWriteStates.set(origin.id, state);
4826
+ for (const element of this.elementsFor(origin.id)) this.applyItemWriteState(element, origin.id);
4827
+ };
4457
4828
  const arg = {
4458
4829
  item: origin.item,
4459
4830
  start: new Date(p.startMs),
@@ -4469,19 +4840,32 @@ var Calendar = class {
4469
4840
  jsEvent: ev,
4470
4841
  source: ev && "key" in ev ? "keyboard" : "pointer",
4471
4842
  view: this.view,
4472
- revert
4843
+ revert: () => {
4844
+ reverted = true;
4845
+ revert();
4846
+ mark("failed");
4847
+ }
4473
4848
  };
4474
4849
  const handler = kind === "drop" ? this.opts.onItemDrop : this.opts.onItemResize;
4475
4850
  try {
4851
+ mark("pending");
4476
4852
  const res = handler?.(arg);
4477
4853
  if (res && typeof res.then === "function") {
4478
- Promise.resolve(res).catch(() => revert());
4479
- }
4854
+ void Promise.resolve(res).then(() => {
4855
+ if (!reverted) mark("changed");
4856
+ }, () => arg.revert());
4857
+ } else if (!reverted) mark("changed");
4480
4858
  } catch (err) {
4481
- revert();
4859
+ arg.revert();
4482
4860
  throw err;
4483
4861
  }
4484
4862
  }
4863
+ applyItemWriteState(element, id) {
4864
+ const state = this.itemWriteStates.get(id);
4865
+ for (const name of ["pending", "failed", "changed"]) element.classList.toggle(`is-write-${name}`, state === name);
4866
+ if (state === "pending") element.setAttribute("aria-busy", "true");
4867
+ else element.removeAttribute("aria-busy");
4868
+ }
4485
4869
  /* --------------------------------------------------------- click / keys */
4486
4870
  /** Runs in the capture phase so the click ending a drag never reaches content inside the calendar, React portals included. */
4487
4871
  onClickCapture = (ev) => {
@@ -4491,6 +4875,12 @@ var Calendar = class {
4491
4875
  ev.preventDefault();
4492
4876
  };
4493
4877
  onClick = (ev) => {
4878
+ const compactDay = ev.target.closest?.(".tnc-dg--compact .tnc-dg-day");
4879
+ if (compactDay && this.root.contains(compactDay) && !ev.target.closest(".tnc-navlink")) {
4880
+ const day = parseDay(compactDay.dataset.date ?? "");
4881
+ if (day !== null) this.openMore(day, compactDay, ev);
4882
+ return;
4883
+ }
4494
4884
  const yearDay = ev.target.closest?.(".tnc-ym-day");
4495
4885
  if (yearDay && this.root.contains(yearDay)) {
4496
4886
  const day = parseDay(yearDay.dataset.date ?? "");
@@ -4579,7 +4969,8 @@ var Calendar = class {
4579
4969
  const day = parseDay(target.dataset.date ?? "");
4580
4970
  if (day !== null) {
4581
4971
  ev.preventDefault();
4582
- this.clickSlot({ kind: "day", day }, ev, true);
4972
+ if (target.closest(".tnc-dg--compact")) this.openMore(day, target, ev);
4973
+ else this.clickSlot({ kind: "day", day }, ev, true);
4583
4974
  }
4584
4975
  return;
4585
4976
  }
@@ -4667,9 +5058,13 @@ var Calendar = class {
4667
5058
  const dt = ev.dataTransfer;
4668
5059
  if (!dt || !this.opts.acceptNativeDrop?.(dt) || !this.current) return;
4669
5060
  ev.preventDefault();
5061
+ const hit = this.hitAt(ev.clientX, ev.clientY);
5062
+ if (!hit || hit.kind === "timeline" && hit.area !== "track" && hit.area !== "header") {
5063
+ dt.dropEffect = "none";
5064
+ this.unselectSilently();
5065
+ return;
5066
+ }
4670
5067
  dt.dropEffect = "copy";
4671
- const hit = this.current.hit(ev.clientX, ev.clientY);
4672
- if (!hit || hit.kind === "timeline" && hit.area !== "track" && hit.area !== "header") return;
4673
5068
  const sel = this.selectionFor(hit, hit);
4674
5069
  if (!sel) return;
4675
5070
  this.selection = sel;
@@ -4683,7 +5078,7 @@ var Calendar = class {
4683
5078
  const dt = ev.dataTransfer;
4684
5079
  if (!dt || !this.opts.acceptNativeDrop?.(dt) || !this.current) return;
4685
5080
  ev.preventDefault();
4686
- const hit = this.current.hit(ev.clientX, ev.clientY);
5081
+ const hit = this.hitAt(ev.clientX, ev.clientY);
4687
5082
  this.unselectSilently();
4688
5083
  if (!hit || hit.kind === "timeline" && hit.area !== "track" && hit.area !== "header") return;
4689
5084
  const sel = this.selectionFor(hit, hit);
@@ -4714,7 +5109,7 @@ function scrollerOf(el, axis) {
4714
5109
  return doc.scrollingElement;
4715
5110
  }
4716
5111
  function rangeMoved(prev, next) {
4717
- return !prev || prev.type !== next.type || prev.range.startDay !== next.range.startDay || prev.range.endDay !== next.range.endDay || prev.dataRange.endDay !== next.dataRange.endDay;
5112
+ return !prev || prev.type !== next.type || prev.range.startDay !== next.range.startDay || prev.range.endDay !== next.range.endDay || prev.dataRange.startDay !== next.dataRange.startDay || prev.dataRange.endDay !== next.dataRange.endDay;
4718
5113
  }
4719
5114
 
4720
5115
  // src/react/index.tsx