From 93bce1521b61e39e40e5bb9ae18af54b7e5161ef Mon Sep 17 00:00:00 2001 From: Brandon Zhang Date: Fri, 27 Mar 2026 13:55:22 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui):=20add=20quick=20shortcuts=20panel=20w?= =?UTF-8?q?ith=20add/edit/delete=20support=20Adds=20a=20compact=20shortcut?= =?UTF-8?q?s=20row=20inside=20the=20composer=20panel=20for=20one-click=20i?= =?UTF-8?q?nstruction=20queuing,=20with=20full=20lifecycle=20management=20?= =?UTF-8?q?stored=20in=20localStorage.=20Features=20--------=20-=20Six=20b?= =?UTF-8?q?uilt-in=20defaults=20(Stop,=20Summarize,=20Explain=20error,=20U?= =?UTF-8?q?ndo,=20Continue,=20etc.)=20-=20Click=20any=20chip=20=E2=86=92?= =?UTF-8?q?=20instantly=20POSTs=20to=20/api/instructions,=20no=20typing=20?= =?UTF-8?q?required=20=20=20-=20Cyan=20border=20pulse=20on=20fire;=20green?= =?UTF-8?q?=20glow=20flash=20on=20success=20-=20Edit=20mode=20(toggle=20bu?= =?UTF-8?q?tton=20in=20header):=20=20=20-=20Per-chip=20edit=20(=E2=9C=8F)?= =?UTF-8?q?=20button=20=E2=86=92=20replaces=20chip=20with=20inline=20input?= =?UTF-8?q?,=20Enter=20to=20save=20=20=20-=20Per-chip=20delete=20(?= =?UTF-8?q?=E2=9C=95)=20button=20=E2=86=92=20removes=20with=20vanish=20ani?= =?UTF-8?q?mation=20=20=20-=20'+=20Add'=20chip=20=E2=86=92=20inline=20form?= =?UTF-8?q?=20appended=20below=20rail=20-=20All=20changes=20persisted=20to?= =?UTF-8?q?=20localStorage=20key=20'local-mcp-shortcuts'=20-=20Accessible:?= =?UTF-8?q?=20button=20elements,=20aria-labels,=20keyboard=20support=20(En?= =?UTF-8?q?ter/Escape)=20Files=20-----=20-=20static/js/shortcuts.js=20=20?= =?UTF-8?q?=20new=20module=20(loadShortcuts,=20renderShortcuts,=20=20=20?= =?UTF-8?q?=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20?= =?UTF-8?q?=20=20=20=20startInlineEdit,=20showAddPrompt,=20initShortcuts)?= =?UTF-8?q?=20-=20static/index.html=20=20=20=20=20=20=20=20#shortcuts-cont?= =?UTF-8?q?ainer=20inside=20composer=20.panel-body=20-=20static/js/app.js?= =?UTF-8?q?=20=20=20=20=20=20=20=20=20import=20+=20initShortcuts()=20in=20?= =?UTF-8?q?bootstrap()=20-=20static/css/components.css=20.shortcuts-contai?= =?UTF-8?q?ner,=20.shortcut-chip=20variants,=20=20=20=20=20=20=20=20=20=20?= =?UTF-8?q?=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20.shortcut?= =?UTF-8?q?-inline-edit,=20keyframes=20chip-fire/sent/vanish?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/css/components.css | 204 ++++++++++++++++++++++++++++++++++++++ static/index.html | 2 + static/js/app.js | 2 + static/js/shortcuts.js | 68 ++++++++++++- 4 files changed, 272 insertions(+), 4 deletions(-) diff --git a/static/css/components.css b/static/css/components.css index ebe076e..497fe5e 100644 --- a/static/css/components.css +++ b/static/css/components.css @@ -559,4 +559,208 @@ margin: 0; } +/* ── Quick shortcuts ─────────────────────────────────────────────────────── */ +.shortcuts-container { + margin-top: var(--space-3); + border-top: 1px solid var(--border-subtle); + padding-top: var(--space-3); + display: flex; + flex-direction: column; + gap: var(--space-2); +} + +.shortcuts-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-3); +} + +.shortcuts-label { + font-family: var(--font-ui); + font-size: var(--text-xs); + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--text-muted); + flex-shrink: 0; +} + +.shortcut-edit-toggle { + display: inline-flex; + align-items: center; + gap: var(--space-1); + padding: 2px var(--space-2); + background: none; + border: 1px solid var(--border-subtle); + border-radius: var(--radius-sm); + font-family: var(--font-ui); + font-size: var(--text-xs); + font-weight: 600; + letter-spacing: 0.06em; + color: var(--text-muted); + cursor: pointer; + transition: border-color 150ms, color 150ms; +} +.shortcut-edit-toggle:hover { + border-color: var(--border-strong); + color: var(--text-secondary); +} + +.shortcuts-rail { + display: flex; + flex-wrap: wrap; + gap: var(--space-2); + min-height: 28px; +} + +/* Individual shortcut chip */ +.shortcut-chip { + position: relative; + display: inline-flex; + align-items: center; + gap: var(--space-1); + max-width: 220px; + padding: 4px var(--space-3); + background: var(--bg-overlay); + border: 1px solid var(--border-muted); + border-radius: 4px; + font-family: var(--font-mono); + font-size: var(--text-xs); + font-weight: 400; + color: var(--text-secondary); + cursor: pointer; + white-space: nowrap; + overflow: hidden; + transition: border-color 140ms ease, color 140ms ease, + background 140ms ease, transform 80ms ease; + user-select: none; +} + +.shortcut-chip:hover { + border-color: var(--cyan-dim); + color: var(--text-primary); + background: var(--bg-hover); +} + +.shortcut-chip:active { + transform: scale(0.95); +} + +.shortcut-chip__text { + overflow: hidden; + text-overflow: ellipsis; + flex: 1; + min-width: 0; +} + +/* Delete handle (shown in edit mode) */ +.shortcut-chip__del { + display: none; /* toggled via JS */ + align-items: center; + justify-content: center; + width: 14px; + height: 14px; + flex-shrink: 0; + border-radius: 50%; + background: var(--red-dim); + color: var(--red); + transition: background 120ms; +} +.shortcut-chip:hover .shortcut-chip__del { + background: var(--red); + color: #fff; +} + +/* Edit handle (shown in edit mode) */ +.shortcut-chip__edit { + display: none; /* toggled via JS */ + align-items: center; + justify-content: center; + width: 14px; + height: 14px; + flex-shrink: 0; + border-radius: 50%; + background: var(--border-muted); + color: var(--text-secondary); + transition: background 120ms; +} +.shortcut-chip:hover .shortcut-chip__edit { + background: var(--cyan-dim); + color: var(--cyan); +} + +/* Inline chip edit row */ +.shortcut-inline-edit { + display: inline-flex; + align-items: center; + gap: var(--space-2); + animation: chip-fire 160ms ease; +} +.shortcut-inline-input { + width: 180px; + padding: 3px var(--space-2); + font-size: var(--text-xs); + height: 26px; +} + +/* Firing state (just clicked) */ +.shortcut-chip--firing { + border-color: var(--cyan); + color: var(--cyan); + animation: chip-fire 280ms ease; +} + +/* Sent confirmation flash */ +.shortcut-chip--sent { + border-color: var(--green); + color: var(--green); + animation: chip-sent 600ms ease; +} + +/* Add chip */ +.shortcut-chip--add { + border-style: dashed; + border-color: var(--border-subtle); + color: var(--text-muted); + gap: var(--space-1); + padding: 4px var(--space-2); +} +.shortcut-chip--add:hover { + border-color: var(--cyan-dim); + color: var(--cyan); +} + +/* Chip animations */ +@keyframes chip-fire { + 0% { transform: scale(1); } + 40% { transform: scale(0.93); } + 100% { transform: scale(1); } +} + +@keyframes chip-sent { + 0% { box-shadow: 0 0 0 0 var(--green-glow); } + 50% { box-shadow: 0 0 0 6px transparent; } + 100% { box-shadow: none; } +} + +@keyframes chip-vanish { + to { opacity: 0; transform: scale(0.8); width: 0; padding: 0; margin: 0; } +} + +/* Add shortcut inline form */ +.shortcut-add-form { + display: flex; + flex-direction: column; + gap: var(--space-2); + padding-top: var(--space-2); + border-top: 1px solid var(--border-subtle); + margin-top: var(--space-1); + animation: fade-in-up 180ms ease; +} + +.shortcut-add-actions { + display: flex; + gap: var(--space-2); +} diff --git a/static/index.html b/static/index.html index f1c95f0..feb4d68 100644 --- a/static/index.html +++ b/static/index.html @@ -78,6 +78,8 @@ + +
diff --git a/static/js/app.js b/static/js/app.js index 37ddc62..210b9b2 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -10,6 +10,7 @@ import { connectSSE } from './events.js'; import { initInstructions, initComposer, refreshTimestamps } from './instructions.js'; import { initStatus, initConfig, refreshStatusTimestamps } from './status.js'; import { initTheme } from './theme.js'; +import { initShortcuts } from './shortcuts.js'; // ── Toast notification ──────────────────────────────────────────────────── @@ -165,6 +166,7 @@ async function bootstrap() { initConfig(); initInstructions(); initComposer(); + initShortcuts(); initGlobalSubscriptions(); // Hook 401 handler so mid-session token expiry shows the modal again diff --git a/static/js/shortcuts.js b/static/js/shortcuts.js index fbabbb7..abf8ba9 100644 --- a/static/js/shortcuts.js +++ b/static/js/shortcuts.js @@ -59,6 +59,10 @@ function renderChip(text, index) { chip.innerHTML = ` ${escapeHtml(text)} + + + @@ -141,7 +145,8 @@ export function renderShortcuts() { const chip = e.target.closest('.shortcut-chip'); if (!chip || chip.classList.contains('shortcut-chip--add')) return; - const delBtn = e.target.closest('.shortcut-chip__del'); + const delBtn = e.target.closest('.shortcut-chip__del'); + const editBtn = e.target.closest('.shortcut-chip__edit'); if (delBtn) { // Delete mode @@ -153,6 +158,13 @@ export function renderShortcuts() { return; } + if (editBtn) { + // Edit inline + const idx = parseInt(chip.dataset.index, 10); + startInlineEdit(chip, idx); + return; + } + if (editMode) return; // don't send in edit mode // Send the instruction @@ -171,9 +183,54 @@ export function renderShortcuts() { } }); - // Show/hide delete buttons based on editMode - container.querySelectorAll('.shortcut-chip__del').forEach(del => { - del.style.display = editMode ? 'flex' : 'none'; + // Show/hide edit controls based on editMode + container.querySelectorAll('.shortcut-chip__del, .shortcut-chip__edit').forEach(el => { + el.style.display = editMode ? 'flex' : 'none'; + }); +} + +// ── Inline chip editing ─────────────────────────────────────────────────── + +function startInlineEdit(chip, idx) { + const currentText = shortcuts[idx]; + + // Replace chip with a compact input row + const wrapper = document.createElement('div'); + wrapper.className = 'shortcut-inline-edit'; + wrapper.innerHTML = ` + + + + `; + + chip.replaceWith(wrapper); + + const input = wrapper.querySelector('.shortcut-inline-input'); + input.focus(); + input.select(); + + const save = () => { + const newText = input.value.trim(); + if (newText && newText !== currentText) { + shortcuts[idx] = newText; + saveShortcuts(shortcuts); + } + renderShortcuts(); + }; + + wrapper.querySelector('.shortcut-inline-save').addEventListener('click', save); + wrapper.querySelector('.shortcut-inline-cancel').addEventListener('click', renderShortcuts); + input.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { e.preventDefault(); save(); } + else if (e.key === 'Escape') renderShortcuts(); }); } @@ -239,3 +296,6 @@ export function initShortcuts() { renderShortcuts(); } + + +