@x-tech-laboratories/x-libs 0.2.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.
Files changed (63) hide show
  1. package/README.md +23 -13
  2. package/dist/agent/client.d.ts +9 -6
  3. package/dist/agent/client.js +3 -3
  4. package/dist/agent/keys.js +31 -119
  5. package/dist/agent/long_runs.d.ts +5 -5
  6. package/dist/agent/long_runs.js +9 -10
  7. package/dist/agent/protocol.d.ts +20 -14
  8. package/dist/agent/protocol.js +34 -17
  9. package/dist/agent/session.d.ts +11 -8
  10. package/dist/agent/session.js +13 -9
  11. package/dist/agent/tasks.d.ts +17 -0
  12. package/dist/agent/tasks.js +15 -0
  13. package/dist/durations.js +18 -20
  14. package/dist/server/agents/agent.d.ts +4 -4
  15. package/dist/server/agents/connection.d.ts +8 -7
  16. package/dist/server/agents/connection.js +11 -9
  17. package/dist/server/agents/hub.d.ts +12 -12
  18. package/dist/server/agents/hub.js +14 -14
  19. package/dist/server/agents/local.d.ts +10 -7
  20. package/dist/server/agents/local.js +12 -9
  21. package/dist/server/agents/route.d.ts +1 -1
  22. package/dist/server/hash_password.js +16 -34
  23. package/dist/web/format.js +25 -29
  24. package/dist/web/menus.d.ts +0 -7
  25. package/dist/web/menus.js +2 -87
  26. package/dist/web/preact/badges.d.ts +26 -0
  27. package/dist/web/preact/badges.js +28 -0
  28. package/dist/web/preact/controls.d.ts +14 -1
  29. package/dist/web/preact/controls.js +14 -7
  30. package/dist/web/preact/dialog.d.ts +25 -1
  31. package/dist/web/preact/dialog.js +56 -3
  32. package/dist/web/preact/layout.d.ts +37 -37
  33. package/dist/web/preact/layout.js +27 -25
  34. package/dist/web/preact/menus.d.ts +4 -3
  35. package/dist/web/preact/menus.js +56 -15
  36. package/dist/web/preact/shell.d.ts +27 -0
  37. package/dist/web/preact/shell.js +63 -0
  38. package/dist/web/preact/states.d.ts +27 -0
  39. package/dist/web/preact/states.js +48 -0
  40. package/dist/web/preact/table.d.ts +6 -1
  41. package/dist/web/preact/table.js +8 -1
  42. package/dist/web/preact/time.d.ts +4 -0
  43. package/dist/web/preact/time.js +9 -0
  44. package/dist/web/{toasts.d.ts → preact/toasts.d.ts} +2 -1
  45. package/dist/web/preact/toasts.js +34 -0
  46. package/dist/web/states.d.ts +1 -8
  47. package/dist/web/states.js +6 -43
  48. package/dist/web/tables.d.ts +4 -14
  49. package/dist/web/tables.js +1 -27
  50. package/dist/web/theme.d.ts +1 -1
  51. package/dist/web/theme.js +4 -1
  52. package/package.json +9 -2
  53. package/dist/web/badges.d.ts +0 -10
  54. package/dist/web/badges.js +0 -30
  55. package/dist/web/controls.d.ts +0 -6
  56. package/dist/web/controls.js +0 -55
  57. package/dist/web/dialogs.d.ts +0 -18
  58. package/dist/web/dialogs.js +0 -49
  59. package/dist/web/layout.d.ts +0 -28
  60. package/dist/web/layout.js +0 -53
  61. package/dist/web/shell.d.ts +0 -7
  62. package/dist/web/shell.js +0 -51
  63. package/dist/web/toasts.js +0 -22
@@ -1,26 +1,36 @@
1
1
  // Writes times, durations, sizes and counts short enough for a table cell.
2
+ import pretty_bytes from "pretty-bytes";
2
3
  // ── Times ────────────────────────────────────────────────────────────────────
3
4
  export function now_seconds() {
4
5
  return Date.now() / 1000;
5
6
  }
7
+ const relative = new Intl.RelativeTimeFormat("en", { style: "narrow", numeric: "always" });
8
+ const plurals = new Intl.PluralRules("en");
9
+ // A whole number of the largest unit that reads well, such as -3 hours, for "3h ago" or "in 3h".
10
+ function relative_time(seconds) {
11
+ const magnitude = Math.abs(seconds);
12
+ const sign = seconds < 0 ? -1 : 1;
13
+ if (magnitude < 90)
14
+ return relative.format(sign * Math.round(magnitude), "second");
15
+ if (magnitude < 5400)
16
+ return relative.format(sign * Math.round(magnitude / 60), "minute");
17
+ if (magnitude < 129600)
18
+ return relative.format(sign * Math.round(magnitude / 3600), "hour");
19
+ return relative.format(sign * Math.round(magnitude / 86400), "day");
20
+ }
6
21
  export function age(epoch) {
7
22
  if (!epoch)
8
23
  return "never";
9
- const seconds = Math.max(0, now_seconds() - epoch);
10
- if (seconds < 90)
11
- return `${Math.round(seconds)}s ago`;
12
- if (seconds < 5400)
13
- return `${Math.round(seconds / 60)}m ago`;
14
- if (seconds < 129600)
15
- return `${Math.round(seconds / 3600)}h ago`;
16
- return `${Math.round(seconds / 86400)}d ago`;
24
+ return relative_time(-Math.max(0, now_seconds() - epoch));
17
25
  }
18
- // How long until the epoch, such as "in 3d", or how long ago when it has passed.
26
+ // How long until the epoch, such as "in 3d", "now" when it is less than 90 seconds off, or how long ago once passed.
19
27
  export function until(epoch) {
20
28
  if (epoch === null)
21
29
  return "—";
22
30
  const seconds = epoch - now_seconds();
23
- return seconds < 0 ? age(epoch) : `in ${duration(seconds)}`;
31
+ if (seconds < 0)
32
+ return age(epoch);
33
+ return seconds < 90 ? "now" : relative_time(seconds);
24
34
  }
25
35
  export function duration(seconds) {
26
36
  if (seconds === null)
@@ -42,33 +52,19 @@ export function stamp(epoch) {
42
52
  // ── Sizes ────────────────────────────────────────────────────────────────────
43
53
  // Sizes in binary units, such as "44.4 GiB".
44
54
  export function bytes(count) {
45
- const units = ["B", "KiB", "MiB", "GiB", "TiB", "PiB"];
46
- let value = count;
47
- let unit = 0;
48
- while (value >= 1024 && unit < units.length - 1) {
49
- value /= 1024;
50
- unit += 1;
51
- }
52
- return unit === 0 ? `${Math.round(value)} B` : `${value.toFixed(1)} ${units[unit]}`;
55
+ return pretty_bytes(count, { binary: true, maximumFractionDigits: 1 });
53
56
  }
54
57
  // Sizes in decimal units, such as "1.2 GB".
55
58
  export function size(count) {
56
59
  if (count === null || count === undefined)
57
60
  return "—";
58
- const units = ["B", "kB", "MB", "GB", "TB", "PB"];
59
- let value = Math.abs(count);
60
- let unit = 0;
61
- while (value >= 1000 && unit < units.length - 1) {
62
- value /= 1000;
63
- unit += 1;
64
- }
65
- return `${count < 0 ? "−" : ""}${value.toFixed(value < 10 && unit > 0 ? 1 : 0)} ${units[unit]}`;
61
+ return pretty_bytes(count, { maximumFractionDigits: 1 });
66
62
  }
67
- // A size change with its sign, such as +1.2 GB or −300 MB.
63
+ // A size change with its sign, such as +1.2 GB or -300 MB.
68
64
  export function size_change(count) {
69
65
  if (count === null || count === undefined)
70
66
  return "—";
71
- return count > 0 ? `+${size(count)}` : size(count);
67
+ return count === 0 ? size(0) : pretty_bytes(count, { signed: true, maximumFractionDigits: 1 });
72
68
  }
73
69
  // ── Counts ───────────────────────────────────────────────────────────────────
74
70
  export function count_change(value) {
@@ -77,5 +73,5 @@ export function count_change(value) {
77
73
  return value > 0 ? `+${value.toLocaleString()}` : value < 0 ? `−${Math.abs(value).toLocaleString()}` : "0";
78
74
  }
79
75
  export function plural(count, word) {
80
- return `${count.toLocaleString()} ${word}${count === 1 ? "" : "s"}`;
76
+ return `${count.toLocaleString()} ${word}${plurals.select(count) === "one" ? "" : "s"}`;
81
77
  }
@@ -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
- // Menus and popovers opened at a point: kept on screen, and closed by Escape, a click elsewhere, scrolling or leaving the window.
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
- // Where an element opened at a point goes so it stays on screen: flipped to the point's other side, or pushed back from the edge.
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
- // Preact controls drawn like the page's own: segmented choices, switches and search fields.
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
- return (_jsx("input", { type: "search", class: "filter", placeholder: label, "aria-label": label, value: value, onInput: (event) => {
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 { ComponentChildren } from "preact";
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
- 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.
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 { ComponentChildren } from "preact";
2
- import type { Tone } from "../badges.ts";
3
- export type { Tone } from "../badges.ts";
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: ComponentChildren;
8
- }
9
- export declare function Section({ title, tools, children }: SectionProps): import("preact").JSX.Element;
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: [label: string, value: ComponentChildren][];
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 TabsProps<Id extends string> {
38
- items: {
39
- id: Id;
40
- label: string;
41
- href?: string;
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 Chip({ title, on_click, children }: {
54
- title?: string;
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 Skeleton({ lines }: {
59
- lines?: number;
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
- export declare function Failure({ error, retry }: {
66
- error: unknown;
67
- retry: () => void;
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
- interface SettingRowProps {
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 declare function SettingRow({ label, hint, as_label, children }: SettingRowProps): import("preact").JSX.Element;
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
- // One headline figure under its label, its edge marked by its tone.
10
- export function Tile({ label, value, tone, href }) {
11
- const tile_class = tone && tone !== "none" ? `tile tile-${tone}` : "tile";
12
- const children = _jsxs(_Fragment, { children: [_jsx("div", { class: "tile-label", children: label }), _jsx("div", { class: "tile-value", children: value })] });
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
- // ── Badges ───────────────────────────────────────────────────────────────────
41
- export function Badge({ tone = "none", title, children }) {
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
- export function Empty({ title, hint }) {
54
- return (_jsxs("div", { class: "empty", children: [_jsx("p", { class: "empty-title", children: title }), hint && _jsx("p", { class: "muted", children: hint })] }));
52
+ // ── Toolbars ─────────────────────────────────────────────────────────────────
53
+ export function Toolbar({ children }) {
54
+ return _jsx("div", { class: "toolbar", children: children });
55
55
  }
56
- export function Failure({ error, retry }) {
57
- return (_jsxs("div", { class: "failure", role: "alert", children: [_jsxs("p", { children: ["Could not load this: ", error instanceof Error ? error.message : String(error)] }), _jsx("button", { onClick: retry, children: "Retry" })] }));
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
  }