feat: LCD aesthetic CSS with responsive layout and per-state animations
This commit is contained in:
427
frontend/style.css
Normal file
427
frontend/style.css
Normal file
@@ -0,0 +1,427 @@
|
||||
/* ── Reset & base ─────────────────────────────────────────────────────────── */
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
:root {
|
||||
--bg: #0a0a04;
|
||||
--amber: #ffb000;
|
||||
--amber-dim: #a07000;
|
||||
--amber-dk: #6a4a00;
|
||||
--green: #00ff88;
|
||||
--red: #ff4444;
|
||||
--font: 'Press Start 2P', monospace;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
background: var(--bg);
|
||||
color: var(--amber);
|
||||
font-family: var(--font);
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
/* ── Two-panel layout ────────────────────────────────────────────────────── */
|
||||
#app {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
gap: 2rem;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
/* Portrait / narrow: stack vertically */
|
||||
@media (orientation: portrait) {
|
||||
#app {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Screen panel ────────────────────────────────────────────────────────── */
|
||||
#screen-panel {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
#screen {
|
||||
position: relative;
|
||||
width: 280px;
|
||||
min-height: 380px;
|
||||
background: #0d0d06;
|
||||
border: 3px solid var(--amber-dim);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem 1rem 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.8rem;
|
||||
/* CRT scanline overlay */
|
||||
background-image: repeating-linear-gradient(
|
||||
0deg,
|
||||
transparent,
|
||||
transparent 2px,
|
||||
rgba(0,0,0,0.15) 2px,
|
||||
rgba(0,0,0,0.15) 4px
|
||||
);
|
||||
box-shadow: 0 0 24px rgba(255,176,0,0.15), inset 0 0 40px rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
/* ── WebSocket status dot ────────────────────────────────────────────────── */
|
||||
#ws-dot {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--red);
|
||||
transition: background 0.3s;
|
||||
}
|
||||
#ws-dot.connected { background: var(--green); }
|
||||
#ws-dot.pulse { animation: pulse-dot 0.3s ease-out; }
|
||||
|
||||
@keyframes pulse-dot {
|
||||
0% { transform: scale(1.8); opacity: 0.6; }
|
||||
100% { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
|
||||
/* ── Llama container ─────────────────────────────────────────────────────── */
|
||||
#llama-wrap {
|
||||
position: relative;
|
||||
width: 160px;
|
||||
height: 200px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#llama {
|
||||
width: 140px;
|
||||
height: 180px;
|
||||
}
|
||||
|
||||
/* SVG part colors */
|
||||
.part { fill: var(--amber); stroke: none; }
|
||||
.tail { stroke: var(--amber); fill: none; }
|
||||
.hoof { fill: var(--amber-dk); }
|
||||
.nostril { fill: var(--bg); }
|
||||
.shine { fill: var(--bg); }
|
||||
.mouth-closed { stroke: var(--bg); fill: none; }
|
||||
.mouth-open { fill: var(--bg); }
|
||||
.book { fill: var(--amber-dim); stroke: var(--amber-dk); stroke-width: 0.5; }
|
||||
.book line { stroke: var(--amber); }
|
||||
.sleep-eye { stroke: var(--bg); fill: none; }
|
||||
|
||||
/* ── State-based element visibility ─────────────────────────────────────── */
|
||||
|
||||
/* Default: open eyes, no accessories */
|
||||
.sleep-eye { display: none; }
|
||||
.mouth-open { display: none; }
|
||||
.book { display: none; }
|
||||
#zzz-wrap { display: none; }
|
||||
#speech-bubble { display: none; }
|
||||
#progress-wrap { display: none; }
|
||||
|
||||
/* sleeping */
|
||||
body.state-sleeping .sleep-eye { display: block; }
|
||||
body.state-sleeping .eye { display: none; }
|
||||
body.state-sleeping .shine { display: none; }
|
||||
body.state-sleeping #zzz-wrap { display: flex; }
|
||||
|
||||
/* processing_prompt */
|
||||
body.state-processing_prompt .book { display: block; }
|
||||
|
||||
/* generating */
|
||||
body.state-generating .mouth-open { display: block; }
|
||||
body.state-generating .mouth-closed { display: none; }
|
||||
body.state-generating #speech-bubble { display: flex; }
|
||||
|
||||
/* loading */
|
||||
body.state-loading #progress-wrap { display: flex; }
|
||||
|
||||
/* offline */
|
||||
body.state-offline #llama { opacity: 0.15; }
|
||||
|
||||
/* ── ZZZ ─────────────────────────────────────────────────────────────────── */
|
||||
#zzz-wrap {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 4px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 2px;
|
||||
pointer-events: none;
|
||||
}
|
||||
.z {
|
||||
font-family: var(--font);
|
||||
color: var(--amber);
|
||||
animation: float-z 2.4s ease-in-out infinite;
|
||||
opacity: 0;
|
||||
}
|
||||
.z1 { font-size: 9px; animation-delay: 0s; }
|
||||
.z2 { font-size: 11px; animation-delay: 0.6s; }
|
||||
.z3 { font-size: 13px; animation-delay: 1.2s; }
|
||||
|
||||
@keyframes float-z {
|
||||
0% { opacity: 0; transform: translateY(0); }
|
||||
20% { opacity: 1; }
|
||||
80% { opacity: 0.8; }
|
||||
100% { opacity: 0; transform: translateY(-28px); }
|
||||
}
|
||||
|
||||
/* ── Speech bubble ───────────────────────────────────────────────────────── */
|
||||
#speech-bubble {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 0;
|
||||
background: var(--amber);
|
||||
color: var(--bg);
|
||||
border-radius: 8px;
|
||||
padding: 4px 7px;
|
||||
font-size: 14px;
|
||||
gap: 1px;
|
||||
pointer-events: none;
|
||||
}
|
||||
#speech-bubble::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -7px;
|
||||
left: 10px;
|
||||
border: 7px solid transparent;
|
||||
border-top-color: var(--amber);
|
||||
border-bottom: 0;
|
||||
}
|
||||
.dot {
|
||||
animation: blink-dot 1.2s ease-in-out infinite;
|
||||
opacity: 0;
|
||||
}
|
||||
.d1 { animation-delay: 0s; }
|
||||
.d2 { animation-delay: 0.3s; }
|
||||
.d3 { animation-delay: 0.6s; }
|
||||
|
||||
@keyframes blink-dot {
|
||||
0%, 100% { opacity: 0; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
|
||||
/* ── State label ─────────────────────────────────────────────────────────── */
|
||||
#state-label {
|
||||
font-size: 8px;
|
||||
color: var(--amber-dim);
|
||||
letter-spacing: 2px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* ── Progress bar ────────────────────────────────────────────────────────── */
|
||||
#progress-wrap {
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
#progress-bar {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
border: 1px solid var(--amber-dim);
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
#progress-fill {
|
||||
height: 100%;
|
||||
background: var(--amber);
|
||||
width: 0%;
|
||||
transition: width 0.4s ease;
|
||||
}
|
||||
#progress-label {
|
||||
font-size: 7px;
|
||||
color: var(--amber-dim);
|
||||
}
|
||||
|
||||
/* ── Stats panel ─────────────────────────────────────────────────────────── */
|
||||
#stats-panel {
|
||||
flex: 0 0 220px;
|
||||
background: #0d0d06;
|
||||
border: 3px solid var(--amber-dim);
|
||||
border-radius: 12px;
|
||||
padding: 1.2rem 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.9rem;
|
||||
box-shadow: 0 0 24px rgba(255,176,0,0.10);
|
||||
}
|
||||
|
||||
@media (orientation: portrait) {
|
||||
#stats-panel { width: 280px; flex: 0 0 auto; }
|
||||
}
|
||||
|
||||
.stat-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
.sk {
|
||||
font-size: 7px;
|
||||
color: var(--amber-dk);
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
.sv {
|
||||
font-size: 8px;
|
||||
color: var(--amber);
|
||||
word-break: break-all;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* hide irrelevant rows per state */
|
||||
body.state-offline #row-model,
|
||||
body.state-offline #row-prompt,
|
||||
body.state-offline #row-speed,
|
||||
body.state-offline #row-tokens,
|
||||
body.state-offline #row-load-stage { display: none; }
|
||||
|
||||
body.state-starting #row-prompt,
|
||||
body.state-starting #row-speed,
|
||||
body.state-starting #row-tokens,
|
||||
body.state-starting #row-load-stage { display: none; }
|
||||
|
||||
body.state-loading #row-prompt,
|
||||
body.state-loading #row-speed,
|
||||
body.state-loading #row-tokens { display: none; }
|
||||
|
||||
body.state-idle #row-prompt,
|
||||
body.state-idle #row-speed,
|
||||
body.state-idle #row-tokens,
|
||||
body.state-idle #row-load-stage { display: none; }
|
||||
|
||||
body.state-sleeping #row-prompt,
|
||||
body.state-sleeping #row-speed,
|
||||
body.state-sleeping #row-tokens,
|
||||
body.state-sleeping #row-load-stage { display: none; }
|
||||
|
||||
body.state-waiting #row-prompt,
|
||||
body.state-waiting #row-speed,
|
||||
body.state-waiting #row-tokens,
|
||||
body.state-waiting #row-load-stage { display: none; }
|
||||
|
||||
body.state-unloading #row-prompt,
|
||||
body.state-unloading #row-speed,
|
||||
body.state-unloading #row-tokens,
|
||||
body.state-unloading #row-load-stage { display: none; }
|
||||
|
||||
body.state-processing_prompt #row-speed,
|
||||
body.state-processing_prompt #row-load-stage { display: none; }
|
||||
|
||||
body.state-generating #row-load-stage { display: none; }
|
||||
|
||||
/* ── Per-state SVG animations ────────────────────────────────────────────── */
|
||||
|
||||
/* sleeping: gentle breathe on body */
|
||||
body.state-sleeping .body,
|
||||
body.state-sleeping .neck,
|
||||
body.state-sleeping .head,
|
||||
body.state-sleeping .ear {
|
||||
animation: breathe 3.5s ease-in-out infinite;
|
||||
transform-origin: 50px 90px;
|
||||
}
|
||||
|
||||
@keyframes breathe {
|
||||
0%, 100% { transform: scaleY(1); }
|
||||
50% { transform: scaleY(1.03); }
|
||||
}
|
||||
|
||||
/* idle: occasional ear twitch */
|
||||
body.state-idle .right-ear {
|
||||
animation: ear-twitch 5s ease-in-out infinite;
|
||||
transform-origin: 62px 22px;
|
||||
}
|
||||
|
||||
@keyframes ear-twitch {
|
||||
0%, 85%, 100% { transform: rotate(15deg); }
|
||||
90% { transform: rotate(5deg); }
|
||||
95% { transform: rotate(22deg); }
|
||||
}
|
||||
|
||||
/* waiting: both ears perk forward */
|
||||
body.state-waiting .left-ear {
|
||||
transform: rotate(-5deg);
|
||||
transform-origin: 38px 22px;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
body.state-waiting .right-ear {
|
||||
transform: rotate(5deg);
|
||||
transform-origin: 62px 22px;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
/* loading: walk cycle on legs */
|
||||
body.state-loading .leg-fl,
|
||||
body.state-loading .leg-bl {
|
||||
animation: walk-a 0.7s ease-in-out infinite;
|
||||
transform-origin: 37px 100px;
|
||||
}
|
||||
body.state-loading .leg-fr,
|
||||
body.state-loading .leg-br {
|
||||
animation: walk-b 0.7s ease-in-out infinite;
|
||||
transform-origin: 51px 100px;
|
||||
}
|
||||
|
||||
@keyframes walk-a {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-6px); }
|
||||
}
|
||||
@keyframes walk-b {
|
||||
0%, 100% { transform: translateY(-6px); }
|
||||
50% { transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* processing_prompt: eyes move side to side (reading) */
|
||||
body.state-processing_prompt .left-eye,
|
||||
body.state-processing_prompt .right-eye,
|
||||
body.state-processing_prompt .shine {
|
||||
animation: read-eyes 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes read-eyes {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
30% { transform: translateX(-1.5px); }
|
||||
70% { transform: translateX(1.5px); }
|
||||
}
|
||||
|
||||
/* generating: tail wag + mouth animation */
|
||||
body.state-generating .tail {
|
||||
animation: wag 0.45s ease-in-out infinite alternate;
|
||||
transform-origin: 72px 82px;
|
||||
}
|
||||
body.state-generating .mouth-open {
|
||||
animation: talk 0.3s ease-in-out infinite alternate;
|
||||
transform-origin: 50px 39px;
|
||||
}
|
||||
|
||||
@keyframes wag {
|
||||
0% { transform: rotate(-18deg); }
|
||||
100% { transform: rotate(18deg); }
|
||||
}
|
||||
@keyframes talk {
|
||||
0% { transform: scaleY(0.4); }
|
||||
100% { transform: scaleY(1); }
|
||||
}
|
||||
|
||||
/* warming_up: yawn — jaw drops */
|
||||
body.state-warming_up .mouth-open { display: block; }
|
||||
body.state-warming_up .mouth-closed { display: none; }
|
||||
body.state-warming_up .mouth-open {
|
||||
animation: yawn 2.5s ease-in-out infinite;
|
||||
transform-origin: 50px 39px;
|
||||
}
|
||||
@keyframes yawn {
|
||||
0%, 20%, 100% { transform: scaleY(0.3); }
|
||||
50%, 70% { transform: scaleY(1.4); }
|
||||
}
|
||||
|
||||
/* unloading: whole llama fades and drifts right */
|
||||
body.state-unloading #llama {
|
||||
animation: unload 1.5s ease-in-out infinite alternate;
|
||||
}
|
||||
@keyframes unload {
|
||||
0% { transform: translateX(0); opacity: 1; }
|
||||
100% { transform: translateX(12px); opacity: 0.4; }
|
||||
}
|
||||
Reference in New Issue
Block a user