@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.
@@ -948,6 +948,10 @@ function defaultOptions() {
948
948
  dayCount: 3,
949
949
  listDayCount: 7,
950
950
  listStart: "date",
951
+ listAgenda: false,
952
+ dateStrip: false,
953
+ listLeadTitle: null,
954
+ listAddText: null,
951
955
  monthDisplay: "full",
952
956
  timeZone: "local",
953
957
  locale: void 0,
@@ -997,6 +1001,10 @@ var LAYOUT_KEYS = /* @__PURE__ */ new Set([
997
1001
  "dayCount",
998
1002
  "listDayCount",
999
1003
  "listStart",
1004
+ "listAgenda",
1005
+ "dateStrip",
1006
+ "listLeadTitle",
1007
+ "listAddText",
1000
1008
  "monthDisplay",
1001
1009
  "timeZone",
1002
1010
  "locale",
@@ -1339,21 +1347,48 @@ var DayGridView = class {
1339
1347
  };
1340
1348
 
1341
1349
  // src/dom/views/list.ts
1350
+ var LOAD_AHEAD = "0px 0px 600px 0px";
1342
1351
  var ListView = class {
1343
1352
  constructor(ctx) {
1344
1353
  this.ctx = ctx;
1345
- this.el = h(ctx.doc, "div", "tnc-list");
1346
- this.scroller = h(ctx.doc, "div", "tnc-list-scroll");
1347
- this.body = h(ctx.doc, "div", "tnc-list-body", { role: "list", "aria-label": ctx.view.title });
1354
+ const { doc, win, opts } = ctx;
1355
+ this.el = h(doc, "div", "tnc-list");
1356
+ this.scroller = h(doc, "div", "tnc-list-scroll");
1357
+ this.body = h(doc, "div", "tnc-list-body", { role: "list", "aria-label": ctx.view.title });
1348
1358
  this.scroller.append(this.body);
1349
1359
  this.el.append(this.scroller);
1360
+ this.agenda = opts.listAgenda;
1361
+ if (!this.agenda) return;
1362
+ this.el.classList.add("tnc-list--agenda");
1363
+ this.sentinel = h(doc, "div", "tnc-list-more", { "aria-hidden": "true" });
1364
+ this.scroller.append(this.sentinel);
1365
+ const IO = win.IntersectionObserver;
1366
+ if (IO) {
1367
+ this.loader = new IO(
1368
+ (entries) => {
1369
+ if (entries.some((e) => e.isIntersecting)) ctx.extendList(ctx.view.range.endDay + Math.max(7, opts.listDayCount));
1370
+ },
1371
+ // Auto height scrolls the page, so watch the viewport; fixed height scrolls the list.
1372
+ { root: opts.height === "auto" ? null : this.scroller, rootMargin: LOAD_AHEAD }
1373
+ );
1374
+ }
1375
+ win.addEventListener("scroll", this.onScroll, { capture: true, passive: true });
1376
+ this.scroller.addEventListener("wheel", this.fold, { passive: true });
1377
+ this.scroller.addEventListener("touchmove", this.fold, { passive: true });
1350
1378
  }
1351
1379
  ctx;
1352
1380
  el;
1353
1381
  scroller;
1354
1382
  body;
1383
+ agenda;
1355
1384
  pools = /* @__PURE__ */ new Map();
1356
1385
  sections = /* @__PURE__ */ new Map();
1386
+ lead = null;
1387
+ sentinel = null;
1388
+ loader = null;
1389
+ frame = null;
1390
+ /** A picked day stays marked until the list is scrolled by hand. */
1391
+ pinned = false;
1357
1392
  itemsOn(day) {
1358
1393
  const { store, opts } = this.ctx;
1359
1394
  const out = store.spanningIn(day, day + 1, true);
@@ -1362,46 +1397,45 @@ var ListView = class {
1362
1397
  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));
1363
1398
  }
1364
1399
  render(_change) {
1365
- const { ctx } = this;
1366
- const { doc, days } = ctx;
1400
+ const { ctx, agenda } = this;
1401
+ const { doc, days, opts } = ctx;
1402
+ const includes = agenda && opts.listLeadTitle !== null ? opts.listLeadIncludes : void 0;
1403
+ const leadItems = [];
1404
+ const inLead = /* @__PURE__ */ new Set();
1367
1405
  const keep = /* @__PURE__ */ new Set();
1368
1406
  let prev = null;
1369
1407
  for (const day of days) {
1370
- const items = this.itemsOn(day);
1371
- if (!items.length) continue;
1408
+ let items = this.itemsOn(day);
1409
+ if (includes) {
1410
+ items = items.filter((n) => {
1411
+ if (!includes(n.item)) return true;
1412
+ if (!inLead.has(n.id)) {
1413
+ inLead.add(n.id);
1414
+ leadItems.push(n);
1415
+ }
1416
+ return false;
1417
+ });
1418
+ }
1419
+ if (!items.length && !agenda) continue;
1372
1420
  keep.add(day);
1373
1421
  let section = this.sections.get(day);
1374
1422
  let pool = this.pools.get(day);
1375
1423
  if (!section || !pool) {
1376
1424
  const arg = ctx.dayArg(day);
1377
1425
  const heading = h(doc, "div", ["tnc-list-day", ...ctx.dayClasses(day)].join(" "), { "data-date": arg.dateStr });
1378
- const custom = ctx.opts.dayHeaderContent?.(arg);
1426
+ const custom = opts.dayHeaderContent?.(arg);
1379
1427
  const label = custom ?? ctx.fmt.longDate(day);
1380
- heading.append(ctx.opts.navLinks ? ctx.navLink(day, label, "tnc-list-day-link") : label);
1381
- const rows2 = h(doc, "div", "tnc-list-rows", { role: "group" });
1382
- section = h(doc, "div", "tnc-list-section", { role: "listitem", "aria-label": ctx.fmt.longDate(day), "data-date": arg.dateStr }, heading, rows2);
1383
- ctx.opts.dayHeaderDidMount?.({ ...arg, el: heading });
1384
- pool = new ItemPool(ctx, rows2);
1428
+ heading.append(opts.navLinks ? ctx.navLink(day, label, "tnc-list-day-link") : label);
1429
+ const rows = h(doc, "div", "tnc-list-rows", { role: "group" });
1430
+ section = h(doc, "div", "tnc-list-section", { role: "listitem", "aria-label": ctx.fmt.longDate(day), "data-date": arg.dateStr }, heading, rows);
1431
+ if (agenda && opts.listAddText !== null) section.append(this.addRow(day));
1432
+ opts.dayHeaderDidMount?.({ ...arg, el: heading });
1433
+ pool = new ItemPool(ctx, rows);
1385
1434
  this.sections.set(day, section);
1386
1435
  this.pools.set(day, pool);
1387
1436
  }
1388
- pool.begin();
1389
- for (const n of items) {
1390
- pool.place(n.id, {
1391
- n,
1392
- day,
1393
- isStart: n.startDay === day,
1394
- isEnd: n.endDay === day + 1,
1395
- variant: "list",
1396
- mirror: false
1397
- });
1398
- }
1399
- pool.end();
1400
- const rows = pool.container;
1401
- for (const n of items) {
1402
- const el = pool.elementsFor(n.id)[0];
1403
- if (el) rows.append(el);
1404
- }
1437
+ section.classList.toggle("is-empty", !items.length);
1438
+ this.fill(pool, items, () => day);
1405
1439
  if (prev ? prev.nextSibling !== section : this.body.firstChild !== section) {
1406
1440
  if (prev) prev.after(section);
1407
1441
  else this.body.prepend(section);
@@ -1415,18 +1449,111 @@ var ListView = class {
1415
1449
  section.remove();
1416
1450
  this.sections.delete(day);
1417
1451
  }
1452
+ this.renderLead(leadItems);
1418
1453
  let empty = this.body.querySelector(":scope > .tnc-list-empty");
1419
- if (!keep.size) {
1454
+ if (!keep.size && !leadItems.length) {
1420
1455
  if (!empty) {
1421
1456
  empty = h(doc, "div", "tnc-list-empty", { role: "listitem" }, ctx.text.noItems);
1422
1457
  this.body.append(empty);
1423
1458
  }
1424
1459
  } else empty?.remove();
1460
+ this.ctx.strip?.draw();
1461
+ if (this.loader && this.sentinel) {
1462
+ this.loader.unobserve(this.sentinel);
1463
+ this.loader.observe(this.sentinel);
1464
+ }
1465
+ }
1466
+ /** Places items in a pool, keeping DOM order equal to item order. */
1467
+ fill(pool, items, dayFor) {
1468
+ pool.begin();
1469
+ for (const n of items) {
1470
+ const d = dayFor(n);
1471
+ pool.place(n.id, { n, day: d, isStart: n.startDay === d, isEnd: n.endDay === d + 1, variant: "list", mirror: false });
1472
+ }
1473
+ pool.end();
1474
+ for (const n of items) {
1475
+ const el = pool.elementsFor(n.id)[0];
1476
+ if (el) pool.container.append(el);
1477
+ }
1478
+ }
1479
+ renderLead(items) {
1480
+ const { ctx } = this;
1481
+ const { doc, opts } = ctx;
1482
+ if (!items.length || opts.listLeadTitle === null) {
1483
+ if (!this.lead) return;
1484
+ opts.listLeadWillUnmount?.({ el: this.lead.slot });
1485
+ this.lead.pool.clear();
1486
+ this.lead.section.remove();
1487
+ this.lead = null;
1488
+ return;
1489
+ }
1490
+ if (!this.lead) {
1491
+ const title = opts.listLeadTitle;
1492
+ const slot = h(doc, "div", "tnc-list-lead-slot", { "data-tnc-no-drag": "" });
1493
+ const heading = h(doc, "div", "tnc-list-day tnc-list-lead-head", null, h(doc, "span", "tnc-list-lead-title", null, title), slot);
1494
+ const rows = h(doc, "div", "tnc-list-rows", { role: "group" });
1495
+ const section = h(doc, "div", "tnc-list-section tnc-list-lead", { role: "listitem", "aria-label": title }, heading, rows);
1496
+ this.lead = { section, slot, pool: new ItemPool(ctx, rows) };
1497
+ opts.listLeadDidMount?.({ el: slot });
1498
+ }
1499
+ if (this.body.firstChild !== this.lead.section) this.body.prepend(this.lead.section);
1500
+ const first = ctx.days[0] ?? ctx.view.range.startDay;
1501
+ this.fill(this.lead.pool, items, (n) => Math.max(n.startDay, first));
1502
+ }
1503
+ addRow(day) {
1504
+ const { ctx } = this;
1505
+ const label = ctx.opts.listAddText ?? "";
1506
+ const btn = h(ctx.doc, "button", "tnc-list-add", { type: "button", "aria-label": `${label}, ${ctx.fmt.longDate(day)}` }, label);
1507
+ btn.addEventListener("click", (ev) => ctx.opts.onListAdd?.({ ...ctx.dayArg(day), jsEvent: ev }));
1508
+ return btn;
1509
+ }
1510
+ /** Brings a day's section to the top, under the strip; the first day brings the lead section too. */
1511
+ scrollToDay(day) {
1512
+ const section = day === this.ctx.days[0] && this.lead ? this.lead.section : this.sections.get(day);
1513
+ if (!section) return;
1514
+ this.ctx.strip?.measure();
1515
+ section.scrollIntoView({ block: "start" });
1516
+ this.ctx.strip?.setFocus(day);
1517
+ this.pinned = true;
1518
+ }
1519
+ fold = () => {
1520
+ this.pinned = false;
1521
+ const strip = this.ctx.strip;
1522
+ if (strip?.expanded) strip.setExpanded(false);
1523
+ };
1524
+ onScroll = () => {
1525
+ if (this.frame !== null) return;
1526
+ this.frame = this.ctx.win.requestAnimationFrame(() => {
1527
+ this.frame = null;
1528
+ this.followTop();
1529
+ });
1530
+ };
1531
+ /** Marks the first day not hidden under the strip. */
1532
+ followTop() {
1533
+ const strip = this.ctx.strip;
1534
+ if (!strip || this.pinned) return;
1535
+ const edge = strip.el.getBoundingClientRect().bottom + 1;
1536
+ for (const day of this.ctx.days) {
1537
+ const section = this.sections.get(day);
1538
+ if (section && section.getBoundingClientRect().bottom > edge) {
1539
+ strip.setFocus(day);
1540
+ return;
1541
+ }
1542
+ }
1425
1543
  }
1426
1544
  showSelection(_sel) {
1427
1545
  }
1428
- hit(_x, _y) {
1429
- return null;
1546
+ hit(x, y) {
1547
+ const el = this.ctx.doc.elementFromPoint(x, y);
1548
+ const section = el?.closest(".tnc-list-section");
1549
+ if (!section || !this.body.contains(section)) return null;
1550
+ if (section === this.lead?.section) {
1551
+ const item = el?.closest(".tnc-item");
1552
+ const meta = item ? this.ctx.meta.get(item) : void 0;
1553
+ return { kind: "day", day: meta?.day ?? this.ctx.days[0] ?? this.ctx.view.range.startDay };
1554
+ }
1555
+ const day = parseDay(section.dataset.date ?? "");
1556
+ return day === null ? null : { kind: "day", day };
1430
1557
  }
1431
1558
  getScroll() {
1432
1559
  return { top: this.scroller.scrollTop, left: 0 };
@@ -1435,14 +1562,231 @@ var ListView = class {
1435
1562
  this.scroller.scrollTop = s.top;
1436
1563
  }
1437
1564
  elementsFor(id) {
1438
- return [...this.pools.values()].flatMap((p) => p.elementsFor(id));
1565
+ const pools = [...this.pools.values(), ...this.lead ? [this.lead.pool] : []];
1566
+ return pools.flatMap((p) => p.elementsFor(id));
1439
1567
  }
1440
1568
  destroy() {
1569
+ const { win, opts } = this.ctx;
1570
+ win.removeEventListener("scroll", this.onScroll, { capture: true });
1571
+ if (this.frame !== null) win.cancelAnimationFrame(this.frame);
1572
+ this.loader?.disconnect();
1573
+ if (this.lead) opts.listLeadWillUnmount?.({ el: this.lead.slot });
1574
+ this.lead?.pool.clear();
1441
1575
  for (const p of this.pools.values()) p.clear();
1442
1576
  this.el.remove();
1443
1577
  }
1444
1578
  };
1445
1579
 
1580
+ // src/dom/views/strip.ts
1581
+ var SWIPE_PX = 40;
1582
+ var DateStrip = class {
1583
+ constructor(ctx, pick, focus, onShown) {
1584
+ this.ctx = ctx;
1585
+ this.pick = pick;
1586
+ this.onShown = onShown;
1587
+ const { doc, text } = ctx;
1588
+ this.focus = focus;
1589
+ this.shown = startOfWeek(focus, ctx.firstDay);
1590
+ this.el = h(doc, "div", "tnc-strip");
1591
+ const head = h(doc, "div", "tnc-strip-head");
1592
+ this.title = h(doc, "button", "tnc-strip-title", { type: "button", "aria-expanded": "false" });
1593
+ this.title.addEventListener("click", () => this.setExpanded(!this.expanded));
1594
+ const prev = h(doc, "button", "tnc-strip-nav tnc-strip-prev", { type: "button", "aria-label": text.prev, title: text.prev }, "\u2039");
1595
+ const next = h(doc, "button", "tnc-strip-nav tnc-strip-next", { type: "button", "aria-label": text.next, title: text.next }, "\u203A");
1596
+ prev.addEventListener("click", () => this.step(-1));
1597
+ next.addEventListener("click", () => this.step(1));
1598
+ this.todayBtn = h(doc, "button", "tnc-strip-today", { type: "button" }, text.today);
1599
+ this.todayBtn.addEventListener("click", (ev) => this.pick(ctx.today, ev));
1600
+ this.slot = h(doc, "div", "tnc-strip-slot", { "data-tnc-no-drag": "" });
1601
+ head.append(this.title, this.todayBtn, prev, next, this.slot);
1602
+ this.dows = h(doc, "div", "tnc-strip-dows", { "aria-hidden": "true" });
1603
+ this.grid = h(doc, "div", "tnc-strip-grid", { role: "group" });
1604
+ const handle = h(doc, "button", "tnc-strip-handle", { type: "button", tabindex: "-1", "aria-hidden": "true" });
1605
+ handle.addEventListener("click", () => this.setExpanded(!this.expanded));
1606
+ this.el.append(head, this.dows, this.grid, handle);
1607
+ this.grid.addEventListener("click", this.onClick);
1608
+ this.grid.addEventListener("keydown", this.onKeyDown);
1609
+ this.el.addEventListener("pointerdown", this.onPointerDown);
1610
+ this.el.addEventListener("pointerup", this.onPointerUp);
1611
+ this.el.addEventListener("pointercancel", this.onPointerCancel);
1612
+ this.el.addEventListener("click", this.onClickCapture, true);
1613
+ this.draw();
1614
+ ctx.opts.stripSlotDidMount?.({ el: this.slot });
1615
+ }
1616
+ ctx;
1617
+ pick;
1618
+ onShown;
1619
+ el;
1620
+ title;
1621
+ dows;
1622
+ grid;
1623
+ slot;
1624
+ todayBtn;
1625
+ /** Day at the top of the list. */
1626
+ focus;
1627
+ /** First day of the week shown, or the month shown when expanded. */
1628
+ shown;
1629
+ target = null;
1630
+ /** Days tinted as on screen besides the focus (a time grid's range). */
1631
+ range = null;
1632
+ shownKey = "";
1633
+ swipe = null;
1634
+ swallowClick = false;
1635
+ expanded = false;
1636
+ /** The list scrolled or the grid moved: mark the day and keep it in view. */
1637
+ setFocus(day, range = this.range) {
1638
+ const sameRange = range?.start === this.range?.start && range?.end === this.range?.end;
1639
+ if (day === this.focus && sameRange) return;
1640
+ this.range = range;
1641
+ if (day !== this.focus) {
1642
+ this.focus = day;
1643
+ if (!this.expanded) this.shown = startOfWeek(day, this.ctx.firstDay);
1644
+ else if (startOfMonth(day) !== this.shown) this.shown = startOfMonth(day);
1645
+ }
1646
+ this.draw();
1647
+ }
1648
+ /** Publishes the strip's height as `--tnc-strip-h`, for what sticks below it. */
1649
+ measure() {
1650
+ this.el.parentElement?.style.setProperty("--tnc-strip-h", `${this.el.offsetHeight}px`);
1651
+ }
1652
+ /** First day and end (exclusive) of the days on show. */
1653
+ window() {
1654
+ const days = this.days();
1655
+ return { start: days[0], end: days[days.length - 1] + 1 };
1656
+ }
1657
+ setExpanded(on) {
1658
+ if (on === this.expanded) return;
1659
+ this.expanded = on;
1660
+ this.shown = on ? startOfMonth(this.focus) : startOfWeek(this.focus, this.ctx.firstDay);
1661
+ this.draw();
1662
+ }
1663
+ /** Highlights the day a dragged item would land on. */
1664
+ markTarget(day) {
1665
+ if (day === this.target) return;
1666
+ this.target = day;
1667
+ for (const cell of this.grid.querySelectorAll(".tnc-strip-day"))
1668
+ cell.classList.toggle("is-drop-target", day !== null && cell.dataset.date === this.ctx.dayArg(day).dateStr);
1669
+ }
1670
+ dayAt(el) {
1671
+ const cell = el.closest(".tnc-strip-day");
1672
+ return cell && this.grid.contains(cell) ? parseDay(cell.dataset.date ?? "") : null;
1673
+ }
1674
+ step(dir) {
1675
+ this.shown = this.expanded ? addMonths(this.shown, dir) : this.shown + 7 * dir;
1676
+ this.draw();
1677
+ }
1678
+ days() {
1679
+ if (!this.expanded) return Array.from({ length: 7 }, (_, i) => this.shown + i);
1680
+ const first = startOfWeek(this.shown, this.ctx.firstDay);
1681
+ const end = addMonths(this.shown, 1);
1682
+ const weeks = Math.ceil((end - first) / 7);
1683
+ return Array.from({ length: weeks * 7 }, (_, i) => first + i);
1684
+ }
1685
+ /** Redraws the cells; also called on every list render to refresh the dots. */
1686
+ draw() {
1687
+ const { ctx } = this;
1688
+ const { doc, fmt, store } = ctx;
1689
+ const days = this.days();
1690
+ const titleDay = this.expanded ? this.shown : days.includes(this.focus) ? this.focus : this.shown + 3;
1691
+ this.title.replaceChildren(fmt.monthYear(titleDay), h(doc, "span", "tnc-strip-caret", { "aria-hidden": "true" }));
1692
+ this.title.setAttribute("aria-expanded", String(this.expanded));
1693
+ this.el.classList.toggle("is-expanded", this.expanded);
1694
+ this.todayBtn.hidden = this.focus === ctx.today;
1695
+ this.dows.replaceChildren(...days.slice(0, 7).map((d) => h(doc, "span", null, null, fmt.weekdayNarrow(d))));
1696
+ const focused = this.grid.contains(doc.activeElement) ? doc.activeElement.dataset.date : void 0;
1697
+ const month = ymdOfDay(this.shown).month;
1698
+ const tabDay = days.includes(this.focus) ? this.focus : days[0];
1699
+ const cells = days.map((d) => {
1700
+ const arg = ctx.dayArg(d);
1701
+ const count = store.inRange(d, d + 1).filter((n) => n.item.display !== "background" && n.item.display !== "none").length;
1702
+ const first = ymdOfDay(d).day === 1 && d !== days[0] && !this.expanded;
1703
+ const label = fmt.longDate(d);
1704
+ const cell = h(
1705
+ doc,
1706
+ "button",
1707
+ [
1708
+ "tnc-strip-day",
1709
+ ...ctx.dayClasses(d),
1710
+ d === this.focus ? "is-focus" : "",
1711
+ d !== this.focus && this.range && d >= this.range.start && d < this.range.end ? "is-in-range" : "",
1712
+ count ? "has-items" : "",
1713
+ this.expanded && ymdOfDay(d).month !== month ? "is-other-month" : "",
1714
+ d === this.target ? "is-drop-target" : ""
1715
+ ].filter(Boolean).join(" "),
1716
+ {
1717
+ type: "button",
1718
+ "data-date": arg.dateStr,
1719
+ "aria-label": count ? `${label}, ${ctx.text.itemCount(count)}` : label,
1720
+ "aria-pressed": String(d === this.focus),
1721
+ tabindex: d === tabDay ? "0" : "-1"
1722
+ }
1723
+ );
1724
+ if (first) cell.append(h(doc, "span", "tnc-strip-month", { "aria-hidden": "true" }, fmt.monthShort(d)));
1725
+ cell.append(h(doc, "span", "tnc-strip-num", null, fmt.dayNumber(d)), h(doc, "span", "tnc-strip-dot", { "aria-hidden": "true" }));
1726
+ return cell;
1727
+ });
1728
+ this.grid.replaceChildren(...cells);
1729
+ this.grid.setAttribute("aria-label", fmt.monthYear(titleDay));
1730
+ if (focused) this.grid.querySelector(`[data-date="${focused}"]`)?.focus({ preventScroll: true });
1731
+ const key = `${days[0]}:${days.length}`;
1732
+ if (key !== this.shownKey) {
1733
+ this.shownKey = key;
1734
+ this.onShown();
1735
+ }
1736
+ }
1737
+ onClick = (ev) => {
1738
+ const day = this.dayAt(ev.target);
1739
+ if (day === null) return;
1740
+ if (this.expanded) this.setExpanded(false);
1741
+ this.pick(day, ev);
1742
+ };
1743
+ onKeyDown = (ev) => {
1744
+ const day = this.dayAt(ev.target);
1745
+ if (day === null) return;
1746
+ const move = { ArrowLeft: -1, ArrowRight: 1, ArrowUp: -7, ArrowDown: 7 }[ev.key];
1747
+ if (move === void 0) return;
1748
+ ev.preventDefault();
1749
+ const next = day + move;
1750
+ const days = this.days();
1751
+ if (next < days[0] || next > days[days.length - 1]) {
1752
+ this.shown = this.expanded ? startOfMonth(next) : startOfWeek(next, this.ctx.firstDay);
1753
+ this.draw();
1754
+ }
1755
+ const cell = this.grid.querySelector(`[data-date="${this.ctx.dayArg(next).dateStr}"]`);
1756
+ if (!cell) return;
1757
+ for (const c of this.grid.querySelectorAll("[tabindex='0']")) c.setAttribute("tabindex", "-1");
1758
+ cell.setAttribute("tabindex", "0");
1759
+ cell.focus();
1760
+ };
1761
+ onPointerDown = (ev) => {
1762
+ if (ev.button !== 0 || this.ctx.draggingId) return;
1763
+ this.swipe = { id: ev.pointerId, x: ev.clientX, y: ev.clientY };
1764
+ };
1765
+ onPointerUp = (ev) => {
1766
+ const s = this.swipe;
1767
+ this.swipe = null;
1768
+ if (!s || s.id !== ev.pointerId) return;
1769
+ const dx = ev.clientX - s.x;
1770
+ if (Math.abs(dx) < SWIPE_PX || Math.abs(dx) < Math.abs(ev.clientY - s.y)) return;
1771
+ this.swallowClick = true;
1772
+ this.step(dx < 0 ? 1 : -1);
1773
+ };
1774
+ onPointerCancel = () => {
1775
+ this.swipe = null;
1776
+ };
1777
+ /** A swipe that ends on a day is not a pick. */
1778
+ onClickCapture = (ev) => {
1779
+ if (!this.swallowClick) return;
1780
+ this.swallowClick = false;
1781
+ ev.stopPropagation();
1782
+ ev.preventDefault();
1783
+ };
1784
+ destroy() {
1785
+ this.ctx.opts.stripSlotWillUnmount?.({ el: this.slot });
1786
+ this.el.remove();
1787
+ }
1788
+ };
1789
+
1446
1790
  // src/dom/views/timegrid.ts
1447
1791
  var BAR_GAP2 = 2;
1448
1792
  var TimeGridView = class {
@@ -2579,6 +2923,7 @@ ${this.ctx.text.more(items.length - 8)}` : ""}` : label;
2579
2923
 
2580
2924
  // src/dom/calendar.ts
2581
2925
  var EXTERNAL_ID = "__tnc_external__";
2926
+ var MAX_AGENDA_DAYS = 366;
2582
2927
  function sameOption(a, b, depth = 0) {
2583
2928
  if (a === b) return true;
2584
2929
  if (typeof a === "function" && typeof b === "function") return true;
@@ -2647,10 +2992,15 @@ var Calendar = class {
2647
2992
  slot = { min: 0, max: MINUTES_PER_DAY, duration: 30, snap: 30, scroll: 480 };
2648
2993
  draggingId = null;
2649
2994
  dragRow = null;
2995
+ strip = null;
2996
+ stripSize = null;
2997
+ rebuilding = false;
2650
2998
  toolbarEl;
2651
2999
  viewHost;
2652
3000
  current = null;
2653
3001
  anchor = 0;
3002
+ /** End of an agenda list grown past its `listDayCount`. */
3003
+ listEnd = null;
2654
3004
  session = null;
2655
3005
  selection = null;
2656
3006
  suppressClick = false;
@@ -2843,6 +3193,10 @@ var Calendar = class {
2843
3193
  if (!p) return;
2844
3194
  const day = p.kind === "day" ? p.day : dayOf(p.ms, this.zone);
2845
3195
  const ms = p.kind === "day" ? toInstant(p.day, 0, this.zone) : p.ms;
3196
+ if (this.agendaList()) {
3197
+ this.showListDay(day);
3198
+ return;
3199
+ }
2846
3200
  const { startDay, endDay } = this.view.range;
2847
3201
  if (day < startDay || day >= endDay) {
2848
3202
  this.anchor = day;
@@ -2854,7 +3208,7 @@ var Calendar = class {
2854
3208
  }
2855
3209
  /** What is under a client point: a time slot, a day, or a timeline row and date. Null outside the view. */
2856
3210
  hitTest(clientX, clientY) {
2857
- return this.current?.hit(clientX, clientY) ?? null;
3211
+ return this.hitAt(clientX, clientY);
2858
3212
  }
2859
3213
  /** Item elements currently rendered for an id. */
2860
3214
  elementsFor(id) {
@@ -2896,6 +3250,9 @@ var Calendar = class {
2896
3250
  this.unbindEvents();
2897
3251
  this.current?.destroy();
2898
3252
  this.current = null;
3253
+ this.stripSize?.disconnect();
3254
+ this.strip?.destroy();
3255
+ this.strip = null;
2899
3256
  this.root.remove();
2900
3257
  }
2901
3258
  /* ========================================================== view context */
@@ -2926,7 +3283,8 @@ var Calendar = class {
2926
3283
  return out;
2927
3284
  }
2928
3285
  isEditable(n, what) {
2929
- if (n.id === EXTERNAL_ID || this.opts.view === "list" || this.opts.view === "year") return false;
3286
+ if (n.id === EXTERNAL_ID || this.opts.view === "year") return false;
3287
+ if (this.opts.view === "list" && (!this.opts.listAgenda || what === "duration")) return false;
2930
3288
  const it = n.item;
2931
3289
  const base = it.editable ?? this.opts.editable;
2932
3290
  const specific = what === "start" ? it.startEditable : it.durationEditable;
@@ -3123,7 +3481,8 @@ var Calendar = class {
3123
3481
  }
3124
3482
  case "list": {
3125
3483
  const s = this.opts.listStart === "week" ? startOfWeek(a, this.firstDay) : a;
3126
- return { startDay: s, endDay: s + Math.max(1, listDayCount) };
3484
+ const end = s + Math.max(1, listDayCount);
3485
+ return { startDay: s, endDay: this.opts.listAgenda && this.listEnd !== null ? Math.max(end, this.listEnd) : end };
3127
3486
  }
3128
3487
  case "month": {
3129
3488
  const s = startOfWeek(startOfMonth(a), this.firstDay);
@@ -3161,6 +3520,33 @@ var Calendar = class {
3161
3520
  const duration = Math.max(1, parseDurationMinutes(this.opts.slotDuration, 30));
3162
3521
  const snap = Math.max(1, parseDurationMinutes(this.opts.snapDuration ?? null, duration));
3163
3522
  this.slot = { min, max, duration, snap, scroll: parseDurationMinutes(this.opts.scrollTime, 480) };
3523
+ if (navigated) this.listEnd = null;
3524
+ const prev = this.view;
3525
+ this.applyRange();
3526
+ this.conflicts = dependencyConflicts(this.store);
3527
+ this.root.className = "tnc";
3528
+ this.root.classList.add(`tnc--${this.opts.view}`, `tnc--height-${this.opts.height}`);
3529
+ if (!this.opts.timeGrid) this.root.classList.add("tnc--no-time-grid");
3530
+ this.renderToolbar();
3531
+ this.rebuilding = true;
3532
+ this.syncStrip(navigated);
3533
+ this.rebuilding = false;
3534
+ this.view.dataRange = this.dataRangeFor(this.view.range);
3535
+ const rangeChanged = rangeMoved(prev, this.view);
3536
+ this.current?.destroy();
3537
+ this.current = this.createView();
3538
+ this.viewHost.replaceChildren(this.current.el);
3539
+ this.current.render(null);
3540
+ if (keep && this.current instanceof TimelineView) {
3541
+ this.current.setScroll({ top: keep.top, left: 0 });
3542
+ this.current.placeInstant(keep.ms, keep.vx);
3543
+ } else if (scroll) this.current.setScroll(scroll);
3544
+ else if (this.current instanceof TimeGridView) this.current.scrollToTime(this.slot.scroll);
3545
+ else if (this.current instanceof TimelineView) this.current.scrollToDay(this.anchor);
3546
+ if (rangeChanged) this.opts.onRangeChange?.(this.view);
3547
+ }
3548
+ /** Sets the visible days and view info from the anchor. */
3549
+ applyRange() {
3164
3550
  const { startDay, endDay } = this.computeRange();
3165
3551
  const hidden = new Set(this.opts.hiddenDays);
3166
3552
  if (!this.opts.weekends) {
@@ -3177,26 +3563,100 @@ var Calendar = class {
3177
3563
  start: new Date(toInstant(startDay, 0, this.zone)),
3178
3564
  end: new Date(toInstant(endDay, 0, this.zone))
3179
3565
  };
3180
- const prevView = this.view;
3181
- this.view = { type: this.opts.view, range, anchor: this.anchor, title: "" };
3566
+ this.view = { type: this.opts.view, range, anchor: this.anchor, title: "", dataRange: this.dataRangeFor(range) };
3182
3567
  this.view.title = this.computeTitle(startDay, endDay);
3183
- this.conflicts = dependencyConflicts(this.store);
3184
- this.root.className = "tnc";
3185
- this.root.classList.add(`tnc--${this.opts.view}`, `tnc--height-${this.opts.height}`);
3186
- if (!this.opts.timeGrid) this.root.classList.add("tnc--no-time-grid");
3187
- this.renderToolbar();
3188
- this.current?.destroy();
3189
- this.current = this.createView();
3190
- this.viewHost.replaceChildren(this.current.el);
3191
- this.current.render(null);
3192
- if (keep && this.current instanceof TimelineView) {
3193
- this.current.setScroll({ top: keep.top, left: 0 });
3194
- this.current.placeInstant(keep.ms, keep.vx);
3195
- } else if (scroll) this.current.setScroll(scroll);
3196
- else if (this.current instanceof TimeGridView) this.current.scrollToTime(this.slot.scroll);
3197
- else if (this.current instanceof TimelineView) this.current.scrollToDay(this.anchor);
3198
- const rangeChanged = !prevView || prevView.type !== this.view.type || prevView.range.startDay !== startDay || prevView.range.endDay !== endDay;
3199
- if (rangeChanged) this.opts.onRangeChange?.(this.view);
3568
+ }
3569
+ /** The range, run on to the end of the strip's days so their dots are right. */
3570
+ dataRangeFor(range) {
3571
+ const end = this.strip ? Math.max(range.endDay, this.strip.window().end) : range.endDay;
3572
+ if (end === range.endDay) return range;
3573
+ return { ...range, endDay: end, end: new Date(toInstant(end, 0, this.zone)) };
3574
+ }
3575
+ /** Adds, updates or removes the date strip for the current view. */
3576
+ syncStrip(navigated) {
3577
+ const { view, listAgenda, dateStrip } = this.opts;
3578
+ const list = view === "list";
3579
+ const want = list ? listAgenda : dateStrip && (view === "day" || view === "days" || view === "week");
3580
+ if (!want) {
3581
+ if (!this.strip) return;
3582
+ this.stripSize?.disconnect();
3583
+ this.stripSize = null;
3584
+ this.strip.destroy();
3585
+ this.strip = null;
3586
+ this.root.style.removeProperty("--tnc-strip-h");
3587
+ return;
3588
+ }
3589
+ this.root.classList.add("tnc--has-strip");
3590
+ const { startDay, endDay } = this.view.range;
3591
+ if (!this.strip) {
3592
+ const strip = new DateStrip(this, (day, ev) => this.pickStripDay(day, ev), startDay, () => this.stripShown());
3593
+ this.strip = strip;
3594
+ this.root.insertBefore(strip.el, this.viewHost);
3595
+ const RO = this.win.ResizeObserver;
3596
+ if (RO) {
3597
+ this.stripSize = new RO(() => strip.measure());
3598
+ this.stripSize.observe(strip.el);
3599
+ }
3600
+ }
3601
+ if (!list) this.strip.setFocus(startDay, { start: startDay, end: endDay });
3602
+ else if (navigated) this.strip.setFocus(startDay, null);
3603
+ this.strip.draw();
3604
+ }
3605
+ pickStripDay(day, ev) {
3606
+ this.dateClick(day, ev);
3607
+ if (this.agendaList()) {
3608
+ this.showListDay(day);
3609
+ return;
3610
+ }
3611
+ const { startDay, endDay } = this.view.range;
3612
+ if (this.opts.view === "week" && day >= startDay && day < endDay) return;
3613
+ this.anchor = day;
3614
+ this.rebuild(true);
3615
+ }
3616
+ /** The strip moved to other days: load theirs too. */
3617
+ stripShown() {
3618
+ if (!this.strip || this.rebuilding || !this.view) return;
3619
+ const next = this.dataRangeFor(this.view.range);
3620
+ if (next.endDay === this.view.dataRange.endDay) return;
3621
+ this.view = { ...this.view, dataRange: next };
3622
+ this.opts.onRangeChange?.(this.view);
3623
+ }
3624
+ /** What is under a point, the date strip included. */
3625
+ hitAt(x, y) {
3626
+ const strip = this.strip;
3627
+ if (strip) {
3628
+ const el = this.doc.elementFromPoint(x, y);
3629
+ const day = el ? strip.dayAt(el) : null;
3630
+ strip.markTarget(day !== null && this.draggingId ? day : null);
3631
+ if (day !== null) return { kind: "day", day };
3632
+ }
3633
+ return this.current?.hit(x, y) ?? null;
3634
+ }
3635
+ agendaList() {
3636
+ return this.opts.view === "list" && this.opts.listAgenda && this.current instanceof ListView ? this.current : null;
3637
+ }
3638
+ extendList(endDay) {
3639
+ if (!this.agendaList()) return;
3640
+ const { startDay, endDay: current } = this.view.range;
3641
+ const end = Math.min(endDay, startDay + MAX_AGENDA_DAYS);
3642
+ if (end <= current) return;
3643
+ this.listEnd = end;
3644
+ this.applyRange();
3645
+ this.current?.render(null);
3646
+ this.strip?.draw();
3647
+ this.opts.onRangeChange?.(this.view);
3648
+ }
3649
+ showListDay(day) {
3650
+ if (!this.agendaList()) return;
3651
+ if (day < this.view.range.startDay || day - this.view.range.startDay >= MAX_AGENDA_DAYS) {
3652
+ this.anchor = day;
3653
+ this.rebuild(true);
3654
+ }
3655
+ this.extendList(day + 2 * Math.max(7, this.opts.listDayCount));
3656
+ this.agendaList()?.scrollToDay(day);
3657
+ }
3658
+ dateClick(day, jsEvent) {
3659
+ this.clickSlot({ kind: "day", day }, jsEvent, true);
3200
3660
  }
3201
3661
  createView() {
3202
3662
  switch (this.opts.view) {
@@ -3499,8 +3959,9 @@ var Calendar = class {
3499
3959
  };
3500
3960
  this.session = s;
3501
3961
  if (touch && itemEl?.classList.contains("is-draggable")) {
3502
- const el = itemEl.closest(".tnc-tg-scroll, .tnc-dg-scroll, .tnc-tl-scroll, .tnc-list-scroll, .tnc-year-scroll");
3503
- if (el) s.pan = { el, left: el.scrollLeft, top: el.scrollTop, active: false };
3962
+ const x = scrollerOf(itemEl, "x");
3963
+ const y = scrollerOf(itemEl, "y");
3964
+ s.pan = { x, y, left: x?.scrollLeft ?? 0, top: y?.scrollTop ?? 0, active: false };
3504
3965
  }
3505
3966
  if (touch) {
3506
3967
  s.timer = this.win.setTimeout(() => {
@@ -3541,8 +4002,8 @@ var Calendar = class {
3541
4002
  if (!pan.active && s.timer !== null) this.win.clearTimeout(s.timer);
3542
4003
  s.timer = null;
3543
4004
  pan.active = true;
3544
- pan.el.scrollLeft = pan.left - (ev.clientX - s.x0);
3545
- pan.el.scrollTop = pan.top - (ev.clientY - s.y0);
4005
+ if (pan.x) pan.x.scrollLeft = pan.left - (ev.clientX - s.x0);
4006
+ if (pan.y) pan.y.scrollTop = pan.top - (ev.clientY - s.y0);
3546
4007
  } else if (dist > 8) this.endSession(null, true);
3547
4008
  return;
3548
4009
  }
@@ -3580,7 +4041,7 @@ var Calendar = class {
3580
4041
  };
3581
4042
  /** Proposal for the current pointer position. */
3582
4043
  track(s, ev) {
3583
- const hit = this.current?.hit(ev.clientX, ev.clientY) ?? null;
4044
+ const hit = this.hitAt(ev.clientX, ev.clientY);
3584
4045
  if (s.mode === "external") {
3585
4046
  this.trackExternal(s, hit);
3586
4047
  return;
@@ -3706,7 +4167,7 @@ var Calendar = class {
3706
4167
  return this.dayProposal(hit.day, hit.day + this.opts.defaultAllDayDays);
3707
4168
  }
3708
4169
  if (hit.kind === "day") {
3709
- const dd = hit.day - (grab.kind === "day" ? grab.day : hit.day);
4170
+ const dd = hit.day - (grab.kind === "timeline" ? hit.day : grab.day);
3710
4171
  if (o.allDay) return this.dayProposal(o.startDay + dd, o.startDay + dd + days);
3711
4172
  const startMs2 = this.civilToMs(this.civil(o.startMs) + dd * MINUTES_PER_DAY);
3712
4173
  const endMs = this.civilToMs(this.civil(o.endMs) + dd * MINUTES_PER_DAY);
@@ -3841,6 +4302,7 @@ var Calendar = class {
3841
4302
  this.draggingId = null;
3842
4303
  this.dragRow = null;
3843
4304
  if (draggedId) for (const el of this.elementsFor(draggedId)) el.classList.remove("is-dragging");
4305
+ this.strip?.markTarget(null);
3844
4306
  if (!s.active) return;
3845
4307
  if (s.mode === "select") {
3846
4308
  if (cancel) this.unselectSilently();
@@ -4127,6 +4589,19 @@ var Calendar = class {
4127
4589
  });
4128
4590
  };
4129
4591
  };
4592
+ function scrollerOf(el, axis) {
4593
+ const doc = el.ownerDocument;
4594
+ const win = doc.defaultView;
4595
+ for (let p = el.parentElement; p && p !== doc.body && p !== doc.documentElement; p = p.parentElement) {
4596
+ const overflow = win?.getComputedStyle(p)[axis === "x" ? "overflowX" : "overflowY"] ?? "";
4597
+ const room = axis === "x" ? p.scrollWidth > p.clientWidth : p.scrollHeight > p.clientHeight;
4598
+ if (room && /auto|scroll|overlay/.test(overflow)) return p;
4599
+ }
4600
+ return doc.scrollingElement;
4601
+ }
4602
+ function rangeMoved(prev, next) {
4603
+ return !prev || prev.type !== next.type || prev.range.startDay !== next.range.startDay || prev.range.endDay !== next.range.endDay || prev.dataRange.endDay !== next.dataRange.endDay;
4604
+ }
4130
4605
 
4131
4606
  // src/tasknotes/bases.ts
4132
4607
  var FROM_BASES = {