diff --git a/gallery/.gitignore b/gallery/.gitignore new file mode 100644 index 0000000..7c8ed23 --- /dev/null +++ b/gallery/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +.next/ +out/ diff --git a/gallery/app/globals.css b/gallery/app/globals.css new file mode 100644 index 0000000..38aaa2d --- /dev/null +++ b/gallery/app/globals.css @@ -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; + } +} diff --git a/gallery/app/layout.tsx b/gallery/app/layout.tsx new file mode 100644 index 0000000..5d71385 --- /dev/null +++ b/gallery/app/layout.tsx @@ -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 ( + + + {children} + + + ); +} diff --git a/gallery/app/page.tsx b/gallery/app/page.tsx new file mode 100644 index 0000000..4936f6a --- /dev/null +++ b/gallery/app/page.tsx @@ -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 ( +
+ +
+ ); +} diff --git a/gallery/app/providers.tsx b/gallery/app/providers.tsx new file mode 100644 index 0000000..d0da32b --- /dev/null +++ b/gallery/app/providers.tsx @@ -0,0 +1,11 @@ +"use client"; + +import { ThemeProvider } from "next-themes"; + +export function Providers({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} diff --git a/gallery/components/CodeSnippet.tsx b/gallery/components/CodeSnippet.tsx new file mode 100644 index 0000000..38b4cdc --- /dev/null +++ b/gallery/components/CodeSnippet.tsx @@ -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 ( +
+ {label && ( + + {label} + + )} +
+        {code}
+      
+ +
+ ); +} diff --git a/gallery/components/ColorPalette.tsx b/gallery/components/ColorPalette.tsx new file mode 100644 index 0000000..9e4e654 --- /dev/null +++ b/gallery/components/ColorPalette.tsx @@ -0,0 +1,47 @@ +"use client"; + +import { useState } from "react"; + +interface ColorPaletteProps { + colors: string[]; +} + +export function ColorPalette({ colors }: ColorPaletteProps) { + const [copiedIdx, setCopiedIdx] = useState(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 ( +
+ {displayed.map((color, i) => ( +
+
+ ))} + {colors.length > 8 && ( + +{colors.length - 8} + )} +
+ ); +} diff --git a/gallery/components/DarkModeToggle.tsx b/gallery/components/DarkModeToggle.tsx new file mode 100644 index 0000000..22b8f2d --- /dev/null +++ b/gallery/components/DarkModeToggle.tsx @@ -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
; + } + + const isDark = theme === "dark"; + + return ( + + ); +} diff --git a/gallery/components/ExpandableSection.tsx b/gallery/components/ExpandableSection.tsx new file mode 100644 index 0000000..394cc80 --- /dev/null +++ b/gallery/components/ExpandableSection.tsx @@ -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 ( +
+ + {open &&
{children}
} +
+ ); +} diff --git a/gallery/components/FilterBar.tsx b/gallery/components/FilterBar.tsx new file mode 100644 index 0000000..dab0820 --- /dev/null +++ b/gallery/components/FilterBar.tsx @@ -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 ( +
+ + {label}: + + + {options.map((opt) => ( + + ))} +
+ ); +} + +export function FilterBar({ + filters, + onChange, + types, + complexities, + eras, + resultCount, + totalCount, +}: FilterBarProps) { + const hasFilters = + filters.search || filters.type || filters.complexity || filters.era; + + return ( +
+
+ onChange({ ...filters, type })} + /> + onChange({ ...filters, complexity })} + /> +
+
+ onChange({ ...filters, era })} + /> +
+
+

+ Showing {resultCount} of {totalCount} styles +

+ {hasFilters && ( + + )} +
+
+ ); +} diff --git a/gallery/components/GalleryGrid.tsx b/gallery/components/GalleryGrid.tsx new file mode 100644 index 0000000..e171156 --- /dev/null +++ b/gallery/components/GalleryGrid.tsx @@ -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(DEFAULT_FILTERS); + const [selectedStyle, setSelectedStyle] = useState(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 ( +
+ {/* Header */} +
+
+

+ Style Gallery +

+

+ Browse {styles.length} UI styles from Antigravity Kit +

+
+ +
+ + {/* Search */} +
+ setFilters((f) => ({ ...f, search }))} + /> +
+ + {/* Filters */} +
+ +
+ + {/* Grid */} + {filtered.length > 0 ? ( +
+ {filtered.map((style) => ( + + ))} +
+ ) : ( +
+

+ No styles match your filters. +

+ +
+ )} + + {/* Style Detail Modal */} + {selectedStyle && ( + setSelectedStyle(null)} + /> + )} +
+ ); +} diff --git a/gallery/components/InteractiveChecklist.tsx b/gallery/components/InteractiveChecklist.tsx new file mode 100644 index 0000000..9f8b39f --- /dev/null +++ b/gallery/components/InteractiveChecklist.tsx @@ -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>(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 ( +
+
+ {checked.size} of {items.length} completed +
+
+ {items.map((item, i) => ( + + ))} +
+
+ ); +} diff --git a/gallery/components/MetadataBadges.tsx b/gallery/components/MetadataBadges.tsx new file mode 100644 index 0000000..7c5f2b6 --- /dev/null +++ b/gallery/components/MetadataBadges.tsx @@ -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 ( + + {label} + + ); +} + +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 ( +
+ + + + {style.lightMode.includes("Full") && } + {style.darkMode.includes("Full") && } +
+ ); +} diff --git a/gallery/components/PhoneMockup.tsx b/gallery/components/PhoneMockup.tsx new file mode 100644 index 0000000..aa36001 --- /dev/null +++ b/gallery/components/PhoneMockup.tsx @@ -0,0 +1,85 @@ +"use client"; + +import React from "react"; + +interface PhoneMockupProps { + children: React.ReactNode; +} + +export function PhoneMockup({ children }: PhoneMockupProps) { + return ( +
+ {/* iPhone outer frame */} +
+ {/* Notch / Dynamic Island */} +
+
+
+
+
+ + {/* Screen bezel */} +
+ {/* Status bar */} +
+ 9:41 +
+ {/* Signal */} + + + + + + + {/* WiFi */} + + + + + + + {/* Battery */} +
+
+
+
+
+
+
+
+ + {/* Content area */} +
+ {children} +
+ + {/* Home indicator */} +
+
+
+
+
+
+ ); +} diff --git a/gallery/components/SearchInput.tsx b/gallery/components/SearchInput.tsx new file mode 100644 index 0000000..71b7933 --- /dev/null +++ b/gallery/components/SearchInput.tsx @@ -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(null); + + useEffect(() => { + setLocal(value); + }, [value]); + + function handleChange(e: React.ChangeEvent) { + const v = e.target.value; + setLocal(v); + if (timerRef.current) clearTimeout(timerRef.current); + timerRef.current = setTimeout(() => onChange(v), 300); + } + + return ( +
+ + + + + {local && ( + + )} +
+ ); +} diff --git a/gallery/components/StyleCard.tsx b/gallery/components/StyleCard.tsx new file mode 100644 index 0000000..c134fc2 --- /dev/null +++ b/gallery/components/StyleCard.tsx @@ -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 ( +
onSelect?.(style)} + > + {/* Header */} +
+
+

+ {style.styleCategory} +

+
+ + {style.type} + + {style.eraOrigin} +
+
+
+ + {/* Preview */} +
+ +
+ + {/* Color Palette */} +
+ +
+ + {/* Metadata Badges */} +
+ +
+ + {/* Expandable Sections */} +
+ + + {style.designSystemVariables && ( +
+ +
+ )} +
+ + + + + + + + + + +
+ {style.bestFor && ( +
+ + Best for:{" "} + + + {style.bestFor} + +
+ )} + {style.doNotUseFor && ( +
+ + Avoid for:{" "} + + + {style.doNotUseFor} + +
+ )} + {style.frameworkCompatibility && ( +
+ + Frameworks:{" "} + + + {style.frameworkCompatibility} + +
+ )} + {style.keywords && ( +
+ {style.keywords + .split(",") + .slice(0, 6) + .map((kw, i) => ( + + {kw.trim()} + + ))} +
+ )} +
+
+
+
+ ); +} diff --git a/gallery/components/StyleDetailModal.tsx b/gallery/components/StyleDetailModal.tsx new file mode 100644 index 0000000..7f7124e --- /dev/null +++ b/gallery/components/StyleDetailModal.tsx @@ -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 ( +
+ {/* Backdrop */} +
+ + {/* Modal container */} +
e.stopPropagation()} + > + {/* Header */} +
+ +

+ {style.styleCategory} +

+
{/* Spacer for centering */} +
+ + {/* Body — two-column on desktop, single-column on mobile */} +
+
+ {/* Left: Phone mockup */} +
+ + + +
+ + {/* Right: Details */} +
+ {/* Color Palette */} +
+

+ Color Palette +

+ +
+ + {/* Metadata */} +
+ +
+ + {/* Expandable sections */} +
+ + + {style.designSystemVariables && ( +
+ +
+ )} +
+ + + + + + + + + + +
+ {style.bestFor && ( +
+ + Best for:{" "} + + + {style.bestFor} + +
+ )} + {style.doNotUseFor && ( +
+ + Avoid for:{" "} + + + {style.doNotUseFor} + +
+ )} + {style.frameworkCompatibility && ( +
+ + Frameworks:{" "} + + + {style.frameworkCompatibility} + +
+ )} + {style.keywords && ( +
+ {style.keywords + .split(",") + .slice(0, 6) + .map((kw, i) => ( + + {kw.trim()} + + ))} +
+ )} +
+
+
+
+
+
+
+
+ ); +} diff --git a/gallery/components/StylePreview.tsx b/gallery/components/StylePreview.tsx new file mode 100644 index 0000000..32f88db --- /dev/null +++ b/gallery/components/StylePreview.tsx @@ -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 ( +
+ {/* Demo card */} +
+
+
+
+
+
+
+ + Button + +
+
+
+ + {/* Style name overlay */} +
+ + Preview + +
+
+ ); +} diff --git a/gallery/components/UIControlsShowcase.tsx b/gallery/components/UIControlsShowcase.tsx new file mode 100644 index 0000000..2be09b4 --- /dev/null +++ b/gallery/components/UIControlsShowcase.tsx @@ -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 ( +
+ {/* 1. Buttons */} +
+
+ + + +
+
+ + {/* 2. Text Input */} +
+
+ + +
+
+ + {/* 3. Toggle Switch */} +
+
+ setToggle1(!toggle1)} + accentColor={accentColor} + borderRadius={borderRadius} + /> + setToggle2(!toggle2)} + accentColor={accentColor} + borderRadius={borderRadius} + /> +
+
+ + {/* 4. Checkbox */} +
+
+ setCheck1(!check1)} + label="Checked option" + accentColor={accentColor} + halfRadius={halfRadius} + textPrimary={textPrimary} + baseFont={baseFont} + /> + setCheck2(!check2)} + label="Unchecked option" + accentColor={accentColor} + halfRadius={halfRadius} + textPrimary={textPrimary} + baseFont={baseFont} + /> +
+
+ + {/* 5. Radio Button */} +
+
+ setRadio("a")} + label="Option A" + accentColor={accentColor} + textPrimary={textPrimary} + baseFont={baseFont} + /> + setRadio("b")} + label="Option B" + accentColor={accentColor} + textPrimary={textPrimary} + baseFont={baseFont} + /> +
+
+ + {/* 6. Card */} +
+
+
+
+
+ Card Title +
+
+ A short description of this card with some preview text. +
+
+
+
+ + {/* 7. Badge / Tag */} +
+
+ {["Design", "UI/UX", "Style", "Modern"].map((tag) => ( + + {tag} + + ))} +
+
+ + {/* 8. Slider */} +
+
+
+ 0 + + {slider} + + 100 +
+ setSlider(Number(e.target.value))} + style={{ + width: "100%", + accentColor, + cursor: "pointer", + }} + /> +
+
+ + {/* 9. Navigation Bar */} +
+
+ + + + +
+
+
+ ); +} + +/* --- Sub-components --- */ + +function Section({ + label, + textSecondary, + children, +}: { + label: string; + textSecondary: string; + children: React.ReactNode; +}) { + return ( +
+
+ {label} +
+ {children} +
+ ); +} + +function ToggleSwitch({ + on, + onToggle, + accentColor, + borderRadius, +}: { + on: boolean; + onToggle: () => void; + accentColor: string; + borderRadius: string; +}) { + const trackRadius = + borderRadius && parseInt(borderRadius) > 20 ? "999px" : "12px"; + return ( + + ); +} + +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 ( + + ); +} + +function RadioItem({ + selected, + onSelect, + label, + accentColor, + textPrimary, + baseFont, +}: { + selected: boolean; + onSelect: () => void; + label: string; + accentColor: string; + textPrimary: string; + baseFont: React.CSSProperties; +}) { + return ( +