feat(ui): remove design tab from primary navigation

This commit is contained in:
2026-05-15 15:49:01 -07:00
parent dc82ccc47a
commit 4adf7a7659
4 changed files with 0 additions and 915 deletions

View File

@@ -1,670 +0,0 @@
"use client";
/**
* Visual panels for the design kit explorer — all driven by ResolvedKitTokens
* so previews stay in sync with the users starter kit + overrides.
*/
import type { CSSProperties, FC, ReactNode } from "react";
import type { ResolvedKitTokens } from "@/lib/design-kits/resolve";
export interface KitSectionItemMeta {
id: string;
title: string;
subtitle?: string;
panelKey: string;
}
export interface KitSectionMeta {
title: string;
items: KitSectionItemMeta[];
}
export const DESIGN_KIT_SECTIONS: KitSectionMeta[] = [
{
title: "Type",
items: [
{
id: "t-fw",
title: "Font Weights & Colors",
subtitle: "Weights + semantic text roles (starter font)",
panelKey: "fontWeights",
},
{
id: "t-scale",
title: "Type Scale",
subtitle: "Pixel/rem steps matched to kit density",
panelKey: "typeScale",
},
],
},
{
title: "Colors",
items: [
{
id: "c-accent",
title: "Accent scale",
subtitle: "12-step ramp from your accent · CTAs, focus, links",
panelKey: "accentScale",
},
{
id: "c-gray",
title: "Gray Scale",
subtitle: "12-step neutral — surfaces, borders, text",
panelKey: "grayScale",
},
{
id: "c-semantic",
title: "Semantic Colors",
subtitle: "Status & tag hues (reference palette)",
panelKey: "semanticColors",
},
],
},
{
title: "Spacing",
items: [
{ id: "s-radius", title: "Border Radius", subtitle: "Derived from radius token", panelKey: "radius" },
{ id: "s-shadow", title: "Box Shadows", subtitle: "Elevation system + focus line", panelKey: "shadows" },
{ id: "s-space", title: "Spacing Tokens", subtitle: "4px base unit spacing scale", panelKey: "spacing" },
],
},
{
title: "Components",
items: [
{ id: "x-btn", title: "Buttons", subtitle: "Primary · accent · danger · ghost", panelKey: "buttons" },
{ id: "x-input", title: "Input Fields", subtitle: "Default · focus · error · disabled", panelKey: "inputs" },
{ id: "x-nav", title: "Navigation Items", subtitle: "Sidebar density preview", panelKey: "nav" },
{ id: "x-tag", title: "Tag Colors", subtitle: "Status chips", panelKey: "tags" },
],
},
{
title: "Brand",
items: [
{ id: "b-icon", title: "Illustration Icons", subtitle: "Placeholder grid · swap with assets", panelKey: "illustrations" },
{ id: "b-logo", title: "Logo & Brand", subtitle: "Mark preview uses accent-on-neutral", panelKey: "logo" },
],
},
];
const codeSm: CSSProperties = {
fontFamily: "var(--font-ibm-plex-mono), ui-monospace, monospace",
fontSize: "0.68rem",
color: "#52525b",
};
const panelMiniTitle: CSSProperties = {
fontSize: "0.65rem",
fontWeight: 700,
letterSpacing: "0.06em",
color: "#a1a1aa",
marginBottom: 10,
};
const inputBase: CSSProperties = {
width: "100%",
boxSizing: "border-box",
padding: "10px 12px",
borderRadius: 8,
border: "1px solid #e4e4e7",
fontSize: "0.82rem",
fontFamily: "inherit",
background: "#fff",
};
function densityPad(tokens: ResolvedKitTokens, base: number): number {
return tokens.density === "compact" ? Math.round(base * 0.88) : base;
}
function SwatchRow({ colors, labels }: { colors: string[]; labels?: Record<number, string> }) {
return (
<div style={{ display: "flex", flexWrap: "wrap", gap: 6, alignItems: "flex-end" }}>
{colors.map((c, i) => (
<div key={i} style={{ textAlign: "center", width: 52 }}>
<div style={{ height: 40, borderRadius: 6, background: c, border: "1px solid #e4e4e7" }} />
<div style={{ fontSize: "0.62rem", color: "#71717a", marginTop: 4 }}>{labels?.[i] ?? i + 1}</div>
</div>
))}
</div>
);
}
export function FontWeightsPanel({ tokens }: { tokens: ResolvedKitTokens }) {
return (
<div style={{ fontFamily: tokens.fontFamily, display: "grid", gridTemplateColumns: "1fr 1fr", gap: 20 }}>
<div>
<div style={panelMiniTitle}>Font weights</div>
{[
["400", "Regular — body, field values"],
["500", "Medium — buttons, nav, labels"],
["600", "Semibold — headings, CTAs"],
].map(([w, desc]) => (
<div key={w} style={{ marginBottom: densityPad(tokens, 12) }}>
<div style={{ fontWeight: Number(w), fontSize: "0.95rem", color: "#18181b" }}>Aa {w}</div>
<div style={{ fontSize: "0.72rem", color: "#71717a" }}>{desc}</div>
</div>
))}
</div>
<div>
<div style={panelMiniTitle}>Semantic text colors</div>
{[
["primary", "#18181b", "Headings, record names"],
["secondary", "#52525b", "Body, descriptions"],
["tertiary", "#a1a1aa", "Labels, hints"],
["accent", tokens.accentHex, "Links & emphasis"],
["danger", "#dc2626", "Errors"],
].map(([role, hex, desc]) => (
<div key={role} style={{ marginBottom: 10, display: "flex", alignItems: "baseline", gap: 10 }}>
<span style={{ fontSize: "0.72rem", width: 72, color: "#71717a" }}>{role}</span>
<span style={{ fontSize: "0.88rem", color: hex, fontWeight: 500 }}>Sample</span>
<span style={{ fontSize: "0.7rem", color: "#a1a1aa", flex: 1 }}>{desc}</span>
</div>
))}
</div>
</div>
);
}
export function TypeScalePanel({ tokens }: { tokens: ResolvedKitTokens }) {
const scale =
tokens.density === "compact"
? [
{ token: "--font-size-xxl", rem: "1.65rem", sample: "Hero title", note: "Hero" },
{ token: "--font-size-xl", rem: "1.38rem", sample: "Page title", note: "Page" },
{ token: "--font-size-lg", rem: "1.12rem", sample: "Section heading", note: "Section" },
{ token: "--font-size-md", rem: "0.94rem", sample: "Body — primary interface size", note: "Body" },
{ token: "--font-size-sm", rem: "0.85rem", sample: "Secondary label", note: "Meta" },
{ token: "--font-size-xs", rem: "0.78rem", sample: "Caption · nav", note: "Dense" },
]
: [
{ token: "--font-size-xxl", rem: "1.85rem", sample: "The Open-Source CRM", note: "Hero titles" },
{ token: "--font-size-xl", rem: "1.54rem", sample: "People & Companies", note: "Section titles" },
{ token: "--font-size-lg", rem: "1.23rem", sample: "Section heading", note: "Card headers" },
{ token: "--font-size-md", rem: "1rem", sample: "Body text — primary interface size", note: "Default UI copy" },
{ token: "--font-size-sm", rem: "0.92rem", sample: "Secondary label · timestamp", note: "Meta" },
{ token: "--font-size-xs", rem: "0.85rem", sample: "Caption · nav item", note: "Dense UI" },
];
const rowPad = densityPad(tokens, 12);
return (
<div style={{ fontFamily: tokens.fontFamily, display: "flex", flexDirection: "column", gap: 0 }}>
{scale.map((r) => (
<div
key={r.token}
style={{
display: "grid",
gridTemplateColumns: "160px 1fr",
gap: 16,
padding: `${rowPad}px 0`,
borderBottom: "1px solid #f4f4f5",
}}
>
<div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
<code style={codeSm}>{r.token}</code>
<span style={{ color: "#a1a1aa", fontSize: "0.7rem" }}>{r.rem}</span>
</div>
<div style={{ display: "flex", flexDirection: "column", justifyContent: "center" }}>
<span style={{ fontSize: r.rem, fontWeight: 500, color: "#18181b" }}>{r.sample}</span>
<span style={{ fontSize: "0.7rem", color: "#a1a1aa", marginTop: 4 }}>{r.note}</span>
</div>
</div>
))}
</div>
);
}
export function AccentScalePanel({ tokens }: { tokens: ResolvedKitTokens }) {
const a = tokens.accentScale;
const md = tokens.radiusMdPx;
return (
<div style={{ fontFamily: tokens.fontFamily }}>
<SwatchRow colors={a} labels={{ 4: "primary", 8: "brand", 10: "text" }} />
<div style={{ display: "flex", flexWrap: "wrap", gap: 10, marginTop: 16 }}>
<button
type="button"
style={{
padding: "8px 14px",
borderRadius: md,
background: a[8] ?? tokens.accentHex,
color: "#fff",
border: "none",
fontSize: "0.8rem",
fontWeight: 500,
}}
>
Primary CTA
</button>
<button
type="button"
style={{
padding: "8px 14px",
borderRadius: md,
background: a[2],
color: a[10],
border: `1px solid ${a[6]}`,
fontSize: "0.8rem",
fontWeight: 500,
}}
>
Secondary
</button>
<span style={{ fontSize: "0.8rem", color: a[8], textDecoration: "underline", alignSelf: "center" }}>
Link · accent {tokens.accentHex}
</span>
</div>
</div>
);
}
export function GrayScalePanel({ tokens }: { tokens: ResolvedKitTokens }) {
return (
<div style={{ fontFamily: tokens.fontFamily }}>
<SwatchRow colors={tokens.grayScale} />
<p style={{ fontSize: "0.72rem", color: "#71717a", marginTop: 12, lineHeight: 1.55 }}>
Neutrals for surfaces & typography pair with your accent ramp above.
</p>
</div>
);
}
export function SemanticColorsPanel({ tokens }: { tokens: ResolvedKitTokens }) {
const named = [
"red",
"orange",
"yellow",
"grass",
"green",
"cyan",
"blue",
"purple",
"crimson",
"amber",
"lime",
"jade",
"mint",
"iris",
"plum",
"pink",
];
const hues = [
"#ef4444",
"#f97316",
"#eab308",
"#84cc16",
"#22c55e",
"#06b6d4",
"#3b82f6",
"#a855f7",
"#e11d48",
"#f59e0b",
"#65a30d",
"#059669",
"#14b8a6",
"#6366f1",
"#9333ea",
"#ec4899",
];
return (
<div style={{ fontFamily: tokens.fontFamily }}>
<div style={{ display: "grid", gridTemplateColumns: "repeat(8, 1fr)", gap: 8 }}>
{named.map((n, i) => (
<div key={n} style={{ textAlign: "center" }}>
<div style={{ height: 36, borderRadius: tokens.radiusSm, background: hues[i] }} />
<div style={{ fontSize: "0.62rem", color: "#71717a", marginTop: 4 }}>{n}</div>
</div>
))}
</div>
<div style={{ display: "flex", gap: 8, marginTop: 14, flexWrap: "wrap" }}>
{[
["Danger", "#fef2f2", "#dc2626"],
["Success", "#f0fdf4", "#16a34a"],
["Warning", "#fffbeb", "#ca8a04"],
["Info", "#eff6ff", "#2563eb"],
].map(([label, bg, fg]) => (
<span
key={label}
style={{
padding: "4px 10px",
borderRadius: 999,
background: bg as string,
color: fg as string,
fontSize: "0.72rem",
fontWeight: 600,
}}
>
{label}
</span>
))}
</div>
<p style={{ fontSize: "0.7rem", color: "#a1a1aa", marginTop: 12 }}>Reference hues for tags & status (not overridden by accent).</p>
</div>
);
}
export function RadiusPanel({ tokens }: { tokens: ResolvedKitTokens }) {
const rows = [
{ id: "radius-xs", px: tokens.radiusXs, use: "micro chips" },
{ id: "radius-sm", px: tokens.radiusSm, use: "inputs, buttons" },
{ id: "radius-md", px: tokens.radiusMdPx, use: "cards, dropdowns" },
{ id: "radius-xl", px: tokens.radiusXl, use: "large panels" },
{ id: "radius-xxl", px: tokens.radiusXxl, use: "hero surfaces" },
{ id: "radius-pill", px: 999, use: "tags" },
{ id: "radius-full", px: 100, use: "avatars" },
];
const fill = tokens.accentScale[3] ?? "#eceeff";
return (
<div style={{ display: "flex", flexWrap: "wrap", gap: 14, alignItems: "flex-end", fontFamily: tokens.fontFamily }}>
{rows.map((x) => (
<div key={x.id} style={{ textAlign: "center", width: 72 }}>
<div
style={{
width: 56,
height: 40,
margin: "0 auto",
background: fill,
borderRadius: x.px >= 100 ? "50%" : x.px,
}}
/>
<div style={{ fontSize: "0.62rem", fontWeight: 600, marginTop: 6, color: "#3f3f46" }}>{x.id}</div>
<div style={{ fontSize: "0.62rem", color: "#a1a1aa" }}>{x.use}</div>
</div>
))}
</div>
);
}
export function ShadowsPanel({ tokens }: { tokens: ResolvedKitTokens }) {
const focus = tokens.accentHex;
const cards = [
{ id: "shadow-light", desc: "Buttons, rows", shadow: "0 1px 2px rgba(0,0,0,0.05)" },
{ id: "shadow-strong", desc: "Dropdowns", shadow: "0 4px 16px rgba(0,0,0,0.08)" },
{ id: "shadow-super-heavy", desc: "Modals", shadow: "0 12px 48px rgba(0,0,0,0.12)" },
{ id: "shadow-underline", desc: "Focus line", shadow: `inset 0 -2px 0 ${focus}` },
];
return (
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(140px, 1fr))", gap: 12, fontFamily: tokens.fontFamily }}>
{cards.map((c) => (
<div key={c.id} style={{ padding: 14, borderRadius: tokens.radiusMdPx, background: "#fff", boxShadow: c.shadow, border: "1px solid #f4f4f5" }}>
<div style={{ fontSize: "0.72rem", fontWeight: 600, color: "#18181b" }}>{c.id}</div>
<div style={{ fontSize: "0.65rem", color: "#71717a", marginTop: 6 }}>{c.desc}</div>
</div>
))}
</div>
);
}
export function SpacingPanel({ tokens }: { tokens: ResolvedKitTokens }) {
const bar = tokens.accentScale[5] ?? "#9aa6ff";
const steps = [
[1, 4, "sibling gap min"],
[2, 8, "button padding"],
[3, 12, "sidebar gap"],
[4, 16, "section padding"],
[6, 24, "card padding"],
[8, 32, "large gap"],
[12, 48, "table row"],
[16, 64, "section spacer"],
];
return (
<div style={{ fontFamily: tokens.fontFamily }}>
<div style={{ fontSize: "0.65rem", fontWeight: 700, letterSpacing: "0.06em", color: "#a1a1aa", marginBottom: 12 }}>
SPACING 4PX BASE · space(n) = n × 4px
</div>
{steps.map(([n, px, note]) => (
<div key={n} style={{ display: "flex", alignItems: "center", gap: 12, marginBottom: 8 }}>
<code style={{ ...codeSm, width: 72 }}>space({n})</code>
<span style={{ fontSize: "0.72rem", color: "#71717a", width: 36 }}>{px}px</span>
<div style={{ flex: 1, height: 8, background: "#f4f4f5", borderRadius: 4, overflow: "hidden" }}>
<div style={{ width: Math.min((px as number) * 2, 200), height: "100%", background: bar }} />
</div>
<span style={{ fontSize: "0.72rem", color: "#71717a", flex: 1 }}>{note}</span>
</div>
))}
</div>
);
}
export function ButtonsPanel({ tokens }: { tokens: ResolvedKitTokens }) {
const a = tokens.accentScale;
const md = tokens.radiusMdPx;
const accent = a[8] ?? tokens.accentHex;
return (
<div style={{ display: "flex", flexDirection: "column", gap: 14, fontFamily: tokens.fontFamily }}>
<div style={panelMiniTitle}>Primary</div>
<div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
<button type="button" style={{ padding: "8px 16px", borderRadius: md, background: "#18181b", color: "#fff", border: "none", fontSize: "0.8rem" }}>
Continue
</button>
<button type="button" style={{ padding: "8px 16px", borderRadius: md, background: accent, color: "#fff", border: "none", fontSize: "0.8rem" }}>
Accent
</button>
<button type="button" style={{ padding: "8px 16px", borderRadius: md, background: "#dc2626", color: "#fff", border: "none", fontSize: "0.8rem" }}>
Danger
</button>
<button type="button" disabled style={{ padding: "8px 16px", borderRadius: md, background: "#e4e4e7", color: "#a1a1aa", border: "none", fontSize: "0.8rem" }}>
Disabled
</button>
</div>
<div style={panelMiniTitle}>Secondary · outlined</div>
<div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
<button type="button" style={{ padding: "8px 16px", borderRadius: md, background: "#fff", border: "1px solid #e4e4e7", fontSize: "0.8rem" }}>
Default
</button>
<button type="button" style={{ padding: "8px 16px", borderRadius: md, background: "#fff", border: `1px solid ${accent}`, color: accent, fontSize: "0.8rem" }}>
Accent outline
</button>
</div>
<div style={panelMiniTitle}>Tertiary · text</div>
<div style={{ display: "flex", gap: 16 }}>
<button type="button" style={{ background: "none", border: "none", fontSize: "0.8rem", color: "#18181b", cursor: "pointer" }}>
Cancel
</button>
<button type="button" style={{ background: "none", border: "none", fontSize: "0.8rem", color: accent, cursor: "pointer" }}>
Save changes
</button>
</div>
</div>
);
}
function Field({ label, children }: { label: string; children: ReactNode }) {
return (
<div>
<div style={{ fontSize: "0.68rem", fontWeight: 500, color: "#71717a", marginBottom: 6 }}>{label}</div>
{children}
</div>
);
}
export function InputsPanel({ tokens }: { tokens: ResolvedKitTokens }) {
const accent = tokens.accentHex;
const md = tokens.radiusMdPx;
return (
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(168px, 1fr))", gap: 14, fontFamily: tokens.fontFamily }}>
<Field label="Default">
<input placeholder="Type something…" style={{ ...inputBase, borderRadius: md }} />
</Field>
<Field label="Focused">
<input defaultValue="Acme Corporation" style={{ ...inputBase, borderRadius: md, outline: `2px solid ${accent}`, borderColor: accent }} />
</Field>
<Field label="Error">
<input defaultValue="invalid@" style={{ ...inputBase, borderRadius: md, borderColor: "#fca5a5", boxShadow: "0 0 0 3px rgba(252,165,165,0.35)" }} />
</Field>
<Field label="Disabled">
<input defaultValue="Read only" disabled style={{ ...inputBase, borderRadius: md, opacity: 0.55, background: "#fafafa" }} />
</Field>
<Field label="Search">
<input placeholder="Search…" style={{ ...inputBase, borderRadius: md }} />
</Field>
<Field label="Multiline">
<textarea placeholder="Add a note…" rows={3} style={{ ...inputBase, borderRadius: md, resize: "vertical" }} />
</Field>
</div>
);
}
export function NavPanel({ tokens }: { tokens: ResolvedKitTokens }) {
const rs = tokens.radiusSm;
const pad = tokens.density === "compact" ? 6 : 8;
const Item = ({ active, children }: { active?: boolean; children: ReactNode }) => (
<div
style={{
padding: `${pad}px 10px`,
borderRadius: rs,
background: active ? "#ebebeb" : "transparent",
fontSize: tokens.density === "compact" ? "0.76rem" : "0.8rem",
color: active ? "#18181b" : "#52525b",
fontWeight: active ? 600 : 400,
fontFamily: tokens.fontFamily,
}}
>
{children}
</div>
);
return (
<div style={{ display: "grid", gridTemplateColumns: "220px 1fr", gap: 24 }}>
<div
style={{
background: tokens.grayScale[2],
borderRadius: tokens.radiusMdPx,
border: `1px solid ${tokens.grayScale[4]}`,
padding: 10,
fontFamily: tokens.fontFamily,
}}
>
<div style={{ fontSize: "0.62rem", fontWeight: 700, color: "#a1a1aa", letterSpacing: "0.06em", margin: "4px 8px 8px" }}>MAIN</div>
<Item active>People</Item>
<Item>Companies</Item>
<Item>
Opportunities{" "}
<span style={{ float: "right", fontSize: "0.65rem", background: "#e4e4e7", padding: "1px 6px", borderRadius: 999 }}>12</span>
</Item>
<Item>Inbox</Item>
<div style={{ fontSize: "0.62rem", fontWeight: 700, color: "#a1a1aa", letterSpacing: "0.06em", margin: "14px 8px 8px" }}>WORKSPACE</div>
<Item>Workflows</Item>
<Item>Tasks</Item>
</div>
<div style={{ fontSize: "0.72rem", color: "#71717a", lineHeight: 1.65, fontFamily: tokens.fontFamily }}>
<div>Drawer preview uses kit neutrals + radius tokens.</div>
<div>Accent {tokens.accentHex} applies to links & focus off-nav.</div>
</div>
</div>
);
}
export function TagsPanel({ tokens }: { tokens: ResolvedKitTokens }) {
const pills = [
["Active", "#dcfce7", "#166534"],
["Closed", "#fee2e2", "#991b1b"],
["In Progress", "#fef9c3", "#854d0e"],
["Draft", tokens.accentScale[2] ?? "#e0e7ff", tokens.accentScale[10] ?? "#3730a3"],
];
return (
<div style={{ fontFamily: tokens.fontFamily }}>
<div style={{ fontSize: "0.72rem", color: "#71717a", marginBottom: 10 }}>Pill tags · radius-full</div>
<div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
{pills.map(([label, bg, fg]) => (
<span
key={label}
style={{
padding: "4px 12px",
borderRadius: 999,
background: bg as string,
color: fg as string,
fontSize: "0.75rem",
fontWeight: 600,
}}
>
{label}
</span>
))}
</div>
</div>
);
}
export function IllustrationsPanel({ tokens }: { tokens: ResolvedKitTokens }) {
const icons = ["user", "mail", "calendar", "file", "link", "tag", "settings", "numbers"];
const r = tokens.radiusMdPx;
return (
<div style={{ fontFamily: tokens.fontFamily }}>
<div style={{ display: "flex", flexWrap: "wrap", gap: 10 }}>
{icons.map((name) => (
<div
key={name}
style={{
width: 52,
height: 52,
borderRadius: r,
border: "1px solid #e4e4e7",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: "0.62rem",
color: "#52525b",
background: "#fafafa",
}}
>
{name}
</div>
))}
</div>
<p style={{ fontSize: "0.7rem", color: "#a1a1aa", marginTop: 12 }}>
Replace with project SVGs · <code style={codeSm}>assets/illustrations/</code>
</p>
</div>
);
}
export function LogoPanel({ tokens }: { tokens: ResolvedKitTokens }) {
const r = tokens.radiusMdPx;
return (
<div style={{ display: "flex", flexWrap: "wrap", gap: 14, fontFamily: tokens.fontFamily }}>
{["Logo light", "Logo dark", "Icon mark", "Workspace"].map((label) => (
<div key={label} style={{ padding: 16, borderRadius: r, border: "1px solid #e4e4e7", background: "#fafafa", minWidth: 120 }}>
<div style={{ fontSize: "0.62rem", color: "#71717a", marginBottom: 10 }}>{label}</div>
<div
style={{
width: 40,
height: 40,
borderRadius: tokens.radiusSm,
background: `linear-gradient(135deg, ${tokens.accentHex}, ${tokens.accentScale[10] ?? "#1a1a1a"})`,
color: "#fff",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontWeight: 700,
fontSize: "0.75rem",
}}
>
V
</div>
</div>
))}
</div>
);
}
const PANEL_COMPONENTS: Record<string, FC<{ tokens: ResolvedKitTokens }>> = {
fontWeights: FontWeightsPanel,
typeScale: TypeScalePanel,
accentScale: AccentScalePanel,
grayScale: GrayScalePanel,
semanticColors: SemanticColorsPanel,
radius: RadiusPanel,
shadows: ShadowsPanel,
spacing: SpacingPanel,
buttons: ButtonsPanel,
inputs: InputsPanel,
nav: NavPanel,
tags: TagsPanel,
illustrations: IllustrationsPanel,
logo: LogoPanel,
};
export function renderKitPanel(panelKey: string, tokens: ResolvedKitTokens): ReactNode {
const C = PANEL_COMPONENTS[panelKey];
if (!C) return <p style={{ color: "#71717a", fontSize: "0.82rem" }}>Unknown panel: {panelKey}</p>;
return <C tokens={tokens} />;
}

View File

@@ -1,233 +0,0 @@
"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>
);
}

View File

@@ -35,7 +35,6 @@ interface RailItem {
const PRIMARY_ITEMS: RailItem[] = [
{ segment: "preview", label: "Preview", Icon: Eye },
{ segment: "plan", label: "Plan", Icon: ClipboardList },
{ segment: "design-system", label: "Design", Icon: Palette },
{ segment: "market", label: "Market", Icon: PlaneTakeoff },
{ segment: "product", label: "Code", Icon: Code2, aliases: ["code"] },
{ segment: "hosting", label: "Hosting", Icon: Globe },