From e2445980f9a09a65295e47d6f34dc0d09baa3f96 Mon Sep 17 00:00:00 2001 From: Rbanh Date: Wed, 18 Feb 2026 22:15:55 -0500 Subject: [PATCH] Add keyboard shortcut UX and command palette interactions --- README.md | 2 + frontend/app.js | 161 +++++++++++++++++- frontend/index.html | 32 ++++ frontend/styles.css | 69 ++++++++ tests/baselines/ui/dense-analog.png | Bin 233073 -> 233304 bytes .../baselines/ui/explicit-mode-auto-tidy.png | Bin 179068 -> 179489 bytes tests/baselines/ui/initial.png | Bin 181189 -> 181513 bytes tests/baselines/ui/laptop-viewport.png | Bin 275208 -> 275730 bytes tests/baselines/ui/post-migration-apply.png | Bin 177952 -> 178187 bytes tests/baselines/ui/selected-u2.png | Bin 168815 -> 169236 bytes tests/ui-regression-runner.js | 5 + 11 files changed, 267 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4c84b2a..5d216ff 100644 --- a/README.md +++ b/README.md @@ -165,9 +165,11 @@ Tools: - Selected panel editors for component properties, full pin properties, full symbol body/pin editing, and net connect/disconnect operations - Click diagnostics to jump/flash focused net/component/pin - Auto Layout and Auto Tidy actions +- `Shortcuts` helper modal and `Ctrl/Cmd+K` command palette quick actions - Keyboard shortcuts: - `Ctrl/Cmd+Z` undo - `Ctrl/Cmd+Shift+Z` or `Ctrl/Cmd+Y` redo + - `Ctrl/Cmd+K` open command palette - `Space` rotate selected components (or pan when no selection) - `F` focus current selection - `Alt+Enter` apply current selection editor (component/pin/net) diff --git a/frontend/app.js b/frontend/app.js index fd92129..6e39d55 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -45,7 +45,8 @@ const state = { historyFuture: [], historyLimit: 80, historyRestoring: false, - symbolMigrationAckHash: null + symbolMigrationAckHash: null, + commandIndex: 0 }; const el = { @@ -140,6 +141,7 @@ const el = { copyReproBtn: document.getElementById("copyReproBtn"), autoLayoutBtn: document.getElementById("autoLayoutBtn"), autoTidyBtn: document.getElementById("autoTidyBtn"), + shortcutsBtn: document.getElementById("shortcutsBtn"), undoBtn: document.getElementById("undoBtn"), redoBtn: document.getElementById("redoBtn"), renderModeSelect: document.getElementById("renderModeSelect"), @@ -150,7 +152,13 @@ const el = { schemaViewer: document.getElementById("schemaViewer"), closeSchemaBtn: document.getElementById("closeSchemaBtn"), copySchemaBtn: document.getElementById("copySchemaBtn"), - downloadSchemaBtn: document.getElementById("downloadSchemaBtn") + downloadSchemaBtn: document.getElementById("downloadSchemaBtn"), + shortcutsModal: document.getElementById("shortcutsModal"), + closeShortcutsBtn: document.getElementById("closeShortcutsBtn"), + commandModal: document.getElementById("commandModal"), + closeCommandBtn: document.getElementById("closeCommandBtn"), + commandInput: document.getElementById("commandInput"), + commandList: document.getElementById("commandList") }; function toGrid(v) { @@ -2139,6 +2147,77 @@ function closeSchemaModal() { el.schemaModal.classList.add("hidden"); } +function openShortcutsModal() { + el.shortcutsModal?.classList.remove("hidden"); + el.closeShortcutsBtn?.focus(); +} + +function closeShortcutsModal() { + el.shortcutsModal?.classList.add("hidden"); +} + +function commandEntries() { + return [ + { id: "auto-layout", label: "Run Auto Layout", run: () => el.autoLayoutBtn.click() }, + { id: "auto-tidy", label: "Run Auto Tidy", run: () => el.autoTidyBtn.click() }, + { id: "fit-view", label: "Fit View", run: () => el.fitViewBtn.click() }, + { id: "focus-selection", label: "Focus Selection", run: () => el.focusSelectionBtn.click() }, + { id: "toggle-labels", label: "Toggle Net Labels", run: () => el.showLabelsInput.click() }, + { id: "reset-sample", label: "Reset Sample", run: () => el.resetSampleBtn.click() }, + { id: "load-sample", label: "Load Sample", run: () => el.loadSampleBtn.click() }, + { id: "new-project", label: "New Project", run: () => el.newProjectBtn.click() }, + { id: "show-shortcuts", label: "Show Keyboard Shortcuts", run: () => openShortcutsModal() } + ]; +} + +function filteredCommands(query) { + const q = String(query ?? "") + .trim() + .toLowerCase(); + const cmds = commandEntries(); + if (!q) { + return cmds; + } + return cmds.filter((c) => c.label.toLowerCase().includes(q) || c.id.includes(q.replace(/\s+/g, "-"))); +} + +function renderCommandList() { + const cmds = filteredCommands(el.commandInput?.value ?? ""); + state.commandIndex = Math.max(0, Math.min(state.commandIndex, Math.max(0, cmds.length - 1))); + if (!cmds.length) { + el.commandList.innerHTML = `
No commands.
`; + return cmds; + } + el.commandList.innerHTML = cmds + .map( + (cmd, idx) => + `` + ) + .join(""); + return cmds; +} + +function openCommandModal() { + state.commandIndex = 0; + el.commandModal?.classList.remove("hidden"); + renderCommandList(); + el.commandInput?.focus(); + el.commandInput?.select(); +} + +function closeCommandModal() { + el.commandModal?.classList.add("hidden"); +} + +function runCommandById(id) { + const cmd = commandEntries().find((c) => c.id === id); + if (!cmd) { + return; + } + closeCommandModal(); + cmd.run(); +} + function buildMinimalRepro(model) { if (!state.selectedRefs.length && !state.selectedNet) { return model; @@ -3299,6 +3378,25 @@ function setupEvents() { return; } + if (mod && evt.key.toLowerCase() === "k") { + evt.preventDefault(); + if (el.commandModal?.classList.contains("hidden")) { + openCommandModal(); + } else { + closeCommandModal(); + } + return; + } + + if (!mod && evt.shiftKey && evt.key === "?") { + if (isTypingContext(evt.target)) { + return; + } + evt.preventDefault(); + openShortcutsModal(); + return; + } + if (!mod && !evt.altKey && !evt.shiftKey && evt.key.toLowerCase() === "f") { if (isTypingContext(evt.target)) { return; @@ -3323,6 +3421,14 @@ function setupEvents() { } } if (evt.code === "Escape") { + if (!el.commandModal.classList.contains("hidden")) { + closeCommandModal(); + return; + } + if (!el.shortcutsModal.classList.contains("hidden")) { + closeShortcutsModal(); + return; + } if (!el.schemaModal.classList.contains("hidden")) { closeSchemaModal(); return; @@ -3375,6 +3481,57 @@ function setupEvents() { } }); + el.shortcutsBtn?.addEventListener("click", openShortcutsModal); + el.closeShortcutsBtn?.addEventListener("click", closeShortcutsModal); + el.shortcutsModal?.addEventListener("click", (evt) => { + if (evt.target === el.shortcutsModal) { + closeShortcutsModal(); + } + }); + + el.closeCommandBtn?.addEventListener("click", closeCommandModal); + el.commandModal?.addEventListener("click", (evt) => { + if (evt.target === el.commandModal) { + closeCommandModal(); + } + }); + el.commandInput?.addEventListener("input", () => { + state.commandIndex = 0; + renderCommandList(); + }); + el.commandInput?.addEventListener("keydown", (evt) => { + const cmds = renderCommandList(); + if (!cmds.length) { + return; + } + if (evt.key === "ArrowDown") { + evt.preventDefault(); + state.commandIndex = Math.min(cmds.length - 1, state.commandIndex + 1); + renderCommandList(); + return; + } + if (evt.key === "ArrowUp") { + evt.preventDefault(); + state.commandIndex = Math.max(0, state.commandIndex - 1); + renderCommandList(); + return; + } + if (evt.key === "Enter") { + evt.preventDefault(); + const cmd = cmds[state.commandIndex]; + if (cmd) { + runCommandById(cmd.id); + } + } + }); + el.commandList?.addEventListener("click", (evt) => { + const row = evt.target.closest("[data-command-id]"); + if (!row) { + return; + } + runCommandById(row.getAttribute("data-command-id")); + }); + el.validateJsonBtn.addEventListener("click", validateJsonEditor); el.formatJsonBtn.addEventListener("click", () => { diff --git a/frontend/index.html b/frontend/index.html index 5f28258..3890801 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -20,6 +20,7 @@ +