@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.
@@ -152,6 +152,7 @@ var DEFAULT_TEXT = {
152
152
  allDay: "All day",
153
153
  noItems: "Nothing scheduled",
154
154
  more: (n) => `+${n} more`,
155
+ appointments: (n) => `${n} appointments`,
155
156
  itemCount: (n) => n === 1 ? "1 item" : `${n} items`,
156
157
  weekNumber: (n) => `W${n}`,
157
158
  views: { week: "Week", day: "Day", days: "Days", month: "Month", list: "List", year: "Year", timeline: "Timeline" },
@@ -221,6 +222,7 @@ var LAYOUT_KEYS = /* @__PURE__ */ new Set([
221
222
  "dateStrip",
222
223
  "listLeadTitle",
223
224
  "listAddText",
225
+ "listEmptyContent",
224
226
  "monthDisplay",
225
227
  "timeZone",
226
228
  "locale",
@@ -546,19 +548,27 @@ var DayGridView = class {
546
548
  });
547
549
  }
548
550
  w.pool.end();
549
- w.more.replaceChildren();
551
+ const existing = new Map(Array.from(w.more.children, (element) => [element.getAttribute("data-day"), element]));
550
552
  hidden.forEach((items, col) => {
551
553
  if (!items.length) return;
552
554
  const day = w.days[col];
553
- const link = h(ctx.doc, "button", "tnc-more", { type: "button", "aria-haspopup": "dialog" }, ctx.text.more(items.length));
555
+ const key = String(day);
556
+ let link = existing.get(key);
557
+ if (!link) {
558
+ link = h(ctx.doc, "button", "tnc-more", { type: "button", "aria-haspopup": "dialog", "data-day": key });
559
+ const anchor = link;
560
+ link.addEventListener("click", (ev) => ctx.openMore(day, anchor, ev));
561
+ w.more.append(link);
562
+ }
563
+ existing.delete(key);
564
+ link.textContent = ctx.text.more(items.length);
554
565
  setStyle(link, {
555
566
  left: `calc(${pct(col / n)} + 2px)`,
556
567
  width: `calc(${pct(1 / n)} - 4px)`,
557
568
  top: `calc(var(--tnc-dg-top-h) + ${visible(col)} * (var(--tnc-bar-h) + ${BAR_GAP}px))`
558
569
  });
559
- link.addEventListener("click", (ev) => ctx.openMore(day, link, ev));
560
- w.more.append(link);
561
570
  });
571
+ for (const link of existing.values()) link.remove();
562
572
  const step = this.barPx + BAR_GAP;
563
573
  let need = maxLevel >= 0 ? (maxLevel + 1) * step - BAR_GAP : 0;
564
574
  hidden.forEach((items, col) => {
@@ -761,6 +771,8 @@ var ListView = class {
761
771
  if (!keep.size && !leadItems.length) {
762
772
  if (!empty) {
763
773
  empty = h(doc, "div", "tnc-list-empty", { role: "listitem" }, ctx.text.noItems);
774
+ const content = ctx.opts.listEmptyContent?.();
775
+ if (content) empty.replaceChildren(content);
764
776
  this.body.append(empty);
765
777
  }
766
778
  } else empty?.remove();
@@ -1109,6 +1121,8 @@ var TimeGridView = class {
1109
1121
  this.el = h(doc, "div", "tnc-tg", { role: "table", "aria-label": ctx.view.title });
1110
1122
  this.el.classList.toggle("tnc-tg--no-grid", !showGrid);
1111
1123
  this.scroller = h(doc, "div", "tnc-tg-scroll");
1124
+ this.dayNavigation = h(doc, "div", "tnc-tg-day-navigation", { role: "group", "aria-label": ctx.view.title });
1125
+ this.scroller.addEventListener("scroll", () => this.updateDayNavigation());
1112
1126
  this.top = h(doc, "div", "tnc-tg-top");
1113
1127
  this.headRow = h(doc, "div", "tnc-tg-head tnc-tg-cols", { role: "row" });
1114
1128
  this.top.append(this.headRow);
@@ -1136,7 +1150,7 @@ var TimeGridView = class {
1136
1150
  } else {
1137
1151
  this.body = this.gutter = null;
1138
1152
  }
1139
- this.el.append(this.scroller);
1153
+ this.el.append(this.dayNavigation, this.scroller);
1140
1154
  this.build();
1141
1155
  if (showGrid && opts.height === "fill") {
1142
1156
  const RO = ctx.win.ResizeObserver;
@@ -1149,6 +1163,7 @@ var TimeGridView = class {
1149
1163
  ctx;
1150
1164
  el;
1151
1165
  scroller;
1166
+ dayNavigation;
1152
1167
  top;
1153
1168
  headRow;
1154
1169
  allDayRow;
@@ -1204,6 +1219,9 @@ var TimeGridView = class {
1204
1219
  head.append(cap);
1205
1220
  }
1206
1221
  this.headRow.append(head);
1222
+ const dayButton = h(doc, "button", "tnc-btn", { type: "button", "aria-label": ctx.fmt.longDate(day) }, `${ctx.fmt.weekdayShort(day)} ${ctx.fmt.dayNumber(day)}`);
1223
+ dayButton.addEventListener("click", () => this.revealDay(day));
1224
+ this.dayNavigation.append(dayButton);
1207
1225
  opts.dayHeaderDidMount?.({ ...arg, el: head });
1208
1226
  const col = h(doc, "div", ["tnc-col", ...ctx.dayClasses(day)].join(" "), { role: "cell", "aria-label": ctx.fmt.longDate(day), "data-date": arg.dateStr });
1209
1227
  const bgLayer = h(doc, "div", "tnc-col-bg");
@@ -1220,6 +1238,7 @@ var TimeGridView = class {
1220
1238
  this.body?.append(col);
1221
1239
  return {
1222
1240
  day,
1241
+ collisionColumns: 1,
1223
1242
  head,
1224
1243
  col,
1225
1244
  plan: new ItemPool(ctx, planLayer),
@@ -1276,12 +1295,17 @@ var TimeGridView = class {
1276
1295
  /* --------------------------------------------------------------- render */
1277
1296
  render(change) {
1278
1297
  this.renderAllDay();
1279
- if (!this.body) return;
1298
+ if (!this.body) {
1299
+ this.updateDayNavigation();
1300
+ return;
1301
+ }
1280
1302
  const dirty = change ? change.days : null;
1281
1303
  for (const c of this.cols) {
1282
1304
  if (dirty && !dirty.has(c.day) && !(this.ctx.slot.max > MINUTES_PER_DAY && dirty.has(c.day + 1))) continue;
1283
1305
  this.renderColumn(c);
1284
1306
  }
1307
+ this.el.style.setProperty("--tnc-collision-columns", String(Math.max(1, ...this.cols.map((column) => column.collisionColumns))));
1308
+ this.updateDayNavigation();
1285
1309
  this.renderCapacity();
1286
1310
  this.tick();
1287
1311
  }
@@ -1399,6 +1423,11 @@ var TimeGridView = class {
1399
1423
  }
1400
1424
  const minMinutes = opts.eventMinHeight / this.ppm;
1401
1425
  const layout = layoutTimedDay(items, c.day, zone, minMinutes, ctx.order, [slot.min, slot.max]);
1426
+ c.collisionColumns = Math.max(
1427
+ 1,
1428
+ ...layout.plan.map((placement) => opts.eventMaxStack > 0 ? Math.min(placement.cols, opts.eventMaxStack) : placement.cols),
1429
+ ...layout.actual.filter((placement) => !placement.split).map((placement) => placement.cols)
1430
+ );
1402
1431
  const total = this.minutes;
1403
1432
  const top = (m) => pct((m - slot.min) / total);
1404
1433
  const height = (s) => pct((s.endMin - s.startMin) / total);
@@ -1410,14 +1439,22 @@ var TimeGridView = class {
1410
1439
  c.bg.end();
1411
1440
  const hasActual = layout.actual.length > 0;
1412
1441
  c.col.classList.toggle("has-actual", hasActual);
1442
+ for (const link of c.more.children) link.setAttribute("data-stale", "true");
1413
1443
  this.placeLane(c.plan, layout.plan, c, top, height, "plan");
1414
1444
  this.placeLane(c.actual, layout.actual, c, top, height, "actual");
1445
+ for (const link of [...c.more.children]) if (link.hasAttribute("data-stale")) link.remove();
1415
1446
  }
1416
1447
  placeLane(pool, placements, c, top, height, lane) {
1417
1448
  const { ctx } = this;
1418
1449
  const { opts } = ctx;
1419
- const withMore = lane === "plan";
1420
- const maxStack = opts.eventMaxStack;
1450
+ const readableWidth = Number.parseFloat(ctx.win.getComputedStyle(this.el).getPropertyValue("--tnc-event-min-width")) || 128;
1451
+ const laneWidth = c.col.clientWidth * (lane === "actual" && placements.some((placement) => placement.split) ? 0.22 : 1);
1452
+ const collapse = placements.some((placement) => {
1453
+ const columns = opts.eventMaxStack > 0 ? Math.min(placement.cols, opts.eventMaxStack) : placement.cols;
1454
+ return columns > 1 && columns * readableWidth > laneWidth;
1455
+ });
1456
+ const withMore = lane === "plan" || collapse;
1457
+ const maxStack = collapse ? 1 : opts.eventMaxStack;
1421
1458
  const hiddenByCluster = /* @__PURE__ */ new Map();
1422
1459
  pool.begin();
1423
1460
  for (const p of placements) {
@@ -1443,21 +1480,38 @@ var TimeGridView = class {
1443
1480
  top: top(p.startMin),
1444
1481
  height: `max(${height(p)}, ${opts.eventMinHeight}px)`,
1445
1482
  ...inLane(lane, p.split, left, width),
1483
+ "--tnc-event-readable-width": pct(span / cols / width),
1446
1484
  "z-index": String(p.col + 1)
1447
1485
  });
1448
1486
  }
1449
1487
  pool.end();
1450
1488
  if (!withMore) return;
1451
- c.more.replaceChildren();
1452
- for (const rec of hiddenByCluster.values()) {
1453
- const link = h(ctx.doc, "button", "tnc-more tnc-more--stack", { type: "button", "aria-haspopup": "dialog" }, `+${rec.count}`);
1489
+ for (const [cluster, rec] of hiddenByCluster) {
1490
+ const key = `${lane}-${cluster}`;
1491
+ let link = c.more.querySelector(`[data-cluster="${key}"]`);
1492
+ if (!link) {
1493
+ link = h(ctx.doc, "button", "tnc-more tnc-more--stack", { type: "button", "aria-haspopup": "dialog", "data-cluster": key });
1494
+ const anchor = link;
1495
+ link.addEventListener("click", (ev) => ctx.openMore(c.day, anchor, ev, JSON.parse(anchor.dataset.itemIds ?? "[]")));
1496
+ c.more.append(link);
1497
+ }
1498
+ link.removeAttribute("data-stale");
1499
+ link.dataset.itemIds = JSON.stringify(placements.filter((placement) => placement.cluster === cluster).map((placement) => placement.item.id));
1500
+ link.classList.toggle("tnc-more--collision", collapse);
1501
+ link.textContent = `+${rec.count}`;
1502
+ if (collapse) {
1503
+ link.textContent = ctx.text.appointments(rec.count);
1504
+ link.setAttribute("aria-label", link.textContent);
1505
+ link.title = link.getAttribute("aria-label");
1506
+ } else {
1507
+ link.removeAttribute("aria-label");
1508
+ link.removeAttribute("title");
1509
+ }
1454
1510
  const cols = maxStack;
1455
1511
  setStyle(link, {
1456
1512
  top: top(rec.start),
1457
1513
  ...inLane(lane, rec.split, (cols - 1) / cols, 1 / cols)
1458
1514
  });
1459
- link.addEventListener("click", (ev) => ctx.openMore(c.day, link, ev));
1460
- c.more.append(link);
1461
1515
  }
1462
1516
  }
1463
1517
  meta(s, bg) {
@@ -1568,6 +1622,26 @@ var TimeGridView = class {
1568
1622
  }
1569
1623
  resize() {
1570
1624
  this.fitRows();
1625
+ this.render(null);
1626
+ }
1627
+ revealDay(day) {
1628
+ const column = this.cols.find((candidate) => candidate.day === day);
1629
+ if (!column) return;
1630
+ const bounds = column.head.getBoundingClientRect();
1631
+ const viewport = this.scroller.getBoundingClientRect();
1632
+ this.scroller.scrollLeft += bounds.left - viewport.left - (viewport.width - bounds.width) / 2;
1633
+ this.updateDayNavigation();
1634
+ }
1635
+ updateDayNavigation() {
1636
+ this.dayNavigation.hidden = this.scroller.scrollWidth <= this.scroller.clientWidth + 1;
1637
+ const viewport = this.scroller.getBoundingClientRect();
1638
+ this.cols.forEach((column, index) => {
1639
+ const bounds = column.head.getBoundingClientRect();
1640
+ const visible = bounds.left >= viewport.left && bounds.right <= viewport.right;
1641
+ const button = this.dayNavigation.children[index];
1642
+ button?.classList.toggle("is-offscreen", !visible);
1643
+ button?.setAttribute("aria-pressed", String(visible));
1644
+ });
1571
1645
  }
1572
1646
  scrollToTime(minute) {
1573
1647
  if (!this.body) return;
@@ -2333,7 +2407,11 @@ var Calendar = class {
2333
2407
  this.resizeObs?.disconnect();
2334
2408
  const RO = this.win.ResizeObserver;
2335
2409
  if (RO) {
2336
- this.resizeObs = new RO(() => this.current?.resize?.());
2410
+ this.resizeObs = new RO(() => {
2411
+ this.syncToolbarViewDisplay();
2412
+ this.current?.resize?.();
2413
+ this.positionMore();
2414
+ });
2337
2415
  this.resizeObs.observe(this.root);
2338
2416
  }
2339
2417
  }
@@ -2353,9 +2431,11 @@ var Calendar = class {
2353
2431
  if (this.frame !== null) old.cancelAnimationFrame(this.frame);
2354
2432
  this.timer = this.frame = null;
2355
2433
  this.doc.removeEventListener("pointerdown", this.onDocPointerDown, true);
2434
+ this.listenMoreGeometry(false);
2356
2435
  this.doc = doc;
2357
2436
  this.win = doc.defaultView ?? old;
2358
2437
  this.doc.addEventListener("pointerdown", this.onDocPointerDown, true);
2438
+ this.listenMoreGeometry(true);
2359
2439
  this.observeResize();
2360
2440
  this.rebuild(false);
2361
2441
  this.scheduleTick();
@@ -2473,6 +2553,7 @@ var Calendar = class {
2473
2553
  goToday() {
2474
2554
  this.anchor = this.todayDay();
2475
2555
  this.rebuild(true);
2556
+ if (this.current instanceof TimeGridView) this.current.revealDay(this.anchor);
2476
2557
  }
2477
2558
  /** The anchor date, as local midnight of that civil day in the calendar zone. */
2478
2559
  getDate() {
@@ -2491,6 +2572,7 @@ var Calendar = class {
2491
2572
  /** Re-measures (e.g. after the host was shown or resized by a parent). */
2492
2573
  updateSize() {
2493
2574
  this.current?.resize?.();
2575
+ this.positionMore();
2494
2576
  }
2495
2577
  getScroll() {
2496
2578
  return this.current?.getScroll() ?? { top: 0, left: 0 };
@@ -2522,7 +2604,10 @@ var Calendar = class {
2522
2604
  }
2523
2605
  const view = this.current;
2524
2606
  if (view instanceof TimelineView) view.scrollToInstant(ms, opts.align ?? "center", opts.smooth ?? false);
2525
- else if (view instanceof TimeGridView && p.kind !== "day") view.scrollToTime(wallTime(ms, this.zone).minute);
2607
+ else if (view instanceof TimeGridView) {
2608
+ view.revealDay(day);
2609
+ if (p.kind !== "day") view.scrollToTime(wallTime(ms, this.zone).minute);
2610
+ }
2526
2611
  }
2527
2612
  /** What is under a client point: a time slot, a day, or a timeline row and date. Null outside the view. */
2528
2613
  hitTest(clientX, clientY) {
@@ -2697,9 +2782,10 @@ var Calendar = class {
2697
2782
  if (meta) this.opts.itemWillUnmount?.(this.itemArg(el, meta));
2698
2783
  el.remove();
2699
2784
  }
2700
- openMore(day, anchor, jsEvent) {
2785
+ openMore(day, anchor, jsEvent, itemIds) {
2701
2786
  jsEvent.stopPropagation();
2702
- 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));
2787
+ const group = itemIds ? new Set(itemIds) : null;
2788
+ 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));
2703
2789
  const shown = new Set(
2704
2790
  [...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 ?? "")
2705
2791
  );
@@ -2728,15 +2814,7 @@ var Calendar = class {
2728
2814
  }
2729
2815
  pool.end();
2730
2816
  this.popover = { el, pool, anchor, day };
2731
- const rr = this.root.getBoundingClientRect();
2732
- const ar = anchor.getBoundingClientRect();
2733
- const w = Math.max(220, ar.width + 24);
2734
- let left = ar.left - rr.left - 12;
2735
- left = Math.max(4, Math.min(left, rr.width - w - 4));
2736
- let top = ar.top - rr.top - 40;
2737
- const height = el.offsetHeight;
2738
- top = Math.max(4, Math.min(top, rr.height - height - 4));
2739
- setStyle(el, { left, top, width: w });
2817
+ this.positionMore();
2740
2818
  el.focus();
2741
2819
  }
2742
2820
  /* =============================================================== render */
@@ -2860,8 +2938,10 @@ var Calendar = class {
2860
2938
  this.current.setScroll({ top: keep.top, left: 0 });
2861
2939
  this.current.placeInstant(keep.ms, keep.vx);
2862
2940
  } else if (scroll) this.current.setScroll(scroll);
2863
- else if (this.current instanceof TimeGridView) this.current.scrollToTime(this.slot.scroll);
2864
- else if (this.current instanceof TimelineView) this.current.scrollToDay(this.anchor);
2941
+ else if (this.current instanceof TimeGridView) {
2942
+ this.current.scrollToTime(this.slot.scroll);
2943
+ this.current.revealDay(this.anchor);
2944
+ } else if (this.current instanceof TimelineView) this.current.scrollToDay(this.anchor);
2865
2945
  if (rangeChanged) this.opts.onRangeChange?.(this.view);
2866
2946
  }
2867
2947
  /** Sets the visible days and view info from the anchor. */
@@ -3032,17 +3112,42 @@ var Calendar = class {
3032
3112
  );
3033
3113
  const title = h(this.doc, "h2", "tnc-title", { "aria-live": "polite" }, this.view.title);
3034
3114
  const viewGroup = h(this.doc, "div", "tnc-toolbar-group tnc-toolbar-views", { role: "group" });
3115
+ const viewSelect = h(this.doc, "select", "tnc-btn tnc-toolbar-view-select", {
3116
+ "aria-label": this.text.viewHints[this.opts.view] ?? this.text.views[this.opts.view] ?? this.opts.view
3117
+ });
3035
3118
  for (const v of views) {
3036
3119
  const label = v === "days" ? this.text.views.days ?? `${this.opts.dayCount} days` : this.text.views[v] ?? v;
3037
3120
  viewGroup.append(btn(label, this.text.viewHints[v] ?? label, `tnc-btn-view tnc-btn-view-${v}`, () => this.changeView(v), v === this.opts.view));
3121
+ viewSelect.append(h(this.doc, "option", "", { value: v }, label));
3038
3122
  }
3123
+ viewSelect.value = this.opts.view;
3124
+ viewSelect.addEventListener("change", () => this.changeView(viewSelect.value));
3039
3125
  const start = h(this.doc, "div", "tnc-toolbar-start");
3040
3126
  const end = h(this.doc, "div", "tnc-toolbar-end");
3041
3127
  if (cfg.extraPlacement === "start") start.append(...extras);
3042
3128
  start.append(nav, title);
3043
- end.append(viewGroup);
3129
+ end.append(viewGroup, viewSelect);
3044
3130
  if (cfg.extraPlacement !== "start") end.append(...extras);
3045
3131
  this.toolbarEl.append(start, end);
3132
+ this.syncToolbarViewDisplay();
3133
+ }
3134
+ syncToolbarViewDisplay() {
3135
+ const buttons = this.toolbarEl.querySelector(".tnc-toolbar-views");
3136
+ const select = this.toolbarEl.querySelector(".tnc-toolbar-view-select");
3137
+ const start = this.toolbarEl.querySelector(".tnc-toolbar-start");
3138
+ if (!buttons || !select || !start) return;
3139
+ const config = this.opts.toolbar;
3140
+ const mode = typeof config === "object" ? config.viewDisplay ?? "auto" : "auto";
3141
+ buttons.hidden = false;
3142
+ select.hidden = true;
3143
+ buttons.style.display = "flex";
3144
+ select.style.display = "none";
3145
+ const startWidth = Array.from(start.children).reduce((width, child) => width + child.scrollWidth, 0);
3146
+ const compact = mode === "select" || mode === "auto" && startWidth + buttons.scrollWidth + 32 > this.root.clientWidth;
3147
+ buttons.hidden = compact;
3148
+ select.hidden = !compact;
3149
+ buttons.style.display = compact ? "none" : "flex";
3150
+ select.style.display = compact ? "" : "none";
3046
3151
  }
3047
3152
  applyChange(change) {
3048
3153
  if (!change.added.length && !change.updated.length && !change.removed.length && !change.days.size && !change.spans) return;
@@ -3175,6 +3280,38 @@ var Calendar = class {
3175
3280
  }
3176
3281
  }
3177
3282
  /* ============================================================== popover */
3283
+ onMoreGeometryChange = () => this.positionMore();
3284
+ listenMoreGeometry(on) {
3285
+ const method = on ? "addEventListener" : "removeEventListener";
3286
+ this.doc[method]("scroll", this.onMoreGeometryChange, true);
3287
+ this.win[method]("resize", this.onMoreGeometryChange);
3288
+ this.win.visualViewport?.[method]("resize", this.onMoreGeometryChange);
3289
+ this.win.visualViewport?.[method]("scroll", this.onMoreGeometryChange);
3290
+ }
3291
+ positionMore() {
3292
+ const popover = this.popover;
3293
+ if (!popover) return;
3294
+ if (!popover.anchor.isConnected) {
3295
+ this.closeMore();
3296
+ return;
3297
+ }
3298
+ const viewport = this.win.visualViewport;
3299
+ const root = this.root.getBoundingClientRect();
3300
+ const anchor = popover.anchor.getBoundingClientRect();
3301
+ const leftEdge = Math.max(root.left, viewport?.offsetLeft ?? 0) + 4;
3302
+ const topEdge = Math.max(root.top, viewport?.offsetTop ?? 0) + 4;
3303
+ const rightEdge = Math.min(root.right, (viewport?.offsetLeft ?? 0) + (viewport?.width ?? this.win.innerWidth)) - 4;
3304
+ const bottomEdge = Math.min(root.bottom, (viewport?.offsetTop ?? 0) + (viewport?.height ?? this.win.innerHeight)) - 4;
3305
+ if (rightEdge <= leftEdge || bottomEdge <= topEdge) {
3306
+ this.closeMore();
3307
+ return;
3308
+ }
3309
+ const width = Math.min(360, rightEdge - leftEdge);
3310
+ setStyle(popover.el, { width, "max-height": Math.min(420, bottomEdge - topEdge) });
3311
+ const left = Math.max(leftEdge, Math.min(anchor.left - 12, rightEdge - width));
3312
+ const top = Math.max(topEdge, Math.min(anchor.top - 40, bottomEdge - popover.el.offsetHeight));
3313
+ setStyle(popover.el, { left: left - root.left, top: top - root.top });
3314
+ }
3178
3315
  refreshMore() {
3179
3316
  const p = this.popover;
3180
3317
  if (!p) return;
@@ -3184,6 +3321,7 @@ var Calendar = class {
3184
3321
  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 });
3185
3322
  }
3186
3323
  p.pool.end();
3324
+ this.positionMore();
3187
3325
  }
3188
3326
  closeMore(restoreFocus = false) {
3189
3327
  const p = this.popover;
@@ -3211,6 +3349,7 @@ var Calendar = class {
3211
3349
  r.addEventListener("pointerover", this.onAdoptCheck);
3212
3350
  r.addEventListener("focusin", this.onAdoptCheck);
3213
3351
  this.doc.addEventListener("pointerdown", this.onDocPointerDown, true);
3352
+ this.listenMoreGeometry(true);
3214
3353
  }
3215
3354
  unbindEvents() {
3216
3355
  const r = this.root;
@@ -3229,6 +3368,7 @@ var Calendar = class {
3229
3368
  r.removeEventListener("pointerover", this.onAdoptCheck);
3230
3369
  r.removeEventListener("focusin", this.onAdoptCheck);
3231
3370
  this.doc.removeEventListener("pointerdown", this.onDocPointerDown, true);
3371
+ this.listenMoreGeometry(false);
3232
3372
  this.listenDoc(false);
3233
3373
  }
3234
3374
  listenDoc(on) {
@@ -3274,6 +3414,7 @@ var Calendar = class {
3274
3414
  mode = "select";
3275
3415
  }
3276
3416
  const touch = ev.pointerType === "touch";
3417
+ const overflowOrigin = itemEl?.closest(".tnc-popover") && origin ? wallTime(origin.startMs, this.zone) : null;
3277
3418
  const s = {
3278
3419
  mode,
3279
3420
  pointerId: ev.pointerId,
@@ -3282,7 +3423,7 @@ var Calendar = class {
3282
3423
  y0: ev.clientY,
3283
3424
  active: false,
3284
3425
  origin,
3285
- grab: this.current?.hit(ev.clientX, ev.clientY) ?? null,
3426
+ grab: overflowOrigin && this.current instanceof TimeGridView && !origin?.allDay ? { kind: "time", day: overflowOrigin.day, minute: overflowOrigin.minute } : this.current?.hit(ev.clientX, ev.clientY) ?? null,
3286
3427
  proposal: null,
3287
3428
  denied: false,
3288
3429
  timer: null,
@@ -3785,6 +3926,25 @@ var Calendar = class {
3785
3926
  if (el && meta) this.opts.onItemHover?.({ ...this.itemArg(el, meta), jsEvent: ev, entering: false });
3786
3927
  }
3787
3928
  onKeyDown = (ev) => {
3929
+ if (this.popover && !ev.altKey && !ev.ctrlKey && !ev.metaKey && ["ArrowDown", "ArrowUp", "Home", "End"].includes(ev.key)) {
3930
+ const rows = [...this.popover.el.querySelectorAll(".tnc-item")];
3931
+ const index = rows.indexOf(this.doc.activeElement);
3932
+ 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;
3933
+ ev.preventDefault();
3934
+ ev.stopPropagation();
3935
+ rows[next]?.focus();
3936
+ rows[next]?.scrollIntoView({ block: "nearest", inline: "nearest" });
3937
+ return;
3938
+ }
3939
+ if (ev.key === "Tab" && this.popover) {
3940
+ const controls = [...this.popover.el.querySelectorAll('button, a[href], [tabindex="0"]')];
3941
+ const index = controls.indexOf(this.doc.activeElement);
3942
+ const next = ev.shiftKey ? index <= 0 ? controls.length - 1 : index - 1 : (index + 1) % controls.length;
3943
+ ev.preventDefault();
3944
+ ev.stopPropagation();
3945
+ controls[next]?.focus();
3946
+ return;
3947
+ }
3788
3948
  if (ev.key === "Escape" && this.popover) {
3789
3949
  ev.preventDefault();
3790
3950
  ev.stopPropagation();
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-URE7QLU3.js";
10
+ } from "./chunk-EDRCVOY7.js";
11
11
  import {
12
12
  DAY_MS,
13
13
  ItemStore,
package/dist/esm/react.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  Calendar
3
- } from "./chunk-URE7QLU3.js";
3
+ } from "./chunk-EDRCVOY7.js";
4
4
  import "./chunk-66A6EMCU.js";
5
5
 
6
6
  // src/react/index.tsx
@@ -144,7 +144,7 @@ export declare class Calendar<D = unknown> implements ViewContext<D> {
144
144
  private selectedSet;
145
145
  private applySelected;
146
146
  destroyItemEl(el: HTMLElement): void;
147
- openMore(day: Day, anchor: HTMLElement, jsEvent: MouseEvent | KeyboardEvent): void;
147
+ openMore(day: Day, anchor: HTMLElement, jsEvent: MouseEvent | KeyboardEvent, itemIds?: readonly string[]): void;
148
148
  private resolveText;
149
149
  private todayDay;
150
150
  private toDay;
@@ -170,6 +170,7 @@ export declare class Calendar<D = unknown> implements ViewContext<D> {
170
170
  dateClick(day: Day, jsEvent: MouseEvent | KeyboardEvent): void;
171
171
  private createView;
172
172
  private renderToolbar;
173
+ private syncToolbarViewDisplay;
173
174
  private applyChange;
174
175
  /** Renders pending changes now. Called automatically on the next frame. */
175
176
  flush(): void;
@@ -184,6 +185,9 @@ export declare class Calendar<D = unknown> implements ViewContext<D> {
184
185
  private addHandles;
185
186
  /** Start and finish link handles (a milestone has one). */
186
187
  private addPorts;
188
+ private readonly onMoreGeometryChange;
189
+ private listenMoreGeometry;
190
+ private positionMore;
187
191
  private refreshMore;
188
192
  private closeMore;
189
193
  private bindEvents;
@@ -144,7 +144,7 @@ export declare class Calendar<D = unknown> implements ViewContext<D> {
144
144
  private selectedSet;
145
145
  private applySelected;
146
146
  destroyItemEl(el: HTMLElement): void;
147
- openMore(day: Day, anchor: HTMLElement, jsEvent: MouseEvent | KeyboardEvent): void;
147
+ openMore(day: Day, anchor: HTMLElement, jsEvent: MouseEvent | KeyboardEvent, itemIds?: readonly string[]): void;
148
148
  private resolveText;
149
149
  private todayDay;
150
150
  private toDay;
@@ -170,6 +170,7 @@ export declare class Calendar<D = unknown> implements ViewContext<D> {
170
170
  dateClick(day: Day, jsEvent: MouseEvent | KeyboardEvent): void;
171
171
  private createView;
172
172
  private renderToolbar;
173
+ private syncToolbarViewDisplay;
173
174
  private applyChange;
174
175
  /** Renders pending changes now. Called automatically on the next frame. */
175
176
  flush(): void;
@@ -184,6 +185,9 @@ export declare class Calendar<D = unknown> implements ViewContext<D> {
184
185
  private addHandles;
185
186
  /** Start and finish link handles (a milestone has one). */
186
187
  private addPorts;
188
+ private readonly onMoreGeometryChange;
189
+ private listenMoreGeometry;
190
+ private positionMore;
187
191
  private refreshMore;
188
192
  private closeMore;
189
193
  private bindEvents;
@@ -61,7 +61,7 @@ export interface ViewContext<D = unknown> {
61
61
  destroyItemEl(el: HTMLElement): void;
62
62
  /** Builds a nav link (day number / header) for a day. */
63
63
  navLink(day: Day, label: Node | string, cls: string): HTMLElement;
64
- openMore(day: Day, anchor: HTMLElement, jsEvent: MouseEvent | KeyboardEvent): void;
64
+ openMore(day: Day, anchor: HTMLElement, jsEvent: MouseEvent | KeyboardEvent, itemIds?: readonly string[]): void;
65
65
  /** Follows a date link: `onNavLink`, or switches to `view` at `day`. */
66
66
  navigate(day: Day, jsEvent: MouseEvent | KeyboardEvent, view?: "day" | "month"): void;
67
67
  /** Swallows the click that follows a drag the view handled itself. */
@@ -61,7 +61,7 @@ export interface ViewContext<D = unknown> {
61
61
  destroyItemEl(el: HTMLElement): void;
62
62
  /** Builds a nav link (day number / header) for a day. */
63
63
  navLink(day: Day, label: Node | string, cls: string): HTMLElement;
64
- openMore(day: Day, anchor: HTMLElement, jsEvent: MouseEvent | KeyboardEvent): void;
64
+ openMore(day: Day, anchor: HTMLElement, jsEvent: MouseEvent | KeyboardEvent, itemIds?: readonly string[]): void;
65
65
  /** Follows a date link: `onNavLink`, or switches to `view` at `day`. */
66
66
  navigate(day: Day, jsEvent: MouseEvent | KeyboardEvent, view?: "day" | "month"): void;
67
67
  /** Swallows the click that follows a drag the view handled itself. */
@@ -258,6 +258,7 @@ export interface ToolbarButton {
258
258
  export interface ToolbarOptions {
259
259
  /** View buttons, in order. */
260
260
  views?: ViewType[];
261
+ viewDisplay?: "auto" | "buttons" | "select";
261
262
  extraButtons?: ToolbarButton[];
262
263
  /** Extra buttons go before prev/next ("start") or after the view buttons ("end"). */
263
264
  extraPlacement?: "start" | "end";
@@ -269,6 +270,7 @@ export interface CalendarText {
269
270
  allDay: string;
270
271
  noItems: string;
271
272
  more: (n: number) => string;
273
+ appointments: (n: number) => string;
272
274
  /** Day-cell label suffix in `monthDisplay: "dots"`. */
273
275
  itemCount: (n: number) => string;
274
276
  weekNumber: (n: number) => string;
@@ -319,6 +321,7 @@ export interface CalendarOptions<D = unknown> {
319
321
  }) => void;
320
322
  /** Agenda lists: an add row with this label under every day, reported through `onListAdd`. Null for none. */
321
323
  listAddText: string | null;
324
+ listEmptyContent?: () => Node;
322
325
  onListAdd?: (arg: DayArg & {
323
326
  jsEvent: MouseEvent | KeyboardEvent;
324
327
  }) => void;
@@ -258,6 +258,7 @@ export interface ToolbarButton {
258
258
  export interface ToolbarOptions {
259
259
  /** View buttons, in order. */
260
260
  views?: ViewType[];
261
+ viewDisplay?: "auto" | "buttons" | "select";
261
262
  extraButtons?: ToolbarButton[];
262
263
  /** Extra buttons go before prev/next ("start") or after the view buttons ("end"). */
263
264
  extraPlacement?: "start" | "end";
@@ -269,6 +270,7 @@ export interface CalendarText {
269
270
  allDay: string;
270
271
  noItems: string;
271
272
  more: (n: number) => string;
273
+ appointments: (n: number) => string;
272
274
  /** Day-cell label suffix in `monthDisplay: "dots"`. */
273
275
  itemCount: (n: number) => string;
274
276
  weekNumber: (n: number) => string;
@@ -319,6 +321,7 @@ export interface CalendarOptions<D = unknown> {
319
321
  }) => void;
320
322
  /** Agenda lists: an add row with this label under every day, reported through `onListAdd`. Null for none. */
321
323
  listAddText: string | null;
324
+ listEmptyContent?: () => Node;
322
325
  onListAdd?: (arg: DayArg & {
323
326
  jsEvent: MouseEvent | KeyboardEvent;
324
327
  }) => void;
@@ -1,3 +1,4 @@
1
+ import { type Day } from "../../core/date.cjs";
1
2
  import type { StoreChange } from "../../core/store.cjs";
2
3
  import { type Selection, type View, type ViewContext } from "../context.cjs";
3
4
  import type { Hit } from "../options.cjs";
@@ -6,6 +7,7 @@ export declare class TimeGridView<D> implements View {
6
7
  private readonly ctx;
7
8
  readonly el: HTMLElement;
8
9
  private readonly scroller;
10
+ private readonly dayNavigation;
9
11
  private readonly top;
10
12
  private readonly headRow;
11
13
  private readonly allDayRow;
@@ -40,6 +42,8 @@ export declare class TimeGridView<D> implements View {
40
42
  hit(x: number, y: number): Hit | null;
41
43
  tick(): void;
42
44
  resize(): void;
45
+ revealDay(day: Day): void;
46
+ private updateDayNavigation;
43
47
  scrollToTime(minute: number): void;
44
48
  getScroll(): {
45
49
  top: number;
@@ -1,3 +1,4 @@
1
+ import { type Day } from "../../core/date.js";
1
2
  import type { StoreChange } from "../../core/store.js";
2
3
  import { type Selection, type View, type ViewContext } from "../context.js";
3
4
  import type { Hit } from "../options.js";
@@ -6,6 +7,7 @@ export declare class TimeGridView<D> implements View {
6
7
  private readonly ctx;
7
8
  readonly el: HTMLElement;
8
9
  private readonly scroller;
10
+ private readonly dayNavigation;
9
11
  private readonly top;
10
12
  private readonly headRow;
11
13
  private readonly allDayRow;
@@ -40,6 +42,8 @@ export declare class TimeGridView<D> implements View {
40
42
  hit(x: number, y: number): Hit | null;
41
43
  tick(): void;
42
44
  resize(): void;
45
+ revealDay(day: Day): void;
46
+ private updateDayNavigation;
43
47
  scrollToTime(minute: number): void;
44
48
  getScroll(): {
45
49
  top: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tasknotes/calendar",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
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",