feat(appearance): opt-in glass tuner toggle in Settings

Graduate the floating glass tuner from a localhost-only dev tool to a
user-facing opt-in: it's now gated on a per-user `theme.showTuner` pref (off by
default) with a "Show the glass tuner" switch in Settings → Appearance, riding
the existing theme draft-save. Softened the dev-only framing. Behaviour and look
are unchanged; the full preset system is a separate epic.
This commit is contained in:
2026-07-14 21:39:53 +02:00
parent ad1cc4586a
commit 6a9bf948f9
6 changed files with 26 additions and 12 deletions
+7 -10
View File
@@ -1,16 +1,13 @@
import { useEffect, useMemo, useState } from "react";
/**
* Local-only live appearance tuner. Mounted at the app root so it's reachable on every page; it
* A floating live appearance tuner. Mounted at the app root so it's reachable on every page; it
* writes the glass CSS custom properties (index.css `:root`) inline on the document element for
* instant, build-free feedback, and persists to localStorage.
*
* Only active on localhost, so it never appears in a hosted build. A scratch tool for dialling in
* the surface values — "Copy CSS" exports them to paste into index.css.
* Opt-in via Settings → Appearance (`theme.showTuner`, off by default) — a secondary "geek toy"
* for dialling in the surface values. "Copy CSS" exports the values.
*/
const DEV_TUNER =
typeof window !== "undefined" && /^(localhost|127\.0\.0\.1)$/.test(window.location.hostname);
const LS_KEY = "siftlode.devGlassTuner";
type Slider = {
@@ -93,7 +90,7 @@ function loadSaved(): Saved {
}
}
export default function GlassTuner() {
export default function GlassTuner({ enabled }: { enabled: boolean }) {
const initial = useMemo(loadSaved, []);
const [vars, setVars] = useState<Record<string, number>>(() => {
const base: Record<string, number> = {};
@@ -216,7 +213,7 @@ export default function GlassTuner() {
persist({ open: v });
}
if (!DEV_TUNER) return null;
if (!enabled) return null;
return (
<div className="fixed right-0 top-24 z-[9999] flex items-start" style={{ pointerEvents: "none" }}>
@@ -225,7 +222,7 @@ export default function GlassTuner() {
onClick={() => persistOpen(true)}
style={{ pointerEvents: "auto" }}
className="glass-menu glass-hover rounded-l-xl rounded-r-none px-2 py-3 text-xs font-semibold tracking-wide text-fg"
title="Open the glass tuner (dev only)"
title="Open the glass tuner"
>
<span style={{ writingMode: "vertical-rl" }}>◐ GLASS</span>
</button>
@@ -237,7 +234,7 @@ export default function GlassTuner() {
>
<div className="flex items-center gap-2 px-3 py-2 border-b border-border/60">
<span className="text-sm font-semibold text-fg">◐ Glass Tuner</span>
<span className="text-[10px] uppercase tracking-wider text-muted">dev</span>
<span className="text-[10px] uppercase tracking-wider text-muted">lab</span>
<button
onClick={() => persistOpen(false)}
className="ml-auto text-muted hover:text-fg text-lg leading-none px-1"
+10
View File
@@ -205,6 +205,16 @@ function Appearance({ prefs }: { prefs: PrefsController }) {
>
<Switch label={t("settings.appearance.showHints")} checked={hints} onChange={setHints} />
</SettingRow>
<SettingRow
label={t("settings.appearance.showTuner")}
hint={t("settings.appearance.showTunerHint")}
>
<Switch
label={t("settings.appearance.showTuner")}
checked={theme.showTuner}
onChange={(v) => setTheme({ ...theme, showTuner: v })}
/>
</SettingRow>
</Section>
<Section title={t("settings.appearance.textSize")}>