@x-tech-laboratories/x-libs 0.3.0 → 0.6.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 +63 -10
- package/dist/dates.d.ts +9 -0
- package/dist/dates.js +60 -0
- package/dist/durations.d.ts +8 -1
- package/dist/durations.js +9 -5
- package/dist/electron/bridge.d.ts +30 -0
- package/dist/electron/bridge.js +32 -0
- package/dist/electron/window.d.ts +85 -0
- package/dist/electron/window.js +85 -0
- package/dist/web/menus.d.ts +4 -7
- package/dist/web/menus.js +28 -86
- package/dist/web/react/badges.d.ts +26 -0
- package/dist/web/react/badges.js +28 -0
- package/dist/web/react/controls.d.ts +37 -0
- package/dist/web/react/controls.js +44 -0
- package/dist/web/react/dialog.d.ts +35 -0
- package/dist/web/react/dialog.js +78 -0
- package/dist/web/react/layout.d.ts +80 -0
- package/dist/web/react/layout.js +71 -0
- package/dist/web/react/menus.d.ts +31 -0
- package/dist/web/react/menus.js +159 -0
- package/dist/web/react/shell.d.ts +37 -0
- package/dist/web/react/shell.js +81 -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/react/states.d.ts +27 -0
- package/dist/web/react/states.js +49 -0
- package/dist/web/react/table.d.ts +46 -0
- package/dist/web/react/table.js +210 -0
- package/dist/web/react/time.d.ts +4 -0
- package/dist/web/react/time.js +9 -0
- package/dist/web/{toasts.d.ts → react/toasts.d.ts} +4 -1
- package/dist/web/react/toasts.js +41 -0
- package/dist/web/react/tree.d.ts +23 -0
- package/dist/web/react/tree.js +168 -0
- package/dist/web/states.d.ts +1 -8
- package/dist/web/states.js +6 -43
- package/dist/web/tables.d.ts +21 -14
- package/dist/web/tables.js +69 -33
- package/dist/web/theme.d.ts +7 -2
- package/dist/web/theme.js +35 -22
- package/dist/web/tree.d.ts +35 -0
- package/dist/web/tree.js +107 -0
- package/package.json +15 -4
- 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/preact/controls.d.ts +0 -24
- package/dist/web/preact/controls.js +0 -36
- package/dist/web/preact/dialog.d.ts +0 -11
- package/dist/web/preact/dialog.js +0 -20
- package/dist/web/preact/layout.d.ts +0 -80
- package/dist/web/preact/layout.js +0 -69
- package/dist/web/preact/menus.d.ts +0 -18
- package/dist/web/preact/menus.js +0 -29
- package/dist/web/preact/table.d.ts +0 -18
- package/dist/web/preact/table.js +0 -13
- package/dist/web/shell.d.ts +0 -7
- package/dist/web/shell.js +0 -51
- package/dist/web/toasts.js +0 -22
package/dist/web/tree.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// Trees of rows that name their parent and their place among siblings, and where a dragged or keyed row lands.
|
|
2
|
+
// ── Shape ────────────────────────────────────────────────────────────────────
|
|
3
|
+
export function by_position(first, second) {
|
|
4
|
+
return first.position - second.position;
|
|
5
|
+
}
|
|
6
|
+
export function children_of(parent_id, items) {
|
|
7
|
+
return items.filter((item) => item.parent_id === parent_id);
|
|
8
|
+
}
|
|
9
|
+
// The item itself plus everything below it, at any depth.
|
|
10
|
+
export function subtree_ids(item_id, items) {
|
|
11
|
+
const ids = new Set([item_id]);
|
|
12
|
+
for (const id of ids) {
|
|
13
|
+
for (const child of children_of(id, items))
|
|
14
|
+
ids.add(child.id);
|
|
15
|
+
}
|
|
16
|
+
return ids;
|
|
17
|
+
}
|
|
18
|
+
// Each shown item sits under its nearest ancestor that is also shown, or at the top when none is, in the order given.
|
|
19
|
+
export function nest(shown, all = shown) {
|
|
20
|
+
const item_by_id = new Map(all.map((item) => [item.id, item]));
|
|
21
|
+
const node_by_id = new Map(shown.map((item) => [item.id, { item, children: [] }]));
|
|
22
|
+
const top_nodes = [];
|
|
23
|
+
for (const node of node_by_id.values()) {
|
|
24
|
+
const seen = new Set([node.item.id]);
|
|
25
|
+
let ancestor_id = node.item.parent_id;
|
|
26
|
+
while (ancestor_id && !node_by_id.has(ancestor_id) && !seen.has(ancestor_id)) {
|
|
27
|
+
seen.add(ancestor_id);
|
|
28
|
+
ancestor_id = item_by_id.get(ancestor_id)?.parent_id ?? null;
|
|
29
|
+
}
|
|
30
|
+
if (ancestor_id && node_by_id.has(ancestor_id) && !seen.has(ancestor_id))
|
|
31
|
+
node_by_id.get(ancestor_id).children.push(node);
|
|
32
|
+
else
|
|
33
|
+
top_nodes.push(node);
|
|
34
|
+
}
|
|
35
|
+
return top_nodes;
|
|
36
|
+
}
|
|
37
|
+
// ── Dropping ─────────────────────────────────────────────────────────────────
|
|
38
|
+
// The top and bottom quarters of a row drop beside it and the middle drops into it, or halves when nothing drops inside.
|
|
39
|
+
export function placement_at(fraction_down, allow_inside) {
|
|
40
|
+
if (!allow_inside)
|
|
41
|
+
return fraction_down < 0.5 ? "before" : "after";
|
|
42
|
+
if (fraction_down < 0.25)
|
|
43
|
+
return "before";
|
|
44
|
+
if (fraction_down > 0.75)
|
|
45
|
+
return "after";
|
|
46
|
+
return "inside";
|
|
47
|
+
}
|
|
48
|
+
// A row cannot drop onto itself or anything below it.
|
|
49
|
+
export function can_drop(dragged_id, target_id, items) {
|
|
50
|
+
return !subtree_ids(dragged_id, items).has(target_id);
|
|
51
|
+
}
|
|
52
|
+
// Picks a position between the neighbours at the drop point, so only the moved item changes.
|
|
53
|
+
function position_beside(siblings, target, placement) {
|
|
54
|
+
const sorted = [...siblings].sort(by_position);
|
|
55
|
+
const index = sorted.findIndex((sibling) => sibling.id === target.id);
|
|
56
|
+
const neighbour = sorted[placement === "before" ? index - 1 : index + 1];
|
|
57
|
+
if (!neighbour)
|
|
58
|
+
return placement === "before" ? target.position - 1 : target.position + 1;
|
|
59
|
+
return (target.position + neighbour.position) / 2;
|
|
60
|
+
}
|
|
61
|
+
// For a flat list, where items only move before or after each other.
|
|
62
|
+
export function position_for_drop(dragged, target, placement, items) {
|
|
63
|
+
return position_beside(items.filter((item) => item.id !== dragged.id), target, placement);
|
|
64
|
+
}
|
|
65
|
+
// Inside drops after the target's last child; before and after drop among the target's siblings.
|
|
66
|
+
export function move_for_drop(dragged, target, placement, items) {
|
|
67
|
+
const others = items.filter((item) => item.id !== dragged.id);
|
|
68
|
+
if (placement === "inside") {
|
|
69
|
+
const last_child_position = Math.max(target.position, ...children_of(target.id, others).map((item) => item.position));
|
|
70
|
+
return { parent_id: target.id, position: last_child_position + 1 };
|
|
71
|
+
}
|
|
72
|
+
return { parent_id: target.parent_id, position: position_beside(children_of(target.parent_id, others), target, placement) };
|
|
73
|
+
}
|
|
74
|
+
// Moves several items together, keeping their order: inside the target after its last child, or in a run between it and its neighbour.
|
|
75
|
+
export function moves_for_drop(dragged, target, placement, items) {
|
|
76
|
+
const dragged_ids = new Set(dragged.map((item) => item.id));
|
|
77
|
+
const others = items.filter((item) => !dragged_ids.has(item.id));
|
|
78
|
+
if (placement === "inside") {
|
|
79
|
+
const last_child_position = Math.max(target.position, ...children_of(target.id, others).map((item) => item.position));
|
|
80
|
+
return dragged.map((_, index) => ({ parent_id: target.id, position: last_child_position + index + 1 }));
|
|
81
|
+
}
|
|
82
|
+
const siblings = children_of(target.parent_id, others).sort(by_position);
|
|
83
|
+
const index = siblings.findIndex((sibling) => sibling.id === target.id);
|
|
84
|
+
const neighbour = siblings[placement === "before" ? index - 1 : index + 1];
|
|
85
|
+
const low = placement === "before" ? (neighbour?.position ?? target.position - 1) : target.position;
|
|
86
|
+
const high = placement === "before" ? target.position : (neighbour?.position ?? target.position + 1);
|
|
87
|
+
return dragged.map((_, index) => ({ parent_id: target.parent_id, position: low + ((high - low) * (index + 1)) / (dragged.length + 1) }));
|
|
88
|
+
}
|
|
89
|
+
// Up and down pass the sibling beside it, indent goes last inside the sibling above, outdent goes after its parent; null when it cannot go that way.
|
|
90
|
+
export function key_drop(item, direction, items) {
|
|
91
|
+
const siblings = children_of(item.parent_id, items).sort(by_position);
|
|
92
|
+
const index = siblings.findIndex((sibling) => sibling.id === item.id);
|
|
93
|
+
const above = siblings[index - 1];
|
|
94
|
+
const below = siblings[index + 1];
|
|
95
|
+
const parent = items.find((each) => each.id === item.parent_id);
|
|
96
|
+
if (direction === "up")
|
|
97
|
+
return above ? { target: above, placement: "before" } : null;
|
|
98
|
+
if (direction === "down")
|
|
99
|
+
return below ? { target: below, placement: "after" } : null;
|
|
100
|
+
if (direction === "indent")
|
|
101
|
+
return above ? { target: above, placement: "inside" } : null;
|
|
102
|
+
return parent ? { target: parent, placement: "after" } : null;
|
|
103
|
+
}
|
|
104
|
+
export function move_for_key(item, direction, items) {
|
|
105
|
+
const drop = key_drop(item, direction, items);
|
|
106
|
+
return drop && move_for_drop(item, drop.target, drop.placement, items);
|
|
107
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@x-tech-laboratories/x-libs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Code the X-apps share: agents and their long runs, passwords, durations, and the web page's helpers and widgets",
|
|
5
5
|
"license": "GPL-3.0-or-later",
|
|
6
6
|
"repository": {
|
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
"peerDependencies": {
|
|
30
30
|
"@fastify/websocket": "^11.3.5",
|
|
31
31
|
"fastify": "^5.12.7",
|
|
32
|
-
"
|
|
32
|
+
"react": "^19",
|
|
33
|
+
"react-dom": "^19"
|
|
33
34
|
},
|
|
34
35
|
"peerDependenciesMeta": {
|
|
35
36
|
"@fastify/websocket": {
|
|
@@ -38,22 +39,32 @@
|
|
|
38
39
|
"fastify": {
|
|
39
40
|
"optional": true
|
|
40
41
|
},
|
|
41
|
-
"
|
|
42
|
+
"react": {
|
|
43
|
+
"optional": true
|
|
44
|
+
},
|
|
45
|
+
"react-dom": {
|
|
42
46
|
"optional": true
|
|
43
47
|
}
|
|
44
48
|
},
|
|
45
49
|
"devDependencies": {
|
|
46
50
|
"@fastify/websocket": "^11.3.5",
|
|
51
|
+
"@testing-library/dom": "^10.4.2",
|
|
52
|
+
"@testing-library/react": "^16.3.3",
|
|
47
53
|
"@types/node": "^24.19.2",
|
|
54
|
+
"@types/react": "^19.3.0",
|
|
55
|
+
"@types/react-dom": "^19.3.0",
|
|
48
56
|
"@types/sshpk": "^1.17.5",
|
|
49
57
|
"fastify": "^5.12.7",
|
|
58
|
+
"jsdom": "^31.0.0",
|
|
50
59
|
"oxlint": "^1.90.0",
|
|
51
|
-
"
|
|
60
|
+
"react": "^19.3.1",
|
|
61
|
+
"react-dom": "^19.3.1",
|
|
52
62
|
"typescript": "^7.0.2",
|
|
53
63
|
"vitest": "^5.0.5"
|
|
54
64
|
},
|
|
55
65
|
"dependencies": {
|
|
56
66
|
"@inquirer/password": "^5.2.5",
|
|
67
|
+
"@tanstack/react-table": "^8.21.3",
|
|
57
68
|
"parse-duration": "^2.1.11",
|
|
58
69
|
"pretty-bytes": "^7.2.0",
|
|
59
70
|
"sshpk": "^1.18.0"
|
package/dist/web/badges.d.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { type Child } from "./dom.ts";
|
|
2
|
-
export type Tone = "good" | "warn" | "bad" | "info" | "none";
|
|
3
|
-
export declare function badge(text: Child, tone?: Tone, title?: string): HTMLElement;
|
|
4
|
-
export declare function state_badge<State extends string>(labels: Record<State, [text: string, tone: Tone]>, state: State): HTMLElement;
|
|
5
|
-
export declare function chip(text: Child, title?: string): HTMLElement;
|
|
6
|
-
export declare function chip_button(text: Child, on_click: () => void, title?: string): HTMLElement;
|
|
7
|
-
export declare function chips(items: Child[], options?: {
|
|
8
|
-
inline?: boolean;
|
|
9
|
-
}): HTMLElement;
|
|
10
|
-
export declare function verdict(ok: boolean | null | undefined, label: string): HTMLElement;
|
package/dist/web/badges.js
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
// Badges, chips and verdicts: short labels whose tone is written out, so meaning never rests on color alone.
|
|
2
|
-
import { h } from "./dom.js";
|
|
3
|
-
// ── Badges ───────────────────────────────────────────────────────────────────
|
|
4
|
-
export function badge(text, tone = "none", title = "") {
|
|
5
|
-
return h("span", { class: `badge tone-${tone}`, title: title || null }, text);
|
|
6
|
-
}
|
|
7
|
-
// A badge for one of a fixed set of states, each named by its text and tone.
|
|
8
|
-
export function state_badge(labels, state) {
|
|
9
|
-
const [text, tone] = labels[state];
|
|
10
|
-
return badge(text, tone);
|
|
11
|
-
}
|
|
12
|
-
// ── Chips ────────────────────────────────────────────────────────────────────
|
|
13
|
-
export function chip(text, title = "") {
|
|
14
|
-
return h("span", { class: "chip", title: title || null }, text);
|
|
15
|
-
}
|
|
16
|
-
// A chip that picks what it names, such as a tag to filter by.
|
|
17
|
-
export function chip_button(text, on_click, title = "") {
|
|
18
|
-
return h("button", { type: "button", class: "chip", title: title || null, onclick: on_click }, text);
|
|
19
|
-
}
|
|
20
|
-
// A row of chips or badges, with less space between them when they sit inside a line of text.
|
|
21
|
-
export function chips(items, options = {}) {
|
|
22
|
-
return h("span", { class: options.inline ? "chips inline-chips" : "chips" }, ...items);
|
|
23
|
-
}
|
|
24
|
-
// ── Verdicts ─────────────────────────────────────────────────────────────────
|
|
25
|
-
// A pass, fail or not-yet verdict, such as "checks: pass".
|
|
26
|
-
export function verdict(ok, label) {
|
|
27
|
-
const text = ok === null || ok === undefined ? "no verdict" : ok ? "pass" : "fail";
|
|
28
|
-
const tone = ok === null || ok === undefined ? "none" : ok ? "good" : "bad";
|
|
29
|
-
return h("span", { class: `verdict tone-${tone}` }, `${label}: ${text}`);
|
|
30
|
-
}
|
package/dist/web/controls.d.ts
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import { type Child } from "./dom.ts";
|
|
2
|
-
export declare function search_box(label: string, value: string, on_input: (value: string) => void): HTMLInputElement;
|
|
3
|
-
export declare function select(label: string, options: [value: string, text: string][], value: string, on_change: (value: string) => void): HTMLElement;
|
|
4
|
-
export declare function segmented<Value extends string>(label: string, choices: [value: Value, text: string][], value: Value, on_change: (value: Value) => void): HTMLElement;
|
|
5
|
-
export declare function button_group(label: string, ...buttons: Child[]): HTMLElement;
|
|
6
|
-
export declare function switch_toggle(label: string, checked: boolean, on_change: (on: boolean) => Promise<boolean | void> | boolean | void): HTMLInputElement;
|
package/dist/web/controls.js
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
// Controls that change what a view shows or does: filters, selects, segmented choices, switches and button groups.
|
|
2
|
-
import { h } from "./dom.js";
|
|
3
|
-
// ── Filters ──────────────────────────────────────────────────────────────────
|
|
4
|
-
// A search field that hands on what was typed once typing pauses.
|
|
5
|
-
export function search_box(label, value, on_input) {
|
|
6
|
-
let timer;
|
|
7
|
-
return h("input", {
|
|
8
|
-
type: "search", class: "filter", placeholder: label, "aria-label": label, value,
|
|
9
|
-
oninput: (event) => {
|
|
10
|
-
clearTimeout(timer);
|
|
11
|
-
const text = event.target.value;
|
|
12
|
-
timer = window.setTimeout(() => on_input(text), 200);
|
|
13
|
-
},
|
|
14
|
-
});
|
|
15
|
-
}
|
|
16
|
-
export function select(label, options, value, on_change) {
|
|
17
|
-
return h("label", { class: "field" }, h("span", { class: "field-label" }, label), h("select", { onchange: (event) => on_change(event.target.value) }, ...options.map(([option, text]) => h("option", { value: option, selected: option === value }, text))));
|
|
18
|
-
}
|
|
19
|
-
// ── Choices ──────────────────────────────────────────────────────────────────
|
|
20
|
-
// One of a few choices side by side, such as cards or a table; the chosen one takes the accent.
|
|
21
|
-
export function segmented(label, choices, value, on_change) {
|
|
22
|
-
const buttons = choices.map(([choice, text]) => h("button", {
|
|
23
|
-
type: "button", role: "radio", "aria-checked": String(choice === value),
|
|
24
|
-
onclick: () => {
|
|
25
|
-
for (const [index, button] of buttons.entries())
|
|
26
|
-
button.setAttribute("aria-checked", String(choices[index][0] === choice));
|
|
27
|
-
on_change(choice);
|
|
28
|
-
},
|
|
29
|
-
}, text));
|
|
30
|
-
return h("div", { class: "segmented", role: "radiogroup", "aria-label": label }, ...buttons);
|
|
31
|
-
}
|
|
32
|
-
// Buttons joined into one control, such as zoom out, zoom in and fit.
|
|
33
|
-
export function button_group(label, ...buttons) {
|
|
34
|
-
return h("div", { class: "segmented", role: "group", "aria-label": label }, ...buttons);
|
|
35
|
-
}
|
|
36
|
-
// An on and off switch that waits on the change and flips back when the change did not take.
|
|
37
|
-
export function switch_toggle(label, checked, on_change) {
|
|
38
|
-
const box = h("input", { type: "checkbox", role: "switch", checked, "aria-label": label });
|
|
39
|
-
box.addEventListener("change", async () => {
|
|
40
|
-
const on = box.checked;
|
|
41
|
-
box.disabled = true;
|
|
42
|
-
try {
|
|
43
|
-
if ((await on_change(on)) === false)
|
|
44
|
-
box.checked = !on;
|
|
45
|
-
}
|
|
46
|
-
catch (error) {
|
|
47
|
-
box.checked = !on;
|
|
48
|
-
throw error;
|
|
49
|
-
}
|
|
50
|
-
finally {
|
|
51
|
-
box.disabled = false;
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
return box;
|
|
55
|
-
}
|
package/dist/web/dialogs.d.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { type Child } from "./dom.ts";
|
|
2
|
-
export interface DialogOptions {
|
|
3
|
-
wide?: boolean;
|
|
4
|
-
class_name?: string;
|
|
5
|
-
}
|
|
6
|
-
export declare function open_dialog(title: string, body: Child, buttons: (close: () => void) => Child, options?: DialogOptions): () => void;
|
|
7
|
-
export interface ConfirmOptions {
|
|
8
|
-
title: string;
|
|
9
|
-
message: Child;
|
|
10
|
-
confirm_label: string;
|
|
11
|
-
busy_label?: string;
|
|
12
|
-
danger?: boolean;
|
|
13
|
-
wide?: boolean;
|
|
14
|
-
type_to_confirm?: string;
|
|
15
|
-
run: () => Promise<void>;
|
|
16
|
-
}
|
|
17
|
-
export declare function confirm_dialog(options: ConfirmOptions): void;
|
|
18
|
-
export declare function text_dialog(title: string, text: string): void;
|
package/dist/web/dialogs.js
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
// Modal dialogs: one with any body and buttons, one that asks before an action, and one that shows a long text.
|
|
2
|
-
import { h } from "./dom.js";
|
|
3
|
-
import { toast } from "./toasts.js";
|
|
4
|
-
// A modal dialog: the page behind it is inert, focus stays inside, and Escape closes it.
|
|
5
|
-
export function open_dialog(title, body, buttons, options = {}) {
|
|
6
|
-
const classes = ["dialog", options.wide && "wide", options.class_name].filter(Boolean).join(" ");
|
|
7
|
-
const dialog = h("dialog", { class: classes, "aria-labelledby": "dialog-title" });
|
|
8
|
-
const close = () => {
|
|
9
|
-
dialog.close();
|
|
10
|
-
dialog.remove();
|
|
11
|
-
};
|
|
12
|
-
dialog.addEventListener("close", () => dialog.remove());
|
|
13
|
-
dialog.append(h("h2", { id: "dialog-title" }, title), h("div", { class: "dialog-body" }, body), h("div", { class: "dialog-buttons" }, buttons(close)));
|
|
14
|
-
document.body.append(dialog);
|
|
15
|
-
dialog.showModal();
|
|
16
|
-
return close;
|
|
17
|
-
}
|
|
18
|
-
export function confirm_dialog(options) {
|
|
19
|
-
const confirm = h("button", { class: options.danger ? "danger solid" : "accent solid", disabled: options.type_to_confirm !== undefined }, options.confirm_label);
|
|
20
|
-
const typed = options.type_to_confirm === undefined ? null : h("input", {
|
|
21
|
-
type: "text", class: "filter", autocomplete: "off", "aria-label": `Type ${options.type_to_confirm} to confirm`,
|
|
22
|
-
oninput: (event) => { confirm.disabled = event.target.value !== options.type_to_confirm; },
|
|
23
|
-
});
|
|
24
|
-
const body = h("div", {}, typeof options.message === "string" ? h("p", {}, options.message) : options.message, typed && h("label", { class: "type-confirm" }, h("span", {}, "Type ", h("strong", {}, options.type_to_confirm), " to confirm:"), typed));
|
|
25
|
-
open_dialog(options.title, body, (close) => {
|
|
26
|
-
const cancel = h("button", { onclick: close }, "Cancel");
|
|
27
|
-
confirm.addEventListener("click", async () => {
|
|
28
|
-
confirm.disabled = true;
|
|
29
|
-
cancel.disabled = true;
|
|
30
|
-
confirm.textContent = options.busy_label ?? "Working…";
|
|
31
|
-
try {
|
|
32
|
-
await options.run();
|
|
33
|
-
}
|
|
34
|
-
finally {
|
|
35
|
-
close();
|
|
36
|
-
}
|
|
37
|
-
});
|
|
38
|
-
return [cancel, confirm];
|
|
39
|
-
}, { wide: options.wide });
|
|
40
|
-
(typed ?? confirm).focus();
|
|
41
|
-
}
|
|
42
|
-
// ── Text ─────────────────────────────────────────────────────────────────────
|
|
43
|
-
// Shows a long text result, such as a preview, with a button to copy it.
|
|
44
|
-
export function text_dialog(title, text) {
|
|
45
|
-
open_dialog(title, h("pre", { class: "output" }, text), (close) => [
|
|
46
|
-
h("button", { onclick: () => void navigator.clipboard?.writeText(text).then(() => toast("Copied.")) }, "Copy"),
|
|
47
|
-
h("button", { class: "accent solid", onclick: close }, "Close"),
|
|
48
|
-
], { wide: true });
|
|
49
|
-
}
|
package/dist/web/layout.d.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { type Child } from "./dom.ts";
|
|
2
|
-
export declare function section(title: Child, body: Child, tools?: Child): HTMLElement;
|
|
3
|
-
export declare function facts(rows: [label: string, value: Child, class_name?: string][], layout?: "grid" | "rows"): HTMLElement;
|
|
4
|
-
export declare function tile(label: string, value: Child, class_name?: string | null, href?: string | null): HTMLElement;
|
|
5
|
-
export declare function tiles(...items: Child[]): HTMLElement;
|
|
6
|
-
export interface CardOptions {
|
|
7
|
-
subtitle?: Child;
|
|
8
|
-
badge?: Child;
|
|
9
|
-
class_name?: string;
|
|
10
|
-
href?: string;
|
|
11
|
-
}
|
|
12
|
-
export declare function card(title: Child, body: Child, options?: CardOptions): HTMLElement;
|
|
13
|
-
export declare function cards(...items: Child[]): HTMLElement;
|
|
14
|
-
export interface TabItem {
|
|
15
|
-
id: string;
|
|
16
|
-
label: string;
|
|
17
|
-
href: string;
|
|
18
|
-
}
|
|
19
|
-
export declare function tabs(items: TabItem[], active: string, label?: string): HTMLElement;
|
|
20
|
-
export declare function crumbs(items: [label: string, href: string | null][]): HTMLElement;
|
|
21
|
-
export declare function toolbar(...children: Child[]): HTMLElement;
|
|
22
|
-
export declare function stepper(label: string, previous: {
|
|
23
|
-
text: string;
|
|
24
|
-
href: string | null;
|
|
25
|
-
}, position: Child, next: {
|
|
26
|
-
text: string;
|
|
27
|
-
href: string | null;
|
|
28
|
-
}): HTMLElement;
|
package/dist/web/layout.js
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
// The pieces a page is laid out with: panels, facts, tiles, cards, tabs, breadcrumbs, toolbars and steppers.
|
|
2
|
-
import { h } from "./dom.js";
|
|
3
|
-
// ── Panels ───────────────────────────────────────────────────────────────────
|
|
4
|
-
export function section(title, body, tools = null) {
|
|
5
|
-
return h("section", { class: "panel" }, h("div", { class: "panel-head" }, h("h2", {}, title), tools && h("div", { class: "panel-tools" }, tools)), body);
|
|
6
|
-
}
|
|
7
|
-
// Labelled values in a grid of tiles, or as rows of label and value for long values such as paths.
|
|
8
|
-
export function facts(rows, layout = "grid") {
|
|
9
|
-
return h("dl", { class: layout === "rows" ? "facts facts-rows" : "facts" }, ...rows.map(([label, value, class_name]) => h("div", {}, h("dt", {}, label), h("dd", { class: class_name ?? null }, value ?? "—"))));
|
|
10
|
-
}
|
|
11
|
-
// ── Tiles and cards ──────────────────────────────────────────────────────────
|
|
12
|
-
// One headline figure under its label; a class such as tile-bad or tone-edge-warn marks its edge, and a link makes it open a page.
|
|
13
|
-
export function tile(label, value, class_name = null, href = null) {
|
|
14
|
-
const tile_class = `tile${class_name ? ` ${class_name}` : ""}`;
|
|
15
|
-
const children = [h("div", { class: "tile-label" }, label), h("div", { class: "tile-value" }, value)];
|
|
16
|
-
return href === null ? h("div", { class: tile_class }, ...children) : h("a", { class: tile_class, href }, ...children);
|
|
17
|
-
}
|
|
18
|
-
export function tiles(...items) {
|
|
19
|
-
return h("div", { class: "tiles" }, ...items);
|
|
20
|
-
}
|
|
21
|
-
// A titled card for one thing among many, laid out in a grid.
|
|
22
|
-
export function card(title, body, options = {}) {
|
|
23
|
-
const card_class = `card${options.class_name ? ` ${options.class_name}` : ""}`;
|
|
24
|
-
const children = [
|
|
25
|
-
h("div", { class: "card-head" }, h("strong", {}, title), options.badge),
|
|
26
|
-
options.subtitle && h("div", { class: "muted card-host" }, options.subtitle),
|
|
27
|
-
body,
|
|
28
|
-
];
|
|
29
|
-
return options.href ? h("a", { class: card_class, href: options.href }, ...children) : h("div", { class: card_class }, ...children);
|
|
30
|
-
}
|
|
31
|
-
export function cards(...items) {
|
|
32
|
-
return h("div", { class: "grid" }, ...items);
|
|
33
|
-
}
|
|
34
|
-
export function tabs(items, active, label = "Sections") {
|
|
35
|
-
return h("nav", { class: "tabs", "aria-label": label }, ...items.map((item) => h("a", { href: item.href, class: item.id === active ? "tab active" : "tab", "aria-current": item.id === active ? "page" : null }, item.label)));
|
|
36
|
-
}
|
|
37
|
-
export function crumbs(items) {
|
|
38
|
-
return h("nav", { class: "crumbs", "aria-label": "Breadcrumb" }, ...items.flatMap(([label, href], index) => [
|
|
39
|
-
index > 0 ? h("span", { class: "muted", "aria-hidden": "true" }, " / ") : null,
|
|
40
|
-
href ? h("a", { href }, label) : h("span", { "aria-current": "page" }, label),
|
|
41
|
-
]));
|
|
42
|
-
}
|
|
43
|
-
// ── Toolbars ─────────────────────────────────────────────────────────────────
|
|
44
|
-
export function toolbar(...children) {
|
|
45
|
-
return h("div", { class: "toolbar" }, ...children);
|
|
46
|
-
}
|
|
47
|
-
// 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.
|
|
48
|
-
export function stepper(label, previous, position, next) {
|
|
49
|
-
const step = (target) => target.href === null
|
|
50
|
-
? h("button", { disabled: true }, target.text)
|
|
51
|
-
: h("a", { class: "button", href: target.href }, target.text);
|
|
52
|
-
return h("div", { class: "stepper", role: "group", "aria-label": label }, step(previous), h("span", { class: "muted" }, position), step(next));
|
|
53
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
interface SegmentedProps<Value extends string> {
|
|
2
|
-
label: string;
|
|
3
|
-
choices: {
|
|
4
|
-
value: Value;
|
|
5
|
-
label: string;
|
|
6
|
-
}[];
|
|
7
|
-
value: Value;
|
|
8
|
-
on_change: (value: Value) => void;
|
|
9
|
-
}
|
|
10
|
-
export declare function Segmented<Value extends string>({ label, choices, value, on_change }: SegmentedProps<Value>): import("preact").JSX.Element;
|
|
11
|
-
interface SwitchProps {
|
|
12
|
-
label: string;
|
|
13
|
-
checked: boolean;
|
|
14
|
-
on_change: (on: boolean) => Promise<boolean | void> | boolean | void;
|
|
15
|
-
disabled?: boolean;
|
|
16
|
-
}
|
|
17
|
-
export declare function Switch({ label, checked, on_change, disabled }: SwitchProps): import("preact").JSX.Element;
|
|
18
|
-
interface SearchBoxProps {
|
|
19
|
-
label: string;
|
|
20
|
-
value: string;
|
|
21
|
-
on_input: (value: string) => void;
|
|
22
|
-
}
|
|
23
|
-
export declare function SearchBox({ label, value, on_input }: SearchBoxProps): import("preact").JSX.Element;
|
|
24
|
-
export {};
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx } from "preact/jsx-runtime";
|
|
2
|
-
// Preact controls drawn like the page's own: segmented choices, switches and search fields.
|
|
3
|
-
import { useRef, useState } from "preact/hooks";
|
|
4
|
-
// 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))) }));
|
|
7
|
-
}
|
|
8
|
-
export function Switch({ label, checked, on_change, disabled = false }) {
|
|
9
|
-
const [busy, set_busy] = useState(false);
|
|
10
|
-
async function change(event) {
|
|
11
|
-
const box = event.currentTarget;
|
|
12
|
-
const on = box.checked;
|
|
13
|
-
set_busy(true);
|
|
14
|
-
try {
|
|
15
|
-
if ((await on_change(on)) === false)
|
|
16
|
-
box.checked = !on;
|
|
17
|
-
}
|
|
18
|
-
catch (error) {
|
|
19
|
-
box.checked = !on;
|
|
20
|
-
throw error;
|
|
21
|
-
}
|
|
22
|
-
finally {
|
|
23
|
-
set_busy(false);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
return _jsx("input", { type: "checkbox", role: "switch", "aria-label": label, checked: checked, disabled: disabled || busy, onChange: change });
|
|
27
|
-
}
|
|
28
|
-
// A search field that hands on what was typed once typing pauses.
|
|
29
|
-
export function SearchBox({ label, value, on_input }) {
|
|
30
|
-
const timer = useRef(undefined);
|
|
31
|
-
return (_jsx("input", { type: "search", class: "filter", placeholder: label, "aria-label": label, value: value, onInput: (event) => {
|
|
32
|
-
clearTimeout(timer.current);
|
|
33
|
-
const text = event.currentTarget.value;
|
|
34
|
-
timer.current = window.setTimeout(() => on_input(text), 200);
|
|
35
|
-
} }));
|
|
36
|
-
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import type { ComponentChildren } from "preact";
|
|
2
|
-
interface DialogProps {
|
|
3
|
-
title: ComponentChildren;
|
|
4
|
-
on_close: () => void;
|
|
5
|
-
buttons?: ComponentChildren;
|
|
6
|
-
wide?: boolean;
|
|
7
|
-
class_name?: string;
|
|
8
|
-
children: ComponentChildren;
|
|
9
|
-
}
|
|
10
|
-
export declare function Dialog({ title, on_close, buttons, wide, class_name, children }: DialogProps): import("preact").JSX.Element;
|
|
11
|
-
export {};
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
|
|
2
|
-
import { useEffect, useId, useRef } from "preact/hooks";
|
|
3
|
-
// Shown while it is rendered; closing it by Escape or a button calls on_close, which should stop rendering it.
|
|
4
|
-
export function Dialog({ title, on_close, buttons, wide = false, class_name, children }) {
|
|
5
|
-
const dialog = useRef(null);
|
|
6
|
-
const title_id = useId();
|
|
7
|
-
const close = useRef(on_close);
|
|
8
|
-
close.current = on_close;
|
|
9
|
-
useEffect(() => {
|
|
10
|
-
const element = dialog.current;
|
|
11
|
-
element.showModal();
|
|
12
|
-
return () => element.close();
|
|
13
|
-
}, []);
|
|
14
|
-
return (_jsxs("dialog", { ref: dialog, class: ["dialog", wide && "wide", class_name].filter(Boolean).join(" "), "aria-labelledby": title_id, onCancel: (event) => {
|
|
15
|
-
// Escape asks the owner to close, so the dialog never closes behind its owner's back.
|
|
16
|
-
event.preventDefault();
|
|
17
|
-
event.stopPropagation();
|
|
18
|
-
close.current();
|
|
19
|
-
}, children: [_jsx("h2", { id: title_id, children: title }), _jsx("div", { class: "dialog-body", children: children }), buttons && _jsx("div", { class: "dialog-buttons", children: buttons })] }));
|
|
20
|
-
}
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import type { ComponentChildren } from "preact";
|
|
2
|
-
import type { Tone } from "../badges.ts";
|
|
3
|
-
export type { Tone } from "../badges.ts";
|
|
4
|
-
interface SectionProps {
|
|
5
|
-
title: ComponentChildren;
|
|
6
|
-
tools?: ComponentChildren;
|
|
7
|
-
children: ComponentChildren;
|
|
8
|
-
}
|
|
9
|
-
export declare function Section({ title, tools, children }: SectionProps): import("preact").JSX.Element;
|
|
10
|
-
export declare function Facts({ rows, layout }: {
|
|
11
|
-
rows: [label: string, value: ComponentChildren][];
|
|
12
|
-
layout?: "grid" | "rows";
|
|
13
|
-
}): import("preact").JSX.Element;
|
|
14
|
-
interface TileProps {
|
|
15
|
-
label: string;
|
|
16
|
-
value: ComponentChildren;
|
|
17
|
-
tone?: Tone;
|
|
18
|
-
href?: string;
|
|
19
|
-
}
|
|
20
|
-
export declare function Tile({ label, value, tone, href }: TileProps): import("preact").JSX.Element;
|
|
21
|
-
export declare function Tiles({ children }: {
|
|
22
|
-
children: ComponentChildren;
|
|
23
|
-
}): import("preact").JSX.Element;
|
|
24
|
-
interface CardProps {
|
|
25
|
-
title: ComponentChildren;
|
|
26
|
-
subtitle?: ComponentChildren;
|
|
27
|
-
badge?: ComponentChildren;
|
|
28
|
-
class_name?: string;
|
|
29
|
-
href?: string;
|
|
30
|
-
on_click?: () => void;
|
|
31
|
-
children?: ComponentChildren;
|
|
32
|
-
}
|
|
33
|
-
export declare function Card({ title, subtitle, badge, class_name, href, on_click, children }: CardProps): import("preact").JSX.Element;
|
|
34
|
-
export declare function Cards({ children }: {
|
|
35
|
-
children: ComponentChildren;
|
|
36
|
-
}): import("preact").JSX.Element;
|
|
37
|
-
interface TabsProps<Id extends string> {
|
|
38
|
-
items: {
|
|
39
|
-
id: Id;
|
|
40
|
-
label: string;
|
|
41
|
-
href?: string;
|
|
42
|
-
}[];
|
|
43
|
-
active: Id;
|
|
44
|
-
label?: string;
|
|
45
|
-
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
|
-
}): import("preact").JSX.Element;
|
|
53
|
-
export declare function Chip({ title, on_click, children }: {
|
|
54
|
-
title?: string;
|
|
55
|
-
on_click?: () => void;
|
|
56
|
-
children: ComponentChildren;
|
|
57
|
-
}): import("preact").JSX.Element;
|
|
58
|
-
export declare function Skeleton({ lines }: {
|
|
59
|
-
lines?: number;
|
|
60
|
-
}): import("preact").JSX.Element;
|
|
61
|
-
export declare function Empty({ title, hint }: {
|
|
62
|
-
title: string;
|
|
63
|
-
hint?: ComponentChildren;
|
|
64
|
-
}): import("preact").JSX.Element;
|
|
65
|
-
export declare function Failure({ error, retry }: {
|
|
66
|
-
error: unknown;
|
|
67
|
-
retry: () => void;
|
|
68
|
-
}): import("preact").JSX.Element;
|
|
69
|
-
export declare function SettingsGroup({ heading, hint, children }: {
|
|
70
|
-
heading: string;
|
|
71
|
-
hint?: ComponentChildren;
|
|
72
|
-
children: ComponentChildren;
|
|
73
|
-
}): import("preact").JSX.Element;
|
|
74
|
-
interface SettingRowProps {
|
|
75
|
-
label: ComponentChildren;
|
|
76
|
-
hint?: ComponentChildren;
|
|
77
|
-
as_label?: boolean;
|
|
78
|
-
children: ComponentChildren;
|
|
79
|
-
}
|
|
80
|
-
export declare function SettingRow({ label, hint, as_label, children }: SettingRowProps): import("preact").JSX.Element;
|