fix(ui): handle fallback UI for design systems without visual previews
This commit is contained in:
@@ -12,19 +12,57 @@ export async function GET(
|
|||||||
return new NextResponse("Invalid design system ID", { status: 400 });
|
return new NextResponse("Invalid design system ID", { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const htmlPath = path.join(
|
const systemPath = path.join(
|
||||||
process.cwd(),
|
process.cwd(),
|
||||||
"lib",
|
"lib",
|
||||||
"scaffold",
|
"scaffold",
|
||||||
"open-design",
|
"open-design",
|
||||||
"design-systems",
|
"design-systems",
|
||||||
id,
|
id
|
||||||
"components.html"
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const htmlPath = path.join(systemPath, "components.html");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (!fs.existsSync(systemPath)) {
|
||||||
|
return new NextResponse(`Design system not found for ${id}`, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
if (!fs.existsSync(htmlPath)) {
|
if (!fs.existsSync(htmlPath)) {
|
||||||
return new NextResponse(`Preview not found for ${id}`, { status: 404 });
|
// If there's no components.html, serve a beautiful fallback UI
|
||||||
|
// that explains this system only has Markdown guidelines for the AI
|
||||||
|
const fallbackHtml = `
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>${id} — No preview available</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; background: #fbfaf9; color: #52525b; text-align: center; }
|
||||||
|
.card { background: white; padding: 40px; border-radius: 12px; border: 1px solid #e4e4e7; box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.05); max-width: 400px; }
|
||||||
|
h2 { color: #18181b; margin-top: 0; }
|
||||||
|
p { line-height: 1.5; font-size: 14px; }
|
||||||
|
.badge { display: inline-block; background: #e0e7ff; color: #4338ca; padding: 4px 8px; border-radius: 6px; font-size: 12px; font-weight: 500; font-family: monospace; margin-top: 16px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="card">
|
||||||
|
<h2>Preview Not Available</h2>
|
||||||
|
<p>The <strong>${id}</strong> design system is a "Rules-Only" system.</p>
|
||||||
|
<p>It provides strict typographic, spacing, and layout instructions to the AI via its <code>DESIGN.md</code>, but it doesn't currently ship with a visual HTML preview gallery.</p>
|
||||||
|
<div class="badge">AI Guidelines Active</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
return new NextResponse(fallbackHtml, {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "text/html; charset=utf-8",
|
||||||
|
"X-Frame-Options": "SAMEORIGIN",
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const html = await fs.promises.readFile(htmlPath, "utf-8");
|
const html = await fs.promises.readFile(htmlPath, "utf-8");
|
||||||
@@ -33,7 +71,6 @@ export async function GET(
|
|||||||
status: 200,
|
status: 200,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "text/html; charset=utf-8",
|
"Content-Type": "text/html; charset=utf-8",
|
||||||
// Allow framing only from same origin for security
|
|
||||||
"X-Frame-Options": "SAMEORIGIN",
|
"X-Frame-Options": "SAMEORIGIN",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import { Loader2, Check, Search, Eye, Sparkles } from "lucide-react";
|
import { Loader2, Check, Search, Eye, Sparkles, LayoutTemplate } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { STARTER_KITS } from "@/lib/design-kits/registry";
|
import { STARTER_KITS } from "@/lib/design-kits/registry";
|
||||||
import { DEFAULT_DESIGN_KIT_ID } from "@/lib/design-kits/types";
|
import { DEFAULT_DESIGN_KIT_ID } from "@/lib/design-kits/types";
|
||||||
@@ -88,7 +88,7 @@ export function DesignSystemExplorer() {
|
|||||||
<div className="flex-1 bg-[#fbfaf9] h-full flex flex-col md:flex-row overflow-hidden">
|
<div className="flex-1 bg-[#fbfaf9] h-full flex flex-col md:flex-row overflow-hidden">
|
||||||
|
|
||||||
{/* LEFT SIDEBAR: Library & Search */}
|
{/* 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">
|
<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">
|
<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">
|
<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" />
|
<Sparkles className="w-4 h-4 text-indigo-600" />
|
||||||
@@ -114,7 +114,7 @@ export function DesignSystemExplorer() {
|
|||||||
<button
|
<button
|
||||||
key={kit.id}
|
key={kit.id}
|
||||||
onClick={() => setPreviewKitId(kit.id)}
|
onClick={() => setPreviewKitId(kit.id)}
|
||||||
className={`w-full text-left p-3 rounded-lg flex items-start gap-3 transition-colors ${
|
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'
|
isSelected ? 'bg-indigo-50/80 ring-1 ring-indigo-500/30' : 'hover:bg-zinc-50'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@@ -127,7 +127,7 @@ export function DesignSystemExplorer() {
|
|||||||
}`}>
|
}`}>
|
||||||
{isCurrentlyActive && <Check className="w-2.5 h-2.5 text-white" />}
|
{isCurrentlyActive && <Check className="w-2.5 h-2.5 text-white" />}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0 pr-6">
|
||||||
<div className={`text-sm font-medium truncate ${isSelected ? 'text-indigo-950' : 'text-zinc-900'}`}>
|
<div className={`text-sm font-medium truncate ${isSelected ? 'text-indigo-950' : 'text-zinc-900'}`}>
|
||||||
{kit.name}
|
{kit.name}
|
||||||
</div>
|
</div>
|
||||||
@@ -135,6 +135,12 @@ export function DesignSystemExplorer() {
|
|||||||
{kit.tagline || kit.id}
|
{kit.tagline || kit.id}
|
||||||
</div>
|
</div>
|
||||||
</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="Visual Preview Available">
|
||||||
|
<LayoutTemplate className="w-3.5 h-3.5" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -152,8 +158,11 @@ export function DesignSystemExplorer() {
|
|||||||
<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="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-3 min-w-0">
|
<div className="flex items-center gap-3 min-w-0">
|
||||||
<Eye className="w-4 h-4 text-zinc-400" />
|
<Eye className="w-4 h-4 text-zinc-400" />
|
||||||
<span className="text-sm font-medium text-zinc-900 truncate">
|
<span className="text-sm font-medium text-zinc-900 truncate flex items-center gap-2">
|
||||||
Previewing: {previewKit.name}
|
Previewing: {previewKit.name}
|
||||||
|
{!previewKit.hasPreview && (
|
||||||
|
<span className="px-2 py-0.5 rounded-full bg-zinc-100 text-zinc-500 text-[10px] font-mono tracking-wide uppercase border border-zinc-200">Rules Only</span>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -162,8 +171,8 @@ export function DesignSystemExplorer() {
|
|||||||
disabled={saving || isActive}
|
disabled={saving || isActive}
|
||||||
className={`px-4 py-1.5 rounded-md text-sm font-medium transition-all flex items-center gap-2 ${
|
className={`px-4 py-1.5 rounded-md text-sm font-medium transition-all flex items-center gap-2 ${
|
||||||
isActive
|
isActive
|
||||||
? 'bg-zinc-100 text-zinc-400 cursor-default'
|
? 'bg-zinc-100 text-zinc-400 cursor-default shadow-inner'
|
||||||
: 'bg-indigo-600 hover:bg-indigo-700 text-white shadow-sm'
|
: '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" />}
|
{saving && <Loader2 className="w-3.5 h-3.5 animate-spin" />}
|
||||||
@@ -172,22 +181,22 @@ export function DesignSystemExplorer() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Live Iframe */}
|
{/* Live Iframe */}
|
||||||
<div className="flex-1 relative bg-white m-4 rounded-xl border border-zinc-200 shadow-sm overflow-hidden flex flex-col">
|
<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="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="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-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-amber-400/80"></div>
|
||||||
<div className="w-2.5 h-2.5 rounded-full bg-green-400/80"></div>
|
<div className="w-2.5 h-2.5 rounded-full bg-green-400/80"></div>
|
||||||
</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">
|
<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}.design.preview
|
{previewKit.id}.design.preview
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<iframe
|
<iframe
|
||||||
key={previewKit.id} // Forces iframe to remount and show loading state if needed
|
key={previewKit.id} // Forces iframe to remount
|
||||||
src={`/api/design-systems/${previewKit.id}/preview`}
|
src={`/api/design-systems/${previewKit.id}/preview`}
|
||||||
className="flex-1 w-full h-full bg-white"
|
className="flex-1 w-full h-full bg-zinc-50"
|
||||||
sandbox="allow-scripts allow-same-origin"
|
sandbox="allow-scripts allow-same-origin"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -40,6 +40,7 @@ export interface DesignSystemDefinition {
|
|||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
tagline: string;
|
tagline: string;
|
||||||
|
hasPreview?: boolean;
|
||||||
defaults: DesignKitOverrides;
|
defaults: DesignKitOverrides;
|
||||||
/** Customize panel fields shown for this starter */
|
/** Customize panel fields shown for this starter */
|
||||||
customizeFields: Array<"accent" | "radius" | "font" | "density">;
|
customizeFields: Array<"accent" | "radius" | "font" | "density">;
|
||||||
|
|||||||
Reference in New Issue
Block a user