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;
|
--muted: #6b6b6b;
|
||||||
--rule: #000;
|
--rule: #000;
|
||||||
--soft: #f4f4f4;
|
--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; }
|
* { box-sizing: border-box; }
|
||||||
html, body {
|
html, body {
|
||||||
@@ -20,7 +49,7 @@
|
|||||||
font-size: 14px; line-height: 1.55;
|
font-size: 14px; line-height: 1.55;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
}
|
}
|
||||||
::selection { background: #000; color: #fff; }
|
::selection { background: var(--fg); color: var(--bg); }
|
||||||
main {
|
main {
|
||||||
max-width: 620px;
|
max-width: 620px;
|
||||||
margin: 72px auto 96px;
|
margin: 72px auto 96px;
|
||||||
@@ -76,7 +105,7 @@
|
|||||||
content: "";
|
content: "";
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 10px; height: 1px;
|
width: 10px; height: 1px;
|
||||||
background: #000;
|
background: var(--fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
label {
|
label {
|
||||||
@@ -101,10 +130,10 @@
|
|||||||
transition: background 0.08s, color 0.08s;
|
transition: background 0.08s, color 0.08s;
|
||||||
}
|
}
|
||||||
input[type=text]:focus {
|
input[type=text]:focus {
|
||||||
background: #000; color: #fff;
|
background: var(--fg); color: var(--bg);
|
||||||
}
|
}
|
||||||
input[type=text]::placeholder { color: var(--muted); }
|
input[type=text]::placeholder { color: var(--muted); }
|
||||||
input[type=text]:focus::placeholder { color: #888; }
|
input[type=text]:focus::placeholder { color: var(--focus-muted); }
|
||||||
|
|
||||||
button {
|
button {
|
||||||
display: block;
|
display: block;
|
||||||
@@ -122,8 +151,8 @@
|
|||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
transition: background 0.08s, color 0.08s;
|
transition: background 0.08s, color 0.08s;
|
||||||
}
|
}
|
||||||
button:hover:not(:disabled) { background: #000; color: #fff; }
|
button:hover:not(:disabled) { background: var(--fg); color: var(--bg); }
|
||||||
button:active:not(:disabled) { background: #222; }
|
button:active:not(:disabled) { background: var(--active); }
|
||||||
button:disabled {
|
button:disabled {
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
@@ -154,7 +183,7 @@
|
|||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
#log .line::before { content: "> "; color: var(--fg); }
|
#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: "! "; }
|
#log .line.err::before { content: "! "; }
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
@@ -168,7 +197,26 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
footer a { color: var(--muted); text-decoration: none; border-bottom: 1px dotted var(--muted); }
|
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 {
|
.diag {
|
||||||
border: 1px solid var(--rule);
|
border: 1px solid var(--rule);
|
||||||
@@ -187,11 +235,23 @@
|
|||||||
.diag .kv { display: grid; grid-template-columns: 140px 1fr; gap: 4px 12px; }
|
.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 .k { color: var(--muted); text-transform: uppercase; letter-spacing: 0.1em; font-size: 10px; }
|
||||||
.diag .v { word-break: break-all; }
|
.diag .v { word-break: break-all; }
|
||||||
.diag .v.ok { color: #000; }
|
.diag .v.ok { color: var(--fg); }
|
||||||
.diag .v.bad { color: #000; font-weight: 700; }
|
.diag .v.bad { color: var(--fg); font-weight: 700; }
|
||||||
.diag .v.bad::before { content: "✗ "; }
|
.diag .v.bad::before { content: "✗ "; }
|
||||||
.diag .v.ok::before { content: "✓ "; }
|
.diag .v.ok::before { content: "✓ "; }
|
||||||
</style>
|
</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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main>
|
<main>
|
||||||
@@ -199,6 +259,7 @@
|
|||||||
<div class="title">
|
<div class="title">
|
||||||
<h1>nostr-passkey</h1>
|
<h1>nostr-passkey</h1>
|
||||||
<span class="ver">v0.1 · prototype</span>
|
<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>
|
||||||
<div class="sub"><b>passkey</b> ⟶ <b>prf</b> ⟶ <b>hkdf</b> ⟶ <b>nostr key</b></div>
|
<div class="sub"><b>passkey</b> ⟶ <b>prf</b> ⟶ <b>hkdf</b> ⟶ <b>nostr key</b></div>
|
||||||
</header>
|
</header>
|
||||||
@@ -531,6 +592,18 @@ async function doDerive() {
|
|||||||
|
|
||||||
document.getElementById("btn-register").addEventListener("click", doRegister);
|
document.getElementById("btn-register").addEventListener("click", doRegister);
|
||||||
document.getElementById("btn-derive").addEventListener("click", doDerive);
|
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>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user