Skip to content

Commit db65a71

Browse files
CedrickGDclaude
andcommitted
Add global search command palette triggered by Ctrl+K
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 9da0770 commit db65a71

4 files changed

Lines changed: 531 additions & 0 deletions

File tree

RazorReaper/Components/Layout/MainLayout.razor

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
@using RazorReaper.Components.Shared
33

44
<NotificationContainer />
5+
<GlobalSearch />
56

67
<div class="app-container">
78
<SharedNavbar />
Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
@using Microsoft.AspNetCore.Components.Routing
2+
@inject NavigationManager Navigation
3+
@inject IJSRuntime JS
4+
@implements IAsyncDisposable
5+
6+
@if (_isVisible)
7+
{
8+
<div class="search-overlay" @onclick="Close" @onkeydown="HandleKeyDown">
9+
<div class="search-container" @onclick:stopPropagation>
10+
<div class="search-input-wrapper">
11+
<svg class="search-input-icon" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
12+
<circle cx="9" cy="9" r="5.5" />
13+
<path d="m13.5 13.5 3 3" />
14+
</svg>
15+
<input @ref="_inputRef"
16+
type="text"
17+
class="search-input"
18+
placeholder="Search pages..."
19+
@bind-value="_query"
20+
@bind-value:event="oninput"
21+
@onkeydown="HandleKeyDown"
22+
autocomplete="off"
23+
spellcheck="false" />
24+
<kbd class="search-kbd">ESC</kbd>
25+
</div>
26+
27+
@if (FilteredResults.Any())
28+
{
29+
<ul class="search-results">
30+
@foreach (var (item, index) in FilteredResults.Select((v, i) => (v, i)))
31+
{
32+
<li class="search-result-item @(index == _selectedIndex ? "selected" : "")"
33+
@onclick="() => NavigateTo(item.Route)"
34+
@onmouseenter="() => _selectedIndex = index">
35+
<div class="search-result-icon">
36+
@((MarkupString)item.Icon)
37+
</div>
38+
<div class="search-result-text">
39+
<span class="search-result-title">@item.Title</span>
40+
<span class="search-result-description">@item.Description</span>
41+
</div>
42+
<span class="search-result-category">@item.Category</span>
43+
</li>
44+
}
45+
</ul>
46+
}
47+
else if (!string.IsNullOrWhiteSpace(_query))
48+
{
49+
<div class="search-empty">
50+
<span>No results for "<strong>@_query</strong>"</span>
51+
</div>
52+
}
53+
</div>
54+
</div>
55+
}
56+
57+
@code {
58+
private bool _isVisible;
59+
private string _query = "";
60+
private int _selectedIndex;
61+
private ElementReference _inputRef;
62+
private DotNetObjectReference<GlobalSearch>? _dotNetRef;
63+
64+
private record SearchEntry(string Title, string Description, string Route, string Category, string Icon, string[] Keywords);
65+
66+
private static readonly List<SearchEntry> AllPages = new()
67+
{
68+
new("Home", "Dashboard, updates & recent activity", "/home", "Core",
69+
"<svg viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 9.5 10 4l7 5.5\"/><path d=\"M5.5 8.5V16h9V8.5\"/><path d=\"M8.5 16v-3.5h3V16\"/></svg>",
70+
new[] { "home", "dashboard", "updates", "activity", "welcome" }),
71+
72+
new("Server", "Connect and manage ARK servers", "/server", "Core",
73+
"<svg viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"4\" width=\"14\" height=\"5\" rx=\"1.4\"/><rect x=\"3\" y=\"11\" width=\"14\" height=\"5\" rx=\"1.4\"/><path d=\"M12.5 6.5h1\"/><path d=\"M15 6.5h.5\"/><path d=\"M12.5 13.5h1\"/><path d=\"M15 13.5h.5\"/></svg>",
74+
new[] { "server", "connect", "ip", "manage", "query" }),
75+
76+
new("Game", "Control and monitor ARK: Survival Evolved", "/game", "Core",
77+
"<svg viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"2.5\" y=\"4\" width=\"15\" height=\"12\" rx=\"2\"/><path d=\"M7 8l3 2-3 2\"/><path d=\"M11.5 12h3\"/></svg>",
78+
new[] { "game", "launch", "start", "ark", "monitor", "status", "running" }),
79+
80+
new("INI Changer", "Manage & optimize ARK configuration presets", "/ini-changer", "ARK Tweaks",
81+
"<svg viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M4 5.5h12\"/><path d=\"M4 10h12\"/><path d=\"M4 14.5h12\"/><circle cx=\"7\" cy=\"5.5\" r=\"1.25\"/><circle cx=\"12.5\" cy=\"10\" r=\"1.25\"/><circle cx=\"9\" cy=\"14.5\" r=\"1.25\"/></svg>",
82+
new[] { "ini", "config", "configuration", "presets", "settings", "basedeviceprofiles", "optimize" }),
83+
84+
new("Vision Tools", "TEK camera behavior and scope visibility", "/suitfov", "ARK Tweaks",
85+
"<svg viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M2.5 10s3-4.5 7.5-4.5S17.5 10 17.5 10s-3 4.5-7.5 4.5S2.5 10 2.5 10Z\"/><circle cx=\"10\" cy=\"10\" r=\"2.2\"/></svg>",
86+
new[] { "vision", "fov", "camera", "tek", "suit", "scope", "zoom" }),
87+
88+
new("Launch Options", "Quick ARK startup flags with trade-offs", "/launch-options", "ARK Tweaks",
89+
"<svg viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M5 16.5V3.5\"/><path d=\"M5 4h8l-1.6 2.6L13 9H5\"/></svg>",
90+
new[] { "launch", "options", "flags", "startup", "arguments", "steam", "parameters" }),
91+
92+
new("Fonts", "Customize ARK font settings", "/fonts", "ARK Tweaks",
93+
"<svg viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M6 15 10 5l4 10\"/><path d=\"M7.4 11.5h5.2\"/></svg>",
94+
new[] { "fonts", "text", "typography", "chinese", "global", "install" }),
95+
96+
new("Pixel Glitch", "Manage ARK texture files for visual mods", "/pixel", "ARK Tweaks",
97+
"<svg viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M10.5 2.5 4 10h5l-.6 7.5L16 10h-5l-.5-7.5Z\"/></svg>",
98+
new[] { "pixel", "glitch", "texture", "visual", "riot", "saddle", "armor" }),
99+
100+
new("Paintings", "Manage your in-game paintings", "/paintings", "ARK Tweaks",
101+
"<svg viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"4\" width=\"14\" height=\"12\" rx=\"2\"/><path d=\"m6.2 12.5 2.5-2.8 2.4 2.4 2.7-3.3 1.9 3.7\"/><circle cx=\"7\" cy=\"7.5\" r=\"1\"/></svg>",
102+
new[] { "paintings", "mypaintings", "art", "images" }),
103+
104+
new("Mutagen Prices", "Browse creature mutation values by name", "/dino-prices", "Mods & Intel",
105+
"<svg viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M11 3H6.5a2 2 0 0 0-2 2v4.5L11 16l5-5-6-8Z\"/><circle cx=\"7.3\" cy=\"7.3\" r=\"1\"/></svg>",
106+
new[] { "mutagen", "prices", "dino", "creature", "gen2", "mutation", "values" }),
107+
108+
new("OC BPs", "Genesis 2 mission rewards for overcapped blueprints", "/oc-bps", "Mods & Intel",
109+
"<svg viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"4.5\" y=\"3.5\" width=\"11\" height=\"13\" rx=\"2\"/><path d=\"M8 6h4\"/><path d=\"M7 9.5h6\"/><path d=\"M7 12.5h6\"/></svg>",
110+
new[] { "oc", "blueprints", "bps", "genesis", "mission", "rewards", "overcapped" }),
111+
112+
new("Bosses", "Boss tribute guide sorted by map", "/bosses", "Mods & Intel",
113+
"<svg viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M10 3 5 5.8V10c0 3 1.7 5.3 5 6.8 3.3-1.5 5-3.8 5-6.8V5.8L10 3Z\"/></svg>",
114+
new[] { "bosses", "tribute", "guide", "map", "requirements", "mini-boss", "fight" }),
115+
116+
new("Map Mods", "Server map mods with custom coordinates", "/server-specific", "Mods & Intel",
117+
"<svg viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M10 16s4.8-4.8 4.8-8A4.8 4.8 0 1 0 5.2 8c0 3.2 4.8 8 4.8 8Z\"/><circle cx=\"10\" cy=\"8\" r=\"1.8\"/></svg>",
118+
new[] { "map", "mods", "coordinates", "landmarks", "spots", "mesa", "server-specific" }),
119+
120+
new("Steam Mods", "Browse installed workshop mods", "/steam-mods", "Mods & Intel",
121+
"<svg viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M10 3.5 4.5 6.7v6.6L10 16.5l5.5-3.2V6.7L10 3.5Z\"/><path d=\"M10 10V16\"/><path d=\"M4.5 6.7 10 10l5.5-3.3\"/></svg>",
122+
new[] { "steam", "workshop", "mods", "installed", "browse" }),
123+
124+
new("Building", "Foundation raising and lowering tutorials", "/building", "Utilities",
125+
"<svg viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"4\" y=\"4\" width=\"4\" height=\"4\" rx=\"1\"/><rect x=\"12\" y=\"4\" width=\"4\" height=\"4\" rx=\"1\"/><rect x=\"8\" y=\"12\" width=\"4\" height=\"4\" rx=\"1\"/></svg>",
126+
new[] { "building", "foundation", "raising", "lowering", "tutorials", "video" }),
127+
128+
new("Auto Clicker", "Advanced mouse automation tool", "/autoclicker", "Utilities",
129+
"<svg viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M10 3.5v3\"/><path d=\"M7 5.5h6\"/><path d=\"M10 8.5v8\"/><path d=\"M10 16.5c3 0 4.8-1.8 4.8-4.3 0-1.8-1.1-3.2-2.9-3.7\"/><path d=\"M10 16.5c-3 0-4.8-1.8-4.8-4.3 0-1.8 1.1-3.2 2.9-3.7\"/></svg>",
130+
new[] { "autoclicker", "auto", "clicker", "mouse", "automation", "click", "hotkey" }),
131+
132+
new("Macro / AHK", "Video, refs, scripts for macros", "/crafting-scripts", "Utilities",
133+
"<svg viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"2.5\" y=\"6\" width=\"15\" height=\"8.5\" rx=\"2\"/><path d=\"M5.5 9.5h1\"/><path d=\"M8.5 9.5h1\"/><path d=\"M11.5 9.5h1\"/><path d=\"M14.5 9.5h0.5\"/><path d=\"M5.5 12h5\"/><path d=\"M12.5 12h2.5\"/></svg>",
134+
new[] { "macro", "ahk", "autohotkey", "crafting", "scripts", "jitbit" }),
135+
136+
new("Troubleshoot", "Logging, diagnostics, and quick fixes", "/troubleshoot", "Help & About",
137+
"<svg viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M10 3.5 16.5 15H3.5L10 3.5Z\"/><path d=\"M10 7.2v4.3\"/><path d=\"M10 13.6h.01\"/></svg>",
138+
new[] { "troubleshoot", "diagnostics", "logging", "error", "fix", "debug", "logs" }),
139+
140+
new("Credits", "Developer information & contact", "/credits", "Help & About",
141+
"<svg viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"10\" cy=\"10\" r=\"6.5\"/><path d=\"M10 8v4\"/><path d=\"M10 6.3h.01\"/></svg>",
142+
new[] { "credits", "about", "developer", "contact", "info", "support" }),
143+
};
144+
145+
private List<SearchEntry> FilteredResults
146+
{
147+
get
148+
{
149+
if (string.IsNullOrWhiteSpace(_query))
150+
return AllPages;
151+
152+
var q = _query.Trim().ToLowerInvariant();
153+
var scored = new List<(SearchEntry Entry, int Score)>();
154+
155+
foreach (var entry in AllPages)
156+
{
157+
int score = 0;
158+
159+
// Title exact start match (highest priority)
160+
if (entry.Title.StartsWith(q, StringComparison.OrdinalIgnoreCase))
161+
score += 100;
162+
// Title contains
163+
else if (entry.Title.Contains(q, StringComparison.OrdinalIgnoreCase))
164+
score += 80;
165+
166+
// Keyword exact match
167+
if (entry.Keywords.Any(k => k.Equals(q, StringComparison.OrdinalIgnoreCase)))
168+
score += 90;
169+
// Keyword starts with
170+
else if (entry.Keywords.Any(k => k.StartsWith(q, StringComparison.OrdinalIgnoreCase)))
171+
score += 60;
172+
// Keyword contains
173+
else if (entry.Keywords.Any(k => k.Contains(q, StringComparison.OrdinalIgnoreCase)))
174+
score += 40;
175+
176+
// Description contains
177+
if (entry.Description.Contains(q, StringComparison.OrdinalIgnoreCase))
178+
score += 30;
179+
180+
// Category contains
181+
if (entry.Category.Contains(q, StringComparison.OrdinalIgnoreCase))
182+
score += 20;
183+
184+
if (score > 0)
185+
scored.Add((entry, score));
186+
}
187+
188+
_selectedIndex = 0;
189+
return scored.OrderByDescending(s => s.Score).Select(s => s.Entry).ToList();
190+
}
191+
}
192+
193+
protected override async Task OnAfterRenderAsync(bool firstRender)
194+
{
195+
if (firstRender)
196+
{
197+
_dotNetRef = DotNetObjectReference.Create(this);
198+
await JS.InvokeVoidAsync("registerGlobalSearch", _dotNetRef);
199+
}
200+
201+
if (_isVisible)
202+
{
203+
try
204+
{
205+
await _inputRef.FocusAsync();
206+
}
207+
catch
208+
{
209+
// Element may not be rendered yet
210+
}
211+
}
212+
}
213+
214+
[JSInvokable]
215+
public void ToggleSearch()
216+
{
217+
_isVisible = !_isVisible;
218+
if (_isVisible)
219+
{
220+
_query = "";
221+
_selectedIndex = 0;
222+
}
223+
_ = InvokeAsync(StateHasChanged);
224+
}
225+
226+
private void Close()
227+
{
228+
_isVisible = false;
229+
_query = "";
230+
_selectedIndex = 0;
231+
}
232+
233+
private void HandleKeyDown(KeyboardEventArgs e)
234+
{
235+
var results = FilteredResults;
236+
237+
switch (e.Key)
238+
{
239+
case "Escape":
240+
Close();
241+
break;
242+
case "ArrowDown":
243+
_selectedIndex = (_selectedIndex + 1) % Math.Max(results.Count, 1);
244+
break;
245+
case "ArrowUp":
246+
_selectedIndex = (_selectedIndex - 1 + Math.Max(results.Count, 1)) % Math.Max(results.Count, 1);
247+
break;
248+
case "Enter":
249+
if (results.Count > 0 && _selectedIndex < results.Count)
250+
NavigateTo(results[_selectedIndex].Route);
251+
break;
252+
}
253+
}
254+
255+
private void NavigateTo(string route)
256+
{
257+
Close();
258+
Navigation.NavigateTo(route);
259+
}
260+
261+
public async ValueTask DisposeAsync()
262+
{
263+
try
264+
{
265+
await JS.InvokeVoidAsync("unregisterGlobalSearch");
266+
}
267+
catch
268+
{
269+
// JS interop may fail during disposal
270+
}
271+
_dotNetRef?.Dispose();
272+
}
273+
}

0 commit comments

Comments
 (0)