@x-tech-laboratories/x-libs 0.4.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +89 -13
- package/dist/backups.d.ts +31 -0
- package/dist/backups.js +43 -0
- package/dist/dates.d.ts +9 -0
- package/dist/dates.js +60 -0
- package/dist/durations.d.ts +9 -1
- package/dist/durations.js +19 -5
- package/dist/electron/app.d.ts +13 -0
- package/dist/electron/app.js +27 -0
- package/dist/electron/bridge.d.ts +32 -0
- package/dist/electron/bridge.js +53 -0
- package/dist/electron/events.d.ts +21 -0
- package/dist/electron/events.js +25 -0
- package/dist/electron/fetch.d.ts +25 -0
- package/dist/electron/fetch.js +67 -0
- package/dist/electron/window.d.ts +96 -0
- package/dist/electron/window.js +128 -0
- package/dist/log.d.ts +30 -0
- package/dist/log.js +79 -0
- package/dist/server/backup_files.d.ts +3 -0
- package/dist/server/backup_files.js +25 -0
- package/dist/web/drag.d.ts +3 -0
- package/dist/web/drag.js +13 -0
- package/dist/web/format.d.ts +1 -0
- package/dist/web/format.js +5 -0
- package/dist/web/menus.d.ts +4 -0
- package/dist/web/menus.js +28 -1
- package/dist/web/prefs.d.ts +12 -0
- package/dist/web/prefs.js +62 -0
- package/dist/web/{preact → react}/badges.d.ts +9 -9
- package/dist/web/{preact → react}/badges.js +6 -6
- package/dist/web/react/controls.d.ts +72 -0
- package/dist/web/react/controls.js +88 -0
- package/dist/web/{preact → react}/dialog.d.ts +10 -10
- package/dist/web/{preact → react}/dialog.js +14 -9
- package/dist/web/react/layout.d.ts +100 -0
- package/dist/web/react/layout.js +80 -0
- package/dist/web/react/menus.d.ts +31 -0
- package/dist/web/react/menus.js +159 -0
- package/dist/web/react/prefs.d.ts +2 -0
- package/dist/web/react/prefs.js +11 -0
- package/dist/web/react/reveal.d.ts +9 -0
- package/dist/web/react/reveal.js +30 -0
- package/dist/web/react/shell.d.ts +41 -0
- package/dist/web/react/shell.js +90 -0
- package/dist/web/react/shortcuts.d.ts +23 -0
- package/dist/web/react/shortcuts.js +95 -0
- package/dist/web/{preact → react}/split.d.ts +4 -4
- package/dist/web/{preact → react}/split.js +9 -10
- package/dist/web/{preact → react}/states.d.ts +7 -7
- package/dist/web/{preact → react}/states.js +6 -5
- package/dist/web/react/table.d.ts +69 -0
- package/dist/web/react/table.js +278 -0
- package/dist/web/{preact → react}/time.d.ts +2 -1
- package/dist/web/react/time.js +19 -0
- package/dist/web/react/toasts.d.ts +29 -0
- package/dist/web/react/toasts.js +72 -0
- package/dist/web/react/tree.d.ts +28 -0
- package/dist/web/react/tree.js +183 -0
- package/dist/web/react/watch.d.ts +3 -0
- package/dist/web/react/watch.js +5 -0
- package/dist/web/selection.d.ts +25 -0
- package/dist/web/selection.js +106 -0
- package/dist/web/tables.d.ts +19 -1
- package/dist/web/tables.js +78 -8
- package/dist/web/theme.d.ts +6 -1
- package/dist/web/theme.js +31 -21
- package/dist/web/tree.d.ts +37 -0
- package/dist/web/tree.js +128 -0
- package/dist/web/virtual.d.ts +7 -0
- package/dist/web/virtual.js +10 -0
- package/dist/web/watch.d.ts +6 -0
- package/dist/web/watch.js +17 -0
- package/package.json +15 -4
- package/dist/web/preact/controls.d.ts +0 -37
- package/dist/web/preact/controls.js +0 -43
- package/dist/web/preact/layout.d.ts +0 -80
- package/dist/web/preact/layout.js +0 -71
- package/dist/web/preact/menus.d.ts +0 -19
- package/dist/web/preact/menus.js +0 -70
- package/dist/web/preact/shell.d.ts +0 -27
- package/dist/web/preact/shell.js +0 -63
- package/dist/web/preact/table.d.ts +0 -23
- package/dist/web/preact/table.js +0 -20
- package/dist/web/preact/time.js +0 -9
- package/dist/web/preact/toasts.d.ts +0 -15
- package/dist/web/preact/toasts.js +0 -34
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { type InputHTMLAttributes, type ReactNode } from "react";
|
|
2
|
+
import { type TreeItem } from "../tree.ts";
|
|
3
|
+
interface SegmentedProps<Value extends string> {
|
|
4
|
+
label: string;
|
|
5
|
+
choices: {
|
|
6
|
+
value: Value;
|
|
7
|
+
label: string;
|
|
8
|
+
}[];
|
|
9
|
+
value: Value;
|
|
10
|
+
on_change: (value: Value) => void;
|
|
11
|
+
class_name?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function Segmented<Value extends string>({ label, choices, value, on_change, class_name }: SegmentedProps<Value>): import("react").JSX.Element;
|
|
14
|
+
export declare function ButtonGroup({ label, children }: {
|
|
15
|
+
label: string;
|
|
16
|
+
children: ReactNode;
|
|
17
|
+
}): import("react").JSX.Element;
|
|
18
|
+
interface SwitchProps {
|
|
19
|
+
label: string;
|
|
20
|
+
checked: boolean;
|
|
21
|
+
on_change: (on: boolean) => Promise<boolean | void> | boolean | void;
|
|
22
|
+
disabled?: boolean;
|
|
23
|
+
}
|
|
24
|
+
export declare function Switch({ label, checked, on_change, disabled }: SwitchProps): import("react").JSX.Element;
|
|
25
|
+
interface SearchBoxProps {
|
|
26
|
+
label: string;
|
|
27
|
+
value: string;
|
|
28
|
+
on_input: (value: string) => void;
|
|
29
|
+
auto_focus?: boolean;
|
|
30
|
+
}
|
|
31
|
+
export declare function SearchBox({ label, value, on_input, auto_focus }: SearchBoxProps): import("react").JSX.Element;
|
|
32
|
+
interface SelectProps<Value extends string | number> {
|
|
33
|
+
label: string;
|
|
34
|
+
options: [value: Value, text: string][];
|
|
35
|
+
value: Value;
|
|
36
|
+
on_change: (value: Value) => void;
|
|
37
|
+
bare?: boolean;
|
|
38
|
+
}
|
|
39
|
+
export declare function Select<Value extends string | number>({ label, options, value, on_change, bare }: SelectProps<Value>): import("react").JSX.Element;
|
|
40
|
+
type CommitInputProps = Omit<InputHTMLAttributes<HTMLInputElement>, "value" | "defaultValue" | "onChange"> & {
|
|
41
|
+
value: string | number;
|
|
42
|
+
on_commit: (event: {
|
|
43
|
+
currentTarget: HTMLInputElement;
|
|
44
|
+
}) => void;
|
|
45
|
+
};
|
|
46
|
+
export declare function CommitInput({ value, on_commit, onBlur, onKeyDown, ...rest }: CommitInputProps): import("react").JSX.Element;
|
|
47
|
+
interface DateTimeFieldProps {
|
|
48
|
+
label: string;
|
|
49
|
+
value: string | null;
|
|
50
|
+
on_change: (value: string | null) => void;
|
|
51
|
+
allow_time?: boolean;
|
|
52
|
+
}
|
|
53
|
+
export declare function DateTimeField({ label, value, on_change, allow_time }: DateTimeFieldProps): import("react").JSX.Element;
|
|
54
|
+
interface ColorPickerProps {
|
|
55
|
+
label: string;
|
|
56
|
+
value: string | null;
|
|
57
|
+
swatches: string[];
|
|
58
|
+
on_change: (color: string | null) => void;
|
|
59
|
+
allow_none?: boolean;
|
|
60
|
+
}
|
|
61
|
+
export declare function ColorPicker({ label, value, swatches, on_change, allow_none }: ColorPickerProps): import("react").JSX.Element;
|
|
62
|
+
interface TreeSelectProps<Item extends TreeItem> {
|
|
63
|
+
label: string;
|
|
64
|
+
items: Item[];
|
|
65
|
+
name_of: (item: Item) => string;
|
|
66
|
+
value: string;
|
|
67
|
+
blank_label: string;
|
|
68
|
+
disabled_ids?: readonly string[];
|
|
69
|
+
on_change: (id: string) => void;
|
|
70
|
+
}
|
|
71
|
+
export declare function TreeSelect<Item extends TreeItem>({ label, items, name_of, value, blank_label, disabled_ids, on_change }: TreeSelectProps<Item>): import("react").JSX.Element;
|
|
72
|
+
export {};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { createElement as _createElement } from "react";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
// React controls drawn like the page's own: segmented choices, switches, search fields, selects, and date, colour and tree pickers.
|
|
4
|
+
import { useEffect, useRef, useState } from "react";
|
|
5
|
+
import { in_tree_order, path_names } from "../tree.js";
|
|
6
|
+
// One of a few choices side by side; the chosen one takes the accent.
|
|
7
|
+
export function Segmented({ label, choices, value, on_change, class_name }) {
|
|
8
|
+
return (_jsx("div", { className: class_name ? `segmented ${class_name}` : "segmented", role: "radiogroup", "aria-label": label, children: choices.map((choice) => (_jsx("button", { type: "button", role: "radio", "aria-checked": value === choice.value, onClick: () => on_change(choice.value), children: choice.label }, choice.value))) }));
|
|
9
|
+
}
|
|
10
|
+
// Buttons joined into one control, such as zoom out, zoom in and fit.
|
|
11
|
+
export function ButtonGroup({ label, children }) {
|
|
12
|
+
return _jsx("div", { className: "segmented", role: "group", "aria-label": label, children: children });
|
|
13
|
+
}
|
|
14
|
+
export function Switch({ label, checked, on_change, disabled = false }) {
|
|
15
|
+
const [busy, set_busy] = useState(false);
|
|
16
|
+
async function change(event) {
|
|
17
|
+
const box = event.currentTarget;
|
|
18
|
+
const on = box.checked;
|
|
19
|
+
set_busy(true);
|
|
20
|
+
try {
|
|
21
|
+
if ((await on_change(on)) === false)
|
|
22
|
+
box.checked = !on;
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
box.checked = !on;
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
set_busy(false);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return _jsx("input", { type: "checkbox", role: "switch", "aria-label": label, checked: checked, disabled: disabled || busy, onChange: change });
|
|
33
|
+
}
|
|
34
|
+
// A search field that hands on what was typed once typing pauses; the field keeps its own text, so a redraw never wipes typing, and shows a value changed from outside, such as one cleared.
|
|
35
|
+
export function SearchBox({ label, value, on_input, auto_focus = false }) {
|
|
36
|
+
const field = useRef(null);
|
|
37
|
+
const timer = useRef(undefined);
|
|
38
|
+
const handed_on = useRef(value);
|
|
39
|
+
useEffect(() => () => clearTimeout(timer.current), []);
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (value === handed_on.current || !field.current)
|
|
42
|
+
return;
|
|
43
|
+
clearTimeout(timer.current);
|
|
44
|
+
handed_on.current = value;
|
|
45
|
+
field.current.value = value;
|
|
46
|
+
}, [value]);
|
|
47
|
+
return (_jsx("input", { ref: field, type: "search", className: "filter", placeholder: label, "aria-label": label, defaultValue: value, autoFocus: auto_focus, onChange: (event) => {
|
|
48
|
+
clearTimeout(timer.current);
|
|
49
|
+
const text = event.currentTarget.value;
|
|
50
|
+
timer.current = window.setTimeout(() => {
|
|
51
|
+
handed_on.current = text;
|
|
52
|
+
on_input(text);
|
|
53
|
+
}, 200);
|
|
54
|
+
} }));
|
|
55
|
+
}
|
|
56
|
+
export function Select({ label, options, value, on_change, bare = false }) {
|
|
57
|
+
const change = (event) => {
|
|
58
|
+
const text = event.currentTarget.value;
|
|
59
|
+
on_change((options.find(([option]) => String(option) === text)?.[0] ?? text));
|
|
60
|
+
};
|
|
61
|
+
const select = (_jsx("select", { value: value, "aria-label": bare ? label : undefined, onChange: change, children: options.map(([option, text]) => _jsx("option", { value: option, children: text }, option)) }));
|
|
62
|
+
if (bare)
|
|
63
|
+
return select;
|
|
64
|
+
return (_jsxs("label", { className: "field", children: [_jsx("span", { className: "field-label", children: label }), select] }));
|
|
65
|
+
}
|
|
66
|
+
// A field that saves once it is left or Enter is pressed, as a native change event does, and shows the saved value again whenever that changes.
|
|
67
|
+
export function CommitInput({ value, on_commit, onBlur, onKeyDown, ...rest }) {
|
|
68
|
+
return (_createElement("input", { ...rest, key: String(value), defaultValue: value, onBlur: (event) => { onBlur?.(event); on_commit(event); }, onKeyDown: (event) => { onKeyDown?.(event); if (event.key === "Enter")
|
|
69
|
+
event.currentTarget.blur(); } }));
|
|
70
|
+
}
|
|
71
|
+
// A local date with an optional time and a clear button; clearing the date clears the time.
|
|
72
|
+
export function DateTimeField({ label, value, on_change, allow_time = true }) {
|
|
73
|
+
const [day = "", time = ""] = (value ?? "").split("T");
|
|
74
|
+
const join = (next_day, next_time) => on_change(next_day ? (next_time && allow_time ? `${next_day}T${next_time}` : next_day) : null);
|
|
75
|
+
return (_jsxs("div", { className: "field date-time-field", children: [_jsx("span", { className: "field-label", children: label }), _jsxs("div", { className: "date-time-inputs", children: [_jsx("input", { type: "date", "aria-label": `${label} date`, value: day, onChange: (event) => join(event.currentTarget.value, time) }), day && allow_time && _jsx("input", { type: "time", "aria-label": `${label} time`, value: time, onChange: (event) => join(day, event.currentTarget.value) }), day && _jsx("button", { type: "button", className: "icon", "aria-label": `Clear ${label.toLowerCase()} date`, onClick: () => on_change(null), children: "\u00D7" })] })] }));
|
|
76
|
+
}
|
|
77
|
+
// Swatches, a None choice, and the system colour chooser for any other colour.
|
|
78
|
+
export function ColorPicker({ label, value, swatches, on_change, allow_none = true }) {
|
|
79
|
+
const is_swatch = value !== null && swatches.some((swatch) => swatch.toLowerCase() === value.toLowerCase());
|
|
80
|
+
const custom = value !== null && !is_swatch;
|
|
81
|
+
return (_jsxs("div", { className: "color-picker", role: "radiogroup", "aria-label": label, children: [allow_none && _jsx("button", { type: "button", role: "radio", className: "color-swatch none", "aria-checked": value === null, "aria-label": "No colour", onClick: () => on_change(null) }), swatches.map((swatch) => (_jsx("button", { type: "button", role: "radio", className: "color-swatch", style: { "--swatch-color": swatch }, "aria-checked": value !== null && swatch.toLowerCase() === value.toLowerCase(), "aria-label": swatch, title: swatch, onClick: () => on_change(swatch) }, swatch))), _jsx("input", { type: "color", className: custom ? "color-custom chosen" : "color-custom", "aria-label": "Other colour", value: value ?? "#888888", onChange: (event) => on_change(event.currentTarget.value) })] }));
|
|
82
|
+
}
|
|
83
|
+
// A select listing a tree indented, each choice's whole path on hover and in the closed select.
|
|
84
|
+
export function TreeSelect({ label, items, name_of, value, blank_label, disabled_ids = [], on_change }) {
|
|
85
|
+
const depth_of = (item) => path_names(item, items, name_of).length - 1;
|
|
86
|
+
const chosen = items.find((item) => item.id === value);
|
|
87
|
+
return (_jsxs("select", { className: "tree-select", "aria-label": label, value: value, title: chosen ? path_names(chosen, items, name_of).join(" › ") : undefined, onChange: (event) => on_change(event.currentTarget.value), children: [_jsx("option", { value: "", children: blank_label }), in_tree_order(items).map((item) => (_jsx("option", { value: item.id, disabled: disabled_ids.includes(item.id), title: path_names(item, items, name_of).join(" › "), children: `${"\u2003".repeat(depth_of(item))}${name_of(item)}` }, item.id)))] }));
|
|
88
|
+
}
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
2
|
interface DialogProps {
|
|
3
|
-
title:
|
|
3
|
+
title: ReactNode;
|
|
4
4
|
on_close: () => void;
|
|
5
|
-
buttons?:
|
|
5
|
+
buttons?: ReactNode;
|
|
6
6
|
wide?: boolean;
|
|
7
7
|
class_name?: string;
|
|
8
|
-
children:
|
|
8
|
+
children: ReactNode;
|
|
9
9
|
}
|
|
10
|
-
export declare function Dialog({ title, on_close, buttons, wide, class_name, children }: DialogProps): import("
|
|
10
|
+
export declare function Dialog({ title, on_close, buttons, wide, class_name, children }: DialogProps): import("react").JSX.Element;
|
|
11
11
|
export interface ConfirmOptions {
|
|
12
12
|
title: string;
|
|
13
|
-
message:
|
|
13
|
+
message: ReactNode;
|
|
14
14
|
confirm_label: string;
|
|
15
15
|
busy_label?: string;
|
|
16
16
|
danger?: boolean;
|
|
@@ -21,15 +21,15 @@ export interface ConfirmOptions {
|
|
|
21
21
|
export declare function ConfirmDialog({ options, on_close }: {
|
|
22
22
|
options: ConfirmOptions;
|
|
23
23
|
on_close: () => void;
|
|
24
|
-
}): import("
|
|
24
|
+
}): import("react").JSX.Element;
|
|
25
25
|
export declare function TextDialog({ title, text, on_close }: {
|
|
26
26
|
title: string;
|
|
27
27
|
text: string;
|
|
28
28
|
on_close: () => void;
|
|
29
|
-
}): import("
|
|
30
|
-
type Builder = (close: () => void) =>
|
|
29
|
+
}): import("react").JSX.Element;
|
|
30
|
+
type Builder = (close: () => void) => ReactNode;
|
|
31
31
|
export declare function open_dialog(build: Builder): () => void;
|
|
32
32
|
export declare function confirm_dialog(options: ConfirmOptions): void;
|
|
33
33
|
export declare function text_dialog(title: string, text: string): void;
|
|
34
|
-
export declare function Dialogs(): import("
|
|
34
|
+
export declare function Dialogs(): import("react").JSX.Element;
|
|
35
35
|
export {};
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
// Modal dialogs on the native one: any body and buttons, one that asks before an action, and one that shows a long text.
|
|
3
|
-
import { Fragment } from "
|
|
4
|
-
import { useEffect, useId, useRef, useState } from "preact/hooks";
|
|
3
|
+
import { Fragment, useEffect, useId, useRef, useState } from "react";
|
|
5
4
|
import { toast } from "./toasts.js";
|
|
6
|
-
// Shown while it is rendered: the page behind it is inert, focus stays inside,
|
|
5
|
+
// Shown while it is rendered: the page behind it is inert, focus stays inside, Escape calls on_close, and focus goes back to the opener after.
|
|
7
6
|
export function Dialog({ title, on_close, buttons, wide = false, class_name, children }) {
|
|
8
7
|
const dialog = useRef(null);
|
|
9
8
|
const title_id = useId();
|
|
@@ -11,15 +10,21 @@ export function Dialog({ title, on_close, buttons, wide = false, class_name, chi
|
|
|
11
10
|
close.current = on_close;
|
|
12
11
|
useEffect(() => {
|
|
13
12
|
const element = dialog.current;
|
|
13
|
+
const opener = document.activeElement;
|
|
14
14
|
element.showModal();
|
|
15
|
-
return () =>
|
|
15
|
+
return () => {
|
|
16
|
+
element.close();
|
|
17
|
+
// The dialog is already gone from the page here, so the browser cannot hand focus back by itself.
|
|
18
|
+
if (opener?.isConnected && opener !== document.body)
|
|
19
|
+
opener.focus();
|
|
20
|
+
};
|
|
16
21
|
}, []);
|
|
17
|
-
return (_jsxs("dialog", { ref: dialog,
|
|
22
|
+
return (_jsxs("dialog", { ref: dialog, className: ["dialog", wide && "wide", class_name].filter(Boolean).join(" "), "aria-labelledby": title_id, onCancel: (event) => {
|
|
18
23
|
// Escape asks the owner to close, so the dialog never closes behind its owner's back.
|
|
19
24
|
event.preventDefault();
|
|
20
25
|
event.stopPropagation();
|
|
21
26
|
close.current();
|
|
22
|
-
}, children: [_jsx("h2", { id: title_id, children: title }), _jsx("div", {
|
|
27
|
+
}, children: [_jsx("h2", { id: title_id, children: title }), _jsx("div", { className: "dialog-body", children: children }), buttons && _jsx("div", { className: "dialog-buttons", children: buttons })] }));
|
|
23
28
|
}
|
|
24
29
|
export function ConfirmDialog({ options, on_close }) {
|
|
25
30
|
const [typed, set_typed] = useState("");
|
|
@@ -35,11 +40,11 @@ export function ConfirmDialog({ options, on_close }) {
|
|
|
35
40
|
}
|
|
36
41
|
}
|
|
37
42
|
return (_jsxs(Dialog, { title: options.title, on_close: () => { if (!busy)
|
|
38
|
-
on_close(); }, wide: options.wide, buttons: _jsxs(_Fragment, { children: [_jsx("button", { disabled: busy, onClick: on_close, children: "Cancel" }), _jsx("button", {
|
|
43
|
+
on_close(); }, wide: options.wide, buttons: _jsxs(_Fragment, { children: [_jsx("button", { disabled: busy, onClick: on_close, children: "Cancel" }), _jsx("button", { className: options.danger ? "danger solid" : "accent solid", disabled: !ready || busy, onClick: confirm, autoFocus: options.type_to_confirm === undefined, children: busy ? options.busy_label ?? "Working…" : options.confirm_label })] }), children: [typeof options.message === "string" ? _jsx("p", { children: options.message }) : options.message, options.type_to_confirm !== undefined && (_jsxs("label", { className: "type-confirm", children: [_jsxs("span", { children: ["Type ", _jsx("strong", { children: options.type_to_confirm }), " to confirm:"] }), _jsx("input", { type: "text", className: "filter", autoComplete: "off", autoFocus: true, "aria-label": `Type ${options.type_to_confirm} to confirm`, value: typed, onChange: (event) => set_typed(event.currentTarget.value) })] }))] }));
|
|
39
44
|
}
|
|
40
45
|
// Shows a long text result, such as a preview, with a button to copy it.
|
|
41
46
|
export function TextDialog({ title, text, on_close }) {
|
|
42
|
-
return (_jsx(Dialog, { title: title, on_close: on_close, wide: true, buttons: _jsxs(_Fragment, { children: [_jsx("button", { onClick: () => void navigator.clipboard?.writeText(text).then(() => toast("Copied.")), children: "Copy" }), _jsx("button", {
|
|
47
|
+
return (_jsx(Dialog, { title: title, on_close: on_close, wide: true, buttons: _jsxs(_Fragment, { children: [_jsx("button", { onClick: () => void navigator.clipboard?.writeText(text).then(() => toast("Copied.")), children: "Copy" }), _jsx("button", { className: "accent solid", onClick: on_close, children: "Close" })] }), children: _jsx("pre", { className: "output", children: text }) }));
|
|
43
48
|
}
|
|
44
49
|
let open = [];
|
|
45
50
|
let next_id = 1;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { type MouseEvent, type ReactNode } from "react";
|
|
2
|
+
import type { Tone } from "./badges.tsx";
|
|
3
|
+
export { moved } from "../tables.ts";
|
|
4
|
+
interface SectionProps {
|
|
5
|
+
title: ReactNode;
|
|
6
|
+
tools?: ReactNode;
|
|
7
|
+
class_name?: string;
|
|
8
|
+
on_context_menu?: (event: MouseEvent<HTMLElement>) => void;
|
|
9
|
+
children?: ReactNode;
|
|
10
|
+
}
|
|
11
|
+
export declare function Section({ title, tools, class_name, on_context_menu, children }: SectionProps): import("react").JSX.Element;
|
|
12
|
+
export type Fact = [label: string, value: ReactNode, class_name?: string];
|
|
13
|
+
export declare function Facts({ rows, layout }: {
|
|
14
|
+
rows: (Fact | null | false)[];
|
|
15
|
+
layout?: "grid" | "rows";
|
|
16
|
+
}): import("react").JSX.Element;
|
|
17
|
+
export declare function Stack({ tight, children }: {
|
|
18
|
+
tight?: boolean;
|
|
19
|
+
children: ReactNode;
|
|
20
|
+
}): import("react").JSX.Element;
|
|
21
|
+
interface TileProps {
|
|
22
|
+
label: string;
|
|
23
|
+
value: ReactNode;
|
|
24
|
+
tone?: Tone;
|
|
25
|
+
class_name?: string | null;
|
|
26
|
+
href?: string;
|
|
27
|
+
value_first?: boolean;
|
|
28
|
+
}
|
|
29
|
+
export declare function Tile({ label, value, tone, class_name, href, value_first }: TileProps): import("react").JSX.Element;
|
|
30
|
+
export declare function Tiles({ children }: {
|
|
31
|
+
children: ReactNode;
|
|
32
|
+
}): import("react").JSX.Element;
|
|
33
|
+
interface CardProps {
|
|
34
|
+
title: ReactNode;
|
|
35
|
+
subtitle?: ReactNode;
|
|
36
|
+
badge?: ReactNode;
|
|
37
|
+
class_name?: string;
|
|
38
|
+
href?: string;
|
|
39
|
+
on_click?: () => void;
|
|
40
|
+
children?: ReactNode;
|
|
41
|
+
}
|
|
42
|
+
export declare function Card({ title, subtitle, badge, class_name, href, on_click, children }: CardProps): import("react").JSX.Element;
|
|
43
|
+
export declare function Cards({ children }: {
|
|
44
|
+
children: ReactNode;
|
|
45
|
+
}): import("react").JSX.Element;
|
|
46
|
+
export interface TabItem<Id extends string> {
|
|
47
|
+
id: Id;
|
|
48
|
+
label: string;
|
|
49
|
+
href?: string;
|
|
50
|
+
}
|
|
51
|
+
export declare function Tabs<Id extends string>({ items, active, label, on_select }: {
|
|
52
|
+
items: TabItem<Id>[];
|
|
53
|
+
active: Id;
|
|
54
|
+
label?: string;
|
|
55
|
+
on_select?: (id: Id) => void;
|
|
56
|
+
}): import("react").JSX.Element;
|
|
57
|
+
export declare function Crumbs({ items }: {
|
|
58
|
+
items: [label: string, href: string | null][];
|
|
59
|
+
}): import("react").JSX.Element;
|
|
60
|
+
export declare function Toolbar({ children }: {
|
|
61
|
+
children: ReactNode;
|
|
62
|
+
}): import("react").JSX.Element;
|
|
63
|
+
interface Step {
|
|
64
|
+
text: string;
|
|
65
|
+
href: string | null;
|
|
66
|
+
}
|
|
67
|
+
export declare function Stepper({ label, previous, position, next }: {
|
|
68
|
+
label: string;
|
|
69
|
+
previous: Step;
|
|
70
|
+
position: ReactNode;
|
|
71
|
+
next: Step;
|
|
72
|
+
}): import("react").JSX.Element;
|
|
73
|
+
export declare function SettingsGroup({ heading, hint, children }: {
|
|
74
|
+
heading: ReactNode;
|
|
75
|
+
hint?: ReactNode;
|
|
76
|
+
children: ReactNode;
|
|
77
|
+
}): import("react").JSX.Element;
|
|
78
|
+
export declare function SettingRow({ label, hint, as_label, class_name, children }: {
|
|
79
|
+
label: ReactNode;
|
|
80
|
+
hint?: ReactNode;
|
|
81
|
+
as_label?: boolean;
|
|
82
|
+
class_name?: string;
|
|
83
|
+
children: ReactNode;
|
|
84
|
+
}): import("react").JSX.Element;
|
|
85
|
+
interface RowActionsProps {
|
|
86
|
+
index: number;
|
|
87
|
+
count: number;
|
|
88
|
+
on_move: (from: number, to: number) => void;
|
|
89
|
+
on_remove?: () => void;
|
|
90
|
+
remove_label: string;
|
|
91
|
+
}
|
|
92
|
+
export declare function RowActions({ index, count, on_move, on_remove, remove_label }: RowActionsProps): import("react").JSX.Element;
|
|
93
|
+
interface SelectionBarProps {
|
|
94
|
+
count: number;
|
|
95
|
+
total: number;
|
|
96
|
+
on_clear: () => void;
|
|
97
|
+
on_select_all: () => void;
|
|
98
|
+
children: ReactNode;
|
|
99
|
+
}
|
|
100
|
+
export declare function SelectionBar({ count, total, on_clear, on_select_all, children }: SelectionBarProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// The pieces a page is laid out with: panels, facts, tiles, cards, tabs, breadcrumbs, toolbars, steppers, settings rows and list row buttons.
|
|
3
|
+
import { Fragment } from "react";
|
|
4
|
+
export { moved } from "../tables.js";
|
|
5
|
+
export function Section({ title, tools, class_name, on_context_menu, children }) {
|
|
6
|
+
return (_jsxs("section", { className: class_name ? `panel ${class_name}` : "panel", onContextMenu: on_context_menu, children: [_jsxs("div", { className: "panel-head", children: [_jsx("h2", { children: title }), tools && _jsx("div", { className: "panel-tools", children: tools })] }), children] }));
|
|
7
|
+
}
|
|
8
|
+
// Labelled values in a grid of tiles, or as rows of label and value for long values such as paths.
|
|
9
|
+
export function Facts({ rows, layout = "grid" }) {
|
|
10
|
+
return (_jsx("dl", { className: layout === "rows" ? "facts facts-rows" : "facts", children: rows.filter((row) => Boolean(row)).map(([label, value, class_name]) => (_jsxs("div", { children: [_jsx("dt", { children: label }), _jsx("dd", { className: class_name, children: value ?? "—" })] }, label))) }));
|
|
11
|
+
}
|
|
12
|
+
// A stack of panels or blocks spaced by its gap, tighter when tight.
|
|
13
|
+
export function Stack({ tight = false, children }) {
|
|
14
|
+
return _jsx("div", { className: tight ? "stack tight" : "stack", children: children });
|
|
15
|
+
}
|
|
16
|
+
// One headline figure and its label.
|
|
17
|
+
export function Tile({ label, value, tone, class_name, href, value_first = false }) {
|
|
18
|
+
const tile_class = ["tile", tone && tone !== "none" && `tile-${tone}`, class_name].filter(Boolean).join(" ");
|
|
19
|
+
const label_part = _jsx("div", { className: "tile-label", children: label });
|
|
20
|
+
const value_part = _jsx("div", { className: "tile-value", children: value });
|
|
21
|
+
const children = value_first ? _jsxs(_Fragment, { children: [value_part, label_part] }) : _jsxs(_Fragment, { children: [label_part, value_part] });
|
|
22
|
+
return href ? _jsx("a", { className: tile_class, href: href, children: children }) : _jsx("div", { className: tile_class, children: children });
|
|
23
|
+
}
|
|
24
|
+
export function Tiles({ children }) {
|
|
25
|
+
return _jsx("div", { className: "tiles", children: children });
|
|
26
|
+
}
|
|
27
|
+
// A titled card for one thing among many, laid out in a grid of Cards.
|
|
28
|
+
export function Card({ title, subtitle, badge, class_name, href, on_click, children }) {
|
|
29
|
+
const card_class = class_name ? `card ${class_name}` : "card";
|
|
30
|
+
const body = (_jsxs(_Fragment, { children: [_jsxs("div", { className: "card-head", children: [_jsx("strong", { children: title }), badge] }), subtitle && _jsx("div", { className: "muted card-host", children: subtitle }), children] }));
|
|
31
|
+
if (href)
|
|
32
|
+
return _jsx("a", { className: card_class, href: href, children: body });
|
|
33
|
+
if (on_click)
|
|
34
|
+
return _jsx("button", { type: "button", className: card_class, onClick: on_click, children: body });
|
|
35
|
+
return _jsx("div", { className: card_class, children: body });
|
|
36
|
+
}
|
|
37
|
+
export function Cards({ children }) {
|
|
38
|
+
return _jsx("div", { className: "grid", children: children });
|
|
39
|
+
}
|
|
40
|
+
export function Tabs({ items, active, label = "Sections", on_select }) {
|
|
41
|
+
return (_jsx("nav", { className: "tabs", "aria-label": label, children: items.map((item) => {
|
|
42
|
+
const tab_class = item.id === active ? "tab active" : "tab";
|
|
43
|
+
const current = item.id === active ? "page" : undefined;
|
|
44
|
+
return item.href
|
|
45
|
+
? _jsx("a", { className: tab_class, href: item.href, "aria-current": current, children: item.label }, item.id)
|
|
46
|
+
: _jsx("button", { type: "button", className: tab_class, "aria-current": current, onClick: () => on_select?.(item.id), children: item.label }, item.id);
|
|
47
|
+
}) }));
|
|
48
|
+
}
|
|
49
|
+
export function Crumbs({ items }) {
|
|
50
|
+
return (_jsx("nav", { className: "crumbs", "aria-label": "Breadcrumb", children: items.map(([label, href], index) => (_jsxs(Fragment, { children: [index > 0 && _jsx("span", { className: "muted", "aria-hidden": "true", children: " / " }), href ? _jsx("a", { href: href, children: label }) : _jsx("span", { "aria-current": "page", children: label })] }, index))) }));
|
|
51
|
+
}
|
|
52
|
+
// ── Toolbars ─────────────────────────────────────────────────────────────────
|
|
53
|
+
export function Toolbar({ children }) {
|
|
54
|
+
return _jsx("div", { className: "toolbar", children: children });
|
|
55
|
+
}
|
|
56
|
+
// Steps to the item before or after this one, each a link or a dead button when there is none, with where this one stands between.
|
|
57
|
+
export function Stepper({ label, previous, position, next }) {
|
|
58
|
+
const step = (target) => target.href === null
|
|
59
|
+
? _jsx("button", { disabled: true, children: target.text })
|
|
60
|
+
: _jsx("a", { className: "button", href: target.href, children: target.text });
|
|
61
|
+
return _jsxs("div", { className: "stepper", role: "group", "aria-label": label, children: [step(previous), _jsx("span", { className: "muted", children: position }), step(next)] });
|
|
62
|
+
}
|
|
63
|
+
// ── Settings ─────────────────────────────────────────────────────────────────
|
|
64
|
+
export function SettingsGroup({ heading, hint, children }) {
|
|
65
|
+
return (_jsxs("section", { className: "settings-group", children: [_jsx("h3", { className: "settings-heading", children: heading }), hint && _jsx("p", { className: "setting-hint", children: hint }), children] }));
|
|
66
|
+
}
|
|
67
|
+
// A setting's name and hint on one side and its control on the other; as a label, clicking anywhere works its one control.
|
|
68
|
+
export function SettingRow({ label, hint, as_label = false, class_name, children }) {
|
|
69
|
+
const row_class = class_name ? `setting-row ${class_name}` : "setting-row";
|
|
70
|
+
const text = (_jsxs("span", { className: "setting-text", children: [_jsx("span", { className: "setting-label", children: label }), hint && _jsx("span", { className: "setting-hint", children: hint })] }));
|
|
71
|
+
return as_label ? _jsxs("label", { className: row_class, children: [text, children] }) : _jsxs("div", { className: row_class, children: [text, children] });
|
|
72
|
+
}
|
|
73
|
+
// The up, down and remove buttons of a row in an ordered list.
|
|
74
|
+
export function RowActions({ index, count, on_move, on_remove, remove_label }) {
|
|
75
|
+
return (_jsxs("span", { className: "row-actions", children: [_jsx("button", { type: "button", className: "icon", "aria-label": "Move up", disabled: index === 0, onClick: () => on_move(index, index - 1), children: "\u2191" }), _jsx("button", { type: "button", className: "icon", "aria-label": "Move down", disabled: index === count - 1, onClick: () => on_move(index, index + 1), children: "\u2193" }), on_remove && _jsx("button", { type: "button", className: "icon danger", "aria-label": remove_label, onClick: on_remove, children: "\u00D7" })] }));
|
|
76
|
+
}
|
|
77
|
+
// A bar floating at the foot of the page while rows are picked: how many, Select all, the actions, and a button that lets them go.
|
|
78
|
+
export function SelectionBar({ count, total, on_clear, on_select_all, children }) {
|
|
79
|
+
return (_jsxs("div", { className: "selection-bar", role: "toolbar", "aria-label": "Selected rows", children: [_jsx("button", { type: "button", className: "icon", "aria-label": "Clear selection", title: "Clear selection (Esc)", onClick: on_clear, children: "\u2715" }), _jsxs("span", { className: "selection-count", children: [count, " selected"] }), count < total && _jsx("button", { type: "button", onClick: on_select_all, children: "Select all" }), _jsx("span", { className: "selection-divider", "aria-hidden": "true" }), children] }));
|
|
80
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { type MenuItem } from "../menus.ts";
|
|
3
|
+
export type { MenuItem } from "../menus.ts";
|
|
4
|
+
interface MenuListProps {
|
|
5
|
+
items: MenuItem[];
|
|
6
|
+
on_close: () => void;
|
|
7
|
+
}
|
|
8
|
+
export declare function MenuList({ items, on_close }: MenuListProps): import("react").JSX.Element;
|
|
9
|
+
interface ContextMenuProps {
|
|
10
|
+
x: number;
|
|
11
|
+
y: number;
|
|
12
|
+
items: MenuItem[];
|
|
13
|
+
on_close: () => void;
|
|
14
|
+
label?: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function ContextMenu({ x, y, items, on_close, label }: ContextMenuProps): import("react").JSX.Element;
|
|
17
|
+
export declare function open_context_menu(event: {
|
|
18
|
+
clientX: number;
|
|
19
|
+
clientY: number;
|
|
20
|
+
preventDefault(): void;
|
|
21
|
+
}, items: MenuItem[]): void;
|
|
22
|
+
export declare function ContextMenus(): import("react").JSX.Element | null;
|
|
23
|
+
interface PopoverProps {
|
|
24
|
+
anchor?: HTMLElement | null;
|
|
25
|
+
x?: number;
|
|
26
|
+
y?: number;
|
|
27
|
+
label: string;
|
|
28
|
+
on_close: () => void;
|
|
29
|
+
children: ReactNode;
|
|
30
|
+
}
|
|
31
|
+
export declare function Popover({ anchor, x, y, label, on_close, children }: PopoverProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// Menus and popovers on the native popover: closed by Escape or a click elsewhere, and here also by scrolling or leaving the window.
|
|
3
|
+
import { Fragment, useEffect, useId, useLayoutEffect, useRef, useState } from "react";
|
|
4
|
+
import { item_after_key, item_starting_with, on_screen_position } from "../menus.js";
|
|
5
|
+
// Shows the element as a popover while it is rendered, calling the latest on_close once it closes for any reason.
|
|
6
|
+
function use_popover(on_close, opener) {
|
|
7
|
+
const element = useRef(null);
|
|
8
|
+
const close = useRef(on_close);
|
|
9
|
+
close.current = on_close;
|
|
10
|
+
const focused_before = useRef(opener ?? document.activeElement);
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
const popover = element.current;
|
|
13
|
+
const hide = () => { if (popover.matches(":popover-open"))
|
|
14
|
+
popover.hidePopover(); };
|
|
15
|
+
const toggled = (event) => { if (event.newState === "closed")
|
|
16
|
+
close.current(); };
|
|
17
|
+
const outside_scroll = (event) => { if (!popover.contains(event.target))
|
|
18
|
+
hide(); };
|
|
19
|
+
popover.addEventListener("toggle", toggled);
|
|
20
|
+
window.addEventListener("blur", hide);
|
|
21
|
+
window.addEventListener("scroll", outside_scroll, true);
|
|
22
|
+
return () => {
|
|
23
|
+
popover.removeEventListener("toggle", toggled);
|
|
24
|
+
window.removeEventListener("blur", hide);
|
|
25
|
+
window.removeEventListener("scroll", outside_scroll, true);
|
|
26
|
+
};
|
|
27
|
+
}, []);
|
|
28
|
+
const return_focus = () => {
|
|
29
|
+
const before = focused_before.current;
|
|
30
|
+
if (before?.isConnected && before !== document.body)
|
|
31
|
+
before.focus();
|
|
32
|
+
};
|
|
33
|
+
const dismiss = () => {
|
|
34
|
+
const popover = element.current;
|
|
35
|
+
return_focus();
|
|
36
|
+
if (popover?.matches(":popover-open"))
|
|
37
|
+
popover.hidePopover();
|
|
38
|
+
else
|
|
39
|
+
close.current();
|
|
40
|
+
};
|
|
41
|
+
return { element, dismiss, return_focus };
|
|
42
|
+
}
|
|
43
|
+
// Shows the popover at the point, flipped or pushed back so it stays on screen.
|
|
44
|
+
function show_at(popover, x, y) {
|
|
45
|
+
if (!popover.matches(":popover-open"))
|
|
46
|
+
popover.showPopover();
|
|
47
|
+
const size = popover.getBoundingClientRect();
|
|
48
|
+
const position = on_screen_position(x, y, size.width, size.height);
|
|
49
|
+
popover.style.left = `${position.left}px`;
|
|
50
|
+
popover.style.top = `${position.top}px`;
|
|
51
|
+
}
|
|
52
|
+
// ── Keys ─────────────────────────────────────────────────────────────────────
|
|
53
|
+
const item_selector = '[role^="menuitem"]:not([aria-disabled="true"])';
|
|
54
|
+
const typeahead_pause_in_milliseconds = 600;
|
|
55
|
+
// Arrows, Home and End move between items, typing jumps to one by its first letters, Enter or Space runs it, and Escape closes.
|
|
56
|
+
function use_menu_keys(container, dismiss, whole_menu) {
|
|
57
|
+
const typed = useRef({ text: "", at: 0 });
|
|
58
|
+
return (event) => {
|
|
59
|
+
if (event.key === "Escape") {
|
|
60
|
+
event.preventDefault();
|
|
61
|
+
event.stopPropagation();
|
|
62
|
+
dismiss();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (whole_menu && event.key === "Tab")
|
|
66
|
+
return dismiss();
|
|
67
|
+
const items = [...container.current.querySelectorAll(item_selector)];
|
|
68
|
+
const current = items.indexOf(document.activeElement);
|
|
69
|
+
// In a popover the keys belong to its fields until focus is on one of its items.
|
|
70
|
+
if (!whole_menu && current < 0)
|
|
71
|
+
return;
|
|
72
|
+
if ((event.key === "Enter" || event.key === " ") && current >= 0) {
|
|
73
|
+
event.preventDefault();
|
|
74
|
+
items[current].click();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const moved = item_after_key(event.key, current, items.length);
|
|
78
|
+
if (moved !== null) {
|
|
79
|
+
event.preventDefault();
|
|
80
|
+
items[moved].focus();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (event.key.length !== 1 || event.ctrlKey || event.metaKey || event.altKey)
|
|
84
|
+
return;
|
|
85
|
+
const now = Date.now();
|
|
86
|
+
typed.current = { text: now - typed.current.at > typeahead_pause_in_milliseconds ? event.key : typed.current.text + event.key, at: now };
|
|
87
|
+
const labels = items.map((item) => item.querySelector(".menu-label")?.textContent ?? item.textContent ?? "");
|
|
88
|
+
const found = item_starting_with(labels, typed.current.text, current);
|
|
89
|
+
if (found !== null) {
|
|
90
|
+
event.preventDefault();
|
|
91
|
+
items[found].focus();
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
// A menu's buttons, also for use inside a Popover, such as a column filter's choices.
|
|
96
|
+
export function MenuList({ items, on_close }) {
|
|
97
|
+
return (_jsx(_Fragment, { children: items.map((item) => (_jsxs(Fragment, { children: [item.separator_before && _jsx("div", { className: "menu-separator", role: "separator" }), _jsxs("button", { type: "button", className: item.danger ? "menu-item danger" : "menu-item", role: item.checked === undefined ? "menuitem" : item.radio ? "menuitemradio" : "menuitemcheckbox", "aria-checked": item.checked, "aria-disabled": item.disabled || undefined, tabIndex: -1, onClick: () => {
|
|
98
|
+
if (item.disabled)
|
|
99
|
+
return;
|
|
100
|
+
if (!item.keep_open)
|
|
101
|
+
on_close();
|
|
102
|
+
item.run();
|
|
103
|
+
}, children: [_jsx("span", { className: "menu-label", children: item.label }), item.hint && _jsx("span", { className: "menu-hint", children: item.hint })] })] }, item.label))) }));
|
|
104
|
+
}
|
|
105
|
+
export function ContextMenu({ x, y, items, on_close, label }) {
|
|
106
|
+
const { element: menu, dismiss, return_focus } = use_popover(on_close);
|
|
107
|
+
const keys = use_menu_keys(menu, dismiss, true);
|
|
108
|
+
useLayoutEffect(() => {
|
|
109
|
+
show_at(menu.current, x, y);
|
|
110
|
+
menu.current.querySelector(item_selector)?.focus();
|
|
111
|
+
}, [x, y]);
|
|
112
|
+
return (_jsx("div", { ref: menu, popover: "auto", className: "menu", role: "menu", "aria-label": label, tabIndex: -1, onKeyDown: keys, children: _jsx(MenuList, { items: items, on_close: () => { return_focus(); on_close(); } }) }));
|
|
113
|
+
}
|
|
114
|
+
let open_menu = null;
|
|
115
|
+
let next_menu_id = 1;
|
|
116
|
+
const menu_listeners = new Set();
|
|
117
|
+
function publish_menu(menu) {
|
|
118
|
+
open_menu = menu;
|
|
119
|
+
for (const listener of menu_listeners)
|
|
120
|
+
listener(open_menu);
|
|
121
|
+
}
|
|
122
|
+
// Opens a menu where the event happened, such as a right-click, replacing any menu already open.
|
|
123
|
+
export function open_context_menu(event, items) {
|
|
124
|
+
event.preventDefault();
|
|
125
|
+
publish_menu({ id: next_menu_id++, x: event.clientX, y: event.clientY, items });
|
|
126
|
+
}
|
|
127
|
+
// Where menus opened from anywhere are drawn; the Shell holds one.
|
|
128
|
+
export function ContextMenus() {
|
|
129
|
+
const [menu, set_menu] = useState(open_menu);
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
menu_listeners.add(set_menu);
|
|
132
|
+
return () => void menu_listeners.delete(set_menu);
|
|
133
|
+
}, []);
|
|
134
|
+
if (!menu)
|
|
135
|
+
return null;
|
|
136
|
+
return _jsx(ContextMenu, { x: menu.x, y: menu.y, items: menu.items, on_close: () => { if (open_menu?.id === menu.id)
|
|
137
|
+
publish_menu(null); } }, menu.id);
|
|
138
|
+
}
|
|
139
|
+
// Holds any content, such as a column's filter, with its first field ready to type into and its menu items reachable by arrow keys.
|
|
140
|
+
export function Popover({ anchor, x = 0, y = 0, label, on_close, children }) {
|
|
141
|
+
const { element: popover, dismiss } = use_popover(on_close, anchor);
|
|
142
|
+
const keys = use_menu_keys(popover, dismiss, false);
|
|
143
|
+
const anchor_name = `--popover-${useId().replace(/[^\w-]/g, "")}`;
|
|
144
|
+
useLayoutEffect(() => {
|
|
145
|
+
const element = popover.current;
|
|
146
|
+
if (anchor) {
|
|
147
|
+
// The anchor's name ties it to this popover, which CSS then hangs below it, flipping where there is no room.
|
|
148
|
+
anchor.style.setProperty("anchor-name", anchor_name);
|
|
149
|
+
element.style.setProperty("position-anchor", anchor_name);
|
|
150
|
+
element.showPopover();
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
show_at(element, x, y);
|
|
154
|
+
}
|
|
155
|
+
element.querySelector(`input, select, textarea, ${item_selector}, button`)?.focus();
|
|
156
|
+
return () => void anchor?.style.removeProperty("anchor-name");
|
|
157
|
+
}, [anchor, x, y]);
|
|
158
|
+
return (_jsx("div", { ref: popover, popover: "auto", className: anchor ? "popover anchored" : "popover", role: "dialog", "aria-label": label, onKeyDown: keys, children: children }));
|
|
159
|
+
}
|