@zeyos/zx 2.1.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/CHANGELOG.md +74 -0
- package/LICENSE +21 -0
- package/MIGRATION.md +598 -0
- package/README.md +288 -0
- package/dist/zx-compat.esm.js +16880 -0
- package/dist/zx-compat.esm.min.js +1 -0
- package/dist/zx-compat.global.js +23068 -0
- package/dist/zx-compat.global.min.js +3 -0
- package/dist/zx-zeyos.esm.js +11581 -0
- package/dist/zx-zeyos.esm.min.js +1 -0
- package/dist/zx.css +4946 -0
- package/dist/zx.esm.js +19934 -0
- package/dist/zx.esm.min.js +3 -0
- package/dist/zx.global.js +19958 -0
- package/dist/zx.global.min.js +3 -0
- package/dist/zx.min.css +1 -0
- package/docs/api.json +5248 -0
- package/docs/llms.md +1315 -0
- package/docs/llms.txt +45 -0
- package/package.json +74 -0
package/docs/llms.md
ADDED
|
@@ -0,0 +1,1315 @@
|
|
|
1
|
+
# Zx — agent reference
|
|
2
|
+
|
|
3
|
+
> Zx is the dependency-free, vanilla-JavaScript UI component library for ZeyOS business
|
|
4
|
+
> applications. ES2022 modules, WAI-ARIA-accessible native controls, semantic `--zx-*` design
|
|
5
|
+
> tokens (light/dark + cozy/compact), and lifecycle-safe components. Zero runtime dependencies.
|
|
6
|
+
> This file is the single, complete reference a coding agent needs to build UI with Zx; each
|
|
7
|
+
> component below is wrapped in `<!-- doc:<name> -->` markers so tools can extract a single
|
|
8
|
+
> component's section (`website/docs.html` renders them as each component's Reference tab).
|
|
9
|
+
|
|
10
|
+
## Mental model
|
|
11
|
+
|
|
12
|
+
Every visual component is an ES class extending `Component`, instantiated identically:
|
|
13
|
+
|
|
14
|
+
```js
|
|
15
|
+
import { Table, Dialog, Toggle } from '/assets/zx.esm.js'; // or window.zx.* from zx.global.js
|
|
16
|
+
const t = new Table(target, options);
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
- **`target`** — an `Element`, a selector string, or `null`. `null` means the component creates and
|
|
20
|
+
**owns** its root element; retrieve it with `t.toElement()` (or `t.el`) and insert it. A non-null
|
|
21
|
+
target is *enhanced in place* and restored on `destroy()`.
|
|
22
|
+
- **Options** — a plain object; each component declares `static defaults`. Never mutate the passed
|
|
23
|
+
object.
|
|
24
|
+
- **Value/data** — set via constructor options and mutated via methods (`set`, `setData`, …);
|
|
25
|
+
read via getters/methods (`.value`, `getValues()`, …).
|
|
26
|
+
|
|
27
|
+
## Events & lifecycle
|
|
28
|
+
|
|
29
|
+
- Subscribe with `t.on('change', fn)` (alias `addEvent`), `t.once(...)`, or an `on<type>` function
|
|
30
|
+
**option** (`onchange`, `onrowclick`, …). Handlers receive a `CustomEvent`; read `event.detail`
|
|
31
|
+
(always an object, e.g. `{ value, item }`).
|
|
32
|
+
- Every `emit` also dispatches a **bubbling, composed** `zx-<type>` `CustomEvent` on `t.el`, so
|
|
33
|
+
`t.el.addEventListener('zx-change', …)` and event delegation work.
|
|
34
|
+
- `t.destroy()` aborts all listeners (one AbortController), unregisters the element, and removes an
|
|
35
|
+
owned root. `Component.from(el)` returns the component for a root element (replaces the legacy
|
|
36
|
+
`el.retrieve('com')`).
|
|
37
|
+
|
|
38
|
+
## Entry points
|
|
39
|
+
|
|
40
|
+
- ESM: `import { … } from '/assets/zx.esm.js'` (or `../src/index.js` in-repo, no build).
|
|
41
|
+
- Global: `<script src="/assets/zx.global.js">` → `window.zx.*`.
|
|
42
|
+
- Styles: load `/assets/zx.css` once.
|
|
43
|
+
- Build bundles with `npm run build` (esbuild). No build is needed to develop against `src/`.
|
|
44
|
+
|
|
45
|
+
## Theming
|
|
46
|
+
|
|
47
|
+
Load `zx.css`, then set theme/density on any ancestor (usually `<html>`):
|
|
48
|
+
|
|
49
|
+
```html
|
|
50
|
+
<html data-zx-theme="dark" data-zx-density="compact">
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Themes: `light` | `dark` | `auto` (follows OS). Density: `cozy` | `compact`. Define a product theme
|
|
54
|
+
by overriding **semantic** tokens under a `[data-zx-theme="name"]` selector:
|
|
55
|
+
`--zx-color-bg-page/surface/raised/control/hover/selected`, `--zx-color-border(-strong/-control)`,
|
|
56
|
+
`--zx-color-text(-muted/-placeholder)`, `--zx-color-accent(-hover)/on-accent`,
|
|
57
|
+
`--zx-color-danger/warning/success/info(+ -bg)`, `--zx-focus-ring`, `--zx-control-height/-radius`,
|
|
58
|
+
`--zx-space-*`, `--zx-radius-*`, `--zx-text-*`. Never use tier-1 palette tokens (`--zx-gray-*`,
|
|
59
|
+
`--zx-green-*`) or raw color literals in application/component CSS. Prefer the least-specific
|
|
60
|
+
override that works (global token → then, if needed, a component style).
|
|
61
|
+
|
|
62
|
+
## Talking to ZeyOS — use `@zeyos/client`
|
|
63
|
+
|
|
64
|
+
For ZeyOS business data use the **dedicated client library** `@zeyos/client`
|
|
65
|
+
(`npm install @zeyos/client`) — a zero-dependency, typed OpenAPI client (accounts,
|
|
66
|
+
transactions/invoices, tickets, 50+ resources), OAuth2/session auth, retries, schema
|
|
67
|
+
introspection. It replaces the legacy `gx.zeyos.Client` / `gx.zeyos.Request`.
|
|
68
|
+
|
|
69
|
+
```js
|
|
70
|
+
import { createZeyosClient, MemoryTokenStore, normalizeListResult } from '@zeyos/client';
|
|
71
|
+
const zeyos = createZeyosClient({
|
|
72
|
+
platform: 'https://cloud.zeyos.com/<instance>/',
|
|
73
|
+
auth: { mode: 'oauth', oauth: { tokenStore: new MemoryTokenStore({ accessToken }) } }
|
|
74
|
+
});
|
|
75
|
+
const { data } = normalizeListResult(await zeyos.api.listTransactions({ filters: { visibility: 0 }, limit: 50 }));
|
|
76
|
+
await zeyos.api.updateTransaction({ ID, status: 9 });
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Feed results straight into Zx components (Table `data`, Select `items`, Form values). See the
|
|
80
|
+
`website/layouts/zeyos-invoices.layout.js` layout for the reference integration. Zx also ships a minimal `Http` /
|
|
81
|
+
`zeyosService(service, accesskey)` for ad-hoc `remotecall`, but `@zeyos/client` is the default.
|
|
82
|
+
|
|
83
|
+
<!-- doc:zeyos -->
|
|
84
|
+
## ZeyOS binding (zx-zeyos)
|
|
85
|
+
|
|
86
|
+
`zx-zeyos` is the optional schema-driven layer above `@zeyos/client`: inject a client instance and
|
|
87
|
+
declare a resource to generate typed Zx controls. It is a separate ESM entry at
|
|
88
|
+
`dist/zx-zeyos.esm.js` (or `src/zeyos/index.js` during no-build development) and is deliberately
|
|
89
|
+
not re-exported from `src/index.js`. The binding has zero runtime dependencies and never imports or
|
|
90
|
+
bundles `@zeyos/client`; the application owns client creation, authentication, and transport.
|
|
91
|
+
|
|
92
|
+
```js
|
|
93
|
+
import { createZeyosClient } from '@zeyos/client';
|
|
94
|
+
import {
|
|
95
|
+
connect, dataFilterStateToFilters, zeyosForm, zeyosSelect, zeyosTable
|
|
96
|
+
} from '/assets/zx-zeyos.esm.js';
|
|
97
|
+
|
|
98
|
+
const client = createZeyosClient({ platform, auth });
|
|
99
|
+
const api = connect(client, { locale: 'en-GB' });
|
|
100
|
+
|
|
101
|
+
const list = zeyosTable(client, 'transactions', {
|
|
102
|
+
fields: ['transactionnum', 'account', 'date', 'netamount', 'status'],
|
|
103
|
+
sort: { id: 'date', dir: 'desc' },
|
|
104
|
+
selectable: 'multi'
|
|
105
|
+
});
|
|
106
|
+
await list.load();
|
|
107
|
+
|
|
108
|
+
const editor = zeyosForm(client, 'transactions', {
|
|
109
|
+
fields: ['transactionnum', 'account', 'date', 'netamount', 'status'],
|
|
110
|
+
onSaved: () => list.load()
|
|
111
|
+
});
|
|
112
|
+
await editor.load(transactionId);
|
|
113
|
+
await editor.save();
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
- `connect(client, { onError, locale })` returns `{client, list, get, create, update, reportError}`;
|
|
117
|
+
generated forms/tables use the same operation discovery and default `Message.error` reporting.
|
|
118
|
+
- `zeyosSelect(client, resource, opts)` returns an async Zx `Select`. Options include `fields`,
|
|
119
|
+
`labelKey`, `valueKey`, `searchFields`, `filters`, `limit`, and normal Select options. Its list
|
|
120
|
+
operation receives a query built by `buildListQuery`.
|
|
121
|
+
- `zeyosForm(client, resource, opts)` returns
|
|
122
|
+
`{form, ready, load(id), save(), getForm(), destroy()}`. `fields` is an ordered allow-list;
|
|
123
|
+
`exclude`, `labels`, `title`, `columns`, `value|id`, and `onSaved` curate behavior. `load` maps
|
|
124
|
+
stored timestamps to `Date`; `save` validates and creates/updates, maps back to Unix seconds,
|
|
125
|
+
calls `onSaved`, and shows a success toast.
|
|
126
|
+
- `zeyosTable(client, resource, opts)` returns
|
|
127
|
+
`{table, load(), setSearch(), setFilters(), loadMore(), count, page, hasMore, destroy()}`. Columns
|
|
128
|
+
come from schema metadata, sorting uses `sortMode:'server'`, and page zero replaces rows while
|
|
129
|
+
later pages append them using server `limit`/`offset` and `count`.
|
|
130
|
+
|
|
131
|
+
Field mapping is schema-driven: entity/FK → async `zxselect`; enum/list → `optionlist` or native
|
|
132
|
+
`select`; indexed `date` bigint → `date`; other date/time bigints → `datetime`; money/price/numeric
|
|
133
|
+
→ right-aligned `float`/number columns (amount columns use the row currency); boolean/checked →
|
|
134
|
+
`toggle` and a check column; percent/progress → numeric percent; email/tel/url → typed text; arrays
|
|
135
|
+
→ `valuelist`; long text/JSON → `textarea`; short scalar text → `text`.
|
|
136
|
+
|
|
137
|
+
ZeyOS queries always use `filters` (plural), not `filter`. Full-text input becomes `query`; sort is
|
|
138
|
+
sent as signed server fields (`+field`/`-field`); projection supports aliases and dot joins; and
|
|
139
|
+
search/filter/sort/pagination remain server-side. ZeyOS timestamps are integer Unix seconds, so
|
|
140
|
+
forms convert them to/from `Date`. Use `dataFilterStateToFilters(state, defs)` before
|
|
141
|
+
`tableBinding.setFilters(...)` when wiring a `DataFilter`.
|
|
142
|
+
|
|
143
|
+
### Module icons and colours
|
|
144
|
+
|
|
145
|
+
`src/zeyos/modules.js` is the configuration file for module identity — one entry per ZeyOS module,
|
|
146
|
+
holding the icon and the colour it is drawn in:
|
|
147
|
+
|
|
148
|
+
```js
|
|
149
|
+
notes: { label: 'Notes', icon: 'zeyos-notes', color: '#008853', fa: 'note-sticky' }
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
`icon` is the glyph's name in the ZeyOS Font Awesome kit (61 custom `zeyos-*` icons, rendered as
|
|
153
|
+
`<i class="fa-kit fa-zeyos-notes">`); `fa` is a stock Font Awesome fallback that also exists in
|
|
154
|
+
Font Awesome Free. Colours are the ZeyOS runtime palette (`ICO.Colors`) where ZeyOS defines one and
|
|
155
|
+
Zx defaults elsewhere; they are also emitted as `--zx-module-*` CSS custom properties, generated
|
|
156
|
+
from this file by `tools/build-module-tokens.js` and checked by the unit tests.
|
|
157
|
+
|
|
158
|
+
```js
|
|
159
|
+
import { moduleChip, moduleColor, registerModules, useZeyosIcons } from '/assets/zx-zeyos.esm.js';
|
|
160
|
+
|
|
161
|
+
await useZeyosIcons(); // loads ZEYOS_ICON_KIT, switches Zx to Font Awesome
|
|
162
|
+
nav.append(moduleChip('tickets', { size: 24, title: true })); // glyph on the module colour
|
|
163
|
+
moduleColor('invoices'); // '#535494' — aliases resolve to their module
|
|
164
|
+
registerModules({ tickets: '#f04639', 'my-fork': { label: 'My Fork', icon: 'zeyos-weblets' } });
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
- `moduleIcon(name, {size, label, standard})` → the bare glyph; `standard: true` uses the stock
|
|
168
|
+
Font Awesome fallback, for pages on a kit without the ZeyOS uploads.
|
|
169
|
+
- `moduleChip(name, {size, iconSize, label, title, standard})` → `<span class="zx-module-icon">`
|
|
170
|
+
with the glyph on the module colour. The glyph colour is whichever of ZeyOS's two foregrounds
|
|
171
|
+
(`#ffffff`, `#141414`) contrasts better, via `moduleGlyphColor(hex)`.
|
|
172
|
+
- `moduleInfo`/`moduleColor`/`moduleIconName`/`moduleKeys`/`normalizeModuleName` read the config.
|
|
173
|
+
Module, entity, and API resource names are all accepted, case-insensitively, with dots and
|
|
174
|
+
underscores normalized: `'transactions.billing'`, `'invoices'`, and `'Transactions Billing'` all
|
|
175
|
+
land on `transactions-billing`. Unknown names fall back to the `default` module — lookups never
|
|
176
|
+
throw.
|
|
177
|
+
- `registerModules(map)` adds forks and weblets or overrides shipped entries (a bare string sets
|
|
178
|
+
the colour only). Runtime ZeyOS menu data is authoritative: feed its colours in at startup.
|
|
179
|
+
<!-- /doc -->
|
|
180
|
+
|
|
181
|
+
## Custom elements
|
|
182
|
+
|
|
183
|
+
`defineElements()` (not auto-called) registers light-DOM `<zx-*>` wrappers with attribute↔option
|
|
184
|
+
reflection and ElementInternals form association: `<zx-toggle>`, `<zx-check-button>`, `<zx-select>`,
|
|
185
|
+
`<zx-checklist>`, `<zx-datebox>`, `<zx-timebox>`, `<zx-search>`, `<zx-groupbox>`, `<zx-tabbox>`,
|
|
186
|
+
`<zx-table>`, `<zx-dialog>`.
|
|
187
|
+
|
|
188
|
+
## gx compatibility
|
|
189
|
+
|
|
190
|
+
Load `zx-compat.global.js` (or import `zx-compat.esm.js`) →
|
|
191
|
+
`window.gx.{core,ui,util,zeyos,bootstrap}` wrapping Zx components (constructor/option/event-name
|
|
192
|
+
translation, MooTools-style `addEvent`). `gx.compat.installGlobals()` (opt-in) installs `__()`,
|
|
193
|
+
`_()`, `String.htmlSpecialChars`, and the element `store/retrieve` shim. Full namespace listing in
|
|
194
|
+
the "gx compatibility layer" component section below; class map and unsupported legacy APIs in
|
|
195
|
+
`MIGRATION.md`.
|
|
196
|
+
|
|
197
|
+
## Conventions (editing component source)
|
|
198
|
+
|
|
199
|
+
Follow `AGENTS.md`. Key rules: never declare instance class fields for state used in `render()`
|
|
200
|
+
(render runs inside the base constructor before field initializers); route all DOM listeners
|
|
201
|
+
through `this.listen()`; no `innerHTML` outside `h.raw()`; express state via ARIA/`data-*`, not
|
|
202
|
+
state classes; small glyph buttons use the shared `.zx-icon-btn` utility; follow the named APG
|
|
203
|
+
pattern with `:focus-visible` rings and `prefers-reduced-motion` guards.
|
|
204
|
+
|
|
205
|
+
## File map & commands
|
|
206
|
+
|
|
207
|
+
```
|
|
208
|
+
src/components/<name>/ component + CSS src/core/ kernel (component, dom, http, i18n, date…)
|
|
209
|
+
src/compat/ gx compat layer styles/ tokens + base.css
|
|
210
|
+
website/ marketing page + docs.html (docs.js) specs/ per-component specs
|
|
211
|
+
website/demos/ per-component demo modules website/layouts/ application layout examples
|
|
212
|
+
tests/ node unit + smoke dist/ built bundles
|
|
213
|
+
|
|
214
|
+
npm run serve # http://127.0.0.1:8321/website/docs.html (no build)
|
|
215
|
+
npm test # node --test tests/unit/*.test.js + node tests/lint-tokens.js
|
|
216
|
+
npm run build # dist/: zx.esm.js, zx.global.js (window.zx), zx-compat.global.js (window.gx), zx.css
|
|
217
|
+
node tools/build-module-tokens.js # regenerate styles/tokens/modules.css from src/zeyos/modules.js
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
# Components
|
|
223
|
+
|
|
224
|
+
Each component is `new Component(target, options)`. Events are the component-level names (also
|
|
225
|
+
dispatched as bubbling `zx-<name>` on the root). Detail is always an object.
|
|
226
|
+
|
|
227
|
+
<!-- doc:button -->
|
|
228
|
+
### Button — `button(options)` / `buttonGroup(buttons)`
|
|
229
|
+
|
|
230
|
+
Factories (not classes) returning elements, used both standalone and as the
|
|
231
|
+
`buttons: [{ label, kind, action|onclick }]` descriptors Dialog, Modal, Panel, and MasterPanel
|
|
232
|
+
accept.
|
|
233
|
+
|
|
234
|
+
- **`button(options)`** → `HTMLButtonElement`. Options: `label`, `icon` (an icon name), `kind:
|
|
235
|
+
'default'|'primary'|'danger'|'ghost'`, `size: 'md'|'sm'`, `disabled`, `title`, `onclick`.
|
|
236
|
+
- **`buttonGroup([button(…), …])`** → a container that joins the buttons into one control.
|
|
237
|
+
<!-- /doc -->
|
|
238
|
+
|
|
239
|
+
<!-- doc:badge -->
|
|
240
|
+
### Badge — `badge(options)` / `badgeGroup(badges)`
|
|
241
|
+
|
|
242
|
+
Factories (not classes) returning a status pill: a small, non-interactive `<span>` carrying a
|
|
243
|
+
semantic colour, used for stages, counts, tags, and record-header metadata. A badge with no label
|
|
244
|
+
but a `title` is named for assistive technology through `role="img"`; everything else is plain text
|
|
245
|
+
and needs no ARIA.
|
|
246
|
+
|
|
247
|
+
- **Options** — `label`, `icon` (an icon name, rendered before the label), `kind:
|
|
248
|
+
'neutral'|'accent'|'success'|'warning'|'danger'|'info'`, `variant: 'soft'|'solid'|'outline'`,
|
|
249
|
+
`size: 'md'|'sm'`, `dot` (a leading status dot instead of an icon), `title`.
|
|
250
|
+
- **Methods** — none. `badge(options)` → `HTMLSpanElement`, `badgeGroup([badge(…), …])` → a wrapping
|
|
251
|
+
inline row with its own spacing; you own both elements.
|
|
252
|
+
- **Events** — none; a badge is not interactive.
|
|
253
|
+
<!-- /doc -->
|
|
254
|
+
|
|
255
|
+
<!-- doc:toolbar -->
|
|
256
|
+
### Toolbar — APG toolbar
|
|
257
|
+
|
|
258
|
+
A single non-wrapping row of controls that is one tab stop: arrows move between the controls, and
|
|
259
|
+
what no longer fits collapses into a trailing overflow menu. Items are `button()` descriptors,
|
|
260
|
+
ready-made Elements, or `'-'` for a separator; a `ResizeObserver` remeasures the row, hides the
|
|
261
|
+
items that overflow — removing them from the tab order and the roving group — and mirrors them into
|
|
262
|
+
a `MenuButton`, where choosing one activates the original control.
|
|
263
|
+
|
|
264
|
+
- **Options** — `items: [Element | '-' | {name, label, icon, kind, size, disabled, title, active,
|
|
265
|
+
onclick}]`, `align: 'start'|'end'|'between'`, `label` (accessible name, default `'Toolbar'`),
|
|
266
|
+
`overflow` (default `true`), `overflowLabel` (default `'More'`), `dense`. `active` renders the
|
|
267
|
+
item as a pressed toggle (`aria-pressed`).
|
|
268
|
+
- **Methods** — `setItems()`, `getItem(name)`, `enable(name)`, `disable(name)`,
|
|
269
|
+
`setActive(name, active?)`, `destroy()`.
|
|
270
|
+
- **Events** — `action {name, item}`, emitted for descriptor-built items in addition to their own
|
|
271
|
+
`onclick`, from the row and from the overflow menu alike.
|
|
272
|
+
- **Keyboard** — Tab reaches the toolbar once; ArrowLeft/ArrowRight move between controls and wrap;
|
|
273
|
+
Home/End jump to the first and last control.
|
|
274
|
+
<!-- /doc -->
|
|
275
|
+
|
|
276
|
+
<!-- doc:empty-state -->
|
|
277
|
+
### EmptyState — `emptyState(options)`
|
|
278
|
+
|
|
279
|
+
A factory (not a class) returning the placeholder shown where a list, table, or panel has no content
|
|
280
|
+
yet: an optional decorative icon, a headline, a sentence, and optional actions. The headline is a
|
|
281
|
+
paragraph rather than a heading, so dropping one into a Table's empty slot or a Panel body never
|
|
282
|
+
disturbs the page's heading outline.
|
|
283
|
+
|
|
284
|
+
- **Options** — `icon` (an icon name, `null` for none; default `'folder-open'`), `title`,
|
|
285
|
+
`description`, `actions: [Element | buttonOptions]`, `size: 'md'|'sm'`, `align: 'center'|'start'`.
|
|
286
|
+
- **Methods** — none. `emptyState(options)` → `HTMLDivElement`, which you own and place yourself.
|
|
287
|
+
- **Events** — none; action buttons carry their own `onclick`.
|
|
288
|
+
<!-- /doc -->
|
|
289
|
+
|
|
290
|
+
<!-- doc:loading -->
|
|
291
|
+
### Spinner / ProgressBar / InlineLoading
|
|
292
|
+
|
|
293
|
+
Three shapes of "wait". A **spinner** when the duration is unknown and there is nothing to say
|
|
294
|
+
about progress; a **progress bar** when the completed share is known; **inline loading** when the
|
|
295
|
+
wait resolves into its own outcome in the place it started. `spinner()` is a factory returning an
|
|
296
|
+
element — there is no state to update and nothing to destroy — while the other two are components.
|
|
297
|
+
|
|
298
|
+
#### spinner()
|
|
299
|
+
|
|
300
|
+
- **Signature** — `spinner({size, label, showLabel, kind})` → `HTMLSpanElement`.
|
|
301
|
+
- **Options** — `size: 'sm'|'md'|'lg'` (16px, 24px, 40px), `label: ''`, `showLabel: false`,
|
|
302
|
+
`kind: 'accent'|'current'` — `current` draws the ring in `currentColor`, which is what a ring
|
|
303
|
+
inside a button or a badge needs.
|
|
304
|
+
- **Accessibility** — a `label` makes the element `role="status"` and keeps the text in the
|
|
305
|
+
accessibility tree even when `showLabel` is false; without a label the ring is `aria-hidden`,
|
|
306
|
+
which is correct whenever a nearby element already announces the wait.
|
|
307
|
+
|
|
308
|
+
#### ProgressBar
|
|
309
|
+
|
|
310
|
+
- **Options** — `value: 0`, `max: 100`, `label: ''`, `helperText: ''`,
|
|
311
|
+
`status: 'active'|'success'|'error'`, `indeterminate: false`, `size: 'md'|'sm'`,
|
|
312
|
+
`hideLabel: false`, `showValue: true`, `formatValue: (value, max) => string`.
|
|
313
|
+
- **Methods** — `get()`, `set(value, {silent})` (clamped to `[0, max]`), `percent()`, `setMax()`,
|
|
314
|
+
`setStatus(status, helperText?)`, `setLabel()`, `setHelperText()`, `setIndeterminate(flag)`.
|
|
315
|
+
- **Events** — `change {value, percent}`, `complete {value}` once the value reaches `max`.
|
|
316
|
+
- **Behaviour** — `status` is a display concern only: a run that failed at 100% still reads 100%.
|
|
317
|
+
An indeterminate bar drops `aria-valuenow` rather than reporting a number it does not have, and
|
|
318
|
+
under reduced motion its travelling band becomes a static muted track — visibly busy, never a
|
|
319
|
+
wrong number.
|
|
320
|
+
- **Accessibility** — the track is `role="progressbar"` with `aria-valuemin`, `aria-valuemax`,
|
|
321
|
+
`aria-valuenow`, and `aria-valuetext`, labelled by the visible label (kept for assistive
|
|
322
|
+
technology when `hideLabel` is set).
|
|
323
|
+
|
|
324
|
+
#### InlineLoading
|
|
325
|
+
|
|
326
|
+
- **Options** — `status: 'inactive'|'active'|'success'|'error'`, `description: ''`,
|
|
327
|
+
`size: 'sm'|'md'`, `live: true`.
|
|
328
|
+
- **Methods** — `get()`, `getDescription()`, `set(status, description?, {silent})`,
|
|
329
|
+
`setDescription()`.
|
|
330
|
+
- **Events** — `statuschange {status, description}`.
|
|
331
|
+
- **Accessibility** — the whole element is one polite live region (`role="status"`), so the
|
|
332
|
+
outcome is announced without the focus moving off the control that started the work. Use
|
|
333
|
+
`Message` instead when the outcome deserves a dismissible notification rather than a line of
|
|
334
|
+
text that stays put.
|
|
335
|
+
<!-- /doc -->
|
|
336
|
+
|
|
337
|
+
<!-- doc:skeleton -->
|
|
338
|
+
### Skeleton
|
|
339
|
+
|
|
340
|
+
Placeholders shaped like the content that is coming, so the layout does not jump when it arrives.
|
|
341
|
+
That is the whole reason to prefer them over a spinner, and the reason they only pay off where the
|
|
342
|
+
shape is genuinely known. All three are factories returning elements.
|
|
343
|
+
|
|
344
|
+
- **Factories** — `skeleton({width, height, radius})` → one block; `skeletonText({lines, width,
|
|
345
|
+
heading, lastLineWidth})` → a stack of lines; `skeletonTable({rows, columns, header})` → the
|
|
346
|
+
grid a table will fill.
|
|
347
|
+
- **skeleton options** — `width: '100%'`, `height: '1rem'` (a number is read as pixels),
|
|
348
|
+
`radius: 'sm'|'md'|'full'`. Equal sides plus `radius: 'full'` gives the circle an avatar or icon
|
|
349
|
+
placeholder needs.
|
|
350
|
+
- **skeletonText options** — `lines: 3` (1–20), `width: '100%'`, `heading: false` (draws the first
|
|
351
|
+
line taller), `lastLineWidth: '60%'` — the short final line is what makes the block read as
|
|
352
|
+
prose rather than as a filled rectangle.
|
|
353
|
+
- **skeletonTable options** — `rows: 5` (1–50), `columns: 4` (1–20), `header: true`. Rendered as
|
|
354
|
+
plain elements, not a `<table>`: there is no data to expose, and a table whose cells are all
|
|
355
|
+
empty is worse for assistive technology than no table at all.
|
|
356
|
+
- **Accessibility** — every skeleton is `aria-hidden`, because a screen reader gains nothing from
|
|
357
|
+
an announced grey box. Set `aria-busy="true"` on the region being filled and remove it together
|
|
358
|
+
with the placeholder.
|
|
359
|
+
- **Behaviour** — the pulse is wrapped in `prefers-reduced-motion: no-preference`; without motion
|
|
360
|
+
a legible static block stays behind.
|
|
361
|
+
<!-- /doc -->
|
|
362
|
+
|
|
363
|
+
<!-- doc:check-button -->
|
|
364
|
+
### CheckButton
|
|
365
|
+
|
|
366
|
+
Two-state pressed button on a native `<button aria-pressed>`. The check indicator has a glyph in
|
|
367
|
+
both states — an empty box when off, a check when on — so an unpressed or disabled button still
|
|
368
|
+
reads as a two-state control.
|
|
369
|
+
|
|
370
|
+
- **Options** — `label: '' | [onLabel, offLabel]`, `checked: false`, `icon: true`,
|
|
371
|
+
`disabled: false`.
|
|
372
|
+
- **Methods** — `get()`, `set(checked, {silent})`, `toggle()`, `setLabel()`, `enable()`/`disable()`.
|
|
373
|
+
- **Events** — `change {checked}`, fired only on a real state change.
|
|
374
|
+
- **Keyboard** — Space/Enter.
|
|
375
|
+
<!-- /doc -->
|
|
376
|
+
|
|
377
|
+
<!-- doc:toggle -->
|
|
378
|
+
### Toggle
|
|
379
|
+
|
|
380
|
+
Switch (`role="switch"`, `aria-checked`).
|
|
381
|
+
|
|
382
|
+
- **Options** — `checked: false`, `value: true`, `label: null`, `disabled: false`.
|
|
383
|
+
- **Methods** — `get()`, `set(checked, {silent})`, `toggle()`, `getValue()` (returns
|
|
384
|
+
`options.value` when on, `false` when off), `enable()`/`disable()`.
|
|
385
|
+
- **Events** — `change {checked, value}`.
|
|
386
|
+
- **Keyboard** — Space/Enter.
|
|
387
|
+
<!-- /doc -->
|
|
388
|
+
|
|
389
|
+
<!-- doc:search -->
|
|
390
|
+
### Search
|
|
391
|
+
|
|
392
|
+
Search input with embedded search and clear buttons (`role="search"`).
|
|
393
|
+
|
|
394
|
+
- **Options** — `placeholder`, `value`, `clearable: true`, `debounce: 250`.
|
|
395
|
+
- **Methods** — `get()`, `set(value, {silent})`, `focus()`, `clear()`.
|
|
396
|
+
- **Events** — `input {value}` (debounced), `submit {value}` (Enter or the button), `clear`.
|
|
397
|
+
<!-- /doc -->
|
|
398
|
+
|
|
399
|
+
<!-- doc:select -->
|
|
400
|
+
### Select — APG editable combobox
|
|
401
|
+
|
|
402
|
+
Unifies single-select, local filtering, and async loading.
|
|
403
|
+
|
|
404
|
+
- **Options** — `items: []`, `valueKey: 'ID'` (string key or `(item)=>id`), `labelKey: 'name'`
|
|
405
|
+
(string or `(item)=>string`), `renderItem`, `renderValue`, `value`, `disabled`, `placeholder`,
|
|
406
|
+
`clearable: false`, `filter: false | 'local' | async (query)=>items`, `searchKeys`,
|
|
407
|
+
`minQuery: 0`, `debounce: 200`, `listHeight: 280`, `groupKey`.
|
|
408
|
+
- **Getters** — `.value`, `.selected`.
|
|
409
|
+
- **Methods** — `set(id, {silent})`, `setItems()`, `reset()`, `open()`/`close()`,
|
|
410
|
+
`enable()`/`disable()`, `focus()`.
|
|
411
|
+
- **Events** — `change {value, item}` (`item` is null on clear), `open`, `close`, `query {query}`,
|
|
412
|
+
`loaded {items}`.
|
|
413
|
+
- **Preset** — `Select.priority(target, opts)`, a 5-level priority picker.
|
|
414
|
+
- **Keyboard** (APG combobox) — ArrowDown/Alt+ArrowDown open; arrows navigate and wrap; Home/End;
|
|
415
|
+
Enter selects; Esc closes; Tab closes; printable characters filter (editable) or run a typeahead
|
|
416
|
+
(readonly). `aria-activedescendant` tracks the active option.
|
|
417
|
+
<!-- /doc -->
|
|
418
|
+
|
|
419
|
+
<!-- doc:checklist -->
|
|
420
|
+
### Checklist
|
|
421
|
+
|
|
422
|
+
Searchable multi-check list with optional async loading.
|
|
423
|
+
|
|
424
|
+
- **Options** — `items`, `valueKey: 'ID'`, `labelKey: 'name'`, `checkedKey: 'on'`, `search: true`,
|
|
425
|
+
`height: 280`, `defaultChecked: false`, `load: async ()=>items`.
|
|
426
|
+
- **Methods** — `setItems()`, `getValues()`, `setValues(ids)`, `checkAll()`/`uncheckAll()`,
|
|
427
|
+
`search(query)`, `reload()`.
|
|
428
|
+
- **Events** — `change {values}`, `loaded`.
|
|
429
|
+
<!-- /doc -->
|
|
430
|
+
|
|
431
|
+
<!-- doc:number-field -->
|
|
432
|
+
### NumberField
|
|
433
|
+
|
|
434
|
+
Numeric input with decrement/increment buttons, following the APG **spinbutton** pattern. The step
|
|
435
|
+
buttons are `tabindex="-1"`/`aria-hidden` pointer affordances — the input itself is the spinbutton.
|
|
436
|
+
Field type: `number`.
|
|
437
|
+
|
|
438
|
+
- **Options** — `value` (number|null), `min`, `max`, `step: 1`, `largeStep: 10` (PageUp/PageDown
|
|
439
|
+
multiplier), `precision` (decimals; derived from `step` when null), `wrap: false` (stepping past
|
|
440
|
+
a bound jumps to the other one), `placeholder`, `unit` (suffix rendered inside the control),
|
|
441
|
+
`group: false` (thousands separators while idle), `locale`, `disabled`, `readonly`, `required`,
|
|
442
|
+
`name`.
|
|
443
|
+
- **Methods** — `get()`, `set(value, {silent})` (parses strings, snaps to the step grid, clamps to
|
|
444
|
+
the range; `''`/null clear it), `stepUp(n)`, `stepDown(n)`, `setRange(min, max)`,
|
|
445
|
+
`setReadonly()`, `reset()`, `focus()`, `getInput()`, `enable()`/`disable()`.
|
|
446
|
+
- **Events** — `change {value}` (committed), `input {value}` (per keystroke, possibly unsnapped).
|
|
447
|
+
- **Keyboard** — ↑/↓ one step, PageUp/PageDown `largeStep` steps, Home/End jump to `min`/`max` when
|
|
448
|
+
set (otherwise they keep their caret meaning), Enter commits, wheel steps while focused.
|
|
449
|
+
- **Also exported** — `parseNumber(raw)` (accepts `.` and `,` as the decimal separator, ignores
|
|
450
|
+
grouping, returns null for junk) and `snapNumber(value, {min, max, step, precision})`.
|
|
451
|
+
<!-- /doc -->
|
|
452
|
+
|
|
453
|
+
<!-- doc:rating -->
|
|
454
|
+
### Rating
|
|
455
|
+
|
|
456
|
+
Star rating built as an APG **radio group**: one radio per selectable step, a single tab stop,
|
|
457
|
+
arrow-key selection. Field type: `rating`.
|
|
458
|
+
|
|
459
|
+
- **Options** — `value: 0` (0 = unrated), `max: 5`, `allowHalf: false` (renders two radios per
|
|
460
|
+
symbol so every reachable value has a nameable control), `clearable: true` (re-selecting the
|
|
461
|
+
current value clears it), `readonly`, `disabled`, `label: 'Rating'`, `icon: 'star'`,
|
|
462
|
+
`labels: []` (per-step accessible names, lowest first), `showValue: false`, `count` (rating
|
|
463
|
+
count shown beside the value), `size: 'sm'|'md'|'lg'`.
|
|
464
|
+
- **Methods** — `get()`, `set(value, {silent})`, `clear()`, `reset()`, `setCount()`,
|
|
465
|
+
`setReadonly()`, `focus()`, `enable()`/`disable()`.
|
|
466
|
+
- **Events** — `change {value}`, `hover {value|null}` (pointer or focus preview; null on leave).
|
|
467
|
+
- **Keyboard** — ←/↓ and →/↑ step, Home selects the first step, End the last, Delete/Backspace
|
|
468
|
+
clear.
|
|
469
|
+
<!-- /doc -->
|
|
470
|
+
|
|
471
|
+
<!-- doc:slider -->
|
|
472
|
+
### Slider
|
|
473
|
+
|
|
474
|
+
A bounded numeric value set by dragging — a discount rate, a threshold, a weighting. Built on a
|
|
475
|
+
native `<input type="range">`, which is where the whole keyboard map and the announcement come
|
|
476
|
+
from; everything Zx adds is chrome around it. Field type: `slider`.
|
|
477
|
+
|
|
478
|
+
- **Options** — `value: 0`, `min: 0`, `max: 100`, `step: 1`, `label: ''`, `hideLabel: false`,
|
|
479
|
+
`showValue: true`, `showBounds: false` (draws `min` and `max` either side of the track),
|
|
480
|
+
`showInput: false` (adds a number box for entering a value precisely instead of aiming at it),
|
|
481
|
+
`unit: ''`, `marks: null` (numbers, or `{value, label}` objects), `disabled: false`,
|
|
482
|
+
`readonly: false`, `size: 'md'|'sm'`, `formatValue: (value) => string`.
|
|
483
|
+
- **Methods** — `get()`, `set(value, {silent})`, `setRange(min, max, step?)`, `setLabel()`,
|
|
484
|
+
`setReadonly()`, `focus()`, `enable()`/`disable()`. `value` is also a getter/setter.
|
|
485
|
+
- **Events** — `input {value}` continuously while dragging, `change {value}` once the value is
|
|
486
|
+
committed (pointer released, or a key pressed).
|
|
487
|
+
- **Behaviour** — values snap to the step grid anchored at `min`, so 5…95 by 10 gives 5, 15, 25 —
|
|
488
|
+
not 10, 20, 30. Bounds win over the grid, so a `max` that is not a whole number of steps above
|
|
489
|
+
`min` stays reachable. A fractional step snaps to the decimals it carries: 0.1 gives 0.3, never
|
|
490
|
+
0.30000000000000004. `stepPrecision(step)` is exported for the same arithmetic elsewhere;
|
|
491
|
+
snapping itself reuses `snapNumber` from NumberField.
|
|
492
|
+
- **Keyboard** — arrows step by `step`, Page Up/Page Down jump, Home and End go to the bounds —
|
|
493
|
+
all native. A read-only slider stays focusable and announced but refuses every edit, pointer and
|
|
494
|
+
keyboard alike; a disabled one leaves the tab order entirely.
|
|
495
|
+
<!-- /doc -->
|
|
496
|
+
|
|
497
|
+
<!-- doc:copy -->
|
|
498
|
+
### copyButton() / CopyInput
|
|
499
|
+
|
|
500
|
+
Copying a value, and saying so. A clipboard write is silent, so both of these confirm it in place
|
|
501
|
+
— and stay quiet when the browser refuses. Both go through `copyToClipboard()`, which falls back
|
|
502
|
+
to a hidden textarea when the async Clipboard API is unavailable or denied.
|
|
503
|
+
|
|
504
|
+
#### copyButton()
|
|
505
|
+
|
|
506
|
+
- **Signature** — `copyButton({text, label, title, feedback, feedbackDuration, size, kind, oncopy})`
|
|
507
|
+
→ `HTMLButtonElement`.
|
|
508
|
+
- **Options** — `text: ''` — a string, or **a function returning one**, which is what a button
|
|
509
|
+
beside a live value needs so the copy is never stale; `label: ''` (empty renders an icon-only
|
|
510
|
+
ghost button), `title: 'Copy'`, `feedback: 'Copied'`, `feedbackDuration: 2000`,
|
|
511
|
+
`size: 'md'|'sm'`, `kind: 'default'|'primary'|'ghost'`.
|
|
512
|
+
- **Events** — `oncopy(text, copied)` is called for every attempt; `copied` is `false` when the
|
|
513
|
+
clipboard refused the write, and the button then stays untouched rather than claiming a copy
|
|
514
|
+
that did not happen.
|
|
515
|
+
- **Accessibility** — on success the glyph becomes a tick and a visually hidden `role="status"`
|
|
516
|
+
speaks the confirmation, then both revert. The icon-only form carries the `title` as its
|
|
517
|
+
accessible name.
|
|
518
|
+
|
|
519
|
+
#### CopyInput
|
|
520
|
+
|
|
521
|
+
- **Options** — `value: ''`, `label: ''`, `hideLabel: false`, `feedback: 'Copied'`,
|
|
522
|
+
`buttonTitle: 'Copy'`, `size: 'md'|'sm'`.
|
|
523
|
+
- **Methods** — `get()`, `set(value)`, `focus()` (focuses and selects the whole value).
|
|
524
|
+
- **Events** — `copy {value, copied}`.
|
|
525
|
+
- **Behaviour** — the field is `readonly`, not `disabled`, so the text stays selectable and
|
|
526
|
+
reachable by keyboard; focusing it selects the whole value, which is what someone reaching for
|
|
527
|
+
Ctrl+C expects.
|
|
528
|
+
<!-- /doc -->
|
|
529
|
+
|
|
530
|
+
<!-- doc:date-picker -->
|
|
531
|
+
### DatePicker / MonthPicker / TimePicker
|
|
532
|
+
|
|
533
|
+
Inline, embeddable pickers.
|
|
534
|
+
|
|
535
|
+
#### DatePicker — APG date grid
|
|
536
|
+
|
|
537
|
+
- **Options** — `value: Date|null`, `min`, `max`, `weekStart: 1`, `showWeekNumbers: false`,
|
|
538
|
+
`time: false`.
|
|
539
|
+
- **Methods** — `get()`, `set()`, `focus()`.
|
|
540
|
+
- **Events** — `change {date}`, `monthchange {year, month}`.
|
|
541
|
+
- **Keyboard** — arrows move a day, PageUp/PageDown a month, Shift+PageUp/PageDown a year,
|
|
542
|
+
Home/End, Enter/Space select. Day cells carry an `aria-label`.
|
|
543
|
+
|
|
544
|
+
#### MonthPicker
|
|
545
|
+
|
|
546
|
+
A 12-month grid. `get()`/`set()` work on the first of the month; event `change {date}`.
|
|
547
|
+
|
|
548
|
+
#### TimePicker
|
|
549
|
+
|
|
550
|
+
Two spinbuttons (three with `seconds`), plus an optional clock face behind a toggle button: pick an
|
|
551
|
+
hour on the outer (1–12) or inner (13–23, 00) ring and the dial moves on to the minutes. Every mark
|
|
552
|
+
is a radio in an APG radio group; the spinbuttons stay the primary control.
|
|
553
|
+
|
|
554
|
+
- **Options** — `value: null`, `seconds: false`, `step: 5` (minute increment for the arrow keys),
|
|
555
|
+
`clock: true`.
|
|
556
|
+
- **Methods** — `get()`, `set()`, `openClock(unit)`, `closeClock()`, `toggleClock()`.
|
|
557
|
+
- **Events** — `change {time}`.
|
|
558
|
+
<!-- /doc -->
|
|
559
|
+
|
|
560
|
+
<!-- doc:datebox -->
|
|
561
|
+
### Datebox / DateTimeBox
|
|
562
|
+
|
|
563
|
+
Text date input with a calendar popover. `DateTimeBox(target, opts)` is a Datebox with
|
|
564
|
+
`time: true`, which also brings the TimePicker (and its clock face) into the popover. Format
|
|
565
|
+
tokens are the date-utility ones: `%d %m %Y %H %M`.
|
|
566
|
+
|
|
567
|
+
- **Options** — `value: Date | number(unix) | string`, `format: '%d.%m.%Y'`, `time: false`, `min`,
|
|
568
|
+
`max`, `placeholder`, `clearable: true`, `disabled`.
|
|
569
|
+
- **Methods** — `get(unit='date')` (`'seconds'` → unix), `set(value, {silent})` (accepts a Date;
|
|
570
|
+
pass Date objects, not display strings), `open()`/`close()`, `enable()`/`disable()`, `focus()`.
|
|
571
|
+
- **Events** — `change {date}`, `invalid {text}`, `open`, `close`.
|
|
572
|
+
<!-- /doc -->
|
|
573
|
+
|
|
574
|
+
<!-- doc:date-range -->
|
|
575
|
+
### DateRangePicker / DateRangeBox
|
|
576
|
+
|
|
577
|
+
An inline two-month calendar for picking a start and an end day, plus the form-usable text input that wraps it. The first click opens a range and the second closes it; while an end is pending, hovering and arrow-key focus preview where the range would land, and clicking on or before the pending start restarts it there instead of producing an inverted range. Both grids behave as one roving-focus unit, so arrowing right off 31 August continues on 1 September. Range maths is exported separately as `normalizeRange`, `rangeNights`, `rangeStateOf`, and `clampRange`, so the rules can be reused and tested without a DOM.
|
|
578
|
+
|
|
579
|
+
#### DateRangePicker
|
|
580
|
+
|
|
581
|
+
- **Options** — `start: null`, `end: null` (Dates), `min: null`, `max: null`, `months: 2` (a container query drops to one month in narrow hosts), `weekStart: 1`, `showWeekNumbers: false`, `minNights: 0`, `maxNights: null`, `presets: []` (an array of `{name, label, range: () => ({start, end})}`, or `true` for the built-in Today, Yesterday, Last 7 days, Last 30 days, This month, Last month, This quarter, This year), `disabled: false`.
|
|
582
|
+
- **Methods** — `get()` → `{start, end}` copies, `set({start, end}, {silent})` (clamped to the bounds, scrolls the first month to the start), `clear({silent})`, `focus()`, `enable()`, `disable()`, `destroy()`.
|
|
583
|
+
- **Events** — `change {start, end}` once a range is complete and on clear, `select {start, end}` on every click including the half-open state where `end` is still null, `monthchange {year, month}` when the first visible month moves.
|
|
584
|
+
- **Keyboard** — arrows move a day, PageUp/PageDown a month, Shift+PageUp/PageDown a year, Home/End the ends of the week, Enter or Space selects, Escape abandons a half-picked range and restores the last complete one. Days outside `min`/`max` — and, once a start is pending, days that would break `minNights`/`maxNights` — stay keyboard-focusable but carry `aria-disabled="true"`. Committed days carry `data-range="start" | "middle" | "end"` and previewed days `data-range="preview"`; the picker is a `role="group"` holding one `role="grid"` per month, and the selection is announced through a visually hidden `aria-live="polite"` region ("1 – 14 August 2026, 13 nights").
|
|
585
|
+
- **Exported helpers** — `normalizeRange(start, end)` orders a pair at local midnight (invalid values become null), `rangeNights(start, end)` counts nights from calendar days so DST never shifts the total, `rangeStateOf(day, start, end)` → `'start' | 'middle' | 'end' | null`, `clampRange(range, {min, max, minNights, maxNights})` pulls a range inside its bounds.
|
|
586
|
+
|
|
587
|
+
#### DateRangeBox
|
|
588
|
+
|
|
589
|
+
- **Options** — everything DateRangePicker takes, plus `format: '%d.%m.%Y'`, `separator: ' – '`, `placeholder: null` (derived from the format when null), `clearable: true`. Typed text is parsed as `"<start><separator><end>"`; a lone date reads as a one-day range and a trailing separator keeps the range half open.
|
|
590
|
+
- **Methods** — `get(unit = 'date')` (`'seconds'` returns `{start, end}` as Unix seconds), `set(range, {silent})` accepting `{start, end}` of Dates, Unix-seconds numbers, or formatted strings, `clear({silent})`, `open()`, `close()`, `enable()`, `disable()`, `focus()`, `destroy()`.
|
|
591
|
+
- **Events** — `change {start, end}`, `invalid {text}` when the typed value cannot be parsed (the text stays editable and the field takes `data-state="invalid"` plus `aria-invalid`), `open`, `close`.
|
|
592
|
+
<!-- /doc -->
|
|
593
|
+
|
|
594
|
+
<!-- doc:timebox -->
|
|
595
|
+
### Timebox
|
|
596
|
+
|
|
597
|
+
Duration input (H:MM[:SS], optional sign) — a length of time, not a time of day. For a time of day
|
|
598
|
+
use `TimePicker` or `DateTimeBox`.
|
|
599
|
+
|
|
600
|
+
- **Options** — `value: 0`, `unit: 'minutes'|'seconds'|'hours'`, `seconds: false`, `signed: false`,
|
|
601
|
+
`disabled`.
|
|
602
|
+
- **Methods** — `get(unit)`, `set(value, unit?, {silent})`, `enable()`/`disable()`.
|
|
603
|
+
- **Events** — `change {value}`, in `options.unit`.
|
|
604
|
+
- **Also exported** — the pure helpers `splitTime` and `joinTime`.
|
|
605
|
+
<!-- /doc -->
|
|
606
|
+
|
|
607
|
+
<!-- doc:message -->
|
|
608
|
+
### Message — toasts, inline, progress
|
|
609
|
+
|
|
610
|
+
Floating toasts come from statics; `new Message(target)` gives an inline, non-floating area with
|
|
611
|
+
the same `.show()`. The toast region is `role="status"`/`aria-live` (`role="alert"` for errors).
|
|
612
|
+
|
|
613
|
+
- **Statics** — `Message.info/success/warning/error(msg, opts)`,
|
|
614
|
+
`Message.show(msg, { kind, timeout=4000, closable })` → `{ close() }`,
|
|
615
|
+
`Message.progress(text)` → `{ update(pct, text?), done(), fail(text?) }`.
|
|
616
|
+
- **Per-message options** — `kind: 'info'`, `timeout`, `closable: true`, `maxVisible: 5` (extras
|
|
617
|
+
queue, and hovering pauses dismissal).
|
|
618
|
+
<!-- /doc -->
|
|
619
|
+
|
|
620
|
+
<!-- doc:modal -->
|
|
621
|
+
### Modal
|
|
622
|
+
|
|
623
|
+
Thin overlay on the native `<dialog>` element. The backdrop uses `--zx-color-bg-backdrop`, and
|
|
624
|
+
focus returns to the opener on close.
|
|
625
|
+
|
|
626
|
+
- **Options** — `content`, `width: 'auto'`, `closable: true`, `lightDismiss: false`,
|
|
627
|
+
`destroyOnClose: false`.
|
|
628
|
+
- **Methods** — `open()`, `close(result?)`, `setContent()`, `isOpen()`.
|
|
629
|
+
- **Events** — `open`, `close {result}`, `cancel` (Esc; preventable with `event.preventDefault()`).
|
|
630
|
+
<!-- /doc -->
|
|
631
|
+
|
|
632
|
+
<!-- doc:dialog -->
|
|
633
|
+
### Dialog (extends Modal)
|
|
634
|
+
|
|
635
|
+
Structured modal: a header with title and close button, a body, and footer buttons.
|
|
636
|
+
|
|
637
|
+
- **Options** — `title`, `size: 'sm'|'md'|'lg'|number`,
|
|
638
|
+
`buttons: [{ label, kind, action: 'close'|'cancel'|fn(dialog), autofocus }]`, `closable: true`.
|
|
639
|
+
- **Methods** — `setTitle()`, `setContent()`, `setButtons()`,
|
|
640
|
+
`addView(key, { content, buttons? })`, `showView(key)`.
|
|
641
|
+
- **Statics returning Promises** — `Dialog.alert({ title, message })`,
|
|
642
|
+
`Dialog.confirm({ title, message, danger })` → boolean,
|
|
643
|
+
`Dialog.prompt({ title, message, value })` → string|null.
|
|
644
|
+
<!-- /doc -->
|
|
645
|
+
|
|
646
|
+
<!-- doc:dropdown -->
|
|
647
|
+
### Dropdown
|
|
648
|
+
|
|
649
|
+
Generic anchored popover in the top layer, constructed as `new Dropdown(anchor, options)`. It
|
|
650
|
+
light-dismisses on an outside click or Esc, and mirrors `aria-expanded` onto the anchor.
|
|
651
|
+
|
|
652
|
+
- **Options** — `content`,
|
|
653
|
+
`placement: 'bottom-start'|'bottom-end'|'top-start'|'top-end'|'bottom'|'top'`, `offset: 4`,
|
|
654
|
+
`matchWidth: false`, `openOn: 'click'|'manual'`, `closeOnSelect: false`.
|
|
655
|
+
- **Methods** — `open()`, `close()`, `toggle()`, `isOpen()`, `setContent()`, `getPanel()`.
|
|
656
|
+
- **Events** — `open`, `close`.
|
|
657
|
+
<!-- /doc -->
|
|
658
|
+
|
|
659
|
+
<!-- doc:menu-button -->
|
|
660
|
+
### MenuButton — APG menu button
|
|
661
|
+
|
|
662
|
+
A button that opens a `role="menu"`.
|
|
663
|
+
|
|
664
|
+
- **Options** — `label`, `icon`, `kind`,
|
|
665
|
+
`items: [{ label, icon?, value?, disabled?, danger?, onselect? } | '-']`, `placement`.
|
|
666
|
+
- **Methods** — `setItems()`, `open()`, `close()`, `setLabel()`.
|
|
667
|
+
- **Events** — `select {value, item}`, `open`, `close`.
|
|
668
|
+
- **Keyboard** — ArrowDown/Enter/Space open and focus the first item; ArrowUp opens and focuses the
|
|
669
|
+
last; arrows cycle; Home/End; typeahead; Enter/Space select; Esc/Tab close.
|
|
670
|
+
<!-- /doc -->
|
|
671
|
+
|
|
672
|
+
<!-- doc:context-menu -->
|
|
673
|
+
### ContextMenu
|
|
674
|
+
|
|
675
|
+
Right-click menu for a region or a row, implementing the APG menu pattern. Attaches to an existing
|
|
676
|
+
element and never changes it: the menu lives in the top layer, anchored to a zero-sized element
|
|
677
|
+
parked at the pointer, so it flips near a viewport edge like every other floating panel.
|
|
678
|
+
|
|
679
|
+
- **Constructor** — `new ContextMenu(target, options)` where `target` is an element or selector.
|
|
680
|
+
Unlike most components the target is not enhanced or replaced; `destroy()` removes only the
|
|
681
|
+
anchor the menu created.
|
|
682
|
+
- **Options** — `items: []` — the same `{label, icon, value, disabled, danger, onselect}` shape
|
|
683
|
+
MenuButton takes, with `'-'` for a separator, **or a function** `(context) => items` called on
|
|
684
|
+
every opening so a row menu can disable the actions that row does not allow; returning an empty
|
|
685
|
+
array cancels the opening and leaves the platform menu alone. `selector: null` restricts the
|
|
686
|
+
menu to matching descendants and reports the matched element as the context — a table passes
|
|
687
|
+
`'tbody tr'` here and needs one instance, not one per row.
|
|
688
|
+
- **Methods** — `setItems(items)`, `openAt(x, y, context?)` (viewport coordinates),
|
|
689
|
+
`openAtElement(element)`, `close()`, `isOpen()`, `getContext()`.
|
|
690
|
+
- **Events** — `select {value, item, context}`, `open {context}`, `close`. Subscribe with
|
|
691
|
+
`menu.on('select', …)` or the `onselect` option: the component's root is the anchor on
|
|
692
|
+
`<body>`, so the bubbling `zx-select` DOM event is dispatched there and not through the
|
|
693
|
+
region the menu belongs to — delegation on the target will not see it.
|
|
694
|
+
- **Keyboard** — the Menu key and Shift+F10 open the menu at the focused element, which is the
|
|
695
|
+
half most pointer-only context menus miss. Arrows move between items, typing jumps to one by its
|
|
696
|
+
first letters, Enter and Space activate, Escape and Tab close — and closing returns focus to
|
|
697
|
+
wherever it came from.
|
|
698
|
+
<!-- /doc -->
|
|
699
|
+
|
|
700
|
+
<!-- doc:tooltip -->
|
|
701
|
+
### Tooltip
|
|
702
|
+
|
|
703
|
+
A description bubble anchored to another element. The panel is a manual popover positioned by the kernel's `position()`, so it escapes clipping and stacking contexts and flips near a viewport edge; it carries `role="tooltip"` and a generated id, and the anchor gets `aria-describedby` pointing at it only while it is open — closing puts the anchor's original value back, including no value at all. The bubble is never interactive: `pointer-events: none` keeps it from stealing the hover it depends on, and nothing inside it can take focus. Hover opens it after `delay`; keyboard focus opens it immediately, because a keyboard user cannot hover a moment longer; a touch pointer never opens it, since a bubble with no "move away" gesture would simply stick. Only one is ever visible at a time, which falls out of the dismissal rules rather than a registry.
|
|
704
|
+
|
|
705
|
+
- **Options** — `content` (string, Node, or a function re-evaluated on every open), `placement` (`'top'` default, plus `bottom`, `top-start`, `top-end`, `bottom-start`, `bottom-end`), `offset` (6), `delay` (400), `hideDelay` (80), `maxWidth` (260; a number is pixels, a string is any CSS length), `trigger` (`'both'` default, or `'hover'`, `'focus'`, `'manual'` — `manual` installs no listeners at all and leaves everything to the methods), `disabled` (false).
|
|
706
|
+
- **Methods** — `show()`, `hide()`, `toggle()`, `isOpen()`, `setContent(content)`, `enable()`, `disable()`, `isDisabled()`, `getAnchor()`, `destroy()`. Convenience factories: `tooltip(anchor, contentOrOptions)` takes either a content value or a full option object, and `describe(anchor, text)` is the one-liner replacement for a `title` attribute. Both return the instance.
|
|
707
|
+
- **Events** — `open`, `close`. Both fire only on a real transition, so a `show()` on an open tooltip is silent.
|
|
708
|
+
- **Keyboard** — Focusing the anchor opens the tooltip at once; moving focus away closes it. `Escape` closes it without moving focus, satisfying WCAG 1.4.13. A `pointerdown` anywhere closes it, and focus that arrives with a click does not re-open it.
|
|
709
|
+
<!-- /doc -->
|
|
710
|
+
|
|
711
|
+
<!-- doc:split-view -->
|
|
712
|
+
### SplitView
|
|
713
|
+
|
|
714
|
+
Two panes with a divider the user owns. The layout is a three-track CSS grid driven by one custom property, `--zx-split-size`, so a drag writes a single style value per frame and the browser does the rest. That property holds whatever was asked for: `38%` stays a percentage and keeps tracking the container until someone drags, at which point it becomes pixels and stays put. The divider follows the WAI-ARIA window splitter pattern — a focusable `separator` with live pixel `aria-valuenow`/`aria-valuemin`/`aria-valuemax` — and a `ResizeObserver` re-clamps the size when the container shrinks, bailing on a container that measures zero (a hidden tab, a detached node) rather than flattening a good size to nothing. A split view given an existing element with children adopts the first two as its panes and hands them back on `destroy()`.
|
|
715
|
+
|
|
716
|
+
- **Options** — `orientation` (`'horizontal'` default: panes side by side behind a vertical divider; `'vertical'` stacks them), `start` / `end` (Node, Component, string, or null), `size` (`'38%'`; a number is pixels, a string is any CSS length), `min` (160), `max` (null — "whatever the container allows"), `collapsible` (`false`, `'start'`, or `'end'`), `collapsed` (false, true, or a pane name), `snap` (0 — a radius in pixels within which a settled drag lands exactly on the initial size, the minimum, or the maximum), `storageKey` (null; storage failures degrade to memory rather than throwing), `disabled` (false), `label` (`'Resize panes'`).
|
|
717
|
+
- **Methods** — `setSize(value)`, `getSize()` (pixels), `getRatio()`, `setStart(content)`, `setEnd(content)`, `collapse(which)`, `expand()`, `isCollapsed()` (returns the pane name or false), `enable()`, `disable()`, `isDisabled()`, `destroy()`. The setters are programmatic and emit nothing.
|
|
718
|
+
- **Events** — `resize` `{size, ratio}`, throttled to at most once per animation frame during a drag; `resizeend` `{size, ratio}` once an interaction settles (a drag ends, a key moves the divider, a double-click resets it); `collapse` `{pane}`; `expand`. A press that never moved is a click, not a resize, and reports nothing.
|
|
719
|
+
- **Keyboard** — With the divider focused: `←` / `→` (horizontal) or `↑` / `↓` (vertical) move it 16px, `Shift` with an arrow moves 64px, `Home` goes to the minimum, `End` to the maximum, and `Enter` or `Space` folds a collapsible pane away and brings it back. Any key that moves the divider expands a collapsed pane first. Double-clicking the divider resets it to the initial `size`.
|
|
720
|
+
<!-- /doc -->
|
|
721
|
+
|
|
722
|
+
<!-- doc:table -->
|
|
723
|
+
### Table
|
|
724
|
+
|
|
725
|
+
Sortable, selectable, sticky-header data table — one component covering both the legacy
|
|
726
|
+
`gx.ui.Table` and `SimpleTable`. `fr` widths fill the container while px/auto widths scroll
|
|
727
|
+
horizontally, and multi-select adds a tri-state header checkbox plus Shift+click range select.
|
|
728
|
+
|
|
729
|
+
- **Options** — `columns: [{ id, label, sortable?, width ('120px'|'2fr'|'auto'), align,
|
|
730
|
+
render?: (row,i)=>Node|string, sortValue?, headerTitle? }]`, `data`, `rowId: 'ID'`,
|
|
731
|
+
`sort: { id, dir }`, `sortMode: 'local'|'server'`, `selectable: false|'single'|'multi'`,
|
|
732
|
+
`stickyHeader: true`, `height`, `emptyText`, `rowClass`, `zebra: true`.
|
|
733
|
+
- **Methods** — `setData()`, `addData()`, `updateRow(id,row)`, `removeRow(id)`, `getRow(id)`,
|
|
734
|
+
`getData()`, `empty()`, `setSort(id,dir)`, `getSelection()`, `setSelection(ids)`,
|
|
735
|
+
`clearSelection()`, `setLoading(bool)` (busy/skeleton state — dims rows, shows an indeterminate
|
|
736
|
+
top bar, sets `aria-busy`, and is cleared by the next `setData`).
|
|
737
|
+
- **Events** — `rowclick {row, id, index, event}`, `rowdblclick`, `sort {id, dir}`,
|
|
738
|
+
`selectionchange {rows, ids}`, `datachange {rows}`.
|
|
739
|
+
- **Editing** — `editMode: false|'cell'|'row'` (default `false`, and completely inert until set).
|
|
740
|
+
`'cell'` edits one cell, `'row'` opens every editable cell of the row and commits them as a unit.
|
|
741
|
+
Editors are sized into the cell without changing row height, and an editing cell carries
|
|
742
|
+
`data-editing="true"` (`data-invalid="true"` plus an `aria-live` message when a validator rejects).
|
|
743
|
+
- **Editable columns** — extra column keys: `editable: false|true|'text'|'number'|'select'|'date'|
|
|
744
|
+
'checkbox'|'textarea'|((row)=>boolean)` (`true` means `'text'`; a function is evaluated per row, so
|
|
745
|
+
individual rows stay read-only), `options` for `'select'` (`[{value,label}]`, a `{value: label}` map
|
|
746
|
+
whose keys are always strings, or `(row)=>items`), `editorProps` (forwarded to the underlying
|
|
747
|
+
NumberField/Select/Datebox/Toggle or to the generated `<input>`/`<textarea>`),
|
|
748
|
+
`editor: (row, api)=>Node` for a fully custom editor (`api` is `{value, commit(value), cancel(),
|
|
749
|
+
row, column}`; it wins over `editable`, but a per-row `editable` function returning `false` still
|
|
750
|
+
marks the cell read-only), `parse: (raw,row)=>value` and `format: (value,row)=>string` (`format`
|
|
751
|
+
seeds the text of a `'text'`/`'textarea'` editor, `parse` converts every editor's raw output back),
|
|
752
|
+
and `validate: (value,row)=>true|string` (a returned string marks the cell invalid, shows the
|
|
753
|
+
message, and refuses the commit).
|
|
754
|
+
- **Editing methods** — `startEdit(rowId, columnId)`, `commitEdit()`, `cancelEdit()`, `isEditing()`,
|
|
755
|
+
`getEditing()` → `{id, columnId}|null`.
|
|
756
|
+
- **Editing events** — `editstart {row, id, column, columnId, value}`;
|
|
757
|
+
`editcommit {row, id, column, columnId, value, previous, changes}` — **cancelable**:
|
|
758
|
+
`preventDefault()` on the component event (`table.on('editcommit', …)` or the `oneditcommit` option,
|
|
759
|
+
not the mirrored `zx-editcommit` DOM event) keeps the editor open so a server round-trip can reject
|
|
760
|
+
the change (`changes` is the `{columnId: value}` map and holds every changed cell in row mode);
|
|
761
|
+
`editcancel {row, id, column, columnId}`;
|
|
762
|
+
`editinvalid {row, id, column, columnId, value, message}`. A committed change is written
|
|
763
|
+
back through `updateRow()`, so `datachange` fires exactly as for a programmatic row update — a
|
|
764
|
+
commit with no changes emits `editcommit` with an empty `changes` map and leaves the data alone.
|
|
765
|
+
Sorting, filtering, or `setData()` while editing cancels the edit cleanly and emits `editcancel`.
|
|
766
|
+
- **Editing keyboard** — Enter or F2 on a focused editable cell starts editing (editable cells share
|
|
767
|
+
one roving tab stop), double-click starts editing without emitting `rowdblclick`, Enter commits and
|
|
768
|
+
returns focus to the cell (Shift+Enter stays a newline in a `'textarea'`, Space toggles a
|
|
769
|
+
`'checkbox'`), Escape cancels and returns focus to the cell, Tab commits and moves to the next
|
|
770
|
+
editable cell (Shift+Tab the previous), wrapping across rows, and tabbing past the last editable
|
|
771
|
+
cell commits and leaves the table normally. Clicking outside the editor commits rather than
|
|
772
|
+
discarding what was typed.
|
|
773
|
+
<!-- /doc -->
|
|
774
|
+
|
|
775
|
+
<!-- doc:data-filter -->
|
|
776
|
+
### DataFilter
|
|
777
|
+
|
|
778
|
+
Declarative client-side filter bar producing a filtered array, commonly wired to a Table with
|
|
779
|
+
`onfilter: (e) => table.setData(e.detail.rows)`.
|
|
780
|
+
|
|
781
|
+
- **Options** — `filters: [{ type: 'select'|'text'|'custom', id, label, field(s)|get, options?,
|
|
782
|
+
predicate?, emptyLabel?, placeholder? }]`, `data`, `autoApply: true`, `clearLabel`.
|
|
783
|
+
- **Methods** — `setData()`, `apply()` → rows, `clear()`, `getState()`, `setState()`,
|
|
784
|
+
`addFilter()`.
|
|
785
|
+
- **Events** — `filter {rows, state}`.
|
|
786
|
+
<!-- /doc -->
|
|
787
|
+
|
|
788
|
+
<!-- doc:tree -->
|
|
789
|
+
### TreeView
|
|
790
|
+
|
|
791
|
+
Hierarchical tree implementing the APG **tree** pattern. Rows render as a flat list carrying
|
|
792
|
+
`aria-level`/`aria-setsize`/`aria-posinset` (the flattened variant the APG allows) rather than
|
|
793
|
+
nested `role="group"` containers. The component clones `items`, so lazily loaded children never
|
|
794
|
+
mutate the caller's data.
|
|
795
|
+
|
|
796
|
+
- **Options** — `items` (nested), `valueKey: 'ID'`, `labelKey: 'name'`, `childrenKey: 'children'`,
|
|
797
|
+
`expanded: []`, `selection: 'single'|'multi'|false`, `selected: []`, `checkboxes: false`
|
|
798
|
+
(tri-state), `checked: []`, `icons: true`, `renderLabel`, `load: (node)=>Promise<children>`
|
|
799
|
+
(lazy children; mark a branch with `hasChildren: true`), `filter`, `emptyText`, `height`.
|
|
800
|
+
- **Methods** — `setItems()`, `getItems()`, `getNode(id)`, `getPath(id)`, `setFilter(query)` (keeps
|
|
801
|
+
matching nodes' ancestors and opens the surviving branches),
|
|
802
|
+
`expand(id)`/`collapse(id)`/`toggle(id)` (async — they await the loader),
|
|
803
|
+
`expandAll()`/`collapseAll()`, `reveal(id)`, `select(id, {additive})`,
|
|
804
|
+
`getSelection()`/`setSelection()`/`clearSelection()`, `check(id, checked)`,
|
|
805
|
+
`getChecked({leavesOnly})`, `setChecked()`, `focusNode(id)`.
|
|
806
|
+
- **Events** — `select {node, id, ids}`, `expand`, `collapse`, `activate {node, id}` (Enter or
|
|
807
|
+
double-click), `check {ids, node}`.
|
|
808
|
+
- **Keyboard** — ↑/↓ move, → expands then descends, ← collapses then ascends, Home/End, Enter
|
|
809
|
+
activates, Space selects or toggles the checkbox, `*` expands every sibling, typing jumps to the
|
|
810
|
+
next match.
|
|
811
|
+
<!-- /doc -->
|
|
812
|
+
|
|
813
|
+
<!-- doc:finder -->
|
|
814
|
+
### Finder
|
|
815
|
+
|
|
816
|
+
Miller-columns hierarchy browser: each column lists the children of the row selected in the column
|
|
817
|
+
to its left, like the macOS Finder's column view. The columns are linked listboxes sharing **one**
|
|
818
|
+
tab stop. The constructor applies `path` silently, so read `getNodes()` for the first paint of any
|
|
819
|
+
breadcrumb.
|
|
820
|
+
|
|
821
|
+
- **Options** — `items` (the same nested shape as `TreeView`), `valueKey`, `labelKey`,
|
|
822
|
+
`childrenKey`, `path: []` (initial selection, root first), `load: (node)=>Promise<children>`,
|
|
823
|
+
`preview: (node)=>Node|null` (renders a pane after the last column for a selected leaf),
|
|
824
|
+
`rootLabel`, `columnWidth: 220`, `height: 320`, `icons: true`, `renderItem`, `emptyText`.
|
|
825
|
+
- **Methods** — `getPath()`, `getNodes()`, `getSelection()`, `setPath(ids, {silent, focus})`
|
|
826
|
+
(async — loads every branch along the way), `reveal(id)`, `setItems()`, `focus()`.
|
|
827
|
+
- **Events** — `change {path, nodes, node}`, `activate {node, id}`.
|
|
828
|
+
- **Keyboard** — ↑/↓ move within the active column, → steps into the selected branch and lands on
|
|
829
|
+
its first row, ← returns to the parent column and truncates the path, Home/End, Enter opens a
|
|
830
|
+
leaf, typing jumps to the next match.
|
|
831
|
+
<!-- /doc -->
|
|
832
|
+
|
|
833
|
+
<!-- doc:form -->
|
|
834
|
+
### Form / Fieldset / Field
|
|
835
|
+
|
|
836
|
+
#### Form
|
|
837
|
+
|
|
838
|
+
- **Options** — `fieldsets: []`, `actions: [button descriptors]`, `novalidate: true`.
|
|
839
|
+
- **Methods** (proxied across the fieldsets) — `getValues()`, `setValues()`, `getField()`,
|
|
840
|
+
`setValue()`, `getValue()`, `reset()`, `setHighlights()`, `clearHighlights()`, `addFieldset()`,
|
|
841
|
+
`setActions()`, `submit()`.
|
|
842
|
+
- **Events** — `submit {values}` (preventable; required/int/float validation runs first),
|
|
843
|
+
`invalid {errors}`, `change {id, value}`.
|
|
844
|
+
|
|
845
|
+
#### Fieldset
|
|
846
|
+
|
|
847
|
+
- **Options** — `title`, `columns: 1|2|3`, `fields: { id: Field-options }`.
|
|
848
|
+
- **Methods** — `addField(id, opts)`, `getField`/`hasField`/`getFields`,
|
|
849
|
+
`getValues`/`setValues`/`getValue`/`setValue`, `reset`, `clear`, `focus(id)`,
|
|
850
|
+
`setHighlights`/`clearHighlights`.
|
|
851
|
+
|
|
852
|
+
#### Field
|
|
853
|
+
|
|
854
|
+
- **Options** — `id`, `type: 'text'`, `label`, `description`, `value`, `placeholder`, `required`,
|
|
855
|
+
`disabled`, `options`, `layout: 'stack'|'inline'`, `props: {}`.
|
|
856
|
+
- **Built-in types** — `text`, `password`, `textarea`, `checkbox`, `int`, `float`, `select`,
|
|
857
|
+
`optionlist`, `hidden`, `html`.
|
|
858
|
+
- **Methods** — `getValue`/`setValue`, `focus`, `reset`, `setDisabled`,
|
|
859
|
+
`setHighlight(msg, kind)`, `clearHighlight`, `getInput`, `own(child)`, plus the statics
|
|
860
|
+
`Field.register(type, adapter)` and `Field.has(type)`.
|
|
861
|
+
- **Events** — `change {value}`, `invalid {message}`.
|
|
862
|
+
<!-- /doc -->
|
|
863
|
+
|
|
864
|
+
<!-- doc:form-widgets -->
|
|
865
|
+
### Field widget types
|
|
866
|
+
|
|
867
|
+
Beyond the built-in field types, these widget types wrap whole components, used as
|
|
868
|
+
`{ type, label, props: {…component options…} }`. They are registered by
|
|
869
|
+
`registerFieldAdapters()`, which `src/index.js` calls by default.
|
|
870
|
+
|
|
871
|
+
- `zxselect` (Select), `checklist` (Checklist), `tagpicker` (TagPicker), `number` (NumberField),
|
|
872
|
+
`rating` (Rating).
|
|
873
|
+
- `date`/`month`/`datetime` (Datebox/MonthPicker — pass a `Date` value), `time` (Timebox).
|
|
874
|
+
- `valuelist` (ValueList), `multivalueeditor` (MultiValueEditor), `upload` (FieldUpload),
|
|
875
|
+
`toggle` (Toggle).
|
|
876
|
+
<!-- /doc -->
|
|
877
|
+
|
|
878
|
+
<!-- doc:value-list -->
|
|
879
|
+
### ValueList
|
|
880
|
+
|
|
881
|
+
Tag/chip input. Enter adds a value, Backspace on an empty input removes the last one, and dragging
|
|
882
|
+
or Ctrl+Arrow reorders.
|
|
883
|
+
|
|
884
|
+
- **Options** — `values: []`, `placeholder`, `deletable: true`, `sortable: true`, `unique: true`,
|
|
885
|
+
`validate: (str)=>bool|string`.
|
|
886
|
+
- **Methods** — `getValues()`, `setValues()`, `addValue()`, `removeValue()`, `focus()`,
|
|
887
|
+
`enable()`/`disable()`.
|
|
888
|
+
- **Events** — `change {values}`, `add {value}`, `remove {value}`.
|
|
889
|
+
<!-- /doc -->
|
|
890
|
+
|
|
891
|
+
<!-- doc:multi-value-editor -->
|
|
892
|
+
### MultiValueEditor
|
|
893
|
+
|
|
894
|
+
Ordered value editor with explicit rows (add / remove / move).
|
|
895
|
+
|
|
896
|
+
- **Options** — `values`, `options` (allowed values, which turns the rows into selects),
|
|
897
|
+
`addLabel`.
|
|
898
|
+
- **Methods** — `getValues()`, `setValues()`.
|
|
899
|
+
- **Events** — `change {values}`.
|
|
900
|
+
<!-- /doc -->
|
|
901
|
+
|
|
902
|
+
<!-- doc:field-upload -->
|
|
903
|
+
### FieldUpload
|
|
904
|
+
|
|
905
|
+
Click/drop file upload with progress. The drop zone is keyboard-accessible and progress comes from
|
|
906
|
+
`XMLHttpRequest`.
|
|
907
|
+
|
|
908
|
+
- **Options** — `url`, `paramName: 'upload'`, `params`, `headers`, `multiple: false`, `accept`,
|
|
909
|
+
`maxSize`, `autoUpload: true`, `preview: true`, `http`.
|
|
910
|
+
- **Methods** — `upload(files?)`, `abort()`, `clear()`, `setDisabled()`.
|
|
911
|
+
- **Events** — `select {files}`, `progress {percent}`, `success {response}`, `error {error}`,
|
|
912
|
+
`abort`.
|
|
913
|
+
<!-- /doc -->
|
|
914
|
+
|
|
915
|
+
<!-- doc:tag-picker -->
|
|
916
|
+
### TagPicker
|
|
917
|
+
|
|
918
|
+
Multi-select combobox that renders its selection as removable tags inside the control (APG combobox
|
|
919
|
+
plus multi-selectable listbox). Use it when the catalogue is too large to show in full: `Checklist`
|
|
920
|
+
shows a fixed set, and `ValueList` takes free text with no catalogue. Unknown IDs passed to
|
|
921
|
+
`setValues` still render as tags, so a stored selection survives a catalogue that has not loaded
|
|
922
|
+
yet. Field type: `tagpicker`.
|
|
923
|
+
|
|
924
|
+
- **Options** — `items`, `values: []`, `valueKey: 'ID'`, `labelKey: 'name'`, `searchKeys`,
|
|
925
|
+
`filter: 'local'|(query)=>Promise<items>`, `minQuery: 0`, `debounce: 200`, `allowCreate: false`,
|
|
926
|
+
`max` (maximum tags; further options become `aria-disabled`), `closeOnSelect: false`,
|
|
927
|
+
`placeholder`, `listHeight: 260`, `disabled`, `readonly`, `renderItem`, `renderTag`.
|
|
928
|
+
- **Methods** — `getValues()`, `getItems()`, `setValues(values, {silent})`, `addValue()`,
|
|
929
|
+
`removeValue()`, `clear()`, `reset()`, `setItems()`, `isFull()`, `open()`/`close()`, `focus()`,
|
|
930
|
+
`setReadonly()`, `enable()`/`disable()`.
|
|
931
|
+
- **Events** — `change {values, items}`, `add {value, item}`, `remove {value, item}`,
|
|
932
|
+
`create {value, item}`, `query {query}`, `open`, `close`.
|
|
933
|
+
- **Keyboard** — ↓ opens and moves, ↑ moves, Enter toggles the active option (or creates one from
|
|
934
|
+
the query), Escape closes, Backspace on an empty input removes the last tag, Home/End jump
|
|
935
|
+
within the list.
|
|
936
|
+
<!-- /doc -->
|
|
937
|
+
|
|
938
|
+
<!-- doc:permission -->
|
|
939
|
+
### Permission
|
|
940
|
+
|
|
941
|
+
Private / public / group record-permission selector.
|
|
942
|
+
|
|
943
|
+
- **Options** — `value: true|false|groupId`, `groups: []`, `groupsValueKey: 'ID'`,
|
|
944
|
+
`groupsLabelKey: 'name'`.
|
|
945
|
+
- **Methods** — `get()` → `'private'|'public'|groupId`, `set(value)`.
|
|
946
|
+
- **Events** — `change {value}`.
|
|
947
|
+
<!-- /doc -->
|
|
948
|
+
|
|
949
|
+
<!-- doc:groupbox -->
|
|
950
|
+
### Groupbox
|
|
951
|
+
|
|
952
|
+
Collapsible titled section built on the native `<details>` element.
|
|
953
|
+
|
|
954
|
+
- **Options** — `title`, `open: true`.
|
|
955
|
+
- **Methods** — `open()`, `close()`, `toggle()`, `isOpen()`, `setTitle()`, `setContent()`.
|
|
956
|
+
- **Events** — `open`, `close`.
|
|
957
|
+
<!-- /doc -->
|
|
958
|
+
|
|
959
|
+
<!-- doc:panel -->
|
|
960
|
+
### Panel / MasterPanel
|
|
961
|
+
|
|
962
|
+
#### Panel
|
|
963
|
+
|
|
964
|
+
Framed, optionally collapsible section. Its header is a row rather than one big button: the chevron
|
|
965
|
+
and title form the collapse control and `buttons` sit at the trailing edge beside it, so clicking
|
|
966
|
+
an action never collapses the panel. The footer bar appears as soon as it has `footer` content,
|
|
967
|
+
`footerButtons`, or both, and its buttons are right-aligned.
|
|
968
|
+
|
|
969
|
+
- **Options** — `title`, `content`, `open: true`, `collapsible: true`, `buttons: []` (elements or
|
|
970
|
+
`button()` descriptors, right-aligned in the header), `footer`, `footerButtons: []`
|
|
971
|
+
(right-aligned in the footer).
|
|
972
|
+
- **Methods** — `setTitle()`, `setContent()`, `setButtons()`, `setFooter()`, `setFooterButtons()`,
|
|
973
|
+
`open()`/`close()`/`toggle()`, `isOpen()`.
|
|
974
|
+
- **Events** — `open`, `close`.
|
|
975
|
+
|
|
976
|
+
#### MasterPanel
|
|
977
|
+
|
|
978
|
+
Full-height application panel with a fixed header (title plus action buttons) and an optional
|
|
979
|
+
fixed footer, while the body scrolls.
|
|
980
|
+
|
|
981
|
+
- **Options** — `title`, `content`, `buttons: []`,
|
|
982
|
+
`module: <ZeyOS module name → accent token>`, `footer`.
|
|
983
|
+
- **Methods** — `setTitle()`, `setContent()`, `setButtons()`, `setFooter()`.
|
|
984
|
+
<!-- /doc -->
|
|
985
|
+
|
|
986
|
+
<!-- doc:layout -->
|
|
987
|
+
### stack() / grid() / aspect()
|
|
988
|
+
|
|
989
|
+
The small layout pieces that sit under Panel, SplitView, and MasterPanel: consistent spacing, a
|
|
990
|
+
grid that reflows on its own width, and a fixed-ratio box. Zx ships no page grid on purpose — an
|
|
991
|
+
ERP screen is a shell of panels, and those components already own that layer. All three are
|
|
992
|
+
factories returning elements, and all three are plain classes that work just as well in static
|
|
993
|
+
markup with no JavaScript.
|
|
994
|
+
|
|
995
|
+
- **Factories** — `stack(options, ...children)`, `grid(options, ...children)`,
|
|
996
|
+
`aspect(options, ...children)`; the classes are `.zx-stack`, `.zx-grid`, `.zx-aspect`.
|
|
997
|
+
- **stack options** — `direction: 'column'|'row'`, `gap: 4` (1–8 picks the matching `--zx-space-*`
|
|
998
|
+
step; any other value is used as a CSS length), `align: 'start'|'center'|'end'|'stretch'|
|
|
999
|
+
'baseline'`, `justify: 'start'|'center'|'end'|'between'|'around'`, `wrap: false`,
|
|
1000
|
+
`inline: false`, `class: ''`.
|
|
1001
|
+
- **grid options** — `columns: null` (the count at full width; null fills each row with as many
|
|
1002
|
+
`min`-wide tracks as fit), `min: '16rem'` (a number is read as pixels), `gap: 4`,
|
|
1003
|
+
`align: 'start'|'center'|'end'|'stretch'`, `class: ''`.
|
|
1004
|
+
- **aspect options** — `ratio: '16 / 9'`, `fit: 'cover'|'contain'|'fill'`, `class: ''`.
|
|
1005
|
+
- **Behaviour** — the grid reflows intrinsically: each track asks for its share of the row but
|
|
1006
|
+
never goes below `min`, so `auto-fit` drops one column at a time as the container narrows. No
|
|
1007
|
+
media query is involved, which is why a grid behaves the same in a full-width page, a split
|
|
1008
|
+
pane, and a modal — the usual reason a "responsive" grid still breaks inside a narrow panel.
|
|
1009
|
+
- **Breakpoints** — `breakpoints` (`sm: 480`, `md: 768`, `lg: 1024`, `xl: 1280`),
|
|
1010
|
+
`breakpointOf(width)` → `'xs'|'sm'|'md'|'lg'|'xl'`, `matchBreakpoint(name, width)` (the script
|
|
1011
|
+
equivalent of a `min-width` query), and `onBreakpoint(handler, {target})` → `{current, destroy}`,
|
|
1012
|
+
which fires immediately and then on every crossing. `target` defaults to the window but takes an
|
|
1013
|
+
**element**, watched with a `ResizeObserver` — inside a split pane the viewport width says
|
|
1014
|
+
nothing about the space actually available. The scale lives in JavaScript rather than as tokens
|
|
1015
|
+
because a CSS custom property cannot be used inside a media query.
|
|
1016
|
+
<!-- /doc -->
|
|
1017
|
+
|
|
1018
|
+
<!-- doc:tabbox -->
|
|
1019
|
+
### Tabbox — APG tabs
|
|
1020
|
+
|
|
1021
|
+
Four appearances, all square-cornered. `divided` (the default) sets flat blocks on a muted track,
|
|
1022
|
+
split by hairline dividers, above a bordered panel, with the active block lifted onto the panel
|
|
1023
|
+
surface under an accent rule. `bracket` outlines folder tabs that fuse into that same panel, the
|
|
1024
|
+
active tab bridging the seam. `line` underlines the active tab across a full-width rule, for page-
|
|
1025
|
+
and section-level navigation. `segmented` renders a compact group on a muted fill with the active
|
|
1026
|
+
tab raised onto a control surface, so the row reads as one control inside a toolbar or card header.
|
|
1027
|
+
|
|
1028
|
+
`divided` and `bracket` draw the panel box themselves; `line` and `segmented` leave the panel bare
|
|
1029
|
+
for the surrounding layout to frame. The boxed variants read `--zx-tabbox-radius` (default `0px`)
|
|
1030
|
+
from the component root, so `el.style.setProperty('--zx-tabbox-radius', 'var(--zx-radius-lg)')`
|
|
1031
|
+
restores rounded corners without forking the stylesheet.
|
|
1032
|
+
|
|
1033
|
+
- **Options** — `tabs: [{ name, title, content, icon?, closable?, disabled? }]`, `active`,
|
|
1034
|
+
`variant: 'divided'|'bracket'|'line'|'segmented'`, `keepAlive: true`.
|
|
1035
|
+
- **Methods** — `addTab()`, `removeTab()`, `openTab(name)`, `getActive()`, `setTitle()`,
|
|
1036
|
+
`setBadge(name, text|null)`, `enableTab()`/`disableTab()`.
|
|
1037
|
+
- **Events** — `change {name, previous}` (preventable), `close {name}`.
|
|
1038
|
+
- **Keyboard** — ArrowLeft/Right and Home/End move focus, Enter/Space activate, Delete closes a
|
|
1039
|
+
closable tab.
|
|
1040
|
+
<!-- /doc -->
|
|
1041
|
+
|
|
1042
|
+
<!-- doc:stepper -->
|
|
1043
|
+
### Stepper
|
|
1044
|
+
|
|
1045
|
+
Linear progress through a multi-step flow: a numbered `<ol>` where each step is upcoming, active,
|
|
1046
|
+
complete, or errored. It promotes the markup the checkout and record wizards used to hand-roll.
|
|
1047
|
+
Advancing past a step marks it complete automatically, and only steps the `clickable` policy allows
|
|
1048
|
+
are rendered as real buttons — the rest are not focusable at all.
|
|
1049
|
+
|
|
1050
|
+
- **Options** — `steps: [{name, title, description?, optional?, disabled?}]`, `active` (defaults to
|
|
1051
|
+
the first enabled step), `orientation: 'horizontal'|'vertical'`, `clickable:
|
|
1052
|
+
'completed'|'all'|false` (default `'completed'`, so users can go back but not skip ahead),
|
|
1053
|
+
`showNumbers: true`, `counter: false` (adds a "Step 2 of 4" line).
|
|
1054
|
+
- **Methods** — `setSteps()`, `goTo(name)`, `next()`, `previous()`, `getActive()`, `getIndex()`,
|
|
1055
|
+
`complete(name)`/`uncomplete(name)`, `setError(name, boolean)`, `setDisabled(name, boolean)`,
|
|
1056
|
+
`getState()` → `{active, index, completed, errored}`, `destroy()`.
|
|
1057
|
+
- **Events** — `change {name, previous, index}`, **preventable** with `event.preventDefault()` so a
|
|
1058
|
+
step can refuse to be left until its form validates.
|
|
1059
|
+
- **Semantics** — the active step carries `aria-current="step"`; every step carries
|
|
1060
|
+
`data-state="upcoming"|"active"|"complete"|"error"`, and complete and errored steps swap the
|
|
1061
|
+
number for a `check` or `warning` glyph.
|
|
1062
|
+
<!-- /doc -->
|
|
1063
|
+
|
|
1064
|
+
<!-- doc:breadcrumb -->
|
|
1065
|
+
### Breadcrumb
|
|
1066
|
+
|
|
1067
|
+
The trail above a hierarchy view. It pairs directly with `Finder` and `TreeView`: feed
|
|
1068
|
+
`getNodes()` from a Finder's `change` event into `setItems()` and the two stay in step. When
|
|
1069
|
+
`maxVisible` is exceeded the middle collapses behind an ellipsis `MenuButton`, always keeping the
|
|
1070
|
+
first and last items.
|
|
1071
|
+
|
|
1072
|
+
- **Options** — `items: [{name, label, icon?, href?}]`, `maxVisible: 0` (0 shows all),
|
|
1073
|
+
`separator: 'chevron'|'slash'`, `rootIcon: null`.
|
|
1074
|
+
- **Methods** — `setItems()`, `getItems()`, `push(item)`, `pop()`, `truncateTo(name)`, `destroy()`.
|
|
1075
|
+
- **Events** — `select {name, item, index}`. An item with an `href` renders as a real `<a>` and its
|
|
1076
|
+
default is left alone, so ordinary navigation and middle-click still work; without one it renders
|
|
1077
|
+
as a `<button>` and only the event fires.
|
|
1078
|
+
- **Semantics** — `<nav aria-label="Breadcrumb"><ol>`; the last item is plain text carrying
|
|
1079
|
+
`aria-current="page"` and is deliberately not interactive; separators are `aria-hidden`.
|
|
1080
|
+
<!-- /doc -->
|
|
1081
|
+
|
|
1082
|
+
<!-- doc:pagination -->
|
|
1083
|
+
### Pagination
|
|
1084
|
+
|
|
1085
|
+
The pager for a server-backed list, shaped to drop straight onto `Table` with
|
|
1086
|
+
`sortMode: 'server'` or onto the `zeyosTable` binding — `getState()` hands back the `offset` and
|
|
1087
|
+
`pageSize` a query needs. `page` is 1-based and always clamped into `[1, pages]`; a `total` of 0 is
|
|
1088
|
+
treated as one empty page rather than zero pages.
|
|
1089
|
+
|
|
1090
|
+
- **Options** — `page: 1`, `pageSize: 25`, `total: 0`, `pageSizes: [25, 50, 100]`, `siblings: 1`,
|
|
1091
|
+
`boundaries: 1`, `showPageSize: true`, `showSummary: true` (renders "26–50 of 312"),
|
|
1092
|
+
`mode: 'pages'|'loadmore'`, `disabled: false`.
|
|
1093
|
+
- **Methods** — `setPage(n)`, `setTotal(n)`, `setPageSize(n)`, `setState({page, pageSize, total},
|
|
1094
|
+
{silent})`, `getState()` → `{page, pageSize, total, pages, offset}`, `enable()`/`disable()`,
|
|
1095
|
+
`destroy()`.
|
|
1096
|
+
- **Events** — `change {page, pageSize, offset, total, pages}`, fired for a page change, a
|
|
1097
|
+
page-size change (which resets to page 1), and Load more.
|
|
1098
|
+
- **Also exported** — `paginationRange({page, pages, siblings, boundaries})` → `Array<number|'…'>`,
|
|
1099
|
+
the pure page-window calculation, usable on its own.
|
|
1100
|
+
- **Semantics** — `<nav aria-label="Pagination">`; the current page carries `aria-current="page"`;
|
|
1101
|
+
previous/next are labelled icon buttons that go `disabled` at the ends; the ellipses are
|
|
1102
|
+
`aria-hidden` spans, not buttons.
|
|
1103
|
+
<!-- /doc -->
|
|
1104
|
+
|
|
1105
|
+
<!-- doc:navigation-bar -->
|
|
1106
|
+
### NavigationBar
|
|
1107
|
+
|
|
1108
|
+
Application navigation bar: brand, items, and right-aligned actions. Items overflow into a "More"
|
|
1109
|
+
menu on narrow widths.
|
|
1110
|
+
|
|
1111
|
+
- **Options** — `title`, `items: [{ name, title, badge? }]`, `active`, `actions: []`.
|
|
1112
|
+
- **Methods** — `setTitle()`, `setItems()`, `setActive()`, `setBadge()`, `setActions()`.
|
|
1113
|
+
- **Events** — `change {name}`.
|
|
1114
|
+
<!-- /doc -->
|
|
1115
|
+
|
|
1116
|
+
<!-- doc:kernel -->
|
|
1117
|
+
### Core helpers
|
|
1118
|
+
|
|
1119
|
+
- **Component** — the base class: `on`/`off`/`once`/`emit`, `listen`, `toElement`, `msg`,
|
|
1120
|
+
`destroy`, and the static `Component.from(el)`.
|
|
1121
|
+
- **DOM** — `h(tag, props, ...children)`, `h.raw(html)`, `htmlEscape`, `resolveElement`.
|
|
1122
|
+
- **Icons** — `icon(name, {size, label})` and `icons`; see the Icons section — bundled inline SVG
|
|
1123
|
+
by default, Font Awesome after an opt-in.
|
|
1124
|
+
- **Positioning** — `position(anchor, floating, {placement, offset, flip, matchWidth})` →
|
|
1125
|
+
`{update, destroy}`. It anchors a manual popover with CSS anchor positioning where the browser
|
|
1126
|
+
supports it and a scroll/resize-tracked fallback where it does not. Options are resolved once,
|
|
1127
|
+
so changing a placement means `destroy()` and a fresh `position()` call.
|
|
1128
|
+
- **i18n** — `setTranslator`, `setLanguage`, `getLanguage`, `translate`, `printf`.
|
|
1129
|
+
- **Dates** — `formatDate(d, fmt)`, `parseDate(s, fmt)` (tokens
|
|
1130
|
+
`%d %m %Y %y %H %M %S %a %A %b %B %s`), `clampDate`, `isSameDay`, `addDays`, `addMonths`,
|
|
1131
|
+
`getWeekStart`.
|
|
1132
|
+
- **Keyboard** — `focusTrap`, `rovingTabindex`, `typeahead`.
|
|
1133
|
+
- **Utils** — `debounce`, `uid`, `deepMerge`, `isElement`, `clamp`, `toArray`.
|
|
1134
|
+
<!-- /doc -->
|
|
1135
|
+
|
|
1136
|
+
<!-- doc:icons -->
|
|
1137
|
+
### Icons — bundled glyphs or Font Awesome
|
|
1138
|
+
|
|
1139
|
+
`icon(name, {size, label, class, style, family, fixedWidth})` returns an element: an inline `<svg>`
|
|
1140
|
+
from the bundled set, or an `<i>` carrying Font Awesome classes. Both get `class="zx-icon"`, so
|
|
1141
|
+
component CSS styles either. Zx renders the bundled glyphs by default and **never loads anything on
|
|
1142
|
+
its own** — Font Awesome is an opt-in an application makes.
|
|
1143
|
+
|
|
1144
|
+
```js
|
|
1145
|
+
import { loadFontAwesome, useFontAwesome, registerIcons } from '/assets/zx.esm.js';
|
|
1146
|
+
|
|
1147
|
+
await loadFontAwesome({ kit: 'ae8320b210', style: 'duotone' }); // injects the kit, switches over
|
|
1148
|
+
useFontAwesome(); // page already carries Font Awesome; no network
|
|
1149
|
+
registerIcons({ rocket: ['0 0 512 512', 'M…'] }); // extend the bundled inline set
|
|
1150
|
+
```
|
|
1151
|
+
|
|
1152
|
+
- `loadFontAwesome(kit | {kit, style, family, fixedWidth, activate})` — injects the kit script once
|
|
1153
|
+
per URL (a kit token expands to `https://kit.fontawesome.com/<token>.js`), adopts a kit the page
|
|
1154
|
+
already embeds, then switches the renderer. Rejects if the script fails; kits only load on the
|
|
1155
|
+
domains configured in the Font Awesome account.
|
|
1156
|
+
- `useFontAwesome(opts)` / `useBuiltinIcons()` / `configureIcons(opts)` / `getIconConfig()` —
|
|
1157
|
+
switch or read the renderer without touching the network.
|
|
1158
|
+
- `iconNames()` lists every inline glyph; `registerIcons(map)` adds `[viewBox, path]` entries.
|
|
1159
|
+
|
|
1160
|
+
**Names decide their own renderer.** A bare name (`'check'`) follows the active provider, and the
|
|
1161
|
+
built-in names are translated to their Font Awesome counterparts (`x` → `fa-xmark`, `search` →
|
|
1162
|
+
`fa-magnifying-glass`, `warning` → `fa-triangle-exclamation`, …), so component code never changes.
|
|
1163
|
+
Prefixes override: `'fa:user'`, `'fas:user'`/`'regular:user'`/`'duotone:user'`/`'thin:user'`,
|
|
1164
|
+
`'kit:zeyos-notes'` (custom kit icons), `'builtin:check'` (force the inline SVG). A literal class
|
|
1165
|
+
list — `'fa-sharp fa-solid fa-user'` — is used verbatim. Unknown *built-in* names throw
|
|
1166
|
+
`RangeError`; unknown names under Font Awesome are passed through to the kit.
|
|
1167
|
+
|
|
1168
|
+
Font Awesome elements are sized with `font-size` and occupy 1em; bundled SVGs use width/height
|
|
1169
|
+
attributes. Both honour `label` (`null` ⇒ `aria-hidden`, otherwise `role="img"` + `aria-label`).
|
|
1170
|
+
<!-- /doc -->
|
|
1171
|
+
|
|
1172
|
+
<!-- doc:helpers -->
|
|
1173
|
+
### Helper functions
|
|
1174
|
+
|
|
1175
|
+
Standalone exports with no DOM ownership and no lifecycle, importable individually from the same
|
|
1176
|
+
entry point as the components. See also the Kernel section for `Component`, `h`, `icon`, and
|
|
1177
|
+
`position` — the substrate the components themselves are built on.
|
|
1178
|
+
|
|
1179
|
+
- **Dates** — `formatDate(date, fmt)`, `parseDate(str, fmt)` (returns `null` when the string does
|
|
1180
|
+
not match, so it doubles as a validator), `clampDate(date, min, max)`, `isSameDay(a, b)`,
|
|
1181
|
+
`addDays(date, n)`, `addMonths(date, n)`, `getWeekStart(lang?)` → `0` (Sunday) or `1` (Monday).
|
|
1182
|
+
strftime-style tokens: `%d %m %Y %y %H %M %S %a %A %b %B %s`.
|
|
1183
|
+
- **Numbers, money and time** — `formatNumber(value, {locale, decimals, minDecimals, maxDecimals,
|
|
1184
|
+
group=true})`, `formatCurrency(value, currency, {locale, decimals})`, `formatPercent(value,
|
|
1185
|
+
{locale, decimals=0})` (takes a fraction, so `0.42` → `42%`), `formatFileSize(bytes, {locale,
|
|
1186
|
+
decimals=1, standard='iec'})` (`'iec'` → KiB/MiB/GiB in steps of 1024, `'si'` → kB/MB/GB in steps
|
|
1187
|
+
of 1000; `0` → `'0 B'`, trailing zeros dropped), `formatRelativeTime(date, {now=new Date(),
|
|
1188
|
+
locale, numeric='auto'})` (picks the largest sensible unit from second to year; accepts a `Date`,
|
|
1189
|
+
Unix seconds, or an ISO string). `locale` defaults to `getLanguage()` throughout, so
|
|
1190
|
+
`setLanguage()` drives all five. Nothing throws: `null`, `undefined` and `NaN` all format as
|
|
1191
|
+
`''`, an invalid currency code falls back to plain number formatting, and every `Intl` instance
|
|
1192
|
+
is cached because tables call these once per cell.
|
|
1193
|
+
- **Utilities** — `debounce(fn, ms)` (trailing edge, preserves `this`), `throttle(fn, ms,
|
|
1194
|
+
{leading=true, trailing=true})` (its rate-limiting counterpart: runs at most once per interval
|
|
1195
|
+
and replays the latest arguments on the trailing edge), `uid(prefix='zx')` (unique
|
|
1196
|
+
per page load, safe as a DOM id), `deepMerge(a, b)` (never mutates either input; replaces arrays
|
|
1197
|
+
rather than concatenating), `clamp(n, min, max)`, `toArray(value)` (null → `[]`, string → single
|
|
1198
|
+
entry, iterable/array-like → array), `isElement(value)` (cross-realm safe).
|
|
1199
|
+
- **Collections** — `groupBy(items, key)` → `Record<string, T[]>` ordered by first appearance;
|
|
1200
|
+
`sortBy(items, ...keys)` returns a **new** array and never mutates its input, taking a property
|
|
1201
|
+
name, a `'-name'` prefix or a `{key, dir}` object for descending, or an accessor function,
|
|
1202
|
+
comparing digit runs numerically and always sorting `null`, `undefined` and `NaN` last in either
|
|
1203
|
+
direction; `uniqueBy(items, key?)` keeps the first occurrence.
|
|
1204
|
+
- **Escaping** — `htmlEscape(str)`, and `h.raw(html)` as the only sanctioned `innerHTML` path, for
|
|
1205
|
+
component-generated markup only. `escapeRegExp(value)` makes user input safe as a literal
|
|
1206
|
+
pattern, and `highlightMatch(text, query, {className='zx-mark'})` → `DocumentFragment` wraps
|
|
1207
|
+
every case-insensitive occurrence in a `<mark>` built from text nodes, with no `innerHTML`
|
|
1208
|
+
involved — the pairing every filtering list wants.
|
|
1209
|
+
- **Storage** — `storage(namespace, {area='local'})` → `{get, set, remove, keys, clear}`, a
|
|
1210
|
+
namespaced JSON-encoded view of `localStorage` (or `sessionStorage` with `{area: 'session'}`).
|
|
1211
|
+
Keys are written as `zx:<namespace>:<key>`, so features share an origin without colliding and
|
|
1212
|
+
`clear()` empties only its own namespace; `keys()` reports namespace-relative names.
|
|
1213
|
+
`get(key, fallback)` returns the fallback both for a missing key and for unparseable JSON, and
|
|
1214
|
+
`set()` drops values JSON cannot represent rather than throwing. When the area is missing or
|
|
1215
|
+
refuses a write — private browsing, a full quota, cookies disabled — the view degrades silently
|
|
1216
|
+
to an in-memory map for the life of the page, so callers never need a `try`/`catch`.
|
|
1217
|
+
- **Export** — `toCsv(rows, columns, {delimiter=',', header=true, newline='\r\n'})` serialises to
|
|
1218
|
+
RFC 4180: fields containing the delimiter, a quote, CR or LF are quoted with inner quotes
|
|
1219
|
+
doubled, `Date`s become ISO strings, `null`/`undefined` become empty fields, and text starting
|
|
1220
|
+
with `=`, `+`, `-` or `@` is prefixed with a tab so spreadsheets treat it as text rather than a
|
|
1221
|
+
formula (numbers are exempt, so negative amounts survive). `columns` accepts plain id strings or
|
|
1222
|
+
`{id, label, value(row)}` descriptors. `downloadBlob(filename, data, {type})` saves through a
|
|
1223
|
+
temporary object URL, prepending a UTF-8 BOM for `text/csv` so Excel reads it as UTF-8;
|
|
1224
|
+
`copyToClipboard(text)` → `Promise<boolean>` uses the async Clipboard API and falls back to a
|
|
1225
|
+
hidden textarea, never rejecting.
|
|
1226
|
+
- **Translation** — `setTranslator(fn)`, `setLanguage(lang)`, `getLanguage()`,
|
|
1227
|
+
`translate(key, args)`, `printf(str, args)` (`%s` placeholders). Every component routes its
|
|
1228
|
+
built-in strings through `translate()`, so one translator covers the library, and
|
|
1229
|
+
`setLanguage()` also drives locale-dependent behaviour such as the first day of the week.
|
|
1230
|
+
- **Keyboard** — `focusTrap(container)` → `{activate, release}` confines Tab to a container;
|
|
1231
|
+
`rovingTabindex(container, itemSelector, {orientation, wrap})` → `{focusFirst, focusLast,
|
|
1232
|
+
destroy}` keeps one tab stop in a group; `typeahead(getItems, onMatch)` →
|
|
1233
|
+
`(event|string) => void` resolves buffered printable keystrokes to an item (500 ms buffer,
|
|
1234
|
+
repeated-letter cycling).
|
|
1235
|
+
<!-- /doc -->
|
|
1236
|
+
|
|
1237
|
+
<!-- doc:truncate -->
|
|
1238
|
+
### truncate()
|
|
1239
|
+
|
|
1240
|
+
Clamps text to a number of lines and gives the cut-off text back on hover. The tooltip half is
|
|
1241
|
+
what makes it more than a CSS class: a cell that silently drops the end of a value is a data-loss
|
|
1242
|
+
bug wearing a layout costume, while a tooltip that is always there is noise.
|
|
1243
|
+
|
|
1244
|
+
- **Signature** — `truncate(target, {lines, title})` → `{update, isTruncated, destroy}`.
|
|
1245
|
+
- **Options** — `lines: 1` (1–10; one line ends in an ellipsis, more clamp the block),
|
|
1246
|
+
`title: true` — sets a native `title` **only while the text is actually cut off**, re-measured
|
|
1247
|
+
through a `ResizeObserver` on every resize, and restores any previous title on `destroy()`.
|
|
1248
|
+
- **Controller** — `update()` re-measures and returns the current state, `isTruncated()` reports
|
|
1249
|
+
the last measurement, `destroy()` removes the class, the observer, and any title it set.
|
|
1250
|
+
- **Exported helper** — `isTruncated(element)` compares scroll and client sizes on both axes (a
|
|
1251
|
+
single line runs out of width, a clamped block runs out of height) with a one-pixel tolerance
|
|
1252
|
+
for the sub-pixel difference fractional zoom produces.
|
|
1253
|
+
- **Class** — `.zx-truncate` alone does the clamping from static markup with no JavaScript;
|
|
1254
|
+
`data-lines` plus `--zx-truncate-lines` select the multi-line form.
|
|
1255
|
+
<!-- /doc -->
|
|
1256
|
+
|
|
1257
|
+
<!-- doc:gx-compat -->
|
|
1258
|
+
### gx compatibility layer
|
|
1259
|
+
|
|
1260
|
+
Opt-in re-implementation of the legacy MooTools-era `gx` API on top of Zx, so existing ZeyOS
|
|
1261
|
+
screens keep running while their code is modernised file by file. It lives in its own bundle, is
|
|
1262
|
+
never imported by `src/index.js`, and adds nothing to an application that does not load it.
|
|
1263
|
+
|
|
1264
|
+
- **Loading** — classic: `<script src="/assets/zx-compat.global.js">` → `window.gx`; module:
|
|
1265
|
+
`import { gx } from '/assets/zx-compat.esm.js'` (or `src/compat/index.js` in-repo).
|
|
1266
|
+
`gx.compat.installGlobals()` additionally installs the legacy free functions `__()`, `_()`,
|
|
1267
|
+
`String.htmlSpecialChars`, and the element `store`/`retrieve` shim. `gx.install(host)` assigns
|
|
1268
|
+
the namespace without the optional globals.
|
|
1269
|
+
- **`gx.zeyos`** — Select/SelectFilter/SelectDyn/SelectPrio, Table, Tabbox, Panel, MasterPanel,
|
|
1270
|
+
Groupbox, Search, Datebox, DatePicker, MonthPicker, TimePicker, Timebox, Toggle, Msgbox, Popup,
|
|
1271
|
+
Dialog, Dropdown, Checklist, Permission, Client, Request, Factory.
|
|
1272
|
+
- **`gx.bootstrap`** — Form, Fieldset, Field, the Select variants, Checklist, Table, Tabbox,
|
|
1273
|
+
NavigationBar, MenuButton, Message, Popup/PopupAlert/PopupConfirm, CheckButton, DataFilter,
|
|
1274
|
+
ValueList, MultiValueEditor, FieldUpload, Timebox.
|
|
1275
|
+
- **`gx.util`** — `formatNum`, `formatTime`, `getMinutes`, `getNumber`, `printf`, `parseResult`,
|
|
1276
|
+
`Parse`, `isArray`/`isObject`/`isFunction`/`isString`/`isNumber`/`isElement`/`isNode`,
|
|
1277
|
+
`Console`.
|
|
1278
|
+
- **`gx.ui`** — Container, SimpleTable, Timebox, and the inert visual-effect stubs
|
|
1279
|
+
Blend/Collapse/Hud/Toggling/HGroup/Templates. **`gx.core`** — Settings. **`gx.compat`** —
|
|
1280
|
+
installGlobals, installElementStorage, parse, GxWrapper.
|
|
1281
|
+
- **Migrating off it** — legacy classes collapse onto fewer Zx components: all four Select
|
|
1282
|
+
variants → one `Select` with a `filter` option; `gx.ui.Table` + `SimpleTable` → one `Table`;
|
|
1283
|
+
`Msgbox`/`bootstrap.Message` → `Message`; `Popup`/`PopupAlert`/`PopupConfirm` → `Modal`/`Dialog`
|
|
1284
|
+
with Promise-returning statics; `gx.zeyos.Client`/`Request` → `@zeyos/client`. Anything that
|
|
1285
|
+
depended on MooTools prototype extensions is gone. `MIGRATION.md` has the full map and the
|
|
1286
|
+
deliberate behaviour changes; `website/compat.html` runs legacy snippets against Zx as a smoke
|
|
1287
|
+
test.
|
|
1288
|
+
<!-- /doc -->
|
|
1289
|
+
|
|
1290
|
+
<!-- doc:elements -->
|
|
1291
|
+
### Custom elements — `defineElements()`
|
|
1292
|
+
|
|
1293
|
+
Call `defineElements()` once to register `<zx-*>` light-DOM wrappers with attribute↔option
|
|
1294
|
+
reflection and ElementInternals form association, so they participate in a native `<form>` and its
|
|
1295
|
+
`FormData`. Events bubble as `zx-*` on the host element, and removing an element from the DOM
|
|
1296
|
+
destroys its component.
|
|
1297
|
+
|
|
1298
|
+
Registered elements: `<zx-toggle>`, `<zx-check-button>`, `<zx-select items='[…]' required>`,
|
|
1299
|
+
`<zx-checklist>`, `<zx-datebox>`, `<zx-timebox>`, `<zx-search>`, `<zx-groupbox>`, `<zx-tabbox>`,
|
|
1300
|
+
`<zx-table>`, `<zx-dialog>`.
|
|
1301
|
+
<!-- /doc -->
|
|
1302
|
+
|
|
1303
|
+
<!-- doc:tokens -->
|
|
1304
|
+
### Design tokens
|
|
1305
|
+
|
|
1306
|
+
Two tiers of CSS custom properties. Dark mode and density are pure token/attribute swaps
|
|
1307
|
+
(`[data-zx-theme="dark"]`, `[data-zx-density="compact"]`), and `tests/lint-tokens.js` forbids raw
|
|
1308
|
+
colour literals and tier-1 references in component CSS.
|
|
1309
|
+
|
|
1310
|
+
- **Tier 1** — the global palette in `styles/tokens/global.css`, never referenced by components:
|
|
1311
|
+
`--zx-gray-0…950`, `--zx-green-*`, `--zx-red/amber/blue-*`, `--zx-space-1…8`,
|
|
1312
|
+
`--zx-radius-sm/md/lg/full`, the type scale, shadows, and motion.
|
|
1313
|
+
- **Tier 2** — the semantic set, the only tokens components may use: `--zx-color-*`,
|
|
1314
|
+
`--zx-control-*`, `--zx-focus-ring`, `--zx-overlay-shadow`.
|
|
1315
|
+
<!-- /doc -->
|