@tasknotes/calendar 0.1.2 → 0.1.4

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,10 @@ function defaultOptions() {
164
164
  dayCount: 3,
165
165
  listDayCount: 7,
166
166
  listStart: "date",
167
+ listAgenda: false,
168
+ dateStrip: false,
169
+ listLeadTitle: null,
170
+ listAddText: null,
167
171
  monthDisplay: "full",
168
172
  timeZone: "local",
169
173
  locale: void 0,
@@ -213,6 +217,10 @@ var LAYOUT_KEYS = /* @__PURE__ */ new Set([
213
217
  "dayCount",
214
218
  "listDayCount",
215
219
  "listStart",
220
+ "listAgenda",
221
+ "dateStrip",
222
+ "listLeadTitle",
223
+ "listAddText",
216
224
  "monthDisplay",
217
225
  "timeZone",
218
226
  "locale",
@@ -641,21 +649,48 @@ var DayGridView = class {
641
649
  };
642
650
 
643
651
  // src/dom/views/list.ts
652
+ var LOAD_AHEAD = "0px 0px 600px 0px";
644
653
  var ListView = class {
645
654
  constructor(ctx) {
646
655
  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 });
656
+ const { doc, win, opts } = ctx;
657
+ this.el = h(doc, "div", "tnc-list");
658
+ this.scroller = h(doc, "div", "tnc-list-scroll");
659
+ this.body = h(doc, "div", "tnc-list-body", { role: "list", "aria-label": ctx.view.title });
650
660
  this.scroller.append(this.body);
651
661
  this.el.append(this.scroller);
662
+ this.agenda = opts.listAgenda;
663
+ if (!this.agenda) return;
664
+ this.el.classList.add("tnc-list--agenda");
665
+ this.sentinel = h(doc, "div", "tnc-list-more", { "aria-hidden": "true" });
666
+ this.scroller.append(this.sentinel);
667
+ const IO = win.IntersectionObserver;
668
+ if (IO) {
669
+ this.loader = new IO(
670
+ (entries) => {
671
+ if (entries.some((e) => e.isIntersecting)) ctx.extendList(ctx.view.range.endDay + Math.max(7, opts.listDayCount));
672
+ },
673
+ // Auto height scrolls the page, so watch the viewport; fixed height scrolls the list.
674
+ { root: opts.height === "auto" ? null : this.scroller, rootMargin: LOAD_AHEAD }
675
+ );
676
+ }
677
+ win.addEventListener("scroll", this.onScroll, { capture: true, passive: true });
678
+ this.scroller.addEventListener("wheel", this.fold, { passive: true });
679
+ this.scroller.addEventListener("touchmove", this.fold, { passive: true });
652
680
  }
653
681
  ctx;
654
682
  el;
655
683
  scroller;
656
684
  body;
685
+ agenda;
657
686
  pools = /* @__PURE__ */ new Map();
658
687
  sections = /* @__PURE__ */ new Map();
688
+ lead = null;
689
+ sentinel = null;
690
+ loader = null;
691
+ frame = null;
692
+ /** A picked day stays marked until the list is scrolled by hand. */
693
+ pinned = false;
659
694
  itemsOn(day) {
660
695
  const { store, opts } = this.ctx;
661
696
  const out = store.spanningIn(day, day + 1, true);
@@ -664,46 +699,45 @@ var ListView = class {
664
699
  return out.filter((n) => n.item.display !== "background" && n.item.display !== "none" && (opts.allDaySlot || !n.allDay)).sort((a, b) => Number(b.allDay) - Number(a.allDay) || this.ctx.order(a, b));
665
700
  }
666
701
  render(_change) {
667
- const { ctx } = this;
668
- const { doc, days } = ctx;
702
+ const { ctx, agenda } = this;
703
+ const { doc, days, opts } = ctx;
704
+ const includes = agenda && opts.listLeadTitle !== null ? opts.listLeadIncludes : void 0;
705
+ const leadItems = [];
706
+ const inLead = /* @__PURE__ */ new Set();
669
707
  const keep = /* @__PURE__ */ new Set();
670
708
  let prev = null;
671
709
  for (const day of days) {
672
- const items = this.itemsOn(day);
673
- if (!items.length) continue;
710
+ let items = this.itemsOn(day);
711
+ if (includes) {
712
+ items = items.filter((n) => {
713
+ if (!includes(n.item)) return true;
714
+ if (!inLead.has(n.id)) {
715
+ inLead.add(n.id);
716
+ leadItems.push(n);
717
+ }
718
+ return false;
719
+ });
720
+ }
721
+ if (!items.length && !agenda) continue;
674
722
  keep.add(day);
675
723
  let section = this.sections.get(day);
676
724
  let pool = this.pools.get(day);
677
725
  if (!section || !pool) {
678
726
  const arg = ctx.dayArg(day);
679
727
  const heading = h(doc, "div", ["tnc-list-day", ...ctx.dayClasses(day)].join(" "), { "data-date": arg.dateStr });
680
- const custom = ctx.opts.dayHeaderContent?.(arg);
728
+ const custom = opts.dayHeaderContent?.(arg);
681
729
  const label = custom ?? ctx.fmt.longDate(day);
682
- heading.append(ctx.opts.navLinks ? ctx.navLink(day, label, "tnc-list-day-link") : label);
683
- const rows2 = h(doc, "div", "tnc-list-rows", { role: "group" });
684
- section = h(doc, "div", "tnc-list-section", { role: "listitem", "aria-label": ctx.fmt.longDate(day), "data-date": arg.dateStr }, heading, rows2);
685
- ctx.opts.dayHeaderDidMount?.({ ...arg, el: heading });
686
- pool = new ItemPool(ctx, rows2);
730
+ heading.append(opts.navLinks ? ctx.navLink(day, label, "tnc-list-day-link") : label);
731
+ const rows = h(doc, "div", "tnc-list-rows", { role: "group" });
732
+ section = h(doc, "div", "tnc-list-section", { role: "listitem", "aria-label": ctx.fmt.longDate(day), "data-date": arg.dateStr }, heading, rows);
733
+ if (agenda && opts.listAddText !== null) section.append(this.addRow(day));
734
+ opts.dayHeaderDidMount?.({ ...arg, el: heading });
735
+ pool = new ItemPool(ctx, rows);
687
736
  this.sections.set(day, section);
688
737
  this.pools.set(day, pool);
689
738
  }
690
- pool.begin();
691
- for (const n of items) {
692
- pool.place(n.id, {
693
- n,
694
- day,
695
- isStart: n.startDay === day,
696
- isEnd: n.endDay === day + 1,
697
- variant: "list",
698
- mirror: false
699
- });
700
- }
701
- pool.end();
702
- const rows = pool.container;
703
- for (const n of items) {
704
- const el = pool.elementsFor(n.id)[0];
705
- if (el) rows.append(el);
706
- }
739
+ section.classList.toggle("is-empty", !items.length);
740
+ this.fill(pool, items, () => day);
707
741
  if (prev ? prev.nextSibling !== section : this.body.firstChild !== section) {
708
742
  if (prev) prev.after(section);
709
743
  else this.body.prepend(section);
@@ -717,18 +751,111 @@ var ListView = class {
717
751
  section.remove();
718
752
  this.sections.delete(day);
719
753
  }
754
+ this.renderLead(leadItems);
720
755
  let empty = this.body.querySelector(":scope > .tnc-list-empty");
721
- if (!keep.size) {
756
+ if (!keep.size && !leadItems.length) {
722
757
  if (!empty) {
723
758
  empty = h(doc, "div", "tnc-list-empty", { role: "listitem" }, ctx.text.noItems);
724
759
  this.body.append(empty);
725
760
  }
726
761
  } else empty?.remove();
762
+ this.ctx.strip?.draw();
763
+ if (this.loader && this.sentinel) {
764
+ this.loader.unobserve(this.sentinel);
765
+ this.loader.observe(this.sentinel);
766
+ }
767
+ }
768
+ /** Places items in a pool, keeping DOM order equal to item order. */
769
+ fill(pool, items, dayFor) {
770
+ pool.begin();
771
+ for (const n of items) {
772
+ const d = dayFor(n);
773
+ pool.place(n.id, { n, day: d, isStart: n.startDay === d, isEnd: n.endDay === d + 1, variant: "list", mirror: false });
774
+ }
775
+ pool.end();
776
+ for (const n of items) {
777
+ const el = pool.elementsFor(n.id)[0];
778
+ if (el) pool.container.append(el);
779
+ }
780
+ }
781
+ renderLead(items) {
782
+ const { ctx } = this;
783
+ const { doc, opts } = ctx;
784
+ if (!items.length || opts.listLeadTitle === null) {
785
+ if (!this.lead) return;
786
+ opts.listLeadWillUnmount?.({ el: this.lead.slot });
787
+ this.lead.pool.clear();
788
+ this.lead.section.remove();
789
+ this.lead = null;
790
+ return;
791
+ }
792
+ if (!this.lead) {
793
+ const title = opts.listLeadTitle;
794
+ const slot = h(doc, "div", "tnc-list-lead-slot", { "data-tnc-no-drag": "" });
795
+ const heading = h(doc, "div", "tnc-list-day tnc-list-lead-head", null, h(doc, "span", "tnc-list-lead-title", null, title), slot);
796
+ const rows = h(doc, "div", "tnc-list-rows", { role: "group" });
797
+ const section = h(doc, "div", "tnc-list-section tnc-list-lead", { role: "listitem", "aria-label": title }, heading, rows);
798
+ this.lead = { section, slot, pool: new ItemPool(ctx, rows) };
799
+ opts.listLeadDidMount?.({ el: slot });
800
+ }
801
+ if (this.body.firstChild !== this.lead.section) this.body.prepend(this.lead.section);
802
+ const first = ctx.days[0] ?? ctx.view.range.startDay;
803
+ this.fill(this.lead.pool, items, (n) => Math.max(n.startDay, first));
804
+ }
805
+ addRow(day) {
806
+ const { ctx } = this;
807
+ const label = ctx.opts.listAddText ?? "";
808
+ const btn = h(ctx.doc, "button", "tnc-list-add", { type: "button", "aria-label": `${label}, ${ctx.fmt.longDate(day)}` }, label);
809
+ btn.addEventListener("click", (ev) => ctx.opts.onListAdd?.({ ...ctx.dayArg(day), jsEvent: ev }));
810
+ return btn;
811
+ }
812
+ /** Brings a day's section to the top, under the strip; the first day brings the lead section too. */
813
+ scrollToDay(day) {
814
+ const section = day === this.ctx.days[0] && this.lead ? this.lead.section : this.sections.get(day);
815
+ if (!section) return;
816
+ this.ctx.strip?.measure();
817
+ section.scrollIntoView({ block: "start" });
818
+ this.ctx.strip?.setFocus(day);
819
+ this.pinned = true;
820
+ }
821
+ fold = () => {
822
+ this.pinned = false;
823
+ const strip = this.ctx.strip;
824
+ if (strip?.expanded) strip.setExpanded(false);
825
+ };
826
+ onScroll = () => {
827
+ if (this.frame !== null) return;
828
+ this.frame = this.ctx.win.requestAnimationFrame(() => {
829
+ this.frame = null;
830
+ this.followTop();
831
+ });
832
+ };
833
+ /** Marks the first day not hidden under the strip. */
834
+ followTop() {
835
+ const strip = this.ctx.strip;
836
+ if (!strip || this.pinned) return;
837
+ const edge = strip.el.getBoundingClientRect().bottom + 1;
838
+ for (const day of this.ctx.days) {
839
+ const section = this.sections.get(day);
840
+ if (section && section.getBoundingClientRect().bottom > edge) {
841
+ strip.setFocus(day);
842
+ return;
843
+ }
844
+ }
727
845
  }
728
846
  showSelection(_sel) {
729
847
  }
730
- hit(_x, _y) {
731
- return null;
848
+ hit(x, y) {
849
+ const el = this.ctx.doc.elementFromPoint(x, y);
850
+ const section = el?.closest(".tnc-list-section");
851
+ if (!section || !this.body.contains(section)) return null;
852
+ if (section === this.lead?.section) {
853
+ const item = el?.closest(".tnc-item");
854
+ const meta = item ? this.ctx.meta.get(item) : void 0;
855
+ return { kind: "day", day: meta?.day ?? this.ctx.days[0] ?? this.ctx.view.range.startDay };
856
+ }
857
+ const day = parseDay(section.dataset.date ?? "");
858
+ return day === null ? null : { kind: "day", day };
732
859
  }
733
860
  getScroll() {
734
861
  return { top: this.scroller.scrollTop, left: 0 };
@@ -737,14 +864,231 @@ var ListView = class {
737
864
  this.scroller.scrollTop = s.top;
738
865
  }
739
866
  elementsFor(id) {
740
- return [...this.pools.values()].flatMap((p) => p.elementsFor(id));
867
+ const pools = [...this.pools.values(), ...this.lead ? [this.lead.pool] : []];
868
+ return pools.flatMap((p) => p.elementsFor(id));
741
869
  }
742
870
  destroy() {
871
+ const { win, opts } = this.ctx;
872
+ win.removeEventListener("scroll", this.onScroll, { capture: true });
873
+ if (this.frame !== null) win.cancelAnimationFrame(this.frame);
874
+ this.loader?.disconnect();
875
+ if (this.lead) opts.listLeadWillUnmount?.({ el: this.lead.slot });
876
+ this.lead?.pool.clear();
743
877
  for (const p of this.pools.values()) p.clear();
744
878
  this.el.remove();
745
879
  }
746
880
  };
747
881
 
882
+ // src/dom/views/strip.ts
883
+ var SWIPE_PX = 40;
884
+ var DateStrip = class {
885
+ constructor(ctx, pick, focus, onShown) {
886
+ this.ctx = ctx;
887
+ this.pick = pick;
888
+ this.onShown = onShown;
889
+ const { doc, text } = ctx;
890
+ this.focus = focus;
891
+ this.shown = startOfWeek(focus, ctx.firstDay);
892
+ this.el = h(doc, "div", "tnc-strip");
893
+ const head = h(doc, "div", "tnc-strip-head");
894
+ this.title = h(doc, "button", "tnc-strip-title", { type: "button", "aria-expanded": "false" });
895
+ this.title.addEventListener("click", () => this.setExpanded(!this.expanded));
896
+ const prev = h(doc, "button", "tnc-strip-nav tnc-strip-prev", { type: "button", "aria-label": text.prev, title: text.prev }, "\u2039");
897
+ const next = h(doc, "button", "tnc-strip-nav tnc-strip-next", { type: "button", "aria-label": text.next, title: text.next }, "\u203A");
898
+ prev.addEventListener("click", () => this.step(-1));
899
+ next.addEventListener("click", () => this.step(1));
900
+ this.todayBtn = h(doc, "button", "tnc-strip-today", { type: "button" }, text.today);
901
+ this.todayBtn.addEventListener("click", (ev) => this.pick(ctx.today, ev));
902
+ this.slot = h(doc, "div", "tnc-strip-slot", { "data-tnc-no-drag": "" });
903
+ head.append(this.title, this.todayBtn, prev, next, this.slot);
904
+ this.dows = h(doc, "div", "tnc-strip-dows", { "aria-hidden": "true" });
905
+ this.grid = h(doc, "div", "tnc-strip-grid", { role: "group" });
906
+ const handle = h(doc, "button", "tnc-strip-handle", { type: "button", tabindex: "-1", "aria-hidden": "true" });
907
+ handle.addEventListener("click", () => this.setExpanded(!this.expanded));
908
+ this.el.append(head, this.dows, this.grid, handle);
909
+ this.grid.addEventListener("click", this.onClick);
910
+ this.grid.addEventListener("keydown", this.onKeyDown);
911
+ this.el.addEventListener("pointerdown", this.onPointerDown);
912
+ this.el.addEventListener("pointerup", this.onPointerUp);
913
+ this.el.addEventListener("pointercancel", this.onPointerCancel);
914
+ this.el.addEventListener("click", this.onClickCapture, true);
915
+ this.draw();
916
+ ctx.opts.stripSlotDidMount?.({ el: this.slot });
917
+ }
918
+ ctx;
919
+ pick;
920
+ onShown;
921
+ el;
922
+ title;
923
+ dows;
924
+ grid;
925
+ slot;
926
+ todayBtn;
927
+ /** Day at the top of the list. */
928
+ focus;
929
+ /** First day of the week shown, or the month shown when expanded. */
930
+ shown;
931
+ target = null;
932
+ /** Days tinted as on screen besides the focus (a time grid's range). */
933
+ range = null;
934
+ shownKey = "";
935
+ swipe = null;
936
+ swallowClick = false;
937
+ expanded = false;
938
+ /** The list scrolled or the grid moved: mark the day and keep it in view. */
939
+ setFocus(day, range = this.range) {
940
+ const sameRange = range?.start === this.range?.start && range?.end === this.range?.end;
941
+ if (day === this.focus && sameRange) return;
942
+ this.range = range;
943
+ if (day !== this.focus) {
944
+ this.focus = day;
945
+ if (!this.expanded) this.shown = startOfWeek(day, this.ctx.firstDay);
946
+ else if (startOfMonth(day) !== this.shown) this.shown = startOfMonth(day);
947
+ }
948
+ this.draw();
949
+ }
950
+ /** Publishes the strip's height as `--tnc-strip-h`, for what sticks below it. */
951
+ measure() {
952
+ this.el.parentElement?.style.setProperty("--tnc-strip-h", `${this.el.offsetHeight}px`);
953
+ }
954
+ /** First day and end (exclusive) of the days on show. */
955
+ window() {
956
+ const days = this.days();
957
+ return { start: days[0], end: days[days.length - 1] + 1 };
958
+ }
959
+ setExpanded(on) {
960
+ if (on === this.expanded) return;
961
+ this.expanded = on;
962
+ this.shown = on ? startOfMonth(this.focus) : startOfWeek(this.focus, this.ctx.firstDay);
963
+ this.draw();
964
+ }
965
+ /** Highlights the day a dragged item would land on. */
966
+ markTarget(day) {
967
+ if (day === this.target) return;
968
+ this.target = day;
969
+ for (const cell of this.grid.querySelectorAll(".tnc-strip-day"))
970
+ cell.classList.toggle("is-drop-target", day !== null && cell.dataset.date === this.ctx.dayArg(day).dateStr);
971
+ }
972
+ dayAt(el) {
973
+ const cell = el.closest(".tnc-strip-day");
974
+ return cell && this.grid.contains(cell) ? parseDay(cell.dataset.date ?? "") : null;
975
+ }
976
+ step(dir) {
977
+ this.shown = this.expanded ? addMonths(this.shown, dir) : this.shown + 7 * dir;
978
+ this.draw();
979
+ }
980
+ days() {
981
+ if (!this.expanded) return Array.from({ length: 7 }, (_, i) => this.shown + i);
982
+ const first = startOfWeek(this.shown, this.ctx.firstDay);
983
+ const end = addMonths(this.shown, 1);
984
+ const weeks = Math.ceil((end - first) / 7);
985
+ return Array.from({ length: weeks * 7 }, (_, i) => first + i);
986
+ }
987
+ /** Redraws the cells; also called on every list render to refresh the dots. */
988
+ draw() {
989
+ const { ctx } = this;
990
+ const { doc, fmt, store } = ctx;
991
+ const days = this.days();
992
+ const titleDay = this.expanded ? this.shown : days.includes(this.focus) ? this.focus : this.shown + 3;
993
+ this.title.replaceChildren(fmt.monthYear(titleDay), h(doc, "span", "tnc-strip-caret", { "aria-hidden": "true" }));
994
+ this.title.setAttribute("aria-expanded", String(this.expanded));
995
+ this.el.classList.toggle("is-expanded", this.expanded);
996
+ this.todayBtn.hidden = this.focus === ctx.today;
997
+ this.dows.replaceChildren(...days.slice(0, 7).map((d) => h(doc, "span", null, null, fmt.weekdayNarrow(d))));
998
+ const focused = this.grid.contains(doc.activeElement) ? doc.activeElement.dataset.date : void 0;
999
+ const month = ymdOfDay(this.shown).month;
1000
+ const tabDay = days.includes(this.focus) ? this.focus : days[0];
1001
+ const cells = days.map((d) => {
1002
+ const arg = ctx.dayArg(d);
1003
+ const count = store.inRange(d, d + 1).filter((n) => n.item.display !== "background" && n.item.display !== "none").length;
1004
+ const first = ymdOfDay(d).day === 1 && d !== days[0] && !this.expanded;
1005
+ const label = fmt.longDate(d);
1006
+ const cell = h(
1007
+ doc,
1008
+ "button",
1009
+ [
1010
+ "tnc-strip-day",
1011
+ ...ctx.dayClasses(d),
1012
+ d === this.focus ? "is-focus" : "",
1013
+ d !== this.focus && this.range && d >= this.range.start && d < this.range.end ? "is-in-range" : "",
1014
+ count ? "has-items" : "",
1015
+ this.expanded && ymdOfDay(d).month !== month ? "is-other-month" : "",
1016
+ d === this.target ? "is-drop-target" : ""
1017
+ ].filter(Boolean).join(" "),
1018
+ {
1019
+ type: "button",
1020
+ "data-date": arg.dateStr,
1021
+ "aria-label": count ? `${label}, ${ctx.text.itemCount(count)}` : label,
1022
+ "aria-pressed": String(d === this.focus),
1023
+ tabindex: d === tabDay ? "0" : "-1"
1024
+ }
1025
+ );
1026
+ if (first) cell.append(h(doc, "span", "tnc-strip-month", { "aria-hidden": "true" }, fmt.monthShort(d)));
1027
+ cell.append(h(doc, "span", "tnc-strip-num", null, fmt.dayNumber(d)), h(doc, "span", "tnc-strip-dot", { "aria-hidden": "true" }));
1028
+ return cell;
1029
+ });
1030
+ this.grid.replaceChildren(...cells);
1031
+ this.grid.setAttribute("aria-label", fmt.monthYear(titleDay));
1032
+ if (focused) this.grid.querySelector(`[data-date="${focused}"]`)?.focus({ preventScroll: true });
1033
+ const key = `${days[0]}:${days.length}`;
1034
+ if (key !== this.shownKey) {
1035
+ this.shownKey = key;
1036
+ this.onShown();
1037
+ }
1038
+ }
1039
+ onClick = (ev) => {
1040
+ const day = this.dayAt(ev.target);
1041
+ if (day === null) return;
1042
+ if (this.expanded) this.setExpanded(false);
1043
+ this.pick(day, ev);
1044
+ };
1045
+ onKeyDown = (ev) => {
1046
+ const day = this.dayAt(ev.target);
1047
+ if (day === null) return;
1048
+ const move = { ArrowLeft: -1, ArrowRight: 1, ArrowUp: -7, ArrowDown: 7 }[ev.key];
1049
+ if (move === void 0) return;
1050
+ ev.preventDefault();
1051
+ const next = day + move;
1052
+ const days = this.days();
1053
+ if (next < days[0] || next > days[days.length - 1]) {
1054
+ this.shown = this.expanded ? startOfMonth(next) : startOfWeek(next, this.ctx.firstDay);
1055
+ this.draw();
1056
+ }
1057
+ const cell = this.grid.querySelector(`[data-date="${this.ctx.dayArg(next).dateStr}"]`);
1058
+ if (!cell) return;
1059
+ for (const c of this.grid.querySelectorAll("[tabindex='0']")) c.setAttribute("tabindex", "-1");
1060
+ cell.setAttribute("tabindex", "0");
1061
+ cell.focus();
1062
+ };
1063
+ onPointerDown = (ev) => {
1064
+ if (ev.button !== 0 || this.ctx.draggingId) return;
1065
+ this.swipe = { id: ev.pointerId, x: ev.clientX, y: ev.clientY };
1066
+ };
1067
+ onPointerUp = (ev) => {
1068
+ const s = this.swipe;
1069
+ this.swipe = null;
1070
+ if (!s || s.id !== ev.pointerId) return;
1071
+ const dx = ev.clientX - s.x;
1072
+ if (Math.abs(dx) < SWIPE_PX || Math.abs(dx) < Math.abs(ev.clientY - s.y)) return;
1073
+ this.swallowClick = true;
1074
+ this.step(dx < 0 ? 1 : -1);
1075
+ };
1076
+ onPointerCancel = () => {
1077
+ this.swipe = null;
1078
+ };
1079
+ /** A swipe that ends on a day is not a pick. */
1080
+ onClickCapture = (ev) => {
1081
+ if (!this.swallowClick) return;
1082
+ this.swallowClick = false;
1083
+ ev.stopPropagation();
1084
+ ev.preventDefault();
1085
+ };
1086
+ destroy() {
1087
+ this.ctx.opts.stripSlotWillUnmount?.({ el: this.slot });
1088
+ this.el.remove();
1089
+ }
1090
+ };
1091
+
748
1092
  // src/dom/views/timegrid.ts
749
1093
  var BAR_GAP2 = 2;
750
1094
  var TimeGridView = class {
@@ -1881,6 +2225,7 @@ ${this.ctx.text.more(items.length - 8)}` : ""}` : label;
1881
2225
 
1882
2226
  // src/dom/calendar.ts
1883
2227
  var EXTERNAL_ID = "__tnc_external__";
2228
+ var MAX_AGENDA_DAYS = 366;
1884
2229
  function sameOption(a, b, depth = 0) {
1885
2230
  if (a === b) return true;
1886
2231
  if (typeof a === "function" && typeof b === "function") return true;
@@ -1949,10 +2294,15 @@ var Calendar = class {
1949
2294
  slot = { min: 0, max: MINUTES_PER_DAY, duration: 30, snap: 30, scroll: 480 };
1950
2295
  draggingId = null;
1951
2296
  dragRow = null;
2297
+ strip = null;
2298
+ stripSize = null;
2299
+ rebuilding = false;
1952
2300
  toolbarEl;
1953
2301
  viewHost;
1954
2302
  current = null;
1955
2303
  anchor = 0;
2304
+ /** End of an agenda list grown past its `listDayCount`. */
2305
+ listEnd = null;
1956
2306
  session = null;
1957
2307
  selection = null;
1958
2308
  suppressClick = false;
@@ -2145,6 +2495,10 @@ var Calendar = class {
2145
2495
  if (!p) return;
2146
2496
  const day = p.kind === "day" ? p.day : dayOf(p.ms, this.zone);
2147
2497
  const ms = p.kind === "day" ? toInstant(p.day, 0, this.zone) : p.ms;
2498
+ if (this.agendaList()) {
2499
+ this.showListDay(day);
2500
+ return;
2501
+ }
2148
2502
  const { startDay, endDay } = this.view.range;
2149
2503
  if (day < startDay || day >= endDay) {
2150
2504
  this.anchor = day;
@@ -2156,7 +2510,7 @@ var Calendar = class {
2156
2510
  }
2157
2511
  /** What is under a client point: a time slot, a day, or a timeline row and date. Null outside the view. */
2158
2512
  hitTest(clientX, clientY) {
2159
- return this.current?.hit(clientX, clientY) ?? null;
2513
+ return this.hitAt(clientX, clientY);
2160
2514
  }
2161
2515
  /** Item elements currently rendered for an id. */
2162
2516
  elementsFor(id) {
@@ -2198,6 +2552,9 @@ var Calendar = class {
2198
2552
  this.unbindEvents();
2199
2553
  this.current?.destroy();
2200
2554
  this.current = null;
2555
+ this.stripSize?.disconnect();
2556
+ this.strip?.destroy();
2557
+ this.strip = null;
2201
2558
  this.root.remove();
2202
2559
  }
2203
2560
  /* ========================================================== view context */
@@ -2228,7 +2585,8 @@ var Calendar = class {
2228
2585
  return out;
2229
2586
  }
2230
2587
  isEditable(n, what) {
2231
- if (n.id === EXTERNAL_ID || this.opts.view === "list" || this.opts.view === "year") return false;
2588
+ if (n.id === EXTERNAL_ID || this.opts.view === "year") return false;
2589
+ if (this.opts.view === "list" && (!this.opts.listAgenda || what === "duration")) return false;
2232
2590
  const it = n.item;
2233
2591
  const base = it.editable ?? this.opts.editable;
2234
2592
  const specific = what === "start" ? it.startEditable : it.durationEditable;
@@ -2425,7 +2783,8 @@ var Calendar = class {
2425
2783
  }
2426
2784
  case "list": {
2427
2785
  const s = this.opts.listStart === "week" ? startOfWeek(a, this.firstDay) : a;
2428
- return { startDay: s, endDay: s + Math.max(1, listDayCount) };
2786
+ const end = s + Math.max(1, listDayCount);
2787
+ return { startDay: s, endDay: this.opts.listAgenda && this.listEnd !== null ? Math.max(end, this.listEnd) : end };
2429
2788
  }
2430
2789
  case "month": {
2431
2790
  const s = startOfWeek(startOfMonth(a), this.firstDay);
@@ -2463,6 +2822,33 @@ var Calendar = class {
2463
2822
  const duration = Math.max(1, parseDurationMinutes(this.opts.slotDuration, 30));
2464
2823
  const snap = Math.max(1, parseDurationMinutes(this.opts.snapDuration ?? null, duration));
2465
2824
  this.slot = { min, max, duration, snap, scroll: parseDurationMinutes(this.opts.scrollTime, 480) };
2825
+ if (navigated) this.listEnd = null;
2826
+ const prev = this.view;
2827
+ this.applyRange();
2828
+ this.conflicts = dependencyConflicts(this.store);
2829
+ this.root.className = "tnc";
2830
+ this.root.classList.add(`tnc--${this.opts.view}`, `tnc--height-${this.opts.height}`);
2831
+ if (!this.opts.timeGrid) this.root.classList.add("tnc--no-time-grid");
2832
+ this.renderToolbar();
2833
+ this.rebuilding = true;
2834
+ this.syncStrip(navigated);
2835
+ this.rebuilding = false;
2836
+ this.view.dataRange = this.dataRangeFor(this.view.range);
2837
+ const rangeChanged = rangeMoved(prev, this.view);
2838
+ this.current?.destroy();
2839
+ this.current = this.createView();
2840
+ this.viewHost.replaceChildren(this.current.el);
2841
+ this.current.render(null);
2842
+ if (keep && this.current instanceof TimelineView) {
2843
+ this.current.setScroll({ top: keep.top, left: 0 });
2844
+ this.current.placeInstant(keep.ms, keep.vx);
2845
+ } else if (scroll) this.current.setScroll(scroll);
2846
+ else if (this.current instanceof TimeGridView) this.current.scrollToTime(this.slot.scroll);
2847
+ else if (this.current instanceof TimelineView) this.current.scrollToDay(this.anchor);
2848
+ if (rangeChanged) this.opts.onRangeChange?.(this.view);
2849
+ }
2850
+ /** Sets the visible days and view info from the anchor. */
2851
+ applyRange() {
2466
2852
  const { startDay, endDay } = this.computeRange();
2467
2853
  const hidden = new Set(this.opts.hiddenDays);
2468
2854
  if (!this.opts.weekends) {
@@ -2479,26 +2865,100 @@ var Calendar = class {
2479
2865
  start: new Date(toInstant(startDay, 0, this.zone)),
2480
2866
  end: new Date(toInstant(endDay, 0, this.zone))
2481
2867
  };
2482
- const prevView = this.view;
2483
- this.view = { type: this.opts.view, range, anchor: this.anchor, title: "" };
2868
+ this.view = { type: this.opts.view, range, anchor: this.anchor, title: "", dataRange: this.dataRangeFor(range) };
2484
2869
  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);
2870
+ }
2871
+ /** The range, run on to the end of the strip's days so their dots are right. */
2872
+ dataRangeFor(range) {
2873
+ const end = this.strip ? Math.max(range.endDay, this.strip.window().end) : range.endDay;
2874
+ if (end === range.endDay) return range;
2875
+ return { ...range, endDay: end, end: new Date(toInstant(end, 0, this.zone)) };
2876
+ }
2877
+ /** Adds, updates or removes the date strip for the current view. */
2878
+ syncStrip(navigated) {
2879
+ const { view, listAgenda, dateStrip } = this.opts;
2880
+ const list = view === "list";
2881
+ const want = list ? listAgenda : dateStrip && (view === "day" || view === "days" || view === "week");
2882
+ if (!want) {
2883
+ if (!this.strip) return;
2884
+ this.stripSize?.disconnect();
2885
+ this.stripSize = null;
2886
+ this.strip.destroy();
2887
+ this.strip = null;
2888
+ this.root.style.removeProperty("--tnc-strip-h");
2889
+ return;
2890
+ }
2891
+ this.root.classList.add("tnc--has-strip");
2892
+ const { startDay, endDay } = this.view.range;
2893
+ if (!this.strip) {
2894
+ const strip = new DateStrip(this, (day, ev) => this.pickStripDay(day, ev), startDay, () => this.stripShown());
2895
+ this.strip = strip;
2896
+ this.root.insertBefore(strip.el, this.viewHost);
2897
+ const RO = this.win.ResizeObserver;
2898
+ if (RO) {
2899
+ this.stripSize = new RO(() => strip.measure());
2900
+ this.stripSize.observe(strip.el);
2901
+ }
2902
+ }
2903
+ if (!list) this.strip.setFocus(startDay, { start: startDay, end: endDay });
2904
+ else if (navigated) this.strip.setFocus(startDay, null);
2905
+ this.strip.draw();
2906
+ }
2907
+ pickStripDay(day, ev) {
2908
+ this.dateClick(day, ev);
2909
+ if (this.agendaList()) {
2910
+ this.showListDay(day);
2911
+ return;
2912
+ }
2913
+ const { startDay, endDay } = this.view.range;
2914
+ if (this.opts.view === "week" && day >= startDay && day < endDay) return;
2915
+ this.anchor = day;
2916
+ this.rebuild(true);
2917
+ }
2918
+ /** The strip moved to other days: load theirs too. */
2919
+ stripShown() {
2920
+ if (!this.strip || this.rebuilding || !this.view) return;
2921
+ const next = this.dataRangeFor(this.view.range);
2922
+ if (next.endDay === this.view.dataRange.endDay) return;
2923
+ this.view = { ...this.view, dataRange: next };
2924
+ this.opts.onRangeChange?.(this.view);
2925
+ }
2926
+ /** What is under a point, the date strip included. */
2927
+ hitAt(x, y) {
2928
+ const strip = this.strip;
2929
+ if (strip) {
2930
+ const el = this.doc.elementFromPoint(x, y);
2931
+ const day = el ? strip.dayAt(el) : null;
2932
+ strip.markTarget(day !== null && this.draggingId ? day : null);
2933
+ if (day !== null) return { kind: "day", day };
2934
+ }
2935
+ return this.current?.hit(x, y) ?? null;
2936
+ }
2937
+ agendaList() {
2938
+ return this.opts.view === "list" && this.opts.listAgenda && this.current instanceof ListView ? this.current : null;
2939
+ }
2940
+ extendList(endDay) {
2941
+ if (!this.agendaList()) return;
2942
+ const { startDay, endDay: current } = this.view.range;
2943
+ const end = Math.min(endDay, startDay + MAX_AGENDA_DAYS);
2944
+ if (end <= current) return;
2945
+ this.listEnd = end;
2946
+ this.applyRange();
2947
+ this.current?.render(null);
2948
+ this.strip?.draw();
2949
+ this.opts.onRangeChange?.(this.view);
2950
+ }
2951
+ showListDay(day) {
2952
+ if (!this.agendaList()) return;
2953
+ if (day < this.view.range.startDay || day - this.view.range.startDay >= MAX_AGENDA_DAYS) {
2954
+ this.anchor = day;
2955
+ this.rebuild(true);
2956
+ }
2957
+ this.extendList(day + 2 * Math.max(7, this.opts.listDayCount));
2958
+ this.agendaList()?.scrollToDay(day);
2959
+ }
2960
+ dateClick(day, jsEvent) {
2961
+ this.clickSlot({ kind: "day", day }, jsEvent, true);
2502
2962
  }
2503
2963
  createView() {
2504
2964
  switch (this.opts.view) {
@@ -2801,8 +3261,9 @@ var Calendar = class {
2801
3261
  };
2802
3262
  this.session = s;
2803
3263
  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 };
3264
+ const x = scrollerOf(itemEl, "x");
3265
+ const y = scrollerOf(itemEl, "y");
3266
+ s.pan = { x, y, left: x?.scrollLeft ?? 0, top: y?.scrollTop ?? 0, active: false };
2806
3267
  }
2807
3268
  if (touch) {
2808
3269
  s.timer = this.win.setTimeout(() => {
@@ -2843,8 +3304,8 @@ var Calendar = class {
2843
3304
  if (!pan.active && s.timer !== null) this.win.clearTimeout(s.timer);
2844
3305
  s.timer = null;
2845
3306
  pan.active = true;
2846
- pan.el.scrollLeft = pan.left - (ev.clientX - s.x0);
2847
- pan.el.scrollTop = pan.top - (ev.clientY - s.y0);
3307
+ if (pan.x) pan.x.scrollLeft = pan.left - (ev.clientX - s.x0);
3308
+ if (pan.y) pan.y.scrollTop = pan.top - (ev.clientY - s.y0);
2848
3309
  } else if (dist > 8) this.endSession(null, true);
2849
3310
  return;
2850
3311
  }
@@ -2882,7 +3343,7 @@ var Calendar = class {
2882
3343
  };
2883
3344
  /** Proposal for the current pointer position. */
2884
3345
  track(s, ev) {
2885
- const hit = this.current?.hit(ev.clientX, ev.clientY) ?? null;
3346
+ const hit = this.hitAt(ev.clientX, ev.clientY);
2886
3347
  if (s.mode === "external") {
2887
3348
  this.trackExternal(s, hit);
2888
3349
  return;
@@ -3008,7 +3469,7 @@ var Calendar = class {
3008
3469
  return this.dayProposal(hit.day, hit.day + this.opts.defaultAllDayDays);
3009
3470
  }
3010
3471
  if (hit.kind === "day") {
3011
- const dd = hit.day - (grab.kind === "day" ? grab.day : hit.day);
3472
+ const dd = hit.day - (grab.kind === "timeline" ? hit.day : grab.day);
3012
3473
  if (o.allDay) return this.dayProposal(o.startDay + dd, o.startDay + dd + days);
3013
3474
  const startMs2 = this.civilToMs(this.civil(o.startMs) + dd * MINUTES_PER_DAY);
3014
3475
  const endMs = this.civilToMs(this.civil(o.endMs) + dd * MINUTES_PER_DAY);
@@ -3143,6 +3604,7 @@ var Calendar = class {
3143
3604
  this.draggingId = null;
3144
3605
  this.dragRow = null;
3145
3606
  if (draggedId) for (const el of this.elementsFor(draggedId)) el.classList.remove("is-dragging");
3607
+ this.strip?.markTarget(null);
3146
3608
  if (!s.active) return;
3147
3609
  if (s.mode === "select") {
3148
3610
  if (cancel) this.unselectSilently();
@@ -3429,6 +3891,19 @@ var Calendar = class {
3429
3891
  });
3430
3892
  };
3431
3893
  };
3894
+ function scrollerOf(el, axis) {
3895
+ const doc = el.ownerDocument;
3896
+ const win = doc.defaultView;
3897
+ for (let p = el.parentElement; p && p !== doc.body && p !== doc.documentElement; p = p.parentElement) {
3898
+ const overflow = win?.getComputedStyle(p)[axis === "x" ? "overflowX" : "overflowY"] ?? "";
3899
+ const room = axis === "x" ? p.scrollWidth > p.clientWidth : p.scrollHeight > p.clientHeight;
3900
+ if (room && /auto|scroll|overlay/.test(overflow)) return p;
3901
+ }
3902
+ return doc.scrollingElement;
3903
+ }
3904
+ function rangeMoved(prev, next) {
3905
+ return !prev || prev.type !== next.type || prev.range.startDay !== next.range.startDay || prev.range.endDay !== next.range.endDay || prev.dataRange.endDay !== next.dataRange.endDay;
3906
+ }
3432
3907
 
3433
3908
  export {
3434
3909
  Formatter,