Compare commits

...

2 Commits

Author SHA1 Message Date
Yadnesh Teli
4d140cf8ff
feat(cli): add --global flag to uipro update and support --ai universal target (#435, #436) (#437)
* feat(cli): add --global flag to update command and support --ai universal (#435, #436)

* fix(cli): support dist package.json resolution in update command
2026-08-03 19:30:53 +07:00
Yong Tang
14ddef5c05
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>
2026-08-01 08:29:30 +07:00
42 changed files with 3866 additions and 8 deletions

View File

@ -315,6 +315,7 @@ uipro init --ai kilocode # KiloCode
uipro init --ai warp # Warp uipro init --ai warp # Warp
uipro init --ai augment # Augment uipro init --ai augment # Augment
uipro init --ai codewhale # CodeWhale uipro init --ai codewhale # CodeWhale
uipro init --ai universal # Universal / Agent Standard (.agents/skills/)
uipro init --ai all # All assistants uipro init --ai all # All assistants
``` ```
@ -325,6 +326,7 @@ The npm package is `ui-ux-pro-max-cli`; it still installs the `uipro` command. O
```bash ```bash
uipro init --ai claude --global # Install to ~/.claude/skills/ uipro init --ai claude --global # Install to ~/.claude/skills/
uipro init --ai cursor --global # Install to ~/.cursor/skills/ uipro init --ai cursor --global # Install to ~/.cursor/skills/
uipro init --ai universal --global # Install to ~/.agents/skills/
``` ```
### Other CLI Commands ### Other CLI Commands
@ -332,6 +334,7 @@ uipro init --ai cursor --global # Install to ~/.cursor/skills/
```bash ```bash
uipro versions # List available versions uipro versions # List available versions
uipro update # Refresh skill files from installed CLI package uipro update # Refresh skill files from installed CLI package
uipro update --global # Refresh global skill files from installed CLI package
uipro init --offline # Compatibility flag; installs bundled templates uipro init --offline # Compatibility flag; installs bundled templates
uipro uninstall # Remove skill (auto-detect platform) uipro uninstall # Remove skill (auto-detect platform)
uipro uninstall --ai claude # Remove specific platform uipro uninstall --ai claude # Remove specific platform

View File

@ -315,6 +315,7 @@ uipro init --ai kilocode # KiloCode
uipro init --ai warp # Warp uipro init --ai warp # Warp
uipro init --ai augment # Augment uipro init --ai augment # Augment
uipro init --ai codewhale # CodeWhale uipro init --ai codewhale # CodeWhale
uipro init --ai universal # Universal / Agent Standard (.agents/skills/)
uipro init --ai all # 所有助手 uipro init --ai all # 所有助手
``` ```
@ -325,6 +326,7 @@ npm 包名为 `ui-ux-pro-max-cli`;它仍然安装 `uipro` 命令。旧版 `uip
```bash ```bash
uipro init --ai claude --global # 安装到 ~/.claude/skills/ uipro init --ai claude --global # 安装到 ~/.claude/skills/
uipro init --ai cursor --global # 安装到 ~/.cursor/skills/ uipro init --ai cursor --global # 安装到 ~/.cursor/skills/
uipro init --ai universal --global # 安装到 ~/.agents/skills/
``` ```
### 其他 CLI 命令 ### 其他 CLI 命令
@ -332,6 +334,7 @@ uipro init --ai cursor --global # 安装到 ~/.cursor/skills/
```bash ```bash
uipro versions # 列出可用版本 uipro versions # 列出可用版本
uipro update # 从已安装的 CLI 包刷新技能文件 uipro update # 从已安装的 CLI 包刷新技能文件
uipro update --global # 从已安装的 CLI 包刷新全局技能文件
uipro init --offline # 兼容性标志;安装捆绑模板 uipro init --offline # 兼容性标志;安装捆绑模板
uipro uninstall # 移除技能(自动检测平台) uipro uninstall # 移除技能(自动检测平台)
uipro uninstall --ai claude # 移除特定平台 uipro uninstall --ai claude # 移除特定平台

View File

@ -24,16 +24,18 @@ uipro init --ai qoder # Qoder
uipro init --ai gemini # Gemini CLI uipro init --ai gemini # Gemini CLI
uipro init --ai trae # Trae uipro init --ai trae # Trae
uipro init --ai opencode # OpenCode uipro init --ai opencode # OpenCode
uipro init --ai continue # Continue (Skills) uipro init --ai universal # Universal / Agent Standard (.agents/skills/)
uipro init --ai all # All assistants uipro init --ai all # All assistants
# Options # Options
uipro init --offline # Compatibility flag; installs bundled templates uipro init --offline # Compatibility flag; installs bundled templates
uipro init --force # Overwrite existing files uipro init --force # Overwrite existing files
uipro init --global # Install globally to home directory (~/)
# Other commands # Other commands
uipro versions # List available versions uipro versions # List available versions
uipro update # Update the global CLI to the latest release uipro update # Update the global CLI to the latest release
uipro update --global # Refresh globally installed skill files from this CLI package
``` ```
## GitHub Authentication ## GitHub Authentication

View File

@ -0,0 +1,21 @@
{
"platform": "universal",
"displayName": "Universal / Agent Standard",
"installType": "full",
"folderStructure": {
"root": ".agents",
"skillPath": "skills/ui-ux-pro-max",
"filename": "SKILL.md"
},
"scriptPath": "skills/ui-ux-pro-max/scripts/search.py",
"frontmatter": {
"name": "ui-ux-pro-max",
"description": "Comprehensive design guide for web, mobile, and desktop applications. Contains 67 styles, 161 color palettes, 57 font pairings, 99 UX guidelines, and 25 chart types across 22 technology stacks."
},
"sections": {
"quickReference": false
},
"title": "ui-ux-pro-max",
"description": "Comprehensive design guide for web, mobile, and desktop applications. Contains 67 styles, 161 color palettes, 57 font pairings, 99 UX guidelines, and 25 chart types across 22 technology stacks. Searchable database with priority-based recommendations.",
"skillOrWorkflow": "Skill"
}

View File

@ -1,4 +1,5 @@
import { execFileSync } from 'node:child_process'; import { execFileSync } from 'node:child_process';
import { existsSync } from 'node:fs';
import { readFile } from 'node:fs/promises'; import { readFile } from 'node:fs/promises';
import { dirname, join } from 'node:path'; import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
@ -14,11 +15,18 @@ const CLI_PACKAGE_NAME = 'ui-ux-pro-max-cli';
interface UpdateOptions { interface UpdateOptions {
ai?: AIType; ai?: AIType;
global?: boolean;
token?: string; token?: string;
} }
async function getPackageVersion(): Promise<string> { async function getPackageVersion(): Promise<string> {
const packagePath = join(__dirname, '..', 'package.json'); const packageCandidates = [
// Bun bundle or root: dist/index.js -> ../package.json
join(__dirname, '..', 'package.json'),
// TypeScript fallback: dist/commands/update.js -> ../../package.json
join(__dirname, '..', '..', 'package.json'),
];
const packagePath = packageCandidates.find(path => existsSync(path)) ?? packageCandidates[0];
const pkg = JSON.parse(await readFile(packagePath, 'utf-8')) as { version: string }; const pkg = JSON.parse(await readFile(packagePath, 'utf-8')) as { version: string };
return pkg.version; return pkg.version;
} }
@ -38,6 +46,9 @@ export async function updateCommand(options: UpdateOptions): Promise<void> {
const latestVersion = normalizeTagVersion(release.tag_name); const latestVersion = normalizeTagVersion(release.tag_name);
spinner.succeed(`Latest version: ${chalk.cyan(release.tag_name)}`); spinner.succeed(`Latest version: ${chalk.cyan(release.tag_name)}`);
const aiFlag = options.ai ? ` --ai ${options.ai}` : ' --ai <platform>';
const globalFlag = options.global ? ' --global' : '';
if (currentVersion !== latestVersion) { if (currentVersion !== latestVersion) {
console.log(); console.log();
@ -46,7 +57,7 @@ export async function updateCommand(options: UpdateOptions): Promise<void> {
if (!/^\d+\.\d+\.\d+([.-][0-9A-Za-z.-]+)?$/.test(latestVersion)) { if (!/^\d+\.\d+\.\d+([.-][0-9A-Za-z.-]+)?$/.test(latestVersion)) {
logger.warn(`Installed CLI is ${chalk.cyan(currentVersion)}; latest release is ${chalk.cyan(release.tag_name)}.`); logger.warn(`Installed CLI is ${chalk.cyan(currentVersion)}; latest release is ${chalk.cyan(release.tag_name)}.`);
logger.info(`Update the CLI package: ${chalk.cyan(`npm install -g ${CLI_PACKAGE_NAME}@${latestVersion}`)}`); logger.info(`Update the CLI package: ${chalk.cyan(`npm install -g ${CLI_PACKAGE_NAME}@${latestVersion}`)}`);
logger.info('Then rerun: uipro init --ai <platform> --force'); logger.info(`Then rerun: uipro init${aiFlag} --force${globalFlag}`);
return; return;
} }
@ -71,7 +82,7 @@ export async function updateCommand(options: UpdateOptions): Promise<void> {
console.log(); console.log();
logger.success(`Updated to ${chalk.cyan(latestVersion)}.`); logger.success(`Updated to ${chalk.cyan(latestVersion)}.`);
logger.info(`Now rerun ${chalk.cyan('uipro init --ai <platform> --force')} to refresh your skill files.`); logger.info(`Now rerun ${chalk.cyan(`uipro init${aiFlag} --force${globalFlag}`)} to refresh your skill files.`);
return; return;
} }
@ -82,6 +93,7 @@ export async function updateCommand(options: UpdateOptions): Promise<void> {
await initCommand({ await initCommand({
ai: options.ai, ai: options.ai,
force: true, force: true,
global: options.global,
token: options.token, token: options.token,
}); });
} catch (error) { } catch (error) {

View File

@ -55,6 +55,7 @@ program
.command('update') .command('update')
.description('Update UI/UX Pro Max to latest version') .description('Update UI/UX Pro Max to latest version')
.option('-a, --ai <type>', `AI assistant type (${AI_TYPES.join(', ')})`) .option('-a, --ai <type>', `AI assistant type (${AI_TYPES.join(', ')})`)
.option('-g, --global', 'Update global installation in home directory (~/)')
.option('-t, --token <token>', 'GitHub Personal Access Token for higher API rate limits') .option('-t, --token <token>', 'GitHub Personal Access Token for higher API rate limits')
.action(async (options) => { .action(async (options) => {
if (options.ai && !AI_TYPES.includes(options.ai)) { if (options.ai && !AI_TYPES.includes(options.ai)) {
@ -64,6 +65,7 @@ program
} }
await updateCommand({ await updateCommand({
ai: options.ai as AIType | undefined, ai: options.ai as AIType | undefined,
global: options.global,
token: options.token, token: options.token,
}); });
}); });

View File

@ -1,4 +1,4 @@
export type AIType = 'claude' | 'cursor' | 'windsurf' | 'antigravity' | 'copilot' | 'kiro' | 'roocode' | 'codex' | 'qoder' | 'gemini' | 'trae' | 'opencode' | 'continue' | 'codebuddy' | 'droid' | 'kilocode' | 'warp' | 'augment' | 'codewhale' | 'all'; export type AIType = 'claude' | 'cursor' | 'windsurf' | 'antigravity' | 'copilot' | 'kiro' | 'roocode' | 'codex' | 'qoder' | 'gemini' | 'trae' | 'opencode' | 'continue' | 'codebuddy' | 'droid' | 'kilocode' | 'warp' | 'augment' | 'codewhale' | 'universal' | 'all';
export type ConcreteAIType = Exclude<AIType, 'all'>; export type ConcreteAIType = Exclude<AIType, 'all'>;
export type InstallType = 'full' | 'reference'; export type InstallType = 'full' | 'reference';
@ -43,7 +43,7 @@ export interface PlatformConfig {
skillOrWorkflow: string; skillOrWorkflow: string;
} }
export const AI_TYPES: AIType[] = ['claude', 'cursor', 'windsurf', 'antigravity', 'copilot', 'roocode', 'kiro', 'codex', 'qoder', 'gemini', 'trae', 'opencode', 'continue', 'codebuddy', 'droid', 'kilocode', 'warp', 'augment', 'codewhale', 'all']; export const AI_TYPES: AIType[] = ['claude', 'cursor', 'windsurf', 'antigravity', 'copilot', 'roocode', 'kiro', 'codex', 'qoder', 'gemini', 'trae', 'opencode', 'continue', 'codebuddy', 'droid', 'kilocode', 'warp', 'augment', 'codewhale', 'universal', 'all'];
// Legacy folder mapping for backward compatibility with ZIP-based installs. // Legacy folder mapping for backward compatibility with ZIP-based installs.
// Note: .shared is included for platforms that used ZIP installs. Post-ZIP platforms // Note: .shared is included for platforms that used ZIP installs. Post-ZIP platforms
@ -68,4 +68,5 @@ export const AI_FOLDERS: Record<ConcreteAIType, string[]> = {
warp: ['.warp', '.shared'], warp: ['.warp', '.shared'],
augment: ['.augment', '.shared'], augment: ['.augment', '.shared'],
codewhale: ['.codewhale', '.shared'], codewhale: ['.codewhale', '.shared'],
universal: ['.agents', '.shared'],
}; };

View File

@ -22,6 +22,7 @@ export function detectAIType(cwd: string = process.cwd()): DetectionResult {
if (existsSync(join(cwd, '.agents'))) { if (existsSync(join(cwd, '.agents'))) {
detected.push('antigravity'); detected.push('antigravity');
detected.push('codex'); detected.push('codex');
detected.push('universal');
} else if (existsSync(join(cwd, '.agent'))) { } else if (existsSync(join(cwd, '.agent'))) {
detected.push('antigravity'); detected.push('antigravity');
} }
@ -76,11 +77,11 @@ export function detectAIType(cwd: string = process.cwd()): DetectionResult {
if (detected.length === 1) { if (detected.length === 1) {
suggested = detected[0]; suggested = detected[0];
} else if ( } else if (
detected.length === 2 && (detected.length === 2 || detected.length === 3) &&
detected.includes('antigravity') && detected.includes('antigravity') &&
detected.includes('codex') detected.includes('codex')
) { ) {
// Both platforms share `.agents`; avoid suggesting an install for every AI. // Platforms share `.agents`; avoid suggesting an install for every AI.
suggested = 'codex'; suggested = 'codex';
} else if (detected.length > 1) { } else if (detected.length > 1) {
suggested = 'all'; suggested = 'all';
@ -129,6 +130,8 @@ export function getAITypeDescription(aiType: AIType): string {
return 'Augment (.augment/skills/)'; return 'Augment (.augment/skills/)';
case 'codewhale': case 'codewhale':
return 'CodeWhale (.codewhale/skills/)'; return 'CodeWhale (.codewhale/skills/)';
case 'universal':
return 'Universal (.agents/skills/)';
case 'all': case 'all':
return 'All AI assistants'; return 'All AI assistants';
} }

View File

@ -54,6 +54,7 @@ const AI_TO_PLATFORM: Record<string, string> = {
warp: 'warp', warp: 'warp',
augment: 'augment', augment: 'augment',
codewhale: 'codewhale', codewhale: 'codewhale',
universal: 'universal',
}; };
async function exists(path: string): Promise<boolean> { async function exists(path: string): Promise<boolean> {

3
gallery/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
node_modules/
.next/
out/

22
gallery/app/globals.css Normal file
View 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
View 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
View 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
View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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
View File

@ -0,0 +1 @@
../../src/ui-ux-pro-max/data/styles.csv
1 ../../src/ui-ux-pro-max/data/styles.csv

View 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
View 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)`;
}

View 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;
});
}

View 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
View 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
View 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
View File

@ -0,0 +1,3 @@
/** @type {import('next').NextConfig} */
const nextConfig = {};
module.exports = nextConfig;

1628
gallery/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

25
gallery/package.json Normal file
View 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"
}
}

View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

View 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
View 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"]
}

View File

@ -0,0 +1,21 @@
{
"platform": "universal",
"displayName": "Universal / Agent Standard",
"installType": "full",
"folderStructure": {
"root": ".agents",
"skillPath": "skills/ui-ux-pro-max",
"filename": "SKILL.md"
},
"scriptPath": "skills/ui-ux-pro-max/scripts/search.py",
"frontmatter": {
"name": "ui-ux-pro-max",
"description": "Comprehensive design guide for web, mobile, and desktop applications. Contains 67 styles, 161 color palettes, 57 font pairings, 99 UX guidelines, and 25 chart types across 22 technology stacks."
},
"sections": {
"quickReference": false
},
"title": "ui-ux-pro-max",
"description": "Comprehensive design guide for web, mobile, and desktop applications. Contains 67 styles, 161 color palettes, 57 font pairings, 99 UX guidelines, and 25 chart types across 22 technology stacks. Searchable database with priority-based recommendations.",
"skillOrWorkflow": "Skill"
}