@tasknotes/calendar 0.1.6 → 0.1.8

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.
@@ -696,6 +696,7 @@ var DEFAULT_TEXT = {
696
696
  allDay: "All day",
697
697
  noItems: "Nothing scheduled",
698
698
  more: (n) => `+${n} more`,
699
+ appointments: (n) => `${n} appointments`,
699
700
  itemCount: (n) => n === 1 ? "1 item" : `${n} items`,
700
701
  weekNumber: (n) => `W${n}`,
701
702
  views: { week: "Week", day: "Day", days: "Days", month: "Month", list: "List", year: "Year", timeline: "Timeline" },
@@ -765,6 +766,7 @@ var LAYOUT_KEYS = /* @__PURE__ */ new Set([
765
766
  "dateStrip",
766
767
  "listLeadTitle",
767
768
  "listAddText",
769
+ "listEmptyContent",
768
770
  "monthDisplay",
769
771
  "timeZone",
770
772
  "locale",
@@ -980,8 +982,9 @@ var DayGridView = class {
980
982
  constructor(ctx) {
981
983
  this.ctx = ctx;
982
984
  const { doc, opts, days } = ctx;
983
- this.dotsMode = opts.monthDisplay === "dots";
985
+ this.dotsMode = opts.monthDisplay === "dots" || opts.monthDisplay === "chips";
984
986
  this.el = h(doc, "div", this.dotsMode ? "tnc-dg tnc-dg--dots" : "tnc-dg");
987
+ this.el.classList.toggle("tnc-dg--chips", opts.monthDisplay === "chips");
985
988
  this.scroller = h(doc, "div", "tnc-dg-scroll");
986
989
  this.grid = h(doc, "div", "tnc-dg-grid", { role: "grid", "aria-label": ctx.view.title });
987
990
  const perWeek = this.daysPerWeek();
@@ -1011,14 +1014,14 @@ var DayGridView = class {
1011
1014
  const cell = h(doc, "div", cls.join(" "), { role: "gridcell", "aria-label": ctx.fmt.longDate(d), "data-date": arg.dateStr });
1012
1015
  if (this.dotsMode) cell.tabIndex = 0;
1013
1016
  const num = ctx.fmt.dayNumber(d);
1014
- const label = ymdOfDay(d).day === 1 ? `${ctx.fmt.monthShort(d)} ${num}` : num;
1017
+ const label = !this.dotsMode && ymdOfDay(d).day === 1 ? `${ctx.fmt.monthShort(d)} ${num}` : num;
1015
1018
  cell.append(
1016
1019
  h(
1017
1020
  doc,
1018
1021
  "div",
1019
1022
  "tnc-dg-top",
1020
1023
  null,
1021
- opts.navLinks ? ctx.navLink(d, label, "tnc-dg-num") : h(doc, "span", "tnc-dg-num", null, label)
1024
+ opts.navLinks && !this.dotsMode ? ctx.navLink(d, label, "tnc-dg-num") : h(doc, "span", "tnc-dg-num", null, label)
1022
1025
  )
1023
1026
  );
1024
1027
  dates.append(cell);
@@ -1032,20 +1035,20 @@ var DayGridView = class {
1032
1035
  layer.append(content);
1033
1036
  row.append(dates, layer);
1034
1037
  this.grid.append(row);
1035
- const dots = this.dotsMode ? cells.map((cell) => {
1038
+ const dots = cells.map((cell) => {
1036
1039
  const box = h(doc, "div", "tnc-dg-dots", { "aria-hidden": "true" });
1037
1040
  if (opts.dayMaxEvents === false) box.classList.add("is-unlimited");
1038
1041
  const count = h(doc, "span", "tnc-dg-dots-more");
1039
1042
  cell.append(box);
1040
1043
  return { box, pool: new ItemPool(ctx, box), count };
1041
- }) : null;
1044
+ });
1042
1045
  this.weeks.push({ days: wdays, row, cells, layer, sel, more, pool: new ItemPool(ctx, content), dots });
1043
1046
  }
1044
1047
  this.scroller.append(this.grid);
1045
1048
  this.el.append(this.scroller);
1046
1049
  this.el.classList.toggle("tnc-dg--auto", opts.height === "auto");
1047
1050
  const RO = ctx.win.ResizeObserver;
1048
- if (RO && (opts.dayMaxEvents === true || this.dotsMode && opts.dayMaxEvents !== false)) {
1051
+ if (RO) {
1049
1052
  this.resizeObs = new RO(() => {
1050
1053
  if (this.sizeKey() !== this.lastHeights) this.render(null);
1051
1054
  });
@@ -1076,14 +1079,35 @@ var DayGridView = class {
1076
1079
  }
1077
1080
  render(_change) {
1078
1081
  const { ctx } = this;
1082
+ this.dotsMode = ctx.opts.monthDisplay !== "full" || this.grid.clientWidth / this.daysPerWeek() < 72;
1083
+ this.el.classList.toggle("tnc-dg--dots", this.dotsMode);
1084
+ this.el.classList.toggle("tnc-dg--compact", this.dotsMode && ctx.opts.monthDisplay === "full");
1085
+ this.el.classList.toggle("tnc-dg--chips", ctx.opts.monthDisplay === "chips" && this.weeks[0].dots[0].box.clientHeight >= 44);
1079
1086
  const cs = ctx.win.getComputedStyle(this.el);
1080
1087
  this.barPx = px(cs, "--tnc-bar-h", 20);
1081
1088
  this.topPx = this.weeks[0]?.cells[0]?.querySelector(".tnc-dg-top")?.offsetHeight || px(cs, "--tnc-dg-top-h", 24);
1082
1089
  this.morePx = this.measureMore(cs);
1083
1090
  this.lastHeights = this.sizeKey();
1084
1091
  for (const w of this.weeks) {
1085
- if (w.dots) this.renderDots(w);
1086
- else this.renderWeek(w);
1092
+ for (const cell of w.cells) {
1093
+ if (this.dotsMode) cell.tabIndex = 0;
1094
+ else cell.removeAttribute("tabindex");
1095
+ if (this.dotsMode && ctx.opts.monthDisplay === "full") cell.setAttribute("aria-haspopup", "dialog");
1096
+ else cell.removeAttribute("aria-haspopup");
1097
+ }
1098
+ for (const cell of w.dots) cell.box.hidden = !this.dotsMode;
1099
+ if (this.dotsMode) {
1100
+ w.pool.begin();
1101
+ w.pool.end();
1102
+ w.more.replaceChildren();
1103
+ this.renderDots(w);
1104
+ } else {
1105
+ for (const cell of w.dots) {
1106
+ cell.pool.begin();
1107
+ cell.pool.end();
1108
+ }
1109
+ this.renderWeek(w);
1110
+ }
1087
1111
  }
1088
1112
  }
1089
1113
  /** Height of a "+N more" link as the host styles it (`--tnc-more-h`, or a taller min-height). */
@@ -1169,19 +1193,28 @@ var DayGridView = class {
1169
1193
  });
1170
1194
  }
1171
1195
  w.pool.end();
1172
- w.more.replaceChildren();
1196
+ const existing = new Map(Array.from(w.more.children, (element) => [element.getAttribute("data-day"), element]));
1173
1197
  hidden.forEach((items, col) => {
1174
1198
  if (!items.length) return;
1175
1199
  const day = w.days[col];
1176
- const link = h(ctx.doc, "button", "tnc-more", { type: "button", "aria-haspopup": "dialog" }, ctx.text.more(items.length));
1200
+ const key = String(day);
1201
+ let link = existing.get(key);
1202
+ if (!link) {
1203
+ link = h(ctx.doc, "button", "tnc-more", { type: "button", "aria-haspopup": "dialog", "data-day": key });
1204
+ const anchor = link;
1205
+ link.addEventListener("click", (ev) => ctx.openMore(day, anchor, ev));
1206
+ w.more.append(link);
1207
+ }
1208
+ existing.delete(key);
1209
+ link.setAttribute("aria-label", ctx.text.more(items.length));
1210
+ link.textContent = w.row.clientWidth / n < 72 ? `+${new Intl.NumberFormat(ctx.opts.locale).format(items.length)}` : ctx.text.more(items.length);
1177
1211
  setStyle(link, {
1178
1212
  left: `calc(${pct(col / n)} + 2px)`,
1179
1213
  width: `calc(${pct(1 / n)} - 4px)`,
1180
1214
  top: `calc(var(--tnc-dg-top-h) + ${visible(col)} * (var(--tnc-bar-h) + ${BAR_GAP}px))`
1181
1215
  });
1182
- link.addEventListener("click", (ev) => ctx.openMore(day, link, ev));
1183
- w.more.append(link);
1184
1216
  });
1217
+ for (const link of existing.values()) link.remove();
1185
1218
  const step = this.barPx + BAR_GAP;
1186
1219
  let need = maxLevel >= 0 ? (maxLevel + 1) * step - BAR_GAP : 0;
1187
1220
  hidden.forEach((items, col) => {
@@ -1204,10 +1237,11 @@ var DayGridView = class {
1204
1237
  const { dayMaxEvents } = ctx.opts;
1205
1238
  const first = w.dots[0].box;
1206
1239
  const cs = ctx.win.getComputedStyle(first);
1207
- const size = px(cs, "--tnc-dot-size", 8);
1240
+ const chips = this.el.classList.contains("tnc-dg--chips");
1241
+ const size = chips ? px(cs, "--tnc-chip-h", 14) : px(cs, "--tnc-dot-size", 8);
1208
1242
  const gap = px(cs, "--tnc-dot-gap", 3);
1209
1243
  const step = size + gap;
1210
- const perRow = Math.max(1, Math.floor((first.clientWidth + gap) / step));
1244
+ const perRow = chips ? 1 : Math.max(1, Math.floor((first.clientWidth + gap) / step));
1211
1245
  const rows = Math.max(1, Math.floor((first.clientHeight + gap) / step));
1212
1246
  const fits = dayMaxEvents === false ? Infinity : perRow * rows;
1213
1247
  const cap = typeof dayMaxEvents === "number" && dayMaxEvents > 0 ? Math.min(dayMaxEvents, fits) : fits;
@@ -1216,7 +1250,7 @@ var DayGridView = class {
1216
1250
  w.days.forEach((day, i) => {
1217
1251
  const cell = w.dots[i];
1218
1252
  const items = this.itemsOn(day);
1219
- const shown = items.length <= cap ? items.length : cap >= fits ? Math.max(0, cap - 2) : cap;
1253
+ const shown = items.length <= cap ? items.length : cap >= fits ? Math.max(1, cap - (chips ? 1 : 2)) : Math.max(1, cap);
1220
1254
  cell.pool.begin();
1221
1255
  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 }));
1222
1256
  cell.pool.end();
@@ -1333,6 +1367,7 @@ var ListView = class {
1333
1367
  const leadItems = [];
1334
1368
  const inLead = /* @__PURE__ */ new Set();
1335
1369
  const keep = /* @__PURE__ */ new Set();
1370
+ let itemCount = 0;
1336
1371
  let prev = null;
1337
1372
  for (const day of days) {
1338
1373
  let items = this.itemsOn(day);
@@ -1346,7 +1381,7 @@ var ListView = class {
1346
1381
  return false;
1347
1382
  });
1348
1383
  }
1349
- if (!items.length && !agenda) continue;
1384
+ if (!items.length && !agenda && opts.listDayCount !== 1) continue;
1350
1385
  keep.add(day);
1351
1386
  let section = this.sections.get(day);
1352
1387
  let pool = this.pools.get(day);
@@ -1365,6 +1400,7 @@ var ListView = class {
1365
1400
  this.pools.set(day, pool);
1366
1401
  }
1367
1402
  section.classList.toggle("is-empty", !items.length);
1403
+ itemCount += items.length;
1368
1404
  this.fill(pool, items, () => day);
1369
1405
  if (prev ? prev.nextSibling !== section : this.body.firstChild !== section) {
1370
1406
  if (prev) prev.after(section);
@@ -1380,11 +1416,14 @@ var ListView = class {
1380
1416
  this.sections.delete(day);
1381
1417
  }
1382
1418
  this.renderLead(leadItems);
1383
- let empty = this.body.querySelector(":scope > .tnc-list-empty");
1384
- if (!keep.size && !leadItems.length) {
1419
+ let empty = this.body.querySelector(".tnc-list-empty");
1420
+ if (!itemCount && !leadItems.length && (!agenda || opts.listEmptyContent)) {
1385
1421
  if (!empty) {
1386
1422
  empty = h(doc, "div", "tnc-list-empty", { role: "listitem" }, ctx.text.noItems);
1387
- this.body.append(empty);
1423
+ const content = ctx.opts.listEmptyContent?.();
1424
+ if (content) empty.replaceChildren(content);
1425
+ if (this.sections.size === 1) this.sections.values().next().value.append(empty);
1426
+ else this.body.prepend(empty);
1388
1427
  }
1389
1428
  } else empty?.remove();
1390
1429
  this.ctx.strip?.draw();
@@ -1442,7 +1481,7 @@ var ListView = class {
1442
1481
  const section = day === this.ctx.days[0] && this.lead ? this.lead.section : this.sections.get(day);
1443
1482
  if (!section) return;
1444
1483
  this.ctx.strip?.measure();
1445
- section.scrollIntoView({ block: "start" });
1484
+ this.scroller.scrollTop += section.getBoundingClientRect().top - this.scroller.getBoundingClientRect().top;
1446
1485
  this.ctx.strip?.setFocus(day);
1447
1486
  this.pinned = true;
1448
1487
  }
@@ -1531,8 +1570,7 @@ var DateStrip = class {
1531
1570
  head.append(this.title, this.todayBtn, prev, next, this.slot);
1532
1571
  this.dows = h(doc, "div", "tnc-strip-dows", { "aria-hidden": "true" });
1533
1572
  this.grid = h(doc, "div", "tnc-strip-grid", { role: "group" });
1534
- const handle = h(doc, "button", "tnc-strip-handle", { type: "button", tabindex: "-1", "aria-hidden": "true" });
1535
- handle.addEventListener("click", () => this.setExpanded(!this.expanded));
1573
+ const handle = h(doc, "div", "tnc-strip-handle", { "aria-hidden": "true" });
1536
1574
  this.el.append(head, this.dows, this.grid, handle);
1537
1575
  this.grid.addEventListener("click", this.onClick);
1538
1576
  this.grid.addEventListener("keydown", this.onKeyDown);
@@ -1732,6 +1770,8 @@ var TimeGridView = class {
1732
1770
  this.el = h(doc, "div", "tnc-tg", { role: "table", "aria-label": ctx.view.title });
1733
1771
  this.el.classList.toggle("tnc-tg--no-grid", !showGrid);
1734
1772
  this.scroller = h(doc, "div", "tnc-tg-scroll");
1773
+ this.dayNavigation = h(doc, "div", "tnc-tg-day-navigation", { role: "group", "aria-label": ctx.view.title });
1774
+ this.scroller.addEventListener("scroll", () => this.updateDayNavigation());
1735
1775
  this.top = h(doc, "div", "tnc-tg-top");
1736
1776
  this.headRow = h(doc, "div", "tnc-tg-head tnc-tg-cols", { role: "row" });
1737
1777
  this.top.append(this.headRow);
@@ -1759,7 +1799,7 @@ var TimeGridView = class {
1759
1799
  } else {
1760
1800
  this.body = this.gutter = null;
1761
1801
  }
1762
- this.el.append(this.scroller);
1802
+ this.el.append(this.dayNavigation, this.scroller);
1763
1803
  this.build();
1764
1804
  if (showGrid && opts.height === "fill") {
1765
1805
  const RO = ctx.win.ResizeObserver;
@@ -1772,6 +1812,7 @@ var TimeGridView = class {
1772
1812
  ctx;
1773
1813
  el;
1774
1814
  scroller;
1815
+ dayNavigation;
1775
1816
  top;
1776
1817
  headRow;
1777
1818
  allDayRow;
@@ -1827,6 +1868,9 @@ var TimeGridView = class {
1827
1868
  head.append(cap);
1828
1869
  }
1829
1870
  this.headRow.append(head);
1871
+ const dayButton = h(doc, "button", "tnc-btn", { type: "button", "aria-label": ctx.fmt.longDate(day) }, `${ctx.fmt.weekdayShort(day)} ${ctx.fmt.dayNumber(day)}`);
1872
+ dayButton.addEventListener("click", () => this.revealDay(day));
1873
+ this.dayNavigation.append(dayButton);
1830
1874
  opts.dayHeaderDidMount?.({ ...arg, el: head });
1831
1875
  const col = h(doc, "div", ["tnc-col", ...ctx.dayClasses(day)].join(" "), { role: "cell", "aria-label": ctx.fmt.longDate(day), "data-date": arg.dateStr });
1832
1876
  const bgLayer = h(doc, "div", "tnc-col-bg");
@@ -1843,6 +1887,7 @@ var TimeGridView = class {
1843
1887
  this.body?.append(col);
1844
1888
  return {
1845
1889
  day,
1890
+ collisionColumns: 1,
1846
1891
  head,
1847
1892
  col,
1848
1893
  plan: new ItemPool(ctx, planLayer),
@@ -1899,12 +1944,17 @@ var TimeGridView = class {
1899
1944
  /* --------------------------------------------------------------- render */
1900
1945
  render(change) {
1901
1946
  this.renderAllDay();
1902
- if (!this.body) return;
1947
+ if (!this.body) {
1948
+ this.updateDayNavigation();
1949
+ return;
1950
+ }
1903
1951
  const dirty = change ? change.days : null;
1904
1952
  for (const c of this.cols) {
1905
1953
  if (dirty && !dirty.has(c.day) && !(this.ctx.slot.max > MINUTES_PER_DAY && dirty.has(c.day + 1))) continue;
1906
1954
  this.renderColumn(c);
1907
1955
  }
1956
+ this.el.style.setProperty("--tnc-collision-columns", String(Math.max(1, ...this.cols.map((column) => column.collisionColumns))));
1957
+ this.updateDayNavigation();
1908
1958
  this.renderCapacity();
1909
1959
  this.tick();
1910
1960
  }
@@ -2022,6 +2072,11 @@ var TimeGridView = class {
2022
2072
  }
2023
2073
  const minMinutes = opts.eventMinHeight / this.ppm;
2024
2074
  const layout = layoutTimedDay(items, c.day, zone, minMinutes, ctx.order, [slot.min, slot.max]);
2075
+ c.collisionColumns = Math.max(
2076
+ 1,
2077
+ ...layout.plan.map((placement) => opts.eventMaxStack > 0 ? Math.min(placement.cols, opts.eventMaxStack) : placement.cols),
2078
+ ...layout.actual.filter((placement) => !placement.split).map((placement) => placement.cols)
2079
+ );
2025
2080
  const total = this.minutes;
2026
2081
  const top = (m) => pct((m - slot.min) / total);
2027
2082
  const height = (s) => pct((s.endMin - s.startMin) / total);
@@ -2033,14 +2088,22 @@ var TimeGridView = class {
2033
2088
  c.bg.end();
2034
2089
  const hasActual = layout.actual.length > 0;
2035
2090
  c.col.classList.toggle("has-actual", hasActual);
2091
+ for (const link of c.more.children) link.setAttribute("data-stale", "true");
2036
2092
  this.placeLane(c.plan, layout.plan, c, top, height, "plan");
2037
2093
  this.placeLane(c.actual, layout.actual, c, top, height, "actual");
2094
+ for (const link of [...c.more.children]) if (link.hasAttribute("data-stale")) link.remove();
2038
2095
  }
2039
2096
  placeLane(pool, placements, c, top, height, lane) {
2040
2097
  const { ctx } = this;
2041
2098
  const { opts } = ctx;
2042
- const withMore = lane === "plan";
2043
- const maxStack = opts.eventMaxStack;
2099
+ const readableWidth = Number.parseFloat(ctx.win.getComputedStyle(this.el).getPropertyValue("--tnc-event-min-width")) || 128;
2100
+ const laneWidth = c.col.clientWidth * (lane === "actual" && placements.some((placement) => placement.split) ? 0.22 : 1);
2101
+ const collapse = placements.some((placement) => {
2102
+ const columns = opts.eventMaxStack > 0 ? Math.min(placement.cols, opts.eventMaxStack) : placement.cols;
2103
+ return columns > 1 && columns * readableWidth > laneWidth;
2104
+ });
2105
+ const withMore = lane === "plan" || collapse;
2106
+ const maxStack = collapse ? 1 : opts.eventMaxStack;
2044
2107
  const hiddenByCluster = /* @__PURE__ */ new Map();
2045
2108
  pool.begin();
2046
2109
  for (const p of placements) {
@@ -2066,21 +2129,46 @@ var TimeGridView = class {
2066
2129
  top: top(p.startMin),
2067
2130
  height: `max(${height(p)}, ${opts.eventMinHeight}px)`,
2068
2131
  ...inLane(lane, p.split, left, width),
2132
+ "--tnc-event-readable-width": pct(span / cols / width),
2069
2133
  "z-index": String(p.col + 1)
2070
2134
  });
2071
2135
  }
2072
2136
  pool.end();
2073
2137
  if (!withMore) return;
2074
- c.more.replaceChildren();
2075
- for (const rec of hiddenByCluster.values()) {
2076
- const link = h(ctx.doc, "button", "tnc-more tnc-more--stack", { type: "button", "aria-haspopup": "dialog" }, `+${rec.count}`);
2138
+ for (const [cluster, rec] of hiddenByCluster) {
2139
+ const key = `${lane}-${cluster}`;
2140
+ let link = c.more.querySelector(`[data-cluster="${key}"]`);
2141
+ if (!link) {
2142
+ link = h(ctx.doc, "button", "tnc-more tnc-more--stack", { type: "button", "aria-haspopup": "dialog", "data-cluster": key });
2143
+ const anchor = link;
2144
+ link.addEventListener("click", (ev) => ctx.openMore(c.day, anchor, ev, JSON.parse(anchor.dataset.itemIds ?? "[]")));
2145
+ c.more.append(link);
2146
+ }
2147
+ link.removeAttribute("data-stale");
2148
+ link.dataset.itemIds = JSON.stringify(placements.filter((placement) => placement.cluster === cluster).map((placement) => placement.item.id));
2149
+ link.classList.toggle("tnc-more--collision", collapse);
2150
+ link.textContent = `+${rec.count}`;
2151
+ if (collapse) {
2152
+ const items = placements.filter((placement) => placement.cluster === cluster).map((placement) => placement.item.item.title);
2153
+ const count = ctx.text.appointments(rec.count);
2154
+ const interval = `${ctx.fmt.time(rec.start)} \u2013 ${ctx.fmt.time(rec.end)}`;
2155
+ link.replaceChildren(
2156
+ h(ctx.doc, "span", "tnc-collision-count", null, String(rec.count)),
2157
+ h(ctx.doc, "span", "tnc-collision-range", null, interval),
2158
+ h(ctx.doc, "span", "tnc-collision-title", { "aria-hidden": "true" }, items.join(" \xB7 "))
2159
+ );
2160
+ link.setAttribute("aria-label", `${count}, ${interval}: ${items.join(", ")}`);
2161
+ link.title = link.getAttribute("aria-label");
2162
+ } else {
2163
+ link.removeAttribute("aria-label");
2164
+ link.removeAttribute("title");
2165
+ }
2077
2166
  const cols = maxStack;
2078
2167
  setStyle(link, {
2079
2168
  top: top(rec.start),
2169
+ height: collapse ? pct((rec.end - rec.start) / this.minutes) : "",
2080
2170
  ...inLane(lane, rec.split, (cols - 1) / cols, 1 / cols)
2081
2171
  });
2082
- link.addEventListener("click", (ev) => ctx.openMore(c.day, link, ev));
2083
- c.more.append(link);
2084
2172
  }
2085
2173
  }
2086
2174
  meta(s, bg) {
@@ -2145,12 +2233,10 @@ var TimeGridView = class {
2145
2233
  const r2 = cells[i2].getBoundingClientRect();
2146
2234
  if (x >= r2.left && x < r2.right) return i2;
2147
2235
  }
2148
- const firstR = cells[0]?.getBoundingClientRect();
2149
- const lastR = cells[cells.length - 1]?.getBoundingClientRect();
2150
- if (firstR && x < firstR.left) return 0;
2151
- if (lastR && x >= lastR.right) return cells.length - 1;
2152
2236
  return -1;
2153
2237
  };
2238
+ const bounds = this.scroller.getBoundingClientRect();
2239
+ if (x < bounds.left || x >= bounds.right || y < bounds.top || y >= bounds.bottom) return null;
2154
2240
  if (this.allDayRow) {
2155
2241
  const r2 = this.allDayRow.getBoundingClientRect();
2156
2242
  if (y >= r2.top && y < r2.bottom || !this.body && y >= r2.top) {
@@ -2191,6 +2277,26 @@ var TimeGridView = class {
2191
2277
  }
2192
2278
  resize() {
2193
2279
  this.fitRows();
2280
+ this.render(null);
2281
+ }
2282
+ revealDay(day) {
2283
+ const column = this.cols.find((candidate) => candidate.day === day);
2284
+ if (!column) return;
2285
+ const bounds = column.head.getBoundingClientRect();
2286
+ const viewport = this.scroller.getBoundingClientRect();
2287
+ this.scroller.scrollLeft += bounds.left - viewport.left - (viewport.width - bounds.width) / 2;
2288
+ this.updateDayNavigation();
2289
+ }
2290
+ updateDayNavigation() {
2291
+ this.dayNavigation.hidden = this.scroller.scrollWidth <= this.scroller.clientWidth + 1;
2292
+ const viewport = this.scroller.getBoundingClientRect();
2293
+ this.cols.forEach((column, index) => {
2294
+ const bounds = column.head.getBoundingClientRect();
2295
+ const visible = bounds.left >= viewport.left && bounds.right <= viewport.right;
2296
+ const button = this.dayNavigation.children[index];
2297
+ button?.classList.toggle("is-offscreen", !visible);
2298
+ button?.setAttribute("aria-pressed", String(visible));
2299
+ });
2194
2300
  }
2195
2301
  scrollToTime(minute) {
2196
2302
  if (!this.body) return;
@@ -2853,6 +2959,14 @@ ${this.ctx.text.more(items.length - 8)}` : ""}` : label;
2853
2959
  setScroll(s) {
2854
2960
  this.scroller.scrollTop = s.top;
2855
2961
  }
2962
+ revealDay(day) {
2963
+ const month = this.cells.find((cell) => cell.day === day)?.el.closest(".tnc-ym");
2964
+ if (!month) return;
2965
+ const bounds = month.getBoundingClientRect();
2966
+ const viewport = this.scroller.getBoundingClientRect();
2967
+ if (bounds.top < viewport.top) this.scroller.scrollTop += bounds.top - viewport.top;
2968
+ else if (bounds.bottom > viewport.bottom) this.scroller.scrollTop += bounds.bottom - viewport.bottom;
2969
+ }
2856
2970
  elementsFor(_id) {
2857
2971
  return [];
2858
2972
  }
@@ -2932,6 +3046,11 @@ var Calendar = class {
2932
3046
  conflicts = /* @__PURE__ */ new Map();
2933
3047
  slot = { min: 0, max: MINUTES_PER_DAY, duration: 30, snap: 30, scroll: 480 };
2934
3048
  draggingId = null;
3049
+ listDragPreview = null;
3050
+ dragSources = [];
3051
+ dragScrollFrame = null;
3052
+ dragScrollOrigins = /* @__PURE__ */ new Map();
3053
+ itemWriteStates = /* @__PURE__ */ new Map();
2935
3054
  dragRow = null;
2936
3055
  strip = null;
2937
3056
  stripSize = null;
@@ -2945,6 +3064,7 @@ var Calendar = class {
2945
3064
  session = null;
2946
3065
  selection = null;
2947
3066
  suppressClick = false;
3067
+ cancelledPointerId = null;
2948
3068
  selectedIds = /* @__PURE__ */ new Set();
2949
3069
  popover = null;
2950
3070
  timer = null;
@@ -2956,8 +3076,13 @@ var Calendar = class {
2956
3076
  this.resizeObs?.disconnect();
2957
3077
  const RO = this.win.ResizeObserver;
2958
3078
  if (RO) {
2959
- this.resizeObs = new RO(() => this.current?.resize?.());
3079
+ this.resizeObs = new RO(() => {
3080
+ this.syncToolbarViewDisplay();
3081
+ this.current?.resize?.();
3082
+ this.positionMore();
3083
+ });
2960
3084
  this.resizeObs.observe(this.root);
3085
+ this.resizeObs.observe(this.toolbarEl);
2961
3086
  }
2962
3087
  }
2963
3088
  onAdoptCheck = () => {
@@ -2976,9 +3101,11 @@ var Calendar = class {
2976
3101
  if (this.frame !== null) old.cancelAnimationFrame(this.frame);
2977
3102
  this.timer = this.frame = null;
2978
3103
  this.doc.removeEventListener("pointerdown", this.onDocPointerDown, true);
3104
+ this.listenMoreGeometry(false);
2979
3105
  this.doc = doc;
2980
3106
  this.win = doc.defaultView ?? old;
2981
3107
  this.doc.addEventListener("pointerdown", this.onDocPointerDown, true);
3108
+ this.listenMoreGeometry(true);
2982
3109
  this.observeResize();
2983
3110
  this.rebuild(false);
2984
3111
  this.scheduleTick();
@@ -3082,7 +3209,12 @@ var Calendar = class {
3082
3209
  gotoDate(date) {
3083
3210
  const day = this.toDay(date);
3084
3211
  if (day === null) return;
3212
+ const sameMonth = this.opts.view === "month" && startOfMonth(day) === startOfMonth(this.anchor);
3085
3213
  this.anchor = day;
3214
+ if (sameMonth) {
3215
+ this.view = { ...this.view, anchor: day };
3216
+ return;
3217
+ }
3086
3218
  this.rebuild(true);
3087
3219
  }
3088
3220
  prev() {
@@ -3096,6 +3228,7 @@ var Calendar = class {
3096
3228
  goToday() {
3097
3229
  this.anchor = this.todayDay();
3098
3230
  this.rebuild(true);
3231
+ if (this.current instanceof TimeGridView) this.current.revealDay(this.anchor);
3099
3232
  }
3100
3233
  /** The anchor date, as local midnight of that civil day in the calendar zone. */
3101
3234
  getDate() {
@@ -3114,6 +3247,7 @@ var Calendar = class {
3114
3247
  /** Re-measures (e.g. after the host was shown or resized by a parent). */
3115
3248
  updateSize() {
3116
3249
  this.current?.resize?.();
3250
+ this.positionMore();
3117
3251
  }
3118
3252
  getScroll() {
3119
3253
  return this.current?.getScroll() ?? { top: 0, left: 0 };
@@ -3145,7 +3279,10 @@ var Calendar = class {
3145
3279
  }
3146
3280
  const view = this.current;
3147
3281
  if (view instanceof TimelineView) view.scrollToInstant(ms, opts.align ?? "center", opts.smooth ?? false);
3148
- else if (view instanceof TimeGridView && p.kind !== "day") view.scrollToTime(wallTime(ms, this.zone).minute);
3282
+ else if (view instanceof TimeGridView) {
3283
+ view.revealDay(day);
3284
+ if (p.kind !== "day") view.scrollToTime(wallTime(ms, this.zone).minute);
3285
+ }
3149
3286
  }
3150
3287
  /** What is under a client point: a time slot, a day, or a timeline row and date. Null outside the view. */
3151
3288
  hitTest(clientX, clientY) {
@@ -3223,7 +3360,8 @@ var Calendar = class {
3223
3360
  out.push(...classList(this.opts.dayClassNames?.(this.dayArg(day))));
3224
3361
  return out;
3225
3362
  }
3226
- isEditable(n, what) {
3363
+ isEditable(n, what, presentation = false) {
3364
+ if (!presentation && this.itemWriteStates.get(n.id) === "pending") return false;
3227
3365
  if (n.id === EXTERNAL_ID || n.item.preview || this.opts.view === "year") return false;
3228
3366
  if (this.opts.view === "list" && (!this.opts.listAgenda || what === "duration")) return false;
3229
3367
  const it = n.item;
@@ -3263,13 +3401,14 @@ var Calendar = class {
3263
3401
  if (preview || n.id === EXTERNAL_ID) el.classList.add("is-mirror");
3264
3402
  if (this.conflicts.has(n.id)) el.classList.add("is-conflict");
3265
3403
  const indicator = variant === "indicator";
3266
- if (!preview && !indicator && (this.isEditable(n, "start") || this.isEditable(n, "duration"))) el.classList.add("is-draggable");
3404
+ if (!preview && !indicator && (this.isEditable(n, "start", true) || this.isEditable(n, "duration", true))) el.classList.add("is-draggable");
3267
3405
  if (this.draggingId === n.id) el.classList.add("is-dragging");
3268
3406
  if (this.selectedIds.has(n.id) && !preview) el.classList.add("is-selected");
3269
3407
  el.classList.add(...classList(it.className));
3270
3408
  const arg = this.itemArg(el, meta);
3271
3409
  el.classList.add(...classList(this.opts.itemClassNames?.(arg)));
3272
3410
  el.dataset.id = n.id;
3411
+ this.applyItemWriteState(el, n.id);
3273
3412
  if (it.kind) el.dataset.kind = it.kind;
3274
3413
  const color = it.color ?? it.borderColor;
3275
3414
  if (color) el.style.setProperty("--tnc-c", color);
@@ -3286,6 +3425,7 @@ var Calendar = class {
3286
3425
  const custom = variant === "list" ? this.opts.listItemContent?.(arg) : null;
3287
3426
  const content = indicator ? null : custom ?? this.opts.itemContent?.(arg);
3288
3427
  if (indicator) {
3428
+ if (this.opts.monthDisplay === "chips") el.append(h(this.doc, "span", "tnc-item-title", null, it.title));
3289
3429
  } else if (content != null) {
3290
3430
  const wrap = h(this.doc, "div", "tnc-item-main");
3291
3431
  wrap.append(content);
@@ -3298,6 +3438,7 @@ var Calendar = class {
3298
3438
  return el;
3299
3439
  }
3300
3440
  refreshItemEl(el, meta) {
3441
+ this.applyItemWriteState(el, meta.n.id);
3301
3442
  const time = el.querySelector(":scope .tnc-item-time");
3302
3443
  if (time) this.fillTime(time, meta.n, meta.variant, meta.day);
3303
3444
  el.classList.toggle("is-dragging", this.draggingId === meta.n.id);
@@ -3320,9 +3461,10 @@ var Calendar = class {
3320
3461
  if (meta) this.opts.itemWillUnmount?.(this.itemArg(el, meta));
3321
3462
  el.remove();
3322
3463
  }
3323
- openMore(day, anchor, jsEvent) {
3464
+ openMore(day, anchor, jsEvent, itemIds) {
3324
3465
  jsEvent.stopPropagation();
3325
- const items = this.store.inRange(day, day + 1).filter((n) => n.item.display !== "background" && (n.allDay || this.opts.view === "month" || !this.opts.timeGrid || this.opts.eventMaxStack > 0)).sort((a, b) => Number(b.allDay) - Number(a.allDay) || this.order(a, b));
3466
+ const group = itemIds ? new Set(itemIds) : null;
3467
+ const items = this.store.inRange(day, day + 1).filter((item) => !group || group.has(item.id)).filter((n) => n.item.display !== "background" && (n.allDay || anchor.classList.contains("tnc-more--stack") || this.opts.view === "month" || !this.opts.timeGrid || this.opts.eventMaxStack > 0)).sort((a, b) => Number(b.allDay) - Number(a.allDay) || this.order(a, b));
3326
3468
  const shown = new Set(
3327
3469
  [...this.viewHost.querySelectorAll(".tnc-item")].filter((el2) => this.meta.get(el2)?.day === day || (this.meta.get(el2)?.n.startDay ?? Infinity) <= day).map((el2) => el2.dataset.id ?? "")
3328
3470
  );
@@ -3351,15 +3493,7 @@ var Calendar = class {
3351
3493
  }
3352
3494
  pool.end();
3353
3495
  this.popover = { el, pool, anchor, day };
3354
- const rr = this.root.getBoundingClientRect();
3355
- const ar = anchor.getBoundingClientRect();
3356
- const w = Math.max(220, ar.width + 24);
3357
- let left = ar.left - rr.left - 12;
3358
- left = Math.max(4, Math.min(left, rr.width - w - 4));
3359
- let top = ar.top - rr.top - 40;
3360
- const height = el.offsetHeight;
3361
- top = Math.max(4, Math.min(top, rr.height - height - 4));
3362
- setStyle(el, { left, top, width: w });
3496
+ this.positionMore();
3363
3497
  el.focus();
3364
3498
  }
3365
3499
  /* =============================================================== render */
@@ -3483,8 +3617,11 @@ var Calendar = class {
3483
3617
  this.current.setScroll({ top: keep.top, left: 0 });
3484
3618
  this.current.placeInstant(keep.ms, keep.vx);
3485
3619
  } else if (scroll) this.current.setScroll(scroll);
3486
- else if (this.current instanceof TimeGridView) this.current.scrollToTime(this.slot.scroll);
3487
- else if (this.current instanceof TimelineView) this.current.scrollToDay(this.anchor);
3620
+ else if (this.current instanceof TimeGridView) {
3621
+ this.current.scrollToTime(this.slot.scroll);
3622
+ this.current.revealDay(this.anchor);
3623
+ } else if (this.current instanceof TimelineView) this.current.scrollToDay(this.anchor);
3624
+ else if (this.current instanceof YearView) this.current.revealDay(this.anchor);
3488
3625
  if (rangeChanged) this.opts.onRangeChange?.(this.view);
3489
3626
  }
3490
3627
  /** Sets the visible days and view info from the anchor. */
@@ -3510,9 +3647,11 @@ var Calendar = class {
3510
3647
  }
3511
3648
  /** The range, run on to the end of the strip's days so their dots are right. */
3512
3649
  dataRangeFor(range) {
3513
- const end = this.strip ? Math.max(range.endDay, this.strip.window().end) : range.endDay;
3514
- if (end === range.endDay) return range;
3515
- return { ...range, endDay: end, end: new Date(toInstant(end, 0, this.zone)) };
3650
+ const window = this.strip?.window();
3651
+ const start = window ? Math.min(range.startDay, window.start) : range.startDay;
3652
+ const end = window ? Math.max(range.endDay, window.end) : range.endDay;
3653
+ if (start === range.startDay && end === range.endDay) return range;
3654
+ return { ...range, startDay: start, start: new Date(toInstant(start, 0, this.zone)), endDay: end, end: new Date(toInstant(end, 0, this.zone)) };
3516
3655
  }
3517
3656
  /** Adds, updates or removes the date strip for the current view. */
3518
3657
  syncStrip(navigated) {
@@ -3559,20 +3698,24 @@ var Calendar = class {
3559
3698
  stripShown() {
3560
3699
  if (!this.strip || this.rebuilding || !this.view) return;
3561
3700
  const next = this.dataRangeFor(this.view.range);
3562
- if (next.endDay === this.view.dataRange.endDay) return;
3701
+ if (next.startDay === this.view.dataRange.startDay && next.endDay === this.view.dataRange.endDay) return;
3563
3702
  this.view = { ...this.view, dataRange: next };
3564
3703
  this.opts.onRangeChange?.(this.view);
3565
3704
  }
3566
3705
  /** What is under a point, the date strip included. */
3567
3706
  hitAt(x, y) {
3707
+ const target = this.doc.elementFromPoint(x, y);
3708
+ if (!target || !this.root.contains(target)) {
3709
+ this.strip?.markTarget(null);
3710
+ return null;
3711
+ }
3568
3712
  const strip = this.strip;
3569
3713
  if (strip) {
3570
- const el = this.doc.elementFromPoint(x, y);
3571
- const day = el ? strip.dayAt(el) : null;
3714
+ const day = strip.dayAt(target);
3572
3715
  strip.markTarget(day !== null && this.draggingId ? day : null);
3573
3716
  if (day !== null) return { kind: "day", day };
3574
3717
  }
3575
- return this.current?.hit(x, y) ?? null;
3718
+ return this.current?.el.contains(target) ? this.current.hit(x, y) : null;
3576
3719
  }
3577
3720
  agendaList() {
3578
3721
  return this.opts.view === "list" && this.opts.listAgenda && this.current instanceof ListView ? this.current : null;
@@ -3655,17 +3798,44 @@ var Calendar = class {
3655
3798
  );
3656
3799
  const title = h(this.doc, "h2", "tnc-title", { "aria-live": "polite" }, this.view.title);
3657
3800
  const viewGroup = h(this.doc, "div", "tnc-toolbar-group tnc-toolbar-views", { role: "group" });
3801
+ const viewSelect = h(this.doc, "select", "tnc-btn tnc-toolbar-view-select", {
3802
+ "aria-label": this.text.viewHints[this.opts.view] ?? this.text.views[this.opts.view] ?? this.opts.view
3803
+ });
3658
3804
  for (const v of views) {
3659
3805
  const label = v === "days" ? this.text.views.days ?? `${this.opts.dayCount} days` : this.text.views[v] ?? v;
3660
3806
  viewGroup.append(btn(label, this.text.viewHints[v] ?? label, `tnc-btn-view tnc-btn-view-${v}`, () => this.changeView(v), v === this.opts.view));
3807
+ viewSelect.append(h(this.doc, "option", "", { value: v }, label));
3661
3808
  }
3809
+ viewSelect.value = this.opts.view;
3810
+ viewSelect.addEventListener("change", () => this.changeView(viewSelect.value));
3662
3811
  const start = h(this.doc, "div", "tnc-toolbar-start");
3663
3812
  const end = h(this.doc, "div", "tnc-toolbar-end");
3664
3813
  if (cfg.extraPlacement === "start") start.append(...extras);
3665
3814
  start.append(nav, title);
3666
- end.append(viewGroup);
3815
+ end.append(viewGroup, viewSelect);
3667
3816
  if (cfg.extraPlacement !== "start") end.append(...extras);
3668
3817
  this.toolbarEl.append(start, end);
3818
+ this.syncToolbarViewDisplay();
3819
+ }
3820
+ syncToolbarViewDisplay() {
3821
+ const buttons = this.toolbarEl.querySelector(".tnc-toolbar-views");
3822
+ const select = this.toolbarEl.querySelector(".tnc-toolbar-view-select");
3823
+ const start = this.toolbarEl.querySelector(".tnc-toolbar-start");
3824
+ if (!buttons || !select || !start) return;
3825
+ const config = this.opts.toolbar;
3826
+ const mode = typeof config === "object" ? config.viewDisplay ?? "auto" : "auto";
3827
+ buttons.hidden = false;
3828
+ select.hidden = true;
3829
+ buttons.style.display = "flex";
3830
+ select.style.display = "none";
3831
+ const startWidth = Array.from(start.children).reduce((width, child) => width + child.scrollWidth, 0);
3832
+ const availableWidth = Math.min(this.root.clientWidth, this.toolbarEl.clientWidth);
3833
+ const compact = mode === "select" || mode === "auto" && startWidth + buttons.scrollWidth + 32 > availableWidth;
3834
+ buttons.hidden = compact;
3835
+ select.hidden = !compact;
3836
+ buttons.style.display = compact ? "none" : "flex";
3837
+ select.style.display = compact ? "" : "none";
3838
+ select.parentElement?.classList.toggle("tnc-toolbar-end--compact", compact);
3669
3839
  }
3670
3840
  applyChange(change) {
3671
3841
  if (!change.added.length && !change.updated.length && !change.removed.length && !change.days.size && !change.spans) return;
@@ -3730,7 +3900,7 @@ var Calendar = class {
3730
3900
  const s = wallTime(n.startMs, this.zone);
3731
3901
  const start = this.fmt.time(s.minute, true);
3732
3902
  if (variant === "dot" || variant === "bar" || variant === "indicator") return { start: s.day === day ? start : "", end: "" };
3733
- if (n.startMs === n.endMs) return { start, end: "" };
3903
+ if (n.startMs === n.endMs || n.item.timeDisplay === "start") return { start, end: "" };
3734
3904
  return { start, end: this.fmt.time(wallTime(n.endMs, this.zone).minute, true) };
3735
3905
  }
3736
3906
  fillTime(el, n, variant, day) {
@@ -3746,7 +3916,8 @@ var Calendar = class {
3746
3916
  }
3747
3917
  const s = wallTime(n.startMs, this.zone);
3748
3918
  const e = wallTime(n.endMs, this.zone);
3749
- return `${n.item.title}, ${this.fmt.longDate(s.day)} ${this.fmt.time(s.minute)} \u2013 ${this.fmt.time(e.minute)}`;
3919
+ const start = `${n.item.title}, ${this.fmt.longDate(s.day)} ${this.fmt.time(s.minute)}`;
3920
+ return n.item.timeDisplay === "start" || n.startMs === n.endMs ? start : `${start} \u2013 ${this.fmt.time(e.minute)}`;
3750
3921
  }
3751
3922
  defaultContent(meta) {
3752
3923
  const { n, variant, day } = meta;
@@ -3774,7 +3945,7 @@ var Calendar = class {
3774
3945
  addHandles(el, meta) {
3775
3946
  const { n, variant } = meta;
3776
3947
  const ports = variant === "timeline" && !meta.mirror && n.id !== EXTERNAL_ID && !!this.opts.timeline?.onLink;
3777
- if (variant === "dot" || !this.isEditable(n, "duration")) {
3948
+ if (variant === "dot" || !this.isEditable(n, "duration", true)) {
3778
3949
  if (ports) this.addPorts(el, n);
3779
3950
  return;
3780
3951
  }
@@ -3798,6 +3969,38 @@ var Calendar = class {
3798
3969
  }
3799
3970
  }
3800
3971
  /* ============================================================== popover */
3972
+ onMoreGeometryChange = () => this.positionMore();
3973
+ listenMoreGeometry(on) {
3974
+ const method = on ? "addEventListener" : "removeEventListener";
3975
+ this.doc[method]("scroll", this.onMoreGeometryChange, true);
3976
+ this.win[method]("resize", this.onMoreGeometryChange);
3977
+ this.win.visualViewport?.[method]("resize", this.onMoreGeometryChange);
3978
+ this.win.visualViewport?.[method]("scroll", this.onMoreGeometryChange);
3979
+ }
3980
+ positionMore() {
3981
+ const popover = this.popover;
3982
+ if (!popover) return;
3983
+ if (!popover.anchor.isConnected) {
3984
+ this.closeMore();
3985
+ return;
3986
+ }
3987
+ const viewport = this.win.visualViewport;
3988
+ const root = this.root.getBoundingClientRect();
3989
+ const anchor = popover.anchor.getBoundingClientRect();
3990
+ const leftEdge = Math.max(root.left, viewport?.offsetLeft ?? 0) + 4;
3991
+ const topEdge = Math.max(root.top, viewport?.offsetTop ?? 0) + 4;
3992
+ const rightEdge = Math.min(root.right, (viewport?.offsetLeft ?? 0) + (viewport?.width ?? this.win.innerWidth)) - 4;
3993
+ const bottomEdge = Math.min(root.bottom, (viewport?.offsetTop ?? 0) + (viewport?.height ?? this.win.innerHeight)) - 4;
3994
+ if (rightEdge <= leftEdge || bottomEdge <= topEdge) {
3995
+ this.closeMore();
3996
+ return;
3997
+ }
3998
+ const width = Math.min(360, rightEdge - leftEdge);
3999
+ setStyle(popover.el, { width, "max-height": Math.min(420, bottomEdge - topEdge) });
4000
+ const left = Math.max(leftEdge, Math.min(anchor.left - 12, rightEdge - width));
4001
+ const top = Math.max(topEdge, Math.min(anchor.top - 40, bottomEdge - popover.el.offsetHeight));
4002
+ setStyle(popover.el, { left: left - root.left, top: top - root.top });
4003
+ }
3801
4004
  refreshMore() {
3802
4005
  const p = this.popover;
3803
4006
  if (!p) return;
@@ -3807,6 +4010,7 @@ var Calendar = class {
3807
4010
  p.pool.place(n.id, { n, day: p.day, isStart: n.startDay >= p.day, isEnd: n.endDay <= p.day + 1, variant: n.allDay ? "bar" : "dot", mirror: false });
3808
4011
  }
3809
4012
  p.pool.end();
4013
+ this.positionMore();
3810
4014
  }
3811
4015
  closeMore(restoreFocus = false) {
3812
4016
  const p = this.popover;
@@ -3834,6 +4038,7 @@ var Calendar = class {
3834
4038
  r.addEventListener("pointerover", this.onAdoptCheck);
3835
4039
  r.addEventListener("focusin", this.onAdoptCheck);
3836
4040
  this.doc.addEventListener("pointerdown", this.onDocPointerDown, true);
4041
+ this.listenMoreGeometry(true);
3837
4042
  }
3838
4043
  unbindEvents() {
3839
4044
  const r = this.root;
@@ -3852,6 +4057,7 @@ var Calendar = class {
3852
4057
  r.removeEventListener("pointerover", this.onAdoptCheck);
3853
4058
  r.removeEventListener("focusin", this.onAdoptCheck);
3854
4059
  this.doc.removeEventListener("pointerdown", this.onDocPointerDown, true);
4060
+ this.listenMoreGeometry(false);
3855
4061
  this.listenDoc(false);
3856
4062
  }
3857
4063
  listenDoc(on) {
@@ -3867,6 +4073,10 @@ var Calendar = class {
3867
4073
  return el && this.root.contains(el) && this.meta.has(el) && !this.meta.get(el)?.n.item.preview ? el : null;
3868
4074
  }
3869
4075
  onDocPointerDown = (ev) => {
4076
+ if (this.cancelledPointerId !== null) {
4077
+ this.cancelledPointerId = null;
4078
+ this.listenDoc(false);
4079
+ }
3870
4080
  const t = ev.target;
3871
4081
  if (this.popover && !this.popover.el.contains(t) && !this.popover.anchor.contains(t)) this.closeMore();
3872
4082
  if (this.selection && !this.root.contains(t)) this.unselect();
@@ -3874,6 +4084,7 @@ var Calendar = class {
3874
4084
  onPointerDown = (ev) => {
3875
4085
  if (ev.button !== 0 || this.session) return;
3876
4086
  const target = ev.target;
4087
+ if (target.closest(".tnc-dg--compact .tnc-dg-day")) return;
3877
4088
  if (target.closest("button, a, input, select, textarea, .tnc-toolbar, .tnc-port, [data-tnc-no-drag]")) {
3878
4089
  if (!target.closest(".tnc-item")) return;
3879
4090
  if (target.closest(".tnc-port, [data-tnc-no-drag], input, select, textarea")) return;
@@ -3897,6 +4108,7 @@ var Calendar = class {
3897
4108
  mode = "select";
3898
4109
  }
3899
4110
  const touch = ev.pointerType === "touch";
4111
+ const overflowOrigin = itemEl?.closest(".tnc-popover") && origin ? wallTime(origin.startMs, this.zone) : null;
3900
4112
  const s = {
3901
4113
  mode,
3902
4114
  pointerId: ev.pointerId,
@@ -3905,7 +4117,7 @@ var Calendar = class {
3905
4117
  y0: ev.clientY,
3906
4118
  active: false,
3907
4119
  origin,
3908
- grab: this.current?.hit(ev.clientX, ev.clientY) ?? null,
4120
+ 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,
3909
4121
  proposal: null,
3910
4122
  denied: false,
3911
4123
  timer: null,
@@ -3930,16 +4142,79 @@ var Calendar = class {
3930
4142
  };
3931
4143
  activate(s) {
3932
4144
  s.active = true;
4145
+ if (s.lastEvent) this.leaveHover(s.lastEvent);
3933
4146
  if (s.timer !== null) this.win.clearTimeout(s.timer);
3934
4147
  s.timer = null;
3935
4148
  if (s.origin) {
3936
4149
  this.draggingId = s.origin.id;
3937
- for (const el of this.elementsFor(s.origin.id)) el.classList.add("is-dragging");
4150
+ for (const el of this.elementsFor(s.origin.id)) {
4151
+ if (this.opts.view !== "list" && s.mode === "move") {
4152
+ const source = el.cloneNode(true);
4153
+ source.classList.remove("is-selected");
4154
+ source.classList.add("tnc-drag-source");
4155
+ source.setAttribute("aria-hidden", "true");
4156
+ source.inert = true;
4157
+ for (const node of [source, ...source.querySelectorAll("[id], [data-id], [data-task-path]")]) {
4158
+ node.removeAttribute("id");
4159
+ node.removeAttribute("data-id");
4160
+ node.removeAttribute("data-task-path");
4161
+ }
4162
+ el.before(source);
4163
+ this.dragSources.push(source);
4164
+ }
4165
+ if (this.opts.view === "list" && !this.listDragPreview && s.mode === "move") {
4166
+ const bounds = el.getBoundingClientRect();
4167
+ const preview = el.cloneNode(true);
4168
+ preview.classList.add("tnc-drag-preview", "is-dragging");
4169
+ preview.removeAttribute("data-id");
4170
+ preview.removeAttribute("data-task-path");
4171
+ preview.setAttribute("aria-hidden", "true");
4172
+ preview.inert = true;
4173
+ preview.style.width = `${bounds.width}px`;
4174
+ this.root.append(preview);
4175
+ this.listDragPreview = { element: preview, offsetX: s.x0 - bounds.left, offsetY: s.y0 - bounds.top, width: bounds.width, height: bounds.height };
4176
+ }
4177
+ el.classList.add("is-dragging");
4178
+ }
3938
4179
  }
3939
4180
  this.closeMore();
3940
4181
  if (s.mode !== "select") this.unselectSilently();
3941
4182
  this.root.classList.add("tnc-dragging", `tnc-dragging-${s.mode}`);
3942
4183
  if (s.lastEvent) this.track(s, s.lastEvent);
4184
+ this.startDragScroll();
4185
+ }
4186
+ startDragScroll() {
4187
+ if (this.dragScrollFrame !== null) return;
4188
+ const tick = () => {
4189
+ this.dragScrollFrame = null;
4190
+ const session = this.session;
4191
+ const pointer = session?.lastEvent;
4192
+ if (!session?.active || !pointer) return;
4193
+ const origin = session.origin ? this.elementsFor(session.origin.id)[0] : this.viewHost;
4194
+ const underPointer = this.doc.elementFromPoint(pointer.clientX, pointer.clientY);
4195
+ let moved = false;
4196
+ if (origin && underPointer && this.root.contains(underPointer)) {
4197
+ for (const axis of ["x", "y"]) {
4198
+ const scroller = scrollerOf(origin, axis);
4199
+ if (!scroller) continue;
4200
+ const viewport = scroller === this.doc.scrollingElement ? { left: 0, top: 0, right: this.win.innerWidth, bottom: this.win.innerHeight } : scroller.getBoundingClientRect();
4201
+ const coordinate = axis === "x" ? pointer.clientX : pointer.clientY;
4202
+ const lower = Math.max(0, axis === "x" ? viewport.left : viewport.top);
4203
+ const upper = Math.min(axis === "x" ? this.win.innerWidth : this.win.innerHeight, axis === "x" ? viewport.right : viewport.bottom);
4204
+ if (coordinate < lower || coordinate > upper) continue;
4205
+ const delta = coordinate < lower + 32 ? -Math.min(12, (lower + 32 - coordinate) / 2) : coordinate > upper - 32 ? Math.min(12, (coordinate - upper + 32) / 2) : 0;
4206
+ if (!delta) continue;
4207
+ if (!this.dragScrollOrigins.has(scroller)) this.dragScrollOrigins.set(scroller, { top: scroller.scrollTop, left: scroller.scrollLeft });
4208
+ const before = axis === "x" ? scroller.scrollLeft : scroller.scrollTop;
4209
+ if (axis === "x") scroller.scrollLeft += delta;
4210
+ else scroller.scrollTop += delta;
4211
+ moved ||= before !== (axis === "x" ? scroller.scrollLeft : scroller.scrollTop);
4212
+ }
4213
+ }
4214
+ if (moved) this.track(session, pointer);
4215
+ this.dragScrollFrame = this.win.requestAnimationFrame(tick);
4216
+ };
4217
+ this.dragScrollFrame = this.win.requestAnimationFrame(tick);
3943
4218
  }
3944
4219
  onTouchMove = (ev) => {
3945
4220
  if (this.session?.active) ev.preventDefault();
@@ -3969,6 +4244,12 @@ var Calendar = class {
3969
4244
  this.track(s, ev);
3970
4245
  };
3971
4246
  onPointerUp = (ev) => {
4247
+ if (ev.pointerId === this.cancelledPointerId) {
4248
+ this.cancelledPointerId = null;
4249
+ this.suppressNextClick();
4250
+ this.listenDoc(false);
4251
+ return;
4252
+ }
3972
4253
  const s = this.session;
3973
4254
  if (!s || ev.pointerId !== s.pointerId) return;
3974
4255
  if (!s.active) {
@@ -3981,28 +4262,49 @@ var Calendar = class {
3981
4262
  return;
3982
4263
  }
3983
4264
  this.suppressNextClick();
4265
+ this.track(s, ev);
3984
4266
  this.endSession(ev, false);
3985
4267
  };
3986
4268
  onPointerCancel = (ev) => {
4269
+ if (ev.pointerId === this.cancelledPointerId) {
4270
+ this.cancelledPointerId = null;
4271
+ this.listenDoc(false);
4272
+ }
3987
4273
  if (this.session && ev.pointerId === this.session.pointerId) this.endSession(null, true);
3988
4274
  };
3989
4275
  onDocKeyDown = (ev) => {
3990
4276
  if (ev.key === "Escape" && this.session) {
3991
4277
  ev.preventDefault();
3992
4278
  ev.stopPropagation();
4279
+ const pointerId = this.session.active ? this.session.pointerId : null;
3993
4280
  this.endSession(null, true);
4281
+ if (pointerId !== null) {
4282
+ this.cancelledPointerId = pointerId;
4283
+ this.listenDoc(true);
4284
+ }
3994
4285
  }
3995
4286
  };
3996
4287
  /** Proposal for the current pointer position. */
3997
4288
  track(s, ev) {
4289
+ if (this.listDragPreview) {
4290
+ const preview = this.listDragPreview;
4291
+ const bounds = this.root.getBoundingClientRect();
4292
+ preview.element.style.left = `${Math.max(8, Math.min(this.win.innerWidth - preview.width - 8, ev.clientX - preview.offsetX)) - bounds.left}px`;
4293
+ preview.element.style.top = `${Math.max(8, Math.min(this.win.innerHeight - preview.height - 8, ev.clientY - preview.offsetY)) - bounds.top}px`;
4294
+ }
3998
4295
  const hit = this.hitAt(ev.clientX, ev.clientY);
3999
4296
  if (s.mode === "external") {
4000
4297
  this.trackExternal(s, hit);
4001
4298
  return;
4002
4299
  }
4003
4300
  if (!s.grab) s.grab = hit;
4004
- if (!hit || !s.grab) return;
4301
+ if (!hit || !s.grab) {
4302
+ this.invalidateProposal(s);
4303
+ return;
4304
+ }
4005
4305
  if (s.mode === "select") {
4306
+ s.denied = false;
4307
+ this.root.classList.remove("tnc-drop-denied");
4006
4308
  const sel = this.selectionFor(s.grab, hit);
4007
4309
  if (!sel) return;
4008
4310
  this.selection = sel;
@@ -4011,25 +4313,40 @@ var Calendar = class {
4011
4313
  }
4012
4314
  const origin = s.origin;
4013
4315
  const p = s.mode === "move" ? this.moveProposal(origin, s.grab, hit) : this.resizeProposal(origin, hit, s.mode === "resize-start");
4014
- if (!p) return;
4316
+ if (!p) {
4317
+ this.invalidateProposal(s);
4318
+ return;
4319
+ }
4015
4320
  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;
4016
4321
  s.denied = !allowed;
4017
4322
  this.root.classList.toggle("tnc-drop-denied", !allowed);
4018
- if (!allowed || sameProposal(p, s.proposal)) return;
4323
+ if (!allowed) {
4324
+ this.invalidateProposal(s);
4325
+ return;
4326
+ }
4327
+ if (sameProposal(p, s.proposal)) return;
4019
4328
  s.proposal = p;
4020
4329
  this.dragRow = p.rowId !== void 0 ? { id: origin.id, rowId: p.rowId } : null;
4021
- this.applyChange(this.store.upsert([this.withProposal(origin, p)]));
4330
+ if (this.opts.view !== "list") this.applyChange(this.store.upsert([this.withProposal(origin, p)]));
4331
+ }
4332
+ invalidateProposal(s) {
4333
+ const hadProposal = s.proposal !== null;
4334
+ s.proposal = null;
4335
+ s.hit = null;
4336
+ s.denied = true;
4337
+ this.dragRow = null;
4338
+ this.root.classList.add("tnc-drop-denied");
4339
+ this.strip?.markTarget(null);
4340
+ this.unselectSilently();
4341
+ if (s.mode === "external") this.applyChange(this.store.remove([EXTERNAL_ID]));
4342
+ else if (hadProposal && s.origin) this.applyChange(this.store.upsert([s.origin.item]));
4022
4343
  }
4023
4344
  trackExternal(s, raw) {
4024
4345
  const src = s.source;
4025
4346
  const hit = raw?.kind === "timeline" && raw.area !== "track" && raw.area !== "header" ? null : raw;
4026
4347
  s.hit = hit;
4027
4348
  if (!hit) {
4028
- if (s.proposal) {
4029
- s.proposal = null;
4030
- this.applyChange(this.store.remove([EXTERNAL_ID]));
4031
- }
4032
- this.unselectSilently();
4349
+ this.invalidateProposal(s);
4033
4350
  return;
4034
4351
  }
4035
4352
  const dur = src.durationMinutes ?? this.opts.defaultTimedMinutes;
@@ -4048,6 +4365,10 @@ var Calendar = class {
4048
4365
  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;
4049
4366
  s.denied = !allowed;
4050
4367
  this.root.classList.toggle("tnc-drop-denied", !allowed);
4368
+ if (!allowed) {
4369
+ this.invalidateProposal(s);
4370
+ return;
4371
+ }
4051
4372
  if (sameProposal(p, s.proposal)) return;
4052
4373
  s.proposal = p;
4053
4374
  if (hit.kind === "timeline" && p.rowId === void 0) {
@@ -4240,7 +4561,18 @@ var Calendar = class {
4240
4561
  endSession(ev, cancel) {
4241
4562
  const s = this.session;
4242
4563
  if (!s) return;
4564
+ if (this.dragScrollFrame !== null) this.win.cancelAnimationFrame(this.dragScrollFrame);
4565
+ this.dragScrollFrame = null;
4566
+ if (cancel || s.denied) for (const [scroller, position] of this.dragScrollOrigins) {
4567
+ scroller.scrollTop = position.top;
4568
+ scroller.scrollLeft = position.left;
4569
+ }
4570
+ this.dragScrollOrigins.clear();
4243
4571
  this.session = null;
4572
+ this.listDragPreview?.element.remove();
4573
+ this.listDragPreview = null;
4574
+ for (const source of this.dragSources) source.remove();
4575
+ this.dragSources = [];
4244
4576
  if (s.timer !== null) this.win.clearTimeout(s.timer);
4245
4577
  this.listenDoc(false);
4246
4578
  this.root.classList.remove(
@@ -4309,10 +4641,16 @@ var Calendar = class {
4309
4641
  revert();
4310
4642
  return;
4311
4643
  }
4644
+ if (this.opts.view === "list") this.applyChange(this.store.upsert([this.withProposal(origin, p)]));
4312
4645
  this.flush();
4313
4646
  this.commit(s.mode === "move" ? "drop" : "resize", origin, p, ev, revert);
4314
4647
  }
4315
4648
  commit(kind, origin, p, ev, revert) {
4649
+ let reverted = false;
4650
+ const mark = (state) => {
4651
+ this.itemWriteStates.set(origin.id, state);
4652
+ for (const element of this.elementsFor(origin.id)) this.applyItemWriteState(element, origin.id);
4653
+ };
4316
4654
  const arg = {
4317
4655
  item: origin.item,
4318
4656
  start: new Date(p.startMs),
@@ -4328,19 +4666,32 @@ var Calendar = class {
4328
4666
  jsEvent: ev,
4329
4667
  source: ev && "key" in ev ? "keyboard" : "pointer",
4330
4668
  view: this.view,
4331
- revert
4669
+ revert: () => {
4670
+ reverted = true;
4671
+ revert();
4672
+ mark("failed");
4673
+ }
4332
4674
  };
4333
4675
  const handler = kind === "drop" ? this.opts.onItemDrop : this.opts.onItemResize;
4334
4676
  try {
4677
+ mark("pending");
4335
4678
  const res = handler?.(arg);
4336
4679
  if (res && typeof res.then === "function") {
4337
- Promise.resolve(res).catch(() => revert());
4338
- }
4680
+ void Promise.resolve(res).then(() => {
4681
+ if (!reverted) mark("changed");
4682
+ }, () => arg.revert());
4683
+ } else if (!reverted) mark("changed");
4339
4684
  } catch (err) {
4340
- revert();
4685
+ arg.revert();
4341
4686
  throw err;
4342
4687
  }
4343
4688
  }
4689
+ applyItemWriteState(element, id) {
4690
+ const state = this.itemWriteStates.get(id);
4691
+ for (const name of ["pending", "failed", "changed"]) element.classList.toggle(`is-write-${name}`, state === name);
4692
+ if (state === "pending") element.setAttribute("aria-busy", "true");
4693
+ else element.removeAttribute("aria-busy");
4694
+ }
4344
4695
  /* --------------------------------------------------------- click / keys */
4345
4696
  /** Runs in the capture phase so the click ending a drag never reaches content inside the calendar, React portals included. */
4346
4697
  onClickCapture = (ev) => {
@@ -4350,6 +4701,12 @@ var Calendar = class {
4350
4701
  ev.preventDefault();
4351
4702
  };
4352
4703
  onClick = (ev) => {
4704
+ const compactDay = ev.target.closest?.(".tnc-dg--compact .tnc-dg-day");
4705
+ if (compactDay && this.root.contains(compactDay) && !ev.target.closest(".tnc-navlink")) {
4706
+ const day = parseDay(compactDay.dataset.date ?? "");
4707
+ if (day !== null) this.openMore(day, compactDay, ev);
4708
+ return;
4709
+ }
4353
4710
  const yearDay = ev.target.closest?.(".tnc-ym-day");
4354
4711
  if (yearDay && this.root.contains(yearDay)) {
4355
4712
  const day = parseDay(yearDay.dataset.date ?? "");
@@ -4408,6 +4765,25 @@ var Calendar = class {
4408
4765
  if (el && meta) this.opts.onItemHover?.({ ...this.itemArg(el, meta), jsEvent: ev, entering: false });
4409
4766
  }
4410
4767
  onKeyDown = (ev) => {
4768
+ if (this.popover && !ev.altKey && !ev.ctrlKey && !ev.metaKey && ["ArrowDown", "ArrowUp", "Home", "End"].includes(ev.key)) {
4769
+ const rows = [...this.popover.el.querySelectorAll(".tnc-item")];
4770
+ const index = rows.indexOf(this.doc.activeElement);
4771
+ const next = ev.key === "Home" ? 0 : ev.key === "End" ? rows.length - 1 : ev.key === "ArrowDown" ? (index + 1) % rows.length : index <= 0 ? rows.length - 1 : index - 1;
4772
+ ev.preventDefault();
4773
+ ev.stopPropagation();
4774
+ rows[next]?.focus();
4775
+ rows[next]?.scrollIntoView({ block: "nearest", inline: "nearest" });
4776
+ return;
4777
+ }
4778
+ if (ev.key === "Tab" && this.popover) {
4779
+ const controls = [...this.popover.el.querySelectorAll('button, a[href], [tabindex="0"]')];
4780
+ const index = controls.indexOf(this.doc.activeElement);
4781
+ const next = ev.shiftKey ? index <= 0 ? controls.length - 1 : index - 1 : (index + 1) % controls.length;
4782
+ ev.preventDefault();
4783
+ ev.stopPropagation();
4784
+ controls[next]?.focus();
4785
+ return;
4786
+ }
4411
4787
  if (ev.key === "Escape" && this.popover) {
4412
4788
  ev.preventDefault();
4413
4789
  ev.stopPropagation();
@@ -4419,7 +4795,8 @@ var Calendar = class {
4419
4795
  const day = parseDay(target.dataset.date ?? "");
4420
4796
  if (day !== null) {
4421
4797
  ev.preventDefault();
4422
- this.clickSlot({ kind: "day", day }, ev, true);
4798
+ if (target.closest(".tnc-dg--compact")) this.openMore(day, target, ev);
4799
+ else this.clickSlot({ kind: "day", day }, ev, true);
4423
4800
  }
4424
4801
  return;
4425
4802
  }
@@ -4507,9 +4884,13 @@ var Calendar = class {
4507
4884
  const dt = ev.dataTransfer;
4508
4885
  if (!dt || !this.opts.acceptNativeDrop?.(dt) || !this.current) return;
4509
4886
  ev.preventDefault();
4887
+ const hit = this.hitAt(ev.clientX, ev.clientY);
4888
+ if (!hit || hit.kind === "timeline" && hit.area !== "track" && hit.area !== "header") {
4889
+ dt.dropEffect = "none";
4890
+ this.unselectSilently();
4891
+ return;
4892
+ }
4510
4893
  dt.dropEffect = "copy";
4511
- const hit = this.current.hit(ev.clientX, ev.clientY);
4512
- if (!hit || hit.kind === "timeline" && hit.area !== "track" && hit.area !== "header") return;
4513
4894
  const sel = this.selectionFor(hit, hit);
4514
4895
  if (!sel) return;
4515
4896
  this.selection = sel;
@@ -4523,7 +4904,7 @@ var Calendar = class {
4523
4904
  const dt = ev.dataTransfer;
4524
4905
  if (!dt || !this.opts.acceptNativeDrop?.(dt) || !this.current) return;
4525
4906
  ev.preventDefault();
4526
- const hit = this.current.hit(ev.clientX, ev.clientY);
4907
+ const hit = this.hitAt(ev.clientX, ev.clientY);
4527
4908
  this.unselectSilently();
4528
4909
  if (!hit || hit.kind === "timeline" && hit.area !== "track" && hit.area !== "header") return;
4529
4910
  const sel = this.selectionFor(hit, hit);
@@ -4554,7 +4935,7 @@ function scrollerOf(el, axis) {
4554
4935
  return doc.scrollingElement;
4555
4936
  }
4556
4937
  function rangeMoved(prev, next) {
4557
- return !prev || prev.type !== next.type || prev.range.startDay !== next.range.startDay || prev.range.endDay !== next.range.endDay || prev.dataRange.endDay !== next.dataRange.endDay;
4938
+ 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;
4558
4939
  }
4559
4940
 
4560
4941
  // src/react/index.tsx