@tasknotes/calendar 0.0.0-stage → 0.1.1

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.
Files changed (56) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/LICENSE +21 -0
  3. package/README.md +163 -2
  4. package/dist/calendar.css +1358 -0
  5. package/dist/cjs/core.cjs +716 -0
  6. package/dist/cjs/index.cjs +4240 -0
  7. package/dist/cjs/react.cjs +4068 -0
  8. package/dist/esm/chunk-6RRDNLZM.js +3435 -0
  9. package/dist/esm/chunk-DVMZ5OXN.js +694 -0
  10. package/dist/esm/chunk-L4WKZ6WT.js +0 -0
  11. package/dist/esm/core.js +85 -0
  12. package/dist/esm/index.js +223 -0
  13. package/dist/esm/react.js +46 -0
  14. package/dist/types/core/date.d.cts +30 -0
  15. package/dist/types/core/date.d.ts +30 -0
  16. package/dist/types/core/index.d.cts +7 -0
  17. package/dist/types/core/index.d.ts +7 -0
  18. package/dist/types/core/layout.d.cts +82 -0
  19. package/dist/types/core/layout.d.ts +82 -0
  20. package/dist/types/core/order.d.cts +7 -0
  21. package/dist/types/core/order.d.ts +7 -0
  22. package/dist/types/core/plan.d.cts +42 -0
  23. package/dist/types/core/plan.d.ts +42 -0
  24. package/dist/types/core/store.d.cts +60 -0
  25. package/dist/types/core/store.d.ts +60 -0
  26. package/dist/types/core/types.d.cts +69 -0
  27. package/dist/types/core/types.d.ts +69 -0
  28. package/dist/types/core/zone.d.cts +43 -0
  29. package/dist/types/core/zone.d.ts +43 -0
  30. package/dist/types/dom/calendar.d.cts +215 -0
  31. package/dist/types/dom/calendar.d.ts +215 -0
  32. package/dist/types/dom/context.d.cts +107 -0
  33. package/dist/types/dom/context.d.ts +107 -0
  34. package/dist/types/dom/dom.d.cts +8 -0
  35. package/dist/types/dom/dom.d.ts +8 -0
  36. package/dist/types/dom/format.d.cts +30 -0
  37. package/dist/types/dom/format.d.ts +30 -0
  38. package/dist/types/dom/options.d.cts +401 -0
  39. package/dist/types/dom/options.d.ts +401 -0
  40. package/dist/types/dom/views/daygrid.d.cts +42 -0
  41. package/dist/types/dom/views/daygrid.d.ts +42 -0
  42. package/dist/types/dom/views/list.d.cts +27 -0
  43. package/dist/types/dom/views/list.d.ts +27 -0
  44. package/dist/types/dom/views/timegrid.d.cts +54 -0
  45. package/dist/types/dom/views/timegrid.d.ts +54 -0
  46. package/dist/types/dom/views/timeline.d.cts +83 -0
  47. package/dist/types/dom/views/timeline.d.ts +83 -0
  48. package/dist/types/dom/views/year.d.cts +26 -0
  49. package/dist/types/dom/views/year.d.ts +26 -0
  50. package/dist/types/index.d.cts +6 -0
  51. package/dist/types/index.d.ts +6 -0
  52. package/dist/types/react/index.d.cts +20 -0
  53. package/dist/types/react/index.d.ts +20 -0
  54. package/dist/types/tasknotes/bases.d.cts +50 -0
  55. package/dist/types/tasknotes/bases.d.ts +50 -0
  56. package/package.json +108 -5
@@ -0,0 +1,3435 @@
1
+ import {
2
+ DAY_MS,
3
+ ItemStore,
4
+ MINUTES_PER_DAY,
5
+ addMonths,
6
+ dayOf,
7
+ daysInMonth,
8
+ dependencyConflicts,
9
+ dependencyType,
10
+ formatDay,
11
+ formatFloating,
12
+ getTimeZone,
13
+ isoWeekNumber,
14
+ itemDependencies,
15
+ layoutLane,
16
+ layoutRow,
17
+ layoutTimedDay,
18
+ parseDateInput,
19
+ parseDay,
20
+ parseDurationMinutes,
21
+ parseOrder,
22
+ plannedMinutes,
23
+ startOfMonth,
24
+ startOfWeek,
25
+ startOfYear,
26
+ toInstant,
27
+ wallTime,
28
+ weekdayOf,
29
+ ymdOfDay
30
+ } from "./chunk-DVMZ5OXN.js";
31
+
32
+ // src/dom/format.ts
33
+ var Formatter = class {
34
+ cache = /* @__PURE__ */ new Map();
35
+ locale;
36
+ hour12;
37
+ constructor(locale, hour12) {
38
+ this.locale = locale;
39
+ const probe = new Intl.DateTimeFormat(locale, { hour: "numeric" }).resolvedOptions();
40
+ this.hour12 = hour12 ?? (probe.hour12 === true || probe.hourCycle === "h12" || probe.hourCycle === "h11");
41
+ }
42
+ fmt(key, opts) {
43
+ let f = this.cache.get(key);
44
+ if (!f) {
45
+ f = new Intl.DateTimeFormat(this.locale, { timeZone: "UTC", ...opts });
46
+ this.cache.set(key, f);
47
+ }
48
+ return f;
49
+ }
50
+ d(day) {
51
+ return new Date(day * DAY_MS);
52
+ }
53
+ weekdayShort(day) {
54
+ return this.fmt("wds", { weekday: "short" }).format(this.d(day));
55
+ }
56
+ weekdayLong(day) {
57
+ return this.fmt("wdl", { weekday: "long" }).format(this.d(day));
58
+ }
59
+ weekdayNarrow(day) {
60
+ return this.fmt("wdn", { weekday: "narrow" }).format(this.d(day));
61
+ }
62
+ dayNumber(day) {
63
+ return this.fmt("dn", { day: "numeric" }).format(this.d(day));
64
+ }
65
+ monthShort(day) {
66
+ return this.fmt("ms", { month: "short" }).format(this.d(day));
67
+ }
68
+ monthLong(day) {
69
+ return this.fmt("ml", { month: "long" }).format(this.d(day));
70
+ }
71
+ monthYear(day) {
72
+ return this.fmt("my", { month: "long", year: "numeric" }).format(this.d(day));
73
+ }
74
+ year(day) {
75
+ return this.fmt("y", { year: "numeric" }).format(this.d(day));
76
+ }
77
+ fullDate(day) {
78
+ return this.fmt("fd", { year: "numeric", month: "long", day: "numeric" }).format(this.d(day));
79
+ }
80
+ longDate(day) {
81
+ return this.fmt("ld", { weekday: "long", year: "numeric", month: "long", day: "numeric" }).format(this.d(day));
82
+ }
83
+ listDay(day) {
84
+ return this.fmt("lst", { weekday: "short", month: "short", day: "numeric" }).format(this.d(day));
85
+ }
86
+ /** "Oct 19 – 25, 2026" style. */
87
+ range(startDay, lastDay) {
88
+ if (startDay === lastDay) return this.fullDate(startDay);
89
+ const f = this.fmt("rng", { year: "numeric", month: "short", day: "numeric" });
90
+ try {
91
+ return f.formatRange(this.d(startDay), this.d(lastDay));
92
+ } catch {
93
+ return `${f.format(this.d(startDay))} \u2013 ${f.format(this.d(lastDay))}`;
94
+ }
95
+ }
96
+ /** Wall-clock minutes to a label. `compact` drops ":00" in 12-hour formats ("9am"). */
97
+ time(minute, compact = false) {
98
+ const m = (Math.round(minute) % 1440 + 1440) % 1440;
99
+ const date = new Date(Date.UTC(2e3, 0, 1, Math.floor(m / 60), m % 60));
100
+ if (this.hour12) {
101
+ const parts = this.fmt("t12", { hour: "numeric", minute: "2-digit", hour12: true }).formatToParts(date);
102
+ const english = (this.fmt("t12", {}).resolvedOptions().locale ?? "en").startsWith("en");
103
+ const out = [];
104
+ parts.forEach((p, i) => {
105
+ const prev = parts[i - 1];
106
+ if (compact && p.type === "minute" && p.value === "00") {
107
+ if (prev?.type === "literal") out.pop();
108
+ return;
109
+ }
110
+ if (compact && english && p.type === "dayPeriod") {
111
+ if (prev?.type === "literal" && /^\s+$/.test(prev.value)) out.pop();
112
+ out.push(p.value.toLowerCase().replace(/\./g, ""));
113
+ return;
114
+ }
115
+ out.push(p.value);
116
+ });
117
+ return out.join("").trim();
118
+ }
119
+ return this.fmt("t24", { hour: "2-digit", minute: "2-digit", hourCycle: "h23" }).format(date);
120
+ }
121
+ /** Hour labels for the gutter. */
122
+ slotLabel(minute) {
123
+ return this.time(minute, true);
124
+ }
125
+ isoDate(day) {
126
+ const { year, month, day: d } = ymdOfDay(day);
127
+ return `${year}-${String(month).padStart(2, "0")}-${String(d).padStart(2, "0")}`;
128
+ }
129
+ };
130
+ function localeFirstDay(locale) {
131
+ try {
132
+ const loc = new Intl.Locale(locale ?? new Intl.DateTimeFormat().resolvedOptions().locale);
133
+ const info = loc.getWeekInfo?.() ?? loc.weekInfo;
134
+ if (info) return info.firstDay % 7;
135
+ } catch {
136
+ }
137
+ return 0;
138
+ }
139
+
140
+ // src/dom/options.ts
141
+ var TIMELINE_UNIT_DAYS = {
142
+ hour: 1 / 24,
143
+ day: 1,
144
+ week: 7,
145
+ month: 30.44,
146
+ quarter: 91.31
147
+ };
148
+ var DEFAULT_TEXT = {
149
+ today: "Today",
150
+ prev: "Previous",
151
+ next: "Next",
152
+ allDay: "All day",
153
+ noItems: "Nothing scheduled",
154
+ more: (n) => `+${n} more`,
155
+ itemCount: (n) => n === 1 ? "1 item" : `${n} items`,
156
+ weekNumber: (n) => `W${n}`,
157
+ views: { week: "Week", day: "Day", days: "Days", month: "Month", list: "List", year: "Year", timeline: "Timeline" },
158
+ viewHints: {}
159
+ };
160
+ function defaultOptions() {
161
+ return {
162
+ view: "week",
163
+ date: Date.now(),
164
+ dayCount: 3,
165
+ listDayCount: 7,
166
+ listStart: "date",
167
+ monthDisplay: "full",
168
+ timeZone: "local",
169
+ locale: void 0,
170
+ firstDay: void 0,
171
+ hour12: void 0,
172
+ weekNumbers: false,
173
+ weekends: true,
174
+ hiddenDays: [],
175
+ allDaySlot: true,
176
+ timeGrid: true,
177
+ todayHighlight: true,
178
+ todayWidth: 1,
179
+ slotMinTime: "00:00",
180
+ slotMaxTime: "24:00",
181
+ slotDuration: "00:30",
182
+ snapDuration: void 0,
183
+ scrollTime: "08:00",
184
+ slotHeight: 24,
185
+ expandRows: false,
186
+ nowIndicator: true,
187
+ selectMirror: true,
188
+ slotEventOverlap: true,
189
+ eventMinHeight: 15,
190
+ eventMaxStack: 0,
191
+ dayMaxEvents: true,
192
+ dayMaxEventRows: 0,
193
+ eventOrder: "start,-duration,allDay,title",
194
+ height: "fill",
195
+ editable: true,
196
+ selectable: true,
197
+ navLinks: true,
198
+ resizableFromStart: false,
199
+ longPressDelay: 350,
200
+ defaultTimedMinutes: 60,
201
+ defaultAllDayDays: 1,
202
+ capacityMinutes: null,
203
+ planning: false,
204
+ toolbar: true,
205
+ text: {},
206
+ timeline: null,
207
+ acceptNativeDrop: null,
208
+ selected: null
209
+ };
210
+ }
211
+ var LAYOUT_KEYS = /* @__PURE__ */ new Set([
212
+ "view",
213
+ "dayCount",
214
+ "listDayCount",
215
+ "listStart",
216
+ "monthDisplay",
217
+ "timeZone",
218
+ "locale",
219
+ "firstDay",
220
+ "hour12",
221
+ "weekNumbers",
222
+ "weekends",
223
+ "hiddenDays",
224
+ "allDaySlot",
225
+ "timeGrid",
226
+ "todayHighlight",
227
+ "todayWidth",
228
+ "slotMinTime",
229
+ "slotMaxTime",
230
+ "slotDuration",
231
+ "snapDuration",
232
+ "slotHeight",
233
+ "expandRows",
234
+ "nowIndicator",
235
+ "slotEventOverlap",
236
+ "eventMinHeight",
237
+ "eventMaxStack",
238
+ "dayMaxEvents",
239
+ "dayMaxEventRows",
240
+ "eventOrder",
241
+ "height",
242
+ "capacityMinutes",
243
+ "planning",
244
+ "toolbar",
245
+ "text",
246
+ "timeline",
247
+ "editable",
248
+ "itemContent",
249
+ "itemClassNames",
250
+ "listItemContent",
251
+ "dayHeaderContent",
252
+ "dayClassNames"
253
+ ]);
254
+
255
+ // src/dom/context.ts
256
+ function focusKey(el) {
257
+ const cls = [...el.classList].filter((c) => c.startsWith("tnc-"));
258
+ return cls.length ? cls.map((c) => `.${CSS.escape(c)}`).join("") : "";
259
+ }
260
+ var ItemPool = class {
261
+ constructor(ctx, container) {
262
+ this.ctx = ctx;
263
+ this.container = container;
264
+ }
265
+ ctx;
266
+ container;
267
+ recs = /* @__PURE__ */ new Map();
268
+ used = /* @__PURE__ */ new Set();
269
+ begin() {
270
+ this.used = /* @__PURE__ */ new Set();
271
+ }
272
+ place(key, meta) {
273
+ const sig = `${meta.n.sig}|${meta.variant}|${meta.isStart ? 1 : 0}${meta.isEnd ? 1 : 0}|${meta.mirror ? 1 : 0}`;
274
+ let rec = this.recs.get(key);
275
+ let refocus = null;
276
+ if (rec && rec.sig !== sig) {
277
+ const active = this.ctx.doc.activeElement;
278
+ if (active && rec.el.contains(active)) refocus = active === rec.el ? "" : focusKey(active);
279
+ this.ctx.destroyItemEl(rec.el);
280
+ rec = void 0;
281
+ }
282
+ if (!rec) {
283
+ rec = { el: this.ctx.createItemEl(meta), sig };
284
+ this.recs.set(key, rec);
285
+ this.container.append(rec.el);
286
+ if (refocus !== null) (refocus ? rec.el.querySelector(refocus) : rec.el)?.focus({ preventScroll: true });
287
+ } else {
288
+ this.ctx.meta.set(rec.el, meta);
289
+ this.ctx.refreshItemEl(rec.el, meta);
290
+ if (rec.el.parentNode !== this.container) this.container.append(rec.el);
291
+ }
292
+ this.used.add(key);
293
+ return rec.el;
294
+ }
295
+ end() {
296
+ for (const [key, rec] of this.recs) {
297
+ if (this.used.has(key)) continue;
298
+ this.ctx.destroyItemEl(rec.el);
299
+ this.recs.delete(key);
300
+ }
301
+ }
302
+ clear() {
303
+ this.begin();
304
+ this.end();
305
+ }
306
+ elementsFor(id) {
307
+ const out = [];
308
+ for (const rec of this.recs.values()) if (this.ctx.meta.get(rec.el)?.n.id === id) out.push(rec.el);
309
+ return out;
310
+ }
311
+ };
312
+
313
+ // src/dom/dom.ts
314
+ function h(doc, tag, cls, attrs, ...children) {
315
+ const el = doc.createElement(tag);
316
+ if (cls) el.className = cls;
317
+ if (attrs) setAttrs(el, attrs);
318
+ for (const c of children) if (c !== null && c !== void 0 && c !== false) el.append(c);
319
+ return el;
320
+ }
321
+ function setAttrs(el, attrs) {
322
+ for (const [k, v] of Object.entries(attrs)) {
323
+ if (v === null || v === void 0 || v === false) el.removeAttribute(k);
324
+ else el.setAttribute(k, v === true ? "" : String(v));
325
+ }
326
+ }
327
+ function classList(value) {
328
+ if (!value) return [];
329
+ return (Array.isArray(value) ? value : value.split(/\s+/)).filter(Boolean);
330
+ }
331
+ function setStyle(el, props) {
332
+ for (const [k, v] of Object.entries(props)) {
333
+ if (v === null) el.style.removeProperty(k);
334
+ else el.style.setProperty(k, typeof v === "number" ? `${v}px` : v);
335
+ }
336
+ }
337
+ function pct(n) {
338
+ return `${Math.round(n * 1e4) / 100}%`;
339
+ }
340
+
341
+ // src/dom/views/daygrid.ts
342
+ var BAR_GAP = 2;
343
+ var CELL_PAD = 6;
344
+ function px(cs, name, fallback) {
345
+ const v = parseFloat(cs.getPropertyValue(name));
346
+ return Number.isFinite(v) ? v : fallback;
347
+ }
348
+ var DayGridView = class {
349
+ constructor(ctx) {
350
+ this.ctx = ctx;
351
+ const { doc, opts, days } = ctx;
352
+ this.dotsMode = opts.monthDisplay === "dots";
353
+ this.el = h(doc, "div", this.dotsMode ? "tnc-dg tnc-dg--dots" : "tnc-dg");
354
+ this.scroller = h(doc, "div", "tnc-dg-scroll");
355
+ this.grid = h(doc, "div", "tnc-dg-grid", { role: "grid", "aria-label": ctx.view.title });
356
+ const perWeek = this.daysPerWeek();
357
+ const cols = `${opts.weekNumbers ? "var(--tnc-weeknum-w) " : ""}repeat(${perWeek}, minmax(0, 1fr))`;
358
+ this.el.style.setProperty("--tnc-cols", cols);
359
+ const head = h(doc, "div", "tnc-dg-head", { role: "row" });
360
+ if (opts.weekNumbers) head.append(h(doc, "div", "tnc-weeknum-head"));
361
+ for (const d of days.slice(0, perWeek)) {
362
+ const arg = ctx.dayArg(d);
363
+ const cell = h(doc, "div", "tnc-dg-dow", { role: "columnheader", "aria-label": ctx.fmt.weekdayLong(d) });
364
+ const custom = opts.dayHeaderContent?.(arg);
365
+ cell.append(custom ?? ctx.fmt.weekdayShort(d));
366
+ head.append(cell);
367
+ opts.dayHeaderDidMount?.({ ...arg, el: cell });
368
+ }
369
+ this.grid.append(head);
370
+ const anchorMonth = ymdOfDay(ctx.view.anchor).month;
371
+ for (let i = 0; i < days.length; i += perWeek) {
372
+ const wdays = days.slice(i, i + perWeek);
373
+ const row = h(doc, "div", "tnc-dg-week", { role: "rowgroup" });
374
+ const dates = h(doc, "div", "tnc-dg-dates", { role: "row" });
375
+ if (opts.weekNumbers) dates.append(h(doc, "div", "tnc-weeknum", { role: "rowheader" }, ctx.text.weekNumber(isoWeekNumber(wdays[0]))));
376
+ const cells = wdays.map((d) => {
377
+ const arg = ctx.dayArg(d);
378
+ const cls = ["tnc-dg-day", ...ctx.dayClasses(d)];
379
+ if (ymdOfDay(d).month !== anchorMonth) cls.push("is-other-month");
380
+ const cell = h(doc, "div", cls.join(" "), { role: "gridcell", "aria-label": ctx.fmt.longDate(d), "data-date": arg.dateStr });
381
+ if (this.dotsMode) cell.tabIndex = 0;
382
+ const num = ctx.fmt.dayNumber(d);
383
+ const label = ymdOfDay(d).day === 1 ? `${ctx.fmt.monthShort(d)} ${num}` : num;
384
+ cell.append(
385
+ h(
386
+ doc,
387
+ "div",
388
+ "tnc-dg-top",
389
+ null,
390
+ opts.navLinks ? ctx.navLink(d, label, "tnc-dg-num") : h(doc, "span", "tnc-dg-num", null, label)
391
+ )
392
+ );
393
+ dates.append(cell);
394
+ return cell;
395
+ });
396
+ const layer = h(doc, "div", "tnc-dg-layer", { role: "row" });
397
+ const content = h(doc, "div", "tnc-dg-content", { role: "gridcell", "aria-colspan": String(perWeek), "aria-label": ctx.fmt.range(wdays[0], wdays[wdays.length - 1]) });
398
+ const sel = h(doc, "div", "tnc-sel-layer");
399
+ const more = h(doc, "div", "tnc-more-layer");
400
+ content.append(sel, more);
401
+ layer.append(content);
402
+ row.append(dates, layer);
403
+ this.grid.append(row);
404
+ const dots = this.dotsMode ? cells.map((cell) => {
405
+ const box = h(doc, "div", "tnc-dg-dots", { "aria-hidden": "true" });
406
+ if (opts.dayMaxEvents === false) box.classList.add("is-unlimited");
407
+ const count = h(doc, "span", "tnc-dg-dots-more");
408
+ cell.append(box);
409
+ return { box, pool: new ItemPool(ctx, box), count };
410
+ }) : null;
411
+ this.weeks.push({ days: wdays, row, cells, layer, sel, more, pool: new ItemPool(ctx, content), dots });
412
+ }
413
+ this.scroller.append(this.grid);
414
+ this.el.append(this.scroller);
415
+ this.el.classList.toggle("tnc-dg--auto", opts.height === "auto");
416
+ const RO = ctx.win.ResizeObserver;
417
+ if (RO && (opts.dayMaxEvents === true || this.dotsMode && opts.dayMaxEvents !== false)) {
418
+ this.resizeObs = new RO(() => {
419
+ if (this.sizeKey() !== this.lastHeights) this.render(null);
420
+ });
421
+ this.resizeObs.observe(this.grid);
422
+ }
423
+ }
424
+ ctx;
425
+ el;
426
+ scroller;
427
+ grid;
428
+ weeks = [];
429
+ resizeObs = null;
430
+ barPx = 20;
431
+ morePx = 20;
432
+ topPx = 24;
433
+ lastHeights = "";
434
+ dotsMode;
435
+ daysPerWeek() {
436
+ const { days } = this.ctx;
437
+ const first = days[0];
438
+ if (first === void 0) return 7;
439
+ let n = 0;
440
+ while (n < days.length && days[n] < first + 7) n++;
441
+ return n || 7;
442
+ }
443
+ sizeKey() {
444
+ return `${this.grid.clientWidth}|${this.weeks.map((w) => w.row.clientHeight).join(",")}`;
445
+ }
446
+ render(_change) {
447
+ const { ctx } = this;
448
+ const cs = ctx.win.getComputedStyle(this.el);
449
+ this.barPx = px(cs, "--tnc-bar-h", 20);
450
+ this.topPx = this.weeks[0]?.cells[0]?.querySelector(".tnc-dg-top")?.offsetHeight || px(cs, "--tnc-dg-top-h", 24);
451
+ this.morePx = this.measureMore(cs);
452
+ this.lastHeights = this.sizeKey();
453
+ for (const w of this.weeks) {
454
+ if (w.dots) this.renderDots(w);
455
+ else this.renderWeek(w);
456
+ }
457
+ }
458
+ /** Height of a "+N more" link as the host styles it (`--tnc-more-h`, or a taller min-height). */
459
+ measureMore(cs) {
460
+ const fallback = px(cs, "--tnc-more-h", this.barPx);
461
+ const layer = this.weeks[0]?.more;
462
+ if (this.dotsMode || !layer) return fallback;
463
+ const probe = h(this.ctx.doc, "button", "tnc-more", { type: "button", tabindex: "-1", "aria-hidden": "true" }, "+0");
464
+ probe.style.visibility = "hidden";
465
+ layer.append(probe);
466
+ const measured = probe.offsetHeight;
467
+ probe.remove();
468
+ return Math.max(fallback, measured);
469
+ }
470
+ limitFor(w) {
471
+ const { dayMaxEvents, dayMaxEventRows, height } = this.ctx.opts;
472
+ const rows = (n) => ({ fit: n, withMore: n - 1 });
473
+ if (dayMaxEventRows > 0) return rows(dayMaxEventRows);
474
+ if (typeof dayMaxEvents === "number" && dayMaxEvents > 0) return rows(dayMaxEvents + 1);
475
+ if (dayMaxEvents !== true) return null;
476
+ if (height === "auto") return rows(5);
477
+ const step = this.barPx + BAR_GAP;
478
+ const avail = w.row.clientHeight - this.topPx - CELL_PAD;
479
+ return {
480
+ fit: Math.max(1, Math.floor((avail + BAR_GAP) / step)),
481
+ withMore: Math.max(0, Math.floor((avail - this.morePx) / step))
482
+ };
483
+ }
484
+ itemsFor(w) {
485
+ const { store } = this.ctx;
486
+ const first = w.days[0];
487
+ const last = w.days[w.days.length - 1];
488
+ const out = store.spanningIn(first, last + 1, true).filter((n) => n.item.display !== "background");
489
+ const seen = new Set(out.map((n) => n.id));
490
+ for (const d of w.days) {
491
+ for (const n of store.timedOn(d)) {
492
+ if (seen.has(n.id) || n.item.display === "background") continue;
493
+ seen.add(n.id);
494
+ out.push(n);
495
+ }
496
+ }
497
+ return out;
498
+ }
499
+ renderWeek(w) {
500
+ const { ctx } = this;
501
+ const row = layoutRow(this.itemsFor(w), w.days, ctx.order);
502
+ const limit = this.limitFor(w);
503
+ const visible = (col) => !limit ? Infinity : (row.countByCol[col] ?? 0) > limit.fit ? limit.withMore : limit.fit;
504
+ const n = w.days.length;
505
+ const hidden = new Array(n).fill([]).map(() => []);
506
+ w.pool.begin();
507
+ let maxLevel = -1;
508
+ for (const p of row.placements) {
509
+ let level = p.level;
510
+ let room = Infinity;
511
+ for (let c = p.startCol; c < p.endCol; c++) room = Math.min(room, visible(c));
512
+ if (level >= room) {
513
+ if (p.item.id !== ctx.draggingId) {
514
+ for (let c = p.startCol; c < p.endCol; c++) hidden[c].push(p.item);
515
+ continue;
516
+ }
517
+ level = Math.max(0, room - 1);
518
+ }
519
+ maxLevel = Math.max(maxLevel, level);
520
+ const single = !p.item.allDay && p.item.endDay - p.item.startDay <= 1;
521
+ const el = w.pool.place(p.item.id, {
522
+ n: p.item,
523
+ day: w.days[p.startCol],
524
+ isStart: p.isStart,
525
+ isEnd: p.isEnd,
526
+ variant: single ? "dot" : "bar",
527
+ mirror: false
528
+ });
529
+ setStyle(el, {
530
+ left: `calc(${pct(p.startCol / n)} + 2px)`,
531
+ width: `calc(${pct((p.endCol - p.startCol) / n)} - 4px)`,
532
+ top: `calc(var(--tnc-dg-top-h) + ${level} * (var(--tnc-bar-h) + ${BAR_GAP}px))`
533
+ });
534
+ }
535
+ w.pool.end();
536
+ w.more.replaceChildren();
537
+ hidden.forEach((items, col) => {
538
+ if (!items.length) return;
539
+ const day = w.days[col];
540
+ const link = h(ctx.doc, "button", "tnc-more", { type: "button", "aria-haspopup": "dialog" }, ctx.text.more(items.length));
541
+ setStyle(link, {
542
+ left: `calc(${pct(col / n)} + 2px)`,
543
+ width: `calc(${pct(1 / n)} - 4px)`,
544
+ top: `calc(var(--tnc-dg-top-h) + ${visible(col)} * (var(--tnc-bar-h) + ${BAR_GAP}px))`
545
+ });
546
+ link.addEventListener("click", (ev) => ctx.openMore(day, link, ev));
547
+ w.more.append(link);
548
+ });
549
+ const step = this.barPx + BAR_GAP;
550
+ let need = maxLevel >= 0 ? (maxLevel + 1) * step - BAR_GAP : 0;
551
+ hidden.forEach((items, col) => {
552
+ if (items.length) need = Math.max(need, visible(col) * step + this.morePx);
553
+ });
554
+ w.row.style.setProperty("--tnc-levels", String(maxLevel + 1));
555
+ w.row.style.setProperty("--tnc-dg-need", `${this.topPx + need + CELL_PAD}px`);
556
+ }
557
+ /** Items touching one day, all-day first, as a dot cell lists them. */
558
+ itemsOn(day) {
559
+ const { store } = this.ctx;
560
+ const out = store.spanningIn(day, day + 1, true);
561
+ const seen = new Set(out.map((n) => n.id));
562
+ for (const n of store.timedOn(day)) if (!seen.has(n.id)) out.push(n);
563
+ return out.filter((n) => n.item.display !== "background" && n.item.display !== "none").sort((a, b) => Number(b.allDay) - Number(a.allDay) || this.ctx.order(a, b));
564
+ }
565
+ /** `monthDisplay: "dots"`: each cell wraps one dot per item, then "+N" for the rest. */
566
+ renderDots(w) {
567
+ const { ctx } = this;
568
+ const { dayMaxEvents } = ctx.opts;
569
+ const first = w.dots[0].box;
570
+ const cs = ctx.win.getComputedStyle(first);
571
+ const size = px(cs, "--tnc-dot-size", 8);
572
+ const gap = px(cs, "--tnc-dot-gap", 3);
573
+ const step = size + gap;
574
+ const perRow = Math.max(1, Math.floor((first.clientWidth + gap) / step));
575
+ const rows = Math.max(1, Math.floor((first.clientHeight + gap) / step));
576
+ const fits = dayMaxEvents === false ? Infinity : perRow * rows;
577
+ const cap = typeof dayMaxEvents === "number" && dayMaxEvents > 0 ? Math.min(dayMaxEvents, fits) : fits;
578
+ w.row.style.setProperty("--tnc-levels", "0");
579
+ w.row.style.removeProperty("--tnc-dg-need");
580
+ w.days.forEach((day, i) => {
581
+ const cell = w.dots[i];
582
+ const items = this.itemsOn(day);
583
+ const shown = items.length <= cap ? items.length : cap >= fits ? Math.max(0, cap - 2) : cap;
584
+ cell.pool.begin();
585
+ const els = items.slice(0, shown).map((n) => cell.pool.place(n.id, { n, day, isStart: n.startDay >= day, isEnd: n.endDay <= day + 1, variant: "indicator", mirror: false }));
586
+ cell.pool.end();
587
+ els.forEach((el, k) => {
588
+ if (cell.box.children[k] !== el) cell.box.insertBefore(el, cell.box.children[k] ?? null);
589
+ });
590
+ const rest = items.length - shown;
591
+ cell.count.textContent = rest > 0 ? `+${rest}` : "";
592
+ if (rest > 0) cell.box.append(cell.count);
593
+ else cell.count.remove();
594
+ const label = ctx.fmt.longDate(day);
595
+ w.cells[i].setAttribute("aria-label", items.length ? `${label}, ${ctx.text.itemCount(items.length)}` : label);
596
+ });
597
+ }
598
+ showSelection(sel) {
599
+ for (const w of this.weeks) {
600
+ w.sel.replaceChildren();
601
+ if (!sel) continue;
602
+ const n = w.days.length;
603
+ const startCol = w.days.findIndex((d) => d >= sel.startDay);
604
+ if (startCol < 0) continue;
605
+ let endCol = w.days.findIndex((d) => d >= sel.endDay);
606
+ if (endCol < 0) endCol = n;
607
+ if (endCol <= startCol) continue;
608
+ const box = h(this.ctx.doc, "div", "tnc-sel");
609
+ setStyle(box, { left: pct(startCol / n), width: pct((endCol - startCol) / n), top: "0", bottom: "0" });
610
+ w.sel.append(box);
611
+ }
612
+ }
613
+ hit(x, y) {
614
+ for (const w of this.weeks) {
615
+ const r = w.row.getBoundingClientRect();
616
+ if (y < r.top || y >= r.bottom) continue;
617
+ for (let i = 0; i < w.cells.length; i++) {
618
+ const cr = w.cells[i].getBoundingClientRect();
619
+ if (x >= cr.left && x < cr.right) return { kind: "day", day: w.days[i] };
620
+ }
621
+ }
622
+ return null;
623
+ }
624
+ getScroll() {
625
+ return { top: this.scroller.scrollTop, left: 0 };
626
+ }
627
+ setScroll(s) {
628
+ this.scroller.scrollTop = s.top;
629
+ }
630
+ elementsFor(id) {
631
+ return this.weeks.flatMap((w) => [...w.pool.elementsFor(id), ...(w.dots ?? []).flatMap((c) => c.pool.elementsFor(id))]);
632
+ }
633
+ destroy() {
634
+ this.resizeObs?.disconnect();
635
+ for (const w of this.weeks) {
636
+ w.pool.clear();
637
+ for (const c of w.dots ?? []) c.pool.clear();
638
+ }
639
+ this.el.remove();
640
+ }
641
+ };
642
+
643
+ // src/dom/views/list.ts
644
+ var ListView = class {
645
+ constructor(ctx) {
646
+ this.ctx = ctx;
647
+ this.el = h(ctx.doc, "div", "tnc-list");
648
+ this.scroller = h(ctx.doc, "div", "tnc-list-scroll");
649
+ this.body = h(ctx.doc, "div", "tnc-list-body", { role: "list", "aria-label": ctx.view.title });
650
+ this.scroller.append(this.body);
651
+ this.el.append(this.scroller);
652
+ }
653
+ ctx;
654
+ el;
655
+ scroller;
656
+ body;
657
+ pools = /* @__PURE__ */ new Map();
658
+ sections = /* @__PURE__ */ new Map();
659
+ itemsOn(day) {
660
+ const { store, opts } = this.ctx;
661
+ const out = store.spanningIn(day, day + 1, true);
662
+ const seen = new Set(out.map((n) => n.id));
663
+ for (const n of store.timedOn(day)) if (!seen.has(n.id)) out.push(n);
664
+ 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));
665
+ }
666
+ render(_change) {
667
+ const { ctx } = this;
668
+ const { doc, days } = ctx;
669
+ const keep = /* @__PURE__ */ new Set();
670
+ let prev = null;
671
+ for (const day of days) {
672
+ const items = this.itemsOn(day);
673
+ if (!items.length) continue;
674
+ keep.add(day);
675
+ let section = this.sections.get(day);
676
+ let pool = this.pools.get(day);
677
+ if (!section || !pool) {
678
+ const arg = ctx.dayArg(day);
679
+ const heading = h(doc, "div", ["tnc-list-day", ...ctx.dayClasses(day)].join(" "), { "data-date": arg.dateStr });
680
+ const custom = ctx.opts.dayHeaderContent?.(arg);
681
+ const label = custom ?? ctx.fmt.longDate(day);
682
+ heading.append(ctx.opts.navLinks ? ctx.navLink(day, label, "tnc-list-day-link") : label);
683
+ const rows2 = h(doc, "div", "tnc-list-rows", { role: "group" });
684
+ section = h(doc, "div", "tnc-list-section", { role: "listitem", "aria-label": ctx.fmt.longDate(day), "data-date": arg.dateStr }, heading, rows2);
685
+ ctx.opts.dayHeaderDidMount?.({ ...arg, el: heading });
686
+ pool = new ItemPool(ctx, rows2);
687
+ this.sections.set(day, section);
688
+ this.pools.set(day, pool);
689
+ }
690
+ pool.begin();
691
+ for (const n of items) {
692
+ pool.place(n.id, {
693
+ n,
694
+ day,
695
+ isStart: n.startDay === day,
696
+ isEnd: n.endDay === day + 1,
697
+ variant: "list",
698
+ mirror: false
699
+ });
700
+ }
701
+ pool.end();
702
+ const rows = pool.container;
703
+ for (const n of items) {
704
+ const el = pool.elementsFor(n.id)[0];
705
+ if (el) rows.append(el);
706
+ }
707
+ if (prev ? prev.nextSibling !== section : this.body.firstChild !== section) {
708
+ if (prev) prev.after(section);
709
+ else this.body.prepend(section);
710
+ }
711
+ prev = section;
712
+ }
713
+ for (const [day, section] of this.sections) {
714
+ if (keep.has(day)) continue;
715
+ this.pools.get(day)?.clear();
716
+ this.pools.delete(day);
717
+ section.remove();
718
+ this.sections.delete(day);
719
+ }
720
+ let empty = this.body.querySelector(":scope > .tnc-list-empty");
721
+ if (!keep.size) {
722
+ if (!empty) {
723
+ empty = h(doc, "div", "tnc-list-empty", { role: "listitem" }, ctx.text.noItems);
724
+ this.body.append(empty);
725
+ }
726
+ } else empty?.remove();
727
+ }
728
+ showSelection(_sel) {
729
+ }
730
+ hit(_x, _y) {
731
+ return null;
732
+ }
733
+ getScroll() {
734
+ return { top: this.scroller.scrollTop, left: 0 };
735
+ }
736
+ setScroll(s) {
737
+ this.scroller.scrollTop = s.top;
738
+ }
739
+ elementsFor(id) {
740
+ return [...this.pools.values()].flatMap((p) => p.elementsFor(id));
741
+ }
742
+ destroy() {
743
+ for (const p of this.pools.values()) p.clear();
744
+ this.el.remove();
745
+ }
746
+ };
747
+
748
+ // src/dom/views/timegrid.ts
749
+ var BAR_GAP2 = 2;
750
+ var TimeGridView = class {
751
+ constructor(ctx) {
752
+ this.ctx = ctx;
753
+ const { doc, opts } = ctx;
754
+ const showGrid = opts.timeGrid;
755
+ this.el = h(doc, "div", "tnc-tg", { role: "table", "aria-label": ctx.view.title });
756
+ this.el.classList.toggle("tnc-tg--no-grid", !showGrid);
757
+ this.scroller = h(doc, "div", "tnc-tg-scroll");
758
+ this.top = h(doc, "div", "tnc-tg-top");
759
+ this.headRow = h(doc, "div", "tnc-tg-head tnc-tg-cols", { role: "row" });
760
+ this.top.append(this.headRow);
761
+ if (opts.allDaySlot || !showGrid) {
762
+ this.allDayRow = h(doc, "div", "tnc-tg-allday tnc-tg-cols", { role: "row" });
763
+ const label = h(doc, "div", "tnc-gutter-label", { role: "rowheader" }, showGrid ? ctx.text.allDay : "");
764
+ this.allDayLayer = h(doc, "div", "tnc-allday-layer", { role: "cell", "aria-colspan": String(ctx.days.length) });
765
+ this.allDaySel = h(doc, "div", "tnc-sel-layer");
766
+ this.allDayMore = h(doc, "div", "tnc-more-layer");
767
+ this.allDayLayer.append(this.allDaySel, this.allDayMore);
768
+ this.allDayRow.append(label, this.allDayLayer);
769
+ this.allDayPool = new ItemPool(ctx, this.allDayLayer);
770
+ this.top.append(this.allDayRow);
771
+ } else {
772
+ this.allDayRow = this.allDayLayer = this.allDaySel = this.allDayMore = null;
773
+ this.allDayPool = null;
774
+ }
775
+ this.scroller.append(this.top);
776
+ if (showGrid) {
777
+ this.body = h(doc, "div", "tnc-tg-body tnc-tg-cols", { role: "row" });
778
+ this.gutter = h(doc, "div", "tnc-gutter", { "aria-hidden": "true" });
779
+ this.body.append(this.gutter);
780
+ this.nowGutter = h(doc, "div", "tnc-now-label");
781
+ this.scroller.append(this.body);
782
+ } else {
783
+ this.body = this.gutter = null;
784
+ }
785
+ this.el.append(this.scroller);
786
+ this.build();
787
+ if (showGrid && opts.height === "fill") {
788
+ const RO = ctx.win.ResizeObserver;
789
+ if (RO) {
790
+ this.resizeObs = new RO(() => this.fitRows());
791
+ this.resizeObs.observe(this.scroller);
792
+ }
793
+ }
794
+ }
795
+ ctx;
796
+ el;
797
+ scroller;
798
+ top;
799
+ headRow;
800
+ allDayRow;
801
+ allDayLayer;
802
+ allDaySel;
803
+ allDayMore;
804
+ allDayPool;
805
+ body;
806
+ gutter;
807
+ nowGutter = null;
808
+ cols = [];
809
+ weights = [];
810
+ ppm = 1;
811
+ resizeObs = null;
812
+ get minutes() {
813
+ return this.ctx.slot.max - this.ctx.slot.min;
814
+ }
815
+ /* ------------------------------------------------------------ structure */
816
+ build() {
817
+ const { ctx } = this;
818
+ const { doc, opts, days, today } = ctx;
819
+ this.weights = days.map((d) => d === today && opts.todayWidth > 1 && days.length > 1 ? opts.todayWidth : 1);
820
+ const tpl = `var(--tnc-gutter-w) ${this.weights.map((w) => `minmax(var(--tnc-col-min), ${w}fr)`).join(" ")}`;
821
+ this.el.style.setProperty("--tnc-cols", tpl);
822
+ this.el.style.setProperty("--tnc-ncols", String(days.length));
823
+ const gutterHead = h(doc, "div", "tnc-gutter-head", { role: "columnheader" });
824
+ if (opts.weekNumbers && days[0] !== void 0) gutterHead.textContent = ctx.text.weekNumber(isoWeekNumber(days[0]));
825
+ this.headRow.replaceChildren(gutterHead);
826
+ const capacity = opts.capacityMinutes;
827
+ this.cols = days.map((day) => {
828
+ const arg = ctx.dayArg(day);
829
+ const head = h(doc, "div", ["tnc-dh", ...ctx.dayClasses(day)].join(" "), {
830
+ role: "columnheader",
831
+ "data-date": arg.dateStr,
832
+ "aria-label": ctx.fmt.longDate(day)
833
+ });
834
+ const custom = opts.dayHeaderContent?.(arg);
835
+ if (custom != null) head.append(custom);
836
+ else {
837
+ const label = h(
838
+ doc,
839
+ "span",
840
+ "tnc-dh-label",
841
+ null,
842
+ h(doc, "span", "tnc-dh-dow", null, ctx.fmt.weekdayShort(day)),
843
+ h(doc, "span", "tnc-dh-num", null, ctx.fmt.dayNumber(day))
844
+ );
845
+ head.append(opts.navLinks ? ctx.navLink(day, label, "tnc-dh-link") : label);
846
+ }
847
+ let cap = null;
848
+ if (capacity !== null) {
849
+ cap = h(doc, "div", "tnc-cap", null, h(doc, "i"), h(doc, "span", "tnc-cap-txt"));
850
+ head.append(cap);
851
+ }
852
+ this.headRow.append(head);
853
+ opts.dayHeaderDidMount?.({ ...arg, el: head });
854
+ const col = h(doc, "div", ["tnc-col", ...ctx.dayClasses(day)].join(" "), { role: "cell", "aria-label": ctx.fmt.longDate(day), "data-date": arg.dateStr });
855
+ const bgLayer = h(doc, "div", "tnc-col-bg");
856
+ const planLayer = h(doc, "div", "tnc-col-plan");
857
+ const actualLane = h(doc, "div", "tnc-col-actual");
858
+ const more = h(doc, "div", "tnc-more-layer");
859
+ const sel = h(doc, "div", "tnc-sel-layer");
860
+ col.append(bgLayer, sel, planLayer, actualLane, more);
861
+ let now = null;
862
+ if (opts.nowIndicator && day === today) {
863
+ now = h(doc, "div", "tnc-now", { "aria-hidden": "true" });
864
+ col.append(now);
865
+ }
866
+ this.body?.append(col);
867
+ return {
868
+ day,
869
+ head,
870
+ col,
871
+ plan: new ItemPool(ctx, planLayer),
872
+ actual: new ItemPool(ctx, actualLane),
873
+ bg: new ItemPool(ctx, bgLayer),
874
+ actualLane,
875
+ more,
876
+ sel,
877
+ now,
878
+ cap
879
+ };
880
+ });
881
+ if (this.gutter) this.buildGutter();
882
+ this.applyScale();
883
+ }
884
+ buildGutter() {
885
+ const { ctx } = this;
886
+ const gutter = this.gutter;
887
+ gutter.replaceChildren();
888
+ const { min, max, duration } = ctx.slot;
889
+ const interval = duration >= 60 ? duration : 60;
890
+ const first = Math.ceil(min / interval) * interval;
891
+ for (let m = first; m < max; m += interval) {
892
+ if (m === min) continue;
893
+ const label = h(ctx.doc, "div", "tnc-slot-label", null, ctx.fmt.slotLabel(m));
894
+ label.style.top = pct((m - min) / this.minutes);
895
+ gutter.append(label);
896
+ }
897
+ if (this.nowGutter) gutter.append(this.nowGutter);
898
+ }
899
+ applyScale() {
900
+ const { opts, slot } = this.ctx;
901
+ this.ppm = opts.slotHeight / slot.duration;
902
+ this.fitRows(false);
903
+ this.el.style.setProperty("--tnc-slot-px", `${slot.duration * this.ppm}px`);
904
+ this.el.style.setProperty("--tnc-hour-px", `${60 * this.ppm}px`);
905
+ this.el.style.setProperty("--tnc-grid-offset", `${-(slot.min % 60 * this.ppm)}px`);
906
+ if (this.body) this.body.style.height = `${this.minutes * this.ppm}px`;
907
+ }
908
+ /** In fill mode with expandRows, stretches slots so the grid fills the height. */
909
+ fitRows(rerender = true) {
910
+ const { opts, slot } = this.ctx;
911
+ if (!this.body || opts.height !== "fill" || !opts.expandRows) return;
912
+ const avail = this.scroller.clientHeight - this.top.offsetHeight;
913
+ const base = opts.slotHeight / slot.duration;
914
+ const next = avail > 0 ? Math.max(base, avail / this.minutes) : base;
915
+ if (Math.abs(next - this.ppm) < 1e-3) return;
916
+ this.ppm = next;
917
+ this.body.style.height = `${this.minutes * this.ppm}px`;
918
+ this.el.style.setProperty("--tnc-slot-px", `${slot.duration * this.ppm}px`);
919
+ this.el.style.setProperty("--tnc-hour-px", `${60 * this.ppm}px`);
920
+ if (rerender) this.render(null);
921
+ }
922
+ /* --------------------------------------------------------------- render */
923
+ render(change) {
924
+ this.renderAllDay();
925
+ if (!this.body) return;
926
+ const dirty = change ? change.days : null;
927
+ for (const c of this.cols) {
928
+ if (dirty && !dirty.has(c.day) && !(this.ctx.slot.max > MINUTES_PER_DAY && dirty.has(c.day + 1))) continue;
929
+ this.renderColumn(c);
930
+ }
931
+ this.renderCapacity();
932
+ this.tick();
933
+ }
934
+ renderCapacity() {
935
+ const cap = this.ctx.opts.capacityMinutes;
936
+ if (cap === null) return;
937
+ const planned = plannedMinutes(this.ctx.store, this.cols.map((c) => c.day));
938
+ for (const c of this.cols) {
939
+ if (!c.cap) continue;
940
+ const limit = typeof cap === "function" ? cap(c.day) : cap;
941
+ const used = planned.get(c.day) ?? 0;
942
+ const ratio = limit > 0 ? used / limit : 0;
943
+ const bar = c.cap.firstElementChild;
944
+ bar.style.width = pct(Math.min(1, ratio));
945
+ c.cap.classList.toggle("is-warn", ratio > 0.85 && ratio <= 1);
946
+ c.cap.classList.toggle("is-over", ratio > 1);
947
+ const hours = (m) => `${Math.round(m / 60 * 10) / 10}h`;
948
+ c.cap.lastElementChild.textContent = limit > 0 ? `${hours(used)} / ${hours(limit)}` : hours(used);
949
+ }
950
+ }
951
+ renderAllDay() {
952
+ const { ctx } = this;
953
+ if (!this.allDayLayer || !this.allDayPool || !this.allDayMore) return;
954
+ const { days, store, opts } = ctx;
955
+ const first = days[0];
956
+ const last = days[days.length - 1];
957
+ if (first === void 0 || last === void 0) return;
958
+ const items = opts.timeGrid ? store.spanningIn(first, last + 1, false) : store.inRange(first, last + 1).filter((n) => n.item.display !== "background");
959
+ const row = layoutRow(items.filter((n) => n.item.display !== "background"), days, ctx.order);
960
+ const limit = this.allDayLimit();
961
+ const visibleLevels = (col) => limit && (row.countByCol[col] ?? 0) > limit ? limit - 1 : limit || Infinity;
962
+ const offsets = this.colOffsets();
963
+ const pool = this.allDayPool;
964
+ pool.begin();
965
+ const hidden = new Array(days.length).fill(0);
966
+ let maxLevel = -1;
967
+ for (const p of row.placements) {
968
+ let level = p.level;
969
+ let room = Infinity;
970
+ for (let c = p.startCol; c < p.endCol; c++) room = Math.min(room, visibleLevels(c));
971
+ if (level >= room) {
972
+ if (p.item.id !== ctx.draggingId) {
973
+ for (let c = p.startCol; c < p.endCol; c++) hidden[c]++;
974
+ continue;
975
+ }
976
+ level = Math.max(0, room - 1);
977
+ }
978
+ maxLevel = Math.max(maxLevel, level);
979
+ const el = pool.place(p.item.id, {
980
+ n: p.item,
981
+ day: days[p.startCol],
982
+ isStart: p.isStart,
983
+ isEnd: p.isEnd,
984
+ variant: "bar",
985
+ mirror: false
986
+ });
987
+ setStyle(el, {
988
+ left: `calc(${pct(offsets[p.startCol])} + 2px)`,
989
+ width: `calc(${pct(offsets[p.endCol] - offsets[p.startCol])} - 4px)`,
990
+ top: `calc(${level} * (var(--tnc-bar-h) + ${BAR_GAP2}px) + 3px)`
991
+ });
992
+ }
993
+ pool.end();
994
+ this.allDayMore.replaceChildren();
995
+ let anyMore = false;
996
+ hidden.forEach((count, col) => {
997
+ if (!count) return;
998
+ anyMore = true;
999
+ const day = days[col];
1000
+ const link = h(ctx.doc, "button", "tnc-more", { type: "button", "aria-haspopup": "dialog" }, ctx.text.more(count));
1001
+ setStyle(link, {
1002
+ left: `calc(${pct(offsets[col])} + 2px)`,
1003
+ width: `calc(${pct(offsets[col + 1] - offsets[col])} - 4px)`,
1004
+ top: `calc(${visibleLevels(col)} * (var(--tnc-bar-h) + ${BAR_GAP2}px) + 3px)`
1005
+ });
1006
+ link.addEventListener("click", (ev) => ctx.openMore(day, link, ev));
1007
+ this.allDayMore.append(link);
1008
+ });
1009
+ const bars = `calc(${Math.max(1, maxLevel + 1)} * (var(--tnc-bar-h) + ${BAR_GAP2}px) + 6px)`;
1010
+ if (!anyMore) {
1011
+ this.allDayLayer.style.height = bars;
1012
+ return;
1013
+ }
1014
+ const moreH = this.allDayMore.querySelector(".tnc-more")?.offsetHeight ?? 0;
1015
+ const moreTop = Math.max(...hidden.map((count, col) => count ? visibleLevels(col) : 0));
1016
+ this.allDayLayer.style.height = `max(${bars}, calc(${moreTop} * (var(--tnc-bar-h) + ${BAR_GAP2}px) + ${moreH}px + 6px))`;
1017
+ }
1018
+ allDayLimit() {
1019
+ const { dayMaxEvents, dayMaxEventRows } = this.ctx.opts;
1020
+ if (dayMaxEventRows > 0) return dayMaxEventRows;
1021
+ if (typeof dayMaxEvents === "number" && dayMaxEvents > 0) return dayMaxEvents + 1;
1022
+ if (dayMaxEvents === true && this.ctx.opts.timeGrid) return 4;
1023
+ return 0;
1024
+ }
1025
+ /** Fractional left edge of each column (length n + 1). */
1026
+ colOffsets() {
1027
+ const total = this.weights.reduce((a, b) => a + b, 0) || 1;
1028
+ const out = [0];
1029
+ let acc = 0;
1030
+ for (const w of this.weights) out.push((acc += w) / total);
1031
+ return out;
1032
+ }
1033
+ renderColumn(c) {
1034
+ const { ctx } = this;
1035
+ const { store, opts, slot, zone } = ctx;
1036
+ const items = store.timedOn(c.day);
1037
+ if (slot.max > MINUTES_PER_DAY) {
1038
+ const seen = new Set(items.map((n) => n.id));
1039
+ for (const n of store.timedOn(c.day + 1)) if (!seen.has(n.id)) items.push(n);
1040
+ }
1041
+ const minMinutes = opts.eventMinHeight / this.ppm;
1042
+ const layout = layoutTimedDay(items, c.day, zone, minMinutes, ctx.order, [slot.min, slot.max]);
1043
+ const total = this.minutes;
1044
+ const top = (m) => pct((m - slot.min) / total);
1045
+ const height = (s) => pct((s.endMin - s.startMin) / total);
1046
+ c.bg.begin();
1047
+ for (const s of layout.background) {
1048
+ const el = c.bg.place(s.item.id, this.meta(s, true));
1049
+ setStyle(el, { top: top(s.startMin), height: height(s) });
1050
+ }
1051
+ c.bg.end();
1052
+ const hasActual = layout.actual.length > 0;
1053
+ c.col.classList.toggle("has-actual", hasActual);
1054
+ this.placeLane(c.plan, layout.plan, c, top, height, true);
1055
+ this.placeLane(c.actual, layout.actual, c, top, height, false);
1056
+ }
1057
+ placeLane(pool, placements, c, top, height, withMore) {
1058
+ const { ctx } = this;
1059
+ const { opts } = ctx;
1060
+ const maxStack = opts.eventMaxStack;
1061
+ const hiddenByCluster = /* @__PURE__ */ new Map();
1062
+ pool.begin();
1063
+ for (const p of placements) {
1064
+ if (withMore && maxStack > 0 && p.cols > maxStack && p.col >= maxStack - 1) {
1065
+ const rec = hiddenByCluster.get(p.cluster) ?? { count: 0, start: p.startMin, end: p.endMin, items: [] };
1066
+ rec.count++;
1067
+ rec.start = Math.min(rec.start, p.startMin);
1068
+ rec.end = Math.max(rec.end, p.endMin);
1069
+ rec.items.push(p);
1070
+ hiddenByCluster.set(p.cluster, rec);
1071
+ continue;
1072
+ }
1073
+ const cols = withMore && maxStack > 0 ? Math.min(p.cols, maxStack) : p.cols;
1074
+ const span = Math.min(p.span, cols - p.col);
1075
+ const left = p.col / cols;
1076
+ let width = span / cols;
1077
+ if (opts.slotEventOverlap && cols > 1 && p.col + span < cols) width = Math.min(1 - left, width * 1.7);
1078
+ const el = pool.place(p.item.id, this.meta(p, false));
1079
+ const short = (p.endMin - p.startMin) * this.ppm < 34;
1080
+ el.classList.toggle("is-short", short);
1081
+ setStyle(el, {
1082
+ top: top(p.startMin),
1083
+ height: `max(${height(p)}, ${opts.eventMinHeight}px)`,
1084
+ left: pct(left),
1085
+ width: `calc(${pct(width)} - 1px)`,
1086
+ "z-index": String(p.col + 1)
1087
+ });
1088
+ }
1089
+ pool.end();
1090
+ if (!withMore) return;
1091
+ c.more.replaceChildren();
1092
+ for (const rec of hiddenByCluster.values()) {
1093
+ const link = h(ctx.doc, "button", "tnc-more tnc-more--stack", { type: "button", "aria-haspopup": "dialog" }, `+${rec.count}`);
1094
+ const cols = maxStack;
1095
+ setStyle(link, {
1096
+ top: top(rec.start),
1097
+ left: pct((cols - 1) / cols),
1098
+ width: `calc(${pct(1 / cols)} - 1px)`
1099
+ });
1100
+ link.addEventListener("click", (ev) => ctx.openMore(c.day, link, ev));
1101
+ c.more.append(link);
1102
+ }
1103
+ }
1104
+ meta(s, bg) {
1105
+ return {
1106
+ n: s.item,
1107
+ day: s.day,
1108
+ isStart: s.isStart,
1109
+ isEnd: s.isEnd,
1110
+ variant: bg ? "bar" : "timed",
1111
+ mirror: false
1112
+ };
1113
+ }
1114
+ /* ------------------------------------------------------------ selection */
1115
+ showSelection(sel) {
1116
+ const { ctx } = this;
1117
+ const offsets = this.colOffsets();
1118
+ if (this.allDaySel) {
1119
+ this.allDaySel.replaceChildren();
1120
+ if (sel?.allDay) {
1121
+ const startCol = this.cols.findIndex((c) => c.day >= sel.startDay);
1122
+ let endCol = this.cols.findIndex((c) => c.day >= sel.endDay);
1123
+ if (endCol < 0) endCol = this.cols.length;
1124
+ if (startCol >= 0 && endCol > startCol) {
1125
+ const box = h(ctx.doc, "div", "tnc-sel");
1126
+ setStyle(box, {
1127
+ left: pct(offsets[startCol]),
1128
+ width: pct(offsets[endCol] - offsets[startCol]),
1129
+ top: "0",
1130
+ bottom: "0"
1131
+ });
1132
+ this.allDaySel.append(box);
1133
+ }
1134
+ }
1135
+ }
1136
+ for (const c of this.cols) {
1137
+ c.sel.replaceChildren();
1138
+ if (!sel || sel.allDay) continue;
1139
+ const winStart = toInstant(c.day, ctx.slot.min, ctx.zone);
1140
+ const winEnd = toInstant(c.day, ctx.slot.max, ctx.zone);
1141
+ const s = Math.max(sel.startMs, winStart);
1142
+ const e = Math.min(sel.endMs, winEnd);
1143
+ if (e <= s) continue;
1144
+ const rel = (ms) => {
1145
+ const w = wallTime(ms, ctx.zone);
1146
+ return (w.day - c.day) * MINUTES_PER_DAY + w.minute;
1147
+ };
1148
+ const sm = s === winStart ? ctx.slot.min : rel(s);
1149
+ const em = e === winEnd ? ctx.slot.max : rel(e);
1150
+ const box = h(ctx.doc, "div", "tnc-sel");
1151
+ if (ctx.opts.selectMirror) {
1152
+ box.classList.add("tnc-sel--mirror");
1153
+ box.textContent = `${ctx.fmt.time(sm)} \u2013 ${ctx.fmt.time(em)}`;
1154
+ }
1155
+ setStyle(box, { top: pct((sm - ctx.slot.min) / this.minutes), height: pct((em - sm) / this.minutes) });
1156
+ c.sel.append(box);
1157
+ }
1158
+ }
1159
+ /* ------------------------------------------------------------- hit test */
1160
+ hit(x, y) {
1161
+ const colIndex = (cells) => {
1162
+ for (let i2 = 0; i2 < cells.length; i2++) {
1163
+ const r2 = cells[i2].getBoundingClientRect();
1164
+ if (x >= r2.left && x < r2.right) return i2;
1165
+ }
1166
+ const firstR = cells[0]?.getBoundingClientRect();
1167
+ const lastR = cells[cells.length - 1]?.getBoundingClientRect();
1168
+ if (firstR && x < firstR.left) return 0;
1169
+ if (lastR && x >= lastR.right) return cells.length - 1;
1170
+ return -1;
1171
+ };
1172
+ if (this.allDayRow) {
1173
+ const r2 = this.allDayRow.getBoundingClientRect();
1174
+ if (y >= r2.top && y < r2.bottom || !this.body && y >= r2.top) {
1175
+ const i2 = colIndex(this.cols.map((c2) => c2.head));
1176
+ return i2 >= 0 ? { kind: "allDay", day: this.cols[i2].day } : null;
1177
+ }
1178
+ }
1179
+ if (!this.body) return null;
1180
+ const i = colIndex(this.cols.map((c2) => c2.col));
1181
+ if (i < 0) return null;
1182
+ const c = this.cols[i];
1183
+ const r = c.col.getBoundingClientRect();
1184
+ const scrollerR = this.scroller.getBoundingClientRect();
1185
+ const topR = this.top.getBoundingClientRect();
1186
+ if (y < Math.max(topR.bottom, scrollerR.top)) {
1187
+ if (y < topR.top) return null;
1188
+ return this.allDayRow ? { kind: "allDay", day: c.day } : null;
1189
+ }
1190
+ const ratio = Math.min(1, Math.max(0, (y - r.top) / r.height));
1191
+ return { kind: "time", day: c.day, minute: this.ctx.slot.min + ratio * this.minutes };
1192
+ }
1193
+ /* ----------------------------------------------------------------- misc */
1194
+ tick() {
1195
+ const { ctx } = this;
1196
+ const nowMs = ctx.now();
1197
+ const w = wallTime(nowMs, ctx.zone);
1198
+ for (const c of this.cols) {
1199
+ if (!c.now) continue;
1200
+ const minute = (w.day - c.day) * MINUTES_PER_DAY + w.minute;
1201
+ const visible = minute >= ctx.slot.min && minute <= ctx.slot.max;
1202
+ c.now.hidden = !visible;
1203
+ if (visible) c.now.style.top = pct((minute - ctx.slot.min) / this.minutes);
1204
+ if (this.nowGutter) {
1205
+ this.nowGutter.hidden = !visible;
1206
+ if (visible) this.nowGutter.style.top = pct((minute - ctx.slot.min) / this.minutes);
1207
+ }
1208
+ }
1209
+ }
1210
+ resize() {
1211
+ this.fitRows();
1212
+ }
1213
+ scrollToTime(minute) {
1214
+ if (!this.body) return;
1215
+ const y = (minute - this.ctx.slot.min) * this.ppm;
1216
+ this.scroller.scrollTop = Math.max(0, y);
1217
+ }
1218
+ getScroll() {
1219
+ return { top: this.scroller.scrollTop, left: this.scroller.scrollLeft };
1220
+ }
1221
+ setScroll(s) {
1222
+ this.scroller.scrollTop = s.top;
1223
+ this.scroller.scrollLeft = s.left;
1224
+ }
1225
+ elementsFor(id) {
1226
+ const out = this.allDayPool?.elementsFor(id) ?? [];
1227
+ for (const c of this.cols) out.push(...c.plan.elementsFor(id), ...c.actual.elementsFor(id), ...c.bg.elementsFor(id));
1228
+ return out;
1229
+ }
1230
+ destroy() {
1231
+ this.resizeObs?.disconnect();
1232
+ this.allDayPool?.clear();
1233
+ for (const c of this.cols) {
1234
+ c.plan.clear();
1235
+ c.actual.clear();
1236
+ c.bg.clear();
1237
+ }
1238
+ this.el.remove();
1239
+ }
1240
+ };
1241
+
1242
+ // src/dom/views/timeline.ts
1243
+ var SVG = "http://www.w3.org/2000/svg";
1244
+ var COMPACT_PX = 28;
1245
+ var NARROW_PX = 20;
1246
+ var MILESTONE_R = 7;
1247
+ function sameRow(a, b) {
1248
+ return a === b || a.id === b.id && a.title === b.title && a.depth === b.depth && a.group === b.group && a.color === b.color && a.collapsed === b.collapsed;
1249
+ }
1250
+ var TimelineView = class {
1251
+ constructor(ctx) {
1252
+ this.ctx = ctx;
1253
+ const { doc } = ctx;
1254
+ const given = this.tl.scale ?? {};
1255
+ const unit = given.unit ?? "day";
1256
+ this.dayWidth = given.dayWidth ?? (given.unitWidth ?? 36) / TIMELINE_UNIT_DAYS[unit];
1257
+ this.scale = { snapMinutes: MINUTES_PER_DAY, ...given, unit, unitWidth: this.dayWidth * TIMELINE_UNIT_DAYS[unit] };
1258
+ this.startDay = ctx.view.range.startDay;
1259
+ this.endDay = ctx.view.range.endDay;
1260
+ this.labelWidth = this.tl.labelWidth ?? 240;
1261
+ this.rowHeight = this.tl.rowHeight ?? 32;
1262
+ this.el = h(doc, "div", "tnc-tl");
1263
+ this.el.style.setProperty("--tnc-tl-label-w", `${this.labelWidth}px`);
1264
+ this.el.style.setProperty("--tnc-tl-row-h", `${this.rowHeight}px`);
1265
+ this.el.style.setProperty("--tnc-tl-day-w", `${this.dayWidth}px`);
1266
+ this.scroller = h(doc, "div", "tnc-tl-scroll");
1267
+ this.canvas = h(doc, "div", "tnc-tl-canvas", { role: "table", "aria-label": ctx.view.title });
1268
+ this.canvas.style.width = `${this.labelWidth + this.trackWidth()}px`;
1269
+ this.head = h(doc, "div", "tnc-tl-head");
1270
+ this.ticks = h(doc, "div", "tnc-tl-ticks");
1271
+ this.rowsEl = h(doc, "div", "tnc-tl-rows", { role: "rowgroup" });
1272
+ this.svg = doc.createElementNS(SVG, "svg");
1273
+ this.svg.classList.add("tnc-tl-deps");
1274
+ this.svg.setAttribute("aria-hidden", "true");
1275
+ this.todayLine = h(doc, "div", "tnc-tl-today", { "aria-hidden": "true" });
1276
+ this.todayTag = this.tl.todayLabel ? h(doc, "div", "tnc-tl-today-label", { "aria-hidden": "true" }, this.tl.todayLabel) : null;
1277
+ this.selLayer = h(doc, "div", "tnc-tl-sel");
1278
+ this.canvas.append(this.head, this.rowsEl);
1279
+ this.scroller.append(this.canvas);
1280
+ this.el.append(this.scroller);
1281
+ this.buildHead();
1282
+ this.el.addEventListener("pointerdown", this.onLinkDown, true);
1283
+ }
1284
+ ctx;
1285
+ el;
1286
+ scroller;
1287
+ canvas;
1288
+ head;
1289
+ ticks;
1290
+ rowsEl;
1291
+ svg;
1292
+ todayLine;
1293
+ todayTag;
1294
+ selLayer;
1295
+ rows = [];
1296
+ scale;
1297
+ dayWidth;
1298
+ startDay;
1299
+ endDay;
1300
+ labelWidth;
1301
+ rowHeight;
1302
+ link = null;
1303
+ get tl() {
1304
+ return this.ctx.opts.timeline ?? { rows: [], rowOf: () => null };
1305
+ }
1306
+ /* ------------------------------------------------------------- geometry */
1307
+ trackWidth() {
1308
+ return (this.endDay - this.startDay) * this.dayWidth;
1309
+ }
1310
+ /** X offset (within the track) of an instant. */
1311
+ x(ms) {
1312
+ const w = wallTime(ms, this.ctx.zone);
1313
+ return (w.day - this.startDay + w.minute / MINUTES_PER_DAY) * this.dayWidth;
1314
+ }
1315
+ xDay(day) {
1316
+ return (day - this.startDay) * this.dayWidth;
1317
+ }
1318
+ /** Instant at a track x offset. */
1319
+ msAt(x) {
1320
+ const civil = this.startDay * MINUTES_PER_DAY + x / this.dayWidth * MINUTES_PER_DAY;
1321
+ const day = Math.floor(civil / MINUTES_PER_DAY);
1322
+ return toInstant(day, civil - day * MINUTES_PER_DAY, this.ctx.zone);
1323
+ }
1324
+ /** Viewport x (from the scroller's left edge) where the track starts showing, and its visible width. */
1325
+ trackViewport() {
1326
+ return { left: this.labelWidth, width: Math.max(0, this.scroller.clientWidth - this.labelWidth) };
1327
+ }
1328
+ /** Instant under a viewport x, measured from the scroller's left edge. */
1329
+ instantAt(viewportX) {
1330
+ return this.msAt(this.scroller.scrollLeft + viewportX - this.labelWidth);
1331
+ }
1332
+ /** Viewport x of an instant at the current scroll. */
1333
+ viewportXOf(ms) {
1334
+ return this.labelWidth + this.x(ms) - this.scroller.scrollLeft;
1335
+ }
1336
+ /** Scrolls so `ms` sits at `viewportX`. */
1337
+ placeInstant(ms, viewportX, smooth = false) {
1338
+ this.scrollLeftTo(this.labelWidth + this.x(ms) - viewportX, smooth);
1339
+ }
1340
+ scrollToInstant(ms, align, smooth) {
1341
+ const vp = this.trackViewport();
1342
+ const vx = vp.left + (align === "start" ? 0 : align === "end" ? vp.width : vp.width / 2);
1343
+ this.placeInstant(ms, vx, smooth);
1344
+ }
1345
+ /** Viewport x of the middle of the visible track. */
1346
+ viewportCentre() {
1347
+ const vp = this.trackViewport();
1348
+ return vp.left + vp.width / 2;
1349
+ }
1350
+ /** The scroller's left edge in client coordinates. */
1351
+ viewportLeft() {
1352
+ return this.scroller.getBoundingClientRect().left;
1353
+ }
1354
+ scrollLeftTo(left, smooth) {
1355
+ const max = Math.max(0, this.scroller.scrollWidth - this.scroller.clientWidth);
1356
+ const target = Math.max(0, Math.min(max || left, left));
1357
+ if (typeof this.scroller.scrollTo === "function") this.scroller.scrollTo({ left: target, behavior: smooth ? "smooth" : "auto" });
1358
+ else this.scroller.scrollLeft = target;
1359
+ }
1360
+ buildHead() {
1361
+ const { doc, fmt, today } = this.ctx;
1362
+ const top = h(doc, "div", "tnc-tl-tier tnc-tl-tier--major");
1363
+ const bottom = h(doc, "div", "tnc-tl-tier tnc-tl-tier--minor");
1364
+ const corner = h(doc, "div", "tnc-tl-corner");
1365
+ const ticks = this.ticks;
1366
+ ticks.append(top, bottom);
1367
+ if (this.todayTag) ticks.append(this.todayTag);
1368
+ this.head.append(corner, ticks);
1369
+ const unit = this.scale.unit;
1370
+ const add = (tier, x0, x1, label, cls = "") => {
1371
+ const t = h(doc, "div", `tnc-tl-tick ${cls}`, null, h(doc, "span", null, null, label));
1372
+ setStyle(t, { left: x0, width: Math.max(0, x1 - x0) });
1373
+ tier.append(t);
1374
+ };
1375
+ const s = this.startDay;
1376
+ const e = this.endDay;
1377
+ if (unit === "hour") {
1378
+ for (let d = s; d < e; d++) add(top, this.xDay(d), this.xDay(d + 1), fmt.longDate(d), d === today ? "is-today" : "");
1379
+ } else if (unit === "month" || unit === "quarter") {
1380
+ for (let d = startOfYear(s); d < e; d = startOfYear(addMonths(d, 12))) {
1381
+ add(top, this.xDay(Math.max(d, s)), this.xDay(Math.min(startOfYear(addMonths(d, 12)), e)), fmt.year(d));
1382
+ }
1383
+ } else {
1384
+ for (let d = startOfMonth(s); d < e; d = addMonths(d, 1)) {
1385
+ add(top, this.xDay(Math.max(d, s)), this.xDay(Math.min(addMonths(d, 1), e)), fmt.monthYear(d));
1386
+ }
1387
+ }
1388
+ if (unit === "hour") {
1389
+ const step = this.scale.unitWidth >= 28 ? 1 : this.scale.unitWidth >= 14 ? 2 : 6;
1390
+ for (let d = s; d < e; d++) {
1391
+ for (let hr = 0; hr < 24; hr += step) {
1392
+ const x0 = this.xDay(d) + hr / 24 * this.dayWidth;
1393
+ add(bottom, x0, x0 + step / 24 * this.dayWidth, fmt.slotLabel(hr * 60));
1394
+ }
1395
+ }
1396
+ } else if (unit === "day") {
1397
+ for (let d = s; d < e; d++) {
1398
+ const narrow = this.dayWidth < 26;
1399
+ add(bottom, this.xDay(d), this.xDay(d + 1), narrow ? fmt.dayNumber(d) : `${fmt.weekdayNarrow(d)} ${fmt.dayNumber(d)}`, [
1400
+ d === today ? "is-today" : "",
1401
+ ...this.ctx.dayClasses(d).filter((c) => c === "is-weekend")
1402
+ ].join(" "));
1403
+ }
1404
+ } else if (unit === "week") {
1405
+ for (let d = startOfWeek(s, this.ctx.firstDay); d < e; d += 7) {
1406
+ add(bottom, this.xDay(Math.max(d, s)), this.xDay(Math.min(d + 7, e)), `${fmt.monthShort(d)} ${fmt.dayNumber(d)}`);
1407
+ }
1408
+ } else {
1409
+ const step = unit === "quarter" ? 3 : 1;
1410
+ for (let d = startOfMonth(s); d < e; d = addMonths(d, step)) {
1411
+ const label = unit === "quarter" ? `Q${Math.floor((ymdOfDay(d).month - 1) / 3) + 1}` : fmt.monthShort(d);
1412
+ add(bottom, this.xDay(Math.max(d, s)), this.xDay(Math.min(addMonths(d, step), e)), label);
1413
+ }
1414
+ }
1415
+ const colPx = unit === "hour" ? this.dayWidth / 24 : unit === "day" ? this.dayWidth : this.dayWidth * 7;
1416
+ this.el.style.setProperty("--tnc-tl-col-w", `${colPx}px`);
1417
+ this.el.style.setProperty("--tnc-tl-day-px", `${this.dayWidth}px`);
1418
+ }
1419
+ /* --------------------------------------------------------------- render */
1420
+ mountRow(row) {
1421
+ const { ctx } = this;
1422
+ const { doc } = ctx;
1423
+ const el = h(doc, "div", `tnc-tl-row${row.group ? " is-group" : ""}`, { role: "row", "data-row": row.id });
1424
+ const label = h(doc, "div", "tnc-tl-label", { role: "rowheader" });
1425
+ label.style.paddingLeft = `${10 + (row.depth ?? 0) * 16}px`;
1426
+ if (row.color) label.style.setProperty("--tnc-c", row.color);
1427
+ const tl = this.tl;
1428
+ if (tl.rowLabelDidMount) tl.rowLabelDidMount({ row, el: label });
1429
+ else if (tl.rowContent) tl.rowContent(row, label);
1430
+ else label.append(h(doc, "span", "tnc-tl-label-text", null, row.title));
1431
+ if (tl.onRowClick) label.addEventListener("click", (ev) => this.tl.onRowClick?.(row, ev));
1432
+ const track = h(doc, "div", "tnc-tl-track", { role: "cell" });
1433
+ el.append(label, track);
1434
+ return { row, el, label, track, pool: row.group ? null : new ItemPool(ctx, track), top: 0, height: this.rowHeight };
1435
+ }
1436
+ unmountRow(rec) {
1437
+ rec.pool?.clear();
1438
+ this.tl.rowLabelWillUnmount?.({ row: rec.row, el: rec.label });
1439
+ rec.el.remove();
1440
+ }
1441
+ render(_change) {
1442
+ const { ctx } = this;
1443
+ const { store } = ctx;
1444
+ const byRow = /* @__PURE__ */ new Map();
1445
+ for (const n of store.inRange(this.startDay, this.endDay)) {
1446
+ if (n.item.display === "none") continue;
1447
+ const rowId = ctx.dragRow?.id === n.id ? ctx.dragRow.rowId : this.tl.rowOf(n.item);
1448
+ if (rowId == null) continue;
1449
+ const list = byRow.get(rowId) ?? [];
1450
+ list.push(n);
1451
+ byRow.set(rowId, list);
1452
+ }
1453
+ const old = new Map(this.rows.map((r) => [r.row.id, r]));
1454
+ const next = [];
1455
+ let y = 0;
1456
+ for (const row of this.tl.rows) {
1457
+ let rec = old.get(row.id);
1458
+ if (rec && !sameRow(rec.row, row)) {
1459
+ this.unmountRow(rec);
1460
+ rec = void 0;
1461
+ }
1462
+ rec ??= this.mountRow(row);
1463
+ old.delete(row.id);
1464
+ const items = byRow.get(row.id) ?? [];
1465
+ const lane = layoutLane(items, Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY);
1466
+ const levels = Math.max(1, lane.levels);
1467
+ rec.height = this.rowHeight * levels;
1468
+ rec.el.style.height = `${rec.height}px`;
1469
+ rec.top = y;
1470
+ y += rec.height;
1471
+ if (rec.pool) {
1472
+ rec.pool.begin();
1473
+ for (const bar of lane.bars) this.placeBar(rec.pool, bar.item, bar.level);
1474
+ rec.pool.end();
1475
+ }
1476
+ next.push(rec);
1477
+ }
1478
+ for (const rec of old.values()) this.unmountRow(rec);
1479
+ let prev = null;
1480
+ for (const rec of next) {
1481
+ if (prev ? prev.nextSibling !== rec.el : this.rowsEl.firstChild !== rec.el) {
1482
+ if (prev) prev.after(rec.el);
1483
+ else this.rowsEl.prepend(rec.el);
1484
+ }
1485
+ prev = rec.el;
1486
+ }
1487
+ this.rows = next;
1488
+ this.rowsEl.style.height = `${y}px`;
1489
+ this.rowsEl.append(this.svg, this.selLayer, this.todayLine);
1490
+ this.drawDependencies();
1491
+ this.tick();
1492
+ }
1493
+ placeBar(pool, n, level) {
1494
+ const { ctx } = this;
1495
+ const milestone = n.item.kind === "milestone";
1496
+ const el = pool.place(n.id, { n, day: n.startDay, isStart: true, isEnd: true, variant: "timeline", mirror: false });
1497
+ const x0 = n.allDay ? this.xDay(n.startDay) : this.x(n.startMs);
1498
+ const x1 = n.allDay ? this.xDay(n.endDay) : this.x(n.endMs);
1499
+ el.classList.toggle("is-milestone", milestone);
1500
+ el.classList.toggle("is-conflict", ctx.conflicts.has(n.id));
1501
+ if (milestone) {
1502
+ const cx = n.allDay ? this.xDay(n.startDay) + this.dayWidth / 2 : x0;
1503
+ setStyle(el, { left: cx, width: null, top: level * this.rowHeight, "--tnc-grip": null });
1504
+ return;
1505
+ }
1506
+ const width = Math.max(4, x1 - x0);
1507
+ el.classList.toggle("is-compact", width < COMPACT_PX);
1508
+ el.classList.toggle("is-narrow", width < NARROW_PX);
1509
+ const grip = Math.min(8, Math.floor((width - 12) / 2));
1510
+ setStyle(el, { left: x0, width, top: level * this.rowHeight, "--tnc-grip": width < NARROW_PX ? null : `${grip}px` });
1511
+ }
1512
+ rowOfItem(id) {
1513
+ for (const r of this.rows) if (r.pool?.elementsFor(id).length) return r;
1514
+ return void 0;
1515
+ }
1516
+ /** Track x of a bar's start or finish edge. */
1517
+ edgeX(el, edge) {
1518
+ if (el.classList.contains("is-milestone")) return el.offsetLeft + (edge === "start" ? -MILESTONE_R : MILESTONE_R);
1519
+ return edge === "start" ? el.offsetLeft : el.offsetLeft + el.offsetWidth;
1520
+ }
1521
+ drawDependencies() {
1522
+ const { ctx } = this;
1523
+ this.svg.replaceChildren();
1524
+ if (!this.tl.dependencies) return;
1525
+ this.svg.setAttribute("width", String(this.trackWidth()));
1526
+ this.svg.setAttribute("height", this.rowsEl.style.height.replace("px", "") || "0");
1527
+ this.svg.style.left = `${this.labelWidth}px`;
1528
+ for (const n of ctx.store.all()) {
1529
+ if (!n.item.dependsOn?.length) continue;
1530
+ const toRow = this.rowOfItem(n.id);
1531
+ const toEl = toRow?.pool?.elementsFor(n.id)[0];
1532
+ if (!toRow || !toEl) continue;
1533
+ for (const dep of itemDependencies(n.item)) {
1534
+ const fromRow = this.rowOfItem(dep.id);
1535
+ const fromEl = fromRow?.pool?.elementsFor(dep.id)[0];
1536
+ if (!fromRow || !fromEl) continue;
1537
+ const path = this.ctx.doc.createElementNS(SVG, "path");
1538
+ path.setAttribute(
1539
+ "d",
1540
+ dependencyPath(
1541
+ dep.type,
1542
+ this.edgeX(fromEl, dep.type[0] === "S" ? "start" : "finish"),
1543
+ fromRow.top + fromEl.offsetTop + fromEl.offsetHeight / 2,
1544
+ this.edgeX(toEl, dep.type[1] === "S" ? "start" : "finish"),
1545
+ toRow.top + toEl.offsetTop + toEl.offsetHeight / 2
1546
+ )
1547
+ );
1548
+ path.setAttribute("marker-end", "url(#tnc-arrow)");
1549
+ path.dataset.from = dep.id;
1550
+ path.dataset.to = n.id;
1551
+ path.dataset.type = dep.type;
1552
+ if (ctx.conflicts.get(n.id)?.includes(dep.id)) path.classList.add("is-conflict");
1553
+ this.svg.append(path);
1554
+ }
1555
+ }
1556
+ const defs = this.ctx.doc.createElementNS(SVG, "defs");
1557
+ defs.innerHTML = '<marker id="tnc-arrow" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L8,4 L0,8 z" fill="currentColor"/></marker>';
1558
+ this.svg.prepend(defs);
1559
+ }
1560
+ /* ---------------------------------------------------------------- links */
1561
+ linkTargetAt(clientX, clientY, from) {
1562
+ const under = this.ctx.doc.elementFromPoint(clientX, clientY);
1563
+ const el = under?.closest(".tnc-item--timeline");
1564
+ if (!el || !this.el.contains(el)) return null;
1565
+ const meta = this.ctx.meta.get(el);
1566
+ if (!meta || meta.mirror || meta.n.id === from.id) return null;
1567
+ const port = under?.closest(".tnc-port") ?? null;
1568
+ let edge;
1569
+ if (port) edge = port.dataset.edge === "start" ? "start" : "finish";
1570
+ else if (el.classList.contains("is-milestone")) edge = "start";
1571
+ else {
1572
+ const r = el.getBoundingClientRect();
1573
+ edge = clientX < r.left + r.width / 2 ? "start" : "finish";
1574
+ }
1575
+ return { n: meta.n, el, edge, port };
1576
+ }
1577
+ /** Centre of a target edge's port, in track coordinates. */
1578
+ targetPoint(t) {
1579
+ const port = t.port ?? t.el.querySelector(`.tnc-port[data-edge="${t.edge}"]`);
1580
+ const rowsR = this.rowsEl.getBoundingClientRect();
1581
+ const r = (port ?? t.el).getBoundingClientRect();
1582
+ return { x: r.left + r.width / 2 - rowsR.left - this.labelWidth, y: r.top + r.height / 2 - rowsR.top };
1583
+ }
1584
+ linkArg(from, fromEdge, t) {
1585
+ return { from: from.item, to: t.n.item, fromEdge, toEdge: t.edge, type: dependencyType(fromEdge, t.edge) };
1586
+ }
1587
+ linkState(s, phase, linked = false) {
1588
+ const t = s.target;
1589
+ return {
1590
+ phase,
1591
+ from: s.from.item,
1592
+ fromEdge: s.fromEdge,
1593
+ to: t?.n.item ?? null,
1594
+ toEdge: t?.edge ?? null,
1595
+ type: t ? dependencyType(s.fromEdge, t.edge) : null,
1596
+ valid: s.valid,
1597
+ reason: s.reason,
1598
+ linked
1599
+ };
1600
+ }
1601
+ markTarget(t, on, invalid = false) {
1602
+ if (!t) return;
1603
+ t.el.classList.toggle("is-link-target", on);
1604
+ t.el.classList.toggle("is-link-invalid", on && invalid);
1605
+ for (const p of t.el.querySelectorAll(".tnc-port")) {
1606
+ const hit = on && p.dataset.edge === t.edge;
1607
+ p.classList.toggle("is-target", hit);
1608
+ p.classList.toggle("is-invalid", hit && invalid);
1609
+ }
1610
+ }
1611
+ onLinkDown = (ev) => {
1612
+ const port = ev.target.closest?.(".tnc-port");
1613
+ if (!port || !this.tl.onLink || ev.button !== 0 || this.link) return;
1614
+ const barEl = port.closest(".tnc-item");
1615
+ const meta = barEl ? this.ctx.meta.get(barEl) : void 0;
1616
+ if (!meta) return;
1617
+ ev.stopPropagation();
1618
+ ev.preventDefault();
1619
+ const rowsR = this.rowsEl.getBoundingClientRect();
1620
+ const pr = port.getBoundingClientRect();
1621
+ const x = pr.left + pr.width / 2 - rowsR.left - this.labelWidth;
1622
+ const y = pr.top + pr.height / 2 - rowsR.top;
1623
+ const path = this.ctx.doc.createElementNS(SVG, "path");
1624
+ path.classList.add("is-linking");
1625
+ path.setAttribute("d", `M${x},${y}`);
1626
+ this.svg.append(path);
1627
+ const doc = this.ctx.doc;
1628
+ const move = (e) => {
1629
+ const s = this.link;
1630
+ if (!s || e.pointerId !== s.pointerId) return;
1631
+ e.preventDefault();
1632
+ const t = this.linkTargetAt(e.clientX, e.clientY, s.from);
1633
+ const r = this.rowsEl.getBoundingClientRect();
1634
+ const end = t ? this.targetPoint(t) : { x: e.clientX - r.left - this.labelWidth, y: e.clientY - r.top };
1635
+ s.path.setAttribute("d", `M${s.x},${s.y} L${end.x},${end.y}`);
1636
+ if (t?.n.id === s.target?.n.id && t?.edge === s.target?.edge) return;
1637
+ this.markTarget(s.target, false);
1638
+ s.target = t;
1639
+ s.valid = true;
1640
+ s.reason = null;
1641
+ if (t) {
1642
+ const verdict = this.tl.canLink?.(this.linkArg(s.from, s.fromEdge, t)) ?? true;
1643
+ s.valid = verdict === true;
1644
+ s.reason = typeof verdict === "string" ? verdict : null;
1645
+ }
1646
+ s.path.classList.toggle("is-invalid", !s.valid);
1647
+ this.markTarget(t, true, !s.valid);
1648
+ this.tl.onLinkDrag?.(this.linkState(s, "over"));
1649
+ };
1650
+ const finish = (e) => {
1651
+ const s = this.link;
1652
+ if (!s) return;
1653
+ if (e) move(e);
1654
+ this.endLink(e !== null);
1655
+ };
1656
+ const up = (e) => {
1657
+ if (this.link && e.pointerId === this.link.pointerId) finish(e);
1658
+ };
1659
+ const cancel = (e) => {
1660
+ if (this.link && e.pointerId === this.link.pointerId) finish(null);
1661
+ };
1662
+ const key = (e) => {
1663
+ if (e.key !== "Escape") return;
1664
+ e.preventDefault();
1665
+ e.stopPropagation();
1666
+ finish(null);
1667
+ };
1668
+ doc.addEventListener("pointermove", move);
1669
+ doc.addEventListener("pointerup", up);
1670
+ doc.addEventListener("pointercancel", cancel);
1671
+ doc.addEventListener("keydown", key, true);
1672
+ this.link = {
1673
+ from: meta.n,
1674
+ fromEdge: port.dataset.edge === "start" ? "start" : "finish",
1675
+ path,
1676
+ x,
1677
+ y,
1678
+ pointerId: ev.pointerId,
1679
+ target: null,
1680
+ valid: true,
1681
+ reason: null,
1682
+ cleanup: () => {
1683
+ doc.removeEventListener("pointermove", move);
1684
+ doc.removeEventListener("pointerup", up);
1685
+ doc.removeEventListener("pointercancel", cancel);
1686
+ doc.removeEventListener("keydown", key, true);
1687
+ }
1688
+ };
1689
+ this.el.classList.add("is-linking");
1690
+ this.tl.onLinkDrag?.(this.linkState(this.link, "start"));
1691
+ };
1692
+ endLink(commit) {
1693
+ const s = this.link;
1694
+ if (!s) return;
1695
+ this.link = null;
1696
+ s.cleanup();
1697
+ s.path.remove();
1698
+ this.markTarget(s.target, false);
1699
+ this.el.classList.remove("is-linking");
1700
+ if (commit) this.ctx.suppressNextClick();
1701
+ else s.target = null;
1702
+ const linked = commit && !!s.target && s.valid;
1703
+ if (linked && s.target) this.tl.onLink?.(this.linkArg(s.from, s.fromEdge, s.target));
1704
+ this.tl.onLinkDrag?.(this.linkState(s, "end", linked));
1705
+ }
1706
+ /* ------------------------------------------------------------ selection */
1707
+ showSelection(sel) {
1708
+ this.selLayer.replaceChildren();
1709
+ if (!sel) return;
1710
+ const row = sel.rowId ? this.rows.find((r) => r.row.id === sel.rowId) : void 0;
1711
+ if (sel.rowId && !row) return;
1712
+ const x0 = sel.allDay ? this.xDay(sel.startDay) : this.x(sel.startMs);
1713
+ const x1 = sel.allDay ? this.xDay(sel.endDay) : this.x(sel.endMs);
1714
+ const box = h(this.ctx.doc, "div", `tnc-sel${row ? "" : " is-column"}`);
1715
+ const height = row ? row.height : parseFloat(this.rowsEl.style.height) || 0;
1716
+ setStyle(box, { left: this.labelWidth + x0, width: Math.max(2, x1 - x0), top: row ? row.top : 0, height });
1717
+ this.selLayer.append(box);
1718
+ }
1719
+ /* ------------------------------------------------------------- hit test */
1720
+ hit(x, y) {
1721
+ const vr = this.scroller.getBoundingClientRect();
1722
+ const right = vr.left + (this.scroller.clientWidth || vr.width);
1723
+ const bottom = vr.top + (this.scroller.clientHeight || vr.height);
1724
+ if (x < vr.left || x >= right || y < vr.top || y >= bottom) return null;
1725
+ const r = this.rowsEl.getBoundingClientRect();
1726
+ const px2 = x - r.left - this.labelWidth;
1727
+ const days = px2 / this.dayWidth;
1728
+ const day = this.startDay + Math.floor(days);
1729
+ const minute = (days - Math.floor(days)) * MINUTES_PER_DAY;
1730
+ const inLabel = x < vr.left + this.labelWidth;
1731
+ const inHead = y < this.head.getBoundingClientRect().bottom;
1732
+ const rel = y - r.top;
1733
+ const row = inHead ? void 0 : this.rows.find((rec) => rel >= rec.top && rel < rec.top + rec.height);
1734
+ let area;
1735
+ if (inLabel) area = "label";
1736
+ else if (inHead) area = px2 < this.trackWidth() ? "header" : "outside";
1737
+ else area = row && px2 >= 0 && px2 < this.trackWidth() ? "track" : "outside";
1738
+ return { kind: "timeline", rowId: row?.row.id ?? null, day, minute, area, overTrack: area === "track" && !!row && !row.row.group };
1739
+ }
1740
+ tick() {
1741
+ const x = this.x(this.ctx.now());
1742
+ const inRange = x >= 0 && x <= this.trackWidth();
1743
+ this.todayLine.hidden = !inRange;
1744
+ this.todayLine.style.left = `${this.labelWidth + x}px`;
1745
+ if (this.todayTag) {
1746
+ this.todayTag.hidden = !inRange;
1747
+ this.todayTag.style.left = `${x}px`;
1748
+ }
1749
+ }
1750
+ /** Scrolls so `day` sits a little in from the left edge. */
1751
+ scrollToDay(day) {
1752
+ this.scroller.scrollLeft = Math.max(0, this.xDay(day) - this.dayWidth * 2);
1753
+ }
1754
+ getScroll() {
1755
+ return { top: this.scroller.scrollTop, left: this.scroller.scrollLeft };
1756
+ }
1757
+ setScroll(s) {
1758
+ this.scroller.scrollTop = s.top;
1759
+ this.scroller.scrollLeft = s.left;
1760
+ }
1761
+ elementsFor(id) {
1762
+ return this.rows.flatMap((r) => r.pool?.elementsFor(id) ?? []);
1763
+ }
1764
+ destroy() {
1765
+ this.endLink(false);
1766
+ this.el.removeEventListener("pointerdown", this.onLinkDown, true);
1767
+ for (const r of this.rows) this.unmountRow(r);
1768
+ this.rows = [];
1769
+ this.el.remove();
1770
+ }
1771
+ };
1772
+ function dependencyPath(type, x1, y1, x2, y2) {
1773
+ const gap = 10;
1774
+ const out = type[0] === "S" ? -1 : 1;
1775
+ const side = type[1] === "S" ? -1 : 1;
1776
+ const xa = x1 + out * gap;
1777
+ const xb = x2 + side * gap;
1778
+ const xEnd = x2 + side * 2;
1779
+ if (out === side) {
1780
+ const xe = out < 0 ? Math.min(xa, xb) : Math.max(xa, xb);
1781
+ return `M${x1},${y1} H${xe} V${y2} H${xEnd}`;
1782
+ }
1783
+ if (out > 0 ? xa <= xb : xa >= xb) {
1784
+ const mid = (x1 + x2) / 2;
1785
+ return `M${x1},${y1} H${mid} V${y2} H${xEnd}`;
1786
+ }
1787
+ return `M${x1},${y1} H${xa} V${(y1 + y2) / 2} H${xb} V${y2} H${xEnd}`;
1788
+ }
1789
+
1790
+ // src/dom/views/year.ts
1791
+ var YearView = class {
1792
+ constructor(ctx) {
1793
+ this.ctx = ctx;
1794
+ const { doc, fmt, opts } = ctx;
1795
+ this.el = h(doc, "div", "tnc-year");
1796
+ this.scroller = h(doc, "div", "tnc-year-scroll");
1797
+ const grid = h(doc, "div", "tnc-year-grid");
1798
+ let month = ctx.view.range.startDay;
1799
+ for (let i = 0; i < 12; i++) {
1800
+ const { year, month: m } = ymdOfDay(month);
1801
+ const box = h(doc, "section", "tnc-ym", { "aria-label": fmt.monthYear(month) });
1802
+ const title = opts.navLinks ? h(doc, "button", "tnc-ym-title", { type: "button", "data-month": `${year}-${m}` }, fmt.monthLong(month)) : h(doc, "div", "tnc-ym-title", null, fmt.monthLong(month));
1803
+ const first = month;
1804
+ if (opts.navLinks) title.addEventListener("click", (ev) => this.ctx.navigate(first, ev, "month"));
1805
+ box.append(title);
1806
+ const days = h(doc, "div", "tnc-ym-days", { role: "group", "aria-label": fmt.monthYear(month) });
1807
+ const start = startOfWeek(month, ctx.firstDay);
1808
+ for (let d = start; d < start + 7; d++) days.append(h(doc, "div", "tnc-ym-dow", { "aria-hidden": "true" }, fmt.weekdayNarrow(d)));
1809
+ const len = daysInMonth(year, m);
1810
+ for (let d = start; d < month; d++) days.append(h(doc, "div", "tnc-ym-pad"));
1811
+ for (let d = month; d < month + len; d++) {
1812
+ const arg = ctx.dayArg(d);
1813
+ const cell = h(doc, "button", ["tnc-ym-day", ...ctx.dayClasses(d)].join(" "), {
1814
+ type: "button",
1815
+ "data-date": arg.dateStr,
1816
+ "aria-label": fmt.longDate(d)
1817
+ });
1818
+ const dots = h(doc, "span", "tnc-ym-dots", { "aria-hidden": "true" });
1819
+ cell.append(h(doc, "span", "tnc-ym-num", null, fmt.dayNumber(d)), dots);
1820
+ days.append(cell);
1821
+ this.cells.push({ day: d, el: cell, dots });
1822
+ }
1823
+ box.append(days);
1824
+ grid.append(box);
1825
+ month = addMonths(month, 1);
1826
+ }
1827
+ this.scroller.append(grid);
1828
+ this.el.append(this.scroller);
1829
+ }
1830
+ ctx;
1831
+ el;
1832
+ scroller;
1833
+ cells = [];
1834
+ render(_change) {
1835
+ const { store } = this.ctx;
1836
+ const doc = this.ctx.doc;
1837
+ for (const c of this.cells) {
1838
+ const items = store.inRange(c.day, c.day + 1).filter((n) => n.item.display !== "background").sort(this.ctx.order);
1839
+ c.el.dataset.count = String(items.length);
1840
+ c.el.classList.toggle("has-items", items.length > 0);
1841
+ c.el.style.setProperty("--tnc-heat", String(Math.min(1, items.length / 6)));
1842
+ c.dots.replaceChildren(
1843
+ ...items.slice(0, 3).map((n) => {
1844
+ const dot = h(doc, "i");
1845
+ const color = n.item.color ?? n.item.borderColor ?? n.item.backgroundColor;
1846
+ if (color) dot.style.setProperty("--tnc-c", color);
1847
+ return dot;
1848
+ })
1849
+ );
1850
+ const label = this.ctx.fmt.longDate(c.day);
1851
+ c.el.setAttribute("aria-label", items.length ? `${label}, ${this.ctx.text.itemCount(items.length)}` : label);
1852
+ c.el.title = items.length ? `${label}
1853
+ ${items.slice(0, 8).map((n) => `\u2022 ${n.item.title}`).join("\n")}${items.length > 8 ? `
1854
+ ${this.ctx.text.more(items.length - 8)}` : ""}` : label;
1855
+ }
1856
+ }
1857
+ showSelection(_sel) {
1858
+ }
1859
+ hit(x, y) {
1860
+ const el = this.ctx.doc.elementFromPoint(x, y)?.closest(".tnc-ym-day");
1861
+ const c = el ? this.cells.find((cell) => cell.el === el) : void 0;
1862
+ return c ? { kind: "day", day: c.day } : null;
1863
+ }
1864
+ dayAt(el) {
1865
+ const btn = el.closest(".tnc-ym-day");
1866
+ return this.cells.find((c) => c.el === btn)?.day ?? null;
1867
+ }
1868
+ getScroll() {
1869
+ return { top: this.scroller.scrollTop, left: 0 };
1870
+ }
1871
+ setScroll(s) {
1872
+ this.scroller.scrollTop = s.top;
1873
+ }
1874
+ elementsFor(_id) {
1875
+ return [];
1876
+ }
1877
+ destroy() {
1878
+ this.el.remove();
1879
+ }
1880
+ };
1881
+
1882
+ // src/dom/calendar.ts
1883
+ var EXTERNAL_ID = "__tnc_external__";
1884
+ function sameOption(a, b, depth = 0) {
1885
+ if (a === b) return true;
1886
+ if (typeof a === "function" && typeof b === "function") return true;
1887
+ if (depth > 4 || !a || !b || typeof a !== "object" || typeof b !== "object") return false;
1888
+ if (Array.isArray(a) || Array.isArray(b)) {
1889
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
1890
+ return a.every((v, i) => sameOption(v, b[i], depth + 1));
1891
+ }
1892
+ if (Object.getPrototypeOf(a) !== Object.prototype || Object.getPrototypeOf(b) !== Object.prototype) return false;
1893
+ const ka = Object.keys(a);
1894
+ const kb = Object.keys(b);
1895
+ if (ka.length !== kb.length) return false;
1896
+ return ka.every((k) => sameOption(a[k], b[k], depth + 1));
1897
+ }
1898
+ function timelineSansRows(t) {
1899
+ return t ? { ...t, rows: null } : t;
1900
+ }
1901
+ function sameProposal(a, b) {
1902
+ return !!a && !!b && a.startMs === b.startMs && a.endMs === b.endMs && a.allDay === b.allDay && a.rowId === b.rowId;
1903
+ }
1904
+ var Calendar = class {
1905
+ constructor(host, options = {}, items = []) {
1906
+ this.host = host;
1907
+ this.doc = host.ownerDocument;
1908
+ this.win = this.doc.defaultView ?? globalThis;
1909
+ this.opts = { ...defaultOptions(), ...options };
1910
+ this.zone = getTimeZone(this.opts.timeZone);
1911
+ this.fmt = new Formatter(this.opts.locale, this.opts.hour12);
1912
+ this.text = this.resolveText();
1913
+ this.order = parseOrder(this.opts.eventOrder);
1914
+ this.store = new ItemStore(this.zone, {
1915
+ defaultTimedMinutes: this.opts.defaultTimedMinutes,
1916
+ defaultAllDayDays: this.opts.defaultAllDayDays
1917
+ });
1918
+ this.store.setAll(items);
1919
+ this.conflicts = dependencyConflicts(this.store);
1920
+ this.anchor = this.toDay(this.opts.date) ?? this.todayDay();
1921
+ this.selectedIds = this.selectedSet();
1922
+ this.root = h(this.doc, "div", "tnc", { "data-tnc": "" });
1923
+ this.toolbarEl = h(this.doc, "div", "tnc-toolbar", { role: "toolbar" });
1924
+ this.viewHost = h(this.doc, "div", "tnc-view");
1925
+ this.root.append(this.toolbarEl, this.viewHost);
1926
+ host.append(this.root);
1927
+ this.bindEvents();
1928
+ this.rebuild(true);
1929
+ this.scheduleTick();
1930
+ this.observeResize();
1931
+ }
1932
+ host;
1933
+ /** The document/window the calendar currently lives in; see `adopt()`. */
1934
+ doc;
1935
+ win;
1936
+ root;
1937
+ store;
1938
+ meta = /* @__PURE__ */ new WeakMap();
1939
+ opts;
1940
+ zone;
1941
+ fmt;
1942
+ text;
1943
+ order;
1944
+ view;
1945
+ firstDay = 0;
1946
+ days = [];
1947
+ today = 0;
1948
+ conflicts = /* @__PURE__ */ new Map();
1949
+ slot = { min: 0, max: MINUTES_PER_DAY, duration: 30, snap: 30, scroll: 480 };
1950
+ draggingId = null;
1951
+ dragRow = null;
1952
+ toolbarEl;
1953
+ viewHost;
1954
+ current = null;
1955
+ anchor = 0;
1956
+ session = null;
1957
+ selection = null;
1958
+ suppressClick = false;
1959
+ selectedIds = /* @__PURE__ */ new Set();
1960
+ popover = null;
1961
+ timer = null;
1962
+ resizeObs = null;
1963
+ destroyed = false;
1964
+ pendingChange = null;
1965
+ frame = null;
1966
+ observeResize() {
1967
+ this.resizeObs?.disconnect();
1968
+ const RO = this.win.ResizeObserver;
1969
+ if (RO) {
1970
+ this.resizeObs = new RO(() => this.current?.resize?.());
1971
+ this.resizeObs.observe(this.root);
1972
+ }
1973
+ }
1974
+ onAdoptCheck = () => {
1975
+ this.adopt();
1976
+ };
1977
+ /**
1978
+ * Hosts may move a live calendar into another window (e.g. Obsidian's
1979
+ * "Move to new window"). Listeners, timers and observers belong to a window,
1980
+ * so rebind them to the root's current document and rebuild the view there.
1981
+ */
1982
+ adopt() {
1983
+ const doc = this.root.ownerDocument;
1984
+ if (this.destroyed || doc === this.doc || this.session) return;
1985
+ const old = this.win;
1986
+ if (this.timer !== null) old.clearTimeout(this.timer);
1987
+ if (this.frame !== null) old.cancelAnimationFrame(this.frame);
1988
+ this.timer = this.frame = null;
1989
+ this.doc.removeEventListener("pointerdown", this.onDocPointerDown, true);
1990
+ this.doc = doc;
1991
+ this.win = doc.defaultView ?? old;
1992
+ this.doc.addEventListener("pointerdown", this.onDocPointerDown, true);
1993
+ this.observeResize();
1994
+ this.rebuild(false);
1995
+ this.scheduleTick();
1996
+ }
1997
+ /* ================================================================ public */
1998
+ /** Replaces all items, re-rendering only what changed. */
1999
+ setItems(items) {
2000
+ const drag = this.session?.active && this.session.origin ? this.store.get(this.session.origin.id) : void 0;
2001
+ const change = this.store.setAll(items);
2002
+ if (drag && this.session?.origin) {
2003
+ const fresh = this.store.get(drag.id);
2004
+ if (fresh) this.session.origin = fresh;
2005
+ this.merge(change, this.store.upsert([drag.item]));
2006
+ }
2007
+ this.applyChange(change);
2008
+ }
2009
+ upsertItems(items) {
2010
+ this.applyChange(this.store.upsert(items));
2011
+ }
2012
+ removeItems(ids) {
2013
+ this.applyChange(this.store.remove(ids));
2014
+ }
2015
+ getItem(id) {
2016
+ return this.store.get(id)?.item;
2017
+ }
2018
+ /**
2019
+ * Updates options. Only what the change affects is rebuilt.
2020
+ *
2021
+ * In the timeline, a new `date`, range or scale keeps what is on screen
2022
+ * instead of jumping back to the anchor date: a scale change keeps the
2023
+ * instant at the middle of the visible track, other changes keep the left
2024
+ * edge. Pass `anchor` to pin a different instant, e.g. the one under the
2025
+ * pointer when zooming with the wheel.
2026
+ */
2027
+ setOptions(next, how = {}) {
2028
+ const prev = this.opts;
2029
+ this.opts = { ...prev, ...next };
2030
+ const changed = Object.keys(next).filter(
2031
+ (k) => !sameOption(next[k], prev[k])
2032
+ );
2033
+ if (!changed.length) return;
2034
+ const has = (k) => changed.includes(k);
2035
+ if (has("selected")) this.applySelected();
2036
+ const rest = changed.filter((k) => k !== "selected");
2037
+ if (!rest.length) return;
2038
+ if (rest.length === 1 && has("timeline") && this.current instanceof TimelineView && sameOption(timelineSansRows(this.opts.timeline), timelineSansRows(prev.timeline))) {
2039
+ this.current.render(null);
2040
+ if (this.selection) this.current.showSelection(this.selection);
2041
+ return;
2042
+ }
2043
+ if (has("timeZone") || has("defaultTimedMinutes") || has("defaultAllDayDays")) {
2044
+ this.zone = getTimeZone(this.opts.timeZone);
2045
+ this.store.reconfigure(this.zone, {
2046
+ defaultTimedMinutes: this.opts.defaultTimedMinutes,
2047
+ defaultAllDayDays: this.opts.defaultAllDayDays
2048
+ });
2049
+ }
2050
+ if (has("locale") || has("hour12")) this.fmt = new Formatter(this.opts.locale, this.opts.hour12);
2051
+ if (has("text") || has("locale")) this.text = this.resolveText();
2052
+ if (has("eventOrder")) this.order = parseOrder(this.opts.eventOrder);
2053
+ if (has("date")) this.anchor = this.toDay(this.opts.date) ?? this.anchor;
2054
+ const needsRebuild = changed.some((k) => LAYOUT_KEYS.has(k)) || has("date");
2055
+ if (!needsRebuild) return;
2056
+ const keep = !has("view") && this.opts.view === "timeline" ? this.timelineKeep(prev, how.anchor) : null;
2057
+ if (keep) this.rebuild(false, keep);
2058
+ else this.rebuild(has("view") || has("date"));
2059
+ }
2060
+ /** What a timeline rebuild should keep on screen. */
2061
+ timelineKeep(prev, anchor) {
2062
+ const view = this.current;
2063
+ if (!(view instanceof TimelineView)) return null;
2064
+ const top = view.getScroll().top;
2065
+ if (anchor) {
2066
+ const date = anchor.date !== void 0 ? this.toMs(anchor.date) : null;
2067
+ if (anchor.clientX !== void 0) {
2068
+ const vx2 = anchor.clientX - view.viewportLeft();
2069
+ return { ms: date ?? view.instantAt(vx2), vx: vx2, top };
2070
+ }
2071
+ if (date !== null) return { ms: date, vx: view.viewportXOf(date), top };
2072
+ }
2073
+ const before = prev.timeline?.scale;
2074
+ const after = this.opts.timeline?.scale;
2075
+ const rescaled = !sameOption(before, after) || prev.timeline?.labelWidth !== this.opts.timeline?.labelWidth;
2076
+ const vx = rescaled ? view.viewportCentre() : this.opts.timeline?.labelWidth ?? 240;
2077
+ return { ms: view.instantAt(vx), vx, top };
2078
+ }
2079
+ /** Re-renders everything, e.g. after render hooks start returning different content. */
2080
+ rerender() {
2081
+ this.rebuild(false);
2082
+ }
2083
+ getOptions() {
2084
+ return this.opts;
2085
+ }
2086
+ changeView(view, date) {
2087
+ if (date !== void 0) this.anchor = this.toDay(date) ?? this.anchor;
2088
+ if (view === this.opts.view && date === void 0) return;
2089
+ this.opts = { ...this.opts, view };
2090
+ this.rebuild(true);
2091
+ this.opts.onViewChange?.(this.view);
2092
+ }
2093
+ gotoDate(date) {
2094
+ const day = this.toDay(date);
2095
+ if (day === null) return;
2096
+ this.anchor = day;
2097
+ this.rebuild(true);
2098
+ }
2099
+ prev() {
2100
+ this.anchor = this.step(-1);
2101
+ this.rebuild(true);
2102
+ }
2103
+ next() {
2104
+ this.anchor = this.step(1);
2105
+ this.rebuild(true);
2106
+ }
2107
+ goToday() {
2108
+ this.anchor = this.todayDay();
2109
+ this.rebuild(true);
2110
+ }
2111
+ /** The anchor date, as local midnight of that civil day in the calendar zone. */
2112
+ getDate() {
2113
+ return new Date(toInstant(this.anchor, 0, this.zone));
2114
+ }
2115
+ getView() {
2116
+ return this.view;
2117
+ }
2118
+ /** Clears the current selection. */
2119
+ unselect() {
2120
+ if (!this.selection) return;
2121
+ this.selection = null;
2122
+ this.current?.showSelection(null);
2123
+ this.opts.onUnselect?.();
2124
+ }
2125
+ /** Re-measures (e.g. after the host was shown or resized by a parent). */
2126
+ updateSize() {
2127
+ this.current?.resize?.();
2128
+ }
2129
+ getScroll() {
2130
+ return this.current?.getScroll() ?? { top: 0, left: 0 };
2131
+ }
2132
+ setScroll(s) {
2133
+ this.current?.setScroll(s);
2134
+ }
2135
+ scrollToTime(time) {
2136
+ this.current?.scrollToTime?.(parseDurationMinutes(time, this.slot.scroll));
2137
+ }
2138
+ /**
2139
+ * Scrolls a date (or instant) into view. In the timeline `align` places it
2140
+ * at the start, middle or end of the visible track; other views move to
2141
+ * the date if it is outside the range, and time grids scroll to its time.
2142
+ */
2143
+ scrollToDate(date, opts = {}) {
2144
+ const p = parseDateInput(date, this.zone);
2145
+ if (!p) return;
2146
+ const day = p.kind === "day" ? p.day : dayOf(p.ms, this.zone);
2147
+ const ms = p.kind === "day" ? toInstant(p.day, 0, this.zone) : p.ms;
2148
+ const { startDay, endDay } = this.view.range;
2149
+ if (day < startDay || day >= endDay) {
2150
+ this.anchor = day;
2151
+ this.rebuild(true);
2152
+ }
2153
+ const view = this.current;
2154
+ if (view instanceof TimelineView) view.scrollToInstant(ms, opts.align ?? "center", opts.smooth ?? false);
2155
+ else if (view instanceof TimeGridView && p.kind !== "day") view.scrollToTime(wallTime(ms, this.zone).minute);
2156
+ }
2157
+ /** What is under a client point: a time slot, a day, or a timeline row and date. Null outside the view. */
2158
+ hitTest(clientX, clientY) {
2159
+ return this.current?.hit(clientX, clientY) ?? null;
2160
+ }
2161
+ /** Item elements currently rendered for an id. */
2162
+ elementsFor(id) {
2163
+ return this.current?.elementsFor(id) ?? [];
2164
+ }
2165
+ /**
2166
+ * Starts dragging something from outside the calendar (a task tray, an
2167
+ * unscheduled list). Call from a pointerdown handler; the calendar follows
2168
+ * the pointer and fires `onExternalDrop` on release.
2169
+ */
2170
+ beginExternalDrag(ev, source) {
2171
+ this.endSession(null, true);
2172
+ this.session = {
2173
+ mode: "external",
2174
+ pointerId: ev.pointerId,
2175
+ pointerType: ev.pointerType,
2176
+ x0: ev.clientX,
2177
+ y0: ev.clientY,
2178
+ active: true,
2179
+ origin: null,
2180
+ grab: null,
2181
+ proposal: null,
2182
+ denied: false,
2183
+ timer: null,
2184
+ lastEvent: ev,
2185
+ source
2186
+ };
2187
+ this.root.classList.add("tnc-dragging", "tnc-dragging-external");
2188
+ this.listenDoc(true);
2189
+ }
2190
+ destroy() {
2191
+ if (this.destroyed) return;
2192
+ this.destroyed = true;
2193
+ this.endSession(null, true);
2194
+ this.closeMore();
2195
+ if (this.timer !== null) this.win.clearTimeout(this.timer);
2196
+ if (this.frame !== null) this.win.cancelAnimationFrame(this.frame);
2197
+ this.resizeObs?.disconnect();
2198
+ this.unbindEvents();
2199
+ this.current?.destroy();
2200
+ this.current = null;
2201
+ this.root.remove();
2202
+ }
2203
+ /* ========================================================== view context */
2204
+ now() {
2205
+ return Date.now();
2206
+ }
2207
+ suppressNextClick() {
2208
+ this.suppressClick = true;
2209
+ this.win.setTimeout(() => this.suppressClick = false, 0);
2210
+ }
2211
+ dayArg(day) {
2212
+ return {
2213
+ day,
2214
+ date: new Date(toInstant(day, 0, this.zone)),
2215
+ dateStr: formatDay(day),
2216
+ view: this.view,
2217
+ isToday: day === this.today,
2218
+ isPast: day < this.today
2219
+ };
2220
+ }
2221
+ dayClasses(day) {
2222
+ const out = [];
2223
+ const wd = weekdayOf(day);
2224
+ if (wd === 0 || wd === 6) out.push("is-weekend");
2225
+ if (day === this.today && this.opts.todayHighlight) out.push("is-today");
2226
+ if (day < this.today) out.push("is-past");
2227
+ out.push(...classList(this.opts.dayClassNames?.(this.dayArg(day))));
2228
+ return out;
2229
+ }
2230
+ isEditable(n, what) {
2231
+ if (n.id === EXTERNAL_ID || this.opts.view === "list" || this.opts.view === "year") return false;
2232
+ const it = n.item;
2233
+ const base = it.editable ?? this.opts.editable;
2234
+ const specific = what === "start" ? it.startEditable : it.durationEditable;
2235
+ return specific ?? base;
2236
+ }
2237
+ navLink(day, label, cls) {
2238
+ const btn = h(this.doc, "button", `tnc-navlink ${cls}`, { type: "button", "data-nav-date": formatDay(day) }, label);
2239
+ btn.addEventListener("click", (ev) => {
2240
+ ev.stopPropagation();
2241
+ this.navigate(day, ev);
2242
+ });
2243
+ btn.addEventListener("pointerdown", (ev) => ev.stopPropagation());
2244
+ return btn;
2245
+ }
2246
+ navigate(day, jsEvent, view = "day") {
2247
+ if (this.opts.onNavLink) {
2248
+ this.opts.onNavLink({ ...this.dayArg(day), target: view, jsEvent });
2249
+ return;
2250
+ }
2251
+ this.changeView(view, formatDay(day));
2252
+ }
2253
+ createItemEl(meta) {
2254
+ const { n, variant } = meta;
2255
+ const it = n.item;
2256
+ const el = h(this.doc, "div", "tnc-item");
2257
+ el.classList.add(`tnc-item--${variant}`);
2258
+ if (it.kind) el.classList.add(`tnc-k-${it.kind.replace(/[^\w-]/g, "-")}`);
2259
+ if (it.completed) el.classList.add("is-completed");
2260
+ if (it.display === "background") el.classList.add("is-background");
2261
+ if (it.lane === "actual") el.classList.add("is-actual");
2262
+ if (!meta.isStart) el.classList.add("is-cont-start");
2263
+ if (!meta.isEnd) el.classList.add("is-cont-end");
2264
+ if (n.allDay) el.classList.add("is-all-day");
2265
+ if (meta.mirror || n.id === EXTERNAL_ID) el.classList.add("is-mirror");
2266
+ if (this.conflicts.has(n.id)) el.classList.add("is-conflict");
2267
+ const indicator = variant === "indicator";
2268
+ if (!meta.mirror && !indicator && (this.isEditable(n, "start") || this.isEditable(n, "duration"))) el.classList.add("is-draggable");
2269
+ if (this.draggingId === n.id) el.classList.add("is-dragging");
2270
+ if (this.selectedIds.has(n.id) && !meta.mirror) el.classList.add("is-selected");
2271
+ el.classList.add(...classList(it.className));
2272
+ const arg = this.itemArg(el, meta);
2273
+ el.classList.add(...classList(this.opts.itemClassNames?.(arg)));
2274
+ el.dataset.id = n.id;
2275
+ if (it.kind) el.dataset.kind = it.kind;
2276
+ const color = it.color ?? it.borderColor;
2277
+ if (color) el.style.setProperty("--tnc-c", color);
2278
+ if (it.backgroundColor) el.style.setProperty("--tnc-bg", it.backgroundColor);
2279
+ if (it.borderColor) el.style.setProperty("--tnc-border", it.borderColor);
2280
+ if (it.textColor) el.style.setProperty("--tnc-fg", it.textColor);
2281
+ if (indicator) el.setAttribute("aria-hidden", "true");
2282
+ else if (it.display !== "background") {
2283
+ el.tabIndex = 0;
2284
+ el.setAttribute("role", "button");
2285
+ el.setAttribute("aria-label", this.ariaLabel(n));
2286
+ }
2287
+ this.meta.set(el, meta);
2288
+ const custom = variant === "list" ? this.opts.listItemContent?.(arg) : null;
2289
+ const content = indicator ? null : custom ?? this.opts.itemContent?.(arg);
2290
+ if (indicator) {
2291
+ } else if (content != null) {
2292
+ const wrap = h(this.doc, "div", "tnc-item-main");
2293
+ wrap.append(content);
2294
+ el.append(wrap);
2295
+ } else {
2296
+ el.append(this.defaultContent(meta));
2297
+ }
2298
+ if (variant !== "list" && !indicator && it.display !== "background") this.addHandles(el, meta);
2299
+ this.opts.itemDidMount?.(arg);
2300
+ return el;
2301
+ }
2302
+ refreshItemEl(el, meta) {
2303
+ const time = el.querySelector(":scope .tnc-item-time");
2304
+ if (time) this.fillTime(time, meta.n, meta.variant, meta.day);
2305
+ el.classList.toggle("is-dragging", this.draggingId === meta.n.id);
2306
+ el.classList.toggle("is-conflict", this.conflicts.has(meta.n.id));
2307
+ el.classList.toggle("is-selected", this.selectedIds.has(meta.n.id) && !meta.mirror);
2308
+ }
2309
+ selectedSet() {
2310
+ const s = this.opts.selected;
2311
+ return new Set(s == null ? [] : typeof s === "string" ? [s] : s);
2312
+ }
2313
+ applySelected() {
2314
+ this.selectedIds = this.selectedSet();
2315
+ for (const el of this.viewHost.querySelectorAll(".tnc-item")) {
2316
+ const meta = this.meta.get(el);
2317
+ if (meta) el.classList.toggle("is-selected", this.selectedIds.has(meta.n.id) && !meta.mirror);
2318
+ }
2319
+ }
2320
+ destroyItemEl(el) {
2321
+ const meta = this.meta.get(el);
2322
+ if (meta) this.opts.itemWillUnmount?.(this.itemArg(el, meta));
2323
+ el.remove();
2324
+ }
2325
+ openMore(day, anchor, jsEvent) {
2326
+ jsEvent.stopPropagation();
2327
+ const items = this.store.inRange(day, day + 1).filter((n) => n.item.display !== "background" && (n.allDay || this.opts.view === "month" || !this.opts.timeGrid || this.opts.eventMaxStack > 0)).sort((a, b) => Number(b.allDay) - Number(a.allDay) || this.order(a, b));
2328
+ const shown = new Set(
2329
+ [...this.viewHost.querySelectorAll(".tnc-item")].filter((el2) => this.meta.get(el2)?.day === day || (this.meta.get(el2)?.n.startDay ?? Infinity) <= day).map((el2) => el2.dataset.id ?? "")
2330
+ );
2331
+ const res = this.opts.onMoreClick?.({
2332
+ day,
2333
+ date: new Date(toInstant(day, 0, this.zone)),
2334
+ items: items.map((n) => n.item),
2335
+ hidden: items.filter((n) => !shown.has(n.id)).map((n) => n.item),
2336
+ jsEvent,
2337
+ el: anchor,
2338
+ view: this.view
2339
+ });
2340
+ if (res === false) return;
2341
+ this.closeMore();
2342
+ const el = h(this.doc, "div", "tnc-popover", { role: "dialog", "aria-label": this.fmt.longDate(day), tabindex: "-1" });
2343
+ const close = h(this.doc, "button", "tnc-popover-close", { type: "button", "aria-label": "Close" }, "\xD7");
2344
+ close.addEventListener("click", () => this.closeMore(true));
2345
+ const title = this.opts.navLinks ? this.navLink(day, this.fmt.longDate(day), "tnc-popover-title") : h(this.doc, "div", "tnc-popover-title", null, this.fmt.longDate(day));
2346
+ const body = h(this.doc, "div", "tnc-popover-body");
2347
+ el.append(h(this.doc, "div", "tnc-popover-head", null, title, close), body);
2348
+ this.root.append(el);
2349
+ const pool = new ItemPool(this, body);
2350
+ pool.begin();
2351
+ for (const n of items) {
2352
+ pool.place(n.id, { n, day, isStart: n.startDay >= day, isEnd: n.endDay <= day + 1, variant: n.allDay ? "bar" : "dot", mirror: false });
2353
+ }
2354
+ pool.end();
2355
+ this.popover = { el, pool, anchor, day };
2356
+ const rr = this.root.getBoundingClientRect();
2357
+ const ar = anchor.getBoundingClientRect();
2358
+ const w = Math.max(220, ar.width + 24);
2359
+ let left = ar.left - rr.left - 12;
2360
+ left = Math.max(4, Math.min(left, rr.width - w - 4));
2361
+ let top = ar.top - rr.top - 40;
2362
+ const height = el.offsetHeight;
2363
+ top = Math.max(4, Math.min(top, rr.height - height - 4));
2364
+ setStyle(el, { left, top, width: w });
2365
+ el.focus();
2366
+ }
2367
+ /* =============================================================== render */
2368
+ resolveText() {
2369
+ const t = this.opts.text;
2370
+ return {
2371
+ ...DEFAULT_TEXT,
2372
+ ...t,
2373
+ views: { ...DEFAULT_TEXT.views, ...t.views },
2374
+ viewHints: { ...DEFAULT_TEXT.viewHints, ...t.viewHints }
2375
+ };
2376
+ }
2377
+ todayDay() {
2378
+ return dayOf(this.now(), this.zone);
2379
+ }
2380
+ toDay(date) {
2381
+ if (date === void 0 || date === null) return null;
2382
+ const p = parseDateInput(date, this.zone);
2383
+ if (!p) return null;
2384
+ return p.kind === "day" ? p.day : dayOf(p.ms, this.zone);
2385
+ }
2386
+ toMs(date) {
2387
+ const p = parseDateInput(date, this.zone);
2388
+ if (!p) return null;
2389
+ return p.kind === "day" ? toInstant(p.day, 0, this.zone) : p.ms;
2390
+ }
2391
+ step(dir) {
2392
+ const { view, dayCount, listDayCount } = this.opts;
2393
+ switch (view) {
2394
+ case "day":
2395
+ return this.anchor + dir;
2396
+ case "days":
2397
+ return this.anchor + dir * Math.max(1, dayCount);
2398
+ case "week":
2399
+ return this.anchor + dir * 7;
2400
+ case "list": {
2401
+ const n = Math.max(1, listDayCount);
2402
+ return this.anchor + dir * (this.opts.listStart === "week" ? Math.ceil(n / 7) * 7 : n);
2403
+ }
2404
+ case "month":
2405
+ return addMonths(startOfMonth(this.anchor), dir);
2406
+ case "year":
2407
+ return addMonths(startOfYear(this.anchor), 12 * dir);
2408
+ case "timeline": {
2409
+ const len = this.opts.timeline?.days ?? 120;
2410
+ return this.anchor + dir * Math.max(7, Math.round(len / 3));
2411
+ }
2412
+ }
2413
+ }
2414
+ computeRange() {
2415
+ const { view, dayCount, listDayCount } = this.opts;
2416
+ const a = this.anchor;
2417
+ switch (view) {
2418
+ case "day":
2419
+ return { startDay: a, endDay: a + 1 };
2420
+ case "days":
2421
+ return { startDay: a, endDay: a + Math.max(1, dayCount) };
2422
+ case "week": {
2423
+ const s = startOfWeek(a, this.firstDay);
2424
+ return { startDay: s, endDay: s + 7 };
2425
+ }
2426
+ case "list": {
2427
+ const s = this.opts.listStart === "week" ? startOfWeek(a, this.firstDay) : a;
2428
+ return { startDay: s, endDay: s + Math.max(1, listDayCount) };
2429
+ }
2430
+ case "month": {
2431
+ const s = startOfWeek(startOfMonth(a), this.firstDay);
2432
+ return { startDay: s, endDay: s + 42 };
2433
+ }
2434
+ case "year": {
2435
+ const s = startOfYear(a);
2436
+ return { startDay: s, endDay: addMonths(s, 12) };
2437
+ }
2438
+ case "timeline": {
2439
+ const lead = this.opts.timeline?.leadDays ?? 14;
2440
+ const len = this.opts.timeline?.days ?? 120;
2441
+ return { startDay: a - lead, endDay: a - lead + len };
2442
+ }
2443
+ }
2444
+ }
2445
+ computeTitle(startDay, endDay) {
2446
+ const { view } = this.opts;
2447
+ if (view === "month") return this.fmt.monthYear(this.anchor);
2448
+ if (view === "year") return this.fmt.year(this.anchor);
2449
+ if (view === "day") return this.fmt.fullDate(this.anchor);
2450
+ const visible = this.days;
2451
+ const first = visible[0] ?? startDay;
2452
+ const last = visible[visible.length - 1] ?? endDay - 1;
2453
+ return this.fmt.range(first, last);
2454
+ }
2455
+ rebuild(navigated, keep) {
2456
+ const scroll = navigated || keep ? null : this.current?.getScroll();
2457
+ this.closeMore();
2458
+ this.unselectSilently();
2459
+ this.firstDay = this.opts.firstDay ?? localeFirstDay(this.opts.locale);
2460
+ this.today = this.todayDay();
2461
+ const min = parseDurationMinutes(this.opts.slotMinTime, 0);
2462
+ const max = Math.max(min + 30, parseDurationMinutes(this.opts.slotMaxTime, MINUTES_PER_DAY));
2463
+ const duration = Math.max(1, parseDurationMinutes(this.opts.slotDuration, 30));
2464
+ const snap = Math.max(1, parseDurationMinutes(this.opts.snapDuration ?? null, duration));
2465
+ this.slot = { min, max, duration, snap, scroll: parseDurationMinutes(this.opts.scrollTime, 480) };
2466
+ const { startDay, endDay } = this.computeRange();
2467
+ const hidden = new Set(this.opts.hiddenDays);
2468
+ if (!this.opts.weekends) {
2469
+ hidden.add(0);
2470
+ hidden.add(6);
2471
+ }
2472
+ const days = [];
2473
+ const listLike = this.opts.view === "year" || this.opts.view === "timeline" || this.opts.view === "day";
2474
+ for (let d = startDay; d < endDay; d++) if (listLike || !hidden.has(weekdayOf(d))) days.push(d);
2475
+ this.days = days.length ? days : [startDay];
2476
+ const range = {
2477
+ startDay,
2478
+ endDay,
2479
+ start: new Date(toInstant(startDay, 0, this.zone)),
2480
+ end: new Date(toInstant(endDay, 0, this.zone))
2481
+ };
2482
+ const prevView = this.view;
2483
+ this.view = { type: this.opts.view, range, anchor: this.anchor, title: "" };
2484
+ this.view.title = this.computeTitle(startDay, endDay);
2485
+ this.conflicts = dependencyConflicts(this.store);
2486
+ this.root.className = "tnc";
2487
+ this.root.classList.add(`tnc--${this.opts.view}`, `tnc--height-${this.opts.height}`);
2488
+ if (!this.opts.timeGrid) this.root.classList.add("tnc--no-time-grid");
2489
+ this.renderToolbar();
2490
+ this.current?.destroy();
2491
+ this.current = this.createView();
2492
+ this.viewHost.replaceChildren(this.current.el);
2493
+ this.current.render(null);
2494
+ if (keep && this.current instanceof TimelineView) {
2495
+ this.current.setScroll({ top: keep.top, left: 0 });
2496
+ this.current.placeInstant(keep.ms, keep.vx);
2497
+ } else if (scroll) this.current.setScroll(scroll);
2498
+ else if (this.current instanceof TimeGridView) this.current.scrollToTime(this.slot.scroll);
2499
+ else if (this.current instanceof TimelineView) this.current.scrollToDay(this.anchor);
2500
+ const rangeChanged = !prevView || prevView.type !== this.view.type || prevView.range.startDay !== startDay || prevView.range.endDay !== endDay;
2501
+ if (rangeChanged) this.opts.onRangeChange?.(this.view);
2502
+ }
2503
+ createView() {
2504
+ switch (this.opts.view) {
2505
+ case "month":
2506
+ return new DayGridView(this);
2507
+ case "list":
2508
+ return new ListView(this);
2509
+ case "year":
2510
+ return new YearView(this);
2511
+ case "timeline":
2512
+ return new TimelineView(this);
2513
+ default:
2514
+ return new TimeGridView(this);
2515
+ }
2516
+ }
2517
+ renderToolbar() {
2518
+ const tb = this.opts.toolbar;
2519
+ this.toolbarEl.replaceChildren();
2520
+ this.toolbarEl.hidden = !tb;
2521
+ if (!tb) return;
2522
+ const cfg = tb === true ? {} : tb;
2523
+ const views = cfg.views ?? ["month", "week", "days", "day", "list", "year"];
2524
+ const btn = (label, hint, cls, onClick, pressed) => {
2525
+ const b = h(this.doc, "button", `tnc-btn ${cls}`, { type: "button", title: hint, "aria-label": hint }, label);
2526
+ if (pressed !== void 0) b.setAttribute("aria-pressed", String(pressed));
2527
+ b.addEventListener("click", onClick);
2528
+ return b;
2529
+ };
2530
+ const extras = (cfg.extraButtons ?? []).map((e) => {
2531
+ const b = btn(e.text, e.hint ?? e.text, `tnc-btn-extra tnc-btn-${e.id}`, (ev) => e.onClick(ev));
2532
+ if (e.icon) b.replaceChildren(e.icon());
2533
+ return b;
2534
+ });
2535
+ const nav = h(
2536
+ this.doc,
2537
+ "div",
2538
+ "tnc-toolbar-group tnc-toolbar-nav",
2539
+ null,
2540
+ btn("\u2039", this.text.prev, "tnc-btn-prev", () => this.prev()),
2541
+ btn(this.text.today, this.text.today, "tnc-btn-today", () => this.goToday()),
2542
+ btn("\u203A", this.text.next, "tnc-btn-next", () => this.next())
2543
+ );
2544
+ const title = h(this.doc, "h2", "tnc-title", { "aria-live": "polite" }, this.view.title);
2545
+ const viewGroup = h(this.doc, "div", "tnc-toolbar-group tnc-toolbar-views", { role: "group" });
2546
+ for (const v of views) {
2547
+ const label = v === "days" ? this.text.views.days ?? `${this.opts.dayCount} days` : this.text.views[v] ?? v;
2548
+ viewGroup.append(btn(label, this.text.viewHints[v] ?? label, `tnc-btn-view tnc-btn-view-${v}`, () => this.changeView(v), v === this.opts.view));
2549
+ }
2550
+ const start = h(this.doc, "div", "tnc-toolbar-start");
2551
+ const end = h(this.doc, "div", "tnc-toolbar-end");
2552
+ if (cfg.extraPlacement === "start") start.append(...extras);
2553
+ start.append(nav, title);
2554
+ end.append(viewGroup);
2555
+ if (cfg.extraPlacement !== "start") end.append(...extras);
2556
+ this.toolbarEl.append(start, end);
2557
+ }
2558
+ applyChange(change) {
2559
+ if (!change.added.length && !change.updated.length && !change.removed.length && !change.days.size && !change.spans) return;
2560
+ this.conflicts = dependencyConflicts(this.store);
2561
+ this.adopt();
2562
+ this.pendingChange = this.pendingChange ? this.merge(this.pendingChange, change) : change;
2563
+ if (this.session?.active) {
2564
+ this.flush();
2565
+ return;
2566
+ }
2567
+ if (this.frame === null) this.frame = this.win.requestAnimationFrame(() => this.flush());
2568
+ }
2569
+ /** Renders pending changes now. Called automatically on the next frame. */
2570
+ flush() {
2571
+ if (this.frame !== null) {
2572
+ this.win.cancelAnimationFrame(this.frame);
2573
+ this.frame = null;
2574
+ }
2575
+ const change = this.pendingChange;
2576
+ this.pendingChange = null;
2577
+ if (!change || !this.current) return;
2578
+ this.current.render(change);
2579
+ if (this.selection) this.current.showSelection(this.selection);
2580
+ if (this.popover) this.refreshMore();
2581
+ }
2582
+ merge(a, b) {
2583
+ for (const d of b.days) a.days.add(d);
2584
+ a.added.push(...b.added);
2585
+ a.updated.push(...b.updated);
2586
+ a.removed.push(...b.removed);
2587
+ a.spans ||= b.spans;
2588
+ return a;
2589
+ }
2590
+ scheduleTick() {
2591
+ const delay = 6e4 - this.now() % 6e4 + 50;
2592
+ this.timer = this.win.setTimeout(() => {
2593
+ if (this.destroyed) return;
2594
+ if (this.todayDay() !== this.today) this.rebuild(false);
2595
+ else this.current?.tick?.();
2596
+ this.scheduleTick();
2597
+ }, delay);
2598
+ }
2599
+ /* ========================================================= item content */
2600
+ itemArg(el, meta) {
2601
+ const item = this.store.get(meta.n.id)?.item ?? meta.n.item;
2602
+ const { start, end } = this.timeLabel(meta.n, meta.variant, meta.day);
2603
+ return {
2604
+ item,
2605
+ el,
2606
+ view: this.view,
2607
+ day: meta.day,
2608
+ isStart: meta.isStart,
2609
+ isEnd: meta.isEnd,
2610
+ isMirror: meta.mirror,
2611
+ variant: meta.variant,
2612
+ timeText: meta.n.allDay ? "" : end ? `${start} \u2013 ${end}` : start
2613
+ };
2614
+ }
2615
+ /** Start and (for timed and list items) end labels; the end is hidden on short items by CSS. */
2616
+ timeLabel(n, variant, day) {
2617
+ if (n.allDay) return { start: variant === "list" ? this.text.allDay : "", end: "" };
2618
+ const s = wallTime(n.startMs, this.zone);
2619
+ const start = this.fmt.time(s.minute, true);
2620
+ if (variant === "dot" || variant === "bar" || variant === "indicator") return { start: s.day === day ? start : "", end: "" };
2621
+ if (n.startMs === n.endMs) return { start, end: "" };
2622
+ return { start, end: this.fmt.time(wallTime(n.endMs, this.zone).minute, true) };
2623
+ }
2624
+ fillTime(el, n, variant, day) {
2625
+ const { start, end } = this.timeLabel(n, variant, day);
2626
+ el.textContent = start;
2627
+ if (end) el.append(h(this.doc, "span", "tnc-item-time-end", null, ` \u2013 ${end}`));
2628
+ return start !== "";
2629
+ }
2630
+ ariaLabel(n) {
2631
+ if (n.allDay) {
2632
+ const last = n.endDay - 1;
2633
+ return `${n.item.title}, ${last > n.startDay ? this.fmt.range(n.startDay, last) : this.fmt.longDate(n.startDay)}`;
2634
+ }
2635
+ const s = wallTime(n.startMs, this.zone);
2636
+ const e = wallTime(n.endMs, this.zone);
2637
+ return `${n.item.title}, ${this.fmt.longDate(s.day)} ${this.fmt.time(s.minute)} \u2013 ${this.fmt.time(e.minute)}`;
2638
+ }
2639
+ defaultContent(meta) {
2640
+ const { n, variant, day } = meta;
2641
+ const doc = this.doc;
2642
+ const main = h(doc, "div", "tnc-item-main");
2643
+ const title = h(doc, "span", "tnc-item-title", null, n.item.title);
2644
+ const time = h(doc, "span", "tnc-item-time");
2645
+ const label = this.fillTime(time, n, variant, day);
2646
+ switch (variant) {
2647
+ case "timed":
2648
+ main.append(title, time);
2649
+ break;
2650
+ case "dot":
2651
+ main.append(h(doc, "span", "tnc-item-dot", { "aria-hidden": "true" }), time, title);
2652
+ break;
2653
+ case "list":
2654
+ main.append(time, h(doc, "span", "tnc-item-dot", { "aria-hidden": "true" }), title);
2655
+ break;
2656
+ default:
2657
+ if (label) main.append(time);
2658
+ main.append(title);
2659
+ }
2660
+ return main;
2661
+ }
2662
+ addHandles(el, meta) {
2663
+ const { n, variant } = meta;
2664
+ const ports = variant === "timeline" && !meta.mirror && n.id !== EXTERNAL_ID && !!this.opts.timeline?.onLink;
2665
+ if (variant === "dot" || !this.isEditable(n, "duration")) {
2666
+ if (ports) this.addPorts(el, n);
2667
+ return;
2668
+ }
2669
+ const vertical = variant === "timed";
2670
+ const milestone = variant === "timeline" && n.item.kind === "milestone";
2671
+ const handleLabel = variant === "timeline" ? this.opts.timeline?.resizeHandleLabel : void 0;
2672
+ const handle = (edge) => {
2673
+ const label = handleLabel?.({ item: n.item, edge: edge === "start" ? "start" : "finish" });
2674
+ const attrs = label ? { role: "button", tabindex: "0", "aria-label": label } : { "aria-hidden": "true" };
2675
+ el.append(h(this.doc, "div", `tnc-resize tnc-resize-${edge} ${vertical ? "is-v" : "is-h"}`, attrs));
2676
+ };
2677
+ if (meta.isStart && (this.opts.resizableFromStart || variant === "timeline") && !milestone) handle("start");
2678
+ if (meta.isEnd && !milestone) handle("end");
2679
+ if (ports) this.addPorts(el, n);
2680
+ }
2681
+ /** Start and finish link handles (a milestone has one). */
2682
+ addPorts(el, n) {
2683
+ const edges = n.item.kind === "milestone" ? ["finish"] : ["start", "finish"];
2684
+ for (const edge of edges) {
2685
+ el.append(h(this.doc, "span", `tnc-port tnc-port-${edge}`, { "aria-hidden": "true", "data-edge": edge, title: "Drag to link" }));
2686
+ }
2687
+ }
2688
+ /* ============================================================== popover */
2689
+ refreshMore() {
2690
+ const p = this.popover;
2691
+ if (!p) return;
2692
+ const items = this.store.inRange(p.day, p.day + 1).filter((n) => n.item.display !== "background").sort((a, b) => Number(b.allDay) - Number(a.allDay) || this.order(a, b));
2693
+ p.pool.begin();
2694
+ for (const n of items) {
2695
+ p.pool.place(n.id, { n, day: p.day, isStart: n.startDay >= p.day, isEnd: n.endDay <= p.day + 1, variant: n.allDay ? "bar" : "dot", mirror: false });
2696
+ }
2697
+ p.pool.end();
2698
+ }
2699
+ closeMore(restoreFocus = false) {
2700
+ const p = this.popover;
2701
+ if (!p) return;
2702
+ this.popover = null;
2703
+ p.pool.clear();
2704
+ p.el.remove();
2705
+ if (restoreFocus && p.anchor.isConnected) p.anchor.focus();
2706
+ }
2707
+ /* =========================================================== interaction */
2708
+ bindEvents() {
2709
+ const r = this.root;
2710
+ r.addEventListener("pointerdown", this.onPointerDown);
2711
+ r.addEventListener("click", this.onClickCapture, true);
2712
+ r.addEventListener("click", this.onClick);
2713
+ r.addEventListener("dblclick", this.onDblClick);
2714
+ r.addEventListener("contextmenu", this.onContextMenu);
2715
+ r.addEventListener("mouseover", this.onMouseOver);
2716
+ r.addEventListener("mouseout", this.onMouseOut);
2717
+ r.addEventListener("keydown", this.onKeyDown);
2718
+ r.addEventListener("dragover", this.onNativeDragOver);
2719
+ r.addEventListener("dragleave", this.onNativeDragLeave);
2720
+ r.addEventListener("drop", this.onNativeDrop);
2721
+ r.addEventListener("touchmove", this.onTouchMove, { passive: false });
2722
+ r.addEventListener("pointerover", this.onAdoptCheck);
2723
+ r.addEventListener("focusin", this.onAdoptCheck);
2724
+ this.doc.addEventListener("pointerdown", this.onDocPointerDown, true);
2725
+ }
2726
+ unbindEvents() {
2727
+ const r = this.root;
2728
+ r.removeEventListener("pointerdown", this.onPointerDown);
2729
+ r.removeEventListener("click", this.onClickCapture, true);
2730
+ r.removeEventListener("click", this.onClick);
2731
+ r.removeEventListener("dblclick", this.onDblClick);
2732
+ r.removeEventListener("contextmenu", this.onContextMenu);
2733
+ r.removeEventListener("mouseover", this.onMouseOver);
2734
+ r.removeEventListener("mouseout", this.onMouseOut);
2735
+ r.removeEventListener("keydown", this.onKeyDown);
2736
+ r.removeEventListener("dragover", this.onNativeDragOver);
2737
+ r.removeEventListener("dragleave", this.onNativeDragLeave);
2738
+ r.removeEventListener("drop", this.onNativeDrop);
2739
+ r.removeEventListener("touchmove", this.onTouchMove);
2740
+ r.removeEventListener("pointerover", this.onAdoptCheck);
2741
+ r.removeEventListener("focusin", this.onAdoptCheck);
2742
+ this.doc.removeEventListener("pointerdown", this.onDocPointerDown, true);
2743
+ this.listenDoc(false);
2744
+ }
2745
+ listenDoc(on) {
2746
+ const d = this.doc;
2747
+ const fn = on ? d.addEventListener.bind(d) : d.removeEventListener.bind(d);
2748
+ fn("pointermove", this.onPointerMove);
2749
+ fn("pointerup", this.onPointerUp);
2750
+ fn("pointercancel", this.onPointerCancel);
2751
+ fn("keydown", this.onDocKeyDown, true);
2752
+ }
2753
+ itemElFrom(target) {
2754
+ const el = target?.closest?.(".tnc-item");
2755
+ return el && this.root.contains(el) && this.meta.has(el) ? el : null;
2756
+ }
2757
+ onDocPointerDown = (ev) => {
2758
+ const t = ev.target;
2759
+ if (this.popover && !this.popover.el.contains(t) && !this.popover.anchor.contains(t)) this.closeMore();
2760
+ if (this.selection && !this.root.contains(t)) this.unselect();
2761
+ };
2762
+ onPointerDown = (ev) => {
2763
+ if (ev.button !== 0 || this.session) return;
2764
+ const target = ev.target;
2765
+ if (target.closest("button, a, input, select, textarea, .tnc-toolbar, .tnc-port, [data-tnc-no-drag]")) {
2766
+ if (!target.closest(".tnc-item")) return;
2767
+ if (target.closest(".tnc-port, [data-tnc-no-drag], input, select, textarea")) return;
2768
+ }
2769
+ const itemEl = this.itemElFrom(target);
2770
+ let mode = null;
2771
+ let origin = null;
2772
+ if (itemEl) {
2773
+ const meta = this.meta.get(itemEl);
2774
+ origin = this.store.get(meta.n.id) ?? null;
2775
+ if (!origin || origin.id === EXTERNAL_ID) return;
2776
+ if (target.closest(".tnc-resize-end")) mode = this.isEditable(origin, "duration") ? "resize-end" : null;
2777
+ else if (target.closest(".tnc-resize-start")) mode = this.isEditable(origin, "duration") ? "resize-start" : null;
2778
+ else mode = this.isEditable(origin, "start") ? "move" : null;
2779
+ if (!mode) return;
2780
+ } else {
2781
+ if (!this.opts.selectable || !this.current) return;
2782
+ if (target.closest(".tnc-popover, .tnc-more")) return;
2783
+ const hit = this.current.hit(ev.clientX, ev.clientY);
2784
+ if (!hit || hit.kind === "timeline" && !hit.overTrack) return;
2785
+ mode = "select";
2786
+ }
2787
+ const touch = ev.pointerType === "touch";
2788
+ const s = {
2789
+ mode,
2790
+ pointerId: ev.pointerId,
2791
+ pointerType: ev.pointerType,
2792
+ x0: ev.clientX,
2793
+ y0: ev.clientY,
2794
+ active: false,
2795
+ origin,
2796
+ grab: this.current?.hit(ev.clientX, ev.clientY) ?? null,
2797
+ proposal: null,
2798
+ denied: false,
2799
+ timer: null,
2800
+ lastEvent: ev
2801
+ };
2802
+ this.session = s;
2803
+ if (touch && itemEl?.classList.contains("is-draggable")) {
2804
+ const el = itemEl.closest(".tnc-tg-scroll, .tnc-dg-scroll, .tnc-tl-scroll, .tnc-list-scroll, .tnc-year-scroll");
2805
+ if (el) s.pan = { el, left: el.scrollLeft, top: el.scrollTop, active: false };
2806
+ }
2807
+ if (touch) {
2808
+ s.timer = this.win.setTimeout(() => {
2809
+ if (this.session !== s) return;
2810
+ this.activate(s);
2811
+ navigator.vibrate?.(10);
2812
+ }, this.opts.longPressDelay);
2813
+ } else if (mode !== "select") {
2814
+ ev.preventDefault();
2815
+ }
2816
+ this.listenDoc(true);
2817
+ };
2818
+ activate(s) {
2819
+ s.active = true;
2820
+ if (s.timer !== null) this.win.clearTimeout(s.timer);
2821
+ s.timer = null;
2822
+ if (s.origin) {
2823
+ this.draggingId = s.origin.id;
2824
+ for (const el of this.elementsFor(s.origin.id)) el.classList.add("is-dragging");
2825
+ }
2826
+ this.closeMore();
2827
+ if (s.mode !== "select") this.unselectSilently();
2828
+ this.root.classList.add("tnc-dragging", `tnc-dragging-${s.mode}`);
2829
+ if (s.lastEvent) this.track(s, s.lastEvent);
2830
+ }
2831
+ onTouchMove = (ev) => {
2832
+ if (this.session?.active) ev.preventDefault();
2833
+ };
2834
+ onPointerMove = (ev) => {
2835
+ const s = this.session;
2836
+ if (!s || ev.pointerId !== s.pointerId) return;
2837
+ s.lastEvent = ev;
2838
+ if (!s.active) {
2839
+ const dist = Math.hypot(ev.clientX - s.x0, ev.clientY - s.y0);
2840
+ if (s.pointerType === "touch") {
2841
+ const pan = s.pan;
2842
+ if (pan && (pan.active || dist > 8)) {
2843
+ if (!pan.active && s.timer !== null) this.win.clearTimeout(s.timer);
2844
+ s.timer = null;
2845
+ pan.active = true;
2846
+ pan.el.scrollLeft = pan.left - (ev.clientX - s.x0);
2847
+ pan.el.scrollTop = pan.top - (ev.clientY - s.y0);
2848
+ } else if (dist > 8) this.endSession(null, true);
2849
+ return;
2850
+ }
2851
+ if (dist < 4) return;
2852
+ this.activate(s);
2853
+ return;
2854
+ }
2855
+ ev.preventDefault();
2856
+ this.track(s, ev);
2857
+ };
2858
+ onPointerUp = (ev) => {
2859
+ const s = this.session;
2860
+ if (!s || ev.pointerId !== s.pointerId) return;
2861
+ if (!s.active) {
2862
+ this.endSession(null, true);
2863
+ if (s.pan?.active) {
2864
+ this.suppressNextClick();
2865
+ return;
2866
+ }
2867
+ if (s.mode === "select" && s.grab) this.clickSlot(s.grab, ev, s.pointerType === "touch");
2868
+ return;
2869
+ }
2870
+ this.suppressNextClick();
2871
+ this.endSession(ev, false);
2872
+ };
2873
+ onPointerCancel = (ev) => {
2874
+ if (this.session && ev.pointerId === this.session.pointerId) this.endSession(null, true);
2875
+ };
2876
+ onDocKeyDown = (ev) => {
2877
+ if (ev.key === "Escape" && this.session) {
2878
+ ev.preventDefault();
2879
+ ev.stopPropagation();
2880
+ this.endSession(null, true);
2881
+ }
2882
+ };
2883
+ /** Proposal for the current pointer position. */
2884
+ track(s, ev) {
2885
+ const hit = this.current?.hit(ev.clientX, ev.clientY) ?? null;
2886
+ if (s.mode === "external") {
2887
+ this.trackExternal(s, hit);
2888
+ return;
2889
+ }
2890
+ if (!s.grab) s.grab = hit;
2891
+ if (!hit || !s.grab) return;
2892
+ if (s.mode === "select") {
2893
+ const sel = this.selectionFor(s.grab, hit);
2894
+ if (!sel) return;
2895
+ this.selection = sel;
2896
+ this.current?.showSelection(sel);
2897
+ return;
2898
+ }
2899
+ const origin = s.origin;
2900
+ const p = s.mode === "move" ? this.moveProposal(origin, s.grab, hit) : this.resizeProposal(origin, hit, s.mode === "resize-start");
2901
+ if (!p) return;
2902
+ const allowed = this.opts.canDrop?.({ item: origin.item, start: new Date(p.startMs), end: new Date(p.endMs), allDay: p.allDay, rowId: p.rowId }) ?? true;
2903
+ s.denied = !allowed;
2904
+ this.root.classList.toggle("tnc-drop-denied", !allowed);
2905
+ if (!allowed || sameProposal(p, s.proposal)) return;
2906
+ s.proposal = p;
2907
+ this.dragRow = p.rowId !== void 0 ? { id: origin.id, rowId: p.rowId } : null;
2908
+ this.applyChange(this.store.upsert([this.withProposal(origin, p)]));
2909
+ }
2910
+ trackExternal(s, raw) {
2911
+ const src = s.source;
2912
+ const hit = raw?.kind === "timeline" && raw.area !== "track" && raw.area !== "header" ? null : raw;
2913
+ s.hit = hit;
2914
+ if (!hit) {
2915
+ if (s.proposal) {
2916
+ s.proposal = null;
2917
+ this.applyChange(this.store.remove([EXTERNAL_ID]));
2918
+ }
2919
+ this.unselectSilently();
2920
+ return;
2921
+ }
2922
+ const dur = src.durationMinutes ?? this.opts.defaultTimedMinutes;
2923
+ let p;
2924
+ if (hit.kind === "time" || hit.kind === "timeline" && this.timelineSnap() < MINUTES_PER_DAY) {
2925
+ const snap = hit.kind === "time" ? this.slot.snap : this.timelineSnap();
2926
+ const startCivil = Math.floor((hit.day * MINUTES_PER_DAY + hit.minute) / snap) * snap;
2927
+ const startMs = this.civilToMs(startCivil);
2928
+ const endMs = this.civilToMs(startCivil + dur);
2929
+ p = { startMs, endMs, allDay: false, startDay: dayOf(startMs, this.zone), endDay: dayOf(endMs - 1, this.zone) + 1 };
2930
+ } else {
2931
+ const day = hit.day;
2932
+ p = this.dayProposal(day, day + 1);
2933
+ }
2934
+ if (hit.kind === "timeline" && hit.overTrack && hit.rowId !== null) p.rowId = hit.rowId;
2935
+ const allowed = this.opts.canDrop?.({ item: this.externalItem(src, p), start: new Date(p.startMs), end: new Date(p.endMs), allDay: p.allDay, rowId: p.rowId }) ?? true;
2936
+ s.denied = !allowed;
2937
+ this.root.classList.toggle("tnc-drop-denied", !allowed);
2938
+ if (sameProposal(p, s.proposal)) return;
2939
+ s.proposal = p;
2940
+ if (hit.kind === "timeline" && p.rowId === void 0) {
2941
+ this.selection = { ...p };
2942
+ this.current?.showSelection(this.selection);
2943
+ } else this.unselectSilently();
2944
+ this.dragRow = p.rowId !== void 0 ? { id: EXTERNAL_ID, rowId: p.rowId } : null;
2945
+ this.applyChange(this.store.upsert([this.externalItem(src, p)]));
2946
+ }
2947
+ externalItem(src, p) {
2948
+ return {
2949
+ id: EXTERNAL_ID,
2950
+ title: src.title,
2951
+ kind: src.kind,
2952
+ color: src.color,
2953
+ start: p.allDay ? formatDay(p.startDay) : p.startMs,
2954
+ end: p.allDay ? formatDay(p.endDay) : p.endMs,
2955
+ allDay: p.allDay,
2956
+ editable: false,
2957
+ data: src.data,
2958
+ rev: `${p.startMs}:${p.endMs}:${p.allDay ? 1 : 0}:${p.rowId ?? ""}`
2959
+ };
2960
+ }
2961
+ timelineSnap() {
2962
+ return this.opts.timeline?.scale?.snapMinutes ?? MINUTES_PER_DAY;
2963
+ }
2964
+ civil(ms) {
2965
+ const w = wallTime(ms, this.zone);
2966
+ return w.day * MINUTES_PER_DAY + w.minute;
2967
+ }
2968
+ civilToMs(c) {
2969
+ const day = Math.floor(c / MINUTES_PER_DAY);
2970
+ return toInstant(day, c - day * MINUTES_PER_DAY, this.zone);
2971
+ }
2972
+ hitCivil(hit) {
2973
+ return hit.kind === "time" || hit.kind === "timeline" ? hit.day * MINUTES_PER_DAY + hit.minute : hit.day * MINUTES_PER_DAY;
2974
+ }
2975
+ dayProposal(startDay, endDay) {
2976
+ return {
2977
+ startMs: toInstant(startDay, 0, this.zone),
2978
+ endMs: toInstant(endDay, 0, this.zone),
2979
+ allDay: true,
2980
+ startDay,
2981
+ endDay
2982
+ };
2983
+ }
2984
+ timedProposal(startMs, endMs) {
2985
+ return { startMs, endMs, allDay: false, startDay: dayOf(startMs, this.zone), endDay: dayOf(Math.max(startMs, endMs - 1), this.zone) + 1 };
2986
+ }
2987
+ moveProposal(o, grab, hit) {
2988
+ const days = o.endDay - o.startDay;
2989
+ if (hit.kind === "timeline") {
2990
+ const snap2 = o.allDay ? Math.max(MINUTES_PER_DAY, this.timelineSnap()) : this.timelineSnap();
2991
+ const delta2 = Math.round((this.hitCivil(hit) - this.hitCivil(grab)) / snap2) * snap2;
2992
+ let p;
2993
+ if (o.allDay) {
2994
+ const dd = Math.round(delta2 / MINUTES_PER_DAY);
2995
+ p = this.dayProposal(o.startDay + dd, o.endDay + dd);
2996
+ } else {
2997
+ const startMs2 = this.civilToMs(this.civil(o.startMs) + delta2);
2998
+ p = this.timedProposal(startMs2, startMs2 + (o.endMs - o.startMs));
2999
+ }
3000
+ p.rowId = (hit.overTrack ? hit.rowId : null) ?? this.dragRow?.rowId ?? this.opts.timeline?.rowOf(o.item) ?? void 0;
3001
+ return p;
3002
+ }
3003
+ if (hit.kind === "allDay") {
3004
+ if (o.allDay) {
3005
+ const dd = hit.day - (grab.kind === "allDay" || grab.kind === "day" ? grab.day : hit.day);
3006
+ return this.dayProposal(o.startDay + dd, o.endDay + dd);
3007
+ }
3008
+ return this.dayProposal(hit.day, hit.day + this.opts.defaultAllDayDays);
3009
+ }
3010
+ if (hit.kind === "day") {
3011
+ const dd = hit.day - (grab.kind === "day" ? grab.day : hit.day);
3012
+ if (o.allDay) return this.dayProposal(o.startDay + dd, o.startDay + dd + days);
3013
+ const startMs2 = this.civilToMs(this.civil(o.startMs) + dd * MINUTES_PER_DAY);
3014
+ const endMs = this.civilToMs(this.civil(o.endMs) + dd * MINUTES_PER_DAY);
3015
+ return this.timedProposal(startMs2, endMs);
3016
+ }
3017
+ const snap = this.slot.snap;
3018
+ if (o.allDay || grab.kind !== "time") {
3019
+ const c = Math.floor((hit.day * MINUTES_PER_DAY + hit.minute) / snap) * snap;
3020
+ const startMs2 = this.civilToMs(c);
3021
+ return this.timedProposal(startMs2, this.civilToMs(c + this.opts.defaultTimedMinutes));
3022
+ }
3023
+ const delta = Math.round((this.hitCivil(hit) - this.hitCivil(grab)) / snap) * snap;
3024
+ const startCivil = this.civil(o.startMs) + delta;
3025
+ const startMs = this.civilToMs(startCivil);
3026
+ return this.timedProposal(startMs, startMs + (o.endMs - o.startMs));
3027
+ }
3028
+ resizeProposal(o, hit, fromStart) {
3029
+ if (o.allDay || hit.kind === "allDay" || hit.kind === "day") {
3030
+ if (hit.kind === "time") return null;
3031
+ const pointerDay = hit.kind === "timeline" ? Math.round(this.hitCivil(hit) / MINUTES_PER_DAY) : fromStart ? hit.day : hit.day + 1;
3032
+ if (!o.allDay) {
3033
+ const end = wallTime(o.endMs - 1, this.zone);
3034
+ const dd = (fromStart ? 0 : pointerDay - 1) - end.day;
3035
+ if (fromStart) return null;
3036
+ const endMs = Math.max(o.startMs + 6e4 * this.slot.snap, this.civilToMs(this.civil(o.endMs) + dd * MINUTES_PER_DAY));
3037
+ return this.timedProposal(o.startMs, endMs);
3038
+ }
3039
+ const p2 = fromStart ? this.dayProposal(Math.min(o.endDay - 1, pointerDay), o.endDay) : this.dayProposal(o.startDay, Math.max(o.startDay + 1, pointerDay));
3040
+ if (hit.kind === "timeline") p2.rowId = this.opts.timeline?.rowOf(o.item) ?? void 0;
3041
+ return p2;
3042
+ }
3043
+ const snap = hit.kind === "timeline" ? this.timelineSnap() : this.slot.snap;
3044
+ const c = Math.round(this.hitCivil(hit) / snap) * snap;
3045
+ const ms = this.civilToMs(c);
3046
+ const min = snap * 6e4;
3047
+ const p = fromStart ? this.timedProposal(Math.min(ms, o.endMs - min), o.endMs) : this.timedProposal(o.startMs, Math.max(ms, o.startMs + min));
3048
+ if (hit.kind === "timeline") p.rowId = this.opts.timeline?.rowOf(o.item) ?? void 0;
3049
+ return p;
3050
+ }
3051
+ withProposal(o, p) {
3052
+ return {
3053
+ ...o.item,
3054
+ start: p.allDay ? formatDay(p.startDay) : p.startMs,
3055
+ end: p.allDay ? formatDay(p.endDay) : p.endMs,
3056
+ allDay: p.allDay,
3057
+ // Unique per proposal, so the store redraws the item wherever the pointer takes it.
3058
+ rev: `drag:${p.startMs}:${p.endMs}:${p.allDay ? 1 : 0}:${p.rowId ?? ""}:${o.sig}`
3059
+ };
3060
+ }
3061
+ selectionFor(a, b) {
3062
+ if (a.kind !== b.kind) {
3063
+ if (!(a.kind === "allDay" && b.kind === "time") && !(a.kind === "time" && b.kind === "allDay")) return null;
3064
+ b = a.kind === "allDay" ? { kind: "allDay", day: b.day } : { kind: "time", day: b.day, minute: a.minute };
3065
+ }
3066
+ if (a.kind === "time" || a.kind === "timeline" && this.timelineSnap() < MINUTES_PER_DAY) {
3067
+ const snap = a.kind === "time" ? this.slot.snap : this.timelineSnap();
3068
+ const ca = Math.floor(this.hitCivil(a) / snap) * snap;
3069
+ const cb = Math.floor(this.hitCivil(b) / snap) * snap;
3070
+ const startMs = this.civilToMs(Math.min(ca, cb));
3071
+ const endMs = this.civilToMs(Math.max(ca, cb) + snap);
3072
+ const sel2 = { ...this.timedProposal(startMs, endMs) };
3073
+ if (a.kind === "timeline" && a.overTrack && a.rowId !== null) sel2.rowId = a.rowId;
3074
+ return sel2;
3075
+ }
3076
+ const da = a.day;
3077
+ const db = b.day;
3078
+ const sel = this.dayProposal(Math.min(da, db), Math.max(da, db) + 1);
3079
+ if (a.kind === "timeline" && a.overTrack && a.rowId !== null) sel.rowId = a.rowId;
3080
+ return sel;
3081
+ }
3082
+ clickSlot(hit, ev, touch) {
3083
+ if (hit.kind === "timeline" && !hit.overTrack) return;
3084
+ const day = hit.day;
3085
+ if (this.opts.onDateClick) {
3086
+ let minute = null;
3087
+ if (hit.kind === "time") minute = Math.floor(hit.minute / this.slot.duration) * this.slot.duration;
3088
+ else if (hit.kind === "timeline" && this.timelineSnap() < MINUTES_PER_DAY) minute = Math.floor(hit.minute / this.timelineSnap()) * this.timelineSnap();
3089
+ const civil = day * MINUTES_PER_DAY + (minute ?? 0);
3090
+ const at = Math.floor(civil / MINUTES_PER_DAY);
3091
+ const startStr = minute === null ? formatDay(day) : formatFloating(this.civilToMs(civil), this.zone);
3092
+ this.opts.onDateClick({
3093
+ ...this.dayArg(minute === null ? day : at),
3094
+ minute: minute === null ? null : civil - at * MINUTES_PER_DAY,
3095
+ allDay: minute === null,
3096
+ startStr,
3097
+ jsEvent: ev
3098
+ });
3099
+ }
3100
+ if (touch || !this.opts.selectable) return;
3101
+ const sel = hit.kind === "time" ? (() => {
3102
+ const c = Math.floor(this.hitCivil(hit) / this.slot.duration) * this.slot.duration;
3103
+ return this.timedProposal(this.civilToMs(c), this.civilToMs(c + this.slot.duration));
3104
+ })() : this.selectionFor(hit, hit);
3105
+ if (!sel) return;
3106
+ this.selection = sel;
3107
+ this.current?.showSelection(sel);
3108
+ this.fireSelect(sel, ev);
3109
+ }
3110
+ fireSelect(sel, ev) {
3111
+ this.opts.onSelect?.({
3112
+ start: new Date(sel.startMs),
3113
+ end: new Date(sel.endMs),
3114
+ allDay: sel.allDay,
3115
+ startStr: sel.allDay ? formatDay(sel.startDay) : formatFloating(sel.startMs, this.zone),
3116
+ endStr: sel.allDay ? formatDay(sel.endDay) : formatFloating(sel.endMs, this.zone),
3117
+ jsEvent: ev,
3118
+ view: this.view,
3119
+ rowId: sel.rowId
3120
+ });
3121
+ }
3122
+ unselectSilently() {
3123
+ this.selection = null;
3124
+ this.current?.showSelection(null);
3125
+ }
3126
+ /** Ends the current pointer session, committing unless `cancel`. */
3127
+ endSession(ev, cancel) {
3128
+ const s = this.session;
3129
+ if (!s) return;
3130
+ this.session = null;
3131
+ if (s.timer !== null) this.win.clearTimeout(s.timer);
3132
+ this.listenDoc(false);
3133
+ this.root.classList.remove(
3134
+ "tnc-dragging",
3135
+ "tnc-drop-denied",
3136
+ "tnc-dragging-move",
3137
+ "tnc-dragging-resize-end",
3138
+ "tnc-dragging-resize-start",
3139
+ "tnc-dragging-select",
3140
+ "tnc-dragging-external"
3141
+ );
3142
+ const draggedId = this.draggingId;
3143
+ this.draggingId = null;
3144
+ this.dragRow = null;
3145
+ if (draggedId) for (const el of this.elementsFor(draggedId)) el.classList.remove("is-dragging");
3146
+ if (!s.active) return;
3147
+ if (s.mode === "select") {
3148
+ if (cancel) this.unselectSilently();
3149
+ else if (this.selection) this.fireSelect(this.selection, ev);
3150
+ return;
3151
+ }
3152
+ if (s.mode === "external") {
3153
+ const p2 = s.proposal;
3154
+ if (p2) this.applyChange(this.store.remove([EXTERNAL_ID]));
3155
+ this.unselectSilently();
3156
+ const dropped = !cancel && !!p2 && !s.denied && !!ev;
3157
+ const hit = s.hit ?? null;
3158
+ const ended = () => {
3159
+ if (s.source) this.opts.onExternalDragEnd?.({ source: s.source, dropped, hit, jsEvent: ev });
3160
+ };
3161
+ if (!dropped || !p2 || !ev) {
3162
+ ended();
3163
+ return;
3164
+ }
3165
+ this.flush();
3166
+ this.opts.onExternalDrop?.({
3167
+ start: new Date(p2.startMs),
3168
+ end: new Date(p2.endMs),
3169
+ allDay: p2.allDay,
3170
+ startStr: p2.allDay ? formatDay(p2.startDay) : formatFloating(p2.startMs, this.zone),
3171
+ endStr: p2.allDay ? formatDay(p2.endDay) : formatFloating(p2.endMs, this.zone),
3172
+ jsEvent: ev,
3173
+ view: this.view,
3174
+ rowId: p2.rowId,
3175
+ source: s.source ?? null,
3176
+ hit,
3177
+ dataTransfer: null
3178
+ });
3179
+ ended();
3180
+ return;
3181
+ }
3182
+ const origin = s.origin;
3183
+ const p = s.proposal;
3184
+ const revert = () => {
3185
+ const cur = this.store.get(origin.id);
3186
+ if (cur && cur.sig !== origin.sig) this.applyChange(this.store.upsert([origin.item]));
3187
+ this.flush();
3188
+ };
3189
+ if (cancel || s.denied || !p) {
3190
+ revert();
3191
+ return;
3192
+ }
3193
+ const changed = p.startMs !== origin.startMs || p.endMs !== origin.endMs || p.allDay !== origin.allDay || p.rowId !== void 0 && p.rowId !== this.opts.timeline?.rowOf(origin.item);
3194
+ if (!changed) {
3195
+ revert();
3196
+ return;
3197
+ }
3198
+ this.flush();
3199
+ this.commit(s.mode === "move" ? "drop" : "resize", origin, p, ev, revert);
3200
+ }
3201
+ commit(kind, origin, p, ev, revert) {
3202
+ const arg = {
3203
+ item: origin.item,
3204
+ start: new Date(p.startMs),
3205
+ end: new Date(p.endMs),
3206
+ allDay: p.allDay,
3207
+ startStr: p.allDay ? formatDay(p.startDay) : formatFloating(p.startMs, this.zone),
3208
+ endStr: p.allDay ? formatDay(p.endDay) : formatFloating(p.endMs, this.zone),
3209
+ oldStart: new Date(origin.startMs),
3210
+ oldEnd: new Date(origin.endMs),
3211
+ oldAllDay: origin.allDay,
3212
+ delta: p.startMs - origin.startMs,
3213
+ rowId: p.rowId,
3214
+ jsEvent: ev,
3215
+ source: ev && "key" in ev ? "keyboard" : "pointer",
3216
+ view: this.view,
3217
+ revert
3218
+ };
3219
+ const handler = kind === "drop" ? this.opts.onItemDrop : this.opts.onItemResize;
3220
+ try {
3221
+ const res = handler?.(arg);
3222
+ if (res && typeof res.then === "function") {
3223
+ Promise.resolve(res).catch(() => revert());
3224
+ }
3225
+ } catch (err) {
3226
+ revert();
3227
+ throw err;
3228
+ }
3229
+ }
3230
+ /* --------------------------------------------------------- click / keys */
3231
+ /** Runs in the capture phase so the click ending a drag never reaches content inside the calendar, React portals included. */
3232
+ onClickCapture = (ev) => {
3233
+ if (!this.suppressClick) return;
3234
+ this.suppressClick = false;
3235
+ ev.stopPropagation();
3236
+ ev.preventDefault();
3237
+ };
3238
+ onClick = (ev) => {
3239
+ const el = this.itemElFrom(ev.target);
3240
+ if (!el || ev.target.closest(".tnc-resize, .tnc-port")) return;
3241
+ const meta = this.meta.get(el);
3242
+ if (meta.n.id === EXTERNAL_ID) return;
3243
+ this.opts.onItemClick?.({ ...this.itemArg(el, meta), jsEvent: ev });
3244
+ };
3245
+ onDblClick = (ev) => {
3246
+ const el = this.itemElFrom(ev.target);
3247
+ if (!el) return;
3248
+ const meta = this.meta.get(el);
3249
+ this.opts.onItemDoubleClick?.({ ...this.itemArg(el, meta), jsEvent: ev });
3250
+ };
3251
+ onContextMenu = (ev) => {
3252
+ const el = this.itemElFrom(ev.target);
3253
+ if (!el) return;
3254
+ if (this.session?.active) {
3255
+ ev.preventDefault();
3256
+ return;
3257
+ }
3258
+ if (!this.opts.onItemContextMenu) return;
3259
+ ev.preventDefault();
3260
+ const meta = this.meta.get(el);
3261
+ this.opts.onItemContextMenu({ ...this.itemArg(el, meta), jsEvent: ev });
3262
+ };
3263
+ hoverId = null;
3264
+ onMouseOver = (ev) => {
3265
+ const el = this.itemElFrom(ev.target);
3266
+ const id = el ? this.meta.get(el).n.id : null;
3267
+ if (id === this.hoverId) return;
3268
+ this.leaveHover(ev);
3269
+ if (!el || !id || this.session?.active) return;
3270
+ this.hoverId = id;
3271
+ for (const other of this.elementsFor(id)) other.classList.add("is-hover");
3272
+ this.opts.onItemHover?.({ ...this.itemArg(el, this.meta.get(el)), jsEvent: ev, entering: true });
3273
+ };
3274
+ onMouseOut = (ev) => {
3275
+ const to = ev.relatedTarget;
3276
+ const toEl = to ? this.itemElFrom(to) : null;
3277
+ if (toEl && this.meta.get(toEl)?.n.id === this.hoverId) return;
3278
+ this.leaveHover(ev);
3279
+ };
3280
+ leaveHover(ev) {
3281
+ const id = this.hoverId;
3282
+ if (!id) return;
3283
+ this.hoverId = null;
3284
+ const els = this.elementsFor(id);
3285
+ for (const el2 of els) el2.classList.remove("is-hover");
3286
+ const el = els[0];
3287
+ const meta = el ? this.meta.get(el) : void 0;
3288
+ if (el && meta) this.opts.onItemHover?.({ ...this.itemArg(el, meta), jsEvent: ev, entering: false });
3289
+ }
3290
+ onKeyDown = (ev) => {
3291
+ if (ev.key === "Escape" && this.popover) {
3292
+ ev.preventDefault();
3293
+ ev.stopPropagation();
3294
+ this.closeMore(true);
3295
+ return;
3296
+ }
3297
+ const target = ev.target;
3298
+ if ((ev.key === "Enter" || ev.key === " ") && target.matches(".tnc-ym-day, .tnc-dg-day[tabindex]")) {
3299
+ const day = parseDay(target.dataset.date ?? "");
3300
+ if (day !== null) {
3301
+ ev.preventDefault();
3302
+ this.clickSlot({ kind: "day", day }, ev, true);
3303
+ }
3304
+ return;
3305
+ }
3306
+ const el = this.itemElFrom(ev.target);
3307
+ if (!el) return;
3308
+ const grip = ev.target.closest?.(".tnc-resize[tabindex]");
3309
+ if (grip && el.contains(grip) && (ev.key === "ArrowLeft" || ev.key === "ArrowRight") && !ev.ctrlKey && !ev.metaKey) {
3310
+ ev.preventDefault();
3311
+ this.nudge(this.meta.get(el).n.id, ev, grip.classList.contains("tnc-resize-start") ? "start" : "end");
3312
+ return;
3313
+ }
3314
+ if (ev.target !== el) return;
3315
+ const meta = this.meta.get(el);
3316
+ if (ev.key === "Enter" || ev.key === " ") {
3317
+ ev.preventDefault();
3318
+ this.opts.onItemClick?.({ ...this.itemArg(el, meta), jsEvent: ev });
3319
+ return;
3320
+ }
3321
+ if (ev.key === "ContextMenu" || ev.key === "F10" && ev.shiftKey) {
3322
+ ev.preventDefault();
3323
+ this.opts.onItemContextMenu?.({ ...this.itemArg(el, meta), jsEvent: ev });
3324
+ return;
3325
+ }
3326
+ if (ev.altKey && ev.key.startsWith("Arrow")) {
3327
+ ev.preventDefault();
3328
+ this.nudge(meta.n.id, ev);
3329
+ }
3330
+ };
3331
+ /** Alt+Arrow moves an item; Alt+Shift+Arrow changes its end; arrows on a focused grip move that edge. */
3332
+ nudge(id, ev, edge = null) {
3333
+ const o = this.store.get(id);
3334
+ if (!o) return;
3335
+ const resize = edge !== null || ev.shiftKey;
3336
+ const fromStart = edge === "start";
3337
+ if (!this.isEditable(o, resize ? "duration" : "start")) return;
3338
+ const view = this.opts.view;
3339
+ const timeStep = view === "timeline" ? this.timelineSnap() : this.slot.snap;
3340
+ let dMin = 0;
3341
+ switch (ev.key) {
3342
+ case "ArrowLeft":
3343
+ dMin = view === "timeline" ? -Math.max(timeStep, o.allDay ? MINUTES_PER_DAY : 0) : -MINUTES_PER_DAY;
3344
+ break;
3345
+ case "ArrowRight":
3346
+ dMin = view === "timeline" ? Math.max(timeStep, o.allDay ? MINUTES_PER_DAY : 0) : MINUTES_PER_DAY;
3347
+ break;
3348
+ case "ArrowUp":
3349
+ dMin = view === "month" ? -7 * MINUTES_PER_DAY : o.allDay ? 0 : -timeStep;
3350
+ break;
3351
+ case "ArrowDown":
3352
+ dMin = view === "month" ? 7 * MINUTES_PER_DAY : o.allDay ? 0 : timeStep;
3353
+ break;
3354
+ }
3355
+ if (!dMin) return;
3356
+ let p;
3357
+ if (o.allDay) {
3358
+ const dd = Math.round(dMin / MINUTES_PER_DAY);
3359
+ if (!dd) return;
3360
+ if (fromStart) p = this.dayProposal(Math.min(o.endDay - 1, o.startDay + dd), o.endDay);
3361
+ else if (resize) p = this.dayProposal(o.startDay, Math.max(o.startDay + 1, o.endDay + dd));
3362
+ else p = this.dayProposal(o.startDay + dd, o.endDay + dd);
3363
+ } else if (fromStart) {
3364
+ p = this.timedProposal(Math.min(o.endMs - timeStep * 6e4, this.civilToMs(this.civil(o.startMs) + dMin)), o.endMs);
3365
+ } else if (resize) {
3366
+ p = this.timedProposal(o.startMs, Math.max(o.startMs + timeStep * 6e4, this.civilToMs(this.civil(o.endMs) + dMin)));
3367
+ } else {
3368
+ const startMs = this.civilToMs(this.civil(o.startMs) + dMin);
3369
+ p = this.timedProposal(startMs, startMs + (o.endMs - o.startMs));
3370
+ }
3371
+ const allowed = this.opts.canDrop?.({ item: o.item, start: new Date(p.startMs), end: new Date(p.endMs), allDay: p.allDay }) ?? true;
3372
+ if (!allowed) return;
3373
+ const revert = () => {
3374
+ this.applyChange(this.store.upsert([o.item]));
3375
+ this.flush();
3376
+ };
3377
+ if (p.startMs === o.startMs && p.endMs === o.endMs) return;
3378
+ this.applyChange(this.store.upsert([this.withProposal(o, p)]));
3379
+ this.flush();
3380
+ const next = this.elementsFor(id)[0];
3381
+ const grip = edge ? next?.querySelector(`.tnc-resize-${edge}[tabindex]`) : null;
3382
+ (grip ?? next)?.focus();
3383
+ this.commit(resize ? "resize" : "drop", o, p, ev, revert);
3384
+ }
3385
+ /* ---------------------------------------------------------- native drop */
3386
+ onNativeDragOver = (ev) => {
3387
+ const dt = ev.dataTransfer;
3388
+ if (!dt || !this.opts.acceptNativeDrop?.(dt) || !this.current) return;
3389
+ ev.preventDefault();
3390
+ dt.dropEffect = "copy";
3391
+ const hit = this.current.hit(ev.clientX, ev.clientY);
3392
+ if (!hit || hit.kind === "timeline" && hit.area !== "track" && hit.area !== "header") return;
3393
+ const sel = this.selectionFor(hit, hit);
3394
+ if (!sel) return;
3395
+ this.selection = sel;
3396
+ this.current.showSelection(sel);
3397
+ };
3398
+ onNativeDragLeave = (ev) => {
3399
+ if (ev.relatedTarget && this.root.contains(ev.relatedTarget)) return;
3400
+ this.unselectSilently();
3401
+ };
3402
+ onNativeDrop = (ev) => {
3403
+ const dt = ev.dataTransfer;
3404
+ if (!dt || !this.opts.acceptNativeDrop?.(dt) || !this.current) return;
3405
+ ev.preventDefault();
3406
+ const hit = this.current.hit(ev.clientX, ev.clientY);
3407
+ this.unselectSilently();
3408
+ if (!hit || hit.kind === "timeline" && hit.area !== "track" && hit.area !== "header") return;
3409
+ const sel = this.selectionFor(hit, hit);
3410
+ if (!sel) return;
3411
+ this.opts.onExternalDrop?.({
3412
+ start: new Date(sel.startMs),
3413
+ end: new Date(sel.endMs),
3414
+ allDay: sel.allDay,
3415
+ startStr: sel.allDay ? formatDay(sel.startDay) : formatFloating(sel.startMs, this.zone),
3416
+ endStr: sel.allDay ? formatDay(sel.endDay) : formatFloating(sel.endMs, this.zone),
3417
+ jsEvent: null,
3418
+ view: this.view,
3419
+ rowId: sel.rowId,
3420
+ source: null,
3421
+ hit,
3422
+ dataTransfer: dt
3423
+ });
3424
+ };
3425
+ };
3426
+
3427
+ export {
3428
+ Formatter,
3429
+ localeFirstDay,
3430
+ TIMELINE_UNIT_DAYS,
3431
+ DEFAULT_TEXT,
3432
+ defaultOptions,
3433
+ LAYOUT_KEYS,
3434
+ Calendar
3435
+ };