@tasknotes/calendar 0.1.10 → 0.1.12

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.12
4
+
5
+ - Hosts can pass a `tooltip(el, text)` option to show hover tooltips their own way (Obsidian's `setTooltip`, say). Without it, tooltips stay as `title` attributes.
6
+
7
+ ## 0.1.11
8
+
9
+ - Hour-long timed items keep their time in narrow columns. Only single-row short items drop the time label when narrow; in the stacked layout the title is clamped to the lines left above the time (callumalpass/tasknotes#2528).
10
+ - Back-to-back timed items no longer count as overlapping: an item starting exactly where a short item ends stays in the same column instead of going side by side or into a collision stack, and the short item is drawn at its real height (callumalpass/tasknotes#2529).
11
+
3
12
  ## 0.1.10
4
13
 
5
14
  - Freeze Month source layout during a move and draw an inert target placeholder; preserve timed drag width across collision layouts.
package/dist/calendar.css CHANGED
@@ -293,8 +293,42 @@
293
293
  .tnc-item--timed.is-short .tnc-item-time {
294
294
  flex: none;
295
295
  }
296
- @container (max-height: 50px) and (max-width: 239px) {
297
- .tnc-item--timed .tnc-item-time {
296
+ /* Stacked layout: the time keeps its line and the title gets the lines left over
297
+ (11px text: n title lines need 13.75 * (n + 1) + 1px). Single-row short items drop the time when narrow. */
298
+ .tnc-item--timed:not(.is-short) .tnc-item-title {
299
+ flex: 0 1 auto;
300
+ min-height: 0;
301
+ }
302
+ .tnc-item--timed:not(.is-short) .tnc-item-time {
303
+ flex: none;
304
+ }
305
+ @container (max-height: 97.2px) {
306
+ .tnc-item--timed:not(.is-short) .tnc-item-title {
307
+ -webkit-line-clamp: 5;
308
+ }
309
+ }
310
+ @container (max-height: 83.4px) {
311
+ .tnc-item--timed:not(.is-short) .tnc-item-title {
312
+ -webkit-line-clamp: 4;
313
+ }
314
+ }
315
+ @container (max-height: 69.7px) {
316
+ .tnc-item--timed:not(.is-short) .tnc-item-title {
317
+ -webkit-line-clamp: 3;
318
+ }
319
+ }
320
+ @container (max-height: 55.9px) {
321
+ .tnc-item--timed:not(.is-short) .tnc-item-title {
322
+ -webkit-line-clamp: 2;
323
+ }
324
+ }
325
+ @container (max-height: 42.2px) {
326
+ .tnc-item--timed:not(.is-short) .tnc-item-title {
327
+ -webkit-line-clamp: 1;
328
+ }
329
+ }
330
+ @container (max-width: 239px) {
331
+ .tnc-item--timed.is-short .tnc-item-time {
298
332
  display: none;
299
333
  }
300
334
  }
package/dist/cjs/core.cjs CHANGED
@@ -490,43 +490,44 @@ function segmentInWindow(n, day, minMinute, maxMinute, zone) {
490
490
  };
491
491
  }
492
492
  var defaultOrder = (a, b) => a.startMs - b.startMs || b.endMs - b.startMs - (a.endMs - a.startMs) || Number(b.allDay) - Number(a.allDay) || a.item.title.localeCompare(b.item.title);
493
+ function collides(a, b, minMinutes) {
494
+ if (b.startMin < a.endMin) return true;
495
+ if (b.startMin === a.endMin && a.endMin > a.startMin) return false;
496
+ return b.startMin < a.startMin + minMinutes;
497
+ }
498
+ function overlaps(a, b, minMinutes) {
499
+ return a.startMin <= b.startMin ? collides(a, b, minMinutes) : collides(b, a, minMinutes);
500
+ }
493
501
  function packTimed(segs, minMinutes, order = defaultOrder) {
494
- const sorted = segs.map((s) => ({ ...s, end: Math.max(s.endMin, s.startMin + minMinutes) })).sort((a, b) => a.startMin - b.startMin || b.end - a.end || order(a.item, b.item));
502
+ const sorted = [...segs].sort((a, b) => a.startMin - b.startMin || Math.max(b.endMin, b.startMin + minMinutes) - Math.max(a.endMin, a.startMin + minMinutes) || order(a.item, b.item));
495
503
  const out = [];
496
504
  let cluster = [];
497
- let colEnds = [];
498
- let clusterEnd = -Infinity;
505
+ let colLast = [];
499
506
  let clusterIndex = 0;
500
507
  const flush = () => {
501
508
  if (cluster.length) clusterIndex++;
502
- const cols = colEnds.length;
509
+ const cols = colLast.length;
503
510
  for (const p of cluster) {
504
511
  p.cols = cols;
505
512
  let span = 1;
506
- while (p.col + span < cols && !cluster.some((o) => o.col === p.col + span && o.startMin < p.end && p.startMin < o.end)) span++;
513
+ while (p.col + span < cols && !cluster.some((o) => o.col === p.col + span && overlaps(o, p, minMinutes))) span++;
507
514
  p.span = span;
508
515
  }
509
516
  cluster = [];
510
- colEnds = [];
517
+ colLast = [];
511
518
  };
512
519
  for (const s of sorted) {
513
- if (s.startMin >= clusterEnd) {
514
- flush();
515
- clusterEnd = -Infinity;
516
- }
517
- let col = colEnds.findIndex((end) => end <= s.startMin);
518
- if (col < 0) {
519
- col = colEnds.length;
520
- colEnds.push(0);
521
- }
522
- colEnds[col] = s.end;
520
+ if (!cluster.some((o) => collides(o, s, minMinutes))) flush();
521
+ let col = colLast.findIndex((last) => !collides(last, s, minMinutes));
522
+ if (col < 0) col = colLast.length;
523
523
  const p = { ...s, col, cols: 1, span: 1, cluster: clusterIndex, split: false };
524
+ colLast[col] = p;
524
525
  cluster.push(p);
525
526
  out.push(p);
526
- clusterEnd = Math.max(clusterEnd, s.end);
527
527
  }
528
528
  flush();
529
- for (const p of out) delete p.end;
529
+ const starts = new Set(out.map((p) => p.startMin));
530
+ for (const p of out) if (p.endMin > p.startMin && p.startMin + minMinutes > p.endMin && starts.has(p.endMin)) p.abutted = true;
530
531
  return out;
531
532
  }
532
533
  function layoutTimedDay(items, day, zone, minMinutes, order, window = [0, MINUTES_PER_DAY]) {
@@ -548,15 +549,8 @@ function layoutTimedDay(items, day, zone, minMinutes, order, window = [0, MINUTE
548
549
  }
549
550
  function markSplit(lane, other, minMinutes) {
550
551
  if (!lane.length || !other.length) return;
551
- const end = (p) => Math.max(p.endMin, p.startMin + minMinutes);
552
- const ranges = /* @__PURE__ */ new Map();
553
- for (const p of lane) {
554
- const r = ranges.get(p.cluster);
555
- if (r) r[1] = Math.max(r[1], end(p));
556
- else ranges.set(p.cluster, [p.startMin, end(p)]);
557
- }
558
552
  const split = /* @__PURE__ */ new Set();
559
- for (const [cluster, [s, e]] of ranges) if (other.some((o) => o.startMin < e && s < end(o))) split.add(cluster);
553
+ for (const p of lane) if (!split.has(p.cluster) && other.some((o) => overlaps(p, o, minMinutes))) split.add(p.cluster);
560
554
  for (const p of lane) p.split = split.has(p.cluster);
561
555
  }
562
556
  function layoutRow(items, days, order = defaultOrder) {
@@ -502,43 +502,44 @@ function segmentInWindow(n, day, minMinute, maxMinute, zone) {
502
502
  };
503
503
  }
504
504
  var defaultOrder = (a, b) => a.startMs - b.startMs || b.endMs - b.startMs - (a.endMs - a.startMs) || Number(b.allDay) - Number(a.allDay) || a.item.title.localeCompare(b.item.title);
505
+ function collides(a, b, minMinutes) {
506
+ if (b.startMin < a.endMin) return true;
507
+ if (b.startMin === a.endMin && a.endMin > a.startMin) return false;
508
+ return b.startMin < a.startMin + minMinutes;
509
+ }
510
+ function overlaps(a, b, minMinutes) {
511
+ return a.startMin <= b.startMin ? collides(a, b, minMinutes) : collides(b, a, minMinutes);
512
+ }
505
513
  function packTimed(segs, minMinutes, order = defaultOrder) {
506
- const sorted = segs.map((s) => ({ ...s, end: Math.max(s.endMin, s.startMin + minMinutes) })).sort((a, b) => a.startMin - b.startMin || b.end - a.end || order(a.item, b.item));
514
+ const sorted = [...segs].sort((a, b) => a.startMin - b.startMin || Math.max(b.endMin, b.startMin + minMinutes) - Math.max(a.endMin, a.startMin + minMinutes) || order(a.item, b.item));
507
515
  const out = [];
508
516
  let cluster = [];
509
- let colEnds = [];
510
- let clusterEnd = -Infinity;
517
+ let colLast = [];
511
518
  let clusterIndex = 0;
512
519
  const flush = () => {
513
520
  if (cluster.length) clusterIndex++;
514
- const cols = colEnds.length;
521
+ const cols = colLast.length;
515
522
  for (const p of cluster) {
516
523
  p.cols = cols;
517
524
  let span = 1;
518
- while (p.col + span < cols && !cluster.some((o) => o.col === p.col + span && o.startMin < p.end && p.startMin < o.end)) span++;
525
+ while (p.col + span < cols && !cluster.some((o) => o.col === p.col + span && overlaps(o, p, minMinutes))) span++;
519
526
  p.span = span;
520
527
  }
521
528
  cluster = [];
522
- colEnds = [];
529
+ colLast = [];
523
530
  };
524
531
  for (const s of sorted) {
525
- if (s.startMin >= clusterEnd) {
526
- flush();
527
- clusterEnd = -Infinity;
528
- }
529
- let col = colEnds.findIndex((end) => end <= s.startMin);
530
- if (col < 0) {
531
- col = colEnds.length;
532
- colEnds.push(0);
533
- }
534
- colEnds[col] = s.end;
532
+ if (!cluster.some((o) => collides(o, s, minMinutes))) flush();
533
+ let col = colLast.findIndex((last) => !collides(last, s, minMinutes));
534
+ if (col < 0) col = colLast.length;
535
535
  const p = { ...s, col, cols: 1, span: 1, cluster: clusterIndex, split: false };
536
+ colLast[col] = p;
536
537
  cluster.push(p);
537
538
  out.push(p);
538
- clusterEnd = Math.max(clusterEnd, s.end);
539
539
  }
540
540
  flush();
541
- for (const p of out) delete p.end;
541
+ const starts = new Set(out.map((p) => p.startMin));
542
+ for (const p of out) if (p.endMin > p.startMin && p.startMin + minMinutes > p.endMin && starts.has(p.endMin)) p.abutted = true;
542
543
  return out;
543
544
  }
544
545
  function layoutTimedDay(items, day, zone, minMinutes, order, window = [0, MINUTES_PER_DAY]) {
@@ -560,15 +561,8 @@ function layoutTimedDay(items, day, zone, minMinutes, order, window = [0, MINUTE
560
561
  }
561
562
  function markSplit(lane, other, minMinutes) {
562
563
  if (!lane.length || !other.length) return;
563
- const end = (p) => Math.max(p.endMin, p.startMin + minMinutes);
564
- const ranges = /* @__PURE__ */ new Map();
565
- for (const p of lane) {
566
- const r = ranges.get(p.cluster);
567
- if (r) r[1] = Math.max(r[1], end(p));
568
- else ranges.set(p.cluster, [p.startMin, end(p)]);
569
- }
570
564
  const split = /* @__PURE__ */ new Set();
571
- for (const [cluster, [s, e]] of ranges) if (other.some((o) => o.startMin < e && s < end(o))) split.add(cluster);
565
+ for (const p of lane) if (!split.has(p.cluster) && other.some((o) => overlaps(p, o, minMinutes))) split.add(p.cluster);
572
566
  for (const p of lane) p.split = split.has(p.cluster);
573
567
  }
574
568
  function layoutRow(items, days, order = defaultOrder) {
@@ -1063,6 +1057,7 @@ var LAYOUT_KEYS = /* @__PURE__ */ new Set([
1063
1057
  "text",
1064
1058
  "timeline",
1065
1059
  "editable",
1060
+ "tooltip",
1066
1061
  "itemContent",
1067
1062
  "itemClassNames",
1068
1063
  "listItemContent",
@@ -1699,8 +1694,10 @@ var DateStrip = class {
1699
1694
  const head = h(doc, "div", "tnc-strip-head");
1700
1695
  this.title = h(doc, "button", "tnc-strip-title", { type: "button", "aria-expanded": "false" });
1701
1696
  this.title.addEventListener("click", () => this.setExpanded(!this.expanded));
1702
- const prev = h(doc, "button", "tnc-strip-nav tnc-strip-prev", { type: "button", "aria-label": text.prev, title: text.prev }, "\u2039");
1703
- const next = h(doc, "button", "tnc-strip-nav tnc-strip-next", { type: "button", "aria-label": text.next, title: text.next }, "\u203A");
1697
+ const prev = h(doc, "button", "tnc-strip-nav tnc-strip-prev", { type: "button", "aria-label": text.prev }, "\u2039");
1698
+ const next = h(doc, "button", "tnc-strip-nav tnc-strip-next", { type: "button", "aria-label": text.next }, "\u203A");
1699
+ ctx.tooltip(prev, text.prev);
1700
+ ctx.tooltip(next, text.next);
1704
1701
  prev.addEventListener("click", () => this.step(-1));
1705
1702
  next.addEventListener("click", () => this.step(1));
1706
1703
  this.todayBtn = h(doc, "button", "tnc-strip-today", { type: "button" }, text.today);
@@ -2292,7 +2289,7 @@ var TimeGridView = class {
2292
2289
  el.classList.toggle("is-short", short);
2293
2290
  setStyle(el, {
2294
2291
  top: top(p.startMin),
2295
- height: `max(${height(p)}, ${opts.eventMinHeight}px)`,
2292
+ height: p.abutted ? height(p) : `max(${height(p)}, ${opts.eventMinHeight}px)`,
2296
2293
  ...inLane(lane, p.split, left, width),
2297
2294
  "--tnc-event-readable-width": pct(span / cols / width),
2298
2295
  "z-index": String(p.col + 1)
@@ -2324,10 +2321,10 @@ var TimeGridView = class {
2324
2321
  h(ctx.doc, "span", "tnc-collision-title", { "aria-hidden": "true" }, items.join(" \xB7 "))
2325
2322
  );
2326
2323
  link.setAttribute("aria-label", `${count}, ${interval}: ${items.join(", ")}`);
2327
- link.title = link.getAttribute("aria-label");
2324
+ ctx.tooltip(link, link.getAttribute("aria-label"));
2328
2325
  } else {
2326
+ ctx.tooltip(link, "");
2329
2327
  link.removeAttribute("aria-label");
2330
- link.removeAttribute("title");
2331
2328
  }
2332
2329
  const cols = maxStack;
2333
2330
  setStyle(link, {
@@ -3105,9 +3102,9 @@ var YearView = class {
3105
3102
  );
3106
3103
  const label = this.ctx.fmt.longDate(c.day);
3107
3104
  c.el.setAttribute("aria-label", items.length ? `${label}, ${this.ctx.text.itemCount(items.length)}` : label);
3108
- c.el.title = items.length ? `${label}
3105
+ this.ctx.tooltip(c.el, items.length ? `${label}
3109
3106
  ${items.slice(0, 8).map((n) => `\u2022 ${n.item.title}`).join("\n")}${items.length > 8 ? `
3110
- ${this.ctx.text.more(items.length - 8)}` : ""}` : label;
3107
+ ${this.ctx.text.more(items.length - 8)}` : ""}` : label);
3111
3108
  }
3112
3109
  }
3113
3110
  showSelection(_sel) {
@@ -3542,6 +3539,11 @@ var Calendar = class {
3542
3539
  const specific = what === "start" ? it.startEditable : it.durationEditable;
3543
3540
  return specific ?? base;
3544
3541
  }
3542
+ tooltip(el, text) {
3543
+ if (this.opts.tooltip) this.opts.tooltip(el, text);
3544
+ else if (text) el.title = text;
3545
+ else el.removeAttribute("title");
3546
+ }
3545
3547
  navLink(day, label, cls) {
3546
3548
  const btn = h(this.doc, "button", `tnc-navlink ${cls}`, { type: "button", "data-nav-date": formatDay(day) }, label);
3547
3549
  btn.addEventListener("click", (ev) => {
@@ -3943,7 +3945,8 @@ var Calendar = class {
3943
3945
  const cfg = tb === true ? {} : tb;
3944
3946
  const views = cfg.views ?? ["month", "week", "days", "day", "list", "year"];
3945
3947
  const btn = (label, hint, cls, onClick, pressed) => {
3946
- const b = h(this.doc, "button", `tnc-btn ${cls}`, { type: "button", title: hint, "aria-label": hint }, label);
3948
+ const b = h(this.doc, "button", `tnc-btn ${cls}`, { type: "button", "aria-label": hint }, label);
3949
+ this.tooltip(b, hint);
3947
3950
  if (pressed !== void 0) b.setAttribute("aria-pressed", String(pressed));
3948
3951
  b.addEventListener("click", onClick);
3949
3952
  return b;
@@ -4143,7 +4146,9 @@ var Calendar = class {
4143
4146
  addPorts(el, n) {
4144
4147
  const edges = n.item.kind === "milestone" ? ["finish"] : ["start", "finish"];
4145
4148
  for (const edge of edges) {
4146
- el.append(h(this.doc, "span", `tnc-port tnc-port-${edge}`, { "aria-hidden": "true", "data-edge": edge, title: "Drag to link" }));
4149
+ const port = h(this.doc, "span", `tnc-port tnc-port-${edge}`, { "aria-hidden": "true", "data-edge": edge });
4150
+ this.tooltip(port, "Drag to link");
4151
+ el.append(port);
4147
4152
  }
4148
4153
  }
4149
4154
  /* ============================================================== popover */
@@ -806,6 +806,7 @@ var LAYOUT_KEYS = /* @__PURE__ */ new Set([
806
806
  "text",
807
807
  "timeline",
808
808
  "editable",
809
+ "tooltip",
809
810
  "itemContent",
810
811
  "itemClassNames",
811
812
  "listItemContent",
@@ -838,43 +839,44 @@ function segmentInWindow(n, day, minMinute, maxMinute, zone) {
838
839
  };
839
840
  }
840
841
  var defaultOrder = (a, b) => a.startMs - b.startMs || b.endMs - b.startMs - (a.endMs - a.startMs) || Number(b.allDay) - Number(a.allDay) || a.item.title.localeCompare(b.item.title);
842
+ function collides(a, b, minMinutes) {
843
+ if (b.startMin < a.endMin) return true;
844
+ if (b.startMin === a.endMin && a.endMin > a.startMin) return false;
845
+ return b.startMin < a.startMin + minMinutes;
846
+ }
847
+ function overlaps(a, b, minMinutes) {
848
+ return a.startMin <= b.startMin ? collides(a, b, minMinutes) : collides(b, a, minMinutes);
849
+ }
841
850
  function packTimed(segs, minMinutes, order = defaultOrder) {
842
- const sorted = segs.map((s) => ({ ...s, end: Math.max(s.endMin, s.startMin + minMinutes) })).sort((a, b) => a.startMin - b.startMin || b.end - a.end || order(a.item, b.item));
851
+ const sorted = [...segs].sort((a, b) => a.startMin - b.startMin || Math.max(b.endMin, b.startMin + minMinutes) - Math.max(a.endMin, a.startMin + minMinutes) || order(a.item, b.item));
843
852
  const out = [];
844
853
  let cluster = [];
845
- let colEnds = [];
846
- let clusterEnd = -Infinity;
854
+ let colLast = [];
847
855
  let clusterIndex = 0;
848
856
  const flush = () => {
849
857
  if (cluster.length) clusterIndex++;
850
- const cols = colEnds.length;
858
+ const cols = colLast.length;
851
859
  for (const p of cluster) {
852
860
  p.cols = cols;
853
861
  let span = 1;
854
- while (p.col + span < cols && !cluster.some((o) => o.col === p.col + span && o.startMin < p.end && p.startMin < o.end)) span++;
862
+ while (p.col + span < cols && !cluster.some((o) => o.col === p.col + span && overlaps(o, p, minMinutes))) span++;
855
863
  p.span = span;
856
864
  }
857
865
  cluster = [];
858
- colEnds = [];
866
+ colLast = [];
859
867
  };
860
868
  for (const s of sorted) {
861
- if (s.startMin >= clusterEnd) {
862
- flush();
863
- clusterEnd = -Infinity;
864
- }
865
- let col = colEnds.findIndex((end) => end <= s.startMin);
866
- if (col < 0) {
867
- col = colEnds.length;
868
- colEnds.push(0);
869
- }
870
- colEnds[col] = s.end;
869
+ if (!cluster.some((o) => collides(o, s, minMinutes))) flush();
870
+ let col = colLast.findIndex((last) => !collides(last, s, minMinutes));
871
+ if (col < 0) col = colLast.length;
871
872
  const p = { ...s, col, cols: 1, span: 1, cluster: clusterIndex, split: false };
873
+ colLast[col] = p;
872
874
  cluster.push(p);
873
875
  out.push(p);
874
- clusterEnd = Math.max(clusterEnd, s.end);
875
876
  }
876
877
  flush();
877
- for (const p of out) delete p.end;
878
+ const starts = new Set(out.map((p) => p.startMin));
879
+ for (const p of out) if (p.endMin > p.startMin && p.startMin + minMinutes > p.endMin && starts.has(p.endMin)) p.abutted = true;
878
880
  return out;
879
881
  }
880
882
  function layoutTimedDay(items, day, zone, minMinutes, order, window = [0, MINUTES_PER_DAY]) {
@@ -896,15 +898,8 @@ function layoutTimedDay(items, day, zone, minMinutes, order, window = [0, MINUTE
896
898
  }
897
899
  function markSplit(lane, other, minMinutes) {
898
900
  if (!lane.length || !other.length) return;
899
- const end = (p) => Math.max(p.endMin, p.startMin + minMinutes);
900
- const ranges = /* @__PURE__ */ new Map();
901
- for (const p of lane) {
902
- const r = ranges.get(p.cluster);
903
- if (r) r[1] = Math.max(r[1], end(p));
904
- else ranges.set(p.cluster, [p.startMin, end(p)]);
905
- }
906
901
  const split = /* @__PURE__ */ new Set();
907
- for (const [cluster, [s, e]] of ranges) if (other.some((o) => o.startMin < e && s < end(o))) split.add(cluster);
902
+ for (const p of lane) if (!split.has(p.cluster) && other.some((o) => overlaps(p, o, minMinutes))) split.add(p.cluster);
908
903
  for (const p of lane) p.split = split.has(p.cluster);
909
904
  }
910
905
  function layoutRow(items, days, order = defaultOrder) {
@@ -1607,8 +1602,10 @@ var DateStrip = class {
1607
1602
  const head = h(doc, "div", "tnc-strip-head");
1608
1603
  this.title = h(doc, "button", "tnc-strip-title", { type: "button", "aria-expanded": "false" });
1609
1604
  this.title.addEventListener("click", () => this.setExpanded(!this.expanded));
1610
- const prev = h(doc, "button", "tnc-strip-nav tnc-strip-prev", { type: "button", "aria-label": text.prev, title: text.prev }, "\u2039");
1611
- const next = h(doc, "button", "tnc-strip-nav tnc-strip-next", { type: "button", "aria-label": text.next, title: text.next }, "\u203A");
1605
+ const prev = h(doc, "button", "tnc-strip-nav tnc-strip-prev", { type: "button", "aria-label": text.prev }, "\u2039");
1606
+ const next = h(doc, "button", "tnc-strip-nav tnc-strip-next", { type: "button", "aria-label": text.next }, "\u203A");
1607
+ ctx.tooltip(prev, text.prev);
1608
+ ctx.tooltip(next, text.next);
1612
1609
  prev.addEventListener("click", () => this.step(-1));
1613
1610
  next.addEventListener("click", () => this.step(1));
1614
1611
  this.todayBtn = h(doc, "button", "tnc-strip-today", { type: "button" }, text.today);
@@ -2200,7 +2197,7 @@ var TimeGridView = class {
2200
2197
  el.classList.toggle("is-short", short);
2201
2198
  setStyle(el, {
2202
2199
  top: top(p.startMin),
2203
- height: `max(${height(p)}, ${opts.eventMinHeight}px)`,
2200
+ height: p.abutted ? height(p) : `max(${height(p)}, ${opts.eventMinHeight}px)`,
2204
2201
  ...inLane(lane, p.split, left, width),
2205
2202
  "--tnc-event-readable-width": pct(span / cols / width),
2206
2203
  "z-index": String(p.col + 1)
@@ -2232,10 +2229,10 @@ var TimeGridView = class {
2232
2229
  h(ctx.doc, "span", "tnc-collision-title", { "aria-hidden": "true" }, items.join(" \xB7 "))
2233
2230
  );
2234
2231
  link.setAttribute("aria-label", `${count}, ${interval}: ${items.join(", ")}`);
2235
- link.title = link.getAttribute("aria-label");
2232
+ ctx.tooltip(link, link.getAttribute("aria-label"));
2236
2233
  } else {
2234
+ ctx.tooltip(link, "");
2237
2235
  link.removeAttribute("aria-label");
2238
- link.removeAttribute("title");
2239
2236
  }
2240
2237
  const cols = maxStack;
2241
2238
  setStyle(link, {
@@ -3013,9 +3010,9 @@ var YearView = class {
3013
3010
  );
3014
3011
  const label = this.ctx.fmt.longDate(c.day);
3015
3012
  c.el.setAttribute("aria-label", items.length ? `${label}, ${this.ctx.text.itemCount(items.length)}` : label);
3016
- c.el.title = items.length ? `${label}
3013
+ this.ctx.tooltip(c.el, items.length ? `${label}
3017
3014
  ${items.slice(0, 8).map((n) => `\u2022 ${n.item.title}`).join("\n")}${items.length > 8 ? `
3018
- ${this.ctx.text.more(items.length - 8)}` : ""}` : label;
3015
+ ${this.ctx.text.more(items.length - 8)}` : ""}` : label);
3019
3016
  }
3020
3017
  }
3021
3018
  showSelection(_sel) {
@@ -3450,6 +3447,11 @@ var Calendar = class {
3450
3447
  const specific = what === "start" ? it.startEditable : it.durationEditable;
3451
3448
  return specific ?? base;
3452
3449
  }
3450
+ tooltip(el, text) {
3451
+ if (this.opts.tooltip) this.opts.tooltip(el, text);
3452
+ else if (text) el.title = text;
3453
+ else el.removeAttribute("title");
3454
+ }
3453
3455
  navLink(day, label, cls) {
3454
3456
  const btn = h(this.doc, "button", `tnc-navlink ${cls}`, { type: "button", "data-nav-date": formatDay(day) }, label);
3455
3457
  btn.addEventListener("click", (ev) => {
@@ -3851,7 +3853,8 @@ var Calendar = class {
3851
3853
  const cfg = tb === true ? {} : tb;
3852
3854
  const views = cfg.views ?? ["month", "week", "days", "day", "list", "year"];
3853
3855
  const btn = (label, hint, cls, onClick, pressed) => {
3854
- const b = h(this.doc, "button", `tnc-btn ${cls}`, { type: "button", title: hint, "aria-label": hint }, label);
3856
+ const b = h(this.doc, "button", `tnc-btn ${cls}`, { type: "button", "aria-label": hint }, label);
3857
+ this.tooltip(b, hint);
3855
3858
  if (pressed !== void 0) b.setAttribute("aria-pressed", String(pressed));
3856
3859
  b.addEventListener("click", onClick);
3857
3860
  return b;
@@ -4051,7 +4054,9 @@ var Calendar = class {
4051
4054
  addPorts(el, n) {
4052
4055
  const edges = n.item.kind === "milestone" ? ["finish"] : ["start", "finish"];
4053
4056
  for (const edge of edges) {
4054
- el.append(h(this.doc, "span", `tnc-port tnc-port-${edge}`, { "aria-hidden": "true", "data-edge": edge, title: "Drag to link" }));
4057
+ const port = h(this.doc, "span", `tnc-port tnc-port-${edge}`, { "aria-hidden": "true", "data-edge": edge });
4058
+ this.tooltip(port, "Drag to link");
4059
+ el.append(port);
4055
4060
  }
4056
4061
  }
4057
4062
  /* ============================================================== popover */
@@ -27,7 +27,7 @@ import {
27
27
  wallTime,
28
28
  weekdayOf,
29
29
  ymdOfDay
30
- } from "./chunk-66A6EMCU.js";
30
+ } from "./chunk-JWORVABV.js";
31
31
 
32
32
  // src/dom/format.ts
33
33
  var Formatter = class {
@@ -262,6 +262,7 @@ var LAYOUT_KEYS = /* @__PURE__ */ new Set([
262
262
  "text",
263
263
  "timeline",
264
264
  "editable",
265
+ "tooltip",
265
266
  "itemContent",
266
267
  "itemClassNames",
267
268
  "listItemContent",
@@ -984,8 +985,10 @@ var DateStrip = class {
984
985
  const head = h(doc, "div", "tnc-strip-head");
985
986
  this.title = h(doc, "button", "tnc-strip-title", { type: "button", "aria-expanded": "false" });
986
987
  this.title.addEventListener("click", () => this.setExpanded(!this.expanded));
987
- const prev = h(doc, "button", "tnc-strip-nav tnc-strip-prev", { type: "button", "aria-label": text.prev, title: text.prev }, "\u2039");
988
- const next = h(doc, "button", "tnc-strip-nav tnc-strip-next", { type: "button", "aria-label": text.next, title: text.next }, "\u203A");
988
+ const prev = h(doc, "button", "tnc-strip-nav tnc-strip-prev", { type: "button", "aria-label": text.prev }, "\u2039");
989
+ const next = h(doc, "button", "tnc-strip-nav tnc-strip-next", { type: "button", "aria-label": text.next }, "\u203A");
990
+ ctx.tooltip(prev, text.prev);
991
+ ctx.tooltip(next, text.next);
989
992
  prev.addEventListener("click", () => this.step(-1));
990
993
  next.addEventListener("click", () => this.step(1));
991
994
  this.todayBtn = h(doc, "button", "tnc-strip-today", { type: "button" }, text.today);
@@ -1577,7 +1580,7 @@ var TimeGridView = class {
1577
1580
  el.classList.toggle("is-short", short);
1578
1581
  setStyle(el, {
1579
1582
  top: top(p.startMin),
1580
- height: `max(${height(p)}, ${opts.eventMinHeight}px)`,
1583
+ height: p.abutted ? height(p) : `max(${height(p)}, ${opts.eventMinHeight}px)`,
1581
1584
  ...inLane(lane, p.split, left, width),
1582
1585
  "--tnc-event-readable-width": pct(span / cols / width),
1583
1586
  "z-index": String(p.col + 1)
@@ -1609,10 +1612,10 @@ var TimeGridView = class {
1609
1612
  h(ctx.doc, "span", "tnc-collision-title", { "aria-hidden": "true" }, items.join(" \xB7 "))
1610
1613
  );
1611
1614
  link.setAttribute("aria-label", `${count}, ${interval}: ${items.join(", ")}`);
1612
- link.title = link.getAttribute("aria-label");
1615
+ ctx.tooltip(link, link.getAttribute("aria-label"));
1613
1616
  } else {
1617
+ ctx.tooltip(link, "");
1614
1618
  link.removeAttribute("aria-label");
1615
- link.removeAttribute("title");
1616
1619
  }
1617
1620
  const cols = maxStack;
1618
1621
  setStyle(link, {
@@ -2390,9 +2393,9 @@ var YearView = class {
2390
2393
  );
2391
2394
  const label = this.ctx.fmt.longDate(c.day);
2392
2395
  c.el.setAttribute("aria-label", items.length ? `${label}, ${this.ctx.text.itemCount(items.length)}` : label);
2393
- c.el.title = items.length ? `${label}
2396
+ this.ctx.tooltip(c.el, items.length ? `${label}
2394
2397
  ${items.slice(0, 8).map((n) => `\u2022 ${n.item.title}`).join("\n")}${items.length > 8 ? `
2395
- ${this.ctx.text.more(items.length - 8)}` : ""}` : label;
2398
+ ${this.ctx.text.more(items.length - 8)}` : ""}` : label);
2396
2399
  }
2397
2400
  }
2398
2401
  showSelection(_sel) {
@@ -2827,6 +2830,11 @@ var Calendar = class {
2827
2830
  const specific = what === "start" ? it.startEditable : it.durationEditable;
2828
2831
  return specific ?? base;
2829
2832
  }
2833
+ tooltip(el, text) {
2834
+ if (this.opts.tooltip) this.opts.tooltip(el, text);
2835
+ else if (text) el.title = text;
2836
+ else el.removeAttribute("title");
2837
+ }
2830
2838
  navLink(day, label, cls) {
2831
2839
  const btn = h(this.doc, "button", `tnc-navlink ${cls}`, { type: "button", "data-nav-date": formatDay(day) }, label);
2832
2840
  btn.addEventListener("click", (ev) => {
@@ -3228,7 +3236,8 @@ var Calendar = class {
3228
3236
  const cfg = tb === true ? {} : tb;
3229
3237
  const views = cfg.views ?? ["month", "week", "days", "day", "list", "year"];
3230
3238
  const btn = (label, hint, cls, onClick, pressed) => {
3231
- const b = h(this.doc, "button", `tnc-btn ${cls}`, { type: "button", title: hint, "aria-label": hint }, label);
3239
+ const b = h(this.doc, "button", `tnc-btn ${cls}`, { type: "button", "aria-label": hint }, label);
3240
+ this.tooltip(b, hint);
3232
3241
  if (pressed !== void 0) b.setAttribute("aria-pressed", String(pressed));
3233
3242
  b.addEventListener("click", onClick);
3234
3243
  return b;
@@ -3428,7 +3437,9 @@ var Calendar = class {
3428
3437
  addPorts(el, n) {
3429
3438
  const edges = n.item.kind === "milestone" ? ["finish"] : ["start", "finish"];
3430
3439
  for (const edge of edges) {
3431
- el.append(h(this.doc, "span", `tnc-port tnc-port-${edge}`, { "aria-hidden": "true", "data-edge": edge, title: "Drag to link" }));
3440
+ const port = h(this.doc, "span", `tnc-port tnc-port-${edge}`, { "aria-hidden": "true", "data-edge": edge });
3441
+ this.tooltip(port, "Drag to link");
3442
+ el.append(port);
3432
3443
  }
3433
3444
  }
3434
3445
  /* ============================================================== popover */
@@ -425,43 +425,44 @@ function segmentInWindow(n, day, minMinute, maxMinute, zone) {
425
425
  };
426
426
  }
427
427
  var defaultOrder = (a, b) => a.startMs - b.startMs || b.endMs - b.startMs - (a.endMs - a.startMs) || Number(b.allDay) - Number(a.allDay) || a.item.title.localeCompare(b.item.title);
428
+ function collides(a, b, minMinutes) {
429
+ if (b.startMin < a.endMin) return true;
430
+ if (b.startMin === a.endMin && a.endMin > a.startMin) return false;
431
+ return b.startMin < a.startMin + minMinutes;
432
+ }
433
+ function overlaps(a, b, minMinutes) {
434
+ return a.startMin <= b.startMin ? collides(a, b, minMinutes) : collides(b, a, minMinutes);
435
+ }
428
436
  function packTimed(segs, minMinutes, order = defaultOrder) {
429
- const sorted = segs.map((s) => ({ ...s, end: Math.max(s.endMin, s.startMin + minMinutes) })).sort((a, b) => a.startMin - b.startMin || b.end - a.end || order(a.item, b.item));
437
+ const sorted = [...segs].sort((a, b) => a.startMin - b.startMin || Math.max(b.endMin, b.startMin + minMinutes) - Math.max(a.endMin, a.startMin + minMinutes) || order(a.item, b.item));
430
438
  const out = [];
431
439
  let cluster = [];
432
- let colEnds = [];
433
- let clusterEnd = -Infinity;
440
+ let colLast = [];
434
441
  let clusterIndex = 0;
435
442
  const flush = () => {
436
443
  if (cluster.length) clusterIndex++;
437
- const cols = colEnds.length;
444
+ const cols = colLast.length;
438
445
  for (const p of cluster) {
439
446
  p.cols = cols;
440
447
  let span = 1;
441
- while (p.col + span < cols && !cluster.some((o) => o.col === p.col + span && o.startMin < p.end && p.startMin < o.end)) span++;
448
+ while (p.col + span < cols && !cluster.some((o) => o.col === p.col + span && overlaps(o, p, minMinutes))) span++;
442
449
  p.span = span;
443
450
  }
444
451
  cluster = [];
445
- colEnds = [];
452
+ colLast = [];
446
453
  };
447
454
  for (const s of sorted) {
448
- if (s.startMin >= clusterEnd) {
449
- flush();
450
- clusterEnd = -Infinity;
451
- }
452
- let col = colEnds.findIndex((end) => end <= s.startMin);
453
- if (col < 0) {
454
- col = colEnds.length;
455
- colEnds.push(0);
456
- }
457
- colEnds[col] = s.end;
455
+ if (!cluster.some((o) => collides(o, s, minMinutes))) flush();
456
+ let col = colLast.findIndex((last) => !collides(last, s, minMinutes));
457
+ if (col < 0) col = colLast.length;
458
458
  const p = { ...s, col, cols: 1, span: 1, cluster: clusterIndex, split: false };
459
+ colLast[col] = p;
459
460
  cluster.push(p);
460
461
  out.push(p);
461
- clusterEnd = Math.max(clusterEnd, s.end);
462
462
  }
463
463
  flush();
464
- for (const p of out) delete p.end;
464
+ const starts = new Set(out.map((p) => p.startMin));
465
+ for (const p of out) if (p.endMin > p.startMin && p.startMin + minMinutes > p.endMin && starts.has(p.endMin)) p.abutted = true;
465
466
  return out;
466
467
  }
467
468
  function layoutTimedDay(items, day, zone, minMinutes, order, window = [0, MINUTES_PER_DAY]) {
@@ -483,15 +484,8 @@ function layoutTimedDay(items, day, zone, minMinutes, order, window = [0, MINUTE
483
484
  }
484
485
  function markSplit(lane, other, minMinutes) {
485
486
  if (!lane.length || !other.length) return;
486
- const end = (p) => Math.max(p.endMin, p.startMin + minMinutes);
487
- const ranges = /* @__PURE__ */ new Map();
488
- for (const p of lane) {
489
- const r = ranges.get(p.cluster);
490
- if (r) r[1] = Math.max(r[1], end(p));
491
- else ranges.set(p.cluster, [p.startMin, end(p)]);
492
- }
493
487
  const split = /* @__PURE__ */ new Set();
494
- for (const [cluster, [s, e]] of ranges) if (other.some((o) => o.startMin < e && s < end(o))) split.add(cluster);
488
+ for (const p of lane) if (!split.has(p.cluster) && other.some((o) => overlaps(p, o, minMinutes))) split.add(p.cluster);
495
489
  for (const p of lane) p.split = split.has(p.cluster);
496
490
  }
497
491
  function layoutRow(items, days, order = defaultOrder) {
package/dist/esm/core.js CHANGED
@@ -40,7 +40,7 @@ import {
40
40
  wallTime,
41
41
  weekdayOf,
42
42
  ymdOfDay
43
- } from "./chunk-66A6EMCU.js";
43
+ } from "./chunk-JWORVABV.js";
44
44
  export {
45
45
  DAY_MS,
46
46
  ItemStore,
package/dist/esm/index.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  TIMELINE_UNIT_DAYS,
8
8
  defaultOptions,
9
9
  localeFirstDay
10
- } from "./chunk-5PHSJK36.js";
10
+ } from "./chunk-AACI7SZC.js";
11
11
  import {
12
12
  DAY_MS,
13
13
  ItemStore,
@@ -49,7 +49,7 @@ import {
49
49
  wallTime,
50
50
  weekdayOf,
51
51
  ymdOfDay
52
- } from "./chunk-66A6EMCU.js";
52
+ } from "./chunk-JWORVABV.js";
53
53
 
54
54
  // src/tasknotes/bases.ts
55
55
  var FROM_BASES = {
package/dist/esm/react.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  Calendar
3
- } from "./chunk-5PHSJK36.js";
4
- import "./chunk-66A6EMCU.js";
3
+ } from "./chunk-AACI7SZC.js";
4
+ import "./chunk-JWORVABV.js";
5
5
 
6
6
  // src/react/index.tsx
7
7
  import { useEffect, useLayoutEffect, useRef } from "react";
@@ -24,6 +24,11 @@ export interface TimedPlacement<D = unknown> extends TimedSegment<D> {
24
24
  * Only then does the item keep to its lane's share of the column.
25
25
  */
26
26
  split: boolean;
27
+ /**
28
+ * Another item starts exactly where this short one ends, so it is drawn at
29
+ * its real height instead of being stretched to the minimum height.
30
+ */
31
+ abutted?: boolean;
27
32
  }
28
33
  export interface TimedDayLayout<D = unknown> {
29
34
  day: Day;
@@ -24,6 +24,11 @@ export interface TimedPlacement<D = unknown> extends TimedSegment<D> {
24
24
  * Only then does the item keep to its lane's share of the column.
25
25
  */
26
26
  split: boolean;
27
+ /**
28
+ * Another item starts exactly where this short one ends, so it is drawn at
29
+ * its real height instead of being stretched to the minimum height.
30
+ */
31
+ abutted?: boolean;
27
32
  }
28
33
  export interface TimedDayLayout<D = unknown> {
29
34
  day: Day;
@@ -146,6 +146,7 @@ export declare class Calendar<D = unknown> implements ViewContext<D> {
146
146
  dayArg(day: Day): DayArg;
147
147
  dayClasses(day: Day): string[];
148
148
  isEditable(n: NormalizedItem<D>, what: "start" | "duration", presentation?: boolean): boolean;
149
+ tooltip(el: HTMLElement, text: string): void;
149
150
  navLink(day: Day, label: Node | string, cls: string): HTMLElement;
150
151
  navigate(day: Day, jsEvent: MouseEvent | KeyboardEvent, view?: "day" | "month"): void;
151
152
  createItemEl(meta: ElMeta<D>): HTMLElement;
@@ -146,6 +146,7 @@ export declare class Calendar<D = unknown> implements ViewContext<D> {
146
146
  dayArg(day: Day): DayArg;
147
147
  dayClasses(day: Day): string[];
148
148
  isEditable(n: NormalizedItem<D>, what: "start" | "duration", presentation?: boolean): boolean;
149
+ tooltip(el: HTMLElement, text: string): void;
149
150
  navLink(day: Day, label: Node | string, cls: string): HTMLElement;
150
151
  navigate(day: Day, jsEvent: MouseEvent | KeyboardEvent, view?: "day" | "month"): void;
151
152
  createItemEl(meta: ElMeta<D>): HTMLElement;
@@ -59,6 +59,8 @@ export interface ViewContext<D = unknown> {
59
59
  /** Refreshes parts of an item element that depend on position (time label). */
60
60
  refreshItemEl(el: HTMLElement, meta: ElMeta<D>): void;
61
61
  destroyItemEl(el: HTMLElement): void;
62
+ /** Sets or clears (empty `text`) an element's hover tooltip through `opts.tooltip`. */
63
+ tooltip(el: HTMLElement, text: string): void;
62
64
  /** Builds a nav link (day number / header) for a day. */
63
65
  navLink(day: Day, label: Node | string, cls: string): HTMLElement;
64
66
  openMore(day: Day, anchor: HTMLElement, jsEvent: MouseEvent | KeyboardEvent, itemIds?: readonly string[]): void;
@@ -59,6 +59,8 @@ export interface ViewContext<D = unknown> {
59
59
  /** Refreshes parts of an item element that depend on position (time label). */
60
60
  refreshItemEl(el: HTMLElement, meta: ElMeta<D>): void;
61
61
  destroyItemEl(el: HTMLElement): void;
62
+ /** Sets or clears (empty `text`) an element's hover tooltip through `opts.tooltip`. */
63
+ tooltip(el: HTMLElement, text: string): void;
62
64
  /** Builds a nav link (day number / header) for a day. */
63
65
  navLink(day: Day, label: Node | string, cls: string): HTMLElement;
64
66
  openMore(day: Day, anchor: HTMLElement, jsEvent: MouseEvent | KeyboardEvent, itemIds?: readonly string[]): void;
@@ -433,6 +433,12 @@ export interface CalendarOptions<D = unknown> {
433
433
  }) => void;
434
434
  /** Return false to skip the built-in popover. */
435
435
  onMoreClick?: (arg: MoreArg<D>) => boolean | void;
436
+ /**
437
+ * Shows `text` as `el`'s hover tooltip; an empty `text` clears it. Default:
438
+ * the `title` attribute. Hosts with their own tooltips (Obsidian's
439
+ * `setTooltip`, say) pass them here.
440
+ */
441
+ tooltip?: (el: HTMLElement, text: string) => void;
436
442
  itemContent?: (arg: ItemArg<D>) => Node | string | null | undefined;
437
443
  itemClassNames?: (arg: ItemArg<D>) => string[] | string | undefined;
438
444
  itemDidMount?: (arg: ItemArg<D>) => void;
@@ -433,6 +433,12 @@ export interface CalendarOptions<D = unknown> {
433
433
  }) => void;
434
434
  /** Return false to skip the built-in popover. */
435
435
  onMoreClick?: (arg: MoreArg<D>) => boolean | void;
436
+ /**
437
+ * Shows `text` as `el`'s hover tooltip; an empty `text` clears it. Default:
438
+ * the `title` attribute. Hosts with their own tooltips (Obsidian's
439
+ * `setTooltip`, say) pass them here.
440
+ */
441
+ tooltip?: (el: HTMLElement, text: string) => void;
436
442
  itemContent?: (arg: ItemArg<D>) => Node | string | null | undefined;
437
443
  itemClassNames?: (arg: ItemArg<D>) => string[] | string | undefined;
438
444
  itemDidMount?: (arg: ItemArg<D>) => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tasknotes/calendar",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "description": "Task-native calendar engine for TaskNotes: incremental layout, time grid, month, list, year and timeline views.",
5
5
  "license": "MIT",
6
6
  "type": "module",