@x-tech-laboratories/x-libs 0.4.0 → 0.7.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 (87) hide show
  1. package/README.md +89 -13
  2. package/dist/backups.d.ts +31 -0
  3. package/dist/backups.js +43 -0
  4. package/dist/dates.d.ts +9 -0
  5. package/dist/dates.js +60 -0
  6. package/dist/durations.d.ts +9 -1
  7. package/dist/durations.js +19 -5
  8. package/dist/electron/app.d.ts +13 -0
  9. package/dist/electron/app.js +27 -0
  10. package/dist/electron/bridge.d.ts +32 -0
  11. package/dist/electron/bridge.js +53 -0
  12. package/dist/electron/events.d.ts +21 -0
  13. package/dist/electron/events.js +25 -0
  14. package/dist/electron/fetch.d.ts +25 -0
  15. package/dist/electron/fetch.js +67 -0
  16. package/dist/electron/window.d.ts +96 -0
  17. package/dist/electron/window.js +128 -0
  18. package/dist/log.d.ts +30 -0
  19. package/dist/log.js +79 -0
  20. package/dist/server/backup_files.d.ts +3 -0
  21. package/dist/server/backup_files.js +25 -0
  22. package/dist/web/drag.d.ts +3 -0
  23. package/dist/web/drag.js +13 -0
  24. package/dist/web/format.d.ts +1 -0
  25. package/dist/web/format.js +5 -0
  26. package/dist/web/menus.d.ts +4 -0
  27. package/dist/web/menus.js +28 -1
  28. package/dist/web/prefs.d.ts +12 -0
  29. package/dist/web/prefs.js +62 -0
  30. package/dist/web/{preact → react}/badges.d.ts +9 -9
  31. package/dist/web/{preact → react}/badges.js +6 -6
  32. package/dist/web/react/controls.d.ts +72 -0
  33. package/dist/web/react/controls.js +88 -0
  34. package/dist/web/{preact → react}/dialog.d.ts +10 -10
  35. package/dist/web/{preact → react}/dialog.js +14 -9
  36. package/dist/web/react/layout.d.ts +100 -0
  37. package/dist/web/react/layout.js +80 -0
  38. package/dist/web/react/menus.d.ts +31 -0
  39. package/dist/web/react/menus.js +159 -0
  40. package/dist/web/react/prefs.d.ts +2 -0
  41. package/dist/web/react/prefs.js +11 -0
  42. package/dist/web/react/reveal.d.ts +9 -0
  43. package/dist/web/react/reveal.js +30 -0
  44. package/dist/web/react/shell.d.ts +41 -0
  45. package/dist/web/react/shell.js +90 -0
  46. package/dist/web/react/shortcuts.d.ts +23 -0
  47. package/dist/web/react/shortcuts.js +95 -0
  48. package/dist/web/{preact → react}/split.d.ts +4 -4
  49. package/dist/web/{preact → react}/split.js +9 -10
  50. package/dist/web/{preact → react}/states.d.ts +7 -7
  51. package/dist/web/{preact → react}/states.js +6 -5
  52. package/dist/web/react/table.d.ts +69 -0
  53. package/dist/web/react/table.js +278 -0
  54. package/dist/web/{preact → react}/time.d.ts +2 -1
  55. package/dist/web/react/time.js +19 -0
  56. package/dist/web/react/toasts.d.ts +29 -0
  57. package/dist/web/react/toasts.js +72 -0
  58. package/dist/web/react/tree.d.ts +28 -0
  59. package/dist/web/react/tree.js +183 -0
  60. package/dist/web/react/watch.d.ts +3 -0
  61. package/dist/web/react/watch.js +5 -0
  62. package/dist/web/selection.d.ts +25 -0
  63. package/dist/web/selection.js +106 -0
  64. package/dist/web/tables.d.ts +19 -1
  65. package/dist/web/tables.js +78 -8
  66. package/dist/web/theme.d.ts +6 -1
  67. package/dist/web/theme.js +31 -21
  68. package/dist/web/tree.d.ts +37 -0
  69. package/dist/web/tree.js +128 -0
  70. package/dist/web/virtual.d.ts +7 -0
  71. package/dist/web/virtual.js +10 -0
  72. package/dist/web/watch.d.ts +6 -0
  73. package/dist/web/watch.js +17 -0
  74. package/package.json +15 -4
  75. package/dist/web/preact/controls.d.ts +0 -37
  76. package/dist/web/preact/controls.js +0 -43
  77. package/dist/web/preact/layout.d.ts +0 -80
  78. package/dist/web/preact/layout.js +0 -71
  79. package/dist/web/preact/menus.d.ts +0 -19
  80. package/dist/web/preact/menus.js +0 -70
  81. package/dist/web/preact/shell.d.ts +0 -27
  82. package/dist/web/preact/shell.js +0 -63
  83. package/dist/web/preact/table.d.ts +0 -23
  84. package/dist/web/preact/table.js +0 -20
  85. package/dist/web/preact/time.js +0 -9
  86. package/dist/web/preact/toasts.d.ts +0 -15
  87. package/dist/web/preact/toasts.js +0 -34
@@ -1,43 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
2
- import { useEffect, useRef, useState } from "preact/hooks";
3
- // One of a few choices side by side; the chosen one takes the accent.
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 });
10
- }
11
- export function Switch({ label, checked, on_change, disabled = false }) {
12
- const [busy, set_busy] = useState(false);
13
- async function change(event) {
14
- const box = event.currentTarget;
15
- const on = box.checked;
16
- set_busy(true);
17
- try {
18
- if ((await on_change(on)) === false)
19
- box.checked = !on;
20
- }
21
- catch (error) {
22
- box.checked = !on;
23
- throw error;
24
- }
25
- finally {
26
- set_busy(false);
27
- }
28
- }
29
- return _jsx("input", { type: "checkbox", role: "switch", "aria-label": label, checked: checked, disabled: disabled || busy, onChange: change });
30
- }
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.
32
- export function SearchBox({ label, value, on_input }) {
33
- const timer = useRef(undefined);
34
- useEffect(() => () => clearTimeout(timer.current), []);
35
- return (_jsx("input", { type: "search", class: "filter", placeholder: label, "aria-label": label, defaultValue: value, onInput: (event) => {
36
- clearTimeout(timer.current);
37
- const text = event.currentTarget.value;
38
- timer.current = window.setTimeout(() => on_input(text), 200);
39
- } }));
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,80 +0,0 @@
1
- import { type ComponentChildren } from "preact";
2
- import type { Tone } from "./badges.tsx";
3
- export declare function Section({ title, tools, children }: {
4
- title: ComponentChildren;
5
- tools?: ComponentChildren;
6
- children?: ComponentChildren;
7
- }): import("preact").JSX.Element;
8
- export type Fact = [label: string, value: ComponentChildren, class_name?: string];
9
- export declare function Facts({ rows, layout }: {
10
- rows: (Fact | null | false)[];
11
- layout?: "grid" | "rows";
12
- }): import("preact").JSX.Element;
13
- export declare function Stack({ tight, children }: {
14
- tight?: boolean;
15
- children: ComponentChildren;
16
- }): import("preact").JSX.Element;
17
- interface TileProps {
18
- label: string;
19
- value: ComponentChildren;
20
- tone?: Tone;
21
- class_name?: string | null;
22
- href?: string;
23
- value_first?: boolean;
24
- }
25
- export declare function Tile({ label, value, tone, class_name, href, value_first }: TileProps): import("preact").JSX.Element;
26
- export declare function Tiles({ children }: {
27
- children: ComponentChildren;
28
- }): import("preact").JSX.Element;
29
- interface CardProps {
30
- title: ComponentChildren;
31
- subtitle?: ComponentChildren;
32
- badge?: ComponentChildren;
33
- class_name?: string;
34
- href?: string;
35
- on_click?: () => void;
36
- children?: ComponentChildren;
37
- }
38
- export declare function Card({ title, subtitle, badge, class_name, href, on_click, children }: CardProps): import("preact").JSX.Element;
39
- export declare function Cards({ children }: {
40
- children: ComponentChildren;
41
- }): import("preact").JSX.Element;
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>[];
49
- active: Id;
50
- label?: string;
51
- on_select?: (id: Id) => void;
52
- }): import("preact").JSX.Element;
53
- export declare function Crumbs({ items }: {
54
- items: [label: string, href: string | null][];
55
- }): import("preact").JSX.Element;
56
- export declare function Toolbar({ children }: {
57
- children: ComponentChildren;
58
- }): import("preact").JSX.Element;
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
- }): import("preact").JSX.Element;
69
- export declare function SettingsGroup({ heading, hint, children }: {
70
- heading: string;
71
- hint?: ComponentChildren;
72
- children: ComponentChildren;
73
- }): import("preact").JSX.Element;
74
- export declare function SettingRow({ label, hint, as_label, children }: {
75
- label: ComponentChildren;
76
- hint?: ComponentChildren;
77
- as_label?: boolean;
78
- children: ComponentChildren;
79
- }): import("preact").JSX.Element;
80
- export {};
@@ -1,71 +0,0 @@
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 ───────────────────────────────────────────────────────────────────
5
- export function Section({ title, tools, children }) {
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] }));
7
- }
8
- // Labelled values in a grid of tiles, or as rows of label and value for long values such as paths.
9
- export function Facts({ rows, layout = "grid" }) {
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))) }));
11
- }
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] });
22
- return href ? _jsx("a", { class: tile_class, href: href, children: children }) : _jsx("div", { class: tile_class, children: children });
23
- }
24
- export function Tiles({ children }) {
25
- return _jsx("div", { class: "tiles", children: children });
26
- }
27
- // A titled card for one thing among many, laid out in a grid of Cards.
28
- export function Card({ title, subtitle, badge, class_name, href, on_click, children }) {
29
- const card_class = class_name ? `card ${class_name}` : "card";
30
- const body = (_jsxs(_Fragment, { children: [_jsxs("div", { class: "card-head", children: [_jsx("strong", { children: title }), badge] }), subtitle && _jsx("div", { class: "muted card-host", children: subtitle }), children] }));
31
- if (href)
32
- return _jsx("a", { class: card_class, href: href, children: body });
33
- if (on_click)
34
- return _jsx("button", { type: "button", class: card_class, onClick: on_click, children: body });
35
- return _jsx("div", { class: card_class, children: body });
36
- }
37
- export function Cards({ children }) {
38
- return _jsx("div", { class: "grid", children: children });
39
- }
40
- export function Tabs({ items, active, label = "Sections", on_select }) {
41
- return (_jsx("nav", { class: "tabs", "aria-label": label, children: items.map((item) => {
42
- const tab_class = item.id === active ? "tab active" : "tab";
43
- const current = item.id === active ? "page" : undefined;
44
- return item.href
45
- ? _jsx("a", { class: tab_class, href: item.href, "aria-current": current, children: item.label }, item.id)
46
- : _jsx("button", { type: "button", class: tab_class, "aria-current": current, onClick: () => on_select?.(item.id), children: item.label }, item.id);
47
- }) }));
48
- }
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))) }));
51
- }
52
- // ── Toolbars ─────────────────────────────────────────────────────────────────
53
- export function Toolbar({ children }) {
54
- return _jsx("div", { class: "toolbar", children: children });
55
- }
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)] });
62
- }
63
- // ── Settings ─────────────────────────────────────────────────────────────────
64
- export function SettingsGroup({ heading, hint, children }) {
65
- return (_jsxs("section", { class: "settings-group", children: [_jsx("h3", { class: "settings-heading", children: heading }), hint && _jsx("p", { class: "setting-hint", children: hint }), children] }));
66
- }
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.
68
- export function SettingRow({ label, hint, as_label = false, children }) {
69
- const text = (_jsxs("span", { class: "setting-text", children: [_jsx("span", { class: "setting-label", children: label }), hint && _jsx("span", { class: "setting-hint", children: hint })] }));
70
- return as_label ? _jsxs("label", { class: "setting-row", children: [text, children] }) : _jsxs("div", { class: "setting-row", children: [text, children] });
71
- }
@@ -1,19 +0,0 @@
1
- import { type ComponentChildren } from "preact";
2
- import { type MenuItem } from "../menus.ts";
3
- export type { MenuItem } from "../menus.ts";
4
- interface ContextMenuProps {
5
- x: number;
6
- y: number;
7
- items: MenuItem[];
8
- on_close: () => void;
9
- }
10
- export declare function ContextMenu({ x, y, items, on_close }: ContextMenuProps): import("preact").JSX.Element;
11
- interface PopoverProps {
12
- anchor?: HTMLElement | null;
13
- x?: number;
14
- y?: number;
15
- label: string;
16
- on_close: () => void;
17
- children: ComponentChildren;
18
- }
19
- export declare function Popover({ anchor, x, y, label, on_close, children }: PopoverProps): import("preact").JSX.Element;
@@ -1,70 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
2
- // Menus and popovers on the native popover: closed by Escape or a click elsewhere, and here also by scrolling or leaving the window.
3
- import { Fragment } from "preact";
4
- import { useEffect, useId, useLayoutEffect, useRef } from "preact/hooks";
5
- import { on_screen_position } from "../menus.js";
6
- // ── Opening ──────────────────────────────────────────────────────────────────
7
- // Shows the element as a popover while it is rendered, calling the latest on_close once it closes for any reason.
8
- function use_popover(on_close) {
9
- const element = useRef(null);
10
- const close = useRef(on_close);
11
- close.current = on_close;
12
- useEffect(() => {
13
- const popover = element.current;
14
- const hide = () => { if (popover.matches(":popover-open"))
15
- popover.hidePopover(); };
16
- const toggled = (event) => { if (event.newState === "closed")
17
- close.current(); };
18
- const outside_scroll = (event) => { if (!popover.contains(event.target))
19
- hide(); };
20
- popover.addEventListener("toggle", toggled);
21
- window.addEventListener("blur", hide);
22
- window.addEventListener("scroll", outside_scroll, true);
23
- return () => {
24
- popover.removeEventListener("toggle", toggled);
25
- window.removeEventListener("blur", hide);
26
- window.removeEventListener("scroll", outside_scroll, true);
27
- };
28
- }, []);
29
- return element;
30
- }
31
- // Shows the popover at the point, flipped or pushed back so it stays on screen.
32
- function show_at(popover, x, y) {
33
- if (!popover.matches(":popover-open"))
34
- popover.showPopover();
35
- const size = popover.getBoundingClientRect();
36
- const position = on_screen_position(x, y, size.width, size.height);
37
- popover.style.left = `${position.left}px`;
38
- popover.style.top = `${position.top}px`;
39
- }
40
- export function ContextMenu({ x, y, items, on_close }) {
41
- const menu = use_popover(on_close);
42
- useLayoutEffect(() => {
43
- show_at(menu.current, x, y);
44
- menu.current.querySelector(".menu-item:not(:disabled)")?.focus();
45
- }, [x, y]);
46
- return (_jsx("div", { ref: menu, popover: "auto", class: "menu", role: "menu", tabIndex: -1, children: items.map((item) => (_jsxs(Fragment, { children: [item.separator_before && _jsx("div", { class: "menu-separator", role: "separator" }), _jsxs("button", { type: "button", class: item.danger ? "menu-item danger" : "menu-item", role: item.checked === undefined ? "menuitem" : "menuitemcheckbox", "aria-checked": item.checked, disabled: item.disabled, onClick: () => {
47
- on_close();
48
- item.run();
49
- }, children: [_jsx("span", { class: "menu-label", children: item.label }), item.hint && _jsx("span", { class: "menu-hint", children: item.hint })] })] }, item.label))) }));
50
- }
51
- // Holds any content, such as a column's filter, with its first field ready to type into.
52
- export function Popover({ anchor, x = 0, y = 0, label, on_close, children }) {
53
- const popover = use_popover(on_close);
54
- const anchor_name = `--popover-${useId().replace(/[^\w-]/g, "")}`;
55
- useLayoutEffect(() => {
56
- const element = popover.current;
57
- if (anchor) {
58
- // The anchor's name ties it to this popover, which CSS then hangs below it, flipping where there is no room.
59
- anchor.style.setProperty("anchor-name", anchor_name);
60
- element.style.setProperty("position-anchor", anchor_name);
61
- element.showPopover();
62
- }
63
- else {
64
- show_at(element, x, y);
65
- }
66
- element.querySelector("input, select, textarea, button")?.focus();
67
- return () => anchor?.style.removeProperty("anchor-name");
68
- }, [anchor, x, y]);
69
- return (_jsx("div", { ref: popover, popover: "auto", class: anchor ? "popover anchored" : "popover", role: "dialog", "aria-label": label, children: children }));
70
- }
@@ -1,27 +0,0 @@
1
- import type { ComponentChildren } from "preact";
2
- import { type Route } from "../router.ts";
3
- import { type ThemeChoice } from "../theme.ts";
4
- export declare function use_route(): Route;
5
- export declare function ThemeButton({ choice }: {
6
- choice: ThemeChoice;
7
- }): import("preact").JSX.Element;
8
- export interface ShellSection {
9
- id: string;
10
- label: string;
11
- href: string;
12
- }
13
- interface ShellProps {
14
- brand: string;
15
- sections: ShellSection[];
16
- active: string;
17
- theme: ThemeChoice;
18
- user: string | null;
19
- user_href?: string;
20
- on_logout?: () => void;
21
- tools?: ComponentChildren;
22
- sidebar_foot?: ComponentChildren;
23
- view_key?: string;
24
- children: ComponentChildren;
25
- }
26
- export declare function Shell({ brand, sections, active, theme, user, user_href, on_logout, tools, sidebar_foot, view_key, children }: ShellProps): import("preact").JSX.Element;
27
- export {};
@@ -1,63 +0,0 @@
1
- import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
2
- import { useEffect, useRef, useState } from "preact/hooks";
3
- import { current_route } from "../router.js";
4
- import { next_theme } from "../theme.js";
5
- import { Dialogs } from "./dialog.js";
6
- import { Toasts } from "./toasts.js";
7
- // ── Route ────────────────────────────────────────────────────────────────────
8
- // The hash route, read again whenever it changes.
9
- export function use_route() {
10
- const [route, set_route] = useState(current_route);
11
- useEffect(() => {
12
- const changed = () => set_route(current_route());
13
- window.addEventListener("hashchange", changed);
14
- return () => window.removeEventListener("hashchange", changed);
15
- }, []);
16
- return route;
17
- }
18
- // ── Theme ────────────────────────────────────────────────────────────────────
19
- const theme_labels = { system: ["◐", "Theme: system"], light: ["☀", "Theme: light"], dark: ["☾", "Theme: dark"] };
20
- // Steps through system, light and dark, its glyph and label showing the current one.
21
- export function ThemeButton({ choice }) {
22
- const [theme, set_theme] = useState(choice.get);
23
- useEffect(() => choice.on_change(set_theme), []);
24
- const [icon, text] = theme_labels[theme];
25
- return (_jsx("button", { id: "theme", class: "icon", title: `${text} (click to change)`, "aria-label": `${text}, click to change`, onClick: () => choice.set(next_theme(choice.get())), children: icon }));
26
- }
27
- export function Shell({ brand, sections, active, theme, user, user_href, on_logout, tools, sidebar_foot, view_key, children }) {
28
- const [menu_open, set_menu_open] = useState(false);
29
- const main = useRef(null);
30
- const menu_button = useRef(null);
31
- const sidebar = useRef(null);
32
- const first_view = useRef(true);
33
- const logged_out = user === null;
34
- // The login page has no theme button, so the saved theme is set here.
35
- useEffect(() => theme.apply(), []);
36
- useEffect(() => {
37
- document.body.classList.toggle("logged-out", logged_out);
38
- document.body.classList.toggle("menu-open", menu_open);
39
- }, [logged_out, menu_open]);
40
- useEffect(() => {
41
- if (menu_open)
42
- sidebar.current?.querySelector("a")?.focus();
43
- if (!menu_open)
44
- return;
45
- const escape = (event) => {
46
- if (event.key !== "Escape")
47
- return;
48
- set_menu_open(false);
49
- menu_button.current?.focus();
50
- };
51
- document.addEventListener("keydown", escape);
52
- return () => document.removeEventListener("keydown", escape);
53
- }, [menu_open]);
54
- // A new view closes the menu and takes focus, which tells a screen reader the page changed, but not on the first load.
55
- useEffect(() => {
56
- set_menu_open(false);
57
- if (!first_view.current)
58
- main.current?.focus({ preventScroll: true });
59
- first_view.current = false;
60
- window.scrollTo(0, 0);
61
- }, [view_key]);
62
- return (_jsxs(_Fragment, { children: [_jsx("a", { class: "skip", href: "#main", children: "Skip to content" }), _jsxs("header", { class: "topbar", children: [_jsx("button", { ref: menu_button, class: "icon menu-button", "aria-label": "Open the menu", "aria-expanded": menu_open, "aria-controls": "sidebar", onClick: () => set_menu_open(!menu_open), children: "\u2630" }), _jsx("a", { href: "#/", class: "brand", children: brand }), !logged_out && tools, !logged_out && _jsx(ThemeButton, { choice: theme }), !logged_out && (user_href ? _jsx("a", { class: "account-link", href: user_href, children: user }) : _jsx("span", { class: "user-name", children: user })), !logged_out && on_logout && _jsx("button", { onClick: on_logout, children: "Log out" })] }), _jsxs("div", { class: "layout", children: [_jsxs("nav", { ref: sidebar, id: "sidebar", class: "sidebar", "aria-label": "Main", children: [sections.map((section) => (_jsx("a", { href: section.href, "data-section": section.id, class: section.id === active ? "active" : undefined, "aria-current": section.id === active ? "page" : undefined, children: section.label }, section.id))), sidebar_foot] }), _jsx("div", { class: "scrim", hidden: !menu_open, onClick: () => set_menu_open(false) }), _jsx("main", { ref: main, id: "main", tabIndex: -1, children: children })] }), _jsx(Toasts, {}), _jsx(Dialogs, {})] }));
63
- }
@@ -1,23 +0,0 @@
1
- import type { ComponentChildren } from "preact";
2
- import { type Sorting } from "../tables.ts";
3
- export type { Sorting } from "../tables.ts";
4
- export interface TableColumn<Row> {
5
- label: string;
6
- cell: (row: Row) => ComponentChildren;
7
- sort?: (row: Row) => string | number | null;
8
- class?: string;
9
- }
10
- interface DataTableProps<Row> {
11
- columns: TableColumn<Row>[];
12
- rows: Row[];
13
- row_key?: (row: Row) => string | number;
14
- sorting?: Sorting;
15
- on_sort?: (sorting: Sorting) => void;
16
- caption?: string;
17
- }
18
- export declare function DataTable<Row>({ columns, rows, row_key, sorting, on_sort, caption }: DataTableProps<Row>): import("preact").JSX.Element;
19
- export declare function Pager({ page, has_more, go }: {
20
- page: number;
21
- has_more: boolean;
22
- go: (page: number) => void;
23
- }): import("preact").JSX.Element | null;
@@ -1,20 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
2
- import { sorted_rows } from "../tables.js";
3
- export function DataTable({ columns, rows, row_key, sorting, on_sort, caption }) {
4
- const sort_column = sorting ? columns.find((column) => column.label === sorting.key && column.sort) : undefined;
5
- function header(column) {
6
- if (!column.sort || !on_sort)
7
- return _jsx("th", { class: column.class, scope: "col", children: column.label }, column.label);
8
- const active = sort_column === column;
9
- const descending = active && sorting.descending;
10
- return (_jsx("th", { class: column.class, scope: "col", "aria-sort": active ? (descending ? "descending" : "ascending") : "none", children: _jsxs("button", { class: "sort", onClick: () => on_sort({ key: column.label, descending: active ? !descending : false }), children: [column.label, active ? (descending ? " ↓" : " ↑") : ""] }) }, column.label));
11
- }
12
- return (_jsx("div", { class: "table-wrap", children: _jsxs("table", { class: "responsive", children: [caption && _jsx("caption", { class: "visually-hidden", children: caption }), _jsx("thead", { children: _jsx("tr", { children: columns.map(header) }) }), _jsx("tbody", { children: sorted_rows(columns, rows, sorting).map((row) => (_jsx("tr", { children: columns.map((column) => _jsx("td", { class: column.class, "data-label": column.label, children: column.cell(row) }, column.label)) }, row_key ? row_key(row) : undefined))) })] }) }));
13
- }
14
- // ── Pages ────────────────────────────────────────────────────────────────────
15
- // Newer and older buttons around the page number, or nothing when everything fits on one page.
16
- export function Pager({ page, has_more, go }) {
17
- if (page === 1 && !has_more)
18
- return null;
19
- return (_jsxs("nav", { class: "pager", "aria-label": "Pages", children: [_jsx("button", { disabled: page <= 1, onClick: () => go(page - 1), children: "\u2190 Newer" }), _jsxs("span", { class: "muted", children: ["Page ", page] }), _jsx("button", { disabled: !has_more, onClick: () => go(page + 1), children: "Older \u2192" })] }));
20
- }
@@ -1,9 +0,0 @@
1
- import { Fragment as _Fragment, jsx as _jsx } from "preact/jsx-runtime";
2
- // Times as the page shows them: how long ago, with the full date and time on hover.
3
- import { age, stamp } from "../format.js";
4
- // A time as its age, such as "3h ago", or the missing text, a dash unless told otherwise, when there is none.
5
- export function Age({ epoch, missing = "—" }) {
6
- if (!epoch)
7
- return _jsx(_Fragment, { children: missing });
8
- return _jsx("span", { title: stamp(epoch), children: age(epoch) });
9
- }
@@ -1,15 +0,0 @@
1
- export interface ToastOptions {
2
- tone?: "good" | "bad" | "warn" | "info";
3
- link?: {
4
- href: string;
5
- text: string;
6
- } | null;
7
- action?: {
8
- text: string;
9
- run: () => void;
10
- };
11
- sticky?: boolean;
12
- duration_in_milliseconds?: number;
13
- }
14
- export declare function toast(message: string, options?: ToastOptions): () => void;
15
- export declare function Toasts(): import("preact").JSX.Element;
@@ -1,34 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
2
- // Short notes in the page's corner after something happened, with an optional link or action such as Undo.
3
- import { useEffect, useState } from "preact/hooks";
4
- let shown = [];
5
- let next_id = 1;
6
- const listeners = new Set();
7
- function publish(toasts) {
8
- shown = toasts;
9
- for (const listener of listeners)
10
- listener(shown);
11
- }
12
- // Shows the message and returns a function that closes it early.
13
- export function toast(message, options = {}) {
14
- const id = next_id++;
15
- const close = () => publish(shown.filter((item) => item.id !== id));
16
- publish([...shown, { ...options, id, message }]);
17
- if (!(options.sticky ?? options.tone === "bad"))
18
- setTimeout(close, options.duration_in_milliseconds ?? 6000);
19
- return close;
20
- }
21
- // ── Host ─────────────────────────────────────────────────────────────────────
22
- // Where toasts stack; the Shell holds one, and a page without the Shell renders its own.
23
- export function Toasts() {
24
- const [toasts, set_toasts] = useState(shown);
25
- useEffect(() => {
26
- listeners.add(set_toasts);
27
- return () => void listeners.delete(set_toasts);
28
- }, []);
29
- return (_jsx("div", { id: "toasts", class: "toasts", children: toasts.map((item) => {
30
- const tone = item.tone ?? "info";
31
- const close = () => publish(shown.filter((other) => other.id !== item.id));
32
- return (_jsxs("div", { class: `toast tone-${tone}`, role: tone === "bad" ? "alert" : "status", children: [_jsx("span", { children: item.message }), item.link && _jsx("a", { href: item.link.href, onClick: close, children: item.link.text }), item.action && _jsx("button", { class: "toast-action", onClick: () => { close(); item.action.run(); }, children: item.action.text }), _jsx("button", { class: "icon", "aria-label": "Dismiss", onClick: close, children: "\u00D7" })] }, item.id));
33
- }) }));
34
- }