feat(ui): remove design tab from primary navigation
This commit is contained in:
@@ -1,11 +0,0 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* Design systems tab — UI kit / token documentation (reference layouts).
|
||||
* Distinct from /design in (workspace), which is the scaffold & theme studio.
|
||||
*/
|
||||
import { DesignSystemExplorer } from "@/components/project/design-system-explorer";
|
||||
|
||||
export default function DesignSystemPage() {
|
||||
return <DesignSystemExplorer />;
|
||||
}
|
||||
@@ -1,670 +0,0 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* Visual panels for the design kit explorer — all driven by ResolvedKitTokens
|
||||
* so previews stay in sync with the user’s 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} />;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 },
|
||||
|
||||
Reference in New Issue
Block a user