From c04cfb4179068144d47c0fe64fc08344dd605457 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Mon, 9 Nov 2026 19:23:56 +0100 Subject: feat: tablet-first dashboard tiles and match the Hyprland border speed --- static/deck.css | 83 ++++++++++++++++++++++++++++++++++++++------- static/modules/llm.js | 78 +++++++++++++++++++++++------------------- static/modules/sbbuilder.js | 51 ++++++++++++++-------------- static/modules/stats.js | 65 ++++++++++++++++------------------- static/modules/windows.js | 38 +++++++++++---------- 5 files changed, 188 insertions(+), 127 deletions(-) (limited to 'static') diff --git a/static/deck.css b/static/deck.css index e200507..f5c6824 100644 --- a/static/deck.css +++ b/static/deck.css @@ -56,7 +56,7 @@ body { overflow: hidden; border-radius: 16px; background: var(--udt-surface0); - border: 1px solid transparent; + border: 0; box-shadow: 0 10px 30px rgba(0, 0, 0, .18); } @@ -67,7 +67,7 @@ body { position: absolute; inset: 0; border-radius: inherit; - padding: 1.5px; + padding: 2px; background: linear-gradient(var(--border-angle), var(--udt-border-1), var(--udt-border-2), var(--udt-border-3), var(--udt-border-1)); -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); @@ -75,7 +75,8 @@ body { mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); mask-composite: exclude; pointer-events: none; - animation: border-rotate 8s linear infinite; + /* Hyprland: speed 30 * 100 ms, flatlinetwo = linear, style = loop */ + animation: border-rotate 3s linear infinite; } @keyframes border-rotate { @@ -130,9 +131,9 @@ body { .music .top { display: flex; gap: 12px; align-items: center; margin-bottom: 12px; } .music .art { - width: 64px; - height: 64px; - border-radius: 10px; + width: 72px; + height: 72px; + border-radius: 12px; object-fit: cover; background: var(--udt-surface1); flex: none; @@ -146,12 +147,13 @@ body { } .music .meta { min-width: 0; } .music .title { - font-size: 15px; + font-size: 17px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.music .controls button { min-height: 40px; min-width: 44px; } .music .artist, .music .album { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .music .album { font-size: 12px; color: var(--udt-overlay0); } .music .seek { display: flex; align-items: center; gap: 10px; margin: 6px 0 12px; } @@ -204,13 +206,68 @@ select { .sbb .id { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.stats .row, .windows .row { margin-bottom: 6px; } -.now b { display: block; font-size: 16px; } +.stats { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; } +.mtile { + display: flex; + flex-direction: column; + gap: 2px; + padding: 10px 12px; + border-radius: 12px; + background: var(--udt-surface1); + min-width: 0; +} +.mlabel { font-size: 10px; text-transform: uppercase; letter-spacing: .08em; color: var(--udt-overlay0); } +.mvalue { + font-size: 26px; + font-weight: 600; + line-height: 1.1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.msub { font-size: 12px; color: var(--udt-subtext0); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.mfoot { margin-top: 10px; font-size: 12px; color: var(--udt-overlay0); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + +.windows .ws { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 10px; } +.wtile { + min-width: 56px; + min-height: 56px; + padding: 0 10px; + display: flex; + align-items: center; + justify-content: center; + font-size: 18px; + font-weight: 600; + border-radius: 12px; + background: var(--udt-surface1); + border: 1px solid var(--udt-surface2); + color: var(--udt-subtext0); + cursor: pointer; +} +.wtile.on { background: var(--udt-accent); color: var(--udt-crust); border-color: var(--udt-accent); } +.wtile.launch { font-size: 13px; font-weight: 500; min-width: 0; } +.windows .winlist { display: flex; flex-direction: column; gap: 6px; margin-bottom: 10px; } +.wrow { + display: flex; + align-items: center; + gap: 8px; + min-height: 44px; + padding: 0 6px 0 10px; + border-radius: 10px; + background: var(--udt-surface1); +} +.wrow.on { color: var(--udt-accent); } .wt { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; cursor: pointer; } -.windows .row.on { color: var(--udt-accent); } - -.llm .meter { height: 6px; background: var(--udt-surface1); border-radius: 3px; overflow: hidden; margin: 2px 0 8px; } -.llm .meter > i { display: block; height: 100%; background: var(--udt-accent); } +.wclose { flex: none; width: 36px; height: 36px; padding: 0; } +.windows .launches { display: flex; flex-wrap: wrap; gap: 8px; } + +.gauges { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-bottom: 12px; } +.gauge { display: flex; flex-direction: column; gap: 4px; padding: 10px 12px; border-radius: 12px; background: var(--udt-surface1); } +.glabel { font-size: 10px; text-transform: uppercase; letter-spacing: .08em; color: var(--udt-overlay0); } +.gvalue { font-size: 24px; font-weight: 600; line-height: 1.1; } +.gbar { height: 6px; background: var(--udt-surface2); border-radius: 3px; overflow: hidden; } +.gbar > i { display: block; height: 100%; background: var(--udt-accent); } +.lbig { display: flex; flex-direction: column; gap: 2px; padding: 10px 12px; border-radius: 12px; background: var(--udt-surface1); } .good { color: var(--udt-green); } .bad { color: var(--udt-red); } diff --git a/static/modules/llm.js b/static/modules/llm.js index a1b775b..0af8d7b 100644 --- a/static/modules/llm.js +++ b/static/modules/llm.js @@ -1,6 +1,5 @@ const num = (v) => (v == null ? null : Number(v)); -const pct = (v) => (num(v) == null ? "—" : `${Math.round(num(v))}%`); -const money = (v, cur) => (num(v) == null ? "—" : `${num(v).toFixed(2)}${cur ? " " + cur : ""}`); +const money = (v) => (num(v) == null ? "—" : `$${num(v).toFixed(2)}`); const tokens = (n) => { if (n == null) return "—"; if (n >= 1e9) return `${(n / 1e9).toFixed(1)}B`; @@ -8,28 +7,39 @@ const tokens = (n) => { if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`; return String(n); }; -const clock = (sec) => (sec ? new Date(sec * 1000).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : ""); -function line(label, value, percent) { - const row = document.createElement("div"); - row.className = "row"; - const name = document.createElement("span"); - name.textContent = label; - const val = document.createElement("span"); - val.className = "num"; - val.textContent = value; - row.append(name, val); - const wrap = document.createElement("div"); - wrap.append(row); - if (percent != null) { - const meter = document.createElement("div"); - meter.className = "meter"; - const fill = document.createElement("i"); - fill.style.width = `${Math.max(0, Math.min(100, Number(percent)))}%`; - meter.append(fill); - wrap.append(meter); - } - return wrap; +function gauge(label, value) { + const d = document.createElement("div"); + d.className = "gauge"; + const l = document.createElement("span"); + l.className = "glabel"; + l.textContent = label; + const v = document.createElement("span"); + v.className = "gvalue num"; + v.textContent = value == null ? "—" : `${Math.round(value)}%`; + const bar = document.createElement("div"); + bar.className = "gbar"; + const fill = document.createElement("i"); + fill.style.width = `${Math.max(0, Math.min(100, value || 0))}%`; + bar.append(fill); + d.append(l, v, bar); + return d; +} + +function big(label, value, sub) { + const d = document.createElement("div"); + d.className = "lbig"; + const l = document.createElement("span"); + l.className = "mlabel"; + l.textContent = label; + const v = document.createElement("span"); + v.className = "mvalue num"; + v.textContent = value; + const s = document.createElement("span"); + s.className = "msub"; + s.textContent = sub || ""; + d.append(l, v, s); + return d; } export default { @@ -37,9 +47,7 @@ export default { interval: 5, mount(el) { - const box = document.createElement("div"); - box.className = "llm"; - el.append(box); + el.innerHTML = `
`; }, update(state, el) { @@ -51,16 +59,16 @@ export default { } const c = state.claude ?? {}; const o = state.opencode ?? {}; + const gauges = document.createElement("div"); + gauges.className = "gauges"; + gauges.append( + gauge("Context", c.context_pct), + gauge("5h", c.five_hour_pct), + gauge("7d", c.seven_day_pct), + ); + box.append(gauges); box.append( - line("Context", pct(c.context_pct), c.context_pct), - line(`5h window ${clock(c.five_hour_resets_at)}`, pct(c.five_hour_pct), c.five_hour_pct), - line(`7d window ${clock(c.seven_day_resets_at)}`, pct(c.seven_day_pct), c.seven_day_pct), - line("Month", money(c.extra_used, c.extra_currency)), - line("opencode spend", money(o.cost)), - line("opencode tokens", `${tokens(o.tokens)} · ${o.calls ?? 0} calls`), + big("opencode spend", money(o.cost), `${tokens(o.tokens)} · ${o.calls ?? 0} calls`), ); - for (const m of (o.models ?? []).slice(0, 4)) { - box.append(line(m.model || m.provider, money(m.cost))); - } }, }; diff --git a/static/modules/sbbuilder.js b/static/modules/sbbuilder.js index 02493af..751ffdd 100644 --- a/static/modules/sbbuilder.js +++ b/static/modules/sbbuilder.js @@ -1,11 +1,5 @@ const gib = (b) => (b == null ? "?" : (b / 1073741824).toFixed(1)); const pct = (n) => (n == null ? "?" : `${Math.round(n)}%`); -const up = (s) => { - if (s == null) return "?"; - const d = Math.floor(s / 86400); - const h = Math.floor((s % 86400) / 3600); - return d ? `${d}d ${h}h` : `${h}h`; -}; const ago = (ts) => { if (!ts) return "—"; const d = Math.max(0, Math.floor(Date.now() / 1000 - ts)); @@ -16,17 +10,20 @@ const ago = (ts) => { const ACTIVE = ["running", "busy", "starting"]; -function metric(label, value) { - const row = document.createElement("div"); - row.className = "metric"; - const k = document.createElement("span"); - k.className = "k"; - k.textContent = label; +function tile(label, value, sub) { + const d = document.createElement("div"); + d.className = "mtile"; + const l = document.createElement("span"); + l.className = "mlabel"; + l.textContent = label; const v = document.createElement("span"); - v.className = "v num"; + v.className = "mvalue num"; v.textContent = value; - row.append(k, v); - return row; + const s = document.createElement("span"); + s.className = "msub"; + s.textContent = sub || ""; + d.append(l, v, s); + return d; } export default { @@ -47,26 +44,28 @@ export default { const s = state.stats ?? {}; const p = s.packages ?? {}; const sr = s.slackrepo ?? {}; + const results = sr.results ?? {}; const jobs = state.jobs ?? []; - const running = jobs.find((j) => ACTIVE.includes(j.status)); + const now = document.createElement("div"); now.className = "sbb-now"; - now.textContent = running ? `● ${running.kind || "job"} running` : "idle"; + now.textContent = running ? `● ${running.kind || "job"}` : "idle"; now.classList.toggle("warn", Boolean(running)); box.append(now); const overlays = Object.entries(p).filter(([k]) => k !== "total" && k !== "shadowed"); - const results = Object.entries(sr.results ?? {}); box.append( - metric("CPU · load", `${pct(s.cpu)} · ${s.load ? s.load[0].toFixed(2) : "?"}`), - metric("Uptime", up(s.uptime)), - metric("Memory", `${gib((s.mem?.total ?? 0) - (s.mem?.available ?? 0))} / ${gib(s.mem?.total)} GiB`), - metric("Disk free", `${gib(s.disk?.free)} / ${gib(s.disk?.total)} GiB`), - metric("Packages", `${p.total ?? "?"}${overlays.length ? " · " + overlays.map(([k, v]) => `${k} ${v}`).join(" · ") : ""}`), - metric("Shadowed · orphans", `${p.shadowed ?? "?"} · ${s.orphans ?? "?"}`), - metric("Built · results", `${sr.built ?? "?"}${results.length ? " · " + results.map(([k, v]) => `${v} ${k}`).join(" · ") : ""}`), - metric("Overlay pulls", Object.entries(s.sync ?? {}).map(([k, v]) => `${k} ${ago(v)}`).join(" · ") || "—"), + tile("Packages", String(p.total ?? "?"), overlays.map(([k, v]) => `${k} ${v}`).join(" · ")), + tile("Failed", String(results.failed ?? 0), `${sr.built ?? "?"} built`), + tile("Disk free", `${gib(s.disk?.free)}`, `of ${gib(s.disk?.total)} GiB`), + tile("CPU", pct(s.cpu), `load ${s.load ? s.load[0].toFixed(2) : "?"}`), ); + + const foot = document.createElement("div"); + foot.className = "mfoot"; + foot.textContent = `orphans ${s.orphans ?? "?"} · ${ + Object.entries(s.sync ?? {}).map(([k, v]) => `${k} ${ago(v)}`).join(" · ") || "no pulls"}`; + box.append(foot); }, }; diff --git a/static/modules/stats.js b/static/modules/stats.js index a119c6c..16dffd2 100644 --- a/static/modules/stats.js +++ b/static/modules/stats.js @@ -1,36 +1,26 @@ -const gb = (kb) => (kb / 1048576).toFixed(1); -const rate = (b) => (b >= 1048576 ? `${(b / 1048576).toFixed(1)} MB/s` : `${Math.round(b / 1024)} KB/s`); +const gib = (kb) => (kb / 1048576).toFixed(1); +const bytes = (b) => (b == null ? "?" : (b / 1073741824).toFixed(1)); +const rate = (b) => (b >= 1048576 ? `${(b / 1048576).toFixed(1)}M` : `${Math.round(b / 1024)}k`); const uptime = (s) => { const d = Math.floor(s / 86400); const h = Math.floor((s % 86400) / 3600); - const m = Math.floor((s % 3600) / 60); - return d ? `${d}d ${h}h` : `${h}h ${m}m`; -}; -const gib = (b) => (b / 1073741824).toFixed(1); -const gpu = (g) => { - if (!g) return "—"; - if (g.asleep) return `${g.name} (asleep)`; - const parts = [g.name]; - if (g.util != null) parts.push(`${g.util}%`); - if (g.temp != null) parts.push(`${g.temp}°C`); - if (g.fan != null) parts.push(`${g.fan} rpm`); - if (g.memUsed != null) { - parts.push(g.memTotal ? `${gib(g.memUsed)} / ${gib(g.memTotal)} GiB` : `${gib(g.memUsed)} GiB`); - } - return parts.join(" · "); + return d ? `${d}d ${h}h` : `${h}h`; }; -function row(label, value) { - const div = document.createElement("div"); - div.className = "row"; - const name = document.createElement("span"); - name.className = "muted"; - name.textContent = label; - const val = document.createElement("span"); - val.className = "num"; - val.textContent = value; - div.append(name, val); - return div; +function tile(label, value, sub) { + const d = document.createElement("div"); + d.className = "mtile"; + const l = document.createElement("span"); + l.className = "mlabel"; + l.textContent = label; + const v = document.createElement("span"); + v.className = "mvalue num"; + v.textContent = value; + const s = document.createElement("span"); + s.className = "msub"; + s.textContent = sub || ""; + d.append(l, v, s); + return d; } export default { @@ -38,20 +28,23 @@ export default { interval: 1, mount(el) { - const box = document.createElement("div"); - box.className = "stats"; - el.append(box); + el.innerHTML = `
`; }, update(st, el) { const box = el.querySelector(".stats"); box.textContent = ""; + const g = st.gpu; box.append( - row("CPU", `${st.cpu}%${st.cpuTemp != null ? ` · ${st.cpuTemp}°C` : ""}`), - row("Memory", `${gb(st.memUsed)} / ${gb(st.memTotal)} GiB`), - row("GPU", gpu(st.gpu)), - row("Network", `↓ ${rate(st.net.down)} ↑ ${rate(st.net.up)}`), - row("Uptime", uptime(st.uptime)), + tile("CPU", `${st.cpu}%`, st.cpuTemp != null ? `${st.cpuTemp}°C` : "—"), + tile("Memory", gib(st.memUsed), `/ ${gib(st.memTotal)} GiB`), + tile("GPU", g?.temp != null ? `${g.temp}°` : "—", + g ? `${bytes(g.memUsed)} / ${bytes(g.memTotal)} GiB` : "no GPU"), + tile("Network", `↓${rate(st.net.down)}`, `↑${rate(st.net.up)}`), ); + const foot = document.createElement("div"); + foot.className = "mfoot"; + foot.textContent = `up ${uptime(st.uptime)}`; + box.append(foot); }, }; diff --git a/static/modules/windows.js b/static/modules/windows.js index 7a2eeb8..a92d9e4 100644 --- a/static/modules/windows.js +++ b/static/modules/windows.js @@ -10,8 +10,9 @@ async function post(action, body = {}) { } } -function button(label, onclick) { +function tile(label, onclick) { const b = document.createElement("button"); + b.className = "wtile"; b.textContent = label; b.onclick = onclick; return b; @@ -22,44 +23,47 @@ export default { interval: 1, mount(el) { - const box = document.createElement("div"); - box.className = "windows"; - box.innerHTML = ` -
+ el.innerHTML = `
+
-
`; - el.append(box); +
+
`; }, update(st, el) { - const box = el.querySelector(".windows"); - - const ws = box.querySelector(".ws"); + const ws = el.querySelector(".ws"); ws.textContent = ""; for (const id of st.workspaces ?? []) { - const b = button(String(id), () => post("workspace", { n: id })); + const b = tile(String(id), () => post("workspace", { n: id })); b.classList.toggle("on", (st.windows ?? []).some((w) => w.workspace === id)); ws.append(b); } - const list = box.querySelector(".winlist"); + const list = el.querySelector(".winlist"); list.textContent = ""; - for (const w of (st.windows ?? []).slice(0, 12)) { + for (const w of (st.windows ?? []).slice(0, 8)) { const row = document.createElement("div"); - row.className = "row"; + row.className = "wrow"; row.classList.toggle("on", Boolean(w.active)); const title = document.createElement("span"); title.className = "wt"; title.textContent = w.title || w.class; title.onclick = () => post("focus", { address: w.address }); - row.append(title, button("×", () => post("close", { address: w.address }))); + const close = document.createElement("button"); + close.className = "wclose"; + close.textContent = "×"; + close.setAttribute("aria-label", "Close window"); + close.onclick = () => post("close", { address: w.address }); + row.append(title, close); list.append(row); } - const launches = box.querySelector(".launches"); + const launches = el.querySelector(".launches"); launches.textContent = ""; for (const key of st.launches ?? []) { - launches.append(button(key, () => post("launch", { key }))); + const b = tile(key, () => post("launch", { key })); + b.classList.add("launch"); + launches.append(b); } }, }; -- cgit v1.2.3