aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-11-09 19:23:56 +0100
committerDanilo M. <danix@danix.xyz>2026-11-09 19:23:56 +0100
commitc04cfb4179068144d47c0fe64fc08344dd605457 (patch)
treefbc7ac0ac7710329f58a2230a2dd79ffc52b602c
parent04bd03db72c7165debb79d1858f39121757ca358 (diff)
downloaddeck-it-c04cfb4179068144d47c0fe64fc08344dd605457.tar.gz
deck-it-c04cfb4179068144d47c0fe64fc08344dd605457.zip
feat: tablet-first dashboard tiles and match the Hyprland border speed
-rw-r--r--static/deck.css83
-rw-r--r--static/modules/llm.js78
-rw-r--r--static/modules/sbbuilder.js51
-rw-r--r--static/modules/stats.js65
-rw-r--r--static/modules/windows.js38
5 files changed, 188 insertions, 127 deletions
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 = `<div class="llm"></div>`;
},
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 = `<div class="stats"></div>`;
},
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 = `
- <div class="row ws"></div>
+ el.innerHTML = `<div class="windows">
+ <div class="ws"></div>
<div class="winlist"></div>
- <div class="row launches"></div>`;
- el.append(box);
+ <div class="launches"></div>
+ </div>`;
},
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);
}
},
};