@tasknotes/calendar 0.1.6 → 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",
@@ -1169,19 +1171,27 @@ var DayGridView = class {
1169
1171
  });
1170
1172
  }
1171
1173
  w.pool.end();
1172
- w.more.replaceChildren();
1174
+ const existing = new Map(Array.from(w.more.children, (element) => [element.getAttribute("data-day"), element]));
1173
1175
  hidden.forEach((items, col) => {
1174
1176
  if (!items.length) return;
1175
1177
  const day = w.days[col];
1176
- 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);
1177
1188
  setStyle(link, {
1178
1189
  left: `calc(${pct(col / n)} + 2px)`,
1179
1190
  width: `calc(${pct(1 / n)} - 4px)`,
1180
1191
  top: `calc(var(--tnc-dg-top-h) + ${visible(col)} * (var(--tnc-bar-h) + ${BAR_GAP}px))`
1181
1192
  });
1182
- link.addEventListener("click", (ev) => ctx.openMore(day, link, ev));
1183
- w.more.append(link);
1184
1193
  });
1194
+ for (const link of existing.values()) link.remove();
1185
1195
  const step = this.barPx + BAR_GAP;
1186
1196
  let need = maxLevel >= 0 ? (maxLevel + 1) * step - BAR_GAP : 0;
1187
1197
  hidden.forEach((items, col) => {
@@ -1384,6 +1394,8 @@ var ListView = class {
1384
1394
  if (!keep.size && !leadItems.length) {
1385
1395
  if (!empty) {
1386
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);
1387
1399
  this.body.append(empty);
1388
1400
  }
1389
1401
  } else empty?.remove();
@@ -1732,6 +1744,8 @@ var TimeGridView = class {
1732
1744
  this.el = h(doc, "div", "tnc-tg", { role: "table", "aria-label": ctx.view.title });
1733
1745
  this.el.classList.toggle("tnc-tg--no-grid", !showGrid);
1734
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());
1735
1749
  this.top = h(doc, "div", "tnc-tg-top");
1736
1750
  this.headRow = h(doc, "div", "tnc-tg-head tnc-tg-cols", { role: "row" });
1737
1751
  this.top.append(this.headRow);
@@ -1759,7 +1773,7 @@ var TimeGridView = class {
1759
1773
  } else {
1760
1774
  this.body = this.gutter = null;
1761
1775
  }
1762
- this.el.append(this.scroller);
1776
+ this.el.append(this.dayNavigation, this.scroller);
1763
1777
  this.build();
1764
1778
  if (showGrid && opts.height === "fill") {
1765
1779
  const RO = ctx.win.ResizeObserver;
@@ -1772,6 +1786,7 @@ var TimeGridView = class {
1772
1786
  ctx;
1773
1787
  el;
1774
1788
  scroller;
1789
+ dayNavigation;
1775
1790
  top;
1776
1791
  headRow;
1777
1792
  allDayRow;
@@ -1827,6 +1842,9 @@ var TimeGridView = class {
1827
1842
  head.append(cap);
1828
1843
  }
1829
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);
1830
1848
  opts.dayHeaderDidMount?.({ ...arg, el: head });
1831
1849
  const col = h(doc, "div", ["tnc-col", ...ctx.dayClasses(day)].join(" "), { role: "cell", "aria-label": ctx.fmt.longDate(day), "data-date": arg.dateStr });
1832
1850
  const bgLayer = h(doc, "div", "tnc-col-bg");
@@ -1843,6 +1861,7 @@ var TimeGridView = class {
1843
1861
  this.body?.append(col);
1844
1862
  return {
1845
1863
  day,
1864
+ collisionColumns: 1,
1846
1865
  head,
1847
1866
  col,
1848
1867
  plan: new ItemPool(ctx, planLayer),
@@ -1899,12 +1918,17 @@ var TimeGridView = class {
1899
1918
  /* --------------------------------------------------------------- render */
1900
1919
  render(change) {
1901
1920
  this.renderAllDay();
1902
- if (!this.body) return;
1921
+ if (!this.body) {
1922
+ this.updateDayNavigation();
1923
+ return;
1924
+ }
1903
1925
  const dirty = change ? change.days : null;
1904
1926
  for (const c of this.cols) {
1905
1927
  if (dirty && !dirty.has(c.day) && !(this.ctx.slot.max > MINUTES_PER_DAY && dirty.has(c.day + 1))) continue;
1906
1928
  this.renderColumn(c);
1907
1929
  }
1930
+ this.el.style.setProperty("--tnc-collision-columns", String(Math.max(1, ...this.cols.map((column) => column.collisionColumns))));
1931
+ this.updateDayNavigation();
1908
1932
  this.renderCapacity();
1909
1933
  this.tick();
1910
1934
  }
@@ -2022,6 +2046,11 @@ var TimeGridView = class {
2022
2046
  }
2023
2047
  const minMinutes = opts.eventMinHeight / this.ppm;
2024
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
+ );
2025
2054
  const total = this.minutes;
2026
2055
  const top = (m) => pct((m - slot.min) / total);
2027
2056
  const height = (s) => pct((s.endMin - s.startMin) / total);
@@ -2033,14 +2062,22 @@ var TimeGridView = class {
2033
2062
  c.bg.end();
2034
2063
  const hasActual = layout.actual.length > 0;
2035
2064
  c.col.classList.toggle("has-actual", hasActual);
2065
+ for (const link of c.more.children) link.setAttribute("data-stale", "true");
2036
2066
  this.placeLane(c.plan, layout.plan, c, top, height, "plan");
2037
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();
2038
2069
  }
2039
2070
  placeLane(pool, placements, c, top, height, lane) {
2040
2071
  const { ctx } = this;
2041
2072
  const { opts } = ctx;
2042
- const withMore = lane === "plan";
2043
- 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;
2044
2081
  const hiddenByCluster = /* @__PURE__ */ new Map();
2045
2082
  pool.begin();
2046
2083
  for (const p of placements) {
@@ -2066,21 +2103,38 @@ var TimeGridView = class {
2066
2103
  top: top(p.startMin),
2067
2104
  height: `max(${height(p)}, ${opts.eventMinHeight}px)`,
2068
2105
  ...inLane(lane, p.split, left, width),
2106
+ "--tnc-event-readable-width": pct(span / cols / width),
2069
2107
  "z-index": String(p.col + 1)
2070
2108
  });
2071
2109
  }
2072
2110
  pool.end();
2073
2111
  if (!withMore) return;
2074
- c.more.replaceChildren();
2075
- for (const rec of hiddenByCluster.values()) {
2076
- 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
+ }
2077
2133
  const cols = maxStack;
2078
2134
  setStyle(link, {
2079
2135
  top: top(rec.start),
2080
2136
  ...inLane(lane, rec.split, (cols - 1) / cols, 1 / cols)
2081
2137
  });
2082
- link.addEventListener("click", (ev) => ctx.openMore(c.day, link, ev));
2083
- c.more.append(link);
2084
2138
  }
2085
2139
  }
2086
2140
  meta(s, bg) {
@@ -2191,6 +2245,26 @@ var TimeGridView = class {
2191
2245
  }
2192
2246
  resize() {
2193
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
+ });
2194
2268
  }
2195
2269
  scrollToTime(minute) {
2196
2270
  if (!this.body) return;
@@ -2956,7 +3030,11 @@ var Calendar = class {
2956
3030
  this.resizeObs?.disconnect();
2957
3031
  const RO = this.win.ResizeObserver;
2958
3032
  if (RO) {
2959
- this.resizeObs = new RO(() => this.current?.resize?.());
3033
+ this.resizeObs = new RO(() => {
3034
+ this.syncToolbarViewDisplay();
3035
+ this.current?.resize?.();
3036
+ this.positionMore();
3037
+ });
2960
3038
  this.resizeObs.observe(this.root);
2961
3039
  }
2962
3040
  }
@@ -2976,9 +3054,11 @@ var Calendar = class {
2976
3054
  if (this.frame !== null) old.cancelAnimationFrame(this.frame);
2977
3055
  this.timer = this.frame = null;
2978
3056
  this.doc.removeEventListener("pointerdown", this.onDocPointerDown, true);
3057
+ this.listenMoreGeometry(false);
2979
3058
  this.doc = doc;
2980
3059
  this.win = doc.defaultView ?? old;
2981
3060
  this.doc.addEventListener("pointerdown", this.onDocPointerDown, true);
3061
+ this.listenMoreGeometry(true);
2982
3062
  this.observeResize();
2983
3063
  this.rebuild(false);
2984
3064
  this.scheduleTick();
@@ -3096,6 +3176,7 @@ var Calendar = class {
3096
3176
  goToday() {
3097
3177
  this.anchor = this.todayDay();
3098
3178
  this.rebuild(true);
3179
+ if (this.current instanceof TimeGridView) this.current.revealDay(this.anchor);
3099
3180
  }
3100
3181
  /** The anchor date, as local midnight of that civil day in the calendar zone. */
3101
3182
  getDate() {
@@ -3114,6 +3195,7 @@ var Calendar = class {
3114
3195
  /** Re-measures (e.g. after the host was shown or resized by a parent). */
3115
3196
  updateSize() {
3116
3197
  this.current?.resize?.();
3198
+ this.positionMore();
3117
3199
  }
3118
3200
  getScroll() {
3119
3201
  return this.current?.getScroll() ?? { top: 0, left: 0 };
@@ -3145,7 +3227,10 @@ var Calendar = class {
3145
3227
  }
3146
3228
  const view = this.current;
3147
3229
  if (view instanceof TimelineView) view.scrollToInstant(ms, opts.align ?? "center", opts.smooth ?? false);
3148
- 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
+ }
3149
3234
  }
3150
3235
  /** What is under a client point: a time slot, a day, or a timeline row and date. Null outside the view. */
3151
3236
  hitTest(clientX, clientY) {
@@ -3320,9 +3405,10 @@ var Calendar = class {
3320
3405
  if (meta) this.opts.itemWillUnmount?.(this.itemArg(el, meta));
3321
3406
  el.remove();
3322
3407
  }
3323
- openMore(day, anchor, jsEvent) {
3408
+ openMore(day, anchor, jsEvent, itemIds) {
3324
3409
  jsEvent.stopPropagation();
3325
- 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));
3326
3412
  const shown = new Set(
3327
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 ?? "")
3328
3414
  );
@@ -3351,15 +3437,7 @@ var Calendar = class {
3351
3437
  }
3352
3438
  pool.end();
3353
3439
  this.popover = { el, pool, anchor, day };
3354
- const rr = this.root.getBoundingClientRect();
3355
- const ar = anchor.getBoundingClientRect();
3356
- const w = Math.max(220, ar.width + 24);
3357
- let left = ar.left - rr.left - 12;
3358
- left = Math.max(4, Math.min(left, rr.width - w - 4));
3359
- let top = ar.top - rr.top - 40;
3360
- const height = el.offsetHeight;
3361
- top = Math.max(4, Math.min(top, rr.height - height - 4));
3362
- setStyle(el, { left, top, width: w });
3440
+ this.positionMore();
3363
3441
  el.focus();
3364
3442
  }
3365
3443
  /* =============================================================== render */
@@ -3483,8 +3561,10 @@ var Calendar = class {
3483
3561
  this.current.setScroll({ top: keep.top, left: 0 });
3484
3562
  this.current.placeInstant(keep.ms, keep.vx);
3485
3563
  } else if (scroll) this.current.setScroll(scroll);
3486
- else if (this.current instanceof TimeGridView) this.current.scrollToTime(this.slot.scroll);
3487
- 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);
3488
3568
  if (rangeChanged) this.opts.onRangeChange?.(this.view);
3489
3569
  }
3490
3570
  /** Sets the visible days and view info from the anchor. */
@@ -3655,17 +3735,42 @@ var Calendar = class {
3655
3735
  );
3656
3736
  const title = h(this.doc, "h2", "tnc-title", { "aria-live": "polite" }, this.view.title);
3657
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
+ });
3658
3741
  for (const v of views) {
3659
3742
  const label = v === "days" ? this.text.views.days ?? `${this.opts.dayCount} days` : this.text.views[v] ?? v;
3660
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));
3661
3745
  }
3746
+ viewSelect.value = this.opts.view;
3747
+ viewSelect.addEventListener("change", () => this.changeView(viewSelect.value));
3662
3748
  const start = h(this.doc, "div", "tnc-toolbar-start");
3663
3749
  const end = h(this.doc, "div", "tnc-toolbar-end");
3664
3750
  if (cfg.extraPlacement === "start") start.append(...extras);
3665
3751
  start.append(nav, title);
3666
- end.append(viewGroup);
3752
+ end.append(viewGroup, viewSelect);
3667
3753
  if (cfg.extraPlacement !== "start") end.append(...extras);
3668
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";
3669
3774
  }
3670
3775
  applyChange(change) {
3671
3776
  if (!change.added.length && !change.updated.length && !change.removed.length && !change.days.size && !change.spans) return;
@@ -3798,6 +3903,38 @@ var Calendar = class {
3798
3903
  }
3799
3904
  }
3800
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
+ }
3801
3938
  refreshMore() {
3802
3939
  const p = this.popover;
3803
3940
  if (!p) return;
@@ -3807,6 +3944,7 @@ var Calendar = class {
3807
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 });
3808
3945
  }
3809
3946
  p.pool.end();
3947
+ this.positionMore();
3810
3948
  }
3811
3949
  closeMore(restoreFocus = false) {
3812
3950
  const p = this.popover;
@@ -3834,6 +3972,7 @@ var Calendar = class {
3834
3972
  r.addEventListener("pointerover", this.onAdoptCheck);
3835
3973
  r.addEventListener("focusin", this.onAdoptCheck);
3836
3974
  this.doc.addEventListener("pointerdown", this.onDocPointerDown, true);
3975
+ this.listenMoreGeometry(true);
3837
3976
  }
3838
3977
  unbindEvents() {
3839
3978
  const r = this.root;
@@ -3852,6 +3991,7 @@ var Calendar = class {
3852
3991
  r.removeEventListener("pointerover", this.onAdoptCheck);
3853
3992
  r.removeEventListener("focusin", this.onAdoptCheck);
3854
3993
  this.doc.removeEventListener("pointerdown", this.onDocPointerDown, true);
3994
+ this.listenMoreGeometry(false);
3855
3995
  this.listenDoc(false);
3856
3996
  }
3857
3997
  listenDoc(on) {
@@ -3897,6 +4037,7 @@ var Calendar = class {
3897
4037
  mode = "select";
3898
4038
  }
3899
4039
  const touch = ev.pointerType === "touch";
4040
+ const overflowOrigin = itemEl?.closest(".tnc-popover") && origin ? wallTime(origin.startMs, this.zone) : null;
3900
4041
  const s = {
3901
4042
  mode,
3902
4043
  pointerId: ev.pointerId,
@@ -3905,7 +4046,7 @@ var Calendar = class {
3905
4046
  y0: ev.clientY,
3906
4047
  active: false,
3907
4048
  origin,
3908
- 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,
3909
4050
  proposal: null,
3910
4051
  denied: false,
3911
4052
  timer: null,
@@ -4408,6 +4549,25 @@ var Calendar = class {
4408
4549
  if (el && meta) this.opts.onItemHover?.({ ...this.itemArg(el, meta), jsEvent: ev, entering: false });
4409
4550
  }
4410
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
+ }
4411
4571
  if (ev.key === "Escape" && this.popover) {
4412
4572
  ev.preventDefault();
4413
4573
  ev.stopPropagation();