Import source: src/components/features/appearance-settings
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
This commit is contained in:
@@ -0,0 +1,235 @@
|
||||
"use client";
|
||||
|
||||
import { Check, Monitor, Moon, RotateCcw, Sparkles, Sun } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
useUiStore,
|
||||
type AccentId,
|
||||
type GlassLevel,
|
||||
type RadiusScale,
|
||||
} from "@/stores/ui-store";
|
||||
|
||||
const accents: Array<{ id: AccentId; label: string; swatch: string }> = [
|
||||
{ id: "azure", label: "Azure", swatch: "hsl(224 88% 55%)" },
|
||||
{ id: "violet", label: "Violet", swatch: "hsl(261 80% 60%)" },
|
||||
{ id: "emerald", label: "Emerald", swatch: "hsl(158 72% 40%)" },
|
||||
{ id: "sunset", label: "Sunset", swatch: "hsl(25 95% 53%)" },
|
||||
{ id: "rose", label: "Rose", swatch: "hsl(346 85% 56%)" },
|
||||
{ id: "cyan", label: "Cyan", swatch: "hsl(195 93% 45%)" },
|
||||
];
|
||||
|
||||
const glassLevels: Array<{ id: GlassLevel; label: string }> = [
|
||||
{ id: "off", label: "Off" },
|
||||
{ id: "subtle", label: "Subtle" },
|
||||
{ id: "standard", label: "Standard" },
|
||||
{ id: "heavy", label: "Heavy" },
|
||||
];
|
||||
|
||||
const radiusScales: Array<{ id: RadiusScale; label: string }> = [
|
||||
{ id: "sharp", label: "Sharp" },
|
||||
{ id: "rounded", label: "Rounded" },
|
||||
{ id: "soft", label: "Soft" },
|
||||
];
|
||||
|
||||
function Segmented<T extends string>({
|
||||
options,
|
||||
value,
|
||||
onChange,
|
||||
ariaLabel,
|
||||
}: {
|
||||
options: Array<{ id: T; label: string; icon?: React.ReactNode }>;
|
||||
value: T;
|
||||
onChange: (value: T) => void;
|
||||
ariaLabel: string;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
role="radiogroup"
|
||||
aria-label={ariaLabel}
|
||||
className="inline-flex flex-wrap gap-1 rounded-lg border border-border/70 bg-muted/50 p-1"
|
||||
>
|
||||
{options.map((option) => (
|
||||
<button
|
||||
key={option.id}
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={value === option.id}
|
||||
onClick={() => onChange(option.id)}
|
||||
className={cn(
|
||||
"focus-ring inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm transition-all duration-150",
|
||||
value === option.id
|
||||
? "bg-card font-medium text-primary shadow-panel ring-1 ring-primary/30"
|
||||
: "text-muted-foreground hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
{option.icon}
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function AppearanceSettings() {
|
||||
const theme = useUiStore((state) => state.theme);
|
||||
const setTheme = useUiStore((state) => state.setTheme);
|
||||
const accent = useUiStore((state) => state.accent);
|
||||
const setAccent = useUiStore((state) => state.setAccent);
|
||||
const glass = useUiStore((state) => state.glass);
|
||||
const setGlass = useUiStore((state) => state.setGlass);
|
||||
const radius = useUiStore((state) => state.radius);
|
||||
const setRadius = useUiStore((state) => state.setRadius);
|
||||
const ambiance = useUiStore((state) => state.ambiance);
|
||||
const setAmbiance = useUiStore((state) => state.setAmbiance);
|
||||
const density = useUiStore((state) => state.density);
|
||||
const setDensity = useUiStore((state) => state.setDensity);
|
||||
const resetAppearance = useUiStore((state) => state.resetAppearance);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex-row items-start justify-between space-y-0">
|
||||
<div>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Sparkles className="h-4 w-4 text-primary" />
|
||||
Appearance
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Liquid-glass customization. Every choice applies instantly and is saved on this
|
||||
device.
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" onClick={resetAppearance}>
|
||||
<RotateCcw className="h-3.5 w-3.5" />
|
||||
Reset
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-6 lg:grid-cols-[1fr_280px]">
|
||||
<div className="space-y-5">
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm font-medium">Theme</div>
|
||||
<Segmented
|
||||
ariaLabel="Theme"
|
||||
value={theme}
|
||||
onChange={setTheme}
|
||||
options={[
|
||||
{ id: "light", label: "Light", icon: <Sun className="h-3.5 w-3.5" /> },
|
||||
{ id: "dark", label: "Dark", icon: <Moon className="h-3.5 w-3.5" /> },
|
||||
{ id: "system", label: "System", icon: <Monitor className="h-3.5 w-3.5" /> },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm font-medium">Accent color</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{accents.map((option) => (
|
||||
<button
|
||||
key={option.id}
|
||||
type="button"
|
||||
onClick={() => setAccent(option.id)}
|
||||
aria-label={`${option.label} accent`}
|
||||
aria-pressed={accent === option.id}
|
||||
className={cn(
|
||||
"focus-ring flex h-9 w-9 items-center justify-center rounded-full transition-transform duration-150 hover:scale-110",
|
||||
accent === option.id &&
|
||||
"ring-2 ring-ring ring-offset-2 ring-offset-background",
|
||||
)}
|
||||
style={{ background: option.swatch }}
|
||||
>
|
||||
{accent === option.id ? <Check className="h-4 w-4 text-white" /> : null}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm font-medium">Glass intensity</div>
|
||||
<Segmented
|
||||
ariaLabel="Glass intensity"
|
||||
value={glass}
|
||||
onChange={setGlass}
|
||||
options={glassLevels}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Controls surface translucency and backdrop blur across the panel.
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm font-medium">Corner radius</div>
|
||||
<Segmented
|
||||
ariaLabel="Corner radius"
|
||||
value={radius}
|
||||
onChange={setRadius}
|
||||
options={radiusScales}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm font-medium">Density</div>
|
||||
<Segmented
|
||||
ariaLabel="Density"
|
||||
value={density}
|
||||
onChange={setDensity}
|
||||
options={[
|
||||
{ id: "compact" as const, label: "Compact" },
|
||||
{ id: "comfortable" as const, label: "Comfortable" },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<label className="flex cursor-pointer items-center justify-between gap-3 rounded-lg border border-border/70 bg-card/50 px-3 py-2.5">
|
||||
<span>
|
||||
<span className="block text-sm font-medium">Ambient background</span>
|
||||
<span className="block text-xs text-muted-foreground">
|
||||
Slow-drifting aurora, grid, and grain behind the interface.
|
||||
</span>
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={ambiance}
|
||||
aria-label="Ambient background"
|
||||
onClick={() => setAmbiance(!ambiance)}
|
||||
className={cn(
|
||||
"focus-ring relative h-6 w-11 shrink-0 rounded-full transition-colors duration-200",
|
||||
ambiance ? "bg-primary" : "bg-muted-foreground/30",
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
"absolute top-0.5 h-5 w-5 rounded-full bg-white shadow transition-transform duration-200",
|
||||
ambiance ? "translate-x-[22px]" : "translate-x-0.5",
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
</label>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="text-sm font-medium">Live preview</div>
|
||||
<div className="glass-panel space-y-3 rounded-lg p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-display text-sm font-semibold">Preview surface</span>
|
||||
<Badge variant="success">healthy</Badge>
|
||||
</div>
|
||||
<Progress value={64} />
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Glass, accent, and radius react in real time.
|
||||
</p>
|
||||
<Button className="w-full" size="sm">
|
||||
Primary action
|
||||
</Button>
|
||||
<Button className="w-full" size="sm" variant="outline">
|
||||
Secondary action
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user