@comitor/ui 3.11.1 → 3.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +10 -5
  2. package/dist/chat/chat-composer.d.ts +36 -0
  3. package/dist/chat/chat-composer.js +85 -0
  4. package/dist/chat/chat-composer.js.map +1 -0
  5. package/dist/chat/chat-constants.d.ts +63 -0
  6. package/dist/chat/chat-constants.js +1 -0
  7. package/dist/chat/chat-constants.js.map +1 -0
  8. package/dist/chat/chat-message.d.ts +51 -0
  9. package/dist/chat/chat-message.js +137 -0
  10. package/dist/chat/chat-message.js.map +1 -0
  11. package/dist/chat/chat-sources.d.ts +25 -0
  12. package/dist/chat/chat-sources.js +97 -0
  13. package/dist/chat/chat-sources.js.map +1 -0
  14. package/dist/chat/chat-steps.d.ts +20 -0
  15. package/dist/chat/chat-steps.js +84 -0
  16. package/dist/chat/chat-steps.js.map +1 -0
  17. package/dist/chat/chat-thread.d.ts +21 -0
  18. package/dist/chat/chat-thread.js +87 -0
  19. package/dist/chat/chat-thread.js.map +1 -0
  20. package/dist/chat/index.d.ts +12 -0
  21. package/dist/chat/index.js +14 -0
  22. package/dist/chat/index.js.map +1 -0
  23. package/dist/components/composite/gantt-constants.d.ts +127 -0
  24. package/dist/components/composite/gantt-constants.js +5 -0
  25. package/dist/components/composite/gantt-constants.js.map +1 -0
  26. package/dist/components/composite/gantt-utils.d.ts +106 -0
  27. package/dist/components/composite/gantt-utils.js +196 -0
  28. package/dist/components/composite/gantt-utils.js.map +1 -0
  29. package/dist/components/composite/gantt.d.ts +93 -0
  30. package/dist/components/composite/gantt.js +1137 -0
  31. package/dist/components/composite/gantt.js.map +1 -0
  32. package/dist/components/composite/stat-card.d.ts +1 -1
  33. package/dist/components/composite/tree-view-constants.d.ts +96 -0
  34. package/dist/components/composite/tree-view-constants.js +85 -0
  35. package/dist/components/composite/tree-view-constants.js.map +1 -0
  36. package/dist/components/composite/tree-view.d.ts +58 -0
  37. package/dist/components/composite/tree-view.js +385 -0
  38. package/dist/components/composite/tree-view.js.map +1 -0
  39. package/dist/components/ui/badge.d.ts +1 -1
  40. package/dist/components/ui/button.d.ts +1 -1
  41. package/dist/editor/bubble-toolbar.d.ts +1 -1
  42. package/dist/editor/editor-core.d.ts +2 -2
  43. package/dist/editor/editor-core.js +6 -0
  44. package/dist/editor/editor-core.js.map +1 -1
  45. package/dist/editor/editor-types.d.ts +1 -1
  46. package/dist/editor/fixed-toolbar.d.ts +1 -1
  47. package/dist/editor/image-upload.d.ts +1 -1
  48. package/dist/editor/index.d.ts +1 -1
  49. package/dist/editor/markdown-editor.d.ts +1 -1
  50. package/dist/editor/markdown-extensions.d.ts +3 -1
  51. package/dist/editor/markdown-extensions.js +3 -1
  52. package/dist/editor/markdown-extensions.js.map +1 -1
  53. package/dist/editor/rich-text-editor.d.ts +1 -1
  54. package/dist/editor/slash-commands.d.ts +1 -1
  55. package/dist/editor/toolbar-actions.d.ts +1 -1
  56. package/dist/{editor-types-CSrMeuSw.d.ts → editor-types-BacXD2VN.d.ts} +7 -1
  57. package/dist/i18n/locale-provider.d.ts +3 -0
  58. package/dist/i18n/messages.d.ts +6 -0
  59. package/dist/i18n/messages.js +11 -2
  60. package/dist/i18n/messages.js.map +1 -1
  61. package/dist/i18n/namespaces/chat.d.ts +8 -0
  62. package/dist/i18n/namespaces/chat.js +47 -0
  63. package/dist/i18n/namespaces/chat.js.map +1 -0
  64. package/dist/i18n/namespaces/gantt.d.ts +10 -0
  65. package/dist/i18n/namespaces/gantt.js +57 -0
  66. package/dist/i18n/namespaces/gantt.js.map +1 -0
  67. package/dist/i18n/namespaces/tree-view.d.ts +8 -0
  68. package/dist/i18n/namespaces/tree-view.js +21 -0
  69. package/dist/i18n/namespaces/tree-view.js.map +1 -0
  70. package/dist/index.d.ts +5 -0
  71. package/dist/index.js +4 -0
  72. package/dist/index.js.map +1 -1
  73. package/package.json +9 -2
@@ -0,0 +1,1137 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { useVirtualizer } from "@tanstack/react-virtual";
4
+ import { cva } from "class-variance-authority";
5
+ import { ChevronDown, ChevronLeft, ChevronRight } from "lucide-react";
6
+ import * as React from "react";
7
+ import { useComitorLabels, useComitorLocale } from "../../i18n/locale-provider.js";
8
+ import { cn } from "../../lib/cn.js";
9
+ import { useIsomorphicLayoutEffect } from "../../lib/use-isomorphic-layout-effect.js";
10
+ import { Button } from "../ui/button.js";
11
+ import { ToggleGroup, ToggleGroupItem } from "../ui/toggle-group.js";
12
+ import {
13
+ GANTT_SCALES
14
+ } from "./gantt-constants.js";
15
+ import {
16
+ computeRange,
17
+ dayToLocalDate,
18
+ formatDay,
19
+ isDependencyViolated,
20
+ parseDay,
21
+ routeDependency,
22
+ SCALE_CONFIG,
23
+ shiftByUnit,
24
+ ticksBetween,
25
+ todayDay,
26
+ weekday
27
+ } from "./gantt-utils.js";
28
+ const ganttBarVariants = cva(
29
+ "absolute flex items-center overflow-hidden rounded-md border text-xs font-medium touch-none select-none",
30
+ {
31
+ variants: {
32
+ tone: {
33
+ neutral: "border-control-edge-strong bg-secondary text-secondary-foreground",
34
+ primary: "border-primary-ink bg-primary text-primary-foreground",
35
+ blue: "border-blue-ink bg-blue text-blue-foreground",
36
+ teal: "border-teal-ink bg-teal text-teal-foreground",
37
+ green: "border-green-ink bg-green text-green-foreground",
38
+ yellow: "border-yellow-ink bg-yellow text-yellow-foreground",
39
+ orange: "border-orange-ink bg-orange text-orange-foreground",
40
+ red: "border-destructive-ink bg-destructive text-destructive-foreground",
41
+ purple: "border-purple-ink bg-purple text-purple-foreground",
42
+ pink: "border-pink-ink bg-pink text-pink-foreground",
43
+ navy: "border-navy-ink bg-navy text-navy-foreground"
44
+ }
45
+ },
46
+ defaultVariants: { tone: "neutral" }
47
+ }
48
+ );
49
+ const ganttMarkVariants = cva("", {
50
+ variants: {
51
+ tone: {
52
+ neutral: "bg-foreground",
53
+ primary: "bg-primary-ink",
54
+ blue: "bg-blue-ink",
55
+ teal: "bg-teal-ink",
56
+ green: "bg-green-ink",
57
+ yellow: "bg-yellow-ink",
58
+ orange: "bg-orange-ink",
59
+ red: "bg-destructive-ink",
60
+ purple: "bg-purple-ink",
61
+ pink: "bg-pink-ink",
62
+ navy: "bg-navy-ink"
63
+ }
64
+ },
65
+ defaultVariants: { tone: "neutral" }
66
+ });
67
+ const HEADER_ROW = 26;
68
+ const MIN_INNER_LABEL = 56;
69
+ const MIN_BAR = 6;
70
+ const SCHEDULE_DAYS = { day: 1, week: 5, month: 14, quarter: 30, year: 90 };
71
+ const EDGE_SCROLL = 40;
72
+ function useCoarsePointer() {
73
+ const subscribe = React.useCallback((onChange) => {
74
+ const mql = window.matchMedia("(pointer: coarse)");
75
+ mql.addEventListener("change", onChange);
76
+ return () => mql.removeEventListener("change", onChange);
77
+ }, []);
78
+ return React.useSyncExternalStore(
79
+ subscribe,
80
+ () => window.matchMedia("(pointer: coarse)").matches,
81
+ () => false
82
+ );
83
+ }
84
+ function Gantt({
85
+ items,
86
+ dependencies = [],
87
+ scale: scaleProp,
88
+ defaultScale = "week",
89
+ onScaleChange,
90
+ today: todayProp,
91
+ weekendDays = [0, 6],
92
+ nonWorkingDays,
93
+ weekStartsOn = 1,
94
+ range: rangeProp,
95
+ readOnly = false,
96
+ onItemChange,
97
+ onItemOpen,
98
+ onExpandedChange,
99
+ onDependencyCreate,
100
+ onDependencyClick,
101
+ rowHeight: rowHeightProp,
102
+ sidebarWidth = 280,
103
+ sidebarTitle,
104
+ renderSidebarItem,
105
+ toolbar = true,
106
+ labels,
107
+ className,
108
+ "aria-label": ariaLabel,
109
+ ...props
110
+ }) {
111
+ const t = useComitorLabels("gantt", labels);
112
+ const { locale } = useComitorLocale();
113
+ const coarse = useCoarsePointer();
114
+ const rowHeight = rowHeightProp ?? (coarse ? 44 : 36);
115
+ const barHeight = rowHeight - 12;
116
+ const [scaleState, setScaleState] = React.useState(defaultScale);
117
+ const scale = scaleProp ?? scaleState;
118
+ const setScale = React.useCallback(
119
+ (next) => {
120
+ if (scaleProp === void 0) setScaleState(next);
121
+ onScaleChange?.(next);
122
+ },
123
+ [scaleProp, onScaleChange]
124
+ );
125
+ const config = SCALE_CONFIG[scale];
126
+ const ppd = config.pxPerDay;
127
+ const headerHeight = config.major ? HEADER_ROW * 2 : HEADER_ROW;
128
+ const today = React.useMemo(() => parseDay(todayProp) ?? todayDay(), [todayProp]);
129
+ const canEdit = !readOnly && !!onItemChange;
130
+ const rows = React.useMemo(
131
+ () => items.map((item, index) => {
132
+ const start = parseDay(item.start);
133
+ let end = item.type === "milestone" ? start : parseDay(item.end);
134
+ if (start != null && (end == null || end < start)) end = start;
135
+ return { item, index, start: end == null ? null : start, end: start == null ? null : end };
136
+ }),
137
+ [items]
138
+ );
139
+ const indexById = React.useMemo(() => new Map(rows.map((row) => [row.item.id, row.index])), [rows]);
140
+ const range = React.useMemo(() => {
141
+ const days = [];
142
+ for (const row of rows) {
143
+ if (row.start != null && row.end != null) days.push(row.start, row.end);
144
+ }
145
+ const override = rangeProp ? { start: parseDay(rangeProp.start), end: parseDay(rangeProp.end) } : void 0;
146
+ return computeRange(days, today, scale, weekStartsOn, override);
147
+ }, [rows, today, scale, weekStartsOn, rangeProp]);
148
+ const timelineWidth = Math.round((range.end - range.start) * ppd);
149
+ const xOf = React.useCallback((day) => (day - range.start) * ppd, [range.start, ppd]);
150
+ const nonWorking = React.useMemo(() => {
151
+ const set = /* @__PURE__ */ new Set();
152
+ for (const value of nonWorkingDays ?? []) {
153
+ const day = parseDay(value);
154
+ if (day != null) set.add(day);
155
+ }
156
+ return set;
157
+ }, [nonWorkingDays]);
158
+ const weekendSet = React.useMemo(() => new Set(weekendDays), [weekendDays]);
159
+ const isOffDay = React.useCallback(
160
+ (day) => weekendSet.has(weekday(day)) || nonWorking.has(day),
161
+ [weekendSet, nonWorking]
162
+ );
163
+ const fmt = React.useMemo(
164
+ () => ({
165
+ dayNumber: new Intl.DateTimeFormat(locale, { day: "numeric" }),
166
+ weekdayNarrow: new Intl.DateTimeFormat(locale, { weekday: "narrow" }),
167
+ dayMonth: new Intl.DateTimeFormat(locale, { day: "numeric", month: "short" }),
168
+ monthShort: new Intl.DateTimeFormat(locale, { month: "short" }),
169
+ monthYear: new Intl.DateTimeFormat(locale, { month: "long", year: "numeric" }),
170
+ year: new Intl.DateTimeFormat(locale, { year: "numeric" }),
171
+ full: new Intl.DateTimeFormat(locale, { day: "numeric", month: "short", year: "numeric" })
172
+ }),
173
+ [locale]
174
+ );
175
+ const formatFull = React.useCallback((day) => fmt.full.format(dayToLocalDate(day)), [fmt]);
176
+ const formatShort = React.useCallback((day) => fmt.dayMonth.format(dayToLocalDate(day)), [fmt]);
177
+ const describeRange = React.useCallback(
178
+ (row) => row.item.type === "milestone" ? t.milestoneDate(formatFull(row.start)) : t.itemRange(formatFull(row.start), formatFull(row.end), row.end - row.start + 1),
179
+ [t, formatFull]
180
+ );
181
+ const scrollRef = React.useRef(null);
182
+ const [viewport, setViewport] = React.useState({ left: 0, width: 0 });
183
+ const readViewport = React.useCallback(() => {
184
+ const el = scrollRef.current;
185
+ if (!el) return;
186
+ setViewport(
187
+ (prev) => prev.left === el.scrollLeft && prev.width === el.clientWidth ? prev : { left: el.scrollLeft, width: el.clientWidth }
188
+ );
189
+ }, []);
190
+ useIsomorphicLayoutEffect(() => {
191
+ const el = scrollRef.current;
192
+ if (!el) return;
193
+ readViewport();
194
+ const observer = new ResizeObserver(readViewport);
195
+ observer.observe(el);
196
+ return () => observer.disconnect();
197
+ }, [readViewport]);
198
+ const virtualizer = useVirtualizer({
199
+ count: rows.length,
200
+ getScrollElement: () => scrollRef.current,
201
+ estimateSize: () => rowHeight,
202
+ overscan: 8,
203
+ paddingStart: headerHeight,
204
+ scrollPaddingStart: headerHeight
205
+ });
206
+ React.useEffect(() => {
207
+ virtualizer.measure();
208
+ }, [virtualizer, rowHeight, headerHeight]);
209
+ const virtualRows = virtualizer.getVirtualItems();
210
+ const firstVisible = virtualRows[0]?.index ?? 0;
211
+ const lastVisible = virtualRows[virtualRows.length - 1]?.index ?? -1;
212
+ const visibleWidth = Math.max(0, viewport.width - sidebarWidth);
213
+ const visible = { from: viewport.left, to: viewport.left + visibleWidth };
214
+ const anchorRef = React.useRef(null);
215
+ const placedRef = React.useRef(false);
216
+ const rangeRef = React.useRef(range);
217
+ rangeRef.current = range;
218
+ const ppdRef = React.useRef(ppd);
219
+ const scrollToDay = React.useCallback(
220
+ (day, behavior = "auto", ratio = 0.5) => {
221
+ const el = scrollRef.current;
222
+ if (!el) return;
223
+ const width = Math.max(0, el.clientWidth - sidebarWidth);
224
+ el.scrollTo({ left: Math.max(0, xOf(day) - width * ratio), behavior });
225
+ },
226
+ [xOf, sidebarWidth]
227
+ );
228
+ useIsomorphicLayoutEffect(() => {
229
+ const el = scrollRef.current;
230
+ if (!el || el.clientWidth === 0) return;
231
+ if (!placedRef.current) {
232
+ placedRef.current = true;
233
+ scrollToDay(today, "auto", 1 / 3);
234
+ } else if (anchorRef.current) {
235
+ scrollToDay(anchorRef.current.day);
236
+ }
237
+ anchorRef.current = null;
238
+ ppdRef.current = ppd;
239
+ readViewport();
240
+ }, [ppd, viewport.width > 0]);
241
+ const changeScale = (next) => {
242
+ const el = scrollRef.current;
243
+ if (el && next !== scale) {
244
+ const center = el.scrollLeft + Math.max(0, el.clientWidth - sidebarWidth) / 2;
245
+ anchorRef.current = { day: rangeRef.current.start + center / ppdRef.current };
246
+ }
247
+ setScale(next);
248
+ };
249
+ const [draft, setDraftState] = React.useState(null);
250
+ const draftRef = React.useRef(null);
251
+ const setDraft = React.useCallback((next) => {
252
+ const prev = draftRef.current;
253
+ if (prev === next || prev && next && prev.id === next.id && prev.start === next.start && prev.end === next.end) {
254
+ return;
255
+ }
256
+ draftRef.current = next;
257
+ setDraftState(next);
258
+ }, []);
259
+ const [linkDraft, setLinkDraft] = React.useState(null);
260
+ const dragRef = React.useRef(null);
261
+ const onItemChangeRef = React.useRef(onItemChange);
262
+ onItemChangeRef.current = onItemChange;
263
+ const datesOf = (row) => {
264
+ if (draft && draft.id === row.item.id) return draft;
265
+ if (row.start == null || row.end == null) return null;
266
+ return { start: row.start, end: row.end };
267
+ };
268
+ const geometryOf = (row) => {
269
+ const dates = datesOf(row);
270
+ if (!dates) return null;
271
+ const y = row.index * rowHeight + rowHeight / 2;
272
+ if (row.item.type === "milestone") {
273
+ const cx = xOf(dates.start) + ppd / 2;
274
+ const half = barHeight / 2 - 5;
275
+ return { x1: cx - half, x2: cx + half, y, ...dates };
276
+ }
277
+ const x1 = xOf(dates.start);
278
+ const x2 = Math.max(xOf(dates.end + 1), x1 + MIN_BAR);
279
+ return { x1, x2, y, ...dates };
280
+ };
281
+ const toTimeline = (clientX, clientY) => {
282
+ const el = scrollRef.current;
283
+ if (!el) return { x: 0, y: 0 };
284
+ const rect = el.getBoundingClientRect();
285
+ return {
286
+ x: clientX - rect.left + el.scrollLeft - sidebarWidth,
287
+ y: clientY - rect.top + el.scrollTop - headerHeight
288
+ };
289
+ };
290
+ const autoScroll = (clientX) => {
291
+ const el = scrollRef.current;
292
+ if (!el) return;
293
+ const rect = el.getBoundingClientRect();
294
+ if (clientX < rect.left + sidebarWidth + EDGE_SCROLL) el.scrollLeft -= 12;
295
+ else if (clientX > rect.right - EDGE_SCROLL) el.scrollLeft += 12;
296
+ };
297
+ const startDrag = (event, row, mode) => {
298
+ if (event.button !== 0 || !canEdit || row.item.readOnly || row.item.type === "summary") return;
299
+ const dates = datesOf(row);
300
+ if (!dates) return;
301
+ event.preventDefault();
302
+ event.stopPropagation();
303
+ focusRow(row.index, false);
304
+ dragRef.current = {
305
+ id: row.item.id,
306
+ mode: row.item.type === "milestone" ? "move" : mode,
307
+ originX: event.clientX,
308
+ originScrollLeft: scrollRef.current?.scrollLeft ?? 0,
309
+ start: dates.start,
310
+ end: dates.end,
311
+ moved: false
312
+ };
313
+ const onMove = (e) => {
314
+ const drag = dragRef.current;
315
+ if (!drag) return;
316
+ autoScroll(e.clientX);
317
+ const scrolled = (scrollRef.current?.scrollLeft ?? 0) - drag.originScrollLeft;
318
+ const dx = e.clientX - drag.originX + scrolled;
319
+ if (Math.abs(dx) > 3) drag.moved = true;
320
+ const delta = Math.round(dx / ppd);
321
+ let start = drag.start;
322
+ let end = drag.end;
323
+ if (drag.mode === "move") {
324
+ start += delta;
325
+ end += delta;
326
+ } else if (drag.mode === "resize-start") {
327
+ start = Math.min(drag.start + delta, drag.end);
328
+ } else {
329
+ end = Math.max(drag.end + delta, drag.start);
330
+ }
331
+ setDraft({ id: drag.id, start, end });
332
+ };
333
+ const finish = (commit) => {
334
+ window.removeEventListener("pointermove", onMove);
335
+ window.removeEventListener("pointerup", onUp);
336
+ window.removeEventListener("pointercancel", onCancel);
337
+ window.removeEventListener("keydown", onKey, true);
338
+ const drag = dragRef.current;
339
+ dragRef.current = null;
340
+ const current = draftRef.current;
341
+ setDraft(null);
342
+ if (commit && drag && current && (current.start !== drag.start || current.end !== drag.end)) {
343
+ onItemChangeRef.current?.({
344
+ id: drag.id,
345
+ start: formatDay(current.start),
346
+ end: formatDay(current.end),
347
+ kind: drag.mode
348
+ });
349
+ }
350
+ };
351
+ const onUp = () => finish(true);
352
+ const onCancel = () => finish(false);
353
+ const onKey = (e) => {
354
+ if (e.key === "Escape") {
355
+ e.preventDefault();
356
+ e.stopPropagation();
357
+ finish(false);
358
+ }
359
+ };
360
+ window.addEventListener("pointermove", onMove);
361
+ window.addEventListener("pointerup", onUp);
362
+ window.addEventListener("pointercancel", onCancel);
363
+ window.addEventListener("keydown", onKey, true);
364
+ };
365
+ const startLink = (event, row) => {
366
+ if (event.button !== 0 || !onDependencyCreate) return;
367
+ event.preventDefault();
368
+ event.stopPropagation();
369
+ const from = row.item.id;
370
+ const update = (e) => {
371
+ autoScroll(e.clientX);
372
+ const point = toTimeline(e.clientX, e.clientY);
373
+ const index = Math.floor(point.y / rowHeight);
374
+ const target = rows[index];
375
+ const valid = target && target.item.id !== from && target.start != null && target.item.type !== "summary" ? target.item.id : null;
376
+ setLinkDraft({ from, x: point.x, y: point.y, target: valid });
377
+ };
378
+ const end = (commit, e) => {
379
+ window.removeEventListener("pointermove", update);
380
+ window.removeEventListener("pointerup", onUp);
381
+ window.removeEventListener("pointercancel", onCancel);
382
+ setLinkDraft(null);
383
+ if (!commit || !e) return;
384
+ const point = toTimeline(e.clientX, e.clientY);
385
+ const target = rows[Math.floor(point.y / rowHeight)];
386
+ if (target && target.item.id !== from && target.start != null && target.item.type !== "summary") {
387
+ onDependencyCreate({ from, to: target.item.id, type: "finish-to-start" });
388
+ }
389
+ };
390
+ const onUp = (e) => end(true, e);
391
+ const onCancel = () => end(false);
392
+ window.addEventListener("pointermove", update);
393
+ window.addEventListener("pointerup", onUp);
394
+ window.addEventListener("pointercancel", onCancel);
395
+ update(event.nativeEvent);
396
+ };
397
+ const [hover, setHover] = React.useState(null);
398
+ const onPointerMove = (event) => {
399
+ if (dragRef.current || event.pointerType === "touch") return;
400
+ const point = toTimeline(event.clientX, event.clientY);
401
+ const index = Math.floor(point.y / rowHeight);
402
+ const inTrack = point.x >= viewport.left && point.y >= 0 && index < rows.length;
403
+ const next = inTrack ? { index, day: range.start + Math.floor(point.x / ppd) } : null;
404
+ setHover((prev) => prev?.index === next?.index && prev?.day === next?.day ? prev : next);
405
+ };
406
+ const schedule = (row, day) => {
407
+ if (!canEdit || row.item.readOnly) return;
408
+ const length = row.item.type === "milestone" ? 1 : SCHEDULE_DAYS[scale];
409
+ onItemChange?.({ id: row.item.id, start: formatDay(day), end: formatDay(day + length - 1), kind: "schedule" });
410
+ };
411
+ const [focusedIndex, setFocusedIndex] = React.useState(0);
412
+ const pendingFocusRef = React.useRef(null);
413
+ const [announcement, setAnnouncement] = React.useState("");
414
+ const focusIndex = Math.min(focusedIndex, Math.max(0, rows.length - 1));
415
+ const focusRow = (index, scroll = true) => {
416
+ const clamped = Math.max(0, Math.min(rows.length - 1, index));
417
+ setFocusedIndex(clamped);
418
+ pendingFocusRef.current = clamped;
419
+ if (scroll) virtualizer.scrollToIndex(clamped, { align: "auto" });
420
+ };
421
+ React.useEffect(() => {
422
+ const index = pendingFocusRef.current;
423
+ if (index == null) return;
424
+ const el = scrollRef.current?.querySelector(`[data-gantt-row="${index}"]`);
425
+ if (el) {
426
+ pendingFocusRef.current = null;
427
+ if (document.activeElement !== el) el.focus({ preventScroll: true });
428
+ }
429
+ }, [focusIndex, virtualRows]);
430
+ const revealBar = (geometry) => {
431
+ const el = scrollRef.current;
432
+ if (!el || !geometry) return;
433
+ const width = Math.max(0, el.clientWidth - sidebarWidth);
434
+ if (geometry.x2 < el.scrollLeft + 24 || geometry.x1 > el.scrollLeft + width - 24) {
435
+ el.scrollLeft = Math.max(0, geometry.x1 - Math.min(96, width / 4));
436
+ }
437
+ };
438
+ const commitKeyboard = (row, start, end, kind) => {
439
+ if (!onItemChange) return;
440
+ onItemChange({ id: row.item.id, start: formatDay(start), end: formatDay(end), kind });
441
+ const text = row.item.type === "milestone" ? formatFull(start) : `${formatFull(start)} \u2013 ${formatFull(end)}`;
442
+ setAnnouncement(t.rescheduled(row.item.title, text));
443
+ revealBar({ x1: xOf(start), x2: xOf(end + 1) });
444
+ };
445
+ const onRowKeyDown = (event, row) => {
446
+ if (event.target !== event.currentTarget) return;
447
+ const page = Math.max(1, Math.floor((scrollRef.current?.clientHeight ?? 0) / rowHeight) - 1);
448
+ switch (event.key) {
449
+ case "ArrowDown":
450
+ event.preventDefault();
451
+ focusRow(row.index + 1);
452
+ revealBar(geometryOf(rows[row.index + 1] ?? row));
453
+ return;
454
+ case "ArrowUp":
455
+ event.preventDefault();
456
+ focusRow(row.index - 1);
457
+ revealBar(geometryOf(rows[row.index - 1] ?? row));
458
+ return;
459
+ case "Home":
460
+ event.preventDefault();
461
+ focusRow(0);
462
+ return;
463
+ case "End":
464
+ event.preventDefault();
465
+ focusRow(rows.length - 1);
466
+ return;
467
+ case "PageDown":
468
+ event.preventDefault();
469
+ focusRow(row.index + page);
470
+ return;
471
+ case "PageUp":
472
+ event.preventDefault();
473
+ focusRow(row.index - page);
474
+ return;
475
+ case "Enter":
476
+ event.preventDefault();
477
+ onItemOpen?.(row.item);
478
+ return;
479
+ case " ":
480
+ if (row.item.hasChildren && onExpandedChange) {
481
+ event.preventDefault();
482
+ onExpandedChange(row.item.id, !row.item.expanded);
483
+ }
484
+ return;
485
+ case "ArrowLeft":
486
+ case "ArrowRight": {
487
+ if (!canEdit || row.item.readOnly || row.item.type === "summary") return;
488
+ if (row.start == null || row.end == null) return;
489
+ event.preventDefault();
490
+ const direction = event.key === "ArrowRight" ? 1 : -1;
491
+ const milestone = row.item.type === "milestone";
492
+ if (event.altKey && !milestone) {
493
+ const step = event.shiftKey ? "resize-start" : "resize-end";
494
+ if (step === "resize-end") {
495
+ commitKeyboard(row, row.start, Math.max(row.start, row.end + direction), step);
496
+ } else {
497
+ commitKeyboard(row, Math.min(row.end, row.start + direction), row.end, step);
498
+ }
499
+ return;
500
+ }
501
+ const next = event.shiftKey ? shiftByUnit(row.start, row.end, config.bigStep, direction) : { start: row.start + direction, end: row.end + direction };
502
+ commitKeyboard(row, next.start, next.end, "move");
503
+ return;
504
+ }
505
+ }
506
+ };
507
+ const conflicted = React.useMemo(() => {
508
+ const set = /* @__PURE__ */ new Set();
509
+ for (const dep of dependencies) {
510
+ const a = rows[indexById.get(dep.from) ?? -1];
511
+ const b = rows[indexById.get(dep.to) ?? -1];
512
+ if (!a || !b || a.start == null || a.end == null || b.start == null || b.end == null) continue;
513
+ if (isDependencyViolated(
514
+ { start: a.start, end: a.end },
515
+ { start: b.start, end: b.end },
516
+ dep.type ?? "finish-to-start"
517
+ )) {
518
+ set.add(dep.to);
519
+ }
520
+ }
521
+ return set;
522
+ }, [dependencies, rows, indexById]);
523
+ const dependencyPaths = [];
524
+ for (const dep of dependencies) {
525
+ const a = rows[indexById.get(dep.from) ?? -1];
526
+ const b = rows[indexById.get(dep.to) ?? -1];
527
+ if (!a || !b) continue;
528
+ if (Math.max(a.index, b.index) < firstVisible || Math.min(a.index, b.index) > lastVisible) continue;
529
+ const ga = geometryOf(a);
530
+ const gb = geometryOf(b);
531
+ if (!ga || !gb) continue;
532
+ const type = dep.type ?? "finish-to-start";
533
+ dependencyPaths.push({
534
+ dep,
535
+ d: routeDependency(ga, gb, type, rowHeight),
536
+ violated: isDependencyViolated(ga, gb, type),
537
+ label: t.dependency(a.item.title, b.item.title)
538
+ });
539
+ }
540
+ const overscanPx = 400;
541
+ const visFrom = range.start + Math.max(0, Math.floor((visible.from - overscanPx) / ppd));
542
+ const visTo = Math.min(range.end, range.start + Math.ceil((visible.to + overscanPx) / ppd));
543
+ const minorTicks = viewport.width > 0 ? ticksBetween(visFrom, visTo, config.minor, weekStartsOn) : [];
544
+ const majorTicks = viewport.width > 0 && config.major ? ticksBetween(visFrom, visTo, config.major, weekStartsOn) : [];
545
+ const minorLabel = (day) => {
546
+ const date = dayToLocalDate(day);
547
+ switch (config.minor) {
548
+ case "day":
549
+ return fmt.dayNumber.format(date);
550
+ case "week":
551
+ return fmt.dayMonth.format(date);
552
+ case "month":
553
+ return fmt.monthShort.format(date);
554
+ case "quarter":
555
+ return t.quarter(Math.floor(date.getMonth() / 3) + 1, date.getFullYear());
556
+ default:
557
+ return fmt.year.format(date);
558
+ }
559
+ };
560
+ const majorLabel = (day) => config.major === "month" ? fmt.monthYear.format(dayToLocalDate(day)) : fmt.year.format(dayToLocalDate(day));
561
+ const offBands = [];
562
+ if (config.shadeDays && viewport.width > 0) {
563
+ for (let day = visFrom; day < visTo; day++) {
564
+ if (!isOffDay(day)) continue;
565
+ const last = offBands[offBands.length - 1];
566
+ if (last && last.end === day) last.end = day + 1;
567
+ else offBands.push({ start: day, end: day + 1 });
568
+ }
569
+ }
570
+ const rowsHeight = rows.length * rowHeight;
571
+ const totalHeight = headerHeight + rowsHeight;
572
+ const totalWidth = sidebarWidth + timelineWidth;
573
+ const todayX = xOf(today) + ppd / 2;
574
+ const showToday = today >= range.start && today < range.end;
575
+ const instructionsId = React.useId();
576
+ const toolbarExtra = typeof toolbar === "boolean" ? null : toolbar;
577
+ const scaleNames = {
578
+ day: t.scaleDay,
579
+ week: t.scaleWeek,
580
+ month: t.scaleMonth,
581
+ quarter: t.scaleQuarter,
582
+ year: t.scaleYear
583
+ };
584
+ return /* @__PURE__ */ jsxs(
585
+ "div",
586
+ {
587
+ "data-slot": "gantt",
588
+ "data-scale": scale,
589
+ className: cn("flex min-h-0 flex-col overflow-hidden rounded-lg border bg-card text-card-foreground", className),
590
+ ...props,
591
+ children: [
592
+ toolbar !== false && /* @__PURE__ */ jsxs("div", { "data-slot": "gantt-toolbar", className: "flex flex-wrap items-center gap-2 border-b px-3 py-2", children: [
593
+ /* @__PURE__ */ jsx(
594
+ ToggleGroup,
595
+ {
596
+ type: "single",
597
+ variant: "outline",
598
+ size: "sm",
599
+ value: scale,
600
+ onValueChange: (value) => value && changeScale(value),
601
+ "aria-label": t.scaleLabel,
602
+ children: GANTT_SCALES.map((value) => /* @__PURE__ */ jsx(ToggleGroupItem, { value, className: "px-2.5 text-xs", children: scaleNames[value] }, value))
603
+ }
604
+ ),
605
+ /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", onClick: () => scrollToDay(today, "smooth", 1 / 3), children: t.today }),
606
+ toolbarExtra && /* @__PURE__ */ jsx("div", { className: "ms-auto flex items-center gap-2", children: toolbarExtra })
607
+ ] }),
608
+ /* @__PURE__ */ jsx(
609
+ "div",
610
+ {
611
+ ref: scrollRef,
612
+ "data-slot": "gantt-viewport",
613
+ className: "relative min-h-0 flex-1 overflow-auto overscroll-contain",
614
+ onScroll: readViewport,
615
+ onPointerMove,
616
+ onPointerLeave: () => setHover(null),
617
+ children: /* @__PURE__ */ jsxs(
618
+ "div",
619
+ {
620
+ role: "treegrid",
621
+ "aria-label": ariaLabel ?? t.label,
622
+ "aria-rowcount": rows.length + 1,
623
+ "aria-colcount": 2,
624
+ "aria-describedby": instructionsId,
625
+ "aria-readonly": !canEdit || void 0,
626
+ className: "group/gantt relative",
627
+ style: { width: totalWidth, height: totalHeight },
628
+ children: [
629
+ /* @__PURE__ */ jsxs(
630
+ "div",
631
+ {
632
+ role: "row",
633
+ "aria-rowindex": 1,
634
+ tabIndex: -1,
635
+ className: "sticky top-0 z-40 flex",
636
+ style: { height: headerHeight },
637
+ children: [
638
+ /* @__PURE__ */ jsx(
639
+ "div",
640
+ {
641
+ role: "columnheader",
642
+ tabIndex: -1,
643
+ className: "sticky left-0 z-10 flex items-end border-e border-b bg-card px-3 pb-1.5 font-medium text-muted-foreground text-xs",
644
+ style: { width: sidebarWidth },
645
+ children: sidebarTitle ?? t.sidebarTitle
646
+ }
647
+ ),
648
+ /* @__PURE__ */ jsxs(
649
+ "div",
650
+ {
651
+ role: "columnheader",
652
+ tabIndex: -1,
653
+ "aria-label": t.label,
654
+ className: "relative border-b bg-card",
655
+ style: { width: timelineWidth },
656
+ children: [
657
+ majorTicks.map((tick) => /* @__PURE__ */ jsx(
658
+ "div",
659
+ {
660
+ className: "absolute top-0 border-s border-border",
661
+ style: { left: xOf(tick.day), width: (tick.next - tick.day) * ppd, height: HEADER_ROW },
662
+ children: /* @__PURE__ */ jsx(
663
+ "span",
664
+ {
665
+ className: "sticky inline-block whitespace-nowrap px-2 pt-1.5 font-medium text-foreground text-xs",
666
+ style: { left: sidebarWidth },
667
+ children: majorLabel(tick.day)
668
+ }
669
+ )
670
+ },
671
+ `major-${tick.day}`
672
+ )),
673
+ minorTicks.map((tick) => {
674
+ const width = (tick.next - tick.day) * ppd;
675
+ const isToday = config.minor === "day" && tick.day === today;
676
+ return /* @__PURE__ */ jsx(
677
+ "div",
678
+ {
679
+ title: formatFull(tick.day),
680
+ className: cn(
681
+ "absolute flex items-center border-s border-border text-muted-foreground text-xs tabular-nums",
682
+ config.minor === "day" ? "justify-center" : "px-2"
683
+ ),
684
+ style: { left: xOf(tick.day), width, top: headerHeight - HEADER_ROW, height: HEADER_ROW },
685
+ children: isToday ? /* @__PURE__ */ jsx("span", { className: "rounded-sm bg-primary px-1 font-semibold text-primary-foreground", children: minorLabel(tick.day) }) : /* @__PURE__ */ jsxs("span", { className: "truncate", children: [
686
+ config.minor === "day" && /* @__PURE__ */ jsx("span", { className: "me-0.5 text-[10px]", children: fmt.weekdayNarrow.format(dayToLocalDate(tick.day)) }),
687
+ minorLabel(tick.day)
688
+ ] })
689
+ },
690
+ `minor-${tick.day}`
691
+ );
692
+ }),
693
+ showToday && config.minor !== "day" && /* @__PURE__ */ jsx(
694
+ "span",
695
+ {
696
+ "aria-hidden": "true",
697
+ className: "absolute bottom-1 -translate-x-1/2 whitespace-nowrap rounded-sm bg-primary px-1 font-semibold text-[11px] text-primary-foreground",
698
+ style: { left: todayX },
699
+ children: formatShort(today)
700
+ }
701
+ )
702
+ ]
703
+ }
704
+ )
705
+ ]
706
+ }
707
+ ),
708
+ /* @__PURE__ */ jsxs(
709
+ "div",
710
+ {
711
+ "aria-hidden": "true",
712
+ className: "pointer-events-none absolute z-0",
713
+ style: { top: headerHeight, left: sidebarWidth, width: timelineWidth, height: rowsHeight },
714
+ children: [
715
+ offBands.map((band) => /* @__PURE__ */ jsx(
716
+ "div",
717
+ {
718
+ "data-slot": "gantt-off-day",
719
+ className: "absolute inset-y-0 bg-muted",
720
+ style: { left: xOf(band.start), width: (band.end - band.start) * ppd }
721
+ },
722
+ `off-${band.start}`
723
+ )),
724
+ minorTicks.map((tick) => /* @__PURE__ */ jsx(
725
+ "div",
726
+ {
727
+ className: "absolute inset-y-0 border-s border-border",
728
+ style: { left: xOf(tick.day) }
729
+ },
730
+ `line-${tick.day}`
731
+ )),
732
+ showToday && /* @__PURE__ */ jsx(
733
+ "div",
734
+ {
735
+ "data-slot": "gantt-today",
736
+ className: "absolute inset-y-0 w-0.5 -translate-x-1/2 bg-primary-ink",
737
+ style: { left: todayX }
738
+ }
739
+ )
740
+ ]
741
+ }
742
+ ),
743
+ /* @__PURE__ */ jsxs(
744
+ "svg",
745
+ {
746
+ "aria-hidden": "true",
747
+ className: "pointer-events-none absolute z-10 overflow-visible",
748
+ style: { top: headerHeight, left: sidebarWidth },
749
+ width: timelineWidth,
750
+ height: rowsHeight,
751
+ children: [
752
+ /* @__PURE__ */ jsxs("defs", { children: [
753
+ /* @__PURE__ */ jsx(
754
+ "marker",
755
+ {
756
+ id: `${instructionsId}-arrow`,
757
+ viewBox: "0 0 8 8",
758
+ refX: "7",
759
+ refY: "4",
760
+ markerWidth: "7",
761
+ markerHeight: "7",
762
+ orient: "auto",
763
+ children: /* @__PURE__ */ jsx("path", { d: "M0,0 L8,4 L0,8 z", className: "fill-muted-foreground" })
764
+ }
765
+ ),
766
+ /* @__PURE__ */ jsx(
767
+ "marker",
768
+ {
769
+ id: `${instructionsId}-arrow-alert`,
770
+ viewBox: "0 0 8 8",
771
+ refX: "7",
772
+ refY: "4",
773
+ markerWidth: "7",
774
+ markerHeight: "7",
775
+ orient: "auto",
776
+ children: /* @__PURE__ */ jsx("path", { d: "M0,0 L8,4 L0,8 z", className: "fill-destructive-ink" })
777
+ }
778
+ )
779
+ ] }),
780
+ dependencyPaths.map(({ dep, d, violated, label }) => {
781
+ const alert = violated || dep.critical;
782
+ return /* @__PURE__ */ jsxs(
783
+ "g",
784
+ {
785
+ "data-slot": "gantt-dependency",
786
+ "data-critical": dep.critical || void 0,
787
+ "data-conflict": violated || void 0,
788
+ children: [
789
+ /* @__PURE__ */ jsx("title", { children: label }),
790
+ /* @__PURE__ */ jsx(
791
+ "path",
792
+ {
793
+ d,
794
+ fill: "none",
795
+ strokeWidth: dep.critical ? 2 : 1.5,
796
+ strokeDasharray: violated ? "4 3" : void 0,
797
+ markerEnd: `url(#${instructionsId}-${alert ? "arrow-alert" : "arrow"})`,
798
+ className: alert ? "stroke-destructive-ink" : "stroke-muted-foreground"
799
+ }
800
+ ),
801
+ onDependencyClick && // Vùng bấm rộng 12px dọc theo đường — nét 1,5px thì không ai bấm trúng.
802
+ // biome-ignore lint/a11y/noStaticElementInteractions: mũi tên chỉ là lối tắt CHUỘT; app có lối bàn phím riêng
803
+ /* @__PURE__ */ jsx(
804
+ "path",
805
+ {
806
+ d,
807
+ fill: "none",
808
+ stroke: "transparent",
809
+ strokeWidth: 12,
810
+ className: "pointer-events-auto cursor-pointer",
811
+ onClick: () => onDependencyClick(dep)
812
+ }
813
+ )
814
+ ]
815
+ },
816
+ dep.id
817
+ );
818
+ }),
819
+ linkDraft && (() => {
820
+ const source = rows[indexById.get(linkDraft.from) ?? -1];
821
+ const geometry = source ? geometryOf(source) : null;
822
+ if (!geometry) return null;
823
+ return /* @__PURE__ */ jsx(
824
+ "path",
825
+ {
826
+ d: `M${geometry.x2},${geometry.y} L${linkDraft.x},${linkDraft.y}`,
827
+ fill: "none",
828
+ strokeWidth: 1.5,
829
+ strokeDasharray: "4 3",
830
+ markerEnd: `url(#${instructionsId}-arrow)`,
831
+ className: "stroke-muted-foreground"
832
+ }
833
+ );
834
+ })()
835
+ ]
836
+ }
837
+ ),
838
+ virtualRows.map((virtualRow) => {
839
+ const row = rows[virtualRow.index];
840
+ if (!row) return null;
841
+ const { item } = row;
842
+ const geometry = geometryOf(row);
843
+ const tone = item.tone ?? "neutral";
844
+ const depth = item.depth ?? 0;
845
+ const editable = canEdit && !item.readOnly && item.type !== "summary";
846
+ const isDragging = draft?.id === item.id;
847
+ const isHovered = hover?.index === row.index;
848
+ const isLinkTarget = linkDraft?.target === item.id;
849
+ const conflict = conflicted.has(item.id);
850
+ const description = [
851
+ geometry ? describeRange({ item, start: geometry.start, end: geometry.end }) : t.unscheduled,
852
+ item.progress != null ? t.progress(Math.round(item.progress)) : null,
853
+ item.critical ? t.critical : null,
854
+ conflict ? t.conflict : null
855
+ ].filter(Boolean).join(". ");
856
+ return /* @__PURE__ */ jsxs(
857
+ "div",
858
+ {
859
+ role: "row",
860
+ "aria-rowindex": row.index + 2,
861
+ "aria-level": depth + 1,
862
+ "aria-expanded": item.hasChildren ? !!item.expanded : void 0,
863
+ "aria-selected": row.index === focusIndex,
864
+ tabIndex: row.index === focusIndex ? 0 : -1,
865
+ "data-gantt-row": row.index,
866
+ "data-slot": "gantt-row",
867
+ className: "group/row pointer-events-none absolute left-0 z-20 flex outline-hidden focus-visible:outline-2 focus-visible:outline-solid focus-visible:-outline-offset-2 focus-visible:outline-ring",
868
+ style: { top: virtualRow.start, height: rowHeight, width: totalWidth },
869
+ onKeyDown: (event) => onRowKeyDown(event, row),
870
+ onFocus: (event) => {
871
+ setFocusedIndex(row.index);
872
+ if (event.target !== event.currentTarget) event.currentTarget.focus({ preventScroll: true });
873
+ },
874
+ children: [
875
+ /* @__PURE__ */ jsxs(
876
+ "div",
877
+ {
878
+ role: "rowheader",
879
+ tabIndex: -1,
880
+ className: cn(
881
+ "pointer-events-auto sticky left-0 z-10 flex min-w-0 items-center gap-1 border-e border-b bg-card pe-2",
882
+ isHovered && "bg-muted",
883
+ // Dòng đang chọn chỉ tô khi lưới đang giữ focus — rời lưới thì vệt tô không còn
884
+ // đứng đó như một lựa chọn thật.
885
+ row.index === focusIndex && "group-focus-within/gantt:bg-accent"
886
+ ),
887
+ style: { width: sidebarWidth, paddingInlineStart: 8 + depth * 16 },
888
+ children: [
889
+ item.hasChildren ? /* @__PURE__ */ jsx(
890
+ "button",
891
+ {
892
+ type: "button",
893
+ tabIndex: -1,
894
+ "aria-label": item.expanded ? t.collapse : t.expand,
895
+ className: "flex size-5 shrink-0 items-center justify-center rounded-sm text-muted-foreground hover:bg-accent hover:text-accent-foreground",
896
+ onClick: () => onExpandedChange?.(item.id, !item.expanded),
897
+ children: item.expanded ? /* @__PURE__ */ jsx(ChevronDown, { "aria-hidden": "true", className: "size-3.5" }) : /* @__PURE__ */ jsx(ChevronRight, { "aria-hidden": "true", className: "size-3.5 rtl:rotate-180" })
898
+ }
899
+ ) : /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "w-5 shrink-0" }),
900
+ /* @__PURE__ */ jsx(
901
+ "div",
902
+ {
903
+ className: cn(
904
+ "min-w-0 flex-1 truncate text-foreground text-sm",
905
+ item.type === "summary" && "font-semibold",
906
+ onItemOpen && "cursor-pointer"
907
+ ),
908
+ onClick: () => {
909
+ focusRow(row.index, false);
910
+ onItemOpen?.(item);
911
+ },
912
+ children: renderSidebarItem ? renderSidebarItem(item) : item.title
913
+ }
914
+ )
915
+ ]
916
+ }
917
+ ),
918
+ /* @__PURE__ */ jsxs(
919
+ "div",
920
+ {
921
+ role: "gridcell",
922
+ tabIndex: -1,
923
+ className: cn("relative border-b border-border", isHovered && "bg-muted/40"),
924
+ style: { width: timelineWidth },
925
+ children: [
926
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: description }),
927
+ !geometry && editable && // Dòng chưa có ngày: cả dải trục là một vùng bấm để xếp lịch ở ngày dưới con trỏ.
928
+ /* @__PURE__ */ jsx(
929
+ "div",
930
+ {
931
+ "aria-hidden": "true",
932
+ className: "pointer-events-auto absolute inset-0 cursor-copy",
933
+ onClick: (event) => {
934
+ const point = toTimeline(event.clientX, event.clientY);
935
+ schedule(row, range.start + Math.floor(point.x / ppd));
936
+ },
937
+ children: isHovered && hover && /* @__PURE__ */ jsx(
938
+ "div",
939
+ {
940
+ className: "absolute flex items-center rounded-md border border-control-edge-strong border-dashed px-2 text-muted-foreground text-xs",
941
+ style: {
942
+ left: xOf(hover.day),
943
+ width: Math.max(SCHEDULE_DAYS[scale] * ppd, 96),
944
+ top: (rowHeight - barHeight) / 2,
945
+ height: barHeight
946
+ },
947
+ children: /* @__PURE__ */ jsx("span", { className: "truncate", children: t.scheduleHint })
948
+ }
949
+ )
950
+ }
951
+ ),
952
+ geometry && item.type === "milestone" && /* @__PURE__ */ jsx(
953
+ "div",
954
+ {
955
+ "aria-hidden": "true",
956
+ "data-slot": "gantt-milestone",
957
+ "data-critical": item.critical || void 0,
958
+ className: cn(
959
+ "pointer-events-auto absolute rotate-45 rounded-[2px]",
960
+ ganttMarkVariants({ tone }),
961
+ item.critical && "outline-2 outline-destructive-ink",
962
+ editable ? "cursor-grab" : "cursor-default",
963
+ isDragging && "cursor-grabbing shadow-overlay"
964
+ ),
965
+ style: {
966
+ left: geometry.x1,
967
+ top: rowHeight / 2 - (geometry.x2 - geometry.x1) / 2,
968
+ width: geometry.x2 - geometry.x1,
969
+ height: geometry.x2 - geometry.x1
970
+ },
971
+ onPointerDown: (event) => startDrag(event, row, "move"),
972
+ onDoubleClick: () => onItemOpen?.(item)
973
+ }
974
+ ),
975
+ geometry && item.type === "summary" && /* @__PURE__ */ jsx(
976
+ "div",
977
+ {
978
+ "aria-hidden": "true",
979
+ "data-slot": "gantt-summary",
980
+ className: cn("absolute h-2 rounded-sm", ganttMarkVariants({ tone })),
981
+ style: { left: geometry.x1, width: geometry.x2 - geometry.x1, top: rowHeight / 2 - 4 }
982
+ }
983
+ ),
984
+ geometry && (item.type ?? "task") === "task" && /* @__PURE__ */ jsxs(Fragment, { children: [
985
+ /* @__PURE__ */ jsxs(
986
+ "div",
987
+ {
988
+ "aria-hidden": "true",
989
+ "data-slot": "gantt-bar",
990
+ "data-critical": item.critical || void 0,
991
+ "data-conflict": conflict || void 0,
992
+ "data-dragging": isDragging || void 0,
993
+ className: cn(
994
+ ganttBarVariants({ tone }),
995
+ "pointer-events-auto overflow-visible",
996
+ item.critical && "border-2 border-destructive-ink",
997
+ isLinkTarget && "outline-2 outline-offset-2 outline-ring",
998
+ editable ? "cursor-grab" : "cursor-default",
999
+ isDragging && "z-10 cursor-grabbing shadow-overlay"
1000
+ ),
1001
+ style: {
1002
+ left: geometry.x1,
1003
+ width: geometry.x2 - geometry.x1,
1004
+ top: (rowHeight - barHeight) / 2,
1005
+ height: barHeight
1006
+ },
1007
+ onPointerDown: (event) => startDrag(event, row, "move"),
1008
+ onDoubleClick: () => onItemOpen?.(item),
1009
+ children: [
1010
+ geometry.x2 - geometry.x1 >= MIN_INNER_LABEL && /* @__PURE__ */ jsx("span", { className: "min-w-0 truncate px-2", children: item.title }),
1011
+ item.progress != null && item.progress > 0 && /* @__PURE__ */ jsx(
1012
+ "span",
1013
+ {
1014
+ className: cn(
1015
+ "absolute bottom-0 left-0 h-[3px] rounded-bs-md",
1016
+ ganttMarkVariants({ tone })
1017
+ ),
1018
+ style: { width: `${Math.min(100, item.progress)}%` }
1019
+ }
1020
+ ),
1021
+ editable && /* @__PURE__ */ jsxs(Fragment, { children: [
1022
+ !(coarse && geometry.x2 - geometry.x1 < 96) && /* @__PURE__ */ jsx(
1023
+ "span",
1024
+ {
1025
+ "data-slot": "gantt-handle",
1026
+ className: cn(
1027
+ "absolute inset-y-0 cursor-ew-resize",
1028
+ coarse ? "-start-[22px] w-11" : "start-0 w-2"
1029
+ ),
1030
+ onPointerDown: (event) => startDrag(event, row, "resize-start")
1031
+ }
1032
+ ),
1033
+ /* @__PURE__ */ jsx(
1034
+ "span",
1035
+ {
1036
+ "data-slot": "gantt-handle",
1037
+ className: cn(
1038
+ "absolute inset-y-0 cursor-ew-resize",
1039
+ coarse ? geometry.x2 - geometry.x1 < 96 ? "-end-11 w-11" : "-end-[22px] w-11" : "end-0 w-2"
1040
+ ),
1041
+ onPointerDown: (event) => startDrag(event, row, "resize-end")
1042
+ }
1043
+ )
1044
+ ] }),
1045
+ onDependencyCreate && editable && !coarse && /* @__PURE__ */ jsx(
1046
+ "span",
1047
+ {
1048
+ "data-slot": "gantt-link-handle",
1049
+ title: t.linkHandle(item.title),
1050
+ className: "invisible absolute -end-4 top-1/2 size-2.5 -translate-y-1/2 cursor-crosshair rounded-full bg-primary-ink group-hover/row:visible",
1051
+ onPointerDown: (event) => startLink(event, row)
1052
+ }
1053
+ )
1054
+ ]
1055
+ }
1056
+ ),
1057
+ geometry.x2 - geometry.x1 < MIN_INNER_LABEL && /* @__PURE__ */ jsx(
1058
+ "span",
1059
+ {
1060
+ "aria-hidden": "true",
1061
+ className: "pointer-events-none absolute flex items-center whitespace-nowrap text-foreground text-xs",
1062
+ style: {
1063
+ left: geometry.x2 + (onDependencyCreate ? 20 : 6),
1064
+ top: 0,
1065
+ height: rowHeight
1066
+ },
1067
+ children: item.title
1068
+ }
1069
+ )
1070
+ ] }),
1071
+ geometry && item.type !== "summary" && (isHovered || isDragging) && /* @__PURE__ */ jsxs(Fragment, { children: [
1072
+ /* @__PURE__ */ jsx(
1073
+ "span",
1074
+ {
1075
+ "aria-hidden": "true",
1076
+ className: "pointer-events-none absolute flex -translate-x-full items-center whitespace-nowrap pe-1.5 text-[11px] text-muted-foreground tabular-nums",
1077
+ style: { left: geometry.x1, top: 0, height: rowHeight },
1078
+ children: formatShort(geometry.start)
1079
+ }
1080
+ ),
1081
+ item.type !== "milestone" && geometry.x2 - geometry.x1 >= MIN_INNER_LABEL && /* @__PURE__ */ jsx(
1082
+ "span",
1083
+ {
1084
+ "aria-hidden": "true",
1085
+ className: "pointer-events-none absolute flex items-center whitespace-nowrap ps-1.5 text-[11px] text-muted-foreground tabular-nums",
1086
+ style: { left: geometry.x2 + (onDependencyCreate ? 14 : 0), top: 0, height: rowHeight },
1087
+ children: formatShort(geometry.end)
1088
+ }
1089
+ )
1090
+ ] }),
1091
+ geometry && viewport.width > 0 && geometry.x2 < visible.from + 4 && /* @__PURE__ */ jsx(
1092
+ "button",
1093
+ {
1094
+ type: "button",
1095
+ tabIndex: -1,
1096
+ "aria-label": t.scrollToItem(item.title),
1097
+ className: "pointer-events-auto absolute flex size-6 items-center justify-center rounded-full border border-input bg-card text-muted-foreground shadow-control hover:text-foreground",
1098
+ style: { left: visible.from + 6, top: rowHeight / 2 - 12 },
1099
+ onClick: () => revealBar(geometry),
1100
+ children: /* @__PURE__ */ jsx(ChevronLeft, { "aria-hidden": "true", className: "size-3.5" })
1101
+ }
1102
+ ),
1103
+ geometry && viewport.width > 0 && geometry.x1 > visible.to - 4 && /* @__PURE__ */ jsx(
1104
+ "button",
1105
+ {
1106
+ type: "button",
1107
+ tabIndex: -1,
1108
+ "aria-label": t.scrollToItem(item.title),
1109
+ className: "pointer-events-auto absolute flex size-6 items-center justify-center rounded-full border border-input bg-card text-muted-foreground shadow-control hover:text-foreground",
1110
+ style: { left: visible.to - 30, top: rowHeight / 2 - 12 },
1111
+ onClick: () => revealBar(geometry),
1112
+ children: /* @__PURE__ */ jsx(ChevronRight, { "aria-hidden": "true", className: "size-3.5" })
1113
+ }
1114
+ )
1115
+ ]
1116
+ }
1117
+ )
1118
+ ]
1119
+ },
1120
+ item.id
1121
+ );
1122
+ })
1123
+ ]
1124
+ }
1125
+ )
1126
+ }
1127
+ ),
1128
+ /* @__PURE__ */ jsx("p", { id: instructionsId, className: "sr-only", children: t.instructions }),
1129
+ /* @__PURE__ */ jsx("div", { role: "status", "aria-live": "polite", className: "sr-only", children: announcement })
1130
+ ]
1131
+ }
1132
+ );
1133
+ }
1134
+ export {
1135
+ Gantt
1136
+ };
1137
+ //# sourceMappingURL=gantt.js.map