@signal9/era-ui 2.34.0 → 2.35.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.
@@ -35,29 +35,43 @@
35
35
  const viewItems = $derived(view?.items ?? items);
36
36
  const viewPlaceholder = $derived(view?.placeholder ?? placeholder);
37
37
 
38
+ // Normalise the searchable text ONCE per view, not once per keystroke. The
39
+ // item set only changes on drill-in/out or when the host swaps `items`
40
+ // (a window opening, a setting toggling) — never while typing — so this
41
+ // derived is stable across a burst of keystrokes. Without it every character
42
+ // re-lowercased every label, value, and keyword (whole descriptions, for the
43
+ // doc commands) and allocated a fresh array per item, per key.
44
+ type Indexed = { item: CommandBarItem; label: string; value: string; keywords: string[] };
45
+ const searchIndex = $derived.by((): Indexed[] =>
46
+ viewItems.map((item) => ({
47
+ item,
48
+ label: item.label.toLowerCase(),
49
+ value: item.value.toLowerCase(),
50
+ keywords: item.keywords?.map((k) => k.toLowerCase()) ?? []
51
+ }))
52
+ );
53
+
38
54
  // Substring filter with exact-match priority (2 exact, 1 substring), the
39
55
  // same scoring the terminal uses — exact hits sort first so completion
40
- // always grabs the literal match.
56
+ // always grabs the literal match. One pass over the pre-lowercased index;
57
+ // sort is stable, so equal scores keep registry order.
41
58
  const filtered = $derived.by(() => {
42
59
  const q = value.trim().toLowerCase();
43
60
  if (!q) return viewItems;
44
- return viewItems
45
- .map((item) => {
46
- const label = item.label.toLowerCase();
47
- const kw = item.keywords?.map((k) => k.toLowerCase()) ?? [];
48
- let score = 0;
49
- if (label === q || kw.includes(q)) score = 2;
50
- else if (
51
- label.includes(q) ||
52
- item.value.toLowerCase().includes(q) ||
53
- kw.some((k) => k.includes(q))
54
- )
55
- score = 1;
56
- return { item, score };
57
- })
58
- .filter((s) => s.score > 0)
59
- .sort((a, b) => b.score - a.score)
60
- .map((s) => s.item);
61
+ const scored: { item: CommandBarItem; score: number }[] = [];
62
+ for (const entry of searchIndex) {
63
+ let score = 0;
64
+ if (entry.label === q || entry.keywords.includes(q)) score = 2;
65
+ else if (
66
+ entry.label.includes(q) ||
67
+ entry.value.includes(q) ||
68
+ entry.keywords.some((k) => k.includes(q))
69
+ )
70
+ score = 1;
71
+ if (score) scored.push({ item: entry.item, score });
72
+ }
73
+ scored.sort((a, b) => b.score - a.score);
74
+ return scored.map((s) => s.item);
61
75
  });
62
76
  const selected = $derived(filtered[selectedIndex]);
63
77
  const open = $derived(focused || value !== '');
@@ -68,7 +82,7 @@
68
82
  // always closes.
69
83
  const closeKeys = $derived(new Set(['Escape', ...hotkeyList.filter((b) => b.length === 1)]));
70
84
 
71
- // zsh-style typeahead: hint what Enter will run, right under the caret.
85
+ // Ghost completion: hint what Enter will run, right under the caret.
72
86
  // · empty input → the view placeholder
73
87
  // · label prefix match → the label's remaining tail, a completion you can
74
88
  // accept with Tab (type `cha`, ghost `ngelog`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signal9/era-ui",
3
- "version": "2.34.0",
3
+ "version": "2.35.0",
4
4
  "scripts": {
5
5
  "dev": "vite dev --host",
6
6
  "build": "vite build && npm run prepack",