diff --git a/vibn-frontend/app/api/design-systems/[id]/preview/route.ts b/vibn-frontend/app/api/design-systems/[id]/preview/route.ts new file mode 100644 index 0000000..9f07fd2 --- /dev/null +++ b/vibn-frontend/app/api/design-systems/[id]/preview/route.ts @@ -0,0 +1,45 @@ +import { NextResponse } from "next/server"; +import fs from "fs"; +import path from "path"; + +export async function GET( + request: Request, + { params }: { params: Promise<{ id: string }> } +) { + const { id } = await params; + + if (!id || id.includes("..") || id.includes("/")) { + return new NextResponse("Invalid design system ID", { status: 400 }); + } + + const htmlPath = path.join( + process.cwd(), + "lib", + "scaffold", + "open-design", + "design-systems", + id, + "components.html" + ); + + try { + if (!fs.existsSync(htmlPath)) { + return new NextResponse(`Preview not found for ${id}`, { status: 404 }); + } + + const html = await fs.promises.readFile(htmlPath, "utf-8"); + + return new NextResponse(html, { + status: 200, + headers: { + "Content-Type": "text/html; charset=utf-8", + // Allow framing only from same origin for security + "X-Frame-Options": "SAMEORIGIN", + }, + }); + } catch (err: any) { + return new NextResponse(`Error loading preview: ${err.message}`, { + status: 500, + }); + } +} diff --git a/vibn-frontend/components/project/design-system-explorer.tsx b/vibn-frontend/components/project/design-system-explorer.tsx index 8d34e19..3e2032a 100644 --- a/vibn-frontend/components/project/design-system-explorer.tsx +++ b/vibn-frontend/components/project/design-system-explorer.tsx @@ -1,89 +1,37 @@ "use client"; -/** - * Design kit explorer — persisted starter kit + user overrides per project. - * See lib/design-kits/* and /api/projects/[id]/design-kit. - */ - -import type { CSSProperties, ReactNode } from "react"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { useParams } from "next/navigation"; -import { - ChevronRight, - LayoutGrid, - Loader2, - Palette, - RotateCcw, - Save, -} from "lucide-react"; +import { Loader2, Check, Search, Eye, Sparkles } from "lucide-react"; import { toast } from "sonner"; -import type { DesignKitOverrides } from "@/lib/design-kits/types"; -import { - DEFAULT_DESIGN_KIT_ID, - UI_FOUNDATION_LABELS, -} from "@/lib/design-kits/types"; -import { STARTER_KITS, getStarterKit } from "@/lib/design-kits/registry"; -import { mergeOverrides, resolveKitTokens } from "@/lib/design-kits/resolve"; -import { - DESIGN_KIT_SECTIONS, - renderKitPanel, -} from "@/components/project/design-kit-panels"; +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 [kitId, setKitId] = useState(DEFAULT_DESIGN_KIT_ID); - const [perKit, setPerKit] = useState>({}); - const [draft, setDraft] = useState({}); + const [activeKitId, setActiveKitId] = useState(DEFAULT_DESIGN_KIT_ID); + const [previewKitId, setPreviewKitId] = useState(DEFAULT_DESIGN_KIT_ID); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); - const [openIds, setOpenIds] = useState>( - () => new Set(["c-accent"]), - ); - const iframeRef = useRef(null); - - const kit = useMemo( - () => - getStarterKit(kitId) ?? - getStarterKit(DEFAULT_DESIGN_KIT_ID) ?? - STARTER_KITS[0], - [kitId], - ); - const savedForKit = perKit[kitId] ?? {}; - const mergedDraft = useMemo( - () => ({ ...savedForKit, ...draft }), - [savedForKit, draft], - ); - const tokens = useMemo( - () => resolveKitTokens(kit, mergedDraft), - [kit, mergedDraft], - ); + 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}`, - ); + 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; - perKit?: Record; - }) => { - if (cancelled) return; - if (d.kitId && getStarterKit(d.kitId)) setKitId(d.kitId); - setPerKit(d.perKit && typeof d.perKit === "object" ? d.perKit : {}); - setDraft({}); - }, - ) + .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"); }) @@ -95,694 +43,156 @@ export function DesignSystemExplorer() { }; }, [projectId]); - const persistKitId = useCallback( - async (nextId: string) => { - const res = await fetch(`/api/projects/${projectId}/design-kit`, { - method: "PATCH", - credentials: "include", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ kitId: nextId }), - }); - if (!res.ok) throw new Error("Could not save kit"); - const data = await res.json(); - setKitId(data.kitId); - setPerKit(data.perKit ?? {}); - setDraft({}); - toast.success("Starter kit saved", { - description: - "Ask Vibn in chat to apply this theme to your codebase (globals.css / Tailwind / theme provider). Large apps may need a token refactor first.", - }); - }, - [projectId], - ); - - const persistOverrides = useCallback(async () => { + 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({ overrides: draft }), + body: JSON.stringify({ kitId }), }); - if (!res.ok) throw new Error("Could not save customization"); + if (!res.ok) throw new Error("Could not save kit"); const data = await res.json(); - setPerKit(data.perKit ?? {}); - setDraft({}); - toast.success("Theme saved", { - description: - "Saved to this project for the AI. Ask Vibn in chat to wire these tokens into your app. If colors are scattered across files, it may need a short refactor.", + 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 (e) { - toast.error(e instanceof Error ? e.message : "Save failed"); + } catch (err: any) { + toast.error(err.message || "Failed to switch design system"); } finally { setSaving(false); } - }, [projectId, draft]); + }, [projectId]); - const resetToKitDefaults = useCallback(() => { - setDraft({}); - void (async () => { - try { - const defaults = kit.defaults; - const res = await fetch(`/api/projects/${projectId}/design-kit`, { - method: "PATCH", - credentials: "include", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - overrides: { - accentHex: defaults.accentHex, - radiusMdPx: defaults.radiusMdPx, - fontPreset: defaults.fontPreset, - density: defaults.density, - }, - }), - }); - if (!res.ok) throw new Error("Reset failed"); - const data = await res.json(); - setPerKit(data.perKit ?? {}); - toast.message("Reset to starter defaults"); - } catch { - toast.error("Reset failed"); - } - })(); - }, [kit.defaults, projectId]); - - const toggle = useCallback((id: string) => { - setOpenIds((prev) => { - const next = new Set(prev); - if (next.has(id)) next.delete(id); - else next.add(id); - return next; - }); - }, []); - - const effective = mergeOverrides(kit, mergedDraft); + 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 ( -
- - - Loading design kit… - +
+
+ + Loading design system... +
); } + const previewKit = STARTER_KITS.find(k => k.id === previewKitId) || STARTER_KITS[0]; + const isActive = activeKitId === previewKitId; + return ( -
-
-
-
- -
-
-

- {kit.name} -

-

- {kit.tagline} - - {UI_FOUNDATION_LABELS[kit.uiFoundation]} - -

-
-
-
- - -
-
- - {kitId === "flyonui" ? ( -
-
- - FlyonUI Themes: - - {[ - "light", - "dark", - "cupcake", - "retro", - "cyberpunk", - "synthwave", - "valentine", - "aqua", - ].map((t) => ( - - ))} -
-