Files
local-mcp/static/css/components.css
cabbage 009fd039a2 feat: optional Bearer-token authentication via API_TOKEN env var
Disabled by default (empty API_TOKEN). When set:
- All /api/* and /mcp requests require: Authorization: Bearer <token>
- Public exemptions: /, /healthz, /static/*, /auth-check
- Web UI: pre-flight /auth-check on load; shows token modal if required
- Token stored in sessionStorage, sent on every API request
- Mid-session 401s re-trigger the token modal
- MCP clients must pass the header: Authorization: Bearer <token>
Files changed:
- app/config.py: api_token field + API_TOKEN env var
- app/api/auth.py: Starlette BaseHTTPMiddleware for token enforcement
- main.py: register middleware + /auth-check public endpoint
- static/js/api.js: token storage, auth header, 401 handler hook
- static/js/app.js: auth pre-flight, showTokenModal(), bootstrap()
- static/css/components.css: .auth-overlay / .auth-card styles
- README.md: API_TOKEN env var docs + MCP client header example
2026-03-27 04:28:12 +08:00

563 lines
14 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;
}