diff --git a/app/community/_content.tsx b/app/community/_content.tsx index a6cd3e6a..c328a225 100644 --- a/app/community/_content.tsx +++ b/app/community/_content.tsx @@ -3,11 +3,13 @@ import Image from "next/image"; import { useEffect, useMemo, useState } from "react"; import { usePathname, useRouter } from "next/navigation"; -import { Loader2, LogIn, RefreshCw, Send } from "lucide-react"; +import { Loader2, LogIn, RefreshCw, Send, TrendingUp } from "lucide-react"; import { LocalizedLink } from "@/components/i18n/localized-link"; import { useCommunityFeed } from "@/lib/swr"; import { useUser } from "@/lib/auth/use-user"; import { useI18n } from "@/lib/i18n/context"; +import { SORT_OPTIONS, type SortMethod } from "@/lib/community/leaderboard"; +import { CommunityStatsCard } from "@/components/community/community-stats-card"; const PAGE_SIZE = 12; @@ -34,6 +36,7 @@ export function CommunityContent({ const router = useRouter(); const pathname = usePathname(); const [offset, setOffset] = useState(initialOffset); + const [sortMethod, setSortMethod] = useState("recent"); const normalizedSlug = useMemo( () => initialSlug.trim().toLowerCase() || undefined, [initialSlug] @@ -94,8 +97,19 @@ export function CommunityContent({ }; return ( -
-
+
+ {/* Community Stats */} + + +
+

@@ -147,6 +161,25 @@ export function CommunityContent({

)} + {/* Sort Controls */} +
+ {SORT_OPTIONS.map((option) => ( + + ))} +
+ {error && (
{error.message || t("community.loadError")} diff --git a/app/guides/[slug]/page.tsx b/app/guides/[slug]/page.tsx new file mode 100644 index 00000000..b0da2c0d --- /dev/null +++ b/app/guides/[slug]/page.tsx @@ -0,0 +1,193 @@ +import { Metadata } from "next"; +import { notFound } from "next/navigation"; +import { Header } from "@/components/layout/header"; +import { Footer } from "@/components/layout/footer"; +import { styleGuides, generateStyleGuideMetadata } from "@/lib/seo/style-guides"; +import { LocalizedLink } from "@/components/i18n/localized-link"; + +interface StyleGuidePageProps { + params: Promise<{ slug: string }>; +} + +export async function generateMetadata({ + params, +}: StyleGuidePageProps): Promise { + const { slug } = await params; + const guide = styleGuides[slug as keyof typeof styleGuides]; + + if (!guide) { + return { + title: "Guide Not Found", + description: "The design guide you are looking for does not exist.", + }; + } + + return generateStyleGuideMetadata(guide); +} + +export default async function StyleGuidePage({ params }: StyleGuidePageProps) { + const { slug } = await params; + const guide = styleGuides[slug as keyof typeof styleGuides]; + + if (!guide) { + notFound(); + } + + return ( +
+
+
+ {/* Hero */} +
+
+
+ + ← Back to Guides + +
+ +

+ {guide.nameEn} +

+ + {guide.name !== guide.nameEn && ( +

{guide.name}

+ )} + +

+ {guide.descriptionEn} +

+ + {guide.influencedBy && guide.influencedBy.length > 0 && ( +
+ Influenced by: + {guide.influencedBy.map((style) => ( + + {style} + + ))} +
+ )} +
+
+ + {/* Philosophy */} +
+
+

Design Philosophy

+

+ {guide.philosophyEn} +

+ +
+ {/* Additional philosophy content can be added here */} +
+
+
+ + {/* History */} +
+
+

Design History

+

{guide.historyEn}

+
+
+ + {/* Use Cases */} + {guide.useCases.length > 0 && ( +
+
+

Use Cases

+ +
+ {guide.useCases.map((useCase, idx) => ( +
+

{useCase.titleEn}

+

+ {useCase.descriptionEn} +

+
+ + {useCase.industry} + +
+
+ ))} +
+
+
+ )} + + {/* References */} + {guide.references.length > 0 && ( +
+
+

References & Resources

+ +
+ {guide.references.map((ref, idx) => ( + + + {ref.type} + +
+

+ {ref.title} +

+

{ref.url}

+
+
+ ))} +
+
+
+ )} + + {/* Related Styles */} + {guide.influenced && guide.influenced.length > 0 && ( +
+
+

Related Design Styles

+ +
+ {guide.influenced.map((style) => ( + + {style} + + + ))} +
+
+
+ )} +
+
+
+ ); +} + +/** + * Generate static params for all available style guides + */ +export function generateStaticParams() { + return Object.keys(styleGuides).map((slug) => ({ slug })); +} diff --git a/app/guides/page.tsx b/app/guides/page.tsx new file mode 100644 index 00000000..93189ebc --- /dev/null +++ b/app/guides/page.tsx @@ -0,0 +1,132 @@ +import { Metadata } from "next"; +import { Header } from "@/components/layout/header"; +import { Footer } from "@/components/layout/footer"; +import { LocalizedLink } from "@/components/i18n/localized-link"; +import { styleGuides } from "@/lib/seo/style-guides"; +import { BookOpen, ArrowRight } from "lucide-react"; + +export const metadata: Metadata = { + title: "Design Style Guides - StyleKit", + description: + "Learn the history, philosophy, and best practices of popular design styles. Comprehensive guides to help you choose the right design direction for your project.", + keywords: [ + "design guide", + "design history", + "design philosophy", + "design styles", + "UI design", + ], +}; + +export default function GuidesPage() { + const guides = Object.values(styleGuides); + + return ( +
+
+
+ {/* Hero */} +
+
+
+
+ + + Design Education + +
+ +

+ Design Style Guides +

+ +

+ Deep dive into the history, philosophy, and practical applications of popular web design styles. Learn what influenced each style, when to use it, and real-world examples from leading companies. +

+ +

+ These comprehensive guides are designed to help you understand design trends, make informed choices for your projects, and improve your design literacy. +

+
+
+
+ + {/* Guides Grid */} +
+
+
+ {guides.map((guide) => ( + +
+
+

+ {guide.nameEn} +

+ {guide.name !== guide.nameEn && ( +

{guide.name}

+ )} +
+ +
+ +

+ {guide.descriptionEn} +

+ + {guide.influencedBy && guide.influencedBy.length > 0 && ( +
+ {guide.influencedBy.map((style) => ( + + {style} + + ))} +
+ )} + +
+ + {guide.useCases.length} use case + {guide.useCases.length !== 1 ? "s" : ""} + + + + {guide.references.length} reference + {guide.references.length !== 1 ? "s" : ""} + +
+
+ ))} +
+
+
+ + {/* CTA */} +
+
+
+

Ready to apply these styles?

+

+ Browse our full design style collection and start building beautiful interfaces today. +

+ + Browse All Styles + + +
+
+
+
+
+
+ ); +} diff --git a/app/recipes/[id]/_content.tsx b/app/recipes/[id]/_content.tsx new file mode 100644 index 00000000..ff419533 --- /dev/null +++ b/app/recipes/[id]/_content.tsx @@ -0,0 +1,287 @@ +"use client"; + +import { ArrowRight, Copy, Check, Sparkles, Layout, Layers, ExternalLink } from "lucide-react"; +import { useState } from "react"; +import { LocalizedLink } from "@/components/i18n/localized-link"; +import { StyleCoverPreview } from "@/components/style-preview/style-cover-preview"; +import { RecipeCard } from "@/components/recipes/recipe-card"; +import { useI18n } from "@/lib/i18n/context"; +import { + type StyleRecipe, + getRecipesByUseCase, + getRecipesByVisualStyle, +} from "@/lib/styles/recipes"; +import type { DesignStyle } from "@/lib/styles"; + +interface Props { + recipe: StyleRecipe; + visualStyle: DesignStyle | undefined; + layoutStyle: DesignStyle | undefined; +} + +export function RecipeDetailContent({ recipe, visualStyle, layoutStyle }: Props) { + const { locale } = useI18n(); + const [copied, setCopied] = useState(false); + + const name = locale === "zh" ? recipe.nameZh : recipe.name; + const description = locale === "zh" ? recipe.descriptionZh : recipe.description; + const reasoning = locale === "zh" ? recipe.reasoningZh : recipe.reasoning; + + // Related recipes + const relatedByUseCase = getRecipesByUseCase(recipe.useCase) + .filter((r) => r.id !== recipe.id) + .slice(0, 3); + const relatedByStyle = getRecipesByVisualStyle(recipe.visualStyle) + .filter((r) => r.id !== recipe.id) + .slice(0, 3); + + // Generate prompt for this recipe + const generatePrompt = () => { + const parts = [ + `Design Style: ${recipe.visualStyle}`, + `Layout Pattern: ${recipe.layout}`, + recipe.animations?.length + ? `Animations: ${recipe.animations.join(", ")}` + : null, + `Use Case: ${recipe.useCase}`, + `Tags: ${recipe.tags.join(", ")}`, + ].filter(Boolean); + + return `Create a ${recipe.useCase.replace("-", " ")} using the following design specifications: + +${parts.join("\n")} + +${reasoning}`; + }; + + const handleCopyPrompt = async () => { + const prompt = generatePrompt(); + await navigator.clipboard.writeText(prompt); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + return ( + <> + {/* Hero */} +
+
+
+ {/* Left: Info */} +
+
+ {recipe.featured && ( + + {locale === "zh" ? "精选配方" : "Featured Recipe"} + + )} + + {recipe.useCase} + +
+ +

{name}

+

{description}

+ + {/* Recipe Components */} +
+ + + {recipe.visualStyle} + + + + + {recipe.layout} + + + {recipe.animations && recipe.animations.length > 0 && ( +
+ + + {recipe.animations.length}{" "} + {locale === "zh" ? "个动画" : "animations"} + +
+ )} +
+ + {/* Tags */} +
+ {recipe.tags.map((tag) => ( + + {tag} + + ))} +
+ + {/* Reasoning */} +
+

+ {locale === "zh" ? "为什么这个组合有效" : "Why This Combination Works"} +

+

{reasoning}

+
+ + {/* Quick Copy Prompt */} + +
+ + {/* Right: Preview */} +
+ {/* Visual Style Preview */} + {visualStyle && ( +
+

+ {locale === "zh" ? "视觉风格预览" : "Visual Style Preview"} +

+ +
+ +
+
+
+

{visualStyle.name}

+

{visualStyle.nameEn}

+
+ + {locale === "zh" ? "查看展示" : "View Showcase"} + + +
+
+
+ )} + + {/* Layout Preview */} + {layoutStyle && ( +
+

+ {locale === "zh" ? "布局风格预览" : "Layout Pattern Preview"} +

+ +
+ +
+
+
+

{layoutStyle.name}

+

{layoutStyle.nameEn}

+
+ + {locale === "zh" ? "查看展示" : "View Showcase"} + + +
+
+
+ )} +
+
+
+
+ + {/* Animations */} + {recipe.animations && recipe.animations.length > 0 && ( +
+
+

+ {locale === "zh" ? "推荐动画" : "Recommended Animations"} +

+

+ {locale === "zh" ? "为此配方精选的动画效果" : "Curated Animations for This Recipe"} +

+
+ {recipe.animations.map((anim) => ( + +

{anim}

+

+ {locale === "zh" ? "查看动画" : "View Animation"} + +

+
+ ))} +
+
+
+ )} + + {/* Related Recipes */} + {(relatedByUseCase.length > 0 || relatedByStyle.length > 0) && ( +
+
+ {relatedByUseCase.length > 0 && ( +
+

+ {locale === "zh" ? "同类使用场景" : "Same Use Case"} +

+

+ {locale === "zh" + ? `其他 ${recipe.useCase} 配方` + : `Other ${recipe.useCase} Recipes`} +

+
+ {relatedByUseCase.map((r) => ( + + ))} +
+
+ )} + + {relatedByStyle.length > 0 && ( +
+

+ {locale === "zh" ? "相同视觉风格" : "Same Visual Style"} +

+

+ {locale === "zh" + ? `其他使用 ${recipe.visualStyle} 的配方` + : `Other Recipes Using ${recipe.visualStyle}`} +

+
+ {relatedByStyle.map((r) => ( + + ))} +
+
+ )} +
+
+ )} + + ); +} diff --git a/app/recipes/[id]/page.tsx b/app/recipes/[id]/page.tsx new file mode 100644 index 00000000..297f1cd4 --- /dev/null +++ b/app/recipes/[id]/page.tsx @@ -0,0 +1,83 @@ +import { notFound } from "next/navigation"; +import { Metadata } from "next"; +import { Header } from "@/components/layout/header"; +import { Footer } from "@/components/layout/footer"; +import { Breadcrumb } from "@/components/ui/breadcrumb"; +import { RecipeDetailContent } from "./_content"; +import { + getAllRecipes, + getRecipeById, + resolveRecipeStyles, +} from "@/lib/styles/recipes"; + +export function generateStaticParams() { + return getAllRecipes().map((recipe) => ({ + id: recipe.id, + })); +} + +export async function generateMetadata({ + params, +}: { + params: Promise<{ id: string }>; +}): Promise { + const { id } = await params; + const recipe = getRecipeById(id); + + if (!recipe) { + return { title: "Recipe Not Found" }; + } + + return { + title: `${recipe.name} - Design Recipe | StyleKit`, + description: recipe.description, + keywords: [ + recipe.name, + recipe.visualStyle, + recipe.layout, + recipe.useCase, + ...recipe.tags, + ], + }; +} + +export default async function RecipeDetailPage({ + params, +}: { + params: Promise<{ id: string }>; +}) { + const { id } = await params; + const recipe = getRecipeById(id); + + if (!recipe) { + notFound(); + } + + const { visual, layout } = resolveRecipeStyles(recipe); + + return ( +
+
+ +
+
+ +
+ + +
+ +
+
+ ); +} diff --git a/app/recipes/page.tsx b/app/recipes/page.tsx new file mode 100644 index 00000000..7ce98eb0 --- /dev/null +++ b/app/recipes/page.tsx @@ -0,0 +1,68 @@ +import { Metadata } from "next"; +import { Header } from "@/components/layout/header"; +import { Footer } from "@/components/layout/footer"; +import { Breadcrumb } from "@/components/ui/breadcrumb"; +import { RecipeShowcase } from "@/components/recipes/recipe-showcase"; +import { getAllRecipes } from "@/lib/styles/recipes"; + +export const metadata: Metadata = { + title: "Design Recipes - Ready-to-Use Style Combinations | StyleKit", + description: + "Curated combinations of visual styles, layouts, and animations optimized for specific use cases. SaaS, e-commerce, portfolio, blog, and more.", + keywords: [ + "design recipes", + "style combinations", + "UI patterns", + "SaaS design", + "landing page templates", + "design system", + ], +}; + +export default function RecipesPage() { + const allRecipes = getAllRecipes(); + + return ( +
+
+ +
+
+ +
+ + {/* Hero */} +
+
+

+ Design Recipes +

+

+ Curated combinations of visual styles, layouts, and animations. + Each recipe is optimized for a specific use case and includes + reasoning for why it works. +

+
+ + {allRecipes.length} Recipes + + + {allRecipes.filter((r) => r.featured).length} Featured + +
+
+
+ + {/* Recipe Showcase */} + +
+ +
+
+ ); +} diff --git a/app/sitemap.ts b/app/sitemap.ts index 611066ee..53dae798 100644 --- a/app/sitemap.ts +++ b/app/sitemap.ts @@ -5,6 +5,7 @@ import { getAllStylesMeta } from "@/lib/styles/meta"; import { getAllAnimationsMeta } from "@/lib/animations/meta"; import { getAllTopicSlugs } from "@/lib/prompts"; import { getAllPosts } from "@/lib/blog"; +import { styleGuides } from "@/lib/seo/style-guides"; import { getAlternateLocalePath, getBaseUrl, @@ -57,6 +58,8 @@ export default function sitemap(): MetadataRoute.Sitemap { const staticPages: MetadataRoute.Sitemap = [ ...createLocalizedEntries("/", CONTENT_UPDATED, "weekly", 1), ...createLocalizedEntries("/styles", CONTENT_UPDATED, "weekly", 0.9), + ...createLocalizedEntries("/guides", CONTENT_UPDATED, "monthly", 0.8), + ...createLocalizedEntries("/recipes", CONTENT_UPDATED, "weekly", 0.8), ...createLocalizedEntries("/ui-prompts", CONTENT_UPDATED, "weekly", 0.9), ...createLocalizedEntries("/landing-page-prompts", CONTENT_UPDATED, "weekly", 0.8), ...createLocalizedEntries("/dashboard-prompts", CONTENT_UPDATED, "weekly", 0.8), @@ -113,6 +116,10 @@ export default function sitemap(): MetadataRoute.Sitemap { ) ); + const guidePages: MetadataRoute.Sitemap = Object.values(styleGuides).flatMap((guide) => + createLocalizedEntries(`/guides/${guide.slug}`, CONTENT_UPDATED, "monthly", 0.7) + ); + return [ ...staticPages, ...stylePages, @@ -121,5 +128,6 @@ export default function sitemap(): MetadataRoute.Sitemap { ...promptPages, ...animationPages, ...blogPostPages, + ...guidePages, ]; } diff --git a/app/styles/[slug]/_content.tsx b/app/styles/[slug]/_content.tsx index 5e10afa5..d8d101da 100644 --- a/app/styles/[slug]/_content.tsx +++ b/app/styles/[slug]/_content.tsx @@ -28,6 +28,8 @@ import type { DesignStyle } from "@/lib/styles"; import type { AccessibilityScore } from "@/lib/accessibility"; import type { StyleVersion } from "@/lib/versioning"; import type { RuntimeStyleSource } from "@/lib/styles/community-runtime"; +import { getRecipesByVisualStyle, getRecipesByLayout } from "@/lib/styles/recipes"; +import { RecipeCard } from "@/components/recipes/recipe-card"; interface Props { style: DesignStyle; @@ -74,6 +76,11 @@ export function StyleDetailContent({ cancelAnimationFrame(rafId); }; }, [updateShowcaseScale]); + // Get related recipes + const relatedRecipes = style.styleType === "layout" + ? getRecipesByLayout(style.slug).slice(0, 3) + : getRecipesByVisualStyle(style.slug).slice(0, 3); + const localizedDescription = localizedString( locale, style.description, @@ -483,6 +490,32 @@ export function StyleDetailContent({
)} + {/* Related Recipes */} + {relatedRecipes.length > 0 && ( +
+
+

+ {locale === "zh" ? "设计配方" : "Design Recipes"} +

+

+ {locale === "zh" + ? `使用 ${style.name} 的推荐组合` + : `Recommended Combinations with ${style.nameEn}`} +

+

+ {locale === "zh" + ? "这些精选配方将此风格与布局和动画组合,针对特定场景优化。" + : "These curated recipes combine this style with layouts and animations, optimized for specific use cases."} +

+
+ {relatedRecipes.map((recipe) => ( + + ))} +
+
+
+ )} + {/* SEO Extended */}
diff --git a/components/community/community-stats-card.tsx b/components/community/community-stats-card.tsx new file mode 100644 index 00000000..891b99cd --- /dev/null +++ b/components/community/community-stats-card.tsx @@ -0,0 +1,84 @@ +import { Heart, Eye, Share2, TrendingUp, Clock, Award } from "lucide-react"; + +export interface CommunityStats { + totalSubmissions: number; + totalCollaborators: number; + recentSubmissions: number; + topStyle: { + title: string; + author: string; + likes: number; + views: number; + } | null; +} + +interface CommunityStatsCardProps { + stats: CommunityStats; +} + +export function CommunityStatsCard({ stats }: CommunityStatsCardProps) { + const statItems = [ + { + icon: , + label: "Total Styles", + value: stats.totalSubmissions.toLocaleString(), + color: "bg-blue-500/10 text-blue-600 dark:text-blue-400", + }, + { + icon: , + label: "Contributors", + value: stats.totalCollaborators.toLocaleString(), + color: "bg-purple-500/10 text-purple-600 dark:text-purple-400", + }, + { + icon: , + label: "This Month", + value: stats.recentSubmissions.toLocaleString(), + color: "bg-green-500/10 text-green-600 dark:text-green-400", + }, + ]; + + return ( +
+
+
+ {statItems.map((item, idx) => ( +
+ {item.icon} +
+
{item.label}
+
{item.value}
+
+
+ ))} +
+ + {stats.topStyle && ( +
+
+ + Featured This Month +
+

{stats.topStyle.title}

+
+
by {stats.topStyle.author}
+
+
+ + {stats.topStyle.likes} +
+
+ + {stats.topStyle.views} +
+
+
+
+ )} +
+
+ ); +} diff --git a/components/home/home-content.tsx b/components/home/home-content.tsx index 011731cd..873532e2 100644 --- a/components/home/home-content.tsx +++ b/components/home/home-content.tsx @@ -27,6 +27,11 @@ const BuiltForSection = dynamic( { ssr: true } ); +const RecipeShowcase = dynamic( + () => import("@/components/recipes/recipe-showcase").then((m) => ({ default: m.RecipeShowcase })), + { ssr: true } +); + import type { StyleMeta } from "@/lib/styles/meta"; import { getScenarioLabel, @@ -606,6 +611,8 @@ export function HomeContent({ styles, stats }: HomeContentProps) { + + diff --git a/components/playground/playground-container.tsx b/components/playground/playground-container.tsx index b4a020de..7913d987 100644 --- a/components/playground/playground-container.tsx +++ b/components/playground/playground-container.tsx @@ -10,11 +10,13 @@ import { styleTokensRegistry } from "@/lib/styles/tokens-registry"; import { getAllArchetypes } from "@/lib/archetypes"; import type { StyleTokens } from "@/lib/styles/tokens"; -import { getTemplateCode } from "@/lib/playground/template-code"; -import { PlaygroundPreview } from "./playground-preview"; -import { PlaygroundToolbar } from "./playground-toolbar"; -import { StyleSwitcher } from "./style-switcher"; -import { TemplateSelector } from "./template-selector"; +import { getTemplateCode } from "@/lib/playground/template-code"; +import { PlaygroundPreview, type ElementInfo } from "./playground-preview"; +import { PlaygroundToolbar } from "./playground-toolbar"; +import { StyleSwitcher } from "./style-switcher"; +import { TemplateSelector } from "./template-selector"; +import { StyleComparison } from "./style-comparison"; +import { ProjectExport } from "./project-export"; const PlaygroundEditor = dynamic( () => @@ -108,57 +110,112 @@ function getDefaultCode(): string {
`; } -/** - * Convert StyleTokens into a CSS string that can be injected into the iframe. - * Extracts key color values from Tailwind class references. - */ -function tokensToCSS(tokens: StyleTokens): string { - const lines: string[] = []; - - // Extract background colors - const bgPrimary = tokens.colors.background.primary; - const bgSecondary = tokens.colors.background.secondary; - - // Extract text colors - const textPrimary = tokens.colors.text.primary; - - // Build a basic CSS based on token info - lines.push("/* StyleKit Token Overrides */"); - - // Typography - if (tokens.typography.heading.includes("font-serif")) { - lines.push("h1, h2, h3, h4, h5, h6 { font-family: Georgia, 'Times New Roman', serif; }"); - } - if (tokens.typography.body.includes("font-mono")) { - lines.push("body, p { font-family: 'JetBrains Mono', 'Fira Code', monospace; }"); - } - if (tokens.typography.body.includes("font-sans")) { - lines.push("body, p { font-family: system-ui, -apple-system, sans-serif; }"); - } - - // Border radius - if (tokens.border.radius.includes("rounded-none")) { - lines.push("* { --tw-border-radius: 0; }"); - lines.push("button, input, .card, [class*='rounded'] { border-radius: 0 !important; }"); - } else if (tokens.border.radius.includes("rounded-full")) { - lines.push("button { border-radius: 9999px; }"); - } else if (tokens.border.radius.includes("rounded-2xl") || tokens.border.radius.includes("rounded-3xl")) { - lines.push("button, .card { border-radius: 1rem; }"); - } - - // Additional styling hints from token color classes - if (bgPrimary.includes("bg-black") || bgPrimary.includes("bg-zinc-950") || bgPrimary.includes("bg-gray-950")) { - lines.push("body { background-color: #09090b; color: #fafafa; }"); - } - if (textPrimary.includes("text-white")) { - lines.push("body { color: #ffffff; }"); - } - if (bgSecondary.includes("bg-zinc-900")) { - lines.push(".card, section:nth-child(even) { background-color: #18181b; }"); - } - - return lines.join("\n"); -} +/** + * Convert StyleTokens into a CSS string that can be injected into the iframe. + * Extracts key color values from Tailwind class references. + */ +function tokensToCSS(tokens: StyleTokens): string { + const lines: string[] = []; + + // Extract background colors + const bgPrimary = tokens.colors.background.primary; + const bgSecondary = tokens.colors.background.secondary; + + // Extract text colors + const textPrimary = tokens.colors.text.primary; + + // Build a basic CSS based on token info + lines.push("/* StyleKit Token Overrides */"); + + // Typography - 更完整的字体处理 + if (tokens.typography.heading.includes("font-serif")) { + lines.push("h1, h2, h3, h4, h5, h6 { font-family: Georgia, 'Times New Roman', serif; }"); + } + if (tokens.typography.body.includes("font-mono")) { + lines.push("body, p { font-family: 'JetBrains Mono', 'Fira Code', monospace; }"); + } + if (tokens.typography.body.includes("font-sans")) { + lines.push("body, p { font-family: system-ui, -apple-system, sans-serif; }"); + } + + // 字重处理 + if (tokens.typography.heading.includes("font-black")) { + lines.push("h1, h2, h3 { font-weight: 900; }"); + } else if (tokens.typography.heading.includes("font-bold")) { + lines.push("h1, h2, h3 { font-weight: 700; }"); + } else if (tokens.typography.heading.includes("font-medium")) { + lines.push("h1, h2, h3 { font-weight: 500; }"); + } + + // 行高处理 + if (tokens.typography.body.includes("leading-relaxed")) { + lines.push("body, p { line-height: 1.625; }"); + } else if (tokens.typography.body.includes("leading-loose")) { + lines.push("body, p { line-height: 2; }"); + } + + // Border radius - 更细致的圆角处理 + if (tokens.border.radius.includes("rounded-none")) { + lines.push("button, input, .card, [class*='rounded'] { border-radius: 0 !important; }"); + } else if (tokens.border.radius.includes("rounded-full")) { + lines.push("button { border-radius: 9999px; }"); + } else if (tokens.border.radius.includes("rounded-3xl")) { + lines.push("button, .card { border-radius: 1.5rem; }"); + } else if (tokens.border.radius.includes("rounded-2xl")) { + lines.push("button, .card { border-radius: 1rem; }"); + } else if (tokens.border.radius.includes("rounded-xl")) { + lines.push("button, .card { border-radius: 0.75rem; }"); + } else if (tokens.border.radius.includes("rounded-lg")) { + lines.push("button, .card { border-radius: 0.5rem; }"); + } + + // 边框样式 + if (tokens.border.style.includes("border-4") || tokens.border.style.includes("border-3")) { + lines.push("button, .card { border-width: 3px; }"); + } else if (tokens.border.style.includes("border-2")) { + lines.push("button, .card { border-width: 2px; }"); + } + + // 阴影样式 + if (tokens.shadow.card.includes("shadow-brutal") || tokens.shadow.card.includes("shadow-[")) { + // Neo-brutalist 风格阴影 + if (tokens.shadow.card.includes("4px_4px") || tokens.shadow.card.includes("6px_6px")) { + lines.push(".card, button { box-shadow: 4px 4px 0px 0px rgba(0,0,0,1); }"); + lines.push("button:hover { transform: translate(2px, 2px); box-shadow: 2px 2px 0px 0px rgba(0,0,0,1); }"); + } + } else if (tokens.shadow.card.includes("shadow-lg")) { + lines.push(".card { box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1); }"); + } else if (tokens.shadow.card.includes("shadow-xl")) { + lines.push(".card { box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1); }"); + } + + // Background color themes + if (bgPrimary.includes("bg-black") || bgPrimary.includes("bg-zinc-950") || bgPrimary.includes("bg-gray-950")) { + lines.push("body { background-color: #09090b; color: #fafafa; }"); + lines.push("a { color: #60a5fa; }"); + } else if (bgPrimary.includes("bg-zinc-900") || bgPrimary.includes("bg-gray-900")) { + lines.push("body { background-color: #18181b; color: #f4f4f5; }"); + } else if (bgPrimary.includes("bg-slate-900")) { + lines.push("body { background-color: #0f172a; color: #f1f5f9; }"); + } + + if (textPrimary.includes("text-white")) { + lines.push("body { color: #ffffff; }"); + } + if (bgSecondary.includes("bg-zinc-900")) { + lines.push(".card, section:nth-child(even) { background-color: #18181b; }"); + } else if (bgSecondary.includes("bg-zinc-800")) { + lines.push(".card, section:nth-child(even) { background-color: #27272a; }"); + } + + // 按钮过渡效果 + lines.push("button { transition: all 0.15s ease; }"); + + // 链接悬停效果 + lines.push("a:hover { opacity: 0.8; }"); + + return lines.join("\n"); +} export function PlaygroundContainer() { const searchParams = useSearchParams(); @@ -194,9 +251,12 @@ export function PlaygroundContainer() { const [styleSlug, setStyleSlug] = useState(initialState.style); const [templateId, setTemplateId] = useState(initialState.template); const [deviceSize, setDeviceSize] = useState("desktop"); - const [editorVisible, setEditorVisible] = useState(true); - const [templatesVisible, setTemplatesVisible] = useState(false); - const [editorTab, setEditorTab] = useState("code"); + const [editorVisible, setEditorVisible] = useState(true); + const [templatesVisible, setTemplatesVisible] = useState(false); + const [editorTab, setEditorTab] = useState("code"); + const [comparisonVisible, setComparisonVisible] = useState(false); + const [exportVisible, setExportVisible] = useState(false); + const [selectedElementInfo, setSelectedElementInfo] = useState(null); // Get style metadata for switcher const stylesForSwitcher = useMemo( @@ -263,10 +323,20 @@ export function PlaygroundContainer() { router.replace("/playground", { scroll: false }); }, [router]); - // Handle editor change - const handleCodeChange = useCallback((newCode: string) => { - setCode(newCode); - }, []); + // Handle editor change + const handleCodeChange = useCallback((newCode: string) => { + setCode(newCode); + }, []); + + // Handle element selection from preview inspector + const handleElementSelect = useCallback((info: ElementInfo | null) => { + setSelectedElementInfo(info); + // 如果选中了元素,尝试在代码中搜索相关类名 + if (info && info.classes.length > 0) { + // 可以后续扩展:高亮编辑器中的相关行 + console.log('[v0] Selected element:', info); + } + }, []); return (
@@ -279,10 +349,12 @@ export function PlaygroundContainer() { deviceSize={deviceSize} onDeviceSizeChange={setDeviceSize} editorVisible={editorVisible} - onToggleEditor={() => setEditorVisible((v) => !v)} - onToggleTemplates={() => setTemplatesVisible((v) => !v)} - templatesVisible={templatesVisible} - /> + onToggleEditor={() => setEditorVisible((v) => !v)} + onToggleTemplates={() => setTemplatesVisible((v) => !v)} + templatesVisible={templatesVisible} + onOpenComparison={() => setComparisonVisible(true)} + onOpenExport={() => setExportVisible(true)} + /> {/* Style switcher bar */}
@@ -377,16 +449,36 @@ export function PlaygroundContainer() { maxWidth: "100%", }} > - -
-
-
- - - ); -} + + + + + + + {/* Style Comparison Modal */} + {comparisonVisible && ( + setComparisonVisible(false)} + /> + )} + + {/* Project Export Modal */} + {exportVisible && ( + setExportVisible(false)} + /> + )} + + ); +} diff --git a/components/playground/playground-preview.tsx b/components/playground/playground-preview.tsx index 080a3f37..e2b2a70b 100644 --- a/components/playground/playground-preview.tsx +++ b/components/playground/playground-preview.tsx @@ -1,84 +1,420 @@ -"use client"; - -import { useState, useEffect, useRef, useMemo } from "react"; -import { Loader2 } from "lucide-react"; - -interface PlaygroundPreviewProps { - code: string; - styleSlug: string; - /** CSS variables/tokens to inject as inline styles */ - tokenCss: string; - deviceWidth?: number; -} - -function buildSrcdoc(code: string, tokenCss: string): string { - // Escape closing script tags in user code to prevent srcdoc breakout - const safeCode = code.replace(/<\/script/gi, "<\\/script"); - - return ` - - - - - + +`; +} + +function generateStandaloneHtml(code: string, css: string, styleName: string): string { + return ` + + + + + ${styleName} - Built with StyleKit + + + + +${code} + +`; +} + +async function downloadZip(files: Record, styleSlug: string) { + // Create a simple text-based download for now + // In production, you'd use a library like JSZip + const content = Object.entries(files) + .map(([filename, fileContent]) => `// ===== ${filename} =====\n\n${fileContent}`) + .join("\n\n"); + + const blob = new Blob([content], { type: "text/plain" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `stylekit-${styleSlug}-project.txt`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); +} diff --git a/components/playground/style-comparison.tsx b/components/playground/style-comparison.tsx new file mode 100644 index 00000000..1e47f06c --- /dev/null +++ b/components/playground/style-comparison.tsx @@ -0,0 +1,145 @@ +"use client"; + +import { useState, useMemo } from "react"; +import { ChevronDown, X, ArrowLeftRight } from "lucide-react"; +import { stylesMeta, type StyleMeta } from "@/lib/styles/meta"; +import { useI18n } from "@/lib/i18n/context"; + +interface StyleComparisonProps { + baseStyleSlug: string; + code: string; + onClose: () => void; +} + +export function StyleComparison({ baseStyleSlug, code, onClose }: StyleComparisonProps) { + const { locale } = useI18n(); + const [compareSlug, setCompareSlug] = useState(""); + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + + const styles = useMemo(() => { + return stylesMeta + .filter((s) => s.styleType === "visual" && s.slug !== baseStyleSlug) + .slice(0, 30); + }, [baseStyleSlug]); + + const baseStyle = stylesMeta.find((s) => s.slug === baseStyleSlug); + const compareStyle = stylesMeta.find((s) => s.slug === compareSlug); + + return ( +
+ {/* Header */} +
+
+

+ + {locale === "zh" ? "风格对比" : "Style Comparison"} +

+ + {/* Base style badge */} +
+ + {locale === "zh" ? "基础:" : "Base:"} + + {baseStyle?.name || baseStyleSlug} +
+ + {/* VS */} + vs + + {/* Compare style selector */} +
+ + + {isDropdownOpen && ( +
+ {styles.map((style) => ( + + ))} +
+ )} +
+
+ + +
+ + {/* Comparison view */} +
+ {/* Left: Base style */} +
+
+ + {baseStyle?.name || baseStyleSlug} + +
+
+