Files
local-mcp/static/css/components.css
Brandon Zhang 93bce1521b feat(ui): add quick shortcuts panel with add/edit/delete support
Adds a compact shortcuts row inside the composer panel for one-click
instruction queuing, with full lifecycle management stored in localStorage.
Features
--------
- Six built-in defaults (Stop, Summarize, Explain error, Undo, Continue, etc.)
- Click any chip → instantly POSTs to /api/instructions, no typing required
  - Cyan border pulse on fire; green glow flash on success
- Edit mode (toggle button in header):
  - Per-chip edit (✏) button → replaces chip with inline input, Enter to save
  - Per-chip delete (✕) button → removes with vanish animation
  - '+ Add' chip → inline form appended below rail
- All changes persisted to localStorage key 'local-mcp-shortcuts'
- Accessible: button elements, aria-labels, keyboard support (Enter/Escape)
Files
-----
- static/js/shortcuts.js   new module (loadShortcuts, renderShortcuts,
                           startInlineEdit, showAddPrompt, initShortcuts)
- static/index.html        #shortcuts-container inside composer .panel-body
- static/js/app.js         import + initShortcuts() in bootstrap()
- static/css/components.css .shortcuts-container, .shortcut-chip variants,
                           .shortcut-inline-edit, keyframes chip-fire/sent/vanish
2026-03-27 13:55:22 +08:00

767 lines
19 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* static/css/components.css
* Reusable UI components: buttons, inputs, badges, status indicators,
* instruction cards, and animations.
*/
/* ── Status indicator (LED dot) ─────────────────────────────────────────── */
.led {
display: inline-flex;
align-items: center;
gap: var(--space-2);
font-size: var(--text-xs);
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
}
.led__dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.led--green .led__dot { background: var(--green); box-shadow: 0 0 6px var(--green), 0 0 12px var(--green-glow); }
.led--amber .led__dot { background: var(--amber); box-shadow: 0 0 6px var(--amber), 0 0 12px var(--amber-glow); }
.led--red .led__dot { background: var(--red); box-shadow: 0 0 6px var(--red), 0 0 12px var(--red-glow); }
.led--muted .led__dot { background: var(--text-muted); box-shadow: none; }
.led--cyan .led__dot { background: var(--cyan); box-shadow: 0 0 6px var(--cyan), 0 0 12px var(--cyan-glow); }
.led--green .led__label { color: var(--green); }
.led--amber .led__label { color: var(--amber); }
.led--red .led__label { color: var(--red); }
.led--muted .led__label { color: var(--text-muted); }
.led--cyan .led__label { color: var(--cyan); }
/* Pulse for "connected" / "active" */
.led--pulse .led__dot {
animation: pulse 2.4s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.5; transform: scale(0.85); }
}
/* ── Queue count badge ───────────────────────────────────────────────────── */
.badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 20px;
height: 20px;
padding: 0 var(--space-2);
border-radius: 10px;
font-size: var(--text-xs);
font-weight: 700;
line-height: 1;
}
.badge--cyan { background: var(--cyan-glow); color: var(--cyan); border: 1px solid rgba(0,212,255,0.3); }
.badge--amber { background: var(--amber-glow); color: var(--amber); border: 1px solid rgba(255,190,11,0.3); }
.badge--muted { background: var(--bg-overlay); color: var(--text-muted); border: 1px solid var(--border-subtle); }
/* ── Buttons ─────────────────────────────────────────────────────────────── */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--space-2);
padding: var(--space-2) var(--space-4);
border: 1px solid transparent;
border-radius: var(--radius-md);
font-family: var(--font-ui);
font-size: var(--text-sm);
font-weight: 600;
letter-spacing: 0.04em;
cursor: pointer;
text-decoration: none;
transition:
background var(--duration-fast) var(--ease-in-out),
border-color var(--duration-fast) var(--ease-in-out),
box-shadow var(--duration-fast) var(--ease-in-out),
transform var(--duration-fast) var(--ease-in-out);
user-select: none;
white-space: nowrap;
}
.btn:disabled {
opacity: 0.4;
cursor: not-allowed;
pointer-events: none;
}
.btn:active:not(:disabled) {
transform: scale(0.97);
}
/* Primary cyan */
.btn--primary {
background: var(--cyan);
border-color: var(--cyan);
color: #000;
}
.btn--primary:hover {
background: #1de9ff;
box-shadow: 0 0 16px var(--cyan-glow);
}
/* Ghost */
.btn--ghost {
background: transparent;
border-color: var(--border-muted);
color: var(--text-secondary);
}
.btn--ghost:hover {
background: var(--bg-hover);
border-color: var(--border-strong);
color: var(--text-primary);
}
/* Danger */
.btn--danger {
background: transparent;
border-color: var(--red-dim);
color: var(--red);
}
.btn--danger:hover {
background: var(--red-glow);
border-color: var(--red);
}
/* Icon-only */
.btn--icon {
padding: var(--space-2);
width: 32px;
height: 32px;
border-radius: var(--radius-sm);
}
/* Small */
.btn--sm {
padding: var(--space-1) var(--space-3);
font-size: var(--text-xs);
height: 28px;
}
/* ── Form elements ──────────────────────────────────────────────────────── */
.input, .textarea {
width: 100%;
background: var(--bg-overlay);
border: 1px solid var(--border-muted);
border-radius: var(--radius-md);
color: var(--text-primary);
font-family: var(--font-mono);
font-size: var(--text-base);
padding: var(--space-3) var(--space-4);
transition:
border-color var(--duration-fast) var(--ease-in-out),
box-shadow var(--duration-fast) var(--ease-in-out);
outline: none;
resize: vertical;
}
.input::placeholder, .textarea::placeholder {
color: var(--text-muted);
}
.input:focus, .textarea:focus {
border-color: var(--cyan-dim);
box-shadow: 0 0 0 3px rgba(0,212,255,0.08);
}
.textarea {
min-height: 88px;
line-height: 1.6;
}
.form-group {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.form-row {
display: flex;
gap: var(--space-3);
align-items: flex-end;
}
.form-row .textarea { flex: 1; }
/* ── Instruction card ─────────────────────────────────────────────────────── */
.instruction-card {
padding: var(--space-4) var(--space-5);
border-bottom: 1px solid var(--border-subtle);
display: grid;
grid-template-columns: 1fr auto;
gap: var(--space-2) var(--space-4);
align-items: start;
transition: background var(--duration-fast) var(--ease-in-out);
animation: card-in var(--duration-slow) var(--ease-out-expo) both;
}
@keyframes card-in {
from { opacity: 0; transform: translateY(-8px); }
to { opacity: 1; transform: translateY(0); }
}
.instruction-card:last-child {
border-bottom: none;
}
.instruction-card:hover {
background: var(--bg-hover);
}
.instruction-card__meta {
grid-column: 1 / -1;
display: flex;
align-items: center;
gap: var(--space-4);
flex-wrap: wrap;
}
.instruction-card__pos {
font-size: var(--text-xs);
font-weight: 700;
font-family: var(--font-mono);
color: var(--cyan);
background: var(--cyan-glow);
border: 1px solid rgba(0,212,255,0.25);
border-radius: var(--radius-sm);
padding: 1px 6px;
letter-spacing: 0.08em;
}
.instruction-card__content {
font-family: var(--font-mono);
font-size: var(--text-base);
line-height: 1.65;
color: var(--text-primary);
word-break: break-word;
white-space: pre-wrap;
}
.instruction-card__time {
font-size: var(--text-xs);
color: var(--text-muted);
font-family: var(--font-mono);
}
.instruction-card__actions {
display: flex;
gap: var(--space-2);
align-items: center;
flex-shrink: 0;
}
/* Edit mode */
.instruction-card__edit-area {
grid-column: 1 / -1;
display: flex;
gap: var(--space-3);
align-items: flex-end;
margin-top: var(--space-2);
}
.instruction-card__edit-area .textarea {
flex: 1;
min-height: 72px;
}
/* Consumed card */
.instruction-card--consumed {
opacity: 0.45;
}
.instruction-card--consumed .instruction-card__content {
text-decoration: line-through;
text-decoration-color: var(--amber-dim);
color: var(--text-secondary);
}
.instruction-card--consumed .instruction-card__pos {
background: var(--amber-glow);
border-color: rgba(255,190,11,0.2);
color: var(--amber);
}
.instruction-card--consumed:hover {
opacity: 0.65;
}
.instruction-card__consumed-by {
font-size: var(--text-xs);
color: var(--amber);
font-family: var(--font-mono);
}
/* ── Empty state ─────────────────────────────────────────────────────────── */
.empty-state {
padding: var(--space-10) var(--space-6);
text-align: center;
color: var(--text-muted);
font-size: var(--text-sm);
letter-spacing: 0.04em;
}
.empty-state__icon {
font-size: 1.5rem;
margin-bottom: var(--space-3);
opacity: 0.4;
}
/* ── Status panel items ──────────────────────────────────────────────────── */
.stat-row {
display: flex;
align-items: baseline;
justify-content: space-between;
padding: var(--space-2) 0;
border-bottom: 1px solid var(--border-subtle);
}
.stat-row:last-child { border-bottom: none; }
.stat-label {
font-size: var(--text-xs);
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--text-muted);
}
.stat-value {
font-family: var(--font-mono);
font-size: var(--text-sm);
color: var(--text-primary);
}
.stat-value--cyan { color: var(--cyan); }
.stat-value--amber { color: var(--amber); }
/* ── Config form ─────────────────────────────────────────────────────────── */
.config-field {
display: flex;
flex-direction: column;
gap: var(--space-1);
margin-bottom: var(--space-4);
}
.config-field:last-child { margin-bottom: 0; }
.config-label {
font-size: var(--text-xs);
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--text-secondary);
}
.config-hint {
font-size: var(--text-xs);
color: var(--text-muted);
margin-top: var(--space-1);
}
.input--sm {
padding: var(--space-2) var(--space-3);
font-size: var(--text-sm);
}
/* ── Toast notification ──────────────────────────────────────────────────── */
#toast-container {
position: fixed;
bottom: var(--space-6);
right: var(--space-6);
z-index: 9999;
display: flex;
flex-direction: column;
gap: var(--space-2);
pointer-events: none;
}
.toast {
background: var(--bg-overlay);
border: 1px solid var(--border-strong);
border-radius: var(--radius-md);
padding: var(--space-3) var(--space-5);
font-size: var(--text-sm);
color: var(--text-primary);
box-shadow: var(--shadow-lg);
animation: toast-in var(--duration-normal) var(--ease-out-expo) both;
pointer-events: auto;
max-width: 320px;
}
.toast--success { border-color: var(--green-dim); color: var(--green); }
.toast--error { border-color: var(--red-dim); color: var(--red); }
.toast--info { border-color: var(--cyan-dim); color: var(--cyan); }
@keyframes toast-in {
from { opacity: 0; transform: translateY(12px) scale(0.95); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
@keyframes toast-out {
from { opacity: 1; transform: translateY(0) scale(1); }
to { opacity: 0; transform: translateY(4px) scale(0.97); }
}
/* ── Theme toggle (header) ────────────────────────────────────────────────── */
#theme-toggle {
color: var(--text-secondary);
border-color: transparent;
transition: color var(--duration-fast) var(--ease-in-out),
background var(--duration-fast) var(--ease-in-out);
}
#theme-toggle:hover {
color: var(--text-primary);
background: var(--bg-hover);
border-color: var(--border-subtle);
}
/* ── SVG icons ───────────────────────────────────────────────────────────── */
.icon {
width: 16px;
height: 16px;
flex-shrink: 0;
stroke-width: 2;
stroke: currentColor;
fill: none;
stroke-linecap: round;
stroke-linejoin: round;
}
.icon--sm { width: 12px; height: 12px; }
/* ── Page load animation ─────────────────────────────────────────────────── */
.fade-in {
animation: fade-in var(--duration-slow) var(--ease-out-expo) both;
}
.fade-in--delay-1 { animation-delay: 60ms; }
.fade-in--delay-2 { animation-delay: 120ms; }
.fade-in--delay-3 { animation-delay: 200ms; }
.fade-in--delay-4 { animation-delay: 300ms; }
@keyframes fade-in {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
/* ── Loading spinner ─────────────────────────────────────────────────────── */
.spinner {
display: inline-block;
width: 14px;
height: 14px;
border: 2px solid var(--border-muted);
border-top-color: var(--cyan);
border-radius: 50%;
animation: spin 0.7s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* ── Auth overlay ─────────────────────────────────────────────────────────── */
.auth-overlay {
position: fixed;
inset: 0;
z-index: 9000;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.72);
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
}
.auth-card {
width: 100%;
max-width: 380px;
margin: var(--space-4);
padding: var(--space-8);
background: var(--surface-raised);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
box-shadow: 0 32px 64px rgba(0, 0, 0, 0.48);
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-4);
text-align: center;
}
.auth-card__icon {
display: flex;
align-items: center;
justify-content: center;
width: 64px;
height: 64px;
border-radius: 50%;
background: color-mix(in srgb, var(--cyan) 10%, transparent);
border: 1px solid color-mix(in srgb, var(--cyan) 25%, transparent);
margin-bottom: var(--space-2);
}
.auth-card__title {
font-family: var(--font-display);
font-size: var(--text-lg);
font-weight: 700;
color: var(--text-primary);
margin: 0;
}
.auth-card__desc {
font-size: var(--text-sm);
color: var(--text-muted);
margin: 0;
line-height: 1.6;
}
.auth-card__desc code {
font-family: var(--font-mono);
font-size: 0.9em;
color: var(--cyan);
background: color-mix(in srgb, var(--cyan) 10%, transparent);
padding: 1px 5px;
border-radius: 3px;
}
.auth-card__form {
width: 100%;
display: flex;
flex-direction: column;
gap: var(--space-3);
}
.auth-card__error {
font-size: var(--text-xs);
color: var(--red);
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);
}