mirror of
https://github.com/nextlevelbuilder/ui-ux-pro-max-skill.git
synced 2026-08-04 03:48:41 +00:00
Compare commits
No commits in common. "main" and "v2.11.5" have entirely different histories.
@ -315,7 +315,6 @@ 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
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -326,7 +325,6 @@ 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
|
||||||
@ -334,7 +332,6 @@ uipro init --ai universal --global # Install to ~/.agents/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
|
||||||
|
|||||||
@ -315,7 +315,6 @@ 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 # 所有助手
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -326,7 +325,6 @@ 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 命令
|
||||||
@ -334,7 +332,6 @@ uipro init --ai universal --global # 安装到 ~/.agents/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 # 移除特定平台
|
||||||
|
|||||||
@ -24,18 +24,16 @@ 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 universal # Universal / Agent Standard (.agents/skills/)
|
uipro init --ai continue # Continue (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
|
||||||
|
|||||||
@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
@ -1,5 +1,4 @@
|
|||||||
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';
|
||||||
@ -15,18 +14,11 @@ 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 packageCandidates = [
|
const packagePath = join(__dirname, '..', 'package.json');
|
||||||
// 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;
|
||||||
}
|
}
|
||||||
@ -46,9 +38,6 @@ 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();
|
||||||
|
|
||||||
@ -57,7 +46,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${aiFlag} --force${globalFlag}`);
|
logger.info('Then rerun: uipro init --ai <platform> --force');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,7 +71,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${aiFlag} --force${globalFlag}`)} to refresh your skill files.`);
|
logger.info(`Now rerun ${chalk.cyan('uipro init --ai <platform> --force')} to refresh your skill files.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,7 +82,6 @@ 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) {
|
||||||
|
|||||||
@ -55,7 +55,6 @@ 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)) {
|
||||||
@ -65,7 +64,6 @@ 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,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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' | 'universal' | 'all';
|
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 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', 'universal', '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', '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,5 +68,4 @@ 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'],
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -22,7 +22,6 @@ 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');
|
||||||
}
|
}
|
||||||
@ -77,11 +76,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 === 3) &&
|
detected.length === 2 &&
|
||||||
detected.includes('antigravity') &&
|
detected.includes('antigravity') &&
|
||||||
detected.includes('codex')
|
detected.includes('codex')
|
||||||
) {
|
) {
|
||||||
// Platforms share `.agents`; avoid suggesting an install for every AI.
|
// Both 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';
|
||||||
@ -130,8 +129,6 @@ 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';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -54,7 +54,6 @@ 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
3
gallery/.gitignore
vendored
@ -1,3 +0,0 @@
|
|||||||
node_modules/
|
|
||||||
.next/
|
|
||||||
out/
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
@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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { ThemeProvider } from "next-themes";
|
|
||||||
|
|
||||||
export function Providers({ children }: { children: React.ReactNode }) {
|
|
||||||
return (
|
|
||||||
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
|
||||||
{children}
|
|
||||||
</ThemeProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,50 +0,0 @@
|
|||||||
"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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,47 +0,0 @@
|
|||||||
"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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
"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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
"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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,111 +0,0 @@
|
|||||||
"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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,103 +0,0 @@
|
|||||||
"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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,62 +0,0 @@
|
|||||||
"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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,57 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,85 +0,0 @@
|
|||||||
"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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,62 +0,0 @@
|
|||||||
"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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,126 +0,0 @@
|
|||||||
"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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,220 +0,0 @@
|
|||||||
"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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,80 +0,0 @@
|
|||||||
"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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,610 +0,0 @@
|
|||||||
"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 +0,0 @@
|
|||||||
../../src/ui-ux-pro-max/data/styles.csv
|
|
||||||
|
@ -1,34 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@ -1,133 +0,0 @@
|
|||||||
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)`;
|
|
||||||
}
|
|
||||||
@ -1,55 +0,0 @@
|
|||||||
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;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -1,77 +0,0 @@
|
|||||||
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),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
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
5
gallery/next-env.d.ts
vendored
@ -1,5 +0,0 @@
|
|||||||
/// <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.
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
/** @type {import('next').NextConfig} */
|
|
||||||
const nextConfig = {};
|
|
||||||
module.exports = nextConfig;
|
|
||||||
1628
gallery/package-lock.json
generated
1628
gallery/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,25 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
plugins: {
|
|
||||||
tailwindcss: {},
|
|
||||||
autoprefixer: {},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
import type { Config } from "tailwindcss";
|
|
||||||
|
|
||||||
const config: Config = {
|
|
||||||
content: [
|
|
||||||
"./app/**/*.{ts,tsx}",
|
|
||||||
"./components/**/*.{ts,tsx}",
|
|
||||||
],
|
|
||||||
darkMode: "class",
|
|
||||||
theme: {
|
|
||||||
extend: {},
|
|
||||||
},
|
|
||||||
plugins: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export default config;
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"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"]
|
|
||||||
}
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user