Files
web-app-launcher/design-mockups/01-command-deck.html
T
alexei.dolgolyov 5dcadd1c20 feat(ui): migrate entire UI to "Cozy Home" design
Warm, friendly redesign replacing the generic cold-shadcn look. Built as a
swappable token bundle so other presets can be added later; dark mode and the
user-tunable accent hue are retained.

Foundation
- app.css: warm cream (light) + "dusk" (dark) token system; terracotta accent
  (default hue 16); pastel --room-* palette; vivid --status-* (dots/bars) plus
  AA-legible --status-*-ink (text); soft warm shadows; --radius 1rem; font tokens
- Fonts: Fraunces (display) + Figtree (body), self-hosted in static/fonts
  (no Google CDN) so offline/LAN installs work; system-ui fallbacks kept
- h1/h2/h3 render in Fraunces via base layer

Chrome and surfaces
- Sidebar, Header, home, AppCard/BoardCard, BoardHeader, sections, favorites
- 29 widgets + integration renderers: cozy card shells, room-palette charts
- Default background is a static warm "cozy" glow (mesh demoted, rAF gated on
  prefers-reduced-motion)

System-wide
- Status colors tokenized (no raw bg/text-*-500 or status hex); success/warning
  to status tokens, categorical to room palette, errors to destructive
- Inputs rounded-xl; buttons rounded-xl; cards/dialogs rounded-[1.4rem];
  soft-shadow vocabulary only; focus-visible:ring-primary/30
- Forms, admin tables (now cozy cards), dialogs, popovers, auth screens

a11y: reduced-motion guards; darker status "ink" text for AA on cream.
Known tradeoff: terracotta primary + white button text ~2.96:1 (signature color,
user-tunable).

Verified: svelte-check 0/0, build ok, 274 tests pass, eslint 0 errors.
Design refs + system sheet in design-mockups/.
2026-05-27 23:04:47 +03:00

790 lines
20 KiB
HTML

<!doctype html>
<html lang="en" class="dark">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Web App Launcher — Command Deck</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Saira:wght@400;500;600;700&family=Saira+Condensed:wght@500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap"
rel="stylesheet"
/>
<style>
:root {
--bg: #070a0d;
--panel: #0d1217;
--panel-2: #10171e;
--line: #1d2730;
--line-bright: #2b3946;
--ink: #e7eef3;
--ink-dim: #7c8b97;
--ink-faint: #4a5763;
--accent: #36e0a4; /* tactical green */
--accent-2: #ffb020; /* amber */
--danger: #ff4d5e;
--warn: #ffb020;
--grid: rgba(54, 224, 164, 0.04);
--radius: 4px;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html,
body {
height: 100%;
}
body {
background: var(--bg);
color: var(--ink);
font-family: 'Saira', system-ui, sans-serif;
-webkit-font-smoothing: antialiased;
overflow-x: hidden;
}
/* subtle scanline grid backdrop */
body::before {
content: '';
position: fixed;
inset: 0;
z-index: 0;
pointer-events: none;
background-image:
linear-gradient(var(--grid) 1px, transparent 1px),
linear-gradient(90deg, var(--grid) 1px, transparent 1px);
background-size: 40px 40px;
mask-image: radial-gradient(ellipse 80% 60% at 70% 0%, #000 30%, transparent 90%);
}
.app {
position: relative;
z-index: 1;
display: grid;
grid-template-columns: 74px 1fr;
min-height: 100vh;
}
.mono {
font-family: 'JetBrains Mono', monospace;
}
.cond {
font-family: 'Saira Condensed', sans-serif;
}
/* ===== Rail ===== */
.rail {
border-right: 1px solid var(--line);
background: linear-gradient(180deg, var(--panel), #080c10);
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
padding: 16px 0;
}
.logo {
width: 40px;
height: 40px;
border: 1px solid var(--accent);
border-radius: var(--radius);
display: grid;
place-items: center;
color: var(--accent);
margin-bottom: 18px;
box-shadow:
0 0 0 1px #0a1f18,
0 0 18px -4px var(--accent);
position: relative;
}
.logo svg {
width: 20px;
height: 20px;
}
.rail-btn {
width: 44px;
height: 44px;
border-radius: var(--radius);
display: grid;
place-items: center;
color: var(--ink-dim);
position: relative;
cursor: pointer;
transition: 0.15s;
}
.rail-btn:hover {
color: var(--ink);
background: var(--panel-2);
}
.rail-btn.active {
color: var(--accent);
}
.rail-btn.active::before {
content: '';
position: absolute;
left: -16px;
top: 10px;
bottom: 10px;
width: 2px;
background: var(--accent);
box-shadow: 0 0 8px var(--accent);
}
.rail-btn svg {
width: 20px;
height: 20px;
}
.rail-spacer {
flex: 1;
}
.rail-avatar {
width: 36px;
height: 36px;
border-radius: var(--radius);
background: linear-gradient(135deg, #1a2a22, #0f1a14);
border: 1px solid var(--line-bright);
display: grid;
place-items: center;
font-weight: 700;
color: var(--accent);
font-size: 13px;
}
/* ===== Main ===== */
.main {
display: flex;
flex-direction: column;
min-width: 0;
}
.topbar {
display: flex;
align-items: center;
gap: 16px;
height: 60px;
padding: 0 26px;
border-bottom: 1px solid var(--line);
background: rgba(8, 12, 16, 0.6);
backdrop-filter: blur(8px);
}
.crumbs {
font-family: 'Saira Condensed';
text-transform: uppercase;
letter-spacing: 0.14em;
font-size: 12px;
color: var(--ink-faint);
}
.crumbs b {
color: var(--ink-dim);
font-weight: 600;
}
.search {
margin-left: auto;
display: flex;
align-items: center;
gap: 10px;
width: 340px;
max-width: 38vw;
background: var(--panel);
border: 1px solid var(--line);
border-radius: var(--radius);
padding: 9px 12px;
color: var(--ink-dim);
font-size: 13px;
cursor: text;
transition: 0.15s;
}
.search:hover {
border-color: var(--line-bright);
}
.search svg {
width: 15px;
height: 15px;
}
.search .kbd {
margin-left: auto;
font-family: 'JetBrains Mono';
font-size: 10px;
color: var(--ink-faint);
border: 1px solid var(--line);
border-radius: 3px;
padding: 2px 6px;
}
.ico-btn {
width: 38px;
height: 38px;
border: 1px solid var(--line);
border-radius: var(--radius);
display: grid;
place-items: center;
color: var(--ink-dim);
cursor: pointer;
transition: 0.15s;
background: var(--panel);
}
.ico-btn:hover {
color: var(--ink);
border-color: var(--line-bright);
}
.ico-btn svg {
width: 17px;
height: 17px;
}
.content {
padding: 26px;
max-width: 1320px;
width: 100%;
margin: 0 auto;
}
/* status bar */
.statline {
display: flex;
align-items: stretch;
gap: 1px;
border: 1px solid var(--line);
border-radius: var(--radius);
overflow: hidden;
margin-bottom: 24px;
background: var(--line);
}
.stat {
flex: 1;
background: var(--panel);
padding: 16px 18px;
position: relative;
}
.stat .lbl {
font-family: 'Saira Condensed';
text-transform: uppercase;
letter-spacing: 0.16em;
font-size: 11px;
color: var(--ink-faint);
}
.stat .val {
font-family: 'JetBrains Mono';
font-size: 26px;
font-weight: 700;
margin-top: 6px;
letter-spacing: -0.02em;
}
.stat .val.ok {
color: var(--accent);
}
.stat .val.warn {
color: var(--warn);
}
.stat .val.bad {
color: var(--danger);
}
.stat .sub {
font-size: 12px;
color: var(--ink-dim);
margin-top: 2px;
}
.stat::after {
content: '';
position: absolute;
left: 0;
top: 0;
height: 2px;
width: 100%;
background: linear-gradient(90deg, var(--accent), transparent);
}
.stat.s2::after {
background: linear-gradient(90deg, var(--accent-2), transparent);
}
.stat.s3::after {
background: linear-gradient(90deg, #3aa0ff, transparent);
}
.stat.s4::after {
background: linear-gradient(90deg, var(--danger), transparent);
}
.sec-head {
display: flex;
align-items: center;
gap: 14px;
margin: 30px 0 16px;
}
.sec-head h2 {
font-family: 'Saira Condensed';
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.12em;
font-size: 15px;
color: var(--ink);
}
.sec-head .rule {
flex: 1;
height: 1px;
background: linear-gradient(90deg, var(--line-bright), transparent);
}
.sec-head .count {
font-family: 'JetBrains Mono';
font-size: 11px;
color: var(--ink-faint);
}
/* app grid */
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(248px, 1fr));
gap: 14px;
}
.node {
background: linear-gradient(180deg, var(--panel), var(--panel-2));
border: 1px solid var(--line);
border-radius: var(--radius);
padding: 16px;
cursor: pointer;
position: relative;
transition:
transform 0.15s,
border-color 0.15s,
box-shadow 0.15s;
overflow: hidden;
}
.node::before {
content: '';
position: absolute;
inset: 0;
background: radial-gradient(circle at 100% 0%, rgba(54, 224, 164, 0.08), transparent 60%);
opacity: 0;
transition: 0.2s;
}
.node:hover {
transform: translateY(-2px);
border-color: var(--line-bright);
box-shadow: 0 10px 30px -12px #000;
}
.node:hover::before {
opacity: 1;
}
.node-top {
display: flex;
align-items: center;
gap: 12px;
}
.node-ico {
width: 40px;
height: 40px;
border-radius: var(--radius);
background: #0a0f13;
border: 1px solid var(--line);
display: grid;
place-items: center;
font-size: 18px;
flex-shrink: 0;
}
.node-name {
font-weight: 600;
font-size: 15px;
line-height: 1.1;
}
.node-cat {
font-family: 'Saira Condensed';
text-transform: uppercase;
letter-spacing: 0.1em;
font-size: 10px;
color: var(--ink-faint);
margin-top: 3px;
}
.led {
width: 8px;
height: 8px;
border-radius: 50%;
margin-left: auto;
flex-shrink: 0;
position: relative;
}
.led.ok {
background: var(--accent);
box-shadow: 0 0 10px var(--accent);
}
.led.ok::after {
content: '';
position: absolute;
inset: -4px;
border-radius: 50%;
border: 1px solid var(--accent);
opacity: 0.4;
animation: ping 2s ease-out infinite;
}
.led.warn {
background: var(--warn);
box-shadow: 0 0 10px var(--warn);
}
.led.bad {
background: var(--danger);
box-shadow: 0 0 10px var(--danger);
}
@keyframes ping {
0% {
transform: scale(0.8);
opacity: 0.5;
}
100% {
transform: scale(2.2);
opacity: 0;
}
}
.node-foot {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 16px;
padding-top: 12px;
border-top: 1px solid var(--line);
}
.node-foot .up {
font-family: 'JetBrains Mono';
font-size: 11px;
color: var(--ink-dim);
}
.node-foot .up b {
color: var(--accent);
font-weight: 500;
}
.node-foot .up.bad b {
color: var(--danger);
}
.spark {
height: 22px;
width: 96px;
}
.footer-note {
margin-top: 40px;
text-align: center;
font-family: 'JetBrains Mono';
font-size: 11px;
color: var(--ink-faint);
}
/* entrance */
@keyframes rise {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: none;
}
}
.node,
.stat {
animation: rise 0.5s both;
}
.stat:nth-child(2) {
animation-delay: 0.05s;
}
.stat:nth-child(3) {
animation-delay: 0.1s;
}
.stat:nth-child(4) {
animation-delay: 0.15s;
}
.grid .node:nth-child(1) {
animation-delay: 0.1s;
}
.grid .node:nth-child(2) {
animation-delay: 0.16s;
}
.grid .node:nth-child(3) {
animation-delay: 0.22s;
}
.grid .node:nth-child(4) {
animation-delay: 0.28s;
}
.grid .node:nth-child(5) {
animation-delay: 0.34s;
}
.grid .node:nth-child(6) {
animation-delay: 0.4s;
}
.grid .node:nth-child(7) {
animation-delay: 0.46s;
}
.grid .node:nth-child(8) {
animation-delay: 0.52s;
}
</style>
</head>
<body>
<div class="app">
<!-- Rail -->
<nav class="rail">
<div class="logo" title="Launcher">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="7" height="7" />
<rect x="14" y="3" width="7" height="7" />
<rect x="14" y="14" width="7" height="7" />
<rect x="3" y="14" width="7" height="7" />
</svg>
</div>
<a class="rail-btn active" title="Overview"
><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
<rect x="3" y="3" width="18" height="18" rx="2" />
<line x1="3" y1="9" x2="21" y2="9" />
<line x1="9" y1="21" x2="9" y2="9" /></svg
></a>
<a class="rail-btn" title="Apps"
><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
<circle cx="12" cy="12" r="9" />
<line x1="3" y1="12" x2="21" y2="12" />
<path d="M12 3a14 14 0 0 1 0 18 14 14 0 0 1 0-18" /></svg
></a>
<a class="rail-btn" title="Status"
><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
<path d="M22 12h-4l-3 9L9 3l-3 9H2" /></svg
></a>
<a class="rail-btn" title="Admin"
><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
<circle cx="12" cy="12" r="3" />
<path
d="M19 12a7 7 0 0 0-.1-1l2-1.6-2-3.4-2.4 1a7 7 0 0 0-1.7-1l-.4-2.6h-4l-.4 2.6a7 7 0 0 0-1.7 1l-2.4-1-2 3.4 2 1.6a7 7 0 0 0 0 2l-2 1.6 2 3.4 2.4-1a7 7 0 0 0 1.7 1l.4 2.6h4l.4-2.6a7 7 0 0 0 1.7-1l2.4 1 2-3.4-2-1.6a7 7 0 0 0 .1-1z"
/></svg
></a>
<div class="rail-spacer"></div>
<div class="rail-avatar">AD</div>
</nav>
<!-- Main -->
<div class="main">
<header class="topbar">
<div class="crumbs">SYSTEMS / <b>OVERVIEW</b></div>
<div class="search">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="7" />
<line x1="21" y1="21" x2="16.65" y2="16.65" />
</svg>
Search apps, boards, commands…
<span class="kbd">⌘K</span>
</div>
<div class="ico-btn" title="Notifications">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
<path d="M18 8a6 6 0 1 0-12 0c0 7-3 9-3 9h18s-3-2-3-9" />
<path d="M13.7 21a2 2 0 0 1-3.4 0" />
</svg>
</div>
<div class="ico-btn" title="Theme">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
<circle cx="12" cy="12" r="4" />
<path
d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"
/>
</svg>
</div>
</header>
<div class="content">
<!-- Status line -->
<div class="statline">
<div class="stat">
<div class="lbl">Services Online</div>
<div class="val ok">08 / 10</div>
<div class="sub">2 require attention</div>
</div>
<div class="stat s2">
<div class="lbl">Avg Response</div>
<div class="val warn">
142<span style="font-size: 14px; color: var(--ink-faint)"> ms</span>
</div>
<div class="sub">p95 over 24h</div>
</div>
<div class="stat s3">
<div class="lbl">Fleet Uptime</div>
<div class="val" style="color: #3aa0ff">99.4%</div>
<div class="sub">rolling 30 days</div>
</div>
<div class="stat s4">
<div class="lbl">UPS Load</div>
<div class="val bad">61%</div>
<div class="sub">est. 38 min on battery</div>
</div>
</div>
<!-- Favorites / pinned -->
<div class="sec-head">
<h2>Pinned Services</h2>
<span class="rule"></span><span class="count">8 ACTIVE</span>
</div>
<div class="grid">
<!-- 1 Jellyfin -->
<div class="node">
<div class="node-top">
<div class="node-ico">🎬</div>
<div>
<div class="node-name">Jellyfin</div>
<div class="node-cat">Media</div>
</div>
<div class="led ok"></div>
</div>
<div class="node-foot">
<svg class="spark" viewBox="0 0 96 22" preserveAspectRatio="none">
<polyline
fill="none"
stroke="#36e0a4"
stroke-width="1.5"
points="0,16 12,14 24,17 36,9 48,12 60,7 72,10 84,5 96,8"
/></svg
><span class="up"><b>99.9%</b> 24h</span>
</div>
</div>
<!-- 2 Immich -->
<div class="node">
<div class="node-top">
<div class="node-ico">📷</div>
<div>
<div class="node-name">Immich</div>
<div class="node-cat">Photos</div>
</div>
<div class="led ok"></div>
</div>
<div class="node-foot">
<svg class="spark" viewBox="0 0 96 22" preserveAspectRatio="none">
<polyline
fill="none"
stroke="#36e0a4"
stroke-width="1.5"
points="0,12 12,13 24,11 36,12 48,10 60,11 72,9 84,11 96,10"
/></svg
><span class="up"><b>100%</b> 24h</span>
</div>
</div>
<!-- 3 Gitea -->
<div class="node">
<div class="node-top">
<div class="node-ico">🌿</div>
<div>
<div class="node-name">Gitea</div>
<div class="node-cat">Git</div>
</div>
<div class="led ok"></div>
</div>
<div class="node-foot">
<svg class="spark" viewBox="0 0 96 22" preserveAspectRatio="none">
<polyline
fill="none"
stroke="#36e0a4"
stroke-width="1.5"
points="0,14 12,10 24,12 36,8 48,11 60,9 72,13 84,8 96,9"
/></svg
><span class="up"><b>99.8%</b> 24h</span>
</div>
</div>
<!-- 4 Portainer -->
<div class="node">
<div class="node-top">
<div class="node-ico">🐳</div>
<div>
<div class="node-name">Portainer</div>
<div class="node-cat">Containers</div>
</div>
<div class="led warn"></div>
</div>
<div class="node-foot">
<svg class="spark" viewBox="0 0 96 22" preserveAspectRatio="none">
<polyline
fill="none"
stroke="#ffb020"
stroke-width="1.5"
points="0,10 12,12 24,9 36,15 48,11 60,18 72,12 84,16 96,13"
/></svg
><span class="up"><b style="color: var(--warn)">98.1%</b> 24h</span>
</div>
</div>
<!-- 5 Pi-hole -->
<div class="node">
<div class="node-top">
<div class="node-ico">🛡️</div>
<div>
<div class="node-name">Pi-hole</div>
<div class="node-cat">DNS</div>
</div>
<div class="led ok"></div>
</div>
<div class="node-foot">
<svg class="spark" viewBox="0 0 96 22" preserveAspectRatio="none">
<polyline
fill="none"
stroke="#36e0a4"
stroke-width="1.5"
points="0,13 12,11 24,12 36,10 48,11 60,9 72,10 84,8 96,9"
/></svg
><span class="up"><b>100%</b> 24h</span>
</div>
</div>
<!-- 6 Planka -->
<div class="node">
<div class="node-top">
<div class="node-ico">📋</div>
<div>
<div class="node-name">Planka</div>
<div class="node-cat">Kanban</div>
</div>
<div class="led ok"></div>
</div>
<div class="node-foot">
<svg class="spark" viewBox="0 0 96 22" preserveAspectRatio="none">
<polyline
fill="none"
stroke="#36e0a4"
stroke-width="1.5"
points="0,15 12,12 24,14 36,11 48,12 60,10 72,12 84,9 96,11"
/></svg
><span class="up"><b>99.5%</b> 24h</span>
</div>
</div>
<!-- 7 Deluge -->
<div class="node">
<div class="node-top">
<div class="node-ico">⬇️</div>
<div>
<div class="node-name">Deluge</div>
<div class="node-cat">Downloads</div>
</div>
<div class="led bad"></div>
</div>
<div class="node-foot">
<svg class="spark" viewBox="0 0 96 22" preserveAspectRatio="none">
<polyline
fill="none"
stroke="#ff4d5e"
stroke-width="1.5"
points="0,9 12,11 24,14 36,12 48,18 60,16 72,20 84,19 96,21"
/></svg
><span class="up bad"><b>OFFLINE</b></span>
</div>
</div>
<!-- 8 Pi-hole / NPM -->
<div class="node">
<div class="node-top">
<div class="node-ico">🔀</div>
<div>
<div class="node-name">Nginx Proxy Mgr</div>
<div class="node-cat">Network</div>
</div>
<div class="led ok"></div>
</div>
<div class="node-foot">
<svg class="spark" viewBox="0 0 96 22" preserveAspectRatio="none">
<polyline
fill="none"
stroke="#36e0a4"
stroke-width="1.5"
points="0,12 12,11 24,12 36,10 48,11 60,11 72,9 84,10 96,9"
/></svg
><span class="up"><b>99.9%</b> 24h</span>
</div>
</div>
</div>
<div class="footer-note">
// COMMAND DECK — Saira + JetBrains Mono · tactical dark · LED telemetry · monospace
data
</div>
</div>
</div>
</div>
</body>
</html>