@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.
@@ -982,8 +982,9 @@ var DayGridView = class {
982
982
  constructor(ctx) {
983
983
  this.ctx = ctx;
984
984
  const { doc, opts, days } = ctx;
985
- this.dotsMode = opts.monthDisplay === "dots";
985
+ this.dotsMode = opts.monthDisplay === "dots" || opts.monthDisplay === "chips";
986
986
  this.el = h(doc, "div", this.dotsMode ? "tnc-dg tnc-dg--dots" : "tnc-dg");
987
+ this.el.classList.toggle("tnc-dg--chips", opts.monthDisplay === "chips");
987
988
  this.scroller = h(doc, "div", "tnc-dg-scroll");
988
989
  this.grid = h(doc, "div", "tnc-dg-grid", { role: "grid", "aria-label": ctx.view.title });
989
990
  const perWeek = this.daysPerWeek();
@@ -1013,14 +1014,14 @@ var DayGridView = class {
1013
1014
  const cell = h(doc, "div", cls.join(" "), { role: "gridcell", "aria-label": ctx.fmt.longDate(d), "data-date": arg.dateStr });
1014
1015
  if (this.dotsMode) cell.tabIndex = 0;
1015
1016
  const num = ctx.fmt.dayNumber(d);
1016
- const label = ymdOfDay(d).day === 1 ? `${ctx.fmt.monthShort(d)} ${num}` : num;
1017
+ const label = !this.dotsMode && ymdOfDay(d).day === 1 ? `${ctx.fmt.monthShort(d)} ${num}` : num;
1017
1018
  cell.append(
1018
1019
  h(
1019
1020
  doc,
1020
1021
  "div",
1021
1022
  "tnc-dg-top",
1022
1023
  null,
1023
- opts.navLinks ? ctx.navLink(d, label, "tnc-dg-num") : h(doc, "span", "tnc-dg-num", null, label)
1024
+ opts.navLinks && !this.dotsMode ? ctx.navLink(d, label, "tnc-dg-num") : h(doc, "span", "tnc-dg-num", null, label)
1024
1025
  )
1025
1026
  );
1026
1027
  dates.append(cell);
@@ -1034,20 +1035,20 @@ var DayGridView = class {
1034
1035
  layer.append(content);
1035
1036
  row.append(dates, layer);
1036
1037
  this.grid.append(row);
1037
- const dots = this.dotsMode ? cells.map((cell) => {
1038
+ const dots = cells.map((cell) => {
1038
1039
  const box = h(doc, "div", "tnc-dg-dots", { "aria-hidden": "true" });
1039
1040
  if (opts.dayMaxEvents === false) box.classList.add("is-unlimited");
1040
1041
  const count = h(doc, "span", "tnc-dg-dots-more");
1041
1042
  cell.append(box);
1042
1043
  return { box, pool: new ItemPool(ctx, box), count };
1043
- }) : null;
1044
+ });
1044
1045
  this.weeks.push({ days: wdays, row, cells, layer, sel, more, pool: new ItemPool(ctx, content), dots });
1045
1046
  }
1046
1047
  this.scroller.append(this.grid);
1047
1048
  this.el.append(this.scroller);
1048
1049
  this.el.classList.toggle("tnc-dg--auto", opts.height === "auto");
1049
1050
  const RO = ctx.win.ResizeObserver;
1050
- if (RO && (opts.dayMaxEvents === true || this.dotsMode && opts.dayMaxEvents !== false)) {
1051
+ if (RO) {
1051
1052
  this.resizeObs = new RO(() => {
1052
1053
  if (this.sizeKey() !== this.lastHeights) this.render(null);
1053
1054
  });
@@ -1078,14 +1079,35 @@ var DayGridView = class {
1078
1079
  }
1079
1080
  render(_change) {
1080
1081
  const { ctx } = this;
1082
+ this.dotsMode = ctx.opts.monthDisplay !== "full" || this.grid.clientWidth / this.daysPerWeek() < 72;
1083
+ this.el.classList.toggle("tnc-dg--dots", this.dotsMode);
1084
+ this.el.classList.toggle("tnc-dg--compact", this.dotsMode && ctx.opts.monthDisplay === "full");
1085
+ this.el.classList.toggle("tnc-dg--chips", ctx.opts.monthDisplay === "chips" && this.weeks[0].dots[0].box.clientHeight >= 44);
1081
1086
  const cs = ctx.win.getComputedStyle(this.el);
1082
1087
  this.barPx = px(cs, "--tnc-bar-h", 20);
1083
1088
  this.topPx = this.weeks[0]?.cells[0]?.querySelector(".tnc-dg-top")?.offsetHeight || px(cs, "--tnc-dg-top-h", 24);
1084
1089
  this.morePx = this.measureMore(cs);
1085
1090
  this.lastHeights = this.sizeKey();
1086
1091
  for (const w of this.weeks) {
1087
- if (w.dots) this.renderDots(w);
1088
- else this.renderWeek(w);
1092
+ for (const cell of w.cells) {
1093
+ if (this.dotsMode) cell.tabIndex = 0;
1094
+ else cell.removeAttribute("tabindex");
1095
+ if (this.dotsMode && ctx.opts.monthDisplay === "full") cell.setAttribute("aria-haspopup", "dialog");
1096
+ else cell.removeAttribute("aria-haspopup");
1097
+ }
1098
+ for (const cell of w.dots) cell.box.hidden = !this.dotsMode;
1099
+ if (this.dotsMode) {
1100
+ w.pool.begin();
1101
+ w.pool.end();
1102
+ w.more.replaceChildren();
1103
+ this.renderDots(w);
1104
+ } else {
1105
+ for (const cell of w.dots) {
1106
+ cell.pool.begin();
1107
+ cell.pool.end();
1108
+ }
1109
+ this.renderWeek(w);
1110
+ }
1089
1111
  }
1090
1112
  }
1091
1113
  /** Height of a "+N more" link as the host styles it (`--tnc-more-h`, or a taller min-height). */
@@ -1184,7 +1206,8 @@ var DayGridView = class {
1184
1206
  w.more.append(link);
1185
1207
  }
1186
1208
  existing.delete(key);
1187
- link.textContent = ctx.text.more(items.length);
1209
+ link.setAttribute("aria-label", ctx.text.more(items.length));
1210
+ link.textContent = w.row.clientWidth / n < 72 ? `+${new Intl.NumberFormat(ctx.opts.locale).format(items.length)}` : ctx.text.more(items.length);
1188
1211
  setStyle(link, {
1189
1212
  left: `calc(${pct(col / n)} + 2px)`,
1190
1213
  width: `calc(${pct(1 / n)} - 4px)`,
@@ -1214,10 +1237,11 @@ var DayGridView = class {
1214
1237
  const { dayMaxEvents } = ctx.opts;
1215
1238
  const first = w.dots[0].box;
1216
1239
  const cs = ctx.win.getComputedStyle(first);
1217
- const size = px(cs, "--tnc-dot-size", 8);
1240
+ const chips = this.el.classList.contains("tnc-dg--chips");
1241
+ const size = chips ? px(cs, "--tnc-chip-h", 14) : px(cs, "--tnc-dot-size", 8);
1218
1242
  const gap = px(cs, "--tnc-dot-gap", 3);
1219
1243
  const step = size + gap;
1220
- const perRow = Math.max(1, Math.floor((first.clientWidth + gap) / step));
1244
+ const perRow = chips ? 1 : Math.max(1, Math.floor((first.clientWidth + gap) / step));
1221
1245
  const rows = Math.max(1, Math.floor((first.clientHeight + gap) / step));
1222
1246
  const fits = dayMaxEvents === false ? Infinity : perRow * rows;
1223
1247
  const cap = typeof dayMaxEvents === "number" && dayMaxEvents > 0 ? Math.min(dayMaxEvents, fits) : fits;
@@ -1226,7 +1250,7 @@ var DayGridView = class {
1226
1250
  w.days.forEach((day, i) => {
1227
1251
  const cell = w.dots[i];
1228
1252
  const items = this.itemsOn(day);
1229
- const shown = items.length <= cap ? items.length : cap >= fits ? Math.max(0, cap - 2) : cap;
1253
+ const shown = items.length <= cap ? items.length : cap >= fits ? Math.max(1, cap - (chips ? 1 : 2)) : Math.max(1, cap);
1230
1254
  cell.pool.begin();
1231
1255
  const els = items.slice(0, shown).map((n) => cell.pool.place(n.id, { n, day, isStart: n.startDay >= day, isEnd: n.endDay <= day + 1, variant: "indicator", mirror: false }));
1232
1256
  cell.pool.end();
@@ -1343,6 +1367,7 @@ var ListView = class {
1343
1367
  const leadItems = [];
1344
1368
  const inLead = /* @__PURE__ */ new Set();
1345
1369
  const keep = /* @__PURE__ */ new Set();
1370
+ let itemCount = 0;
1346
1371
  let prev = null;
1347
1372
  for (const day of days) {
1348
1373
  let items = this.itemsOn(day);
@@ -1356,7 +1381,7 @@ var ListView = class {
1356
1381
  return false;
1357
1382
  });
1358
1383
  }
1359
- if (!items.length && !agenda) continue;
1384
+ if (!items.length && !agenda && opts.listDayCount !== 1) continue;
1360
1385
  keep.add(day);
1361
1386
  let section = this.sections.get(day);
1362
1387
  let pool = this.pools.get(day);
@@ -1375,6 +1400,7 @@ var ListView = class {
1375
1400
  this.pools.set(day, pool);
1376
1401
  }
1377
1402
  section.classList.toggle("is-empty", !items.length);
1403
+ itemCount += items.length;
1378
1404
  this.fill(pool, items, () => day);
1379
1405
  if (prev ? prev.nextSibling !== section : this.body.firstChild !== section) {
1380
1406
  if (prev) prev.after(section);
@@ -1390,13 +1416,14 @@ var ListView = class {
1390
1416
  this.sections.delete(day);
1391
1417
  }
1392
1418
  this.renderLead(leadItems);
1393
- let empty = this.body.querySelector(":scope > .tnc-list-empty");
1394
- if (!keep.size && !leadItems.length) {
1419
+ let empty = this.body.querySelector(".tnc-list-empty");
1420
+ if (!itemCount && !leadItems.length && (!agenda || opts.listEmptyContent)) {
1395
1421
  if (!empty) {
1396
1422
  empty = h(doc, "div", "tnc-list-empty", { role: "listitem" }, ctx.text.noItems);
1397
1423
  const content = ctx.opts.listEmptyContent?.();
1398
1424
  if (content) empty.replaceChildren(content);
1399
- this.body.append(empty);
1425
+ if (this.sections.size === 1) this.sections.values().next().value.append(empty);
1426
+ else this.body.prepend(empty);
1400
1427
  }
1401
1428
  } else empty?.remove();
1402
1429
  this.ctx.strip?.draw();
@@ -1454,7 +1481,7 @@ var ListView = class {
1454
1481
  const section = day === this.ctx.days[0] && this.lead ? this.lead.section : this.sections.get(day);
1455
1482
  if (!section) return;
1456
1483
  this.ctx.strip?.measure();
1457
- section.scrollIntoView({ block: "start" });
1484
+ this.scroller.scrollTop += section.getBoundingClientRect().top - this.scroller.getBoundingClientRect().top;
1458
1485
  this.ctx.strip?.setFocus(day);
1459
1486
  this.pinned = true;
1460
1487
  }
@@ -1543,8 +1570,7 @@ var DateStrip = class {
1543
1570
  head.append(this.title, this.todayBtn, prev, next, this.slot);
1544
1571
  this.dows = h(doc, "div", "tnc-strip-dows", { "aria-hidden": "true" });
1545
1572
  this.grid = h(doc, "div", "tnc-strip-grid", { role: "group" });
1546
- const handle = h(doc, "button", "tnc-strip-handle", { type: "button", tabindex: "-1", "aria-hidden": "true" });
1547
- handle.addEventListener("click", () => this.setExpanded(!this.expanded));
1573
+ const handle = h(doc, "div", "tnc-strip-handle", { "aria-hidden": "true" });
1548
1574
  this.el.append(head, this.dows, this.grid, handle);
1549
1575
  this.grid.addEventListener("click", this.onClick);
1550
1576
  this.grid.addEventListener("keydown", this.onKeyDown);
@@ -2123,8 +2149,15 @@ var TimeGridView = class {
2123
2149
  link.classList.toggle("tnc-more--collision", collapse);
2124
2150
  link.textContent = `+${rec.count}`;
2125
2151
  if (collapse) {
2126
- link.textContent = ctx.text.appointments(rec.count);
2127
- link.setAttribute("aria-label", link.textContent);
2152
+ const items = placements.filter((placement) => placement.cluster === cluster).map((placement) => placement.item.item.title);
2153
+ const count = ctx.text.appointments(rec.count);
2154
+ const interval = `${ctx.fmt.time(rec.start)} \u2013 ${ctx.fmt.time(rec.end)}`;
2155
+ link.replaceChildren(
2156
+ h(ctx.doc, "span", "tnc-collision-count", null, String(rec.count)),
2157
+ h(ctx.doc, "span", "tnc-collision-range", null, interval),
2158
+ h(ctx.doc, "span", "tnc-collision-title", { "aria-hidden": "true" }, items.join(" \xB7 "))
2159
+ );
2160
+ link.setAttribute("aria-label", `${count}, ${interval}: ${items.join(", ")}`);
2128
2161
  link.title = link.getAttribute("aria-label");
2129
2162
  } else {
2130
2163
  link.removeAttribute("aria-label");
@@ -2133,6 +2166,7 @@ var TimeGridView = class {
2133
2166
  const cols = maxStack;
2134
2167
  setStyle(link, {
2135
2168
  top: top(rec.start),
2169
+ height: collapse ? pct((rec.end - rec.start) / this.minutes) : "",
2136
2170
  ...inLane(lane, rec.split, (cols - 1) / cols, 1 / cols)
2137
2171
  });
2138
2172
  }
@@ -2199,12 +2233,10 @@ var TimeGridView = class {
2199
2233
  const r2 = cells[i2].getBoundingClientRect();
2200
2234
  if (x >= r2.left && x < r2.right) return i2;
2201
2235
  }
2202
- const firstR = cells[0]?.getBoundingClientRect();
2203
- const lastR = cells[cells.length - 1]?.getBoundingClientRect();
2204
- if (firstR && x < firstR.left) return 0;
2205
- if (lastR && x >= lastR.right) return cells.length - 1;
2206
2236
  return -1;
2207
2237
  };
2238
+ const bounds = this.scroller.getBoundingClientRect();
2239
+ if (x < bounds.left || x >= bounds.right || y < bounds.top || y >= bounds.bottom) return null;
2208
2240
  if (this.allDayRow) {
2209
2241
  const r2 = this.allDayRow.getBoundingClientRect();
2210
2242
  if (y >= r2.top && y < r2.bottom || !this.body && y >= r2.top) {
@@ -2927,6 +2959,14 @@ ${this.ctx.text.more(items.length - 8)}` : ""}` : label;
2927
2959
  setScroll(s) {
2928
2960
  this.scroller.scrollTop = s.top;
2929
2961
  }
2962
+ revealDay(day) {
2963
+ const month = this.cells.find((cell) => cell.day === day)?.el.closest(".tnc-ym");
2964
+ if (!month) return;
2965
+ const bounds = month.getBoundingClientRect();
2966
+ const viewport = this.scroller.getBoundingClientRect();
2967
+ if (bounds.top < viewport.top) this.scroller.scrollTop += bounds.top - viewport.top;
2968
+ else if (bounds.bottom > viewport.bottom) this.scroller.scrollTop += bounds.bottom - viewport.bottom;
2969
+ }
2930
2970
  elementsFor(_id) {
2931
2971
  return [];
2932
2972
  }
@@ -3006,6 +3046,11 @@ var Calendar = class {
3006
3046
  conflicts = /* @__PURE__ */ new Map();
3007
3047
  slot = { min: 0, max: MINUTES_PER_DAY, duration: 30, snap: 30, scroll: 480 };
3008
3048
  draggingId = null;
3049
+ listDragPreview = null;
3050
+ dragSources = [];
3051
+ dragScrollFrame = null;
3052
+ dragScrollOrigins = /* @__PURE__ */ new Map();
3053
+ itemWriteStates = /* @__PURE__ */ new Map();
3009
3054
  dragRow = null;
3010
3055
  strip = null;
3011
3056
  stripSize = null;
@@ -3019,6 +3064,7 @@ var Calendar = class {
3019
3064
  session = null;
3020
3065
  selection = null;
3021
3066
  suppressClick = false;
3067
+ cancelledPointerId = null;
3022
3068
  selectedIds = /* @__PURE__ */ new Set();
3023
3069
  popover = null;
3024
3070
  timer = null;
@@ -3036,6 +3082,7 @@ var Calendar = class {
3036
3082
  this.positionMore();
3037
3083
  });
3038
3084
  this.resizeObs.observe(this.root);
3085
+ this.resizeObs.observe(this.toolbarEl);
3039
3086
  }
3040
3087
  }
3041
3088
  onAdoptCheck = () => {
@@ -3162,7 +3209,12 @@ var Calendar = class {
3162
3209
  gotoDate(date) {
3163
3210
  const day = this.toDay(date);
3164
3211
  if (day === null) return;
3212
+ const sameMonth = this.opts.view === "month" && startOfMonth(day) === startOfMonth(this.anchor);
3165
3213
  this.anchor = day;
3214
+ if (sameMonth) {
3215
+ this.view = { ...this.view, anchor: day };
3216
+ return;
3217
+ }
3166
3218
  this.rebuild(true);
3167
3219
  }
3168
3220
  prev() {
@@ -3308,7 +3360,8 @@ var Calendar = class {
3308
3360
  out.push(...classList(this.opts.dayClassNames?.(this.dayArg(day))));
3309
3361
  return out;
3310
3362
  }
3311
- isEditable(n, what) {
3363
+ isEditable(n, what, presentation = false) {
3364
+ if (!presentation && this.itemWriteStates.get(n.id) === "pending") return false;
3312
3365
  if (n.id === EXTERNAL_ID || n.item.preview || this.opts.view === "year") return false;
3313
3366
  if (this.opts.view === "list" && (!this.opts.listAgenda || what === "duration")) return false;
3314
3367
  const it = n.item;
@@ -3348,13 +3401,14 @@ var Calendar = class {
3348
3401
  if (preview || n.id === EXTERNAL_ID) el.classList.add("is-mirror");
3349
3402
  if (this.conflicts.has(n.id)) el.classList.add("is-conflict");
3350
3403
  const indicator = variant === "indicator";
3351
- if (!preview && !indicator && (this.isEditable(n, "start") || this.isEditable(n, "duration"))) el.classList.add("is-draggable");
3404
+ if (!preview && !indicator && (this.isEditable(n, "start", true) || this.isEditable(n, "duration", true))) el.classList.add("is-draggable");
3352
3405
  if (this.draggingId === n.id) el.classList.add("is-dragging");
3353
3406
  if (this.selectedIds.has(n.id) && !preview) el.classList.add("is-selected");
3354
3407
  el.classList.add(...classList(it.className));
3355
3408
  const arg = this.itemArg(el, meta);
3356
3409
  el.classList.add(...classList(this.opts.itemClassNames?.(arg)));
3357
3410
  el.dataset.id = n.id;
3411
+ this.applyItemWriteState(el, n.id);
3358
3412
  if (it.kind) el.dataset.kind = it.kind;
3359
3413
  const color = it.color ?? it.borderColor;
3360
3414
  if (color) el.style.setProperty("--tnc-c", color);
@@ -3371,6 +3425,7 @@ var Calendar = class {
3371
3425
  const custom = variant === "list" ? this.opts.listItemContent?.(arg) : null;
3372
3426
  const content = indicator ? null : custom ?? this.opts.itemContent?.(arg);
3373
3427
  if (indicator) {
3428
+ if (this.opts.monthDisplay === "chips") el.append(h(this.doc, "span", "tnc-item-title", null, it.title));
3374
3429
  } else if (content != null) {
3375
3430
  const wrap = h(this.doc, "div", "tnc-item-main");
3376
3431
  wrap.append(content);
@@ -3383,6 +3438,7 @@ var Calendar = class {
3383
3438
  return el;
3384
3439
  }
3385
3440
  refreshItemEl(el, meta) {
3441
+ this.applyItemWriteState(el, meta.n.id);
3386
3442
  const time = el.querySelector(":scope .tnc-item-time");
3387
3443
  if (time) this.fillTime(time, meta.n, meta.variant, meta.day);
3388
3444
  el.classList.toggle("is-dragging", this.draggingId === meta.n.id);
@@ -3565,6 +3621,7 @@ var Calendar = class {
3565
3621
  this.current.scrollToTime(this.slot.scroll);
3566
3622
  this.current.revealDay(this.anchor);
3567
3623
  } else if (this.current instanceof TimelineView) this.current.scrollToDay(this.anchor);
3624
+ else if (this.current instanceof YearView) this.current.revealDay(this.anchor);
3568
3625
  if (rangeChanged) this.opts.onRangeChange?.(this.view);
3569
3626
  }
3570
3627
  /** Sets the visible days and view info from the anchor. */
@@ -3590,9 +3647,11 @@ var Calendar = class {
3590
3647
  }
3591
3648
  /** The range, run on to the end of the strip's days so their dots are right. */
3592
3649
  dataRangeFor(range) {
3593
- const end = this.strip ? Math.max(range.endDay, this.strip.window().end) : range.endDay;
3594
- if (end === range.endDay) return range;
3595
- return { ...range, endDay: end, end: new Date(toInstant(end, 0, this.zone)) };
3650
+ const window = this.strip?.window();
3651
+ const start = window ? Math.min(range.startDay, window.start) : range.startDay;
3652
+ const end = window ? Math.max(range.endDay, window.end) : range.endDay;
3653
+ if (start === range.startDay && end === range.endDay) return range;
3654
+ return { ...range, startDay: start, start: new Date(toInstant(start, 0, this.zone)), endDay: end, end: new Date(toInstant(end, 0, this.zone)) };
3596
3655
  }
3597
3656
  /** Adds, updates or removes the date strip for the current view. */
3598
3657
  syncStrip(navigated) {
@@ -3639,20 +3698,24 @@ var Calendar = class {
3639
3698
  stripShown() {
3640
3699
  if (!this.strip || this.rebuilding || !this.view) return;
3641
3700
  const next = this.dataRangeFor(this.view.range);
3642
- if (next.endDay === this.view.dataRange.endDay) return;
3701
+ if (next.startDay === this.view.dataRange.startDay && next.endDay === this.view.dataRange.endDay) return;
3643
3702
  this.view = { ...this.view, dataRange: next };
3644
3703
  this.opts.onRangeChange?.(this.view);
3645
3704
  }
3646
3705
  /** What is under a point, the date strip included. */
3647
3706
  hitAt(x, y) {
3707
+ const target = this.doc.elementFromPoint(x, y);
3708
+ if (!target || !this.root.contains(target)) {
3709
+ this.strip?.markTarget(null);
3710
+ return null;
3711
+ }
3648
3712
  const strip = this.strip;
3649
3713
  if (strip) {
3650
- const el = this.doc.elementFromPoint(x, y);
3651
- const day = el ? strip.dayAt(el) : null;
3714
+ const day = strip.dayAt(target);
3652
3715
  strip.markTarget(day !== null && this.draggingId ? day : null);
3653
3716
  if (day !== null) return { kind: "day", day };
3654
3717
  }
3655
- return this.current?.hit(x, y) ?? null;
3718
+ return this.current?.el.contains(target) ? this.current.hit(x, y) : null;
3656
3719
  }
3657
3720
  agendaList() {
3658
3721
  return this.opts.view === "list" && this.opts.listAgenda && this.current instanceof ListView ? this.current : null;
@@ -3766,11 +3829,13 @@ var Calendar = class {
3766
3829
  buttons.style.display = "flex";
3767
3830
  select.style.display = "none";
3768
3831
  const startWidth = Array.from(start.children).reduce((width, child) => width + child.scrollWidth, 0);
3769
- const compact = mode === "select" || mode === "auto" && startWidth + buttons.scrollWidth + 32 > this.root.clientWidth;
3832
+ const availableWidth = Math.min(this.root.clientWidth, this.toolbarEl.clientWidth);
3833
+ const compact = mode === "select" || mode === "auto" && startWidth + buttons.scrollWidth + 32 > availableWidth;
3770
3834
  buttons.hidden = compact;
3771
3835
  select.hidden = !compact;
3772
3836
  buttons.style.display = compact ? "none" : "flex";
3773
3837
  select.style.display = compact ? "" : "none";
3838
+ select.parentElement?.classList.toggle("tnc-toolbar-end--compact", compact);
3774
3839
  }
3775
3840
  applyChange(change) {
3776
3841
  if (!change.added.length && !change.updated.length && !change.removed.length && !change.days.size && !change.spans) return;
@@ -3835,7 +3900,7 @@ var Calendar = class {
3835
3900
  const s = wallTime(n.startMs, this.zone);
3836
3901
  const start = this.fmt.time(s.minute, true);
3837
3902
  if (variant === "dot" || variant === "bar" || variant === "indicator") return { start: s.day === day ? start : "", end: "" };
3838
- if (n.startMs === n.endMs) return { start, end: "" };
3903
+ if (n.startMs === n.endMs || n.item.timeDisplay === "start") return { start, end: "" };
3839
3904
  return { start, end: this.fmt.time(wallTime(n.endMs, this.zone).minute, true) };
3840
3905
  }
3841
3906
  fillTime(el, n, variant, day) {
@@ -3851,7 +3916,8 @@ var Calendar = class {
3851
3916
  }
3852
3917
  const s = wallTime(n.startMs, this.zone);
3853
3918
  const e = wallTime(n.endMs, this.zone);
3854
- return `${n.item.title}, ${this.fmt.longDate(s.day)} ${this.fmt.time(s.minute)} \u2013 ${this.fmt.time(e.minute)}`;
3919
+ const start = `${n.item.title}, ${this.fmt.longDate(s.day)} ${this.fmt.time(s.minute)}`;
3920
+ return n.item.timeDisplay === "start" || n.startMs === n.endMs ? start : `${start} \u2013 ${this.fmt.time(e.minute)}`;
3855
3921
  }
3856
3922
  defaultContent(meta) {
3857
3923
  const { n, variant, day } = meta;
@@ -3879,7 +3945,7 @@ var Calendar = class {
3879
3945
  addHandles(el, meta) {
3880
3946
  const { n, variant } = meta;
3881
3947
  const ports = variant === "timeline" && !meta.mirror && n.id !== EXTERNAL_ID && !!this.opts.timeline?.onLink;
3882
- if (variant === "dot" || !this.isEditable(n, "duration")) {
3948
+ if (variant === "dot" || !this.isEditable(n, "duration", true)) {
3883
3949
  if (ports) this.addPorts(el, n);
3884
3950
  return;
3885
3951
  }
@@ -4007,6 +4073,10 @@ var Calendar = class {
4007
4073
  return el && this.root.contains(el) && this.meta.has(el) && !this.meta.get(el)?.n.item.preview ? el : null;
4008
4074
  }
4009
4075
  onDocPointerDown = (ev) => {
4076
+ if (this.cancelledPointerId !== null) {
4077
+ this.cancelledPointerId = null;
4078
+ this.listenDoc(false);
4079
+ }
4010
4080
  const t = ev.target;
4011
4081
  if (this.popover && !this.popover.el.contains(t) && !this.popover.anchor.contains(t)) this.closeMore();
4012
4082
  if (this.selection && !this.root.contains(t)) this.unselect();
@@ -4014,6 +4084,7 @@ var Calendar = class {
4014
4084
  onPointerDown = (ev) => {
4015
4085
  if (ev.button !== 0 || this.session) return;
4016
4086
  const target = ev.target;
4087
+ if (target.closest(".tnc-dg--compact .tnc-dg-day")) return;
4017
4088
  if (target.closest("button, a, input, select, textarea, .tnc-toolbar, .tnc-port, [data-tnc-no-drag]")) {
4018
4089
  if (!target.closest(".tnc-item")) return;
4019
4090
  if (target.closest(".tnc-port, [data-tnc-no-drag], input, select, textarea")) return;
@@ -4046,7 +4117,7 @@ var Calendar = class {
4046
4117
  y0: ev.clientY,
4047
4118
  active: false,
4048
4119
  origin,
4049
- grab: overflowOrigin && this.current instanceof TimeGridView && !origin?.allDay ? { kind: "time", day: overflowOrigin.day, minute: overflowOrigin.minute } : this.current?.hit(ev.clientX, ev.clientY) ?? null,
4120
+ grab: overflowOrigin ? this.current instanceof TimeGridView && !origin?.allDay ? { kind: "time", day: overflowOrigin.day, minute: overflowOrigin.minute } : { kind: "day", day: this.meta.get(itemEl).day } : this.current?.hit(ev.clientX, ev.clientY) ?? null,
4050
4121
  proposal: null,
4051
4122
  denied: false,
4052
4123
  timer: null,
@@ -4071,16 +4142,79 @@ var Calendar = class {
4071
4142
  };
4072
4143
  activate(s) {
4073
4144
  s.active = true;
4145
+ if (s.lastEvent) this.leaveHover(s.lastEvent);
4074
4146
  if (s.timer !== null) this.win.clearTimeout(s.timer);
4075
4147
  s.timer = null;
4076
4148
  if (s.origin) {
4077
4149
  this.draggingId = s.origin.id;
4078
- for (const el of this.elementsFor(s.origin.id)) el.classList.add("is-dragging");
4150
+ for (const el of this.elementsFor(s.origin.id)) {
4151
+ if (this.opts.view !== "list" && s.mode === "move") {
4152
+ const source = el.cloneNode(true);
4153
+ source.classList.remove("is-selected");
4154
+ source.classList.add("tnc-drag-source");
4155
+ source.setAttribute("aria-hidden", "true");
4156
+ source.inert = true;
4157
+ for (const node of [source, ...source.querySelectorAll("[id], [data-id], [data-task-path]")]) {
4158
+ node.removeAttribute("id");
4159
+ node.removeAttribute("data-id");
4160
+ node.removeAttribute("data-task-path");
4161
+ }
4162
+ el.before(source);
4163
+ this.dragSources.push(source);
4164
+ }
4165
+ if (this.opts.view === "list" && !this.listDragPreview && s.mode === "move") {
4166
+ const bounds = el.getBoundingClientRect();
4167
+ const preview = el.cloneNode(true);
4168
+ preview.classList.add("tnc-drag-preview", "is-dragging");
4169
+ preview.removeAttribute("data-id");
4170
+ preview.removeAttribute("data-task-path");
4171
+ preview.setAttribute("aria-hidden", "true");
4172
+ preview.inert = true;
4173
+ preview.style.width = `${bounds.width}px`;
4174
+ this.root.append(preview);
4175
+ this.listDragPreview = { element: preview, offsetX: s.x0 - bounds.left, offsetY: s.y0 - bounds.top, width: bounds.width, height: bounds.height };
4176
+ }
4177
+ el.classList.add("is-dragging");
4178
+ }
4079
4179
  }
4080
4180
  this.closeMore();
4081
4181
  if (s.mode !== "select") this.unselectSilently();
4082
4182
  this.root.classList.add("tnc-dragging", `tnc-dragging-${s.mode}`);
4083
4183
  if (s.lastEvent) this.track(s, s.lastEvent);
4184
+ this.startDragScroll();
4185
+ }
4186
+ startDragScroll() {
4187
+ if (this.dragScrollFrame !== null) return;
4188
+ const tick = () => {
4189
+ this.dragScrollFrame = null;
4190
+ const session = this.session;
4191
+ const pointer = session?.lastEvent;
4192
+ if (!session?.active || !pointer) return;
4193
+ const origin = session.origin ? this.elementsFor(session.origin.id)[0] : this.viewHost;
4194
+ const underPointer = this.doc.elementFromPoint(pointer.clientX, pointer.clientY);
4195
+ let moved = false;
4196
+ if (origin && underPointer && this.root.contains(underPointer)) {
4197
+ for (const axis of ["x", "y"]) {
4198
+ const scroller = scrollerOf(origin, axis);
4199
+ if (!scroller) continue;
4200
+ const viewport = scroller === this.doc.scrollingElement ? { left: 0, top: 0, right: this.win.innerWidth, bottom: this.win.innerHeight } : scroller.getBoundingClientRect();
4201
+ const coordinate = axis === "x" ? pointer.clientX : pointer.clientY;
4202
+ const lower = Math.max(0, axis === "x" ? viewport.left : viewport.top);
4203
+ const upper = Math.min(axis === "x" ? this.win.innerWidth : this.win.innerHeight, axis === "x" ? viewport.right : viewport.bottom);
4204
+ if (coordinate < lower || coordinate > upper) continue;
4205
+ const delta = coordinate < lower + 32 ? -Math.min(12, (lower + 32 - coordinate) / 2) : coordinate > upper - 32 ? Math.min(12, (coordinate - upper + 32) / 2) : 0;
4206
+ if (!delta) continue;
4207
+ if (!this.dragScrollOrigins.has(scroller)) this.dragScrollOrigins.set(scroller, { top: scroller.scrollTop, left: scroller.scrollLeft });
4208
+ const before = axis === "x" ? scroller.scrollLeft : scroller.scrollTop;
4209
+ if (axis === "x") scroller.scrollLeft += delta;
4210
+ else scroller.scrollTop += delta;
4211
+ moved ||= before !== (axis === "x" ? scroller.scrollLeft : scroller.scrollTop);
4212
+ }
4213
+ }
4214
+ if (moved) this.track(session, pointer);
4215
+ this.dragScrollFrame = this.win.requestAnimationFrame(tick);
4216
+ };
4217
+ this.dragScrollFrame = this.win.requestAnimationFrame(tick);
4084
4218
  }
4085
4219
  onTouchMove = (ev) => {
4086
4220
  if (this.session?.active) ev.preventDefault();
@@ -4110,6 +4244,12 @@ var Calendar = class {
4110
4244
  this.track(s, ev);
4111
4245
  };
4112
4246
  onPointerUp = (ev) => {
4247
+ if (ev.pointerId === this.cancelledPointerId) {
4248
+ this.cancelledPointerId = null;
4249
+ this.suppressNextClick();
4250
+ this.listenDoc(false);
4251
+ return;
4252
+ }
4113
4253
  const s = this.session;
4114
4254
  if (!s || ev.pointerId !== s.pointerId) return;
4115
4255
  if (!s.active) {
@@ -4122,28 +4262,49 @@ var Calendar = class {
4122
4262
  return;
4123
4263
  }
4124
4264
  this.suppressNextClick();
4265
+ this.track(s, ev);
4125
4266
  this.endSession(ev, false);
4126
4267
  };
4127
4268
  onPointerCancel = (ev) => {
4269
+ if (ev.pointerId === this.cancelledPointerId) {
4270
+ this.cancelledPointerId = null;
4271
+ this.listenDoc(false);
4272
+ }
4128
4273
  if (this.session && ev.pointerId === this.session.pointerId) this.endSession(null, true);
4129
4274
  };
4130
4275
  onDocKeyDown = (ev) => {
4131
4276
  if (ev.key === "Escape" && this.session) {
4132
4277
  ev.preventDefault();
4133
4278
  ev.stopPropagation();
4279
+ const pointerId = this.session.active ? this.session.pointerId : null;
4134
4280
  this.endSession(null, true);
4281
+ if (pointerId !== null) {
4282
+ this.cancelledPointerId = pointerId;
4283
+ this.listenDoc(true);
4284
+ }
4135
4285
  }
4136
4286
  };
4137
4287
  /** Proposal for the current pointer position. */
4138
4288
  track(s, ev) {
4289
+ if (this.listDragPreview) {
4290
+ const preview = this.listDragPreview;
4291
+ const bounds = this.root.getBoundingClientRect();
4292
+ preview.element.style.left = `${Math.max(8, Math.min(this.win.innerWidth - preview.width - 8, ev.clientX - preview.offsetX)) - bounds.left}px`;
4293
+ preview.element.style.top = `${Math.max(8, Math.min(this.win.innerHeight - preview.height - 8, ev.clientY - preview.offsetY)) - bounds.top}px`;
4294
+ }
4139
4295
  const hit = this.hitAt(ev.clientX, ev.clientY);
4140
4296
  if (s.mode === "external") {
4141
4297
  this.trackExternal(s, hit);
4142
4298
  return;
4143
4299
  }
4144
4300
  if (!s.grab) s.grab = hit;
4145
- if (!hit || !s.grab) return;
4301
+ if (!hit || !s.grab) {
4302
+ this.invalidateProposal(s);
4303
+ return;
4304
+ }
4146
4305
  if (s.mode === "select") {
4306
+ s.denied = false;
4307
+ this.root.classList.remove("tnc-drop-denied");
4147
4308
  const sel = this.selectionFor(s.grab, hit);
4148
4309
  if (!sel) return;
4149
4310
  this.selection = sel;
@@ -4152,25 +4313,40 @@ var Calendar = class {
4152
4313
  }
4153
4314
  const origin = s.origin;
4154
4315
  const p = s.mode === "move" ? this.moveProposal(origin, s.grab, hit) : this.resizeProposal(origin, hit, s.mode === "resize-start");
4155
- if (!p) return;
4316
+ if (!p) {
4317
+ this.invalidateProposal(s);
4318
+ return;
4319
+ }
4156
4320
  const allowed = this.opts.canDrop?.({ item: origin.item, start: new Date(p.startMs), end: new Date(p.endMs), allDay: p.allDay, rowId: p.rowId }) ?? true;
4157
4321
  s.denied = !allowed;
4158
4322
  this.root.classList.toggle("tnc-drop-denied", !allowed);
4159
- if (!allowed || sameProposal(p, s.proposal)) return;
4323
+ if (!allowed) {
4324
+ this.invalidateProposal(s);
4325
+ return;
4326
+ }
4327
+ if (sameProposal(p, s.proposal)) return;
4160
4328
  s.proposal = p;
4161
4329
  this.dragRow = p.rowId !== void 0 ? { id: origin.id, rowId: p.rowId } : null;
4162
- this.applyChange(this.store.upsert([this.withProposal(origin, p)]));
4330
+ if (this.opts.view !== "list") this.applyChange(this.store.upsert([this.withProposal(origin, p)]));
4331
+ }
4332
+ invalidateProposal(s) {
4333
+ const hadProposal = s.proposal !== null;
4334
+ s.proposal = null;
4335
+ s.hit = null;
4336
+ s.denied = true;
4337
+ this.dragRow = null;
4338
+ this.root.classList.add("tnc-drop-denied");
4339
+ this.strip?.markTarget(null);
4340
+ this.unselectSilently();
4341
+ if (s.mode === "external") this.applyChange(this.store.remove([EXTERNAL_ID]));
4342
+ else if (hadProposal && s.origin) this.applyChange(this.store.upsert([s.origin.item]));
4163
4343
  }
4164
4344
  trackExternal(s, raw) {
4165
4345
  const src = s.source;
4166
4346
  const hit = raw?.kind === "timeline" && raw.area !== "track" && raw.area !== "header" ? null : raw;
4167
4347
  s.hit = hit;
4168
4348
  if (!hit) {
4169
- if (s.proposal) {
4170
- s.proposal = null;
4171
- this.applyChange(this.store.remove([EXTERNAL_ID]));
4172
- }
4173
- this.unselectSilently();
4349
+ this.invalidateProposal(s);
4174
4350
  return;
4175
4351
  }
4176
4352
  const dur = src.durationMinutes ?? this.opts.defaultTimedMinutes;
@@ -4189,6 +4365,10 @@ var Calendar = class {
4189
4365
  const allowed = this.opts.canDrop?.({ item: this.externalItem(src, p), start: new Date(p.startMs), end: new Date(p.endMs), allDay: p.allDay, rowId: p.rowId }) ?? true;
4190
4366
  s.denied = !allowed;
4191
4367
  this.root.classList.toggle("tnc-drop-denied", !allowed);
4368
+ if (!allowed) {
4369
+ this.invalidateProposal(s);
4370
+ return;
4371
+ }
4192
4372
  if (sameProposal(p, s.proposal)) return;
4193
4373
  s.proposal = p;
4194
4374
  if (hit.kind === "timeline" && p.rowId === void 0) {
@@ -4381,7 +4561,18 @@ var Calendar = class {
4381
4561
  endSession(ev, cancel) {
4382
4562
  const s = this.session;
4383
4563
  if (!s) return;
4564
+ if (this.dragScrollFrame !== null) this.win.cancelAnimationFrame(this.dragScrollFrame);
4565
+ this.dragScrollFrame = null;
4566
+ if (cancel || s.denied) for (const [scroller, position] of this.dragScrollOrigins) {
4567
+ scroller.scrollTop = position.top;
4568
+ scroller.scrollLeft = position.left;
4569
+ }
4570
+ this.dragScrollOrigins.clear();
4384
4571
  this.session = null;
4572
+ this.listDragPreview?.element.remove();
4573
+ this.listDragPreview = null;
4574
+ for (const source of this.dragSources) source.remove();
4575
+ this.dragSources = [];
4385
4576
  if (s.timer !== null) this.win.clearTimeout(s.timer);
4386
4577
  this.listenDoc(false);
4387
4578
  this.root.classList.remove(
@@ -4450,10 +4641,16 @@ var Calendar = class {
4450
4641
  revert();
4451
4642
  return;
4452
4643
  }
4644
+ if (this.opts.view === "list") this.applyChange(this.store.upsert([this.withProposal(origin, p)]));
4453
4645
  this.flush();
4454
4646
  this.commit(s.mode === "move" ? "drop" : "resize", origin, p, ev, revert);
4455
4647
  }
4456
4648
  commit(kind, origin, p, ev, revert) {
4649
+ let reverted = false;
4650
+ const mark = (state) => {
4651
+ this.itemWriteStates.set(origin.id, state);
4652
+ for (const element of this.elementsFor(origin.id)) this.applyItemWriteState(element, origin.id);
4653
+ };
4457
4654
  const arg = {
4458
4655
  item: origin.item,
4459
4656
  start: new Date(p.startMs),
@@ -4469,19 +4666,32 @@ var Calendar = class {
4469
4666
  jsEvent: ev,
4470
4667
  source: ev && "key" in ev ? "keyboard" : "pointer",
4471
4668
  view: this.view,
4472
- revert
4669
+ revert: () => {
4670
+ reverted = true;
4671
+ revert();
4672
+ mark("failed");
4673
+ }
4473
4674
  };
4474
4675
  const handler = kind === "drop" ? this.opts.onItemDrop : this.opts.onItemResize;
4475
4676
  try {
4677
+ mark("pending");
4476
4678
  const res = handler?.(arg);
4477
4679
  if (res && typeof res.then === "function") {
4478
- Promise.resolve(res).catch(() => revert());
4479
- }
4680
+ void Promise.resolve(res).then(() => {
4681
+ if (!reverted) mark("changed");
4682
+ }, () => arg.revert());
4683
+ } else if (!reverted) mark("changed");
4480
4684
  } catch (err) {
4481
- revert();
4685
+ arg.revert();
4482
4686
  throw err;
4483
4687
  }
4484
4688
  }
4689
+ applyItemWriteState(element, id) {
4690
+ const state = this.itemWriteStates.get(id);
4691
+ for (const name of ["pending", "failed", "changed"]) element.classList.toggle(`is-write-${name}`, state === name);
4692
+ if (state === "pending") element.setAttribute("aria-busy", "true");
4693
+ else element.removeAttribute("aria-busy");
4694
+ }
4485
4695
  /* --------------------------------------------------------- click / keys */
4486
4696
  /** Runs in the capture phase so the click ending a drag never reaches content inside the calendar, React portals included. */
4487
4697
  onClickCapture = (ev) => {
@@ -4491,6 +4701,12 @@ var Calendar = class {
4491
4701
  ev.preventDefault();
4492
4702
  };
4493
4703
  onClick = (ev) => {
4704
+ const compactDay = ev.target.closest?.(".tnc-dg--compact .tnc-dg-day");
4705
+ if (compactDay && this.root.contains(compactDay) && !ev.target.closest(".tnc-navlink")) {
4706
+ const day = parseDay(compactDay.dataset.date ?? "");
4707
+ if (day !== null) this.openMore(day, compactDay, ev);
4708
+ return;
4709
+ }
4494
4710
  const yearDay = ev.target.closest?.(".tnc-ym-day");
4495
4711
  if (yearDay && this.root.contains(yearDay)) {
4496
4712
  const day = parseDay(yearDay.dataset.date ?? "");
@@ -4579,7 +4795,8 @@ var Calendar = class {
4579
4795
  const day = parseDay(target.dataset.date ?? "");
4580
4796
  if (day !== null) {
4581
4797
  ev.preventDefault();
4582
- this.clickSlot({ kind: "day", day }, ev, true);
4798
+ if (target.closest(".tnc-dg--compact")) this.openMore(day, target, ev);
4799
+ else this.clickSlot({ kind: "day", day }, ev, true);
4583
4800
  }
4584
4801
  return;
4585
4802
  }
@@ -4667,9 +4884,13 @@ var Calendar = class {
4667
4884
  const dt = ev.dataTransfer;
4668
4885
  if (!dt || !this.opts.acceptNativeDrop?.(dt) || !this.current) return;
4669
4886
  ev.preventDefault();
4887
+ const hit = this.hitAt(ev.clientX, ev.clientY);
4888
+ if (!hit || hit.kind === "timeline" && hit.area !== "track" && hit.area !== "header") {
4889
+ dt.dropEffect = "none";
4890
+ this.unselectSilently();
4891
+ return;
4892
+ }
4670
4893
  dt.dropEffect = "copy";
4671
- const hit = this.current.hit(ev.clientX, ev.clientY);
4672
- if (!hit || hit.kind === "timeline" && hit.area !== "track" && hit.area !== "header") return;
4673
4894
  const sel = this.selectionFor(hit, hit);
4674
4895
  if (!sel) return;
4675
4896
  this.selection = sel;
@@ -4683,7 +4904,7 @@ var Calendar = class {
4683
4904
  const dt = ev.dataTransfer;
4684
4905
  if (!dt || !this.opts.acceptNativeDrop?.(dt) || !this.current) return;
4685
4906
  ev.preventDefault();
4686
- const hit = this.current.hit(ev.clientX, ev.clientY);
4907
+ const hit = this.hitAt(ev.clientX, ev.clientY);
4687
4908
  this.unselectSilently();
4688
4909
  if (!hit || hit.kind === "timeline" && hit.area !== "track" && hit.area !== "header") return;
4689
4910
  const sel = this.selectionFor(hit, hit);
@@ -4714,7 +4935,7 @@ function scrollerOf(el, axis) {
4714
4935
  return doc.scrollingElement;
4715
4936
  }
4716
4937
  function rangeMoved(prev, next) {
4717
- return !prev || prev.type !== next.type || prev.range.startDay !== next.range.startDay || prev.range.endDay !== next.range.endDay || prev.dataRange.endDay !== next.dataRange.endDay;
4938
+ return !prev || prev.type !== next.type || prev.range.startDay !== next.range.startDay || prev.range.endDay !== next.range.endDay || prev.dataRange.startDay !== next.dataRange.startDay || prev.dataRange.endDay !== next.dataRange.endDay;
4718
4939
  }
4719
4940
 
4720
4941
  // src/react/index.tsx