feat(ui): implement split-pane live preview for design systems

This commit is contained in:
2026-05-15 13:56:12 -07:00
parent 688378aa1f
commit 3bed7ff3a9
2 changed files with 186 additions and 731 deletions

View File

@@ -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,
});
}
}

View File

@@ -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<string>(DEFAULT_DESIGN_KIT_ID);
const [perKit, setPerKit] = useState<Record<string, DesignKitOverrides>>({});
const [draft, setDraft] = useState<DesignKitOverrides>({});
const [activeKitId, setActiveKitId] = useState<string>(DEFAULT_DESIGN_KIT_ID);
const [previewKitId, setPreviewKitId] = useState<string>(DEFAULT_DESIGN_KIT_ID);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [openIds, setOpenIds] = useState<Set<string>>(
() => new Set(["c-accent"]),
);
const iframeRef = useRef<HTMLIFrameElement>(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<string, DesignKitOverrides>;
}) => {
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 (
<div
style={{
...page,
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: 12,
}}
>
<Loader2
className="animate-spin"
style={{ width: 22, height: 22, color: "#9c9590" }}
/>
<span style={{ fontSize: "0.85rem", color: "#71717a" }}>
Loading design kit
</span>
<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 style={page}>
<header style={header}>
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
<div style={headerIcon}>
<LayoutGrid size={18} strokeWidth={2} />
</div>
<div>
<h1
style={{
margin: 0,
fontSize: "1.05rem",
fontWeight: 600,
color: "#18181b",
letterSpacing: "-0.02em",
}}
>
{kit.name}
</h1>
<p
style={{
margin: "4px 0 0",
fontSize: "0.78rem",
color: "#71717a",
}}
>
{kit.tagline}
<span
style={{
display: "block",
marginTop: 4,
fontSize: "0.72rem",
color: "#a1a1aa",
}}
>
{UI_FOUNDATION_LABELS[kit.uiFoundation]}
</span>
</p>
</div>
</div>
<div
style={{
display: "flex",
flexWrap: "wrap",
alignItems: "center",
gap: 10,
}}
>
<label
style={{
fontSize: "0.72rem",
color: "#71717a",
display: "flex",
alignItems: "center",
gap: 8,
}}
>
<Palette size={14} />
Starter kit
</label>
<select
value={kitId}
onChange={(e) => {
const next = e.target.value;
void persistKitId(next).catch(() =>
toast.error("Could not switch kit"),
);
}}
style={selectStyle}
>
{STARTER_KITS.map((k) => (
<option key={k.id} value={k.id}>
{k.name}
</option>
))}
</select>
</div>
</header>
{kitId === "flyonui" ? (
<section
style={{
marginBottom: 24,
borderRadius: 12,
overflow: "hidden",
border: "1px solid #e4e4e7",
background: "#fff",
}}
>
<div
style={{
padding: "12px 16px",
borderBottom: "1px solid #e4e4e7",
display: "flex",
gap: 12,
alignItems: "center",
background: "#fafafa",
}}
>
<span
style={{ fontSize: "0.85rem", fontWeight: 500, color: "#18181b" }}
>
FlyonUI Themes:
</span>
{[
"light",
"dark",
"cupcake",
"retro",
"cyberpunk",
"synthwave",
"valentine",
"aqua",
].map((t) => (
<button
key={t}
type="button"
onClick={() =>
iframeRef.current?.contentWindow?.postMessage(
{ type: "SET_THEME", theme: t },
"*",
)
}
style={{
padding: "6px 12px",
borderRadius: 6,
border: "1px solid #e4e4e7",
background: "#fff",
fontSize: "0.75rem",
cursor: "pointer",
color: "#18181b",
}}
>
{t.charAt(0).toUpperCase() + t.slice(1)}
</button>
))}
</div>
<iframe
ref={iframeRef}
src="/preview/daisyui"
style={{
width: "100%",
height: 800,
border: "none",
display: "block",
}}
title="Theme Sandbox"
/>
</section>
) : null}
<aside style={customizeCard}>
<div
style={{
fontSize: "0.72rem",
fontWeight: 700,
letterSpacing: "0.06em",
color: "#71717a",
marginBottom: 14,
}}
>
CUSTOMIZE THIS KIT
</div>
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fill, minmax(200px, 1fr))",
gap: 16,
}}
>
{kit.customizeFields.includes("accent") ? (
<label style={fieldLabel}>
Accent color
<input
type="color"
value={effective.accentHex}
onChange={(e) =>
setDraft((d) => ({ ...d, accentHex: e.target.value }))
}
style={{
width: "100%",
height: 40,
padding: 4,
borderRadius: 8,
border: "1px solid #e4e4e7",
cursor: "pointer",
}}
/>
<input
type="text"
value={
draft.accentHex ??
savedForKit.accentHex ??
kit.defaults.accentHex ??
""
}
onChange={(e) =>
setDraft((d) => ({ ...d, accentHex: e.target.value }))
}
style={textInput}
placeholder="#5f6bf5"
/>
</label>
) : null}
{kit.customizeFields.includes("radius") ? (
<label style={fieldLabel}>
Radius (md) · {effective.radiusMdPx}px
<input
type="range"
min={4}
max={16}
value={effective.radiusMdPx}
onChange={(e) =>
setDraft((d) => ({
...d,
radiusMdPx: Number(e.target.value),
}))
}
style={{ width: "100%", marginTop: 8 }}
/>
</label>
) : null}
{kit.customizeFields.includes("font") ? (
<label style={fieldLabel}>
Font
<select
value={effective.fontPreset}
onChange={(e) =>
setDraft((d) => ({
...d,
fontPreset: e.target
.value as DesignKitOverrides["fontPreset"],
}))
}
style={selectStyleFull}
>
<option value="inter">Inter (app default)</option>
<option value="system">System UI</option>
</select>
</label>
) : null}
{kit.customizeFields.includes("density") ? (
<label style={fieldLabel}>
Density
<select
value={effective.density}
onChange={(e) =>
setDraft((d) => ({
...d,
density: e.target.value as DesignKitOverrides["density"],
}))
}
style={selectStyleFull}
>
<option value="comfortable">Comfortable</option>
<option value="compact">Compact</option>
</select>
</label>
) : null}
</div>
<div
style={{ display: "flex", flexWrap: "wrap", gap: 10, marginTop: 18 }}
>
<button
type="button"
disabled={saving || Object.keys(draft).length === 0}
onClick={() => void persistOverrides()}
style={btnPrimary}
>
<Save size={14} />
Save customization
</button>
<button type="button" onClick={resetToKitDefaults} style={btnGhost}>
<RotateCcw size={14} />
Reset to starter defaults
</button>
</div>
<p
style={{
fontSize: "0.72rem",
color: "#a1a1aa",
marginTop: 14,
lineHeight: 1.5,
}}
>
Each starter kit keeps its own saved overrides. Switch kits anytime
nothing is lost. Next step: export these as CSS variables for codegen
/ dev container.
</p>
</aside>
<div style={{ display: "flex", flexDirection: "column", gap: 28 }}>
{kit.id === "flyonui" ? (
<section>
<div
style={{
display: "flex",
alignItems: "center",
gap: 12,
marginBottom: 12,
flexWrap: "wrap",
}}
>
<span
style={{
fontSize: "0.85rem",
fontWeight: 600,
color: "#18181b",
}}
>
FlyonUI Themes:
</span>
{[
"light",
"dark",
"cupcake",
"retro",
"cyberpunk",
"synthwave",
"valentine",
"aqua",
].map((t) => (
<button
key={t}
type="button"
onClick={() =>
iframeRef.current?.contentWindow?.postMessage(
{ type: "SET_THEME", theme: t },
"*",
)
}
style={{
padding: "6px 12px",
borderRadius: 6,
border: "1px solid #e4e4e7",
background: "#fff",
fontSize: "0.75rem",
cursor: "pointer",
color: "#18181b",
}}
>
{t.charAt(0).toUpperCase() + t.slice(1)}
</button>
))}
</div>
<iframe
ref={iframeRef}
src="/preview/daisyui"
style={{
width: "100%",
height: 800,
border: "1px solid #e4e4e7",
borderRadius: 12,
background: "#fff",
}}
<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">
<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"
/>
</section>
) : null}
{DESIGN_KIT_SECTIONS.map((section) => (
<section key={section.title}>
<h2 style={sectionTitle}>{section.title}</h2>
<div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
{section.items.map((item) => (
<AccordionRow
key={item.id}
open={openIds.has(item.id)}
onToggle={() => toggle(item.id)}
title={item.title}
subtitle={item.subtitle}
>
{renderKitPanel(item.panelKey, tokens)}
</AccordionRow>
))}
</div>
</section>
))}
</div>
</div>
);
}
function AccordionRow({
open,
onToggle,
title,
subtitle,
children,
}: {
open: boolean;
onToggle: () => void;
title: string;
subtitle?: string;
children: ReactNode;
}) {
return (
<div style={accordionOuter}>
<button type="button" onClick={onToggle} style={accordionTrigger}>
<ChevronRight
size={16}
strokeWidth={2}
style={{
color: "#8c8580",
flexShrink: 0,
transform: open ? "rotate(90deg)" : "none",
transition: "transform 0.15s ease",
}}
/>
<div style={{ flex: 1, minWidth: 0, textAlign: "left" }}>
<div
style={{ fontSize: "0.875rem", fontWeight: 600, color: "#18181b" }}
>
{title}
</div>
{subtitle && open ? (
<div
style={{
fontSize: "0.72rem",
color: "#71717a",
marginTop: 4,
fontWeight: 400,
}}
>
{subtitle}
</div>
) : null}
</div>
<span
title="Live preview from your tokens"
style={{
width: 7,
height: 7,
borderRadius: "50%",
background: "#3d5afe",
flexShrink: 0,
}}
/>
</button>
{open ? <div style={accordionBody}>{children}</div> : null}
<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)}
className={`w-full text-left p-3 rounded-lg flex items-start gap-3 transition-colors ${
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">
<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>
</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-3 min-w-0">
<Eye className="w-4 h-4 text-zinc-400" />
<span className="text-sm font-medium text-zinc-900 truncate">
Previewing: {previewKit.name}
</span>
</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'
: 'bg-indigo-600 hover:bg-indigo-700 text-white shadow-sm'
}`}
>
{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">
<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">
{previewKit.id}.design.preview
</div>
</div>
<iframe
key={previewKit.id} // Forces iframe to remount and show loading state if needed
src={`/api/design-systems/${previewKit.id}/preview`}
className="flex-1 w-full h-full bg-white"
sandbox="allow-scripts allow-same-origin"
/>
</div>
</div>
</div>
);
}
const page: CSSProperties = {
flex: 1,
minHeight: 0,
width: "100%",
overflow: "auto",
boxSizing: "border-box",
padding: "28px 36px 48px",
background: "#fafafa",
fontFamily: "var(--font-inter), ui-sans-serif, system-ui, sans-serif",
};
const header: CSSProperties = {
display: "flex",
flexWrap: "wrap",
justifyContent: "space-between",
alignItems: "flex-start",
gap: 16,
marginBottom: 22,
paddingBottom: 20,
borderBottom: "1px solid #e4e4e7",
};
const headerIcon: CSSProperties = {
width: 40,
height: 40,
borderRadius: 10,
background: "#fff",
border: "1px solid #e4e4e7",
display: "flex",
alignItems: "center",
justifyContent: "center",
color: "#52525b",
};
const selectStyle: CSSProperties = {
padding: "8px 12px",
borderRadius: 8,
border: "1px solid #e4e4e7",
fontSize: "0.82rem",
background: "#fff",
color: "#18181b",
minWidth: 220,
};
const customizeCard: CSSProperties = {
marginBottom: 28,
padding: 20,
borderRadius: 12,
border: "1px solid #e4e4e7",
background: "#fff",
boxShadow: "0 1px 3px rgba(0,0,0,0.04)",
};
const fieldLabel: CSSProperties = {
display: "flex",
flexDirection: "column",
gap: 6,
fontSize: "0.72rem",
fontWeight: 600,
color: "#52525b",
};
const textInput: CSSProperties = {
padding: "8px 10px",
borderRadius: 8,
border: "1px solid #e4e4e7",
fontSize: "0.82rem",
fontFamily: "var(--font-ibm-plex-mono), monospace",
};
const selectStyleFull: CSSProperties = {
padding: "8px 12px",
borderRadius: 8,
border: "1px solid #e4e4e7",
fontSize: "0.82rem",
background: "#fff",
color: "#18181b",
minWidth: "100%",
marginTop: 4,
};
const btnPrimary: CSSProperties = {
display: "inline-flex",
alignItems: "center",
gap: 8,
padding: "10px 16px",
borderRadius: 10,
border: "none",
background: "#18181b",
color: "#fff",
fontSize: "0.82rem",
fontWeight: 500,
cursor: "pointer",
};
const btnGhost: CSSProperties = {
display: "inline-flex",
alignItems: "center",
gap: 8,
padding: "10px 16px",
borderRadius: 10,
border: "1px solid #e4e4e7",
background: "#fafafa",
color: "#52525b",
fontSize: "0.82rem",
fontWeight: 500,
cursor: "pointer",
};
const sectionTitle: CSSProperties = {
margin: "0 0 12px",
fontSize: "0.72rem",
fontWeight: 700,
letterSpacing: "0.08em",
color: "#a1a1aa",
textTransform: "uppercase",
};
const accordionOuter: CSSProperties = {
borderRadius: 10,
border: "1px solid #e4e4e7",
background: "#fff",
overflow: "hidden",
};
const accordionTrigger: CSSProperties = {
width: "100%",
display: "flex",
alignItems: "flex-start",
gap: 10,
padding: "14px 16px",
border: "none",
background: "#fff",
cursor: "pointer",
boxSizing: "border-box",
};
const accordionBody: CSSProperties = {
padding: "16px 16px 18px 44px",
borderTop: "1px solid #f4f4f5",
background: "#fafafa",
};