@tasknotes/calendar 0.0.0-stage → 0.1.0

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 +12 -0
  2. package/LICENSE +21 -0
  3. package/README.md +163 -2
  4. package/dist/calendar.css +1345 -0
  5. package/dist/cjs/core.cjs +716 -0
  6. package/dist/cjs/index.cjs +4206 -0
  7. package/dist/cjs/react.cjs +4034 -0
  8. package/dist/esm/chunk-DVMZ5OXN.js +694 -0
  9. package/dist/esm/chunk-L4WKZ6WT.js +0 -0
  10. package/dist/esm/chunk-UWCSL5AN.js +3401 -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 +206 -0
  31. package/dist/types/dom/calendar.d.ts +206 -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,3401 @@
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(0, ${w}fr)`).join(" ")}`;
821
+ this.el.style.setProperty("--tnc-cols", tpl);
822
+ const gutterHead = h(doc, "div", "tnc-gutter-head", { role: "columnheader" });
823
+ if (opts.weekNumbers && days[0] !== void 0) gutterHead.textContent = ctx.text.weekNumber(isoWeekNumber(days[0]));
824
+ this.headRow.replaceChildren(gutterHead);
825
+ const capacity = opts.capacityMinutes;
826
+ this.cols = days.map((day) => {
827
+ const arg = ctx.dayArg(day);
828
+ const head = h(doc, "div", ["tnc-dh", ...ctx.dayClasses(day)].join(" "), {
829
+ role: "columnheader",
830
+ "data-date": arg.dateStr,
831
+ "aria-label": ctx.fmt.longDate(day)
832
+ });
833
+ const custom = opts.dayHeaderContent?.(arg);
834
+ if (custom != null) head.append(custom);
835
+ else {
836
+ const label = h(
837
+ doc,
838
+ "span",
839
+ "tnc-dh-label",
840
+ null,
841
+ h(doc, "span", "tnc-dh-dow", null, ctx.fmt.weekdayShort(day)),
842
+ h(doc, "span", "tnc-dh-num", null, ctx.fmt.dayNumber(day))
843
+ );
844
+ head.append(opts.navLinks ? ctx.navLink(day, label, "tnc-dh-link") : label);
845
+ }
846
+ let cap = null;
847
+ if (capacity !== null) {
848
+ cap = h(doc, "div", "tnc-cap", null, h(doc, "i"), h(doc, "span", "tnc-cap-txt"));
849
+ head.append(cap);
850
+ }
851
+ this.headRow.append(head);
852
+ opts.dayHeaderDidMount?.({ ...arg, el: head });
853
+ const col = h(doc, "div", ["tnc-col", ...ctx.dayClasses(day)].join(" "), { role: "cell", "aria-label": ctx.fmt.longDate(day), "data-date": arg.dateStr });
854
+ const bgLayer = h(doc, "div", "tnc-col-bg");
855
+ const planLayer = h(doc, "div", "tnc-col-plan");
856
+ const actualLane = h(doc, "div", "tnc-col-actual");
857
+ const more = h(doc, "div", "tnc-more-layer");
858
+ const sel = h(doc, "div", "tnc-sel-layer");
859
+ col.append(bgLayer, sel, planLayer, actualLane, more);
860
+ let now = null;
861
+ if (opts.nowIndicator && day === today) {
862
+ now = h(doc, "div", "tnc-now", { "aria-hidden": "true" });
863
+ col.append(now);
864
+ }
865
+ this.body?.append(col);
866
+ return {
867
+ day,
868
+ head,
869
+ col,
870
+ plan: new ItemPool(ctx, planLayer),
871
+ actual: new ItemPool(ctx, actualLane),
872
+ bg: new ItemPool(ctx, bgLayer),
873
+ actualLane,
874
+ more,
875
+ sel,
876
+ now,
877
+ cap
878
+ };
879
+ });
880
+ if (this.gutter) this.buildGutter();
881
+ this.applyScale();
882
+ }
883
+ buildGutter() {
884
+ const { ctx } = this;
885
+ const gutter = this.gutter;
886
+ gutter.replaceChildren();
887
+ const { min, max, duration } = ctx.slot;
888
+ const interval = duration >= 60 ? duration : 60;
889
+ const first = Math.ceil(min / interval) * interval;
890
+ for (let m = first; m < max; m += interval) {
891
+ if (m === min) continue;
892
+ const label = h(ctx.doc, "div", "tnc-slot-label", null, ctx.fmt.slotLabel(m));
893
+ label.style.top = pct((m - min) / this.minutes);
894
+ gutter.append(label);
895
+ }
896
+ if (this.nowGutter) gutter.append(this.nowGutter);
897
+ }
898
+ applyScale() {
899
+ const { opts, slot } = this.ctx;
900
+ this.ppm = opts.slotHeight / slot.duration;
901
+ this.fitRows(false);
902
+ this.el.style.setProperty("--tnc-slot-px", `${slot.duration * this.ppm}px`);
903
+ this.el.style.setProperty("--tnc-hour-px", `${60 * this.ppm}px`);
904
+ this.el.style.setProperty("--tnc-grid-offset", `${-(slot.min % 60 * this.ppm)}px`);
905
+ if (this.body) this.body.style.height = `${this.minutes * this.ppm}px`;
906
+ }
907
+ /** In fill mode with expandRows, stretches slots so the grid fills the height. */
908
+ fitRows(rerender = true) {
909
+ const { opts, slot } = this.ctx;
910
+ if (!this.body || opts.height !== "fill" || !opts.expandRows) return;
911
+ const avail = this.scroller.clientHeight - this.top.offsetHeight;
912
+ const base = opts.slotHeight / slot.duration;
913
+ const next = avail > 0 ? Math.max(base, avail / this.minutes) : base;
914
+ if (Math.abs(next - this.ppm) < 1e-3) return;
915
+ this.ppm = next;
916
+ this.body.style.height = `${this.minutes * this.ppm}px`;
917
+ this.el.style.setProperty("--tnc-slot-px", `${slot.duration * this.ppm}px`);
918
+ this.el.style.setProperty("--tnc-hour-px", `${60 * this.ppm}px`);
919
+ if (rerender) this.render(null);
920
+ }
921
+ /* --------------------------------------------------------------- render */
922
+ render(change) {
923
+ this.renderAllDay();
924
+ if (!this.body) return;
925
+ const dirty = change ? change.days : null;
926
+ for (const c of this.cols) {
927
+ if (dirty && !dirty.has(c.day) && !(this.ctx.slot.max > MINUTES_PER_DAY && dirty.has(c.day + 1))) continue;
928
+ this.renderColumn(c);
929
+ }
930
+ this.renderCapacity();
931
+ this.tick();
932
+ }
933
+ renderCapacity() {
934
+ const cap = this.ctx.opts.capacityMinutes;
935
+ if (cap === null) return;
936
+ const planned = plannedMinutes(this.ctx.store, this.cols.map((c) => c.day));
937
+ for (const c of this.cols) {
938
+ if (!c.cap) continue;
939
+ const limit = typeof cap === "function" ? cap(c.day) : cap;
940
+ const used = planned.get(c.day) ?? 0;
941
+ const ratio = limit > 0 ? used / limit : 0;
942
+ const bar = c.cap.firstElementChild;
943
+ bar.style.width = pct(Math.min(1, ratio));
944
+ c.cap.classList.toggle("is-warn", ratio > 0.85 && ratio <= 1);
945
+ c.cap.classList.toggle("is-over", ratio > 1);
946
+ const hours = (m) => `${Math.round(m / 60 * 10) / 10}h`;
947
+ c.cap.lastElementChild.textContent = limit > 0 ? `${hours(used)} / ${hours(limit)}` : hours(used);
948
+ }
949
+ }
950
+ renderAllDay() {
951
+ const { ctx } = this;
952
+ if (!this.allDayLayer || !this.allDayPool || !this.allDayMore) return;
953
+ const { days, store, opts } = ctx;
954
+ const first = days[0];
955
+ const last = days[days.length - 1];
956
+ if (first === void 0 || last === void 0) return;
957
+ const items = opts.timeGrid ? store.spanningIn(first, last + 1, false) : store.inRange(first, last + 1).filter((n) => n.item.display !== "background");
958
+ const row = layoutRow(items.filter((n) => n.item.display !== "background"), days, ctx.order);
959
+ const limit = this.allDayLimit();
960
+ const visibleLevels = (col) => limit && (row.countByCol[col] ?? 0) > limit ? limit - 1 : limit || Infinity;
961
+ const offsets = this.colOffsets();
962
+ const pool = this.allDayPool;
963
+ pool.begin();
964
+ const hidden = new Array(days.length).fill(0);
965
+ let maxLevel = -1;
966
+ for (const p of row.placements) {
967
+ let level = p.level;
968
+ let room = Infinity;
969
+ for (let c = p.startCol; c < p.endCol; c++) room = Math.min(room, visibleLevels(c));
970
+ if (level >= room) {
971
+ if (p.item.id !== ctx.draggingId) {
972
+ for (let c = p.startCol; c < p.endCol; c++) hidden[c]++;
973
+ continue;
974
+ }
975
+ level = Math.max(0, room - 1);
976
+ }
977
+ maxLevel = Math.max(maxLevel, level);
978
+ const el = pool.place(p.item.id, {
979
+ n: p.item,
980
+ day: days[p.startCol],
981
+ isStart: p.isStart,
982
+ isEnd: p.isEnd,
983
+ variant: "bar",
984
+ mirror: false
985
+ });
986
+ setStyle(el, {
987
+ left: `calc(${pct(offsets[p.startCol])} + 2px)`,
988
+ width: `calc(${pct(offsets[p.endCol] - offsets[p.startCol])} - 4px)`,
989
+ top: `calc(${level} * (var(--tnc-bar-h) + ${BAR_GAP2}px) + 3px)`
990
+ });
991
+ }
992
+ pool.end();
993
+ this.allDayMore.replaceChildren();
994
+ let anyMore = false;
995
+ hidden.forEach((count, col) => {
996
+ if (!count) return;
997
+ anyMore = true;
998
+ const day = days[col];
999
+ const link = h(ctx.doc, "button", "tnc-more", { type: "button", "aria-haspopup": "dialog" }, ctx.text.more(count));
1000
+ setStyle(link, {
1001
+ left: `calc(${pct(offsets[col])} + 2px)`,
1002
+ width: `calc(${pct(offsets[col + 1] - offsets[col])} - 4px)`,
1003
+ top: `calc(${visibleLevels(col)} * (var(--tnc-bar-h) + ${BAR_GAP2}px) + 3px)`
1004
+ });
1005
+ link.addEventListener("click", (ev) => ctx.openMore(day, link, ev));
1006
+ this.allDayMore.append(link);
1007
+ });
1008
+ const bars = `calc(${Math.max(1, maxLevel + 1)} * (var(--tnc-bar-h) + ${BAR_GAP2}px) + 6px)`;
1009
+ if (!anyMore) {
1010
+ this.allDayLayer.style.height = bars;
1011
+ return;
1012
+ }
1013
+ const moreH = this.allDayMore.querySelector(".tnc-more")?.offsetHeight ?? 0;
1014
+ const moreTop = Math.max(...hidden.map((count, col) => count ? visibleLevels(col) : 0));
1015
+ this.allDayLayer.style.height = `max(${bars}, calc(${moreTop} * (var(--tnc-bar-h) + ${BAR_GAP2}px) + ${moreH}px + 6px))`;
1016
+ }
1017
+ allDayLimit() {
1018
+ const { dayMaxEvents, dayMaxEventRows } = this.ctx.opts;
1019
+ if (dayMaxEventRows > 0) return dayMaxEventRows;
1020
+ if (typeof dayMaxEvents === "number" && dayMaxEvents > 0) return dayMaxEvents + 1;
1021
+ if (dayMaxEvents === true && this.ctx.opts.timeGrid) return 4;
1022
+ return 0;
1023
+ }
1024
+ /** Fractional left edge of each column (length n + 1). */
1025
+ colOffsets() {
1026
+ const total = this.weights.reduce((a, b) => a + b, 0) || 1;
1027
+ const out = [0];
1028
+ let acc = 0;
1029
+ for (const w of this.weights) out.push((acc += w) / total);
1030
+ return out;
1031
+ }
1032
+ renderColumn(c) {
1033
+ const { ctx } = this;
1034
+ const { store, opts, slot, zone } = ctx;
1035
+ const items = store.timedOn(c.day);
1036
+ if (slot.max > MINUTES_PER_DAY) {
1037
+ const seen = new Set(items.map((n) => n.id));
1038
+ for (const n of store.timedOn(c.day + 1)) if (!seen.has(n.id)) items.push(n);
1039
+ }
1040
+ const minMinutes = opts.eventMinHeight / this.ppm;
1041
+ const layout = layoutTimedDay(items, c.day, zone, minMinutes, ctx.order, [slot.min, slot.max]);
1042
+ const total = this.minutes;
1043
+ const top = (m) => pct((m - slot.min) / total);
1044
+ const height = (s) => pct((s.endMin - s.startMin) / total);
1045
+ c.bg.begin();
1046
+ for (const s of layout.background) {
1047
+ const el = c.bg.place(s.item.id, this.meta(s, true));
1048
+ setStyle(el, { top: top(s.startMin), height: height(s) });
1049
+ }
1050
+ c.bg.end();
1051
+ const hasActual = layout.actual.length > 0;
1052
+ c.col.classList.toggle("has-actual", hasActual);
1053
+ this.placeLane(c.plan, layout.plan, c, top, height, true);
1054
+ this.placeLane(c.actual, layout.actual, c, top, height, false);
1055
+ }
1056
+ placeLane(pool, placements, c, top, height, withMore) {
1057
+ const { ctx } = this;
1058
+ const { opts } = ctx;
1059
+ const maxStack = opts.eventMaxStack;
1060
+ const hiddenByCluster = /* @__PURE__ */ new Map();
1061
+ pool.begin();
1062
+ for (const p of placements) {
1063
+ if (withMore && maxStack > 0 && p.cols > maxStack && p.col >= maxStack - 1) {
1064
+ const rec = hiddenByCluster.get(p.cluster) ?? { count: 0, start: p.startMin, end: p.endMin, items: [] };
1065
+ rec.count++;
1066
+ rec.start = Math.min(rec.start, p.startMin);
1067
+ rec.end = Math.max(rec.end, p.endMin);
1068
+ rec.items.push(p);
1069
+ hiddenByCluster.set(p.cluster, rec);
1070
+ continue;
1071
+ }
1072
+ const cols = withMore && maxStack > 0 ? Math.min(p.cols, maxStack) : p.cols;
1073
+ const span = Math.min(p.span, cols - p.col);
1074
+ const left = p.col / cols;
1075
+ let width = span / cols;
1076
+ if (opts.slotEventOverlap && cols > 1 && p.col + span < cols) width = Math.min(1 - left, width * 1.7);
1077
+ const el = pool.place(p.item.id, this.meta(p, false));
1078
+ const short = (p.endMin - p.startMin) * this.ppm < 34;
1079
+ el.classList.toggle("is-short", short);
1080
+ setStyle(el, {
1081
+ top: top(p.startMin),
1082
+ height: `max(${height(p)}, ${opts.eventMinHeight}px)`,
1083
+ left: pct(left),
1084
+ width: `calc(${pct(width)} - 1px)`,
1085
+ "z-index": String(p.col + 1)
1086
+ });
1087
+ }
1088
+ pool.end();
1089
+ if (!withMore) return;
1090
+ c.more.replaceChildren();
1091
+ for (const rec of hiddenByCluster.values()) {
1092
+ const link = h(ctx.doc, "button", "tnc-more tnc-more--stack", { type: "button", "aria-haspopup": "dialog" }, `+${rec.count}`);
1093
+ const cols = maxStack;
1094
+ setStyle(link, {
1095
+ top: top(rec.start),
1096
+ left: pct((cols - 1) / cols),
1097
+ width: `calc(${pct(1 / cols)} - 1px)`
1098
+ });
1099
+ link.addEventListener("click", (ev) => ctx.openMore(c.day, link, ev));
1100
+ c.more.append(link);
1101
+ }
1102
+ }
1103
+ meta(s, bg) {
1104
+ return {
1105
+ n: s.item,
1106
+ day: s.day,
1107
+ isStart: s.isStart,
1108
+ isEnd: s.isEnd,
1109
+ variant: bg ? "bar" : "timed",
1110
+ mirror: false
1111
+ };
1112
+ }
1113
+ /* ------------------------------------------------------------ selection */
1114
+ showSelection(sel) {
1115
+ const { ctx } = this;
1116
+ const offsets = this.colOffsets();
1117
+ if (this.allDaySel) {
1118
+ this.allDaySel.replaceChildren();
1119
+ if (sel?.allDay) {
1120
+ const startCol = this.cols.findIndex((c) => c.day >= sel.startDay);
1121
+ let endCol = this.cols.findIndex((c) => c.day >= sel.endDay);
1122
+ if (endCol < 0) endCol = this.cols.length;
1123
+ if (startCol >= 0 && endCol > startCol) {
1124
+ const box = h(ctx.doc, "div", "tnc-sel");
1125
+ setStyle(box, {
1126
+ left: pct(offsets[startCol]),
1127
+ width: pct(offsets[endCol] - offsets[startCol]),
1128
+ top: "0",
1129
+ bottom: "0"
1130
+ });
1131
+ this.allDaySel.append(box);
1132
+ }
1133
+ }
1134
+ }
1135
+ for (const c of this.cols) {
1136
+ c.sel.replaceChildren();
1137
+ if (!sel || sel.allDay) continue;
1138
+ const winStart = toInstant(c.day, ctx.slot.min, ctx.zone);
1139
+ const winEnd = toInstant(c.day, ctx.slot.max, ctx.zone);
1140
+ const s = Math.max(sel.startMs, winStart);
1141
+ const e = Math.min(sel.endMs, winEnd);
1142
+ if (e <= s) continue;
1143
+ const rel = (ms) => {
1144
+ const w = wallTime(ms, ctx.zone);
1145
+ return (w.day - c.day) * MINUTES_PER_DAY + w.minute;
1146
+ };
1147
+ const sm = s === winStart ? ctx.slot.min : rel(s);
1148
+ const em = e === winEnd ? ctx.slot.max : rel(e);
1149
+ const box = h(ctx.doc, "div", "tnc-sel");
1150
+ if (ctx.opts.selectMirror) {
1151
+ box.classList.add("tnc-sel--mirror");
1152
+ box.textContent = `${ctx.fmt.time(sm)} \u2013 ${ctx.fmt.time(em)}`;
1153
+ }
1154
+ setStyle(box, { top: pct((sm - ctx.slot.min) / this.minutes), height: pct((em - sm) / this.minutes) });
1155
+ c.sel.append(box);
1156
+ }
1157
+ }
1158
+ /* ------------------------------------------------------------- hit test */
1159
+ hit(x, y) {
1160
+ const colIndex = (cells) => {
1161
+ for (let i2 = 0; i2 < cells.length; i2++) {
1162
+ const r2 = cells[i2].getBoundingClientRect();
1163
+ if (x >= r2.left && x < r2.right) return i2;
1164
+ }
1165
+ const firstR = cells[0]?.getBoundingClientRect();
1166
+ const lastR = cells[cells.length - 1]?.getBoundingClientRect();
1167
+ if (firstR && x < firstR.left) return 0;
1168
+ if (lastR && x >= lastR.right) return cells.length - 1;
1169
+ return -1;
1170
+ };
1171
+ if (this.allDayRow) {
1172
+ const r2 = this.allDayRow.getBoundingClientRect();
1173
+ if (y >= r2.top && y < r2.bottom || !this.body && y >= r2.top) {
1174
+ const i2 = colIndex(this.cols.map((c2) => c2.head));
1175
+ return i2 >= 0 ? { kind: "allDay", day: this.cols[i2].day } : null;
1176
+ }
1177
+ }
1178
+ if (!this.body) return null;
1179
+ const i = colIndex(this.cols.map((c2) => c2.col));
1180
+ if (i < 0) return null;
1181
+ const c = this.cols[i];
1182
+ const r = c.col.getBoundingClientRect();
1183
+ const scrollerR = this.scroller.getBoundingClientRect();
1184
+ const topR = this.top.getBoundingClientRect();
1185
+ if (y < Math.max(topR.bottom, scrollerR.top)) {
1186
+ if (y < topR.top) return null;
1187
+ return this.allDayRow ? { kind: "allDay", day: c.day } : null;
1188
+ }
1189
+ const ratio = Math.min(1, Math.max(0, (y - r.top) / r.height));
1190
+ return { kind: "time", day: c.day, minute: this.ctx.slot.min + ratio * this.minutes };
1191
+ }
1192
+ /* ----------------------------------------------------------------- misc */
1193
+ tick() {
1194
+ const { ctx } = this;
1195
+ const nowMs = ctx.now();
1196
+ const w = wallTime(nowMs, ctx.zone);
1197
+ for (const c of this.cols) {
1198
+ if (!c.now) continue;
1199
+ const minute = (w.day - c.day) * MINUTES_PER_DAY + w.minute;
1200
+ const visible = minute >= ctx.slot.min && minute <= ctx.slot.max;
1201
+ c.now.hidden = !visible;
1202
+ if (visible) c.now.style.top = pct((minute - ctx.slot.min) / this.minutes);
1203
+ if (this.nowGutter) {
1204
+ this.nowGutter.hidden = !visible;
1205
+ if (visible) this.nowGutter.style.top = pct((minute - ctx.slot.min) / this.minutes);
1206
+ }
1207
+ }
1208
+ }
1209
+ resize() {
1210
+ this.fitRows();
1211
+ }
1212
+ scrollToTime(minute) {
1213
+ if (!this.body) return;
1214
+ const y = (minute - this.ctx.slot.min) * this.ppm;
1215
+ this.scroller.scrollTop = Math.max(0, y);
1216
+ }
1217
+ getScroll() {
1218
+ return { top: this.scroller.scrollTop, left: this.scroller.scrollLeft };
1219
+ }
1220
+ setScroll(s) {
1221
+ this.scroller.scrollTop = s.top;
1222
+ this.scroller.scrollLeft = s.left;
1223
+ }
1224
+ elementsFor(id) {
1225
+ const out = this.allDayPool?.elementsFor(id) ?? [];
1226
+ for (const c of this.cols) out.push(...c.plan.elementsFor(id), ...c.actual.elementsFor(id), ...c.bg.elementsFor(id));
1227
+ return out;
1228
+ }
1229
+ destroy() {
1230
+ this.resizeObs?.disconnect();
1231
+ this.allDayPool?.clear();
1232
+ for (const c of this.cols) {
1233
+ c.plan.clear();
1234
+ c.actual.clear();
1235
+ c.bg.clear();
1236
+ }
1237
+ this.el.remove();
1238
+ }
1239
+ };
1240
+
1241
+ // src/dom/views/timeline.ts
1242
+ var SVG = "http://www.w3.org/2000/svg";
1243
+ var COMPACT_PX = 28;
1244
+ var NARROW_PX = 20;
1245
+ var MILESTONE_R = 7;
1246
+ function sameRow(a, b) {
1247
+ 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;
1248
+ }
1249
+ var TimelineView = class {
1250
+ constructor(ctx) {
1251
+ this.ctx = ctx;
1252
+ const { doc } = ctx;
1253
+ const given = this.tl.scale ?? {};
1254
+ const unit = given.unit ?? "day";
1255
+ this.dayWidth = given.dayWidth ?? (given.unitWidth ?? 36) / TIMELINE_UNIT_DAYS[unit];
1256
+ this.scale = { snapMinutes: MINUTES_PER_DAY, ...given, unit, unitWidth: this.dayWidth * TIMELINE_UNIT_DAYS[unit] };
1257
+ this.startDay = ctx.view.range.startDay;
1258
+ this.endDay = ctx.view.range.endDay;
1259
+ this.labelWidth = this.tl.labelWidth ?? 240;
1260
+ this.rowHeight = this.tl.rowHeight ?? 32;
1261
+ this.el = h(doc, "div", "tnc-tl");
1262
+ this.el.style.setProperty("--tnc-tl-label-w", `${this.labelWidth}px`);
1263
+ this.el.style.setProperty("--tnc-tl-row-h", `${this.rowHeight}px`);
1264
+ this.el.style.setProperty("--tnc-tl-day-w", `${this.dayWidth}px`);
1265
+ this.scroller = h(doc, "div", "tnc-tl-scroll");
1266
+ this.canvas = h(doc, "div", "tnc-tl-canvas", { role: "table", "aria-label": ctx.view.title });
1267
+ this.canvas.style.width = `${this.labelWidth + this.trackWidth()}px`;
1268
+ this.head = h(doc, "div", "tnc-tl-head");
1269
+ this.ticks = h(doc, "div", "tnc-tl-ticks");
1270
+ this.rowsEl = h(doc, "div", "tnc-tl-rows", { role: "rowgroup" });
1271
+ this.svg = doc.createElementNS(SVG, "svg");
1272
+ this.svg.classList.add("tnc-tl-deps");
1273
+ this.svg.setAttribute("aria-hidden", "true");
1274
+ this.todayLine = h(doc, "div", "tnc-tl-today", { "aria-hidden": "true" });
1275
+ this.todayTag = this.tl.todayLabel ? h(doc, "div", "tnc-tl-today-label", { "aria-hidden": "true" }, this.tl.todayLabel) : null;
1276
+ this.selLayer = h(doc, "div", "tnc-tl-sel");
1277
+ this.canvas.append(this.head, this.rowsEl);
1278
+ this.scroller.append(this.canvas);
1279
+ this.el.append(this.scroller);
1280
+ this.buildHead();
1281
+ this.el.addEventListener("pointerdown", this.onLinkDown, true);
1282
+ }
1283
+ ctx;
1284
+ el;
1285
+ scroller;
1286
+ canvas;
1287
+ head;
1288
+ ticks;
1289
+ rowsEl;
1290
+ svg;
1291
+ todayLine;
1292
+ todayTag;
1293
+ selLayer;
1294
+ rows = [];
1295
+ scale;
1296
+ dayWidth;
1297
+ startDay;
1298
+ endDay;
1299
+ labelWidth;
1300
+ rowHeight;
1301
+ link = null;
1302
+ get tl() {
1303
+ return this.ctx.opts.timeline ?? { rows: [], rowOf: () => null };
1304
+ }
1305
+ /* ------------------------------------------------------------- geometry */
1306
+ trackWidth() {
1307
+ return (this.endDay - this.startDay) * this.dayWidth;
1308
+ }
1309
+ /** X offset (within the track) of an instant. */
1310
+ x(ms) {
1311
+ const w = wallTime(ms, this.ctx.zone);
1312
+ return (w.day - this.startDay + w.minute / MINUTES_PER_DAY) * this.dayWidth;
1313
+ }
1314
+ xDay(day) {
1315
+ return (day - this.startDay) * this.dayWidth;
1316
+ }
1317
+ /** Instant at a track x offset. */
1318
+ msAt(x) {
1319
+ const civil = this.startDay * MINUTES_PER_DAY + x / this.dayWidth * MINUTES_PER_DAY;
1320
+ const day = Math.floor(civil / MINUTES_PER_DAY);
1321
+ return toInstant(day, civil - day * MINUTES_PER_DAY, this.ctx.zone);
1322
+ }
1323
+ /** Viewport x (from the scroller's left edge) where the track starts showing, and its visible width. */
1324
+ trackViewport() {
1325
+ return { left: this.labelWidth, width: Math.max(0, this.scroller.clientWidth - this.labelWidth) };
1326
+ }
1327
+ /** Instant under a viewport x, measured from the scroller's left edge. */
1328
+ instantAt(viewportX) {
1329
+ return this.msAt(this.scroller.scrollLeft + viewportX - this.labelWidth);
1330
+ }
1331
+ /** Viewport x of an instant at the current scroll. */
1332
+ viewportXOf(ms) {
1333
+ return this.labelWidth + this.x(ms) - this.scroller.scrollLeft;
1334
+ }
1335
+ /** Scrolls so `ms` sits at `viewportX`. */
1336
+ placeInstant(ms, viewportX, smooth = false) {
1337
+ this.scrollLeftTo(this.labelWidth + this.x(ms) - viewportX, smooth);
1338
+ }
1339
+ scrollToInstant(ms, align, smooth) {
1340
+ const vp = this.trackViewport();
1341
+ const vx = vp.left + (align === "start" ? 0 : align === "end" ? vp.width : vp.width / 2);
1342
+ this.placeInstant(ms, vx, smooth);
1343
+ }
1344
+ /** Viewport x of the middle of the visible track. */
1345
+ viewportCentre() {
1346
+ const vp = this.trackViewport();
1347
+ return vp.left + vp.width / 2;
1348
+ }
1349
+ /** The scroller's left edge in client coordinates. */
1350
+ viewportLeft() {
1351
+ return this.scroller.getBoundingClientRect().left;
1352
+ }
1353
+ scrollLeftTo(left, smooth) {
1354
+ const max = Math.max(0, this.scroller.scrollWidth - this.scroller.clientWidth);
1355
+ const target = Math.max(0, Math.min(max || left, left));
1356
+ if (typeof this.scroller.scrollTo === "function") this.scroller.scrollTo({ left: target, behavior: smooth ? "smooth" : "auto" });
1357
+ else this.scroller.scrollLeft = target;
1358
+ }
1359
+ buildHead() {
1360
+ const { doc, fmt, today } = this.ctx;
1361
+ const top = h(doc, "div", "tnc-tl-tier tnc-tl-tier--major");
1362
+ const bottom = h(doc, "div", "tnc-tl-tier tnc-tl-tier--minor");
1363
+ const corner = h(doc, "div", "tnc-tl-corner");
1364
+ const ticks = this.ticks;
1365
+ ticks.append(top, bottom);
1366
+ if (this.todayTag) ticks.append(this.todayTag);
1367
+ this.head.append(corner, ticks);
1368
+ const unit = this.scale.unit;
1369
+ const add = (tier, x0, x1, label, cls = "") => {
1370
+ const t = h(doc, "div", `tnc-tl-tick ${cls}`, null, h(doc, "span", null, null, label));
1371
+ setStyle(t, { left: x0, width: Math.max(0, x1 - x0) });
1372
+ tier.append(t);
1373
+ };
1374
+ const s = this.startDay;
1375
+ const e = this.endDay;
1376
+ if (unit === "hour") {
1377
+ for (let d = s; d < e; d++) add(top, this.xDay(d), this.xDay(d + 1), fmt.longDate(d), d === today ? "is-today" : "");
1378
+ } else if (unit === "month" || unit === "quarter") {
1379
+ for (let d = startOfYear(s); d < e; d = startOfYear(addMonths(d, 12))) {
1380
+ add(top, this.xDay(Math.max(d, s)), this.xDay(Math.min(startOfYear(addMonths(d, 12)), e)), fmt.year(d));
1381
+ }
1382
+ } else {
1383
+ for (let d = startOfMonth(s); d < e; d = addMonths(d, 1)) {
1384
+ add(top, this.xDay(Math.max(d, s)), this.xDay(Math.min(addMonths(d, 1), e)), fmt.monthYear(d));
1385
+ }
1386
+ }
1387
+ if (unit === "hour") {
1388
+ const step = this.scale.unitWidth >= 28 ? 1 : this.scale.unitWidth >= 14 ? 2 : 6;
1389
+ for (let d = s; d < e; d++) {
1390
+ for (let hr = 0; hr < 24; hr += step) {
1391
+ const x0 = this.xDay(d) + hr / 24 * this.dayWidth;
1392
+ add(bottom, x0, x0 + step / 24 * this.dayWidth, fmt.slotLabel(hr * 60));
1393
+ }
1394
+ }
1395
+ } else if (unit === "day") {
1396
+ for (let d = s; d < e; d++) {
1397
+ const narrow = this.dayWidth < 26;
1398
+ add(bottom, this.xDay(d), this.xDay(d + 1), narrow ? fmt.dayNumber(d) : `${fmt.weekdayNarrow(d)} ${fmt.dayNumber(d)}`, [
1399
+ d === today ? "is-today" : "",
1400
+ ...this.ctx.dayClasses(d).filter((c) => c === "is-weekend")
1401
+ ].join(" "));
1402
+ }
1403
+ } else if (unit === "week") {
1404
+ for (let d = startOfWeek(s, this.ctx.firstDay); d < e; d += 7) {
1405
+ add(bottom, this.xDay(Math.max(d, s)), this.xDay(Math.min(d + 7, e)), `${fmt.monthShort(d)} ${fmt.dayNumber(d)}`);
1406
+ }
1407
+ } else {
1408
+ const step = unit === "quarter" ? 3 : 1;
1409
+ for (let d = startOfMonth(s); d < e; d = addMonths(d, step)) {
1410
+ const label = unit === "quarter" ? `Q${Math.floor((ymdOfDay(d).month - 1) / 3) + 1}` : fmt.monthShort(d);
1411
+ add(bottom, this.xDay(Math.max(d, s)), this.xDay(Math.min(addMonths(d, step), e)), label);
1412
+ }
1413
+ }
1414
+ const colPx = unit === "hour" ? this.dayWidth / 24 : unit === "day" ? this.dayWidth : this.dayWidth * 7;
1415
+ this.el.style.setProperty("--tnc-tl-col-w", `${colPx}px`);
1416
+ this.el.style.setProperty("--tnc-tl-day-px", `${this.dayWidth}px`);
1417
+ }
1418
+ /* --------------------------------------------------------------- render */
1419
+ mountRow(row) {
1420
+ const { ctx } = this;
1421
+ const { doc } = ctx;
1422
+ const el = h(doc, "div", `tnc-tl-row${row.group ? " is-group" : ""}`, { role: "row", "data-row": row.id });
1423
+ const label = h(doc, "div", "tnc-tl-label", { role: "rowheader" });
1424
+ label.style.paddingLeft = `${10 + (row.depth ?? 0) * 16}px`;
1425
+ if (row.color) label.style.setProperty("--tnc-c", row.color);
1426
+ const tl = this.tl;
1427
+ if (tl.rowLabelDidMount) tl.rowLabelDidMount({ row, el: label });
1428
+ else if (tl.rowContent) tl.rowContent(row, label);
1429
+ else label.append(h(doc, "span", "tnc-tl-label-text", null, row.title));
1430
+ if (tl.onRowClick) label.addEventListener("click", (ev) => this.tl.onRowClick?.(row, ev));
1431
+ const track = h(doc, "div", "tnc-tl-track", { role: "cell" });
1432
+ el.append(label, track);
1433
+ return { row, el, label, track, pool: row.group ? null : new ItemPool(ctx, track), top: 0, height: this.rowHeight };
1434
+ }
1435
+ unmountRow(rec) {
1436
+ rec.pool?.clear();
1437
+ this.tl.rowLabelWillUnmount?.({ row: rec.row, el: rec.label });
1438
+ rec.el.remove();
1439
+ }
1440
+ render(_change) {
1441
+ const { ctx } = this;
1442
+ const { store } = ctx;
1443
+ const byRow = /* @__PURE__ */ new Map();
1444
+ for (const n of store.inRange(this.startDay, this.endDay)) {
1445
+ if (n.item.display === "none") continue;
1446
+ const rowId = ctx.dragRow?.id === n.id ? ctx.dragRow.rowId : this.tl.rowOf(n.item);
1447
+ if (rowId == null) continue;
1448
+ const list = byRow.get(rowId) ?? [];
1449
+ list.push(n);
1450
+ byRow.set(rowId, list);
1451
+ }
1452
+ const old = new Map(this.rows.map((r) => [r.row.id, r]));
1453
+ const next = [];
1454
+ let y = 0;
1455
+ for (const row of this.tl.rows) {
1456
+ let rec = old.get(row.id);
1457
+ if (rec && !sameRow(rec.row, row)) {
1458
+ this.unmountRow(rec);
1459
+ rec = void 0;
1460
+ }
1461
+ rec ??= this.mountRow(row);
1462
+ old.delete(row.id);
1463
+ const items = byRow.get(row.id) ?? [];
1464
+ const lane = layoutLane(items, Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY);
1465
+ const levels = Math.max(1, lane.levels);
1466
+ rec.height = this.rowHeight * levels;
1467
+ rec.el.style.height = `${rec.height}px`;
1468
+ rec.top = y;
1469
+ y += rec.height;
1470
+ if (rec.pool) {
1471
+ rec.pool.begin();
1472
+ for (const bar of lane.bars) this.placeBar(rec.pool, bar.item, bar.level);
1473
+ rec.pool.end();
1474
+ }
1475
+ next.push(rec);
1476
+ }
1477
+ for (const rec of old.values()) this.unmountRow(rec);
1478
+ let prev = null;
1479
+ for (const rec of next) {
1480
+ if (prev ? prev.nextSibling !== rec.el : this.rowsEl.firstChild !== rec.el) {
1481
+ if (prev) prev.after(rec.el);
1482
+ else this.rowsEl.prepend(rec.el);
1483
+ }
1484
+ prev = rec.el;
1485
+ }
1486
+ this.rows = next;
1487
+ this.rowsEl.style.height = `${y}px`;
1488
+ this.rowsEl.append(this.svg, this.selLayer, this.todayLine);
1489
+ this.drawDependencies();
1490
+ this.tick();
1491
+ }
1492
+ placeBar(pool, n, level) {
1493
+ const { ctx } = this;
1494
+ const milestone = n.item.kind === "milestone";
1495
+ const el = pool.place(n.id, { n, day: n.startDay, isStart: true, isEnd: true, variant: "timeline", mirror: false });
1496
+ const x0 = n.allDay ? this.xDay(n.startDay) : this.x(n.startMs);
1497
+ const x1 = n.allDay ? this.xDay(n.endDay) : this.x(n.endMs);
1498
+ el.classList.toggle("is-milestone", milestone);
1499
+ el.classList.toggle("is-conflict", ctx.conflicts.has(n.id));
1500
+ if (milestone) {
1501
+ const cx = n.allDay ? this.xDay(n.startDay) + this.dayWidth / 2 : x0;
1502
+ setStyle(el, { left: cx, width: null, top: level * this.rowHeight, "--tnc-grip": null });
1503
+ return;
1504
+ }
1505
+ const width = Math.max(4, x1 - x0);
1506
+ el.classList.toggle("is-compact", width < COMPACT_PX);
1507
+ el.classList.toggle("is-narrow", width < NARROW_PX);
1508
+ const grip = Math.min(8, Math.floor((width - 12) / 2));
1509
+ setStyle(el, { left: x0, width, top: level * this.rowHeight, "--tnc-grip": width < NARROW_PX ? null : `${grip}px` });
1510
+ }
1511
+ rowOfItem(id) {
1512
+ for (const r of this.rows) if (r.pool?.elementsFor(id).length) return r;
1513
+ return void 0;
1514
+ }
1515
+ /** Track x of a bar's start or finish edge. */
1516
+ edgeX(el, edge) {
1517
+ if (el.classList.contains("is-milestone")) return el.offsetLeft + (edge === "start" ? -MILESTONE_R : MILESTONE_R);
1518
+ return edge === "start" ? el.offsetLeft : el.offsetLeft + el.offsetWidth;
1519
+ }
1520
+ drawDependencies() {
1521
+ const { ctx } = this;
1522
+ this.svg.replaceChildren();
1523
+ if (!this.tl.dependencies) return;
1524
+ this.svg.setAttribute("width", String(this.trackWidth()));
1525
+ this.svg.setAttribute("height", this.rowsEl.style.height.replace("px", "") || "0");
1526
+ this.svg.style.left = `${this.labelWidth}px`;
1527
+ for (const n of ctx.store.all()) {
1528
+ if (!n.item.dependsOn?.length) continue;
1529
+ const toRow = this.rowOfItem(n.id);
1530
+ const toEl = toRow?.pool?.elementsFor(n.id)[0];
1531
+ if (!toRow || !toEl) continue;
1532
+ for (const dep of itemDependencies(n.item)) {
1533
+ const fromRow = this.rowOfItem(dep.id);
1534
+ const fromEl = fromRow?.pool?.elementsFor(dep.id)[0];
1535
+ if (!fromRow || !fromEl) continue;
1536
+ const path = this.ctx.doc.createElementNS(SVG, "path");
1537
+ path.setAttribute(
1538
+ "d",
1539
+ dependencyPath(
1540
+ dep.type,
1541
+ this.edgeX(fromEl, dep.type[0] === "S" ? "start" : "finish"),
1542
+ fromRow.top + fromEl.offsetTop + fromEl.offsetHeight / 2,
1543
+ this.edgeX(toEl, dep.type[1] === "S" ? "start" : "finish"),
1544
+ toRow.top + toEl.offsetTop + toEl.offsetHeight / 2
1545
+ )
1546
+ );
1547
+ path.setAttribute("marker-end", "url(#tnc-arrow)");
1548
+ path.dataset.from = dep.id;
1549
+ path.dataset.to = n.id;
1550
+ path.dataset.type = dep.type;
1551
+ if (ctx.conflicts.get(n.id)?.includes(dep.id)) path.classList.add("is-conflict");
1552
+ this.svg.append(path);
1553
+ }
1554
+ }
1555
+ const defs = this.ctx.doc.createElementNS(SVG, "defs");
1556
+ 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>';
1557
+ this.svg.prepend(defs);
1558
+ }
1559
+ /* ---------------------------------------------------------------- links */
1560
+ linkTargetAt(clientX, clientY, from) {
1561
+ const under = this.ctx.doc.elementFromPoint(clientX, clientY);
1562
+ const el = under?.closest(".tnc-item--timeline");
1563
+ if (!el || !this.el.contains(el)) return null;
1564
+ const meta = this.ctx.meta.get(el);
1565
+ if (!meta || meta.mirror || meta.n.id === from.id) return null;
1566
+ const port = under?.closest(".tnc-port") ?? null;
1567
+ let edge;
1568
+ if (port) edge = port.dataset.edge === "start" ? "start" : "finish";
1569
+ else if (el.classList.contains("is-milestone")) edge = "start";
1570
+ else {
1571
+ const r = el.getBoundingClientRect();
1572
+ edge = clientX < r.left + r.width / 2 ? "start" : "finish";
1573
+ }
1574
+ return { n: meta.n, el, edge, port };
1575
+ }
1576
+ /** Centre of a target edge's port, in track coordinates. */
1577
+ targetPoint(t) {
1578
+ const port = t.port ?? t.el.querySelector(`.tnc-port[data-edge="${t.edge}"]`);
1579
+ const rowsR = this.rowsEl.getBoundingClientRect();
1580
+ const r = (port ?? t.el).getBoundingClientRect();
1581
+ return { x: r.left + r.width / 2 - rowsR.left - this.labelWidth, y: r.top + r.height / 2 - rowsR.top };
1582
+ }
1583
+ linkArg(from, fromEdge, t) {
1584
+ return { from: from.item, to: t.n.item, fromEdge, toEdge: t.edge, type: dependencyType(fromEdge, t.edge) };
1585
+ }
1586
+ linkState(s, phase, linked = false) {
1587
+ const t = s.target;
1588
+ return {
1589
+ phase,
1590
+ from: s.from.item,
1591
+ fromEdge: s.fromEdge,
1592
+ to: t?.n.item ?? null,
1593
+ toEdge: t?.edge ?? null,
1594
+ type: t ? dependencyType(s.fromEdge, t.edge) : null,
1595
+ valid: s.valid,
1596
+ reason: s.reason,
1597
+ linked
1598
+ };
1599
+ }
1600
+ markTarget(t, on, invalid = false) {
1601
+ if (!t) return;
1602
+ t.el.classList.toggle("is-link-target", on);
1603
+ t.el.classList.toggle("is-link-invalid", on && invalid);
1604
+ for (const p of t.el.querySelectorAll(".tnc-port")) {
1605
+ const hit = on && p.dataset.edge === t.edge;
1606
+ p.classList.toggle("is-target", hit);
1607
+ p.classList.toggle("is-invalid", hit && invalid);
1608
+ }
1609
+ }
1610
+ onLinkDown = (ev) => {
1611
+ const port = ev.target.closest?.(".tnc-port");
1612
+ if (!port || !this.tl.onLink || ev.button !== 0 || this.link) return;
1613
+ const barEl = port.closest(".tnc-item");
1614
+ const meta = barEl ? this.ctx.meta.get(barEl) : void 0;
1615
+ if (!meta) return;
1616
+ ev.stopPropagation();
1617
+ ev.preventDefault();
1618
+ const rowsR = this.rowsEl.getBoundingClientRect();
1619
+ const pr = port.getBoundingClientRect();
1620
+ const x = pr.left + pr.width / 2 - rowsR.left - this.labelWidth;
1621
+ const y = pr.top + pr.height / 2 - rowsR.top;
1622
+ const path = this.ctx.doc.createElementNS(SVG, "path");
1623
+ path.classList.add("is-linking");
1624
+ path.setAttribute("d", `M${x},${y}`);
1625
+ this.svg.append(path);
1626
+ const doc = this.ctx.doc;
1627
+ const move = (e) => {
1628
+ const s = this.link;
1629
+ if (!s || e.pointerId !== s.pointerId) return;
1630
+ e.preventDefault();
1631
+ const t = this.linkTargetAt(e.clientX, e.clientY, s.from);
1632
+ const r = this.rowsEl.getBoundingClientRect();
1633
+ const end = t ? this.targetPoint(t) : { x: e.clientX - r.left - this.labelWidth, y: e.clientY - r.top };
1634
+ s.path.setAttribute("d", `M${s.x},${s.y} L${end.x},${end.y}`);
1635
+ if (t?.n.id === s.target?.n.id && t?.edge === s.target?.edge) return;
1636
+ this.markTarget(s.target, false);
1637
+ s.target = t;
1638
+ s.valid = true;
1639
+ s.reason = null;
1640
+ if (t) {
1641
+ const verdict = this.tl.canLink?.(this.linkArg(s.from, s.fromEdge, t)) ?? true;
1642
+ s.valid = verdict === true;
1643
+ s.reason = typeof verdict === "string" ? verdict : null;
1644
+ }
1645
+ s.path.classList.toggle("is-invalid", !s.valid);
1646
+ this.markTarget(t, true, !s.valid);
1647
+ this.tl.onLinkDrag?.(this.linkState(s, "over"));
1648
+ };
1649
+ const finish = (e) => {
1650
+ const s = this.link;
1651
+ if (!s) return;
1652
+ if (e) move(e);
1653
+ this.endLink(e !== null);
1654
+ };
1655
+ const up = (e) => {
1656
+ if (this.link && e.pointerId === this.link.pointerId) finish(e);
1657
+ };
1658
+ const cancel = (e) => {
1659
+ if (this.link && e.pointerId === this.link.pointerId) finish(null);
1660
+ };
1661
+ const key = (e) => {
1662
+ if (e.key !== "Escape") return;
1663
+ e.preventDefault();
1664
+ e.stopPropagation();
1665
+ finish(null);
1666
+ };
1667
+ doc.addEventListener("pointermove", move);
1668
+ doc.addEventListener("pointerup", up);
1669
+ doc.addEventListener("pointercancel", cancel);
1670
+ doc.addEventListener("keydown", key, true);
1671
+ this.link = {
1672
+ from: meta.n,
1673
+ fromEdge: port.dataset.edge === "start" ? "start" : "finish",
1674
+ path,
1675
+ x,
1676
+ y,
1677
+ pointerId: ev.pointerId,
1678
+ target: null,
1679
+ valid: true,
1680
+ reason: null,
1681
+ cleanup: () => {
1682
+ doc.removeEventListener("pointermove", move);
1683
+ doc.removeEventListener("pointerup", up);
1684
+ doc.removeEventListener("pointercancel", cancel);
1685
+ doc.removeEventListener("keydown", key, true);
1686
+ }
1687
+ };
1688
+ this.el.classList.add("is-linking");
1689
+ this.tl.onLinkDrag?.(this.linkState(this.link, "start"));
1690
+ };
1691
+ endLink(commit) {
1692
+ const s = this.link;
1693
+ if (!s) return;
1694
+ this.link = null;
1695
+ s.cleanup();
1696
+ s.path.remove();
1697
+ this.markTarget(s.target, false);
1698
+ this.el.classList.remove("is-linking");
1699
+ if (commit) this.ctx.suppressNextClick();
1700
+ else s.target = null;
1701
+ const linked = commit && !!s.target && s.valid;
1702
+ if (linked && s.target) this.tl.onLink?.(this.linkArg(s.from, s.fromEdge, s.target));
1703
+ this.tl.onLinkDrag?.(this.linkState(s, "end", linked));
1704
+ }
1705
+ /* ------------------------------------------------------------ selection */
1706
+ showSelection(sel) {
1707
+ this.selLayer.replaceChildren();
1708
+ if (!sel) return;
1709
+ const row = sel.rowId ? this.rows.find((r) => r.row.id === sel.rowId) : void 0;
1710
+ if (sel.rowId && !row) return;
1711
+ const x0 = sel.allDay ? this.xDay(sel.startDay) : this.x(sel.startMs);
1712
+ const x1 = sel.allDay ? this.xDay(sel.endDay) : this.x(sel.endMs);
1713
+ const box = h(this.ctx.doc, "div", `tnc-sel${row ? "" : " is-column"}`);
1714
+ const height = row ? row.height : parseFloat(this.rowsEl.style.height) || 0;
1715
+ setStyle(box, { left: this.labelWidth + x0, width: Math.max(2, x1 - x0), top: row ? row.top : 0, height });
1716
+ this.selLayer.append(box);
1717
+ }
1718
+ /* ------------------------------------------------------------- hit test */
1719
+ hit(x, y) {
1720
+ const vr = this.scroller.getBoundingClientRect();
1721
+ const right = vr.left + (this.scroller.clientWidth || vr.width);
1722
+ const bottom = vr.top + (this.scroller.clientHeight || vr.height);
1723
+ if (x < vr.left || x >= right || y < vr.top || y >= bottom) return null;
1724
+ const r = this.rowsEl.getBoundingClientRect();
1725
+ const px2 = x - r.left - this.labelWidth;
1726
+ const days = px2 / this.dayWidth;
1727
+ const day = this.startDay + Math.floor(days);
1728
+ const minute = (days - Math.floor(days)) * MINUTES_PER_DAY;
1729
+ const inLabel = x < vr.left + this.labelWidth;
1730
+ const inHead = y < this.head.getBoundingClientRect().bottom;
1731
+ const rel = y - r.top;
1732
+ const row = inHead ? void 0 : this.rows.find((rec) => rel >= rec.top && rel < rec.top + rec.height);
1733
+ let area;
1734
+ if (inLabel) area = "label";
1735
+ else if (inHead) area = px2 < this.trackWidth() ? "header" : "outside";
1736
+ else area = row && px2 >= 0 && px2 < this.trackWidth() ? "track" : "outside";
1737
+ return { kind: "timeline", rowId: row?.row.id ?? null, day, minute, area, overTrack: area === "track" && !!row && !row.row.group };
1738
+ }
1739
+ tick() {
1740
+ const x = this.x(this.ctx.now());
1741
+ const inRange = x >= 0 && x <= this.trackWidth();
1742
+ this.todayLine.hidden = !inRange;
1743
+ this.todayLine.style.left = `${this.labelWidth + x}px`;
1744
+ if (this.todayTag) {
1745
+ this.todayTag.hidden = !inRange;
1746
+ this.todayTag.style.left = `${x}px`;
1747
+ }
1748
+ }
1749
+ /** Scrolls so `day` sits a little in from the left edge. */
1750
+ scrollToDay(day) {
1751
+ this.scroller.scrollLeft = Math.max(0, this.xDay(day) - this.dayWidth * 2);
1752
+ }
1753
+ getScroll() {
1754
+ return { top: this.scroller.scrollTop, left: this.scroller.scrollLeft };
1755
+ }
1756
+ setScroll(s) {
1757
+ this.scroller.scrollTop = s.top;
1758
+ this.scroller.scrollLeft = s.left;
1759
+ }
1760
+ elementsFor(id) {
1761
+ return this.rows.flatMap((r) => r.pool?.elementsFor(id) ?? []);
1762
+ }
1763
+ destroy() {
1764
+ this.endLink(false);
1765
+ this.el.removeEventListener("pointerdown", this.onLinkDown, true);
1766
+ for (const r of this.rows) this.unmountRow(r);
1767
+ this.rows = [];
1768
+ this.el.remove();
1769
+ }
1770
+ };
1771
+ function dependencyPath(type, x1, y1, x2, y2) {
1772
+ const gap = 10;
1773
+ const out = type[0] === "S" ? -1 : 1;
1774
+ const side = type[1] === "S" ? -1 : 1;
1775
+ const xa = x1 + out * gap;
1776
+ const xb = x2 + side * gap;
1777
+ const xEnd = x2 + side * 2;
1778
+ if (out === side) {
1779
+ const xe = out < 0 ? Math.min(xa, xb) : Math.max(xa, xb);
1780
+ return `M${x1},${y1} H${xe} V${y2} H${xEnd}`;
1781
+ }
1782
+ if (out > 0 ? xa <= xb : xa >= xb) {
1783
+ const mid = (x1 + x2) / 2;
1784
+ return `M${x1},${y1} H${mid} V${y2} H${xEnd}`;
1785
+ }
1786
+ return `M${x1},${y1} H${xa} V${(y1 + y2) / 2} H${xb} V${y2} H${xEnd}`;
1787
+ }
1788
+
1789
+ // src/dom/views/year.ts
1790
+ var YearView = class {
1791
+ constructor(ctx) {
1792
+ this.ctx = ctx;
1793
+ const { doc, fmt, opts } = ctx;
1794
+ this.el = h(doc, "div", "tnc-year");
1795
+ this.scroller = h(doc, "div", "tnc-year-scroll");
1796
+ const grid = h(doc, "div", "tnc-year-grid");
1797
+ let month = ctx.view.range.startDay;
1798
+ for (let i = 0; i < 12; i++) {
1799
+ const { year, month: m } = ymdOfDay(month);
1800
+ const box = h(doc, "section", "tnc-ym", { "aria-label": fmt.monthYear(month) });
1801
+ 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));
1802
+ const first = month;
1803
+ if (opts.navLinks) title.addEventListener("click", (ev) => this.ctx.navigate(first, ev, "month"));
1804
+ box.append(title);
1805
+ const days = h(doc, "div", "tnc-ym-days", { role: "group", "aria-label": fmt.monthYear(month) });
1806
+ const start = startOfWeek(month, ctx.firstDay);
1807
+ for (let d = start; d < start + 7; d++) days.append(h(doc, "div", "tnc-ym-dow", { "aria-hidden": "true" }, fmt.weekdayNarrow(d)));
1808
+ const len = daysInMonth(year, m);
1809
+ for (let d = start; d < month; d++) days.append(h(doc, "div", "tnc-ym-pad"));
1810
+ for (let d = month; d < month + len; d++) {
1811
+ const arg = ctx.dayArg(d);
1812
+ const cell = h(doc, "button", ["tnc-ym-day", ...ctx.dayClasses(d)].join(" "), {
1813
+ type: "button",
1814
+ "data-date": arg.dateStr,
1815
+ "aria-label": fmt.longDate(d)
1816
+ });
1817
+ const dots = h(doc, "span", "tnc-ym-dots", { "aria-hidden": "true" });
1818
+ cell.append(h(doc, "span", "tnc-ym-num", null, fmt.dayNumber(d)), dots);
1819
+ days.append(cell);
1820
+ this.cells.push({ day: d, el: cell, dots });
1821
+ }
1822
+ box.append(days);
1823
+ grid.append(box);
1824
+ month = addMonths(month, 1);
1825
+ }
1826
+ this.scroller.append(grid);
1827
+ this.el.append(this.scroller);
1828
+ }
1829
+ ctx;
1830
+ el;
1831
+ scroller;
1832
+ cells = [];
1833
+ render(_change) {
1834
+ const { store } = this.ctx;
1835
+ const doc = this.ctx.doc;
1836
+ for (const c of this.cells) {
1837
+ const items = store.inRange(c.day, c.day + 1).filter((n) => n.item.display !== "background").sort(this.ctx.order);
1838
+ c.el.dataset.count = String(items.length);
1839
+ c.el.classList.toggle("has-items", items.length > 0);
1840
+ c.el.style.setProperty("--tnc-heat", String(Math.min(1, items.length / 6)));
1841
+ c.dots.replaceChildren(
1842
+ ...items.slice(0, 3).map((n) => {
1843
+ const dot = h(doc, "i");
1844
+ const color = n.item.color ?? n.item.borderColor ?? n.item.backgroundColor;
1845
+ if (color) dot.style.setProperty("--tnc-c", color);
1846
+ return dot;
1847
+ })
1848
+ );
1849
+ const label = this.ctx.fmt.longDate(c.day);
1850
+ c.el.setAttribute("aria-label", items.length ? `${label}, ${this.ctx.text.itemCount(items.length)}` : label);
1851
+ c.el.title = items.length ? `${label}
1852
+ ${items.slice(0, 8).map((n) => `\u2022 ${n.item.title}`).join("\n")}${items.length > 8 ? `
1853
+ ${this.ctx.text.more(items.length - 8)}` : ""}` : label;
1854
+ }
1855
+ }
1856
+ showSelection(_sel) {
1857
+ }
1858
+ hit(x, y) {
1859
+ const el = this.ctx.doc.elementFromPoint(x, y)?.closest(".tnc-ym-day");
1860
+ const c = el ? this.cells.find((cell) => cell.el === el) : void 0;
1861
+ return c ? { kind: "day", day: c.day } : null;
1862
+ }
1863
+ dayAt(el) {
1864
+ const btn = el.closest(".tnc-ym-day");
1865
+ return this.cells.find((c) => c.el === btn)?.day ?? null;
1866
+ }
1867
+ getScroll() {
1868
+ return { top: this.scroller.scrollTop, left: 0 };
1869
+ }
1870
+ setScroll(s) {
1871
+ this.scroller.scrollTop = s.top;
1872
+ }
1873
+ elementsFor(_id) {
1874
+ return [];
1875
+ }
1876
+ destroy() {
1877
+ this.el.remove();
1878
+ }
1879
+ };
1880
+
1881
+ // src/dom/calendar.ts
1882
+ var EXTERNAL_ID = "__tnc_external__";
1883
+ function sameOption(a, b, depth = 0) {
1884
+ if (a === b) return true;
1885
+ if (typeof a === "function" && typeof b === "function") return true;
1886
+ if (depth > 4 || !a || !b || typeof a !== "object" || typeof b !== "object") return false;
1887
+ if (Array.isArray(a) || Array.isArray(b)) {
1888
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
1889
+ return a.every((v, i) => sameOption(v, b[i], depth + 1));
1890
+ }
1891
+ if (Object.getPrototypeOf(a) !== Object.prototype || Object.getPrototypeOf(b) !== Object.prototype) return false;
1892
+ const ka = Object.keys(a);
1893
+ const kb = Object.keys(b);
1894
+ if (ka.length !== kb.length) return false;
1895
+ return ka.every((k) => sameOption(a[k], b[k], depth + 1));
1896
+ }
1897
+ function timelineSansRows(t) {
1898
+ return t ? { ...t, rows: null } : t;
1899
+ }
1900
+ function sameProposal(a, b) {
1901
+ return !!a && !!b && a.startMs === b.startMs && a.endMs === b.endMs && a.allDay === b.allDay && a.rowId === b.rowId;
1902
+ }
1903
+ var Calendar = class {
1904
+ constructor(host, options = {}, items = []) {
1905
+ this.host = host;
1906
+ this.doc = host.ownerDocument;
1907
+ this.win = this.doc.defaultView ?? globalThis;
1908
+ this.opts = { ...defaultOptions(), ...options };
1909
+ this.zone = getTimeZone(this.opts.timeZone);
1910
+ this.fmt = new Formatter(this.opts.locale, this.opts.hour12);
1911
+ this.text = this.resolveText();
1912
+ this.order = parseOrder(this.opts.eventOrder);
1913
+ this.store = new ItemStore(this.zone, {
1914
+ defaultTimedMinutes: this.opts.defaultTimedMinutes,
1915
+ defaultAllDayDays: this.opts.defaultAllDayDays
1916
+ });
1917
+ this.store.setAll(items);
1918
+ this.conflicts = dependencyConflicts(this.store);
1919
+ this.anchor = this.toDay(this.opts.date) ?? this.todayDay();
1920
+ this.selectedIds = this.selectedSet();
1921
+ this.root = h(this.doc, "div", "tnc", { "data-tnc": "" });
1922
+ this.toolbarEl = h(this.doc, "div", "tnc-toolbar", { role: "toolbar" });
1923
+ this.viewHost = h(this.doc, "div", "tnc-view");
1924
+ this.root.append(this.toolbarEl, this.viewHost);
1925
+ host.append(this.root);
1926
+ this.bindEvents();
1927
+ this.rebuild(true);
1928
+ this.scheduleTick();
1929
+ const RO = this.win.ResizeObserver;
1930
+ if (RO) {
1931
+ this.resizeObs = new RO(() => this.current?.resize?.());
1932
+ this.resizeObs.observe(this.root);
1933
+ }
1934
+ }
1935
+ host;
1936
+ doc;
1937
+ win;
1938
+ root;
1939
+ store;
1940
+ meta = /* @__PURE__ */ new WeakMap();
1941
+ opts;
1942
+ zone;
1943
+ fmt;
1944
+ text;
1945
+ order;
1946
+ view;
1947
+ firstDay = 0;
1948
+ days = [];
1949
+ today = 0;
1950
+ conflicts = /* @__PURE__ */ new Map();
1951
+ slot = { min: 0, max: MINUTES_PER_DAY, duration: 30, snap: 30, scroll: 480 };
1952
+ draggingId = null;
1953
+ dragRow = null;
1954
+ toolbarEl;
1955
+ viewHost;
1956
+ current = null;
1957
+ anchor = 0;
1958
+ session = null;
1959
+ selection = null;
1960
+ suppressClick = false;
1961
+ selectedIds = /* @__PURE__ */ new Set();
1962
+ popover = null;
1963
+ timer = null;
1964
+ resizeObs = null;
1965
+ destroyed = false;
1966
+ pendingChange = null;
1967
+ frame = null;
1968
+ /* ================================================================ public */
1969
+ /** Replaces all items, re-rendering only what changed. */
1970
+ setItems(items) {
1971
+ const drag = this.session?.active && this.session.origin ? this.store.get(this.session.origin.id) : void 0;
1972
+ const change = this.store.setAll(items);
1973
+ if (drag && this.session?.origin) {
1974
+ const fresh = this.store.get(drag.id);
1975
+ if (fresh) this.session.origin = fresh;
1976
+ this.merge(change, this.store.upsert([drag.item]));
1977
+ }
1978
+ this.applyChange(change);
1979
+ }
1980
+ upsertItems(items) {
1981
+ this.applyChange(this.store.upsert(items));
1982
+ }
1983
+ removeItems(ids) {
1984
+ this.applyChange(this.store.remove(ids));
1985
+ }
1986
+ getItem(id) {
1987
+ return this.store.get(id)?.item;
1988
+ }
1989
+ /**
1990
+ * Updates options. Only what the change affects is rebuilt.
1991
+ *
1992
+ * In the timeline, a new `date`, range or scale keeps what is on screen
1993
+ * instead of jumping back to the anchor date: a scale change keeps the
1994
+ * instant at the middle of the visible track, other changes keep the left
1995
+ * edge. Pass `anchor` to pin a different instant, e.g. the one under the
1996
+ * pointer when zooming with the wheel.
1997
+ */
1998
+ setOptions(next, how = {}) {
1999
+ const prev = this.opts;
2000
+ this.opts = { ...prev, ...next };
2001
+ const changed = Object.keys(next).filter(
2002
+ (k) => !sameOption(next[k], prev[k])
2003
+ );
2004
+ if (!changed.length) return;
2005
+ const has = (k) => changed.includes(k);
2006
+ if (has("selected")) this.applySelected();
2007
+ const rest = changed.filter((k) => k !== "selected");
2008
+ if (!rest.length) return;
2009
+ if (rest.length === 1 && has("timeline") && this.current instanceof TimelineView && sameOption(timelineSansRows(this.opts.timeline), timelineSansRows(prev.timeline))) {
2010
+ this.current.render(null);
2011
+ if (this.selection) this.current.showSelection(this.selection);
2012
+ return;
2013
+ }
2014
+ if (has("timeZone") || has("defaultTimedMinutes") || has("defaultAllDayDays")) {
2015
+ this.zone = getTimeZone(this.opts.timeZone);
2016
+ this.store.reconfigure(this.zone, {
2017
+ defaultTimedMinutes: this.opts.defaultTimedMinutes,
2018
+ defaultAllDayDays: this.opts.defaultAllDayDays
2019
+ });
2020
+ }
2021
+ if (has("locale") || has("hour12")) this.fmt = new Formatter(this.opts.locale, this.opts.hour12);
2022
+ if (has("text") || has("locale")) this.text = this.resolveText();
2023
+ if (has("eventOrder")) this.order = parseOrder(this.opts.eventOrder);
2024
+ if (has("date")) this.anchor = this.toDay(this.opts.date) ?? this.anchor;
2025
+ const needsRebuild = changed.some((k) => LAYOUT_KEYS.has(k)) || has("date");
2026
+ if (!needsRebuild) return;
2027
+ const keep = !has("view") && this.opts.view === "timeline" ? this.timelineKeep(prev, how.anchor) : null;
2028
+ if (keep) this.rebuild(false, keep);
2029
+ else this.rebuild(has("view") || has("date"));
2030
+ }
2031
+ /** What a timeline rebuild should keep on screen. */
2032
+ timelineKeep(prev, anchor) {
2033
+ const view = this.current;
2034
+ if (!(view instanceof TimelineView)) return null;
2035
+ const top = view.getScroll().top;
2036
+ if (anchor) {
2037
+ const date = anchor.date !== void 0 ? this.toMs(anchor.date) : null;
2038
+ if (anchor.clientX !== void 0) {
2039
+ const vx2 = anchor.clientX - view.viewportLeft();
2040
+ return { ms: date ?? view.instantAt(vx2), vx: vx2, top };
2041
+ }
2042
+ if (date !== null) return { ms: date, vx: view.viewportXOf(date), top };
2043
+ }
2044
+ const before = prev.timeline?.scale;
2045
+ const after = this.opts.timeline?.scale;
2046
+ const rescaled = !sameOption(before, after) || prev.timeline?.labelWidth !== this.opts.timeline?.labelWidth;
2047
+ const vx = rescaled ? view.viewportCentre() : this.opts.timeline?.labelWidth ?? 240;
2048
+ return { ms: view.instantAt(vx), vx, top };
2049
+ }
2050
+ /** Re-renders everything, e.g. after render hooks start returning different content. */
2051
+ rerender() {
2052
+ this.rebuild(false);
2053
+ }
2054
+ getOptions() {
2055
+ return this.opts;
2056
+ }
2057
+ changeView(view, date) {
2058
+ if (date !== void 0) this.anchor = this.toDay(date) ?? this.anchor;
2059
+ if (view === this.opts.view && date === void 0) return;
2060
+ this.opts = { ...this.opts, view };
2061
+ this.rebuild(true);
2062
+ this.opts.onViewChange?.(this.view);
2063
+ }
2064
+ gotoDate(date) {
2065
+ const day = this.toDay(date);
2066
+ if (day === null) return;
2067
+ this.anchor = day;
2068
+ this.rebuild(true);
2069
+ }
2070
+ prev() {
2071
+ this.anchor = this.step(-1);
2072
+ this.rebuild(true);
2073
+ }
2074
+ next() {
2075
+ this.anchor = this.step(1);
2076
+ this.rebuild(true);
2077
+ }
2078
+ goToday() {
2079
+ this.anchor = this.todayDay();
2080
+ this.rebuild(true);
2081
+ }
2082
+ /** The anchor date, as local midnight of that civil day in the calendar zone. */
2083
+ getDate() {
2084
+ return new Date(toInstant(this.anchor, 0, this.zone));
2085
+ }
2086
+ getView() {
2087
+ return this.view;
2088
+ }
2089
+ /** Clears the current selection. */
2090
+ unselect() {
2091
+ if (!this.selection) return;
2092
+ this.selection = null;
2093
+ this.current?.showSelection(null);
2094
+ this.opts.onUnselect?.();
2095
+ }
2096
+ /** Re-measures (e.g. after the host was shown or resized by a parent). */
2097
+ updateSize() {
2098
+ this.current?.resize?.();
2099
+ }
2100
+ getScroll() {
2101
+ return this.current?.getScroll() ?? { top: 0, left: 0 };
2102
+ }
2103
+ setScroll(s) {
2104
+ this.current?.setScroll(s);
2105
+ }
2106
+ scrollToTime(time) {
2107
+ this.current?.scrollToTime?.(parseDurationMinutes(time, this.slot.scroll));
2108
+ }
2109
+ /**
2110
+ * Scrolls a date (or instant) into view. In the timeline `align` places it
2111
+ * at the start, middle or end of the visible track; other views move to
2112
+ * the date if it is outside the range, and time grids scroll to its time.
2113
+ */
2114
+ scrollToDate(date, opts = {}) {
2115
+ const p = parseDateInput(date, this.zone);
2116
+ if (!p) return;
2117
+ const day = p.kind === "day" ? p.day : dayOf(p.ms, this.zone);
2118
+ const ms = p.kind === "day" ? toInstant(p.day, 0, this.zone) : p.ms;
2119
+ const { startDay, endDay } = this.view.range;
2120
+ if (day < startDay || day >= endDay) {
2121
+ this.anchor = day;
2122
+ this.rebuild(true);
2123
+ }
2124
+ const view = this.current;
2125
+ if (view instanceof TimelineView) view.scrollToInstant(ms, opts.align ?? "center", opts.smooth ?? false);
2126
+ else if (view instanceof TimeGridView && p.kind !== "day") view.scrollToTime(wallTime(ms, this.zone).minute);
2127
+ }
2128
+ /** What is under a client point: a time slot, a day, or a timeline row and date. Null outside the view. */
2129
+ hitTest(clientX, clientY) {
2130
+ return this.current?.hit(clientX, clientY) ?? null;
2131
+ }
2132
+ /** Item elements currently rendered for an id. */
2133
+ elementsFor(id) {
2134
+ return this.current?.elementsFor(id) ?? [];
2135
+ }
2136
+ /**
2137
+ * Starts dragging something from outside the calendar (a task tray, an
2138
+ * unscheduled list). Call from a pointerdown handler; the calendar follows
2139
+ * the pointer and fires `onExternalDrop` on release.
2140
+ */
2141
+ beginExternalDrag(ev, source) {
2142
+ this.endSession(null, true);
2143
+ this.session = {
2144
+ mode: "external",
2145
+ pointerId: ev.pointerId,
2146
+ pointerType: ev.pointerType,
2147
+ x0: ev.clientX,
2148
+ y0: ev.clientY,
2149
+ active: true,
2150
+ origin: null,
2151
+ grab: null,
2152
+ proposal: null,
2153
+ denied: false,
2154
+ timer: null,
2155
+ lastEvent: ev,
2156
+ source
2157
+ };
2158
+ this.root.classList.add("tnc-dragging", "tnc-dragging-external");
2159
+ this.listenDoc(true);
2160
+ }
2161
+ destroy() {
2162
+ if (this.destroyed) return;
2163
+ this.destroyed = true;
2164
+ this.endSession(null, true);
2165
+ this.closeMore();
2166
+ if (this.timer !== null) this.win.clearTimeout(this.timer);
2167
+ if (this.frame !== null) this.win.cancelAnimationFrame(this.frame);
2168
+ this.resizeObs?.disconnect();
2169
+ this.unbindEvents();
2170
+ this.current?.destroy();
2171
+ this.current = null;
2172
+ this.root.remove();
2173
+ }
2174
+ /* ========================================================== view context */
2175
+ now() {
2176
+ return Date.now();
2177
+ }
2178
+ suppressNextClick() {
2179
+ this.suppressClick = true;
2180
+ this.win.setTimeout(() => this.suppressClick = false, 0);
2181
+ }
2182
+ dayArg(day) {
2183
+ return {
2184
+ day,
2185
+ date: new Date(toInstant(day, 0, this.zone)),
2186
+ dateStr: formatDay(day),
2187
+ view: this.view,
2188
+ isToday: day === this.today,
2189
+ isPast: day < this.today
2190
+ };
2191
+ }
2192
+ dayClasses(day) {
2193
+ const out = [];
2194
+ const wd = weekdayOf(day);
2195
+ if (wd === 0 || wd === 6) out.push("is-weekend");
2196
+ if (day === this.today && this.opts.todayHighlight) out.push("is-today");
2197
+ if (day < this.today) out.push("is-past");
2198
+ out.push(...classList(this.opts.dayClassNames?.(this.dayArg(day))));
2199
+ return out;
2200
+ }
2201
+ isEditable(n, what) {
2202
+ if (n.id === EXTERNAL_ID || this.opts.view === "list" || this.opts.view === "year") return false;
2203
+ const it = n.item;
2204
+ const base = it.editable ?? this.opts.editable;
2205
+ const specific = what === "start" ? it.startEditable : it.durationEditable;
2206
+ return specific ?? base;
2207
+ }
2208
+ navLink(day, label, cls) {
2209
+ const btn = h(this.doc, "button", `tnc-navlink ${cls}`, { type: "button", "data-nav-date": formatDay(day) }, label);
2210
+ btn.addEventListener("click", (ev) => {
2211
+ ev.stopPropagation();
2212
+ this.navigate(day, ev);
2213
+ });
2214
+ btn.addEventListener("pointerdown", (ev) => ev.stopPropagation());
2215
+ return btn;
2216
+ }
2217
+ navigate(day, jsEvent, view = "day") {
2218
+ if (this.opts.onNavLink) {
2219
+ this.opts.onNavLink({ ...this.dayArg(day), target: view, jsEvent });
2220
+ return;
2221
+ }
2222
+ this.changeView(view, formatDay(day));
2223
+ }
2224
+ createItemEl(meta) {
2225
+ const { n, variant } = meta;
2226
+ const it = n.item;
2227
+ const el = h(this.doc, "div", "tnc-item");
2228
+ el.classList.add(`tnc-item--${variant}`);
2229
+ if (it.kind) el.classList.add(`tnc-k-${it.kind.replace(/[^\w-]/g, "-")}`);
2230
+ if (it.completed) el.classList.add("is-completed");
2231
+ if (it.display === "background") el.classList.add("is-background");
2232
+ if (it.lane === "actual") el.classList.add("is-actual");
2233
+ if (!meta.isStart) el.classList.add("is-cont-start");
2234
+ if (!meta.isEnd) el.classList.add("is-cont-end");
2235
+ if (n.allDay) el.classList.add("is-all-day");
2236
+ if (meta.mirror || n.id === EXTERNAL_ID) el.classList.add("is-mirror");
2237
+ if (this.conflicts.has(n.id)) el.classList.add("is-conflict");
2238
+ const indicator = variant === "indicator";
2239
+ if (!meta.mirror && !indicator && (this.isEditable(n, "start") || this.isEditable(n, "duration"))) el.classList.add("is-draggable");
2240
+ if (this.draggingId === n.id) el.classList.add("is-dragging");
2241
+ if (this.selectedIds.has(n.id) && !meta.mirror) el.classList.add("is-selected");
2242
+ el.classList.add(...classList(it.className));
2243
+ const arg = this.itemArg(el, meta);
2244
+ el.classList.add(...classList(this.opts.itemClassNames?.(arg)));
2245
+ el.dataset.id = n.id;
2246
+ if (it.kind) el.dataset.kind = it.kind;
2247
+ const color = it.color ?? it.borderColor;
2248
+ if (color) el.style.setProperty("--tnc-c", color);
2249
+ if (it.backgroundColor) el.style.setProperty("--tnc-bg", it.backgroundColor);
2250
+ if (it.borderColor) el.style.setProperty("--tnc-border", it.borderColor);
2251
+ if (it.textColor) el.style.setProperty("--tnc-fg", it.textColor);
2252
+ if (indicator) el.setAttribute("aria-hidden", "true");
2253
+ else if (it.display !== "background") {
2254
+ el.tabIndex = 0;
2255
+ el.setAttribute("role", "button");
2256
+ el.setAttribute("aria-label", this.ariaLabel(n));
2257
+ }
2258
+ this.meta.set(el, meta);
2259
+ const custom = variant === "list" ? this.opts.listItemContent?.(arg) : null;
2260
+ const content = indicator ? null : custom ?? this.opts.itemContent?.(arg);
2261
+ if (indicator) {
2262
+ } else if (content != null) {
2263
+ const wrap = h(this.doc, "div", "tnc-item-main");
2264
+ wrap.append(content);
2265
+ el.append(wrap);
2266
+ } else {
2267
+ el.append(this.defaultContent(meta));
2268
+ }
2269
+ if (variant !== "list" && !indicator && it.display !== "background") this.addHandles(el, meta);
2270
+ this.opts.itemDidMount?.(arg);
2271
+ return el;
2272
+ }
2273
+ refreshItemEl(el, meta) {
2274
+ const time = el.querySelector(":scope .tnc-item-time");
2275
+ if (time) this.fillTime(time, meta.n, meta.variant, meta.day);
2276
+ el.classList.toggle("is-dragging", this.draggingId === meta.n.id);
2277
+ el.classList.toggle("is-conflict", this.conflicts.has(meta.n.id));
2278
+ el.classList.toggle("is-selected", this.selectedIds.has(meta.n.id) && !meta.mirror);
2279
+ }
2280
+ selectedSet() {
2281
+ const s = this.opts.selected;
2282
+ return new Set(s == null ? [] : typeof s === "string" ? [s] : s);
2283
+ }
2284
+ applySelected() {
2285
+ this.selectedIds = this.selectedSet();
2286
+ for (const el of this.viewHost.querySelectorAll(".tnc-item")) {
2287
+ const meta = this.meta.get(el);
2288
+ if (meta) el.classList.toggle("is-selected", this.selectedIds.has(meta.n.id) && !meta.mirror);
2289
+ }
2290
+ }
2291
+ destroyItemEl(el) {
2292
+ const meta = this.meta.get(el);
2293
+ if (meta) this.opts.itemWillUnmount?.(this.itemArg(el, meta));
2294
+ el.remove();
2295
+ }
2296
+ openMore(day, anchor, jsEvent) {
2297
+ jsEvent.stopPropagation();
2298
+ 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));
2299
+ const shown = new Set(
2300
+ [...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 ?? "")
2301
+ );
2302
+ const res = this.opts.onMoreClick?.({
2303
+ day,
2304
+ date: new Date(toInstant(day, 0, this.zone)),
2305
+ items: items.map((n) => n.item),
2306
+ hidden: items.filter((n) => !shown.has(n.id)).map((n) => n.item),
2307
+ jsEvent,
2308
+ el: anchor,
2309
+ view: this.view
2310
+ });
2311
+ if (res === false) return;
2312
+ this.closeMore();
2313
+ const el = h(this.doc, "div", "tnc-popover", { role: "dialog", "aria-label": this.fmt.longDate(day), tabindex: "-1" });
2314
+ const close = h(this.doc, "button", "tnc-popover-close", { type: "button", "aria-label": "Close" }, "\xD7");
2315
+ close.addEventListener("click", () => this.closeMore(true));
2316
+ 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));
2317
+ const body = h(this.doc, "div", "tnc-popover-body");
2318
+ el.append(h(this.doc, "div", "tnc-popover-head", null, title, close), body);
2319
+ this.root.append(el);
2320
+ const pool = new ItemPool(this, body);
2321
+ pool.begin();
2322
+ for (const n of items) {
2323
+ pool.place(n.id, { n, day, isStart: n.startDay >= day, isEnd: n.endDay <= day + 1, variant: n.allDay ? "bar" : "dot", mirror: false });
2324
+ }
2325
+ pool.end();
2326
+ this.popover = { el, pool, anchor, day };
2327
+ const rr = this.root.getBoundingClientRect();
2328
+ const ar = anchor.getBoundingClientRect();
2329
+ const w = Math.max(220, ar.width + 24);
2330
+ let left = ar.left - rr.left - 12;
2331
+ left = Math.max(4, Math.min(left, rr.width - w - 4));
2332
+ let top = ar.top - rr.top - 40;
2333
+ const height = el.offsetHeight;
2334
+ top = Math.max(4, Math.min(top, rr.height - height - 4));
2335
+ setStyle(el, { left, top, width: w });
2336
+ el.focus();
2337
+ }
2338
+ /* =============================================================== render */
2339
+ resolveText() {
2340
+ const t = this.opts.text;
2341
+ return {
2342
+ ...DEFAULT_TEXT,
2343
+ ...t,
2344
+ views: { ...DEFAULT_TEXT.views, ...t.views },
2345
+ viewHints: { ...DEFAULT_TEXT.viewHints, ...t.viewHints }
2346
+ };
2347
+ }
2348
+ todayDay() {
2349
+ return dayOf(this.now(), this.zone);
2350
+ }
2351
+ toDay(date) {
2352
+ if (date === void 0 || date === null) return null;
2353
+ const p = parseDateInput(date, this.zone);
2354
+ if (!p) return null;
2355
+ return p.kind === "day" ? p.day : dayOf(p.ms, this.zone);
2356
+ }
2357
+ toMs(date) {
2358
+ const p = parseDateInput(date, this.zone);
2359
+ if (!p) return null;
2360
+ return p.kind === "day" ? toInstant(p.day, 0, this.zone) : p.ms;
2361
+ }
2362
+ step(dir) {
2363
+ const { view, dayCount, listDayCount } = this.opts;
2364
+ switch (view) {
2365
+ case "day":
2366
+ return this.anchor + dir;
2367
+ case "days":
2368
+ return this.anchor + dir * Math.max(1, dayCount);
2369
+ case "week":
2370
+ return this.anchor + dir * 7;
2371
+ case "list": {
2372
+ const n = Math.max(1, listDayCount);
2373
+ return this.anchor + dir * (this.opts.listStart === "week" ? Math.ceil(n / 7) * 7 : n);
2374
+ }
2375
+ case "month":
2376
+ return addMonths(startOfMonth(this.anchor), dir);
2377
+ case "year":
2378
+ return addMonths(startOfYear(this.anchor), 12 * dir);
2379
+ case "timeline": {
2380
+ const len = this.opts.timeline?.days ?? 120;
2381
+ return this.anchor + dir * Math.max(7, Math.round(len / 3));
2382
+ }
2383
+ }
2384
+ }
2385
+ computeRange() {
2386
+ const { view, dayCount, listDayCount } = this.opts;
2387
+ const a = this.anchor;
2388
+ switch (view) {
2389
+ case "day":
2390
+ return { startDay: a, endDay: a + 1 };
2391
+ case "days":
2392
+ return { startDay: a, endDay: a + Math.max(1, dayCount) };
2393
+ case "week": {
2394
+ const s = startOfWeek(a, this.firstDay);
2395
+ return { startDay: s, endDay: s + 7 };
2396
+ }
2397
+ case "list": {
2398
+ const s = this.opts.listStart === "week" ? startOfWeek(a, this.firstDay) : a;
2399
+ return { startDay: s, endDay: s + Math.max(1, listDayCount) };
2400
+ }
2401
+ case "month": {
2402
+ const s = startOfWeek(startOfMonth(a), this.firstDay);
2403
+ return { startDay: s, endDay: s + 42 };
2404
+ }
2405
+ case "year": {
2406
+ const s = startOfYear(a);
2407
+ return { startDay: s, endDay: addMonths(s, 12) };
2408
+ }
2409
+ case "timeline": {
2410
+ const lead = this.opts.timeline?.leadDays ?? 14;
2411
+ const len = this.opts.timeline?.days ?? 120;
2412
+ return { startDay: a - lead, endDay: a - lead + len };
2413
+ }
2414
+ }
2415
+ }
2416
+ computeTitle(startDay, endDay) {
2417
+ const { view } = this.opts;
2418
+ if (view === "month") return this.fmt.monthYear(this.anchor);
2419
+ if (view === "year") return this.fmt.year(this.anchor);
2420
+ if (view === "day") return this.fmt.fullDate(this.anchor);
2421
+ const visible = this.days;
2422
+ const first = visible[0] ?? startDay;
2423
+ const last = visible[visible.length - 1] ?? endDay - 1;
2424
+ return this.fmt.range(first, last);
2425
+ }
2426
+ rebuild(navigated, keep) {
2427
+ const scroll = navigated || keep ? null : this.current?.getScroll();
2428
+ this.closeMore();
2429
+ this.unselectSilently();
2430
+ this.firstDay = this.opts.firstDay ?? localeFirstDay(this.opts.locale);
2431
+ this.today = this.todayDay();
2432
+ const min = parseDurationMinutes(this.opts.slotMinTime, 0);
2433
+ const max = Math.max(min + 30, parseDurationMinutes(this.opts.slotMaxTime, MINUTES_PER_DAY));
2434
+ const duration = Math.max(1, parseDurationMinutes(this.opts.slotDuration, 30));
2435
+ const snap = Math.max(1, parseDurationMinutes(this.opts.snapDuration ?? null, duration));
2436
+ this.slot = { min, max, duration, snap, scroll: parseDurationMinutes(this.opts.scrollTime, 480) };
2437
+ const { startDay, endDay } = this.computeRange();
2438
+ const hidden = new Set(this.opts.hiddenDays);
2439
+ if (!this.opts.weekends) {
2440
+ hidden.add(0);
2441
+ hidden.add(6);
2442
+ }
2443
+ const days = [];
2444
+ const listLike = this.opts.view === "year" || this.opts.view === "timeline" || this.opts.view === "day";
2445
+ for (let d = startDay; d < endDay; d++) if (listLike || !hidden.has(weekdayOf(d))) days.push(d);
2446
+ this.days = days.length ? days : [startDay];
2447
+ const range = {
2448
+ startDay,
2449
+ endDay,
2450
+ start: new Date(toInstant(startDay, 0, this.zone)),
2451
+ end: new Date(toInstant(endDay, 0, this.zone))
2452
+ };
2453
+ const prevView = this.view;
2454
+ this.view = { type: this.opts.view, range, anchor: this.anchor, title: "" };
2455
+ this.view.title = this.computeTitle(startDay, endDay);
2456
+ this.conflicts = dependencyConflicts(this.store);
2457
+ this.root.className = "tnc";
2458
+ this.root.classList.add(`tnc--${this.opts.view}`, `tnc--height-${this.opts.height}`);
2459
+ if (!this.opts.timeGrid) this.root.classList.add("tnc--no-time-grid");
2460
+ this.renderToolbar();
2461
+ this.current?.destroy();
2462
+ this.current = this.createView();
2463
+ this.viewHost.replaceChildren(this.current.el);
2464
+ this.current.render(null);
2465
+ if (keep && this.current instanceof TimelineView) {
2466
+ this.current.setScroll({ top: keep.top, left: 0 });
2467
+ this.current.placeInstant(keep.ms, keep.vx);
2468
+ } else if (scroll) this.current.setScroll(scroll);
2469
+ else if (this.current instanceof TimeGridView) this.current.scrollToTime(this.slot.scroll);
2470
+ else if (this.current instanceof TimelineView) this.current.scrollToDay(this.anchor);
2471
+ const rangeChanged = !prevView || prevView.type !== this.view.type || prevView.range.startDay !== startDay || prevView.range.endDay !== endDay;
2472
+ if (rangeChanged) this.opts.onRangeChange?.(this.view);
2473
+ }
2474
+ createView() {
2475
+ switch (this.opts.view) {
2476
+ case "month":
2477
+ return new DayGridView(this);
2478
+ case "list":
2479
+ return new ListView(this);
2480
+ case "year":
2481
+ return new YearView(this);
2482
+ case "timeline":
2483
+ return new TimelineView(this);
2484
+ default:
2485
+ return new TimeGridView(this);
2486
+ }
2487
+ }
2488
+ renderToolbar() {
2489
+ const tb = this.opts.toolbar;
2490
+ this.toolbarEl.replaceChildren();
2491
+ this.toolbarEl.hidden = !tb;
2492
+ if (!tb) return;
2493
+ const cfg = tb === true ? {} : tb;
2494
+ const views = cfg.views ?? ["month", "week", "days", "day", "list", "year"];
2495
+ const btn = (label, hint, cls, onClick, pressed) => {
2496
+ const b = h(this.doc, "button", `tnc-btn ${cls}`, { type: "button", title: hint, "aria-label": hint }, label);
2497
+ if (pressed !== void 0) b.setAttribute("aria-pressed", String(pressed));
2498
+ b.addEventListener("click", onClick);
2499
+ return b;
2500
+ };
2501
+ const extras = (cfg.extraButtons ?? []).map((e) => {
2502
+ const b = btn(e.text, e.hint ?? e.text, `tnc-btn-extra tnc-btn-${e.id}`, (ev) => e.onClick(ev));
2503
+ if (e.icon) b.replaceChildren(e.icon());
2504
+ return b;
2505
+ });
2506
+ const nav = h(
2507
+ this.doc,
2508
+ "div",
2509
+ "tnc-toolbar-group tnc-toolbar-nav",
2510
+ null,
2511
+ btn("\u2039", this.text.prev, "tnc-btn-prev", () => this.prev()),
2512
+ btn(this.text.today, this.text.today, "tnc-btn-today", () => this.goToday()),
2513
+ btn("\u203A", this.text.next, "tnc-btn-next", () => this.next())
2514
+ );
2515
+ const title = h(this.doc, "h2", "tnc-title", { "aria-live": "polite" }, this.view.title);
2516
+ const viewGroup = h(this.doc, "div", "tnc-toolbar-group tnc-toolbar-views", { role: "group" });
2517
+ for (const v of views) {
2518
+ const label = v === "days" ? this.text.views.days ?? `${this.opts.dayCount} days` : this.text.views[v] ?? v;
2519
+ viewGroup.append(btn(label, this.text.viewHints[v] ?? label, `tnc-btn-view tnc-btn-view-${v}`, () => this.changeView(v), v === this.opts.view));
2520
+ }
2521
+ const start = h(this.doc, "div", "tnc-toolbar-start");
2522
+ const end = h(this.doc, "div", "tnc-toolbar-end");
2523
+ if (cfg.extraPlacement === "start") start.append(...extras);
2524
+ start.append(nav, title);
2525
+ end.append(viewGroup);
2526
+ if (cfg.extraPlacement !== "start") end.append(...extras);
2527
+ this.toolbarEl.append(start, end);
2528
+ }
2529
+ applyChange(change) {
2530
+ if (!change.added.length && !change.updated.length && !change.removed.length && !change.days.size && !change.spans) return;
2531
+ this.conflicts = dependencyConflicts(this.store);
2532
+ this.pendingChange = this.pendingChange ? this.merge(this.pendingChange, change) : change;
2533
+ if (this.session?.active) {
2534
+ this.flush();
2535
+ return;
2536
+ }
2537
+ if (this.frame === null) this.frame = this.win.requestAnimationFrame(() => this.flush());
2538
+ }
2539
+ /** Renders pending changes now. Called automatically on the next frame. */
2540
+ flush() {
2541
+ if (this.frame !== null) {
2542
+ this.win.cancelAnimationFrame(this.frame);
2543
+ this.frame = null;
2544
+ }
2545
+ const change = this.pendingChange;
2546
+ this.pendingChange = null;
2547
+ if (!change || !this.current) return;
2548
+ this.current.render(change);
2549
+ if (this.selection) this.current.showSelection(this.selection);
2550
+ if (this.popover) this.refreshMore();
2551
+ }
2552
+ merge(a, b) {
2553
+ for (const d of b.days) a.days.add(d);
2554
+ a.added.push(...b.added);
2555
+ a.updated.push(...b.updated);
2556
+ a.removed.push(...b.removed);
2557
+ a.spans ||= b.spans;
2558
+ return a;
2559
+ }
2560
+ scheduleTick() {
2561
+ const delay = 6e4 - this.now() % 6e4 + 50;
2562
+ this.timer = this.win.setTimeout(() => {
2563
+ if (this.destroyed) return;
2564
+ if (this.todayDay() !== this.today) this.rebuild(false);
2565
+ else this.current?.tick?.();
2566
+ this.scheduleTick();
2567
+ }, delay);
2568
+ }
2569
+ /* ========================================================= item content */
2570
+ itemArg(el, meta) {
2571
+ const item = this.store.get(meta.n.id)?.item ?? meta.n.item;
2572
+ const { start, end } = this.timeLabel(meta.n, meta.variant, meta.day);
2573
+ return {
2574
+ item,
2575
+ el,
2576
+ view: this.view,
2577
+ day: meta.day,
2578
+ isStart: meta.isStart,
2579
+ isEnd: meta.isEnd,
2580
+ isMirror: meta.mirror,
2581
+ variant: meta.variant,
2582
+ timeText: meta.n.allDay ? "" : end ? `${start} \u2013 ${end}` : start
2583
+ };
2584
+ }
2585
+ /** Start and (for timed and list items) end labels; the end is hidden on short items by CSS. */
2586
+ timeLabel(n, variant, day) {
2587
+ if (n.allDay) return { start: variant === "list" ? this.text.allDay : "", end: "" };
2588
+ const s = wallTime(n.startMs, this.zone);
2589
+ const start = this.fmt.time(s.minute, true);
2590
+ if (variant === "dot" || variant === "bar" || variant === "indicator") return { start: s.day === day ? start : "", end: "" };
2591
+ if (n.startMs === n.endMs) return { start, end: "" };
2592
+ return { start, end: this.fmt.time(wallTime(n.endMs, this.zone).minute, true) };
2593
+ }
2594
+ fillTime(el, n, variant, day) {
2595
+ const { start, end } = this.timeLabel(n, variant, day);
2596
+ el.textContent = start;
2597
+ if (end) el.append(h(this.doc, "span", "tnc-item-time-end", null, ` \u2013 ${end}`));
2598
+ return start !== "";
2599
+ }
2600
+ ariaLabel(n) {
2601
+ if (n.allDay) {
2602
+ const last = n.endDay - 1;
2603
+ return `${n.item.title}, ${last > n.startDay ? this.fmt.range(n.startDay, last) : this.fmt.longDate(n.startDay)}`;
2604
+ }
2605
+ const s = wallTime(n.startMs, this.zone);
2606
+ const e = wallTime(n.endMs, this.zone);
2607
+ return `${n.item.title}, ${this.fmt.longDate(s.day)} ${this.fmt.time(s.minute)} \u2013 ${this.fmt.time(e.minute)}`;
2608
+ }
2609
+ defaultContent(meta) {
2610
+ const { n, variant, day } = meta;
2611
+ const doc = this.doc;
2612
+ const main = h(doc, "div", "tnc-item-main");
2613
+ const title = h(doc, "span", "tnc-item-title", null, n.item.title);
2614
+ const time = h(doc, "span", "tnc-item-time");
2615
+ const label = this.fillTime(time, n, variant, day);
2616
+ switch (variant) {
2617
+ case "timed":
2618
+ main.append(title, time);
2619
+ break;
2620
+ case "dot":
2621
+ main.append(h(doc, "span", "tnc-item-dot", { "aria-hidden": "true" }), time, title);
2622
+ break;
2623
+ case "list":
2624
+ main.append(time, h(doc, "span", "tnc-item-dot", { "aria-hidden": "true" }), title);
2625
+ break;
2626
+ default:
2627
+ if (label) main.append(time);
2628
+ main.append(title);
2629
+ }
2630
+ return main;
2631
+ }
2632
+ addHandles(el, meta) {
2633
+ const { n, variant } = meta;
2634
+ const ports = variant === "timeline" && !meta.mirror && n.id !== EXTERNAL_ID && !!this.opts.timeline?.onLink;
2635
+ if (variant === "dot" || !this.isEditable(n, "duration")) {
2636
+ if (ports) this.addPorts(el, n);
2637
+ return;
2638
+ }
2639
+ const vertical = variant === "timed";
2640
+ const milestone = variant === "timeline" && n.item.kind === "milestone";
2641
+ const handleLabel = variant === "timeline" ? this.opts.timeline?.resizeHandleLabel : void 0;
2642
+ const handle = (edge) => {
2643
+ const label = handleLabel?.({ item: n.item, edge: edge === "start" ? "start" : "finish" });
2644
+ const attrs = label ? { role: "button", tabindex: "0", "aria-label": label } : { "aria-hidden": "true" };
2645
+ el.append(h(this.doc, "div", `tnc-resize tnc-resize-${edge} ${vertical ? "is-v" : "is-h"}`, attrs));
2646
+ };
2647
+ if (meta.isStart && (this.opts.resizableFromStart || variant === "timeline") && !milestone) handle("start");
2648
+ if (meta.isEnd && !milestone) handle("end");
2649
+ if (ports) this.addPorts(el, n);
2650
+ }
2651
+ /** Start and finish link handles (a milestone has one). */
2652
+ addPorts(el, n) {
2653
+ const edges = n.item.kind === "milestone" ? ["finish"] : ["start", "finish"];
2654
+ for (const edge of edges) {
2655
+ el.append(h(this.doc, "span", `tnc-port tnc-port-${edge}`, { "aria-hidden": "true", "data-edge": edge, title: "Drag to link" }));
2656
+ }
2657
+ }
2658
+ /* ============================================================== popover */
2659
+ refreshMore() {
2660
+ const p = this.popover;
2661
+ if (!p) return;
2662
+ 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));
2663
+ p.pool.begin();
2664
+ for (const n of items) {
2665
+ 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 });
2666
+ }
2667
+ p.pool.end();
2668
+ }
2669
+ closeMore(restoreFocus = false) {
2670
+ const p = this.popover;
2671
+ if (!p) return;
2672
+ this.popover = null;
2673
+ p.pool.clear();
2674
+ p.el.remove();
2675
+ if (restoreFocus && p.anchor.isConnected) p.anchor.focus();
2676
+ }
2677
+ /* =========================================================== interaction */
2678
+ bindEvents() {
2679
+ const r = this.root;
2680
+ r.addEventListener("pointerdown", this.onPointerDown);
2681
+ r.addEventListener("click", this.onClickCapture, true);
2682
+ r.addEventListener("click", this.onClick);
2683
+ r.addEventListener("dblclick", this.onDblClick);
2684
+ r.addEventListener("contextmenu", this.onContextMenu);
2685
+ r.addEventListener("mouseover", this.onMouseOver);
2686
+ r.addEventListener("mouseout", this.onMouseOut);
2687
+ r.addEventListener("keydown", this.onKeyDown);
2688
+ r.addEventListener("dragover", this.onNativeDragOver);
2689
+ r.addEventListener("dragleave", this.onNativeDragLeave);
2690
+ r.addEventListener("drop", this.onNativeDrop);
2691
+ r.addEventListener("touchmove", this.onTouchMove, { passive: false });
2692
+ this.doc.addEventListener("pointerdown", this.onDocPointerDown, true);
2693
+ }
2694
+ unbindEvents() {
2695
+ const r = this.root;
2696
+ r.removeEventListener("pointerdown", this.onPointerDown);
2697
+ r.removeEventListener("click", this.onClickCapture, true);
2698
+ r.removeEventListener("click", this.onClick);
2699
+ r.removeEventListener("dblclick", this.onDblClick);
2700
+ r.removeEventListener("contextmenu", this.onContextMenu);
2701
+ r.removeEventListener("mouseover", this.onMouseOver);
2702
+ r.removeEventListener("mouseout", this.onMouseOut);
2703
+ r.removeEventListener("keydown", this.onKeyDown);
2704
+ r.removeEventListener("dragover", this.onNativeDragOver);
2705
+ r.removeEventListener("dragleave", this.onNativeDragLeave);
2706
+ r.removeEventListener("drop", this.onNativeDrop);
2707
+ r.removeEventListener("touchmove", this.onTouchMove);
2708
+ this.doc.removeEventListener("pointerdown", this.onDocPointerDown, true);
2709
+ this.listenDoc(false);
2710
+ }
2711
+ listenDoc(on) {
2712
+ const d = this.doc;
2713
+ const fn = on ? d.addEventListener.bind(d) : d.removeEventListener.bind(d);
2714
+ fn("pointermove", this.onPointerMove);
2715
+ fn("pointerup", this.onPointerUp);
2716
+ fn("pointercancel", this.onPointerCancel);
2717
+ fn("keydown", this.onDocKeyDown, true);
2718
+ }
2719
+ itemElFrom(target) {
2720
+ const el = target?.closest?.(".tnc-item");
2721
+ return el && this.root.contains(el) && this.meta.has(el) ? el : null;
2722
+ }
2723
+ onDocPointerDown = (ev) => {
2724
+ const t = ev.target;
2725
+ if (this.popover && !this.popover.el.contains(t) && !this.popover.anchor.contains(t)) this.closeMore();
2726
+ if (this.selection && !this.root.contains(t)) this.unselect();
2727
+ };
2728
+ onPointerDown = (ev) => {
2729
+ if (ev.button !== 0 || this.session) return;
2730
+ const target = ev.target;
2731
+ if (target.closest("button, a, input, select, textarea, .tnc-toolbar, .tnc-port, [data-tnc-no-drag]")) {
2732
+ if (!target.closest(".tnc-item")) return;
2733
+ if (target.closest(".tnc-port, [data-tnc-no-drag], input, select, textarea")) return;
2734
+ }
2735
+ const itemEl = this.itemElFrom(target);
2736
+ let mode = null;
2737
+ let origin = null;
2738
+ if (itemEl) {
2739
+ const meta = this.meta.get(itemEl);
2740
+ origin = this.store.get(meta.n.id) ?? null;
2741
+ if (!origin || origin.id === EXTERNAL_ID) return;
2742
+ if (target.closest(".tnc-resize-end")) mode = this.isEditable(origin, "duration") ? "resize-end" : null;
2743
+ else if (target.closest(".tnc-resize-start")) mode = this.isEditable(origin, "duration") ? "resize-start" : null;
2744
+ else mode = this.isEditable(origin, "start") ? "move" : null;
2745
+ if (!mode) return;
2746
+ } else {
2747
+ if (!this.opts.selectable || !this.current) return;
2748
+ if (target.closest(".tnc-popover, .tnc-more")) return;
2749
+ const hit = this.current.hit(ev.clientX, ev.clientY);
2750
+ if (!hit || hit.kind === "timeline" && !hit.overTrack) return;
2751
+ mode = "select";
2752
+ }
2753
+ const touch = ev.pointerType === "touch";
2754
+ const s = {
2755
+ mode,
2756
+ pointerId: ev.pointerId,
2757
+ pointerType: ev.pointerType,
2758
+ x0: ev.clientX,
2759
+ y0: ev.clientY,
2760
+ active: false,
2761
+ origin,
2762
+ grab: this.current?.hit(ev.clientX, ev.clientY) ?? null,
2763
+ proposal: null,
2764
+ denied: false,
2765
+ timer: null,
2766
+ lastEvent: ev
2767
+ };
2768
+ this.session = s;
2769
+ if (touch && itemEl?.classList.contains("is-draggable")) {
2770
+ const el = itemEl.closest(".tnc-tg-scroll, .tnc-dg-scroll, .tnc-tl-scroll, .tnc-list-scroll, .tnc-year-scroll");
2771
+ if (el) s.pan = { el, left: el.scrollLeft, top: el.scrollTop, active: false };
2772
+ }
2773
+ if (touch) {
2774
+ s.timer = this.win.setTimeout(() => {
2775
+ if (this.session !== s) return;
2776
+ this.activate(s);
2777
+ navigator.vibrate?.(10);
2778
+ }, this.opts.longPressDelay);
2779
+ } else if (mode !== "select") {
2780
+ ev.preventDefault();
2781
+ }
2782
+ this.listenDoc(true);
2783
+ };
2784
+ activate(s) {
2785
+ s.active = true;
2786
+ if (s.timer !== null) this.win.clearTimeout(s.timer);
2787
+ s.timer = null;
2788
+ if (s.origin) {
2789
+ this.draggingId = s.origin.id;
2790
+ for (const el of this.elementsFor(s.origin.id)) el.classList.add("is-dragging");
2791
+ }
2792
+ this.closeMore();
2793
+ if (s.mode !== "select") this.unselectSilently();
2794
+ this.root.classList.add("tnc-dragging", `tnc-dragging-${s.mode}`);
2795
+ if (s.lastEvent) this.track(s, s.lastEvent);
2796
+ }
2797
+ onTouchMove = (ev) => {
2798
+ if (this.session?.active) ev.preventDefault();
2799
+ };
2800
+ onPointerMove = (ev) => {
2801
+ const s = this.session;
2802
+ if (!s || ev.pointerId !== s.pointerId) return;
2803
+ s.lastEvent = ev;
2804
+ if (!s.active) {
2805
+ const dist = Math.hypot(ev.clientX - s.x0, ev.clientY - s.y0);
2806
+ if (s.pointerType === "touch") {
2807
+ const pan = s.pan;
2808
+ if (pan && (pan.active || dist > 8)) {
2809
+ if (!pan.active && s.timer !== null) this.win.clearTimeout(s.timer);
2810
+ s.timer = null;
2811
+ pan.active = true;
2812
+ pan.el.scrollLeft = pan.left - (ev.clientX - s.x0);
2813
+ pan.el.scrollTop = pan.top - (ev.clientY - s.y0);
2814
+ } else if (dist > 8) this.endSession(null, true);
2815
+ return;
2816
+ }
2817
+ if (dist < 4) return;
2818
+ this.activate(s);
2819
+ return;
2820
+ }
2821
+ ev.preventDefault();
2822
+ this.track(s, ev);
2823
+ };
2824
+ onPointerUp = (ev) => {
2825
+ const s = this.session;
2826
+ if (!s || ev.pointerId !== s.pointerId) return;
2827
+ if (!s.active) {
2828
+ this.endSession(null, true);
2829
+ if (s.pan?.active) {
2830
+ this.suppressNextClick();
2831
+ return;
2832
+ }
2833
+ if (s.mode === "select" && s.grab) this.clickSlot(s.grab, ev, s.pointerType === "touch");
2834
+ return;
2835
+ }
2836
+ this.suppressNextClick();
2837
+ this.endSession(ev, false);
2838
+ };
2839
+ onPointerCancel = (ev) => {
2840
+ if (this.session && ev.pointerId === this.session.pointerId) this.endSession(null, true);
2841
+ };
2842
+ onDocKeyDown = (ev) => {
2843
+ if (ev.key === "Escape" && this.session) {
2844
+ ev.preventDefault();
2845
+ ev.stopPropagation();
2846
+ this.endSession(null, true);
2847
+ }
2848
+ };
2849
+ /** Proposal for the current pointer position. */
2850
+ track(s, ev) {
2851
+ const hit = this.current?.hit(ev.clientX, ev.clientY) ?? null;
2852
+ if (s.mode === "external") {
2853
+ this.trackExternal(s, hit);
2854
+ return;
2855
+ }
2856
+ if (!s.grab) s.grab = hit;
2857
+ if (!hit || !s.grab) return;
2858
+ if (s.mode === "select") {
2859
+ const sel = this.selectionFor(s.grab, hit);
2860
+ if (!sel) return;
2861
+ this.selection = sel;
2862
+ this.current?.showSelection(sel);
2863
+ return;
2864
+ }
2865
+ const origin = s.origin;
2866
+ const p = s.mode === "move" ? this.moveProposal(origin, s.grab, hit) : this.resizeProposal(origin, hit, s.mode === "resize-start");
2867
+ if (!p) return;
2868
+ 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;
2869
+ s.denied = !allowed;
2870
+ this.root.classList.toggle("tnc-drop-denied", !allowed);
2871
+ if (!allowed || sameProposal(p, s.proposal)) return;
2872
+ s.proposal = p;
2873
+ this.dragRow = p.rowId !== void 0 ? { id: origin.id, rowId: p.rowId } : null;
2874
+ this.applyChange(this.store.upsert([this.withProposal(origin, p)]));
2875
+ }
2876
+ trackExternal(s, raw) {
2877
+ const src = s.source;
2878
+ const hit = raw?.kind === "timeline" && raw.area !== "track" && raw.area !== "header" ? null : raw;
2879
+ s.hit = hit;
2880
+ if (!hit) {
2881
+ if (s.proposal) {
2882
+ s.proposal = null;
2883
+ this.applyChange(this.store.remove([EXTERNAL_ID]));
2884
+ }
2885
+ this.unselectSilently();
2886
+ return;
2887
+ }
2888
+ const dur = src.durationMinutes ?? this.opts.defaultTimedMinutes;
2889
+ let p;
2890
+ if (hit.kind === "time" || hit.kind === "timeline" && this.timelineSnap() < MINUTES_PER_DAY) {
2891
+ const snap = hit.kind === "time" ? this.slot.snap : this.timelineSnap();
2892
+ const startCivil = Math.floor((hit.day * MINUTES_PER_DAY + hit.minute) / snap) * snap;
2893
+ const startMs = this.civilToMs(startCivil);
2894
+ const endMs = this.civilToMs(startCivil + dur);
2895
+ p = { startMs, endMs, allDay: false, startDay: dayOf(startMs, this.zone), endDay: dayOf(endMs - 1, this.zone) + 1 };
2896
+ } else {
2897
+ const day = hit.day;
2898
+ p = this.dayProposal(day, day + 1);
2899
+ }
2900
+ if (hit.kind === "timeline" && hit.overTrack && hit.rowId !== null) p.rowId = hit.rowId;
2901
+ 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;
2902
+ s.denied = !allowed;
2903
+ this.root.classList.toggle("tnc-drop-denied", !allowed);
2904
+ if (sameProposal(p, s.proposal)) return;
2905
+ s.proposal = p;
2906
+ if (hit.kind === "timeline" && p.rowId === void 0) {
2907
+ this.selection = { ...p };
2908
+ this.current?.showSelection(this.selection);
2909
+ } else this.unselectSilently();
2910
+ this.dragRow = p.rowId !== void 0 ? { id: EXTERNAL_ID, rowId: p.rowId } : null;
2911
+ this.applyChange(this.store.upsert([this.externalItem(src, p)]));
2912
+ }
2913
+ externalItem(src, p) {
2914
+ return {
2915
+ id: EXTERNAL_ID,
2916
+ title: src.title,
2917
+ kind: src.kind,
2918
+ color: src.color,
2919
+ start: p.allDay ? formatDay(p.startDay) : p.startMs,
2920
+ end: p.allDay ? formatDay(p.endDay) : p.endMs,
2921
+ allDay: p.allDay,
2922
+ editable: false,
2923
+ data: src.data,
2924
+ rev: `${p.startMs}:${p.endMs}:${p.allDay ? 1 : 0}:${p.rowId ?? ""}`
2925
+ };
2926
+ }
2927
+ timelineSnap() {
2928
+ return this.opts.timeline?.scale?.snapMinutes ?? MINUTES_PER_DAY;
2929
+ }
2930
+ civil(ms) {
2931
+ const w = wallTime(ms, this.zone);
2932
+ return w.day * MINUTES_PER_DAY + w.minute;
2933
+ }
2934
+ civilToMs(c) {
2935
+ const day = Math.floor(c / MINUTES_PER_DAY);
2936
+ return toInstant(day, c - day * MINUTES_PER_DAY, this.zone);
2937
+ }
2938
+ hitCivil(hit) {
2939
+ return hit.kind === "time" || hit.kind === "timeline" ? hit.day * MINUTES_PER_DAY + hit.minute : hit.day * MINUTES_PER_DAY;
2940
+ }
2941
+ dayProposal(startDay, endDay) {
2942
+ return {
2943
+ startMs: toInstant(startDay, 0, this.zone),
2944
+ endMs: toInstant(endDay, 0, this.zone),
2945
+ allDay: true,
2946
+ startDay,
2947
+ endDay
2948
+ };
2949
+ }
2950
+ timedProposal(startMs, endMs) {
2951
+ return { startMs, endMs, allDay: false, startDay: dayOf(startMs, this.zone), endDay: dayOf(Math.max(startMs, endMs - 1), this.zone) + 1 };
2952
+ }
2953
+ moveProposal(o, grab, hit) {
2954
+ const days = o.endDay - o.startDay;
2955
+ if (hit.kind === "timeline") {
2956
+ const snap2 = o.allDay ? Math.max(MINUTES_PER_DAY, this.timelineSnap()) : this.timelineSnap();
2957
+ const delta2 = Math.round((this.hitCivil(hit) - this.hitCivil(grab)) / snap2) * snap2;
2958
+ let p;
2959
+ if (o.allDay) {
2960
+ const dd = Math.round(delta2 / MINUTES_PER_DAY);
2961
+ p = this.dayProposal(o.startDay + dd, o.endDay + dd);
2962
+ } else {
2963
+ const startMs2 = this.civilToMs(this.civil(o.startMs) + delta2);
2964
+ p = this.timedProposal(startMs2, startMs2 + (o.endMs - o.startMs));
2965
+ }
2966
+ p.rowId = (hit.overTrack ? hit.rowId : null) ?? this.dragRow?.rowId ?? this.opts.timeline?.rowOf(o.item) ?? void 0;
2967
+ return p;
2968
+ }
2969
+ if (hit.kind === "allDay") {
2970
+ if (o.allDay) {
2971
+ const dd = hit.day - (grab.kind === "allDay" || grab.kind === "day" ? grab.day : hit.day);
2972
+ return this.dayProposal(o.startDay + dd, o.endDay + dd);
2973
+ }
2974
+ return this.dayProposal(hit.day, hit.day + this.opts.defaultAllDayDays);
2975
+ }
2976
+ if (hit.kind === "day") {
2977
+ const dd = hit.day - (grab.kind === "day" ? grab.day : hit.day);
2978
+ if (o.allDay) return this.dayProposal(o.startDay + dd, o.startDay + dd + days);
2979
+ const startMs2 = this.civilToMs(this.civil(o.startMs) + dd * MINUTES_PER_DAY);
2980
+ const endMs = this.civilToMs(this.civil(o.endMs) + dd * MINUTES_PER_DAY);
2981
+ return this.timedProposal(startMs2, endMs);
2982
+ }
2983
+ const snap = this.slot.snap;
2984
+ if (o.allDay || grab.kind !== "time") {
2985
+ const c = Math.floor((hit.day * MINUTES_PER_DAY + hit.minute) / snap) * snap;
2986
+ const startMs2 = this.civilToMs(c);
2987
+ return this.timedProposal(startMs2, this.civilToMs(c + this.opts.defaultTimedMinutes));
2988
+ }
2989
+ const delta = Math.round((this.hitCivil(hit) - this.hitCivil(grab)) / snap) * snap;
2990
+ const startCivil = this.civil(o.startMs) + delta;
2991
+ const startMs = this.civilToMs(startCivil);
2992
+ return this.timedProposal(startMs, startMs + (o.endMs - o.startMs));
2993
+ }
2994
+ resizeProposal(o, hit, fromStart) {
2995
+ if (o.allDay || hit.kind === "allDay" || hit.kind === "day") {
2996
+ if (hit.kind === "time") return null;
2997
+ const pointerDay = hit.kind === "timeline" ? Math.round(this.hitCivil(hit) / MINUTES_PER_DAY) : fromStart ? hit.day : hit.day + 1;
2998
+ if (!o.allDay) {
2999
+ const end = wallTime(o.endMs - 1, this.zone);
3000
+ const dd = (fromStart ? 0 : pointerDay - 1) - end.day;
3001
+ if (fromStart) return null;
3002
+ const endMs = Math.max(o.startMs + 6e4 * this.slot.snap, this.civilToMs(this.civil(o.endMs) + dd * MINUTES_PER_DAY));
3003
+ return this.timedProposal(o.startMs, endMs);
3004
+ }
3005
+ const p2 = fromStart ? this.dayProposal(Math.min(o.endDay - 1, pointerDay), o.endDay) : this.dayProposal(o.startDay, Math.max(o.startDay + 1, pointerDay));
3006
+ if (hit.kind === "timeline") p2.rowId = this.opts.timeline?.rowOf(o.item) ?? void 0;
3007
+ return p2;
3008
+ }
3009
+ const snap = hit.kind === "timeline" ? this.timelineSnap() : this.slot.snap;
3010
+ const c = Math.round(this.hitCivil(hit) / snap) * snap;
3011
+ const ms = this.civilToMs(c);
3012
+ const min = snap * 6e4;
3013
+ const p = fromStart ? this.timedProposal(Math.min(ms, o.endMs - min), o.endMs) : this.timedProposal(o.startMs, Math.max(ms, o.startMs + min));
3014
+ if (hit.kind === "timeline") p.rowId = this.opts.timeline?.rowOf(o.item) ?? void 0;
3015
+ return p;
3016
+ }
3017
+ withProposal(o, p) {
3018
+ return {
3019
+ ...o.item,
3020
+ start: p.allDay ? formatDay(p.startDay) : p.startMs,
3021
+ end: p.allDay ? formatDay(p.endDay) : p.endMs,
3022
+ allDay: p.allDay,
3023
+ // Unique per proposal, so the store redraws the item wherever the pointer takes it.
3024
+ rev: `drag:${p.startMs}:${p.endMs}:${p.allDay ? 1 : 0}:${p.rowId ?? ""}:${o.sig}`
3025
+ };
3026
+ }
3027
+ selectionFor(a, b) {
3028
+ if (a.kind !== b.kind) {
3029
+ if (!(a.kind === "allDay" && b.kind === "time") && !(a.kind === "time" && b.kind === "allDay")) return null;
3030
+ b = a.kind === "allDay" ? { kind: "allDay", day: b.day } : { kind: "time", day: b.day, minute: a.minute };
3031
+ }
3032
+ if (a.kind === "time" || a.kind === "timeline" && this.timelineSnap() < MINUTES_PER_DAY) {
3033
+ const snap = a.kind === "time" ? this.slot.snap : this.timelineSnap();
3034
+ const ca = Math.floor(this.hitCivil(a) / snap) * snap;
3035
+ const cb = Math.floor(this.hitCivil(b) / snap) * snap;
3036
+ const startMs = this.civilToMs(Math.min(ca, cb));
3037
+ const endMs = this.civilToMs(Math.max(ca, cb) + snap);
3038
+ const sel2 = { ...this.timedProposal(startMs, endMs) };
3039
+ if (a.kind === "timeline" && a.overTrack && a.rowId !== null) sel2.rowId = a.rowId;
3040
+ return sel2;
3041
+ }
3042
+ const da = a.day;
3043
+ const db = b.day;
3044
+ const sel = this.dayProposal(Math.min(da, db), Math.max(da, db) + 1);
3045
+ if (a.kind === "timeline" && a.overTrack && a.rowId !== null) sel.rowId = a.rowId;
3046
+ return sel;
3047
+ }
3048
+ clickSlot(hit, ev, touch) {
3049
+ if (hit.kind === "timeline" && !hit.overTrack) return;
3050
+ const day = hit.day;
3051
+ if (this.opts.onDateClick) {
3052
+ let minute = null;
3053
+ if (hit.kind === "time") minute = Math.floor(hit.minute / this.slot.duration) * this.slot.duration;
3054
+ else if (hit.kind === "timeline" && this.timelineSnap() < MINUTES_PER_DAY) minute = Math.floor(hit.minute / this.timelineSnap()) * this.timelineSnap();
3055
+ const civil = day * MINUTES_PER_DAY + (minute ?? 0);
3056
+ const at = Math.floor(civil / MINUTES_PER_DAY);
3057
+ const startStr = minute === null ? formatDay(day) : formatFloating(this.civilToMs(civil), this.zone);
3058
+ this.opts.onDateClick({
3059
+ ...this.dayArg(minute === null ? day : at),
3060
+ minute: minute === null ? null : civil - at * MINUTES_PER_DAY,
3061
+ allDay: minute === null,
3062
+ startStr,
3063
+ jsEvent: ev
3064
+ });
3065
+ }
3066
+ if (touch || !this.opts.selectable) return;
3067
+ const sel = hit.kind === "time" ? (() => {
3068
+ const c = Math.floor(this.hitCivil(hit) / this.slot.duration) * this.slot.duration;
3069
+ return this.timedProposal(this.civilToMs(c), this.civilToMs(c + this.slot.duration));
3070
+ })() : this.selectionFor(hit, hit);
3071
+ if (!sel) return;
3072
+ this.selection = sel;
3073
+ this.current?.showSelection(sel);
3074
+ this.fireSelect(sel, ev);
3075
+ }
3076
+ fireSelect(sel, ev) {
3077
+ this.opts.onSelect?.({
3078
+ start: new Date(sel.startMs),
3079
+ end: new Date(sel.endMs),
3080
+ allDay: sel.allDay,
3081
+ startStr: sel.allDay ? formatDay(sel.startDay) : formatFloating(sel.startMs, this.zone),
3082
+ endStr: sel.allDay ? formatDay(sel.endDay) : formatFloating(sel.endMs, this.zone),
3083
+ jsEvent: ev,
3084
+ view: this.view,
3085
+ rowId: sel.rowId
3086
+ });
3087
+ }
3088
+ unselectSilently() {
3089
+ this.selection = null;
3090
+ this.current?.showSelection(null);
3091
+ }
3092
+ /** Ends the current pointer session, committing unless `cancel`. */
3093
+ endSession(ev, cancel) {
3094
+ const s = this.session;
3095
+ if (!s) return;
3096
+ this.session = null;
3097
+ if (s.timer !== null) this.win.clearTimeout(s.timer);
3098
+ this.listenDoc(false);
3099
+ this.root.classList.remove(
3100
+ "tnc-dragging",
3101
+ "tnc-drop-denied",
3102
+ "tnc-dragging-move",
3103
+ "tnc-dragging-resize-end",
3104
+ "tnc-dragging-resize-start",
3105
+ "tnc-dragging-select",
3106
+ "tnc-dragging-external"
3107
+ );
3108
+ const draggedId = this.draggingId;
3109
+ this.draggingId = null;
3110
+ this.dragRow = null;
3111
+ if (draggedId) for (const el of this.elementsFor(draggedId)) el.classList.remove("is-dragging");
3112
+ if (!s.active) return;
3113
+ if (s.mode === "select") {
3114
+ if (cancel) this.unselectSilently();
3115
+ else if (this.selection) this.fireSelect(this.selection, ev);
3116
+ return;
3117
+ }
3118
+ if (s.mode === "external") {
3119
+ const p2 = s.proposal;
3120
+ if (p2) this.applyChange(this.store.remove([EXTERNAL_ID]));
3121
+ this.unselectSilently();
3122
+ const dropped = !cancel && !!p2 && !s.denied && !!ev;
3123
+ const hit = s.hit ?? null;
3124
+ const ended = () => {
3125
+ if (s.source) this.opts.onExternalDragEnd?.({ source: s.source, dropped, hit, jsEvent: ev });
3126
+ };
3127
+ if (!dropped || !p2 || !ev) {
3128
+ ended();
3129
+ return;
3130
+ }
3131
+ this.flush();
3132
+ this.opts.onExternalDrop?.({
3133
+ start: new Date(p2.startMs),
3134
+ end: new Date(p2.endMs),
3135
+ allDay: p2.allDay,
3136
+ startStr: p2.allDay ? formatDay(p2.startDay) : formatFloating(p2.startMs, this.zone),
3137
+ endStr: p2.allDay ? formatDay(p2.endDay) : formatFloating(p2.endMs, this.zone),
3138
+ jsEvent: ev,
3139
+ view: this.view,
3140
+ rowId: p2.rowId,
3141
+ source: s.source ?? null,
3142
+ hit,
3143
+ dataTransfer: null
3144
+ });
3145
+ ended();
3146
+ return;
3147
+ }
3148
+ const origin = s.origin;
3149
+ const p = s.proposal;
3150
+ const revert = () => {
3151
+ const cur = this.store.get(origin.id);
3152
+ if (cur && cur.sig !== origin.sig) this.applyChange(this.store.upsert([origin.item]));
3153
+ this.flush();
3154
+ };
3155
+ if (cancel || s.denied || !p) {
3156
+ revert();
3157
+ return;
3158
+ }
3159
+ 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);
3160
+ if (!changed) {
3161
+ revert();
3162
+ return;
3163
+ }
3164
+ this.flush();
3165
+ this.commit(s.mode === "move" ? "drop" : "resize", origin, p, ev, revert);
3166
+ }
3167
+ commit(kind, origin, p, ev, revert) {
3168
+ const arg = {
3169
+ item: origin.item,
3170
+ start: new Date(p.startMs),
3171
+ end: new Date(p.endMs),
3172
+ allDay: p.allDay,
3173
+ startStr: p.allDay ? formatDay(p.startDay) : formatFloating(p.startMs, this.zone),
3174
+ endStr: p.allDay ? formatDay(p.endDay) : formatFloating(p.endMs, this.zone),
3175
+ oldStart: new Date(origin.startMs),
3176
+ oldEnd: new Date(origin.endMs),
3177
+ oldAllDay: origin.allDay,
3178
+ delta: p.startMs - origin.startMs,
3179
+ rowId: p.rowId,
3180
+ jsEvent: ev,
3181
+ source: ev && "key" in ev ? "keyboard" : "pointer",
3182
+ view: this.view,
3183
+ revert
3184
+ };
3185
+ const handler = kind === "drop" ? this.opts.onItemDrop : this.opts.onItemResize;
3186
+ try {
3187
+ const res = handler?.(arg);
3188
+ if (res && typeof res.then === "function") {
3189
+ Promise.resolve(res).catch(() => revert());
3190
+ }
3191
+ } catch (err) {
3192
+ revert();
3193
+ throw err;
3194
+ }
3195
+ }
3196
+ /* --------------------------------------------------------- click / keys */
3197
+ /** Runs in the capture phase so the click ending a drag never reaches content inside the calendar, React portals included. */
3198
+ onClickCapture = (ev) => {
3199
+ if (!this.suppressClick) return;
3200
+ this.suppressClick = false;
3201
+ ev.stopPropagation();
3202
+ ev.preventDefault();
3203
+ };
3204
+ onClick = (ev) => {
3205
+ const el = this.itemElFrom(ev.target);
3206
+ if (!el || ev.target.closest(".tnc-resize, .tnc-port")) return;
3207
+ const meta = this.meta.get(el);
3208
+ if (meta.n.id === EXTERNAL_ID) return;
3209
+ this.opts.onItemClick?.({ ...this.itemArg(el, meta), jsEvent: ev });
3210
+ };
3211
+ onDblClick = (ev) => {
3212
+ const el = this.itemElFrom(ev.target);
3213
+ if (!el) return;
3214
+ const meta = this.meta.get(el);
3215
+ this.opts.onItemDoubleClick?.({ ...this.itemArg(el, meta), jsEvent: ev });
3216
+ };
3217
+ onContextMenu = (ev) => {
3218
+ const el = this.itemElFrom(ev.target);
3219
+ if (!el) return;
3220
+ if (this.session?.active) {
3221
+ ev.preventDefault();
3222
+ return;
3223
+ }
3224
+ if (!this.opts.onItemContextMenu) return;
3225
+ ev.preventDefault();
3226
+ const meta = this.meta.get(el);
3227
+ this.opts.onItemContextMenu({ ...this.itemArg(el, meta), jsEvent: ev });
3228
+ };
3229
+ hoverId = null;
3230
+ onMouseOver = (ev) => {
3231
+ const el = this.itemElFrom(ev.target);
3232
+ const id = el ? this.meta.get(el).n.id : null;
3233
+ if (id === this.hoverId) return;
3234
+ this.leaveHover(ev);
3235
+ if (!el || !id || this.session?.active) return;
3236
+ this.hoverId = id;
3237
+ for (const other of this.elementsFor(id)) other.classList.add("is-hover");
3238
+ this.opts.onItemHover?.({ ...this.itemArg(el, this.meta.get(el)), jsEvent: ev, entering: true });
3239
+ };
3240
+ onMouseOut = (ev) => {
3241
+ const to = ev.relatedTarget;
3242
+ const toEl = to ? this.itemElFrom(to) : null;
3243
+ if (toEl && this.meta.get(toEl)?.n.id === this.hoverId) return;
3244
+ this.leaveHover(ev);
3245
+ };
3246
+ leaveHover(ev) {
3247
+ const id = this.hoverId;
3248
+ if (!id) return;
3249
+ this.hoverId = null;
3250
+ const els = this.elementsFor(id);
3251
+ for (const el2 of els) el2.classList.remove("is-hover");
3252
+ const el = els[0];
3253
+ const meta = el ? this.meta.get(el) : void 0;
3254
+ if (el && meta) this.opts.onItemHover?.({ ...this.itemArg(el, meta), jsEvent: ev, entering: false });
3255
+ }
3256
+ onKeyDown = (ev) => {
3257
+ if (ev.key === "Escape" && this.popover) {
3258
+ ev.preventDefault();
3259
+ ev.stopPropagation();
3260
+ this.closeMore(true);
3261
+ return;
3262
+ }
3263
+ const target = ev.target;
3264
+ if ((ev.key === "Enter" || ev.key === " ") && target.matches(".tnc-ym-day, .tnc-dg-day[tabindex]")) {
3265
+ const day = parseDay(target.dataset.date ?? "");
3266
+ if (day !== null) {
3267
+ ev.preventDefault();
3268
+ this.clickSlot({ kind: "day", day }, ev, true);
3269
+ }
3270
+ return;
3271
+ }
3272
+ const el = this.itemElFrom(ev.target);
3273
+ if (!el) return;
3274
+ const grip = ev.target.closest?.(".tnc-resize[tabindex]");
3275
+ if (grip && el.contains(grip) && (ev.key === "ArrowLeft" || ev.key === "ArrowRight") && !ev.ctrlKey && !ev.metaKey) {
3276
+ ev.preventDefault();
3277
+ this.nudge(this.meta.get(el).n.id, ev, grip.classList.contains("tnc-resize-start") ? "start" : "end");
3278
+ return;
3279
+ }
3280
+ if (ev.target !== el) return;
3281
+ const meta = this.meta.get(el);
3282
+ if (ev.key === "Enter" || ev.key === " ") {
3283
+ ev.preventDefault();
3284
+ this.opts.onItemClick?.({ ...this.itemArg(el, meta), jsEvent: ev });
3285
+ return;
3286
+ }
3287
+ if (ev.key === "ContextMenu" || ev.key === "F10" && ev.shiftKey) {
3288
+ ev.preventDefault();
3289
+ this.opts.onItemContextMenu?.({ ...this.itemArg(el, meta), jsEvent: ev });
3290
+ return;
3291
+ }
3292
+ if (ev.altKey && ev.key.startsWith("Arrow")) {
3293
+ ev.preventDefault();
3294
+ this.nudge(meta.n.id, ev);
3295
+ }
3296
+ };
3297
+ /** Alt+Arrow moves an item; Alt+Shift+Arrow changes its end; arrows on a focused grip move that edge. */
3298
+ nudge(id, ev, edge = null) {
3299
+ const o = this.store.get(id);
3300
+ if (!o) return;
3301
+ const resize = edge !== null || ev.shiftKey;
3302
+ const fromStart = edge === "start";
3303
+ if (!this.isEditable(o, resize ? "duration" : "start")) return;
3304
+ const view = this.opts.view;
3305
+ const timeStep = view === "timeline" ? this.timelineSnap() : this.slot.snap;
3306
+ let dMin = 0;
3307
+ switch (ev.key) {
3308
+ case "ArrowLeft":
3309
+ dMin = view === "timeline" ? -Math.max(timeStep, o.allDay ? MINUTES_PER_DAY : 0) : -MINUTES_PER_DAY;
3310
+ break;
3311
+ case "ArrowRight":
3312
+ dMin = view === "timeline" ? Math.max(timeStep, o.allDay ? MINUTES_PER_DAY : 0) : MINUTES_PER_DAY;
3313
+ break;
3314
+ case "ArrowUp":
3315
+ dMin = view === "month" ? -7 * MINUTES_PER_DAY : o.allDay ? 0 : -timeStep;
3316
+ break;
3317
+ case "ArrowDown":
3318
+ dMin = view === "month" ? 7 * MINUTES_PER_DAY : o.allDay ? 0 : timeStep;
3319
+ break;
3320
+ }
3321
+ if (!dMin) return;
3322
+ let p;
3323
+ if (o.allDay) {
3324
+ const dd = Math.round(dMin / MINUTES_PER_DAY);
3325
+ if (!dd) return;
3326
+ if (fromStart) p = this.dayProposal(Math.min(o.endDay - 1, o.startDay + dd), o.endDay);
3327
+ else if (resize) p = this.dayProposal(o.startDay, Math.max(o.startDay + 1, o.endDay + dd));
3328
+ else p = this.dayProposal(o.startDay + dd, o.endDay + dd);
3329
+ } else if (fromStart) {
3330
+ p = this.timedProposal(Math.min(o.endMs - timeStep * 6e4, this.civilToMs(this.civil(o.startMs) + dMin)), o.endMs);
3331
+ } else if (resize) {
3332
+ p = this.timedProposal(o.startMs, Math.max(o.startMs + timeStep * 6e4, this.civilToMs(this.civil(o.endMs) + dMin)));
3333
+ } else {
3334
+ const startMs = this.civilToMs(this.civil(o.startMs) + dMin);
3335
+ p = this.timedProposal(startMs, startMs + (o.endMs - o.startMs));
3336
+ }
3337
+ const allowed = this.opts.canDrop?.({ item: o.item, start: new Date(p.startMs), end: new Date(p.endMs), allDay: p.allDay }) ?? true;
3338
+ if (!allowed) return;
3339
+ const revert = () => {
3340
+ this.applyChange(this.store.upsert([o.item]));
3341
+ this.flush();
3342
+ };
3343
+ if (p.startMs === o.startMs && p.endMs === o.endMs) return;
3344
+ this.applyChange(this.store.upsert([this.withProposal(o, p)]));
3345
+ this.flush();
3346
+ const next = this.elementsFor(id)[0];
3347
+ const grip = edge ? next?.querySelector(`.tnc-resize-${edge}[tabindex]`) : null;
3348
+ (grip ?? next)?.focus();
3349
+ this.commit(resize ? "resize" : "drop", o, p, ev, revert);
3350
+ }
3351
+ /* ---------------------------------------------------------- native drop */
3352
+ onNativeDragOver = (ev) => {
3353
+ const dt = ev.dataTransfer;
3354
+ if (!dt || !this.opts.acceptNativeDrop?.(dt) || !this.current) return;
3355
+ ev.preventDefault();
3356
+ dt.dropEffect = "copy";
3357
+ const hit = this.current.hit(ev.clientX, ev.clientY);
3358
+ if (!hit || hit.kind === "timeline" && hit.area !== "track" && hit.area !== "header") return;
3359
+ const sel = this.selectionFor(hit, hit);
3360
+ if (!sel) return;
3361
+ this.selection = sel;
3362
+ this.current.showSelection(sel);
3363
+ };
3364
+ onNativeDragLeave = (ev) => {
3365
+ if (ev.relatedTarget && this.root.contains(ev.relatedTarget)) return;
3366
+ this.unselectSilently();
3367
+ };
3368
+ onNativeDrop = (ev) => {
3369
+ const dt = ev.dataTransfer;
3370
+ if (!dt || !this.opts.acceptNativeDrop?.(dt) || !this.current) return;
3371
+ ev.preventDefault();
3372
+ const hit = this.current.hit(ev.clientX, ev.clientY);
3373
+ this.unselectSilently();
3374
+ if (!hit || hit.kind === "timeline" && hit.area !== "track" && hit.area !== "header") return;
3375
+ const sel = this.selectionFor(hit, hit);
3376
+ if (!sel) return;
3377
+ this.opts.onExternalDrop?.({
3378
+ start: new Date(sel.startMs),
3379
+ end: new Date(sel.endMs),
3380
+ allDay: sel.allDay,
3381
+ startStr: sel.allDay ? formatDay(sel.startDay) : formatFloating(sel.startMs, this.zone),
3382
+ endStr: sel.allDay ? formatDay(sel.endDay) : formatFloating(sel.endMs, this.zone),
3383
+ jsEvent: null,
3384
+ view: this.view,
3385
+ rowId: sel.rowId,
3386
+ source: null,
3387
+ hit,
3388
+ dataTransfer: dt
3389
+ });
3390
+ };
3391
+ };
3392
+
3393
+ export {
3394
+ Formatter,
3395
+ localeFirstDay,
3396
+ TIMELINE_UNIT_DAYS,
3397
+ DEFAULT_TEXT,
3398
+ defaultOptions,
3399
+ LAYOUT_KEYS,
3400
+ Calendar
3401
+ };