@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.
@@ -953,6 +953,7 @@ var DEFAULT_TEXT = {
953
953
  allDay: "All day",
954
954
  noItems: "Nothing scheduled",
955
955
  more: (n) => `+${n} more`,
956
+ appointments: (n) => `${n} appointments`,
956
957
  itemCount: (n) => n === 1 ? "1 item" : `${n} items`,
957
958
  weekNumber: (n) => `W${n}`,
958
959
  views: { week: "Week", day: "Day", days: "Days", month: "Month", list: "List", year: "Year", timeline: "Timeline" },
@@ -1022,6 +1023,7 @@ var LAYOUT_KEYS = /* @__PURE__ */ new Set([
1022
1023
  "dateStrip",
1023
1024
  "listLeadTitle",
1024
1025
  "listAddText",
1026
+ "listEmptyContent",
1025
1027
  "monthDisplay",
1026
1028
  "timeZone",
1027
1029
  "locale",
@@ -1072,8 +1074,9 @@ var DayGridView = class {
1072
1074
  constructor(ctx) {
1073
1075
  this.ctx = ctx;
1074
1076
  const { doc, opts, days } = ctx;
1075
- this.dotsMode = opts.monthDisplay === "dots";
1077
+ this.dotsMode = opts.monthDisplay === "dots" || opts.monthDisplay === "chips";
1076
1078
  this.el = h(doc, "div", this.dotsMode ? "tnc-dg tnc-dg--dots" : "tnc-dg");
1079
+ this.el.classList.toggle("tnc-dg--chips", opts.monthDisplay === "chips");
1077
1080
  this.scroller = h(doc, "div", "tnc-dg-scroll");
1078
1081
  this.grid = h(doc, "div", "tnc-dg-grid", { role: "grid", "aria-label": ctx.view.title });
1079
1082
  const perWeek = this.daysPerWeek();
@@ -1103,14 +1106,14 @@ var DayGridView = class {
1103
1106
  const cell = h(doc, "div", cls.join(" "), { role: "gridcell", "aria-label": ctx.fmt.longDate(d), "data-date": arg.dateStr });
1104
1107
  if (this.dotsMode) cell.tabIndex = 0;
1105
1108
  const num2 = ctx.fmt.dayNumber(d);
1106
- const label = ymdOfDay(d).day === 1 ? `${ctx.fmt.monthShort(d)} ${num2}` : num2;
1109
+ const label = !this.dotsMode && ymdOfDay(d).day === 1 ? `${ctx.fmt.monthShort(d)} ${num2}` : num2;
1107
1110
  cell.append(
1108
1111
  h(
1109
1112
  doc,
1110
1113
  "div",
1111
1114
  "tnc-dg-top",
1112
1115
  null,
1113
- opts.navLinks ? ctx.navLink(d, label, "tnc-dg-num") : h(doc, "span", "tnc-dg-num", null, label)
1116
+ opts.navLinks && !this.dotsMode ? ctx.navLink(d, label, "tnc-dg-num") : h(doc, "span", "tnc-dg-num", null, label)
1114
1117
  )
1115
1118
  );
1116
1119
  dates.append(cell);
@@ -1124,20 +1127,20 @@ var DayGridView = class {
1124
1127
  layer.append(content);
1125
1128
  row.append(dates, layer);
1126
1129
  this.grid.append(row);
1127
- const dots = this.dotsMode ? cells.map((cell) => {
1130
+ const dots = cells.map((cell) => {
1128
1131
  const box = h(doc, "div", "tnc-dg-dots", { "aria-hidden": "true" });
1129
1132
  if (opts.dayMaxEvents === false) box.classList.add("is-unlimited");
1130
1133
  const count = h(doc, "span", "tnc-dg-dots-more");
1131
1134
  cell.append(box);
1132
1135
  return { box, pool: new ItemPool(ctx, box), count };
1133
- }) : null;
1136
+ });
1134
1137
  this.weeks.push({ days: wdays, row, cells, layer, sel, more, pool: new ItemPool(ctx, content), dots });
1135
1138
  }
1136
1139
  this.scroller.append(this.grid);
1137
1140
  this.el.append(this.scroller);
1138
1141
  this.el.classList.toggle("tnc-dg--auto", opts.height === "auto");
1139
1142
  const RO = ctx.win.ResizeObserver;
1140
- if (RO && (opts.dayMaxEvents === true || this.dotsMode && opts.dayMaxEvents !== false)) {
1143
+ if (RO) {
1141
1144
  this.resizeObs = new RO(() => {
1142
1145
  if (this.sizeKey() !== this.lastHeights) this.render(null);
1143
1146
  });
@@ -1168,14 +1171,35 @@ var DayGridView = class {
1168
1171
  }
1169
1172
  render(_change) {
1170
1173
  const { ctx } = this;
1174
+ this.dotsMode = ctx.opts.monthDisplay !== "full" || this.grid.clientWidth / this.daysPerWeek() < 72;
1175
+ this.el.classList.toggle("tnc-dg--dots", this.dotsMode);
1176
+ this.el.classList.toggle("tnc-dg--compact", this.dotsMode && ctx.opts.monthDisplay === "full");
1177
+ this.el.classList.toggle("tnc-dg--chips", ctx.opts.monthDisplay === "chips" && this.weeks[0].dots[0].box.clientHeight >= 44);
1171
1178
  const cs = ctx.win.getComputedStyle(this.el);
1172
1179
  this.barPx = px(cs, "--tnc-bar-h", 20);
1173
1180
  this.topPx = this.weeks[0]?.cells[0]?.querySelector(".tnc-dg-top")?.offsetHeight || px(cs, "--tnc-dg-top-h", 24);
1174
1181
  this.morePx = this.measureMore(cs);
1175
1182
  this.lastHeights = this.sizeKey();
1176
1183
  for (const w of this.weeks) {
1177
- if (w.dots) this.renderDots(w);
1178
- else this.renderWeek(w);
1184
+ for (const cell of w.cells) {
1185
+ if (this.dotsMode) cell.tabIndex = 0;
1186
+ else cell.removeAttribute("tabindex");
1187
+ if (this.dotsMode && ctx.opts.monthDisplay === "full") cell.setAttribute("aria-haspopup", "dialog");
1188
+ else cell.removeAttribute("aria-haspopup");
1189
+ }
1190
+ for (const cell of w.dots) cell.box.hidden = !this.dotsMode;
1191
+ if (this.dotsMode) {
1192
+ w.pool.begin();
1193
+ w.pool.end();
1194
+ w.more.replaceChildren();
1195
+ this.renderDots(w);
1196
+ } else {
1197
+ for (const cell of w.dots) {
1198
+ cell.pool.begin();
1199
+ cell.pool.end();
1200
+ }
1201
+ this.renderWeek(w);
1202
+ }
1179
1203
  }
1180
1204
  }
1181
1205
  /** Height of a "+N more" link as the host styles it (`--tnc-more-h`, or a taller min-height). */
@@ -1261,19 +1285,28 @@ var DayGridView = class {
1261
1285
  });
1262
1286
  }
1263
1287
  w.pool.end();
1264
- w.more.replaceChildren();
1288
+ const existing = new Map(Array.from(w.more.children, (element) => [element.getAttribute("data-day"), element]));
1265
1289
  hidden.forEach((items, col) => {
1266
1290
  if (!items.length) return;
1267
1291
  const day = w.days[col];
1268
- const link = h(ctx.doc, "button", "tnc-more", { type: "button", "aria-haspopup": "dialog" }, ctx.text.more(items.length));
1292
+ const key = String(day);
1293
+ let link = existing.get(key);
1294
+ if (!link) {
1295
+ link = h(ctx.doc, "button", "tnc-more", { type: "button", "aria-haspopup": "dialog", "data-day": key });
1296
+ const anchor = link;
1297
+ link.addEventListener("click", (ev) => ctx.openMore(day, anchor, ev));
1298
+ w.more.append(link);
1299
+ }
1300
+ existing.delete(key);
1301
+ link.setAttribute("aria-label", ctx.text.more(items.length));
1302
+ link.textContent = w.row.clientWidth / n < 72 ? `+${new Intl.NumberFormat(ctx.opts.locale).format(items.length)}` : ctx.text.more(items.length);
1269
1303
  setStyle(link, {
1270
1304
  left: `calc(${pct(col / n)} + 2px)`,
1271
1305
  width: `calc(${pct(1 / n)} - 4px)`,
1272
1306
  top: `calc(var(--tnc-dg-top-h) + ${visible(col)} * (var(--tnc-bar-h) + ${BAR_GAP}px))`
1273
1307
  });
1274
- link.addEventListener("click", (ev) => ctx.openMore(day, link, ev));
1275
- w.more.append(link);
1276
1308
  });
1309
+ for (const link of existing.values()) link.remove();
1277
1310
  const step = this.barPx + BAR_GAP;
1278
1311
  let need = maxLevel >= 0 ? (maxLevel + 1) * step - BAR_GAP : 0;
1279
1312
  hidden.forEach((items, col) => {
@@ -1296,10 +1329,11 @@ var DayGridView = class {
1296
1329
  const { dayMaxEvents } = ctx.opts;
1297
1330
  const first = w.dots[0].box;
1298
1331
  const cs = ctx.win.getComputedStyle(first);
1299
- const size = px(cs, "--tnc-dot-size", 8);
1332
+ const chips = this.el.classList.contains("tnc-dg--chips");
1333
+ const size = chips ? px(cs, "--tnc-chip-h", 14) : px(cs, "--tnc-dot-size", 8);
1300
1334
  const gap = px(cs, "--tnc-dot-gap", 3);
1301
1335
  const step = size + gap;
1302
- const perRow = Math.max(1, Math.floor((first.clientWidth + gap) / step));
1336
+ const perRow = chips ? 1 : Math.max(1, Math.floor((first.clientWidth + gap) / step));
1303
1337
  const rows = Math.max(1, Math.floor((first.clientHeight + gap) / step));
1304
1338
  const fits = dayMaxEvents === false ? Infinity : perRow * rows;
1305
1339
  const cap = typeof dayMaxEvents === "number" && dayMaxEvents > 0 ? Math.min(dayMaxEvents, fits) : fits;
@@ -1308,7 +1342,7 @@ var DayGridView = class {
1308
1342
  w.days.forEach((day, i) => {
1309
1343
  const cell = w.dots[i];
1310
1344
  const items = this.itemsOn(day);
1311
- const shown = items.length <= cap ? items.length : cap >= fits ? Math.max(0, cap - 2) : cap;
1345
+ const shown = items.length <= cap ? items.length : cap >= fits ? Math.max(1, cap - (chips ? 1 : 2)) : Math.max(1, cap);
1312
1346
  cell.pool.begin();
1313
1347
  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 }));
1314
1348
  cell.pool.end();
@@ -1425,6 +1459,7 @@ var ListView = class {
1425
1459
  const leadItems = [];
1426
1460
  const inLead = /* @__PURE__ */ new Set();
1427
1461
  const keep = /* @__PURE__ */ new Set();
1462
+ let itemCount = 0;
1428
1463
  let prev = null;
1429
1464
  for (const day of days) {
1430
1465
  let items = this.itemsOn(day);
@@ -1438,7 +1473,7 @@ var ListView = class {
1438
1473
  return false;
1439
1474
  });
1440
1475
  }
1441
- if (!items.length && !agenda) continue;
1476
+ if (!items.length && !agenda && opts.listDayCount !== 1) continue;
1442
1477
  keep.add(day);
1443
1478
  let section = this.sections.get(day);
1444
1479
  let pool = this.pools.get(day);
@@ -1457,6 +1492,7 @@ var ListView = class {
1457
1492
  this.pools.set(day, pool);
1458
1493
  }
1459
1494
  section.classList.toggle("is-empty", !items.length);
1495
+ itemCount += items.length;
1460
1496
  this.fill(pool, items, () => day);
1461
1497
  if (prev ? prev.nextSibling !== section : this.body.firstChild !== section) {
1462
1498
  if (prev) prev.after(section);
@@ -1472,11 +1508,14 @@ var ListView = class {
1472
1508
  this.sections.delete(day);
1473
1509
  }
1474
1510
  this.renderLead(leadItems);
1475
- let empty = this.body.querySelector(":scope > .tnc-list-empty");
1476
- if (!keep.size && !leadItems.length) {
1511
+ let empty = this.body.querySelector(".tnc-list-empty");
1512
+ if (!itemCount && !leadItems.length && (!agenda || opts.listEmptyContent)) {
1477
1513
  if (!empty) {
1478
1514
  empty = h(doc, "div", "tnc-list-empty", { role: "listitem" }, ctx.text.noItems);
1479
- this.body.append(empty);
1515
+ const content = ctx.opts.listEmptyContent?.();
1516
+ if (content) empty.replaceChildren(content);
1517
+ if (this.sections.size === 1) this.sections.values().next().value.append(empty);
1518
+ else this.body.prepend(empty);
1480
1519
  }
1481
1520
  } else empty?.remove();
1482
1521
  this.ctx.strip?.draw();
@@ -1534,7 +1573,7 @@ var ListView = class {
1534
1573
  const section = day === this.ctx.days[0] && this.lead ? this.lead.section : this.sections.get(day);
1535
1574
  if (!section) return;
1536
1575
  this.ctx.strip?.measure();
1537
- section.scrollIntoView({ block: "start" });
1576
+ this.scroller.scrollTop += section.getBoundingClientRect().top - this.scroller.getBoundingClientRect().top;
1538
1577
  this.ctx.strip?.setFocus(day);
1539
1578
  this.pinned = true;
1540
1579
  }
@@ -1623,8 +1662,7 @@ var DateStrip = class {
1623
1662
  head.append(this.title, this.todayBtn, prev, next, this.slot);
1624
1663
  this.dows = h(doc, "div", "tnc-strip-dows", { "aria-hidden": "true" });
1625
1664
  this.grid = h(doc, "div", "tnc-strip-grid", { role: "group" });
1626
- const handle = h(doc, "button", "tnc-strip-handle", { type: "button", tabindex: "-1", "aria-hidden": "true" });
1627
- handle.addEventListener("click", () => this.setExpanded(!this.expanded));
1665
+ const handle = h(doc, "div", "tnc-strip-handle", { "aria-hidden": "true" });
1628
1666
  this.el.append(head, this.dows, this.grid, handle);
1629
1667
  this.grid.addEventListener("click", this.onClick);
1630
1668
  this.grid.addEventListener("keydown", this.onKeyDown);
@@ -1824,6 +1862,8 @@ var TimeGridView = class {
1824
1862
  this.el = h(doc, "div", "tnc-tg", { role: "table", "aria-label": ctx.view.title });
1825
1863
  this.el.classList.toggle("tnc-tg--no-grid", !showGrid);
1826
1864
  this.scroller = h(doc, "div", "tnc-tg-scroll");
1865
+ this.dayNavigation = h(doc, "div", "tnc-tg-day-navigation", { role: "group", "aria-label": ctx.view.title });
1866
+ this.scroller.addEventListener("scroll", () => this.updateDayNavigation());
1827
1867
  this.top = h(doc, "div", "tnc-tg-top");
1828
1868
  this.headRow = h(doc, "div", "tnc-tg-head tnc-tg-cols", { role: "row" });
1829
1869
  this.top.append(this.headRow);
@@ -1851,7 +1891,7 @@ var TimeGridView = class {
1851
1891
  } else {
1852
1892
  this.body = this.gutter = null;
1853
1893
  }
1854
- this.el.append(this.scroller);
1894
+ this.el.append(this.dayNavigation, this.scroller);
1855
1895
  this.build();
1856
1896
  if (showGrid && opts.height === "fill") {
1857
1897
  const RO = ctx.win.ResizeObserver;
@@ -1864,6 +1904,7 @@ var TimeGridView = class {
1864
1904
  ctx;
1865
1905
  el;
1866
1906
  scroller;
1907
+ dayNavigation;
1867
1908
  top;
1868
1909
  headRow;
1869
1910
  allDayRow;
@@ -1919,6 +1960,9 @@ var TimeGridView = class {
1919
1960
  head.append(cap);
1920
1961
  }
1921
1962
  this.headRow.append(head);
1963
+ const dayButton = h(doc, "button", "tnc-btn", { type: "button", "aria-label": ctx.fmt.longDate(day) }, `${ctx.fmt.weekdayShort(day)} ${ctx.fmt.dayNumber(day)}`);
1964
+ dayButton.addEventListener("click", () => this.revealDay(day));
1965
+ this.dayNavigation.append(dayButton);
1922
1966
  opts.dayHeaderDidMount?.({ ...arg, el: head });
1923
1967
  const col = h(doc, "div", ["tnc-col", ...ctx.dayClasses(day)].join(" "), { role: "cell", "aria-label": ctx.fmt.longDate(day), "data-date": arg.dateStr });
1924
1968
  const bgLayer = h(doc, "div", "tnc-col-bg");
@@ -1935,6 +1979,7 @@ var TimeGridView = class {
1935
1979
  this.body?.append(col);
1936
1980
  return {
1937
1981
  day,
1982
+ collisionColumns: 1,
1938
1983
  head,
1939
1984
  col,
1940
1985
  plan: new ItemPool(ctx, planLayer),
@@ -1991,12 +2036,17 @@ var TimeGridView = class {
1991
2036
  /* --------------------------------------------------------------- render */
1992
2037
  render(change) {
1993
2038
  this.renderAllDay();
1994
- if (!this.body) return;
2039
+ if (!this.body) {
2040
+ this.updateDayNavigation();
2041
+ return;
2042
+ }
1995
2043
  const dirty = change ? change.days : null;
1996
2044
  for (const c of this.cols) {
1997
2045
  if (dirty && !dirty.has(c.day) && !(this.ctx.slot.max > MINUTES_PER_DAY && dirty.has(c.day + 1))) continue;
1998
2046
  this.renderColumn(c);
1999
2047
  }
2048
+ this.el.style.setProperty("--tnc-collision-columns", String(Math.max(1, ...this.cols.map((column) => column.collisionColumns))));
2049
+ this.updateDayNavigation();
2000
2050
  this.renderCapacity();
2001
2051
  this.tick();
2002
2052
  }
@@ -2114,6 +2164,11 @@ var TimeGridView = class {
2114
2164
  }
2115
2165
  const minMinutes = opts.eventMinHeight / this.ppm;
2116
2166
  const layout = layoutTimedDay(items, c.day, zone, minMinutes, ctx.order, [slot.min, slot.max]);
2167
+ c.collisionColumns = Math.max(
2168
+ 1,
2169
+ ...layout.plan.map((placement) => opts.eventMaxStack > 0 ? Math.min(placement.cols, opts.eventMaxStack) : placement.cols),
2170
+ ...layout.actual.filter((placement) => !placement.split).map((placement) => placement.cols)
2171
+ );
2117
2172
  const total = this.minutes;
2118
2173
  const top = (m) => pct((m - slot.min) / total);
2119
2174
  const height = (s) => pct((s.endMin - s.startMin) / total);
@@ -2125,14 +2180,22 @@ var TimeGridView = class {
2125
2180
  c.bg.end();
2126
2181
  const hasActual = layout.actual.length > 0;
2127
2182
  c.col.classList.toggle("has-actual", hasActual);
2183
+ for (const link of c.more.children) link.setAttribute("data-stale", "true");
2128
2184
  this.placeLane(c.plan, layout.plan, c, top, height, "plan");
2129
2185
  this.placeLane(c.actual, layout.actual, c, top, height, "actual");
2186
+ for (const link of [...c.more.children]) if (link.hasAttribute("data-stale")) link.remove();
2130
2187
  }
2131
2188
  placeLane(pool, placements, c, top, height, lane) {
2132
2189
  const { ctx } = this;
2133
2190
  const { opts } = ctx;
2134
- const withMore = lane === "plan";
2135
- const maxStack = opts.eventMaxStack;
2191
+ const readableWidth = Number.parseFloat(ctx.win.getComputedStyle(this.el).getPropertyValue("--tnc-event-min-width")) || 128;
2192
+ const laneWidth = c.col.clientWidth * (lane === "actual" && placements.some((placement) => placement.split) ? 0.22 : 1);
2193
+ const collapse = placements.some((placement) => {
2194
+ const columns = opts.eventMaxStack > 0 ? Math.min(placement.cols, opts.eventMaxStack) : placement.cols;
2195
+ return columns > 1 && columns * readableWidth > laneWidth;
2196
+ });
2197
+ const withMore = lane === "plan" || collapse;
2198
+ const maxStack = collapse ? 1 : opts.eventMaxStack;
2136
2199
  const hiddenByCluster = /* @__PURE__ */ new Map();
2137
2200
  pool.begin();
2138
2201
  for (const p of placements) {
@@ -2158,21 +2221,46 @@ var TimeGridView = class {
2158
2221
  top: top(p.startMin),
2159
2222
  height: `max(${height(p)}, ${opts.eventMinHeight}px)`,
2160
2223
  ...inLane(lane, p.split, left, width),
2224
+ "--tnc-event-readable-width": pct(span / cols / width),
2161
2225
  "z-index": String(p.col + 1)
2162
2226
  });
2163
2227
  }
2164
2228
  pool.end();
2165
2229
  if (!withMore) return;
2166
- c.more.replaceChildren();
2167
- for (const rec of hiddenByCluster.values()) {
2168
- const link = h(ctx.doc, "button", "tnc-more tnc-more--stack", { type: "button", "aria-haspopup": "dialog" }, `+${rec.count}`);
2230
+ for (const [cluster, rec] of hiddenByCluster) {
2231
+ const key = `${lane}-${cluster}`;
2232
+ let link = c.more.querySelector(`[data-cluster="${key}"]`);
2233
+ if (!link) {
2234
+ link = h(ctx.doc, "button", "tnc-more tnc-more--stack", { type: "button", "aria-haspopup": "dialog", "data-cluster": key });
2235
+ const anchor = link;
2236
+ link.addEventListener("click", (ev) => ctx.openMore(c.day, anchor, ev, JSON.parse(anchor.dataset.itemIds ?? "[]")));
2237
+ c.more.append(link);
2238
+ }
2239
+ link.removeAttribute("data-stale");
2240
+ link.dataset.itemIds = JSON.stringify(placements.filter((placement) => placement.cluster === cluster).map((placement) => placement.item.id));
2241
+ link.classList.toggle("tnc-more--collision", collapse);
2242
+ link.textContent = `+${rec.count}`;
2243
+ if (collapse) {
2244
+ const items = placements.filter((placement) => placement.cluster === cluster).map((placement) => placement.item.item.title);
2245
+ const count = ctx.text.appointments(rec.count);
2246
+ const interval = `${ctx.fmt.time(rec.start)} \u2013 ${ctx.fmt.time(rec.end)}`;
2247
+ link.replaceChildren(
2248
+ h(ctx.doc, "span", "tnc-collision-count", null, String(rec.count)),
2249
+ h(ctx.doc, "span", "tnc-collision-range", null, interval),
2250
+ h(ctx.doc, "span", "tnc-collision-title", { "aria-hidden": "true" }, items.join(" \xB7 "))
2251
+ );
2252
+ link.setAttribute("aria-label", `${count}, ${interval}: ${items.join(", ")}`);
2253
+ link.title = link.getAttribute("aria-label");
2254
+ } else {
2255
+ link.removeAttribute("aria-label");
2256
+ link.removeAttribute("title");
2257
+ }
2169
2258
  const cols = maxStack;
2170
2259
  setStyle(link, {
2171
2260
  top: top(rec.start),
2261
+ height: collapse ? pct((rec.end - rec.start) / this.minutes) : "",
2172
2262
  ...inLane(lane, rec.split, (cols - 1) / cols, 1 / cols)
2173
2263
  });
2174
- link.addEventListener("click", (ev) => ctx.openMore(c.day, link, ev));
2175
- c.more.append(link);
2176
2264
  }
2177
2265
  }
2178
2266
  meta(s, bg) {
@@ -2237,12 +2325,10 @@ var TimeGridView = class {
2237
2325
  const r2 = cells[i2].getBoundingClientRect();
2238
2326
  if (x >= r2.left && x < r2.right) return i2;
2239
2327
  }
2240
- const firstR = cells[0]?.getBoundingClientRect();
2241
- const lastR = cells[cells.length - 1]?.getBoundingClientRect();
2242
- if (firstR && x < firstR.left) return 0;
2243
- if (lastR && x >= lastR.right) return cells.length - 1;
2244
2328
  return -1;
2245
2329
  };
2330
+ const bounds = this.scroller.getBoundingClientRect();
2331
+ if (x < bounds.left || x >= bounds.right || y < bounds.top || y >= bounds.bottom) return null;
2246
2332
  if (this.allDayRow) {
2247
2333
  const r2 = this.allDayRow.getBoundingClientRect();
2248
2334
  if (y >= r2.top && y < r2.bottom || !this.body && y >= r2.top) {
@@ -2283,6 +2369,26 @@ var TimeGridView = class {
2283
2369
  }
2284
2370
  resize() {
2285
2371
  this.fitRows();
2372
+ this.render(null);
2373
+ }
2374
+ revealDay(day) {
2375
+ const column = this.cols.find((candidate) => candidate.day === day);
2376
+ if (!column) return;
2377
+ const bounds = column.head.getBoundingClientRect();
2378
+ const viewport = this.scroller.getBoundingClientRect();
2379
+ this.scroller.scrollLeft += bounds.left - viewport.left - (viewport.width - bounds.width) / 2;
2380
+ this.updateDayNavigation();
2381
+ }
2382
+ updateDayNavigation() {
2383
+ this.dayNavigation.hidden = this.scroller.scrollWidth <= this.scroller.clientWidth + 1;
2384
+ const viewport = this.scroller.getBoundingClientRect();
2385
+ this.cols.forEach((column, index) => {
2386
+ const bounds = column.head.getBoundingClientRect();
2387
+ const visible = bounds.left >= viewport.left && bounds.right <= viewport.right;
2388
+ const button = this.dayNavigation.children[index];
2389
+ button?.classList.toggle("is-offscreen", !visible);
2390
+ button?.setAttribute("aria-pressed", String(visible));
2391
+ });
2286
2392
  }
2287
2393
  scrollToTime(minute) {
2288
2394
  if (!this.body) return;
@@ -2945,6 +3051,14 @@ ${this.ctx.text.more(items.length - 8)}` : ""}` : label;
2945
3051
  setScroll(s) {
2946
3052
  this.scroller.scrollTop = s.top;
2947
3053
  }
3054
+ revealDay(day) {
3055
+ const month = this.cells.find((cell) => cell.day === day)?.el.closest(".tnc-ym");
3056
+ if (!month) return;
3057
+ const bounds = month.getBoundingClientRect();
3058
+ const viewport = this.scroller.getBoundingClientRect();
3059
+ if (bounds.top < viewport.top) this.scroller.scrollTop += bounds.top - viewport.top;
3060
+ else if (bounds.bottom > viewport.bottom) this.scroller.scrollTop += bounds.bottom - viewport.bottom;
3061
+ }
2948
3062
  elementsFor(_id) {
2949
3063
  return [];
2950
3064
  }
@@ -3024,6 +3138,11 @@ var Calendar = class {
3024
3138
  conflicts = /* @__PURE__ */ new Map();
3025
3139
  slot = { min: 0, max: MINUTES_PER_DAY, duration: 30, snap: 30, scroll: 480 };
3026
3140
  draggingId = null;
3141
+ listDragPreview = null;
3142
+ dragSources = [];
3143
+ dragScrollFrame = null;
3144
+ dragScrollOrigins = /* @__PURE__ */ new Map();
3145
+ itemWriteStates = /* @__PURE__ */ new Map();
3027
3146
  dragRow = null;
3028
3147
  strip = null;
3029
3148
  stripSize = null;
@@ -3037,6 +3156,7 @@ var Calendar = class {
3037
3156
  session = null;
3038
3157
  selection = null;
3039
3158
  suppressClick = false;
3159
+ cancelledPointerId = null;
3040
3160
  selectedIds = /* @__PURE__ */ new Set();
3041
3161
  popover = null;
3042
3162
  timer = null;
@@ -3048,8 +3168,13 @@ var Calendar = class {
3048
3168
  this.resizeObs?.disconnect();
3049
3169
  const RO = this.win.ResizeObserver;
3050
3170
  if (RO) {
3051
- this.resizeObs = new RO(() => this.current?.resize?.());
3171
+ this.resizeObs = new RO(() => {
3172
+ this.syncToolbarViewDisplay();
3173
+ this.current?.resize?.();
3174
+ this.positionMore();
3175
+ });
3052
3176
  this.resizeObs.observe(this.root);
3177
+ this.resizeObs.observe(this.toolbarEl);
3053
3178
  }
3054
3179
  }
3055
3180
  onAdoptCheck = () => {
@@ -3068,9 +3193,11 @@ var Calendar = class {
3068
3193
  if (this.frame !== null) old.cancelAnimationFrame(this.frame);
3069
3194
  this.timer = this.frame = null;
3070
3195
  this.doc.removeEventListener("pointerdown", this.onDocPointerDown, true);
3196
+ this.listenMoreGeometry(false);
3071
3197
  this.doc = doc;
3072
3198
  this.win = doc.defaultView ?? old;
3073
3199
  this.doc.addEventListener("pointerdown", this.onDocPointerDown, true);
3200
+ this.listenMoreGeometry(true);
3074
3201
  this.observeResize();
3075
3202
  this.rebuild(false);
3076
3203
  this.scheduleTick();
@@ -3174,7 +3301,12 @@ var Calendar = class {
3174
3301
  gotoDate(date) {
3175
3302
  const day = this.toDay(date);
3176
3303
  if (day === null) return;
3304
+ const sameMonth = this.opts.view === "month" && startOfMonth(day) === startOfMonth(this.anchor);
3177
3305
  this.anchor = day;
3306
+ if (sameMonth) {
3307
+ this.view = { ...this.view, anchor: day };
3308
+ return;
3309
+ }
3178
3310
  this.rebuild(true);
3179
3311
  }
3180
3312
  prev() {
@@ -3188,6 +3320,7 @@ var Calendar = class {
3188
3320
  goToday() {
3189
3321
  this.anchor = this.todayDay();
3190
3322
  this.rebuild(true);
3323
+ if (this.current instanceof TimeGridView) this.current.revealDay(this.anchor);
3191
3324
  }
3192
3325
  /** The anchor date, as local midnight of that civil day in the calendar zone. */
3193
3326
  getDate() {
@@ -3206,6 +3339,7 @@ var Calendar = class {
3206
3339
  /** Re-measures (e.g. after the host was shown or resized by a parent). */
3207
3340
  updateSize() {
3208
3341
  this.current?.resize?.();
3342
+ this.positionMore();
3209
3343
  }
3210
3344
  getScroll() {
3211
3345
  return this.current?.getScroll() ?? { top: 0, left: 0 };
@@ -3237,7 +3371,10 @@ var Calendar = class {
3237
3371
  }
3238
3372
  const view = this.current;
3239
3373
  if (view instanceof TimelineView) view.scrollToInstant(ms, opts.align ?? "center", opts.smooth ?? false);
3240
- else if (view instanceof TimeGridView && p.kind !== "day") view.scrollToTime(wallTime(ms, this.zone).minute);
3374
+ else if (view instanceof TimeGridView) {
3375
+ view.revealDay(day);
3376
+ if (p.kind !== "day") view.scrollToTime(wallTime(ms, this.zone).minute);
3377
+ }
3241
3378
  }
3242
3379
  /** What is under a client point: a time slot, a day, or a timeline row and date. Null outside the view. */
3243
3380
  hitTest(clientX, clientY) {
@@ -3315,7 +3452,8 @@ var Calendar = class {
3315
3452
  out.push(...classList(this.opts.dayClassNames?.(this.dayArg(day))));
3316
3453
  return out;
3317
3454
  }
3318
- isEditable(n, what) {
3455
+ isEditable(n, what, presentation = false) {
3456
+ if (!presentation && this.itemWriteStates.get(n.id) === "pending") return false;
3319
3457
  if (n.id === EXTERNAL_ID || n.item.preview || this.opts.view === "year") return false;
3320
3458
  if (this.opts.view === "list" && (!this.opts.listAgenda || what === "duration")) return false;
3321
3459
  const it = n.item;
@@ -3355,13 +3493,14 @@ var Calendar = class {
3355
3493
  if (preview || n.id === EXTERNAL_ID) el.classList.add("is-mirror");
3356
3494
  if (this.conflicts.has(n.id)) el.classList.add("is-conflict");
3357
3495
  const indicator = variant === "indicator";
3358
- if (!preview && !indicator && (this.isEditable(n, "start") || this.isEditable(n, "duration"))) el.classList.add("is-draggable");
3496
+ if (!preview && !indicator && (this.isEditable(n, "start", true) || this.isEditable(n, "duration", true))) el.classList.add("is-draggable");
3359
3497
  if (this.draggingId === n.id) el.classList.add("is-dragging");
3360
3498
  if (this.selectedIds.has(n.id) && !preview) el.classList.add("is-selected");
3361
3499
  el.classList.add(...classList(it.className));
3362
3500
  const arg = this.itemArg(el, meta);
3363
3501
  el.classList.add(...classList(this.opts.itemClassNames?.(arg)));
3364
3502
  el.dataset.id = n.id;
3503
+ this.applyItemWriteState(el, n.id);
3365
3504
  if (it.kind) el.dataset.kind = it.kind;
3366
3505
  const color = it.color ?? it.borderColor;
3367
3506
  if (color) el.style.setProperty("--tnc-c", color);
@@ -3378,6 +3517,7 @@ var Calendar = class {
3378
3517
  const custom = variant === "list" ? this.opts.listItemContent?.(arg) : null;
3379
3518
  const content = indicator ? null : custom ?? this.opts.itemContent?.(arg);
3380
3519
  if (indicator) {
3520
+ if (this.opts.monthDisplay === "chips") el.append(h(this.doc, "span", "tnc-item-title", null, it.title));
3381
3521
  } else if (content != null) {
3382
3522
  const wrap = h(this.doc, "div", "tnc-item-main");
3383
3523
  wrap.append(content);
@@ -3390,6 +3530,7 @@ var Calendar = class {
3390
3530
  return el;
3391
3531
  }
3392
3532
  refreshItemEl(el, meta) {
3533
+ this.applyItemWriteState(el, meta.n.id);
3393
3534
  const time = el.querySelector(":scope .tnc-item-time");
3394
3535
  if (time) this.fillTime(time, meta.n, meta.variant, meta.day);
3395
3536
  el.classList.toggle("is-dragging", this.draggingId === meta.n.id);
@@ -3412,9 +3553,10 @@ var Calendar = class {
3412
3553
  if (meta) this.opts.itemWillUnmount?.(this.itemArg(el, meta));
3413
3554
  el.remove();
3414
3555
  }
3415
- openMore(day, anchor, jsEvent) {
3556
+ openMore(day, anchor, jsEvent, itemIds) {
3416
3557
  jsEvent.stopPropagation();
3417
- 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));
3558
+ const group = itemIds ? new Set(itemIds) : null;
3559
+ 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));
3418
3560
  const shown = new Set(
3419
3561
  [...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 ?? "")
3420
3562
  );
@@ -3443,15 +3585,7 @@ var Calendar = class {
3443
3585
  }
3444
3586
  pool.end();
3445
3587
  this.popover = { el, pool, anchor, day };
3446
- const rr = this.root.getBoundingClientRect();
3447
- const ar = anchor.getBoundingClientRect();
3448
- const w = Math.max(220, ar.width + 24);
3449
- let left = ar.left - rr.left - 12;
3450
- left = Math.max(4, Math.min(left, rr.width - w - 4));
3451
- let top = ar.top - rr.top - 40;
3452
- const height = el.offsetHeight;
3453
- top = Math.max(4, Math.min(top, rr.height - height - 4));
3454
- setStyle(el, { left, top, width: w });
3588
+ this.positionMore();
3455
3589
  el.focus();
3456
3590
  }
3457
3591
  /* =============================================================== render */
@@ -3575,8 +3709,11 @@ var Calendar = class {
3575
3709
  this.current.setScroll({ top: keep.top, left: 0 });
3576
3710
  this.current.placeInstant(keep.ms, keep.vx);
3577
3711
  } else if (scroll) this.current.setScroll(scroll);
3578
- else if (this.current instanceof TimeGridView) this.current.scrollToTime(this.slot.scroll);
3579
- else if (this.current instanceof TimelineView) this.current.scrollToDay(this.anchor);
3712
+ else if (this.current instanceof TimeGridView) {
3713
+ this.current.scrollToTime(this.slot.scroll);
3714
+ this.current.revealDay(this.anchor);
3715
+ } else if (this.current instanceof TimelineView) this.current.scrollToDay(this.anchor);
3716
+ else if (this.current instanceof YearView) this.current.revealDay(this.anchor);
3580
3717
  if (rangeChanged) this.opts.onRangeChange?.(this.view);
3581
3718
  }
3582
3719
  /** Sets the visible days and view info from the anchor. */
@@ -3602,9 +3739,11 @@ var Calendar = class {
3602
3739
  }
3603
3740
  /** The range, run on to the end of the strip's days so their dots are right. */
3604
3741
  dataRangeFor(range) {
3605
- const end = this.strip ? Math.max(range.endDay, this.strip.window().end) : range.endDay;
3606
- if (end === range.endDay) return range;
3607
- return { ...range, endDay: end, end: new Date(toInstant(end, 0, this.zone)) };
3742
+ const window = this.strip?.window();
3743
+ const start = window ? Math.min(range.startDay, window.start) : range.startDay;
3744
+ const end = window ? Math.max(range.endDay, window.end) : range.endDay;
3745
+ if (start === range.startDay && end === range.endDay) return range;
3746
+ return { ...range, startDay: start, start: new Date(toInstant(start, 0, this.zone)), endDay: end, end: new Date(toInstant(end, 0, this.zone)) };
3608
3747
  }
3609
3748
  /** Adds, updates or removes the date strip for the current view. */
3610
3749
  syncStrip(navigated) {
@@ -3651,20 +3790,24 @@ var Calendar = class {
3651
3790
  stripShown() {
3652
3791
  if (!this.strip || this.rebuilding || !this.view) return;
3653
3792
  const next = this.dataRangeFor(this.view.range);
3654
- if (next.endDay === this.view.dataRange.endDay) return;
3793
+ if (next.startDay === this.view.dataRange.startDay && next.endDay === this.view.dataRange.endDay) return;
3655
3794
  this.view = { ...this.view, dataRange: next };
3656
3795
  this.opts.onRangeChange?.(this.view);
3657
3796
  }
3658
3797
  /** What is under a point, the date strip included. */
3659
3798
  hitAt(x, y) {
3799
+ const target = this.doc.elementFromPoint(x, y);
3800
+ if (!target || !this.root.contains(target)) {
3801
+ this.strip?.markTarget(null);
3802
+ return null;
3803
+ }
3660
3804
  const strip = this.strip;
3661
3805
  if (strip) {
3662
- const el = this.doc.elementFromPoint(x, y);
3663
- const day = el ? strip.dayAt(el) : null;
3806
+ const day = strip.dayAt(target);
3664
3807
  strip.markTarget(day !== null && this.draggingId ? day : null);
3665
3808
  if (day !== null) return { kind: "day", day };
3666
3809
  }
3667
- return this.current?.hit(x, y) ?? null;
3810
+ return this.current?.el.contains(target) ? this.current.hit(x, y) : null;
3668
3811
  }
3669
3812
  agendaList() {
3670
3813
  return this.opts.view === "list" && this.opts.listAgenda && this.current instanceof ListView ? this.current : null;
@@ -3747,17 +3890,44 @@ var Calendar = class {
3747
3890
  );
3748
3891
  const title = h(this.doc, "h2", "tnc-title", { "aria-live": "polite" }, this.view.title);
3749
3892
  const viewGroup = h(this.doc, "div", "tnc-toolbar-group tnc-toolbar-views", { role: "group" });
3893
+ const viewSelect = h(this.doc, "select", "tnc-btn tnc-toolbar-view-select", {
3894
+ "aria-label": this.text.viewHints[this.opts.view] ?? this.text.views[this.opts.view] ?? this.opts.view
3895
+ });
3750
3896
  for (const v of views) {
3751
3897
  const label = v === "days" ? this.text.views.days ?? `${this.opts.dayCount} days` : this.text.views[v] ?? v;
3752
3898
  viewGroup.append(btn(label, this.text.viewHints[v] ?? label, `tnc-btn-view tnc-btn-view-${v}`, () => this.changeView(v), v === this.opts.view));
3899
+ viewSelect.append(h(this.doc, "option", "", { value: v }, label));
3753
3900
  }
3901
+ viewSelect.value = this.opts.view;
3902
+ viewSelect.addEventListener("change", () => this.changeView(viewSelect.value));
3754
3903
  const start = h(this.doc, "div", "tnc-toolbar-start");
3755
3904
  const end = h(this.doc, "div", "tnc-toolbar-end");
3756
3905
  if (cfg.extraPlacement === "start") start.append(...extras);
3757
3906
  start.append(nav, title);
3758
- end.append(viewGroup);
3907
+ end.append(viewGroup, viewSelect);
3759
3908
  if (cfg.extraPlacement !== "start") end.append(...extras);
3760
3909
  this.toolbarEl.append(start, end);
3910
+ this.syncToolbarViewDisplay();
3911
+ }
3912
+ syncToolbarViewDisplay() {
3913
+ const buttons = this.toolbarEl.querySelector(".tnc-toolbar-views");
3914
+ const select = this.toolbarEl.querySelector(".tnc-toolbar-view-select");
3915
+ const start = this.toolbarEl.querySelector(".tnc-toolbar-start");
3916
+ if (!buttons || !select || !start) return;
3917
+ const config = this.opts.toolbar;
3918
+ const mode = typeof config === "object" ? config.viewDisplay ?? "auto" : "auto";
3919
+ buttons.hidden = false;
3920
+ select.hidden = true;
3921
+ buttons.style.display = "flex";
3922
+ select.style.display = "none";
3923
+ const startWidth = Array.from(start.children).reduce((width, child) => width + child.scrollWidth, 0);
3924
+ const availableWidth = Math.min(this.root.clientWidth, this.toolbarEl.clientWidth);
3925
+ const compact = mode === "select" || mode === "auto" && startWidth + buttons.scrollWidth + 32 > availableWidth;
3926
+ buttons.hidden = compact;
3927
+ select.hidden = !compact;
3928
+ buttons.style.display = compact ? "none" : "flex";
3929
+ select.style.display = compact ? "" : "none";
3930
+ select.parentElement?.classList.toggle("tnc-toolbar-end--compact", compact);
3761
3931
  }
3762
3932
  applyChange(change) {
3763
3933
  if (!change.added.length && !change.updated.length && !change.removed.length && !change.days.size && !change.spans) return;
@@ -3822,7 +3992,7 @@ var Calendar = class {
3822
3992
  const s = wallTime(n.startMs, this.zone);
3823
3993
  const start = this.fmt.time(s.minute, true);
3824
3994
  if (variant === "dot" || variant === "bar" || variant === "indicator") return { start: s.day === day ? start : "", end: "" };
3825
- if (n.startMs === n.endMs) return { start, end: "" };
3995
+ if (n.startMs === n.endMs || n.item.timeDisplay === "start") return { start, end: "" };
3826
3996
  return { start, end: this.fmt.time(wallTime(n.endMs, this.zone).minute, true) };
3827
3997
  }
3828
3998
  fillTime(el, n, variant, day) {
@@ -3838,7 +4008,8 @@ var Calendar = class {
3838
4008
  }
3839
4009
  const s = wallTime(n.startMs, this.zone);
3840
4010
  const e = wallTime(n.endMs, this.zone);
3841
- return `${n.item.title}, ${this.fmt.longDate(s.day)} ${this.fmt.time(s.minute)} \u2013 ${this.fmt.time(e.minute)}`;
4011
+ const start = `${n.item.title}, ${this.fmt.longDate(s.day)} ${this.fmt.time(s.minute)}`;
4012
+ return n.item.timeDisplay === "start" || n.startMs === n.endMs ? start : `${start} \u2013 ${this.fmt.time(e.minute)}`;
3842
4013
  }
3843
4014
  defaultContent(meta) {
3844
4015
  const { n, variant, day } = meta;
@@ -3866,7 +4037,7 @@ var Calendar = class {
3866
4037
  addHandles(el, meta) {
3867
4038
  const { n, variant } = meta;
3868
4039
  const ports = variant === "timeline" && !meta.mirror && n.id !== EXTERNAL_ID && !!this.opts.timeline?.onLink;
3869
- if (variant === "dot" || !this.isEditable(n, "duration")) {
4040
+ if (variant === "dot" || !this.isEditable(n, "duration", true)) {
3870
4041
  if (ports) this.addPorts(el, n);
3871
4042
  return;
3872
4043
  }
@@ -3890,6 +4061,38 @@ var Calendar = class {
3890
4061
  }
3891
4062
  }
3892
4063
  /* ============================================================== popover */
4064
+ onMoreGeometryChange = () => this.positionMore();
4065
+ listenMoreGeometry(on) {
4066
+ const method = on ? "addEventListener" : "removeEventListener";
4067
+ this.doc[method]("scroll", this.onMoreGeometryChange, true);
4068
+ this.win[method]("resize", this.onMoreGeometryChange);
4069
+ this.win.visualViewport?.[method]("resize", this.onMoreGeometryChange);
4070
+ this.win.visualViewport?.[method]("scroll", this.onMoreGeometryChange);
4071
+ }
4072
+ positionMore() {
4073
+ const popover = this.popover;
4074
+ if (!popover) return;
4075
+ if (!popover.anchor.isConnected) {
4076
+ this.closeMore();
4077
+ return;
4078
+ }
4079
+ const viewport = this.win.visualViewport;
4080
+ const root = this.root.getBoundingClientRect();
4081
+ const anchor = popover.anchor.getBoundingClientRect();
4082
+ const leftEdge = Math.max(root.left, viewport?.offsetLeft ?? 0) + 4;
4083
+ const topEdge = Math.max(root.top, viewport?.offsetTop ?? 0) + 4;
4084
+ const rightEdge = Math.min(root.right, (viewport?.offsetLeft ?? 0) + (viewport?.width ?? this.win.innerWidth)) - 4;
4085
+ const bottomEdge = Math.min(root.bottom, (viewport?.offsetTop ?? 0) + (viewport?.height ?? this.win.innerHeight)) - 4;
4086
+ if (rightEdge <= leftEdge || bottomEdge <= topEdge) {
4087
+ this.closeMore();
4088
+ return;
4089
+ }
4090
+ const width = Math.min(360, rightEdge - leftEdge);
4091
+ setStyle(popover.el, { width, "max-height": Math.min(420, bottomEdge - topEdge) });
4092
+ const left = Math.max(leftEdge, Math.min(anchor.left - 12, rightEdge - width));
4093
+ const top = Math.max(topEdge, Math.min(anchor.top - 40, bottomEdge - popover.el.offsetHeight));
4094
+ setStyle(popover.el, { left: left - root.left, top: top - root.top });
4095
+ }
3893
4096
  refreshMore() {
3894
4097
  const p = this.popover;
3895
4098
  if (!p) return;
@@ -3899,6 +4102,7 @@ var Calendar = class {
3899
4102
  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 });
3900
4103
  }
3901
4104
  p.pool.end();
4105
+ this.positionMore();
3902
4106
  }
3903
4107
  closeMore(restoreFocus = false) {
3904
4108
  const p = this.popover;
@@ -3926,6 +4130,7 @@ var Calendar = class {
3926
4130
  r.addEventListener("pointerover", this.onAdoptCheck);
3927
4131
  r.addEventListener("focusin", this.onAdoptCheck);
3928
4132
  this.doc.addEventListener("pointerdown", this.onDocPointerDown, true);
4133
+ this.listenMoreGeometry(true);
3929
4134
  }
3930
4135
  unbindEvents() {
3931
4136
  const r = this.root;
@@ -3944,6 +4149,7 @@ var Calendar = class {
3944
4149
  r.removeEventListener("pointerover", this.onAdoptCheck);
3945
4150
  r.removeEventListener("focusin", this.onAdoptCheck);
3946
4151
  this.doc.removeEventListener("pointerdown", this.onDocPointerDown, true);
4152
+ this.listenMoreGeometry(false);
3947
4153
  this.listenDoc(false);
3948
4154
  }
3949
4155
  listenDoc(on) {
@@ -3959,6 +4165,10 @@ var Calendar = class {
3959
4165
  return el && this.root.contains(el) && this.meta.has(el) && !this.meta.get(el)?.n.item.preview ? el : null;
3960
4166
  }
3961
4167
  onDocPointerDown = (ev) => {
4168
+ if (this.cancelledPointerId !== null) {
4169
+ this.cancelledPointerId = null;
4170
+ this.listenDoc(false);
4171
+ }
3962
4172
  const t = ev.target;
3963
4173
  if (this.popover && !this.popover.el.contains(t) && !this.popover.anchor.contains(t)) this.closeMore();
3964
4174
  if (this.selection && !this.root.contains(t)) this.unselect();
@@ -3966,6 +4176,7 @@ var Calendar = class {
3966
4176
  onPointerDown = (ev) => {
3967
4177
  if (ev.button !== 0 || this.session) return;
3968
4178
  const target = ev.target;
4179
+ if (target.closest(".tnc-dg--compact .tnc-dg-day")) return;
3969
4180
  if (target.closest("button, a, input, select, textarea, .tnc-toolbar, .tnc-port, [data-tnc-no-drag]")) {
3970
4181
  if (!target.closest(".tnc-item")) return;
3971
4182
  if (target.closest(".tnc-port, [data-tnc-no-drag], input, select, textarea")) return;
@@ -3989,6 +4200,7 @@ var Calendar = class {
3989
4200
  mode = "select";
3990
4201
  }
3991
4202
  const touch = ev.pointerType === "touch";
4203
+ const overflowOrigin = itemEl?.closest(".tnc-popover") && origin ? wallTime(origin.startMs, this.zone) : null;
3992
4204
  const s = {
3993
4205
  mode,
3994
4206
  pointerId: ev.pointerId,
@@ -3997,7 +4209,7 @@ var Calendar = class {
3997
4209
  y0: ev.clientY,
3998
4210
  active: false,
3999
4211
  origin,
4000
- grab: 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,
4001
4213
  proposal: null,
4002
4214
  denied: false,
4003
4215
  timer: null,
@@ -4022,16 +4234,79 @@ var Calendar = class {
4022
4234
  };
4023
4235
  activate(s) {
4024
4236
  s.active = true;
4237
+ if (s.lastEvent) this.leaveHover(s.lastEvent);
4025
4238
  if (s.timer !== null) this.win.clearTimeout(s.timer);
4026
4239
  s.timer = null;
4027
4240
  if (s.origin) {
4028
4241
  this.draggingId = s.origin.id;
4029
- 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.opts.view !== "list" && s.mode === "move") {
4244
+ const source = el.cloneNode(true);
4245
+ source.classList.remove("is-selected");
4246
+ source.classList.add("tnc-drag-source");
4247
+ source.setAttribute("aria-hidden", "true");
4248
+ source.inert = true;
4249
+ for (const node of [source, ...source.querySelectorAll("[id], [data-id], [data-task-path]")]) {
4250
+ node.removeAttribute("id");
4251
+ node.removeAttribute("data-id");
4252
+ node.removeAttribute("data-task-path");
4253
+ }
4254
+ el.before(source);
4255
+ this.dragSources.push(source);
4256
+ }
4257
+ if (this.opts.view === "list" && !this.listDragPreview && s.mode === "move") {
4258
+ const bounds = el.getBoundingClientRect();
4259
+ const preview = el.cloneNode(true);
4260
+ preview.classList.add("tnc-drag-preview", "is-dragging");
4261
+ preview.removeAttribute("data-id");
4262
+ preview.removeAttribute("data-task-path");
4263
+ preview.setAttribute("aria-hidden", "true");
4264
+ preview.inert = true;
4265
+ preview.style.width = `${bounds.width}px`;
4266
+ this.root.append(preview);
4267
+ this.listDragPreview = { element: preview, offsetX: s.x0 - bounds.left, offsetY: s.y0 - bounds.top, width: bounds.width, height: bounds.height };
4268
+ }
4269
+ el.classList.add("is-dragging");
4270
+ }
4030
4271
  }
4031
4272
  this.closeMore();
4032
4273
  if (s.mode !== "select") this.unselectSilently();
4033
4274
  this.root.classList.add("tnc-dragging", `tnc-dragging-${s.mode}`);
4034
4275
  if (s.lastEvent) this.track(s, s.lastEvent);
4276
+ this.startDragScroll();
4277
+ }
4278
+ startDragScroll() {
4279
+ if (this.dragScrollFrame !== null) return;
4280
+ const tick = () => {
4281
+ this.dragScrollFrame = null;
4282
+ const session = this.session;
4283
+ const pointer = session?.lastEvent;
4284
+ if (!session?.active || !pointer) return;
4285
+ const origin = session.origin ? this.elementsFor(session.origin.id)[0] : this.viewHost;
4286
+ const underPointer = this.doc.elementFromPoint(pointer.clientX, pointer.clientY);
4287
+ let moved = false;
4288
+ if (origin && underPointer && this.root.contains(underPointer)) {
4289
+ for (const axis of ["x", "y"]) {
4290
+ const scroller = scrollerOf(origin, axis);
4291
+ if (!scroller) continue;
4292
+ const viewport = scroller === this.doc.scrollingElement ? { left: 0, top: 0, right: this.win.innerWidth, bottom: this.win.innerHeight } : scroller.getBoundingClientRect();
4293
+ const coordinate = axis === "x" ? pointer.clientX : pointer.clientY;
4294
+ const lower = Math.max(0, axis === "x" ? viewport.left : viewport.top);
4295
+ const upper = Math.min(axis === "x" ? this.win.innerWidth : this.win.innerHeight, axis === "x" ? viewport.right : viewport.bottom);
4296
+ if (coordinate < lower || coordinate > upper) continue;
4297
+ const delta = coordinate < lower + 32 ? -Math.min(12, (lower + 32 - coordinate) / 2) : coordinate > upper - 32 ? Math.min(12, (coordinate - upper + 32) / 2) : 0;
4298
+ if (!delta) continue;
4299
+ if (!this.dragScrollOrigins.has(scroller)) this.dragScrollOrigins.set(scroller, { top: scroller.scrollTop, left: scroller.scrollLeft });
4300
+ const before = axis === "x" ? scroller.scrollLeft : scroller.scrollTop;
4301
+ if (axis === "x") scroller.scrollLeft += delta;
4302
+ else scroller.scrollTop += delta;
4303
+ moved ||= before !== (axis === "x" ? scroller.scrollLeft : scroller.scrollTop);
4304
+ }
4305
+ }
4306
+ if (moved) this.track(session, pointer);
4307
+ this.dragScrollFrame = this.win.requestAnimationFrame(tick);
4308
+ };
4309
+ this.dragScrollFrame = this.win.requestAnimationFrame(tick);
4035
4310
  }
4036
4311
  onTouchMove = (ev) => {
4037
4312
  if (this.session?.active) ev.preventDefault();
@@ -4061,6 +4336,12 @@ var Calendar = class {
4061
4336
  this.track(s, ev);
4062
4337
  };
4063
4338
  onPointerUp = (ev) => {
4339
+ if (ev.pointerId === this.cancelledPointerId) {
4340
+ this.cancelledPointerId = null;
4341
+ this.suppressNextClick();
4342
+ this.listenDoc(false);
4343
+ return;
4344
+ }
4064
4345
  const s = this.session;
4065
4346
  if (!s || ev.pointerId !== s.pointerId) return;
4066
4347
  if (!s.active) {
@@ -4073,28 +4354,49 @@ var Calendar = class {
4073
4354
  return;
4074
4355
  }
4075
4356
  this.suppressNextClick();
4357
+ this.track(s, ev);
4076
4358
  this.endSession(ev, false);
4077
4359
  };
4078
4360
  onPointerCancel = (ev) => {
4361
+ if (ev.pointerId === this.cancelledPointerId) {
4362
+ this.cancelledPointerId = null;
4363
+ this.listenDoc(false);
4364
+ }
4079
4365
  if (this.session && ev.pointerId === this.session.pointerId) this.endSession(null, true);
4080
4366
  };
4081
4367
  onDocKeyDown = (ev) => {
4082
4368
  if (ev.key === "Escape" && this.session) {
4083
4369
  ev.preventDefault();
4084
4370
  ev.stopPropagation();
4371
+ const pointerId = this.session.active ? this.session.pointerId : null;
4085
4372
  this.endSession(null, true);
4373
+ if (pointerId !== null) {
4374
+ this.cancelledPointerId = pointerId;
4375
+ this.listenDoc(true);
4376
+ }
4086
4377
  }
4087
4378
  };
4088
4379
  /** Proposal for the current pointer position. */
4089
4380
  track(s, ev) {
4381
+ if (this.listDragPreview) {
4382
+ const preview = this.listDragPreview;
4383
+ const bounds = this.root.getBoundingClientRect();
4384
+ preview.element.style.left = `${Math.max(8, Math.min(this.win.innerWidth - preview.width - 8, ev.clientX - preview.offsetX)) - bounds.left}px`;
4385
+ preview.element.style.top = `${Math.max(8, Math.min(this.win.innerHeight - preview.height - 8, ev.clientY - preview.offsetY)) - bounds.top}px`;
4386
+ }
4090
4387
  const hit = this.hitAt(ev.clientX, ev.clientY);
4091
4388
  if (s.mode === "external") {
4092
4389
  this.trackExternal(s, hit);
4093
4390
  return;
4094
4391
  }
4095
4392
  if (!s.grab) s.grab = hit;
4096
- if (!hit || !s.grab) return;
4393
+ if (!hit || !s.grab) {
4394
+ this.invalidateProposal(s);
4395
+ return;
4396
+ }
4097
4397
  if (s.mode === "select") {
4398
+ s.denied = false;
4399
+ this.root.classList.remove("tnc-drop-denied");
4098
4400
  const sel = this.selectionFor(s.grab, hit);
4099
4401
  if (!sel) return;
4100
4402
  this.selection = sel;
@@ -4103,25 +4405,40 @@ var Calendar = class {
4103
4405
  }
4104
4406
  const origin = s.origin;
4105
4407
  const p = s.mode === "move" ? this.moveProposal(origin, s.grab, hit) : this.resizeProposal(origin, hit, s.mode === "resize-start");
4106
- if (!p) return;
4408
+ if (!p) {
4409
+ this.invalidateProposal(s);
4410
+ return;
4411
+ }
4107
4412
  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;
4108
4413
  s.denied = !allowed;
4109
4414
  this.root.classList.toggle("tnc-drop-denied", !allowed);
4110
- if (!allowed || sameProposal(p, s.proposal)) return;
4415
+ if (!allowed) {
4416
+ this.invalidateProposal(s);
4417
+ return;
4418
+ }
4419
+ if (sameProposal(p, s.proposal)) return;
4111
4420
  s.proposal = p;
4112
4421
  this.dragRow = p.rowId !== void 0 ? { id: origin.id, rowId: p.rowId } : null;
4113
- this.applyChange(this.store.upsert([this.withProposal(origin, p)]));
4422
+ if (this.opts.view !== "list") this.applyChange(this.store.upsert([this.withProposal(origin, p)]));
4423
+ }
4424
+ invalidateProposal(s) {
4425
+ const hadProposal = s.proposal !== null;
4426
+ s.proposal = null;
4427
+ s.hit = null;
4428
+ s.denied = true;
4429
+ this.dragRow = null;
4430
+ this.root.classList.add("tnc-drop-denied");
4431
+ this.strip?.markTarget(null);
4432
+ this.unselectSilently();
4433
+ if (s.mode === "external") this.applyChange(this.store.remove([EXTERNAL_ID]));
4434
+ else if (hadProposal && s.origin) this.applyChange(this.store.upsert([s.origin.item]));
4114
4435
  }
4115
4436
  trackExternal(s, raw) {
4116
4437
  const src = s.source;
4117
4438
  const hit = raw?.kind === "timeline" && raw.area !== "track" && raw.area !== "header" ? null : raw;
4118
4439
  s.hit = hit;
4119
4440
  if (!hit) {
4120
- if (s.proposal) {
4121
- s.proposal = null;
4122
- this.applyChange(this.store.remove([EXTERNAL_ID]));
4123
- }
4124
- this.unselectSilently();
4441
+ this.invalidateProposal(s);
4125
4442
  return;
4126
4443
  }
4127
4444
  const dur = src.durationMinutes ?? this.opts.defaultTimedMinutes;
@@ -4140,6 +4457,10 @@ var Calendar = class {
4140
4457
  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;
4141
4458
  s.denied = !allowed;
4142
4459
  this.root.classList.toggle("tnc-drop-denied", !allowed);
4460
+ if (!allowed) {
4461
+ this.invalidateProposal(s);
4462
+ return;
4463
+ }
4143
4464
  if (sameProposal(p, s.proposal)) return;
4144
4465
  s.proposal = p;
4145
4466
  if (hit.kind === "timeline" && p.rowId === void 0) {
@@ -4332,7 +4653,18 @@ var Calendar = class {
4332
4653
  endSession(ev, cancel) {
4333
4654
  const s = this.session;
4334
4655
  if (!s) return;
4656
+ if (this.dragScrollFrame !== null) this.win.cancelAnimationFrame(this.dragScrollFrame);
4657
+ this.dragScrollFrame = null;
4658
+ if (cancel || s.denied) for (const [scroller, position] of this.dragScrollOrigins) {
4659
+ scroller.scrollTop = position.top;
4660
+ scroller.scrollLeft = position.left;
4661
+ }
4662
+ this.dragScrollOrigins.clear();
4335
4663
  this.session = null;
4664
+ this.listDragPreview?.element.remove();
4665
+ this.listDragPreview = null;
4666
+ for (const source of this.dragSources) source.remove();
4667
+ this.dragSources = [];
4336
4668
  if (s.timer !== null) this.win.clearTimeout(s.timer);
4337
4669
  this.listenDoc(false);
4338
4670
  this.root.classList.remove(
@@ -4401,10 +4733,16 @@ var Calendar = class {
4401
4733
  revert();
4402
4734
  return;
4403
4735
  }
4736
+ if (this.opts.view === "list") this.applyChange(this.store.upsert([this.withProposal(origin, p)]));
4404
4737
  this.flush();
4405
4738
  this.commit(s.mode === "move" ? "drop" : "resize", origin, p, ev, revert);
4406
4739
  }
4407
4740
  commit(kind, origin, p, ev, revert) {
4741
+ let reverted = false;
4742
+ const mark = (state) => {
4743
+ this.itemWriteStates.set(origin.id, state);
4744
+ for (const element of this.elementsFor(origin.id)) this.applyItemWriteState(element, origin.id);
4745
+ };
4408
4746
  const arg = {
4409
4747
  item: origin.item,
4410
4748
  start: new Date(p.startMs),
@@ -4420,19 +4758,32 @@ var Calendar = class {
4420
4758
  jsEvent: ev,
4421
4759
  source: ev && "key" in ev ? "keyboard" : "pointer",
4422
4760
  view: this.view,
4423
- revert
4761
+ revert: () => {
4762
+ reverted = true;
4763
+ revert();
4764
+ mark("failed");
4765
+ }
4424
4766
  };
4425
4767
  const handler = kind === "drop" ? this.opts.onItemDrop : this.opts.onItemResize;
4426
4768
  try {
4769
+ mark("pending");
4427
4770
  const res = handler?.(arg);
4428
4771
  if (res && typeof res.then === "function") {
4429
- Promise.resolve(res).catch(() => revert());
4430
- }
4772
+ void Promise.resolve(res).then(() => {
4773
+ if (!reverted) mark("changed");
4774
+ }, () => arg.revert());
4775
+ } else if (!reverted) mark("changed");
4431
4776
  } catch (err) {
4432
- revert();
4777
+ arg.revert();
4433
4778
  throw err;
4434
4779
  }
4435
4780
  }
4781
+ applyItemWriteState(element, id) {
4782
+ const state = this.itemWriteStates.get(id);
4783
+ for (const name of ["pending", "failed", "changed"]) element.classList.toggle(`is-write-${name}`, state === name);
4784
+ if (state === "pending") element.setAttribute("aria-busy", "true");
4785
+ else element.removeAttribute("aria-busy");
4786
+ }
4436
4787
  /* --------------------------------------------------------- click / keys */
4437
4788
  /** Runs in the capture phase so the click ending a drag never reaches content inside the calendar, React portals included. */
4438
4789
  onClickCapture = (ev) => {
@@ -4442,6 +4793,12 @@ var Calendar = class {
4442
4793
  ev.preventDefault();
4443
4794
  };
4444
4795
  onClick = (ev) => {
4796
+ const compactDay = ev.target.closest?.(".tnc-dg--compact .tnc-dg-day");
4797
+ if (compactDay && this.root.contains(compactDay) && !ev.target.closest(".tnc-navlink")) {
4798
+ const day = parseDay(compactDay.dataset.date ?? "");
4799
+ if (day !== null) this.openMore(day, compactDay, ev);
4800
+ return;
4801
+ }
4445
4802
  const yearDay = ev.target.closest?.(".tnc-ym-day");
4446
4803
  if (yearDay && this.root.contains(yearDay)) {
4447
4804
  const day = parseDay(yearDay.dataset.date ?? "");
@@ -4500,6 +4857,25 @@ var Calendar = class {
4500
4857
  if (el && meta) this.opts.onItemHover?.({ ...this.itemArg(el, meta), jsEvent: ev, entering: false });
4501
4858
  }
4502
4859
  onKeyDown = (ev) => {
4860
+ if (this.popover && !ev.altKey && !ev.ctrlKey && !ev.metaKey && ["ArrowDown", "ArrowUp", "Home", "End"].includes(ev.key)) {
4861
+ const rows = [...this.popover.el.querySelectorAll(".tnc-item")];
4862
+ const index = rows.indexOf(this.doc.activeElement);
4863
+ 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;
4864
+ ev.preventDefault();
4865
+ ev.stopPropagation();
4866
+ rows[next]?.focus();
4867
+ rows[next]?.scrollIntoView({ block: "nearest", inline: "nearest" });
4868
+ return;
4869
+ }
4870
+ if (ev.key === "Tab" && this.popover) {
4871
+ const controls = [...this.popover.el.querySelectorAll('button, a[href], [tabindex="0"]')];
4872
+ const index = controls.indexOf(this.doc.activeElement);
4873
+ const next = ev.shiftKey ? index <= 0 ? controls.length - 1 : index - 1 : (index + 1) % controls.length;
4874
+ ev.preventDefault();
4875
+ ev.stopPropagation();
4876
+ controls[next]?.focus();
4877
+ return;
4878
+ }
4503
4879
  if (ev.key === "Escape" && this.popover) {
4504
4880
  ev.preventDefault();
4505
4881
  ev.stopPropagation();
@@ -4511,7 +4887,8 @@ var Calendar = class {
4511
4887
  const day = parseDay(target.dataset.date ?? "");
4512
4888
  if (day !== null) {
4513
4889
  ev.preventDefault();
4514
- this.clickSlot({ kind: "day", day }, ev, true);
4890
+ if (target.closest(".tnc-dg--compact")) this.openMore(day, target, ev);
4891
+ else this.clickSlot({ kind: "day", day }, ev, true);
4515
4892
  }
4516
4893
  return;
4517
4894
  }
@@ -4599,9 +4976,13 @@ var Calendar = class {
4599
4976
  const dt = ev.dataTransfer;
4600
4977
  if (!dt || !this.opts.acceptNativeDrop?.(dt) || !this.current) return;
4601
4978
  ev.preventDefault();
4979
+ const hit = this.hitAt(ev.clientX, ev.clientY);
4980
+ if (!hit || hit.kind === "timeline" && hit.area !== "track" && hit.area !== "header") {
4981
+ dt.dropEffect = "none";
4982
+ this.unselectSilently();
4983
+ return;
4984
+ }
4602
4985
  dt.dropEffect = "copy";
4603
- const hit = this.current.hit(ev.clientX, ev.clientY);
4604
- if (!hit || hit.kind === "timeline" && hit.area !== "track" && hit.area !== "header") return;
4605
4986
  const sel = this.selectionFor(hit, hit);
4606
4987
  if (!sel) return;
4607
4988
  this.selection = sel;
@@ -4615,7 +4996,7 @@ var Calendar = class {
4615
4996
  const dt = ev.dataTransfer;
4616
4997
  if (!dt || !this.opts.acceptNativeDrop?.(dt) || !this.current) return;
4617
4998
  ev.preventDefault();
4618
- const hit = this.current.hit(ev.clientX, ev.clientY);
4999
+ const hit = this.hitAt(ev.clientX, ev.clientY);
4619
5000
  this.unselectSilently();
4620
5001
  if (!hit || hit.kind === "timeline" && hit.area !== "track" && hit.area !== "header") return;
4621
5002
  const sel = this.selectionFor(hit, hit);
@@ -4646,7 +5027,7 @@ function scrollerOf(el, axis) {
4646
5027
  return doc.scrollingElement;
4647
5028
  }
4648
5029
  function rangeMoved(prev, next) {
4649
- return !prev || prev.type !== next.type || prev.range.startDay !== next.range.startDay || prev.range.endDay !== next.range.endDay || prev.dataRange.endDay !== next.dataRange.endDay;
5030
+ 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;
4650
5031
  }
4651
5032
 
4652
5033
  // src/tasknotes/bases.ts