126 lines
6.0 KiB
TypeScript
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>
|
|
);
|
|
}
|