@tensor_1/react 2.35.0 → 2.37.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 (39) hide show
  1. package/README.md +154 -15
  2. package/dist/components/Avatar.d.ts +26 -4
  3. package/dist/components/Avatar.js +26 -3
  4. package/dist/components/CommandPalette.d.ts +6 -1
  5. package/dist/components/CommandPalette.js +9 -3
  6. package/dist/components/DateFieldParts.d.ts +61 -0
  7. package/dist/components/DateFieldParts.js +195 -0
  8. package/dist/components/DatePicker.d.ts +129 -0
  9. package/dist/components/DatePicker.js +340 -0
  10. package/dist/components/DateRangePicker.d.ts +34 -3
  11. package/dist/components/DateRangePicker.js +120 -17
  12. package/dist/components/EntityPicker.d.ts +29 -4
  13. package/dist/components/EntityPicker.js +20 -8
  14. package/dist/components/InlineEdit.d.ts +74 -0
  15. package/dist/components/InlineEdit.js +176 -0
  16. package/dist/components/Radio.d.ts +23 -2
  17. package/dist/components/Radio.js +18 -3
  18. package/dist/components/SchedulingAgenda.js +3 -1
  19. package/dist/components/SchedulingTimeGrid.js +6 -4
  20. package/dist/components/SchedulingTimeline.d.ts +130 -15
  21. package/dist/components/SchedulingTimeline.js +768 -141
  22. package/dist/components/SearchSuggest.d.ts +12 -1
  23. package/dist/components/SearchSuggest.js +7 -2
  24. package/dist/components/Tree.d.ts +30 -2
  25. package/dist/components/Tree.js +135 -10
  26. package/dist/date-field-model.d.ts +40 -0
  27. package/dist/date-field-model.js +79 -0
  28. package/dist/hooks/usePrinting.d.ts +15 -0
  29. package/dist/hooks/usePrinting.js +29 -0
  30. package/dist/index.cjs +3890 -2070
  31. package/dist/index.d.cts +808 -262
  32. package/dist/index.d.ts +9 -6
  33. package/dist/index.js +40 -4
  34. package/dist/labels-context.d.ts +14 -1
  35. package/dist/scheduling-timeline-model.d.ts +61 -11
  36. package/dist/scheduling-timeline-model.js +85 -26
  37. package/dist/scheduling-timeline-rows.d.ts +40 -0
  38. package/dist/scheduling-timeline-rows.js +42 -0
  39. package/package.json +5 -3
@@ -1,10 +1,14 @@
1
1
  "use client";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
- import { useId, useMemo, useRef, useState } from "react";
3
+ import { useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
4
+ import { cachedDateTimeFormat, zonedDayBounds, zonedTimeOfDay } from "../calendar-model.js";
5
+ import { proposeMove } from "../scheduling-calendar-model.js";
4
6
  import { cx } from "../cx.js";
5
7
  import { fillLabel } from "../fill-label.js";
6
8
  import { useUixLabels } from "../labels-context.js";
7
- import { defaultTimelineStep, layoutLane, pixelsToMs, placeSpan, shiftSpan, timelineTicks } from "../scheduling-timeline-model.js";
9
+ import { usePrinting } from "../hooks/usePrinting.js";
10
+ import { defaultTimelineStep, layoutLane, pixelsToMs, placeSpan, shiftSpan, timelineRepeatedHourOffset, timelineStepDelta, timelineSubTicks, timelineTicks } from "../scheduling-timeline-model.js";
11
+ import { timelineRowExtents, timelineWindow } from "../scheduling-timeline-rows.js";
8
12
  const DEFAULT_SCHEDULING_TIMELINE_LABELS = {
9
13
  region: "Schedule timeline in {timeZone}",
10
14
  lanes: "Lanes",
@@ -19,7 +23,17 @@ const DEFAULT_SCHEDULING_TIMELINE_LABELS = {
19
23
  windows: "Windows and markers in this range",
20
24
  now: "Now",
21
25
  states: { scheduled: "Scheduled", conflicted: "Conflicted", "in-progress": "In progress", "blackout-violation": "Blackout violation" },
22
- overlays: { freeze: "Change freeze", maintenance: "Maintenance window", blackout: "Blackout" }
26
+ overlays: { "freeze": "Change freeze", maintenance: "Maintenance window", "blackout": "Blackout" },
27
+ statuses: { tentative: "Tentative", committed: "Scheduled", live: "In progress", done: "Done", dead: "Cancelled" },
28
+ overlay: "{title}, {start} to {end}",
29
+ moveKeysHint: "Shift and an arrow key moves it.",
30
+ resizeHint: "Alt, Shift and an arrow key moves its end.",
31
+ proposeHint: "Hold Shift and press an arrow key to move it. Enter confirms, Escape cancels.",
32
+ moveProposed: "Move to {start} \u2013 {end}. Enter confirms, Escape cancels.",
33
+ moveCancelled: "Move cancelled.",
34
+ gapForward: "That time does not exist on this day. Moved forward to {time}.",
35
+ groupSummary: "Items: {count}",
36
+ moveSent: "Requested a move to {start} \u2013 {end}."
23
37
  };
24
38
  const TICK_WIDTH = { hour: "3.5rem", day: "7rem", week: "6rem", month: "8rem" };
25
39
  const TICK_FORMAT = {
@@ -28,6 +42,36 @@ const TICK_FORMAT = {
28
42
  week: { day: "numeric", month: "short" },
29
43
  month: { month: "short", year: "numeric" }
30
44
  };
45
+ const EMPTY_ITEMS = [];
46
+ const EMPTY_OVERLAYS = [];
47
+ const EMPTY_MARKERS = [];
48
+ const DRAG_THRESHOLD = 4;
49
+ const DEFAULT_UNITS = { row: 36, pad: 16, head: 40 };
50
+ const ESTIMATED_VIEWPORT = 720;
51
+ const OVERSCAN_ROWS = 4;
52
+ const BAR_MIN_FALLBACK = 1.5 * 16;
53
+ const useIsomorphicLayoutEffect = typeof document !== "undefined" ? useLayoutEffect : useEffect;
54
+ const sameExtent = (a, b) => a.subRows === b.subRows && a.lanes === b.lanes && a.heads === b.heads;
55
+ const IN_LANE = { scope: "lane", labelTops: ["0%"] };
56
+ const vars = (values) => values;
57
+ const extentCss = (extent) => `calc(${extent.subRows} * var(--timeline-bar-row) + ${extent.lanes} * var(--timeline-lane-pad) + ${extent.heads} * var(--timeline-head))`;
58
+ const between = (from, to) => ({ subRows: to.subRows - from.subRows, lanes: to.lanes - from.lanes, heads: to.heads - from.heads });
59
+ function uniqueKeys(ids) {
60
+ const seen = /* @__PURE__ */ new Map();
61
+ return ids.map((id) => {
62
+ const count = seen.get(id) ?? 0;
63
+ seen.set(id, count + 1);
64
+ return count === 0 ? id : `${id}#${count}`;
65
+ });
66
+ }
67
+ const lanesOf = (target) => target.laneIds !== void 0 ? new Set(target.laneIds) : target.laneId ? /* @__PURE__ */ new Set([target.laneId]) : null;
68
+ function ItemMarker({ marker, showLabel }) {
69
+ const emphasis = marker.emphasis ?? "neutral";
70
+ return /* @__PURE__ */ jsxs("span", { className: "uix-scheduling-timeline__item-marker", "data-emphasis": emphasis, "data-glyph": marker.icon == null ? emphasis : void 0, children: [
71
+ marker.icon != null && /* @__PURE__ */ jsx("span", { className: "uix-scheduling-timeline__item-marker-icon", "aria-hidden": "true", children: marker.icon }),
72
+ /* @__PURE__ */ jsx("span", { className: showLabel ? "uix-scheduling-timeline__item-marker-label" : "uix-visually-hidden", children: marker.label })
73
+ ] });
74
+ }
31
75
  function SchedulingTimeline({
32
76
  lanes,
33
77
  items,
@@ -35,17 +79,28 @@ function SchedulingTimeline({
35
79
  scale = "day",
36
80
  timeZone,
37
81
  locale,
38
- overlays = [],
39
- markers = [],
82
+ groups,
83
+ onToggleGroup,
84
+ overlays = EMPTY_OVERLAYS,
85
+ onSelectOverlay,
86
+ markers = EMPTY_MARKERS,
40
87
  now,
41
88
  weekStartsOn = 1,
42
89
  tickWidth,
43
90
  step: stepProp,
91
+ subTicks,
44
92
  formatTick,
45
93
  formatInstant: formatInstantProp,
46
94
  renderItem,
47
95
  onSelectItem,
48
96
  onMoveItem,
97
+ onProposeMove,
98
+ onResizeItem,
99
+ flagOverlaps = true,
100
+ virtualizeAbove = 150,
101
+ maxHeight,
102
+ notice,
103
+ columnNotes,
49
104
  loading,
50
105
  error,
51
106
  onRetry,
@@ -53,47 +108,360 @@ function SchedulingTimeline({
53
108
  className
54
109
  }) {
55
110
  const uixLabels = useUixLabels();
111
+ const provided = { ...uixLabels.schedulingTimeline, ...labelOverrides };
56
112
  const labels = {
57
113
  ...DEFAULT_SCHEDULING_TIMELINE_LABELS,
58
- ...uixLabels.schedulingTimeline,
59
- ...labelOverrides,
114
+ ...provided,
60
115
  states: { ...DEFAULT_SCHEDULING_TIMELINE_LABELS.states, ...uixLabels.schedulingTimeline?.states, ...labelOverrides?.states },
61
- overlays: { ...DEFAULT_SCHEDULING_TIMELINE_LABELS.overlays, ...uixLabels.schedulingTimeline?.overlays, ...labelOverrides?.overlays }
116
+ overlays: { ...DEFAULT_SCHEDULING_TIMELINE_LABELS.overlays, ...uixLabels.schedulingTimeline?.overlays, ...labelOverrides?.overlays },
117
+ statuses: { ...DEFAULT_SCHEDULING_TIMELINE_LABELS.statuses, ...uixLabels.schedulingTimeline?.statuses, ...labelOverrides?.statuses }
62
118
  };
63
119
  const id = useId();
64
- const step = stepProp ?? defaultTimelineStep(scale);
120
+ const hintId = `${id}-hint`;
121
+ const step = stepProp !== void 0 && Number.isFinite(stepProp) && stepProp > 0 ? stepProp : defaultTimelineStep(scale);
122
+ const rootRef = useRef(null);
123
+ const [scroller, setScroller] = useState(null);
124
+ const [layerTrack, setLayerTrack] = useState(null);
125
+ const probeRef = useRef(null);
126
+ const bodyRef = useRef(null);
127
+ const barMinRef = useRef(null);
128
+ const [barShare, setBarShare] = useState(0);
129
+ const barShareRef = useRef(0);
130
+ const anchorRef = useRef(null);
131
+ const [anchorKey, setAnchorKey] = useState(null);
132
+ const [focusRowKey, setFocusRowKey] = useState(null);
133
+ const [activeId, setActiveId] = useState();
134
+ const [focusRequest, setFocusRequest] = useState(null);
135
+ const [stepDrag, setStepDrag] = useState(null);
136
+ const [ghost, setGhost] = useState(null);
137
+ const [announcement, setAnnouncement] = useState("");
138
+ const [selfCollapsed, setSelfCollapsed] = useState({});
139
+ const [units, setUnits] = useState(DEFAULT_UNITS);
140
+ const [view, setView] = useState({ top: 0, height: ESTIMATED_VIEWPORT });
141
+ const measureViewRef = useRef(null);
142
+ const proposing = onProposeMove !== void 0;
143
+ const stepping = !proposing && onMoveItem !== void 0;
144
+ const resizing = onResizeItem !== void 0;
145
+ const canMove = (item) => (proposing || stepping) && item.movable !== false;
146
+ const canResize = (item) => resizing && item.movable !== false;
147
+ const hint = proposing ? [labels.proposeHint, resizing ? labels.resizeHint : ""].filter(Boolean).join(" ") : stepping ? resizing ? labels.moveHint : labels.moveKeysHint : resizing ? labels.resizeHint : "";
148
+ const fmtInstant = (instant) => {
149
+ const custom = formatInstantProp?.(instant);
150
+ if (custom != null) return custom;
151
+ const text = cachedDateTimeFormat(locale, { timeZone, dateStyle: "medium", timeStyle: "short" }).format(new Date(instant));
152
+ const offset = timelineRepeatedHourOffset(instant, timeZone);
153
+ return offset ? `${text} ${offset}` : text;
154
+ };
155
+ const fmtTick = (at) => formatTick?.(new Date(at), scale) ?? cachedDateTimeFormat(locale, { timeZone, ...TICK_FORMAT[scale] }).format(new Date(at));
65
156
  const ticks = useMemo(() => timelineTicks(range, scale, timeZone, weekStartsOn), [range.start, range.end, scale, timeZone, weekStartsOn]);
66
- const laneLayouts = useMemo(
67
- () => lanes.map((lane) => ({ lane, placed: layoutLane(items.filter((item) => item.laneId === lane.id), range) })),
68
- [lanes, items, range.start, range.end]
157
+ const minorTicks = useMemo(
158
+ () => scale === "day" && subTicks && subTicks.length > 0 ? timelineSubTicks(range, subTicks, timeZone) : [],
159
+ [range.start, range.end, scale, timeZone, subTicks]
69
160
  // eslint-disable-line react-hooks/exhaustive-deps
70
161
  );
71
- const ordered = useMemo(() => laneLayouts.flatMap((l) => l.placed.map((p) => p.item.id)), [laneLayouts]);
72
- const [activeId, setActiveId] = useState();
73
- const focusId = activeId && ordered.includes(activeId) ? activeId : ordered[0];
74
- const [drag, setDrag] = useState(null);
75
- const [announcement, setAnnouncement] = useState("");
76
- const rootRef = useRef(null);
77
- const fmtInstant = (instant) => formatInstantProp?.(instant) ?? new Intl.DateTimeFormat(locale, { timeZone, dateStyle: "medium", timeStyle: "short" }).format(new Date(instant));
78
- const fmtTick = (at) => formatTick?.(new Date(at), scale) ?? new Intl.DateTimeFormat(locale, { timeZone, ...TICK_FORMAT[scale] }).format(new Date(at));
79
- const movable = (item) => !!onMoveItem && item.movable !== false;
162
+ const tickCount = ticks.length;
163
+ useIsomorphicLayoutEffect(() => {
164
+ if (!layerTrack) return void 0;
165
+ const measure = () => {
166
+ const width = layerTrack.getBoundingClientRect().width;
167
+ const bar = barMinRef.current?.getBoundingClientRect().width || BAR_MIN_FALLBACK;
168
+ const share = width > 0 ? Math.round(bar / width * 1e5) / 1e5 : 0;
169
+ if (share === barShareRef.current) return;
170
+ barShareRef.current = share;
171
+ const body2 = bodyRef.current;
172
+ const edge = Math.max(0, scroller?.getBoundingClientRect().top ?? 0);
173
+ const first = body2 ? Array.from(body2.children).find((row) => row.hasAttribute("data-row-key") && row.getBoundingClientRect().bottom > edge + 1) : void 0;
174
+ anchorRef.current = first ? { key: first.getAttribute("data-row-key"), top: first.getBoundingClientRect().top } : null;
175
+ setAnchorKey(anchorRef.current?.key ?? null);
176
+ setBarShare(share);
177
+ };
178
+ measure();
179
+ const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(measure);
180
+ observer?.observe(layerTrack);
181
+ return () => observer?.disconnect();
182
+ }, [layerTrack, scroller, tickWidth, scale, tickCount]);
183
+ const minSpan = Math.max(0, (Date.parse(range.end) - Date.parse(range.start)) * barShare) || 0;
184
+ const isCollapsed = (group) => onToggleGroup ? !!group.collapsed : selfCollapsed[group.id] ?? !!group.collapsed;
185
+ const collapsedSignature = groups?.map((group) => isCollapsed(group) ? "1" : "0").join("") ?? "";
186
+ const itemsByLane = useMemo(() => {
187
+ const byLane = /* @__PURE__ */ new Map();
188
+ for (const item of items) {
189
+ const list = byLane.get(item.laneId);
190
+ if (list) list.push(item);
191
+ else byLane.set(item.laneId, [item]);
192
+ }
193
+ return byLane;
194
+ }, [items]);
195
+ const rows = useMemo(() => {
196
+ const laneRow = (lane, laneIndex, groupIndex) => {
197
+ const placed = layoutLane(itemsByLane.get(lane.id) ?? EMPTY_ITEMS, range, { flagOverlaps, minSpan });
198
+ return { kind: "lane", key: `lane-${laneIndex}`, lane, laneIndex, groupIndex, placed, keys: uniqueKeys(placed.map((p) => p.item.id)), subRows: placed.reduce((max, p) => Math.max(max, p.row + 1), 1) };
199
+ };
200
+ if (!groups) return lanes.map((lane, laneIndex) => laneRow(lane, laneIndex));
201
+ const indexes = /* @__PURE__ */ new Map();
202
+ lanes.forEach((lane, laneIndex) => {
203
+ const list2 = indexes.get(lane.id);
204
+ if (list2) list2.push(laneIndex);
205
+ else indexes.set(lane.id, [laneIndex]);
206
+ });
207
+ const used = /* @__PURE__ */ new Set();
208
+ const list = [];
209
+ groups.forEach((group, groupIndex) => {
210
+ const collapsed = collapsedSignature[groupIndex] === "1";
211
+ const stands = /* @__PURE__ */ new Set();
212
+ list.push({ kind: "head", key: `group-${groupIndex}`, group, groupIndex, collapsed, stands });
213
+ for (const laneId of group.laneIds) {
214
+ for (const laneIndex of indexes.get(laneId) ?? []) {
215
+ if (used.has(laneIndex)) continue;
216
+ used.add(laneIndex);
217
+ if (collapsed) stands.add(laneId);
218
+ else list.push(laneRow(lanes[laneIndex], laneIndex, groupIndex));
219
+ }
220
+ }
221
+ });
222
+ lanes.forEach((lane, laneIndex) => {
223
+ if (!used.has(laneIndex)) list.push(laneRow(lane, laneIndex));
224
+ });
225
+ return list;
226
+ }, [lanes, groups, collapsedSignature, itemsByLane, range.start, range.end, flagOverlaps, minSpan]);
227
+ const laneRows = useMemo(() => rows.filter((row) => row.kind === "lane"), [rows]);
228
+ const { ordered, rowOfItem } = useMemo(() => {
229
+ const ids = [];
230
+ const rowOf = /* @__PURE__ */ new Map();
231
+ rows.forEach((row, rowIndex) => {
232
+ if (row.kind !== "lane") return;
233
+ for (const p of row.placed) {
234
+ ids.push(p.item.id);
235
+ if (!rowOf.has(p.item.id)) rowOf.set(p.item.id, rowIndex);
236
+ }
237
+ });
238
+ return { ordered: ids, rowOfItem: rowOf };
239
+ }, [rows]);
240
+ const nothingToShow = useMemo(() => {
241
+ if (ordered.length > 0) return false;
242
+ const laneIds = new Set(lanes.map((lane) => lane.id));
243
+ return !items.some((item) => laneIds.has(item.laneId) && placeSpan(item, range) !== null);
244
+ }, [ordered, lanes, items, range.start, range.end]);
245
+ const focusId = activeId !== void 0 && rowOfItem.has(activeId) ? activeId : ordered[0];
246
+ const strips = useMemo(() => {
247
+ const set = /* @__PURE__ */ new Set();
248
+ const shown = overlays.filter((overlay) => placeSpan(overlay, range) !== null).map(lanesOf);
249
+ if (shown.length === 0) return set;
250
+ rows.forEach((row, index) => {
251
+ if (row.kind !== "head" || !row.collapsed || row.stands.size === 0) return;
252
+ if (shown.some((targets) => targets === null || [...row.stands].some((laneId) => targets.has(laneId)))) set.add(index);
253
+ });
254
+ return set;
255
+ }, [rows, overlays, range.start, range.end]);
256
+ const extents = useMemo(() => timelineRowExtents(rows.map((row, index) => row.kind === "head" ? { kind: "head", subRows: strips.has(index) ? 1 : 0 } : { kind: "lane", subRows: row.subRows })), [rows, strips]);
257
+ const windowed = lanes.length + (groups?.length ?? 0) > virtualizeAbove;
258
+ const printing = usePrinting(windowed);
259
+ const virtual = windowed && !printing;
260
+ const fixed = windowed || groups !== void 0 || overlays.some((overlay) => overlay.laneIds !== void 0) || markers.some((marker) => marker.laneIds !== void 0);
261
+ useEffect(() => {
262
+ if (!virtual || !scroller) return void 0;
263
+ let frame = 0;
264
+ const measure = () => {
265
+ frame = 0;
266
+ const probe = probeRef.current;
267
+ if (probe) {
268
+ const [row, pad, head] = Array.from(probe.children, (child) => child.getBoundingClientRect().height);
269
+ if (row && pad && head) setUnits((current) => current.row === row && current.pad === pad && current.head === head ? current : { row, pad, head });
270
+ }
271
+ const box = scroller.getBoundingClientRect();
272
+ const viewport = window.innerHeight || document.documentElement.clientHeight || 0;
273
+ const shown = Math.min(box.bottom, viewport) - Math.max(box.top, 0);
274
+ const top = scroller.scrollTop + Math.max(0, -box.top);
275
+ const next = { top: Math.round(top), height: shown > 0 ? Math.round(shown) : ESTIMATED_VIEWPORT };
276
+ setView((current) => Math.abs(current.top - next.top) < 8 && current.height === next.height ? current : next);
277
+ };
278
+ const schedule = () => {
279
+ if (frame) return;
280
+ frame = -1;
281
+ const handle = requestAnimationFrame(measure);
282
+ if (frame === -1) frame = handle || 1;
283
+ };
284
+ measure();
285
+ measureViewRef.current = measure;
286
+ scroller.addEventListener("scroll", schedule, { passive: true });
287
+ window.addEventListener("scroll", schedule, { passive: true, capture: true });
288
+ window.addEventListener("resize", schedule);
289
+ const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(schedule);
290
+ observer?.observe(scroller);
291
+ return () => {
292
+ measureViewRef.current = null;
293
+ scroller.removeEventListener("scroll", schedule);
294
+ window.removeEventListener("scroll", schedule, { capture: true });
295
+ window.removeEventListener("resize", schedule);
296
+ observer?.disconnect();
297
+ if (frame) cancelAnimationFrame(frame);
298
+ };
299
+ }, [virtual, scroller]);
300
+ const px = (extent) => extent.subRows * units.row + extent.lanes * units.pad + extent.heads * units.head;
301
+ const offsets = useMemo(() => virtual ? extents.map(px) : [], [virtual, extents, units]);
302
+ const overscan = OVERSCAN_ROWS * units.row;
303
+ const pinned = focusId === void 0 ? -1 : rowOfItem.get(focusId) ?? -1;
304
+ const focusedRow = useMemo(() => focusRowKey === null ? -1 : rows.findIndex((row) => row.key === focusRowKey), [rows, focusRowKey]);
305
+ const anchorRow = useMemo(() => anchorKey === null ? -1 : rows.findIndex((row) => row.key === anchorKey), [rows, anchorKey]);
306
+ const { mounted, windowStart } = useMemo(() => {
307
+ if (!virtual) return { mounted: rows.map((_, index) => index), windowStart: 0 };
308
+ const window_ = timelineWindow(offsets, view.top, view.top + view.height, { overscan });
309
+ const list = [];
310
+ for (let index = window_.start; index < window_.end; index++) list.push(index);
311
+ for (const index of /* @__PURE__ */ new Set([pinned, focusedRow, anchorRow])) {
312
+ if (index !== -1 && (index < window_.start || index >= window_.end)) list.push(index);
313
+ }
314
+ list.sort((a, b) => a - b);
315
+ return { mounted: list, windowStart: window_.start };
316
+ }, [virtual, rows, offsets, view, overscan, pinned, focusedRow, anchorRow]);
317
+ useIsomorphicLayoutEffect(() => {
318
+ const anchor = anchorRef.current;
319
+ if (anchorKey === null || !anchor || anchor.key !== anchorKey) return;
320
+ anchorRef.current = null;
321
+ setAnchorKey(null);
322
+ const body2 = bodyRef.current;
323
+ const row = body2 ? Array.from(body2.children).find((child) => child.getAttribute("data-row-key") === anchor.key) : void 0;
324
+ if (!row || !scroller) return;
325
+ const delta = row.getBoundingClientRect().top - anchor.top;
326
+ if (Math.abs(delta) < 1) return;
327
+ const view_ = scroller.ownerDocument.defaultView;
328
+ const scrolls = (element) => {
329
+ const overflow = view_?.getComputedStyle(element).overflowY;
330
+ return (overflow === "auto" || overflow === "scroll") && element.scrollHeight > element.clientHeight + 1;
331
+ };
332
+ let holder = scroller;
333
+ while (holder && !scrolls(holder)) holder = holder.parentElement;
334
+ if (holder) holder.scrollTop += delta;
335
+ else view_?.scrollBy(0, delta);
336
+ measureViewRef.current?.();
337
+ }, [anchorKey, barShare]);
338
+ const barMounted = (rowIndex, placed) => {
339
+ if (!virtual || placed.item.id === focusId) return true;
340
+ const top = offsets[rowIndex] + placed.row * units.row;
341
+ return top + units.row + units.pad > view.top - overscan && top < view.top + view.height + overscan;
342
+ };
343
+ const findBar = (itemId) => {
344
+ for (const candidate of Array.from(rootRef.current?.querySelectorAll("[data-timeline-item]") ?? [])) {
345
+ if (candidate.getAttribute("data-timeline-item") === itemId) return candidate;
346
+ }
347
+ return null;
348
+ };
80
349
  const focusItem = (itemId) => {
81
- if (!itemId) return;
350
+ if (itemId === void 0) return;
82
351
  setActiveId(itemId);
83
- requestAnimationFrame(() => rootRef.current?.querySelector(`[data-timeline-item="${CSS.escape(itemId)}"]`)?.focus());
352
+ setFocusRequest({ id: itemId });
353
+ };
354
+ useEffect(() => {
355
+ if (focusRequest) findBar(focusRequest.id)?.focus();
356
+ }, [focusRequest]);
357
+ const itemById = useMemo(() => {
358
+ const map = /* @__PURE__ */ new Map();
359
+ for (const row of laneRows) for (const p of row.placed) if (!map.has(p.item.id)) map.set(p.item.id, p.item);
360
+ return map;
361
+ }, [laneRows]);
362
+ const stepDelta = timelineStepDelta(step);
363
+ const deltaFor = (ms) => {
364
+ const steps = Math.round(ms / step);
365
+ return { days: steps * stepDelta.days, minutes: steps * stepDelta.minutes };
366
+ };
367
+ const live = useRef({ itemById, timeZone, range, step, onProposeMove, canMove, deltaFor, labels, fmtInstant });
368
+ live.current = { itemById, timeZone, range, step, onProposeMove, canMove, deltaFor, labels, fmtInstant };
369
+ const dragRef = useRef(null);
370
+ const suppressClick = useRef(false);
371
+ const refocus = useRef(null);
372
+ useEffect(() => {
373
+ if (ghost && !itemById.has(ghost.id)) setGhost(null);
374
+ }, [ghost, itemById]);
375
+ useEffect(() => {
376
+ const wanted = refocus.current;
377
+ if (!wanted || !rootRef.current) return;
378
+ const active = document.activeElement;
379
+ const lost = !active || active === document.body;
380
+ if (!lost) {
381
+ if (active.getAttribute("data-item-id") !== wanted) refocus.current = null;
382
+ return;
383
+ }
384
+ findBar(wanted)?.focus();
385
+ });
386
+ useEffect(() => () => {
387
+ dragRef.current?.stop();
388
+ dragRef.current = null;
389
+ }, []);
390
+ useEffect(() => {
391
+ if (proposing) return;
392
+ dragRef.current?.stop();
393
+ dragRef.current = null;
394
+ if (ghost) {
395
+ setGhost(null);
396
+ setAnnouncement("");
397
+ }
398
+ }, [proposing]);
399
+ const send = (itemId, delta) => {
400
+ const state = live.current;
401
+ const item = state.itemById.get(itemId);
402
+ if (!item || !state.onProposeMove || !state.canMove(item) || delta.days === 0 && delta.minutes === 0) {
403
+ setGhost(null);
404
+ return;
405
+ }
406
+ const proposal = proposeMove(item, delta, state.timeZone);
407
+ const result = state.onProposeMove(itemId, proposal);
408
+ setAnnouncement(fillLabel(state.labels.moveSent, { start: state.fmtInstant(proposal.start), end: state.fmtInstant(proposal.end) }));
409
+ if (result && typeof result.then === "function") {
410
+ setGhost({ id: itemId, delta, source: "sent", proposal });
411
+ const clear = () => setGhost((current) => current && current.source === "sent" && current.proposal === proposal ? null : current);
412
+ result.then(clear, clear);
413
+ } else setGhost(null);
84
414
  };
85
415
  const commit = (item, delta, mode) => {
86
- if (!onMoveItem || delta === 0) return;
416
+ const callback = mode === "move" ? onMoveItem : onResizeItem;
417
+ if (!callback || delta === 0) return;
87
418
  const next = shiftSpan(item, delta, mode, step);
88
- onMoveItem(item.id, next);
419
+ callback(item.id, next);
89
420
  setAnnouncement(mode === "move" ? fillLabel(labels.moved, { title: item.title, start: fmtInstant(next.start), end: fmtInstant(next.end) }) : fillLabel(labels.resized, { title: item.title, end: fmtInstant(next.end) }));
90
421
  };
91
- const onItemKeyDown = (event, laneIndex, placed, index) => {
422
+ const onItemKeyDown = (event, rowIndex, placed, index) => {
92
423
  const item = placed[index].item;
424
+ const pending = ghost && ghost.id === item.id && ghost.source === "keys" ? ghost.delta : null;
425
+ if (event.key === "Enter" && pending) {
426
+ event.preventDefault();
427
+ refocus.current = item.id;
428
+ setAnnouncement("");
429
+ send(item.id, pending);
430
+ return;
431
+ }
432
+ if (event.key === "Escape" && pending) {
433
+ event.preventDefault();
434
+ event.stopPropagation();
435
+ setGhost(null);
436
+ setAnnouncement(labels.moveCancelled);
437
+ return;
438
+ }
93
439
  const horizontal = event.key === "ArrowLeft" ? -1 : event.key === "ArrowRight" ? 1 : 0;
94
- if (horizontal && event.shiftKey && movable(item)) {
440
+ if (horizontal && event.shiftKey && event.altKey) {
441
+ if (canResize(item)) {
442
+ event.preventDefault();
443
+ commit(item, horizontal * step, "resize");
444
+ }
445
+ return;
446
+ }
447
+ if (horizontal && event.shiftKey && proposing && canMove(item)) {
95
448
  event.preventDefault();
96
- commit(item, horizontal * step, event.altKey ? "resize" : "move");
449
+ const base = pending ?? { days: 0, minutes: 0 };
450
+ const next = { days: base.days + horizontal * stepDelta.days, minutes: base.minutes + horizontal * stepDelta.minutes };
451
+ if (next.days === 0 && next.minutes === 0) {
452
+ setGhost(null);
453
+ setAnnouncement(labels.moveCancelled);
454
+ return;
455
+ }
456
+ const proposal = proposeMove(item, next, timeZone);
457
+ setGhost({ id: item.id, delta: next, source: "keys", proposal });
458
+ const proposed = fillLabel(labels.moveProposed, { start: fmtInstant(proposal.start), end: fmtInstant(proposal.end) });
459
+ setAnnouncement(proposal.adjusted ? `${fillLabel(labels.gapForward, { time: zonedTimeOfDay(proposal.start, timeZone) })} ${proposed}` : proposed);
460
+ return;
461
+ }
462
+ if (horizontal && event.shiftKey && stepping && canMove(item)) {
463
+ event.preventDefault();
464
+ commit(item, horizontal * step, "move");
97
465
  return;
98
466
  }
99
467
  if (horizontal) {
@@ -110,134 +478,393 @@ function SchedulingTimeline({
110
478
  if (vertical) {
111
479
  event.preventDefault();
112
480
  const start = Date.parse(item.start);
113
- for (let l = laneIndex + vertical; l >= 0 && l < laneLayouts.length; l += vertical) {
114
- const candidates = laneLayouts[l].placed;
115
- if (candidates.length === 0) continue;
116
- const nearest = candidates.reduce((best, p) => Math.abs(Date.parse(p.item.start) - start) < Math.abs(Date.parse(best.item.start) - start) ? p : best);
481
+ for (let r = rowIndex + vertical; r >= 0 && r < rows.length; r += vertical) {
482
+ const row = rows[r];
483
+ if (row.kind !== "lane" || row.placed.length === 0) continue;
484
+ const nearest = row.placed.reduce((best, p) => Math.abs(Date.parse(p.item.start) - start) < Math.abs(Date.parse(best.item.start) - start) ? p : best);
117
485
  focusItem(nearest.item.id);
118
486
  return;
119
487
  }
120
488
  }
121
489
  };
122
- const onPointerDown = (event, item) => {
123
- if (!movable(item) || event.button !== 0) return;
490
+ const onStepPointerDown = (event, item) => {
491
+ if (event.button !== 0) return;
124
492
  const track = event.currentTarget.closest(".uix-scheduling-timeline__track");
125
493
  if (!track) return;
126
494
  event.currentTarget.setPointerCapture?.(event.pointerId);
127
- setDrag({ id: item.id, x: event.clientX, width: track.getBoundingClientRect().width, delta: 0, moved: false });
495
+ setStepDrag({ id: item.id, x: event.clientX, width: track.getBoundingClientRect().width, delta: 0, moved: false });
128
496
  };
129
- const onPointerMove = (event) => {
130
- if (!drag) return;
131
- const dx = event.clientX - drag.x;
132
- const delta = pixelsToMs(dx, drag.width, range, step);
133
- if (delta !== drag.delta || !drag.moved && Math.abs(dx) > 3) setDrag({ ...drag, delta, moved: drag.moved || Math.abs(dx) > 3 });
497
+ const onStepPointerMove = (event) => {
498
+ if (!stepDrag) return;
499
+ const dx = event.clientX - stepDrag.x;
500
+ const delta = pixelsToMs(dx, stepDrag.width, range, step);
501
+ if (delta !== stepDrag.delta || !stepDrag.moved && Math.abs(dx) > 3) setStepDrag({ ...stepDrag, delta, moved: stepDrag.moved || Math.abs(dx) > 3 });
134
502
  };
135
- const onPointerUp = (item) => {
136
- if (!drag || drag.id !== item.id) return;
137
- if (drag.moved) commit(item, drag.delta, "move");
138
- setDrag(drag.moved ? { ...drag, delta: 0 } : null);
139
- if (drag.moved) window.setTimeout(() => setDrag(null), 0);
503
+ const onStepPointerUp = (item) => {
504
+ if (!stepDrag || stepDrag.id !== item.id) return;
505
+ if (stepDrag.moved) commit(item, stepDrag.delta, "move");
506
+ setStepDrag(stepDrag.moved ? { ...stepDrag, delta: 0 } : null);
507
+ if (stepDrag.moved) window.setTimeout(() => setStepDrag(null), 0);
140
508
  };
141
- const nowPos = now ? placeSpan({ start: now, end: now }, range) : null;
142
- const overlaysFor = (laneId) => overlays.filter((o) => !o.laneId || o.laneId === laneId);
143
- const markersFor = (laneId) => markers.filter((m) => !m.laneId || m.laneId === laneId);
509
+ const onProposalPointerDown = (event, item) => {
510
+ if (event.button !== 0 || dragRef.current) return;
511
+ const track = event.currentTarget.closest(".uix-scheduling-timeline__track");
512
+ if (!track) return;
513
+ const finish = (drop) => {
514
+ const drag = dragRef.current;
515
+ if (!drag) return;
516
+ drag.stop();
517
+ dragRef.current = null;
518
+ if (!drag.moved) return;
519
+ suppressClick.current = true;
520
+ setTimeout(() => {
521
+ suppressClick.current = false;
522
+ }, 0);
523
+ if (drop && !drag.dropped) send(drag.id, live.current.deltaFor(drag.ms));
524
+ else setGhost(null);
525
+ };
526
+ const onMove = (move) => {
527
+ const drag = dragRef.current;
528
+ if (!drag || move.pointerId !== drag.pointerId) return;
529
+ if (move.buttons === 0 && move.pointerType === "mouse") {
530
+ finish(false);
531
+ return;
532
+ }
533
+ if (drag.dropped) return;
534
+ const dx = move.clientX - drag.x;
535
+ if (!drag.moved && Math.hypot(dx, move.clientY - drag.y) < DRAG_THRESHOLD) return;
536
+ drag.moved = true;
537
+ const state = live.current;
538
+ const ms = pixelsToMs(dx, drag.width, state.range, state.step);
539
+ if (ms === drag.ms) return;
540
+ drag.ms = ms;
541
+ const dragged = state.itemById.get(drag.id);
542
+ const delta = state.deltaFor(ms);
543
+ setGhost(dragged && ms !== 0 ? { id: drag.id, delta, source: "pointer", proposal: proposeMove(dragged, delta, state.timeZone) } : null);
544
+ };
545
+ const onUp = (up) => {
546
+ if (up.pointerId === dragRef.current?.pointerId) finish(true);
547
+ };
548
+ const onCancel = (cancel) => {
549
+ if (cancel.pointerId === dragRef.current?.pointerId) finish(false);
550
+ };
551
+ const onKey = (key) => {
552
+ const drag = dragRef.current;
553
+ if (key.key !== "Escape" || !drag?.moved || drag.dropped) return;
554
+ key.preventDefault();
555
+ key.stopPropagation();
556
+ drag.dropped = true;
557
+ setGhost(null);
558
+ };
559
+ window.addEventListener("pointermove", onMove);
560
+ window.addEventListener("pointerup", onUp);
561
+ window.addEventListener("pointercancel", onCancel);
562
+ window.addEventListener("keydown", onKey, true);
563
+ dragRef.current = {
564
+ id: item.id,
565
+ pointerId: event.pointerId,
566
+ x: event.clientX,
567
+ y: event.clientY,
568
+ width: track.getBoundingClientRect().width,
569
+ moved: false,
570
+ dropped: false,
571
+ ms: 0,
572
+ stop: () => {
573
+ window.removeEventListener("pointermove", onMove);
574
+ window.removeEventListener("pointerup", onUp);
575
+ window.removeEventListener("pointercancel", onCancel);
576
+ window.removeEventListener("keydown", onKey, true);
577
+ }
578
+ };
579
+ };
580
+ const toggleGroup = (group) => {
581
+ if (onToggleGroup) {
582
+ onToggleGroup(group.id);
583
+ return;
584
+ }
585
+ setSelfCollapsed((current) => ({ ...current, [group.id]: !(current[group.id] ?? !!group.collapsed) }));
586
+ };
587
+ const statusOf = (item) => item.status ?? (item.state === "in-progress" ? "live" : "committed");
588
+ const markersOf = (item) => {
589
+ const flagged = item.state === "conflicted" || item.state === "blackout-violation";
590
+ return flagged ? [...item.markers ?? [], { id: "state", label: labels.states[item.state], emphasis: "warning" }] : item.markers ?? [];
591
+ };
592
+ const stateText = (item) => item.state ? labels.states[item.state] : item.status ? labels.statuses[item.status] : labels.states.scheduled;
593
+ const itemName = (item, conflict) => item.accessibleName ?? [fillLabel(labels.item, { title: item.title, state: stateText(item), start: fmtInstant(item.start), end: fmtInstant(item.end), conflict: conflict ? labels.conflict : "" }), ...(item.markers ?? []).map((marker) => marker.label)].join(", ");
594
+ const dataState = (item) => item.state ?? (item.band === void 0 && item.status === void 0 && item.markers === void 0 && item.accessibleName === void 0 ? "scheduled" : void 0);
595
+ const overlayWords = (overlay) => [overlay.kindLabel ?? (overlay.kind ? labels.overlays[overlay.kind] : void 0), overlay.label, overlay.scopeLabel].filter(Boolean).join(", ");
596
+ const overlayName = (overlay) => overlay.accessibleName ?? fillLabel(labels.overlay, { title: overlayWords(overlay), start: fmtInstant(overlay.start), end: fmtInstant(overlay.end) });
597
+ const patternOf = (overlay) => overlay.pattern ?? (overlay.kind === void 0 || overlay.kind === "maintenance" ? "solid" : "diagonal");
144
598
  const trackStyle = { minWidth: `calc(${Math.max(ticks.length - 1, 1)} * ${tickWidth ?? TICK_WIDTH[scale]})` };
145
- const band = (o, showLabel) => {
146
- const pos = placeSpan(o, range);
147
- return pos && /* @__PURE__ */ jsx("span", { className: "uix-scheduling-timeline__overlay", "data-kind": o.kind, style: { left: `${pos.left}%`, width: `${pos.width}%` }, children: showLabel && /* @__PURE__ */ jsx("span", { className: "uix-scheduling-timeline__overlay-label", children: o.label }) }, o.id);
148
- };
149
- return /* @__PURE__ */ jsxs("section", { ref: rootRef, className: cx("uix-scheduling-timeline", className), "aria-label": fillLabel(labels.region, { timeZone }), children: [
150
- loading ? /* @__PURE__ */ jsx("div", { className: "uix-scheduling-timeline__state", role: "status", children: labels.loading }) : error ? /* @__PURE__ */ jsxs("div", { className: "uix-scheduling-timeline__state", role: "alert", children: [
151
- /* @__PURE__ */ jsx("p", { children: error }),
152
- onRetry && /* @__PURE__ */ jsx("button", { type: "button", className: "uix-btn uix-btn--secondary", onClick: onRetry, children: labels.retry })
153
- ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
154
- (overlays.length > 0 || markers.length > 0) && /* @__PURE__ */ jsxs("div", { className: "uix-visually-hidden", children: [
155
- /* @__PURE__ */ jsx("p", { id: `${id}-windows`, children: labels.windows }),
156
- /* @__PURE__ */ jsxs("ul", { "aria-labelledby": `${id}-windows`, children: [
157
- overlays.map((o) => /* @__PURE__ */ jsxs("li", { children: [
158
- labels.overlays[o.kind],
159
- ": ",
160
- o.label,
161
- ", ",
162
- fmtInstant(o.start),
163
- " \u2013 ",
164
- fmtInstant(o.end)
165
- ] }, o.id)),
166
- markers.map((m) => /* @__PURE__ */ jsxs("li", { children: [
167
- m.label,
168
- ", ",
169
- fmtInstant(m.at)
170
- ] }, m.id))
171
- ] })
599
+ const nowPos = now ? placeSpan({ start: now, end: now }, range) : null;
600
+ const stretchOf = (index) => {
601
+ const row = rows[index];
602
+ if (row.kind === "lane") return { row: index, from: extents[index], to: extents[index + 1] };
603
+ if (!strips.has(index)) return null;
604
+ const top = extents[index];
605
+ return { row: index, from: { subRows: top.subRows, lanes: top.lanes, heads: top.heads + 1 }, to: extents[index + 1] };
606
+ };
607
+ const labelTopIn = (run) => {
608
+ for (const stretch of run) {
609
+ if (!virtual) return [extentCss(stretch.from)];
610
+ if (stretch.row < windowStart) continue;
611
+ const top = px(stretch.from);
612
+ if (top > view.top + view.height + overscan) return [];
613
+ if (top >= view.top) return [extentCss(stretch.from)];
614
+ }
615
+ return [];
616
+ };
617
+ const scopeOf = (targets) => {
618
+ if (targets === null) {
619
+ const all = rows.flatMap((_, index) => stretchOf(index) ?? []);
620
+ const onLane = labelTopIn(all.filter((stretch) => rows[stretch.row].kind === "lane"));
621
+ return { scope: "all", labelTops: onLane.length > 0 ? onLane : labelTopIn(all) };
622
+ }
623
+ const covers = (row) => row.kind === "lane" ? targets.has(row.lane.id) : row.collapsed && [...row.stands].some((laneId) => targets.has(laneId));
624
+ const runs = [];
625
+ rows.forEach((row, index) => {
626
+ const stretch = covers(row) ? stretchOf(index) : null;
627
+ if (!stretch) return;
628
+ const last = runs[runs.length - 1];
629
+ if (last && sameExtent(last[last.length - 1].to, stretch.from)) last.push(stretch);
630
+ else runs.push([stretch]);
631
+ });
632
+ if (runs.length === 0) return null;
633
+ const clip = `polygon(${runs.map((run) => {
634
+ const from = extentCss(run[0].from);
635
+ const to = extentCss(run[run.length - 1].to);
636
+ return `0 ${from}, 100% ${from}, 100% ${to}, 0 ${to}`;
637
+ }).join(", ")})`;
638
+ return { scope: "lanes", clip, labelTops: runs.flatMap(labelTopIn) };
639
+ };
640
+ const renderOverlay = (overlay, key, scope) => {
641
+ const pos = placeSpan(overlay, range);
642
+ if (!pos || !scope) return null;
643
+ const shared = {
644
+ className: "uix-scheduling-timeline__overlay",
645
+ "data-overlay-id": overlay.id,
646
+ "data-pattern": patternOf(overlay),
647
+ "data-kind": overlay.kind,
648
+ "data-scope": scope.scope,
649
+ style: { left: `${pos.left}%`, width: `${pos.width}%`, ...vars({ "--uix-timeline-clip": scope.clip }) }
650
+ };
651
+ const text = scope.labelTops.map((top) => /* @__PURE__ */ jsxs("span", { className: "uix-scheduling-timeline__overlay-label", style: vars({ "--uix-timeline-label-top": top }), children: [
652
+ overlay.kindLabel && /* @__PURE__ */ jsx("span", { className: "uix-scheduling-timeline__overlay-kind", children: overlay.kindLabel }),
653
+ /* @__PURE__ */ jsx("span", { className: "uix-scheduling-timeline__overlay-name", children: overlay.label }),
654
+ overlay.scopeLabel && /* @__PURE__ */ jsx("span", { className: "uix-scheduling-timeline__overlay-scope", children: overlay.scopeLabel })
655
+ ] }, top));
656
+ return onSelectOverlay ? /* @__PURE__ */ jsx("button", { type: "button", ...shared, "aria-label": overlayName(overlay), onClick: () => onSelectOverlay(overlay), children: text }, key) : /* @__PURE__ */ jsx("span", { ...shared, "aria-hidden": "true", children: text }, key);
657
+ };
658
+ const renderMarker = (marker, key, scope) => {
659
+ const pos = placeSpan({ start: marker.at, end: marker.at }, range);
660
+ if (!pos || !scope) return null;
661
+ return /* @__PURE__ */ jsx("span", { className: "uix-scheduling-timeline__marker", "data-marker-id": marker.id, "data-scope": scope.scope, style: { left: `${pos.left}%`, ...vars({ "--uix-timeline-clip": scope.clip }) }, title: marker.label, "aria-hidden": "true" }, key);
662
+ };
663
+ const overlayKeys = uniqueKeys(overlays.map((overlay) => overlay.id));
664
+ const markerKeys = uniqueKeys(markers.map((marker) => marker.id));
665
+ const inLayer = (target) => fixed || lanesOf(target) === null;
666
+ const columnNoteList = columnNotes ? Object.keys(columnNotes).flatMap((date) => {
667
+ const node = columnNotes[date];
668
+ if (node == null || node === false) return [];
669
+ let pos = null;
670
+ try {
671
+ const bounds = zonedDayBounds(date, timeZone);
672
+ pos = placeSpan({ start: bounds.start.toISOString(), end: bounds.end.toISOString() }, range);
673
+ } catch {
674
+ pos = null;
675
+ }
676
+ return pos && pos.width > 0 ? [{ date, node, pos }] : [];
677
+ }) : [];
678
+ const renderItems = (row, rowIndex) => row.placed.map((p, index) => {
679
+ if (!barMounted(rowIndex, p)) return null;
680
+ const item = p.item;
681
+ const dragging = stepDrag?.id === item.id && stepDrag.moved;
682
+ const shown = dragging && stepDrag ? placeSpan(shiftSpan(item, stepDrag.delta, "move", step), range) ?? p : p;
683
+ const movable = canMove(item);
684
+ const described = movable || canResize(item);
685
+ const itemMarkers = markersOf(item);
686
+ return /* @__PURE__ */ jsx("li", { className: "uix-scheduling-timeline__slot", style: { left: `${shown.left}%`, width: `${shown.width}%`, ...vars({ "--uix-timeline-row": p.row }) }, children: /* @__PURE__ */ jsxs(
687
+ "button",
688
+ {
689
+ type: "button",
690
+ className: "uix-scheduling-timeline__item",
691
+ "data-timeline-item": item.id,
692
+ "data-item-id": item.id,
693
+ title: item.title,
694
+ "data-band": item.band ?? "none",
695
+ "data-status": statusOf(item),
696
+ "data-state": dataState(item),
697
+ "data-conflict": p.conflict || void 0,
698
+ "data-clipped-start": p.clippedStart || void 0,
699
+ "data-clipped-end": p.clippedEnd || void 0,
700
+ "data-movable": movable || void 0,
701
+ "data-dragging": dragging || void 0,
702
+ tabIndex: focusId === item.id ? 0 : -1,
703
+ "aria-label": itemName(item, p.conflict),
704
+ "aria-describedby": described ? hintId : void 0,
705
+ onFocus: () => setActiveId(item.id),
706
+ onBlur: proposing ? () => {
707
+ refocus.current = null;
708
+ if (ghost && ghost.id === item.id && ghost.source === "keys") {
709
+ setGhost(null);
710
+ setAnnouncement(labels.moveCancelled);
711
+ }
712
+ } : void 0,
713
+ onKeyDown: (event) => onItemKeyDown(event, rowIndex, row.placed, index),
714
+ onPointerDown: !movable ? void 0 : proposing ? (event) => onProposalPointerDown(event, item) : (event) => onStepPointerDown(event, item),
715
+ onPointerMove: stepping ? onStepPointerMove : void 0,
716
+ onPointerUp: stepping ? () => onStepPointerUp(item) : void 0,
717
+ onPointerCancel: stepping ? () => setStepDrag(null) : void 0,
718
+ onClick: () => {
719
+ if (stepDrag?.moved) return;
720
+ if (ghost && ghost.id === item.id && ghost.source === "keys") return;
721
+ onSelectItem?.(item);
722
+ },
723
+ children: [
724
+ /* @__PURE__ */ jsx("span", { className: "uix-scheduling-timeline__item-title", children: renderItem?.(item) ?? item.title }),
725
+ item.meta && /* @__PURE__ */ jsx("span", { className: "uix-scheduling-timeline__item-meta", children: item.meta }),
726
+ itemMarkers.length > 0 && /* @__PURE__ */ jsx("span", { className: "uix-scheduling-timeline__item-markers", children: itemMarkers.map((marker) => /* @__PURE__ */ jsx(ItemMarker, { marker }, marker.id)) })
727
+ ]
728
+ }
729
+ ) }, row.keys[index]);
730
+ });
731
+ const renderRow = (row, rowIndex) => {
732
+ if (row.kind === "head") {
733
+ const { group, collapsed } = row;
734
+ return /* @__PURE__ */ jsxs("div", { className: cx("uix-scheduling-timeline__row", "uix-scheduling-timeline__row--group", collapsed && "uix-scheduling-timeline__row--summary"), "data-row-key": row.key, "data-group-id": group.id, "data-collapsed": collapsed || void 0, "data-strip": strips.has(rowIndex) || void 0, children: [
735
+ /* @__PURE__ */ jsx("div", { className: "uix-scheduling-timeline__group-head", children: /* @__PURE__ */ jsxs("div", { className: "uix-scheduling-timeline__group-label", children: [
736
+ /* @__PURE__ */ jsxs("button", { type: "button", className: "uix-scheduling-timeline__group-toggle", "aria-expanded": !collapsed, onClick: () => toggleGroup(group), children: [
737
+ /* @__PURE__ */ jsx("span", { className: "uix-scheduling-timeline__group-chevron", "aria-hidden": "true" }),
738
+ /* @__PURE__ */ jsx("span", { className: "uix-scheduling-timeline__group-name", id: `${id}-group-${row.groupIndex}`, children: group.label })
739
+ ] }),
740
+ group.meta != null && /* @__PURE__ */ jsx("span", { className: "uix-scheduling-timeline__group-meta", children: group.meta }),
741
+ collapsed && group.summary && /* @__PURE__ */ jsxs("span", { className: "uix-scheduling-timeline__group-summary", children: [
742
+ /* @__PURE__ */ jsx("span", { className: "uix-scheduling-timeline__group-count", children: fillLabel(labels.groupSummary, { count: group.summary.count }) }),
743
+ group.summary.markers && group.summary.markers.length > 0 && /* @__PURE__ */ jsx("span", { className: "uix-scheduling-timeline__item-markers", children: group.summary.markers.map((marker) => /* @__PURE__ */ jsx(ItemMarker, { marker, showLabel: true }, marker.id)) })
744
+ ] })
745
+ ] }) }),
746
+ strips.has(rowIndex) && /* @__PURE__ */ jsx("div", { className: "uix-scheduling-timeline__group-strip", "aria-hidden": "true" })
747
+ ] }, row.key);
748
+ }
749
+ const { lane } = row;
750
+ const laneLabelId = `${id}-lane-${row.laneIndex}`;
751
+ const ghostPlaced = ghost ? row.placed.find((p) => p.item.id === ghost.id) : void 0;
752
+ const ghostPos = ghost && ghostPlaced && rowOfItem.get(ghost.id) === rowIndex ? placeSpan(ghost.proposal, range) : null;
753
+ return /* @__PURE__ */ jsxs("div", { className: "uix-scheduling-timeline__row", "data-row-key": row.key, "data-lane-id": lane.id, style: vars({ "--uix-timeline-rows": row.subRows }), children: [
754
+ /* @__PURE__ */ jsxs("div", { className: "uix-scheduling-timeline__lane-label", id: laneLabelId, children: [
755
+ /* @__PURE__ */ jsx("span", { className: "uix-scheduling-timeline__lane-name", children: lane.label }),
756
+ lane.meta != null && /* @__PURE__ */ jsx("span", { className: "uix-scheduling-timeline__lane-meta", children: lane.meta })
172
757
  ] }),
173
- /* @__PURE__ */ jsx("div", { className: "uix-scheduling-timeline__scroller", children: /* @__PURE__ */ jsxs("div", { className: "uix-scheduling-timeline__grid", children: [
174
- /* @__PURE__ */ jsxs("div", { className: "uix-scheduling-timeline__row uix-scheduling-timeline__row--axis", "aria-hidden": "true", children: [
175
- /* @__PURE__ */ jsx("div", { className: "uix-scheduling-timeline__lane-label", children: labels.lanes }),
176
- /* @__PURE__ */ jsx("div", { className: "uix-scheduling-timeline__track uix-scheduling-timeline__axis", style: trackStyle, children: ticks.map((tick) => /* @__PURE__ */ jsx("span", { className: "uix-scheduling-timeline__tick", "data-major": tick.major || void 0, style: { left: `${tick.offset}%` }, children: tick.offset < 100 && /* @__PURE__ */ jsx("span", { className: "uix-scheduling-timeline__tick-label", children: fmtTick(tick.at) }) }, tick.at)) })
177
- ] }),
178
- laneLayouts.map(({ lane, placed }, laneIndex) => {
179
- const rows = placed.reduce((max, p) => Math.max(max, p.row + 1), 1);
180
- const laneLabelId = `${id}-lane-${laneIndex}`;
181
- return /* @__PURE__ */ jsxs("div", { className: "uix-scheduling-timeline__row", style: { "--uix-timeline-rows": rows }, children: [
182
- /* @__PURE__ */ jsxs("div", { className: "uix-scheduling-timeline__lane-label", id: laneLabelId, children: [
183
- /* @__PURE__ */ jsx("span", { className: "uix-scheduling-timeline__lane-name", children: lane.label }),
184
- lane.meta != null && /* @__PURE__ */ jsx("span", { className: "uix-scheduling-timeline__lane-meta", children: lane.meta })
758
+ /* @__PURE__ */ jsxs("div", { className: "uix-scheduling-timeline__track", style: trackStyle, children: [
759
+ overlays.map((overlay, index) => !inLayer(overlay) && lanesOf(overlay).has(lane.id) ? renderOverlay(overlay, overlayKeys[index], IN_LANE) : null),
760
+ markers.map((marker, index) => !inLayer(marker) && lanesOf(marker).has(lane.id) ? renderMarker(marker, markerKeys[index], IN_LANE) : null),
761
+ /* @__PURE__ */ jsx("ul", { className: "uix-scheduling-timeline__items", "aria-labelledby": row.groupIndex === void 0 ? laneLabelId : `${id}-group-${row.groupIndex} ${laneLabelId}`, children: renderItems(row, rowIndex) }),
762
+ ghostPos && /* @__PURE__ */ jsx("span", { className: "uix-scheduling-timeline__ghost", "aria-hidden": "true", "data-sent": ghost?.source === "sent" || void 0, style: { left: `${ghostPos.left}%`, width: `${ghostPos.width}%`, ...vars({ "--uix-timeline-row": ghostPlaced.row }) } })
763
+ ] })
764
+ ] }, row.key);
765
+ };
766
+ const body = [];
767
+ let cursor = 0;
768
+ const spacer = (from, to) => /* @__PURE__ */ jsx("div", { className: "uix-scheduling-timeline__spacer", "aria-hidden": "true", style: { height: extentCss(between(extents[from], extents[to])) } }, `spacer-${from}`);
769
+ for (const index of mounted) {
770
+ if (index > cursor) body.push(spacer(cursor, index));
771
+ body.push(renderRow(rows[index], index));
772
+ cursor = index + 1;
773
+ }
774
+ if (cursor < rows.length) body.push(spacer(cursor, rows.length));
775
+ const listed = onSelectOverlay ? EMPTY_OVERLAYS : overlays;
776
+ return /* @__PURE__ */ jsxs(
777
+ "section",
778
+ {
779
+ ref: rootRef,
780
+ className: cx("uix-scheduling-timeline", className),
781
+ "aria-label": fillLabel(labels.region, { timeZone }),
782
+ "data-fixed": fixed || void 0,
783
+ onClickCapture: (event) => {
784
+ if (suppressClick.current) {
785
+ suppressClick.current = false;
786
+ event.preventDefault();
787
+ event.stopPropagation();
788
+ }
789
+ },
790
+ children: [
791
+ notice != null && notice !== false && /* @__PURE__ */ jsx("div", { className: "uix-scheduling-timeline__notice", children: notice }),
792
+ loading ? /* @__PURE__ */ jsx("div", { className: "uix-scheduling-timeline__state", role: "status", children: labels.loading }) : error ? /* @__PURE__ */ jsxs("div", { className: "uix-scheduling-timeline__state", role: "alert", children: [
793
+ /* @__PURE__ */ jsx("p", { children: error }),
794
+ onRetry && /* @__PURE__ */ jsx("button", { type: "button", className: "uix-btn uix-btn--secondary", onClick: onRetry, children: labels.retry })
795
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
796
+ (listed.length > 0 || markers.length > 0) && /* @__PURE__ */ jsxs("div", { className: "uix-visually-hidden", children: [
797
+ /* @__PURE__ */ jsx("p", { id: `${id}-windows`, children: labels.windows }),
798
+ /* @__PURE__ */ jsxs("ul", { "aria-labelledby": `${id}-windows`, children: [
799
+ listed.map((o, index) => /* @__PURE__ */ jsx("li", { children: o.accessibleName ?? /* @__PURE__ */ jsxs(Fragment, { children: [
800
+ o.kindLabel === void 0 && o.kind ? `${labels.overlays[o.kind]}: ${o.label}${o.scopeLabel ? `, ${o.scopeLabel}` : ""}` : overlayWords(o),
801
+ ", ",
802
+ fmtInstant(o.start),
803
+ " \u2013 ",
804
+ fmtInstant(o.end)
805
+ ] }) }, overlayKeys[index])),
806
+ markers.map((m, index) => /* @__PURE__ */ jsxs("li", { children: [
807
+ m.label,
808
+ ", ",
809
+ fmtInstant(m.at)
810
+ ] }, markerKeys[index]))
811
+ ] })
812
+ ] }),
813
+ /* @__PURE__ */ jsxs("div", { ref: setScroller, className: "uix-scheduling-timeline__scroller", "data-virtual": virtual || void 0, "data-scroll-y": maxHeight !== void 0 || void 0, style: maxHeight !== void 0 ? { maxHeight } : void 0, children: [
814
+ /* @__PURE__ */ jsxs("div", { className: "uix-scheduling-timeline__grid", "data-sub-ticks": minorTicks.length > 0 || void 0, children: [
815
+ /* @__PURE__ */ jsxs("div", { className: "uix-scheduling-timeline__row uix-scheduling-timeline__row--axis", "aria-hidden": "true", children: [
816
+ /* @__PURE__ */ jsx("div", { className: "uix-scheduling-timeline__lane-label", children: labels.lanes }),
817
+ /* @__PURE__ */ jsxs("div", { className: "uix-scheduling-timeline__track uix-scheduling-timeline__axis", style: trackStyle, children: [
818
+ ticks.map((tick) => /* @__PURE__ */ jsx("span", { className: "uix-scheduling-timeline__tick", "data-major": tick.major || void 0, style: { left: `${tick.offset}%` }, children: tick.offset < 100 && /* @__PURE__ */ jsxs("span", { className: "uix-scheduling-timeline__tick-label", children: [
819
+ fmtTick(tick.at),
820
+ tick.offsetLabel && /* @__PURE__ */ jsx("span", { className: "uix-scheduling-timeline__tick-offset", children: tick.offsetLabel })
821
+ ] }) }, tick.at)),
822
+ minorTicks.map((tick) => /* @__PURE__ */ jsx("span", { className: "uix-scheduling-timeline__tick", "data-minor": "true", style: { left: `${tick.offset}%` }, children: /* @__PURE__ */ jsx("span", { className: "uix-scheduling-timeline__tick-label", children: formatTick?.(new Date(tick.at), "hour") ?? tick.label }) }, `minor-${tick.at}`))
823
+ ] })
824
+ ] }),
825
+ columnNoteList.length > 0 && /* @__PURE__ */ jsxs("div", { className: "uix-scheduling-timeline__row uix-scheduling-timeline__row--notes", children: [
826
+ /* @__PURE__ */ jsx("div", { className: "uix-scheduling-timeline__lane-label" }),
827
+ /* @__PURE__ */ jsx("div", { className: "uix-scheduling-timeline__track", style: trackStyle, children: columnNoteList.map(({ date, node, pos }) => /* @__PURE__ */ jsx("div", { className: "uix-scheduling-timeline__column-note", "data-date": date, style: { left: `${pos.left}%`, width: `${pos.width}%` }, children: node }, date)) })
828
+ ] }),
829
+ /* @__PURE__ */ jsxs(
830
+ "div",
831
+ {
832
+ ref: bodyRef,
833
+ className: "uix-scheduling-timeline__body",
834
+ onFocus: (event) => setFocusRowKey(event.target.closest?.("[data-row-key]")?.getAttribute("data-row-key") ?? null),
835
+ onBlur: (event) => {
836
+ if (!event.currentTarget.contains(event.relatedTarget)) setFocusRowKey(null);
837
+ },
838
+ children: [
839
+ /* @__PURE__ */ jsxs("div", { className: "uix-scheduling-timeline__layer", children: [
840
+ /* @__PURE__ */ jsx("div", { className: "uix-scheduling-timeline__lane-label", "aria-hidden": "true" }),
841
+ /* @__PURE__ */ jsxs("div", { ref: setLayerTrack, className: "uix-scheduling-timeline__track", style: trackStyle, children: [
842
+ /* @__PURE__ */ jsx("span", { ref: barMinRef, className: "uix-scheduling-timeline__bar-min", "aria-hidden": "true" }),
843
+ ticks.map((tick) => /* @__PURE__ */ jsx("span", { className: "uix-scheduling-timeline__gridline", "data-major": tick.major || void 0, style: { left: `${tick.offset}%` }, "aria-hidden": "true" }, tick.at)),
844
+ minorTicks.map((tick) => /* @__PURE__ */ jsx("span", { className: "uix-scheduling-timeline__gridline", "data-minor": "true", style: { left: `${tick.offset}%` }, "aria-hidden": "true" }, `minor-${tick.at}`)),
845
+ overlays.map((overlay, index) => inLayer(overlay) ? renderOverlay(overlay, overlayKeys[index], scopeOf(lanesOf(overlay))) : null),
846
+ markers.map((marker, index) => inLayer(marker) ? renderMarker(marker, markerKeys[index], scopeOf(lanesOf(marker))) : null),
847
+ nowPos && /* @__PURE__ */ jsx("span", { className: "uix-scheduling-timeline__now", style: { left: `${nowPos.left}%` }, "aria-hidden": "true", children: /* @__PURE__ */ jsx("span", { className: "uix-scheduling-timeline__now-label", children: labels.now }) })
848
+ ] })
849
+ ] }),
850
+ body
851
+ ]
852
+ }
853
+ )
185
854
  ] }),
186
- /* @__PURE__ */ jsxs("div", { className: "uix-scheduling-timeline__track", style: trackStyle, children: [
187
- ticks.map((tick) => /* @__PURE__ */ jsx("span", { className: "uix-scheduling-timeline__gridline", "data-major": tick.major || void 0, style: { left: `${tick.offset}%` }, "aria-hidden": "true" }, tick.at)),
188
- /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: overlaysFor(lane.id).map((o) => band(o, laneIndex === 0 || !!o.laneId)) }),
189
- nowPos && /* @__PURE__ */ jsx("span", { className: "uix-scheduling-timeline__now", style: { left: `${nowPos.left}%` }, "aria-hidden": "true", children: laneIndex === 0 && /* @__PURE__ */ jsx("span", { className: "uix-scheduling-timeline__now-label", children: labels.now }) }),
190
- markersFor(lane.id).map((m) => {
191
- const pos = placeSpan({ start: m.at, end: m.at }, range);
192
- return pos && /* @__PURE__ */ jsx("span", { className: "uix-scheduling-timeline__marker", style: { left: `${pos.left}%` }, title: m.label, "aria-hidden": "true" }, m.id);
193
- }),
194
- /* @__PURE__ */ jsx("ul", { className: "uix-scheduling-timeline__items", "aria-labelledby": laneLabelId, children: placed.map((p, index) => {
195
- const item = p.item;
196
- const state = item.state ?? "scheduled";
197
- const dragging = drag?.id === item.id && drag.moved;
198
- const shown = dragging && drag ? placeSpan(shiftSpan(item, drag.delta, "move", step), range) ?? p : p;
199
- return /* @__PURE__ */ jsx("li", { className: "uix-scheduling-timeline__slot", style: { left: `${shown.left}%`, width: `${shown.width}%`, "--uix-timeline-row": p.row }, children: /* @__PURE__ */ jsxs(
200
- "button",
201
- {
202
- type: "button",
203
- className: "uix-scheduling-timeline__item",
204
- "data-timeline-item": item.id,
205
- title: item.title,
206
- "data-state": state,
207
- "data-conflict": p.conflict || void 0,
208
- "data-clipped-start": p.clippedStart || void 0,
209
- "data-clipped-end": p.clippedEnd || void 0,
210
- "data-movable": movable(item) || void 0,
211
- "data-dragging": dragging || void 0,
212
- tabIndex: focusId === item.id ? 0 : -1,
213
- "aria-label": fillLabel(labels.item, { title: item.title, state: labels.states[state], start: fmtInstant(item.start), end: fmtInstant(item.end), conflict: p.conflict ? labels.conflict : "" }),
214
- "aria-describedby": movable(item) ? `${id}-hint` : void 0,
215
- onFocus: () => setActiveId(item.id),
216
- onKeyDown: (event) => onItemKeyDown(event, laneIndex, placed, index),
217
- onPointerDown: (event) => onPointerDown(event, item),
218
- onPointerMove,
219
- onPointerUp: () => onPointerUp(item),
220
- onPointerCancel: () => setDrag(null),
221
- onClick: () => {
222
- if (drag?.moved) return;
223
- onSelectItem?.(item);
224
- },
225
- children: [
226
- /* @__PURE__ */ jsx("span", { className: "uix-scheduling-timeline__item-title", children: renderItem?.(item) ?? item.title }),
227
- item.meta && /* @__PURE__ */ jsx("span", { className: "uix-scheduling-timeline__item-meta", children: item.meta })
228
- ]
229
- }
230
- ) }, item.id);
231
- }) })
855
+ virtual && /* @__PURE__ */ jsxs("span", { ref: probeRef, className: "uix-scheduling-timeline__probe", "aria-hidden": "true", children: [
856
+ /* @__PURE__ */ jsx("span", {}),
857
+ /* @__PURE__ */ jsx("span", {}),
858
+ /* @__PURE__ */ jsx("span", {})
232
859
  ] })
233
- ] }, lane.id);
234
- })
235
- ] }) }),
236
- ordered.length === 0 && /* @__PURE__ */ jsx("p", { className: "uix-scheduling-timeline__empty", children: labels.empty })
237
- ] }),
238
- onMoveItem && /* @__PURE__ */ jsx("span", { id: `${id}-hint`, className: "uix-visually-hidden", children: labels.moveHint }),
239
- /* @__PURE__ */ jsx("span", { className: "uix-visually-hidden", role: "status", "aria-live": "polite", children: announcement })
240
- ] });
860
+ ] }),
861
+ nothingToShow && /* @__PURE__ */ jsx("p", { className: "uix-scheduling-timeline__empty", children: labels.empty })
862
+ ] }),
863
+ hint && /* @__PURE__ */ jsx("span", { id: hintId, className: "uix-visually-hidden", children: hint }),
864
+ /* @__PURE__ */ jsx("span", { className: "uix-visually-hidden", role: "status", "aria-live": "polite", children: announcement })
865
+ ]
866
+ }
867
+ );
241
868
  }
242
869
  export {
243
870
  DEFAULT_SCHEDULING_TIMELINE_LABELS,