@ninthforge/gantt 0.0.0-stage → 1.0.0-rc.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.
- package/LICENSE.md +9 -0
- package/README.md +82 -3
- package/dist/index.cjs +3279 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +439 -0
- package/dist/index.d.ts +439 -0
- package/dist/index.js +3237 -0
- package/dist/index.js.map +1 -0
- package/package.json +50 -3
package/dist/index.js
ADDED
|
@@ -0,0 +1,3237 @@
|
|
|
1
|
+
// src/Gantt.tsx
|
|
2
|
+
import * as React5 from "react";
|
|
3
|
+
|
|
4
|
+
// src/version.ts
|
|
5
|
+
var VERSION = "1.0.0-rc.1";
|
|
6
|
+
|
|
7
|
+
// src/Gantt.tsx
|
|
8
|
+
import {
|
|
9
|
+
useEffect as useEffect3,
|
|
10
|
+
useLayoutEffect,
|
|
11
|
+
useMemo as useMemo2,
|
|
12
|
+
useRef as useRef3,
|
|
13
|
+
useState as useState4
|
|
14
|
+
} from "react";
|
|
15
|
+
|
|
16
|
+
// ../core/dist/chunk-ZAYD57MJ.js
|
|
17
|
+
var MAX = 80;
|
|
18
|
+
function clean(value) {
|
|
19
|
+
const s = String(value ?? "").replace(/[\u0000-\u001f\u007f]+/g, " ").trim();
|
|
20
|
+
return s.length > MAX ? `${s.slice(0, MAX - 1)}\u2026` : s;
|
|
21
|
+
}
|
|
22
|
+
function envOf(win) {
|
|
23
|
+
if (win) return win;
|
|
24
|
+
return typeof window === "undefined" ? {} : window;
|
|
25
|
+
}
|
|
26
|
+
function ancestors(env) {
|
|
27
|
+
try {
|
|
28
|
+
const list = env.location?.ancestorOrigins;
|
|
29
|
+
return list ? Array.from(list, String) : [];
|
|
30
|
+
} catch {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function hostnameOf(origin) {
|
|
35
|
+
return origin.replace(/^[a-z]+:\/\//i, "").replace(/[:/].*$/, "").toLowerCase();
|
|
36
|
+
}
|
|
37
|
+
var endsWith = (host, suffix) => host === suffix || host.endsWith(`.${suffix}`);
|
|
38
|
+
function detectHost(win) {
|
|
39
|
+
const env = envOf(win);
|
|
40
|
+
const xrm = env.Xrm;
|
|
41
|
+
if (xrm) {
|
|
42
|
+
try {
|
|
43
|
+
if (xrm.Utility?.getPageContext?.()?.input?.pageType === "custom") return "custom page";
|
|
44
|
+
} catch {
|
|
45
|
+
}
|
|
46
|
+
return "model-driven";
|
|
47
|
+
}
|
|
48
|
+
const up = ancestors(env).map(hostnameOf);
|
|
49
|
+
if (up.some((h) => endsWith(h, "powerbi.com") || endsWith(h, "fabric.microsoft.com"))) return "Power BI";
|
|
50
|
+
const host = (env.location?.hostname ?? "").toLowerCase();
|
|
51
|
+
if (endsWith(host, "powerbi.com")) return "Power BI";
|
|
52
|
+
if (up.some((h) => endsWith(h, "dynamics.com"))) return "custom page";
|
|
53
|
+
if (host.startsWith("authoring.") && endsWith(host, "powerapps.com")) return "canvas (Studio)";
|
|
54
|
+
if (endsWith(host, "powerapps.com") || endsWith(host, "powerplatformusercontent.com")) return "canvas";
|
|
55
|
+
return "web page";
|
|
56
|
+
}
|
|
57
|
+
var BRANDS = [
|
|
58
|
+
[/Edg(?:e|A|iOS)?\/(\d+)/, "Edge"],
|
|
59
|
+
[/OPR\/(\d+)/, "Opera"],
|
|
60
|
+
[/Firefox\/(\d+)/, "Firefox"],
|
|
61
|
+
[/FxiOS\/(\d+)/, "Firefox"],
|
|
62
|
+
[/CriOS\/(\d+)/, "Chrome"],
|
|
63
|
+
[/Chrome\/(\d+)/, "Chrome"],
|
|
64
|
+
[/Version\/(\d+)[.\d]* (?:Mobile\/\S+ )?Safari\//, "Safari"]
|
|
65
|
+
];
|
|
66
|
+
var PLATFORMS = [
|
|
67
|
+
[/iPhone|iPad|iPod/, "iOS"],
|
|
68
|
+
[/Android/, "Android"],
|
|
69
|
+
[/Windows/, "Windows"],
|
|
70
|
+
[/Mac OS X|Macintosh/, "macOS"],
|
|
71
|
+
[/CrOS/, "ChromeOS"],
|
|
72
|
+
[/Linux/, "Linux"]
|
|
73
|
+
];
|
|
74
|
+
function describeBrowser(win) {
|
|
75
|
+
const nav = envOf(win).navigator;
|
|
76
|
+
const ua = nav?.userAgent ?? "";
|
|
77
|
+
let name = "";
|
|
78
|
+
for (const [re, brand2] of BRANDS) {
|
|
79
|
+
const m = re.exec(ua);
|
|
80
|
+
if (m) {
|
|
81
|
+
name = `${brand2} ${m[1]}`;
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (!name) {
|
|
86
|
+
const brands = nav?.userAgentData?.brands ?? [];
|
|
87
|
+
const b = brands.find((x) => !/not.?a.?brand|chromium/i.test(x.brand)) ?? brands[0];
|
|
88
|
+
name = b ? `${b.brand.replace("Microsoft ", "")} ${b.version}` : "Unknown browser";
|
|
89
|
+
}
|
|
90
|
+
const platform = PLATFORMS.find(([re]) => re.test(ua))?.[1] ?? nav?.userAgentData?.platform;
|
|
91
|
+
return clean(platform ? `${name} on ${platform}` : name);
|
|
92
|
+
}
|
|
93
|
+
function offsetLabel(minutes) {
|
|
94
|
+
const sign = minutes < 0 ? "\u2212" : "+";
|
|
95
|
+
const abs = Math.abs(minutes);
|
|
96
|
+
return `UTC${sign}${String(Math.floor(abs / 60)).padStart(2, "0")}:${String(abs % 60).padStart(2, "0")}`;
|
|
97
|
+
}
|
|
98
|
+
function licenceOf(l) {
|
|
99
|
+
if (!l) return { state: "not checked" };
|
|
100
|
+
const p = l.payload ?? void 0;
|
|
101
|
+
let state = clean(l.status) || "unknown";
|
|
102
|
+
if ((state === "valid" || state === "unverifiable") && p?.tier === "trial") state = "trial";
|
|
103
|
+
else if (state === "unverifiable") state = "valid (browser cannot verify)";
|
|
104
|
+
const out = { state };
|
|
105
|
+
if (p?.org) out.org = clean(p.org);
|
|
106
|
+
if (p?.exp && /^\d{4}-\d{2}-\d{2}$/.test(p.exp)) out.expires = p.exp;
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
function propertiesOf(p) {
|
|
110
|
+
if (!p) return [];
|
|
111
|
+
if (Array.isArray(p)) return p.map(clean).filter(Boolean);
|
|
112
|
+
return Object.keys(p).map((name) => {
|
|
113
|
+
const column = p[name];
|
|
114
|
+
return column ? `${clean(name)} \u2192 ${clean(column)}` : `${clean(name)} (not mapped)`;
|
|
115
|
+
}).filter(Boolean);
|
|
116
|
+
}
|
|
117
|
+
function collectDiagnostics(input, win) {
|
|
118
|
+
const env = envOf(win);
|
|
119
|
+
const now = input.now ?? /* @__PURE__ */ new Date();
|
|
120
|
+
let locale = clean(env.navigator?.language);
|
|
121
|
+
let zone = "";
|
|
122
|
+
try {
|
|
123
|
+
const opts = Intl.DateTimeFormat().resolvedOptions();
|
|
124
|
+
if (!locale) locale = opts.locale;
|
|
125
|
+
else if (opts.locale && opts.locale !== locale) locale = `${locale} (formats ${opts.locale})`;
|
|
126
|
+
zone = opts.timeZone ?? "";
|
|
127
|
+
} catch {
|
|
128
|
+
}
|
|
129
|
+
const offset = offsetLabel(-now.getTimezoneOffset());
|
|
130
|
+
const extra = Object.keys(input.extra ?? {}).filter((k) => input.extra[k] !== void 0 && input.extra[k] !== null).map((k) => [clean(k), clean(input.extra[k])]);
|
|
131
|
+
const d = {
|
|
132
|
+
product: clean(input.product),
|
|
133
|
+
control: clean(input.control),
|
|
134
|
+
version: clean(input.version),
|
|
135
|
+
licence: licenceOf(input.licence),
|
|
136
|
+
host: input.host ?? detectHost(env),
|
|
137
|
+
browser: describeBrowser(env),
|
|
138
|
+
locale: locale || "unknown",
|
|
139
|
+
timeZone: zone ? `${clean(zone)} (${offset})` : offset,
|
|
140
|
+
properties: propertiesOf(input.properties),
|
|
141
|
+
extra,
|
|
142
|
+
at: now.toISOString().replace(/\.\d{3}Z$/, "Z")
|
|
143
|
+
};
|
|
144
|
+
if (typeof input.rows === "number" && Number.isFinite(input.rows)) d.rows = Math.max(0, Math.round(input.rows));
|
|
145
|
+
return d;
|
|
146
|
+
}
|
|
147
|
+
function formatDiagnostics(d) {
|
|
148
|
+
const lic = [d.licence.state, d.licence.org, d.licence.expires && `expires ${d.licence.expires}`].filter(Boolean).join(" \xB7 ");
|
|
149
|
+
const lines = [
|
|
150
|
+
"Ninth Forge diagnostics",
|
|
151
|
+
`Product: ${d.product} \xB7 ${d.control} ${d.version}`,
|
|
152
|
+
`Licence: ${lic}`,
|
|
153
|
+
`Host: ${d.host}`,
|
|
154
|
+
`Browser: ${d.browser}`,
|
|
155
|
+
`Locale: ${d.locale}`,
|
|
156
|
+
`Time zone: ${d.timeZone}`,
|
|
157
|
+
`Properties: ${d.properties.length ? d.properties.join(", ") : "none mapped"}`
|
|
158
|
+
];
|
|
159
|
+
if (d.rows !== void 0) lines.push(`Rows: ${d.rows}`);
|
|
160
|
+
for (const [k, v] of d.extra) lines.push(`${k}: ${v}`);
|
|
161
|
+
lines.push(`Copied: ${d.at}`);
|
|
162
|
+
return lines.join("\n");
|
|
163
|
+
}
|
|
164
|
+
async function copyText(text, win) {
|
|
165
|
+
const env = win ?? (typeof window === "undefined" ? {} : window);
|
|
166
|
+
const fallback = () => {
|
|
167
|
+
const doc = env.document;
|
|
168
|
+
if (!doc?.body) return false;
|
|
169
|
+
const active = doc.activeElement;
|
|
170
|
+
const area = doc.createElement("textarea");
|
|
171
|
+
area.value = text;
|
|
172
|
+
area.setAttribute("readonly", "");
|
|
173
|
+
area.setAttribute("aria-hidden", "true");
|
|
174
|
+
area.style.cssText = "position:fixed;top:0;left:0;width:1px;height:1px;opacity:0;pointer-events:none";
|
|
175
|
+
doc.body.appendChild(area);
|
|
176
|
+
let ok = false;
|
|
177
|
+
try {
|
|
178
|
+
area.select();
|
|
179
|
+
ok = doc.execCommand("copy");
|
|
180
|
+
} catch {
|
|
181
|
+
ok = false;
|
|
182
|
+
}
|
|
183
|
+
area.remove();
|
|
184
|
+
active?.focus?.();
|
|
185
|
+
return ok;
|
|
186
|
+
};
|
|
187
|
+
const write = env.navigator?.clipboard?.writeText;
|
|
188
|
+
if (write) {
|
|
189
|
+
try {
|
|
190
|
+
await write.call(env.navigator.clipboard, text);
|
|
191
|
+
return true;
|
|
192
|
+
} catch {
|
|
193
|
+
return fallback();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return fallback();
|
|
197
|
+
}
|
|
198
|
+
var DIAGNOSTICS_COPIED = "Diagnostics copied. Paste them into your e-mail to support.";
|
|
199
|
+
var DIAGNOSTICS_NOT_COPIED = "Couldn't copy diagnostics. Your browser blocked the clipboard.";
|
|
200
|
+
async function copyDiagnostics(input, win) {
|
|
201
|
+
const text = formatDiagnostics(collectDiagnostics(input, win));
|
|
202
|
+
const ok = await copyText(text, win);
|
|
203
|
+
return { ok, text, message: ok ? DIAGNOSTICS_COPIED : DIAGNOSTICS_NOT_COPIED };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ../core/dist/index.js
|
|
207
|
+
var DAY_MS = 864e5;
|
|
208
|
+
var ISO = /^(\d{4})-(\d{2})-(\d{2})/;
|
|
209
|
+
var MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
210
|
+
var MONTHS_LONG = [
|
|
211
|
+
"January",
|
|
212
|
+
"February",
|
|
213
|
+
"March",
|
|
214
|
+
"April",
|
|
215
|
+
"May",
|
|
216
|
+
"June",
|
|
217
|
+
"July",
|
|
218
|
+
"August",
|
|
219
|
+
"September",
|
|
220
|
+
"October",
|
|
221
|
+
"November",
|
|
222
|
+
"December"
|
|
223
|
+
];
|
|
224
|
+
var WEEKDAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
|
|
225
|
+
function toDay(iso) {
|
|
226
|
+
if (!iso) return NaN;
|
|
227
|
+
const m = ISO.exec(iso);
|
|
228
|
+
if (!m) return NaN;
|
|
229
|
+
const y = Number(m[1]);
|
|
230
|
+
const mo = Number(m[2]);
|
|
231
|
+
const d = Number(m[3]);
|
|
232
|
+
if (mo < 1 || mo > 12 || d < 1 || d > 31) return NaN;
|
|
233
|
+
return Math.round(Date.UTC(y, mo - 1, d) / DAY_MS);
|
|
234
|
+
}
|
|
235
|
+
function fromDay(day) {
|
|
236
|
+
return new Date(day * DAY_MS).toISOString().slice(0, 10);
|
|
237
|
+
}
|
|
238
|
+
function weekday(day) {
|
|
239
|
+
return ((day + 3) % 7 + 7) % 7;
|
|
240
|
+
}
|
|
241
|
+
function parts(day) {
|
|
242
|
+
const d = new Date(day * DAY_MS);
|
|
243
|
+
return { year: d.getUTCFullYear(), month: d.getUTCMonth(), date: d.getUTCDate() };
|
|
244
|
+
}
|
|
245
|
+
function dayOf(year, month, date = 1) {
|
|
246
|
+
return Math.round(Date.UTC(year, month, date) / DAY_MS);
|
|
247
|
+
}
|
|
248
|
+
function startOfWeek(day) {
|
|
249
|
+
return day - weekday(day);
|
|
250
|
+
}
|
|
251
|
+
function startOfMonth(day) {
|
|
252
|
+
const p = parts(day);
|
|
253
|
+
return dayOf(p.year, p.month, 1);
|
|
254
|
+
}
|
|
255
|
+
function addMonths(day, n) {
|
|
256
|
+
const p = parts(day);
|
|
257
|
+
return dayOf(p.year, p.month + n, 1);
|
|
258
|
+
}
|
|
259
|
+
function startOfQuarter(day) {
|
|
260
|
+
const p = parts(day);
|
|
261
|
+
return dayOf(p.year, Math.floor(p.month / 3) * 3, 1);
|
|
262
|
+
}
|
|
263
|
+
function today(now = /* @__PURE__ */ new Date()) {
|
|
264
|
+
return dayOf(now.getFullYear(), now.getMonth(), now.getDate());
|
|
265
|
+
}
|
|
266
|
+
function formatShort(day) {
|
|
267
|
+
const p = parts(day);
|
|
268
|
+
return `${MONTHS[p.month]} ${p.date}`;
|
|
269
|
+
}
|
|
270
|
+
function formatLong(day) {
|
|
271
|
+
const p = parts(day);
|
|
272
|
+
return `${p.date} ${MONTHS[p.month]} ${p.year}`;
|
|
273
|
+
}
|
|
274
|
+
function isWorkingDay(day, week = "mon-fri") {
|
|
275
|
+
return week === "all" || weekday(day) < 5;
|
|
276
|
+
}
|
|
277
|
+
function workingDaysBetween(start, end, week = "mon-fri") {
|
|
278
|
+
if (Number.isNaN(start) || Number.isNaN(end) || end < start) return 0;
|
|
279
|
+
const total = end - start + 1;
|
|
280
|
+
if (week === "all") return total;
|
|
281
|
+
const full = Math.floor(total / 7);
|
|
282
|
+
let count = full * 5;
|
|
283
|
+
for (let d = start + full * 7; d <= end; d++) if (isWorkingDay(d, week)) count++;
|
|
284
|
+
return count;
|
|
285
|
+
}
|
|
286
|
+
function snapToWorkingDay(day, dir, week = "mon-fri") {
|
|
287
|
+
if (Number.isNaN(day)) return day;
|
|
288
|
+
const step = dir < 0 ? -1 : 1;
|
|
289
|
+
let d = day;
|
|
290
|
+
for (let i = 0; i < 7 && !isWorkingDay(d, week); i++) d += step;
|
|
291
|
+
return d;
|
|
292
|
+
}
|
|
293
|
+
function addWorkingDays(day, n, week = "mon-fri") {
|
|
294
|
+
let d = snapToWorkingDay(day, 1, week);
|
|
295
|
+
if (week === "all") return d + n;
|
|
296
|
+
d += Math.floor(n / 5) * 7;
|
|
297
|
+
for (let left = n % 5; left > 0; ) {
|
|
298
|
+
d++;
|
|
299
|
+
if (isWorkingDay(d, week)) left--;
|
|
300
|
+
}
|
|
301
|
+
return d;
|
|
302
|
+
}
|
|
303
|
+
var ZOOMS = ["week", "month", "quarter"];
|
|
304
|
+
var DAY_WIDTH = { week: 36, month: 9, quarter: 3 };
|
|
305
|
+
function snapStart(day, zoom) {
|
|
306
|
+
if (zoom === "week") return startOfWeek(day);
|
|
307
|
+
if (zoom === "month") return startOfMonth(day);
|
|
308
|
+
return startOfQuarter(day);
|
|
309
|
+
}
|
|
310
|
+
function snapEnd(day, zoom) {
|
|
311
|
+
if (zoom === "week") return startOfWeek(day) + 7;
|
|
312
|
+
if (zoom === "month") return addMonths(day, 1);
|
|
313
|
+
return addMonths(startOfQuarter(day), 3);
|
|
314
|
+
}
|
|
315
|
+
function createTimeScale({ from, to, zoom, padDays, minWidth = 0 }) {
|
|
316
|
+
const pad = padDays ?? (zoom === "week" ? 3 : zoom === "month" ? 10 : 30);
|
|
317
|
+
const startDay = snapStart(from, zoom);
|
|
318
|
+
const dayWidth = DAY_WIDTH[zoom];
|
|
319
|
+
let endDay = snapEnd(Math.max(to, from) + pad, zoom);
|
|
320
|
+
for (let guard = 0; (endDay - startDay) * dayWidth < minWidth && guard < 120; guard++) {
|
|
321
|
+
endDay = snapEnd(endDay, zoom);
|
|
322
|
+
}
|
|
323
|
+
const x = (day) => (day - startDay) * dayWidth;
|
|
324
|
+
const major = [];
|
|
325
|
+
const minor = [];
|
|
326
|
+
const weekends = [];
|
|
327
|
+
if (zoom === "quarter") {
|
|
328
|
+
for (let d = startDay; d < endDay; d = addMonths(d, 3)) {
|
|
329
|
+
const p = parts(d);
|
|
330
|
+
const next2 = addMonths(d, 3);
|
|
331
|
+
major.push({ day: d, x: x(d), width: (next2 - d) * dayWidth, label: `Q${Math.floor(p.month / 3) + 1} ${p.year}` });
|
|
332
|
+
}
|
|
333
|
+
for (let d = startDay; d < endDay; d = addMonths(d, 1)) {
|
|
334
|
+
const next2 = addMonths(d, 1);
|
|
335
|
+
minor.push({ day: d, x: x(d), width: (next2 - d) * dayWidth, label: MONTHS[parts(d).month] ?? "" });
|
|
336
|
+
}
|
|
337
|
+
} else {
|
|
338
|
+
let first = true;
|
|
339
|
+
for (let d = startOfMonth(startDay); d < endDay; d = addMonths(d, 1)) {
|
|
340
|
+
const next2 = addMonths(d, 1);
|
|
341
|
+
const left = Math.max(d, startDay);
|
|
342
|
+
const p = parts(d);
|
|
343
|
+
const label = first || p.month === 0 ? `${MONTHS_LONG[p.month]} ${p.year}` : `${MONTHS_LONG[p.month]}`;
|
|
344
|
+
major.push({ day: left, x: x(left), width: (Math.min(next2, endDay) - left) * dayWidth, label });
|
|
345
|
+
first = false;
|
|
346
|
+
}
|
|
347
|
+
if (zoom === "week") {
|
|
348
|
+
for (let d = startDay; d < endDay; d++) {
|
|
349
|
+
minor.push({ day: d, x: x(d), width: dayWidth, label: String(parts(d).date) });
|
|
350
|
+
if (weekday(d) === 5) weekends.push({ x: x(d), width: dayWidth * 2 });
|
|
351
|
+
}
|
|
352
|
+
} else {
|
|
353
|
+
for (let d = startOfWeek(startDay); d < endDay; d += 7) {
|
|
354
|
+
if (d < startDay) continue;
|
|
355
|
+
minor.push({ day: d, x: x(d), width: 7 * dayWidth, label: String(parts(d).date) });
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return {
|
|
360
|
+
zoom,
|
|
361
|
+
startDay,
|
|
362
|
+
endDay,
|
|
363
|
+
dayWidth,
|
|
364
|
+
width: (endDay - startDay) * dayWidth,
|
|
365
|
+
x,
|
|
366
|
+
dayAt: (px) => startDay + Math.floor(px / dayWidth),
|
|
367
|
+
major,
|
|
368
|
+
minor,
|
|
369
|
+
weekends
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
function escapeHtml(s) {
|
|
373
|
+
return s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
374
|
+
}
|
|
375
|
+
function exportFileName(title, isoDay, ext = "png", fallback = "plan") {
|
|
376
|
+
const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || fallback;
|
|
377
|
+
return `${slug}-${isoDay}.${ext}`;
|
|
378
|
+
}
|
|
379
|
+
function printDocument(html, doc = document) {
|
|
380
|
+
const frame = doc.createElement("iframe");
|
|
381
|
+
frame.setAttribute("aria-hidden", "true");
|
|
382
|
+
frame.setAttribute("tabindex", "-1");
|
|
383
|
+
frame.style.cssText = "position:fixed;right:0;bottom:0;width:0;height:0;border:0;visibility:hidden";
|
|
384
|
+
doc.body.appendChild(frame);
|
|
385
|
+
const win = frame.contentWindow;
|
|
386
|
+
const fdoc = frame.contentDocument;
|
|
387
|
+
if (!win || !fdoc) {
|
|
388
|
+
frame.remove();
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
fdoc.open();
|
|
392
|
+
fdoc.write(html);
|
|
393
|
+
fdoc.close();
|
|
394
|
+
let done = false;
|
|
395
|
+
const cleanup = () => {
|
|
396
|
+
if (done) return;
|
|
397
|
+
done = true;
|
|
398
|
+
frame.remove();
|
|
399
|
+
};
|
|
400
|
+
win.addEventListener("afterprint", () => setTimeout(cleanup, 0));
|
|
401
|
+
setTimeout(cleanup, 6e4);
|
|
402
|
+
setTimeout(() => {
|
|
403
|
+
win.focus();
|
|
404
|
+
win.print();
|
|
405
|
+
}, 50);
|
|
406
|
+
}
|
|
407
|
+
function isMultiSelect(e) {
|
|
408
|
+
return !!(e.ctrlKey || e.metaKey);
|
|
409
|
+
}
|
|
410
|
+
var HOST_DIM = 0.3;
|
|
411
|
+
function isHighlighted(id, highlighted) {
|
|
412
|
+
if (!highlighted) return true;
|
|
413
|
+
if (isSet(highlighted)) return highlighted.size === 0 || highlighted.has(id);
|
|
414
|
+
return highlighted.length === 0 || highlighted.indexOf(id) !== -1;
|
|
415
|
+
}
|
|
416
|
+
function isSet(v) {
|
|
417
|
+
return typeof v.has === "function" && !Array.isArray(v);
|
|
418
|
+
}
|
|
419
|
+
function isContextMenuKey(e) {
|
|
420
|
+
return e.key === "ContextMenu" || e.key === "F10" && !!e.shiftKey;
|
|
421
|
+
}
|
|
422
|
+
var ratio = (drawn, layout) => layout > 0 && drawn > 0 && Number.isFinite(drawn / layout) ? drawn / layout : 1;
|
|
423
|
+
function cssScale(el) {
|
|
424
|
+
if (!el) return { x: 1, y: 1 };
|
|
425
|
+
const r = el.getBoundingClientRect();
|
|
426
|
+
return { x: ratio(r.width, el.offsetWidth), y: ratio(r.height, el.offsetHeight) };
|
|
427
|
+
}
|
|
428
|
+
function localPoint(el, clientX, clientY) {
|
|
429
|
+
if (!el) return { x: 0, y: 0 };
|
|
430
|
+
const r = el.getBoundingClientRect();
|
|
431
|
+
return { x: (clientX - r.left) / ratio(r.width, el.offsetWidth), y: (clientY - r.top) / ratio(r.height, el.offsetHeight) };
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// ../license/dist/chunk-Q7LEK2CU.js
|
|
435
|
+
var PREFIX = "NF1";
|
|
436
|
+
function utf8(s) {
|
|
437
|
+
return new TextEncoder().encode(s);
|
|
438
|
+
}
|
|
439
|
+
function b64urlDecode(s) {
|
|
440
|
+
if (!/^[A-Za-z0-9_-]*$/.test(s)) throw new Error("Invalid base64url");
|
|
441
|
+
const pad = s.length % 4 === 0 ? "" : "=".repeat(4 - s.length % 4);
|
|
442
|
+
const bin = atob(s.replace(/-/g, "+").replace(/_/g, "/") + pad);
|
|
443
|
+
const out = new Uint8Array(bin.length);
|
|
444
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
445
|
+
return out;
|
|
446
|
+
}
|
|
447
|
+
function hexToBytes(hex) {
|
|
448
|
+
if (!/^[0-9a-f]*$/i.test(hex) || hex.length % 2 !== 0) throw new Error("Invalid hex");
|
|
449
|
+
const out = new Uint8Array(hex.length / 2);
|
|
450
|
+
for (let i = 0; i < out.length; i++) out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
451
|
+
return out;
|
|
452
|
+
}
|
|
453
|
+
function subtle() {
|
|
454
|
+
const c = globalThis.crypto;
|
|
455
|
+
return c && c.subtle ? c.subtle : void 0;
|
|
456
|
+
}
|
|
457
|
+
var ED25519 = { name: "Ed25519" };
|
|
458
|
+
|
|
459
|
+
// ../license/dist/index.js
|
|
460
|
+
function normaliseHost(host) {
|
|
461
|
+
return host.trim().toLowerCase().replace(/\.+$/, "");
|
|
462
|
+
}
|
|
463
|
+
function browserHost() {
|
|
464
|
+
const loc = globalThis.location;
|
|
465
|
+
const name = loc && typeof loc.hostname === "string" ? normaliseHost(loc.hostname) : "";
|
|
466
|
+
return name || void 0;
|
|
467
|
+
}
|
|
468
|
+
function hostMatches(host, pattern) {
|
|
469
|
+
const h = normaliseHost(host);
|
|
470
|
+
const p = normaliseHost(pattern);
|
|
471
|
+
if (!h || !p) return false;
|
|
472
|
+
if (p.startsWith("*.")) {
|
|
473
|
+
const suffix = p.slice(1);
|
|
474
|
+
return suffix.length > 1 && h.length > suffix.length && h.endsWith(suffix);
|
|
475
|
+
}
|
|
476
|
+
return h === p;
|
|
477
|
+
}
|
|
478
|
+
var GRACE_DAYS = 14;
|
|
479
|
+
var SUITE_PRODUCTS = ["plan", "charts"];
|
|
480
|
+
var PUBLIC_KEYS = {
|
|
481
|
+
"nf-2027-1": "ad97c33249018681b3b315a303551442552d917d337690cac9b4783e931f88d1"
|
|
482
|
+
};
|
|
483
|
+
var DAY = 864e5;
|
|
484
|
+
function isoToUtcMs(iso) {
|
|
485
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(iso);
|
|
486
|
+
if (!m) return NaN;
|
|
487
|
+
return Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
|
|
488
|
+
}
|
|
489
|
+
function parsePayload(segment) {
|
|
490
|
+
try {
|
|
491
|
+
const json = JSON.parse(new TextDecoder().decode(b64urlDecode(segment)));
|
|
492
|
+
if (!json || typeof json.kid !== "string" || typeof json.exp !== "string") return void 0;
|
|
493
|
+
if (json.v === 1) return json;
|
|
494
|
+
if (json.v === 2 && Array.isArray(json.hosts) && json.hosts.length > 0 && json.hosts.every((h) => typeof h === "string" && h.trim() !== "")) {
|
|
495
|
+
return json;
|
|
496
|
+
}
|
|
497
|
+
} catch {
|
|
498
|
+
}
|
|
499
|
+
return void 0;
|
|
500
|
+
}
|
|
501
|
+
async function verifySignature(pubHex, message, signature) {
|
|
502
|
+
const s = subtle();
|
|
503
|
+
if (!s) return "unsupported";
|
|
504
|
+
let key;
|
|
505
|
+
try {
|
|
506
|
+
key = await s.importKey("raw", hexToBytes(pubHex), ED25519, false, ["verify"]);
|
|
507
|
+
} catch (err) {
|
|
508
|
+
return err?.name === "NotSupportedError" ? "unsupported" : false;
|
|
509
|
+
}
|
|
510
|
+
try {
|
|
511
|
+
return await s.verify(ED25519, key, signature, message);
|
|
512
|
+
} catch {
|
|
513
|
+
return false;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
async function verifyLicense(key, opts) {
|
|
517
|
+
const trimmed = (key ?? "").trim();
|
|
518
|
+
if (!trimmed) return { status: "missing", licensed: false };
|
|
519
|
+
const parts2 = trimmed.split(".");
|
|
520
|
+
if (parts2.length !== 3 || parts2[0] !== PREFIX) return { status: "invalid", licensed: false };
|
|
521
|
+
const [, payloadSeg, sigSeg] = parts2;
|
|
522
|
+
const payload = parsePayload(payloadSeg);
|
|
523
|
+
const keys = opts.publicKeys ?? PUBLIC_KEYS;
|
|
524
|
+
const pubHex = payload ? keys[payload.kid] : void 0;
|
|
525
|
+
if (!payload || !pubHex) return { status: "invalid", licensed: false };
|
|
526
|
+
let sig;
|
|
527
|
+
try {
|
|
528
|
+
sig = b64urlDecode(sigSeg);
|
|
529
|
+
} catch {
|
|
530
|
+
return { status: "invalid", licensed: false };
|
|
531
|
+
}
|
|
532
|
+
const ok = await verifySignature(pubHex, utf8(payloadSeg), sig);
|
|
533
|
+
if (ok === false) return { status: "invalid", licensed: false };
|
|
534
|
+
if (payload.v === 2) {
|
|
535
|
+
const host = opts.host === void 0 ? "" : normaliseHost(opts.host);
|
|
536
|
+
const onHost = host !== "" && (payload.hosts ?? []).some((p) => hostMatches(host, p));
|
|
537
|
+
if (opts.tenantId !== void 0 && opts.tenantId !== null || !onHost) return { status: "wrong-host", licensed: false, payload };
|
|
538
|
+
}
|
|
539
|
+
if (ok === "unsupported") return { status: "unverifiable", licensed: true, payload };
|
|
540
|
+
const suiteCovers = payload.pid === "suite" && SUITE_PRODUCTS.includes(opts.product);
|
|
541
|
+
if (payload.pid !== opts.product && !suiteCovers) {
|
|
542
|
+
return { status: "wrong-product", licensed: false, payload };
|
|
543
|
+
}
|
|
544
|
+
const tenant = typeof opts.tenantId === "string" && opts.tenantId.trim() ? opts.tenantId : void 0;
|
|
545
|
+
if (payload.v === 1 && payload.tid !== "*" && tenant !== void 0 && payload.tid.toLowerCase() !== tenant.toLowerCase()) {
|
|
546
|
+
return { status: "wrong-tenant", licensed: false, payload };
|
|
547
|
+
}
|
|
548
|
+
const now = (opts.now ?? /* @__PURE__ */ new Date()).getTime();
|
|
549
|
+
const expMs = isoToUtcMs(payload.exp) + DAY;
|
|
550
|
+
if (Number.isNaN(expMs)) return { status: "invalid", licensed: false };
|
|
551
|
+
const daysLeft = Math.floor((expMs - now) / DAY);
|
|
552
|
+
if (now > expMs + GRACE_DAYS * DAY) return { status: "expired", licensed: false, payload, daysLeft };
|
|
553
|
+
if (now > expMs) return { status: "grace", licensed: true, payload, daysLeft };
|
|
554
|
+
return { status: "valid", licensed: true, payload, daysLeft };
|
|
555
|
+
}
|
|
556
|
+
var FAILURE_RANK = {
|
|
557
|
+
expired: 0,
|
|
558
|
+
"wrong-tenant": 1,
|
|
559
|
+
"wrong-host": 2,
|
|
560
|
+
"wrong-product": 3,
|
|
561
|
+
invalid: 4,
|
|
562
|
+
missing: 5
|
|
563
|
+
};
|
|
564
|
+
async function verifyLicenses(keys, opts) {
|
|
565
|
+
const list = (keys ?? "").split(/[\s,;]+/).filter(Boolean);
|
|
566
|
+
if (list.length <= 1) return verifyLicense(list[0], opts);
|
|
567
|
+
let best;
|
|
568
|
+
for (const key of list) {
|
|
569
|
+
const r = await verifyLicense(key, opts);
|
|
570
|
+
if (r.licensed) return r;
|
|
571
|
+
if (!best || (FAILURE_RANK[r.status] ?? 9) < (FAILURE_RANK[best.status] ?? 9)) best = r;
|
|
572
|
+
}
|
|
573
|
+
return best ?? { status: "missing", licensed: false };
|
|
574
|
+
}
|
|
575
|
+
var RENEW_URL = "https://ninthforge.com/licence?ref=renewal";
|
|
576
|
+
function renewalNotice(result, now = /* @__PURE__ */ new Date()) {
|
|
577
|
+
if (!result || result.status !== "grace" || !result.payload) return null;
|
|
578
|
+
const expMs = isoToUtcMs(result.payload.exp) + DAY;
|
|
579
|
+
if (Number.isNaN(expMs)) return null;
|
|
580
|
+
const daysLeft = Math.min(GRACE_DAYS, Math.max(1, Math.ceil((expMs + GRACE_DAYS * DAY - now.getTime()) / DAY)));
|
|
581
|
+
const days = daysLeft === 1 ? "1 day" : `${daysLeft} days`;
|
|
582
|
+
return {
|
|
583
|
+
text: `Licence expired \xB7 renew within ${days}`,
|
|
584
|
+
detail: `Your Ninth Forge licence expired on ${result.payload.exp}. Renew within ${days} to keep the watermark off. Only makers see this.`,
|
|
585
|
+
href: RENEW_URL,
|
|
586
|
+
daysLeft
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
function licenseMessage(result) {
|
|
590
|
+
switch (result.status) {
|
|
591
|
+
case "valid":
|
|
592
|
+
case "unverifiable":
|
|
593
|
+
return `Licensed to ${result.payload?.org ?? "your organisation"}`;
|
|
594
|
+
case "grace":
|
|
595
|
+
return "Licence expired \u2014 renew within 14 days to keep the watermark off";
|
|
596
|
+
case "expired":
|
|
597
|
+
return "Licence expired";
|
|
598
|
+
case "missing":
|
|
599
|
+
return "Unlicensed \u2014 add a licence key in the control properties";
|
|
600
|
+
case "wrong-product":
|
|
601
|
+
return "This key is for a different Ninth Forge product";
|
|
602
|
+
case "wrong-tenant":
|
|
603
|
+
return "This key belongs to a different Microsoft tenant";
|
|
604
|
+
case "wrong-host":
|
|
605
|
+
return `This demo key only works on ${result.payload?.hosts?.[0] ?? "the Ninth Forge website"}`;
|
|
606
|
+
default:
|
|
607
|
+
return "Licence key not recognised";
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
var PAID_TIERS = ["team", "business", "enterprise"];
|
|
611
|
+
function unlocksPro(r) {
|
|
612
|
+
if (!r) return false;
|
|
613
|
+
if (r.licensed) return true;
|
|
614
|
+
return r.status === "expired" && !!r.payload && PAID_TIERS.includes(r.payload.tier);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// ../tokens/dist/index.js
|
|
618
|
+
var TEXT_ROLES = ["caption", "body", "label", "value", "callout", "header", "title", "hero"];
|
|
619
|
+
var TEXT_SIZES = {
|
|
620
|
+
caption: 11,
|
|
621
|
+
body: 13,
|
|
622
|
+
label: 12,
|
|
623
|
+
value: 12,
|
|
624
|
+
callout: 12,
|
|
625
|
+
header: 13,
|
|
626
|
+
title: 16,
|
|
627
|
+
hero: 28
|
|
628
|
+
};
|
|
629
|
+
var TEXT_SCALE_MIN = 0.75;
|
|
630
|
+
var TEXT_SCALE_MAX = 2;
|
|
631
|
+
function clampTextScale(scale) {
|
|
632
|
+
if (typeof scale !== "number" || !Number.isFinite(scale) || scale <= 0) return 1;
|
|
633
|
+
return Math.min(TEXT_SCALE_MAX, Math.max(TEXT_SCALE_MIN, scale));
|
|
634
|
+
}
|
|
635
|
+
var half = (px) => Math.round(px * 2) / 2;
|
|
636
|
+
function textSize(role, scale = 1, sizes) {
|
|
637
|
+
const base = sizes?.[role] ?? TEXT_SIZES[role];
|
|
638
|
+
return half(base * clampTextScale(scale));
|
|
639
|
+
}
|
|
640
|
+
function textVars(scale = 1, sizes) {
|
|
641
|
+
const out = {};
|
|
642
|
+
for (const role of TEXT_ROLES) out[`--nf-text-${role}`] = `${textSize(role, scale, sizes)}px`;
|
|
643
|
+
return out;
|
|
644
|
+
}
|
|
645
|
+
function scaleWithText(px, scale = 1) {
|
|
646
|
+
const s = clampTextScale(scale);
|
|
647
|
+
if (s === 1) return px;
|
|
648
|
+
return Math.ceil(px * s / 8) * 8;
|
|
649
|
+
}
|
|
650
|
+
var brand = {
|
|
651
|
+
ink: "#0E1116",
|
|
652
|
+
ember: "#D9480F",
|
|
653
|
+
paper: "#F6F6F3",
|
|
654
|
+
line: "#E4E5E9",
|
|
655
|
+
muted: "#5D6472",
|
|
656
|
+
cobalt: "#2E54FF"
|
|
657
|
+
};
|
|
658
|
+
var semantic = {
|
|
659
|
+
milestone: "#E8930C",
|
|
660
|
+
positive: "#138A52",
|
|
661
|
+
negative: "#D93F2B"
|
|
662
|
+
};
|
|
663
|
+
var font = {
|
|
664
|
+
sans: "'Geist', 'Segoe UI', system-ui, -apple-system, sans-serif",
|
|
665
|
+
mono: "'Geist Mono', 'Cascadia Mono', ui-monospace, monospace",
|
|
666
|
+
/** Components inherit the host app's font unless a theme says otherwise. */
|
|
667
|
+
inherit: "inherit"
|
|
668
|
+
};
|
|
669
|
+
var motion = {
|
|
670
|
+
fast: "150ms",
|
|
671
|
+
base: "220ms",
|
|
672
|
+
ease: "cubic-bezier(0.2, 0.8, 0.2, 1)"
|
|
673
|
+
};
|
|
674
|
+
var lightTheme = {
|
|
675
|
+
accent: brand.cobalt,
|
|
676
|
+
accentTint: "#DCE3FF",
|
|
677
|
+
accentText: "#FFFFFF",
|
|
678
|
+
surface: "#FFFFFF",
|
|
679
|
+
surfaceAlt: "#F6F6F3",
|
|
680
|
+
ink: brand.ink,
|
|
681
|
+
muted: brand.muted,
|
|
682
|
+
line: brand.line,
|
|
683
|
+
lineSoft: "#F2F3F5",
|
|
684
|
+
phase: brand.ink,
|
|
685
|
+
milestone: semantic.milestone,
|
|
686
|
+
baseline: "#B9BDC7",
|
|
687
|
+
selected: "#F4F6FF",
|
|
688
|
+
focus: brand.cobalt,
|
|
689
|
+
positive: semantic.positive,
|
|
690
|
+
negative: semantic.negative,
|
|
691
|
+
tooltipBg: brand.ink,
|
|
692
|
+
tooltipInk: "#FFFFFF",
|
|
693
|
+
font: font.inherit,
|
|
694
|
+
monoFont: font.mono
|
|
695
|
+
};
|
|
696
|
+
var darkTheme = {
|
|
697
|
+
accent: "#6F8BFF",
|
|
698
|
+
accentTint: "#28305A",
|
|
699
|
+
accentText: "#0E1116",
|
|
700
|
+
surface: "#14171D",
|
|
701
|
+
surfaceAlt: "#0E1116",
|
|
702
|
+
ink: "#F2F3F5",
|
|
703
|
+
muted: "#A3A9B6",
|
|
704
|
+
line: "#2A2F3A",
|
|
705
|
+
lineSoft: "#1D2129",
|
|
706
|
+
phase: "#E4E5E9",
|
|
707
|
+
milestone: "#F2A53A",
|
|
708
|
+
baseline: "#5D6472",
|
|
709
|
+
selected: "#1C2238",
|
|
710
|
+
focus: "#8FA6FF",
|
|
711
|
+
positive: "#3FBF7F",
|
|
712
|
+
negative: "#F06A58",
|
|
713
|
+
tooltipBg: "#F2F3F5",
|
|
714
|
+
tooltipInk: "#0E1116",
|
|
715
|
+
font: font.inherit,
|
|
716
|
+
monoFont: font.mono
|
|
717
|
+
};
|
|
718
|
+
var kebab = (s) => s.replace(/[A-Z]/g, (m) => "-" + m.toLowerCase());
|
|
719
|
+
function themeVars(theme = {}, base = lightTheme) {
|
|
720
|
+
const merged = { ...base, ...theme };
|
|
721
|
+
const out = {};
|
|
722
|
+
Object.keys(merged).forEach((k) => {
|
|
723
|
+
out[`--nf-${kebab(k)}`] = merged[k];
|
|
724
|
+
});
|
|
725
|
+
return out;
|
|
726
|
+
}
|
|
727
|
+
function markSvg({ size = 40, tile = true, ink = brand.ink, ember = brand.ember } = {}) {
|
|
728
|
+
const stroke = tile ? "#FFFFFF" : ink;
|
|
729
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 40 40" role="img" aria-label="Ninth Forge">` + (tile ? `<rect width="40" height="40" rx="10" fill="${ink}"/>` : "") + `<rect x="8.5" y="10" width="4.6" height="20" fill="${stroke}"/><path d="M17 10 L31.5 30" stroke="${stroke}" stroke-width="4.6"/><path d="M31.5 10 L17 30" stroke="${stroke}" stroke-width="4.6"/><path d="M31.5 10 L27.6 15.4" stroke="${ember}" stroke-width="4.6"/></svg>`;
|
|
730
|
+
}
|
|
731
|
+
function luminance(hex) {
|
|
732
|
+
const m = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(hex.trim());
|
|
733
|
+
if (!m) return null;
|
|
734
|
+
const h = m[1].length === 3 ? m[1].replace(/./g, (c) => c + c) : m[1];
|
|
735
|
+
const ch = [0, 2, 4].map((i) => {
|
|
736
|
+
const v = parseInt(h.slice(i, i + 2), 16) / 255;
|
|
737
|
+
return v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4;
|
|
738
|
+
});
|
|
739
|
+
return 0.2126 * ch[0] + 0.7152 * ch[1] + 0.0722 * ch[2];
|
|
740
|
+
}
|
|
741
|
+
function contrastRatio(a, b) {
|
|
742
|
+
const la = luminance(a);
|
|
743
|
+
const lb = luminance(b);
|
|
744
|
+
if (la === null || lb === null) return null;
|
|
745
|
+
const [hi, lo] = la > lb ? [la, lb] : [lb, la];
|
|
746
|
+
return (hi + 0.05) / (lo + 0.05);
|
|
747
|
+
}
|
|
748
|
+
function readableTextOn(background, light = "#FFFFFF", dark = brand.ink) {
|
|
749
|
+
const onLight = contrastRatio(background, light);
|
|
750
|
+
const onDark = contrastRatio(background, dark);
|
|
751
|
+
if (onLight === null || onDark === null) return light;
|
|
752
|
+
return onLight >= onDark ? light : dark;
|
|
753
|
+
}
|
|
754
|
+
function mixHex(a, b, t) {
|
|
755
|
+
const p = (h) => {
|
|
756
|
+
const s = h.replace("#", "");
|
|
757
|
+
const f = s.length === 3 ? s.replace(/./g, (c) => c + c) : s;
|
|
758
|
+
return [0, 2, 4].map((i) => parseInt(f.slice(i, i + 2), 16));
|
|
759
|
+
};
|
|
760
|
+
const [x, y] = [p(a), p(b)];
|
|
761
|
+
return "#" + x.map((v, i) => Math.round(v + (y[i] - v) * t).toString(16).padStart(2, "0")).join("").toUpperCase();
|
|
762
|
+
}
|
|
763
|
+
function accentForText(accent, light = "#FFFFFF", dark = brand.ink) {
|
|
764
|
+
const text = readableTextOn(accent, light, dark);
|
|
765
|
+
if ((contrastRatio(accent, text) ?? 21) >= 4.5) return { background: accent, text };
|
|
766
|
+
const toward = text === light ? "#000000" : "#FFFFFF";
|
|
767
|
+
for (let t = 0.05; t <= 1.0001; t += 0.05) {
|
|
768
|
+
const bg = mixHex(accent, toward, t);
|
|
769
|
+
if ((contrastRatio(bg, text) ?? 0) >= 4.5) return { background: bg, text };
|
|
770
|
+
}
|
|
771
|
+
return { background: toward === "#000000" ? "#000000" : "#FFFFFF", text };
|
|
772
|
+
}
|
|
773
|
+
function visibleOn(color, background, ratio2 = 3) {
|
|
774
|
+
const own = contrastRatio(color, background);
|
|
775
|
+
if (own === null || own >= ratio2) return color;
|
|
776
|
+
const dark = (luminance(background) ?? 1) < 0.18;
|
|
777
|
+
const toward = dark ? "#FFFFFF" : "#000000";
|
|
778
|
+
for (let t = 0.1; t <= 1.0001; t += 0.1) {
|
|
779
|
+
const mixed = mixHex(color, toward, t);
|
|
780
|
+
if ((contrastRatio(mixed, background) ?? 0) >= ratio2) return mixed;
|
|
781
|
+
}
|
|
782
|
+
return toward;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// ../editing/dist/chunk-S4GYYU33.js
|
|
786
|
+
var isEmpty = (v) => v === void 0 || v === null || v === "";
|
|
787
|
+
function defaultEqual(_key, a, b) {
|
|
788
|
+
if (isEmpty(a) && isEmpty(b)) return true;
|
|
789
|
+
return a === b;
|
|
790
|
+
}
|
|
791
|
+
function sameStringSet(a, b) {
|
|
792
|
+
const x = Array.isArray(a) ? [...a].sort() : [];
|
|
793
|
+
const y = Array.isArray(b) ? [...b].sort() : [];
|
|
794
|
+
return x.length === y.length && x.every((v, i) => v === y[i]);
|
|
795
|
+
}
|
|
796
|
+
function fieldsOf(change, spec) {
|
|
797
|
+
const out = {};
|
|
798
|
+
for (const k of spec.keys) if (k in change) out[k] = change[k];
|
|
799
|
+
return out;
|
|
800
|
+
}
|
|
801
|
+
function previousOf(item, next2, spec) {
|
|
802
|
+
const prev = {};
|
|
803
|
+
for (const k of spec.keys) if (k in next2) prev[k] = item[k];
|
|
804
|
+
return prev;
|
|
805
|
+
}
|
|
806
|
+
function isNoop(item, change, spec) {
|
|
807
|
+
const eq = spec.equal ?? defaultEqual;
|
|
808
|
+
const f = fieldsOf(change, spec);
|
|
809
|
+
return Object.keys(f).every((k) => eq(k, item[k], f[k]));
|
|
810
|
+
}
|
|
811
|
+
function applyOverrides(items, overrides) {
|
|
812
|
+
if (overrides.size === 0) return items;
|
|
813
|
+
return items.map((t) => {
|
|
814
|
+
const o = overrides.get(t.id);
|
|
815
|
+
return o ? { ...t, ...o.patch } : t;
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
function reconcileOverrides(items, overrides, spec = {}) {
|
|
819
|
+
const eq = spec.equal ?? defaultEqual;
|
|
820
|
+
const byId = new Map(items.map((t) => [t.id, t]));
|
|
821
|
+
const next2 = new Map(overrides);
|
|
822
|
+
for (const [id, o] of overrides) {
|
|
823
|
+
const t = byId.get(id);
|
|
824
|
+
if (!t) {
|
|
825
|
+
next2.delete(id);
|
|
826
|
+
continue;
|
|
827
|
+
}
|
|
828
|
+
const matches = Object.keys(o.patch).every(
|
|
829
|
+
(k) => eq(k, t[k], o.patch[k])
|
|
830
|
+
);
|
|
831
|
+
if (matches) next2.delete(id);
|
|
832
|
+
}
|
|
833
|
+
return next2;
|
|
834
|
+
}
|
|
835
|
+
var HISTORY_LIMIT = 20;
|
|
836
|
+
function emptyHistory() {
|
|
837
|
+
return { undo: [], redo: [] };
|
|
838
|
+
}
|
|
839
|
+
function pushHistory(h, change) {
|
|
840
|
+
return { undo: [...h.undo, change].slice(-HISTORY_LIMIT), redo: [] };
|
|
841
|
+
}
|
|
842
|
+
function dropFromHistory(h, change) {
|
|
843
|
+
return { undo: h.undo.filter((c) => c !== change), redo: h.redo.filter((c) => c !== change) };
|
|
844
|
+
}
|
|
845
|
+
function invertChange(change, source, spec) {
|
|
846
|
+
const forward = fieldsOf(change, spec);
|
|
847
|
+
const back = {};
|
|
848
|
+
for (const k of Object.keys(forward)) {
|
|
849
|
+
const v = change.previous[k];
|
|
850
|
+
back[k] = v !== void 0 ? v : spec.blank ? spec.blank(k) : void 0;
|
|
851
|
+
}
|
|
852
|
+
return { id: change.id, ...back, previous: forward, source };
|
|
853
|
+
}
|
|
854
|
+
function undoStep(h, undoSource, spec) {
|
|
855
|
+
const entry = h.undo[h.undo.length - 1];
|
|
856
|
+
if (!entry) return null;
|
|
857
|
+
return { commit: invertChange(entry, undoSource, spec), entry, history: { undo: h.undo.slice(0, -1), redo: [...h.redo, entry] } };
|
|
858
|
+
}
|
|
859
|
+
function redoStep(h, redoSource) {
|
|
860
|
+
const entry = h.redo[h.redo.length - 1];
|
|
861
|
+
if (!entry) return null;
|
|
862
|
+
const commit = { ...entry, source: redoSource };
|
|
863
|
+
return { commit, entry, history: { undo: [...h.undo, entry].slice(-HISTORY_LIMIT), redo: h.redo.slice(0, -1) } };
|
|
864
|
+
}
|
|
865
|
+
function errorText(err, fallback = "The change was not saved.") {
|
|
866
|
+
if (err instanceof Error && err.message) return err.message;
|
|
867
|
+
if (typeof err === "string" && err) return err;
|
|
868
|
+
const m = err?.message;
|
|
869
|
+
return typeof m === "string" && m ? m : fallback;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// ../editing/dist/index.js
|
|
873
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
874
|
+
import * as React from "react";
|
|
875
|
+
import * as React2 from "react";
|
|
876
|
+
import * as React3 from "react";
|
|
877
|
+
import * as React4 from "react";
|
|
878
|
+
var INFO_MS = 5e3;
|
|
879
|
+
var ERROR_MS = 9e3;
|
|
880
|
+
function trialUrl(ref) {
|
|
881
|
+
return `https://ninthforge.com/trial?ref=${encodeURIComponent(ref)}`;
|
|
882
|
+
}
|
|
883
|
+
function lockedMessage(product) {
|
|
884
|
+
return `Saving changes needs a Ninth Forge ${product} key. Start a free 30-day trial at ninthforge.com/trial.`;
|
|
885
|
+
}
|
|
886
|
+
function lockFor(license, unlocked, product, trialRef) {
|
|
887
|
+
if (unlocked) return { locked: false };
|
|
888
|
+
if (!license) return { locked: true };
|
|
889
|
+
return trialRef ? { locked: true, lockedMessage: lockedMessage(product), lockedTrialUrl: trialUrl(trialRef) } : { locked: true, lockedMessage: lockedMessage(product) };
|
|
890
|
+
}
|
|
891
|
+
var toastedLocks = /* @__PURE__ */ new Set();
|
|
892
|
+
function firstLockToast(message) {
|
|
893
|
+
if (toastedLocks.has(message)) return false;
|
|
894
|
+
toastedLocks.add(message);
|
|
895
|
+
return true;
|
|
896
|
+
}
|
|
897
|
+
function openTrial(url) {
|
|
898
|
+
if (typeof window !== "undefined") window.open(url, "_blank", "noopener");
|
|
899
|
+
}
|
|
900
|
+
function useEditing(items, onChange, spec) {
|
|
901
|
+
const [overrides, setOverrides] = useState(() => /* @__PURE__ */ new Map());
|
|
902
|
+
const historyRef = useRef(emptyHistory());
|
|
903
|
+
const [, setHistoryVersion] = useState(0);
|
|
904
|
+
const setHistory = (fn) => {
|
|
905
|
+
historyRef.current = fn(historyRef.current);
|
|
906
|
+
setHistoryVersion((v) => v + 1);
|
|
907
|
+
};
|
|
908
|
+
const seqRef = useRef(0);
|
|
909
|
+
const [toasts, setToasts] = useState([]);
|
|
910
|
+
const [announcement, setAnnouncement] = useState({ text: "", n: 0 });
|
|
911
|
+
const timers = useRef(/* @__PURE__ */ new Map());
|
|
912
|
+
const itemsRef = useRef(items);
|
|
913
|
+
itemsRef.current = items;
|
|
914
|
+
const handlerRef = useRef(onChange);
|
|
915
|
+
handlerRef.current = onChange;
|
|
916
|
+
const specRef = useRef(spec);
|
|
917
|
+
specRef.current = spec;
|
|
918
|
+
useEffect(() => {
|
|
919
|
+
setOverrides((o) => o.size ? reconcileOverrides(items, o, specRef.current) : o);
|
|
920
|
+
}, [items]);
|
|
921
|
+
useEffect(() => {
|
|
922
|
+
const map = timers.current;
|
|
923
|
+
return () => map.forEach((t) => clearTimeout(t));
|
|
924
|
+
}, []);
|
|
925
|
+
const dismiss = useCallback((id) => {
|
|
926
|
+
const t = timers.current.get(id);
|
|
927
|
+
if (t) clearTimeout(t);
|
|
928
|
+
timers.current.delete(id);
|
|
929
|
+
setToasts((list) => list.filter((x) => x.id !== id));
|
|
930
|
+
}, []);
|
|
931
|
+
const toast = useCallback(
|
|
932
|
+
(item) => {
|
|
933
|
+
const id = ++seqRef.current;
|
|
934
|
+
setToasts((list) => [...list.filter((x) => x.tone === "error"), { ...item, id }].slice(-4));
|
|
935
|
+
timers.current.set(
|
|
936
|
+
id,
|
|
937
|
+
setTimeout(() => dismiss(id), item.tone === "error" ? ERROR_MS : INFO_MS)
|
|
938
|
+
);
|
|
939
|
+
},
|
|
940
|
+
[dismiss]
|
|
941
|
+
);
|
|
942
|
+
const announce = useCallback((text) => setAnnouncement((a) => ({ text, n: a.n + 1 })), []);
|
|
943
|
+
const [lockNote, setLockNote] = useState(null);
|
|
944
|
+
const refuse = useCallback(() => {
|
|
945
|
+
const msg = specRef.current.lockedMessage;
|
|
946
|
+
if (!msg) return;
|
|
947
|
+
announce(msg);
|
|
948
|
+
setLockNote(msg);
|
|
949
|
+
if (!firstLockToast(msg)) return;
|
|
950
|
+
const url = specRef.current.lockedTrialUrl;
|
|
951
|
+
toast({ text: msg, tone: "info", action: url ? { label: specRef.current.lockedTrialLabel ?? "Start a free trial", run: () => openTrial(url) } : void 0 });
|
|
952
|
+
}, [announce, toast]);
|
|
953
|
+
const nameOf = (id) => {
|
|
954
|
+
const item = itemsRef.current.find((t) => t.id === id);
|
|
955
|
+
const s = specRef.current;
|
|
956
|
+
if (!item) return s.noun ?? "item";
|
|
957
|
+
return s.name ? s.name(item) : item.name ?? s.noun ?? "item";
|
|
958
|
+
};
|
|
959
|
+
const undoRef = useRef(() => void 0);
|
|
960
|
+
const redoRef = useRef(() => void 0);
|
|
961
|
+
const commit = (change, mode, entry) => {
|
|
962
|
+
if (specRef.current.locked) return refuse();
|
|
963
|
+
const seq = ++seqRef.current;
|
|
964
|
+
setOverrides((m) => {
|
|
965
|
+
const next2 = new Map(m);
|
|
966
|
+
next2.set(change.id, { patch: { ...m.get(change.id)?.patch, ...fieldsOf(change, specRef.current) }, seq });
|
|
967
|
+
return next2;
|
|
968
|
+
});
|
|
969
|
+
if (mode === "new") setHistory((h) => pushHistory(h, change));
|
|
970
|
+
const text = specRef.current.describe(change, nameOf);
|
|
971
|
+
announce(text);
|
|
972
|
+
toast({
|
|
973
|
+
text,
|
|
974
|
+
tone: "info",
|
|
975
|
+
action: mode === "undo" ? { label: "Redo", run: () => redoRef.current() } : { label: "Undo", run: () => undoRef.current() }
|
|
976
|
+
});
|
|
977
|
+
let result;
|
|
978
|
+
try {
|
|
979
|
+
result = handlerRef.current?.(change);
|
|
980
|
+
} catch (err) {
|
|
981
|
+
result = Promise.reject(err);
|
|
982
|
+
}
|
|
983
|
+
if (result && typeof result.then === "function") {
|
|
984
|
+
result.then(void 0, (err) => {
|
|
985
|
+
setOverrides((m) => {
|
|
986
|
+
const o = m.get(change.id);
|
|
987
|
+
if (!o || o.seq !== seq) return m;
|
|
988
|
+
const next2 = new Map(m);
|
|
989
|
+
next2.delete(change.id);
|
|
990
|
+
return next2;
|
|
991
|
+
});
|
|
992
|
+
setHistory((h) => {
|
|
993
|
+
if (mode === "new") return dropFromHistory(h, change);
|
|
994
|
+
if (!entry) return h;
|
|
995
|
+
return mode === "undo" ? { undo: [...h.undo, entry], redo: h.redo.filter((c) => c !== entry) } : { undo: h.undo.filter((c) => c !== entry), redo: [...h.redo, entry] };
|
|
996
|
+
});
|
|
997
|
+
const msg = errorText(err);
|
|
998
|
+
toast({ text: `Couldn't save '${nameOf(change.id)}', so it was put back. ${msg}`, tone: "error" });
|
|
999
|
+
announce(`Not saved, put back. ${msg}`);
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
};
|
|
1003
|
+
const undo = () => {
|
|
1004
|
+
const step = undoStep(historyRef.current, "undo", specRef.current);
|
|
1005
|
+
if (!step) return announce("Nothing to undo");
|
|
1006
|
+
setHistory(() => step.history);
|
|
1007
|
+
commit(step.commit, "undo", step.entry);
|
|
1008
|
+
};
|
|
1009
|
+
const redo = () => {
|
|
1010
|
+
const step = redoStep(historyRef.current, "redo");
|
|
1011
|
+
if (!step) return announce("Nothing to redo");
|
|
1012
|
+
setHistory(() => step.history);
|
|
1013
|
+
commit(step.commit, "redo", step.entry);
|
|
1014
|
+
};
|
|
1015
|
+
undoRef.current = undo;
|
|
1016
|
+
redoRef.current = redo;
|
|
1017
|
+
const shown = useMemo(() => applyOverrides(items, overrides), [items, overrides]);
|
|
1018
|
+
return {
|
|
1019
|
+
overrides,
|
|
1020
|
+
shown,
|
|
1021
|
+
commit: (change) => commit(change, "new"),
|
|
1022
|
+
undo,
|
|
1023
|
+
redo,
|
|
1024
|
+
canUndo: historyRef.current.undo.length > 0,
|
|
1025
|
+
canRedo: historyRef.current.redo.length > 0,
|
|
1026
|
+
toasts,
|
|
1027
|
+
toast,
|
|
1028
|
+
dismiss,
|
|
1029
|
+
announcement,
|
|
1030
|
+
announce,
|
|
1031
|
+
/** View-only: Pro actions are refused (see `EditingSpec.locked`). */
|
|
1032
|
+
locked: !!spec.locked,
|
|
1033
|
+
/** Call instead of an action that bypasses `commit` (create, add) while `locked`. */
|
|
1034
|
+
refuse,
|
|
1035
|
+
/** The locked message once an action was refused, for a quiet status line. */
|
|
1036
|
+
lockNote: spec.locked ? lockNote : null
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
function Toasts({
|
|
1040
|
+
items,
|
|
1041
|
+
announcement,
|
|
1042
|
+
onDismiss,
|
|
1043
|
+
prefix = "nf-g"
|
|
1044
|
+
}) {
|
|
1045
|
+
return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { className: `${prefix}-sr`, "aria-live": "polite", "aria-atomic": "true" }, /* @__PURE__ */ React.createElement("span", { key: announcement.n }, announcement.text)), items.length ? /* @__PURE__ */ React.createElement("div", { className: `${prefix}-toasts` }, items.map((t) => /* @__PURE__ */ React.createElement("div", { key: t.id, className: `${prefix}-toast`, "data-tone": t.tone, role: t.tone === "error" ? "alert" : "status" }, /* @__PURE__ */ React.createElement("span", { className: `${prefix}-toast-text` }, t.text), t.action ? /* @__PURE__ */ React.createElement(
|
|
1046
|
+
"button",
|
|
1047
|
+
{
|
|
1048
|
+
type: "button",
|
|
1049
|
+
className: `${prefix}-toast-btn`,
|
|
1050
|
+
onClick: () => {
|
|
1051
|
+
t.action.run();
|
|
1052
|
+
onDismiss(t.id);
|
|
1053
|
+
}
|
|
1054
|
+
},
|
|
1055
|
+
t.action.label
|
|
1056
|
+
) : null, /* @__PURE__ */ React.createElement("button", { type: "button", className: `${prefix}-toast-x`, "aria-label": "Dismiss", onClick: () => onDismiss(t.id) }, "\xD7")))) : null);
|
|
1057
|
+
}
|
|
1058
|
+
function EmptyActionView({ action, prefix, buttonClass }) {
|
|
1059
|
+
return /* @__PURE__ */ React2.createElement("div", { className: `${prefix}-emptyaction` }, action.busy ? /* @__PURE__ */ React2.createElement("span", { role: "status" }, action.status ?? `${action.label}\u2026`) : /* @__PURE__ */ React2.createElement("button", { type: "button", className: buttonClass, onClick: action.onClick }, action.label), action.hint && !action.busy ? /* @__PURE__ */ React2.createElement("small", null, action.hint) : null, action.error ? /* @__PURE__ */ React2.createElement("span", { role: "alert" }, action.error) : null);
|
|
1060
|
+
}
|
|
1061
|
+
var PLAN_WATERMARK_URL = "https://ninthforge.com/plan?ref=watermark";
|
|
1062
|
+
function Watermark({ prefix, title, markHtml, variant = "mark", onClick, href = PLAN_WATERMARK_URL }) {
|
|
1063
|
+
const mark = variant === "text" ? null : /* @__PURE__ */ React4.createElement("span", { "aria-hidden": "true", style: { display: "inline-flex" }, dangerouslySetInnerHTML: { __html: markHtml } });
|
|
1064
|
+
return onClick ? /* @__PURE__ */ React4.createElement("button", { type: "button", className: `${prefix}-mark`, title, onClick }, mark, "Ninth Forge \xB7 Unlicensed") : /* @__PURE__ */ React4.createElement("a", { className: `${prefix}-mark`, href, target: "_blank", rel: "noreferrer", title }, mark, "Ninth Forge \xB7 Unlicensed");
|
|
1065
|
+
}
|
|
1066
|
+
var next = 0;
|
|
1067
|
+
function nextUid(prefix) {
|
|
1068
|
+
next += 1;
|
|
1069
|
+
return `${prefix}${next.toString(36)}`;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// src/edit.ts
|
|
1073
|
+
function canEdit(editable, hasHandler, action, row) {
|
|
1074
|
+
if (!hasHandler || editable === false) return false;
|
|
1075
|
+
if (typeof editable === "object" && editable[action] === false) return false;
|
|
1076
|
+
if (!row || action === "add") return true;
|
|
1077
|
+
if (row.task.readOnly) return false;
|
|
1078
|
+
if (action === "rename") return true;
|
|
1079
|
+
if (row.kind === "phase") return false;
|
|
1080
|
+
if (row.kind === "milestone" && (action === "resize" || action === "progress")) return false;
|
|
1081
|
+
return true;
|
|
1082
|
+
}
|
|
1083
|
+
function pxToDays(dx, dayWidth) {
|
|
1084
|
+
return Math.round(dx / dayWidth) || 0;
|
|
1085
|
+
}
|
|
1086
|
+
function shiftSpan(mode, span, days, week = "all") {
|
|
1087
|
+
if (week === "all" || days === 0) {
|
|
1088
|
+
if (mode === "move") return { start: span.start + days, end: span.end + days };
|
|
1089
|
+
if (mode === "start") return { start: Math.min(span.start + days, span.end), end: span.end };
|
|
1090
|
+
return { start: span.start, end: Math.max(span.end + days, span.start) };
|
|
1091
|
+
}
|
|
1092
|
+
const snap = (d) => snapToWorkingDay(d, days, week);
|
|
1093
|
+
if (mode === "move") {
|
|
1094
|
+
const start = snap(span.start + days);
|
|
1095
|
+
const len = workingDaysBetween(span.start, span.end, week);
|
|
1096
|
+
const end = len > 0 ? addWorkingDays(start, len - 1, week) : start + (span.end - span.start);
|
|
1097
|
+
return { start, end };
|
|
1098
|
+
}
|
|
1099
|
+
if (mode === "start") return { start: Math.min(snap(span.start + days), span.end), end: span.end };
|
|
1100
|
+
return { start: span.start, end: Math.max(snap(span.end + days), span.start) };
|
|
1101
|
+
}
|
|
1102
|
+
function durationLabel(start, end, week = "all") {
|
|
1103
|
+
const n = week === "all" ? end - start + 1 : workingDaysBetween(start, end, week);
|
|
1104
|
+
const unit = week === "all" ? "day" : "working day";
|
|
1105
|
+
return `${n} ${unit}${n === 1 ? "" : "s"}`;
|
|
1106
|
+
}
|
|
1107
|
+
function dragProgress(progress, dx, barWidth) {
|
|
1108
|
+
if (barWidth <= 0) return clampProgress(progress);
|
|
1109
|
+
return clampProgress(Math.round(progress + dx / barWidth * 100));
|
|
1110
|
+
}
|
|
1111
|
+
function clampProgress(p) {
|
|
1112
|
+
return Math.max(0, Math.min(100, Math.round(p)));
|
|
1113
|
+
}
|
|
1114
|
+
function keyEdit(e) {
|
|
1115
|
+
if (!e.altKey || e.ctrlKey || e.metaKey) return null;
|
|
1116
|
+
switch (e.key) {
|
|
1117
|
+
case "ArrowLeft":
|
|
1118
|
+
return { kind: "span", mode: e.shiftKey ? "end" : "move", days: -1 };
|
|
1119
|
+
case "ArrowRight":
|
|
1120
|
+
return { kind: "span", mode: e.shiftKey ? "end" : "move", days: 1 };
|
|
1121
|
+
case "ArrowUp":
|
|
1122
|
+
return e.shiftKey ? null : { kind: "progress", delta: 10 };
|
|
1123
|
+
case "ArrowDown":
|
|
1124
|
+
return e.shiftKey ? null : { kind: "progress", delta: -10 };
|
|
1125
|
+
default:
|
|
1126
|
+
return null;
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
var GANTT_FIELDS = {
|
|
1130
|
+
keys: ["name", "start", "end", "date", "progress", "parentId", "dependsOn"],
|
|
1131
|
+
equal: (key, a, b) => key === "dependsOn" ? sameStringSet(a, b) : defaultEqual(key, a, b),
|
|
1132
|
+
blank: (key) => key === "dependsOn" ? [] : key === "parentId" ? null : void 0
|
|
1133
|
+
};
|
|
1134
|
+
function fieldsOf2(change) {
|
|
1135
|
+
return fieldsOf(change, GANTT_FIELDS);
|
|
1136
|
+
}
|
|
1137
|
+
function previousOf2(task, next2) {
|
|
1138
|
+
return previousOf(task, next2, GANTT_FIELDS);
|
|
1139
|
+
}
|
|
1140
|
+
function spanChange(task, isMilestone, span, source) {
|
|
1141
|
+
let next2;
|
|
1142
|
+
if (isMilestone) next2 = task.date !== void 0 ? { date: fromDay(span.start) } : { start: fromDay(span.start) };
|
|
1143
|
+
else next2 = { start: fromDay(span.start), end: fromDay(span.end) };
|
|
1144
|
+
return { id: task.id, ...next2, previous: previousOf2(task, next2), source };
|
|
1145
|
+
}
|
|
1146
|
+
function progressChange(task, progress, source) {
|
|
1147
|
+
const next2 = { progress: clampProgress(progress) };
|
|
1148
|
+
return { id: task.id, ...next2, previous: previousOf2(task, next2), source };
|
|
1149
|
+
}
|
|
1150
|
+
function renameChange(task, name, source) {
|
|
1151
|
+
const next2 = name.trim();
|
|
1152
|
+
if (!next2 || next2 === task.name) return null;
|
|
1153
|
+
return { id: task.id, name: next2, previous: { name: task.name }, source };
|
|
1154
|
+
}
|
|
1155
|
+
function dateCellChange(task, isMilestone, span, field, day, source) {
|
|
1156
|
+
if (isMilestone) return spanChange(task, true, { start: day, end: day }, source);
|
|
1157
|
+
const next2 = field === "start" ? { start: day, end: Math.max(span.end, day) } : { start: span.start, end: Math.max(day, span.start) };
|
|
1158
|
+
return spanChange(task, false, next2, source);
|
|
1159
|
+
}
|
|
1160
|
+
function isNoop2(task, change) {
|
|
1161
|
+
return isNoop(task, change, GANTT_FIELDS);
|
|
1162
|
+
}
|
|
1163
|
+
var emptyHistory2 = emptyHistory();
|
|
1164
|
+
function brokenLinks(rows) {
|
|
1165
|
+
const byId = new Map(rows.map((r) => [r.task.id, r]));
|
|
1166
|
+
const out = /* @__PURE__ */ new Set();
|
|
1167
|
+
for (const to of rows) {
|
|
1168
|
+
for (const fromId of to.task.dependsOn ?? []) {
|
|
1169
|
+
const from = byId.get(fromId);
|
|
1170
|
+
if (!from || Number.isNaN(from.end) || Number.isNaN(to.start)) continue;
|
|
1171
|
+
const broken = from.kind === "milestone" ? to.start < from.end : to.start <= from.end;
|
|
1172
|
+
if (broken) out.add(`${fromId}>${to.task.id}`);
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
return out;
|
|
1176
|
+
}
|
|
1177
|
+
function brokenAmong(links, rows) {
|
|
1178
|
+
const out = /* @__PURE__ */ new Set();
|
|
1179
|
+
for (const l of links) {
|
|
1180
|
+
const from = rows[l.fromIndex];
|
|
1181
|
+
const to = rows[l.toIndex];
|
|
1182
|
+
if (!from || !to || Number.isNaN(from.end) || Number.isNaN(to.start)) continue;
|
|
1183
|
+
const broken = from.kind === "milestone" ? to.start < from.end : to.start <= from.end;
|
|
1184
|
+
if (broken) out.add(l.key);
|
|
1185
|
+
}
|
|
1186
|
+
return out;
|
|
1187
|
+
}
|
|
1188
|
+
function validateLink(tasks, fromId, toId) {
|
|
1189
|
+
if (fromId === toId) return "self";
|
|
1190
|
+
const byId = new Map(tasks.map((t) => [t.id, t]));
|
|
1191
|
+
if (byId.get(toId)?.dependsOn?.includes(fromId)) return "exists";
|
|
1192
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1193
|
+
const stack = [fromId];
|
|
1194
|
+
while (stack.length) {
|
|
1195
|
+
const id = stack.pop();
|
|
1196
|
+
if (id === toId) return "cycle";
|
|
1197
|
+
if (seen.has(id)) continue;
|
|
1198
|
+
seen.add(id);
|
|
1199
|
+
for (const p of byId.get(id)?.dependsOn ?? []) stack.push(p);
|
|
1200
|
+
}
|
|
1201
|
+
return "ok";
|
|
1202
|
+
}
|
|
1203
|
+
function linkChange(to, fromId, single, source) {
|
|
1204
|
+
const existing = to.dependsOn ?? [];
|
|
1205
|
+
const dependsOn = single ? [fromId] : [...existing, fromId];
|
|
1206
|
+
const replaces = single && existing.length > 0 && existing[0] !== fromId ? existing[0] : null;
|
|
1207
|
+
return { change: { id: to.id, dependsOn, previous: { dependsOn: existing }, source }, replaces };
|
|
1208
|
+
}
|
|
1209
|
+
function unlinkChange(to, fromId, source) {
|
|
1210
|
+
const existing = to.dependsOn ?? [];
|
|
1211
|
+
const dependsOn = fromId === void 0 ? [] : existing.filter((d) => d !== fromId);
|
|
1212
|
+
return { id: to.id, dependsOn, previous: { dependsOn: existing }, source };
|
|
1213
|
+
}
|
|
1214
|
+
function dayLabel(day) {
|
|
1215
|
+
const p = parts(day);
|
|
1216
|
+
return `${WEEKDAYS[weekday(day)]} ${p.date} ${MONTHS[p.month]}`;
|
|
1217
|
+
}
|
|
1218
|
+
function spanLabel(span, isMilestone, week = "all") {
|
|
1219
|
+
if (isMilestone) return dayLabel(span.start);
|
|
1220
|
+
return `${dayLabel(span.start)} \u2192 ${dayLabel(span.end)} \xB7 ${durationLabel(span.start, span.end, week)}`;
|
|
1221
|
+
}
|
|
1222
|
+
function describeChange(change, name) {
|
|
1223
|
+
const who = `'${name(change.id)}'`;
|
|
1224
|
+
const prefix = change.source === "undo" ? "Undone: " : change.source === "redo" ? "Redone: " : "";
|
|
1225
|
+
const d = (iso) => iso ? dayLabel(Math.round(Date.parse(iso) / 864e5)) : "";
|
|
1226
|
+
let text;
|
|
1227
|
+
if (change.name !== void 0) {
|
|
1228
|
+
const before = change.previous.name;
|
|
1229
|
+
text = before ? `'${before}' renamed to '${change.name}'` : `Renamed to '${change.name}'`;
|
|
1230
|
+
} else if (change.dependsOn !== void 0) {
|
|
1231
|
+
text = change.dependsOn.length ? `${who} now follows ${change.dependsOn.map((id) => `'${name(id)}'`).join(", ")}` : `${who} no longer depends on another task`;
|
|
1232
|
+
} else if (change.progress !== void 0) {
|
|
1233
|
+
text = `${who} is ${change.progress}% done`;
|
|
1234
|
+
} else if (change.date !== void 0 || change.start !== void 0 && change.end === void 0) {
|
|
1235
|
+
text = `${who} moved to ${d(change.date ?? change.start)}`;
|
|
1236
|
+
} else {
|
|
1237
|
+
text = `${who} now runs ${d(change.start)} to ${d(change.end)}`;
|
|
1238
|
+
}
|
|
1239
|
+
return prefix + text;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
// src/layout.ts
|
|
1243
|
+
var ROW_H = 40;
|
|
1244
|
+
var HEADER_H = 52;
|
|
1245
|
+
function kindOf(t, hasChildren) {
|
|
1246
|
+
if (t.kind) return t.kind;
|
|
1247
|
+
if (hasChildren) return "phase";
|
|
1248
|
+
if (t.date || t.start && !t.end) return "milestone";
|
|
1249
|
+
return "task";
|
|
1250
|
+
}
|
|
1251
|
+
function buildRows(tasks, collapsed = /* @__PURE__ */ new Set()) {
|
|
1252
|
+
const byId = new Map(tasks.map((t) => [t.id, t]));
|
|
1253
|
+
const children = /* @__PURE__ */ new Map();
|
|
1254
|
+
const roots = [];
|
|
1255
|
+
const inCycle = (id) => {
|
|
1256
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1257
|
+
for (let cur = byId.get(id)?.parentId; cur && byId.has(cur); cur = byId.get(cur).parentId) {
|
|
1258
|
+
if (cur === id) return true;
|
|
1259
|
+
if (seen.has(cur)) return false;
|
|
1260
|
+
seen.add(cur);
|
|
1261
|
+
}
|
|
1262
|
+
return false;
|
|
1263
|
+
};
|
|
1264
|
+
for (const t of tasks) {
|
|
1265
|
+
const p = t.parentId && byId.has(t.parentId) && t.parentId !== t.id && !inCycle(t.id) ? t.parentId : null;
|
|
1266
|
+
if (p) {
|
|
1267
|
+
const list = children.get(p) ?? [];
|
|
1268
|
+
list.push(t);
|
|
1269
|
+
children.set(p, list);
|
|
1270
|
+
} else {
|
|
1271
|
+
roots.push(t);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
const spanCache = /* @__PURE__ */ new Map();
|
|
1275
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
1276
|
+
const span = (t) => {
|
|
1277
|
+
const cached = spanCache.get(t.id);
|
|
1278
|
+
if (cached) return cached;
|
|
1279
|
+
if (visiting.has(t.id)) return [NaN, NaN];
|
|
1280
|
+
visiting.add(t.id);
|
|
1281
|
+
let s = toDay(t.date ?? t.start);
|
|
1282
|
+
let e = toDay(t.date ?? t.end ?? t.start);
|
|
1283
|
+
for (const c of children.get(t.id) ?? []) {
|
|
1284
|
+
const [cs, ce] = span(c);
|
|
1285
|
+
if (!Number.isNaN(cs)) s = Number.isNaN(s) ? cs : Math.min(s, cs);
|
|
1286
|
+
if (!Number.isNaN(ce)) e = Number.isNaN(e) ? ce : Math.max(e, ce);
|
|
1287
|
+
}
|
|
1288
|
+
visiting.delete(t.id);
|
|
1289
|
+
const result = [s, e];
|
|
1290
|
+
spanCache.set(t.id, result);
|
|
1291
|
+
return result;
|
|
1292
|
+
};
|
|
1293
|
+
const progCache = /* @__PURE__ */ new Map();
|
|
1294
|
+
const prog = (t, guard) => {
|
|
1295
|
+
const cached = progCache.get(t.id);
|
|
1296
|
+
if (cached) return cached;
|
|
1297
|
+
if (guard.has(t.id)) return [0, 0];
|
|
1298
|
+
guard.add(t.id);
|
|
1299
|
+
const kids = children.get(t.id) ?? [];
|
|
1300
|
+
let result;
|
|
1301
|
+
if (kids.length === 0) {
|
|
1302
|
+
const [s, e] = span(t);
|
|
1303
|
+
const days = Number.isNaN(s) || Number.isNaN(e) || t.date ? 0 : e - s + 1;
|
|
1304
|
+
result = [pct(t.progress) * days, days];
|
|
1305
|
+
} else {
|
|
1306
|
+
result = kids.reduce(
|
|
1307
|
+
(acc, c) => {
|
|
1308
|
+
const [p, d] = prog(c, guard);
|
|
1309
|
+
return [acc[0] + p, acc[1] + d];
|
|
1310
|
+
},
|
|
1311
|
+
[0, 0]
|
|
1312
|
+
);
|
|
1313
|
+
}
|
|
1314
|
+
guard.delete(t.id);
|
|
1315
|
+
progCache.set(t.id, result);
|
|
1316
|
+
return result;
|
|
1317
|
+
};
|
|
1318
|
+
const progressOf = (t) => {
|
|
1319
|
+
if (!children.has(t.id)) return pct(t.progress);
|
|
1320
|
+
const [p, d] = prog(t, /* @__PURE__ */ new Set());
|
|
1321
|
+
return d > 0 ? Math.round(p / d) : 0;
|
|
1322
|
+
};
|
|
1323
|
+
const rows = [];
|
|
1324
|
+
const walk = (list, depth, seen) => {
|
|
1325
|
+
for (const t of list) {
|
|
1326
|
+
if (seen.has(t.id)) continue;
|
|
1327
|
+
seen.add(t.id);
|
|
1328
|
+
const kids = children.get(t.id) ?? [];
|
|
1329
|
+
const [s, e] = span(t);
|
|
1330
|
+
const isCollapsed = collapsed.has(t.id);
|
|
1331
|
+
rows.push({
|
|
1332
|
+
task: t,
|
|
1333
|
+
kind: kindOf(t, kids.length > 0),
|
|
1334
|
+
depth,
|
|
1335
|
+
index: rows.length,
|
|
1336
|
+
hasChildren: kids.length > 0,
|
|
1337
|
+
collapsed: isCollapsed,
|
|
1338
|
+
start: s,
|
|
1339
|
+
end: e,
|
|
1340
|
+
baselineStart: toDay(t.baselineStart),
|
|
1341
|
+
baselineEnd: toDay(t.baselineEnd),
|
|
1342
|
+
progress: progressOf(t)
|
|
1343
|
+
});
|
|
1344
|
+
if (!isCollapsed) walk(kids, depth + 1, seen);
|
|
1345
|
+
}
|
|
1346
|
+
};
|
|
1347
|
+
walk(roots, 0, /* @__PURE__ */ new Set());
|
|
1348
|
+
return rows;
|
|
1349
|
+
}
|
|
1350
|
+
var OVERSCAN = 8;
|
|
1351
|
+
function visibleRange(scrollTop, viewportHeight, rowCount, rowH = ROW_H, headerH = HEADER_H, overscan = OVERSCAN) {
|
|
1352
|
+
if (rowCount <= 0) return { first: 0, last: -1 };
|
|
1353
|
+
const top = Math.max(0, scrollTop - headerH);
|
|
1354
|
+
const last = Math.min(rowCount - 1, Math.ceil((top + Math.max(viewportHeight, rowH)) / rowH) + overscan);
|
|
1355
|
+
const first = Math.min(last, Math.max(0, Math.floor(top / rowH) - overscan));
|
|
1356
|
+
return { first, last };
|
|
1357
|
+
}
|
|
1358
|
+
function parentIds(rows) {
|
|
1359
|
+
return new Set(rows.filter((r) => r.hasChildren).map((r) => r.task.id));
|
|
1360
|
+
}
|
|
1361
|
+
var COLUMN_ORDER = ["owner", "progress", "start", "end", "duration"];
|
|
1362
|
+
var COLUMN_LABEL = {
|
|
1363
|
+
owner: "Owner",
|
|
1364
|
+
progress: "Done",
|
|
1365
|
+
start: "Start",
|
|
1366
|
+
end: "End",
|
|
1367
|
+
duration: "Days"
|
|
1368
|
+
};
|
|
1369
|
+
var COLUMN_WIDTH = { owner: 56, progress: 56, start: 92, end: 92, duration: 56 };
|
|
1370
|
+
var MIN_COLUMN_WIDTH = 40;
|
|
1371
|
+
function scaleColumns(widths, textScale = 1) {
|
|
1372
|
+
const out = { ...widths };
|
|
1373
|
+
if (textScale === 1) return out;
|
|
1374
|
+
Object.keys(out).forEach((k) => {
|
|
1375
|
+
out[k] = Math.max(MIN_COLUMN_WIDTH, Math.round(out[k] * textScale));
|
|
1376
|
+
});
|
|
1377
|
+
return out;
|
|
1378
|
+
}
|
|
1379
|
+
var MIN_NAME_WIDTH = 120;
|
|
1380
|
+
function visibleColumns(columns = {}) {
|
|
1381
|
+
const defaults = { owner: true, progress: true, start: false, end: false, duration: false };
|
|
1382
|
+
return COLUMN_ORDER.filter((c) => columns[c] ?? defaults[c]);
|
|
1383
|
+
}
|
|
1384
|
+
function rowMatches(r, query) {
|
|
1385
|
+
const q = query.trim().toLowerCase();
|
|
1386
|
+
return !!q && (r.task.name.toLowerCase().includes(q) || (r.task.owner?.name.toLowerCase().includes(q) ?? false));
|
|
1387
|
+
}
|
|
1388
|
+
function filterRows(rows, query) {
|
|
1389
|
+
const q = query.trim().toLowerCase();
|
|
1390
|
+
if (!q) return rows;
|
|
1391
|
+
const byId = new Map(rows.map((r) => [r.task.id, r]));
|
|
1392
|
+
const keep = /* @__PURE__ */ new Set();
|
|
1393
|
+
for (const r of rows) {
|
|
1394
|
+
if (!rowMatches(r, q)) continue;
|
|
1395
|
+
let cur = r;
|
|
1396
|
+
const guard = /* @__PURE__ */ new Set();
|
|
1397
|
+
while (cur && !guard.has(cur.task.id)) {
|
|
1398
|
+
guard.add(cur.task.id);
|
|
1399
|
+
keep.add(cur.task.id);
|
|
1400
|
+
cur = cur.task.parentId ? byId.get(cur.task.parentId) : void 0;
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
return rows.filter((r) => keep.has(r.task.id)).map((r, index) => ({ ...r, index }));
|
|
1404
|
+
}
|
|
1405
|
+
function rowsRange(rows) {
|
|
1406
|
+
const days = [];
|
|
1407
|
+
for (const r of rows) {
|
|
1408
|
+
for (const d of [r.start, r.end, r.baselineStart, r.baselineEnd]) {
|
|
1409
|
+
if (Number.isFinite(d)) days.push(d);
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
if (!days.length) return null;
|
|
1413
|
+
days.sort((a, b) => a - b);
|
|
1414
|
+
const from = days[0];
|
|
1415
|
+
const to = days[days.length - 1];
|
|
1416
|
+
if (to - from <= MAX_SPAN_DAYS) return { from, to };
|
|
1417
|
+
const mid = days[Math.floor(days.length / 2)];
|
|
1418
|
+
const lo = Math.max(from, Math.min(mid - MAX_SPAN_DAYS / 2, to - MAX_SPAN_DAYS));
|
|
1419
|
+
return { from: lo, to: lo + MAX_SPAN_DAYS };
|
|
1420
|
+
}
|
|
1421
|
+
var MAX_SPAN_DAYS = Math.round(15 * 365.25);
|
|
1422
|
+
function pct(v) {
|
|
1423
|
+
const n = typeof v === "number" ? v : Number(v);
|
|
1424
|
+
return Number.isFinite(n) ? Math.max(0, Math.min(100, n)) : 0;
|
|
1425
|
+
}
|
|
1426
|
+
function barGeometry(start, end, scale) {
|
|
1427
|
+
if (Number.isNaN(start) || Number.isNaN(end)) return null;
|
|
1428
|
+
const left = scale.x(start);
|
|
1429
|
+
return { left, width: Math.max(scale.x(end + 1) - left, scale.dayWidth) };
|
|
1430
|
+
}
|
|
1431
|
+
function dependencyLinks(rows, scale, rowH = ROW_H, window2) {
|
|
1432
|
+
const index = new Map(rows.map((r) => [r.task.id, r]));
|
|
1433
|
+
const out = [];
|
|
1434
|
+
const r1 = (n) => Math.round(n * 10) / 10;
|
|
1435
|
+
for (const to of rows) {
|
|
1436
|
+
for (const fromId of to.task.dependsOn ?? []) {
|
|
1437
|
+
const from = index.get(fromId);
|
|
1438
|
+
if (!from || Number.isNaN(from.end) || Number.isNaN(to.start)) continue;
|
|
1439
|
+
if (window2 && (Math.min(from.index, to.index) > window2.last || Math.max(from.index, to.index) < window2.first)) continue;
|
|
1440
|
+
const fromMs = from.kind === "milestone";
|
|
1441
|
+
const toMs = to.kind === "milestone";
|
|
1442
|
+
const x1 = r1(fromMs ? scale.x(from.end) + scale.dayWidth / 2 + 7 : scale.x(from.end + 1));
|
|
1443
|
+
const x2 = r1(toMs ? scale.x(to.start) + scale.dayWidth / 2 - 8 : scale.x(to.start));
|
|
1444
|
+
const y1 = from.index * rowH + rowH / 2;
|
|
1445
|
+
const y2 = to.index * rowH + rowH / 2;
|
|
1446
|
+
let d;
|
|
1447
|
+
let hit;
|
|
1448
|
+
const away = y1 + (y2 >= y1 ? 10 : -10);
|
|
1449
|
+
if (x2 >= x1 + 12) {
|
|
1450
|
+
d = `M${x1} ${y1}H${r1(x1 + 6)}V${y2}H${x2}`;
|
|
1451
|
+
hit = `M${r1(x1 + 6)} ${away}V${y2}H${r1(Math.max(x1 + 6, x2 - 10))}`;
|
|
1452
|
+
} else {
|
|
1453
|
+
const ym = y1 + (y2 > y1 ? rowH / 2 : -rowH / 2);
|
|
1454
|
+
d = `M${x1} ${y1}H${r1(x1 + 6)}V${ym}H${r1(x2 - 8)}V${y2}H${x2}`;
|
|
1455
|
+
hit = `M${r1(x1 + 6)} ${away}V${ym}H${r1(x2 - 8)}V${y2}`;
|
|
1456
|
+
}
|
|
1457
|
+
out.push({
|
|
1458
|
+
key: `${fromId}>${to.task.id}`,
|
|
1459
|
+
from: fromId,
|
|
1460
|
+
to: to.task.id,
|
|
1461
|
+
fromIndex: from.index,
|
|
1462
|
+
toIndex: to.index,
|
|
1463
|
+
d,
|
|
1464
|
+
arrow: `M${x2} ${y2}l-6 -4v8z`,
|
|
1465
|
+
hit
|
|
1466
|
+
});
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
return out;
|
|
1470
|
+
}
|
|
1471
|
+
function dependencyPaths(rows, scale, rowH = ROW_H) {
|
|
1472
|
+
const links = dependencyLinks(rows, scale, rowH);
|
|
1473
|
+
return { lines: links.map((l) => l.d).join(" "), arrows: links.map((l) => l.arrow).join(" ") };
|
|
1474
|
+
}
|
|
1475
|
+
var TIP_GAP = 14;
|
|
1476
|
+
function placeTip(bar, rowTop, view, w, h, rowH = ROW_H) {
|
|
1477
|
+
let left = bar.left + bar.width + TIP_GAP;
|
|
1478
|
+
if (left + w > view.right - 8) {
|
|
1479
|
+
const before = bar.left - w - TIP_GAP;
|
|
1480
|
+
left = before >= view.left + 8 ? before : Math.max(view.left + 8, view.right - 8 - w);
|
|
1481
|
+
}
|
|
1482
|
+
if (left < view.left + 8) left = view.left + 8;
|
|
1483
|
+
let top = rowTop - 4;
|
|
1484
|
+
if (top + h > view.bottom - 4) top = rowTop + rowH - h;
|
|
1485
|
+
top = Math.max(top, view.top + 4);
|
|
1486
|
+
return { left, top };
|
|
1487
|
+
}
|
|
1488
|
+
var MINOR_CHAR_W = 6.6;
|
|
1489
|
+
var TAG_CHAR_W = 6;
|
|
1490
|
+
var TAG_PAD_X = 6;
|
|
1491
|
+
function todayTagWidth(label, textScale = 1) {
|
|
1492
|
+
return label.length * TAG_CHAR_W * textScale + TAG_PAD_X * 2;
|
|
1493
|
+
}
|
|
1494
|
+
function ticksUnderTag(ticks, centerX, tagWidth, gap = 2, textScale = 1) {
|
|
1495
|
+
const left = centerX - tagWidth / 2 - gap;
|
|
1496
|
+
const right = centerX + tagWidth / 2 + gap;
|
|
1497
|
+
const out = /* @__PURE__ */ new Set();
|
|
1498
|
+
ticks.forEach((t, i) => {
|
|
1499
|
+
const a = t.x + 4;
|
|
1500
|
+
const b = a + t.label.length * MINOR_CHAR_W * textScale;
|
|
1501
|
+
if (b > left && a < right) out.add(i);
|
|
1502
|
+
});
|
|
1503
|
+
return out;
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
// src/styles.ts
|
|
1507
|
+
var css = `
|
|
1508
|
+
.nf-gantt{--nf-row:${ROW_H}px;--nf-head:${HEADER_H}px;position:relative;isolation:isolate;display:flex;flex-direction:column;width:100%;box-sizing:border-box;min-height:0;height:100%;box-sizing:border-box;background:var(--nf-surface);color:var(--nf-ink);font-family:var(--nf-font);font-size:var(--nf-text-body);line-height:1.3;border:1px solid var(--nf-line);border-radius:16px;overflow:hidden;-webkit-font-smoothing:antialiased}
|
|
1509
|
+
.nf-gantt *,.nf-gantt *::before,.nf-gantt *::after{box-sizing:border-box}
|
|
1510
|
+
.nf-g-toolbar{display:flex;align-items:center;gap:10px;flex-wrap:wrap;min-height:60px;padding:10px 16px;border-bottom:1px solid var(--nf-line)}
|
|
1511
|
+
.nf-g-title{font-weight:600;font-size:var(--nf-text-title);letter-spacing:-0.01em;margin-inline-end:6px}
|
|
1512
|
+
.nf-g-seg{display:inline-flex;gap:2px;padding:3px;border-radius:9px;background:var(--nf-line-soft)}
|
|
1513
|
+
.nf-g-btn{font:inherit;font-size:var(--nf-text-body);border:1px solid var(--nf-line);background:var(--nf-surface);color:var(--nf-ink);padding:7px 12px;border-radius:9px;cursor:pointer;min-height:32px}
|
|
1514
|
+
.nf-g-btn:hover{background:var(--nf-line-soft)}
|
|
1515
|
+
.nf-g-seg .nf-g-btn{border:0;background:transparent;color:var(--nf-muted);padding:6px 12px;border-radius:7px}
|
|
1516
|
+
.nf-g-seg .nf-g-btn[aria-pressed="true"]{background:var(--nf-surface);color:var(--nf-ink);font-weight:500;box-shadow:0 1px 2px rgba(14,17,22,.12)}
|
|
1517
|
+
.nf-g-btn:focus-visible,.nf-g-scroll:focus-visible,.nf-g-chev:focus-visible{outline:2px solid var(--nf-focus);outline-offset:2px}
|
|
1518
|
+
.nf-g-switch{display:inline-flex;align-items:center;gap:8px;font:inherit;font-size:var(--nf-text-body);color:var(--nf-ink);background:none;border:0;padding:6px 4px;cursor:pointer;border-radius:8px}
|
|
1519
|
+
.nf-g-switch-track{position:relative;width:30px;height:18px;border-radius:9px;background:var(--nf-line)}
|
|
1520
|
+
.nf-g-switch-track::after{content:"";position:absolute;top:2px;left:2px;width:14px;height:14px;border-radius:7px;background:#fff;transition:transform ${motion.base} ${motion.ease}}
|
|
1521
|
+
.nf-g-switch[aria-pressed="true"] .nf-g-switch-track{background:var(--nf-accent)}
|
|
1522
|
+
.nf-g-switch[aria-pressed="true"] .nf-g-switch-track::after{transform:translateX(12px)}
|
|
1523
|
+
.nf-g-spacer{flex:1}
|
|
1524
|
+
.nf-g-scroll{position:relative;flex:1;min-height:0;overflow:auto;outline:none}
|
|
1525
|
+
.nf-g-head{position:sticky;top:0;z-index:4;display:flex;height:var(--nf-head);background:var(--nf-surface);border-bottom:1px solid var(--nf-line)}
|
|
1526
|
+
.nf-g-cols{position:sticky;left:0;z-index:5;flex:none;display:grid;align-items:end;padding:0 16px 10px;background:var(--nf-surface);border-right:1px solid var(--nf-line);font-family:var(--nf-mono-font);font-size:var(--nf-text-caption);letter-spacing:.06em;color:var(--nf-muted);text-transform:uppercase}
|
|
1527
|
+
.nf-g-cols>:not(:first-child){text-align:end}
|
|
1528
|
+
.nf-g-colhead{position:relative}
|
|
1529
|
+
.nf-g-colresize{position:absolute;z-index:1;top:-8px;bottom:-10px;inset-inline-end:-6px;width:8px;cursor:col-resize;touch-action:none}
|
|
1530
|
+
.nf-g-colresize:hover{background:var(--nf-line)}
|
|
1531
|
+
.nf-g-divider{position:absolute;top:0;bottom:0;right:-4px;width:8px;cursor:col-resize;touch-action:none;z-index:1;outline:none}
|
|
1532
|
+
.nf-g-divider:hover,.nf-g-divider:focus-visible{background:var(--nf-accent);opacity:.5}
|
|
1533
|
+
.nf-g-filter{font:inherit;font-size:var(--nf-text-body);min-height:32px;width:calc(var(--nf-text-body) * 180 / 13);max-width:40vw;padding:6px 10px;border-radius:9px;border:1px solid var(--nf-line);background:var(--nf-surface);color:var(--nf-ink)}
|
|
1534
|
+
.nf-g-filter:focus-visible{outline:2px solid var(--nf-focus);outline-offset:1px}
|
|
1535
|
+
.nf-g-menuwrap{position:relative}
|
|
1536
|
+
.nf-g-colmenu{top:calc(100% + 6px);inset-inline-end:0;inset-inline-start:auto}
|
|
1537
|
+
.nf-g-check{display:inline-block;width:16px;color:var(--nf-accent);font-weight:700}
|
|
1538
|
+
.nf-g-inline{font:inherit;font-size:var(--nf-text-body);width:100%;min-width:0;padding:3px 6px;border-radius:6px;border:1.5px solid var(--nf-accent);background:var(--nf-surface);color:var(--nf-ink);outline:none}
|
|
1539
|
+
.nf-g-datecell .nf-g-inline{font-family:var(--nf-mono-font);font-size:var(--nf-text-caption);padding:2px 2px}
|
|
1540
|
+
.nf-g-datecell[data-editable]{cursor:text}
|
|
1541
|
+
.nf-g-row[aria-selected="true"] .nf-g-name>span[title]{cursor:text}
|
|
1542
|
+
.nf-g-axis{position:relative;flex:none}
|
|
1543
|
+
.nf-g-major{position:absolute;top:0;height:calc(var(--nf-text-label) * 26 / 12);padding:8px 10px 0;border-left:1px solid var(--nf-line);font-size:var(--nf-text-label);font-weight:600;white-space:nowrap;clip-path:inset(0)}
|
|
1544
|
+
.nf-g-major-label{position:sticky;display:inline-block}
|
|
1545
|
+
.nf-g-minor{position:absolute;top:calc(var(--nf-text-label) * 30 / 12);padding-left:4px;font-family:var(--nf-mono-font);font-size:var(--nf-text-caption);color:var(--nf-muted);white-space:nowrap}
|
|
1546
|
+
.nf-g-today-tag{position:absolute;top:calc(var(--nf-text-label) * 30 / 12);transform:translateX(-50%);padding:2px 6px;border-radius:5px;background:var(--nf-accent-solid,var(--nf-accent));color:var(--nf-accent-text);font-family:var(--nf-mono-font);font-size:var(--nf-text-caption);font-weight:500;white-space:nowrap;z-index:1}
|
|
1547
|
+
.nf-g-body{position:relative}
|
|
1548
|
+
.nf-g-layer{position:absolute;top:0;pointer-events:none}
|
|
1549
|
+
.nf-g-vline{position:absolute;top:0;bottom:0;width:1px;background:var(--nf-line-soft)}
|
|
1550
|
+
.nf-g-weekend{position:absolute;top:0;bottom:0;background:var(--nf-surface-alt);opacity:.6}
|
|
1551
|
+
.nf-g-band{position:absolute;left:0;right:0;height:var(--nf-row);background:var(--nf-selected)}
|
|
1552
|
+
.nf-g-today{position:absolute;top:0;bottom:0;width:2px;margin-left:-1px;background:var(--nf-accent)}
|
|
1553
|
+
.nf-g-row{position:absolute;left:0;display:flex;height:var(--nf-row);cursor:default}
|
|
1554
|
+
.nf-g-addwrap{position:absolute;left:0;height:var(--nf-row)}
|
|
1555
|
+
.nf-g-settled .nf-g-bar,.nf-g-settled .nf-g-phase,.nf-g-settled .nf-g-ms{animation:none}
|
|
1556
|
+
.nf-g-row[data-dim] .nf-g-lane>*,.nf-g-row[data-dim] .nf-g-name{opacity:${HOST_DIM}}
|
|
1557
|
+
.nf-g-cell{position:sticky;left:0;z-index:3;flex:none;display:grid;align-items:center;padding:0 16px;background:var(--nf-surface);border-right:1px solid var(--nf-line);border-bottom:1px solid var(--nf-line-soft)}
|
|
1558
|
+
.nf-g-row[aria-selected="true"] .nf-g-cell{background:var(--nf-selected)}
|
|
1559
|
+
.nf-g-name{display:flex;align-items:center;gap:8px;min-width:0;white-space:nowrap}
|
|
1560
|
+
.nf-g-name span{overflow:hidden;text-overflow:ellipsis}
|
|
1561
|
+
.nf-g-row[data-kind="phase"] .nf-g-name{font-weight:600}
|
|
1562
|
+
.nf-g-chev{flex:none;display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;margin-inline-start:-4px;border:0;border-radius:5px;background:none;color:var(--nf-ink);cursor:pointer;padding:0}
|
|
1563
|
+
.nf-g-chev:hover{background:var(--nf-line-soft)}
|
|
1564
|
+
.nf-g-chev svg{transition:transform ${motion.fast} ${motion.ease}}
|
|
1565
|
+
.nf-g-chev[aria-expanded="false"] svg{transform:rotate(-90deg)}
|
|
1566
|
+
.nf-g-dot{flex:none;width:8px;height:8px;border-radius:2px;background:var(--nf-milestone);transform:rotate(45deg)}
|
|
1567
|
+
.nf-gantt[data-hc] .nf-g-avatar{background:var(--nf-surface);color:var(--nf-ink);box-shadow:inset 0 0 0 1px var(--nf-ink)}
|
|
1568
|
+
.nf-g-avatar{justify-self:end;width:calc(var(--nf-text-caption) * 26 / 11);height:calc(var(--nf-text-caption) * 26 / 11);border-radius:50%;display:inline-flex;align-items:center;justify-content:center;font-size:var(--nf-text-caption);font-weight:600;background:var(--nf-accent-tint);color:var(--nf-ink)}
|
|
1569
|
+
.nf-g-num{text-align:end;font-family:var(--nf-mono-font);font-size:var(--nf-text-value);color:var(--nf-muted)}
|
|
1570
|
+
.nf-g-lane{position:relative;flex:none;border-bottom:1px solid var(--nf-line-soft);user-select:none}
|
|
1571
|
+
.nf-g-bar{position:absolute;top:calc((var(--nf-row) - 20px) / 2);height:20px;border-radius:6px;background:var(--nf-accent-tint);overflow:hidden;transform-origin:left center;animation:nf-g-grow ${motion.base} ${motion.ease} both;cursor:pointer}
|
|
1572
|
+
.nf-g-bar::after{content:"";position:absolute;inset:0;border-radius:inherit;border:1px solid var(--nf-accent);pointer-events:none}
|
|
1573
|
+
.nf-g-fill{height:100%;background:var(--nf-accent)}
|
|
1574
|
+
.nf-g-row[aria-selected="true"] .nf-g-bar{box-shadow:0 0 0 2px var(--nf-surface),0 0 0 4px var(--nf-accent)}
|
|
1575
|
+
.nf-g-phase{position:absolute;top:calc((var(--nf-row) - 8px) / 2);height:8px;border-radius:2px;background:var(--nf-phase);transform-origin:left center;animation:nf-g-grow ${motion.base} ${motion.ease} both}
|
|
1576
|
+
.nf-g-ms{position:absolute;top:calc((var(--nf-row) - 14px) / 2);width:14px;height:14px;margin-left:-7px;border-radius:3px;background:var(--nf-milestone);border:1.5px solid var(--nf-milestone-edge,color-mix(in srgb,var(--nf-milestone) 55%,var(--nf-ink)));box-sizing:border-box;transform:rotate(45deg);animation:nf-g-pop ${motion.base} ${motion.ease} both;cursor:pointer}
|
|
1577
|
+
.nf-g-base{position:absolute;top:calc((var(--nf-row) - 20px) / 2 + 22px);height:3px;border-radius:2px;background:var(--nf-baseline)}
|
|
1578
|
+
.nf-g-deps{z-index:2;pointer-events:none;overflow:visible}
|
|
1579
|
+
.nf-g-deps path{fill:none;stroke:var(--nf-muted);stroke-width:1.4;opacity:.75}
|
|
1580
|
+
.nf-g-deps path.nf-g-arrow{fill:var(--nf-muted);stroke:none}
|
|
1581
|
+
.nf-g-deps .is-broken path{stroke:var(--nf-negative);opacity:1}
|
|
1582
|
+
.nf-g-deps .is-broken path.nf-g-arrow{fill:var(--nf-negative);stroke:none}
|
|
1583
|
+
.nf-g-deps .is-selected path{stroke:var(--nf-accent);stroke-width:2.4;opacity:1}
|
|
1584
|
+
.nf-g-deps .is-selected path.nf-g-arrow{fill:var(--nf-accent);stroke:none}
|
|
1585
|
+
.nf-g-deps path.nf-g-hit{stroke:transparent;stroke-width:10;opacity:1;pointer-events:stroke;cursor:pointer}
|
|
1586
|
+
.nf-g-deps path.nf-g-linkline{stroke:var(--nf-accent);stroke-width:2;stroke-dasharray:4 3;opacity:1}
|
|
1587
|
+
.nf-g-bar[data-editable]{cursor:grab}
|
|
1588
|
+
.nf-g-ms[data-editable]{cursor:grab}
|
|
1589
|
+
.nf-g-bar[data-locked],.nf-g-ms[data-locked]{cursor:not-allowed}
|
|
1590
|
+
.nf-g-dragging,.nf-g-dragging *{cursor:grabbing!important;user-select:none}
|
|
1591
|
+
.nf-g-h{position:absolute;top:0;bottom:0;width:8px;cursor:ew-resize;touch-action:none}
|
|
1592
|
+
.nf-g-h-l{left:0}
|
|
1593
|
+
.nf-g-h-r{right:0}
|
|
1594
|
+
.nf-g-knob{position:absolute;top:2px;bottom:2px;width:8px;border-radius:4px;background:var(--nf-surface);box-shadow:0 0 0 1.5px var(--nf-accent);cursor:ew-resize;opacity:0;transition:opacity ${motion.fast} ${motion.ease};touch-action:none}
|
|
1595
|
+
.nf-g-row:hover .nf-g-knob,.nf-g-row[aria-selected="true"] .nf-g-knob{opacity:1}
|
|
1596
|
+
.nf-g-bar,.nf-g-ms,.nf-g-linkdot{touch-action:none}
|
|
1597
|
+
.nf-g-linkdot{position:absolute;top:calc((var(--nf-row) - 10px) / 2);width:10px;height:10px;border-radius:5px;background:var(--nf-surface);box-shadow:0 0 0 2px var(--nf-accent);cursor:crosshair;opacity:0;transition:opacity ${motion.fast} ${motion.ease};z-index:1}
|
|
1598
|
+
.nf-g-row:hover .nf-g-linkdot,.nf-g-row[aria-selected="true"] .nf-g-linkdot{opacity:1}
|
|
1599
|
+
.nf-g-linktarget .nf-g-bar,.nf-g-linktarget .nf-g-ms{box-shadow:0 0 0 2px var(--nf-surface),0 0 0 4px var(--nf-accent)}
|
|
1600
|
+
.nf-g-linksource .nf-g-bar{box-shadow:0 0 0 2px var(--nf-surface),0 0 0 4px var(--nf-milestone)}
|
|
1601
|
+
.nf-g-ghost{position:absolute;top:calc((var(--nf-row) - 20px) / 2);height:20px;border-radius:6px;border:1.5px dashed var(--nf-muted);opacity:.6;pointer-events:none}
|
|
1602
|
+
.nf-g-draglabel{position:absolute;z-index:6;padding:4px 8px;border-radius:7px;background:var(--nf-tooltip-bg);color:var(--nf-tooltip-ink);font-family:var(--nf-mono-font);font-size:var(--nf-text-caption);white-space:nowrap;pointer-events:none}
|
|
1603
|
+
.nf-g-rowadd{flex:none;margin-left:auto;width:22px;height:22px;border:0;border-radius:6px;background:none;color:var(--nf-muted);font:inherit;font-size:var(--nf-text-title);line-height:1;cursor:pointer;opacity:0;transition:opacity ${motion.fast} ${motion.ease}}
|
|
1604
|
+
.nf-g-row:hover .nf-g-rowadd,.nf-g-row[aria-selected="true"] .nf-g-rowadd{opacity:1}
|
|
1605
|
+
.nf-g-rowadd:hover{background:var(--nf-line-soft);color:var(--nf-ink)}
|
|
1606
|
+
.nf-g-addrow{position:sticky;left:0;z-index:3;height:var(--nf-row);display:flex;align-items:center;padding:0 10px;background:var(--nf-surface);border-right:1px solid var(--nf-line)}
|
|
1607
|
+
.nf-g-addbtn{font:inherit;font-size:var(--nf-text-body);color:var(--nf-muted);background:none;border:0;padding:6px 8px;border-radius:7px;cursor:pointer}
|
|
1608
|
+
.nf-g-addbtn:hover{background:var(--nf-line-soft);color:var(--nf-ink)}
|
|
1609
|
+
.nf-g-addbtn:focus-visible,.nf-g-rowadd:focus-visible,.nf-g-menu button:focus-visible,.nf-g-toast button:focus-visible{outline:2px solid var(--nf-focus);outline-offset:2px}
|
|
1610
|
+
.nf-g-menu{position:absolute;z-index:9;min-width:200px;padding:4px;border-radius:10px;background:var(--nf-surface);border:1px solid var(--nf-line);box-shadow:0 12px 28px rgba(14,17,22,.18);display:flex;flex-direction:column;animation:nf-g-fade ${motion.fast} ${motion.ease} both}
|
|
1611
|
+
.nf-g-menu button{display:flex;justify-content:space-between;gap:16px;font:inherit;font-size:var(--nf-text-body);text-align:left;color:var(--nf-ink);background:none;border:0;padding:8px 10px;border-radius:7px;cursor:pointer}
|
|
1612
|
+
.nf-g-menu button:hover{background:var(--nf-line-soft)}
|
|
1613
|
+
.nf-g-menu kbd{font-family:var(--nf-mono-font);font-size:var(--nf-text-caption);color:var(--nf-muted)}
|
|
1614
|
+
.nf-g-confirm{position:absolute;z-index:8;display:flex;align-items:center;gap:8px;padding:8px 8px 8px 12px;border-radius:10px;background:var(--nf-surface);border:1px solid var(--nf-line);box-shadow:0 12px 28px rgba(14,17,22,.18);font-size:var(--nf-text-body);white-space:nowrap;animation:nf-g-fade ${motion.fast} ${motion.ease} both}
|
|
1615
|
+
.nf-g-primary{background:var(--nf-accent-solid,var(--nf-accent));border-color:var(--nf-accent-solid,var(--nf-accent));color:var(--nf-accent-text)}
|
|
1616
|
+
.nf-g-primary:hover{background:var(--nf-accent);opacity:.9}
|
|
1617
|
+
.nf-g-toasts{position:absolute;left:12px;bottom:12px;z-index:9;display:flex;flex-direction:column;gap:8px;max-width:min(440px,calc(100% - 200px))}
|
|
1618
|
+
.nf-g-toast{display:flex;align-items:center;gap:10px;padding:8px 8px 8px 12px;border-radius:10px;background:var(--nf-tooltip-bg);color:var(--nf-tooltip-ink);box-shadow:0 8px 24px rgba(14,17,22,.24);font-size:var(--nf-text-body);animation:nf-g-fade ${motion.fast} ${motion.ease} both}
|
|
1619
|
+
.nf-g-toast[data-tone="error"]{background:var(--nf-negative);color:#fff}
|
|
1620
|
+
.nf-g-toast-text{flex:1;min-width:0}
|
|
1621
|
+
.nf-g-toast-btn{font:inherit;font-size:var(--nf-text-body);font-weight:600;color:inherit;background:rgba(255,255,255,.14);border:0;padding:5px 10px;border-radius:7px;cursor:pointer}
|
|
1622
|
+
.nf-g-toast-x{font:inherit;font-size:var(--nf-text-title);line-height:1;color:inherit;opacity:.7;background:none;border:0;padding:4px 6px;border-radius:6px;cursor:pointer}
|
|
1623
|
+
.nf-g-sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap}
|
|
1624
|
+
|
|
1625
|
+
.nf-g-tip{position:absolute;z-index:6;width:var(--nf-tip-w,264px);padding:12px 14px;border-radius:12px;background:var(--nf-tooltip-bg);color:var(--nf-tooltip-ink);box-shadow:0 12px 28px rgba(14,17,22,.24);display:flex;flex-direction:column;gap:6px;pointer-events:none;animation:nf-g-fade ${motion.fast} ${motion.ease} both}
|
|
1626
|
+
.nf-g-tip b{font-size:var(--nf-text-body);font-weight:600}
|
|
1627
|
+
.nf-g-tip .nf-g-meta{font-family:var(--nf-mono-font);font-size:var(--nf-text-value);opacity:.75}
|
|
1628
|
+
.nf-g-tip .nf-g-warn{display:flex;align-items:center;gap:6px;font-size:var(--nf-text-callout);color:var(--nf-tip-warn,var(--nf-milestone))}
|
|
1629
|
+
.nf-g-tip .nf-g-warn::before{content:"";width:6px;height:6px;border-radius:3px;background:currentColor}
|
|
1630
|
+
.nf-g-tip .nf-g-warn[data-tone="danger"]{color:var(--nf-tip-danger,var(--nf-negative))}
|
|
1631
|
+
.nf-g-empty{padding:48px 16px;text-align:center;color:var(--nf-muted)}.nf-g-emptyaction{display:flex;flex-direction:column;align-items:center;gap:8px;margin-top:12px;color:var(--nf-muted);font-size:var(--nf-text-body)}.nf-g-emptyaction small{font-size:var(--nf-text-callout);max-width:360px}.nf-g-emptyaction [role="alert"]{color:var(--nf-negative,#D93F2B)}
|
|
1632
|
+
.nf-g-mark{position:absolute;right:12px;bottom:12px;z-index:7;display:inline-flex;align-items:center;gap:6px;padding:5px 9px 5px 6px;border-radius:8px;background:var(--nf-surface);border:1px solid var(--nf-line);box-shadow:0 2px 8px rgba(14,17,22,.08);font-size:var(--nf-text-caption);color:var(--nf-muted);text-decoration:none;font-family:inherit;cursor:pointer}
|
|
1633
|
+
.nf-g-mark:hover{color:var(--nf-ink)}
|
|
1634
|
+
.nf-g-renew{position:absolute;right:12px;bottom:12px;z-index:7;display:inline-flex;align-items:center;gap:6px;padding:5px 9px;border-radius:8px;background:var(--nf-surface);border:1px solid var(--nf-line);box-shadow:0 2px 8px rgba(14,17,22,.08);font-size:var(--nf-text-caption);color:var(--nf-muted);text-decoration:none}
|
|
1635
|
+
.nf-g-renew:hover{color:var(--nf-ink)}
|
|
1636
|
+
.nf-g-locknote{position:absolute;left:12px;bottom:12px;z-index:7;margin:0;max-width:calc(100% - 220px);padding:5px 9px;border-radius:8px;background:var(--nf-surface);border:1px solid var(--nf-line);font-size:var(--nf-text-caption);color:var(--nf-muted)}
|
|
1637
|
+
.nf-g-key{margin-inline-start:6px;vertical-align:-1px;opacity:.7}
|
|
1638
|
+
.nf-g-loading{position:absolute;left:0;right:0;z-index:3;pointer-events:none}
|
|
1639
|
+
.nf-g-renew:focus-visible{outline:2px solid var(--nf-focus);outline-offset:2px}
|
|
1640
|
+
.nf-g-renew::before{content:"";width:6px;height:6px;border-radius:3px;background:var(--nf-milestone)}
|
|
1641
|
+
@keyframes nf-g-grow{from{transform:scaleX(0);opacity:0}to{transform:scaleX(1);opacity:1}}
|
|
1642
|
+
@keyframes nf-g-pop{from{transform:rotate(45deg) scale(0)}to{transform:rotate(45deg) scale(1)}}
|
|
1643
|
+
@keyframes nf-g-fade{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}
|
|
1644
|
+
@media (prefers-reduced-motion: reduce){.nf-gantt *{animation:none!important;transition:none!important}}
|
|
1645
|
+
.nf-gantt[data-static] *{animation:none!important;transition:none!important}
|
|
1646
|
+
.nf-g-switch:focus-visible,.nf-g-mark:focus-visible,.nf-g-primary:focus-visible,.nf-g-divider:focus-visible{outline:2px solid var(--nf-focus);outline-offset:2px}
|
|
1647
|
+
.nf-gantt[data-rtl] .nf-g-chev[aria-expanded="false"] svg{transform:rotate(90deg)}
|
|
1648
|
+
@media (forced-colors: active){
|
|
1649
|
+
.nf-gantt{border-color:CanvasText}
|
|
1650
|
+
.nf-g-bar,.nf-g-ms,.nf-g-phase,.nf-g-fill,.nf-g-base,.nf-g-today,.nf-g-today-tag,.nf-g-knob,.nf-g-linkdot,.nf-g-switch-track,.nf-g-switch-track::after,.nf-g-avatar,.nf-g-dot,.nf-g-ghost,.nf-g-band,.nf-g-weekend{forced-color-adjust:none}
|
|
1651
|
+
.nf-g-bar{background:Canvas;border:1px solid CanvasText}
|
|
1652
|
+
.nf-g-bar::after{display:none}
|
|
1653
|
+
.nf-g-ms{border-color:CanvasText}
|
|
1654
|
+
.nf-g-fill{background:Highlight}
|
|
1655
|
+
.nf-g-phase,.nf-g-ms,.nf-g-base{background:CanvasText}
|
|
1656
|
+
.nf-g-today{background:Highlight}
|
|
1657
|
+
.nf-g-today-tag{background:Highlight;color:HighlightText}
|
|
1658
|
+
.nf-g-knob,.nf-g-linkdot{background:Canvas;border-color:CanvasText}
|
|
1659
|
+
.nf-g-avatar,.nf-g-dot{background:Canvas;color:CanvasText;border:1px solid CanvasText}
|
|
1660
|
+
.nf-g-ghost{background:transparent;border:1px dashed CanvasText}
|
|
1661
|
+
.nf-g-band{background:transparent}
|
|
1662
|
+
.nf-g-weekend{background:transparent}
|
|
1663
|
+
.nf-g-switch-track{background:Canvas;border:1px solid CanvasText}
|
|
1664
|
+
.nf-g-switch-track::after{background:CanvasText}
|
|
1665
|
+
.nf-g-switch[aria-pressed="true"] .nf-g-switch-track{background:Highlight}
|
|
1666
|
+
.nf-g-row[aria-selected="true"] .nf-g-cell{outline:2px solid Highlight;outline-offset:-2px}
|
|
1667
|
+
.nf-g-row[aria-selected="true"] .nf-g-bar,.nf-g-linktarget .nf-g-bar,.nf-g-linktarget .nf-g-ms,.nf-g-linksource .nf-g-bar{box-shadow:none;outline:2px solid Highlight;outline-offset:2px}
|
|
1668
|
+
.nf-g-deps path{stroke:CanvasText}
|
|
1669
|
+
.nf-g-deps path.nf-g-arrow{fill:CanvasText}
|
|
1670
|
+
.nf-g-deps .is-broken path{stroke:CanvasText;stroke-dasharray:4 3}
|
|
1671
|
+
.nf-g-deps .is-broken path.nf-g-arrow{fill:CanvasText;stroke:none}
|
|
1672
|
+
.nf-g-deps .is-selected path{stroke:Highlight}
|
|
1673
|
+
.nf-g-deps .is-selected path.nf-g-arrow{fill:Highlight}
|
|
1674
|
+
.nf-g-deps path.nf-g-hit{stroke:transparent}
|
|
1675
|
+
.nf-gantt :focus-visible{outline:2px solid Highlight}
|
|
1676
|
+
.nf-g-tip,.nf-g-toast,.nf-g-menu,.nf-g-confirm,.nf-g-draglabel,.nf-g-renew{border:1px solid CanvasText}
|
|
1677
|
+
.nf-g-seg .nf-g-btn[aria-pressed="true"]{forced-color-adjust:none;background:Highlight;color:HighlightText}
|
|
1678
|
+
}
|
|
1679
|
+
`;
|
|
1680
|
+
|
|
1681
|
+
// src/exportPng.ts
|
|
1682
|
+
var MAX_CANVAS_SIDE = 16e3;
|
|
1683
|
+
var MAX_CANVAS_AREA = 12e7;
|
|
1684
|
+
var TITLE_H = 44;
|
|
1685
|
+
function pngLayout(rowCount, gridWidth, timelineWidth, preferredRatio = 2) {
|
|
1686
|
+
const width = Math.ceil(gridWidth + timelineWidth);
|
|
1687
|
+
const height = TITLE_H + HEADER_H + Math.max(1, rowCount) * ROW_H;
|
|
1688
|
+
const ratio2 = Math.min(preferredRatio, MAX_CANVAS_SIDE / width, MAX_CANVAS_SIDE / height, Math.sqrt(MAX_CANVAS_AREA / (width * height)));
|
|
1689
|
+
return ratio2 >= 0.5 ? { width, height, ratio: ratio2 } : null;
|
|
1690
|
+
}
|
|
1691
|
+
function pngFileName(title, isoDay) {
|
|
1692
|
+
return exportFileName(title, isoDay);
|
|
1693
|
+
}
|
|
1694
|
+
function solid(ctx, color, fallback) {
|
|
1695
|
+
ctx.fillStyle = fallback;
|
|
1696
|
+
ctx.fillStyle = color;
|
|
1697
|
+
return String(ctx.fillStyle);
|
|
1698
|
+
}
|
|
1699
|
+
function drawChart(ctx, layout, o) {
|
|
1700
|
+
const c = (k, fb = "#888") => solid(ctx, o.theme[k], fb);
|
|
1701
|
+
const T = {
|
|
1702
|
+
surface: c("surface", "#fff"),
|
|
1703
|
+
ink: c("ink", "#111"),
|
|
1704
|
+
muted: c("muted", "#666"),
|
|
1705
|
+
line: c("line", "#ddd"),
|
|
1706
|
+
lineSoft: c("lineSoft", "#eee"),
|
|
1707
|
+
accent: c("accent", "#2E54FF"),
|
|
1708
|
+
phase: c("phase", "#111"),
|
|
1709
|
+
milestone: c("milestone", "#d90"),
|
|
1710
|
+
baseline: c("baseline", "#bbb"),
|
|
1711
|
+
negative: c("negative", "#d33")
|
|
1712
|
+
};
|
|
1713
|
+
const font2 = 'Geist, system-ui, -apple-system, "Segoe UI", sans-serif';
|
|
1714
|
+
const mono = '"Geist Mono", ui-monospace, monospace';
|
|
1715
|
+
const { width, height, ratio: ratio2 } = layout;
|
|
1716
|
+
const { scale, gridWidth } = o;
|
|
1717
|
+
ctx.save();
|
|
1718
|
+
ctx.scale(ratio2, ratio2);
|
|
1719
|
+
ctx.fillStyle = T.surface;
|
|
1720
|
+
ctx.fillRect(0, 0, width, height);
|
|
1721
|
+
ctx.textBaseline = "middle";
|
|
1722
|
+
ctx.fillStyle = T.ink;
|
|
1723
|
+
ctx.font = `600 16px ${font2}`;
|
|
1724
|
+
ctx.fillText(o.title, 16, TITLE_H / 2);
|
|
1725
|
+
line(ctx, 0, TITLE_H - 0.5, width, TITLE_H - 0.5, T.line);
|
|
1726
|
+
const top = TITLE_H;
|
|
1727
|
+
ctx.font = `500 11px ${mono}`;
|
|
1728
|
+
ctx.fillStyle = T.muted;
|
|
1729
|
+
ctx.fillText("TASK", 16, top + HEADER_H - 16);
|
|
1730
|
+
for (const tk of scale.major) {
|
|
1731
|
+
ctx.fillStyle = T.ink;
|
|
1732
|
+
ctx.font = `500 12px ${font2}`;
|
|
1733
|
+
ctx.fillText(tk.label, gridWidth + tk.x + 8, top + 16);
|
|
1734
|
+
line(ctx, gridWidth + tk.x + 0.5, top, gridWidth + tk.x + 0.5, height, T.line);
|
|
1735
|
+
}
|
|
1736
|
+
ctx.font = `11px ${mono}`;
|
|
1737
|
+
ctx.fillStyle = T.muted;
|
|
1738
|
+
for (const tk of scale.minor) {
|
|
1739
|
+
if (tk.width >= 24) ctx.fillText(tk.label, gridWidth + tk.x + 4, top + HEADER_H - 14);
|
|
1740
|
+
}
|
|
1741
|
+
line(ctx, 0, top + HEADER_H - 0.5, width, top + HEADER_H - 0.5, T.line);
|
|
1742
|
+
line(ctx, gridWidth - 0.5, top, gridWidth - 0.5, height, T.line);
|
|
1743
|
+
const body = top + HEADER_H;
|
|
1744
|
+
ctx.fillStyle = T.lineSoft;
|
|
1745
|
+
for (const w of scale.weekends) ctx.fillRect(gridWidth + w.x, body, w.width, height - body);
|
|
1746
|
+
const tint = (alpha) => {
|
|
1747
|
+
ctx.globalAlpha = alpha;
|
|
1748
|
+
};
|
|
1749
|
+
for (const r of o.rows) {
|
|
1750
|
+
const y = body + r.index * ROW_H;
|
|
1751
|
+
line(ctx, 0, y + ROW_H - 0.5, width, y + ROW_H - 0.5, T.lineSoft);
|
|
1752
|
+
ctx.save();
|
|
1753
|
+
ctx.beginPath();
|
|
1754
|
+
ctx.rect(0, y, gridWidth - 8, ROW_H);
|
|
1755
|
+
ctx.clip();
|
|
1756
|
+
ctx.fillStyle = T.ink;
|
|
1757
|
+
ctx.font = `${r.kind === "phase" ? 600 : 400} 13px ${font2}`;
|
|
1758
|
+
ctx.fillText(r.task.name, 16 + r.depth * 18 + (r.hasChildren || r.depth > 0 ? 18 : 0), y + ROW_H / 2);
|
|
1759
|
+
ctx.restore();
|
|
1760
|
+
ctx.save();
|
|
1761
|
+
ctx.translate(gridWidth, y);
|
|
1762
|
+
if (o.showBaseline && !Number.isNaN(r.baselineStart) && !Number.isNaN(r.baselineEnd)) {
|
|
1763
|
+
const g = barGeometry(r.baselineStart, r.baselineEnd, scale);
|
|
1764
|
+
if (g) roundRect(ctx, g.left, 32, g.width, 3, 1.5, T.baseline);
|
|
1765
|
+
}
|
|
1766
|
+
if (r.kind === "milestone") {
|
|
1767
|
+
if (!Number.isNaN(r.start)) {
|
|
1768
|
+
const cx = scale.x(r.start) + scale.dayWidth / 2;
|
|
1769
|
+
ctx.save();
|
|
1770
|
+
ctx.translate(cx, ROW_H / 2);
|
|
1771
|
+
ctx.rotate(Math.PI / 4);
|
|
1772
|
+
roundRect(ctx, -7, -7, 14, 14, 3, T.milestone);
|
|
1773
|
+
ctx.restore();
|
|
1774
|
+
}
|
|
1775
|
+
} else {
|
|
1776
|
+
const g = barGeometry(r.start, r.end, scale);
|
|
1777
|
+
if (g && r.kind === "phase") roundRect(ctx, g.left, 16, g.width, 8, 2, T.phase);
|
|
1778
|
+
else if (g) {
|
|
1779
|
+
tint(0.22);
|
|
1780
|
+
roundRect(ctx, g.left, 10, g.width, 20, 6, T.accent);
|
|
1781
|
+
tint(1);
|
|
1782
|
+
if (r.progress > 0) {
|
|
1783
|
+
ctx.save();
|
|
1784
|
+
ctx.beginPath();
|
|
1785
|
+
ctx.rect(g.left, 10, g.width * r.progress / 100, 20);
|
|
1786
|
+
ctx.clip();
|
|
1787
|
+
roundRect(ctx, g.left, 10, g.width, 20, 6, T.accent);
|
|
1788
|
+
ctx.restore();
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
ctx.restore();
|
|
1793
|
+
}
|
|
1794
|
+
if (typeof Path2D !== "undefined") {
|
|
1795
|
+
ctx.save();
|
|
1796
|
+
ctx.translate(gridWidth, body);
|
|
1797
|
+
ctx.lineWidth = 1.5;
|
|
1798
|
+
for (const l of dependencyLinks(o.rows, scale)) {
|
|
1799
|
+
const bad = o.broken.has(l.key);
|
|
1800
|
+
ctx.strokeStyle = bad ? T.negative : T.muted;
|
|
1801
|
+
ctx.fillStyle = bad ? T.negative : T.muted;
|
|
1802
|
+
ctx.stroke(new Path2D(l.d));
|
|
1803
|
+
ctx.fill(new Path2D(l.arrow));
|
|
1804
|
+
}
|
|
1805
|
+
ctx.restore();
|
|
1806
|
+
}
|
|
1807
|
+
if (!Number.isNaN(o.today) && o.today >= scale.startDay && o.today < scale.endDay) {
|
|
1808
|
+
const x = gridWidth + scale.x(o.today);
|
|
1809
|
+
ctx.fillStyle = T.accent;
|
|
1810
|
+
ctx.fillRect(x - 1, top, 2, height - top);
|
|
1811
|
+
ctx.font = `500 10px ${mono}`;
|
|
1812
|
+
const label = formatShort(o.today);
|
|
1813
|
+
const w = ctx.measureText(label).width + 12;
|
|
1814
|
+
roundRect(ctx, x - w / 2, top + HEADER_H - 22, w, 16, 5, solid(ctx, accentForText(T.accent).background, T.accent));
|
|
1815
|
+
ctx.fillStyle = solid(ctx, o.theme.accentText, "#fff");
|
|
1816
|
+
ctx.fillText(label, x - w / 2 + 6, top + HEADER_H - 14);
|
|
1817
|
+
}
|
|
1818
|
+
ctx.restore();
|
|
1819
|
+
}
|
|
1820
|
+
function line(ctx, x1, y1, x2, y2, color) {
|
|
1821
|
+
ctx.strokeStyle = color;
|
|
1822
|
+
ctx.lineWidth = 1;
|
|
1823
|
+
ctx.beginPath();
|
|
1824
|
+
ctx.moveTo(x1, y1);
|
|
1825
|
+
ctx.lineTo(x2, y2);
|
|
1826
|
+
ctx.stroke();
|
|
1827
|
+
}
|
|
1828
|
+
function roundRect(ctx, x, y, w, h, r, color) {
|
|
1829
|
+
const rr = Math.min(r, w / 2, h / 2);
|
|
1830
|
+
ctx.fillStyle = color;
|
|
1831
|
+
ctx.beginPath();
|
|
1832
|
+
ctx.moveTo(x + rr, y);
|
|
1833
|
+
ctx.arcTo(x + w, y, x + w, y + h, rr);
|
|
1834
|
+
ctx.arcTo(x + w, y + h, x, y + h, rr);
|
|
1835
|
+
ctx.arcTo(x, y + h, x, y, rr);
|
|
1836
|
+
ctx.arcTo(x, y, x + w, y, rr);
|
|
1837
|
+
ctx.closePath();
|
|
1838
|
+
ctx.fill();
|
|
1839
|
+
}
|
|
1840
|
+
function exportPng(o, fileName, doc = document) {
|
|
1841
|
+
const layout = pngLayout(o.rows.length, o.gridWidth, o.scale.width);
|
|
1842
|
+
if (!layout) return false;
|
|
1843
|
+
const canvas = doc.createElement("canvas");
|
|
1844
|
+
canvas.width = Math.round(layout.width * layout.ratio);
|
|
1845
|
+
canvas.height = Math.round(layout.height * layout.ratio);
|
|
1846
|
+
const ctx = canvas.getContext("2d");
|
|
1847
|
+
if (!ctx) return false;
|
|
1848
|
+
drawChart(ctx, layout, o);
|
|
1849
|
+
canvas.toBlob((blob) => {
|
|
1850
|
+
if (!blob) return;
|
|
1851
|
+
const url = URL.createObjectURL(blob);
|
|
1852
|
+
const a = doc.createElement("a");
|
|
1853
|
+
a.href = url;
|
|
1854
|
+
a.download = fileName;
|
|
1855
|
+
a.style.display = "none";
|
|
1856
|
+
doc.body.appendChild(a);
|
|
1857
|
+
a.click();
|
|
1858
|
+
a.remove();
|
|
1859
|
+
setTimeout(() => URL.revokeObjectURL(url), 1e4);
|
|
1860
|
+
}, "image/png");
|
|
1861
|
+
return true;
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
// src/print.ts
|
|
1865
|
+
var PRINT_TIMELINE_W = 560;
|
|
1866
|
+
var PRINT_ROW_H = 26;
|
|
1867
|
+
function printAxis(rows, width, today2 = NaN) {
|
|
1868
|
+
const range = rowsRange(rows);
|
|
1869
|
+
if (!range) return null;
|
|
1870
|
+
let from = range.from;
|
|
1871
|
+
let to = range.to;
|
|
1872
|
+
if (!Number.isNaN(today2) && today2 >= from - 14 && today2 <= to + 14) {
|
|
1873
|
+
from = Math.min(from, today2);
|
|
1874
|
+
to = Math.max(to, today2);
|
|
1875
|
+
}
|
|
1876
|
+
const short = to - from < 70;
|
|
1877
|
+
from = short ? startOfWeek(from) : startOfMonth(from);
|
|
1878
|
+
const end = short ? startOfWeek(to) + 7 : addMonths(startOfMonth(to), 1);
|
|
1879
|
+
const per = width / (end - from);
|
|
1880
|
+
const x = (d) => Math.round((d - from) * per * 10) / 10;
|
|
1881
|
+
const ticks = [];
|
|
1882
|
+
for (let d = from; d < end; d = short ? d + 7 : addMonths(d, 1)) {
|
|
1883
|
+
const p = parts(d);
|
|
1884
|
+
ticks.push({ x: x(d), label: short ? formatShort(d) : p.month === 0 || ticks.length === 0 ? `${MONTHS[p.month]} ${p.year}` : MONTHS[p.month] });
|
|
1885
|
+
}
|
|
1886
|
+
return { from, to: end, x, ticks };
|
|
1887
|
+
}
|
|
1888
|
+
function barSvg(r, axis, w, o) {
|
|
1889
|
+
const t = o.theme;
|
|
1890
|
+
const h = PRINT_ROW_H;
|
|
1891
|
+
const parts2 = [];
|
|
1892
|
+
for (const tk of axis.ticks) parts2.push(`<line x1="${tk.x}" x2="${tk.x}" y1="0" y2="${h}" stroke="${t.lineSoft}"/>`);
|
|
1893
|
+
if (!Number.isNaN(o.today) && o.today >= axis.from && o.today < axis.to) {
|
|
1894
|
+
const tx = axis.x(o.today) + 0.5;
|
|
1895
|
+
parts2.push(`<line x1="${tx}" x2="${tx}" y1="0" y2="${h}" stroke="${t.accent}" stroke-width="1.5"/>`);
|
|
1896
|
+
}
|
|
1897
|
+
if (o.showBaseline && !Number.isNaN(r.baselineStart) && !Number.isNaN(r.baselineEnd)) {
|
|
1898
|
+
const bx = axis.x(r.baselineStart);
|
|
1899
|
+
parts2.push(`<rect x="${bx}" y="${h - 5}" width="${Math.max(2, axis.x(r.baselineEnd + 1) - bx)}" height="2" rx="1" fill="${t.baseline}"/>`);
|
|
1900
|
+
}
|
|
1901
|
+
if (!Number.isNaN(r.start)) {
|
|
1902
|
+
const x = axis.x(r.start);
|
|
1903
|
+
const bw = Math.max(2, axis.x(r.end + 1) - x);
|
|
1904
|
+
if (r.kind === "milestone") {
|
|
1905
|
+
const cx = axis.x(r.start) + (axis.x(r.start + 1) - axis.x(r.start)) / 2;
|
|
1906
|
+
parts2.push(`<rect x="${cx - 5}" y="${h / 2 - 5}" width="10" height="10" rx="2" fill="${t.milestone}" transform="rotate(45 ${cx} ${h / 2})"/>`);
|
|
1907
|
+
} else if (r.kind === "phase") {
|
|
1908
|
+
parts2.push(`<rect x="${x}" y="${h / 2 - 3}" width="${bw}" height="6" rx="2" fill="${t.phase}"/>`);
|
|
1909
|
+
} else {
|
|
1910
|
+
parts2.push(`<rect x="${x}" y="${h / 2 - 7}" width="${bw}" height="14" rx="4" fill="${t.accent}" fill-opacity=".22"/>`);
|
|
1911
|
+
if (r.progress > 0)
|
|
1912
|
+
parts2.push(`<rect x="${x}" y="${h / 2 - 7}" width="${Math.round(bw * r.progress / 10) / 10}" height="14" rx="4" fill="${t.accent}"/>`);
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
return `<svg width="${w}" height="${h}" viewBox="0 0 ${w} ${h}" aria-hidden="true">${parts2.join("")}</svg>`;
|
|
1916
|
+
}
|
|
1917
|
+
function printHtml(o) {
|
|
1918
|
+
const t = o.theme;
|
|
1919
|
+
const w = o.timelineWidth ?? PRINT_TIMELINE_W;
|
|
1920
|
+
const axis = printAxis(o.rows, w, o.today);
|
|
1921
|
+
const e = escapeHtml;
|
|
1922
|
+
const head = axis ? `<svg width="${w}" height="22" viewBox="0 0 ${w} 22" aria-hidden="true">${axis.ticks.map((tk) => `<line x1="${tk.x}" x2="${tk.x}" y1="0" y2="22" stroke="${t.line}"/><text x="${tk.x + 4}" y="15">${e(tk.label)}</text>`).join("")}</svg>` : "";
|
|
1923
|
+
const body = o.rows.map((r) => {
|
|
1924
|
+
const isMs = r.kind === "milestone";
|
|
1925
|
+
const dates = Number.isNaN(r.start) ? "" : isMs ? formatShort(r.start) : `${formatShort(r.start)} \u2013 ${formatShort(r.end)}`;
|
|
1926
|
+
const dur = Number.isNaN(r.start) || isMs ? "" : durationLabel(r.start, r.end, o.week).replace(" working", "").replace(/ days?$/, "d");
|
|
1927
|
+
return `<tr class="${r.kind}"><td class="name" style="padding-left:${8 + r.depth * 14}px">${e(r.task.name)}</td><td>${e(
|
|
1928
|
+
r.task.owner?.name ?? ""
|
|
1929
|
+
)}</td><td class="num">${dates}</td><td class="num">${dur}</td><td class="num">${isMs ? "" : `${r.progress}%`}</td><td class="tl">${axis ? barSvg(r, axis, w, o) : ""}</td></tr>`;
|
|
1930
|
+
}).join("");
|
|
1931
|
+
const legend = [
|
|
1932
|
+
`<span><i style="border-radius:3px"><u style="background:${t.accent}"></u><b style="background:${t.accent}"></b></i>Task (dark part done)</span>`,
|
|
1933
|
+
`<span><i style="background:${t.phase};height:5px"></i>Phase</span>`,
|
|
1934
|
+
`<span><i style="background:${t.milestone};width:9px;height:9px;transform:rotate(45deg);border-radius:2px"></i>Milestone</span>`,
|
|
1935
|
+
Number.isNaN(o.today) ? "" : `<span><i style="background:${t.accent};width:2px;height:12px"></i>Today</span>`,
|
|
1936
|
+
o.showBaseline ? `<span><i style="background:${t.baseline};height:2px"></i>Baseline</span>` : ""
|
|
1937
|
+
].join("");
|
|
1938
|
+
const unit = o.week === "mon-fri" ? "Working days" : "Days";
|
|
1939
|
+
return `<!doctype html><html><head><meta charset="utf-8"><title>${e(o.title)}</title><style>
|
|
1940
|
+
@page{size:landscape;margin:10mm}
|
|
1941
|
+
*{box-sizing:border-box}
|
|
1942
|
+
html,body{margin:0;background:#fff;color:${t.ink};font-family:Geist,system-ui,-apple-system,"Segoe UI",sans-serif;font-size:10px;-webkit-print-color-adjust:exact;print-color-adjust:exact}
|
|
1943
|
+
header{display:flex;align-items:baseline;gap:12px;margin-bottom:8px}
|
|
1944
|
+
h1{font-size:16px;margin:0;letter-spacing:-0.01em}
|
|
1945
|
+
.meta{color:${t.muted};font-size:10px}
|
|
1946
|
+
.legend{display:flex;flex-wrap:wrap;gap:14px;margin-left:auto;color:${t.muted}}
|
|
1947
|
+
.legend span{display:inline-flex;align-items:center;gap:6px}
|
|
1948
|
+
.legend i{display:inline-block;position:relative;width:22px;height:9px;overflow:hidden}
|
|
1949
|
+
.legend u{position:absolute;inset:0;opacity:.22}
|
|
1950
|
+
.legend b{position:absolute;inset:0 50% 0 0}
|
|
1951
|
+
table{width:100%;border-collapse:collapse;table-layout:fixed}
|
|
1952
|
+
thead{display:table-header-group}
|
|
1953
|
+
tr{page-break-inside:avoid;break-inside:avoid}
|
|
1954
|
+
th{font-family:"Geist Mono",ui-monospace,monospace;font-weight:500;font-size:8.5px;letter-spacing:.06em;text-transform:uppercase;color:${t.muted};text-align:left;padding:0 6px 4px;border-bottom:1px solid ${t.line};vertical-align:bottom}
|
|
1955
|
+
th.tl{padding:0}
|
|
1956
|
+
th.tl text{font-family:inherit;font-size:8.5px;fill:${t.muted}}
|
|
1957
|
+
td{height:${PRINT_ROW_H}px;padding:0 6px;border-bottom:1px solid ${t.lineSoft};white-space:nowrap;overflow:hidden;text-overflow:ellipsis;vertical-align:middle}
|
|
1958
|
+
td.tl{padding:0;width:${w}px}
|
|
1959
|
+
td.tl svg{display:block}
|
|
1960
|
+
.num{text-align:right;font-variant-numeric:tabular-nums}
|
|
1961
|
+
th.num{text-align:right}
|
|
1962
|
+
tr.phase td.name{font-weight:600}
|
|
1963
|
+
footer{margin-top:8px;color:${t.muted}}
|
|
1964
|
+
</style></head><body>
|
|
1965
|
+
<header><h1>${e(o.title)}</h1><span class="meta">Printed on ${formatLong(o.printedOn)} \xB7 ${o.rows.length} ${o.rows.length === 1 ? "row" : "rows"}</span><div class="legend">${legend}</div></header>
|
|
1966
|
+
<table><colgroup><col><col style="width:90px"><col style="width:124px"><col style="width:48px"><col style="width:44px"><col style="width:${w}px"></colgroup>
|
|
1967
|
+
<thead><tr><th>Task</th><th>Owner</th><th class="num">Dates</th><th class="num" title="${unit}">${o.week === "mon-fri" ? "Wkdays" : "Days"}</th><th class="num">Done</th><th class="tl">${head}</th></tr></thead>
|
|
1968
|
+
<tbody>${body}</tbody></table>
|
|
1969
|
+
</body></html>`;
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
// src/useEditing.ts
|
|
1973
|
+
var SPEC = {
|
|
1974
|
+
...GANTT_FIELDS,
|
|
1975
|
+
describe: (change, nameOf) => describeChange(change, nameOf),
|
|
1976
|
+
noun: "task"
|
|
1977
|
+
};
|
|
1978
|
+
function useEditing2(tasks, onTaskChange, lock = {}) {
|
|
1979
|
+
return useEditing(tasks, onTaskChange, { ...SPEC, ...lock });
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
// src/useLicense.ts
|
|
1983
|
+
import { useEffect as useEffect2, useState as useState3 } from "react";
|
|
1984
|
+
function licenseOptions(opts) {
|
|
1985
|
+
return { product: opts.product, tenantId: opts.tenantId, host: browserHost() };
|
|
1986
|
+
}
|
|
1987
|
+
function useLicense(key, opts) {
|
|
1988
|
+
const [result, setResult] = useState3(null);
|
|
1989
|
+
const { product, tenantId } = opts;
|
|
1990
|
+
useEffect2(() => {
|
|
1991
|
+
let live = true;
|
|
1992
|
+
Promise.resolve(key).catch(() => void 0).then((k) => verifyLicenses(k, licenseOptions({ product, tenantId }))).then((r) => {
|
|
1993
|
+
if (live) setResult(r);
|
|
1994
|
+
});
|
|
1995
|
+
return () => {
|
|
1996
|
+
live = false;
|
|
1997
|
+
};
|
|
1998
|
+
}, [key, product, tenantId]);
|
|
1999
|
+
return result;
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
// src/Gantt.tsx
|
|
2003
|
+
var ZOOM_LABEL = { week: "Week", month: "Month", quarter: "Quarter" };
|
|
2004
|
+
var TIP_W = 264;
|
|
2005
|
+
var TIP_BASE_H = 42;
|
|
2006
|
+
var TIP_LINE_H = 22;
|
|
2007
|
+
var DRAG_THRESHOLD = 4;
|
|
2008
|
+
var MIN_HANDLE_BAR = 24;
|
|
2009
|
+
var CONFIRM_W = 440;
|
|
2010
|
+
var MAX_GRID_WIDTH = 900;
|
|
2011
|
+
var NARROW_WIDTH = 600;
|
|
2012
|
+
var NARROW_SHARE = 0.42;
|
|
2013
|
+
var DIVIDER_STEP = 24;
|
|
2014
|
+
var EDIT_CLICK_DELAY = 280;
|
|
2015
|
+
var INITIAL_ROWS = 60;
|
|
2016
|
+
var SETTLE_MS = 900;
|
|
2017
|
+
var COLUMN_FULL_LABEL = {
|
|
2018
|
+
owner: "Owner",
|
|
2019
|
+
progress: "% done",
|
|
2020
|
+
start: "Start date",
|
|
2021
|
+
end: "End date",
|
|
2022
|
+
duration: "Duration"
|
|
2023
|
+
};
|
|
2024
|
+
function initials(name) {
|
|
2025
|
+
return name.split(/\s+/).filter(Boolean).slice(0, 2).map((w) => w[0].toUpperCase()).join("");
|
|
2026
|
+
}
|
|
2027
|
+
function resolveTheme(theme, accent, colors) {
|
|
2028
|
+
const base = theme === "dark" ? darkTheme : lightTheme;
|
|
2029
|
+
const overrides = { ...typeof theme === "object" ? theme : {}, ...hostColors(colors) };
|
|
2030
|
+
const ownText = overrides.accentText;
|
|
2031
|
+
if (accent && !colors?.progress) {
|
|
2032
|
+
overrides.accent = accent;
|
|
2033
|
+
overrides.focus = overrides.focus ?? accent;
|
|
2034
|
+
const surface = overrides.surface ?? base.surface;
|
|
2035
|
+
const dark = theme === "dark";
|
|
2036
|
+
overrides.accentTint = overrides.accentTint ?? `color-mix(in srgb, ${accent} ${dark ? 32 : 20}%, ${surface})`;
|
|
2037
|
+
overrides.selected = overrides.selected ?? `color-mix(in srgb, ${accent} ${dark ? 14 : 6}%, ${surface})`;
|
|
2038
|
+
}
|
|
2039
|
+
const finalAccent = overrides.accent ?? base.accent;
|
|
2040
|
+
const pair = ownText ? { background: finalAccent, text: ownText } : accentForText(finalAccent);
|
|
2041
|
+
overrides.accentText = pair.text;
|
|
2042
|
+
const tipBg = overrides.tooltipBg ?? base.tooltipBg;
|
|
2043
|
+
return {
|
|
2044
|
+
...themeVars(overrides, base),
|
|
2045
|
+
"--nf-accent-solid": pair.background,
|
|
2046
|
+
"--nf-tip-warn": visibleOn(overrides.milestone ?? base.milestone, tipBg, 4.5),
|
|
2047
|
+
"--nf-tip-danger": visibleOn(overrides.negative ?? base.negative, tipBg, 4.5)
|
|
2048
|
+
};
|
|
2049
|
+
}
|
|
2050
|
+
function hostColors(c) {
|
|
2051
|
+
if (!c) return {};
|
|
2052
|
+
const o = {};
|
|
2053
|
+
if (c.bar) o.accentTint = c.bar;
|
|
2054
|
+
if (c.progress) o.accent = o.focus = c.progress;
|
|
2055
|
+
if (c.phase) o.phase = c.phase;
|
|
2056
|
+
if (c.milestone) o.milestone = c.milestone;
|
|
2057
|
+
if (c.background) o.surface = o.surfaceAlt = o.selected = o.lineSoft = o.tooltipInk = c.background;
|
|
2058
|
+
if (c.foreground) o.ink = o.muted = o.line = o.baseline = o.tooltipBg = c.foreground;
|
|
2059
|
+
if (c.foreground) o.negative = c.milestone ?? c.foreground;
|
|
2060
|
+
return o;
|
|
2061
|
+
}
|
|
2062
|
+
function palette(theme, accent, forceLight) {
|
|
2063
|
+
const base = theme === "dark" && !forceLight ? darkTheme : lightTheme;
|
|
2064
|
+
const own = typeof theme === "object" && !forceLight ? theme : {};
|
|
2065
|
+
return { ...base, ...own, ...accent ? { accent, accentText: own.accentText ?? accentForText(accent).text } : {} };
|
|
2066
|
+
}
|
|
2067
|
+
var KeyIcon = () => /* @__PURE__ */ React5.createElement("svg", { className: "nf-g-key", width: "12", height: "12", viewBox: "0 0 12 12", "aria-hidden": "true" }, /* @__PURE__ */ React5.createElement("circle", { cx: "4", cy: "6", r: "2.4", fill: "none", stroke: "currentColor", strokeWidth: "1.4" }), /* @__PURE__ */ React5.createElement("path", { d: "M6.4 6 H11 M9.2 6 V8 M10.6 6 V7.4", fill: "none", stroke: "currentColor", strokeWidth: "1.4", strokeLinecap: "round" }));
|
|
2068
|
+
var Chevron = () => /* @__PURE__ */ React5.createElement("svg", { width: "12", height: "12", viewBox: "0 0 12 12", "aria-hidden": "true" }, /* @__PURE__ */ React5.createElement("path", { d: "M3 4.5 L6 7.5 L9 4.5", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round" }));
|
|
2069
|
+
function Tooltip({
|
|
2070
|
+
row,
|
|
2071
|
+
left,
|
|
2072
|
+
top,
|
|
2073
|
+
fmtDay,
|
|
2074
|
+
fmtPct,
|
|
2075
|
+
brokenFrom,
|
|
2076
|
+
locked,
|
|
2077
|
+
week,
|
|
2078
|
+
rtl
|
|
2079
|
+
}) {
|
|
2080
|
+
const t = row.task;
|
|
2081
|
+
const isMs = row.kind === "milestone";
|
|
2082
|
+
const slip = !Number.isNaN(row.baselineEnd) && !Number.isNaN(row.end) ? row.end - row.baselineEnd : 0;
|
|
2083
|
+
return /* @__PURE__ */ React5.createElement("div", { className: "nf-g-tip", style: { left, top }, role: "tooltip", dir: rtl ? "rtl" : void 0 }, /* @__PURE__ */ React5.createElement("b", null, t.name), /* @__PURE__ */ React5.createElement("div", { className: "nf-g-meta" }, isMs ? fmtDay(row.start) : `${fmtDay(row.start)} \u2192 ${fmtDay(row.end)} \xB7 ${durationLabel(row.start, row.end, week)}`, !isMs ? ` \xB7 ${fmtPct(row.progress, "tooltip")} done` : ""), t.owner ? /* @__PURE__ */ React5.createElement("div", { className: "nf-g-meta" }, t.owner.name) : null, slip !== 0 ? /* @__PURE__ */ React5.createElement("div", { className: "nf-g-warn" }, Math.abs(slip), " ", Math.abs(slip) === 1 ? "day" : "days", " ", slip > 0 ? "behind" : "ahead of", " baseline") : null, brokenFrom.map((name) => /* @__PURE__ */ React5.createElement("div", { key: name, className: "nf-g-warn", "data-tone": "danger" }, "Starts before '", name, "' ends")), locked ? /* @__PURE__ */ React5.createElement("div", { className: "nf-g-meta" }, "Read-only") : null);
|
|
2084
|
+
}
|
|
2085
|
+
function Gantt(props) {
|
|
2086
|
+
const {
|
|
2087
|
+
tasks,
|
|
2088
|
+
title = "Work plan",
|
|
2089
|
+
showToolbar = true,
|
|
2090
|
+
height = "100%",
|
|
2091
|
+
gridWidth: initialGridWidthProp,
|
|
2092
|
+
columns: columnsProp = {},
|
|
2093
|
+
showFilter = true,
|
|
2094
|
+
onOpen,
|
|
2095
|
+
onTaskChange,
|
|
2096
|
+
onAddTask,
|
|
2097
|
+
singlePredecessor = false,
|
|
2098
|
+
workingWeek = "mon-fri"
|
|
2099
|
+
} = props;
|
|
2100
|
+
const [columnOverride, setColumnOverride] = useState4({});
|
|
2101
|
+
const [boxWidth, setBoxWidth] = useState4(0);
|
|
2102
|
+
const gridTouched = useRef3(false);
|
|
2103
|
+
const narrow = boxWidth > 0 && boxWidth < NARROW_WIDTH;
|
|
2104
|
+
const allCols = visibleColumns({ ...columnsProp, ...columnOverride });
|
|
2105
|
+
const cols = narrow ? allCols.filter((c) => columnOverride[c] === true) : allCols;
|
|
2106
|
+
const [colWidths, setColWidths] = useState4(() => scaleColumns(COLUMN_WIDTH, clampTextScale(props.textScale ?? 1)));
|
|
2107
|
+
const [gridWidthState, setGridWidthState] = useState4(() => initialGridWidthProp ?? Math.round(360 * clampTextScale(props.textScale ?? 1)));
|
|
2108
|
+
const minGrid = Math.round(MIN_NAME_WIDTH * clampTextScale(props.textScale ?? 1)) + 32 + cols.reduce((s, c) => s + colWidths[c], 0);
|
|
2109
|
+
const gridWidth = Math.max(narrow && !gridTouched.current ? Math.min(gridWidthState, Math.round(boxWidth * NARROW_SHARE)) : gridWidthState, minGrid);
|
|
2110
|
+
const setGridWidth = (w) => {
|
|
2111
|
+
const next2 = Math.round(Math.max(minGrid, Math.min(w, MAX_GRID_WIDTH)));
|
|
2112
|
+
gridTouched.current = true;
|
|
2113
|
+
setGridWidthState(next2);
|
|
2114
|
+
props.onGridWidthChange?.(next2);
|
|
2115
|
+
};
|
|
2116
|
+
const toggleColumn = (c) => {
|
|
2117
|
+
const adding = !cols.includes(c);
|
|
2118
|
+
gridTouched.current = true;
|
|
2119
|
+
const nextWidth = Math.min(MAX_GRID_WIDTH, gridWidth + (adding ? colWidths[c] : -colWidths[c]));
|
|
2120
|
+
setGridWidthState(nextWidth);
|
|
2121
|
+
props.onGridWidthChange?.(nextWidth);
|
|
2122
|
+
const nextOverride = { ...columnOverride, [c]: adding };
|
|
2123
|
+
setColumnOverride(nextOverride);
|
|
2124
|
+
const now = visibleColumns({ ...columnsProp, ...nextOverride });
|
|
2125
|
+
props.onColumnsChange?.(Object.fromEntries(COLUMN_ORDER.map((k) => [k, now.includes(k)])));
|
|
2126
|
+
};
|
|
2127
|
+
const [filter, setFilter] = useState4("");
|
|
2128
|
+
const filtering = filter.trim() !== "";
|
|
2129
|
+
const [toolMenu, setToolMenu] = useState4(null);
|
|
2130
|
+
const colMenu = toolMenu === "columns";
|
|
2131
|
+
const toolMenuRef = useRef3(null);
|
|
2132
|
+
useEffect3(() => {
|
|
2133
|
+
if (!toolMenu) return;
|
|
2134
|
+
toolMenuRef.current?.querySelector("button")?.focus();
|
|
2135
|
+
const close = (e) => {
|
|
2136
|
+
if (e.target.closest?.(".nf-g-menuwrap")?.getAttribute("data-menu") !== toolMenu) setToolMenu(null);
|
|
2137
|
+
};
|
|
2138
|
+
document.addEventListener("pointerdown", close, true);
|
|
2139
|
+
return () => document.removeEventListener("pointerdown", close, true);
|
|
2140
|
+
}, [toolMenu]);
|
|
2141
|
+
const menuKeys = (e) => {
|
|
2142
|
+
const items = Array.from(toolMenuRef.current?.querySelectorAll("button") ?? []);
|
|
2143
|
+
const at = items.indexOf(document.activeElement);
|
|
2144
|
+
if (e.key === "Escape" || e.key === "Tab") {
|
|
2145
|
+
setToolMenu(null);
|
|
2146
|
+
if (e.key === "Tab") return;
|
|
2147
|
+
toolMenuRef.current?.parentElement?.querySelector("[aria-haspopup]")?.focus();
|
|
2148
|
+
} else if (e.key === "ArrowDown") items[(at + 1) % items.length]?.focus();
|
|
2149
|
+
else if (e.key === "ArrowUp") items[(at - 1 + items.length) % items.length]?.focus();
|
|
2150
|
+
else if (e.key === "Home") items[0]?.focus();
|
|
2151
|
+
else if (e.key === "End") items[items.length - 1]?.focus();
|
|
2152
|
+
else return;
|
|
2153
|
+
e.preventDefault();
|
|
2154
|
+
};
|
|
2155
|
+
const uid = useState4(() => nextUid("nfg"))[0];
|
|
2156
|
+
const textScale = clampTextScale(props.textScale ?? 1);
|
|
2157
|
+
const rowH = scaleWithText(ROW_H, textScale);
|
|
2158
|
+
const headerH = scaleWithText(HEADER_H, textScale);
|
|
2159
|
+
const tipW = Math.round(TIP_W * textScale);
|
|
2160
|
+
const rootRef = useRef3(null);
|
|
2161
|
+
const [inheritedRtl, setInheritedRtl] = useState4(false);
|
|
2162
|
+
useLayoutEffect(() => {
|
|
2163
|
+
const parent = rootRef.current?.parentElement;
|
|
2164
|
+
if (parent) setInheritedRtl(getComputedStyle(parent).direction === "rtl");
|
|
2165
|
+
}, []);
|
|
2166
|
+
const rtl = props.dir ? props.dir === "rtl" : inheritedRtl;
|
|
2167
|
+
const scrollRef = useRef3(null);
|
|
2168
|
+
const bodyRef = useRef3(null);
|
|
2169
|
+
const [zoomState, setZoomState] = useState4(props.defaultZoom ?? "month");
|
|
2170
|
+
const zoom = props.zoom ?? zoomState;
|
|
2171
|
+
const setZoom = (z) => {
|
|
2172
|
+
if (props.zoom === void 0) setZoomState(z);
|
|
2173
|
+
props.onZoomChange?.(z);
|
|
2174
|
+
};
|
|
2175
|
+
const [selState, setSelState] = useState4(null);
|
|
2176
|
+
const selectedId = props.selectedId !== void 0 ? props.selectedId : selState;
|
|
2177
|
+
const select = (id, multi = false) => {
|
|
2178
|
+
if (props.selectedId === void 0) setSelState(id);
|
|
2179
|
+
props.onSelect?.(id, { multi });
|
|
2180
|
+
};
|
|
2181
|
+
const fmtDay = (day) => props.formatDate ? props.formatDate(fromDay(day)) : formatShort(day);
|
|
2182
|
+
const fmtPct = (v, use) => props.formatValue ? props.formatValue(v, { field: "progress", use }) : `${v}%`;
|
|
2183
|
+
const hl = props.highlighted && props.highlighted.length ? new Set(props.highlighted) : void 0;
|
|
2184
|
+
const hovering = useRef3(false);
|
|
2185
|
+
const hostHover = (task, at) => {
|
|
2186
|
+
if (!props.onHover) return;
|
|
2187
|
+
if (task === null && !hovering.current) return;
|
|
2188
|
+
hovering.current = task !== null;
|
|
2189
|
+
props.onHover(task, at);
|
|
2190
|
+
};
|
|
2191
|
+
const anchorOf = (id) => {
|
|
2192
|
+
const el = typeof document !== "undefined" ? document.getElementById(`${uid}-${id}`) : null;
|
|
2193
|
+
const mark = el?.querySelector(".nf-g-bar, .nf-g-ms") ?? el;
|
|
2194
|
+
const r = mark?.getBoundingClientRect();
|
|
2195
|
+
return r ? { clientX: r.left + r.width / 2, clientY: r.top + r.height / 2, multi: false } : null;
|
|
2196
|
+
};
|
|
2197
|
+
const [baselineState, setBaselineState] = useState4(true);
|
|
2198
|
+
const showBaseline = props.showBaseline ?? baselineState;
|
|
2199
|
+
const [collapsed, setCollapsed] = useState4(() => /* @__PURE__ */ new Set());
|
|
2200
|
+
const [hoverId, setHoverId] = useState4(null);
|
|
2201
|
+
const toggle = (id, open) => setCollapsed((prev) => {
|
|
2202
|
+
const next2 = new Set(prev);
|
|
2203
|
+
const shouldOpen = open ?? next2.has(id);
|
|
2204
|
+
if (shouldOpen) next2.delete(id);
|
|
2205
|
+
else next2.add(id);
|
|
2206
|
+
return next2;
|
|
2207
|
+
});
|
|
2208
|
+
const license = useLicense(props.licenseKey, { product: "plan", tenantId: props.tenantId });
|
|
2209
|
+
const pro = unlocksPro(license);
|
|
2210
|
+
const editing = useEditing2(tasks, onTaskChange, lockFor(license, pro, "Plan", "gantt"));
|
|
2211
|
+
const [preview, setPreview] = useState4(null);
|
|
2212
|
+
const [linkDrag, setLinkDrag] = useState4(null);
|
|
2213
|
+
const [linkMode, setLinkMode] = useState4(null);
|
|
2214
|
+
const [selectedLink, setSelectedLink] = useState4(null);
|
|
2215
|
+
const [menu, setMenu] = useState4(null);
|
|
2216
|
+
const [confirm, setConfirm] = useState4(null);
|
|
2217
|
+
const dragRef = useRef3(null);
|
|
2218
|
+
const rafRef = useRef3(0);
|
|
2219
|
+
const suppressClick = useRef3(false);
|
|
2220
|
+
const can = (action, row) => canEdit(props.editable, action === "add" ? !!onAddTask : !!onTaskChange, action, row);
|
|
2221
|
+
const committed = editing.shown;
|
|
2222
|
+
const shown = useMemo2(
|
|
2223
|
+
() => preview ? committed.map((t) => t.id === preview.id ? { ...t, ...preview.patch } : t) : committed,
|
|
2224
|
+
[committed, preview]
|
|
2225
|
+
);
|
|
2226
|
+
const taskById = useMemo2(() => new Map(shown.map((t) => [t.id, t])), [shown]);
|
|
2227
|
+
const nameOf = (id) => taskById.get(id)?.name ?? "task";
|
|
2228
|
+
const [viewport, setViewport] = useState4(0);
|
|
2229
|
+
useEffect3(() => {
|
|
2230
|
+
const el = scrollRef.current;
|
|
2231
|
+
if (!el) return;
|
|
2232
|
+
const measure = () => {
|
|
2233
|
+
setBoxWidth(el.clientWidth);
|
|
2234
|
+
setViewport(Math.max(0, el.clientWidth - gridWidth));
|
|
2235
|
+
};
|
|
2236
|
+
measure();
|
|
2237
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
2238
|
+
const ro = new ResizeObserver(measure);
|
|
2239
|
+
ro.observe(el);
|
|
2240
|
+
return () => ro.disconnect();
|
|
2241
|
+
}, [gridWidth]);
|
|
2242
|
+
const noneCollapsed = useMemo2(() => /* @__PURE__ */ new Set(), []);
|
|
2243
|
+
const allRows = useMemo2(() => buildRows(shown, filtering ? noneCollapsed : collapsed), [shown, collapsed, filtering, noneCollapsed]);
|
|
2244
|
+
const rows = useMemo2(() => filterRows(allRows, filter), [allRows, filter]);
|
|
2245
|
+
useEffect3(() => {
|
|
2246
|
+
const n = rows.filter((r) => rowMatches(r, filter)).length;
|
|
2247
|
+
if (filtering) editing.announce(`${n} ${n === 1 ? "task matches" : "tasks match"}`);
|
|
2248
|
+
}, [filter]);
|
|
2249
|
+
const range = useMemo2(() => rowsRange(buildRows(committed)), [committed]);
|
|
2250
|
+
const todayDay = props.today ? toDay(props.today) : today();
|
|
2251
|
+
const scale = useMemo2(() => {
|
|
2252
|
+
const r = range ?? { from: todayDay - 14, to: todayDay + 60 };
|
|
2253
|
+
return createTimeScale({ from: r.from, to: r.to, zoom, minWidth: viewport });
|
|
2254
|
+
}, [range, zoom, todayDay, viewport]);
|
|
2255
|
+
const rowsRef = useRef3(rows);
|
|
2256
|
+
rowsRef.current = rows;
|
|
2257
|
+
const scaleRef = useRef3(scale);
|
|
2258
|
+
scaleRef.current = scale;
|
|
2259
|
+
const rowHRef = useRef3(rowH);
|
|
2260
|
+
rowHRef.current = rowH;
|
|
2261
|
+
const headerHRef = useRef3(headerH);
|
|
2262
|
+
headerHRef.current = headerH;
|
|
2263
|
+
const renewal = props.renewalNotice !== false ? renewalNotice(license) : null;
|
|
2264
|
+
const style = {
|
|
2265
|
+
...resolveTheme(props.theme, props.accent, props.colors),
|
|
2266
|
+
...textVars(textScale, props.textSizes),
|
|
2267
|
+
"--nf-row": `${rowH}px`,
|
|
2268
|
+
"--nf-head": `${headerH}px`,
|
|
2269
|
+
"--nf-tip-w": `${tipW}px`,
|
|
2270
|
+
height
|
|
2271
|
+
};
|
|
2272
|
+
const todayInView = todayDay >= scale.startDay && todayDay < scale.endDay;
|
|
2273
|
+
const todayX = scale.x(todayDay) + scale.dayWidth / 2;
|
|
2274
|
+
const underTag = useMemo2(
|
|
2275
|
+
() => todayInView ? ticksUnderTag(scale.minor, todayX, todayTagWidth(formatShort(todayDay), textScale), 2, textScale) : /* @__PURE__ */ new Set(),
|
|
2276
|
+
[todayInView, scale, todayX, todayDay]
|
|
2277
|
+
);
|
|
2278
|
+
useEffect3(() => {
|
|
2279
|
+
const el = scrollRef.current;
|
|
2280
|
+
if (!el) return;
|
|
2281
|
+
el.scrollLeft = todayInView ? Math.max(0, todayX - 200) : 0;
|
|
2282
|
+
}, [zoom]);
|
|
2283
|
+
const scrollToToday = () => {
|
|
2284
|
+
const el = scrollRef.current;
|
|
2285
|
+
if (!el || !todayInView) return;
|
|
2286
|
+
const still = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
2287
|
+
el.scrollTo({ left: Math.max(0, todayX - 200), behavior: still ? "auto" : "smooth" });
|
|
2288
|
+
editing.announce(`Scrolled to today, ${formatShort(todayDay)}`);
|
|
2289
|
+
};
|
|
2290
|
+
const selectedIndex = rows.findIndex((r) => r.task.id === selectedId);
|
|
2291
|
+
const [vis, setVis] = useState4(() => ({ first: 0, last: Math.min(rows.length, INITIAL_ROWS) - 1 }));
|
|
2292
|
+
const scrollRaf = useRef3(0);
|
|
2293
|
+
const updateVisible = () => {
|
|
2294
|
+
const el = scrollRef.current;
|
|
2295
|
+
if (!el) return;
|
|
2296
|
+
const v = visibleRange(el.scrollTop, el.clientHeight, rowsRef.current.length, rowHRef.current, headerHRef.current);
|
|
2297
|
+
setVis((prev) => prev.first === v.first && prev.last === v.last ? prev : v);
|
|
2298
|
+
};
|
|
2299
|
+
const tipShown = useRef3(false);
|
|
2300
|
+
const [, setScrollTick] = useState4(0);
|
|
2301
|
+
const onScroll = () => {
|
|
2302
|
+
if (scrollRaf.current) return;
|
|
2303
|
+
scrollRaf.current = requestAnimationFrame(() => {
|
|
2304
|
+
scrollRaf.current = 0;
|
|
2305
|
+
updateVisible();
|
|
2306
|
+
if (tipShown.current) setScrollTick((t) => t + 1);
|
|
2307
|
+
});
|
|
2308
|
+
};
|
|
2309
|
+
useEffect3(() => {
|
|
2310
|
+
updateVisible();
|
|
2311
|
+
}, [rows.length, viewport]);
|
|
2312
|
+
useEffect3(() => () => cancelAnimationFrame(scrollRaf.current), []);
|
|
2313
|
+
const visLinks = useMemo2(() => dependencyLinks(rows, scale, rowH, vis), [rows, scale, vis, rowH]);
|
|
2314
|
+
const broken = useMemo2(() => brokenAmong(visLinks, rows), [visLinks, rows]);
|
|
2315
|
+
const [settled, setSettled] = useState4(props.animate === false);
|
|
2316
|
+
useEffect3(() => {
|
|
2317
|
+
if (props.animate === false) return;
|
|
2318
|
+
const t = setTimeout(() => setSettled(true), SETTLE_MS);
|
|
2319
|
+
return () => clearTimeout(t);
|
|
2320
|
+
}, []);
|
|
2321
|
+
const collapseAll = () => {
|
|
2322
|
+
setCollapsed(parentIds(buildRows(shown)));
|
|
2323
|
+
editing.announce("All phases collapsed");
|
|
2324
|
+
};
|
|
2325
|
+
const expandAll = () => {
|
|
2326
|
+
setCollapsed(/* @__PURE__ */ new Set());
|
|
2327
|
+
editing.announce("All phases expanded");
|
|
2328
|
+
};
|
|
2329
|
+
const dragging = preview !== null || linkDrag !== null;
|
|
2330
|
+
const tipRow = dragging || confirm || menu || props.tooltip === "none" ? void 0 : rows.find((r) => r.task.id === (hoverId ?? selectedId) && r.kind !== "phase" && !Number.isNaN(r.start));
|
|
2331
|
+
const focusGrid = () => scrollRef.current?.focus({ preventScroll: true });
|
|
2332
|
+
const emptyStyle = { position: "sticky", left: 0, width: viewport + gridWidth || void 0 };
|
|
2333
|
+
const loadingShown = rows.length === 0 && !filtering && !!props.loading;
|
|
2334
|
+
const [scrollTop0, setScrollTop0] = useState4(0);
|
|
2335
|
+
useLayoutEffect(() => {
|
|
2336
|
+
if (loadingShown && scrollRef.current) setScrollTop0(scrollRef.current.offsetTop);
|
|
2337
|
+
}, [loadingShown, height]);
|
|
2338
|
+
const commitEdit = (change) => {
|
|
2339
|
+
const t = committed.find((x) => x.id === change.id);
|
|
2340
|
+
if (!t || isNoop2(t, change)) return;
|
|
2341
|
+
editing.commit(change);
|
|
2342
|
+
};
|
|
2343
|
+
const spanFor = (r) => ({ start: r.start, end: r.end });
|
|
2344
|
+
const resizeRef = useRef3(null);
|
|
2345
|
+
const resizeHandlers = (kind) => ({
|
|
2346
|
+
onPointerDown: (e) => {
|
|
2347
|
+
if (e.button !== 0) return;
|
|
2348
|
+
e.preventDefault();
|
|
2349
|
+
e.stopPropagation();
|
|
2350
|
+
e.currentTarget.setPointerCapture?.(e.pointerId);
|
|
2351
|
+
resizeRef.current = { kind, x0: e.clientX, w0: kind === "grid" ? gridWidth : colWidths[kind], g0: gridWidth, pointerId: e.pointerId, sx: cssScale(rootRef.current).x };
|
|
2352
|
+
},
|
|
2353
|
+
onPointerMove: (e) => {
|
|
2354
|
+
const r = resizeRef.current;
|
|
2355
|
+
if (!r || r.pointerId !== e.pointerId) return;
|
|
2356
|
+
const dx = (e.clientX - r.x0) / r.sx;
|
|
2357
|
+
if (r.kind === "grid") {
|
|
2358
|
+
setGridWidth(r.w0 + dx);
|
|
2359
|
+
return;
|
|
2360
|
+
}
|
|
2361
|
+
const w = Math.max(MIN_COLUMN_WIDTH, Math.round(r.w0 + (rtl ? -dx : dx)));
|
|
2362
|
+
const key = r.kind;
|
|
2363
|
+
setColWidths((cw) => ({ ...cw, [key]: w }));
|
|
2364
|
+
setGridWidth(r.g0 + (w - r.w0));
|
|
2365
|
+
},
|
|
2366
|
+
onPointerUp: () => {
|
|
2367
|
+
resizeRef.current = null;
|
|
2368
|
+
},
|
|
2369
|
+
onPointerCancel: () => {
|
|
2370
|
+
resizeRef.current = null;
|
|
2371
|
+
}
|
|
2372
|
+
});
|
|
2373
|
+
const [inline, setInline] = useState4(null);
|
|
2374
|
+
const inlineDone = useRef3(false);
|
|
2375
|
+
const editTimer = useRef3(null);
|
|
2376
|
+
const cancelEditTimer = () => {
|
|
2377
|
+
if (editTimer.current) clearTimeout(editTimer.current);
|
|
2378
|
+
editTimer.current = null;
|
|
2379
|
+
};
|
|
2380
|
+
useEffect3(() => cancelEditTimer, []);
|
|
2381
|
+
const canInline = (r, field) => field === "name" ? can("rename", r) : r.kind !== "phase" && !Number.isNaN(r.start) && (field === "start" ? can("move", r) : r.kind === "task" && can("resize", r));
|
|
2382
|
+
const startInline = (r, field) => {
|
|
2383
|
+
if (!canInline(r, field)) return;
|
|
2384
|
+
const value = field === "name" ? r.task.name : fromDay(field === "start" ? r.start : r.end);
|
|
2385
|
+
inlineDone.current = false;
|
|
2386
|
+
setInline({ id: r.task.id, field, value });
|
|
2387
|
+
editing.announce(`Editing ${field === "name" ? "name" : field + " date"} of '${r.task.name}'. Enter saves, Escape cancels.`);
|
|
2388
|
+
};
|
|
2389
|
+
const finishInline = (save) => {
|
|
2390
|
+
if (inlineDone.current) return;
|
|
2391
|
+
inlineDone.current = true;
|
|
2392
|
+
const cur = inline;
|
|
2393
|
+
setInline(null);
|
|
2394
|
+
focusGrid();
|
|
2395
|
+
if (!cur || !save) return;
|
|
2396
|
+
const row = rows.find((r) => r.task.id === cur.id);
|
|
2397
|
+
const task = committed.find((t) => t.id === cur.id);
|
|
2398
|
+
if (!row || !task) return;
|
|
2399
|
+
if (cur.field === "name") {
|
|
2400
|
+
const change = renameChange(task, cur.value, "keyboard");
|
|
2401
|
+
if (change) commitEdit(change);
|
|
2402
|
+
return;
|
|
2403
|
+
}
|
|
2404
|
+
const day = toDay(cur.value);
|
|
2405
|
+
if (Number.isNaN(day)) return;
|
|
2406
|
+
commitEdit(dateCellChange(task, row.kind === "milestone", spanFor(row), cur.field, day, "keyboard"));
|
|
2407
|
+
};
|
|
2408
|
+
const clickToEdit = (r, field, wasSelected) => {
|
|
2409
|
+
cancelEditTimer();
|
|
2410
|
+
if (!wasSelected || !canInline(r, field)) return;
|
|
2411
|
+
editTimer.current = setTimeout(() => startInline(r, field), EDIT_CLICK_DELAY);
|
|
2412
|
+
};
|
|
2413
|
+
const inlineInput = (r, field, label) => inline && inline.id === r.task.id && inline.field === field ? /* @__PURE__ */ React5.createElement(
|
|
2414
|
+
"input",
|
|
2415
|
+
{
|
|
2416
|
+
className: "nf-g-inline",
|
|
2417
|
+
type: field === "name" ? "text" : "date",
|
|
2418
|
+
"aria-label": `${label} of ${r.task.name}`,
|
|
2419
|
+
value: inline.value,
|
|
2420
|
+
autoFocus: true,
|
|
2421
|
+
onFocus: (e) => field === "name" && e.currentTarget.select(),
|
|
2422
|
+
onChange: (e) => setInline({ ...inline, value: e.target.value }),
|
|
2423
|
+
onClick: (e) => e.stopPropagation(),
|
|
2424
|
+
onDoubleClick: (e) => e.stopPropagation(),
|
|
2425
|
+
onPointerDown: (e) => e.stopPropagation(),
|
|
2426
|
+
onKeyDown: (e) => {
|
|
2427
|
+
if (e.key === "Enter") {
|
|
2428
|
+
e.preventDefault();
|
|
2429
|
+
finishInline(true);
|
|
2430
|
+
} else if (e.key === "Escape") {
|
|
2431
|
+
e.preventDefault();
|
|
2432
|
+
finishInline(false);
|
|
2433
|
+
editing.announce("Edit cancelled");
|
|
2434
|
+
}
|
|
2435
|
+
},
|
|
2436
|
+
onBlur: () => finishInline(true)
|
|
2437
|
+
}
|
|
2438
|
+
) : null;
|
|
2439
|
+
const requestLink = (fromId, toId, source) => {
|
|
2440
|
+
const toRow = rows.find((r) => r.task.id === toId);
|
|
2441
|
+
const target = committed.find((t) => t.id === toId);
|
|
2442
|
+
if (!toRow || !target) return;
|
|
2443
|
+
if (!can("link", toRow)) {
|
|
2444
|
+
editing.toast({ text: `'${target.name}' can't be linked.`, tone: "error" });
|
|
2445
|
+
return;
|
|
2446
|
+
}
|
|
2447
|
+
const check = validateLink(committed, fromId, toId);
|
|
2448
|
+
if (check === "self") return editing.toast({ text: "A task can't depend on itself.", tone: "error" });
|
|
2449
|
+
if (check === "exists") return editing.announce(`'${target.name}' already follows '${nameOf(fromId)}'`);
|
|
2450
|
+
if (check === "cycle")
|
|
2451
|
+
return editing.toast({
|
|
2452
|
+
text: `That would make a loop: '${nameOf(fromId)}' already depends on '${target.name}'.`,
|
|
2453
|
+
tone: "error"
|
|
2454
|
+
});
|
|
2455
|
+
const { change, replaces } = linkChange(target, fromId, singlePredecessor, source);
|
|
2456
|
+
if (replaces) {
|
|
2457
|
+
const el = scrollRef.current;
|
|
2458
|
+
const want = gridWidth + (barGeometry(toRow.start, toRow.end, scale)?.left ?? 0);
|
|
2459
|
+
const visibleFrom = (el?.scrollLeft ?? 0) + gridWidth + 8;
|
|
2460
|
+
const visibleTo = (el?.scrollLeft ?? 0) + (el?.clientWidth ?? 800) - CONFIRM_W - 8;
|
|
2461
|
+
setConfirm({
|
|
2462
|
+
change,
|
|
2463
|
+
text: `Replace dependency on '${nameOf(replaces)}'?`,
|
|
2464
|
+
top: toRow.index * rowH + rowH,
|
|
2465
|
+
left: Math.max(visibleFrom, Math.min(want, visibleTo))
|
|
2466
|
+
});
|
|
2467
|
+
return;
|
|
2468
|
+
}
|
|
2469
|
+
commitEdit(change);
|
|
2470
|
+
};
|
|
2471
|
+
const zoneAction = (zone) => zone === "progress" ? "progress" : zone === "link" ? "link" : zone === "move" ? "move" : "resize";
|
|
2472
|
+
const bodyPoint = (clientX, clientY) => {
|
|
2473
|
+
if (!bodyRef.current) return { x: 0, y: 0 };
|
|
2474
|
+
const p = localPoint(bodyRef.current, clientX, clientY);
|
|
2475
|
+
return { x: p.x - gridWidth, y: p.y };
|
|
2476
|
+
};
|
|
2477
|
+
const patchFor = (d) => {
|
|
2478
|
+
const t = d.row.task;
|
|
2479
|
+
if (d.zone === "progress") return { progress: dragProgress(d.row.progress, d.dx, d.barWidth) };
|
|
2480
|
+
const mode = d.zone === "start" ? "start" : d.zone === "end" ? "end" : "move";
|
|
2481
|
+
const span = shiftSpan(mode, spanFor(d.row), pxToDays(d.dx, scaleRef.current.dayWidth), workingWeek);
|
|
2482
|
+
return fieldsOf2(spanChange(t, d.row.kind === "milestone", span, "drag"));
|
|
2483
|
+
};
|
|
2484
|
+
const flush = () => {
|
|
2485
|
+
rafRef.current = 0;
|
|
2486
|
+
const d = dragRef.current;
|
|
2487
|
+
if (!d || !d.started) return;
|
|
2488
|
+
if (d.zone === "link") {
|
|
2489
|
+
const p = bodyPoint(d.clientX, d.clientY);
|
|
2490
|
+
const target = rowsRef.current[Math.floor(p.y / rowH)];
|
|
2491
|
+
setLinkDrag({ fromId: d.id, x: p.x, y: p.y, targetId: target && target.task.id !== d.id ? target.task.id : null });
|
|
2492
|
+
return;
|
|
2493
|
+
}
|
|
2494
|
+
setPreview({ id: d.id, patch: patchFor(d), ghost: d.ghost });
|
|
2495
|
+
};
|
|
2496
|
+
const cancelDrag = (announce = true) => {
|
|
2497
|
+
if (rafRef.current) cancelAnimationFrame(rafRef.current);
|
|
2498
|
+
rafRef.current = 0;
|
|
2499
|
+
const wasActive = dragRef.current?.started;
|
|
2500
|
+
dragRef.current = null;
|
|
2501
|
+
setPreview(null);
|
|
2502
|
+
setLinkDrag(null);
|
|
2503
|
+
if (announce && wasActive) editing.announce("Drag cancelled");
|
|
2504
|
+
};
|
|
2505
|
+
const onEditPointerDown = (e, r) => {
|
|
2506
|
+
if (e.button !== 0) return;
|
|
2507
|
+
const el = e.target.closest("[data-drag]");
|
|
2508
|
+
const zone = el?.getAttribute("data-drag") ?? "move";
|
|
2509
|
+
if (!can(zoneAction(zone), r)) return;
|
|
2510
|
+
e.stopPropagation();
|
|
2511
|
+
e.preventDefault();
|
|
2512
|
+
focusGrid();
|
|
2513
|
+
e.currentTarget.setPointerCapture?.(e.pointerId);
|
|
2514
|
+
const g = r.kind === "milestone" ? null : barGeometry(r.start, r.end, scale);
|
|
2515
|
+
dragRef.current = {
|
|
2516
|
+
id: r.task.id,
|
|
2517
|
+
zone,
|
|
2518
|
+
pointerId: e.pointerId,
|
|
2519
|
+
x0: e.clientX,
|
|
2520
|
+
y0: e.clientY,
|
|
2521
|
+
dx: 0,
|
|
2522
|
+
clientX: e.clientX,
|
|
2523
|
+
clientY: e.clientY,
|
|
2524
|
+
started: false,
|
|
2525
|
+
row: r,
|
|
2526
|
+
barWidth: g?.width ?? 0,
|
|
2527
|
+
ghost: zone === "link" || zone === "progress" ? null : g,
|
|
2528
|
+
sx: cssScale(rootRef.current).x
|
|
2529
|
+
};
|
|
2530
|
+
};
|
|
2531
|
+
const onEditPointerMove = (e) => {
|
|
2532
|
+
const d = dragRef.current;
|
|
2533
|
+
if (!d || e.pointerId !== d.pointerId) return;
|
|
2534
|
+
const dx = (e.clientX - d.x0) / d.sx;
|
|
2535
|
+
const dy = (e.clientY - d.y0) / d.sx;
|
|
2536
|
+
if (!d.started) {
|
|
2537
|
+
if (Math.hypot(dx, dy) < DRAG_THRESHOLD) return;
|
|
2538
|
+
d.started = true;
|
|
2539
|
+
setMenu(null);
|
|
2540
|
+
setSelectedLink(null);
|
|
2541
|
+
}
|
|
2542
|
+
d.dx = dx;
|
|
2543
|
+
d.clientX = e.clientX;
|
|
2544
|
+
d.clientY = e.clientY;
|
|
2545
|
+
if (!rafRef.current) rafRef.current = requestAnimationFrame(flush);
|
|
2546
|
+
};
|
|
2547
|
+
const onEditPointerUp = (e) => {
|
|
2548
|
+
const d = dragRef.current;
|
|
2549
|
+
if (!d || e.pointerId !== d.pointerId) return;
|
|
2550
|
+
if (rafRef.current) cancelAnimationFrame(rafRef.current);
|
|
2551
|
+
rafRef.current = 0;
|
|
2552
|
+
dragRef.current = null;
|
|
2553
|
+
if (!d.started) return;
|
|
2554
|
+
suppressClick.current = true;
|
|
2555
|
+
d.dx = (e.clientX - d.x0) / d.sx;
|
|
2556
|
+
setPreview(null);
|
|
2557
|
+
setLinkDrag(null);
|
|
2558
|
+
select(d.id);
|
|
2559
|
+
const task = committed.find((t) => t.id === d.id);
|
|
2560
|
+
if (!task) return;
|
|
2561
|
+
if (d.zone === "link") {
|
|
2562
|
+
const p = bodyPoint(e.clientX, e.clientY);
|
|
2563
|
+
const target = rowsRef.current[Math.floor(p.y / rowH)];
|
|
2564
|
+
if (target && target.task.id !== d.id) requestLink(d.id, target.task.id, "drag");
|
|
2565
|
+
return;
|
|
2566
|
+
}
|
|
2567
|
+
if (d.zone === "progress") {
|
|
2568
|
+
commitEdit(progressChange(task, dragProgress(d.row.progress, d.dx, d.barWidth), "drag"));
|
|
2569
|
+
return;
|
|
2570
|
+
}
|
|
2571
|
+
const mode = d.zone === "start" ? "start" : d.zone === "end" ? "end" : "move";
|
|
2572
|
+
const span = shiftSpan(mode, spanFor(d.row), pxToDays(d.dx, scale.dayWidth), workingWeek);
|
|
2573
|
+
commitEdit(spanChange(task, d.row.kind === "milestone", span, "drag"));
|
|
2574
|
+
};
|
|
2575
|
+
const editHandlers = (r) => ({
|
|
2576
|
+
onPointerDown: (e) => onEditPointerDown(e, r),
|
|
2577
|
+
onPointerMove: onEditPointerMove,
|
|
2578
|
+
onPointerUp: onEditPointerUp,
|
|
2579
|
+
onPointerCancel: () => cancelDrag(false)
|
|
2580
|
+
});
|
|
2581
|
+
useEffect3(() => {
|
|
2582
|
+
if (!dragging) return;
|
|
2583
|
+
const onKey = (e) => {
|
|
2584
|
+
if (e.key === "Escape") {
|
|
2585
|
+
e.preventDefault();
|
|
2586
|
+
cancelDrag();
|
|
2587
|
+
}
|
|
2588
|
+
};
|
|
2589
|
+
window.addEventListener("keydown", onKey, true);
|
|
2590
|
+
return () => window.removeEventListener("keydown", onKey, true);
|
|
2591
|
+
}, [dragging]);
|
|
2592
|
+
useEffect3(() => {
|
|
2593
|
+
if (!menu) return;
|
|
2594
|
+
const close = (e) => {
|
|
2595
|
+
if (!e.target.closest?.(".nf-g-menu")) setMenu(null);
|
|
2596
|
+
};
|
|
2597
|
+
document.addEventListener("pointerdown", close, true);
|
|
2598
|
+
return () => document.removeEventListener("pointerdown", close, true);
|
|
2599
|
+
}, [menu]);
|
|
2600
|
+
const menuRef = useRef3(null);
|
|
2601
|
+
useEffect3(() => {
|
|
2602
|
+
if (menu) menuRef.current?.querySelector("button")?.focus();
|
|
2603
|
+
}, [menu]);
|
|
2604
|
+
const confirmRef = useRef3(null);
|
|
2605
|
+
useEffect3(() => {
|
|
2606
|
+
if (confirm) confirmRef.current?.focus({ preventScroll: true });
|
|
2607
|
+
}, [confirm]);
|
|
2608
|
+
const suggestStart = (r) => fromDay(r && !Number.isNaN(r.end) ? r.end + 1 : todayDay);
|
|
2609
|
+
const requestAdd = (req, words) => {
|
|
2610
|
+
if (!can("add")) return;
|
|
2611
|
+
setMenu(null);
|
|
2612
|
+
if (editing.locked) return editing.refuse();
|
|
2613
|
+
editing.announce(words);
|
|
2614
|
+
onAddTask?.(req);
|
|
2615
|
+
};
|
|
2616
|
+
const addBelow = (r) => requestAdd({ parentId: r.task.parentId ?? null, afterId: r.task.id, start: suggestStart(r) }, `Adding a task below '${r.task.name}'`);
|
|
2617
|
+
const addSubtask = (r) => requestAdd({ parentId: r.task.id, start: suggestStart(r) }, `Adding a subtask under '${r.task.name}'`);
|
|
2618
|
+
const openMenuAt = (id, clientX, clientY) => {
|
|
2619
|
+
const root = rootRef.current;
|
|
2620
|
+
if (!root) return;
|
|
2621
|
+
const p = localPoint(root, clientX, clientY);
|
|
2622
|
+
setMenu({ id, x: Math.min(p.x, root.offsetWidth - 200), y: Math.min(p.y, root.offsetHeight - 100) });
|
|
2623
|
+
};
|
|
2624
|
+
const onKeyDown = (e) => {
|
|
2625
|
+
if (e.target !== e.currentTarget) return;
|
|
2626
|
+
if (rows.length === 0) return;
|
|
2627
|
+
const i = selectedIndex;
|
|
2628
|
+
const go = (n) => {
|
|
2629
|
+
const r = rows[Math.max(0, Math.min(rows.length - 1, n))];
|
|
2630
|
+
select(r.task.id);
|
|
2631
|
+
setSelectedLink(null);
|
|
2632
|
+
if (props.onHover) {
|
|
2633
|
+
requestAnimationFrame(() => {
|
|
2634
|
+
const at = anchorOf(r.task.id);
|
|
2635
|
+
if (at) hostHover(r.task, at);
|
|
2636
|
+
});
|
|
2637
|
+
}
|
|
2638
|
+
const el = scrollRef.current;
|
|
2639
|
+
if (el) {
|
|
2640
|
+
const top = r.index * rowH;
|
|
2641
|
+
if (top < el.scrollTop) el.scrollTop = top;
|
|
2642
|
+
else if (top + rowH + headerH > el.scrollTop + el.clientHeight) el.scrollTop = top + rowH + headerH - el.clientHeight;
|
|
2643
|
+
}
|
|
2644
|
+
};
|
|
2645
|
+
const row = i >= 0 ? rows[i] : void 0;
|
|
2646
|
+
const mod = e.ctrlKey || e.metaKey;
|
|
2647
|
+
if (e.key === "F2" && row) {
|
|
2648
|
+
startInline(row, "name");
|
|
2649
|
+
e.preventDefault();
|
|
2650
|
+
return;
|
|
2651
|
+
}
|
|
2652
|
+
if (mod && !e.altKey && (e.key === "z" || e.key === "Z" || e.key === "y" || e.key === "Y") && onTaskChange) {
|
|
2653
|
+
if (e.key === "y" || e.key === "Y" || e.shiftKey) editing.redo();
|
|
2654
|
+
else editing.undo();
|
|
2655
|
+
e.preventDefault();
|
|
2656
|
+
return;
|
|
2657
|
+
}
|
|
2658
|
+
const ke = keyEdit(e);
|
|
2659
|
+
if (ke) {
|
|
2660
|
+
e.preventDefault();
|
|
2661
|
+
if (!row) return;
|
|
2662
|
+
const task = committed.find((t) => t.id === row.task.id);
|
|
2663
|
+
if (!task) return;
|
|
2664
|
+
if (ke.kind === "progress") {
|
|
2665
|
+
if (can("progress", row)) commitEdit(progressChange(task, row.progress + ke.delta, "keyboard"));
|
|
2666
|
+
return;
|
|
2667
|
+
}
|
|
2668
|
+
if (ke.mode === "end" ? !can("resize", row) : !can("move", row)) return;
|
|
2669
|
+
commitEdit(spanChange(task, row.kind === "milestone", shiftSpan(ke.mode, spanFor(row), ke.days, workingWeek), "keyboard"));
|
|
2670
|
+
return;
|
|
2671
|
+
}
|
|
2672
|
+
if (linkMode) {
|
|
2673
|
+
if (e.key === "Enter") {
|
|
2674
|
+
if (row) requestLink(linkMode, row.task.id, "keyboard");
|
|
2675
|
+
setLinkMode(null);
|
|
2676
|
+
e.preventDefault();
|
|
2677
|
+
return;
|
|
2678
|
+
}
|
|
2679
|
+
if (e.key === "Escape") {
|
|
2680
|
+
setLinkMode(null);
|
|
2681
|
+
editing.announce("Linking cancelled");
|
|
2682
|
+
e.preventDefault();
|
|
2683
|
+
return;
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
if ((e.key === "l" || e.key === "L") && !mod && !e.altKey && row && can("link", row)) {
|
|
2687
|
+
setLinkMode(row.task.id);
|
|
2688
|
+
editing.announce(`Linking from '${row.task.name}'. Choose the task that follows it with the arrow keys, then press Enter. Escape cancels.`);
|
|
2689
|
+
e.preventDefault();
|
|
2690
|
+
return;
|
|
2691
|
+
}
|
|
2692
|
+
if (e.key === "Delete" || e.key === "Backspace") {
|
|
2693
|
+
if (selectedLink) {
|
|
2694
|
+
const [fromId, toId] = selectedLink.split(">");
|
|
2695
|
+
const to = committed.find((t) => t.id === toId);
|
|
2696
|
+
const toRow = rows.find((r) => r.task.id === toId);
|
|
2697
|
+
if (to && toRow && can("link", toRow)) commitEdit(unlinkChange(to, fromId, "keyboard"));
|
|
2698
|
+
setSelectedLink(null);
|
|
2699
|
+
e.preventDefault();
|
|
2700
|
+
return;
|
|
2701
|
+
}
|
|
2702
|
+
if (e.key === "Delete" && e.shiftKey && row && can("link", row) && row.task.dependsOn?.length) {
|
|
2703
|
+
const task = committed.find((t) => t.id === row.task.id);
|
|
2704
|
+
if (task) commitEdit(unlinkChange(task, void 0, "keyboard"));
|
|
2705
|
+
e.preventDefault();
|
|
2706
|
+
return;
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
if (e.key === "Insert" && can("add")) {
|
|
2710
|
+
if (row && mod) addSubtask(row);
|
|
2711
|
+
else if (row) addBelow(row);
|
|
2712
|
+
else requestAdd({ parentId: null, start: suggestStart() }, "Adding a task");
|
|
2713
|
+
e.preventDefault();
|
|
2714
|
+
return;
|
|
2715
|
+
}
|
|
2716
|
+
if (isContextMenuKey(e) && row && props.onContextMenu) {
|
|
2717
|
+
const at = anchorOf(row.task.id);
|
|
2718
|
+
if (at) props.onContextMenu(row.task, at);
|
|
2719
|
+
e.preventDefault();
|
|
2720
|
+
return;
|
|
2721
|
+
}
|
|
2722
|
+
if (isContextMenuKey(e) && row && can("add")) {
|
|
2723
|
+
const el = document.getElementById(`${uid}-${row.task.id}`);
|
|
2724
|
+
const rect = el?.getBoundingClientRect();
|
|
2725
|
+
if (rect) openMenuAt(row.task.id, rect.left + 24, rect.bottom - 4);
|
|
2726
|
+
e.preventDefault();
|
|
2727
|
+
return;
|
|
2728
|
+
}
|
|
2729
|
+
switch (e.key) {
|
|
2730
|
+
case "ArrowDown":
|
|
2731
|
+
go(i + 1);
|
|
2732
|
+
break;
|
|
2733
|
+
case "ArrowUp":
|
|
2734
|
+
go(i < 0 ? 0 : i - 1);
|
|
2735
|
+
break;
|
|
2736
|
+
case "Home":
|
|
2737
|
+
go(0);
|
|
2738
|
+
break;
|
|
2739
|
+
case "End":
|
|
2740
|
+
go(rows.length - 1);
|
|
2741
|
+
break;
|
|
2742
|
+
case "ArrowLeft":
|
|
2743
|
+
if (row?.hasChildren && row.collapsed === rtl) toggle(row.task.id, rtl);
|
|
2744
|
+
break;
|
|
2745
|
+
case "ArrowRight":
|
|
2746
|
+
if (row?.hasChildren && row.collapsed !== rtl) toggle(row.task.id, !rtl);
|
|
2747
|
+
break;
|
|
2748
|
+
case "Enter":
|
|
2749
|
+
if (row) onOpen?.(row.task);
|
|
2750
|
+
break;
|
|
2751
|
+
case "Escape":
|
|
2752
|
+
if (selectedLink) setSelectedLink(null);
|
|
2753
|
+
else select(null);
|
|
2754
|
+
break;
|
|
2755
|
+
default:
|
|
2756
|
+
return;
|
|
2757
|
+
}
|
|
2758
|
+
e.preventDefault();
|
|
2759
|
+
};
|
|
2760
|
+
const copySupport = () => {
|
|
2761
|
+
const host = (props.diagnostics || void 0)?.() ?? {};
|
|
2762
|
+
void copyDiagnostics({
|
|
2763
|
+
product: "Plan",
|
|
2764
|
+
control: "Gantt",
|
|
2765
|
+
version: VERSION,
|
|
2766
|
+
licence: license,
|
|
2767
|
+
rows: props.tasks.length,
|
|
2768
|
+
...host
|
|
2769
|
+
}).then((r) => editing.toast({ text: r.message, tone: r.ok ? "info" : "error" }));
|
|
2770
|
+
setToolMenu(null);
|
|
2771
|
+
};
|
|
2772
|
+
const printChart = () => {
|
|
2773
|
+
setToolMenu(null);
|
|
2774
|
+
printDocument(
|
|
2775
|
+
printHtml({
|
|
2776
|
+
title,
|
|
2777
|
+
rows,
|
|
2778
|
+
theme: palette(props.theme, props.accent, true),
|
|
2779
|
+
today: todayDay,
|
|
2780
|
+
printedOn: today(),
|
|
2781
|
+
week: workingWeek,
|
|
2782
|
+
showBaseline
|
|
2783
|
+
}),
|
|
2784
|
+
rootRef.current?.ownerDocument ?? document
|
|
2785
|
+
);
|
|
2786
|
+
editing.announce("Print dialog opened");
|
|
2787
|
+
};
|
|
2788
|
+
const downloadPng = () => {
|
|
2789
|
+
setToolMenu(null);
|
|
2790
|
+
const ok = exportPng(
|
|
2791
|
+
{ title, rows, scale, gridWidth, theme: palette(props.theme, props.accent, false), today: todayDay, showBaseline, broken: brokenLinks(rows) },
|
|
2792
|
+
pngFileName(title, fromDay(today())),
|
|
2793
|
+
rootRef.current?.ownerDocument ?? document
|
|
2794
|
+
);
|
|
2795
|
+
if (ok) editing.announce("Image downloaded");
|
|
2796
|
+
else editing.toast({ text: "Too big for one image. Collapse phases or filter first, or use Print.", tone: "info" });
|
|
2797
|
+
};
|
|
2798
|
+
const gridCols = ["minmax(0,1fr)", ...cols.map((c) => `${colWidths[c]}px`)].join(" ");
|
|
2799
|
+
const canAdd = can("add");
|
|
2800
|
+
const bodyHeight = rows.length * rowH + (canAdd ? rowH : 0);
|
|
2801
|
+
const pinned = new Set([selectedId, inline?.id, preview?.id, linkDrag?.fromId].filter((x) => !!x));
|
|
2802
|
+
const renderRows = rows.filter((r) => r.index >= vis.first && r.index <= vis.last || pinned.has(r.task.id));
|
|
2803
|
+
const linksEditable = !!onTaskChange && props.editable !== false && !(typeof props.editable === "object" && props.editable.link === false);
|
|
2804
|
+
const tipBroken = tipRow ? (tipRow.task.dependsOn ?? []).filter((f) => broken.has(`${f}>${tipRow.task.id}`)).map(nameOf) : [];
|
|
2805
|
+
let tip = null;
|
|
2806
|
+
if (tipRow) {
|
|
2807
|
+
const g = tipRow.kind === "milestone" ? { left: scale.x(tipRow.start), width: scale.dayWidth } : barGeometry(tipRow.start, tipRow.end, scale);
|
|
2808
|
+
if (g) {
|
|
2809
|
+
const sc = scrollRef.current;
|
|
2810
|
+
const view = sc ? { left: sc.scrollLeft + gridWidth, right: sc.scrollLeft + sc.clientWidth, top: sc.scrollTop, bottom: sc.scrollTop + sc.clientHeight - headerH } : { left: gridWidth, right: gridWidth + scale.width, top: 0, bottom: Infinity };
|
|
2811
|
+
const slips = !Number.isNaN(tipRow.baselineEnd) && !Number.isNaN(tipRow.end) && tipRow.end !== tipRow.baselineEnd;
|
|
2812
|
+
const lines = 1 + (tipRow.task.owner ? 1 : 0) + (slips ? 1 : 0) + tipBroken.length + (tipRow.task.readOnly ? 1 : 0);
|
|
2813
|
+
const inView = gridWidth + g.left < view.right && gridWidth + g.left + g.width > view.left;
|
|
2814
|
+
if (inView) tip = { row: tipRow, ...placeTip({ left: gridWidth + g.left, width: g.width }, tipRow.index * rowH, view, tipW, (TIP_BASE_H + lines * TIP_LINE_H) * textScale, rowH) };
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2817
|
+
const toRoot = (p) => {
|
|
2818
|
+
const sc = scrollRef.current;
|
|
2819
|
+
const body = bodyRef.current;
|
|
2820
|
+
if (!sc || !body) return { left: p.left, top: p.top + headerH };
|
|
2821
|
+
return { left: sc.offsetLeft + p.left - sc.scrollLeft, top: sc.offsetTop + body.offsetTop + p.top - sc.scrollTop };
|
|
2822
|
+
};
|
|
2823
|
+
const tipAt = tip ? toRoot(tip) : null;
|
|
2824
|
+
const confirmAt = confirm ? toRoot(confirm) : null;
|
|
2825
|
+
tipShown.current = !!tipAt || !!confirmAt;
|
|
2826
|
+
let dragLabel = null;
|
|
2827
|
+
if (preview) {
|
|
2828
|
+
const r = rows.find((x) => x.task.id === preview.id);
|
|
2829
|
+
if (r) {
|
|
2830
|
+
const g = r.kind === "milestone" ? { left: scale.x(r.start), width: scale.dayWidth } : barGeometry(r.start, r.end, scale);
|
|
2831
|
+
if (g) {
|
|
2832
|
+
const text = preview.patch.progress !== void 0 ? `${r.progress}% done` : spanLabel(spanFor(r), r.kind === "milestone", workingWeek);
|
|
2833
|
+
const above = r.index * rowH - 26;
|
|
2834
|
+
dragLabel = { text, left: gridWidth + g.left, top: above >= 2 ? above : r.index * rowH + rowH + 2 };
|
|
2835
|
+
}
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2838
|
+
const linkFromRow = linkDrag ? rows.find((r) => r.task.id === linkDrag.fromId) : void 0;
|
|
2839
|
+
const linkStart = linkFromRow ? {
|
|
2840
|
+
x: linkFromRow.kind === "milestone" ? scale.x(linkFromRow.start) + scale.dayWidth / 2 + 10 : (barGeometry(linkFromRow.start, linkFromRow.end, scale)?.left ?? 0) + (barGeometry(linkFromRow.start, linkFromRow.end, scale)?.width ?? 0) + 6,
|
|
2841
|
+
y: linkFromRow.index * rowH + rowH / 2
|
|
2842
|
+
} : null;
|
|
2843
|
+
return /* @__PURE__ */ React5.createElement(
|
|
2844
|
+
"div",
|
|
2845
|
+
{
|
|
2846
|
+
ref: rootRef,
|
|
2847
|
+
dir: "ltr",
|
|
2848
|
+
"data-rtl": rtl ? "" : void 0,
|
|
2849
|
+
"data-static": props.animate === false ? "" : void 0,
|
|
2850
|
+
"data-hl": hl ? "" : void 0,
|
|
2851
|
+
"data-hc": props.colors?.foreground && props.colors?.background ? "" : void 0,
|
|
2852
|
+
className: `nf-gantt${dragging ? " nf-g-dragging" : ""}${settled ? " nf-g-settled" : ""}${props.className ? " " + props.className : ""}`,
|
|
2853
|
+
style
|
|
2854
|
+
},
|
|
2855
|
+
/* @__PURE__ */ React5.createElement("style", { dangerouslySetInnerHTML: { __html: css } }),
|
|
2856
|
+
showToolbar ? /* @__PURE__ */ React5.createElement("div", { className: "nf-g-toolbar", dir: rtl ? "rtl" : void 0 }, /* @__PURE__ */ React5.createElement("div", { className: "nf-g-title" }, title), /* @__PURE__ */ React5.createElement("div", { className: "nf-g-seg", role: "group", "aria-label": "Zoom" }, ZOOMS.map((z) => /* @__PURE__ */ React5.createElement("button", { key: z, type: "button", className: "nf-g-btn", "aria-pressed": z === zoom, onClick: () => setZoom(z) }, ZOOM_LABEL[z]))), /* @__PURE__ */ React5.createElement("button", { type: "button", className: "nf-g-btn", onClick: scrollToToday, disabled: !todayInView }, "Today"), /* @__PURE__ */ React5.createElement("div", { className: "nf-g-seg", role: "group", "aria-label": "Phases" }, /* @__PURE__ */ React5.createElement("button", { type: "button", className: "nf-g-btn", onClick: expandAll, disabled: collapsed.size === 0 || filtering }, "Expand all"), /* @__PURE__ */ React5.createElement("button", { type: "button", className: "nf-g-btn", onClick: collapseAll, disabled: filtering }, "Collapse all")), /* @__PURE__ */ React5.createElement("div", { className: "nf-g-spacer" }), showFilter ? /* @__PURE__ */ React5.createElement(
|
|
2857
|
+
"input",
|
|
2858
|
+
{
|
|
2859
|
+
className: "nf-g-filter",
|
|
2860
|
+
type: "search",
|
|
2861
|
+
placeholder: "Filter tasks",
|
|
2862
|
+
"aria-label": "Filter tasks by name or owner",
|
|
2863
|
+
value: filter,
|
|
2864
|
+
onChange: (e) => setFilter(e.target.value),
|
|
2865
|
+
onKeyDown: (e) => {
|
|
2866
|
+
if (e.key === "Escape" && filter) {
|
|
2867
|
+
setFilter("");
|
|
2868
|
+
e.preventDefault();
|
|
2869
|
+
}
|
|
2870
|
+
}
|
|
2871
|
+
}
|
|
2872
|
+
) : null, /* @__PURE__ */ React5.createElement("div", { className: "nf-g-menuwrap", "data-menu": "columns" }, /* @__PURE__ */ React5.createElement(
|
|
2873
|
+
"button",
|
|
2874
|
+
{
|
|
2875
|
+
type: "button",
|
|
2876
|
+
className: "nf-g-btn",
|
|
2877
|
+
"aria-haspopup": "menu",
|
|
2878
|
+
"aria-expanded": colMenu,
|
|
2879
|
+
onClick: () => setToolMenu(colMenu ? null : "columns")
|
|
2880
|
+
},
|
|
2881
|
+
"Columns"
|
|
2882
|
+
), colMenu ? /* @__PURE__ */ React5.createElement("div", { ref: toolMenuRef, className: "nf-g-menu nf-g-colmenu", role: "menu", "aria-label": "Columns", onKeyDown: menuKeys }, COLUMN_ORDER.map((c) => /* @__PURE__ */ React5.createElement(
|
|
2883
|
+
"button",
|
|
2884
|
+
{
|
|
2885
|
+
key: c,
|
|
2886
|
+
type: "button",
|
|
2887
|
+
role: "menuitemcheckbox",
|
|
2888
|
+
"aria-checked": cols.includes(c),
|
|
2889
|
+
onClick: () => toggleColumn(c)
|
|
2890
|
+
},
|
|
2891
|
+
/* @__PURE__ */ React5.createElement("span", { className: "nf-g-check", "aria-hidden": "true" }, cols.includes(c) ? "\u2713" : ""),
|
|
2892
|
+
COLUMN_FULL_LABEL[c]
|
|
2893
|
+
))) : null), props.exports !== false ? /* @__PURE__ */ React5.createElement("div", { className: "nf-g-menuwrap", "data-menu": "export" }, /* @__PURE__ */ React5.createElement(
|
|
2894
|
+
"button",
|
|
2895
|
+
{
|
|
2896
|
+
type: "button",
|
|
2897
|
+
className: "nf-g-btn",
|
|
2898
|
+
"aria-haspopup": "menu",
|
|
2899
|
+
"aria-expanded": toolMenu === "export",
|
|
2900
|
+
onClick: () => setToolMenu(toolMenu === "export" ? null : "export")
|
|
2901
|
+
},
|
|
2902
|
+
"Export"
|
|
2903
|
+
), toolMenu === "export" ? /* @__PURE__ */ React5.createElement("div", { ref: toolMenuRef, className: "nf-g-menu nf-g-colmenu", role: "menu", "aria-label": "Export", onKeyDown: menuKeys }, /* @__PURE__ */ React5.createElement("button", { type: "button", role: "menuitem", onClick: printChart }, /* @__PURE__ */ React5.createElement("span", { className: "nf-g-check", "aria-hidden": "true" }), "Print\u2026"), /* @__PURE__ */ React5.createElement("button", { type: "button", role: "menuitem", onClick: downloadPng }, /* @__PURE__ */ React5.createElement("span", { className: "nf-g-check", "aria-hidden": "true" }), "Download image (PNG)"), props.diagnostics !== false ? /* @__PURE__ */ React5.createElement("button", { type: "button", role: "menuitem", onClick: copySupport }, /* @__PURE__ */ React5.createElement("span", { className: "nf-g-check", "aria-hidden": "true" }), "Copy diagnostics") : null) : null) : null, /* @__PURE__ */ React5.createElement(
|
|
2904
|
+
"button",
|
|
2905
|
+
{
|
|
2906
|
+
type: "button",
|
|
2907
|
+
className: "nf-g-switch",
|
|
2908
|
+
"aria-pressed": showBaseline,
|
|
2909
|
+
onClick: () => {
|
|
2910
|
+
if (props.showBaseline === void 0) setBaselineState(!showBaseline);
|
|
2911
|
+
}
|
|
2912
|
+
},
|
|
2913
|
+
/* @__PURE__ */ React5.createElement("span", { className: "nf-g-switch-track", "aria-hidden": "true" }),
|
|
2914
|
+
"Baseline"
|
|
2915
|
+
)) : null,
|
|
2916
|
+
/* @__PURE__ */ React5.createElement(
|
|
2917
|
+
"div",
|
|
2918
|
+
{
|
|
2919
|
+
ref: scrollRef,
|
|
2920
|
+
className: "nf-g-scroll",
|
|
2921
|
+
role: "treegrid",
|
|
2922
|
+
"aria-label": title,
|
|
2923
|
+
"aria-busy": loadingShown || void 0,
|
|
2924
|
+
"aria-rowcount": rows.length + 1 + (canAdd ? 1 : 0),
|
|
2925
|
+
"aria-activedescendant": selectedId ? `${uid}-${selectedId}` : void 0,
|
|
2926
|
+
"aria-describedby": onTaskChange ? `${uid}-help` : void 0,
|
|
2927
|
+
tabIndex: 0,
|
|
2928
|
+
onKeyDown,
|
|
2929
|
+
onScroll
|
|
2930
|
+
},
|
|
2931
|
+
onTaskChange ? /* @__PURE__ */ React5.createElement("span", { id: `${uid}-help`, className: "nf-g-sr" }, "Alt plus arrow keys move the selected task, Alt plus Shift plus arrows change its end, Alt plus up or down changes progress. L links tasks. Control plus Z undoes.", canAdd ? " Insert adds a task, Control plus Insert adds a subtask." : "") : null,
|
|
2932
|
+
/* @__PURE__ */ React5.createElement("div", { style: { width: gridWidth + scale.width } }, /* @__PURE__ */ React5.createElement("div", { className: "nf-g-head", role: "row", "aria-rowindex": 1 }, /* @__PURE__ */ React5.createElement("div", { className: "nf-g-cols", role: "columnheader", dir: rtl ? "rtl" : void 0, style: { width: gridWidth, gridTemplateColumns: gridCols } }, /* @__PURE__ */ React5.createElement("div", null, "Task"), cols.map((c) => /* @__PURE__ */ React5.createElement("div", { key: c, className: "nf-g-colhead" }, COLUMN_LABEL[c], /* @__PURE__ */ React5.createElement("span", { className: "nf-g-colresize", "aria-hidden": "true", title: `Resize ${COLUMN_LABEL[c]}`, ...resizeHandlers(c) }))), /* @__PURE__ */ React5.createElement(
|
|
2933
|
+
"div",
|
|
2934
|
+
{
|
|
2935
|
+
className: "nf-g-divider",
|
|
2936
|
+
role: "separator",
|
|
2937
|
+
"aria-orientation": "vertical",
|
|
2938
|
+
"aria-label": "Resize the task list",
|
|
2939
|
+
"aria-valuenow": gridWidth,
|
|
2940
|
+
"aria-valuemin": minGrid,
|
|
2941
|
+
"aria-valuemax": MAX_GRID_WIDTH,
|
|
2942
|
+
tabIndex: 0,
|
|
2943
|
+
title: "Drag to resize the task list",
|
|
2944
|
+
onKeyDown: (e) => {
|
|
2945
|
+
if (e.key === "ArrowLeft" || e.key === "ArrowRight") {
|
|
2946
|
+
setGridWidth(gridWidth + (e.key === "ArrowRight" ? DIVIDER_STEP : -DIVIDER_STEP));
|
|
2947
|
+
e.preventDefault();
|
|
2948
|
+
}
|
|
2949
|
+
},
|
|
2950
|
+
...resizeHandlers("grid")
|
|
2951
|
+
}
|
|
2952
|
+
)), /* @__PURE__ */ React5.createElement("div", { className: "nf-g-axis", "aria-hidden": "true", style: { width: scale.width, height: headerH } }, scale.major.map((t) => /* @__PURE__ */ React5.createElement("div", { key: `M${t.day}`, className: "nf-g-major", style: { left: t.x, width: t.width } }, /* @__PURE__ */ React5.createElement("span", { className: "nf-g-major-label", style: { left: gridWidth + 10 } }, t.label))), scale.minor.map(
|
|
2953
|
+
(t, i) => (
|
|
2954
|
+
// Skip a week label the today tag would half-cover (unreadable, and an axe contrast failure).
|
|
2955
|
+
underTag.has(i) ? null : /* @__PURE__ */ React5.createElement("div", { key: `m${t.day}`, className: "nf-g-minor", style: { left: t.x } }, t.label)
|
|
2956
|
+
)
|
|
2957
|
+
), todayInView ? /* @__PURE__ */ React5.createElement("div", { className: "nf-g-today-tag", style: { left: todayX } }, formatShort(todayDay)) : null)), rows.length === 0 && filtering ? /* @__PURE__ */ React5.createElement("div", { className: "nf-g-empty", style: emptyStyle }, "No tasks match \u201C", filter.trim(), "\u201D.") : null, rows.length === 0 && !filtering && !props.loading && (!canAdd || props.emptyAction) ? /* @__PURE__ */ React5.createElement("div", { className: "nf-g-empty", style: emptyStyle }, "No tasks to show yet.", props.emptyAction ? /* @__PURE__ */ React5.createElement(EmptyActionView, { action: props.emptyAction, prefix: "nf-g", buttonClass: "nf-g-btn" }) : null) : null, /* @__PURE__ */ React5.createElement("div", { ref: bodyRef, className: "nf-g-body", style: { height: bodyHeight } }, /* @__PURE__ */ React5.createElement("div", { className: "nf-g-layer", "aria-hidden": "true", style: { left: gridWidth, width: scale.width, height: bodyHeight } }, scale.weekends.map((w) => /* @__PURE__ */ React5.createElement("div", { key: `w${w.x}`, className: "nf-g-weekend", style: { left: w.x, width: w.width } })), scale.minor.map((t) => /* @__PURE__ */ React5.createElement("div", { key: `v${t.day}`, className: "nf-g-vline", style: { left: t.x } })), selectedIndex >= 0 ? /* @__PURE__ */ React5.createElement("div", { className: "nf-g-band", style: { top: selectedIndex * rowH } }) : null, todayInView ? /* @__PURE__ */ React5.createElement("div", { className: "nf-g-today", style: { left: todayX } }) : null), renderRows.map((r) => {
|
|
2958
|
+
const t = r.task;
|
|
2959
|
+
const selected = t.id === selectedId;
|
|
2960
|
+
const g = barGeometry(r.start, r.end, scale);
|
|
2961
|
+
const base = showBaseline ? barGeometry(r.baselineStart, r.baselineEnd, scale) : null;
|
|
2962
|
+
const delay = { animationDelay: `${Math.min(r.index * 25, 400)}ms` };
|
|
2963
|
+
const movable = can("move", r);
|
|
2964
|
+
const roomy = !!g && g.width >= MIN_HANDLE_BAR;
|
|
2965
|
+
const resizable = roomy && can("resize", r);
|
|
2966
|
+
const progressable = roomy && can("progress", r);
|
|
2967
|
+
const linkable = can("link", r);
|
|
2968
|
+
const locked = !!onTaskChange && props.editable !== false && !!t.readOnly && r.kind !== "phase";
|
|
2969
|
+
const isGhosted = preview?.id === t.id && preview.ghost;
|
|
2970
|
+
const isLinkTarget = linkDrag?.targetId === t.id || linkMode !== null && selected && linkMode !== t.id;
|
|
2971
|
+
const editAttrs = movable || resizable || progressable || linkable ? editHandlers(r) : {};
|
|
2972
|
+
return /* @__PURE__ */ React5.createElement(
|
|
2973
|
+
"div",
|
|
2974
|
+
{
|
|
2975
|
+
key: t.id,
|
|
2976
|
+
id: `${uid}-${t.id}`,
|
|
2977
|
+
className: `nf-g-row${isLinkTarget ? " nf-g-linktarget" : ""}${linkMode === t.id ? " nf-g-linksource" : ""}`,
|
|
2978
|
+
role: "row",
|
|
2979
|
+
style: { top: r.index * rowH },
|
|
2980
|
+
"aria-rowindex": r.index + 2,
|
|
2981
|
+
"aria-level": r.depth + 1,
|
|
2982
|
+
"aria-selected": selected,
|
|
2983
|
+
"aria-expanded": r.hasChildren ? !r.collapsed : void 0,
|
|
2984
|
+
"aria-readonly": locked || void 0,
|
|
2985
|
+
"data-kind": r.kind,
|
|
2986
|
+
"data-dim": hl && !isHighlighted(t.id, hl) ? "" : void 0,
|
|
2987
|
+
onClick: (e) => {
|
|
2988
|
+
if (suppressClick.current) {
|
|
2989
|
+
suppressClick.current = false;
|
|
2990
|
+
return;
|
|
2991
|
+
}
|
|
2992
|
+
select(t.id, isMultiSelect(e));
|
|
2993
|
+
setSelectedLink(null);
|
|
2994
|
+
},
|
|
2995
|
+
onDoubleClick: () => {
|
|
2996
|
+
cancelEditTimer();
|
|
2997
|
+
onOpen?.(t);
|
|
2998
|
+
},
|
|
2999
|
+
onMouseDown: (e) => {
|
|
3000
|
+
if (e.detail > 1) e.preventDefault();
|
|
3001
|
+
},
|
|
3002
|
+
onContextMenu: (e) => {
|
|
3003
|
+
if (props.onContextMenu) {
|
|
3004
|
+
e.preventDefault();
|
|
3005
|
+
props.onContextMenu(t, { clientX: e.clientX, clientY: e.clientY, multi: isMultiSelect(e) });
|
|
3006
|
+
return;
|
|
3007
|
+
}
|
|
3008
|
+
if (!canAdd) return;
|
|
3009
|
+
e.preventDefault();
|
|
3010
|
+
select(t.id);
|
|
3011
|
+
openMenuAt(t.id, e.clientX, e.clientY);
|
|
3012
|
+
},
|
|
3013
|
+
onMouseEnter: () => setHoverId(t.id),
|
|
3014
|
+
onMouseMove: props.onHover ? (e) => hostHover(t, { clientX: e.clientX, clientY: e.clientY, multi: isMultiSelect(e) }) : void 0,
|
|
3015
|
+
onMouseLeave: (e) => {
|
|
3016
|
+
setHoverId((h) => h === t.id ? null : h);
|
|
3017
|
+
hostHover(null, { clientX: e.clientX, clientY: e.clientY, multi: false });
|
|
3018
|
+
}
|
|
3019
|
+
},
|
|
3020
|
+
/* @__PURE__ */ React5.createElement("div", { className: "nf-g-cell", role: "gridcell", dir: rtl ? "rtl" : void 0, style: { width: gridWidth, gridTemplateColumns: gridCols } }, /* @__PURE__ */ React5.createElement("div", { className: "nf-g-name", style: { paddingInlineStart: r.depth * 18 } }, r.hasChildren ? /* @__PURE__ */ React5.createElement(
|
|
3021
|
+
"button",
|
|
3022
|
+
{
|
|
3023
|
+
type: "button",
|
|
3024
|
+
className: "nf-g-chev",
|
|
3025
|
+
tabIndex: -1,
|
|
3026
|
+
"aria-expanded": !r.collapsed,
|
|
3027
|
+
"aria-label": `${r.collapsed ? "Expand" : "Collapse"} ${t.name}`,
|
|
3028
|
+
onClick: (e) => {
|
|
3029
|
+
e.stopPropagation();
|
|
3030
|
+
toggle(t.id);
|
|
3031
|
+
}
|
|
3032
|
+
},
|
|
3033
|
+
/* @__PURE__ */ React5.createElement(Chevron, null)
|
|
3034
|
+
) : r.kind === "milestone" ? /* @__PURE__ */ React5.createElement("span", { className: "nf-g-dot", "aria-hidden": "true" }) : null, inlineInput(r, "name", "Name") ?? /* @__PURE__ */ React5.createElement("span", { title: t.name, onClick: () => clickToEdit(r, "name", selected) }, t.name), canAdd ? /* @__PURE__ */ React5.createElement(
|
|
3035
|
+
"button",
|
|
3036
|
+
{
|
|
3037
|
+
type: "button",
|
|
3038
|
+
className: "nf-g-rowadd",
|
|
3039
|
+
tabIndex: -1,
|
|
3040
|
+
"aria-label": `Add a task near ${t.name}`,
|
|
3041
|
+
title: "Add task",
|
|
3042
|
+
onClick: (e) => {
|
|
3043
|
+
e.stopPropagation();
|
|
3044
|
+
select(t.id);
|
|
3045
|
+
const rect = e.currentTarget.getBoundingClientRect();
|
|
3046
|
+
openMenuAt(t.id, rect.left, rect.bottom + 4);
|
|
3047
|
+
}
|
|
3048
|
+
},
|
|
3049
|
+
"+"
|
|
3050
|
+
) : null), cols.map((c) => {
|
|
3051
|
+
if (c === "owner") {
|
|
3052
|
+
return /* @__PURE__ */ React5.createElement("div", { key: c }, t.owner ? /* @__PURE__ */ React5.createElement("span", { className: "nf-g-avatar", title: t.owner.name }, t.owner.initials ?? initials(t.owner.name)) : null);
|
|
3053
|
+
}
|
|
3054
|
+
if (c === "progress") {
|
|
3055
|
+
return /* @__PURE__ */ React5.createElement("div", { key: c, className: "nf-g-num" }, r.kind === "milestone" ? Number.isNaN(r.start) ? "" : fmtDay(r.start) : fmtPct(r.progress, "grid"));
|
|
3056
|
+
}
|
|
3057
|
+
if (c === "duration") {
|
|
3058
|
+
return /* @__PURE__ */ React5.createElement("div", { key: c, className: "nf-g-num" }, r.kind === "milestone" || Number.isNaN(r.start) ? "" : workingWeek === "all" ? r.end - r.start + 1 : workingDaysBetween(r.start, r.end, workingWeek));
|
|
3059
|
+
}
|
|
3060
|
+
const day = c === "start" ? r.start : r.end;
|
|
3061
|
+
const blank = Number.isNaN(day) || c === "end" && r.kind === "milestone";
|
|
3062
|
+
return /* @__PURE__ */ React5.createElement(
|
|
3063
|
+
"div",
|
|
3064
|
+
{
|
|
3065
|
+
key: c,
|
|
3066
|
+
className: "nf-g-num nf-g-datecell",
|
|
3067
|
+
"data-editable": canInline(r, c) || void 0,
|
|
3068
|
+
onClick: () => clickToEdit(r, c, selected)
|
|
3069
|
+
},
|
|
3070
|
+
inlineInput(r, c, COLUMN_LABEL[c]) ?? (blank ? "" : fmtDay(day))
|
|
3071
|
+
);
|
|
3072
|
+
})),
|
|
3073
|
+
/* @__PURE__ */ React5.createElement("div", { className: "nf-g-lane", role: "gridcell", style: { width: scale.width } }, base && r.kind === "task" ? /* @__PURE__ */ React5.createElement("div", { className: "nf-g-base", style: { left: base.left, width: base.width } }) : null, isGhosted ? /* @__PURE__ */ React5.createElement("div", { className: "nf-g-ghost", style: { left: preview.ghost.left, width: preview.ghost.width } }) : null, g && r.kind === "phase" ? /* @__PURE__ */ React5.createElement("div", { className: "nf-g-phase", style: { left: g.left, width: g.width, ...delay } }) : null, g && r.kind === "task" ? /* @__PURE__ */ React5.createElement(
|
|
3074
|
+
"div",
|
|
3075
|
+
{
|
|
3076
|
+
className: "nf-g-bar",
|
|
3077
|
+
"data-editable": movable || void 0,
|
|
3078
|
+
"data-locked": locked || void 0,
|
|
3079
|
+
style: { left: g.left, width: g.width, ...delay },
|
|
3080
|
+
...editAttrs
|
|
3081
|
+
},
|
|
3082
|
+
/* @__PURE__ */ React5.createElement("div", { className: "nf-g-fill", style: { width: `${r.progress}%` } }),
|
|
3083
|
+
resizable ? /* @__PURE__ */ React5.createElement(React5.Fragment, null, /* @__PURE__ */ React5.createElement("div", { className: "nf-g-h nf-g-h-l", "data-drag": "start" }), /* @__PURE__ */ React5.createElement("div", { className: "nf-g-h nf-g-h-r", "data-drag": "end" })) : null,
|
|
3084
|
+
progressable ? /* @__PURE__ */ React5.createElement("div", { className: "nf-g-knob", "data-drag": "progress", style: { left: `calc(${r.progress}% - 4px)` } }) : null
|
|
3085
|
+
) : null, r.kind === "milestone" && !Number.isNaN(r.start) ? /* @__PURE__ */ React5.createElement(
|
|
3086
|
+
"div",
|
|
3087
|
+
{
|
|
3088
|
+
className: "nf-g-ms",
|
|
3089
|
+
"data-editable": movable || void 0,
|
|
3090
|
+
"data-locked": locked || void 0,
|
|
3091
|
+
style: { left: scale.x(r.start) + scale.dayWidth / 2, ...delay },
|
|
3092
|
+
...editAttrs
|
|
3093
|
+
}
|
|
3094
|
+
) : null, linkable && (g || r.kind === "milestone") && !Number.isNaN(r.start) ? /* @__PURE__ */ React5.createElement(
|
|
3095
|
+
"div",
|
|
3096
|
+
{
|
|
3097
|
+
className: "nf-g-linkdot",
|
|
3098
|
+
"data-drag": "link",
|
|
3099
|
+
title: "Drag onto another task to link",
|
|
3100
|
+
style: {
|
|
3101
|
+
left: r.kind === "milestone" ? scale.x(r.start) + scale.dayWidth / 2 + 12 : (g?.left ?? 0) + (g?.width ?? 0) + 6
|
|
3102
|
+
},
|
|
3103
|
+
...editAttrs
|
|
3104
|
+
}
|
|
3105
|
+
) : null)
|
|
3106
|
+
);
|
|
3107
|
+
}), canAdd ? /* @__PURE__ */ React5.createElement("div", { className: "nf-g-addwrap", role: "row", "aria-rowindex": rows.length + 2, style: { top: rows.length * rowH, width: gridWidth + scale.width } }, /* @__PURE__ */ React5.createElement("div", { className: "nf-g-addrow", role: "gridcell", style: { width: gridWidth } }, /* @__PURE__ */ React5.createElement(
|
|
3108
|
+
"button",
|
|
3109
|
+
{
|
|
3110
|
+
type: "button",
|
|
3111
|
+
className: "nf-g-addbtn",
|
|
3112
|
+
onClick: () => requestAdd({ parentId: null, start: suggestStart(rows[rows.length - 1]) }, "Adding a task"),
|
|
3113
|
+
title: editing.locked && license ? lockedMessage("Plan") : void 0
|
|
3114
|
+
},
|
|
3115
|
+
"+ Add task",
|
|
3116
|
+
editing.locked && license ? /* @__PURE__ */ React5.createElement(KeyIcon, null) : null
|
|
3117
|
+
))) : null, /* @__PURE__ */ React5.createElement(
|
|
3118
|
+
"svg",
|
|
3119
|
+
{
|
|
3120
|
+
className: "nf-g-deps",
|
|
3121
|
+
"aria-hidden": "true",
|
|
3122
|
+
width: scale.width,
|
|
3123
|
+
height: bodyHeight,
|
|
3124
|
+
style: { position: "absolute", left: gridWidth, top: 0 }
|
|
3125
|
+
},
|
|
3126
|
+
visLinks.map((l) => {
|
|
3127
|
+
const cls = `${broken.has(l.key) ? "is-broken" : ""}${selectedLink === l.key ? " is-selected" : ""}`;
|
|
3128
|
+
return /* @__PURE__ */ React5.createElement("g", { key: l.key, className: cls || void 0 }, /* @__PURE__ */ React5.createElement("path", { d: l.d }), /* @__PURE__ */ React5.createElement("path", { className: "nf-g-arrow", d: l.arrow }), linksEditable ? /* @__PURE__ */ React5.createElement(
|
|
3129
|
+
"path",
|
|
3130
|
+
{
|
|
3131
|
+
className: "nf-g-hit",
|
|
3132
|
+
d: l.hit,
|
|
3133
|
+
onClick: (e) => {
|
|
3134
|
+
e.stopPropagation();
|
|
3135
|
+
setSelectedLink(l.key);
|
|
3136
|
+
editing.announce(
|
|
3137
|
+
`Link from '${nameOf(l.from)}' to '${nameOf(l.to)}' selected. Press Delete to remove it.`
|
|
3138
|
+
);
|
|
3139
|
+
focusGrid();
|
|
3140
|
+
}
|
|
3141
|
+
}
|
|
3142
|
+
) : null);
|
|
3143
|
+
}),
|
|
3144
|
+
linkDrag && linkStart ? /* @__PURE__ */ React5.createElement("path", { className: "nf-g-linkline", d: `M${linkStart.x} ${linkStart.y}L${linkDrag.x} ${linkDrag.y}` }) : null
|
|
3145
|
+
), dragLabel ? /* @__PURE__ */ React5.createElement("div", { className: "nf-g-draglabel", style: { left: dragLabel.left, top: dragLabel.top } }, dragLabel.text) : null))
|
|
3146
|
+
),
|
|
3147
|
+
confirm && confirmAt ? /* @__PURE__ */ React5.createElement("div", { className: "nf-g-confirm", role: "dialog", dir: rtl ? "rtl" : void 0, "aria-label": confirm.text, style: { left: confirmAt.left, top: confirmAt.top } }, /* @__PURE__ */ React5.createElement("span", null, confirm.text), /* @__PURE__ */ React5.createElement(
|
|
3148
|
+
"button",
|
|
3149
|
+
{
|
|
3150
|
+
ref: confirmRef,
|
|
3151
|
+
type: "button",
|
|
3152
|
+
className: "nf-g-btn nf-g-primary",
|
|
3153
|
+
onClick: () => {
|
|
3154
|
+
commitEdit(confirm.change);
|
|
3155
|
+
setConfirm(null);
|
|
3156
|
+
focusGrid();
|
|
3157
|
+
}
|
|
3158
|
+
},
|
|
3159
|
+
"Replace"
|
|
3160
|
+
), /* @__PURE__ */ React5.createElement(
|
|
3161
|
+
"button",
|
|
3162
|
+
{
|
|
3163
|
+
type: "button",
|
|
3164
|
+
className: "nf-g-btn",
|
|
3165
|
+
onKeyDown: (e) => e.key === "Escape" && (setConfirm(null), focusGrid()),
|
|
3166
|
+
onClick: () => {
|
|
3167
|
+
setConfirm(null);
|
|
3168
|
+
focusGrid();
|
|
3169
|
+
}
|
|
3170
|
+
},
|
|
3171
|
+
"Cancel"
|
|
3172
|
+
)) : null,
|
|
3173
|
+
menu ? /* @__PURE__ */ React5.createElement(
|
|
3174
|
+
"div",
|
|
3175
|
+
{
|
|
3176
|
+
ref: menuRef,
|
|
3177
|
+
className: "nf-g-menu",
|
|
3178
|
+
dir: rtl ? "rtl" : void 0,
|
|
3179
|
+
role: "menu",
|
|
3180
|
+
style: { left: menu.x, top: menu.y },
|
|
3181
|
+
onKeyDown: (e) => {
|
|
3182
|
+
const items = Array.from(menuRef.current?.querySelectorAll("button") ?? []);
|
|
3183
|
+
const at = items.indexOf(document.activeElement);
|
|
3184
|
+
if (e.key === "Escape") {
|
|
3185
|
+
setMenu(null);
|
|
3186
|
+
focusGrid();
|
|
3187
|
+
} else if (e.key === "ArrowDown") items[(at + 1) % items.length]?.focus();
|
|
3188
|
+
else if (e.key === "ArrowUp") items[(at - 1 + items.length) % items.length]?.focus();
|
|
3189
|
+
else return;
|
|
3190
|
+
e.preventDefault();
|
|
3191
|
+
}
|
|
3192
|
+
},
|
|
3193
|
+
(() => {
|
|
3194
|
+
const r = rows.find((x) => x.task.id === menu.id);
|
|
3195
|
+
if (!r) return null;
|
|
3196
|
+
return /* @__PURE__ */ React5.createElement(React5.Fragment, null, /* @__PURE__ */ React5.createElement("button", { type: "button", role: "menuitem", onClick: () => (addBelow(r), focusGrid()) }, "Add task below ", /* @__PURE__ */ React5.createElement("kbd", null, "Insert")), /* @__PURE__ */ React5.createElement("button", { type: "button", role: "menuitem", onClick: () => (addSubtask(r), focusGrid()) }, "Add subtask ", /* @__PURE__ */ React5.createElement("kbd", null, "Ctrl+Insert")));
|
|
3197
|
+
})()
|
|
3198
|
+
) : null,
|
|
3199
|
+
tip && tipAt ? /* @__PURE__ */ React5.createElement(Tooltip, { row: tip.row, left: tipAt.left, top: tipAt.top, fmtDay, fmtPct, brokenFrom: tipBroken, locked: !!tip.row.task.readOnly && !!onTaskChange, week: workingWeek, rtl }) : null,
|
|
3200
|
+
loadingShown ? /* @__PURE__ */ React5.createElement("div", { className: "nf-g-empty nf-g-loading", style: { top: scrollTop0 + headerH }, role: "status", "aria-busy": "true" }, "Loading tasks\u2026") : null,
|
|
3201
|
+
/* @__PURE__ */ React5.createElement("div", { style: { display: "contents" }, dir: rtl ? "rtl" : void 0 }, /* @__PURE__ */ React5.createElement(Toasts, { items: editing.toasts, announcement: editing.announcement, onDismiss: editing.dismiss })),
|
|
3202
|
+
license && !license.licensed && props.watermark !== false ? /* @__PURE__ */ React5.createElement(Watermark, { prefix: "nf-g", title: licenseMessage(license), markHtml: markSvg({ size: 16 }), variant: props.watermarkStyle, onClick: props.onWatermarkClick }) : null,
|
|
3203
|
+
editing.lockNote ? /* @__PURE__ */ React5.createElement("p", { className: "nf-g-locknote", role: "status" }, editing.lockNote) : null,
|
|
3204
|
+
renewal ? /* @__PURE__ */ React5.createElement("a", { className: "nf-g-renew", href: renewal.href, target: "_blank", rel: "noreferrer", title: renewal.detail }, renewal.text) : null
|
|
3205
|
+
);
|
|
3206
|
+
}
|
|
3207
|
+
|
|
3208
|
+
// src/sample.ts
|
|
3209
|
+
var sampleTasks = [
|
|
3210
|
+
{ id: "design", name: "Design", kind: "phase" },
|
|
3211
|
+
{ id: "survey", parentId: "design", name: "Site survey", start: "2026-10-05", end: "2026-10-16", progress: 100, owner: { name: "Jess Lee" }, baselineStart: "2026-10-05", baselineEnd: "2026-10-16" },
|
|
3212
|
+
{ id: "concept", parentId: "design", name: "Concept design", start: "2026-10-12", end: "2026-10-30", progress: 70, owner: { name: "Alex Morgan" }, baselineStart: "2026-10-12", baselineEnd: "2026-10-26", dependsOn: ["survey"] },
|
|
3213
|
+
{ id: "review", parentId: "design", name: "Design review", kind: "milestone", date: "2026-11-02", dependsOn: ["concept"] },
|
|
3214
|
+
{ id: "procure", name: "Procurement", kind: "phase" },
|
|
3215
|
+
{ id: "tender", parentId: "procure", name: "Tender package", start: "2026-11-02", end: "2026-11-20", progress: 25, owner: { name: "Sam Kaur" }, baselineStart: "2026-10-30", baselineEnd: "2026-11-18", dependsOn: ["review"] },
|
|
3216
|
+
{ id: "eval", parentId: "procure", name: "Supplier evaluation", start: "2026-11-16", end: "2026-12-04", progress: 0, owner: { name: "Jess Lee" } },
|
|
3217
|
+
{ id: "award", parentId: "procure", name: "Contract award", kind: "milestone", date: "2026-12-07", dependsOn: ["eval"] },
|
|
3218
|
+
{ id: "deliver", name: "Delivery", kind: "phase" },
|
|
3219
|
+
{ id: "mobilise", parentId: "deliver", name: "Mobilisation", start: "2026-12-07", end: "2026-12-18", progress: 0, owner: { name: "Ravi Tan" }, dependsOn: ["award"] },
|
|
3220
|
+
{ id: "build1", parentId: "deliver", name: "Construction stage 1", start: "2026-12-14", end: "2027-01-29", progress: 0, owner: { name: "Alex Morgan" } },
|
|
3221
|
+
{ id: "handover", parentId: "deliver", name: "Stage 1 handover", kind: "milestone", date: "2027-02-05", dependsOn: ["build1"] }
|
|
3222
|
+
];
|
|
3223
|
+
export {
|
|
3224
|
+
Gantt,
|
|
3225
|
+
HEADER_H,
|
|
3226
|
+
ROW_H,
|
|
3227
|
+
VERSION,
|
|
3228
|
+
barGeometry,
|
|
3229
|
+
brokenLinks,
|
|
3230
|
+
buildRows,
|
|
3231
|
+
dependencyLinks,
|
|
3232
|
+
dependencyPaths,
|
|
3233
|
+
rowsRange,
|
|
3234
|
+
sampleTasks,
|
|
3235
|
+
validateLink
|
|
3236
|
+
};
|
|
3237
|
+
//# sourceMappingURL=index.js.map
|