@tasknotes/calendar 0.1.5 → 0.1.7

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.
@@ -696,6 +696,7 @@ var DEFAULT_TEXT = {
696
696
  allDay: "All day",
697
697
  noItems: "Nothing scheduled",
698
698
  more: (n) => `+${n} more`,
699
+ appointments: (n) => `${n} appointments`,
699
700
  itemCount: (n) => n === 1 ? "1 item" : `${n} items`,
700
701
  weekNumber: (n) => `W${n}`,
701
702
  views: { week: "Week", day: "Day", days: "Days", month: "Month", list: "List", year: "Year", timeline: "Timeline" },
@@ -765,6 +766,7 @@ var LAYOUT_KEYS = /* @__PURE__ */ new Set([
765
766
  "dateStrip",
766
767
  "listLeadTitle",
767
768
  "listAddText",
769
+ "listEmptyContent",
768
770
  "monthDisplay",
769
771
  "timeZone",
770
772
  "locale",
@@ -859,7 +861,7 @@ function packTimed(segs, minMinutes, order = defaultOrder) {
859
861
  colEnds.push(0);
860
862
  }
861
863
  colEnds[col] = s.end;
862
- const p = { ...s, col, cols: 1, span: 1, cluster: clusterIndex };
864
+ const p = { ...s, col, cols: 1, span: 1, cluster: clusterIndex, split: false };
863
865
  cluster.push(p);
864
866
  out.push(p);
865
867
  clusterEnd = Math.max(clusterEnd, s.end);
@@ -879,7 +881,24 @@ function layoutTimedDay(items, day, zone, minMinutes, order, window = [0, MINUTE
879
881
  else if (n.item.lane === "actual") actual.push(seg);
880
882
  else plan.push(seg);
881
883
  }
882
- return { day, plan: packTimed(plan, minMinutes, order), actual: packTimed(actual, minMinutes, order), background };
884
+ const planPlaced = packTimed(plan, minMinutes, order);
885
+ const actualPlaced = packTimed(actual, minMinutes, order);
886
+ markSplit(planPlaced, actualPlaced, minMinutes);
887
+ markSplit(actualPlaced, planPlaced, minMinutes);
888
+ return { day, plan: planPlaced, actual: actualPlaced, background };
889
+ }
890
+ function markSplit(lane, other, minMinutes) {
891
+ if (!lane.length || !other.length) return;
892
+ const end = (p) => Math.max(p.endMin, p.startMin + minMinutes);
893
+ const ranges = /* @__PURE__ */ new Map();
894
+ for (const p of lane) {
895
+ const r = ranges.get(p.cluster);
896
+ if (r) r[1] = Math.max(r[1], end(p));
897
+ else ranges.set(p.cluster, [p.startMin, end(p)]);
898
+ }
899
+ const split = /* @__PURE__ */ new Set();
900
+ for (const [cluster, [s, e]] of ranges) if (other.some((o) => o.startMin < e && s < end(o))) split.add(cluster);
901
+ for (const p of lane) p.split = split.has(p.cluster);
883
902
  }
884
903
  function layoutRow(items, days, order = defaultOrder) {
885
904
  const first = days[0];
@@ -1152,19 +1171,27 @@ var DayGridView = class {
1152
1171
  });
1153
1172
  }
1154
1173
  w.pool.end();
1155
- w.more.replaceChildren();
1174
+ const existing = new Map(Array.from(w.more.children, (element) => [element.getAttribute("data-day"), element]));
1156
1175
  hidden.forEach((items, col) => {
1157
1176
  if (!items.length) return;
1158
1177
  const day = w.days[col];
1159
- const link = h(ctx.doc, "button", "tnc-more", { type: "button", "aria-haspopup": "dialog" }, ctx.text.more(items.length));
1178
+ const key = String(day);
1179
+ let link = existing.get(key);
1180
+ if (!link) {
1181
+ link = h(ctx.doc, "button", "tnc-more", { type: "button", "aria-haspopup": "dialog", "data-day": key });
1182
+ const anchor = link;
1183
+ link.addEventListener("click", (ev) => ctx.openMore(day, anchor, ev));
1184
+ w.more.append(link);
1185
+ }
1186
+ existing.delete(key);
1187
+ link.textContent = ctx.text.more(items.length);
1160
1188
  setStyle(link, {
1161
1189
  left: `calc(${pct(col / n)} + 2px)`,
1162
1190
  width: `calc(${pct(1 / n)} - 4px)`,
1163
1191
  top: `calc(var(--tnc-dg-top-h) + ${visible(col)} * (var(--tnc-bar-h) + ${BAR_GAP}px))`
1164
1192
  });
1165
- link.addEventListener("click", (ev) => ctx.openMore(day, link, ev));
1166
- w.more.append(link);
1167
1193
  });
1194
+ for (const link of existing.values()) link.remove();
1168
1195
  const step = this.barPx + BAR_GAP;
1169
1196
  let need = maxLevel >= 0 ? (maxLevel + 1) * step - BAR_GAP : 0;
1170
1197
  hidden.forEach((items, col) => {
@@ -1367,6 +1394,8 @@ var ListView = class {
1367
1394
  if (!keep.size && !leadItems.length) {
1368
1395
  if (!empty) {
1369
1396
  empty = h(doc, "div", "tnc-list-empty", { role: "listitem" }, ctx.text.noItems);
1397
+ const content = ctx.opts.listEmptyContent?.();
1398
+ if (content) empty.replaceChildren(content);
1370
1399
  this.body.append(empty);
1371
1400
  }
1372
1401
  } else empty?.remove();
@@ -1702,6 +1731,11 @@ var DateStrip = class {
1702
1731
 
1703
1732
  // src/dom/views/timegrid.ts
1704
1733
  var BAR_GAP2 = 2;
1734
+ function inLane(lane, split, left, width) {
1735
+ const [x, w] = !split ? ["1px", "100% - 9px"] : lane === "plan" ? ["1px", "100% - var(--tnc-actual-w) - 5px"] : ["100% - var(--tnc-actual-w) - 2px", "var(--tnc-actual-w)"];
1736
+ const f = (n) => Math.round(n * 1e4) / 1e4;
1737
+ return { left: `calc(${x} + (${w}) * ${f(left)})`, width: `calc((${w}) * ${f(width)} - 1px)` };
1738
+ }
1705
1739
  var TimeGridView = class {
1706
1740
  constructor(ctx) {
1707
1741
  this.ctx = ctx;
@@ -1710,6 +1744,8 @@ var TimeGridView = class {
1710
1744
  this.el = h(doc, "div", "tnc-tg", { role: "table", "aria-label": ctx.view.title });
1711
1745
  this.el.classList.toggle("tnc-tg--no-grid", !showGrid);
1712
1746
  this.scroller = h(doc, "div", "tnc-tg-scroll");
1747
+ this.dayNavigation = h(doc, "div", "tnc-tg-day-navigation", { role: "group", "aria-label": ctx.view.title });
1748
+ this.scroller.addEventListener("scroll", () => this.updateDayNavigation());
1713
1749
  this.top = h(doc, "div", "tnc-tg-top");
1714
1750
  this.headRow = h(doc, "div", "tnc-tg-head tnc-tg-cols", { role: "row" });
1715
1751
  this.top.append(this.headRow);
@@ -1737,7 +1773,7 @@ var TimeGridView = class {
1737
1773
  } else {
1738
1774
  this.body = this.gutter = null;
1739
1775
  }
1740
- this.el.append(this.scroller);
1776
+ this.el.append(this.dayNavigation, this.scroller);
1741
1777
  this.build();
1742
1778
  if (showGrid && opts.height === "fill") {
1743
1779
  const RO = ctx.win.ResizeObserver;
@@ -1750,6 +1786,7 @@ var TimeGridView = class {
1750
1786
  ctx;
1751
1787
  el;
1752
1788
  scroller;
1789
+ dayNavigation;
1753
1790
  top;
1754
1791
  headRow;
1755
1792
  allDayRow;
@@ -1805,6 +1842,9 @@ var TimeGridView = class {
1805
1842
  head.append(cap);
1806
1843
  }
1807
1844
  this.headRow.append(head);
1845
+ const dayButton = h(doc, "button", "tnc-btn", { type: "button", "aria-label": ctx.fmt.longDate(day) }, `${ctx.fmt.weekdayShort(day)} ${ctx.fmt.dayNumber(day)}`);
1846
+ dayButton.addEventListener("click", () => this.revealDay(day));
1847
+ this.dayNavigation.append(dayButton);
1808
1848
  opts.dayHeaderDidMount?.({ ...arg, el: head });
1809
1849
  const col = h(doc, "div", ["tnc-col", ...ctx.dayClasses(day)].join(" "), { role: "cell", "aria-label": ctx.fmt.longDate(day), "data-date": arg.dateStr });
1810
1850
  const bgLayer = h(doc, "div", "tnc-col-bg");
@@ -1821,6 +1861,7 @@ var TimeGridView = class {
1821
1861
  this.body?.append(col);
1822
1862
  return {
1823
1863
  day,
1864
+ collisionColumns: 1,
1824
1865
  head,
1825
1866
  col,
1826
1867
  plan: new ItemPool(ctx, planLayer),
@@ -1877,12 +1918,17 @@ var TimeGridView = class {
1877
1918
  /* --------------------------------------------------------------- render */
1878
1919
  render(change) {
1879
1920
  this.renderAllDay();
1880
- if (!this.body) return;
1921
+ if (!this.body) {
1922
+ this.updateDayNavigation();
1923
+ return;
1924
+ }
1881
1925
  const dirty = change ? change.days : null;
1882
1926
  for (const c of this.cols) {
1883
1927
  if (dirty && !dirty.has(c.day) && !(this.ctx.slot.max > MINUTES_PER_DAY && dirty.has(c.day + 1))) continue;
1884
1928
  this.renderColumn(c);
1885
1929
  }
1930
+ this.el.style.setProperty("--tnc-collision-columns", String(Math.max(1, ...this.cols.map((column) => column.collisionColumns))));
1931
+ this.updateDayNavigation();
1886
1932
  this.renderCapacity();
1887
1933
  this.tick();
1888
1934
  }
@@ -2000,6 +2046,11 @@ var TimeGridView = class {
2000
2046
  }
2001
2047
  const minMinutes = opts.eventMinHeight / this.ppm;
2002
2048
  const layout = layoutTimedDay(items, c.day, zone, minMinutes, ctx.order, [slot.min, slot.max]);
2049
+ c.collisionColumns = Math.max(
2050
+ 1,
2051
+ ...layout.plan.map((placement) => opts.eventMaxStack > 0 ? Math.min(placement.cols, opts.eventMaxStack) : placement.cols),
2052
+ ...layout.actual.filter((placement) => !placement.split).map((placement) => placement.cols)
2053
+ );
2003
2054
  const total = this.minutes;
2004
2055
  const top = (m) => pct((m - slot.min) / total);
2005
2056
  const height = (s) => pct((s.endMin - s.startMin) / total);
@@ -2011,19 +2062,28 @@ var TimeGridView = class {
2011
2062
  c.bg.end();
2012
2063
  const hasActual = layout.actual.length > 0;
2013
2064
  c.col.classList.toggle("has-actual", hasActual);
2014
- this.placeLane(c.plan, layout.plan, c, top, height, true);
2015
- this.placeLane(c.actual, layout.actual, c, top, height, false);
2065
+ for (const link of c.more.children) link.setAttribute("data-stale", "true");
2066
+ this.placeLane(c.plan, layout.plan, c, top, height, "plan");
2067
+ this.placeLane(c.actual, layout.actual, c, top, height, "actual");
2068
+ for (const link of [...c.more.children]) if (link.hasAttribute("data-stale")) link.remove();
2016
2069
  }
2017
- placeLane(pool, placements, c, top, height, withMore) {
2070
+ placeLane(pool, placements, c, top, height, lane) {
2018
2071
  const { ctx } = this;
2019
2072
  const { opts } = ctx;
2020
- const maxStack = opts.eventMaxStack;
2073
+ const readableWidth = Number.parseFloat(ctx.win.getComputedStyle(this.el).getPropertyValue("--tnc-event-min-width")) || 128;
2074
+ const laneWidth = c.col.clientWidth * (lane === "actual" && placements.some((placement) => placement.split) ? 0.22 : 1);
2075
+ const collapse = placements.some((placement) => {
2076
+ const columns = opts.eventMaxStack > 0 ? Math.min(placement.cols, opts.eventMaxStack) : placement.cols;
2077
+ return columns > 1 && columns * readableWidth > laneWidth;
2078
+ });
2079
+ const withMore = lane === "plan" || collapse;
2080
+ const maxStack = collapse ? 1 : opts.eventMaxStack;
2021
2081
  const hiddenByCluster = /* @__PURE__ */ new Map();
2022
2082
  pool.begin();
2023
2083
  for (const p of placements) {
2024
2084
  const preview = p.item.item.preview || p.item.id === ctx.draggingId;
2025
2085
  if (!preview && withMore && maxStack > 0 && p.cols > maxStack && p.col >= maxStack - 1) {
2026
- const rec = hiddenByCluster.get(p.cluster) ?? { count: 0, start: p.startMin, end: p.endMin, items: [] };
2086
+ const rec = hiddenByCluster.get(p.cluster) ?? { count: 0, start: p.startMin, end: p.endMin, split: p.split, items: [] };
2027
2087
  rec.count++;
2028
2088
  rec.start = Math.min(rec.start, p.startMin);
2029
2089
  rec.end = Math.max(rec.end, p.endMin);
@@ -2042,24 +2102,39 @@ var TimeGridView = class {
2042
2102
  setStyle(el, {
2043
2103
  top: top(p.startMin),
2044
2104
  height: `max(${height(p)}, ${opts.eventMinHeight}px)`,
2045
- left: pct(left),
2046
- width: `calc(${pct(width)} - 1px)`,
2105
+ ...inLane(lane, p.split, left, width),
2106
+ "--tnc-event-readable-width": pct(span / cols / width),
2047
2107
  "z-index": String(p.col + 1)
2048
2108
  });
2049
2109
  }
2050
2110
  pool.end();
2051
2111
  if (!withMore) return;
2052
- c.more.replaceChildren();
2053
- for (const rec of hiddenByCluster.values()) {
2054
- const link = h(ctx.doc, "button", "tnc-more tnc-more--stack", { type: "button", "aria-haspopup": "dialog" }, `+${rec.count}`);
2112
+ for (const [cluster, rec] of hiddenByCluster) {
2113
+ const key = `${lane}-${cluster}`;
2114
+ let link = c.more.querySelector(`[data-cluster="${key}"]`);
2115
+ if (!link) {
2116
+ link = h(ctx.doc, "button", "tnc-more tnc-more--stack", { type: "button", "aria-haspopup": "dialog", "data-cluster": key });
2117
+ const anchor = link;
2118
+ link.addEventListener("click", (ev) => ctx.openMore(c.day, anchor, ev, JSON.parse(anchor.dataset.itemIds ?? "[]")));
2119
+ c.more.append(link);
2120
+ }
2121
+ link.removeAttribute("data-stale");
2122
+ link.dataset.itemIds = JSON.stringify(placements.filter((placement) => placement.cluster === cluster).map((placement) => placement.item.id));
2123
+ link.classList.toggle("tnc-more--collision", collapse);
2124
+ link.textContent = `+${rec.count}`;
2125
+ if (collapse) {
2126
+ link.textContent = ctx.text.appointments(rec.count);
2127
+ link.setAttribute("aria-label", link.textContent);
2128
+ link.title = link.getAttribute("aria-label");
2129
+ } else {
2130
+ link.removeAttribute("aria-label");
2131
+ link.removeAttribute("title");
2132
+ }
2055
2133
  const cols = maxStack;
2056
2134
  setStyle(link, {
2057
2135
  top: top(rec.start),
2058
- left: pct((cols - 1) / cols),
2059
- width: `calc(${pct(1 / cols)} - 1px)`
2136
+ ...inLane(lane, rec.split, (cols - 1) / cols, 1 / cols)
2060
2137
  });
2061
- link.addEventListener("click", (ev) => ctx.openMore(c.day, link, ev));
2062
- c.more.append(link);
2063
2138
  }
2064
2139
  }
2065
2140
  meta(s, bg) {
@@ -2170,6 +2245,26 @@ var TimeGridView = class {
2170
2245
  }
2171
2246
  resize() {
2172
2247
  this.fitRows();
2248
+ this.render(null);
2249
+ }
2250
+ revealDay(day) {
2251
+ const column = this.cols.find((candidate) => candidate.day === day);
2252
+ if (!column) return;
2253
+ const bounds = column.head.getBoundingClientRect();
2254
+ const viewport = this.scroller.getBoundingClientRect();
2255
+ this.scroller.scrollLeft += bounds.left - viewport.left - (viewport.width - bounds.width) / 2;
2256
+ this.updateDayNavigation();
2257
+ }
2258
+ updateDayNavigation() {
2259
+ this.dayNavigation.hidden = this.scroller.scrollWidth <= this.scroller.clientWidth + 1;
2260
+ const viewport = this.scroller.getBoundingClientRect();
2261
+ this.cols.forEach((column, index) => {
2262
+ const bounds = column.head.getBoundingClientRect();
2263
+ const visible = bounds.left >= viewport.left && bounds.right <= viewport.right;
2264
+ const button = this.dayNavigation.children[index];
2265
+ button?.classList.toggle("is-offscreen", !visible);
2266
+ button?.setAttribute("aria-pressed", String(visible));
2267
+ });
2173
2268
  }
2174
2269
  scrollToTime(minute) {
2175
2270
  if (!this.body) return;
@@ -2842,6 +2937,7 @@ ${this.ctx.text.more(items.length - 8)}` : ""}` : label;
2842
2937
 
2843
2938
  // src/dom/calendar.ts
2844
2939
  var EXTERNAL_ID = "__tnc_external__";
2940
+ var SVG_NS = "http://www.w3.org/2000/svg";
2845
2941
  var MAX_AGENDA_DAYS = 366;
2846
2942
  function sameOption(a, b, depth = 0) {
2847
2943
  if (a === b) return true;
@@ -2934,7 +3030,11 @@ var Calendar = class {
2934
3030
  this.resizeObs?.disconnect();
2935
3031
  const RO = this.win.ResizeObserver;
2936
3032
  if (RO) {
2937
- this.resizeObs = new RO(() => this.current?.resize?.());
3033
+ this.resizeObs = new RO(() => {
3034
+ this.syncToolbarViewDisplay();
3035
+ this.current?.resize?.();
3036
+ this.positionMore();
3037
+ });
2938
3038
  this.resizeObs.observe(this.root);
2939
3039
  }
2940
3040
  }
@@ -2954,9 +3054,11 @@ var Calendar = class {
2954
3054
  if (this.frame !== null) old.cancelAnimationFrame(this.frame);
2955
3055
  this.timer = this.frame = null;
2956
3056
  this.doc.removeEventListener("pointerdown", this.onDocPointerDown, true);
3057
+ this.listenMoreGeometry(false);
2957
3058
  this.doc = doc;
2958
3059
  this.win = doc.defaultView ?? old;
2959
3060
  this.doc.addEventListener("pointerdown", this.onDocPointerDown, true);
3061
+ this.listenMoreGeometry(true);
2960
3062
  this.observeResize();
2961
3063
  this.rebuild(false);
2962
3064
  this.scheduleTick();
@@ -3074,6 +3176,7 @@ var Calendar = class {
3074
3176
  goToday() {
3075
3177
  this.anchor = this.todayDay();
3076
3178
  this.rebuild(true);
3179
+ if (this.current instanceof TimeGridView) this.current.revealDay(this.anchor);
3077
3180
  }
3078
3181
  /** The anchor date, as local midnight of that civil day in the calendar zone. */
3079
3182
  getDate() {
@@ -3092,6 +3195,7 @@ var Calendar = class {
3092
3195
  /** Re-measures (e.g. after the host was shown or resized by a parent). */
3093
3196
  updateSize() {
3094
3197
  this.current?.resize?.();
3198
+ this.positionMore();
3095
3199
  }
3096
3200
  getScroll() {
3097
3201
  return this.current?.getScroll() ?? { top: 0, left: 0 };
@@ -3123,7 +3227,10 @@ var Calendar = class {
3123
3227
  }
3124
3228
  const view = this.current;
3125
3229
  if (view instanceof TimelineView) view.scrollToInstant(ms, opts.align ?? "center", opts.smooth ?? false);
3126
- else if (view instanceof TimeGridView && p.kind !== "day") view.scrollToTime(wallTime(ms, this.zone).minute);
3230
+ else if (view instanceof TimeGridView) {
3231
+ view.revealDay(day);
3232
+ if (p.kind !== "day") view.scrollToTime(wallTime(ms, this.zone).minute);
3233
+ }
3127
3234
  }
3128
3235
  /** What is under a client point: a time slot, a day, or a timeline row and date. Null outside the view. */
3129
3236
  hitTest(clientX, clientY) {
@@ -3298,9 +3405,10 @@ var Calendar = class {
3298
3405
  if (meta) this.opts.itemWillUnmount?.(this.itemArg(el, meta));
3299
3406
  el.remove();
3300
3407
  }
3301
- openMore(day, anchor, jsEvent) {
3408
+ openMore(day, anchor, jsEvent, itemIds) {
3302
3409
  jsEvent.stopPropagation();
3303
- const items = this.store.inRange(day, day + 1).filter((n) => n.item.display !== "background" && (n.allDay || this.opts.view === "month" || !this.opts.timeGrid || this.opts.eventMaxStack > 0)).sort((a, b) => Number(b.allDay) - Number(a.allDay) || this.order(a, b));
3410
+ const group = itemIds ? new Set(itemIds) : null;
3411
+ const items = this.store.inRange(day, day + 1).filter((item) => !group || group.has(item.id)).filter((n) => n.item.display !== "background" && (n.allDay || anchor.classList.contains("tnc-more--stack") || this.opts.view === "month" || !this.opts.timeGrid || this.opts.eventMaxStack > 0)).sort((a, b) => Number(b.allDay) - Number(a.allDay) || this.order(a, b));
3304
3412
  const shown = new Set(
3305
3413
  [...this.viewHost.querySelectorAll(".tnc-item")].filter((el2) => this.meta.get(el2)?.day === day || (this.meta.get(el2)?.n.startDay ?? Infinity) <= day).map((el2) => el2.dataset.id ?? "")
3306
3414
  );
@@ -3329,15 +3437,7 @@ var Calendar = class {
3329
3437
  }
3330
3438
  pool.end();
3331
3439
  this.popover = { el, pool, anchor, day };
3332
- const rr = this.root.getBoundingClientRect();
3333
- const ar = anchor.getBoundingClientRect();
3334
- const w = Math.max(220, ar.width + 24);
3335
- let left = ar.left - rr.left - 12;
3336
- left = Math.max(4, Math.min(left, rr.width - w - 4));
3337
- let top = ar.top - rr.top - 40;
3338
- const height = el.offsetHeight;
3339
- top = Math.max(4, Math.min(top, rr.height - height - 4));
3340
- setStyle(el, { left, top, width: w });
3440
+ this.positionMore();
3341
3441
  el.focus();
3342
3442
  }
3343
3443
  /* =============================================================== render */
@@ -3461,8 +3561,10 @@ var Calendar = class {
3461
3561
  this.current.setScroll({ top: keep.top, left: 0 });
3462
3562
  this.current.placeInstant(keep.ms, keep.vx);
3463
3563
  } else if (scroll) this.current.setScroll(scroll);
3464
- else if (this.current instanceof TimeGridView) this.current.scrollToTime(this.slot.scroll);
3465
- else if (this.current instanceof TimelineView) this.current.scrollToDay(this.anchor);
3564
+ else if (this.current instanceof TimeGridView) {
3565
+ this.current.scrollToTime(this.slot.scroll);
3566
+ this.current.revealDay(this.anchor);
3567
+ } else if (this.current instanceof TimelineView) this.current.scrollToDay(this.anchor);
3466
3568
  if (rangeChanged) this.opts.onRangeChange?.(this.view);
3467
3569
  }
3468
3570
  /** Sets the visible days and view info from the anchor. */
@@ -3610,28 +3712,65 @@ var Calendar = class {
3610
3712
  if (e.icon) b.replaceChildren(e.icon());
3611
3713
  return b;
3612
3714
  });
3715
+ const chevron = (d) => {
3716
+ const svg = this.doc.createElementNS(SVG_NS, "svg");
3717
+ setAttrs(svg, { viewBox: "0 0 24 24", "aria-hidden": "true", focusable: "false" });
3718
+ const path = this.doc.createElementNS(SVG_NS, "path");
3719
+ path.setAttribute("d", d);
3720
+ svg.append(path);
3721
+ return svg;
3722
+ };
3723
+ const prev = btn("", this.text.prev, "tnc-btn-prev", () => this.prev());
3724
+ prev.append(chevron("m15 18-6-6 6-6"));
3725
+ const next = btn("", this.text.next, "tnc-btn-next", () => this.next());
3726
+ next.append(chevron("m9 18 6-6-6-6"));
3613
3727
  const nav = h(
3614
3728
  this.doc,
3615
3729
  "div",
3616
3730
  "tnc-toolbar-group tnc-toolbar-nav",
3617
3731
  null,
3618
- btn("\u2039", this.text.prev, "tnc-btn-prev", () => this.prev()),
3732
+ prev,
3619
3733
  btn(this.text.today, this.text.today, "tnc-btn-today", () => this.goToday()),
3620
- btn("\u203A", this.text.next, "tnc-btn-next", () => this.next())
3734
+ next
3621
3735
  );
3622
3736
  const title = h(this.doc, "h2", "tnc-title", { "aria-live": "polite" }, this.view.title);
3623
3737
  const viewGroup = h(this.doc, "div", "tnc-toolbar-group tnc-toolbar-views", { role: "group" });
3738
+ const viewSelect = h(this.doc, "select", "tnc-btn tnc-toolbar-view-select", {
3739
+ "aria-label": this.text.viewHints[this.opts.view] ?? this.text.views[this.opts.view] ?? this.opts.view
3740
+ });
3624
3741
  for (const v of views) {
3625
3742
  const label = v === "days" ? this.text.views.days ?? `${this.opts.dayCount} days` : this.text.views[v] ?? v;
3626
3743
  viewGroup.append(btn(label, this.text.viewHints[v] ?? label, `tnc-btn-view tnc-btn-view-${v}`, () => this.changeView(v), v === this.opts.view));
3744
+ viewSelect.append(h(this.doc, "option", "", { value: v }, label));
3627
3745
  }
3746
+ viewSelect.value = this.opts.view;
3747
+ viewSelect.addEventListener("change", () => this.changeView(viewSelect.value));
3628
3748
  const start = h(this.doc, "div", "tnc-toolbar-start");
3629
3749
  const end = h(this.doc, "div", "tnc-toolbar-end");
3630
3750
  if (cfg.extraPlacement === "start") start.append(...extras);
3631
3751
  start.append(nav, title);
3632
- end.append(viewGroup);
3752
+ end.append(viewGroup, viewSelect);
3633
3753
  if (cfg.extraPlacement !== "start") end.append(...extras);
3634
3754
  this.toolbarEl.append(start, end);
3755
+ this.syncToolbarViewDisplay();
3756
+ }
3757
+ syncToolbarViewDisplay() {
3758
+ const buttons = this.toolbarEl.querySelector(".tnc-toolbar-views");
3759
+ const select = this.toolbarEl.querySelector(".tnc-toolbar-view-select");
3760
+ const start = this.toolbarEl.querySelector(".tnc-toolbar-start");
3761
+ if (!buttons || !select || !start) return;
3762
+ const config = this.opts.toolbar;
3763
+ const mode = typeof config === "object" ? config.viewDisplay ?? "auto" : "auto";
3764
+ buttons.hidden = false;
3765
+ select.hidden = true;
3766
+ buttons.style.display = "flex";
3767
+ select.style.display = "none";
3768
+ 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;
3770
+ buttons.hidden = compact;
3771
+ select.hidden = !compact;
3772
+ buttons.style.display = compact ? "none" : "flex";
3773
+ select.style.display = compact ? "" : "none";
3635
3774
  }
3636
3775
  applyChange(change) {
3637
3776
  if (!change.added.length && !change.updated.length && !change.removed.length && !change.days.size && !change.spans) return;
@@ -3764,6 +3903,38 @@ var Calendar = class {
3764
3903
  }
3765
3904
  }
3766
3905
  /* ============================================================== popover */
3906
+ onMoreGeometryChange = () => this.positionMore();
3907
+ listenMoreGeometry(on) {
3908
+ const method = on ? "addEventListener" : "removeEventListener";
3909
+ this.doc[method]("scroll", this.onMoreGeometryChange, true);
3910
+ this.win[method]("resize", this.onMoreGeometryChange);
3911
+ this.win.visualViewport?.[method]("resize", this.onMoreGeometryChange);
3912
+ this.win.visualViewport?.[method]("scroll", this.onMoreGeometryChange);
3913
+ }
3914
+ positionMore() {
3915
+ const popover = this.popover;
3916
+ if (!popover) return;
3917
+ if (!popover.anchor.isConnected) {
3918
+ this.closeMore();
3919
+ return;
3920
+ }
3921
+ const viewport = this.win.visualViewport;
3922
+ const root = this.root.getBoundingClientRect();
3923
+ const anchor = popover.anchor.getBoundingClientRect();
3924
+ const leftEdge = Math.max(root.left, viewport?.offsetLeft ?? 0) + 4;
3925
+ const topEdge = Math.max(root.top, viewport?.offsetTop ?? 0) + 4;
3926
+ const rightEdge = Math.min(root.right, (viewport?.offsetLeft ?? 0) + (viewport?.width ?? this.win.innerWidth)) - 4;
3927
+ const bottomEdge = Math.min(root.bottom, (viewport?.offsetTop ?? 0) + (viewport?.height ?? this.win.innerHeight)) - 4;
3928
+ if (rightEdge <= leftEdge || bottomEdge <= topEdge) {
3929
+ this.closeMore();
3930
+ return;
3931
+ }
3932
+ const width = Math.min(360, rightEdge - leftEdge);
3933
+ setStyle(popover.el, { width, "max-height": Math.min(420, bottomEdge - topEdge) });
3934
+ const left = Math.max(leftEdge, Math.min(anchor.left - 12, rightEdge - width));
3935
+ const top = Math.max(topEdge, Math.min(anchor.top - 40, bottomEdge - popover.el.offsetHeight));
3936
+ setStyle(popover.el, { left: left - root.left, top: top - root.top });
3937
+ }
3767
3938
  refreshMore() {
3768
3939
  const p = this.popover;
3769
3940
  if (!p) return;
@@ -3773,6 +3944,7 @@ var Calendar = class {
3773
3944
  p.pool.place(n.id, { n, day: p.day, isStart: n.startDay >= p.day, isEnd: n.endDay <= p.day + 1, variant: n.allDay ? "bar" : "dot", mirror: false });
3774
3945
  }
3775
3946
  p.pool.end();
3947
+ this.positionMore();
3776
3948
  }
3777
3949
  closeMore(restoreFocus = false) {
3778
3950
  const p = this.popover;
@@ -3800,6 +3972,7 @@ var Calendar = class {
3800
3972
  r.addEventListener("pointerover", this.onAdoptCheck);
3801
3973
  r.addEventListener("focusin", this.onAdoptCheck);
3802
3974
  this.doc.addEventListener("pointerdown", this.onDocPointerDown, true);
3975
+ this.listenMoreGeometry(true);
3803
3976
  }
3804
3977
  unbindEvents() {
3805
3978
  const r = this.root;
@@ -3818,6 +3991,7 @@ var Calendar = class {
3818
3991
  r.removeEventListener("pointerover", this.onAdoptCheck);
3819
3992
  r.removeEventListener("focusin", this.onAdoptCheck);
3820
3993
  this.doc.removeEventListener("pointerdown", this.onDocPointerDown, true);
3994
+ this.listenMoreGeometry(false);
3821
3995
  this.listenDoc(false);
3822
3996
  }
3823
3997
  listenDoc(on) {
@@ -3863,6 +4037,7 @@ var Calendar = class {
3863
4037
  mode = "select";
3864
4038
  }
3865
4039
  const touch = ev.pointerType === "touch";
4040
+ const overflowOrigin = itemEl?.closest(".tnc-popover") && origin ? wallTime(origin.startMs, this.zone) : null;
3866
4041
  const s = {
3867
4042
  mode,
3868
4043
  pointerId: ev.pointerId,
@@ -3871,7 +4046,7 @@ var Calendar = class {
3871
4046
  y0: ev.clientY,
3872
4047
  active: false,
3873
4048
  origin,
3874
- grab: this.current?.hit(ev.clientX, ev.clientY) ?? null,
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,
3875
4050
  proposal: null,
3876
4051
  denied: false,
3877
4052
  timer: null,
@@ -4374,6 +4549,25 @@ var Calendar = class {
4374
4549
  if (el && meta) this.opts.onItemHover?.({ ...this.itemArg(el, meta), jsEvent: ev, entering: false });
4375
4550
  }
4376
4551
  onKeyDown = (ev) => {
4552
+ if (this.popover && !ev.altKey && !ev.ctrlKey && !ev.metaKey && ["ArrowDown", "ArrowUp", "Home", "End"].includes(ev.key)) {
4553
+ const rows = [...this.popover.el.querySelectorAll(".tnc-item")];
4554
+ const index = rows.indexOf(this.doc.activeElement);
4555
+ const next = ev.key === "Home" ? 0 : ev.key === "End" ? rows.length - 1 : ev.key === "ArrowDown" ? (index + 1) % rows.length : index <= 0 ? rows.length - 1 : index - 1;
4556
+ ev.preventDefault();
4557
+ ev.stopPropagation();
4558
+ rows[next]?.focus();
4559
+ rows[next]?.scrollIntoView({ block: "nearest", inline: "nearest" });
4560
+ return;
4561
+ }
4562
+ if (ev.key === "Tab" && this.popover) {
4563
+ const controls = [...this.popover.el.querySelectorAll('button, a[href], [tabindex="0"]')];
4564
+ const index = controls.indexOf(this.doc.activeElement);
4565
+ const next = ev.shiftKey ? index <= 0 ? controls.length - 1 : index - 1 : (index + 1) % controls.length;
4566
+ ev.preventDefault();
4567
+ ev.stopPropagation();
4568
+ controls[next]?.focus();
4569
+ return;
4570
+ }
4377
4571
  if (ev.key === "Escape" && this.popover) {
4378
4572
  ev.preventDefault();
4379
4573
  ev.stopPropagation();
@@ -455,7 +455,7 @@ function packTimed(segs, minMinutes, order = defaultOrder) {
455
455
  colEnds.push(0);
456
456
  }
457
457
  colEnds[col] = s.end;
458
- const p = { ...s, col, cols: 1, span: 1, cluster: clusterIndex };
458
+ const p = { ...s, col, cols: 1, span: 1, cluster: clusterIndex, split: false };
459
459
  cluster.push(p);
460
460
  out.push(p);
461
461
  clusterEnd = Math.max(clusterEnd, s.end);
@@ -475,7 +475,24 @@ function layoutTimedDay(items, day, zone, minMinutes, order, window = [0, MINUTE
475
475
  else if (n.item.lane === "actual") actual.push(seg);
476
476
  else plan.push(seg);
477
477
  }
478
- return { day, plan: packTimed(plan, minMinutes, order), actual: packTimed(actual, minMinutes, order), background };
478
+ const planPlaced = packTimed(plan, minMinutes, order);
479
+ const actualPlaced = packTimed(actual, minMinutes, order);
480
+ markSplit(planPlaced, actualPlaced, minMinutes);
481
+ markSplit(actualPlaced, planPlaced, minMinutes);
482
+ return { day, plan: planPlaced, actual: actualPlaced, background };
483
+ }
484
+ function markSplit(lane, other, minMinutes) {
485
+ 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
+ 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);
495
+ for (const p of lane) p.split = split.has(p.cluster);
479
496
  }
480
497
  function layoutRow(items, days, order = defaultOrder) {
481
498
  const first = days[0];