@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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
//
|
|
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`)
|