@tensor_1/react 2.30.0 → 2.32.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 (122) hide show
  1. package/README.md +430 -0
  2. package/dist/attachment-labels.d.ts +16 -0
  3. package/dist/attachment-labels.js +9 -0
  4. package/dist/calendar-model.d.ts +141 -1
  5. package/dist/calendar-model.js +200 -11
  6. package/dist/chart-preset.cjs +127 -7
  7. package/dist/chart-preset.d.cts +2 -2
  8. package/dist/chart-preset.d.ts +1 -0
  9. package/dist/chart-preset.js +6 -1
  10. package/dist/chart-theme-Cl0IrkKQ.d.cts +112 -0
  11. package/dist/chart-theme.d.ts +51 -0
  12. package/dist/chart-theme.js +99 -0
  13. package/dist/chart.cjs +127 -7
  14. package/dist/chart.d.cts +2 -2
  15. package/dist/chart.d.ts +1 -0
  16. package/dist/chart.js +6 -2
  17. package/dist/components/Alert.d.ts +17 -1
  18. package/dist/components/Alert.js +9 -4
  19. package/dist/components/AlertDismiss.d.ts +14 -0
  20. package/dist/components/AlertDismiss.js +11 -0
  21. package/dist/components/Attachment.d.ts +58 -0
  22. package/dist/components/Attachment.js +69 -0
  23. package/dist/components/AttachmentParts.d.ts +24 -0
  24. package/dist/components/AttachmentParts.js +20 -0
  25. package/dist/components/Chart.d.ts +1 -0
  26. package/dist/components/Chart.js +6 -1
  27. package/dist/components/ChartCore.d.ts +6 -1
  28. package/dist/components/ChartCore.js +23 -5
  29. package/dist/components/Chip.d.ts +17 -2
  30. package/dist/components/Chip.js +35 -11
  31. package/dist/components/ChipRemove.d.ts +17 -0
  32. package/dist/components/ChipRemove.js +13 -0
  33. package/dist/components/CollapsibleParts.d.ts +29 -0
  34. package/dist/components/CollapsibleParts.js +20 -0
  35. package/dist/components/CollapsibleSection.d.ts +30 -3
  36. package/dist/components/CollapsibleSection.js +7 -11
  37. package/dist/components/CollapsibleSectionState.d.ts +1 -1
  38. package/dist/components/CollapsibleSectionState.js +54 -27
  39. package/dist/components/DashboardGrid.d.ts +60 -0
  40. package/dist/components/DashboardGrid.js +40 -0
  41. package/dist/components/DateRangePicker.d.ts +7 -1
  42. package/dist/components/DateRangePicker.js +5 -1
  43. package/dist/components/DescriptionList.d.ts +12 -1
  44. package/dist/components/DescriptionList.js +5 -5
  45. package/dist/components/Dialogs.d.ts +24 -3
  46. package/dist/components/Dialogs.js +74 -16
  47. package/dist/components/EntityPicker.d.ts +56 -0
  48. package/dist/components/EntityPicker.js +168 -0
  49. package/dist/components/FileIcon.d.ts +9 -0
  50. package/dist/components/FileIcon.js +17 -0
  51. package/dist/components/FileUpload.d.ts +95 -0
  52. package/dist/components/FileUpload.js +145 -0
  53. package/dist/components/FilterEditor.d.ts +55 -0
  54. package/dist/components/FilterEditor.js +186 -0
  55. package/dist/components/Heartbeat.d.ts +31 -0
  56. package/dist/components/Heartbeat.js +28 -0
  57. package/dist/components/Lightbox.d.ts +45 -0
  58. package/dist/components/Lightbox.js +102 -0
  59. package/dist/components/Menu.d.ts +53 -5
  60. package/dist/components/Menu.js +104 -4
  61. package/dist/components/Meter.d.ts +7 -3
  62. package/dist/components/Meter.js +2 -1
  63. package/dist/components/Popover.d.ts +21 -2
  64. package/dist/components/Popover.js +94 -17
  65. package/dist/components/Prose.d.ts +6 -2
  66. package/dist/components/Prose.js +1 -1
  67. package/dist/components/RuleBuilder.d.ts +52 -4
  68. package/dist/components/RuleBuilder.js +109 -24
  69. package/dist/components/SchedulingCalendar.d.ts +142 -11
  70. package/dist/components/SchedulingCalendar.js +234 -57
  71. package/dist/components/SchedulingTimeline.d.ts +1 -0
  72. package/dist/components/SearchSuggest.d.ts +6 -2
  73. package/dist/components/SearchSuggest.js +4 -0
  74. package/dist/components/Segmented.d.ts +17 -3
  75. package/dist/components/Segmented.js +59 -5
  76. package/dist/components/Select.d.ts +66 -2
  77. package/dist/components/Select.js +800 -10
  78. package/dist/components/Spinner.d.ts +2 -1
  79. package/dist/components/Spinner.js +1 -1
  80. package/dist/components/StatusPill.d.ts +8 -2
  81. package/dist/components/StatusPill.js +2 -2
  82. package/dist/components/Steps.d.ts +28 -4
  83. package/dist/components/Steps.js +46 -16
  84. package/dist/components/TableControls.js +4 -3
  85. package/dist/components/Tabs.d.ts +7 -2
  86. package/dist/components/Tabs.js +14 -1
  87. package/dist/components/TextDiff.d.ts +49 -0
  88. package/dist/components/TextDiff.js +124 -0
  89. package/dist/{emoji-image-6mPZ15Zy.d.cts → emoji-image-DjCKpdwE.d.cts} +1 -1
  90. package/dist/emoji.cjs +169 -27
  91. package/dist/emoji.d.cts +3 -3
  92. package/dist/file-model.d.ts +56 -0
  93. package/dist/file-model.js +52 -0
  94. package/dist/filter-model.d.ts +112 -0
  95. package/dist/filter-model.js +103 -0
  96. package/dist/hooks/useAnchoredPosition.d.ts +24 -1
  97. package/dist/hooks/useAnchoredPosition.js +58 -8
  98. package/dist/hooks/useVirtualRows.d.ts +17 -2
  99. package/dist/hooks/useVirtualRows.js +6 -5
  100. package/dist/icons/Icon.d.ts +14 -14
  101. package/dist/index.cjs +4938 -1631
  102. package/dist/index.d.cts +1622 -178
  103. package/dist/index.d.ts +20 -6
  104. package/dist/index.js +88 -4
  105. package/dist/labels-context.d.ts +28 -0
  106. package/dist/{overlay-position-Cjv5E7mn.d.cts → overlay-position-2Afke51l.d.cts} +20 -0
  107. package/dist/overlay-position.d.ts +20 -0
  108. package/dist/overlay-position.js +17 -2
  109. package/dist/rich-text.cjs +169 -27
  110. package/dist/rich-text.d.cts +3 -3
  111. package/dist/rule-builder-model.d.ts +34 -2
  112. package/dist/rule-builder-model.js +48 -12
  113. package/dist/scheduling-calendar-model.d.ts +66 -0
  114. package/dist/scheduling-calendar-model.js +82 -0
  115. package/dist/select-model.d.ts +80 -0
  116. package/dist/select-model.js +109 -0
  117. package/dist/table-engine.d.ts +7 -0
  118. package/dist/table-engine.js +2 -1
  119. package/dist/text-diff-model.d.ts +71 -0
  120. package/dist/text-diff-model.js +261 -0
  121. package/package.json +1 -1
  122. package/dist/ChartCore-DzWe10bN.d.cts +0 -59
@@ -0,0 +1,145 @@
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 { useUixLabels } from "../labels-context.js";
8
+ import { Button } from "./Button.js";
9
+ import { Input } from "./Input.js";
10
+ import { Progress } from "./Progress.js";
11
+ import { FileIcon } from "./FileIcon.js";
12
+ const DEFAULT_FILE_UPLOAD_LABELS = {
13
+ title: "Drop files here",
14
+ choose: "Choose files",
15
+ drop: "Release to add the files",
16
+ list: "Files",
17
+ remove: "Remove {name}",
18
+ retry: "Retry {name}",
19
+ retryShort: "Retry",
20
+ queued: "Waiting",
21
+ uploading: "Uploading, {progress}%",
22
+ done: "Uploaded",
23
+ failed: "Upload failed",
24
+ alt: "Description of {name} for screen readers",
25
+ added: "{count} added",
26
+ wrongType: "{name} is not an accepted file type ({accept}).",
27
+ tooLarge: "{name} is larger than {max}.",
28
+ tooMany: "{name} was not added: at most {max} files."
29
+ };
30
+ function FileUpload({
31
+ items,
32
+ onFilesAdded,
33
+ onRemove,
34
+ onRetry,
35
+ onAltChange,
36
+ accept,
37
+ maxSize,
38
+ maxFiles,
39
+ multiple = true,
40
+ capture,
41
+ disabled,
42
+ hint,
43
+ locale,
44
+ sizeBase,
45
+ formatSize,
46
+ labels: labelOverrides,
47
+ className
48
+ }) {
49
+ const labels = { ...DEFAULT_FILE_UPLOAD_LABELS, ...useUixLabels().fileUpload, ...labelOverrides };
50
+ const sizeText = (bytes) => formatSize ? formatSize(bytes, locale) : formatFileSize(bytes, locale, { base: sizeBase });
51
+ const id = useId();
52
+ const inputRef = useRef(null);
53
+ const [over, setOver] = useState(false);
54
+ const [messages, setMessages] = useState([]);
55
+ const [announce, setAnnounce] = useState("");
56
+ const take = (list) => {
57
+ if (!list || disabled) return;
58
+ const files = Array.from(list);
59
+ const { accepted, rejected } = partitionFiles(files, { accept, maxSize, maxFiles, existing: items.length });
60
+ const max = maxSize !== void 0 ? sizeText(maxSize) : "";
61
+ setMessages(rejected.map(({ file, reason }) => fillLabel(
62
+ reason === "type" ? labels.wrongType : reason === "size" ? labels.tooLarge : labels.tooMany,
63
+ { name: file.name, accept: accept ?? "", max: reason === "count" ? String(maxFiles) : max }
64
+ )));
65
+ setAnnounce(accepted.length ? fillLabel(labels.added, { count: accepted.length }) : "");
66
+ onFilesAdded(accepted, rejected);
67
+ };
68
+ const onDrop = (event) => {
69
+ event.preventDefault();
70
+ setOver(false);
71
+ take(event.dataTransfer?.files ?? null);
72
+ };
73
+ return /* @__PURE__ */ jsxs("div", { className: cx("uix-file-upload", className), "data-disabled": disabled || void 0, children: [
74
+ /* @__PURE__ */ jsxs(
75
+ "div",
76
+ {
77
+ className: "uix-dropzone",
78
+ "data-dragover": over || void 0,
79
+ onDragOver: (event) => {
80
+ if (disabled) return;
81
+ event.preventDefault();
82
+ setOver(true);
83
+ },
84
+ onDragLeave: () => setOver(false),
85
+ onDrop,
86
+ onClick: (event) => {
87
+ if (event.target === event.currentTarget) inputRef.current?.click();
88
+ },
89
+ children: [
90
+ /* @__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" }) }),
91
+ /* @__PURE__ */ jsx("strong", { "aria-hidden": over || void 0, children: over ? labels.drop : labels.title }),
92
+ hint != null && /* @__PURE__ */ jsx("span", { className: "uix-file-upload__hint", id: `${id}-hint`, children: hint }),
93
+ /* @__PURE__ */ jsx(Button, { type: "button", size: "sm", disabled, "aria-describedby": hint != null ? `${id}-hint` : void 0, onClick: () => inputRef.current?.click(), children: labels.choose }),
94
+ /* @__PURE__ */ jsx(
95
+ "input",
96
+ {
97
+ ref: inputRef,
98
+ type: "file",
99
+ className: "uix-visually-hidden",
100
+ tabIndex: -1,
101
+ "aria-hidden": "true",
102
+ accept,
103
+ multiple,
104
+ capture,
105
+ disabled,
106
+ onChange: (event) => {
107
+ take(event.currentTarget.files);
108
+ event.currentTarget.value = "";
109
+ }
110
+ }
111
+ )
112
+ ]
113
+ }
114
+ ),
115
+ messages.length > 0 && // The alert wraps the list: role="alert" on the <ul> itself replaced its list role and
116
+ // left the <li>s outside any list (axe `listitem`).
117
+ /* @__PURE__ */ jsx("div", { role: "alert", children: /* @__PURE__ */ jsx("ul", { className: "uix-file-upload__errors", children: messages.map((m, i) => /* @__PURE__ */ jsx("li", { children: m }, i)) }) }),
118
+ /* @__PURE__ */ jsx("span", { className: "uix-visually-hidden", role: "status", "aria-live": "polite", children: announce }),
119
+ items.length > 0 && /* @__PURE__ */ jsx("ul", { className: "uix-filelist uix-file-upload__list", "aria-label": labels.list, children: items.map((item) => {
120
+ const statusText = item.status === "uploading" ? fillLabel(labels.uploading, { progress: item.progress ?? 0 }) : item.status === "done" ? labels.done : item.status === "error" ? labels.failed : labels.queued;
121
+ const image = !!item.previewUrl;
122
+ return /* @__PURE__ */ jsxs("li", { className: "uix-filelist__item uix-file-upload__item", "data-status": item.status, children: [
123
+ 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 }) }),
124
+ /* @__PURE__ */ jsxs("div", { className: "uix-file-upload__body", children: [
125
+ /* @__PURE__ */ jsx("span", { className: "uix-file-upload__name", children: item.name }),
126
+ /* @__PURE__ */ jsxs("span", { className: "uix-file-upload__meta", children: [
127
+ /* @__PURE__ */ jsx("span", { children: sizeText(item.size) }),
128
+ item.status !== "uploading" && /* @__PURE__ */ jsx("span", { className: "uix-file-upload__status", children: statusText })
129
+ ] }),
130
+ item.status === "uploading" && /* @__PURE__ */ jsx(Progress, { value: item.progress, indeterminate: item.progress === void 0, max: 100, label: statusText }),
131
+ item.status === "error" && item.error != null && /* @__PURE__ */ jsx("span", { className: "uix-file-upload__error", children: item.error }),
132
+ 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) })
133
+ ] }),
134
+ /* @__PURE__ */ jsxs("div", { className: "uix-file-upload__actions", children: [
135
+ 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 }),
136
+ 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" }) }) })
137
+ ] })
138
+ ] }, item.id);
139
+ }) })
140
+ ] });
141
+ }
142
+ export {
143
+ DEFAULT_FILE_UPLOAD_LABELS,
144
+ FileUpload
145
+ };
@@ -0,0 +1,55 @@
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
+ /** Names the row of `field.presets` chips. Optional so existing label objects still type-check; default "Presets". */
28
+ presets?: string;
29
+ operators: Record<TextFilterOperator | NumberFilterOperator, string>;
30
+ }
31
+ declare const DEFAULT_FILTER_EDITOR_LABELS: FilterEditorLabels;
32
+ interface FilterEditorProps {
33
+ field: FilterField;
34
+ /** Undefined means "not filtered yet". */
35
+ value: FilterValue | undefined;
36
+ onValueChange: (value: FilterValue) => void;
37
+ onApply: () => void;
38
+ onClear: () => void;
39
+ labels?: Partial<FilterEditorLabels>;
40
+ /** Show an option filter above an enum list once it has more options than this. Default 8. */
41
+ searchThreshold?: number;
42
+ /** Delay before a reference search runs, in ms. Default 250. */
43
+ searchDelay?: number;
44
+ }
45
+ /**
46
+ * Editor content for one typed column filter (HAR-1365; TENSOR C6, MOTUS C-1): an enum
47
+ * multi-select, text or number with a condition, a date range, yes/no, or records found by
48
+ * an async search. The product owns the popover trigger and the query; `summarizeFilter`
49
+ * turns the applied value into the chip text. `FilterPopover` stays for the one-string case.
50
+ * An enum field's `presets` (HAR-1505) render as toggle chips above its options. Words come
51
+ * from `labels`, or from `UixLabelsProvider`'s `filterEditor` entry.
52
+ */
53
+ declare function FilterEditor({ field, value: valueProp, onValueChange, onApply, onClear, labels: labelOverrides, searchThreshold, searchDelay }: FilterEditorProps): react.JSX.Element;
54
+
55
+ export { DEFAULT_FILTER_EDITOR_LABELS, FilterEditor, type FilterEditorLabels, type FilterEditorProps };
@@ -0,0 +1,186 @@
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 { useUixLabels } from "../labels-context.js";
12
+ import { foldForSearch } from "../search-suggest-model.js";
13
+ import { NUMBER_FILTER_OPERATORS, TEXT_FILTER_OPERATORS, emptyFilterValue, matchFilterPreset } from "../filter-model.js";
14
+ const DEFAULT_FILTER_EDITOR_LABELS = {
15
+ apply: "Apply",
16
+ clear: "Clear",
17
+ operator: "Condition",
18
+ value: "Value",
19
+ from: "From",
20
+ to: "To",
21
+ and: "and",
22
+ any: "Any",
23
+ yes: "Yes",
24
+ no: "No",
25
+ search: "Search",
26
+ searching: "Searching\u2026",
27
+ noMatches: "No matches.",
28
+ searchFailed: "Search failed.",
29
+ retry: "Try again",
30
+ selectAll: "Select all",
31
+ selectNone: "Clear selection",
32
+ selected: "{count} selected",
33
+ remove: "Remove {label}",
34
+ presets: "Presets",
35
+ operators: {
36
+ contains: "contains",
37
+ "not-contains": "does not contain",
38
+ is: "is",
39
+ "is-not": "is not",
40
+ "starts-with": "starts with",
41
+ eq: "equals",
42
+ neq: "does not equal",
43
+ lt: "less than",
44
+ lte: "at most",
45
+ gt: "more than",
46
+ gte: "at least",
47
+ between: "between"
48
+ }
49
+ };
50
+ function FilterEditor({ field, value: valueProp, onValueChange, onApply, onClear, labels: labelOverrides, searchThreshold = 8, searchDelay = 250 }) {
51
+ const provided = useUixLabels().filterEditor;
52
+ const labels = {
53
+ ...DEFAULT_FILTER_EDITOR_LABELS,
54
+ ...provided,
55
+ ...labelOverrides,
56
+ operators: { ...DEFAULT_FILTER_EDITOR_LABELS.operators, ...provided?.operators, ...labelOverrides?.operators }
57
+ };
58
+ const id = useId();
59
+ const value = valueProp?.kind === field.kind ? valueProp : emptyFilterValue(field.kind);
60
+ let body = null;
61
+ switch (value.kind) {
62
+ case "enum":
63
+ body = /* @__PURE__ */ jsx(EnumBody, { field, value, onChange: onValueChange, labels, threshold: searchThreshold });
64
+ break;
65
+ case "reference":
66
+ body = /* @__PURE__ */ jsx(ReferenceBody, { field, value, onChange: onValueChange, labels, delay: searchDelay });
67
+ break;
68
+ case "text": {
69
+ const ops = field.operators ?? TEXT_FILTER_OPERATORS;
70
+ body = /* @__PURE__ */ jsxs("div", { className: "uix-filter-editor__row", children: [
71
+ 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)) }),
72
+ /* @__PURE__ */ jsx(Input, { "aria-label": labels.value, value: value.text, onChange: (e) => onValueChange({ ...value, text: e.currentTarget.value }), onKeyDown: (e) => {
73
+ if (e.key === "Enter") onApply();
74
+ }, autoFocus: true })
75
+ ] });
76
+ break;
77
+ }
78
+ case "number": {
79
+ const ops = field.operators ?? NUMBER_FILTER_OPERATORS;
80
+ const toNumber = (raw) => raw.trim() === "" ? void 0 : Number(raw);
81
+ body = /* @__PURE__ */ jsxs("div", { className: "uix-filter-editor__row", children: [
82
+ 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)) }),
83
+ /* @__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 }),
84
+ value.operator === "between" && /* @__PURE__ */ jsxs(Fragment, { children: [
85
+ /* @__PURE__ */ jsx("span", { className: "uix-filter-editor__and", children: labels.and }),
86
+ /* @__PURE__ */ jsx(Input, { type: "number", inputMode: "decimal", "aria-label": labels.to, value: value.to ?? "", onChange: (e) => onValueChange({ ...value, to: toNumber(e.currentTarget.value) }) })
87
+ ] }),
88
+ field.unit && /* @__PURE__ */ jsx("span", { className: "uix-filter-editor__unit", children: field.unit })
89
+ ] });
90
+ break;
91
+ }
92
+ case "date-range":
93
+ body = /* @__PURE__ */ jsxs("div", { className: "uix-filter-editor__row", children: [
94
+ /* @__PURE__ */ jsxs("label", { className: "uix-filter-editor__date", children: [
95
+ /* @__PURE__ */ jsx("span", { children: labels.from }),
96
+ /* @__PURE__ */ jsx(Input, { type: "date", value: value.from ?? "", max: value.to, onChange: (e) => onValueChange({ ...value, from: e.currentTarget.value || void 0 }) })
97
+ ] }),
98
+ /* @__PURE__ */ jsxs("label", { className: "uix-filter-editor__date", children: [
99
+ /* @__PURE__ */ jsx("span", { children: labels.to }),
100
+ /* @__PURE__ */ jsx(Input, { type: "date", value: value.to ?? "", min: value.from, onChange: (e) => onValueChange({ ...value, to: e.currentTarget.value || void 0 }) })
101
+ ] })
102
+ ] });
103
+ break;
104
+ case "boolean": {
105
+ const pick = (v) => onValueChange({ kind: "boolean", value: v });
106
+ body = /* @__PURE__ */ jsxs(RadioGroup, { className: "uix-filter-editor__choices", children: [
107
+ /* @__PURE__ */ jsx(Radio, { name: `${id}-bool`, label: labels.any, checked: value.value === void 0, onChange: () => pick(void 0) }),
108
+ /* @__PURE__ */ jsx(Radio, { name: `${id}-bool`, label: field.trueLabel ?? labels.yes, checked: value.value === true, onChange: () => pick(true) }),
109
+ /* @__PURE__ */ jsx(Radio, { name: `${id}-bool`, label: field.falseLabel ?? labels.no, checked: value.value === false, onChange: () => pick(false) })
110
+ ] });
111
+ break;
112
+ }
113
+ }
114
+ return /* @__PURE__ */ jsxs("div", { className: "uix-filter-popover uix-filter-editor", role: "group", "aria-labelledby": `${id}-label`, "data-kind": field.kind, children: [
115
+ /* @__PURE__ */ jsx("div", { id: `${id}-label`, className: "uix-filter-editor__label", children: field.label }),
116
+ body,
117
+ /* @__PURE__ */ jsxs("div", { className: "uix-filter-popover__actions", children: [
118
+ /* @__PURE__ */ jsx(Button, { type: "button", size: "sm", onClick: onClear, children: labels.clear }),
119
+ /* @__PURE__ */ jsx(Button, { type: "button", size: "sm", variant: "primary", onClick: onApply, children: labels.apply })
120
+ ] })
121
+ ] });
122
+ }
123
+ function EnumBody({ field, value, onChange, labels, threshold }) {
124
+ const [query, setQuery] = useState("");
125
+ const options = field.options ?? [];
126
+ const shown = useMemo(() => {
127
+ const q = foldForSearch(query.trim()).text;
128
+ return q ? options.filter((o) => foldForSearch(o.label).text.includes(q)) : options;
129
+ }, [options, query]);
130
+ const selected = new Set(value.values);
131
+ const toggle = (v, on) => onChange({ kind: "enum", values: on ? [...value.values, v] : value.values.filter((x) => x !== v) });
132
+ const presets = field.presets ?? [];
133
+ const active = matchFilterPreset(field, value);
134
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
135
+ presets.length > 0 && /* @__PURE__ */ jsx("div", { className: "uix-chip-group uix-filter-editor__presets", role: "group", "aria-label": labels.presets ?? DEFAULT_FILTER_EDITOR_LABELS.presets, children: presets.map((p) => /* @__PURE__ */ jsx(Chip, { pressed: p === active, onPressedChange: () => onChange({ kind: "enum", values: [...p.values] }), children: p.label }, p.id)) }),
136
+ 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 }),
137
+ /* @__PURE__ */ jsxs("div", { className: "uix-filter-editor__bulk", children: [
138
+ /* @__PURE__ */ jsx("span", { className: "uix-filter-editor__count", "aria-live": "polite", children: fillLabel(labels.selected, { count: value.values.length }) }),
139
+ /* @__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 }),
140
+ /* @__PURE__ */ jsx(Button, { type: "button", variant: "link", size: "sm", onClick: () => onChange({ kind: "enum", values: [] }), children: labels.selectNone })
141
+ ] }),
142
+ /* @__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)) })
143
+ ] });
144
+ }
145
+ function ReferenceBody({ field, value, onChange, labels, delay }) {
146
+ const [query, setQuery] = useState("");
147
+ const [state, setState] = useState({ status: "idle", results: [] });
148
+ const [attempt, setAttempt] = useState(0);
149
+ const latest = useRef(0);
150
+ useEffect(() => {
151
+ if (!field.onSearch) return;
152
+ const run = ++latest.current;
153
+ const timer = window.setTimeout(() => {
154
+ setState((s) => ({ ...s, status: "loading" }));
155
+ field.onSearch(query.trim()).then(
156
+ (results) => {
157
+ if (run === latest.current) setState({ status: "done", results });
158
+ },
159
+ () => {
160
+ if (run === latest.current) setState({ status: "error", results: [] });
161
+ }
162
+ );
163
+ }, delay);
164
+ return () => window.clearTimeout(timer);
165
+ }, [query, attempt, field, delay]);
166
+ const chosen = new Map(value.values.map((v) => [v.value, v]));
167
+ const toggle = (choice, on) => onChange({ kind: "reference", values: on ? [...value.values, choice] : value.values.filter((v) => v.value !== choice.value) });
168
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
169
+ 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)) }),
170
+ /* @__PURE__ */ jsx(Input, { type: "search", size: "sm", "aria-label": labels.search, placeholder: labels.search, value: query, onChange: (e) => setQuery(e.currentTarget.value), autoFocus: true }),
171
+ /* @__PURE__ */ jsxs("div", { className: "uix-filter-editor__list", "aria-busy": state.status === "loading" || void 0, children: [
172
+ state.status === "loading" && /* @__PURE__ */ jsx("p", { className: "uix-filter-editor__note", role: "status", children: labels.searching }),
173
+ state.status === "error" && /* @__PURE__ */ jsxs("p", { className: "uix-filter-editor__note", role: "alert", children: [
174
+ labels.searchFailed,
175
+ " ",
176
+ /* @__PURE__ */ jsx(Button, { type: "button", variant: "link", size: "sm", onClick: () => setAttempt((n) => n + 1), children: labels.retry })
177
+ ] }),
178
+ state.status === "done" && state.results.length === 0 && /* @__PURE__ */ jsx("p", { className: "uix-filter-editor__note", children: labels.noMatches }),
179
+ 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))
180
+ ] })
181
+ ] });
182
+ }
183
+ export {
184
+ DEFAULT_FILTER_EDITOR_LABELS,
185
+ FilterEditor
186
+ };
@@ -0,0 +1,31 @@
1
+ import * as react from 'react';
2
+ import { HTMLAttributes, ReactNode } from 'react';
3
+
4
+ type HeartbeatState = 'live' | 'idle' | 'warning' | 'danger';
5
+ interface HeartbeatProps extends Omit<HTMLAttributes<HTMLSpanElement>, 'children'> {
6
+ /**
7
+ * `live` (default): a green dot with a pulsing ring. `warning` / `danger`: the same in that
8
+ * tone. `idle`: a grey dot, no ring.
9
+ */
10
+ state?: HeartbeatState;
11
+ /**
12
+ * What the dot means, for assistive technology ("Live", "Connection lost"). Without it the
13
+ * dot is decorative (`aria-hidden`): say the state in text beside it, as `LiveIndicator` does.
14
+ */
15
+ label?: string;
16
+ }
17
+ /**
18
+ * The pulsing status dot over `.uix-heartbeat` (HAR-1606), so products stop hand-writing its
19
+ * three spans. Decorative unless it has a `label`. The ring stops under
20
+ * `prefers-reduced-motion` (the kit's global motion guard); the dot and its colour stay.
21
+ */
22
+ declare function Heartbeat({ state, label, className, ...props }: HeartbeatProps): react.JSX.Element;
23
+ interface LiveIndicatorProps extends HTMLAttributes<HTMLSpanElement> {
24
+ state?: HeartbeatState;
25
+ /** The state in words: "Live", "Paused", "Reconnecting…". It is what a screen reader gets. */
26
+ children?: ReactNode;
27
+ }
28
+ /** A `Heartbeat` with its state in text beside it, over `.uix-live`. */
29
+ declare function LiveIndicator({ state, children, className, ...props }: LiveIndicatorProps): react.JSX.Element;
30
+
31
+ export { Heartbeat, type HeartbeatProps, type HeartbeatState, LiveIndicator, type LiveIndicatorProps };
@@ -0,0 +1,28 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { cx } from "../cx.js";
3
+ function Heartbeat({ state = "live", label, className, ...props }) {
4
+ return /* @__PURE__ */ jsxs(
5
+ "span",
6
+ {
7
+ className: cx("uix-heartbeat", state !== "live" && `uix-heartbeat--${state}`, className),
8
+ role: label ? "img" : void 0,
9
+ "aria-label": label,
10
+ "aria-hidden": label ? void 0 : true,
11
+ ...props,
12
+ children: [
13
+ /* @__PURE__ */ jsx("span", { className: "uix-heartbeat__ping" }),
14
+ /* @__PURE__ */ jsx("span", { className: "uix-heartbeat__dot" })
15
+ ]
16
+ }
17
+ );
18
+ }
19
+ function LiveIndicator({ state = "live", children, className, ...props }) {
20
+ return /* @__PURE__ */ jsxs("span", { className: cx("uix-live", className), ...props, children: [
21
+ /* @__PURE__ */ jsx(Heartbeat, { state }),
22
+ children
23
+ ] });
24
+ }
25
+ export {
26
+ Heartbeat,
27
+ LiveIndicator
28
+ };
@@ -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
+ };