@x-tech-laboratories/x-libs 0.3.0 → 0.4.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 +11 -8
- package/dist/web/menus.d.ts +0 -7
- package/dist/web/menus.js +2 -87
- package/dist/web/preact/badges.d.ts +26 -0
- package/dist/web/preact/badges.js +28 -0
- package/dist/web/preact/controls.d.ts +14 -1
- package/dist/web/preact/controls.js +14 -7
- package/dist/web/preact/dialog.d.ts +25 -1
- package/dist/web/preact/dialog.js +56 -3
- package/dist/web/preact/layout.d.ts +37 -37
- package/dist/web/preact/layout.js +27 -25
- package/dist/web/preact/menus.d.ts +4 -3
- package/dist/web/preact/menus.js +56 -15
- package/dist/web/preact/shell.d.ts +27 -0
- package/dist/web/preact/shell.js +63 -0
- package/dist/web/preact/states.d.ts +27 -0
- package/dist/web/preact/states.js +48 -0
- package/dist/web/preact/table.d.ts +6 -1
- package/dist/web/preact/table.js +8 -1
- package/dist/web/preact/time.d.ts +4 -0
- package/dist/web/preact/time.js +9 -0
- package/dist/web/{toasts.d.ts → preact/toasts.d.ts} +2 -1
- package/dist/web/preact/toasts.js +34 -0
- package/dist/web/states.d.ts +1 -8
- package/dist/web/states.js +6 -43
- package/dist/web/tables.d.ts +4 -14
- package/dist/web/tables.js +1 -27
- package/dist/web/theme.d.ts +1 -1
- package/dist/web/theme.js +4 -1
- package/package.json +1 -1
- package/dist/web/badges.d.ts +0 -10
- package/dist/web/badges.js +0 -30
- package/dist/web/controls.d.ts +0 -6
- package/dist/web/controls.js +0 -55
- package/dist/web/dialogs.d.ts +0 -18
- package/dist/web/dialogs.js +0 -49
- package/dist/web/layout.d.ts +0 -28
- package/dist/web/layout.js +0 -53
- package/dist/web/shell.d.ts +0 -7
- package/dist/web/shell.js +0 -51
- package/dist/web/toasts.js +0 -22
package/README.md
CHANGED
|
@@ -18,11 +18,12 @@ Code the X-apps share, published as one npm package, `@x-tech-laboratories/x-lib
|
|
|
18
18
|
| `server/passwords` | scrypt hashes and the `name:hash` accounts file. |
|
|
19
19
|
| `server/hash_password` | The `hash-password` command. |
|
|
20
20
|
| `web/dom`, `web/format`, `web/router`, `web/theme` | The page's element builder, formatters, hash router and theme choice. |
|
|
21
|
-
| `web/
|
|
22
|
-
| `web/
|
|
23
|
-
| `web/
|
|
24
|
-
| `web/
|
|
25
|
-
| `web/preact
|
|
21
|
+
| `web/states`, `web/tables`, `web/menus`, `web/split` | `LoginNeeded`; table sorting; menu items and on-screen placement; pane sizing. |
|
|
22
|
+
| `web/preact/shell` | `Shell`: top bar, sidebar, theme button, toasts and dialogs; `use_route`. |
|
|
23
|
+
| `web/preact/layout`, `badges`, `states` | Panels, facts, tiles, cards, tabs, crumbs, steppers, settings rows; badges and chips; `use_resource` and its loading states. |
|
|
24
|
+
| `web/preact/time` | `Age`: a time as how long ago, with the full stamp on hover. |
|
|
25
|
+
| `web/preact/controls`, `table` | Segmented choices, button groups, switches, search boxes, selects; sortable tables that turn into cards on phones, and a pager. |
|
|
26
|
+
| `web/preact/toasts`, `dialog`, `menus`, `split` | Toasts with a link or Undo; modal, confirm and text dialogs openable from anywhere; context menus and popovers; resize handles and splits. |
|
|
26
27
|
|
|
27
28
|
```ts
|
|
28
29
|
import { AgentHub } from "@x-tech-laboratories/x-libs/server/agents/hub";
|
|
@@ -37,11 +38,13 @@ which an app imports once:
|
|
|
37
38
|
```ts
|
|
38
39
|
import "@x-tech-laboratories/halon/halon-tokens.css";
|
|
39
40
|
import "@x-tech-laboratories/halon/halon.css";
|
|
40
|
-
import {
|
|
41
|
-
import {
|
|
41
|
+
import { DataTable } from "@x-tech-laboratories/x-libs/web/preact/table";
|
|
42
|
+
import { toast } from "@x-tech-laboratories/x-libs/web/preact/toasts";
|
|
42
43
|
```
|
|
43
44
|
|
|
44
|
-
|
|
45
|
+
The widgets are Preact components, so a page renders with Preact; `Shell` holds the toasts and dialogs that `toast` and
|
|
46
|
+
`confirm_dialog` open from anywhere. A request whose session has lapsed throws `LoginNeeded` from `web/states`, and
|
|
47
|
+
`use_resource` passes it on as an unhandled rejection, which the page's login listens for.
|
|
45
48
|
|
|
46
49
|
## The app and its tasks
|
|
47
50
|
|
package/dist/web/menus.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { type Child } from "./dom.ts";
|
|
2
1
|
export interface MenuItem {
|
|
3
2
|
label: string;
|
|
4
3
|
run: () => void;
|
|
@@ -8,13 +7,7 @@ export interface MenuItem {
|
|
|
8
7
|
separator_before?: boolean;
|
|
9
8
|
checked?: boolean;
|
|
10
9
|
}
|
|
11
|
-
export declare function menu_item_class(item: MenuItem): string;
|
|
12
|
-
export declare function menu_item_role(item: MenuItem): "menuitem" | "menuitemcheckbox";
|
|
13
10
|
export declare function on_screen_position(x: number, y: number, width: number, height: number, room_width?: number, room_height?: number): {
|
|
14
11
|
left: number;
|
|
15
12
|
top: number;
|
|
16
13
|
};
|
|
17
|
-
export declare function place_on_screen(element: HTMLElement, x: number, y: number): void;
|
|
18
|
-
export declare function dismiss_on_outside(element: HTMLElement, close: () => void): () => void;
|
|
19
|
-
export declare function open_context_menu(x: number, y: number, items: MenuItem[], on_close?: () => void): () => void;
|
|
20
|
-
export declare function open_popover(x: number, y: number, content: Child, label: string, on_close?: () => void): () => void;
|
package/dist/web/menus.js
CHANGED
|
@@ -1,14 +1,6 @@
|
|
|
1
|
-
//
|
|
2
|
-
import { h } from "./dom.js";
|
|
3
|
-
export function menu_item_class(item) {
|
|
4
|
-
return item.danger ? "menu-item danger" : "menu-item";
|
|
5
|
-
}
|
|
6
|
-
export function menu_item_role(item) {
|
|
7
|
-
return item.checked === undefined ? "menuitem" : "menuitemcheckbox";
|
|
8
|
-
}
|
|
9
|
-
// ── Placing ──────────────────────────────────────────────────────────────────
|
|
1
|
+
// What a menu item holds, and where a menu opened at a point goes so it stays on screen.
|
|
10
2
|
const edge_gap = 4;
|
|
11
|
-
//
|
|
3
|
+
// Flipped to the point's other side near an edge, or pushed back from the edge when neither side has room.
|
|
12
4
|
export function on_screen_position(x, y, width, height, room_width = window.innerWidth, room_height = window.innerHeight) {
|
|
13
5
|
const place = (start, size, room) => {
|
|
14
6
|
if (start + size + edge_gap <= room)
|
|
@@ -19,80 +11,3 @@ export function on_screen_position(x, y, width, height, room_width = window.inne
|
|
|
19
11
|
};
|
|
20
12
|
return { left: place(x, width, room_width), top: place(y, height, room_height) };
|
|
21
13
|
}
|
|
22
|
-
export function place_on_screen(element, x, y) {
|
|
23
|
-
const size = element.getBoundingClientRect();
|
|
24
|
-
const position = on_screen_position(x, y, size.width, size.height);
|
|
25
|
-
element.style.left = `${position.left}px`;
|
|
26
|
-
element.style.top = `${position.top}px`;
|
|
27
|
-
}
|
|
28
|
-
// ── Dismissing ───────────────────────────────────────────────────────────────
|
|
29
|
-
// Calls close on Escape, a press or right-click outside the element, scrolling outside it, or the window losing focus; returns a function that stops listening.
|
|
30
|
-
export function dismiss_on_outside(element, close) {
|
|
31
|
-
const outside = (event) => {
|
|
32
|
-
if (!element.contains(event.target))
|
|
33
|
-
close();
|
|
34
|
-
};
|
|
35
|
-
const escape = (event) => {
|
|
36
|
-
if (event.key !== "Escape")
|
|
37
|
-
return;
|
|
38
|
-
event.preventDefault();
|
|
39
|
-
event.stopPropagation();
|
|
40
|
-
close();
|
|
41
|
-
};
|
|
42
|
-
// Listening at once would catch the very click that opened the element, which would close it straight away.
|
|
43
|
-
const attach = setTimeout(() => {
|
|
44
|
-
document.addEventListener("pointerdown", outside);
|
|
45
|
-
document.addEventListener("contextmenu", outside);
|
|
46
|
-
}, 0);
|
|
47
|
-
window.addEventListener("keydown", escape);
|
|
48
|
-
window.addEventListener("blur", close);
|
|
49
|
-
window.addEventListener("scroll", outside, true);
|
|
50
|
-
return () => {
|
|
51
|
-
clearTimeout(attach);
|
|
52
|
-
document.removeEventListener("pointerdown", outside);
|
|
53
|
-
document.removeEventListener("contextmenu", outside);
|
|
54
|
-
window.removeEventListener("keydown", escape);
|
|
55
|
-
window.removeEventListener("blur", close);
|
|
56
|
-
window.removeEventListener("scroll", outside, true);
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
// ── Opening ──────────────────────────────────────────────────────────────────
|
|
60
|
-
// Puts the element on the page at the point and wires its closing; returns the function that closes it.
|
|
61
|
-
function open_at(element, x, y, on_close) {
|
|
62
|
-
let stop = () => { };
|
|
63
|
-
const close = () => {
|
|
64
|
-
stop();
|
|
65
|
-
element.remove();
|
|
66
|
-
on_close?.();
|
|
67
|
-
};
|
|
68
|
-
document.body.append(element);
|
|
69
|
-
place_on_screen(element, x, y);
|
|
70
|
-
stop = dismiss_on_outside(element, close);
|
|
71
|
-
return close;
|
|
72
|
-
}
|
|
73
|
-
// A right-click menu at the point, such as a mouse event's clientX and clientY.
|
|
74
|
-
export function open_context_menu(x, y, items, on_close) {
|
|
75
|
-
const menu = h("div", { class: "menu", role: "menu", tabindex: -1 });
|
|
76
|
-
const close = open_at(menu, x, y, on_close);
|
|
77
|
-
menu.append(...items.flatMap((item) => [
|
|
78
|
-
item.separator_before ? h("div", { class: "menu-separator", role: "separator" }) : null,
|
|
79
|
-
h("button", {
|
|
80
|
-
type: "button", class: menu_item_class(item), role: menu_item_role(item),
|
|
81
|
-
"aria-checked": item.checked === undefined ? null : String(item.checked), disabled: item.disabled ?? false,
|
|
82
|
-
onclick: () => {
|
|
83
|
-
close();
|
|
84
|
-
item.run();
|
|
85
|
-
},
|
|
86
|
-
}, h("span", { class: "menu-label" }, item.label), item.hint && h("span", { class: "menu-hint" }, item.hint)),
|
|
87
|
-
].filter((node) => node !== null)));
|
|
88
|
-
place_on_screen(menu, x, y);
|
|
89
|
-
menu.querySelector(".menu-item:not(:disabled)")?.focus();
|
|
90
|
-
return close;
|
|
91
|
-
}
|
|
92
|
-
// A popover holding any content, such as a column's filter, with its first field ready to type into.
|
|
93
|
-
export function open_popover(x, y, content, label, on_close) {
|
|
94
|
-
const popover = h("div", { class: "popover", role: "dialog", "aria-label": label }, content);
|
|
95
|
-
const close = open_at(popover, x, y, on_close);
|
|
96
|
-
popover.querySelector("input, select, textarea, button")?.focus();
|
|
97
|
-
return close;
|
|
98
|
-
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ComponentChildren } from "preact";
|
|
2
|
+
export type Tone = "good" | "warn" | "bad" | "info" | "none";
|
|
3
|
+
export declare function Badge({ tone, title, children }: {
|
|
4
|
+
tone?: Tone;
|
|
5
|
+
title?: string;
|
|
6
|
+
children: ComponentChildren;
|
|
7
|
+
}): import("preact").JSX.Element;
|
|
8
|
+
export declare function StateBadge<State extends string>({ labels, state, title }: {
|
|
9
|
+
labels: Record<State, [text: string, tone: Tone]>;
|
|
10
|
+
state: State;
|
|
11
|
+
title?: string;
|
|
12
|
+
}): import("preact").JSX.Element;
|
|
13
|
+
export declare function Chip({ title, on_click, class_name, children }: {
|
|
14
|
+
title?: string;
|
|
15
|
+
on_click?: () => void;
|
|
16
|
+
class_name?: string;
|
|
17
|
+
children: ComponentChildren;
|
|
18
|
+
}): import("preact").JSX.Element;
|
|
19
|
+
export declare function Chips({ inline, children }: {
|
|
20
|
+
inline?: boolean;
|
|
21
|
+
children: ComponentChildren;
|
|
22
|
+
}): import("preact").JSX.Element;
|
|
23
|
+
export declare function Verdict({ ok, label }: {
|
|
24
|
+
ok: boolean | null | undefined;
|
|
25
|
+
label: string;
|
|
26
|
+
}): import("preact").JSX.Element;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
|
|
2
|
+
// ── Badges ───────────────────────────────────────────────────────────────────
|
|
3
|
+
export function Badge({ tone = "none", title, children }) {
|
|
4
|
+
return _jsx("span", { class: `badge tone-${tone}`, title: title, children: children });
|
|
5
|
+
}
|
|
6
|
+
// A badge for one of a fixed set of states, each named by its text and tone.
|
|
7
|
+
export function StateBadge({ labels, state, title }) {
|
|
8
|
+
const [text, tone] = labels[state];
|
|
9
|
+
return _jsx(Badge, { tone: tone, title: title, children: text });
|
|
10
|
+
}
|
|
11
|
+
// ── Chips ────────────────────────────────────────────────────────────────────
|
|
12
|
+
// A chip, or a button chip that picks what it names when it has on_click, such as a tag to filter by.
|
|
13
|
+
export function Chip({ title, on_click, class_name, children }) {
|
|
14
|
+
const chip_class = class_name ? `chip ${class_name}` : "chip";
|
|
15
|
+
return on_click
|
|
16
|
+
? _jsx("button", { type: "button", class: chip_class, title: title, onClick: on_click, children: children })
|
|
17
|
+
: _jsx("span", { class: chip_class, title: title, children: children });
|
|
18
|
+
}
|
|
19
|
+
// A row of chips or badges, with less space between them when they sit inside a line of text.
|
|
20
|
+
export function Chips({ inline = false, children }) {
|
|
21
|
+
return _jsx("span", { class: inline ? "chips inline-chips" : "chips", children: children });
|
|
22
|
+
}
|
|
23
|
+
// ── Verdicts ─────────────────────────────────────────────────────────────────
|
|
24
|
+
// A pass, fail or not-yet verdict, such as "checks: pass".
|
|
25
|
+
export function Verdict({ ok, label }) {
|
|
26
|
+
const known = ok !== null && ok !== undefined;
|
|
27
|
+
return _jsxs("span", { class: `verdict tone-${known ? (ok ? "good" : "bad") : "none"}`, children: [label, ": ", known ? (ok ? "pass" : "fail") : "no verdict"] });
|
|
28
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ComponentChildren } from "preact";
|
|
1
2
|
interface SegmentedProps<Value extends string> {
|
|
2
3
|
label: string;
|
|
3
4
|
choices: {
|
|
@@ -6,8 +7,13 @@ interface SegmentedProps<Value extends string> {
|
|
|
6
7
|
}[];
|
|
7
8
|
value: Value;
|
|
8
9
|
on_change: (value: Value) => void;
|
|
10
|
+
class_name?: string;
|
|
9
11
|
}
|
|
10
|
-
export declare function Segmented<Value extends string>({ label, choices, value, on_change }: SegmentedProps<Value>): import("preact").JSX.Element;
|
|
12
|
+
export declare function Segmented<Value extends string>({ label, choices, value, on_change, class_name }: SegmentedProps<Value>): import("preact").JSX.Element;
|
|
13
|
+
export declare function ButtonGroup({ label, children }: {
|
|
14
|
+
label: string;
|
|
15
|
+
children: ComponentChildren;
|
|
16
|
+
}): import("preact").JSX.Element;
|
|
11
17
|
interface SwitchProps {
|
|
12
18
|
label: string;
|
|
13
19
|
checked: boolean;
|
|
@@ -21,4 +27,11 @@ interface SearchBoxProps {
|
|
|
21
27
|
on_input: (value: string) => void;
|
|
22
28
|
}
|
|
23
29
|
export declare function SearchBox({ label, value, on_input }: SearchBoxProps): import("preact").JSX.Element;
|
|
30
|
+
interface SelectProps<Value extends string> {
|
|
31
|
+
label: string;
|
|
32
|
+
options: [value: Value, text: string][];
|
|
33
|
+
value: Value;
|
|
34
|
+
on_change: (value: Value) => void;
|
|
35
|
+
}
|
|
36
|
+
export declare function Select<Value extends string>({ label, options, value, on_change }: SelectProps<Value>): import("preact").JSX.Element;
|
|
24
37
|
export {};
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import { jsx as _jsx } from "preact/jsx-runtime";
|
|
2
|
-
|
|
3
|
-
import { useRef, useState } from "preact/hooks";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from "preact/hooks";
|
|
4
3
|
// One of a few choices side by side; the chosen one takes the accent.
|
|
5
|
-
export function Segmented({ label, choices, value, on_change }) {
|
|
6
|
-
return (_jsx("div", { class: "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))) }));
|
|
4
|
+
export function Segmented({ label, choices, value, on_change, class_name }) {
|
|
5
|
+
return (_jsx("div", { class: 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))) }));
|
|
6
|
+
}
|
|
7
|
+
// Buttons joined into one control, such as zoom out, zoom in and fit.
|
|
8
|
+
export function ButtonGroup({ label, children }) {
|
|
9
|
+
return _jsx("div", { class: "segmented", role: "group", "aria-label": label, children: children });
|
|
7
10
|
}
|
|
8
11
|
export function Switch({ label, checked, on_change, disabled = false }) {
|
|
9
12
|
const [busy, set_busy] = useState(false);
|
|
@@ -25,12 +28,16 @@ export function Switch({ label, checked, on_change, disabled = false }) {
|
|
|
25
28
|
}
|
|
26
29
|
return _jsx("input", { type: "checkbox", role: "switch", "aria-label": label, checked: checked, disabled: disabled || busy, onChange: change });
|
|
27
30
|
}
|
|
28
|
-
// A search field that hands on what was typed once typing pauses.
|
|
31
|
+
// A search field that hands on what was typed once typing pauses; the field keeps its own text, so a redraw never wipes typing.
|
|
29
32
|
export function SearchBox({ label, value, on_input }) {
|
|
30
33
|
const timer = useRef(undefined);
|
|
31
|
-
|
|
34
|
+
useEffect(() => () => clearTimeout(timer.current), []);
|
|
35
|
+
return (_jsx("input", { type: "search", class: "filter", placeholder: label, "aria-label": label, defaultValue: value, onInput: (event) => {
|
|
32
36
|
clearTimeout(timer.current);
|
|
33
37
|
const text = event.currentTarget.value;
|
|
34
38
|
timer.current = window.setTimeout(() => on_input(text), 200);
|
|
35
39
|
} }));
|
|
36
40
|
}
|
|
41
|
+
export function Select({ label, options, value, on_change }) {
|
|
42
|
+
return (_jsxs("label", { class: "field", children: [_jsx("span", { class: "field-label", children: label }), _jsx("select", { value: value, onChange: (event) => on_change(event.currentTarget.value), children: options.map(([option, text]) => _jsx("option", { value: option, children: text }, option)) })] }));
|
|
43
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ComponentChildren } from "preact";
|
|
2
2
|
interface DialogProps {
|
|
3
3
|
title: ComponentChildren;
|
|
4
4
|
on_close: () => void;
|
|
@@ -8,4 +8,28 @@ interface DialogProps {
|
|
|
8
8
|
children: ComponentChildren;
|
|
9
9
|
}
|
|
10
10
|
export declare function Dialog({ title, on_close, buttons, wide, class_name, children }: DialogProps): import("preact").JSX.Element;
|
|
11
|
+
export interface ConfirmOptions {
|
|
12
|
+
title: string;
|
|
13
|
+
message: ComponentChildren;
|
|
14
|
+
confirm_label: string;
|
|
15
|
+
busy_label?: string;
|
|
16
|
+
danger?: boolean;
|
|
17
|
+
wide?: boolean;
|
|
18
|
+
type_to_confirm?: string;
|
|
19
|
+
run: () => Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
export declare function ConfirmDialog({ options, on_close }: {
|
|
22
|
+
options: ConfirmOptions;
|
|
23
|
+
on_close: () => void;
|
|
24
|
+
}): import("preact").JSX.Element;
|
|
25
|
+
export declare function TextDialog({ title, text, on_close }: {
|
|
26
|
+
title: string;
|
|
27
|
+
text: string;
|
|
28
|
+
on_close: () => void;
|
|
29
|
+
}): import("preact").JSX.Element;
|
|
30
|
+
type Builder = (close: () => void) => ComponentChildren;
|
|
31
|
+
export declare function open_dialog(build: Builder): () => void;
|
|
32
|
+
export declare function confirm_dialog(options: ConfirmOptions): void;
|
|
33
|
+
export declare function text_dialog(title: string, text: string): void;
|
|
34
|
+
export declare function Dialogs(): import("preact").JSX.Element;
|
|
11
35
|
export {};
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
|
|
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 "preact";
|
|
4
|
+
import { useEffect, useId, useRef, useState } from "preact/hooks";
|
|
5
|
+
import { toast } from "./toasts.js";
|
|
6
|
+
// Shown while it is rendered: the page behind it is inert, focus stays inside, and Escape calls on_close.
|
|
4
7
|
export function Dialog({ title, on_close, buttons, wide = false, class_name, children }) {
|
|
5
8
|
const dialog = useRef(null);
|
|
6
9
|
const title_id = useId();
|
|
@@ -18,3 +21,53 @@ export function Dialog({ title, on_close, buttons, wide = false, class_name, chi
|
|
|
18
21
|
close.current();
|
|
19
22
|
}, children: [_jsx("h2", { id: title_id, children: title }), _jsx("div", { class: "dialog-body", children: children }), buttons && _jsx("div", { class: "dialog-buttons", children: buttons })] }));
|
|
20
23
|
}
|
|
24
|
+
export function ConfirmDialog({ options, on_close }) {
|
|
25
|
+
const [typed, set_typed] = useState("");
|
|
26
|
+
const [busy, set_busy] = useState(false);
|
|
27
|
+
const ready = options.type_to_confirm === undefined || typed === options.type_to_confirm;
|
|
28
|
+
async function confirm() {
|
|
29
|
+
set_busy(true);
|
|
30
|
+
try {
|
|
31
|
+
await options.run();
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
on_close();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
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", { class: 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", { class: "type-confirm", children: [_jsxs("span", { children: ["Type ", _jsx("strong", { children: options.type_to_confirm }), " to confirm:"] }), _jsx("input", { type: "text", class: "filter", autocomplete: "off", autoFocus: true, "aria-label": `Type ${options.type_to_confirm} to confirm`, value: typed, onInput: (event) => set_typed(event.currentTarget.value) })] }))] }));
|
|
39
|
+
}
|
|
40
|
+
// Shows a long text result, such as a preview, with a button to copy it.
|
|
41
|
+
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", { class: "accent solid", onClick: on_close, children: "Close" })] }), children: _jsx("pre", { class: "output", children: text }) }));
|
|
43
|
+
}
|
|
44
|
+
let open = [];
|
|
45
|
+
let next_id = 1;
|
|
46
|
+
const listeners = new Set();
|
|
47
|
+
function publish(dialogs) {
|
|
48
|
+
open = dialogs;
|
|
49
|
+
for (const listener of listeners)
|
|
50
|
+
listener(open);
|
|
51
|
+
}
|
|
52
|
+
// Opens what the builder makes, such as a Dialog, from an event handler; returns the function that closes it.
|
|
53
|
+
export function open_dialog(build) {
|
|
54
|
+
const id = next_id++;
|
|
55
|
+
const close = () => publish(open.filter((dialog) => dialog.id !== id));
|
|
56
|
+
publish([...open, { id, build }]);
|
|
57
|
+
return close;
|
|
58
|
+
}
|
|
59
|
+
export function confirm_dialog(options) {
|
|
60
|
+
open_dialog((close) => _jsx(ConfirmDialog, { options: options, on_close: close }));
|
|
61
|
+
}
|
|
62
|
+
export function text_dialog(title, text) {
|
|
63
|
+
open_dialog((close) => _jsx(TextDialog, { title: title, text: text, on_close: close }));
|
|
64
|
+
}
|
|
65
|
+
// Where dialogs opened from anywhere are drawn; the Shell holds one.
|
|
66
|
+
export function Dialogs() {
|
|
67
|
+
const [dialogs, set_dialogs] = useState(open);
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
listeners.add(set_dialogs);
|
|
70
|
+
return () => void listeners.delete(set_dialogs);
|
|
71
|
+
}, []);
|
|
72
|
+
return _jsx(_Fragment, { children: dialogs.map((dialog) => _jsx(Fragment, { children: dialog.build(() => publish(open.filter((other) => other.id !== dialog.id))) }, dialog.id)) });
|
|
73
|
+
}
|
|
@@ -1,23 +1,28 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import type { Tone } from "
|
|
3
|
-
export
|
|
4
|
-
interface SectionProps {
|
|
1
|
+
import { type ComponentChildren } from "preact";
|
|
2
|
+
import type { Tone } from "./badges.tsx";
|
|
3
|
+
export declare function Section({ title, tools, children }: {
|
|
5
4
|
title: ComponentChildren;
|
|
6
5
|
tools?: ComponentChildren;
|
|
7
|
-
children
|
|
8
|
-
}
|
|
9
|
-
export
|
|
6
|
+
children?: ComponentChildren;
|
|
7
|
+
}): import("preact").JSX.Element;
|
|
8
|
+
export type Fact = [label: string, value: ComponentChildren, class_name?: string];
|
|
10
9
|
export declare function Facts({ rows, layout }: {
|
|
11
|
-
rows:
|
|
10
|
+
rows: (Fact | null | false)[];
|
|
12
11
|
layout?: "grid" | "rows";
|
|
13
12
|
}): import("preact").JSX.Element;
|
|
13
|
+
export declare function Stack({ tight, children }: {
|
|
14
|
+
tight?: boolean;
|
|
15
|
+
children: ComponentChildren;
|
|
16
|
+
}): import("preact").JSX.Element;
|
|
14
17
|
interface TileProps {
|
|
15
18
|
label: string;
|
|
16
19
|
value: ComponentChildren;
|
|
17
20
|
tone?: Tone;
|
|
21
|
+
class_name?: string | null;
|
|
18
22
|
href?: string;
|
|
23
|
+
value_first?: boolean;
|
|
19
24
|
}
|
|
20
|
-
export declare function Tile({ label, value, tone, href }: TileProps): import("preact").JSX.Element;
|
|
25
|
+
export declare function Tile({ label, value, tone, class_name, href, value_first }: TileProps): import("preact").JSX.Element;
|
|
21
26
|
export declare function Tiles({ children }: {
|
|
22
27
|
children: ComponentChildren;
|
|
23
28
|
}): import("preact").JSX.Element;
|
|
@@ -34,47 +39,42 @@ export declare function Card({ title, subtitle, badge, class_name, href, on_clic
|
|
|
34
39
|
export declare function Cards({ children }: {
|
|
35
40
|
children: ComponentChildren;
|
|
36
41
|
}): import("preact").JSX.Element;
|
|
37
|
-
interface
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
export interface TabItem<Id extends string> {
|
|
43
|
+
id: Id;
|
|
44
|
+
label: string;
|
|
45
|
+
href?: string;
|
|
46
|
+
}
|
|
47
|
+
export declare function Tabs<Id extends string>({ items, active, label, on_select }: {
|
|
48
|
+
items: TabItem<Id>[];
|
|
43
49
|
active: Id;
|
|
44
50
|
label?: string;
|
|
45
51
|
on_select?: (id: Id) => void;
|
|
46
|
-
}
|
|
47
|
-
export declare function Tabs<Id extends string>({ items, active, label, on_select }: TabsProps<Id>): import("preact").JSX.Element;
|
|
48
|
-
export declare function Badge({ tone, title, children }: {
|
|
49
|
-
tone?: Tone;
|
|
50
|
-
title?: string;
|
|
51
|
-
children: ComponentChildren;
|
|
52
52
|
}): import("preact").JSX.Element;
|
|
53
|
-
export declare function
|
|
54
|
-
|
|
55
|
-
on_click?: () => void;
|
|
56
|
-
children: ComponentChildren;
|
|
53
|
+
export declare function Crumbs({ items }: {
|
|
54
|
+
items: [label: string, href: string | null][];
|
|
57
55
|
}): import("preact").JSX.Element;
|
|
58
|
-
export declare function
|
|
59
|
-
|
|
60
|
-
}): import("preact").JSX.Element;
|
|
61
|
-
export declare function Empty({ title, hint }: {
|
|
62
|
-
title: string;
|
|
63
|
-
hint?: ComponentChildren;
|
|
56
|
+
export declare function Toolbar({ children }: {
|
|
57
|
+
children: ComponentChildren;
|
|
64
58
|
}): import("preact").JSX.Element;
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
59
|
+
interface Step {
|
|
60
|
+
text: string;
|
|
61
|
+
href: string | null;
|
|
62
|
+
}
|
|
63
|
+
export declare function Stepper({ label, previous, position, next }: {
|
|
64
|
+
label: string;
|
|
65
|
+
previous: Step;
|
|
66
|
+
position: ComponentChildren;
|
|
67
|
+
next: Step;
|
|
68
68
|
}): import("preact").JSX.Element;
|
|
69
69
|
export declare function SettingsGroup({ heading, hint, children }: {
|
|
70
70
|
heading: string;
|
|
71
71
|
hint?: ComponentChildren;
|
|
72
72
|
children: ComponentChildren;
|
|
73
73
|
}): import("preact").JSX.Element;
|
|
74
|
-
|
|
74
|
+
export declare function SettingRow({ label, hint, as_label, children }: {
|
|
75
75
|
label: ComponentChildren;
|
|
76
76
|
hint?: ComponentChildren;
|
|
77
77
|
as_label?: boolean;
|
|
78
78
|
children: ComponentChildren;
|
|
79
|
-
}
|
|
80
|
-
export
|
|
79
|
+
}): import("preact").JSX.Element;
|
|
80
|
+
export {};
|
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
|
|
2
|
+
// The pieces a page is laid out with: panels, facts, tiles, cards, tabs, breadcrumbs, toolbars, steppers and settings rows.
|
|
3
|
+
import { Fragment } from "preact";
|
|
4
|
+
// ── Panels ───────────────────────────────────────────────────────────────────
|
|
2
5
|
export function Section({ title, tools, children }) {
|
|
3
6
|
return (_jsxs("section", { class: "panel", children: [_jsxs("div", { class: "panel-head", children: [_jsx("h2", { children: title }), tools && _jsx("div", { class: "panel-tools", children: tools })] }), children] }));
|
|
4
7
|
}
|
|
5
8
|
// Labelled values in a grid of tiles, or as rows of label and value for long values such as paths.
|
|
6
9
|
export function Facts({ rows, layout = "grid" }) {
|
|
7
|
-
return (_jsx("dl", { class: layout === "rows" ? "facts facts-rows" : "facts", children: rows.map(([label, value]) => (_jsxs("div", { children: [_jsx("dt", { children: label }), _jsx("dd", { children: value ?? "—" })] }, label))) }));
|
|
10
|
+
return (_jsx("dl", { class: 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", { class: class_name, children: value ?? "—" })] }, label))) }));
|
|
8
11
|
}
|
|
9
|
-
//
|
|
10
|
-
export function
|
|
11
|
-
|
|
12
|
-
|
|
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", { class: 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", { class: "tile-label", children: label });
|
|
20
|
+
const value_part = _jsx("div", { class: "tile-value", children: value });
|
|
21
|
+
const children = value_first ? _jsxs(_Fragment, { children: [value_part, label_part] }) : _jsxs(_Fragment, { children: [label_part, value_part] });
|
|
13
22
|
return href ? _jsx("a", { class: tile_class, href: href, children: children }) : _jsx("div", { class: tile_class, children: children });
|
|
14
23
|
}
|
|
15
24
|
export function Tiles({ children }) {
|
|
@@ -37,33 +46,26 @@ export function Tabs({ items, active, label = "Sections", on_select }) {
|
|
|
37
46
|
: _jsx("button", { type: "button", class: tab_class, "aria-current": current, onClick: () => on_select?.(item.id), children: item.label }, item.id);
|
|
38
47
|
}) }));
|
|
39
48
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
return _jsx("span", { class: `badge tone-${tone}`, title: title, children: children });
|
|
43
|
-
}
|
|
44
|
-
export function Chip({ title, on_click, children }) {
|
|
45
|
-
return on_click
|
|
46
|
-
? _jsx("button", { type: "button", class: "chip", title: title, onClick: on_click, children: children })
|
|
47
|
-
: _jsx("span", { class: "chip", title: title, children: children });
|
|
48
|
-
}
|
|
49
|
-
// ── States ───────────────────────────────────────────────────────────────────
|
|
50
|
-
export function Skeleton({ lines = 3 }) {
|
|
51
|
-
return (_jsx("div", { class: "skeleton", "aria-busy": "true", "aria-label": "Loading", children: Array.from({ length: lines }, (_, index) => _jsx("div", { class: "skeleton-line", style: { width: `${90 - index * 15}%` } }, index)) }));
|
|
49
|
+
export function Crumbs({ items }) {
|
|
50
|
+
return (_jsx("nav", { class: "crumbs", "aria-label": "Breadcrumb", children: items.map(([label, href], index) => (_jsxs(Fragment, { children: [index > 0 && _jsx("span", { class: "muted", "aria-hidden": "true", children: " / " }), href ? _jsx("a", { href: href, children: label }) : _jsx("span", { "aria-current": "page", children: label })] }, index))) }));
|
|
52
51
|
}
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
// ── Toolbars ─────────────────────────────────────────────────────────────────
|
|
53
|
+
export function Toolbar({ children }) {
|
|
54
|
+
return _jsx("div", { class: "toolbar", children: children });
|
|
55
55
|
}
|
|
56
|
-
|
|
57
|
-
|
|
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", { class: "button", href: target.href, children: target.text });
|
|
61
|
+
return _jsxs("div", { class: "stepper", role: "group", "aria-label": label, children: [step(previous), _jsx("span", { class: "muted", children: position }), step(next)] });
|
|
58
62
|
}
|
|
59
63
|
// ── Settings ─────────────────────────────────────────────────────────────────
|
|
60
64
|
export function SettingsGroup({ heading, hint, children }) {
|
|
61
65
|
return (_jsxs("section", { class: "settings-group", children: [_jsx("h3", { class: "settings-heading", children: heading }), hint && _jsx("p", { class: "setting-hint", children: hint }), children] }));
|
|
62
66
|
}
|
|
63
|
-
// A setting's name and hint on one side and its control on the other.
|
|
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.
|
|
64
68
|
export function SettingRow({ label, hint, as_label = false, children }) {
|
|
65
69
|
const text = (_jsxs("span", { class: "setting-text", children: [_jsx("span", { class: "setting-label", children: label }), hint && _jsx("span", { class: "setting-hint", children: hint })] }));
|
|
66
|
-
return as_label
|
|
67
|
-
? _jsxs("label", { class: "setting-row", children: [text, children] })
|
|
68
|
-
: _jsxs("div", { class: "setting-row", children: [text, children] });
|
|
70
|
+
return as_label ? _jsxs("label", { class: "setting-row", children: [text, children] }) : _jsxs("div", { class: "setting-row", children: [text, children] });
|
|
69
71
|
}
|
|
@@ -9,10 +9,11 @@ interface ContextMenuProps {
|
|
|
9
9
|
}
|
|
10
10
|
export declare function ContextMenu({ x, y, items, on_close }: ContextMenuProps): import("preact").JSX.Element;
|
|
11
11
|
interface PopoverProps {
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
anchor?: HTMLElement | null;
|
|
13
|
+
x?: number;
|
|
14
|
+
y?: number;
|
|
14
15
|
label: string;
|
|
15
16
|
on_close: () => void;
|
|
16
17
|
children: ComponentChildren;
|
|
17
18
|
}
|
|
18
|
-
export declare function Popover({ x, y, label, on_close, children }: PopoverProps): import("preact").JSX.Element;
|
|
19
|
+
export declare function Popover({ anchor, x, y, label, on_close, children }: PopoverProps): import("preact").JSX.Element;
|