@ponchia/ui 0.13.0 → 0.15.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 +60 -0
- package/README.md +4 -4
- package/behaviors/command.d.ts +34 -13
- package/behaviors/command.d.ts.map +1 -1
- package/behaviors/command.js +195 -129
- package/classes/classes.json +19 -2
- package/classes/index.d.ts +16 -0
- package/classes/index.js +21 -2
- package/css/blocknote.css +14 -0
- package/css/content.css +34 -1
- package/css/feedback.css +95 -3
- package/css/fonts-inter.css +35 -0
- package/css/fonts-jetbrains-mono.css +49 -0
- package/css/primitives.css +13 -0
- package/css/site.css +0 -31
- package/css/tool.css +29 -0
- package/dist/bronto.css +1 -1
- package/dist/css/blocknote.css +1 -1
- package/dist/css/content.css +1 -1
- package/dist/css/feedback.css +1 -1
- package/dist/css/fonts-inter.css +1 -0
- package/dist/css/fonts-jetbrains-mono.css +1 -0
- package/dist/css/primitives.css +1 -1
- package/dist/css/site.css +1 -1
- package/dist/css/tool.css +1 -0
- package/docs/command.md +47 -7
- package/docs/compositions.md +2 -2
- package/docs/interop/blocknote.md +5 -0
- package/docs/package-contract.md +6 -1
- package/docs/reference.md +12 -1
- package/docs/reporting.md +8 -8
- package/docs/stability.md +4 -2
- package/docs/theming.md +8 -1
- package/docs/usage.md +55 -0
- package/fonts/OFL-Inter.txt +92 -0
- package/fonts/OFL-JetBrainsMono.txt +93 -0
- package/fonts/inter-variable-italic.woff2 +0 -0
- package/fonts/inter-variable.woff2 +0 -0
- package/fonts/jetbrains-mono-400-italic.woff2 +0 -0
- package/fonts/jetbrains-mono-400.woff2 +0 -0
- package/fonts/jetbrains-mono-700-italic.woff2 +0 -0
- package/fonts/jetbrains-mono-700.woff2 +0 -0
- package/llms.txt +1 -1
- package/package.json +7 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,66 @@
|
|
|
5
5
|
|> `^0` / `*` wildcard does **not** protect you. See README → Versioning, and
|
|
6
6
|
|> the deprecation policy in CONTRIBUTING.md.
|
|
7
7
|
|
|
8
|
+
## 0.15.0 — 2026-10-02
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **`ui-eyebrow--caps` and `ui-eyebrow--mono`**, the label voice of a tool
|
|
13
|
+
surface, taken from Spatial. `--caps` sets capitals on the wide tracking step,
|
|
14
|
+
for a label that names a region (a panel, a section, a gate); `--mono` sets the
|
|
15
|
+
mono face, for a label over data, code or a readout. Both compose with
|
|
16
|
+
`--muted` and `--sm`, and `ui.eyebrow({ caps, mono })` emits them. A tool that
|
|
17
|
+
restated this voice by hand can use the base eyebrow and keep only its spacing.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- **`css/blocknote.css` draws the nested-block guide in `--line`.** BlockNote
|
|
22
|
+
draws the rule beside a nested block in its side-menu colour, which the leaf
|
|
23
|
+
maps to `--text-dim` for the drag handle. In 0.13 that made the guide as dark as
|
|
24
|
+
a control: light mode went from BlockNote's `#cfcfcf` to `#686863`. The guide
|
|
25
|
+
now takes the border token and the handle keeps `--text-dim`. The rule mirrors
|
|
26
|
+
BlockNote 0.54's selector under `.bn-root`, and a test pins that shape.
|
|
27
|
+
|
|
28
|
+
## 0.14.0 — 2026-10-01
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- **`initCommand({ match })` and `initCommand({ headless: true })`.** `match(row,
|
|
33
|
+
query)` replaces the substring test, so a row can match on keywords it does
|
|
34
|
+
not show, or stay visible for every query (a "Search everything" row inside
|
|
35
|
+
the list, where the keyboard reaches it). Headless mode leaves the result set
|
|
36
|
+
to the host: Bronto hides nothing and reads the list live, so the host can
|
|
37
|
+
render new rows on every keystroke without re-running `initCommand`, and
|
|
38
|
+
Bronto keeps ids, roles, the active row, the keyboard and the empty state's
|
|
39
|
+
live region. Cleanup restores rows added after init as well.
|
|
40
|
+
- **`@ponchia/ui/css/tool.css`**, the default bundle for a tool, imported
|
|
41
|
+
instead of `@ponchia/ui`. It is core in core's order without the
|
|
42
|
+
`navigation`, `site`, `table` and `app` leaves, about 15 kB less.
|
|
43
|
+
- **`css/fonts-inter.css` and `css/fonts-jetbrains-mono.css`**, opt-in leaves
|
|
44
|
+
that ship the faces `--sans` and `--mono` name first. Before, the package
|
|
45
|
+
shipped Doto only and every OS drew its own fallback. Inter 4.1 is one
|
|
46
|
+
variable face per style, and JetBrains Mono 2.304 comes in regular, bold and
|
|
47
|
+
their italics. Both are unmodified upstream files under the SIL OFL 1.1, with
|
|
48
|
+
their licenses in `fonts/`.
|
|
49
|
+
- **`ui-body-state` and `ui-alert--band`** for node, panel and card bodies of
|
|
50
|
+
200–400px. A body state replaces the content: it fills the body and centres
|
|
51
|
+
one sentence. Use `ui.bodyState()` when empty or loading (`aria-busy` says
|
|
52
|
+
loading) and `ui.bodyState({ state: 'error' | 'stale' })` for a tone dot. A
|
|
53
|
+
band is a full-bleed, one-line alert above content the body still shows
|
|
54
|
+
(`ui.alert({ tone, band: true })`). See usage.md → "Small bodies".
|
|
55
|
+
|
|
56
|
+
### Changed
|
|
57
|
+
|
|
58
|
+
- The default bundle grows from 95.8 kB / 16.6 kB gzip to 97.1 kB / 16.8 kB,
|
|
59
|
+
for the body state, the band and `--alert-tone`. `css/tool.css` is 81.8 kB /
|
|
60
|
+
14.7 kB.
|
|
61
|
+
- Each `ui-alert--<tone>` also sets `--alert-tone`, which the band variant
|
|
62
|
+
tints with.
|
|
63
|
+
- `.ui-meta` (the date · author line) moved from `css/site.css` to
|
|
64
|
+
`css/content.css`, so the tool entry keeps it. The default bundle is
|
|
65
|
+
unchanged, since content follows site in its cascade. A direct import of
|
|
66
|
+
`css/site.css` that used `.ui-meta` also needs `css/content.css`.
|
|
67
|
+
|
|
8
68
|
## 0.13.0 — 2026-10-01
|
|
9
69
|
|
|
10
70
|
### Added
|
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/@ponchia/ui)
|
|
4
4
|
[](https://www.npmjs.com/package/@ponchia/ui#provenance)
|
|
5
5
|
[](https://github.com/Ponchia/bronto-ui/blob/main/package.json)
|
|
6
|
-
[](https://github.com/Ponchia/bronto-ui/blob/main/scripts/check-dist.mjs)
|
|
7
7
|
[](https://github.com/Ponchia/bronto-ui/actions/workflows/ci.yml)
|
|
8
8
|
[](https://scorecard.dev/viewer/?uri=github.com/Ponchia/bronto-ui)
|
|
9
9
|
[](https://github.com/Ponchia/bronto-ui/blob/main/LICENSE)
|
|
@@ -93,12 +93,12 @@ Or drop it in with no build step, straight from a CDN (replace the version only
|
|
|
93
93
|
when deliberately upgrading across a breaking pre-1.0 minor):
|
|
94
94
|
|
|
95
95
|
```html
|
|
96
|
-
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ponchia/ui@0.
|
|
96
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ponchia/ui@0.15.0/dist/bronto.css">
|
|
97
97
|
```
|
|
98
98
|
|
|
99
99
|
## Quick start
|
|
100
100
|
|
|
101
|
-
**1. Load the CSS.** One flattened, minified default CSS bundle — the standard component set, one request (~
|
|
101
|
+
**1. Load the CSS.** One flattened, minified default CSS bundle — the standard component set, one request (~97 kB raw / ~17 kB gzip) — that is `dist/bronto.css`, not the whole package tarball:
|
|
102
102
|
|
|
103
103
|
```css
|
|
104
104
|
@import '@ponchia/ui'; /* via a bundler */
|
|
@@ -256,4 +256,4 @@ Release candidates publish to the `next` dist-tag, never to `latest` — opt in
|
|
|
256
256
|
|
|
257
257
|
[MIT](https://github.com/Ponchia/bronto-ui/blob/main/LICENSE) © Ponchia.
|
|
258
258
|
|
|
259
|
-
The bundled
|
|
259
|
+
The bundled fonts are licensed separately under the [SIL Open Font License 1.1](https://github.com/Ponchia/bronto-ui/blob/main/fonts/OFL.txt): **Doto** (`fonts/doto-*.woff2`, © 2024 The Doto Project Authors, `fonts/OFL.txt`), and the opt-in **Inter** (`fonts/inter-*.woff2`, © 2016 The Inter Project Authors, `fonts/OFL-Inter.txt`) and **JetBrains Mono** (`fonts/jetbrains-mono-*.woff2`, © 2020 The JetBrains Mono Project Authors, `fonts/OFL-JetBrainsMono.txt`).
|
package/behaviors/command.d.ts
CHANGED
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @typedef {object} CommandSelectDetail
|
|
3
|
-
* @property {string} value The chosen command's value.
|
|
4
|
-
* @property {string} label The chosen command's visible label.
|
|
5
|
-
*/
|
|
6
1
|
/**
|
|
7
2
|
* Command palette — filter + keyboard-navigate a DOM-authored command list.
|
|
8
3
|
* The CSS shell (`.ui-command`) is opt-in; this wires the listbox behavior the
|
|
@@ -15,19 +10,21 @@
|
|
|
15
10
|
* and a list (`.ui-command__list`) of `.ui-command__item` rows (optional
|
|
16
11
|
* `data-value`), interleaved with `.ui-command__group` labels and an optional
|
|
17
12
|
* `.ui-command__empty`. The behavior owns ids, `role=combobox/listbox/option`,
|
|
18
|
-
* `aria-activedescendant`, a roving active item,
|
|
19
|
-
* empty groups), keyboard list
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* instance; returns
|
|
13
|
+
* `aria-activedescendant`, a roving active item, filtering (substring by
|
|
14
|
+
* default, `match` to replace it, hiding empty groups), keyboard list
|
|
15
|
+
* navigation (Down/Up/Enter/Escape), and pointer select. It emits
|
|
16
|
+
* `bronto:command:select` ({ detail: { value, label } }) on choose and
|
|
17
|
+
* `bronto:command:close` on Escape. SSR-safe, idempotent per instance; returns
|
|
18
|
+
* a cleanup function.
|
|
23
19
|
*
|
|
24
20
|
* Items are read from the DOM at init; re-run initCommand after replacing the
|
|
25
|
-
* command list so filtering/navigation see the current nodes
|
|
21
|
+
* command list so filtering/navigation see the current nodes — or pass
|
|
22
|
+
* `headless: true` and render the results yourself.
|
|
26
23
|
*
|
|
27
|
-
* @param {
|
|
24
|
+
* @param {CommandOpts} [opts]
|
|
28
25
|
* @returns {import('./internal.js').Cleanup}
|
|
29
26
|
*/
|
|
30
|
-
export function initCommand({ root }?:
|
|
27
|
+
export function initCommand({ root, match, headless }?: CommandOpts): import("./internal.js").Cleanup;
|
|
31
28
|
export type CommandSelectDetail = {
|
|
32
29
|
/**
|
|
33
30
|
* The chosen command's value.
|
|
@@ -38,4 +35,28 @@ export type CommandSelectDetail = {
|
|
|
38
35
|
*/
|
|
39
36
|
label: string;
|
|
40
37
|
};
|
|
38
|
+
export type CommandOpts = {
|
|
39
|
+
/**
|
|
40
|
+
* Event-delegation root; also scopes which palettes are queried. Default: `document`.
|
|
41
|
+
* `null` means a scope was requested but is not ready yet, so the behavior no-ops.
|
|
42
|
+
*/
|
|
43
|
+
root?: Element | Document | null | undefined;
|
|
44
|
+
/**
|
|
45
|
+
* Decides whether an item stays visible for a query. `query` is the trimmed
|
|
46
|
+
* input, lower-cased in the palette's locale; an empty query shows every item.
|
|
47
|
+
* Default: the item's text contains the query. Use it to match on keywords
|
|
48
|
+
* (`data-keywords`, say) or to keep a row such as "Search everything" visible
|
|
49
|
+
* for every query. Not called in headless mode.
|
|
50
|
+
*/
|
|
51
|
+
match?: ((item: HTMLElement, query: string) => boolean) | undefined;
|
|
52
|
+
/**
|
|
53
|
+
* The host renders the result set. Bronto never hides an item or group; it
|
|
54
|
+
* reads the list live, so the host can replace rows on every keystroke
|
|
55
|
+
* without re-running `initCommand`. It still owns ids, roles, the roving
|
|
56
|
+
* active item, the keyboard, pointer select and the empty state's live
|
|
57
|
+
* region. After the query changes, the first row the host renders becomes
|
|
58
|
+
* active.
|
|
59
|
+
*/
|
|
60
|
+
headless?: boolean | undefined;
|
|
61
|
+
};
|
|
41
62
|
//# sourceMappingURL=command.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"command.d.ts","sourceRoot":"","sources":["command.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"command.d.ts","sourceRoot":"","sources":["command.js"],"names":[],"mappings":"AA6GA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wDAHW,WAAW,GACT,OAAO,eAAe,EAAE,OAAO,CAuO3C;;;;;WA7Ua,MAAM;;;;WACN,MAAM;;;;;;;;;;;;;;;oBAQC,WAAW,SAAS,MAAM,KAAK,OAAO"}
|
package/behaviors/command.js
CHANGED
|
@@ -33,6 +33,80 @@ const lowerForSearch = (value, locale) => {
|
|
|
33
33
|
* @property {string} label The chosen command's visible label.
|
|
34
34
|
*/
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* @typedef {object} CommandOpts
|
|
38
|
+
* @property {Document | Element | null} [root]
|
|
39
|
+
* Event-delegation root; also scopes which palettes are queried. Default: `document`.
|
|
40
|
+
* `null` means a scope was requested but is not ready yet, so the behavior no-ops.
|
|
41
|
+
* @property {(item: HTMLElement, query: string) => boolean} [match]
|
|
42
|
+
* Decides whether an item stays visible for a query. `query` is the trimmed
|
|
43
|
+
* input, lower-cased in the palette's locale; an empty query shows every item.
|
|
44
|
+
* Default: the item's text contains the query. Use it to match on keywords
|
|
45
|
+
* (`data-keywords`, say) or to keep a row such as "Search everything" visible
|
|
46
|
+
* for every query. Not called in headless mode.
|
|
47
|
+
* @property {boolean} [headless]
|
|
48
|
+
* The host renders the result set. Bronto never hides an item or group; it
|
|
49
|
+
* reads the list live, so the host can replace rows on every keystroke
|
|
50
|
+
* without re-running `initCommand`. It still owns ids, roles, the roving
|
|
51
|
+
* active item, the keyboard, pointer select and the empty state's live
|
|
52
|
+
* region. After the query changes, the first row the host renders becomes
|
|
53
|
+
* active.
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
const ITEM = '.ui-command__item, [role="option"]';
|
|
57
|
+
|
|
58
|
+
/** The first non-blank text node under `el`, depth first. */
|
|
59
|
+
function firstTextNode(el) {
|
|
60
|
+
for (const node of el.childNodes) {
|
|
61
|
+
if (node.nodeType === 3 && node.nodeValue.trim()) return node;
|
|
62
|
+
if (node.nodeType === 1) {
|
|
63
|
+
const child = firstTextNode(node);
|
|
64
|
+
if (child) return child;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Re-set an element's first text so a live region announces it again. */
|
|
71
|
+
function refreshLiveText(el) {
|
|
72
|
+
const node = firstTextNode(el);
|
|
73
|
+
if (!node) return;
|
|
74
|
+
const text = node.nodeValue;
|
|
75
|
+
node.nodeValue = '';
|
|
76
|
+
node.nodeValue = text;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Records everything a palette changes the first time it is touched, so
|
|
81
|
+
* cleanup restores rows the host added after init as well.
|
|
82
|
+
*/
|
|
83
|
+
function changeRecorder() {
|
|
84
|
+
const touched = new Map();
|
|
85
|
+
const touch = (el, names) => {
|
|
86
|
+
let saved = touched.get(el);
|
|
87
|
+
if (!saved) {
|
|
88
|
+
saved = { hidden: el.hidden, active: el.classList.contains('is-active'), attrs: {} };
|
|
89
|
+
touched.set(el, saved);
|
|
90
|
+
}
|
|
91
|
+
for (const name of names) {
|
|
92
|
+
if (name in saved.attrs) continue;
|
|
93
|
+
saved.attrs[name] = { had: el.hasAttribute(name), value: el.getAttribute(name) };
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
const restore = () => {
|
|
97
|
+
for (const [el, saved] of touched) {
|
|
98
|
+
el.hidden = saved.hidden;
|
|
99
|
+
el.classList.toggle('is-active', saved.active);
|
|
100
|
+
for (const [name, attr] of Object.entries(saved.attrs)) {
|
|
101
|
+
if (attr.had) el.setAttribute(name, attr.value);
|
|
102
|
+
else el.removeAttribute(name);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
touched.clear();
|
|
106
|
+
};
|
|
107
|
+
return { touch, restore };
|
|
108
|
+
}
|
|
109
|
+
|
|
36
110
|
/**
|
|
37
111
|
* Command palette — filter + keyboard-navigate a DOM-authored command list.
|
|
38
112
|
* The CSS shell (`.ui-command`) is opt-in; this wires the listbox behavior the
|
|
@@ -45,178 +119,148 @@ const lowerForSearch = (value, locale) => {
|
|
|
45
119
|
* and a list (`.ui-command__list`) of `.ui-command__item` rows (optional
|
|
46
120
|
* `data-value`), interleaved with `.ui-command__group` labels and an optional
|
|
47
121
|
* `.ui-command__empty`. The behavior owns ids, `role=combobox/listbox/option`,
|
|
48
|
-
* `aria-activedescendant`, a roving active item,
|
|
49
|
-
* empty groups), keyboard list
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* instance; returns
|
|
122
|
+
* `aria-activedescendant`, a roving active item, filtering (substring by
|
|
123
|
+
* default, `match` to replace it, hiding empty groups), keyboard list
|
|
124
|
+
* navigation (Down/Up/Enter/Escape), and pointer select. It emits
|
|
125
|
+
* `bronto:command:select` ({ detail: { value, label } }) on choose and
|
|
126
|
+
* `bronto:command:close` on Escape. SSR-safe, idempotent per instance; returns
|
|
127
|
+
* a cleanup function.
|
|
53
128
|
*
|
|
54
129
|
* Items are read from the DOM at init; re-run initCommand after replacing the
|
|
55
|
-
* command list so filtering/navigation see the current nodes
|
|
130
|
+
* command list so filtering/navigation see the current nodes — or pass
|
|
131
|
+
* `headless: true` and render the results yourself.
|
|
56
132
|
*
|
|
57
|
-
* @param {
|
|
133
|
+
* @param {CommandOpts} [opts]
|
|
58
134
|
* @returns {import('./internal.js').Cleanup}
|
|
59
135
|
*/
|
|
60
|
-
export function initCommand({ root } = {}) {
|
|
136
|
+
export function initCommand({ root, match, headless = false } = {}) {
|
|
61
137
|
if (!hasDom()) return noop;
|
|
62
138
|
const host = resolveHost(root);
|
|
63
139
|
if (!host) return noop;
|
|
64
140
|
const palettes = collectHosts(host, '[data-bronto-command]');
|
|
65
141
|
const cleanups = [];
|
|
66
142
|
|
|
67
|
-
const snapshotAttrs = (el, names) => {
|
|
68
|
-
const out = {};
|
|
69
|
-
for (const name of names) {
|
|
70
|
-
out[name] = {
|
|
71
|
-
had: el.hasAttribute(name),
|
|
72
|
-
value: el.getAttribute(name),
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
return out;
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
const restoreAttrs = (el, attrs) => {
|
|
79
|
-
for (const [name, attr] of Object.entries(attrs)) {
|
|
80
|
-
if (attr.had) el.setAttribute(name, attr.value);
|
|
81
|
-
else el.removeAttribute(name);
|
|
82
|
-
}
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
const firstTextNode = (el) => {
|
|
86
|
-
for (const node of el.childNodes) {
|
|
87
|
-
if (node.nodeType === 3 && node.nodeValue.trim()) return node;
|
|
88
|
-
if (node.nodeType === 1) {
|
|
89
|
-
const child = firstTextNode(node);
|
|
90
|
-
if (child) return child;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
return null;
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
const refreshLiveText = (el) => {
|
|
97
|
-
const node = firstTextNode(el);
|
|
98
|
-
if (!node) return;
|
|
99
|
-
const text = node.nodeValue;
|
|
100
|
-
node.nodeValue = '';
|
|
101
|
-
node.nodeValue = text;
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
const prepareEmptyState = (el) => {
|
|
105
|
-
if (!el) return;
|
|
106
|
-
el.setAttribute('role', 'status');
|
|
107
|
-
el.setAttribute('aria-live', 'polite');
|
|
108
|
-
};
|
|
109
|
-
|
|
110
143
|
for (const box of palettes) {
|
|
111
144
|
const input = box.querySelector('.ui-command__input, input');
|
|
112
145
|
const list = box.querySelector('.ui-command__list, [role="listbox"]');
|
|
113
146
|
if (!input || !list) continue;
|
|
114
|
-
const empty = box.querySelector('.ui-command__empty');
|
|
115
|
-
const items = [...list.querySelectorAll('.ui-command__item, [role="option"]')];
|
|
116
|
-
const groups = [...list.querySelectorAll('.ui-command__group')];
|
|
117
147
|
const locale = localeOf(box);
|
|
148
|
+
const initialItems = [...list.querySelectorAll(ITEM)];
|
|
149
|
+
const initialGroups = [...list.querySelectorAll('.ui-command__group')];
|
|
150
|
+
// Headless reads the host's current rows; otherwise the rows at init.
|
|
151
|
+
const itemsNow = () => (headless ? [...list.querySelectorAll(ITEM)] : initialItems);
|
|
152
|
+
const groupsNow = () =>
|
|
153
|
+
headless ? [...list.querySelectorAll('.ui-command__group')] : initialGroups;
|
|
154
|
+
const emptyNow = () => box.querySelector('.ui-command__empty');
|
|
118
155
|
|
|
119
|
-
const
|
|
120
|
-
input: snapshotAttrs(input, [
|
|
121
|
-
'role',
|
|
122
|
-
'aria-controls',
|
|
123
|
-
'aria-autocomplete',
|
|
124
|
-
'aria-expanded',
|
|
125
|
-
'aria-activedescendant',
|
|
126
|
-
'autocomplete',
|
|
127
|
-
]),
|
|
128
|
-
list: snapshotAttrs(list, ['id', 'role']),
|
|
129
|
-
empty: empty
|
|
130
|
-
? {
|
|
131
|
-
hidden: empty.hidden,
|
|
132
|
-
attrs: snapshotAttrs(empty, ['role', 'aria-live']),
|
|
133
|
-
}
|
|
134
|
-
: null,
|
|
135
|
-
groups: groups.map((g) => ({
|
|
136
|
-
el: g,
|
|
137
|
-
hidden: g.hidden,
|
|
138
|
-
attrs: snapshotAttrs(g, ['role']),
|
|
139
|
-
})),
|
|
140
|
-
items: items.map((it) => ({
|
|
141
|
-
el: it,
|
|
142
|
-
hidden: it.hidden,
|
|
143
|
-
active: it.classList.contains('is-active'),
|
|
144
|
-
attrs: snapshotAttrs(it, ['id', 'role', 'aria-selected']),
|
|
145
|
-
})),
|
|
146
|
-
});
|
|
156
|
+
const { touch, restore } = changeRecorder();
|
|
147
157
|
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
158
|
+
const optionIdBase = `bronto-cmd-opt-${nextFieldUid()}`;
|
|
159
|
+
let nextOption = 0;
|
|
160
|
+
const prepared = new WeakSet();
|
|
161
|
+
const prepareItem = (it) => {
|
|
162
|
+
if (prepared.has(it)) return;
|
|
163
|
+
prepared.add(it);
|
|
164
|
+
touch(it, ['id', 'role', 'aria-selected']);
|
|
165
|
+
if (!it.id) it.id = `${optionIdBase}-${nextOption++}`;
|
|
166
|
+
it.setAttribute('role', 'option');
|
|
167
|
+
};
|
|
168
|
+
const prepareGroup = (g) => {
|
|
169
|
+
if (prepared.has(g)) return;
|
|
170
|
+
prepared.add(g);
|
|
171
|
+
touch(g, ['role']);
|
|
172
|
+
g.setAttribute('role', 'presentation');
|
|
173
|
+
};
|
|
174
|
+
const prepareEmpty = (el) => {
|
|
175
|
+
if (!el || prepared.has(el)) return;
|
|
176
|
+
prepared.add(el);
|
|
177
|
+
touch(el, ['role', 'aria-live']);
|
|
178
|
+
el.setAttribute('role', 'status');
|
|
179
|
+
el.setAttribute('aria-live', 'polite');
|
|
180
|
+
};
|
|
181
|
+
const prepareRows = () => {
|
|
182
|
+
itemsNow().forEach(prepareItem);
|
|
183
|
+
groupsNow().forEach(prepareGroup);
|
|
184
|
+
prepareEmpty(emptyNow());
|
|
164
185
|
};
|
|
165
186
|
|
|
166
|
-
let
|
|
167
|
-
const visible = () =>
|
|
187
|
+
let activeItem = null;
|
|
188
|
+
const visible = () => itemsNow().filter((it) => !it.hidden);
|
|
168
189
|
|
|
169
190
|
const setActive = (item) => {
|
|
170
|
-
|
|
191
|
+
for (const it of itemsNow()) {
|
|
192
|
+
prepareItem(it);
|
|
171
193
|
it.classList.toggle('is-active', it === item);
|
|
172
194
|
it.setAttribute('aria-selected', String(it === item));
|
|
173
|
-
}
|
|
195
|
+
}
|
|
196
|
+
activeItem = item;
|
|
174
197
|
if (item) {
|
|
175
|
-
active = items.indexOf(item);
|
|
176
198
|
input.setAttribute('aria-activedescendant', item.id);
|
|
177
199
|
scrollIntoViewSafe(item);
|
|
178
200
|
} else {
|
|
179
|
-
active = -1;
|
|
180
201
|
input.removeAttribute('aria-activedescendant');
|
|
181
202
|
}
|
|
182
203
|
};
|
|
183
204
|
|
|
184
205
|
// Hide a group whose items are all filtered out.
|
|
185
206
|
const syncGroups = () => {
|
|
186
|
-
for (const g of
|
|
207
|
+
for (const g of groupsNow()) {
|
|
187
208
|
let any = false;
|
|
188
209
|
for (
|
|
189
210
|
let n = g.nextElementSibling;
|
|
190
211
|
n && !n.matches('.ui-command__group');
|
|
191
212
|
n = n.nextElementSibling
|
|
192
213
|
) {
|
|
193
|
-
if (n.matches(
|
|
214
|
+
if (n.matches(ITEM) && !n.hidden) any = true;
|
|
194
215
|
}
|
|
195
216
|
g.hidden = !any;
|
|
196
217
|
}
|
|
197
218
|
};
|
|
198
219
|
|
|
220
|
+
const matches = match
|
|
221
|
+
? (it, q) => !q || Boolean(match(it, q))
|
|
222
|
+
: (it, q) => !q || lowerForSearch(it.textContent, locale).includes(q);
|
|
223
|
+
|
|
199
224
|
const filter = () => {
|
|
200
225
|
const q = lowerForSearch(input.value.trim(), locale);
|
|
201
226
|
let any = false;
|
|
202
|
-
for (const it of
|
|
203
|
-
const
|
|
204
|
-
it.hidden = !
|
|
205
|
-
if (
|
|
227
|
+
for (const it of itemsNow()) {
|
|
228
|
+
const shown = matches(it, q);
|
|
229
|
+
it.hidden = !shown;
|
|
230
|
+
if (shown) any = true;
|
|
206
231
|
}
|
|
207
232
|
syncGroups();
|
|
233
|
+
const empty = emptyNow();
|
|
208
234
|
if (empty) {
|
|
209
235
|
empty.hidden = any;
|
|
210
236
|
if (!any) refreshLiveText(empty);
|
|
211
237
|
}
|
|
212
|
-
|
|
213
|
-
|
|
238
|
+
setActive(visible()[0] || null);
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
// Headless: the host re-renders after the query changes, so the first row
|
|
242
|
+
// it renders becomes active then; a row that disappears hands its place to
|
|
243
|
+
// the first one left. An empty state the host reveals is announced.
|
|
244
|
+
let queryChanged = false;
|
|
245
|
+
let emptyShown = false;
|
|
246
|
+
const onRowsChanged = () => {
|
|
247
|
+
prepareRows();
|
|
248
|
+
const rows = visible();
|
|
249
|
+
if (queryChanged || !activeItem || !rows.includes(activeItem)) {
|
|
250
|
+
queryChanged = false;
|
|
251
|
+
setActive(rows[0] || null);
|
|
252
|
+
}
|
|
253
|
+
const empty = emptyNow();
|
|
254
|
+
const shown = Boolean(empty && !empty.hidden);
|
|
255
|
+
if (shown && !emptyShown) refreshLiveText(empty);
|
|
256
|
+
emptyShown = shown;
|
|
214
257
|
};
|
|
215
258
|
|
|
216
259
|
const move = (delta) => {
|
|
260
|
+
queryChanged = false;
|
|
217
261
|
const vis = visible();
|
|
218
262
|
if (!vis.length) return;
|
|
219
|
-
setActive(vis[wrapIndex(vis.indexOf(
|
|
263
|
+
setActive(vis[wrapIndex(vis.indexOf(activeItem), delta, vis.length)]);
|
|
220
264
|
};
|
|
221
265
|
|
|
222
266
|
const choose = (item) => {
|
|
@@ -234,7 +278,10 @@ export function initCommand({ root } = {}) {
|
|
|
234
278
|
);
|
|
235
279
|
};
|
|
236
280
|
|
|
237
|
-
const onInput = () =>
|
|
281
|
+
const onInput = () => {
|
|
282
|
+
if (headless) queryChanged = true;
|
|
283
|
+
else filter();
|
|
284
|
+
};
|
|
238
285
|
const onKey = (e) => {
|
|
239
286
|
switch (e.key) {
|
|
240
287
|
case 'ArrowDown':
|
|
@@ -246,8 +293,8 @@ export function initCommand({ root } = {}) {
|
|
|
246
293
|
move(-1);
|
|
247
294
|
break;
|
|
248
295
|
case 'Enter':
|
|
249
|
-
if (
|
|
250
|
-
choose(
|
|
296
|
+
if (activeItem && activeItem.isConnected && !activeItem.hidden) {
|
|
297
|
+
choose(activeItem);
|
|
251
298
|
e.preventDefault();
|
|
252
299
|
}
|
|
253
300
|
break;
|
|
@@ -259,21 +306,23 @@ export function initCommand({ root } = {}) {
|
|
|
259
306
|
}
|
|
260
307
|
};
|
|
261
308
|
const onClick = (e) => {
|
|
262
|
-
const item = closestSafe(e.target,
|
|
309
|
+
const item = closestSafe(e.target, ITEM);
|
|
263
310
|
if (item && list.contains(item)) choose(item);
|
|
264
311
|
};
|
|
265
312
|
|
|
266
313
|
const bound = bindOnce(box, 'command', () => {
|
|
267
|
-
|
|
314
|
+
touch(input, [
|
|
315
|
+
'role',
|
|
316
|
+
'aria-controls',
|
|
317
|
+
'aria-autocomplete',
|
|
318
|
+
'aria-expanded',
|
|
319
|
+
'aria-activedescendant',
|
|
320
|
+
'autocomplete',
|
|
321
|
+
]);
|
|
322
|
+
touch(list, ['id', 'role']);
|
|
268
323
|
const listId = list.id || (list.id = `bronto-cmd-${nextFieldUid()}`);
|
|
269
|
-
|
|
270
|
-
items.forEach((it, i) => {
|
|
271
|
-
if (!it.id) it.id = `${optionIdBase}-${i}`;
|
|
272
|
-
it.setAttribute('role', 'option');
|
|
273
|
-
});
|
|
274
|
-
groups.forEach((g) => g.setAttribute('role', 'presentation'));
|
|
324
|
+
prepareRows();
|
|
275
325
|
list.setAttribute('role', 'listbox');
|
|
276
|
-
prepareEmptyState(empty);
|
|
277
326
|
input.setAttribute('role', 'combobox');
|
|
278
327
|
input.setAttribute('aria-controls', listId);
|
|
279
328
|
input.setAttribute('aria-autocomplete', 'list');
|
|
@@ -282,14 +331,31 @@ export function initCommand({ root } = {}) {
|
|
|
282
331
|
input.addEventListener('input', onInput);
|
|
283
332
|
input.addEventListener('keydown', onKey);
|
|
284
333
|
list.addEventListener('click', onClick);
|
|
285
|
-
|
|
286
|
-
|
|
334
|
+
let observer = null;
|
|
335
|
+
if (headless) {
|
|
336
|
+
// The palette's own window, so a palette in another realm still observes.
|
|
337
|
+
const Observer = box.ownerDocument?.defaultView?.MutationObserver;
|
|
338
|
+
observer = Observer ? new Observer(onRowsChanged) : null;
|
|
339
|
+
observer?.observe(box, {
|
|
340
|
+
childList: true,
|
|
341
|
+
subtree: true,
|
|
342
|
+
attributes: true,
|
|
343
|
+
attributeFilter: ['hidden'],
|
|
344
|
+
});
|
|
345
|
+
const empty = emptyNow();
|
|
346
|
+
emptyShown = Boolean(empty && !empty.hidden);
|
|
347
|
+
setActive(visible()[0] || null);
|
|
348
|
+
} else {
|
|
349
|
+
// Seed the initial active item (first visible).
|
|
350
|
+
filter();
|
|
351
|
+
}
|
|
287
352
|
return () => {
|
|
353
|
+
observer?.disconnect();
|
|
288
354
|
input.removeEventListener('input', onInput);
|
|
289
355
|
input.removeEventListener('keydown', onKey);
|
|
290
356
|
list.removeEventListener('click', onClick);
|
|
291
|
-
|
|
292
|
-
|
|
357
|
+
restore();
|
|
358
|
+
activeItem = null;
|
|
293
359
|
};
|
|
294
360
|
});
|
|
295
361
|
cleanups.push(bound);
|
package/classes/classes.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$comment": "@ponchia/ui class vocabulary as language-neutral data — validate emitted markup without executing the ESM cls map or parsing the .d.ts. Generated from classes/index.js — do not edit by hand; run `npm run classes:json:build`. Drift-checked in CI. `groups[].base` is null for a parts-only namespace (no standalone base class — do NOT emit it). A modifier whose name contains `__` (e.g. `ui-spark__bar--pos`) attaches to THAT part, not the base host. `states` is the author-applied `is-*` hooks (runtime/behavior-managed hooks are excluded); `behaviorAttributes` are the `data-bronto-*` wiring hooks the optional behaviors delegate on; `requiredAria` is the role/aria a generator must emit per component. `states` + `customProperties` are documented in docs/reference.md and ship outside `cls` by design.",
|
|
3
3
|
"counts": {
|
|
4
|
-
"classes":
|
|
5
|
-
"groups":
|
|
4
|
+
"classes": 702,
|
|
5
|
+
"groups": 188
|
|
6
6
|
},
|
|
7
7
|
"groups": {
|
|
8
8
|
"ui-accordion": {
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"base": "ui-alert",
|
|
24
24
|
"modifiers": [
|
|
25
25
|
"ui-alert--accent",
|
|
26
|
+
"ui-alert--band",
|
|
26
27
|
"ui-alert--danger",
|
|
27
28
|
"ui-alert--info",
|
|
28
29
|
"ui-alert--success",
|
|
@@ -195,6 +196,14 @@
|
|
|
195
196
|
],
|
|
196
197
|
"parts": []
|
|
197
198
|
},
|
|
199
|
+
"ui-body-state": {
|
|
200
|
+
"base": "ui-body-state",
|
|
201
|
+
"modifiers": [
|
|
202
|
+
"ui-body-state--error",
|
|
203
|
+
"ui-body-state--stale"
|
|
204
|
+
],
|
|
205
|
+
"parts": []
|
|
206
|
+
},
|
|
198
207
|
"ui-bracket-note": {
|
|
199
208
|
"base": "ui-bracket-note",
|
|
200
209
|
"modifiers": [
|
|
@@ -611,6 +620,8 @@
|
|
|
611
620
|
"ui-eyebrow": {
|
|
612
621
|
"base": "ui-eyebrow",
|
|
613
622
|
"modifiers": [
|
|
623
|
+
"ui-eyebrow--caps",
|
|
624
|
+
"ui-eyebrow--mono",
|
|
614
625
|
"ui-eyebrow--muted",
|
|
615
626
|
"ui-eyebrow--sm"
|
|
616
627
|
],
|
|
@@ -1604,6 +1615,7 @@
|
|
|
1604
1615
|
"ui-activity",
|
|
1605
1616
|
"ui-alert",
|
|
1606
1617
|
"ui-alert--accent",
|
|
1618
|
+
"ui-alert--band",
|
|
1607
1619
|
"ui-alert--danger",
|
|
1608
1620
|
"ui-alert--info",
|
|
1609
1621
|
"ui-alert--success",
|
|
@@ -1685,6 +1697,9 @@
|
|
|
1685
1697
|
"ui-badge--muted",
|
|
1686
1698
|
"ui-badge--success",
|
|
1687
1699
|
"ui-badge--warning",
|
|
1700
|
+
"ui-body-state",
|
|
1701
|
+
"ui-body-state--error",
|
|
1702
|
+
"ui-body-state--stale",
|
|
1688
1703
|
"ui-bracket-note",
|
|
1689
1704
|
"ui-bracket-note--accent",
|
|
1690
1705
|
"ui-bracket-note--danger",
|
|
@@ -1878,6 +1893,8 @@
|
|
|
1878
1893
|
"ui-evidence-item__window",
|
|
1879
1894
|
"ui-evidence-ledger",
|
|
1880
1895
|
"ui-eyebrow",
|
|
1896
|
+
"ui-eyebrow--caps",
|
|
1897
|
+
"ui-eyebrow--mono",
|
|
1881
1898
|
"ui-eyebrow--muted",
|
|
1882
1899
|
"ui-eyebrow--sm",
|
|
1883
1900
|
"ui-field",
|