@x-tech-laboratories/x-libs 0.11.1 → 0.13.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
CHANGED
|
@@ -39,7 +39,7 @@ Code the X-apps share, published as one npm package, `@x-tech-laboratories/x-lib
|
|
|
39
39
|
| `web/store`, `web/react/store` | `create_store`: a value with a watch, set or updated, redrawing at a steady pace when `coalesce_in_milliseconds` is given; `use_store` reads it. |
|
|
40
40
|
| `web/prefs`, `web/react/prefs` | Remembered view settings that read as their default when stored corrupt, stale or of the wrong shape; `use_pref` keeps one as component state. |
|
|
41
41
|
| `web/selection`, `web/virtual`, `web/drag` | Picking rows with plain, Ctrl and Shift clicks from a fixed anchor, or as lists of ids with the anchor kept by id, the row an arrow or page key moves to; which rows of a long list a viewport needs; a badge such as "3 tasks" as the drag image. |
|
|
42
|
-
| `web/react/shell` | `Shell`: top bar, section links or the app's own sidebar in a `.sidebar-content` box, resizable, theme button, a `status_bar` at the foot, toasts, dialogs and menus, with or without logins (no account UI without a `user`, `logged_out` hiding the sidebar and tools), its phone menu held by the Shell or the app; `use_route`. |
|
|
42
|
+
| `web/react/shell` | `Shell`: top bar with the brand and its icon, section links or the app's own sidebar in a `.sidebar-content` box, resizable, theme button, a `status_bar` at the foot, toasts, dialogs and menus, with or without logins (no account UI without a `user`, `logged_out` hiding the sidebar and tools), its phone menu held by the Shell or the app, and for app-like pages a `contained` window where only main scrolls, the brand heading the sidebar; `use_route`. |
|
|
43
43
|
| `web/react/layout`, `badges`, `states` | `PageHeading` with a subtitle, icon, banner, actions and a `level` for inside dialogs and panels; panels, facts, tiles, cards, tabs, crumbs, steppers, settings rows, `RowActions` and `moved` for ordered lists, `SelectionBar` for picked rows; badges, chips and `StatusPill`, a state dot with text and an action; `use_resource` with its `status` (idle until `enabled`, loading, loaded, failed), a `watch` that reloads it quietly, and its loading states, `Loaded` going by the status so a null result shows its `empty` slot or the children. |
|
|
44
44
|
| `web/react/time` | `Age`: a time as how long ago, with the full stamp on hover; `use_now`, the time read again each minute. |
|
|
45
45
|
| `web/react/reveal` | `use_reveal`: a long list shown a page at a time, the next page added as its end scrolls into view, starting from the count shown before the page was left if given. |
|
|
@@ -47,7 +47,7 @@ Code the X-apps share, published as one npm package, `@x-tech-laboratories/x-lib
|
|
|
47
47
|
| `web/react/menu_button` | `MenuButton`: a ⋮ button that opens a menu under itself, even inside a link, saying through `aria-expanded` whether it is open. |
|
|
48
48
|
| `web/react/test_stubs` | Stands in for `showPopover`, `hidePopover`, `showModal`, `close`, `matchMedia` and `scrollTo`, which jsdom lacks; an app's widget tests import it for its side effects, and a node test's shared setup may import it too. |
|
|
49
49
|
| `web/react/list_picker` | `pick_list_dialog`: adds one thing to any of the app's lists, such as a video to playlists, ticking the lists that hold it and making a new one. |
|
|
50
|
-
| `web/react/controls`, `table` | Segmented choices, button groups, switches, search boxes that hand on typing as it pauses, submit on Enter or a button, clear on Escape, can take a paste whole, take focus and follow a value cleared from outside, selects, `CommitInput`, `DateTimeField`, `PathInput` with a Browse button on the app's picker, `ColorPicker`, `TreeSelect
|
|
50
|
+
| `web/react/controls`, `table` | Segmented choices, button groups, switches, search boxes that hand on typing as it pauses, submit on Enter or a button, clear on Escape, can take a paste whole, take focus and follow a value cleared from outside, selects, `CommitInput`, `DateTimeField`, `PathInput` with a Browse button on the app's picker, `ColorPicker`, `TreeSelect` opening an indented card that shows the picked path; `DataTable` on TanStack Table, sorting by several columns, with columns to move, resize, hide and filter from buttons pinned over the heading band, tree rows, groups under headings, rows the app drags and styles, rows that come sorted from a database, rows worked by keyboard, rows that draw again only when they change, `virtual` rows drawn only where they show, a last column that fills the room left, group headings that hide when the filter empties them, a `wrap_class` for Halon's `scroll` and `sticky-head`, and cards on phones; a pager. |
|
|
51
51
|
| `web/react/shortcuts` | `use_shortcuts` for keys such as `"mod+z"`, quiet while typing or while a dialog or popover is open; `describe_keys` for help text. |
|
|
52
52
|
| `web/react/tree` | `Tree`: nested rows that fold, walk with the arrow keys, reorder by dragging or with Alt and the arrow keys, show several picked, and take drops from outside. |
|
|
53
53
|
| `web/react/toasts`, `dialog`, `menus`, `split` | Toasts with a link or Undo that say how they closed, and `undoable` actions that commit or undo exactly once; modal dialogs with a subtitle, header actions, a full-window size and a `form` whose submit button sends it, and confirm and text dialogs, openable from anywhere; context menus and popovers worked by keyboard, openable from anywhere, their keep-open items ticking at once; resize handles and splits. |
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createElement as _createElement } from "react";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
// React controls drawn like the page's own: segmented choices, switches, search fields, selects, and date, colour and tree pickers.
|
|
4
|
-
import { useEffect, useRef, useState } from "react";
|
|
4
|
+
import { createElement, useEffect, useRef, useState } from "react";
|
|
5
5
|
import { in_tree_order, path_names } from "../tree.js";
|
|
6
6
|
// One of a few choices side by side; the chosen one takes the accent.
|
|
7
7
|
export function Segmented({ label, choices, value, on_change, class_name }) {
|
|
@@ -121,7 +121,7 @@ export function CommitInput({ value, on_commit, onBlur, onKeyDown, ...rest }) {
|
|
|
121
121
|
export function DateTimeField({ label, value, on_change, allow_time = true }) {
|
|
122
122
|
const [day = "", time = ""] = (value ?? "").split("T");
|
|
123
123
|
const join = (next_day, next_time) => on_change(next_day ? (next_time && allow_time ? `${next_day}T${next_time}` : next_day) : null);
|
|
124
|
-
return (_jsxs("div", { className: "field date-time-field", children: [_jsx("span", { className: "field-label", children: label }), _jsxs("div", { className: "date-time-inputs", children: [_jsx("input", { type: "date", "aria-label": `${label} date`, value: day, onChange: (event) => join(event.currentTarget.value, time) }), day && allow_time && _jsx("input", { type: "time", "aria-label": `${label} time`, value: time, onChange: (event) => join(day, event.currentTarget.value) }), day && _jsx("button", { type: "button", className: "icon", "aria-label": `Clear ${label.toLowerCase()} date`, onClick: () => on_change(null), children: "\u00D7" })] })] }));
|
|
124
|
+
return (_jsxs("div", { className: "field date-time-field", children: [_jsx("span", { className: "field-label", children: label }), _jsxs("div", { className: "date-time-inputs", children: [_jsx("input", { type: "date", "aria-label": `${label} date`, value: day, onChange: (event) => join(event.currentTarget.value, time) }), day && allow_time && _jsx("input", { type: "time", "aria-label": `${label} time`, value: time, onChange: (event) => join(day, event.currentTarget.value) }), day && _jsx("button", { type: "button", className: "icon remove", "aria-label": `Clear ${label.toLowerCase()} date`, onClick: () => on_change(null), children: "\u00D7" })] })] }));
|
|
125
125
|
}
|
|
126
126
|
// A path typed by hand or picked through a Browse button.
|
|
127
127
|
export function PathInput({ label, value, on_change, pick, placeholder, disabled = false, browse_label = "Browse…" }) {
|
|
@@ -149,5 +149,6 @@ export function ColorPicker({ label, value, swatches, on_change, allow_none = tr
|
|
|
149
149
|
export function TreeSelect({ label, items, name_of, value, blank_label, disabled_ids = [], on_change }) {
|
|
150
150
|
const depth_of = (item) => path_names(item, items, name_of).length - 1;
|
|
151
151
|
const chosen = items.find((item) => item.id === value);
|
|
152
|
-
|
|
152
|
+
// The open list indents each name by its depth, and the closed face shows the picked choice's whole path.
|
|
153
|
+
return (_jsxs("select", { className: "tree-select", "aria-label": label, value: value, title: chosen ? path_names(chosen, items, name_of).join(" › ") : undefined, onChange: (event) => on_change(event.currentTarget.value), children: [_jsx("button", { type: "button", children: createElement("selectedcontent") }), _jsx("option", { value: "", children: blank_label }), in_tree_order(items).map((item) => (_jsxs("option", { value: item.id, disabled: disabled_ids.includes(item.id), title: path_names(item, items, name_of).join(" › "), style: { "--depth": depth_of(item) }, children: [_jsx("span", { className: "tree-option-name", children: name_of(item) }), _jsx("span", { className: "tree-option-path", children: path_names(item, items, name_of).join(" › ") })] }, item.id)))] }));
|
|
153
154
|
}
|
package/dist/web/react/layout.js
CHANGED
|
@@ -77,9 +77,9 @@ export function SettingRow({ label, hint, as_label = false, class_name, children
|
|
|
77
77
|
}
|
|
78
78
|
// The up, down and remove buttons of a row in an ordered list.
|
|
79
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
|
|
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
81
|
}
|
|
82
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
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", "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] }));
|
|
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
85
|
}
|
|
@@ -37,7 +37,9 @@ interface ShellProps {
|
|
|
37
37
|
sidebar_foot?: ReactNode;
|
|
38
38
|
status_bar?: ReactNode;
|
|
39
39
|
view_key?: string;
|
|
40
|
+
contained?: boolean;
|
|
41
|
+
brand_in_sidebar?: boolean;
|
|
40
42
|
children: ReactNode;
|
|
41
43
|
}
|
|
42
|
-
export declare function Shell({ brand, sections, active, sidebar: sidebar_content, sidebar_width, theme, user, logged_out: held_logged_out, user_href, on_logout, brand_href, tools, sidebar_foot, status_bar, view_key, menu_open: held_menu_open, on_menu_open_change, main_class, children, }: ShellProps): import("react").JSX.Element;
|
|
44
|
+
export declare function Shell({ brand, sections, active, sidebar: sidebar_content, sidebar_width, theme, user, logged_out: held_logged_out, user_href, on_logout, brand_href, tools, sidebar_foot, status_bar, view_key, menu_open: held_menu_open, on_menu_open_change, main_class, contained, brand_in_sidebar, children, }: ShellProps): import("react").JSX.Element;
|
|
43
45
|
export {};
|
package/dist/web/react/shell.js
CHANGED
|
@@ -41,7 +41,7 @@ function use_sidebar_width(spec) {
|
|
|
41
41
|
};
|
|
42
42
|
return [width, change];
|
|
43
43
|
}
|
|
44
|
-
export function Shell({ brand, sections = [], active, sidebar: sidebar_content, sidebar_width, theme, user, logged_out: held_logged_out, user_href, on_logout, brand_href = "#/", tools, sidebar_foot, status_bar, view_key, menu_open: held_menu_open, on_menu_open_change, main_class, children, }) {
|
|
44
|
+
export function Shell({ brand, brand_icon, sections = [], active, sidebar: sidebar_content, sidebar_width, theme, user, logged_out: held_logged_out, user_href, on_logout, brand_href = "#/", tools, sidebar_foot, status_bar, view_key, menu_open: held_menu_open, on_menu_open_change, main_class, contained = false, brand_in_sidebar = false, children, }) {
|
|
45
45
|
const [own_menu_open, set_own_menu_open] = useState(false);
|
|
46
46
|
const menu_open = held_menu_open ?? own_menu_open;
|
|
47
47
|
const main = useRef(null);
|
|
@@ -65,7 +65,8 @@ export function Shell({ brand, sections = [], active, sidebar: sidebar_content,
|
|
|
65
65
|
document.body.classList.toggle("logged-out", logged_out);
|
|
66
66
|
document.body.classList.toggle("menu-open", menu_open);
|
|
67
67
|
document.body.classList.toggle("has-status-bar", status_bar !== undefined);
|
|
68
|
-
|
|
68
|
+
document.body.classList.toggle("contained", contained);
|
|
69
|
+
}, [logged_out, menu_open, status_bar !== undefined, contained]);
|
|
69
70
|
useEffect(() => {
|
|
70
71
|
if (menu_open)
|
|
71
72
|
sidebar.current?.querySelector("a[href], button:not(:disabled), input, select, textarea, [tabindex='0']")?.focus();
|
|
@@ -87,6 +88,7 @@ export function Shell({ brand, sections = [], active, sidebar: sidebar_content,
|
|
|
87
88
|
main.current?.focus({ preventScroll: true });
|
|
88
89
|
first_view.current = false;
|
|
89
90
|
window.scrollTo(0, 0);
|
|
91
|
+
main.current?.scrollTo?.(0, 0);
|
|
90
92
|
}, [view_key]);
|
|
91
|
-
return (_jsxs(_Fragment, { children: [_jsx("a", { className: "skip", href: "#main", children: "Skip to content" }), _jsxs("header", { className: "topbar", children: [_jsx("button", { ref: menu_button, className: "icon menu-button", "aria-label": "Open the menu", "aria-expanded": menu_open, "aria-controls": "sidebar", onClick: () => set_menu_open(!menu_open), children: "\u2630" }),
|
|
93
|
+
return (_jsxs(_Fragment, { children: [_jsx("a", { className: "skip", href: "#main", children: "Skip to content" }), _jsxs("header", { className: "topbar", children: [_jsx("button", { ref: menu_button, className: "icon menu-button", "aria-label": "Open the menu", "aria-expanded": menu_open, "aria-controls": "sidebar", onClick: () => set_menu_open(!menu_open), children: "\u2630" }), !(brand_in_sidebar && !logged_out) && _jsxs("a", { href: brand_href, className: "brand", children: [brand_icon && _jsx("img", { className: "brand-icon", src: brand_icon, alt: "" }), brand] }), !logged_out && tools, !logged_out && _jsx(ThemeButton, { choice: theme }), user_name !== undefined && (user_href ? _jsx("a", { className: "account-link", href: user_href, children: user_name }) : _jsx("span", { className: "user-name", children: user_name })), user_name !== undefined && on_logout && _jsx("button", { onClick: on_logout, children: "Log out" })] }), _jsxs("div", { className: "layout", style: width === undefined ? undefined : { "--sidebar-width": `${width}px` }, children: [_jsxs("nav", { ref: sidebar, id: "sidebar", className: "sidebar", "aria-label": "Main", children: [brand_in_sidebar && _jsxs("a", { href: brand_href, className: "sidebar-brand", children: [brand_icon && _jsx("img", { className: "brand-icon", src: brand_icon, alt: "" }), brand] }), sidebar_content !== undefined && sidebar_content !== null ? _jsx("div", { className: "sidebar-content", children: sidebar_content }) : sections.map((section) => (_jsx("a", { href: section.href, "data-section": section.id, className: section.id === active ? "active" : undefined, "aria-current": section.id === active ? "page" : undefined, children: section.label }, section.id))), sidebar_foot, sidebar_width && width !== undefined && (_jsx(ResizeHandle, { side: "left", size: width, min: sidebar_width.min, max: sidebar_width.max, default_size: sidebar_width.default_width, on_resize: (next) => change_width(next, false), on_resize_end: (next) => change_width(next, true), label: "Resize the sidebar", class_name: "sidebar-resize" }))] }), _jsx("div", { className: "scrim", hidden: !menu_open, onClick: () => set_menu_open(false) }), _jsx("main", { ref: main, id: "main", className: main_class, tabIndex: -1, children: children })] }), status_bar !== undefined && _jsx("footer", { className: "status-bar", children: status_bar }), _jsx(Toasts, {}), _jsx(Dialogs, {}), _jsx(ContextMenus, {})] }));
|
|
92
94
|
}
|
package/dist/web/react/table.js
CHANGED
|
@@ -284,7 +284,7 @@ export function DataTable(props) {
|
|
|
284
284
|
const class_name = [
|
|
285
285
|
column.class, column.filter_active && "filtered", dragged === id && "dragged", drop_target?.id === id && (drop_target.after ? "drop-after" : "drop-before"),
|
|
286
286
|
].filter(Boolean).join(" ") || undefined;
|
|
287
|
-
const marker = key && (_jsxs("span", { className: "sort-marker", "aria-hidden": "true", children: [key.descending ? "
|
|
287
|
+
const marker = key && (_jsxs("span", { className: "sort-marker", "aria-hidden": "true", children: [key.descending ? " ▼" : " ▲", sorting.length > 1 && _jsx("sup", { className: "sort-rank", children: index + 1 })] }));
|
|
288
288
|
return (_jsxs("th", { ref: (cell) => { if (cell)
|
|
289
289
|
header_cells.current.set(id, cell);
|
|
290
290
|
else
|
|
@@ -386,8 +386,8 @@ export function DataTable(props) {
|
|
|
386
386
|
const filtered_column = open_filter ? by_id.get(open_filter.id) : undefined;
|
|
387
387
|
const filter_body = filtered_column?.filter;
|
|
388
388
|
const tools = (props.on_clear_filters && any_filter) || props.on_hidden_columns_change;
|
|
389
|
-
return (_jsxs(_Fragment, { children: [tools && (
|
|
390
|
-
|
|
389
|
+
return (_jsxs(_Fragment, { children: [_jsxs("div", { ref: wrap, className: cx("table-wrap", props.virtual && "virtual", props.wrap_class), style: props.virtual ? { overflowY: "auto" } : undefined, onScroll: line_height === undefined ? undefined : measure, children: [tools && (_jsx("div", { className: "table-tools", children: _jsxs("span", { className: "table-tools-inner", children: [props.on_clear_filters && any_filter && _jsx("button", { type: "button", "aria-label": "Clear filters", title: "Clear filters", onClick: props.on_clear_filters, children: "Clear" }), props.on_hidden_columns_change && (_jsx("button", { type: "button", className: "icon columns-button", "aria-label": "Show or hide columns", title: "Show or hide columns", "aria-haspopup": "menu", onClick: open_columns_menu, children: _jsx("svg", { viewBox: "0 0 16 16", width: "12", height: "12", "aria-hidden": "true", children: _jsx("path", { d: "M2 2h3v12H2zm4.5 0h3v12h-3zM11 2h3v12h-3z" }) }) }))] }) })), _jsxs("table", { className: fill_last ? "responsive fill-last-column" : "responsive", style: fills_by_fixed ? { tableLayout: "fixed" } : undefined, children: [caption && _jsx("caption", { className: "visually-hidden", children: caption }), _jsx("thead", { ref: head, onContextMenu: props.on_hidden_columns_change && open_columns_menu, children: _jsx("tr", { children: shown.map(header) }) }), _jsxs("tbody", { onFocus: () => { rows_focused.current = true; }, onBlur: (event) => { if (event.relatedTarget && !event.currentTarget.contains(event.relatedTarget))
|
|
390
|
+
rows_focused.current = false; }, children: [windowed && spacer(windowed.offset_px, "spacer before"), drawn.map(draw_line), windowed && spacer(windowed.total_px - windowed.end * line_height, "spacer after")] })] })] }), open_filter && filtered_column && filter_body !== undefined && (_jsx(Popover, { anchor: open_filter.anchor, label: `Filter by ${filtered_column.label}`, on_close: close_filter, children: typeof filter_body === "function" ? filter_body(close_filter) : filter_body })), columns_menu && _jsx(ContextMenu, { x: columns_menu.x, y: columns_menu.y, label: "Columns", items: column_choices, on_close: () => set_columns_menu(null) })] }));
|
|
391
391
|
}
|
|
392
392
|
// ── Pages ────────────────────────────────────────────────────────────────────
|
|
393
393
|
// Newer and older buttons around the page number, or nothing when everything fits on one page.
|
package/dist/web/react/toasts.js
CHANGED
|
@@ -67,6 +67,6 @@ export function Toasts() {
|
|
|
67
67
|
return (_jsx("div", { id: "toasts", className: "toasts", children: toasts.map((item) => {
|
|
68
68
|
const tone = item.tone ?? "info";
|
|
69
69
|
const close = () => close_toast(item.id, "dismissed");
|
|
70
|
-
return (_jsxs("div", { className: `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", { className: "toast-action", onClick: () => { close_toast(item.id, "action"); item.action.run(); }, children: item.action.text }), _jsx("button", { className: "icon", "aria-label": "Dismiss", onClick: close, children: "\u00D7" })] }, item.id));
|
|
70
|
+
return (_jsxs("div", { className: `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", { className: "toast-action", onClick: () => { close_toast(item.id, "action"); item.action.run(); }, children: item.action.text }), _jsx("button", { className: "icon remove", "aria-label": "Dismiss", onClick: close, children: "\u00D7" })] }, item.id));
|
|
71
71
|
}) }));
|
|
72
72
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@x-tech-laboratories/x-libs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Code the X-apps share: agents and their long runs, passwords and sign-in, the Fastify server pieces, durations, and the web page's helpers and widgets",
|
|
5
5
|
"license": "GPL-3.0-or-later",
|
|
6
6
|
"repository": {
|