@tasknotes/calendar 0.1.3 → 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.
@@ -949,6 +949,9 @@ function defaultOptions() {
949
949
  listDayCount: 7,
950
950
  listStart: "date",
951
951
  listAgenda: false,
952
+ dateStrip: false,
953
+ listLeadTitle: null,
954
+ listAddText: null,
952
955
  monthDisplay: "full",
953
956
  timeZone: "local",
954
957
  locale: void 0,
@@ -999,6 +1002,9 @@ var LAYOUT_KEYS = /* @__PURE__ */ new Set([
999
1002
  "listDayCount",
1000
1003
  "listStart",
1001
1004
  "listAgenda",
1005
+ "dateStrip",
1006
+ "listLeadTitle",
1007
+ "listAddText",
1002
1008
  "monthDisplay",
1003
1009
  "timeZone",
1004
1010
  "locale",
@@ -1340,12 +1346,244 @@ var DayGridView = class {
1340
1346
  }
1341
1347
  };
1342
1348
 
1349
+ // src/dom/views/list.ts
1350
+ var LOAD_AHEAD = "0px 0px 600px 0px";
1351
+ var ListView = class {
1352
+ constructor(ctx) {
1353
+ this.ctx = ctx;
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 });
1358
+ this.scroller.append(this.body);
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 });
1378
+ }
1379
+ ctx;
1380
+ el;
1381
+ scroller;
1382
+ body;
1383
+ agenda;
1384
+ pools = /* @__PURE__ */ new Map();
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;
1392
+ itemsOn(day) {
1393
+ const { store, opts } = this.ctx;
1394
+ const out = store.spanningIn(day, day + 1, true);
1395
+ const seen = new Set(out.map((n) => n.id));
1396
+ for (const n of store.timedOn(day)) if (!seen.has(n.id)) out.push(n);
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));
1398
+ }
1399
+ render(_change) {
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();
1405
+ const keep = /* @__PURE__ */ new Set();
1406
+ let prev = null;
1407
+ for (const day of days) {
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;
1420
+ keep.add(day);
1421
+ let section = this.sections.get(day);
1422
+ let pool = this.pools.get(day);
1423
+ if (!section || !pool) {
1424
+ const arg = ctx.dayArg(day);
1425
+ const heading = h(doc, "div", ["tnc-list-day", ...ctx.dayClasses(day)].join(" "), { "data-date": arg.dateStr });
1426
+ const custom = opts.dayHeaderContent?.(arg);
1427
+ const label = custom ?? ctx.fmt.longDate(day);
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);
1434
+ this.sections.set(day, section);
1435
+ this.pools.set(day, pool);
1436
+ }
1437
+ section.classList.toggle("is-empty", !items.length);
1438
+ this.fill(pool, items, () => day);
1439
+ if (prev ? prev.nextSibling !== section : this.body.firstChild !== section) {
1440
+ if (prev) prev.after(section);
1441
+ else this.body.prepend(section);
1442
+ }
1443
+ prev = section;
1444
+ }
1445
+ for (const [day, section] of this.sections) {
1446
+ if (keep.has(day)) continue;
1447
+ this.pools.get(day)?.clear();
1448
+ this.pools.delete(day);
1449
+ section.remove();
1450
+ this.sections.delete(day);
1451
+ }
1452
+ this.renderLead(leadItems);
1453
+ let empty = this.body.querySelector(":scope > .tnc-list-empty");
1454
+ if (!keep.size && !leadItems.length) {
1455
+ if (!empty) {
1456
+ empty = h(doc, "div", "tnc-list-empty", { role: "listitem" }, ctx.text.noItems);
1457
+ this.body.append(empty);
1458
+ }
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
+ }
1543
+ }
1544
+ showSelection(_sel) {
1545
+ }
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 };
1557
+ }
1558
+ getScroll() {
1559
+ return { top: this.scroller.scrollTop, left: 0 };
1560
+ }
1561
+ setScroll(s) {
1562
+ this.scroller.scrollTop = s.top;
1563
+ }
1564
+ elementsFor(id) {
1565
+ const pools = [...this.pools.values(), ...this.lead ? [this.lead.pool] : []];
1566
+ return pools.flatMap((p) => p.elementsFor(id));
1567
+ }
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();
1575
+ for (const p of this.pools.values()) p.clear();
1576
+ this.el.remove();
1577
+ }
1578
+ };
1579
+
1343
1580
  // src/dom/views/strip.ts
1344
1581
  var SWIPE_PX = 40;
1345
1582
  var DateStrip = class {
1346
- constructor(ctx, pick, focus) {
1583
+ constructor(ctx, pick, focus, onShown) {
1347
1584
  this.ctx = ctx;
1348
1585
  this.pick = pick;
1586
+ this.onShown = onShown;
1349
1587
  const { doc, text } = ctx;
1350
1588
  this.focus = focus;
1351
1589
  this.shown = startOfWeek(focus, ctx.firstDay);
@@ -1357,8 +1595,10 @@ var DateStrip = class {
1357
1595
  const next = h(doc, "button", "tnc-strip-nav tnc-strip-next", { type: "button", "aria-label": text.next, title: text.next }, "\u203A");
1358
1596
  prev.addEventListener("click", () => this.step(-1));
1359
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));
1360
1600
  this.slot = h(doc, "div", "tnc-strip-slot", { "data-tnc-no-drag": "" });
1361
- head.append(this.title, prev, next, this.slot);
1601
+ head.append(this.title, this.todayBtn, prev, next, this.slot);
1362
1602
  this.dows = h(doc, "div", "tnc-strip-dows", { "aria-hidden": "true" });
1363
1603
  this.grid = h(doc, "div", "tnc-strip-grid", { role: "group" });
1364
1604
  const handle = h(doc, "button", "tnc-strip-handle", { type: "button", tabindex: "-1", "aria-hidden": "true" });
@@ -1375,27 +1615,45 @@ var DateStrip = class {
1375
1615
  }
1376
1616
  ctx;
1377
1617
  pick;
1618
+ onShown;
1378
1619
  el;
1379
1620
  title;
1380
1621
  dows;
1381
1622
  grid;
1382
1623
  slot;
1624
+ todayBtn;
1383
1625
  /** Day at the top of the list. */
1384
1626
  focus;
1385
1627
  /** First day of the week shown, or the month shown when expanded. */
1386
1628
  shown;
1387
1629
  target = null;
1630
+ /** Days tinted as on screen besides the focus (a time grid's range). */
1631
+ range = null;
1632
+ shownKey = "";
1388
1633
  swipe = null;
1389
1634
  swallowClick = false;
1390
1635
  expanded = false;
1391
- /** The list scrolled: mark its top day and keep it in view. */
1392
- setFocus(day) {
1393
- if (day === this.focus) return;
1394
- this.focus = day;
1395
- if (!this.expanded) this.shown = startOfWeek(day, this.ctx.firstDay);
1396
- else if (startOfMonth(day) !== this.shown) this.shown = startOfMonth(day);
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
+ }
1397
1646
  this.draw();
1398
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
+ }
1399
1657
  setExpanded(on) {
1400
1658
  if (on === this.expanded) return;
1401
1659
  this.expanded = on;
@@ -1433,6 +1691,7 @@ var DateStrip = class {
1433
1691
  this.title.replaceChildren(fmt.monthYear(titleDay), h(doc, "span", "tnc-strip-caret", { "aria-hidden": "true" }));
1434
1692
  this.title.setAttribute("aria-expanded", String(this.expanded));
1435
1693
  this.el.classList.toggle("is-expanded", this.expanded);
1694
+ this.todayBtn.hidden = this.focus === ctx.today;
1436
1695
  this.dows.replaceChildren(...days.slice(0, 7).map((d) => h(doc, "span", null, null, fmt.weekdayNarrow(d))));
1437
1696
  const focused = this.grid.contains(doc.activeElement) ? doc.activeElement.dataset.date : void 0;
1438
1697
  const month = ymdOfDay(this.shown).month;
@@ -1449,6 +1708,7 @@ var DateStrip = class {
1449
1708
  "tnc-strip-day",
1450
1709
  ...ctx.dayClasses(d),
1451
1710
  d === this.focus ? "is-focus" : "",
1711
+ d !== this.focus && this.range && d >= this.range.start && d < this.range.end ? "is-in-range" : "",
1452
1712
  count ? "has-items" : "",
1453
1713
  this.expanded && ymdOfDay(d).month !== month ? "is-other-month" : "",
1454
1714
  d === this.target ? "is-drop-target" : ""
@@ -1468,6 +1728,11 @@ var DateStrip = class {
1468
1728
  this.grid.replaceChildren(...cells);
1469
1729
  this.grid.setAttribute("aria-label", fmt.monthYear(titleDay));
1470
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
+ }
1471
1736
  }
1472
1737
  onClick = (ev) => {
1473
1738
  const day = this.dayAt(ev.target);
@@ -1522,205 +1787,6 @@ var DateStrip = class {
1522
1787
  }
1523
1788
  };
1524
1789
 
1525
- // src/dom/views/list.ts
1526
- var LOAD_AHEAD = "0px 0px 600px 0px";
1527
- var ListView = class {
1528
- constructor(ctx) {
1529
- this.ctx = ctx;
1530
- const { doc, win, opts } = ctx;
1531
- this.el = h(doc, "div", "tnc-list");
1532
- this.scroller = h(doc, "div", "tnc-list-scroll");
1533
- this.body = h(doc, "div", "tnc-list-body", { role: "list", "aria-label": ctx.view.title });
1534
- this.scroller.append(this.body);
1535
- this.el.append(this.scroller);
1536
- if (!opts.listAgenda) return;
1537
- this.el.classList.add("tnc-list--agenda");
1538
- this.strip = new DateStrip(ctx, (day, ev) => this.pick(day, ev), ctx.view.range.startDay);
1539
- this.el.prepend(this.strip.el);
1540
- this.sentinel = h(doc, "div", "tnc-list-more", { "aria-hidden": "true" });
1541
- this.scroller.append(this.sentinel);
1542
- const globals = win;
1543
- const IO = globals.IntersectionObserver;
1544
- if (IO) {
1545
- this.loader = new IO(
1546
- (entries) => {
1547
- if (entries.some((e) => e.isIntersecting)) ctx.extendList(ctx.view.range.endDay + Math.max(7, opts.listDayCount));
1548
- },
1549
- // Auto height scrolls the page, so watch the viewport; fixed height scrolls the list.
1550
- { root: opts.height === "auto" ? null : this.scroller, rootMargin: LOAD_AHEAD }
1551
- );
1552
- }
1553
- const RO = globals.ResizeObserver;
1554
- if (RO) {
1555
- const strip = this.strip.el;
1556
- this.stripSize = new RO(() => this.el.style.setProperty("--tnc-strip-h", `${strip.offsetHeight}px`));
1557
- this.stripSize.observe(strip);
1558
- }
1559
- win.addEventListener("scroll", this.onScroll, { capture: true, passive: true });
1560
- this.scroller.addEventListener("wheel", this.fold, { passive: true });
1561
- this.scroller.addEventListener("touchmove", this.fold, { passive: true });
1562
- }
1563
- ctx;
1564
- el;
1565
- scroller;
1566
- body;
1567
- pools = /* @__PURE__ */ new Map();
1568
- sections = /* @__PURE__ */ new Map();
1569
- strip = null;
1570
- sentinel = null;
1571
- loader = null;
1572
- stripSize = null;
1573
- frame = null;
1574
- /** A picked day stays marked until the list is scrolled by hand. */
1575
- pinned = false;
1576
- itemsOn(day) {
1577
- const { store, opts } = this.ctx;
1578
- const out = store.spanningIn(day, day + 1, true);
1579
- const seen = new Set(out.map((n) => n.id));
1580
- for (const n of store.timedOn(day)) if (!seen.has(n.id)) out.push(n);
1581
- 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));
1582
- }
1583
- render(_change) {
1584
- const { ctx } = this;
1585
- const { doc, days } = ctx;
1586
- const agenda = !!this.strip;
1587
- const keep = /* @__PURE__ */ new Set();
1588
- let prev = null;
1589
- for (const day of days) {
1590
- const items = this.itemsOn(day);
1591
- if (!items.length && !agenda) continue;
1592
- keep.add(day);
1593
- let section = this.sections.get(day);
1594
- let pool = this.pools.get(day);
1595
- if (!section || !pool) {
1596
- const arg = ctx.dayArg(day);
1597
- const heading = h(doc, "div", ["tnc-list-day", ...ctx.dayClasses(day)].join(" "), { "data-date": arg.dateStr });
1598
- const custom = ctx.opts.dayHeaderContent?.(arg);
1599
- const label = custom ?? ctx.fmt.longDate(day);
1600
- heading.append(ctx.opts.navLinks ? ctx.navLink(day, label, "tnc-list-day-link") : label);
1601
- const rows2 = h(doc, "div", "tnc-list-rows", { role: "group" });
1602
- section = h(doc, "div", "tnc-list-section", { role: "listitem", "aria-label": ctx.fmt.longDate(day), "data-date": arg.dateStr }, heading, rows2);
1603
- ctx.opts.dayHeaderDidMount?.({ ...arg, el: heading });
1604
- pool = new ItemPool(ctx, rows2);
1605
- this.sections.set(day, section);
1606
- this.pools.set(day, pool);
1607
- }
1608
- section.classList.toggle("is-empty", !items.length);
1609
- pool.begin();
1610
- for (const n of items) {
1611
- pool.place(n.id, {
1612
- n,
1613
- day,
1614
- isStart: n.startDay === day,
1615
- isEnd: n.endDay === day + 1,
1616
- variant: "list",
1617
- mirror: false
1618
- });
1619
- }
1620
- pool.end();
1621
- const rows = pool.container;
1622
- for (const n of items) {
1623
- const el = pool.elementsFor(n.id)[0];
1624
- if (el) rows.append(el);
1625
- }
1626
- if (prev ? prev.nextSibling !== section : this.body.firstChild !== section) {
1627
- if (prev) prev.after(section);
1628
- else this.body.prepend(section);
1629
- }
1630
- prev = section;
1631
- }
1632
- for (const [day, section] of this.sections) {
1633
- if (keep.has(day)) continue;
1634
- this.pools.get(day)?.clear();
1635
- this.pools.delete(day);
1636
- section.remove();
1637
- this.sections.delete(day);
1638
- }
1639
- let empty = this.body.querySelector(":scope > .tnc-list-empty");
1640
- if (!keep.size) {
1641
- if (!empty) {
1642
- empty = h(doc, "div", "tnc-list-empty", { role: "listitem" }, ctx.text.noItems);
1643
- this.body.append(empty);
1644
- }
1645
- } else empty?.remove();
1646
- this.strip?.draw();
1647
- if (this.loader && this.sentinel) {
1648
- this.loader.unobserve(this.sentinel);
1649
- this.loader.observe(this.sentinel);
1650
- }
1651
- }
1652
- /** A strip day: the host hears of it, and the list brings it to the top. */
1653
- pick(day, ev) {
1654
- this.ctx.dateClick(day, ev);
1655
- this.ctx.showListDay(day);
1656
- }
1657
- /** Brings a day's section to the top, under the strip. */
1658
- scrollToDay(day) {
1659
- const section = this.sections.get(day);
1660
- if (!section) return;
1661
- if (this.strip) this.el.style.setProperty("--tnc-strip-h", `${this.strip.el.offsetHeight}px`);
1662
- section.scrollIntoView({ block: "start" });
1663
- this.strip?.setFocus(day);
1664
- this.pinned = true;
1665
- }
1666
- fold = () => {
1667
- this.pinned = false;
1668
- if (this.strip?.expanded) this.strip.setExpanded(false);
1669
- };
1670
- onScroll = () => {
1671
- if (this.frame !== null) return;
1672
- this.frame = this.ctx.win.requestAnimationFrame(() => {
1673
- this.frame = null;
1674
- this.followTop();
1675
- });
1676
- };
1677
- /** Marks the first day not hidden under the strip. */
1678
- followTop() {
1679
- if (!this.strip || this.pinned) return;
1680
- const edge = this.strip.el.getBoundingClientRect().bottom + 1;
1681
- for (const day of this.ctx.days) {
1682
- const section = this.sections.get(day);
1683
- if (section && section.getBoundingClientRect().bottom > edge) {
1684
- this.strip.setFocus(day);
1685
- return;
1686
- }
1687
- }
1688
- }
1689
- showSelection(_sel) {
1690
- }
1691
- hit(x, y) {
1692
- const el = this.ctx.doc.elementFromPoint(x, y);
1693
- const stripDay = el && this.strip ? this.strip.dayAt(el) : null;
1694
- this.strip?.markTarget(stripDay !== null && this.ctx.draggingId ? stripDay : null);
1695
- if (stripDay !== null) return { kind: "day", day: stripDay };
1696
- const section = el?.closest(".tnc-list-section");
1697
- const day = section && this.body.contains(section) ? parseDay(section.dataset.date ?? "") : null;
1698
- return day === null ? null : { kind: "day", day };
1699
- }
1700
- dragEnded() {
1701
- this.strip?.markTarget(null);
1702
- }
1703
- getScroll() {
1704
- return { top: this.scroller.scrollTop, left: 0 };
1705
- }
1706
- setScroll(s) {
1707
- this.scroller.scrollTop = s.top;
1708
- }
1709
- elementsFor(id) {
1710
- return [...this.pools.values()].flatMap((p) => p.elementsFor(id));
1711
- }
1712
- destroy() {
1713
- const { win } = this.ctx;
1714
- win.removeEventListener("scroll", this.onScroll, { capture: true });
1715
- if (this.frame !== null) win.cancelAnimationFrame(this.frame);
1716
- this.loader?.disconnect();
1717
- this.stripSize?.disconnect();
1718
- this.strip?.destroy();
1719
- for (const p of this.pools.values()) p.clear();
1720
- this.el.remove();
1721
- }
1722
- };
1723
-
1724
1790
  // src/dom/views/timegrid.ts
1725
1791
  var BAR_GAP2 = 2;
1726
1792
  var TimeGridView = class {
@@ -2926,6 +2992,9 @@ var Calendar = class {
2926
2992
  slot = { min: 0, max: MINUTES_PER_DAY, duration: 30, snap: 30, scroll: 480 };
2927
2993
  draggingId = null;
2928
2994
  dragRow = null;
2995
+ strip = null;
2996
+ stripSize = null;
2997
+ rebuilding = false;
2929
2998
  toolbarEl;
2930
2999
  viewHost;
2931
3000
  current = null;
@@ -3139,7 +3208,7 @@ var Calendar = class {
3139
3208
  }
3140
3209
  /** What is under a client point: a time slot, a day, or a timeline row and date. Null outside the view. */
3141
3210
  hitTest(clientX, clientY) {
3142
- return this.current?.hit(clientX, clientY) ?? null;
3211
+ return this.hitAt(clientX, clientY);
3143
3212
  }
3144
3213
  /** Item elements currently rendered for an id. */
3145
3214
  elementsFor(id) {
@@ -3181,6 +3250,9 @@ var Calendar = class {
3181
3250
  this.unbindEvents();
3182
3251
  this.current?.destroy();
3183
3252
  this.current = null;
3253
+ this.stripSize?.disconnect();
3254
+ this.strip?.destroy();
3255
+ this.strip = null;
3184
3256
  this.root.remove();
3185
3257
  }
3186
3258
  /* ========================================================== view context */
@@ -3449,12 +3521,18 @@ var Calendar = class {
3449
3521
  const snap = Math.max(1, parseDurationMinutes(this.opts.snapDuration ?? null, duration));
3450
3522
  this.slot = { min, max, duration, snap, scroll: parseDurationMinutes(this.opts.scrollTime, 480) };
3451
3523
  if (navigated) this.listEnd = null;
3452
- const rangeChanged = this.applyRange();
3524
+ const prev = this.view;
3525
+ this.applyRange();
3453
3526
  this.conflicts = dependencyConflicts(this.store);
3454
3527
  this.root.className = "tnc";
3455
3528
  this.root.classList.add(`tnc--${this.opts.view}`, `tnc--height-${this.opts.height}`);
3456
3529
  if (!this.opts.timeGrid) this.root.classList.add("tnc--no-time-grid");
3457
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);
3458
3536
  this.current?.destroy();
3459
3537
  this.current = this.createView();
3460
3538
  this.viewHost.replaceChildren(this.current.el);
@@ -3467,7 +3545,7 @@ var Calendar = class {
3467
3545
  else if (this.current instanceof TimelineView) this.current.scrollToDay(this.anchor);
3468
3546
  if (rangeChanged) this.opts.onRangeChange?.(this.view);
3469
3547
  }
3470
- /** Sets the visible days and view info from the anchor; true when the range moved. */
3548
+ /** Sets the visible days and view info from the anchor. */
3471
3549
  applyRange() {
3472
3550
  const { startDay, endDay } = this.computeRange();
3473
3551
  const hidden = new Set(this.opts.hiddenDays);
@@ -3485,10 +3563,74 @@ var Calendar = class {
3485
3563
  start: new Date(toInstant(startDay, 0, this.zone)),
3486
3564
  end: new Date(toInstant(endDay, 0, this.zone))
3487
3565
  };
3488
- const prevView = this.view;
3489
- 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) };
3490
3567
  this.view.title = this.computeTitle(startDay, endDay);
3491
- return !prevView || prevView.type !== this.view.type || prevView.range.startDay !== startDay || prevView.range.endDay !== endDay;
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;
3492
3634
  }
3493
3635
  agendaList() {
3494
3636
  return this.opts.view === "list" && this.opts.listAgenda && this.current instanceof ListView ? this.current : null;
@@ -3501,6 +3643,7 @@ var Calendar = class {
3501
3643
  this.listEnd = end;
3502
3644
  this.applyRange();
3503
3645
  this.current?.render(null);
3646
+ this.strip?.draw();
3504
3647
  this.opts.onRangeChange?.(this.view);
3505
3648
  }
3506
3649
  showListDay(day) {
@@ -3898,7 +4041,7 @@ var Calendar = class {
3898
4041
  };
3899
4042
  /** Proposal for the current pointer position. */
3900
4043
  track(s, ev) {
3901
- const hit = this.current?.hit(ev.clientX, ev.clientY) ?? null;
4044
+ const hit = this.hitAt(ev.clientX, ev.clientY);
3902
4045
  if (s.mode === "external") {
3903
4046
  this.trackExternal(s, hit);
3904
4047
  return;
@@ -4024,7 +4167,7 @@ var Calendar = class {
4024
4167
  return this.dayProposal(hit.day, hit.day + this.opts.defaultAllDayDays);
4025
4168
  }
4026
4169
  if (hit.kind === "day") {
4027
- const dd = hit.day - (grab.kind === "day" ? grab.day : hit.day);
4170
+ const dd = hit.day - (grab.kind === "timeline" ? hit.day : grab.day);
4028
4171
  if (o.allDay) return this.dayProposal(o.startDay + dd, o.startDay + dd + days);
4029
4172
  const startMs2 = this.civilToMs(this.civil(o.startMs) + dd * MINUTES_PER_DAY);
4030
4173
  const endMs = this.civilToMs(this.civil(o.endMs) + dd * MINUTES_PER_DAY);
@@ -4159,7 +4302,7 @@ var Calendar = class {
4159
4302
  this.draggingId = null;
4160
4303
  this.dragRow = null;
4161
4304
  if (draggedId) for (const el of this.elementsFor(draggedId)) el.classList.remove("is-dragging");
4162
- this.current?.dragEnded?.();
4305
+ this.strip?.markTarget(null);
4163
4306
  if (!s.active) return;
4164
4307
  if (s.mode === "select") {
4165
4308
  if (cancel) this.unselectSilently();
@@ -4456,6 +4599,9 @@ function scrollerOf(el, axis) {
4456
4599
  }
4457
4600
  return doc.scrollingElement;
4458
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
+ }
4459
4605
 
4460
4606
  // src/tasknotes/bases.ts
4461
4607
  var FROM_BASES = {