@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.
@@ -532,7 +532,7 @@ function packTimed(segs, minMinutes, order = defaultOrder) {
532
532
  colEnds.push(0);
533
533
  }
534
534
  colEnds[col] = s.end;
535
- const p = { ...s, col, cols: 1, span: 1, cluster: clusterIndex };
535
+ const p = { ...s, col, cols: 1, span: 1, cluster: clusterIndex, split: false };
536
536
  cluster.push(p);
537
537
  out.push(p);
538
538
  clusterEnd = Math.max(clusterEnd, s.end);
@@ -552,7 +552,24 @@ function layoutTimedDay(items, day, zone, minMinutes, order, window = [0, MINUTE
552
552
  else if (n.item.lane === "actual") actual.push(seg);
553
553
  else plan.push(seg);
554
554
  }
555
- return { day, plan: packTimed(plan, minMinutes, order), actual: packTimed(actual, minMinutes, order), background };
555
+ const planPlaced = packTimed(plan, minMinutes, order);
556
+ const actualPlaced = packTimed(actual, minMinutes, order);
557
+ markSplit(planPlaced, actualPlaced, minMinutes);
558
+ markSplit(actualPlaced, planPlaced, minMinutes);
559
+ return { day, plan: planPlaced, actual: actualPlaced, background };
560
+ }
561
+ function markSplit(lane, other, minMinutes) {
562
+ 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
+ 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);
572
+ for (const p of lane) p.split = split.has(p.cluster);
556
573
  }
557
574
  function layoutRow(items, days, order = defaultOrder) {
558
575
  const first = days[0];
@@ -936,6 +953,7 @@ var DEFAULT_TEXT = {
936
953
  allDay: "All day",
937
954
  noItems: "Nothing scheduled",
938
955
  more: (n) => `+${n} more`,
956
+ appointments: (n) => `${n} appointments`,
939
957
  itemCount: (n) => n === 1 ? "1 item" : `${n} items`,
940
958
  weekNumber: (n) => `W${n}`,
941
959
  views: { week: "Week", day: "Day", days: "Days", month: "Month", list: "List", year: "Year", timeline: "Timeline" },
@@ -1005,6 +1023,7 @@ var LAYOUT_KEYS = /* @__PURE__ */ new Set([
1005
1023
  "dateStrip",
1006
1024
  "listLeadTitle",
1007
1025
  "listAddText",
1026
+ "listEmptyContent",
1008
1027
  "monthDisplay",
1009
1028
  "timeZone",
1010
1029
  "locale",
@@ -1244,19 +1263,27 @@ var DayGridView = class {
1244
1263
  });
1245
1264
  }
1246
1265
  w.pool.end();
1247
- w.more.replaceChildren();
1266
+ const existing = new Map(Array.from(w.more.children, (element) => [element.getAttribute("data-day"), element]));
1248
1267
  hidden.forEach((items, col) => {
1249
1268
  if (!items.length) return;
1250
1269
  const day = w.days[col];
1251
- const link = h(ctx.doc, "button", "tnc-more", { type: "button", "aria-haspopup": "dialog" }, ctx.text.more(items.length));
1270
+ const key = String(day);
1271
+ let link = existing.get(key);
1272
+ if (!link) {
1273
+ link = h(ctx.doc, "button", "tnc-more", { type: "button", "aria-haspopup": "dialog", "data-day": key });
1274
+ const anchor = link;
1275
+ link.addEventListener("click", (ev) => ctx.openMore(day, anchor, ev));
1276
+ w.more.append(link);
1277
+ }
1278
+ existing.delete(key);
1279
+ link.textContent = ctx.text.more(items.length);
1252
1280
  setStyle(link, {
1253
1281
  left: `calc(${pct(col / n)} + 2px)`,
1254
1282
  width: `calc(${pct(1 / n)} - 4px)`,
1255
1283
  top: `calc(var(--tnc-dg-top-h) + ${visible(col)} * (var(--tnc-bar-h) + ${BAR_GAP}px))`
1256
1284
  });
1257
- link.addEventListener("click", (ev) => ctx.openMore(day, link, ev));
1258
- w.more.append(link);
1259
1285
  });
1286
+ for (const link of existing.values()) link.remove();
1260
1287
  const step = this.barPx + BAR_GAP;
1261
1288
  let need = maxLevel >= 0 ? (maxLevel + 1) * step - BAR_GAP : 0;
1262
1289
  hidden.forEach((items, col) => {
@@ -1459,6 +1486,8 @@ var ListView = class {
1459
1486
  if (!keep.size && !leadItems.length) {
1460
1487
  if (!empty) {
1461
1488
  empty = h(doc, "div", "tnc-list-empty", { role: "listitem" }, ctx.text.noItems);
1489
+ const content = ctx.opts.listEmptyContent?.();
1490
+ if (content) empty.replaceChildren(content);
1462
1491
  this.body.append(empty);
1463
1492
  }
1464
1493
  } else empty?.remove();
@@ -1794,6 +1823,11 @@ var DateStrip = class {
1794
1823
 
1795
1824
  // src/dom/views/timegrid.ts
1796
1825
  var BAR_GAP2 = 2;
1826
+ function inLane(lane, split, left, width) {
1827
+ 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)"];
1828
+ const f = (n) => Math.round(n * 1e4) / 1e4;
1829
+ return { left: `calc(${x} + (${w}) * ${f(left)})`, width: `calc((${w}) * ${f(width)} - 1px)` };
1830
+ }
1797
1831
  var TimeGridView = class {
1798
1832
  constructor(ctx) {
1799
1833
  this.ctx = ctx;
@@ -1802,6 +1836,8 @@ var TimeGridView = class {
1802
1836
  this.el = h(doc, "div", "tnc-tg", { role: "table", "aria-label": ctx.view.title });
1803
1837
  this.el.classList.toggle("tnc-tg--no-grid", !showGrid);
1804
1838
  this.scroller = h(doc, "div", "tnc-tg-scroll");
1839
+ this.dayNavigation = h(doc, "div", "tnc-tg-day-navigation", { role: "group", "aria-label": ctx.view.title });
1840
+ this.scroller.addEventListener("scroll", () => this.updateDayNavigation());
1805
1841
  this.top = h(doc, "div", "tnc-tg-top");
1806
1842
  this.headRow = h(doc, "div", "tnc-tg-head tnc-tg-cols", { role: "row" });
1807
1843
  this.top.append(this.headRow);
@@ -1829,7 +1865,7 @@ var TimeGridView = class {
1829
1865
  } else {
1830
1866
  this.body = this.gutter = null;
1831
1867
  }
1832
- this.el.append(this.scroller);
1868
+ this.el.append(this.dayNavigation, this.scroller);
1833
1869
  this.build();
1834
1870
  if (showGrid && opts.height === "fill") {
1835
1871
  const RO = ctx.win.ResizeObserver;
@@ -1842,6 +1878,7 @@ var TimeGridView = class {
1842
1878
  ctx;
1843
1879
  el;
1844
1880
  scroller;
1881
+ dayNavigation;
1845
1882
  top;
1846
1883
  headRow;
1847
1884
  allDayRow;
@@ -1897,6 +1934,9 @@ var TimeGridView = class {
1897
1934
  head.append(cap);
1898
1935
  }
1899
1936
  this.headRow.append(head);
1937
+ const dayButton = h(doc, "button", "tnc-btn", { type: "button", "aria-label": ctx.fmt.longDate(day) }, `${ctx.fmt.weekdayShort(day)} ${ctx.fmt.dayNumber(day)}`);
1938
+ dayButton.addEventListener("click", () => this.revealDay(day));
1939
+ this.dayNavigation.append(dayButton);
1900
1940
  opts.dayHeaderDidMount?.({ ...arg, el: head });
1901
1941
  const col = h(doc, "div", ["tnc-col", ...ctx.dayClasses(day)].join(" "), { role: "cell", "aria-label": ctx.fmt.longDate(day), "data-date": arg.dateStr });
1902
1942
  const bgLayer = h(doc, "div", "tnc-col-bg");
@@ -1913,6 +1953,7 @@ var TimeGridView = class {
1913
1953
  this.body?.append(col);
1914
1954
  return {
1915
1955
  day,
1956
+ collisionColumns: 1,
1916
1957
  head,
1917
1958
  col,
1918
1959
  plan: new ItemPool(ctx, planLayer),
@@ -1969,12 +2010,17 @@ var TimeGridView = class {
1969
2010
  /* --------------------------------------------------------------- render */
1970
2011
  render(change) {
1971
2012
  this.renderAllDay();
1972
- if (!this.body) return;
2013
+ if (!this.body) {
2014
+ this.updateDayNavigation();
2015
+ return;
2016
+ }
1973
2017
  const dirty = change ? change.days : null;
1974
2018
  for (const c of this.cols) {
1975
2019
  if (dirty && !dirty.has(c.day) && !(this.ctx.slot.max > MINUTES_PER_DAY && dirty.has(c.day + 1))) continue;
1976
2020
  this.renderColumn(c);
1977
2021
  }
2022
+ this.el.style.setProperty("--tnc-collision-columns", String(Math.max(1, ...this.cols.map((column) => column.collisionColumns))));
2023
+ this.updateDayNavigation();
1978
2024
  this.renderCapacity();
1979
2025
  this.tick();
1980
2026
  }
@@ -2092,6 +2138,11 @@ var TimeGridView = class {
2092
2138
  }
2093
2139
  const minMinutes = opts.eventMinHeight / this.ppm;
2094
2140
  const layout = layoutTimedDay(items, c.day, zone, minMinutes, ctx.order, [slot.min, slot.max]);
2141
+ c.collisionColumns = Math.max(
2142
+ 1,
2143
+ ...layout.plan.map((placement) => opts.eventMaxStack > 0 ? Math.min(placement.cols, opts.eventMaxStack) : placement.cols),
2144
+ ...layout.actual.filter((placement) => !placement.split).map((placement) => placement.cols)
2145
+ );
2095
2146
  const total = this.minutes;
2096
2147
  const top = (m) => pct((m - slot.min) / total);
2097
2148
  const height = (s) => pct((s.endMin - s.startMin) / total);
@@ -2103,19 +2154,28 @@ var TimeGridView = class {
2103
2154
  c.bg.end();
2104
2155
  const hasActual = layout.actual.length > 0;
2105
2156
  c.col.classList.toggle("has-actual", hasActual);
2106
- this.placeLane(c.plan, layout.plan, c, top, height, true);
2107
- this.placeLane(c.actual, layout.actual, c, top, height, false);
2157
+ for (const link of c.more.children) link.setAttribute("data-stale", "true");
2158
+ this.placeLane(c.plan, layout.plan, c, top, height, "plan");
2159
+ this.placeLane(c.actual, layout.actual, c, top, height, "actual");
2160
+ for (const link of [...c.more.children]) if (link.hasAttribute("data-stale")) link.remove();
2108
2161
  }
2109
- placeLane(pool, placements, c, top, height, withMore) {
2162
+ placeLane(pool, placements, c, top, height, lane) {
2110
2163
  const { ctx } = this;
2111
2164
  const { opts } = ctx;
2112
- const maxStack = opts.eventMaxStack;
2165
+ const readableWidth = Number.parseFloat(ctx.win.getComputedStyle(this.el).getPropertyValue("--tnc-event-min-width")) || 128;
2166
+ const laneWidth = c.col.clientWidth * (lane === "actual" && placements.some((placement) => placement.split) ? 0.22 : 1);
2167
+ const collapse = placements.some((placement) => {
2168
+ const columns = opts.eventMaxStack > 0 ? Math.min(placement.cols, opts.eventMaxStack) : placement.cols;
2169
+ return columns > 1 && columns * readableWidth > laneWidth;
2170
+ });
2171
+ const withMore = lane === "plan" || collapse;
2172
+ const maxStack = collapse ? 1 : opts.eventMaxStack;
2113
2173
  const hiddenByCluster = /* @__PURE__ */ new Map();
2114
2174
  pool.begin();
2115
2175
  for (const p of placements) {
2116
2176
  const preview = p.item.item.preview || p.item.id === ctx.draggingId;
2117
2177
  if (!preview && withMore && maxStack > 0 && p.cols > maxStack && p.col >= maxStack - 1) {
2118
- const rec = hiddenByCluster.get(p.cluster) ?? { count: 0, start: p.startMin, end: p.endMin, items: [] };
2178
+ const rec = hiddenByCluster.get(p.cluster) ?? { count: 0, start: p.startMin, end: p.endMin, split: p.split, items: [] };
2119
2179
  rec.count++;
2120
2180
  rec.start = Math.min(rec.start, p.startMin);
2121
2181
  rec.end = Math.max(rec.end, p.endMin);
@@ -2134,24 +2194,39 @@ var TimeGridView = class {
2134
2194
  setStyle(el, {
2135
2195
  top: top(p.startMin),
2136
2196
  height: `max(${height(p)}, ${opts.eventMinHeight}px)`,
2137
- left: pct(left),
2138
- width: `calc(${pct(width)} - 1px)`,
2197
+ ...inLane(lane, p.split, left, width),
2198
+ "--tnc-event-readable-width": pct(span / cols / width),
2139
2199
  "z-index": String(p.col + 1)
2140
2200
  });
2141
2201
  }
2142
2202
  pool.end();
2143
2203
  if (!withMore) return;
2144
- c.more.replaceChildren();
2145
- for (const rec of hiddenByCluster.values()) {
2146
- const link = h(ctx.doc, "button", "tnc-more tnc-more--stack", { type: "button", "aria-haspopup": "dialog" }, `+${rec.count}`);
2204
+ for (const [cluster, rec] of hiddenByCluster) {
2205
+ const key = `${lane}-${cluster}`;
2206
+ let link = c.more.querySelector(`[data-cluster="${key}"]`);
2207
+ if (!link) {
2208
+ link = h(ctx.doc, "button", "tnc-more tnc-more--stack", { type: "button", "aria-haspopup": "dialog", "data-cluster": key });
2209
+ const anchor = link;
2210
+ link.addEventListener("click", (ev) => ctx.openMore(c.day, anchor, ev, JSON.parse(anchor.dataset.itemIds ?? "[]")));
2211
+ c.more.append(link);
2212
+ }
2213
+ link.removeAttribute("data-stale");
2214
+ link.dataset.itemIds = JSON.stringify(placements.filter((placement) => placement.cluster === cluster).map((placement) => placement.item.id));
2215
+ link.classList.toggle("tnc-more--collision", collapse);
2216
+ link.textContent = `+${rec.count}`;
2217
+ if (collapse) {
2218
+ link.textContent = ctx.text.appointments(rec.count);
2219
+ link.setAttribute("aria-label", link.textContent);
2220
+ link.title = link.getAttribute("aria-label");
2221
+ } else {
2222
+ link.removeAttribute("aria-label");
2223
+ link.removeAttribute("title");
2224
+ }
2147
2225
  const cols = maxStack;
2148
2226
  setStyle(link, {
2149
2227
  top: top(rec.start),
2150
- left: pct((cols - 1) / cols),
2151
- width: `calc(${pct(1 / cols)} - 1px)`
2228
+ ...inLane(lane, rec.split, (cols - 1) / cols, 1 / cols)
2152
2229
  });
2153
- link.addEventListener("click", (ev) => ctx.openMore(c.day, link, ev));
2154
- c.more.append(link);
2155
2230
  }
2156
2231
  }
2157
2232
  meta(s, bg) {
@@ -2262,6 +2337,26 @@ var TimeGridView = class {
2262
2337
  }
2263
2338
  resize() {
2264
2339
  this.fitRows();
2340
+ this.render(null);
2341
+ }
2342
+ revealDay(day) {
2343
+ const column = this.cols.find((candidate) => candidate.day === day);
2344
+ if (!column) return;
2345
+ const bounds = column.head.getBoundingClientRect();
2346
+ const viewport = this.scroller.getBoundingClientRect();
2347
+ this.scroller.scrollLeft += bounds.left - viewport.left - (viewport.width - bounds.width) / 2;
2348
+ this.updateDayNavigation();
2349
+ }
2350
+ updateDayNavigation() {
2351
+ this.dayNavigation.hidden = this.scroller.scrollWidth <= this.scroller.clientWidth + 1;
2352
+ const viewport = this.scroller.getBoundingClientRect();
2353
+ this.cols.forEach((column, index) => {
2354
+ const bounds = column.head.getBoundingClientRect();
2355
+ const visible = bounds.left >= viewport.left && bounds.right <= viewport.right;
2356
+ const button = this.dayNavigation.children[index];
2357
+ button?.classList.toggle("is-offscreen", !visible);
2358
+ button?.setAttribute("aria-pressed", String(visible));
2359
+ });
2265
2360
  }
2266
2361
  scrollToTime(minute) {
2267
2362
  if (!this.body) return;
@@ -2934,6 +3029,7 @@ ${this.ctx.text.more(items.length - 8)}` : ""}` : label;
2934
3029
 
2935
3030
  // src/dom/calendar.ts
2936
3031
  var EXTERNAL_ID = "__tnc_external__";
3032
+ var SVG_NS = "http://www.w3.org/2000/svg";
2937
3033
  var MAX_AGENDA_DAYS = 366;
2938
3034
  function sameOption(a, b, depth = 0) {
2939
3035
  if (a === b) return true;
@@ -3026,7 +3122,11 @@ var Calendar = class {
3026
3122
  this.resizeObs?.disconnect();
3027
3123
  const RO = this.win.ResizeObserver;
3028
3124
  if (RO) {
3029
- this.resizeObs = new RO(() => this.current?.resize?.());
3125
+ this.resizeObs = new RO(() => {
3126
+ this.syncToolbarViewDisplay();
3127
+ this.current?.resize?.();
3128
+ this.positionMore();
3129
+ });
3030
3130
  this.resizeObs.observe(this.root);
3031
3131
  }
3032
3132
  }
@@ -3046,9 +3146,11 @@ var Calendar = class {
3046
3146
  if (this.frame !== null) old.cancelAnimationFrame(this.frame);
3047
3147
  this.timer = this.frame = null;
3048
3148
  this.doc.removeEventListener("pointerdown", this.onDocPointerDown, true);
3149
+ this.listenMoreGeometry(false);
3049
3150
  this.doc = doc;
3050
3151
  this.win = doc.defaultView ?? old;
3051
3152
  this.doc.addEventListener("pointerdown", this.onDocPointerDown, true);
3153
+ this.listenMoreGeometry(true);
3052
3154
  this.observeResize();
3053
3155
  this.rebuild(false);
3054
3156
  this.scheduleTick();
@@ -3166,6 +3268,7 @@ var Calendar = class {
3166
3268
  goToday() {
3167
3269
  this.anchor = this.todayDay();
3168
3270
  this.rebuild(true);
3271
+ if (this.current instanceof TimeGridView) this.current.revealDay(this.anchor);
3169
3272
  }
3170
3273
  /** The anchor date, as local midnight of that civil day in the calendar zone. */
3171
3274
  getDate() {
@@ -3184,6 +3287,7 @@ var Calendar = class {
3184
3287
  /** Re-measures (e.g. after the host was shown or resized by a parent). */
3185
3288
  updateSize() {
3186
3289
  this.current?.resize?.();
3290
+ this.positionMore();
3187
3291
  }
3188
3292
  getScroll() {
3189
3293
  return this.current?.getScroll() ?? { top: 0, left: 0 };
@@ -3215,7 +3319,10 @@ var Calendar = class {
3215
3319
  }
3216
3320
  const view = this.current;
3217
3321
  if (view instanceof TimelineView) view.scrollToInstant(ms, opts.align ?? "center", opts.smooth ?? false);
3218
- else if (view instanceof TimeGridView && p.kind !== "day") view.scrollToTime(wallTime(ms, this.zone).minute);
3322
+ else if (view instanceof TimeGridView) {
3323
+ view.revealDay(day);
3324
+ if (p.kind !== "day") view.scrollToTime(wallTime(ms, this.zone).minute);
3325
+ }
3219
3326
  }
3220
3327
  /** What is under a client point: a time slot, a day, or a timeline row and date. Null outside the view. */
3221
3328
  hitTest(clientX, clientY) {
@@ -3390,9 +3497,10 @@ var Calendar = class {
3390
3497
  if (meta) this.opts.itemWillUnmount?.(this.itemArg(el, meta));
3391
3498
  el.remove();
3392
3499
  }
3393
- openMore(day, anchor, jsEvent) {
3500
+ openMore(day, anchor, jsEvent, itemIds) {
3394
3501
  jsEvent.stopPropagation();
3395
- 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));
3502
+ const group = itemIds ? new Set(itemIds) : null;
3503
+ 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));
3396
3504
  const shown = new Set(
3397
3505
  [...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 ?? "")
3398
3506
  );
@@ -3421,15 +3529,7 @@ var Calendar = class {
3421
3529
  }
3422
3530
  pool.end();
3423
3531
  this.popover = { el, pool, anchor, day };
3424
- const rr = this.root.getBoundingClientRect();
3425
- const ar = anchor.getBoundingClientRect();
3426
- const w = Math.max(220, ar.width + 24);
3427
- let left = ar.left - rr.left - 12;
3428
- left = Math.max(4, Math.min(left, rr.width - w - 4));
3429
- let top = ar.top - rr.top - 40;
3430
- const height = el.offsetHeight;
3431
- top = Math.max(4, Math.min(top, rr.height - height - 4));
3432
- setStyle(el, { left, top, width: w });
3532
+ this.positionMore();
3433
3533
  el.focus();
3434
3534
  }
3435
3535
  /* =============================================================== render */
@@ -3553,8 +3653,10 @@ var Calendar = class {
3553
3653
  this.current.setScroll({ top: keep.top, left: 0 });
3554
3654
  this.current.placeInstant(keep.ms, keep.vx);
3555
3655
  } else if (scroll) this.current.setScroll(scroll);
3556
- else if (this.current instanceof TimeGridView) this.current.scrollToTime(this.slot.scroll);
3557
- else if (this.current instanceof TimelineView) this.current.scrollToDay(this.anchor);
3656
+ else if (this.current instanceof TimeGridView) {
3657
+ this.current.scrollToTime(this.slot.scroll);
3658
+ this.current.revealDay(this.anchor);
3659
+ } else if (this.current instanceof TimelineView) this.current.scrollToDay(this.anchor);
3558
3660
  if (rangeChanged) this.opts.onRangeChange?.(this.view);
3559
3661
  }
3560
3662
  /** Sets the visible days and view info from the anchor. */
@@ -3702,28 +3804,65 @@ var Calendar = class {
3702
3804
  if (e.icon) b.replaceChildren(e.icon());
3703
3805
  return b;
3704
3806
  });
3807
+ const chevron = (d) => {
3808
+ const svg = this.doc.createElementNS(SVG_NS, "svg");
3809
+ setAttrs(svg, { viewBox: "0 0 24 24", "aria-hidden": "true", focusable: "false" });
3810
+ const path = this.doc.createElementNS(SVG_NS, "path");
3811
+ path.setAttribute("d", d);
3812
+ svg.append(path);
3813
+ return svg;
3814
+ };
3815
+ const prev = btn("", this.text.prev, "tnc-btn-prev", () => this.prev());
3816
+ prev.append(chevron("m15 18-6-6 6-6"));
3817
+ const next = btn("", this.text.next, "tnc-btn-next", () => this.next());
3818
+ next.append(chevron("m9 18 6-6-6-6"));
3705
3819
  const nav = h(
3706
3820
  this.doc,
3707
3821
  "div",
3708
3822
  "tnc-toolbar-group tnc-toolbar-nav",
3709
3823
  null,
3710
- btn("\u2039", this.text.prev, "tnc-btn-prev", () => this.prev()),
3824
+ prev,
3711
3825
  btn(this.text.today, this.text.today, "tnc-btn-today", () => this.goToday()),
3712
- btn("\u203A", this.text.next, "tnc-btn-next", () => this.next())
3826
+ next
3713
3827
  );
3714
3828
  const title = h(this.doc, "h2", "tnc-title", { "aria-live": "polite" }, this.view.title);
3715
3829
  const viewGroup = h(this.doc, "div", "tnc-toolbar-group tnc-toolbar-views", { role: "group" });
3830
+ const viewSelect = h(this.doc, "select", "tnc-btn tnc-toolbar-view-select", {
3831
+ "aria-label": this.text.viewHints[this.opts.view] ?? this.text.views[this.opts.view] ?? this.opts.view
3832
+ });
3716
3833
  for (const v of views) {
3717
3834
  const label = v === "days" ? this.text.views.days ?? `${this.opts.dayCount} days` : this.text.views[v] ?? v;
3718
3835
  viewGroup.append(btn(label, this.text.viewHints[v] ?? label, `tnc-btn-view tnc-btn-view-${v}`, () => this.changeView(v), v === this.opts.view));
3836
+ viewSelect.append(h(this.doc, "option", "", { value: v }, label));
3719
3837
  }
3838
+ viewSelect.value = this.opts.view;
3839
+ viewSelect.addEventListener("change", () => this.changeView(viewSelect.value));
3720
3840
  const start = h(this.doc, "div", "tnc-toolbar-start");
3721
3841
  const end = h(this.doc, "div", "tnc-toolbar-end");
3722
3842
  if (cfg.extraPlacement === "start") start.append(...extras);
3723
3843
  start.append(nav, title);
3724
- end.append(viewGroup);
3844
+ end.append(viewGroup, viewSelect);
3725
3845
  if (cfg.extraPlacement !== "start") end.append(...extras);
3726
3846
  this.toolbarEl.append(start, end);
3847
+ this.syncToolbarViewDisplay();
3848
+ }
3849
+ syncToolbarViewDisplay() {
3850
+ const buttons = this.toolbarEl.querySelector(".tnc-toolbar-views");
3851
+ const select = this.toolbarEl.querySelector(".tnc-toolbar-view-select");
3852
+ const start = this.toolbarEl.querySelector(".tnc-toolbar-start");
3853
+ if (!buttons || !select || !start) return;
3854
+ const config = this.opts.toolbar;
3855
+ const mode = typeof config === "object" ? config.viewDisplay ?? "auto" : "auto";
3856
+ buttons.hidden = false;
3857
+ select.hidden = true;
3858
+ buttons.style.display = "flex";
3859
+ select.style.display = "none";
3860
+ const startWidth = Array.from(start.children).reduce((width, child) => width + child.scrollWidth, 0);
3861
+ const compact = mode === "select" || mode === "auto" && startWidth + buttons.scrollWidth + 32 > this.root.clientWidth;
3862
+ buttons.hidden = compact;
3863
+ select.hidden = !compact;
3864
+ buttons.style.display = compact ? "none" : "flex";
3865
+ select.style.display = compact ? "" : "none";
3727
3866
  }
3728
3867
  applyChange(change) {
3729
3868
  if (!change.added.length && !change.updated.length && !change.removed.length && !change.days.size && !change.spans) return;
@@ -3856,6 +3995,38 @@ var Calendar = class {
3856
3995
  }
3857
3996
  }
3858
3997
  /* ============================================================== popover */
3998
+ onMoreGeometryChange = () => this.positionMore();
3999
+ listenMoreGeometry(on) {
4000
+ const method = on ? "addEventListener" : "removeEventListener";
4001
+ this.doc[method]("scroll", this.onMoreGeometryChange, true);
4002
+ this.win[method]("resize", this.onMoreGeometryChange);
4003
+ this.win.visualViewport?.[method]("resize", this.onMoreGeometryChange);
4004
+ this.win.visualViewport?.[method]("scroll", this.onMoreGeometryChange);
4005
+ }
4006
+ positionMore() {
4007
+ const popover = this.popover;
4008
+ if (!popover) return;
4009
+ if (!popover.anchor.isConnected) {
4010
+ this.closeMore();
4011
+ return;
4012
+ }
4013
+ const viewport = this.win.visualViewport;
4014
+ const root = this.root.getBoundingClientRect();
4015
+ const anchor = popover.anchor.getBoundingClientRect();
4016
+ const leftEdge = Math.max(root.left, viewport?.offsetLeft ?? 0) + 4;
4017
+ const topEdge = Math.max(root.top, viewport?.offsetTop ?? 0) + 4;
4018
+ const rightEdge = Math.min(root.right, (viewport?.offsetLeft ?? 0) + (viewport?.width ?? this.win.innerWidth)) - 4;
4019
+ const bottomEdge = Math.min(root.bottom, (viewport?.offsetTop ?? 0) + (viewport?.height ?? this.win.innerHeight)) - 4;
4020
+ if (rightEdge <= leftEdge || bottomEdge <= topEdge) {
4021
+ this.closeMore();
4022
+ return;
4023
+ }
4024
+ const width = Math.min(360, rightEdge - leftEdge);
4025
+ setStyle(popover.el, { width, "max-height": Math.min(420, bottomEdge - topEdge) });
4026
+ const left = Math.max(leftEdge, Math.min(anchor.left - 12, rightEdge - width));
4027
+ const top = Math.max(topEdge, Math.min(anchor.top - 40, bottomEdge - popover.el.offsetHeight));
4028
+ setStyle(popover.el, { left: left - root.left, top: top - root.top });
4029
+ }
3859
4030
  refreshMore() {
3860
4031
  const p = this.popover;
3861
4032
  if (!p) return;
@@ -3865,6 +4036,7 @@ var Calendar = class {
3865
4036
  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 });
3866
4037
  }
3867
4038
  p.pool.end();
4039
+ this.positionMore();
3868
4040
  }
3869
4041
  closeMore(restoreFocus = false) {
3870
4042
  const p = this.popover;
@@ -3892,6 +4064,7 @@ var Calendar = class {
3892
4064
  r.addEventListener("pointerover", this.onAdoptCheck);
3893
4065
  r.addEventListener("focusin", this.onAdoptCheck);
3894
4066
  this.doc.addEventListener("pointerdown", this.onDocPointerDown, true);
4067
+ this.listenMoreGeometry(true);
3895
4068
  }
3896
4069
  unbindEvents() {
3897
4070
  const r = this.root;
@@ -3910,6 +4083,7 @@ var Calendar = class {
3910
4083
  r.removeEventListener("pointerover", this.onAdoptCheck);
3911
4084
  r.removeEventListener("focusin", this.onAdoptCheck);
3912
4085
  this.doc.removeEventListener("pointerdown", this.onDocPointerDown, true);
4086
+ this.listenMoreGeometry(false);
3913
4087
  this.listenDoc(false);
3914
4088
  }
3915
4089
  listenDoc(on) {
@@ -3955,6 +4129,7 @@ var Calendar = class {
3955
4129
  mode = "select";
3956
4130
  }
3957
4131
  const touch = ev.pointerType === "touch";
4132
+ const overflowOrigin = itemEl?.closest(".tnc-popover") && origin ? wallTime(origin.startMs, this.zone) : null;
3958
4133
  const s = {
3959
4134
  mode,
3960
4135
  pointerId: ev.pointerId,
@@ -3963,7 +4138,7 @@ var Calendar = class {
3963
4138
  y0: ev.clientY,
3964
4139
  active: false,
3965
4140
  origin,
3966
- grab: this.current?.hit(ev.clientX, ev.clientY) ?? null,
4141
+ grab: overflowOrigin && this.current instanceof TimeGridView && !origin?.allDay ? { kind: "time", day: overflowOrigin.day, minute: overflowOrigin.minute } : this.current?.hit(ev.clientX, ev.clientY) ?? null,
3967
4142
  proposal: null,
3968
4143
  denied: false,
3969
4144
  timer: null,
@@ -4466,6 +4641,25 @@ var Calendar = class {
4466
4641
  if (el && meta) this.opts.onItemHover?.({ ...this.itemArg(el, meta), jsEvent: ev, entering: false });
4467
4642
  }
4468
4643
  onKeyDown = (ev) => {
4644
+ if (this.popover && !ev.altKey && !ev.ctrlKey && !ev.metaKey && ["ArrowDown", "ArrowUp", "Home", "End"].includes(ev.key)) {
4645
+ const rows = [...this.popover.el.querySelectorAll(".tnc-item")];
4646
+ const index = rows.indexOf(this.doc.activeElement);
4647
+ 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;
4648
+ ev.preventDefault();
4649
+ ev.stopPropagation();
4650
+ rows[next]?.focus();
4651
+ rows[next]?.scrollIntoView({ block: "nearest", inline: "nearest" });
4652
+ return;
4653
+ }
4654
+ if (ev.key === "Tab" && this.popover) {
4655
+ const controls = [...this.popover.el.querySelectorAll('button, a[href], [tabindex="0"]')];
4656
+ const index = controls.indexOf(this.doc.activeElement);
4657
+ const next = ev.shiftKey ? index <= 0 ? controls.length - 1 : index - 1 : (index + 1) % controls.length;
4658
+ ev.preventDefault();
4659
+ ev.stopPropagation();
4660
+ controls[next]?.focus();
4661
+ return;
4662
+ }
4469
4663
  if (ev.key === "Escape" && this.popover) {
4470
4664
  ev.preventDefault();
4471
4665
  ev.stopPropagation();