← back to Brand Selector Viewer
outputs/magic-BrandSelector.tsx
190 lines
// Generated via 21st.dev Magic — Brand Selector
// Drop into a React/Tailwind project. Self-contained; props-driven.
// Pairs with Paper editorial spec + Figma design tokens.
import { useMemo, useState } from "react";
export type Brand = {
name: string;
country: string;
founded: number;
palette: [string, string, string];
};
export type BrandPayload = {
schema: "figma-build/brand-selection.v1";
generated_at: string;
counts: { fashion: number; wallcovering: number; total: number };
fashion: Brand[];
wallcovering: Brand[];
};
export const FASHION_20: Brand[] = [
{ name: "Louis Vuitton", country: "FR", founded: 1854, palette: ["#5d3a22", "#d4b886", "#1a1a1a"] },
{ name: "Chanel", country: "FR", founded: 1910, palette: ["#000000", "#ffffff", "#c9a96e"] },
{ name: "Hermès", country: "FR", founded: 1837, palette: ["#f37021", "#1a1a1a", "#f4e7d3"] },
{ name: "Gucci", country: "IT", founded: 1921, palette: ["#006341", "#a01c1f", "#d4af37"] },
{ name: "Prada", country: "IT", founded: 1913, palette: ["#000000", "#8b8680", "#e8e4dc"] },
{ name: "Dior", country: "FR", founded: 1946, palette: ["#1a1a1a", "#c8a883", "#ffffff"] },
{ name: "Versace", country: "IT", founded: 1978, palette: ["#d4af37", "#000000", "#a01c1f"] },
{ name: "Burberry", country: "UK", founded: 1856, palette: ["#a08661", "#000000", "#c1272d"] },
{ name: "Saint Laurent", country: "FR", founded: 1961, palette: ["#000000", "#d4b886", "#ffffff"] },
{ name: "Balenciaga", country: "ES", founded: 1919, palette: ["#000000", "#5a5a5a", "#ffffff"] },
{ name: "Bottega Veneta",country: "IT", founded: 1966, palette: ["#3d5a2c", "#c9b27a", "#1a1a1a"] },
{ name: "Fendi", country: "IT", founded: 1925, palette: ["#5d3a22", "#f0e6d2", "#000000"] },
{ name: "Givenchy", country: "FR", founded: 1952, palette: ["#000000", "#d4af37", "#a01c1f"] },
{ name: "Valentino", country: "IT", founded: 1960, palette: ["#a01c1f", "#1a1a1a", "#f0e6d2"] },
{ name: "Armani", country: "IT", founded: 1975, palette: ["#1a1a2e", "#8b8680", "#000000"] },
{ name: "Ralph Lauren", country: "US", founded: 1967, palette: ["#1a3a6c", "#a01c1f", "#f0e6d2"] },
{ name: "Tom Ford", country: "US", founded: 2005, palette: ["#000000", "#5d3a22", "#d4af37"] },
{ name: "Celine", country: "FR", founded: 1945, palette: ["#000000", "#d4b886", "#ffffff"] },
{ name: "Loewe", country: "ES", founded: 1846, palette: ["#5d3a22", "#d4b886", "#1a1a1a"] },
{ name: "Alexander McQueen", country: "UK", founded: 1992, palette: ["#000000", "#a01c1f", "#8b8680"] },
];
export const WALL_10: Brand[] = [
{ name: "Phillip Jeffries", country: "US", founded: 1976, palette: ["#d4b886", "#5d3a22", "#f0e6d2"] },
{ name: "Schumacher", country: "US", founded: 1889, palette: ["#a01c1f", "#3d5a2c", "#d4af37"] },
{ name: "Cole & Son", country: "UK", founded: 1875, palette: ["#1a3a6c", "#c9a96e", "#f0e6d2"] },
{ name: "Thibaut", country: "US", founded: 1886, palette: ["#3d5a2c", "#c9a96e", "#1a1a1a"] },
{ name: "Zoffany", country: "UK", founded: 1895, palette: ["#5d3a22", "#d4b886", "#3d5a2c"] },
{ name: "Farrow & Ball", country: "UK", founded: 1946, palette: ["#1a3a3a", "#e8e4dc", "#5d3a22"] },
{ name: "Brunschwig & Fils", country: "US", founded: 1909, palette: ["#a01c1f", "#1a3a6c", "#d4b886"] },
{ name: "de Gournay", country: "UK", founded: 1986, palette: ["#c9a96e", "#5d3a22", "#3d5a2c"] },
{ name: "Fromental", country: "UK", founded: 2005, palette: ["#1a3a6c", "#d4af37", "#5d3a22"] },
{ name: "Pierre Frey", country: "FR", founded: 1935, palette: ["#a01c1f", "#d4b886", "#1a3a6c"] },
];
type Props = {
fashion?: Brand[];
wallcovering?: Brand[];
onSubmit?: (payload: BrandPayload) => void;
};
export default function BrandSelector({
fashion = FASHION_20,
wallcovering = WALL_10,
onSubmit,
}: Props) {
const [picked, setPicked] = useState<Record<string, boolean>>({});
const toggle = (name: string) =>
setPicked((p) => ({ ...p, [name]: !p[name] }));
const buildPayload = (): BrandPayload => {
const f = fashion.filter((b) => picked[b.name]);
const w = wallcovering.filter((b) => picked[b.name]);
return {
schema: "figma-build/brand-selection.v1",
generated_at: new Date().toISOString(),
counts: { fashion: f.length, wallcovering: w.length, total: f.length + w.length },
fashion: f,
wallcovering: w,
};
};
const summary = useMemo(() => {
const all = [...fashion, ...wallcovering].filter((b) => picked[b.name]);
return { count: all.length, names: all.map((b) => b.name).join(" · ") || "none" };
}, [picked, fashion, wallcovering]);
return (
<div className="min-h-screen bg-[#0b0b0d] text-zinc-100 pb-32 font-sans">
<header className="px-10 pt-12 pb-6 border-b border-zinc-800 flex flex-wrap items-end justify-between gap-6">
<div>
<div className="text-[10px] tracking-[3px] uppercase text-amber-400/80">Figma Build Source · v1</div>
<h1 className="font-serif text-5xl tracking-tight mt-2">Brand Selector</h1>
<p className="italic text-zinc-400 mt-1">Pick the labels that will source the next Figma build · Top 20 fashion · Top 10 wallcovering</p>
</div>
<div className="flex gap-12 text-right">
<Stat n={fashion.filter((b) => picked[b.name]).length} label="Fashion" />
<Stat n={wallcovering.filter((b) => picked[b.name]).length} label="Wallcovering" />
</div>
</header>
<Section index="I." title="Fashion" sub="Top 20 — Luxury & Iconic Houses" brands={fashion} picked={picked} onToggle={toggle} />
<Section index="II." title="Wallcovering" sub="Top 10 — Heritage & Designer Houses" brands={wallcovering} picked={picked} onToggle={toggle} />
<div className="fixed bottom-0 inset-x-0 bg-[#0b0b0d]/90 backdrop-blur border-t border-zinc-800 px-10 py-3 flex items-center justify-between gap-4">
<div className="flex gap-3 items-center text-sm">
<span className="bg-zinc-900 px-3 py-1.5 rounded-full"><b className="text-amber-400 font-serif mr-1">{summary.count}</b> selected</span>
<span className="text-zinc-500 truncate max-w-[600px]">{summary.names}</span>
</div>
<button
onClick={() => onSubmit?.(buildPayload())}
className="bg-amber-400 text-zinc-900 px-5 py-2 rounded-full text-sm font-medium hover:bg-amber-300 transition"
>
Send to Figma Build →
</button>
</div>
</div>
);
}
function Stat({ n, label }: { n: number; label: string }) {
return (
<div>
<div className="font-serif text-3xl text-amber-400 leading-none">{n}</div>
<div className="text-[10px] tracking-[2px] uppercase text-zinc-500 mt-1">{label}</div>
</div>
);
}
function Section({
index, title, sub, brands, picked, onToggle,
}: { index: string; title: string; sub: string; brands: Brand[]; picked: Record<string, boolean>; onToggle: (n: string) => void; }) {
return (
<section className="px-10 pt-12">
<div className="mb-5">
<div className="text-[10px] tracking-[3px] uppercase text-amber-400">{index}</div>
<h2 className="font-serif text-3xl mt-1">{title}</h2>
<div className="italic text-zinc-500 text-sm tracking-wide">{sub}</div>
</div>
<div className="grid grid-cols-[repeat(auto-fill,minmax(220px,1fr))] gap-3">
{brands.map((b) => (
<BrandCard key={b.name} brand={b} selected={!!picked[b.name]} onClick={() => onToggle(b.name)} />
))}
</div>
</section>
);
}
function BrandCard({ brand, selected, onClick }: { brand: Brand; selected: boolean; onClick: () => void }) {
return (
<button
onClick={onClick}
className={[
"relative text-left rounded-2xl p-5 min-h-[140px] flex flex-col justify-between transition border",
selected
? "bg-gradient-to-br from-amber-400/10 to-zinc-900 border-amber-400 shadow-[inset_0_0_0_1px_rgba(212,175,55,1)]"
: "bg-zinc-900 border-zinc-800 hover:border-zinc-600 hover:-translate-y-0.5",
].join(" ")}
>
<span className={[
"absolute top-3 right-3 w-5 h-5 rounded-full border flex items-center justify-center text-[11px]",
selected ? "bg-amber-400 border-amber-400 text-zinc-900" : "border-zinc-700 text-transparent",
].join(" ")}>✓</span>
<div>
<div className="font-serif text-lg leading-tight pr-7">{brand.name}</div>
<div className="flex gap-1.5 mt-2.5">
<Tag>{brand.country}</Tag>
<Tag>est. {brand.founded}</Tag>
</div>
</div>
<div className="flex gap-1 mt-3">
{brand.palette.map((c) => (
<span key={c} className="w-4 h-4 rounded border border-white/10" style={{ background: c }} />
))}
</div>
</button>
);
}
function Tag({ children }: { children: React.ReactNode }) {
return (
<span className="text-[9.5px] tracking-[1.2px] uppercase text-zinc-500 border border-zinc-800 px-2 py-0.5 rounded-full">
{children}
</span>
);
}