@peers-app/peers-ui 0.28.4 → 0.28.6
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/dist/command-palette/command-palette-ui.d.ts +1 -0
- package/dist/command-palette/command-palette-ui.js +11 -8
- package/dist/components/data-grid/autocomplete.d.ts +31 -0
- package/dist/components/data-grid/autocomplete.js +199 -0
- package/dist/components/data-grid/data-grid.d.ts +56 -0
- package/dist/components/data-grid/data-grid.js +179 -0
- package/dist/components/data-grid/index.d.ts +3 -0
- package/dist/components/data-grid/index.js +7 -0
- package/dist/components/data-grid/reactive.d.ts +5 -0
- package/dist/components/data-grid/reactive.js +9 -0
- package/dist/components/group-switcher.js +14 -12
- package/dist/components/router.js +2 -9
- package/dist/components/tooltip.d.ts +1 -0
- package/dist/components/tooltip.js +2 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/operator-console/operator-console.js +7 -7
- package/dist/root-layout/layouts/full-screen-layout.js +10 -4
- package/dist/screens/console-logs/console-logs-list.d.ts +1 -0
- package/dist/screens/console-logs/console-logs-list.js +4 -3
- package/dist/screens/console-logs/mobile-log-card.d.ts +1 -0
- package/dist/screens/console-logs/mobile-log-card.js +8 -7
- package/dist/screens/console-logs/resizable-table-header.d.ts +1 -0
- package/dist/screens/console-logs/resizable-table-header.js +3 -2
- package/dist/screens/network-viewer/usage-graph.d.ts +1 -1
- package/dist/screens/network-viewer/usage-graph.js +1 -1
- package/dist/screens/search/global-search.js +5 -5
- package/dist/screens/settings/color-mode-dropdown.d.ts +5 -0
- package/dist/screens/settings/color-mode-dropdown.js +8 -2
- package/dist/screens/settings/settings-page.js +1 -8
- package/dist/screens/setup-user.d.ts +1 -0
- package/dist/screens/setup-user.js +9 -8
- package/dist/screens/welcome-modal.js +9 -9
- package/dist/tabs-layout/tabs-layout.js +25 -31
- package/dist/theme.css +126 -0
- package/package.json +4 -4
- package/dist/screens/tools/tool-test-details.d.ts +0 -3
- package/dist/screens/tools/tool-test-details.js +0 -38
- package/dist/screens/tools/tool-test-list.d.ts +0 -3
- package/dist/screens/tools/tool-test-list.js +0 -43
|
@@ -12,6 +12,7 @@ const system_apps_1 = require("../system-apps");
|
|
|
12
12
|
const tabs_state_1 = require("../tabs-layout/tabs-state");
|
|
13
13
|
const routes_loader_1 = require("../ui-router/routes-loader");
|
|
14
14
|
const command_palette_1 = require("./command-palette");
|
|
15
|
+
/** Searchable overlay for launching apps, running commands, and starting threads. */
|
|
15
16
|
function CommandPaletteOverlay() {
|
|
16
17
|
const [isOpen] = (0, hooks_1.useObservable)(command_palette_1.isCommandPaletteOpen);
|
|
17
18
|
const [_persistedQuery] = (0, hooks_1.useObservable)(command_palette_1.commandSearchQuery);
|
|
@@ -198,15 +199,15 @@ function CommandPaletteOverlay() {
|
|
|
198
199
|
if (e.target === e.currentTarget) {
|
|
199
200
|
(0, command_palette_1.closeCommandPalette)();
|
|
200
201
|
}
|
|
201
|
-
}, children: (0, jsx_runtime_1.jsxs)("div", { className: `${isDark ? "bg-dark text-light" : "
|
|
202
|
+
}, children: (0, jsx_runtime_1.jsxs)("div", { className: `${isDark ? "bg-dark text-light" : "peers-surface text-body"}`, style: {
|
|
202
203
|
width: "90%",
|
|
203
204
|
maxWidth: "600px",
|
|
204
205
|
borderRadius: "12px",
|
|
205
206
|
boxShadow: isDark ? "0 20px 40px rgba(0, 0, 0, 0.5)" : "0 20px 40px rgba(0, 0, 0, 0.1)",
|
|
206
|
-
border: isDark ? "1px solid #495057" : "1px solid
|
|
207
|
+
border: isDark ? "1px solid #495057" : "1px solid var(--peers-surface-border)",
|
|
207
208
|
overflow: "hidden",
|
|
208
209
|
}, children: [(0, jsx_runtime_1.jsx)("div", { className: "p-3 border-bottom", style: {
|
|
209
|
-
borderBottomColor: isDark ? "#495057" : "
|
|
210
|
+
borderBottomColor: isDark ? "#495057" : "var(--peers-surface-border)",
|
|
210
211
|
}, children: (0, jsx_runtime_1.jsxs)("div", { className: "position-relative", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi-search position-absolute", style: {
|
|
211
212
|
left: "12px",
|
|
212
213
|
top: "50%",
|
|
@@ -230,7 +231,7 @@ function CommandPaletteOverlay() {
|
|
|
230
231
|
border: "none",
|
|
231
232
|
outline: "none",
|
|
232
233
|
boxShadow: "none",
|
|
233
|
-
backgroundColor: isDark ? "#343a40" : "
|
|
234
|
+
backgroundColor: isDark ? "#343a40" : "var(--peers-surface-bg)",
|
|
234
235
|
} })] }) }), (0, jsx_runtime_1.jsx)("div", { style: {
|
|
235
236
|
maxHeight: "400px",
|
|
236
237
|
overflowY: "auto",
|
|
@@ -275,11 +276,13 @@ function CommandPaletteOverlay() {
|
|
|
275
276
|
fontSize: "16px",
|
|
276
277
|
color: isSelected ? (isDark ? "#ffffff" : "#0d6efd") : "#6c757d",
|
|
277
278
|
minWidth: "16px",
|
|
278
|
-
} })), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("div", { className: "fw-medium", children: command.label }), command.description && ((0, jsx_runtime_1.jsx)("div", { className: "small text-muted", style: { fontSize: "12px" }, children: command.description }))] })] }), command.shortcut && ((0, jsx_runtime_1.jsx)("div", { className: `small px-2 py-1 rounded ${isDark ? "bg-secondary bg-opacity-50" : "bg-
|
|
279
|
+
} })), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("div", { className: "fw-medium", children: command.label }), command.description && ((0, jsx_runtime_1.jsx)("div", { className: "small text-muted", style: { fontSize: "12px" }, children: command.description }))] })] }), command.shortcut && ((0, jsx_runtime_1.jsx)("div", { className: `small px-2 py-1 rounded ${isDark ? "bg-secondary bg-opacity-50" : "bg-body"}`, style: {
|
|
279
280
|
fontSize: "11px",
|
|
280
281
|
fontFamily: "monospace",
|
|
281
282
|
color: isDark ? "#adb5bd" : "#6c757d",
|
|
282
|
-
border: isDark
|
|
283
|
+
border: isDark
|
|
284
|
+
? "1px solid #495057"
|
|
285
|
+
: "1px solid var(--peers-surface-border)",
|
|
283
286
|
}, children: command.shortcut }))] }, command.id));
|
|
284
287
|
})] }, category))), searchQuery.trim() && ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("div", { className: "px-3 py-1 small text-muted fw-bold text-uppercase", style: { fontSize: "11px", letterSpacing: "0.5px" }, children: "Actions" }), (0, jsx_runtime_1.jsx)("div", { className: `px-3 py-2 d-flex align-items-center ${(allItems.length === 0 || selectedIndex >= allItems.length) &&
|
|
285
288
|
searchQuery.trim()
|
|
@@ -290,7 +293,7 @@ function CommandPaletteOverlay() {
|
|
|
290
293
|
cursor: "pointer",
|
|
291
294
|
transition: "background-color 0.1s ease",
|
|
292
295
|
borderTop: allItems.length > 0
|
|
293
|
-
? `1px solid ${isDark ? "#495057" : "
|
|
296
|
+
? `1px solid ${isDark ? "#495057" : "var(--peers-surface-border)"}`
|
|
294
297
|
: "none",
|
|
295
298
|
marginTop: allItems.length > 0 ? "8px" : "0",
|
|
296
299
|
paddingTop: allItems.length > 0 ? "12px" : "8px",
|
|
@@ -310,7 +313,7 @@ function CommandPaletteOverlay() {
|
|
|
310
313
|
: "#6c757d",
|
|
311
314
|
minWidth: "16px",
|
|
312
315
|
} }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsxs)("div", { className: "fw-medium", children: ["Start new thread:", " ", (0, jsx_runtime_1.jsxs)("span", { className: "text-muted", children: [searchQuery.trim().slice(0, 50), searchQuery.trim().length > 50 ? "..." : ""] })] }), (0, jsx_runtime_1.jsx)("div", { className: "small text-muted", style: { fontSize: "12px" }, children: "Press Enter to create" })] })] }) })] }))] })) }), (0, jsx_runtime_1.jsxs)("div", { className: `px-3 py-2 small text-muted border-top d-flex align-items-center justify-content-between`, style: {
|
|
313
|
-
borderTopColor: isDark ? "#495057" : "
|
|
316
|
+
borderTopColor: isDark ? "#495057" : "var(--peers-surface-border)",
|
|
314
317
|
fontSize: "11px",
|
|
315
318
|
}, children: [(0, jsx_runtime_1.jsxs)("div", { className: "d-flex align-items-center gap-3", children: [(0, jsx_runtime_1.jsxs)("span", { children: [(0, jsx_runtime_1.jsx)("kbd", { className: "small", children: "\u21B5" }), " to select"] }), (0, jsx_runtime_1.jsxs)("span", { children: [(0, jsx_runtime_1.jsx)("kbd", { className: "small", children: "\u2191\u2193" }), " to navigate"] }), (0, jsx_runtime_1.jsxs)("span", { children: [(0, jsx_runtime_1.jsx)("kbd", { className: "small", children: "esc" }), " to close"] })] }), (0, jsx_runtime_1.jsxs)("div", { children: [allItems.length, " result", allItems.length !== 1 ? "s" : "", searchQuery.trim() && allItems.length === 0 && " • Start thread"] })] })] }) }));
|
|
316
319
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type Ref } from "react";
|
|
2
|
+
import { type GridValue } from "./reactive";
|
|
3
|
+
/** Single-selection lookup, independent of any table, ORM, or contract. */
|
|
4
|
+
export interface AutocompleteProps<T> {
|
|
5
|
+
/** Current selection; observable updates are reflected without recreating the control. */
|
|
6
|
+
value: GridValue<T | null>;
|
|
7
|
+
/** Local choices in display order, optionally supplied by a Peers observable. */
|
|
8
|
+
options?: GridValue<readonly T[]>;
|
|
9
|
+
/** Remote search. Stale responses are ignored and the signal is aborted on replacement. */
|
|
10
|
+
loadOptions?: (query: string, signal: AbortSignal) => Promise<readonly T[]>;
|
|
11
|
+
getOptionKey: (option: T) => string;
|
|
12
|
+
getOptionLabel: (option: T) => string;
|
|
13
|
+
/** Explicit write boundary; a writable local observable can be passed directly. */
|
|
14
|
+
onChange: (value: T | null) => void;
|
|
15
|
+
label: string;
|
|
16
|
+
placeholder?: string;
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
required?: boolean;
|
|
19
|
+
clearable?: boolean;
|
|
20
|
+
minLength?: number;
|
|
21
|
+
debounceMs?: number;
|
|
22
|
+
maxResults?: number;
|
|
23
|
+
inputRef?: Ref<HTMLInputElement>;
|
|
24
|
+
invalid?: boolean;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Searchable, clearable Bootstrap combobox inspired by JobTrak's Typeahead/FK controls.
|
|
28
|
+
* Arrow keys and Enter belong to the popup while open; Tab selects a highlighted result
|
|
29
|
+
* and continues naturally. The popup is portaled so a scrolling grid cannot clip it.
|
|
30
|
+
*/
|
|
31
|
+
export declare function Autocomplete<T>(props: AutocompleteProps<T>): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Autocomplete = Autocomplete;
|
|
4
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
+
const react_1 = require("react");
|
|
6
|
+
const react_dom_1 = require("react-dom");
|
|
7
|
+
const reactive_1 = require("./reactive");
|
|
8
|
+
const emptyOptions = [];
|
|
9
|
+
// Bootstrap button states remain available without drawing separate button boxes.
|
|
10
|
+
const indicatorStyle = {
|
|
11
|
+
width: "2rem",
|
|
12
|
+
height: "100%",
|
|
13
|
+
padding: 0,
|
|
14
|
+
border: 0,
|
|
15
|
+
"--bs-btn-color": "var(--bs-tertiary-color)",
|
|
16
|
+
"--bs-btn-hover-color": "var(--bs-secondary-color)",
|
|
17
|
+
"--bs-btn-active-color": "var(--bs-body-color)",
|
|
18
|
+
"--bs-btn-disabled-color": "var(--bs-tertiary-color)",
|
|
19
|
+
"--bs-btn-bg": "transparent",
|
|
20
|
+
"--bs-btn-hover-bg": "transparent",
|
|
21
|
+
"--bs-btn-active-bg": "transparent",
|
|
22
|
+
"--bs-btn-disabled-bg": "transparent",
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Searchable, clearable Bootstrap combobox inspired by JobTrak's Typeahead/FK controls.
|
|
26
|
+
* Arrow keys and Enter belong to the popup while open; Tab selects a highlighted result
|
|
27
|
+
* and continues naturally. The popup is portaled so a scrolling grid cannot clip it.
|
|
28
|
+
*/
|
|
29
|
+
function Autocomplete(props) {
|
|
30
|
+
const value = (0, reactive_1.useGridValue)(props.value);
|
|
31
|
+
const options = (0, reactive_1.useGridValue)(props.options ?? emptyOptions);
|
|
32
|
+
const id = (0, react_1.useId)();
|
|
33
|
+
const input = (0, react_1.useRef)(null);
|
|
34
|
+
const [open, setOpen] = (0, react_1.useState)(false);
|
|
35
|
+
const [query, setQuery] = (0, react_1.useState)(null);
|
|
36
|
+
const [active, setActive] = (0, react_1.useState)(-1);
|
|
37
|
+
const [remote, setRemote] = (0, react_1.useState)([]);
|
|
38
|
+
const [loading, setLoading] = (0, react_1.useState)(false);
|
|
39
|
+
const [error, setError] = (0, react_1.useState)("");
|
|
40
|
+
const [position, setPosition] = (0, react_1.useState)({ left: 0, top: 0, width: 0, maxHeight: 240 });
|
|
41
|
+
const search = query ?? "";
|
|
42
|
+
const eligible = search.length >= (props.minLength ?? 0);
|
|
43
|
+
const results = (props.loadOptions
|
|
44
|
+
? remote
|
|
45
|
+
: options.filter((option) => props.getOptionLabel(option).toLocaleLowerCase().includes(search.toLocaleLowerCase()))).slice(0, props.maxResults ?? 100);
|
|
46
|
+
(0, react_1.useEffect)(() => {
|
|
47
|
+
if (!open || props.disabled || !eligible || !props.loadOptions) {
|
|
48
|
+
setLoading(false);
|
|
49
|
+
setRemote([]);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const controller = new AbortController();
|
|
53
|
+
let current = true;
|
|
54
|
+
setLoading(true);
|
|
55
|
+
setRemote([]);
|
|
56
|
+
setError("");
|
|
57
|
+
const timer = setTimeout(async () => {
|
|
58
|
+
try {
|
|
59
|
+
const rows = await props.loadOptions?.(search, controller.signal);
|
|
60
|
+
if (current)
|
|
61
|
+
setRemote(rows ?? []);
|
|
62
|
+
}
|
|
63
|
+
catch (cause) {
|
|
64
|
+
if (current)
|
|
65
|
+
setError(cause instanceof Error ? cause.message : "Could not load choices");
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
if (current)
|
|
69
|
+
setLoading(false);
|
|
70
|
+
}
|
|
71
|
+
}, props.debounceMs ?? 150);
|
|
72
|
+
return () => {
|
|
73
|
+
current = false;
|
|
74
|
+
controller.abort();
|
|
75
|
+
clearTimeout(timer);
|
|
76
|
+
};
|
|
77
|
+
}, [open, props.disabled, search, eligible, props.loadOptions, props.debounceMs]);
|
|
78
|
+
(0, react_1.useLayoutEffect)(() => {
|
|
79
|
+
if (!open)
|
|
80
|
+
return;
|
|
81
|
+
const place = () => {
|
|
82
|
+
const rect = input.current?.parentElement?.getBoundingClientRect();
|
|
83
|
+
if (!rect)
|
|
84
|
+
return;
|
|
85
|
+
const below = window.innerHeight - rect.bottom - 8;
|
|
86
|
+
const height = Math.min(240, Math.max(100, below >= 140 ? below : rect.top - 8));
|
|
87
|
+
setPosition({
|
|
88
|
+
left: rect.left,
|
|
89
|
+
width: rect.width,
|
|
90
|
+
top: below >= 140 ? rect.bottom : Math.max(8, rect.top - height),
|
|
91
|
+
maxHeight: height,
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
place();
|
|
95
|
+
window.addEventListener("resize", place);
|
|
96
|
+
window.addEventListener("scroll", place, true);
|
|
97
|
+
return () => {
|
|
98
|
+
window.removeEventListener("resize", place);
|
|
99
|
+
window.removeEventListener("scroll", place, true);
|
|
100
|
+
};
|
|
101
|
+
}, [open]);
|
|
102
|
+
(0, react_1.useEffect)(() => {
|
|
103
|
+
if (open && active >= 0)
|
|
104
|
+
document.getElementById(`${id}-${active}`)?.scrollIntoView?.({ block: "nearest" });
|
|
105
|
+
}, [open, active, id]);
|
|
106
|
+
const close = () => {
|
|
107
|
+
setOpen(false);
|
|
108
|
+
setQuery(null);
|
|
109
|
+
setActive(-1);
|
|
110
|
+
};
|
|
111
|
+
const choose = (option) => {
|
|
112
|
+
props.onChange(option);
|
|
113
|
+
close();
|
|
114
|
+
};
|
|
115
|
+
const setRef = (node) => {
|
|
116
|
+
input.current = node;
|
|
117
|
+
if (typeof props.inputRef === "function")
|
|
118
|
+
props.inputRef(node);
|
|
119
|
+
else if (props.inputRef)
|
|
120
|
+
props.inputRef.current = node;
|
|
121
|
+
};
|
|
122
|
+
const show = open && !props.disabled;
|
|
123
|
+
const showClear = (props.clearable ?? !props.required) && value !== null;
|
|
124
|
+
const selectedLabel = value === null ? "" : props.getOptionLabel(value);
|
|
125
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "peers-autocomplete position-relative", style: { minWidth: 0, width: "100%" }, children: [(0, jsx_runtime_1.jsx)("input", { ref: setRef, className: `form-control ${props.invalid ? "is-invalid" : ""}`, style: {
|
|
126
|
+
minWidth: 0,
|
|
127
|
+
paddingRight: props.invalid
|
|
128
|
+
? showClear
|
|
129
|
+
? "6.25rem"
|
|
130
|
+
: "4.25rem"
|
|
131
|
+
: showClear
|
|
132
|
+
? "4.75rem"
|
|
133
|
+
: "2.75rem",
|
|
134
|
+
backgroundPosition: props.invalid
|
|
135
|
+
? `right ${showClear ? "4.75rem" : "2.75rem"} center`
|
|
136
|
+
: undefined,
|
|
137
|
+
}, role: "combobox", "aria-label": props.label, "aria-autocomplete": "list", "aria-expanded": show, "aria-controls": show ? id : undefined, "aria-activedescendant": show && active >= 0 && results[active] ? `${id}-${active}` : undefined, "aria-invalid": props.invalid || undefined, autoComplete: "off", value: query ?? selectedLabel, placeholder: props.placeholder, disabled: props.disabled, required: props.required, onFocus: (event) => {
|
|
138
|
+
event.target.select();
|
|
139
|
+
}, onBlur: close, onChange: (event) => {
|
|
140
|
+
setQuery(event.target.value);
|
|
141
|
+
setError("");
|
|
142
|
+
if (props.loadOptions) {
|
|
143
|
+
setRemote([]);
|
|
144
|
+
setLoading(true);
|
|
145
|
+
}
|
|
146
|
+
setOpen(true);
|
|
147
|
+
setActive(-1);
|
|
148
|
+
}, onKeyDown: (event) => {
|
|
149
|
+
if (event.nativeEvent.isComposing || event.ctrlKey || event.metaKey || event.altKey)
|
|
150
|
+
return;
|
|
151
|
+
if (event.key === "ArrowDown" || (event.key === "ArrowUp" && show)) {
|
|
152
|
+
event.preventDefault();
|
|
153
|
+
event.stopPropagation();
|
|
154
|
+
setOpen(true);
|
|
155
|
+
setActive((prior) => event.key === "ArrowDown"
|
|
156
|
+
? Math.min(prior + 1, results.length - 1)
|
|
157
|
+
: prior <= 0
|
|
158
|
+
? results.length - 1
|
|
159
|
+
: prior - 1);
|
|
160
|
+
}
|
|
161
|
+
else if (event.key === "Enter" && show) {
|
|
162
|
+
event.preventDefault();
|
|
163
|
+
event.stopPropagation();
|
|
164
|
+
const option = results[active >= 0 ? active : 0];
|
|
165
|
+
if (eligible && option && !loading && !error)
|
|
166
|
+
choose(option);
|
|
167
|
+
}
|
|
168
|
+
else if (event.key === "Escape" && (show || query !== null)) {
|
|
169
|
+
event.preventDefault();
|
|
170
|
+
event.stopPropagation();
|
|
171
|
+
close();
|
|
172
|
+
}
|
|
173
|
+
else if (event.key === "Tab" &&
|
|
174
|
+
eligible &&
|
|
175
|
+
show &&
|
|
176
|
+
active >= 0 &&
|
|
177
|
+
results[active] &&
|
|
178
|
+
!loading &&
|
|
179
|
+
!error) {
|
|
180
|
+
choose(results[active]);
|
|
181
|
+
}
|
|
182
|
+
} }), (0, jsx_runtime_1.jsxs)("span", { className: "position-absolute top-0 end-0 h-100 d-flex align-items-center", style: { paddingRight: "0.25rem" }, children: [showClear && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn d-inline-flex align-items-center justify-content-center", style: indicatorStyle, tabIndex: -1, "aria-label": `Clear ${props.label}`, disabled: props.disabled, onMouseDown: (event) => event.preventDefault(), onClick: () => {
|
|
183
|
+
choose(null);
|
|
184
|
+
input.current?.focus();
|
|
185
|
+
}, children: (0, jsx_runtime_1.jsx)("svg", { width: "14", height: "14", viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", focusable: "false", children: (0, jsx_runtime_1.jsx)("path", { d: "m4 4 8 8M12 4l-8 8", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }) }) })), (0, jsx_runtime_1.jsx)("span", { "aria-hidden": "true", style: {
|
|
186
|
+
height: "55%",
|
|
187
|
+
borderLeft: "1px solid var(--bs-border-color)",
|
|
188
|
+
marginInline: "0.125rem",
|
|
189
|
+
} }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn d-inline-flex align-items-center justify-content-center", style: indicatorStyle, tabIndex: -1, "aria-label": `Show ${props.label} choices`, disabled: props.disabled, onMouseDown: (event) => event.preventDefault(), onClick: () => {
|
|
190
|
+
input.current?.focus();
|
|
191
|
+
setQuery(null);
|
|
192
|
+
setActive(-1);
|
|
193
|
+
setOpen(!open);
|
|
194
|
+
}, children: (0, jsx_runtime_1.jsx)("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", focusable: "false", children: (0, jsx_runtime_1.jsx)("path", { d: "m4 6 4 4 4-4", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }) })] }), show &&
|
|
195
|
+
(0, react_dom_1.createPortal)((0, jsx_runtime_1.jsx)("div", { id: id, role: "listbox", "aria-label": `${props.label} choices`, className: "border rounded bg-body text-body shadow", style: { position: "fixed", ...position, overflowY: "auto", zIndex: 1080 }, children: loading || error || !eligible || !results.length ? ((0, jsx_runtime_1.jsx)("div", { className: `p-2 ${error ? "text-danger" : "text-body-secondary"}`, role: "status", children: loading
|
|
196
|
+
? "Loading…"
|
|
197
|
+
: error ||
|
|
198
|
+
(!eligible ? `Type at least ${props.minLength} characters` : "No matches") })) : (results.map((option, index) => ((0, jsx_runtime_1.jsx)("div", { id: `${id}-${index}`, role: "option", "aria-selected": value !== null && props.getOptionKey(value) === props.getOptionKey(option), className: `px-3 py-2 ${active === index ? "text-bg-primary" : ""}`, style: { cursor: "pointer" }, onMouseEnter: () => setActive(index), onMouseDown: (event) => event.preventDefault(), onClick: () => choose(option), children: props.getOptionLabel(option) }, props.getOptionKey(option))))) }), document.body)] }));
|
|
199
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { type CSSProperties, type ReactNode } from "react";
|
|
2
|
+
import { type GridValue } from "./reactive";
|
|
3
|
+
/** Stable lookup value; labels can change without changing the stored relationship. */
|
|
4
|
+
export interface DataGridOption {
|
|
5
|
+
value: string;
|
|
6
|
+
label: string;
|
|
7
|
+
}
|
|
8
|
+
/** One entry in a multi-column sort, in priority order. */
|
|
9
|
+
export interface DataGridSort {
|
|
10
|
+
columnId: string;
|
|
11
|
+
direction: "asc" | "desc";
|
|
12
|
+
}
|
|
13
|
+
/** Declarative column, with optional editing, lookup, custom actions, and totals. */
|
|
14
|
+
export interface DataGridColumn<T> {
|
|
15
|
+
id: string;
|
|
16
|
+
label: string;
|
|
17
|
+
type?: "text" | "number" | "date" | "boolean" | "select";
|
|
18
|
+
width?: CSSProperties["width"];
|
|
19
|
+
/** Column width hint in pixels, including cell padding; editors fit the remaining space. */
|
|
20
|
+
minWidth?: number;
|
|
21
|
+
getValue?: (row: T) => unknown;
|
|
22
|
+
/** Omit to make this a display/action column. Numbers remain text until the owner validates. */
|
|
23
|
+
editable?: boolean | ((row: T) => boolean);
|
|
24
|
+
required?: boolean;
|
|
25
|
+
options?: GridValue<readonly DataGridOption[]>;
|
|
26
|
+
renderCell?: (row: T) => ReactNode;
|
|
27
|
+
sortValue?: (row: T) => string | number | null | undefined;
|
|
28
|
+
sortable?: boolean;
|
|
29
|
+
footer?: (rows: readonly T[]) => ReactNode;
|
|
30
|
+
align?: "left" | "right" | "center";
|
|
31
|
+
}
|
|
32
|
+
/** Persistence-free grid. Owners stage edits and validate/save through their own tools. */
|
|
33
|
+
export interface DataGridProps<T> {
|
|
34
|
+
rows: GridValue<readonly T[]>;
|
|
35
|
+
columns: readonly DataGridColumn<T>[];
|
|
36
|
+
rowKey: (row: T) => string;
|
|
37
|
+
rowLabel?: (row: T) => string;
|
|
38
|
+
label: string;
|
|
39
|
+
onCellChange: (row: T, columnId: string, value: string | boolean) => void;
|
|
40
|
+
/** Optional controlled or observable sort; use onSortChange as its explicit write boundary. */
|
|
41
|
+
sort?: GridValue<readonly DataGridSort[]>;
|
|
42
|
+
defaultSort?: readonly DataGridSort[];
|
|
43
|
+
onSortChange?: (sort: DataGridSort[]) => void;
|
|
44
|
+
disabled?: boolean;
|
|
45
|
+
getRowError?: (row: T) => string | undefined;
|
|
46
|
+
onAddRow?: () => void;
|
|
47
|
+
addRowLabel?: string;
|
|
48
|
+
className?: string;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* An always-editable, Bootstrap grid ported from JobTrak's spreadsheet interactions.
|
|
52
|
+
* Up/down move vertically; left/right move at caret boundaries or when all text is selected.
|
|
53
|
+
* Enter moves to the next row's first editor. Native Tab/Shift+Tab are preserved, and popup
|
|
54
|
+
* editors can consume keys. Sorting never mutates caller rows; focus retains a stable order.
|
|
55
|
+
*/
|
|
56
|
+
export declare function DataGrid<T>(props: DataGridProps<T>): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DataGrid = DataGrid;
|
|
4
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
+
const react_1 = require("react");
|
|
6
|
+
const autocomplete_1 = require("./autocomplete");
|
|
7
|
+
const reactive_1 = require("./reactive");
|
|
8
|
+
const noSort = [];
|
|
9
|
+
/**
|
|
10
|
+
* An always-editable, Bootstrap grid ported from JobTrak's spreadsheet interactions.
|
|
11
|
+
* Up/down move vertically; left/right move at caret boundaries or when all text is selected.
|
|
12
|
+
* Enter moves to the next row's first editor. Native Tab/Shift+Tab are preserved, and popup
|
|
13
|
+
* editors can consume keys. Sorting never mutates caller rows; focus retains a stable order.
|
|
14
|
+
*/
|
|
15
|
+
function DataGrid(props) {
|
|
16
|
+
const rows = (0, reactive_1.useGridValue)(props.rows);
|
|
17
|
+
const [localSort, setLocalSort] = (0, react_1.useState)(props.defaultSort ?? noSort);
|
|
18
|
+
const sort = (0, reactive_1.useGridValue)(props.sort ?? localSort);
|
|
19
|
+
const table = (0, react_1.useRef)(null);
|
|
20
|
+
const add = (0, react_1.useRef)(null);
|
|
21
|
+
const order = (0, react_1.useRef)([]);
|
|
22
|
+
const pendingNew = (0, react_1.useRef)(null);
|
|
23
|
+
const [focused, setFocused] = (0, react_1.useState)(false);
|
|
24
|
+
const sorted = (0, react_1.useMemo)(() => {
|
|
25
|
+
const next = [...rows];
|
|
26
|
+
if (focused && order.current.length) {
|
|
27
|
+
const rank = new Map(order.current.map((id, index) => [id, index]));
|
|
28
|
+
next.sort((a, b) => (rank.get(props.rowKey(a)) ?? Infinity) - (rank.get(props.rowKey(b)) ?? Infinity));
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
next.sort((a, b) => {
|
|
32
|
+
for (const spec of sort) {
|
|
33
|
+
const column = props.columns.find((item) => item.id === spec.columnId);
|
|
34
|
+
const av = column?.sortValue?.(a) ?? column?.getValue?.(a) ?? "";
|
|
35
|
+
const bv = column?.sortValue?.(b) ?? column?.getValue?.(b) ?? "";
|
|
36
|
+
const result = typeof av === "number" && typeof bv === "number"
|
|
37
|
+
? av - bv
|
|
38
|
+
: String(av).localeCompare(String(bv), undefined, {
|
|
39
|
+
numeric: true,
|
|
40
|
+
sensitivity: "base",
|
|
41
|
+
});
|
|
42
|
+
if (result)
|
|
43
|
+
return spec.direction === "asc" ? result : -result;
|
|
44
|
+
}
|
|
45
|
+
return 0;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
order.current = next.map(props.rowKey);
|
|
49
|
+
return next;
|
|
50
|
+
}, [rows, sort, props.columns, props.rowKey, focused]);
|
|
51
|
+
const focusCell = (r, c) => {
|
|
52
|
+
const cell = table.current?.querySelector(`[data-grid-row="${r}"][data-grid-col="${c}"]`);
|
|
53
|
+
const target = cell?.querySelector("input:not(:disabled),textarea:not(:disabled),button:not(:disabled):not([tabindex='-1']),select:not(:disabled)");
|
|
54
|
+
if (!target)
|
|
55
|
+
return false;
|
|
56
|
+
target.focus();
|
|
57
|
+
target.select?.();
|
|
58
|
+
return true;
|
|
59
|
+
};
|
|
60
|
+
const focusNextRow = (r) => {
|
|
61
|
+
for (let row = r; row < sorted.length; row++) {
|
|
62
|
+
for (let col = 0; col < props.columns.length; col++) {
|
|
63
|
+
if (props.columns[col].editable && focusCell(row, col))
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (add.current && !add.current.disabled) {
|
|
68
|
+
add.current.focus();
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
return false;
|
|
72
|
+
};
|
|
73
|
+
const onKey = (event, r, c) => {
|
|
74
|
+
if (event.defaultPrevented ||
|
|
75
|
+
event.nativeEvent.isComposing ||
|
|
76
|
+
event.altKey ||
|
|
77
|
+
event.ctrlKey ||
|
|
78
|
+
event.metaKey ||
|
|
79
|
+
event.shiftKey)
|
|
80
|
+
return;
|
|
81
|
+
const target = event.target;
|
|
82
|
+
if (target.tagName === "BUTTON" && event.key === "Enter")
|
|
83
|
+
return;
|
|
84
|
+
let moved = false;
|
|
85
|
+
if (event.key === "Enter")
|
|
86
|
+
moved = focusNextRow(r + 1);
|
|
87
|
+
if (event.key === "ArrowUp" || event.key === "ArrowDown") {
|
|
88
|
+
const step = event.key === "ArrowUp" ? -1 : 1;
|
|
89
|
+
for (let row = r + step; row >= 0 && row < sorted.length; row += step) {
|
|
90
|
+
if (focusCell(row, c)) {
|
|
91
|
+
moved = true;
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (event.key === "ArrowLeft" || event.key === "ArrowRight") {
|
|
97
|
+
const textInput = target.tagName === "INPUT" || target.tagName === "TEXTAREA";
|
|
98
|
+
const all = textInput && target.selectionStart === 0 && target.selectionEnd === target.value.length;
|
|
99
|
+
const atEdge = !textInput ||
|
|
100
|
+
target.selectionStart === null ||
|
|
101
|
+
all ||
|
|
102
|
+
(event.key === "ArrowLeft"
|
|
103
|
+
? target.selectionStart === 0 && target.selectionEnd === 0
|
|
104
|
+
: target.selectionStart === target.value.length);
|
|
105
|
+
if (!atEdge)
|
|
106
|
+
return;
|
|
107
|
+
const step = event.key === "ArrowLeft" ? -1 : 1;
|
|
108
|
+
const count = props.columns.length;
|
|
109
|
+
for (let index = r * count + c + step; index >= 0 && index < sorted.length * count; index += step) {
|
|
110
|
+
if (focusCell(Math.floor(index / count), index % count)) {
|
|
111
|
+
moved = true;
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (moved) {
|
|
117
|
+
event.preventDefault();
|
|
118
|
+
event.stopPropagation();
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
const toggleSort = (column, append) => {
|
|
122
|
+
const prior = sort.find((entry) => entry.columnId === column.id);
|
|
123
|
+
const rest = append ? sort.filter((entry) => entry.columnId !== column.id) : [];
|
|
124
|
+
const next = prior?.direction === "desc"
|
|
125
|
+
? rest
|
|
126
|
+
: [...rest, { columnId: column.id, direction: prior ? "desc" : "asc" }];
|
|
127
|
+
setFocused(false);
|
|
128
|
+
setLocalSort(next);
|
|
129
|
+
props.onSortChange?.(next);
|
|
130
|
+
};
|
|
131
|
+
return ((0, jsx_runtime_1.jsx)("div", { className: `table-responsive ${props.className ?? ""}`, children: (0, jsx_runtime_1.jsxs)("table", { ref: table, className: "table table-striped table-hover table-bordered align-middle mb-0", "aria-label": props.label, style: {
|
|
132
|
+
tableLayout: "fixed",
|
|
133
|
+
minWidth: props.columns.reduce((sum, column) => sum + (column.minWidth ?? (typeof column.width === "number" ? column.width : 80)), 0),
|
|
134
|
+
}, onFocus: () => setFocused(true), onBlur: (event) => {
|
|
135
|
+
if (!event.currentTarget.contains(event.relatedTarget))
|
|
136
|
+
setFocused(false);
|
|
137
|
+
}, children: [(0, jsx_runtime_1.jsx)("thead", { children: (0, jsx_runtime_1.jsx)("tr", { children: props.columns.map((column) => {
|
|
138
|
+
const spec = sort.find((entry) => entry.columnId === column.id);
|
|
139
|
+
const sortable = column.sortable !== false && Boolean(column.getValue || column.sortValue);
|
|
140
|
+
return ((0, jsx_runtime_1.jsx)("th", { scope: "col", style: { width: column.width, minWidth: column.minWidth, whiteSpace: "nowrap" }, "aria-sort": sort[0]?.columnId === column.id
|
|
141
|
+
? spec?.direction === "asc"
|
|
142
|
+
? "ascending"
|
|
143
|
+
: "descending"
|
|
144
|
+
: undefined, children: sortable ? ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "btn p-0 border-0 text-body fw-bold text-start", title: "Sort; Shift-click adds another column", onClick: (e) => toggleSort(column, e.shiftKey), children: [column.label, spec && ((0, jsx_runtime_1.jsxs)("span", { "aria-hidden": "true", children: [" ", spec.direction === "asc" ? "↓" : "↑", sort.length > 1 ? sort.indexOf(spec) + 1 : ""] }))] })) : (column.label) }, column.id));
|
|
145
|
+
}) }) }), (0, jsx_runtime_1.jsx)("tbody", { children: sorted.map((row, r) => {
|
|
146
|
+
const key = props.rowKey(row);
|
|
147
|
+
const label = props.rowLabel?.(row) || key;
|
|
148
|
+
const error = props.getRowError?.(row);
|
|
149
|
+
return ((0, jsx_runtime_1.jsx)(GridRow, { row: row, columns: props.columns, label: label, index: r, error: error, disabled: props.disabled, onChange: props.onCellChange, onKey: onKey, onMount: (node) => {
|
|
150
|
+
if (node && pendingNew.current && !pendingNew.current.has(key)) {
|
|
151
|
+
pendingNew.current = null;
|
|
152
|
+
const input = node.querySelector("input:not(:disabled)");
|
|
153
|
+
input?.focus();
|
|
154
|
+
input?.select?.();
|
|
155
|
+
}
|
|
156
|
+
} }, key));
|
|
157
|
+
}) }), (props.onAddRow || props.columns.some((column) => column.footer)) && ((0, jsx_runtime_1.jsxs)("tfoot", { children: [props.columns.some((column) => column.footer) && ((0, jsx_runtime_1.jsx)("tr", { children: props.columns.map((column) => ((0, jsx_runtime_1.jsx)("td", { className: "fw-bold", style: { textAlign: column.align }, children: column.footer?.(rows) }, column.id))) })), props.onAddRow && ((0, jsx_runtime_1.jsx)("tr", { children: (0, jsx_runtime_1.jsx)("td", { colSpan: props.columns.length, children: (0, jsx_runtime_1.jsxs)("button", { ref: add, type: "button", className: "btn btn-outline-secondary btn-sm", disabled: props.disabled, onClick: () => {
|
|
158
|
+
pendingNew.current = new Set(rows.map(props.rowKey));
|
|
159
|
+
props.onAddRow?.();
|
|
160
|
+
}, children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-plus-lg", "aria-hidden": "true" }), " ", props.addRowLabel || "Add row"] }) }) }))] }))] }) }));
|
|
161
|
+
}
|
|
162
|
+
function GridRow({ row, columns, label, index, error, disabled, onChange, onKey, onMount, }) {
|
|
163
|
+
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("tr", { ref: onMount, className: error ? "table-danger" : undefined, children: columns.map((column, c) => ((0, jsx_runtime_1.jsx)("td", { "data-grid-row": index, "data-grid-col": c, style: { textAlign: column.align ?? (column.type === "number" ? "right" : undefined) }, onKeyDown: (event) => onKey(event, index, c), children: (0, jsx_runtime_1.jsx)(GridCell, { row: row, column: column, label: `${column.label} — ${label}`, disabled: disabled, invalid: !!error, onChange: (value) => onChange(row, column.id, value) }) }, column.id))) }), error && ((0, jsx_runtime_1.jsx)("tr", { className: "table-danger", children: (0, jsx_runtime_1.jsxs)("td", { colSpan: columns.length, role: "alert", children: [label, ": ", error] }) }))] }));
|
|
164
|
+
}
|
|
165
|
+
function GridCell({ row, column, label, disabled, invalid, onChange, }) {
|
|
166
|
+
const options = (0, reactive_1.useGridValue)(column.options ?? []);
|
|
167
|
+
const editable = typeof column.editable === "function" ? column.editable(row) : column.editable;
|
|
168
|
+
const raw = column.getValue?.(row);
|
|
169
|
+
if (!editable)
|
|
170
|
+
return (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: column.renderCell?.(row) ?? (raw == null ? "" : String(raw)) });
|
|
171
|
+
if (column.type === "select")
|
|
172
|
+
return ((0, jsx_runtime_1.jsx)(autocomplete_1.Autocomplete, { label: label, value: options.find((option) => option.value === raw) ??
|
|
173
|
+
(raw ? { value: String(raw), label: `Unavailable (${String(raw)})` } : null), options: options, getOptionKey: (option) => option.value, getOptionLabel: (option) => option.label, onChange: (option) => onChange(option?.value ?? ""), disabled: disabled, required: column.required, invalid: invalid }));
|
|
174
|
+
return ((0, jsx_runtime_1.jsx)("input", { "aria-label": label, "aria-invalid": invalid || undefined, required: column.required, disabled: disabled, type: column.type === "boolean" ? "checkbox" : column.type === "date" ? "date" : "text", className: `${column.type === "boolean" ? "form-check-input" : "form-control"}${invalid ? " is-invalid" : ""}`, style: {
|
|
175
|
+
// Column sizing belongs to the table; reusing it here would consume the cell padding.
|
|
176
|
+
minWidth: 0,
|
|
177
|
+
textAlign: column.type === "number" ? "right" : undefined,
|
|
178
|
+
}, inputMode: column.type === "number" ? "decimal" : undefined, value: column.type === "boolean" ? undefined : String(raw ?? ""), checked: column.type === "boolean" ? Boolean(raw) : undefined, onChange: (event) => onChange(column.type === "boolean" ? event.target.checked : event.target.value) }));
|
|
179
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DataGrid = exports.Autocomplete = void 0;
|
|
4
|
+
var autocomplete_1 = require("./autocomplete");
|
|
5
|
+
Object.defineProperty(exports, "Autocomplete", { enumerable: true, get: function () { return autocomplete_1.Autocomplete; } });
|
|
6
|
+
var data_grid_1 = require("./data-grid");
|
|
7
|
+
Object.defineProperty(exports, "DataGrid", { enumerable: true, get: function () { return data_grid_1.DataGrid; } });
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ReadonlyObservable } from "@peers-app/peers-sdk";
|
|
2
|
+
/** A plain value or a Peers observable/computed, read without acquiring write authority. */
|
|
3
|
+
export type GridValue<T> = T | ReadonlyObservable<T>;
|
|
4
|
+
/** Subscribe through Peers' concurrent-safe React adapter; never write to the source. */
|
|
5
|
+
export declare function useGridValue<T>(source: GridValue<T>): T;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useGridValue = useGridValue;
|
|
4
|
+
const use_observable_1 = require("../../hooks/use-observable");
|
|
5
|
+
/** Subscribe through Peers' concurrent-safe React adapter; never write to the source. */
|
|
6
|
+
function useGridValue(source) {
|
|
7
|
+
// useObservable's public overload accepts a writable union; we use only its read side.
|
|
8
|
+
return (0, use_observable_1.useObservable)(source)[0];
|
|
9
|
+
}
|