Files
2026-07-17 21:48:37 +05:30

126 lines
6.0 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import { useTheme } from "@lynkd/messaging-inbox-sdk";
import { useUi } from "@/lib/ui-state";
import { X, RotateCcw, Sun, Moon, Monitor, Check } from "lucide-react";
import clsx from "clsx";
const ACCENTS = ["#FDA913", "#5b9dff", "#3ecf8e", "#bb98ff", "#f2555a", "#ff7a3d", "#54e7ff", "#f5b544"];
const RADII = [
{ label: "Sharp", card: "8px", control: "6px" },
{ label: "Default", card: "20px", control: "10px" },
{ label: "Round", card: "28px", control: "14px" },
];
const GRADIENTS = [
"linear-gradient(135deg, #FDA913 0%, #ff7a3d 55%, #ff4d6d 100%)",
"linear-gradient(135deg, #5b9dff 0%, #bb98ff 100%)",
"linear-gradient(135deg, #3ecf8e 0%, #54e7ff 100%)",
"linear-gradient(135deg, #f2555a 0%, #ff7a3d 100%)",
];
type Mode = "light" | "dark" | "system";
export function ThemingPanel() {
const { themingOpen, setThemingOpen } = useUi();
const { theme, scheme, setScheme, setThemeOverrides, resetTheme } = useTheme();
const [mode, setMode] = useState<Mode>(scheme);
// when in "system" mode, follow OS changes live
useEffect(() => {
if (mode !== "system" || typeof window === "undefined") return;
const mq = window.matchMedia("(prefers-color-scheme: dark)");
const apply = () => setScheme(mq.matches ? "dark" : "light");
apply();
mq.addEventListener("change", apply);
return () => mq.removeEventListener("change", apply);
}, [mode, setScheme]);
if (!themingOpen) return null;
const activeAccent = scheme === "light" ? theme.light.accent : theme.dark.accent;
const pickMode = (m: Mode) => {
setMode(m);
if (m !== "system") setScheme(m);
};
return (
<div className="fixed inset-0 z-40 flex justify-end bg-black/40 animate-fade-in" onClick={() => setThemingOpen(false)}>
<div role="dialog" aria-modal="true" aria-label="Appearance" className="flex h-full w-[min(360px,100vw)] flex-col border-l border-border bg-surface shadow-pop" onClick={(e) => e.stopPropagation()}>
<div className="flex items-center justify-between border-b border-border px-4 py-3">
<div>
<div className="text-[15px] font-bold">Appearance</div>
<div className="text-[12px] text-muted">Live theme persists to this browser</div>
</div>
<button onClick={() => setThemingOpen(false)} className="text-muted hover:text-ink"><X size={18} /></button>
</div>
<div className="min-h-0 flex-1 overflow-y-auto p-4">
<Group title="Color mode">
<div className="grid grid-cols-3 gap-2">
{([{ v: "light", icon: Sun, label: "Light" }, { v: "dark", icon: Moon, label: "Dark" }, { v: "system", icon: Monitor, label: "System" }] as const).map((m) => (
<button key={m.v} onClick={() => pickMode(m.v)} className={clsx("flex flex-col items-center gap-1.5 rounded-control border py-3 text-[12px]", mode === m.v ? "border-accent bg-accent-soft text-accent" : "border-border hover:bg-hover")}>
<m.icon size={18} />
{m.label}
</button>
))}
</div>
</Group>
<Group title="Accent color">
<div className="flex flex-wrap gap-2">
{ACCENTS.map((c) => (
<button key={c} onClick={() => setThemeOverrides({ accent: c })} className="grid h-9 w-9 place-items-center rounded-full transition-all hover:scale-110" style={{ background: c, boxShadow: activeAccent === c ? "0 0 0 2px var(--c-bg), 0 0 0 4px " + c : undefined }}>
{activeAccent === c && <Check size={16} className="text-black/70" />}
</button>
))}
<label className="flex items-center gap-1.5 rounded-control border border-border px-2 text-[12px] text-muted">
Hex
<input type="color" value={/^#[0-9a-f]{6}$/i.test(activeAccent) ? activeAccent : "#FDA913"} onChange={(e) => setThemeOverrides({ accent: e.target.value })} className="h-7 w-8 cursor-pointer border-0 bg-transparent" />
</label>
</div>
</Group>
<Group title="Corner radius">
<div className="grid grid-cols-3 gap-2">
{RADII.map((r) => (
<button key={r.label} onClick={() => setThemeOverrides({ radiusCard: r.card, radiusControl: r.control })} className={clsx("border py-3 text-[12px]", theme.radiusCard === r.card ? "border-accent bg-accent-soft text-accent" : "border-border hover:bg-hover")} style={{ borderRadius: r.control }}>
{r.label}
</button>
))}
</div>
</Group>
<Group title="Brand gradient">
<div className="flex gap-2">
{GRADIENTS.map((g) => (
<button key={g} onClick={() => setThemeOverrides({ gradient: g })} className={clsx("h-10 flex-1 rounded-control ring-2 ring-offset-2 ring-offset-surface", theme.gradient === g ? "ring-accent" : "ring-transparent")} style={{ background: g }} />
))}
</div>
</Group>
<div className="rounded-control border border-border bg-panel p-3 text-[12px] text-muted">
All tokens here are also settable at build time via <code className="text-accent">NEXT_PUBLIC_THEME_*</code> env vars. See <code className="text-accent">docs/THEMING.md</code>.
</div>
</div>
<div className="border-t border-border p-3">
<button onClick={() => { resetTheme(); setMode("dark"); }} className="focus-ring flex w-full items-center justify-center gap-2 rounded-control border border-border py-2.5 text-[13px] font-semibold hover:bg-hover">
<RotateCcw size={15} /> Reset to defaults
</button>
</div>
</div>
</div>
);
}
function Group({ title, children }: { title: string; children: React.ReactNode }) {
return (
<div className="mb-5">
<div className="mb-2 text-[12px] font-bold uppercase tracking-wide text-dim">{title}</div>
{children}
</div>
);
}