@tasknotes/calendar 0.0.0-stage → 0.1.1

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