Files
Tomas Dvorak 564a39b0e8 docs: add mockups D-F, mark casa glass rejected
D merges the two shortlisted directions (bento grid, mono palette,
no glow). E and F probe two different paradigms: editorial list and
terminal console.


Generated with [Devin](https://devin.ai)

Authored By: TDvorak <[email protected]>
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-17 20:29:21 +02:00

436 lines
22 KiB
HTML
Raw Permalink 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.
<!doctype html>
<html lang="en" data-theme="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Dash — F · Console</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/fontsource/css/jetbrains-mono@latest/index.css">
<style>
:root { --ease: cubic-bezier(0.2, 0, 0, 1); }
[data-theme="dark"] {
--bg: #0c0c0d;
--surface: #111113;
--surface-hover: #17171a;
--border: rgba(255,255,255,0.10);
--border-strong: rgba(255,255,255,0.2);
--text: #e8e8ea;
--text-dim: #9c9ca4;
--text-faint: #55555e;
--up: #4ade80;
--down: #fbbf24;
--sel: rgba(255,255,255,0.06);
--icon-bg: #1a1a1d;
--popover: #141416;
}
[data-theme="light"] {
--bg: #f5f4ef;
--surface: #fdfdfb;
--surface-hover: #efeee9;
--border: rgba(0,0,0,0.14);
--border-strong: rgba(0,0,0,0.28);
--text: #1c1c1f;
--text-dim: #55555e;
--text-faint: #9d9d95;
--up: #15803d;
--down: #b45309;
--sel: rgba(0,0,0,0.05);
--icon-bg: #ecebe6;
--popover: #fdfdfb;
}
* { box-sizing: border-box; margin: 0; }
html { -webkit-font-smoothing: antialiased; }
body {
font-family: "JetBrains Mono", ui-monospace, "SF Mono", monospace;
background: var(--bg); color: var(--text); min-height: 100dvh;
font-size: 13px;
transition: background 200ms var(--ease), color 200ms var(--ease);
}
/* ---------- top bar ---------- */
.topbar {
position: sticky; top: 0; z-index: 10;
display: flex; align-items: center; gap: 14px;
padding: 0 24px; height: 46px;
background: var(--bg);
border-bottom: 1px solid var(--border);
font-size: 12px;
}
.brand { display: flex; align-items: center; gap: 8px; font-weight: 700; }
.brand .cursor { display: inline-block; width: 8px; height: 15px; background: var(--text); animation: blink 1.1s steps(1) infinite; }
@keyframes blink { 50% { opacity: 0; } }
.path { color: var(--text-faint); }
.path b { color: var(--text-dim); font-weight: 500; }
.search {
margin-left: auto; display: flex; align-items: center; gap: 8px;
width: 230px; padding: 5px 10px;
border: 1px solid var(--border); border-radius: 4px;
color: var(--text-faint); font-size: 11.5px;
}
.tbtn {
display: flex; align-items: center; gap: 6px;
height: 28px; padding: 0 10px;
border: 1px solid var(--border); border-radius: 4px;
background: transparent; color: var(--text-dim);
font-family: inherit; font-size: 11.5px; cursor: pointer;
transition: color 120ms var(--ease), border-color 120ms var(--ease), background 120ms var(--ease);
}
.tbtn:hover { color: var(--text); border-color: var(--border-strong); background: var(--sel); }
.tbtn.primary { background: var(--text); color: var(--bg); border-color: var(--text); font-weight: 700; }
/* ---------- layout ---------- */
main { max-width: 1060px; margin: 0 auto; padding: 30px 24px 90px; }
.banner {
font-size: 11px; color: var(--text-faint); line-height: 1.7;
padding: 14px 16px; margin-bottom: 26px;
border: 1px solid var(--border); border-radius: 6px;
white-space: pre-wrap;
}
.banner b { color: var(--text-dim); font-weight: 500; }
.banner .ok { color: var(--up); }
/* ---------- widget panels ---------- */
.panels { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; margin-bottom: 26px; }
.panel {
border: 1px solid var(--border); border-radius: 6px;
background: var(--surface); overflow: hidden;
}
.panel .phead {
display: flex; align-items: center; gap: 7px;
padding: 8px 12px; border-bottom: 1px solid var(--border);
font-size: 10px; text-transform: uppercase; letter-spacing: 0.1em; color: var(--text-faint);
}
.panel .phead img { width: 12px; height: 12px; }
.panel .phead .r { margin-left: auto; color: var(--up); font-size: 9px; }
.panel .pbody { padding: 12px; }
.panel .v { font-size: 22px; font-weight: 500; letter-spacing: -0.02em; font-variant-numeric: tabular-nums; }
.panel .s { font-size: 10.5px; color: var(--text-faint); margin-top: 4px; }
.panel.wide { grid-column: span 2; }
.panel .clockv { font-size: 26px; font-variant-numeric: tabular-nums; }
.tzrow { display: flex; gap: 14px; margin-top: 8px; font-size: 10.5px; color: var(--text-faint); font-variant-numeric: tabular-nums; }
.tzrow b { color: var(--text-dim); font-weight: 500; }
/* ---------- sections ---------- */
.sec { margin-bottom: 24px; }
.sechead {
display: flex; align-items: center; gap: 10px;
padding: 4px 2px; margin-bottom: 8px;
font-size: 12px; cursor: pointer; user-select: none;
}
.sechead .hash { color: var(--text-faint); }
.sechead h2 { font-size: 12px; font-weight: 700; text-transform: lowercase; letter-spacing: 0.02em; }
.sechead .n { color: var(--text-faint); font-size: 11px; }
.sechead .fill { flex: 1; color: var(--text-faint); overflow: hidden; white-space: nowrap; }
.sechead .chev { color: var(--text-faint); transition: transform 150ms var(--ease); }
.sec.closed .chev { transform: rotate(-90deg); }
.table {
border: 1px solid var(--border); border-radius: 6px;
background: var(--surface); overflow: hidden;
}
.sec.closed .table { display: none; }
.row {
position: relative;
display: grid;
grid-template-columns: 20px 30px minmax(0,1.2fr) minmax(0,1fr) auto;
align-items: center; gap: 12px;
padding: 8px 12px;
border-bottom: 1px solid var(--border);
cursor: grab; font-size: 12.5px;
transition: background 120ms var(--ease);
}
.row:last-child { border-bottom: none; }
.row:hover { background: var(--sel); }
.row.dragging { opacity: 0.35; }
.row .prompt { color: var(--text-faint); }
.icon {
position: relative; width: 22px; height: 22px; border-radius: 5px;
background: var(--icon-bg);
display: flex; align-items: center; justify-content: center;
font-size: 10px; font-weight: 700; color: var(--text-dim);
overflow: hidden;
}
.icon::before { content: attr(data-letter); }
.icon img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; padding: 3px; background: var(--icon-bg); }
.row .name { font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.row .host { color: var(--text-faint); font-size: 11px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.row .st { font-size: 10.5px; letter-spacing: 0.04em; }
.row .st.up { color: var(--up); }
.row .st.down { color: var(--down); }
/* ---------- url popover ---------- */
.urlpop {
position: absolute; top: calc(100% + 4px); left: 42px; z-index: 20;
min-width: 250px; padding: 4px;
background: var(--popover); border: 1px solid var(--border-strong);
border-radius: 6px;
box-shadow: 0 10px 34px rgba(0,0,0,0.4);
opacity: 0; transform: translateY(-3px); pointer-events: none;
transition: opacity 120ms var(--ease), transform 120ms var(--ease);
}
.row.open .urlpop { opacity: 1; transform: none; pointer-events: auto; }
.urlpop a {
display: flex; align-items: center; gap: 9px;
padding: 7px 9px; border-radius: 4px;
color: var(--text); text-decoration: none; font-size: 12px;
}
.urlpop a:hover { background: var(--sel); }
.urlpop .tag { color: var(--up); font-size: 10.5px; }
.urlpop .u { color: var(--text-faint); font-size: 11px; margin-left: auto; }
.foot { margin-top: 30px; font-size: 10.5px; color: var(--text-faint); text-align: center; }
@media (max-width: 860px) { .panels { grid-template-columns: 1fr 1fr; } .panel.wide { grid-column: 1 / -1; } }
@media (max-width: 620px) {
.topbar { padding: 0 14px; } .search { display: none; } .path { display: none; }
main { padding: 20px 14px 70px; }
.row { grid-template-columns: 18px 26px minmax(0,1fr) auto; }
.row .host { display: none; }
.panels { grid-template-columns: 1fr; }
}
@media (prefers-reduced-motion: reduce) { * { transition: none !important; animation: none !important; } }
:focus-visible { outline: 1px solid var(--text); outline-offset: 2px; }
</style>
</head>
<body>
<header class="topbar">
<div class="brand">dash<span class="cursor"></span></div>
<span class="path">~/<b>board</b> · 17 services · 5 widgets</span>
<div class="search">/<span style="color:var(--text-dim)">search</span></div>
<button class="tbtn" id="theme">[t] theme</button>
<button class="tbtn primary">[+] add</button>
</header>
<main>
<div class="banner">$ dash status --all
<b>17 services</b> indexed · <span class="ok">16 up</span> · <b>1 down</b> (radarr) · last sync 2s ago
type <b>/</b> to search · drag rows to reorder · click rows with multiple endpoints to choose</div>
<div class="panels">
<div class="panel wide">
<div class="phead">clock --local <span class="r">● live</span></div>
<div class="pbody">
<div class="clockv" id="clock">--:--:--</div>
<div class="tzrow"><span><b>PRG</b> <span id="tz1"></span></span><span><b>NYC</b> <span id="tz2"></span></span><span><b>TYO</b> <span id="tz3"></span></span></div>
</div>
</div>
<div class="panel">
<div class="phead"><img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/pi-hole.png" alt="" onerror="this.remove()">pihole</div>
<div class="pbody"><div class="v">24.6%</div><div class="s">ads blocked · 84,213 q</div></div>
</div>
<div class="panel">
<div class="phead"><img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/adguard-home.png" alt="" onerror="this.remove()">adguard</div>
<div class="pbody"><div class="v">11.2%</div><div class="s">blocked · 31 clients</div></div>
</div>
<div class="panel">
<div class="phead"><img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/immich.png" alt="" onerror="this.remove()">immich</div>
<div class="pbody"><div class="v">12,483</div><div class="s">photos · 96.4 GB</div></div>
</div>
<div class="panel">
<div class="phead"><img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/truenas.png" alt="" onerror="this.remove()">storage</div>
<div class="pbody"><div class="v">61%</div><div class="s">14.2 / 23.2 TB</div></div>
</div>
<div class="panel">
<div class="phead">uptime --30d</div>
<div class="pbody"><div class="v">99.7%</div><div class="s">1 incident · radarr</div></div>
</div>
<div class="panel">
<div class="phead"><img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/qbittorrent.png" alt="" onerror="this.remove()">transfer</div>
<div class="pbody"><div class="v">42 MB/s</div><div class="s">↓ 38.4 · ↑ 3.6</div></div>
</div>
</div>
<div class="sec">
<div class="sechead"><span class="hash">##</span><h2>network</h2><span class="n">[4]</span>
<span class="fill">────────────────────────────────────────────</span>
<svg class="chev" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m6 9 6 6 6-6"/></svg>
</div>
<div class="table">
<div class="row" draggable="true" data-urls='[{"tag":"local","u":"http://10.0.0.2/admin"},{"tag":"ext","u":"https://pihole.tdvorak.dev"}]'>
<span class="prompt">›</span>
<div class="icon" data-letter="P"><img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/pi-hole.png" alt="" onerror="this.remove()"></div>
<span class="name">pi-hole</span><span class="host">10.0.0.2 · 2 endpoints</span>
<span class="st up">[ up ]</span>
<div class="urlpop"></div>
</div>
<div class="row" draggable="true" data-urls='[{"tag":"local","u":"http://10.0.0.3:3000"}]'>
<span class="prompt">›</span>
<div class="icon" data-letter="A"><img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/adguard-home.png" alt="" onerror="this.remove()"></div>
<span class="name">adguard-home</span><span class="host">10.0.0.3:3000</span>
<span class="st up">[ up ]</span>
</div>
<div class="row" draggable="true" data-urls='[{"tag":"local","u":"http://10.0.0.2:81"},{"tag":"ext","u":"https://npm.tdvorak.dev"}]'>
<span class="prompt">›</span>
<div class="icon" data-letter="N"><img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/nginx-proxy-manager.png" alt="" onerror="this.remove()"></div>
<span class="name">nginx-proxy-mgr</span><span class="host">10.0.0.2:81 · 2 endpoints</span>
<span class="st up">[ up ]</span>
<div class="urlpop"></div>
</div>
<div class="row" draggable="true" data-urls='[{"tag":"local","u":"http://10.0.0.5:3001"},{"tag":"ext","u":"https://status.tdvorak.dev"}]'>
<span class="prompt">›</span>
<div class="icon" data-letter="U"><img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/uptime-kuma.png" alt="" onerror="this.remove()"></div>
<span class="name">uptime-kuma</span><span class="host">status.tdvorak.dev · 2 endpoints</span>
<span class="st up">[ up ]</span>
<div class="urlpop"></div>
</div>
</div>
</div>
<div class="sec">
<div class="sechead"><span class="hash">##</span><h2>media</h2><span class="n">[6]</span>
<span class="fill">────────────────────────────────────────────</span>
<svg class="chev" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m6 9 6 6 6-6"/></svg>
</div>
<div class="table">
<div class="row" draggable="true" data-urls='[{"tag":"local","u":"http://10.0.0.10:8096"},{"tag":"ext","u":"https://watch.tdvorak.dev"}]'>
<span class="prompt">›</span>
<div class="icon" data-letter="J"><img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/jellyfin.png" alt="" onerror="this.remove()"></div>
<span class="name">jellyfin</span><span class="host">watch.tdvorak.dev · 2 endpoints</span>
<span class="st up">[ up ]</span>
<div class="urlpop"></div>
</div>
<div class="row" draggable="true" data-urls='[{"tag":"local","u":"http://10.0.0.10:2283"},{"tag":"ext","u":"https://photos.tdvorak.dev"}]'>
<span class="prompt">›</span>
<div class="icon" data-letter="I"><img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/immich.png" alt="" onerror="this.remove()"></div>
<span class="name">immich</span><span class="host">photos.tdvorak.dev · 2 endpoints</span>
<span class="st up">[ up ]</span>
<div class="urlpop"></div>
</div>
<div class="row" draggable="true" data-urls='[{"tag":"local","u":"http://10.0.0.10:4533"}]'>
<span class="prompt">›</span>
<div class="icon" data-letter="N"><img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/navidrome.png" alt="" onerror="this.remove()"></div>
<span class="name">navidrome</span><span class="host">10.0.0.10:4533</span>
<span class="st up">[ up ]</span>
</div>
<div class="row" draggable="true" data-urls='[{"tag":"local","u":"http://10.0.0.10:8080"}]'>
<span class="prompt">›</span>
<div class="icon" data-letter="q"><img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/qbittorrent.png" alt="" onerror="this.remove()"></div>
<span class="name">qbittorrent</span><span class="host">10.0.0.10:8080</span>
<span class="st up">[ up ]</span>
</div>
<div class="row" draggable="true" data-urls='[{"tag":"local","u":"http://10.0.0.10:8989"}]'>
<span class="prompt">›</span>
<div class="icon" data-letter="S"><img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/sonarr.png" alt="" onerror="this.remove()"></div>
<span class="name">sonarr</span><span class="host">10.0.0.10:8989</span>
<span class="st up">[ up ]</span>
</div>
<div class="row" draggable="true" data-urls='[{"tag":"local","u":"http://10.0.0.10:7878"}]'>
<span class="prompt">›</span>
<div class="icon" data-letter="R"><img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/radarr.png" alt="" onerror="this.remove()"></div>
<span class="name">radarr</span><span class="host">10.0.0.10:7878</span>
<span class="st down">[ down ]</span>
</div>
</div>
</div>
<div class="sec">
<div class="sechead"><span class="hash">##</span><h2>infrastructure</h2><span class="n">[7]</span>
<span class="fill">────────────────────────────────────────────</span>
<svg class="chev" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m6 9 6 6 6-6"/></svg>
</div>
<div class="table">
<div class="row" draggable="true" data-urls='[{"tag":"local","u":"https://10.0.0.4:8006"}]'>
<span class="prompt">›</span>
<div class="icon" data-letter="P"><img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/proxmox.png" alt="" onerror="this.remove()"></div>
<span class="name">proxmox</span><span class="host">10.0.0.4:8006</span>
<span class="st up">[ up ]</span>
</div>
<div class="row" draggable="true" data-urls='[{"tag":"local","u":"http://10.0.0.4:9000"}]'>
<span class="prompt">›</span>
<div class="icon" data-letter="P"><img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/portainer.png" alt="" onerror="this.remove()"></div>
<span class="name">portainer</span><span class="host">10.0.0.4:9000</span>
<span class="st up">[ up ]</span>
</div>
<div class="row" draggable="true" data-urls='[{"tag":"local","u":"http://10.0.0.6"},{"tag":"ext","u":"https://nas.tdvorak.dev"}]'>
<span class="prompt">›</span>
<div class="icon" data-letter="T"><img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/truenas.png" alt="" onerror="this.remove()"></div>
<span class="name">truenas</span><span class="host">nas.tdvorak.dev · 2 endpoints</span>
<span class="st up">[ up ]</span>
<div class="urlpop"></div>
</div>
<div class="row" draggable="true" data-urls='[{"tag":"local","u":"http://10.0.0.4:3100"}]'>
<span class="prompt">›</span>
<div class="icon" data-letter="G"><img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/grafana.png" alt="" onerror="this.remove()"></div>
<span class="name">grafana</span><span class="host">10.0.0.4:3100</span>
<span class="st up">[ up ]</span>
</div>
<div class="row" draggable="true" data-urls='[{"tag":"ext","u":"https://vault.tdvorak.dev"}]'>
<span class="prompt">›</span>
<div class="icon" data-letter="V"><img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/vaultwarden.png" alt="" onerror="this.remove()"></div>
<span class="name">vaultwarden</span><span class="host">vault.tdvorak.dev</span>
<span class="st up">[ up ]</span>
</div>
<div class="row" draggable="true" data-urls='[{"tag":"local","u":"http://10.0.0.7:8123"},{"tag":"ext","u":"https://home.tdvorak.dev"}]'>
<span class="prompt">›</span>
<div class="icon" data-letter="H"><img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/home-assistant.png" alt="" onerror="this.remove()"></div>
<span class="name">home-assistant</span><span class="host">home.tdvorak.dev · 2 endpoints</span>
<span class="st up">[ up ]</span>
<div class="urlpop"></div>
</div>
<div class="row" draggable="true" data-urls='[{"tag":"ext","u":"https://cloud.tdvorak.dev"}]'>
<span class="prompt">›</span>
<div class="icon" data-letter="N"><img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/nextcloud.png" alt="" onerror="this.remove()"></div>
<span class="name">nextcloud</span><span class="host">cloud.tdvorak.dev</span>
<span class="st up">[ up ]</span>
</div>
</div>
</div>
<p class="foot">mockup F — console · [t] toggles theme · rows with “2 endpoints” open the chooser</p>
</main>
<script>
document.getElementById('theme').onclick = () => {
const h = document.documentElement;
h.dataset.theme = h.dataset.theme === 'dark' ? 'light' : 'dark';
};
const fmt = (tz, s) => new Intl.DateTimeFormat('en-GB', { hour: '2-digit', minute: '2-digit', second: s ? '2-digit' : undefined, timeZone: tz });
function tick() {
const n = new Date();
document.getElementById('clock').textContent = fmt(undefined, true).format(n);
document.getElementById('tz1').textContent = fmt('Europe/Prague').format(n);
document.getElementById('tz2').textContent = fmt('America/New_York').format(n);
document.getElementById('tz3').textContent = fmt('Asia/Tokyo').format(n);
}
tick(); setInterval(tick, 1000);
document.querySelectorAll('.sechead').forEach(h =>
h.addEventListener('click', () => h.parentElement.classList.toggle('closed')));
document.querySelectorAll('.row[data-urls]').forEach(card => {
const urls = JSON.parse(card.dataset.urls);
if (urls.length < 2) return;
const pop = card.querySelector('.urlpop');
pop.innerHTML = urls.map(x => `<a href="#" onclick="return false"><span class="tag">[${x.tag}]</span>${new URL(x.u).hostname}<span class="u">↗</span></a>`).join('');
card.addEventListener('click', e => {
if (card.classList.contains('dragging')) return;
document.querySelectorAll('.row.open').forEach(c => c !== card && c.classList.remove('open'));
card.classList.toggle('open');
e.stopPropagation();
});
});
document.addEventListener('click', () => document.querySelectorAll('.row.open').forEach(c => c.classList.remove('open')));
let dragEl = null;
document.querySelectorAll('.row').forEach(card => {
card.addEventListener('dragstart', () => { dragEl = card; setTimeout(() => card.classList.add('dragging')); });
card.addEventListener('dragend', () => card.classList.remove('dragging'));
card.addEventListener('dragover', e => {
e.preventDefault();
if (card === dragEl) return;
const r = card.getBoundingClientRect();
const before = e.clientY < r.top + r.height / 2;
card.parentElement.insertBefore(dragEl, before ? card : card.nextSibling);
});
});
document.querySelectorAll('.table').forEach(b => b.addEventListener('dragover', e => {
e.preventDefault();
if (dragEl && e.target === b) b.appendChild(dragEl);
}));
</script>
</body>
</html>