Restyle design page to match Stackless aesthetic
- Replace all Tailwind/shadcn classes with inline styles - Use warm beige palette, Outfit/Newsreader fonts, Stackless card pattern - Replace Lucide icons with simple Unicode glyphs - Surface picker and left nav match the sidebar/activity visual language - Controls bar (library tabs, swatches, lock-in) restyled to match Made-with: Cursor
This commit is contained in:
@@ -1,14 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { use, useState, useEffect } from "react";
|
import { use, useState, useEffect } from "react";
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import {
|
|
||||||
Monitor, Globe, Settings, Smartphone, Mail, BookOpen,
|
|
||||||
Lock, CheckCircle2, Loader2, ChevronRight, Pencil,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { SCAFFOLD_REGISTRY, THEME_REGISTRY, type ThemeColor } from "@/components/design-scaffolds";
|
import { SCAFFOLD_REGISTRY, THEME_REGISTRY, type ThemeColor } from "@/components/design-scaffolds";
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -19,7 +12,7 @@ interface Surface {
|
|||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
icon: React.ElementType;
|
icon: string;
|
||||||
themes: Theme[];
|
themes: Theme[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,269 +22,77 @@ interface Theme {
|
|||||||
description: string;
|
description: string;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
url: string;
|
url: string;
|
||||||
preview: React.ReactNode;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mini UI mockups — each styled to feel like the library
|
|
||||||
const ShadcnPreview = () => (
|
|
||||||
<div className="w-full h-full bg-white border border-zinc-200 rounded-lg p-2.5 flex flex-col gap-2">
|
|
||||||
<div className="flex items-center gap-1.5">
|
|
||||||
<div className="w-3 h-3 rounded bg-zinc-900" />
|
|
||||||
<div className="h-2 w-14 bg-zinc-200 rounded" />
|
|
||||||
</div>
|
|
||||||
<div className="h-px bg-zinc-100" />
|
|
||||||
<div className="space-y-1 flex-1">
|
|
||||||
<div className="h-1.5 w-full bg-zinc-100 rounded" />
|
|
||||||
<div className="h-1.5 w-2/3 bg-zinc-100 rounded" />
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-1.5">
|
|
||||||
<span className="h-5 px-2 bg-zinc-900 text-white rounded text-[8px] flex items-center font-medium">Save</span>
|
|
||||||
<span className="h-5 px-2 border border-zinc-200 rounded text-[8px] flex items-center text-zinc-500">Cancel</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const MantiinePreview = () => (
|
|
||||||
<div className="w-full h-full bg-white border border-zinc-200 rounded-lg p-2.5 flex flex-col gap-2">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="h-2 w-12 bg-zinc-800 rounded" />
|
|
||||||
<span className="h-4 px-1.5 bg-blue-600 text-white rounded text-[7px] flex items-center">New</span>
|
|
||||||
</div>
|
|
||||||
<div className="border border-zinc-200 rounded overflow-hidden">
|
|
||||||
<div className="h-4 bg-zinc-50 border-b border-zinc-200 flex items-center px-1.5 gap-1">
|
|
||||||
<div className="h-1.5 w-8 bg-zinc-300 rounded" />
|
|
||||||
<div className="h-1.5 w-10 bg-zinc-300 rounded ml-auto" />
|
|
||||||
</div>
|
|
||||||
{[1,2].map(i => (
|
|
||||||
<div key={i} className="h-4 border-b border-zinc-100 flex items-center px-1.5 gap-1 last:border-0">
|
|
||||||
<div className="h-1.5 w-12 bg-zinc-200 rounded" />
|
|
||||||
<div className="h-1.5 w-8 bg-zinc-200 rounded ml-auto" />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-1 mt-auto">
|
|
||||||
<span className="h-5 px-2 bg-blue-600 text-white rounded text-[8px] flex items-center">Apply</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const HeroUIPreview = () => (
|
|
||||||
<div className="w-full h-full bg-white rounded-lg p-2.5 flex flex-col gap-2" style={{ background: 'linear-gradient(135deg, #f8faff 0%, #fff 100%)' }}>
|
|
||||||
<div className="flex items-center gap-1.5">
|
|
||||||
<div className="w-3 h-3 rounded-full" style={{ background: 'linear-gradient(135deg, #7c3aed, #ec4899)' }} />
|
|
||||||
<div className="h-2 w-12 bg-zinc-200 rounded" />
|
|
||||||
</div>
|
|
||||||
<div className="rounded-lg p-2 flex-1" style={{ background: 'linear-gradient(135deg, rgba(124,58,237,0.05), rgba(236,72,153,0.05))' }}>
|
|
||||||
<div className="h-1.5 w-full bg-zinc-200 rounded mb-1" />
|
|
||||||
<div className="h-1.5 w-3/4 bg-zinc-200 rounded" />
|
|
||||||
</div>
|
|
||||||
<span className="h-5 px-2 text-white rounded-full text-[8px] flex items-center justify-center self-start font-medium" style={{ background: 'linear-gradient(135deg, #7c3aed, #ec4899)' }}>
|
|
||||||
Get started →
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const DaisyPreview = () => (
|
|
||||||
<div className="w-full h-full bg-[#1d232a] rounded-lg p-2.5 flex flex-col gap-2">
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<div className="w-2 h-2 rounded-full bg-[#f59e0b]" />
|
|
||||||
<div className="w-2 h-2 rounded-full bg-[#10b981]" />
|
|
||||||
<div className="w-2 h-2 rounded-full bg-[#3b82f6]" />
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1 flex-1">
|
|
||||||
<div className="h-1.5 w-full bg-zinc-700 rounded" />
|
|
||||||
<div className="h-1.5 w-5/6 bg-zinc-700 rounded" />
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-1">
|
|
||||||
<span className="h-5 px-2 bg-[#f59e0b] text-black rounded-full text-[8px] flex items-center font-bold">Primary</span>
|
|
||||||
<span className="h-5 px-2 bg-[#10b981] text-black rounded-full text-[8px] flex items-center font-bold">Success</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const AcernityPreview = () => (
|
|
||||||
<div className="w-full h-full bg-zinc-950 rounded-lg p-2.5 flex flex-col gap-2">
|
|
||||||
<div className="h-2 w-20 rounded" style={{ background: 'linear-gradient(90deg, #a855f7, #3b82f6)' }} />
|
|
||||||
<div className="space-y-1 flex-1">
|
|
||||||
<div className="h-1.5 w-full bg-zinc-800 rounded" />
|
|
||||||
<div className="h-1.5 w-2/3 bg-zinc-800 rounded" />
|
|
||||||
</div>
|
|
||||||
<div className="h-5 rounded border flex items-center justify-center" style={{ borderColor: 'rgba(168,85,247,0.4)', background: 'rgba(168,85,247,0.05)' }}>
|
|
||||||
<span className="text-[8px] font-medium" style={{ background: 'linear-gradient(90deg, #a855f7, #3b82f6)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent' }}>Get started</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const TailwindPreview = () => (
|
|
||||||
<div className="w-full h-full bg-white rounded-lg p-2.5 flex flex-col gap-2 border border-zinc-100">
|
|
||||||
<div className="text-[8px] font-mono text-zinc-400">className="flex…"</div>
|
|
||||||
<div className="space-y-1 flex-1">
|
|
||||||
<div className="h-5 w-full bg-gradient-to-r from-violet-100 to-blue-100 rounded flex items-center px-2">
|
|
||||||
<div className="h-1.5 w-10 bg-violet-300 rounded" />
|
|
||||||
</div>
|
|
||||||
<div className="h-5 w-full bg-zinc-100 rounded" />
|
|
||||||
</div>
|
|
||||||
<div className="h-5 bg-zinc-900 rounded flex items-center justify-center">
|
|
||||||
<span className="text-white text-[8px] font-medium">Custom →</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const TremorPreview = () => (
|
|
||||||
<div className="w-full h-full bg-white rounded-lg p-2.5 flex flex-col gap-2 border border-zinc-200">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="h-2 w-14 bg-zinc-800 rounded" />
|
|
||||||
<div className="h-2 w-6 bg-blue-500 rounded" />
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-1 flex-1 items-end">
|
|
||||||
{[60, 80, 45, 90, 70, 55].map((h, i) => (
|
|
||||||
<div key={i} className="flex-1 bg-blue-100 rounded-t" style={{ height: `${h}%` }}>
|
|
||||||
<div className="bg-blue-500 rounded-t w-full" style={{ height: '30%' }} />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const NativewindPreview = () => (
|
|
||||||
<div className="w-full h-full flex items-center justify-center">
|
|
||||||
<div className="w-16 h-24 bg-zinc-900 rounded-xl border-2 border-zinc-700 flex flex-col overflow-hidden">
|
|
||||||
<div className="h-2 bg-zinc-800 flex items-center justify-center">
|
|
||||||
<div className="w-4 h-0.5 bg-zinc-600 rounded-full" />
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 bg-zinc-950 p-1 flex flex-col gap-1">
|
|
||||||
<div className="h-2 w-full bg-zinc-800 rounded" />
|
|
||||||
<div className="h-6 w-full bg-zinc-800 rounded flex items-center justify-center">
|
|
||||||
<div className="h-1 w-6 bg-zinc-600 rounded" />
|
|
||||||
</div>
|
|
||||||
<div className="h-3 w-full bg-violet-600 rounded flex items-center justify-center">
|
|
||||||
<span className="text-white text-[5px]">Button</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const GluestackPreview = () => (
|
|
||||||
<div className="w-full h-full flex items-center justify-center">
|
|
||||||
<div className="w-16 h-24 bg-white rounded-xl border border-zinc-200 flex flex-col overflow-hidden shadow-sm">
|
|
||||||
<div className="h-3 bg-blue-600 flex items-center px-1.5">
|
|
||||||
<div className="h-1 w-6 bg-blue-400 rounded" />
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 p-1 flex flex-col gap-1">
|
|
||||||
<div className="h-2 w-full bg-zinc-100 rounded" />
|
|
||||||
<div className="h-2 w-4/5 bg-zinc-100 rounded" />
|
|
||||||
<div className="mt-auto h-4 w-full bg-blue-600 rounded flex items-center justify-center">
|
|
||||||
<span className="text-white text-[5px] font-bold">Submit</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const ReactEmailPreview = () => (
|
|
||||||
<div className="w-full h-full bg-zinc-50 rounded-lg p-2.5 flex flex-col gap-1.5 border border-zinc-200">
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<div className="w-3 h-3 rounded bg-blue-600" />
|
|
||||||
<div className="h-1.5 w-16 bg-zinc-300 rounded" />
|
|
||||||
</div>
|
|
||||||
<div className="h-px bg-zinc-200" />
|
|
||||||
<div className="h-1.5 w-2/3 bg-zinc-800 rounded" />
|
|
||||||
<div className="space-y-1">
|
|
||||||
<div className="h-1.5 w-full bg-zinc-300 rounded" />
|
|
||||||
<div className="h-1.5 w-5/6 bg-zinc-300 rounded" />
|
|
||||||
<div className="h-1.5 w-4/5 bg-zinc-300 rounded" />
|
|
||||||
</div>
|
|
||||||
<div className="h-5 bg-blue-600 rounded flex items-center justify-center mt-1">
|
|
||||||
<span className="text-white text-[8px] font-medium">Open App →</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const NextraPreview = () => (
|
|
||||||
<div className="w-full h-full bg-white rounded-lg border border-zinc-200 overflow-hidden flex">
|
|
||||||
<div className="w-1/3 bg-zinc-50 border-r border-zinc-200 p-1.5 flex flex-col gap-1">
|
|
||||||
{['Getting started', 'Installation', 'API', 'Examples'].map(l => (
|
|
||||||
<div key={l} className="h-2 bg-zinc-200 rounded" style={{ width: `${60 + Math.random() * 30}%` }} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 p-2 flex flex-col gap-1.5">
|
|
||||||
<div className="h-2 w-3/4 bg-zinc-800 rounded" />
|
|
||||||
<div className="h-1.5 w-full bg-zinc-200 rounded" />
|
|
||||||
<div className="h-1.5 w-5/6 bg-zinc-200 rounded" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const ALL_SURFACES: Surface[] = [
|
const ALL_SURFACES: Surface[] = [
|
||||||
{
|
{
|
||||||
id: "web-app",
|
id: "web-app",
|
||||||
name: "Web App",
|
name: "Web App",
|
||||||
description: "The core product your users log into — dashboards, features, settings",
|
icon: "⬡",
|
||||||
icon: Monitor,
|
description: "The core product users log into — dashboards, features, settings",
|
||||||
themes: [
|
themes: [
|
||||||
{ id: "shadcn", name: "shadcn/ui", description: "Copy-paste components on Radix primitives. You own the code, fully customisable.", tags: ["Tailwind", "Radix", "Copy-paste"], url: "https://ui.shadcn.com", preview: <ShadcnPreview /> },
|
{ id: "shadcn", name: "shadcn/ui", description: "Copy-paste components on Radix primitives. You own the code, fully customisable.", tags: ["Tailwind", "Radix", "Copy-paste"], url: "https://ui.shadcn.com" },
|
||||||
{ id: "mantine", name: "Mantine", description: "100+ components with hooks, forms, charts. Best for data-heavy apps.", tags: ["React", "Charts", "Forms"], url: "https://mantine.dev", preview: <MantiinePreview /> },
|
{ id: "mantine", name: "Mantine", description: "100+ components with hooks, forms, charts. Best for data-heavy apps.", tags: ["React", "Charts", "Forms"], url: "https://mantine.dev" },
|
||||||
{ id: "hero-ui", name: "HeroUI", description: "Beautiful, accessible components with smooth animations and dark mode.", tags: ["Tailwind", "Animations", "Accessible"], url: "https://heroui.com", preview: <HeroUIPreview /> },
|
{ id: "hero-ui", name: "HeroUI", description: "Beautiful, accessible components with smooth animations and dark mode.", tags: ["Tailwind", "Animations", "Accessible"], url: "https://heroui.com" },
|
||||||
{ id: "tremor", name: "Tremor", description: "Dashboard components — charts, KPIs, tables — designed for analytics UIs.", tags: ["Charts", "Dashboard", "Analytics"], url: "https://tremor.so", preview: <TremorPreview /> },
|
{ id: "tremor", name: "Tremor", description: "Dashboard components — charts, KPIs, tables — designed for analytics UIs.", tags: ["Charts", "Dashboard", "Analytics"], url: "https://tremor.so" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "marketing",
|
id: "marketing",
|
||||||
name: "Marketing Site",
|
name: "Marketing Site",
|
||||||
|
icon: "◎",
|
||||||
description: "Public-facing landing page, blog, pricing — brand expression and conversion",
|
description: "Public-facing landing page, blog, pricing — brand expression and conversion",
|
||||||
icon: Globe,
|
|
||||||
themes: [
|
themes: [
|
||||||
{ id: "daisy-ui", name: "DaisyUI", description: "Tailwind plugin with 48 built-in themes. Fastest path to a beautiful site.", tags: ["Tailwind", "Themes", "Plugin"], url: "https://daisyui.com", preview: <DaisyPreview /> },
|
{ id: "daisy-ui", name: "DaisyUI", description: "Tailwind plugin with 48 built-in themes. Fastest path to a beautiful site.", tags: ["Tailwind", "Themes", "Plugin"], url: "https://daisyui.com" },
|
||||||
{ id: "hero-ui", name: "HeroUI", description: "Beautiful components with gradients and smooth animations.", tags: ["Tailwind", "Animations", "Modern"], url: "https://heroui.com", preview: <HeroUIPreview /> },
|
{ id: "hero-ui", name: "HeroUI", description: "Beautiful components with gradients and smooth animations.", tags: ["Tailwind", "Animations", "Modern"], url: "https://heroui.com" },
|
||||||
{ id: "aceternity", name: "Aceternity UI", description: "Animated, visually striking components for premium landing pages.", tags: ["Animations", "Dark", "Premium"], url: "https://ui.aceternity.com", preview: <AcernityPreview /> },
|
{ id: "aceternity", name: "Aceternity UI", description: "Animated, visually striking components for premium landing pages.", tags: ["Animations", "Dark", "Premium"], url: "https://ui.aceternity.com" },
|
||||||
{ id: "tailwind-only", name: "Tailwind only", description: "No component library — full creative control with pure Tailwind CSS.", tags: ["Custom", "Flexible", "Minimal"], url: "https://tailwindcss.com", preview: <TailwindPreview /> },
|
{ id: "tailwind-only", name: "Tailwind only", description: "No component library — full creative control with pure Tailwind CSS.", tags: ["Custom", "Flexible", "Minimal"], url: "https://tailwindcss.com" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "admin",
|
id: "admin",
|
||||||
name: "Admin Panel",
|
name: "Admin Panel",
|
||||||
|
icon: "◫",
|
||||||
description: "Internal tool for managing your business — users, support, billing, analytics",
|
description: "Internal tool for managing your business — users, support, billing, analytics",
|
||||||
icon: Settings,
|
|
||||||
themes: [
|
themes: [
|
||||||
{ id: "mantine", name: "Mantine", description: "The best choice for admin — comprehensive tables, forms, and data components.", tags: ["Tables", "Forms", "Charts"], url: "https://mantine.dev", preview: <MantiinePreview /> },
|
{ id: "mantine", name: "Mantine", description: "The best choice for admin — comprehensive tables, forms, and data components.", tags: ["Tables", "Forms", "Charts"], url: "https://mantine.dev" },
|
||||||
{ id: "shadcn", name: "shadcn/ui", description: "Clean, neutral components. Great if you want the admin to match the main app.", tags: ["Tailwind", "Consistent", "Clean"], url: "https://ui.shadcn.com", preview: <ShadcnPreview /> },
|
{ id: "shadcn", name: "shadcn/ui", description: "Clean, neutral components. Great if you want the admin to match the main app.", tags: ["Tailwind", "Consistent", "Clean"], url: "https://ui.shadcn.com" },
|
||||||
{ id: "tremor", name: "Tremor", description: "Analytics-first — built for KPI dashboards, charts, and data tables.", tags: ["Analytics", "Charts", "KPIs"], url: "https://tremor.so", preview: <TremorPreview /> },
|
{ id: "tremor", name: "Tremor", description: "Analytics-first — built for KPI dashboards, charts, and data tables.", tags: ["Analytics", "Charts", "KPIs"], url: "https://tremor.so" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "mobile",
|
id: "mobile",
|
||||||
name: "Mobile App",
|
name: "Mobile App",
|
||||||
|
icon: "▢",
|
||||||
description: "iOS and Android companion app — touch-first, native feel",
|
description: "iOS and Android companion app — touch-first, native feel",
|
||||||
icon: Smartphone,
|
|
||||||
themes: [
|
themes: [
|
||||||
{ id: "nativewind", name: "NativeWind", description: "Use Tailwind CSS in React Native. Consistent style across web and mobile.", tags: ["Tailwind", "React Native", "Expo"], url: "https://nativewind.dev", preview: <NativewindPreview /> },
|
{ id: "nativewind", name: "NativeWind", description: "Use Tailwind CSS in React Native. Consistent style across web and mobile.", tags: ["Tailwind", "React Native", "Expo"], url: "https://nativewind.dev" },
|
||||||
{ id: "gluestack", name: "Gluestack UI", description: "Universal components for React Native — accessible, well-tested, comprehensive.", tags: ["Universal", "Accessible", "Expo"], url: "https://gluestack.io", preview: <GluestackPreview /> },
|
{ id: "gluestack", name: "Gluestack UI", description: "Universal components for React Native — accessible, well-tested, comprehensive.", tags: ["Universal", "Accessible", "Expo"], url: "https://gluestack.io" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "email",
|
id: "email",
|
||||||
name: "Email",
|
name: "Email",
|
||||||
|
icon: "✉",
|
||||||
description: "Transactional and marketing emails — welcome, billing, notifications",
|
description: "Transactional and marketing emails — welcome, billing, notifications",
|
||||||
icon: Mail,
|
|
||||||
themes: [
|
themes: [
|
||||||
{ id: "react-email", name: "React Email", description: "Build emails with React components. Works with any email provider.", tags: ["React", "Resend", "Cross-client"], url: "https://react.email", preview: <ReactEmailPreview /> },
|
{ id: "react-email", name: "React Email", description: "Build emails with React components. Works with any email provider.", tags: ["React", "Resend", "Cross-client"], url: "https://react.email" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "docs",
|
id: "docs",
|
||||||
name: "Docs / Content",
|
name: "Docs / Content",
|
||||||
|
icon: "☰",
|
||||||
description: "Documentation, knowledge base, or blog for your product",
|
description: "Documentation, knowledge base, or blog for your product",
|
||||||
icon: BookOpen,
|
|
||||||
themes: [
|
themes: [
|
||||||
{ id: "nextra", name: "Nextra", description: "Next.js-based docs site. Markdown-first, fast, with great search.", tags: ["Next.js", "Markdown", "Search"], url: "https://nextra.site", preview: <NextraPreview /> },
|
{ id: "nextra", name: "Nextra", description: "Next.js-based docs site. Markdown-first, fast, with great search.", tags: ["Next.js", "Markdown", "Search"], url: "https://nextra.site" },
|
||||||
{ id: "shadcn", name: "shadcn/ui + custom", description: "Build a fully custom docs site that matches your product exactly.", tags: ["Custom", "Tailwind", "Flexible"], url: "https://ui.shadcn.com", preview: <ShadcnPreview /> },
|
{ id: "shadcn", name: "shadcn/ui + custom", description: "Build a fully custom docs site that matches your product exactly.", tags: ["Custom", "Tailwind", "Flexible"], url: "https://ui.shadcn.com" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Surface section — tab toggle + scaffold preview + lock in
|
// Surface section
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
function SurfaceSection({
|
function SurfaceSection({
|
||||||
@@ -311,38 +112,54 @@ function SurfaceSection({
|
|||||||
onUnlock: () => void;
|
onUnlock: () => void;
|
||||||
saving: boolean;
|
saving: boolean;
|
||||||
}) {
|
}) {
|
||||||
// Active preview tab — if locked show that, otherwise the selected/first
|
|
||||||
const previewId = lockedThemeId ?? selectedThemeId ?? surface.themes[0]?.id ?? null;
|
const previewId = lockedThemeId ?? selectedThemeId ?? surface.themes[0]?.id ?? null;
|
||||||
const activeTheme = surface.themes.find(t => t.id === previewId);
|
const activeTheme = surface.themes.find(t => t.id === previewId);
|
||||||
const ScaffoldComponent = previewId ? SCAFFOLD_REGISTRY[surface.id]?.[previewId] : null;
|
const ScaffoldComponent = previewId ? SCAFFOLD_REGISTRY[surface.id]?.[previewId] : null;
|
||||||
|
|
||||||
// Theme color variants for the active library (e.g. shadcn has 8 color themes)
|
|
||||||
const availableColorThemes: ThemeColor[] = previewId
|
const availableColorThemes: ThemeColor[] = previewId
|
||||||
? (THEME_REGISTRY[surface.id]?.[previewId] ?? [])
|
? (THEME_REGISTRY[surface.id]?.[previewId] ?? [])
|
||||||
: [];
|
: [];
|
||||||
const [selectedColorTheme, setSelectedColorTheme] = useState<ThemeColor | null>(null);
|
const [selectedColorTheme, setSelectedColorTheme] = useState<ThemeColor | null>(null);
|
||||||
const activeColorTheme = selectedColorTheme ?? availableColorThemes[0] ?? null;
|
const activeColorTheme = selectedColorTheme ?? availableColorThemes[0] ?? null;
|
||||||
|
|
||||||
return (
|
const isLocked = !!lockedThemeId;
|
||||||
<div className="flex flex-col h-full gap-0">
|
|
||||||
|
|
||||||
{/* Scaffold preview — browser chrome frame */}
|
return (
|
||||||
<div className="flex-1 rounded-xl overflow-hidden border" style={{ minHeight: 460 }}>
|
<div style={{ display: "flex", flexDirection: "column", height: "100%", gap: 0 }}>
|
||||||
<div className="flex items-center gap-1.5 px-3 h-8 border-b bg-muted/50 shrink-0">
|
|
||||||
<div className="w-2.5 h-2.5 rounded-full bg-zinc-300" />
|
{/* Browser chrome + scaffold */}
|
||||||
<div className="w-2.5 h-2.5 rounded-full bg-zinc-300" />
|
<div style={{
|
||||||
<div className="w-2.5 h-2.5 rounded-full bg-zinc-300" />
|
flex: 1, borderRadius: 10, overflow: "hidden",
|
||||||
<div className="ml-3 flex-1 max-w-xs h-5 rounded-md bg-border/60 flex items-center px-2">
|
border: "1px solid #e8e4dc",
|
||||||
<span className="text-[9px] text-muted-foreground truncate">
|
boxShadow: "0 1px 4px #1a1a1a06",
|
||||||
{activeTheme ? `/${surface.id} — ${activeTheme.name}${activeColorTheme ? ` / ${activeColorTheme.label}` : ""}` : ""}
|
display: "flex", flexDirection: "column",
|
||||||
|
minHeight: 460,
|
||||||
|
}}>
|
||||||
|
{/* Chrome bar */}
|
||||||
|
<div style={{
|
||||||
|
display: "flex", alignItems: "center", gap: 6,
|
||||||
|
padding: "0 14px", height: 34, flexShrink: 0,
|
||||||
|
borderBottom: "1px solid #e8e4dc", background: "#faf8f5",
|
||||||
|
}}>
|
||||||
|
{["#d0ccc4", "#d0ccc4", "#d0ccc4"].map((c, i) => (
|
||||||
|
<div key={i} style={{ width: 9, height: 9, borderRadius: "50%", background: c }} />
|
||||||
|
))}
|
||||||
|
<div style={{
|
||||||
|
marginLeft: 12, flex: 1, maxWidth: 280, height: 20, borderRadius: 5,
|
||||||
|
background: "#f0ece4", display: "flex", alignItems: "center", padding: "0 10px",
|
||||||
|
}}>
|
||||||
|
<span style={{ fontSize: "0.65rem", color: "#b5b0a6", fontFamily: "IBM Plex Mono", overflow: "hidden", whiteSpace: "nowrap", textOverflow: "ellipsis" }}>
|
||||||
|
{activeTheme ? `/${surface.id} · ${activeTheme.name}${activeColorTheme ? ` · ${activeColorTheme.label}` : ""}` : ""}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ height: 460 }}>
|
|
||||||
|
{/* Scaffold */}
|
||||||
|
<div style={{ flex: 1, overflow: "hidden" }}>
|
||||||
{ScaffoldComponent
|
{ScaffoldComponent
|
||||||
? <ScaffoldComponent themeColor={activeColorTheme ?? undefined} />
|
? <ScaffoldComponent themeColor={activeColorTheme ?? undefined} />
|
||||||
: (
|
: (
|
||||||
<div className="h-full flex items-center justify-center text-muted-foreground text-xs">
|
<div style={{ height: "100%", display: "flex", alignItems: "center", justifyContent: "center", color: "#b5b0a6", fontSize: "0.82rem", fontFamily: "Outfit" }}>
|
||||||
Select a library below to preview
|
Select a library below to preview
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -350,89 +167,123 @@ function SurfaceSection({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Controls bar — all below the render */}
|
{/* Controls below render */}
|
||||||
<div className="shrink-0 pt-4 space-y-3">
|
<div style={{ flexShrink: 0, paddingTop: 18, display: "flex", flexDirection: "column", gap: 14 }}>
|
||||||
|
|
||||||
{/* Row 1: library tabs */}
|
{/* Library tabs */}
|
||||||
<div className="flex items-center gap-1.5 flex-wrap">
|
<div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
|
||||||
{surface.themes.map(theme => {
|
{surface.themes.map(theme => {
|
||||||
const isActive = theme.id === previewId;
|
const isActive = theme.id === previewId;
|
||||||
const isLocked = theme.id === lockedThemeId;
|
const isThisLocked = theme.id === lockedThemeId;
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={theme.id}
|
key={theme.id}
|
||||||
onClick={() => { if (!lockedThemeId) { onSelect(theme.id); setSelectedColorTheme(null); } }}
|
onClick={() => { if (!isLocked) { onSelect(theme.id); setSelectedColorTheme(null); } }}
|
||||||
disabled={!!lockedThemeId && !isLocked}
|
disabled={isLocked && !isThisLocked}
|
||||||
className={cn(
|
style={{
|
||||||
"flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-medium transition-all border",
|
display: "flex", alignItems: "center", gap: 5,
|
||||||
isActive
|
padding: "6px 14px", borderRadius: 6, border: "1px solid",
|
||||||
? "bg-foreground text-background border-foreground"
|
fontSize: "0.78rem", fontWeight: isActive ? 600 : 500,
|
||||||
: lockedThemeId
|
fontFamily: "Outfit", cursor: isLocked && !isThisLocked ? "not-allowed" : "pointer",
|
||||||
? "opacity-30 border-transparent text-muted-foreground cursor-not-allowed"
|
transition: "all 0.12s",
|
||||||
: "border-border text-muted-foreground hover:border-foreground/40 hover:text-foreground"
|
borderColor: isActive ? "#1a1a1a" : "#e0dcd4",
|
||||||
)}
|
background: isActive ? "#1a1a1a" : "#fff",
|
||||||
|
color: isActive ? "#fff" : isLocked && !isThisLocked ? "#c5c0b8" : "#6b6560",
|
||||||
|
opacity: isLocked && !isThisLocked ? 0.4 : 1,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{isLocked && <Lock className="w-2.5 h-2.5" />}
|
{isThisLocked && <span style={{ fontSize: "0.65rem" }}>🔒</span>}
|
||||||
{theme.name}
|
{theme.name}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Row 2: color theme swatches (only if this library has color variants) */}
|
{/* Color swatches */}
|
||||||
{availableColorThemes.length > 0 && (
|
{availableColorThemes.length > 0 && (
|
||||||
<div className="flex items-center gap-2">
|
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
||||||
<span className="text-[10px] text-muted-foreground shrink-0">Theme</span>
|
<span style={{ fontSize: "0.68rem", fontWeight: 600, color: "#a09a90", textTransform: "uppercase", letterSpacing: "0.08em", fontFamily: "Outfit" }}>Theme</span>
|
||||||
<div className="flex items-center gap-1.5 flex-wrap">
|
<div style={{ display: "flex", gap: 6, flexWrap: "wrap", alignItems: "center" }}>
|
||||||
{availableColorThemes.map(ct => (
|
{availableColorThemes.map(ct => (
|
||||||
<button
|
<button
|
||||||
key={ct.id}
|
key={ct.id}
|
||||||
title={ct.label}
|
title={ct.label}
|
||||||
onClick={() => !lockedThemeId && setSelectedColorTheme(ct)}
|
onClick={() => !isLocked && setSelectedColorTheme(ct)}
|
||||||
disabled={!!lockedThemeId}
|
disabled={isLocked}
|
||||||
className="w-5 h-5 rounded-full transition-transform hover:scale-110 disabled:opacity-40"
|
|
||||||
style={{
|
style={{
|
||||||
background: ct.bg
|
width: 20, height: 20, borderRadius: "50%", border: "none", cursor: isLocked ? "not-allowed" : "pointer",
|
||||||
? `linear-gradient(135deg, ${ct.bg} 50%, ${ct.primary} 50%)`
|
background: ct.bg ? `linear-gradient(135deg, ${ct.bg} 50%, ${ct.primary} 50%)` : ct.primary,
|
||||||
: ct.primary,
|
|
||||||
outline: activeColorTheme?.id === ct.id ? `2px solid ${ct.primary}` : "none",
|
outline: activeColorTheme?.id === ct.id ? `2px solid ${ct.primary}` : "none",
|
||||||
outlineOffset: 2,
|
outlineOffset: 2, transition: "transform 0.12s",
|
||||||
|
opacity: isLocked ? 0.4 : 1,
|
||||||
}}
|
}}
|
||||||
|
onMouseEnter={e => { if (!isLocked) (e.currentTarget as HTMLElement).style.transform = "scale(1.15)"; }}
|
||||||
|
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.transform = "scale(1)"; }}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{activeColorTheme && (
|
{activeColorTheme && (
|
||||||
<span className="text-[10px] text-muted-foreground ml-1">{activeColorTheme.label}</span>
|
<span style={{ fontSize: "0.72rem", color: "#a09a90", fontFamily: "Outfit" }}>{activeColorTheme.label}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Row 3: description + tags + docs + lock in */}
|
{/* Description + tags + docs + lock-in */}
|
||||||
<div className="flex items-center gap-3">
|
<div style={{ display: "flex", alignItems: "center", gap: 12, paddingTop: 4, borderTop: "1px solid #f0ece4" }}>
|
||||||
{activeTheme && (
|
{activeTheme && (
|
||||||
<>
|
<>
|
||||||
<p className="text-xs text-muted-foreground flex-1">{activeTheme.description}</p>
|
<p style={{ flex: 1, fontSize: "0.78rem", color: "#8a8478", lineHeight: 1.5, fontFamily: "Outfit" }}>
|
||||||
<div className="flex gap-1">
|
{activeTheme.description}
|
||||||
|
</p>
|
||||||
|
<div style={{ display: "flex", gap: 4 }}>
|
||||||
{activeTheme.tags.map(t => (
|
{activeTheme.tags.map(t => (
|
||||||
<Badge key={t} variant="secondary" className="text-[9px] px-1.5 py-0">{t}</Badge>
|
<span key={t} style={{ padding: "3px 8px", borderRadius: 4, fontSize: "0.65rem", fontWeight: 600, color: "#6b6560", background: "#f0ece4", fontFamily: "Outfit" }}>
|
||||||
|
{t}
|
||||||
|
</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<a href={activeTheme.url} target="_blank" rel="noopener noreferrer"
|
<a
|
||||||
className="text-[11px] text-muted-foreground hover:text-foreground transition-colors shrink-0">
|
href={activeTheme.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
style={{ fontSize: "0.75rem", color: "#a09a90", textDecoration: "none", fontFamily: "Outfit", flexShrink: 0 }}
|
||||||
|
onMouseEnter={e => (e.currentTarget.style.color = "#1a1a1a")}
|
||||||
|
onMouseLeave={e => (e.currentTarget.style.color = "#a09a90")}
|
||||||
|
>
|
||||||
Docs ↗
|
Docs ↗
|
||||||
</a>
|
</a>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{lockedThemeId ? (
|
{isLocked ? (
|
||||||
<Button variant="outline" size="sm" onClick={onUnlock} className="gap-1.5 text-xs h-7 shrink-0">
|
<button
|
||||||
<Pencil className="h-3 w-3" />Change
|
onClick={onUnlock}
|
||||||
</Button>
|
style={{
|
||||||
|
padding: "7px 14px", borderRadius: 7, border: "1px solid #e0dcd4",
|
||||||
|
background: "#fff", color: "#1a1a1a", fontSize: "0.76rem", fontWeight: 600,
|
||||||
|
fontFamily: "Outfit", cursor: "pointer", flexShrink: 0, transition: "opacity 0.15s",
|
||||||
|
}}
|
||||||
|
onMouseEnter={e => (e.currentTarget.style.opacity = "0.7")}
|
||||||
|
onMouseLeave={e => (e.currentTarget.style.opacity = "1")}
|
||||||
|
>
|
||||||
|
✎ Change
|
||||||
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<Button size="sm" onClick={onLock} disabled={!selectedThemeId || saving}
|
<button
|
||||||
className="gap-1.5 text-xs h-7 shrink-0">
|
onClick={onLock}
|
||||||
{saving ? <Loader2 className="h-3 w-3 animate-spin" /> : <Lock className="h-3 w-3" />}
|
disabled={!selectedThemeId || saving}
|
||||||
Lock in
|
style={{
|
||||||
</Button>
|
padding: "7px 14px", borderRadius: 7, border: "1px solid #1a1a1a",
|
||||||
|
background: !selectedThemeId || saving ? "#e0dcd4" : "#1a1a1a",
|
||||||
|
color: !selectedThemeId || saving ? "#b5b0a6" : "#fff",
|
||||||
|
fontSize: "0.76rem", fontWeight: 600, fontFamily: "Outfit",
|
||||||
|
cursor: !selectedThemeId || saving ? "not-allowed" : "pointer",
|
||||||
|
flexShrink: 0, transition: "opacity 0.15s",
|
||||||
|
}}
|
||||||
|
onMouseEnter={e => { if (selectedThemeId && !saving) (e.currentTarget.style.opacity = "0.8"); }}
|
||||||
|
onMouseLeave={e => { (e.currentTarget.style.opacity = "1"); }}
|
||||||
|
>
|
||||||
|
{saving ? "Saving…" : "🔒 Lock in"}
|
||||||
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -444,13 +295,7 @@ function SurfaceSection({
|
|||||||
// Phase 1 — Surface picker
|
// Phase 1 — Surface picker
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
function SurfacePicker({
|
function SurfacePicker({ onConfirm, saving }: { onConfirm: (ids: string[]) => void; saving: boolean }) {
|
||||||
onConfirm,
|
|
||||||
saving,
|
|
||||||
}: {
|
|
||||||
onConfirm: (ids: string[]) => void;
|
|
||||||
saving: boolean;
|
|
||||||
}) {
|
|
||||||
const [selected, setSelected] = useState<Set<string>>(new Set());
|
const [selected, setSelected] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
const toggle = (id: string) => {
|
const toggle = (id: string) => {
|
||||||
@@ -462,61 +307,74 @@ function SurfacePicker({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div style={{ padding: "28px 32px", fontFamily: "Outfit, sans-serif", animation: "enter 0.3s ease" }}>
|
||||||
<div>
|
<h3 style={{ fontFamily: "Newsreader, serif", fontSize: "1.2rem", fontWeight: 400, color: "#1a1a1a", marginBottom: 4 }}>
|
||||||
<h1 className="text-xl font-bold">Design surfaces</h1>
|
Design surfaces
|
||||||
<p className="text-sm text-muted-foreground mt-0.5">
|
</h3>
|
||||||
Which surfaces does your product need? Select all that apply — you can always add more later.
|
<p style={{ fontSize: "0.8rem", color: "#a09a90", marginBottom: 24 }}>
|
||||||
|
Which surfaces does your product need? Select all that apply.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(220px, 1fr))", gap: 8, marginBottom: 24 }}>
|
||||||
{ALL_SURFACES.map(surface => {
|
{ALL_SURFACES.map(surface => {
|
||||||
const Icon = surface.icon;
|
|
||||||
const isSelected = selected.has(surface.id);
|
const isSelected = selected.has(surface.id);
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={surface.id}
|
key={surface.id}
|
||||||
onClick={() => toggle(surface.id)}
|
onClick={() => toggle(surface.id)}
|
||||||
className={cn(
|
style={{
|
||||||
"flex items-start gap-3 rounded-xl border p-4 text-left transition-all",
|
display: "flex", alignItems: "flex-start", gap: 14,
|
||||||
isSelected
|
padding: "16px 18px", borderRadius: 10, textAlign: "left",
|
||||||
? "border-foreground bg-foreground/5 ring-1 ring-foreground"
|
border: `1px solid ${isSelected ? "#1a1a1a" : "#e8e4dc"}`,
|
||||||
: "border-border hover:border-foreground/40"
|
background: isSelected ? "#1a1a1a08" : "#fff",
|
||||||
)}
|
boxShadow: isSelected ? "0 0 0 1px #1a1a1a" : "0 1px 2px #1a1a1a05",
|
||||||
|
cursor: "pointer", transition: "all 0.12s", fontFamily: "Outfit",
|
||||||
|
}}
|
||||||
|
onMouseEnter={e => { if (!isSelected) (e.currentTarget.style.borderColor = "#d0ccc4"); }}
|
||||||
|
onMouseLeave={e => { if (!isSelected) (e.currentTarget.style.borderColor = "#e8e4dc"); }}
|
||||||
>
|
>
|
||||||
<div className={cn(
|
<div style={{
|
||||||
"w-8 h-8 rounded-lg flex items-center justify-center shrink-0 mt-0.5",
|
width: 34, height: 34, borderRadius: 8, flexShrink: 0, marginTop: 1,
|
||||||
isSelected ? "bg-foreground text-background" : "bg-muted text-muted-foreground"
|
background: isSelected ? "#1a1a1a" : "#f6f4f0",
|
||||||
)}>
|
display: "flex", alignItems: "center", justifyContent: "center",
|
||||||
<Icon className="h-4 w-4" />
|
fontSize: "1rem", color: isSelected ? "#fff" : "#8a8478",
|
||||||
|
}}>
|
||||||
|
{surface.icon}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
<p className="text-sm font-semibold">{surface.name}</p>
|
<div style={{ fontSize: "0.84rem", fontWeight: 600, color: "#1a1a1a", marginBottom: 3 }}>{surface.name}</div>
|
||||||
<p className="text-xs text-muted-foreground mt-0.5 leading-relaxed">{surface.description}</p>
|
<div style={{ fontSize: "0.74rem", color: "#8a8478", lineHeight: 1.5 }}>{surface.description}</div>
|
||||||
</div>
|
</div>
|
||||||
{isSelected && (
|
{isSelected && (
|
||||||
<CheckCircle2 className="h-4 w-4 text-foreground ml-auto shrink-0 mt-0.5" />
|
<span style={{ flexShrink: 0, color: "#1a1a1a", fontSize: "0.85rem", marginTop: 2 }}>✓</span>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
<button
|
||||||
<Button
|
|
||||||
onClick={() => onConfirm(Array.from(selected))}
|
onClick={() => onConfirm(Array.from(selected))}
|
||||||
disabled={selected.size === 0 || saving}
|
disabled={selected.size === 0 || saving}
|
||||||
className="gap-2"
|
style={{
|
||||||
|
padding: "9px 20px", borderRadius: 7, border: "none",
|
||||||
|
background: selected.size === 0 || saving ? "#e0dcd4" : "#1a1a1a",
|
||||||
|
color: selected.size === 0 || saving ? "#b5b0a6" : "#fff",
|
||||||
|
fontSize: "0.82rem", fontWeight: 600, fontFamily: "Outfit",
|
||||||
|
cursor: selected.size === 0 || saving ? "not-allowed" : "pointer",
|
||||||
|
transition: "opacity 0.15s",
|
||||||
|
}}
|
||||||
|
onMouseEnter={e => { if (selected.size > 0 && !saving) (e.currentTarget.style.opacity = "0.8"); }}
|
||||||
|
onMouseLeave={e => { (e.currentTarget.style.opacity = "1"); }}
|
||||||
>
|
>
|
||||||
{saving ? <Loader2 className="h-4 w-4 animate-spin" /> : <ChevronRight className="h-4 w-4" />}
|
{saving ? "Saving…" : `Confirm surfaces (${selected.size})`} →
|
||||||
Confirm surfaces ({selected.size})
|
</button>
|
||||||
</Button>
|
|
||||||
{selected.size === 0 && (
|
{selected.size === 0 && (
|
||||||
<p className="text-xs text-muted-foreground">Select at least one surface to continue</p>
|
<p style={{ display: "inline-block", marginLeft: 12, fontSize: "0.74rem", color: "#b5b0a6", fontFamily: "Outfit" }}>
|
||||||
|
Select at least one surface to continue
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -524,11 +382,7 @@ function SurfacePicker({
|
|||||||
// Page
|
// Page
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export default function DesignPage({
|
export default function DesignPage({ params }: { params: Promise<{ workspace: string; projectId: string }> }) {
|
||||||
params,
|
|
||||||
}: {
|
|
||||||
params: Promise<{ workspace: string; projectId: string }>;
|
|
||||||
}) {
|
|
||||||
const { projectId } = use(params);
|
const { projectId } = use(params);
|
||||||
|
|
||||||
const [surfaces, setSurfaces] = useState<string[]>([]);
|
const [surfaces, setSurfaces] = useState<string[]>([]);
|
||||||
@@ -595,79 +449,75 @@ export default function DesignPage({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleUnlock = (surfaceId: string) => {
|
const handleUnlock = (surfaceId: string) => {
|
||||||
setSurfaceThemes(prev => {
|
setSurfaceThemes(prev => { const next = { ...prev }; delete next[surfaceId]; return next; });
|
||||||
const next = { ...prev };
|
|
||||||
delete next[surfaceId];
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-full">
|
<div style={{ display: "flex", alignItems: "center", justifyContent: "center", height: "100%", fontFamily: "Outfit" }}>
|
||||||
<Loader2 className="h-7 w-7 animate-spin text-muted-foreground" />
|
<div style={{ width: 18, height: 18, borderRadius: "50%", border: "2px solid #e8e4dc", borderTopColor: "#1a1a1a", animation: "spin 0.8s linear infinite" }} />
|
||||||
|
<style>{`@keyframes spin { to { transform: rotate(360deg); } }`}</style>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase 1 — no surfaces set yet
|
|
||||||
if (surfaces.length === 0) {
|
if (surfaces.length === 0) {
|
||||||
return (
|
return <SurfacePicker onConfirm={handleConfirmSurfaces} saving={savingSurfaces} />;
|
||||||
<div className="p-6 max-w-4xl mx-auto">
|
|
||||||
<SurfacePicker onConfirm={handleConfirmSurfaces} saving={savingSurfaces} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase 2 — left nav + main content
|
|
||||||
const activeSurfaces = ALL_SURFACES.filter(s => surfaces.includes(s.id));
|
const activeSurfaces = ALL_SURFACES.filter(s => surfaces.includes(s.id));
|
||||||
const currentSurface = activeSurfaces.find(s => s.id === activeSurfaceId) ?? activeSurfaces[0];
|
const currentSurface = activeSurfaces.find(s => s.id === activeSurfaceId) ?? activeSurfaces[0];
|
||||||
const lockedCount = Object.keys(surfaceThemes).length;
|
const lockedCount = Object.keys(surfaceThemes).length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full">
|
<div style={{ display: "flex", height: "100%", fontFamily: "Outfit, sans-serif" }}>
|
||||||
|
|
||||||
{/* Left nav */}
|
{/* Left nav */}
|
||||||
<div className="w-52 shrink-0 border-r flex flex-col">
|
<div style={{ width: 180, flexShrink: 0, borderRight: "1px solid #e8e4dc", display: "flex", flexDirection: "column", background: "#fff" }}>
|
||||||
<div className="px-4 py-4 border-b">
|
<div style={{ padding: "16px 18px 10px", borderBottom: "1px solid #f0ece4" }}>
|
||||||
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wide">Surfaces</p>
|
<div style={{ fontSize: "0.6rem", fontWeight: 600, color: "#a09a90", letterSpacing: "0.1em", textTransform: "uppercase" }}>Surfaces</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav className="flex-1 py-2">
|
<nav style={{ flex: 1, padding: "6px 8px", overflow: "auto" }}>
|
||||||
{activeSurfaces.map(surface => {
|
{activeSurfaces.map(surface => {
|
||||||
const Icon = surface.icon;
|
|
||||||
const isActive = surface.id === currentSurface?.id;
|
const isActive = surface.id === currentSurface?.id;
|
||||||
const isLocked = !!surfaceThemes[surface.id];
|
const isLocked = !!surfaceThemes[surface.id];
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={surface.id}
|
key={surface.id}
|
||||||
onClick={() => setActiveSurfaceId(surface.id)}
|
onClick={() => setActiveSurfaceId(surface.id)}
|
||||||
className={cn(
|
style={{
|
||||||
"flex items-center gap-2.5 w-full px-4 py-2.5 text-left transition-colors relative",
|
display: "flex", alignItems: "center", gap: 8, width: "100%",
|
||||||
isActive
|
padding: "8px 10px", borderRadius: 6, border: "none", textAlign: "left",
|
||||||
? "bg-muted text-foreground"
|
background: isActive ? "#f6f4f0" : "transparent",
|
||||||
: "text-muted-foreground hover:text-foreground hover:bg-muted/50"
|
color: isActive ? "#1a1a1a" : "#6b6560",
|
||||||
)}
|
fontSize: "0.8rem", fontWeight: isActive ? 600 : 450,
|
||||||
|
cursor: "pointer", transition: "all 0.12s", position: "relative",
|
||||||
|
fontFamily: "Outfit",
|
||||||
|
}}
|
||||||
|
onMouseEnter={e => { if (!isActive) (e.currentTarget.style.background = "#f6f4f0"); }}
|
||||||
|
onMouseLeave={e => { if (!isActive) (e.currentTarget.style.background = "transparent"); }}
|
||||||
>
|
>
|
||||||
{isActive && (
|
{isActive && (
|
||||||
<div className="absolute left-0 top-1/2 -translate-y-1/2 h-6 w-0.5 bg-foreground rounded-r" />
|
<div style={{ position: "absolute", left: 0, top: "50%", transform: "translateY(-50%)", width: 2, height: 16, background: "#1a1a1a", borderRadius: "0 2px 2px 0" }} />
|
||||||
)}
|
)}
|
||||||
<Icon className="h-4 w-4 shrink-0" />
|
<span style={{ fontSize: "0.9rem", opacity: 0.5, flexShrink: 0 }}>{surface.icon}</span>
|
||||||
<span className="text-sm font-medium flex-1 truncate">{surface.name}</span>
|
<span style={{ flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{surface.name}</span>
|
||||||
{isLocked && <Lock className="h-3 w-3 shrink-0 text-muted-foreground" />}
|
{isLocked && <span style={{ fontSize: "0.6rem", flexShrink: 0, opacity: 0.5 }}>🔒</span>}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div className="px-4 py-3 border-t space-y-2">
|
<div style={{ padding: "12px 18px", borderTop: "1px solid #f0ece4" }}>
|
||||||
{lockedCount === activeSurfaces.length && lockedCount > 0 && (
|
{lockedCount === activeSurfaces.length && lockedCount > 0 && (
|
||||||
<p className="text-[10px] text-muted-foreground flex items-center gap-1">
|
<p style={{ fontSize: "0.68rem", color: "#2e7d32", fontFamily: "Outfit", marginBottom: 6 }}>✓ All locked</p>
|
||||||
<CheckCircle2 className="h-3 w-3" /> All surfaces locked
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={() => setSurfaces([])}
|
onClick={() => setSurfaces([])}
|
||||||
className="text-[11px] text-muted-foreground hover:text-foreground transition-colors"
|
style={{ fontSize: "0.72rem", color: "#a09a90", background: "none", border: "none", cursor: "pointer", fontFamily: "Outfit", padding: 0 }}
|
||||||
|
onMouseEnter={e => (e.currentTarget.style.color = "#1a1a1a")}
|
||||||
|
onMouseLeave={e => (e.currentTarget.style.color = "#a09a90")}
|
||||||
>
|
>
|
||||||
+ Edit surfaces
|
+ Edit surfaces
|
||||||
</button>
|
</button>
|
||||||
@@ -675,7 +525,7 @@ export default function DesignPage({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Main content */}
|
{/* Main content */}
|
||||||
<div className="flex-1 overflow-y-auto p-8">
|
<div style={{ flex: 1, overflow: "auto", padding: "24px 28px" }}>
|
||||||
{currentSurface && (
|
{currentSurface && (
|
||||||
<SurfaceSection
|
<SurfaceSection
|
||||||
surface={currentSurface}
|
surface={currentSurface}
|
||||||
|
|||||||
Reference in New Issue
Block a user