- frontend/: Next.js 16 app (App Router, React 19, Tailwind v4) - skills/: project skills (seedance, automation, trae-agents, etc.) - Docs: PRD, UI-Design-System, DEV-LOG, seedance integration notes - skills-lock.json: skills version lock Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1395 lines
66 KiB
TypeScript
1395 lines
66 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import {
|
|
Zap,
|
|
Settings,
|
|
Users,
|
|
Check,
|
|
X,
|
|
RefreshCw,
|
|
ChevronRight,
|
|
ChevronDown,
|
|
Play,
|
|
AlertTriangle,
|
|
Pencil,
|
|
Loader2,
|
|
Info,
|
|
FolderPlus,
|
|
Search,
|
|
ChevronsUpDown,
|
|
Trash2,
|
|
Hash,
|
|
User,
|
|
} from "lucide-react";
|
|
|
|
/* ─────────────────────────────────────────────
|
|
Section wrapper
|
|
───────────────────────────────────────────── */
|
|
function Section({
|
|
title,
|
|
children,
|
|
}: {
|
|
title: string;
|
|
children: React.ReactNode;
|
|
}) {
|
|
return (
|
|
<section className="mb-16">
|
|
<h2 className="font-[family-name:var(--font-heading)] text-2xl font-semibold mb-6 text-text-primary">
|
|
{title}
|
|
</h2>
|
|
{children}
|
|
</section>
|
|
);
|
|
}
|
|
|
|
/* ─────────────────────────────────────────────
|
|
Main Showcase Page
|
|
───────────────────────────────────────────── */
|
|
export default function ShowcasePage() {
|
|
const [logOpen, setLogOpen] = useState(false);
|
|
const [confirmCountdown, setConfirmCountdown] = useState<number | null>(null);
|
|
const [selectOpen, setSelectOpen] = useState(false);
|
|
const [selectValue, setSelectValue] = useState("Original Animation");
|
|
const [activeTab, setActiveTab] = useState("overview");
|
|
const [activeSeg, setActiveSeg] = useState("Grid");
|
|
const [lang, setLang] = useState<"en" | "zh">("en");
|
|
|
|
const t = lang === "zh" ? {
|
|
// Page
|
|
title: "组件展示",
|
|
subtitle: "Air Spark 设计系统 v0.4 — 视觉规范源文件",
|
|
// Nav
|
|
navProject: "T仔的上班日记 / 第一集",
|
|
// Sections
|
|
s2: "2. 色彩 & 玻璃层",
|
|
bgLayers: "背景层",
|
|
glassCards: "玻璃卡片",
|
|
stdGlass: "标准玻璃",
|
|
stdGlassDesc: "bg-white/[0.06] + 背景模糊",
|
|
activeGlass: "激活玻璃",
|
|
activeGlassDesc: "+ 品牌光晕边框",
|
|
hoverGlass: "悬停玻璃",
|
|
hoverGlassDesc: "悬停 → bg-white/[0.12]",
|
|
accentColors: "品牌 & 状态色",
|
|
s3: "3. 字体排版",
|
|
typoDisplay: "Air Spark",
|
|
typoH1: "流水线总览",
|
|
typoH2: "第六阶段 — 视频生成",
|
|
typoH3: "角色设计审核",
|
|
typoBody: "导演与 AI 对话生成剧本,一键触发七阶段自动化流水线,输出成片视频。整个过程全并发执行,无需手动干预。",
|
|
typoCn: "思源黑体用于中文正文排版,笔画均匀、重心稳定,与 DM Sans 在视觉密度上高度匹配。",
|
|
s4: "4. 按钮",
|
|
btnConfirm: "确认并启动",
|
|
btnContinue: "继续编辑",
|
|
btnWaiting: "等待中...",
|
|
btnProcessing: "处理中...",
|
|
s5: "5. 状态格子(第六阶段片段网格)",
|
|
stageTitle: "第六阶段 — 视频生成",
|
|
completed: "已完成",
|
|
remaining: "56% — 预计剩余 ~8 分钟",
|
|
done: "完成",
|
|
running: "生成中",
|
|
pending: "队列中",
|
|
failed: "失败",
|
|
seg08fail: "片段 08 — 重试 3 次后超时失败",
|
|
editPrompt: "编辑提示词",
|
|
retry: "重跑",
|
|
s6: "6. 流水线步骤条",
|
|
steps: ["剧本", "提示词提取", "图片资产", "分镜", "切分", "视频生成", "时间轴"],
|
|
s7: "7. 审核卡片(图片资产审核)",
|
|
charRef: "角色参考图",
|
|
approved: "已通过",
|
|
approveStatus: "3 / 4 角色已通过。请重跑失败角色后继续。",
|
|
confirmAll: "全部确认 & 继续",
|
|
cancel: "取消",
|
|
launchIn: "即将启动",
|
|
s8: "8. 骨架屏加载",
|
|
s9: "9. 剧本确认(第一阶段)",
|
|
scriptReady: "剧本已就绪",
|
|
scriptInfo: "共 8 场景 / 预估 4 分 20 秒",
|
|
confirmScript: "确认剧本,启动流水线",
|
|
continueEdit: "继续修改剧本",
|
|
s10: "10. 可折叠日志面板",
|
|
sysLog: "系统日志",
|
|
s11: "11. 模态框(内联演示)",
|
|
regenTitle: "重新生成片段?",
|
|
regenDesc: "将使用修改后的提示词重新提交片段 08 到 Seedance。之前的结果将被覆盖。",
|
|
regenBtn: "重新生成",
|
|
s12: "12. 表单元素",
|
|
projectName: "项目名称",
|
|
projectPlaceholder: "输入项目名称...",
|
|
contentType: "内容类型",
|
|
scriptPrompt: "剧本提示词",
|
|
scriptPlaceholder: "描述你的故事...",
|
|
apiKey: "API Key",
|
|
apiError: "无效的 API Key 格式",
|
|
s13: "13. Toast 通知(左边框)",
|
|
toastSuccess: "保存成功",
|
|
toastSuccessDesc: "剧本已保存为正式版本",
|
|
toastError: "片段 08 失败",
|
|
toastErrorDesc: "Seedance API 超时,已重试 3 次",
|
|
toastWarning: "等待审核",
|
|
toastWarningDesc: "角色设计需要导演审核",
|
|
toastInfo: "流水线已启动",
|
|
toastInfoDesc: "正在运行第二阶段 — 提示词提取",
|
|
s14: "14. 空状态",
|
|
noProjects: "还没有项目",
|
|
noProjectsDesc: "创建你的第一个动画项目",
|
|
createProject: "创建项目",
|
|
noResults: "没有找到匹配项",
|
|
noResultsDesc: "试试其他关键词或筛选条件",
|
|
s15: "15. 徽章 / 标签",
|
|
statusBadges: "状态徽章",
|
|
contentTags: "内容标签",
|
|
s16: "16. 头像",
|
|
avatarGroup: "头像组",
|
|
s17: "17. 导航路径",
|
|
projects: "项目",
|
|
pipeline: "流水线",
|
|
s18: "18. 标签页 / 分段控制",
|
|
underlineTabs: "下划线标签页",
|
|
segCtrl: "分段控制",
|
|
activeTabLabel: "当前标签页",
|
|
tabs: ["概览", "资产", "设置"],
|
|
s19: "19. 危险按钮 & 搜索框",
|
|
dangerBtns: "危险按钮",
|
|
deleteProject: "删除项目",
|
|
confirmDelete: "确认删除",
|
|
searchInput: "搜索框",
|
|
searchPlaceholder: "搜索项目、剧集...",
|
|
searchNote: "带前置搜索图标 — 左内边距 pl-10",
|
|
s20: "20. 提示气泡",
|
|
tooltipSettings: "项目设置",
|
|
tooltipRetry: "重跑失败项",
|
|
tooltipHint: "悬停图标查看提示气泡位置",
|
|
s21: "21. 文字对比度验证",
|
|
primaryContrast: "主文字 — #f1f0ff on #07070f",
|
|
secondaryContrast: "次要文字 — #8b8ea8 on #07070f",
|
|
mutedContrast: "弱化文字 — #4c4f6b on #07070f",
|
|
mutedNote: "仅用于装饰",
|
|
footer: "液态玻璃 · 影院暗色 · AI 原生 · 高端工具",
|
|
selectOptions: ["原创动画", "网文改编", "短片 / PV", "自定义"],
|
|
} : {
|
|
title: "Component Showcase",
|
|
subtitle: "Air Spark Design System v0.4 — Visual Source of Truth",
|
|
navProject: "T仔的上班日记 / EP01",
|
|
s2: "2. Colors & Glass Layers",
|
|
bgLayers: "Background Layers",
|
|
glassCards: "Glass Cards",
|
|
stdGlass: "Standard Glass",
|
|
stdGlassDesc: "bg-white/[0.06] + backdrop-blur-2xl",
|
|
activeGlass: "Active Glass",
|
|
activeGlassDesc: "+ accent glow border",
|
|
hoverGlass: "Hover Glass",
|
|
hoverGlassDesc: "hover → bg-white/[0.12]",
|
|
accentColors: "Accent & Status Colors",
|
|
s3: "3. Typography",
|
|
typoDisplay: "Air Spark",
|
|
typoH1: "Pipeline Overview",
|
|
typoH2: "Stage 6 — Video Generation",
|
|
typoH3: "Image Assets Review",
|
|
typoBody: "Directors chat with AI to generate scripts, one-click triggers a 7-stage automated pipeline, and outputs finished video. Fully concurrent, no manual intervention needed.",
|
|
typoCn: "Noto Sans SC for Chinese body text — even strokes, stable center of gravity, visually matched with DM Sans density.",
|
|
s4: "4. Buttons",
|
|
btnConfirm: "Confirm & Launch",
|
|
btnContinue: "Continue Editing",
|
|
btnWaiting: "Waiting...",
|
|
btnProcessing: "Processing...",
|
|
s5: "5. Status Tiles (Stage 6 Segment Grid)",
|
|
stageTitle: "Stage 6 — Video Generation",
|
|
completed: "completed",
|
|
remaining: "56% — est. ~8 min remaining",
|
|
done: "Done",
|
|
running: "Running",
|
|
pending: "Pending",
|
|
failed: "Failed",
|
|
seg08fail: "Segment 08 — Failed after 3 retries (timeout)",
|
|
editPrompt: "Edit Prompt",
|
|
retry: "Retry",
|
|
s6: "6. Pipeline Stepper",
|
|
steps: ["Script", "Prompt Extract", "Image Assets", "Keyshots", "Segments", "Video Gen", "Timeline"],
|
|
s7: "7. Review Cards (Image Assets Approval)",
|
|
charRef: "Character Reference Sheet",
|
|
approved: "Approved",
|
|
approveStatus: "3 / 4 characters approved. Retry failed character to proceed.",
|
|
confirmAll: "Confirm All & Continue",
|
|
cancel: "Cancel",
|
|
launchIn: "Launching in",
|
|
s8: "8. Skeleton Loading",
|
|
s9: "9. Script Confirm CTA (Stage 1)",
|
|
scriptReady: "Script Ready",
|
|
scriptInfo: "8 scenes / estimated 4m 20s",
|
|
confirmScript: "Confirm Script & Launch Pipeline",
|
|
continueEdit: "Continue editing script",
|
|
s10: "10. Collapsible Log Panel",
|
|
sysLog: "System Log",
|
|
s11: "11. Modal Dialog (Inline Demo)",
|
|
regenTitle: "Regenerate Segment?",
|
|
regenDesc: "This will re-submit segment 08 to Seedance with modified prompt. Previous result will be overwritten.",
|
|
regenBtn: "Regenerate",
|
|
s12: "12. Form Elements",
|
|
projectName: "Project Name",
|
|
projectPlaceholder: "Enter project name...",
|
|
contentType: "Content Type",
|
|
scriptPrompt: "Script Prompt",
|
|
scriptPlaceholder: "Describe your story...",
|
|
apiKey: "API Key",
|
|
apiError: "Invalid API key format",
|
|
s13: "13. Toast Notifications (Left Border)",
|
|
toastSuccess: "Save successful",
|
|
toastSuccessDesc: "Script saved as official version",
|
|
toastError: "Segment 08 failed",
|
|
toastErrorDesc: "Seedance API timeout after 3 retries",
|
|
toastWarning: "Awaiting approval",
|
|
toastWarningDesc: "Character designs need director review",
|
|
toastInfo: "Pipeline started",
|
|
toastInfoDesc: "Running Stage 2 — asset & keyshot planning",
|
|
s14: "14. Empty States",
|
|
noProjects: "No Projects Yet",
|
|
noProjectsDesc: "Create your first animation project",
|
|
createProject: "Create Project",
|
|
noResults: "No Results Found",
|
|
noResultsDesc: "Try different keywords or filters",
|
|
s15: "15. Badge / Tag",
|
|
statusBadges: "Status Badges",
|
|
contentTags: "Content Tags",
|
|
s16: "16. Avatar",
|
|
avatarGroup: "Avatar Group",
|
|
s17: "17. Breadcrumb",
|
|
projects: "Projects",
|
|
pipeline: "Pipeline",
|
|
s18: "18. Tab / Segmented Control",
|
|
underlineTabs: "Underline Tabs",
|
|
segCtrl: "Segmented Control",
|
|
activeTabLabel: "Active tab",
|
|
tabs: ["overview", "assets", "settings"],
|
|
s19: "19. Danger Button & Search Input",
|
|
dangerBtns: "Danger Buttons",
|
|
deleteProject: "Delete Project",
|
|
confirmDelete: "Confirm Delete",
|
|
searchInput: "Search Input",
|
|
searchPlaceholder: "Search projects, episodes...",
|
|
searchNote: "With leading search icon — prefix padding pl-10",
|
|
s20: "20. Tooltip",
|
|
tooltipSettings: "Project Settings",
|
|
tooltipRetry: "Retry Failed",
|
|
tooltipHint: "Hover the icons to see tooltip positions",
|
|
s21: "21. Text Contrast Verification",
|
|
primaryContrast: "Primary text — #f1f0ff on #07070f",
|
|
secondaryContrast: "Secondary text — #8b8ea8 on #07070f",
|
|
mutedContrast: "Muted text — #4c4f6b on #07070f",
|
|
mutedNote: "decorative only",
|
|
footer: "Liquid Glass · Cinematic Dark · AI Native · Premium Tool",
|
|
selectOptions: ["Original Animation", "Web Novel Adaptation", "Short Film / PV", "Custom"],
|
|
};
|
|
|
|
const handleConfirm = () => {
|
|
setConfirmCountdown(3);
|
|
const id = setInterval(() => {
|
|
setConfirmCountdown((prev) => {
|
|
if (prev === null || prev <= 1) {
|
|
clearInterval(id);
|
|
return null;
|
|
}
|
|
return prev - 1;
|
|
});
|
|
}, 1000);
|
|
};
|
|
|
|
return (
|
|
<div className="relative z-10 min-h-screen text-text-primary">
|
|
{/* ═══════════════════════════════════════
|
|
1. TOP NAV BAR (Glass, fixed)
|
|
═══════════════════════════════════════ */}
|
|
<nav className="fixed top-0 left-0 right-0 z-30 !rounded-none border-b border-white/[0.06]" style={{ background: 'rgba(7, 7, 15, 0.85)', backdropFilter: 'blur(20px) saturate(180%)', WebkitBackdropFilter: 'blur(20px) saturate(180%)' }}>
|
|
<div className="max-w-7xl mx-auto px-6 h-16 flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-8 h-8 rounded-lg bg-accent flex items-center justify-center">
|
|
<Zap className="w-4 h-4 text-white" />
|
|
</div>
|
|
<span className="font-[family-name:var(--font-heading)] text-lg font-bold tracking-tight">
|
|
Air Spark
|
|
</span>
|
|
</div>
|
|
<div className="font-[family-name:var(--font-body)] text-sm text-text-secondary">
|
|
{t.navProject}
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<button
|
|
onClick={() => setLang(lang === "en" ? "zh" : "en")}
|
|
className="px-3 py-1.5 rounded-lg text-xs font-medium bg-white/[0.06] border border-white/10 hover:bg-white/[0.12] motion-safe:transition-colors cursor-pointer text-text-secondary"
|
|
>
|
|
{lang === "en" ? "中文" : "EN"}
|
|
</button>
|
|
<button
|
|
className="p-2 rounded-lg hover:bg-glass-03 motion-safe:transition-colors cursor-pointer"
|
|
aria-label="设置"
|
|
>
|
|
<Settings className="w-5 h-5 text-text-secondary" />
|
|
</button>
|
|
<button
|
|
className="p-2 rounded-lg hover:bg-glass-03 motion-safe:transition-colors cursor-pointer"
|
|
aria-label="成员"
|
|
>
|
|
<Users className="w-5 h-5 text-text-secondary" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
{/* Spacer for fixed nav */}
|
|
<div className="h-16" />
|
|
|
|
{/* Main content */}
|
|
<div className="max-w-7xl mx-auto px-6 py-12">
|
|
{/* Page title */}
|
|
<div className="mb-16">
|
|
<h1 className="font-[family-name:var(--font-heading)] text-5xl font-bold tracking-tight leading-tight mb-3"
|
|
style={{ letterSpacing: "-0.02em" }}>
|
|
{t.title}
|
|
</h1>
|
|
<p className="text-text-secondary text-lg">
|
|
{t.subtitle}
|
|
</p>
|
|
</div>
|
|
|
|
{/* ═══════════════════════════════════════
|
|
2. COLORS & GLASS
|
|
═══════════════════════════════════════ */}
|
|
<Section title={t.s2}>
|
|
{/* Background layers */}
|
|
<div className="mb-8">
|
|
<h3 className="font-[family-name:var(--font-heading)] text-lg font-semibold mb-4">
|
|
{t.bgLayers}
|
|
</h3>
|
|
<div className="grid grid-cols-3 gap-4">
|
|
{[
|
|
{ name: "bg-base", color: "#07070f" },
|
|
{ name: "bg-surface", color: "#0d0d1a" },
|
|
{ name: "bg-elevated", color: "#12121f" },
|
|
].map((item) => (
|
|
<div
|
|
key={item.name}
|
|
className="h-24 rounded-2xl flex items-end p-4 border border-white/5"
|
|
style={{ background: item.color }}
|
|
>
|
|
<div>
|
|
<p className="text-sm font-medium">{item.name}</p>
|
|
<p className="font-[family-name:var(--font-mono)] text-xs text-text-muted">
|
|
{item.color}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Glass cards */}
|
|
<div className="mb-8">
|
|
<h3 className="font-[family-name:var(--font-heading)] text-lg font-semibold mb-4">
|
|
{t.glassCards}
|
|
</h3>
|
|
<div className="grid grid-cols-3 gap-6">
|
|
<div className="glass-card p-6">
|
|
<p className="text-sm font-medium mb-1">{t.stdGlass}</p>
|
|
<p className="text-xs text-text-secondary">
|
|
{t.stdGlassDesc}
|
|
</p>
|
|
</div>
|
|
<div className="glass-card glass-card-active p-6">
|
|
<p className="text-sm font-medium mb-1">{t.activeGlass}</p>
|
|
<p className="text-xs text-text-secondary">
|
|
{t.activeGlassDesc}
|
|
</p>
|
|
</div>
|
|
<div className="glass-card p-6 hover:bg-white/[0.12] motion-safe:transition-colors cursor-pointer">
|
|
<p className="text-sm font-medium mb-1">{t.hoverGlass}</p>
|
|
<p className="text-xs text-text-secondary">
|
|
{t.hoverGlassDesc}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Accent colors */}
|
|
<div>
|
|
<h3 className="font-[family-name:var(--font-heading)] text-lg font-semibold mb-4">
|
|
{t.accentColors}
|
|
</h3>
|
|
<div className="flex flex-wrap gap-4">
|
|
{[
|
|
{ name: "Accent", color: "#6c63ff", tw: "bg-accent" },
|
|
{ name: "Blue", color: "#3b82f6", tw: "bg-accent-blue" },
|
|
{ name: "Completed", color: "#10b981", tw: "bg-emerald-500" },
|
|
{ name: "Failed", color: "#ef4444", tw: "bg-red-500" },
|
|
{ name: "Waiting", color: "#f59e0b", tw: "bg-amber-500" },
|
|
{ name: "Pending", color: "#4b5563", tw: "bg-gray-600" },
|
|
].map((item) => (
|
|
<div key={item.name} className="flex items-center gap-3">
|
|
<div
|
|
className="w-10 h-10 rounded-xl"
|
|
style={{ background: item.color }}
|
|
/>
|
|
<div>
|
|
<p className="text-sm font-medium">{item.name}</p>
|
|
<p className="font-[family-name:var(--font-mono)] text-xs text-text-muted">
|
|
{item.color}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</Section>
|
|
|
|
{/* ═══════════════════════════════════════
|
|
3. TYPOGRAPHY
|
|
═══════════════════════════════════════ */}
|
|
<Section title={t.s3}>
|
|
<div className="glass-card p-8 space-y-6">
|
|
<div>
|
|
<p className="text-xs text-text-muted mb-1 font-[family-name:var(--font-mono)]">
|
|
Display / 48px / Space Grotesk 700
|
|
</p>
|
|
<p
|
|
className="font-[family-name:var(--font-heading)] text-5xl font-bold"
|
|
style={{ letterSpacing: "-0.02em" }}
|
|
>
|
|
{t.typoDisplay}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-xs text-text-muted mb-1 font-[family-name:var(--font-mono)]">
|
|
H1 / 32px / Space Grotesk 700
|
|
</p>
|
|
<p
|
|
className="font-[family-name:var(--font-heading)] text-3xl font-bold"
|
|
style={{ letterSpacing: "-0.01em" }}
|
|
>
|
|
{t.typoH1}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-xs text-text-muted mb-1 font-[family-name:var(--font-mono)]">
|
|
H2 / 24px / Space Grotesk 600
|
|
</p>
|
|
<p className="font-[family-name:var(--font-heading)] text-2xl font-semibold">
|
|
{t.typoH2}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-xs text-text-muted mb-1 font-[family-name:var(--font-mono)]">
|
|
H3 / 18px / Space Grotesk 600
|
|
</p>
|
|
<p className="font-[family-name:var(--font-heading)] text-lg font-semibold">
|
|
{t.typoH3}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-xs text-text-muted mb-1 font-[family-name:var(--font-mono)]">
|
|
Body / 15px / DM Sans 400
|
|
</p>
|
|
<p className="text-[15px] leading-relaxed text-text-primary max-w-xl">
|
|
{t.typoBody}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-xs text-text-muted mb-1 font-[family-name:var(--font-mono)]">
|
|
CN Body / 15px / Noto Sans SC 400
|
|
</p>
|
|
<p className="font-[family-name:var(--font-cn)] text-[15px] leading-relaxed text-text-primary max-w-xl">
|
|
{t.typoCn}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-xs text-text-muted mb-1 font-[family-name:var(--font-mono)]">
|
|
Small / 13px / DM Sans 400
|
|
</p>
|
|
<p className="text-[13px] text-text-secondary">
|
|
seg_001 · 00:00.000 → 00:08.500 · scene_01
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-xs text-text-muted mb-1 font-[family-name:var(--font-mono)]">
|
|
Mono / 13px / JetBrains Mono 400
|
|
</p>
|
|
<p className="font-[family-name:var(--font-mono)] text-[13px] text-text-secondary">
|
|
cell_num = floor((seg_start - scene_start) / cell_duration) + 1
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</Section>
|
|
|
|
{/* ═══════════════════════════════════════
|
|
4. BUTTONS
|
|
═══════════════════════════════════════ */}
|
|
<Section title={t.s4}>
|
|
<div className="flex flex-wrap items-center gap-6">
|
|
{/* Primary CTA */}
|
|
<button className="bg-gradient-to-br from-violet-500 to-violet-700 shadow-[0_0_20px_rgba(139,92,246,0.4),0_4px_12px_rgba(0,0,0,0.3)] border border-white/15 rounded-xl px-8 py-3.5 text-white font-semibold text-[15px] hover:shadow-[0_0_30px_rgba(139,92,246,0.6)] motion-safe:transition-all motion-safe:duration-200 active:scale-[0.98] cursor-pointer focus-visible:ring-2 focus-visible:ring-violet-400 focus-visible:ring-offset-2 focus-visible:ring-offset-bg-base">
|
|
{t.btnConfirm}
|
|
</button>
|
|
|
|
{/* Secondary */}
|
|
<button className="glass-card !rounded-xl px-6 py-3 text-[15px] font-medium text-text-primary hover:bg-white/[0.12] motion-safe:transition-colors cursor-pointer focus-visible:ring-2 focus-visible:ring-violet-400 focus-visible:ring-offset-2 focus-visible:ring-offset-bg-base">
|
|
{t.btnContinue}
|
|
</button>
|
|
|
|
{/* Disabled */}
|
|
<button
|
|
disabled
|
|
className="bg-white/[0.04] border border-white/5 rounded-xl px-6 py-3 text-[15px] font-medium text-text-muted cursor-not-allowed"
|
|
>
|
|
{t.btnWaiting}
|
|
</button>
|
|
|
|
{/* Loading */}
|
|
<button
|
|
disabled
|
|
className="bg-gradient-to-br from-violet-500/50 to-violet-700/50 border border-white/10 rounded-xl px-6 py-3 text-[15px] font-medium text-white/70 cursor-not-allowed flex items-center gap-2"
|
|
>
|
|
<Loader2 className="w-4 h-4 motion-safe:animate-spin" />
|
|
{t.btnProcessing}
|
|
</button>
|
|
|
|
{/* Icon button */}
|
|
<button
|
|
className="p-2.5 rounded-xl glass-card hover:bg-white/[0.12] motion-safe:transition-colors cursor-pointer focus-visible:ring-2 focus-visible:ring-violet-400 focus-visible:ring-offset-2 focus-visible:ring-offset-bg-base"
|
|
aria-label="Retry"
|
|
>
|
|
<RefreshCw className="w-5 h-5 text-text-secondary" />
|
|
</button>
|
|
</div>
|
|
</Section>
|
|
|
|
{/* ═══════════════════════════════════════
|
|
5. STATUS TILES (Stage 6 Grid)
|
|
═══════════════════════════════════════ */}
|
|
<Section title={t.s5}>
|
|
<div className="glass-card p-6">
|
|
{/* Progress bar */}
|
|
<div className="mb-6">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<span className="font-[family-name:var(--font-heading)] text-lg font-semibold">
|
|
{t.stageTitle}
|
|
</span>
|
|
<span className="text-sm text-text-secondary">
|
|
18 / 32 {t.completed}
|
|
</span>
|
|
</div>
|
|
<div className="h-2 bg-white/[0.06] rounded-full overflow-hidden">
|
|
<div
|
|
className="h-full bg-gradient-to-r from-accent to-accent-blue rounded-full motion-safe:transition-all motion-safe:duration-500"
|
|
style={{ width: "56%" }}
|
|
/>
|
|
</div>
|
|
<p className="text-xs text-text-muted mt-2">
|
|
{t.remaining}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Segment grid */}
|
|
<div className="grid grid-cols-8 gap-2.5 mb-6">
|
|
{Array.from({ length: 32 }, (_, i) => {
|
|
const num = i + 1;
|
|
let status: "done" | "running" | "pending" | "failed";
|
|
if (num === 8) status = "failed";
|
|
else if (num <= 12) status = "done";
|
|
else if (num <= 18) status = "running";
|
|
else status = "pending";
|
|
|
|
const styles = {
|
|
done: "bg-emerald-500/15 border-emerald-400/30 text-emerald-400",
|
|
running:
|
|
"bg-blue-500/20 border-blue-400/40 text-blue-400 motion-safe:animate-pulse",
|
|
pending: "bg-white/[0.03] border-white/[0.06] text-text-muted",
|
|
failed: "bg-red-500/15 border-red-400/30 text-red-400",
|
|
};
|
|
const icons = {
|
|
done: <Check className="w-3.5 h-3.5" />,
|
|
running: (
|
|
<div className="w-2 h-2 rounded-full bg-blue-400" />
|
|
),
|
|
pending: (
|
|
<div className="w-2 h-2 rounded-full bg-white/20" />
|
|
),
|
|
failed: <X className="w-3.5 h-3.5" />,
|
|
};
|
|
|
|
return (
|
|
<div
|
|
key={num}
|
|
className={`border rounded-lg p-2.5 flex flex-col items-center gap-1.5 cursor-pointer hover:bg-white/[0.08] motion-safe:transition-colors ${styles[status]}`}
|
|
>
|
|
<span className="font-[family-name:var(--font-mono)] text-xs">
|
|
{String(num).padStart(2, "0")}
|
|
</span>
|
|
{icons[status]}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{/* Legend */}
|
|
<div className="flex items-center gap-6 text-xs text-text-secondary">
|
|
<span className="flex items-center gap-1.5">
|
|
<Check className="w-3 h-3 text-emerald-400" /> {t.done}
|
|
</span>
|
|
<span className="flex items-center gap-1.5">
|
|
<div className="w-2 h-2 rounded-full bg-blue-400" /> {t.running}
|
|
</span>
|
|
<span className="flex items-center gap-1.5">
|
|
<div className="w-2 h-2 rounded-full bg-white/20" /> {t.pending}
|
|
</span>
|
|
<span className="flex items-center gap-1.5">
|
|
<X className="w-3 h-3 text-red-400" /> {t.failed}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Failed segment action */}
|
|
<div className="mt-4 p-4 rounded-xl bg-red-500/[0.06] border border-red-400/20 flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<AlertTriangle className="w-5 h-5 text-red-400" />
|
|
<span className="text-sm">
|
|
{t.seg08fail}
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<button className="px-3 py-1.5 rounded-lg text-xs font-medium glass-card hover:bg-white/[0.12] motion-safe:transition-colors cursor-pointer flex items-center gap-1.5">
|
|
<Pencil className="w-3 h-3" /> {t.editPrompt}
|
|
</button>
|
|
<button className="px-3 py-1.5 rounded-lg text-xs font-medium bg-red-500/20 border border-red-400/30 hover:bg-red-500/30 motion-safe:transition-colors cursor-pointer flex items-center gap-1.5">
|
|
<RefreshCw className="w-3 h-3" /> {t.retry}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Section>
|
|
|
|
{/* ═══════════════════════════════════════
|
|
6. PIPELINE STEPPER
|
|
═══════════════════════════════════════ */}
|
|
<Section title={t.s6}>
|
|
<div className="glass-card p-6">
|
|
<div className="flex items-center justify-between">
|
|
{[
|
|
{ num: 1, name: t.steps[0], status: "done" as const },
|
|
{ num: 2, name: t.steps[1], status: "done" as const },
|
|
{ num: 3, name: t.steps[2], status: "waiting" as const },
|
|
{ num: 4, name: t.steps[3], status: "idle" as const },
|
|
{ num: 5, name: t.steps[4], status: "idle" as const },
|
|
{ num: 6, name: t.steps[5], status: "idle" as const },
|
|
{ num: 7, name: t.steps[6], status: "idle" as const },
|
|
].map((step, idx) => (
|
|
<div key={step.num} className="flex items-center">
|
|
<div className="flex flex-col items-center gap-2">
|
|
<div
|
|
className={`w-10 h-10 rounded-full flex items-center justify-center text-sm font-semibold border motion-safe:transition-colors ${
|
|
step.status === "done"
|
|
? "bg-emerald-500/20 border-emerald-400/40 text-emerald-400"
|
|
: step.status === "waiting"
|
|
? "bg-amber-500/20 border-amber-400/40 text-amber-400 motion-safe:animate-pulse"
|
|
: "bg-white/[0.04] border-white/[0.08] text-text-muted"
|
|
}`}
|
|
>
|
|
{step.status === "done" ? (
|
|
<Check className="w-4 h-4" />
|
|
) : (
|
|
step.num
|
|
)}
|
|
</div>
|
|
<span
|
|
className={`text-xs font-medium ${
|
|
step.status === "done"
|
|
? "text-emerald-400"
|
|
: step.status === "waiting"
|
|
? "text-amber-400"
|
|
: "text-text-muted"
|
|
}`}
|
|
>
|
|
{step.name}
|
|
</span>
|
|
</div>
|
|
{idx < 6 && (
|
|
<ChevronRight className="w-4 h-4 text-text-muted mx-3 mt-[-20px]" />
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</Section>
|
|
|
|
{/* ═══════════════════════════════════════
|
|
7. REVIEW CARDS (Stage 3 Character Approval)
|
|
═══════════════════════════════════════ */}
|
|
<Section title={t.s7}>
|
|
<div className="grid grid-cols-4 gap-4 mb-6">
|
|
{[
|
|
{ name: "T仔", status: "approved" as const },
|
|
{ name: "特特", status: "approved" as const },
|
|
{ name: "班长", status: "approved" as const },
|
|
{ name: "路人甲", status: "failed" as const },
|
|
].map((char) => (
|
|
<div key={char.name} className="glass-card overflow-hidden">
|
|
{/* Image placeholder */}
|
|
<div
|
|
className={`h-40 flex items-center justify-center ${
|
|
char.status === "failed"
|
|
? "bg-red-500/[0.06]"
|
|
: "bg-white/[0.03]"
|
|
}`}
|
|
>
|
|
<span className="text-4xl text-text-muted/30 font-[family-name:var(--font-heading)] font-bold">
|
|
{char.name[0]}
|
|
</span>
|
|
</div>
|
|
<div className="p-4">
|
|
<p className="font-[family-name:var(--font-heading)] font-semibold mb-1">
|
|
{char.name}
|
|
</p>
|
|
<p className="text-xs text-text-secondary mb-3">
|
|
{t.charRef}
|
|
</p>
|
|
{char.status === "approved" ? (
|
|
<button className="w-full py-2 rounded-lg text-xs font-medium bg-emerald-500/15 border border-emerald-400/30 text-emerald-400 flex items-center justify-center gap-1.5 cursor-pointer">
|
|
<Check className="w-3 h-3" /> {t.approved}
|
|
</button>
|
|
) : (
|
|
<button className="w-full py-2 rounded-lg text-xs font-medium bg-red-500/15 border border-red-400/30 text-red-400 flex items-center justify-center gap-1.5 cursor-pointer hover:bg-red-500/25 motion-safe:transition-colors">
|
|
<RefreshCw className="w-3 h-3" /> {t.retry}
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* Confirm CTA with countdown */}
|
|
<div className="glass-card p-6 text-center">
|
|
{confirmCountdown !== null ? (
|
|
<div>
|
|
<p className="text-sm text-text-secondary mb-3">
|
|
{t.launchIn} {confirmCountdown}s...
|
|
</p>
|
|
<button
|
|
onClick={() => setConfirmCountdown(null)}
|
|
className="px-6 py-2 rounded-lg text-sm font-medium glass-card hover:bg-white/[0.12] motion-safe:transition-colors cursor-pointer"
|
|
>
|
|
{t.cancel}
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<div>
|
|
<p className="text-sm text-text-secondary mb-4">
|
|
{t.approveStatus}
|
|
</p>
|
|
<button
|
|
onClick={handleConfirm}
|
|
disabled
|
|
className="bg-white/[0.04] border border-white/5 rounded-xl px-10 py-3.5 text-[15px] font-semibold text-text-muted cursor-not-allowed"
|
|
>
|
|
{t.confirmAll}
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</Section>
|
|
|
|
{/* ═══════════════════════════════════════
|
|
8. SKELETON LOADING
|
|
═══════════════════════════════════════ */}
|
|
<Section title={t.s8}>
|
|
<div className="grid grid-cols-3 gap-4">
|
|
{[1, 2, 3].map((i) => (
|
|
<div
|
|
key={i}
|
|
className="glass-card overflow-hidden motion-safe:animate-pulse"
|
|
>
|
|
<div className="h-40 bg-white/[0.06]" />
|
|
<div className="p-4 space-y-3">
|
|
<div className="h-4 bg-white/[0.08] rounded w-3/4" />
|
|
<div className="h-3 bg-white/[0.06] rounded w-1/2" />
|
|
<div className="h-8 bg-white/[0.04] rounded-lg mt-4" />
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</Section>
|
|
|
|
{/* ═══════════════════════════════════════
|
|
9. CONFIRM CTA (Stage 1 Script)
|
|
═══════════════════════════════════════ */}
|
|
<Section title={t.s9}>
|
|
<div className="glass-card p-8 text-center max-w-lg mx-auto">
|
|
<div className="flex items-center justify-center gap-2 mb-2">
|
|
<Check className="w-5 h-5 text-emerald-400" />
|
|
<span className="font-[family-name:var(--font-heading)] font-semibold">
|
|
{t.scriptReady}
|
|
</span>
|
|
</div>
|
|
<p className="text-sm text-text-secondary mb-6">
|
|
{t.scriptInfo}
|
|
</p>
|
|
<button
|
|
onClick={handleConfirm}
|
|
className="w-full bg-gradient-to-br from-violet-500 to-violet-700 shadow-[0_0_20px_rgba(139,92,246,0.4),0_4px_12px_rgba(0,0,0,0.3)] border border-white/15 rounded-xl px-8 py-4 text-white font-semibold text-base hover:shadow-[0_0_30px_rgba(139,92,246,0.6)] motion-safe:transition-all motion-safe:duration-200 active:scale-[0.98] cursor-pointer flex items-center justify-center gap-2"
|
|
>
|
|
{t.confirmScript}
|
|
<Play className="w-4 h-4" />
|
|
</button>
|
|
<button className="mt-3 text-sm text-text-secondary hover:text-text-primary motion-safe:transition-colors cursor-pointer">
|
|
{t.continueEdit}
|
|
</button>
|
|
</div>
|
|
</Section>
|
|
|
|
{/* ═══════════════════════════════════════
|
|
10. COLLAPSIBLE LOG PANEL
|
|
═══════════════════════════════════════ */}
|
|
<Section title={t.s10}>
|
|
<div className="glass-card overflow-hidden">
|
|
<button
|
|
onClick={() => setLogOpen(!logOpen)}
|
|
className="w-full px-6 py-4 flex items-center justify-between hover:bg-white/[0.04] motion-safe:transition-colors cursor-pointer"
|
|
>
|
|
<span className="text-sm font-medium text-text-secondary">
|
|
{t.sysLog}
|
|
</span>
|
|
{logOpen ? (
|
|
<ChevronDown className="w-4 h-4 text-text-muted" />
|
|
) : (
|
|
<ChevronRight className="w-4 h-4 text-text-muted" />
|
|
)}
|
|
</button>
|
|
{logOpen && (
|
|
<div className="px-6 pb-4 border-t border-white/5 pt-4">
|
|
<div className="space-y-2 font-[family-name:var(--font-mono)] text-xs text-text-secondary">
|
|
<p>
|
|
<span className="text-text-muted">[14:32:01]</span>{" "}
|
|
<span className="text-emerald-400">OK</span> Stage 2
|
|
completed — 3 characters, 5 scenes
|
|
</p>
|
|
<p>
|
|
<span className="text-text-muted">[14:32:03]</span>{" "}
|
|
<span className="text-accent-blue">INFO</span> Starting
|
|
Stage 3 — Banana Pro batch (8 images)
|
|
</p>
|
|
<p>
|
|
<span className="text-text-muted">[14:32:15]</span>{" "}
|
|
<span className="text-emerald-400">OK</span>{" "}
|
|
character_001_front.jpg generated
|
|
</p>
|
|
<p>
|
|
<span className="text-text-muted">[14:32:28]</span>{" "}
|
|
<span className="text-red-400">ERR</span>{" "}
|
|
character_004 — Banana Pro 429, retrying in 4s
|
|
</p>
|
|
<p>
|
|
<span className="text-text-muted">[14:32:32]</span>{" "}
|
|
<span className="text-amber-400">WARN</span>{" "}
|
|
character_004 — retry 2/3
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</Section>
|
|
|
|
{/* ═══════════════════════════════════════
|
|
11. MODAL (Confirm Dialog)
|
|
═══════════════════════════════════════ */}
|
|
<Section title={t.s11}>
|
|
<div className="relative rounded-2xl bg-bg-surface p-8 overflow-hidden">
|
|
{/* Overlay simulation */}
|
|
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm rounded-2xl" />
|
|
<div className="relative z-10 max-w-md mx-auto p-8 text-center rounded-2xl border border-white/[0.08]" style={{ background: 'rgba(13, 13, 26, 0.92)', backdropFilter: 'blur(24px) saturate(180%)', WebkitBackdropFilter: 'blur(24px) saturate(180%)' }}>
|
|
<div className="w-12 h-12 rounded-full bg-amber-500/20 flex items-center justify-center mx-auto mb-4">
|
|
<AlertTriangle className="w-6 h-6 text-amber-400" />
|
|
</div>
|
|
<h3 className="font-[family-name:var(--font-heading)] text-lg font-semibold mb-2">
|
|
{t.regenTitle}
|
|
</h3>
|
|
<p className="text-sm text-text-secondary mb-6">
|
|
{t.regenDesc}
|
|
</p>
|
|
<div className="flex gap-3">
|
|
<button className="flex-1 py-3 rounded-xl glass-card text-sm font-medium hover:bg-white/[0.12] motion-safe:transition-colors cursor-pointer">
|
|
{t.cancel}
|
|
</button>
|
|
<button className="flex-1 py-3 rounded-xl bg-gradient-to-br from-violet-500 to-violet-700 border border-white/15 text-sm font-medium text-white hover:shadow-[0_0_20px_rgba(139,92,246,0.4)] motion-safe:transition-all cursor-pointer">
|
|
{t.regenBtn}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Section>
|
|
|
|
{/* ═══════════════════════════════════════
|
|
12. FORM ELEMENTS
|
|
═══════════════════════════════════════ */}
|
|
<Section title={t.s12}>
|
|
<div className="glass-card p-8 max-w-lg">
|
|
<div className="space-y-4">
|
|
{/* Label + Input */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-text-secondary mb-2">
|
|
{t.projectName}
|
|
</label>
|
|
<input
|
|
type="text"
|
|
placeholder={t.projectPlaceholder}
|
|
className="w-full bg-white/[0.04] border border-white/10 rounded-lg px-4 py-3 text-[15px] text-text-primary placeholder:text-text-muted focus:outline-none focus:border-accent/50 focus:ring-3 focus:ring-accent/15 transition-all duration-200"
|
|
/>
|
|
</div>
|
|
|
|
{/* Custom Select */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-text-secondary mb-2">
|
|
{t.contentType}
|
|
</label>
|
|
<div className="relative">
|
|
<button
|
|
type="button"
|
|
onClick={() => setSelectOpen(!selectOpen)}
|
|
className={`w-full bg-white/[0.04] border rounded-lg px-4 py-3 text-[15px] text-text-primary text-left flex items-center justify-between transition-all duration-200 cursor-pointer ${
|
|
selectOpen ? "border-accent/50 ring-3 ring-accent/15" : "border-white/10"
|
|
}`}
|
|
>
|
|
{selectValue}
|
|
<ChevronsUpDown className="w-4 h-4 text-text-muted shrink-0" />
|
|
</button>
|
|
{selectOpen && (
|
|
<div className="absolute z-20 mt-1 w-full rounded-lg border border-white/10 overflow-hidden" style={{ background: 'rgba(13, 13, 26, 0.95)', backdropFilter: 'blur(20px)', WebkitBackdropFilter: 'blur(20px)' }}>
|
|
{t.selectOptions.map((opt) => (
|
|
<button
|
|
key={opt}
|
|
type="button"
|
|
onClick={() => { setSelectValue(opt); setSelectOpen(false); }}
|
|
className={`w-full px-4 py-2.5 text-[15px] text-left transition-colors cursor-pointer ${
|
|
selectValue === opt
|
|
? "text-accent bg-accent/10"
|
|
: "text-text-primary hover:bg-white/[0.06]"
|
|
}`}
|
|
>
|
|
{opt}
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Textarea */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-text-secondary mb-2">
|
|
{t.scriptPrompt} <span className="text-red-400">*</span>
|
|
</label>
|
|
<textarea
|
|
placeholder={t.scriptPlaceholder}
|
|
className="w-full bg-white/[0.04] border border-white/10 rounded-lg px-4 py-3 text-[15px] text-text-primary placeholder:text-text-muted focus:outline-none focus:border-accent/50 focus:ring-3 focus:ring-accent/15 transition-all duration-200 resize-none min-h-[120px]"
|
|
/>
|
|
</div>
|
|
|
|
{/* Error state */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-text-secondary mb-2">
|
|
{t.apiKey}
|
|
</label>
|
|
<input
|
|
type="text"
|
|
defaultValue="sk-invalid-key"
|
|
className="w-full bg-white/[0.04] border border-red-500/50 rounded-lg px-4 py-3 text-[15px] text-text-primary focus:outline-none focus:ring-3 focus:ring-red-500/12 transition-all duration-200"
|
|
/>
|
|
<p className="text-sm text-red-400 mt-1">{t.apiError}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Section>
|
|
|
|
{/* ═══════════════════════════════════════
|
|
13. TOAST WITH BORDER
|
|
═══════════════════════════════════════ */}
|
|
<Section title={t.s13}>
|
|
<div className="space-y-3 max-w-md">
|
|
<div className="glass-card !rounded-xl px-4 py-3 flex items-start gap-3 border-l-[3px] border-l-emerald-500">
|
|
<Check className="w-5 h-5 text-emerald-400 mt-0.5 shrink-0" />
|
|
<div className="flex-1">
|
|
<p className="text-sm font-medium">{t.toastSuccess}</p>
|
|
<p className="text-xs text-text-secondary mt-0.5">{t.toastSuccessDesc}</p>
|
|
</div>
|
|
<button className="text-text-muted hover:text-text-secondary cursor-pointer" aria-label="Close">
|
|
<X className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
<div className="glass-card !rounded-xl px-4 py-3 flex items-start gap-3 border-l-[3px] border-l-red-500">
|
|
<X className="w-5 h-5 text-red-400 mt-0.5 shrink-0" />
|
|
<div className="flex-1">
|
|
<p className="text-sm font-medium">{t.toastError}</p>
|
|
<p className="text-xs text-text-secondary mt-0.5">{t.toastErrorDesc}</p>
|
|
</div>
|
|
<button className="text-text-muted hover:text-text-secondary cursor-pointer" aria-label="Close">
|
|
<X className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
<div className="glass-card !rounded-xl px-4 py-3 flex items-start gap-3 border-l-[3px] border-l-amber-500">
|
|
<AlertTriangle className="w-5 h-5 text-amber-400 mt-0.5 shrink-0" />
|
|
<div className="flex-1">
|
|
<p className="text-sm font-medium">{t.toastWarning}</p>
|
|
<p className="text-xs text-text-secondary mt-0.5">{t.toastWarningDesc}</p>
|
|
</div>
|
|
<button className="text-text-muted hover:text-text-secondary cursor-pointer" aria-label="Close">
|
|
<X className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
<div className="glass-card !rounded-xl px-4 py-3 flex items-start gap-3 border-l-[3px] border-l-blue-500">
|
|
<Info className="w-5 h-5 text-blue-400 mt-0.5 shrink-0" />
|
|
<div className="flex-1">
|
|
<p className="text-sm font-medium">{t.toastInfo}</p>
|
|
<p className="text-xs text-text-secondary mt-0.5">{t.toastInfoDesc}</p>
|
|
</div>
|
|
<button className="text-text-muted hover:text-text-secondary cursor-pointer" aria-label="Close">
|
|
<X className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</Section>
|
|
|
|
{/* ═══════════════════════════════════════
|
|
14. EMPTY STATES
|
|
═══════════════════════════════════════ */}
|
|
<Section title={t.s14}>
|
|
<div className="grid grid-cols-2 gap-6">
|
|
{/* No projects */}
|
|
<div className="glass-card flex flex-col items-center justify-center py-16 text-center">
|
|
<div className="w-12 h-12 rounded-2xl bg-white/[0.04] flex items-center justify-center mb-4">
|
|
<FolderPlus className="w-6 h-6 text-text-muted" />
|
|
</div>
|
|
<h3 className="font-[family-name:var(--font-heading)] text-lg font-semibold text-text-primary mb-1">
|
|
{t.noProjects}
|
|
</h3>
|
|
<p className="text-sm text-text-secondary mb-6">
|
|
{t.noProjectsDesc}
|
|
</p>
|
|
<button className="bg-gradient-to-br from-violet-500 to-violet-700 border border-white/15 rounded-xl px-6 py-2.5 text-sm text-white font-medium hover:shadow-[0_0_20px_rgba(139,92,246,0.4)] transition-all duration-200 cursor-pointer">
|
|
{t.createProject}
|
|
</button>
|
|
</div>
|
|
|
|
{/* No search results */}
|
|
<div className="glass-card flex flex-col items-center justify-center py-16 text-center">
|
|
<div className="w-12 h-12 rounded-2xl bg-white/[0.04] flex items-center justify-center mb-4">
|
|
<Search className="w-6 h-6 text-text-muted" />
|
|
</div>
|
|
<h3 className="font-[family-name:var(--font-heading)] text-lg font-semibold text-text-primary mb-1">
|
|
{t.noResults}
|
|
</h3>
|
|
<p className="text-sm text-text-secondary">
|
|
{t.noResultsDesc}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</Section>
|
|
|
|
{/* ═══════════════════════════════════════
|
|
15. BADGE / TAG
|
|
═══════════════════════════════════════ */}
|
|
<Section title={t.s15}>
|
|
<div className="glass-card p-8">
|
|
<h3 className="font-[family-name:var(--font-heading)] text-lg font-semibold mb-4">
|
|
{t.statusBadges}
|
|
</h3>
|
|
<div className="flex flex-wrap items-center gap-3 mb-8">
|
|
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md text-xs font-medium bg-emerald-500/15 text-emerald-400 border border-emerald-400/20">
|
|
<Check className="w-3 h-3" /> Completed
|
|
</span>
|
|
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md text-xs font-medium bg-blue-500/15 text-blue-400 border border-blue-400/20">
|
|
<Loader2 className="w-3 h-3 motion-safe:animate-spin" /> Running
|
|
</span>
|
|
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md text-xs font-medium bg-amber-500/15 text-amber-400 border border-amber-400/20">
|
|
<AlertTriangle className="w-3 h-3" /> Waiting
|
|
</span>
|
|
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md text-xs font-medium bg-red-500/15 text-red-400 border border-red-400/20">
|
|
<X className="w-3 h-3" /> Failed
|
|
</span>
|
|
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md text-xs font-medium bg-white/[0.06] text-text-secondary border border-white/10">
|
|
Pending
|
|
</span>
|
|
</div>
|
|
|
|
<h3 className="font-[family-name:var(--font-heading)] text-lg font-semibold mb-4">
|
|
{t.contentTags}
|
|
</h3>
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-medium bg-accent/15 text-accent border border-accent/20">
|
|
<Hash className="w-3 h-3" /> screenplay-skill
|
|
</span>
|
|
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-medium bg-accent/15 text-accent border border-accent/20">
|
|
<Hash className="w-3 h-3" /> storyboard-skill
|
|
</span>
|
|
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-medium bg-accent-blue/15 text-accent-blue border border-accent-blue/20">
|
|
EP01
|
|
</span>
|
|
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-medium bg-accent-blue/15 text-accent-blue border border-accent-blue/20">
|
|
EP02
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</Section>
|
|
|
|
{/* ═══════════════════════════════════════
|
|
16. AVATAR
|
|
═══════════════════════════════════════ */}
|
|
<Section title={t.s16}>
|
|
<div className="glass-card p-8">
|
|
<div className="flex items-end gap-6 mb-8">
|
|
{/* Sizes */}
|
|
{[
|
|
{ size: "w-8 h-8", text: "text-xs", label: "SM" },
|
|
{ size: "w-10 h-10", text: "text-sm", label: "MD" },
|
|
{ size: "w-12 h-12", text: "text-base", label: "LG" },
|
|
{ size: "w-16 h-16", text: "text-lg", label: "XL" },
|
|
].map((a) => (
|
|
<div key={a.label} className="flex flex-col items-center gap-2">
|
|
<div className={`${a.size} rounded-full bg-gradient-to-br from-violet-500 to-accent-blue flex items-center justify-center ${a.text} font-semibold text-white`}>
|
|
T
|
|
</div>
|
|
<span className="text-xs text-text-muted">{a.label}</span>
|
|
</div>
|
|
))}
|
|
|
|
{/* With icon fallback */}
|
|
<div className="flex flex-col items-center gap-2">
|
|
<div className="w-10 h-10 rounded-full bg-white/[0.06] border border-white/10 flex items-center justify-center">
|
|
<User className="w-5 h-5 text-text-muted" />
|
|
</div>
|
|
<span className="text-xs text-text-muted">Fallback</span>
|
|
</div>
|
|
|
|
{/* With status dot */}
|
|
<div className="flex flex-col items-center gap-2">
|
|
<div className="relative">
|
|
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-violet-500 to-accent-blue flex items-center justify-center text-sm font-semibold text-white">
|
|
D
|
|
</div>
|
|
<div className="absolute -bottom-0.5 -right-0.5 w-3.5 h-3.5 rounded-full bg-emerald-500 border-2 border-bg-base" />
|
|
</div>
|
|
<span className="text-xs text-text-muted">Online</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Avatar group */}
|
|
<h3 className="font-[family-name:var(--font-heading)] text-lg font-semibold mb-4">
|
|
{t.avatarGroup}
|
|
</h3>
|
|
<div className="flex -space-x-2">
|
|
{["T", "D", "B", "L"].map((initial, i) => (
|
|
<div key={i} className="w-10 h-10 rounded-full bg-gradient-to-br from-violet-500 to-accent-blue flex items-center justify-center text-sm font-semibold text-white border-2 border-bg-base" style={{ opacity: 1 - i * 0.15 }}>
|
|
{initial}
|
|
</div>
|
|
))}
|
|
<div className="w-10 h-10 rounded-full bg-white/[0.08] border-2 border-bg-base flex items-center justify-center text-xs font-medium text-text-secondary">
|
|
+3
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Section>
|
|
|
|
{/* ═══════════════════════════════════════
|
|
17. BREADCRUMB
|
|
═══════════════════════════════════════ */}
|
|
<Section title={t.s17}>
|
|
<div className="glass-card p-6">
|
|
<nav aria-label="Breadcrumb" className="flex items-center gap-2 text-sm">
|
|
<a href="#" className="text-text-secondary hover:text-text-primary motion-safe:transition-colors cursor-pointer">
|
|
{t.projects}
|
|
</a>
|
|
<ChevronRight className="w-3.5 h-3.5 text-text-muted" />
|
|
<a href="#" className="text-text-secondary hover:text-text-primary motion-safe:transition-colors cursor-pointer">
|
|
T仔的上班日记
|
|
</a>
|
|
<ChevronRight className="w-3.5 h-3.5 text-text-muted" />
|
|
<a href="#" className="text-text-secondary hover:text-text-primary motion-safe:transition-colors cursor-pointer">
|
|
EP01
|
|
</a>
|
|
<ChevronRight className="w-3.5 h-3.5 text-text-muted" />
|
|
<span className="text-text-primary font-medium">{t.pipeline}</span>
|
|
</nav>
|
|
</div>
|
|
</Section>
|
|
|
|
{/* ═══════════════════════════════════════
|
|
18. TAB / SEGMENTED CONTROL
|
|
═══════════════════════════════════════ */}
|
|
<Section title={t.s18}>
|
|
<div className="glass-card p-8">
|
|
{/* Underline tabs */}
|
|
<h3 className="font-[family-name:var(--font-heading)] text-lg font-semibold mb-4">
|
|
{t.underlineTabs}
|
|
</h3>
|
|
<div className="flex border-b border-white/10 mb-6">
|
|
{t.tabs.map((tab) => (
|
|
<button
|
|
key={tab}
|
|
onClick={() => setActiveTab(tab)}
|
|
className={`px-4 py-3 text-sm font-medium capitalize motion-safe:transition-colors cursor-pointer relative ${
|
|
activeTab === tab
|
|
? "text-accent"
|
|
: "text-text-secondary hover:text-text-primary"
|
|
}`}
|
|
>
|
|
{tab}
|
|
{activeTab === tab && (
|
|
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-accent rounded-full" />
|
|
)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
<div className="text-sm text-text-secondary p-4 rounded-lg bg-white/[0.02]">
|
|
{t.activeTabLabel}: <span className="text-text-primary font-medium capitalize">{activeTab}</span>
|
|
</div>
|
|
|
|
{/* Segmented control (pill style) */}
|
|
<h3 className="font-[family-name:var(--font-heading)] text-lg font-semibold mb-4 mt-8">
|
|
{t.segCtrl}
|
|
</h3>
|
|
<div className="inline-flex bg-white/[0.04] rounded-lg p-1 border border-white/[0.06]">
|
|
{["Grid", "List", "Timeline"].map((seg) => (
|
|
<button
|
|
key={seg}
|
|
onClick={() => setActiveSeg(seg)}
|
|
className={`px-4 py-2 rounded-md text-sm font-medium motion-safe:transition-all cursor-pointer ${
|
|
activeSeg === seg
|
|
? "bg-accent/20 text-accent shadow-sm"
|
|
: "text-text-secondary hover:text-text-primary"
|
|
}`}
|
|
>
|
|
{seg}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</Section>
|
|
|
|
{/* ═══════════════════════════════════════
|
|
19. DANGER BUTTON + SEARCH INPUT
|
|
═══════════════════════════════════════ */}
|
|
<Section title={t.s19}>
|
|
<div className="grid grid-cols-2 gap-6">
|
|
{/* Danger buttons */}
|
|
<div className="glass-card p-8">
|
|
<h3 className="font-[family-name:var(--font-heading)] text-lg font-semibold mb-4">
|
|
{t.dangerBtns}
|
|
</h3>
|
|
<div className="flex flex-wrap items-center gap-4">
|
|
<button className="bg-red-500/15 border border-red-400/30 rounded-xl px-6 py-3 text-[15px] font-medium text-red-400 hover:bg-red-500/25 motion-safe:transition-colors cursor-pointer flex items-center gap-2 focus-visible:ring-2 focus-visible:ring-red-400 focus-visible:ring-offset-2 focus-visible:ring-offset-bg-base">
|
|
<Trash2 className="w-4 h-4" />
|
|
{t.deleteProject}
|
|
</button>
|
|
<button className="bg-red-600 border border-red-500 rounded-xl px-6 py-3 text-[15px] font-medium text-white hover:bg-red-700 motion-safe:transition-colors cursor-pointer focus-visible:ring-2 focus-visible:ring-red-400 focus-visible:ring-offset-2 focus-visible:ring-offset-bg-base">
|
|
{t.confirmDelete}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Search input */}
|
|
<div className="glass-card p-8">
|
|
<h3 className="font-[family-name:var(--font-heading)] text-lg font-semibold mb-4">
|
|
{t.searchInput}
|
|
</h3>
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-text-muted" />
|
|
<input
|
|
type="text"
|
|
placeholder={t.searchPlaceholder}
|
|
className="w-full bg-white/[0.04] border border-white/10 rounded-lg pl-10 pr-4 py-3 text-[15px] text-text-primary placeholder:text-text-muted focus:outline-none focus:border-accent/50 focus:ring-3 focus:ring-accent/15 transition-all duration-200"
|
|
/>
|
|
</div>
|
|
<p className="text-xs text-text-muted mt-2">
|
|
{t.searchNote}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</Section>
|
|
|
|
{/* ═══════════════════════════════════════
|
|
20. TOOLTIP (Hover Demo)
|
|
═══════════════════════════════════════ */}
|
|
<Section title={t.s20}>
|
|
<div className="glass-card p-8">
|
|
<div className="flex items-center gap-8">
|
|
{/* Top tooltip */}
|
|
<div className="group relative">
|
|
<button className="p-2.5 rounded-xl glass-card hover:bg-white/[0.12] motion-safe:transition-colors cursor-pointer" aria-label="Settings">
|
|
<Settings className="w-5 h-5 text-text-secondary" />
|
|
</button>
|
|
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-3 py-1.5 rounded-lg text-xs font-medium text-text-primary whitespace-nowrap opacity-0 group-hover:opacity-100 motion-safe:transition-opacity pointer-events-none border border-white/10" style={{ background: 'rgba(13, 13, 26, 0.95)', backdropFilter: 'blur(12px)' }}>
|
|
{t.tooltipSettings}
|
|
<div className="absolute top-full left-1/2 -translate-x-1/2 w-2 h-2 rotate-45 border-b border-r border-white/10" style={{ background: 'rgba(13, 13, 26, 0.95)' }} />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Right tooltip */}
|
|
<div className="group relative">
|
|
<button className="p-2.5 rounded-xl glass-card hover:bg-white/[0.12] motion-safe:transition-colors cursor-pointer" aria-label="Retry">
|
|
<RefreshCw className="w-5 h-5 text-text-secondary" />
|
|
</button>
|
|
<div className="absolute left-full top-1/2 -translate-y-1/2 ml-2 px-3 py-1.5 rounded-lg text-xs font-medium text-text-primary whitespace-nowrap opacity-0 group-hover:opacity-100 motion-safe:transition-opacity pointer-events-none border border-white/10" style={{ background: 'rgba(13, 13, 26, 0.95)', backdropFilter: 'blur(12px)' }}>
|
|
{t.tooltipRetry}
|
|
<div className="absolute right-full top-1/2 -translate-y-1/2 w-2 h-2 rotate-45 border-l border-b border-white/10" style={{ background: 'rgba(13, 13, 26, 0.95)' }} />
|
|
</div>
|
|
</div>
|
|
|
|
<p className="text-sm text-text-secondary ml-4">
|
|
{t.tooltipHint}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</Section>
|
|
|
|
{/* ═══════════════════════════════════════
|
|
21. TEXT CONTRAST CHECK
|
|
═══════════════════════════════════════ */}
|
|
<Section title={t.s21}>
|
|
<div className="glass-card p-6 space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-text-primary">
|
|
{t.primaryContrast}
|
|
</span>
|
|
<span className="font-[family-name:var(--font-mono)] text-xs text-emerald-400">
|
|
18.5:1 ✓ AAA
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-text-secondary">
|
|
{t.secondaryContrast}
|
|
</span>
|
|
<span className="font-[family-name:var(--font-mono)] text-xs text-emerald-400">
|
|
6.2:1 ✓ AA
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-text-muted">
|
|
{t.mutedContrast}
|
|
</span>
|
|
<span className="font-[family-name:var(--font-mono)] text-xs text-amber-400">
|
|
3.1:1 ⚠ {t.mutedNote}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</Section>
|
|
|
|
{/* Footer */}
|
|
<div className="text-center py-12 border-t border-white/5">
|
|
<p className="text-sm text-text-muted">
|
|
Air Spark — Design System v0.4
|
|
</p>
|
|
<p className="text-xs text-text-muted mt-1">
|
|
{t.footer}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|