@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.
@@ -708,6 +708,10 @@ function defaultOptions() {
708
708
  dayCount: 3,
709
709
  listDayCount: 7,
710
710
  listStart: "date",
711
+ listAgenda: false,
712
+ dateStrip: false,
713
+ listLeadTitle: null,
714
+ listAddText: null,
711
715
  monthDisplay: "full",
712
716
  timeZone: "local",
713
717
  locale: void 0,
@@ -757,6 +761,10 @@ var LAYOUT_KEYS = /* @__PURE__ */ new Set([
757
761
  "dayCount",
758
762
  "listDayCount",
759
763
  "listStart",
764
+ "listAgenda",
765
+ "dateStrip",
766
+ "listLeadTitle",
767
+ "listAddText",
760
768
  "monthDisplay",
761
769
  "timeZone",
762
770
  "locale",
@@ -1247,21 +1255,48 @@ var DayGridView = class {
1247
1255
  };
1248
1256
 
1249
1257
  // src/dom/views/list.ts
1258
+ var LOAD_AHEAD = "0px 0px 600px 0px";
1250
1259
  var ListView = class {
1251
1260
  constructor(ctx) {
1252
1261
  this.ctx = ctx;
1253
- this.el = h(ctx.doc, "div", "tnc-list");
1254
- this.scroller = h(ctx.doc, "div", "tnc-list-scroll");
1255
- this.body = h(ctx.doc, "div", "tnc-list-body", { role: "list", "aria-label": ctx.view.title });
1262
+ const { doc, win, opts } = ctx;
1263
+ this.el = h(doc, "div", "tnc-list");
1264
+ this.scroller = h(doc, "div", "tnc-list-scroll");
1265
+ this.body = h(doc, "div", "tnc-list-body", { role: "list", "aria-label": ctx.view.title });
1256
1266
  this.scroller.append(this.body);
1257
1267
  this.el.append(this.scroller);
1268
+ this.agenda = opts.listAgenda;
1269
+ if (!this.agenda) return;
1270
+ this.el.classList.add("tnc-list--agenda");
1271
+ this.sentinel = h(doc, "div", "tnc-list-more", { "aria-hidden": "true" });
1272
+ this.scroller.append(this.sentinel);
1273
+ const IO = win.IntersectionObserver;
1274
+ if (IO) {
1275
+ this.loader = new IO(
1276
+ (entries) => {
1277
+ if (entries.some((e) => e.isIntersecting)) ctx.extendList(ctx.view.range.endDay + Math.max(7, opts.listDayCount));
1278
+ },
1279
+ // Auto height scrolls the page, so watch the viewport; fixed height scrolls the list.
1280
+ { root: opts.height === "auto" ? null : this.scroller, rootMargin: LOAD_AHEAD }
1281
+ );
1282
+ }
1283
+ win.addEventListener("scroll", this.onScroll, { capture: true, passive: true });
1284
+ this.scroller.addEventListener("wheel", this.fold, { passive: true });
1285
+ this.scroller.addEventListener("touchmove", this.fold, { passive: true });
1258
1286
  }
1259
1287
  ctx;
1260
1288
  el;
1261
1289
  scroller;
1262
1290
  body;
1291
+ agenda;
1263
1292
  pools = /* @__PURE__ */ new Map();
1264
1293
  sections = /* @__PURE__ */ new Map();
1294
+ lead = null;
1295
+ sentinel = null;
1296
+ loader = null;
1297
+ frame = null;
1298
+ /** A picked day stays marked until the list is scrolled by hand. */
1299
+ pinned = false;
1265
1300
  itemsOn(day) {
1266
1301
  const { store, opts } = this.ctx;
1267
1302
  const out = store.spanningIn(day, day + 1, true);
@@ -1270,46 +1305,45 @@ var ListView = class {
1270
1305
  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));
1271
1306
  }
1272
1307
  render(_change) {
1273
- const { ctx } = this;
1274
- const { doc, days } = ctx;
1308
+ const { ctx, agenda } = this;
1309
+ const { doc, days, opts } = ctx;
1310
+ const includes = agenda && opts.listLeadTitle !== null ? opts.listLeadIncludes : void 0;
1311
+ const leadItems = [];
1312
+ const inLead = /* @__PURE__ */ new Set();
1275
1313
  const keep = /* @__PURE__ */ new Set();
1276
1314
  let prev = null;
1277
1315
  for (const day of days) {
1278
- const items = this.itemsOn(day);
1279
- if (!items.length) continue;
1316
+ let items = this.itemsOn(day);
1317
+ if (includes) {
1318
+ items = items.filter((n) => {
1319
+ if (!includes(n.item)) return true;
1320
+ if (!inLead.has(n.id)) {
1321
+ inLead.add(n.id);
1322
+ leadItems.push(n);
1323
+ }
1324
+ return false;
1325
+ });
1326
+ }
1327
+ if (!items.length && !agenda) continue;
1280
1328
  keep.add(day);
1281
1329
  let section = this.sections.get(day);
1282
1330
  let pool = this.pools.get(day);
1283
1331
  if (!section || !pool) {
1284
1332
  const arg = ctx.dayArg(day);
1285
1333
  const heading = h(doc, "div", ["tnc-list-day", ...ctx.dayClasses(day)].join(" "), { "data-date": arg.dateStr });
1286
- const custom = ctx.opts.dayHeaderContent?.(arg);
1334
+ const custom = opts.dayHeaderContent?.(arg);
1287
1335
  const label = custom ?? ctx.fmt.longDate(day);
1288
- heading.append(ctx.opts.navLinks ? ctx.navLink(day, label, "tnc-list-day-link") : label);
1289
- const rows2 = h(doc, "div", "tnc-list-rows", { role: "group" });
1290
- section = h(doc, "div", "tnc-list-section", { role: "listitem", "aria-label": ctx.fmt.longDate(day), "data-date": arg.dateStr }, heading, rows2);
1291
- ctx.opts.dayHeaderDidMount?.({ ...arg, el: heading });
1292
- pool = new ItemPool(ctx, rows2);
1336
+ heading.append(opts.navLinks ? ctx.navLink(day, label, "tnc-list-day-link") : label);
1337
+ const rows = h(doc, "div", "tnc-list-rows", { role: "group" });
1338
+ section = h(doc, "div", "tnc-list-section", { role: "listitem", "aria-label": ctx.fmt.longDate(day), "data-date": arg.dateStr }, heading, rows);
1339
+ if (agenda && opts.listAddText !== null) section.append(this.addRow(day));
1340
+ opts.dayHeaderDidMount?.({ ...arg, el: heading });
1341
+ pool = new ItemPool(ctx, rows);
1293
1342
  this.sections.set(day, section);
1294
1343
  this.pools.set(day, pool);
1295
1344
  }
1296
- pool.begin();
1297
- for (const n of items) {
1298
- pool.place(n.id, {
1299
- n,
1300
- day,
1301
- isStart: n.startDay === day,
1302
- isEnd: n.endDay === day + 1,
1303
- variant: "list",
1304
- mirror: false
1305
- });
1306
- }
1307
- pool.end();
1308
- const rows = pool.container;
1309
- for (const n of items) {
1310
- const el = pool.elementsFor(n.id)[0];
1311
- if (el) rows.append(el);
1312
- }
1345
+ section.classList.toggle("is-empty", !items.length);
1346
+ this.fill(pool, items, () => day);
1313
1347
  if (prev ? prev.nextSibling !== section : this.body.firstChild !== section) {
1314
1348
  if (prev) prev.after(section);
1315
1349
  else this.body.prepend(section);
@@ -1323,18 +1357,111 @@ var ListView = class {
1323
1357
  section.remove();
1324
1358
  this.sections.delete(day);
1325
1359
  }
1360
+ this.renderLead(leadItems);
1326
1361
  let empty = this.body.querySelector(":scope > .tnc-list-empty");
1327
- if (!keep.size) {
1362
+ if (!keep.size && !leadItems.length) {
1328
1363
  if (!empty) {
1329
1364
  empty = h(doc, "div", "tnc-list-empty", { role: "listitem" }, ctx.text.noItems);
1330
1365
  this.body.append(empty);
1331
1366
  }
1332
1367
  } else empty?.remove();
1368
+ this.ctx.strip?.draw();
1369
+ if (this.loader && this.sentinel) {
1370
+ this.loader.unobserve(this.sentinel);
1371
+ this.loader.observe(this.sentinel);
1372
+ }
1373
+ }
1374
+ /** Places items in a pool, keeping DOM order equal to item order. */
1375
+ fill(pool, items, dayFor) {
1376
+ pool.begin();
1377
+ for (const n of items) {
1378
+ const d = dayFor(n);
1379
+ pool.place(n.id, { n, day: d, isStart: n.startDay === d, isEnd: n.endDay === d + 1, variant: "list", mirror: false });
1380
+ }
1381
+ pool.end();
1382
+ for (const n of items) {
1383
+ const el = pool.elementsFor(n.id)[0];
1384
+ if (el) pool.container.append(el);
1385
+ }
1386
+ }
1387
+ renderLead(items) {
1388
+ const { ctx } = this;
1389
+ const { doc, opts } = ctx;
1390
+ if (!items.length || opts.listLeadTitle === null) {
1391
+ if (!this.lead) return;
1392
+ opts.listLeadWillUnmount?.({ el: this.lead.slot });
1393
+ this.lead.pool.clear();
1394
+ this.lead.section.remove();
1395
+ this.lead = null;
1396
+ return;
1397
+ }
1398
+ if (!this.lead) {
1399
+ const title = opts.listLeadTitle;
1400
+ const slot = h(doc, "div", "tnc-list-lead-slot", { "data-tnc-no-drag": "" });
1401
+ const heading = h(doc, "div", "tnc-list-day tnc-list-lead-head", null, h(doc, "span", "tnc-list-lead-title", null, title), slot);
1402
+ const rows = h(doc, "div", "tnc-list-rows", { role: "group" });
1403
+ const section = h(doc, "div", "tnc-list-section tnc-list-lead", { role: "listitem", "aria-label": title }, heading, rows);
1404
+ this.lead = { section, slot, pool: new ItemPool(ctx, rows) };
1405
+ opts.listLeadDidMount?.({ el: slot });
1406
+ }
1407
+ if (this.body.firstChild !== this.lead.section) this.body.prepend(this.lead.section);
1408
+ const first = ctx.days[0] ?? ctx.view.range.startDay;
1409
+ this.fill(this.lead.pool, items, (n) => Math.max(n.startDay, first));
1410
+ }
1411
+ addRow(day) {
1412
+ const { ctx } = this;
1413
+ const label = ctx.opts.listAddText ?? "";
1414
+ const btn = h(ctx.doc, "button", "tnc-list-add", { type: "button", "aria-label": `${label}, ${ctx.fmt.longDate(day)}` }, label);
1415
+ btn.addEventListener("click", (ev) => ctx.opts.onListAdd?.({ ...ctx.dayArg(day), jsEvent: ev }));
1416
+ return btn;
1417
+ }
1418
+ /** Brings a day's section to the top, under the strip; the first day brings the lead section too. */
1419
+ scrollToDay(day) {
1420
+ const section = day === this.ctx.days[0] && this.lead ? this.lead.section : this.sections.get(day);
1421
+ if (!section) return;
1422
+ this.ctx.strip?.measure();
1423
+ section.scrollIntoView({ block: "start" });
1424
+ this.ctx.strip?.setFocus(day);
1425
+ this.pinned = true;
1426
+ }
1427
+ fold = () => {
1428
+ this.pinned = false;
1429
+ const strip = this.ctx.strip;
1430
+ if (strip?.expanded) strip.setExpanded(false);
1431
+ };
1432
+ onScroll = () => {
1433
+ if (this.frame !== null) return;
1434
+ this.frame = this.ctx.win.requestAnimationFrame(() => {
1435
+ this.frame = null;
1436
+ this.followTop();
1437
+ });
1438
+ };
1439
+ /** Marks the first day not hidden under the strip. */
1440
+ followTop() {
1441
+ const strip = this.ctx.strip;
1442
+ if (!strip || this.pinned) return;
1443
+ const edge = strip.el.getBoundingClientRect().bottom + 1;
1444
+ for (const day of this.ctx.days) {
1445
+ const section = this.sections.get(day);
1446
+ if (section && section.getBoundingClientRect().bottom > edge) {
1447
+ strip.setFocus(day);
1448
+ return;
1449
+ }
1450
+ }
1333
1451
  }
1334
1452
  showSelection(_sel) {
1335
1453
  }
1336
- hit(_x, _y) {
1337
- return null;
1454
+ hit(x, y) {
1455
+ const el = this.ctx.doc.elementFromPoint(x, y);
1456
+ const section = el?.closest(".tnc-list-section");
1457
+ if (!section || !this.body.contains(section)) return null;
1458
+ if (section === this.lead?.section) {
1459
+ const item = el?.closest(".tnc-item");
1460
+ const meta = item ? this.ctx.meta.get(item) : void 0;
1461
+ return { kind: "day", day: meta?.day ?? this.ctx.days[0] ?? this.ctx.view.range.startDay };
1462
+ }
1463
+ const day = parseDay(section.dataset.date ?? "");
1464
+ return day === null ? null : { kind: "day", day };
1338
1465
  }
1339
1466
  getScroll() {
1340
1467
  return { top: this.scroller.scrollTop, left: 0 };
@@ -1343,14 +1470,231 @@ var ListView = class {
1343
1470
  this.scroller.scrollTop = s.top;
1344
1471
  }
1345
1472
  elementsFor(id) {
1346
- return [...this.pools.values()].flatMap((p) => p.elementsFor(id));
1473
+ const pools = [...this.pools.values(), ...this.lead ? [this.lead.pool] : []];
1474
+ return pools.flatMap((p) => p.elementsFor(id));
1347
1475
  }
1348
1476
  destroy() {
1477
+ const { win, opts } = this.ctx;
1478
+ win.removeEventListener("scroll", this.onScroll, { capture: true });
1479
+ if (this.frame !== null) win.cancelAnimationFrame(this.frame);
1480
+ this.loader?.disconnect();
1481
+ if (this.lead) opts.listLeadWillUnmount?.({ el: this.lead.slot });
1482
+ this.lead?.pool.clear();
1349
1483
  for (const p of this.pools.values()) p.clear();
1350
1484
  this.el.remove();
1351
1485
  }
1352
1486
  };
1353
1487
 
1488
+ // src/dom/views/strip.ts
1489
+ var SWIPE_PX = 40;
1490
+ var DateStrip = class {
1491
+ constructor(ctx, pick, focus, onShown) {
1492
+ this.ctx = ctx;
1493
+ this.pick = pick;
1494
+ this.onShown = onShown;
1495
+ const { doc, text } = ctx;
1496
+ this.focus = focus;
1497
+ this.shown = startOfWeek(focus, ctx.firstDay);
1498
+ this.el = h(doc, "div", "tnc-strip");
1499
+ const head = h(doc, "div", "tnc-strip-head");
1500
+ this.title = h(doc, "button", "tnc-strip-title", { type: "button", "aria-expanded": "false" });
1501
+ this.title.addEventListener("click", () => this.setExpanded(!this.expanded));
1502
+ const prev = h(doc, "button", "tnc-strip-nav tnc-strip-prev", { type: "button", "aria-label": text.prev, title: text.prev }, "\u2039");
1503
+ const next = h(doc, "button", "tnc-strip-nav tnc-strip-next", { type: "button", "aria-label": text.next, title: text.next }, "\u203A");
1504
+ prev.addEventListener("click", () => this.step(-1));
1505
+ next.addEventListener("click", () => this.step(1));
1506
+ this.todayBtn = h(doc, "button", "tnc-strip-today", { type: "button" }, text.today);
1507
+ this.todayBtn.addEventListener("click", (ev) => this.pick(ctx.today, ev));
1508
+ this.slot = h(doc, "div", "tnc-strip-slot", { "data-tnc-no-drag": "" });
1509
+ head.append(this.title, this.todayBtn, prev, next, this.slot);
1510
+ this.dows = h(doc, "div", "tnc-strip-dows", { "aria-hidden": "true" });
1511
+ this.grid = h(doc, "div", "tnc-strip-grid", { role: "group" });
1512
+ const handle = h(doc, "button", "tnc-strip-handle", { type: "button", tabindex: "-1", "aria-hidden": "true" });
1513
+ handle.addEventListener("click", () => this.setExpanded(!this.expanded));
1514
+ this.el.append(head, this.dows, this.grid, handle);
1515
+ this.grid.addEventListener("click", this.onClick);
1516
+ this.grid.addEventListener("keydown", this.onKeyDown);
1517
+ this.el.addEventListener("pointerdown", this.onPointerDown);
1518
+ this.el.addEventListener("pointerup", this.onPointerUp);
1519
+ this.el.addEventListener("pointercancel", this.onPointerCancel);
1520
+ this.el.addEventListener("click", this.onClickCapture, true);
1521
+ this.draw();
1522
+ ctx.opts.stripSlotDidMount?.({ el: this.slot });
1523
+ }
1524
+ ctx;
1525
+ pick;
1526
+ onShown;
1527
+ el;
1528
+ title;
1529
+ dows;
1530
+ grid;
1531
+ slot;
1532
+ todayBtn;
1533
+ /** Day at the top of the list. */
1534
+ focus;
1535
+ /** First day of the week shown, or the month shown when expanded. */
1536
+ shown;
1537
+ target = null;
1538
+ /** Days tinted as on screen besides the focus (a time grid's range). */
1539
+ range = null;
1540
+ shownKey = "";
1541
+ swipe = null;
1542
+ swallowClick = false;
1543
+ expanded = false;
1544
+ /** The list scrolled or the grid moved: mark the day and keep it in view. */
1545
+ setFocus(day, range = this.range) {
1546
+ const sameRange = range?.start === this.range?.start && range?.end === this.range?.end;
1547
+ if (day === this.focus && sameRange) return;
1548
+ this.range = range;
1549
+ if (day !== this.focus) {
1550
+ this.focus = day;
1551
+ if (!this.expanded) this.shown = startOfWeek(day, this.ctx.firstDay);
1552
+ else if (startOfMonth(day) !== this.shown) this.shown = startOfMonth(day);
1553
+ }
1554
+ this.draw();
1555
+ }
1556
+ /** Publishes the strip's height as `--tnc-strip-h`, for what sticks below it. */
1557
+ measure() {
1558
+ this.el.parentElement?.style.setProperty("--tnc-strip-h", `${this.el.offsetHeight}px`);
1559
+ }
1560
+ /** First day and end (exclusive) of the days on show. */
1561
+ window() {
1562
+ const days = this.days();
1563
+ return { start: days[0], end: days[days.length - 1] + 1 };
1564
+ }
1565
+ setExpanded(on) {
1566
+ if (on === this.expanded) return;
1567
+ this.expanded = on;
1568
+ this.shown = on ? startOfMonth(this.focus) : startOfWeek(this.focus, this.ctx.firstDay);
1569
+ this.draw();
1570
+ }
1571
+ /** Highlights the day a dragged item would land on. */
1572
+ markTarget(day) {
1573
+ if (day === this.target) return;
1574
+ this.target = day;
1575
+ for (const cell of this.grid.querySelectorAll(".tnc-strip-day"))
1576
+ cell.classList.toggle("is-drop-target", day !== null && cell.dataset.date === this.ctx.dayArg(day).dateStr);
1577
+ }
1578
+ dayAt(el) {
1579
+ const cell = el.closest(".tnc-strip-day");
1580
+ return cell && this.grid.contains(cell) ? parseDay(cell.dataset.date ?? "") : null;
1581
+ }
1582
+ step(dir) {
1583
+ this.shown = this.expanded ? addMonths(this.shown, dir) : this.shown + 7 * dir;
1584
+ this.draw();
1585
+ }
1586
+ days() {
1587
+ if (!this.expanded) return Array.from({ length: 7 }, (_, i) => this.shown + i);
1588
+ const first = startOfWeek(this.shown, this.ctx.firstDay);
1589
+ const end = addMonths(this.shown, 1);
1590
+ const weeks = Math.ceil((end - first) / 7);
1591
+ return Array.from({ length: weeks * 7 }, (_, i) => first + i);
1592
+ }
1593
+ /** Redraws the cells; also called on every list render to refresh the dots. */
1594
+ draw() {
1595
+ const { ctx } = this;
1596
+ const { doc, fmt, store } = ctx;
1597
+ const days = this.days();
1598
+ const titleDay = this.expanded ? this.shown : days.includes(this.focus) ? this.focus : this.shown + 3;
1599
+ this.title.replaceChildren(fmt.monthYear(titleDay), h(doc, "span", "tnc-strip-caret", { "aria-hidden": "true" }));
1600
+ this.title.setAttribute("aria-expanded", String(this.expanded));
1601
+ this.el.classList.toggle("is-expanded", this.expanded);
1602
+ this.todayBtn.hidden = this.focus === ctx.today;
1603
+ this.dows.replaceChildren(...days.slice(0, 7).map((d) => h(doc, "span", null, null, fmt.weekdayNarrow(d))));
1604
+ const focused = this.grid.contains(doc.activeElement) ? doc.activeElement.dataset.date : void 0;
1605
+ const month = ymdOfDay(this.shown).month;
1606
+ const tabDay = days.includes(this.focus) ? this.focus : days[0];
1607
+ const cells = days.map((d) => {
1608
+ const arg = ctx.dayArg(d);
1609
+ const count = store.inRange(d, d + 1).filter((n) => n.item.display !== "background" && n.item.display !== "none").length;
1610
+ const first = ymdOfDay(d).day === 1 && d !== days[0] && !this.expanded;
1611
+ const label = fmt.longDate(d);
1612
+ const cell = h(
1613
+ doc,
1614
+ "button",
1615
+ [
1616
+ "tnc-strip-day",
1617
+ ...ctx.dayClasses(d),
1618
+ d === this.focus ? "is-focus" : "",
1619
+ d !== this.focus && this.range && d >= this.range.start && d < this.range.end ? "is-in-range" : "",
1620
+ count ? "has-items" : "",
1621
+ this.expanded && ymdOfDay(d).month !== month ? "is-other-month" : "",
1622
+ d === this.target ? "is-drop-target" : ""
1623
+ ].filter(Boolean).join(" "),
1624
+ {
1625
+ type: "button",
1626
+ "data-date": arg.dateStr,
1627
+ "aria-label": count ? `${label}, ${ctx.text.itemCount(count)}` : label,
1628
+ "aria-pressed": String(d === this.focus),
1629
+ tabindex: d === tabDay ? "0" : "-1"
1630
+ }
1631
+ );
1632
+ if (first) cell.append(h(doc, "span", "tnc-strip-month", { "aria-hidden": "true" }, fmt.monthShort(d)));
1633
+ cell.append(h(doc, "span", "tnc-strip-num", null, fmt.dayNumber(d)), h(doc, "span", "tnc-strip-dot", { "aria-hidden": "true" }));
1634
+ return cell;
1635
+ });
1636
+ this.grid.replaceChildren(...cells);
1637
+ this.grid.setAttribute("aria-label", fmt.monthYear(titleDay));
1638
+ if (focused) this.grid.querySelector(`[data-date="${focused}"]`)?.focus({ preventScroll: true });
1639
+ const key = `${days[0]}:${days.length}`;
1640
+ if (key !== this.shownKey) {
1641
+ this.shownKey = key;
1642
+ this.onShown();
1643
+ }
1644
+ }
1645
+ onClick = (ev) => {
1646
+ const day = this.dayAt(ev.target);
1647
+ if (day === null) return;
1648
+ if (this.expanded) this.setExpanded(false);
1649
+ this.pick(day, ev);
1650
+ };
1651
+ onKeyDown = (ev) => {
1652
+ const day = this.dayAt(ev.target);
1653
+ if (day === null) return;
1654
+ const move = { ArrowLeft: -1, ArrowRight: 1, ArrowUp: -7, ArrowDown: 7 }[ev.key];
1655
+ if (move === void 0) return;
1656
+ ev.preventDefault();
1657
+ const next = day + move;
1658
+ const days = this.days();
1659
+ if (next < days[0] || next > days[days.length - 1]) {
1660
+ this.shown = this.expanded ? startOfMonth(next) : startOfWeek(next, this.ctx.firstDay);
1661
+ this.draw();
1662
+ }
1663
+ const cell = this.grid.querySelector(`[data-date="${this.ctx.dayArg(next).dateStr}"]`);
1664
+ if (!cell) return;
1665
+ for (const c of this.grid.querySelectorAll("[tabindex='0']")) c.setAttribute("tabindex", "-1");
1666
+ cell.setAttribute("tabindex", "0");
1667
+ cell.focus();
1668
+ };
1669
+ onPointerDown = (ev) => {
1670
+ if (ev.button !== 0 || this.ctx.draggingId) return;
1671
+ this.swipe = { id: ev.pointerId, x: ev.clientX, y: ev.clientY };
1672
+ };
1673
+ onPointerUp = (ev) => {
1674
+ const s = this.swipe;
1675
+ this.swipe = null;
1676
+ if (!s || s.id !== ev.pointerId) return;
1677
+ const dx = ev.clientX - s.x;
1678
+ if (Math.abs(dx) < SWIPE_PX || Math.abs(dx) < Math.abs(ev.clientY - s.y)) return;
1679
+ this.swallowClick = true;
1680
+ this.step(dx < 0 ? 1 : -1);
1681
+ };
1682
+ onPointerCancel = () => {
1683
+ this.swipe = null;
1684
+ };
1685
+ /** A swipe that ends on a day is not a pick. */
1686
+ onClickCapture = (ev) => {
1687
+ if (!this.swallowClick) return;
1688
+ this.swallowClick = false;
1689
+ ev.stopPropagation();
1690
+ ev.preventDefault();
1691
+ };
1692
+ destroy() {
1693
+ this.ctx.opts.stripSlotWillUnmount?.({ el: this.slot });
1694
+ this.el.remove();
1695
+ }
1696
+ };
1697
+
1354
1698
  // src/dom/views/timegrid.ts
1355
1699
  var BAR_GAP2 = 2;
1356
1700
  var TimeGridView = class {
@@ -2487,6 +2831,7 @@ ${this.ctx.text.more(items.length - 8)}` : ""}` : label;
2487
2831
 
2488
2832
  // src/dom/calendar.ts
2489
2833
  var EXTERNAL_ID = "__tnc_external__";
2834
+ var MAX_AGENDA_DAYS = 366;
2490
2835
  function sameOption(a, b, depth = 0) {
2491
2836
  if (a === b) return true;
2492
2837
  if (typeof a === "function" && typeof b === "function") return true;
@@ -2555,10 +2900,15 @@ var Calendar = class {
2555
2900
  slot = { min: 0, max: MINUTES_PER_DAY, duration: 30, snap: 30, scroll: 480 };
2556
2901
  draggingId = null;
2557
2902
  dragRow = null;
2903
+ strip = null;
2904
+ stripSize = null;
2905
+ rebuilding = false;
2558
2906
  toolbarEl;
2559
2907
  viewHost;
2560
2908
  current = null;
2561
2909
  anchor = 0;
2910
+ /** End of an agenda list grown past its `listDayCount`. */
2911
+ listEnd = null;
2562
2912
  session = null;
2563
2913
  selection = null;
2564
2914
  suppressClick = false;
@@ -2751,6 +3101,10 @@ var Calendar = class {
2751
3101
  if (!p) return;
2752
3102
  const day = p.kind === "day" ? p.day : dayOf(p.ms, this.zone);
2753
3103
  const ms = p.kind === "day" ? toInstant(p.day, 0, this.zone) : p.ms;
3104
+ if (this.agendaList()) {
3105
+ this.showListDay(day);
3106
+ return;
3107
+ }
2754
3108
  const { startDay, endDay } = this.view.range;
2755
3109
  if (day < startDay || day >= endDay) {
2756
3110
  this.anchor = day;
@@ -2762,7 +3116,7 @@ var Calendar = class {
2762
3116
  }
2763
3117
  /** What is under a client point: a time slot, a day, or a timeline row and date. Null outside the view. */
2764
3118
  hitTest(clientX, clientY) {
2765
- return this.current?.hit(clientX, clientY) ?? null;
3119
+ return this.hitAt(clientX, clientY);
2766
3120
  }
2767
3121
  /** Item elements currently rendered for an id. */
2768
3122
  elementsFor(id) {
@@ -2804,6 +3158,9 @@ var Calendar = class {
2804
3158
  this.unbindEvents();
2805
3159
  this.current?.destroy();
2806
3160
  this.current = null;
3161
+ this.stripSize?.disconnect();
3162
+ this.strip?.destroy();
3163
+ this.strip = null;
2807
3164
  this.root.remove();
2808
3165
  }
2809
3166
  /* ========================================================== view context */
@@ -2834,7 +3191,8 @@ var Calendar = class {
2834
3191
  return out;
2835
3192
  }
2836
3193
  isEditable(n, what) {
2837
- if (n.id === EXTERNAL_ID || this.opts.view === "list" || this.opts.view === "year") return false;
3194
+ if (n.id === EXTERNAL_ID || this.opts.view === "year") return false;
3195
+ if (this.opts.view === "list" && (!this.opts.listAgenda || what === "duration")) return false;
2838
3196
  const it = n.item;
2839
3197
  const base = it.editable ?? this.opts.editable;
2840
3198
  const specific = what === "start" ? it.startEditable : it.durationEditable;
@@ -3031,7 +3389,8 @@ var Calendar = class {
3031
3389
  }
3032
3390
  case "list": {
3033
3391
  const s = this.opts.listStart === "week" ? startOfWeek(a, this.firstDay) : a;
3034
- return { startDay: s, endDay: s + Math.max(1, listDayCount) };
3392
+ const end = s + Math.max(1, listDayCount);
3393
+ return { startDay: s, endDay: this.opts.listAgenda && this.listEnd !== null ? Math.max(end, this.listEnd) : end };
3035
3394
  }
3036
3395
  case "month": {
3037
3396
  const s = startOfWeek(startOfMonth(a), this.firstDay);
@@ -3069,6 +3428,33 @@ var Calendar = class {
3069
3428
  const duration = Math.max(1, parseDurationMinutes(this.opts.slotDuration, 30));
3070
3429
  const snap = Math.max(1, parseDurationMinutes(this.opts.snapDuration ?? null, duration));
3071
3430
  this.slot = { min, max, duration, snap, scroll: parseDurationMinutes(this.opts.scrollTime, 480) };
3431
+ if (navigated) this.listEnd = null;
3432
+ const prev = this.view;
3433
+ this.applyRange();
3434
+ this.conflicts = dependencyConflicts(this.store);
3435
+ this.root.className = "tnc";
3436
+ this.root.classList.add(`tnc--${this.opts.view}`, `tnc--height-${this.opts.height}`);
3437
+ if (!this.opts.timeGrid) this.root.classList.add("tnc--no-time-grid");
3438
+ this.renderToolbar();
3439
+ this.rebuilding = true;
3440
+ this.syncStrip(navigated);
3441
+ this.rebuilding = false;
3442
+ this.view.dataRange = this.dataRangeFor(this.view.range);
3443
+ const rangeChanged = rangeMoved(prev, this.view);
3444
+ this.current?.destroy();
3445
+ this.current = this.createView();
3446
+ this.viewHost.replaceChildren(this.current.el);
3447
+ this.current.render(null);
3448
+ if (keep && this.current instanceof TimelineView) {
3449
+ this.current.setScroll({ top: keep.top, left: 0 });
3450
+ this.current.placeInstant(keep.ms, keep.vx);
3451
+ } else if (scroll) this.current.setScroll(scroll);
3452
+ else if (this.current instanceof TimeGridView) this.current.scrollToTime(this.slot.scroll);
3453
+ else if (this.current instanceof TimelineView) this.current.scrollToDay(this.anchor);
3454
+ if (rangeChanged) this.opts.onRangeChange?.(this.view);
3455
+ }
3456
+ /** Sets the visible days and view info from the anchor. */
3457
+ applyRange() {
3072
3458
  const { startDay, endDay } = this.computeRange();
3073
3459
  const hidden = new Set(this.opts.hiddenDays);
3074
3460
  if (!this.opts.weekends) {
@@ -3085,26 +3471,100 @@ var Calendar = class {
3085
3471
  start: new Date(toInstant(startDay, 0, this.zone)),
3086
3472
  end: new Date(toInstant(endDay, 0, this.zone))
3087
3473
  };
3088
- const prevView = this.view;
3089
- this.view = { type: this.opts.view, range, anchor: this.anchor, title: "" };
3474
+ this.view = { type: this.opts.view, range, anchor: this.anchor, title: "", dataRange: this.dataRangeFor(range) };
3090
3475
  this.view.title = this.computeTitle(startDay, endDay);
3091
- this.conflicts = dependencyConflicts(this.store);
3092
- this.root.className = "tnc";
3093
- this.root.classList.add(`tnc--${this.opts.view}`, `tnc--height-${this.opts.height}`);
3094
- if (!this.opts.timeGrid) this.root.classList.add("tnc--no-time-grid");
3095
- this.renderToolbar();
3096
- this.current?.destroy();
3097
- this.current = this.createView();
3098
- this.viewHost.replaceChildren(this.current.el);
3099
- this.current.render(null);
3100
- if (keep && this.current instanceof TimelineView) {
3101
- this.current.setScroll({ top: keep.top, left: 0 });
3102
- this.current.placeInstant(keep.ms, keep.vx);
3103
- } else if (scroll) this.current.setScroll(scroll);
3104
- else if (this.current instanceof TimeGridView) this.current.scrollToTime(this.slot.scroll);
3105
- else if (this.current instanceof TimelineView) this.current.scrollToDay(this.anchor);
3106
- const rangeChanged = !prevView || prevView.type !== this.view.type || prevView.range.startDay !== startDay || prevView.range.endDay !== endDay;
3107
- if (rangeChanged) this.opts.onRangeChange?.(this.view);
3476
+ }
3477
+ /** The range, run on to the end of the strip's days so their dots are right. */
3478
+ dataRangeFor(range) {
3479
+ const end = this.strip ? Math.max(range.endDay, this.strip.window().end) : range.endDay;
3480
+ if (end === range.endDay) return range;
3481
+ return { ...range, endDay: end, end: new Date(toInstant(end, 0, this.zone)) };
3482
+ }
3483
+ /** Adds, updates or removes the date strip for the current view. */
3484
+ syncStrip(navigated) {
3485
+ const { view, listAgenda, dateStrip } = this.opts;
3486
+ const list = view === "list";
3487
+ const want = list ? listAgenda : dateStrip && (view === "day" || view === "days" || view === "week");
3488
+ if (!want) {
3489
+ if (!this.strip) return;
3490
+ this.stripSize?.disconnect();
3491
+ this.stripSize = null;
3492
+ this.strip.destroy();
3493
+ this.strip = null;
3494
+ this.root.style.removeProperty("--tnc-strip-h");
3495
+ return;
3496
+ }
3497
+ this.root.classList.add("tnc--has-strip");
3498
+ const { startDay, endDay } = this.view.range;
3499
+ if (!this.strip) {
3500
+ const strip = new DateStrip(this, (day, ev) => this.pickStripDay(day, ev), startDay, () => this.stripShown());
3501
+ this.strip = strip;
3502
+ this.root.insertBefore(strip.el, this.viewHost);
3503
+ const RO = this.win.ResizeObserver;
3504
+ if (RO) {
3505
+ this.stripSize = new RO(() => strip.measure());
3506
+ this.stripSize.observe(strip.el);
3507
+ }
3508
+ }
3509
+ if (!list) this.strip.setFocus(startDay, { start: startDay, end: endDay });
3510
+ else if (navigated) this.strip.setFocus(startDay, null);
3511
+ this.strip.draw();
3512
+ }
3513
+ pickStripDay(day, ev) {
3514
+ this.dateClick(day, ev);
3515
+ if (this.agendaList()) {
3516
+ this.showListDay(day);
3517
+ return;
3518
+ }
3519
+ const { startDay, endDay } = this.view.range;
3520
+ if (this.opts.view === "week" && day >= startDay && day < endDay) return;
3521
+ this.anchor = day;
3522
+ this.rebuild(true);
3523
+ }
3524
+ /** The strip moved to other days: load theirs too. */
3525
+ stripShown() {
3526
+ if (!this.strip || this.rebuilding || !this.view) return;
3527
+ const next = this.dataRangeFor(this.view.range);
3528
+ if (next.endDay === this.view.dataRange.endDay) return;
3529
+ this.view = { ...this.view, dataRange: next };
3530
+ this.opts.onRangeChange?.(this.view);
3531
+ }
3532
+ /** What is under a point, the date strip included. */
3533
+ hitAt(x, y) {
3534
+ const strip = this.strip;
3535
+ if (strip) {
3536
+ const el = this.doc.elementFromPoint(x, y);
3537
+ const day = el ? strip.dayAt(el) : null;
3538
+ strip.markTarget(day !== null && this.draggingId ? day : null);
3539
+ if (day !== null) return { kind: "day", day };
3540
+ }
3541
+ return this.current?.hit(x, y) ?? null;
3542
+ }
3543
+ agendaList() {
3544
+ return this.opts.view === "list" && this.opts.listAgenda && this.current instanceof ListView ? this.current : null;
3545
+ }
3546
+ extendList(endDay) {
3547
+ if (!this.agendaList()) return;
3548
+ const { startDay, endDay: current } = this.view.range;
3549
+ const end = Math.min(endDay, startDay + MAX_AGENDA_DAYS);
3550
+ if (end <= current) return;
3551
+ this.listEnd = end;
3552
+ this.applyRange();
3553
+ this.current?.render(null);
3554
+ this.strip?.draw();
3555
+ this.opts.onRangeChange?.(this.view);
3556
+ }
3557
+ showListDay(day) {
3558
+ if (!this.agendaList()) return;
3559
+ if (day < this.view.range.startDay || day - this.view.range.startDay >= MAX_AGENDA_DAYS) {
3560
+ this.anchor = day;
3561
+ this.rebuild(true);
3562
+ }
3563
+ this.extendList(day + 2 * Math.max(7, this.opts.listDayCount));
3564
+ this.agendaList()?.scrollToDay(day);
3565
+ }
3566
+ dateClick(day, jsEvent) {
3567
+ this.clickSlot({ kind: "day", day }, jsEvent, true);
3108
3568
  }
3109
3569
  createView() {
3110
3570
  switch (this.opts.view) {
@@ -3407,8 +3867,9 @@ var Calendar = class {
3407
3867
  };
3408
3868
  this.session = s;
3409
3869
  if (touch && itemEl?.classList.contains("is-draggable")) {
3410
- const el = itemEl.closest(".tnc-tg-scroll, .tnc-dg-scroll, .tnc-tl-scroll, .tnc-list-scroll, .tnc-year-scroll");
3411
- if (el) s.pan = { el, left: el.scrollLeft, top: el.scrollTop, active: false };
3870
+ const x = scrollerOf(itemEl, "x");
3871
+ const y = scrollerOf(itemEl, "y");
3872
+ s.pan = { x, y, left: x?.scrollLeft ?? 0, top: y?.scrollTop ?? 0, active: false };
3412
3873
  }
3413
3874
  if (touch) {
3414
3875
  s.timer = this.win.setTimeout(() => {
@@ -3449,8 +3910,8 @@ var Calendar = class {
3449
3910
  if (!pan.active && s.timer !== null) this.win.clearTimeout(s.timer);
3450
3911
  s.timer = null;
3451
3912
  pan.active = true;
3452
- pan.el.scrollLeft = pan.left - (ev.clientX - s.x0);
3453
- pan.el.scrollTop = pan.top - (ev.clientY - s.y0);
3913
+ if (pan.x) pan.x.scrollLeft = pan.left - (ev.clientX - s.x0);
3914
+ if (pan.y) pan.y.scrollTop = pan.top - (ev.clientY - s.y0);
3454
3915
  } else if (dist > 8) this.endSession(null, true);
3455
3916
  return;
3456
3917
  }
@@ -3488,7 +3949,7 @@ var Calendar = class {
3488
3949
  };
3489
3950
  /** Proposal for the current pointer position. */
3490
3951
  track(s, ev) {
3491
- const hit = this.current?.hit(ev.clientX, ev.clientY) ?? null;
3952
+ const hit = this.hitAt(ev.clientX, ev.clientY);
3492
3953
  if (s.mode === "external") {
3493
3954
  this.trackExternal(s, hit);
3494
3955
  return;
@@ -3614,7 +4075,7 @@ var Calendar = class {
3614
4075
  return this.dayProposal(hit.day, hit.day + this.opts.defaultAllDayDays);
3615
4076
  }
3616
4077
  if (hit.kind === "day") {
3617
- const dd = hit.day - (grab.kind === "day" ? grab.day : hit.day);
4078
+ const dd = hit.day - (grab.kind === "timeline" ? hit.day : grab.day);
3618
4079
  if (o.allDay) return this.dayProposal(o.startDay + dd, o.startDay + dd + days);
3619
4080
  const startMs2 = this.civilToMs(this.civil(o.startMs) + dd * MINUTES_PER_DAY);
3620
4081
  const endMs = this.civilToMs(this.civil(o.endMs) + dd * MINUTES_PER_DAY);
@@ -3749,6 +4210,7 @@ var Calendar = class {
3749
4210
  this.draggingId = null;
3750
4211
  this.dragRow = null;
3751
4212
  if (draggedId) for (const el of this.elementsFor(draggedId)) el.classList.remove("is-dragging");
4213
+ this.strip?.markTarget(null);
3752
4214
  if (!s.active) return;
3753
4215
  if (s.mode === "select") {
3754
4216
  if (cancel) this.unselectSilently();
@@ -4035,6 +4497,19 @@ var Calendar = class {
4035
4497
  });
4036
4498
  };
4037
4499
  };
4500
+ function scrollerOf(el, axis) {
4501
+ const doc = el.ownerDocument;
4502
+ const win = doc.defaultView;
4503
+ for (let p = el.parentElement; p && p !== doc.body && p !== doc.documentElement; p = p.parentElement) {
4504
+ const overflow = win?.getComputedStyle(p)[axis === "x" ? "overflowX" : "overflowY"] ?? "";
4505
+ const room = axis === "x" ? p.scrollWidth > p.clientWidth : p.scrollHeight > p.clientHeight;
4506
+ if (room && /auto|scroll|overlay/.test(overflow)) return p;
4507
+ }
4508
+ return doc.scrollingElement;
4509
+ }
4510
+ function rangeMoved(prev, next) {
4511
+ return !prev || prev.type !== next.type || prev.range.startDay !== next.range.startDay || prev.range.endDay !== next.range.endDay || prev.dataRange.endDay !== next.dataRange.endDay;
4512
+ }
4038
4513
 
4039
4514
  // src/react/index.tsx
4040
4515
  var import_jsx_runtime = require("react/jsx-runtime");