@tensor_1/react 2.23.0 → 2.25.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,299 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useId, useImperativeHandle, useLayoutEffect, useRef, useState } from "react";
4
+ import { cx } from "../cx.js";
5
+ import { useAnchoredPosition } from "../hooks/useAnchoredPosition.js";
6
+ import { searchSegments } from "../search-suggest-model.js";
7
+ import { Spinner } from "./Spinner.js";
8
+ const SearchIcon = () => /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
9
+ /* @__PURE__ */ jsx("circle", { cx: "11", cy: "11", r: "7" }),
10
+ /* @__PURE__ */ jsx("path", { d: "m20 20-3.5-3.5" })
11
+ ] });
12
+ const ClearIcon = () => /* @__PURE__ */ jsx("svg", { viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M4 4l8 8M12 4l-8 8" }) });
13
+ function Emphasised({ text, query }) {
14
+ return /* @__PURE__ */ jsx(Fragment, { children: searchSegments(text, query).map(
15
+ (segment, index) => segment.match ? /* @__PURE__ */ jsx("mark", { className: "uix-search-suggest__match", children: segment.text }, index) : /* @__PURE__ */ jsx("span", { children: segment.text }, index)
16
+ ) });
17
+ }
18
+ const keepFocus = (event) => event.preventDefault();
19
+ const useIsomorphicLayoutEffect = typeof document !== "undefined" ? useLayoutEffect : useEffect;
20
+ function SearchSuggest({
21
+ value,
22
+ onValueChange,
23
+ options,
24
+ onSelect,
25
+ label,
26
+ placeholder,
27
+ highlight,
28
+ heading,
29
+ headingAction,
30
+ footer,
31
+ loading = false,
32
+ loadingLabel,
33
+ empty,
34
+ error,
35
+ shortcutHint,
36
+ clearLabel = "Clear search",
37
+ status,
38
+ size = "md",
39
+ open: openProp,
40
+ strategy = "absolute",
41
+ onOpenChange,
42
+ inputRef,
43
+ autoFocus,
44
+ name,
45
+ className,
46
+ onBlur,
47
+ ...props
48
+ }) {
49
+ const id = useId();
50
+ const listboxId = `${id}-listbox`;
51
+ const headingId = `${id}-heading`;
52
+ const rootRef = useRef(null);
53
+ const fieldRef = useRef(null);
54
+ useImperativeHandle(inputRef, () => fieldRef.current, []);
55
+ const [openState, setOpenState] = useState(false);
56
+ const open = openProp ?? openState;
57
+ const setOpen = (next) => {
58
+ if (openProp === void 0) setOpenState(next);
59
+ if (next !== open) onOpenChange?.(next);
60
+ };
61
+ const [focused, setFocused] = useState(false);
62
+ const [activeState, setActive] = useState(-1);
63
+ const itemCount = options.length + (footer ? 1 : 0);
64
+ const active = activeState < itemCount ? activeState : -1;
65
+ const optionKey = options.map((option) => option.id).join("|");
66
+ useEffect(() => setActive(-1), [optionKey]);
67
+ const hasText = value.trim().length > 0;
68
+ const showEmpty = hasText && !loading && !error && options.length === 0 && empty != null;
69
+ const showLoading = loading && options.length === 0;
70
+ const visible = open && (itemCount > 0 || showLoading || showEmpty || error != null);
71
+ const optionId = (index) => `${id}-option-${index}`;
72
+ useEffect(() => {
73
+ if (!visible || active < 0) return;
74
+ const node = document.getElementById(optionId(active));
75
+ node?.scrollIntoView?.({ block: "nearest" });
76
+ }, [active, visible]);
77
+ const choose = (index) => {
78
+ const option = options[index];
79
+ if (option) onSelect(option.id, option);
80
+ else if (footer && index === options.length) footer.onSelect();
81
+ else return;
82
+ setOpen(false);
83
+ setActive(-1);
84
+ };
85
+ const handleKeyDown = (event) => {
86
+ if (event.nativeEvent.isComposing || event.keyCode === 229) return;
87
+ switch (event.key) {
88
+ case "ArrowDown":
89
+ case "ArrowUp": {
90
+ event.preventDefault();
91
+ if (!itemCount) {
92
+ setOpen(true);
93
+ return;
94
+ }
95
+ const down = event.key === "ArrowDown";
96
+ if (!visible) {
97
+ setOpen(true);
98
+ setActive(down ? 0 : itemCount - 1);
99
+ return;
100
+ }
101
+ setActive((stored) => {
102
+ const current = stored < itemCount ? stored : -1;
103
+ if (current < 0) return down ? 0 : itemCount - 1;
104
+ return (current + (down ? 1 : -1) + itemCount) % itemCount;
105
+ });
106
+ return;
107
+ }
108
+ case "Home":
109
+ case "End":
110
+ if (visible && active >= 0 && itemCount) {
111
+ event.preventDefault();
112
+ setActive(event.key === "Home" ? 0 : itemCount - 1);
113
+ }
114
+ return;
115
+ case "Enter":
116
+ if (visible && active >= 0) {
117
+ event.preventDefault();
118
+ choose(active);
119
+ return;
120
+ }
121
+ if (visible && hasText && options.length) {
122
+ event.preventDefault();
123
+ choose(0);
124
+ }
125
+ return;
126
+ case "Escape":
127
+ if (visible) {
128
+ event.preventDefault();
129
+ event.stopPropagation();
130
+ setOpen(false);
131
+ setActive(-1);
132
+ } else if (value) {
133
+ event.preventDefault();
134
+ event.stopPropagation();
135
+ onValueChange("");
136
+ }
137
+ return;
138
+ default:
139
+ }
140
+ };
141
+ const handleBlur = (event) => {
142
+ onBlur?.(event);
143
+ if (!rootRef.current?.contains(event.relatedTarget)) {
144
+ setFocused(false);
145
+ setOpen(false);
146
+ setActive(-1);
147
+ }
148
+ };
149
+ const clear = () => {
150
+ onValueChange("");
151
+ setActive(-1);
152
+ fieldRef.current?.focus();
153
+ };
154
+ const activeId = visible && active >= 0 ? optionId(active) : void 0;
155
+ const fieldBoxRef = useRef(null);
156
+ const popupRef = useRef(null);
157
+ const floating = strategy === "fixed";
158
+ useIsomorphicLayoutEffect(() => {
159
+ const popup = popupRef.current;
160
+ const box = fieldBoxRef.current;
161
+ if (!floating || !visible || !popup || !box) return;
162
+ const size2 = () => {
163
+ popup.style.width = `${box.offsetWidth}px`;
164
+ };
165
+ size2();
166
+ window.addEventListener("resize", size2);
167
+ return () => window.removeEventListener("resize", size2);
168
+ }, [floating, visible]);
169
+ useAnchoredPosition(floating ? fieldBoxRef : null, popupRef, { open: floating && visible, placement: "bottom-start", offset: 4 });
170
+ return /* @__PURE__ */ jsxs(
171
+ "div",
172
+ {
173
+ ref: rootRef,
174
+ className: cx("uix-search-suggest", size === "lg" && "uix-search-suggest--lg", className),
175
+ "data-open": visible || void 0,
176
+ onBlur: handleBlur,
177
+ ...props,
178
+ children: [
179
+ /* @__PURE__ */ jsxs("div", { ref: fieldBoxRef, className: "uix-search-suggest__field", children: [
180
+ /* @__PURE__ */ jsx("span", { className: "uix-search-suggest__lead", "aria-hidden": "true", children: /* @__PURE__ */ jsx(SearchIcon, {}) }),
181
+ /* @__PURE__ */ jsx(
182
+ "input",
183
+ {
184
+ ref: fieldRef,
185
+ className: "uix-input uix-search-suggest__input",
186
+ type: "text",
187
+ role: "combobox",
188
+ "aria-label": label,
189
+ "aria-autocomplete": "list",
190
+ "aria-expanded": visible,
191
+ "aria-controls": listboxId,
192
+ "aria-activedescendant": activeId,
193
+ "aria-busy": loading || void 0,
194
+ autoComplete: "off",
195
+ spellCheck: false,
196
+ enterKeyHint: "search",
197
+ name,
198
+ value,
199
+ placeholder,
200
+ autoFocus,
201
+ onChange: (event) => {
202
+ onValueChange(event.target.value);
203
+ setActive(-1);
204
+ setOpen(true);
205
+ },
206
+ onFocus: () => {
207
+ setFocused(true);
208
+ setOpen(true);
209
+ },
210
+ onClick: () => setOpen(true),
211
+ onKeyDown: handleKeyDown
212
+ }
213
+ ),
214
+ /* @__PURE__ */ jsxs("span", { className: "uix-search-suggest__trail", children: [
215
+ loading && /* @__PURE__ */ jsx(Spinner, { className: "uix-search-suggest__spinner", label: loadingLabel }),
216
+ !loading && value && /* @__PURE__ */ jsx("button", { type: "button", className: "uix-search-suggest__clear", "aria-label": clearLabel, onMouseDown: keepFocus, onClick: clear, children: /* @__PURE__ */ jsx(ClearIcon, {}) }),
217
+ !loading && !value && !focused && shortcutHint && /* @__PURE__ */ jsx("kbd", { className: "uix-kbd", "aria-hidden": "true", children: shortcutHint })
218
+ ] })
219
+ ] }),
220
+ /* @__PURE__ */ jsxs("div", { ref: popupRef, className: "uix-search-suggest__popup", "data-strategy": floating ? "fixed" : void 0, hidden: !visible, children: [
221
+ heading && options.length > 0 && /* @__PURE__ */ jsxs("div", { className: "uix-search-suggest__header", children: [
222
+ /* @__PURE__ */ jsx("span", { className: "uix-search-suggest__heading", id: headingId, children: heading }),
223
+ headingAction && /* @__PURE__ */ jsx("button", { type: "button", className: "uix-search-suggest__header-action", onMouseDown: keepFocus, onClick: headingAction.onSelect, children: headingAction.label })
224
+ ] }),
225
+ /* @__PURE__ */ jsxs(
226
+ "ul",
227
+ {
228
+ id: listboxId,
229
+ role: "listbox",
230
+ className: "uix-search-suggest__list",
231
+ "aria-label": heading && options.length > 0 ? void 0 : label,
232
+ "aria-labelledby": heading && options.length > 0 ? headingId : void 0,
233
+ "aria-busy": loading || void 0,
234
+ children: [
235
+ options.map((option, index) => {
236
+ const crumbs = option.meta ?? [];
237
+ const descId = option.description ? `${optionId(index)}-desc` : void 0;
238
+ return /* @__PURE__ */ jsxs(
239
+ "li",
240
+ {
241
+ id: optionId(index),
242
+ role: "option",
243
+ "aria-selected": index === active,
244
+ "aria-label": crumbs.length ? `${option.title}, ${crumbs.join(" \u203A ")}` : option.title,
245
+ "aria-describedby": descId,
246
+ className: "uix-search-suggest__option",
247
+ "data-active": index === active || void 0,
248
+ onMouseDown: keepFocus,
249
+ onMouseMove: () => {
250
+ if (active !== index) setActive(index);
251
+ },
252
+ onClick: () => choose(index),
253
+ children: [
254
+ option.icon != null && /* @__PURE__ */ jsx("span", { className: "uix-search-suggest__icon", "aria-hidden": "true", children: option.icon }),
255
+ /* @__PURE__ */ jsxs("span", { className: "uix-search-suggest__body", children: [
256
+ /* @__PURE__ */ jsx("span", { className: "uix-search-suggest__title", children: /* @__PURE__ */ jsx(Emphasised, { text: option.title, query: highlight ?? value }) }),
257
+ crumbs.length > 0 && /* @__PURE__ */ jsx("span", { className: "uix-search-suggest__meta", title: crumbs.join(" \u203A "), children: crumbs.map((crumb, crumbIndex) => /* @__PURE__ */ jsx("span", { className: "uix-search-suggest__crumb", children: crumb }, crumbIndex)) }),
258
+ option.description && /* @__PURE__ */ jsx("span", { id: descId, className: "uix-search-suggest__desc", children: /* @__PURE__ */ jsx(Emphasised, { text: option.description, query: highlight ?? value }) })
259
+ ] })
260
+ ]
261
+ },
262
+ option.id
263
+ );
264
+ }),
265
+ footer && /* @__PURE__ */ jsx(
266
+ "li",
267
+ {
268
+ id: optionId(options.length),
269
+ role: "option",
270
+ "aria-selected": active === options.length,
271
+ className: "uix-search-suggest__footer",
272
+ "data-active": active === options.length || void 0,
273
+ onMouseDown: keepFocus,
274
+ onMouseMove: () => {
275
+ if (active !== options.length) setActive(options.length);
276
+ },
277
+ onClick: () => choose(options.length),
278
+ children: footer.label
279
+ }
280
+ )
281
+ ]
282
+ }
283
+ ),
284
+ showLoading && // The field's spinner is the announced status; this row is the visual echo in the list.
285
+ /* @__PURE__ */ jsxs("div", { className: "uix-search-suggest__state", "aria-hidden": "true", children: [
286
+ /* @__PURE__ */ jsx("span", { className: "uix-spinner" }),
287
+ loadingLabel && /* @__PURE__ */ jsx("span", { children: loadingLabel })
288
+ ] }),
289
+ error != null && options.length === 0 && !loading && /* @__PURE__ */ jsx("div", { className: "uix-search-suggest__state", children: error }),
290
+ showEmpty && /* @__PURE__ */ jsx("div", { className: "uix-search-suggest__state", children: empty })
291
+ ] }),
292
+ /* @__PURE__ */ jsx("span", { className: "uix-visually-hidden", role: "status", "aria-live": "polite", children: status ?? "" })
293
+ ]
294
+ }
295
+ );
296
+ }
297
+ export {
298
+ SearchSuggest
299
+ };
@@ -6,15 +6,40 @@ interface ViewMenuColumn {
6
6
  label: ReactNode;
7
7
  visible: boolean;
8
8
  disabled?: boolean;
9
+ /** Always shown (e.g. the record id): the row has no checkbox and no Hide / Show, its name sits in the checkbox's slot. */
10
+ required?: boolean;
11
+ /** Plain-text name for the `{label}` templates in `columnLabels` when `label` is not a string. */
12
+ textLabel?: string;
13
+ }
14
+ /**
15
+ * Words for the column rows. `{label}` is replaced by the column name; `moved` also gets
16
+ * `{position}` and `{count}`. Every key has an English default.
17
+ */
18
+ interface ViewMenuColumnLabels {
19
+ /** Name of a row's ⋯ menu button and of the menu. Default `Column actions: {label}`. */
20
+ rowActions: string;
21
+ /** Name of a row's drag grip. Default `Drag to reorder: {label}`. */
22
+ reorder: string;
23
+ moveUp: string;
24
+ moveDown: string;
25
+ hide: string;
26
+ show: string;
27
+ /** Tooltip on a required column's name. Default `Always shown`. */
28
+ required: string;
29
+ /** Polite announcement after a move. Default `{label} moved to position {position} of {count}`. */
30
+ moved: string;
9
31
  }
10
32
  interface ViewMenuProps {
11
- density: string;
12
- densityLabel: ReactNode;
13
- densityOptions: readonly {
33
+ /** The density section renders when `densityOptions` and `onDensityChange` are given. */
34
+ density?: string;
35
+ densityLabel?: ReactNode;
36
+ densityOptions?: readonly {
14
37
  value: string;
15
38
  label: ReactNode;
16
39
  }[];
17
- onDensityChange: (value: string) => void;
40
+ onDensityChange?: (value: string) => void;
41
+ /** Optional title over the zebra / freeze switches. */
42
+ displayLabel?: ReactNode;
18
43
  zebra?: {
19
44
  checked: boolean;
20
45
  label: ReactNode;
@@ -28,9 +53,23 @@ interface ViewMenuProps {
28
53
  columns?: readonly ViewMenuColumn[];
29
54
  columnsLabel?: ReactNode;
30
55
  onColumnVisibilityChange?: (id: string, visible: boolean) => void;
56
+ /**
57
+ * Turns on reordering: every column row gets a drag grip (pointer; ArrowUp / ArrowDown once
58
+ * focused) and a ⋯ menu with Move up · Move down · Hide / Show, which is the keyboard path.
59
+ * Receives every column id, first to last. The order is the application's to persist.
60
+ */
61
+ onReorder?: (orderedIds: string[]) => void;
62
+ columnLabels?: Partial<ViewMenuColumnLabels>;
63
+ /** Pinned under the columns, e.g. a Reset sort `.uix-menu__item` button. */
64
+ footer?: ReactNode;
65
+ className?: string;
31
66
  }
32
- /** Controlled table presentation menu; wrap it with Popover when trigger behavior is needed. */
33
- declare function ViewMenu({ density, densityLabel, densityOptions, onDensityChange, zebra, freeze, columns, columnsLabel, onColumnVisibilityChange }: ViewMenuProps): react.JSX.Element;
67
+ /**
68
+ * Controlled table presentation panel with its own surface: density, zebra / freeze and a column
69
+ * list under sticky section titles. A column row is grip · checkbox · name · ⋯ (grip and ⋯ with
70
+ * `onReorder`). Usable inside a Popover or any overlay shell, including one that adds no chrome.
71
+ */
72
+ declare function ViewMenu({ density, densityLabel, densityOptions, onDensityChange, displayLabel, zebra, freeze, columns, columnsLabel, onColumnVisibilityChange, onReorder, columnLabels, footer, className, }: ViewMenuProps): react.JSX.Element;
34
73
  interface FilterOption {
35
74
  value: string;
36
75
  label: string;
@@ -102,4 +141,4 @@ type SavedViewMenuProps = SavedViewMenuBaseProps & ({
102
141
  */
103
142
  declare function SavedViewMenu({ items, sections, onSelect, onPinChange, pinLabel, unpinLabel, onReorder, reorderLabel, emptyLabel, actions, footer }: SavedViewMenuProps): react.JSX.Element;
104
143
 
105
- export { type FilterOption, FilterPopover, type FilterPopoverProps, type SavedViewItem, SavedViewMenu, type SavedViewMenuProps, type SavedViewSection, ViewMenu, type ViewMenuColumn, type ViewMenuProps };
144
+ export { type FilterOption, FilterPopover, type FilterPopoverProps, type SavedViewItem, SavedViewMenu, type SavedViewMenuProps, type SavedViewSection, ViewMenu, type ViewMenuColumn, type ViewMenuColumnLabels, type ViewMenuProps };