@tasknotes/calendar 0.1.3 → 0.1.5

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",
@@ -1112,17 +1118,22 @@ var DayGridView = class {
1112
1118
  const n = w.days.length;
1113
1119
  const hidden = new Array(n).fill([]).map(() => []);
1114
1120
  w.pool.begin();
1121
+ const previewLevels = new Array(n).fill(0);
1115
1122
  let maxLevel = -1;
1116
1123
  for (const p of row.placements) {
1117
1124
  let level = p.level;
1118
1125
  let room = Infinity;
1119
1126
  for (let c = p.startCol; c < p.endCol; c++) room = Math.min(room, visible(c));
1120
1127
  if (level >= room) {
1121
- if (p.item.id !== ctx.draggingId) {
1128
+ if (!p.item.item.preview && p.item.id !== ctx.draggingId) {
1122
1129
  for (let c = p.startCol; c < p.endCol; c++) hidden[c].push(p.item);
1123
1130
  continue;
1124
1131
  }
1125
- level = Math.max(0, room - 1);
1132
+ if (p.item.item.preview) {
1133
+ level = 0;
1134
+ for (let c = p.startCol; c < p.endCol; c++) level = Math.max(level, visible(c) + 1, previewLevels[c]);
1135
+ for (let c = p.startCol; c < p.endCol; c++) previewLevels[c] = level + 1;
1136
+ } else level = Math.max(0, room - 1);
1126
1137
  }
1127
1138
  maxLevel = Math.max(maxLevel, level);
1128
1139
  const single = !p.item.allDay && p.item.endDay - p.item.startDay <= 1;
@@ -1248,12 +1259,244 @@ var DayGridView = class {
1248
1259
  }
1249
1260
  };
1250
1261
 
1262
+ // src/dom/views/list.ts
1263
+ var LOAD_AHEAD = "0px 0px 600px 0px";
1264
+ var ListView = class {
1265
+ constructor(ctx) {
1266
+ this.ctx = ctx;
1267
+ const { doc, win, opts } = ctx;
1268
+ this.el = h(doc, "div", "tnc-list");
1269
+ this.scroller = h(doc, "div", "tnc-list-scroll");
1270
+ this.body = h(doc, "div", "tnc-list-body", { role: "list", "aria-label": ctx.view.title });
1271
+ this.scroller.append(this.body);
1272
+ this.el.append(this.scroller);
1273
+ this.agenda = opts.listAgenda;
1274
+ if (!this.agenda) return;
1275
+ this.el.classList.add("tnc-list--agenda");
1276
+ this.sentinel = h(doc, "div", "tnc-list-more", { "aria-hidden": "true" });
1277
+ this.scroller.append(this.sentinel);
1278
+ const IO = win.IntersectionObserver;
1279
+ if (IO) {
1280
+ this.loader = new IO(
1281
+ (entries) => {
1282
+ if (entries.some((e) => e.isIntersecting)) ctx.extendList(ctx.view.range.endDay + Math.max(7, opts.listDayCount));
1283
+ },
1284
+ // Auto height scrolls the page, so watch the viewport; fixed height scrolls the list.
1285
+ { root: opts.height === "auto" ? null : this.scroller, rootMargin: LOAD_AHEAD }
1286
+ );
1287
+ }
1288
+ win.addEventListener("scroll", this.onScroll, { capture: true, passive: true });
1289
+ this.scroller.addEventListener("wheel", this.fold, { passive: true });
1290
+ this.scroller.addEventListener("touchmove", this.fold, { passive: true });
1291
+ }
1292
+ ctx;
1293
+ el;
1294
+ scroller;
1295
+ body;
1296
+ agenda;
1297
+ pools = /* @__PURE__ */ new Map();
1298
+ sections = /* @__PURE__ */ new Map();
1299
+ lead = null;
1300
+ sentinel = null;
1301
+ loader = null;
1302
+ frame = null;
1303
+ /** A picked day stays marked until the list is scrolled by hand. */
1304
+ pinned = false;
1305
+ itemsOn(day) {
1306
+ const { store, opts } = this.ctx;
1307
+ const out = store.spanningIn(day, day + 1, true);
1308
+ const seen = new Set(out.map((n) => n.id));
1309
+ for (const n of store.timedOn(day)) if (!seen.has(n.id)) out.push(n);
1310
+ 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));
1311
+ }
1312
+ render(_change) {
1313
+ const { ctx, agenda } = this;
1314
+ const { doc, days, opts } = ctx;
1315
+ const includes = agenda && opts.listLeadTitle !== null ? opts.listLeadIncludes : void 0;
1316
+ const leadItems = [];
1317
+ const inLead = /* @__PURE__ */ new Set();
1318
+ const keep = /* @__PURE__ */ new Set();
1319
+ let prev = null;
1320
+ for (const day of days) {
1321
+ let items = this.itemsOn(day);
1322
+ if (includes) {
1323
+ items = items.filter((n) => {
1324
+ if (!includes(n.item)) return true;
1325
+ if (!inLead.has(n.id)) {
1326
+ inLead.add(n.id);
1327
+ leadItems.push(n);
1328
+ }
1329
+ return false;
1330
+ });
1331
+ }
1332
+ if (!items.length && !agenda) continue;
1333
+ keep.add(day);
1334
+ let section = this.sections.get(day);
1335
+ let pool = this.pools.get(day);
1336
+ if (!section || !pool) {
1337
+ const arg = ctx.dayArg(day);
1338
+ const heading = h(doc, "div", ["tnc-list-day", ...ctx.dayClasses(day)].join(" "), { "data-date": arg.dateStr });
1339
+ const custom = opts.dayHeaderContent?.(arg);
1340
+ const label = custom ?? ctx.fmt.longDate(day);
1341
+ heading.append(opts.navLinks ? ctx.navLink(day, label, "tnc-list-day-link") : label);
1342
+ const rows = h(doc, "div", "tnc-list-rows", { role: "group" });
1343
+ section = h(doc, "div", "tnc-list-section", { role: "listitem", "aria-label": ctx.fmt.longDate(day), "data-date": arg.dateStr }, heading, rows);
1344
+ if (agenda && opts.listAddText !== null) section.append(this.addRow(day));
1345
+ opts.dayHeaderDidMount?.({ ...arg, el: heading });
1346
+ pool = new ItemPool(ctx, rows);
1347
+ this.sections.set(day, section);
1348
+ this.pools.set(day, pool);
1349
+ }
1350
+ section.classList.toggle("is-empty", !items.length);
1351
+ this.fill(pool, items, () => day);
1352
+ if (prev ? prev.nextSibling !== section : this.body.firstChild !== section) {
1353
+ if (prev) prev.after(section);
1354
+ else this.body.prepend(section);
1355
+ }
1356
+ prev = section;
1357
+ }
1358
+ for (const [day, section] of this.sections) {
1359
+ if (keep.has(day)) continue;
1360
+ this.pools.get(day)?.clear();
1361
+ this.pools.delete(day);
1362
+ section.remove();
1363
+ this.sections.delete(day);
1364
+ }
1365
+ this.renderLead(leadItems);
1366
+ let empty = this.body.querySelector(":scope > .tnc-list-empty");
1367
+ if (!keep.size && !leadItems.length) {
1368
+ if (!empty) {
1369
+ empty = h(doc, "div", "tnc-list-empty", { role: "listitem" }, ctx.text.noItems);
1370
+ this.body.append(empty);
1371
+ }
1372
+ } else empty?.remove();
1373
+ this.ctx.strip?.draw();
1374
+ if (this.loader && this.sentinel) {
1375
+ this.loader.unobserve(this.sentinel);
1376
+ this.loader.observe(this.sentinel);
1377
+ }
1378
+ }
1379
+ /** Places items in a pool, keeping DOM order equal to item order. */
1380
+ fill(pool, items, dayFor) {
1381
+ pool.begin();
1382
+ for (const n of items) {
1383
+ const d = dayFor(n);
1384
+ pool.place(n.id, { n, day: d, isStart: n.startDay === d, isEnd: n.endDay === d + 1, variant: "list", mirror: false });
1385
+ }
1386
+ pool.end();
1387
+ for (const n of items) {
1388
+ const el = pool.elementsFor(n.id)[0];
1389
+ if (el) pool.container.append(el);
1390
+ }
1391
+ }
1392
+ renderLead(items) {
1393
+ const { ctx } = this;
1394
+ const { doc, opts } = ctx;
1395
+ if (!items.length || opts.listLeadTitle === null) {
1396
+ if (!this.lead) return;
1397
+ opts.listLeadWillUnmount?.({ el: this.lead.slot });
1398
+ this.lead.pool.clear();
1399
+ this.lead.section.remove();
1400
+ this.lead = null;
1401
+ return;
1402
+ }
1403
+ if (!this.lead) {
1404
+ const title = opts.listLeadTitle;
1405
+ const slot = h(doc, "div", "tnc-list-lead-slot", { "data-tnc-no-drag": "" });
1406
+ const heading = h(doc, "div", "tnc-list-day tnc-list-lead-head", null, h(doc, "span", "tnc-list-lead-title", null, title), slot);
1407
+ const rows = h(doc, "div", "tnc-list-rows", { role: "group" });
1408
+ const section = h(doc, "div", "tnc-list-section tnc-list-lead", { role: "listitem", "aria-label": title }, heading, rows);
1409
+ this.lead = { section, slot, pool: new ItemPool(ctx, rows) };
1410
+ opts.listLeadDidMount?.({ el: slot });
1411
+ }
1412
+ if (this.body.firstChild !== this.lead.section) this.body.prepend(this.lead.section);
1413
+ const first = ctx.days[0] ?? ctx.view.range.startDay;
1414
+ this.fill(this.lead.pool, items, (n) => Math.max(n.startDay, first));
1415
+ }
1416
+ addRow(day) {
1417
+ const { ctx } = this;
1418
+ const label = ctx.opts.listAddText ?? "";
1419
+ const btn = h(ctx.doc, "button", "tnc-list-add", { type: "button", "aria-label": `${label}, ${ctx.fmt.longDate(day)}` }, label);
1420
+ btn.addEventListener("click", (ev) => ctx.opts.onListAdd?.({ ...ctx.dayArg(day), jsEvent: ev }));
1421
+ return btn;
1422
+ }
1423
+ /** Brings a day's section to the top, under the strip; the first day brings the lead section too. */
1424
+ scrollToDay(day) {
1425
+ const section = day === this.ctx.days[0] && this.lead ? this.lead.section : this.sections.get(day);
1426
+ if (!section) return;
1427
+ this.ctx.strip?.measure();
1428
+ section.scrollIntoView({ block: "start" });
1429
+ this.ctx.strip?.setFocus(day);
1430
+ this.pinned = true;
1431
+ }
1432
+ fold = () => {
1433
+ this.pinned = false;
1434
+ const strip = this.ctx.strip;
1435
+ if (strip?.expanded) strip.setExpanded(false);
1436
+ };
1437
+ onScroll = () => {
1438
+ if (this.frame !== null) return;
1439
+ this.frame = this.ctx.win.requestAnimationFrame(() => {
1440
+ this.frame = null;
1441
+ this.followTop();
1442
+ });
1443
+ };
1444
+ /** Marks the first day not hidden under the strip. */
1445
+ followTop() {
1446
+ const strip = this.ctx.strip;
1447
+ if (!strip || this.pinned) return;
1448
+ const edge = strip.el.getBoundingClientRect().bottom + 1;
1449
+ for (const day of this.ctx.days) {
1450
+ const section = this.sections.get(day);
1451
+ if (section && section.getBoundingClientRect().bottom > edge) {
1452
+ strip.setFocus(day);
1453
+ return;
1454
+ }
1455
+ }
1456
+ }
1457
+ showSelection(_sel) {
1458
+ }
1459
+ hit(x, y) {
1460
+ const el = this.ctx.doc.elementFromPoint(x, y);
1461
+ const section = el?.closest(".tnc-list-section");
1462
+ if (!section || !this.body.contains(section)) return null;
1463
+ if (section === this.lead?.section) {
1464
+ const item = el?.closest(".tnc-item");
1465
+ const meta = item ? this.ctx.meta.get(item) : void 0;
1466
+ return { kind: "day", day: meta?.day ?? this.ctx.days[0] ?? this.ctx.view.range.startDay };
1467
+ }
1468
+ const day = parseDay(section.dataset.date ?? "");
1469
+ return day === null ? null : { kind: "day", day };
1470
+ }
1471
+ getScroll() {
1472
+ return { top: this.scroller.scrollTop, left: 0 };
1473
+ }
1474
+ setScroll(s) {
1475
+ this.scroller.scrollTop = s.top;
1476
+ }
1477
+ elementsFor(id) {
1478
+ const pools = [...this.pools.values(), ...this.lead ? [this.lead.pool] : []];
1479
+ return pools.flatMap((p) => p.elementsFor(id));
1480
+ }
1481
+ destroy() {
1482
+ const { win, opts } = this.ctx;
1483
+ win.removeEventListener("scroll", this.onScroll, { capture: true });
1484
+ if (this.frame !== null) win.cancelAnimationFrame(this.frame);
1485
+ this.loader?.disconnect();
1486
+ if (this.lead) opts.listLeadWillUnmount?.({ el: this.lead.slot });
1487
+ this.lead?.pool.clear();
1488
+ for (const p of this.pools.values()) p.clear();
1489
+ this.el.remove();
1490
+ }
1491
+ };
1492
+
1251
1493
  // src/dom/views/strip.ts
1252
1494
  var SWIPE_PX = 40;
1253
1495
  var DateStrip = class {
1254
- constructor(ctx, pick, focus) {
1496
+ constructor(ctx, pick, focus, onShown) {
1255
1497
  this.ctx = ctx;
1256
1498
  this.pick = pick;
1499
+ this.onShown = onShown;
1257
1500
  const { doc, text } = ctx;
1258
1501
  this.focus = focus;
1259
1502
  this.shown = startOfWeek(focus, ctx.firstDay);
@@ -1265,8 +1508,10 @@ var DateStrip = class {
1265
1508
  const next = h(doc, "button", "tnc-strip-nav tnc-strip-next", { type: "button", "aria-label": text.next, title: text.next }, "\u203A");
1266
1509
  prev.addEventListener("click", () => this.step(-1));
1267
1510
  next.addEventListener("click", () => this.step(1));
1511
+ this.todayBtn = h(doc, "button", "tnc-strip-today", { type: "button" }, text.today);
1512
+ this.todayBtn.addEventListener("click", (ev) => this.pick(ctx.today, ev));
1268
1513
  this.slot = h(doc, "div", "tnc-strip-slot", { "data-tnc-no-drag": "" });
1269
- head.append(this.title, prev, next, this.slot);
1514
+ head.append(this.title, this.todayBtn, prev, next, this.slot);
1270
1515
  this.dows = h(doc, "div", "tnc-strip-dows", { "aria-hidden": "true" });
1271
1516
  this.grid = h(doc, "div", "tnc-strip-grid", { role: "group" });
1272
1517
  const handle = h(doc, "button", "tnc-strip-handle", { type: "button", tabindex: "-1", "aria-hidden": "true" });
@@ -1283,27 +1528,45 @@ var DateStrip = class {
1283
1528
  }
1284
1529
  ctx;
1285
1530
  pick;
1531
+ onShown;
1286
1532
  el;
1287
1533
  title;
1288
1534
  dows;
1289
1535
  grid;
1290
1536
  slot;
1537
+ todayBtn;
1291
1538
  /** Day at the top of the list. */
1292
1539
  focus;
1293
1540
  /** First day of the week shown, or the month shown when expanded. */
1294
1541
  shown;
1295
1542
  target = null;
1543
+ /** Days tinted as on screen besides the focus (a time grid's range). */
1544
+ range = null;
1545
+ shownKey = "";
1296
1546
  swipe = null;
1297
1547
  swallowClick = false;
1298
1548
  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);
1549
+ /** The list scrolled or the grid moved: mark the day and keep it in view. */
1550
+ setFocus(day, range = this.range) {
1551
+ const sameRange = range?.start === this.range?.start && range?.end === this.range?.end;
1552
+ if (day === this.focus && sameRange) return;
1553
+ this.range = range;
1554
+ if (day !== this.focus) {
1555
+ this.focus = day;
1556
+ if (!this.expanded) this.shown = startOfWeek(day, this.ctx.firstDay);
1557
+ else if (startOfMonth(day) !== this.shown) this.shown = startOfMonth(day);
1558
+ }
1305
1559
  this.draw();
1306
1560
  }
1561
+ /** Publishes the strip's height as `--tnc-strip-h`, for what sticks below it. */
1562
+ measure() {
1563
+ this.el.parentElement?.style.setProperty("--tnc-strip-h", `${this.el.offsetHeight}px`);
1564
+ }
1565
+ /** First day and end (exclusive) of the days on show. */
1566
+ window() {
1567
+ const days = this.days();
1568
+ return { start: days[0], end: days[days.length - 1] + 1 };
1569
+ }
1307
1570
  setExpanded(on) {
1308
1571
  if (on === this.expanded) return;
1309
1572
  this.expanded = on;
@@ -1341,6 +1604,7 @@ var DateStrip = class {
1341
1604
  this.title.replaceChildren(fmt.monthYear(titleDay), h(doc, "span", "tnc-strip-caret", { "aria-hidden": "true" }));
1342
1605
  this.title.setAttribute("aria-expanded", String(this.expanded));
1343
1606
  this.el.classList.toggle("is-expanded", this.expanded);
1607
+ this.todayBtn.hidden = this.focus === ctx.today;
1344
1608
  this.dows.replaceChildren(...days.slice(0, 7).map((d) => h(doc, "span", null, null, fmt.weekdayNarrow(d))));
1345
1609
  const focused = this.grid.contains(doc.activeElement) ? doc.activeElement.dataset.date : void 0;
1346
1610
  const month = ymdOfDay(this.shown).month;
@@ -1357,6 +1621,7 @@ var DateStrip = class {
1357
1621
  "tnc-strip-day",
1358
1622
  ...ctx.dayClasses(d),
1359
1623
  d === this.focus ? "is-focus" : "",
1624
+ d !== this.focus && this.range && d >= this.range.start && d < this.range.end ? "is-in-range" : "",
1360
1625
  count ? "has-items" : "",
1361
1626
  this.expanded && ymdOfDay(d).month !== month ? "is-other-month" : "",
1362
1627
  d === this.target ? "is-drop-target" : ""
@@ -1376,6 +1641,11 @@ var DateStrip = class {
1376
1641
  this.grid.replaceChildren(...cells);
1377
1642
  this.grid.setAttribute("aria-label", fmt.monthYear(titleDay));
1378
1643
  if (focused) this.grid.querySelector(`[data-date="${focused}"]`)?.focus({ preventScroll: true });
1644
+ const key = `${days[0]}:${days.length}`;
1645
+ if (key !== this.shownKey) {
1646
+ this.shownKey = key;
1647
+ this.onShown();
1648
+ }
1379
1649
  }
1380
1650
  onClick = (ev) => {
1381
1651
  const day = this.dayAt(ev.target);
@@ -1430,205 +1700,6 @@ var DateStrip = class {
1430
1700
  }
1431
1701
  };
1432
1702
 
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
1703
  // src/dom/views/timegrid.ts
1633
1704
  var BAR_GAP2 = 2;
1634
1705
  var TimeGridView = class {
@@ -1847,17 +1918,22 @@ var TimeGridView = class {
1847
1918
  const pool = this.allDayPool;
1848
1919
  pool.begin();
1849
1920
  const hidden = new Array(days.length).fill(0);
1921
+ const previewLevels = new Array(days.length).fill(0);
1850
1922
  let maxLevel = -1;
1851
1923
  for (const p of row.placements) {
1852
1924
  let level = p.level;
1853
1925
  let room = Infinity;
1854
1926
  for (let c = p.startCol; c < p.endCol; c++) room = Math.min(room, visibleLevels(c));
1855
1927
  if (level >= room) {
1856
- if (p.item.id !== ctx.draggingId) {
1928
+ if (!p.item.item.preview && p.item.id !== ctx.draggingId) {
1857
1929
  for (let c = p.startCol; c < p.endCol; c++) hidden[c]++;
1858
1930
  continue;
1859
1931
  }
1860
- level = Math.max(0, room - 1);
1932
+ if (p.item.item.preview) {
1933
+ level = 0;
1934
+ for (let c = p.startCol; c < p.endCol; c++) level = Math.max(level, visibleLevels(c) + 1, previewLevels[c]);
1935
+ for (let c = p.startCol; c < p.endCol; c++) previewLevels[c] = level + 1;
1936
+ } else level = Math.max(0, room - 1);
1861
1937
  }
1862
1938
  maxLevel = Math.max(maxLevel, level);
1863
1939
  const el = pool.place(p.item.id, {
@@ -1945,7 +2021,8 @@ var TimeGridView = class {
1945
2021
  const hiddenByCluster = /* @__PURE__ */ new Map();
1946
2022
  pool.begin();
1947
2023
  for (const p of placements) {
1948
- if (withMore && maxStack > 0 && p.cols > maxStack && p.col >= maxStack - 1) {
2024
+ const preview = p.item.item.preview || p.item.id === ctx.draggingId;
2025
+ if (!preview && withMore && maxStack > 0 && p.cols > maxStack && p.col >= maxStack - 1) {
1949
2026
  const rec = hiddenByCluster.get(p.cluster) ?? { count: 0, start: p.startMin, end: p.endMin, items: [] };
1950
2027
  rec.count++;
1951
2028
  rec.start = Math.min(rec.start, p.startMin);
@@ -1954,7 +2031,7 @@ var TimeGridView = class {
1954
2031
  hiddenByCluster.set(p.cluster, rec);
1955
2032
  continue;
1956
2033
  }
1957
- const cols = withMore && maxStack > 0 ? Math.min(p.cols, maxStack) : p.cols;
2034
+ const cols = !preview && withMore && maxStack > 0 ? Math.min(p.cols, maxStack) : p.cols;
1958
2035
  const span = Math.min(p.span, cols - p.col);
1959
2036
  const left = p.col / cols;
1960
2037
  let width = span / cols;
@@ -2834,6 +2911,9 @@ var Calendar = class {
2834
2911
  slot = { min: 0, max: MINUTES_PER_DAY, duration: 30, snap: 30, scroll: 480 };
2835
2912
  draggingId = null;
2836
2913
  dragRow = null;
2914
+ strip = null;
2915
+ stripSize = null;
2916
+ rebuilding = false;
2837
2917
  toolbarEl;
2838
2918
  viewHost;
2839
2919
  current = null;
@@ -3047,7 +3127,7 @@ var Calendar = class {
3047
3127
  }
3048
3128
  /** What is under a client point: a time slot, a day, or a timeline row and date. Null outside the view. */
3049
3129
  hitTest(clientX, clientY) {
3050
- return this.current?.hit(clientX, clientY) ?? null;
3130
+ return this.hitAt(clientX, clientY);
3051
3131
  }
3052
3132
  /** Item elements currently rendered for an id. */
3053
3133
  elementsFor(id) {
@@ -3089,6 +3169,9 @@ var Calendar = class {
3089
3169
  this.unbindEvents();
3090
3170
  this.current?.destroy();
3091
3171
  this.current = null;
3172
+ this.stripSize?.disconnect();
3173
+ this.strip?.destroy();
3174
+ this.strip = null;
3092
3175
  this.root.remove();
3093
3176
  }
3094
3177
  /* ========================================================== view context */
@@ -3119,7 +3202,7 @@ var Calendar = class {
3119
3202
  return out;
3120
3203
  }
3121
3204
  isEditable(n, what) {
3122
- if (n.id === EXTERNAL_ID || this.opts.view === "year") return false;
3205
+ if (n.id === EXTERNAL_ID || n.item.preview || this.opts.view === "year") return false;
3123
3206
  if (this.opts.view === "list" && (!this.opts.listAgenda || what === "duration")) return false;
3124
3207
  const it = n.item;
3125
3208
  const base = it.editable ?? this.opts.editable;
@@ -3145,6 +3228,7 @@ var Calendar = class {
3145
3228
  createItemEl(meta) {
3146
3229
  const { n, variant } = meta;
3147
3230
  const it = n.item;
3231
+ const preview = meta.mirror || it.preview === true;
3148
3232
  const el = h(this.doc, "div", "tnc-item");
3149
3233
  el.classList.add(`tnc-item--${variant}`);
3150
3234
  if (it.kind) el.classList.add(`tnc-k-${it.kind.replace(/[^\w-]/g, "-")}`);
@@ -3154,12 +3238,12 @@ var Calendar = class {
3154
3238
  if (!meta.isStart) el.classList.add("is-cont-start");
3155
3239
  if (!meta.isEnd) el.classList.add("is-cont-end");
3156
3240
  if (n.allDay) el.classList.add("is-all-day");
3157
- if (meta.mirror || n.id === EXTERNAL_ID) el.classList.add("is-mirror");
3241
+ if (preview || n.id === EXTERNAL_ID) el.classList.add("is-mirror");
3158
3242
  if (this.conflicts.has(n.id)) el.classList.add("is-conflict");
3159
3243
  const indicator = variant === "indicator";
3160
- if (!meta.mirror && !indicator && (this.isEditable(n, "start") || this.isEditable(n, "duration"))) el.classList.add("is-draggable");
3244
+ if (!preview && !indicator && (this.isEditable(n, "start") || this.isEditable(n, "duration"))) el.classList.add("is-draggable");
3161
3245
  if (this.draggingId === n.id) el.classList.add("is-dragging");
3162
- if (this.selectedIds.has(n.id) && !meta.mirror) el.classList.add("is-selected");
3246
+ if (this.selectedIds.has(n.id) && !preview) el.classList.add("is-selected");
3163
3247
  el.classList.add(...classList(it.className));
3164
3248
  const arg = this.itemArg(el, meta);
3165
3249
  el.classList.add(...classList(this.opts.itemClassNames?.(arg)));
@@ -3170,7 +3254,7 @@ var Calendar = class {
3170
3254
  if (it.backgroundColor) el.style.setProperty("--tnc-bg", it.backgroundColor);
3171
3255
  if (it.borderColor) el.style.setProperty("--tnc-border", it.borderColor);
3172
3256
  if (it.textColor) el.style.setProperty("--tnc-fg", it.textColor);
3173
- if (indicator) el.setAttribute("aria-hidden", "true");
3257
+ if (indicator || preview) el.setAttribute("aria-hidden", "true");
3174
3258
  else if (it.display !== "background") {
3175
3259
  el.tabIndex = 0;
3176
3260
  el.setAttribute("role", "button");
@@ -3187,7 +3271,7 @@ var Calendar = class {
3187
3271
  } else {
3188
3272
  el.append(this.defaultContent(meta));
3189
3273
  }
3190
- if (variant !== "list" && !indicator && it.display !== "background") this.addHandles(el, meta);
3274
+ if (!preview && variant !== "list" && !indicator && it.display !== "background") this.addHandles(el, meta);
3191
3275
  this.opts.itemDidMount?.(arg);
3192
3276
  return el;
3193
3277
  }
@@ -3196,7 +3280,7 @@ var Calendar = class {
3196
3280
  if (time) this.fillTime(time, meta.n, meta.variant, meta.day);
3197
3281
  el.classList.toggle("is-dragging", this.draggingId === meta.n.id);
3198
3282
  el.classList.toggle("is-conflict", this.conflicts.has(meta.n.id));
3199
- el.classList.toggle("is-selected", this.selectedIds.has(meta.n.id) && !meta.mirror);
3283
+ el.classList.toggle("is-selected", this.selectedIds.has(meta.n.id) && !meta.mirror && !meta.n.item.preview);
3200
3284
  }
3201
3285
  selectedSet() {
3202
3286
  const s = this.opts.selected;
@@ -3206,7 +3290,7 @@ var Calendar = class {
3206
3290
  this.selectedIds = this.selectedSet();
3207
3291
  for (const el of this.viewHost.querySelectorAll(".tnc-item")) {
3208
3292
  const meta = this.meta.get(el);
3209
- if (meta) el.classList.toggle("is-selected", this.selectedIds.has(meta.n.id) && !meta.mirror);
3293
+ if (meta) el.classList.toggle("is-selected", this.selectedIds.has(meta.n.id) && !meta.mirror && !meta.n.item.preview);
3210
3294
  }
3211
3295
  }
3212
3296
  destroyItemEl(el) {
@@ -3357,12 +3441,18 @@ var Calendar = class {
3357
3441
  const snap = Math.max(1, parseDurationMinutes(this.opts.snapDuration ?? null, duration));
3358
3442
  this.slot = { min, max, duration, snap, scroll: parseDurationMinutes(this.opts.scrollTime, 480) };
3359
3443
  if (navigated) this.listEnd = null;
3360
- const rangeChanged = this.applyRange();
3444
+ const prev = this.view;
3445
+ this.applyRange();
3361
3446
  this.conflicts = dependencyConflicts(this.store);
3362
3447
  this.root.className = "tnc";
3363
3448
  this.root.classList.add(`tnc--${this.opts.view}`, `tnc--height-${this.opts.height}`);
3364
3449
  if (!this.opts.timeGrid) this.root.classList.add("tnc--no-time-grid");
3365
3450
  this.renderToolbar();
3451
+ this.rebuilding = true;
3452
+ this.syncStrip(navigated);
3453
+ this.rebuilding = false;
3454
+ this.view.dataRange = this.dataRangeFor(this.view.range);
3455
+ const rangeChanged = rangeMoved(prev, this.view);
3366
3456
  this.current?.destroy();
3367
3457
  this.current = this.createView();
3368
3458
  this.viewHost.replaceChildren(this.current.el);
@@ -3375,7 +3465,7 @@ var Calendar = class {
3375
3465
  else if (this.current instanceof TimelineView) this.current.scrollToDay(this.anchor);
3376
3466
  if (rangeChanged) this.opts.onRangeChange?.(this.view);
3377
3467
  }
3378
- /** Sets the visible days and view info from the anchor; true when the range moved. */
3468
+ /** Sets the visible days and view info from the anchor. */
3379
3469
  applyRange() {
3380
3470
  const { startDay, endDay } = this.computeRange();
3381
3471
  const hidden = new Set(this.opts.hiddenDays);
@@ -3393,10 +3483,74 @@ var Calendar = class {
3393
3483
  start: new Date(toInstant(startDay, 0, this.zone)),
3394
3484
  end: new Date(toInstant(endDay, 0, this.zone))
3395
3485
  };
3396
- const prevView = this.view;
3397
- this.view = { type: this.opts.view, range, anchor: this.anchor, title: "" };
3486
+ this.view = { type: this.opts.view, range, anchor: this.anchor, title: "", dataRange: this.dataRangeFor(range) };
3398
3487
  this.view.title = this.computeTitle(startDay, endDay);
3399
- return !prevView || prevView.type !== this.view.type || prevView.range.startDay !== startDay || prevView.range.endDay !== endDay;
3488
+ }
3489
+ /** The range, run on to the end of the strip's days so their dots are right. */
3490
+ dataRangeFor(range) {
3491
+ const end = this.strip ? Math.max(range.endDay, this.strip.window().end) : range.endDay;
3492
+ if (end === range.endDay) return range;
3493
+ return { ...range, endDay: end, end: new Date(toInstant(end, 0, this.zone)) };
3494
+ }
3495
+ /** Adds, updates or removes the date strip for the current view. */
3496
+ syncStrip(navigated) {
3497
+ const { view, listAgenda, dateStrip } = this.opts;
3498
+ const list = view === "list";
3499
+ const want = list ? listAgenda : dateStrip && (view === "day" || view === "days" || view === "week");
3500
+ if (!want) {
3501
+ if (!this.strip) return;
3502
+ this.stripSize?.disconnect();
3503
+ this.stripSize = null;
3504
+ this.strip.destroy();
3505
+ this.strip = null;
3506
+ this.root.style.removeProperty("--tnc-strip-h");
3507
+ return;
3508
+ }
3509
+ this.root.classList.add("tnc--has-strip");
3510
+ const { startDay, endDay } = this.view.range;
3511
+ if (!this.strip) {
3512
+ const strip = new DateStrip(this, (day, ev) => this.pickStripDay(day, ev), startDay, () => this.stripShown());
3513
+ this.strip = strip;
3514
+ this.root.insertBefore(strip.el, this.viewHost);
3515
+ const RO = this.win.ResizeObserver;
3516
+ if (RO) {
3517
+ this.stripSize = new RO(() => strip.measure());
3518
+ this.stripSize.observe(strip.el);
3519
+ }
3520
+ }
3521
+ if (!list) this.strip.setFocus(startDay, { start: startDay, end: endDay });
3522
+ else if (navigated) this.strip.setFocus(startDay, null);
3523
+ this.strip.draw();
3524
+ }
3525
+ pickStripDay(day, ev) {
3526
+ this.dateClick(day, ev);
3527
+ if (this.agendaList()) {
3528
+ this.showListDay(day);
3529
+ return;
3530
+ }
3531
+ const { startDay, endDay } = this.view.range;
3532
+ if (this.opts.view === "week" && day >= startDay && day < endDay) return;
3533
+ this.anchor = day;
3534
+ this.rebuild(true);
3535
+ }
3536
+ /** The strip moved to other days: load theirs too. */
3537
+ stripShown() {
3538
+ if (!this.strip || this.rebuilding || !this.view) return;
3539
+ const next = this.dataRangeFor(this.view.range);
3540
+ if (next.endDay === this.view.dataRange.endDay) return;
3541
+ this.view = { ...this.view, dataRange: next };
3542
+ this.opts.onRangeChange?.(this.view);
3543
+ }
3544
+ /** What is under a point, the date strip included. */
3545
+ hitAt(x, y) {
3546
+ const strip = this.strip;
3547
+ if (strip) {
3548
+ const el = this.doc.elementFromPoint(x, y);
3549
+ const day = el ? strip.dayAt(el) : null;
3550
+ strip.markTarget(day !== null && this.draggingId ? day : null);
3551
+ if (day !== null) return { kind: "day", day };
3552
+ }
3553
+ return this.current?.hit(x, y) ?? null;
3400
3554
  }
3401
3555
  agendaList() {
3402
3556
  return this.opts.view === "list" && this.opts.listAgenda && this.current instanceof ListView ? this.current : null;
@@ -3409,6 +3563,7 @@ var Calendar = class {
3409
3563
  this.listEnd = end;
3410
3564
  this.applyRange();
3411
3565
  this.current?.render(null);
3566
+ this.strip?.draw();
3412
3567
  this.opts.onRangeChange?.(this.view);
3413
3568
  }
3414
3569
  showListDay(day) {
@@ -3530,7 +3685,7 @@ var Calendar = class {
3530
3685
  day: meta.day,
3531
3686
  isStart: meta.isStart,
3532
3687
  isEnd: meta.isEnd,
3533
- isMirror: meta.mirror,
3688
+ isMirror: meta.mirror || meta.n.item.preview === true,
3534
3689
  variant: meta.variant,
3535
3690
  timeText: meta.n.allDay ? "" : end ? `${start} \u2013 ${end}` : start
3536
3691
  };
@@ -3675,7 +3830,7 @@ var Calendar = class {
3675
3830
  }
3676
3831
  itemElFrom(target) {
3677
3832
  const el = target?.closest?.(".tnc-item");
3678
- return el && this.root.contains(el) && this.meta.has(el) ? el : null;
3833
+ return el && this.root.contains(el) && this.meta.has(el) && !this.meta.get(el)?.n.item.preview ? el : null;
3679
3834
  }
3680
3835
  onDocPointerDown = (ev) => {
3681
3836
  const t = ev.target;
@@ -3806,7 +3961,7 @@ var Calendar = class {
3806
3961
  };
3807
3962
  /** Proposal for the current pointer position. */
3808
3963
  track(s, ev) {
3809
- const hit = this.current?.hit(ev.clientX, ev.clientY) ?? null;
3964
+ const hit = this.hitAt(ev.clientX, ev.clientY);
3810
3965
  if (s.mode === "external") {
3811
3966
  this.trackExternal(s, hit);
3812
3967
  return;
@@ -3932,7 +4087,7 @@ var Calendar = class {
3932
4087
  return this.dayProposal(hit.day, hit.day + this.opts.defaultAllDayDays);
3933
4088
  }
3934
4089
  if (hit.kind === "day") {
3935
- const dd = hit.day - (grab.kind === "day" ? grab.day : hit.day);
4090
+ const dd = hit.day - (grab.kind === "timeline" ? hit.day : grab.day);
3936
4091
  if (o.allDay) return this.dayProposal(o.startDay + dd, o.startDay + dd + days);
3937
4092
  const startMs2 = this.civilToMs(this.civil(o.startMs) + dd * MINUTES_PER_DAY);
3938
4093
  const endMs = this.civilToMs(this.civil(o.endMs) + dd * MINUTES_PER_DAY);
@@ -4067,7 +4222,7 @@ var Calendar = class {
4067
4222
  this.draggingId = null;
4068
4223
  this.dragRow = null;
4069
4224
  if (draggedId) for (const el of this.elementsFor(draggedId)) el.classList.remove("is-dragging");
4070
- this.current?.dragEnded?.();
4225
+ this.strip?.markTarget(null);
4071
4226
  if (!s.active) return;
4072
4227
  if (s.mode === "select") {
4073
4228
  if (cancel) this.unselectSilently();
@@ -4364,6 +4519,9 @@ function scrollerOf(el, axis) {
4364
4519
  }
4365
4520
  return doc.scrollingElement;
4366
4521
  }
4522
+ function rangeMoved(prev, next) {
4523
+ return !prev || prev.type !== next.type || prev.range.startDay !== next.range.startDay || prev.range.endDay !== next.range.endDay || prev.dataRange.endDay !== next.dataRange.endDay;
4524
+ }
4367
4525
 
4368
4526
  // src/react/index.tsx
4369
4527
  var import_jsx_runtime = require("react/jsx-runtime");