@ChildContent
diff --git a/src/RolemasterDb.App/Components/Shell/ShellOmniboxPalette.razor b/src/RolemasterDb.App/Components/Shell/ShellOmniboxPalette.razor
new file mode 100644
index 0000000..c47ed98
--- /dev/null
+++ b/src/RolemasterDb.App/Components/Shell/ShellOmniboxPalette.razor
@@ -0,0 +1,313 @@
+@implements IDisposable
+@using System.Linq
+@using RolemasterDb.App.Frontend.AppState
+@inject NavigationManager NavigationManager
+@inject LookupService LookupService
+@inject RolemasterDb.App.Frontend.AppState.PinnedTablesState PinnedTablesState
+@inject RolemasterDb.App.Frontend.AppState.RecentTablesState RecentTablesState
+@inject RolemasterDb.App.Frontend.AppState.TableContextState TableContextState
+@inject RolemasterDb.App.Frontend.AppState.ShellOmniboxState ShellOmniboxState
+
+@if (ShellOmniboxState.IsOpen)
+{
+
+
+
+
+
+
+ @if (isLoading)
+ {
+
+ Loading searchable tables…
+
+ }
+ else
+ {
+ @if (MatchingCommands.Count > 0)
+ {
+
+
+ @foreach (var command in MatchingCommands)
+ {
+ OpenCommandAsync(command.Href)">
+
+ @command.Shortcut
+ @command.Description
+
+
+ @command.Label
+
+
+ }
+
+
+ }
+
+ @if (MatchingPinned.Count > 0)
+ {
+
+
+ @foreach (var table in MatchingPinned)
+ {
+ OpenTableAsync(table.Slug)">
+
+ @table.Label
+ @table.Family
+
+
+ Pinned
+
+
+ }
+
+
+ }
+
+ @if (MatchingRecent.Count > 0)
+ {
+
+
+ @foreach (var table in MatchingRecent)
+ {
+ OpenTableAsync(table.Slug)">
+
+ @table.Label
+ @table.Family
+
+
+ Recent
+
+
+ }
+
+
+ }
+
+ @if (MatchingTables.Count > 0)
+ {
+
+
+ @foreach (var table in MatchingTables)
+ {
+ OpenTableAsync(table.Key)">
+
+ @table.Label
+ @table.Family
+
+
+ @if (PinnedTablesState.IsPinned(table.Key))
+ {
+ Pinned
+ }
+ @($"{table.CurationPercentage}%")
+
+
+ }
+
+
+ }
+
+ @if (!HasResults)
+ {
+
+ Nothing matches “@query”.
+
+ }
+ }
+
+
+}
+
+@code {
+ private static readonly IReadOnlyList
Commands =
+ [
+ new("/tables", "Reference", "Open the reference tables surface.", "/tables"),
+ new("/curation", "Curation", "Open the queue-first curation workflow.", "/curation"),
+ new("/tools", "Tools", "Open the developer tools hub.", "/tools"),
+ new("/diag", "Diagnostics", "Open tooling diagnostics.", "/tools/diagnostics"),
+ new("/api", "API", "Open API surface documentation.", "/tools/api")
+ ];
+
+ private ElementReference queryInput;
+ private LookupReferenceData? referenceData;
+ private bool isLoading;
+ private bool shouldFocusInput;
+ private string query = string.Empty;
+
+ private bool HasResults =>
+ MatchingCommands.Count > 0 ||
+ MatchingPinned.Count > 0 ||
+ MatchingRecent.Count > 0 ||
+ MatchingTables.Count > 0;
+
+ private IReadOnlyList MatchingTables =>
+ referenceData?.CriticalTables
+ .Where(MatchesTableQuery)
+ .Take(8)
+ .ToList()
+ ?? [];
+
+ private IReadOnlyList MatchingPinned =>
+ PinnedTablesState.Items
+ .Where(item => MatchesText(item.Label, item.Family, item.Slug))
+ .Take(6)
+ .ToList();
+
+ private IReadOnlyList MatchingRecent =>
+ RecentTablesState.Items
+ .Where(item => MatchesText(item.Label, item.Family, item.Slug))
+ .Take(6)
+ .ToList();
+
+ private IReadOnlyList MatchingCommands =>
+ Commands
+ .Where(command => QueryStartsCommandMode()
+ ? command.Shortcut.Contains(query.Trim(), StringComparison.OrdinalIgnoreCase)
+ : MatchesText(command.Shortcut, command.Label, command.Description))
+ .Take(5)
+ .ToList();
+
+ protected override void OnInitialized()
+ {
+ ShellOmniboxState.Changed += HandleStateChanged;
+ NavigationManager.LocationChanged += HandleLocationChanged;
+ }
+
+ protected override async Task OnAfterRenderAsync(bool firstRender)
+ {
+ if (shouldFocusInput && ShellOmniboxState.IsOpen)
+ {
+ shouldFocusInput = false;
+ await queryInput.FocusAsync();
+ }
+ }
+
+ public void Dispose()
+ {
+ ShellOmniboxState.Changed -= HandleStateChanged;
+ NavigationManager.LocationChanged -= HandleLocationChanged;
+ }
+
+ private void HandleStateChanged()
+ {
+ _ = InvokeAsync(HandleStateChangedAsync);
+ }
+
+ private async Task HandleStateChangedAsync()
+ {
+ if (ShellOmniboxState.IsOpen)
+ {
+ shouldFocusInput = true;
+ await EnsureLoadedAsync();
+ }
+ else
+ {
+ query = string.Empty;
+ shouldFocusInput = false;
+ }
+
+ await InvokeAsync(StateHasChanged);
+ }
+
+ private void HandleLocationChanged(object? sender, LocationChangedEventArgs args)
+ {
+ if (!ShellOmniboxState.IsOpen)
+ {
+ return;
+ }
+
+ ShellOmniboxState.Close();
+ }
+
+ private async Task EnsureLoadedAsync()
+ {
+ if (referenceData is not null)
+ {
+ return;
+ }
+
+ isLoading = true;
+
+ try
+ {
+ await RecentTablesState.InitializeAsync();
+ await PinnedTablesState.InitializeAsync();
+ referenceData = await LookupService.GetReferenceDataAsync();
+ }
+ finally
+ {
+ isLoading = false;
+ }
+ }
+
+ private Task CloseAsync()
+ {
+ ShellOmniboxState.Close();
+ return Task.CompletedTask;
+ }
+
+ private async Task HandleInputKeyDownAsync(KeyboardEventArgs args)
+ {
+ if (string.Equals(args.Key, "Escape", StringComparison.Ordinal))
+ {
+ await CloseAsync();
+ }
+ }
+
+ private async Task OpenTableAsync(string tableSlug)
+ {
+ var snapshot = new TableContextSnapshot(
+ TableSlug: tableSlug,
+ Mode: TableContextMode.Reference);
+
+ await TableContextState.PersistAsync("tables", snapshot);
+ ShellOmniboxState.Close();
+ NavigationManager.NavigateTo(TableContextState.BuildUri("/tables", snapshot));
+ }
+
+ private Task OpenCommandAsync(string href)
+ {
+ ShellOmniboxState.Close();
+ NavigationManager.NavigateTo(href);
+ return Task.CompletedTask;
+ }
+
+ private bool MatchesTableQuery(CriticalTableReference table) =>
+ MatchesText(table.Label, table.Key, table.Family, table.SourceDocument);
+
+ private bool MatchesText(params string?[] values)
+ {
+ var normalizedQuery = query.Trim();
+ if (string.IsNullOrWhiteSpace(normalizedQuery))
+ {
+ return true;
+ }
+
+ return values.Any(value =>
+ !string.IsNullOrWhiteSpace(value) &&
+ value.Contains(normalizedQuery, StringComparison.OrdinalIgnoreCase));
+ }
+
+ private bool QueryStartsCommandMode() =>
+ query.TrimStart().StartsWith("/", StringComparison.Ordinal);
+}
diff --git a/src/RolemasterDb.App/Components/Shell/ShellOmniboxTrigger.razor b/src/RolemasterDb.App/Components/Shell/ShellOmniboxTrigger.razor
index ee1106a..4d64998 100644
--- a/src/RolemasterDb.App/Components/Shell/ShellOmniboxTrigger.razor
+++ b/src/RolemasterDb.App/Components/Shell/ShellOmniboxTrigger.razor
@@ -1,317 +1,19 @@
-@implements IDisposable
-@using System.Linq
-@using RolemasterDb.App.Frontend.AppState
-@inject NavigationManager NavigationManager
-@inject LookupService LookupService
-@inject RolemasterDb.App.Frontend.AppState.PinnedTablesState PinnedTablesState
-@inject RolemasterDb.App.Frontend.AppState.RecentTablesState RecentTablesState
-@inject RolemasterDb.App.Frontend.AppState.TableContextState TableContextState
+@inject RolemasterDb.App.Frontend.AppState.ShellOmniboxState ShellOmniboxState
Search tables or commands
-
- @if (isOpen)
- {
-
-
-
-
-
-
- @if (isLoading)
- {
-
- Loading searchable tables…
-
- }
- else
- {
- @if (MatchingCommands.Count > 0)
- {
-
-
- @foreach (var command in MatchingCommands)
- {
- OpenCommandAsync(command.Href)">
-
- @command.Shortcut
- @command.Description
-
-
- @command.Label
-
-
- }
-
-
- }
-
- @if (MatchingPinned.Count > 0)
- {
-
-
- @foreach (var table in MatchingPinned)
- {
- OpenTableAsync(table.Slug)">
-
- @table.Label
- @table.Family
-
-
- Pinned
-
-
- }
-
-
- }
-
- @if (MatchingRecent.Count > 0)
- {
-
-
- @foreach (var table in MatchingRecent)
- {
- OpenTableAsync(table.Slug)">
-
- @table.Label
- @table.Family
-
-
- Recent
-
-
- }
-
-
- }
-
- @if (MatchingTables.Count > 0)
- {
-
-
- @foreach (var table in MatchingTables)
- {
- OpenTableAsync(table.Key)">
-
- @table.Label
- @table.Family
-
-
- @if (PinnedTablesState.IsPinned(table.Key))
- {
- Pinned
- }
- @($"{table.CurationPercentage}%")
-
-
- }
-
-
- }
-
- @if (!HasResults)
- {
-
- Nothing matches “@query”.
-
- }
- }
-
-
- }
@code {
- private static readonly IReadOnlyList Commands =
- [
- new("/tables", "Reference", "Open the reference tables surface.", "/tables"),
- new("/curation", "Curation", "Open the queue-first curation workflow.", "/curation"),
- new("/tools", "Tools", "Open the developer tools hub.", "/tools"),
- new("/diag", "Diagnostics", "Open tooling diagnostics.", "/tools/diagnostics"),
- new("/api", "API", "Open API surface documentation.", "/tools/api")
- ];
-
- private ElementReference queryInput;
- private LookupReferenceData? referenceData;
- private bool isOpen;
- private bool isLoading;
- private bool shouldFocusInput;
- private string query = string.Empty;
-
- private bool HasResults =>
- MatchingCommands.Count > 0 ||
- MatchingPinned.Count > 0 ||
- MatchingRecent.Count > 0 ||
- MatchingTables.Count > 0;
-
- private IReadOnlyList MatchingTables =>
- referenceData?.CriticalTables
- .Where(MatchesTableQuery)
- .Take(8)
- .ToList()
- ?? [];
-
- private IReadOnlyList MatchingPinned =>
- PinnedTablesState.Items
- .Where(item => MatchesText(item.Label, item.Family, item.Slug))
- .Take(6)
- .ToList();
-
- private IReadOnlyList MatchingRecent =>
- RecentTablesState.Items
- .Where(item => MatchesText(item.Label, item.Family, item.Slug))
- .Take(6)
- .ToList();
-
- private IReadOnlyList MatchingCommands =>
- Commands
- .Where(command => QueryStartsCommandMode()
- ? command.Shortcut.Contains(query.Trim(), StringComparison.OrdinalIgnoreCase)
- : MatchesText(command.Shortcut, command.Label, command.Description))
- .Take(5)
- .ToList();
-
- protected override void OnInitialized()
+ private Task ToggleOpenAsync(MouseEventArgs _)
{
- NavigationManager.LocationChanged += HandleLocationChanged;
- }
-
- protected override async Task OnAfterRenderAsync(bool firstRender)
- {
- if (shouldFocusInput && isOpen)
- {
- shouldFocusInput = false;
- await queryInput.FocusAsync();
- }
- }
-
- private async Task ToggleOpenAsync(MouseEventArgs _)
- {
- if (isOpen)
- {
- await CloseAsync();
- return;
- }
-
- isOpen = true;
- shouldFocusInput = true;
- await EnsureLoadedAsync();
- }
-
- private Task CloseAsync()
- {
- isOpen = false;
- query = string.Empty;
- shouldFocusInput = false;
+ ShellOmniboxState.Toggle();
return Task.CompletedTask;
}
-
- private async Task EnsureLoadedAsync()
- {
- if (referenceData is not null)
- {
- return;
- }
-
- isLoading = true;
-
- try
- {
- await RecentTablesState.InitializeAsync();
- await PinnedTablesState.InitializeAsync();
- referenceData = await LookupService.GetReferenceDataAsync();
- }
- finally
- {
- isLoading = false;
- }
- }
-
- private async Task HandleInputKeyDownAsync(KeyboardEventArgs args)
- {
- if (string.Equals(args.Key, "Escape", StringComparison.Ordinal))
- {
- await CloseAsync();
- }
- }
-
- private async Task OpenTableAsync(string tableSlug)
- {
- var snapshot = new RolemasterDb.App.Frontend.AppState.TableContextSnapshot(
- TableSlug: tableSlug,
- Mode: RolemasterDb.App.Frontend.AppState.TableContextMode.Reference);
-
- await TableContextState.PersistAsync("tables", snapshot);
- await CloseAsync();
- NavigationManager.NavigateTo(TableContextState.BuildUri("/tables", snapshot));
- }
-
- private async Task OpenCommandAsync(string href)
- {
- await CloseAsync();
- NavigationManager.NavigateTo(href);
- }
-
- private bool MatchesTableQuery(CriticalTableReference table) =>
- MatchesText(table.Label, table.Key, table.Family, table.SourceDocument);
-
- private bool MatchesText(params string?[] values)
- {
- var normalizedQuery = query.Trim();
- if (string.IsNullOrWhiteSpace(normalizedQuery))
- {
- return true;
- }
-
- return values.Any(value =>
- !string.IsNullOrWhiteSpace(value) &&
- value.Contains(normalizedQuery, StringComparison.OrdinalIgnoreCase));
- }
-
- private bool QueryStartsCommandMode() =>
- query.TrimStart().StartsWith("/", StringComparison.Ordinal);
-
- private void HandleLocationChanged(object? sender, LocationChangedEventArgs args)
- {
- if (!isOpen)
- {
- return;
- }
-
- isOpen = false;
- query = string.Empty;
- shouldFocusInput = false;
- _ = InvokeAsync(StateHasChanged);
- }
-
- public void Dispose()
- {
- NavigationManager.LocationChanged -= HandleLocationChanged;
- }
}
diff --git a/src/RolemasterDb.App/Frontend/AppState/ShellOmniboxState.cs b/src/RolemasterDb.App/Frontend/AppState/ShellOmniboxState.cs
new file mode 100644
index 0000000..ff0900f
--- /dev/null
+++ b/src/RolemasterDb.App/Frontend/AppState/ShellOmniboxState.cs
@@ -0,0 +1,41 @@
+namespace RolemasterDb.App.Frontend.AppState;
+
+public sealed class ShellOmniboxState
+{
+ public bool IsOpen { get; private set; }
+
+ public event Action? Changed;
+
+ public void Open()
+ {
+ if (IsOpen)
+ {
+ return;
+ }
+
+ IsOpen = true;
+ Changed?.Invoke();
+ }
+
+ public void Close()
+ {
+ if (!IsOpen)
+ {
+ return;
+ }
+
+ IsOpen = false;
+ Changed?.Invoke();
+ }
+
+ public void Toggle()
+ {
+ if (IsOpen)
+ {
+ Close();
+ return;
+ }
+
+ Open();
+ }
+}
diff --git a/src/RolemasterDb.App/Program.cs b/src/RolemasterDb.App/Program.cs
index 5580603..b1f4296 100644
--- a/src/RolemasterDb.App/Program.cs
+++ b/src/RolemasterDb.App/Program.cs
@@ -15,6 +15,7 @@ builder.Services.AddScoped();
builder.Services.AddScoped();
builder.Services.AddScoped();
builder.Services.AddScoped();
+builder.Services.AddScoped();
builder.Services.AddScoped();
builder.Services.AddSingleton();
builder.Services.AddScoped();