@x-tech-laboratories/x-libs 0.14.1 → 0.14.2

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 (150) hide show
  1. package/dist/agent/client.d.ts +32 -0
  2. package/dist/agent/client.js +148 -0
  3. package/dist/agent/commands.d.ts +23 -0
  4. package/dist/agent/commands.js +118 -0
  5. package/dist/agent/keys.d.ts +14 -0
  6. package/dist/agent/keys.js +81 -0
  7. package/dist/agent/long_runs.d.ts +32 -0
  8. package/dist/agent/long_runs.js +316 -0
  9. package/dist/agent/protocol.d.ts +148 -0
  10. package/dist/agent/protocol.js +131 -0
  11. package/dist/agent/session.d.ts +50 -0
  12. package/dist/agent/session.js +225 -0
  13. package/dist/agent/tasks.d.ts +17 -0
  14. package/dist/agent/tasks.js +15 -0
  15. package/dist/backups.d.ts +31 -0
  16. package/dist/backups.js +43 -0
  17. package/dist/dates.d.ts +9 -0
  18. package/dist/dates.js +60 -0
  19. package/dist/durations.d.ts +11 -0
  20. package/dist/durations.js +49 -0
  21. package/dist/electron/app.d.ts +13 -0
  22. package/dist/electron/app.js +27 -0
  23. package/dist/electron/bridge.d.ts +55 -0
  24. package/dist/electron/bridge.js +105 -0
  25. package/dist/electron/events.d.ts +21 -0
  26. package/dist/electron/events.js +25 -0
  27. package/dist/electron/fetch.d.ts +52 -0
  28. package/dist/electron/fetch.js +175 -0
  29. package/dist/electron/window.d.ts +96 -0
  30. package/dist/electron/window.js +128 -0
  31. package/dist/log.d.ts +30 -0
  32. package/dist/log.js +79 -0
  33. package/dist/server/agents/agent.d.ts +27 -0
  34. package/dist/server/agents/agent.js +16 -0
  35. package/dist/server/agents/connection.d.ts +51 -0
  36. package/dist/server/agents/connection.js +239 -0
  37. package/dist/server/agents/hub.d.ts +86 -0
  38. package/dist/server/agents/hub.js +241 -0
  39. package/dist/server/agents/local.d.ts +24 -0
  40. package/dist/server/agents/local.js +62 -0
  41. package/dist/server/agents/route.d.ts +3 -0
  42. package/dist/server/agents/route.js +24 -0
  43. package/dist/server/backup_files.d.ts +3 -0
  44. package/dist/server/backup_files.js +25 -0
  45. package/dist/server/config.d.ts +39 -0
  46. package/dist/server/config.js +109 -0
  47. package/dist/server/cors.d.ts +8 -0
  48. package/dist/server/cors.js +13 -0
  49. package/dist/server/events.d.ts +33 -0
  50. package/dist/server/events.js +68 -0
  51. package/dist/server/hash_password.d.ts +1 -0
  52. package/dist/server/hash_password.js +46 -0
  53. package/dist/server/login_page.d.ts +8 -0
  54. package/dist/server/login_page.js +79 -0
  55. package/dist/server/passwords.d.ts +8 -0
  56. package/dist/server/passwords.js +59 -0
  57. package/dist/server/replies.d.ts +12 -0
  58. package/dist/server/replies.js +41 -0
  59. package/dist/server/sign_in.d.ts +27 -0
  60. package/dist/server/sign_in.js +95 -0
  61. package/dist/server/web_folder.d.ts +11 -0
  62. package/dist/server/web_folder.js +40 -0
  63. package/dist/web/dom.d.ts +7 -0
  64. package/dist/web/dom.js +32 -0
  65. package/dist/web/drag.d.ts +3 -0
  66. package/dist/web/drag.js +13 -0
  67. package/dist/web/errors.d.ts +1 -0
  68. package/dist/web/errors.js +27 -0
  69. package/dist/web/format.d.ts +11 -0
  70. package/dist/web/format.js +82 -0
  71. package/dist/web/menus.d.ts +17 -0
  72. package/dist/web/menus.js +40 -0
  73. package/dist/web/prefs.d.ts +12 -0
  74. package/dist/web/prefs.js +62 -0
  75. package/dist/web/react/badges.d.ts +35 -0
  76. package/dist/web/react/badges.js +32 -0
  77. package/dist/web/react/controls.d.ts +86 -0
  78. package/dist/web/react/controls.js +154 -0
  79. package/dist/web/react/dialog.d.ts +45 -0
  80. package/dist/web/react/dialog.js +82 -0
  81. package/dist/web/react/following.d.ts +25 -0
  82. package/dist/web/react/following.js +34 -0
  83. package/dist/web/react/layout.d.ts +111 -0
  84. package/dist/web/react/layout.js +85 -0
  85. package/dist/web/react/list_picker.d.ts +19 -0
  86. package/dist/web/react/list_picker.js +50 -0
  87. package/dist/web/react/markdown.d.ts +8 -0
  88. package/dist/web/react/markdown.js +40 -0
  89. package/dist/web/react/media.d.ts +24 -0
  90. package/dist/web/react/media.js +18 -0
  91. package/dist/web/react/menu_button.d.ts +10 -0
  92. package/dist/web/react/menu_button.js +16 -0
  93. package/dist/web/react/menus.d.ts +31 -0
  94. package/dist/web/react/page_view.d.ts +16 -0
  95. package/dist/web/react/page_view.js +6 -0
  96. package/dist/web/react/prefs.d.ts +2 -0
  97. package/dist/web/react/prefs.js +11 -0
  98. package/dist/web/react/progress.d.ts +10 -0
  99. package/dist/web/react/progress.js +9 -0
  100. package/dist/web/react/rating.d.ts +8 -0
  101. package/dist/web/react/rating.js +23 -0
  102. package/dist/web/react/reveal.d.ts +10 -0
  103. package/dist/web/react/reveal.js +30 -0
  104. package/dist/web/react/shell.d.ts +46 -0
  105. package/dist/web/react/shell.js +94 -0
  106. package/dist/web/react/shortcuts.d.ts +23 -0
  107. package/dist/web/react/shortcuts.js +95 -0
  108. package/dist/web/react/split.d.ts +23 -0
  109. package/dist/web/react/split.js +94 -0
  110. package/dist/web/react/states.d.ts +36 -0
  111. package/dist/web/react/states.js +64 -0
  112. package/dist/web/react/store.d.ts +4 -0
  113. package/dist/web/react/store.js +7 -0
  114. package/dist/web/react/suggestions.d.ts +23 -0
  115. package/dist/web/react/suggestions.js +57 -0
  116. package/dist/web/react/table.d.ts +78 -0
  117. package/dist/web/react/table.js +404 -0
  118. package/dist/web/react/test_stubs.d.ts +1 -0
  119. package/dist/web/react/test_stubs.js +25 -0
  120. package/dist/web/react/time.d.ts +5 -0
  121. package/dist/web/react/time.js +19 -0
  122. package/dist/web/react/toasts.d.ts +29 -0
  123. package/dist/web/react/toasts.js +72 -0
  124. package/dist/web/react/tree.d.ts +28 -0
  125. package/dist/web/react/tree.js +183 -0
  126. package/dist/web/react/volume.d.ts +8 -0
  127. package/dist/web/react/volume.js +42 -0
  128. package/dist/web/react/watch.d.ts +3 -0
  129. package/dist/web/react/watch.js +5 -0
  130. package/dist/web/router.d.ts +7 -0
  131. package/dist/web/router.js +29 -0
  132. package/dist/web/selection.d.ts +25 -0
  133. package/dist/web/selection.js +106 -0
  134. package/dist/web/split.d.ts +15 -0
  135. package/dist/web/split.js +104 -0
  136. package/dist/web/states.d.ts +3 -0
  137. package/dist/web/states.js +10 -0
  138. package/dist/web/store.d.ts +11 -0
  139. package/dist/web/store.js +17 -0
  140. package/dist/web/tables.d.ts +28 -0
  141. package/dist/web/tables.js +88 -0
  142. package/dist/web/theme.d.ts +14 -0
  143. package/dist/web/theme.js +52 -0
  144. package/dist/web/tree.d.ts +40 -0
  145. package/dist/web/tree.js +130 -0
  146. package/dist/web/virtual.d.ts +7 -0
  147. package/dist/web/virtual.js +10 -0
  148. package/dist/web/watch.d.ts +11 -0
  149. package/dist/web/watch.js +37 -0
  150. package/package.json +1 -1
@@ -0,0 +1,82 @@
1
+ // Writes times, durations, sizes and counts short enough for a table cell.
2
+ import pretty_bytes from "pretty-bytes";
3
+ // ── Times ────────────────────────────────────────────────────────────────────
4
+ export function now_seconds() {
5
+ return Date.now() / 1000;
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
+ }
21
+ export function age(epoch) {
22
+ if (!epoch)
23
+ return "never";
24
+ return relative_time(-Math.max(0, now_seconds() - epoch));
25
+ }
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.
27
+ export function until(epoch) {
28
+ if (epoch === null)
29
+ return "—";
30
+ const seconds = epoch - now_seconds();
31
+ if (seconds < 0)
32
+ return age(epoch);
33
+ return seconds < 90 ? "now" : relative_time(seconds);
34
+ }
35
+ export function duration(seconds) {
36
+ if (seconds === null)
37
+ return "—";
38
+ // Settings are often whole days or hours, which read better without a decimal.
39
+ if (seconds >= 86400 && seconds % 86400 === 0)
40
+ return `${seconds / 86400}d`;
41
+ if (seconds < 120)
42
+ return `${Math.round(seconds)}s`;
43
+ if (seconds < 7200)
44
+ return `${Math.round(seconds / 60)}m`;
45
+ if (seconds < 172800)
46
+ return seconds % 3600 === 0 ? `${seconds / 3600}h` : `${(seconds / 3600).toFixed(1)}h`;
47
+ return `${Math.round(seconds / 86400)}d`;
48
+ }
49
+ export function stamp(epoch) {
50
+ return new Date(epoch * 1000).toLocaleString();
51
+ }
52
+ // ── Sizes ────────────────────────────────────────────────────────────────────
53
+ // Sizes in binary units, such as "44.4 GiB".
54
+ export function bytes(count) {
55
+ return pretty_bytes(count, { binary: true, maximumFractionDigits: 1 });
56
+ }
57
+ // Sizes in decimal units, such as "1.2 GB".
58
+ export function size(count) {
59
+ if (count === null || count === undefined)
60
+ return "—";
61
+ return pretty_bytes(count, { maximumFractionDigits: 1 });
62
+ }
63
+ // A size change with its sign, such as +1.2 GB or -300 MB.
64
+ export function size_change(count) {
65
+ if (count === null || count === undefined)
66
+ return "—";
67
+ return count === 0 ? size(0) : pretty_bytes(count, { signed: true, maximumFractionDigits: 1 });
68
+ }
69
+ // ── Counts ───────────────────────────────────────────────────────────────────
70
+ export function count_change(value) {
71
+ if (value === null || value === undefined)
72
+ return "—";
73
+ return value > 0 ? `+${value.toLocaleString()}` : value < 0 ? `−${Math.abs(value).toLocaleString()}` : "0";
74
+ }
75
+ export function plural(count, word) {
76
+ return `${count.toLocaleString()} ${word}${plurals.select(count) === "one" ? "" : "s"}`;
77
+ }
78
+ const compact = new Intl.NumberFormat("en", { notation: "compact", maximumFractionDigits: 1 });
79
+ // A count in a few characters, such as "1.4M", "150K" or "950".
80
+ export function compact_count(count) {
81
+ return compact.format(count);
82
+ }
@@ -0,0 +1,17 @@
1
+ export interface MenuItem {
2
+ label: string;
3
+ run: () => void;
4
+ hint?: string;
5
+ disabled?: boolean;
6
+ danger?: boolean;
7
+ separator_before?: boolean;
8
+ checked?: boolean;
9
+ radio?: boolean;
10
+ keep_open?: boolean;
11
+ }
12
+ export declare function on_screen_position(x: number, y: number, width: number, height: number, room_width?: number, room_height?: number): {
13
+ left: number;
14
+ top: number;
15
+ };
16
+ export declare function item_after_key(key: string, current: number, count: number): number | null;
17
+ export declare function item_starting_with(labels: readonly string[], typed: string, current: number): number | null;
@@ -0,0 +1,40 @@
1
+ // What a menu item holds, where a menu opened at a point goes so it stays on screen, and which item a key moves to.
2
+ const edge_gap = 4;
3
+ // Flipped to the point's other side near an edge, or pushed back from the edge when neither side has room.
4
+ export function on_screen_position(x, y, width, height, room_width = window.innerWidth, room_height = window.innerHeight) {
5
+ const place = (start, size, room) => {
6
+ if (start + size + edge_gap <= room)
7
+ return start;
8
+ if (start - size >= edge_gap)
9
+ return start - size;
10
+ return Math.max(edge_gap, room - size - edge_gap);
11
+ };
12
+ return { left: place(x, width, room_width), top: place(y, height, room_height) };
13
+ }
14
+ // ── Keys ─────────────────────────────────────────────────────────────────────
15
+ // The item an arrow, Home or End key moves to from the current one, wrapping round, or null for any other key.
16
+ export function item_after_key(key, current, count) {
17
+ if (count === 0)
18
+ return null;
19
+ switch (key) {
20
+ case "ArrowDown": return current < 0 ? 0 : (current + 1) % count;
21
+ case "ArrowUp": return current < 0 ? count - 1 : (current - 1 + count) % count;
22
+ case "Home": return 0;
23
+ case "End": return count - 1;
24
+ default: return null;
25
+ }
26
+ }
27
+ // The first item after the current one whose label starts with the typed text, or null when none does.
28
+ export function item_starting_with(labels, typed, current) {
29
+ const wanted = typed.toLocaleLowerCase();
30
+ // A repeated first letter steps on through the items that start with it.
31
+ const repeated = wanted.length > 1 && [...wanted].every((letter) => letter === wanted[0]);
32
+ const prefix = repeated ? wanted[0] : wanted;
33
+ const start = repeated || wanted.length === 1 ? current + 1 : Math.max(current, 0);
34
+ for (let offset = 0; offset < labels.length; offset += 1) {
35
+ const index = (start + offset) % labels.length;
36
+ if (labels[index].trim().toLocaleLowerCase().startsWith(prefix))
37
+ return index;
38
+ }
39
+ return null;
40
+ }
@@ -0,0 +1,12 @@
1
+ export interface StorageLike {
2
+ getItem(key: string): string | null;
3
+ setItem(key: string, value: string): void;
4
+ removeItem(key: string): void;
5
+ }
6
+ export type Validator<Value> = (value: unknown) => value is Value;
7
+ export declare function browser_storage(): StorageLike | undefined;
8
+ export declare function read_pref<Value>(storage: StorageLike | undefined, key: string, initial: Value, validate?: Validator<Value>): Value;
9
+ export declare function write_pref(storage: StorageLike | undefined, key: string, value: unknown): void;
10
+ export declare function one_of<Value extends string>(...allowed: readonly Value[]): Validator<Value>;
11
+ export declare function is_boolean(value: unknown): value is boolean;
12
+ export declare function is_number_in(min: number, max: number): Validator<number>;
@@ -0,0 +1,62 @@
1
+ // Remembered view settings: a stored value that is corrupt, from an older version or of the wrong shape reads as the default.
2
+ // This browser's localStorage, or nothing where there is none or it may not be touched.
3
+ export function browser_storage() {
4
+ try {
5
+ return typeof localStorage === "undefined" ? undefined : localStorage;
6
+ }
7
+ catch {
8
+ return undefined;
9
+ }
10
+ }
11
+ // ── Reading and writing ──────────────────────────────────────────────────────
12
+ // The stored value, or the initial one whenever the stored one cannot be used.
13
+ export function read_pref(storage, key, initial, validate) {
14
+ if (!storage)
15
+ return initial;
16
+ let raw;
17
+ try {
18
+ raw = storage.getItem(key);
19
+ }
20
+ catch {
21
+ return initial;
22
+ }
23
+ if (raw === null)
24
+ return initial;
25
+ let parsed;
26
+ try {
27
+ parsed = JSON.parse(raw);
28
+ }
29
+ catch {
30
+ return initial;
31
+ }
32
+ if (validate)
33
+ return validate(parsed) ? parsed : initial;
34
+ // Without a validator the stored value must at least have the initial value's shape.
35
+ if (parsed === null || initial === null)
36
+ return parsed ?? initial;
37
+ if (Array.isArray(initial) !== Array.isArray(parsed))
38
+ return initial;
39
+ if (typeof parsed !== typeof initial)
40
+ return initial;
41
+ return parsed;
42
+ }
43
+ export function write_pref(storage, key, value) {
44
+ if (!storage)
45
+ return;
46
+ try {
47
+ storage.setItem(key, JSON.stringify(value));
48
+ }
49
+ catch {
50
+ // A full or disabled storage keeps the setting until the page closes.
51
+ }
52
+ }
53
+ // ── Validators ───────────────────────────────────────────────────────────────
54
+ export function one_of(...allowed) {
55
+ return (value) => typeof value === "string" && allowed.includes(value);
56
+ }
57
+ export function is_boolean(value) {
58
+ return typeof value === "boolean";
59
+ }
60
+ export function is_number_in(min, max) {
61
+ return (value) => typeof value === "number" && Number.isFinite(value) && value >= min && value <= max;
62
+ }
@@ -0,0 +1,35 @@
1
+ import type { ReactNode } from "react";
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: ReactNode;
7
+ }): import("react").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("react").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: ReactNode;
18
+ }): import("react").JSX.Element;
19
+ export declare function Chips({ inline, children }: {
20
+ inline?: boolean;
21
+ children: ReactNode;
22
+ }): import("react").JSX.Element;
23
+ export declare function Verdict({ ok, label }: {
24
+ ok: boolean | null | undefined;
25
+ label: string;
26
+ }): import("react").JSX.Element;
27
+ interface StatusPillProps {
28
+ tone?: Tone;
29
+ busy?: boolean;
30
+ label: ReactNode;
31
+ action?: ReactNode;
32
+ title?: string;
33
+ }
34
+ export declare function StatusPill({ tone, busy, label, action, title }: StatusPillProps): import("react").JSX.Element;
35
+ export {};
@@ -0,0 +1,32 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // ── Badges ───────────────────────────────────────────────────────────────────
3
+ export function Badge({ tone = "none", title, children }) {
4
+ return _jsx("span", { className: `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", className: chip_class, title: title, onClick: on_click, children: children })
17
+ : _jsx("span", { className: 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", { className: 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", { className: `verdict tone-${known ? (ok ? "good" : "bad") : "none"}`, children: [label, ": ", known ? (ok ? "pass" : "fail") : "no verdict"] });
28
+ }
29
+ // A state dot and its text, such as "Idle" or "Researching", with an optional button, for a top bar.
30
+ export function StatusPill({ tone = "none", busy = false, label, action, title }) {
31
+ return (_jsxs("span", { className: "status-pill", title: title, children: [_jsx("span", { className: busy ? `status-dot tone-${tone} busy` : `status-dot tone-${tone}`, "aria-hidden": "true" }), _jsx("span", { className: "status-text small", children: label }), action] }));
32
+ }
@@ -0,0 +1,86 @@
1
+ import { type InputHTMLAttributes, type ReactNode } from "react";
2
+ import { type TreeItem } from "../tree.ts";
3
+ interface SegmentedProps<Value extends string> {
4
+ label: string;
5
+ choices: {
6
+ value: Value;
7
+ label: string;
8
+ }[];
9
+ value: Value;
10
+ on_change: (value: Value) => void;
11
+ class_name?: string;
12
+ }
13
+ export declare function Segmented<Value extends string>({ label, choices, value, on_change, class_name }: SegmentedProps<Value>): import("react").JSX.Element;
14
+ export declare function ButtonGroup({ label, children }: {
15
+ label: string;
16
+ children: ReactNode;
17
+ }): import("react").JSX.Element;
18
+ interface SwitchProps {
19
+ label: string;
20
+ checked: boolean;
21
+ on_change: (on: boolean) => Promise<boolean | void> | boolean | void;
22
+ disabled?: boolean;
23
+ }
24
+ export declare function Switch({ label, checked, on_change, disabled }: SwitchProps): import("react").JSX.Element;
25
+ interface SearchBoxProps {
26
+ label: string;
27
+ value: string;
28
+ on_input?: (value: string) => void;
29
+ on_submit?: (value: string) => void;
30
+ on_paste?: (text: string) => boolean;
31
+ submit_label?: string;
32
+ clear_on_submit?: boolean;
33
+ auto_focus?: boolean;
34
+ }
35
+ export declare function SearchBox({ label, value, on_input, on_submit, on_paste, submit_label, clear_on_submit, auto_focus }: SearchBoxProps): import("react").JSX.Element;
36
+ interface SelectProps<Value extends string | number> {
37
+ label: string;
38
+ options: [value: Value, text: string][];
39
+ value: Value;
40
+ on_change: (value: Value) => void;
41
+ bare?: boolean;
42
+ }
43
+ export declare function Select<Value extends string | number>({ label, options, value, on_change, bare }: SelectProps<Value>): import("react").JSX.Element;
44
+ type CommitInputProps = Omit<InputHTMLAttributes<HTMLInputElement>, "value" | "defaultValue" | "onChange"> & {
45
+ value: string | number;
46
+ on_commit: (event: {
47
+ currentTarget: HTMLInputElement;
48
+ }) => void;
49
+ };
50
+ export declare function CommitInput({ value, on_commit, onBlur, onKeyDown, ...rest }: CommitInputProps): import("react").JSX.Element;
51
+ interface DateTimeFieldProps {
52
+ label: string;
53
+ value: string | null;
54
+ on_change: (value: string | null) => void;
55
+ allow_time?: boolean;
56
+ }
57
+ export declare function DateTimeField({ label, value, on_change, allow_time }: DateTimeFieldProps): import("react").JSX.Element;
58
+ interface PathInputProps {
59
+ label: string;
60
+ value: string;
61
+ on_change: (value: string) => void;
62
+ pick: (current: string) => Promise<string | null>;
63
+ placeholder?: string;
64
+ disabled?: boolean;
65
+ browse_label?: string;
66
+ }
67
+ export declare function PathInput({ label, value, on_change, pick, placeholder, disabled, browse_label }: PathInputProps): import("react").JSX.Element;
68
+ interface ColorPickerProps {
69
+ label: string;
70
+ value: string | null;
71
+ swatches: string[];
72
+ on_change: (color: string | null) => void;
73
+ allow_none?: boolean;
74
+ }
75
+ export declare function ColorPicker({ label, value, swatches, on_change, allow_none }: ColorPickerProps): import("react").JSX.Element;
76
+ interface TreeSelectProps<Item extends TreeItem> {
77
+ label: string;
78
+ items: Item[];
79
+ name_of: (item: Item) => string;
80
+ value: string;
81
+ blank_label: string;
82
+ disabled_ids?: readonly string[];
83
+ on_change: (id: string) => void;
84
+ }
85
+ export declare function TreeSelect<Item extends TreeItem>({ label, items, name_of, value, blank_label, disabled_ids, on_change }: TreeSelectProps<Item>): import("react").JSX.Element;
86
+ export {};
@@ -0,0 +1,154 @@
1
+ import { createElement as _createElement } from "react";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ // React controls drawn like the page's own: segmented choices, switches, search fields, selects, and date, colour and tree pickers.
4
+ import { createElement, useEffect, useRef, useState } from "react";
5
+ import { in_tree_order, path_names } from "../tree.js";
6
+ // One of a few choices side by side; the chosen one takes the accent.
7
+ export function Segmented({ label, choices, value, on_change, class_name }) {
8
+ return (_jsx("div", { className: class_name ? `segmented ${class_name}` : "segmented", role: "radiogroup", "aria-label": label, children: choices.map((choice) => (_jsx("button", { type: "button", role: "radio", "aria-checked": value === choice.value, onClick: () => on_change(choice.value), children: choice.label }, choice.value))) }));
9
+ }
10
+ // Buttons joined into one control, such as zoom out, zoom in and fit.
11
+ export function ButtonGroup({ label, children }) {
12
+ return _jsx("div", { className: "segmented", role: "group", "aria-label": label, children: children });
13
+ }
14
+ export function Switch({ label, checked, on_change, disabled = false }) {
15
+ const [busy, set_busy] = useState(false);
16
+ async function change(event) {
17
+ const box = event.currentTarget;
18
+ const on = box.checked;
19
+ set_busy(true);
20
+ try {
21
+ if ((await on_change(on)) === false)
22
+ box.checked = !on;
23
+ }
24
+ catch (error) {
25
+ box.checked = !on;
26
+ throw error;
27
+ }
28
+ finally {
29
+ set_busy(false);
30
+ }
31
+ }
32
+ return _jsx("input", { type: "checkbox", role: "switch", "aria-label": label, checked: checked, disabled: disabled || busy, onChange: change });
33
+ }
34
+ // A search field that hands on what was typed once typing pauses and submits it on Enter; Escape clears it; the field keeps its own text, so a redraw never wipes typing, and shows a value changed from outside, such as one cleared.
35
+ export function SearchBox({ label, value, on_input, on_submit, on_paste, submit_label, clear_on_submit = false, auto_focus = false }) {
36
+ const field = useRef(null);
37
+ const timer = useRef(undefined);
38
+ const handed_on = useRef(value);
39
+ const submitted = useRef(value);
40
+ useEffect(() => () => clearTimeout(timer.current), []);
41
+ useEffect(() => {
42
+ if (value === handed_on.current || !field.current)
43
+ return;
44
+ clearTimeout(timer.current);
45
+ handed_on.current = value;
46
+ submitted.current = value;
47
+ field.current.value = value;
48
+ }, [value]);
49
+ function hand_on(text) {
50
+ clearTimeout(timer.current);
51
+ if (text === handed_on.current)
52
+ return;
53
+ handed_on.current = text;
54
+ on_input?.(text);
55
+ }
56
+ function submit() {
57
+ const text = field.current?.value ?? "";
58
+ hand_on(text);
59
+ submitted.current = clear_on_submit ? "" : text;
60
+ on_submit?.(text);
61
+ if (clear_on_submit && field.current) {
62
+ field.current.value = "";
63
+ handed_on.current = "";
64
+ }
65
+ }
66
+ function clear() {
67
+ if (!field.current)
68
+ return;
69
+ field.current.value = "";
70
+ hand_on("");
71
+ if (submitted.current !== "") {
72
+ submitted.current = "";
73
+ on_submit?.("");
74
+ }
75
+ }
76
+ function typed(event) {
77
+ clearTimeout(timer.current);
78
+ const text = event.currentTarget.value;
79
+ // An emptied field goes straight back to everything, taking back what was submitted.
80
+ if (text === "")
81
+ return clear();
82
+ if (!on_input)
83
+ return;
84
+ timer.current = window.setTimeout(() => hand_on(text), 200);
85
+ }
86
+ function keyed(event) {
87
+ if (event.key === "Enter") {
88
+ event.preventDefault();
89
+ submit();
90
+ }
91
+ else if (event.key === "Escape" && event.currentTarget.value !== "") {
92
+ event.preventDefault();
93
+ clear();
94
+ }
95
+ }
96
+ function pasted(event) {
97
+ if (on_paste?.(event.clipboardData.getData("text")))
98
+ event.preventDefault();
99
+ }
100
+ const input = (_jsx("input", { ref: field, type: "search", className: "filter", placeholder: label, "aria-label": label, defaultValue: value, autoFocus: auto_focus, onChange: typed, onKeyDown: keyed, onPaste: on_paste && pasted }));
101
+ if (!submit_label)
102
+ return input;
103
+ return (_jsxs("form", { className: "search-box", role: "search", onSubmit: (event) => { event.preventDefault(); submit(); }, children: [input, _jsx("button", { type: "submit", className: "accent solid", children: submit_label })] }));
104
+ }
105
+ export function Select({ label, options, value, on_change, bare = false }) {
106
+ const change = (event) => {
107
+ const text = event.currentTarget.value;
108
+ on_change((options.find(([option]) => String(option) === text)?.[0] ?? text));
109
+ };
110
+ const select = (_jsx("select", { value: value, "aria-label": bare ? label : undefined, onChange: change, children: options.map(([option, text]) => _jsx("option", { value: option, children: text }, option)) }));
111
+ if (bare)
112
+ return select;
113
+ return (_jsxs("label", { className: "field", children: [_jsx("span", { className: "field-label", children: label }), select] }));
114
+ }
115
+ // A field that saves once it is left or Enter is pressed, as a native change event does, and shows the saved value again whenever that changes.
116
+ export function CommitInput({ value, on_commit, onBlur, onKeyDown, ...rest }) {
117
+ return (_createElement("input", { ...rest, key: String(value), defaultValue: value, onBlur: (event) => { onBlur?.(event); on_commit(event); }, onKeyDown: (event) => { onKeyDown?.(event); if (event.key === "Enter")
118
+ event.currentTarget.blur(); } }));
119
+ }
120
+ // A local date with an optional time and a clear button; clearing the date clears the time.
121
+ export function DateTimeField({ label, value, on_change, allow_time = true }) {
122
+ const [day = "", time = ""] = (value ?? "").split("T");
123
+ const join = (next_day, next_time) => on_change(next_day ? (next_time && allow_time ? `${next_day}T${next_time}` : next_day) : null);
124
+ return (_jsxs("div", { className: "field date-time-field", children: [_jsx("span", { className: "field-label", children: label }), _jsxs("div", { className: "date-time-inputs", children: [_jsx("input", { type: "date", "aria-label": `${label} date`, value: day, onChange: (event) => join(event.currentTarget.value, time) }), day && allow_time && _jsx("input", { type: "time", "aria-label": `${label} time`, value: time, onChange: (event) => join(day, event.currentTarget.value) }), day && _jsx("button", { type: "button", className: "icon remove", "aria-label": `Clear ${label.toLowerCase()} date`, onClick: () => on_change(null), children: "\u00D7" })] })] }));
125
+ }
126
+ // A path typed by hand or picked through a Browse button.
127
+ export function PathInput({ label, value, on_change, pick, placeholder, disabled = false, browse_label = "Browse…" }) {
128
+ const [picking, set_picking] = useState(false);
129
+ async function browse() {
130
+ set_picking(true);
131
+ try {
132
+ const chosen = await pick(value);
133
+ if (chosen !== null)
134
+ on_change(chosen);
135
+ }
136
+ finally {
137
+ set_picking(false);
138
+ }
139
+ }
140
+ return (_jsxs("span", { className: "path-row", children: [_jsx("input", { type: "text", "aria-label": label, spellCheck: false, placeholder: placeholder, disabled: disabled, value: value, onChange: (event) => on_change(event.currentTarget.value) }), _jsx("button", { type: "button", disabled: disabled || picking, onClick: () => void browse(), children: browse_label })] }));
141
+ }
142
+ // Swatches, a None choice, and the system colour chooser for any other colour.
143
+ export function ColorPicker({ label, value, swatches, on_change, allow_none = true }) {
144
+ const is_swatch = value !== null && swatches.some((swatch) => swatch.toLowerCase() === value.toLowerCase());
145
+ const custom = value !== null && !is_swatch;
146
+ return (_jsxs("div", { className: "color-picker", role: "radiogroup", "aria-label": label, children: [allow_none && _jsx("button", { type: "button", role: "radio", className: "color-swatch none", "aria-checked": value === null, "aria-label": "No colour", onClick: () => on_change(null) }), swatches.map((swatch) => (_jsx("button", { type: "button", role: "radio", className: "color-swatch", style: { "--swatch-color": swatch }, "aria-checked": value !== null && swatch.toLowerCase() === value.toLowerCase(), "aria-label": swatch, title: swatch, onClick: () => on_change(swatch) }, swatch))), _jsx("input", { type: "color", className: custom ? "color-custom chosen" : "color-custom", "aria-label": "Other colour", value: value ?? "#888888", onChange: (event) => on_change(event.currentTarget.value) })] }));
147
+ }
148
+ // A select listing a tree indented, each choice's whole path on hover and in the closed select.
149
+ export function TreeSelect({ label, items, name_of, value, blank_label, disabled_ids = [], on_change }) {
150
+ const depth_of = (item) => path_names(item, items, name_of).length - 1;
151
+ const chosen = items.find((item) => item.id === value);
152
+ // The open list indents each name by its depth, and the closed face shows the picked choice's whole path.
153
+ return (_jsxs("select", { className: "tree-select", "aria-label": label, value: value, title: chosen ? path_names(chosen, items, name_of).join(" › ") : undefined, onChange: (event) => on_change(event.currentTarget.value), children: [_jsx("button", { type: "button", children: createElement("selectedcontent") }), _jsx("option", { value: "", children: blank_label }), in_tree_order(items).map((item) => (_jsxs("option", { value: item.id, disabled: disabled_ids.includes(item.id), title: path_names(item, items, name_of).join(" › "), style: { "--depth": depth_of(item) }, children: [_jsx("span", { className: "tree-option-name", children: name_of(item) }), _jsx("span", { className: "tree-option-path", children: path_names(item, items, name_of).join(" › ") })] }, item.id)))] }));
154
+ }
@@ -0,0 +1,45 @@
1
+ import { type FormEvent, type ReactNode } from "react";
2
+ export interface DialogForm {
3
+ on_submit: (event: FormEvent<HTMLFormElement>) => void;
4
+ submit_label?: ReactNode;
5
+ submit_disabled?: boolean;
6
+ danger?: boolean;
7
+ }
8
+ interface DialogProps {
9
+ title: ReactNode;
10
+ subtitle?: ReactNode;
11
+ actions?: ReactNode;
12
+ on_close: () => void;
13
+ buttons?: ReactNode;
14
+ form?: DialogForm;
15
+ wide?: boolean;
16
+ full?: boolean;
17
+ class_name?: string;
18
+ children: ReactNode;
19
+ }
20
+ export declare function Dialog({ title, subtitle, actions, on_close, buttons, form, wide, full, class_name, children }: DialogProps): import("react").JSX.Element;
21
+ export interface ConfirmOptions {
22
+ title: string;
23
+ message: ReactNode;
24
+ confirm_label: string;
25
+ busy_label?: string;
26
+ danger?: boolean;
27
+ wide?: boolean;
28
+ type_to_confirm?: string;
29
+ run: () => Promise<void>;
30
+ }
31
+ export declare function ConfirmDialog({ options, on_close }: {
32
+ options: ConfirmOptions;
33
+ on_close: () => void;
34
+ }): import("react").JSX.Element;
35
+ export declare function TextDialog({ title, text, on_close }: {
36
+ title: string;
37
+ text: string;
38
+ on_close: () => void;
39
+ }): import("react").JSX.Element;
40
+ type Builder = (close: () => void) => ReactNode;
41
+ export declare function open_dialog(build: Builder): () => void;
42
+ export declare function confirm_dialog(options: ConfirmOptions): void;
43
+ export declare function text_dialog(title: string, text: string): void;
44
+ export declare function Dialogs(): import("react").JSX.Element;
45
+ export {};
@@ -0,0 +1,82 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/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, useEffect, useId, useRef, useState } from "react";
4
+ import { toast } from "./toasts.js";
5
+ // Shown while it is rendered: the page behind it is inert, focus stays inside, Escape calls on_close, and focus goes back to the opener after.
6
+ export function Dialog({ title, subtitle, actions, on_close, buttons, form, wide = false, full = false, class_name, children }) {
7
+ const dialog = useRef(null);
8
+ const title_id = useId();
9
+ const close = useRef(on_close);
10
+ close.current = on_close;
11
+ useEffect(() => {
12
+ const element = dialog.current;
13
+ const opener = document.activeElement;
14
+ element.showModal();
15
+ return () => {
16
+ element.close();
17
+ // The dialog is already gone from the page here, so the browser cannot hand focus back by itself.
18
+ if (opener?.isConnected && opener !== document.body)
19
+ opener.focus();
20
+ };
21
+ }, []);
22
+ const heading = _jsx("h2", { id: title_id, children: title });
23
+ const submit = form?.submit_label !== undefined && (_jsx("button", { type: "submit", className: form.danger ? "danger solid" : "accent solid", disabled: form.submit_disabled, children: form.submit_label }));
24
+ const foot = (buttons || submit) && _jsxs("div", { className: "dialog-buttons", children: [buttons, submit] });
25
+ const body = _jsx("div", { className: "dialog-body", children: children });
26
+ return (_jsxs("dialog", { ref: dialog, className: ["dialog", wide && "wide", full && "full", class_name].filter(Boolean).join(" "), "aria-labelledby": title_id, onCancel: (event) => {
27
+ // Escape asks the owner to close, so the dialog never closes behind its owner's back.
28
+ event.preventDefault();
29
+ event.stopPropagation();
30
+ close.current();
31
+ }, children: [subtitle === undefined && actions === undefined ? heading : (_jsxs("header", { className: "dialog-header", children: [_jsxs("div", { className: "dialog-heading", children: [heading, subtitle !== undefined && _jsx("p", { className: "dialog-subtitle", children: subtitle })] }), actions !== undefined && _jsx("div", { className: "dialog-actions", children: actions })] })), form ? (_jsxs("form", { className: "dialog-form", onSubmit: (event) => { event.preventDefault(); form.on_submit(event); }, children: [body, foot] })) : _jsxs(_Fragment, { children: [body, foot] })] }));
32
+ }
33
+ export function ConfirmDialog({ options, on_close }) {
34
+ const [typed, set_typed] = useState("");
35
+ const [busy, set_busy] = useState(false);
36
+ const ready = options.type_to_confirm === undefined || typed === options.type_to_confirm;
37
+ async function confirm() {
38
+ set_busy(true);
39
+ try {
40
+ await options.run();
41
+ }
42
+ finally {
43
+ on_close();
44
+ }
45
+ }
46
+ return (_jsxs(Dialog, { title: options.title, on_close: () => { if (!busy)
47
+ on_close(); }, wide: options.wide, buttons: _jsxs(_Fragment, { children: [_jsx("button", { disabled: busy, onClick: on_close, children: "Cancel" }), _jsx("button", { className: options.danger ? "danger solid" : "accent solid", disabled: !ready || busy, onClick: confirm, autoFocus: options.type_to_confirm === undefined, children: busy ? options.busy_label ?? "Working…" : options.confirm_label })] }), children: [typeof options.message === "string" ? _jsx("p", { children: options.message }) : options.message, options.type_to_confirm !== undefined && (_jsxs("label", { className: "type-confirm", children: [_jsxs("span", { children: ["Type ", _jsx("strong", { children: options.type_to_confirm }), " to confirm:"] }), _jsx("input", { type: "text", className: "filter", autoComplete: "off", autoFocus: true, "aria-label": `Type ${options.type_to_confirm} to confirm`, value: typed, onChange: (event) => set_typed(event.currentTarget.value) })] }))] }));
48
+ }
49
+ // Shows a long text result, such as a preview, with a button to copy it.
50
+ export function TextDialog({ title, text, on_close }) {
51
+ return (_jsx(Dialog, { title: title, on_close: on_close, wide: true, buttons: _jsxs(_Fragment, { children: [_jsx("button", { onClick: () => void navigator.clipboard?.writeText(text).then(() => toast("Copied.")), children: "Copy" }), _jsx("button", { className: "accent solid", onClick: on_close, children: "Close" })] }), children: _jsx("pre", { className: "output", children: text }) }));
52
+ }
53
+ let open = [];
54
+ let next_id = 1;
55
+ const listeners = new Set();
56
+ function publish(dialogs) {
57
+ open = dialogs;
58
+ for (const listener of listeners)
59
+ listener(open);
60
+ }
61
+ // Opens what the builder makes, such as a Dialog, from an event handler; returns the function that closes it.
62
+ export function open_dialog(build) {
63
+ const id = next_id++;
64
+ const close = () => publish(open.filter((dialog) => dialog.id !== id));
65
+ publish([...open, { id, build }]);
66
+ return close;
67
+ }
68
+ export function confirm_dialog(options) {
69
+ open_dialog((close) => _jsx(ConfirmDialog, { options: options, on_close: close }));
70
+ }
71
+ export function text_dialog(title, text) {
72
+ open_dialog((close) => _jsx(TextDialog, { title: title, text: text, on_close: close }));
73
+ }
74
+ // Where dialogs opened from anywhere are drawn; the Shell holds one.
75
+ export function Dialogs() {
76
+ const [dialogs, set_dialogs] = useState(open);
77
+ useEffect(() => {
78
+ listeners.add(set_dialogs);
79
+ return () => void listeners.delete(set_dialogs);
80
+ }, []);
81
+ return _jsx(_Fragment, { children: dialogs.map((dialog) => _jsx(Fragment, { children: dialog.build(() => publish(open.filter((other) => other.id !== dialog.id))) }, dialog.id)) });
82
+ }