@tasknotes/calendar 0.1.7 → 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.
@@ -1074,8 +1074,9 @@ var DayGridView = class {
1074
1074
  constructor(ctx) {
1075
1075
  this.ctx = ctx;
1076
1076
  const { doc, opts, days } = ctx;
1077
- this.dotsMode = opts.monthDisplay === "dots";
1077
+ this.dotsMode = opts.monthDisplay === "dots" || opts.monthDisplay === "chips";
1078
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");
1079
1080
  this.scroller = h(doc, "div", "tnc-dg-scroll");
1080
1081
  this.grid = h(doc, "div", "tnc-dg-grid", { role: "grid", "aria-label": ctx.view.title });
1081
1082
  const perWeek = this.daysPerWeek();
@@ -1105,14 +1106,14 @@ var DayGridView = class {
1105
1106
  const cell = h(doc, "div", cls.join(" "), { role: "gridcell", "aria-label": ctx.fmt.longDate(d), "data-date": arg.dateStr });
1106
1107
  if (this.dotsMode) cell.tabIndex = 0;
1107
1108
  const num2 = ctx.fmt.dayNumber(d);
1108
- 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;
1109
1110
  cell.append(
1110
1111
  h(
1111
1112
  doc,
1112
1113
  "div",
1113
1114
  "tnc-dg-top",
1114
1115
  null,
1115
- 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)
1116
1117
  )
1117
1118
  );
1118
1119
  dates.append(cell);
@@ -1126,20 +1127,20 @@ var DayGridView = class {
1126
1127
  layer.append(content);
1127
1128
  row.append(dates, layer);
1128
1129
  this.grid.append(row);
1129
- const dots = this.dotsMode ? cells.map((cell) => {
1130
+ const dots = cells.map((cell) => {
1130
1131
  const box = h(doc, "div", "tnc-dg-dots", { "aria-hidden": "true" });
1131
1132
  if (opts.dayMaxEvents === false) box.classList.add("is-unlimited");
1132
1133
  const count = h(doc, "span", "tnc-dg-dots-more");
1133
1134
  cell.append(box);
1134
1135
  return { box, pool: new ItemPool(ctx, box), count };
1135
- }) : null;
1136
+ });
1136
1137
  this.weeks.push({ days: wdays, row, cells, layer, sel, more, pool: new ItemPool(ctx, content), dots });
1137
1138
  }
1138
1139
  this.scroller.append(this.grid);
1139
1140
  this.el.append(this.scroller);
1140
1141
  this.el.classList.toggle("tnc-dg--auto", opts.height === "auto");
1141
1142
  const RO = ctx.win.ResizeObserver;
1142
- if (RO && (opts.dayMaxEvents === true || this.dotsMode && opts.dayMaxEvents !== false)) {
1143
+ if (RO) {
1143
1144
  this.resizeObs = new RO(() => {
1144
1145
  if (this.sizeKey() !== this.lastHeights) this.render(null);
1145
1146
  });
@@ -1170,14 +1171,35 @@ var DayGridView = class {
1170
1171
  }
1171
1172
  render(_change) {
1172
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);
1173
1178
  const cs = ctx.win.getComputedStyle(this.el);
1174
1179
  this.barPx = px(cs, "--tnc-bar-h", 20);
1175
1180
  this.topPx = this.weeks[0]?.cells[0]?.querySelector(".tnc-dg-top")?.offsetHeight || px(cs, "--tnc-dg-top-h", 24);
1176
1181
  this.morePx = this.measureMore(cs);
1177
1182
  this.lastHeights = this.sizeKey();
1178
1183
  for (const w of this.weeks) {
1179
- if (w.dots) this.renderDots(w);
1180
- 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
+ }
1181
1203
  }
1182
1204
  }
1183
1205
  /** Height of a "+N more" link as the host styles it (`--tnc-more-h`, or a taller min-height). */
@@ -1276,7 +1298,8 @@ var DayGridView = class {
1276
1298
  w.more.append(link);
1277
1299
  }
1278
1300
  existing.delete(key);
1279
- link.textContent = ctx.text.more(items.length);
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);
1280
1303
  setStyle(link, {
1281
1304
  left: `calc(${pct(col / n)} + 2px)`,
1282
1305
  width: `calc(${pct(1 / n)} - 4px)`,
@@ -1306,10 +1329,11 @@ var DayGridView = class {
1306
1329
  const { dayMaxEvents } = ctx.opts;
1307
1330
  const first = w.dots[0].box;
1308
1331
  const cs = ctx.win.getComputedStyle(first);
1309
- 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);
1310
1334
  const gap = px(cs, "--tnc-dot-gap", 3);
1311
1335
  const step = size + gap;
1312
- 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));
1313
1337
  const rows = Math.max(1, Math.floor((first.clientHeight + gap) / step));
1314
1338
  const fits = dayMaxEvents === false ? Infinity : perRow * rows;
1315
1339
  const cap = typeof dayMaxEvents === "number" && dayMaxEvents > 0 ? Math.min(dayMaxEvents, fits) : fits;
@@ -1318,7 +1342,7 @@ var DayGridView = class {
1318
1342
  w.days.forEach((day, i) => {
1319
1343
  const cell = w.dots[i];
1320
1344
  const items = this.itemsOn(day);
1321
- 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);
1322
1346
  cell.pool.begin();
1323
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 }));
1324
1348
  cell.pool.end();
@@ -1435,6 +1459,7 @@ var ListView = class {
1435
1459
  const leadItems = [];
1436
1460
  const inLead = /* @__PURE__ */ new Set();
1437
1461
  const keep = /* @__PURE__ */ new Set();
1462
+ let itemCount = 0;
1438
1463
  let prev = null;
1439
1464
  for (const day of days) {
1440
1465
  let items = this.itemsOn(day);
@@ -1448,7 +1473,7 @@ var ListView = class {
1448
1473
  return false;
1449
1474
  });
1450
1475
  }
1451
- if (!items.length && !agenda) continue;
1476
+ if (!items.length && !agenda && opts.listDayCount !== 1) continue;
1452
1477
  keep.add(day);
1453
1478
  let section = this.sections.get(day);
1454
1479
  let pool = this.pools.get(day);
@@ -1467,6 +1492,7 @@ var ListView = class {
1467
1492
  this.pools.set(day, pool);
1468
1493
  }
1469
1494
  section.classList.toggle("is-empty", !items.length);
1495
+ itemCount += items.length;
1470
1496
  this.fill(pool, items, () => day);
1471
1497
  if (prev ? prev.nextSibling !== section : this.body.firstChild !== section) {
1472
1498
  if (prev) prev.after(section);
@@ -1482,13 +1508,14 @@ var ListView = class {
1482
1508
  this.sections.delete(day);
1483
1509
  }
1484
1510
  this.renderLead(leadItems);
1485
- let empty = this.body.querySelector(":scope > .tnc-list-empty");
1486
- if (!keep.size && !leadItems.length) {
1511
+ let empty = this.body.querySelector(".tnc-list-empty");
1512
+ if (!itemCount && !leadItems.length && (!agenda || opts.listEmptyContent)) {
1487
1513
  if (!empty) {
1488
1514
  empty = h(doc, "div", "tnc-list-empty", { role: "listitem" }, ctx.text.noItems);
1489
1515
  const content = ctx.opts.listEmptyContent?.();
1490
1516
  if (content) empty.replaceChildren(content);
1491
- this.body.append(empty);
1517
+ if (this.sections.size === 1) this.sections.values().next().value.append(empty);
1518
+ else this.body.prepend(empty);
1492
1519
  }
1493
1520
  } else empty?.remove();
1494
1521
  this.ctx.strip?.draw();
@@ -1546,7 +1573,7 @@ var ListView = class {
1546
1573
  const section = day === this.ctx.days[0] && this.lead ? this.lead.section : this.sections.get(day);
1547
1574
  if (!section) return;
1548
1575
  this.ctx.strip?.measure();
1549
- section.scrollIntoView({ block: "start" });
1576
+ this.scroller.scrollTop += section.getBoundingClientRect().top - this.scroller.getBoundingClientRect().top;
1550
1577
  this.ctx.strip?.setFocus(day);
1551
1578
  this.pinned = true;
1552
1579
  }
@@ -1635,8 +1662,7 @@ var DateStrip = class {
1635
1662
  head.append(this.title, this.todayBtn, prev, next, this.slot);
1636
1663
  this.dows = h(doc, "div", "tnc-strip-dows", { "aria-hidden": "true" });
1637
1664
  this.grid = h(doc, "div", "tnc-strip-grid", { role: "group" });
1638
- const handle = h(doc, "button", "tnc-strip-handle", { type: "button", tabindex: "-1", "aria-hidden": "true" });
1639
- handle.addEventListener("click", () => this.setExpanded(!this.expanded));
1665
+ const handle = h(doc, "div", "tnc-strip-handle", { "aria-hidden": "true" });
1640
1666
  this.el.append(head, this.dows, this.grid, handle);
1641
1667
  this.grid.addEventListener("click", this.onClick);
1642
1668
  this.grid.addEventListener("keydown", this.onKeyDown);
@@ -2215,8 +2241,15 @@ var TimeGridView = class {
2215
2241
  link.classList.toggle("tnc-more--collision", collapse);
2216
2242
  link.textContent = `+${rec.count}`;
2217
2243
  if (collapse) {
2218
- link.textContent = ctx.text.appointments(rec.count);
2219
- link.setAttribute("aria-label", link.textContent);
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(", ")}`);
2220
2253
  link.title = link.getAttribute("aria-label");
2221
2254
  } else {
2222
2255
  link.removeAttribute("aria-label");
@@ -2225,6 +2258,7 @@ var TimeGridView = class {
2225
2258
  const cols = maxStack;
2226
2259
  setStyle(link, {
2227
2260
  top: top(rec.start),
2261
+ height: collapse ? pct((rec.end - rec.start) / this.minutes) : "",
2228
2262
  ...inLane(lane, rec.split, (cols - 1) / cols, 1 / cols)
2229
2263
  });
2230
2264
  }
@@ -2291,12 +2325,10 @@ var TimeGridView = class {
2291
2325
  const r2 = cells[i2].getBoundingClientRect();
2292
2326
  if (x >= r2.left && x < r2.right) return i2;
2293
2327
  }
2294
- const firstR = cells[0]?.getBoundingClientRect();
2295
- const lastR = cells[cells.length - 1]?.getBoundingClientRect();
2296
- if (firstR && x < firstR.left) return 0;
2297
- if (lastR && x >= lastR.right) return cells.length - 1;
2298
2328
  return -1;
2299
2329
  };
2330
+ const bounds = this.scroller.getBoundingClientRect();
2331
+ if (x < bounds.left || x >= bounds.right || y < bounds.top || y >= bounds.bottom) return null;
2300
2332
  if (this.allDayRow) {
2301
2333
  const r2 = this.allDayRow.getBoundingClientRect();
2302
2334
  if (y >= r2.top && y < r2.bottom || !this.body && y >= r2.top) {
@@ -3019,6 +3051,14 @@ ${this.ctx.text.more(items.length - 8)}` : ""}` : label;
3019
3051
  setScroll(s) {
3020
3052
  this.scroller.scrollTop = s.top;
3021
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
+ }
3022
3062
  elementsFor(_id) {
3023
3063
  return [];
3024
3064
  }
@@ -3098,6 +3138,11 @@ var Calendar = class {
3098
3138
  conflicts = /* @__PURE__ */ new Map();
3099
3139
  slot = { min: 0, max: MINUTES_PER_DAY, duration: 30, snap: 30, scroll: 480 };
3100
3140
  draggingId = null;
3141
+ listDragPreview = null;
3142
+ dragSources = [];
3143
+ dragScrollFrame = null;
3144
+ dragScrollOrigins = /* @__PURE__ */ new Map();
3145
+ itemWriteStates = /* @__PURE__ */ new Map();
3101
3146
  dragRow = null;
3102
3147
  strip = null;
3103
3148
  stripSize = null;
@@ -3111,6 +3156,7 @@ var Calendar = class {
3111
3156
  session = null;
3112
3157
  selection = null;
3113
3158
  suppressClick = false;
3159
+ cancelledPointerId = null;
3114
3160
  selectedIds = /* @__PURE__ */ new Set();
3115
3161
  popover = null;
3116
3162
  timer = null;
@@ -3128,6 +3174,7 @@ var Calendar = class {
3128
3174
  this.positionMore();
3129
3175
  });
3130
3176
  this.resizeObs.observe(this.root);
3177
+ this.resizeObs.observe(this.toolbarEl);
3131
3178
  }
3132
3179
  }
3133
3180
  onAdoptCheck = () => {
@@ -3254,7 +3301,12 @@ var Calendar = class {
3254
3301
  gotoDate(date) {
3255
3302
  const day = this.toDay(date);
3256
3303
  if (day === null) return;
3304
+ const sameMonth = this.opts.view === "month" && startOfMonth(day) === startOfMonth(this.anchor);
3257
3305
  this.anchor = day;
3306
+ if (sameMonth) {
3307
+ this.view = { ...this.view, anchor: day };
3308
+ return;
3309
+ }
3258
3310
  this.rebuild(true);
3259
3311
  }
3260
3312
  prev() {
@@ -3400,7 +3452,8 @@ var Calendar = class {
3400
3452
  out.push(...classList(this.opts.dayClassNames?.(this.dayArg(day))));
3401
3453
  return out;
3402
3454
  }
3403
- isEditable(n, what) {
3455
+ isEditable(n, what, presentation = false) {
3456
+ if (!presentation && this.itemWriteStates.get(n.id) === "pending") return false;
3404
3457
  if (n.id === EXTERNAL_ID || n.item.preview || this.opts.view === "year") return false;
3405
3458
  if (this.opts.view === "list" && (!this.opts.listAgenda || what === "duration")) return false;
3406
3459
  const it = n.item;
@@ -3440,13 +3493,14 @@ var Calendar = class {
3440
3493
  if (preview || n.id === EXTERNAL_ID) el.classList.add("is-mirror");
3441
3494
  if (this.conflicts.has(n.id)) el.classList.add("is-conflict");
3442
3495
  const indicator = variant === "indicator";
3443
- 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");
3444
3497
  if (this.draggingId === n.id) el.classList.add("is-dragging");
3445
3498
  if (this.selectedIds.has(n.id) && !preview) el.classList.add("is-selected");
3446
3499
  el.classList.add(...classList(it.className));
3447
3500
  const arg = this.itemArg(el, meta);
3448
3501
  el.classList.add(...classList(this.opts.itemClassNames?.(arg)));
3449
3502
  el.dataset.id = n.id;
3503
+ this.applyItemWriteState(el, n.id);
3450
3504
  if (it.kind) el.dataset.kind = it.kind;
3451
3505
  const color = it.color ?? it.borderColor;
3452
3506
  if (color) el.style.setProperty("--tnc-c", color);
@@ -3463,6 +3517,7 @@ var Calendar = class {
3463
3517
  const custom = variant === "list" ? this.opts.listItemContent?.(arg) : null;
3464
3518
  const content = indicator ? null : custom ?? this.opts.itemContent?.(arg);
3465
3519
  if (indicator) {
3520
+ if (this.opts.monthDisplay === "chips") el.append(h(this.doc, "span", "tnc-item-title", null, it.title));
3466
3521
  } else if (content != null) {
3467
3522
  const wrap = h(this.doc, "div", "tnc-item-main");
3468
3523
  wrap.append(content);
@@ -3475,6 +3530,7 @@ var Calendar = class {
3475
3530
  return el;
3476
3531
  }
3477
3532
  refreshItemEl(el, meta) {
3533
+ this.applyItemWriteState(el, meta.n.id);
3478
3534
  const time = el.querySelector(":scope .tnc-item-time");
3479
3535
  if (time) this.fillTime(time, meta.n, meta.variant, meta.day);
3480
3536
  el.classList.toggle("is-dragging", this.draggingId === meta.n.id);
@@ -3657,6 +3713,7 @@ var Calendar = class {
3657
3713
  this.current.scrollToTime(this.slot.scroll);
3658
3714
  this.current.revealDay(this.anchor);
3659
3715
  } else if (this.current instanceof TimelineView) this.current.scrollToDay(this.anchor);
3716
+ else if (this.current instanceof YearView) this.current.revealDay(this.anchor);
3660
3717
  if (rangeChanged) this.opts.onRangeChange?.(this.view);
3661
3718
  }
3662
3719
  /** Sets the visible days and view info from the anchor. */
@@ -3682,9 +3739,11 @@ var Calendar = class {
3682
3739
  }
3683
3740
  /** The range, run on to the end of the strip's days so their dots are right. */
3684
3741
  dataRangeFor(range) {
3685
- const end = this.strip ? Math.max(range.endDay, this.strip.window().end) : range.endDay;
3686
- if (end === range.endDay) return range;
3687
- 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)) };
3688
3747
  }
3689
3748
  /** Adds, updates or removes the date strip for the current view. */
3690
3749
  syncStrip(navigated) {
@@ -3731,20 +3790,24 @@ var Calendar = class {
3731
3790
  stripShown() {
3732
3791
  if (!this.strip || this.rebuilding || !this.view) return;
3733
3792
  const next = this.dataRangeFor(this.view.range);
3734
- if (next.endDay === this.view.dataRange.endDay) return;
3793
+ if (next.startDay === this.view.dataRange.startDay && next.endDay === this.view.dataRange.endDay) return;
3735
3794
  this.view = { ...this.view, dataRange: next };
3736
3795
  this.opts.onRangeChange?.(this.view);
3737
3796
  }
3738
3797
  /** What is under a point, the date strip included. */
3739
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
+ }
3740
3804
  const strip = this.strip;
3741
3805
  if (strip) {
3742
- const el = this.doc.elementFromPoint(x, y);
3743
- const day = el ? strip.dayAt(el) : null;
3806
+ const day = strip.dayAt(target);
3744
3807
  strip.markTarget(day !== null && this.draggingId ? day : null);
3745
3808
  if (day !== null) return { kind: "day", day };
3746
3809
  }
3747
- return this.current?.hit(x, y) ?? null;
3810
+ return this.current?.el.contains(target) ? this.current.hit(x, y) : null;
3748
3811
  }
3749
3812
  agendaList() {
3750
3813
  return this.opts.view === "list" && this.opts.listAgenda && this.current instanceof ListView ? this.current : null;
@@ -3858,11 +3921,13 @@ var Calendar = class {
3858
3921
  buttons.style.display = "flex";
3859
3922
  select.style.display = "none";
3860
3923
  const startWidth = Array.from(start.children).reduce((width, child) => width + child.scrollWidth, 0);
3861
- const compact = mode === "select" || mode === "auto" && startWidth + buttons.scrollWidth + 32 > this.root.clientWidth;
3924
+ const availableWidth = Math.min(this.root.clientWidth, this.toolbarEl.clientWidth);
3925
+ const compact = mode === "select" || mode === "auto" && startWidth + buttons.scrollWidth + 32 > availableWidth;
3862
3926
  buttons.hidden = compact;
3863
3927
  select.hidden = !compact;
3864
3928
  buttons.style.display = compact ? "none" : "flex";
3865
3929
  select.style.display = compact ? "" : "none";
3930
+ select.parentElement?.classList.toggle("tnc-toolbar-end--compact", compact);
3866
3931
  }
3867
3932
  applyChange(change) {
3868
3933
  if (!change.added.length && !change.updated.length && !change.removed.length && !change.days.size && !change.spans) return;
@@ -3927,7 +3992,7 @@ var Calendar = class {
3927
3992
  const s = wallTime(n.startMs, this.zone);
3928
3993
  const start = this.fmt.time(s.minute, true);
3929
3994
  if (variant === "dot" || variant === "bar" || variant === "indicator") return { start: s.day === day ? start : "", end: "" };
3930
- if (n.startMs === n.endMs) return { start, end: "" };
3995
+ if (n.startMs === n.endMs || n.item.timeDisplay === "start") return { start, end: "" };
3931
3996
  return { start, end: this.fmt.time(wallTime(n.endMs, this.zone).minute, true) };
3932
3997
  }
3933
3998
  fillTime(el, n, variant, day) {
@@ -3943,7 +4008,8 @@ var Calendar = class {
3943
4008
  }
3944
4009
  const s = wallTime(n.startMs, this.zone);
3945
4010
  const e = wallTime(n.endMs, this.zone);
3946
- 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)}`;
3947
4013
  }
3948
4014
  defaultContent(meta) {
3949
4015
  const { n, variant, day } = meta;
@@ -3971,7 +4037,7 @@ var Calendar = class {
3971
4037
  addHandles(el, meta) {
3972
4038
  const { n, variant } = meta;
3973
4039
  const ports = variant === "timeline" && !meta.mirror && n.id !== EXTERNAL_ID && !!this.opts.timeline?.onLink;
3974
- if (variant === "dot" || !this.isEditable(n, "duration")) {
4040
+ if (variant === "dot" || !this.isEditable(n, "duration", true)) {
3975
4041
  if (ports) this.addPorts(el, n);
3976
4042
  return;
3977
4043
  }
@@ -4099,6 +4165,10 @@ var Calendar = class {
4099
4165
  return el && this.root.contains(el) && this.meta.has(el) && !this.meta.get(el)?.n.item.preview ? el : null;
4100
4166
  }
4101
4167
  onDocPointerDown = (ev) => {
4168
+ if (this.cancelledPointerId !== null) {
4169
+ this.cancelledPointerId = null;
4170
+ this.listenDoc(false);
4171
+ }
4102
4172
  const t = ev.target;
4103
4173
  if (this.popover && !this.popover.el.contains(t) && !this.popover.anchor.contains(t)) this.closeMore();
4104
4174
  if (this.selection && !this.root.contains(t)) this.unselect();
@@ -4106,6 +4176,7 @@ var Calendar = class {
4106
4176
  onPointerDown = (ev) => {
4107
4177
  if (ev.button !== 0 || this.session) return;
4108
4178
  const target = ev.target;
4179
+ if (target.closest(".tnc-dg--compact .tnc-dg-day")) return;
4109
4180
  if (target.closest("button, a, input, select, textarea, .tnc-toolbar, .tnc-port, [data-tnc-no-drag]")) {
4110
4181
  if (!target.closest(".tnc-item")) return;
4111
4182
  if (target.closest(".tnc-port, [data-tnc-no-drag], input, select, textarea")) return;
@@ -4138,7 +4209,7 @@ var Calendar = class {
4138
4209
  y0: ev.clientY,
4139
4210
  active: false,
4140
4211
  origin,
4141
- grab: overflowOrigin && this.current instanceof TimeGridView && !origin?.allDay ? { kind: "time", day: overflowOrigin.day, minute: overflowOrigin.minute } : this.current?.hit(ev.clientX, ev.clientY) ?? null,
4212
+ grab: overflowOrigin ? this.current instanceof TimeGridView && !origin?.allDay ? { kind: "time", day: overflowOrigin.day, minute: overflowOrigin.minute } : { kind: "day", day: this.meta.get(itemEl).day } : this.current?.hit(ev.clientX, ev.clientY) ?? null,
4142
4213
  proposal: null,
4143
4214
  denied: false,
4144
4215
  timer: null,
@@ -4163,16 +4234,79 @@ var Calendar = class {
4163
4234
  };
4164
4235
  activate(s) {
4165
4236
  s.active = true;
4237
+ if (s.lastEvent) this.leaveHover(s.lastEvent);
4166
4238
  if (s.timer !== null) this.win.clearTimeout(s.timer);
4167
4239
  s.timer = null;
4168
4240
  if (s.origin) {
4169
4241
  this.draggingId = s.origin.id;
4170
- 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
+ }
4171
4271
  }
4172
4272
  this.closeMore();
4173
4273
  if (s.mode !== "select") this.unselectSilently();
4174
4274
  this.root.classList.add("tnc-dragging", `tnc-dragging-${s.mode}`);
4175
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);
4176
4310
  }
4177
4311
  onTouchMove = (ev) => {
4178
4312
  if (this.session?.active) ev.preventDefault();
@@ -4202,6 +4336,12 @@ var Calendar = class {
4202
4336
  this.track(s, ev);
4203
4337
  };
4204
4338
  onPointerUp = (ev) => {
4339
+ if (ev.pointerId === this.cancelledPointerId) {
4340
+ this.cancelledPointerId = null;
4341
+ this.suppressNextClick();
4342
+ this.listenDoc(false);
4343
+ return;
4344
+ }
4205
4345
  const s = this.session;
4206
4346
  if (!s || ev.pointerId !== s.pointerId) return;
4207
4347
  if (!s.active) {
@@ -4214,28 +4354,49 @@ var Calendar = class {
4214
4354
  return;
4215
4355
  }
4216
4356
  this.suppressNextClick();
4357
+ this.track(s, ev);
4217
4358
  this.endSession(ev, false);
4218
4359
  };
4219
4360
  onPointerCancel = (ev) => {
4361
+ if (ev.pointerId === this.cancelledPointerId) {
4362
+ this.cancelledPointerId = null;
4363
+ this.listenDoc(false);
4364
+ }
4220
4365
  if (this.session && ev.pointerId === this.session.pointerId) this.endSession(null, true);
4221
4366
  };
4222
4367
  onDocKeyDown = (ev) => {
4223
4368
  if (ev.key === "Escape" && this.session) {
4224
4369
  ev.preventDefault();
4225
4370
  ev.stopPropagation();
4371
+ const pointerId = this.session.active ? this.session.pointerId : null;
4226
4372
  this.endSession(null, true);
4373
+ if (pointerId !== null) {
4374
+ this.cancelledPointerId = pointerId;
4375
+ this.listenDoc(true);
4376
+ }
4227
4377
  }
4228
4378
  };
4229
4379
  /** Proposal for the current pointer position. */
4230
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
+ }
4231
4387
  const hit = this.hitAt(ev.clientX, ev.clientY);
4232
4388
  if (s.mode === "external") {
4233
4389
  this.trackExternal(s, hit);
4234
4390
  return;
4235
4391
  }
4236
4392
  if (!s.grab) s.grab = hit;
4237
- if (!hit || !s.grab) return;
4393
+ if (!hit || !s.grab) {
4394
+ this.invalidateProposal(s);
4395
+ return;
4396
+ }
4238
4397
  if (s.mode === "select") {
4398
+ s.denied = false;
4399
+ this.root.classList.remove("tnc-drop-denied");
4239
4400
  const sel = this.selectionFor(s.grab, hit);
4240
4401
  if (!sel) return;
4241
4402
  this.selection = sel;
@@ -4244,25 +4405,40 @@ var Calendar = class {
4244
4405
  }
4245
4406
  const origin = s.origin;
4246
4407
  const p = s.mode === "move" ? this.moveProposal(origin, s.grab, hit) : this.resizeProposal(origin, hit, s.mode === "resize-start");
4247
- if (!p) return;
4408
+ if (!p) {
4409
+ this.invalidateProposal(s);
4410
+ return;
4411
+ }
4248
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;
4249
4413
  s.denied = !allowed;
4250
4414
  this.root.classList.toggle("tnc-drop-denied", !allowed);
4251
- if (!allowed || sameProposal(p, s.proposal)) return;
4415
+ if (!allowed) {
4416
+ this.invalidateProposal(s);
4417
+ return;
4418
+ }
4419
+ if (sameProposal(p, s.proposal)) return;
4252
4420
  s.proposal = p;
4253
4421
  this.dragRow = p.rowId !== void 0 ? { id: origin.id, rowId: p.rowId } : null;
4254
- 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]));
4255
4435
  }
4256
4436
  trackExternal(s, raw) {
4257
4437
  const src = s.source;
4258
4438
  const hit = raw?.kind === "timeline" && raw.area !== "track" && raw.area !== "header" ? null : raw;
4259
4439
  s.hit = hit;
4260
4440
  if (!hit) {
4261
- if (s.proposal) {
4262
- s.proposal = null;
4263
- this.applyChange(this.store.remove([EXTERNAL_ID]));
4264
- }
4265
- this.unselectSilently();
4441
+ this.invalidateProposal(s);
4266
4442
  return;
4267
4443
  }
4268
4444
  const dur = src.durationMinutes ?? this.opts.defaultTimedMinutes;
@@ -4281,6 +4457,10 @@ var Calendar = class {
4281
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;
4282
4458
  s.denied = !allowed;
4283
4459
  this.root.classList.toggle("tnc-drop-denied", !allowed);
4460
+ if (!allowed) {
4461
+ this.invalidateProposal(s);
4462
+ return;
4463
+ }
4284
4464
  if (sameProposal(p, s.proposal)) return;
4285
4465
  s.proposal = p;
4286
4466
  if (hit.kind === "timeline" && p.rowId === void 0) {
@@ -4473,7 +4653,18 @@ var Calendar = class {
4473
4653
  endSession(ev, cancel) {
4474
4654
  const s = this.session;
4475
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();
4476
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 = [];
4477
4668
  if (s.timer !== null) this.win.clearTimeout(s.timer);
4478
4669
  this.listenDoc(false);
4479
4670
  this.root.classList.remove(
@@ -4542,10 +4733,16 @@ var Calendar = class {
4542
4733
  revert();
4543
4734
  return;
4544
4735
  }
4736
+ if (this.opts.view === "list") this.applyChange(this.store.upsert([this.withProposal(origin, p)]));
4545
4737
  this.flush();
4546
4738
  this.commit(s.mode === "move" ? "drop" : "resize", origin, p, ev, revert);
4547
4739
  }
4548
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
+ };
4549
4746
  const arg = {
4550
4747
  item: origin.item,
4551
4748
  start: new Date(p.startMs),
@@ -4561,19 +4758,32 @@ var Calendar = class {
4561
4758
  jsEvent: ev,
4562
4759
  source: ev && "key" in ev ? "keyboard" : "pointer",
4563
4760
  view: this.view,
4564
- revert
4761
+ revert: () => {
4762
+ reverted = true;
4763
+ revert();
4764
+ mark("failed");
4765
+ }
4565
4766
  };
4566
4767
  const handler = kind === "drop" ? this.opts.onItemDrop : this.opts.onItemResize;
4567
4768
  try {
4769
+ mark("pending");
4568
4770
  const res = handler?.(arg);
4569
4771
  if (res && typeof res.then === "function") {
4570
- Promise.resolve(res).catch(() => revert());
4571
- }
4772
+ void Promise.resolve(res).then(() => {
4773
+ if (!reverted) mark("changed");
4774
+ }, () => arg.revert());
4775
+ } else if (!reverted) mark("changed");
4572
4776
  } catch (err) {
4573
- revert();
4777
+ arg.revert();
4574
4778
  throw err;
4575
4779
  }
4576
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
+ }
4577
4787
  /* --------------------------------------------------------- click / keys */
4578
4788
  /** Runs in the capture phase so the click ending a drag never reaches content inside the calendar, React portals included. */
4579
4789
  onClickCapture = (ev) => {
@@ -4583,6 +4793,12 @@ var Calendar = class {
4583
4793
  ev.preventDefault();
4584
4794
  };
4585
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
+ }
4586
4802
  const yearDay = ev.target.closest?.(".tnc-ym-day");
4587
4803
  if (yearDay && this.root.contains(yearDay)) {
4588
4804
  const day = parseDay(yearDay.dataset.date ?? "");
@@ -4671,7 +4887,8 @@ var Calendar = class {
4671
4887
  const day = parseDay(target.dataset.date ?? "");
4672
4888
  if (day !== null) {
4673
4889
  ev.preventDefault();
4674
- 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);
4675
4892
  }
4676
4893
  return;
4677
4894
  }
@@ -4759,9 +4976,13 @@ var Calendar = class {
4759
4976
  const dt = ev.dataTransfer;
4760
4977
  if (!dt || !this.opts.acceptNativeDrop?.(dt) || !this.current) return;
4761
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
+ }
4762
4985
  dt.dropEffect = "copy";
4763
- const hit = this.current.hit(ev.clientX, ev.clientY);
4764
- if (!hit || hit.kind === "timeline" && hit.area !== "track" && hit.area !== "header") return;
4765
4986
  const sel = this.selectionFor(hit, hit);
4766
4987
  if (!sel) return;
4767
4988
  this.selection = sel;
@@ -4775,7 +4996,7 @@ var Calendar = class {
4775
4996
  const dt = ev.dataTransfer;
4776
4997
  if (!dt || !this.opts.acceptNativeDrop?.(dt) || !this.current) return;
4777
4998
  ev.preventDefault();
4778
- const hit = this.current.hit(ev.clientX, ev.clientY);
4999
+ const hit = this.hitAt(ev.clientX, ev.clientY);
4779
5000
  this.unselectSilently();
4780
5001
  if (!hit || hit.kind === "timeline" && hit.area !== "track" && hit.area !== "header") return;
4781
5002
  const sel = this.selectionFor(hit, hit);
@@ -4806,7 +5027,7 @@ function scrollerOf(el, axis) {
4806
5027
  return doc.scrollingElement;
4807
5028
  }
4808
5029
  function rangeMoved(prev, next) {
4809
- 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;
4810
5031
  }
4811
5032
 
4812
5033
  // src/tasknotes/bases.ts