@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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Component } from '../../core/component.js';
|
|
2
|
-
import { h } from '../../core/dom.js';
|
|
2
|
+
import { h, safeHref } from '../../core/dom.js';
|
|
3
3
|
import { icon } from '../../core/icons.js';
|
|
4
4
|
import { uid } from '../../core/util.js';
|
|
5
5
|
|
|
@@ -10,10 +10,15 @@ import { uid } from '../../core/util.js';
|
|
|
10
10
|
* @property {string} [description] Secondary text.
|
|
11
11
|
* @property {string|string[]} [keywords] Additional search text.
|
|
12
12
|
* @property {string} [group] Result group.
|
|
13
|
-
* @property {string} [icon]
|
|
13
|
+
* @property {string|Node|((item: LauncherItem) => Node|null)} [icon] Decorative icon name, node, or factory.
|
|
14
14
|
* @property {string|number} [badge] Compact metadata.
|
|
15
|
+
* @property {'application'|'record'|'action'} [kind='action'] Tile or row presentation.
|
|
16
|
+
* @property {boolean} [current=false] Whether this is the current application or destination.
|
|
17
|
+
* @property {'always'|'empty'|'query'} [when='always'] Query state in which the item is visible.
|
|
18
|
+
* @property {number} [groupOrder] Explicit group order.
|
|
19
|
+
* @property {number} [itemOrder] Stable order within equally ranked results.
|
|
15
20
|
* @property {unknown} [value] Emitted value; defaults to `id`.
|
|
16
|
-
* @property {string} [href] Native link target.
|
|
21
|
+
* @property {string} [href] Native link target; executable/data schemes are disabled.
|
|
17
22
|
* @property {string} [target] Native link browsing context.
|
|
18
23
|
* @property {() => void} [invoke] Application-owned callback for non-link actions.
|
|
19
24
|
* @property {boolean|number} [pinned] Pinned state or explicit pinned order.
|
|
@@ -24,6 +29,8 @@ import { uid } from '../../core/util.js';
|
|
|
24
29
|
* @property {string} id Source identifier.
|
|
25
30
|
* @property {string} [label] Default group label.
|
|
26
31
|
* @property {number} [minQuery] Per-source minimum query length.
|
|
32
|
+
* @property {'always'|'empty'|'query'} [when='always'] Query state in which the source runs.
|
|
33
|
+
* @property {'rank'|'source'} [order='rank'] Whether Zx ranks results or preserves source order.
|
|
27
34
|
* @property {(query: string, context: {signal: AbortSignal}) => Promise<LauncherItem[]>|LauncherItem[]} load Loader.
|
|
28
35
|
*/
|
|
29
36
|
/**
|
|
@@ -36,9 +43,12 @@ import { uid } from '../../core/util.js';
|
|
|
36
43
|
* @property {number} [maxResults=100] Maximum combined results.
|
|
37
44
|
* @property {string} [placeholder='Search applications and records'] Search-field placeholder.
|
|
38
45
|
* @property {string} [label='Launcher'] Dialog and field label.
|
|
46
|
+
* @property {string} [closeLabel='Close launcher'] Close-button accessible label.
|
|
47
|
+
* @property {string} [resultsLabel='Launcher results'] Results-region accessible label.
|
|
39
48
|
* @property {string} [emptyText='No results'] Empty-state text.
|
|
40
49
|
* @property {string} [loadingText='Searching…'] Loading-state text.
|
|
41
50
|
* @property {'mod+k'|false} [shortcut='mod+k'] Optional global keyboard shortcut.
|
|
51
|
+
* @property {false|{move?: string, open?: string, close?: string}} [hints] Visible keyboard hints, or false.
|
|
42
52
|
* @property {(event: CustomEvent<LauncherSelectDetail>) => void} [onselect] Preventable selection listener.
|
|
43
53
|
* @property {(event: CustomEvent<{query: string}>) => void} [onquery] Query-change listener.
|
|
44
54
|
* @property {(event: CustomEvent<Record<string, never>>) => void} [onopen] Open listener.
|
|
@@ -70,9 +80,12 @@ export class Launcher extends Component {
|
|
|
70
80
|
maxResults: 100,
|
|
71
81
|
placeholder: 'Search applications and records',
|
|
72
82
|
label: 'Launcher',
|
|
83
|
+
closeLabel: 'Close launcher',
|
|
84
|
+
resultsLabel: 'Launcher results',
|
|
73
85
|
emptyText: 'No results',
|
|
74
86
|
loadingText: 'Searching…',
|
|
75
|
-
shortcut: 'mod+k'
|
|
87
|
+
shortcut: 'mod+k',
|
|
88
|
+
hints: { move: 'Move', open: 'Open', close: 'Close' }
|
|
76
89
|
};
|
|
77
90
|
|
|
78
91
|
/**
|
|
@@ -100,6 +113,7 @@ export class Launcher extends Component {
|
|
|
100
113
|
this._sources = normalizeSources(this.options.sources);
|
|
101
114
|
this._sourceItems = new Map();
|
|
102
115
|
this._query = String(this.options.query ?? '');
|
|
116
|
+
this._renderedQuery = null;
|
|
103
117
|
this._results = [];
|
|
104
118
|
this._active = -1;
|
|
105
119
|
this._request = null;
|
|
@@ -127,7 +141,7 @@ export class Launcher extends Component {
|
|
|
127
141
|
ref: 'close',
|
|
128
142
|
class: 'zx-icon-btn zx-launcher__close',
|
|
129
143
|
type: 'button',
|
|
130
|
-
ariaLabel:
|
|
144
|
+
ariaLabel: this.options.closeLabel
|
|
131
145
|
}, icon('x'));
|
|
132
146
|
const status = h('div', {
|
|
133
147
|
ref: 'status',
|
|
@@ -140,14 +154,23 @@ export class Launcher extends Component {
|
|
|
140
154
|
class: 'zx-launcher__results',
|
|
141
155
|
id: listId,
|
|
142
156
|
role: 'listbox',
|
|
143
|
-
ariaLabel:
|
|
157
|
+
ariaLabel: this.options.resultsLabel
|
|
144
158
|
});
|
|
159
|
+
const shortcut = this.options.shortcut === 'mod+k' ? h('kbd', {
|
|
160
|
+
class: 'zx-launcher__shortcut',
|
|
161
|
+
ariaHidden: 'true'
|
|
162
|
+
}, launcherShortcutLabel()) : null;
|
|
163
|
+
const footer = launcherHints(this.options.hints);
|
|
145
164
|
dialog.setAttribute('aria-label', String(this.options.label));
|
|
146
165
|
dialog.replaceChildren(
|
|
147
166
|
h('div', { class: 'zx-launcher__surface' },
|
|
148
|
-
h('div', {
|
|
167
|
+
h('div', {
|
|
168
|
+
class: 'zx-launcher__search',
|
|
169
|
+
dataset: { shortcut: shortcut ? 'true' : 'false' }
|
|
170
|
+
}, icon('search'), input, shortcut, close),
|
|
149
171
|
status,
|
|
150
|
-
results
|
|
172
|
+
results,
|
|
173
|
+
footer));
|
|
151
174
|
dialog.dataset.state = 'closed';
|
|
152
175
|
if (this._createdRoot) document.body.append(dialog);
|
|
153
176
|
|
|
@@ -163,6 +186,14 @@ export class Launcher extends Component {
|
|
|
163
186
|
if (!option || !results.contains(option)) return;
|
|
164
187
|
this._activate(Number(option.getAttribute('data-launcher-index')), event);
|
|
165
188
|
});
|
|
189
|
+
this.listen(results, 'pointermove', (event) => {
|
|
190
|
+
const option = /** @type {Element|null} */ (event.target)?.closest?.('[data-launcher-index]');
|
|
191
|
+
if (!option || !results.contains(option)) return;
|
|
192
|
+
const index = Number(option.getAttribute('data-launcher-index'));
|
|
193
|
+
if (!Number.isInteger(index) || this._results[index]?.disabled || index === this._active) return;
|
|
194
|
+
this._active = index;
|
|
195
|
+
this._syncActive();
|
|
196
|
+
});
|
|
166
197
|
this.listen(dialog, 'cancel', (event) => {
|
|
167
198
|
event.preventDefault();
|
|
168
199
|
this.close();
|
|
@@ -238,6 +269,14 @@ export class Launcher extends Component {
|
|
|
238
269
|
return this;
|
|
239
270
|
}
|
|
240
271
|
|
|
272
|
+
/** Marks one local item as the current destination. @param {string|number|null} id Item id. @returns {this} */
|
|
273
|
+
setCurrent(id) {
|
|
274
|
+
const key = id == null ? null : String(id);
|
|
275
|
+
this._items = this._items.map((item) => ({ ...item, current: key !== null && String(item.id) === key }));
|
|
276
|
+
this._renderResults();
|
|
277
|
+
return this;
|
|
278
|
+
}
|
|
279
|
+
|
|
241
280
|
/** Replaces asynchronous sources and aborts work from the old set. @param {LauncherSource[]} sources @returns {this} */
|
|
242
281
|
setSources(sources) {
|
|
243
282
|
this._abortRequest();
|
|
@@ -276,8 +315,8 @@ export class Launcher extends Component {
|
|
|
276
315
|
async _loadSources() {
|
|
277
316
|
this._queryTimer = null;
|
|
278
317
|
const query = this._query.trim();
|
|
279
|
-
const sources = this._sources.filter((source) =>
|
|
280
|
-
query.length >= Math.max(Number(this.options.minQuery) || 0, Number(source.minQuery) || 0));
|
|
318
|
+
const sources = this._sources.filter((source) => visibleForQuery(source.when, query)
|
|
319
|
+
&& query.length >= Math.max(Number(this.options.minQuery) || 0, Number(source.minQuery) || 0));
|
|
281
320
|
if (sources.length === 0) return;
|
|
282
321
|
const request = new AbortController();
|
|
283
322
|
const sequence = ++this._requestSequence;
|
|
@@ -287,8 +326,13 @@ export class Launcher extends Component {
|
|
|
287
326
|
try {
|
|
288
327
|
const loaded = await source.load(query, { signal: request.signal });
|
|
289
328
|
if (request.signal.aborted || sequence !== this._requestSequence) return;
|
|
290
|
-
this._sourceItems.set(source.id, normalizeItems(loaded).map((item) => ({
|
|
291
|
-
|
|
329
|
+
this._sourceItems.set(source.id, normalizeItems(loaded).map((item, itemOrder) => ({
|
|
330
|
+
...item,
|
|
331
|
+
_source: source.id,
|
|
332
|
+
_sourceOrder: source.order === 'source',
|
|
333
|
+
group: item.group ?? source.label ?? '',
|
|
334
|
+
itemOrder: item.itemOrder ?? (source.order === 'source' ? itemOrder : undefined)
|
|
335
|
+
})));
|
|
292
336
|
this._renderResults();
|
|
293
337
|
} catch (error) {
|
|
294
338
|
if (request.signal.aborted || sequence !== this._requestSequence) return;
|
|
@@ -319,18 +363,30 @@ export class Launcher extends Component {
|
|
|
319
363
|
|
|
320
364
|
/** @returns {void} */
|
|
321
365
|
_renderResults() {
|
|
322
|
-
const
|
|
366
|
+
const queryKey = this._query;
|
|
367
|
+
const activeId = this._renderedQuery === queryKey && this._active >= 0 && this._results[this._active]
|
|
368
|
+
? String(this._results[this._active].id) : null;
|
|
369
|
+
const query = this._query.trim();
|
|
370
|
+
const local = this._items.filter((item) => visibleForQuery(item.when, query));
|
|
371
|
+
const rankedLocal = rankLauncherItems(local, query, this.options.maxResults);
|
|
372
|
+
const remote = this._sources.flatMap((source) => {
|
|
373
|
+
const loaded = this._sourceItems.get(String(source.id)) ?? [];
|
|
374
|
+
return source.order === 'source'
|
|
375
|
+
? loaded.filter((item) => visibleForQuery(item.when, query))
|
|
376
|
+
: rankLauncherItems(loaded.filter((item) => visibleForQuery(item.when, query)),
|
|
377
|
+
query, this.options.maxResults);
|
|
378
|
+
});
|
|
323
379
|
const seen = new Set();
|
|
324
|
-
const combined = [...
|
|
380
|
+
const combined = [...rankedLocal.map((item) => ({ ...item, _source: null })), ...remote]
|
|
325
381
|
.filter((item) => {
|
|
326
382
|
const key = String(item.id);
|
|
327
383
|
if (seen.has(key)) return false;
|
|
328
384
|
seen.add(key);
|
|
329
385
|
return true;
|
|
330
|
-
})
|
|
331
|
-
|
|
386
|
+
})
|
|
387
|
+
.slice(0, Math.max(0, Number(this.options.maxResults) || 0));
|
|
332
388
|
const groups = new Map();
|
|
333
|
-
for (const item of
|
|
389
|
+
for (const item of combined) {
|
|
334
390
|
const group = String(item.group ?? '');
|
|
335
391
|
if (!groups.has(group)) groups.set(group, []);
|
|
336
392
|
groups.get(group).push(item);
|
|
@@ -338,7 +394,10 @@ export class Launcher extends Component {
|
|
|
338
394
|
// Grouping changes visual order, so it must also become keyboard order. Keeping the original
|
|
339
395
|
// ranked indices made ArrowDown jump past an intervening option whenever groups interleaved.
|
|
340
396
|
this._results = [...groups.values()].flat();
|
|
341
|
-
this.
|
|
397
|
+
this._renderedQuery = queryKey;
|
|
398
|
+
const preserved = activeId === null ? -1
|
|
399
|
+
: this._results.findIndex((item) => !item.disabled && String(item.id) === activeId);
|
|
400
|
+
this._active = preserved >= 0 ? preserved : this._results.findIndex((item) => !item.disabled);
|
|
342
401
|
const nodes = [];
|
|
343
402
|
let index = 0;
|
|
344
403
|
for (const [group, items] of groups) {
|
|
@@ -346,7 +405,8 @@ export class Launcher extends Component {
|
|
|
346
405
|
if (group) nodes.push(h('div', {
|
|
347
406
|
class: 'zx-launcher__group',
|
|
348
407
|
role: 'group',
|
|
349
|
-
ariaLabel: group
|
|
408
|
+
ariaLabel: group,
|
|
409
|
+
dataset: { layout: items.every((item) => item.kind === 'application') ? 'grid' : 'list' }
|
|
350
410
|
}, h('div', { class: 'zx-launcher__group-label', ariaHidden: 'true' }, group), options));
|
|
351
411
|
else nodes.push(...options);
|
|
352
412
|
}
|
|
@@ -357,9 +417,14 @@ export class Launcher extends Component {
|
|
|
357
417
|
|
|
358
418
|
/** @param {RankedLauncherItem} item @param {number} index @returns {HTMLElement} */
|
|
359
419
|
_option(item, index) {
|
|
360
|
-
const
|
|
420
|
+
const href = safeHref(item.href);
|
|
421
|
+
const tag = href ? 'a' : 'button';
|
|
361
422
|
const children = [];
|
|
362
|
-
|
|
423
|
+
const visual = launcherItemIcon(item);
|
|
424
|
+
if (visual) children.push(h('span', {
|
|
425
|
+
class: 'zx-launcher__icon',
|
|
426
|
+
ariaHidden: 'true'
|
|
427
|
+
}, visual));
|
|
363
428
|
children.push(h('span', { class: 'zx-launcher__copy' },
|
|
364
429
|
h('span', { class: 'zx-launcher__label' }, item.label),
|
|
365
430
|
item.description ? h('span', { class: 'zx-launcher__description' }, item.description) : null));
|
|
@@ -367,14 +432,20 @@ export class Launcher extends Component {
|
|
|
367
432
|
return /** @type {HTMLElement} */ (h(tag, {
|
|
368
433
|
class: 'zx-launcher__option',
|
|
369
434
|
type: tag === 'button' ? 'button' : null,
|
|
370
|
-
href
|
|
435
|
+
href,
|
|
371
436
|
target: item.target ?? null,
|
|
437
|
+
rel: tag === 'a' ? 'noopener' : null,
|
|
372
438
|
role: 'option',
|
|
373
439
|
id: uid('zx-launcher-option'),
|
|
374
440
|
tabIndex: -1,
|
|
375
441
|
ariaSelected: 'false',
|
|
376
442
|
ariaDisabled: item.disabled ? 'true' : null,
|
|
377
|
-
|
|
443
|
+
ariaCurrent: item.current ? 'page' : null,
|
|
444
|
+
dataset: {
|
|
445
|
+
launcherIndex: index,
|
|
446
|
+
pinned: item.pinned !== undefined && item.pinned !== false ? 'true' : null,
|
|
447
|
+
kind: normalizeLauncherKind(item.kind)
|
|
448
|
+
}
|
|
378
449
|
}, children));
|
|
379
450
|
}
|
|
380
451
|
|
|
@@ -413,6 +484,18 @@ export class Launcher extends Component {
|
|
|
413
484
|
}
|
|
414
485
|
const enabled = this._results.map((item, index) => item.disabled ? -1 : index).filter((index) => index >= 0);
|
|
415
486
|
if (enabled.length === 0) return;
|
|
487
|
+
if (/^Arrow(?:Left|Right|Up|Down)$/.test(event.key)) {
|
|
488
|
+
const next = this._spatialIndex(event.key, enabled);
|
|
489
|
+
if (next !== null) {
|
|
490
|
+
event.preventDefault();
|
|
491
|
+
this._active = next;
|
|
492
|
+
this._syncActive();
|
|
493
|
+
this.refs.results.querySelector(`[data-launcher-index="${this._active}"]`)
|
|
494
|
+
?.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') return;
|
|
498
|
+
}
|
|
416
499
|
let position = enabled.indexOf(this._active);
|
|
417
500
|
if (event.key === 'Home' || event.key === 'PageUp') position = 0;
|
|
418
501
|
else if (event.key === 'End' || event.key === 'PageDown') position = enabled.length - 1;
|
|
@@ -425,6 +508,20 @@ export class Launcher extends Component {
|
|
|
425
508
|
this.refs.results.querySelector(`[data-launcher-index="${this._active}"]`)?.scrollIntoView({ block: 'nearest' });
|
|
426
509
|
}
|
|
427
510
|
|
|
511
|
+
/** @param {string} direction @param {number[]} enabled @returns {number|null} */
|
|
512
|
+
_spatialIndex(direction, enabled) {
|
|
513
|
+
const current = this.refs.results.querySelector(`[data-launcher-index="${this._active}"]`);
|
|
514
|
+
if (!(current instanceof HTMLElement)) return null;
|
|
515
|
+
if ((direction === 'ArrowLeft' || direction === 'ArrowRight')
|
|
516
|
+
&& this._results[this._active]?.kind !== 'application') return null;
|
|
517
|
+
const candidates = [...this.refs.results.querySelectorAll('[data-launcher-index]')]
|
|
518
|
+
.filter((option) => enabled.includes(Number(option.getAttribute('data-launcher-index')))
|
|
519
|
+
&& (direction !== 'ArrowLeft' && direction !== 'ArrowRight'
|
|
520
|
+
|| option.getAttribute('data-kind') === 'application'));
|
|
521
|
+
const next = findSpatialLauncherOption(candidates, current, direction);
|
|
522
|
+
return next ? Number(next.getAttribute('data-launcher-index')) : null;
|
|
523
|
+
}
|
|
524
|
+
|
|
428
525
|
/** @param {number} index @param {Event} event @returns {void} */
|
|
429
526
|
_activate(index, event) {
|
|
430
527
|
const item = this._results[index];
|
|
@@ -452,9 +549,10 @@ export class Launcher extends Component {
|
|
|
452
549
|
/** @param {KeyboardEvent} event @returns {void} */
|
|
453
550
|
_onDocumentKeydown(event) {
|
|
454
551
|
if (this.options.shortcut !== 'mod+k' || event.isComposing || event.key.toLowerCase() !== 'k'
|
|
455
|
-
|| (
|
|
552
|
+
|| Number(event.metaKey) + Number(event.ctrlKey) !== 1 || event.altKey || event.shiftKey) return;
|
|
456
553
|
const active = document.activeElement;
|
|
457
554
|
if (active instanceof HTMLInputElement || active instanceof HTMLTextAreaElement
|
|
555
|
+
|| active instanceof HTMLSelectElement
|
|
458
556
|
|| (active instanceof HTMLElement && active.isContentEditable)) return;
|
|
459
557
|
const modal = document.querySelector('dialog:modal');
|
|
460
558
|
if (modal && modal !== this.el) return;
|
|
@@ -463,6 +561,92 @@ export class Launcher extends Component {
|
|
|
463
561
|
}
|
|
464
562
|
}
|
|
465
563
|
|
|
564
|
+
/** @returns {string} */
|
|
565
|
+
function launcherShortcutLabel() {
|
|
566
|
+
const platform = String((/** @type {any} */ (navigator)).userAgentData?.platform
|
|
567
|
+
?? navigator.platform ?? '');
|
|
568
|
+
return /mac|iphone|ipad/i.test(platform) ? '⌘ K' : 'Ctrl K';
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/** @param {LauncherOptions['hints']} hints @returns {HTMLElement|null} */
|
|
572
|
+
function launcherHints(hints) {
|
|
573
|
+
if (hints === false) return null;
|
|
574
|
+
const labels = hints && typeof hints === 'object' ? hints : {};
|
|
575
|
+
return h('footer', { class: 'zx-launcher__hints', ariaHidden: 'true' },
|
|
576
|
+
h('span', {}, h('kbd', {}, '↑↓←→'), labels.move ?? 'Move'),
|
|
577
|
+
h('span', {}, h('kbd', {}, '↵'), labels.open ?? 'Open'),
|
|
578
|
+
h('span', {}, h('kbd', {}, 'Esc'), labels.close ?? 'Close'));
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/** @param {LauncherItem} item @returns {Node|null} */
|
|
582
|
+
function launcherItemIcon(item) {
|
|
583
|
+
let visual = item.icon;
|
|
584
|
+
if (typeof visual === 'function') visual = visual(item);
|
|
585
|
+
if (typeof visual === 'string') return icon(visual);
|
|
586
|
+
if (visual instanceof Node) return visual.cloneNode(true);
|
|
587
|
+
if (visual && typeof visual === 'object'
|
|
588
|
+
&& typeof (/** @type {any} */ (visual)).toElement === 'function') {
|
|
589
|
+
const element = (/** @type {any} */ (visual)).toElement();
|
|
590
|
+
return element instanceof Node ? element.cloneNode(true) : null;
|
|
591
|
+
}
|
|
592
|
+
return null;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/** @param {unknown} kind @returns {'application'|'record'|'action'} */
|
|
596
|
+
function normalizeLauncherKind(kind) {
|
|
597
|
+
return kind === 'application' || kind === 'record' ? kind : 'action';
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/** @param {unknown} when @param {string} query @returns {boolean} */
|
|
601
|
+
function visibleForQuery(when, query) {
|
|
602
|
+
if (when === 'empty') return query === '';
|
|
603
|
+
if (when === 'query') return query !== '';
|
|
604
|
+
return true;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Finds the nearest launcher option in a visual direction. Horizontal movement is constrained by
|
|
609
|
+
* the caller to application tiles; vertical movement may cross from the app grid into row groups.
|
|
610
|
+
* @param {Element[]} options Candidate options.
|
|
611
|
+
* @param {HTMLElement} current Active option.
|
|
612
|
+
* @param {string} direction Arrow key.
|
|
613
|
+
* @returns {Element|null}
|
|
614
|
+
*/
|
|
615
|
+
function findSpatialLauncherOption(options, current, direction) {
|
|
616
|
+
const origin = current.getBoundingClientRect();
|
|
617
|
+
if (origin.width === 0 && origin.height === 0) return null;
|
|
618
|
+
const originX = origin.left + origin.width / 2;
|
|
619
|
+
const originY = origin.top + origin.height / 2;
|
|
620
|
+
let best = null;
|
|
621
|
+
let bestScore = Infinity;
|
|
622
|
+
for (const option of options) {
|
|
623
|
+
if (option === current) continue;
|
|
624
|
+
const rect = option.getBoundingClientRect();
|
|
625
|
+
const dx = rect.left + rect.width / 2 - originX;
|
|
626
|
+
const dy = rect.top + rect.height / 2 - originY;
|
|
627
|
+
let valid = false;
|
|
628
|
+
let score = Infinity;
|
|
629
|
+
if (direction === 'ArrowLeft') {
|
|
630
|
+
valid = dx < -2 && Math.abs(dy) < Math.max(origin.height, rect.height) / 2;
|
|
631
|
+
score = Math.abs(dx) + Math.abs(dy) * 4;
|
|
632
|
+
} else if (direction === 'ArrowRight') {
|
|
633
|
+
valid = dx > 2 && Math.abs(dy) < Math.max(origin.height, rect.height) / 2;
|
|
634
|
+
score = Math.abs(dx) + Math.abs(dy) * 4;
|
|
635
|
+
} else if (direction === 'ArrowUp') {
|
|
636
|
+
valid = dy < -2;
|
|
637
|
+
score = Math.abs(dy) * 10 + Math.abs(dx);
|
|
638
|
+
} else if (direction === 'ArrowDown') {
|
|
639
|
+
valid = dy > 2;
|
|
640
|
+
score = Math.abs(dy) * 10 + Math.abs(dx);
|
|
641
|
+
}
|
|
642
|
+
if (valid && score < bestScore) {
|
|
643
|
+
best = option;
|
|
644
|
+
bestScore = score;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
return best;
|
|
648
|
+
}
|
|
649
|
+
|
|
466
650
|
/**
|
|
467
651
|
* Ranks launcher items using the current ZeyOS application tiers.
|
|
468
652
|
* @param {RankedLauncherItem[]} items Items to rank.
|
|
@@ -475,12 +659,18 @@ export function rankLauncherItems(items, query, limit = 100) {
|
|
|
475
659
|
const ranked = items.map((item, order) => ({ item, order, score: launcherScore(item, needle) }))
|
|
476
660
|
.filter(({ score }) => score < Infinity);
|
|
477
661
|
ranked.sort((a, b) => {
|
|
662
|
+
const aGroup = finiteOrder(a.item.groupOrder);
|
|
663
|
+
const bGroup = finiteOrder(b.item.groupOrder);
|
|
664
|
+
if (aGroup !== bGroup) return aGroup - bGroup;
|
|
478
665
|
if (needle === '') {
|
|
479
666
|
const aPinned = pinOrder(a.item.pinned, a.order);
|
|
480
667
|
const bPinned = pinOrder(b.item.pinned, b.order);
|
|
481
668
|
if (aPinned !== bPinned) return aPinned - bPinned;
|
|
482
669
|
}
|
|
483
|
-
return a.score - b.score
|
|
670
|
+
return a.score - b.score
|
|
671
|
+
|| finiteOrder(a.item.itemOrder) - finiteOrder(b.item.itemOrder)
|
|
672
|
+
|| compareLauncherText(a.item.label, b.item.label)
|
|
673
|
+
|| a.order - b.order;
|
|
484
674
|
});
|
|
485
675
|
const maximum = Math.max(0, Number(limit) || 0);
|
|
486
676
|
return ranked.slice(0, maximum || 0).map(({ item, score }) => ({ ...item, _score: score }));
|
|
@@ -509,6 +699,11 @@ function pinOrder(pinned, order) {
|
|
|
509
699
|
return Number.MAX_SAFE_INTEGER;
|
|
510
700
|
}
|
|
511
701
|
|
|
702
|
+
/** @param {unknown} value @returns {number} */
|
|
703
|
+
function finiteOrder(value) {
|
|
704
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : Number.MAX_SAFE_INTEGER;
|
|
705
|
+
}
|
|
706
|
+
|
|
512
707
|
/** @param {unknown} value @returns {string} */
|
|
513
708
|
function searchable(value) {
|
|
514
709
|
return String(value ?? '').normalize('NFKD').replace(/\p{M}/gu, '').toLowerCase();
|
|
@@ -529,7 +724,20 @@ function compareLauncherText(left, right) {
|
|
|
529
724
|
function normalizeItems(items) {
|
|
530
725
|
if (!Array.isArray(items)) return [];
|
|
531
726
|
return items.filter((item) => item && typeof item === 'object' && item.id != null && item.label != null)
|
|
532
|
-
.map((item) => (
|
|
727
|
+
.map((item) => normalizeLauncherItem(item));
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/** @param {Record<string, any>} item @returns {LauncherItem} */
|
|
731
|
+
function normalizeLauncherItem(item) {
|
|
732
|
+
const normalized = { ...item, label: String(item.label) };
|
|
733
|
+
if (item.href == null) return normalized;
|
|
734
|
+
const href = safeHref(item.href);
|
|
735
|
+
if (href !== null) normalized.href = href;
|
|
736
|
+
else {
|
|
737
|
+
delete normalized.href;
|
|
738
|
+
if (typeof normalized.invoke !== 'function') normalized.disabled = true;
|
|
739
|
+
}
|
|
740
|
+
return normalized;
|
|
533
741
|
}
|
|
534
742
|
|
|
535
743
|
/** @param {unknown} sources @returns {LauncherSource[]} */
|
|
@@ -7,10 +7,15 @@ import { Select } from './select.js';
|
|
|
7
7
|
*/
|
|
8
8
|
export function registerSelectFieldAdapter() {
|
|
9
9
|
Field.register('zxselect', (field, options) => {
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
const props = options.props ?? {};
|
|
11
|
+
const preset = props.preset;
|
|
12
|
+
const selectOptions = { ...props, value: options.value };
|
|
13
|
+
delete selectOptions.preset;
|
|
14
|
+
const select = field.own(preset === 'priority'
|
|
15
|
+
? Select.priority(null, selectOptions)
|
|
16
|
+
: preset === 'status'
|
|
17
|
+
? Select.status(null, selectOptions)
|
|
18
|
+
: new Select(null, selectOptions));
|
|
14
19
|
field.listen(select.el, 'change', (event) => event.stopImmediatePropagation());
|
|
15
20
|
select.on('change', (event) => field.emit('change', { value: event.detail.value }));
|
|
16
21
|
return {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
.zx-select__control {
|
|
10
10
|
display: grid;
|
|
11
|
-
grid-template-columns: minmax(0, 1fr) auto auto auto;
|
|
11
|
+
grid-template-columns: auto minmax(0, 1fr) auto auto auto;
|
|
12
12
|
align-items: center;
|
|
13
13
|
min-block-size: var(--zx-control-height);
|
|
14
14
|
overflow: hidden;
|
|
@@ -17,6 +17,18 @@
|
|
|
17
17
|
background: var(--zx-color-bg-control);
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
.zx-select__value-adornment {
|
|
21
|
+
display: inline-flex;
|
|
22
|
+
align-items: center;
|
|
23
|
+
padding-inline-start: var(--zx-control-pad-x);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.zx-select__value-adornment[hidden] { display: none; }
|
|
27
|
+
|
|
28
|
+
.zx-select__value-adornment:not([hidden]) + .zx-select__input {
|
|
29
|
+
padding-inline-start: var(--zx-space-2) !important;
|
|
30
|
+
}
|
|
31
|
+
|
|
20
32
|
.zx-select__control:has(.zx-select__input:focus-visible) {
|
|
21
33
|
box-shadow: var(--zx-focus-ring);
|
|
22
34
|
}
|
|
@@ -155,21 +167,143 @@
|
|
|
155
167
|
}
|
|
156
168
|
|
|
157
169
|
.zx-select__priority-icon {
|
|
158
|
-
inline-
|
|
159
|
-
|
|
170
|
+
display: inline-flex;
|
|
171
|
+
gap: 2px;
|
|
160
172
|
flex: none;
|
|
161
|
-
|
|
162
|
-
|
|
173
|
+
color: var(--zx-color-warning);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.zx-select__priority-icon > span {
|
|
177
|
+
inline-size: 7px;
|
|
178
|
+
block-size: 7px;
|
|
179
|
+
border: 1px solid currentColor;
|
|
180
|
+
border-radius: 2px;
|
|
163
181
|
}
|
|
164
182
|
|
|
165
|
-
.zx-select__priority-icon[data-
|
|
183
|
+
.zx-select__priority-icon > span[data-filled="true"] { background: currentColor; }
|
|
184
|
+
.zx-select__priority-icon[data-level="0"] { color: var(--zx-color-success); }
|
|
166
185
|
.zx-select__priority-icon[data-level="1"] {
|
|
167
|
-
|
|
186
|
+
color: color-mix(in srgb, var(--zx-color-success) 35%, var(--zx-color-warning));
|
|
187
|
+
}
|
|
188
|
+
.zx-select__priority-icon[data-level="2"] { color: var(--zx-color-warning); }
|
|
189
|
+
.zx-select__priority-icon[data-level="3"] {
|
|
190
|
+
color: color-mix(in srgb, var(--zx-color-warning) 35%, var(--zx-color-danger));
|
|
191
|
+
}
|
|
192
|
+
.zx-select__priority-icon[data-level="4"] { color: var(--zx-color-danger); }
|
|
193
|
+
|
|
194
|
+
.zx-select__status {
|
|
195
|
+
display: grid;
|
|
196
|
+
grid-template-columns: auto minmax(0, 1fr) auto;
|
|
197
|
+
align-items: center;
|
|
198
|
+
gap: var(--zx-space-2);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.zx-select__status-copy,
|
|
202
|
+
.zx-select__action-copy {
|
|
203
|
+
display: grid;
|
|
204
|
+
min-inline-size: 0;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.zx-select__status-description,
|
|
208
|
+
.zx-select__action-description {
|
|
209
|
+
overflow: hidden;
|
|
210
|
+
color: var(--zx-color-text-muted);
|
|
211
|
+
font-size: var(--zx-text-xs);
|
|
212
|
+
text-overflow: ellipsis;
|
|
213
|
+
white-space: nowrap;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.zx-select__status-shortcut {
|
|
217
|
+
border: 0;
|
|
218
|
+
background: transparent;
|
|
219
|
+
color: var(--zx-color-text-muted);
|
|
220
|
+
font-family: var(--zx-font-mono);
|
|
221
|
+
font-size: var(--zx-text-xs);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.zx-select__status-indicator {
|
|
225
|
+
position: relative;
|
|
226
|
+
display: inline-grid;
|
|
227
|
+
place-items: center;
|
|
228
|
+
inline-size: 14px;
|
|
229
|
+
block-size: 14px;
|
|
230
|
+
flex: none;
|
|
231
|
+
border: 2px solid currentColor;
|
|
232
|
+
border-radius: var(--zx-radius-full);
|
|
233
|
+
color: var(--zx-color-text-muted);
|
|
168
234
|
}
|
|
169
235
|
|
|
170
|
-
.zx-
|
|
171
|
-
.zx-
|
|
172
|
-
|
|
236
|
+
.zx-select__status-indicator[data-tone="neutral"] { color: var(--zx-color-border-strong); }
|
|
237
|
+
.zx-select__status-indicator[data-tone="warning"] { color: var(--zx-color-warning); }
|
|
238
|
+
.zx-select__status-indicator[data-tone="info"] { color: var(--zx-color-info); }
|
|
239
|
+
.zx-select__status-indicator[data-tone="accent"] { color: var(--zx-color-accent); }
|
|
240
|
+
.zx-select__status-indicator[data-tone="success"] { color: var(--zx-color-success); }
|
|
241
|
+
.zx-select__status-indicator[data-tone="danger"] { color: var(--zx-color-danger); }
|
|
242
|
+
|
|
243
|
+
.zx-select__status-indicator[data-indicator="dotted"] { border-style: dotted; }
|
|
244
|
+
.zx-select__status-indicator[data-indicator="progress"]::after {
|
|
245
|
+
content: "";
|
|
246
|
+
inline-size: 4px;
|
|
247
|
+
block-size: 4px;
|
|
248
|
+
border-radius: inherit;
|
|
249
|
+
background: currentColor;
|
|
250
|
+
}
|
|
251
|
+
.zx-select__status-indicator[data-indicator="review"] {
|
|
252
|
+
background: linear-gradient(90deg, currentColor 50%, transparent 50%);
|
|
253
|
+
}
|
|
254
|
+
.zx-select__status-indicator[data-indicator="ready"] {
|
|
255
|
+
background: conic-gradient(currentColor 0 35%, transparent 35% 100%);
|
|
256
|
+
}
|
|
257
|
+
.zx-select__status-indicator[data-indicator="waiting"] {
|
|
258
|
+
background: conic-gradient(currentColor 0 70%, transparent 70% 100%);
|
|
259
|
+
}
|
|
260
|
+
.zx-select__status-indicator:is([data-indicator="done"], [data-indicator="canceled"]) {
|
|
261
|
+
background: currentColor;
|
|
262
|
+
}
|
|
263
|
+
.zx-select__status-indicator:is([data-indicator="done"], [data-indicator="canceled"]) > .zx-icon {
|
|
264
|
+
color: var(--zx-color-bg-raised);
|
|
265
|
+
}
|
|
266
|
+
.zx-select__status-indicator[data-indicator="duplicate"]::after {
|
|
267
|
+
content: "";
|
|
268
|
+
inline-size: 8px;
|
|
269
|
+
block-size: 2px;
|
|
270
|
+
border-radius: var(--zx-radius-full);
|
|
271
|
+
background: currentColor;
|
|
272
|
+
transform: rotate(-45deg);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.zx-select__action-content {
|
|
276
|
+
display: flex;
|
|
277
|
+
align-items: center;
|
|
278
|
+
gap: var(--zx-space-2);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.zx-select__action-icon { display: inline-flex; color: var(--zx-color-text-muted); }
|
|
282
|
+
|
|
283
|
+
.zx-select__entity,
|
|
284
|
+
.zx-select__entity-none {
|
|
285
|
+
display: flex;
|
|
286
|
+
align-items: center;
|
|
287
|
+
gap: var(--zx-space-2);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.zx-select__entity-icon { display: inline-flex; flex: none; }
|
|
291
|
+
.zx-select__entity-copy { display: grid; min-inline-size: 0; }
|
|
292
|
+
.zx-select__entity-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
293
|
+
.zx-select__entity-subtitle {
|
|
294
|
+
overflow: hidden;
|
|
295
|
+
color: var(--zx-color-text-muted);
|
|
296
|
+
font-size: var(--zx-text-xs);
|
|
297
|
+
text-overflow: ellipsis;
|
|
298
|
+
white-space: nowrap;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.zx-select__entity-none-icon {
|
|
302
|
+
inline-size: 14px;
|
|
303
|
+
block-size: 14px;
|
|
304
|
+
flex: none;
|
|
305
|
+
border: 2px dotted var(--zx-color-text-muted);
|
|
306
|
+
border-radius: var(--zx-radius-full);
|
|
173
307
|
}
|
|
174
308
|
|
|
175
309
|
@media (prefers-reduced-motion: no-preference) {
|