@zeyos/zx 3.0.0 → 4.0.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 +51 -0
- package/DESIGN-SYSTEM.md +28 -68
- package/README.md +16 -37
- package/dist/types/components/app-sidebar/app-sidebar.d.ts +106 -32
- package/dist/types/components/card/card.d.ts +128 -0
- package/dist/types/components/chart/chart.d.ts +1 -0
- package/dist/types/components/launcher/launcher.d.ts +65 -6
- package/dist/types/components/select/select.d.ts +51 -0
- package/dist/types/components/table/table.d.ts +4 -3
- package/dist/types/core/dom.d.ts +8 -0
- package/dist/types/index.d.ts +2 -1
- package/dist/types/{components/app-rail → internal}/app-rail.d.ts +34 -19
- package/dist/types/zeyos/icons.d.ts +5 -0
- package/dist/types/zeyos/index.d.ts +2 -1
- package/dist/types/zeyos/launcher.d.ts +147 -0
- package/dist/types/zeyos/select.d.ts +39 -0
- package/dist/zx-zeyos.esm.js +1324 -46
- package/dist/zx-zeyos.esm.min.js +1 -1
- package/dist/zx.css +400 -12
- package/dist/zx.esm.js +1059 -189
- package/dist/zx.esm.min.js +3 -3
- package/dist/zx.global.js +1059 -189
- package/dist/zx.global.min.js +3 -3
- package/dist/zx.min.css +1 -1
- package/docs/api.json +258 -145
- package/docs/llms.md +93 -112
- package/docs/llms.txt +16 -15
- package/package.json +8 -17
- package/src/components/app-sidebar/app-sidebar.css +12 -0
- package/src/components/app-sidebar/app-sidebar.js +310 -110
- package/src/components/card/card.css +149 -0
- package/src/components/card/card.js +285 -0
- package/src/components/chart/chart.js +8 -3
- package/src/components/grid/grid.js +10 -3
- package/src/components/launcher/launcher.css +113 -3
- package/src/components/launcher/launcher.js +234 -26
- package/src/components/select/field-adapter.js +9 -4
- package/src/components/select/select.css +144 -10
- package/src/components/select/select.js +259 -27
- package/src/components/table/table.js +8 -7
- package/src/core/dom.js +20 -0
- package/src/index.js +2 -1
- package/src/{components/app-rail → internal}/app-rail.js +80 -30
- package/src/zeyos/icons.js +8 -3
- package/src/zeyos/index.js +7 -1
- package/src/zeyos/launcher.js +338 -0
- package/src/zeyos/modules.js +1 -0
- package/src/zeyos/select.js +115 -1
- package/styles/tokens/global.css +1 -1
- package/styles/tokens/modules.css +1 -0
- package/styles/zx.css +1 -0
- package/MIGRATION.md +0 -749
- package/dist/types/compat/base.d.ts +0 -156
- package/dist/types/compat/globals.d.ts +0 -20
- package/dist/types/compat/index.d.ts +0 -128
- package/dist/types/compat/map/basic.d.ts +0 -175
- package/dist/types/compat/map/data.d.ts +0 -103
- package/dist/types/compat/map/date-time.d.ts +0 -80
- package/dist/types/compat/map/factory.d.ts +0 -9
- package/dist/types/compat/map/form.d.ts +0 -98
- package/dist/types/compat/map/helpers.d.ts +0 -12
- package/dist/types/compat/map/http.d.ts +0 -36
- package/dist/types/compat/map/layout.d.ts +0 -106
- package/dist/types/compat/map/options.d.ts +0 -80
- package/dist/types/compat/map/select.d.ts +0 -95
- package/dist/types/compat/map/stubs.d.ts +0 -32
- package/dist/types/compat/map/utilities.d.ts +0 -37
- package/dist/types/compat-entry.d.ts +0 -5
- package/dist/zx-compat.esm.js +0 -17848
- package/dist/zx-compat.esm.min.js +0 -1
- package/dist/zx-compat.global.js +0 -29008
- package/dist/zx-compat.global.min.js +0 -3
- package/src/compat/base.js +0 -291
- package/src/compat/globals.js +0 -167
- package/src/compat/index.js +0 -84
- package/src/compat/map/basic.js +0 -464
- package/src/compat/map/data.js +0 -254
- package/src/compat/map/date-time.js +0 -177
- package/src/compat/map/factory.js +0 -45
- package/src/compat/map/form.js +0 -330
- package/src/compat/map/helpers.js +0 -46
- package/src/compat/map/http.js +0 -117
- package/src/compat/map/layout.js +0 -256
- package/src/compat/map/options.js +0 -267
- package/src/compat/map/select.js +0 -198
- package/src/compat/map/stubs.js +0 -26
- package/src/compat/map/utilities.js +0 -113
- package/src/compat-entry.js +0 -11
package/MIGRATION.md
DELETED
|
@@ -1,749 +0,0 @@
|
|
|
1
|
-
# Migrating from `gx` to Zx
|
|
2
|
-
|
|
3
|
-
Zx replaces the MooTools-era `gx` UI libraries with dependency-free ES2022 modules. The
|
|
4
|
-
compatibility bundle lets mapped legacy code keep running while classes are migrated one at a
|
|
5
|
-
time. Compatibility constructors warn once per legacy class so remaining call sites are visible.
|
|
6
|
-
|
|
7
|
-
Xenon is the ZeyOS design system; Zx is its browser implementation. A complete migration therefore
|
|
8
|
-
means more than swapping constructors: foundations, components, application patterns, and layout
|
|
9
|
-
contracts must move together while routing and business behavior stay authoritative.
|
|
10
|
-
|
|
11
|
-
## Introducing Xenon into ZeyOS Classic
|
|
12
|
-
|
|
13
|
-
The current ZeyOS Classic application is **not** a `gx.*` application. Its modules call the global
|
|
14
|
-
`UI` factories, use `PG` for routing and page lifecycle, and rely on an extended DOM API including a
|
|
15
|
-
CSS-like `__()` element builder. Do not call `gx.compat.installGlobals()` there: that opt-in helper
|
|
16
|
-
preserves an existing `__()` but installs unrelated `gx` globals and prototype compatibility that
|
|
17
|
-
the current application does not need.
|
|
18
|
-
|
|
19
|
-
The migration does not need a separate application bridge. Zx now exports the established builder
|
|
20
|
-
directly, including its exact compact tag and property conventions:
|
|
21
|
-
|
|
22
|
-
```js
|
|
23
|
-
const node = zx.__('button#save.primary', {
|
|
24
|
-
Daction: 'save',
|
|
25
|
-
onclick: () => save()
|
|
26
|
-
}, 'Save');
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
Zx does not install globals or alter DOM prototypes. The current runtime keeps owning its global
|
|
30
|
-
`__`, `_`, `Node._`, `Node.__`, `Node.$__`, and `Element.be` seams during the migration and delegates
|
|
31
|
-
their bodies to `zx.compactElement()`, `zx.appendContent()`, `zx.appendCompact()`,
|
|
32
|
-
`zx.appendCompactChild()`, and `zx.applyCompactProperties()`. Its `DOM.on`, `DOM.remEvt`, and
|
|
33
|
-
`DOM.fireEvt` facade likewise delegates to Zx's shared compact-event helpers, so registration,
|
|
34
|
-
removal, and synthetic firing always see the same handlers. Replace the implementations of
|
|
35
|
-
`UI.new*` component families progressively with Zx components; do not insert a second routing or
|
|
36
|
-
application abstraction between those components and `PG`.
|
|
37
|
-
|
|
38
|
-
### Keep these authorities in place
|
|
39
|
-
|
|
40
|
-
- `PG` remains responsible for URL construction, history, transport, exit guards, page dispatch,
|
|
41
|
-
and the `page:beforeUnmount` / `page:mounted` lifecycle.
|
|
42
|
-
- Existing `MD` handlers and business logic continue receiving the same page data.
|
|
43
|
-
- Existing `UI.new*` call sites keep their DOM return values and `getV` / `setV` / chaining
|
|
44
|
-
contracts while each factory implementation is replaced. New code uses the Zx component API
|
|
45
|
-
directly; the old factory is removed when its call-site count reaches zero.
|
|
46
|
-
- Zx owns only the mounted component subtree and tears that subtree down exactly once.
|
|
47
|
-
- New client/schema-driven screens may use `@zeyos/zx/zeyos`; adopting that optional layer is not a
|
|
48
|
-
prerequisite for converting current server-rendered screens.
|
|
49
|
-
|
|
50
|
-
Safe bootstrap order is the normal Zx bundle, the current Classic runtime delegating its compact
|
|
51
|
-
DOM seams to Zx, then the existing `Z(data)` application bootstrap. Component actions must call
|
|
52
|
-
`PG.go()` for internal routes; do not let a navigation component fall back to `location.assign()`.
|
|
53
|
-
|
|
54
|
-
### Recommended adoption waves
|
|
55
|
-
|
|
56
|
-
1. Mount persistent shell components behind current lifecycle events: launcher, account menu,
|
|
57
|
-
application rail, detail chrome, and overlays.
|
|
58
|
-
2. Replace the highest-volume `UI.new*` implementations—inputs, Selects, tabs, menus, dialogs, and
|
|
59
|
-
tables—one family at a time while preserving their existing call-site contract.
|
|
60
|
-
3. Move record and transaction patterns only after old/new behavior has discriminating fixtures.
|
|
61
|
-
4. Use direct Zx and the ZeyOS schema binding for new surfaces; retire an old factory only after its
|
|
62
|
-
remaining call-site count reaches zero.
|
|
63
|
-
|
|
64
|
-
### Shell components and ownership
|
|
65
|
-
|
|
66
|
-
`Search` remains a field-level input. `Launcher` is the shell-level application/record surface: it
|
|
67
|
-
ranks local items, debounces and aborts grouped sources, ignores stale responses, and supplies the
|
|
68
|
-
Cmd/Ctrl+K dialog/listbox interaction. ZeyOS filters permissions, supplies apps/recent records,
|
|
69
|
-
decides caching, and handles the cancelable selection through `PG`; Launcher never becomes a
|
|
70
|
-
router.
|
|
71
|
-
|
|
72
|
-
`Avatar` and `AccountMenu` provide the identity trigger, profile header, grouped actions, focus
|
|
73
|
-
return, and compact rail presentation. ZeyOS still decides permission-dependent routes, language
|
|
74
|
-
and theme choices, authentication/logout, and About/version data. A selection is a request to the
|
|
75
|
-
application, not a session mutation.
|
|
76
|
-
|
|
77
|
-
`AppSidebar` is the expanded vertical application tree and is the only presentation with inline
|
|
78
|
-
children. Its collapsed state renders `AppRail`. A standalone rail may be vertical or horizontal,
|
|
79
|
-
but every child collection remains a hover/focus/keyboard flyout. Both components accept native
|
|
80
|
-
links or cancelable actions and expose active/collapsed/expanded state without persisting it.
|
|
81
|
-
|
|
82
|
-
Use bundled Zx icons or explicitly registered local assets. The optional ZeyOS icon helper's
|
|
83
|
-
remote Font Awesome kit is not appropriate for a dependency-free production shell.
|
|
84
|
-
|
|
85
|
-
### Persistent shell contract
|
|
86
|
-
|
|
87
|
-
These invariants were validated while prototyping the replacement shell and remain binding for a
|
|
88
|
-
Xenon implementation:
|
|
89
|
-
|
|
90
|
-
- Application-menu position is `top`, `bottom`, `left`, `right`, or `none`, defaulting to `left`.
|
|
91
|
-
Persist it under `zeyos.appMenu.position` (scope-suffixed where separate workspaces share an
|
|
92
|
-
origin) and publish the resolved value on `:root[data-app-menu-effective]`.
|
|
93
|
-
- Sidebar minimization is a user choice, never a hover side effect. Keep the existing
|
|
94
|
-
`zeyos.appRail.expanded` preference while migrating and map it to
|
|
95
|
-
`AppSidebar.setCollapsed(!expanded)`. `AppRail` itself never expands labels or inline children.
|
|
96
|
-
- The minimized rail is 48 px and the expanded sidebar 184 px. The application bar is 48 px; tiles
|
|
97
|
-
are 30 px, glyphs 22 px, and compact controls 31 px. Workspace tools in a side layout remain
|
|
98
|
-
inside the rail, and page insets derive from the effective rail/bar geometry.
|
|
99
|
-
- Theme preference is `auto`, `light`, or `dark`, persisted under `zeyos.theme` and applied before
|
|
100
|
-
paint. `auto` follows the system color-scheme media query.
|
|
101
|
-
- The dark neutral ramp is a coal surface rebased from Carbon Gray 100 toward `#0b0c0e`. Avoid a
|
|
102
|
-
cyan/slate cast that competes with ZeyOS module colors; module accents must retain clear contrast
|
|
103
|
-
on both shell and content surfaces.
|
|
104
|
-
|
|
105
|
-
### Migration proof points
|
|
106
|
-
|
|
107
|
-
Before replacing any Classic surface, verify that internal navigation and browser history still use
|
|
108
|
-
`PG.go()` without a full reload; page-scoped instances are destroyed exactly once; persistent shell
|
|
109
|
-
listeners do not multiply across navigation or language changes; launcher output matches permission,
|
|
110
|
-
app, recent, and search fixtures; account content matches ordinary/admin/developer roles; and the
|
|
111
|
-
production shell loads no remote UI or icon dependency.
|
|
112
|
-
|
|
113
|
-
The shell wiring stays direct:
|
|
114
|
-
|
|
115
|
-
```js
|
|
116
|
-
const launcher = new zx.Launcher(null, {
|
|
117
|
-
items: applicationCatalogue,
|
|
118
|
-
sources: recordSources,
|
|
119
|
-
onselect(event) {
|
|
120
|
-
event.preventDefault();
|
|
121
|
-
PG.go(resolveLauncherRoute(event.detail.item));
|
|
122
|
-
}
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
const sidebar = new zx.AppSidebar(shellHost, {
|
|
126
|
-
items: applicationTree,
|
|
127
|
-
active: currentRouteId,
|
|
128
|
-
collapsed: !savedAppRailExpanded,
|
|
129
|
-
footer: accountMenu,
|
|
130
|
-
onselect(event) {
|
|
131
|
-
event.preventDefault();
|
|
132
|
-
PG.go(event.detail.href);
|
|
133
|
-
}
|
|
134
|
-
});
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
## Deployment script swap
|
|
138
|
-
|
|
139
|
-
For deployments that serve the old libraries directly from
|
|
140
|
-
`net.zeyon.lib.gx-3000/assets/`, copy `dist/zx.global.js`, `dist/zx-compat.global.js`, and
|
|
141
|
-
`dist/zx.css` into that asset directory. Replace:
|
|
142
|
-
|
|
143
|
-
```html
|
|
144
|
-
<link rel="stylesheet" href="/net.zeyon.lib.gx-3000/assets/gx.css">
|
|
145
|
-
<script src="/net.zeyon.lib.gx-3000/assets/gx-core.js"></script>
|
|
146
|
-
<script src="/net.zeyon.lib.gx-3000/assets/gx-zeyos.js"></script>
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
with this order:
|
|
150
|
-
|
|
151
|
-
```html
|
|
152
|
-
<link rel="stylesheet" href="/net.zeyon.lib.gx-3000/assets/zx.css">
|
|
153
|
-
<script src="/net.zeyon.lib.gx-3000/assets/zx.global.js"></script>
|
|
154
|
-
<script src="/net.zeyon.lib.gx-3000/assets/zx-compat.global.js"></script>
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
The compatibility entry exposes both `window.zx` and `window.gx`. Element
|
|
158
|
-
`store`/`retrieve`/`eliminate` is installed automatically. If a genuine gx application uses its
|
|
159
|
-
free parser, `_()`, `String.prototype.htmlSpecialChars`, or `Array.prototype.findBy`, opt in after
|
|
160
|
-
both scripts load. An existing `__()` is deliberately never replaced:
|
|
161
|
-
|
|
162
|
-
```js
|
|
163
|
-
gx.compat.installGlobals();
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
Do not call this for newly migrated code; import `h`, `htmlEscape`, and the i18n helpers directly.
|
|
167
|
-
|
|
168
|
-
## Class-by-class replacements
|
|
169
|
-
|
|
170
|
-
The snippets show the normal direct-Zx replacement. All strings supplied as component content are
|
|
171
|
-
inserted as text; pass a DOM node when structured content is needed.
|
|
172
|
-
|
|
173
|
-
### `gx.zeyos.Toggle` → `zx.Toggle`
|
|
174
|
-
|
|
175
|
-
```js
|
|
176
|
-
// gx
|
|
177
|
-
const control = new gx.zeyos.Toggle(host, { on: true, value: 'enabled' });
|
|
178
|
-
control.addEvent('uncheck', () => save(false));
|
|
179
|
-
|
|
180
|
-
// Zx
|
|
181
|
-
const control = new zx.Toggle(host, { checked: true, value: 'enabled' });
|
|
182
|
-
control.on('change', (event) => save(event.detail.checked));
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
Use `get()`, `getValue()`, `set(boolean)`, and `toggle()` in place of
|
|
186
|
-
`getState()`, `setChecked()`, and `setUnchecked()`.
|
|
187
|
-
|
|
188
|
-
### `gx.zeyos.Msgbox` → `zx.Message`
|
|
189
|
-
|
|
190
|
-
```js
|
|
191
|
-
// gx
|
|
192
|
-
const messages = new gx.zeyos.Msgbox();
|
|
193
|
-
messages.show('Saved', 's_msg_32_success');
|
|
194
|
-
|
|
195
|
-
// Zx
|
|
196
|
-
const messages = new zx.Message(null, { timeout: 0 });
|
|
197
|
-
document.body.append(messages.el);
|
|
198
|
-
const handle = messages.show('Saved', { kind: 'success' });
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
Call `handle.close()` instead of `hide()`.
|
|
202
|
-
|
|
203
|
-
### `gx.bootstrap.Message` → `zx.Message`
|
|
204
|
-
|
|
205
|
-
```js
|
|
206
|
-
// gx
|
|
207
|
-
const messages = new gx.bootstrap.Message(host);
|
|
208
|
-
messages.addMessage('Saved', 'success', true);
|
|
209
|
-
messages.showStatus(40, 'Uploading');
|
|
210
|
-
|
|
211
|
-
// Zx
|
|
212
|
-
const messages = new zx.Message(host);
|
|
213
|
-
messages.show('Saved', { kind: 'success', closable: true });
|
|
214
|
-
const progress = zx.Message.progress('Uploading');
|
|
215
|
-
progress.update(40);
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
Use `progress.done()` or `progress.fail(message)` instead of `hideStatus()`.
|
|
219
|
-
|
|
220
|
-
### `gx.zeyos.Popup` and `gx.bootstrap.Popup` → `zx.Dialog`
|
|
221
|
-
|
|
222
|
-
```js
|
|
223
|
-
// gx (either Popup class)
|
|
224
|
-
const popup = new gx.zeyos.Popup({ width: 480, content: 'Details', overlayDismiss: true });
|
|
225
|
-
popup.show();
|
|
226
|
-
|
|
227
|
-
// Zx
|
|
228
|
-
const popup = new zx.Dialog(null, { size: 480, content: 'Details', lightDismiss: true });
|
|
229
|
-
popup.open();
|
|
230
|
-
```
|
|
231
|
-
|
|
232
|
-
Replace `show`/`hide` events and methods with `open`/`close`. Use CSS for custom positioning;
|
|
233
|
-
legacy `setPosition()` is only a warning-producing compatibility no-op.
|
|
234
|
-
|
|
235
|
-
### `gx.bootstrap.PopupAlert` → `zx.Dialog.alert`
|
|
236
|
-
|
|
237
|
-
```js
|
|
238
|
-
// gx
|
|
239
|
-
await gx.bootstrap.PopupAlert('Saved', 'The record was saved.');
|
|
240
|
-
|
|
241
|
-
// Zx
|
|
242
|
-
await zx.Dialog.alert({ title: 'Saved', message: 'The record was saved.' });
|
|
243
|
-
```
|
|
244
|
-
|
|
245
|
-
### `gx.bootstrap.PopupConfirm` and `PopupConfirmCanceled` → `zx.Dialog.confirm`
|
|
246
|
-
|
|
247
|
-
```js
|
|
248
|
-
// gx
|
|
249
|
-
const result = await gx.bootstrap.PopupConfirm('Delete', 'Delete this record?');
|
|
250
|
-
if (result !== gx.bootstrap.PopupConfirmCanceled) removeRecord();
|
|
251
|
-
|
|
252
|
-
// Zx
|
|
253
|
-
if (await zx.Dialog.confirm({ title: 'Delete', message: 'Delete this record?', danger: true })) {
|
|
254
|
-
removeRecord();
|
|
255
|
-
}
|
|
256
|
-
```
|
|
257
|
-
|
|
258
|
-
### `gx.zeyos.Dialog` → `zx.Dialog` views
|
|
259
|
-
|
|
260
|
-
```js
|
|
261
|
-
// gx
|
|
262
|
-
const dialog = new gx.zeyos.Dialog(null, { title: 'Wizard' });
|
|
263
|
-
dialog.addFrame('review', { title: 'Review', content: reviewNode }, true);
|
|
264
|
-
dialog.openFrame('review');
|
|
265
|
-
|
|
266
|
-
// Zx
|
|
267
|
-
const dialog = new zx.Dialog(null, { title: 'Wizard' });
|
|
268
|
-
dialog.addView('review', { title: 'Review', content: reviewNode });
|
|
269
|
-
dialog.showView('review');
|
|
270
|
-
dialog.open();
|
|
271
|
-
```
|
|
272
|
-
|
|
273
|
-
Replace `addSubmitFrame`, `addFormFrame`, and `addSuccessFrame` with explicit views, `Form`
|
|
274
|
-
instances, and dialog button actions. Read form data with `form.getValues()`.
|
|
275
|
-
|
|
276
|
-
### `gx.zeyos.Dropdown` → `zx.MenuButton`
|
|
277
|
-
|
|
278
|
-
```js
|
|
279
|
-
// gx
|
|
280
|
-
const menu = new gx.zeyos.Dropdown(host, { items: { edit: 'Edit', remove: 'Remove' } });
|
|
281
|
-
menu.addEvent('change', (value) => run(value));
|
|
282
|
-
|
|
283
|
-
// Zx
|
|
284
|
-
const menu = new zx.MenuButton(null, {
|
|
285
|
-
label: 'Actions',
|
|
286
|
-
items: [{ value: 'edit', label: 'Edit' }, { value: 'remove', label: 'Remove' }],
|
|
287
|
-
onselect: (event) => run(event.detail.value)
|
|
288
|
-
});
|
|
289
|
-
host.append(menu.el);
|
|
290
|
-
```
|
|
291
|
-
|
|
292
|
-
### `gx.bootstrap.MenuButton` → `zx.MenuButton`
|
|
293
|
-
|
|
294
|
-
```js
|
|
295
|
-
// gx
|
|
296
|
-
const menu = new gx.bootstrap.MenuButton(host, { label: 'Actions' });
|
|
297
|
-
menu.add('Edit').divider().add('Remove');
|
|
298
|
-
|
|
299
|
-
// Zx
|
|
300
|
-
const menu = new zx.MenuButton(host, {
|
|
301
|
-
label: 'Actions', items: [{ label: 'Edit', value: 'edit' }, '-', { label: 'Remove', value: 'remove' }]
|
|
302
|
-
});
|
|
303
|
-
```
|
|
304
|
-
|
|
305
|
-
### `gx.zeyos.Select` and `gx.bootstrap.Select` → `zx.Select`
|
|
306
|
-
|
|
307
|
-
```js
|
|
308
|
-
// gx (either namespace)
|
|
309
|
-
const select = new gx.zeyos.Select(host, {
|
|
310
|
-
data: rows, elementIndex: 'ID', elementLabel: 'name', allowEmpty: true
|
|
311
|
-
});
|
|
312
|
-
select.addEvent('select', (item) => save(item.ID));
|
|
313
|
-
|
|
314
|
-
// Zx
|
|
315
|
-
const select = new zx.Select(host, {
|
|
316
|
-
items: rows, valueKey: 'ID', labelKey: 'name', clearable: true,
|
|
317
|
-
onchange: (event) => save(event.detail.value)
|
|
318
|
-
});
|
|
319
|
-
```
|
|
320
|
-
|
|
321
|
-
Use `value`/`selected`, `set()`, `setItems()`, and `reset()`. A cleared selection is the same
|
|
322
|
-
`change` event with `{value: null, item: null}` rather than a separate `noselect` event.
|
|
323
|
-
|
|
324
|
-
### `gx.zeyos.SelectFilter` and `gx.bootstrap.SelectFilter` → local `zx.Select`
|
|
325
|
-
|
|
326
|
-
```js
|
|
327
|
-
// gx
|
|
328
|
-
const select = new gx.zeyos.SelectFilter(host, { data: rows, searchfields: ['name', 'code'] });
|
|
329
|
-
|
|
330
|
-
// Zx
|
|
331
|
-
const select = new zx.Select(host, { items: rows, filter: 'local', searchKeys: ['name', 'code'] });
|
|
332
|
-
```
|
|
333
|
-
|
|
334
|
-
### `gx.zeyos.SelectDyn` and `gx.bootstrap.SelectDyn` → async `zx.Select`
|
|
335
|
-
|
|
336
|
-
```js
|
|
337
|
-
// gx
|
|
338
|
-
const select = new gx.zeyos.SelectDyn(host, { url: '/people', queryParam: 'query' });
|
|
339
|
-
|
|
340
|
-
// Zx
|
|
341
|
-
const http = new zx.Http();
|
|
342
|
-
const select = new zx.Select(host, {
|
|
343
|
-
filter: (query) => http.get('/people', { query }),
|
|
344
|
-
onloaded: (event) => console.log(event.detail.items)
|
|
345
|
-
});
|
|
346
|
-
```
|
|
347
|
-
|
|
348
|
-
### `gx.bootstrap.SelectDynREST` → `zx.Select` + `zx.zeyosService`
|
|
349
|
-
|
|
350
|
-
```js
|
|
351
|
-
// gx
|
|
352
|
-
const select = new gx.bootstrap.SelectDynREST(host, { entity: 'contacts', limit: 20 });
|
|
353
|
-
|
|
354
|
-
// Zx
|
|
355
|
-
const contacts = zx.zeyosService('contacts');
|
|
356
|
-
const select = new zx.Select(host, {
|
|
357
|
-
filter: (query) => contacts.get('', { search: query, limit: 20 })
|
|
358
|
-
});
|
|
359
|
-
```
|
|
360
|
-
|
|
361
|
-
### `gx.zeyos.SelectPrio` and `gx.bootstrap.SelectPrio` → `zx.Select.priority`
|
|
362
|
-
|
|
363
|
-
```js
|
|
364
|
-
// gx
|
|
365
|
-
const priority = new gx.zeyos.SelectPrio(host, { value: 2 });
|
|
366
|
-
|
|
367
|
-
// Zx
|
|
368
|
-
const priority = zx.Select.priority(host, { value: 2 });
|
|
369
|
-
```
|
|
370
|
-
|
|
371
|
-
### `gx.zeyos.Checklist` and `gx.bootstrap.Checklist` → `zx.Checklist`
|
|
372
|
-
|
|
373
|
-
```js
|
|
374
|
-
// gx (either namespace)
|
|
375
|
-
const list = new gx.zeyos.Checklist(host, { data: rows, listValue: 'ID', listFormat: 'name' });
|
|
376
|
-
list.setValues([1, 2]);
|
|
377
|
-
|
|
378
|
-
// Zx
|
|
379
|
-
const list = new zx.Checklist(host, { items: rows, valueKey: 'ID', labelKey: 'name' });
|
|
380
|
-
list.setValues([1, 2]);
|
|
381
|
-
```
|
|
382
|
-
|
|
383
|
-
For remote data pass `load: () => Promise<items>` and listen for `loaded`.
|
|
384
|
-
|
|
385
|
-
### `gx.zeyos.Permission` → `zx.Select.permission()`
|
|
386
|
-
|
|
387
|
-
```js
|
|
388
|
-
// gx
|
|
389
|
-
const permission = new gx.zeyos.Permission(host, { value: true, groups });
|
|
390
|
-
|
|
391
|
-
// Zx
|
|
392
|
-
const permission = zx.Select.permission(host, { value: true, groups });
|
|
393
|
-
permission.on('change', (event) => save(event.detail.value));
|
|
394
|
-
```
|
|
395
|
-
|
|
396
|
-
### `gx.zeyos.Table` and `gx.bootstrap.Table` → `zx.Table`
|
|
397
|
-
|
|
398
|
-
```js
|
|
399
|
-
// gx (either namespace)
|
|
400
|
-
const table = new gx.zeyos.Table(host, {
|
|
401
|
-
cols: [{ id: 'name', label: 'Name', filterable: true }],
|
|
402
|
-
structure: (row) => [row.name], data: rows
|
|
403
|
-
});
|
|
404
|
-
|
|
405
|
-
// Zx
|
|
406
|
-
const table = new zx.Table(host, {
|
|
407
|
-
columns: [{ id: 'name', label: 'Name', sortable: true }],
|
|
408
|
-
data: rows, rowId: 'ID'
|
|
409
|
-
});
|
|
410
|
-
```
|
|
411
|
-
|
|
412
|
-
Move `structure()` work into per-column `render(row, index)`. Replace `filter` events with `sort`
|
|
413
|
-
and `click`/`dblclick` with `rowclick`/`rowdblclick`; all payloads are in `event.detail`.
|
|
414
|
-
|
|
415
|
-
### `gx.ui.SimpleTable` → `zx.Table`
|
|
416
|
-
|
|
417
|
-
```js
|
|
418
|
-
// gx
|
|
419
|
-
const table = new gx.ui.SimpleTable({ cols: ['Name'] });
|
|
420
|
-
table.addRow({ ID: 1, name: 'Ada' });
|
|
421
|
-
|
|
422
|
-
// Zx
|
|
423
|
-
const table = new zx.Table(null, { columns: [{ id: 'name', label: 'Name' }], rowId: 'ID' });
|
|
424
|
-
table.addData([{ ID: 1, name: 'Ada' }]);
|
|
425
|
-
```
|
|
426
|
-
|
|
427
|
-
Use `setData`, `addData`, `updateRow`, `removeRow`, and `getData` for row operations.
|
|
428
|
-
|
|
429
|
-
### Billing/transaction rows → `zx.Grid.BillingItems()`
|
|
430
|
-
|
|
431
|
-
Use the generic preset when the existing screen has description, quantity, unit, unit price,
|
|
432
|
-
currency, total, and optional parent rows:
|
|
433
|
-
|
|
434
|
-
```js
|
|
435
|
-
const items = zx.Grid.BillingItems(host, {
|
|
436
|
-
data: rows,
|
|
437
|
-
units: unitChoices,
|
|
438
|
-
currencies: currencyChoices,
|
|
439
|
-
oneditcommit(event) {
|
|
440
|
-
if (!controller.accept(event.detail.changes)) event.preventDefault();
|
|
441
|
-
}
|
|
442
|
-
});
|
|
443
|
-
```
|
|
444
|
-
|
|
445
|
-
The default line total is quantity × unit price. Keep taxes, discounts, subtotal/group updates,
|
|
446
|
-
rounding policy, posting, server persistence, and undo in the existing controller. Override
|
|
447
|
-
`lineTotal` only when that calculation is a stable screen-level rule.
|
|
448
|
-
|
|
449
|
-
### `gx.zeyos.Tabbox` and `gx.bootstrap.Tabbox` → `zx.Tabbox`
|
|
450
|
-
|
|
451
|
-
```js
|
|
452
|
-
// gx (either namespace; show is one-based)
|
|
453
|
-
const tabs = new gx.zeyos.Tabbox(host, { frames, show: 2 });
|
|
454
|
-
tabs.openTab('history');
|
|
455
|
-
|
|
456
|
-
// Zx
|
|
457
|
-
const tabs = new zx.Tabbox(host, { tabs, active: 'history' });
|
|
458
|
-
tabs.openTab('history');
|
|
459
|
-
```
|
|
460
|
-
|
|
461
|
-
Rename `frames` to `tabs`; each entry keeps `name`, `title`, and `content`. Use `setBadge()` in
|
|
462
|
-
place of `setHighlight()`.
|
|
463
|
-
|
|
464
|
-
### `gx.bootstrap.NavigationBar` → `zx.NavigationBar`
|
|
465
|
-
|
|
466
|
-
```js
|
|
467
|
-
// gx
|
|
468
|
-
const navigation = new gx.bootstrap.NavigationBar(host, { title: 'ZeyOS', frames, buttons });
|
|
469
|
-
|
|
470
|
-
// Zx
|
|
471
|
-
const navigation = new zx.NavigationBar(host, { title: 'ZeyOS', items: frames, actions: buttons });
|
|
472
|
-
```
|
|
473
|
-
|
|
474
|
-
### `gx.zeyos.Panel` → `zx.Panel`
|
|
475
|
-
|
|
476
|
-
```js
|
|
477
|
-
// gx
|
|
478
|
-
const panel = new gx.zeyos.Panel(host, 'Details', contentNode, true);
|
|
479
|
-
|
|
480
|
-
// Zx
|
|
481
|
-
const panel = new zx.Panel(host, { title: 'Details', content: contentNode, open: true });
|
|
482
|
-
```
|
|
483
|
-
|
|
484
|
-
### `gx.zeyos.MasterPanel` → `zx.MasterPanel`
|
|
485
|
-
|
|
486
|
-
```js
|
|
487
|
-
// gx
|
|
488
|
-
const panel = new gx.zeyos.MasterPanel(host, 'Contacts', contentNode, buttons);
|
|
489
|
-
|
|
490
|
-
// Zx
|
|
491
|
-
const panel = new zx.MasterPanel(host, { title: 'Contacts', content: contentNode, buttons });
|
|
492
|
-
```
|
|
493
|
-
|
|
494
|
-
### `gx.zeyos.Groupbox` → `zx.Groupbox`
|
|
495
|
-
|
|
496
|
-
```js
|
|
497
|
-
// gx
|
|
498
|
-
const group = new gx.zeyos.Groupbox(host, { title: 'Advanced', open: false });
|
|
499
|
-
|
|
500
|
-
// Zx
|
|
501
|
-
const group = new zx.Groupbox(host, { title: 'Advanced', open: false });
|
|
502
|
-
group.open();
|
|
503
|
-
```
|
|
504
|
-
|
|
505
|
-
### `gx.zeyos.Search` → `zx.Search`
|
|
506
|
-
|
|
507
|
-
```js
|
|
508
|
-
// gx
|
|
509
|
-
const search = new gx.zeyos.Search();
|
|
510
|
-
search.addEvent('keypress', () => load(search.get()));
|
|
511
|
-
|
|
512
|
-
// Zx
|
|
513
|
-
const search = new zx.Search(null, { onsubmit: (event) => load(event.detail.value) });
|
|
514
|
-
host.append(search.el);
|
|
515
|
-
```
|
|
516
|
-
|
|
517
|
-
### `gx.zeyos.Datebox` → `zx.Datebox` or `zx.DateTimeBox`
|
|
518
|
-
|
|
519
|
-
```js
|
|
520
|
-
// gx
|
|
521
|
-
const date = new gx.zeyos.Datebox(host, { timestamp, unit: 'seconds', format: ['d', '.', 'M', '.', 'y'] });
|
|
522
|
-
|
|
523
|
-
// Zx
|
|
524
|
-
const date = new zx.Datebox(host, { value: timestamp, format: '%d.%m.%Y' });
|
|
525
|
-
const timestampSeconds = date.get('seconds');
|
|
526
|
-
```
|
|
527
|
-
|
|
528
|
-
Use `zx.DateTimeBox(host, options)` when the legacy format included hours/minutes.
|
|
529
|
-
|
|
530
|
-
### `gx.zeyos.DatePicker` → `zx.DatePicker`
|
|
531
|
-
|
|
532
|
-
```js
|
|
533
|
-
// gx
|
|
534
|
-
const picker = new gx.zeyos.DatePicker(host, { date: initial, weeknumbers: true });
|
|
535
|
-
|
|
536
|
-
// Zx
|
|
537
|
-
const picker = new zx.DatePicker(host, { value: initial, showWeekNumbers: true });
|
|
538
|
-
```
|
|
539
|
-
|
|
540
|
-
### `gx.zeyos.MonthPicker` → `zx.MonthPicker`
|
|
541
|
-
|
|
542
|
-
```js
|
|
543
|
-
// gx
|
|
544
|
-
const picker = new gx.zeyos.MonthPicker(host, { date: initial });
|
|
545
|
-
|
|
546
|
-
// Zx
|
|
547
|
-
const picker = new zx.MonthPicker(host, { value: initial });
|
|
548
|
-
```
|
|
549
|
-
|
|
550
|
-
### `gx.zeyos.TimePicker` → `zx.TimePicker`
|
|
551
|
-
|
|
552
|
-
```js
|
|
553
|
-
// gx
|
|
554
|
-
const picker = new gx.zeyos.TimePicker(host, { date: initial, format: '%H:%M:%S' });
|
|
555
|
-
|
|
556
|
-
// Zx
|
|
557
|
-
const picker = new zx.TimePicker(host, { value: initial, seconds: true });
|
|
558
|
-
```
|
|
559
|
-
|
|
560
|
-
Direct picker `change` events carry `{date}` or `{time}`. Use `Date#getTime()` or the exported
|
|
561
|
-
date formatting helpers when a timestamp or formatted string is required.
|
|
562
|
-
|
|
563
|
-
### `gx.ui.Timebox`, `gx.zeyos.Timebox`, and `gx.bootstrap.Timebox` → `zx.Timebox`
|
|
564
|
-
|
|
565
|
-
```js
|
|
566
|
-
// gx (any namespace)
|
|
567
|
-
const duration = new gx.zeyos.Timebox(host, { time: -90, unit: 'minutes', prefix: true });
|
|
568
|
-
|
|
569
|
-
// Zx
|
|
570
|
-
const duration = new zx.Timebox(host, { value: -90, unit: 'minutes', signed: true });
|
|
571
|
-
```
|
|
572
|
-
|
|
573
|
-
Use `get(unit)`, `set(value, unit)`, `enable()`, and `disable()`.
|
|
574
|
-
|
|
575
|
-
### `gx.bootstrap.CheckButton` → `zx.CheckButton`
|
|
576
|
-
|
|
577
|
-
```js
|
|
578
|
-
// gx
|
|
579
|
-
const check = new gx.bootstrap.CheckButton(host, { label: ['On', 'Off'], value: true });
|
|
580
|
-
|
|
581
|
-
// Zx
|
|
582
|
-
const check = new zx.CheckButton(host, { label: ['On', 'Off'], checked: true });
|
|
583
|
-
check.on('change', (event) => save(event.detail.checked));
|
|
584
|
-
```
|
|
585
|
-
|
|
586
|
-
### `gx.bootstrap.Form` → `zx.Form`
|
|
587
|
-
|
|
588
|
-
```js
|
|
589
|
-
// gx
|
|
590
|
-
const form = new gx.bootstrap.Form(host, { horizontal: true, fields: legacyFields });
|
|
591
|
-
|
|
592
|
-
// Zx
|
|
593
|
-
const form = new zx.Form(host, {
|
|
594
|
-
fieldsets: [{ fields: modernFields }],
|
|
595
|
-
actions: [{ id: 'save', label: 'Save', type: 'submit', kind: 'primary' }]
|
|
596
|
-
});
|
|
597
|
-
```
|
|
598
|
-
|
|
599
|
-
Use `getValues`, `setValues`, `getField`, `setHighlights`, `reset`, and the preventable `submit`
|
|
600
|
-
event.
|
|
601
|
-
|
|
602
|
-
### `gx.bootstrap.Fieldset` → `zx.Fieldset`
|
|
603
|
-
|
|
604
|
-
```js
|
|
605
|
-
// gx
|
|
606
|
-
const fields = new gx.bootstrap.Fieldset(host, { title: 'Contact', fields: legacyFields });
|
|
607
|
-
|
|
608
|
-
// Zx
|
|
609
|
-
const fields = new zx.Fieldset(host, { title: 'Contact', columns: 2, fields: modernFields });
|
|
610
|
-
```
|
|
611
|
-
|
|
612
|
-
### `gx.bootstrap.Field` → `zx.Field`
|
|
613
|
-
|
|
614
|
-
```js
|
|
615
|
-
// gx
|
|
616
|
-
const field = new gx.bootstrap.Field({ type: 'text', label: 'Name', value: 'Ada' });
|
|
617
|
-
host.append(field.toElement());
|
|
618
|
-
|
|
619
|
-
// Zx
|
|
620
|
-
const field = new zx.Field(host, { id: 'name', type: 'text', label: 'Name', value: 'Ada' });
|
|
621
|
-
```
|
|
622
|
-
|
|
623
|
-
Legacy `gxselect`, date/month/time widgets, option lists, and custom HTML fields map to registered
|
|
624
|
-
Zx field adapters. Highlights use `danger`, `warning`, or `success`.
|
|
625
|
-
|
|
626
|
-
### `gx.bootstrap.ValueList` → `zx.ValueList`
|
|
627
|
-
|
|
628
|
-
```js
|
|
629
|
-
// gx
|
|
630
|
-
const values = new gx.bootstrap.ValueList(host, { deletable: true });
|
|
631
|
-
values.addValue('one');
|
|
632
|
-
|
|
633
|
-
// Zx
|
|
634
|
-
const values = new zx.ValueList(host, { deletable: true, sortable: true });
|
|
635
|
-
values.addValue('one');
|
|
636
|
-
```
|
|
637
|
-
|
|
638
|
-
### `gx.bootstrap.MultiValueEditor` → `zx.MultiValueEditor`
|
|
639
|
-
|
|
640
|
-
```js
|
|
641
|
-
// gx (JSON string)
|
|
642
|
-
const editor = new gx.bootstrap.MultiValueEditor(host);
|
|
643
|
-
editor.setValue('["one","two"]');
|
|
644
|
-
|
|
645
|
-
// Zx (array)
|
|
646
|
-
const editor = new zx.MultiValueEditor(host);
|
|
647
|
-
editor.setValues(['one', 'two']);
|
|
648
|
-
```
|
|
649
|
-
|
|
650
|
-
The compatibility wrapper deliberately preserves JSON-string `getValue()`/`setValue()` only for
|
|
651
|
-
unmigrated callers.
|
|
652
|
-
|
|
653
|
-
### `gx.bootstrap.FieldUpload` → `zx.FieldUpload`
|
|
654
|
-
|
|
655
|
-
```js
|
|
656
|
-
// gx
|
|
657
|
-
const upload = new gx.bootstrap.FieldUpload({ uploadurl: '/upload', inputname: 'document' });
|
|
658
|
-
host.append(upload.toElement());
|
|
659
|
-
|
|
660
|
-
// Zx
|
|
661
|
-
const upload = new zx.FieldUpload(host, { url: '/upload', paramName: 'document' });
|
|
662
|
-
```
|
|
663
|
-
|
|
664
|
-
Use `params`, `accept`, `maxSize`, `multiple`, and the `select`, `progress`, `success`, `error`, and
|
|
665
|
-
`abort` events.
|
|
666
|
-
|
|
667
|
-
### `gx.bootstrap.DataFilter` → `zx.DataFilter`
|
|
668
|
-
|
|
669
|
-
```js
|
|
670
|
-
// gx
|
|
671
|
-
const filter = new gx.bootstrap.DataFilter(host);
|
|
672
|
-
filter.addFulltextFilter('query', ['name', 'city']).initData(rows).doFilter();
|
|
673
|
-
|
|
674
|
-
// Zx
|
|
675
|
-
const filter = new zx.DataFilter(host, {
|
|
676
|
-
data: rows,
|
|
677
|
-
filters: [{ type: 'text', id: 'query', label: 'Search', fields: ['name', 'city'] }]
|
|
678
|
-
});
|
|
679
|
-
const visibleRows = filter.apply();
|
|
680
|
-
```
|
|
681
|
-
|
|
682
|
-
Use `setData`, `setState`, `getState`, `apply`, `clear`, and `addFilter`.
|
|
683
|
-
|
|
684
|
-
### `gx.zeyos.Client` → `zx.Http`
|
|
685
|
-
|
|
686
|
-
```js
|
|
687
|
-
// gx
|
|
688
|
-
const client = new gx.zeyos.Client({ url: '/api/' });
|
|
689
|
-
client.get('contacts', { active: true });
|
|
690
|
-
|
|
691
|
-
// Zx
|
|
692
|
-
const client = new zx.Http({ base: '/api/' });
|
|
693
|
-
await client.get('contacts', { active: true });
|
|
694
|
-
```
|
|
695
|
-
|
|
696
|
-
### `gx.zeyos.Request` → `zx.zeyosService`
|
|
697
|
-
|
|
698
|
-
```js
|
|
699
|
-
// gx
|
|
700
|
-
const request = new gx.zeyos.Request({ service: 'contacts', accesskey: token });
|
|
701
|
-
request.post('create', data);
|
|
702
|
-
|
|
703
|
-
// Zx
|
|
704
|
-
const request = zx.zeyosService('contacts', token);
|
|
705
|
-
await request.post('create', data);
|
|
706
|
-
```
|
|
707
|
-
|
|
708
|
-
`Http` uses promises and exposes `request`, `get`, `post`, `put`, and `delete`. Uploads use
|
|
709
|
-
`request(path, {method, files, data})`. Request failures reject; use `try`/`catch` or `onError`.
|
|
710
|
-
|
|
711
|
-
### `gx.zeyos.Factory` → `zx.button`, `zx.buttonGroup`, and `zx.icon`
|
|
712
|
-
|
|
713
|
-
```js
|
|
714
|
-
// gx
|
|
715
|
-
const save = gx.zeyos.Factory.Button('Save', 'primary', 'check');
|
|
716
|
-
const glyph = gx.zeyos.Factory.Icon('trash');
|
|
717
|
-
|
|
718
|
-
// Zx
|
|
719
|
-
const save = zx.button({ label: 'Save', kind: 'primary', icon: 'check' });
|
|
720
|
-
const glyph = zx.icon('trash');
|
|
721
|
-
```
|
|
722
|
-
|
|
723
|
-
## Intentionally not reproduced
|
|
724
|
-
|
|
725
|
-
- `gx.core.Settings` and `gx.ui.Container`: direct construction now throws. Use plain option
|
|
726
|
-
objects and extend `zx.Component`; emulating the MooTools inheritance model would retain the
|
|
727
|
-
coupling Zx removes.
|
|
728
|
-
- `gx.ui.Collapse` and `gx.ui.Blend`: compatibility names are migration-error stubs. Use CSS
|
|
729
|
-
transitions guarded by `prefers-reduced-motion`; Zx does not reproduce `Fx.*` timing or the
|
|
730
|
-
MooTools `Class`/`Fx` APIs.
|
|
731
|
-
- `gx.ui.Hud`: use `zx.Dialog` or `zx.Modal` so focus, Escape, and the top layer remain native.
|
|
732
|
-
- `gx.ui.Toggling`: use `zx.Toggle`, `zx.CheckButton`, `zx.Panel`, or direct ARIA/data state.
|
|
733
|
-
- `gx.ui.HGroup`: use application CSS grid/flex layout.
|
|
734
|
-
- `gx.ui.Templates`: use native `<template>`, `zx.h()`, or explicit renderer functions.
|
|
735
|
-
- Legacy bugs are fixed, not emulated: `MenuButton.setStyle`'s typo, the dead
|
|
736
|
-
`MultiValueEditor` delete path, duplicate CheckButton `check`, the Request.File comma bug,
|
|
737
|
-
Tabbox's wrong root, and `Request.delete()` sending PUT. Update tests that depended on those
|
|
738
|
-
defects; `Http.delete()` sends DELETE.
|
|
739
|
-
- `_theme` class-map overrides, including `gx.ui.Table._theme`, are not supported. Override Zx
|
|
740
|
-
semantic tokens or documented component selectors instead.
|
|
741
|
-
- Legacy atomic utility CSS and pixel-identical legacy styling are not shipped. Keep temporary
|
|
742
|
-
app-owned utilities during migration, then replace them with layout CSS and semantic tokens.
|
|
743
|
-
- Fontello numeric glyph codes are not supported. Use the named icons accepted by `zx.icon()`.
|
|
744
|
-
- Full MooTools `getCoordinates()` objects are not emulated. Wrappers expose only
|
|
745
|
-
`width`/`height`/`top`/`left`; migrated code should use `getBoundingClientRect()`.
|
|
746
|
-
- `dispatchEvents` is not reproduced. Use native `dispatchEvent()` or a component's `emit()` and
|
|
747
|
-
listen for bubbling `zx-*` DOM events.
|
|
748
|
-
- `gx.Browser` is not reproduced. Prefer feature detection (`CSS.supports`, element/method
|
|
749
|
-
presence, media queries) over browser-name branching.
|