flexdesk 0.8.1 → 0.9.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/README.md +32 -0
- package/css/flexdesk.css +127 -0
- package/css/overrides.css +127 -0
- package/css/tokens.css +2 -0
- package/dist/flexdesk.css +127 -0
- package/dist/tokens.css +2 -0
- package/dist/wm.js +488 -26
- package/dist/wm.js.map +3 -3
- package/package.json +1 -1
- package/src/tiling/command_palette.js +573 -33
- package/src/tiling/shell.js +4 -1
package/dist/wm.js
CHANGED
|
@@ -25,42 +25,72 @@ var STATIC_COMMANDS = [
|
|
|
25
25
|
label: "Keyboard shortcuts",
|
|
26
26
|
hint: "Show all key bindings (?)",
|
|
27
27
|
icon: "keyboard",
|
|
28
|
+
shortcut: "?",
|
|
29
|
+
verb: "run",
|
|
28
30
|
action: () => HelpModal.open("keyboard-shortcuts")
|
|
29
31
|
}
|
|
30
32
|
];
|
|
33
|
+
var DEFAULT_GROUP_CAP = 4;
|
|
34
|
+
var DEFAULT_COMMAND_MIN_CHARS = 2;
|
|
35
|
+
var REMOTE_DEBOUNCE_MS = 150;
|
|
36
|
+
var MAX_ROWS = 200;
|
|
31
37
|
function createCommandPalette({
|
|
32
38
|
wm,
|
|
33
39
|
api,
|
|
34
40
|
taxonomy,
|
|
35
41
|
catalog,
|
|
36
42
|
placeholder = "Search\u2026",
|
|
37
|
-
onPick = null
|
|
43
|
+
onPick = null,
|
|
44
|
+
chrome = {},
|
|
45
|
+
layout = "flat",
|
|
46
|
+
groupCap = DEFAULT_GROUP_CAP,
|
|
47
|
+
empty = null,
|
|
48
|
+
emptyLabel = "Recent",
|
|
49
|
+
commands = null,
|
|
50
|
+
commandMinChars = DEFAULT_COMMAND_MIN_CHARS,
|
|
51
|
+
drill = null,
|
|
52
|
+
cache = false,
|
|
53
|
+
tips = null,
|
|
54
|
+
footerNote = ""
|
|
38
55
|
}) {
|
|
39
56
|
if (!taxonomy) throw new Error("createCommandPalette: a taxonomy is required");
|
|
40
57
|
if (!catalog) throw new Error("createCommandPalette: an entity catalog is required");
|
|
58
|
+
const grouped = layout === "grouped";
|
|
41
59
|
let overlay = null;
|
|
42
60
|
let entities = [];
|
|
61
|
+
let cached = null;
|
|
62
|
+
let onClose = null;
|
|
43
63
|
const isOpen = () => !!overlay;
|
|
44
64
|
const close = () => {
|
|
45
65
|
if (!overlay) return;
|
|
66
|
+
try {
|
|
67
|
+
onClose?.();
|
|
68
|
+
} catch {
|
|
69
|
+
}
|
|
70
|
+
onClose = null;
|
|
46
71
|
overlay.remove();
|
|
47
72
|
overlay = null;
|
|
48
73
|
};
|
|
49
74
|
const toggle = () => {
|
|
50
75
|
isOpen() ? close() : open();
|
|
51
76
|
};
|
|
77
|
+
const invalidate = () => {
|
|
78
|
+
cached = null;
|
|
79
|
+
};
|
|
52
80
|
const open = async () => {
|
|
53
81
|
if (overlay) return;
|
|
54
82
|
overlay = document.createElement("div");
|
|
55
83
|
overlay.id = ROOT_ID;
|
|
56
|
-
overlay.className = "twm-cmdpal-overlay";
|
|
57
|
-
overlay.innerHTML = _markup(taxonomy, placeholder, wm);
|
|
84
|
+
overlay.className = "twm-cmdpal-overlay" + (grouped ? " twm-cmdpal-overlay--grouped" : "");
|
|
85
|
+
overlay.innerHTML = _markup(taxonomy, placeholder, wm, { chrome, grouped });
|
|
58
86
|
document.body.appendChild(overlay);
|
|
87
|
+
const mine = overlay;
|
|
59
88
|
overlay.addEventListener("click", (e) => {
|
|
60
89
|
if (e.target === overlay) close();
|
|
61
90
|
});
|
|
62
91
|
const input = overlay.querySelector(".twm-cmdpal__input");
|
|
63
92
|
const list = overlay.querySelector('[data-role="list"]');
|
|
93
|
+
const foot = overlay.querySelector('[data-role="footer"]');
|
|
64
94
|
const chipSlot = overlay.querySelector('[data-role="type-chip"]');
|
|
65
95
|
const chips = overlay.querySelectorAll("[data-shortcut]");
|
|
66
96
|
const toggles = overlay.querySelectorAll("[data-toggle]");
|
|
@@ -82,7 +112,35 @@ function createCommandPalette({
|
|
|
82
112
|
let active = 0;
|
|
83
113
|
let typeFilter = null;
|
|
84
114
|
let typeFilterLabel = null;
|
|
115
|
+
let emptyRows = [];
|
|
116
|
+
let scope = null;
|
|
117
|
+
let remote = { seq: 0, ctl: null, timer: null, rows: [], pending: false, error: null };
|
|
118
|
+
const cancelRemote = () => {
|
|
119
|
+
clearTimeout(remote.timer);
|
|
120
|
+
try {
|
|
121
|
+
remote.ctl?.abort();
|
|
122
|
+
} catch {
|
|
123
|
+
}
|
|
124
|
+
remote.ctl = null;
|
|
125
|
+
};
|
|
126
|
+
onClose = cancelRemote;
|
|
85
127
|
const renderChip = () => {
|
|
128
|
+
if (scope) {
|
|
129
|
+
chipSlot.innerHTML = `
|
|
130
|
+
<span class="twm-cmdpal__type-chip" title="${_esc(scope.label)}">
|
|
131
|
+
<span class="material-symbols-outlined twm-cmdpal__type-chip-icon">${_esc(scope.icon || "arrow_right")}</span>
|
|
132
|
+
<span class="twm-cmdpal__type-chip-label twm-cmdpal__type-chip-label--scope">${_esc(scope.label)}</span>
|
|
133
|
+
<button type="button" class="twm-cmdpal__type-chip-x"
|
|
134
|
+
data-action="clear-type"
|
|
135
|
+
aria-label="Leave ${_esc(scope.label)}">\xD7</button>
|
|
136
|
+
</span>`;
|
|
137
|
+
chipSlot.querySelector('[data-action="clear-type"]')?.addEventListener("click", (ev) => {
|
|
138
|
+
ev.preventDefault();
|
|
139
|
+
leaveScope();
|
|
140
|
+
input.focus();
|
|
141
|
+
});
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
86
144
|
if (!typeFilter) {
|
|
87
145
|
chipSlot.innerHTML = "";
|
|
88
146
|
return;
|
|
@@ -109,7 +167,11 @@ function createCommandPalette({
|
|
|
109
167
|
});
|
|
110
168
|
};
|
|
111
169
|
const render = () => {
|
|
112
|
-
|
|
170
|
+
if (grouped) {
|
|
171
|
+
renderGrouped();
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
list.innerHTML = filtered.slice(0, MAX_ROWS).map((e, i) => `
|
|
113
175
|
<div class="twm-cmdpal__item${i === active ? " twm-cmdpal__item--active" : ""}"
|
|
114
176
|
data-idx="${i}">
|
|
115
177
|
<span class="material-symbols-outlined twm-cmdpal__icon">${e.icon || "arrow_right"}</span>
|
|
@@ -122,13 +184,112 @@ function createCommandPalette({
|
|
|
122
184
|
el.addEventListener("click", () => commit(Number(el.dataset.idx)));
|
|
123
185
|
});
|
|
124
186
|
};
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
187
|
+
let sections = [];
|
|
188
|
+
const namedGroups = /* @__PURE__ */ new Map();
|
|
189
|
+
const groupLabel = (key) => {
|
|
190
|
+
if (namedGroups.has(key)) return namedGroups.get(key);
|
|
191
|
+
if (key === "command") return "Commands";
|
|
192
|
+
const m = taxonomy.meta(key);
|
|
193
|
+
return m?.plural || m?.label || _titleCase(key);
|
|
194
|
+
};
|
|
195
|
+
const kindLabel = (row) => {
|
|
196
|
+
if (row.kindLabel) return row.kindLabel;
|
|
197
|
+
if (row.kind === "command") return "Command";
|
|
198
|
+
return taxonomy.meta(row.kind)?.label || _titleCase(row.kind);
|
|
199
|
+
};
|
|
200
|
+
const rowHtml = (r, i) => {
|
|
201
|
+
const on = i === active ? " twm-cmdpal__item--active" : "";
|
|
202
|
+
if (r.more) {
|
|
203
|
+
return `<div class="twm-cmdpal__item twm-cmdpal__item--more${on}" data-idx="${i}" role="option">
|
|
204
|
+
<span class="material-symbols-outlined twm-cmdpal__icon">expand_more</span>
|
|
205
|
+
<span class="twm-cmdpal__main"><span class="twm-cmdpal__label">${_esc(r.label)}</span></span>
|
|
206
|
+
<span class="twm-cmdpal__side"><span class="twm-cmdpal__acts"><span><kbd>Enter</kbd> show all</span></span></span>
|
|
207
|
+
</div>`;
|
|
208
|
+
}
|
|
209
|
+
const drawn = _drawn(r);
|
|
210
|
+
const ctx = r.cells?.length ? `<span class="twm-cmdpal__cells">${r.cells.map(_esc).join(" \xB7 ")}</span>` : drawn ? `<span class="twm-cmdpal__context">${_esc(drawn)}</span>` : "";
|
|
211
|
+
const verb = r.verb || (r.action || r.kind === "command" ? "run" : "open");
|
|
212
|
+
const modes = Array.isArray(r.modes) ? r.modes : [];
|
|
213
|
+
const acts = [`<span><kbd>Enter</kbd> ${_esc(verb)}</span>`];
|
|
214
|
+
if (r.drillLabel && drill) acts.push(`<span><kbd>Tab</kbd> ${_esc(r.drillLabel)}</span>`);
|
|
215
|
+
if (modes.includes("split")) acts.push("<span><kbd>Ctrl Enter</kbd> split</span>");
|
|
216
|
+
if (modes.includes("window")) acts.push("<span><kbd>Shift Enter</kbd> window</span>");
|
|
217
|
+
const side = r.shortcut ? `<kbd class="twm-cmdpal__shortcut">${_esc(r.shortcut)}</kbd>` : `<span class="twm-cmdpal__kind">${_esc(kindLabel(r))}</span>`;
|
|
218
|
+
return `<div class="twm-cmdpal__item${on}" data-idx="${i}" role="option">
|
|
219
|
+
<span class="material-symbols-outlined twm-cmdpal__icon">${_esc(r.icon || "arrow_right")}</span>
|
|
220
|
+
<span class="twm-cmdpal__main"><span class="twm-cmdpal__label">${_mark(r.label || r.id, r._hits)}</span>${ctx}</span>
|
|
221
|
+
<span class="twm-cmdpal__side">${side}<span class="twm-cmdpal__acts">${acts.join("")}</span></span>
|
|
222
|
+
</div>`;
|
|
223
|
+
};
|
|
224
|
+
const renderGrouped = () => {
|
|
225
|
+
let html = "";
|
|
226
|
+
for (const s of sections) {
|
|
227
|
+
if (!s.count) continue;
|
|
228
|
+
if (s.title) html += `<div class="twm-cmdpal__group" role="presentation">${_esc(s.title)}</div>`;
|
|
229
|
+
for (let i = s.start; i < s.start + s.count; i++) html += rowHtml(filtered[i], i);
|
|
230
|
+
}
|
|
231
|
+
const q = input.value.trim();
|
|
232
|
+
if (!filtered.length) {
|
|
233
|
+
if (scope) {
|
|
234
|
+
const msg = remote.error ? remote.error : remote.pending ? "Searching\u2026" : q ? `Nothing in ${scope.label} matches \u201C${q}\u201D.` : scope.emptyText || `Type to search ${scope.label}.`;
|
|
235
|
+
html += `<div class="twm-cmdpal__empty">${_esc(msg)}</div>`;
|
|
236
|
+
} else if (q && q !== ">") {
|
|
237
|
+
html += `<div class="twm-cmdpal__empty">Nothing matches \u201C${_esc(q)}\u201D.</div>`;
|
|
238
|
+
}
|
|
239
|
+
} else if (scope && remote.pending) {
|
|
240
|
+
html += '<div class="twm-cmdpal__empty twm-cmdpal__empty--quiet">Searching\u2026</div>';
|
|
241
|
+
}
|
|
242
|
+
if (!q && !scope && !typeFilter && Array.isArray(tips) && tips.length) {
|
|
243
|
+
html += `<div class="twm-cmdpal__tips">${tips.map((t) => `<span>${_esc(t)}</span>`).join("")}</div>`;
|
|
244
|
+
}
|
|
245
|
+
list.innerHTML = html;
|
|
246
|
+
renderFooter();
|
|
247
|
+
};
|
|
248
|
+
const paintActive = () => {
|
|
249
|
+
list.querySelectorAll("[data-idx]").forEach((el) => {
|
|
250
|
+
const on = Number(el.dataset.idx) === active;
|
|
251
|
+
el.classList.toggle("twm-cmdpal__item--active", on);
|
|
252
|
+
el.setAttribute("aria-selected", on ? "true" : "false");
|
|
253
|
+
if (on) el.scrollIntoView?.({ block: "nearest" });
|
|
254
|
+
});
|
|
255
|
+
};
|
|
256
|
+
const renderFooter = () => {
|
|
257
|
+
if (!foot || !grouped) return;
|
|
258
|
+
const parts = ["<span><kbd>\u2191</kbd><kbd>\u2193</kbd> move</span>", "<span><kbd>Enter</kbd> open</span>"];
|
|
259
|
+
if (scope || typeFilter) parts.push("<span><kbd>Backspace</kbd> leave</span>");
|
|
260
|
+
parts.push("<span><kbd>Esc</kbd> close</span>");
|
|
261
|
+
const note = scope?.note || footerNote;
|
|
262
|
+
foot.innerHTML = parts.join("") + (note ? `<span class="twm-cmdpal__footnote">${_esc(note)}</span>` : "");
|
|
263
|
+
};
|
|
264
|
+
if (grouped) {
|
|
265
|
+
list.addEventListener("mousemove", (e) => {
|
|
266
|
+
const el = e.target.closest?.("[data-idx]");
|
|
267
|
+
if (!el) return;
|
|
268
|
+
const i = Number(el.dataset.idx);
|
|
269
|
+
if (i !== active) {
|
|
270
|
+
active = i;
|
|
271
|
+
paintActive();
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
list.addEventListener("click", (e) => {
|
|
275
|
+
const el = e.target.closest?.("[data-idx]");
|
|
276
|
+
if (!el) return;
|
|
277
|
+
commit(Number(el.dataset.idx), _modeOf(e));
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
const commit = (i, mode = "open") => {
|
|
281
|
+
const picked = filtered[i];
|
|
282
|
+
if (!picked) return;
|
|
283
|
+
if (picked.more) {
|
|
284
|
+
showAll(picked.more);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
const pick = _public(picked);
|
|
288
|
+
const how = { mode, scope: scope?.id ?? null };
|
|
128
289
|
close();
|
|
129
290
|
if (pick.action) {
|
|
130
291
|
try {
|
|
131
|
-
pick.action();
|
|
292
|
+
pick.action(how);
|
|
132
293
|
} catch (err) {
|
|
133
294
|
console.error(err);
|
|
134
295
|
}
|
|
@@ -137,7 +298,7 @@ function createCommandPalette({
|
|
|
137
298
|
if (onPick) {
|
|
138
299
|
let handled = false;
|
|
139
300
|
try {
|
|
140
|
-
handled = onPick(pick) ?? false;
|
|
301
|
+
handled = onPick(pick, how) ?? false;
|
|
141
302
|
} catch (err) {
|
|
142
303
|
console.error("[cmdpal] onPick threw", err);
|
|
143
304
|
}
|
|
@@ -146,7 +307,7 @@ function createCommandPalette({
|
|
|
146
307
|
wm.openInPrimary(pick.kind, pick.props || { id: pick.id, label: pick.label });
|
|
147
308
|
};
|
|
148
309
|
const tryConsumePrefix = () => {
|
|
149
|
-
if (typeFilter) return false;
|
|
310
|
+
if (typeFilter || scope) return false;
|
|
150
311
|
const m = input.value.match(/^([a-zA-Z][a-zA-Z0-9_-]*):/);
|
|
151
312
|
if (!m) return false;
|
|
152
313
|
const kinds = catalog.resolveTypePrefix(m[1]);
|
|
@@ -154,11 +315,163 @@ function createCommandPalette({
|
|
|
154
315
|
typeFilter = kinds;
|
|
155
316
|
typeFilterLabel = _titleCase(m[1]);
|
|
156
317
|
input.value = input.value.slice(m[0].length);
|
|
318
|
+
if (grouped) {
|
|
319
|
+
input.value = input.value.replace(/^\s+/, "");
|
|
320
|
+
typeFilterLabel = groupLabel(kinds[0]);
|
|
321
|
+
}
|
|
157
322
|
renderChip();
|
|
158
323
|
return true;
|
|
159
324
|
};
|
|
325
|
+
const showAll = (key) => {
|
|
326
|
+
typeFilter = [key];
|
|
327
|
+
typeFilterLabel = groupLabel(key);
|
|
328
|
+
if (input.value.startsWith(">")) input.value = input.value.slice(1);
|
|
329
|
+
renderChip();
|
|
330
|
+
refilter();
|
|
331
|
+
input.focus();
|
|
332
|
+
};
|
|
333
|
+
const enterScope = (row) => {
|
|
334
|
+
if (!drill || !row || row.more) return false;
|
|
335
|
+
let next = null;
|
|
336
|
+
try {
|
|
337
|
+
next = drill(_public(row));
|
|
338
|
+
} catch (err) {
|
|
339
|
+
console.error("[cmdpal] drill threw", err);
|
|
340
|
+
}
|
|
341
|
+
if (!next || typeof next.search !== "function") return false;
|
|
342
|
+
cancelRemote();
|
|
343
|
+
scope = next;
|
|
344
|
+
typeFilter = null;
|
|
345
|
+
typeFilterLabel = null;
|
|
346
|
+
input.value = "";
|
|
347
|
+
input.placeholder = next.placeholder || `Search ${next.label}\u2026`;
|
|
348
|
+
remote = { seq: 0, ctl: null, timer: null, rows: [], pending: false, error: null };
|
|
349
|
+
renderChip();
|
|
350
|
+
refilter();
|
|
351
|
+
return true;
|
|
352
|
+
};
|
|
353
|
+
const leaveScope = () => {
|
|
354
|
+
cancelRemote();
|
|
355
|
+
remote.seq++;
|
|
356
|
+
scope = null;
|
|
357
|
+
typeFilter = null;
|
|
358
|
+
typeFilterLabel = null;
|
|
359
|
+
input.placeholder = placeholder;
|
|
360
|
+
renderChip();
|
|
361
|
+
refilter();
|
|
362
|
+
};
|
|
363
|
+
const runRemote = (q) => {
|
|
364
|
+
cancelRemote();
|
|
365
|
+
const seq = ++remote.seq;
|
|
366
|
+
if (q.length < (scope.minChars ?? 1)) {
|
|
367
|
+
remote.rows = [];
|
|
368
|
+
remote.pending = false;
|
|
369
|
+
remote.error = null;
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
remote.pending = true;
|
|
373
|
+
remote.error = null;
|
|
374
|
+
const s = scope;
|
|
375
|
+
remote.timer = setTimeout(async () => {
|
|
376
|
+
const ctl = typeof AbortController === "function" ? new AbortController() : null;
|
|
377
|
+
remote.ctl = ctl;
|
|
378
|
+
let rows = [];
|
|
379
|
+
let error = null;
|
|
380
|
+
try {
|
|
381
|
+
rows = await s.search(q, ctl?.signal);
|
|
382
|
+
} catch (err) {
|
|
383
|
+
if (err?.name === "AbortError") return;
|
|
384
|
+
error = err?.userMessage || "The search failed.";
|
|
385
|
+
console.warn("[cmdpal] scope search failed", err);
|
|
386
|
+
}
|
|
387
|
+
if (seq !== remote.seq || scope !== s || overlay !== mine) return;
|
|
388
|
+
remote.rows = _decorateIcons(Array.isArray(rows) ? rows : [], taxonomy).map((r) => ({ ...r, _hits: _match(String(r.label || ""), q)?.hits || [] }));
|
|
389
|
+
remote.pending = false;
|
|
390
|
+
remote.error = error;
|
|
391
|
+
filtered = remote.rows.slice(0, MAX_ROWS);
|
|
392
|
+
sections = [{ title: s.label, start: 0, count: filtered.length }];
|
|
393
|
+
active = 0;
|
|
394
|
+
render();
|
|
395
|
+
}, REMOTE_DEBOUNCE_MS);
|
|
396
|
+
};
|
|
397
|
+
const refilterGrouped = () => {
|
|
398
|
+
const raw = input.value;
|
|
399
|
+
if (scope) {
|
|
400
|
+
runRemote(raw.trim());
|
|
401
|
+
filtered = remote.rows;
|
|
402
|
+
sections = [{ title: scope.label, start: 0, count: filtered.length }];
|
|
403
|
+
active = 0;
|
|
404
|
+
render();
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
const commandsOnly = !typeFilter && raw.startsWith(">");
|
|
408
|
+
const q = (commandsOnly ? raw.slice(1) : raw).trim();
|
|
409
|
+
const filterSet = typeFilter ? new Set(typeFilter) : null;
|
|
410
|
+
if (!q && !commandsOnly && !filterSet) {
|
|
411
|
+
filtered = emptyRows.slice(0, MAX_ROWS).map((r) => ({ ...r, _hits: [] }));
|
|
412
|
+
sections = [{ title: filtered.length ? emptyLabel : "", start: 0, count: filtered.length }];
|
|
413
|
+
active = 0;
|
|
414
|
+
render();
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
let pool = entities;
|
|
418
|
+
if (commandsOnly) pool = pool.filter((e) => e.kind === "command");
|
|
419
|
+
else if (filterSet) pool = pool.filter((e) => filterSet.has(_groupOf(e)) || filterSet.has(e.kind));
|
|
420
|
+
else if (q.length < commandMinChars) pool = pool.filter((e) => e.kind !== "command");
|
|
421
|
+
const scored = [];
|
|
422
|
+
if (!q) {
|
|
423
|
+
for (const e of pool) scored.push({ ...e, _hits: [], _score: 0 });
|
|
424
|
+
scored.sort((a, b) => String(a.label || a.id).localeCompare(String(b.label || b.id)));
|
|
425
|
+
} else {
|
|
426
|
+
for (const e of pool) {
|
|
427
|
+
let m = _match(String(e.label || e.id || ""), q);
|
|
428
|
+
if (!m) {
|
|
429
|
+
const extra = _searched(e);
|
|
430
|
+
const k = extra ? _match(extra, q) : null;
|
|
431
|
+
if (k) m = { s: Math.min(k.s, 20), hits: [] };
|
|
432
|
+
}
|
|
433
|
+
if (!m) continue;
|
|
434
|
+
const penalty = e.kind === "command" && !commandsOnly ? 5 : 0;
|
|
435
|
+
scored.push({ ...e, _hits: m.hits, _score: m.s + (Number(e.boost) || 0) - penalty });
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
const order = [];
|
|
439
|
+
const by = /* @__PURE__ */ new Map();
|
|
440
|
+
for (const r of scored) {
|
|
441
|
+
const key = commandsOnly ? "command" : _groupOf(r);
|
|
442
|
+
if (!by.has(key)) {
|
|
443
|
+
by.set(key, []);
|
|
444
|
+
order.push(key);
|
|
445
|
+
}
|
|
446
|
+
by.get(key).push(r);
|
|
447
|
+
if (r.groupLabel && !namedGroups.has(key)) namedGroups.set(key, r.groupLabel);
|
|
448
|
+
}
|
|
449
|
+
const top = (k) => by.get(k).slice().sort(_byScore)[0];
|
|
450
|
+
const keys = order.slice().sort((a, b) => (q ? _byScore(top(a), top(b)) : 0) || order.indexOf(a) - order.indexOf(b));
|
|
451
|
+
const capped = !filterSet && !commandsOnly;
|
|
452
|
+
filtered = [];
|
|
453
|
+
sections = [];
|
|
454
|
+
for (const key of keys) {
|
|
455
|
+
const rows = by.get(key);
|
|
456
|
+
if (q) rows.sort(_byScore);
|
|
457
|
+
const shown = capped ? rows.slice(0, groupCap) : rows;
|
|
458
|
+
const start = filtered.length;
|
|
459
|
+
filtered.push(...shown);
|
|
460
|
+
if (shown.length < rows.length) {
|
|
461
|
+
filtered.push({ more: key, label: `Show all ${rows.length} ${groupLabel(key).toLowerCase()}` });
|
|
462
|
+
}
|
|
463
|
+
sections.push({ title: groupLabel(key), start, count: filtered.length - start });
|
|
464
|
+
if (filtered.length >= MAX_ROWS) break;
|
|
465
|
+
}
|
|
466
|
+
active = 0;
|
|
467
|
+
render();
|
|
468
|
+
};
|
|
160
469
|
const refilter = () => {
|
|
161
470
|
tryConsumePrefix();
|
|
471
|
+
if (grouped) {
|
|
472
|
+
refilterGrouped();
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
162
475
|
const q = input.value.trim().toLowerCase();
|
|
163
476
|
const filterSet = typeFilter ? new Set(typeFilter) : null;
|
|
164
477
|
const pool = filterSet ? entities.filter((e) => filterSet.has(e.kind)) : entities;
|
|
@@ -172,21 +485,34 @@ function createCommandPalette({
|
|
|
172
485
|
};
|
|
173
486
|
input.addEventListener("input", refilter);
|
|
174
487
|
input.addEventListener("keydown", (e) => {
|
|
488
|
+
const move = (to) => {
|
|
489
|
+
active = to;
|
|
490
|
+
if (grouped) paintActive();
|
|
491
|
+
else render();
|
|
492
|
+
};
|
|
175
493
|
if (e.key === "ArrowDown") {
|
|
176
494
|
e.preventDefault();
|
|
177
|
-
|
|
178
|
-
render();
|
|
495
|
+
move(Math.min(filtered.length - 1, active + 1));
|
|
179
496
|
} else if (e.key === "ArrowUp") {
|
|
180
497
|
e.preventDefault();
|
|
181
|
-
|
|
182
|
-
render();
|
|
498
|
+
move(Math.max(0, active - 1));
|
|
183
499
|
} else if (e.key === "Enter") {
|
|
184
500
|
e.preventDefault();
|
|
185
|
-
commit(active);
|
|
501
|
+
commit(active, _modeOf(e));
|
|
186
502
|
} else if (e.key === "Escape") {
|
|
187
503
|
close();
|
|
188
|
-
} else if (e.key === "
|
|
504
|
+
} else if (grouped && e.key === "Tab" && !e.shiftKey) {
|
|
505
|
+
const row = filtered[active];
|
|
506
|
+
if (row?.more) {
|
|
507
|
+
e.preventDefault();
|
|
508
|
+
showAll(row.more);
|
|
509
|
+
} else if (row?.drillLabel && enterScope(row)) e.preventDefault();
|
|
510
|
+
} else if (e.key === "Backspace" && input.value === "" && (typeFilter || scope)) {
|
|
189
511
|
e.preventDefault();
|
|
512
|
+
if (scope) {
|
|
513
|
+
leaveScope();
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
190
516
|
typeFilter = null;
|
|
191
517
|
typeFilterLabel = null;
|
|
192
518
|
renderChip();
|
|
@@ -194,38 +520,81 @@ function createCommandPalette({
|
|
|
194
520
|
}
|
|
195
521
|
});
|
|
196
522
|
input.focus();
|
|
197
|
-
|
|
523
|
+
const commandRows = () => {
|
|
524
|
+
let extra = [];
|
|
525
|
+
if (typeof commands === "function") {
|
|
526
|
+
try {
|
|
527
|
+
extra = (commands() || []).map((c) => ({ kind: "command", ...c }));
|
|
528
|
+
} catch (err) {
|
|
529
|
+
console.warn("[cmdpal] commands() threw", err);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
return [...STATIC_COMMANDS, ...extra];
|
|
533
|
+
};
|
|
534
|
+
const withCommands = (rows) => grouped ? [...rows, ...commandRows()] : [...commandRows(), ...rows];
|
|
535
|
+
entities = _decorateIcons(withCommands(cache && cached ? cached : []), taxonomy);
|
|
536
|
+
if (grouped) {
|
|
537
|
+
refilter();
|
|
538
|
+
if (typeof empty === "function") {
|
|
539
|
+
try {
|
|
540
|
+
const got = await empty();
|
|
541
|
+
if (overlay !== mine) return;
|
|
542
|
+
emptyRows = _decorateIcons(Array.isArray(got) ? got : [], taxonomy);
|
|
543
|
+
if (!input.value && !typeFilter && !scope) refilter();
|
|
544
|
+
} catch (err) {
|
|
545
|
+
console.warn("[cmdpal] empty() failed", err);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
198
549
|
if (api) {
|
|
199
550
|
try {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
);
|
|
204
|
-
if (input.value) refilter();
|
|
551
|
+
const loaded = await catalog.loadAll(api);
|
|
552
|
+
if (cache) cached = loaded;
|
|
553
|
+
if (overlay !== mine) return;
|
|
554
|
+
entities = _decorateIcons(withCommands(loaded), taxonomy);
|
|
555
|
+
if (grouped ? !scope && (input.value || typeFilter) : input.value) refilter();
|
|
205
556
|
} catch (err) {
|
|
206
557
|
console.warn("[cmdpal] entity load failed", err);
|
|
207
558
|
}
|
|
208
559
|
}
|
|
209
560
|
};
|
|
210
|
-
return { open, close, toggle, isOpen };
|
|
561
|
+
return { open, close, toggle, isOpen, invalidate };
|
|
211
562
|
}
|
|
212
|
-
function _markup(taxonomy, placeholder, wm) {
|
|
563
|
+
function _markup(taxonomy, placeholder, wm, { chrome = {}, grouped = false } = {}) {
|
|
213
564
|
const PANEL_CHIPS = [
|
|
214
565
|
{ side: "left", icon: "menu", label: "Left nav" },
|
|
215
566
|
{ side: "right", icon: "dock_to_left", label: "Right panel" },
|
|
216
567
|
{ side: "bottom", icon: "dock_to_bottom", label: "Bottom panel" }
|
|
217
568
|
];
|
|
218
|
-
const panelToggles = PANEL_CHIPS.filter((p) => wm.content?.has?.(`panel:${p.side}`) !== false).map((p) => `
|
|
569
|
+
const panelToggles = chrome.panels === false ? "" : PANEL_CHIPS.filter((p) => wm.content?.has?.(`panel:${p.side}`) !== false).map((p) => `
|
|
219
570
|
<button class="twm-chip" data-toggle="${p.side}">
|
|
220
571
|
<span class="material-symbols-outlined">${p.icon}</span>
|
|
221
572
|
${p.label}
|
|
222
573
|
</button>`).join("");
|
|
223
|
-
const chips = taxonomy.topNavEntries().map((k) => `
|
|
574
|
+
const chips = chrome.pages === false ? "" : taxonomy.topNavEntries().map((k) => `
|
|
224
575
|
<button class="twm-chip" data-shortcut="${k.kind}">
|
|
225
576
|
<span class="material-symbols-outlined">${k.icon}</span>
|
|
226
577
|
${k.label}
|
|
227
578
|
</button>
|
|
228
579
|
`).join("");
|
|
580
|
+
if (grouped) {
|
|
581
|
+
return `
|
|
582
|
+
<div class="twm-cmdpal twm-cmdpal--grouped" role="dialog" aria-label="Command palette">
|
|
583
|
+
<div class="twm-cmdpal__inputrow">
|
|
584
|
+
<span class="material-symbols-outlined twm-cmdpal__search-icon">search</span>
|
|
585
|
+
<span class="twm-cmdpal__type-chip-slot" data-role="type-chip"></span>
|
|
586
|
+
<input type="text" class="twm-cmdpal__input"
|
|
587
|
+
placeholder="${_esc(placeholder)}"
|
|
588
|
+
autocomplete="off" spellcheck="false" role="combobox"
|
|
589
|
+
aria-expanded="true" aria-controls="twm-cmdpal-list" />
|
|
590
|
+
<kbd class="twm-cmdpal__esc">Esc</kbd>
|
|
591
|
+
</div>
|
|
592
|
+
${chips ? `<div class="twm-cmdpal__chips">${chips}</div>` : ""}
|
|
593
|
+
${panelToggles ? `<div class="twm-cmdpal__toggles">${panelToggles}</div>` : ""}
|
|
594
|
+
<div class="twm-cmdpal__list" id="twm-cmdpal-list" role="listbox" data-role="list"></div>
|
|
595
|
+
<div class="twm-cmdpal__footer" data-role="footer"></div>
|
|
596
|
+
</div>`;
|
|
597
|
+
}
|
|
229
598
|
return `
|
|
230
599
|
<div class="twm-cmdpal" role="dialog" aria-label="Command palette">
|
|
231
600
|
<div class="twm-cmdpal__inputrow">
|
|
@@ -271,6 +640,98 @@ function _search(entities, q) {
|
|
|
271
640
|
scored.sort((a, b) => a.r - b.r || a.e.label.localeCompare(b.e.label));
|
|
272
641
|
return scored.map((x) => x.e);
|
|
273
642
|
}
|
|
643
|
+
function _drawn(r) {
|
|
644
|
+
return r.context !== void 0 ? r.context : r.hint || "";
|
|
645
|
+
}
|
|
646
|
+
function _searched(r) {
|
|
647
|
+
const k = r.keywords !== void 0 ? r.keywords : r.hint;
|
|
648
|
+
return Array.isArray(k) ? k.join(" ") : String(k || "");
|
|
649
|
+
}
|
|
650
|
+
function _groupOf(r) {
|
|
651
|
+
return r.group || r.kind;
|
|
652
|
+
}
|
|
653
|
+
function _public(r) {
|
|
654
|
+
const { _hits, _score, ...rest } = r;
|
|
655
|
+
return rest;
|
|
656
|
+
}
|
|
657
|
+
function _byScore(a, b) {
|
|
658
|
+
return b._score - a._score || String(a.label || "").length - String(b.label || "").length || String(a.label || "").localeCompare(String(b.label || ""));
|
|
659
|
+
}
|
|
660
|
+
function _modeOf(e) {
|
|
661
|
+
if (e?.ctrlKey || e?.metaKey) return "split";
|
|
662
|
+
if (e?.shiftKey) return "window";
|
|
663
|
+
return "open";
|
|
664
|
+
}
|
|
665
|
+
var _WORD_BREAK = /[\s_\-–—.:/\\()[\]&,+·›]/;
|
|
666
|
+
function matchLabel(text, query) {
|
|
667
|
+
return _match(text, query);
|
|
668
|
+
}
|
|
669
|
+
function _match(text, query) {
|
|
670
|
+
const t = String(text || "").toLowerCase();
|
|
671
|
+
const q = String(query || "").toLowerCase().trim();
|
|
672
|
+
if (!q || !t) return null;
|
|
673
|
+
const span = (a, n) => Array.from({ length: n }, (_, i) => a + i);
|
|
674
|
+
if (t === q) return { s: 100, hits: span(0, q.length) };
|
|
675
|
+
if (t.startsWith(q)) return { s: 90, hits: span(0, q.length) };
|
|
676
|
+
const words = q.split(/\s+/).filter(Boolean);
|
|
677
|
+
{
|
|
678
|
+
const hits2 = [];
|
|
679
|
+
let from = 0;
|
|
680
|
+
let ok = true;
|
|
681
|
+
for (const w of words) {
|
|
682
|
+
let at = -1;
|
|
683
|
+
for (let i = from; i <= t.length - w.length; i++) {
|
|
684
|
+
if ((i === 0 || _WORD_BREAK.test(t[i - 1])) && t.startsWith(w, i)) {
|
|
685
|
+
at = i;
|
|
686
|
+
break;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
if (at < 0) {
|
|
690
|
+
ok = false;
|
|
691
|
+
break;
|
|
692
|
+
}
|
|
693
|
+
hits2.push(...span(at, w.length));
|
|
694
|
+
from = at + w.length;
|
|
695
|
+
}
|
|
696
|
+
if (ok) return { s: 75 - Math.min(words.length, 5), hits: hits2 };
|
|
697
|
+
}
|
|
698
|
+
const sub = t.indexOf(q);
|
|
699
|
+
if (sub >= 0) return { s: 60, hits: span(sub, q.length) };
|
|
700
|
+
const flat = q.replace(/\s+/g, "");
|
|
701
|
+
const hits = [];
|
|
702
|
+
let j = 0;
|
|
703
|
+
for (let i = 0; i < t.length && j < flat.length; i++) {
|
|
704
|
+
if (t[i] === flat[j]) {
|
|
705
|
+
hits.push(i);
|
|
706
|
+
j++;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
if (j < flat.length) return null;
|
|
710
|
+
const spread = hits[hits.length - 1] - hits[0] + 1 - flat.length;
|
|
711
|
+
if (spread > 3 * flat.length + 6) return null;
|
|
712
|
+
return { s: Math.max(1, 30 - spread), hits };
|
|
713
|
+
}
|
|
714
|
+
function _mark(text, hits) {
|
|
715
|
+
const s = String(text ?? "");
|
|
716
|
+
if (!hits?.length) return _esc(s);
|
|
717
|
+
const set = new Set(hits);
|
|
718
|
+
let out = "";
|
|
719
|
+
let open = false;
|
|
720
|
+
for (let i = 0; i < s.length; i++) {
|
|
721
|
+
const on = set.has(i);
|
|
722
|
+
if (on && !open) {
|
|
723
|
+
out += "<mark>";
|
|
724
|
+
open = true;
|
|
725
|
+
}
|
|
726
|
+
if (!on && open) {
|
|
727
|
+
out += "</mark>";
|
|
728
|
+
open = false;
|
|
729
|
+
}
|
|
730
|
+
out += _esc(s[i]);
|
|
731
|
+
}
|
|
732
|
+
if (open) out += "</mark>";
|
|
733
|
+
return out;
|
|
734
|
+
}
|
|
274
735
|
function _esc(s) {
|
|
275
736
|
return String(s ?? "").replace(/[&<>"']/g, (c) => ({
|
|
276
737
|
"&": "&",
|
|
@@ -8141,6 +8602,7 @@ export {
|
|
|
8141
8602
|
makeLeaf,
|
|
8142
8603
|
makeLoadingOverlay,
|
|
8143
8604
|
makeSplit,
|
|
8605
|
+
matchLabel,
|
|
8144
8606
|
mountLandingActions,
|
|
8145
8607
|
mountLandingShell,
|
|
8146
8608
|
mountNavPanel,
|