mirror of
https://github.com/nextlevelbuilder/ui-ux-pro-max-skill.git
synced 2026-08-01 10:56:17 +00:00
feat(gallery): add Style Detail Modal with phone UI controls preview (#155)
Click any StyleCard to open a full-screen modal showing the style applied to 9 interactive phone UI controls (buttons, inputs, toggles, checkboxes, radios, cards, badges, slider, nav bar) inside an iPhone mockup frame. Co-authored-by: yongtang <jlinux@msn.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
b0ebb1797e
commit
14ddef5c05
3
gallery/.gitignore
vendored
Normal file
3
gallery/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
node_modules/
|
||||
.next/
|
||||
out/
|
||||
22
gallery/app/globals.css
Normal file
22
gallery/app/globals.css
Normal file
@ -0,0 +1,22 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
body {
|
||||
@apply bg-gray-50 text-gray-900 dark:bg-gray-950 dark:text-gray-100;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.scrollbar-thin {
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
.scrollbar-thin::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
.scrollbar-thin::-webkit-scrollbar-thumb {
|
||||
@apply bg-gray-300 dark:bg-gray-700 rounded-full;
|
||||
}
|
||||
}
|
||||
22
gallery/app/layout.tsx
Normal file
22
gallery/app/layout.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Providers } from "./providers";
|
||||
import "./globals.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Style Gallery — UI/UX Pro Max",
|
||||
description: "Browse and explore 67 UI styles with live previews, color palettes, and implementation details.",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body className="min-h-screen antialiased">
|
||||
<Providers>{children}</Providers>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
16
gallery/app/page.tsx
Normal file
16
gallery/app/page.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { parseStylesCSV } from "@/lib/parseStyles";
|
||||
import { GalleryGrid } from "@/components/GalleryGrid";
|
||||
|
||||
export default function Home() {
|
||||
const csvPath = path.join(process.cwd(), "data", "styles.csv");
|
||||
const csvContent = fs.readFileSync(csvPath, "utf-8");
|
||||
const styles = parseStylesCSV(csvContent);
|
||||
|
||||
return (
|
||||
<main>
|
||||
<GalleryGrid styles={styles} />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
11
gallery/app/providers.tsx
Normal file
11
gallery/app/providers.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { ThemeProvider } from "next-themes";
|
||||
|
||||
export function Providers({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
50
gallery/components/CodeSnippet.tsx
Normal file
50
gallery/components/CodeSnippet.tsx
Normal file
@ -0,0 +1,50 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
interface CodeSnippetProps {
|
||||
code: string;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export function CodeSnippet({ code, label }: CodeSnippetProps) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
async function handleCopy() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(code);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch {
|
||||
// Fallback: ignore
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative group">
|
||||
{label && (
|
||||
<span className="text-[10px] font-medium text-gray-400 uppercase tracking-wider">
|
||||
{label}
|
||||
</span>
|
||||
)}
|
||||
<pre className="mt-1 p-3 text-xs bg-gray-50 dark:bg-gray-900 rounded-lg overflow-x-auto scrollbar-thin font-mono text-gray-700 dark:text-gray-300 leading-relaxed">
|
||||
{code}
|
||||
</pre>
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className="absolute top-2 right-2 p-1.5 rounded-md bg-gray-200/80 dark:bg-gray-700/80 text-gray-500 dark:text-gray-400 opacity-0 group-hover:opacity-100 transition-opacity hover:bg-gray-300 dark:hover:bg-gray-600"
|
||||
title="Copy to clipboard"
|
||||
>
|
||||
{copied ? (
|
||||
<svg className="w-3.5 h-3.5 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
47
gallery/components/ColorPalette.tsx
Normal file
47
gallery/components/ColorPalette.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
interface ColorPaletteProps {
|
||||
colors: string[];
|
||||
}
|
||||
|
||||
export function ColorPalette({ colors }: ColorPaletteProps) {
|
||||
const [copiedIdx, setCopiedIdx] = useState<number | null>(null);
|
||||
|
||||
if (colors.length === 0) return null;
|
||||
|
||||
async function copyColor(color: string, idx: number) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(color);
|
||||
setCopiedIdx(idx);
|
||||
setTimeout(() => setCopiedIdx(null), 2000);
|
||||
} catch {
|
||||
// Fallback: ignore
|
||||
}
|
||||
}
|
||||
|
||||
// Show at most 8 colors
|
||||
const displayed = colors.slice(0, 8);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1.5">
|
||||
{displayed.map((color, i) => (
|
||||
<div key={`${color}-${i}`} className="group relative">
|
||||
<button
|
||||
onClick={() => copyColor(color, i)}
|
||||
className="w-6 h-6 rounded-full border border-gray-200 dark:border-gray-700 transition-transform hover:scale-125 focus:outline-none focus:ring-2 focus:ring-blue-400"
|
||||
style={{ backgroundColor: color }}
|
||||
title={color}
|
||||
/>
|
||||
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-1 px-1.5 py-0.5 text-[10px] font-mono bg-gray-900 text-white rounded opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity whitespace-nowrap z-10">
|
||||
{copiedIdx === i ? "Copied!" : color}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{colors.length > 8 && (
|
||||
<span className="text-xs text-gray-400">+{colors.length - 8}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
35
gallery/components/DarkModeToggle.tsx
Normal file
35
gallery/components/DarkModeToggle.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
"use client";
|
||||
|
||||
import { useTheme } from "next-themes";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export function DarkModeToggle() {
|
||||
const { theme, setTheme } = useTheme();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => setMounted(true), []);
|
||||
|
||||
if (!mounted) {
|
||||
return <div className="w-9 h-9" />;
|
||||
}
|
||||
|
||||
const isDark = theme === "dark";
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={() => setTheme(isDark ? "light" : "dark")}
|
||||
className="p-2 rounded-lg bg-gray-200 dark:bg-gray-800 hover:bg-gray-300 dark:hover:bg-gray-700 transition-colors"
|
||||
aria-label={isDark ? "Switch to light mode" : "Switch to dark mode"}
|
||||
>
|
||||
{isDark ? (
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
37
gallery/components/ExpandableSection.tsx
Normal file
37
gallery/components/ExpandableSection.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
interface ExpandableSectionProps {
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
defaultOpen?: boolean;
|
||||
}
|
||||
|
||||
export function ExpandableSection({
|
||||
title,
|
||||
children,
|
||||
defaultOpen = false,
|
||||
}: ExpandableSectionProps) {
|
||||
const [open, setOpen] = useState(defaultOpen);
|
||||
|
||||
return (
|
||||
<div className="border-t border-gray-100 dark:border-gray-800">
|
||||
<button
|
||||
onClick={() => setOpen(!open)}
|
||||
className="w-full flex items-center justify-between py-2 px-1 text-left text-sm font-medium text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 transition-colors"
|
||||
>
|
||||
<span>{title}</span>
|
||||
<svg
|
||||
className={`w-4 h-4 transition-transform ${open ? "rotate-90" : ""}`}
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
{open && <div className="pb-3 px-1">{children}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
111
gallery/components/FilterBar.tsx
Normal file
111
gallery/components/FilterBar.tsx
Normal file
@ -0,0 +1,111 @@
|
||||
"use client";
|
||||
|
||||
import { Filters } from "@/lib/filterStyles";
|
||||
|
||||
interface FilterBarProps {
|
||||
filters: Filters;
|
||||
onChange: (filters: Filters) => void;
|
||||
types: string[];
|
||||
complexities: string[];
|
||||
eras: string[];
|
||||
resultCount: number;
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
function FilterGroup({
|
||||
label,
|
||||
options,
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
label: string;
|
||||
options: string[];
|
||||
value: string;
|
||||
onChange: (v: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
<span className="text-xs font-medium text-gray-500 dark:text-gray-400 mr-1">
|
||||
{label}:
|
||||
</span>
|
||||
<button
|
||||
onClick={() => onChange("")}
|
||||
className={`px-2.5 py-1 text-xs rounded-full transition-colors ${
|
||||
value === ""
|
||||
? "bg-blue-500 text-white"
|
||||
: "bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700"
|
||||
}`}
|
||||
>
|
||||
All
|
||||
</button>
|
||||
{options.map((opt) => (
|
||||
<button
|
||||
key={opt}
|
||||
onClick={() => onChange(value === opt ? "" : opt)}
|
||||
className={`px-2.5 py-1 text-xs rounded-full transition-colors ${
|
||||
value === opt
|
||||
? "bg-blue-500 text-white"
|
||||
: "bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700"
|
||||
}`}
|
||||
>
|
||||
{opt}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function FilterBar({
|
||||
filters,
|
||||
onChange,
|
||||
types,
|
||||
complexities,
|
||||
eras,
|
||||
resultCount,
|
||||
totalCount,
|
||||
}: FilterBarProps) {
|
||||
const hasFilters =
|
||||
filters.search || filters.type || filters.complexity || filters.era;
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<FilterGroup
|
||||
label="Type"
|
||||
options={types}
|
||||
value={filters.type}
|
||||
onChange={(type) => onChange({ ...filters, type })}
|
||||
/>
|
||||
<FilterGroup
|
||||
label="Complexity"
|
||||
options={complexities}
|
||||
value={filters.complexity}
|
||||
onChange={(complexity) => onChange({ ...filters, complexity })}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<FilterGroup
|
||||
label="Era"
|
||||
options={eras}
|
||||
value={filters.era}
|
||||
onChange={(era) => onChange({ ...filters, era })}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Showing <span className="font-semibold text-gray-700 dark:text-gray-200">{resultCount}</span> of {totalCount} styles
|
||||
</p>
|
||||
{hasFilters && (
|
||||
<button
|
||||
onClick={() =>
|
||||
onChange({ search: "", type: "", complexity: "", era: "" })
|
||||
}
|
||||
className="text-xs text-blue-500 hover:text-blue-600 transition-colors"
|
||||
>
|
||||
Clear all filters
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
103
gallery/components/GalleryGrid.tsx
Normal file
103
gallery/components/GalleryGrid.tsx
Normal file
@ -0,0 +1,103 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { StyleData } from "@/lib/types";
|
||||
import {
|
||||
Filters,
|
||||
DEFAULT_FILTERS,
|
||||
filterStyles,
|
||||
getUniqueValues,
|
||||
} from "@/lib/filterStyles";
|
||||
import { StyleCard } from "./StyleCard";
|
||||
import { StyleDetailModal } from "./StyleDetailModal";
|
||||
import { SearchInput } from "./SearchInput";
|
||||
import { FilterBar } from "./FilterBar";
|
||||
import { DarkModeToggle } from "./DarkModeToggle";
|
||||
|
||||
interface GalleryGridProps {
|
||||
styles: StyleData[];
|
||||
}
|
||||
|
||||
export function GalleryGrid({ styles }: GalleryGridProps) {
|
||||
const [filters, setFilters] = useState<Filters>(DEFAULT_FILTERS);
|
||||
const [selectedStyle, setSelectedStyle] = useState<StyleData | null>(null);
|
||||
|
||||
const types = useMemo(() => getUniqueValues(styles, "type"), [styles]);
|
||||
const complexities = useMemo(
|
||||
() => getUniqueValues(styles, "complexity"),
|
||||
[styles]
|
||||
);
|
||||
const eras = useMemo(() => getUniqueValues(styles, "eraOrigin"), [styles]);
|
||||
|
||||
const filtered = useMemo(
|
||||
() => filterStyles(styles, filters),
|
||||
[styles, filters]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100">
|
||||
Style Gallery
|
||||
</h1>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||
Browse {styles.length} UI styles from Antigravity Kit
|
||||
</p>
|
||||
</div>
|
||||
<DarkModeToggle />
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
<div className="mb-4">
|
||||
<SearchInput
|
||||
value={filters.search}
|
||||
onChange={(search) => setFilters((f) => ({ ...f, search }))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<div className="mb-6">
|
||||
<FilterBar
|
||||
filters={filters}
|
||||
onChange={setFilters}
|
||||
types={types}
|
||||
complexities={complexities}
|
||||
eras={eras}
|
||||
resultCount={filtered.length}
|
||||
totalCount={styles.length}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Grid */}
|
||||
{filtered.length > 0 ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{filtered.map((style) => (
|
||||
<StyleCard key={style.no} style={style} onSelect={setSelectedStyle} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-16">
|
||||
<p className="text-gray-500 dark:text-gray-400">
|
||||
No styles match your filters.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => setFilters(DEFAULT_FILTERS)}
|
||||
className="mt-2 text-sm text-blue-500 hover:text-blue-600"
|
||||
>
|
||||
Clear all filters
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Style Detail Modal */}
|
||||
{selectedStyle && (
|
||||
<StyleDetailModal
|
||||
style={selectedStyle}
|
||||
onClose={() => setSelectedStyle(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
62
gallery/components/InteractiveChecklist.tsx
Normal file
62
gallery/components/InteractiveChecklist.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
interface InteractiveChecklistProps {
|
||||
checklist: string;
|
||||
}
|
||||
|
||||
function parseItems(raw: string): string[] {
|
||||
return raw
|
||||
.split(/,\s*(?=☐)/)
|
||||
.map((s) => s.replace(/^☐\s*/, "").trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
export function InteractiveChecklist({ checklist }: InteractiveChecklistProps) {
|
||||
const items = parseItems(checklist);
|
||||
const [checked, setChecked] = useState<Set<number>>(new Set());
|
||||
|
||||
if (items.length === 0) return null;
|
||||
|
||||
function toggle(idx: number) {
|
||||
setChecked((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(idx)) next.delete(idx);
|
||||
else next.add(idx);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="text-[10px] text-gray-400 mb-2">
|
||||
{checked.size} of {items.length} completed
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
{items.map((item, i) => (
|
||||
<label
|
||||
key={i}
|
||||
className="flex items-start gap-2 cursor-pointer group"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked.has(i)}
|
||||
onChange={() => toggle(i)}
|
||||
className="mt-0.5 rounded border-gray-300 dark:border-gray-600 text-blue-500 focus:ring-blue-500"
|
||||
/>
|
||||
<span
|
||||
className={`text-xs leading-relaxed transition-colors ${
|
||||
checked.has(i)
|
||||
? "line-through text-gray-400 dark:text-gray-600"
|
||||
: "text-gray-700 dark:text-gray-300"
|
||||
}`}
|
||||
>
|
||||
{item}
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
57
gallery/components/MetadataBadges.tsx
Normal file
57
gallery/components/MetadataBadges.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import { StyleData } from "@/lib/types";
|
||||
|
||||
interface MetadataBadgesProps {
|
||||
style: StyleData;
|
||||
}
|
||||
|
||||
function Badge({
|
||||
label,
|
||||
variant,
|
||||
}: {
|
||||
label: string;
|
||||
variant: "green" | "yellow" | "red" | "blue" | "gray";
|
||||
}) {
|
||||
const colors = {
|
||||
green: "bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400",
|
||||
yellow: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400",
|
||||
red: "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400",
|
||||
blue: "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400",
|
||||
gray: "bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-400",
|
||||
};
|
||||
|
||||
return (
|
||||
<span className={`inline-flex items-center px-2 py-0.5 text-[10px] font-medium rounded-full ${colors[variant]}`}>
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function getComplexityVariant(c: string): "green" | "yellow" | "red" {
|
||||
if (c === "Low") return "green";
|
||||
if (c === "Medium") return "yellow";
|
||||
return "red";
|
||||
}
|
||||
|
||||
function getPerfVariant(p: string): "green" | "yellow" | "red" {
|
||||
if (p.includes("Excellent")) return "green";
|
||||
if (p.includes("Good")) return "yellow";
|
||||
return "red";
|
||||
}
|
||||
|
||||
function getA11yVariant(a: string): "green" | "yellow" | "red" {
|
||||
if (a.includes("AAA")) return "green";
|
||||
if (a.includes("AA") || a.includes("Good")) return "yellow";
|
||||
return "red";
|
||||
}
|
||||
|
||||
export function MetadataBadges({ style }: MetadataBadgesProps) {
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
<Badge label={style.complexity} variant={getComplexityVariant(style.complexity)} />
|
||||
<Badge label={style.performance.replace(/[⚡❌⚠]/g, "").trim()} variant={getPerfVariant(style.performance)} />
|
||||
<Badge label={style.accessibility.replace(/[✓⚠✗]/g, "").trim()} variant={getA11yVariant(style.accessibility)} />
|
||||
{style.lightMode.includes("Full") && <Badge label="Light" variant="blue" />}
|
||||
{style.darkMode.includes("Full") && <Badge label="Dark" variant="gray" />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
85
gallery/components/PhoneMockup.tsx
Normal file
85
gallery/components/PhoneMockup.tsx
Normal file
@ -0,0 +1,85 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
|
||||
interface PhoneMockupProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function PhoneMockup({ children }: PhoneMockupProps) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-4">
|
||||
{/* iPhone outer frame */}
|
||||
<div
|
||||
className="relative bg-gray-900 rounded-[44px] p-3 shadow-2xl"
|
||||
style={{ width: "300px" }}
|
||||
>
|
||||
{/* Notch / Dynamic Island */}
|
||||
<div className="absolute top-0 left-1/2 -translate-x-1/2 z-20">
|
||||
<div className="bg-gray-900 rounded-b-2xl px-6 pt-0 pb-1.5">
|
||||
<div className="w-20 h-5 bg-black rounded-full" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Screen bezel */}
|
||||
<div className="rounded-[32px] overflow-hidden bg-white dark:bg-gray-950 relative">
|
||||
{/* Status bar */}
|
||||
<div className="flex items-center justify-between px-6 pt-3 pb-1 text-[10px] font-semibold relative z-10">
|
||||
<span className="text-gray-900 dark:text-gray-100">9:41</span>
|
||||
<div className="flex items-center gap-1">
|
||||
{/* Signal */}
|
||||
<svg
|
||||
width="14"
|
||||
height="10"
|
||||
viewBox="0 0 14 10"
|
||||
className="text-gray-900 dark:text-gray-100"
|
||||
>
|
||||
<rect x="0" y="7" width="2.5" height="3" rx="0.5" fill="currentColor" />
|
||||
<rect x="3.5" y="5" width="2.5" height="5" rx="0.5" fill="currentColor" />
|
||||
<rect x="7" y="2.5" width="2.5" height="7.5" rx="0.5" fill="currentColor" />
|
||||
<rect x="10.5" y="0" width="2.5" height="10" rx="0.5" fill="currentColor" />
|
||||
</svg>
|
||||
{/* WiFi */}
|
||||
<svg
|
||||
width="12"
|
||||
height="10"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="text-gray-900 dark:text-gray-100"
|
||||
>
|
||||
<path d="M1.42 9a16 16 0 0121.16 0" />
|
||||
<path d="M5 12.55a11 11 0 0114.08 0" />
|
||||
<path d="M8.53 16.11a6 6 0 016.95 0" />
|
||||
<circle cx="12" cy="20" r="1" fill="currentColor" />
|
||||
</svg>
|
||||
{/* Battery */}
|
||||
<div className="flex items-center">
|
||||
<div className="w-5 h-2.5 border border-current rounded-sm relative">
|
||||
<div className="absolute inset-0.5 bg-current rounded-[1px]" />
|
||||
</div>
|
||||
<div className="w-0.5 h-1 bg-current rounded-r-sm" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content area */}
|
||||
<div
|
||||
className="overflow-y-auto"
|
||||
style={{ height: "540px" }}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
{/* Home indicator */}
|
||||
<div className="flex justify-center pb-2 pt-1">
|
||||
<div className="w-28 h-1 bg-gray-400 dark:bg-gray-600 rounded-full" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
62
gallery/components/SearchInput.tsx
Normal file
62
gallery/components/SearchInput.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
interface SearchInputProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
|
||||
export function SearchInput({ value, onChange }: SearchInputProps) {
|
||||
const [local, setLocal] = useState(value);
|
||||
const timerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setLocal(value);
|
||||
}, [value]);
|
||||
|
||||
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
const v = e.target.value;
|
||||
setLocal(v);
|
||||
if (timerRef.current) clearTimeout(timerRef.current);
|
||||
timerRef.current = setTimeout(() => onChange(v), 300);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<svg
|
||||
className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
||||
/>
|
||||
</svg>
|
||||
<input
|
||||
type="text"
|
||||
value={local}
|
||||
onChange={handleChange}
|
||||
placeholder="Search styles..."
|
||||
className="w-full pl-10 pr-4 py-2.5 rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent placeholder:text-gray-400"
|
||||
/>
|
||||
{local && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setLocal("");
|
||||
onChange("");
|
||||
}}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
126
gallery/components/StyleCard.tsx
Normal file
126
gallery/components/StyleCard.tsx
Normal file
@ -0,0 +1,126 @@
|
||||
"use client";
|
||||
|
||||
import { StyleData } from "@/lib/types";
|
||||
import { StylePreview } from "./StylePreview";
|
||||
import { ColorPalette } from "./ColorPalette";
|
||||
import { MetadataBadges } from "./MetadataBadges";
|
||||
import { ExpandableSection } from "./ExpandableSection";
|
||||
import { CodeSnippet } from "./CodeSnippet";
|
||||
import { InteractiveChecklist } from "./InteractiveChecklist";
|
||||
|
||||
interface StyleCardProps {
|
||||
style: StyleData;
|
||||
onSelect?: (style: StyleData) => void;
|
||||
}
|
||||
|
||||
export function StyleCard({ style, onSelect }: StyleCardProps) {
|
||||
return (
|
||||
<div
|
||||
className="bg-white dark:bg-gray-900 rounded-xl border border-gray-200 dark:border-gray-800 overflow-hidden hover:shadow-lg dark:hover:shadow-gray-900/50 transition-shadow cursor-pointer"
|
||||
onClick={() => onSelect?.(style)}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="px-4 pt-4 pb-2">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-gray-100 leading-tight">
|
||||
{style.styleCategory}
|
||||
</h3>
|
||||
<div className="flex items-center gap-1.5 shrink-0">
|
||||
<span className="text-[10px] px-2 py-0.5 rounded-full bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400">
|
||||
{style.type}
|
||||
</span>
|
||||
<span className="text-[10px] text-gray-400">{style.eraOrigin}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Preview */}
|
||||
<div className="px-4">
|
||||
<StylePreview style={style} />
|
||||
</div>
|
||||
|
||||
{/* Color Palette */}
|
||||
<div className="px-4 py-3">
|
||||
<ColorPalette colors={style.extractedColors} />
|
||||
</div>
|
||||
|
||||
{/* Metadata Badges */}
|
||||
<div className="px-4 pb-3">
|
||||
<MetadataBadges style={style} />
|
||||
</div>
|
||||
|
||||
{/* Expandable Sections */}
|
||||
<div className="px-4 pb-2">
|
||||
<ExpandableSection title="CSS Code">
|
||||
<CodeSnippet code={style.cssTechnicalKeywords} label="CSS/Technical" />
|
||||
{style.designSystemVariables && (
|
||||
<div className="mt-2">
|
||||
<CodeSnippet
|
||||
code={style.designSystemVariables}
|
||||
label="Design Variables"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</ExpandableSection>
|
||||
|
||||
<ExpandableSection title="AI Prompt">
|
||||
<CodeSnippet code={style.aiPromptKeywords} />
|
||||
</ExpandableSection>
|
||||
|
||||
<ExpandableSection title="Implementation Checklist">
|
||||
<InteractiveChecklist checklist={style.implementationChecklist} />
|
||||
</ExpandableSection>
|
||||
|
||||
<ExpandableSection title="Details">
|
||||
<div className="space-y-2 text-xs">
|
||||
{style.bestFor && (
|
||||
<div>
|
||||
<span className="font-medium text-gray-500 dark:text-gray-400">
|
||||
Best for:{" "}
|
||||
</span>
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
{style.bestFor}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{style.doNotUseFor && (
|
||||
<div>
|
||||
<span className="font-medium text-gray-500 dark:text-gray-400">
|
||||
Avoid for:{" "}
|
||||
</span>
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
{style.doNotUseFor}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{style.frameworkCompatibility && (
|
||||
<div>
|
||||
<span className="font-medium text-gray-500 dark:text-gray-400">
|
||||
Frameworks:{" "}
|
||||
</span>
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
{style.frameworkCompatibility}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{style.keywords && (
|
||||
<div className="flex flex-wrap gap-1 mt-1">
|
||||
{style.keywords
|
||||
.split(",")
|
||||
.slice(0, 6)
|
||||
.map((kw, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className="px-1.5 py-0.5 text-[10px] bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 rounded"
|
||||
>
|
||||
{kw.trim()}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ExpandableSection>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
220
gallery/components/StyleDetailModal.tsx
Normal file
220
gallery/components/StyleDetailModal.tsx
Normal file
@ -0,0 +1,220 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { StyleData } from "@/lib/types";
|
||||
import {
|
||||
generatePreviewStyle,
|
||||
generateBackgroundGradient,
|
||||
} from "@/lib/cssGenerator";
|
||||
import { PhoneMockup } from "./PhoneMockup";
|
||||
import { UIControlsShowcase } from "./UIControlsShowcase";
|
||||
import { ColorPalette } from "./ColorPalette";
|
||||
import { MetadataBadges } from "./MetadataBadges";
|
||||
import { ExpandableSection } from "./ExpandableSection";
|
||||
import { CodeSnippet } from "./CodeSnippet";
|
||||
import { InteractiveChecklist } from "./InteractiveChecklist";
|
||||
|
||||
interface StyleDetailModalProps {
|
||||
style: StyleData;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function StyleDetailModal({ style, onClose }: StyleDetailModalProps) {
|
||||
const { theme } = useTheme();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
useEffect(() => setMounted(true), []);
|
||||
|
||||
// ESC to close
|
||||
useEffect(() => {
|
||||
function handleKeyDown(e: KeyboardEvent) {
|
||||
if (e.key === "Escape") onClose();
|
||||
}
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
return () => document.removeEventListener("keydown", handleKeyDown);
|
||||
}, [onClose]);
|
||||
|
||||
// Lock body scroll
|
||||
useEffect(() => {
|
||||
document.body.style.overflow = "hidden";
|
||||
return () => {
|
||||
document.body.style.overflow = "";
|
||||
};
|
||||
}, []);
|
||||
|
||||
const isDark = mounted ? theme === "dark" : false;
|
||||
const cardStyle = generatePreviewStyle(style, isDark);
|
||||
const bgGradient = generateBackgroundGradient(style.extractedColors, isDark);
|
||||
|
||||
const accentColor =
|
||||
style.extractedColors[0] || (isDark ? "#6366f1" : "#3b82f6");
|
||||
const secondaryColor =
|
||||
style.extractedColors[1] || style.extractedColors[0] || (isDark ? "#8b5cf6" : "#6366f1");
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center"
|
||||
onClick={onClose}
|
||||
>
|
||||
{/* Backdrop */}
|
||||
<div className="absolute inset-0 bg-black/50 backdrop-blur-sm" />
|
||||
|
||||
{/* Modal container */}
|
||||
<div
|
||||
className="relative z-10 bg-white dark:bg-gray-900 rounded-2xl shadow-2xl w-[95vw] max-w-5xl max-h-[90vh] overflow-hidden flex flex-col"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-800 shrink-0">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="flex items-center gap-1.5 text-sm text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 transition-colors"
|
||||
>
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
Close
|
||||
</button>
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
|
||||
{style.styleCategory}
|
||||
</h2>
|
||||
<div className="w-16" /> {/* Spacer for centering */}
|
||||
</div>
|
||||
|
||||
{/* Body — two-column on desktop, single-column on mobile */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<div className="flex flex-col lg:flex-row">
|
||||
{/* Left: Phone mockup */}
|
||||
<div
|
||||
className="lg:w-1/2 flex items-start justify-center p-6 lg:border-r border-gray-200 dark:border-gray-800"
|
||||
style={{ background: bgGradient }}
|
||||
>
|
||||
<PhoneMockup>
|
||||
<UIControlsShowcase
|
||||
accentColor={accentColor}
|
||||
secondaryColor={secondaryColor}
|
||||
borderRadius={cardStyle.borderRadius?.toString() || "8px"}
|
||||
boxShadow={
|
||||
cardStyle.boxShadow?.toString() ||
|
||||
"0 4px 12px rgba(0,0,0,0.1)"
|
||||
}
|
||||
backdropFilter={cardStyle.backdropFilter?.toString()}
|
||||
fontFamily={cardStyle.fontFamily?.toString()}
|
||||
fontWeight={cardStyle.fontWeight?.toString()}
|
||||
border={cardStyle.border?.toString()}
|
||||
textShadow={cardStyle.textShadow?.toString()}
|
||||
letterSpacing={cardStyle.letterSpacing?.toString()}
|
||||
isDark={isDark}
|
||||
/>
|
||||
</PhoneMockup>
|
||||
</div>
|
||||
|
||||
{/* Right: Details */}
|
||||
<div className="lg:w-1/2 p-6">
|
||||
{/* Color Palette */}
|
||||
<div className="mb-4">
|
||||
<h4 className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-2">
|
||||
Color Palette
|
||||
</h4>
|
||||
<ColorPalette colors={style.extractedColors} />
|
||||
</div>
|
||||
|
||||
{/* Metadata */}
|
||||
<div className="mb-4">
|
||||
<MetadataBadges style={style} />
|
||||
</div>
|
||||
|
||||
{/* Expandable sections */}
|
||||
<div>
|
||||
<ExpandableSection title="CSS Code" defaultOpen>
|
||||
<CodeSnippet
|
||||
code={style.cssTechnicalKeywords}
|
||||
label="CSS/Technical"
|
||||
/>
|
||||
{style.designSystemVariables && (
|
||||
<div className="mt-2">
|
||||
<CodeSnippet
|
||||
code={style.designSystemVariables}
|
||||
label="Design Variables"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</ExpandableSection>
|
||||
|
||||
<ExpandableSection title="AI Prompt">
|
||||
<CodeSnippet code={style.aiPromptKeywords} />
|
||||
</ExpandableSection>
|
||||
|
||||
<ExpandableSection title="Implementation Checklist">
|
||||
<InteractiveChecklist
|
||||
checklist={style.implementationChecklist}
|
||||
/>
|
||||
</ExpandableSection>
|
||||
|
||||
<ExpandableSection title="Details">
|
||||
<div className="space-y-2 text-xs">
|
||||
{style.bestFor && (
|
||||
<div>
|
||||
<span className="font-medium text-gray-500 dark:text-gray-400">
|
||||
Best for:{" "}
|
||||
</span>
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
{style.bestFor}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{style.doNotUseFor && (
|
||||
<div>
|
||||
<span className="font-medium text-gray-500 dark:text-gray-400">
|
||||
Avoid for:{" "}
|
||||
</span>
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
{style.doNotUseFor}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{style.frameworkCompatibility && (
|
||||
<div>
|
||||
<span className="font-medium text-gray-500 dark:text-gray-400">
|
||||
Frameworks:{" "}
|
||||
</span>
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
{style.frameworkCompatibility}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{style.keywords && (
|
||||
<div className="flex flex-wrap gap-1 mt-1">
|
||||
{style.keywords
|
||||
.split(",")
|
||||
.slice(0, 6)
|
||||
.map((kw, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className="px-1.5 py-0.5 text-[10px] bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 rounded"
|
||||
>
|
||||
{kw.trim()}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ExpandableSection>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
80
gallery/components/StylePreview.tsx
Normal file
80
gallery/components/StylePreview.tsx
Normal file
@ -0,0 +1,80 @@
|
||||
"use client";
|
||||
|
||||
import { StyleData } from "@/lib/types";
|
||||
import {
|
||||
generatePreviewStyle,
|
||||
generateBackgroundGradient,
|
||||
} from "@/lib/cssGenerator";
|
||||
import { useTheme } from "next-themes";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
interface StylePreviewProps {
|
||||
style: StyleData;
|
||||
}
|
||||
|
||||
export function StylePreview({ style }: StylePreviewProps) {
|
||||
const { theme } = useTheme();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
useEffect(() => setMounted(true), []);
|
||||
|
||||
const isDark = mounted ? theme === "dark" : false;
|
||||
const cardStyle = generatePreviewStyle(style, isDark);
|
||||
const bgGradient = generateBackgroundGradient(style.extractedColors, isDark);
|
||||
const accentColor = style.extractedColors[0] || (isDark ? "#6366f1" : "#3b82f6");
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative h-40 rounded-lg overflow-hidden"
|
||||
style={{ background: bgGradient }}
|
||||
>
|
||||
{/* Demo card */}
|
||||
<div className="absolute inset-0 flex items-center justify-center p-4">
|
||||
<div
|
||||
className="w-full max-w-[200px] p-4 bg-white/80 dark:bg-gray-900/80"
|
||||
style={{
|
||||
borderRadius: cardStyle.borderRadius || "8px",
|
||||
boxShadow:
|
||||
cardStyle.boxShadow || "0 4px 12px rgba(0,0,0,0.1)",
|
||||
backdropFilter: cardStyle.backdropFilter,
|
||||
WebkitBackdropFilter: cardStyle.WebkitBackdropFilter,
|
||||
border: cardStyle.border || "1px solid rgba(128,128,128,0.1)",
|
||||
fontFamily: cardStyle.fontFamily,
|
||||
fontWeight: cardStyle.fontWeight,
|
||||
filter: cardStyle.filter,
|
||||
textShadow: cardStyle.textShadow,
|
||||
letterSpacing: cardStyle.letterSpacing,
|
||||
transition: cardStyle.transition || "all 0.2s ease",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="h-2 rounded-full mb-2 w-3/4"
|
||||
style={{ background: accentColor }}
|
||||
/>
|
||||
<div className="h-1.5 rounded-full bg-gray-300 dark:bg-gray-600 mb-1.5 w-full" />
|
||||
<div className="h-1.5 rounded-full bg-gray-300 dark:bg-gray-600 mb-1.5 w-5/6" />
|
||||
<div className="h-1.5 rounded-full bg-gray-300 dark:bg-gray-600 w-2/3" />
|
||||
<div
|
||||
className="mt-3 h-6 rounded flex items-center justify-center"
|
||||
style={{
|
||||
background: accentColor,
|
||||
borderRadius: cardStyle.borderRadius
|
||||
? `calc(${cardStyle.borderRadius} / 2)`
|
||||
: "4px",
|
||||
}}
|
||||
>
|
||||
<span className="text-[10px] font-medium text-white">
|
||||
Button
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Style name overlay */}
|
||||
<div className="absolute bottom-0 left-0 right-0 px-3 py-1.5 bg-gradient-to-t from-black/40 to-transparent">
|
||||
<span className="text-[10px] font-medium text-white/80">
|
||||
Preview
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
610
gallery/components/UIControlsShowcase.tsx
Normal file
610
gallery/components/UIControlsShowcase.tsx
Normal file
@ -0,0 +1,610 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
|
||||
interface UIControlsShowcaseProps {
|
||||
accentColor: string;
|
||||
secondaryColor: string;
|
||||
borderRadius: string;
|
||||
boxShadow: string;
|
||||
backdropFilter?: string;
|
||||
fontFamily?: string;
|
||||
fontWeight?: string;
|
||||
border?: string;
|
||||
textShadow?: string;
|
||||
letterSpacing?: string;
|
||||
isDark: boolean;
|
||||
}
|
||||
|
||||
export function UIControlsShowcase({
|
||||
accentColor,
|
||||
secondaryColor,
|
||||
borderRadius,
|
||||
boxShadow,
|
||||
backdropFilter,
|
||||
fontFamily,
|
||||
fontWeight,
|
||||
border,
|
||||
textShadow,
|
||||
letterSpacing,
|
||||
isDark,
|
||||
}: UIControlsShowcaseProps) {
|
||||
const [toggle1, setToggle1] = useState(true);
|
||||
const [toggle2, setToggle2] = useState(false);
|
||||
const [check1, setCheck1] = useState(true);
|
||||
const [check2, setCheck2] = useState(false);
|
||||
const [radio, setRadio] = useState<"a" | "b">("a");
|
||||
const [slider, setSlider] = useState(65);
|
||||
|
||||
const bg = isDark ? "#1a1a2e" : "#ffffff";
|
||||
const textPrimary = isDark ? "#f1f5f9" : "#1e293b";
|
||||
const textSecondary = isDark ? "#94a3b8" : "#64748b";
|
||||
const surfaceBg = isDark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.03)";
|
||||
const borderColor = isDark ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.08)";
|
||||
const halfRadius = `calc(${borderRadius} / 2)`;
|
||||
|
||||
const baseFont: React.CSSProperties = {
|
||||
fontFamily,
|
||||
fontWeight,
|
||||
textShadow,
|
||||
letterSpacing,
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
background: bg,
|
||||
color: textPrimary,
|
||||
padding: "16px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "16px",
|
||||
fontSize: "13px",
|
||||
lineHeight: 1.4,
|
||||
minHeight: "100%",
|
||||
...baseFont,
|
||||
}}
|
||||
>
|
||||
{/* 1. Buttons */}
|
||||
<Section label="Buttons" textSecondary={textSecondary}>
|
||||
<div style={{ display: "flex", gap: "8px", flexWrap: "wrap" }}>
|
||||
<button
|
||||
style={{
|
||||
background: accentColor,
|
||||
color: "#fff",
|
||||
border: "none",
|
||||
borderRadius: halfRadius,
|
||||
padding: "8px 16px",
|
||||
fontSize: "13px",
|
||||
fontWeight: 600,
|
||||
boxShadow,
|
||||
cursor: "pointer",
|
||||
...baseFont,
|
||||
}}
|
||||
>
|
||||
Primary
|
||||
</button>
|
||||
<button
|
||||
style={{
|
||||
background: secondaryColor,
|
||||
color: "#fff",
|
||||
border: "none",
|
||||
borderRadius: halfRadius,
|
||||
padding: "8px 16px",
|
||||
fontSize: "13px",
|
||||
fontWeight: 600,
|
||||
cursor: "pointer",
|
||||
...baseFont,
|
||||
}}
|
||||
>
|
||||
Secondary
|
||||
</button>
|
||||
<button
|
||||
style={{
|
||||
background: "transparent",
|
||||
color: accentColor,
|
||||
border: `1.5px solid ${accentColor}`,
|
||||
borderRadius: halfRadius,
|
||||
padding: "8px 16px",
|
||||
fontSize: "13px",
|
||||
fontWeight: 600,
|
||||
cursor: "pointer",
|
||||
...baseFont,
|
||||
}}
|
||||
>
|
||||
Outline
|
||||
</button>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* 2. Text Input */}
|
||||
<Section label="Text Input" textSecondary={textSecondary}>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "8px" }}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Placeholder text..."
|
||||
style={{
|
||||
background: surfaceBg,
|
||||
color: textPrimary,
|
||||
border: border || `1px solid ${borderColor}`,
|
||||
borderRadius: halfRadius,
|
||||
padding: "8px 12px",
|
||||
fontSize: "13px",
|
||||
outline: "none",
|
||||
width: "100%",
|
||||
boxSizing: "border-box",
|
||||
...baseFont,
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value="Filled input"
|
||||
readOnly
|
||||
style={{
|
||||
background: surfaceBg,
|
||||
color: textPrimary,
|
||||
border: border || `1px solid ${accentColor}44`,
|
||||
borderRadius: halfRadius,
|
||||
padding: "8px 12px",
|
||||
fontSize: "13px",
|
||||
outline: "none",
|
||||
width: "100%",
|
||||
boxSizing: "border-box",
|
||||
...baseFont,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* 3. Toggle Switch */}
|
||||
<Section label="Toggle" textSecondary={textSecondary}>
|
||||
<div style={{ display: "flex", gap: "16px", alignItems: "center" }}>
|
||||
<ToggleSwitch
|
||||
on={toggle1}
|
||||
onToggle={() => setToggle1(!toggle1)}
|
||||
accentColor={accentColor}
|
||||
borderRadius={borderRadius}
|
||||
/>
|
||||
<ToggleSwitch
|
||||
on={toggle2}
|
||||
onToggle={() => setToggle2(!toggle2)}
|
||||
accentColor={accentColor}
|
||||
borderRadius={borderRadius}
|
||||
/>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* 4. Checkbox */}
|
||||
<Section label="Checkbox" textSecondary={textSecondary}>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "8px" }}>
|
||||
<CheckboxItem
|
||||
checked={check1}
|
||||
onToggle={() => setCheck1(!check1)}
|
||||
label="Checked option"
|
||||
accentColor={accentColor}
|
||||
halfRadius={halfRadius}
|
||||
textPrimary={textPrimary}
|
||||
baseFont={baseFont}
|
||||
/>
|
||||
<CheckboxItem
|
||||
checked={check2}
|
||||
onToggle={() => setCheck2(!check2)}
|
||||
label="Unchecked option"
|
||||
accentColor={accentColor}
|
||||
halfRadius={halfRadius}
|
||||
textPrimary={textPrimary}
|
||||
baseFont={baseFont}
|
||||
/>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* 5. Radio Button */}
|
||||
<Section label="Radio" textSecondary={textSecondary}>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "8px" }}>
|
||||
<RadioItem
|
||||
selected={radio === "a"}
|
||||
onSelect={() => setRadio("a")}
|
||||
label="Option A"
|
||||
accentColor={accentColor}
|
||||
textPrimary={textPrimary}
|
||||
baseFont={baseFont}
|
||||
/>
|
||||
<RadioItem
|
||||
selected={radio === "b"}
|
||||
onSelect={() => setRadio("b")}
|
||||
label="Option B"
|
||||
accentColor={accentColor}
|
||||
textPrimary={textPrimary}
|
||||
baseFont={baseFont}
|
||||
/>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* 6. Card */}
|
||||
<Section label="Card" textSecondary={textSecondary}>
|
||||
<div
|
||||
style={{
|
||||
background: surfaceBg,
|
||||
borderRadius,
|
||||
boxShadow,
|
||||
border: border || `1px solid ${borderColor}`,
|
||||
overflow: "hidden",
|
||||
backdropFilter,
|
||||
WebkitBackdropFilter: backdropFilter,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: "64px",
|
||||
background: `linear-gradient(135deg, ${accentColor}44, ${secondaryColor}44)`,
|
||||
}}
|
||||
/>
|
||||
<div style={{ padding: "10px 12px" }}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "13px",
|
||||
fontWeight: 600,
|
||||
marginBottom: "4px",
|
||||
...baseFont,
|
||||
}}
|
||||
>
|
||||
Card Title
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "11px",
|
||||
color: textSecondary,
|
||||
...baseFont,
|
||||
}}
|
||||
>
|
||||
A short description of this card with some preview text.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* 7. Badge / Tag */}
|
||||
<Section label="Badges" textSecondary={textSecondary}>
|
||||
<div style={{ display: "flex", gap: "6px", flexWrap: "wrap" }}>
|
||||
{["Design", "UI/UX", "Style", "Modern"].map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
style={{
|
||||
background: `${accentColor}22`,
|
||||
color: accentColor,
|
||||
padding: "3px 10px",
|
||||
borderRadius: "999px",
|
||||
fontSize: "11px",
|
||||
fontWeight: 500,
|
||||
...baseFont,
|
||||
}}
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* 8. Slider */}
|
||||
<Section label="Slider" textSecondary={textSecondary}>
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
marginBottom: "6px",
|
||||
fontSize: "11px",
|
||||
color: textSecondary,
|
||||
}}
|
||||
>
|
||||
<span>0</span>
|
||||
<span style={{ color: accentColor, fontWeight: 600 }}>
|
||||
{slider}
|
||||
</span>
|
||||
<span>100</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={100}
|
||||
value={slider}
|
||||
onChange={(e) => setSlider(Number(e.target.value))}
|
||||
style={{
|
||||
width: "100%",
|
||||
accentColor,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* 9. Navigation Bar */}
|
||||
<Section label="Nav Bar" textSecondary={textSecondary}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-around",
|
||||
alignItems: "center",
|
||||
background: surfaceBg,
|
||||
borderRadius: halfRadius,
|
||||
padding: "8px 0",
|
||||
border: border || `1px solid ${borderColor}`,
|
||||
}}
|
||||
>
|
||||
<NavIcon
|
||||
label="Home"
|
||||
active
|
||||
accentColor={accentColor}
|
||||
textSecondary={textSecondary}
|
||||
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-4 0h4"
|
||||
/>
|
||||
<NavIcon
|
||||
label="Search"
|
||||
accentColor={accentColor}
|
||||
textSecondary={textSecondary}
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
||||
/>
|
||||
<NavIcon
|
||||
label="Heart"
|
||||
accentColor={accentColor}
|
||||
textSecondary={textSecondary}
|
||||
d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
|
||||
/>
|
||||
<NavIcon
|
||||
label="Profile"
|
||||
accentColor={accentColor}
|
||||
textSecondary={textSecondary}
|
||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||
/>
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* --- Sub-components --- */
|
||||
|
||||
function Section({
|
||||
label,
|
||||
textSecondary,
|
||||
children,
|
||||
}: {
|
||||
label: string;
|
||||
textSecondary: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "10px",
|
||||
fontWeight: 600,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.05em",
|
||||
color: textSecondary,
|
||||
marginBottom: "6px",
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ToggleSwitch({
|
||||
on,
|
||||
onToggle,
|
||||
accentColor,
|
||||
borderRadius,
|
||||
}: {
|
||||
on: boolean;
|
||||
onToggle: () => void;
|
||||
accentColor: string;
|
||||
borderRadius: string;
|
||||
}) {
|
||||
const trackRadius =
|
||||
borderRadius && parseInt(borderRadius) > 20 ? "999px" : "12px";
|
||||
return (
|
||||
<button
|
||||
onClick={onToggle}
|
||||
style={{
|
||||
width: "44px",
|
||||
height: "24px",
|
||||
borderRadius: trackRadius,
|
||||
background: on ? accentColor : "rgba(128,128,128,0.3)",
|
||||
border: "none",
|
||||
position: "relative",
|
||||
cursor: "pointer",
|
||||
transition: "background 0.2s",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: "18px",
|
||||
height: "18px",
|
||||
borderRadius: trackRadius,
|
||||
background: "#fff",
|
||||
position: "absolute",
|
||||
top: "3px",
|
||||
left: on ? "23px" : "3px",
|
||||
transition: "left 0.2s",
|
||||
boxShadow: "0 1px 3px rgba(0,0,0,0.2)",
|
||||
}}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function CheckboxItem({
|
||||
checked,
|
||||
onToggle,
|
||||
label,
|
||||
accentColor,
|
||||
halfRadius,
|
||||
textPrimary,
|
||||
baseFont,
|
||||
}: {
|
||||
checked: boolean;
|
||||
onToggle: () => void;
|
||||
label: string;
|
||||
accentColor: string;
|
||||
halfRadius: string;
|
||||
textPrimary: string;
|
||||
baseFont: React.CSSProperties;
|
||||
}) {
|
||||
return (
|
||||
<label
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "8px",
|
||||
cursor: "pointer",
|
||||
fontSize: "13px",
|
||||
color: textPrimary,
|
||||
...baseFont,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onToggle();
|
||||
}}
|
||||
style={{
|
||||
width: "18px",
|
||||
height: "18px",
|
||||
borderRadius: `min(${halfRadius}, 4px)`,
|
||||
border: checked
|
||||
? `2px solid ${accentColor}`
|
||||
: "2px solid rgba(128,128,128,0.4)",
|
||||
background: checked ? accentColor : "transparent",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
flexShrink: 0,
|
||||
transition: "all 0.15s",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
{checked && (
|
||||
<svg
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="#fff"
|
||||
strokeWidth="3"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
{label}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
function RadioItem({
|
||||
selected,
|
||||
onSelect,
|
||||
label,
|
||||
accentColor,
|
||||
textPrimary,
|
||||
baseFont,
|
||||
}: {
|
||||
selected: boolean;
|
||||
onSelect: () => void;
|
||||
label: string;
|
||||
accentColor: string;
|
||||
textPrimary: string;
|
||||
baseFont: React.CSSProperties;
|
||||
}) {
|
||||
return (
|
||||
<label
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "8px",
|
||||
cursor: "pointer",
|
||||
fontSize: "13px",
|
||||
color: textPrimary,
|
||||
...baseFont,
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onSelect();
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: "18px",
|
||||
height: "18px",
|
||||
borderRadius: "50%",
|
||||
border: selected
|
||||
? `2px solid ${accentColor}`
|
||||
: "2px solid rgba(128,128,128,0.4)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
flexShrink: 0,
|
||||
transition: "all 0.15s",
|
||||
}}
|
||||
>
|
||||
{selected && (
|
||||
<div
|
||||
style={{
|
||||
width: "10px",
|
||||
height: "10px",
|
||||
borderRadius: "50%",
|
||||
background: accentColor,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{label}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
function NavIcon({
|
||||
label,
|
||||
active,
|
||||
accentColor,
|
||||
textSecondary,
|
||||
d,
|
||||
}: {
|
||||
label: string;
|
||||
active?: boolean;
|
||||
accentColor: string;
|
||||
textSecondary: string;
|
||||
d: string;
|
||||
}) {
|
||||
const color = active ? accentColor : textSecondary;
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
gap: "2px",
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke={color}
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d={d} />
|
||||
</svg>
|
||||
<span style={{ fontSize: "9px", color }}>{label}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
1
gallery/data/styles.csv
Symbolic link
1
gallery/data/styles.csv
Symbolic link
@ -0,0 +1 @@
|
||||
../../src/ui-ux-pro-max/data/styles.csv
|
||||
|
34
gallery/lib/colorExtractor.ts
Normal file
34
gallery/lib/colorExtractor.ts
Normal file
@ -0,0 +1,34 @@
|
||||
const HEX_RE = /#([0-9A-Fa-f]{6}|[0-9A-Fa-f]{3})\b/g;
|
||||
const RGBA_RE = /rgba?\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*(?:,\s*[\d.]+\s*)?\)/g;
|
||||
|
||||
function normalizeHex(hex: string): string {
|
||||
hex = hex.toUpperCase();
|
||||
if (hex.length === 4) {
|
||||
return `#${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}`;
|
||||
}
|
||||
return hex;
|
||||
}
|
||||
|
||||
export function extractColors(text: string): string[] {
|
||||
const colors: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
const hexMatches = text.match(HEX_RE) || [];
|
||||
for (const h of hexMatches) {
|
||||
const normalized = normalizeHex(h);
|
||||
if (!seen.has(normalized)) {
|
||||
seen.add(normalized);
|
||||
colors.push(normalized);
|
||||
}
|
||||
}
|
||||
|
||||
const rgbaMatches = text.match(RGBA_RE) || [];
|
||||
for (const r of rgbaMatches) {
|
||||
if (!seen.has(r)) {
|
||||
seen.add(r);
|
||||
colors.push(r);
|
||||
}
|
||||
}
|
||||
|
||||
return colors;
|
||||
}
|
||||
133
gallery/lib/cssGenerator.ts
Normal file
133
gallery/lib/cssGenerator.ts
Normal file
@ -0,0 +1,133 @@
|
||||
import React from "react";
|
||||
|
||||
const CSS_PROPERTY_MAP: Record<string, string> = {
|
||||
"border-radius": "borderRadius",
|
||||
"box-shadow": "boxShadow",
|
||||
"backdrop-filter": "backdropFilter",
|
||||
"-webkit-backdrop-filter": "WebkitBackdropFilter",
|
||||
background: "background",
|
||||
"background-color": "backgroundColor",
|
||||
color: "color",
|
||||
"font-family": "fontFamily",
|
||||
"font-weight": "fontWeight",
|
||||
"font-size": "fontSize",
|
||||
border: "border",
|
||||
transition: "transition",
|
||||
transform: "transform",
|
||||
filter: "filter",
|
||||
opacity: "opacity",
|
||||
"mix-blend-mode": "mixBlendMode",
|
||||
"text-shadow": "textShadow",
|
||||
"letter-spacing": "letterSpacing",
|
||||
gap: "gap",
|
||||
padding: "padding",
|
||||
"max-width": "maxWidth",
|
||||
display: "display",
|
||||
"text-transform": "textTransform",
|
||||
};
|
||||
|
||||
export function parseCssKeywords(
|
||||
cssTechnical: string
|
||||
): Record<string, string> {
|
||||
const result: Record<string, string> = {};
|
||||
if (!cssTechnical) return result;
|
||||
|
||||
// Match patterns like "property: value"
|
||||
const re = /([\w-]+)\s*:\s*([^,;]+?)(?=\s*[,;]|\s+[\w-]+\s*:|$)/g;
|
||||
let match;
|
||||
|
||||
while ((match = re.exec(cssTechnical)) !== null) {
|
||||
const prop = match[1].trim().toLowerCase();
|
||||
const val = match[2].trim();
|
||||
if (CSS_PROPERTY_MAP[prop]) {
|
||||
result[CSS_PROPERTY_MAP[prop]] = val;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function generatePreviewStyle(
|
||||
style: { extractedColors: string[]; cssProperties: Record<string, string> },
|
||||
isDark: boolean
|
||||
): React.CSSProperties {
|
||||
const colors = style.extractedColors;
|
||||
const props = style.cssProperties;
|
||||
|
||||
const baseStyle: React.CSSProperties = {};
|
||||
|
||||
// Apply border-radius
|
||||
if (props.borderRadius) {
|
||||
baseStyle.borderRadius = props.borderRadius;
|
||||
}
|
||||
|
||||
// Apply box-shadow
|
||||
if (props.boxShadow) {
|
||||
baseStyle.boxShadow = props.boxShadow;
|
||||
}
|
||||
|
||||
// Apply backdrop-filter
|
||||
if (props.backdropFilter) {
|
||||
baseStyle.backdropFilter = props.backdropFilter;
|
||||
baseStyle.WebkitBackdropFilter = props.backdropFilter;
|
||||
}
|
||||
|
||||
// Apply font-family
|
||||
if (props.fontFamily) {
|
||||
baseStyle.fontFamily = props.fontFamily;
|
||||
}
|
||||
|
||||
// Apply font-weight
|
||||
if (props.fontWeight) {
|
||||
baseStyle.fontWeight = props.fontWeight;
|
||||
}
|
||||
|
||||
// Apply border
|
||||
if (props.border) {
|
||||
baseStyle.border = props.border;
|
||||
}
|
||||
|
||||
// Apply transition
|
||||
if (props.transition) {
|
||||
baseStyle.transition = props.transition;
|
||||
}
|
||||
|
||||
// Apply filter
|
||||
if (props.filter) {
|
||||
baseStyle.filter = props.filter;
|
||||
}
|
||||
|
||||
// Apply text-shadow
|
||||
if (props.textShadow) {
|
||||
baseStyle.textShadow = props.textShadow;
|
||||
}
|
||||
|
||||
// Apply letter-spacing
|
||||
if (props.letterSpacing) {
|
||||
baseStyle.letterSpacing = props.letterSpacing;
|
||||
}
|
||||
|
||||
return baseStyle;
|
||||
}
|
||||
|
||||
export function generateBackgroundGradient(
|
||||
colors: string[],
|
||||
isDark: boolean
|
||||
): string {
|
||||
if (colors.length === 0) {
|
||||
return isDark
|
||||
? "linear-gradient(135deg, #1a1a2e, #16213e)"
|
||||
: "linear-gradient(135deg, #f5f7fa, #c3cfe2)";
|
||||
}
|
||||
if (colors.length === 1) {
|
||||
const c = colors[0];
|
||||
return isDark
|
||||
? `linear-gradient(135deg, ${c}33, ${c}11)`
|
||||
: `linear-gradient(135deg, ${c}22, ${c}11)`;
|
||||
}
|
||||
const c1 = colors[0];
|
||||
const c2 = colors[1];
|
||||
return isDark
|
||||
? `linear-gradient(135deg, ${c1}44, ${c2}22)`
|
||||
: `linear-gradient(135deg, ${c1}22, ${c2}11)`;
|
||||
}
|
||||
55
gallery/lib/filterStyles.ts
Normal file
55
gallery/lib/filterStyles.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { StyleData } from "./types";
|
||||
|
||||
export interface Filters {
|
||||
search: string;
|
||||
type: string;
|
||||
complexity: string;
|
||||
era: string;
|
||||
}
|
||||
|
||||
export const DEFAULT_FILTERS: Filters = {
|
||||
search: "",
|
||||
type: "",
|
||||
complexity: "",
|
||||
era: "",
|
||||
};
|
||||
|
||||
export function getUniqueValues(
|
||||
styles: StyleData[],
|
||||
key: keyof StyleData
|
||||
): string[] {
|
||||
const set = new Set<string>();
|
||||
for (const s of styles) {
|
||||
const val = String(s[key]).trim();
|
||||
if (val) set.add(val);
|
||||
}
|
||||
return Array.from(set).sort();
|
||||
}
|
||||
|
||||
export function filterStyles(
|
||||
styles: StyleData[],
|
||||
filters: Filters
|
||||
): StyleData[] {
|
||||
return styles.filter((s) => {
|
||||
if (filters.type && s.type !== filters.type) return false;
|
||||
if (filters.complexity && s.complexity !== filters.complexity) return false;
|
||||
if (filters.era && s.eraOrigin !== filters.era) return false;
|
||||
|
||||
if (filters.search) {
|
||||
const q = filters.search.toLowerCase();
|
||||
const searchable = [
|
||||
s.styleCategory,
|
||||
s.keywords,
|
||||
s.bestFor,
|
||||
s.eraOrigin,
|
||||
s.type,
|
||||
s.complexity,
|
||||
]
|
||||
.join(" ")
|
||||
.toLowerCase();
|
||||
if (!searchable.includes(q)) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
77
gallery/lib/parseStyles.ts
Normal file
77
gallery/lib/parseStyles.ts
Normal file
@ -0,0 +1,77 @@
|
||||
import { StyleData } from "./types";
|
||||
import { extractColors } from "./colorExtractor";
|
||||
import { parseCssKeywords } from "./cssGenerator";
|
||||
|
||||
function parseCSVLine(line: string): string[] {
|
||||
const fields: string[] = [];
|
||||
let current = "";
|
||||
let inQuotes = false;
|
||||
|
||||
for (let i = 0; i < line.length; i++) {
|
||||
const char = line[i];
|
||||
if (inQuotes) {
|
||||
if (char === '"') {
|
||||
if (i + 1 < line.length && line[i + 1] === '"') {
|
||||
current += '"';
|
||||
i++;
|
||||
} else {
|
||||
inQuotes = false;
|
||||
}
|
||||
} else {
|
||||
current += char;
|
||||
}
|
||||
} else {
|
||||
if (char === '"') {
|
||||
inQuotes = true;
|
||||
} else if (char === ",") {
|
||||
fields.push(current.trim());
|
||||
current = "";
|
||||
} else {
|
||||
current += char;
|
||||
}
|
||||
}
|
||||
}
|
||||
fields.push(current.trim());
|
||||
return fields;
|
||||
}
|
||||
|
||||
export function parseStylesCSV(csvContent: string): StyleData[] {
|
||||
const lines = csvContent.split("\n").filter((l) => l.trim().length > 0);
|
||||
if (lines.length < 2) return [];
|
||||
|
||||
// Skip header
|
||||
const dataLines = lines.slice(1);
|
||||
return dataLines.map((line) => {
|
||||
const f = parseCSVLine(line);
|
||||
const primaryColors = f[4] || "";
|
||||
const secondaryColors = f[5] || "";
|
||||
const cssTechnicalKeywords = f[19] || "";
|
||||
|
||||
return {
|
||||
no: parseInt(f[0]) || 0,
|
||||
styleCategory: f[1] || "",
|
||||
type: f[2] || "",
|
||||
keywords: f[3] || "",
|
||||
primaryColors,
|
||||
secondaryColors,
|
||||
effectsAnimation: f[6] || "",
|
||||
bestFor: f[7] || "",
|
||||
doNotUseFor: f[8] || "",
|
||||
lightMode: f[9] || "",
|
||||
darkMode: f[10] || "",
|
||||
performance: f[11] || "",
|
||||
accessibility: f[12] || "",
|
||||
mobileFriendly: f[13] || "",
|
||||
conversionFocused: f[14] || "",
|
||||
frameworkCompatibility: f[15] || "",
|
||||
eraOrigin: f[16] || "",
|
||||
complexity: f[17] || "",
|
||||
aiPromptKeywords: f[18] || "",
|
||||
cssTechnicalKeywords,
|
||||
implementationChecklist: f[20] || "",
|
||||
designSystemVariables: f[21] || "",
|
||||
extractedColors: extractColors(`${primaryColors} ${secondaryColors}`),
|
||||
cssProperties: parseCssKeywords(cssTechnicalKeywords),
|
||||
};
|
||||
});
|
||||
}
|
||||
27
gallery/lib/types.ts
Normal file
27
gallery/lib/types.ts
Normal file
@ -0,0 +1,27 @@
|
||||
export interface StyleData {
|
||||
no: number;
|
||||
styleCategory: string;
|
||||
type: string;
|
||||
keywords: string;
|
||||
primaryColors: string;
|
||||
secondaryColors: string;
|
||||
effectsAnimation: string;
|
||||
bestFor: string;
|
||||
doNotUseFor: string;
|
||||
lightMode: string;
|
||||
darkMode: string;
|
||||
performance: string;
|
||||
accessibility: string;
|
||||
mobileFriendly: string;
|
||||
conversionFocused: string;
|
||||
frameworkCompatibility: string;
|
||||
eraOrigin: string;
|
||||
complexity: string;
|
||||
aiPromptKeywords: string;
|
||||
cssTechnicalKeywords: string;
|
||||
implementationChecklist: string;
|
||||
designSystemVariables: string;
|
||||
// Computed fields
|
||||
extractedColors: string[];
|
||||
cssProperties: Record<string, string>;
|
||||
}
|
||||
5
gallery/next-env.d.ts
vendored
Normal file
5
gallery/next-env.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
|
||||
3
gallery/next.config.js
Normal file
3
gallery/next.config.js
Normal file
@ -0,0 +1,3 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {};
|
||||
module.exports = nextConfig;
|
||||
1628
gallery/package-lock.json
generated
Normal file
1628
gallery/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
gallery/package.json
Normal file
25
gallery/package.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "style-gallery",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "^14.2.0",
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.0",
|
||||
"next-themes": "^0.4.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"@types/react": "^18.3.0",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"autoprefixer": "^10.4.0",
|
||||
"postcss": "^8.4.0",
|
||||
"tailwindcss": "^3.4.0",
|
||||
"typescript": "^5.5.0"
|
||||
}
|
||||
}
|
||||
6
gallery/postcss.config.js
Normal file
6
gallery/postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
15
gallery/tailwind.config.ts
Normal file
15
gallery/tailwind.config.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import type { Config } from "tailwindcss";
|
||||
|
||||
const config: Config = {
|
||||
content: [
|
||||
"./app/**/*.{ts,tsx}",
|
||||
"./components/**/*.{ts,tsx}",
|
||||
],
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
|
||||
export default config;
|
||||
21
gallery/tsconfig.json
Normal file
21
gallery/tsconfig.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [{ "name": "next" }],
|
||||
"paths": { "@/*": ["./*"] }
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user