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:
arsam
2026-09-18 23:10:44 +03:30
parent 1ac12f3e48
commit 718450acc6
@@ -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>
);
}