@tasknotes/calendar 0.1.1 → 0.1.3

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.
@@ -164,6 +164,7 @@ function defaultOptions() {
164
164
  dayCount: 3,
165
165
  listDayCount: 7,
166
166
  listStart: "date",
167
+ listAgenda: false,
167
168
  monthDisplay: "full",
168
169
  timeZone: "local",
169
170
  locale: void 0,
@@ -213,6 +214,7 @@ var LAYOUT_KEYS = /* @__PURE__ */ new Set([
213
214
  "dayCount",
214
215
  "listDayCount",
215
216
  "listStart",
217
+ "listAgenda",
216
218
  "monthDisplay",
217
219
  "timeZone",
218
220
  "locale",
@@ -640,15 +642,225 @@ var DayGridView = class {
640
642
  }
641
643
  };
642
644
 
645
+ // src/dom/views/strip.ts
646
+ var SWIPE_PX = 40;
647
+ var DateStrip = class {
648
+ constructor(ctx, pick, focus) {
649
+ this.ctx = ctx;
650
+ this.pick = pick;
651
+ const { doc, text } = ctx;
652
+ this.focus = focus;
653
+ this.shown = startOfWeek(focus, ctx.firstDay);
654
+ this.el = h(doc, "div", "tnc-strip");
655
+ const head = h(doc, "div", "tnc-strip-head");
656
+ this.title = h(doc, "button", "tnc-strip-title", { type: "button", "aria-expanded": "false" });
657
+ this.title.addEventListener("click", () => this.setExpanded(!this.expanded));
658
+ const prev = h(doc, "button", "tnc-strip-nav tnc-strip-prev", { type: "button", "aria-label": text.prev, title: text.prev }, "\u2039");
659
+ const next = h(doc, "button", "tnc-strip-nav tnc-strip-next", { type: "button", "aria-label": text.next, title: text.next }, "\u203A");
660
+ prev.addEventListener("click", () => this.step(-1));
661
+ next.addEventListener("click", () => this.step(1));
662
+ this.slot = h(doc, "div", "tnc-strip-slot", { "data-tnc-no-drag": "" });
663
+ head.append(this.title, prev, next, this.slot);
664
+ this.dows = h(doc, "div", "tnc-strip-dows", { "aria-hidden": "true" });
665
+ this.grid = h(doc, "div", "tnc-strip-grid", { role: "group" });
666
+ const handle = h(doc, "button", "tnc-strip-handle", { type: "button", tabindex: "-1", "aria-hidden": "true" });
667
+ handle.addEventListener("click", () => this.setExpanded(!this.expanded));
668
+ this.el.append(head, this.dows, this.grid, handle);
669
+ this.grid.addEventListener("click", this.onClick);
670
+ this.grid.addEventListener("keydown", this.onKeyDown);
671
+ this.el.addEventListener("pointerdown", this.onPointerDown);
672
+ this.el.addEventListener("pointerup", this.onPointerUp);
673
+ this.el.addEventListener("pointercancel", this.onPointerCancel);
674
+ this.el.addEventListener("click", this.onClickCapture, true);
675
+ this.draw();
676
+ ctx.opts.stripSlotDidMount?.({ el: this.slot });
677
+ }
678
+ ctx;
679
+ pick;
680
+ el;
681
+ title;
682
+ dows;
683
+ grid;
684
+ slot;
685
+ /** Day at the top of the list. */
686
+ focus;
687
+ /** First day of the week shown, or the month shown when expanded. */
688
+ shown;
689
+ target = null;
690
+ swipe = null;
691
+ swallowClick = false;
692
+ expanded = false;
693
+ /** The list scrolled: mark its top day and keep it in view. */
694
+ setFocus(day) {
695
+ if (day === this.focus) return;
696
+ this.focus = day;
697
+ if (!this.expanded) this.shown = startOfWeek(day, this.ctx.firstDay);
698
+ else if (startOfMonth(day) !== this.shown) this.shown = startOfMonth(day);
699
+ this.draw();
700
+ }
701
+ setExpanded(on) {
702
+ if (on === this.expanded) return;
703
+ this.expanded = on;
704
+ this.shown = on ? startOfMonth(this.focus) : startOfWeek(this.focus, this.ctx.firstDay);
705
+ this.draw();
706
+ }
707
+ /** Highlights the day a dragged item would land on. */
708
+ markTarget(day) {
709
+ if (day === this.target) return;
710
+ this.target = day;
711
+ for (const cell of this.grid.querySelectorAll(".tnc-strip-day"))
712
+ cell.classList.toggle("is-drop-target", day !== null && cell.dataset.date === this.ctx.dayArg(day).dateStr);
713
+ }
714
+ dayAt(el) {
715
+ const cell = el.closest(".tnc-strip-day");
716
+ return cell && this.grid.contains(cell) ? parseDay(cell.dataset.date ?? "") : null;
717
+ }
718
+ step(dir) {
719
+ this.shown = this.expanded ? addMonths(this.shown, dir) : this.shown + 7 * dir;
720
+ this.draw();
721
+ }
722
+ days() {
723
+ if (!this.expanded) return Array.from({ length: 7 }, (_, i) => this.shown + i);
724
+ const first = startOfWeek(this.shown, this.ctx.firstDay);
725
+ const end = addMonths(this.shown, 1);
726
+ const weeks = Math.ceil((end - first) / 7);
727
+ return Array.from({ length: weeks * 7 }, (_, i) => first + i);
728
+ }
729
+ /** Redraws the cells; also called on every list render to refresh the dots. */
730
+ draw() {
731
+ const { ctx } = this;
732
+ const { doc, fmt, store } = ctx;
733
+ const days = this.days();
734
+ const titleDay = this.expanded ? this.shown : days.includes(this.focus) ? this.focus : this.shown + 3;
735
+ this.title.replaceChildren(fmt.monthYear(titleDay), h(doc, "span", "tnc-strip-caret", { "aria-hidden": "true" }));
736
+ this.title.setAttribute("aria-expanded", String(this.expanded));
737
+ this.el.classList.toggle("is-expanded", this.expanded);
738
+ this.dows.replaceChildren(...days.slice(0, 7).map((d) => h(doc, "span", null, null, fmt.weekdayNarrow(d))));
739
+ const focused = this.grid.contains(doc.activeElement) ? doc.activeElement.dataset.date : void 0;
740
+ const month = ymdOfDay(this.shown).month;
741
+ const tabDay = days.includes(this.focus) ? this.focus : days[0];
742
+ const cells = days.map((d) => {
743
+ const arg = ctx.dayArg(d);
744
+ const count = store.inRange(d, d + 1).filter((n) => n.item.display !== "background" && n.item.display !== "none").length;
745
+ const first = ymdOfDay(d).day === 1 && d !== days[0] && !this.expanded;
746
+ const label = fmt.longDate(d);
747
+ const cell = h(
748
+ doc,
749
+ "button",
750
+ [
751
+ "tnc-strip-day",
752
+ ...ctx.dayClasses(d),
753
+ d === this.focus ? "is-focus" : "",
754
+ count ? "has-items" : "",
755
+ this.expanded && ymdOfDay(d).month !== month ? "is-other-month" : "",
756
+ d === this.target ? "is-drop-target" : ""
757
+ ].filter(Boolean).join(" "),
758
+ {
759
+ type: "button",
760
+ "data-date": arg.dateStr,
761
+ "aria-label": count ? `${label}, ${ctx.text.itemCount(count)}` : label,
762
+ "aria-pressed": String(d === this.focus),
763
+ tabindex: d === tabDay ? "0" : "-1"
764
+ }
765
+ );
766
+ if (first) cell.append(h(doc, "span", "tnc-strip-month", { "aria-hidden": "true" }, fmt.monthShort(d)));
767
+ cell.append(h(doc, "span", "tnc-strip-num", null, fmt.dayNumber(d)), h(doc, "span", "tnc-strip-dot", { "aria-hidden": "true" }));
768
+ return cell;
769
+ });
770
+ this.grid.replaceChildren(...cells);
771
+ this.grid.setAttribute("aria-label", fmt.monthYear(titleDay));
772
+ if (focused) this.grid.querySelector(`[data-date="${focused}"]`)?.focus({ preventScroll: true });
773
+ }
774
+ onClick = (ev) => {
775
+ const day = this.dayAt(ev.target);
776
+ if (day === null) return;
777
+ if (this.expanded) this.setExpanded(false);
778
+ this.pick(day, ev);
779
+ };
780
+ onKeyDown = (ev) => {
781
+ const day = this.dayAt(ev.target);
782
+ if (day === null) return;
783
+ const move = { ArrowLeft: -1, ArrowRight: 1, ArrowUp: -7, ArrowDown: 7 }[ev.key];
784
+ if (move === void 0) return;
785
+ ev.preventDefault();
786
+ const next = day + move;
787
+ const days = this.days();
788
+ if (next < days[0] || next > days[days.length - 1]) {
789
+ this.shown = this.expanded ? startOfMonth(next) : startOfWeek(next, this.ctx.firstDay);
790
+ this.draw();
791
+ }
792
+ const cell = this.grid.querySelector(`[data-date="${this.ctx.dayArg(next).dateStr}"]`);
793
+ if (!cell) return;
794
+ for (const c of this.grid.querySelectorAll("[tabindex='0']")) c.setAttribute("tabindex", "-1");
795
+ cell.setAttribute("tabindex", "0");
796
+ cell.focus();
797
+ };
798
+ onPointerDown = (ev) => {
799
+ if (ev.button !== 0 || this.ctx.draggingId) return;
800
+ this.swipe = { id: ev.pointerId, x: ev.clientX, y: ev.clientY };
801
+ };
802
+ onPointerUp = (ev) => {
803
+ const s = this.swipe;
804
+ this.swipe = null;
805
+ if (!s || s.id !== ev.pointerId) return;
806
+ const dx = ev.clientX - s.x;
807
+ if (Math.abs(dx) < SWIPE_PX || Math.abs(dx) < Math.abs(ev.clientY - s.y)) return;
808
+ this.swallowClick = true;
809
+ this.step(dx < 0 ? 1 : -1);
810
+ };
811
+ onPointerCancel = () => {
812
+ this.swipe = null;
813
+ };
814
+ /** A swipe that ends on a day is not a pick. */
815
+ onClickCapture = (ev) => {
816
+ if (!this.swallowClick) return;
817
+ this.swallowClick = false;
818
+ ev.stopPropagation();
819
+ ev.preventDefault();
820
+ };
821
+ destroy() {
822
+ this.ctx.opts.stripSlotWillUnmount?.({ el: this.slot });
823
+ this.el.remove();
824
+ }
825
+ };
826
+
643
827
  // src/dom/views/list.ts
828
+ var LOAD_AHEAD = "0px 0px 600px 0px";
644
829
  var ListView = class {
645
830
  constructor(ctx) {
646
831
  this.ctx = ctx;
647
- this.el = h(ctx.doc, "div", "tnc-list");
648
- this.scroller = h(ctx.doc, "div", "tnc-list-scroll");
649
- this.body = h(ctx.doc, "div", "tnc-list-body", { role: "list", "aria-label": ctx.view.title });
832
+ const { doc, win, opts } = ctx;
833
+ this.el = h(doc, "div", "tnc-list");
834
+ this.scroller = h(doc, "div", "tnc-list-scroll");
835
+ this.body = h(doc, "div", "tnc-list-body", { role: "list", "aria-label": ctx.view.title });
650
836
  this.scroller.append(this.body);
651
837
  this.el.append(this.scroller);
838
+ if (!opts.listAgenda) return;
839
+ this.el.classList.add("tnc-list--agenda");
840
+ this.strip = new DateStrip(ctx, (day, ev) => this.pick(day, ev), ctx.view.range.startDay);
841
+ this.el.prepend(this.strip.el);
842
+ this.sentinel = h(doc, "div", "tnc-list-more", { "aria-hidden": "true" });
843
+ this.scroller.append(this.sentinel);
844
+ const globals = win;
845
+ const IO = globals.IntersectionObserver;
846
+ if (IO) {
847
+ this.loader = new IO(
848
+ (entries) => {
849
+ if (entries.some((e) => e.isIntersecting)) ctx.extendList(ctx.view.range.endDay + Math.max(7, opts.listDayCount));
850
+ },
851
+ // Auto height scrolls the page, so watch the viewport; fixed height scrolls the list.
852
+ { root: opts.height === "auto" ? null : this.scroller, rootMargin: LOAD_AHEAD }
853
+ );
854
+ }
855
+ const RO = globals.ResizeObserver;
856
+ if (RO) {
857
+ const strip = this.strip.el;
858
+ this.stripSize = new RO(() => this.el.style.setProperty("--tnc-strip-h", `${strip.offsetHeight}px`));
859
+ this.stripSize.observe(strip);
860
+ }
861
+ win.addEventListener("scroll", this.onScroll, { capture: true, passive: true });
862
+ this.scroller.addEventListener("wheel", this.fold, { passive: true });
863
+ this.scroller.addEventListener("touchmove", this.fold, { passive: true });
652
864
  }
653
865
  ctx;
654
866
  el;
@@ -656,6 +868,13 @@ var ListView = class {
656
868
  body;
657
869
  pools = /* @__PURE__ */ new Map();
658
870
  sections = /* @__PURE__ */ new Map();
871
+ strip = null;
872
+ sentinel = null;
873
+ loader = null;
874
+ stripSize = null;
875
+ frame = null;
876
+ /** A picked day stays marked until the list is scrolled by hand. */
877
+ pinned = false;
659
878
  itemsOn(day) {
660
879
  const { store, opts } = this.ctx;
661
880
  const out = store.spanningIn(day, day + 1, true);
@@ -666,11 +885,12 @@ var ListView = class {
666
885
  render(_change) {
667
886
  const { ctx } = this;
668
887
  const { doc, days } = ctx;
888
+ const agenda = !!this.strip;
669
889
  const keep = /* @__PURE__ */ new Set();
670
890
  let prev = null;
671
891
  for (const day of days) {
672
892
  const items = this.itemsOn(day);
673
- if (!items.length) continue;
893
+ if (!items.length && !agenda) continue;
674
894
  keep.add(day);
675
895
  let section = this.sections.get(day);
676
896
  let pool = this.pools.get(day);
@@ -687,6 +907,7 @@ var ListView = class {
687
907
  this.sections.set(day, section);
688
908
  this.pools.set(day, pool);
689
909
  }
910
+ section.classList.toggle("is-empty", !items.length);
690
911
  pool.begin();
691
912
  for (const n of items) {
692
913
  pool.place(n.id, {
@@ -724,11 +945,62 @@ var ListView = class {
724
945
  this.body.append(empty);
725
946
  }
726
947
  } else empty?.remove();
948
+ this.strip?.draw();
949
+ if (this.loader && this.sentinel) {
950
+ this.loader.unobserve(this.sentinel);
951
+ this.loader.observe(this.sentinel);
952
+ }
953
+ }
954
+ /** A strip day: the host hears of it, and the list brings it to the top. */
955
+ pick(day, ev) {
956
+ this.ctx.dateClick(day, ev);
957
+ this.ctx.showListDay(day);
958
+ }
959
+ /** Brings a day's section to the top, under the strip. */
960
+ scrollToDay(day) {
961
+ const section = this.sections.get(day);
962
+ if (!section) return;
963
+ if (this.strip) this.el.style.setProperty("--tnc-strip-h", `${this.strip.el.offsetHeight}px`);
964
+ section.scrollIntoView({ block: "start" });
965
+ this.strip?.setFocus(day);
966
+ this.pinned = true;
967
+ }
968
+ fold = () => {
969
+ this.pinned = false;
970
+ if (this.strip?.expanded) this.strip.setExpanded(false);
971
+ };
972
+ onScroll = () => {
973
+ if (this.frame !== null) return;
974
+ this.frame = this.ctx.win.requestAnimationFrame(() => {
975
+ this.frame = null;
976
+ this.followTop();
977
+ });
978
+ };
979
+ /** Marks the first day not hidden under the strip. */
980
+ followTop() {
981
+ if (!this.strip || this.pinned) return;
982
+ const edge = this.strip.el.getBoundingClientRect().bottom + 1;
983
+ for (const day of this.ctx.days) {
984
+ const section = this.sections.get(day);
985
+ if (section && section.getBoundingClientRect().bottom > edge) {
986
+ this.strip.setFocus(day);
987
+ return;
988
+ }
989
+ }
727
990
  }
728
991
  showSelection(_sel) {
729
992
  }
730
- hit(_x, _y) {
731
- return null;
993
+ hit(x, y) {
994
+ const el = this.ctx.doc.elementFromPoint(x, y);
995
+ const stripDay = el && this.strip ? this.strip.dayAt(el) : null;
996
+ this.strip?.markTarget(stripDay !== null && this.ctx.draggingId ? stripDay : null);
997
+ if (stripDay !== null) return { kind: "day", day: stripDay };
998
+ const section = el?.closest(".tnc-list-section");
999
+ const day = section && this.body.contains(section) ? parseDay(section.dataset.date ?? "") : null;
1000
+ return day === null ? null : { kind: "day", day };
1001
+ }
1002
+ dragEnded() {
1003
+ this.strip?.markTarget(null);
732
1004
  }
733
1005
  getScroll() {
734
1006
  return { top: this.scroller.scrollTop, left: 0 };
@@ -740,6 +1012,12 @@ var ListView = class {
740
1012
  return [...this.pools.values()].flatMap((p) => p.elementsFor(id));
741
1013
  }
742
1014
  destroy() {
1015
+ const { win } = this.ctx;
1016
+ win.removeEventListener("scroll", this.onScroll, { capture: true });
1017
+ if (this.frame !== null) win.cancelAnimationFrame(this.frame);
1018
+ this.loader?.disconnect();
1019
+ this.stripSize?.disconnect();
1020
+ this.strip?.destroy();
743
1021
  for (const p of this.pools.values()) p.clear();
744
1022
  this.el.remove();
745
1023
  }
@@ -1881,6 +2159,7 @@ ${this.ctx.text.more(items.length - 8)}` : ""}` : label;
1881
2159
 
1882
2160
  // src/dom/calendar.ts
1883
2161
  var EXTERNAL_ID = "__tnc_external__";
2162
+ var MAX_AGENDA_DAYS = 366;
1884
2163
  function sameOption(a, b, depth = 0) {
1885
2164
  if (a === b) return true;
1886
2165
  if (typeof a === "function" && typeof b === "function") return true;
@@ -1953,6 +2232,8 @@ var Calendar = class {
1953
2232
  viewHost;
1954
2233
  current = null;
1955
2234
  anchor = 0;
2235
+ /** End of an agenda list grown past its `listDayCount`. */
2236
+ listEnd = null;
1956
2237
  session = null;
1957
2238
  selection = null;
1958
2239
  suppressClick = false;
@@ -2145,6 +2426,10 @@ var Calendar = class {
2145
2426
  if (!p) return;
2146
2427
  const day = p.kind === "day" ? p.day : dayOf(p.ms, this.zone);
2147
2428
  const ms = p.kind === "day" ? toInstant(p.day, 0, this.zone) : p.ms;
2429
+ if (this.agendaList()) {
2430
+ this.showListDay(day);
2431
+ return;
2432
+ }
2148
2433
  const { startDay, endDay } = this.view.range;
2149
2434
  if (day < startDay || day >= endDay) {
2150
2435
  this.anchor = day;
@@ -2228,7 +2513,8 @@ var Calendar = class {
2228
2513
  return out;
2229
2514
  }
2230
2515
  isEditable(n, what) {
2231
- if (n.id === EXTERNAL_ID || this.opts.view === "list" || this.opts.view === "year") return false;
2516
+ if (n.id === EXTERNAL_ID || this.opts.view === "year") return false;
2517
+ if (this.opts.view === "list" && (!this.opts.listAgenda || what === "duration")) return false;
2232
2518
  const it = n.item;
2233
2519
  const base = it.editable ?? this.opts.editable;
2234
2520
  const specific = what === "start" ? it.startEditable : it.durationEditable;
@@ -2425,7 +2711,8 @@ var Calendar = class {
2425
2711
  }
2426
2712
  case "list": {
2427
2713
  const s = this.opts.listStart === "week" ? startOfWeek(a, this.firstDay) : a;
2428
- return { startDay: s, endDay: s + Math.max(1, listDayCount) };
2714
+ const end = s + Math.max(1, listDayCount);
2715
+ return { startDay: s, endDay: this.opts.listAgenda && this.listEnd !== null ? Math.max(end, this.listEnd) : end };
2429
2716
  }
2430
2717
  case "month": {
2431
2718
  const s = startOfWeek(startOfMonth(a), this.firstDay);
@@ -2463,6 +2750,27 @@ var Calendar = class {
2463
2750
  const duration = Math.max(1, parseDurationMinutes(this.opts.slotDuration, 30));
2464
2751
  const snap = Math.max(1, parseDurationMinutes(this.opts.snapDuration ?? null, duration));
2465
2752
  this.slot = { min, max, duration, snap, scroll: parseDurationMinutes(this.opts.scrollTime, 480) };
2753
+ if (navigated) this.listEnd = null;
2754
+ const rangeChanged = this.applyRange();
2755
+ this.conflicts = dependencyConflicts(this.store);
2756
+ this.root.className = "tnc";
2757
+ this.root.classList.add(`tnc--${this.opts.view}`, `tnc--height-${this.opts.height}`);
2758
+ if (!this.opts.timeGrid) this.root.classList.add("tnc--no-time-grid");
2759
+ this.renderToolbar();
2760
+ this.current?.destroy();
2761
+ this.current = this.createView();
2762
+ this.viewHost.replaceChildren(this.current.el);
2763
+ this.current.render(null);
2764
+ if (keep && this.current instanceof TimelineView) {
2765
+ this.current.setScroll({ top: keep.top, left: 0 });
2766
+ this.current.placeInstant(keep.ms, keep.vx);
2767
+ } else if (scroll) this.current.setScroll(scroll);
2768
+ else if (this.current instanceof TimeGridView) this.current.scrollToTime(this.slot.scroll);
2769
+ else if (this.current instanceof TimelineView) this.current.scrollToDay(this.anchor);
2770
+ if (rangeChanged) this.opts.onRangeChange?.(this.view);
2771
+ }
2772
+ /** Sets the visible days and view info from the anchor; true when the range moved. */
2773
+ applyRange() {
2466
2774
  const { startDay, endDay } = this.computeRange();
2467
2775
  const hidden = new Set(this.opts.hiddenDays);
2468
2776
  if (!this.opts.weekends) {
@@ -2482,23 +2790,32 @@ var Calendar = class {
2482
2790
  const prevView = this.view;
2483
2791
  this.view = { type: this.opts.view, range, anchor: this.anchor, title: "" };
2484
2792
  this.view.title = this.computeTitle(startDay, endDay);
2485
- this.conflicts = dependencyConflicts(this.store);
2486
- this.root.className = "tnc";
2487
- this.root.classList.add(`tnc--${this.opts.view}`, `tnc--height-${this.opts.height}`);
2488
- if (!this.opts.timeGrid) this.root.classList.add("tnc--no-time-grid");
2489
- this.renderToolbar();
2490
- this.current?.destroy();
2491
- this.current = this.createView();
2492
- this.viewHost.replaceChildren(this.current.el);
2493
- this.current.render(null);
2494
- if (keep && this.current instanceof TimelineView) {
2495
- this.current.setScroll({ top: keep.top, left: 0 });
2496
- this.current.placeInstant(keep.ms, keep.vx);
2497
- } else if (scroll) this.current.setScroll(scroll);
2498
- else if (this.current instanceof TimeGridView) this.current.scrollToTime(this.slot.scroll);
2499
- else if (this.current instanceof TimelineView) this.current.scrollToDay(this.anchor);
2500
- const rangeChanged = !prevView || prevView.type !== this.view.type || prevView.range.startDay !== startDay || prevView.range.endDay !== endDay;
2501
- if (rangeChanged) this.opts.onRangeChange?.(this.view);
2793
+ return !prevView || prevView.type !== this.view.type || prevView.range.startDay !== startDay || prevView.range.endDay !== endDay;
2794
+ }
2795
+ agendaList() {
2796
+ return this.opts.view === "list" && this.opts.listAgenda && this.current instanceof ListView ? this.current : null;
2797
+ }
2798
+ extendList(endDay) {
2799
+ if (!this.agendaList()) return;
2800
+ const { startDay, endDay: current } = this.view.range;
2801
+ const end = Math.min(endDay, startDay + MAX_AGENDA_DAYS);
2802
+ if (end <= current) return;
2803
+ this.listEnd = end;
2804
+ this.applyRange();
2805
+ this.current?.render(null);
2806
+ this.opts.onRangeChange?.(this.view);
2807
+ }
2808
+ showListDay(day) {
2809
+ if (!this.agendaList()) return;
2810
+ if (day < this.view.range.startDay || day - this.view.range.startDay >= MAX_AGENDA_DAYS) {
2811
+ this.anchor = day;
2812
+ this.rebuild(true);
2813
+ }
2814
+ this.extendList(day + 2 * Math.max(7, this.opts.listDayCount));
2815
+ this.agendaList()?.scrollToDay(day);
2816
+ }
2817
+ dateClick(day, jsEvent) {
2818
+ this.clickSlot({ kind: "day", day }, jsEvent, true);
2502
2819
  }
2503
2820
  createView() {
2504
2821
  switch (this.opts.view) {
@@ -2801,8 +3118,9 @@ var Calendar = class {
2801
3118
  };
2802
3119
  this.session = s;
2803
3120
  if (touch && itemEl?.classList.contains("is-draggable")) {
2804
- const el = itemEl.closest(".tnc-tg-scroll, .tnc-dg-scroll, .tnc-tl-scroll, .tnc-list-scroll, .tnc-year-scroll");
2805
- if (el) s.pan = { el, left: el.scrollLeft, top: el.scrollTop, active: false };
3121
+ const x = scrollerOf(itemEl, "x");
3122
+ const y = scrollerOf(itemEl, "y");
3123
+ s.pan = { x, y, left: x?.scrollLeft ?? 0, top: y?.scrollTop ?? 0, active: false };
2806
3124
  }
2807
3125
  if (touch) {
2808
3126
  s.timer = this.win.setTimeout(() => {
@@ -2843,8 +3161,8 @@ var Calendar = class {
2843
3161
  if (!pan.active && s.timer !== null) this.win.clearTimeout(s.timer);
2844
3162
  s.timer = null;
2845
3163
  pan.active = true;
2846
- pan.el.scrollLeft = pan.left - (ev.clientX - s.x0);
2847
- pan.el.scrollTop = pan.top - (ev.clientY - s.y0);
3164
+ if (pan.x) pan.x.scrollLeft = pan.left - (ev.clientX - s.x0);
3165
+ if (pan.y) pan.y.scrollTop = pan.top - (ev.clientY - s.y0);
2848
3166
  } else if (dist > 8) this.endSession(null, true);
2849
3167
  return;
2850
3168
  }
@@ -3143,6 +3461,7 @@ var Calendar = class {
3143
3461
  this.draggingId = null;
3144
3462
  this.dragRow = null;
3145
3463
  if (draggedId) for (const el of this.elementsFor(draggedId)) el.classList.remove("is-dragging");
3464
+ this.current?.dragEnded?.();
3146
3465
  if (!s.active) return;
3147
3466
  if (s.mode === "select") {
3148
3467
  if (cancel) this.unselectSilently();
@@ -3236,6 +3555,12 @@ var Calendar = class {
3236
3555
  ev.preventDefault();
3237
3556
  };
3238
3557
  onClick = (ev) => {
3558
+ const yearDay = ev.target.closest?.(".tnc-ym-day");
3559
+ if (yearDay && this.root.contains(yearDay)) {
3560
+ const day = parseDay(yearDay.dataset.date ?? "");
3561
+ if (day !== null) this.clickSlot({ kind: "day", day }, ev, true);
3562
+ return;
3563
+ }
3239
3564
  const el = this.itemElFrom(ev.target);
3240
3565
  if (!el || ev.target.closest(".tnc-resize, .tnc-port")) return;
3241
3566
  const meta = this.meta.get(el);
@@ -3295,7 +3620,7 @@ var Calendar = class {
3295
3620
  return;
3296
3621
  }
3297
3622
  const target = ev.target;
3298
- if ((ev.key === "Enter" || ev.key === " ") && target.matches(".tnc-ym-day, .tnc-dg-day[tabindex]")) {
3623
+ if ((ev.key === "Enter" || ev.key === " ") && target.matches(".tnc-dg-day[tabindex]")) {
3299
3624
  const day = parseDay(target.dataset.date ?? "");
3300
3625
  if (day !== null) {
3301
3626
  ev.preventDefault();
@@ -3423,6 +3748,16 @@ var Calendar = class {
3423
3748
  });
3424
3749
  };
3425
3750
  };
3751
+ function scrollerOf(el, axis) {
3752
+ const doc = el.ownerDocument;
3753
+ const win = doc.defaultView;
3754
+ for (let p = el.parentElement; p && p !== doc.body && p !== doc.documentElement; p = p.parentElement) {
3755
+ const overflow = win?.getComputedStyle(p)[axis === "x" ? "overflowX" : "overflowY"] ?? "";
3756
+ const room = axis === "x" ? p.scrollWidth > p.clientWidth : p.scrollHeight > p.clientHeight;
3757
+ if (room && /auto|scroll|overlay/.test(overflow)) return p;
3758
+ }
3759
+ return doc.scrollingElement;
3760
+ }
3426
3761
 
3427
3762
  export {
3428
3763
  Formatter,
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-6RRDNLZM.js";
10
+ } from "./chunk-XHPZ7K4W.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-6RRDNLZM.js";
3
+ } from "./chunk-XHPZ7K4W.js";
4
4
  import "./chunk-DVMZ5OXN.js";
5
5
 
6
6
  // src/react/index.tsx
@@ -44,6 +44,8 @@ export declare class Calendar<D = unknown> implements ViewContext<D> {
44
44
  private readonly viewHost;
45
45
  private current;
46
46
  private anchor;
47
+ /** End of an agenda list grown past its `listDayCount`. */
48
+ private listEnd;
47
49
  private session;
48
50
  private selection;
49
51
  private suppressClick;
@@ -147,6 +149,12 @@ export declare class Calendar<D = unknown> implements ViewContext<D> {
147
149
  private computeRange;
148
150
  private computeTitle;
149
151
  private rebuild;
152
+ /** Sets the visible days and view info from the anchor; true when the range moved. */
153
+ private applyRange;
154
+ private agendaList;
155
+ extendList(endDay: Day): void;
156
+ showListDay(day: Day): void;
157
+ dateClick(day: Day, jsEvent: MouseEvent | KeyboardEvent): void;
150
158
  private createView;
151
159
  private renderToolbar;
152
160
  private applyChange;
@@ -44,6 +44,8 @@ export declare class Calendar<D = unknown> implements ViewContext<D> {
44
44
  private readonly viewHost;
45
45
  private current;
46
46
  private anchor;
47
+ /** End of an agenda list grown past its `listDayCount`. */
48
+ private listEnd;
47
49
  private session;
48
50
  private selection;
49
51
  private suppressClick;
@@ -147,6 +149,12 @@ export declare class Calendar<D = unknown> implements ViewContext<D> {
147
149
  private computeRange;
148
150
  private computeTitle;
149
151
  private rebuild;
152
+ /** Sets the visible days and view info from the anchor; true when the range moved. */
153
+ private applyRange;
154
+ private agendaList;
155
+ extendList(endDay: Day): void;
156
+ showListDay(day: Day): void;
157
+ dateClick(day: Day, jsEvent: MouseEvent | KeyboardEvent): void;
150
158
  private createView;
151
159
  private renderToolbar;
152
160
  private applyChange;
@@ -70,6 +70,12 @@ export interface ViewContext<D = unknown> {
70
70
  id: string;
71
71
  rowId: string;
72
72
  } | null;
73
+ /** Reports a day picked outside the grid (year days, the agenda strip) through `onDateClick`. */
74
+ dateClick(day: Day, jsEvent: MouseEvent | KeyboardEvent): void;
75
+ /** Agenda lists: grows the range to end at `endDay`. */
76
+ extendList(endDay: Day): void;
77
+ /** Agenda lists: brings `day` to the top, moving the range to reach it. */
78
+ showListDay(day: Day): void;
73
79
  }
74
80
  export interface View {
75
81
  readonly el: HTMLElement;
@@ -91,6 +97,8 @@ export interface View {
91
97
  scrollToTime?(minute: number): void;
92
98
  /** Item elements currently shown for an id. */
93
99
  elementsFor(id: string): HTMLElement[];
100
+ /** A drag ended, dropped or not. */
101
+ dragEnded?(): void;
94
102
  destroy(): void;
95
103
  }
96
104
  export declare class ItemPool<D> {
@@ -70,6 +70,12 @@ export interface ViewContext<D = unknown> {
70
70
  id: string;
71
71
  rowId: string;
72
72
  } | null;
73
+ /** Reports a day picked outside the grid (year days, the agenda strip) through `onDateClick`. */
74
+ dateClick(day: Day, jsEvent: MouseEvent | KeyboardEvent): void;
75
+ /** Agenda lists: grows the range to end at `endDay`. */
76
+ extendList(endDay: Day): void;
77
+ /** Agenda lists: brings `day` to the top, moving the range to reach it. */
78
+ showListDay(day: Day): void;
73
79
  }
74
80
  export interface View {
75
81
  readonly el: HTMLElement;
@@ -91,6 +97,8 @@ export interface View {
91
97
  scrollToTime?(minute: number): void;
92
98
  /** Item elements currently shown for an id. */
93
99
  elementsFor(id: string): HTMLElement[];
100
+ /** A drag ended, dropped or not. */
101
+ dragEnded?(): void;
94
102
  destroy(): void;
95
103
  }
96
104
  export declare class ItemPool<D> {