This repository has been archived on 2026-06-07. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
master-ai/vibn-frontend/components/project/design-system-explorer.tsx

234 lines
9.8 KiB
TypeScript

"use client";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useParams } from "next/navigation";
import { Loader2, Check, Search, Eye, Sparkles, LayoutTemplate, Palette } from "lucide-react";
import { toast } from "sonner";
import { STARTER_KITS } from "@/lib/design-kits/registry";
import { DEFAULT_DESIGN_KIT_ID } from "@/lib/design-kits/types";
export function DesignSystemExplorer() {
const params = useParams();
const projectId = params.projectId as string;
const [activeKitId, setActiveKitId] = useState<string>(DEFAULT_DESIGN_KIT_ID);
const [previewKitId, setPreviewKitId] = useState<string>(DEFAULT_DESIGN_KIT_ID);
const [previewMode, setPreviewMode] = useState<"showcase" | "tokens">("showcase");
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [search, setSearch] = useState("");
useEffect(() => {
let cancelled = false;
setLoading(true);
fetch(`/api/projects/${projectId}/design-kit`, { credentials: "include" })
.then((r) => {
if (!r.ok) throw new Error(r.status === 401 ? "Sign in to load design kit" : `HTTP ${r.status}`);
return r.json();
})
.then((d: { kitId?: string }) => {
if (cancelled) return;
if (d.kitId) {
setActiveKitId(d.kitId);
setPreviewKitId(d.kitId);
}
})
.catch((e: Error) => {
if (!cancelled) toast.error(e.message || "Failed to load design kit");
})
.finally(() => {
if (!cancelled) setLoading(false);
});
return () => {
cancelled = true;
};
}, [projectId]);
const selectKit = useCallback(async (kitId: string) => {
setSaving(true);
try {
const res = await fetch(`/api/projects/${projectId}/design-kit`, {
method: "PATCH",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ kitId }),
});
if (!res.ok) throw new Error("Could not save kit");
const data = await res.json();
setActiveKitId(data.kitId || kitId);
toast.success("Design System Updated", {
description: "The AI agent now has full context of this design system's guidelines and tokens. Ask it to apply the theme in chat!",
});
} catch (err: any) {
toast.error(err.message || "Failed to switch design system");
} finally {
setSaving(false);
}
}, [projectId]);
const filteredKits = useMemo(() => {
const s = search.toLowerCase();
return STARTER_KITS.filter(k => k.name.toLowerCase().includes(s) || k.tagline.toLowerCase().includes(s));
}, [search]);
if (loading) {
return (
<div className="flex-1 bg-[#fbfaf9] p-6 md:p-10 flex items-center justify-center">
<div className="flex items-center gap-3 text-zinc-500">
<Loader2 className="w-5 h-5 animate-spin" />
<span className="text-sm">Loading design system...</span>
</div>
</div>
);
}
const previewKit = STARTER_KITS.find(k => k.id === previewKitId) || STARTER_KITS[0];
const isActive = activeKitId === previewKitId;
return (
<div className="flex-1 bg-[#fbfaf9] h-full flex flex-col md:flex-row overflow-hidden">
{/* LEFT SIDEBAR: Library & Search */}
<div className="w-full md:w-80 flex-shrink-0 border-r border-zinc-200 bg-white flex flex-col h-full overflow-hidden z-20">
<div className="p-4 border-b border-zinc-200">
<h2 className="text-sm font-semibold text-zinc-900 mb-3 flex items-center gap-2">
<Sparkles className="w-4 h-4 text-indigo-600" />
Vibn Design Library
</h2>
<div className="relative">
<Search className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-zinc-400" />
<input
type="text"
placeholder="Search 150+ systems..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="w-full pl-9 pr-4 py-2 bg-zinc-50 border border-zinc-200 rounded-lg text-xs focus:outline-none focus:ring-2 focus:ring-indigo-500/20 focus:border-indigo-500 transition-all"
/>
</div>
</div>
<div className="flex-1 overflow-y-auto p-2 space-y-1 scrollbar-hide">
{filteredKits.map((kit) => {
const isSelected = previewKitId === kit.id;
const isCurrentlyActive = activeKitId === kit.id;
return (
<button
key={kit.id}
onClick={() => {
setPreviewKitId(kit.id);
if (!kit.hasPreview && previewMode === "tokens") {
setPreviewMode("showcase"); // force showcase if no raw tokens gallery
}
}}
className={`w-full text-left p-3 rounded-lg flex items-start gap-3 transition-colors group relative ${
isSelected ? 'bg-indigo-50/80 ring-1 ring-indigo-500/30' : 'hover:bg-zinc-50'
}`}
>
<div className={`mt-0.5 flex-shrink-0 w-4 h-4 rounded-full border flex items-center justify-center ${
isCurrentlyActive
? 'border-indigo-600 bg-indigo-600'
: isSelected
? 'border-indigo-300'
: 'border-zinc-300'
}`}>
{isCurrentlyActive && <Check className="w-2.5 h-2.5 text-white" />}
</div>
<div className="flex-1 min-w-0 pr-6">
<div className={`text-sm font-medium truncate ${isSelected ? 'text-indigo-950' : 'text-zinc-900'}`}>
{kit.name}
</div>
<div className={`text-[11px] truncate mt-0.5 ${isSelected ? 'text-indigo-700/70' : 'text-zinc-500'}`}>
{kit.tagline || kit.id}
</div>
</div>
{/* Visual Indicator if it has an HTML preview */}
{kit.hasPreview && (
<div className="absolute right-3 top-3.5 text-zinc-300 group-hover:text-indigo-400 transition-colors" title="Detailed Token Gallery Available">
<Palette className="w-3.5 h-3.5" />
</div>
)}
</button>
);
})}
{filteredKits.length === 0 && (
<div className="p-4 text-center text-zinc-500 text-xs">
No design systems found.
</div>
)}
</div>
</div>
{/* RIGHT MAIN AREA: Live Preview & Action Bar */}
<div className="flex-1 flex flex-col h-full bg-zinc-100 overflow-hidden relative">
{/* Top Action Bar */}
<div className="h-14 flex-shrink-0 bg-white border-b border-zinc-200 px-6 flex items-center justify-between shadow-sm z-10">
<div className="flex items-center gap-6 min-w-0">
<div className="flex items-center gap-2">
<Eye className="w-4 h-4 text-zinc-400" />
<span className="text-sm font-medium text-zinc-900 truncate">
{previewKit.name}
</span>
</div>
<div className="flex bg-zinc-100 p-0.5 rounded-lg border border-zinc-200">
<button
onClick={() => setPreviewMode("showcase")}
className={`px-3 py-1 text-xs font-medium rounded-md transition-colors ${
previewMode === "showcase" ? 'bg-white text-zinc-900 shadow-sm' : 'text-zinc-500 hover:text-zinc-700'
}`}
>
Showcase
</button>
{previewKit.hasPreview && (
<button
onClick={() => setPreviewMode("tokens")}
className={`px-3 py-1 text-xs font-medium rounded-md transition-colors ${
previewMode === "tokens" ? 'bg-white text-zinc-900 shadow-sm' : 'text-zinc-500 hover:text-zinc-700'
}`}
>
Raw Tokens
</button>
)}
</div>
</div>
<button
onClick={() => selectKit(previewKit.id)}
disabled={saving || isActive}
className={`px-4 py-1.5 rounded-md text-sm font-medium transition-all flex items-center gap-2 ${
isActive
? 'bg-zinc-100 text-zinc-400 cursor-default shadow-inner'
: 'bg-indigo-600 hover:bg-indigo-700 text-white shadow-sm hover:shadow-md'
}`}
>
{saving && <Loader2 className="w-3.5 h-3.5 animate-spin" />}
{isActive ? 'Active System' : 'Set as Active Theme'}
</button>
</div>
{/* Live Iframe */}
<div className="flex-1 relative bg-white m-4 rounded-xl border border-zinc-200 shadow-sm overflow-hidden flex flex-col transition-all">
<div className="h-8 bg-zinc-50 border-b border-zinc-200 flex items-center px-4 gap-2 flex-shrink-0">
<div className="flex gap-1.5">
<div className="w-2.5 h-2.5 rounded-full bg-red-400/80"></div>
<div className="w-2.5 h-2.5 rounded-full bg-amber-400/80"></div>
<div className="w-2.5 h-2.5 rounded-full bg-green-400/80"></div>
</div>
<div className="mx-auto bg-white px-3 py-0.5 rounded-md border border-zinc-200 text-[10px] text-zinc-400 font-mono shadow-sm">
{previewKit.id}.{previewMode}.preview
</div>
</div>
<iframe
key={`${previewKit.id}-${previewMode}`} // Forces iframe to remount on change
src={previewMode === "showcase" ? `/api/design-systems/${previewKit.id}/showcase` : `/api/design-systems/${previewKit.id}/preview`}
className="flex-1 w-full h-full bg-zinc-50"
sandbox="allow-scripts allow-same-origin"
/>
</div>
</div>
</div>
);
}