@x-tech-laboratories/x-libs 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -13
- package/dist/dates.d.ts +9 -0
- package/dist/dates.js +60 -0
- package/dist/durations.d.ts +8 -1
- package/dist/durations.js +9 -5
- package/dist/electron/bridge.d.ts +30 -0
- package/dist/electron/bridge.js +32 -0
- package/dist/electron/window.d.ts +85 -0
- package/dist/electron/window.js +85 -0
- package/dist/web/menus.d.ts +4 -0
- package/dist/web/menus.js +28 -1
- package/dist/web/{preact → react}/badges.d.ts +9 -9
- package/dist/web/{preact → react}/badges.js +6 -6
- package/dist/web/{preact → react}/controls.d.ts +7 -7
- package/dist/web/{preact → react}/controls.js +7 -6
- package/dist/web/{preact → react}/dialog.d.ts +10 -10
- package/dist/web/{preact → react}/dialog.js +14 -9
- package/dist/web/{preact → react}/layout.d.ts +33 -33
- package/dist/web/react/layout.js +71 -0
- package/dist/web/react/menus.d.ts +31 -0
- package/dist/web/react/menus.js +159 -0
- package/dist/web/react/shell.d.ts +37 -0
- package/dist/web/react/shell.js +81 -0
- package/dist/web/react/shortcuts.d.ts +23 -0
- package/dist/web/react/shortcuts.js +95 -0
- package/dist/web/{preact → react}/split.d.ts +4 -4
- package/dist/web/{preact → react}/split.js +9 -10
- package/dist/web/{preact → react}/states.d.ts +7 -7
- package/dist/web/{preact → react}/states.js +6 -5
- package/dist/web/react/table.d.ts +46 -0
- package/dist/web/react/table.js +210 -0
- package/dist/web/{preact → react}/time.d.ts +1 -1
- package/dist/web/{preact → react}/time.js +1 -1
- package/dist/web/{preact → react}/toasts.d.ts +3 -1
- package/dist/web/react/toasts.js +41 -0
- package/dist/web/react/tree.d.ts +23 -0
- package/dist/web/react/tree.js +168 -0
- package/dist/web/tables.d.ts +18 -1
- package/dist/web/tables.js +70 -8
- package/dist/web/theme.d.ts +6 -1
- package/dist/web/theme.js +31 -21
- package/dist/web/tree.d.ts +35 -0
- package/dist/web/tree.js +107 -0
- package/package.json +15 -4
- package/dist/web/preact/layout.js +0 -71
- package/dist/web/preact/menus.d.ts +0 -19
- package/dist/web/preact/menus.js +0 -70
- package/dist/web/preact/shell.d.ts +0 -27
- package/dist/web/preact/shell.js +0 -63
- package/dist/web/preact/table.d.ts +0 -23
- package/dist/web/preact/table.js +0 -20
- package/dist/web/preact/toasts.js +0 -34
package/dist/web/preact/menus.js
DELETED
|
@@ -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 {};
|
package/dist/web/preact/shell.js
DELETED
|
@@ -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;
|
package/dist/web/preact/table.js
DELETED
|
@@ -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,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
|
-
}
|