@tensor_1/react 2.28.0 → 2.30.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.
package/README.md CHANGED
@@ -331,3 +331,49 @@ toast.dismiss(id); // or toast.dismiss() for all
331
331
  - **Accessibility:** errors are `role="alert"`. Everything else is announced through the toaster's polite live region, and a loading toast that settles is announced again. Esc closes the focused toast. Tone glyphs are decorative; the text carries the meaning.
332
332
  - **Stores:** `toast` writes to a default store that `<Toaster />` reads. `createToastApi(createToastStore())` gives a separate queue (tests, embedded apps); pass its `store` to `<Toaster store>`. The store has no React dependency.
333
333
  - `<Toaster>` still renders hand-managed `<Toast>` children first. `Toast` gains `tone="warning"` and an `action` slot.
334
+ ### Icons (`@tensor_1/react/icons`)
335
+
336
+ The UIx icon set (HAR-996): 233 glyphs drawn in `currentColor` at the `--uix-icon-*` sizes. Icons come only from UIx
337
+ (workspace ADR-0038); no product adds an icon library. The glyphs are Lucide's (ISC); the licence travels in
338
+ `THIRD_PARTY_NOTICES.md`. Import the CSS once: `@tensor_1/tokens/components/icon` (also in the bundle).
339
+
340
+ ```tsx
341
+ import { ShieldCheckIcon, TriangleAlertIcon, Icon } from '@tensor_1/react/icons';
342
+ import type { UixIcon, IconName } from '@tensor_1/react/icons';
343
+
344
+ <ShieldCheckIcon /> // decorative: aria-hidden, 20 px (md)
345
+ <ShieldCheckIcon size="sm" tone="success" label="Verified" /> // role="img" with a name
346
+ <Button icon aria-label="Delete"><Trash2Icon /></Button> // the button carries the name
347
+ <Icon name={entityIconName} /> // chosen at run time (loads the whole set)
348
+ const icons: Record<EntityType, UixIcon> = { incident: SirenIcon, change: GitBranchIcon };
349
+ ```
350
+
351
+ - **Migrating from `lucide-react`:** import from `@tensor_1/react/icons` and add `Icon` to the name: `ShieldCheck` → `ShieldCheckIcon`. The older Lucide names the products use are kept as aliases (`AlertTriangleIcon`, `CheckCircle2Icon`, `Loader2Icon`, …). `etc/icon-names.json` maps every Lucide name to its UIx component and glyph, for a codemod. `LucideIcon` becomes `UixIcon`.
352
+ - **Size:** `sm` 16 px, `md` 20 px (default), `lg` 24 px, or any CSS length or pixel number. A product sizing class (a Tailwind `size-4`) wins over the default.
353
+ - **Tone:** `current` (default, inherits the text colour), `muted`, `accent`, `success`, `warning`, `danger`, `info`.
354
+ - **Accessibility:** decorative unless `label` is set. Never let an icon carry meaning alone; pair it with text.
355
+ - **Bundle size:** each `<Name>Icon` is tree-shaken; one icon adds well under 3 KB minified. `<Icon name>` and `ICON_GLYPHS` load all of them.
356
+ - **A glyph is missing?** Add its Lucide name to `scripts/icon-names.txt`, run `node scripts/generate-icons.mjs <lucide-react dir>` and release. Do not paste an SVG into a product.
357
+ ### SchedulingTimeline
358
+
359
+ Lanes of bars on a time axis (HAR-1364; TENSOR's change day/week timeline, rollout Gantt rows and licence-renewal
360
+ markers). It pairs with `SchedulingCalendar`: same entry states, the same explicit time zone.
361
+
362
+ ```tsx
363
+ <SchedulingTimeline
364
+ lanes={[{ id: 'net', label: 'Network', meta: '3 changes' }]}
365
+ items={changes.map((c) => ({ id: c.id, laneId: c.team, title: c.title, start: c.start, end: c.end, state: c.state }))}
366
+ range={{ start: weekStart, end: weekEnd }} scale="day" timeZone={tz} now={nowIso}
367
+ overlays={[{ id: 'q4', kind: 'freeze', label: 'Q4 freeze', start, end }]}
368
+ markers={[{ id: 'r1', label: 'Renewal: Fortinet', at, laneId: 'net' }]}
369
+ onSelectItem={(item) => openChange(item.id)}
370
+ onMoveItem={(id, { start, end }) => reschedule(id, start, end)}
371
+ />
372
+ ```
373
+
374
+ - **Axis:** `scale` is `hour`, `day` (default), `week` or `month`. Ticks fall on wall-clock boundaries in `timeZone` (DST and half-hour zones included). `tickWidth` sets the width of one unit; the axis scrolls inside the component and the lane column stays put.
375
+ - **Bars:** overlapping bars in a lane stack and are hatched (`data-conflict`), so an overlap reads without colour. A bar cut off by the range shows a dashed edge. `renderItem` replaces the bar text. The full title is in the name and the `title` tooltip.
376
+ - **Moving:** with `onMoveItem`, bars move by drag or Shift+←/→ and their end changes with Alt+Shift+←/→. Moves snap to `step` (15 min on an hour axis, 1 h on a day axis, 1 day otherwise), and each move is announced. `movable: false` pins a bar. The component never moves a bar itself; it reports the new start and end and renders what you pass back.
377
+ - **Keyboard:** one tab stop. ←/→ go to the previous or next bar in a lane, ↑/↓ to the nearest bar in the next lane, Home/End to the lane's ends, and Enter selects.
378
+ - **Windows and markers:** `overlays` (`freeze`, `maintenance`, `blackout`; all lanes, or one with `laneId`), `markers` (a point in time) and `now` are drawn behind the bars and listed for screen readers.
379
+ - Pure helpers are exported for tests and server code: `timelineTicks`, `placeSpan`, `layoutLane`, `shiftSpan`, `snapToStep`, `pixelsToMs`, `defaultTimelineStep`.
@@ -0,0 +1,54 @@
1
+ # Third-party notices
2
+
3
+ `@tensor_1/react` redistributes SVG path data from these projects. Their licences require this notice to travel with copies.
4
+
5
+ ## Lucide (lucide-react 1.39.0)
6
+
7
+ Used by: the icon set (`@tensor_1/react/icons`, `dist/icons/**`) and the editor, emoji, reaction and table-control glyphs (`EditorIcons`). Source: https://lucide.dev
8
+
9
+ ```text
10
+ ISC License
11
+
12
+ Copyright (c) 2026 Lucide Icons and Contributors
13
+
14
+ Permission to use, copy, modify, and/or distribute this software for any
15
+ purpose with or without fee is hereby granted, provided that the above
16
+ copyright notice and this permission notice appear in all copies.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
19
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
20
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
21
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
22
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
23
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
24
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
25
+
26
+ ---
27
+
28
+ The following Lucide icons are derived from the Feather project:
29
+
30
+ airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out
31
+
32
+ The MIT License (MIT) (for the icons listed above)
33
+
34
+ Copyright (c) 2013-present Cole Bemis
35
+
36
+ Permission is hereby granted, free of charge, to any person obtaining a copy
37
+ of this software and associated documentation files (the "Software"), to deal
38
+ in the Software without restriction, including without limitation the rights
39
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
40
+ copies of the Software, and to permit persons to whom the Software is
41
+ furnished to do so, subject to the following conditions:
42
+
43
+ The above copyright notice and this permission notice shall be included in all
44
+ copies or substantial portions of the Software.
45
+
46
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
47
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
48
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
49
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
50
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
51
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
52
+ SOFTWARE.
53
+
54
+ ```
@@ -0,0 +1,109 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+ import { TimelineRange, TimelineScale } from '../scheduling-timeline-model.js';
4
+ import { SchedulingEntryState } from './SchedulingCalendar.js';
5
+ import '../calendar-model.js';
6
+
7
+ interface SchedulingTimelineLane {
8
+ id: string;
9
+ label: ReactNode;
10
+ /** A quieter second line under the lane name (an owner, a count). */
11
+ meta?: ReactNode;
12
+ }
13
+ interface SchedulingTimelineItem {
14
+ id: string;
15
+ laneId: string;
16
+ title: string;
17
+ /** ISO 8601 instants. */
18
+ start: string;
19
+ end: string;
20
+ state?: SchedulingEntryState;
21
+ meta?: string;
22
+ /** `false` keeps this bar fixed even when `onMoveItem` is set. */
23
+ movable?: boolean;
24
+ }
25
+ type SchedulingTimelineOverlayKind = 'freeze' | 'maintenance' | 'blackout';
26
+ interface SchedulingTimelineOverlay {
27
+ id: string;
28
+ label: string;
29
+ start: string;
30
+ end: string;
31
+ kind: SchedulingTimelineOverlayKind;
32
+ /** Only this lane; every lane when unset. */
33
+ laneId?: string;
34
+ }
35
+ interface SchedulingTimelineMarker {
36
+ id: string;
37
+ label: string;
38
+ /** ISO 8601 instant. */
39
+ at: string;
40
+ laneId?: string;
41
+ }
42
+ interface SchedulingTimelineLabels {
43
+ region: string;
44
+ lanes: string;
45
+ /** `{title}`, `{state}`, `{start}`, `{end}`, `{conflict}`. */
46
+ item: string;
47
+ conflict: string;
48
+ moveHint: string;
49
+ /** Announced after a move. `{title}`, `{start}`, `{end}`. */
50
+ moved: string;
51
+ /** Announced after a resize. `{title}`, `{end}`. */
52
+ resized: string;
53
+ loading: string;
54
+ retry: string;
55
+ empty: string;
56
+ /** Heading of the screen-reader list of windows and markers. */
57
+ windows: string;
58
+ now: string;
59
+ states: Record<SchedulingEntryState, string>;
60
+ overlays: Record<SchedulingTimelineOverlayKind, string>;
61
+ }
62
+ declare const DEFAULT_SCHEDULING_TIMELINE_LABELS: SchedulingTimelineLabels;
63
+ interface SchedulingTimelineProps {
64
+ lanes: SchedulingTimelineLane[];
65
+ items: SchedulingTimelineItem[];
66
+ /** The visible window (ISO 8601 instants). */
67
+ range: TimelineRange;
68
+ /** Tick unit of the axis. Default `day`. */
69
+ scale?: TimelineScale;
70
+ /** IANA zone for tick boundaries and every printed time. */
71
+ timeZone: string;
72
+ locale?: string;
73
+ overlays?: SchedulingTimelineOverlay[];
74
+ markers?: SchedulingTimelineMarker[];
75
+ /** Draws a "now" line at this instant (pass the current time; the component keeps no clock). */
76
+ now?: string;
77
+ /** First day of a week tick (0 = Sunday). Default 1. */
78
+ weekStartsOn?: number;
79
+ /** Width of one tick, as a CSS length. Defaults: hour 3.5rem, day 7rem, week 6rem, month 8rem. */
80
+ tickWidth?: string;
81
+ /** Move/resize granularity in ms. Defaults: 15 min (hour), 1 h (day), 1 day (week, month). */
82
+ step?: number;
83
+ /** Tick label. Default: `Intl` in `locale` and `timeZone`. */
84
+ formatTick?: (at: Date, scale: TimelineScale) => string;
85
+ /** Start/end text in names and announcements. Default: `Intl` medium date + short time in `timeZone`. */
86
+ formatInstant?: (instant: string) => string;
87
+ /** Bar content. Default: the title. */
88
+ renderItem?: (item: SchedulingTimelineItem) => ReactNode;
89
+ onSelectItem?: (item: SchedulingTimelineItem) => void;
90
+ /** Makes bars draggable and keyboard-movable. Receives the snapped new start and end. */
91
+ onMoveItem?: (id: string, next: {
92
+ start: string;
93
+ end: string;
94
+ }) => void;
95
+ loading?: boolean;
96
+ error?: string;
97
+ onRetry?: () => void;
98
+ labels?: Partial<SchedulingTimelineLabels>;
99
+ className?: string;
100
+ }
101
+ /**
102
+ * Lanes of bars on a time axis (HAR-1364; TENSOR C8): change schedules, rollout Gantt rows,
103
+ * renewal markers. Overlapping bars in a lane stack and are hatched; freeze, maintenance and
104
+ * blackout windows are bands; with `onMoveItem`, bars move by drag or Shift+arrows and their
105
+ * end changes with Alt+Shift+arrows, snapped to `step`.
106
+ */
107
+ declare function SchedulingTimeline({ lanes, items, range, scale, timeZone, locale, overlays, markers, now, weekStartsOn, tickWidth, step: stepProp, formatTick, formatInstant: formatInstantProp, renderItem, onSelectItem, onMoveItem, loading, error, onRetry, labels: labelOverrides, className, }: SchedulingTimelineProps): react.JSX.Element;
108
+
109
+ export { DEFAULT_SCHEDULING_TIMELINE_LABELS, SchedulingTimeline, type SchedulingTimelineItem, type SchedulingTimelineLabels, type SchedulingTimelineLane, type SchedulingTimelineMarker, type SchedulingTimelineOverlay, type SchedulingTimelineOverlayKind, type SchedulingTimelineProps };
@@ -0,0 +1,245 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { useId, useMemo, useRef, useState } from "react";
4
+ import { cx } from "../cx.js";
5
+ import { fillLabel } from "../fill-label.js";
6
+ import { useUixLabels } from "../labels-context.js";
7
+ import { defaultTimelineStep, layoutLane, pixelsToMs, placeSpan, shiftSpan, timelineTicks } from "../scheduling-timeline-model.js";
8
+ const DEFAULT_SCHEDULING_TIMELINE_LABELS = {
9
+ region: "Schedule timeline in {timeZone}",
10
+ lanes: "Lanes",
11
+ item: "{title}, {state}, {start} to {end}{conflict}",
12
+ conflict: ", overlaps another entry",
13
+ moveHint: "Shift and an arrow key moves it; Alt, Shift and an arrow key changes when it ends.",
14
+ moved: "Moved {title} to {start} \u2013 {end}",
15
+ resized: "{title} now ends {end}",
16
+ loading: "Loading schedule\u2026",
17
+ retry: "Try again",
18
+ empty: "Nothing is scheduled in this range.",
19
+ windows: "Windows and markers in this range",
20
+ now: "Now",
21
+ states: { scheduled: "Scheduled", conflicted: "Conflicted", "in-progress": "In progress", "blackout-violation": "Blackout violation" },
22
+ overlays: { freeze: "Change freeze", maintenance: "Maintenance window", blackout: "Blackout" }
23
+ };
24
+ const TICK_WIDTH = { hour: "3.5rem", day: "7rem", week: "6rem", month: "8rem" };
25
+ const TICK_FORMAT = {
26
+ hour: { hour: "2-digit", minute: "2-digit" },
27
+ day: { weekday: "short", day: "numeric" },
28
+ week: { day: "numeric", month: "short" },
29
+ month: { month: "short", year: "numeric" }
30
+ };
31
+ function SchedulingTimeline({
32
+ lanes,
33
+ items,
34
+ range,
35
+ scale = "day",
36
+ timeZone,
37
+ locale,
38
+ overlays = [],
39
+ markers = [],
40
+ now,
41
+ weekStartsOn = 1,
42
+ tickWidth,
43
+ step: stepProp,
44
+ formatTick,
45
+ formatInstant: formatInstantProp,
46
+ renderItem,
47
+ onSelectItem,
48
+ onMoveItem,
49
+ loading,
50
+ error,
51
+ onRetry,
52
+ labels: labelOverrides,
53
+ className
54
+ }) {
55
+ const uixLabels = useUixLabels();
56
+ const labels = {
57
+ ...DEFAULT_SCHEDULING_TIMELINE_LABELS,
58
+ ...uixLabels.schedulingTimeline,
59
+ ...labelOverrides,
60
+ states: { ...DEFAULT_SCHEDULING_TIMELINE_LABELS.states, ...uixLabels.schedulingTimeline?.states, ...labelOverrides?.states },
61
+ overlays: { ...DEFAULT_SCHEDULING_TIMELINE_LABELS.overlays, ...uixLabels.schedulingTimeline?.overlays, ...labelOverrides?.overlays }
62
+ };
63
+ const id = useId();
64
+ const step = stepProp ?? defaultTimelineStep(scale);
65
+ 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]
69
+ // eslint-disable-line react-hooks/exhaustive-deps
70
+ );
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;
80
+ const focusItem = (itemId) => {
81
+ if (!itemId) return;
82
+ setActiveId(itemId);
83
+ requestAnimationFrame(() => rootRef.current?.querySelector(`[data-timeline-item="${CSS.escape(itemId)}"]`)?.focus());
84
+ };
85
+ const commit = (item, delta, mode) => {
86
+ if (!onMoveItem || delta === 0) return;
87
+ const next = shiftSpan(item, delta, mode, step);
88
+ onMoveItem(item.id, next);
89
+ 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
+ };
91
+ const onItemKeyDown = (event, laneIndex, placed, index) => {
92
+ const item = placed[index].item;
93
+ const horizontal = event.key === "ArrowLeft" ? -1 : event.key === "ArrowRight" ? 1 : 0;
94
+ if (horizontal && event.shiftKey && movable(item)) {
95
+ event.preventDefault();
96
+ commit(item, horizontal * step, event.altKey ? "resize" : "move");
97
+ return;
98
+ }
99
+ if (horizontal) {
100
+ event.preventDefault();
101
+ focusItem(placed[index + horizontal]?.item.id);
102
+ return;
103
+ }
104
+ if (event.key === "Home" || event.key === "End") {
105
+ event.preventDefault();
106
+ focusItem((event.key === "Home" ? placed[0] : placed[placed.length - 1])?.item.id);
107
+ return;
108
+ }
109
+ const vertical = event.key === "ArrowUp" ? -1 : event.key === "ArrowDown" ? 1 : 0;
110
+ if (vertical) {
111
+ event.preventDefault();
112
+ 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);
117
+ focusItem(nearest.item.id);
118
+ return;
119
+ }
120
+ }
121
+ };
122
+ const onPointerDown = (event, item) => {
123
+ if (!movable(item) || event.button !== 0) return;
124
+ const track = event.currentTarget.closest(".uix-scheduling-timeline__track");
125
+ if (!track) return;
126
+ event.currentTarget.setPointerCapture?.(event.pointerId);
127
+ setDrag({ id: item.id, x: event.clientX, width: track.getBoundingClientRect().width, delta: 0, moved: false });
128
+ };
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 });
134
+ };
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);
140
+ };
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);
144
+ 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
+ ] })
172
+ ] }),
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 })
185
+ ] }),
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
+ }) })
232
+ ] })
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
+ ] });
241
+ }
242
+ export {
243
+ DEFAULT_SCHEDULING_TIMELINE_LABELS,
244
+ SchedulingTimeline
245
+ };