@tensor_1/react 2.29.0 → 2.31.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.
@@ -0,0 +1,139 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useId, useRef, useState } from "react";
4
+ import { cx } from "../cx.js";
5
+ import { fillLabel } from "../fill-label.js";
6
+ import { formatFileSize, partitionFiles } from "../file-model.js";
7
+ import { Button } from "./Button.js";
8
+ import { Input } from "./Input.js";
9
+ import { Progress } from "./Progress.js";
10
+ import { FileIcon } from "./FileIcon.js";
11
+ const DEFAULT_FILE_UPLOAD_LABELS = {
12
+ title: "Drop files here",
13
+ choose: "Choose files",
14
+ drop: "Release to add the files",
15
+ list: "Files",
16
+ remove: "Remove {name}",
17
+ retry: "Retry {name}",
18
+ retryShort: "Retry",
19
+ queued: "Waiting",
20
+ uploading: "Uploading, {progress}%",
21
+ done: "Uploaded",
22
+ failed: "Upload failed",
23
+ alt: "Description of {name} for screen readers",
24
+ added: "{count} added",
25
+ wrongType: "{name} is not an accepted file type ({accept}).",
26
+ tooLarge: "{name} is larger than {max}.",
27
+ tooMany: "{name} was not added: at most {max} files."
28
+ };
29
+ function FileUpload({
30
+ items,
31
+ onFilesAdded,
32
+ onRemove,
33
+ onRetry,
34
+ onAltChange,
35
+ accept,
36
+ maxSize,
37
+ maxFiles,
38
+ multiple = true,
39
+ capture,
40
+ disabled,
41
+ hint,
42
+ locale,
43
+ labels: labelOverrides,
44
+ className
45
+ }) {
46
+ const labels = { ...DEFAULT_FILE_UPLOAD_LABELS, ...labelOverrides };
47
+ const id = useId();
48
+ const inputRef = useRef(null);
49
+ const [over, setOver] = useState(false);
50
+ const [messages, setMessages] = useState([]);
51
+ const [announce, setAnnounce] = useState("");
52
+ const take = (list) => {
53
+ if (!list || disabled) return;
54
+ const files = Array.from(list);
55
+ const { accepted, rejected } = partitionFiles(files, { accept, maxSize, maxFiles, existing: items.length });
56
+ const max = maxSize !== void 0 ? formatFileSize(maxSize, locale) : "";
57
+ setMessages(rejected.map(({ file, reason }) => fillLabel(
58
+ reason === "type" ? labels.wrongType : reason === "size" ? labels.tooLarge : labels.tooMany,
59
+ { name: file.name, accept: accept ?? "", max: reason === "count" ? String(maxFiles) : max }
60
+ )));
61
+ setAnnounce(accepted.length ? fillLabel(labels.added, { count: accepted.length }) : "");
62
+ onFilesAdded(accepted, rejected);
63
+ };
64
+ const onDrop = (event) => {
65
+ event.preventDefault();
66
+ setOver(false);
67
+ take(event.dataTransfer?.files ?? null);
68
+ };
69
+ return /* @__PURE__ */ jsxs("div", { className: cx("uix-file-upload", className), "data-disabled": disabled || void 0, children: [
70
+ /* @__PURE__ */ jsxs(
71
+ "div",
72
+ {
73
+ className: "uix-dropzone",
74
+ "data-dragover": over || void 0,
75
+ onDragOver: (event) => {
76
+ if (disabled) return;
77
+ event.preventDefault();
78
+ setOver(true);
79
+ },
80
+ onDragLeave: () => setOver(false),
81
+ onDrop,
82
+ onClick: (event) => {
83
+ if (event.target === event.currentTarget) inputRef.current?.click();
84
+ },
85
+ children: [
86
+ /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.75, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M12 15V4M7 9l5-5 5 5M4 15v4a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-4" }) }),
87
+ /* @__PURE__ */ jsx("strong", { "aria-hidden": over || void 0, children: over ? labels.drop : labels.title }),
88
+ hint != null && /* @__PURE__ */ jsx("span", { className: "uix-file-upload__hint", id: `${id}-hint`, children: hint }),
89
+ /* @__PURE__ */ jsx(Button, { type: "button", size: "sm", disabled, "aria-describedby": hint != null ? `${id}-hint` : void 0, onClick: () => inputRef.current?.click(), children: labels.choose }),
90
+ /* @__PURE__ */ jsx(
91
+ "input",
92
+ {
93
+ ref: inputRef,
94
+ type: "file",
95
+ className: "uix-visually-hidden",
96
+ tabIndex: -1,
97
+ "aria-hidden": "true",
98
+ accept,
99
+ multiple,
100
+ capture,
101
+ disabled,
102
+ onChange: (event) => {
103
+ take(event.currentTarget.files);
104
+ event.currentTarget.value = "";
105
+ }
106
+ }
107
+ )
108
+ ]
109
+ }
110
+ ),
111
+ messages.length > 0 && /* @__PURE__ */ jsx("ul", { className: "uix-file-upload__errors", role: "alert", children: messages.map((m, i) => /* @__PURE__ */ jsx("li", { children: m }, i)) }),
112
+ /* @__PURE__ */ jsx("span", { className: "uix-visually-hidden", role: "status", "aria-live": "polite", children: announce }),
113
+ items.length > 0 && /* @__PURE__ */ jsx("ul", { className: "uix-filelist uix-file-upload__list", "aria-label": labels.list, children: items.map((item) => {
114
+ const statusText = item.status === "uploading" ? fillLabel(labels.uploading, { progress: item.progress ?? 0 }) : item.status === "done" ? labels.done : item.status === "error" ? labels.failed : labels.queued;
115
+ const image = !!item.previewUrl;
116
+ return /* @__PURE__ */ jsxs("li", { className: "uix-filelist__item uix-file-upload__item", "data-status": item.status, children: [
117
+ image ? /* @__PURE__ */ jsx("img", { className: "uix-file-upload__thumb", src: item.previewUrl, alt: "" }) : /* @__PURE__ */ jsx("span", { className: "uix-attachment__icon", children: /* @__PURE__ */ jsx(FileIcon, { name: item.name, type: item.type }) }),
118
+ /* @__PURE__ */ jsxs("div", { className: "uix-file-upload__body", children: [
119
+ /* @__PURE__ */ jsx("span", { className: "uix-file-upload__name", children: item.name }),
120
+ /* @__PURE__ */ jsxs("span", { className: "uix-file-upload__meta", children: [
121
+ /* @__PURE__ */ jsx("span", { children: formatFileSize(item.size, locale) }),
122
+ item.status !== "uploading" && /* @__PURE__ */ jsx("span", { className: "uix-file-upload__status", children: statusText })
123
+ ] }),
124
+ item.status === "uploading" && /* @__PURE__ */ jsx(Progress, { value: item.progress, indeterminate: item.progress === void 0, max: 100, label: statusText }),
125
+ item.status === "error" && item.error != null && /* @__PURE__ */ jsx("span", { className: "uix-file-upload__error", children: item.error }),
126
+ image && onAltChange && /* @__PURE__ */ jsx(Input, { size: "sm", "aria-label": fillLabel(labels.alt, { name: item.name }), placeholder: fillLabel(labels.alt, { name: item.name }), value: item.alt ?? "", onChange: (event) => onAltChange(item.id, event.currentTarget.value) })
127
+ ] }),
128
+ /* @__PURE__ */ jsxs("div", { className: "uix-file-upload__actions", children: [
129
+ item.status === "error" && onRetry && /* @__PURE__ */ jsx(Button, { type: "button", size: "sm", "aria-label": fillLabel(labels.retry, { name: item.name }), onClick: () => onRetry(item.id), children: labels.retryShort }),
130
+ onRemove && /* @__PURE__ */ jsx(Button, { type: "button", size: "xs", variant: "ghost", icon: true, "aria-label": fillLabel(labels.remove, { name: item.name }), onClick: () => onRemove(item.id), children: /* @__PURE__ */ jsx("svg", { viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: 1.75, strokeLinecap: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M4 4l8 8M12 4l-8 8" }) }) })
131
+ ] })
132
+ ] }, item.id);
133
+ }) })
134
+ ] });
135
+ }
136
+ export {
137
+ DEFAULT_FILE_UPLOAD_LABELS,
138
+ FileUpload
139
+ };
@@ -0,0 +1,51 @@
1
+ import * as react from 'react';
2
+ import { TextFilterOperator, NumberFilterOperator, FilterField, FilterValue } from '../filter-model.js';
3
+
4
+ interface FilterEditorLabels {
5
+ apply: string;
6
+ clear: string;
7
+ operator: string;
8
+ value: string;
9
+ from: string;
10
+ to: string;
11
+ /** Between two numbers: "and". */
12
+ and: string;
13
+ any: string;
14
+ yes: string;
15
+ no: string;
16
+ search: string;
17
+ searching: string;
18
+ noMatches: string;
19
+ searchFailed: string;
20
+ retry: string;
21
+ selectAll: string;
22
+ selectNone: string;
23
+ /** `{count}` chosen. */
24
+ selected: string;
25
+ /** Remove one chosen record. `{label}`. */
26
+ remove: string;
27
+ operators: Record<TextFilterOperator | NumberFilterOperator, string>;
28
+ }
29
+ declare const DEFAULT_FILTER_EDITOR_LABELS: FilterEditorLabels;
30
+ interface FilterEditorProps {
31
+ field: FilterField;
32
+ /** Undefined means "not filtered yet". */
33
+ value: FilterValue | undefined;
34
+ onValueChange: (value: FilterValue) => void;
35
+ onApply: () => void;
36
+ onClear: () => void;
37
+ labels?: Partial<FilterEditorLabels>;
38
+ /** Show an option filter above an enum list once it has more options than this. Default 8. */
39
+ searchThreshold?: number;
40
+ /** Delay before a reference search runs, in ms. Default 250. */
41
+ searchDelay?: number;
42
+ }
43
+ /**
44
+ * Editor content for one typed column filter (HAR-1365; TENSOR C6, MOTUS C-1): an enum
45
+ * multi-select, text or number with a condition, a date range, yes/no, or records found by
46
+ * an async search. The product owns the popover trigger and the query; `summarizeFilter`
47
+ * turns the applied value into the chip text. `FilterPopover` stays for the one-string case.
48
+ */
49
+ declare function FilterEditor({ field, value: valueProp, onValueChange, onApply, onClear, labels: labelOverrides, searchThreshold, searchDelay }: FilterEditorProps): react.JSX.Element;
50
+
51
+ export { DEFAULT_FILTER_EDITOR_LABELS, FilterEditor, type FilterEditorLabels, type FilterEditorProps };
@@ -0,0 +1,175 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useId, useMemo, useRef, useState } from "react";
4
+ import { Button } from "./Button.js";
5
+ import { Checkbox } from "./Checkbox.js";
6
+ import { Input } from "./Input.js";
7
+ import { Radio, RadioGroup } from "./Radio.js";
8
+ import { Select } from "./Select.js";
9
+ import { Chip } from "./Chip.js";
10
+ import { fillLabel } from "../fill-label.js";
11
+ import { foldForSearch } from "../search-suggest-model.js";
12
+ import { NUMBER_FILTER_OPERATORS, TEXT_FILTER_OPERATORS, emptyFilterValue } from "../filter-model.js";
13
+ const DEFAULT_FILTER_EDITOR_LABELS = {
14
+ apply: "Apply",
15
+ clear: "Clear",
16
+ operator: "Condition",
17
+ value: "Value",
18
+ from: "From",
19
+ to: "To",
20
+ and: "and",
21
+ any: "Any",
22
+ yes: "Yes",
23
+ no: "No",
24
+ search: "Search",
25
+ searching: "Searching\u2026",
26
+ noMatches: "No matches.",
27
+ searchFailed: "Search failed.",
28
+ retry: "Try again",
29
+ selectAll: "Select all",
30
+ selectNone: "Clear selection",
31
+ selected: "{count} selected",
32
+ remove: "Remove {label}",
33
+ operators: {
34
+ contains: "contains",
35
+ "not-contains": "does not contain",
36
+ is: "is",
37
+ "is-not": "is not",
38
+ "starts-with": "starts with",
39
+ eq: "equals",
40
+ neq: "does not equal",
41
+ lt: "less than",
42
+ lte: "at most",
43
+ gt: "more than",
44
+ gte: "at least",
45
+ between: "between"
46
+ }
47
+ };
48
+ function FilterEditor({ field, value: valueProp, onValueChange, onApply, onClear, labels: labelOverrides, searchThreshold = 8, searchDelay = 250 }) {
49
+ const labels = { ...DEFAULT_FILTER_EDITOR_LABELS, ...labelOverrides, operators: { ...DEFAULT_FILTER_EDITOR_LABELS.operators, ...labelOverrides?.operators } };
50
+ const id = useId();
51
+ const value = valueProp?.kind === field.kind ? valueProp : emptyFilterValue(field.kind);
52
+ let body = null;
53
+ switch (value.kind) {
54
+ case "enum":
55
+ body = /* @__PURE__ */ jsx(EnumBody, { field, value, onChange: onValueChange, labels, threshold: searchThreshold });
56
+ break;
57
+ case "reference":
58
+ body = /* @__PURE__ */ jsx(ReferenceBody, { field, value, onChange: onValueChange, labels, delay: searchDelay });
59
+ break;
60
+ case "text": {
61
+ const ops = field.operators ?? TEXT_FILTER_OPERATORS;
62
+ body = /* @__PURE__ */ jsxs("div", { className: "uix-filter-editor__row", children: [
63
+ ops.length > 1 && /* @__PURE__ */ jsx(Select, { size: "sm", "aria-label": labels.operator, value: value.operator, onChange: (e) => onValueChange({ ...value, operator: e.currentTarget.value }), children: ops.map((op) => /* @__PURE__ */ jsx("option", { value: op, children: labels.operators[op] }, op)) }),
64
+ /* @__PURE__ */ jsx(Input, { "aria-label": labels.value, value: value.text, onChange: (e) => onValueChange({ ...value, text: e.currentTarget.value }), onKeyDown: (e) => {
65
+ if (e.key === "Enter") onApply();
66
+ }, autoFocus: true })
67
+ ] });
68
+ break;
69
+ }
70
+ case "number": {
71
+ const ops = field.operators ?? NUMBER_FILTER_OPERATORS;
72
+ const toNumber = (raw) => raw.trim() === "" ? void 0 : Number(raw);
73
+ body = /* @__PURE__ */ jsxs("div", { className: "uix-filter-editor__row", children: [
74
+ ops.length > 1 && /* @__PURE__ */ jsx(Select, { size: "sm", "aria-label": labels.operator, value: value.operator, onChange: (e) => onValueChange({ ...value, operator: e.currentTarget.value }), children: ops.map((op) => /* @__PURE__ */ jsx("option", { value: op, children: labels.operators[op] }, op)) }),
75
+ /* @__PURE__ */ jsx(Input, { type: "number", inputMode: "decimal", "aria-label": value.operator === "between" ? labels.from : labels.value, value: value.value ?? "", onChange: (e) => onValueChange({ ...value, value: toNumber(e.currentTarget.value) }), autoFocus: true }),
76
+ value.operator === "between" && /* @__PURE__ */ jsxs(Fragment, { children: [
77
+ /* @__PURE__ */ jsx("span", { className: "uix-filter-editor__and", children: labels.and }),
78
+ /* @__PURE__ */ jsx(Input, { type: "number", inputMode: "decimal", "aria-label": labels.to, value: value.to ?? "", onChange: (e) => onValueChange({ ...value, to: toNumber(e.currentTarget.value) }) })
79
+ ] }),
80
+ field.unit && /* @__PURE__ */ jsx("span", { className: "uix-filter-editor__unit", children: field.unit })
81
+ ] });
82
+ break;
83
+ }
84
+ case "date-range":
85
+ body = /* @__PURE__ */ jsxs("div", { className: "uix-filter-editor__row", children: [
86
+ /* @__PURE__ */ jsxs("label", { className: "uix-filter-editor__date", children: [
87
+ /* @__PURE__ */ jsx("span", { children: labels.from }),
88
+ /* @__PURE__ */ jsx(Input, { type: "date", value: value.from ?? "", max: value.to, onChange: (e) => onValueChange({ ...value, from: e.currentTarget.value || void 0 }) })
89
+ ] }),
90
+ /* @__PURE__ */ jsxs("label", { className: "uix-filter-editor__date", children: [
91
+ /* @__PURE__ */ jsx("span", { children: labels.to }),
92
+ /* @__PURE__ */ jsx(Input, { type: "date", value: value.to ?? "", min: value.from, onChange: (e) => onValueChange({ ...value, to: e.currentTarget.value || void 0 }) })
93
+ ] })
94
+ ] });
95
+ break;
96
+ case "boolean": {
97
+ const pick = (v) => onValueChange({ kind: "boolean", value: v });
98
+ body = /* @__PURE__ */ jsxs(RadioGroup, { className: "uix-filter-editor__choices", children: [
99
+ /* @__PURE__ */ jsx(Radio, { name: `${id}-bool`, label: labels.any, checked: value.value === void 0, onChange: () => pick(void 0) }),
100
+ /* @__PURE__ */ jsx(Radio, { name: `${id}-bool`, label: field.trueLabel ?? labels.yes, checked: value.value === true, onChange: () => pick(true) }),
101
+ /* @__PURE__ */ jsx(Radio, { name: `${id}-bool`, label: field.falseLabel ?? labels.no, checked: value.value === false, onChange: () => pick(false) })
102
+ ] });
103
+ break;
104
+ }
105
+ }
106
+ return /* @__PURE__ */ jsxs("div", { className: "uix-filter-popover uix-filter-editor", role: "group", "aria-labelledby": `${id}-label`, "data-kind": field.kind, children: [
107
+ /* @__PURE__ */ jsx("div", { id: `${id}-label`, className: "uix-filter-editor__label", children: field.label }),
108
+ body,
109
+ /* @__PURE__ */ jsxs("div", { className: "uix-filter-popover__actions", children: [
110
+ /* @__PURE__ */ jsx(Button, { type: "button", size: "sm", onClick: onClear, children: labels.clear }),
111
+ /* @__PURE__ */ jsx(Button, { type: "button", size: "sm", variant: "primary", onClick: onApply, children: labels.apply })
112
+ ] })
113
+ ] });
114
+ }
115
+ function EnumBody({ field, value, onChange, labels, threshold }) {
116
+ const [query, setQuery] = useState("");
117
+ const options = field.options ?? [];
118
+ const shown = useMemo(() => {
119
+ const q = foldForSearch(query.trim()).text;
120
+ return q ? options.filter((o) => foldForSearch(o.label).text.includes(q)) : options;
121
+ }, [options, query]);
122
+ const selected = new Set(value.values);
123
+ const toggle = (v, on) => onChange({ kind: "enum", values: on ? [...value.values, v] : value.values.filter((x) => x !== v) });
124
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
125
+ options.length > threshold && /* @__PURE__ */ jsx(Input, { type: "search", size: "sm", "aria-label": labels.search, placeholder: labels.search, value: query, onChange: (e) => setQuery(e.currentTarget.value), autoFocus: true }),
126
+ /* @__PURE__ */ jsxs("div", { className: "uix-filter-editor__bulk", children: [
127
+ /* @__PURE__ */ jsx("span", { className: "uix-filter-editor__count", "aria-live": "polite", children: fillLabel(labels.selected, { count: value.values.length }) }),
128
+ /* @__PURE__ */ jsx(Button, { type: "button", variant: "link", size: "sm", onClick: () => onChange({ kind: "enum", values: [.../* @__PURE__ */ new Set([...value.values, ...shown.map((o) => o.value)])] }), children: labels.selectAll }),
129
+ /* @__PURE__ */ jsx(Button, { type: "button", variant: "link", size: "sm", onClick: () => onChange({ kind: "enum", values: [] }), children: labels.selectNone })
130
+ ] }),
131
+ /* @__PURE__ */ jsx("div", { className: "uix-filter-editor__list", children: shown.length === 0 ? /* @__PURE__ */ jsx("p", { className: "uix-filter-editor__note", children: labels.noMatches }) : shown.map((o) => /* @__PURE__ */ jsx(Checkbox, { label: o.label, checked: selected.has(o.value), onChange: (e) => toggle(o.value, e.currentTarget.checked) }, o.value)) })
132
+ ] });
133
+ }
134
+ function ReferenceBody({ field, value, onChange, labels, delay }) {
135
+ const [query, setQuery] = useState("");
136
+ const [state, setState] = useState({ status: "idle", results: [] });
137
+ const [attempt, setAttempt] = useState(0);
138
+ const latest = useRef(0);
139
+ useEffect(() => {
140
+ if (!field.onSearch) return;
141
+ const run = ++latest.current;
142
+ const timer = window.setTimeout(() => {
143
+ setState((s) => ({ ...s, status: "loading" }));
144
+ field.onSearch(query.trim()).then(
145
+ (results) => {
146
+ if (run === latest.current) setState({ status: "done", results });
147
+ },
148
+ () => {
149
+ if (run === latest.current) setState({ status: "error", results: [] });
150
+ }
151
+ );
152
+ }, delay);
153
+ return () => window.clearTimeout(timer);
154
+ }, [query, attempt, field, delay]);
155
+ const chosen = new Map(value.values.map((v) => [v.value, v]));
156
+ const toggle = (choice, on) => onChange({ kind: "reference", values: on ? [...value.values, choice] : value.values.filter((v) => v.value !== choice.value) });
157
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
158
+ value.values.length > 0 && /* @__PURE__ */ jsx("div", { className: "uix-chip-group", role: "group", "aria-label": fillLabel(labels.selected, { count: value.values.length }), children: value.values.map((v) => /* @__PURE__ */ jsx(Chip, { removeLabel: fillLabel(labels.remove, { label: v.label }), onRemove: () => toggle(v, false), children: v.label }, v.value)) }),
159
+ /* @__PURE__ */ jsx(Input, { type: "search", size: "sm", "aria-label": labels.search, placeholder: labels.search, value: query, onChange: (e) => setQuery(e.currentTarget.value), autoFocus: true }),
160
+ /* @__PURE__ */ jsxs("div", { className: "uix-filter-editor__list", "aria-busy": state.status === "loading" || void 0, children: [
161
+ state.status === "loading" && /* @__PURE__ */ jsx("p", { className: "uix-filter-editor__note", role: "status", children: labels.searching }),
162
+ state.status === "error" && /* @__PURE__ */ jsxs("p", { className: "uix-filter-editor__note", role: "alert", children: [
163
+ labels.searchFailed,
164
+ " ",
165
+ /* @__PURE__ */ jsx(Button, { type: "button", variant: "link", size: "sm", onClick: () => setAttempt((n) => n + 1), children: labels.retry })
166
+ ] }),
167
+ state.status === "done" && state.results.length === 0 && /* @__PURE__ */ jsx("p", { className: "uix-filter-editor__note", children: labels.noMatches }),
168
+ state.status === "done" && state.results.map((r) => /* @__PURE__ */ jsx(Checkbox, { label: r.label, checked: chosen.has(r.value), onChange: (e) => toggle(r, e.currentTarget.checked) }, r.value))
169
+ ] })
170
+ ] });
171
+ }
172
+ export {
173
+ DEFAULT_FILTER_EDITOR_LABELS,
174
+ FilterEditor
175
+ };
@@ -0,0 +1,45 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+
4
+ interface LightboxItem {
5
+ src: string;
6
+ /** Alternative text for an image; the video's name for a video. */
7
+ alt: string;
8
+ caption?: ReactNode;
9
+ kind?: 'image' | 'video';
10
+ /** Intrinsic size, so an SVG or a slow image keeps its box. */
11
+ width?: number;
12
+ height?: number;
13
+ }
14
+ interface LightboxLabels {
15
+ region: string;
16
+ previous: string;
17
+ next: string;
18
+ close: string;
19
+ /** `{index}`, `{total}`. */
20
+ counter: string;
21
+ zoomIn: string;
22
+ zoomOut: string;
23
+ }
24
+ declare const DEFAULT_LIGHTBOX_LABELS: LightboxLabels;
25
+ interface LightboxProps {
26
+ items: readonly LightboxItem[];
27
+ /** The item on show (0-based). */
28
+ index: number;
29
+ onIndexChange: (index: number) => void;
30
+ open: boolean;
31
+ onClose: () => void;
32
+ /** Wrap from the last item to the first. Default false. */
33
+ loop?: boolean;
34
+ labels?: Partial<LightboxLabels>;
35
+ className?: string;
36
+ }
37
+ /**
38
+ * A full-screen media viewer over `.uix-lightbox` (HAR-1367; TENSOR C12 `capture-viewer.tsx`,
39
+ * MOTUS C-8 programme gallery): previous/next buttons, ←/→ and Home/End, swipe on touch, an
40
+ * announced "n of total" counter, a caption, and a fit/actual-size toggle. Native `<dialog>`,
41
+ * so focus stays inside and Escape closes; focus returns to the control that opened it.
42
+ */
43
+ declare function Lightbox({ items, index, onIndexChange, open, onClose, loop, labels: labelOverrides, className }: LightboxProps): react.JSX.Element;
44
+
45
+ export { DEFAULT_LIGHTBOX_LABELS, Lightbox, type LightboxItem, type LightboxLabels, type LightboxProps };
@@ -0,0 +1,102 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useRef, useState } from "react";
4
+ import { cx } from "../cx.js";
5
+ import { fillLabel } from "../fill-label.js";
6
+ import { useDialog } from "../hooks/useDialog.js";
7
+ const DEFAULT_LIGHTBOX_LABELS = {
8
+ region: "Media viewer",
9
+ previous: "Previous",
10
+ next: "Next",
11
+ close: "Close",
12
+ counter: "{index} of {total}",
13
+ zoomIn: "Show actual size",
14
+ zoomOut: "Fit to screen"
15
+ };
16
+ const Chevron = ({ dir }) => /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: dir === "left" ? "m15 18-6-6 6-6" : "m9 18 6-6-6-6" }) });
17
+ function Lightbox({ items, index, onIndexChange, open, onClose, loop = false, labels: labelOverrides, className }) {
18
+ const labels = { ...DEFAULT_LIGHTBOX_LABELS, ...labelOverrides };
19
+ const ref = useDialog(open, onClose);
20
+ const closeRef = useRef(null);
21
+ const returnTo = useRef(null);
22
+ const swipe = useRef(null);
23
+ const [zoomed, setZoomed] = useState(false);
24
+ const total = items.length;
25
+ const current = items[Math.min(Math.max(index, 0), Math.max(total - 1, 0))];
26
+ const hasPrev = loop ? total > 1 : index > 0;
27
+ const hasNext = loop ? total > 1 : index < total - 1;
28
+ useEffect(() => {
29
+ if (open) {
30
+ returnTo.current = document.activeElement;
31
+ closeRef.current?.focus();
32
+ } else if (returnTo.current) {
33
+ returnTo.current.focus?.();
34
+ returnTo.current = null;
35
+ }
36
+ }, [open]);
37
+ useEffect(() => {
38
+ setZoomed(false);
39
+ }, [index]);
40
+ const go = (to) => {
41
+ if (total === 0) return;
42
+ const next = loop ? (to + total) % total : Math.min(Math.max(to, 0), total - 1);
43
+ if (next !== index) onIndexChange(next);
44
+ };
45
+ const onKeyDown = (event) => {
46
+ if (event.target.closest("video")) return;
47
+ if (event.key === "ArrowLeft") {
48
+ event.preventDefault();
49
+ go(index - 1);
50
+ } else if (event.key === "ArrowRight") {
51
+ event.preventDefault();
52
+ go(index + 1);
53
+ } else if (event.key === "Home") {
54
+ event.preventDefault();
55
+ go(0);
56
+ } else if (event.key === "End") {
57
+ event.preventDefault();
58
+ go(total - 1);
59
+ }
60
+ };
61
+ const onPointerDown = (event) => {
62
+ if (event.pointerType !== "mouse") swipe.current = { x: event.clientX, id: event.pointerId };
63
+ };
64
+ const onPointerUp = (event) => {
65
+ const start = swipe.current;
66
+ swipe.current = null;
67
+ if (!start || start.id !== event.pointerId || zoomed) return;
68
+ const dx = event.clientX - start.x;
69
+ if (Math.abs(dx) > 48) go(index + (dx < 0 ? 1 : -1));
70
+ };
71
+ return /* @__PURE__ */ jsxs(
72
+ "dialog",
73
+ {
74
+ ref,
75
+ className: cx("uix-lightbox", "uix-lightbox--gallery", className),
76
+ "aria-label": labels.region,
77
+ onKeyDown,
78
+ onClick: (event) => {
79
+ if (event.target === event.currentTarget) onClose();
80
+ },
81
+ children: [
82
+ current && /* @__PURE__ */ jsxs("figure", { className: "uix-lightbox__figure", children: [
83
+ /* @__PURE__ */ jsx("div", { className: "uix-lightbox__stage", "data-zoomed": zoomed || void 0, onPointerDown, onPointerUp, children: current.kind === "video" ? /* @__PURE__ */ jsx("video", { className: "uix-lightbox__media", src: current.src, controls: true, "aria-label": current.alt, width: current.width, height: current.height }) : /* @__PURE__ */ jsx("img", { className: "uix-lightbox__media", src: current.src, alt: current.alt, width: current.width, height: current.height }) }),
84
+ /* @__PURE__ */ jsxs("figcaption", { className: "uix-lightbox__bar", children: [
85
+ /* @__PURE__ */ jsx("span", { className: "uix-lightbox__counter", "aria-live": "polite", children: fillLabel(labels.counter, { index: index + 1, total }) }),
86
+ current.caption != null && /* @__PURE__ */ jsx("span", { className: "uix-lightbox__caption", children: current.caption }),
87
+ current.kind !== "video" && /* @__PURE__ */ jsx("button", { type: "button", className: "uix-lightbox__tool", "aria-pressed": zoomed, onClick: () => setZoomed((z) => !z), children: zoomed ? labels.zoomOut : labels.zoomIn })
88
+ ] })
89
+ ] }),
90
+ total > 1 && /* @__PURE__ */ jsxs(Fragment, { children: [
91
+ /* @__PURE__ */ jsx("button", { type: "button", className: "uix-lightbox__nav uix-lightbox__nav--prev", "aria-label": labels.previous, disabled: !hasPrev, onClick: () => go(index - 1), children: /* @__PURE__ */ jsx(Chevron, { dir: "left" }) }),
92
+ /* @__PURE__ */ jsx("button", { type: "button", className: "uix-lightbox__nav uix-lightbox__nav--next", "aria-label": labels.next, disabled: !hasNext, onClick: () => go(index + 1), children: /* @__PURE__ */ jsx(Chevron, { dir: "right" }) })
93
+ ] }),
94
+ /* @__PURE__ */ jsx("button", { ref: closeRef, type: "button", className: "uix-lightbox__close", "aria-label": labels.close, onClick: onClose, children: /* @__PURE__ */ jsx("svg", { viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: 1.75, strokeLinecap: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M4 4l8 8M12 4l-8 8" }) }) })
95
+ ]
96
+ }
97
+ );
98
+ }
99
+ export {
100
+ DEFAULT_LIGHTBOX_LABELS,
101
+ Lightbox
102
+ };
@@ -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 };