docs(palette): design Cmd/Ctrl+K command palette (t-paliad-044)
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.
This commit is contained in:
504
docs/design-command-palette.md
Normal file
504
docs/design-command-palette.md
Normal file
@@ -0,0 +1,504 @@
|
||||
# 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.**
|
||||
Reference in New Issue
Block a user