@xoong/ui 0.0.0-stage → 0.0.34

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Duong Tran Xuan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,31 @@
1
- # Temporary Holding Version
1
+ # @xoong/ui
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The admin set of xoong: design tokens in cascade layers, `<xo-table>`, a form generated from the request schema, `<xo-dialog>`, toasts, confirm dialogs and `<xo-router>`.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @xoong/ui
9
+ ```
10
+
11
+ ## Use
12
+
13
+ ```js
14
+ import '@xoong/ui';
15
+ ```
16
+ ```html
17
+ <link rel="stylesheet" href="/@xoong/ui/src/xoong.css">
18
+ <link rel="stylesheet" href="/@xoong/ui/src/mist.css"> <!-- optional: the Mist look (the Rainlet design system), one self-sufficient file -->
19
+ <xo-view get="listPeople">
20
+ <xo-table columns="name,email:E-mail,role" sortable filters="name:Search" page-size="10"></xo-table>
21
+ </xo-view>
22
+ ```
23
+
24
+ ## More
25
+
26
+ * Documentation and demos: https://github.com/fpt-duongtx/xoong#readme
27
+ * Source: https://github.com/fpt-duongtx/xoong/tree/main/packages/ui
28
+ * All packages share one version number and are released together: https://github.com/fpt-duongtx/xoong/releases
29
+ * xoong is a working prototype (0.0.x): the HTML syntax is still Draft.
30
+
31
+ MIT licence.
package/package.json CHANGED
@@ -1,6 +1,53 @@
1
1
  {
2
2
  "name": "@xoong/ui",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.0.34",
4
+ "description": "xoong admin set: xoong.css tokens, <xo-table>, <xo-form>, <xo-dialog>, toast, confirm and a declarative History-API router. Depends on @xoong/core, @xoong/data and @xoong/forms.",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./src/index.d.ts",
10
+ "default": "./src/index.js"
11
+ },
12
+ "./table": "./src/table.js",
13
+ "./form": "./src/form.js",
14
+ "./dialog": "./src/dialog.js",
15
+ "./toast": "./src/toast.js",
16
+ "./router": "./src/router.js",
17
+ "./xoong.css": "./src/xoong.css",
18
+ "./mist.css": "./src/mist.css"
19
+ },
20
+ "sideEffects": true,
21
+ "license": "MIT",
22
+ "dependencies": {
23
+ "@xoong/core": "0.0.34",
24
+ "@xoong/data": "0.0.34",
25
+ "@xoong/forms": "0.0.34"
26
+ },
27
+ "files": [
28
+ "src"
29
+ ],
30
+ "keywords": [
31
+ "xoong",
32
+ "web-framework",
33
+ "openapi",
34
+ "custom-elements"
35
+ ],
36
+ "author": "Duong Tran Xuan <duongtx1505@icloud.com>",
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/fpt-duongtx/xoong.git",
40
+ "directory": "packages/ui"
41
+ },
42
+ "bugs": {
43
+ "url": "https://github.com/fpt-duongtx/xoong/issues"
44
+ },
45
+ "homepage": "https://github.com/fpt-duongtx/xoong#readme",
46
+ "types": "./src/index.d.ts",
47
+ "publishConfig": {
48
+ "access": "public"
49
+ },
50
+ "engines": {
51
+ "node": ">=22.12"
52
+ }
53
+ }
package/src/confirm.js ADDED
@@ -0,0 +1,53 @@
1
+ // @ts-check
2
+ /**
3
+ * The confirmation dialog behind `xo-confirm="Delete {title}?"` (and the leave-page question of a dirty form): a native modal <dialog>,
4
+ * created on first use. Escape or Cancel answers no, Confirm answers yes; the focus starts on Cancel, the safe choice for a
5
+ * destructive action. Replaces the browser's confirm() as soon as @xoong/ui is imported.
6
+ *
7
+ * // from the package entry: confirm(message, { ok, cancel }) resolves true or false
8
+ * if (await confirm('Discard the draft?', { ok: 'Discard' })) ...
9
+ */
10
+ import { setConfirm } from '@xoong/data/view';
11
+ import { text } from '@xoong/core';
12
+
13
+ /** The button texts; replace them for another wording or language. */
14
+ export const labels = { ok: 'Confirm', cancel: 'Cancel' };
15
+
16
+ /** @type {HTMLDialogElement | null} */
17
+ let dialog = null;
18
+
19
+ /** @param {string} message @param {{ ok?: string, cancel?: string }} [options] @returns {Promise<boolean>} */
20
+ export function confirm(message, options = {}) {
21
+ return new Promise((resolve) => {
22
+ dialog?.remove();
23
+ const d = document.createElement('dialog');
24
+ d.className = 'xo-confirm';
25
+ d.setAttribute('aria-labelledby', 'xo-confirm-text');
26
+ const body = document.createElement('p');
27
+ body.id = 'xo-confirm-text';
28
+ body.textContent = message;
29
+ const cancel = document.createElement('button');
30
+ cancel.type = 'button';
31
+ cancel.textContent = options.cancel ?? (labels.cancel === 'Cancel' ? text('xoong.confirm.cancel', 'Cancel') : labels.cancel);
32
+ const ok = document.createElement('button');
33
+ ok.type = 'button';
34
+ ok.className = 'xo-confirm-ok';
35
+ ok.textContent = options.ok ?? (labels.ok === 'Confirm' ? text('xoong.confirm.ok', 'Confirm') : labels.ok);
36
+ const row = document.createElement('div');
37
+ row.className = 'xo-confirm-actions';
38
+ row.append(cancel, ok);
39
+ d.append(body, row);
40
+ let answer = false;
41
+ const finish = () => { d.remove(); if (dialog === d) dialog = null; resolve(answer); };
42
+ cancel.addEventListener('click', () => { answer = false; d.close ? d.close() : finish(); });
43
+ ok.addEventListener('click', () => { answer = true; d.close ? d.close() : finish(); });
44
+ d.addEventListener('close', finish); // Escape closes a native dialog: that answers no
45
+ d.addEventListener('cancel', () => { answer = false; });
46
+ document.body.append(d);
47
+ dialog = d;
48
+ if (d.showModal) d.showModal(); else d.setAttribute('open', ''); // DOMs without <dialog> support (tests)
49
+ cancel.focus();
50
+ });
51
+ }
52
+
53
+ setConfirm((message) => confirm(message));
package/src/dialog.js ADDED
@@ -0,0 +1,71 @@
1
+ // @ts-check
2
+ /**
3
+ * <xo-dialog id="add"> ...content... </xo-dialog>
4
+ * <button xo-open="#add">Add</button> opens it (modal, on a native <dialog>)
5
+ * <button xo-close>Cancel</button> closes the dialog it is in
6
+ *
7
+ * The dialog also closes when a form inside it succeeds (`xo-done`). Escape, focus trapping and the
8
+ * backdrop come from the browser's <dialog>; nothing is reimplemented.
9
+ */
10
+ import { XoElement, define } from '@xoong/core';
11
+
12
+ class XoDialog extends XoElement {
13
+ /** @type {HTMLDialogElement | null} */ #dialog = null;
14
+
15
+ connected() {
16
+ // Children may not exist yet when the element is connected (HTML parser): wrap them once the tree is complete.
17
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () => this.#wrap(), { once: true });
18
+ else queueMicrotask(() => this.#wrap());
19
+ }
20
+
21
+ #wrap() {
22
+ if (this.#dialog || !this.isConnected) return;
23
+ const dialog = document.createElement('dialog');
24
+ dialog.append(...this.childNodes);
25
+ this.append(dialog);
26
+ this.#dialog = dialog;
27
+ this.addEventListener('xo-done', () => this.close());
28
+ }
29
+
30
+ get dialog() { return this.#dialog; }
31
+ show() {
32
+ const d = this.#dialog;
33
+ if (!d || d.open) return;
34
+ if (d.showModal) d.showModal(); else d.setAttribute('open', ''); // DOMs without <dialog> support (tests)
35
+ }
36
+ /** Open it and wait for the answer: resolves with the `returnValue` when it closes (`xo-close value="ok"`, `close('ok')`, or '' for Escape and Cancel). */
37
+ open() {
38
+ const d = this.#dialog;
39
+ if (!d) return Promise.resolve('');
40
+ return new Promise((resolve) => {
41
+ d.addEventListener('close', () => resolve(d.returnValue), { once: true });
42
+ this.show();
43
+ });
44
+ }
45
+ close(/** @type {string | undefined} */ value) {
46
+ const d = this.#dialog;
47
+ if (!d?.open) return;
48
+ if (d.close) d.close(value); else { d.returnValue = value ?? ''; d.removeAttribute('open'); d.dispatchEvent(new Event('close')); }
49
+ }
50
+ }
51
+ define('xo-dialog', XoDialog);
52
+
53
+ // One delegated listener for the page (the module runs once): buttons do not need wiring.
54
+ {
55
+ document.addEventListener('click', (event) => {
56
+ const target = /** @type {Element} */ (event.target);
57
+ const opener = target.closest?.('[xo-open]');
58
+ if (opener) {
59
+ const selector = opener.getAttribute('xo-open') ?? '';
60
+ try {
61
+ const dialog = document.querySelector(selector);
62
+ if (dialog instanceof XoDialog) dialog.show();
63
+ else console.error(`xoong: xo-open="${selector}" matches no <xo-dialog>`);
64
+ } catch {
65
+ console.error(`xoong: xo-open="${selector}" is not a valid selector (use an id such as #add)`);
66
+ }
67
+ }
68
+ const closer = target.closest?.('[xo-close]');
69
+ if (closer) closer.closest('xo-dialog')?.close(closer.getAttribute('value') ?? undefined);
70
+ });
71
+ }
package/src/form.js ADDED
@@ -0,0 +1,148 @@
1
+ // @ts-check
2
+ /**
3
+ * <xo-form op="updateArticle" submit="Save"></xo-form>: the markup of a form, written for you from the operation's request schema
4
+ * (`bodySchema` in operations.json). The same binder as for a hand-written `<form xo-submit>` does the rest (ADR-0009): prefill, validation,
5
+ * dirty tracking, errors, payload. Nothing here is a second code path: it only emits the markup you would have written.
6
+ *
7
+ * - simple fields: one labelled control; enums are selects, booleans checkboxes, formats choose the input type;
8
+ * - nested objects: a fieldset; arrays of values or objects: rows with add, remove, up and down buttons;
9
+ * - unions (oneOf with a tag): one "Add <variant>" button per variant, and each row shows the fields of its variant;
10
+ * - hints (`xoong.hints.json` / the schema): `label`, `hint`, `placeholder`, `widget` (textarea, password, url, email, color, date, datetime,
11
+ * or the tag of any custom element that extends XoControl), `order`, `readOnly`, `hidden` (kept in the model, not drawn), `group` (a fieldset
12
+ * with a legend). `title` / `description` of the schema are the label and the hint.
13
+ * Without a `bodySchema` (a multipart body) the flat `bodyFields` are used as before.
14
+ */
15
+ import { defineTemplate } from '@xoong/data/view';
16
+
17
+ const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' })[c]);
18
+ const label = (name) => /^[a-z]{2}(-[A-Za-z]{2})?$/.test(name) ? String(name).replace(/^[a-z]{2}/, (c) => c.toUpperCase()) : String(name).replace(/[_-]+/g, ' ').replace(/([a-z])([A-Z])/g, '$1 $2').replace(/^./, (c) => c.toUpperCase());
19
+ const TYPES = { email: 'email', password: 'password', date: 'date', 'date-time': 'datetime-local', uri: 'url', url: 'url' }; // the flat generator (bodyFields only)
20
+ const TYPE_OF_WIDGET = { password: 'password', url: 'url', email: 'email', color: 'color', date: 'date', datetime: 'datetime-local', time: 'time' };
21
+ const PRIMITIVE = new Set(['string', 'integer', 'number', 'boolean']);
22
+
23
+ /** Fields in display order: by `order`, then as declared. Hidden ones are not drawn. */
24
+ function ordered(props) {
25
+ return Object.entries(props ?? {})
26
+ .map(([name, node], i) => ({ name, node, i }))
27
+ .filter(({ node }) => !node.hidden && !node.readOnly)
28
+ .sort((a, b) => (a.node.order ?? 1e9) - (b.node.order ?? 1e9) || a.i - b.i);
29
+ }
30
+
31
+ /** One simple control with its label, hint and widget. */
32
+ function control(node, name, text) {
33
+ const attrs = `name="${esc(name)}"${node.readOnly ? ' readonly' : ''}`;
34
+ const widget = node.widget;
35
+ let input;
36
+ if (widget && widget.includes('-')) input = `<${esc(widget)} ${attrs}></${esc(widget)}>`; // a custom XoControl: the binder gives it its schema node, hints and the form model
37
+ else if (widget === 'textarea') input = `<textarea ${attrs}></textarea>`;
38
+ else if (node.enum || widget === 'select') input = `<select ${attrs}></select>`;
39
+ else if (node.t === 'boolean') input = `<input type="checkbox" ${attrs}>`;
40
+ else input = `<input${TYPE_OF_WIDGET[widget] ? ` type="${TYPE_OF_WIDGET[widget]}"` : ''} ${attrs}>`;
41
+ const hint = node.description ? `<small class="xo-hint">${esc(node.description)}</small>` : '';
42
+ const caption = `<span>${esc(node.title ?? text)}</span>`;
43
+ return `<label class="xo-field">${node.t === 'boolean' ? `${input}${caption}` : `${caption}${input}`}${hint}</label>`;
44
+ }
45
+
46
+ /** The fields of an object node, grouped by `group`. `rowPath(name)` gives the name a child gets. */
47
+ function fields(node, path, depth, skip = []) {
48
+ const groups = new Map();
49
+ for (const f of ordered(node.props)) {
50
+ if (skip.includes(f.name)) continue;
51
+ const key = f.node.group ?? '';
52
+ if (!groups.has(key)) groups.set(key, []);
53
+ groups.get(key).push(f);
54
+ }
55
+ return [...groups].map(([group, list]) => {
56
+ const html = list.map((f) => field(f.node, path ? `${path}.${f.name}` : f.name, f.name, depth)).join('');
57
+ return group ? `<fieldset class="xo-group"><legend>${esc(group)}</legend>${html}</fieldset>` : html;
58
+ }).join('');
59
+ }
60
+
61
+ /** The heading of an array row: its number and, for unions, the variant. */
62
+ const rowTitle = (text) => `<span class="xo-row-title"><span class="xo-row-no">{$n}.</span>${text ? ` <strong>${esc(text)}</strong>` : ''}</span>`;
63
+
64
+ /** Buttons of an array row. */
65
+ const rowButtons = (path) => `<span class="xo-row-actions"><span class="xo-handle" xo-drag="${path}" aria-hidden="true">⋮⋮</span><button type="button" xo-move="${path}:up" aria-label="Move up" xo-t="aria-label:xoong.form.moveUp">↑</button><button type="button" xo-move="${path}:down" aria-label="Move down" xo-t="aria-label:xoong.form.moveDown">↓</button><button type="button" xo-remove="${path}" aria-label="Remove" xo-t="aria-label:xoong.form.remove">×</button></span>`;
66
+
67
+ function field(node, path, name, depth) {
68
+ const text = label(name);
69
+ if (node.widget?.includes('-') && !PRIMITIVE.has(node.t)) return control(node, path, text); // a custom control owns a whole array, object or union (tags, a block picker)
70
+ if (PRIMITIVE.has(node.t)) return control(node, path, text);
71
+ if (node.t === 'object') return `<fieldset class="xo-object"><legend>${esc(node.title ?? text)}</legend>${fields(node, path, depth)}</fieldset>`;
72
+ if (node.t === 'array') return array(node, path, text, depth);
73
+ if (node.t === 'union') return `<fieldset class="xo-object"><legend>${esc(node.title ?? text)}</legend>${variants(node, path, depth)}</fieldset>`;
74
+ return ''; // `any`: not drawn
75
+ }
76
+
77
+ /** A union that is a plain property: choose the variant, then show its fields. */
78
+ function variants(node, path, depth) {
79
+ const key = node.key;
80
+ const options = Object.keys(node.map).map((v) => `<option value="${esc(v)}">${esc(label(v))}</option>`).join('');
81
+ return `<label class="xo-field"><span>${esc(label(key))}</span><select name="${esc(path)}.${esc(key)}"><option value=""></option>${options}</select></label>`
82
+ + Object.entries(node.map).map(([v, n]) => `<div xo-if="${esc(path)}.${esc(key)}=${esc(v)}">${fields(n, path, depth, [key])}</div>`).join('');
83
+ }
84
+
85
+ /** An array: a row per item with its controls and buttons, and add buttons below. */
86
+ function array(node, path, text, depth) {
87
+ const item = node.items;
88
+ const at = `{$i${depth}}`; // the row index in names; the generator knows the nesting depth, so inner arrays can name outer rows
89
+ const rowPath = `${path}[${at}]`;
90
+ let row;
91
+ let adds;
92
+ if (item.t === 'union') {
93
+ row = Object.entries(item.map).map(([v, n]) => `<div class="xo-variant" xo-if="type=${esc(v)}">${rowTitle(n.title ?? label(v))}${fields(n, rowPath, depth + 1, [item.key])}</div>`).join('');
94
+ adds = Object.keys(item.map).map((v) => `<button type="button" xo-add="${esc(path)}" xo-with="${esc(item.key)}=${esc(v)}">Add ${esc(label(item.map[v].title ?? v))}</button>`).join('');
95
+ } else {
96
+ row = PRIMITIVE.has(item.t) ? control({ ...item, title: item.title ?? `${text} item` }, rowPath, text) : item.t === 'object' ? rowTitle(item.title ?? '') + fields(item, rowPath, depth + 1) : '';
97
+ adds = `<button type="button" xo-add="${esc(path)}" xo-t="xoong.form.add">Add</button>`;
98
+ }
99
+ return `<fieldset class="xo-array"><legend>${esc(node.title ?? text)}</legend>${node.description ? `<small class="xo-hint">${esc(node.description)}</small>` : ''}`
100
+ + `<div class="xo-row" xo-each="${esc(path)}">${row}${rowButtons(rowPath)}</div><div class="xo-array-add">${adds}</div></fieldset>`;
101
+ }
102
+
103
+ defineTemplate('xo-form', (el, { client }) => {
104
+ const ref = el.getAttribute('op') ?? '';
105
+ const op = client.op(ref); // unknown operations throw with the closest valid name
106
+ const form = document.createElement('form');
107
+ form.setAttribute('xo-submit', ref);
108
+ form.className = 'xo-form';
109
+ if (op.bodySchema) { // the schema-driven generator: nested objects, arrays, unions, hints
110
+ const submit = el.getAttribute('submit');
111
+ form.innerHTML = fields(op.bodySchema, '', 0) + (submit ? `<button class="xo-submit">${esc(submit)}</button>` : '<button class="xo-submit" xo-t="xoong.form.save">Save</button>');
112
+ return form;
113
+ }
114
+ const flat = op.bodyFields ?? [];
115
+ const missing = (op.bodySkipped ?? []).filter((s) => s.required);
116
+ if (missing.length) console.warn(`xoong: <xo-form op="${ref}"> cannot generate the required field(s) ${missing.map((m) => `${m.name} (${m.type})`).join(', ')}: the server will refuse this form. Write those fields by hand or wait for nested form support.`);
117
+ if (!flat.length) console.error(`xoong: <xo-form op="${ref}">: the request body has no simple fields to generate (see docs/admin.md)`);
118
+ for (const f of flat) {
119
+ const wrap = document.createElement('label');
120
+ wrap.className = 'xo-field';
121
+ const text = document.createElement('span');
122
+ text.textContent = f.title ?? label(f.name);
123
+ let input;
124
+ if (f.enum) {
125
+ input = document.createElement('select');
126
+ for (const v of f.required ? f.enum : ['', ...f.enum]) {
127
+ const option = document.createElement('option');
128
+ option.value = option.textContent = String(v);
129
+ input.append(option);
130
+ }
131
+ } else {
132
+ input = document.createElement('input');
133
+ input.type = f.type === 'file' ? 'file' : f.type === 'boolean' ? 'checkbox' : f.type === 'integer' || f.type === 'number' ? 'number' : TYPES[f.format] ?? 'text';
134
+ if (f.type === 'number') input.step = 'any';
135
+ if (f.maxLength) input.maxLength = f.maxLength;
136
+ if (input.type === 'text' || input.type === 'email') input.autocomplete = 'off';
137
+ }
138
+ input.name = f.name;
139
+ if (f.required && f.type !== 'boolean') input.required = true;
140
+ if (f.type === 'boolean') wrap.append(input, text); else wrap.append(text, input);
141
+ form.append(wrap);
142
+ }
143
+ const button = document.createElement('button');
144
+ button.className = 'xo-submit';
145
+ button.textContent = el.getAttribute('submit') || 'Save';
146
+ form.append(button);
147
+ return form;
148
+ });
package/src/index.d.ts ADDED
@@ -0,0 +1,33 @@
1
+ export interface RouteLocation {
2
+ /** The path without the router's `base`, e.g. "/users/3". */
3
+ path: string;
4
+ /** Path parameters of the matched route: { id: "3" }. */
5
+ params: Record<string, string>;
6
+ query: URLSearchParams;
7
+ /** Path, query and hash as in the address bar. */
8
+ url: string;
9
+ /** The matched route template, or null when nothing matched (a 404). */
10
+ route: { pattern: string; layout: string | null; title: string; template: HTMLTemplateElement } | null;
11
+ }
12
+
13
+ /** true or undefined: continue. false: cancel. A string: redirect to that path. */
14
+ export type GuardResult = boolean | string | void;
15
+
16
+ /** What a confirmation is about: an `xo-confirm` action, or leaving a page that has unsaved changes. */
17
+ export interface ConfirmContext { kind: 'action' | 'leave'; element?: Element; forms?: Element[] }
18
+ /** Replace the native confirmation dialog (for `xo-confirm` and the leave guard) with your own; resolve to true to go on. */
19
+ export function setConfirm(fn: (message: string, context?: ConfirmContext) => boolean | Promise<boolean>): void;
20
+ /** Ask with the dialog that is installed. */
21
+ export function confirm(message: string, options?: { ok?: string; cancel?: string }): Promise<boolean>;
22
+ /** Show a notice in <xo-toast>. */
23
+ export const toast: { show(notice: { message: string; kind?: string; action?: { label: string; run: () => unknown }; timeout?: number }): void };
24
+
25
+ export const router: {
26
+ /** Go to a path relative to the base. Resolves with true when the navigation happened. */
27
+ navigate(path: string, options?: { replace?: boolean }): Promise<boolean>;
28
+ /** Run before every navigation; returns a function that removes the guard. */
29
+ guard(fn: (to: RouteLocation, from: RouteLocation | null) => GuardResult | Promise<GuardResult>): () => void;
30
+ /** Run before leaving the current location (links, back/forward, navigate); return false to stay. */
31
+ beforeNavigate(fn: (to: RouteLocation, from: RouteLocation | null, type: 'link' | 'api' | 'popstate') => boolean | void | Promise<boolean | void>): () => void;
32
+ readonly current: RouteLocation | null;
33
+ };
package/src/index.js ADDED
@@ -0,0 +1,10 @@
1
+ // Importing the package registers every element of the admin set.
2
+ import '@xoong/forms'; // every <form xo-submit> (and <xo-form>) uses the schema-driven form model
3
+ import './table.js';
4
+ import './form.js';
5
+ import './dialog.js';
6
+ import './toast.js';
7
+ export { router } from './router.js';
8
+ export { toast } from './toast.js';
9
+ export { confirm, labels as confirmLabels } from './confirm.js';
10
+ export { setConfirm } from '@xoong/data/view';