feat(web): add Ctrl+K command palette search modal

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-01 22:28:02 -05:00
parent 1fafae9705
commit fc674847f5
3 changed files with 162 additions and 30 deletions

View File

@@ -1,33 +1,37 @@
@using TaskTracker.Api.Pages
@using TaskTracker.Core.Enums
@model List<TaskTracker.Core.Entities.WorkTask>
@* Placeholder — full implementation in Task 6 (Search Modal) *@
<div class="search-results">
@if (Model.Count == 0)
@{
var statusColors = new Dictionary<WorkTaskStatus, string>
{
<div style="padding: 16px; text-align: center; color: var(--color-text-secondary); font-size: 13px;">
No results found
[WorkTaskStatus.Pending] = "#64748b",
[WorkTaskStatus.Active] = "#3b82f6",
[WorkTaskStatus.Paused] = "#eab308",
[WorkTaskStatus.Completed] = "#22c55e",
[WorkTaskStatus.Abandoned] = "#ef4444",
};
}
@if (Model.Count == 0)
{
<div class="search-empty">No results found</div>
}
else
{
@for (var i = 0; i < Model.Count; i++)
{
var task = Model[i];
var color = statusColors.GetValueOrDefault(task.Status, "#64748b");
<div class="search-result @(i == 0 ? "search-result--selected" : "")"
data-task-id="@task.Id"
onclick="selectSearchResult(@task.Id)"
onmouseenter="highlightResult(this)">
<span class="search-result-dot" style="background: @color"></span>
<span class="search-result-title">@task.Title</span>
@if (!string.IsNullOrEmpty(task.Category))
{
<span class="search-result-category">@task.Category</span>
}
<span class="search-result-arrow">→</span>
</div>
}
else
{
@foreach (var task in Model)
{
<div class="search-result-item"
hx-get="/board?handler=TaskDetail&id=@task.Id"
hx-target="#detail-panel"
hx-swap="innerHTML"
style="padding: 10px 16px; cursor: pointer; display: flex; align-items: center; gap: 8px; border-bottom: 1px solid var(--color-border);">
<span class="badge badge--@task.Status.ToString().ToLower()" style="flex-shrink: 0;">
@task.Status
</span>
<span style="font-size: 14px; color: var(--color-text-primary);">@task.Title</span>
@if (!string.IsNullOrEmpty(task.Category))
{
<span style="margin-left: auto; font-size: 11px; color: var(--color-text-tertiary);">@task.Category</span>
}
</div>
}
}
</div>
}

View File

@@ -29,7 +29,7 @@
</nav>
</div>
<div class="header-right">
<button type="button" class="btn btn-search" id="search-trigger">
<button type="button" class="btn btn-search" id="search-trigger" onclick="openSearch()">
<svg class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8" />
<line x1="21" y1="21" x2="16.65" y2="16.65" />
@@ -50,7 +50,29 @@
@RenderBody()
</main>
<div id="search-modal"></div>
<div id="search-modal" class="search-backdrop" onclick="if(event.target===this)closeSearch()">
<div class="search-modal">
<div class="search-input-row">
<svg class="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
<input type="text" id="search-input" class="search-input" placeholder="Search tasks..."
autocomplete="off"
hx-get="/board?handler=Search"
hx-target="#search-results"
hx-swap="innerHTML"
hx-trigger="input changed delay:200ms, search"
hx-include="this"
name="q" />
</div>
<div id="search-results" class="search-results">
<!-- Results loaded via htmx -->
</div>
<div class="search-footer">
<span><kbd>↑↓</kbd> navigate</span>
<span><kbd>↵</kbd> open</span>
<span><kbd>esc</kbd> close</span>
</div>
</div>
</div>
<div id="detail-panel"></div>
<script src="~/lib/htmx.min.js"></script>