@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.
@@ -152,6 +152,7 @@ var DEFAULT_TEXT = {
152
152
  allDay: "All day",
153
153
  noItems: "Nothing scheduled",
154
154
  more: (n) => `+${n} more`,
155
+ appointments: (n) => `${n} appointments`,
155
156
  itemCount: (n) => n === 1 ? "1 item" : `${n} items`,
156
157
  weekNumber: (n) => `W${n}`,
157
158
  views: { week: "Week", day: "Day", days: "Days", month: "Month", list: "List", year: "Year", timeline: "Timeline" },
@@ -221,6 +222,7 @@ var LAYOUT_KEYS = /* @__PURE__ */ new Set([
221
222
  "dateStrip",
222
223
  "listLeadTitle",
223
224
  "listAddText",
225
+ "listEmptyContent",
224
226
  "monthDisplay",
225
227
  "timeZone",
226
228
  "locale",
@@ -357,8 +359,9 @@ var DayGridView = class {
357
359
  constructor(ctx) {
358
360
  this.ctx = ctx;
359
361
  const { doc, opts, days } = ctx;
360
- this.dotsMode = opts.monthDisplay === "dots";
362
+ this.dotsMode = opts.monthDisplay === "dots" || opts.monthDisplay === "chips";
361
363
  this.el = h(doc, "div", this.dotsMode ? "tnc-dg tnc-dg--dots" : "tnc-dg");
364
+ this.el.classList.toggle("tnc-dg--chips", opts.monthDisplay === "chips");
362
365
  this.scroller = h(doc, "div", "tnc-dg-scroll");
363
366
  this.grid = h(doc, "div", "tnc-dg-grid", { role: "grid", "aria-label": ctx.view.title });
364
367
  const perWeek = this.daysPerWeek();
@@ -388,14 +391,14 @@ var DayGridView = class {
388
391
  const cell = h(doc, "div", cls.join(" "), { role: "gridcell", "aria-label": ctx.fmt.longDate(d), "data-date": arg.dateStr });
389
392
  if (this.dotsMode) cell.tabIndex = 0;
390
393
  const num = ctx.fmt.dayNumber(d);
391
- const label = ymdOfDay(d).day === 1 ? `${ctx.fmt.monthShort(d)} ${num}` : num;
394
+ const label = !this.dotsMode && ymdOfDay(d).day === 1 ? `${ctx.fmt.monthShort(d)} ${num}` : num;
392
395
  cell.append(
393
396
  h(
394
397
  doc,
395
398
  "div",
396
399
  "tnc-dg-top",
397
400
  null,
398
- opts.navLinks ? ctx.navLink(d, label, "tnc-dg-num") : h(doc, "span", "tnc-dg-num", null, label)
401
+ opts.navLinks && !this.dotsMode ? ctx.navLink(d, label, "tnc-dg-num") : h(doc, "span", "tnc-dg-num", null, label)
399
402
  )
400
403
  );
401
404
  dates.append(cell);
@@ -409,20 +412,20 @@ var DayGridView = class {
409
412
  layer.append(content);
410
413
  row.append(dates, layer);
411
414
  this.grid.append(row);
412
- const dots = this.dotsMode ? cells.map((cell) => {
415
+ const dots = cells.map((cell) => {
413
416
  const box = h(doc, "div", "tnc-dg-dots", { "aria-hidden": "true" });
414
417
  if (opts.dayMaxEvents === false) box.classList.add("is-unlimited");
415
418
  const count = h(doc, "span", "tnc-dg-dots-more");
416
419
  cell.append(box);
417
420
  return { box, pool: new ItemPool(ctx, box), count };
418
- }) : null;
421
+ });
419
422
  this.weeks.push({ days: wdays, row, cells, layer, sel, more, pool: new ItemPool(ctx, content), dots });
420
423
  }
421
424
  this.scroller.append(this.grid);
422
425
  this.el.append(this.scroller);
423
426
  this.el.classList.toggle("tnc-dg--auto", opts.height === "auto");
424
427
  const RO = ctx.win.ResizeObserver;
425
- if (RO && (opts.dayMaxEvents === true || this.dotsMode && opts.dayMaxEvents !== false)) {
428
+ if (RO) {
426
429
  this.resizeObs = new RO(() => {
427
430
  if (this.sizeKey() !== this.lastHeights) this.render(null);
428
431
  });
@@ -453,14 +456,35 @@ var DayGridView = class {
453
456
  }
454
457
  render(_change) {
455
458
  const { ctx } = this;
459
+ this.dotsMode = ctx.opts.monthDisplay !== "full" || this.grid.clientWidth / this.daysPerWeek() < 72;
460
+ this.el.classList.toggle("tnc-dg--dots", this.dotsMode);
461
+ this.el.classList.toggle("tnc-dg--compact", this.dotsMode && ctx.opts.monthDisplay === "full");
462
+ this.el.classList.toggle("tnc-dg--chips", ctx.opts.monthDisplay === "chips" && this.weeks[0].dots[0].box.clientHeight >= 44);
456
463
  const cs = ctx.win.getComputedStyle(this.el);
457
464
  this.barPx = px(cs, "--tnc-bar-h", 20);
458
465
  this.topPx = this.weeks[0]?.cells[0]?.querySelector(".tnc-dg-top")?.offsetHeight || px(cs, "--tnc-dg-top-h", 24);
459
466
  this.morePx = this.measureMore(cs);
460
467
  this.lastHeights = this.sizeKey();
461
468
  for (const w of this.weeks) {
462
- if (w.dots) this.renderDots(w);
463
- else this.renderWeek(w);
469
+ for (const cell of w.cells) {
470
+ if (this.dotsMode) cell.tabIndex = 0;
471
+ else cell.removeAttribute("tabindex");
472
+ if (this.dotsMode && ctx.opts.monthDisplay === "full") cell.setAttribute("aria-haspopup", "dialog");
473
+ else cell.removeAttribute("aria-haspopup");
474
+ }
475
+ for (const cell of w.dots) cell.box.hidden = !this.dotsMode;
476
+ if (this.dotsMode) {
477
+ w.pool.begin();
478
+ w.pool.end();
479
+ w.more.replaceChildren();
480
+ this.renderDots(w);
481
+ } else {
482
+ for (const cell of w.dots) {
483
+ cell.pool.begin();
484
+ cell.pool.end();
485
+ }
486
+ this.renderWeek(w);
487
+ }
464
488
  }
465
489
  }
466
490
  /** Height of a "+N more" link as the host styles it (`--tnc-more-h`, or a taller min-height). */
@@ -546,19 +570,28 @@ var DayGridView = class {
546
570
  });
547
571
  }
548
572
  w.pool.end();
549
- w.more.replaceChildren();
573
+ const existing = new Map(Array.from(w.more.children, (element) => [element.getAttribute("data-day"), element]));
550
574
  hidden.forEach((items, col) => {
551
575
  if (!items.length) return;
552
576
  const day = w.days[col];
553
- const link = h(ctx.doc, "button", "tnc-more", { type: "button", "aria-haspopup": "dialog" }, ctx.text.more(items.length));
577
+ const key = String(day);
578
+ let link = existing.get(key);
579
+ if (!link) {
580
+ link = h(ctx.doc, "button", "tnc-more", { type: "button", "aria-haspopup": "dialog", "data-day": key });
581
+ const anchor = link;
582
+ link.addEventListener("click", (ev) => ctx.openMore(day, anchor, ev));
583
+ w.more.append(link);
584
+ }
585
+ existing.delete(key);
586
+ link.setAttribute("aria-label", ctx.text.more(items.length));
587
+ link.textContent = w.row.clientWidth / n < 72 ? `+${new Intl.NumberFormat(ctx.opts.locale).format(items.length)}` : ctx.text.more(items.length);
554
588
  setStyle(link, {
555
589
  left: `calc(${pct(col / n)} + 2px)`,
556
590
  width: `calc(${pct(1 / n)} - 4px)`,
557
591
  top: `calc(var(--tnc-dg-top-h) + ${visible(col)} * (var(--tnc-bar-h) + ${BAR_GAP}px))`
558
592
  });
559
- link.addEventListener("click", (ev) => ctx.openMore(day, link, ev));
560
- w.more.append(link);
561
593
  });
594
+ for (const link of existing.values()) link.remove();
562
595
  const step = this.barPx + BAR_GAP;
563
596
  let need = maxLevel >= 0 ? (maxLevel + 1) * step - BAR_GAP : 0;
564
597
  hidden.forEach((items, col) => {
@@ -581,10 +614,11 @@ var DayGridView = class {
581
614
  const { dayMaxEvents } = ctx.opts;
582
615
  const first = w.dots[0].box;
583
616
  const cs = ctx.win.getComputedStyle(first);
584
- const size = px(cs, "--tnc-dot-size", 8);
617
+ const chips = this.el.classList.contains("tnc-dg--chips");
618
+ const size = chips ? px(cs, "--tnc-chip-h", 14) : px(cs, "--tnc-dot-size", 8);
585
619
  const gap = px(cs, "--tnc-dot-gap", 3);
586
620
  const step = size + gap;
587
- const perRow = Math.max(1, Math.floor((first.clientWidth + gap) / step));
621
+ const perRow = chips ? 1 : Math.max(1, Math.floor((first.clientWidth + gap) / step));
588
622
  const rows = Math.max(1, Math.floor((first.clientHeight + gap) / step));
589
623
  const fits = dayMaxEvents === false ? Infinity : perRow * rows;
590
624
  const cap = typeof dayMaxEvents === "number" && dayMaxEvents > 0 ? Math.min(dayMaxEvents, fits) : fits;
@@ -593,7 +627,7 @@ var DayGridView = class {
593
627
  w.days.forEach((day, i) => {
594
628
  const cell = w.dots[i];
595
629
  const items = this.itemsOn(day);
596
- const shown = items.length <= cap ? items.length : cap >= fits ? Math.max(0, cap - 2) : cap;
630
+ const shown = items.length <= cap ? items.length : cap >= fits ? Math.max(1, cap - (chips ? 1 : 2)) : Math.max(1, cap);
597
631
  cell.pool.begin();
598
632
  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 }));
599
633
  cell.pool.end();
@@ -710,6 +744,7 @@ var ListView = class {
710
744
  const leadItems = [];
711
745
  const inLead = /* @__PURE__ */ new Set();
712
746
  const keep = /* @__PURE__ */ new Set();
747
+ let itemCount = 0;
713
748
  let prev = null;
714
749
  for (const day of days) {
715
750
  let items = this.itemsOn(day);
@@ -723,7 +758,7 @@ var ListView = class {
723
758
  return false;
724
759
  });
725
760
  }
726
- if (!items.length && !agenda) continue;
761
+ if (!items.length && !agenda && opts.listDayCount !== 1) continue;
727
762
  keep.add(day);
728
763
  let section = this.sections.get(day);
729
764
  let pool = this.pools.get(day);
@@ -742,6 +777,7 @@ var ListView = class {
742
777
  this.pools.set(day, pool);
743
778
  }
744
779
  section.classList.toggle("is-empty", !items.length);
780
+ itemCount += items.length;
745
781
  this.fill(pool, items, () => day);
746
782
  if (prev ? prev.nextSibling !== section : this.body.firstChild !== section) {
747
783
  if (prev) prev.after(section);
@@ -757,11 +793,14 @@ var ListView = class {
757
793
  this.sections.delete(day);
758
794
  }
759
795
  this.renderLead(leadItems);
760
- let empty = this.body.querySelector(":scope > .tnc-list-empty");
761
- if (!keep.size && !leadItems.length) {
796
+ let empty = this.body.querySelector(".tnc-list-empty");
797
+ if (!itemCount && !leadItems.length && (!agenda || opts.listEmptyContent)) {
762
798
  if (!empty) {
763
799
  empty = h(doc, "div", "tnc-list-empty", { role: "listitem" }, ctx.text.noItems);
764
- this.body.append(empty);
800
+ const content = ctx.opts.listEmptyContent?.();
801
+ if (content) empty.replaceChildren(content);
802
+ if (this.sections.size === 1) this.sections.values().next().value.append(empty);
803
+ else this.body.prepend(empty);
765
804
  }
766
805
  } else empty?.remove();
767
806
  this.ctx.strip?.draw();
@@ -819,7 +858,7 @@ var ListView = class {
819
858
  const section = day === this.ctx.days[0] && this.lead ? this.lead.section : this.sections.get(day);
820
859
  if (!section) return;
821
860
  this.ctx.strip?.measure();
822
- section.scrollIntoView({ block: "start" });
861
+ this.scroller.scrollTop += section.getBoundingClientRect().top - this.scroller.getBoundingClientRect().top;
823
862
  this.ctx.strip?.setFocus(day);
824
863
  this.pinned = true;
825
864
  }
@@ -908,8 +947,7 @@ var DateStrip = class {
908
947
  head.append(this.title, this.todayBtn, prev, next, this.slot);
909
948
  this.dows = h(doc, "div", "tnc-strip-dows", { "aria-hidden": "true" });
910
949
  this.grid = h(doc, "div", "tnc-strip-grid", { role: "group" });
911
- const handle = h(doc, "button", "tnc-strip-handle", { type: "button", tabindex: "-1", "aria-hidden": "true" });
912
- handle.addEventListener("click", () => this.setExpanded(!this.expanded));
950
+ const handle = h(doc, "div", "tnc-strip-handle", { "aria-hidden": "true" });
913
951
  this.el.append(head, this.dows, this.grid, handle);
914
952
  this.grid.addEventListener("click", this.onClick);
915
953
  this.grid.addEventListener("keydown", this.onKeyDown);
@@ -1109,6 +1147,8 @@ var TimeGridView = class {
1109
1147
  this.el = h(doc, "div", "tnc-tg", { role: "table", "aria-label": ctx.view.title });
1110
1148
  this.el.classList.toggle("tnc-tg--no-grid", !showGrid);
1111
1149
  this.scroller = h(doc, "div", "tnc-tg-scroll");
1150
+ this.dayNavigation = h(doc, "div", "tnc-tg-day-navigation", { role: "group", "aria-label": ctx.view.title });
1151
+ this.scroller.addEventListener("scroll", () => this.updateDayNavigation());
1112
1152
  this.top = h(doc, "div", "tnc-tg-top");
1113
1153
  this.headRow = h(doc, "div", "tnc-tg-head tnc-tg-cols", { role: "row" });
1114
1154
  this.top.append(this.headRow);
@@ -1136,7 +1176,7 @@ var TimeGridView = class {
1136
1176
  } else {
1137
1177
  this.body = this.gutter = null;
1138
1178
  }
1139
- this.el.append(this.scroller);
1179
+ this.el.append(this.dayNavigation, this.scroller);
1140
1180
  this.build();
1141
1181
  if (showGrid && opts.height === "fill") {
1142
1182
  const RO = ctx.win.ResizeObserver;
@@ -1149,6 +1189,7 @@ var TimeGridView = class {
1149
1189
  ctx;
1150
1190
  el;
1151
1191
  scroller;
1192
+ dayNavigation;
1152
1193
  top;
1153
1194
  headRow;
1154
1195
  allDayRow;
@@ -1204,6 +1245,9 @@ var TimeGridView = class {
1204
1245
  head.append(cap);
1205
1246
  }
1206
1247
  this.headRow.append(head);
1248
+ const dayButton = h(doc, "button", "tnc-btn", { type: "button", "aria-label": ctx.fmt.longDate(day) }, `${ctx.fmt.weekdayShort(day)} ${ctx.fmt.dayNumber(day)}`);
1249
+ dayButton.addEventListener("click", () => this.revealDay(day));
1250
+ this.dayNavigation.append(dayButton);
1207
1251
  opts.dayHeaderDidMount?.({ ...arg, el: head });
1208
1252
  const col = h(doc, "div", ["tnc-col", ...ctx.dayClasses(day)].join(" "), { role: "cell", "aria-label": ctx.fmt.longDate(day), "data-date": arg.dateStr });
1209
1253
  const bgLayer = h(doc, "div", "tnc-col-bg");
@@ -1220,6 +1264,7 @@ var TimeGridView = class {
1220
1264
  this.body?.append(col);
1221
1265
  return {
1222
1266
  day,
1267
+ collisionColumns: 1,
1223
1268
  head,
1224
1269
  col,
1225
1270
  plan: new ItemPool(ctx, planLayer),
@@ -1276,12 +1321,17 @@ var TimeGridView = class {
1276
1321
  /* --------------------------------------------------------------- render */
1277
1322
  render(change) {
1278
1323
  this.renderAllDay();
1279
- if (!this.body) return;
1324
+ if (!this.body) {
1325
+ this.updateDayNavigation();
1326
+ return;
1327
+ }
1280
1328
  const dirty = change ? change.days : null;
1281
1329
  for (const c of this.cols) {
1282
1330
  if (dirty && !dirty.has(c.day) && !(this.ctx.slot.max > MINUTES_PER_DAY && dirty.has(c.day + 1))) continue;
1283
1331
  this.renderColumn(c);
1284
1332
  }
1333
+ this.el.style.setProperty("--tnc-collision-columns", String(Math.max(1, ...this.cols.map((column) => column.collisionColumns))));
1334
+ this.updateDayNavigation();
1285
1335
  this.renderCapacity();
1286
1336
  this.tick();
1287
1337
  }
@@ -1399,6 +1449,11 @@ var TimeGridView = class {
1399
1449
  }
1400
1450
  const minMinutes = opts.eventMinHeight / this.ppm;
1401
1451
  const layout = layoutTimedDay(items, c.day, zone, minMinutes, ctx.order, [slot.min, slot.max]);
1452
+ c.collisionColumns = Math.max(
1453
+ 1,
1454
+ ...layout.plan.map((placement) => opts.eventMaxStack > 0 ? Math.min(placement.cols, opts.eventMaxStack) : placement.cols),
1455
+ ...layout.actual.filter((placement) => !placement.split).map((placement) => placement.cols)
1456
+ );
1402
1457
  const total = this.minutes;
1403
1458
  const top = (m) => pct((m - slot.min) / total);
1404
1459
  const height = (s) => pct((s.endMin - s.startMin) / total);
@@ -1410,14 +1465,22 @@ var TimeGridView = class {
1410
1465
  c.bg.end();
1411
1466
  const hasActual = layout.actual.length > 0;
1412
1467
  c.col.classList.toggle("has-actual", hasActual);
1468
+ for (const link of c.more.children) link.setAttribute("data-stale", "true");
1413
1469
  this.placeLane(c.plan, layout.plan, c, top, height, "plan");
1414
1470
  this.placeLane(c.actual, layout.actual, c, top, height, "actual");
1471
+ for (const link of [...c.more.children]) if (link.hasAttribute("data-stale")) link.remove();
1415
1472
  }
1416
1473
  placeLane(pool, placements, c, top, height, lane) {
1417
1474
  const { ctx } = this;
1418
1475
  const { opts } = ctx;
1419
- const withMore = lane === "plan";
1420
- const maxStack = opts.eventMaxStack;
1476
+ const readableWidth = Number.parseFloat(ctx.win.getComputedStyle(this.el).getPropertyValue("--tnc-event-min-width")) || 128;
1477
+ const laneWidth = c.col.clientWidth * (lane === "actual" && placements.some((placement) => placement.split) ? 0.22 : 1);
1478
+ const collapse = placements.some((placement) => {
1479
+ const columns = opts.eventMaxStack > 0 ? Math.min(placement.cols, opts.eventMaxStack) : placement.cols;
1480
+ return columns > 1 && columns * readableWidth > laneWidth;
1481
+ });
1482
+ const withMore = lane === "plan" || collapse;
1483
+ const maxStack = collapse ? 1 : opts.eventMaxStack;
1421
1484
  const hiddenByCluster = /* @__PURE__ */ new Map();
1422
1485
  pool.begin();
1423
1486
  for (const p of placements) {
@@ -1443,21 +1506,46 @@ var TimeGridView = class {
1443
1506
  top: top(p.startMin),
1444
1507
  height: `max(${height(p)}, ${opts.eventMinHeight}px)`,
1445
1508
  ...inLane(lane, p.split, left, width),
1509
+ "--tnc-event-readable-width": pct(span / cols / width),
1446
1510
  "z-index": String(p.col + 1)
1447
1511
  });
1448
1512
  }
1449
1513
  pool.end();
1450
1514
  if (!withMore) return;
1451
- c.more.replaceChildren();
1452
- for (const rec of hiddenByCluster.values()) {
1453
- const link = h(ctx.doc, "button", "tnc-more tnc-more--stack", { type: "button", "aria-haspopup": "dialog" }, `+${rec.count}`);
1515
+ for (const [cluster, rec] of hiddenByCluster) {
1516
+ const key = `${lane}-${cluster}`;
1517
+ let link = c.more.querySelector(`[data-cluster="${key}"]`);
1518
+ if (!link) {
1519
+ link = h(ctx.doc, "button", "tnc-more tnc-more--stack", { type: "button", "aria-haspopup": "dialog", "data-cluster": key });
1520
+ const anchor = link;
1521
+ link.addEventListener("click", (ev) => ctx.openMore(c.day, anchor, ev, JSON.parse(anchor.dataset.itemIds ?? "[]")));
1522
+ c.more.append(link);
1523
+ }
1524
+ link.removeAttribute("data-stale");
1525
+ link.dataset.itemIds = JSON.stringify(placements.filter((placement) => placement.cluster === cluster).map((placement) => placement.item.id));
1526
+ link.classList.toggle("tnc-more--collision", collapse);
1527
+ link.textContent = `+${rec.count}`;
1528
+ if (collapse) {
1529
+ const items = placements.filter((placement) => placement.cluster === cluster).map((placement) => placement.item.item.title);
1530
+ const count = ctx.text.appointments(rec.count);
1531
+ const interval = `${ctx.fmt.time(rec.start)} \u2013 ${ctx.fmt.time(rec.end)}`;
1532
+ link.replaceChildren(
1533
+ h(ctx.doc, "span", "tnc-collision-count", null, String(rec.count)),
1534
+ h(ctx.doc, "span", "tnc-collision-range", null, interval),
1535
+ h(ctx.doc, "span", "tnc-collision-title", { "aria-hidden": "true" }, items.join(" \xB7 "))
1536
+ );
1537
+ link.setAttribute("aria-label", `${count}, ${interval}: ${items.join(", ")}`);
1538
+ link.title = link.getAttribute("aria-label");
1539
+ } else {
1540
+ link.removeAttribute("aria-label");
1541
+ link.removeAttribute("title");
1542
+ }
1454
1543
  const cols = maxStack;
1455
1544
  setStyle(link, {
1456
1545
  top: top(rec.start),
1546
+ height: collapse ? pct((rec.end - rec.start) / this.minutes) : "",
1457
1547
  ...inLane(lane, rec.split, (cols - 1) / cols, 1 / cols)
1458
1548
  });
1459
- link.addEventListener("click", (ev) => ctx.openMore(c.day, link, ev));
1460
- c.more.append(link);
1461
1549
  }
1462
1550
  }
1463
1551
  meta(s, bg) {
@@ -1522,12 +1610,10 @@ var TimeGridView = class {
1522
1610
  const r2 = cells[i2].getBoundingClientRect();
1523
1611
  if (x >= r2.left && x < r2.right) return i2;
1524
1612
  }
1525
- const firstR = cells[0]?.getBoundingClientRect();
1526
- const lastR = cells[cells.length - 1]?.getBoundingClientRect();
1527
- if (firstR && x < firstR.left) return 0;
1528
- if (lastR && x >= lastR.right) return cells.length - 1;
1529
1613
  return -1;
1530
1614
  };
1615
+ const bounds = this.scroller.getBoundingClientRect();
1616
+ if (x < bounds.left || x >= bounds.right || y < bounds.top || y >= bounds.bottom) return null;
1531
1617
  if (this.allDayRow) {
1532
1618
  const r2 = this.allDayRow.getBoundingClientRect();
1533
1619
  if (y >= r2.top && y < r2.bottom || !this.body && y >= r2.top) {
@@ -1568,6 +1654,26 @@ var TimeGridView = class {
1568
1654
  }
1569
1655
  resize() {
1570
1656
  this.fitRows();
1657
+ this.render(null);
1658
+ }
1659
+ revealDay(day) {
1660
+ const column = this.cols.find((candidate) => candidate.day === day);
1661
+ if (!column) return;
1662
+ const bounds = column.head.getBoundingClientRect();
1663
+ const viewport = this.scroller.getBoundingClientRect();
1664
+ this.scroller.scrollLeft += bounds.left - viewport.left - (viewport.width - bounds.width) / 2;
1665
+ this.updateDayNavigation();
1666
+ }
1667
+ updateDayNavigation() {
1668
+ this.dayNavigation.hidden = this.scroller.scrollWidth <= this.scroller.clientWidth + 1;
1669
+ const viewport = this.scroller.getBoundingClientRect();
1670
+ this.cols.forEach((column, index) => {
1671
+ const bounds = column.head.getBoundingClientRect();
1672
+ const visible = bounds.left >= viewport.left && bounds.right <= viewport.right;
1673
+ const button = this.dayNavigation.children[index];
1674
+ button?.classList.toggle("is-offscreen", !visible);
1675
+ button?.setAttribute("aria-pressed", String(visible));
1676
+ });
1571
1677
  }
1572
1678
  scrollToTime(minute) {
1573
1679
  if (!this.body) return;
@@ -2230,6 +2336,14 @@ ${this.ctx.text.more(items.length - 8)}` : ""}` : label;
2230
2336
  setScroll(s) {
2231
2337
  this.scroller.scrollTop = s.top;
2232
2338
  }
2339
+ revealDay(day) {
2340
+ const month = this.cells.find((cell) => cell.day === day)?.el.closest(".tnc-ym");
2341
+ if (!month) return;
2342
+ const bounds = month.getBoundingClientRect();
2343
+ const viewport = this.scroller.getBoundingClientRect();
2344
+ if (bounds.top < viewport.top) this.scroller.scrollTop += bounds.top - viewport.top;
2345
+ else if (bounds.bottom > viewport.bottom) this.scroller.scrollTop += bounds.bottom - viewport.bottom;
2346
+ }
2233
2347
  elementsFor(_id) {
2234
2348
  return [];
2235
2349
  }
@@ -2309,6 +2423,11 @@ var Calendar = class {
2309
2423
  conflicts = /* @__PURE__ */ new Map();
2310
2424
  slot = { min: 0, max: MINUTES_PER_DAY, duration: 30, snap: 30, scroll: 480 };
2311
2425
  draggingId = null;
2426
+ listDragPreview = null;
2427
+ dragSources = [];
2428
+ dragScrollFrame = null;
2429
+ dragScrollOrigins = /* @__PURE__ */ new Map();
2430
+ itemWriteStates = /* @__PURE__ */ new Map();
2312
2431
  dragRow = null;
2313
2432
  strip = null;
2314
2433
  stripSize = null;
@@ -2322,6 +2441,7 @@ var Calendar = class {
2322
2441
  session = null;
2323
2442
  selection = null;
2324
2443
  suppressClick = false;
2444
+ cancelledPointerId = null;
2325
2445
  selectedIds = /* @__PURE__ */ new Set();
2326
2446
  popover = null;
2327
2447
  timer = null;
@@ -2333,8 +2453,13 @@ var Calendar = class {
2333
2453
  this.resizeObs?.disconnect();
2334
2454
  const RO = this.win.ResizeObserver;
2335
2455
  if (RO) {
2336
- this.resizeObs = new RO(() => this.current?.resize?.());
2456
+ this.resizeObs = new RO(() => {
2457
+ this.syncToolbarViewDisplay();
2458
+ this.current?.resize?.();
2459
+ this.positionMore();
2460
+ });
2337
2461
  this.resizeObs.observe(this.root);
2462
+ this.resizeObs.observe(this.toolbarEl);
2338
2463
  }
2339
2464
  }
2340
2465
  onAdoptCheck = () => {
@@ -2353,9 +2478,11 @@ var Calendar = class {
2353
2478
  if (this.frame !== null) old.cancelAnimationFrame(this.frame);
2354
2479
  this.timer = this.frame = null;
2355
2480
  this.doc.removeEventListener("pointerdown", this.onDocPointerDown, true);
2481
+ this.listenMoreGeometry(false);
2356
2482
  this.doc = doc;
2357
2483
  this.win = doc.defaultView ?? old;
2358
2484
  this.doc.addEventListener("pointerdown", this.onDocPointerDown, true);
2485
+ this.listenMoreGeometry(true);
2359
2486
  this.observeResize();
2360
2487
  this.rebuild(false);
2361
2488
  this.scheduleTick();
@@ -2459,7 +2586,12 @@ var Calendar = class {
2459
2586
  gotoDate(date) {
2460
2587
  const day = this.toDay(date);
2461
2588
  if (day === null) return;
2589
+ const sameMonth = this.opts.view === "month" && startOfMonth(day) === startOfMonth(this.anchor);
2462
2590
  this.anchor = day;
2591
+ if (sameMonth) {
2592
+ this.view = { ...this.view, anchor: day };
2593
+ return;
2594
+ }
2463
2595
  this.rebuild(true);
2464
2596
  }
2465
2597
  prev() {
@@ -2473,6 +2605,7 @@ var Calendar = class {
2473
2605
  goToday() {
2474
2606
  this.anchor = this.todayDay();
2475
2607
  this.rebuild(true);
2608
+ if (this.current instanceof TimeGridView) this.current.revealDay(this.anchor);
2476
2609
  }
2477
2610
  /** The anchor date, as local midnight of that civil day in the calendar zone. */
2478
2611
  getDate() {
@@ -2491,6 +2624,7 @@ var Calendar = class {
2491
2624
  /** Re-measures (e.g. after the host was shown or resized by a parent). */
2492
2625
  updateSize() {
2493
2626
  this.current?.resize?.();
2627
+ this.positionMore();
2494
2628
  }
2495
2629
  getScroll() {
2496
2630
  return this.current?.getScroll() ?? { top: 0, left: 0 };
@@ -2522,7 +2656,10 @@ var Calendar = class {
2522
2656
  }
2523
2657
  const view = this.current;
2524
2658
  if (view instanceof TimelineView) view.scrollToInstant(ms, opts.align ?? "center", opts.smooth ?? false);
2525
- else if (view instanceof TimeGridView && p.kind !== "day") view.scrollToTime(wallTime(ms, this.zone).minute);
2659
+ else if (view instanceof TimeGridView) {
2660
+ view.revealDay(day);
2661
+ if (p.kind !== "day") view.scrollToTime(wallTime(ms, this.zone).minute);
2662
+ }
2526
2663
  }
2527
2664
  /** What is under a client point: a time slot, a day, or a timeline row and date. Null outside the view. */
2528
2665
  hitTest(clientX, clientY) {
@@ -2600,7 +2737,8 @@ var Calendar = class {
2600
2737
  out.push(...classList(this.opts.dayClassNames?.(this.dayArg(day))));
2601
2738
  return out;
2602
2739
  }
2603
- isEditable(n, what) {
2740
+ isEditable(n, what, presentation = false) {
2741
+ if (!presentation && this.itemWriteStates.get(n.id) === "pending") return false;
2604
2742
  if (n.id === EXTERNAL_ID || n.item.preview || this.opts.view === "year") return false;
2605
2743
  if (this.opts.view === "list" && (!this.opts.listAgenda || what === "duration")) return false;
2606
2744
  const it = n.item;
@@ -2640,13 +2778,14 @@ var Calendar = class {
2640
2778
  if (preview || n.id === EXTERNAL_ID) el.classList.add("is-mirror");
2641
2779
  if (this.conflicts.has(n.id)) el.classList.add("is-conflict");
2642
2780
  const indicator = variant === "indicator";
2643
- if (!preview && !indicator && (this.isEditable(n, "start") || this.isEditable(n, "duration"))) el.classList.add("is-draggable");
2781
+ if (!preview && !indicator && (this.isEditable(n, "start", true) || this.isEditable(n, "duration", true))) el.classList.add("is-draggable");
2644
2782
  if (this.draggingId === n.id) el.classList.add("is-dragging");
2645
2783
  if (this.selectedIds.has(n.id) && !preview) el.classList.add("is-selected");
2646
2784
  el.classList.add(...classList(it.className));
2647
2785
  const arg = this.itemArg(el, meta);
2648
2786
  el.classList.add(...classList(this.opts.itemClassNames?.(arg)));
2649
2787
  el.dataset.id = n.id;
2788
+ this.applyItemWriteState(el, n.id);
2650
2789
  if (it.kind) el.dataset.kind = it.kind;
2651
2790
  const color = it.color ?? it.borderColor;
2652
2791
  if (color) el.style.setProperty("--tnc-c", color);
@@ -2663,6 +2802,7 @@ var Calendar = class {
2663
2802
  const custom = variant === "list" ? this.opts.listItemContent?.(arg) : null;
2664
2803
  const content = indicator ? null : custom ?? this.opts.itemContent?.(arg);
2665
2804
  if (indicator) {
2805
+ if (this.opts.monthDisplay === "chips") el.append(h(this.doc, "span", "tnc-item-title", null, it.title));
2666
2806
  } else if (content != null) {
2667
2807
  const wrap = h(this.doc, "div", "tnc-item-main");
2668
2808
  wrap.append(content);
@@ -2675,6 +2815,7 @@ var Calendar = class {
2675
2815
  return el;
2676
2816
  }
2677
2817
  refreshItemEl(el, meta) {
2818
+ this.applyItemWriteState(el, meta.n.id);
2678
2819
  const time = el.querySelector(":scope .tnc-item-time");
2679
2820
  if (time) this.fillTime(time, meta.n, meta.variant, meta.day);
2680
2821
  el.classList.toggle("is-dragging", this.draggingId === meta.n.id);
@@ -2697,9 +2838,10 @@ var Calendar = class {
2697
2838
  if (meta) this.opts.itemWillUnmount?.(this.itemArg(el, meta));
2698
2839
  el.remove();
2699
2840
  }
2700
- openMore(day, anchor, jsEvent) {
2841
+ openMore(day, anchor, jsEvent, itemIds) {
2701
2842
  jsEvent.stopPropagation();
2702
- 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));
2843
+ const group = itemIds ? new Set(itemIds) : null;
2844
+ 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));
2703
2845
  const shown = new Set(
2704
2846
  [...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 ?? "")
2705
2847
  );
@@ -2728,15 +2870,7 @@ var Calendar = class {
2728
2870
  }
2729
2871
  pool.end();
2730
2872
  this.popover = { el, pool, anchor, day };
2731
- const rr = this.root.getBoundingClientRect();
2732
- const ar = anchor.getBoundingClientRect();
2733
- const w = Math.max(220, ar.width + 24);
2734
- let left = ar.left - rr.left - 12;
2735
- left = Math.max(4, Math.min(left, rr.width - w - 4));
2736
- let top = ar.top - rr.top - 40;
2737
- const height = el.offsetHeight;
2738
- top = Math.max(4, Math.min(top, rr.height - height - 4));
2739
- setStyle(el, { left, top, width: w });
2873
+ this.positionMore();
2740
2874
  el.focus();
2741
2875
  }
2742
2876
  /* =============================================================== render */
@@ -2860,8 +2994,11 @@ var Calendar = class {
2860
2994
  this.current.setScroll({ top: keep.top, left: 0 });
2861
2995
  this.current.placeInstant(keep.ms, keep.vx);
2862
2996
  } else if (scroll) this.current.setScroll(scroll);
2863
- else if (this.current instanceof TimeGridView) this.current.scrollToTime(this.slot.scroll);
2864
- else if (this.current instanceof TimelineView) this.current.scrollToDay(this.anchor);
2997
+ else if (this.current instanceof TimeGridView) {
2998
+ this.current.scrollToTime(this.slot.scroll);
2999
+ this.current.revealDay(this.anchor);
3000
+ } else if (this.current instanceof TimelineView) this.current.scrollToDay(this.anchor);
3001
+ else if (this.current instanceof YearView) this.current.revealDay(this.anchor);
2865
3002
  if (rangeChanged) this.opts.onRangeChange?.(this.view);
2866
3003
  }
2867
3004
  /** Sets the visible days and view info from the anchor. */
@@ -2887,9 +3024,11 @@ var Calendar = class {
2887
3024
  }
2888
3025
  /** The range, run on to the end of the strip's days so their dots are right. */
2889
3026
  dataRangeFor(range) {
2890
- const end = this.strip ? Math.max(range.endDay, this.strip.window().end) : range.endDay;
2891
- if (end === range.endDay) return range;
2892
- return { ...range, endDay: end, end: new Date(toInstant(end, 0, this.zone)) };
3027
+ const window = this.strip?.window();
3028
+ const start = window ? Math.min(range.startDay, window.start) : range.startDay;
3029
+ const end = window ? Math.max(range.endDay, window.end) : range.endDay;
3030
+ if (start === range.startDay && end === range.endDay) return range;
3031
+ return { ...range, startDay: start, start: new Date(toInstant(start, 0, this.zone)), endDay: end, end: new Date(toInstant(end, 0, this.zone)) };
2893
3032
  }
2894
3033
  /** Adds, updates or removes the date strip for the current view. */
2895
3034
  syncStrip(navigated) {
@@ -2936,20 +3075,24 @@ var Calendar = class {
2936
3075
  stripShown() {
2937
3076
  if (!this.strip || this.rebuilding || !this.view) return;
2938
3077
  const next = this.dataRangeFor(this.view.range);
2939
- if (next.endDay === this.view.dataRange.endDay) return;
3078
+ if (next.startDay === this.view.dataRange.startDay && next.endDay === this.view.dataRange.endDay) return;
2940
3079
  this.view = { ...this.view, dataRange: next };
2941
3080
  this.opts.onRangeChange?.(this.view);
2942
3081
  }
2943
3082
  /** What is under a point, the date strip included. */
2944
3083
  hitAt(x, y) {
3084
+ const target = this.doc.elementFromPoint(x, y);
3085
+ if (!target || !this.root.contains(target)) {
3086
+ this.strip?.markTarget(null);
3087
+ return null;
3088
+ }
2945
3089
  const strip = this.strip;
2946
3090
  if (strip) {
2947
- const el = this.doc.elementFromPoint(x, y);
2948
- const day = el ? strip.dayAt(el) : null;
3091
+ const day = strip.dayAt(target);
2949
3092
  strip.markTarget(day !== null && this.draggingId ? day : null);
2950
3093
  if (day !== null) return { kind: "day", day };
2951
3094
  }
2952
- return this.current?.hit(x, y) ?? null;
3095
+ return this.current?.el.contains(target) ? this.current.hit(x, y) : null;
2953
3096
  }
2954
3097
  agendaList() {
2955
3098
  return this.opts.view === "list" && this.opts.listAgenda && this.current instanceof ListView ? this.current : null;
@@ -3032,17 +3175,44 @@ var Calendar = class {
3032
3175
  );
3033
3176
  const title = h(this.doc, "h2", "tnc-title", { "aria-live": "polite" }, this.view.title);
3034
3177
  const viewGroup = h(this.doc, "div", "tnc-toolbar-group tnc-toolbar-views", { role: "group" });
3178
+ const viewSelect = h(this.doc, "select", "tnc-btn tnc-toolbar-view-select", {
3179
+ "aria-label": this.text.viewHints[this.opts.view] ?? this.text.views[this.opts.view] ?? this.opts.view
3180
+ });
3035
3181
  for (const v of views) {
3036
3182
  const label = v === "days" ? this.text.views.days ?? `${this.opts.dayCount} days` : this.text.views[v] ?? v;
3037
3183
  viewGroup.append(btn(label, this.text.viewHints[v] ?? label, `tnc-btn-view tnc-btn-view-${v}`, () => this.changeView(v), v === this.opts.view));
3184
+ viewSelect.append(h(this.doc, "option", "", { value: v }, label));
3038
3185
  }
3186
+ viewSelect.value = this.opts.view;
3187
+ viewSelect.addEventListener("change", () => this.changeView(viewSelect.value));
3039
3188
  const start = h(this.doc, "div", "tnc-toolbar-start");
3040
3189
  const end = h(this.doc, "div", "tnc-toolbar-end");
3041
3190
  if (cfg.extraPlacement === "start") start.append(...extras);
3042
3191
  start.append(nav, title);
3043
- end.append(viewGroup);
3192
+ end.append(viewGroup, viewSelect);
3044
3193
  if (cfg.extraPlacement !== "start") end.append(...extras);
3045
3194
  this.toolbarEl.append(start, end);
3195
+ this.syncToolbarViewDisplay();
3196
+ }
3197
+ syncToolbarViewDisplay() {
3198
+ const buttons = this.toolbarEl.querySelector(".tnc-toolbar-views");
3199
+ const select = this.toolbarEl.querySelector(".tnc-toolbar-view-select");
3200
+ const start = this.toolbarEl.querySelector(".tnc-toolbar-start");
3201
+ if (!buttons || !select || !start) return;
3202
+ const config = this.opts.toolbar;
3203
+ const mode = typeof config === "object" ? config.viewDisplay ?? "auto" : "auto";
3204
+ buttons.hidden = false;
3205
+ select.hidden = true;
3206
+ buttons.style.display = "flex";
3207
+ select.style.display = "none";
3208
+ const startWidth = Array.from(start.children).reduce((width, child) => width + child.scrollWidth, 0);
3209
+ const availableWidth = Math.min(this.root.clientWidth, this.toolbarEl.clientWidth);
3210
+ const compact = mode === "select" || mode === "auto" && startWidth + buttons.scrollWidth + 32 > availableWidth;
3211
+ buttons.hidden = compact;
3212
+ select.hidden = !compact;
3213
+ buttons.style.display = compact ? "none" : "flex";
3214
+ select.style.display = compact ? "" : "none";
3215
+ select.parentElement?.classList.toggle("tnc-toolbar-end--compact", compact);
3046
3216
  }
3047
3217
  applyChange(change) {
3048
3218
  if (!change.added.length && !change.updated.length && !change.removed.length && !change.days.size && !change.spans) return;
@@ -3107,7 +3277,7 @@ var Calendar = class {
3107
3277
  const s = wallTime(n.startMs, this.zone);
3108
3278
  const start = this.fmt.time(s.minute, true);
3109
3279
  if (variant === "dot" || variant === "bar" || variant === "indicator") return { start: s.day === day ? start : "", end: "" };
3110
- if (n.startMs === n.endMs) return { start, end: "" };
3280
+ if (n.startMs === n.endMs || n.item.timeDisplay === "start") return { start, end: "" };
3111
3281
  return { start, end: this.fmt.time(wallTime(n.endMs, this.zone).minute, true) };
3112
3282
  }
3113
3283
  fillTime(el, n, variant, day) {
@@ -3123,7 +3293,8 @@ var Calendar = class {
3123
3293
  }
3124
3294
  const s = wallTime(n.startMs, this.zone);
3125
3295
  const e = wallTime(n.endMs, this.zone);
3126
- return `${n.item.title}, ${this.fmt.longDate(s.day)} ${this.fmt.time(s.minute)} \u2013 ${this.fmt.time(e.minute)}`;
3296
+ const start = `${n.item.title}, ${this.fmt.longDate(s.day)} ${this.fmt.time(s.minute)}`;
3297
+ return n.item.timeDisplay === "start" || n.startMs === n.endMs ? start : `${start} \u2013 ${this.fmt.time(e.minute)}`;
3127
3298
  }
3128
3299
  defaultContent(meta) {
3129
3300
  const { n, variant, day } = meta;
@@ -3151,7 +3322,7 @@ var Calendar = class {
3151
3322
  addHandles(el, meta) {
3152
3323
  const { n, variant } = meta;
3153
3324
  const ports = variant === "timeline" && !meta.mirror && n.id !== EXTERNAL_ID && !!this.opts.timeline?.onLink;
3154
- if (variant === "dot" || !this.isEditable(n, "duration")) {
3325
+ if (variant === "dot" || !this.isEditable(n, "duration", true)) {
3155
3326
  if (ports) this.addPorts(el, n);
3156
3327
  return;
3157
3328
  }
@@ -3175,6 +3346,38 @@ var Calendar = class {
3175
3346
  }
3176
3347
  }
3177
3348
  /* ============================================================== popover */
3349
+ onMoreGeometryChange = () => this.positionMore();
3350
+ listenMoreGeometry(on) {
3351
+ const method = on ? "addEventListener" : "removeEventListener";
3352
+ this.doc[method]("scroll", this.onMoreGeometryChange, true);
3353
+ this.win[method]("resize", this.onMoreGeometryChange);
3354
+ this.win.visualViewport?.[method]("resize", this.onMoreGeometryChange);
3355
+ this.win.visualViewport?.[method]("scroll", this.onMoreGeometryChange);
3356
+ }
3357
+ positionMore() {
3358
+ const popover = this.popover;
3359
+ if (!popover) return;
3360
+ if (!popover.anchor.isConnected) {
3361
+ this.closeMore();
3362
+ return;
3363
+ }
3364
+ const viewport = this.win.visualViewport;
3365
+ const root = this.root.getBoundingClientRect();
3366
+ const anchor = popover.anchor.getBoundingClientRect();
3367
+ const leftEdge = Math.max(root.left, viewport?.offsetLeft ?? 0) + 4;
3368
+ const topEdge = Math.max(root.top, viewport?.offsetTop ?? 0) + 4;
3369
+ const rightEdge = Math.min(root.right, (viewport?.offsetLeft ?? 0) + (viewport?.width ?? this.win.innerWidth)) - 4;
3370
+ const bottomEdge = Math.min(root.bottom, (viewport?.offsetTop ?? 0) + (viewport?.height ?? this.win.innerHeight)) - 4;
3371
+ if (rightEdge <= leftEdge || bottomEdge <= topEdge) {
3372
+ this.closeMore();
3373
+ return;
3374
+ }
3375
+ const width = Math.min(360, rightEdge - leftEdge);
3376
+ setStyle(popover.el, { width, "max-height": Math.min(420, bottomEdge - topEdge) });
3377
+ const left = Math.max(leftEdge, Math.min(anchor.left - 12, rightEdge - width));
3378
+ const top = Math.max(topEdge, Math.min(anchor.top - 40, bottomEdge - popover.el.offsetHeight));
3379
+ setStyle(popover.el, { left: left - root.left, top: top - root.top });
3380
+ }
3178
3381
  refreshMore() {
3179
3382
  const p = this.popover;
3180
3383
  if (!p) return;
@@ -3184,6 +3387,7 @@ var Calendar = class {
3184
3387
  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 });
3185
3388
  }
3186
3389
  p.pool.end();
3390
+ this.positionMore();
3187
3391
  }
3188
3392
  closeMore(restoreFocus = false) {
3189
3393
  const p = this.popover;
@@ -3211,6 +3415,7 @@ var Calendar = class {
3211
3415
  r.addEventListener("pointerover", this.onAdoptCheck);
3212
3416
  r.addEventListener("focusin", this.onAdoptCheck);
3213
3417
  this.doc.addEventListener("pointerdown", this.onDocPointerDown, true);
3418
+ this.listenMoreGeometry(true);
3214
3419
  }
3215
3420
  unbindEvents() {
3216
3421
  const r = this.root;
@@ -3229,6 +3434,7 @@ var Calendar = class {
3229
3434
  r.removeEventListener("pointerover", this.onAdoptCheck);
3230
3435
  r.removeEventListener("focusin", this.onAdoptCheck);
3231
3436
  this.doc.removeEventListener("pointerdown", this.onDocPointerDown, true);
3437
+ this.listenMoreGeometry(false);
3232
3438
  this.listenDoc(false);
3233
3439
  }
3234
3440
  listenDoc(on) {
@@ -3244,6 +3450,10 @@ var Calendar = class {
3244
3450
  return el && this.root.contains(el) && this.meta.has(el) && !this.meta.get(el)?.n.item.preview ? el : null;
3245
3451
  }
3246
3452
  onDocPointerDown = (ev) => {
3453
+ if (this.cancelledPointerId !== null) {
3454
+ this.cancelledPointerId = null;
3455
+ this.listenDoc(false);
3456
+ }
3247
3457
  const t = ev.target;
3248
3458
  if (this.popover && !this.popover.el.contains(t) && !this.popover.anchor.contains(t)) this.closeMore();
3249
3459
  if (this.selection && !this.root.contains(t)) this.unselect();
@@ -3251,6 +3461,7 @@ var Calendar = class {
3251
3461
  onPointerDown = (ev) => {
3252
3462
  if (ev.button !== 0 || this.session) return;
3253
3463
  const target = ev.target;
3464
+ if (target.closest(".tnc-dg--compact .tnc-dg-day")) return;
3254
3465
  if (target.closest("button, a, input, select, textarea, .tnc-toolbar, .tnc-port, [data-tnc-no-drag]")) {
3255
3466
  if (!target.closest(".tnc-item")) return;
3256
3467
  if (target.closest(".tnc-port, [data-tnc-no-drag], input, select, textarea")) return;
@@ -3274,6 +3485,7 @@ var Calendar = class {
3274
3485
  mode = "select";
3275
3486
  }
3276
3487
  const touch = ev.pointerType === "touch";
3488
+ const overflowOrigin = itemEl?.closest(".tnc-popover") && origin ? wallTime(origin.startMs, this.zone) : null;
3277
3489
  const s = {
3278
3490
  mode,
3279
3491
  pointerId: ev.pointerId,
@@ -3282,7 +3494,7 @@ var Calendar = class {
3282
3494
  y0: ev.clientY,
3283
3495
  active: false,
3284
3496
  origin,
3285
- grab: this.current?.hit(ev.clientX, ev.clientY) ?? null,
3497
+ 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,
3286
3498
  proposal: null,
3287
3499
  denied: false,
3288
3500
  timer: null,
@@ -3307,16 +3519,79 @@ var Calendar = class {
3307
3519
  };
3308
3520
  activate(s) {
3309
3521
  s.active = true;
3522
+ if (s.lastEvent) this.leaveHover(s.lastEvent);
3310
3523
  if (s.timer !== null) this.win.clearTimeout(s.timer);
3311
3524
  s.timer = null;
3312
3525
  if (s.origin) {
3313
3526
  this.draggingId = s.origin.id;
3314
- for (const el of this.elementsFor(s.origin.id)) el.classList.add("is-dragging");
3527
+ for (const el of this.elementsFor(s.origin.id)) {
3528
+ if (this.opts.view !== "list" && s.mode === "move") {
3529
+ const source = el.cloneNode(true);
3530
+ source.classList.remove("is-selected");
3531
+ source.classList.add("tnc-drag-source");
3532
+ source.setAttribute("aria-hidden", "true");
3533
+ source.inert = true;
3534
+ for (const node of [source, ...source.querySelectorAll("[id], [data-id], [data-task-path]")]) {
3535
+ node.removeAttribute("id");
3536
+ node.removeAttribute("data-id");
3537
+ node.removeAttribute("data-task-path");
3538
+ }
3539
+ el.before(source);
3540
+ this.dragSources.push(source);
3541
+ }
3542
+ if (this.opts.view === "list" && !this.listDragPreview && s.mode === "move") {
3543
+ const bounds = el.getBoundingClientRect();
3544
+ const preview = el.cloneNode(true);
3545
+ preview.classList.add("tnc-drag-preview", "is-dragging");
3546
+ preview.removeAttribute("data-id");
3547
+ preview.removeAttribute("data-task-path");
3548
+ preview.setAttribute("aria-hidden", "true");
3549
+ preview.inert = true;
3550
+ preview.style.width = `${bounds.width}px`;
3551
+ this.root.append(preview);
3552
+ this.listDragPreview = { element: preview, offsetX: s.x0 - bounds.left, offsetY: s.y0 - bounds.top, width: bounds.width, height: bounds.height };
3553
+ }
3554
+ el.classList.add("is-dragging");
3555
+ }
3315
3556
  }
3316
3557
  this.closeMore();
3317
3558
  if (s.mode !== "select") this.unselectSilently();
3318
3559
  this.root.classList.add("tnc-dragging", `tnc-dragging-${s.mode}`);
3319
3560
  if (s.lastEvent) this.track(s, s.lastEvent);
3561
+ this.startDragScroll();
3562
+ }
3563
+ startDragScroll() {
3564
+ if (this.dragScrollFrame !== null) return;
3565
+ const tick = () => {
3566
+ this.dragScrollFrame = null;
3567
+ const session = this.session;
3568
+ const pointer = session?.lastEvent;
3569
+ if (!session?.active || !pointer) return;
3570
+ const origin = session.origin ? this.elementsFor(session.origin.id)[0] : this.viewHost;
3571
+ const underPointer = this.doc.elementFromPoint(pointer.clientX, pointer.clientY);
3572
+ let moved = false;
3573
+ if (origin && underPointer && this.root.contains(underPointer)) {
3574
+ for (const axis of ["x", "y"]) {
3575
+ const scroller = scrollerOf(origin, axis);
3576
+ if (!scroller) continue;
3577
+ const viewport = scroller === this.doc.scrollingElement ? { left: 0, top: 0, right: this.win.innerWidth, bottom: this.win.innerHeight } : scroller.getBoundingClientRect();
3578
+ const coordinate = axis === "x" ? pointer.clientX : pointer.clientY;
3579
+ const lower = Math.max(0, axis === "x" ? viewport.left : viewport.top);
3580
+ const upper = Math.min(axis === "x" ? this.win.innerWidth : this.win.innerHeight, axis === "x" ? viewport.right : viewport.bottom);
3581
+ if (coordinate < lower || coordinate > upper) continue;
3582
+ const delta = coordinate < lower + 32 ? -Math.min(12, (lower + 32 - coordinate) / 2) : coordinate > upper - 32 ? Math.min(12, (coordinate - upper + 32) / 2) : 0;
3583
+ if (!delta) continue;
3584
+ if (!this.dragScrollOrigins.has(scroller)) this.dragScrollOrigins.set(scroller, { top: scroller.scrollTop, left: scroller.scrollLeft });
3585
+ const before = axis === "x" ? scroller.scrollLeft : scroller.scrollTop;
3586
+ if (axis === "x") scroller.scrollLeft += delta;
3587
+ else scroller.scrollTop += delta;
3588
+ moved ||= before !== (axis === "x" ? scroller.scrollLeft : scroller.scrollTop);
3589
+ }
3590
+ }
3591
+ if (moved) this.track(session, pointer);
3592
+ this.dragScrollFrame = this.win.requestAnimationFrame(tick);
3593
+ };
3594
+ this.dragScrollFrame = this.win.requestAnimationFrame(tick);
3320
3595
  }
3321
3596
  onTouchMove = (ev) => {
3322
3597
  if (this.session?.active) ev.preventDefault();
@@ -3346,6 +3621,12 @@ var Calendar = class {
3346
3621
  this.track(s, ev);
3347
3622
  };
3348
3623
  onPointerUp = (ev) => {
3624
+ if (ev.pointerId === this.cancelledPointerId) {
3625
+ this.cancelledPointerId = null;
3626
+ this.suppressNextClick();
3627
+ this.listenDoc(false);
3628
+ return;
3629
+ }
3349
3630
  const s = this.session;
3350
3631
  if (!s || ev.pointerId !== s.pointerId) return;
3351
3632
  if (!s.active) {
@@ -3358,28 +3639,49 @@ var Calendar = class {
3358
3639
  return;
3359
3640
  }
3360
3641
  this.suppressNextClick();
3642
+ this.track(s, ev);
3361
3643
  this.endSession(ev, false);
3362
3644
  };
3363
3645
  onPointerCancel = (ev) => {
3646
+ if (ev.pointerId === this.cancelledPointerId) {
3647
+ this.cancelledPointerId = null;
3648
+ this.listenDoc(false);
3649
+ }
3364
3650
  if (this.session && ev.pointerId === this.session.pointerId) this.endSession(null, true);
3365
3651
  };
3366
3652
  onDocKeyDown = (ev) => {
3367
3653
  if (ev.key === "Escape" && this.session) {
3368
3654
  ev.preventDefault();
3369
3655
  ev.stopPropagation();
3656
+ const pointerId = this.session.active ? this.session.pointerId : null;
3370
3657
  this.endSession(null, true);
3658
+ if (pointerId !== null) {
3659
+ this.cancelledPointerId = pointerId;
3660
+ this.listenDoc(true);
3661
+ }
3371
3662
  }
3372
3663
  };
3373
3664
  /** Proposal for the current pointer position. */
3374
3665
  track(s, ev) {
3666
+ if (this.listDragPreview) {
3667
+ const preview = this.listDragPreview;
3668
+ const bounds = this.root.getBoundingClientRect();
3669
+ preview.element.style.left = `${Math.max(8, Math.min(this.win.innerWidth - preview.width - 8, ev.clientX - preview.offsetX)) - bounds.left}px`;
3670
+ preview.element.style.top = `${Math.max(8, Math.min(this.win.innerHeight - preview.height - 8, ev.clientY - preview.offsetY)) - bounds.top}px`;
3671
+ }
3375
3672
  const hit = this.hitAt(ev.clientX, ev.clientY);
3376
3673
  if (s.mode === "external") {
3377
3674
  this.trackExternal(s, hit);
3378
3675
  return;
3379
3676
  }
3380
3677
  if (!s.grab) s.grab = hit;
3381
- if (!hit || !s.grab) return;
3678
+ if (!hit || !s.grab) {
3679
+ this.invalidateProposal(s);
3680
+ return;
3681
+ }
3382
3682
  if (s.mode === "select") {
3683
+ s.denied = false;
3684
+ this.root.classList.remove("tnc-drop-denied");
3383
3685
  const sel = this.selectionFor(s.grab, hit);
3384
3686
  if (!sel) return;
3385
3687
  this.selection = sel;
@@ -3388,25 +3690,40 @@ var Calendar = class {
3388
3690
  }
3389
3691
  const origin = s.origin;
3390
3692
  const p = s.mode === "move" ? this.moveProposal(origin, s.grab, hit) : this.resizeProposal(origin, hit, s.mode === "resize-start");
3391
- if (!p) return;
3693
+ if (!p) {
3694
+ this.invalidateProposal(s);
3695
+ return;
3696
+ }
3392
3697
  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;
3393
3698
  s.denied = !allowed;
3394
3699
  this.root.classList.toggle("tnc-drop-denied", !allowed);
3395
- if (!allowed || sameProposal(p, s.proposal)) return;
3700
+ if (!allowed) {
3701
+ this.invalidateProposal(s);
3702
+ return;
3703
+ }
3704
+ if (sameProposal(p, s.proposal)) return;
3396
3705
  s.proposal = p;
3397
3706
  this.dragRow = p.rowId !== void 0 ? { id: origin.id, rowId: p.rowId } : null;
3398
- this.applyChange(this.store.upsert([this.withProposal(origin, p)]));
3707
+ if (this.opts.view !== "list") this.applyChange(this.store.upsert([this.withProposal(origin, p)]));
3708
+ }
3709
+ invalidateProposal(s) {
3710
+ const hadProposal = s.proposal !== null;
3711
+ s.proposal = null;
3712
+ s.hit = null;
3713
+ s.denied = true;
3714
+ this.dragRow = null;
3715
+ this.root.classList.add("tnc-drop-denied");
3716
+ this.strip?.markTarget(null);
3717
+ this.unselectSilently();
3718
+ if (s.mode === "external") this.applyChange(this.store.remove([EXTERNAL_ID]));
3719
+ else if (hadProposal && s.origin) this.applyChange(this.store.upsert([s.origin.item]));
3399
3720
  }
3400
3721
  trackExternal(s, raw) {
3401
3722
  const src = s.source;
3402
3723
  const hit = raw?.kind === "timeline" && raw.area !== "track" && raw.area !== "header" ? null : raw;
3403
3724
  s.hit = hit;
3404
3725
  if (!hit) {
3405
- if (s.proposal) {
3406
- s.proposal = null;
3407
- this.applyChange(this.store.remove([EXTERNAL_ID]));
3408
- }
3409
- this.unselectSilently();
3726
+ this.invalidateProposal(s);
3410
3727
  return;
3411
3728
  }
3412
3729
  const dur = src.durationMinutes ?? this.opts.defaultTimedMinutes;
@@ -3425,6 +3742,10 @@ var Calendar = class {
3425
3742
  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;
3426
3743
  s.denied = !allowed;
3427
3744
  this.root.classList.toggle("tnc-drop-denied", !allowed);
3745
+ if (!allowed) {
3746
+ this.invalidateProposal(s);
3747
+ return;
3748
+ }
3428
3749
  if (sameProposal(p, s.proposal)) return;
3429
3750
  s.proposal = p;
3430
3751
  if (hit.kind === "timeline" && p.rowId === void 0) {
@@ -3617,7 +3938,18 @@ var Calendar = class {
3617
3938
  endSession(ev, cancel) {
3618
3939
  const s = this.session;
3619
3940
  if (!s) return;
3941
+ if (this.dragScrollFrame !== null) this.win.cancelAnimationFrame(this.dragScrollFrame);
3942
+ this.dragScrollFrame = null;
3943
+ if (cancel || s.denied) for (const [scroller, position] of this.dragScrollOrigins) {
3944
+ scroller.scrollTop = position.top;
3945
+ scroller.scrollLeft = position.left;
3946
+ }
3947
+ this.dragScrollOrigins.clear();
3620
3948
  this.session = null;
3949
+ this.listDragPreview?.element.remove();
3950
+ this.listDragPreview = null;
3951
+ for (const source of this.dragSources) source.remove();
3952
+ this.dragSources = [];
3621
3953
  if (s.timer !== null) this.win.clearTimeout(s.timer);
3622
3954
  this.listenDoc(false);
3623
3955
  this.root.classList.remove(
@@ -3686,10 +4018,16 @@ var Calendar = class {
3686
4018
  revert();
3687
4019
  return;
3688
4020
  }
4021
+ if (this.opts.view === "list") this.applyChange(this.store.upsert([this.withProposal(origin, p)]));
3689
4022
  this.flush();
3690
4023
  this.commit(s.mode === "move" ? "drop" : "resize", origin, p, ev, revert);
3691
4024
  }
3692
4025
  commit(kind, origin, p, ev, revert) {
4026
+ let reverted = false;
4027
+ const mark = (state) => {
4028
+ this.itemWriteStates.set(origin.id, state);
4029
+ for (const element of this.elementsFor(origin.id)) this.applyItemWriteState(element, origin.id);
4030
+ };
3693
4031
  const arg = {
3694
4032
  item: origin.item,
3695
4033
  start: new Date(p.startMs),
@@ -3705,19 +4043,32 @@ var Calendar = class {
3705
4043
  jsEvent: ev,
3706
4044
  source: ev && "key" in ev ? "keyboard" : "pointer",
3707
4045
  view: this.view,
3708
- revert
4046
+ revert: () => {
4047
+ reverted = true;
4048
+ revert();
4049
+ mark("failed");
4050
+ }
3709
4051
  };
3710
4052
  const handler = kind === "drop" ? this.opts.onItemDrop : this.opts.onItemResize;
3711
4053
  try {
4054
+ mark("pending");
3712
4055
  const res = handler?.(arg);
3713
4056
  if (res && typeof res.then === "function") {
3714
- Promise.resolve(res).catch(() => revert());
3715
- }
4057
+ void Promise.resolve(res).then(() => {
4058
+ if (!reverted) mark("changed");
4059
+ }, () => arg.revert());
4060
+ } else if (!reverted) mark("changed");
3716
4061
  } catch (err) {
3717
- revert();
4062
+ arg.revert();
3718
4063
  throw err;
3719
4064
  }
3720
4065
  }
4066
+ applyItemWriteState(element, id) {
4067
+ const state = this.itemWriteStates.get(id);
4068
+ for (const name of ["pending", "failed", "changed"]) element.classList.toggle(`is-write-${name}`, state === name);
4069
+ if (state === "pending") element.setAttribute("aria-busy", "true");
4070
+ else element.removeAttribute("aria-busy");
4071
+ }
3721
4072
  /* --------------------------------------------------------- click / keys */
3722
4073
  /** Runs in the capture phase so the click ending a drag never reaches content inside the calendar, React portals included. */
3723
4074
  onClickCapture = (ev) => {
@@ -3727,6 +4078,12 @@ var Calendar = class {
3727
4078
  ev.preventDefault();
3728
4079
  };
3729
4080
  onClick = (ev) => {
4081
+ const compactDay = ev.target.closest?.(".tnc-dg--compact .tnc-dg-day");
4082
+ if (compactDay && this.root.contains(compactDay) && !ev.target.closest(".tnc-navlink")) {
4083
+ const day = parseDay(compactDay.dataset.date ?? "");
4084
+ if (day !== null) this.openMore(day, compactDay, ev);
4085
+ return;
4086
+ }
3730
4087
  const yearDay = ev.target.closest?.(".tnc-ym-day");
3731
4088
  if (yearDay && this.root.contains(yearDay)) {
3732
4089
  const day = parseDay(yearDay.dataset.date ?? "");
@@ -3785,6 +4142,25 @@ var Calendar = class {
3785
4142
  if (el && meta) this.opts.onItemHover?.({ ...this.itemArg(el, meta), jsEvent: ev, entering: false });
3786
4143
  }
3787
4144
  onKeyDown = (ev) => {
4145
+ if (this.popover && !ev.altKey && !ev.ctrlKey && !ev.metaKey && ["ArrowDown", "ArrowUp", "Home", "End"].includes(ev.key)) {
4146
+ const rows = [...this.popover.el.querySelectorAll(".tnc-item")];
4147
+ const index = rows.indexOf(this.doc.activeElement);
4148
+ 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;
4149
+ ev.preventDefault();
4150
+ ev.stopPropagation();
4151
+ rows[next]?.focus();
4152
+ rows[next]?.scrollIntoView({ block: "nearest", inline: "nearest" });
4153
+ return;
4154
+ }
4155
+ if (ev.key === "Tab" && this.popover) {
4156
+ const controls = [...this.popover.el.querySelectorAll('button, a[href], [tabindex="0"]')];
4157
+ const index = controls.indexOf(this.doc.activeElement);
4158
+ const next = ev.shiftKey ? index <= 0 ? controls.length - 1 : index - 1 : (index + 1) % controls.length;
4159
+ ev.preventDefault();
4160
+ ev.stopPropagation();
4161
+ controls[next]?.focus();
4162
+ return;
4163
+ }
3788
4164
  if (ev.key === "Escape" && this.popover) {
3789
4165
  ev.preventDefault();
3790
4166
  ev.stopPropagation();
@@ -3796,7 +4172,8 @@ var Calendar = class {
3796
4172
  const day = parseDay(target.dataset.date ?? "");
3797
4173
  if (day !== null) {
3798
4174
  ev.preventDefault();
3799
- this.clickSlot({ kind: "day", day }, ev, true);
4175
+ if (target.closest(".tnc-dg--compact")) this.openMore(day, target, ev);
4176
+ else this.clickSlot({ kind: "day", day }, ev, true);
3800
4177
  }
3801
4178
  return;
3802
4179
  }
@@ -3884,9 +4261,13 @@ var Calendar = class {
3884
4261
  const dt = ev.dataTransfer;
3885
4262
  if (!dt || !this.opts.acceptNativeDrop?.(dt) || !this.current) return;
3886
4263
  ev.preventDefault();
4264
+ const hit = this.hitAt(ev.clientX, ev.clientY);
4265
+ if (!hit || hit.kind === "timeline" && hit.area !== "track" && hit.area !== "header") {
4266
+ dt.dropEffect = "none";
4267
+ this.unselectSilently();
4268
+ return;
4269
+ }
3887
4270
  dt.dropEffect = "copy";
3888
- const hit = this.current.hit(ev.clientX, ev.clientY);
3889
- if (!hit || hit.kind === "timeline" && hit.area !== "track" && hit.area !== "header") return;
3890
4271
  const sel = this.selectionFor(hit, hit);
3891
4272
  if (!sel) return;
3892
4273
  this.selection = sel;
@@ -3900,7 +4281,7 @@ var Calendar = class {
3900
4281
  const dt = ev.dataTransfer;
3901
4282
  if (!dt || !this.opts.acceptNativeDrop?.(dt) || !this.current) return;
3902
4283
  ev.preventDefault();
3903
- const hit = this.current.hit(ev.clientX, ev.clientY);
4284
+ const hit = this.hitAt(ev.clientX, ev.clientY);
3904
4285
  this.unselectSilently();
3905
4286
  if (!hit || hit.kind === "timeline" && hit.area !== "track" && hit.area !== "header") return;
3906
4287
  const sel = this.selectionFor(hit, hit);
@@ -3931,7 +4312,7 @@ function scrollerOf(el, axis) {
3931
4312
  return doc.scrollingElement;
3932
4313
  }
3933
4314
  function rangeMoved(prev, next) {
3934
- return !prev || prev.type !== next.type || prev.range.startDay !== next.range.startDay || prev.range.endDay !== next.range.endDay || prev.dataRange.endDay !== next.dataRange.endDay;
4315
+ 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;
3935
4316
  }
3936
4317
 
3937
4318
  export {