@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.
@@ -709,6 +709,9 @@ function defaultOptions() {
709
709
  listDayCount: 7,
710
710
  listStart: "date",
711
711
  listAgenda: false,
712
+ dateStrip: false,
713
+ listLeadTitle: null,
714
+ listAddText: null,
712
715
  monthDisplay: "full",
713
716
  timeZone: "local",
714
717
  locale: void 0,
@@ -759,6 +762,9 @@ var LAYOUT_KEYS = /* @__PURE__ */ new Set([
759
762
  "listDayCount",
760
763
  "listStart",
761
764
  "listAgenda",
765
+ "dateStrip",
766
+ "listLeadTitle",
767
+ "listAddText",
762
768
  "monthDisplay",
763
769
  "timeZone",
764
770
  "locale",
@@ -1248,12 +1254,244 @@ var DayGridView = class {
1248
1254
  }
1249
1255
  };
1250
1256
 
1257
+ // src/dom/views/list.ts
1258
+ var LOAD_AHEAD = "0px 0px 600px 0px";
1259
+ var ListView = class {
1260
+ constructor(ctx) {
1261
+ this.ctx = ctx;
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 });
1266
+ this.scroller.append(this.body);
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 });
1286
+ }
1287
+ ctx;
1288
+ el;
1289
+ scroller;
1290
+ body;
1291
+ agenda;
1292
+ pools = /* @__PURE__ */ new Map();
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;
1300
+ itemsOn(day) {
1301
+ const { store, opts } = this.ctx;
1302
+ const out = store.spanningIn(day, day + 1, true);
1303
+ const seen = new Set(out.map((n) => n.id));
1304
+ for (const n of store.timedOn(day)) if (!seen.has(n.id)) out.push(n);
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));
1306
+ }
1307
+ render(_change) {
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();
1313
+ const keep = /* @__PURE__ */ new Set();
1314
+ let prev = null;
1315
+ for (const day of days) {
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;
1328
+ keep.add(day);
1329
+ let section = this.sections.get(day);
1330
+ let pool = this.pools.get(day);
1331
+ if (!section || !pool) {
1332
+ const arg = ctx.dayArg(day);
1333
+ const heading = h(doc, "div", ["tnc-list-day", ...ctx.dayClasses(day)].join(" "), { "data-date": arg.dateStr });
1334
+ const custom = opts.dayHeaderContent?.(arg);
1335
+ const label = custom ?? ctx.fmt.longDate(day);
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);
1342
+ this.sections.set(day, section);
1343
+ this.pools.set(day, pool);
1344
+ }
1345
+ section.classList.toggle("is-empty", !items.length);
1346
+ this.fill(pool, items, () => day);
1347
+ if (prev ? prev.nextSibling !== section : this.body.firstChild !== section) {
1348
+ if (prev) prev.after(section);
1349
+ else this.body.prepend(section);
1350
+ }
1351
+ prev = section;
1352
+ }
1353
+ for (const [day, section] of this.sections) {
1354
+ if (keep.has(day)) continue;
1355
+ this.pools.get(day)?.clear();
1356
+ this.pools.delete(day);
1357
+ section.remove();
1358
+ this.sections.delete(day);
1359
+ }
1360
+ this.renderLead(leadItems);
1361
+ let empty = this.body.querySelector(":scope > .tnc-list-empty");
1362
+ if (!keep.size && !leadItems.length) {
1363
+ if (!empty) {
1364
+ empty = h(doc, "div", "tnc-list-empty", { role: "listitem" }, ctx.text.noItems);
1365
+ this.body.append(empty);
1366
+ }
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
+ }
1451
+ }
1452
+ showSelection(_sel) {
1453
+ }
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 };
1465
+ }
1466
+ getScroll() {
1467
+ return { top: this.scroller.scrollTop, left: 0 };
1468
+ }
1469
+ setScroll(s) {
1470
+ this.scroller.scrollTop = s.top;
1471
+ }
1472
+ elementsFor(id) {
1473
+ const pools = [...this.pools.values(), ...this.lead ? [this.lead.pool] : []];
1474
+ return pools.flatMap((p) => p.elementsFor(id));
1475
+ }
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();
1483
+ for (const p of this.pools.values()) p.clear();
1484
+ this.el.remove();
1485
+ }
1486
+ };
1487
+
1251
1488
  // src/dom/views/strip.ts
1252
1489
  var SWIPE_PX = 40;
1253
1490
  var DateStrip = class {
1254
- constructor(ctx, pick, focus) {
1491
+ constructor(ctx, pick, focus, onShown) {
1255
1492
  this.ctx = ctx;
1256
1493
  this.pick = pick;
1494
+ this.onShown = onShown;
1257
1495
  const { doc, text } = ctx;
1258
1496
  this.focus = focus;
1259
1497
  this.shown = startOfWeek(focus, ctx.firstDay);
@@ -1265,8 +1503,10 @@ var DateStrip = class {
1265
1503
  const next = h(doc, "button", "tnc-strip-nav tnc-strip-next", { type: "button", "aria-label": text.next, title: text.next }, "\u203A");
1266
1504
  prev.addEventListener("click", () => this.step(-1));
1267
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));
1268
1508
  this.slot = h(doc, "div", "tnc-strip-slot", { "data-tnc-no-drag": "" });
1269
- head.append(this.title, prev, next, this.slot);
1509
+ head.append(this.title, this.todayBtn, prev, next, this.slot);
1270
1510
  this.dows = h(doc, "div", "tnc-strip-dows", { "aria-hidden": "true" });
1271
1511
  this.grid = h(doc, "div", "tnc-strip-grid", { role: "group" });
1272
1512
  const handle = h(doc, "button", "tnc-strip-handle", { type: "button", tabindex: "-1", "aria-hidden": "true" });
@@ -1283,27 +1523,45 @@ var DateStrip = class {
1283
1523
  }
1284
1524
  ctx;
1285
1525
  pick;
1526
+ onShown;
1286
1527
  el;
1287
1528
  title;
1288
1529
  dows;
1289
1530
  grid;
1290
1531
  slot;
1532
+ todayBtn;
1291
1533
  /** Day at the top of the list. */
1292
1534
  focus;
1293
1535
  /** First day of the week shown, or the month shown when expanded. */
1294
1536
  shown;
1295
1537
  target = null;
1538
+ /** Days tinted as on screen besides the focus (a time grid's range). */
1539
+ range = null;
1540
+ shownKey = "";
1296
1541
  swipe = null;
1297
1542
  swallowClick = false;
1298
1543
  expanded = false;
1299
- /** The list scrolled: mark its top day and keep it in view. */
1300
- setFocus(day) {
1301
- if (day === this.focus) return;
1302
- this.focus = day;
1303
- if (!this.expanded) this.shown = startOfWeek(day, this.ctx.firstDay);
1304
- else if (startOfMonth(day) !== this.shown) this.shown = startOfMonth(day);
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
+ }
1305
1554
  this.draw();
1306
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
+ }
1307
1565
  setExpanded(on) {
1308
1566
  if (on === this.expanded) return;
1309
1567
  this.expanded = on;
@@ -1341,6 +1599,7 @@ var DateStrip = class {
1341
1599
  this.title.replaceChildren(fmt.monthYear(titleDay), h(doc, "span", "tnc-strip-caret", { "aria-hidden": "true" }));
1342
1600
  this.title.setAttribute("aria-expanded", String(this.expanded));
1343
1601
  this.el.classList.toggle("is-expanded", this.expanded);
1602
+ this.todayBtn.hidden = this.focus === ctx.today;
1344
1603
  this.dows.replaceChildren(...days.slice(0, 7).map((d) => h(doc, "span", null, null, fmt.weekdayNarrow(d))));
1345
1604
  const focused = this.grid.contains(doc.activeElement) ? doc.activeElement.dataset.date : void 0;
1346
1605
  const month = ymdOfDay(this.shown).month;
@@ -1357,6 +1616,7 @@ var DateStrip = class {
1357
1616
  "tnc-strip-day",
1358
1617
  ...ctx.dayClasses(d),
1359
1618
  d === this.focus ? "is-focus" : "",
1619
+ d !== this.focus && this.range && d >= this.range.start && d < this.range.end ? "is-in-range" : "",
1360
1620
  count ? "has-items" : "",
1361
1621
  this.expanded && ymdOfDay(d).month !== month ? "is-other-month" : "",
1362
1622
  d === this.target ? "is-drop-target" : ""
@@ -1376,6 +1636,11 @@ var DateStrip = class {
1376
1636
  this.grid.replaceChildren(...cells);
1377
1637
  this.grid.setAttribute("aria-label", fmt.monthYear(titleDay));
1378
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
+ }
1379
1644
  }
1380
1645
  onClick = (ev) => {
1381
1646
  const day = this.dayAt(ev.target);
@@ -1430,205 +1695,6 @@ var DateStrip = class {
1430
1695
  }
1431
1696
  };
1432
1697
 
1433
- // src/dom/views/list.ts
1434
- var LOAD_AHEAD = "0px 0px 600px 0px";
1435
- var ListView = class {
1436
- constructor(ctx) {
1437
- this.ctx = ctx;
1438
- const { doc, win, opts } = ctx;
1439
- this.el = h(doc, "div", "tnc-list");
1440
- this.scroller = h(doc, "div", "tnc-list-scroll");
1441
- this.body = h(doc, "div", "tnc-list-body", { role: "list", "aria-label": ctx.view.title });
1442
- this.scroller.append(this.body);
1443
- this.el.append(this.scroller);
1444
- if (!opts.listAgenda) return;
1445
- this.el.classList.add("tnc-list--agenda");
1446
- this.strip = new DateStrip(ctx, (day, ev) => this.pick(day, ev), ctx.view.range.startDay);
1447
- this.el.prepend(this.strip.el);
1448
- this.sentinel = h(doc, "div", "tnc-list-more", { "aria-hidden": "true" });
1449
- this.scroller.append(this.sentinel);
1450
- const globals = win;
1451
- const IO = globals.IntersectionObserver;
1452
- if (IO) {
1453
- this.loader = new IO(
1454
- (entries) => {
1455
- if (entries.some((e) => e.isIntersecting)) ctx.extendList(ctx.view.range.endDay + Math.max(7, opts.listDayCount));
1456
- },
1457
- // Auto height scrolls the page, so watch the viewport; fixed height scrolls the list.
1458
- { root: opts.height === "auto" ? null : this.scroller, rootMargin: LOAD_AHEAD }
1459
- );
1460
- }
1461
- const RO = globals.ResizeObserver;
1462
- if (RO) {
1463
- const strip = this.strip.el;
1464
- this.stripSize = new RO(() => this.el.style.setProperty("--tnc-strip-h", `${strip.offsetHeight}px`));
1465
- this.stripSize.observe(strip);
1466
- }
1467
- win.addEventListener("scroll", this.onScroll, { capture: true, passive: true });
1468
- this.scroller.addEventListener("wheel", this.fold, { passive: true });
1469
- this.scroller.addEventListener("touchmove", this.fold, { passive: true });
1470
- }
1471
- ctx;
1472
- el;
1473
- scroller;
1474
- body;
1475
- pools = /* @__PURE__ */ new Map();
1476
- sections = /* @__PURE__ */ new Map();
1477
- strip = null;
1478
- sentinel = null;
1479
- loader = null;
1480
- stripSize = null;
1481
- frame = null;
1482
- /** A picked day stays marked until the list is scrolled by hand. */
1483
- pinned = false;
1484
- itemsOn(day) {
1485
- const { store, opts } = this.ctx;
1486
- const out = store.spanningIn(day, day + 1, true);
1487
- const seen = new Set(out.map((n) => n.id));
1488
- for (const n of store.timedOn(day)) if (!seen.has(n.id)) out.push(n);
1489
- 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));
1490
- }
1491
- render(_change) {
1492
- const { ctx } = this;
1493
- const { doc, days } = ctx;
1494
- const agenda = !!this.strip;
1495
- const keep = /* @__PURE__ */ new Set();
1496
- let prev = null;
1497
- for (const day of days) {
1498
- const items = this.itemsOn(day);
1499
- if (!items.length && !agenda) continue;
1500
- keep.add(day);
1501
- let section = this.sections.get(day);
1502
- let pool = this.pools.get(day);
1503
- if (!section || !pool) {
1504
- const arg = ctx.dayArg(day);
1505
- const heading = h(doc, "div", ["tnc-list-day", ...ctx.dayClasses(day)].join(" "), { "data-date": arg.dateStr });
1506
- const custom = ctx.opts.dayHeaderContent?.(arg);
1507
- const label = custom ?? ctx.fmt.longDate(day);
1508
- heading.append(ctx.opts.navLinks ? ctx.navLink(day, label, "tnc-list-day-link") : label);
1509
- const rows2 = h(doc, "div", "tnc-list-rows", { role: "group" });
1510
- section = h(doc, "div", "tnc-list-section", { role: "listitem", "aria-label": ctx.fmt.longDate(day), "data-date": arg.dateStr }, heading, rows2);
1511
- ctx.opts.dayHeaderDidMount?.({ ...arg, el: heading });
1512
- pool = new ItemPool(ctx, rows2);
1513
- this.sections.set(day, section);
1514
- this.pools.set(day, pool);
1515
- }
1516
- section.classList.toggle("is-empty", !items.length);
1517
- pool.begin();
1518
- for (const n of items) {
1519
- pool.place(n.id, {
1520
- n,
1521
- day,
1522
- isStart: n.startDay === day,
1523
- isEnd: n.endDay === day + 1,
1524
- variant: "list",
1525
- mirror: false
1526
- });
1527
- }
1528
- pool.end();
1529
- const rows = pool.container;
1530
- for (const n of items) {
1531
- const el = pool.elementsFor(n.id)[0];
1532
- if (el) rows.append(el);
1533
- }
1534
- if (prev ? prev.nextSibling !== section : this.body.firstChild !== section) {
1535
- if (prev) prev.after(section);
1536
- else this.body.prepend(section);
1537
- }
1538
- prev = section;
1539
- }
1540
- for (const [day, section] of this.sections) {
1541
- if (keep.has(day)) continue;
1542
- this.pools.get(day)?.clear();
1543
- this.pools.delete(day);
1544
- section.remove();
1545
- this.sections.delete(day);
1546
- }
1547
- let empty = this.body.querySelector(":scope > .tnc-list-empty");
1548
- if (!keep.size) {
1549
- if (!empty) {
1550
- empty = h(doc, "div", "tnc-list-empty", { role: "listitem" }, ctx.text.noItems);
1551
- this.body.append(empty);
1552
- }
1553
- } else empty?.remove();
1554
- this.strip?.draw();
1555
- if (this.loader && this.sentinel) {
1556
- this.loader.unobserve(this.sentinel);
1557
- this.loader.observe(this.sentinel);
1558
- }
1559
- }
1560
- /** A strip day: the host hears of it, and the list brings it to the top. */
1561
- pick(day, ev) {
1562
- this.ctx.dateClick(day, ev);
1563
- this.ctx.showListDay(day);
1564
- }
1565
- /** Brings a day's section to the top, under the strip. */
1566
- scrollToDay(day) {
1567
- const section = this.sections.get(day);
1568
- if (!section) return;
1569
- if (this.strip) this.el.style.setProperty("--tnc-strip-h", `${this.strip.el.offsetHeight}px`);
1570
- section.scrollIntoView({ block: "start" });
1571
- this.strip?.setFocus(day);
1572
- this.pinned = true;
1573
- }
1574
- fold = () => {
1575
- this.pinned = false;
1576
- if (this.strip?.expanded) this.strip.setExpanded(false);
1577
- };
1578
- onScroll = () => {
1579
- if (this.frame !== null) return;
1580
- this.frame = this.ctx.win.requestAnimationFrame(() => {
1581
- this.frame = null;
1582
- this.followTop();
1583
- });
1584
- };
1585
- /** Marks the first day not hidden under the strip. */
1586
- followTop() {
1587
- if (!this.strip || this.pinned) return;
1588
- const edge = this.strip.el.getBoundingClientRect().bottom + 1;
1589
- for (const day of this.ctx.days) {
1590
- const section = this.sections.get(day);
1591
- if (section && section.getBoundingClientRect().bottom > edge) {
1592
- this.strip.setFocus(day);
1593
- return;
1594
- }
1595
- }
1596
- }
1597
- showSelection(_sel) {
1598
- }
1599
- hit(x, y) {
1600
- const el = this.ctx.doc.elementFromPoint(x, y);
1601
- const stripDay = el && this.strip ? this.strip.dayAt(el) : null;
1602
- this.strip?.markTarget(stripDay !== null && this.ctx.draggingId ? stripDay : null);
1603
- if (stripDay !== null) return { kind: "day", day: stripDay };
1604
- const section = el?.closest(".tnc-list-section");
1605
- const day = section && this.body.contains(section) ? parseDay(section.dataset.date ?? "") : null;
1606
- return day === null ? null : { kind: "day", day };
1607
- }
1608
- dragEnded() {
1609
- this.strip?.markTarget(null);
1610
- }
1611
- getScroll() {
1612
- return { top: this.scroller.scrollTop, left: 0 };
1613
- }
1614
- setScroll(s) {
1615
- this.scroller.scrollTop = s.top;
1616
- }
1617
- elementsFor(id) {
1618
- return [...this.pools.values()].flatMap((p) => p.elementsFor(id));
1619
- }
1620
- destroy() {
1621
- const { win } = this.ctx;
1622
- win.removeEventListener("scroll", this.onScroll, { capture: true });
1623
- if (this.frame !== null) win.cancelAnimationFrame(this.frame);
1624
- this.loader?.disconnect();
1625
- this.stripSize?.disconnect();
1626
- this.strip?.destroy();
1627
- for (const p of this.pools.values()) p.clear();
1628
- this.el.remove();
1629
- }
1630
- };
1631
-
1632
1698
  // src/dom/views/timegrid.ts
1633
1699
  var BAR_GAP2 = 2;
1634
1700
  var TimeGridView = class {
@@ -2834,6 +2900,9 @@ var Calendar = class {
2834
2900
  slot = { min: 0, max: MINUTES_PER_DAY, duration: 30, snap: 30, scroll: 480 };
2835
2901
  draggingId = null;
2836
2902
  dragRow = null;
2903
+ strip = null;
2904
+ stripSize = null;
2905
+ rebuilding = false;
2837
2906
  toolbarEl;
2838
2907
  viewHost;
2839
2908
  current = null;
@@ -3047,7 +3116,7 @@ var Calendar = class {
3047
3116
  }
3048
3117
  /** What is under a client point: a time slot, a day, or a timeline row and date. Null outside the view. */
3049
3118
  hitTest(clientX, clientY) {
3050
- return this.current?.hit(clientX, clientY) ?? null;
3119
+ return this.hitAt(clientX, clientY);
3051
3120
  }
3052
3121
  /** Item elements currently rendered for an id. */
3053
3122
  elementsFor(id) {
@@ -3089,6 +3158,9 @@ var Calendar = class {
3089
3158
  this.unbindEvents();
3090
3159
  this.current?.destroy();
3091
3160
  this.current = null;
3161
+ this.stripSize?.disconnect();
3162
+ this.strip?.destroy();
3163
+ this.strip = null;
3092
3164
  this.root.remove();
3093
3165
  }
3094
3166
  /* ========================================================== view context */
@@ -3357,12 +3429,18 @@ var Calendar = class {
3357
3429
  const snap = Math.max(1, parseDurationMinutes(this.opts.snapDuration ?? null, duration));
3358
3430
  this.slot = { min, max, duration, snap, scroll: parseDurationMinutes(this.opts.scrollTime, 480) };
3359
3431
  if (navigated) this.listEnd = null;
3360
- const rangeChanged = this.applyRange();
3432
+ const prev = this.view;
3433
+ this.applyRange();
3361
3434
  this.conflicts = dependencyConflicts(this.store);
3362
3435
  this.root.className = "tnc";
3363
3436
  this.root.classList.add(`tnc--${this.opts.view}`, `tnc--height-${this.opts.height}`);
3364
3437
  if (!this.opts.timeGrid) this.root.classList.add("tnc--no-time-grid");
3365
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);
3366
3444
  this.current?.destroy();
3367
3445
  this.current = this.createView();
3368
3446
  this.viewHost.replaceChildren(this.current.el);
@@ -3375,7 +3453,7 @@ var Calendar = class {
3375
3453
  else if (this.current instanceof TimelineView) this.current.scrollToDay(this.anchor);
3376
3454
  if (rangeChanged) this.opts.onRangeChange?.(this.view);
3377
3455
  }
3378
- /** Sets the visible days and view info from the anchor; true when the range moved. */
3456
+ /** Sets the visible days and view info from the anchor. */
3379
3457
  applyRange() {
3380
3458
  const { startDay, endDay } = this.computeRange();
3381
3459
  const hidden = new Set(this.opts.hiddenDays);
@@ -3393,10 +3471,74 @@ var Calendar = class {
3393
3471
  start: new Date(toInstant(startDay, 0, this.zone)),
3394
3472
  end: new Date(toInstant(endDay, 0, this.zone))
3395
3473
  };
3396
- const prevView = this.view;
3397
- 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) };
3398
3475
  this.view.title = this.computeTitle(startDay, endDay);
3399
- return !prevView || prevView.type !== this.view.type || prevView.range.startDay !== startDay || prevView.range.endDay !== endDay;
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;
3400
3542
  }
3401
3543
  agendaList() {
3402
3544
  return this.opts.view === "list" && this.opts.listAgenda && this.current instanceof ListView ? this.current : null;
@@ -3409,6 +3551,7 @@ var Calendar = class {
3409
3551
  this.listEnd = end;
3410
3552
  this.applyRange();
3411
3553
  this.current?.render(null);
3554
+ this.strip?.draw();
3412
3555
  this.opts.onRangeChange?.(this.view);
3413
3556
  }
3414
3557
  showListDay(day) {
@@ -3806,7 +3949,7 @@ var Calendar = class {
3806
3949
  };
3807
3950
  /** Proposal for the current pointer position. */
3808
3951
  track(s, ev) {
3809
- const hit = this.current?.hit(ev.clientX, ev.clientY) ?? null;
3952
+ const hit = this.hitAt(ev.clientX, ev.clientY);
3810
3953
  if (s.mode === "external") {
3811
3954
  this.trackExternal(s, hit);
3812
3955
  return;
@@ -3932,7 +4075,7 @@ var Calendar = class {
3932
4075
  return this.dayProposal(hit.day, hit.day + this.opts.defaultAllDayDays);
3933
4076
  }
3934
4077
  if (hit.kind === "day") {
3935
- const dd = hit.day - (grab.kind === "day" ? grab.day : hit.day);
4078
+ const dd = hit.day - (grab.kind === "timeline" ? hit.day : grab.day);
3936
4079
  if (o.allDay) return this.dayProposal(o.startDay + dd, o.startDay + dd + days);
3937
4080
  const startMs2 = this.civilToMs(this.civil(o.startMs) + dd * MINUTES_PER_DAY);
3938
4081
  const endMs = this.civilToMs(this.civil(o.endMs) + dd * MINUTES_PER_DAY);
@@ -4067,7 +4210,7 @@ var Calendar = class {
4067
4210
  this.draggingId = null;
4068
4211
  this.dragRow = null;
4069
4212
  if (draggedId) for (const el of this.elementsFor(draggedId)) el.classList.remove("is-dragging");
4070
- this.current?.dragEnded?.();
4213
+ this.strip?.markTarget(null);
4071
4214
  if (!s.active) return;
4072
4215
  if (s.mode === "select") {
4073
4216
  if (cancel) this.unselectSilently();
@@ -4364,6 +4507,9 @@ function scrollerOf(el, axis) {
4364
4507
  }
4365
4508
  return doc.scrollingElement;
4366
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
+ }
4367
4513
 
4368
4514
  // src/react/index.tsx
4369
4515
  var import_jsx_runtime = require("react/jsx-runtime");