This commit is contained in:
2026-03-27 03:58:57 +08:00
commit 86eba27a24
38 changed files with 4074 additions and 0 deletions

192
static/css/base.css Normal file
View File

@@ -0,0 +1,192 @@
/*
* static/css/base.css
* Design tokens, reset, and typographic foundation.
*
* Aesthetic: Industrial Ops Terminal
* Dark graphite workspace with electric cyan as the primary action colour
* and amber for warnings/consumed state. Syne for UI chrome, JetBrains Mono
* for instruction content. Strong structure, decisive whitespace.
*/
/* ── Fonts ─────────────────────────────────────────────────────────────── */
@font-face {
font-family: 'Syne';
src: url('/static/assets/fonts/syne.woff2') format('woff2');
font-weight: 100 900;
font-display: swap;
}
@font-face {
font-family: 'JetBrains Mono';
src: url('/static/assets/fonts/jetbrains-mono.woff2') format('woff2');
font-weight: 100 900;
font-display: swap;
}
/* ── Design tokens ──────────────────────────────────────────────────────── */
:root {
/* Surfaces */
--bg-void: #080b0d;
--bg-base: #0d1117;
--bg-raised: #131920;
--bg-overlay: #1a2230;
--bg-hover: #1f2a3a;
--bg-active: #243040;
/* Borders */
--border-subtle: #1e2c3a;
--border-muted: #253345;
--border-strong: #2e4058;
/* Text */
--text-primary: #e8edf2;
--text-secondary:#8da0b3;
--text-muted: #4a6175;
--text-disabled: #2e4a5e;
/* Brand / accent */
--cyan: #00d4ff;
--cyan-dim: #007fa8;
--cyan-glow: rgba(0, 212, 255, 0.15);
/* Status */
--green: #00e676;
--green-dim: #00703a;
--green-glow: rgba(0, 230, 118, 0.15);
--amber: #ffbe0b;
--amber-dim: #8a6500;
--amber-glow: rgba(255, 190, 11, 0.12);
--red: #ff4f4f;
--red-dim: #8a0000;
--red-glow: rgba(255, 79, 79, 0.15);
/* Typography */
--font-ui: 'Syne', system-ui, sans-serif;
--font-mono: 'JetBrains Mono', 'Cascadia Code', 'Fira Code', monospace;
--text-xs: 0.6875rem; /* 11px */
--text-sm: 0.75rem; /* 12px */
--text-base: 0.875rem; /* 14px */
--text-md: 1rem; /* 16px */
--text-lg: 1.125rem; /* 18px */
--text-xl: 1.375rem; /* 22px */
--text-2xl: 1.75rem; /* 28px */
--text-3xl: 2.25rem; /* 36px */
/* Spacing */
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-3: 0.75rem;
--space-4: 1rem;
--space-5: 1.25rem;
--space-6: 1.5rem;
--space-8: 2rem;
--space-10: 2.5rem;
--space-12: 3rem;
/* Radius */
--radius-sm: 4px;
--radius-md: 8px;
--radius-lg: 12px;
/* Shadows */
--shadow-sm: 0 1px 3px rgba(0,0,0,0.4);
--shadow-md: 0 4px 16px rgba(0,0,0,0.5);
--shadow-lg: 0 8px 32px rgba(0,0,0,0.6);
--shadow-cyan: 0 0 20px var(--cyan-glow), 0 0 40px rgba(0,212,255,0.06);
/* Animation */
--ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
--duration-fast: 120ms;
--duration-normal: 240ms;
--duration-slow: 400ms;
}
/* ── Light theme overrides ───────────────────────────────────────────────── */
/* Applied when <html data-theme="light"> is set by theme.js */
[data-theme="light"] {
--bg-void: #e8e8e3;
--bg-base: #f2f2ed;
--bg-raised: #ffffff;
--bg-overlay: #ebebe6;
--bg-hover: #e2e2dc;
--bg-active: #d8d8d2;
--border-subtle: #d0d0ca;
--border-muted: #c0c0b8;
--border-strong: #a8a8a0;
--text-primary: #1a1f2e;
--text-secondary:#4a5568;
--text-muted: #718096;
--text-disabled: #a0aec0;
/* Slightly desaturated accents so they work on a light surface */
--cyan: #007fa8;
--cyan-dim: #005c7a;
--cyan-glow: rgba(0, 127, 168, 0.14);
--green: #166534;
--green-dim: #14532d;
--green-glow: rgba(22, 101, 52, 0.12);
--amber: #92400e;
--amber-dim: #78350f;
--amber-glow: rgba(146, 64, 14, 0.1);
--red: #b91c1c;
--red-dim: #7f1d1d;
--red-glow: rgba(185, 28, 28, 0.1);
--shadow-sm: 0 1px 3px rgba(0,0,0,0.08);
--shadow-md: 0 4px 16px rgba(0,0,0,0.1);
--shadow-lg: 0 8px 32px rgba(0,0,0,0.12);
--shadow-cyan: 0 0 20px rgba(0,127,168,0.1);
}
/* ── Reset ──────────────────────────────────────────────────────────────── */
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
font-size: 16px;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
}
body {
font-family: var(--font-ui);
font-size: var(--text-base);
color: var(--text-primary);
background-color: var(--bg-base);
line-height: 1.6;
min-height: 100vh;
overflow-x: hidden;
}
/* Scrollbar */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: var(--bg-base); }
::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }
/* Focus ring */
:focus-visible {
outline: 2px solid var(--cyan);
outline-offset: 2px;
border-radius: var(--radius-sm);
}
/* ── Typography helpers ──────────────────────────────────────────────────── */
.mono { font-family: var(--font-mono); }
.label { font-size: var(--text-xs); font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; }
.muted { color: var(--text-muted); }
.subtle { color: var(--text-secondary); }

482
static/css/components.css Normal file
View File

@@ -0,0 +1,482 @@
/*
* 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); }
}

131
static/css/layout.css Normal file
View File

@@ -0,0 +1,131 @@
/*
* static/css/layout.css
* Page-level layout: header, main grid, panels.
*/
/* ── Page structure ─────────────────────────────────────────────────────── */
.page-header {
position: sticky;
top: 0;
z-index: 100;
background: var(--bg-void);
border-bottom: 1px solid var(--border-subtle);
padding: 0 var(--space-6);
height: 56px;
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-4);
backdrop-filter: blur(12px);
}
.header-brand {
display: flex;
align-items: center;
gap: var(--space-3);
}
.header-brand-name {
font-size: var(--text-lg);
font-weight: 800;
letter-spacing: -0.02em;
color: var(--text-primary);
}
.header-brand-name span {
color: var(--cyan);
}
.header-indicators {
display: flex;
align-items: center;
gap: var(--space-5);
}
/* ── Main layout ────────────────────────────────────────────────────────── */
.page-main {
max-width: 1280px;
margin: 0 auto;
padding: var(--space-8) var(--space-6) var(--space-12);
display: grid;
grid-template-columns: 1fr 320px;
grid-template-rows: auto;
gap: var(--space-6);
grid-template-areas:
"composer sidebar"
"queue sidebar"
"history sidebar";
}
.area-composer { grid-area: composer; }
.area-queue { grid-area: queue; }
.area-history { grid-area: history; }
.area-sidebar { grid-area: sidebar; display: flex; flex-direction: column; gap: var(--space-4); }
/* ── Panel container ─────────────────────────────────────────────────────── */
.panel {
background: var(--bg-raised);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-lg);
overflow: hidden;
}
.panel-header {
padding: var(--space-4) var(--space-5);
border-bottom: 1px solid var(--border-subtle);
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-3);
}
.panel-title {
font-size: var(--text-xs);
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--text-secondary);
display: flex;
align-items: center;
gap: var(--space-2);
}
.panel-body {
padding: var(--space-5);
}
.panel-body--flush {
padding: 0;
}
/* ── Responsive ──────────────────────────────────────────────────────────── */
@media (max-width: 900px) {
.page-main {
grid-template-columns: 1fr;
grid-template-areas:
"sidebar"
"composer"
"queue"
"history";
}
.area-sidebar {
flex-direction: row;
flex-wrap: wrap;
gap: var(--space-4);
}
.area-sidebar > .panel {
flex: 1 1 260px;
}
}
@media (max-width: 560px) {
.page-header { padding: 0 var(--space-4); }
.page-main { padding: var(--space-5) var(--space-4) var(--space-10); gap: var(--space-4); }
}