import { useState, type ReactNode } from "react";
import { RotateCcw } from "lucide-react";
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
} from "./card";
import {
Accordion,
AccordionItem,
AccordionContent,
AccordionTrigger,
} from "./accordion";
import { Button } from "./button";
import { TemplateGrid } from "./template-grid";
import { ProIndicator } from "@/components/pro/pro-indicator";
import {
AlertDialog,
AlertDialogContent,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogCancel,
AlertDialogAction,
} from "./alert-dialog";
export const APPEARANCE_GRID =
"grid min-w-0 grid-cols-1 items-start gap-6 xl:grid-cols-[minmax(0,1fr)_minmax(0,1.2fr)]";
export const APPEARANCE_FIELD_ROW =
"grid min-w-0 grid-cols-[minmax(0,1fr)_min(60%,15rem)] items-center gap-4";
export function AppearanceChoices({
value,
options,
onChange,
label = "Design choices",
}: {
value: string;
options: { value: string; label: string; icon?: ReactNode }[];
onChange: (value: string) => void;
label?: string;
}) {
return (
{options.map((option) => (
))}
);
}
export function AppearanceSection({
id,
title,
summary,
pro = false,
children,
}: {
id: string;
title: string;
summary?: string;
pro?: boolean;
children: ReactNode;
}) {
return (
{title}
{pro && (
<>
(Pro)
>
)}
{summary && (
{summary}
)}
{children}
);
}
function ReplacementDialog({
open,
onClose,
onApply,
name,
}: {
open: boolean;
onClose: () => void;
onApply: () => void;
name: string;
}) {
return (
{
if (!value) onClose();
}}
>
Replace this design?
Applying {name} replaces the affected draft appearance values,
including manual adjustments. Other feature settings stay unchanged.
Nothing is saved until you select Save Changes.
Keep current design
Apply design
);
}
export function AppearanceControls({
children,
onReset,
}: {
children: ReactNode;
onReset?: () => void;
}) {
const [confirmReset, setConfirmReset] = useState(false);
return (
Design controls
Adjust the appearance of this feature.
{onReset && (
)}
{children}
setConfirmReset(false)}
name="the default design"
onApply={() => {
onReset?.();
setConfirmReset(false);
}}
/>
);
}
export interface AppearancePreset {
id: string;
name: string;
description: string;
values: Partial;
isPro?: boolean;
}
export function matchesAppearance(
current: T,
expected: Partial,
) {
const normalize = (value: unknown) =>
typeof value === "string" && value.startsWith("#")
? value.toLowerCase()
: value === "inherit"
? ""
: value;
return (Object.keys(expected) as (keyof T)[]).every(
(key) => normalize(current[key]) === normalize(expected[key]),
);
}
export function AppearancePresets({
feature,
current,
presets,
baseValues,
onApply,
isPro = false,
}: {
feature: string;
current: T;
presets: AppearancePreset[];
baseValues?: Partial;
onApply: (values: Partial) => void;
isPro?: boolean;
}) {
const [pending, setPending] = useState(null);
const visible = presets.filter((preset) => !preset.isPro || isPro);
const values = (preset: AppearancePreset) => ({
...baseValues,
...preset.values,
});
const selected = visible.find((preset) =>
matchesAppearance(current, values(preset)),
);
const pendingPreset = visible.find((preset) => preset.id === pending);
return (
Design presets
Choose a starting point, then fine-tune the controls below.
Current design: {selected?.name || "Customized"}
{
const preset = visible.find((item) => item.id === id);
if (!preset || selected?.id === id) return;
if (!selected) setPending(id);
else onApply(values(preset));
}}
/>
setPending(null)}
name={pendingPreset?.name || "this preset"}
onApply={() => {
if (pendingPreset) onApply(values(pendingPreset));
setPending(null);
}}
/>
);
}