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/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
- list.innerHTML = filtered.slice(0, 200).map((e, i) => `
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
- const commit = (i) => {
126
- const pick = filtered[i];
127
- if (!pick) return;
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
- active = Math.min(filtered.length - 1, active + 1);
178
- render();
495
+ move(Math.min(filtered.length - 1, active + 1));
179
496
  } else if (e.key === "ArrowUp") {
180
497
  e.preventDefault();
181
- active = Math.max(0, active - 1);
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 === "Backspace" && input.value === "" && typeFilter) {
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
- entities = _decorateIcons(STATIC_COMMANDS.slice(), taxonomy);
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
- entities = _decorateIcons(
201
- [...STATIC_COMMANDS, ...await catalog.loadAll(api)],
202
- taxonomy
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
  "&": "&amp;",
@@ -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,