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