flexdesk 0.8.0 → 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 +34 -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 +498 -29
- 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 +11 -1
- package/src/tiling/tile_renderer.js +9 -3
- package/src/tiling/wm.js +3 -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
|
+
}
|
|
322
|
+
renderChip();
|
|
323
|
+
return true;
|
|
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 };
|
|
157
349
|
renderChip();
|
|
350
|
+
refilter();
|
|
158
351
|
return true;
|
|
159
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
|
"&": "&",
|
|
@@ -2728,7 +3189,8 @@ var TileRenderer = class {
|
|
|
2728
3189
|
ctx,
|
|
2729
3190
|
onFocusChange,
|
|
2730
3191
|
onAfterRender,
|
|
2731
|
-
tabLayout = null
|
|
3192
|
+
tabLayout = null,
|
|
3193
|
+
newTabButton = true
|
|
2732
3194
|
}) {
|
|
2733
3195
|
if (!content || typeof content.mount !== "function") {
|
|
2734
3196
|
throw new Error("TileRenderer: a content registry is required");
|
|
@@ -2741,6 +3203,7 @@ var TileRenderer = class {
|
|
|
2741
3203
|
});
|
|
2742
3204
|
this.onAfterRender = onAfterRender || (() => {
|
|
2743
3205
|
});
|
|
3206
|
+
this.newTabButton = newTabButton !== false;
|
|
2744
3207
|
this._leafCache = /* @__PURE__ */ new Map();
|
|
2745
3208
|
this._drag = null;
|
|
2746
3209
|
this._tabDrag = null;
|
|
@@ -3318,7 +3781,7 @@ var TileRenderer = class {
|
|
|
3318
3781
|
);
|
|
3319
3782
|
}, true);
|
|
3320
3783
|
}
|
|
3321
|
-
if (this.tabLayout === "header" && !bar.querySelector(".twm-leaf__tab-new")) {
|
|
3784
|
+
if (this.tabLayout === "header" && this.newTabButton && !bar.querySelector(".twm-leaf__tab-new")) {
|
|
3322
3785
|
const add = document.createElement("button");
|
|
3323
3786
|
add.type = "button";
|
|
3324
3787
|
add.className = "twm-leaf__tab-new";
|
|
@@ -4051,7 +4514,8 @@ var WindowManager = class _WindowManager {
|
|
|
4051
4514
|
promoteInPlace = false,
|
|
4052
4515
|
tabLayout = null,
|
|
4053
4516
|
backToOpenList = false,
|
|
4054
|
-
floatActiveTab = false
|
|
4517
|
+
floatActiveTab = false,
|
|
4518
|
+
newTabButton = true
|
|
4055
4519
|
}) {
|
|
4056
4520
|
if (!taxonomy) throw new Error("WindowManager: a taxonomy is required");
|
|
4057
4521
|
if (!content || typeof content.mount !== "function") {
|
|
@@ -4102,6 +4566,8 @@ var WindowManager = class _WindowManager {
|
|
|
4102
4566
|
* `data-twm-tabs` and watches it, so an embedder can change it
|
|
4103
4567
|
* live without holding a renderer reference. */
|
|
4104
4568
|
tabLayout,
|
|
4569
|
+
/** C38. Whether the `header` strip draws its `+`. */
|
|
4570
|
+
newTabButton,
|
|
4105
4571
|
ctx: {
|
|
4106
4572
|
...this.ctx,
|
|
4107
4573
|
wm: this,
|
|
@@ -7691,6 +8157,7 @@ async function createShell({
|
|
|
7691
8157
|
backToOpenList = false,
|
|
7692
8158
|
floatActiveTab = false,
|
|
7693
8159
|
tabLayout = null,
|
|
8160
|
+
newTabButton = true,
|
|
7694
8161
|
chrome = {},
|
|
7695
8162
|
// An embedder that moved its sections out of the top bar — into an icon
|
|
7696
8163
|
// rail, say — passes the selector its own buttons match, and F1..F8 keep
|
|
@@ -7732,6 +8199,7 @@ async function createShell({
|
|
|
7732
8199
|
backToOpenList,
|
|
7733
8200
|
floatActiveTab,
|
|
7734
8201
|
tabLayout,
|
|
8202
|
+
newTabButton,
|
|
7735
8203
|
// `ctx` is the delivery vehicle for leaf-mounted chrome: tile_renderer
|
|
7736
8204
|
// spreads it into every content factory, which is how the breadcrumb
|
|
7737
8205
|
// gets `taxonomy` + `rootCrumb` without a content factory knowing they
|
|
@@ -8134,6 +8602,7 @@ export {
|
|
|
8134
8602
|
makeLeaf,
|
|
8135
8603
|
makeLoadingOverlay,
|
|
8136
8604
|
makeSplit,
|
|
8605
|
+
matchLabel,
|
|
8137
8606
|
mountLandingActions,
|
|
8138
8607
|
mountLandingShell,
|
|
8139
8608
|
mountNavPanel,
|