Choose Option B (full palette: actions + entities) over Option A (keybind-only) because: - pwa-baseline.md canon for multi-entity sites (paliad has 8 entity types). - 80% of infrastructure already exists in search.ts (sectioned results, keyboard nav, i18n, debounce, abortable fetch). - Patent-lawyer audience benefits from keyboard-first creation flows. - A-then-B would mean revisiting in 2 weeks anyway. Scope guardrails: no fuzzy lib, no MRU persistence, no extension API, no per-action shortcut keys (Cmd+K only). 20-action catalog (12 navigate, 3 create, 4 toggle/action). Mobile gets palette via drawer (no new BottomNav slot). Desktop preventDefault on Ctrl+K to suppress URL-bar. Doc covers: trigger surface, UX shape (empty + filtered), action catalog, component architecture (extend search.ts, new palette-actions.ts data file), render flow, i18n keys, mobile considerations, acceptance, implementation plan, risks. Implementer choice deferred to m. Awaiting m's go/no-go before coder shift.
505 lines
23 KiB
Markdown
505 lines
23 KiB
Markdown
# Cmd/Ctrl+K Command Palette — Design
|
||
|
||
**Task:** t-paliad-044
|
||
**Author:** cronus (inventor)
|
||
**Date:** 2026-04-26
|
||
**Status:** Design — awaiting m's go/no-go before coder shift
|
||
|
||
---
|
||
|
||
## Decision: Option B — full command palette (with strict scope guardrails)
|
||
|
||
The brief offered two scopes. Rationale for picking B:
|
||
|
||
1. **`pwa-baseline.md` is explicit** — multi-entity sites ship a command palette;
|
||
single-entity sites can skip it. Paliad has 8 entity types (projects, deadlines,
|
||
appointments, glossary, courts, checklists, links, users), so it is squarely in
|
||
the "ship a palette" bucket.
|
||
2. **80% of the infrastructure already exists.** `frontend/src/client/search.ts`
|
||
has sectioned grouped results, keyboard navigation (↑↓ / ↵ / Esc), i18n
|
||
group headers, debounce + AbortController, an in-flight cancellation pattern,
|
||
and language-switch re-render. Adding an *Actions* section on top is
|
||
incremental, not a rewrite.
|
||
3. **Patent lawyers are heavy keyboard users on desktop.** The HLC / HLC-Munich
|
||
audience drafts long documents; Cmd+K → "Neue Frist" without leaving the
|
||
keyboard is genuinely valuable. Sidebar nav is already always-visible on
|
||
desktop, so the *navigate-to* actions are quality-of-life — but the *create*
|
||
actions ("Neue Frist", "Neuer Termin", "Neues Projekt") are real time saves.
|
||
4. **Going A first feels like a half-step.** A "/" key alias for Cmd+K is five
|
||
lines of code, but everyone who hits Cmd+K and sees only entity search will
|
||
wonder where "Gehe zu Dashboard" / "Neue Frist anlegen" are. We'd be back
|
||
here in two weeks anyway.
|
||
5. **Template value.** Paliad is the first paliad-stack PWA to fully implement
|
||
the pwa-baseline `SearchPalette` reference. Doing it right here makes the
|
||
pattern reusable for the next mAi PWA project.
|
||
|
||
---
|
||
|
||
## Scope guardrails (what is NOT in this design)
|
||
|
||
- ❌ Fuzzy matching library — substring match on DE+EN labels is sufficient
|
||
for ~20 actions and small entity result sets. Add `fuse.js` only if the catalog
|
||
passes ~50 entries, which is unlikely to happen in 2026.
|
||
- ❌ Recently-used persistence / localStorage MRU — defer. We can add a
|
||
`paliad-palette-recent` key later if telemetry shows users repeating the same
|
||
3-4 actions.
|
||
- ❌ Action groups beyond Aktionen / Projekte / Fristen / … — no
|
||
meta-categories like "Werkzeuge", "Wissen". Keep flat.
|
||
- ❌ Extension API or plugin registry — the action catalog is a single static
|
||
array in `palette-actions.ts`. Future sections can be added by editing that
|
||
file; no need for a registration callback.
|
||
- ❌ Cross-project search — out of scope per task brief.
|
||
- ❌ AI-powered ranking — out of scope per task brief.
|
||
- ❌ Action shortcut keys beyond Cmd+K itself (e.g. `g d` to go to Dashboard).
|
||
Maybe later; not now.
|
||
- ❌ Recent entities — show entity results only when the user types.
|
||
|
||
---
|
||
|
||
## Trigger surface
|
||
|
||
| Trigger | Behavior | Platform |
|
||
|---------------------|----------------------------------------------|----------|
|
||
| `Cmd+K` (Mac) | Open palette + focus input. `preventDefault`. | desktop |
|
||
| `Ctrl+K` (Win/Lin) | Same. | desktop |
|
||
| `/` (slash) | Same — kept for muscle memory (shipped t-paliad-026). | desktop |
|
||
| Click sidebar input | Same — focuses the input directly. | desktop |
|
||
| `Esc` | Close + clear input. | all |
|
||
| BottomNav menu → drawer → search input | Existing path on mobile. | mobile |
|
||
|
||
### Why not a dedicated mobile slot
|
||
|
||
The BottomNav (5 slots: Start / Projekte / Anlegen / Agenda / Menü) is full.
|
||
Replacing one would degrade an established pattern. The mobile sidebar drawer
|
||
(opened via Menü or hamburger) already contains the same `#global-search-input`,
|
||
so a tap-search path exists. **Mobile users get the palette via the drawer, not
|
||
a dedicated button.** Revisit if telemetry shows mobile users searching often
|
||
enough to justify a topbar search icon.
|
||
|
||
### Browser-native Ctrl+K suppression
|
||
|
||
`Ctrl+K` in Firefox/Chrome focuses the URL bar's "search engine" submenu (rare
|
||
but exists). In Safari, `Cmd+L` focuses the URL bar but `Cmd+K` is unbound.
|
||
We `preventDefault()` on the document-level keydown handler whenever the key
|
||
combo matches and **only** when a textarea / input is not already focused with
|
||
a non-`#global-search-input` element — same skip-rule as the existing `/`
|
||
shortcut.
|
||
|
||
---
|
||
|
||
## UX shape
|
||
|
||
### Empty state (Cmd+K just pressed, input empty)
|
||
|
||
Show all actions, sectioned under "Aktionen". Don't fetch entity search.
|
||
The user can see the catalog at a glance — this is the "discoverability mode"
|
||
of the palette.
|
||
|
||
```
|
||
┌──────────────────────────────────────────────────┐
|
||
│ 🔍 ____________________________________________ │ ← #global-search-input
|
||
├──────────────────────────────────────────────────┤
|
||
│ AKTIONEN │
|
||
│ 📊 Gehe zu Dashboard │
|
||
│ 📁 Gehe zu Projekte │
|
||
│ ⏰ Gehe zu Fristen │
|
||
│ 📅 Gehe zu Termine │
|
||
│ 🗓 Gehe zu Agenda │
|
||
│ 📖 Gehe zu Glossar │
|
||
│ 🏛 Gehe zu Gerichte │
|
||
│ 🔗 Gehe zu Links │
|
||
│ ✓ Gehe zu Checklisten │
|
||
│ ⬇ Gehe zu Downloads │
|
||
│ ⚙ Gehe zu Einstellungen │
|
||
│ ➕ Neue Frist anlegen │
|
||
│ ➕ Neuer Termin anlegen │
|
||
│ ➕ Neues Projekt anlegen │
|
||
│ 🌐 Sprache umschalten (DE → EN) │
|
||
│ 📌 Sidebar anheften / lösen │
|
||
│ ✉ Kolleg:in einladen │
|
||
│ ↪ Abmelden │
|
||
├──────────────────────────────────────────────────┤
|
||
│ ↑↓ Navigieren · ↵ Öffnen · Esc Schließen │ ← footer hint
|
||
└──────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### Filtered state (user typed at least 1 char)
|
||
|
||
Both Actions (filtered by substring on DE+EN labels) AND entity search results
|
||
(via existing `/api/search?q=...`) render together, Actions on top.
|
||
|
||
```
|
||
Query: "frist"
|
||
┌──────────────────────────────────────────────────┐
|
||
│ 🔍 frist │
|
||
├──────────────────────────────────────────────────┤
|
||
│ AKTIONEN │
|
||
│ ⏰ Gehe zu Fristen │
|
||
│ ➕ Neue Frist anlegen │
|
||
│ FRISTEN │
|
||
│ ⏰ Klagebeantwortung — UPC-2024-0042 │
|
||
│ ⏰ Replik einreichen — Patent EP1234567 │
|
||
│ GLOSSAR │
|
||
│ 📖 Frist (Definition + Berechnung) │
|
||
└──────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### Footer keyboard hints
|
||
|
||
A small `<div class="palette-footer">` below the overlay results, visible
|
||
whenever the palette is open. Key hints in DE/EN:
|
||
|
||
- DE: `↑↓ Navigieren · ↵ Öffnen · Esc Schließen`
|
||
- EN: `↑↓ Navigate · ↵ Open · Esc Close`
|
||
|
||
---
|
||
|
||
## Action catalog (initial set)
|
||
|
||
20 actions, divided across two sub-types under "Aktionen" but rendered as one
|
||
flat list (no sub-headers):
|
||
|
||
### Navigate
|
||
|
||
| ID | DE | EN | URL |
|
||
|-----------------------|-------------------------------|-----------------------------|------------------------|
|
||
| `nav.dashboard` | Gehe zu Dashboard | Go to Dashboard | `/dashboard` |
|
||
| `nav.projects` | Gehe zu Projekte | Go to Projects | `/projects` |
|
||
| `nav.deadlines` | Gehe zu Fristen | Go to Deadlines | `/deadlines` |
|
||
| `nav.appointments` | Gehe zu Termine | Go to Appointments | `/appointments` |
|
||
| `nav.agenda` | Gehe zu Agenda | Go to Agenda | `/agenda` |
|
||
| `nav.team` | Gehe zu Team | Go to Team | `/team` |
|
||
| `nav.glossary` | Gehe zu Glossar | Go to Glossary | `/glossary` |
|
||
| `nav.courts` | Gehe zu Gerichte | Go to Courts | `/courts` |
|
||
| `nav.links` | Gehe zu Links | Go to Links | `/links` |
|
||
| `nav.checklists` | Gehe zu Checklisten | Go to Checklists | `/checklists` |
|
||
| `nav.downloads` | Gehe zu Downloads | Go to Downloads | `/downloads` |
|
||
| `nav.settings` | Gehe zu Einstellungen | Go to Settings | `/settings` |
|
||
|
||
### Create
|
||
|
||
| ID | DE | EN | URL |
|
||
|-----------------------|-------------------------------|-----------------------------|------------------------|
|
||
| `create.deadline` | Neue Frist anlegen | New deadline | `/deadlines/new` |
|
||
| `create.appointment` | Neuer Termin anlegen | New appointment | `/appointments/new` |
|
||
| `create.project` | Neues Projekt anlegen | New project | `/projects/new` |
|
||
|
||
### Toggle / action
|
||
|
||
| ID | DE | EN | Behavior |
|
||
|-----------------------|-------------------------------|-----------------------------|-------------------------------|
|
||
| `toggle.lang` | Sprache umschalten | Toggle language | Click `data-lang-toggle` for the OTHER language. |
|
||
| `toggle.pin` | Sidebar anheften / lösen | Pin / unpin sidebar | Click `.sidebar-pin`. |
|
||
| `app.invite` | Kolleg:in einladen | Invite a colleague | Click `#sidebar-invite-btn`. |
|
||
| `app.logout` | Abmelden | Logout | Navigate to `/logout`. |
|
||
|
||
The catalog lives as a single `const ACTIONS: PaletteAction[]` array in a new
|
||
file `frontend/src/client/palette-actions.ts`. Adding/removing actions = editing
|
||
the array. Keep the file small and obvious.
|
||
|
||
### Filtering rule
|
||
|
||
Substring match (case-insensitive, language-agnostic — match against BOTH the
|
||
current-language label AND the other-language label, so an English-speaker who
|
||
types "Frist" still finds the deadline action). No fuzzy distance, no token
|
||
permutations. Sort filtered results by `prefix-match > substring-match`, ties
|
||
broken by catalog order.
|
||
|
||
---
|
||
|
||
## Component architecture
|
||
|
||
### Files touched / created
|
||
|
||
| File | Change |
|
||
|-------------------------------------------------|-------------------------------------|
|
||
| `frontend/src/client/search.ts` | Extended: Cmd+K binding, empty-state action render, footer hint, action filtering. |
|
||
| `frontend/src/client/palette-actions.ts` | **NEW.** Static action catalog + `runAction(id)` dispatcher. |
|
||
| `frontend/src/components/Sidebar.tsx` | Add palette footer markup inside `#global-search-overlay`. Update `<kbd>` shortcut hint to show both `/` and `⌘K` (or just keep `/` — minor UX choice). |
|
||
| `frontend/src/client/i18n.ts` | Add ~20 i18n keys: `palette.action.*`, `palette.section.actions`, `palette.footer.*`. |
|
||
| `frontend/src/styles/sidebar.css` (or wherever overlay styles live) | Add `.search-group-actions` (subtle accent), `.palette-footer` styles, action-icon styles. |
|
||
|
||
### Decision: extend `search.ts`, don't create a new `palette.ts`
|
||
|
||
- Single source of truth for the overlay's keyboard / focus / debounce logic.
|
||
- Avoids two files racing to mutate `#global-search-overlay`.
|
||
- The mental model is "search.ts owns the palette" — palette is just search
|
||
with actions on top.
|
||
- `palette-actions.ts` is a **data file** (catalog + dispatcher), not a
|
||
controller. Keeps the action catalog easy to browse and edit.
|
||
|
||
### High-level flow
|
||
|
||
```
|
||
[Cmd+K pressed]
|
||
↓
|
||
search.ts: focus input, openPalette(empty=true)
|
||
↓
|
||
render(): show all actions, no entity fetch, footer hint visible
|
||
↓
|
||
[user types "f"]
|
||
↓
|
||
input handler debounces 200ms → runSearch("f")
|
||
↓
|
||
runSearch():
|
||
1. filteredActions = filterActions(query) ← synchronous, instant
|
||
2. entityResults = fetch /api/search?q=f ← async
|
||
3. render({ actions: filteredActions, entities: entityResults })
|
||
↓
|
||
[user presses ↵]
|
||
↓
|
||
openActive():
|
||
if active.kind === "action": runAction(active.id)
|
||
else : window.location.href = active.url
|
||
↓
|
||
[user presses Esc] → closeOverlay()
|
||
```
|
||
|
||
### `PaletteAction` type
|
||
|
||
```ts
|
||
type PaletteAction = {
|
||
id: string; // stable id, used for icon lookup
|
||
i18nKey: string; // e.g. "palette.action.nav.dashboard"
|
||
fallbackLabel: { de: string; en: string };
|
||
iconKey: ActionIconKey; // small svg, see below
|
||
group: "navigate" | "create" | "toggle"; // for sort priority only
|
||
run: () => void; // dispatcher closure
|
||
};
|
||
```
|
||
|
||
### `runAction(id)` dispatcher
|
||
|
||
A switch statement that maps action id → DOM action. Examples:
|
||
|
||
```ts
|
||
function runAction(id: string): void {
|
||
switch (id) {
|
||
case "nav.dashboard":
|
||
window.location.href = "/dashboard";
|
||
break;
|
||
case "create.deadline":
|
||
window.location.href = "/deadlines/new";
|
||
break;
|
||
case "toggle.lang": {
|
||
const cur = document.documentElement.getAttribute("lang") || "de";
|
||
const next = cur === "de" ? "en" : "de";
|
||
document.querySelector<HTMLButtonElement>(`[data-lang-toggle="${next}"]`)?.click();
|
||
break;
|
||
}
|
||
case "toggle.pin":
|
||
document.querySelector<HTMLButtonElement>(".sidebar-pin")?.click();
|
||
break;
|
||
case "app.invite":
|
||
document.getElementById("sidebar-invite-btn")?.click();
|
||
break;
|
||
case "app.logout":
|
||
window.location.href = "/logout";
|
||
break;
|
||
// …
|
||
}
|
||
}
|
||
```
|
||
|
||
The dispatcher reuses existing DOM handlers (lang toggle, pin, invite modal)
|
||
so we don't duplicate state. If the underlying button moves, the dispatcher
|
||
breaks — that's acceptable: each action is one line of indirection, and the
|
||
file is small enough to grep.
|
||
|
||
### Keyboard binding for Cmd+K
|
||
|
||
```ts
|
||
document.addEventListener("keydown", (e) => {
|
||
const isCmdK = (e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k";
|
||
if (!isCmdK) return;
|
||
// Allow inputs to consume Cmd+K when they want to (e.g. a future rich-text
|
||
// editor's link insert) ONLY if they explicitly handle it. By default, we
|
||
// intercept globally — paliad has no Cmd+K conflict elsewhere today.
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
input.focus();
|
||
input.select();
|
||
openPaletteEmpty();
|
||
});
|
||
```
|
||
|
||
`/` keeps its existing skip-when-typing rule (don't fire if user is mid-edit).
|
||
`Cmd+K` does NOT skip — power users explicitly intend to open the palette
|
||
even from inside a text input.
|
||
|
||
---
|
||
|
||
## Render changes inside `search.ts`
|
||
|
||
### `render()` becomes `renderResults({ actions, entities, query })`
|
||
|
||
```ts
|
||
function renderResults(opts: {
|
||
actions: PaletteAction[],
|
||
entities: SearchResponse | null,
|
||
query: string,
|
||
}, overlay: HTMLElement): void {
|
||
flatResults = [];
|
||
activeIndex = -1;
|
||
|
||
const sections: string[] = [];
|
||
|
||
if (opts.actions.length > 0) {
|
||
sections.push(renderActionsSection(opts.actions, opts.query));
|
||
}
|
||
if (opts.entities) {
|
||
for (const group of GROUP_ORDER) {
|
||
const items = opts.entities[group.key] as SearchResult[];
|
||
if (!items || items.length === 0) continue;
|
||
sections.push(renderEntityGroup(group, items, opts.query));
|
||
}
|
||
}
|
||
|
||
if (sections.length === 0) {
|
||
overlay.innerHTML = `<div class="search-empty">…</div>`;
|
||
} else {
|
||
overlay.innerHTML = sections.join("") + renderFooter();
|
||
bindResultClicks(overlay);
|
||
}
|
||
overlay.style.display = "block";
|
||
}
|
||
```
|
||
|
||
The `flatResults` array becomes `Array<{ kind: "action" | "entity", … }>` so
|
||
`openActive()` can dispatch correctly.
|
||
|
||
### Footer
|
||
|
||
```html
|
||
<div class="palette-footer">
|
||
<span><kbd>↑↓</kbd> <span data-i18n="palette.footer.navigate">Navigieren</span></span>
|
||
<span><kbd>↵</kbd> <span data-i18n="palette.footer.open">Öffnen</span></span>
|
||
<span><kbd>Esc</kbd> <span data-i18n="palette.footer.close">Schließen</span></span>
|
||
</div>
|
||
```
|
||
|
||
---
|
||
|
||
## Mobile considerations
|
||
|
||
- BottomNav stays as-is (5 slots, full).
|
||
- Hamburger / drawer path to search remains the existing pattern — no change
|
||
required.
|
||
- The palette overlay's `position: fixed` already handles small viewports.
|
||
Verify the Actions section + footer fit without scrolling on a 360×640
|
||
test viewport during implementation; if not, drop the footer on mobile via
|
||
`@media (max-width: 480px) { .palette-footer { display: none; } }`.
|
||
- `Cmd+K` is desktop-only; mobile users never see the keybind.
|
||
|
||
---
|
||
|
||
## i18n additions
|
||
|
||
```ts
|
||
// de
|
||
"palette.section.actions": "Aktionen",
|
||
"palette.action.nav.dashboard": "Gehe zu Dashboard",
|
||
"palette.action.nav.projects": "Gehe zu Projekte",
|
||
"palette.action.nav.deadlines": "Gehe zu Fristen",
|
||
// … (all 20 actions)
|
||
"palette.footer.navigate": "Navigieren",
|
||
"palette.footer.open": "Öffnen",
|
||
"palette.footer.close": "Schließen",
|
||
|
||
// en
|
||
"palette.section.actions": "Actions",
|
||
"palette.action.nav.dashboard": "Go to Dashboard",
|
||
// …
|
||
"palette.footer.navigate": "Navigate",
|
||
"palette.footer.open": "Open",
|
||
"palette.footer.close": "Close",
|
||
```
|
||
|
||
Use the existing `t(key)` helper from `i18n.ts`. Match the pattern from
|
||
`GROUP_ORDER` in search.ts.
|
||
|
||
---
|
||
|
||
## Acceptance criteria (re-stated from task brief)
|
||
|
||
- ✅ `Cmd+K` (Mac) opens palette
|
||
- ✅ `Ctrl+K` (Win/Lin) opens palette
|
||
- ✅ `/` shortcut still works (existing behavior preserved)
|
||
- ✅ `preventDefault()` suppresses browser-native Ctrl+K
|
||
- ✅ Actions filter live as user types
|
||
- ✅ Entity results render alongside actions when query non-empty
|
||
- ✅ ↑↓ navigates the merged result list (actions + entities)
|
||
- ✅ ↵ opens / runs active item
|
||
- ✅ Esc closes
|
||
- ✅ Footer shows kbd hints, switches language with global toggle
|
||
- ✅ Console clean (no errors, no warnings beyond what's already there)
|
||
- ✅ `go build/vet/test ./...` clean
|
||
- ✅ `bun run build` clean
|
||
- ✅ Mobile fallback documented (no new BottomNav slot)
|
||
|
||
---
|
||
|
||
## Implementation plan (for the coder shift)
|
||
|
||
1. Add `frontend/src/client/palette-actions.ts` with the catalog + dispatcher.
|
||
2. Extend `frontend/src/client/search.ts`:
|
||
- Add Cmd+K binding (separate from `/` binding).
|
||
- Change `runSearch()` to also produce filtered actions; render actions
|
||
section first.
|
||
- Add empty-state branch (open palette → show all actions, no fetch).
|
||
- Update `flatResults` to be `Array<{ kind, … }>` so Enter dispatches.
|
||
- Render footer with kbd hints.
|
||
3. Update `frontend/src/components/Sidebar.tsx` `#global-search-overlay`
|
||
markup if needed (probably none — overlay is built dynamically).
|
||
4. Add i18n keys in `frontend/src/client/i18n.ts` (DE + EN, ~25 keys).
|
||
5. Add CSS for `.search-group-actions`, `.palette-footer`, action icon
|
||
colors. Reuse existing `.search-result` / `.search-group` styles where
|
||
possible.
|
||
6. Add a small SVG icon for each action `iconKey` (reuse sidebar nav icons
|
||
where they map 1:1 — `ICON_GAUGE` for dashboard, `ICON_FOLDER` for
|
||
projects, etc.).
|
||
7. Manual smoke (local `bun run build` + `go run ./cmd/server`):
|
||
- Cmd+K with empty input → all actions visible
|
||
- Type "frist" → action filter + entity search both render
|
||
- ↑↓ wraps; ↵ on action runs; ↵ on entity navigates
|
||
- Esc clears; clicking outside clears
|
||
- DE/EN toggle re-renders labels
|
||
- `/` still works
|
||
- Browser URL bar does NOT focus on Cmd+K
|
||
8. `go build ./...`, `go vet ./...`, `go test ./...`, `bun run build`.
|
||
9. Commit: `feat(palette): Cmd/Ctrl+K command palette with actions + entities (t-paliad-044)`.
|
||
10. Push to `mai/cronus/cmd-ctrl-k-command`, self-merge into main with
|
||
`--no-ff` (regression cleanup is in, no conflicts expected).
|
||
11. Verify on prod paliad.de via Playwright after Dokploy auto-deploy
|
||
(test creds: `tester@hlc.de` / `xdMmC7iCeDSTFmPXAlAyY0`).
|
||
|
||
---
|
||
|
||
## Risks + mitigations
|
||
|
||
| Risk | Mitigation |
|
||
|-------------------------------------------------------|----------------------------------------------------------------------|
|
||
| Cmd+K conflicts with future rich-text editor | Single global handler; if a real conflict appears, gate by `closest('.no-cmdk')` opt-out. |
|
||
| Action labels wrong language on first render | Reuse `t()` + `onLangChange()` handler that already exists in search.ts. |
|
||
| Browser URL-bar focus on Ctrl+K (Firefox) | `preventDefault()` + `stopPropagation()` in document keydown handler. |
|
||
| Action dispatcher breaks if sidebar button moves | One-line indirection per action; trivial to fix; covered by manual smoke. |
|
||
| Mobile BottomNav doesn't expose palette | Documented decision — drawer path exists; revisit if usage shows need. |
|
||
| Existing `flatResults` consumers (Enter handler) | Updated in same change — single file controls navigation. |
|
||
| Action catalog grows beyond ~50 | Add fuzzy match later; not now. |
|
||
|
||
---
|
||
|
||
## Implementer recommendation
|
||
|
||
cronus (myself) is a good fit to implement this:
|
||
- Designed it; minimum context-loading cost.
|
||
- Single-file-cluster change (search.ts + 1 new + 1 i18n + a few markup tweaks).
|
||
- No DB / backend touch; pure frontend client-side change.
|
||
- Can self-merge once m approves and t-paliad-043 stays green.
|
||
|
||
Alternative: knuth (frontend-strong, shipped t-paliad-026 global search).
|
||
Either works; this design doc carries enough detail that the implementer choice
|
||
is fungible.
|
||
|
||
**Decision is m's. I will not start coding until the head signals approval.**
|