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
+2 -2
View File
@@ -985,8 +985,8 @@ export default function App() {
<ErrorDialog />
{/* Re-binds to the active page's scroll container on navigation (page or channel change). */}
<BackToTop dep={channelView ? `chan:${channelView.id}` : page} />
{/* Local-only live appearance tuner (renders only on localhost; null everywhere else). */}
<GlassTuner />
{/* Opt-in live appearance tuner (Settings → Appearance; off by default). */}
<GlassTuner enabled={theme.showTuner} />
</div>
);
}
+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")}>
@@ -42,6 +42,8 @@
"performanceModeHint": "Turns off the translucent glass blur and soft shadows for snappier rendering on slower machines.",
"showHints": "Show hints",
"showHintsHint": "These little hover explanations. Turn them off once you know your way around.",
"showTuner": "Show the glass tuner",
"showTunerHint": "A floating panel for live-tweaking the frosted-glass look. A geek toy — off by default.",
"textSize": "Text size"
},
"notifications": {
@@ -42,6 +42,8 @@
"performanceModeHint": "Kikapcsolja az áttetsző üvegelmosást és a lágy árnyékokat a gyorsabb megjelenítésért lassabb gépeken.",
"showHints": "Tippek megjelenítése",
"showHintsHint": "Ezek a kis lebegő magyarázatok. Kapcsold ki őket, ha már kiismerted magad.",
"showTuner": "Glass tuner megjelenítése",
"showTunerHint": "Lebegő panel a mattüveg-hatás élő hangolásához. Geek-játékszer — alapból kikapcsolva.",
"textSize": "Betűméret"
},
"notifications": {
+3
View File
@@ -20,6 +20,8 @@ export interface ThemePrefs {
bgImage: boolean;
/** How strongly the background image is faded out, 0100% (higher = fainter). */
bgFade: number;
/** Show the floating glass/theme tuner (a secondary "geek toy"); off by default. */
showTuner: boolean;
}
export const DEFAULT_THEME: ThemePrefs = {
@@ -28,6 +30,7 @@ export const DEFAULT_THEME: ThemePrefs = {
fontScale: 1.06,
bgImage: true,
bgFade: 60,
showTuner: false,
};
export function applyTheme(t: ThemePrefs): void {