@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,25 @@
1
+ import { type ReactNode, type RefObject, type UIEvent } from "react";
2
+ export declare const at_bottom: (element: HTMLElement) => boolean;
3
+ export interface FollowTail<Element extends HTMLElement> {
4
+ ref: RefObject<Element | null>;
5
+ following: boolean;
6
+ set_following: (following: boolean) => void;
7
+ on_scroll: (event: UIEvent<Element>) => void;
8
+ }
9
+ export declare function use_follow_tail<Element extends HTMLElement>(dependency: unknown, active?: boolean): FollowTail<Element>;
10
+ interface FollowToggleProps {
11
+ following: boolean;
12
+ on_change: (following: boolean) => void;
13
+ disabled?: boolean;
14
+ label?: string;
15
+ }
16
+ export declare function FollowToggle({ following, on_change, disabled, label }: FollowToggleProps): import("react").JSX.Element;
17
+ interface FollowingOutputProps {
18
+ text: string;
19
+ active?: boolean;
20
+ toggle?: boolean;
21
+ class_name?: string;
22
+ tools?: ReactNode;
23
+ }
24
+ export declare function FollowingOutput({ text, active, toggle, class_name, tools }: FollowingOutputProps): import("react").JSX.Element;
25
+ export {};
@@ -0,0 +1,34 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // Output that keeps its newest lines in view as they arrive, unless the reader has scrolled up to read, with a Follow switch.
3
+ import { useCallback, useLayoutEffect, useRef, useState } from "react";
4
+ import { cx } from "../dom.js";
5
+ // How far above the bottom still counts as at the bottom.
6
+ const FOLLOW_SLACK_PIXELS = 8;
7
+ export const at_bottom = (element) => element.scrollTop + element.clientHeight >= element.scrollHeight - FOLLOW_SLACK_PIXELS;
8
+ // Scrolls the box to its end whenever the dependency changes while following; active false, such as once a run has ended, leaves the scroll alone.
9
+ export function use_follow_tail(dependency, active = true) {
10
+ const ref = useRef(null);
11
+ const [following, set_following] = useState(true);
12
+ useLayoutEffect(() => {
13
+ const element = ref.current;
14
+ if (following && active && element)
15
+ element.scrollTop = element.scrollHeight;
16
+ }, [dependency, following, active]);
17
+ const on_scroll = useCallback((event) => {
18
+ if (!active)
19
+ return;
20
+ set_following(at_bottom(event.currentTarget));
21
+ }, [active]);
22
+ return { ref, following, set_following, on_scroll };
23
+ }
24
+ export function FollowToggle({ following, on_change, disabled = false, label = "Follow" }) {
25
+ return (_jsxs("label", { className: "follow-toggle", children: [_jsx("input", { type: "checkbox", checked: following, disabled: disabled, onChange: (event) => on_change(event.currentTarget.checked) }), " ", label] }));
26
+ }
27
+ // Streamed text in an output box that follows its tail.
28
+ export function FollowingOutput({ text, active = true, toggle = false, class_name, tools }) {
29
+ const tail = use_follow_tail(text, active);
30
+ const output = _jsx("pre", { ref: tail.ref, className: cx("output", "following-output", class_name), onScroll: tail.on_scroll, children: text });
31
+ if (!toggle && tools === undefined)
32
+ return output;
33
+ return (_jsxs("div", { className: "following", children: [_jsxs("div", { className: "following-tools", children: [tools, toggle && _jsx(FollowToggle, { following: tail.following, on_change: tail.set_following, disabled: !active })] }), output] }));
34
+ }
@@ -0,0 +1,111 @@
1
+ import { type MouseEvent, type ReactNode } from "react";
2
+ import type { Tone } from "./badges.tsx";
3
+ export { moved } from "../tables.ts";
4
+ interface PageHeadingProps {
5
+ title: ReactNode;
6
+ subtitle?: ReactNode;
7
+ icon?: ReactNode;
8
+ banner?: string | ReactNode;
9
+ actions?: ReactNode;
10
+ class_name?: string;
11
+ level?: 1 | 2 | 3;
12
+ children?: ReactNode;
13
+ }
14
+ export declare function PageHeading({ title, subtitle, icon, banner, actions, class_name, level, children }: PageHeadingProps): import("react").JSX.Element;
15
+ interface SectionProps {
16
+ title: ReactNode;
17
+ tools?: ReactNode;
18
+ class_name?: string;
19
+ on_context_menu?: (event: MouseEvent<HTMLElement>) => void;
20
+ children?: ReactNode;
21
+ }
22
+ export declare function Section({ title, tools, class_name, on_context_menu, children }: SectionProps): import("react").JSX.Element;
23
+ export type Fact = [label: string, value: ReactNode, class_name?: string];
24
+ export declare function Facts({ rows, layout }: {
25
+ rows: (Fact | null | false)[];
26
+ layout?: "grid" | "rows";
27
+ }): import("react").JSX.Element;
28
+ export declare function Stack({ tight, children }: {
29
+ tight?: boolean;
30
+ children: ReactNode;
31
+ }): import("react").JSX.Element;
32
+ interface TileProps {
33
+ label: string;
34
+ value: ReactNode;
35
+ tone?: Tone;
36
+ class_name?: string | null;
37
+ href?: string;
38
+ value_first?: boolean;
39
+ }
40
+ export declare function Tile({ label, value, tone, class_name, href, value_first }: TileProps): import("react").JSX.Element;
41
+ export declare function Tiles({ children }: {
42
+ children: ReactNode;
43
+ }): import("react").JSX.Element;
44
+ interface CardProps {
45
+ title: ReactNode;
46
+ subtitle?: ReactNode;
47
+ badge?: ReactNode;
48
+ class_name?: string;
49
+ href?: string;
50
+ on_click?: () => void;
51
+ children?: ReactNode;
52
+ }
53
+ export declare function Card({ title, subtitle, badge, class_name, href, on_click, children }: CardProps): import("react").JSX.Element;
54
+ export declare function Cards({ children }: {
55
+ children: ReactNode;
56
+ }): import("react").JSX.Element;
57
+ export interface TabItem<Id extends string> {
58
+ id: Id;
59
+ label: string;
60
+ href?: string;
61
+ }
62
+ export declare function Tabs<Id extends string>({ items, active, label, on_select }: {
63
+ items: TabItem<Id>[];
64
+ active: Id;
65
+ label?: string;
66
+ on_select?: (id: Id) => void;
67
+ }): import("react").JSX.Element;
68
+ export declare function Crumbs({ items }: {
69
+ items: [label: string, href: string | null][];
70
+ }): import("react").JSX.Element;
71
+ export declare function Toolbar({ children }: {
72
+ children: ReactNode;
73
+ }): import("react").JSX.Element;
74
+ interface Step {
75
+ text: string;
76
+ href: string | null;
77
+ }
78
+ export declare function Stepper({ label, previous, position, next }: {
79
+ label: string;
80
+ previous: Step;
81
+ position: ReactNode;
82
+ next: Step;
83
+ }): import("react").JSX.Element;
84
+ export declare function SettingsGroup({ heading, hint, children }: {
85
+ heading: ReactNode;
86
+ hint?: ReactNode;
87
+ children: ReactNode;
88
+ }): import("react").JSX.Element;
89
+ export declare function SettingRow({ label, hint, as_label, class_name, children }: {
90
+ label: ReactNode;
91
+ hint?: ReactNode;
92
+ as_label?: boolean;
93
+ class_name?: string;
94
+ children: ReactNode;
95
+ }): import("react").JSX.Element;
96
+ interface RowActionsProps {
97
+ index: number;
98
+ count: number;
99
+ on_move: (from: number, to: number) => void;
100
+ on_remove?: () => void;
101
+ remove_label: string;
102
+ }
103
+ export declare function RowActions({ index, count, on_move, on_remove, remove_label }: RowActionsProps): import("react").JSX.Element;
104
+ interface SelectionBarProps {
105
+ count: number;
106
+ total: number;
107
+ on_clear: () => void;
108
+ on_select_all: () => void;
109
+ children: ReactNode;
110
+ }
111
+ export declare function SelectionBar({ count, total, on_clear, on_select_all, children }: SelectionBarProps): import("react").JSX.Element;
@@ -0,0 +1,85 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // The pieces a page is laid out with: the page heading, panels, facts, tiles, cards, tabs, breadcrumbs, toolbars, steppers, settings rows and list row buttons.
3
+ import { Fragment } from "react";
4
+ export { moved } from "../tables.js";
5
+ // A page's heading: an optional banner, the title with its icon and subtitle, and the page's buttons.
6
+ export function PageHeading({ title, subtitle, icon, banner, actions, class_name, level = 1, children }) {
7
+ const Title = `h${level}`;
8
+ return (_jsxs("header", { className: class_name ? `page-head ${class_name}` : "page-head", children: [typeof banner === "string" ? _jsx("img", { className: "page-banner", src: banner, alt: "", loading: "lazy" }) : banner, _jsxs("div", { className: "page-title spread", children: [_jsxs("div", { className: "page-identity", children: [icon, _jsxs("div", { children: [_jsx(Title, { children: title }), subtitle && _jsx("p", { className: "page-subtitle muted", children: subtitle })] })] }), actions && _jsx("div", { className: "head-actions", children: actions })] }), children] }));
9
+ }
10
+ export function Section({ title, tools, class_name, on_context_menu, children }) {
11
+ return (_jsxs("section", { className: class_name ? `panel ${class_name}` : "panel", onContextMenu: on_context_menu, children: [_jsxs("div", { className: "panel-head", children: [_jsx("h2", { children: title }), tools && _jsx("div", { className: "panel-tools", children: tools })] }), children] }));
12
+ }
13
+ // Labelled values in a grid of tiles, or as rows of label and value for long values such as paths.
14
+ export function Facts({ rows, layout = "grid" }) {
15
+ return (_jsx("dl", { className: layout === "rows" ? "facts facts-rows" : "facts", children: rows.filter((row) => Boolean(row)).map(([label, value, class_name]) => (_jsxs("div", { children: [_jsx("dt", { children: label }), _jsx("dd", { className: class_name, children: value ?? "—" })] }, label))) }));
16
+ }
17
+ // A stack of panels or blocks spaced by its gap, tighter when tight.
18
+ export function Stack({ tight = false, children }) {
19
+ return _jsx("div", { className: tight ? "stack tight" : "stack", children: children });
20
+ }
21
+ // One headline figure and its label.
22
+ export function Tile({ label, value, tone, class_name, href, value_first = false }) {
23
+ const tile_class = ["tile", tone && tone !== "none" && `tile-${tone}`, class_name].filter(Boolean).join(" ");
24
+ const label_part = _jsx("div", { className: "tile-label", children: label });
25
+ const value_part = _jsx("div", { className: "tile-value", children: value });
26
+ const children = value_first ? _jsxs(_Fragment, { children: [value_part, label_part] }) : _jsxs(_Fragment, { children: [label_part, value_part] });
27
+ return href ? _jsx("a", { className: tile_class, href: href, children: children }) : _jsx("div", { className: tile_class, children: children });
28
+ }
29
+ export function Tiles({ children }) {
30
+ return _jsx("div", { className: "tiles", children: children });
31
+ }
32
+ // A titled card for one thing among many, laid out in a grid of Cards.
33
+ export function Card({ title, subtitle, badge, class_name, href, on_click, children }) {
34
+ const card_class = class_name ? `card ${class_name}` : "card";
35
+ const body = (_jsxs(_Fragment, { children: [_jsxs("div", { className: "card-head", children: [_jsx("strong", { children: title }), badge] }), subtitle && _jsx("div", { className: "muted card-host", children: subtitle }), children] }));
36
+ if (href)
37
+ return _jsx("a", { className: card_class, href: href, children: body });
38
+ if (on_click)
39
+ return _jsx("button", { type: "button", className: card_class, onClick: on_click, children: body });
40
+ return _jsx("div", { className: card_class, children: body });
41
+ }
42
+ export function Cards({ children }) {
43
+ return _jsx("div", { className: "grid", children: children });
44
+ }
45
+ export function Tabs({ items, active, label = "Sections", on_select }) {
46
+ return (_jsx("nav", { className: "tabs", "aria-label": label, children: items.map((item) => {
47
+ const tab_class = item.id === active ? "tab active" : "tab";
48
+ const current = item.id === active ? "page" : undefined;
49
+ return item.href
50
+ ? _jsx("a", { className: tab_class, href: item.href, "aria-current": current, children: item.label }, item.id)
51
+ : _jsx("button", { type: "button", className: tab_class, "aria-current": current, onClick: () => on_select?.(item.id), children: item.label }, item.id);
52
+ }) }));
53
+ }
54
+ export function Crumbs({ items }) {
55
+ return (_jsx("nav", { className: "crumbs", "aria-label": "Breadcrumb", children: items.map(([label, href], index) => (_jsxs(Fragment, { children: [index > 0 && _jsx("span", { className: "muted", "aria-hidden": "true", children: " / " }), href ? _jsx("a", { href: href, children: label }) : _jsx("span", { "aria-current": "page", children: label })] }, index))) }));
56
+ }
57
+ // ── Toolbars ─────────────────────────────────────────────────────────────────
58
+ export function Toolbar({ children }) {
59
+ return _jsx("div", { className: "toolbar", children: children });
60
+ }
61
+ // 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.
62
+ export function Stepper({ label, previous, position, next }) {
63
+ const step = (target) => target.href === null
64
+ ? _jsx("button", { disabled: true, children: target.text })
65
+ : _jsx("a", { className: "button", href: target.href, children: target.text });
66
+ return _jsxs("div", { className: "stepper", role: "group", "aria-label": label, children: [step(previous), _jsx("span", { className: "muted", children: position }), step(next)] });
67
+ }
68
+ // ── Settings ─────────────────────────────────────────────────────────────────
69
+ export function SettingsGroup({ heading, hint, children }) {
70
+ return (_jsxs("section", { className: "settings-group", children: [_jsx("h3", { className: "settings-heading", children: heading }), hint && _jsx("p", { className: "setting-hint", children: hint }), children] }));
71
+ }
72
+ // A setting's name and hint on one side and its control on the other; as a label, clicking anywhere works its one control.
73
+ export function SettingRow({ label, hint, as_label = false, class_name, children }) {
74
+ const row_class = class_name ? `setting-row ${class_name}` : "setting-row";
75
+ const text = (_jsxs("span", { className: "setting-text", children: [_jsx("span", { className: "setting-label", children: label }), hint && _jsx("span", { className: "setting-hint", children: hint })] }));
76
+ return as_label ? _jsxs("label", { className: row_class, children: [text, children] }) : _jsxs("div", { className: row_class, children: [text, children] });
77
+ }
78
+ // The up, down and remove buttons of a row in an ordered list.
79
+ export function RowActions({ index, count, on_move, on_remove, remove_label }) {
80
+ return (_jsxs("span", { className: "row-actions", children: [_jsx("button", { type: "button", className: "icon", "aria-label": "Move up", disabled: index === 0, onClick: () => on_move(index, index - 1), children: "\u2191" }), _jsx("button", { type: "button", className: "icon", "aria-label": "Move down", disabled: index === count - 1, onClick: () => on_move(index, index + 1), children: "\u2193" }), on_remove && _jsx("button", { type: "button", className: "icon remove", "aria-label": remove_label, onClick: on_remove, children: "\u00D7" })] }));
81
+ }
82
+ // A bar floating at the foot of the page while rows are picked: how many, Select all, the actions, and a button that lets them go.
83
+ export function SelectionBar({ count, total, on_clear, on_select_all, children }) {
84
+ return (_jsxs("div", { className: "selection-bar", role: "toolbar", "aria-label": "Selected rows", children: [_jsx("button", { type: "button", className: "icon remove", "aria-label": "Clear selection", title: "Clear selection (Esc)", onClick: on_clear, children: "\u2715" }), _jsxs("span", { className: "selection-count", children: [count, " selected"] }), count < total && _jsx("button", { type: "button", onClick: on_select_all, children: "Select all" }), _jsx("span", { className: "selection-divider", "aria-hidden": "true" }), children] }));
85
+ }
@@ -0,0 +1,19 @@
1
+ export interface PickableList {
2
+ id: string;
3
+ name: string;
4
+ count?: number;
5
+ holds: boolean;
6
+ }
7
+ export interface ListPickerOptions {
8
+ title: string;
9
+ load: () => Promise<PickableList[]>;
10
+ add: (id: string) => Promise<void>;
11
+ create: (name: string) => Promise<PickableList>;
12
+ new_label?: string;
13
+ empty_text?: string;
14
+ }
15
+ export declare function ListPicker({ options, on_close }: {
16
+ options: ListPickerOptions;
17
+ on_close: () => void;
18
+ }): import("react").JSX.Element;
19
+ export declare function pick_list_dialog(options: ListPickerOptions): () => void;
@@ -0,0 +1,50 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // A dialog that adds one thing to any of the app's lists, such as a video to playlists, and makes a new list for it.
3
+ import { useState } from "react";
4
+ import { Badge } from "./badges.js";
5
+ import { Dialog, open_dialog } from "./dialog.js";
6
+ import { Empty, Loaded, use_resource } from "./states.js";
7
+ export function ListPicker({ options, on_close }) {
8
+ const lists = use_resource(options.load, []);
9
+ const [added, set_added] = useState([]);
10
+ const [new_name, set_new_name] = useState("");
11
+ const [busy, set_busy] = useState(false);
12
+ const new_label = options.new_label ?? "New list";
13
+ // Lists added to or made here show as holding the thing without loading them again.
14
+ const shown = (loaded) => {
15
+ const merged = loaded.map((list) => added.find((other) => other.id === list.id) ?? list);
16
+ return [...merged, ...added.filter((list) => !loaded.some((other) => other.id === list.id))];
17
+ };
18
+ async function add(list) {
19
+ set_busy(true);
20
+ try {
21
+ await options.add(list.id);
22
+ set_added((before) => [...before, { ...list, holds: true, count: list.count === undefined ? undefined : list.count + 1 }]);
23
+ }
24
+ finally {
25
+ set_busy(false);
26
+ }
27
+ }
28
+ async function create(event) {
29
+ event.preventDefault();
30
+ const name = new_name.trim();
31
+ if (!name)
32
+ return;
33
+ set_busy(true);
34
+ try {
35
+ const list = await options.create(name);
36
+ set_added((before) => [...before, { ...list, holds: true }]);
37
+ set_new_name("");
38
+ }
39
+ finally {
40
+ set_busy(false);
41
+ }
42
+ }
43
+ return (_jsxs(Dialog, { title: options.title, on_close: on_close, class_name: "list-picker", buttons: _jsx("button", { className: "accent solid", onClick: on_close, children: "Done" }), children: [_jsx(Loaded, { resource: lists, lines: 3, children: (loaded) => shown(loaded).length === 0
44
+ ? _jsx(Empty, { title: options.empty_text ?? "No lists yet." })
45
+ : (_jsx("ul", { className: "plain-list list-picker-lists", children: shown(loaded).map((list) => (_jsx("li", { children: _jsxs("button", { type: "button", className: "list-picker-item", "aria-pressed": list.holds, disabled: list.holds || busy, onClick: () => void add(list), children: [_jsx("span", { className: "list-picker-tick", "aria-hidden": "true", children: list.holds ? "✓" : "" }), _jsx("span", { className: "list-picker-name", children: list.name }), list.count !== undefined && _jsx(Badge, { children: list.count })] }) }, list.id))) })) }), _jsxs("form", { className: "list-picker-new", onSubmit: (event) => void create(event), children: [_jsx("input", { type: "text", placeholder: new_label, "aria-label": new_label, value: new_name, onChange: (event) => set_new_name(event.currentTarget.value) }), _jsx("button", { type: "submit", disabled: !new_name.trim() || busy, children: "Create" })] })] }));
46
+ }
47
+ // Opens the picker from anywhere, such as a card's menu; the Shell draws it.
48
+ export function pick_list_dialog(options) {
49
+ return open_dialog((close) => _jsx(ListPicker, { options: options, on_close: close }));
50
+ }
@@ -0,0 +1,8 @@
1
+ export declare function render_markdown(text: string): string;
2
+ interface MarkdownProps {
3
+ text: string;
4
+ class_name?: string;
5
+ follow?: boolean;
6
+ }
7
+ export declare function Markdown({ text, class_name, follow }: MarkdownProps): import("react").JSX.Element;
8
+ export {};
@@ -0,0 +1,40 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ // Markdown drawn as safe HTML on marked and DOMPurify: raw HTML shows as text, only web links are clickable, and they open in a new tab.
3
+ // marked matches the hand-written parser feature for feature (headings, lists, code, quotes, rules, emphasis, links) and adds tables and nested lists; DOMPurify drops anything a model might slip in.
4
+ import DOMPurify from "dompurify";
5
+ import { Marked } from "marked";
6
+ import { useLayoutEffect, useMemo, useRef } from "react";
7
+ import { cx } from "../dom.js";
8
+ import { at_bottom } from "./following.js";
9
+ const LINK_SCHEMES = ["http://", "https://"];
10
+ const escape_html = (text) => text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
11
+ const renderer = {
12
+ // Raw HTML in the text shows as written.
13
+ html({ text }) {
14
+ return escape_html(text);
15
+ },
16
+ // Only web links become links; another scheme shows as its text and address.
17
+ link({ href, title, tokens }) {
18
+ const text = this.parser.parseInline(tokens);
19
+ if (!LINK_SCHEMES.some((scheme) => href.startsWith(scheme)))
20
+ return `${text} (${escape_html(href)})`;
21
+ const title_part = title ? ` title="${escape_html(title)}"` : "";
22
+ return `<a href="${escape_html(href)}"${title_part} target="_blank" rel="noreferrer">${text}</a>`;
23
+ },
24
+ };
25
+ const marked = new Marked({ gfm: true, breaks: true, renderer });
26
+ // Markdown as sanitized HTML, ready for innerHTML.
27
+ export function render_markdown(text) {
28
+ const html = marked.parse(text, { async: false });
29
+ return DOMPurify.sanitize(html, { USE_PROFILES: { html: true }, ADD_ATTR: ["target"] });
30
+ }
31
+ export function Markdown({ text, class_name, follow = false }) {
32
+ const box = useRef(null);
33
+ const html = useMemo(() => render_markdown(text), [text]);
34
+ const following = follow && (box.current === null || at_bottom(box.current));
35
+ useLayoutEffect(() => {
36
+ if (following && box.current !== null)
37
+ box.current.scrollTop = box.current.scrollHeight;
38
+ }, [html]);
39
+ return _jsx("div", { ref: box, className: cx("markdown", class_name), dangerouslySetInnerHTML: { __html: html } });
40
+ }
@@ -0,0 +1,24 @@
1
+ import type { ReactNode } from "react";
2
+ interface AvatarProps {
3
+ src?: string | null;
4
+ name: string;
5
+ size?: "small" | "medium" | "large";
6
+ }
7
+ export declare function Avatar({ src, name, size }: AvatarProps): import("react").JSX.Element;
8
+ interface MediaTileProps {
9
+ title: ReactNode;
10
+ href: string;
11
+ image?: string | null;
12
+ corner?: ReactNode;
13
+ lines?: ReactNode[];
14
+ dimmed?: boolean;
15
+ actions?: ReactNode;
16
+ lead?: ReactNode;
17
+ }
18
+ export declare function MediaTile({ title, href, image, corner, lines, dimmed, actions, lead }: MediaTileProps): import("react").JSX.Element;
19
+ export declare function MediaGrid({ label, rows, children }: {
20
+ label?: string;
21
+ rows?: boolean;
22
+ children: ReactNode;
23
+ }): import("react").JSX.Element;
24
+ export {};
@@ -0,0 +1,18 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // A round picture of a person or channel, or the first letter of its name when there is none.
3
+ export function Avatar({ src, name, size = "medium" }) {
4
+ const avatar_class = `avatar avatar-${size}`;
5
+ if (src)
6
+ return _jsx("img", { className: avatar_class, src: src, alt: "", loading: "lazy" });
7
+ const first_letter = [...name.trim()][0]?.toLocaleUpperCase() ?? "?";
8
+ return _jsx("span", { className: `${avatar_class} avatar-initial`, "aria-hidden": "true", children: first_letter });
9
+ }
10
+ // One video, album or playlist: its picture, its title and a few lines about it.
11
+ export function MediaTile({ title, href, image, corner, lines = [], dimmed = false, actions, lead }) {
12
+ const shown_lines = lines.filter((line) => line !== null && line !== undefined && line !== false && line !== "");
13
+ return (_jsxs("div", { role: "listitem", className: dimmed ? "media-tile media-dimmed dimmed" : "media-tile", children: [lead !== undefined && _jsx("span", { className: "media-lead", children: lead }), _jsxs("a", { className: "media-picture", href: href, tabIndex: -1, "aria-hidden": "true", children: [image ? _jsx("img", { src: image, alt: "", loading: "lazy" }) : _jsx("span", { className: "media-blank" }), corner && _jsx("span", { className: "media-corner", children: corner })] }), _jsxs("div", { className: "media-text", children: [_jsx("a", { className: "media-title", href: href, children: title }), shown_lines.map((line, index) => _jsx("div", { className: "media-line", children: line }, index))] }), actions && _jsx("div", { className: "media-actions", children: actions })] }));
14
+ }
15
+ // Tiles side by side filling the width, or one under another as rows with the picture on the left.
16
+ export function MediaGrid({ label, rows = false, children }) {
17
+ return _jsx("div", { role: "list", "aria-label": label, className: rows ? "media-grid media-rows" : "media-grid", children: children });
18
+ }
@@ -0,0 +1,10 @@
1
+ import { type ReactNode } from "react";
2
+ import { type MenuItem } from "./menus.tsx";
3
+ interface MenuButtonProps {
4
+ label: string;
5
+ items: MenuItem[] | (() => MenuItem[]);
6
+ glyph?: ReactNode;
7
+ class_name?: string;
8
+ }
9
+ export declare function MenuButton({ label, items, glyph, class_name }: MenuButtonProps): import("react").JSX.Element;
10
+ export {};
@@ -0,0 +1,16 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ // A button that opens a menu under itself, such as the ⋮ on a card; the Shell draws the menu.
3
+ import { useState } from "react";
4
+ import { open_context_menu } from "./menus.js";
5
+ export function MenuButton({ label, items, glyph = "⋮", class_name }) {
6
+ const [open, set_open] = useState(false);
7
+ function open_menu(event) {
8
+ // The button may sit inside a link, such as a whole card that opens its video.
9
+ event.preventDefault();
10
+ event.stopPropagation();
11
+ const box = event.currentTarget.getBoundingClientRect();
12
+ set_open(true);
13
+ open_context_menu({ clientX: box.left, clientY: box.bottom, preventDefault: () => { } }, typeof items === "function" ? items() : items, () => set_open(false));
14
+ }
15
+ return (_jsx("button", { type: "button", className: class_name ? `icon menu-trigger ${class_name}` : "icon menu-trigger", "aria-label": label, title: label, "aria-haspopup": "menu", "aria-expanded": open, onClick: open_menu, children: glyph }));
16
+ }
@@ -0,0 +1,31 @@
1
+ import { type ReactNode } from "react";
2
+ import { type MenuItem } from "../menus.ts";
3
+ export type { MenuItem } from "../menus.ts";
4
+ interface MenuListProps {
5
+ items: MenuItem[];
6
+ on_close: () => void;
7
+ }
8
+ export declare function MenuList({ items, on_close }: MenuListProps): import("react").JSX.Element;
9
+ interface ContextMenuProps {
10
+ x: number;
11
+ y: number;
12
+ items: MenuItem[];
13
+ on_close: () => void;
14
+ label?: string;
15
+ }
16
+ export declare function ContextMenu({ x, y, items, on_close, label }: ContextMenuProps): import("react").JSX.Element;
17
+ export declare function open_context_menu(event: {
18
+ clientX: number;
19
+ clientY: number;
20
+ preventDefault(): void;
21
+ }, items: MenuItem[], on_close?: () => void): void;
22
+ export declare function ContextMenus(): import("react").JSX.Element | null;
23
+ interface PopoverProps {
24
+ anchor?: HTMLElement | null;
25
+ x?: number;
26
+ y?: number;
27
+ label: string;
28
+ on_close: () => void;
29
+ children: ReactNode;
30
+ }
31
+ export declare function Popover({ anchor, x, y, label, on_close, children }: PopoverProps): import("react").JSX.Element;
@@ -0,0 +1,16 @@
1
+ import type { ReactNode } from "react";
2
+ import { type TabItem } from "./layout.tsx";
3
+ export type { TabItem } from "./layout.tsx";
4
+ interface PageViewProps<Section extends string> {
5
+ title: string;
6
+ subtitle?: ReactNode;
7
+ on_close: () => void;
8
+ actions?: ReactNode;
9
+ sections?: TabItem<Section>[];
10
+ section?: Section;
11
+ on_section?: (section: Section) => void;
12
+ close_label?: string;
13
+ class_name?: string;
14
+ children: ReactNode;
15
+ }
16
+ export declare function PageView<Section extends string>({ title, subtitle, on_close, actions, sections, section, on_section, close_label, class_name, children }: PageViewProps<Section>): import("react").JSX.Element;
@@ -0,0 +1,6 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Dialog } from "./dialog.js";
3
+ import { Tabs } from "./layout.js";
4
+ export function PageView({ title, subtitle, on_close, actions, sections, section, on_section, close_label = "Close", class_name, children }) {
5
+ return (_jsxs(Dialog, { title: title, subtitle: subtitle, on_close: on_close, full: true, class_name: class_name ? `page-view ${class_name}` : "page-view", actions: _jsxs(_Fragment, { children: [actions, _jsx("button", { type: "button", onClick: on_close, title: `${close_label} (Esc)`, children: close_label })] }), children: [sections && section !== undefined && _jsx(Tabs, { label: `${title} sections`, items: sections, active: section, on_select: on_section }), children] }));
6
+ }
@@ -0,0 +1,2 @@
1
+ import { type StorageLike, type Validator } from "../prefs.ts";
2
+ export declare function use_pref<Value>(key: string, initial: Value, validate?: Validator<Value>, storage?: StorageLike | undefined): [Value, (next: Value) => void];
@@ -0,0 +1,11 @@
1
+ // A remembered setting as component state, read once when the component mounts and written on every change.
2
+ import { useState } from "react";
3
+ import { browser_storage, read_pref, write_pref } from "../prefs.js";
4
+ export function use_pref(key, initial, validate, storage = browser_storage()) {
5
+ const [value, set_value] = useState(() => read_pref(storage, key, initial, validate));
6
+ const change = (next) => {
7
+ set_value(next);
8
+ write_pref(storage, key, next);
9
+ };
10
+ return [value, change];
11
+ }
@@ -0,0 +1,10 @@
1
+ import type { ReactNode } from "react";
2
+ interface ProgressBarProps {
3
+ value: number | null;
4
+ max?: number;
5
+ label: string;
6
+ detail?: ReactNode | true;
7
+ class_name?: string;
8
+ }
9
+ export declare function ProgressBar({ value, max, label, detail, class_name }: ProgressBarProps): import("react").JSX.Element;
10
+ export {};
@@ -0,0 +1,9 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cx } from "../dom.js";
3
+ // The fill reads its width from --progress, as Halon's seek slider reads --range-progress.
4
+ export function ProgressBar({ value, max = 100, label, detail, class_name }) {
5
+ const known = value !== null && max > 0;
6
+ const percent = known ? Math.max(0, Math.min(100, Math.round((value / max) * 100))) : 0;
7
+ const words = detail === true ? `${percent}%` : detail;
8
+ return (_jsxs("div", { className: cx("progress", class_name), children: [_jsx("div", { className: cx("progress-bar", !known && "indeterminate"), role: "progressbar", "aria-label": label, "aria-valuemin": 0, "aria-valuemax": max, "aria-valuenow": known ? value : undefined, style: { "--progress": `${percent}%` }, children: _jsx("div", { className: "progress-fill" }) }), words !== undefined && _jsx("span", { className: "progress-detail muted", children: words })] }));
9
+ }
@@ -0,0 +1,8 @@
1
+ interface RatingProps {
2
+ value: number | null;
3
+ max?: number;
4
+ label?: string;
5
+ on_change?: (value: number | null) => void;
6
+ }
7
+ export declare function Rating({ value, max, label, on_change }: RatingProps): import("react").JSX.Element;
8
+ export {};
@@ -0,0 +1,23 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { cx } from "../dom.js";
3
+ const plural_stars = (count) => `${count} star${count === 1 ? "" : "s"}`;
4
+ // Arrow keys move one star, Home and End to the ends, and Delete or 0 clears the rating.
5
+ export function Rating({ value, max = 5, label = "Rating", on_change }) {
6
+ const lit = value ?? 0;
7
+ const read_only = on_change === undefined;
8
+ function key_down(event) {
9
+ if (read_only)
10
+ return;
11
+ const next = event.key === "ArrowRight" || event.key === "ArrowUp" ? Math.min(max, lit + 1)
12
+ : event.key === "ArrowLeft" || event.key === "ArrowDown" ? Math.max(0, lit - 1)
13
+ : event.key === "Home" ? 0
14
+ : event.key === "End" ? max
15
+ : event.key === "Delete" || event.key === "Backspace" || event.key === "0" ? null
16
+ : undefined;
17
+ if (next === undefined)
18
+ return;
19
+ event.preventDefault();
20
+ on_change(next);
21
+ }
22
+ return (_jsx("div", { className: cx("rating", read_only && "read-only"), role: "slider", tabIndex: read_only ? undefined : 0, "aria-label": label, "aria-readonly": read_only || undefined, "aria-valuemin": 0, "aria-valuemax": max, "aria-valuenow": lit, "aria-valuetext": value === null ? "Unrated" : plural_stars(value), onKeyDown: key_down, children: Array.from({ length: max }, (_, index) => index + 1).map((star) => (_jsx("button", { type: "button", className: cx("star", star <= lit && "on"), tabIndex: -1, disabled: read_only, "aria-label": plural_stars(star), onClick: () => on_change?.(star), children: "\u2605" }, star))) }));
23
+ }
@@ -0,0 +1,10 @@
1
+ export interface RevealOptions {
2
+ root_margin?: string;
3
+ initial_count?: number;
4
+ }
5
+ export interface Reveal {
6
+ shown_count: number;
7
+ more_ref: (element: Element | null) => void;
8
+ reset: () => void;
9
+ }
10
+ export declare function use_reveal(page_size: number, options?: RevealOptions): Reveal;
@@ -0,0 +1,30 @@
1
+ // A long list shown a page at a time, the next page added as its end scrolls into view.
2
+ import { useCallback, useEffect, useRef, useState } from "react";
3
+ export function use_reveal(page_size, options = {}) {
4
+ const root_margin = options.root_margin ?? "200px";
5
+ const [shown_count, set_shown_count] = useState(() => Math.max(page_size, options.initial_count ?? page_size));
6
+ const observer = useRef(null);
7
+ const marker = useRef(null);
8
+ const more_ref = useCallback((element) => {
9
+ observer.current?.disconnect();
10
+ observer.current = null;
11
+ marker.current = element;
12
+ if (!element)
13
+ return;
14
+ observer.current = new IntersectionObserver((entries) => {
15
+ if (entries.some((entry) => entry.isIntersecting))
16
+ set_shown_count((count) => count + page_size);
17
+ }, { rootMargin: root_margin });
18
+ observer.current.observe(element);
19
+ }, [page_size, root_margin]);
20
+ // Watching the marker afresh after each page asks again whether it is still in view, so a short page keeps filling.
21
+ useEffect(() => {
22
+ if (!observer.current || !marker.current)
23
+ return;
24
+ observer.current.unobserve(marker.current);
25
+ observer.current.observe(marker.current);
26
+ }, [shown_count]);
27
+ useEffect(() => () => observer.current?.disconnect(), []);
28
+ const reset = useCallback(() => set_shown_count(page_size), [page_size]);
29
+ return { shown_count, more_ref, reset };
30
+ }