Add dark theme toggle to web UI
CSS variables drive the palette; a ◐ button in the header flips between light and dark, with localStorage persistence and a no-flash inline init script. Respects prefers-color-scheme by default. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,35 @@
|
||||
--muted: #6b6b6b;
|
||||
--rule: #000;
|
||||
--soft: #f4f4f4;
|
||||
--active: #222; /* pressed-button background */
|
||||
--focus-muted: #888; /* placeholder on inverted input */
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
/* Auto dark mode when the user hasn't explicitly chosen light. */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme="light"]) {
|
||||
--fg: #fff;
|
||||
--bg: #000;
|
||||
--muted: #8a8a8a;
|
||||
--rule: #fff;
|
||||
--soft: #0f0f0f;
|
||||
--active: #d8d8d8;
|
||||
--focus-muted: #777;
|
||||
color-scheme: dark;
|
||||
}
|
||||
}
|
||||
|
||||
/* Explicit opt-in via the header toggle wins over OS preference. */
|
||||
:root[data-theme="dark"] {
|
||||
--fg: #fff;
|
||||
--bg: #000;
|
||||
--muted: #8a8a8a;
|
||||
--rule: #fff;
|
||||
--soft: #0f0f0f;
|
||||
--active: #d8d8d8;
|
||||
--focus-muted: #777;
|
||||
color-scheme: dark;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
html, body {
|
||||
@@ -20,7 +49,7 @@
|
||||
font-size: 14px; line-height: 1.55;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
::selection { background: #000; color: #fff; }
|
||||
::selection { background: var(--fg); color: var(--bg); }
|
||||
main {
|
||||
max-width: 620px;
|
||||
margin: 72px auto 96px;
|
||||
@@ -76,7 +105,7 @@
|
||||
content: "";
|
||||
display: inline-block;
|
||||
width: 10px; height: 1px;
|
||||
background: #000;
|
||||
background: var(--fg);
|
||||
}
|
||||
|
||||
label {
|
||||
@@ -101,10 +130,10 @@
|
||||
transition: background 0.08s, color 0.08s;
|
||||
}
|
||||
input[type=text]:focus {
|
||||
background: #000; color: #fff;
|
||||
background: var(--fg); color: var(--bg);
|
||||
}
|
||||
input[type=text]::placeholder { color: var(--muted); }
|
||||
input[type=text]:focus::placeholder { color: #888; }
|
||||
input[type=text]:focus::placeholder { color: var(--focus-muted); }
|
||||
|
||||
button {
|
||||
display: block;
|
||||
@@ -122,8 +151,8 @@
|
||||
border-radius: 0;
|
||||
transition: background 0.08s, color 0.08s;
|
||||
}
|
||||
button:hover:not(:disabled) { background: #000; color: #fff; }
|
||||
button:active:not(:disabled) { background: #222; }
|
||||
button:hover:not(:disabled) { background: var(--fg); color: var(--bg); }
|
||||
button:active:not(:disabled) { background: var(--active); }
|
||||
button:disabled {
|
||||
color: var(--muted);
|
||||
cursor: not-allowed;
|
||||
@@ -154,7 +183,7 @@
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
#log .line::before { content: "> "; color: var(--fg); }
|
||||
#log .line.err { color: #000; font-weight: 600; }
|
||||
#log .line.err { color: var(--fg); font-weight: 600; }
|
||||
#log .line.err::before { content: "! "; }
|
||||
|
||||
footer {
|
||||
@@ -168,7 +197,26 @@
|
||||
justify-content: space-between;
|
||||
}
|
||||
footer a { color: var(--muted); text-decoration: none; border-bottom: 1px dotted var(--muted); }
|
||||
footer a:hover { color: #000; border-bottom-color: #000; }
|
||||
footer a:hover { color: var(--fg); border-bottom-color: var(--fg); }
|
||||
|
||||
/* Theme toggle — overrides the global button rule via class specificity. */
|
||||
.theme-toggle {
|
||||
margin-left: auto;
|
||||
width: auto;
|
||||
padding: 3px 9px;
|
||||
font-size: 13px;
|
||||
line-height: 1;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0;
|
||||
text-transform: none;
|
||||
border: 1px solid var(--rule);
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
cursor: pointer;
|
||||
transition: background 0.08s, color 0.08s;
|
||||
}
|
||||
.theme-toggle:hover { background: var(--fg); color: var(--bg); }
|
||||
.theme-toggle:focus-visible { outline: 1px solid var(--fg); outline-offset: 2px; }
|
||||
|
||||
.diag {
|
||||
border: 1px solid var(--rule);
|
||||
@@ -187,11 +235,23 @@
|
||||
.diag .kv { display: grid; grid-template-columns: 140px 1fr; gap: 4px 12px; }
|
||||
.diag .k { color: var(--muted); text-transform: uppercase; letter-spacing: 0.1em; font-size: 10px; }
|
||||
.diag .v { word-break: break-all; }
|
||||
.diag .v.ok { color: #000; }
|
||||
.diag .v.bad { color: #000; font-weight: 700; }
|
||||
.diag .v.ok { color: var(--fg); }
|
||||
.diag .v.bad { color: var(--fg); font-weight: 700; }
|
||||
.diag .v.bad::before { content: "✗ "; }
|
||||
.diag .v.ok::before { content: "✓ "; }
|
||||
</style>
|
||||
<script>
|
||||
// No-flash theme init: runs synchronously before first paint so a
|
||||
// user who last chose "dark" doesn't see a white flash on load.
|
||||
(function () {
|
||||
try {
|
||||
var t = localStorage.getItem("nostr-passkey:theme");
|
||||
if (t === "dark" || t === "light") {
|
||||
document.documentElement.setAttribute("data-theme", t);
|
||||
}
|
||||
} catch (_) { /* localStorage blocked — fall back to OS preference */ }
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
@@ -199,6 +259,7 @@
|
||||
<div class="title">
|
||||
<h1>nostr-passkey</h1>
|
||||
<span class="ver">v0.1 · prototype</span>
|
||||
<button id="btn-theme" class="theme-toggle" type="button" aria-label="Toggle theme" title="Toggle theme">◐</button>
|
||||
</div>
|
||||
<div class="sub"><b>passkey</b> ⟶ <b>prf</b> ⟶ <b>hkdf</b> ⟶ <b>nostr key</b></div>
|
||||
</header>
|
||||
@@ -531,6 +592,18 @@ async function doDerive() {
|
||||
|
||||
document.getElementById("btn-register").addEventListener("click", doRegister);
|
||||
document.getElementById("btn-derive").addEventListener("click", doDerive);
|
||||
|
||||
// ---------- theme toggle ----------
|
||||
function currentTheme() {
|
||||
return document.documentElement.getAttribute("data-theme")
|
||||
|| (window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");
|
||||
}
|
||||
function toggleTheme() {
|
||||
const next = currentTheme() === "dark" ? "light" : "dark";
|
||||
document.documentElement.setAttribute("data-theme", next);
|
||||
try { localStorage.setItem("nostr-passkey:theme", next); } catch (_) {}
|
||||
}
|
||||
document.getElementById("btn-theme").addEventListener("click", toggleTheme);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user