Files
vibn-agent-runner/vibn-frontend/app/(marketing)/AccentSwitcher.tsx

238 lines
6.5 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
/**
* Dev-only accent color switcher for the marketing site.
*
* Flips the homepage accent live by overriding the `--accent-h` (hue) and
* `--accent-cm` (chroma) CSS variables on `.new-site-wrapper`. Choice persists
* in localStorage so it survives reloads while you experiment.
*
* Visibility: shows only in development, or in any environment when the URL has
* `?theme`. So it's hidden in production by default — nothing to do to hide it.
* To remove entirely later, delete this file and its <AccentSwitcher /> usage
* in app/(marketing)/page.tsx.
*/
const PRESETS: { label: string; h: number; cm: number }[] = [
{ label: "Coral", h: 35, cm: 1 },
{ label: "Silver", h: 35, cm: 0 },
{ label: "Crimson", h: 25, cm: 1 },
{ label: "Amber", h: 75, cm: 1 },
{ label: "Emerald", h: 150, cm: 1 },
{ label: "Teal", h: 195, cm: 1 },
{ label: "Sky", h: 235, cm: 1 },
{ label: "Royal", h: 255, cm: 1 },
{ label: "Indigo", h: 270, cm: 1 },
{ label: "Violet", h: 300, cm: 1 },
{ label: "Pink", h: 350, cm: 1 },
];
const swatchColor = (h: number, cm: number) =>
`oklch(0.74 ${(0.175 * cm).toFixed(3)} ${h})`;
export default function AccentSwitcher() {
const [visible, setVisible] = useState(false);
const [open, setOpen] = useState(true);
const [h, setH] = useState(35);
const [cm, setCm] = useState(1);
useEffect(() => {
const isDev = process.env.NODE_ENV !== "production";
const forced =
typeof window !== "undefined" &&
new URLSearchParams(window.location.search).has("theme");
setVisible(isDev || forced);
try {
const saved = JSON.parse(localStorage.getItem("vibn:accent") || "null");
if (saved && typeof saved.h === "number") {
setH(saved.h);
setCm(typeof saved.cm === "number" ? saved.cm : 1);
}
} catch {
/* ignore */
}
}, []);
useEffect(() => {
if (!visible) return;
const el = document.querySelector(".new-site-wrapper") as HTMLElement | null;
if (el) {
el.style.setProperty("--accent-h", String(h));
el.style.setProperty("--accent-cm", String(cm));
}
try {
localStorage.setItem("vibn:accent", JSON.stringify({ h, cm }));
} catch {
/* ignore */
}
}, [visible, h, cm]);
if (!visible) return null;
const panel: React.CSSProperties = {
position: "fixed",
left: 16,
bottom: 16,
zIndex: 2147483647,
width: 232,
padding: 14,
borderRadius: 14,
background: "rgba(18,17,15,0.92)",
border: "1px solid rgba(255,255,255,0.12)",
backdropFilter: "blur(16px)",
boxShadow: "0 18px 50px rgba(0,0,0,0.5)",
color: "#f5f4f2",
fontFamily: "ui-monospace, 'SF Mono', Menlo, monospace",
fontSize: 11,
userSelect: "none",
};
if (!open) {
return (
<button
type="button"
onClick={() => setOpen(true)}
title="Theme"
style={{
position: "fixed",
left: 16,
bottom: 16,
zIndex: 2147483647,
width: 38,
height: 38,
borderRadius: "50%",
border: "1px solid rgba(255,255,255,0.18)",
background: swatchColor(h, cm),
boxShadow: "0 8px 24px rgba(0,0,0,0.45)",
cursor: "pointer",
}}
/>
);
}
return (
<div style={panel}>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
marginBottom: 10,
}}
>
<span style={{ letterSpacing: "0.1em", textTransform: "uppercase", opacity: 0.6 }}>
Accent · dev
</span>
<button
type="button"
onClick={() => setOpen(false)}
title="Collapse"
style={{
border: "none",
background: "transparent",
color: "#f5f4f2",
cursor: "pointer",
fontSize: 14,
lineHeight: 1,
opacity: 0.7,
}}
>
</button>
</div>
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(6, 1fr)",
gap: 6,
marginBottom: 12,
}}
>
{PRESETS.map((p) => {
const active = p.h === h && p.cm === cm;
return (
<button
key={p.label}
type="button"
title={p.label}
onClick={() => {
setH(p.h);
setCm(p.cm);
}}
style={{
width: "100%",
aspectRatio: "1 / 1",
borderRadius: 7,
border: active
? "2px solid #fff"
: "1px solid rgba(255,255,255,0.15)",
background: swatchColor(p.h, p.cm),
cursor: "pointer",
padding: 0,
}}
/>
);
})}
</div>
<label style={{ display: "block", marginBottom: 8 }}>
<span style={{ display: "flex", justifyContent: "space-between", opacity: 0.7 }}>
<span>Hue</span>
<span>{h}</span>
</span>
<input
type="range"
min={0}
max={360}
value={h}
onChange={(e) => setH(Number(e.target.value))}
style={{ width: "100%", accentColor: swatchColor(h, cm) }}
/>
</label>
<label style={{ display: "block", marginBottom: 10 }}>
<span style={{ display: "flex", justifyContent: "space-between", opacity: 0.7 }}>
<span>Vivid</span>
<span>{cm.toFixed(2)}</span>
</span>
<input
type="range"
min={0}
max={1}
step={0.05}
value={cm}
onChange={(e) => setCm(Number(e.target.value))}
style={{ width: "100%", accentColor: swatchColor(h, cm) }}
/>
</label>
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
<code style={{ opacity: 0.55, fontSize: 10 }}>
h:{h} cm:{cm}
</code>
<button
type="button"
onClick={() => {
setH(35);
setCm(1);
}}
style={{
border: "1px solid rgba(255,255,255,0.18)",
background: "transparent",
color: "#f5f4f2",
borderRadius: 6,
padding: "4px 8px",
cursor: "pointer",
fontSize: 10,
}}
>
Reset
</button>
</div>
</div>
);
}