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/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
+ }
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
- 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;",
@@ -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,