Commit 64cc390e authored by Administrator's avatar Administrator

Update 8 files via Son of Anton

parent 60c96940
# This file intentionally left mostly empty.
# All route modules are imported directly in main.py.
\ No newline at end of file
"""
Design template routes — serve UI component library.
"""
from fastapi import APIRouter, Depends
from backend.auth import get_current_user
from backend.services.design_templates import get_templates, get_template, list_categories
router = APIRouter()
@router.get("/templates")
def list_templates(category: str = None, user=Depends(get_current_user)):
templates = get_templates(category)
return [
{"key": k, "name": v["name"], "category": v["category"]}
for k, v in templates.items()
]
@router.get("/templates/{key}")
def get_template_detail(key: str, user=Depends(get_current_user)):
t = get_template(key)
if not t:
return {"error": "Template not found"}
return t
@router.get("/categories")
def list_template_categories(user=Depends(get_current_user)):
return list_categories()
\ No newline at end of file
"""
Design template library — reusable UI patterns for Son of Anton.
Provides pre-built Tailwind HTML component snippets.
"""
COMPONENT_LIBRARY = {
"hero_gradient": {
"name": "Hero Section — Gradient",
"category": "hero",
"code": """<section class="relative min-h-screen flex items-center justify-center overflow-hidden bg-gradient-to-br from-gray-950 via-gray-900 to-gray-950">
<div class="absolute inset-0 bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-indigo-900/20 via-transparent to-transparent"></div>
<div class="absolute inset-0 bg-grid-white/[0.02]"></div>
<div class="relative z-10 max-w-5xl mx-auto px-6 text-center">
<div class="inline-flex items-center gap-2 px-4 py-1.5 rounded-full bg-indigo-500/10 border border-indigo-500/20 text-indigo-300 text-sm mb-8 animate-fade-in">
<span class="w-2 h-2 rounded-full bg-indigo-400 animate-pulse"></span>
Now Available
</div>
<h1 class="text-5xl md:text-7xl font-extrabold text-white tracking-tight mb-6 animate-fade-in" style="animation-delay:0.1s">
Build something<br/><span class="bg-clip-text text-transparent bg-gradient-to-r from-indigo-400 via-purple-400 to-pink-400">extraordinary</span>
</h1>
<p class="text-lg md:text-xl text-gray-400 max-w-2xl mx-auto mb-10 animate-fade-in" style="animation-delay:0.2s">
The modern platform for teams who ship fast. Beautiful by default, powerful under the hood.
</p>
<div class="flex flex-col sm:flex-row items-center justify-center gap-4 animate-fade-in" style="animation-delay:0.3s">
<a href="#" class="px-8 py-3.5 bg-indigo-500 hover:bg-indigo-400 text-white font-semibold rounded-xl transition-all hover:shadow-lg hover:shadow-indigo-500/25 hover:-translate-y-0.5">
Get Started Free
</a>
<a href="#" class="px-8 py-3.5 bg-white/5 hover:bg-white/10 text-white font-semibold rounded-xl border border-white/10 transition-all">
See Demo →
</a>
</div>
</div>
</section>""",
},
"card_grid": {
"name": "Feature Cards Grid",
"category": "features",
"code": """<section class="py-24 bg-gray-950">
<div class="max-w-6xl mx-auto px-6">
<div class="text-center mb-16">
<h2 class="text-3xl md:text-4xl font-bold text-white mb-4">Everything you need</h2>
<p class="text-gray-400 text-lg max-w-xl mx-auto">Powerful features packed into a beautiful interface</p>
</div>
<div class="grid md:grid-cols-3 gap-6">
<div class="group p-6 rounded-2xl bg-gray-900/50 border border-gray-800 hover:border-indigo-500/30 transition-all hover:shadow-lg hover:shadow-indigo-500/5">
<div class="w-12 h-12 rounded-xl bg-indigo-500/10 flex items-center justify-center mb-4 group-hover:bg-indigo-500/20 transition">
<i data-lucide="zap" class="w-6 h-6 text-indigo-400"></i>
</div>
<h3 class="text-lg font-semibold text-white mb-2">Lightning Fast</h3>
<p class="text-gray-400 text-sm leading-relaxed">Built for speed. Every interaction feels instant with our optimized architecture.</p>
</div>
<div class="group p-6 rounded-2xl bg-gray-900/50 border border-gray-800 hover:border-purple-500/30 transition-all hover:shadow-lg hover:shadow-purple-500/5">
<div class="w-12 h-12 rounded-xl bg-purple-500/10 flex items-center justify-center mb-4 group-hover:bg-purple-500/20 transition">
<i data-lucide="shield" class="w-6 h-6 text-purple-400"></i>
</div>
<h3 class="text-lg font-semibold text-white mb-2">Enterprise Security</h3>
<p class="text-gray-400 text-sm leading-relaxed">SOC2 compliant with end-to-end encryption. Your data is always safe.</p>
</div>
<div class="group p-6 rounded-2xl bg-gray-900/50 border border-gray-800 hover:border-pink-500/30 transition-all hover:shadow-lg hover:shadow-pink-500/5">
<div class="w-12 h-12 rounded-xl bg-pink-500/10 flex items-center justify-center mb-4 group-hover:bg-pink-500/20 transition">
<i data-lucide="sparkles" class="w-6 h-6 text-pink-400"></i>
</div>
<h3 class="text-lg font-semibold text-white mb-2">AI Powered</h3>
<p class="text-gray-400 text-sm leading-relaxed">Smart suggestions and automation that learn from your workflow.</p>
</div>
</div>
</div>
</section>""",
},
"pricing_table": {
"name": "Pricing — 3 Tiers",
"category": "pricing",
"code": """<section class="py-24 bg-gray-950">
<div class="max-w-6xl mx-auto px-6">
<div class="text-center mb-16">
<h2 class="text-3xl md:text-4xl font-bold text-white mb-4">Simple pricing</h2>
<p class="text-gray-400 text-lg">No hidden fees. Cancel anytime.</p>
</div>
<div class="grid md:grid-cols-3 gap-6 items-start">
<div class="p-8 rounded-2xl bg-gray-900/50 border border-gray-800">
<h3 class="text-lg font-semibold text-white mb-1">Starter</h3>
<p class="text-gray-400 text-sm mb-6">For individuals</p>
<div class="mb-6"><span class="text-4xl font-bold text-white">$9</span><span class="text-gray-400">/mo</span></div>
<ul class="space-y-3 mb-8">
<li class="flex items-center gap-2 text-sm text-gray-300"><i data-lucide="check" class="w-4 h-4 text-green-400"></i> 5 projects</li>
<li class="flex items-center gap-2 text-sm text-gray-300"><i data-lucide="check" class="w-4 h-4 text-green-400"></i> 10GB storage</li>
<li class="flex items-center gap-2 text-sm text-gray-300"><i data-lucide="check" class="w-4 h-4 text-green-400"></i> Community support</li>
</ul>
<button class="w-full py-3 rounded-xl bg-white/5 hover:bg-white/10 text-white font-medium border border-white/10 transition">Get Started</button>
</div>
<div class="p-8 rounded-2xl bg-gradient-to-b from-indigo-500/10 to-gray-900/50 border border-indigo-500/30 relative">
<div class="absolute -top-3 left-1/2 -translate-x-1/2 px-3 py-0.5 bg-indigo-500 text-white text-xs font-bold rounded-full">POPULAR</div>
<h3 class="text-lg font-semibold text-white mb-1">Pro</h3>
<p class="text-gray-400 text-sm mb-6">For teams</p>
<div class="mb-6"><span class="text-4xl font-bold text-white">$29</span><span class="text-gray-400">/mo</span></div>
<ul class="space-y-3 mb-8">
<li class="flex items-center gap-2 text-sm text-gray-300"><i data-lucide="check" class="w-4 h-4 text-green-400"></i> Unlimited projects</li>
<li class="flex items-center gap-2 text-sm text-gray-300"><i data-lucide="check" class="w-4 h-4 text-green-400"></i> 100GB storage</li>
<li class="flex items-center gap-2 text-sm text-gray-300"><i data-lucide="check" class="w-4 h-4 text-green-400"></i> Priority support</li>
<li class="flex items-center gap-2 text-sm text-gray-300"><i data-lucide="check" class="w-4 h-4 text-green-400"></i> Advanced analytics</li>
</ul>
<button class="w-full py-3 rounded-xl bg-indigo-500 hover:bg-indigo-400 text-white font-semibold transition hover:shadow-lg hover:shadow-indigo-500/25">Get Started</button>
</div>
<div class="p-8 rounded-2xl bg-gray-900/50 border border-gray-800">
<h3 class="text-lg font-semibold text-white mb-1">Enterprise</h3>
<p class="text-gray-400 text-sm mb-6">Custom solutions</p>
<div class="mb-6"><span class="text-4xl font-bold text-white">Custom</span></div>
<ul class="space-y-3 mb-8">
<li class="flex items-center gap-2 text-sm text-gray-300"><i data-lucide="check" class="w-4 h-4 text-green-400"></i> Everything in Pro</li>
<li class="flex items-center gap-2 text-sm text-gray-300"><i data-lucide="check" class="w-4 h-4 text-green-400"></i> Unlimited storage</li>
<li class="flex items-center gap-2 text-sm text-gray-300"><i data-lucide="check" class="w-4 h-4 text-green-400"></i> Dedicated support</li>
<li class="flex items-center gap-2 text-sm text-gray-300"><i data-lucide="check" class="w-4 h-4 text-green-400"></i> Custom integrations</li>
<li class="flex items-center gap-2 text-sm text-gray-300"><i data-lucide="check" class="w-4 h-4 text-green-400"></i> SLA guarantee</li>
</ul>
<button class="w-full py-3 rounded-xl bg-white/5 hover:bg-white/10 text-white font-medium border border-white/10 transition">Contact Sales</button>
</div>
</div>
</div>
</section>""",
},
}
def get_templates(category=None):
"""Return all templates, optionally filtered by category."""
if category:
return {k: v for k, v in COMPONENT_LIBRARY.items() if v["category"] == category}
return COMPONENT_LIBRARY
def get_template(key):
"""Get a specific template by key."""
return COMPONENT_LIBRARY.get(key)
def list_categories():
"""List all available template categories."""
return list(set(v["category"] for v in COMPONENT_LIBRARY.values()))
\ No newline at end of file
import React, { useState, useMemo } from "react";
import { Copy, Check, Palette, Sun, Moon } from "lucide-react";
function hexToHSL(hex) {
hex = hex.replace("#", "");
const r = parseInt(hex.substr(0, 2), 16) / 255;
const g = parseInt(hex.substr(2, 2), 16) / 255;
const b = parseInt(hex.substr(4, 2), 16) / 255;
const max = Math.max(r, g, b), min = Math.min(r, g, b);
let h = 0, s = 0, l = (max + min) / 2;
if (max !== min) {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break;
case g: h = ((b - r) / d + 2) / 6; break;
case b: h = ((r - g) / d + 4) / 6; break;
}
}
return { h: Math.round(h * 360), s: Math.round(s * 100), l: Math.round(l * 100) };
}
function hslToHex(h, s, l) {
s /= 100; l /= 100;
const a = s * Math.min(l, 1 - l);
const f = (n) => {
const k = (n + h / 30) % 12;
const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
return Math.round(255 * color).toString(16).padStart(2, "0");
};
return `#${f(0)}${f(8)}${f(4)}`;
}
function generateScale(hex) {
const { h, s } = hexToHSL(hex);
return [
{ step: 50, hex: hslToHex(h, Math.min(s + 5, 100), 97) },
{ step: 100, hex: hslToHex(h, Math.min(s + 5, 100), 94) },
{ step: 200, hex: hslToHex(h, s, 86) },
{ step: 300, hex: hslToHex(h, s, 76) },
{ step: 400, hex: hslToHex(h, s, 64) },
{ step: 500, hex: hslToHex(h, s, 50) },
{ step: 600, hex: hslToHex(h, s, 40) },
{ step: 700, hex: hslToHex(h, s, 32) },
{ step: 800, hex: hslToHex(h, s, 24) },
{ step: 900, hex: hslToHex(h, s, 16) },
{ step: 950, hex: hslToHex(h, Math.min(s + 10, 100), 10) },
];
}
function contrastRatio(hex1, hex2) {
function luminance(hex) {
const rgb = [parseInt(hex.slice(1, 3), 16), parseInt(hex.slice(3, 5), 16), parseInt(hex.slice(5, 7), 16)];
const [r, g, b] = rgb.map((v) => {
v /= 255;
return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
});
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}
const l1 = luminance(hex1), l2 = luminance(hex2);
const lighter = Math.max(l1, l2), darker = Math.min(l1, l2);
return ((lighter + 0.05) / (darker + 0.05)).toFixed(2);
}
function wcagGrade(ratio) {
if (ratio >= 7) return { grade: "AAA", color: "text-green-400" };
if (ratio >= 4.5) return { grade: "AA", color: "text-green-400" };
if (ratio >= 3) return { grade: "AA Large", color: "text-yellow-400" };
return { grade: "Fail", color: "text-red-400" };
}
export default function ColorSystemPreview({ colors }) {
const [copiedColor, setCopiedColor] = useState(null);
// Parse colors — expects array of { name, hex } or object { primary: "#xxx", ... }
const colorEntries = useMemo(() => {
if (Array.isArray(colors)) return colors;
if (typeof colors === "object") {
return Object.entries(colors).map(([name, hex]) => ({ name, hex }));
}
return [];
}, [colors]);
function handleCopy(hex) {
navigator.clipboard.writeText(hex);
setCopiedColor(hex);
setTimeout(() => setCopiedColor(null), 1500);
}
return (
<div className="rounded-xl border border-anton-border bg-anton-card overflow-hidden">
<div className="px-4 py-3 border-b border-anton-border flex items-center gap-2">
<Palette size={14} className="text-anton-accent" />
<span className="text-xs font-bold text-white uppercase tracking-wider">Color System</span>
</div>
<div className="p-4 space-y-6">
{colorEntries.map(({ name, hex }) => {
const scale = generateScale(hex);
const whiteContrast = contrastRatio(hex, "#ffffff");
const blackContrast = contrastRatio(hex, "#000000");
const wcagWhite = wcagGrade(whiteContrast);
const wcagBlack = wcagGrade(blackContrast);
return (
<div key={name}>
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<div className="w-4 h-4 rounded-full border border-white/10" style={{ backgroundColor: hex }} />
<span className="text-sm font-semibold text-white capitalize">{name}</span>
<span className="text-[10px] font-mono text-anton-muted">{hex}</span>
</div>
<div className="flex items-center gap-3 text-[10px]">
<span className="text-anton-muted">
vs white: {whiteContrast}:1 <span className={wcagWhite.color}>{wcagWhite.grade}</span>
</span>
<span className="text-anton-muted">
vs black: {blackContrast}:1 <span className={wcagBlack.color}>{wcagBlack.grade}</span>
</span>
</div>
</div>
<div className="flex rounded-lg overflow-hidden">
{scale.map(({ step, hex: scaleHex }) => (
<button
key={step}
onClick={() => handleCopy(scaleHex)}
className="flex-1 h-12 relative group transition-transform hover:scale-y-110 hover:z-10"
style={{ backgroundColor: scaleHex }}
title={`${name}-${step}: ${scaleHex}`}
>
<span className={`absolute inset-0 flex flex-col items-center justify-center opacity-0 group-hover:opacity-100 transition text-[9px] font-mono ${step <= 400 ? "text-gray-900" : "text-white"}`}>
<span className="font-bold">{step}</span>
<span>{copiedColor === scaleHex ? "✓" : scaleHex}</span>
</span>
</button>
))}
</div>
</div>
);
})}
</div>
{/* Tailwind CSS config output */}
<div className="px-4 py-3 bg-[#0d0d1a] border-t border-anton-border">
<div className="text-[10px] text-anton-muted mb-1 font-bold uppercase">Tailwind Config</div>
<pre className="text-[10px] font-mono text-green-300/80 overflow-x-auto whitespace-pre">
{`colors: {\n${colorEntries.map(({ name, hex }) => {
const scale = generateScale(hex);
return ` '${name}': {\n${scale.map(({ step, hex: h }) => ` ${step}: '${h}',`).join("\n")}\n },`;
}).join("\n")}\n}`}
</pre>
</div>
</div>
);
}
\ No newline at end of file
import React, { useRef, useEffect, useState, useMemo } from "react";
import { Monitor, Tablet, Smartphone, Maximize2, Minimize2, Copy, Check, Download, RefreshCw, Sun, Moon, Code2, Eye } from "lucide-react";
const VIEWPORTS = [
{ key: "desktop", label: "Desktop", width: "100%", icon: Monitor },
{ key: "tablet", label: "Tablet", width: "768px", icon: Tablet },
{ key: "mobile", label: "Mobile", width: "375px", icon: Smartphone },
];
const CDN_LIBS = {
tailwind: '<script src="https://cdn.tailwindcss.com"></script>',
alpine: '<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>',
lucide: '<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>',
animate: '<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css"/>',
googleFonts: '<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet"/>',
};
const TAILWIND_CONFIG = `
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
fontFamily: { sans: ['Inter', 'system-ui', 'sans-serif'], mono: ['JetBrains Mono', 'monospace'] },
animation: {
'fade-in': 'fadeIn 0.5s ease-out',
'slide-up': 'slideUp 0.5s ease-out',
'slide-in-right': 'slideInRight 0.3s ease-out',
'bounce-in': 'bounceIn 0.6s ease-out',
'pulse-slow': 'pulse 3s ease-in-out infinite',
'float': 'float 6s ease-in-out infinite',
},
keyframes: {
fadeIn: { from: { opacity: 0 }, to: { opacity: 1 } },
slideUp: { from: { opacity: 0, transform: 'translateY(20px)' }, to: { opacity: 1, transform: 'translateY(0)' } },
slideInRight: { from: { transform: 'translateX(100%)' }, to: { transform: 'translateX(0)' } },
bounceIn: { '0%': { opacity: 0, transform: 'scale(0.3)' }, '50%': { transform: 'scale(1.05)' }, '70%': { transform: 'scale(0.9)' }, '100%': { opacity: 1, transform: 'scale(1)' } },
float: { '0%, 100%': { transform: 'translateY(0)' }, '50%': { transform: 'translateY(-10px)' } },
},
},
},
}
</script>`;
const BASE_STYLES = `
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }
body { font-family: 'Inter', system-ui, sans-serif; line-height: 1.6; }
img { max-width: 100%; height: auto; }
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(155,155,155,0.3); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: rgba(155,155,155,0.5); }
.dark { color-scheme: dark; }
</style>`;
function buildFullHtml(code, darkMode) {
const darkClass = darkMode ? ' class="dark"' : '';
// Detect if user provided full HTML
if (code.trim().startsWith("<!DOCTYPE") || code.trim().startsWith("<html")) {
return code;
}
// Detect if it has <head> or <body>
if (code.includes("<head>") || code.includes("<body>")) {
return `<!DOCTYPE html><html lang="en"${darkClass}>${code}</html>`;
}
// Pure component/body content — wrap with full page
return `<!DOCTYPE html>
<html lang="en"${darkClass}>
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
${CDN_LIBS.googleFonts}
${CDN_LIBS.tailwind}
${TAILWIND_CONFIG}
${CDN_LIBS.animate}
${CDN_LIBS.lucide}
${BASE_STYLES}
</head>
<body class="${darkMode ? 'bg-gray-950 text-white' : 'bg-white text-gray-900'}">
${code}
<script>
try { lucide.createIcons(); } catch(e) {}
</script>
</body>
</html>`;
}
export default function DesignPreview({ code, title }) {
const iframeRef = useRef(null);
const [viewport, setViewport] = useState("desktop");
const [expanded, setExpanded] = useState(false);
const [darkMode, setDarkMode] = useState(false);
const [showCode, setShowCode] = useState(false);
const [copied, setCopied] = useState(false);
const [error, setError] = useState(null);
const [iframeKey, setIframeKey] = useState(0);
const fullHtml = useMemo(() => buildFullHtml(code, darkMode), [code, darkMode]);
useEffect(() => {
const iframe = iframeRef.current;
if (!iframe) return;
try {
const blob = new Blob([fullHtml], { type: "text/html" });
const url = URL.createObjectURL(blob);
iframe.src = url;
setError(null);
return () => URL.revokeObjectURL(url);
} catch (e) {
setError(e.message);
}
}, [fullHtml, iframeKey]);
function handleCopy() {
navigator.clipboard.writeText(code);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
function handleDownload() {
const blob = new Blob([fullHtml], { type: "text/html" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `${(title || "design").replace(/[^a-zA-Z0-9]/g, "-")}.html`;
a.click();
URL.revokeObjectURL(url);
}
const vp = VIEWPORTS.find((v) => v.key === viewport);
const containerClass = expanded
? "fixed inset-0 z-50 bg-black/90 flex flex-col p-4"
: "rounded-xl border border-anton-border overflow-hidden bg-anton-card";
return (
<div className={containerClass}>
{/* Toolbar */}
<div className="flex items-center justify-between px-3 py-2 bg-anton-surface border-b border-anton-border gap-2">
<div className="flex items-center gap-1">
<span className="text-[10px] font-bold text-anton-accent uppercase tracking-wider mr-2">
{title || "UI Preview"}
</span>
{VIEWPORTS.map((v) => {
const Icon = v.icon;
return (
<button
key={v.key}
onClick={() => setViewport(v.key)}
className={`p-1.5 rounded-md transition-colors ${viewport === v.key ? "bg-anton-accent/20 text-anton-accent" : "text-anton-muted hover:text-white"}`}
title={v.label}
>
<Icon size={14} />
</button>
);
})}
</div>
<div className="flex items-center gap-1">
<button onClick={() => setDarkMode(!darkMode)} className="p-1.5 rounded-md text-anton-muted hover:text-white transition" title={darkMode ? "Light mode" : "Dark mode"}>
{darkMode ? <Sun size={14} /> : <Moon size={14} />}
</button>
<button onClick={() => setShowCode(!showCode)} className={`p-1.5 rounded-md transition ${showCode ? "bg-anton-accent/20 text-anton-accent" : "text-anton-muted hover:text-white"}`} title="Toggle code">
{showCode ? <Eye size={14} /> : <Code2 size={14} />}
</button>
<button onClick={() => setIframeKey((k) => k + 1)} className="p-1.5 rounded-md text-anton-muted hover:text-white transition" title="Refresh">
<RefreshCw size={14} />
</button>
<button onClick={handleCopy} className="p-1.5 rounded-md text-anton-muted hover:text-white transition" title="Copy HTML">
{copied ? <Check size={14} className="text-green-400" /> : <Copy size={14} />}
</button>
<button onClick={handleDownload} className="p-1.5 rounded-md text-anton-muted hover:text-white transition" title="Download HTML">
<Download size={14} />
</button>
<button onClick={() => setExpanded(!expanded)} className="p-1.5 rounded-md text-anton-muted hover:text-white transition" title={expanded ? "Minimize" : "Fullscreen"}>
{expanded ? <Minimize2 size={14} /> : <Maximize2 size={14} />}
</button>
</div>
</div>
{/* Preview Area */}
<div className={`flex-1 flex items-start justify-center overflow-auto bg-[#1a1a2e] ${showCode ? "" : "p-4"}`} style={{ minHeight: expanded ? "auto" : "400px" }}>
{showCode ? (
<pre className="w-full h-full overflow-auto p-4 text-xs text-green-300 font-mono whitespace-pre-wrap">{code}</pre>
) : error ? (
<div className="text-red-400 p-6 text-sm">Preview error: {error}</div>
) : (
<div className="transition-all duration-300 h-full" style={{ width: vp.width, maxWidth: "100%" }}>
<iframe
key={iframeKey}
ref={iframeRef}
className="w-full bg-white rounded-lg shadow-2xl"
style={{ height: expanded ? "calc(100vh - 80px)" : "500px", border: "none" }}
sandbox="allow-scripts allow-same-origin allow-popups allow-forms"
title="Design Preview"
/>
</div>
)}
</div>
{/* Viewport indicator */}
<div className="flex items-center justify-center py-1.5 bg-anton-surface border-t border-anton-border">
<span className="text-[10px] text-anton-muted">
{vp.label} {vp.width !== "100%" && `(${vp.width})`}{darkMode ? "Dark" : "Light"} Mode
</span>
</div>
</div>
);
}
\ No newline at end of file
import React from "react";
const SPACING_SCALE = [
{ name: "0.5", px: 2 }, { name: "1", px: 4 }, { name: "1.5", px: 6 },
{ name: "2", px: 8 }, { name: "3", px: 12 }, { name: "4", px: 16 },
{ name: "5", px: 20 }, { name: "6", px: 24 }, { name: "8", px: 32 },
{ name: "10", px: 40 }, { name: "12", px: 48 }, { name: "16", px: 64 },
{ name: "20", px: 80 }, { name: "24", px: 96 },
];
const BORDER_RADIUS = [
{ name: "none", value: "0" }, { name: "sm", value: "2px" },
{ name: "DEFAULT", value: "4px" }, { name: "md", value: "6px" },
{ name: "lg", value: "8px" }, { name: "xl", value: "12px" },
{ name: "2xl", value: "16px" }, { name: "3xl", value: "24px" },
{ name: "full", value: "9999px" },
];
const SHADOWS = [
{ name: "sm", value: "0 1px 2px 0 rgb(0 0 0 / 0.05)" },
{ name: "DEFAULT", value: "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)" },
{ name: "md", value: "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)" },
{ name: "lg", value: "0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)" },
{ name: "xl", value: "0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)" },
{ name: "2xl", value: "0 25px 50px -12px rgb(0 0 0 / 0.25)" },
];
export default function SpacingPreview() {
return (
<div className="rounded-xl border border-anton-border bg-anton-card overflow-hidden">
<div className="px-4 py-3 border-b border-anton-border">
<span className="text-xs font-bold text-white uppercase tracking-wider">Design Tokens</span>
</div>
<div className="p-4 space-y-6">
{/* Spacing */}
<div>
<h3 className="text-[11px] font-bold text-anton-accent uppercase tracking-wider mb-3">Spacing Scale</h3>
<div className="space-y-1.5">
{SPACING_SCALE.map((s) => (
<div key={s.name} className="flex items-center gap-3">
<span className="text-[10px] font-mono text-anton-muted w-8 text-right">{s.name}</span>
<div className="h-3 bg-anton-accent/30 rounded-sm transition-all" style={{ width: `${s.px}px` }} />
<span className="text-[9px] text-anton-muted">{s.px}px</span>
</div>
))}
</div>
</div>
{/* Border Radius */}
<div>
<h3 className="text-[11px] font-bold text-anton-accent uppercase tracking-wider mb-3">Border Radius</h3>
<div className="flex flex-wrap gap-3">
{BORDER_RADIUS.map((r) => (
<div key={r.name} className="flex flex-col items-center gap-1">
<div className="w-12 h-12 bg-anton-accent/20 border border-anton-accent/40" style={{ borderRadius: r.value }} />
<span className="text-[9px] font-mono text-anton-muted">{r.name}</span>
</div>
))}
</div>
</div>
{/* Shadows */}
<div>
<h3 className="text-[11px] font-bold text-anton-accent uppercase tracking-wider mb-3">Shadows</h3>
<div className="flex flex-wrap gap-4">
{SHADOWS.map((s) => (
<div key={s.name} className="flex flex-col items-center gap-2">
<div className="w-16 h-16 bg-white rounded-lg" style={{ boxShadow: s.value }} />
<span className="text-[9px] font-mono text-anton-muted">{s.name}</span>
</div>
))}
</div>
</div>
</div>
</div>
);
}
\ No newline at end of file
import React from "react";
const TYPE_SCALE = [
{ name: "Display", size: "4.5rem", weight: 800, lineHeight: 1.1, tracking: "-0.02em" },
{ name: "H1", size: "3rem", weight: 700, lineHeight: 1.2, tracking: "-0.015em" },
{ name: "H2", size: "2.25rem", weight: 700, lineHeight: 1.25, tracking: "-0.01em" },
{ name: "H3", size: "1.875rem", weight: 600, lineHeight: 1.3, tracking: "-0.005em" },
{ name: "H4", size: "1.5rem", weight: 600, lineHeight: 1.35, tracking: "0" },
{ name: "H5", size: "1.25rem", weight: 600, lineHeight: 1.4, tracking: "0" },
{ name: "Body L", size: "1.125rem", weight: 400, lineHeight: 1.6, tracking: "0" },
{ name: "Body", size: "1rem", weight: 400, lineHeight: 1.6, tracking: "0" },
{ name: "Body S", size: "0.875rem", weight: 400, lineHeight: 1.5, tracking: "0.01em" },
{ name: "Caption", size: "0.75rem", weight: 500, lineHeight: 1.4, tracking: "0.02em" },
{ name: "Overline", size: "0.6875rem", weight: 600, lineHeight: 1.4, tracking: "0.08em" },
];
const SAMPLE_TEXT = "The quick brown fox jumps over the lazy dog";
export default function TypographyPreview({ fontFamily = "Inter" }) {
return (
<div className="rounded-xl border border-anton-border bg-anton-card overflow-hidden">
<div className="px-4 py-3 border-b border-anton-border">
<span className="text-xs font-bold text-white uppercase tracking-wider">Type Scale</span>
<span className="text-[10px] text-anton-muted ml-2">{fontFamily}</span>
</div>
<div className="divide-y divide-anton-border">
{TYPE_SCALE.map((t) => (
<div key={t.name} className="px-4 py-3 flex items-baseline gap-4 group hover:bg-white/[0.02] transition">
<div className="w-20 shrink-0">
<div className="text-[10px] font-bold text-anton-accent uppercase">{t.name}</div>
<div className="text-[9px] text-anton-muted font-mono">{t.size} / {t.weight}</div>
</div>
<div
className="text-white truncate flex-1 transition-colors"
style={{
fontFamily, fontSize: t.size, fontWeight: t.weight,
lineHeight: t.lineHeight, letterSpacing: t.tracking,
}}
>
{t.name === "Overline" ? SAMPLE_TEXT.toUpperCase() : SAMPLE_TEXT}
</div>
</div>
))}
</div>
</div>
);
}
\ No newline at end of file
import React from "react";
import DesignPreview from "./DesignPreview";
import ColorSystemPreview from "./ColorSystemPreview";
/**
* Detects UI code blocks in assistant messages and renders them
* with the enhanced DesignPreview instead of plain code blocks.
*
* Detection rules:
* - Code blocks with language "html" containing Tailwind classes
* - Code blocks with language "ui" or "design" or "preview"
* - Code blocks with filename ending in .html
*/
const UI_LANGUAGES = new Set(["html", "ui", "design", "preview", "htm"]);
const TAILWIND_INDICATORS = [
"className=", "class=\"", "class='",
"flex ", "grid ", "bg-", "text-", "rounded-", "shadow-",
"p-", "m-", "w-", "h-", "border-", "hover:", "dark:",
"tailwindcss", "tailwind",
];
export function isUICodeBlock(language, filename, code) {
const lang = (language || "").toLowerCase();
const file = (filename || "").toLowerCase();
// Explicit UI language
if (UI_LANGUAGES.has(lang)) return true;
// HTML file with Tailwind
if ((lang === "html" || file.endsWith(".html")) && code) {
const indicators = TAILWIND_INDICATORS.filter((i) => code.includes(i));
return indicators.length >= 3;
}
return false;
}
export function isColorSystem(code) {
// Detect JSON color objects
try {
const parsed = JSON.parse(code);
if (typeof parsed === "object" && !Array.isArray(parsed)) {
const values = Object.values(parsed);
return values.every((v) => typeof v === "string" && /^#[0-9a-fA-F]{6}$/.test(v));
}
if (Array.isArray(parsed)) {
return parsed.every((item) => item.name && item.hex);
}
} catch { }
return false;
}
export function UICodeBlockRenderer({ language, filename, code }) {
if (isColorSystem(code)) {
try {
return <ColorSystemPreview colors={JSON.parse(code)} />;
} catch { }
}
if (isUICodeBlock(language, filename, code)) {
return <DesignPreview code={code} title={filename || "UI Preview"} />;
}
return null; // Fallback — caller should render normal CodeBlock
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment