0424重新生成

This commit is contained in:
yuzhantian 2026-04-24 16:08:37 +08:00
parent bdd11126e6
commit 213cef22f7
84 changed files with 3340 additions and 15671 deletions

18
.eslintrc.js Normal file
View File

@ -0,0 +1,18 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.js'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}

56
.gitignore vendored
View File

@ -1,15 +1,27 @@
# Dependencies
node_modules/
demo/
# Build output
dist/
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Environment variables
.env
@ -18,15 +30,25 @@ yarn-error.log*
.env.test.local
.env.production.local
# Editor directories and files
.vscode/
.idea/
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Testing
coverage
.nyc_output
# OS files
# Production
build
# Misc
.DS_Store
Thumbs.db
.env.local
.env.development.local
.env.test.local
.env.production.local
# Debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Package manager
.npm
.yarn-integrity

6
.prettierrc.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
trailingComma: "es5",
tabWidth: 2,
semi: true,
singleQuote: true,
}

View File

@ -0,0 +1,8 @@
{
"hash": "5f8903bf",
"configHash": "996545d1",
"lockfileHash": "e3b0c442",
"browserHash": "9c575c4d",
"optimized": {},
"chunks": {}
}

3
.vite/deps/package.json Normal file
View File

@ -0,0 +1,3 @@
{
"type": "module"
}

View File

@ -0,0 +1,3 @@
{
"type": "module"
}

View File

@ -1,2 +0,0 @@
# uitocode

65
design/BaseInfo.md Normal file
View File

@ -0,0 +1,65 @@
## 项目结构
```
project-root/
├── design/ # 设计文档
│ ├── DESIGN.md # 设计规范
│ └── Page.md # 页面设计
├── public/ # 静态资源
├── src/
│ ├── assets/ # 项目资源文件
│ │ ├── images/ # 图片资源
│ │ └── icons/ # 图标资源
│ ├── components/ # 公共组件
│ │ ├── ui/ # 基础UI组件
│ │ └── layout/ # 布局组件
│ ├── pages/ # 页面组件
│ │ ├── auth/ # 认证相关页面
│ │ │ ├── Login.tsx # 登录页面
│ │ │ └── Register.tsx # 注册页面
│ │ ├── dashboard/ # 首页
│ │ ├── project/ # 项目管理
│ │ │ ├── _components/ # 页面级组件
│ │ │ ├── ProjectList.tsx # 项目列表
│ │ │ └── ProjectDetail.tsx # 项目详情
│ │ └── settings/ # 设置页面
│ ├── services/ # 服务层
│ │ ├── api/ # API 调用
│ │ └── auth/ # 认证服务
│ ├── utils/ # 工具函数
│ ├── constants/ # 常量定义
│ ├── hooks/ # 自定义 hooks
│ ├── types/ # TypeScript 类型定义
│ ├── i18n/ # 国际化文件
│ │ ├── en.json # 英语
│ │ └── zh.json # 中文
│ ├── App.tsx # 应用入口组件
│ ├── main.tsx # 入口文件
│ └── routes.tsx # 路由配置
├── .gitignore # 忽略文件
├── .eslintrc.js # ESLint 配置
├── .prettierrc.js # Prettier 配置
├── tsconfig.json # TypeScript 配置
├── vite.config.ts # Vite 配置
├── package.json # 项目依赖
└── README.md # 项目说明
```
## 设计规范
### 整体风格
- 风格meta-light 风格严格按照design/DESIGN.md中的light-mode设计规范
- 布局:响应式设计,以 1920*1080 分辨率为主
### 组件规范
- 设计规范严格按照design/DESIGN.md中的设计规范
## 国际化
- 支持语言:中文、英语,默认使用中文
- 实现方式i18next
- 翻译文件位置src/i18n/
- 组件内使用useTranslation hook
## 性能优化
- 代码分割:使用 React.lazy 和 Suspense
- 状态管理优化:合理使用 memo、useCallback、useMemo
- 图片优化:使用适当的图片格式和大小
- 网络请求优化:使用 axios 拦截器,合理缓存

131
design/Page.md Normal file
View File

@ -0,0 +1,131 @@
## 项目结构
```
project-root/
├── design/ # 设计文档
│ ├── DESIGN.md # 设计规范
│ └── Page.md # 页面设计
├── public/ # 静态资源
├── src/
│ ├── assets/ # 项目资源文件
│ │ ├── images/ # 图片资源
│ │ └── icons/ # 图标资源
│ ├── components/ # 公共组件
│ │ ├── ui/ # 基础UI组件
│ │ └── layout/ # 布局组件
│ ├── pages/ # 页面组件
│ │ ├── auth/ # 认证相关页面
│ │ │ ├── Login.tsx # 登录页面
│ │ │ └── Register.tsx # 注册页面
│ │ ├── dashboard/ # 首页
│ │ ├── project/ # 项目管理
│ │ │ ├── _components/ # 页面级组件
│ │ │ ├── ProjectList.tsx # 项目列表
│ │ │ └── ProjectDetail.tsx # 项目详情
│ │ └── settings/ # 设置页面
│ ├── services/ # 服务层
│ │ ├── api/ # API 调用
│ │ └── auth/ # 认证服务
│ ├── utils/ # 工具函数
│ ├── constants/ # 常量定义
│ ├── hooks/ # 自定义 hooks
│ ├── types/ # TypeScript 类型定义
│ ├── i18n/ # 国际化文件
│ │ ├── en.json # 英语
│ │ └── zh.json # 中文
│ ├── App.tsx # 应用入口组件
│ ├── main.tsx # 入口文件
│ └── routes.tsx # 路由配置
├── .gitignore # 忽略文件
├── .eslintrc.js # ESLint 配置
├── .prettierrc.js # Prettier 配置
├── tsconfig.json # TypeScript 配置
├── vite.config.ts # Vite 配置
├── package.json # 项目依赖
└── README.md # 项目说明
```
## 设计规范
### 整体风格
- 风格meta-light 风格严格按照design/DESIGN.md中的light-mode设计规范
- 布局:响应式设计,以 1920*1080 分辨率为主
### 组件规范
- 设计规范严格按照design/DESIGN.md中的设计规范
## 数据流转
- 认证流程:
- 登录 → 验证 credentials → 生成 token → 存储 token → 跳转到首页
- 注册 → 验证表单 → 创建用户 → 登录 → 跳转到首页
- 项目流程:
- 创建项目 → 保存到服务器 → 更新项目列表
- 编辑项目 → 保存到服务器 → 更新项目详情
- 删除项目 → 确认对话框 → 从服务器删除 → 更新项目列表
## 国际化
- 支持语言:中文、英语,默认使用中文
- 实现方式i18next
- 翻译文件位置src/i18n/
- 组件内使用useTranslation hook
## 安全考虑
- 密码加密:使用 bcrypt 或类似算法
- 认证JWT token
- CSRF 防护:使用 CSRF token
- XSS 防护:使用 React 的内置防护
- 数据验证:前端和后端双重验证
## 性能优化
- 代码分割:使用 React.lazy 和 Suspense
- 状态管理优化:合理使用 memo、useCallback、useMemo
- 图片优化:使用适当的图片格式和大小
- 网络请求优化:使用 axios 拦截器,合理缓存
## 页面组成
详见design/pages/中各页面的md文件
### 6. 项目详情
进入详情后不显示左侧菜单树、顶部导航栏只显示画布区域、左侧工具栏、右侧顶部工具栏、右侧AI对话框。
- 画布区域:
- 以固定外框将html以iframe嵌套的方式显示出来
- iframe内的元素可在画布区域内选择和高亮
- 添加、删除、修改元素
- 拖拽功能
- 预览功能
- 选中元素后可将元素的id加入到AI对话框的问题中
- 左侧工具栏(绝对定位,距离屏幕左侧 10px顶部 10px抽取为单独组件
- 工具栏可展开/收起,垂直布局,工具栏只显示图标,不显示文字描述,鼠标悬停显示文字描述
- 页面:查看项目内所有页面,点击切换到该页面,切换前会弹窗提示,是否确认切换,用户确认后保存当前页面,切换到新页面
- 原型库:查看项目内所有原型,点击弹窗显示详情
- 备注添加、删除、修改项目元素的备注需先选中iframe中的元素否则会提示用户先选中元素
- 资源管理:查看项目内所有资源,点击弹窗显示详情
- 全屏预览:查看设计稿的全屏预览
- 提示词库:查看项目内所有提示词,点击弹窗显示详情
- 右侧AI对话框绝对定位距离屏幕右侧10px底部10px抽取为单独组件
- 对话框可展开/收起,垂直布局
- 顶部标题:
- 显示“AI助手”
- 对话历史按钮:点击后展开/收起对话历史列表,图标为“历史图标”;
- 对话历史列表显示用户之前与AI助手的对话记录只显示标题和时间不显示回复内容
- 点击对话记录,中间输出框展开显示该对话记录的详细内容
- 展开时对话历史列表紧贴着AI对话框对话历史列表的右侧距离AI对话框的左侧 10px顶部与AI对话框的顶部对齐收起时隐藏对话历史列表
- 右侧切换模型下拉框可选择不同AI模型
- 中间输出框显示用户输入的问题和AI回复
- 每个问题和回复占一行,问题和回复之间用空行隔开,可滚动查看
- 回复内容可复制、重新生成、点赞、踩,图标为“复制图标”、“重新生成图标”、“点赞图标”、“踩下图标”
- 问题可点击删除,删除该问题和对应的回复内容
- 问题可点击编辑,修改该问题的内容
- 问题可点击重试重新触发AI回复图标为“重试图标”
- 底部输入框用户输入问题AI回复会显示在输出框中
- 底部输入框按钮组(相对于底部输入框绝对定位,距离底部 10px
- 上传附件按钮:点击后调用浏览器上传文件功能,支持上传图片、doc、docx、pdf、PDF、MD等类型文件上传后显示在输出框中。
- 快捷命令按钮:点击后显示快捷命令,图标为“/”
- 语音输入图标:点击后调起语音输入功能,支持中文和英文语音,图标为“语音图标”
- 发送按钮点击后触发AI回复图标为“发送图标”
- 右侧顶部工具栏绝对定位距离屏幕右侧10px顶部10px抽取为单独组件
- 顶部工具栏可展开/收起,横向布局,工具栏只显示图标,不显示文字描述,鼠标悬停显示文字描述
- 新建页面:提供新建画布的能力
- 保存页面:提供保存画布的能力
- 导入页面提供导入html创建画布的能力
- 导出页面提供导出为html、导出为XD文件的能力
- 退出:返回到项目,返回前如果有未保存的更改,会弹窗提示,是否确认退出

View File

@ -1,220 +0,0 @@
# Design System Inspiration of Figma
## 1. Visual Theme & Atmosphere
Figma's interface is the design tool that designed itself — a masterclass in typographic sophistication where a custom variable font (figmaSans) modulates between razor-thin (weight 320) and bold (weight 700) with stops at unusual intermediates (330, 340, 450, 480, 540) that most type systems never explore. This granular weight control gives every text element a precisely calibrated visual weight, creating hierarchy through micro-differences rather than the blunt instrument of "regular vs bold."
The page presents a fascinating duality: the interface chrome is strictly black-and-white (literally only `#000000` and `#ffffff` detected as colors), while the hero section and product showcases explode with vibrant multi-color gradients — electric greens, bright yellows, deep purples, hot pinks. This separation means the design system itself is colorless, treating the product's colorful output as the hero content. Figma's marketing page is essentially a white gallery wall displaying colorful art.
What makes Figma distinctive beyond the variable font is its circle-and-pill geometry. Buttons use 50px radius (pill) or 50% (perfect circle for icon buttons), creating an organic, tool-palette-like feel. The dashed-outline focus indicator (`dashed 2px`) is a deliberate design choice that echoes selection handles in the Figma editor itself — the website's UI language references the product's UI language.
**Key Characteristics:**
- Custom variable font (figmaSans) with unusual weight stops: 320, 330, 340, 450, 480, 540, 700
- Strictly black-and-white interface chrome — color exists only in product content
- figmaMono for uppercase technical labels with wide letter-spacing
- Pill (50px) and circular (50%) button geometry
- Dashed focus outlines echoing Figma's editor selection handles
- Vibrant multi-color hero gradients (green, yellow, purple, pink)
- OpenType `"kern"` feature enabled globally
- Negative letter-spacing throughout — even body text at -0.14px to -0.26px
## 2. Color Palette & Roles
### Primary
- **Pure Black** (`#000000`): All text, all solid buttons, all borders. The sole "color" of the interface.
- **Pure White** (`#ffffff`): All backgrounds, white buttons, text on dark surfaces. The other half of the binary.
*Note: Figma's marketing site uses ONLY these two colors for its interface layer. All vibrant colors appear exclusively in product screenshots, hero gradients, and embedded content.*
### Surface & Background
- **Pure White** (`#ffffff`): Primary page background and card surfaces.
- **Glass Black** (`rgba(0, 0, 0, 0.08)`): Subtle dark overlay for secondary circular buttons and glass effects.
- **Glass White** (`rgba(255, 255, 255, 0.16)`): Frosted glass overlay for buttons on dark/colored surfaces.
### Gradient System
- **Hero Gradient**: A vibrant multi-stop gradient using electric green, bright yellow, deep purple, and hot pink. This gradient is the visual signature of the hero section — it represents the creative possibilities of the tool.
- **Product Section Gradients**: Individual product areas (Design, Dev Mode, Prototyping) may use distinct color themes in their showcases.
## 3. Typography Rules
### Font Family
- **Primary**: `figmaSans`, with fallbacks: `figmaSans Fallback, SF Pro Display, system-ui, helvetica`
- **Monospace / Labels**: `figmaMono`, with fallbacks: `figmaMono Fallback, SF Mono, menlo`
### Hierarchy
| Role | Font | Size | Weight | Line Height | Letter Spacing | Notes |
|------|------|------|--------|-------------|----------------|-------|
| Display / Hero | figmaSans | 86px (5.38rem) | 400 | 1.00 (tight) | -1.72px | Maximum impact, extreme tracking |
| Section Heading | figmaSans | 64px (4rem) | 400 | 1.10 (tight) | -0.96px | Feature section titles |
| Sub-heading | figmaSans | 26px (1.63rem) | 540 | 1.35 | -0.26px | Emphasized section text |
| Sub-heading Light | figmaSans | 26px (1.63rem) | 340 | 1.35 | -0.26px | Light-weight section text |
| Feature Title | figmaSans | 24px (1.5rem) | 700 | 1.45 | normal | Bold card headings |
| Body Large | figmaSans | 20px (1.25rem) | 330450 | 1.301.40 | -0.1px to -0.14px | Descriptions, intros |
| Body / Button | figmaSans | 16px (1rem) | 330400 | 1.401.45 | -0.14px to normal | Standard body, nav, buttons |
| Body Light | figmaSans | 18px (1.13rem) | 320 | 1.45 | -0.26px to normal | Light-weight body text |
| Mono Label | figmaMono | 18px (1.13rem) | 400 | 1.30 (tight) | 0.54px | Uppercase section labels |
| Mono Small | figmaMono | 12px (0.75rem) | 400 | 1.00 (tight) | 0.6px | Uppercase tiny tags |
### Principles
- **Variable font precision**: figmaSans uses weights that most systems never touch — 320, 330, 340, 450, 480, 540. This creates hierarchy through subtle weight differences rather than dramatic jumps. The difference between 330 and 340 is nearly imperceptible but structurally significant.
- **Light as the base**: Most body text uses 320340 (lighter than typical 400 "regular"), creating an ethereal, airy reading experience that matches the design-tool aesthetic.
- **Kern everywhere**: Every text element enables OpenType `"kern"` feature — kerning is not optional, it's structural.
- **Negative tracking by default**: Even body text uses -0.1px to -0.26px letter-spacing, creating universally tight text. Display text compresses further to -0.96px and -1.72px.
- **Mono for structure**: figmaMono in uppercase with positive letter-spacing (0.54px0.6px) creates technical signpost labels.
## 4. Component Stylings
### Buttons
**Black Solid (Pill)**
- Background: Pure Black (`#000000`)
- Text: Pure White (`#ffffff`)
- Radius: circle (50%) for icon buttons
- Focus: dashed 2px outline
- Maximum emphasis
**White Pill**
- Background: Pure White (`#ffffff`)
- Text: Pure Black (`#000000`)
- Padding: 8px 18px 10px (asymmetric vertical)
- Radius: pill (50px)
- Focus: dashed 2px outline
- Standard CTA on dark/colored surfaces
**Glass Dark**
- Background: `rgba(0, 0, 0, 0.08)` (subtle dark overlay)
- Text: Pure Black
- Radius: circle (50%)
- Focus: dashed 2px outline
- Secondary action on light surfaces
**Glass Light**
- Background: `rgba(255, 255, 255, 0.16)` (frosted glass)
- Text: Pure White
- Radius: circle (50%)
- Focus: dashed 2px outline
- Secondary action on dark/colored surfaces
### Cards & Containers
- Background: Pure White
- Border: none or minimal
- Radius: 6px (small containers), 8px (images, cards, dialogs)
- Shadow: subtle to medium elevation effects
- Product screenshots as card content
### Navigation
- Clean horizontal nav on white
- Logo: Figma wordmark in black
- Product tabs: pill-shaped (50px) tab navigation
- Links: black text, underline 1px decoration
- CTA: Black pill button
- Hover: text color via CSS variable
### Distinctive Components
**Product Tab Bar**
- Horizontal pill-shaped tabs (50px radius)
- Each tab represents a Figma product area (Design, Dev Mode, Prototyping, etc.)
- Active tab highlighted
**Hero Gradient Section**
- Full-width vibrant multi-color gradient background
- White text overlay with 86px display heading
- Product screenshots floating within the gradient
**Dashed Focus Indicators**
- All interactive elements use `dashed 2px` outline on focus
- References the selection handles in the Figma editor
- A meta-design choice connecting website and product
## 5. Layout Principles
### Spacing System
- Base unit: 8px
- Scale: 1px, 2px, 4px, 4.5px, 8px, 10px, 12px, 16px, 18px, 24px, 32px, 40px, 46px, 48px, 50px
### Grid & Container
- Max container width: up to 1920px
- Hero: full-width gradient with centered content
- Product sections: alternating showcases
- Footer: dark full-width section
- Responsive from 559px to 1920px
### Whitespace Philosophy
- **Gallery-like pacing**: Generous spacing lets each product section breathe as its own exhibit.
- **Color sections as visual breathing**: The gradient hero and product showcases provide chromatic relief between the monochrome interface sections.
### Border Radius Scale
- Minimal (2px): Small link elements
- Subtle (6px): Small containers, dividers
- Comfortable (8px): Cards, images, dialogs
- Pill (50px): Tab buttons, CTAs
- Circle (50%): Icon buttons, circular elements
## 6. Depth & Elevation
| Level | Treatment | Use |
|-------|-----------|-----|
| Flat (Level 0) | No shadow | Page background, most text |
| Surface (Level 1) | White card on gradient/dark section | Cards, product showcases |
| Elevated (Level 2) | Subtle shadow | Floating cards, hover states |
**Shadow Philosophy**: Figma uses shadows sparingly. The primary depth mechanisms are **background contrast** (white content on colorful/dark sections) and the inherent dimensionality of the product screenshots themselves.
## 7. Do's and Don'ts
### Do
- Use figmaSans with precise variable weights (320540) — the granular weight control IS the design
- Keep the interface strictly black-and-white — color comes from product content only
- Use pill (50px) and circular (50%) geometry for all interactive elements
- Apply dashed 2px focus outlines — the signature accessibility pattern
- Enable `"kern"` feature on all text
- Use figmaMono in uppercase with positive letter-spacing for labels
- Apply negative letter-spacing throughout (-0.1px to -1.72px)
### Don't
- Don't add interface colors — the monochrome palette is absolute
- Don't use standard font weights (400, 500, 600, 700) — use the variable font's unique stops (320, 330, 340, 450, 480, 540)
- Don't use sharp corners on buttons — pill and circular geometry only
- Don't use solid focus outlines — dashed is the signature
- Don't increase body font weight above 450 — the light-weight aesthetic is core
- Don't use positive letter-spacing on body text — it's always negative
## 8. Responsive Behavior
### Breakpoints
| Name | Width | Key Changes |
|------|-------|-------------|
| Small Mobile | <560px | Compact layout, stacked |
| Tablet | 560768px | Minor adjustments |
| Small Desktop | 768960px | 2-column layouts |
| Desktop | 9601280px | Standard layout |
| Large Desktop | 12801440px | Expanded |
| Ultra-wide | 14401920px | Maximum width |
### Collapsing Strategy
- Hero text: 86px → 64px → 48px
- Product tabs: horizontal scroll on mobile
- Feature sections: stacked single column
- Footer: multi-column → stacked
## 9. Agent Prompt Guide
### Quick Color Reference
- Everything: "Pure Black (#000000)" and "Pure White (#ffffff)"
- Glass Dark: "rgba(0, 0, 0, 0.08)"
- Glass Light: "rgba(255, 255, 255, 0.16)"
### Example Component Prompts
- "Create a hero on a vibrant multi-color gradient (green, yellow, purple, pink). Headline at 86px figmaSans weight 400, line-height 1.0, letter-spacing -1.72px. White text. White pill CTA button (50px radius, 8px 18px padding)."
- "Design a product tab bar with pill-shaped buttons (50px radius). Active: Black bg, white text. Inactive: transparent, black text. figmaSans at 20px weight 480."
- "Build a section label: figmaMono 18px, uppercase, letter-spacing 0.54px, black text. Kern enabled."
- "Create body text at 20px figmaSans weight 330, line-height 1.40, letter-spacing -0.14px. Pure Black on white."
### Iteration Guide
1. Use variable font weight stops precisely: 320, 330, 340, 450, 480, 540, 700
2. Interface is always black + white — never add colors to chrome
3. Dashed focus outlines, not solid
4. Letter-spacing is always negative on body, always positive on mono labels
5. Pill (50px) for buttons/tabs, circle (50%) for icon buttons

View File

@ -1,24 +0,0 @@
# Figma Inspired Design System
[DESIGN.md](https://github.com/VoltAgent/awesome-design-md/blob/main/design-md/figma/DESIGN.md) extracted from the public [Figma](https://figma.com/) website. This is not the official design system. Colors, fonts, and spacing may not be 100% accurate. But it's a good starting point for building something similar.
## Files
| File | Description |
|------|-------------|
| `DESIGN.md` | Complete design system documentation (9 sections) |
| `preview.html` | Interactive design token catalog (light) |
| `preview-dark.html` | Interactive design token catalog (dark) |
Use [DESIGN.md](https://github.com/VoltAgent/awesome-design-md/blob/main/design-md/figma/DESIGN.md) to use as a reference for AI agents (Claude, Cursor, Stitch) to generate UI that looks like the Figma design language.
## Preview
A sample landing page built with DESIGN.md. It shows the actual colors, typography, buttons, cards, spacing, and elevation, all in one page.
### Dark Mode
![Figma Design System — Dark Mode](https://pub-2e4ecbcbc9b24e7b93f1a6ab5b2bc71f.r2.dev/designs/figma/preview-dark-screenshot.png)
### Light Mode
![Figma Design System — Light Mode](https://pub-2e4ecbcbc9b24e7b93f1a6ab5b2bc71f.r2.dev/designs/figma/preview-screenshot.png)

View File

@ -1,822 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Design System Inspired by Figma</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&display=swap" rel="stylesheet">
<style>
:root {
--color-black: #000000;
--color-white: #ffffff;
--glass-dark: rgba(0, 0, 0, 0.08);
--glass-light: rgba(255, 255, 255, 0.16);
--font-sans: system-ui, -apple-system, 'Segoe UI', 'SF Pro Display', Helvetica, Arial, sans-serif;
--font-mono: 'Space Mono', 'SF Mono', Menlo, monospace;
/* Dark mode tokens */
--bg-page: #000000;
--bg-card: #000000;
--bg-nav: rgba(0, 0, 0, 0.92);
--text-primary: #ffffff;
--text-secondary: #ffffff;
--text-muted: rgba(255, 255, 255, 0.5);
--border-color: rgba(255, 255, 255, 0.12);
--border-subtle: rgba(255, 255, 255, 0.06);
--section-label-color: #ffffff;
--glass-surface: rgba(255, 255, 255, 0.08);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: var(--bg-page);
color: var(--text-primary);
font-family: var(--font-sans);
font-size: 16px;
font-weight: 340;
line-height: 1.45;
letter-spacing: -0.14px;
font-feature-settings: "kern" 1;
-webkit-font-smoothing: antialiased;
}
/* NAV */
.nav {
position: sticky;
top: 0;
z-index: 100;
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 40px;
background: var(--bg-nav);
backdrop-filter: blur(16px);
border-bottom: 1px solid var(--border-color);
}
.nav-brand {
font-family: var(--font-sans);
font-size: 18px;
font-weight: 700;
letter-spacing: -0.26px;
display: flex;
align-items: center;
gap: 10px;
color: var(--color-white);
}
.nav-brand svg { width: 24px; height: 24px; }
.nav-links { display: flex; gap: 32px; align-items: center; }
.nav-links a {
color: var(--text-primary);
text-decoration: none;
font-size: 16px;
font-weight: 400;
letter-spacing: -0.14px;
transition: opacity 0.2s;
}
.nav-links a:hover { opacity: 0.6; }
.nav-cta {
background: var(--color-white);
color: var(--color-black);
padding: 8px 18px 10px;
border: none;
border-radius: 50px;
font-size: 16px;
font-family: var(--font-sans);
font-weight: 480;
letter-spacing: -0.14px;
cursor: pointer;
}
.nav-cta:focus { outline: 2px dashed var(--color-white); outline-offset: 3px; }
/* HERO */
.hero {
position: relative;
text-align: center;
padding: 120px 40px 100px;
overflow: hidden;
background: linear-gradient(135deg, #0acf83 0%, #a259ff 30%, #f24e1e 50%, #ff7262 65%, #1abcfe 80%, #0acf83 100%);
}
.hero h1 {
font-family: var(--font-sans);
font-size: 86px;
font-weight: 400;
line-height: 1.0;
letter-spacing: -1.72px;
margin-bottom: 24px;
color: var(--color-white);
position: relative;
}
.hero p {
color: rgba(255, 255, 255, 0.85);
font-size: 20px;
font-weight: 330;
line-height: 1.4;
letter-spacing: -0.14px;
margin-bottom: 40px;
position: relative;
}
.hero-buttons { display: flex; gap: 16px; justify-content: center; position: relative; }
.btn-hero-primary {
background: var(--color-white);
color: var(--color-black);
padding: 8px 18px 10px;
border: none;
border-radius: 50px;
font-size: 16px;
font-family: var(--font-sans);
font-weight: 480;
letter-spacing: -0.14px;
cursor: pointer;
}
.btn-hero-primary:focus { outline: 2px dashed var(--color-white); outline-offset: 3px; }
.btn-hero-glass {
background: rgba(255, 255, 255, 0.16);
color: var(--color-white);
padding: 8px 18px 10px;
border: none;
border-radius: 50px;
font-size: 16px;
font-family: var(--font-sans);
font-weight: 480;
letter-spacing: -0.14px;
cursor: pointer;
backdrop-filter: blur(8px);
}
.btn-hero-glass:focus { outline: 2px dashed var(--color-white); outline-offset: 3px; }
/* PRODUCT TABS */
.product-tabs {
display: flex;
gap: 8px;
justify-content: center;
margin-bottom: 40px;
position: relative;
}
.product-tab {
background: rgba(255, 255, 255, 0.16);
color: var(--color-white);
padding: 8px 18px;
border: none;
border-radius: 50px;
font-size: 16px;
font-family: var(--font-sans);
font-weight: 480;
letter-spacing: -0.14px;
cursor: pointer;
backdrop-filter: blur(8px);
transition: background 0.2s;
}
.product-tab.active {
background: var(--color-white);
color: var(--color-black);
}
/* SECTIONS */
.section {
max-width: 1200px;
margin: 0 auto;
padding: 80px 40px;
}
.section-title {
font-family: var(--font-mono);
font-size: 18px;
font-weight: 400;
text-transform: uppercase;
letter-spacing: 0.54px;
color: var(--text-muted);
margin-bottom: 12px;
line-height: 1.3;
}
.section-heading {
font-family: var(--font-sans);
font-size: 64px;
font-weight: 400;
line-height: 1.1;
letter-spacing: -0.96px;
margin-bottom: 48px;
}
.section-divider {
border: none;
border-top: 1px solid var(--border-subtle);
margin: 0 40px;
max-width: 1200px;
margin-left: auto;
margin-right: auto;
}
/* COLOR PALETTE */
.color-group { margin-bottom: 40px; }
.color-group-title {
font-size: 26px;
font-weight: 540;
line-height: 1.35;
letter-spacing: -0.26px;
margin-bottom: 20px;
}
.color-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 16px;
}
.color-swatch {
border: 1px solid var(--border-color);
border-radius: 8px;
overflow: hidden;
}
.color-swatch-block {
height: 80px;
position: relative;
}
.color-swatch-info {
padding: 12px;
background: var(--bg-card);
}
.color-swatch-name { font-size: 14px; font-weight: 700; letter-spacing: -0.14px; margin-bottom: 2px; }
.color-swatch-hex { font-family: var(--font-mono); font-size: 12px; color: var(--text-muted); margin-bottom: 4px; letter-spacing: 0.6px; }
.color-swatch-role { font-size: 12px; color: var(--text-muted); line-height: 1.4; font-weight: 340; }
/* GRADIENT PREVIEW */
.gradient-preview {
border-radius: 8px;
overflow: hidden;
margin-bottom: 16px;
}
.gradient-block {
height: 120px;
border-radius: 8px;
}
.gradient-label {
font-family: var(--font-mono);
font-size: 12px;
color: var(--text-muted);
margin-top: 8px;
letter-spacing: 0.6px;
text-transform: uppercase;
}
/* TYPOGRAPHY */
.type-sample { margin-bottom: 32px; padding-bottom: 32px; border-bottom: 1px solid var(--border-subtle); }
.type-sample:last-child { border-bottom: none; }
.type-sample-text { margin-bottom: 8px; font-feature-settings: "kern" 1; }
.type-sample-label {
font-family: var(--font-mono);
font-size: 12px;
color: var(--text-muted);
letter-spacing: 0.6px;
}
/* WEIGHT SPECTRUM */
.weight-spectrum { display: flex; flex-direction: column; gap: 12px; margin-bottom: 40px; padding: 24px; background: var(--glass-surface); border-radius: 8px; }
.weight-sample { display: flex; align-items: baseline; gap: 16px; }
.weight-sample-text { font-family: var(--font-sans); font-size: 24px; letter-spacing: -0.26px; line-height: 1.35; flex: 1; }
.weight-sample-label { font-family: var(--font-mono); font-size: 11px; color: var(--text-muted); letter-spacing: 0.6px; text-transform: uppercase; min-width: 80px; text-align: right; }
/* BUTTONS */
.button-row {
display: flex;
flex-wrap: wrap;
gap: 24px;
align-items: flex-start;
}
.button-demo { text-align: center; }
.button-demo-label {
font-family: var(--font-mono);
font-size: 11px;
color: var(--text-muted);
margin-top: 12px;
text-transform: uppercase;
letter-spacing: 0.6px;
}
.btn-white-pill-solid {
background: var(--color-white);
color: var(--color-black);
padding: 8px 18px 10px;
border: none;
border-radius: 50px;
font-size: 16px;
font-family: var(--font-sans);
font-weight: 480;
letter-spacing: -0.14px;
cursor: pointer;
}
.btn-white-pill-solid:focus { outline: 2px dashed var(--color-white); outline-offset: 3px; }
.btn-dark-pill-outline {
background: transparent;
color: var(--color-white);
padding: 8px 18px 10px;
border: 1px solid var(--border-color);
border-radius: 50px;
font-size: 16px;
font-family: var(--font-sans);
font-weight: 480;
letter-spacing: -0.14px;
cursor: pointer;
}
.btn-dark-pill-outline:focus { outline: 2px dashed var(--color-white); outline-offset: 3px; }
.btn-white-circle {
background: var(--color-white);
color: var(--color-black);
width: 48px;
height: 48px;
border: none;
border-radius: 50%;
font-size: 20px;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
}
.btn-white-circle:focus { outline: 2px dashed var(--color-white); outline-offset: 3px; }
.btn-glass-light-circle {
background: rgba(255, 255, 255, 0.16);
color: var(--color-white);
width: 48px;
height: 48px;
border: none;
border-radius: 50%;
font-size: 20px;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
}
.btn-glass-light-circle:focus { outline: 2px dashed var(--color-white); outline-offset: 3px; }
.btn-glass-dark-circle {
background: rgba(0, 0, 0, 0.08);
color: var(--color-white);
width: 48px;
height: 48px;
border: none;
border-radius: 50%;
font-size: 20px;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
border: 1px solid var(--border-color);
}
.btn-glass-dark-circle:focus { outline: 2px dashed var(--color-white); outline-offset: 3px; }
/* FOCUS DEMO */
.focus-demo-row {
display: flex;
flex-wrap: wrap;
gap: 24px;
align-items: center;
margin-top: 40px;
padding: 24px;
background: var(--glass-surface);
border-radius: 8px;
}
.focus-demo-label {
font-family: var(--font-mono);
font-size: 12px;
color: var(--text-muted);
letter-spacing: 0.6px;
text-transform: uppercase;
margin-bottom: 8px;
}
.btn-focus-visible {
outline: 2px dashed var(--color-white);
outline-offset: 3px;
}
/* CARDS */
.card-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 24px; }
.card {
background: var(--bg-card);
border-radius: 8px;
padding: 28px;
}
.card-standard { border: 1px solid var(--border-color); }
.card-elevated {
border: 1px solid var(--border-color);
box-shadow: 0 4px 24px rgba(255, 255, 255, 0.04);
}
.card-glass {
background: var(--glass-surface);
border: none;
}
.card h3 {
font-family: var(--font-sans);
font-size: 24px;
font-weight: 700;
line-height: 1.45;
letter-spacing: normal;
margin-bottom: 12px;
}
.card p { color: var(--text-muted); font-size: 16px; line-height: 1.45; font-weight: 330; letter-spacing: -0.14px; }
.card-label {
font-family: var(--font-mono);
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.6px;
color: var(--text-muted);
margin-bottom: 16px;
}
/* SPACING */
.spacing-row { display: flex; flex-wrap: wrap; gap: 12px; align-items: flex-end; }
.spacing-item { text-align: center; }
.spacing-box {
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 2px;
margin-bottom: 8px;
}
.spacing-label {
font-family: var(--font-mono);
font-size: 11px;
color: var(--text-muted);
letter-spacing: 0.6px;
}
/* RADIUS */
.radius-row { display: flex; flex-wrap: wrap; gap: 24px; align-items: center; }
.radius-item { text-align: center; }
.radius-box {
width: 80px;
height: 80px;
background: var(--glass-surface);
border: 1px solid var(--border-color);
margin-bottom: 8px;
}
.radius-label {
font-family: var(--font-mono);
font-size: 11px;
color: var(--text-muted);
letter-spacing: 0.6px;
}
.radius-context {
font-size: 11px;
color: var(--text-muted);
margin-top: 2px;
font-weight: 340;
}
/* ELEVATION */
.elevation-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 24px; }
.elevation-card {
background: var(--bg-card);
border-radius: 8px;
padding: 24px;
min-height: 140px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.elevation-flat { border: none; background: var(--bg-page); }
.elevation-surface { background: var(--glass-surface); border: 1px solid var(--border-color); }
.elevation-elevated { border: 1px solid var(--border-color); box-shadow: 0 4px 24px rgba(255, 255, 255, 0.04); }
.elevation-name { font-size: 16px; font-weight: 700; margin-bottom: 8px; letter-spacing: normal; }
.elevation-desc { font-size: 13px; color: var(--text-muted); line-height: 1.5; font-weight: 340; }
.elevation-level {
font-family: var(--font-mono);
font-size: 11px;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.6px;
margin-top: 12px;
}
/* RESPONSIVE */
@media (max-width: 768px) {
.nav { padding: 12px 20px; }
.nav-links a:not(.nav-cta-wrapper) { display: none; }
.hero { padding: 80px 20px 60px; }
.hero h1 { font-size: 48px; letter-spacing: -0.96px; }
.section { padding: 60px 20px; }
.section-heading { font-size: 36px; letter-spacing: -0.72px; }
.color-grid { grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); }
.card-grid { grid-template-columns: 1fr; }
.hero-buttons { flex-direction: column; align-items: center; }
.button-row { flex-direction: column; align-items: flex-start; }
.product-tabs { flex-wrap: wrap; }
}
</style>
</head>
<body>
<!-- NAV -->
<nav class="nav">
<span class="nav-brand">awesome-design-md</span>
<div class="nav-links">
<a href="#colors">Colors</a>
<a href="#typography">Typography</a>
<a href="#buttons">Buttons</a>
<a href="#cards">Cards</a>
<a href="#spacing">Spacing</a>
<a href="#elevation">Elevation</a>
<button class="nav-cta">Get started</button>
</div>
</nav>
<!-- HERO -->
<section class="hero">
<div class="product-tabs">
<button class="product-tab active">Design</button>
<button class="product-tab">Dev Mode</button>
<button class="product-tab">Prototyping</button>
<button class="product-tab">Slides</button>
</div>
<h1>Design System<br>Inspired by Figma</h1>
<p>Auto-generated design token catalog from DESIGN.md</p>
<div class="hero-buttons">
<button class="btn-hero-primary">Explore Tokens</button>
<button class="btn-hero-glass">View Source</button>
</div>
</section>
<hr class="section-divider">
<!-- COLORS -->
<section class="section" id="colors">
<div class="section-title">01 / COLOR PALETTE</div>
<h2 class="section-heading">Color Palette & Roles</h2>
<div class="color-group">
<h3 class="color-group-title">Primary</h3>
<div class="color-grid">
<div class="color-swatch">
<div class="color-swatch-block" style="background: #000000; border-bottom: 1px solid rgba(255,255,255,0.12);"></div>
<div class="color-swatch-info">
<div class="color-swatch-name">Pure Black</div>
<div class="color-swatch-hex">#000000</div>
<div class="color-swatch-role">Page background, dark surface. The foundation of the dark theme.</div>
</div>
</div>
<div class="color-swatch">
<div class="color-swatch-block" style="background: #ffffff;"></div>
<div class="color-swatch-info">
<div class="color-swatch-name">Pure White</div>
<div class="color-swatch-hex">#ffffff</div>
<div class="color-swatch-role">All text, solid buttons, primary interface color on dark.</div>
</div>
</div>
</div>
</div>
<div class="color-group">
<h3 class="color-group-title">Surface & Glass</h3>
<div class="color-grid">
<div class="color-swatch">
<div class="color-swatch-block" style="background: rgba(255,255,255,0.16);"></div>
<div class="color-swatch-info">
<div class="color-swatch-name">Glass White</div>
<div class="color-swatch-hex">rgba(255,255,255,0.16)</div>
<div class="color-swatch-role">Frosted glass overlays, secondary buttons on dark surfaces.</div>
</div>
</div>
<div class="color-swatch">
<div class="color-swatch-block" style="background: rgba(255,255,255,0.08);"></div>
<div class="color-swatch-info">
<div class="color-swatch-name">Glass Subtle</div>
<div class="color-swatch-hex">rgba(255,255,255,0.08)</div>
<div class="color-swatch-role">Card surfaces, grouped content areas on dark background.</div>
</div>
</div>
</div>
</div>
<div class="color-group">
<h3 class="color-group-title">Gradient System</h3>
<div class="gradient-preview">
<div class="gradient-block" style="background: linear-gradient(135deg, #0acf83 0%, #a259ff 30%, #f24e1e 50%, #ff7262 65%, #1abcfe 80%, #0acf83 100%);"></div>
</div>
<div class="gradient-label">Hero Gradient -- Electric Green, Purple, Orange, Pink, Cyan</div>
<p style="color: var(--text-muted); font-size: 14px; margin-top: 8px; font-weight: 340;">Color exists only in hero gradients and product showcases. The interface layer remains strictly monochrome.</p>
</div>
</section>
<hr class="section-divider">
<!-- TYPOGRAPHY -->
<section class="section" id="typography">
<div class="section-title">02 / TYPOGRAPHY SCALE</div>
<h2 class="section-heading">Typography Rules</h2>
<div class="weight-spectrum">
<div style="font-family: var(--font-mono); font-size: 12px; color: var(--text-muted); letter-spacing: 0.6px; text-transform: uppercase; margin-bottom: 8px;">Variable Weight Spectrum</div>
<div class="weight-sample">
<span class="weight-sample-text" style="font-weight: 320;">The quick brown fox jumps</span>
<span class="weight-sample-label">Weight 320</span>
</div>
<div class="weight-sample">
<span class="weight-sample-text" style="font-weight: 330;">The quick brown fox jumps</span>
<span class="weight-sample-label">Weight 330</span>
</div>
<div class="weight-sample">
<span class="weight-sample-text" style="font-weight: 340;">The quick brown fox jumps</span>
<span class="weight-sample-label">Weight 340</span>
</div>
<div class="weight-sample">
<span class="weight-sample-text" style="font-weight: 400;">The quick brown fox jumps</span>
<span class="weight-sample-label">Weight 400</span>
</div>
<div class="weight-sample">
<span class="weight-sample-text" style="font-weight: 450;">The quick brown fox jumps</span>
<span class="weight-sample-label">Weight 450</span>
</div>
<div class="weight-sample">
<span class="weight-sample-text" style="font-weight: 480;">The quick brown fox jumps</span>
<span class="weight-sample-label">Weight 480</span>
</div>
<div class="weight-sample">
<span class="weight-sample-text" style="font-weight: 540;">The quick brown fox jumps</span>
<span class="weight-sample-label">Weight 540</span>
</div>
<div class="weight-sample">
<span class="weight-sample-text" style="font-weight: 700;">The quick brown fox jumps</span>
<span class="weight-sample-label">Weight 700</span>
</div>
</div>
<div class="type-sample">
<div class="type-sample-text" style="font-family: var(--font-sans); font-size: 86px; font-weight: 400; line-height: 1.0; letter-spacing: -1.72px;">Display Hero</div>
<div class="type-sample-label">Display / Hero -- 86px / wt 400 / lh 1.00 / ls -1.72px -- figmaSans</div>
</div>
<div class="type-sample">
<div class="type-sample-text" style="font-family: var(--font-sans); font-size: 64px; font-weight: 400; line-height: 1.1; letter-spacing: -0.96px;">Section Heading</div>
<div class="type-sample-label">Section Heading -- 64px / wt 400 / lh 1.10 / ls -0.96px -- figmaSans</div>
</div>
<div class="type-sample">
<div class="type-sample-text" style="font-family: var(--font-sans); font-size: 26px; font-weight: 540; line-height: 1.35; letter-spacing: -0.26px;">Sub-heading Medium</div>
<div class="type-sample-label">Sub-heading -- 26px / wt 540 / lh 1.35 / ls -0.26px -- figmaSans</div>
</div>
<div class="type-sample">
<div class="type-sample-text" style="font-family: var(--font-sans); font-size: 26px; font-weight: 340; line-height: 1.35; letter-spacing: -0.26px;">Sub-heading Light</div>
<div class="type-sample-label">Sub-heading Light -- 26px / wt 340 / lh 1.35 / ls -0.26px -- figmaSans</div>
</div>
<div class="type-sample">
<div class="type-sample-text" style="font-family: var(--font-sans); font-size: 24px; font-weight: 700; line-height: 1.45; letter-spacing: normal;">Feature Title Bold</div>
<div class="type-sample-label">Feature Title -- 24px / wt 700 / lh 1.45 / ls normal -- figmaSans</div>
</div>
<div class="type-sample">
<div class="type-sample-text" style="font-family: var(--font-sans); font-size: 20px; font-weight: 330; line-height: 1.4; letter-spacing: -0.14px;">Body large text for descriptions and introductions. The light weight creates an airy, ethereal reading experience that matches the design-tool aesthetic.</div>
<div class="type-sample-label">Body Large -- 20px / wt 330 / lh 1.40 / ls -0.14px -- figmaSans</div>
</div>
<div class="type-sample">
<div class="type-sample-text" style="font-family: var(--font-sans); font-size: 18px; font-weight: 320; line-height: 1.45; letter-spacing: -0.26px;">Body light text at the lightest variable weight. Nearly imperceptible thinness for secondary content and delicate UI copy.</div>
<div class="type-sample-label">Body Light -- 18px / wt 320 / lh 1.45 / ls -0.26px -- figmaSans</div>
</div>
<div class="type-sample">
<div class="type-sample-text" style="font-family: var(--font-sans); font-size: 16px; font-weight: 400; line-height: 1.45; letter-spacing: -0.14px;">Standard body text for paragraphs, navigation links, and button labels. The default reading weight for all UI copy.</div>
<div class="type-sample-label">Body / Button -- 16px / wt 400 / lh 1.45 / ls -0.14px -- figmaSans</div>
</div>
<div class="type-sample">
<div class="type-sample-text" style="font-family: var(--font-mono); font-size: 18px; font-weight: 400; line-height: 1.3; letter-spacing: 0.54px; text-transform: uppercase;">MONO SECTION LABEL</div>
<div class="type-sample-label">Mono Label -- 18px / wt 400 / lh 1.30 / ls 0.54px / uppercase -- figmaMono</div>
</div>
<div class="type-sample">
<div class="type-sample-text" style="font-family: var(--font-mono); font-size: 12px; font-weight: 400; line-height: 1.0; letter-spacing: 0.6px; text-transform: uppercase;">MONO SMALL TAG</div>
<div class="type-sample-label">Mono Small -- 12px / wt 400 / lh 1.00 / ls 0.6px / uppercase -- figmaMono</div>
</div>
</section>
<hr class="section-divider">
<!-- BUTTONS -->
<section class="section" id="buttons">
<div class="section-title">03 / BUTTON VARIANTS</div>
<h2 class="section-heading">Buttons</h2>
<div class="button-row">
<div class="button-demo">
<button class="btn-white-pill-solid">Get started</button>
<div class="button-demo-label">White Pill CTA</div>
</div>
<div class="button-demo">
<button class="btn-dark-pill-outline">Learn more</button>
<div class="button-demo-label">Outline Pill</div>
</div>
<div class="button-demo">
<button class="btn-white-circle" aria-label="Menu">&#9776;</button>
<div class="button-demo-label">White Circle</div>
</div>
<div class="button-demo">
<button class="btn-glass-light-circle" aria-label="Play">&#9654;</button>
<div class="button-demo-label">Glass Light</div>
</div>
<div class="button-demo">
<button class="btn-glass-dark-circle" aria-label="Close">&times;</button>
<div class="button-demo-label">Glass Dark</div>
</div>
</div>
<div class="focus-demo-row">
<div>
<div class="focus-demo-label">Dashed Focus Indicator</div>
<p style="font-size: 14px; color: var(--text-muted); font-weight: 340; margin-bottom: 16px;">All interactive elements use <code style="font-family: var(--font-mono); font-size: 12px; background: var(--glass-surface); padding: 2px 6px; border-radius: 4px;">dashed 2px</code> outline on focus, echoing the selection handles in the Figma editor.</p>
</div>
<div style="display: flex; gap: 16px; align-items: center;">
<button class="btn-white-pill-solid btn-focus-visible">Focused Pill</button>
<button class="btn-white-circle btn-focus-visible" aria-label="Focused">&#10003;</button>
</div>
</div>
</section>
<hr class="section-divider">
<!-- CARDS -->
<section class="section" id="cards">
<div class="section-title">04 / CARD EXAMPLES</div>
<h2 class="section-heading">Cards & Containers</h2>
<div class="card-grid">
<div class="card card-standard">
<div class="card-label">STANDARD CARD</div>
<h3>Border Contained</h3>
<p>Standard content card with subtle white-alpha border and 8px radius. The default container on the black surface.</p>
</div>
<div class="card card-elevated">
<div class="card-label">ELEVATED CARD</div>
<h3>Subtle Glow</h3>
<p>Floating card with subtle luminous shadow. Used for product showcases and hover states that lift off the dark surface.</p>
</div>
<div class="card card-glass">
<div class="card-label">GLASS SURFACE</div>
<h3>Glass Overlay</h3>
<p>Glass-effect card using rgba(255,255,255,0.08) background. Secondary containers with a translucent frosted feel.</p>
</div>
</div>
</section>
<hr class="section-divider">
<!-- SPACING -->
<section class="section" id="spacing">
<div class="section-title">05 / SPACING SCALE</div>
<h2 class="section-heading">Spacing System</h2>
<p style="color: var(--text-muted); margin-bottom: 32px; font-weight: 340; font-size: 18px; letter-spacing: -0.26px;">Base unit: 8px. Scale from 1px to 50px.</p>
<div class="spacing-row">
<div class="spacing-item"><div class="spacing-box" style="width: 4px; height: 4px;"></div><div class="spacing-label">1px</div></div>
<div class="spacing-item"><div class="spacing-box" style="width: 8px; height: 8px;"></div><div class="spacing-label">2px</div></div>
<div class="spacing-item"><div class="spacing-box" style="width: 16px; height: 16px;"></div><div class="spacing-label">4px</div></div>
<div class="spacing-item"><div class="spacing-box" style="width: 32px; height: 32px;"></div><div class="spacing-label">8px</div></div>
<div class="spacing-item"><div class="spacing-box" style="width: 40px; height: 40px;"></div><div class="spacing-label">10px</div></div>
<div class="spacing-item"><div class="spacing-box" style="width: 48px; height: 48px;"></div><div class="spacing-label">12px</div></div>
<div class="spacing-item"><div class="spacing-box" style="width: 64px; height: 64px;"></div><div class="spacing-label">16px</div></div>
<div class="spacing-item"><div class="spacing-box" style="width: 72px; height: 72px;"></div><div class="spacing-label">18px</div></div>
<div class="spacing-item"><div class="spacing-box" style="width: 96px; height: 96px;"></div><div class="spacing-label">24px</div></div>
<div class="spacing-item"><div class="spacing-box" style="width: 128px; height: 128px;"></div><div class="spacing-label">32px</div></div>
<div class="spacing-item"><div class="spacing-box" style="width: 160px; height: 160px;"></div><div class="spacing-label">40px</div></div>
<div class="spacing-item"><div class="spacing-box" style="width: 192px; height: 192px;"></div><div class="spacing-label">48px</div></div>
<div class="spacing-item"><div class="spacing-box" style="width: 200px; height: 200px;"></div><div class="spacing-label">50px</div></div>
</div>
</section>
<hr class="section-divider">
<!-- BORDER RADIUS -->
<section class="section">
<div class="section-title">06 / BORDER RADIUS SCALE</div>
<h2 class="section-heading">Border Radius</h2>
<div class="radius-row">
<div class="radius-item"><div class="radius-box" style="border-radius: 2px;"></div><div class="radius-label">2px</div><div class="radius-context">Small links</div></div>
<div class="radius-item"><div class="radius-box" style="border-radius: 6px;"></div><div class="radius-label">6px</div><div class="radius-context">Small containers</div></div>
<div class="radius-item"><div class="radius-box" style="border-radius: 8px;"></div><div class="radius-label">8px</div><div class="radius-context">Cards, images</div></div>
<div class="radius-item"><div class="radius-box" style="border-radius: 50px; width: 120px;"></div><div class="radius-label">50px</div><div class="radius-context">Pill buttons</div></div>
<div class="radius-item"><div class="radius-box" style="border-radius: 50%;"></div><div class="radius-label">50%</div><div class="radius-context">Circle / Icon</div></div>
</div>
</section>
<hr class="section-divider">
<!-- ELEVATION -->
<section class="section" id="elevation">
<div class="section-title">07 / ELEVATION & DEPTH</div>
<h2 class="section-heading">Depth & Elevation</h2>
<div class="elevation-grid">
<div class="elevation-card elevation-flat">
<div><div class="elevation-name">Flat</div><div class="elevation-desc">No shadow. Pure black page background and most text. The default dark surface.</div></div>
<div class="elevation-level">Level 0</div>
</div>
<div class="elevation-card elevation-surface">
<div><div class="elevation-name">Surface</div><div class="elevation-desc">Glass white card on black background. Primary depth through translucent layering.</div></div>
<div class="elevation-level">Level 1</div>
</div>
<div class="elevation-card elevation-elevated">
<div><div class="elevation-name">Elevated</div><div class="elevation-desc">Subtle luminous shadow for floating cards and hover states on dark surfaces.</div></div>
<div class="elevation-level">Level 2</div>
</div>
</div>
</section>
<div style="height: 80px;"></div>
</body>
</html>

View File

@ -1,832 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Design System Inspired by Figma</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&display=swap" rel="stylesheet">
<style>
:root {
--color-black: #000000;
--color-white: #ffffff;
--glass-dark: rgba(0, 0, 0, 0.08);
--glass-light: rgba(255, 255, 255, 0.16);
--font-sans: system-ui, -apple-system, 'Segoe UI', 'SF Pro Display', Helvetica, Arial, sans-serif;
--font-mono: 'Space Mono', 'SF Mono', Menlo, monospace;
/* Light mode tokens */
--bg-page: #ffffff;
--bg-card: #ffffff;
--bg-nav: rgba(255, 255, 255, 0.92);
--text-primary: #000000;
--text-secondary: #000000;
--text-muted: rgba(0, 0, 0, 0.5);
--border-color: rgba(0, 0, 0, 0.12);
--border-subtle: rgba(0, 0, 0, 0.06);
--section-label-color: #000000;
--glass-surface: rgba(0, 0, 0, 0.08);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: var(--bg-page);
color: var(--text-primary);
font-family: var(--font-sans);
font-size: 16px;
font-weight: 340;
line-height: 1.45;
letter-spacing: -0.14px;
font-feature-settings: "kern" 1;
-webkit-font-smoothing: antialiased;
}
/* NAV */
.nav {
position: sticky;
top: 0;
z-index: 100;
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 40px;
background: var(--bg-nav);
backdrop-filter: blur(16px);
border-bottom: 1px solid var(--border-color);
}
.nav-brand {
font-family: var(--font-sans);
font-size: 18px;
font-weight: 700;
letter-spacing: -0.26px;
display: flex;
align-items: center;
gap: 10px;
}
.nav-brand svg { width: 24px; height: 24px; }
.nav-links { display: flex; gap: 32px; align-items: center; }
.nav-links a {
color: var(--text-primary);
text-decoration: none;
font-size: 16px;
font-weight: 400;
letter-spacing: -0.14px;
transition: opacity 0.2s;
}
.nav-links a:hover { opacity: 0.6; }
.nav-cta {
background: var(--color-black);
color: var(--color-white);
padding: 8px 18px 10px;
border: none;
border-radius: 50px;
font-size: 16px;
font-family: var(--font-sans);
font-weight: 480;
letter-spacing: -0.14px;
cursor: pointer;
}
.nav-cta:focus { outline: 2px dashed var(--color-black); outline-offset: 3px; }
/* HERO */
.hero {
position: relative;
text-align: center;
padding: 120px 40px 100px;
overflow: hidden;
background: linear-gradient(135deg, #0acf83 0%, #a259ff 30%, #f24e1e 50%, #ff7262 65%, #1abcfe 80%, #0acf83 100%);
}
.hero h1 {
font-family: var(--font-sans);
font-size: 86px;
font-weight: 400;
line-height: 1.0;
letter-spacing: -1.72px;
margin-bottom: 24px;
color: var(--color-white);
position: relative;
}
.hero p {
color: rgba(255, 255, 255, 0.85);
font-size: 20px;
font-weight: 330;
line-height: 1.4;
letter-spacing: -0.14px;
margin-bottom: 40px;
position: relative;
}
.hero-buttons { display: flex; gap: 16px; justify-content: center; position: relative; }
.btn-hero-primary {
background: var(--color-white);
color: var(--color-black);
padding: 8px 18px 10px;
border: none;
border-radius: 50px;
font-size: 16px;
font-family: var(--font-sans);
font-weight: 480;
letter-spacing: -0.14px;
cursor: pointer;
}
.btn-hero-primary:focus { outline: 2px dashed var(--color-white); outline-offset: 3px; }
.btn-hero-glass {
background: rgba(255, 255, 255, 0.16);
color: var(--color-white);
padding: 8px 18px 10px;
border: none;
border-radius: 50px;
font-size: 16px;
font-family: var(--font-sans);
font-weight: 480;
letter-spacing: -0.14px;
cursor: pointer;
backdrop-filter: blur(8px);
}
.btn-hero-glass:focus { outline: 2px dashed var(--color-white); outline-offset: 3px; }
/* PRODUCT TABS */
.product-tabs {
display: flex;
gap: 8px;
justify-content: center;
margin-bottom: 40px;
position: relative;
}
.product-tab {
background: rgba(255, 255, 255, 0.16);
color: var(--color-white);
padding: 8px 18px;
border: none;
border-radius: 50px;
font-size: 16px;
font-family: var(--font-sans);
font-weight: 480;
letter-spacing: -0.14px;
cursor: pointer;
backdrop-filter: blur(8px);
transition: background 0.2s;
}
.product-tab.active {
background: var(--color-white);
color: var(--color-black);
}
/* SECTIONS */
.section {
max-width: 1200px;
margin: 0 auto;
padding: 80px 40px;
}
.section-title {
font-family: var(--font-mono);
font-size: 18px;
font-weight: 400;
text-transform: uppercase;
letter-spacing: 0.54px;
color: var(--text-muted);
margin-bottom: 12px;
line-height: 1.3;
}
.section-heading {
font-family: var(--font-sans);
font-size: 64px;
font-weight: 400;
line-height: 1.1;
letter-spacing: -0.96px;
margin-bottom: 48px;
}
.section-divider {
border: none;
border-top: 1px solid var(--border-subtle);
margin: 0 40px;
max-width: 1200px;
margin-left: auto;
margin-right: auto;
}
/* COLOR PALETTE */
.color-group { margin-bottom: 40px; }
.color-group-title {
font-size: 26px;
font-weight: 540;
line-height: 1.35;
letter-spacing: -0.26px;
margin-bottom: 20px;
}
.color-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 16px;
}
.color-swatch {
border: 1px solid var(--border-color);
border-radius: 8px;
overflow: hidden;
}
.color-swatch-block {
height: 80px;
position: relative;
}
.color-swatch-info {
padding: 12px;
background: var(--bg-card);
}
.color-swatch-name { font-size: 14px; font-weight: 700; letter-spacing: -0.14px; margin-bottom: 2px; }
.color-swatch-hex { font-family: var(--font-mono); font-size: 12px; color: var(--text-muted); margin-bottom: 4px; letter-spacing: 0.6px; }
.color-swatch-role { font-size: 12px; color: var(--text-muted); line-height: 1.4; font-weight: 340; }
/* GRADIENT PREVIEW */
.gradient-preview {
border-radius: 8px;
overflow: hidden;
margin-bottom: 16px;
}
.gradient-block {
height: 120px;
border-radius: 8px;
}
.gradient-label {
font-family: var(--font-mono);
font-size: 12px;
color: var(--text-muted);
margin-top: 8px;
letter-spacing: 0.6px;
text-transform: uppercase;
}
/* TYPOGRAPHY */
.type-sample { margin-bottom: 32px; padding-bottom: 32px; border-bottom: 1px solid var(--border-subtle); }
.type-sample:last-child { border-bottom: none; }
.type-sample-text { margin-bottom: 8px; font-feature-settings: "kern" 1; }
.type-sample-label {
font-family: var(--font-mono);
font-size: 12px;
color: var(--text-muted);
letter-spacing: 0.6px;
}
/* WEIGHT SPECTRUM */
.weight-spectrum { display: flex; flex-direction: column; gap: 12px; margin-bottom: 40px; padding: 24px; background: var(--glass-surface); border-radius: 8px; }
.weight-sample { display: flex; align-items: baseline; gap: 16px; }
.weight-sample-text { font-family: var(--font-sans); font-size: 24px; letter-spacing: -0.26px; line-height: 1.35; flex: 1; }
.weight-sample-label { font-family: var(--font-mono); font-size: 11px; color: var(--text-muted); letter-spacing: 0.6px; text-transform: uppercase; min-width: 80px; text-align: right; }
/* BUTTONS */
.button-row {
display: flex;
flex-wrap: wrap;
gap: 24px;
align-items: flex-start;
}
.button-demo { text-align: center; }
.button-demo-label {
font-family: var(--font-mono);
font-size: 11px;
color: var(--text-muted);
margin-top: 12px;
text-transform: uppercase;
letter-spacing: 0.6px;
}
.btn-black-pill {
background: var(--color-black);
color: var(--color-white);
padding: 8px 18px 10px;
border: none;
border-radius: 50px;
font-size: 16px;
font-family: var(--font-sans);
font-weight: 480;
letter-spacing: -0.14px;
cursor: pointer;
}
.btn-black-pill:focus { outline: 2px dashed var(--color-black); outline-offset: 3px; }
.btn-white-pill {
background: var(--color-white);
color: var(--color-black);
padding: 8px 18px 10px;
border: 1px solid var(--border-color);
border-radius: 50px;
font-size: 16px;
font-family: var(--font-sans);
font-weight: 480;
letter-spacing: -0.14px;
cursor: pointer;
}
.btn-white-pill:focus { outline: 2px dashed var(--color-black); outline-offset: 3px; }
.btn-black-circle {
background: var(--color-black);
color: var(--color-white);
width: 48px;
height: 48px;
border: none;
border-radius: 50%;
font-size: 20px;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
}
.btn-black-circle:focus { outline: 2px dashed var(--color-black); outline-offset: 3px; }
.btn-glass-dark {
background: var(--glass-dark);
color: var(--color-black);
width: 48px;
height: 48px;
border: none;
border-radius: 50%;
font-size: 20px;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
}
.btn-glass-dark:focus { outline: 2px dashed var(--color-black); outline-offset: 3px; }
.btn-glass-light-demo {
background: rgba(255, 255, 255, 0.16);
color: var(--color-white);
width: 48px;
height: 48px;
border: none;
border-radius: 50%;
font-size: 20px;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
}
.glass-light-wrapper {
background: var(--color-black);
border-radius: 50%;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
}
/* FOCUS DEMO */
.focus-demo-row {
display: flex;
flex-wrap: wrap;
gap: 24px;
align-items: center;
margin-top: 40px;
padding: 24px;
background: var(--glass-surface);
border-radius: 8px;
}
.focus-demo-label {
font-family: var(--font-mono);
font-size: 12px;
color: var(--text-muted);
letter-spacing: 0.6px;
text-transform: uppercase;
margin-bottom: 8px;
}
.btn-focus-visible {
outline: 2px dashed var(--color-black);
outline-offset: 3px;
}
/* CARDS */
.card-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 24px; }
.card {
background: var(--bg-card);
border-radius: 8px;
padding: 28px;
}
.card-standard { border: 1px solid var(--border-color); }
.card-elevated {
border: none;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08);
}
.card-glass {
background: var(--glass-surface);
border: none;
}
.card h3 {
font-family: var(--font-sans);
font-size: 24px;
font-weight: 700;
line-height: 1.45;
letter-spacing: normal;
margin-bottom: 12px;
}
.card p { color: var(--text-muted); font-size: 16px; line-height: 1.45; font-weight: 330; letter-spacing: -0.14px; }
.card-label {
font-family: var(--font-mono);
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.6px;
color: var(--text-muted);
margin-bottom: 16px;
}
/* SPACING */
.spacing-row { display: flex; flex-wrap: wrap; gap: 12px; align-items: flex-end; }
.spacing-item { text-align: center; }
.spacing-box {
background: rgba(0, 0, 0, 0.06);
border: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 2px;
margin-bottom: 8px;
}
.spacing-label {
font-family: var(--font-mono);
font-size: 11px;
color: var(--text-muted);
letter-spacing: 0.6px;
}
/* RADIUS */
.radius-row { display: flex; flex-wrap: wrap; gap: 24px; align-items: center; }
.radius-item { text-align: center; }
.radius-box {
width: 80px;
height: 80px;
background: var(--glass-surface);
border: 1px solid var(--border-color);
margin-bottom: 8px;
}
.radius-label {
font-family: var(--font-mono);
font-size: 11px;
color: var(--text-muted);
letter-spacing: 0.6px;
}
.radius-context {
font-size: 11px;
color: var(--text-muted);
margin-top: 2px;
font-weight: 340;
}
/* ELEVATION */
.elevation-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 24px; }
.elevation-card {
background: var(--bg-card);
border-radius: 8px;
padding: 24px;
min-height: 140px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.elevation-flat { border: none; background: var(--bg-page); }
.elevation-surface { background: var(--bg-card); border: 1px solid var(--border-color); }
.elevation-elevated { border: none; box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08); }
.elevation-name { font-size: 16px; font-weight: 700; margin-bottom: 8px; letter-spacing: normal; }
.elevation-desc { font-size: 13px; color: var(--text-muted); line-height: 1.5; font-weight: 340; }
.elevation-level {
font-family: var(--font-mono);
font-size: 11px;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.6px;
margin-top: 12px;
}
/* RESPONSIVE */
@media (max-width: 768px) {
.nav { padding: 12px 20px; }
.nav-links a:not(.nav-cta-wrapper) { display: none; }
.hero { padding: 80px 20px 60px; }
.hero h1 { font-size: 48px; letter-spacing: -0.96px; }
.section { padding: 60px 20px; }
.section-heading { font-size: 36px; letter-spacing: -0.72px; }
.color-grid { grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); }
.card-grid { grid-template-columns: 1fr; }
.hero-buttons { flex-direction: column; align-items: center; }
.button-row { flex-direction: column; align-items: flex-start; }
.product-tabs { flex-wrap: wrap; }
}
</style>
</head>
<body>
<!-- NAV -->
<nav class="nav">
<span class="nav-brand">awesome-design-md</span>
<div class="nav-links">
<a href="#colors">Colors</a>
<a href="#typography">Typography</a>
<a href="#buttons">Buttons</a>
<a href="#cards">Cards</a>
<a href="#spacing">Spacing</a>
<a href="#elevation">Elevation</a>
<button class="nav-cta">Get started</button>
</div>
</nav>
<!-- HERO -->
<section class="hero">
<div class="product-tabs">
<button class="product-tab active">Design</button>
<button class="product-tab">Dev Mode</button>
<button class="product-tab">Prototyping</button>
<button class="product-tab">Slides</button>
</div>
<h1>Design System<br>Inspired by Figma</h1>
<p>Auto-generated design token catalog from DESIGN.md</p>
<div class="hero-buttons">
<button class="btn-hero-primary">Explore Tokens</button>
<button class="btn-hero-glass">View Source</button>
</div>
</section>
<hr class="section-divider">
<!-- COLORS -->
<section class="section" id="colors">
<div class="section-title">01 / COLOR PALETTE</div>
<h2 class="section-heading">Color Palette & Roles</h2>
<div class="color-group">
<h3 class="color-group-title">Primary</h3>
<div class="color-grid">
<div class="color-swatch">
<div class="color-swatch-block" style="background: #000000;"></div>
<div class="color-swatch-info">
<div class="color-swatch-name">Pure Black</div>
<div class="color-swatch-hex">#000000</div>
<div class="color-swatch-role">All text, solid buttons, all borders. The sole interface color.</div>
</div>
</div>
<div class="color-swatch">
<div class="color-swatch-block" style="background: #ffffff; border: 1px solid rgba(0,0,0,0.12);"></div>
<div class="color-swatch-info">
<div class="color-swatch-name">Pure White</div>
<div class="color-swatch-hex">#ffffff</div>
<div class="color-swatch-role">All backgrounds, white buttons, text on dark surfaces.</div>
</div>
</div>
</div>
</div>
<div class="color-group">
<h3 class="color-group-title">Surface & Glass</h3>
<div class="color-grid">
<div class="color-swatch">
<div class="color-swatch-block" style="background: rgba(0,0,0,0.08); border: 1px solid rgba(0,0,0,0.12);"></div>
<div class="color-swatch-info">
<div class="color-swatch-name">Glass Black</div>
<div class="color-swatch-hex">rgba(0,0,0,0.08)</div>
<div class="color-swatch-role">Secondary circular buttons, glass overlays on light surfaces.</div>
</div>
</div>
<div class="color-swatch">
<div class="color-swatch-block" style="background: rgba(255,255,255,0.16); border: 1px solid rgba(0,0,0,0.12); position: relative;">
<div style="position: absolute; inset: 0; background: #000; z-index: 0;"></div>
<div style="position: absolute; inset: 0; background: rgba(255,255,255,0.16); z-index: 1;"></div>
</div>
<div class="color-swatch-info">
<div class="color-swatch-name">Glass White</div>
<div class="color-swatch-hex">rgba(255,255,255,0.16)</div>
<div class="color-swatch-role">Frosted glass overlay for buttons on dark or colored surfaces.</div>
</div>
</div>
</div>
</div>
<div class="color-group">
<h3 class="color-group-title">Gradient System</h3>
<div class="gradient-preview">
<div class="gradient-block" style="background: linear-gradient(135deg, #0acf83 0%, #a259ff 30%, #f24e1e 50%, #ff7262 65%, #1abcfe 80%, #0acf83 100%);"></div>
</div>
<div class="gradient-label">Hero Gradient -- Electric Green, Purple, Orange, Pink, Cyan</div>
<p style="color: var(--text-muted); font-size: 14px; margin-top: 8px; font-weight: 340;">Color exists only in hero gradients and product showcases. The interface layer remains strictly monochrome.</p>
</div>
</section>
<hr class="section-divider">
<!-- TYPOGRAPHY -->
<section class="section" id="typography">
<div class="section-title">02 / TYPOGRAPHY SCALE</div>
<h2 class="section-heading">Typography Rules</h2>
<div class="weight-spectrum">
<div style="font-family: var(--font-mono); font-size: 12px; color: var(--text-muted); letter-spacing: 0.6px; text-transform: uppercase; margin-bottom: 8px;">Variable Weight Spectrum</div>
<div class="weight-sample">
<span class="weight-sample-text" style="font-weight: 320;">The quick brown fox jumps</span>
<span class="weight-sample-label">Weight 320</span>
</div>
<div class="weight-sample">
<span class="weight-sample-text" style="font-weight: 330;">The quick brown fox jumps</span>
<span class="weight-sample-label">Weight 330</span>
</div>
<div class="weight-sample">
<span class="weight-sample-text" style="font-weight: 340;">The quick brown fox jumps</span>
<span class="weight-sample-label">Weight 340</span>
</div>
<div class="weight-sample">
<span class="weight-sample-text" style="font-weight: 400;">The quick brown fox jumps</span>
<span class="weight-sample-label">Weight 400</span>
</div>
<div class="weight-sample">
<span class="weight-sample-text" style="font-weight: 450;">The quick brown fox jumps</span>
<span class="weight-sample-label">Weight 450</span>
</div>
<div class="weight-sample">
<span class="weight-sample-text" style="font-weight: 480;">The quick brown fox jumps</span>
<span class="weight-sample-label">Weight 480</span>
</div>
<div class="weight-sample">
<span class="weight-sample-text" style="font-weight: 540;">The quick brown fox jumps</span>
<span class="weight-sample-label">Weight 540</span>
</div>
<div class="weight-sample">
<span class="weight-sample-text" style="font-weight: 700;">The quick brown fox jumps</span>
<span class="weight-sample-label">Weight 700</span>
</div>
</div>
<div class="type-sample">
<div class="type-sample-text" style="font-family: var(--font-sans); font-size: 86px; font-weight: 400; line-height: 1.0; letter-spacing: -1.72px;">Display Hero</div>
<div class="type-sample-label">Display / Hero -- 86px / wt 400 / lh 1.00 / ls -1.72px -- figmaSans</div>
</div>
<div class="type-sample">
<div class="type-sample-text" style="font-family: var(--font-sans); font-size: 64px; font-weight: 400; line-height: 1.1; letter-spacing: -0.96px;">Section Heading</div>
<div class="type-sample-label">Section Heading -- 64px / wt 400 / lh 1.10 / ls -0.96px -- figmaSans</div>
</div>
<div class="type-sample">
<div class="type-sample-text" style="font-family: var(--font-sans); font-size: 26px; font-weight: 540; line-height: 1.35; letter-spacing: -0.26px;">Sub-heading Medium</div>
<div class="type-sample-label">Sub-heading -- 26px / wt 540 / lh 1.35 / ls -0.26px -- figmaSans</div>
</div>
<div class="type-sample">
<div class="type-sample-text" style="font-family: var(--font-sans); font-size: 26px; font-weight: 340; line-height: 1.35; letter-spacing: -0.26px;">Sub-heading Light</div>
<div class="type-sample-label">Sub-heading Light -- 26px / wt 340 / lh 1.35 / ls -0.26px -- figmaSans</div>
</div>
<div class="type-sample">
<div class="type-sample-text" style="font-family: var(--font-sans); font-size: 24px; font-weight: 700; line-height: 1.45; letter-spacing: normal;">Feature Title Bold</div>
<div class="type-sample-label">Feature Title -- 24px / wt 700 / lh 1.45 / ls normal -- figmaSans</div>
</div>
<div class="type-sample">
<div class="type-sample-text" style="font-family: var(--font-sans); font-size: 20px; font-weight: 330; line-height: 1.4; letter-spacing: -0.14px;">Body large text for descriptions and introductions. The light weight creates an airy, ethereal reading experience that matches the design-tool aesthetic.</div>
<div class="type-sample-label">Body Large -- 20px / wt 330 / lh 1.40 / ls -0.14px -- figmaSans</div>
</div>
<div class="type-sample">
<div class="type-sample-text" style="font-family: var(--font-sans); font-size: 18px; font-weight: 320; line-height: 1.45; letter-spacing: -0.26px;">Body light text at the lightest variable weight. Nearly imperceptible thinness for secondary content and delicate UI copy.</div>
<div class="type-sample-label">Body Light -- 18px / wt 320 / lh 1.45 / ls -0.26px -- figmaSans</div>
</div>
<div class="type-sample">
<div class="type-sample-text" style="font-family: var(--font-sans); font-size: 16px; font-weight: 400; line-height: 1.45; letter-spacing: -0.14px;">Standard body text for paragraphs, navigation links, and button labels. The default reading weight for all UI copy.</div>
<div class="type-sample-label">Body / Button -- 16px / wt 400 / lh 1.45 / ls -0.14px -- figmaSans</div>
</div>
<div class="type-sample">
<div class="type-sample-text" style="font-family: var(--font-mono); font-size: 18px; font-weight: 400; line-height: 1.3; letter-spacing: 0.54px; text-transform: uppercase;">MONO SECTION LABEL</div>
<div class="type-sample-label">Mono Label -- 18px / wt 400 / lh 1.30 / ls 0.54px / uppercase -- figmaMono</div>
</div>
<div class="type-sample">
<div class="type-sample-text" style="font-family: var(--font-mono); font-size: 12px; font-weight: 400; line-height: 1.0; letter-spacing: 0.6px; text-transform: uppercase;">MONO SMALL TAG</div>
<div class="type-sample-label">Mono Small -- 12px / wt 400 / lh 1.00 / ls 0.6px / uppercase -- figmaMono</div>
</div>
</section>
<hr class="section-divider">
<!-- BUTTONS -->
<section class="section" id="buttons">
<div class="section-title">03 / BUTTON VARIANTS</div>
<h2 class="section-heading">Buttons</h2>
<div class="button-row">
<div class="button-demo">
<button class="btn-black-pill">Get started</button>
<div class="button-demo-label">Black Pill CTA</div>
</div>
<div class="button-demo">
<button class="btn-white-pill">Learn more</button>
<div class="button-demo-label">White Pill</div>
</div>
<div class="button-demo">
<button class="btn-black-circle" aria-label="Menu">&#9776;</button>
<div class="button-demo-label">Black Circle</div>
</div>
<div class="button-demo">
<button class="btn-glass-dark" aria-label="Play">&#9654;</button>
<div class="button-demo-label">Glass Dark</div>
</div>
<div class="button-demo">
<div class="glass-light-wrapper">
<button class="btn-glass-light-demo" aria-label="Close">&times;</button>
</div>
<div class="button-demo-label">Glass Light</div>
</div>
</div>
<div class="focus-demo-row">
<div>
<div class="focus-demo-label">Dashed Focus Indicator</div>
<p style="font-size: 14px; color: var(--text-muted); font-weight: 340; margin-bottom: 16px;">All interactive elements use <code style="font-family: var(--font-mono); font-size: 12px; background: var(--glass-surface); padding: 2px 6px; border-radius: 4px;">dashed 2px</code> outline on focus, echoing the selection handles in the Figma editor.</p>
</div>
<div style="display: flex; gap: 16px; align-items: center;">
<button class="btn-black-pill btn-focus-visible">Focused Pill</button>
<button class="btn-black-circle btn-focus-visible" aria-label="Focused">&#10003;</button>
</div>
</div>
</section>
<hr class="section-divider">
<!-- CARDS -->
<section class="section" id="cards">
<div class="section-title">04 / CARD EXAMPLES</div>
<h2 class="section-heading">Cards & Containers</h2>
<div class="card-grid">
<div class="card card-standard">
<div class="card-label">STANDARD CARD</div>
<h3>Minimal Border</h3>
<p>Standard content card with subtle border and 8px radius. The default container for features and content sections on the white gallery surface.</p>
</div>
<div class="card card-elevated">
<div class="card-label">ELEVATED CARD</div>
<h3>Subtle Shadow</h3>
<p>Floating card with subtle shadow elevation. Used for product showcases and hover states where the card lifts off the surface.</p>
</div>
<div class="card card-glass">
<div class="card-label">GLASS SURFACE</div>
<h3>Glass Overlay</h3>
<p>Glass-effect card using rgba(0,0,0,0.08) background. Secondary containers and grouped content areas with a translucent feel.</p>
</div>
</div>
</section>
<hr class="section-divider">
<!-- SPACING -->
<section class="section" id="spacing">
<div class="section-title">05 / SPACING SCALE</div>
<h2 class="section-heading">Spacing System</h2>
<p style="color: var(--text-muted); margin-bottom: 32px; font-weight: 340; font-size: 18px; letter-spacing: -0.26px;">Base unit: 8px. Scale from 1px to 50px.</p>
<div class="spacing-row">
<div class="spacing-item"><div class="spacing-box" style="width: 4px; height: 4px;"></div><div class="spacing-label">1px</div></div>
<div class="spacing-item"><div class="spacing-box" style="width: 8px; height: 8px;"></div><div class="spacing-label">2px</div></div>
<div class="spacing-item"><div class="spacing-box" style="width: 16px; height: 16px;"></div><div class="spacing-label">4px</div></div>
<div class="spacing-item"><div class="spacing-box" style="width: 32px; height: 32px;"></div><div class="spacing-label">8px</div></div>
<div class="spacing-item"><div class="spacing-box" style="width: 40px; height: 40px;"></div><div class="spacing-label">10px</div></div>
<div class="spacing-item"><div class="spacing-box" style="width: 48px; height: 48px;"></div><div class="spacing-label">12px</div></div>
<div class="spacing-item"><div class="spacing-box" style="width: 64px; height: 64px;"></div><div class="spacing-label">16px</div></div>
<div class="spacing-item"><div class="spacing-box" style="width: 72px; height: 72px;"></div><div class="spacing-label">18px</div></div>
<div class="spacing-item"><div class="spacing-box" style="width: 96px; height: 96px;"></div><div class="spacing-label">24px</div></div>
<div class="spacing-item"><div class="spacing-box" style="width: 128px; height: 128px;"></div><div class="spacing-label">32px</div></div>
<div class="spacing-item"><div class="spacing-box" style="width: 160px; height: 160px;"></div><div class="spacing-label">40px</div></div>
<div class="spacing-item"><div class="spacing-box" style="width: 192px; height: 192px;"></div><div class="spacing-label">48px</div></div>
<div class="spacing-item"><div class="spacing-box" style="width: 200px; height: 200px;"></div><div class="spacing-label">50px</div></div>
</div>
</section>
<hr class="section-divider">
<!-- BORDER RADIUS -->
<section class="section">
<div class="section-title">06 / BORDER RADIUS SCALE</div>
<h2 class="section-heading">Border Radius</h2>
<div class="radius-row">
<div class="radius-item"><div class="radius-box" style="border-radius: 2px;"></div><div class="radius-label">2px</div><div class="radius-context">Small links</div></div>
<div class="radius-item"><div class="radius-box" style="border-radius: 6px;"></div><div class="radius-label">6px</div><div class="radius-context">Small containers</div></div>
<div class="radius-item"><div class="radius-box" style="border-radius: 8px;"></div><div class="radius-label">8px</div><div class="radius-context">Cards, images</div></div>
<div class="radius-item"><div class="radius-box" style="border-radius: 50px; width: 120px;"></div><div class="radius-label">50px</div><div class="radius-context">Pill buttons</div></div>
<div class="radius-item"><div class="radius-box" style="border-radius: 50%;"></div><div class="radius-label">50%</div><div class="radius-context">Circle / Icon</div></div>
</div>
</section>
<hr class="section-divider">
<!-- ELEVATION -->
<section class="section" id="elevation">
<div class="section-title">07 / ELEVATION & DEPTH</div>
<h2 class="section-heading">Depth & Elevation</h2>
<div class="elevation-grid">
<div class="elevation-card elevation-flat">
<div><div class="elevation-name">Flat</div><div class="elevation-desc">No shadow. Page background and most text. The default surface.</div></div>
<div class="elevation-level">Level 0</div>
</div>
<div class="elevation-card elevation-surface">
<div><div class="elevation-name">Surface</div><div class="elevation-desc">White card on gradient or dark section. Primary depth through background contrast.</div></div>
<div class="elevation-level">Level 1</div>
</div>
<div class="elevation-card elevation-elevated">
<div><div class="elevation-name">Elevated</div><div class="elevation-desc">Subtle shadow for floating cards and hover states. Sparingly applied.</div></div>
<div class="elevation-level">Level 2</div>
</div>
</div>
</section>
<div style="height: 80px;"></div>
</body>
</html>

View File

@ -1,326 +0,0 @@
# Design System Inspiration of Mintlify
## 1. Visual Theme & Atmosphere
Mintlify's website is a study in documentation-as-product design — a white, airy, information-rich surface that treats clarity as its highest aesthetic value. The page opens with a luminous white (`#ffffff`) background, near-black (`#0d0d0d`) text, and a signature green brand accent (`#18E299`) that signals freshness and intelligence without dominating the palette. The overall mood is calm, confident, and engineered for legibility — a design system that whispers "we care about your developer experience" in every pixel.
The Inter font family carries the entire typographic load. At display sizes (4064px), it uses tight negative letter-spacing (-0.8px to -1.28px) and semibold weight (600), creating headlines that feel focused and compressed like well-written documentation headers. Body text at 1618px with 150% line-height provides generous reading comfort. Geist Mono appears exclusively for code and technical labels — uppercase, tracked-out, small — the voice of the terminal inside the marketing page.
What distinguishes Mintlify from other documentation platforms is its atmospheric gradient hero. A soft, cloud-like green-to-white gradient wash behind the hero content creates a sense of ethereal intelligence — documentation that floats above the noise. Below the hero, the page settles into a disciplined alternation of white sections separated by subtle 5% opacity borders. Cards use generous padding (24px+) with large radii (16px24px) and whisper-thin borders, creating containers that feel open rather than boxed.
**Key Characteristics:**
- Inter with tight negative tracking at display sizes (-0.8px to -1.28px) — compressed yet readable
- Geist Mono for code labels: uppercase, 12px, tracked-out, the terminal voice
- Brand green (`#18E299`) used sparingly — CTAs, hover states, focus rings, and accent touches
- Atmospheric gradient hero with cloud-like green-white wash
- Ultra-round corners: 16px for containers, 24px for featured cards, full-round (9999px) for buttons and pills
- Subtle 5% opacity borders (`rgba(0,0,0,0.05)`) creating barely-there separation
- 8px base spacing system with generous section padding (48px96px)
- Clean white canvas — no gray backgrounds, no color sections, depth through borders and whitespace alone
## 2. Color Palette & Roles
### Primary
- **Near Black** (`#0d0d0d`): Primary text, headings, dark surfaces. Not pure black — the micro-softness improves reading comfort.
- **Pure White** (`#ffffff`): Page background, card surfaces, input backgrounds.
- **Brand Green** (`#18E299`): The signature accent — CTAs, links on hover, focus rings, brand identity.
### Secondary Accents
- **Brand Green Light** (`#d4fae8`): Tinted green surface for badges, hover states, subtle backgrounds.
- **Brand Green Deep** (`#0fa76e`): Darker green for text on light-green badges, hover states on brand elements.
- **Warm Amber** (`#c37d0d`): Warning states, caution badges — `--twoslash-warn-bg`.
- **Soft Blue** (`#3772cf`): Tag backgrounds, informational annotations — `--twoslash-tag-bg`.
- **Error Red** (`#d45656`): Error states, destructive actions — `--twoslash-error-bg`.
### Neutral Scale
- **Gray 900** (`#0d0d0d`): Primary heading text, nav links.
- **Gray 700** (`#333333`): Secondary text, descriptions, body copy.
- **Gray 500** (`#666666`): Tertiary text, muted labels.
- **Gray 400** (`#888888`): Placeholder text, disabled states, code annotations.
- **Gray 200** (`#e5e5e5`): Borders, dividers, card outlines.
- **Gray 100** (`#f5f5f5`): Subtle surface backgrounds, hover states.
- **Gray 50** (`#fafafa`): Near-white surface tint.
### Interactive
- **Link Default** (`#0d0d0d`): Links match text color, relying on underline/context.
- **Link Hover** (`#18E299`): Brand green on hover — `var(--color-brand)`.
- **Focus Ring** (`#18E299`): Brand green focus outline for inputs and interactive elements.
### Surface & Overlay
- **Card Background** (`#ffffff`): White cards on white background, separated by borders.
- **Border Subtle** (`rgba(0,0,0,0.05)`): 5% black opacity borders — the primary separation mechanism.
- **Border Medium** (`rgba(0,0,0,0.08)`): Slightly stronger borders for interactive elements.
- **Input Border Focus** (`var(--color-brand)`): Green ring on focused inputs.
### Shadows & Depth
- **Card Shadow** (`rgba(0,0,0,0.03) 0px 2px 4px`): Barely-there ambient shadow for subtle lift.
- **Button Shadow** (`rgba(0,0,0,0.06) 0px 1px 2px`): Micro-shadow for button depth.
- **No heavy shadows**: Mintlify relies on borders, not shadows, for depth.
## 3. Typography Rules
### Font Family
- **Primary**: `Inter`, with fallback: `Inter Fallback, system-ui, -apple-system, sans-serif`
- **Monospace**: `Geist Mono`, with fallback: `Geist Mono Fallback, ui-monospace, SFMono-Regular, monospace`
### Hierarchy
| Role | Font | Size | Weight | Line Height | Letter Spacing | Notes |
|------|------|------|--------|-------------|----------------|-------|
| Display Hero | Inter | 64px (4.00rem) | 600 | 1.15 (tight) | -1.28px | Maximum impact, hero headlines |
| Section Heading | Inter | 40px (2.50rem) | 600 | 1.10 (tight) | -0.8px | Feature section titles |
| Sub-heading | Inter | 24px (1.50rem) | 500 | 1.30 (tight) | -0.24px | Card headings, sub-sections |
| Card Title | Inter | 20px (1.25rem) | 600 | 1.30 (tight) | -0.2px | Feature card titles |
| Card Title Light | Inter | 20px (1.25rem) | 500 | 1.30 (tight) | -0.2px | Secondary card headings |
| Body Large | Inter | 18px (1.13rem) | 400 | 1.50 | normal | Hero descriptions, introductions |
| Body | Inter | 16px (1.00rem) | 400 | 1.50 | normal | Standard reading text |
| Body Medium | Inter | 16px (1.00rem) | 500 | 1.50 | normal | Navigation, emphasized text |
| Button | Inter | 15px (0.94rem) | 500 | 1.50 | normal | Button labels |
| Link | Inter | 14px (0.88rem) | 500 | 1.50 | normal | Navigation links, small CTAs |
| Caption | Inter | 14px (0.88rem) | 400500 | 1.501.71 | normal | Metadata, descriptions |
| Label Uppercase | Inter | 13px (0.81rem) | 500 | 1.50 | 0.65px | `text-transform: uppercase`, section labels |
| Small | Inter | 13px (0.81rem) | 400500 | 1.50 | -0.26px | Small body text |
| Mono Code | Geist Mono | 12px (0.75rem) | 500 | 1.50 | 0.6px | `text-transform: uppercase`, technical labels |
| Mono Badge | Geist Mono | 12px (0.75rem) | 600 | 1.50 | 0.6px | `text-transform: uppercase`, status badges |
| Mono Micro | Geist Mono | 10px (0.63rem) | 500 | 1.50 | normal | `text-transform: uppercase`, tiny labels |
### Principles
- **Tight tracking at display sizes**: Inter at 4064px uses -0.8px to -1.28px letter-spacing. This compression creates headlines that feel deliberate and space-efficient — documentation headings, not billboard copy.
- **Relaxed reading at body sizes**: 1618px body text uses normal tracking with 150% line-height, creating generous reading lanes. Documentation demands comfort.
- **Two-font system**: Inter for all human-readable content, Geist Mono exclusively for technical/code contexts. The boundary is strict — no mixing.
- **Uppercase as hierarchy signal**: Section labels and technical tags use uppercase + positive tracking (0.6px0.65px) as a clear visual delimiter between content types.
- **Three weights**: 400 (body/reading), 500 (UI/navigation/emphasis), 600 (headings/titles). No bold (700) in the system.
## 4. Component Stylings
### Buttons
**Primary Brand (Full-round)**
- Background: `#0d0d0d` (near-black)
- Text: `#ffffff`
- Padding: 8px 24px
- Radius: 9999px (full pill)
- Font: Inter 15px weight 500
- Shadow: `rgba(0,0,0,0.06) 0px 1px 2px`
- Hover: opacity 0.9
- Use: Primary CTA ("Get Started", "Start Building")
**Secondary / Ghost (Full-round)**
- Background: `#ffffff`
- Text: `#0d0d0d`
- Padding: 4.5px 12px
- Radius: 9999px (full pill)
- Border: `1px solid rgba(0,0,0,0.08)`
- Font: Inter 15px weight 500
- Hover: opacity 0.9
- Use: Secondary actions ("Request Demo", "View Docs")
**Transparent / Nav Button**
- Background: transparent
- Text: `#0d0d0d`
- Padding: 5px 6px
- Radius: 8px
- Border: none or `1px solid rgba(0,0,0,0.05)`
- Use: Navigation items, icon buttons
**Brand Accent Button**
- Background: `#18E299`
- Text: `#0d0d0d`
- Padding: 8px 24px
- Radius: 9999px
- Use: Special promotional CTAs
### Cards & Containers
**Standard Card**
- Background: `#ffffff`
- Border: `1px solid rgba(0,0,0,0.05)`
- Radius: 16px
- Padding: 24px
- Shadow: `rgba(0,0,0,0.03) 0px 2px 4px`
- Hover: subtle border darkening to `rgba(0,0,0,0.08)`
**Featured Card**
- Background: `#ffffff`
- Border: `1px solid rgba(0,0,0,0.05)`
- Radius: 24px
- Padding: 32px
- Inner content areas may have their own 16px radius containers
**Logo/Trust Card**
- Background: `#fafafa` or `#ffffff`
- Border: `1px solid rgba(0,0,0,0.05)`
- Radius: 16px
- Centered logo/icon with consistent sizing
### Inputs & Forms
**Email Input**
- Background: transparent or `#ffffff`
- Text: `#0d0d0d`
- Padding: 0px 12px (height controlled by line-height)
- Border: `1px solid rgba(0,0,0,0.08)`
- Radius: 9999px (full pill, matching buttons)
- Focus: `1px solid var(--color-brand)` + `outline: 1px solid var(--color-brand)`
- Placeholder: `#888888`
### Navigation
- Clean horizontal nav on white, sticky with backdrop blur
- Brand logotype left-aligned
- Links: Inter 1415px weight 500, `#0d0d0d` text
- Hover: color shifts to brand green `var(--color-brand)`
- CTA: dark pill button right-aligned ("Get Started")
- Mobile: hamburger menu collapse at 768px
### Image Treatment
- Product screenshots with subtle 1px borders
- Rounded containers: 16px24px radius
- Atmospheric gradient backgrounds behind hero images
- Cloud/sky imagery with soft green tinting
### Distinctive Components
**Atmospheric Hero**
- Full-width gradient wash: soft green-to-white cloud-like gradient
- Centered headline with tight tracking
- Subtitle in muted gray
- Dual CTA buttons (dark primary + ghost secondary)
- The gradient creates a sense of elevation and intelligence
**Trust Bar / Logo Grid**
- "Loved by your favorite companies" section
- Company logos in muted grayscale
- Grid or horizontal layout with consistent sizing
- Subtle border separation between logos
**Feature Cards with Icons**
- Icon or illustration at top
- Title at 20px weight 600
- Description at 1416px in gray
- Consistent padding and border treatment
- Grid layout: 23 columns on desktop
**CTA Footer Section**
- Dark or gradient background
- Large headline: "Make documentation your winning advantage"
- Email input with pill styling
- Brand green accent on CTAs
## 5. Layout Principles
### Spacing System
- Base unit: 8px
- Scale: 2px, 4px, 5px, 6px, 7px, 8px, 10px, 12px, 16px, 24px, 32px, 48px, 64px
- Section padding: 48px96px vertical
- Card padding: 24px32px
- Component gaps: 8px16px
### Grid & Container
- Max content width: approximately 1200px
- Hero: centered single-column with generous top padding (96px+)
- Feature sections: 23 column CSS Grid for cards
- Full-width sections with contained content
- Consistent horizontal padding: 24px (mobile) to 32px (desktop)
### Whitespace Philosophy
- **Documentation-grade breathing room**: Every element has generous surrounding whitespace. Mintlify sells documentation, so the marketing page itself demonstrates reading comfort.
- **Sections as chapters**: Each feature section is a self-contained unit with 48px96px vertical padding, creating clear "chapter breaks."
- **Content density is low**: Unlike developer tools that pack the page, Mintlify uses 12 key messages per section with supporting imagery.
### Border Radius Scale
- Small (4px): Inline code, small tags, tooltips
- Medium (8px): Nav buttons, transparent buttons, small containers
- Standard (16px): Cards, content containers, image wrappers
- Large (24px): Featured cards, hero containers, section panels
- Full Pill (9999px): Buttons, inputs, badges, pills — the signature shape
## 6. Depth & Elevation
| Level | Treatment | Use |
|-------|-----------|-----|
| Flat (Level 0) | No shadow, no border | Page background, text blocks |
| Subtle Border (Level 1) | `1px solid rgba(0,0,0,0.05)` | Standard card borders, dividers |
| Medium Border (Level 1b) | `1px solid rgba(0,0,0,0.08)` | Interactive elements, input borders |
| Ambient Shadow (Level 2) | `rgba(0,0,0,0.03) 0px 2px 4px` | Cards with subtle lift |
| Button Shadow (Level 2b) | `rgba(0,0,0,0.06) 0px 1px 2px` | Button micro-depth |
| Focus Ring (Accessibility) | `1px solid #18E299` outline | Focused inputs, active interactive elements |
**Shadow Philosophy**: Mintlify barely uses shadows. The depth system is almost entirely border-driven — ultra-subtle 5% opacity borders create separation without visual weight. When shadows appear, they're atmospheric whispers (`0.03 opacity, 2px blur, 4px spread`) that add the barest sense of lift. This restraint keeps the page feeling flat and paper-like — appropriate for a documentation company whose product is about clarity and readability.
### Decorative Depth
- Hero gradient: atmospheric green-white cloud gradient behind hero content
- No background color alternation — white on white throughout
- Depth comes from border opacity variation (5% → 8%) and whitespace
## 7. Dark Mode
### Color Inversions
- **Background**: `#0d0d0d` (near-black)
- **Text Primary**: `#ededed` (near-white)
- **Text Secondary**: `#a0a0a0` (muted gray)
- **Brand Green**: `#18E299` (unchanged — the green works on both backgrounds)
- **Border**: `rgba(255,255,255,0.08)` (white at 8% opacity)
- **Card Background**: `#141414` (slightly lighter than page)
- **Shadow**: `rgba(0,0,0,0.4) 0px 2px 4px` (stronger shadow for contrast)
### Key Adjustments
- Buttons invert: white background dark text becomes dark background light text
- Badge backgrounds shift to deeper tones with lighter text
- Focus ring remains brand green
- Hero gradient shifts to dark-tinted green atmospheric wash
## 8. Responsive Behavior
### Breakpoints
| Name | Width | Key Changes |
|------|-------|-------------|
| Mobile | <768px | Single column, stacked layout, hamburger nav |
| Tablet | 7681024px | Two-column grids begin, expanded padding |
| Desktop | >1024px | Full layout, 3-column grids, maximum content width |
### Touch Targets
- Buttons with full-pill shape have comfortable 8px+ vertical padding
- Navigation links spaced with adequate 16px+ gaps
- Mobile menu provides full-width tap targets
### Collapsing Strategy
- Hero: 64px → 40px headline, maintains tight tracking proportionally
- Navigation: horizontal links + CTA → hamburger menu at 768px
- Feature cards: 3-column → 2-column → single column stacked
- Section spacing: 96px → 48px on mobile
- Footer: multi-column → stacked single column
- Trust bar: grid → horizontal scroll or stacked
### Image Behavior
- Product screenshots maintain aspect ratio with responsive containers
- Hero gradient simplifies on mobile
- Full-width sections maintain edge-to-edge treatment
## 9. Agent Prompt Guide
### Quick Color Reference
- Primary CTA: Near Black (`#0d0d0d`)
- Background: Pure White (`#ffffff`)
- Heading text: Near Black (`#0d0d0d`)
- Body text: Gray 700 (`#333333`)
- Border: `rgba(0,0,0,0.05)` (5% opacity)
- Brand accent: Green (`#18E299`)
- Link hover: Brand Green (`#18E299`)
- Focus ring: Brand Green (`#18E299`)
### Example Component Prompts
- "Create a hero section on white background with atmospheric green-white gradient wash. Headline at 64px Inter weight 600, line-height 1.15, letter-spacing -1.28px, color #0d0d0d. Subtitle at 18px Inter weight 400, line-height 1.50, color #666666. Dark pill CTA (#0d0d0d, 9999px radius, 8px 24px padding) and ghost pill button (white, 1px solid rgba(0,0,0,0.08), 9999px radius)."
- "Design a card: white background, 1px solid rgba(0,0,0,0.05) border, 16px radius, 24px padding, shadow rgba(0,0,0,0.03) 0px 2px 4px. Title at 20px Inter weight 600, letter-spacing -0.2px. Body at 14px weight 400, #666666."
- "Build a pill badge: #d4fae8 background, #0fa76e text, 9999px radius, 4px 12px padding, 13px Inter weight 500, uppercase."
- "Create navigation: white sticky header with backdrop-filter blur(12px). Inter 15px weight 500 for links, #0d0d0d text. Dark pill CTA 'Get Started' right-aligned, 9999px radius. Bottom border: 1px solid rgba(0,0,0,0.05)."
- "Design a trust section showing company logos in muted gray. Grid layout with 16px radius containers, 1px border at 5% opacity. Label above: 'Loved by your favorite companies' at 13px Inter weight 500, uppercase, tracking 0.65px."
### Iteration Guide
1. Always use full-pill radius (9999px) for buttons and inputs — this is Mintlify's signature shape
2. Keep borders at 5% opacity (`rgba(0,0,0,0.05)`) — stronger borders break the airy feeling
3. Letter-spacing scales with font size: -1.28px at 64px, -0.8px at 40px, -0.24px at 24px, normal at 16px
4. Three weights only: 400 (read), 500 (interact), 600 (announce)
5. Brand green (`#18E299`) is used sparingly — CTAs and hover states only, never for decorative fills
6. Geist Mono uppercase for technical labels, Inter for everything else
7. Section padding is generous: 64px96px on desktop, 48px on mobile
8. No gray background sections — white throughout, separation through borders and whitespace

View File

@ -1,24 +0,0 @@
# Mintlify Inspired Design System
[DESIGN.md](https://github.com/VoltAgent/awesome-design-md/blob/main/design-md/mintlify/DESIGN.md) extracted from the public [Mintlify](https://mintlify.com/) website. This is not the official design system. Colors, fonts, and spacing may not be 100% accurate. But it's a good starting point for building something similar.
## Files
| File | Description |
|------|-------------|
| `DESIGN.md` | Complete design system documentation (9 sections) |
| `preview.html` | Interactive design token catalog (light) |
| `preview-dark.html` | Interactive design token catalog (dark) |
Use [DESIGN.md](https://github.com/VoltAgent/awesome-design-md/blob/main/design-md/mintlify/DESIGN.md) to use as a reference for AI agents (Claude, Cursor, Stitch) to generate UI that looks like the Mintlify design language.
## Preview
A sample landing page built with DESIGN.md. It shows the actual colors, typography, buttons, cards, spacing, and elevation, all in one page.
### Dark Mode
![Mintlify Design System — Dark Mode](https://pub-2e4ecbcbc9b24e7b93f1a6ab5b2bc71f.r2.dev/designs/mintlify/preview-dark-screenshot.png)
### Light Mode
![Mintlify Design System — Light Mode](https://pub-2e4ecbcbc9b24e7b93f1a6ab5b2bc71f.r2.dev/designs/mintlify/preview-screenshot.png)

View File

@ -1,409 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Design System Inspired by Mintlify</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
:root {
--black: #ededed;
--white: #0d0d0d;
--gray-50: #141414;
--gray-100: #1a1a1a;
--gray-200: #2a2a2a;
--gray-400: #666666;
--gray-500: #888888;
--gray-700: #a0a0a0;
--brand: #18E299;
--brand-light: #0d3d2a;
--brand-deep: #5eedb8;
--warn: #e6a733;
--info-blue: #5b94e0;
--error: #e06b6b;
--border-subtle: rgba(255,255,255,0.06);
--border-medium: rgba(255,255,255,0.10);
--shadow-ambient: rgba(0,0,0,0.3) 0px 2px 4px;
--shadow-button: rgba(0,0,0,0.4) 0px 1px 2px;
--font-sans: 'Inter', system-ui, -apple-system, sans-serif;
--font-mono: 'Geist Mono', ui-monospace, SFMono-Regular, 'Roboto Mono', Menlo, monospace;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: var(--white);
color: var(--black);
font-family: var(--font-sans);
font-size: 16px; font-weight: 400; line-height: 1.50;
-webkit-font-smoothing: antialiased;
}
/* DARK MODE BADGE */
.dark-badge {
position: fixed; top: 16px; right: 16px; z-index: 200;
background: var(--brand); color: #0d0d0d;
padding: 6px 14px; border-radius: 9999px;
font-size: 12px; font-weight: 600; letter-spacing: 0.3px;
box-shadow: 0 2px 8px rgba(0,0,0,0.4);
}
/* NAV */
.nav {
position: sticky; top: 0; z-index: 100;
display: flex; align-items: center; justify-content: space-between;
padding: 12px 32px;
background: rgba(13,13,13,0.88);
backdrop-filter: blur(12px);
border-bottom: 1px solid var(--border-subtle);
}
.nav-brand { font-size: 14px; font-weight: 600; color: var(--black); text-decoration: none; letter-spacing: -0.28px; }
.nav-links { display: flex; gap: 24px; list-style: none; }
.nav-links a { font-size: 14px; font-weight: 500; color: var(--gray-500); text-decoration: none; transition: color 0.15s; }
.nav-links a:hover { color: var(--brand); }
.nav-cta {
display: inline-block; background: var(--black); color: var(--white);
padding: 7px 18px; border-radius: 9999px; font-size: 14px; font-weight: 500;
text-decoration: none; transition: opacity 0.15s;
}
.nav-cta:hover { opacity: 0.88; }
/* HERO */
.hero {
padding: 96px 32px 80px; text-align: center;
background: linear-gradient(180deg, #061f14 0%, #0f1a15 30%, #0d0d0d 100%);
position: relative;
}
.hero h1 {
font-size: 56px; font-weight: 600; line-height: 1.10;
letter-spacing: -1.28px; color: var(--black); margin-bottom: 16px;
}
.hero p { font-size: 18px; font-weight: 400; line-height: 1.50; color: var(--gray-500); max-width: 560px; margin: 0 auto 32px; }
.hero-buttons { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; }
.btn-dark {
display: inline-block; background: var(--black); color: var(--white);
padding: 10px 24px; border-radius: 9999px; border: none;
font-family: var(--font-sans); font-size: 15px; font-weight: 500;
text-decoration: none; cursor: pointer; transition: opacity 0.15s;
box-shadow: var(--shadow-button);
}
.btn-dark:hover { opacity: 0.88; }
.btn-ghost {
display: inline-block; background: transparent; color: var(--black);
padding: 10px 24px; border-radius: 9999px; border: 1px solid var(--border-medium);
font-family: var(--font-sans); font-size: 15px; font-weight: 500;
text-decoration: none; cursor: pointer; transition: border-color 0.15s;
}
.btn-ghost:hover { border-color: var(--gray-400); }
/* SECTIONS */
.section { padding: 64px 32px; max-width: 1200px; margin: 0 auto; }
.section-label {
font-family: var(--font-mono); font-size: 12px; font-weight: 500;
color: var(--gray-400); text-transform: uppercase; margin-bottom: 8px;
letter-spacing: 0.6px;
}
.section-title { font-size: 32px; font-weight: 600; line-height: 1.20; letter-spacing: -0.8px; margin-bottom: 32px; }
.section-divider { border: none; border-top: 1px solid var(--border-subtle); margin: 0; }
/* COLORS */
.color-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(155px, 1fr)); gap: 12px; margin-bottom: 24px; }
.color-swatch { border-radius: 16px; overflow: hidden; border: 1px solid var(--border-subtle); }
.color-swatch-block { height: 72px; width: 100%; }
.color-swatch-info { padding: 10px 12px; }
.color-swatch-name { font-size: 13px; font-weight: 600; margin-bottom: 2px; letter-spacing: -0.26px; }
.color-swatch-hex { font-size: 12px; color: var(--gray-500); font-family: var(--font-mono); }
.color-swatch-role { font-size: 11px; color: var(--gray-400); margin-top: 3px; }
.color-group-label { font-size: 14px; font-weight: 600; color: var(--gray-500); letter-spacing: -0.28px; margin: 24px 0 10px; }
/* TYPOGRAPHY */
.type-sample { margin-bottom: 28px; padding-bottom: 24px; border-bottom: 1px solid var(--border-subtle); }
.type-sample:last-child { border-bottom: none; }
.type-meta { font-family: var(--font-mono); font-size: 12px; font-weight: 500; color: var(--gray-400); margin-top: 8px; }
/* BUTTONS */
.button-row { display: flex; gap: 16px; flex-wrap: wrap; align-items: center; }
.button-item { text-align: center; }
.button-label { font-size: 12px; font-weight: 500; color: var(--gray-400); margin-top: 8px; }
.btn-brand {
display: inline-block; background: var(--brand); color: #0d0d0d;
padding: 10px 24px; border-radius: 9999px; font-size: 15px; font-weight: 500;
text-decoration: none; border: none; cursor: pointer;
}
.btn-pill-badge {
display: inline-block; background: var(--brand-light); color: var(--brand-deep);
padding: 4px 12px; border-radius: 9999px; font-size: 13px; font-weight: 500;
text-decoration: none; text-transform: uppercase; letter-spacing: 0.3px;
}
/* CARDS */
.card-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 20px; }
.card {
background: var(--gray-50); border-radius: 16px; padding: 24px;
border: 1px solid var(--border-subtle);
box-shadow: var(--shadow-ambient);
transition: border-color 0.2s;
}
.card:hover { border-color: var(--border-medium); }
.card h3 { font-size: 20px; font-weight: 600; letter-spacing: -0.2px; margin-bottom: 8px; }
.card p { font-size: 14px; color: var(--gray-500); line-height: 1.50; }
.card-badge {
display: inline-block; font-family: var(--font-mono); font-size: 12px; font-weight: 500;
text-transform: uppercase; padding: 2px 10px; border-radius: 9999px; margin-bottom: 12px;
letter-spacing: 0.5px;
}
/* FEATURED CARD */
.card-featured {
background: var(--gray-50); border-radius: 24px; padding: 32px;
border: 1px solid var(--border-subtle);
box-shadow: var(--shadow-ambient);
}
.card-featured h3 { font-size: 24px; font-weight: 600; letter-spacing: -0.24px; margin-bottom: 8px; }
.card-featured p { font-size: 16px; color: var(--gray-500); line-height: 1.50; }
/* FORMS */
.form-group { margin-bottom: 20px; max-width: 400px; }
.form-label { display: block; font-size: 14px; font-weight: 500; color: var(--black); margin-bottom: 6px; }
.form-input {
width: 100%; background: var(--gray-50); color: var(--black);
border: 1px solid var(--border-medium); padding: 10px 16px; border-radius: 9999px;
font-family: var(--font-sans); font-size: 14px; outline: none;
transition: border-color 0.15s;
}
.form-input:focus { border-color: var(--brand); box-shadow: 0 0 0 1px var(--brand); }
.form-input--focus { border-color: var(--brand); box-shadow: 0 0 0 1px var(--brand); }
.form-input--error { border-color: var(--error); box-shadow: 0 0 0 1px var(--error); }
.form-textarea {
width: 100%; min-height: 80px; background: var(--gray-50); color: var(--black);
border: 1px solid var(--border-medium); padding: 12px 16px; border-radius: 16px;
font-family: var(--font-sans); font-size: 14px; resize: vertical; outline: none;
}
.form-state-label { font-size: 11px; color: var(--gray-400); margin-top: 4px; }
/* SPACING */
.spacing-row { display: flex; align-items: flex-end; gap: 10px; flex-wrap: wrap; margin-bottom: 24px; }
.spacing-item { text-align: center; }
.spacing-block { background: var(--brand); border-radius: 4px; margin-bottom: 6px; height: 28px; }
.spacing-value { font-family: var(--font-mono); font-size: 11px; font-weight: 500; color: var(--gray-400); }
/* RADIUS */
.radius-row { display: flex; gap: 14px; flex-wrap: wrap; align-items: center; }
.radius-item { text-align: center; }
.radius-box { width: 64px; height: 64px; background: var(--black); margin-bottom: 6px; }
.radius-label { font-family: var(--font-mono); font-size: 11px; font-weight: 500; color: var(--gray-400); }
.radius-context { font-size: 10px; color: var(--gray-400); }
/* ELEVATION */
.elevation-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; }
.elevation-card { background: var(--gray-50); border-radius: 16px; padding: 20px; text-align: center; }
.elevation-label { font-size: 14px; font-weight: 600; letter-spacing: -0.28px; margin-bottom: 4px; }
.elevation-desc { font-family: var(--font-mono); font-size: 11px; color: var(--gray-400); }
/* FOOTER */
.footer { padding: 32px; text-align: center; border-top: 1px solid var(--border-subtle); font-size: 13px; color: var(--gray-500); }
.footer a { color: var(--brand); text-decoration: underline; }
@media (max-width: 768px) {
.hero h1 { font-size: 40px; letter-spacing: -0.8px; }
.nav-links { display: none; }
.section { padding: 48px 20px; }
.card-grid { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<div class="dark-badge">Dark Mode</div>
<nav class="nav">
<span class="nav-brand">awesome-design-md</span>
<ul class="nav-links">
<li><a href="#colors">Colors</a></li>
<li><a href="#typography">Typography</a></li>
<li><a href="#buttons">Buttons</a></li>
<li><a href="#cards">Cards</a></li>
<li><a href="#forms">Forms</a></li>
<li><a href="#spacing">Spacing</a></li>
</ul>
<a class="nav-cta" href="#">Get Started</a>
</nav>
<section class="hero">
<h1>Design System<br>Inspired by Mintlify</h1>
<p>A design token catalog generated from DESIGN.md. Every color, font, component, and spacing value — visualized.</p>
<div class="hero-buttons">
<a class="btn-dark" href="#">Get Started</a>
<a class="btn-ghost" href="#">View Documentation</a>
</div>
</section>
<hr class="section-divider">
<section class="section" id="colors">
<div class="section-label">01 / Colors</div>
<h2 class="section-title">Color Palette</h2>
<div class="color-group-label">Primary</div>
<div class="color-grid">
<div class="color-swatch"><div class="color-swatch-block" style="background:#0d0d0d"></div><div class="color-swatch-info"><div class="color-swatch-name">Near Black</div><div class="color-swatch-hex">#0d0d0d</div><div class="color-swatch-role">Dark background</div></div></div>
<div class="color-swatch"><div class="color-swatch-block" style="background:#ededed"></div><div class="color-swatch-info"><div class="color-swatch-name">Near White</div><div class="color-swatch-hex">#ededed</div><div class="color-swatch-role">Primary text (dark)</div></div></div>
<div class="color-swatch"><div class="color-swatch-block" style="background:#18E299"></div><div class="color-swatch-info"><div class="color-swatch-name">Brand Green</div><div class="color-swatch-hex">#18E299</div><div class="color-swatch-role">Brand accent, CTAs</div></div></div>
</div>
<div class="color-group-label">Brand Extended</div>
<div class="color-grid">
<div class="color-swatch"><div class="color-swatch-block" style="background:#0d3d2a"></div><div class="color-swatch-info"><div class="color-swatch-name">Green Dark</div><div class="color-swatch-hex">#0d3d2a</div><div class="color-swatch-role">Badge bg (dark)</div></div></div>
<div class="color-swatch"><div class="color-swatch-block" style="background:#5eedb8"></div><div class="color-swatch-info"><div class="color-swatch-name">Green Light</div><div class="color-swatch-hex">#5eedb8</div><div class="color-swatch-role">Badge text (dark)</div></div></div>
<div class="color-swatch"><div class="color-swatch-block" style="background:#1ba673"></div><div class="color-swatch-info"><div class="color-swatch-name">Annotate Green</div><div class="color-swatch-hex">#1ba673</div><div class="color-swatch-role">Code annotations</div></div></div>
</div>
<div class="color-group-label">Semantic</div>
<div class="color-grid">
<div class="color-swatch"><div class="color-swatch-block" style="background:#5b94e0"></div><div class="color-swatch-info"><div class="color-swatch-name">Info Blue</div><div class="color-swatch-hex">#5b94e0</div><div class="color-swatch-role">Tags, annotations</div></div></div>
<div class="color-swatch"><div class="color-swatch-block" style="background:#e6a733"></div><div class="color-swatch-info"><div class="color-swatch-name">Warn Amber</div><div class="color-swatch-hex">#e6a733</div><div class="color-swatch-role">Warnings, caution</div></div></div>
<div class="color-swatch"><div class="color-swatch-block" style="background:#e06b6b"></div><div class="color-swatch-info"><div class="color-swatch-name">Error Red</div><div class="color-swatch-hex">#e06b6b</div><div class="color-swatch-role">Errors, destructive</div></div></div>
</div>
<div class="color-group-label">Neutral Scale (Dark)</div>
<div class="color-grid">
<div class="color-swatch"><div class="color-swatch-block" style="background:#a0a0a0"></div><div class="color-swatch-info"><div class="color-swatch-name">Gray 700</div><div class="color-swatch-hex">#a0a0a0</div><div class="color-swatch-role">Secondary text</div></div></div>
<div class="color-swatch"><div class="color-swatch-block" style="background:#888888"></div><div class="color-swatch-info"><div class="color-swatch-name">Gray 500</div><div class="color-swatch-hex">#888888</div><div class="color-swatch-role">Tertiary text</div></div></div>
<div class="color-swatch"><div class="color-swatch-block" style="background:#666666"></div><div class="color-swatch-info"><div class="color-swatch-name">Gray 400</div><div class="color-swatch-hex">#666666</div><div class="color-swatch-role">Placeholders</div></div></div>
<div class="color-swatch"><div class="color-swatch-block" style="background:#2a2a2a"></div><div class="color-swatch-info"><div class="color-swatch-name">Gray 200</div><div class="color-swatch-hex">#2a2a2a</div><div class="color-swatch-role">Borders</div></div></div>
<div class="color-swatch"><div class="color-swatch-block" style="background:#1a1a1a"></div><div class="color-swatch-info"><div class="color-swatch-name">Gray 100</div><div class="color-swatch-hex">#1a1a1a</div><div class="color-swatch-role">Surface</div></div></div>
<div class="color-swatch"><div class="color-swatch-block" style="background:#141414"></div><div class="color-swatch-info"><div class="color-swatch-name">Gray 50</div><div class="color-swatch-hex">#141414</div><div class="color-swatch-role">Card background</div></div></div>
</div>
</section>
<hr class="section-divider">
<section class="section" id="typography">
<div class="section-label">02 / Typography</div>
<h2 class="section-title">Typography Scale</h2>
<div class="type-sample"><div style="font-size:56px; font-weight:600; line-height:1.10; letter-spacing:-1.28px;">Display Hero</div><div class="type-meta">Display Hero — 56px / 600 / 1.10 / -1.28px / Inter</div></div>
<div class="type-sample"><div style="font-size:40px; font-weight:600; line-height:1.10; letter-spacing:-0.8px;">Section Heading</div><div class="type-meta">Section Heading — 40px / 600 / 1.10 / -0.8px / Inter</div></div>
<div class="type-sample"><div style="font-size:24px; font-weight:500; line-height:1.30; letter-spacing:-0.24px;">Sub-heading</div><div class="type-meta">Sub-heading — 24px / 500 / 1.30 / -0.24px / Inter</div></div>
<div class="type-sample"><div style="font-size:20px; font-weight:600; line-height:1.30; letter-spacing:-0.2px;">Card Title</div><div class="type-meta">Card Title — 20px / 600 / 1.30 / -0.2px / Inter</div></div>
<div class="type-sample"><div style="font-size:18px; font-weight:400; line-height:1.50;">Body Large — The intelligent knowledge platform that powers your documentation, APIs, and guides.</div><div class="type-meta">Body Large — 18px / 400 / 1.50 / normal / Inter</div></div>
<div class="type-sample"><div style="font-size:16px; font-weight:400; line-height:1.50;">Body — Standard reading text for documentation and content.</div><div class="type-meta">Body — 16px / 400 / 1.50 / normal / Inter</div></div>
<div class="type-sample"><div style="font-size:16px; font-weight:500; line-height:1.50;">Body Medium — Navigation and emphasized text</div><div class="type-meta">Body Medium — 16px / 500 / 1.50 / Inter</div></div>
<div class="type-sample"><div style="font-size:15px; font-weight:500; line-height:1.50;">Button Label</div><div class="type-meta">Button — 15px / 500 / 1.50 / Inter</div></div>
<div class="type-sample"><div style="font-size:14px; font-weight:500; line-height:1.50;">Link / Caption</div><div class="type-meta">Link / Caption — 14px / 500 / 1.50 / Inter</div></div>
<div class="type-sample"><div style="font-size:13px; font-weight:500; line-height:1.50; text-transform:uppercase; letter-spacing:0.65px;">Section Label</div><div class="type-meta">Label Uppercase — 13px / 500 / uppercase / 0.65px / Inter</div></div>
<div class="type-sample"><div style="font-family:var(--font-mono); font-size:12px; font-weight:500; line-height:1.50; text-transform:uppercase; letter-spacing:0.6px;">MONO TECHNICAL LABEL</div><div class="type-meta">Mono Code — 12px / 500 / uppercase / 0.6px / Geist Mono</div></div>
<div class="type-sample"><div style="font-family:var(--font-mono); font-size:10px; font-weight:500; line-height:1.50; text-transform:uppercase;">MICRO LABEL</div><div class="type-meta">Mono Micro — 10px / 500 / uppercase / Geist Mono</div></div>
</section>
<hr class="section-divider">
<section class="section" id="buttons">
<div class="section-label">03 / Buttons</div>
<h2 class="section-title">Button Variants</h2>
<div class="button-row">
<div class="button-item"><a class="btn-dark" href="#">Get Started</a><div class="button-label">Primary</div></div>
<div class="button-item"><a class="btn-ghost" href="#">Documentation</a><div class="button-label">Ghost / Outline</div></div>
<div class="button-item"><a class="btn-brand" href="#">Start Building</a><div class="button-label">Brand Accent</div></div>
<div class="button-item"><span class="btn-pill-badge">Documentation</span><div class="button-label">Pill Badge</div></div>
<div class="button-item"><span style="display:inline-block; background:rgba(91,148,224,0.15); color:#5b94e0; padding:4px 12px; border-radius:9999px; font-size:12px; font-weight:500;">Info</span><div class="button-label">Info Badge</div></div>
<div class="button-item"><span style="display:inline-block; background:rgba(230,167,51,0.15); color:#e6a733; padding:4px 12px; border-radius:9999px; font-size:12px; font-weight:500;">Warning</span><div class="button-label">Warning Badge</div></div>
</div>
</section>
<hr class="section-divider">
<section class="section" id="cards">
<div class="section-label">04 / Cards</div>
<h2 class="section-title">Card Examples</h2>
<div class="card-grid">
<div class="card">
<div class="card-badge" style="background:var(--brand-light); color:var(--brand-deep);">Docs</div>
<h3>Intelligent Search</h3>
<p>AI-powered search that understands your documentation and delivers precise answers to developer questions.</p>
</div>
<div class="card">
<div class="card-badge" style="background:rgba(91,148,224,0.12); color:#5b94e0;">API</div>
<h3>API Reference</h3>
<p>Auto-generated API documentation with interactive examples, authentication flows, and code snippets.</p>
</div>
<div class="card">
<div class="card-badge" style="background:rgba(230,167,51,0.12); color:#e6a733;">Analytics</div>
<h3>Documentation Analytics</h3>
<p>Track what developers search for, which pages perform best, and where they get stuck.</p>
</div>
</div>
<div style="margin-top:32px;">
<div class="color-group-label">Featured Card (24px radius)</div>
<div class="card-featured">
<div class="card-badge" style="background:var(--brand-light); color:var(--brand-deep);">Featured</div>
<h3>The Intelligent Knowledge Platform</h3>
<p>Build beautiful documentation that powers your developer community. AI-enhanced, automatically updated, and always in sync with your codebase.</p>
</div>
</div>
</section>
<hr class="section-divider">
<section class="section" id="forms">
<div class="section-label">05 / Forms</div>
<h2 class="section-title">Form Elements</h2>
<div class="form-group"><label class="form-label">Email Address</label><input class="form-input" type="text" placeholder="you@company.com"><div class="form-state-label">Default (pill shape)</div></div>
<div class="form-group"><label class="form-label">Organization</label><input class="form-input form-input--focus" type="text" value="Mintlify"><div class="form-state-label">Focus (green ring)</div></div>
<div class="form-group"><label class="form-label">API Key</label><input class="form-input form-input--error" type="text" value="invalid-key"><div class="form-state-label">Error (red ring)</div></div>
<div class="form-group"><label class="form-label">Description</label><textarea class="form-textarea" placeholder="Tell us about your project..."></textarea></div>
</section>
<hr class="section-divider">
<section class="section" id="spacing">
<div class="section-label">06 / Spacing</div>
<h2 class="section-title">Spacing Scale</h2>
<div class="spacing-row">
<div class="spacing-item"><div class="spacing-block" style="width:2px"></div><div class="spacing-value">2</div></div>
<div class="spacing-item"><div class="spacing-block" style="width:4px"></div><div class="spacing-value">4</div></div>
<div class="spacing-item"><div class="spacing-block" style="width:6px"></div><div class="spacing-value">6</div></div>
<div class="spacing-item"><div class="spacing-block" style="width:8px"></div><div class="spacing-value">8</div></div>
<div class="spacing-item"><div class="spacing-block" style="width:10px"></div><div class="spacing-value">10</div></div>
<div class="spacing-item"><div class="spacing-block" style="width:12px"></div><div class="spacing-value">12</div></div>
<div class="spacing-item"><div class="spacing-block" style="width:16px"></div><div class="spacing-value">16</div></div>
<div class="spacing-item"><div class="spacing-block" style="width:24px"></div><div class="spacing-value">24</div></div>
<div class="spacing-item"><div class="spacing-block" style="width:32px"></div><div class="spacing-value">32</div></div>
<div class="spacing-item"><div class="spacing-block" style="width:48px"></div><div class="spacing-value">48</div></div>
<div class="spacing-item"><div class="spacing-block" style="width:64px"></div><div class="spacing-value">64</div></div>
</div>
</section>
<hr class="section-divider">
<section class="section" id="radius">
<div class="section-label">07 / Radius</div>
<h2 class="section-title">Border Radius Scale</h2>
<div class="radius-row">
<div class="radius-item"><div class="radius-box" style="border-radius:4px"></div><div class="radius-label">4px</div><div class="radius-context">Code, tags</div></div>
<div class="radius-item"><div class="radius-box" style="border-radius:8px"></div><div class="radius-label">8px</div><div class="radius-context">Nav buttons</div></div>
<div class="radius-item"><div class="radius-box" style="border-radius:16px"></div><div class="radius-label">16px</div><div class="radius-context">Cards</div></div>
<div class="radius-item"><div class="radius-box" style="border-radius:24px"></div><div class="radius-label">24px</div><div class="radius-context">Featured</div></div>
<div class="radius-item"><div class="radius-box" style="border-radius:9999px"></div><div class="radius-label">9999px</div><div class="radius-context">Buttons, pills</div></div>
</div>
</section>
<hr class="section-divider">
<section class="section" id="elevation">
<div class="section-label">08 / Elevation</div>
<h2 class="section-title">Elevation &amp; Depth</h2>
<div class="elevation-grid">
<div class="elevation-card" style="border: 1px solid var(--border-subtle);"><div class="elevation-label">Level 0: Flat</div><div class="elevation-desc">No shadow</div></div>
<div class="elevation-card" style="border: 1px solid var(--border-subtle); box-shadow: var(--shadow-ambient);"><div class="elevation-label">Level 1: Subtle</div><div class="elevation-desc">6% border + ambient</div></div>
<div class="elevation-card" style="border: 1px solid var(--border-medium); box-shadow: var(--shadow-ambient);"><div class="elevation-label">Level 2: Medium</div><div class="elevation-desc">10% border + ambient</div></div>
<div class="elevation-card" style="border: 1px solid var(--border-medium); box-shadow: rgba(0,0,0,0.4) 0px 4px 12px;"><div class="elevation-label">Level 3: Raised</div><div class="elevation-desc">Stronger shadow</div></div>
<div class="elevation-card" style="border: 1px solid var(--brand); box-shadow: 0 0 0 1px var(--brand);"><div class="elevation-label">Focus Ring</div><div class="elevation-desc">Brand green ring</div></div>
</div>
</section>
<footer class="footer">Maintained by <a href="https://github.com/VoltAgent/voltagent" target="_blank" rel="noopener noreferrer" style="text-decoration:none;"><img src="https://github.com/VoltAgent.png?size=32" alt="VoltAgent" width="14" height="14" style="border-radius:3px;vertical-align:-2px;margin-right:3px;">VoltAgent</a> team</footer>
</body>
</html>

View File

@ -1,398 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Design System Inspired by Mintlify</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
:root {
--black: #0d0d0d;
--white: #ffffff;
--gray-50: #fafafa;
--gray-100: #f5f5f5;
--gray-200: #e5e5e5;
--gray-400: #888888;
--gray-500: #666666;
--gray-700: #333333;
--brand: #18E299;
--brand-light: #d4fae8;
--brand-deep: #0fa76e;
--warn: #c37d0d;
--info-blue: #3772cf;
--error: #d45656;
--border-subtle: rgba(0,0,0,0.05);
--border-medium: rgba(0,0,0,0.08);
--shadow-ambient: rgba(0,0,0,0.03) 0px 2px 4px;
--shadow-button: rgba(0,0,0,0.06) 0px 1px 2px;
--font-sans: 'Inter', system-ui, -apple-system, sans-serif;
--font-mono: 'Geist Mono', ui-monospace, SFMono-Regular, 'Roboto Mono', Menlo, monospace;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: var(--white);
color: var(--black);
font-family: var(--font-sans);
font-size: 16px; font-weight: 400; line-height: 1.50;
-webkit-font-smoothing: antialiased;
}
/* NAV */
.nav {
position: sticky; top: 0; z-index: 100;
display: flex; align-items: center; justify-content: space-between;
padding: 12px 32px;
background: rgba(255,255,255,0.88);
backdrop-filter: blur(12px);
border-bottom: 1px solid var(--border-subtle);
}
.nav-brand { font-size: 14px; font-weight: 600; color: var(--black); text-decoration: none; letter-spacing: -0.28px; }
.nav-links { display: flex; gap: 24px; list-style: none; }
.nav-links a { font-size: 14px; font-weight: 500; color: var(--gray-500); text-decoration: none; transition: color 0.15s; }
.nav-links a:hover { color: var(--brand); }
.nav-cta {
display: inline-block; background: var(--black); color: var(--white);
padding: 7px 18px; border-radius: 9999px; font-size: 14px; font-weight: 500;
text-decoration: none; transition: opacity 0.15s;
}
.nav-cta:hover { opacity: 0.88; }
/* HERO */
.hero {
padding: 96px 32px 80px; text-align: center;
background: linear-gradient(180deg, #e8faf1 0%, #f0fdf6 30%, #ffffff 100%);
position: relative;
}
.hero h1 {
font-size: 56px; font-weight: 600; line-height: 1.10;
letter-spacing: -1.28px; color: var(--black); margin-bottom: 16px;
}
.hero p { font-size: 18px; font-weight: 400; line-height: 1.50; color: var(--gray-500); max-width: 560px; margin: 0 auto 32px; }
.hero-buttons { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; }
.btn-dark {
display: inline-block; background: var(--black); color: var(--white);
padding: 10px 24px; border-radius: 9999px; border: none;
font-family: var(--font-sans); font-size: 15px; font-weight: 500;
text-decoration: none; cursor: pointer; transition: opacity 0.15s;
box-shadow: var(--shadow-button);
}
.btn-dark:hover { opacity: 0.88; }
.btn-ghost {
display: inline-block; background: var(--white); color: var(--black);
padding: 10px 24px; border-radius: 9999px; border: 1px solid var(--border-medium);
font-family: var(--font-sans); font-size: 15px; font-weight: 500;
text-decoration: none; cursor: pointer; transition: border-color 0.15s;
}
.btn-ghost:hover { border-color: var(--gray-400); }
/* SECTIONS */
.section { padding: 64px 32px; max-width: 1200px; margin: 0 auto; }
.section-label {
font-family: var(--font-mono); font-size: 12px; font-weight: 500;
color: var(--gray-400); text-transform: uppercase; margin-bottom: 8px;
letter-spacing: 0.6px;
}
.section-title { font-size: 32px; font-weight: 600; line-height: 1.20; letter-spacing: -0.8px; margin-bottom: 32px; }
.section-divider { border: none; border-top: 1px solid var(--border-subtle); margin: 0; }
/* COLORS */
.color-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(155px, 1fr)); gap: 12px; margin-bottom: 24px; }
.color-swatch { border-radius: 16px; overflow: hidden; border: 1px solid var(--border-subtle); }
.color-swatch-block { height: 72px; width: 100%; }
.color-swatch-info { padding: 10px 12px; }
.color-swatch-name { font-size: 13px; font-weight: 600; margin-bottom: 2px; letter-spacing: -0.26px; }
.color-swatch-hex { font-size: 12px; color: var(--gray-500); font-family: var(--font-mono); }
.color-swatch-role { font-size: 11px; color: var(--gray-400); margin-top: 3px; }
.color-group-label { font-size: 14px; font-weight: 600; color: var(--gray-500); letter-spacing: -0.28px; margin: 24px 0 10px; }
/* TYPOGRAPHY */
.type-sample { margin-bottom: 28px; padding-bottom: 24px; border-bottom: 1px solid var(--border-subtle); }
.type-sample:last-child { border-bottom: none; }
.type-meta { font-family: var(--font-mono); font-size: 12px; font-weight: 500; color: var(--gray-400); margin-top: 8px; }
/* BUTTONS */
.button-row { display: flex; gap: 16px; flex-wrap: wrap; align-items: center; }
.button-item { text-align: center; }
.button-label { font-size: 12px; font-weight: 500; color: var(--gray-400); margin-top: 8px; }
.btn-brand {
display: inline-block; background: var(--brand); color: var(--black);
padding: 10px 24px; border-radius: 9999px; font-size: 15px; font-weight: 500;
text-decoration: none; border: none; cursor: pointer;
}
.btn-pill-badge {
display: inline-block; background: var(--brand-light); color: var(--brand-deep);
padding: 4px 12px; border-radius: 9999px; font-size: 13px; font-weight: 500;
text-decoration: none; text-transform: uppercase; letter-spacing: 0.3px;
}
/* CARDS */
.card-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 20px; }
.card {
background: var(--white); border-radius: 16px; padding: 24px;
border: 1px solid var(--border-subtle);
box-shadow: var(--shadow-ambient);
transition: border-color 0.2s;
}
.card:hover { border-color: var(--border-medium); }
.card h3 { font-size: 20px; font-weight: 600; letter-spacing: -0.2px; margin-bottom: 8px; }
.card p { font-size: 14px; color: var(--gray-500); line-height: 1.50; }
.card-badge {
display: inline-block; font-family: var(--font-mono); font-size: 12px; font-weight: 500;
text-transform: uppercase; padding: 2px 10px; border-radius: 9999px; margin-bottom: 12px;
letter-spacing: 0.5px;
}
/* FEATURED CARD */
.card-featured {
background: var(--white); border-radius: 24px; padding: 32px;
border: 1px solid var(--border-subtle);
box-shadow: var(--shadow-ambient);
}
.card-featured h3 { font-size: 24px; font-weight: 600; letter-spacing: -0.24px; margin-bottom: 8px; }
.card-featured p { font-size: 16px; color: var(--gray-500); line-height: 1.50; }
/* FORMS */
.form-group { margin-bottom: 20px; max-width: 400px; }
.form-label { display: block; font-size: 14px; font-weight: 500; color: var(--black); margin-bottom: 6px; }
.form-input {
width: 100%; background: var(--white); color: var(--black);
border: 1px solid var(--border-medium); padding: 10px 16px; border-radius: 9999px;
font-family: var(--font-sans); font-size: 14px; outline: none;
transition: border-color 0.15s;
}
.form-input:focus { border-color: var(--brand); box-shadow: 0 0 0 1px var(--brand); }
.form-input--focus { border-color: var(--brand); box-shadow: 0 0 0 1px var(--brand); }
.form-input--error { border-color: var(--error); box-shadow: 0 0 0 1px var(--error); }
.form-textarea {
width: 100%; min-height: 80px; background: var(--white); color: var(--black);
border: 1px solid var(--border-medium); padding: 12px 16px; border-radius: 16px;
font-family: var(--font-sans); font-size: 14px; resize: vertical; outline: none;
}
.form-state-label { font-size: 11px; color: var(--gray-400); margin-top: 4px; }
/* SPACING */
.spacing-row { display: flex; align-items: flex-end; gap: 10px; flex-wrap: wrap; margin-bottom: 24px; }
.spacing-item { text-align: center; }
.spacing-block { background: var(--brand); border-radius: 4px; margin-bottom: 6px; height: 28px; }
.spacing-value { font-family: var(--font-mono); font-size: 11px; font-weight: 500; color: var(--gray-400); }
/* RADIUS */
.radius-row { display: flex; gap: 14px; flex-wrap: wrap; align-items: center; }
.radius-item { text-align: center; }
.radius-box { width: 64px; height: 64px; background: var(--black); margin-bottom: 6px; }
.radius-label { font-family: var(--font-mono); font-size: 11px; font-weight: 500; color: var(--gray-400); }
.radius-context { font-size: 10px; color: var(--gray-400); }
/* ELEVATION */
.elevation-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; }
.elevation-card { background: var(--white); border-radius: 16px; padding: 20px; text-align: center; }
.elevation-label { font-size: 14px; font-weight: 600; letter-spacing: -0.28px; margin-bottom: 4px; }
.elevation-desc { font-family: var(--font-mono); font-size: 11px; color: var(--gray-400); }
/* FOOTER */
.footer { padding: 32px; text-align: center; border-top: 1px solid var(--border-subtle); font-size: 13px; color: var(--gray-500); }
.footer a { color: var(--brand); text-decoration: underline; }
@media (max-width: 768px) {
.hero h1 { font-size: 40px; letter-spacing: -0.8px; }
.nav-links { display: none; }
.section { padding: 48px 20px; }
.card-grid { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<nav class="nav">
<span class="nav-brand">awesome-design-md</span>
<ul class="nav-links">
<li><a href="#colors">Colors</a></li>
<li><a href="#typography">Typography</a></li>
<li><a href="#buttons">Buttons</a></li>
<li><a href="#cards">Cards</a></li>
<li><a href="#forms">Forms</a></li>
<li><a href="#spacing">Spacing</a></li>
</ul>
<a class="nav-cta" href="#">Get Started</a>
</nav>
<section class="hero">
<h1>Design System<br>Inspired by Mintlify</h1>
<p>A design token catalog generated from DESIGN.md. Every color, font, component, and spacing value — visualized.</p>
<div class="hero-buttons">
<a class="btn-dark" href="#">Get Started</a>
<a class="btn-ghost" href="#">View Documentation</a>
</div>
</section>
<hr class="section-divider">
<section class="section" id="colors">
<div class="section-label">01 / Colors</div>
<h2 class="section-title">Color Palette</h2>
<div class="color-group-label">Primary</div>
<div class="color-grid">
<div class="color-swatch"><div class="color-swatch-block" style="background:#0d0d0d"></div><div class="color-swatch-info"><div class="color-swatch-name">Near Black</div><div class="color-swatch-hex">#0d0d0d</div><div class="color-swatch-role">Primary text, headings</div></div></div>
<div class="color-swatch"><div class="color-swatch-block" style="background:#ffffff; border-bottom:1px solid #e5e5e5"></div><div class="color-swatch-info"><div class="color-swatch-name">Pure White</div><div class="color-swatch-hex">#ffffff</div><div class="color-swatch-role">Page background</div></div></div>
<div class="color-swatch"><div class="color-swatch-block" style="background:#18E299"></div><div class="color-swatch-info"><div class="color-swatch-name">Brand Green</div><div class="color-swatch-hex">#18E299</div><div class="color-swatch-role">Brand accent, CTAs</div></div></div>
</div>
<div class="color-group-label">Brand Extended</div>
<div class="color-grid">
<div class="color-swatch"><div class="color-swatch-block" style="background:#d4fae8"></div><div class="color-swatch-info"><div class="color-swatch-name">Green Light</div><div class="color-swatch-hex">#d4fae8</div><div class="color-swatch-role">Badge backgrounds</div></div></div>
<div class="color-swatch"><div class="color-swatch-block" style="background:#0fa76e"></div><div class="color-swatch-info"><div class="color-swatch-name">Green Deep</div><div class="color-swatch-hex">#0fa76e</div><div class="color-swatch-role">Badge text</div></div></div>
<div class="color-swatch"><div class="color-swatch-block" style="background:#1ba673"></div><div class="color-swatch-info"><div class="color-swatch-name">Annotate Green</div><div class="color-swatch-hex">#1ba673</div><div class="color-swatch-role">Code annotations</div></div></div>
</div>
<div class="color-group-label">Semantic</div>
<div class="color-grid">
<div class="color-swatch"><div class="color-swatch-block" style="background:#3772cf"></div><div class="color-swatch-info"><div class="color-swatch-name">Info Blue</div><div class="color-swatch-hex">#3772cf</div><div class="color-swatch-role">Tags, annotations</div></div></div>
<div class="color-swatch"><div class="color-swatch-block" style="background:#c37d0d"></div><div class="color-swatch-info"><div class="color-swatch-name">Warn Amber</div><div class="color-swatch-hex">#c37d0d</div><div class="color-swatch-role">Warnings, caution</div></div></div>
<div class="color-swatch"><div class="color-swatch-block" style="background:#d45656"></div><div class="color-swatch-info"><div class="color-swatch-name">Error Red</div><div class="color-swatch-hex">#d45656</div><div class="color-swatch-role">Errors, destructive</div></div></div>
</div>
<div class="color-group-label">Neutral Scale</div>
<div class="color-grid">
<div class="color-swatch"><div class="color-swatch-block" style="background:#333333"></div><div class="color-swatch-info"><div class="color-swatch-name">Gray 700</div><div class="color-swatch-hex">#333333</div><div class="color-swatch-role">Secondary text</div></div></div>
<div class="color-swatch"><div class="color-swatch-block" style="background:#666666"></div><div class="color-swatch-info"><div class="color-swatch-name">Gray 500</div><div class="color-swatch-hex">#666666</div><div class="color-swatch-role">Tertiary text</div></div></div>
<div class="color-swatch"><div class="color-swatch-block" style="background:#888888"></div><div class="color-swatch-info"><div class="color-swatch-name">Gray 400</div><div class="color-swatch-hex">#888888</div><div class="color-swatch-role">Placeholders</div></div></div>
<div class="color-swatch"><div class="color-swatch-block" style="background:#e5e5e5"></div><div class="color-swatch-info"><div class="color-swatch-name">Gray 200</div><div class="color-swatch-hex">#e5e5e5</div><div class="color-swatch-role">Borders</div></div></div>
<div class="color-swatch"><div class="color-swatch-block" style="background:#f5f5f5; border-bottom:1px solid #e5e5e5"></div><div class="color-swatch-info"><div class="color-swatch-name">Gray 100</div><div class="color-swatch-hex">#f5f5f5</div><div class="color-swatch-role">Subtle surface</div></div></div>
<div class="color-swatch"><div class="color-swatch-block" style="background:#fafafa; border-bottom:1px solid #e5e5e5"></div><div class="color-swatch-info"><div class="color-swatch-name">Gray 50</div><div class="color-swatch-hex">#fafafa</div><div class="color-swatch-role">Near-white tint</div></div></div>
</div>
</section>
<hr class="section-divider">
<section class="section" id="typography">
<div class="section-label">02 / Typography</div>
<h2 class="section-title">Typography Scale</h2>
<div class="type-sample"><div style="font-size:56px; font-weight:600; line-height:1.10; letter-spacing:-1.28px;">Display Hero</div><div class="type-meta">Display Hero — 56px / 600 / 1.10 / -1.28px / Inter</div></div>
<div class="type-sample"><div style="font-size:40px; font-weight:600; line-height:1.10; letter-spacing:-0.8px;">Section Heading</div><div class="type-meta">Section Heading — 40px / 600 / 1.10 / -0.8px / Inter</div></div>
<div class="type-sample"><div style="font-size:24px; font-weight:500; line-height:1.30; letter-spacing:-0.24px;">Sub-heading</div><div class="type-meta">Sub-heading — 24px / 500 / 1.30 / -0.24px / Inter</div></div>
<div class="type-sample"><div style="font-size:20px; font-weight:600; line-height:1.30; letter-spacing:-0.2px;">Card Title</div><div class="type-meta">Card Title — 20px / 600 / 1.30 / -0.2px / Inter</div></div>
<div class="type-sample"><div style="font-size:18px; font-weight:400; line-height:1.50;">Body Large — The intelligent knowledge platform that powers your documentation, APIs, and guides.</div><div class="type-meta">Body Large — 18px / 400 / 1.50 / normal / Inter</div></div>
<div class="type-sample"><div style="font-size:16px; font-weight:400; line-height:1.50;">Body — Standard reading text for documentation and content.</div><div class="type-meta">Body — 16px / 400 / 1.50 / normal / Inter</div></div>
<div class="type-sample"><div style="font-size:16px; font-weight:500; line-height:1.50;">Body Medium — Navigation and emphasized text</div><div class="type-meta">Body Medium — 16px / 500 / 1.50 / Inter</div></div>
<div class="type-sample"><div style="font-size:15px; font-weight:500; line-height:1.50;">Button Label</div><div class="type-meta">Button — 15px / 500 / 1.50 / Inter</div></div>
<div class="type-sample"><div style="font-size:14px; font-weight:500; line-height:1.50;">Link / Caption</div><div class="type-meta">Link / Caption — 14px / 500 / 1.50 / Inter</div></div>
<div class="type-sample"><div style="font-size:13px; font-weight:500; line-height:1.50; text-transform:uppercase; letter-spacing:0.65px;">Section Label</div><div class="type-meta">Label Uppercase — 13px / 500 / uppercase / 0.65px / Inter</div></div>
<div class="type-sample"><div style="font-family:var(--font-mono); font-size:12px; font-weight:500; line-height:1.50; text-transform:uppercase; letter-spacing:0.6px;">MONO TECHNICAL LABEL</div><div class="type-meta">Mono Code — 12px / 500 / uppercase / 0.6px / Geist Mono</div></div>
<div class="type-sample"><div style="font-family:var(--font-mono); font-size:10px; font-weight:500; line-height:1.50; text-transform:uppercase;">MICRO LABEL</div><div class="type-meta">Mono Micro — 10px / 500 / uppercase / Geist Mono</div></div>
</section>
<hr class="section-divider">
<section class="section" id="buttons">
<div class="section-label">03 / Buttons</div>
<h2 class="section-title">Button Variants</h2>
<div class="button-row">
<div class="button-item"><a class="btn-dark" href="#">Get Started</a><div class="button-label">Primary Dark</div></div>
<div class="button-item"><a class="btn-ghost" href="#">Documentation</a><div class="button-label">Ghost / Outline</div></div>
<div class="button-item"><a class="btn-brand" href="#">Start Building</a><div class="button-label">Brand Accent</div></div>
<div class="button-item"><span class="btn-pill-badge">Documentation</span><div class="button-label">Pill Badge</div></div>
<div class="button-item"><span style="display:inline-block; background:rgba(55,114,207,0.12); color:#3772cf; padding:4px 12px; border-radius:9999px; font-size:12px; font-weight:500;">Info</span><div class="button-label">Info Badge</div></div>
<div class="button-item"><span style="display:inline-block; background:rgba(195,125,13,0.12); color:#c37d0d; padding:4px 12px; border-radius:9999px; font-size:12px; font-weight:500;">Warning</span><div class="button-label">Warning Badge</div></div>
</div>
</section>
<hr class="section-divider">
<section class="section" id="cards">
<div class="section-label">04 / Cards</div>
<h2 class="section-title">Card Examples</h2>
<div class="card-grid">
<div class="card">
<div class="card-badge" style="background:var(--brand-light); color:var(--brand-deep);">Docs</div>
<h3>Intelligent Search</h3>
<p>AI-powered search that understands your documentation and delivers precise answers to developer questions.</p>
</div>
<div class="card">
<div class="card-badge" style="background:rgba(55,114,207,0.1); color:#3772cf;">API</div>
<h3>API Reference</h3>
<p>Auto-generated API documentation with interactive examples, authentication flows, and code snippets.</p>
</div>
<div class="card">
<div class="card-badge" style="background:rgba(195,125,13,0.1); color:#c37d0d;">Analytics</div>
<h3>Documentation Analytics</h3>
<p>Track what developers search for, which pages perform best, and where they get stuck.</p>
</div>
</div>
<div style="margin-top:32px;">
<div class="color-group-label">Featured Card (24px radius)</div>
<div class="card-featured">
<div class="card-badge" style="background:var(--brand-light); color:var(--brand-deep);">Featured</div>
<h3>The Intelligent Knowledge Platform</h3>
<p>Build beautiful documentation that powers your developer community. AI-enhanced, automatically updated, and always in sync with your codebase.</p>
</div>
</div>
</section>
<hr class="section-divider">
<section class="section" id="forms">
<div class="section-label">05 / Forms</div>
<h2 class="section-title">Form Elements</h2>
<div class="form-group"><label class="form-label">Email Address</label><input class="form-input" type="text" placeholder="you@company.com"><div class="form-state-label">Default (pill shape)</div></div>
<div class="form-group"><label class="form-label">Organization</label><input class="form-input form-input--focus" type="text" value="Mintlify"><div class="form-state-label">Focus (green ring)</div></div>
<div class="form-group"><label class="form-label">API Key</label><input class="form-input form-input--error" type="text" value="invalid-key"><div class="form-state-label">Error (red ring)</div></div>
<div class="form-group"><label class="form-label">Description</label><textarea class="form-textarea" placeholder="Tell us about your project..."></textarea></div>
</section>
<hr class="section-divider">
<section class="section" id="spacing">
<div class="section-label">06 / Spacing</div>
<h2 class="section-title">Spacing Scale</h2>
<div class="spacing-row">
<div class="spacing-item"><div class="spacing-block" style="width:2px"></div><div class="spacing-value">2</div></div>
<div class="spacing-item"><div class="spacing-block" style="width:4px"></div><div class="spacing-value">4</div></div>
<div class="spacing-item"><div class="spacing-block" style="width:6px"></div><div class="spacing-value">6</div></div>
<div class="spacing-item"><div class="spacing-block" style="width:8px"></div><div class="spacing-value">8</div></div>
<div class="spacing-item"><div class="spacing-block" style="width:10px"></div><div class="spacing-value">10</div></div>
<div class="spacing-item"><div class="spacing-block" style="width:12px"></div><div class="spacing-value">12</div></div>
<div class="spacing-item"><div class="spacing-block" style="width:16px"></div><div class="spacing-value">16</div></div>
<div class="spacing-item"><div class="spacing-block" style="width:24px"></div><div class="spacing-value">24</div></div>
<div class="spacing-item"><div class="spacing-block" style="width:32px"></div><div class="spacing-value">32</div></div>
<div class="spacing-item"><div class="spacing-block" style="width:48px"></div><div class="spacing-value">48</div></div>
<div class="spacing-item"><div class="spacing-block" style="width:64px"></div><div class="spacing-value">64</div></div>
</div>
</section>
<hr class="section-divider">
<section class="section" id="radius">
<div class="section-label">07 / Radius</div>
<h2 class="section-title">Border Radius Scale</h2>
<div class="radius-row">
<div class="radius-item"><div class="radius-box" style="border-radius:4px"></div><div class="radius-label">4px</div><div class="radius-context">Code, tags</div></div>
<div class="radius-item"><div class="radius-box" style="border-radius:8px"></div><div class="radius-label">8px</div><div class="radius-context">Nav buttons</div></div>
<div class="radius-item"><div class="radius-box" style="border-radius:16px"></div><div class="radius-label">16px</div><div class="radius-context">Cards</div></div>
<div class="radius-item"><div class="radius-box" style="border-radius:24px"></div><div class="radius-label">24px</div><div class="radius-context">Featured</div></div>
<div class="radius-item"><div class="radius-box" style="border-radius:9999px"></div><div class="radius-label">9999px</div><div class="radius-context">Buttons, pills</div></div>
</div>
</section>
<hr class="section-divider">
<section class="section" id="elevation">
<div class="section-label">08 / Elevation</div>
<h2 class="section-title">Elevation &amp; Depth</h2>
<div class="elevation-grid">
<div class="elevation-card" style="border: 1px solid var(--border-subtle);"><div class="elevation-label">Level 0: Flat</div><div class="elevation-desc">No shadow</div></div>
<div class="elevation-card" style="border: 1px solid var(--border-subtle); box-shadow: var(--shadow-ambient);"><div class="elevation-label">Level 1: Subtle</div><div class="elevation-desc">5% border + ambient</div></div>
<div class="elevation-card" style="border: 1px solid var(--border-medium); box-shadow: var(--shadow-ambient);"><div class="elevation-label">Level 2: Medium</div><div class="elevation-desc">8% border + ambient</div></div>
<div class="elevation-card" style="border: 1px solid var(--border-medium); box-shadow: rgba(0,0,0,0.06) 0px 4px 12px;"><div class="elevation-label">Level 3: Raised</div><div class="elevation-desc">Stronger shadow</div></div>
<div class="elevation-card" style="border: 1px solid var(--brand); box-shadow: 0 0 0 1px var(--brand);"><div class="elevation-label">Focus Ring</div><div class="elevation-desc">Brand green ring</div></div>
</div>
</section>
<footer class="footer">Maintained by <a href="https://github.com/VoltAgent/voltagent" target="_blank" rel="noopener noreferrer" style="text-decoration:none;"><img src="https://github.com/VoltAgent.png?size=32" alt="VoltAgent" width="14" height="14" style="border-radius:3px;vertical-align:-2px;margin-right:3px;">VoltAgent</a> team</footer>
</body>
</html>

82
design/pages/auth.md Normal file
View File

@ -0,0 +1,82 @@
# 任务
按照/design/BaseInfo.md中要求将登录页面、注册页面和忘记密码页面的元素和交互设计为 React 组件,如果有可以提取为公共组件的元素,先检查是否已存在,没有则放入 /src/components/ 目录下。
## 强制规则
- 支持国际化
- 语言:中文、英语,默认使用中文
- 实现方式i18next
- 翻译文件位置src/i18n/zh.json、src/i18n/en.json
- 组件内使用useTranslation hook
- 组件内必须使用翻译函数进行文本渲染
- 组件内不能直接使用硬编码的文本
- 样式规范
- 组件内必须使用 tailwindcss 进行样式设计
- 组件内不能使用内联样式
## 页面组成
### 1. 登录页面
- 元素:
- 系统 logo
- 用户名输入框(带校验)
- 密码输入框(带校验,显示/隐藏密码功能)
- 记住我 checkbox
- 登录按钮
- 注册(超链接)
- 忘记密码(超链接)
- 交互:
- 用户名:必填,格式校验
- 密码:必填,长度至少 6 位
- 点击登录:提交表单,显示加载状态
- 登录失败:显示错误信息
- 注册链接(超链接):点击跳转到注册页面
### 2. 注册页面
- 元素:
- 系统 logo
- 用户名输入框(带校验)
- 密码输入框(带校验)
- 确认密码输入框(带校验)
- 取消按钮
- 注册按钮
- 返回登录(超链接)
- 交互:
- 用户名:必填,格式校验,唯一性检查
- 密码:必填,长度至少 6 位,包含字母和数字
- 确认密码:必须与密码一致
- 点击注册:提交表单,显示加载状态
- 注册失败:显示错误信息
- 取消按钮:返回登录页面
### 3. 忘记密码页面
- 元素:
- 系统 logo
- 用户名输入框(带校验)
- 密码输入框(带校验)
- 确认密码输入框(带校验)
- 取消按钮
- 重置密码按钮
- 返回登录(超链接)
- 交互:
- 用户名:必填,格式校验,唯一性检查
- 密码:必填,长度至少 6 位,包含字母和数字
- 确认密码:必须与密码一致
- 点击重置密码:提交表单,显示加载状态
- 重置密码失败:显示错误信息
- 取消按钮:返回登录页面
## 安全考虑
- 密码加密:使用 bcrypt 或类似算法
- 认证JWT token
- CSRF 防护:使用 CSRF token
- XSS 防护:使用 React 的内置防护
- 数据验证:前端和后端双重验证
## 国际化
- 支持语言:中文、英语,默认使用中文
- 实现方式i18next
- 翻译文件位置src/i18n/zh.json、src/i18n/en.json
- 组件内使用useTranslation hook
## 检查样式
完成组件生成后,严格按照 /design/DESIGN.md 中的设计规范使用light-mode模式检查本项目中的样式设置修复不符合设计规范的样式

38
design/pages/project.md Normal file
View File

@ -0,0 +1,38 @@
# 任务
按照/design/BaseInfo.md中要求将项目管理页面的元素和交互设计为 React 组件;
如果有可以提取为公共组件的元素,
1、先检查是否公共组件已存在不存在则放入 /src/components/ 目录下。
2、如果已存在是否有新的需求或改进如果需要更新更新组件代码同时检查用到该组件的页面是否需要更新引用。
3、如果已存在但不需要更新则直接使用已有的组件。
## 强制规则
- 支持国际化
- 语言:中文、英语,默认使用中文
- 实现方式i18next
- 翻译文件位置src/i18n/zh.json、src/i18n/en.json
- 组件内使用useTranslation hook
- 组件内必须使用翻译函数进行文本渲染
- 组件内不能直接使用硬编码的文本
- 样式规范
- 组件内必须使用 tailwindcss 进行样式设计
- 组件内不能使用内联样式
## 页面组成
### 5. 项目管理
- 项目列表:
- 卡片的形式展示
- 显示项目名称、创建时间、卡片背景为图片,先占位,后续根据需求调整
- 卡片底部操作:默认不显示,鼠标悬停在卡片上显示操作按钮
- 编辑按钮:点击后跳转到项目编辑页
- 删除按钮:点击后弹窗提示,是否确认删除项目,用户确认后,删除项目,确认删除后刷新页面
- 搜索和筛选功能:
- 搜索框输入项目名称搜索项目输入完成按下enter键后请求后端搜索项目返回搜索结果
- 筛选框:根据项目状态筛选项目,选择项目状态后请求后端筛选项目,返回筛选结果
- 分页功能:
- 显示当前页码、每页显示项目数、总项目数
- 分页按钮:点击后切换到对应页码
- 批量操作:选中项目后,弹窗提示,用户确认删除选中项目,用户确认删除后,删除选中项目,确认删除后刷新页面,预留向后端发送请求删除选中项目
- 创建新项目按钮,点击后打开创建新项目的弹窗,弹窗内容:
- 项目名称输入框
- 项目描述输入框
- 保存按钮:点击后创建新项目,并刷新页面,预留向后端发送请求创建新项目

37
design/pages/setting.md Normal file
View File

@ -0,0 +1,37 @@
# 任务
按照/design/BaseInfo.md中要求将设置页面的元素和交互设计为 React 组件;
如果有可以提取为公共组件的元素,
1、先检查是否公共组件已存在不存在则放入 /src/components/ 目录下。
2、如果已存在是否有新的需求或改进如果需要更新更新组件代码同时检查用到该组件的页面是否需要更新引用。
3、如果已存在但不需要更新则直接使用已有的组件。
## 强制规则
- 支持国际化
- 语言:中文、英语,默认使用中文
- 实现方式i18next
- 翻译文件位置src/i18n/zh.json、src/i18n/en.json
- 组件内使用useTranslation hook
- 组件内必须使用翻译函数进行文本渲染
- 组件内不能直接使用硬编码的文本
- 样式规范
- 组件内必须使用 tailwindcss 进行样式设计
- 组件内不能使用内联样式
## 页面组成
### 1. 设置页面
分成4个tab
- 个人信息:
- 用户名、邮箱、头像等
- 保存按钮:点击后更新个人信息,并刷新页面,预留向后端发送请求更新个人信息
- 密码修改:
- 旧密码输入框
- 新密码输入框
- 确认新密码输入框
- 保存按钮:点击后更新密码,并刷新页面,预留向后端发送请求更新密码
- 语言设置:
- 中文、英语切换
- 保存按钮:点击后切换语言,并刷新页面,预留向后端发送请求更新语言
- 通知设置:
- 邮件通知开关
- 系统通知开关
- 保存按钮:点击后更新通知设置,并刷新页面,预留向后端发送请求更新通知设置

View File

@ -0,0 +1,39 @@
# 任务
按照/design/BaseInfo.md中要求将系统布局、首页的元素和交互设计为 React 组件;
如果有可以提取为公共组件的元素,
1、先检查是否公共组件已存在不存在则放入 /src/components/ 目录下。
2、如果已存在是否有新的需求或改进如果需要更新更新组件代码同时检查用到该组件的页面是否需要更新引用。
3、如果已存在但不需要更新则直接使用已有的组件。
## 强制规则
- 支持国际化
- 语言:中文、英语,默认使用中文
- 实现方式i18next
- 翻译文件位置src/i18n/zh.json、src/i18n/en.json
- 组件内使用useTranslation hook
- 组件内必须使用翻译函数进行文本渲染
- 组件内不能直接使用硬编码的文本
- 样式规范
- 组件内必须使用 tailwindcss 进行样式设计
- 组件内不能使用内联样式
## 页面组成
### 1. 系统布局
- 左侧菜单树:
- 菜单项:首页、项目管理、设置
- 展开/收起功能
- 顶部导航栏:
- 系统标题
- 右侧:通知图标、用户菜单
- 主体区域:显示当前选中的页面内容
- 响应式:在小屏幕设备上,左侧菜单可折叠
### 2. 首页
- 欢迎信息:显示当前用户名
- 项目统计:
- 总项目数
- 活跃项目数
- 最近创建的项目
- 快捷操作:创建新项目、查看所有项目
- 系统状态:显示系统运行状态
- 更新登录按钮逻辑:登录成功后:点击后跳转到首页

View File

@ -1,322 +0,0 @@
# Design System Inspiration of Stripe
## 1. Visual Theme & Atmosphere
Stripe's website is the gold standard of fintech design -- a system that manages to feel simultaneously technical and luxurious, precise and warm. The page opens on a clean white canvas (`#ffffff`) with deep navy headings (`#061b31`) and a signature purple (`#533afd`) that functions as both brand anchor and interactive accent. This isn't the cold, clinical purple of enterprise software; it's a rich, saturated violet that reads as confident and premium. The overall impression is of a financial institution redesigned by a world-class type foundry.
The custom `sohne-var` variable font is the defining element of Stripe's visual identity. Every text element enables the OpenType `"ss01"` stylistic set, which modifies character shapes for a distinctly geometric, modern feel. At display sizes (48px-56px), sohne-var runs at weight 300 -- an extraordinarily light weight for headlines that creates an ethereal, almost whispered authority. This is the opposite of the "bold hero headline" convention; Stripe's headlines feel like they don't need to shout. The negative letter-spacing (-1.4px at 56px, -0.96px at 48px) tightens the text into dense, engineered blocks. At smaller sizes, the system also uses weight 300 with proportionally reduced tracking, and tabular numerals via `"tnum"` for financial data display.
What truly distinguishes Stripe is its shadow system. Rather than the flat or single-layer approach of most sites, Stripe uses multi-layer, blue-tinted shadows: the signature `rgba(50,50,93,0.25)` combined with `rgba(0,0,0,0.1)` creates shadows with a cool, almost atmospheric depth -- like elements are floating in a twilight sky. The blue-gray undertone of the primary shadow color (50,50,93) ties directly to the navy-purple brand palette, making even elevation feel on-brand.
**Key Characteristics:**
- sohne-var with OpenType `"ss01"` on all text -- a custom stylistic set that defines the brand's letterforms
- Weight 300 as the signature headline weight -- light, confident, anti-convention
- Negative letter-spacing at display sizes (-1.4px at 56px, progressive relaxation downward)
- Blue-tinted multi-layer shadows using `rgba(50,50,93,0.25)` -- elevation that feels brand-colored
- Deep navy (`#061b31`) headings instead of black -- warm, premium, financial-grade
- Conservative border-radius (4px-8px) -- nothing pill-shaped, nothing harsh
- Ruby (`#ea2261`) and magenta (`#f96bee`) accents for gradient and decorative elements
- `SourceCodePro` as the monospace companion for code and technical labels
## 2. Color Palette & Roles
### Primary
- **Stripe Purple** (`#533afd`): Primary brand color, CTA backgrounds, link text, interactive highlights. A saturated blue-violet that anchors the entire system.
- **Deep Navy** (`#061b31`): `--hds-color-heading-solid`. Primary heading color. Not black, not gray -- a very dark blue that adds warmth and depth to text.
- **Pure White** (`#ffffff`): Page background, card surfaces, button text on dark backgrounds.
### Brand & Dark
- **Brand Dark** (`#1c1e54`): `--hds-color-util-brand-900`. Deep indigo for dark sections, footer backgrounds, and immersive brand moments.
- **Dark Navy** (`#0d253d`): `--hds-color-core-neutral-975`. The darkest neutral -- almost-black with a blue undertone for maximum depth without harshness.
### Accent Colors
- **Ruby** (`#ea2261`): `--hds-color-accentColorMode-ruby-icon-solid`. Warm red-pink for icons, alerts, and accent elements.
- **Magenta** (`#f96bee`): `--hds-color-accentColorMode-magenta-icon-gradientMiddle`. Vivid pink-purple for gradients and decorative highlights.
- **Magenta Light** (`#ffd7ef`): `--hds-color-util-accent-magenta-100`. Tinted surface for magenta-themed cards and badges.
### Interactive
- **Primary Purple** (`#533afd`): Primary link color, active states, selected elements.
- **Purple Hover** (`#4434d4`): Darker purple for hover states on primary elements.
- **Purple Deep** (`#2e2b8c`): `--hds-color-button-ui-iconHover`. Dark purple for icon hover states.
- **Purple Light** (`#b9b9f9`): `--hds-color-action-bg-subduedHover`. Soft lavender for subdued hover backgrounds.
- **Purple Mid** (`#665efd`): `--hds-color-input-selector-text-range`. Range selector and input highlight color.
### Neutral Scale
- **Heading** (`#061b31`): Primary headings, nav text, strong labels.
- **Label** (`#273951`): `--hds-color-input-text-label`. Form labels, secondary headings.
- **Body** (`#64748d`): Secondary text, descriptions, captions.
- **Success Green** (`#15be53`): Status badges, success indicators (with 0.2-0.4 alpha for backgrounds/borders).
- **Success Text** (`#108c3d`): Success badge text color.
- **Lemon** (`#9b6829`): `--hds-color-core-lemon-500`. Warning and highlight accent.
### Surface & Borders
- **Border Default** (`#e5edf5`): Standard border color for cards, dividers, and containers.
- **Border Purple** (`#b9b9f9`): Active/selected state borders on buttons and inputs.
- **Border Soft Purple** (`#d6d9fc`): Subtle purple-tinted borders for secondary elements.
- **Border Magenta** (`#ffd7ef`): Pink-tinted borders for magenta-themed elements.
- **Border Dashed** (`#362baa`): Dashed borders for drop zones and placeholder elements.
### Shadow Colors
- **Shadow Blue** (`rgba(50,50,93,0.25)`): The signature -- blue-tinted primary shadow color.
- **Shadow Dark Blue** (`rgba(3,3,39,0.25)`): Deeper blue shadow for elevated elements.
- **Shadow Black** (`rgba(0,0,0,0.1)`): Secondary shadow layer for depth reinforcement.
- **Shadow Ambient** (`rgba(23,23,23,0.08)`): Soft ambient shadow for subtle elevation.
- **Shadow Soft** (`rgba(23,23,23,0.06)`): Minimal ambient shadow for light lift.
## 3. Typography Rules
### Font Family
- **Primary**: `sohne-var`, with fallback: `SF Pro Display`
- **Monospace**: `SourceCodePro`, with fallback: `SFMono-Regular`
- **OpenType Features**: `"ss01"` enabled globally on all sohne-var text; `"tnum"` for tabular numbers on financial data and captions.
### Hierarchy
| Role | Font | Size | Weight | Line Height | Letter Spacing | Features | Notes |
|------|------|------|--------|-------------|----------------|----------|-------|
| Display Hero | sohne-var | 56px (3.50rem) | 300 | 1.03 (tight) | -1.4px | ss01 | Maximum size, whisper-weight authority |
| Display Large | sohne-var | 48px (3.00rem) | 300 | 1.15 (tight) | -0.96px | ss01 | Secondary hero headlines |
| Section Heading | sohne-var | 32px (2.00rem) | 300 | 1.10 (tight) | -0.64px | ss01 | Feature section titles |
| Sub-heading Large | sohne-var | 26px (1.63rem) | 300 | 1.12 (tight) | -0.26px | ss01 | Card headings, sub-sections |
| Sub-heading | sohne-var | 22px (1.38rem) | 300 | 1.10 (tight) | -0.22px | ss01 | Smaller section heads |
| Body Large | sohne-var | 18px (1.13rem) | 300 | 1.40 | normal | ss01 | Feature descriptions, intro text |
| Body | sohne-var | 16px (1.00rem) | 300-400 | 1.40 | normal | ss01 | Standard reading text |
| Button | sohne-var | 16px (1.00rem) | 400 | 1.00 (tight) | normal | ss01 | Primary button text |
| Button Small | sohne-var | 14px (0.88rem) | 400 | 1.00 (tight) | normal | ss01 | Secondary/compact buttons |
| Link | sohne-var | 14px (0.88rem) | 400 | 1.00 (tight) | normal | ss01 | Navigation links |
| Caption | sohne-var | 13px (0.81rem) | 400 | normal | normal | ss01 | Small labels, metadata |
| Caption Small | sohne-var | 12px (0.75rem) | 300-400 | 1.33-1.45 | normal | ss01 | Fine print, timestamps |
| Caption Tabular | sohne-var | 12px (0.75rem) | 300-400 | 1.33 | -0.36px | tnum | Financial data, numbers |
| Micro | sohne-var | 10px (0.63rem) | 300 | 1.15 (tight) | 0.1px | ss01 | Tiny labels, axis markers |
| Micro Tabular | sohne-var | 10px (0.63rem) | 300 | 1.15 (tight) | -0.3px | tnum | Chart data, small numbers |
| Nano | sohne-var | 8px (0.50rem) | 300 | 1.07 (tight) | normal | ss01 | Smallest labels |
| Code Body | SourceCodePro | 12px (0.75rem) | 500 | 2.00 (relaxed) | normal | -- | Code blocks, syntax |
| Code Bold | SourceCodePro | 12px (0.75rem) | 700 | 2.00 (relaxed) | normal | -- | Bold code, keywords |
| Code Label | SourceCodePro | 12px (0.75rem) | 500 | 2.00 (relaxed) | normal | uppercase | Technical labels |
| Code Micro | SourceCodePro | 9px (0.56rem) | 500 | 1.00 (tight) | normal | ss01 | Tiny code annotations |
### Principles
- **Light weight as signature**: Weight 300 at display sizes is Stripe's most distinctive typographic choice. Where others use 600-700 to command attention, Stripe uses lightness as luxury -- the text is so confident it doesn't need weight to be authoritative.
- **ss01 everywhere**: The `"ss01"` stylistic set is non-negotiable. It modifies specific glyphs (likely alternate `a`, `g`, `l` forms) to create a more geometric, contemporary feel across all sohne-var text.
- **Two OpenType modes**: `"ss01"` for display/body text, `"tnum"` for tabular numerals in financial data. These never overlap -- a number in a paragraph uses ss01, a number in a data table uses tnum.
- **Progressive tracking**: Letter-spacing tightens proportionally with size: -1.4px at 56px, -0.96px at 48px, -0.64px at 32px, -0.26px at 26px, normal at 16px and below.
- **Two-weight simplicity**: Primarily 300 (body and headings) and 400 (UI/buttons). No bold (700) in the primary font -- SourceCodePro uses 500/700 for code contrast.
## 4. Component Stylings
### Buttons
**Primary Purple**
- Background: `#533afd`
- Text: `#ffffff`
- Padding: 8px 16px
- Radius: 4px
- Font: 16px sohne-var weight 400, `"ss01"`
- Hover: `#4434d4` background
- Use: Primary CTA ("Start now", "Contact sales")
**Ghost / Outlined**
- Background: transparent
- Text: `#533afd`
- Padding: 8px 16px
- Radius: 4px
- Border: `1px solid #b9b9f9`
- Font: 16px sohne-var weight 400, `"ss01"`
- Hover: background shifts to `rgba(83,58,253,0.05)`
- Use: Secondary actions
**Transparent Info**
- Background: transparent
- Text: `#2874ad`
- Padding: 8px 16px
- Radius: 4px
- Border: `1px solid rgba(43,145,223,0.2)`
- Use: Tertiary/info-level actions
**Neutral Ghost**
- Background: transparent (`rgba(255,255,255,0)`)
- Text: `rgba(16,16,16,0.3)`
- Padding: 8px 16px
- Radius: 4px
- Outline: `1px solid rgb(212,222,233)`
- Use: Disabled or muted actions
### Cards & Containers
- Background: `#ffffff`
- Border: `1px solid #e5edf5` (standard) or `1px solid #061b31` (dark accent)
- Radius: 4px (tight), 5px (standard), 6px (comfortable), 8px (featured)
- Shadow (standard): `rgba(50,50,93,0.25) 0px 30px 45px -30px, rgba(0,0,0,0.1) 0px 18px 36px -18px`
- Shadow (ambient): `rgba(23,23,23,0.08) 0px 15px 35px 0px`
- Hover: shadow intensifies, often adding the blue-tinted layer
### Badges / Tags / Pills
**Neutral Pill**
- Background: `#ffffff`
- Text: `#000000`
- Padding: 0px 6px
- Radius: 4px
- Border: `1px solid #f6f9fc`
- Font: 11px weight 400
**Success Badge**
- Background: `rgba(21,190,83,0.2)`
- Text: `#108c3d`
- Padding: 1px 6px
- Radius: 4px
- Border: `1px solid rgba(21,190,83,0.4)`
- Font: 10px weight 300
### Inputs & Forms
- Border: `1px solid #e5edf5`
- Radius: 4px
- Focus: `1px solid #533afd` or purple ring
- Label: `#273951`, 14px sohne-var
- Text: `#061b31`
- Placeholder: `#64748d`
### Navigation
- Clean horizontal nav on white, sticky with blur backdrop
- Brand logotype left-aligned
- Links: sohne-var 14px weight 400, `#061b31` text with `"ss01"`
- Radius: 6px on nav container
- CTA: purple button right-aligned ("Sign in", "Start now")
- Mobile: hamburger toggle with 6px radius
### Decorative Elements
**Dashed Borders**
- `1px dashed #362baa` (purple) for placeholder/drop zones
- `1px dashed #ffd7ef` (magenta) for magenta-themed decorative borders
**Gradient Accents**
- Ruby-to-magenta gradients (`#ea2261` to `#f96bee`) for hero decorations
- Brand dark sections use `#1c1e54` backgrounds with white text
## 5. Layout Principles
### Spacing System
- Base unit: 8px
- Scale: 1px, 2px, 4px, 6px, 8px, 10px, 11px, 12px, 14px, 16px, 18px, 20px
- Notable: The scale is dense at the small end (every 2px from 4-12), reflecting Stripe's precision-oriented UI for financial data
### Grid & Container
- Max content width: approximately 1080px
- Hero: centered single-column with generous padding, lightweight headlines
- Feature sections: 2-3 column grids for feature cards
- Full-width dark sections with `#1c1e54` background for brand immersion
- Code/dashboard previews as contained cards with blue-tinted shadows
### Whitespace Philosophy
- **Precision spacing**: Unlike the vast emptiness of minimalist systems, Stripe uses measured, purposeful whitespace. Every gap is a deliberate typographic choice.
- **Dense data, generous chrome**: Financial data displays (tables, charts) are tightly packed, but the UI chrome around them is generously spaced. This creates a sense of controlled density -- like a well-organized spreadsheet in a beautiful frame.
- **Section rhythm**: White sections alternate with dark brand sections (`#1c1e54`), creating a dramatic light/dark cadence that prevents monotony without introducing arbitrary color.
### Border Radius Scale
- Micro (1px): Fine-grained elements, subtle rounding
- Standard (4px): Buttons, inputs, badges, cards -- the workhorse
- Comfortable (5px): Standard card containers
- Relaxed (6px): Navigation, larger interactive elements
- Large (8px): Featured cards, hero elements
- Compound: `0px 0px 6px 6px` for bottom-rounded containers (tab panels, dropdown footers)
## 6. Depth & Elevation
| Level | Treatment | Use |
|-------|-----------|-----|
| Flat (Level 0) | No shadow | Page background, inline text |
| Ambient (Level 1) | `rgba(23,23,23,0.06) 0px 3px 6px` | Subtle card lift, hover hints |
| Standard (Level 2) | `rgba(23,23,23,0.08) 0px 15px 35px` | Standard cards, content panels |
| Elevated (Level 3) | `rgba(50,50,93,0.25) 0px 30px 45px -30px, rgba(0,0,0,0.1) 0px 18px 36px -18px` | Featured cards, dropdowns, popovers |
| Deep (Level 4) | `rgba(3,3,39,0.25) 0px 14px 21px -14px, rgba(0,0,0,0.1) 0px 8px 17px -8px` | Modals, floating panels |
| Ring (Accessibility) | `2px solid #533afd` outline | Keyboard focus ring |
**Shadow Philosophy**: Stripe's shadow system is built on a principle of chromatic depth. Where most design systems use neutral gray or black shadows, Stripe's primary shadow color (`rgba(50,50,93,0.25)`) is a deep blue-gray that echoes the brand's navy palette. This creates shadows that don't just add depth -- they add brand atmosphere. The multi-layer approach pairs this blue-tinted shadow with a pure black secondary layer (`rgba(0,0,0,0.1)`) at a different offset, creating a parallax-like depth where the branded shadow sits farther from the element and the neutral shadow sits closer. The negative spread values (-30px, -18px) ensure shadows don't extend beyond the element's footprint horizontally, keeping elevation vertical and controlled.
### Decorative Depth
- Dark brand sections (`#1c1e54`) create immersive depth through background color contrast
- Gradient overlays with ruby-to-magenta transitions for hero decorations
- Shadow color `rgba(0,55,112,0.08)` (`--hds-color-shadow-sm-top`) for top-edge shadows on sticky elements
## 7. Do's and Don'ts
### Do
- Use sohne-var with `"ss01"` on every text element -- the stylistic set IS the brand
- Use weight 300 for all headlines and body text -- lightness is the signature
- Apply blue-tinted shadows (`rgba(50,50,93,0.25)`) for all elevated elements
- Use `#061b31` (deep navy) for headings instead of `#000000` -- the warmth matters
- Keep border-radius between 4px-8px -- conservative rounding is intentional
- Use `"tnum"` for any tabular/financial number display
- Layer shadows: blue-tinted far + neutral close for depth parallax
- Use `#533afd` purple as the primary interactive/CTA color
### Don't
- Don't use weight 600-700 for sohne-var headlines -- weight 300 is the brand voice
- Don't use large border-radius (12px+, pill shapes) on cards or buttons -- Stripe is conservative
- Don't use neutral gray shadows -- always tint with blue (`rgba(50,50,93,...)`)
- Don't skip `"ss01"` on any sohne-var text -- the alternate glyphs define the personality
- Don't use pure black (`#000000`) for headings -- always `#061b31` deep navy
- Don't use warm accent colors (orange, yellow) for interactive elements -- purple is primary
- Don't apply positive letter-spacing at display sizes -- Stripe tracks tight
- Don't use the magenta/ruby accents for buttons or links -- they're decorative/gradient only
## 8. Responsive Behavior
### Breakpoints
| Name | Width | Key Changes |
|------|-------|-------------|
| Mobile | <640px | Single column, reduced heading sizes, stacked cards |
| Tablet | 640-1024px | 2-column grids, moderate padding |
| Desktop | 1024-1280px | Full layout, 3-column feature grids |
| Large Desktop | >1280px | Centered content with generous margins |
### Touch Targets
- Buttons use comfortable padding (8px-16px vertical)
- Navigation links at 14px with adequate spacing
- Badges have 6px horizontal padding minimum for tap targets
- Mobile nav toggle with 6px radius button
### Collapsing Strategy
- Hero: 56px display -> 32px on mobile, weight 300 maintained
- Navigation: horizontal links + CTAs -> hamburger toggle
- Feature cards: 3-column -> 2-column -> single column stacked
- Dark brand sections: maintain full-width treatment, reduce internal padding
- Financial data tables: horizontal scroll on mobile
- Section spacing: 64px+ -> 40px on mobile
- Typography scale compresses: 56px -> 48px -> 32px hero sizes across breakpoints
### Image Behavior
- Dashboard/product screenshots maintain blue-tinted shadow at all sizes
- Hero gradient decorations simplify on mobile
- Code blocks maintain `SourceCodePro` treatment, may horizontally scroll
- Card images maintain consistent 4px-6px border-radius
## 9. Agent Prompt Guide
### Quick Color Reference
- Primary CTA: Stripe Purple (`#533afd`)
- CTA Hover: Purple Dark (`#4434d4`)
- Background: Pure White (`#ffffff`)
- Heading text: Deep Navy (`#061b31`)
- Body text: Slate (`#64748d`)
- Label text: Dark Slate (`#273951`)
- Border: Soft Blue (`#e5edf5`)
- Link: Stripe Purple (`#533afd`)
- Dark section: Brand Dark (`#1c1e54`)
- Success: Green (`#15be53`)
- Accent decorative: Ruby (`#ea2261`), Magenta (`#f96bee`)
### Example Component Prompts
- "Create a hero section on white background. Headline at 48px sohne-var weight 300, line-height 1.15, letter-spacing -0.96px, color #061b31, font-feature-settings 'ss01'. Subtitle at 18px weight 300, line-height 1.40, color #64748d. Purple CTA button (#533afd, 4px radius, 8px 16px padding, white text) and ghost button (transparent, 1px solid #b9b9f9, #533afd text, 4px radius)."
- "Design a card: white background, 1px solid #e5edf5 border, 6px radius. Shadow: rgba(50,50,93,0.25) 0px 30px 45px -30px, rgba(0,0,0,0.1) 0px 18px 36px -18px. Title at 22px sohne-var weight 300, letter-spacing -0.22px, color #061b31, 'ss01'. Body at 16px weight 300, #64748d."
- "Build a success badge: rgba(21,190,83,0.2) background, #108c3d text, 4px radius, 1px 6px padding, 10px sohne-var weight 300, border 1px solid rgba(21,190,83,0.4)."
- "Create navigation: white sticky header with backdrop-filter blur(12px). sohne-var 14px weight 400 for links, #061b31 text, 'ss01'. Purple CTA 'Start now' right-aligned (#533afd bg, white text, 4px radius). Nav container 6px radius."
- "Design a dark brand section: #1c1e54 background, white text. Headline 32px sohne-var weight 300, letter-spacing -0.64px, 'ss01'. Body 16px weight 300, rgba(255,255,255,0.7). Cards inside use rgba(255,255,255,0.1) border with 6px radius."
### Iteration Guide
1. Always enable `font-feature-settings: "ss01"` on sohne-var text -- this is the brand's typographic DNA
2. Weight 300 is the default; use 400 only for buttons/links/navigation
3. Shadow formula: `rgba(50,50,93,0.25) 0px Y1 B1 -S1, rgba(0,0,0,0.1) 0px Y2 B2 -S2` where Y1/B1 are larger (far shadow) and Y2/B2 are smaller (near shadow)
4. Heading color is `#061b31` (deep navy), body is `#64748d` (slate), labels are `#273951` (dark slate)
5. Border-radius stays in the 4px-8px range -- never use pill shapes or large rounding
6. Use `"tnum"` for any numbers in tables, charts, or financial displays
7. Dark sections use `#1c1e54` -- not black, not gray, but a deep branded indigo
8. SourceCodePro for code at 12px/500 with 2.00 line-height (very generous for readability)

View File

@ -1,24 +0,0 @@
# Stripe Inspired Design System
[DESIGN.md](https://github.com/VoltAgent/awesome-design-md/blob/main/design-md/stripe/DESIGN.md) extracted from the public [Stripe](https://stripe.com/) website. This is not the official design system. Colors, fonts, and spacing may not be 100% accurate. But it's a good starting point for building something similar.
## Files
| File | Description |
|------|-------------|
| `DESIGN.md` | Complete design system documentation (9 sections) |
| `preview.html` | Interactive design token catalog (light) |
| `preview-dark.html` | Interactive design token catalog (dark) |
Use [DESIGN.md](https://github.com/VoltAgent/awesome-design-md/blob/main/design-md/stripe/DESIGN.md) to use as a reference for AI agents (Claude, Cursor, Stitch) to generate UI that looks like the Stripe design language.
## Preview
A sample landing page built with DESIGN.md. It shows the actual colors, typography, buttons, cards, spacing, and elevation, all in one page.
### Dark Mode
![Stripe Design System — Dark Mode](https://pub-2e4ecbcbc9b24e7b93f1a6ab5b2bc71f.r2.dev/designs/stripe/preview-dark-screenshot.png)
### Light Mode
![Stripe Design System — Light Mode](https://pub-2e4ecbcbc9b24e7b93f1a6ab5b2bc71f.r2.dev/designs/stripe/preview-screenshot.png)

View File

@ -1,428 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Design System Inspired by Stripe</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Source+Code+Pro:wght@400;500;700&display=swap" rel="stylesheet">
<style>
@font-face {
font-family: 'sohne-var';
src: local('sohne-var'), local('Sohne'), local('SF Pro Display');
font-weight: 100 900;
font-display: swap;
}
:root {
--purple: #665efd;
--purple-hover: #7a73ff;
--purple-deep: #533afd;
--purple-light: #3d3a7a;
--purple-mid: #b9b9f9;
--navy: #e8ecf0;
--dark-navy: #c8d0da;
--brand-dark: #0e0f2e;
--white: #0e0f2e;
--ruby: #ea2261;
--magenta: #f96bee;
--magenta-light: rgba(255,215,239,0.1);
--success: #15be53;
--success-text: #4cdf80;
--lemon: #d4a04a;
--slate: #8a95a8;
--dark-slate: #a0aec0;
--border: rgba(255,255,255,0.1);
--border-purple: rgba(185,185,249,0.3);
--border-soft: rgba(214,217,252,0.15);
--shadow-blue: rgba(50,50,93,0.4);
--shadow-dark-blue: rgba(3,3,39,0.5);
--shadow-black: rgba(0,0,0,0.3);
--shadow-ambient: rgba(0,0,0,0.3);
--shadow-soft: rgba(0,0,0,0.2);
--shadow-card: rgba(0,0,0,0.4) 0px 30px 45px -30px, rgba(0,0,0,0.3) 0px 18px 36px -18px;
--shadow-ambient-card: rgba(0,0,0,0.3) 0px 15px 35px 0px;
--shadow-subtle: rgba(0,0,0,0.2) 0px 3px 6px 0px;
--font-primary: 'sohne-var', 'SF Pro Display', -apple-system, system-ui, sans-serif;
--font-mono: 'Source Code Pro', SFMono-Regular, ui-monospace, Menlo, monospace;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: var(--white);
color: var(--navy);
font-family: var(--font-primary);
font-size: 16px; font-weight: 300; line-height: 1.40;
font-feature-settings: "ss01" 1;
-webkit-font-smoothing: antialiased;
}
/* NAV */
.nav {
position: sticky; top: 0; z-index: 100;
display: flex; align-items: center; justify-content: space-between;
padding: 12px 32px;
background: rgba(14,15,46,0.90);
backdrop-filter: blur(12px);
border-bottom: 1px solid var(--border);
border-radius: 0 0 6px 6px;
}
.nav-brand { font-size: 14px; font-weight: 400; color: var(--navy); text-decoration: none; letter-spacing: -0.28px; }
.nav-links { display: flex; gap: 24px; list-style: none; }
.nav-links a { font-size: 14px; font-weight: 400; color: var(--slate); text-decoration: none; transition: color 0.15s; }
.nav-links a:hover { color: var(--navy); }
.nav-cta {
display: inline-block; background: var(--purple); color: #ffffff;
padding: 8px 16px; border-radius: 4px; font-size: 14px; font-weight: 400;
text-decoration: none; transition: background 0.15s;
font-feature-settings: "ss01" 1;
}
.nav-cta:hover { background: var(--purple-hover); }
/* DARK MODE BADGE */
.dark-badge {
position: fixed; top: 16px; right: 16px; z-index: 200;
background: var(--purple); color: #ffffff;
font-size: 11px; font-weight: 400; padding: 4px 10px; border-radius: 4px;
font-feature-settings: "ss01" 1;
}
/* HERO */
.hero { padding: 96px 32px 80px; text-align: center; }
.hero h1 {
font-size: 48px; font-weight: 300; line-height: 1.15;
letter-spacing: -0.96px; color: var(--navy); margin-bottom: 16px;
font-feature-settings: "ss01" 1;
}
.hero p { font-size: 18px; font-weight: 300; line-height: 1.40; color: var(--slate); max-width: 560px; margin: 0 auto 32px; }
.hero-buttons { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; }
.btn-primary {
display: inline-block; background: var(--purple); color: #ffffff;
padding: 10px 20px; border-radius: 4px; border: none;
font-family: var(--font-primary); font-size: 16px; font-weight: 400;
text-decoration: none; cursor: pointer; transition: background 0.15s;
font-feature-settings: "ss01" 1;
}
.btn-primary:hover { background: var(--purple-hover); }
.btn-ghost {
display: inline-block; background: transparent; color: var(--purple-mid);
padding: 10px 20px; border-radius: 4px;
border: 1px solid var(--border-purple);
font-family: var(--font-primary); font-size: 16px; font-weight: 400;
text-decoration: none; cursor: pointer; transition: background 0.15s;
font-feature-settings: "ss01" 1;
}
.btn-ghost:hover { background: rgba(102,94,253,0.1); }
/* SECTIONS */
.section { padding: 64px 32px; max-width: 1080px; margin: 0 auto; }
.section-label { font-family: var(--font-mono); font-size: 12px; font-weight: 500; color: var(--slate); text-transform: uppercase; margin-bottom: 8px; letter-spacing: 0.5px; }
.section-title { font-size: 32px; font-weight: 300; line-height: 1.10; letter-spacing: -0.64px; margin-bottom: 32px; color: var(--navy); }
.section-divider { border: none; border-top: 1px solid var(--border); margin: 0; }
/* COLORS */
.color-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(155px, 1fr)); gap: 12px; margin-bottom: 24px; }
.color-swatch { border-radius: 6px; overflow: hidden; border: 1px solid var(--border); }
.color-swatch-block { height: 72px; width: 100%; }
.color-swatch-info { padding: 10px 12px; }
.color-swatch-name { font-size: 13px; font-weight: 400; margin-bottom: 2px; letter-spacing: -0.26px; color: var(--navy); }
.color-swatch-hex { font-size: 12px; color: var(--slate); font-family: var(--font-mono); }
.color-swatch-role { font-size: 11px; color: var(--slate); margin-top: 3px; font-weight: 300; }
.color-group-label { font-size: 14px; font-weight: 400; color: var(--dark-slate); letter-spacing: -0.28px; margin: 24px 0 10px; }
/* TYPOGRAPHY */
.type-sample { margin-bottom: 28px; padding-bottom: 24px; border-bottom: 1px solid var(--border); }
.type-sample:last-child { border-bottom: none; }
.type-meta { font-family: var(--font-mono); font-size: 12px; font-weight: 500; color: var(--slate); margin-top: 8px; }
/* BUTTONS */
.button-row { display: flex; gap: 16px; flex-wrap: wrap; align-items: center; }
.button-item { text-align: center; }
.button-label { font-size: 12px; font-weight: 300; color: var(--slate); margin-top: 8px; }
.btn-info {
display: inline-block; background: transparent; color: #5ba8d9;
padding: 8px 16px; border-radius: 4px; font-size: 14px; font-weight: 400;
border: 1px solid rgba(91,168,217,0.25); text-decoration: none;
font-feature-settings: "ss01" 1;
}
.btn-neutral {
display: inline-block; background: transparent; color: rgba(232,236,240,0.3);
padding: 8px 16px; border-radius: 4px; font-size: 14px; font-weight: 400;
outline: 1px solid rgba(255,255,255,0.1); border: none; text-decoration: none;
font-feature-settings: "ss01" 1;
}
.badge-success {
display: inline-block; background: rgba(21,190,83,0.15); color: #4cdf80;
padding: 1px 6px; border-radius: 4px; font-size: 10px; font-weight: 300;
border: 1px solid rgba(21,190,83,0.3);
font-feature-settings: "ss01" 1;
}
.badge-neutral {
display: inline-block; background: rgba(255,255,255,0.05); color: var(--slate);
padding: 0px 6px; border-radius: 4px; font-size: 11px; font-weight: 400;
border: 1px solid rgba(255,255,255,0.1);
font-feature-settings: "ss01" 1;
}
/* CARDS */
.card-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 20px; }
.card {
background: rgba(255,255,255,0.03); border-radius: 6px; padding: 24px;
border: 1px solid var(--border);
box-shadow: var(--shadow-subtle);
transition: box-shadow 0.25s ease;
}
.card:hover { box-shadow: var(--shadow-card); }
.card h3 { font-size: 22px; font-weight: 300; letter-spacing: -0.22px; margin-bottom: 8px; color: var(--navy); }
.card p { font-size: 14px; color: var(--slate); line-height: 1.50; font-weight: 300; }
.card-badge { display: inline-block; font-size: 10px; font-weight: 300; padding: 1px 8px; border-radius: 4px; margin-bottom: 12px; }
/* FORMS */
.form-group { margin-bottom: 20px; max-width: 400px; }
.form-label { display: block; font-size: 14px; font-weight: 400; color: var(--dark-slate); margin-bottom: 6px; }
.form-input {
width: 100%; background: rgba(255,255,255,0.05); color: var(--navy);
border: 1px solid var(--border); padding: 10px 12px; border-radius: 4px;
font-family: var(--font-primary); font-size: 14px; font-weight: 300; outline: none;
font-feature-settings: "ss01" 1;
transition: border-color 0.15s;
}
.form-input:focus { border-color: var(--purple); }
.form-input--focus { border-color: var(--purple); box-shadow: 0 0 0 1px var(--purple); }
.form-input--error { border-color: var(--ruby); box-shadow: 0 0 0 1px var(--ruby); }
.form-textarea {
width: 100%; min-height: 80px; background: rgba(255,255,255,0.05); color: var(--navy);
border: 1px solid var(--border); padding: 10px 12px; border-radius: 4px;
font-family: var(--font-primary); font-size: 14px; font-weight: 300; resize: vertical; outline: none;
font-feature-settings: "ss01" 1;
}
.form-state-label { font-size: 11px; color: var(--slate); margin-top: 4px; font-weight: 300; }
/* SPACING */
.spacing-row { display: flex; align-items: flex-end; gap: 10px; flex-wrap: wrap; margin-bottom: 24px; }
.spacing-item { text-align: center; }
.spacing-block { background: var(--purple); border-radius: 2px; margin-bottom: 6px; height: 28px; }
.spacing-value { font-family: var(--font-mono); font-size: 11px; font-weight: 500; color: var(--slate); }
/* RADIUS */
.radius-row { display: flex; gap: 14px; flex-wrap: wrap; align-items: center; }
.radius-item { text-align: center; }
.radius-box { width: 64px; height: 64px; background: var(--purple); margin-bottom: 6px; }
.radius-label { font-family: var(--font-mono); font-size: 11px; font-weight: 500; color: var(--slate); }
.radius-context { font-size: 10px; color: var(--slate); font-weight: 300; }
/* ELEVATION */
.elevation-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; }
.elevation-card { background: rgba(255,255,255,0.03); border-radius: 6px; padding: 20px; text-align: center; }
.elevation-label { font-size: 14px; font-weight: 400; letter-spacing: -0.28px; margin-bottom: 4px; color: var(--navy); }
.elevation-desc { font-family: var(--font-mono); font-size: 11px; color: var(--slate); }
/* FOOTER */
.footer { padding: 32px; text-align: center; border-top: 1px solid var(--border); font-size: 13px; color: var(--slate); font-weight: 300; }
.footer a { color: var(--purple-mid); text-decoration: underline; }
@media (max-width: 768px) {
.hero h1 { font-size: 32px; letter-spacing: -0.64px; }
.nav-links { display: none; }
.section { padding: 48px 20px; }
.card-grid { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<nav class="nav">
<span class="nav-brand">awesome-design-md</span>
<ul class="nav-links">
<li><a href="#colors">Colors</a></li>
<li><a href="#typography">Typography</a></li>
<li><a href="#buttons">Buttons</a></li>
<li><a href="#cards">Cards</a></li>
<li><a href="#forms">Forms</a></li>
<li><a href="#spacing">Spacing</a></li>
</ul>
<a class="nav-cta" href="#">Start now</a>
</nav>
<div class="dark-badge">Dark Mode</div>
<section class="hero">
<h1>Design System<br>Inspired by Stripe</h1>
<p>A design token catalog generated from DESIGN.md. Every color, font, component, and spacing value -- visualized.</p>
<div class="hero-buttons">
<a class="btn-primary" href="#">Start now</a>
<a class="btn-ghost" href="#">View Documentation</a>
</div>
</section>
<hr class="section-divider">
<section class="section" id="colors">
<div class="section-label">01 / Colors</div>
<h2 class="section-title">Color Palette</h2>
<div class="color-group-label">Primary</div>
<div class="color-grid">
<div class="color-swatch"><div class="color-swatch-block" style="background:#533afd"></div><div class="color-swatch-info"><div class="color-swatch-name">Stripe Purple</div><div class="color-swatch-hex">#533afd</div><div class="color-swatch-role">Primary brand, CTA</div></div></div>
<div class="color-swatch"><div class="color-swatch-block" style="background:#061b31"></div><div class="color-swatch-info"><div class="color-swatch-name">Deep Navy</div><div class="color-swatch-hex">#061b31</div><div class="color-swatch-role">Headings</div></div></div>
<div class="color-swatch"><div class="color-swatch-block" style="background:#ffffff; border-bottom:1px solid rgba(255,255,255,0.1)"></div><div class="color-swatch-info"><div class="color-swatch-name">White</div><div class="color-swatch-hex">#ffffff</div><div class="color-swatch-role">Page background</div></div></div>
</div>
<div class="color-group-label">Brand & Dark</div>
<div class="color-grid">
<div class="color-swatch"><div class="color-swatch-block" style="background:#1c1e54"></div><div class="color-swatch-info"><div class="color-swatch-name">Brand Dark</div><div class="color-swatch-hex">#1c1e54</div><div class="color-swatch-role">Dark sections</div></div></div>
<div class="color-swatch"><div class="color-swatch-block" style="background:#0d253d"></div><div class="color-swatch-info"><div class="color-swatch-name">Dark Navy</div><div class="color-swatch-hex">#0d253d</div><div class="color-swatch-role">Darkest neutral</div></div></div>
</div>
<div class="color-group-label">Accent</div>
<div class="color-grid">
<div class="color-swatch"><div class="color-swatch-block" style="background:#ea2261"></div><div class="color-swatch-info"><div class="color-swatch-name">Ruby</div><div class="color-swatch-hex">#ea2261</div><div class="color-swatch-role">Accent, alerts</div></div></div>
<div class="color-swatch"><div class="color-swatch-block" style="background:#f96bee"></div><div class="color-swatch-info"><div class="color-swatch-name">Magenta</div><div class="color-swatch-hex">#f96bee</div><div class="color-swatch-role">Gradients, decorative</div></div></div>
<div class="color-swatch"><div class="color-swatch-block" style="background:#ffd7ef"></div><div class="color-swatch-info"><div class="color-swatch-name">Magenta Light</div><div class="color-swatch-hex">#ffd7ef</div><div class="color-swatch-role">Tinted surface</div></div></div>
</div>
<div class="color-group-label">Interactive Purple Scale</div>
<div class="color-grid">
<div class="color-swatch"><div class="color-swatch-block" style="background:#4434d4"></div><div class="color-swatch-info"><div class="color-swatch-name">Purple Hover</div><div class="color-swatch-hex">#4434d4</div><div class="color-swatch-role">CTA hover state</div></div></div>
<div class="color-swatch"><div class="color-swatch-block" style="background:#665efd"></div><div class="color-swatch-info"><div class="color-swatch-name">Purple Mid</div><div class="color-swatch-hex">#665efd</div><div class="color-swatch-role">Range selectors</div></div></div>
<div class="color-swatch"><div class="color-swatch-block" style="background:#b9b9f9"></div><div class="color-swatch-info"><div class="color-swatch-name">Purple Light</div><div class="color-swatch-hex">#b9b9f9</div><div class="color-swatch-role">Subdued hover bg</div></div></div>
<div class="color-swatch"><div class="color-swatch-block" style="background:#2e2b8c"></div><div class="color-swatch-info"><div class="color-swatch-name">Purple Deep</div><div class="color-swatch-hex">#2e2b8c</div><div class="color-swatch-role">Icon hover</div></div></div>
</div>
<div class="color-group-label">Neutral & Status</div>
<div class="color-grid">
<div class="color-swatch"><div class="color-swatch-block" style="background:#273951"></div><div class="color-swatch-info"><div class="color-swatch-name">Dark Slate</div><div class="color-swatch-hex">#273951</div><div class="color-swatch-role">Labels</div></div></div>
<div class="color-swatch"><div class="color-swatch-block" style="background:#64748d"></div><div class="color-swatch-info"><div class="color-swatch-name">Slate</div><div class="color-swatch-hex">#64748d</div><div class="color-swatch-role">Body text</div></div></div>
<div class="color-swatch"><div class="color-swatch-block" style="background:#e5edf5"></div><div class="color-swatch-info"><div class="color-swatch-name">Border</div><div class="color-swatch-hex">#e5edf5</div><div class="color-swatch-role">Default border</div></div></div>
<div class="color-swatch"><div class="color-swatch-block" style="background:#15be53"></div><div class="color-swatch-info"><div class="color-swatch-name">Success</div><div class="color-swatch-hex">#15be53</div><div class="color-swatch-role">Status, badges</div></div></div>
<div class="color-swatch"><div class="color-swatch-block" style="background:#9b6829"></div><div class="color-swatch-info"><div class="color-swatch-name">Lemon</div><div class="color-swatch-hex">#9b6829</div><div class="color-swatch-role">Warning accent</div></div></div>
</div>
<div class="color-group-label">Border & Surface</div>
<div class="color-grid">
<div class="color-swatch"><div class="color-swatch-block" style="background:#d6d9fc"></div><div class="color-swatch-info"><div class="color-swatch-name">Border Soft</div><div class="color-swatch-hex">#d6d9fc</div><div class="color-swatch-role">Purple-tinted border</div></div></div>
<div class="color-swatch"><div class="color-swatch-block" style="background:#362baa"></div><div class="color-swatch-info"><div class="color-swatch-name">Dashed Border</div><div class="color-swatch-hex">#362baa</div><div class="color-swatch-role">Drop zones</div></div></div>
</div>
</section>
<hr class="section-divider">
<section class="section" id="typography">
<div class="section-label">02 / Typography</div>
<h2 class="section-title">Typography Scale</h2>
<div class="type-sample"><div style="font-size:56px; font-weight:300; line-height:1.03; letter-spacing:-1.4px; color:var(--navy);">Display Hero</div><div class="type-meta">Display Hero -- 56px / 300 / 1.03 / -1.4px / sohne-var "ss01"</div></div>
<div class="type-sample"><div style="font-size:48px; font-weight:300; line-height:1.15; letter-spacing:-0.96px; color:var(--navy);">Display Large</div><div class="type-meta">Display Large -- 48px / 300 / 1.15 / -0.96px / sohne-var "ss01"</div></div>
<div class="type-sample"><div style="font-size:32px; font-weight:300; line-height:1.10; letter-spacing:-0.64px; color:var(--navy);">Section Heading</div><div class="type-meta">Section Heading -- 32px / 300 / 1.10 / -0.64px / sohne-var "ss01"</div></div>
<div class="type-sample"><div style="font-size:26px; font-weight:300; line-height:1.12; letter-spacing:-0.26px; color:var(--navy);">Sub-heading Large</div><div class="type-meta">Sub-heading -- 26px / 300 / 1.12 / -0.26px / sohne-var "ss01"</div></div>
<div class="type-sample"><div style="font-size:22px; font-weight:300; line-height:1.10; letter-spacing:-0.22px; color:var(--navy);">Sub-heading</div><div class="type-meta">Sub-heading -- 22px / 300 / 1.10 / -0.22px / sohne-var "ss01"</div></div>
<div class="type-sample"><div style="font-size:18px; font-weight:300; line-height:1.40; color:var(--slate);">Body Large -- Financial infrastructure for the internet. Millions of companies use Stripe to accept payments.</div><div class="type-meta">Body Large -- 18px / 300 / 1.40 / normal / sohne-var "ss01"</div></div>
<div class="type-sample"><div style="font-size:16px; font-weight:300; line-height:1.40; color:var(--slate);">Body -- Standard reading text for descriptions and content.</div><div class="type-meta">Body -- 16px / 300 / 1.40 / normal / sohne-var "ss01"</div></div>
<div class="type-sample"><div style="font-size:16px; font-weight:400; line-height:1.00; color:var(--navy);">Button Text</div><div class="type-meta">Button -- 16px / 400 / 1.00 / normal / sohne-var "ss01"</div></div>
<div class="type-sample"><div style="font-size:14px; font-weight:400; line-height:1.00; color:var(--navy);">Link / Navigation</div><div class="type-meta">Link -- 14px / 400 / 1.00 / normal / sohne-var "ss01"</div></div>
<div class="type-sample"><div style="font-size:12px; font-weight:300; line-height:1.45; color:var(--slate);">Caption -- Small labels and metadata</div><div class="type-meta">Caption -- 12px / 300 / 1.45 / normal / sohne-var "ss01"</div></div>
<div class="type-sample"><div style="font-size:12px; font-weight:300; line-height:1.33; letter-spacing:-0.36px; color:var(--slate); font-feature-settings:'tnum' 1;">$1,234,567.89</div><div class="type-meta">Tabular Numbers -- 12px / 300 / 1.33 / -0.36px / sohne-var "tnum"</div></div>
<div class="type-sample"><div style="font-family:var(--font-mono); font-size:12px; font-weight:500; line-height:2.00;">const stripe = require('stripe')('sk_test_...');</div><div class="type-meta">Code Body -- 12px / 500 / 2.00 / Source Code Pro</div></div>
<div class="type-sample"><div style="font-family:var(--font-mono); font-size:12px; font-weight:500; line-height:2.00; text-transform:uppercase;">API VERSION</div><div class="type-meta">Code Label -- 12px / 500 / uppercase / Source Code Pro</div></div>
</section>
<hr class="section-divider">
<section class="section" id="buttons">
<div class="section-label">03 / Buttons</div>
<h2 class="section-title">Button Variants</h2>
<div class="button-row">
<div class="button-item"><a class="btn-primary" href="#">Start now</a><div class="button-label">Primary Purple</div></div>
<div class="button-item"><a class="btn-ghost" href="#">Documentation</a><div class="button-label">Ghost / Outlined</div></div>
<div class="button-item"><a class="btn-info" href="#">Learn more</a><div class="button-label">Transparent Info</div></div>
<div class="button-item"><a class="btn-neutral" href="#">Disabled</a><div class="button-label">Neutral Ghost</div></div>
<div class="button-item"><span class="badge-success">Active</span><div class="button-label">Success Badge</div></div>
<div class="button-item"><span class="badge-neutral">v2024-12</span><div class="button-label">Neutral Badge</div></div>
</div>
</section>
<hr class="section-divider">
<section class="section" id="cards">
<div class="section-label">04 / Cards</div>
<h2 class="section-title">Card Examples</h2>
<div class="card-grid">
<div class="card">
<div class="card-badge" style="background:rgba(83,58,253,0.15); color:var(--purple-mid);">Payments</div>
<h3>Online Payments</h3>
<p>Accept payments online with a fully integrated suite of payment products. Optimized for conversion with pre-built checkout pages.</p>
</div>
<div class="card" style="box-shadow: var(--shadow-card);">
<div class="card-badge" style="background:rgba(234,34,97,0.15); color:var(--ruby);">Elevated</div>
<h3>Revenue Recognition</h3>
<p>Automate your revenue reporting. Card shown with full shadow stack for elevated importance.</p>
</div>
<div class="card">
<div class="card-badge" style="background:rgba(21,190,83,0.1); color:var(--success-text);">Connect</div>
<h3>Platform Payments</h3>
<p>Build a marketplace or platform with multi-party payments, instant payouts, and flexible revenue sharing.</p>
</div>
</div>
</section>
<hr class="section-divider">
<section class="section" id="forms">
<div class="section-label">05 / Forms</div>
<h2 class="section-title">Form Elements</h2>
<div class="form-group"><label class="form-label">API Key Name</label><input class="form-input" type="text" placeholder="my-api-key"><div class="form-state-label">Default</div></div>
<div class="form-group"><label class="form-label">Webhook URL</label><input class="form-input form-input--focus" type="text" value="https://api.example.com/webhook"><div class="form-state-label">Focus (purple ring)</div></div>
<div class="form-group"><label class="form-label">Email Address</label><input class="form-input form-input--error" type="text" value="invalid-email"><div class="form-state-label">Error (ruby ring)</div></div>
<div class="form-group"><label class="form-label">Metadata</label><textarea class="form-textarea" placeholder='{"key": "value"}'></textarea></div>
</section>
<hr class="section-divider">
<section class="section" id="spacing">
<div class="section-label">06 / Spacing</div>
<h2 class="section-title">Spacing Scale</h2>
<div class="spacing-row">
<div class="spacing-item"><div class="spacing-block" style="width:2px"></div><div class="spacing-value">2</div></div>
<div class="spacing-item"><div class="spacing-block" style="width:4px"></div><div class="spacing-value">4</div></div>
<div class="spacing-item"><div class="spacing-block" style="width:6px"></div><div class="spacing-value">6</div></div>
<div class="spacing-item"><div class="spacing-block" style="width:8px"></div><div class="spacing-value">8</div></div>
<div class="spacing-item"><div class="spacing-block" style="width:10px"></div><div class="spacing-value">10</div></div>
<div class="spacing-item"><div class="spacing-block" style="width:12px"></div><div class="spacing-value">12</div></div>
<div class="spacing-item"><div class="spacing-block" style="width:14px"></div><div class="spacing-value">14</div></div>
<div class="spacing-item"><div class="spacing-block" style="width:16px"></div><div class="spacing-value">16</div></div>
<div class="spacing-item"><div class="spacing-block" style="width:18px"></div><div class="spacing-value">18</div></div>
<div class="spacing-item"><div class="spacing-block" style="width:20px"></div><div class="spacing-value">20</div></div>
</div>
</section>
<hr class="section-divider">
<section class="section" id="radius">
<div class="section-label">07 / Radius</div>
<h2 class="section-title">Border Radius</h2>
<div class="radius-row">
<div class="radius-item"><div class="radius-box" style="border-radius:1px"></div><div class="radius-label">1px</div><div class="radius-context">Micro</div></div>
<div class="radius-item"><div class="radius-box" style="border-radius:4px"></div><div class="radius-label">4px</div><div class="radius-context">Buttons, inputs</div></div>
<div class="radius-item"><div class="radius-box" style="border-radius:5px"></div><div class="radius-label">5px</div><div class="radius-context">Standard cards</div></div>
<div class="radius-item"><div class="radius-box" style="border-radius:6px"></div><div class="radius-label">6px</div><div class="radius-context">Nav, large cards</div></div>
<div class="radius-item"><div class="radius-box" style="border-radius:8px"></div><div class="radius-label">8px</div><div class="radius-context">Featured</div></div>
</div>
</section>
<hr class="section-divider">
<section class="section" id="elevation">
<div class="section-label">08 / Elevation</div>
<h2 class="section-title">Elevation</h2>
<div class="elevation-grid">
<div class="elevation-card" style="border: 1px solid var(--border);"><div class="elevation-label">Level 0: Flat</div><div class="elevation-desc">No shadow</div></div>
<div class="elevation-card" style="box-shadow: rgba(0,0,0,0.2) 0px 3px 6px 0px;"><div class="elevation-label">Level 1: Subtle</div><div class="elevation-desc">Ambient soft</div></div>
<div class="elevation-card" style="box-shadow: rgba(0,0,0,0.3) 0px 15px 35px 0px;"><div class="elevation-label">Level 2: Standard</div><div class="elevation-desc">Ambient card</div></div>
<div class="elevation-card" style="box-shadow: rgba(0,0,0,0.4) 0px 30px 45px -30px, rgba(0,0,0,0.3) 0px 18px 36px -18px;"><div class="elevation-label">Level 3: Elevated</div><div class="elevation-desc">Dual layer deep</div></div>
<div class="elevation-card" style="box-shadow: rgba(0,0,0,0.5) 0px 14px 21px -14px, rgba(0,0,0,0.3) 0px 8px 17px -8px;"><div class="elevation-label">Level 4: Deep</div><div class="elevation-desc">Dark deep</div></div>
<div class="elevation-card" style="box-shadow: 0 0 0 2px var(--purple);"><div class="elevation-label">Focus</div><div class="elevation-desc">Purple ring</div></div>
</div>
</section>
<footer class="footer">Maintained by <a href="https://github.com/VoltAgent/voltagent" target="_blank" rel="noopener noreferrer" style="text-decoration:none;"><img src="https://github.com/VoltAgent.png?size=32" alt="VoltAgent" width="14" height="14" style="border-radius:3px;vertical-align:-2px;margin-right:3px;">VoltAgent</a> team</footer>
</body>
</html>

View File

@ -1,419 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Design System Inspired by Stripe</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Source+Code+Pro:wght@400;500;700&display=swap" rel="stylesheet">
<style>
@font-face {
font-family: 'sohne-var';
src: local('sohne-var'), local('Sohne'), local('SF Pro Display');
font-weight: 100 900;
font-display: swap;
}
:root {
--purple: #533afd;
--purple-hover: #4434d4;
--purple-deep: #2e2b8c;
--purple-light: #b9b9f9;
--purple-mid: #665efd;
--navy: #061b31;
--dark-navy: #0d253d;
--brand-dark: #1c1e54;
--white: #ffffff;
--ruby: #ea2261;
--magenta: #f96bee;
--magenta-light: #ffd7ef;
--success: #15be53;
--success-text: #108c3d;
--lemon: #9b6829;
--slate: #64748d;
--dark-slate: #273951;
--border: #e5edf5;
--border-purple: #b9b9f9;
--border-soft: #d6d9fc;
--shadow-blue: rgba(50,50,93,0.25);
--shadow-dark-blue: rgba(3,3,39,0.25);
--shadow-black: rgba(0,0,0,0.1);
--shadow-ambient: rgba(23,23,23,0.08);
--shadow-soft: rgba(23,23,23,0.06);
--shadow-card: rgba(50,50,93,0.25) 0px 30px 45px -30px, rgba(0,0,0,0.1) 0px 18px 36px -18px;
--shadow-ambient-card: rgba(23,23,23,0.08) 0px 15px 35px 0px;
--shadow-subtle: rgba(23,23,23,0.06) 0px 3px 6px 0px;
--font-primary: 'sohne-var', 'SF Pro Display', -apple-system, system-ui, sans-serif;
--font-mono: 'Source Code Pro', SFMono-Regular, ui-monospace, Menlo, monospace;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: var(--white);
color: var(--navy);
font-family: var(--font-primary);
font-size: 16px; font-weight: 300; line-height: 1.40;
font-feature-settings: "ss01" 1;
-webkit-font-smoothing: antialiased;
}
/* NAV */
.nav {
position: sticky; top: 0; z-index: 100;
display: flex; align-items: center; justify-content: space-between;
padding: 12px 32px;
background: rgba(255,255,255,0.90);
backdrop-filter: blur(12px);
border-bottom: 1px solid var(--border);
border-radius: 0 0 6px 6px;
}
.nav-brand { font-size: 14px; font-weight: 400; color: var(--navy); text-decoration: none; letter-spacing: -0.28px; }
.nav-links { display: flex; gap: 24px; list-style: none; }
.nav-links a { font-size: 14px; font-weight: 400; color: var(--slate); text-decoration: none; transition: color 0.15s; }
.nav-links a:hover { color: var(--navy); }
.nav-cta {
display: inline-block; background: var(--purple); color: var(--white);
padding: 8px 16px; border-radius: 4px; font-size: 14px; font-weight: 400;
text-decoration: none; transition: background 0.15s;
font-feature-settings: "ss01" 1;
}
.nav-cta:hover { background: var(--purple-hover); }
/* HERO */
.hero { padding: 96px 32px 80px; text-align: center; }
.hero h1 {
font-size: 48px; font-weight: 300; line-height: 1.15;
letter-spacing: -0.96px; color: var(--navy); margin-bottom: 16px;
font-feature-settings: "ss01" 1;
}
.hero p { font-size: 18px; font-weight: 300; line-height: 1.40; color: var(--slate); max-width: 560px; margin: 0 auto 32px; }
.hero-buttons { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; }
.btn-primary {
display: inline-block; background: var(--purple); color: var(--white);
padding: 10px 20px; border-radius: 4px; border: none;
font-family: var(--font-primary); font-size: 16px; font-weight: 400;
text-decoration: none; cursor: pointer; transition: background 0.15s;
font-feature-settings: "ss01" 1;
}
.btn-primary:hover { background: var(--purple-hover); }
.btn-ghost {
display: inline-block; background: transparent; color: var(--purple);
padding: 10px 20px; border-radius: 4px;
border: 1px solid var(--border-purple);
font-family: var(--font-primary); font-size: 16px; font-weight: 400;
text-decoration: none; cursor: pointer; transition: background 0.15s;
font-feature-settings: "ss01" 1;
}
.btn-ghost:hover { background: rgba(83,58,253,0.05); }
/* SECTIONS */
.section { padding: 64px 32px; max-width: 1080px; margin: 0 auto; }
.section-label { font-family: var(--font-mono); font-size: 12px; font-weight: 500; color: var(--slate); text-transform: uppercase; margin-bottom: 8px; letter-spacing: 0.5px; }
.section-title { font-size: 32px; font-weight: 300; line-height: 1.10; letter-spacing: -0.64px; margin-bottom: 32px; color: var(--navy); }
.section-divider { border: none; border-top: 1px solid var(--border); margin: 0; }
/* COLORS */
.color-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(155px, 1fr)); gap: 12px; margin-bottom: 24px; }
.color-swatch { border-radius: 6px; overflow: hidden; border: 1px solid var(--border); }
.color-swatch-block { height: 72px; width: 100%; }
.color-swatch-info { padding: 10px 12px; }
.color-swatch-name { font-size: 13px; font-weight: 400; margin-bottom: 2px; letter-spacing: -0.26px; color: var(--navy); }
.color-swatch-hex { font-size: 12px; color: var(--slate); font-family: var(--font-mono); }
.color-swatch-role { font-size: 11px; color: var(--slate); margin-top: 3px; font-weight: 300; }
.color-group-label { font-size: 14px; font-weight: 400; color: var(--dark-slate); letter-spacing: -0.28px; margin: 24px 0 10px; }
/* TYPOGRAPHY */
.type-sample { margin-bottom: 28px; padding-bottom: 24px; border-bottom: 1px solid var(--border); }
.type-sample:last-child { border-bottom: none; }
.type-meta { font-family: var(--font-mono); font-size: 12px; font-weight: 500; color: var(--slate); margin-top: 8px; }
/* BUTTONS */
.button-row { display: flex; gap: 16px; flex-wrap: wrap; align-items: center; }
.button-item { text-align: center; }
.button-label { font-size: 12px; font-weight: 300; color: var(--slate); margin-top: 8px; }
.btn-info {
display: inline-block; background: transparent; color: #2874ad;
padding: 8px 16px; border-radius: 4px; font-size: 14px; font-weight: 400;
border: 1px solid rgba(43,145,223,0.2); text-decoration: none;
font-feature-settings: "ss01" 1;
}
.btn-neutral {
display: inline-block; background: transparent; color: rgba(16,16,16,0.3);
padding: 8px 16px; border-radius: 4px; font-size: 14px; font-weight: 400;
outline: 1px solid rgb(212,222,233); border: none; text-decoration: none;
font-feature-settings: "ss01" 1;
}
.badge-success {
display: inline-block; background: rgba(21,190,83,0.2); color: #108c3d;
padding: 1px 6px; border-radius: 4px; font-size: 10px; font-weight: 300;
border: 1px solid rgba(21,190,83,0.4);
font-feature-settings: "ss01" 1;
}
.badge-neutral {
display: inline-block; background: #ffffff; color: #000000;
padding: 0px 6px; border-radius: 4px; font-size: 11px; font-weight: 400;
border: 1px solid #f6f9fc;
font-feature-settings: "ss01" 1;
}
/* CARDS */
.card-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 20px; }
.card {
background: var(--white); border-radius: 6px; padding: 24px;
border: 1px solid var(--border);
box-shadow: var(--shadow-subtle);
transition: box-shadow 0.25s ease;
}
.card:hover { box-shadow: var(--shadow-card); }
.card h3 { font-size: 22px; font-weight: 300; letter-spacing: -0.22px; margin-bottom: 8px; color: var(--navy); }
.card p { font-size: 14px; color: var(--slate); line-height: 1.50; font-weight: 300; }
.card-badge { display: inline-block; font-size: 10px; font-weight: 300; padding: 1px 8px; border-radius: 4px; margin-bottom: 12px; }
/* FORMS */
.form-group { margin-bottom: 20px; max-width: 400px; }
.form-label { display: block; font-size: 14px; font-weight: 400; color: var(--dark-slate); margin-bottom: 6px; }
.form-input {
width: 100%; background: var(--white); color: var(--navy);
border: 1px solid var(--border); padding: 10px 12px; border-radius: 4px;
font-family: var(--font-primary); font-size: 14px; font-weight: 300; outline: none;
font-feature-settings: "ss01" 1;
transition: border-color 0.15s;
}
.form-input:focus { border-color: var(--purple); }
.form-input--focus { border-color: var(--purple); box-shadow: 0 0 0 1px var(--purple); }
.form-input--error { border-color: var(--ruby); box-shadow: 0 0 0 1px var(--ruby); }
.form-textarea {
width: 100%; min-height: 80px; background: var(--white); color: var(--navy);
border: 1px solid var(--border); padding: 10px 12px; border-radius: 4px;
font-family: var(--font-primary); font-size: 14px; font-weight: 300; resize: vertical; outline: none;
font-feature-settings: "ss01" 1;
}
.form-state-label { font-size: 11px; color: var(--slate); margin-top: 4px; font-weight: 300; }
/* SPACING */
.spacing-row { display: flex; align-items: flex-end; gap: 10px; flex-wrap: wrap; margin-bottom: 24px; }
.spacing-item { text-align: center; }
.spacing-block { background: var(--purple); border-radius: 2px; margin-bottom: 6px; height: 28px; }
.spacing-value { font-family: var(--font-mono); font-size: 11px; font-weight: 500; color: var(--slate); }
/* RADIUS */
.radius-row { display: flex; gap: 14px; flex-wrap: wrap; align-items: center; }
.radius-item { text-align: center; }
.radius-box { width: 64px; height: 64px; background: var(--purple); margin-bottom: 6px; }
.radius-label { font-family: var(--font-mono); font-size: 11px; font-weight: 500; color: var(--slate); }
.radius-context { font-size: 10px; color: var(--slate); font-weight: 300; }
/* ELEVATION */
.elevation-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; }
.elevation-card { background: var(--white); border-radius: 6px; padding: 20px; text-align: center; }
.elevation-label { font-size: 14px; font-weight: 400; letter-spacing: -0.28px; margin-bottom: 4px; color: var(--navy); }
.elevation-desc { font-family: var(--font-mono); font-size: 11px; color: var(--slate); }
/* FOOTER */
.footer { padding: 32px; text-align: center; border-top: 1px solid var(--border); font-size: 13px; color: var(--slate); font-weight: 300; }
.footer a { color: var(--purple); text-decoration: underline; }
@media (max-width: 768px) {
.hero h1 { font-size: 32px; letter-spacing: -0.64px; }
.nav-links { display: none; }
.section { padding: 48px 20px; }
.card-grid { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<nav class="nav">
<span class="nav-brand">awesome-design-md</span>
<ul class="nav-links">
<li><a href="#colors">Colors</a></li>
<li><a href="#typography">Typography</a></li>
<li><a href="#buttons">Buttons</a></li>
<li><a href="#cards">Cards</a></li>
<li><a href="#forms">Forms</a></li>
<li><a href="#spacing">Spacing</a></li>
</ul>
<a class="nav-cta" href="#">Start now</a>
</nav>
<section class="hero">
<h1>Design System<br>Inspired by Stripe</h1>
<p>A design token catalog generated from DESIGN.md. Every color, font, component, and spacing value -- visualized.</p>
<div class="hero-buttons">
<a class="btn-primary" href="#">Start now</a>
<a class="btn-ghost" href="#">View Documentation</a>
</div>
</section>
<hr class="section-divider">
<section class="section" id="colors">
<div class="section-label">01 / Colors</div>
<h2 class="section-title">Color Palette</h2>
<div class="color-group-label">Primary</div>
<div class="color-grid">
<div class="color-swatch"><div class="color-swatch-block" style="background:#533afd"></div><div class="color-swatch-info"><div class="color-swatch-name">Stripe Purple</div><div class="color-swatch-hex">#533afd</div><div class="color-swatch-role">Primary brand, CTA</div></div></div>
<div class="color-swatch"><div class="color-swatch-block" style="background:#061b31"></div><div class="color-swatch-info"><div class="color-swatch-name">Deep Navy</div><div class="color-swatch-hex">#061b31</div><div class="color-swatch-role">Headings</div></div></div>
<div class="color-swatch"><div class="color-swatch-block" style="background:#ffffff; border-bottom:1px solid #e5edf5"></div><div class="color-swatch-info"><div class="color-swatch-name">White</div><div class="color-swatch-hex">#ffffff</div><div class="color-swatch-role">Page background</div></div></div>
</div>
<div class="color-group-label">Brand & Dark</div>
<div class="color-grid">
<div class="color-swatch"><div class="color-swatch-block" style="background:#1c1e54"></div><div class="color-swatch-info"><div class="color-swatch-name">Brand Dark</div><div class="color-swatch-hex">#1c1e54</div><div class="color-swatch-role">Dark sections</div></div></div>
<div class="color-swatch"><div class="color-swatch-block" style="background:#0d253d"></div><div class="color-swatch-info"><div class="color-swatch-name">Dark Navy</div><div class="color-swatch-hex">#0d253d</div><div class="color-swatch-role">Darkest neutral</div></div></div>
</div>
<div class="color-group-label">Accent</div>
<div class="color-grid">
<div class="color-swatch"><div class="color-swatch-block" style="background:#ea2261"></div><div class="color-swatch-info"><div class="color-swatch-name">Ruby</div><div class="color-swatch-hex">#ea2261</div><div class="color-swatch-role">Accent, alerts</div></div></div>
<div class="color-swatch"><div class="color-swatch-block" style="background:#f96bee"></div><div class="color-swatch-info"><div class="color-swatch-name">Magenta</div><div class="color-swatch-hex">#f96bee</div><div class="color-swatch-role">Gradients, decorative</div></div></div>
<div class="color-swatch"><div class="color-swatch-block" style="background:#ffd7ef"></div><div class="color-swatch-info"><div class="color-swatch-name">Magenta Light</div><div class="color-swatch-hex">#ffd7ef</div><div class="color-swatch-role">Tinted surface</div></div></div>
</div>
<div class="color-group-label">Interactive Purple Scale</div>
<div class="color-grid">
<div class="color-swatch"><div class="color-swatch-block" style="background:#4434d4"></div><div class="color-swatch-info"><div class="color-swatch-name">Purple Hover</div><div class="color-swatch-hex">#4434d4</div><div class="color-swatch-role">CTA hover state</div></div></div>
<div class="color-swatch"><div class="color-swatch-block" style="background:#665efd"></div><div class="color-swatch-info"><div class="color-swatch-name">Purple Mid</div><div class="color-swatch-hex">#665efd</div><div class="color-swatch-role">Range selectors</div></div></div>
<div class="color-swatch"><div class="color-swatch-block" style="background:#b9b9f9"></div><div class="color-swatch-info"><div class="color-swatch-name">Purple Light</div><div class="color-swatch-hex">#b9b9f9</div><div class="color-swatch-role">Subdued hover bg</div></div></div>
<div class="color-swatch"><div class="color-swatch-block" style="background:#2e2b8c"></div><div class="color-swatch-info"><div class="color-swatch-name">Purple Deep</div><div class="color-swatch-hex">#2e2b8c</div><div class="color-swatch-role">Icon hover</div></div></div>
</div>
<div class="color-group-label">Neutral & Status</div>
<div class="color-grid">
<div class="color-swatch"><div class="color-swatch-block" style="background:#273951"></div><div class="color-swatch-info"><div class="color-swatch-name">Dark Slate</div><div class="color-swatch-hex">#273951</div><div class="color-swatch-role">Labels</div></div></div>
<div class="color-swatch"><div class="color-swatch-block" style="background:#64748d"></div><div class="color-swatch-info"><div class="color-swatch-name">Slate</div><div class="color-swatch-hex">#64748d</div><div class="color-swatch-role">Body text</div></div></div>
<div class="color-swatch"><div class="color-swatch-block" style="background:#e5edf5"></div><div class="color-swatch-info"><div class="color-swatch-name">Border</div><div class="color-swatch-hex">#e5edf5</div><div class="color-swatch-role">Default border</div></div></div>
<div class="color-swatch"><div class="color-swatch-block" style="background:#15be53"></div><div class="color-swatch-info"><div class="color-swatch-name">Success</div><div class="color-swatch-hex">#15be53</div><div class="color-swatch-role">Status, badges</div></div></div>
<div class="color-swatch"><div class="color-swatch-block" style="background:#9b6829"></div><div class="color-swatch-info"><div class="color-swatch-name">Lemon</div><div class="color-swatch-hex">#9b6829</div><div class="color-swatch-role">Warning accent</div></div></div>
</div>
<div class="color-group-label">Border & Surface</div>
<div class="color-grid">
<div class="color-swatch"><div class="color-swatch-block" style="background:#d6d9fc"></div><div class="color-swatch-info"><div class="color-swatch-name">Border Soft</div><div class="color-swatch-hex">#d6d9fc</div><div class="color-swatch-role">Purple-tinted border</div></div></div>
<div class="color-swatch"><div class="color-swatch-block" style="background:#362baa"></div><div class="color-swatch-info"><div class="color-swatch-name">Dashed Border</div><div class="color-swatch-hex">#362baa</div><div class="color-swatch-role">Drop zones</div></div></div>
</div>
</section>
<hr class="section-divider">
<section class="section" id="typography">
<div class="section-label">02 / Typography</div>
<h2 class="section-title">Typography Scale</h2>
<div class="type-sample"><div style="font-size:56px; font-weight:300; line-height:1.03; letter-spacing:-1.4px; color:var(--navy);">Display Hero</div><div class="type-meta">Display Hero -- 56px / 300 / 1.03 / -1.4px / sohne-var "ss01"</div></div>
<div class="type-sample"><div style="font-size:48px; font-weight:300; line-height:1.15; letter-spacing:-0.96px; color:var(--navy);">Display Large</div><div class="type-meta">Display Large -- 48px / 300 / 1.15 / -0.96px / sohne-var "ss01"</div></div>
<div class="type-sample"><div style="font-size:32px; font-weight:300; line-height:1.10; letter-spacing:-0.64px; color:var(--navy);">Section Heading</div><div class="type-meta">Section Heading -- 32px / 300 / 1.10 / -0.64px / sohne-var "ss01"</div></div>
<div class="type-sample"><div style="font-size:26px; font-weight:300; line-height:1.12; letter-spacing:-0.26px; color:var(--navy);">Sub-heading Large</div><div class="type-meta">Sub-heading -- 26px / 300 / 1.12 / -0.26px / sohne-var "ss01"</div></div>
<div class="type-sample"><div style="font-size:22px; font-weight:300; line-height:1.10; letter-spacing:-0.22px; color:var(--navy);">Sub-heading</div><div class="type-meta">Sub-heading -- 22px / 300 / 1.10 / -0.22px / sohne-var "ss01"</div></div>
<div class="type-sample"><div style="font-size:18px; font-weight:300; line-height:1.40; color:var(--slate);">Body Large -- Financial infrastructure for the internet. Millions of companies use Stripe to accept payments.</div><div class="type-meta">Body Large -- 18px / 300 / 1.40 / normal / sohne-var "ss01"</div></div>
<div class="type-sample"><div style="font-size:16px; font-weight:300; line-height:1.40; color:var(--slate);">Body -- Standard reading text for descriptions and content.</div><div class="type-meta">Body -- 16px / 300 / 1.40 / normal / sohne-var "ss01"</div></div>
<div class="type-sample"><div style="font-size:16px; font-weight:400; line-height:1.00; color:var(--navy);">Button Text</div><div class="type-meta">Button -- 16px / 400 / 1.00 / normal / sohne-var "ss01"</div></div>
<div class="type-sample"><div style="font-size:14px; font-weight:400; line-height:1.00; color:var(--navy);">Link / Navigation</div><div class="type-meta">Link -- 14px / 400 / 1.00 / normal / sohne-var "ss01"</div></div>
<div class="type-sample"><div style="font-size:12px; font-weight:300; line-height:1.45; color:var(--slate);">Caption -- Small labels and metadata</div><div class="type-meta">Caption -- 12px / 300 / 1.45 / normal / sohne-var "ss01"</div></div>
<div class="type-sample"><div style="font-size:12px; font-weight:300; line-height:1.33; letter-spacing:-0.36px; color:var(--slate); font-feature-settings:'tnum' 1;">$1,234,567.89</div><div class="type-meta">Tabular Numbers -- 12px / 300 / 1.33 / -0.36px / sohne-var "tnum"</div></div>
<div class="type-sample"><div style="font-family:var(--font-mono); font-size:12px; font-weight:500; line-height:2.00;">const stripe = require('stripe')('sk_test_...');</div><div class="type-meta">Code Body -- 12px / 500 / 2.00 / Source Code Pro</div></div>
<div class="type-sample"><div style="font-family:var(--font-mono); font-size:12px; font-weight:500; line-height:2.00; text-transform:uppercase;">API VERSION</div><div class="type-meta">Code Label -- 12px / 500 / uppercase / Source Code Pro</div></div>
</section>
<hr class="section-divider">
<section class="section" id="buttons">
<div class="section-label">03 / Buttons</div>
<h2 class="section-title">Button Variants</h2>
<div class="button-row">
<div class="button-item"><a class="btn-primary" href="#">Start now</a><div class="button-label">Primary Purple</div></div>
<div class="button-item"><a class="btn-ghost" href="#">Documentation</a><div class="button-label">Ghost / Outlined</div></div>
<div class="button-item"><a class="btn-info" href="#">Learn more</a><div class="button-label">Transparent Info</div></div>
<div class="button-item"><a class="btn-neutral" href="#">Disabled</a><div class="button-label">Neutral Ghost</div></div>
<div class="button-item"><span class="badge-success">Active</span><div class="button-label">Success Badge</div></div>
<div class="button-item"><span class="badge-neutral">v2024-12</span><div class="button-label">Neutral Badge</div></div>
</div>
</section>
<hr class="section-divider">
<section class="section" id="cards">
<div class="section-label">04 / Cards</div>
<h2 class="section-title">Card Examples</h2>
<div class="card-grid">
<div class="card">
<div class="card-badge" style="background:rgba(83,58,253,0.1); color:var(--purple);">Payments</div>
<h3>Online Payments</h3>
<p>Accept payments online with a fully integrated suite of payment products. Optimized for conversion with pre-built checkout pages.</p>
</div>
<div class="card" style="box-shadow: var(--shadow-card);">
<div class="card-badge" style="background:rgba(234,34,97,0.1); color:var(--ruby);">Elevated</div>
<h3>Revenue Recognition</h3>
<p>Automate your revenue reporting. Card shown with full blue-tinted shadow stack for elevated importance.</p>
</div>
<div class="card">
<div class="card-badge" style="background:rgba(21,190,83,0.15); color:var(--success-text);">Connect</div>
<h3>Platform Payments</h3>
<p>Build a marketplace or platform with multi-party payments, instant payouts, and flexible revenue sharing.</p>
</div>
</div>
</section>
<hr class="section-divider">
<section class="section" id="forms">
<div class="section-label">05 / Forms</div>
<h2 class="section-title">Form Elements</h2>
<div class="form-group"><label class="form-label">API Key Name</label><input class="form-input" type="text" placeholder="my-api-key"><div class="form-state-label">Default</div></div>
<div class="form-group"><label class="form-label">Webhook URL</label><input class="form-input form-input--focus" type="text" value="https://api.example.com/webhook"><div class="form-state-label">Focus (purple ring)</div></div>
<div class="form-group"><label class="form-label">Email Address</label><input class="form-input form-input--error" type="text" value="invalid-email"><div class="form-state-label">Error (ruby ring)</div></div>
<div class="form-group"><label class="form-label">Metadata</label><textarea class="form-textarea" placeholder='{"key": "value"}'></textarea></div>
</section>
<hr class="section-divider">
<section class="section" id="spacing">
<div class="section-label">06 / Spacing</div>
<h2 class="section-title">Spacing Scale</h2>
<div class="spacing-row">
<div class="spacing-item"><div class="spacing-block" style="width:2px"></div><div class="spacing-value">2</div></div>
<div class="spacing-item"><div class="spacing-block" style="width:4px"></div><div class="spacing-value">4</div></div>
<div class="spacing-item"><div class="spacing-block" style="width:6px"></div><div class="spacing-value">6</div></div>
<div class="spacing-item"><div class="spacing-block" style="width:8px"></div><div class="spacing-value">8</div></div>
<div class="spacing-item"><div class="spacing-block" style="width:10px"></div><div class="spacing-value">10</div></div>
<div class="spacing-item"><div class="spacing-block" style="width:12px"></div><div class="spacing-value">12</div></div>
<div class="spacing-item"><div class="spacing-block" style="width:14px"></div><div class="spacing-value">14</div></div>
<div class="spacing-item"><div class="spacing-block" style="width:16px"></div><div class="spacing-value">16</div></div>
<div class="spacing-item"><div class="spacing-block" style="width:18px"></div><div class="spacing-value">18</div></div>
<div class="spacing-item"><div class="spacing-block" style="width:20px"></div><div class="spacing-value">20</div></div>
</div>
</section>
<hr class="section-divider">
<section class="section" id="radius">
<div class="section-label">07 / Radius</div>
<h2 class="section-title">Border Radius</h2>
<div class="radius-row">
<div class="radius-item"><div class="radius-box" style="border-radius:1px"></div><div class="radius-label">1px</div><div class="radius-context">Micro</div></div>
<div class="radius-item"><div class="radius-box" style="border-radius:4px"></div><div class="radius-label">4px</div><div class="radius-context">Buttons, inputs</div></div>
<div class="radius-item"><div class="radius-box" style="border-radius:5px"></div><div class="radius-label">5px</div><div class="radius-context">Standard cards</div></div>
<div class="radius-item"><div class="radius-box" style="border-radius:6px"></div><div class="radius-label">6px</div><div class="radius-context">Nav, large cards</div></div>
<div class="radius-item"><div class="radius-box" style="border-radius:8px"></div><div class="radius-label">8px</div><div class="radius-context">Featured</div></div>
</div>
</section>
<hr class="section-divider">
<section class="section" id="elevation">
<div class="section-label">08 / Elevation</div>
<h2 class="section-title">Elevation</h2>
<div class="elevation-grid">
<div class="elevation-card" style="border: 1px solid var(--border);"><div class="elevation-label">Level 0: Flat</div><div class="elevation-desc">No shadow</div></div>
<div class="elevation-card" style="box-shadow: rgba(23,23,23,0.06) 0px 3px 6px 0px;"><div class="elevation-label">Level 1: Subtle</div><div class="elevation-desc">Ambient soft</div></div>
<div class="elevation-card" style="box-shadow: rgba(23,23,23,0.08) 0px 15px 35px 0px;"><div class="elevation-label">Level 2: Standard</div><div class="elevation-desc">Ambient card</div></div>
<div class="elevation-card" style="box-shadow: rgba(50,50,93,0.25) 0px 30px 45px -30px, rgba(0,0,0,0.1) 0px 18px 36px -18px;"><div class="elevation-label">Level 3: Elevated</div><div class="elevation-desc">Blue-tinted dual layer</div></div>
<div class="elevation-card" style="box-shadow: rgba(3,3,39,0.25) 0px 14px 21px -14px, rgba(0,0,0,0.1) 0px 8px 17px -8px;"><div class="elevation-label">Level 4: Deep</div><div class="elevation-desc">Dark blue-tinted</div></div>
<div class="elevation-card" style="box-shadow: 0 0 0 2px var(--purple);"><div class="elevation-label">Focus</div><div class="elevation-desc">Purple ring</div></div>
</div>
</section>
<footer class="footer">Maintained by <a href="https://github.com/VoltAgent/voltagent" target="_blank" rel="noopener noreferrer" style="text-decoration:none;"><img src="https://github.com/VoltAgent.png?size=32" alt="VoltAgent" width="14" height="14" style="border-radius:3px;vertical-align:-2px;margin-right:3px;">VoltAgent</a> team</footer>
</body>
</html>

View File

@ -1,373 +0,0 @@
# Design System Inspired by The Verge
## 1. Visual Theme & Atmosphere
The Verge's 2024 redesign feels like somebody wired a Condé Nast magazine to a chiptune soundboard. The canvas is almost-black (`#131313`), the headlines are built from a brutally heavy display face (Manuka) that runs up to 107px, and the whole page is peppered with acid-mint `#3cffd0` and ultraviolet `#5200ff` that behave less like brand colors and more like hazard tape. Story tiles are not quiet gray cards — they're saturated, full-bleed color blocks (yellow, pink, orange, blue, purple) that feel like pasted-up rave flyers arranged into a timeline. The mood is "developer console meets club night meets tech tabloid": serious enough to cover a congressional hearing, loud enough to review a synthesizer.
What makes this system unmistakable is the **StoryStream** timeline: a vertical feed where every post is a rounded rectangle — often 2040px radius — filled edge-to-edge with color, framed by a thin border, and marked by a mono-uppercase timestamp on its left rail. Stories don't float on a grid; they stack on a dashed vertical rule like commits in a git log. Above that, a massive **"The Verge" wordmark** dominates the masthead in Manuka at hero scale, letting the reader know before any headline loads that this is editorial territory, not a template.
There is no "light mode" on the homepage — the dark canvas is the product, and the only time the palette inverts is when a single story tile takes a mint or yellow fill. The depth is almost entirely flat: **hairline 1px borders** (`#ffffff`, `#3cffd0`, or `#5200ff`) do the work that shadows would do on a Material-flavored site. Every container is either `#131313` with a 1px outline, a fully saturated accent block, or a slate-gray `#2d2d2d` secondary surface.
**Key Characteristics:**
- Near-black editorial canvas (`#131313`) as the default surface — no light mode on the homepage
- Acid-mint `#3cffd0` + ultraviolet `#5200ff` as hazard-tape accents, never quiet background wash
- Massive Manuka display headlines up to 107px — the single loudest type move in mainstream tech media
- Rounded pill-card everything: 20/24/30/40px corner radii, never square
- Fully saturated color-block story tiles (mint, purple, yellow, pink, orange, electric blue) on a dark page
- Timeline "StoryStream" feed with mono uppercase timestamps rather than a traditional magazine grid
- Flat depth — 1px borders in white, mint, purple do the work that shadows would do elsewhere
## 2. Color Palette & Roles
### Primary (Brand Hazards)
- **Jelly Mint** (`#3cffd0`): The Verge's signature acid-mint accent. Used as CTA button fill, link underlines, active tab borders, and high-attention story-tile backgrounds. Treat it as the visual equivalent of neon safety paint — applied sparingly to the most important element on screen.
- **Verge Ultraviolet** (`#5200ff`): The complementary brand hazard. Used for secondary color-block tiles, promotional spans, and the occasional outlined button. Often applied at 0.9 alpha to soften its cathode intensity.
### Secondary & Accent
- **Console Mint Border** (`#309875`): A darker variant of the jelly mint used on card outlines and button borders where pure mint would over-saturate.
- **Deep Link Blue** (`#3860be`): The link *hover* color — the one moment blue appears on the site. It replaces mint/white/black on hover across every link style.
- **Focus Cyan** (`#1eaedb`): Reserved for button focus rings. Never shown outside a keyboard-focus state.
- **Purple Rule** (`#3d00bf`): A darker ultraviolet variant used as the vertical border on StoryStream `<li>` items.
### Surface & Background
- **Canvas Black** (`#131313`): The default dark surface for the entire homepage. Almost-but-not-quite pure black — has just enough warmth to feel like a printed newsprint negative rather than an OLED void.
- **Surface Slate** (`#2d2d2d`): Secondary card background, used when a story tile doesn't need to be a saturated color block.
- **Image Frame** (`#313131`): The 1px border that wraps inline imagery.
- **Hazard White** (`#ffffff`): Used as story-tile fill, button border, and primary text. When white appears as a large block, it's an editorial decision — a "spotlight" on that tile.
- **Absolute Black** (`#000000`): Reserved for text on the mint/yellow/white tiles — the only place it appears.
### Neutrals & Text
- **Primary Text** (`#ffffff`): Headlines and display text on the canvas.
- **Secondary Text** (`#949494`): Bylines, timestamps, photo credits. The mid-gray that anchors the metadata layer.
- **Muted Text** (`#e9e9e9`): Button text on dark slate buttons. Slightly off-white to reduce screen glare.
- **Inverted Text** (`#131313`): Used only on accent tiles (mint, yellow, white) to keep contrast legible.
### Semantic & Accent
- **Focus Ring** (`#1eaedb`): Keyboard focus only.
- **Overlay Black** (`rgba(0, 0, 0, 0.33)`): Subtle 1px ring used as the quiet shadow alternative on stacked cards.
- **Dim Gray** (`#8c8c8c`): Active/pressed button background — the "pressed down" state.
### Gradient System
The Verge uses **zero decorative gradients**. The only gradient-like treatment is the transition from a saturated accent story tile (mint/purple/yellow) back to the `#131313` canvas between rows. Color is applied in solid blocks, not as washes. This is a deliberate choice — the site's hazard-tape visual identity would dissolve if anything faded.
## 3. Typography Rules
### Font Family
- **Manuka** (Klim Type Foundry) — fallback: Impact, Helvetica. The signature display face for The Verge wordmark and feature headlines. A heavy-weight (900) industrial sans-serif with a condensed, almost-athletic stance. Runs at 60107px on the homepage, never smaller.
- **PolySans** (PanGram Pangram / Nikolas Wrobel) — fallback: Helvetica, Arial. The UI and secondary headline workhorse. Covers weights 300 / 500 / 700 across the system — everything from kicker captions to body decks.
- **PolySans Mono** — fallback: Courier New, Courier. The monospaced sibling, used exclusively for ALL-CAPS labels: kickers, timestamps, category tags, button labels. This mono-uppercase usage is the second-most-identifiable Verge detail after Manuka.
- **FK Roman Standard** (Florian Karsten) — fallback: Georgia. A serif used sparingly for specific body/caption treatments (article excerpts, certain review pulls). Adds a "print-magazine" counterpoint to the PolySans stack.
- **Roboto** — fallback: `-apple-system`, `system-ui`. Utility UI font for widgets and legacy modules.
### Hierarchy
| Role | Font | Size | Weight | Line Height | Letter Spacing | Notes |
| ----------------------- | ----------------- | --------------- | ------- | ----------- | -------------- | ----------------------------------------------------------- |
| Hero Wordmark / Display | Manuka | 107px / 6.69rem | 900 | 0.80 | 1.07px | The top-of-page "The Verge" logo and feature headlines |
| Secondary Display | Manuka | 90px / 5.63rem | 900 | 0.80 | — | Section-level feature headlines |
| Tertiary Display | Manuka | 60px / 3.75rem | 900 | 0.80 | — | Inline feature callouts |
| Large Headline | PolySans | 34px / 2.13rem | 700 | 1.00 | — | Section and module headlines |
| Heading Wide | PolySans | 32px / 2.00rem | 400 | 1.10 | 0.32px | Sub-heroes, promotional units |
| Heading Medium | PolySans | 24px / 1.50rem | 700 | 1.00 | — | Story tile headlines in the main feed |
| Heading Small | PolySans | 20px / 1.25rem | 700 | 1.00 | — | Compact tile headlines |
| Light Capitalized Label | PolySans | 19px / 1.19rem | 300 | 1.20 | 1.9px | Thin-weight capitalized eyebrows — a distinctive Verge move |
| All-Caps Label XL | PolySans | 18px / 1.13rem | 400 | 1.10 | 1.8px | UPPERCASE section kickers |
| Bold Body | PolySans | 16px / 1.00rem | 700 | 1.00 | — | Emphasis within decks |
| Body Relaxed | PolySans | 16px / 1.00rem | 500 | 1.60 | — | Long-form reading body |
| Inline Label | PolySans | 15px / 0.94rem | 400 | 1.20 | 0.15px | UI labels and secondary headlines |
| Body Compact | PolySans | 13px / 0.81rem | 400 | 1.60 | — | Secondary captions and decks |
| Eyebrow All-Caps | PolySans | 12px / 0.75rem | 400 | 1.30 | 1.8px | UPPERCASE kicker above tile headlines |
| Tag Label | PolySans | 12px / 0.75rem | 400 | 1.20 | 0.72px | UPPERCASE category tag |
| Caption Micro | PolySans | 11px / 0.69rem | 400 | 1.20 | 1.1px | UPPERCASE bylines |
| Meta Nano | PolySans | 10px / 0.63rem | 500 | 1.40 | 1.5px | UPPERCASE timestamp microtext |
| Mono Button Label | PolySans Mono | 12px / 0.75rem | 600 | 2.00 | 1.5px | UPPERCASE button text, very open leading |
| Mono Timestamp | PolySans Mono | 11px / 0.69rem | 500/600 | 1.20 | 1.11.8px | UPPERCASE StoryStream timestamps |
| Serif Body | FK Roman Standard | 16px / 1.00rem | 400 | 1.30 | -0.16px | Review decks, print-voice excerpts |
| Serif Caption | FK Roman Standard | 20px / 1.25rem | 400 | 1.20 | — | Magazine-style pull quotes |
### Principles
- **Manuka is always the hero, never the UI.** If you see Manuka below 60px you're looking at a bug. It exists to *shout the brand*, not to label a button.
- **PolySans is the workhorse, PolySans Mono is its uniformed sibling.** Mono is used exclusively for UPPERCASE labels, timestamps, tags, and certain buttons. Lowercase mono doesn't exist in this system.
- **Thin-weight (300) capitalized headlines** are a signature Verge move. The 1920px weight-300 with 1.9px tracking creates a "fashion magazine whisper" that contrasts with the 107px Manuka shout above it. This whisper-vs-shout contrast is the typographic fingerprint.
- **Letter-spacing has two registers**: positive (0.721.9px) for ALL-CAPS mono and sans labels, negative (`-0.16px`) for the rare serif appearances, barely-positive (0.32px, 1.07px) for massive display. Plain 0 letter-spacing is rare.
- **FK Roman Standard is the editorial exception**, not the rule. Reserve it for long-form print-voice moments — reviews, critic pulls, masthead essays. Never use it in UI.
- **Line heights are tight** (0.801.30) for every display and label, relaxed (1.602.00) only for reading body and mono button labels. The leading jump is intentional — it gives the page a "telegraph ticker" rhythm.
### Note on Font Substitutes
The 0.80 line-height on Manuka display (107px, 90px, 60px) assumes the **proprietary Manuka face from Klim Type Foundry**, which has aggressively tight vertical metrics designed for athletic stance at large sizes. If you substitute with wide-metric open-source condensed displays like **Anton**, **Oswald**, **Bebas Neue**, or **Archivo Black**, loosen display line-heights by approximately **+0.10 to +0.15** to prevent ascender/descender collisions (e.g., 0.80 → 0.95). PolySans substitutes (Space Grotesk, DM Sans, Hanken Grotesk) work at the token values without adjustment — their metrics are close enough. PolySans Mono substitutes (Space Mono, JetBrains Mono) and FK Roman substitutes (Newsreader, Literata) also work without adjustment.
## 4. Component Stylings
### Buttons
**Primary — Jelly Mint Pill**
- Background: `#3cffd0` (Jelly Mint)
- Text: `#000000` (Absolute Black), PolySans 16px / 700 or PolySans Mono 12px / 600 UPPERCASE
- Border: none (pure fill)
- Border radius: `24px` — fully rounded pill
- Padding: `10px 24px`
- Outline: `none` at rest
- Hover: background shifts to `rgba(255, 255, 255, 0.2)` (translucent white), text stays black, adds a 1px `#c2c2c2` ring shadow
- Active: background `rgba(140, 140, 140, 0.87)`, opacity `0.5`, ring shadow `#8c8c8c`
- Focus: background `#1eaedb`, white text, 1px solid `#0500ff` border, translucent white focus ring
- Transition: \~180ms ease on background and shadow
**Secondary — Dark Slate Pill**
- Background: `#2d2d2d` (Surface Slate)
- Text: `#e9e9e9` (Muted Text), PolySans 16px / 400
- Border: none
- Border radius: `24px`
- Padding: `10px 24px`
- Outline: `rgb(233, 233, 233) none 0px`
- Hover: same translucent white invert as primary — `rgba(255, 255, 255, 0.2)` bg, black text, 1px `#c2c2c2` ring
- Focus: same cyan focus treatment as primary
**Tertiary — Outlined Mint**
- Background: transparent
- Text: `#3cffd0`, PolySans Mono 12px / 600 UPPERCASE, 1.5px tracking
- Border: `1px solid #3cffd0`
- Border radius: `40px` — larger pill for secondary outline style
- Padding: \~`10px 20px`
- Hover: inverts to mint fill, black text
- Transition: 150ms ease
**Outlined Ultraviolet (Promotional)**
- Background: transparent
- Text: `#5200ff` or `#ffffff`
- Border: `1px solid #5200ff`
- Border radius: `30px`
- Used for "Subscribe" or "Join the Stream" style promotional callouts
**Pill Tag (Non-interactive)**
- Background: saturated accent (`#3cffd0`, `#5200ff`, yellow, etc.)
- Text: black or white depending on background luminance
- Border radius: `20px` (tighter radius than buttons — this is the *text pill*)
- Font: PolySans Mono 11px / 600 UPPERCASE, 1.8px tracking
- Padding: \~`4px 10px`
### Cards & Containers
**StoryStream Tile**
- Background: either `#131313` + 1px white border, OR a saturated accent fill (mint, purple, yellow, pink, orange, white)
- Border radius: `20px` (standard) or `24px` (feature)
- Border: `1px solid #ffffff` (on dark) or `0px 0px 1px solid #3cffd0` (on mint) or nothing (on saturated fill)
- Padding: \~2432px interior
- Hover: no lift, no scale — the headline text color transitions from white to `#3860be` (deep link blue)
- Transition: 150ms ease on color only
**Feature Card (Top Story)**
- Background: `#131313` with 1px hairline border, OR full-bleed color accent
- Border radius: `24px`
- Padding: 32px+
- Image inside: clipped to match the outer radius (`3px` or `4px` inner radius when nested)
- Hover: text color shift only; the image remains static
**StoryStream Rail (Timeline)**
- A vertical dashed or solid rule (1px `#3d00bf` or `#ffffff`) runs along the left edge of each item, marking the timeline spine
- Timestamps sit on the left rail in PolySans Mono 11px / 500 / UPPERCASE / 1.1px tracking
- Each entry is a pill-cornered rectangle separated from its neighbors by 1216px vertical gap
### Inputs & Forms
- **Default**: `#131313` background, 1px solid `#ffffff` or `#949494` border, `2px` border radius (tight, newspaper-form feel), PolySans 15px text in `#ffffff`, placeholder in `#949494`.
- **Focus**: border transitions to `#3cffd0` (jelly mint) with optional `1px solid #5200ff` inner ring on deep focus. No glow.
- **Error**: border turns `#5200ff` (ultraviolet — used as error/alert accent here, not the usual red).
- **Transition**: \~150ms ease on border-color.
### Navigation
- **Top nav**: thin `#131313` bar with the Verge wordmark (Manuka) left-aligned, a search icon and a few UPPERCASE mono category links (1214px, PolySans Mono, 1.51.8px tracking), and a single mint-pill CTA (usually "Subscribe") pinned right.
- **Wordmark**: massive on first scroll — the homepage treats the "The Verge" logo as a hero element, not a 32px corner logo.
- **Hover**: every link transitions from `#ffffff` to `#3860be` (deep link blue). No underline — it's a color-only response.
- **Active section**: marked by a 1px mint underline (inset box-shadow `0px -1px 0px 0px inset #3cffd0`)
- **Mobile**: the wordmark shrinks, category nav collapses into a hamburger drawer. Inside the drawer, links are mono-uppercase and stack with 1620px gaps.
### Image Treatment
- **Aspect ratios**: 16:9 dominates for hero and feature images, 4:3 for mid-feed, 1:1 for thumbnails and author avatars.
- **Corners**: always rounded to match the parent card — `3px`, `4px`, or inherit `20px` / `24px` from the tile.
- **Frame**: 1px `#313131` or `#ffffff` hairline around photography, giving a "contained Polaroid" feel.
- **Full-bleed**: only within the color-block tiles, where the image runs to the padded edge of the accent fill.
- **Hover**: static — no zoom, no scale, no opacity shift. The headline below is the only interactive response.
- **Lazy loading**: `loading="lazy"` on everything below the first fold; eager on the masthead hero only.
### StoryStream Timeline Item (Distinctive)
- Vertical rail line (1px `#3d00bf` or `#ffffff` on `#131313`)
- Mono timestamp on the left in PolySans Mono 11px / UPPERCASE
- Pill-cornered body card (20px radius) with kicker, headline, and optional deck
- Stacked vertically with 1216px gap, the rail continuing between them
- Often interleaved with full-bleed accent tiles that "break" the timeline rhythm for emphasis
## 5. Layout Principles
### Spacing System
- **Base unit**: 8px.
- **Scale**: 1, 2, 4, 5, 6, 8, 9, 10, 12, 14, 15, 16, 20, 24, 25px.
- **Section padding**: 3264px vertical between major feed sections. StoryStream items themselves are tighter — 1216px gaps.
- **Card padding**: 2032px interior. Feature cards expand to 4048px.
- **Inline spacing**: kickers sit \~610px above headlines; headlines sit \~1014px above decks; timestamps sit \~68px below decks.
- **Micro-scale**: The 2/4/5/6/9/10px values are used inside buttons, pills, and tight label clusters, not in the editorial grid.
### Grid & Container
- **Max width**: \~12801300px (dembrandt detected breakpoints at 1200/1280/1300).
- **Column patterns**: a 12-column underlying grid that resolves into 3-column hero + 1-column StoryStream rail + feature panels. The homepage feels freeform because color-block tiles frequently span 23 columns on a whim.
- **Container padding**: 24px mobile / 48px desktop on the outer edges.
- **Gutters**: 1624px between columns, tighter (812px) inside StoryStream items.
### Whitespace Philosophy
The Verge treats whitespace like a club DJ treats silence — as a dramatic reset between loud moments. The canvas is so dark and the accents are so saturated that even 32px of empty `#131313` between two tiles acts as a palette cleanser. The page is not airy like Apple or Stripe; it's **paced**, with loud hazard-color blocks interrupting stretches of near-black. Whitespace carries the rhythm, not the elegance.
### Border Radius Scale
- **2px** — inputs, small badges (feels like a typewriter tag)
- **3px** — inline images (just enough to soften against the canvas)
- **4px** — nested card images and small button variants
- **20px** — standard pill cards and color-block tiles
- **24px** — feature tile radius and primary button pill
- **30px** — large promotional buttons
- **40px** — outlined CTA pills (the loudest pill in the system)
- **50%** — avatar circles, icon buttons, and certain round badges
Eight discrete radius values — a **lot** for a single site. This is deliberate: the rhythm between 2px typewriter tags, 20px pill cards, and 40px outlined buttons creates a "nested scale" feel where every component announces its hierarchy through its corners.
## 6. Depth & Elevation
| Level | Treatment | Use |
| ----- | ----------------------------------------------------------------- | -------------------------------------------------------- |
| 0 | No border, no shadow | Default `#131313` canvas text |
| 1 | `rgba(0,0,0,0) 0px 0px 0px 0px inset` (placeholder) | Reset state for interactive elements |
| 2 | `1px solid #ffffff` or `#313131` hairline | Image frames and quiet card outlines |
| 3 | `1px solid #3cffd0` hairline | Active button outlines, focused story tiles |
| 4 | `1px solid #5200ff` hairline | Promotional/alternate state outlines |
| 5 | `rgba(0, 0, 0, 0.33) 0px 0px 0px 1px` | The single "atmospheric" ring — applied to layered cards |
| 6 | `0px -1px 0px 0px inset` (mint/black/white) | Active tab underline — a signature Verge move |
| 7 | Saturated accent fill (`#3cffd0`, `#5200ff`, white, yellow, pink) | Story-tile elevation via color, not shadow |
The Verge's depth philosophy is **color-as-elevation**. When something needs to stand out, it doesn't get a shadow — it gets a mint fill or a 1px hazard-color border. There are 14 shadow entries in the extracted tokens, but all of them are either inset underlines (0px -1px inset) or near-transparent 1px rings — none of them are traditional elevation shadows. The `#131313` canvas stays perfectly flat throughout, and hierarchy is carried by color saturation.
### Decorative Depth
- **1px inset underline** on active tabs/nav links (mint, black, or white depending on context)
- **Subtle** **`rgba(0, 0, 0, 0.33)`** **1px ring** on stacked cards — the only effect that faintly resembles a shadow
- **No gradients, no glows, no atmospheric blurs** anywhere. The hazard-tape aesthetic would break if anything faded softly.
## 7. Do's and Don'ts
### Do
- **Do** use `#131313` as the canvas for every view. There is no light mode.
- **Do** use Jelly Mint (`#3cffd0`) and Verge Ultraviolet (`#5200ff`) as hazard accents — buttons, borders, active states, and saturated color-block tiles.
- **Do** use Manuka exclusively at 60px+ for hero headlines. Treat anything smaller as a bug.
- **Do** round everything: 20px for cards, 24px for feature cards, 3040px for pill buttons.
- **Do** use PolySans Mono for UPPERCASE labels, timestamps, kickers, and button text. Lowercase mono doesn't exist here.
- **Do** apply 1.51.9px letter-spacing to every ALL-CAPS label — this is a Verge signature.
- **Do** use saturated color-block tiles (mint, purple, yellow, pink, orange, white) to elevate a story — never a drop shadow.
- **Do** use `#3860be` (deep link blue) as the hover color on every link, regardless of base color.
- **Do** apply the StoryStream timeline rail (1px dashed/solid `#3d00bf` or white) on feed views.
- **Do** use thin-weight (300) PolySans at 1920px with 1.9px tracking for "fashion-whisper" capitalized eyebrows — the contrast with the 107px Manuka shout is the whole voice.
### Don't
- **Don't** use a light background. The dark canvas is the product.
- **Don't** add `box-shadow` for elevation. Use 1px borders or saturated accent fills instead.
- **Don't** use square corners. Every interactive and content container is rounded.
- **Don't** use Manuka for UI, buttons, or body copy. It's strictly display.
- **Don't** use lowercase mono. PolySans Mono is always UPPERCASE.
- **Don't** let mint and ultraviolet appear as background washes — they're hazard accents, not canvas tints.
- **Don't** use gradients anywhere. The system is solid color blocks only.
- **Don't** introduce new accent colors outside the declared mint / purple / yellow / pink / orange tile palette.
- **Don't** pair Manuka with FK Roman Standard in the same headline cluster — Manuka is the only display shout, serif pulls are reserved for body moments.
- **Don't** use `#3cffd0` text on a `#131313` background at under 16px — the contrast vibrates at small sizes.
## 8. Responsive Behavior
### Breakpoints
| Name | Width | Key Changes |
| ------------- | ----------- | ---------------------------------------------------------------------------------------------------- |
| Small Mobile | <400px | Single column, Manuka hero scales down to \~4854px, StoryStream rail collapses to inline timestamps |
| Mobile | 400549px | Single column, color-block tiles stack full-width, nav is a hamburger drawer |
| Large Mobile | 550767px | Still single column but padding opens up, tile radii stay at 20px |
| Tablet | 7681023px | 2-column StoryStream with feature card spanning, wordmark shrinks \~50% |
| Small Desktop | 10241179px | Full 34 column editorial grid, mint pill CTA restored to nav |
| Desktop | 11801299px | Max padding, Manuka wordmark at full hero scale |
| Large Desktop | ≥1300px | Container caps at \~12801300px, whitespace expands at the margins, no further scaling |
The dembrandt sweep detected 26 intermediate breakpoints (1300 → 1280 → 1200 → 1181 → 1180 → 1179 → 1024 → 1023 → 901 → 900 → 897 → 896 → 890 → 769 → 768 → 620 → 605 → 600 → 550 → 549 → 530 → 426 → 425 → 400 → 320). The Verge tunes its grid at virtually every major device boundary — an unusually aggressive responsive strategy.
### Touch Targets
- Primary pill buttons are \~44px minimum height (10px vertical padding + 16px text + 2px border) — meets WCAG AA.
- Mono uppercase nav links are smaller (\~2832px tall) — for derivative work, pad to 44px on mobile.
- Circle icon buttons are 4044px circles, touch-friendly.
### Collapsing Strategy
- **Nav**: wordmark scales from hero (Manuka 60107px) to \~2432px on mobile. Category links collapse to a hamburger drawer below 900px.
- **Grid**: 4-col → 3-col → 2-col → 1-col. Feature cards that span 2 columns on desktop reflow to full-width single-column on mobile.
- **Spacing**: section padding tightens from 64px → 32px → 20px. Tile interior padding tightens from 32px → 20px.
- **Type**: Manuka hero scales from 107px to \~4854px on mobile. PolySans headlines scale from 34px → 24px. Mono labels stay pinned at 1112px (they don't shrink further or they become unreadable).
- **Color tiles**: accent story blocks never lose saturation on mobile — they just reflow to full width.
### Image Behavior
- Responsive raster via `srcset`, aspect ratios preserved.
- No art-direction swaps — same crop scales across all viewports.
- `loading="lazy"` on everything below the fold, `eager` on the masthead hero.
- Images inside color-block tiles inherit the tile's inner radius (4px or 20px nested).
## 9. Agent Prompt Guide
### Quick Color Reference
- **Primary CTA**: "Jelly Mint (`#3cffd0`)"
- **Background (Canvas)**: "Canvas Black (`#131313`)"
- **Accent (Secondary Hazard)**: "Verge Ultraviolet (`#5200ff`)"
- **Heading Text**: "Hazard White (`#ffffff`)"
- **Body Text**: "Hazard White (`#ffffff`)" (primary) or "Muted Text (`#e9e9e9`)"
- **Secondary Text / Metadata**: "Secondary Text (`#949494`)"
- **Card Border**: "Hazard White (`#ffffff`)" hairline on dark, "Console Mint Border (`#309875`)" on mint variants
- **Link Hover**: "Deep Link Blue (`#3860be`)"
### Example Component Prompts
1. *"Create a StoryStream timeline item on a* *`#131313`* *canvas: a 20px-radius rectangle with a 1px solid* *`#ffffff`* *border, a PolySans Mono 11px / 600 / UPPERCASE / 1.1px tracking timestamp on the left rail, a 12px PolySans UPPERCASE kicker in mint (`#3cffd0`), and a 24px / 700 PolySans headline in white below. No shadow, no lift — hover only shifts the headline color to* *`#3860be`."*
2. *"Design a primary subscribe button with a Jelly Mint (`#3cffd0`) fill, black text in PolySans Mono 12px / 600 / UPPERCASE / 1.5px tracking, 24px border radius, 10px × 24px padding. Hover state shifts to* *`rgba(255, 255, 255, 0.2)`* *background with a 1px* *`#c2c2c2`* *ring shadow, 180ms ease."*
3. *"Build a feature hero with a 107px Manuka 900 headline in white with 1.07px letter-spacing and 0.80 line-height, a thin-weight 300 PolySans 20px capitalized kicker above with 1.9px tracking, on a* *`#131313`* *canvas with 64px vertical padding."*
4. *"Create a color-block accent tile filled with Verge Ultraviolet (`#5200ff`) at 0.9 alpha, 24px border radius, white text, a PolySans Mono 11px UPPERCASE category label with 1.5px tracking at the top, and a 32px PolySans 400 capitalized headline with 0.32px tracking below."*
5. *"Design a dark slate secondary button with a* *`#2d2d2d`* *background,* *`#e9e9e9`* *PolySans 16px text, 24px radius pill shape, 10px × 24px padding. Hover matches the primary button — translucent white* *`rgba(255, 255, 255, 0.2)`* *bg with black text."*
### Iteration Guide
When refining existing screens generated with this design system:
1. **Audit the canvas.** If you see a light background anywhere on the homepage, flatten it to `#131313`. There is no light mode.
2. **Audit corners.** Every rectangle should land on 2/3/4/20/24/30/40px or 50%. Square corners break the voice.
3. **Audit shadows.** Strip every `box-shadow` that isn't a 1px inset underline or a 1px hazard-color border. The Verge uses color for elevation, not shadow.
4. **Audit type roles.** Manuka only ≥60px. PolySans Mono only UPPERCASE. PolySans 300 at 1920px should have 1.9px tracking. FK Roman only for body/magazine moments, never UI.
5. **Audit accent usage.** Mint and ultraviolet should appear as hazard accents — buttons, 1px borders, active underlines, saturated tile fills. If they're appearing as background washes or gradient fades, correct to solid blocks.
6. **Audit labels.** Every kicker, timestamp, category tag, and button label should be ALL CAPS with 1.11.9px letter-spacing. Missing tracking = missing voice.
7. **Audit link hover.** Every link, regardless of its base color, should hover to `#3860be` deep link blue with no underline. Any other hover color is drift.

View File

@ -1,315 +0,0 @@
# Design System Inspired by The Verge (Light Mode)
## 1. Visual Theme & Atmosphere
The Verge's light mode maintains the same bold editorial identity but on a clean white canvas (`#ffffff`). The headlines remain impactful with heavy display faces, and the acid-mint `#3cffd0` and ultraviolet `#5200ff` accents still function as visual hazard tape. Story tiles continue to be saturated color blocks, creating a vibrant contrast against the light background. The mood remains "developer console meets club night meets tech tabloid," but with a brighter, more open feel that enhances readability during daytime use.
The StoryStream timeline remains a key identifier: a vertical feed where every post is a rounded rectangle with saturated color, framed by a thin border, and marked by a mono-uppercase timestamp. The massive wordmark still dominates the masthead, asserting the editorial territory.
Depth is achieved through subtle 1px borders (`#131313`, `#3cffd0`, or `#5200ff`) rather than shadows, maintaining the flat, clean aesthetic while providing clear visual hierarchy.
**Key Characteristics:**
- Clean white editorial canvas (`#ffffff`) as the default surface
- Acid-mint `#3cffd0` + ultraviolet `#5200ff` as hazard-tape accents
- Impactful display headlines up to 107px
- Rounded pill-card everything: 20/24/30/40px corner radii
- Fully saturated color-block story tiles on a light page
- Timeline "StoryStream" feed with mono uppercase timestamps
- Flat depth — 1px borders in black, mint, purple for visual hierarchy
## 2. Color Palette & Roles
### Primary (Brand Hazards)
- **Jelly Mint** (`#3cffd0`): The Verge's signature acid-mint accent. Used as CTA button fill, link underlines, active tab borders, and high-attention story-tile backgrounds.
- **Verge Ultraviolet** (`#5200ff`): The complementary brand hazard. Used for secondary color-block tiles, promotional spans, and outlined buttons.
### Secondary & Accent
- **Console Mint Border** (`#309875`): A darker variant of the jelly mint used on card outlines and button borders.
- **Deep Link Blue** (`#3860be`): The link *hover* color — the one moment blue appears on the site.
- **Focus Cyan** (`#1eaedb`): Reserved for button focus rings. Never shown outside a keyboard-focus state.
- **Purple Rule** (`#3d00bf`): A darker ultraviolet variant used as the vertical border on StoryStream `<li>` items.
- **Dim Gray** (`#8c8c8c`): Active/pressed button background — the "pressed down" state.
### Surface & Background
- **Canvas White** (`#ffffff`): The default light surface for the entire homepage. Clean, bright, and optimized for readability.
- **Surface Slate** (`#f2f2f2`): Secondary card background, used when a story tile doesn't need to be a saturated color block.
- **Surface Slate 2** (`#e6e6e6`): Tertiary background for nested components.
- **Image Frame** (`#d4d4d4`): The 1px border that wraps inline imagery.
- **Hazard White** (`#ffffff`): Used as story-tile fill, button border, and primary text.
- **Absolute Black** (`#000000`): Reserved for text on the mint/yellow/white tiles.
### Neutrals & Text
- **Primary Text** (`#131313`): Headlines and display text on the canvas.
- **Secondary Text** (`#6a6a6a`): Bylines, timestamps, photo credits.
- **Muted Text** (`#8c8c8c`): Button text on light slate buttons.
- **Inverted Text** (`#ffffff`): Used only on dark accent tiles (purple, black) to keep contrast legible.
### Semantic & Accent
- **Focus Ring** (`#1eaedb`): Keyboard focus only.
- **Overlay Black** (`rgba(19, 19, 19, 0.1)`): Subtle 1px ring used as the quiet shadow alternative on stacked cards.
### Gradient System
The Verge uses **zero decorative gradients** in light mode as well. Color is applied in solid blocks, not as washes, maintaining the hazard-tape visual identity.
## 3. Typography Rules
### Font Family
- **Anton** (Google Fonts) — fallback: Impact, Helvetica. The signature display face for The Verge wordmark and feature headlines. A heavy-weight industrial sans-serif with a condensed stance. Runs at 60107px on the homepage.
- **Space Grotesk** (Google Fonts) — fallback: Helvetica, Arial. The UI and secondary headline workhorse. Covers weights 300 / 500 / 700.
- **Space Mono** (Google Fonts) — fallback: Courier New, Courier. The monospaced sibling, used exclusively for ALL-CAPS labels: kickers, timestamps, category tags, button labels.
- **Newsreader** (Google Fonts) — fallback: Georgia. A serif used sparingly for specific body/caption treatments.
### Hierarchy
| Role | Font | Size | Weight | Line Height | Letter Spacing | Notes |
| ----------------------- | ------------- | --------------- | ------- | ----------- | -------------- | ----------------------------------------------------------- |
| Hero Wordmark / Display | Anton | 104px / 6.5rem | 400 | 0.95 | 1px | The top-of-page logo and feature headlines |
| Secondary Display | Anton | 72px / 4.5rem | 400 | 0.85 | 0.5px | Section-level feature headlines |
| Large Headline | Space Grotesk | 34px / 2.13rem | 700 | 1.00 | — | Section and module headlines |
| Heading Wide | Space Grotesk | 32px / 2.00rem | 700 | 1.00 | -0.3px | Story tile headlines |
| Heading Medium | Space Grotesk | 24px / 1.50rem | 700 | 1.00 | — | Compact tile headlines |
| Light Capitalized Label | Space Grotesk | 19px / 1.19rem | 300 | 1.20 | 1.9px | Thin-weight capitalized eyebrows |
| All-Caps Label XL | Space Grotesk | 18px / 1.13rem | 300 | 1.40 | 1.8px | UPPERCASE section kickers |
| Body Relaxed | Space Grotesk | 16px / 1.00rem | 500 | 1.60 | — | Long-form reading body |
| Eyebrow All-Caps | Space Grotesk | 12px / 0.75rem | 400 | 1.30 | 1.8px | UPPERCASE kicker above tile headlines |
| Mono Timestamp | Space Mono | 11px / 0.69rem | 700 | 1.20 | 1.1px | UPPERCASE StoryStream timestamps |
| Mono Button Label | Space Mono | 12px / 0.75rem | 700 | 2.00 | 1.5px | UPPERCASE button text, very open leading |
| Serif Body | Newsreader | 16px / 1.00rem | 400 | 1.30 | -0.16px | Review decks, print-voice excerpts |
### Principles
- **Anton is always the hero, never the UI.** If you see Anton below 60px you're looking at a bug.
- **Space Grotesk is the workhorse, Space Mono is its uniformed sibling.** Mono is used exclusively for UPPERCASE labels, timestamps, tags, and certain buttons.
- **Thin-weight (300) capitalized headlines** are a signature Verge move. The 1920px weight-300 with 1.9px tracking creates a "fashion magazine whisper" that contrasts with the large Anton shout above it.
- **Letter-spacing has two registers**: positive (0.721.9px) for ALL-CAPS mono and sans labels, negative (`-0.16px`) for serif appearances.
- **Newsreader is the editorial exception**, not the rule. Reserve it for long-form print-voice moments.
- **Line heights are tight** (0.851.30) for every display and label, relaxed (1.602.00) only for reading body and mono button labels.
## 4. Component Stylings
### Buttons
**Primary — Jelly Mint Pill**
- Background: `#3cffd0` (Jelly Mint)
- Text: `#000000` (Absolute Black), Space Mono 12px / 700 UPPERCASE
- Border: none (pure fill)
- Border radius: `24px` — fully rounded pill
- Padding: `12px 28px`
- Hover: background shifts to `rgba(60, 255, 208, 0.3)`, adds a 1px `#309875` ring
- Active: background `rgba(140, 140, 140, 0.87)`, opacity `0.5`
- Focus: background `#1eaedb`, white text, 1px solid `#0500ff` border
- Transition: ~180ms ease on background and shadow
**Secondary — Dark Pill**
- Background: `#131313` (Primary Text)
- Text: `#ffffff` (Hazard White), Space Mono 12px / 700 UPPERCASE
- Border: none
- Border radius: `24px`
- Padding: `12px 28px`
- Hover: background `rgba(19, 19, 19, 0.2)`, text `#131313`, 1px `#8c8c8c` ring
- Focus: same cyan focus treatment as primary
**Tertiary — Outlined Mint**
- Background: transparent
- Text: `#309875` (Console Mint Border), Space Mono 12px / 700 UPPERCASE
- Border: `1px solid #309875`
- Border radius: `40px` — larger pill for secondary outline style
- Padding: ~`12px 24px`
- Hover: inverts to mint fill, black text
- Transition: 150ms ease
**Outlined Ultraviolet (Promotional)**
- Background: transparent
- Text: `#5200ff` (Verge Ultraviolet)
- Border: `1px solid #5200ff`
- Border radius: `30px`
- Used for "Subscribe" or "Join the Stream" style promotional callouts
**Pill Tag (Non-interactive)**
- Background: `#3cffd0` (Jelly Mint)
- Text: `#000000` (Absolute Black), Space Mono 11px / 700 UPPERCASE
- Border radius: `20px` (tighter radius than buttons)
- Padding: ~`6px 14px`
### Cards & Containers
**StoryStream Tile**
- Background: either `#ffffff` + 1px `rgba(19, 19, 19, 0.1)` border, OR a saturated accent fill (mint, purple, yellow, pink, orange, white)
- Border radius: `20px` (standard) or `24px` (feature)
- Border: `1px solid rgba(19, 19, 19, 0.1)` (on light) or `1px solid #309875` (on mint)
- Padding: ~2432px interior
- Hover: no lift, no scale — the headline text color transitions from `#131313` to `#3860be` (deep link blue)
- Transition: 150ms ease on color only
**Feature Card (Top Story)**
- Background: `#ffffff` with 1px hairline border, OR full-bleed color accent
- Border radius: `24px`
- Padding: 32px+
- Image inside: clipped to match the outer radius
- Hover: text color shift only; the image remains static
**StoryStream Rail (Timeline)**
- A vertical dashed or solid rule (1px `#3d00bf` or `rgba(19, 19, 19, 0.1)` on `#ffffff`) runs along the left edge of each item
- Timestamps sit on the left rail in Space Mono 11px / 700 / UPPERCASE / 1.1px tracking
- Each entry is a pill-cornered rectangle separated from its neighbors by 1216px vertical gap
### Inputs & Forms
- **Default**: `#f2f2f2` (Surface Slate) background, 1px solid `rgba(19, 19, 19, 0.15)` border, `2px` border radius, Space Grotesk 15px text in `#131313`, placeholder in `#8c8c8c`.
- **Focus**: border transitions to `#3cffd0` (jelly mint) with optional `1px solid #5200ff` inner ring on deep focus.
- **Error**: border turns `#5200ff` (ultraviolet — used as error/alert accent here).
- **Transition**: ~150ms ease on border-color.
### Navigation
- **Top nav**: white background, 1px solid `rgba(19, 19, 19, 0.1)` border, with the Verge wordmark (Anton) left-aligned, a search icon and a few UPPERCASE mono category links (11px, Space Mono, 1.1px tracking), and a single mint-pill CTA (usually "Subscribe") pinned right.
- **Wordmark**: massive on first scroll — treated as a hero element.
- **Hover**: every link transitions from `#131313` to `#3860be` (deep link blue). No underline — it's a color-only response.
- **Active section**: marked by a 1px mint underline (inset box-shadow `0px -1px 0px 0px inset #3cffd0`)
- **Mobile**: the wordmark shrinks, category nav collapses into a hamburger drawer. Inside the drawer, links are mono-uppercase and stack with 1620px gaps.
### Image Treatment
- **Aspect ratios**: 16:9 dominates for hero and feature images, 4:3 for mid-feed, 1:1 for thumbnails and author avatars.
- **Corners**: always rounded to match the parent card — `3px`, `4px`, or inherit `20px` / `24px` from the tile.
- **Frame**: 1px `#d4d4d4` or `rgba(19, 19, 19, 0.08)` hairline around photography.
- **Full-bleed**: only within the color-block tiles, where the image runs to the padded edge of the accent fill.
- **Hover**: static — no zoom, no scale, no opacity shift. The headline below is the only interactive response.
- **Lazy loading**: `loading="lazy"` on everything below the first fold; eager on the masthead hero only.
### StoryStream Timeline Item (Distinctive)
- Vertical rail line (1px `#3d00bf` or `rgba(19, 19, 19, 0.1)` on `#ffffff`)
- Mono timestamp on the left in Space Mono 11px / UPPERCASE
- Pill-cornered body card (20px radius) with kicker, headline, and optional deck
- Stacked vertically with 1216px gap, the rail continuing between them
- Often interleaved with full-bleed accent tiles that "break" the timeline rhythm for emphasis
## 5. Layout Principles
### Spacing System
- **Base unit**: 8px.
- **Scale**: 1, 2, 4, 5, 6, 8, 9, 10, 12, 14, 15, 16, 20, 24, 25px.
- **Section padding**: 3272px vertical between major feed sections. StoryStream items themselves are tighter — 1216px gaps.
- **Card padding**: 2032px interior. Feature cards expand to 4048px.
- **Inline spacing**: kickers sit ~610px above headlines; headlines sit ~1014px above decks; timestamps sit ~68px below decks.
- **Micro-scale**: The 2/4/5/6/9/10px values are used inside buttons, pills, and tight label clusters, not in the editorial grid.
### Grid & Container
- **Max width**: ~12801300px.
- **Column patterns**: a 12-column underlying grid that resolves into 3-column hero + 1-column StoryStream rail + feature panels. Color-block tiles frequently span 23 columns.
- **Container padding**: 24px mobile / 48px desktop on the outer edges.
- **Gutters**: 1624px between columns, tighter (812px) inside StoryStream items.
### Whitespace Philosophy
The Verge treats whitespace as a dramatic reset between loud moments. The light canvas allows for more breathing room, creating a balanced rhythm between content and empty space. The page is airy but still maintains the Verge's characteristic boldness through color and typography.
### Border Radius Scale
- **2px** — inputs, small badges (feels like a typewriter tag)
- **3px** — inline images (just enough to soften against the canvas)
- **4px** — nested card images and small button variants
- **20px** — standard pill cards and color-block tiles
- **24px** — feature tile radius and primary button pill
- **30px** — large promotional buttons
- **40px** — outlined CTA pills (the loudest pill in the system)
- **50%** — avatar circles, icon buttons, and certain round badges
## 6. Depth & Elevation
| Level | Treatment | Use |
| ----- | ----------------------------------------------------------------- | -------------------------------------------------------- |
| 0 | No border, no shadow | Default `#ffffff` canvas text |
| 1 | `rgba(0,0,0,0) 0px 0px 0px 0px inset` (placeholder) | Reset state for interactive elements |
| 2 | `1px solid rgba(19, 19, 19, 0.1)` or `#d4d4d4` hairline | Image frames and quiet card outlines |
| 3 | `1px solid #3cffd0` hairline | Active button outlines, focused story tiles |
| 4 | `1px solid #5200ff` hairline | Promotional/alternate state outlines |
| 5 | `rgba(19, 19, 19, 0.33) 0px 0px 0px 1px` | The single "atmospheric" ring — applied to layered cards |
| 6 | `0px -1px 0px 0px inset` (mint/black/white) | Active tab underline — a signature Verge move |
| 7 | Saturated accent fill (`#3cffd0`, `#5200ff`, white, yellow, pink) | Story-tile elevation via color, not shadow |
The Verge's depth philosophy in light mode remains **color-as-elevation**. When something needs to stand out, it gets a mint fill or a 1px hazard-color border rather than a shadow. The `#ffffff` canvas stays clean and flat, with hierarchy carried by color saturation.
### Decorative Depth
- **1px inset underline** on active tabs/nav links (mint, black, or white depending on context)
- **Subtle** **`rgba(19, 19, 19, 0.33)`** **1px ring** on stacked cards — the only effect that faintly resembles a shadow
- **No gradients, no glows, no atmospheric blurs** anywhere. The hazard-tape aesthetic would break if anything faded softly.
## 7. Do's and Don'ts
### Do
- **Do** use `#ffffff` as the canvas for every view in light mode.
- **Do** use Jelly Mint (`#3cffd0`) and Verge Ultraviolet (`#5200ff`) as hazard accents — buttons, borders, active states, and saturated color-block tiles.
- **Do** use Anton exclusively at 60px+ for hero headlines. Treat anything smaller as a bug.
- **Do** round everything: 20px for cards, 24px for feature cards, 3040px for pill buttons.
- **Do** use Space Mono for UPPERCASE labels, timestamps, kickers, and button text. Lowercase mono doesn't exist here.
- **Do** apply 1.51.9px letter-spacing to every ALL-CAPS label — this is a Verge signature.
- **Do** use saturated color-block tiles (mint, purple, yellow, pink, orange, white) to elevate a story — never a drop shadow.
- **Do** use `#3860be` (deep link blue) as the hover color on every link, regardless of base color.
- **Do** apply the StoryStream timeline rail (1px dashed/solid `#3d00bf` or `rgba(19, 19, 19, 0.1)`) on feed views.
- **Do** use thin-weight (300) Space Grotesk at 1920px with 1.9px tracking for "fashion-whisper" capitalized eyebrows.
### Don't
- **Don't** use a dark background in light mode. The white canvas is the product.
- **Don't** add `box-shadow` for elevation. Use 1px borders or saturated accent fills instead.
- **Don't** use square corners. Every interactive and content container is rounded.
- **Don't** use Anton for UI, buttons, or body copy. It's strictly display.
- **Don't** use lowercase mono. Space Mono is always UPPERCASE.
- **Don't** let mint and ultraviolet appear as background washes — they're hazard accents, not canvas tints.
- **Don't** use gradients anywhere. The system is solid color blocks only.
- **Don't** introduce new accent colors outside the declared mint / purple / yellow / pink / orange tile palette.
- **Don't** pair Anton with Newsreader in the same headline cluster — Anton is the only display shout, serif pulls are reserved for body moments.
## 8. Responsive Behavior
### Breakpoints
| Name | Width | Key Changes |
| ------------- | ----------- | ---------------------------------------------------------------------------------------------------- |
| Small Mobile | <400px | Single column, Anton hero scales down to ~4854px, StoryStream rail collapses to inline timestamps |
| Mobile | 400549px | Single column, color-block tiles stack full-width, nav is a hamburger drawer |
| Large Mobile | 550767px | Still single column but padding opens up, tile radii stay at 20px |
| Tablet | 7681023px | 2-column StoryStream with feature card spanning, wordmark shrinks ~50% |
| Small Desktop | 10241179px | Full 34 column editorial grid, mint pill CTA restored to nav |
| Desktop | 11801299px | Max padding, Anton wordmark at full hero scale |
| Large Desktop | ≥1300px | Container caps at ~12801300px, whitespace expands at the margins, no further scaling |
### Touch Targets
- Primary pill buttons are ~44px minimum height (12px vertical padding + 13px text + 2px border) — meets WCAG AA.
- Mono uppercase nav links are smaller (~2832px tall) — for derivative work, pad to 44px on mobile.
- Circle icon buttons are 4044px circles, touch-friendly.
### Collapsing Strategy
- **Nav**: wordmark scales from hero (Anton 60104px) to ~2432px on mobile. Category links collapse to a hamburger drawer below 900px.
- **Grid**: 4-col → 3-col → 2-col → 1-col. Feature cards that span 2 columns on desktop reflow to full-width single-column on mobile.
- **Spacing**: section padding tightens from 72px → 56px → 48px. Tile interior padding tightens from 32px → 20px.
- **Type**: Anton hero scales from 104px to ~4854px on mobile. Space Grotesk headlines scale from 34px → 24px. Mono labels stay pinned at 1112px (they don't shrink further or they become unreadable).
- **Color tiles**: accent story blocks never lose saturation on mobile — they just reflow to full width.
### Image Behavior
- Responsive raster via `srcset`, aspect ratios preserved.
- No art-direction swaps — same crop scales across all viewports.
- `loading="lazy"` on everything below the fold, `eager` on the masthead hero.
- Images inside color-block tiles inherit the tile's inner radius (4px or 20px nested).
## 9. Agent Prompt Guide
### Quick Color Reference
- **Primary CTA**: "Jelly Mint (`#3cffd0`)"
- **Background (Canvas)**: "Canvas White (`#ffffff`)"
- **Accent (Secondary Hazard)**: "Verge Ultraviolet (`#5200ff`)"
- **Heading Text**: "Primary Text (`#131313`)"
- **Body Text**: "Primary Text (`#131313`)" (primary) or "Secondary Text (`#6a6a6a`)"
- **Secondary Text / Metadata**: "Secondary Text (`#6a6a6a`)"
- **Card Border**: "`rgba(19, 19, 19, 0.1)`" hairline on light, "Console Mint Border (`#309875`)" on mint variants
- **Link Hover**: "Deep Link Blue (`#3860be`)"
### Example Component Prompts
1. *"Create a StoryStream timeline item on a* *`#ffffff`* *canvas: a 20px-radius rectangle with a 1px solid* *`rgba(19, 19, 19, 0.1)`* *border, a Space Mono 11px / 700 / UPPERCASE / 1.1px tracking timestamp on the left rail, a 12px Space Grotesk UPPERCASE kicker in mint (`#3cffd0`), and a 24px / 700 Space Grotesk headline in black below. No shadow, no lift — hover only shifts the headline color to* *`#3860be`."*
2. *"Design a primary subscribe button with a Jelly Mint (`#3cffd0`) fill, black text in Space Mono 12px / 700 / UPPERCASE / 1.5px tracking, 24px border radius, 12px × 28px padding. Hover state shifts to* *`rgba(60, 255, 208, 0.3)`* *background with a 1px* *`#309875`* *ring, 180ms ease."*
3. *"Build a feature hero with a 104px Anton 400 headline in black with 1px letter-spacing and 0.95 line-height, a thin-weight 300 Space Grotesk 19px capitalized kicker above with 1.9px tracking, on a* *`#ffffff`* *canvas with 72px vertical padding."*
4. *"Create a color-block accent tile filled with Verge Ultraviolet (`#5200ff`), 24px border radius, white text, a Space Mono 11px UPPERCASE category label with 1.5px tracking at the top, and a 32px Space Grotesk 700 headline below."*
5. *"Design a dark secondary button with a* *`#131313`* *background,* *`#ffffff`* *Space Mono 12px text, 24px radius pill shape, 12px × 28px padding. Hover matches the primary button —* *`rgba(19, 19, 19, 0.2)`* *bg with black text."*
### Iteration Guide
When refining existing screens generated with this design system:
1. **Audit the canvas.** If you see a dark background anywhere in light mode, flatten it to `#ffffff`.
2. **Audit corners.** Every rectangle should land on 2/3/4/20/24/30/40px or 50%. Square corners break the voice.
3. **Audit shadows.** Strip every `box-shadow` that isn't a 1px inset underline or a 1px hazard-color border. The Verge uses color for elevation, not shadow.
4. **Audit type roles.** Anton only ≥60px. Space Mono only UPPERCASE. Space Grotesk 300 at 1920px should have 1.9px tracking. Newsreader only for body/magazine moments, never UI.
5. **Audit accent usage.** Mint and ultraviolet should appear as hazard accents — buttons, 1px borders, active underlines, saturated tile fills. If they're appearing as background washes or gradient fades, correct to solid blocks.
6. **Audit labels.** Every kicker, timestamp, category tag, and button label should be ALL CAPS with 1.11.9px letter-spacing. Missing tracking = missing voice.
7. **Audit link hover.** Every link, regardless of its base color, should hover to `#3860be` deep link blue with no underline. Any other hover color is drift.

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>UIToCode</title>
<title>项目名称</title>
</head>
<body>
<div id="root"></div>

1483
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
{
"name": "UIToCode",
"name": "project-name",
"private": true,
"version": "0.0.0",
"type": "module",
@ -10,30 +10,28 @@
"preview": "vite preview"
},
"dependencies": {
"@tanstack/react-query": "^5.99.2",
"@types/js-cookie": "^3.0.6",
"axios": "^1.15.1",
"i18next": "^26.0.6",
"i18next-browser-languagedetector": "^8.2.1",
"js-cookie": "^3.0.5",
"posthog-js": "^1.369.3",
"posthog-node": "^5.29.2",
"@radix-ui/react-icons": "^1.3.2",
"axios": "^1.6.7",
"i18next": "^23.10.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^7.14.2",
"react-i18next": "^17.0.4",
"react-zoom-pan-pinch": "^4.0.3"
"react-i18next": "^14.1.0",
"react-router-dom": "^6.22.3"
},
"devDependencies": {
"@types/react": "^18.2.43",
"@types/react-dom": "^18.2.17",
"@typescript-eslint/eslint-plugin": "^6.14.0",
"@typescript-eslint/parser": "^6.14.0",
"@types/react": "^18.2.56",
"@types/react-dom": "^18.2.19",
"@typescript-eslint/eslint-plugin": "^7.0.2",
"@typescript-eslint/parser": "^7.0.2",
"@vitejs/plugin-react": "^4.2.1",
"eslint": "^8.55.0",
"autoprefixer": "^10.4.17",
"eslint": "^8.56.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5",
"postcss": "^8.4.35",
"prettier": "^3.2.5",
"tailwindcss": "^3.4.1",
"typescript": "^5.2.2",
"vite": "^5.0.8"
"vite": "^5.1.4"
}
}

6
postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@ -1,26 +0,0 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
margin: 0;
font-family: var(--font-family-primary);
font-weight: var(--font-weight-regular);
letter-spacing: var(--letter-spacing-normal);
font-kerning: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
overflow: hidden;
background-color: var(--color-background);
color: var(--color-text-primary);
}
.app-container {
position: relative;
width: 100vw;
height: 100vh;
overflow: hidden;
background-color: var(--color-background);
}

View File

@ -1,31 +1,27 @@
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'
import Login from './pages/auth/Login'
import Register from './pages/auth/Register'
import Projects from './pages/app/Projects'
import MainApp from './pages/app/MainApp'
import './App.css'
// 路由保护组件
const ProtectedRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const isLoggedIn = localStorage.getItem('isLoggedIn')
return isLoggedIn ? <>{children}</> : <Navigate to="/login" />
}
import ForgotPassword from './pages/auth/ForgotPassword'
import Layout from './components/layout/Layout'
import Dashboard from './pages/dashboard/Dashboard'
import Settings from './pages/settings/Settings'
import ProjectList from './pages/projects/ProjectList'
function App() {
return (
<Router>
<Routes>
<Route path="/" element={<Navigate to="/login" replace />} />
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route path="/projects" element={<ProtectedRoute><Projects /></ProtectedRoute>} />
<Route path="/app" element={<ProtectedRoute><MainApp /></ProtectedRoute>} />
<Route path="/" element={<Navigate to="/login" />} />
<Route path="/forgot-password" element={<ForgotPassword />} />
<Route path="/dashboard" element={<Layout><Dashboard /></Layout>} />
<Route path="/projects" element={<Layout><ProjectList /></Layout>} />
<Route path="/settings" element={<Layout><Settings /></Layout>} />
<Route path="*" element={<Navigate to="/login" replace />} />
</Routes>
</Router>
)
}
export default App
export default App

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-react"><path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z"/><path d="M12 8a4 4 0 0 0-4 4 4 4 0 0 0 4 4 4 4 0 0 0 4-4 4 4 0 0 0-4-4z"/></svg>

Before

Width:  |  Height:  |  Size: 350 B

View File

@ -1,528 +0,0 @@
.ai-chat-panel {
position: absolute;
top: 100px;
right: 20px;
bottom: 20px;
width: 350px;
background-color: var(--color-white);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-card);
display: flex;
flex-direction: column;
z-index: 99;
box-shadow: 0 2px 4px 0 rgba(0,0,0,0.1);
}
.chat-content {
position: relative;
display: flex;
flex: 1;
overflow: hidden;
z-index: 20;
border-radius: var(--radius-card);
background-color: var(--color-white);
}
.chat-header {
padding: var(--space-7) var(--space-9);
border-bottom: 1px solid var(--color-border-subtle);
display: flex;
justify-content: space-between;
align-items: center;
border-radius: var(--radius-card) var(--radius-card) 0 0;
}
.chat-header h3 {
margin: 0;
font-size: var(--font-size-heading-3);
font-weight: var(--font-weight-bold);
color: var(--color-text-primary);
letter-spacing: var(--letter-spacing-normal);
font-family: var(--font-family-primary);
font-feature-settings: "ss01", "ss02";
}
.header-left {
display: flex;
align-items: center;
gap: var(--space-4);
}
.header-right {
display: flex;
align-items: center;
gap: var(--space-4);
}
.new-chat-btn {
padding: var(--space-3);
background-color: var(--color-soft-gray);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-circle);
cursor: pointer;
transition: all 0.15s ease;
display: flex;
align-items: center;
justify-content: center;
color: var(--color-text-primary);
}
.new-chat-btn:hover {
background-color: var(--color-button-hover);
color: var(--color-white);
border-color: var(--color-meta-blue);
}
.new-chat-btn:focus-visible {
outline: 3px ring var(--color-meta-blue);
outline: auto 2px var(--color-meta-blue);
outline-offset: 2px;
}
.history-btn {
padding: var(--space-3);
background-color: var(--color-soft-gray);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-circle);
cursor: pointer;
transition: all 0.15s ease;
display: flex;
align-items: center;
justify-content: center;
color: var(--color-text-primary);
}
.history-btn:hover {
background-color: var(--color-button-hover);
color: var(--color-white);
border-color: var(--color-meta-blue);
}
.history-btn:focus-visible {
outline: 3px ring var(--color-meta-blue);
outline: auto 2px var(--color-meta-blue);
outline-offset: 2px;
}
.history-btn.active {
background-color: var(--color-meta-blue);
border-color: var(--color-meta-blue);
color: var(--color-white);
}
.model-selector {
position: relative;
}
.model-select {
padding: var(--space-3) var(--space-5);
background-color: var(--color-soft-gray);
border: 1px solid var(--color-border-medium);
border-radius: var(--radius-small);
color: var(--color-text-primary);
font-size: var(--font-size-caption);
font-family: var(--font-family-primary);
letter-spacing: var(--letter-spacing-tighter);
cursor: pointer;
outline: none;
transition: border-color 200ms ease, box-shadow 200ms ease;
}
.model-select:hover {
border-color: var(--color-meta-blue);
}
.model-select:focus {
border-color: hsl(214, 89%, 52%);
box-shadow: 0 0 0 3px rgba(0, 100, 224, 0.1);
}
.model-select:focus-visible {
outline: 3px ring var(--color-meta-blue);
outline: auto 2px var(--color-meta-blue);
outline-offset: 2px;
}
.conversation-sidebar {
position: absolute;
left: 0;
top: 0;
width: 250px;
height: 200px;
transform: translateX(0);
background-color: var(--color-white);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-card);
transition: transform 0.3s ease;
z-index: 10;
overflow: hidden;
display: flex;
flex-direction: column;
box-shadow: 0 2px 4px 0 rgba(0,0,0,0.1);
}
.conversation-sidebar.visible {
transform: translateX(-101%);
}
.conversation-sidebar h4 {
margin: 0;
padding: var(--space-5) var(--space-7);
font-family: var(--font-family-primary);
font-size: var(--font-size-caption-bold);
font-weight: var(--font-weight-bold);
color: var(--color-text-primary);
border-bottom: 1px solid var(--color-border-subtle);
flex-shrink: 0;
letter-spacing: var(--letter-spacing-normal);
}
.conversation-list {
overflow-y: auto;
flex: 1;
}
.conversation-item {
padding: var(--space-4) var(--space-7);
cursor: pointer;
transition: background-color 0.15s ease;
border-bottom: 1px solid var(--color-border-subtle);
color: var(--color-text-primary);
}
.conversation-item:hover {
background-color: var(--color-soft-gray);
}
.conversation-item:focus-visible {
outline: 3px ring var(--color-meta-blue);
outline: auto 2px var(--color-meta-blue);
outline-offset: 2px;
}
.conversation-item.active {
background-color: var(--color-meta-blue);
color: var(--color-white);
}
.conversation-title {
font-size: var(--font-size-body);
font-weight: var(--font-weight-medium);
margin-bottom: var(--space-2);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
letter-spacing: var(--letter-spacing-normal);
font-family: var(--font-family-primary);
}
.conversation-time {
font-family: var(--font-family-secondary);
font-size: var(--font-size-small);
color: var(--color-text-muted);
letter-spacing: var(--letter-spacing-normal);
}
.conversation-item.active .conversation-time {
color: var(--color-white);
opacity: 0.8;
}
.chat-main {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
border-radius: var(--radius-card) var(--radius-card) 0 0;
}
.bottom-model-selector {
padding: 0 var(--space-7) var(--space-3);
display: flex;
justify-content: center;
align-items: center;
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: var(--space-9);
display: flex;
flex-direction: column;
gap: var(--space-7);
}
.message {
display: flex;
flex-direction: column;
}
.message.user {
align-items: flex-end;
}
.message.ai {
align-items: flex-start;
}
.message-content {
max-width: 80%;
padding: var(--space-5) var(--space-7);
border-radius: var(--radius-card);
font-size: var(--font-size-body);
line-height: var(--line-height-body);
letter-spacing: var(--letter-spacing-normal);
font-family: var(--font-family-primary);
box-shadow: 0 2px 4px 0 rgba(0,0,0,0.1);
}
.message.user .message-content {
background-color: var(--color-meta-blue);
color: var(--color-white);
border: 1px solid var(--color-meta-blue);
}
.message.ai .message-content {
background-color: var(--color-soft-gray);
color: var(--color-text-primary);
border: 1px solid var(--color-border-subtle);
}
.message.ai.loading .message-content {
background-color: var(--color-soft-gray);
color: var(--color-text-primary);
opacity: 0.7;
border: 1px solid var(--color-border-subtle);
}
.loading-indicator {
display: flex;
align-items: center;
gap: var(--space-3);
}
.loading-indicator span {
animation: pulse 1.5s infinite;
}
.loading-indicator span:nth-child(2) {
animation-delay: 0.2s;
}
.loading-indicator span:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes pulse {
0%, 100% { opacity: 0.5; }
50% { opacity: 1; }
}
.upload-progress {
padding: var(--space-5) var(--space-9);
background-color: var(--color-soft-gray);
border-bottom: 1px solid var(--color-border-subtle);
color: var(--color-text-primary);
}
.upload-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--space-3) 0;
font-size: var(--font-size-body);
letter-spacing: var(--letter-spacing-normal);
font-family: var(--font-family-primary);
}
.upload-status {
font-family: var(--font-family-secondary);
font-size: var(--font-size-small);
color: var(--color-text-muted);
letter-spacing: var(--letter-spacing-normal);
}
.context-display {
padding: var(--space-5) var(--space-9);
background-color: var(--color-soft-gray);
border-bottom: 1px solid var(--color-border-subtle);
display: flex;
align-items: center;
gap: var(--space-4);
}
.context-icon {
font-size: 16px;
color: var(--color-meta-blue);
}
.context-text {
flex: 1;
font-size: var(--font-size-body);
color: var(--color-text-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
letter-spacing: var(--letter-spacing-normal);
font-family: var(--font-family-primary);
}
.context-clear {
background: none;
border: none;
color: var(--color-text-primary);
font-size: 18px;
cursor: pointer;
padding: var(--space-2);
line-height: 1;
transition: all 0.15s ease;
border-radius: var(--radius-circle);
}
.context-clear:hover {
background-color: var(--color-button-hover);
color: var(--color-white);
}
.context-clear:focus-visible {
outline: 3px ring var(--color-meta-blue);
outline: auto 2px var(--color-meta-blue);
outline-offset: 2px;
}
.chat-input {
padding: var(--space-7);
border-top: 1px solid var(--color-border-subtle);
display: flex;
flex-direction: column;
gap: var(--space-5);
}
.input-container {
position: relative;
width: 100%;
}
.chat-input textarea {
width: 100%;
padding: var(--space-5);
background-color: var(--color-white);
border: 1px solid var(--color-border-medium);
border-radius: var(--radius-small);
color: var(--color-text-primary);
font-size: var(--font-size-body);
resize: none;
font-family: var(--font-family-primary);
height: 120px;
padding-bottom: 50px;
letter-spacing: var(--letter-spacing-normal);
transition: border-color 200ms ease, box-shadow 200ms ease;
box-shadow: 0 2px 4px 0 rgba(0,0,0,0.1);
}
.chat-input textarea:focus {
outline: none;
border-color: hsl(214, 89%, 52%);
box-shadow: 0 0 0 3px rgba(0, 100, 224, 0.1);
}
.chat-input textarea:focus-visible {
outline: 3px ring var(--color-meta-blue);
outline: auto 2px var(--color-meta-blue);
outline-offset: 2px;
}
.chat-input textarea::placeholder {
color: var(--color-text-placeholder);
}
.absolute-btn {
position: absolute;
bottom: 15px;
background-color: var(--color-soft-gray);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-pill);
cursor: pointer;
transition: all 0.15s ease;
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
color: var(--color-text-primary);
}
.absolute-btn:focus-visible {
outline: 3px ring var(--color-meta-blue);
outline: auto 2px var(--color-meta-blue);
outline-offset: 2px;
}
.absolute-btn.upload-btn {
left: 12px;
padding: var(--space-3);
border-radius: var(--radius-circle);
}
.absolute-btn.upload-btn:hover {
background-color: var(--color-button-hover);
color: var(--color-white);
border-color: var(--color-meta-blue);
}
.absolute-btn.model-selector-btn {
left: 50%;
transform: translateX(-50%);
padding: 0;
background: none;
border: none;
}
.absolute-btn.model-selector-btn .model-select {
padding: var(--space-3) var(--space-5);
background-color: var(--color-soft-gray);
border: 1px solid var(--color-border-medium);
border-radius: var(--radius-small);
color: var(--color-text-primary);
font-size: var(--font-size-caption);
font-family: var(--font-family-primary);
letter-spacing: var(--letter-spacing-tighter);
cursor: pointer;
outline: none;
transition: border-color 200ms ease, box-shadow 200ms ease;
}
.absolute-btn.send-btn {
right: 12px;
padding: var(--space-3) var(--space-7);
background-color: var(--color-meta-blue);
color: var(--color-white);
border-color: var(--color-meta-blue);
font-family: var(--font-family-primary);
font-size: var(--font-size-button);
font-weight: var(--font-weight-regular);
letter-spacing: var(--letter-spacing-tighter);
transition: background 200ms ease, transform 150ms ease;
}
.absolute-btn.send-btn:hover:not(:disabled) {
background-color: var(--color-meta-blue-hover);
transform: scale(1.05);
}
.absolute-btn.send-btn:focus-visible {
outline: 3px ring var(--color-meta-blue);
outline: auto 2px var(--color-meta-blue);
outline-offset: 2px;
}
.absolute-btn.send-btn:disabled {
background-color: var(--color-soft-gray);
border-color: var(--color-border-subtle);
color: var(--color-text-muted);
opacity: 0.6;
cursor: not-allowed;
}
.file-upload-hidden {
display: none;
}

View File

@ -1,313 +0,0 @@
import React, { useState, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { NewChatIcon, PaperclipIcon, HistoryIcon } from './Icons'
import './AIChatPanel.css'
const AIChatPanel = () => {
// 消息状态
const { t } = useTranslation()
const [messages, setMessages] = useState([
{ id: 1, type: 'ai', content: t('aiChatPanel.greeting') }
])
const [inputValue, setInputValue] = useState('')
const [isLoading, setIsLoading] = useState(false)
// 上传功能状态
const [uploadingFiles, setUploadingFiles] = useState<string[]>([])
const [_pendingFiles, setPendingFiles] = useState<{ fileName: string; originalName: string; fullPath: string }[]>([])
const [currentContext, setCurrentContext] = useState<{ fileName: string; type: string } | null>(null)
// 模型切换状态
const [selectedModel, setSelectedModel] = useState<string>('gpt-4o')
// 对话历史状态
const [conversations, setConversations] = useState([{
id: 1,
title: t('aiChatPanel.newChat'),
messages: messages,
createdAt: new Date()
}])
const [currentConversationId, setCurrentConversationId] = useState(1)
// 侧边栏显示状态
const [isSidebarVisible, setIsSidebarVisible] = useState(false)
// 输入框引用
const inputRef = useRef<HTMLTextAreaElement>(null)
// 发送消息
const handleSendMessage = () => {
if (!inputValue.trim()) return
// 添加用户消息
const userMessage = {
id: Date.now(),
type: 'user' as const,
content: inputValue
}
setMessages(prev => [...prev, userMessage])
setInputValue('')
setIsLoading(true)
// 模拟AI回复
setTimeout(() => {
const aiMessage = {
id: Date.now() + 1,
type: 'ai' as const,
content: t('aiChatPanel.response')
}
setMessages(prev => [...prev, aiMessage])
setIsLoading(false)
}, 1000)
}
// 处理文件上传
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files
if (!files) return
Array.from(files).forEach(file => {
const fileName = file.name
setUploadingFiles(prev => [...prev, fileName])
// 模拟上传过程
setTimeout(() => {
setUploadingFiles(prev => prev.filter(f => f !== fileName))
setPendingFiles(prev => [...prev, {
fileName: fileName,
originalName: fileName,
fullPath: `./uploads/${fileName}`
}])
}, 1000)
})
}
// 清除上下文
const handleClearContext = () => {
setCurrentContext(null)
}
// 处理键盘事件
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
handleSendMessage()
}
}
// 拖拽上传事件处理
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault()
e.stopPropagation()
e.dataTransfer.dropEffect = 'copy'
}
const handleDragEnter = (e: React.DragEvent) => {
e.preventDefault()
e.stopPropagation()
}
const handleDragLeave = (e: React.DragEvent) => {
e.preventDefault()
e.stopPropagation()
}
const handleDrop = (e: React.DragEvent) => {
e.preventDefault()
e.stopPropagation()
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
// const files = Array.from(e.dataTransfer.files)
}
}
// 处理新对话
const handleNewChat = () => {
const newId = conversations.length + 1
const newConversation = {
id: newId,
title: t('aiChatPanel.newChat'),
messages: [{
id: 1,
type: 'ai',
content: t('aiChatPanel.greeting')
}],
createdAt: new Date()
}
setConversations(prev => [...prev, newConversation])
setCurrentConversationId(newId)
setMessages(newConversation.messages)
}
// 处理模型选择
const handleModelChange = (model: string) => {
setSelectedModel(model)
}
// 处理对话选择
const handleConversationSelect = (id: number) => {
const conversation = conversations.find(c => c.id === id)
if (conversation) {
setCurrentConversationId(id)
setMessages(conversation.messages)
}
}
// 切换侧边栏显示
const toggleSidebar = () => {
setIsSidebarVisible(!isSidebarVisible)
}
return (
<div className="ai-chat-panel">
<div className={`conversation-sidebar ${isSidebarVisible ? 'visible' : ''}`}>
<h4>{t('aiChatPanel.conversationHistory')}</h4>
<div className="conversation-list">
{conversations.map(conversation => (
<div
key={conversation.id}
className={`conversation-item ${currentConversationId === conversation.id ? 'active' : ''}`}
onClick={() => handleConversationSelect(conversation.id)}
>
<div className="conversation-title">{conversation.title}</div>
<div className="conversation-time">
{conversation.createdAt.toLocaleTimeString()}
</div>
</div>
))}
</div>
</div>
<div className="chat-content">
<div className="chat-main">
<div className="chat-header">
<div className="header-left">
<h3>{t('aiChatPanel.aiAssistant')}</h3>
</div>
<div className="header-right">
<button className={`history-btn ${isSidebarVisible ? 'active' : ''}`} onClick={toggleSidebar} title={t('aiChatPanel.conversationHistory')}>
<HistoryIcon />
</button>
<button className="new-chat-btn" onClick={handleNewChat} title={t('aiChatPanel.newChat')}>
<NewChatIcon />
</button>
</div>
</div>
{currentContext && (
<div className="context-display">
<span className="context-icon">📄</span>
<span className="context-text">
{currentContext.type === 'design' ? 'Design file: ' : 'Other file: '}
{currentContext.fileName}
</span>
<button
className="context-clear"
onClick={handleClearContext}
title={t('aiChatPanel.clearContext')}
>
×
</button>
</div>
)}
{uploadingFiles.length > 0 && (
<div className="upload-progress">
{uploadingFiles.map(file => (
<div key={file} className="upload-item">
<span>{file}</span>
<span className="upload-status">{t('aiChatPanel.uploading')}</span>
</div>
))}
</div>
)}
<div className="chat-messages">
{messages.map(message => (
<div key={message.id} className={`message ${message.type}`}>
<div className="message-content">
{message.content}
</div>
</div>
))}
{isLoading && (
<div className="message ai loading">
<div className="message-content">
<div className="loading-indicator">
<span>{t('aiChatPanel.loading')}</span>
<span>.</span>
<span>.</span>
<span>.</span>
</div>
</div>
</div>
)}
</div>
<div
className="chat-input"
onDragOver={handleDragOver}
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
<div className="input-container">
<textarea
ref={inputRef}
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyPress={handleKeyPress}
placeholder={t('aiChatPanel.inputPlaceholder')}
rows={4}
/>
<input
type="file"
multiple
onChange={handleFileUpload}
className="file-upload-hidden"
id="file-upload"
/>
<button
className="absolute-btn upload-btn"
onClick={() => document.getElementById('file-upload')?.click()}
title="Upload files"
>
<PaperclipIcon />
</button>
{/* 模型选择器 */}
<div className="absolute-btn model-selector-btn">
<select
value={selectedModel}
onChange={(e) => handleModelChange(e.target.value)}
className="model-select"
>
<option value="gpt-4o">{t('aiChatPanel.models.gpt4o')}</option>
<option value="claude-4-sonnet">{t('aiChatPanel.models.claude4')}</option>
<option value="gemini-pro">{t('aiChatPanel.models.gemini')}</option>
<option value="mistral-7b">{t('aiChatPanel.models.mistral')}</option>
</select>
</div>
{/* 发送按钮 */}
<button
className="absolute-btn send-btn"
onClick={handleSendMessage}
disabled={!inputValue.trim() || isLoading}
title={t('aiChatPanel.send')}
>
{t('aiChatPanel.send')}
</button>
</div>
</div>
</div>
</div>
</div>
)
}
export default AIChatPanel

View File

@ -1,93 +0,0 @@
import React from 'react';
import { ConnectionLine } from '../types/canvas.types';
interface ConnectionLinesProps {
connections: ConnectionLine[];
containerBounds: { width: number; height: number };
isVisible: boolean;
zoomLevel: number;
}
const ConnectionLines: React.FC<ConnectionLinesProps> = ({
connections,
containerBounds,
isVisible,
zoomLevel
}) => {
if (!isVisible || connections.length === 0) {
return null;
}
const getLineStyle = (connection: ConnectionLine) => ({
stroke: connection.color || 'var(--vscode-textLink-foreground)',
strokeWidth: (connection.width || 2) / zoomLevel,
strokeDasharray: zoomLevel < 0.5 ? '5,5' : 'none',
opacity: Math.max(0.3, Math.min(1, zoomLevel)),
markerEnd: 'url(#arrowhead)'
});
const createCurvePath = (from: { x: number; y: number }, to: { x: number; y: number }) => {
const dx = to.x - from.x;
const cp1x = from.x + dx * 0.6;
const cp1y = from.y;
const cp2x = to.x - dx * 0.6;
const cp2y = to.y;
return `M ${from.x} ${from.y} C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${to.x} ${to.y}`;
};
return (
<svg
className="connection-lines"
style={{
position: 'absolute',
top: 0,
left: 0,
width: containerBounds.width,
height: containerBounds.height,
pointerEvents: 'none',
zIndex: 1,
overflow: 'visible'
}}
>
<defs>
<marker
id="arrowhead"
markerWidth="10"
markerHeight="7"
refX="9"
refY="3.5"
orient="auto"
fill="var(--vscode-textLink-foreground)"
>
<polygon points="0 0, 10 3.5, 0 7" />
</marker>
</defs>
{connections.map((connection) => (
<g key={connection.id} className="connection-group">
<path
d={createCurvePath(connection.fromPosition, connection.toPosition)}
fill="none"
style={getLineStyle(connection)}
className="connection-line"
/>
<path
d={createCurvePath(connection.fromPosition, connection.toPosition)}
fill="none"
stroke="transparent"
strokeWidth="10"
className="connection-line-hover-target"
style={{ pointerEvents: 'stroke' }}
>
<title>{`${connection.fromFrame}${connection.toFrame}`}</title>
</path>
</g>
))}
</svg>
);
};
export default ConnectionLines;

View File

@ -1,224 +0,0 @@
.context-modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--color-overlay);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.context-modal {
background-color: var(--color-white);
border-radius: var(--radius-card);
border: 1px solid var(--color-border-subtle);
width: 400px;
max-width: 90%;
max-height: 80%;
display: flex;
flex-direction: column;
position: relative;
z-index: 1001;
box-shadow: 0 12px 28px 0 rgba(0,0,0,0.2), 0 2px 4px 0 rgba(0,0,0,0.1);
}
.context-modal-header {
padding: var(--space-9);
border-bottom: 1px solid var(--color-border-subtle);
display: flex;
align-items: center;
justify-content: space-between;
}
.context-modal-header h3 {
margin: 0;
font-size: var(--font-size-heading-3);
font-weight: var(--font-weight-bold);
color: var(--color-text-primary);
letter-spacing: var(--letter-spacing-normal);
font-family: var(--font-family-primary);
font-feature-settings: "ss01", "ss02";
}
.context-modal-close {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: var(--color-text-primary);
padding: 0;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-circle);
transition: background-color 0.15s ease;
}
.context-modal-close:hover {
background-color: var(--color-soft-gray);
}
.context-modal-close:focus-visible {
outline: 3px ring var(--color-meta-blue);
outline: auto 2px var(--color-meta-blue);
outline-offset: 2px;
}
.context-modal-body {
padding: var(--space-9);
flex: 1;
overflow-y: auto;
}
.context-form {
display: flex;
flex-direction: column;
gap: var(--space-7);
}
.form-group {
display: flex;
flex-direction: column;
gap: var(--space-3);
}
.form-group label {
font-family: var(--font-family-primary);
font-size: var(--font-size-caption-bold);
font-weight: var(--font-weight-bold);
color: var(--color-text-primary);
letter-spacing: var(--letter-spacing-normal);
}
.form-input {
padding: var(--space-5) var(--space-6);
border: 1px solid var(--color-border-medium);
border-radius: var(--radius-small);
font-size: var(--font-size-body);
font-family: var(--font-family-primary);
outline: none;
transition: border-color 200ms ease, box-shadow 200ms ease;
background-color: var(--color-white);
color: var(--color-text-primary);
letter-spacing: var(--letter-spacing-normal);
}
.form-input:focus {
border-color: hsl(214, 89%, 52%);
box-shadow: 0 0 0 3px rgba(0, 100, 224, 0.1);
}
.form-input:focus-visible {
outline: 3px ring var(--color-meta-blue);
outline: auto 2px var(--color-meta-blue);
outline-offset: 2px;
}
.form-input::placeholder {
color: var(--color-text-placeholder);
}
.form-textarea {
padding: var(--space-5) var(--space-6);
border: 1px solid var(--color-border-medium);
border-radius: var(--radius-small);
font-size: var(--font-size-body);
font-family: var(--font-family-primary);
resize: none;
outline: none;
transition: border-color 200ms ease, box-shadow 200ms ease;
min-height: 120px;
background-color: var(--color-white);
color: var(--color-text-primary);
letter-spacing: var(--letter-spacing-normal);
}
.form-textarea:focus {
border-color: hsl(214, 89%, 52%);
box-shadow: 0 0 0 3px rgba(0, 100, 224, 0.1);
}
.form-textarea:focus-visible {
outline: 3px ring var(--color-meta-blue);
outline: auto 2px var(--color-meta-blue);
outline-offset: 2px;
}
.form-textarea::placeholder {
color: var(--color-text-placeholder);
}
.context-modal-footer {
padding: var(--space-9);
border-top: 1px solid var(--color-border-subtle);
display: flex;
justify-content: flex-end;
gap: var(--space-5);
}
.context-modal-cancel {
background-color: var(--color-soft-gray);
color: var(--color-text-primary);
border: 1px solid var(--color-border-subtle);
padding: var(--space-4) var(--space-9);
border-radius: var(--radius-pill);
font-family: var(--font-family-primary);
font-size: var(--font-size-button);
font-weight: var(--font-weight-regular);
letter-spacing: var(--letter-spacing-tighter);
cursor: pointer;
transition: background 200ms ease, transform 150ms ease;
}
.context-modal-cancel:hover {
background-color: var(--color-button-hover);
color: var(--color-white);
transform: scale(1.05);
}
.context-modal-cancel:focus-visible {
outline: 3px ring var(--color-meta-blue);
outline: auto 2px var(--color-meta-blue);
outline-offset: 2px;
}
.context-modal-cancel:active {
opacity: 0.9;
transform: scale(0.95);
}
.context-modal-submit {
background-color: var(--color-meta-blue);
color: var(--color-white);
border: none;
padding: var(--space-4) var(--space-9);
border-radius: var(--radius-pill);
font-family: var(--font-family-primary);
font-size: var(--font-size-button);
font-weight: var(--font-weight-regular);
letter-spacing: var(--letter-spacing-tighter);
cursor: pointer;
transition: background 200ms ease, transform 150ms ease;
}
.context-modal-submit:hover {
background-color: var(--color-meta-blue-hover);
transform: scale(1.05);
}
.context-modal-submit:focus-visible {
outline: 3px ring var(--color-meta-blue);
outline: auto 2px var(--color-meta-blue);
outline-offset: 2px;
}
.context-modal-submit:active {
background-color: var(--color-meta-blue-pressed);
transform: scale(0.9);
opacity: 0.5;
}

View File

@ -1,91 +0,0 @@
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import './ContextModal.css'
interface ContextModalProps {
showModal: boolean
onClose: () => void
onSubmit: (title: string, description: string) => void
}
const ContextModal: React.FC<ContextModalProps> = ({ showModal, onClose, onSubmit }) => {
const { t } = useTranslation()
const [title, setTitle] = useState('')
const [description, setDescription] = useState('')
const handleSubmit = () => {
if (title.trim()) {
onSubmit(title, description)
setTitle('')
setDescription('')
}
}
const handleCancel = () => {
setTitle('')
setDescription('')
onClose()
}
if (!showModal) {
return null
}
return (
<div className="context-modal-overlay">
<div className="context-modal">
<div className="context-modal-header">
<h3>{t('contextModal.title')}</h3>
<button
className="context-modal-close"
onClick={handleCancel}
>
×
</button>
</div>
<div className="context-modal-body">
<form className="context-form">
<div className="form-group">
<label htmlFor="context-title">{t('contextModal.titleLabel')}</label>
<input
id="context-title"
type="text"
className="form-input"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder={t('contextModal.titlePlaceholder')}
/>
</div>
<div className="form-group">
<label htmlFor="context-description">{t('contextModal.descriptionLabel')}</label>
<textarea
id="context-description"
className="form-textarea"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder={t('contextModal.descriptionPlaceholder')}
rows={6}
></textarea>
</div>
</form>
</div>
<div className="context-modal-footer">
<button
className="context-modal-cancel"
onClick={handleCancel}
>
{t('contextModal.cancel')}
</button>
<button
className="context-modal-submit"
onClick={handleSubmit}
>
{t('contextModal.submit')}
</button>
</div>
</div>
</div>
)
}
export default ContextModal

View File

@ -1,735 +0,0 @@
import React, { useState, useEffect } from 'react';
import { DesignFile, ViewportMode, GridPosition, FrameDimensions } from '../types/canvas.types';
import { MobileIcon, TabletIcon, DesktopIcon, GlobeIcon } from './Icons';
import { useTranslation } from 'react-i18next';
interface DesignFrameProps {
file: DesignFile;
position: GridPosition;
dimensions: FrameDimensions;
isSelected: boolean;
onSelect: (fileName: string) => void;
renderMode?: 'placeholder' | 'iframe' | 'html';
showMetadata?: boolean;
viewport?: ViewportMode;
viewportDimensions?: FrameDimensions;
onViewportChange?: (fileName: string, viewport: ViewportMode) => void;
useGlobalViewport?: boolean;
onDragStart?: (fileName: string, startPos: GridPosition, mouseEvent: React.MouseEvent) => void;
isDragging?: boolean;
nonce?: string | null;
onSendToChat?: (fileName: string, prompt: string) => void;
}
const DesignFrame: React.FC<DesignFrameProps> = ({
file,
position,
dimensions,
isSelected,
onSelect,
renderMode = 'placeholder',
showMetadata = true,
viewport = 'desktop',
viewportDimensions,
onViewportChange,
useGlobalViewport = false,
onDragStart,
isDragging = false,
nonce = null,
onSendToChat
}) => {
const { t } = useTranslation()
const [isLoading, setIsLoading] = useState(renderMode === 'iframe');
const [hasError, setHasError] = useState(false);
const [dragPreventOverlay, setDragPreventOverlay] = useState(false);
const [showCopyDropdown, setShowCopyDropdown] = useState(false);
const [copyButtonState, setCopyButtonState] = useState<{ text: string; isSuccess: boolean }>({ text: t('designFrame.floatingActions.copyPromptBtn'), isSuccess: false });
const [copyPathButtonState, setCopyPathButtonState] = useState<{ text: string; isSuccess: boolean }>({ text: t('designFrame.floatingActions.copyPathBtn'), isSuccess: false });
const handleClick = () => {
onSelect(file.name);
};
const handleMouseDown = (e: React.MouseEvent) => {
if (onDragStart && e.button === 0) {
e.preventDefault();
e.stopPropagation();
setDragPreventOverlay(true);
onDragStart(file.name, position, e);
}
};
useEffect(() => {
if (!isDragging) {
setDragPreventOverlay(false);
}
}, [isDragging]);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (showCopyDropdown) {
const target = event.target as Element;
const dropdownElement = target.closest('.copy-prompt-dropdown');
if (!dropdownElement) {
setShowCopyDropdown(false);
}
}
};
if (showCopyDropdown) {
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}
}, [showCopyDropdown]);
const handleViewportToggle = (newViewport: ViewportMode) => {
if (onViewportChange && !useGlobalViewport) {
onViewportChange(file.name, newViewport);
}
};
const handleCopyPrompt = async (e: React.MouseEvent, platform?: string) => {
e.preventDefault();
e.stopPropagation();
let promptText = '';
let platformName = '';
switch (platform) {
case 'cursor':
promptText = `${file.content}\n\n${t('designFrame.prompts.cursor')}`;
platformName = t('designFrame.platforms.cursor');
break;
case 'windsurf':
promptText = `${file.content}\n\n${t('designFrame.prompts.windsurf')}`;
platformName = t('designFrame.platforms.windsurf');
break;
case 'lovable':
promptText = `${file.content}\n\n${t('designFrame.prompts.lovable')}`;
platformName = t('designFrame.platforms.lovable');
break;
case 'bolt':
promptText = `${file.content}\n\n${t('designFrame.prompts.bolt')}`;
platformName = t('designFrame.platforms.bolt');
break;
default:
promptText = `${file.content}\n\n${t('designFrame.prompts.default')}`;
platformName = '';
}
try {
await navigator.clipboard.writeText(promptText);
setCopyButtonState({ text: t('designFrame.floatingActions.copiedSuccessWithPlatform', { platform: platformName }), isSuccess: true });
setTimeout(() => {
setCopyButtonState({ text: t('designFrame.floatingActions.copyPromptBtn'), isSuccess: false });
}, 2000);
setShowCopyDropdown(false);
} catch (err) {
const textarea = document.createElement('textarea');
textarea.value = promptText;
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
setCopyButtonState({ text: t('designFrame.floatingActions.copiedSuccessWithPlatform', { platform: platformName }), isSuccess: true });
setTimeout(() => {
setCopyButtonState({ text: t('designFrame.floatingActions.copyPromptBtn'), isSuccess: false });
}, 2000);
setShowCopyDropdown(false);
}
};
const handleCopyDropdownToggle = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
setShowCopyDropdown(!showCopyDropdown);
};
const handleCopyDesignPath = async (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
const designPath = `Design file: ${file.path}`;
try {
await navigator.clipboard.writeText(designPath);
setCopyPathButtonState({ text: t('designFrame.floatingActions.copiedSuccess'), isSuccess: true });
setTimeout(() => {
setCopyPathButtonState({ text: t('designFrame.floatingActions.copyPathBtn'), isSuccess: false });
}, 2000);
} catch (err) {
const textarea = document.createElement('textarea');
textarea.value = designPath;
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
setCopyPathButtonState({ text: t('designFrame.floatingActions.copiedSuccess'), isSuccess: true });
setTimeout(() => {
setCopyPathButtonState({ text: t('designFrame.floatingActions.copyPathBtn'), isSuccess: false });
}, 2000);
}
};
const handleCreateVariations = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
if (onSendToChat) {
onSendToChat(file.name, t('designFrame.prompts.createVariations'));
}
};
const handleIterateWithFeedback = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
if (onSendToChat) {
onSendToChat(file.name, t('designFrame.prompts.iterateWithFeedback'));
}
};
const getViewportIcon = (mode: ViewportMode): React.ReactElement => {
switch (mode) {
case 'mobile': return <MobileIcon />;
case 'tablet': return <TabletIcon />;
case 'desktop': return <DesktopIcon />;
default: return <DesktopIcon />;
}
};
const getViewportLabel = (mode: ViewportMode): string => {
switch (mode) {
case 'mobile': return t('designFrame.viewport.mobile');
case 'tablet': return t('designFrame.viewport.tablet');
case 'desktop': return t('designFrame.viewport.desktop');
default: return t('designFrame.viewport.desktop');
}
};
const renderContent = () => {
switch (renderMode) {
case 'iframe':
if (file.fileType === 'svg') {
const svgHtml = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob: https: http:; img-src 'self' data: blob: https: http: *; font-src 'self' data: https: http: *; style-src 'self' 'unsafe-inline' https: http: *; script-src 'self' 'unsafe-inline' 'unsafe-eval' https: http: *; connect-src 'self' https: http: *;">
${viewportDimensions ? `<meta name="viewport" content="width=${viewportDimensions.width}, height=${viewportDimensions.height}, initial-scale=1.0">` : ''}
<style>
body {
margin: 0;
padding: 20px;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
background: white;
box-sizing: border-box;
}
svg {
max-width: 100%;
max-height: 100%;
height: auto;
width: auto;
}
img {
max-width: 100%;
height: auto;
}
</style>
</head>
<body>
${file.content}
<script>
document.addEventListener('DOMContentLoaded', function() {
const images = document.querySelectorAll('img');
images.forEach(function(img) {
img.loading = 'eager';
if (!img.complete || img.naturalWidth === 0) {
const originalSrc = img.src;
img.src = '';
img.src = originalSrc;
}
});
});
</script>
</body>
</html>
`;
return (
<iframe
srcDoc={svgHtml}
title={`${file.name} - SVG`}
style={{
width: viewportDimensions ? `${viewportDimensions.width}px` : '100%',
height: viewportDimensions ? `${viewportDimensions.height}px` : '100%',
border: 'none',
background: 'white',
borderRadius: '0 0 6px 6px',
pointerEvents: (isSelected && !dragPreventOverlay && !isDragging) ? 'auto' : 'none'
}}
referrerPolicy="no-referrer"
loading="lazy"
onLoad={() => {
setIsLoading(false);
setHasError(false);
}}
onError={() => {
setIsLoading(false);
setHasError(true);
}}
/>
);
}
let modifiedContent = file.content || '';
const iframeCSP = `<meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob: https: http:; img-src 'self' data: blob: https: http: *; style-src 'self' 'unsafe-inline' data: https: http: *; script-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob: https: http: *; connect-src 'self' https: http: *; frame-src 'self' data: blob: https: http: *;">`;
const serviceWorkerScript = `
<script${nonce ? ` nonce="${nonce}"` : ''}>
if ('serviceWorker' in navigator) {
const swCode = \`
self.addEventListener('fetch', event => {
const url = event.request.url;
if (url.startsWith('http') && (url.includes('placehold.co') || url.includes('media.giphy.com') || url.match(/\\.(jpg|jpeg|png|gif|svg|webp)$/i))) {
event.respondWith(
fetch(event.request, {
mode: 'cors',
credentials: 'omit'
}).catch(() => {
const canvas = new OffscreenCanvas(200, 120);
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#cccccc';
ctx.fillRect(0, 0, 200, 120);
ctx.fillStyle = '#000000';
ctx.font = '16px Arial';
ctx.textAlign = 'center';
ctx.fillText('IMAGE', 100, 60);
return canvas.convertToBlob().then(blob =>
new Response(blob, {
headers: { 'Content-Type': 'image/png' }
})
);
})
);
}
});
\`;
const blob = new Blob([swCode], { type: 'application/javascript' });
const swUrl = URL.createObjectURL(blob);
navigator.serviceWorker.register(swUrl).then(registration => {
if (registration.active) {
processImages();
} else {
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'activated') {
processImages();
}
});
});
}
}).catch(error => {
processImages();
});
} else {
processImages();
}
function processImages() {
const images = document.querySelectorAll('img[src]');
images.forEach(img => {
if (img.src.startsWith('http')) {
const originalSrc = img.src;
img.src = '';
setTimeout(() => {
img.src = originalSrc;
}, 10);
}
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
setTimeout(processImages, 100);
});
} else {
setTimeout(processImages, 100);
}
</script>`;
if (viewportDimensions) {
const viewportMeta = `<meta name="viewport" content="width=${viewportDimensions.width}, height=${viewportDimensions.height}, initial-scale=1.0">`;
if (modifiedContent.includes('<head>')) {
modifiedContent = modifiedContent.replace('<head>', `<head>\n${iframeCSP}\n${viewportMeta}`);
if (modifiedContent.includes('</body>')) {
modifiedContent = modifiedContent.replace('</body>', `${serviceWorkerScript}\n</body>`);
} else {
modifiedContent += serviceWorkerScript;
}
} else if (modifiedContent.includes('<html>')) {
modifiedContent = modifiedContent.replace('<html>', `<html><head>\n${iframeCSP}\n${viewportMeta}\n</head>`);
if (modifiedContent.includes('</body>')) {
modifiedContent = modifiedContent.replace('</body>', `${serviceWorkerScript}\n</body>`);
} else {
modifiedContent += serviceWorkerScript;
}
} else {
modifiedContent = `<head>\n${iframeCSP}\n${viewportMeta}\n</head>\n${modifiedContent}${serviceWorkerScript}`;
}
} else {
if (modifiedContent.includes('<head>')) {
modifiedContent = modifiedContent.replace('<head>', `<head>\n${iframeCSP}`);
if (modifiedContent.includes('</body>')) {
modifiedContent = modifiedContent.replace('</body>', `${serviceWorkerScript}\n</body>`);
} else {
modifiedContent += serviceWorkerScript;
}
} else if (modifiedContent.includes('<html>')) {
modifiedContent = modifiedContent.replace('<html>', `<html><head>\n${iframeCSP}\n</head>`);
if (modifiedContent.includes('</body>')) {
modifiedContent = modifiedContent.replace('</body>', `${serviceWorkerScript}\n</body>`);
} else {
modifiedContent += serviceWorkerScript;
}
} else {
modifiedContent = `<head>\n${iframeCSP}\n</head>\n${modifiedContent}${serviceWorkerScript}`;
}
}
return (
<iframe
srcDoc={modifiedContent}
title={`${file.name} - ${getViewportLabel(viewport)}`}
style={{
width: viewportDimensions ? `${viewportDimensions.width}px` : '100%',
height: viewportDimensions ? `${viewportDimensions.height}px` : '100%',
border: 'none',
background: 'white',
borderRadius: '0 0 6px 6px',
pointerEvents: (isSelected && !dragPreventOverlay && !isDragging) ? 'auto' : 'none'
}}
referrerPolicy="no-referrer"
loading="lazy"
onLoad={() => {
setIsLoading(false);
setHasError(false);
}}
onError={() => {
setIsLoading(false);
setHasError(true);
}}
/>
);
case 'html':
if (file.fileType === 'svg') {
return (
<div
className="frame-html-content"
title="⚠️ Direct SVG rendering - potential security risk"
dangerouslySetInnerHTML={{ __html: file.content || '' }}
/>
);
}
return (
<div
className="frame-html-content"
dangerouslySetInnerHTML={{ __html: file.content || '' }}
title="⚠️ Direct HTML rendering - potential security risk"
/>
);
case 'placeholder':
default:
const placeholderIcon = file.fileType === 'svg' ? '🎨' : '🌐';
const placeholderHint = file.fileType === 'svg' ? t('designFrame.placeholder.svg') : t('designFrame.placeholder.html');
return (
<div className="frame-placeholder">
<div className="placeholder-icon">{placeholderIcon}</div>
<p className="placeholder-name">{file.name}</p>
<div className="placeholder-meta">
<span>{file.size ? (file.size / 1024).toFixed(1) : 0} KB</span>
<span>{file.modified.toLocaleDateString()}</span>
<span className="file-type">{file.fileType?.toUpperCase() || 'FILE'}</span>
</div>
{renderMode === 'placeholder' && (
<small className="placeholder-hint">{placeholderHint} - {t('designFrame.placeholder.hint')}</small>
)}
</div>
);
}
};
return (
<div
className={`design-frame ${isSelected ? 'selected' : ''} ${isDragging ? 'dragging' : ''}`}
style={{
position: 'absolute',
left: `${position.x}px`,
top: `${position.y}px`,
width: `${dimensions.width}px`,
height: `${dimensions.height}px`,
cursor: isDragging ? 'grabbing' : 'grab',
zIndex: isDragging ? 1000 : (isSelected ? 10 : 1),
opacity: isDragging ? 0.8 : 1
}}
data-frame-name={file.name}
onClick={handleClick}
title={`${file.name} (${file.size ? (file.size / 1024).toFixed(1) : 0} KB)`}
onMouseDown={handleMouseDown}
>
<div className="frame-header">
<span className="frame-title">{file.name}</span>
{onViewportChange && !useGlobalViewport && (
<div className="frame-viewport-controls">
<button
className={`frame-viewport-btn ${viewport === 'mobile' ? 'active' : ''}`}
onClick={() => handleViewportToggle('mobile')}
title={t('designFrame.viewport.mobile')}
>
<MobileIcon />
</button>
<button
className={`frame-viewport-btn ${viewport === 'tablet' ? 'active' : ''}`}
onClick={() => handleViewportToggle('tablet')}
title={t('designFrame.viewport.tablet')}
>
<TabletIcon />
</button>
<button
className={`frame-viewport-btn ${viewport === 'desktop' ? 'active' : ''}`}
onClick={() => handleViewportToggle('desktop')}
title={t('designFrame.viewport.desktop')}
>
<DesktopIcon />
</button>
</div>
)}
{useGlobalViewport && (
<div className="frame-viewport-indicator">
<span className="global-indicator"><GlobeIcon /></span>
<span className="viewport-icon">{getViewportIcon(viewport)}</span>
</div>
)}
{showMetadata && (
<div className="frame-meta">
{isLoading && <span className="frame-status loading"></span>}
{hasError && <span className="frame-status error"></span>}
{!isLoading && !hasError && renderMode === 'iframe' && (
<span className="frame-status loaded"></span>
)}
</div>
)}
</div>
<div className="frame-content">
{renderContent()}
{(dragPreventOverlay || isDragging) && isSelected && renderMode === 'iframe' && (
<div className="frame-drag-overlay">
{dragPreventOverlay && !isDragging && (
<div className="drag-ready-hint">
<span></span>
<p>{t('designFrame.drag')}</p>
</div>
)}
</div>
)}
{isLoading && renderMode === 'iframe' && (
<div className="frame-loading-overlay">
<div className="frame-loading-spinner">
<div className="spinner-small"></div>
<span>{t('designFrame.loading')}</span>
</div>
</div>
)}
{hasError && (
<div className="frame-error-overlay">
<div className="frame-error-content">
<span></span>
<p>{t('designFrame.error')}</p>
<small>{file.name}</small>
</div>
</div>
)}
</div>
{isSelected && !isDragging && (
<>
{/* 选中时的工具栏 */}
<div
className="frame-toolbar"
>
<button className="toolbar-btn" title={t('designFrame.toolbar.bringToFront')}>
<svg className="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="18 15 12 9 6 15"></polyline>
</svg>
</button>
<button className="toolbar-btn" title={t('designFrame.toolbar.layout')}>
<svg className="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<line x1="4" y1="8" x2="20" y2="8"></line>
<line x1="4" y1="16" x2="20" y2="16"></line>
<line x1="4" y1="12" x2="20" y2="12"></line>
</svg>
</button>
<button className="toolbar-btn" title={t('designFrame.toolbar.spacing')}>
<svg className="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<line x1="12" y1="8" x2="12" y2="16"></line>
<line x1="8" y1="12" x2="16" y2="12"></line>
</svg>
</button>
<button className="toolbar-btn" title={t('designFrame.toolbar.style')}>
<svg className="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M12 20h9"></path>
<path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"></path>
</svg>
</button>
<button className="toolbar-btn" title={t('designFrame.toolbar.delete')}>
<svg className="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="3 6 5 6 21 6"></polyline>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
<line x1="10" y1="11" x2="10" y2="17"></line>
<line x1="14" y1="11" x2="14" y2="17"></line>
</svg>
</button>
<button className="toolbar-btn" title={t('designFrame.toolbar.more')}>
<svg className="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="1"></circle>
<circle cx="19" cy="12" r="1"></circle>
<circle cx="5" cy="12" r="1"></circle>
</svg>
</button>
</div>
{/* 浮动操作按钮 */}
<div
className="floating-action-buttons"
onClick={(e) => e.stopPropagation()}
onMouseDown={(e) => e.stopPropagation()}
>
<button
className="floating-action-btn"
onClick={handleCreateVariations}
title={t('designFrame.floatingActions.createVariations')}
>
<svg className="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="6" cy="6" r="3"/>
<circle cx="18" cy="18" r="3"/>
<circle cx="18" cy="6" r="3"/>
<path d="M18 9v6"/>
<path d="M9 6h6"/>
</svg>
<span className="btn-text">{t('designFrame.floatingActions.createVariationsBtn')}</span>
</button>
<button
className="floating-action-btn"
onClick={handleIterateWithFeedback}
title={t('designFrame.floatingActions.iterateWithFeedback')}
>
<svg className="btn-icon" viewBox="0 0 24 24" fill="none">
<path d="M17.65 6.35C16.2 4.9 14.21 4 12 4C7.58 4 4 7.58 4 12C4 16.42 7.58 20 12 20C15.73 20 18.84 17.45 19.73 14H17.65C16.83 16.33 14.61 18 12 18C8.69 18 6 15.31 6 12C6 8.69 8.69 6 12 6C13.66 6 15.14 6.69 16.22 7.78L13 11H20V4L17.65 6.35Z" fill="currentColor"/>
</svg>
<span className="btn-text">{t('designFrame.floatingActions.iterateWithFeedbackBtn')}</span>
</button>
<div className="copy-prompt-dropdown">
<button
className={`floating-action-btn copy-prompt-main-btn ${copyButtonState.isSuccess ? 'success' : ''}`}
onClick={handleCopyDropdownToggle}
title={t('designFrame.floatingActions.copyPrompt')}
>
<svg className="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.582a.5.5 0 0 1 0 .962L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z"/>
<path d="M20 3v4"/>
<path d="M22 5h-4"/>
<path d="M4 17v2"/>
<path d="M5 18H3"/>
</svg>
<span className="btn-text">{copyButtonState.text}</span>
<svg className="dropdown-arrow" viewBox="0 0 24 24" fill="none">
<path d="M7 10L12 15L17 10H7Z" fill="currentColor"/>
</svg>
</button>
{showCopyDropdown && (
<div className="copy-dropdown-menu">
<button
className="copy-dropdown-item"
onClick={(e) => handleCopyPrompt(e, 'cursor')}
>
<span>{t('designFrame.platforms.cursor')}</span>
</button>
<button
className="copy-dropdown-item"
onClick={(e) => handleCopyPrompt(e, 'windsurf')}
>
<span>{t('designFrame.platforms.windsurf')}</span>
</button>
<button
className="copy-dropdown-item"
onClick={(e) => handleCopyPrompt(e, 'lovable')}
>
<span>{t('designFrame.platforms.lovable')}</span>
</button>
<button
className="copy-dropdown-item"
onClick={(e) => handleCopyPrompt(e, 'bolt')}
>
<span>{t('designFrame.platforms.bolt')}</span>
</button>
</div>
)}
</div>
<button
className={`floating-action-btn copy-path-btn ${copyPathButtonState.isSuccess ? 'success' : ''}`}
onClick={handleCopyDesignPath}
title={t('designFrame.floatingActions.copyPath')}
>
<svg className="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect width="14" height="14" x="8" y="8" rx="2" ry="2"/>
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/>
</svg>
<span className="btn-text">{copyPathButtonState.text}</span>
</button>
</div>
</>
)}
</div>
);
};
export default DesignFrame;

View File

@ -1,170 +0,0 @@
export const ZoomInIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
<line x1="11" y1="8" x2="11" y2="14"></line>
<line x1="8" y1="11" x2="14" y2="11"></line>
</svg>
);
export const ZoomOutIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
<line x1="8" y1="11" x2="14" y2="11"></line>
</svg>
);
export const HomeIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path>
<polyline points="9 22 9 12 15 12 15 22"></polyline>
</svg>
);
export const ScaleIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect>
<line x1="8" y1="21" x2="16" y2="21"></line>
<line x1="12" y1="17" x2="12" y2="21"></line>
</svg>
);
export const RefreshIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2"></path>
</svg>
);
export const GlobeIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path>
<path d="M2 12h20"></path>
</svg>
);
export const MobileIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="5" y="2" width="14" height="20" rx="2" ry="2"></rect>
<path d="M12 18h.01"></path>
</svg>
);
export const TabletIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="4" y="2" width="16" height="20" rx="2" ry="2"></rect>
<line x1="12" y1="18" x2="12.01" y2="18"></line>
</svg>
);
export const DesktopIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect>
<line x1="8" y1="21" x2="16" y2="21"></line>
<line x1="12" y1="17" x2="12" y2="21"></line>
</svg>
);
export const TreeIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M12 2v20M5 12c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"></path>
</svg>
);
export const LinkIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
);
export const ArrowIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="12 5 12 19"></polyline>
<polyline points="5 12 19 12"></polyline>
</svg>
);
export const MenuIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<line x1="4" y1="8" x2="20" y2="8"></line>
<line x1="4" y1="16" x2="20" y2="16"></line>
<line x1="4" y1="12" x2="20" y2="12"></line>
</svg>
);
export const CopyIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
);
export const PaletteIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="3"></circle>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
</svg>
);
export const LockIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
</svg>
);
export const PushPinIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M12 17.8a5 5 0 0 0 7.54-.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M18 11a3 3 0 1 1-6 0 3 3 0 0 1 6 0z"></path>
<line x1="12" y1="12" x2="12" y2="21"></line>
<line x1="16" y1="8" x2="8" y2="16"></line>
<line x1="8" y1="8" x2="16" y2="16"></line>
</svg>
);
export const PlusIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
);
export const ChatIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
</svg>
);
export const PaperclipIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"></path>
</svg>
);
export const NoteIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
<polyline points="14 2 14 8 20 8"></polyline>
<line x1="16" y1="13" x2="8" y2="13"></line>
<line x1="16" y1="17" x2="8" y2="17"></line>
<polyline points="10 9 9 9 8 9"></polyline>
</svg>
);
export const NewChatIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
<line x1="12" y1="8" x2="12" y2="16"></line>
<line x1="8" y1="12" x2="16" y2="12"></line>
</svg>
);
export const HistoryIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<polyline points="12 6 12 12 16 14"></polyline>
</svg>
);

View File

@ -1,194 +0,0 @@
.language-modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--color-overlay);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
margin: 0;
padding: 0;
box-sizing: border-box;
overflow: hidden;
}
.language-modal {
background-color: var(--color-white);
border-radius: var(--radius-card);
border: 1px solid var(--color-border-subtle);
width: 360px;
max-width: 90%;
max-height: 90vh;
display: flex;
flex-direction: column;
position: relative;
z-index: 1001;
box-shadow: 0 12px 28px 0 rgba(0,0,0,0.2), 0 2px 4px 0 rgba(0,0,0,0.1);
margin: 0;
box-sizing: border-box;
}
.language-modal-header {
padding: var(--space-9);
border-bottom: 1px solid var(--color-border-subtle);
display: flex;
align-items: center;
justify-content: space-between;
}
.language-modal-header h3 {
margin: 0;
font-size: var(--font-size-heading-3);
font-weight: var(--font-weight-bold);
color: var(--color-text-primary);
letter-spacing: var(--letter-spacing-normal);
font-family: var(--font-family-primary);
font-feature-settings: "ss01", "ss02";
}
.language-modal-close {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: var(--color-text-primary);
padding: 0;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-circle);
transition: background-color 0.15s ease;
}
.language-modal-close:hover {
background-color: var(--color-soft-gray);
}
.language-modal-close:focus-visible {
outline: 3px ring var(--color-meta-blue);
outline: auto 2px var(--color-meta-blue);
outline-offset: 2px;
}
.language-modal-body {
padding: var(--space-9);
}
.language-modal-body .form-group {
display: flex;
flex-direction: column;
gap: var(--space-3);
}
.language-modal-body .form-group label {
font-family: var(--font-family-primary);
font-size: var(--font-size-caption-bold);
font-weight: var(--font-weight-bold);
color: var(--color-text-primary);
letter-spacing: var(--letter-spacing-normal);
}
.language-select {
padding: var(--space-5) var(--space-6);
border: 1px solid var(--color-border-medium);
border-radius: var(--radius-small);
font-size: var(--font-size-body);
font-family: var(--font-family-primary);
outline: none;
transition: border-color 200ms ease, box-shadow 200ms ease;
background-color: var(--color-white);
color: var(--color-text-primary);
letter-spacing: var(--letter-spacing-normal);
cursor: pointer;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23333' d='M6 8L1 3h10z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 12px center;
padding-right: 32px;
}
.language-select:focus {
border-color: hsl(214, 89%, 52%);
box-shadow: 0 0 0 3px rgba(0, 100, 224, 0.1);
}
.language-select:focus-visible {
outline: 3px ring var(--color-meta-blue);
outline: auto 2px var(--color-meta-blue);
outline-offset: 2px;
}
.language-modal-footer {
padding: var(--space-9);
border-top: 1px solid var(--color-border-subtle);
display: flex;
justify-content: flex-end;
gap: var(--space-5);
}
.language-modal-cancel {
background-color: var(--color-soft-gray);
color: var(--color-text-primary);
border: 1px solid var(--color-border-subtle);
padding: var(--space-4) var(--space-9);
border-radius: var(--radius-pill);
font-family: var(--font-family-primary);
font-size: var(--font-size-button);
font-weight: var(--font-weight-regular);
letter-spacing: var(--letter-spacing-tighter);
cursor: pointer;
transition: background 200ms ease, transform 150ms ease;
}
.language-modal-cancel:hover {
background-color: var(--color-button-hover);
color: var(--color-white);
transform: scale(1.05);
}
.language-modal-cancel:focus-visible {
outline: 3px ring var(--color-meta-blue);
outline: auto 2px var(--color-meta-blue);
outline-offset: 2px;
}
.language-modal-cancel:active {
opacity: 0.9;
transform: scale(0.95);
}
.language-modal-submit {
background-color: var(--color-meta-blue);
color: var(--color-white);
border: none;
padding: var(--space-4) var(--space-9);
border-radius: var(--radius-pill);
font-family: var(--font-family-primary);
font-size: var(--font-size-button);
font-weight: var(--font-weight-regular);
letter-spacing: var(--letter-spacing-tighter);
cursor: pointer;
transition: background 200ms ease, transform 150ms ease;
}
.language-modal-submit:hover {
background-color: var(--color-meta-blue-hover);
transform: scale(1.05);
}
.language-modal-submit:focus-visible {
outline: 3px ring var(--color-meta-blue);
outline: auto 2px var(--color-meta-blue);
outline-offset: 2px;
}
.language-modal-submit:active {
background-color: var(--color-meta-blue-pressed);
transform: scale(0.9);
opacity: 0.5;
}

View File

@ -1,78 +0,0 @@
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { changeLanguage } from '../i18n'
import './LanguageSwitchModal.css'
interface LanguageSwitchModalProps {
showModal: boolean
onClose: () => void
}
const LanguageSwitchModal: React.FC<LanguageSwitchModalProps> = ({ showModal, onClose }) => {
const { t, i18n } = useTranslation()
const [selectedLanguage, setSelectedLanguage] = useState(i18n.language)
const handleSubmit = async () => {
await changeLanguage(selectedLanguage)
// TODO: Submit language preference to backend API
// await api.updateUserLanguagePreference(selectedLanguage)
onClose()
}
const handleCancel = () => {
setSelectedLanguage(i18n.language)
onClose()
}
if (!showModal) {
return null
}
return (
<div className="language-modal-overlay" onClick={handleCancel}>
<div className="language-modal" onClick={(e) => e.stopPropagation()}>
<div className="language-modal-header">
<h3>{t('languageSwitch.title')}</h3>
<button
className="language-modal-close"
onClick={handleCancel}
>
×
</button>
</div>
<div className="language-modal-body">
<div className="form-group">
<label htmlFor="language-select">{t('languageSwitch.selectLanguage')}</label>
<select
id="language-select"
className="language-select"
value={selectedLanguage}
onChange={(e) => setSelectedLanguage(e.target.value)}
>
<option value="zh">{t('languages.zh')}</option>
<option value="en">{t('languages.en')}</option>
</select>
</div>
</div>
<div className="language-modal-footer">
<button
className="language-modal-cancel"
onClick={handleCancel}
>
{t('languageSwitch.cancel')}
</button>
<button
className="language-modal-submit"
onClick={handleSubmit}
>
{t('languageSwitch.submit')}
</button>
</div>
</div>
</div>
)
}
export default LanguageSwitchModal

View File

@ -1,475 +0,0 @@
.side-bar {
position: absolute;
top: 100px;
left: 20px;
width: 48px;
background-color: var(--color-white);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-card);
display: flex;
flex-direction: column;
z-index: 100;
transition: width 0.3s ease;
box-shadow: 0 2px 4px 0 rgba(0,0,0,0.1);
}
.menu-items {
padding: var(--space-9) 0;
display: flex;
flex-direction: column;
gap: var(--space-3);
}
.menu-item {
display: flex;
align-items: center;
padding: var(--space-4) 0;
cursor: pointer;
transition: all 0.15s ease;
color: var(--color-text-primary);
position: relative;
}
.menu-item:hover {
background-color: var(--color-soft-gray);
}
.menu-item:focus-visible {
outline: 3px ring var(--color-meta-blue);
outline: auto 2px var(--color-meta-blue);
outline-offset: 2px;
}
.menu-item.active {
background-color: var(--color-meta-blue);
color: var(--color-white);
}
.menu-icon {
font-size: 20px;
margin: 0 var(--space-5);
display: flex;
align-items: center;
justify-content: center;
}
.menu-label {
font-family: var(--font-family-primary);
font-size: var(--font-size-caption);
font-weight: var(--font-weight-medium);
letter-spacing: var(--letter-spacing-tighter);
opacity: 0;
visibility: hidden;
position: absolute;
left: 48px;
top: 50%;
transform: translateY(-50%);
background-color: var(--color-soft-gray);
color: var(--color-text-primary);
padding: var(--space-4) var(--space-7);
border-radius: var(--radius-card);
border: 1px solid var(--color-border-subtle);
white-space: nowrap;
transition: all 0.3s ease;
z-index: 101;
box-shadow: 0 2px 4px 0 rgba(0,0,0,0.1);
}
.menu-item:hover .menu-label {
opacity: 1;
visibility: visible;
}
.note-modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--color-overlay);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.note-modal {
background-color: var(--color-white);
border-radius: var(--radius-card);
border: 1px solid var(--color-border-subtle);
width: 400px;
max-width: 90%;
max-height: 80%;
display: flex;
flex-direction: column;
position: relative;
z-index: 1001;
box-shadow: 0 12px 28px 0 rgba(0,0,0,0.2), 0 2px 4px 0 rgba(0,0,0,0.1);
}
.note-modal-position {
position: absolute;
}
.note-modal-header {
padding: var(--space-9);
border-bottom: 1px solid var(--color-border-subtle);
display: flex;
align-items: center;
justify-content: space-between;
}
.note-modal-header h3 {
margin: 0;
font-size: var(--font-size-heading-3);
font-weight: var(--font-weight-bold);
color: var(--color-text-primary);
letter-spacing: var(--letter-spacing-normal);
font-family: var(--font-family-primary);
font-feature-settings: "ss01", "ss02";
}
.note-modal-close {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: var(--color-text-primary);
padding: 0;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-circle);
transition: background-color 0.15s ease;
}
.note-modal-close:hover {
background-color: var(--color-soft-gray);
}
.note-modal-close:focus-visible {
outline: 3px ring var(--color-meta-blue);
outline: auto 2px var(--color-meta-blue);
outline-offset: 2px;
}
.note-modal-body {
padding: var(--space-9);
flex: 1;
position: relative;
}
.note-modal-textarea {
width: 100%;
height: 100%;
min-height: 120px;
padding: var(--space-5);
border: 1px solid var(--color-border-medium);
border-radius: var(--radius-small);
font-size: var(--font-size-body);
font-family: var(--font-family-primary);
resize: none;
outline: none;
transition: border-color 200ms ease, box-shadow 200ms ease;
background-color: var(--color-white);
color: var(--color-text-primary);
letter-spacing: var(--letter-spacing-normal);
}
.note-modal-textarea:focus {
border-color: hsl(214, 89%, 52%);
box-shadow: 0 0 0 3px rgba(0, 100, 224, 0.1);
}
.note-modal-textarea:focus-visible {
outline: 3px ring var(--color-meta-blue);
outline: auto 2px var(--color-meta-blue);
outline-offset: 2px;
}
.note-modal-footer {
padding: var(--space-9);
border-top: 1px solid var(--color-border-subtle);
display: flex;
justify-content: flex-end;
position: relative;
}
.note-modal-submit {
font-family: var(--font-family-primary);
font-size: var(--font-size-button);
font-weight: var(--font-weight-regular);
letter-spacing: var(--letter-spacing-tighter);
background-color: var(--color-meta-blue);
color: var(--color-white);
border: none;
padding: var(--space-4) var(--space-9);
border-radius: var(--radius-pill);
cursor: pointer;
transition: background 200ms ease, transform 150ms ease;
position: absolute;
bottom: var(--space-5);
right: var(--space-5);
}
.note-modal-submit:hover {
background-color: var(--color-meta-blue-hover);
transform: scale(1.05);
}
.note-modal-submit:focus-visible {
outline: 3px ring var(--color-meta-blue);
outline: auto 2px var(--color-meta-blue);
outline-offset: 2px;
}
.note-modal-submit:active {
background-color: var(--color-meta-blue-pressed);
transform: scale(0.9);
opacity: 0.5;
}
.no-selection-message {
position: absolute;
left: 100%;
top: 50%;
transform: translateY(-50%);
background-color: var(--color-soft-gray);
color: var(--color-text-primary);
padding: var(--space-5) var(--space-7);
border-radius: var(--radius-card);
border: 1px solid var(--color-border-subtle);
font-family: var(--font-family-primary);
font-size: var(--font-size-caption);
letter-spacing: var(--letter-spacing-tighter);
white-space: nowrap;
z-index: 101;
animation: fadeInOut 3s ease-in-out;
box-shadow: 0 2px 4px 0 rgba(0,0,0,0.1);
}
@keyframes fadeInOut {
0% {
opacity: 0;
transform: translateY(-50%) translateX(-10px);
}
10% {
opacity: 1;
transform: translateY(-50%) translateX(0);
}
90% {
opacity: 1;
transform: translateY(-50%) translateX(0);
}
100% {
opacity: 0;
transform: translateY(-50%) translateX(0);
}
}
.resource-modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--color-overlay);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.resource-modal {
background-color: var(--color-white);
border-radius: var(--radius-card);
border: 1px solid var(--color-border-subtle);
width: 600px;
max-width: 90%;
max-height: 80%;
display: flex;
flex-direction: column;
position: relative;
z-index: 1001;
box-shadow: 0 12px 28px 0 rgba(0,0,0,0.2), 0 2px 4px 0 rgba(0,0,0,0.1);
}
.resource-modal-header {
padding: var(--space-9);
border-bottom: 1px solid var(--color-border-subtle);
display: flex;
align-items: center;
justify-content: space-between;
}
.resource-modal-header h3 {
margin: 0;
font-size: var(--font-size-heading-3);
font-weight: var(--font-weight-bold);
color: var(--color-text-primary);
letter-spacing: var(--letter-spacing-normal);
font-family: var(--font-family-primary);
font-feature-settings: "ss01", "ss02";
}
.resource-modal-close {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: var(--color-text-primary);
padding: 0;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-circle);
transition: background-color 0.15s ease;
}
.resource-modal-close:hover {
background-color: var(--color-soft-gray);
}
.resource-modal-close:focus-visible {
outline: 3px ring var(--color-meta-blue);
outline: auto 2px var(--color-meta-blue);
outline-offset: 2px;
}
.resource-modal-body {
padding: var(--space-9);
flex: 1;
overflow-y: auto;
}
.resource-row {
display: flex;
gap: var(--space-9);
margin-bottom: var(--space-9);
}
.resource-section {
flex: 1;
background-color: var(--color-soft-gray);
border-radius: var(--radius-card);
padding: var(--space-7);
border: 1px solid var(--color-border-subtle);
}
.resource-section.full-width {
flex: 1;
width: 100%;
}
.resource-section h4 {
margin: 0 0 var(--space-5) 0;
font-family: var(--font-family-primary);
font-size: var(--font-size-caption-bold);
font-weight: var(--font-weight-bold);
color: var(--color-text-primary);
letter-spacing: var(--letter-spacing-normal);
}
.resource-display {
display: flex;
flex-direction: column;
gap: var(--space-5);
}
.logo-display {
display: flex;
gap: var(--space-5);
flex-wrap: wrap;
}
.logo-placeholder {
width: 60px;
height: 60px;
background-color: var(--color-white);
border-radius: var(--radius-small);
display: flex;
align-items: center;
justify-content: center;
font-family: var(--font-family-primary);
font-size: var(--font-size-caption);
color: var(--color-text-secondary);
border: 1px solid var(--color-border-subtle);
letter-spacing: var(--letter-spacing-tighter);
box-shadow: 0 2px 4px 0 rgba(0,0,0,0.1);
}
.font-display {
display: flex;
flex-direction: column;
gap: var(--space-3);
}
.font-item {
padding: var(--space-4) var(--space-5);
background-color: var(--color-white);
border-radius: var(--radius-small);
font-family: var(--font-family-primary);
font-size: var(--font-size-caption);
color: var(--color-text-primary);
border: 1px solid var(--color-border-subtle);
letter-spacing: var(--letter-spacing-tighter);
box-shadow: 0 2px 4px 0 rgba(0,0,0,0.1);
}
.image-display {
display: flex;
gap: var(--space-5);
flex-wrap: wrap;
}
.image-placeholder {
width: 80px;
height: 80px;
background-color: var(--color-white);
border-radius: var(--radius-small);
display: flex;
align-items: center;
justify-content: center;
font-family: var(--font-family-primary);
font-size: var(--font-size-caption);
color: var(--color-text-secondary);
border: 1px solid var(--color-border-subtle);
letter-spacing: var(--letter-spacing-tighter);
box-shadow: 0 2px 4px 0 rgba(0,0,0,0.1);
}
.upload-btn {
font-family: var(--font-family-primary);
font-size: var(--font-size-button);
font-weight: var(--font-weight-regular);
letter-spacing: var(--letter-spacing-tighter);
background-color: var(--color-meta-blue);
color: var(--color-white);
border: none;
padding: var(--space-4) var(--space-9);
border-radius: var(--radius-pill);
cursor: pointer;
transition: background 200ms ease, transform 150ms ease;
align-self: flex-start;
}
.upload-btn:hover {
background-color: var(--color-meta-blue-hover);
transform: scale(1.05);
}
.upload-btn:focus-visible {
outline: 3px ring var(--color-meta-blue);
outline: auto 2px var(--color-meta-blue);
outline-offset: 2px;
}
.upload-btn:active {
background-color: var(--color-meta-blue-pressed);
transform: scale(0.9);
opacity: 0.5;
}

View File

@ -1,225 +0,0 @@
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import './SideBar.css'
import { ArrowIcon, NoteIcon, CopyIcon, PaletteIcon, PushPinIcon, PlusIcon } from './Icons'
import ContextModal from './ContextModal'
interface SideBarProps {
selectedFrames: string[]
onSelectNote: (frameName: string, note: string) => void
onCreateContext: (title: string, description: string) => void
}
const SideBar: React.FC<SideBarProps> = ({ selectedFrames, onSelectNote, onCreateContext }) => {
const { t } = useTranslation()
const [activeItem, setActiveItem] = useState('select')
const [showNoteModal, setShowNoteModal] = useState(false)
const [noteContent, setNoteContent] = useState('')
const [showNoSelectionMessage, setShowNoSelectionMessage] = useState(false)
const [noteModalPosition, setNoteModalPosition] = useState({ x: 0, y: 0 })
const [showResourceModal, setShowResourceModal] = useState(false)
const [showContextModal, setShowContextModal] = useState(false)
const menuItems = [
{ id: 'select', icon: <ArrowIcon />, label: t('sideBar.menu.select') },
{ id: 'note', icon: <NoteIcon />, label: t('sideBar.menu.note') },
{ id: 'copy', icon: <CopyIcon />, label: t('sideBar.menu.copy') },
{ id: 'customize', icon: <PaletteIcon />, label: t('sideBar.menu.customize') },
// { id: 'fix', icon: <PushPinIcon />, label: t('sideBar.menu.fix') },
// { id: 'add', icon: <PlusIcon />, label: t('sideBar.menu.add') },
]
return (
<div className="side-bar">
<div className="menu-items">
{menuItems.map((item) => (
<div
key={item.id}
className={`menu-item ${activeItem === item.id ? 'active' : ''}`}
onClick={() => {
setActiveItem(item.id)
if (item.id === 'note') {
if (selectedFrames.length > 0) {
setShowNoSelectionMessage(false)
setShowNoteModal(true)
// 计算弹窗位置跟随选中的DesignFrame
const selectedFrame = document.querySelector(`[data-frame-name="${selectedFrames[0]}"]`)
if (selectedFrame) {
const rect = selectedFrame.getBoundingClientRect()
setNoteModalPosition({
x: rect.left + rect.width / 2 - 200, // 居中
y: rect.top + rect.height / 2 - 150 // 居中
})
}
} else {
setShowNoSelectionMessage(true)
setShowNoteModal(false)
// 3秒后自动隐藏提示
setTimeout(() => {
setShowNoSelectionMessage(false)
}, 3000)
}
} else if (item.id === 'customize') {
setShowResourceModal(true)
} else if (item.id === 'fix') {
setShowContextModal(true)
}
}}
title={item.label}
>
<span className="menu-icon">{item.icon}</span>
<span className="menu-label">{item.label}</span>
</div>
))}
</div>
{/* 备注弹窗 */}
{showNoteModal && (
<div className="note-modal-overlay">
<div
className="note-modal"
style={{
left: `${noteModalPosition.x}px`,
top: `${noteModalPosition.y}px`,
}}
>
<div className="note-modal-header">
<h3>{t('sideBar.noteModal.title')}</h3>
<button
className="note-modal-close"
onClick={() => setShowNoteModal(false)}
>
×
</button>
</div>
<div className="note-modal-body">
<textarea
className="note-modal-textarea"
value={noteContent}
onChange={(e) => setNoteContent(e.target.value)}
placeholder={t('sideBar.noteModal.placeholder')}
rows={6}
></textarea>
</div>
<div className="note-modal-footer">
<button
className="note-modal-submit"
onClick={() => {
if (selectedFrames.length > 0 && onSelectNote) {
onSelectNote(selectedFrames[0], noteContent)
}
setShowNoteModal(false)
setNoteContent('')
}}
>
{t('sideBar.noteModal.submit')}
</button>
</div>
</div>
</div>
)}
{/* 未选择提示 */}
{showNoSelectionMessage && (
<div className="no-selection-message">
{t('sideBar.noSelection')}
</div>
)}
{/* 资源管理弹窗 */}
{showResourceModal && (
<div className="resource-modal-overlay">
<div className="resource-modal">
<div className="resource-modal-header">
<h3>{t('sideBar.resourceModal.title')}</h3>
<button
className="resource-modal-close"
onClick={() => setShowResourceModal(false)}
>
×
</button>
</div>
<div className="resource-modal-body">
{/* 第一行Logo管理和Font管理 */}
<div className="resource-row">
{/* Logo管理区 */}
<div className="resource-section">
<h4>{t('sideBar.resourceModal.logo')}</h4>
<div className="resource-display">
{/* 已有Logo图标显示区 */}
<div className="logo-display">
<div className="logo-placeholder">
<span>Logo</span>
</div>
</div>
{/* Logo文件上传按钮 */}
<button className="upload-btn">
{t('sideBar.resourceModal.uploadLogo')}
</button>
</div>
</div>
{/* Font管理区 */}
<div className="resource-section">
<h4>{t('sideBar.resourceModal.font')}</h4>
<div className="resource-display">
{/* 已有Font文件展示区 */}
<div className="font-display">
<div className="font-item">
<span>Arial</span>
</div>
<div className="font-item">
<span>Times New Roman</span>
</div>
</div>
{/* Font文件上传按钮 */}
<button className="upload-btn">
{t('sideBar.resourceModal.uploadFont')}
</button>
</div>
</div>
</div>
{/* 第二行Image管理 */}
<div className="resource-row">
{/* Image管理区 */}
<div className="resource-section full-width">
<h4>{t('sideBar.resourceModal.image')}</h4>
<div className="resource-display">
{/* 已有Image管理区 */}
<div className="image-display">
<div className="image-placeholder">
<span>Image</span>
</div>
<div className="image-placeholder">
<span>Image</span>
</div>
<div className="image-placeholder">
<span>Image</span>
</div>
</div>
{/* Image上传按钮 */}
<button className="upload-btn">
{t('sideBar.resourceModal.uploadImage')}
</button>
</div>
</div>
</div>
</div>
</div>
</div>
)}
{/* 创建上下文弹窗 */}
<ContextModal
showModal={showContextModal}
onClose={() => setShowContextModal(false)}
onSubmit={onCreateContext}
/>
</div>
)
}
export default SideBar

View File

@ -1,116 +0,0 @@
.top-bar {
position: absolute;
top: 20px;
right: 20px;
height: 56px;
display: flex;
justify-content: space-between;
align-items: center;
z-index: 100;
background-color: var(--color-white);
border: 1px solid var(--color-cta-gray-border);
border-radius: var(--radius-pill);
padding: 0 var(--space-7);
}
.top-bar-left {
display: flex;
align-items: center;
}
.meta-wordmark {
font-family: var(--font-family-primary);
font-size: var(--font-size-heading-3);
font-weight: var(--font-weight-medium);
color: var(--color-text-primary);
letter-spacing: var(--letter-spacing-normal);
line-height: var(--line-height-heading-3);
margin: 0;
font-feature-settings: "ss01", "ss02";
}
.top-bar-center {
display: flex;
align-items: center;
gap: var(--space-9);
}
.nav-link {
font-family: var(--font-family-primary);
font-size: var(--font-size-body-compact);
font-weight: var(--font-weight-medium);
color: var(--color-text-primary);
text-decoration: none;
transition: all 0.15s ease;
padding: var(--space-3) 0;
position: relative;
}
.nav-link:hover {
color: var(--color-link-hover);
text-decoration: underline;
}
.nav-link.active {
box-shadow: 0px -2px 0px 0px inset var(--color-meta-blue);
}
.top-bar-right {
display: flex;
align-items: center;
gap: var(--space-7);
}
.search-icon {
color: var(--color-text-primary);
font-size: 20px;
cursor: pointer;
transition: color 0.15s ease;
}
.search-icon:hover {
color: var(--color-link-hover);
}
.chat-session {
display: flex;
align-items: center;
margin-left: 10px;
}
.chat-btn {
font-family: var(--font-family-primary);
font-size: var(--font-size-button);
font-weight: var(--font-weight-regular);
letter-spacing: var(--letter-spacing-tighter);
padding: var(--space-4) var(--space-7);
border-radius: var(--radius-pill);
transition: background 200ms ease, transform 150ms ease;
background-color: var(--color-soft-gray);
color: var(--color-text-secondary);
border: none;
}
.chat-btn:hover {
background-color: var(--color-button-hover);
color: var(--color-white);
transform: scale(1.05);
}
.chat-btn.active {
background-color: var(--color-meta-blue);
color: var(--color-white);
}
.chat-btn:focus-visible {
outline: 3px ring var(--color-meta-blue);
outline: auto 2px var(--color-meta-blue);
outline-offset: 2px;
}
.action-buttons {
display: flex;
gap: var(--space-5);
align-items: center;
}

View File

@ -1,46 +0,0 @@
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import './TopBar.css'
import { ChatIcon } from './Icons'
import LanguageSwitchModal from './LanguageSwitchModal'
interface TopBarProps {
onToggleChat: () => void
isChatVisible: boolean
}
const TopBar = ({ onToggleChat, isChatVisible }: TopBarProps) => {
const { t } = useTranslation()
const [isLanguageModalVisible, setIsLanguageModalVisible] = useState(false)
return (
<div className="top-bar">
<div className="action-buttons">
<button className="btn btn-primary">{t('topBar.newProject')}</button>
<button className="btn btn-secondary">{t('topBar.save')}</button>
<button className="btn btn-tertiary">{t('topBar.export')}</button>
<button
className="btn btn-tertiary"
onClick={() => setIsLanguageModalVisible(true)}
>
{t('topBar.settings')}
</button>
</div>
<div className='chat-session'>
<button
className={`btn btn-tertiary chat-btn ${isChatVisible ? 'active' : ''}`}
onClick={onToggleChat}
title={isChatVisible ? t('topBar.hideChatPanel') : t('topBar.showChatPanel')}
>
<ChatIcon />
</button>
</div>
<LanguageSwitchModal
showModal={isLanguageModalVisible}
onClose={() => setIsLanguageModalVisible(false)}
/>
</div>
)
}
export default TopBar

View File

@ -0,0 +1,109 @@
import React, { useState } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import Icon from '../ui/Icon';
interface LayoutProps {
children: React.ReactNode;
}
const Layout: React.FC<LayoutProps> = ({ children }) => {
const { t } = useTranslation();
const location = useLocation();
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
const menuItems = [
{ key: 'dashboard', path: '/dashboard', icon: 'dashboard' },
{ key: 'projects', path: '/projects', icon: 'projects' },
{ key: 'settings', path: '/settings', icon: 'settings' },
];
const getMenuItemClass = (path: string) => {
const isActive = location.pathname === path;
return `flex items-center px-4 py-3 rounded-[8px] transition-all duration-200 ${
isActive
? 'bg-meta-blue text-white'
: 'text-dark-charcoal hover:bg-soft-gray'
}`;
};
return (
<div className="min-h-screen bg-soft-gray">
<div className="flex">
<aside
className={`${
isSidebarCollapsed ? 'w-16' : 'w-64'
} bg-white shadow-[0_12px_28px_0_rgba(0,0,0,0.2),0_2px_4px_0_rgba(0,0,0,0.1)] transition-all duration-300 min-h-screen sticky top-0`}
>
<div className="p-4 border-b border-divider">
<div className="flex items-center justify-between">
{!isSidebarCollapsed && (
<h1 className="text-xl font-medium text-dark-charcoal font-optimistic truncate">
{t('layout.systemName')}
</h1>
)}
<button
onClick={() => setIsSidebarCollapsed(!isSidebarCollapsed)}
className="p-2 rounded-[8px] hover:bg-soft-gray transition-colors duration-200"
>
<Icon
name={isSidebarCollapsed ? 'chevronRight' : 'chevronLeft'}
size="md"
className="text-dark-charcoal"
/>
</button>
</div>
</div>
<nav className="p-4 space-y-2">
{menuItems.map((item) => (
<Link
key={item.key}
to={item.path}
className={getMenuItemClass(item.path)}
>
<Icon name={item.icon} size="md" className="mr-3" />
{!isSidebarCollapsed && (
<span className="font-medium font-optimistic">
{t(`layout.${item.key}`)}
</span>
)}
</Link>
))}
</nav>
</aside>
<div className="flex-1 flex flex-col">
<header className="bg-white shadow-[0_2px_4px_0_rgba(0,0,0,0.1)] sticky top-0 z-10">
<div className="flex items-center justify-between px-6 py-4">
<div className="flex-1">
<h2 className="text-lg font-medium text-dark-charcoal font-optimistic">
{t('layout.systemTitle')}
</h2>
</div>
<div className="flex items-center space-x-4">
<button className="relative p-2 rounded-[8px] hover:bg-soft-gray transition-colors duration-200">
<Icon name="bell" size="md" className="text-dark-charcoal" />
<span className="absolute top-1 right-1 w-2 h-2 bg-error-red rounded-full"></span>
</button>
<div className="flex items-center space-x-3">
<div className="w-8 h-8 rounded-full bg-meta-blue flex items-center justify-center text-white font-medium">
U
</div>
</div>
</div>
</div>
</header>
<main className="p-6 flex-1">
{children}
</main>
</div>
</div>
</div>
);
};
export default Layout;

View File

@ -0,0 +1,62 @@
import React from 'react';
interface ButtonProps {
type: 'primary' | 'secondary' | 'danger';
children: React.ReactNode;
onClick: () => void;
loading?: boolean;
disabled?: boolean;
fullWidth?: boolean;
size?: 'sm' | 'md';
className?: string;
}
const Button: React.FC<ButtonProps> = ({
type,
children,
onClick,
loading = false,
disabled = false,
fullWidth = false,
size = 'md',
className = '',
}) => {
const getButtonClasses = () => {
const sizeClasses = size === 'sm' ? 'px-3 py-1.5 text-xs' : 'px-5.5 py-2.5 text-sm';
const baseClasses = `${sizeClasses} font-medium focus:outline-none focus:ring-3 focus:ring-offset-2 transition-all duration-200 font-optimistic tracking-[0.14px]`;
const widthClasses = fullWidth ? 'w-full' : '';
switch (type) {
case 'primary':
return `${baseClasses} ${widthClasses} bg-meta-blue text-white rounded-full hover:bg-meta-blue-hover hover:scale-105 active:bg-meta-blue-pressed active:scale-95 focus:ring-meta-blue`;
case 'secondary':
return `${baseClasses} ${widthClasses} bg-transparent text-dark-charcoal opacity-50 border-2 border-[rgba(10,19,23,0.12)] rounded-full hover:bg-[rgba(70,90,105,0.7)] hover:text-white focus:ring-dark-charcoal`;
case 'danger':
return `${baseClasses} ${widthClasses} bg-error-red text-white rounded-full hover:bg-store-error hover:scale-105 active:bg-store-error active:scale-95 focus:ring-error-red`;
default:
return baseClasses;
}
};
return (
<button
onClick={onClick}
disabled={loading || disabled}
className={`${getButtonClasses()} ${loading || disabled ? 'bg-divider-gray text-cta-disabled-text cursor-not-allowed hover:scale-100 active:scale-100' : ''} ${className}`}
>
{loading ? (
<div className="flex items-center justify-center">
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-current" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
{children}
</div>
) : (
children
)}
</button>
);
};
export default Button;

View File

@ -0,0 +1,25 @@
import React from 'react';
interface CheckboxProps {
label: string;
checked: boolean;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}
const Checkbox: React.FC<CheckboxProps> = ({ label, checked, onChange }) => {
return (
<div className="flex items-center">
<input
type="checkbox"
checked={checked}
onChange={onChange}
className="h-4 w-4 text-meta-blue focus:ring-3 focus:ring-meta-blue border-divider rounded"
/>
<label className="ml-2 block text-sm text-dark-charcoal font-optimistic">
{label}
</label>
</div>
);
};
export default Checkbox;

View File

@ -0,0 +1,61 @@
import React from 'react';
import {
ChevronLeftIcon,
ChevronRightIcon,
BellIcon,
PieChartIcon,
PlusIcon,
FileTextIcon,
CheckCircledIcon,
GridIcon,
CircleIcon,
CodeIcon,
GearIcon,
ListBulletIcon
} from '@radix-ui/react-icons';
interface IconProps {
name: string;
className?: string;
size?: 'sm' | 'md' | 'lg';
}
const Icon: React.FC<IconProps> = ({ name, className = '', size = 'md' }) => {
const sizeClasses = {
sm: 'h-4 w-4',
md: 'h-5 w-5',
lg: 'h-6 w-6'
};
const icons = {
// 布局相关图标
menu: <GridIcon />,
chevronLeft: <ChevronLeftIcon />,
chevronRight: <ChevronRightIcon />,
bell: <BellIcon />,
user: <CircleIcon />,
// 首页相关图标
dashboard: <PieChartIcon />,
projects: <CodeIcon />,
settings: <GearIcon />,
plus: <PlusIcon />,
list: <ListBulletIcon />,
checkCircle: <CheckCircledIcon />,
file: <FileTextIcon />,
status: <CheckCircledIcon />
};
const icon = icons[name as keyof typeof icons];
if (!icon) {
return null;
}
return (
<span className={`${sizeClasses[size]} ${className}`}>
{icon}
</span>
);
};
export default Icon;

View File

@ -0,0 +1,49 @@
import React from 'react';
interface InputProps {
type: string;
label: string;
placeholder: string;
value: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
error?: string;
required?: boolean;
name?: string;
onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void;
}
const Input: React.FC<InputProps> = ({
type,
placeholder,
value,
onChange,
error,
required = false,
name,
label,
onKeyDown,
}) => {
return (
<div className="mb-4">
{label && (
<label className={`block text-sm font-medium mb-1 font-optimistic ${error ? 'text-error-red' : 'text-dark-charcoal'}`}>
{label} {required && <span className="text-error-red">*</span>}
</label>
)}
<div>
<input
type={type}
placeholder={placeholder}
value={value}
onChange={onChange}
name={name}
onKeyDown={onKeyDown}
className={`w-full px-3 py-2 border ${error ? 'border-error-red' : 'border-divider'} rounded-[8px] focus:outline-none focus:ring-3 focus:ring-meta-blue focus:border-meta-blue transition-all duration-200 font-optimistic text-base`}
/>
</div>
{error && <p className="mt-1 text-sm text-error-red font-optimistic">{error}</p>}
</div>
);
};
export default Input;

View File

@ -1,13 +0,0 @@
export enum Language {
Chinese = 'zh',
English = 'en',
Japanese = 'ja',
Korean = 'ko',
}
export const LANGUAGE_DISPLAY_NAMES: Record<Language, string> = {
[Language.Chinese]: '中文',
[Language.English]: 'English',
[Language.Japanese]: '日本語',
[Language.Korean]: '한국어',
} as const;

167
src/i18n/en.json Normal file
View File

@ -0,0 +1,167 @@
{
"login": {
"username": "Username",
"password": "Password",
"rememberMe": "Remember me",
"login": "Login",
"register": "Register ",
"forgotPassword": "Forgot password ",
"loading": "Logging in...",
"error": "Login failed, please check username and password",
"usernameRequired": "Username is required",
"passwordRequired": "Password is required",
"passwordMinLength": "Password must be at least 6 characters",
"placeholderUsername": "Please enter username",
"placeholderPassword": "Please enter password",
"prompt": "Please enter your username and password",
"noAccount": "Don't have an account? "
},
"register": {
"username": "Username",
"password": "Password",
"confirmPassword": "Confirm password",
"cancel": "Cancel",
"register": "Register",
"backToLogin": "Back to login",
"loading": "Registering...",
"error": "Registration failed, please try again later",
"usernameRequired": "Username is required",
"passwordRequired": "Password is required",
"passwordMinLength": "Password must be at least 6 characters",
"passwordFormat": "Password must contain letters and numbers",
"confirmPasswordRequired": "Please confirm password",
"confirmPasswordMismatch": "Passwords do not match",
"placeholderUsername": "Please enter username",
"placeholderPassword": "Please enter password",
"placeholderConfirmPassword": "Please enter password again",
"prompt": "Please fill in the following information to complete registration",
"hasAccount": "Already have an account? "
},
"dashboard": {
"welcome": "Welcome, {{username}}",
"totalProjects": "Total projects",
"activeProjects": "Active projects",
"recentProjects": "Recently created projects",
"quickActions": "Quick actions",
"createProject": "Create new project",
"viewProjects": "View all projects",
"systemStatus": "System status",
"systemNormal": "System running normally"
},
"projectList": {
"title": "Project Management",
"search": "Search project...",
"createProject": "Create new project",
"batchDelete": "Batch delete",
"selectedCount": "Selected {{count}} projects",
"confirmDelete": "Are you sure you want to delete this project?",
"confirmDeleteMessage": "This action cannot be undone. Are you sure you want to delete this project?",
"confirmBatchDelete": "Are you sure you want to delete the selected projects?",
"confirmBatchDeleteMessage": "Are you sure you want to delete the selected {{count}} projects? This action cannot be undone.",
"projectName": "Project name",
"projectDescription": "Project description",
"createdAt": "Created at",
"actions": "Actions",
"view": "View",
"edit": "Edit",
"delete": "Delete",
"cancel": "Cancel",
"save": "Save",
"status": "Status",
"allStatus": "All status",
"active": "Active",
"inactive": "Inactive",
"completed": "Completed",
"pagination": "Showing {{start}}-{{end}} of {{total}} items"
},
"projectDetail": {
"sidebar": "Left toolbar",
"topBar": "Right top toolbar",
"aiAssistant": "AI Assistant",
"conversationHistory": "Conversation history",
"switchModel": "Switch model",
"user": "User:",
"ai": "AI:",
"delete": "Delete",
"edit": "Edit",
"retry": "Retry",
"copy": "Copy",
"regenerate": "Regenerate",
"like": "Like",
"dislike": "Dislike",
"userInput": "User input",
"uploadAttachment": "Upload attachment",
"quickCommands": "Quick commands",
"voiceInput": "Voice input",
"send": "Send",
"exit": "Exit",
"confirmExit": "Are you sure you want to exit? Unsaved changes will be lost.",
"pages": "Pages",
"prototypes": "Prototypes",
"notes": "Notes",
"resources": "Resources",
"fullscreenPreview": "Fullscreen preview",
"prompts": "Prompt library",
"newPage": "New page",
"savePage": "Save page",
"importPage": "Import page",
"exportPage": "Export page"
},
"settings": {
"title": "Settings",
"profile": "Personal information",
"changePassword": "Change password",
"language": "Language settings",
"notifications": "Notification settings",
"username": "Username",
"email": "Email",
"avatar": "Avatar",
"save": "Save",
"saveSuccess": "Saved successfully",
"oldPassword": "Old password",
"newPassword": "New password",
"confirmPassword": "Confirm new password",
"passwordMismatch": "Password and confirm password do not match",
"passwordMinLength": "Password must be at least 6 characters",
"usernameRequired": "Username is required",
"emailRequired": "Email is required",
"emailInvalid": "Invalid email format",
"oldPasswordRequired": "Old password is required",
"newPasswordRequired": "New password is required",
"confirmPasswordRequired": "Confirm password is required",
"profileSaved": "Personal information saved successfully",
"passwordChanged": "Password changed successfully",
"chinese": "Chinese",
"english": "English",
"emailNotification": "Email notifications",
"systemNotification": "System notifications"
},
"forgotPassword": {
"title": "Reset Password",
"prompt": "Please fill in the following information to reset your password",
"username": "Username",
"newPassword": "New password",
"confirmPassword": "Confirm new password",
"cancel": "Cancel",
"resetPassword": "Reset password",
"backToLogin": "Back to login",
"successMessage": "Password reset successfully, please log in with your new password",
"errorMessage": "Failed to reset password, please try again later",
"usernameRequired": "Username is required",
"passwordRequired": "Password is required",
"passwordMinLength": "Password must be at least 6 characters",
"passwordFormat": "Password must contain letters and numbers",
"confirmPasswordRequired": "Please confirm password",
"confirmPasswordMismatch": "Passwords do not match",
"placeholderUsername": "Please enter username",
"placeholderPassword": "Please enter new password",
"placeholderConfirmPassword": "Please enter new password again"
},
"layout": {
"systemName": "Project Management",
"systemTitle": "Project Management Platform",
"dashboard": "Dashboard",
"projects": "Projects",
"settings": "Settings"
}
}

View File

@ -1,40 +1,25 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import Cookies from 'js-cookie';
import zh from '../messages/zh.json';
import en from '../messages/en.json';
import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'
import zh from './zh.json'
import en from './en.json'
const resources = {
zh: { translation: zh },
en: { translation: en },
};
export const LANGUAGE_COOKIE_KEY = 'ui_to_code_language';
const savedLanguage = Cookies.get(LANGUAGE_COOKIE_KEY);
zh: {
translation: zh
},
en: {
translation: en
}
}
i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
resources,
lng: savedLanguage || 'zh',
fallbackLng: 'zh',
lng: 'zh',
interpolation: {
escapeValue: false,
},
detection: {
order: ['cookie', 'localStorage', 'navigator'],
caches: ['cookie'],
},
});
escapeValue: false
}
})
export const changeLanguage = async (lang: string) => {
await i18n.changeLanguage(lang);
Cookies.set(LANGUAGE_COOKIE_KEY, lang, { expires: 365 });
};
export const getCurrentLanguage = () => i18n.language;
export default i18n;
export default i18n

View File

@ -1,27 +0,0 @@
import zh from '../messages/zh.json';
export type MessageKeyPaths = MessagePaths<typeof zh>;
export const transKeys = buildPaths(zh) as MessageKeyPaths;
function buildPaths(obj: Record<string, any>, prefix = ''): any {
const result: Record<string, any> = {};
for (const key of Object.keys(obj)) {
const path = prefix ? `${prefix}.${key}` : key;
const value = (obj as Record<string, any>)[key];
if (typeof value === 'object' && !Array.isArray(value)) {
result[key] = buildPaths(value, path);
} else {
result[key] = path;
}
}
return result;
}
export type MessagePaths<T, Prefix extends string = ''> = {
[K in keyof T]: T[K] extends string
? `${Prefix}${Extract<K, string>}`
: T[K] extends Record<string, any>
? MessagePaths<T[K], `${Prefix}${Extract<K, string>}.`>
: never;
};

167
src/i18n/zh.json Normal file
View File

@ -0,0 +1,167 @@
{
"login": {
"username": "用户名",
"password": "密码",
"rememberMe": "记住我",
"login": "登录",
"register": "注册",
"forgotPassword": "忘记密码",
"loading": "登录中...",
"error": "登录失败,请检查用户名和密码",
"usernameRequired": "用户名不能为空",
"passwordRequired": "密码不能为空",
"passwordMinLength": "密码长度至少6位",
"placeholderUsername": "请输入用户名",
"placeholderPassword": "请输入密码",
"prompt": "请输入您的用户名和密码",
"noAccount": "还没有账号? "
},
"register": {
"username": "用户名",
"password": "密码",
"confirmPassword": "确认密码",
"cancel": "取消",
"register": "注册",
"backToLogin": "返回登录",
"loading": "注册中...",
"error": "注册失败,请稍后再试",
"usernameRequired": "用户名不能为空",
"passwordRequired": "密码不能为空",
"passwordMinLength": "密码长度至少6位",
"passwordFormat": "密码必须包含字母和数字",
"confirmPasswordRequired": "请确认密码",
"confirmPasswordMismatch": "两次输入的密码不一致",
"placeholderUsername": "请输入用户名",
"placeholderPassword": "请输入密码",
"placeholderConfirmPassword": "请再次输入密码",
"prompt": "请填写以下信息完成注册",
"hasAccount": "已有账号? "
},
"dashboard": {
"welcome": "欢迎,{{username}}",
"totalProjects": "总项目数",
"activeProjects": "活跃项目数",
"recentProjects": "最近创建的项目",
"quickActions": "快捷操作",
"createProject": "创建新项目",
"viewProjects": "查看所有项目",
"systemStatus": "系统状态",
"systemNormal": "系统运行正常"
},
"projectList": {
"title": "项目管理",
"search": "搜索项目...",
"createProject": "创建新项目",
"batchDelete": "批量删除",
"selectedCount": "已选择 {{count}} 个项目",
"confirmDelete": "确定要删除这个项目吗?",
"confirmDeleteMessage": "此操作无法撤销,确定要删除这个项目吗?",
"confirmBatchDelete": "确定要删除选中的项目吗?",
"confirmBatchDeleteMessage": "确定要删除选中的 {{count}} 个项目吗?此操作无法撤销。",
"projectName": "项目名称",
"projectDescription": "项目描述",
"createdAt": "创建时间",
"actions": "操作",
"view": "查看",
"edit": "编辑",
"delete": "删除",
"cancel": "取消",
"save": "保存",
"status": "状态",
"allStatus": "全部状态",
"active": "活跃",
"inactive": "非活跃",
"completed": "已完成",
"pagination": "显示 {{start}}-{{end}} 项,共 {{total}} 项"
},
"projectDetail": {
"sidebar": "左侧工具栏",
"topBar": "右侧顶部工具栏",
"aiAssistant": "AI助手",
"conversationHistory": "对话历史",
"switchModel": "切换模型",
"user": "用户:",
"ai": "AI",
"delete": "删除",
"edit": "编辑",
"retry": "重试",
"copy": "复制",
"regenerate": "重新生成",
"like": "点赞",
"dislike": "踩",
"userInput": "用户输入",
"uploadAttachment": "上传附件",
"quickCommands": "快捷命令",
"voiceInput": "语音输入",
"send": "发送",
"exit": "退出",
"confirmExit": "确定要退出吗?未保存的更改将会丢失。",
"pages": "页面",
"prototypes": "原型库",
"notes": "备注",
"resources": "资源管理",
"fullscreenPreview": "全屏预览",
"prompts": "提示词库",
"newPage": "新建页面",
"savePage": "保存页面",
"importPage": "导入页面",
"exportPage": "导出页面"
},
"settings": {
"title": "设置",
"profile": "个人信息",
"changePassword": "密码修改",
"language": "语言设置",
"notifications": "通知设置",
"username": "用户名",
"email": "邮箱",
"avatar": "头像",
"save": "保存",
"saveSuccess": "保存成功",
"oldPassword": "旧密码",
"newPassword": "新密码",
"confirmPassword": "确认新密码",
"passwordMismatch": "密码和确认密码不一致",
"passwordMinLength": "密码长度至少6位",
"usernameRequired": "用户名不能为空",
"emailRequired": "邮箱不能为空",
"emailInvalid": "邮箱格式无效",
"oldPasswordRequired": "旧密码不能为空",
"newPasswordRequired": "新密码不能为空",
"confirmPasswordRequired": "确认新密码不能为空",
"profileSaved": "个人信息保存成功",
"passwordChanged": "密码修改成功",
"chinese": "中文",
"english": "英语",
"emailNotification": "邮件通知",
"systemNotification": "系统通知"
},
"forgotPassword": {
"title": "重置密码",
"prompt": "请填写以下信息重置您的密码",
"username": "用户名",
"newPassword": "新密码",
"confirmPassword": "确认新密码",
"cancel": "取消",
"resetPassword": "重置密码",
"backToLogin": "返回登录",
"successMessage": "密码重置成功,请使用新密码登录",
"errorMessage": "重置密码失败,请稍后重试",
"usernameRequired": "用户名不能为空",
"passwordRequired": "密码不能为空",
"passwordMinLength": "密码长度至少6位",
"passwordFormat": "密码必须包含字母和数字",
"confirmPasswordRequired": "请确认密码",
"confirmPasswordMismatch": "两次输入的密码不一致",
"placeholderUsername": "请输入用户名",
"placeholderPassword": "请输入新密码",
"placeholderConfirmPassword": "请再次输入新密码"
},
"layout": {
"systemName": "项目管理",
"systemTitle": "项目管理平台",
"dashboard": "首页",
"projects": "项目管理",
"settings": "设置"
}
}

View File

@ -1,678 +1,3 @@
:root {
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-kerning: normal;
}
:root {
/* Primary */
--color-meta-blue: #0064E0;
--color-meta-blue-hover: #0143B5;
--color-meta-blue-pressed: #004BB9;
--color-meta-blue-light: #47A5FA;
--color-facebook-blue: #1877F2;
/* Secondary & Accent */
--color-ray-ban-red: #D6311F;
--color-oculus-purple: #A121CE;
--color-work-purple: #6441D2;
--color-portal-blue: #1B365D;
--color-portal-hero-blue: #C8E4E8;
--color-portal-light-blue: #ADD4E0;
/* Surface & Background */
--color-white: #FFFFFF;
--color-soft-gray: #F1F4F7;
--color-warm-gray: #F7F8FA;
--color-web-wash: #F0F2F5;
--color-linen: #F2F0E6;
--color-baby-blue: #E8F3FF;
--color-near-black: #1C1E21;
--color-oculus-light: #181A1B;
--color-oculus-dark: #000000;
--color-overlay: rgba(0, 0, 0, 0.6);
/* Neutrals & Text */
--color-primary-text: #050505;
--color-dark-charcoal: #1C2B33;
--color-icon-secondary: #465A69;
--color-secondary-text: #65676B;
--color-slate-gray: #5D6C7B;
--color-section-header: #4B4C4F;
--color-button-text-gray: #444950;
--color-disabled-text: #BCC0C4;
--color-cta-disabled-text: #8595A4;
--color-divider: #CED0D4;
--color-divider-gray: #DEE3E9;
--color-cta-gray-border: #CBD2D9;
--color-dark-gray-border: #909396;
/* Semantic & Accent */
--color-success-green: #31A24C;
--color-store-success: #007D1E;
--color-error-red: #E41E3F;
--color-store-error: #C80A28;
--color-warning-amber: #F7B928;
--color-positive-bg: rgba(36, 228, 0, 0.15);
--color-error-bg: rgba(255, 123, 145, 0.15);
--color-warning-bg: rgba(255, 226, 0, 0.15);
--color-info-bg: rgba(0, 145, 255, 0.15);
/* Base Color Spectrum (FDS) */
--color-cherry: #F3425F;
--color-grape: #9360F7;
--color-lime: #45BD62;
--color-seafoam: #54C7EC;
--color-teal: #2ABBA7;
--color-tomato: #FB724B;
--color-pink: #FF66BF;
/* Text Colors */
--color-text-primary: var(--color-dark-charcoal);
--color-text-secondary: var(--color-slate-gray);
--color-text-muted: var(--color-secondary-text);
--color-text-placeholder: var(--color-secondary-text);
/* Background Colors */
--color-background: var(--color-white);
--color-surface: var(--color-white);
--color-secondary-background: var(--color-soft-gray);
/* Border Colors */
--color-border-subtle: var(--color-divider-gray);
--color-border-medium: var(--color-divider);
--color-border-strong: var(--color-dark-gray-border);
/* Link Colors */
--color-link-default: var(--color-meta-blue);
--color-link-hover: var(--color-meta-blue-hover);
/* Button Colors */
--color-primary: var(--color-meta-blue);
--color-primary-hover: var(--color-meta-blue-hover);
--color-primary-pressed: var(--color-meta-blue-pressed);
--color-button-background: var(--color-soft-gray);
--color-button-hover: rgba(70, 90, 105, 0.7);
/* Border Radius */
--radius-small: 8px;
--radius-card: 20px;
--radius-feature: 24px;
--radius-pill: 100px;
--radius-circle: 50%;
/* Spacing */
--spacing-unit: 8px;
--space-1: 1px;
--space-2: 4px;
--space-3: 8px;
--space-4: 10px;
--space-5: 12px;
--space-6: 14px;
--space-7: 16px;
--space-8: 18px;
--space-9: 24px;
--space-10: 32px;
--space-11: 40px;
--space-12: 48px;
--space-13: 64px;
--space-14: 80px;
/* Letter Spacing */
--letter-spacing-tight: -0.16px;
--letter-spacing-tighter: -0.14px;
--letter-spacing-normal: normal;
/* Font Weights */
--font-weight-light: 300;
--font-weight-regular: 400;
--font-weight-medium: 500;
--font-weight-bold: 700;
/* Font Sizes */
--font-size-display-1: 64px;
--font-size-display-2: 48px;
--font-size-heading-1: 36px;
--font-size-heading-2: 28px;
--font-size-heading-3: 18px;
--font-size-body: 18px;
--font-size-body-compact: 16px;
--font-size-caption-bold: 14px;
--font-size-caption: 14px;
--font-size-small: 12px;
--font-size-button: 14px;
/* Line Heights */
--line-height-display-1: 1.16;
--line-height-display-2: 1.17;
--line-height-heading-1: 1.28;
--line-height-heading-2: 1.21;
--line-height-heading-3: 1.44;
--line-height-body: 1.44;
--line-height-body-compact: 1.50;
--line-height-caption: 1.43;
--line-height-small: 1.33;
--line-height-button: 1.43;
}
/* Font Face Definitions */
@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;700&display=swap');
/* Using Montserrat as fallback for Optimistic VF */
:root {
--font-family-primary: 'Optimistic VF', 'Montserrat', Helvetica, Arial, sans-serif;
--font-family-secondary: Helvetica, Arial, sans-serif;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
margin: 0;
min-width: 320px;
min-height: 100vh;
background-color: var(--color-background);
color: var(--color-text-primary);
transition:
background-color 0.3s ease,
color 0.3s ease;
font-family: var(--font-family-primary);
font-weight: var(--font-weight-regular);
letter-spacing: var(--letter-spacing-normal);
}
button {
font-family: inherit;
font-weight: var(--font-weight-regular);
letter-spacing: var(--letter-spacing-tighter);
cursor: pointer;
border: none;
outline: none;
transition: background 200ms ease, transform 150ms ease;
}
input,
textarea,
select {
font-family: inherit;
font-weight: var(--font-weight-regular);
letter-spacing: var(--letter-spacing-normal);
border: 1px solid var(--color-border-medium);
background-color: var(--color-white);
color: var(--color-text-primary);
border-radius: var(--radius-small);
padding: 8px 12px;
transition: border-color 200ms ease, box-shadow 200ms ease;
}
input:focus,
textarea:focus,
select:focus {
border-color: hsl(214, 89%, 52%);
outline: none;
box-shadow: 0 0 0 3px rgba(0, 100, 224, 0.1);
}
:focus-visible {
outline: 3px ring var(--color-meta-blue);
outline: auto 2px var(--color-meta-blue);
outline-offset: 2px;
}
::selection {
background-color: var(--color-meta-blue);
color: var(--color-white);
}
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--color-soft-gray);
}
::-webkit-scrollbar-thumb {
background: var(--color-secondary-text);
border-radius: var(--radius-pill);
}
::-webkit-scrollbar-thumb:hover {
background: var(--color-slate-gray);
}
/* Button Styles */
.btn {
padding: var(--space-4) var(--space-9);
border-radius: var(--radius-pill);
font-size: var(--font-size-button);
font-weight: var(--font-weight-regular);
letter-spacing: var(--letter-spacing-tighter);
transition: background 200ms ease, transform 150ms ease;
font-family: var(--font-family-primary);
}
.btn-primary {
background-color: var(--color-meta-blue);
color: var(--color-white);
border: none;
border-radius: var(--radius-pill);
}
.btn-primary:hover {
background-color: var(--color-meta-blue-hover);
transform: scale(1.1);
}
.btn-primary:active {
background-color: var(--color-meta-blue-pressed);
transform: scale(0.9);
opacity: 0.5;
}
.btn-primary:focus-visible {
outline: 3px ring var(--color-meta-blue);
outline: auto 2px var(--color-meta-blue);
outline-offset: 2px;
}
.btn-secondary {
background-color: transparent;
color: rgba(28, 43, 51, 0.5);
border: 2px solid rgba(10, 19, 23, 0.12);
border-radius: var(--radius-pill);
padding: var(--space-4) var(--space-9);
}
.btn-secondary:hover {
background-color: var(--color-button-hover);
color: var(--color-white);
}
.btn-ghost {
background-color: transparent;
color: #385898;
border-radius: 24px;
padding: var(--space-2) var(--space-5);
}
.btn-disabled {
background-color: var(--color-divider-gray);
color: var(--color-cta-disabled-text);
cursor: not-allowed;
pointer-events: none;
}
/* Card Styles */
.card {
background-color: var(--color-white);
border-radius: var(--radius-card);
padding: var(--space-4) var(--space-9);
transition: transform 300ms ease, box-shadow 300ms ease;
box-shadow: 0 2px 4px 0 rgba(0,0,0,0.1);
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 12px 28px 0 rgba(0,0,0,0.2), 0 2px 4px 0 rgba(0,0,0,0.1);
}
.card-feature {
background-color: var(--color-white);
border-radius: var(--radius-feature);
padding: var(--space-10);
transition: transform 300ms ease, box-shadow 300ms ease;
box-shadow: 0 2px 4px 0 rgba(0,0,0,0.1);
}
.card-feature:hover {
transform: translateY(-2px);
box-shadow: 0 12px 28px 0 rgba(0,0,0,0.2), 0 2px 4px 0 rgba(0,0,0,0.1);
}
/* Link Styles */
a {
color: var(--color-link-default);
text-decoration: none;
transition: color 0.15s ease;
}
a:hover {
color: var(--color-link-hover);
text-decoration: underline;
}
/* Typography Classes */
.text-display-1 {
font-size: var(--font-size-display-1);
font-weight: var(--font-weight-medium);
line-height: var(--line-height-display-1);
font-family: var(--font-family-primary);
font-feature-settings: "ss01", "ss02";
}
.text-display-2 {
font-size: var(--font-size-display-2);
font-weight: var(--font-weight-medium);
line-height: var(--line-height-display-2);
font-family: var(--font-family-primary);
font-feature-settings: "ss01", "ss02";
}
.text-heading-1 {
font-size: var(--font-size-heading-1);
font-weight: var(--font-weight-medium);
line-height: var(--line-height-heading-1);
font-family: var(--font-family-primary);
}
.text-heading-2 {
font-size: var(--font-size-heading-2);
font-weight: var(--font-weight-light);
line-height: var(--line-height-heading-2);
font-family: var(--font-family-primary);
}
.text-heading-3 {
font-size: var(--font-size-heading-3);
font-weight: var(--font-weight-bold);
line-height: var(--line-height-heading-3);
font-family: var(--font-family-primary);
font-feature-settings: "ss01", "ss02";
}
.text-body {
font-size: var(--font-size-body);
font-weight: var(--font-weight-regular);
line-height: var(--line-height-body);
font-family: var(--font-family-primary);
color: var(--color-text-secondary);
}
.text-body-compact {
font-size: var(--font-size-body-compact);
font-weight: var(--font-weight-medium);
line-height: var(--line-height-body-compact);
letter-spacing: var(--letter-spacing-tight);
font-family: var(--font-family-primary);
}
.text-caption-bold {
font-size: var(--font-size-caption-bold);
font-weight: var(--font-weight-bold);
line-height: var(--line-height-caption);
font-family: var(--font-family-primary);
}
.text-caption {
font-size: var(--font-size-caption);
font-weight: var(--font-weight-regular);
line-height: var(--line-height-caption);
letter-spacing: var(--letter-spacing-tighter);
font-family: var(--font-family-primary);
color: var(--color-text-secondary);
}
.text-small {
font-size: var(--font-size-small);
font-weight: var(--font-weight-regular);
line-height: var(--line-height-small);
font-family: var(--font-family-secondary);
color: var(--color-text-secondary);
}
.text-button {
font-size: var(--font-size-button);
font-weight: var(--font-weight-regular);
line-height: var(--line-height-button);
letter-spacing: var(--letter-spacing-tighter);
font-family: var(--font-family-primary);
}
/* Utility Classes */
.container {
max-width: 1440px;
margin: 0 auto;
padding: 0 var(--space-9);
}
.section {
padding: var(--space-13) 0;
}
.section-compact {
padding: var(--space-12) 0;
}
.section-hero {
padding: var(--space-14) 0;
}
.flex {
display: flex;
}
.flex-col {
flex-direction: column;
}
.items-center {
align-items: center;
}
.justify-center {
justify-content: center;
}
.justify-between {
justify-content: space-between;
}
.gap-3 {
gap: var(--space-3);
}
.gap-4 {
gap: var(--space-4);
}
.gap-5 {
gap: var(--space-5);
}
.gap-6 {
gap: var(--space-6);
}
.gap-7 {
gap: var(--space-7);
}
.gap-8 {
gap: var(--space-8);
}
.gap-9 {
gap: var(--space-9);
}
.mt-3 {
margin-top: var(--space-3);
}
.mt-4 {
margin-top: var(--space-4);
}
.mt-5 {
margin-top: var(--space-5);
}
.mt-6 {
margin-top: var(--space-6);
}
.mt-7 {
margin-top: var(--space-7);
}
.mt-8 {
margin-top: var(--space-8);
}
.mt-9 {
margin-top: var(--space-9);
}
.mt-10 {
margin-top: var(--space-10);
}
.mb-3 {
margin-bottom: var(--space-3);
}
.mb-4 {
margin-bottom: var(--space-4);
}
.mb-5 {
margin-bottom: var(--space-5);
}
.mb-6 {
margin-bottom: var(--space-6);
}
.mb-7 {
margin-bottom: var(--space-7);
}
.mb-8 {
margin-bottom: var(--space-8);
}
.mb-9 {
margin-bottom: var(--space-9);
}
.mb-10 {
margin-bottom: var(--space-10);
}
/* Responsive Classes */
@media (max-width: 1440px) {
.container {
max-width: 1200px;
}
}
@media (max-width: 1024px) {
.container {
max-width: 960px;
}
.text-display-1 {
font-size: var(--font-size-display-2);
}
.section-hero {
padding: var(--space-13) 0;
}
}
@media (max-width: 768px) {
.container {
max-width: 100%;
padding: 0 var(--space-7);
}
.text-display-1 {
font-size: var(--font-size-heading-1);
}
.text-display-2 {
font-size: var(--font-size-heading-1);
}
.section {
padding: var(--space-12) 0;
}
.section-hero {
padding: var(--space-11) 0;
}
}
/* Image Treatment */
.img-fluid {
max-width: 100%;
height: auto;
}
.img-hero {
width: 100%;
aspect-ratio: 21/9;
object-fit: cover;
}
.img-card {
width: 100%;
aspect-ratio: 1/1;
object-fit: cover;
border-radius: var(--radius-card) var(--radius-card) 0 0;
}
/* Gradient Overlays */
.gradient-overlay {
position: relative;
}
.gradient-overlay::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 50%;
background: linear-gradient(rgba(0,0,0,0), rgba(0,0,0,0.6));
border-radius: 0 0 var(--radius-card) var(--radius-card);
}
/* Frosted Glass Effect */
.frosted-glass {
background: rgba(241, 244, 247, 0.8);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
}
/* Elevation Levels */
.elevation-1 {
box-shadow: 0 2px 4px 0 rgba(0,0,0,0.1);
}
.elevation-2 {
box-shadow: 0 12px 28px 0 rgba(0,0,0,0.2), 0 2px 4px 0 rgba(0,0,0,0.1);
}
/* Glimmer Loading State */
.glimmer {
background: #979A9F;
border-radius: var(--radius-small);
animation: glimmer 1000ms steps(2) infinite;
}
@keyframes glimmer {
0% {
opacity: 0.25;
}
100% {
opacity: 1.0;
}
}
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@ -1,15 +1,11 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import { QueryClientProvider } from '@tanstack/react-query'
import App from './App'
import App from './App.tsx'
import './index.css'
import './i18n'
import queryClient from './services/queryClient'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
<App />
</React.StrictMode>,
)

View File

@ -1,184 +0,0 @@
{
"topBar": {
"newProject": "New Project",
"save": "Save",
"export": "Export",
"settings": "Settings",
"showChatPanel": "Show Chat Panel",
"hideChatPanel": "Hide Chat Panel"
},
"languageSwitch": {
"title": "Language Settings",
"selectLanguage": "Select Language",
"submit": "Submit",
"cancel": "Cancel"
},
"languages": {
"zh": "Chinese",
"en": "English"
},
"aiChatPanel": {
"greeting": "Hello! I'm your AI assistant. I can help you generate code. What can I assist you with?",
"newChat": "New Chat",
"conversationHistory": "Conversation History",
"aiAssistant": "AI Assistant",
"clearContext": "Clear Context",
"uploading": "Uploading...",
"inputPlaceholder": "Type your question...",
"send": "Send",
"models": {
"gpt4o": "GPT-4o",
"claude4": "Claude 4 Sonnet",
"gemini": "Gemini Pro",
"mistral": "Mistral 7B"
},
"loading": "Thinking...",
"response": "This is AI response content. I can help you generate code, answer questions, or provide suggestions."
},
"canvasArea": {
"loading": "Loading design files...",
"error": {
"title": "Error Loading Canvas",
"retry": "Retry"
},
"empty": {
"title": "No Design Files Found",
"message": "Add design files to start working"
},
"toolbar": {
"zoomOut": "Zoom Out (Cmd/Ctrl + -)",
"zoomIn": "Zoom In (Cmd/Ctrl + +)",
"resetZoom": "Reset Zoom (Cmd/Ctrl + 0)",
"resetPositions": "Reset Frame Positions",
"gridLayout": "Grid Layout",
"hierarchyLayout": "Hierarchy Layout",
"toggleConnections": "Toggle Connections",
"toggleGlobalViewport": "Toggle Global Viewport Mode",
"mobileView": "Mobile View (375×667)",
"tabletView": "Tablet View (768×1024)",
"desktopView": "Desktop View (1200×800)",
"grid": "Grid",
"hierarchy": "Hierarchy"
}
},
"contextModal": {
"title": "Add Note",
"cancel": "Cancel",
"titleLabel": "Title",
"titlePlaceholder": "Enter title...",
"descriptionLabel": "Description",
"descriptionPlaceholder": "Enter description...",
"submit": "Submit"
},
"designFrame": {
"viewport": {
"mobile": "Mobile",
"tablet": "Tablet",
"desktop": "Desktop"
},
"placeholder": {
"svg": "SVG Vector Graphics",
"html": "HTML Design",
"hint": "Zoom in to load"
},
"drag": "Ready to drag",
"loading": "Loading...",
"error": "Load failed",
"toolbar": {
"bringToFront": "Bring to Front",
"layout": "Layout",
"spacing": "Spacing",
"style": "Style",
"delete": "Delete",
"more": "More"
},
"floatingActions": {
"createVariations": "Create more variations based on this style",
"createVariationsBtn": "Create Variations",
"iterateWithFeedback": "Create variations based on feedback",
"iterateWithFeedbackBtn": "Feedback Iteration",
"copyPrompt": "Copy file content and reference prompt",
"copyPromptBtn": "Copy Prompt",
"copyPath": "Copy design file absolute path",
"copyPathBtn": "Copy Design Path",
"copiedSuccess": "Copied!",
"copiedSuccessWithPlatform": "Copied for {{platform}}!"
},
"platforms": {
"cursor": "Cursor",
"windsurf": "Windsurf",
"lovable": "Lovable",
"bolt": "Bolt"
},
"prompts": {
"cursor": "This is a design implementation. Please reference it to build similar UI components. Ensure to follow modern React and TypeScript best practices.",
"windsurf": "This is a design implementation. Please analyze this design and create similar UI components using modern web technologies.",
"lovable": "This is a design implementation. Please recreate this design as a responsive React component using modern styles.",
"bolt": "This is a design implementation. Please reference this content to create similar UI. Ensure to use appropriate styles to make it production-ready.",
"default": "This is a design implementation. Please reference",
"createVariations": "Create more variations based on this style",
"iterateWithFeedback": "Please create some variations based on this feedback:"
}
},
"sideBar": {
"menu": {
"select": "Select",
"note": "Note",
"copy": "Copy",
"customize": "Customize Resources",
"fix": "Fix Context",
"add": "Add Component"
},
"noteModal": {
"title": "Add Note",
"placeholder": "Please enter note content...",
"submit": "Submit"
},
"noSelection": "Please select design elements first",
"resourceModal": {
"title": "Resource Management",
"logo": "Logo Management",
"font": "Font Management",
"image": "Image Management",
"uploadLogo": "Upload Logo",
"uploadFont": "Upload Font",
"uploadImage": "Upload Image"
}
},
"projects": {
"title": "Projects",
"logout": "Logout",
"loading": "Loading...",
"empty": "No projects yet",
"meta": {
"createdAt": "Created:",
"updatedAt": "Updated:"
}
},
"auth": {
"login": {
"title": "Login",
"error": "Login failed, please check username and password",
"username": "Username",
"password": "Password",
"loading": "Logging in...",
"noAccount": "Don't have an account?",
"register": "Register now"
},
"register": {
"title": "Register",
"error": {
"passwordMismatch": "Passwords do not match",
"failed": "Registration failed, please try again later"
},
"username": "Username",
"password": "Password",
"confirmPassword": "Confirm Password",
"cancel": "Cancel",
"loading": "Registering...",
"register": "Register",
"haveAccount": "Already have an account?",
"login": "Login now"
}
}
}

View File

@ -1,184 +0,0 @@
{
"topBar": {
"newProject": "新建项目",
"save": "保存",
"export": "导出",
"settings": "设置",
"showChatPanel": "显示聊天面板",
"hideChatPanel": "隐藏聊天面板"
},
"languageSwitch": {
"title": "语言设置",
"selectLanguage": "选择语言",
"submit": "提交",
"cancel": "取消"
},
"languages": {
"zh": "中文",
"en": "英语"
},
"aiChatPanel": {
"greeting": "你好我是AI助手可以帮助你生成代码。请问有什么需要帮助的吗",
"newChat": "新对话",
"conversationHistory": "对话历史",
"aiAssistant": "AI 助手",
"clearContext": "清除上下文",
"uploading": "上传中...",
"inputPlaceholder": "输入你的问题...",
"send": "发送",
"models": {
"gpt4o": "GPT-4o",
"claude4": "Claude 4 Sonnet",
"gemini": "Gemini Pro",
"mistral": "Mistral 7B"
},
"loading": "思考中...",
"response": "这是AI的回复内容。我可以帮助你生成代码、解答问题或提供建议。"
},
"canvasArea": {
"loading": "正在加载设计文件...",
"error": {
"title": "加载画布出错",
"retry": "重试"
},
"empty": {
"title": "未找到设计文件",
"message": "添加设计文件以开始工作"
},
"toolbar": {
"zoomOut": "缩小 (Cmd/Ctrl + -)",
"zoomIn": "放大 (Cmd/Ctrl + +)",
"resetZoom": "重置缩放 (Cmd/Ctrl + 0)",
"resetPositions": "重置帧位置",
"gridLayout": "网格布局",
"hierarchyLayout": "层级布局",
"toggleConnections": "切换连接线",
"toggleGlobalViewport": "切换全局视口模式",
"mobileView": "手机视图 (375×667)",
"tabletView": "平板视图 (768×1024)",
"desktopView": "桌面视图 (1200×800)",
"grid": "网格",
"hierarchy": "层级"
}
},
"contextModal": {
"title": "添加便签",
"cancel": "取消",
"titleLabel": "标题",
"titlePlaceholder": "请输入标题...",
"descriptionLabel": "说明",
"descriptionPlaceholder": "请输入说明...",
"submit": "提交"
},
"designFrame": {
"viewport": {
"mobile": "手机",
"tablet": "平板",
"desktop": "桌面"
},
"placeholder": {
"svg": "SVG Vector Graphics",
"html": "HTML Design",
"hint": "Zoom in to load"
},
"drag": "准备拖拽",
"loading": "加载中...",
"error": "加载失败",
"toolbar": {
"bringToFront": "置顶",
"layout": "布局",
"spacing": "间距",
"style": "样式",
"delete": "删除",
"more": "更多"
},
"floatingActions": {
"createVariations": "基于此样式创建更多变体",
"createVariationsBtn": "创建变体",
"iterateWithFeedback": "根据反馈创建变体",
"iterateWithFeedbackBtn": "反馈迭代",
"copyPrompt": "复制文件内容和参考提示词",
"copyPromptBtn": "复制提示词",
"copyPath": "复制设计文件绝对路径",
"copyPathBtn": "复制设计路径",
"copiedSuccess": "已复制!",
"copiedSuccessWithPlatform": "已为{{platform}}复制!"
},
"platforms": {
"cursor": "Cursor",
"windsurf": "Windsurf",
"lovable": "Lovable",
"bolt": "Bolt"
},
"prompts": {
"cursor": "以上是设计实现请参考它构建类似的UI组件。确保遵循现代React和TypeScript最佳实践。",
"windsurf": "以上是设计实现。请分析此设计并使用现代Web技术创建类似的UI组件。",
"lovable": "以上是设计实现。请使用现代样式将此设计重新创建为响应式React组件。",
"bolt": "以上是设计实现。请参考此内容创建类似的UI。确保使用适当的样式使其达到生产就绪状态。",
"default": "以上是设计实现,请参考",
"createVariations": "基于此样式创建更多变体",
"iterateWithFeedback": "请根据此反馈创建一些变体:"
}
},
"sideBar": {
"menu": {
"select": "选择",
"note": "备注",
"copy": "复制",
"customize": "自定义资源设置",
"fix": "固定上下文",
"add": "添加组件"
},
"noteModal": {
"title": "添加备注",
"placeholder": "请输入备注内容...",
"submit": "提交"
},
"noSelection": "请先选择设计元素",
"resourceModal": {
"title": "资源管理",
"logo": "Logo管理",
"font": "Font管理",
"image": "Image管理",
"uploadLogo": "上传Logo",
"uploadFont": "上传Font",
"uploadImage": "上传Image"
}
},
"projects": {
"title": "项目列表",
"logout": "退出登录",
"loading": "加载中...",
"empty": "暂无项目",
"meta": {
"createdAt": "创建时间:",
"updatedAt": "更新时间:"
}
},
"auth": {
"login": {
"title": "登录",
"error": "登录失败,请检查用户名和密码",
"username": "用户名",
"password": "密码",
"loading": "登录中...",
"noAccount": "还没有账号?",
"register": "立即注册"
},
"register": {
"title": "注册",
"error": {
"passwordMismatch": "两次输入的密码不一致",
"failed": "注册失败,请稍后再试"
},
"username": "用户名",
"password": "密码",
"confirmPassword": "确认密码",
"cancel": "取消",
"loading": "注册中...",
"register": "注册",
"haveAccount": "已有账号?",
"login": "立即登录"
}
}
}

View File

@ -1,973 +0,0 @@
.canvas-area {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
background-color: var(--color-background);
overflow: hidden;
}
.canvas-toolbar {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--space-4);
background-color: var(--color-white);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-card);
z-index: 100;
box-shadow: 0 2px 4px 0 rgba(0,0,0,0.1);
}
.toolbar-section {
display: flex;
align-items: center;
margin-right: var(--space-4);
}
.toolbar-section:last-child {
margin-right: 0;
}
.control-group {
display: flex;
align-items: center;
gap: var(--space-4);
}
.toolbar-btn {
padding: var(--space-3) var(--space-5);
border: 1px solid var(--color-border-subtle);
background-color: var(--color-soft-gray);
color: var(--color-text-primary);
border-radius: var(--radius-pill);
cursor: pointer;
transition: background 200ms ease, transform 150ms ease;
display: flex;
align-items: center;
justify-content: center;
font-family: var(--font-family-primary);
font-size: var(--font-size-button);
font-weight: var(--font-weight-regular);
letter-spacing: var(--letter-spacing-tighter);
}
.toolbar-btn:hover {
background-color: var(--color-button-hover);
color: var(--color-white);
border-color: var(--color-meta-blue);
transform: scale(1.05);
}
.toolbar-btn:focus-visible {
outline: 3px ring var(--color-meta-blue);
outline: auto 2px var(--color-meta-blue);
outline-offset: 2px;
}
.toolbar-btn.active {
background-color: var(--color-meta-blue);
border-color: var(--color-meta-blue);
color: var(--color-white);
}
.toolbar-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.zoom-display {
padding: 0 var(--space-3);
font-family: var(--font-family-secondary);
font-size: var(--font-size-small);
font-weight: var(--font-weight-bold);
color: var(--color-text-primary);
opacity: 0.8;
min-width: 50px;
text-align: center;
letter-spacing: var(--letter-spacing-normal);
}
.zoom-value {
font-weight: var(--font-weight-bold);
color: var(--color-text-primary);
}
.toolbar-divider {
width: 1px;
height: 16px;
background-color: var(--color-border-subtle);
margin: 0 var(--space-3);
}
.layout-toggle {
display: flex;
background-color: var(--color-soft-gray);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-pill);
overflow: hidden;
}
.toggle-btn {
padding: var(--space-3) var(--space-7);
border: none;
background-color: transparent;
color: var(--color-text-primary);
opacity: 0.8;
cursor: pointer;
transition: all 0.15s ease;
display: flex;
align-items: center;
justify-content: center;
font-family: var(--font-family-primary);
font-size: var(--font-size-caption);
font-weight: var(--font-weight-regular);
letter-spacing: var(--letter-spacing-tighter);
}
.toggle-btn:hover {
background-color: var(--color-button-hover);
color: var(--color-white);
opacity: 1;
}
.toggle-btn:focus-visible {
outline: 3px ring var(--color-meta-blue);
outline: auto 2px var(--color-meta-blue);
outline-offset: 2px;
}
.toggle-btn.active {
background-color: var(--color-meta-blue);
color: var(--color-white);
}
.viewport-selector {
display: flex;
gap: var(--space-3);
}
.viewport-btn {
padding: var(--space-3) var(--space-5);
border: 1px solid var(--color-border-subtle);
background-color: var(--color-soft-gray);
color: var(--color-text-primary);
opacity: 0.8;
border-radius: var(--radius-circle);
cursor: pointer;
transition: all 0.15s ease;
display: flex;
align-items: center;
justify-content: center;
font-family: var(--font-family-primary);
font-size: var(--font-size-caption);
letter-spacing: var(--letter-spacing-tighter);
}
.viewport-btn:hover {
background-color: var(--color-button-hover);
color: var(--color-white);
opacity: 1;
border-color: var(--color-meta-blue);
}
.viewport-btn:focus-visible {
outline: 3px ring var(--color-meta-blue);
outline: auto 2px var(--color-meta-blue);
outline-offset: 2px;
}
.viewport-btn.active {
background-color: var(--color-meta-blue);
color: var(--color-white);
border-color: var(--color-meta-blue);
}
.viewport-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.canvas-transform-wrapper {
width: 100% !important;
height: 100% !important;
overflow: hidden;
}
.react-transform-wrapper {
width: 100% !important;
height: 100% !important;
overflow: hidden !important;
}
.canvas-transform-content {
position: relative;
width: 100%;
height: 100%;
}
.canvas-grid {
position: relative;
width: 100%;
height: 100%;
background-image:
linear-gradient(rgba(28, 43, 51, 0.05) 1px, transparent 1px),
linear-gradient(90deg, rgba(28, 43, 51, 0.05) 1px, transparent 1px);
background-size: 25px 25px;
background-position: center center;
}
.canvas-grid.dragging {
cursor: grabbing;
}
.design-frame {
position: absolute;
background-color: var(--color-white);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-card);
overflow: hidden;
transition: all 0.15s ease;
z-index: 10;
box-shadow: 0 2px 4px 0 rgba(0,0,0,0.1);
}
.design-frame:hover {
border-color: var(--color-meta-blue);
}
.design-frame.selected {
border-color: var(--color-meta-blue);
box-shadow: 0 0 0 2px rgba(0, 100, 224, 0.2);
}
.design-frame.dragging {
z-index: 100;
opacity: 0.9;
cursor: grabbing;
}
.frame-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--space-5) var(--space-7);
background-color: var(--color-soft-gray);
border-bottom: 1px solid var(--color-border-subtle);
}
.frame-title {
font-size: var(--font-size-body);
font-weight: var(--font-weight-medium);
color: var(--color-text-primary);
letter-spacing: var(--letter-spacing-normal);
font-family: var(--font-family-primary);
}
.frame-title h4 {
margin: 0;
font-size: var(--font-size-body);
font-weight: var(--font-weight-medium);
color: var(--color-text-primary);
letter-spacing: var(--letter-spacing-normal);
font-family: var(--font-family-primary);
font-feature-settings: "ss01", "ss02";
}
.frame-date {
font-family: var(--font-family-secondary);
font-size: var(--font-size-small);
color: var(--color-text-muted);
margin-top: var(--space-2);
letter-spacing: var(--letter-spacing-normal);
}
.frame-actions {
display: flex;
align-items: center;
gap: var(--space-4);
}
.viewport-toggle {
display: flex;
gap: var(--space-3);
}
.chat-actions {
display: flex;
gap: var(--space-3);
}
.action-btn {
padding: var(--space-2) var(--space-4);
border: 1px solid var(--color-border-subtle);
background-color: var(--color-soft-gray);
border-radius: var(--radius-pill);
cursor: pointer;
transition: background 200ms ease, transform 150ms ease;
color: var(--color-text-primary);
font-family: var(--font-family-primary);
font-size: var(--font-size-caption);
letter-spacing: var(--letter-spacing-tighter);
}
.action-btn:hover {
background-color: var(--color-button-hover);
color: var(--color-white);
border-color: var(--color-meta-blue);
transform: scale(1.05);
}
.action-btn:focus-visible {
outline: 3px ring var(--color-meta-blue);
outline: auto 2px var(--color-meta-blue);
outline-offset: 2px;
}
.frame-content {
width: 100%;
height: calc(100% - 50px);
position: relative;
}
.frame-placeholder {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: var(--color-soft-gray);
}
.placeholder-icon {
font-size: 48px;
margin-bottom: var(--space-5);
color: var(--color-text-muted);
}
.placeholder-name {
font-size: var(--font-size-body);
font-weight: var(--font-weight-medium);
color: var(--color-text-primary);
margin: 0 0 var(--space-2) 0;
letter-spacing: var(--letter-spacing-normal);
font-family: var(--font-family-primary);
}
.placeholder-meta {
display: flex;
gap: var(--space-5);
font-family: var(--font-family-secondary);
font-size: var(--font-size-small);
color: var(--color-text-muted);
letter-spacing: var(--letter-spacing-normal);
}
.placeholder-hint {
font-family: var(--font-family-secondary);
font-size: var(--font-size-small);
color: var(--color-text-muted);
margin-top: var(--space-3);
letter-spacing: var(--letter-spacing-normal);
}
.viewport-info {
font-family: var(--font-family-secondary);
font-size: var(--font-size-small);
color: var(--color-text-muted);
margin-top: var(--space-2);
letter-spacing: var(--letter-spacing-normal);
}
.selection-indicator {
position: absolute;
top: -2px;
left: -2px;
right: -2px;
bottom: -2px;
border: 2px solid var(--color-meta-blue);
border-radius: var(--radius-card);
pointer-events: none;
z-index: 10;
}
.floating-action-buttons {
position: absolute;
bottom: -50px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: var(--space-3);
z-index: 100;
}
.floating-action-btn {
display: flex;
align-items: center;
gap: var(--space-3);
padding: var(--space-4) var(--space-9);
background-color: var(--color-soft-gray);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-pill);
cursor: pointer;
transition: background 200ms ease, transform 150ms ease;
font-family: var(--font-family-primary);
font-size: var(--font-size-button);
font-weight: var(--font-weight-regular);
letter-spacing: var(--letter-spacing-tighter);
color: var(--color-text-primary);
box-shadow: 0 2px 4px 0 rgba(0,0,0,0.1);
}
.floating-action-btn:hover {
background-color: var(--color-button-hover);
color: var(--color-white);
border-color: var(--color-meta-blue);
transform: scale(1.05);
}
.floating-action-btn:focus-visible {
outline: 3px ring var(--color-meta-blue);
outline: auto 2px var(--color-meta-blue);
outline-offset: 2px;
}
.floating-action-btn.success {
background-color: var(--color-meta-blue);
border-color: var(--color-meta-blue);
color: var(--color-white);
}
.btn-icon {
width: 16px;
height: 16px;
flex-shrink: 0;
}
.btn-text {
font-family: var(--font-family-primary);
font-size: var(--font-size-button);
font-weight: var(--font-weight-regular);
letter-spacing: var(--letter-spacing-tighter);
white-space: nowrap;
}
.copy-prompt-dropdown {
position: relative;
}
.copy-prompt-main-btn {
display: flex;
align-items: center;
gap: var(--space-3);
}
.copy-prompt-main-btn .dropdown-arrow {
width: 12px;
height: 12px;
margin-left: var(--space-2);
}
.copy-dropdown-menu {
position: absolute;
top: 100%;
left: 0;
margin-top: var(--space-3);
background-color: var(--color-white);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-card);
min-width: 120px;
z-index: 101;
box-shadow: 0 2px 4px 0 rgba(0,0,0,0.1);
}
.copy-dropdown-item {
display: flex;
align-items: center;
width: 100%;
padding: var(--space-4) var(--space-7);
background: none;
border: none;
cursor: pointer;
transition: background-color 0.15s;
font-family: var(--font-family-primary);
font-size: var(--font-size-caption);
font-weight: var(--font-weight-regular);
letter-spacing: var(--letter-spacing-tighter);
color: var(--color-text-primary);
text-align: left;
}
.copy-dropdown-item:hover {
background-color: var(--color-soft-gray);
}
.copy-dropdown-item:focus-visible {
outline: 3px ring var(--color-meta-blue);
outline: auto 2px var(--color-meta-blue);
outline-offset: 2px;
}
.copy-dropdown-item:first-child {
border-radius: var(--radius-card) var(--radius-card) 0 0;
}
.copy-dropdown-item:last-child {
border-radius: 0 0 var(--radius-card) var(--radius-card);
}
.connection-lines {
position: absolute;
top: 0;
left: 0;
pointer-events: none;
z-index: 1;
}
.canvas-loading {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--color-white);
}
.loading-spinner {
text-align: center;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid var(--color-soft-gray);
border-top: 4px solid var(--color-meta-blue);
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto var(--space-5);
}
.spinner-small {
width: 20px;
height: 20px;
border: 3px solid var(--color-soft-gray);
border-top: 3px solid var(--color-meta-blue);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.canvas-error {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--color-white);
}
.error-message {
text-align: center;
padding: var(--space-9);
background-color: var(--color-white);
border-radius: var(--radius-card);
border: 1px solid var(--color-border-subtle);
box-shadow: 0 2px 4px 0 rgba(0,0,0,0.1);
}
.error-message h3 {
margin: 0 0 var(--space-5) 0;
font-size: var(--font-size-heading-3);
font-weight: var(--font-weight-bold);
color: var(--color-error-red);
letter-spacing: var(--letter-spacing-normal);
font-family: var(--font-family-primary);
font-feature-settings: "ss01", "ss02";
}
.error-message p {
margin: 0 0 var(--space-7) 0;
font-size: var(--font-size-body);
color: var(--color-text-secondary);
letter-spacing: var(--letter-spacing-normal);
font-family: var(--font-family-primary);
}
.error-message button {
padding: var(--space-4) var(--space-9);
background-color: var(--color-meta-blue);
color: var(--color-white);
border: none;
border-radius: var(--radius-pill);
cursor: pointer;
transition: background 200ms ease, transform 150ms ease;
font-family: var(--font-family-primary);
font-size: var(--font-size-button);
font-weight: var(--font-weight-regular);
letter-spacing: var(--letter-spacing-tighter);
}
.error-message button:hover {
background-color: var(--color-meta-blue-hover);
transform: scale(1.05);
}
.error-message button:focus-visible {
outline: 3px ring var(--color-meta-blue);
outline: auto 2px var(--color-meta-blue);
outline-offset: 2px;
}
.canvas-empty {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--color-white);
}
.empty-state {
text-align: center;
padding: var(--space-9);
background-color: var(--color-white);
border-radius: var(--radius-card);
border: 1px solid var(--color-border-subtle);
box-shadow: 0 2px 4px 0 rgba(0,0,0,0.1);
}
.empty-state h3 {
margin: 0 0 var(--space-5) 0;
font-size: var(--font-size-heading-3);
font-weight: var(--font-weight-bold);
color: var(--color-text-primary);
letter-spacing: var(--letter-spacing-normal);
font-family: var(--font-family-primary);
font-feature-settings: "ss01", "ss02";
}
.empty-state p {
margin: 0;
font-size: var(--font-size-body);
color: var(--color-text-secondary);
letter-spacing: var(--letter-spacing-normal);
font-family: var(--font-family-primary);
}
.empty-state code {
background-color: var(--color-soft-gray);
padding: var(--space-2) var(--space-3);
border-radius: var(--radius-small);
font-family: var(--font-family-secondary);
font-size: var(--font-size-small);
color: var(--color-text-primary);
letter-spacing: var(--letter-spacing-normal);
}
@media (max-width: 768px) {
.canvas-toolbar {
flex-direction: column;
align-items: flex-start;
gap: var(--space-5);
padding: var(--space-5);
}
.toolbar-section {
width: 100%;
justify-content: space-between;
}
.control-group {
gap: var(--space-3);
}
.toolbar-btn {
padding: var(--space-4) var(--space-6);
}
}
.frame-toolbar {
display: flex;
align-items: center;
gap: var(--space-3);
padding: var(--space-3) var(--space-5);
background-color: var(--color-white);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-card);
transition: all 0.15s ease;
position: absolute;
top: 10px;
left: 50%;
transform: translateX(-50%);
z-index: 1001;
box-shadow: 0 2px 4px 0 rgba(0,0,0,0.1);
}
.frame-toolbar:hover {
border-color: var(--color-meta-blue);
}
.frame-toolbar .toolbar-btn {
padding: var(--space-3);
border: 1px solid var(--color-border-subtle);
background-color: var(--color-soft-gray);
color: var(--color-text-primary);
border-radius: var(--radius-circle);
cursor: pointer;
transition: all 0.15s ease;
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
}
.frame-toolbar .toolbar-btn:hover {
background-color: var(--color-button-hover);
color: var(--color-white);
border-color: var(--color-meta-blue);
}
.frame-toolbar .toolbar-btn:focus-visible {
outline: 3px ring var(--color-meta-blue);
outline: auto 2px var(--color-meta-blue);
outline-offset: 2px;
}
.frame-toolbar .toolbar-btn .btn-icon {
width: 18px;
height: 18px;
}
.frame-iframe {
width: 100%;
height: 100%;
border: none;
background: var(--color-white);
border-radius: 0 0 var(--radius-small) var(--radius-small);
}
.frame-iframe-interactive {
pointer-events: auto;
}
.frame-iframe-disabled {
pointer-events: none;
}
.frame-html-content {
width: 100%;
height: 100%;
overflow: hidden;
background: var(--color-white);
border: 1px solid var(--color-meta-blue);
border-radius: 0 0 var(--radius-small) var(--radius-small);
display: flex;
align-items: center;
justify-content: center;
padding: var(--space-9);
box-sizing: border-box;
}
.connection-lines-svg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 1;
overflow: visible;
}
.frame-drag-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--color-overlay);
display: flex;
align-items: center;
justify-content: center;
z-index: 50;
}
.drag-ready-hint {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-3);
}
.drag-ready-hint span {
font-size: 24px;
color: var(--color-meta-blue);
}
.drag-ready-hint p {
font-family: var(--font-family-primary);
font-size: var(--font-size-caption);
color: var(--color-text-primary);
letter-spacing: var(--letter-spacing-tighter);
}
.frame-loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(28, 43, 51, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 40;
}
.frame-error-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--color-soft-gray);
display: flex;
align-items: center;
justify-content: center;
z-index: 40;
}
.frame-error-content {
text-align: center;
padding: var(--space-7);
}
.frame-error-content span {
font-size: 32px;
color: var(--color-error-red);
}
.frame-error-content p {
font-size: var(--font-size-body);
color: var(--color-text-primary);
margin: var(--space-3) 0 var(--space-2);
letter-spacing: var(--letter-spacing-normal);
font-family: var(--font-family-primary);
}
.frame-error-content small {
font-family: var(--font-family-secondary);
font-size: var(--font-size-small);
color: var(--color-text-muted);
letter-spacing: var(--letter-spacing-normal);
}
.frame-viewport-controls {
display: flex;
gap: var(--space-2);
}
.frame-viewport-btn {
padding: var(--space-2);
border: 1px solid var(--color-border-subtle);
background-color: var(--color-soft-gray);
border-radius: var(--radius-circle);
cursor: pointer;
transition: all 0.15s ease;
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
}
.frame-viewport-btn:hover {
background-color: var(--color-button-hover);
color: var(--color-white);
border-color: var(--color-meta-blue);
}
.frame-viewport-btn:focus-visible {
outline: 3px ring var(--color-meta-blue);
outline: auto 2px var(--color-meta-blue);
outline-offset: 2px;
}
.frame-viewport-btn.active {
background-color: var(--color-meta-blue);
border-color: var(--color-meta-blue);
color: var(--color-white);
}
.frame-viewport-indicator {
display: flex;
align-items: center;
gap: var(--space-2);
}
.global-indicator {
display: flex;
align-items: center;
}
.viewport-icon {
display: flex;
align-items: center;
}
.frame-meta {
display: flex;
align-items: center;
gap: var(--space-2);
}
.frame-status {
font-family: var(--font-family-secondary);
font-size: var(--font-size-small);
letter-spacing: var(--letter-spacing-normal);
}
.frame-status.loading {
color: var(--color-meta-blue);
}
.frame-status.error {
color: var(--color-error-red);
}
.frame-status.loaded {
color: var(--color-success-green);
}
.file-type {
padding: var(--space-1) var(--space-3);
background-color: var(--color-soft-gray);
border-radius: var(--radius-small);
font-family: var(--font-family-secondary);
font-size: var(--font-size-small);
font-weight: var(--font-weight-bold);
letter-spacing: var(--letter-spacing-normal);
color: var(--color-text-muted);
}

View File

@ -1,794 +0,0 @@
import { useRef, useEffect, useState, useCallback } from 'react'
import { TransformWrapper, TransformComponent, ReactZoomPanPinchRef } from 'react-zoom-pan-pinch'
import DesignFrame from '../../components/DesignFrame'
import ConnectionLines from '../../components/ConnectionLines'
import {
generateResponsiveConfig,
buildHierarchyTree,
calculateHierarchyPositions,
getHierarchicalPosition,
detectDesignRelationships
} from '../../utils/gridLayout'
import {
DesignFile,
CanvasConfig,
ViewportMode,
FrameViewportState,
FramePositionState,
DragState,
GridPosition,
LayoutMode,
HierarchyTree,
ConnectionLine
} from '../../types/canvas.types'
import {
HomeIcon,
RefreshIcon,
GlobeIcon,
MobileIcon,
TabletIcon,
DesktopIcon,
TreeIcon,
LinkIcon
} from '../../components/Icons'
import { useTranslation } from 'react-i18next'
import './CanvasArea.css'
const CANVAS_CONFIG: CanvasConfig = {
frameSize: { width: 320, height: 400 },
gridSpacing: 50,
framesPerRow: 4,
minZoom: 0.1,
maxZoom: 5,
responsive: {
enableScaling: true,
minFrameSize: { width: 160, height: 200 },
maxFrameSize: { width: 400, height: 500 },
scaleWithZoom: false
},
viewports: {
desktop: { width: 1000, height: 600 },
tablet: { width: 640, height: 800 },
mobile: { width: 320, height: 550 }
},
hierarchy: {
horizontalSpacing: 180,
verticalSpacing: 120,
connectionLineWidth: 2,
connectionLineColor: 'var(--vscode-textLink-foreground)',
showConnections: true
}
}
// 模拟设计文件用于演示
const MOCK_DESIGN_FILES: DesignFile[] = [
{
name: 'text_1.html',
path: '/designs/text_1.html',
modified: new Date(),
content: '<div style="padding: 20px; font-family: Arial, sans-serif;"><h1>Hello World</h1><p>This is a test design file.</p></div>',
fileType: 'html',
relationships: {
parent: undefined,
children: ['text_1_1.html']
}
},
{
name: 'text_1_1.html',
path: '/designs/text_1_1.html',
modified: new Date(),
content: '<div style="padding: 20px; font-family: Arial, sans-serif;"><h1>Hello World - Version 1</h1><p>This is a modified version.</p></div>',
fileType: 'html',
relationships: {
parent: 'text_1.html',
children: ['text_1_1_1.html']
}
},
{
name: 'text_1_1_1.html',
path: '/designs/text_1_1_1.html',
modified: new Date(),
content: '<div style="padding: 20px; font-family: Arial, sans-serif;"><h1>Hello World - Version 1.1</h1><p>This is another modified version.</p></div>',
fileType: 'html',
relationships: {
parent: 'text_1_1.html',
children: []
}
},
{
name: 'text_2.html',
path: '/designs/text_2.html',
modified: new Date(),
content: '<div style="padding: 20px; font-family: Arial, sans-serif;"><h1>Another Design</h1><p>This is a different design file.</p></div>',
fileType: 'html',
relationships: {
parent: undefined,
children: []
}
}
]
interface CanvasAreaProps {
onFrameSelect?: (frames: string[]) => void
designFiles?: DesignFile[]
setDesignFiles?: (files: DesignFile[]) => void
}
const CanvasArea: React.FC<CanvasAreaProps> = ({ onFrameSelect, designFiles: externalDesignFiles }) => {
const { t } = useTranslation()
const [designFiles, setDesignFiles] = useState<DesignFile[]>([])
const [selectedFrames, setSelectedFrames] = useState<string[]>([])
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [currentZoom, setCurrentZoom] = useState(1)
const [currentConfig, setCurrentConfig] = useState<CanvasConfig>(CANVAS_CONFIG)
const [globalViewportMode, setGlobalViewportMode] = useState<ViewportMode>('tablet')
const [frameViewports, setFrameViewports] = useState<FrameViewportState>({})
const [useGlobalViewport, setUseGlobalViewport] = useState(true)
const [customPositions, setCustomPositions] = useState<FramePositionState>({})
const [dragState, setDragState] = useState<DragState>({
isDragging: false,
draggedFrame: null,
startPosition: { x: 0, y: 0 },
currentPosition: { x: 0, y: 0 },
offset: { x: 0, y: 0 }
})
const [layoutMode, setLayoutMode] = useState<LayoutMode>('grid')
const [hierarchyTree, setHierarchyTree] = useState<HierarchyTree | null>(null)
const [showConnections, setShowConnections] = useState(true)
const transformRef = useRef<ReactZoomPanPinchRef>(null)
const canvasContainerRef = useRef<HTMLDivElement>(null)
// 性能优化:根据缩放级别切换渲染模式
const getOptimalRenderMode = useCallback((zoom: number): 'placeholder' | 'iframe' => {
return zoom < 0.3 ? 'placeholder' : 'iframe'
}, [])
// 辅助函数:将鼠标坐标转换为画布空间坐标
const transformMouseToCanvasSpace = useCallback((clientX: number, clientY: number, canvasRect: DOMRect): GridPosition => {
const currentScale = 1 // 当前简化处理
const currentTranslateX = 0 // 当前简化处理
const currentTranslateY = 0 // 当前简化处理
const rawMouseX = clientX - canvasRect.left
const rawMouseY = clientY - canvasRect.top
return {
x: (rawMouseX - currentTranslateX) / currentScale,
y: (rawMouseY - currentTranslateY) / currentScale
}
}, [])
// 视口管理函数
const getFrameViewport = useCallback((fileName: string): ViewportMode => {
if (useGlobalViewport) {
return globalViewportMode
}
return frameViewports[fileName] || 'desktop'
}, [useGlobalViewport, globalViewportMode, frameViewports])
const handleFrameViewportChange = useCallback((fileName: string, viewport: ViewportMode) => {
setFrameViewports((prev: FrameViewportState) => ({
...prev,
[fileName]: viewport
}))
}, [])
const handleGlobalViewportChange = useCallback((viewport: ViewportMode) => {
setGlobalViewportMode(viewport)
if (useGlobalViewport) {
const newFrameViewports: FrameViewportState = {}
designFiles.forEach(f => {
newFrameViewports[f.name] = viewport
})
setFrameViewports(newFrameViewports)
if (hierarchyTree && designFiles.length > 0) {
let totalWidth = 0
let totalHeight = 0
let frameCount = 0
designFiles.forEach(() => {
const viewportDimensions = currentConfig.viewports[viewport]
totalWidth += viewportDimensions.width
totalHeight += viewportDimensions.height + 50
frameCount++
})
const avgFrameDimensions = frameCount > 0 ? {
width: Math.round(totalWidth / frameCount),
height: Math.round(totalHeight / frameCount)
} : { width: 400, height: 550 }
const updatedTree = calculateHierarchyPositions(hierarchyTree, currentConfig, avgFrameDimensions)
setHierarchyTree(updatedTree)
// 视口切换后重新计算中心和缩放
setTimeout(() => {
if (transformRef.current) {
transformRef.current.resetTransform()
}
}, 100)
}
}
}, [useGlobalViewport, designFiles, currentConfig, hierarchyTree])
const toggleGlobalViewport = useCallback(() => {
const newUseGlobal = !useGlobalViewport
setUseGlobalViewport(newUseGlobal)
if (newUseGlobal) {
const newFrameViewports: FrameViewportState = {}
designFiles.forEach(file => {
newFrameViewports[file.name] = globalViewportMode
})
setFrameViewports(newFrameViewports)
}
}, [useGlobalViewport, designFiles, globalViewportMode])
// 响应式配置更新
useEffect(() => {
const updateConfig = () => {
const responsive = generateResponsiveConfig(CANVAS_CONFIG, window.innerWidth)
setCurrentConfig(responsive)
}
updateConfig()
window.addEventListener('resize', updateConfig)
return () => window.removeEventListener('resize', updateConfig)
}, [])
// 加载模拟设计文件
useEffect(() => {
const loadDesignFiles = async () => {
try {
setIsLoading(true)
setError(null)
let files: DesignFile[]
let tree: HierarchyTree
// 如果有外部传入的设计文件,使用外部文件
if (externalDesignFiles && externalDesignFiles.length > 0) {
files = externalDesignFiles
tree = buildHierarchyTree(externalDesignFiles)
} else {
// 模拟加载延迟
await new Promise(resolve => setTimeout(resolve, 500))
const filesWithRelationships = detectDesignRelationships(MOCK_DESIGN_FILES)
files = filesWithRelationships
tree = buildHierarchyTree(filesWithRelationships)
}
setDesignFiles(files)
setHierarchyTree(tree)
let totalWidth = 0
let totalHeight = 0
let frameCount = 0
files.forEach(file => {
const frameViewport = getFrameViewport(file.name)
const viewportDimensions = currentConfig.viewports[frameViewport]
totalWidth += viewportDimensions.width
totalHeight += viewportDimensions.height + 50
frameCount++
})
const avgFrameDimensions = frameCount > 0 ? {
width: Math.round(totalWidth / frameCount),
height: Math.round(totalHeight / frameCount)
} : { width: 400, height: 550 }
const positionedTree = calculateHierarchyPositions(tree, currentConfig, avgFrameDimensions)
setHierarchyTree(positionedTree)
setIsLoading(false)
// 计算合适的初始缩放和位置以居中内容
setTimeout(() => {
if (transformRef.current && canvasContainerRef.current && positionedTree) {
// 获取容器尺寸
const containerRect = canvasContainerRef.current.getBoundingClientRect()
const containerWidth = containerRect.width
const containerHeight = containerRect.height - 50 // 减去工具栏高度
// 根据布局模式获取内容边界
let contentWidth, contentHeight, contentCenterX, contentCenterY
if (layoutMode === 'hierarchy') {
// 层级模式:使用树边界
contentWidth = positionedTree.bounds.maxX - positionedTree.bounds.minX + 200 // 添加内边距
contentHeight = positionedTree.bounds.maxY - positionedTree.bounds.minY + 200 // 添加内边距
contentCenterX = (positionedTree.bounds.minX + positionedTree.bounds.maxX) / 2
contentCenterY = (positionedTree.bounds.minY + positionedTree.bounds.maxY) / 2
} else {
// 网格模式:计算内容边界
let maxX = 0
let maxY = 0
files.forEach((file, index) => {
const position = getFramePosition(file.name, index)
const viewportMode = getFrameViewport(file.name)
const viewportDimensions = currentConfig.viewports[viewportMode]
const frameMaxX = position.x + viewportDimensions.width
const frameMaxY = position.y + viewportDimensions.height + 50
if (frameMaxX > maxX) maxX = frameMaxX
if (frameMaxY > maxY) maxY = frameMaxY
})
contentWidth = maxX + 200 // 添加内边距
contentHeight = maxY + 200 // 添加内边距
contentCenterX = contentWidth / 2
contentCenterY = contentHeight / 2
}
// 计算最佳缩放级别
const scaleX = containerWidth / contentWidth
const scaleY = containerHeight / contentHeight
const optimalScale = Math.min(scaleX, scaleY, 1) // 最大缩放为1
// 计算中心位置
const translateX = (containerWidth / 2) - (contentCenterX * optimalScale)
const translateY = (containerHeight / 2) - (contentCenterY * optimalScale)
// 应用变换
transformRef.current.setTransform(translateX, translateY, optimalScale)
}
}, 100)
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Failed to load design files'
setError(errorMessage)
setIsLoading(false)
}
}
loadDesignFiles()
}, [currentConfig, getFrameViewport, externalDesignFiles, layoutMode])
const handleFrameSelect = useCallback((fileName: string) => {
const newSelectedFrames = [fileName]
setSelectedFrames(newSelectedFrames)
if (onFrameSelect) {
onFrameSelect(newSelectedFrames)
}
}, [onFrameSelect])
const handleSendToChat = useCallback((fileName: string, prompt: string) => {
console.log('Sending to chat:', { fileName, prompt })
}, [])
// 画布控制函数
const handleZoomIn = useCallback(() => {
if (transformRef.current) {
transformRef.current.zoomIn(0.05)
}
}, [])
const handleZoomOut = useCallback(() => {
if (transformRef.current) {
transformRef.current.zoomOut(0.05)
}
}, [])
const handleResetZoom = useCallback(() => {
if (transformRef.current) {
transformRef.current.resetTransform()
}
}, [])
const handleTransformChange = useCallback((ref: any) => {
const state = ref.state
if (state.scale <= 0) {
ref.setTransform(state.positionX, state.positionY, 0.1)
return
}
setCurrentZoom(state.scale)
}, [])
// 获取帧位置(自定义、层级或默认网格位置)
const getFramePosition = useCallback((fileName: string, index: number): GridPosition => {
if (customPositions[fileName]) {
return customPositions[fileName]
}
if (layoutMode === 'hierarchy' && hierarchyTree) {
return getHierarchicalPosition(fileName, hierarchyTree)
}
const viewportMode = getFrameViewport(fileName)
const viewportDimensions = currentConfig.viewports[viewportMode]
const actualWidth = viewportDimensions.width
const actualHeight = viewportDimensions.height + 50
const col = index % currentConfig.framesPerRow
const row = Math.floor(index / currentConfig.framesPerRow)
const x = col * (Math.max(actualWidth, currentConfig.frameSize.width) + currentConfig.gridSpacing)
const y = row * (Math.max(actualHeight, currentConfig.frameSize.height) + currentConfig.gridSpacing)
return { x, y }
}, [customPositions, layoutMode, hierarchyTree, getFrameViewport, currentConfig])
// 拖拽处理
const handleDragStart = useCallback((fileName: string, startPos: GridPosition, mouseEvent: React.MouseEvent) => {
const canvasGrid = document.querySelector('.canvas-grid') as HTMLElement
if (!canvasGrid) return
const canvasRect = canvasGrid.getBoundingClientRect()
const canvasMousePos = transformMouseToCanvasSpace(mouseEvent.clientX, mouseEvent.clientY, canvasRect)
if (!selectedFrames.includes(fileName)) {
setSelectedFrames([fileName])
}
setDragState({
isDragging: true,
draggedFrame: fileName,
startPosition: startPos,
currentPosition: startPos,
offset: {
x: canvasMousePos.x - startPos.x,
y: canvasMousePos.y - startPos.y
}
})
}, [transformMouseToCanvasSpace, selectedFrames])
const handleDragMove = useCallback((mousePos: GridPosition) => {
if (!dragState.isDragging || !dragState.draggedFrame) return
const newPosition = {
x: mousePos.x - dragState.offset.x,
y: mousePos.y - dragState.offset.y
}
setDragState((prev: DragState) => ({
...prev,
currentPosition: newPosition
}))
}, [dragState])
const handleDragEnd = useCallback(() => {
if (!dragState.isDragging || !dragState.draggedFrame) return
const gridSize = 25
const snappedPosition = {
x: Math.round(dragState.currentPosition.x / gridSize) * gridSize,
y: Math.round(dragState.currentPosition.y / gridSize) * gridSize
}
setCustomPositions((prev: FramePositionState) => ({
...prev,
[dragState.draggedFrame!]: snappedPosition
}))
setDragState({
isDragging: false,
draggedFrame: null,
startPosition: { x: 0, y: 0 },
currentPosition: { x: 0, y: 0 },
offset: { x: 0, y: 0 }
})
}, [dragState])
// 重置位置到网格
const handleResetPositions = useCallback(() => {
setCustomPositions({})
}, [])
// 根据当前帧位置更新连接线位置
const updateConnectionPositions = useCallback((connections: ConnectionLine[], files: DesignFile[]): ConnectionLine[] => {
return connections.map(connection => {
const fromIndex = files.findIndex(f => f.name === connection.fromFrame)
const toIndex = files.findIndex(f => f.name === connection.toFrame)
if (fromIndex === -1 || toIndex === -1) {
return connection
}
const fromPosition = getFramePosition(connection.fromFrame, fromIndex)
const toPosition = getFramePosition(connection.toFrame, toIndex)
const fromViewport = getFrameViewport(connection.fromFrame)
const toViewport = getFrameViewport(connection.toFrame)
const fromDimensions = currentConfig.viewports[fromViewport]
const toDimensions = currentConfig.viewports[toViewport]
const fromConnectionPoint = {
x: fromPosition.x + fromDimensions.width,
y: fromPosition.y + (fromDimensions.height + 50) / 2
}
const toConnectionPoint = {
x: toPosition.x,
y: toPosition.y + (toDimensions.height + 50) / 2
}
return {
...connection,
fromPosition: fromConnectionPoint,
toPosition: toConnectionPoint
}
})
}, [getFramePosition, getFrameViewport, currentConfig])
// 缩放键盘快捷键
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && !e.shiftKey) {
switch (e.key) {
case '=':
case '+':
e.preventDefault()
handleZoomIn()
break
case '-':
e.preventDefault()
handleZoomOut()
break
case '0':
e.preventDefault()
handleResetZoom()
break
}
}
}
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
}, [handleZoomIn, handleZoomOut, handleResetZoom])
// 处理窗口大小调整以确保画布尺寸正确更新
useEffect(() => {
const handleResize = () => {
// 窗口大小调整后计算合适的缩放和位置
if (transformRef.current) {
transformRef.current.resetTransform()
}
}
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [])
if (isLoading) {
return (
<div className="canvas-loading">
<div className="loading-spinner">
<div className="spinner"></div>
<p>{t('canvasArea.loading')}</p>
</div>
</div>
)
}
if (error) {
return (
<div className="canvas-error">
<div className="error-message">
<h3>{t('canvasArea.error.title')}</h3>
<p>{error}</p>
<button onClick={() => window.location.reload()}>
{t('canvasArea.error.retry')}
</button>
</div>
</div>
)
}
if (designFiles.length === 0) {
return (
<div className="canvas-empty">
<div className="empty-state">
<h3>{t('canvasArea.empty.title')}</h3>
<p>{t('canvasArea.empty.message')}</p>
</div>
</div>
)
}
return (
<div className="canvas-area" ref={canvasContainerRef}>
<div className="canvas-toolbar">
<div className="toolbar-section">
<div className="control-group">
{/* <button className="toolbar-btn zoom-btn" onClick={handleZoomOut} title={t('canvasArea.toolbar.zoomOut')}>
<ZoomOutIcon />
</button> */}
<div className="zoom-display">
<span className="zoom-value">{Math.round(currentZoom * 100)}%</span>
</div>
{/* <button className="toolbar-btn zoom-btn" onClick={handleZoomIn} title={t('canvasArea.toolbar.zoomIn')}>
<ZoomInIcon />
</button> */}
<div className="toolbar-divider"></div>
<button className="toolbar-btn" onClick={handleResetZoom} title={t('canvasArea.toolbar.resetZoom')}>
<HomeIcon />
</button>
<button className="toolbar-btn" onClick={handleResetPositions} title={t('canvasArea.toolbar.resetPositions')}>
<RefreshIcon />
</button>
</div>
</div>
<div className="toolbar-section">
<div className="control-group">
<div className="layout-toggle">
<button
className={`toggle-btn ${layoutMode === 'grid' ? 'active' : ''}`}
onClick={() => setLayoutMode('grid')}
title={t('canvasArea.toolbar.gridLayout')}
>
<span>{t('canvasArea.toolbar.grid')}</span>
</button>
<button
className={`toggle-btn ${layoutMode === 'hierarchy' ? 'active' : ''}`}
onClick={() => setLayoutMode('hierarchy')}
title={t('canvasArea.toolbar.hierarchyLayout')}
disabled={!hierarchyTree || hierarchyTree.nodes.size === 0}
>
<TreeIcon />
</button>
</div>
{layoutMode === 'hierarchy' && (
<button
className={`toolbar-btn connection-btn ${showConnections ? 'active' : ''}`}
onClick={() => setShowConnections(!showConnections)}
title={t('canvasArea.toolbar.toggleConnections')}
>
<LinkIcon />
</button>
)}
</div>
</div>
<div className="toolbar-section">
<div className="control-group">
<button
className={`toolbar-btn viewport-mode-btn ${useGlobalViewport ? 'active' : ''}`}
onClick={toggleGlobalViewport}
title={t('canvasArea.toolbar.toggleGlobalViewport')}
>
<GlobeIcon />
</button>
<div className="viewport-selector">
<button
className={`viewport-btn ${globalViewportMode === 'mobile' && useGlobalViewport ? 'active' : ''}`}
onClick={() => handleGlobalViewportChange('mobile')}
title={t('canvasArea.toolbar.mobileView')}
disabled={!useGlobalViewport}
>
<MobileIcon />
</button>
<button
className={`viewport-btn ${globalViewportMode === 'tablet' && useGlobalViewport ? 'active' : ''}`}
onClick={() => handleGlobalViewportChange('tablet')}
title={t('canvasArea.toolbar.tabletView')}
disabled={!useGlobalViewport}
>
<TabletIcon />
</button>
<button
className={`viewport-btn ${globalViewportMode === 'desktop' && useGlobalViewport ? 'active' : ''}`}
onClick={() => handleGlobalViewportChange('desktop')}
title={t('canvasArea.toolbar.desktopView')}
disabled={!useGlobalViewport}
>
<DesktopIcon />
</button>
</div>
</div>
</div>
</div>
<TransformWrapper
ref={transformRef}
initialScale={1}
minScale={0.1}
maxScale={3}
limitToBounds={false}
smooth={false}
disablePadding={true}
doubleClick={{
disabled: false,
mode: "zoomIn",
step: 50,
animationTime: 150
}}
wheel={{
wheelDisabled: true,
touchPadDisabled: false,
step: 0.05
}}
panning={{
disabled: dragState.isDragging,
velocityDisabled: true
}}
pinch={{
disabled: false,
step: 1
}}
centerOnInit={true}
onTransform={(ref) => handleTransformChange(ref)}
>
<TransformComponent
wrapperClass="canvas-transform-wrapper"
contentClass="canvas-transform-content"
>
<div
className={`canvas-grid ${dragState.isDragging ? 'dragging' : ''}`}
onMouseMove={(e) => {
if (dragState.isDragging) {
const rect = e.currentTarget.getBoundingClientRect()
const mousePos = transformMouseToCanvasSpace(e.clientX, e.clientY, rect)
handleDragMove(mousePos)
}
}}
onMouseUp={handleDragEnd}
onMouseLeave={handleDragEnd}
onClick={(e) => {
if (e.target === e.currentTarget) {
setSelectedFrames([])
if (onFrameSelect) {
onFrameSelect([])
}
}
}}
>
{layoutMode === 'hierarchy' && hierarchyTree && showConnections && (
<ConnectionLines
connections={updateConnectionPositions(hierarchyTree.connections, designFiles)}
containerBounds={{
width: hierarchyTree.bounds.maxX - hierarchyTree.bounds.minX,
height: hierarchyTree.bounds.maxY - hierarchyTree.bounds.minY
}}
isVisible={showConnections}
zoomLevel={currentZoom}
/>
)}
{designFiles.map((file, index) => {
const frameViewport = getFrameViewport(file.name)
const viewportDimensions = currentConfig.viewports[frameViewport]
const actualWidth = viewportDimensions.width
const actualHeight = viewportDimensions.height + 50
const position = getFramePosition(file.name, index)
const finalPosition = dragState.isDragging && dragState.draggedFrame === file.name
? dragState.currentPosition
: position
return (
<DesignFrame
key={file.name}
file={file}
position={finalPosition}
dimensions={{ width: actualWidth, height: actualHeight }}
isSelected={selectedFrames.includes(file.name)}
onSelect={handleFrameSelect}
renderMode={getOptimalRenderMode(currentZoom)}
viewport={frameViewport}
viewportDimensions={viewportDimensions}
onViewportChange={handleFrameViewportChange}
useGlobalViewport={useGlobalViewport}
onDragStart={handleDragStart}
isDragging={dragState.isDragging && dragState.draggedFrame === file.name}
nonce={null}
onSendToChat={handleSendToChat}
/>
)
})}
</div>
</TransformComponent>
</TransformWrapper>
</div>
)
}
export default CanvasArea

View File

@ -1,106 +0,0 @@
import { useState } from 'react'
import CanvasArea from './CanvasArea'
import TopBar from '../../components/TopBar'
import SideBar from '../../components/SideBar'
import AIChatPanel from '../../components/AIChatPanel'
import '../../App.css'
// 模拟设计文件数据
const MOCK_DESIGN_FILES = [
{
name: 'text_1.html',
path: '/designs/text_1.html',
modified: new Date(),
content: '<div style="padding: 20px; font-family: Arial, sans-serif;"><h1>Hello World</h1><p>This is a test design file.</p></div>',
fileType: 'html',
relationships: {
parent: undefined,
children: ['text_1_1.html']
}
},
{
name: 'text_1_1.html',
path: '/designs/text_1_1.html',
modified: new Date(),
content: '<div style="padding: 20px; font-family: Arial, sans-serif;"><h1>Hello World - Version 1</h1><p>This is a modified version.</p></div>',
fileType: 'html',
relationships: {
parent: 'text_1.html',
children: ['text_1_1_1.html']
}
},
{
name: 'text_1_1_1.html',
path: '/designs/text_1_1_1.html',
modified: new Date(),
content: '<div style="padding: 20px; font-family: Arial, sans-serif;"><h1>Hello World - Version 1.1</h1><p>This is another modified version.</p></div>',
fileType: 'html',
relationships: {
parent: 'text_1_1.html',
children: []
}
},
{
name: 'text_2.html',
path: '/designs/text_2.html',
modified: new Date(),
content: '<div style="padding: 20px; font-family: Arial, sans-serif;"><h1>Another Design</h1><p>This is a different design file.</p></div>',
fileType: 'html',
relationships: {
parent: undefined,
children: []
}
}
]
// 主应用组件
const MainApp: React.FC = () => {
const [isChatVisible, setIsChatVisible] = useState(true)
const [selectedFrames, setSelectedFrames] = useState<string[]>([])
const [designFiles, setDesignFiles] = useState<any[]>(MOCK_DESIGN_FILES)
const toggleChat = () => {
setIsChatVisible(!isChatVisible)
}
const handleFrameSelect = (frames: string[]) => {
setSelectedFrames(frames)
}
const handleSelectNote = (frameName: string, note: string) => {
console.log('添加备注到设计元素:', frameName, note)
}
const handleCreateContext = (title: string, description: string) => {
console.log('创建上下文:', { title, description })
const newFrame = {
name: title,
path: `/context/${title}`,
modified: new Date(),
content: description,
fileType: 'html' as const,
size: description.length,
version: '1.0',
generation: 0,
branchIndex: 0,
parentDesign: null,
children: [],
relationships: {
parent: undefined,
children: []
}
}
setDesignFiles([...designFiles, newFrame])
}
return (
<div className="app-container">
<CanvasArea onFrameSelect={handleFrameSelect} designFiles={designFiles} setDesignFiles={setDesignFiles} />
<TopBar onToggleChat={toggleChat} isChatVisible={isChatVisible} />
<SideBar selectedFrames={selectedFrames} onSelectNote={handleSelectNote} onCreateContext={handleCreateContext} />
{isChatVisible && <AIChatPanel />}
</div>
)
}
export default MainApp

View File

@ -1,129 +0,0 @@
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import '../auth/Auth.css';
interface Project {
id: string;
name: string;
description: string;
createdAt: string;
updatedAt: string;
}
const Projects: React.FC = () => {
const { t } = useTranslation();
const [projects, setProjects] = useState<Project[]>([]);
const [loading, setLoading] = useState(true);
const navigate = useNavigate();
// 检查登录状态
useEffect(() => {
const isLoggedIn = localStorage.getItem('isLoggedIn');
if (!isLoggedIn) {
navigate('/login');
}
}, [navigate]);
// 模拟加载项目数据
useEffect(() => {
const loadProjects = async () => {
try {
// 预留后台接口调用
// const response = await fetch('/api/projects');
// const data = await response.json();
// setProjects(data);
// 模拟项目数据
const mockProjects: Project[] = [
{
id: '1',
name: 'UI设计项目1',
description: '这是一个UI设计项目',
createdAt: '2026-04-01',
updatedAt: '2026-04-20'
},
{
id: '2',
name: '网站重构项目',
description: '网站界面重构项目',
createdAt: '2026-04-05',
updatedAt: '2026-04-18'
},
{
id: '3',
name: '移动应用UI',
description: '移动应用界面设计',
createdAt: '2026-04-10',
updatedAt: '2026-04-22'
}
];
setTimeout(() => {
setProjects(mockProjects);
setLoading(false);
}, 1000);
} catch (error) {
console.error('加载项目失败:', error);
setLoading(false);
}
};
loadProjects();
}, []);
const handleLogout = () => {
// 清除登录状态
localStorage.removeItem('isLoggedIn');
localStorage.removeItem('username');
// 跳转到登录页面
navigate('/login');
};
if (loading) {
return (
<div className="projects-container">
<div className="projects-loading">{t('projects.loading')}</div>
</div>
);
}
const handleProjectClick = (projectId: string) => {
// 跳转到canvas页面并传递项目ID
navigate(`/app?projectId=${projectId}`);
};
return (
<div className="projects-container">
<div className="projects-header">
<h2>{t('projects.title')}</h2>
<button className="logout-button" onClick={handleLogout}>
{t('projects.logout')}
</button>
</div>
<div className="projects-list">
{projects.length === 0 ? (
<div className="projects-empty">{t('projects.empty')}</div>
) : (
projects.map((project) => (
<div
key={project.id}
className="project-card"
onClick={() => handleProjectClick(project.id)}
style={{ cursor: 'pointer' }}
>
<h3>{project.name}</h3>
<p>{project.description}</p>
<div className="project-meta">
<span>{t('projects.meta.createdAt')} {project.createdAt}</span>
<span>{t('projects.meta.updatedAt')} {project.updatedAt}</span>
</div>
</div>
))
)}
</div>
</div>
);
};
export default Projects;

View File

@ -1,225 +0,0 @@
/* 登录和注册页面样式 */
.auth-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background-color: #f5f5f5;
padding: 20px;
}
.auth-card {
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: 30px;
width: 100%;
max-width: 400px;
}
.auth-card h2 {
margin-top: 0;
margin-bottom: 20px;
color: #333;
text-align: center;
}
.auth-error {
background-color: #ffebee;
color: #c62828;
padding: 10px;
border-radius: 4px;
margin-bottom: 20px;
font-size: 14px;
}
.auth-form {
display: flex;
flex-direction: column;
gap: 15px;
}
.form-group {
display: flex;
flex-direction: column;
gap: 5px;
}
.form-group label {
font-size: 14px;
color: #555;
}
.form-group input {
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
}
.form-group input:focus {
outline: none;
border-color: #64b5f6;
box-shadow: 0 0 0 2px rgba(100, 181, 246, 0.2);
}
.auth-button {
background-color: #2196f3;
color: white;
border: none;
padding: 12px;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
transition: background-color 0.3s;
}
.auth-button:hover {
background-color: #1976d2;
}
.auth-button:disabled {
background-color: #bdbdbd;
cursor: not-allowed;
}
.auth-buttons {
display: flex;
gap: 10px;
}
.auth-buttons .auth-button {
flex: 1;
}
.auth-button.cancel {
background-color: #9e9e9e;
}
.auth-button.cancel:hover {
background-color: #757575;
}
.auth-link {
margin-top: 20px;
text-align: center;
font-size: 14px;
color: #666;
}
.auth-link a {
color: #2196f3;
text-decoration: none;
}
.auth-link a:hover {
text-decoration: underline;
}
/* 项目列表页面样式 */
.projects-container {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}
.projects-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
}
.projects-header h2 {
margin: 0;
color: #333;
}
.logout-button {
background-color: #f44336;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: background-color 0.3s;
}
.logout-button:hover {
background-color: #d32f2f;
}
.projects-loading {
text-align: center;
padding: 50px 0;
font-size: 16px;
color: #666;
}
.projects-empty {
text-align: center;
padding: 50px 0;
font-size: 16px;
color: #666;
}
.projects-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
}
.project-card {
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: 20px;
transition: transform 0.3s, box-shadow 0.3s;
}
.project-card:hover {
transform: translateY(-5px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
.project-card h3 {
margin-top: 0;
margin-bottom: 10px;
color: #333;
}
.project-card p {
margin-bottom: 15px;
color: #666;
font-size: 14px;
}
.project-meta {
display: flex;
flex-direction: column;
gap: 5px;
font-size: 12px;
color: #999;
}
/* 响应式设计 */
@media (max-width: 768px) {
.auth-card {
padding: 20px;
}
.projects-container {
padding: 10px;
}
.projects-header {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.projects-list {
grid-template-columns: 1fr;
}
}

View File

@ -0,0 +1,181 @@
import React, { useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import Input from '../../components/ui/Input';
import Button from '../../components/ui/Button';
interface ForgotPasswordFormData {
username: string;
password: string;
confirmPassword: string;
}
const ForgotPassword: React.FC = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const [formData, setFormData] = useState<ForgotPasswordFormData>({
username: '',
password: '',
confirmPassword: '',
});
const [errors, setErrors] = useState<Partial<ForgotPasswordFormData>>({});
const [loading, setLoading] = useState(false);
const [errorMessage, setErrorMessage] = useState('');
const [successMessage, setSuccessMessage] = useState('');
const validateForm = (): boolean => {
const newErrors: Partial<ForgotPasswordFormData> = {};
if (!formData.username) {
newErrors.username = t('forgotPassword.usernameRequired');
}
if (!formData.password) {
newErrors.password = t('forgotPassword.passwordRequired');
} else if (formData.password.length < 6) {
newErrors.password = t('forgotPassword.passwordMinLength');
} else if (!/^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{6,}$/.test(formData.password)) {
newErrors.password = t('forgotPassword.passwordFormat');
}
if (!formData.confirmPassword) {
newErrors.confirmPassword = t('forgotPassword.confirmPasswordRequired');
} else if (formData.confirmPassword !== formData.password) {
newErrors.confirmPassword = t('forgotPassword.confirmPasswordMismatch');
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!validateForm()) {
return;
}
setLoading(true);
setErrorMessage('');
setSuccessMessage('');
// 模拟重置密码请求
try {
await new Promise((resolve) => setTimeout(resolve, 1000));
// 重置密码成功
setSuccessMessage(t('forgotPassword.successMessage'));
// 3秒后跳转到登录页
setTimeout(() => {
navigate('/login');
}, 3000);
} catch (error) {
setErrorMessage(t('forgotPassword.errorMessage'));
} finally {
setLoading(false);
}
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData({
...formData,
[name]: value,
});
// 清除对应字段的错误信息
if (errors[name as keyof ForgotPasswordFormData]) {
setErrors({
...errors,
[name]: undefined,
});
}
};
const handleCancel = () => {
navigate('/login');
};
return (
<div className="min-h-screen flex items-center justify-center bg-soft-gray">
<div className="max-w-md w-full space-y-8 p-8 bg-white rounded-[20px] shadow-[0_12px_28px_0_rgba(0,0,0,0.2),0_2px_4px_0_rgba(0,0,0,0.1)] transition-all duration-300 hover:translate-y-[-2px] hover:shadow-[0_16px_32px_0_rgba(0,0,0,0.25),0_4px_8px_0_rgba(0,0,0,0.15)]">
<div className="text-center">
<h1 className="text-3xl font-medium text-dark-charcoal font-optimistic">{t('forgotPassword.title')}</h1>
<p className="mt-2 text-sm text-slate-gray font-optimistic">{t('forgotPassword.prompt')}</p>
</div>
{errorMessage && (
<div className="p-3 bg-error-bg text-error-red rounded-[8px]">
{errorMessage}
</div>
)}
{successMessage && (
<div className="p-3 bg-positive-bg text-success-green rounded-[8px]">
{successMessage}
</div>
)}
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
<Input
type="text"
label={t('forgotPassword.username')}
placeholder={t('forgotPassword.placeholderUsername')}
value={formData.username}
onChange={handleInputChange}
error={errors.username}
required
name="username"
/>
<Input
type="password"
label={t('forgotPassword.newPassword')}
placeholder={t('forgotPassword.placeholderPassword')}
value={formData.password}
onChange={handleInputChange}
error={errors.password}
required
name="password"
/>
<Input
type="password"
label={t('forgotPassword.confirmPassword')}
placeholder={t('forgotPassword.placeholderConfirmPassword')}
value={formData.confirmPassword}
onChange={handleInputChange}
error={errors.confirmPassword}
required
name="confirmPassword"
/>
<div className="flex space-x-4">
<Button
type="secondary"
onClick={handleCancel}
fullWidth
>
{t('forgotPassword.cancel')}
</Button>
<Button
type="primary"
onClick={() => handleSubmit({ preventDefault: () => {} } as React.FormEvent)}
loading={loading}
fullWidth
>
{t('forgotPassword.resetPassword')}
</Button>
</div>
<div className="text-center text-sm">
<Link to="/login" className="font-medium text-meta-blue hover:text-meta-blue-hover transition-colors duration-200">
{t('forgotPassword.backToLogin')}
</Link>
</div>
</form>
</div>
</div>
);
};
export default ForgotPassword;

View File

@ -1,82 +1,149 @@
import React, { useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import './Auth.css';
import Input from '../../components/ui/Input';
import Button from '../../components/ui/Button';
import Checkbox from '../../components/ui/Checkbox';
interface LoginFormData {
username: string;
password: string;
rememberMe: boolean;
}
const Login: React.FC = () => {
const { t } = useTranslation();
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const navigate = useNavigate();
const [formData, setFormData] = useState<LoginFormData>({
username: '',
password: '',
rememberMe: false,
});
const [errors, setErrors] = useState<Partial<LoginFormData>>({});
const [loading, setLoading] = useState(false);
const [errorMessage, setErrorMessage] = useState('');
const handleLogin = async (e: React.FormEvent) => {
const validateForm = (): boolean => {
const newErrors: Partial<LoginFormData> = {};
if (!formData.username) {
newErrors.username = t('login.usernameRequired');
}
if (!formData.password) {
newErrors.password = t('login.passwordRequired');
} else if (formData.password.length < 6) {
newErrors.password = t('login.passwordMinLength');
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
setLoading(true);
if (!validateForm()) {
return;
}
setLoading(true);
setErrorMessage('');
// 模拟登录请求
try {
// 模拟加密密码
// const encryptedPassword = btoa(password);
// 预留后台接口调用
// const response = await fetch('/api/login', {
// method: 'POST',
// headers: {
// 'Content-Type': 'application/json',
// },
// body: JSON.stringify({ username, password: encryptedPassword }),
// });
// 模拟登录成功
setTimeout(() => {
setLoading(false);
// 存储登录状态
localStorage.setItem('isLoggedIn', 'true');
localStorage.setItem('username', username);
// 跳转到项目列表页面
navigate('/projects');
}, 1000);
} catch (err) {
await new Promise((resolve) => setTimeout(resolve, 1000));
// 登录成功,跳转到首页
navigate('/dashboard');
} catch (error) {
setErrorMessage(t('login.error'));
} finally {
setLoading(false);
setError(t('auth.login.error'));
}
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value, type, checked } = e.target;
setFormData({
...formData,
[name]: type === 'checkbox' ? checked : value,
});
// 清除对应字段的错误信息
if (errors[name as keyof LoginFormData]) {
setErrors({
...errors,
[name]: undefined,
});
}
};
return (
<div className="auth-container">
<div className="auth-card">
<h2>{t('auth.login.title')}</h2>
{error && <div className="auth-error">{error}</div>}
<form onSubmit={handleLogin} className="auth-form">
<div className="form-group">
<label htmlFor="username">{t('auth.login.username')}</label>
<input
type="text"
id="username"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
/>
</div>
<div className="form-group">
<label htmlFor="password">{t('auth.login.password')}</label>
<input
type="password"
id="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
<button type="submit" className="auth-button" disabled={loading}>
{loading ? t('auth.login.loading') : t('auth.login.title')}
</button>
</form>
<div className="auth-link">
{t('auth.login.noAccount')} <Link to="/register">{t('auth.login.register')}</Link>
<div className="min-h-screen flex items-center justify-center bg-soft-gray">
<div className="max-w-md w-full space-y-8 p-8 bg-white rounded-[20px] shadow-[0_12px_28px_0_rgba(0,0,0,0.2),0_2px_4px_0_rgba(0,0,0,0.1)] transition-all duration-300 hover:translate-y-[-2px] hover:shadow-[0_16px_32px_0_rgba(0,0,0,0.25),0_4px_8px_0_rgba(0,0,0,0.15)]">
<div className="text-center">
<h1 className="text-3xl font-medium text-dark-charcoal font-optimistic">{t('login.login')}</h1>
<p className="mt-2 text-sm text-slate-gray font-optimistic">{t('login.prompt')}</p>
</div>
{errorMessage && (
<div className="p-3 bg-error-bg text-error-red rounded-[8px]">
{errorMessage}
</div>
)}
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
<Input
type="text"
label={t('login.username')}
placeholder={t('login.placeholderUsername')}
value={formData.username}
onChange={handleInputChange}
error={errors.username}
required
name="username"
/>
<Input
type="password"
label={t('login.password')}
placeholder={t('login.placeholderPassword')}
value={formData.password}
onChange={handleInputChange}
error={errors.password}
required
name="password"
/>
<div className="flex items-center justify-between">
<Checkbox
label={t('login.rememberMe')}
checked={formData.rememberMe}
onChange={handleInputChange}
/>
<div className="text-sm">
<Link to="/forgot-password" className="font-medium text-meta-blue hover:text-meta-blue-hover transition-colors duration-200">
{t('login.forgotPassword')}
</Link>
</div>
</div>
<Button
type="primary"
onClick={() => handleSubmit({ preventDefault: () => {} } as React.FormEvent)}
loading={loading}
fullWidth
>
{t('login.login')}
</Button>
<div className="text-center text-sm">
<span className="text-slate-gray">{t('login.noAccount')}</span>
<Link to="/register" className="font-medium text-meta-blue hover:text-meta-blue-hover transition-colors duration-200">
{t('login.register')}
</Link>
</div>
</form>
</div>
</div>
);

View File

@ -1,54 +1,87 @@
import React, { useState } from 'react';
import { useNavigate, Link } from 'react-router-dom';
import { Link, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import './Auth.css';
import Input from '../../components/ui/Input';
import Button from '../../components/ui/Button';
interface RegisterFormData {
username: string;
password: string;
confirmPassword: string;
}
const Register: React.FC = () => {
const { t } = useTranslation();
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const navigate = useNavigate();
const [formData, setFormData] = useState<RegisterFormData>({
username: '',
password: '',
confirmPassword: '',
});
const [errors, setErrors] = useState<Partial<RegisterFormData>>({});
const [loading, setLoading] = useState(false);
const [errorMessage, setErrorMessage] = useState('');
const handleRegister = async (e: React.FormEvent) => {
const validateForm = (): boolean => {
const newErrors: Partial<RegisterFormData> = {};
if (!formData.username) {
newErrors.username = t('register.usernameRequired');
}
if (!formData.password) {
newErrors.password = t('register.passwordRequired');
} else if (formData.password.length < 6) {
newErrors.password = t('register.passwordMinLength');
} else if (!/^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{6,}$/.test(formData.password)) {
newErrors.password = t('register.passwordFormat');
}
if (!formData.confirmPassword) {
newErrors.confirmPassword = t('register.confirmPasswordRequired');
} else if (formData.confirmPassword !== formData.password) {
newErrors.confirmPassword = t('register.confirmPasswordMismatch');
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
// 验证密码是否一致
if (password !== confirmPassword) {
setError(t('auth.register.error.passwordMismatch'));
if (!validateForm()) {
return;
}
setLoading(true);
setErrorMessage('');
// 模拟注册请求
try {
// 模拟加密密码
// const encryptedPassword = btoa(password);
// 预留后台接口调用
// const response = await fetch('/api/register', {
// method: 'POST',
// headers: {
// 'Content-Type': 'application/json',
// },
// body: JSON.stringify({ username, password: encryptedPassword }),
// });
// 模拟注册成功
setTimeout(() => {
setLoading(false);
// 存储登录状态
localStorage.setItem('isLoggedIn', 'true');
localStorage.setItem('username', username);
// 跳转到项目列表页面
navigate('/projects');
}, 1000);
} catch (err) {
await new Promise((resolve) => setTimeout(resolve, 1000));
// 注册成功,跳转到登录页
navigate('/login');
} catch (error) {
setErrorMessage(t('register.error'));
} finally {
setLoading(false);
setError(t('auth.register.error.failed'));
}
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData({
...formData,
[name]: value,
});
// 清除对应字段的错误信息
if (errors[name as keyof RegisterFormData]) {
setErrors({
...errors,
[name]: undefined,
});
}
};
@ -57,53 +90,78 @@ const Register: React.FC = () => {
};
return (
<div className="auth-container">
<div className="auth-card">
<h2>{t('auth.register.title')}</h2>
{error && <div className="auth-error">{error}</div>}
<form onSubmit={handleRegister} className="auth-form">
<div className="form-group">
<label htmlFor="username">{t('auth.register.username')}</label>
<input
type="text"
id="username"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
/>
<div className="min-h-screen flex items-center justify-center bg-soft-gray">
<div className="max-w-md w-full space-y-8 p-8 bg-white rounded-[20px] shadow-[0_12px_28px_0_rgba(0,0,0,0.2),0_2px_4px_0_rgba(0,0,0,0.1)] transition-all duration-300 hover:translate-y-[-2px] hover:shadow-[0_16px_32px_0_rgba(0,0,0,0.25),0_4px_8px_0_rgba(0,0,0,0.15)]">
<div className="text-center">
<h1 className="text-3xl font-medium text-dark-charcoal font-optimistic">{t('register.register')}</h1>
<p className="mt-2 text-sm text-slate-gray font-optimistic">{t('register.prompt')}</p>
</div>
{errorMessage && (
<div className="p-3 bg-error-bg text-error-red rounded-[8px]">
{errorMessage}
</div>
<div className="form-group">
<label htmlFor="password">{t('auth.register.password')}</label>
<input
type="password"
id="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
)}
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
<Input
type="text"
label={t('register.username')}
placeholder={t('register.placeholderUsername')}
value={formData.username}
onChange={handleInputChange}
error={errors.username}
required
name="username"
/>
<Input
type="password"
label={t('register.password')}
placeholder={t('register.placeholderPassword')}
value={formData.password}
onChange={handleInputChange}
error={errors.password}
required
name="password"
/>
<Input
type="password"
label={t('register.confirmPassword')}
placeholder={t('register.placeholderConfirmPassword')}
value={formData.confirmPassword}
onChange={handleInputChange}
error={errors.confirmPassword}
required
name="confirmPassword"
/>
<div className="flex space-x-4">
<Button
type="secondary"
onClick={handleCancel}
fullWidth
>
{t('register.cancel')}
</Button>
<Button
type="primary"
onClick={() => handleSubmit({ preventDefault: () => {} } as React.FormEvent)}
loading={loading}
fullWidth
>
{t('register.register')}
</Button>
</div>
<div className="form-group">
<label htmlFor="confirmPassword">{t('auth.register.confirmPassword')}</label>
<input
type="password"
id="confirmPassword"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
required
/>
</div>
<div className="auth-buttons">
<button type="button" className="auth-button cancel" onClick={handleCancel}>
{t('auth.register.cancel')}
</button>
<button type="submit" className="auth-button" disabled={loading}>
{loading ? t('auth.register.loading') : t('auth.register.register')}
</button>
<div className="text-center text-sm">
<span className="text-slate-gray">{t('register.hasAccount')}</span>
<Link to="/login" className="font-medium text-meta-blue hover:text-meta-blue-hover transition-colors duration-200">
{t('register.backToLogin')}
</Link>
</div>
</form>
<div className="auth-link">
{t('auth.register.haveAccount')} <Link to="/login">{t('auth.register.login')}</Link>
</div>
</div>
</div>
);

View File

@ -0,0 +1,121 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import Icon from '../../components/ui/Icon';
const Dashboard: React.FC = () => {
const { t } = useTranslation();
const mockStats = {
totalProjects: 12,
activeProjects: 8,
recentProjects: [
{ id: 1, name: '项目 A', createdAt: '2026-04-20' },
{ id: 2, name: '项目 B', createdAt: '2026-04-19' },
{ id: 3, name: '项目 C', createdAt: '2026-04-18' },
],
};
const StatCard: React.FC<{ title: string; value: number | string; icon: string; color: string }> = ({
title,
value,
icon,
color,
}) => (
<div className="bg-white rounded-[20px] p-6 shadow-[0_12px_28px_0_rgba(0,0,0,0.2),0_2px_4px_0_rgba(0,0,0,0.1)] transition-all duration-300 hover:translate-y-[-2px] hover:shadow-[0_16px_32px_0_rgba(0,0,0,0.25),0_4px_8px_0_rgba(0,0,0,0.15)]">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-slate-gray font-optimistic mb-1">{title}</p>
<p className="text-3xl font-medium text-dark-charcoal font-optimistic">{value}</p>
</div>
<div className={`w-12 h-12 rounded-full ${color} flex items-center justify-center`}>
<Icon name={icon} size="lg" className="text-dark-charcoal" />
</div>
</div>
</div>
);
return (
<div className="space-y-6">
<div className="bg-white rounded-[20px] p-6 shadow-[0_12px_28px_0_rgba(0,0,0,0.2),0_2px_4px_0_rgba(0,0,0,0.1)]">
<h1 className="text-2xl font-medium text-dark-charcoal font-optimistic mb-2">
{t('dashboard.welcome', { username: 'User' })}
</h1>
<p className="text-sm text-slate-gray font-optimistic">
{t('dashboard.systemNormal')}
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<StatCard
title={t('dashboard.totalProjects')}
value={mockStats.totalProjects}
icon="projects"
color="bg-meta-blue bg-opacity-10"
/>
<StatCard
title={t('dashboard.activeProjects')}
value={mockStats.activeProjects}
icon="checkCircle"
color="bg-success-green bg-opacity-10"
/>
<StatCard
title={t('dashboard.systemStatus')}
value={t('dashboard.systemNormal')}
icon="status"
color="bg-lime bg-opacity-10"
/>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="bg-white rounded-[20px] p-6 shadow-[0_12px_28px_0_rgba(0,0,0,0.2),0_2px_4px_0_rgba(0,0,0,0.1)]">
<h2 className="text-lg font-medium text-dark-charcoal font-optimistic mb-4">
{t('dashboard.recentProjects')}
</h2>
<div className="space-y-3">
{mockStats.recentProjects.map((project) => (
<div
key={project.id}
className="flex items-center justify-between p-3 rounded-[8px] hover:bg-soft-gray transition-colors duration-200"
>
<div className="flex items-center">
<Icon name="file" size="md" className="mr-3 text-dark-charcoal" />
<span className="text-sm font-medium text-dark-charcoal font-optimistic">
{project.name}
</span>
</div>
<span className="text-xs text-slate-gray font-optimistic">
{project.createdAt}
</span>
</div>
))}
</div>
</div>
<div className="bg-white rounded-[20px] p-6 shadow-[0_12px_28px_0_rgba(0,0,0,0.2),0_2px_4px_0_rgba(0,0,0,0.1)]">
<h2 className="text-lg font-medium text-dark-charcoal font-optimistic mb-4">
{t('dashboard.quickActions')}
</h2>
<div className="space-y-3">
<Link
to="/projects/new"
className="flex items-center p-4 rounded-[8px] bg-meta-blue text-white hover:bg-meta-blue-hover transition-colors duration-200 font-optimistic"
>
<Icon name="plus" size="md" className="mr-3" />
<span className="font-medium">{t('dashboard.createProject')}</span>
</Link>
<Link
to="/projects"
className="flex items-center p-4 rounded-[8px] bg-soft-gray text-dark-charcoal hover:bg-divider transition-colors duration-200 font-optimistic"
>
<Icon name="list" size="md" className="mr-3" />
<span className="font-medium">{t('dashboard.viewProjects')}</span>
</Link>
</div>
</div>
</div>
</div>
);
};
export default Dashboard;

View File

@ -0,0 +1,384 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import Input from '../../components/ui/Input';
import Button from '../../components/ui/Button';
import Icon from '../../components/ui/Icon';
interface Project {
id: number;
name: string;
description: string;
createdAt: string;
status: 'active' | 'inactive' | 'completed';
}
const ProjectList: React.FC = () => {
const { t } = useTranslation();
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState('all');
const [selectedProjects, setSelectedProjects] = useState<number[]>([]);
const [showCreateModal, setShowCreateModal] = useState(false);
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [showBatchDeleteModal, setShowBatchDeleteModal] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
const [projectsPerPage] = useState(6);
const [newProject, setNewProject] = useState({ name: '', description: '' });
const [deleteProjectId, setDeleteProjectId] = useState<number | null>(null);
// 模拟项目数据
const mockProjects: Project[] = [
{ id: 1, name: '项目 A', description: '这是项目 A 的描述', createdAt: '2026-04-20', status: 'active' },
{ id: 2, name: '项目 B', description: '这是项目 B 的描述', createdAt: '2026-04-19', status: 'inactive' },
{ id: 3, name: '项目 C', description: '这是项目 C 的描述', createdAt: '2026-04-18', status: 'completed' },
{ id: 4, name: '项目 D', description: '这是项目 D 的描述', createdAt: '2026-04-17', status: 'active' },
{ id: 5, name: '项目 E', description: '这是项目 E 的描述', createdAt: '2026-04-16', status: 'inactive' },
{ id: 6, name: '项目 F', description: '这是项目 F 的描述', createdAt: '2026-04-15', status: 'completed' },
{ id: 7, name: '项目 G', description: '这是项目 G 的描述', createdAt: '2026-04-14', status: 'active' },
{ id: 8, name: '项目 H', description: '这是项目 H 的描述', createdAt: '2026-04-13', status: 'inactive' },
];
// 过滤项目
const filteredProjects = mockProjects.filter(project => {
const matchesSearch = project.name.toLowerCase().includes(searchTerm.toLowerCase());
const matchesStatus = statusFilter === 'all' || project.status === statusFilter;
return matchesSearch && matchesStatus;
});
// 分页逻辑
const indexOfLastProject = currentPage * projectsPerPage;
const indexOfFirstProject = indexOfLastProject - projectsPerPage;
const currentProjects = filteredProjects.slice(indexOfFirstProject, indexOfLastProject);
const totalPages = Math.ceil(filteredProjects.length / projectsPerPage);
// 处理项目选择
const handleProjectSelect = (projectId: number) => {
setSelectedProjects(prev =>
prev.includes(projectId)
? prev.filter(id => id !== projectId)
: [...prev, projectId]
);
};
// 处理项目删除
const handleDeleteProject = (projectId: number) => {
setDeleteProjectId(projectId);
setShowDeleteModal(true);
};
// 确认删除项目
const confirmDeleteProject = () => {
if (deleteProjectId) {
// 模拟删除操作
console.log('删除项目:', deleteProjectId);
setShowDeleteModal(false);
setDeleteProjectId(null);
}
};
// 确认批量删除
const confirmBatchDelete = () => {
// 模拟批量删除操作
console.log('批量删除项目:', selectedProjects);
setShowBatchDeleteModal(false);
setSelectedProjects([]);
};
// 处理创建新项目
const handleCreateProject = () => {
if (newProject.name) {
// 模拟创建操作
console.log('创建新项目:', newProject);
setShowCreateModal(false);
setNewProject({ name: '', description: '' });
}
};
// 处理搜索
const handleSearch = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
// 模拟搜索操作
console.log('搜索项目:', searchTerm);
}
};
// 处理筛选
const handleStatusChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setStatusFilter(e.target.value);
setCurrentPage(1);
// 模拟筛选操作
console.log('筛选项目:', e.target.value);
};
return (
<div className="space-y-6">
<div className="bg-white rounded-[20px] p-6 shadow-[0_12px_28px_0_rgba(0,0,0,0.2),0_2px_4px_0_rgba(0,0,0,0.1)]">
<div className="flex flex-col md:flex-row md:items-center md:justify-between mb-6 space-y-4 md:space-y-0">
<h1 className="text-2xl font-medium text-dark-charcoal font-optimistic">{t('projectList.title')}</h1>
<Button
type="primary"
onClick={() => setShowCreateModal(true)}
className="w-full md:w-auto"
>
<Icon name="plus" size="sm" className="mr-2" />
{t('projectList.createProject')}
</Button>
</div>
<div className="flex flex-col md:flex-row md:items-center space-y-4 md:space-y-0 md:space-x-4 mb-6">
<div className="flex-1">
<Input
type="text"
label={t('projectList.search')}
placeholder={t('projectList.search')}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
onKeyDown={handleSearch}
name="search"
/>
</div>
<div className="w-full md:w-48">
<label className="block text-sm font-medium text-dark-charcoal font-optimistic mb-1">
{t('projectList.status')}
</label>
<select
value={statusFilter}
onChange={handleStatusChange}
className="w-full px-3 py-2 border border-divider rounded-[8px] focus:outline-none focus:ring-3 focus:ring-meta-blue focus:border-meta-blue transition-all duration-200 font-optimistic text-base"
>
<option value="all">{t('projectList.allStatus')}</option>
<option value="active">{t('projectList.active')}</option>
<option value="inactive">{t('projectList.inactive')}</option>
<option value="completed">{t('projectList.completed')}</option>
</select>
</div>
</div>
{selectedProjects.length > 0 && (
<div className="flex items-center justify-between p-4 bg-soft-gray rounded-[8px] mb-6">
<p className="text-sm font-medium text-dark-charcoal font-optimistic">
{t('projectList.selectedCount', { count: selectedProjects.length })}
</p>
<Button
type="danger"
onClick={() => setShowBatchDeleteModal(true)}
>
<Icon name="list" size="sm" className="mr-2" />
{t('projectList.batchDelete')}
</Button>
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{currentProjects.map((project) => (
<div key={project.id} className="bg-white rounded-[20px] overflow-hidden shadow-[0_12px_28px_0_rgba(0,0,0,0.2),0_2px_4px_0_rgba(0,0,0,0.1)] transition-all duration-300 hover:shadow-[0_16px_32px_0_rgba(0,0,0,0.25),0_4px_8px_0_rgba(0,0,0,0.15)]">
<div className="relative">
{/* 项目背景图片占位 */}
<div className="h-40 bg-soft-gray flex items-center justify-center">
<Icon name="projects" size="lg" className="text-slate-gray" />
</div>
{/* 选择框 */}
<div className="absolute top-4 left-4">
<input
type="checkbox"
checked={selectedProjects.includes(project.id)}
onChange={() => handleProjectSelect(project.id)}
className="w-4 h-4 text-meta-blue border-gray-300 focus:ring-meta-blue"
/>
</div>
</div>
<div className="p-4">
<h3 className="text-lg font-medium text-dark-charcoal font-optimistic mb-2">
{project.name}
</h3>
<p className="text-sm text-slate-gray font-optimistic mb-4">
{project.description}
</p>
<div className="flex items-center justify-between text-xs text-slate-gray">
<span>{project.createdAt}</span>
<span className={`px-2 py-1 rounded-full text-xs font-medium ${
project.status === 'active' ? 'bg-success-green bg-opacity-10 text-success-green' :
project.status === 'inactive' ? 'bg-error-bg text-error-red' :
'bg-meta-blue bg-opacity-10 text-meta-blue'
}`}>
{project.status === 'active' ? t('projectList.active') :
project.status === 'inactive' ? t('projectList.inactive') :
t('projectList.completed')}
</span>
</div>
{/* 操作按钮 */}
<div className="mt-4 flex space-x-2 opacity-0 transition-opacity duration-200 group-hover:opacity-100">
<Button
type="secondary"
size="sm"
className="flex-1"
onClick={() => console.log('编辑项目:', project.id)}
>
<Icon name="settings" size="sm" className="mr-1" />
{t('projectList.edit')}
</Button>
<Button
type="danger"
size="sm"
className="flex-1"
onClick={() => handleDeleteProject(project.id)}
>
<Icon name="list" size="sm" className="mr-1" />
{t('projectList.delete')}
</Button>
</div>
</div>
</div>
))}
</div>
{/* 分页 */}
{totalPages > 1 && (
<div className="flex items-center justify-between mt-8">
<p className="text-sm text-slate-gray font-optimistic">
{t('projectList.pagination', {
start: indexOfFirstProject + 1,
end: Math.min(indexOfLastProject, filteredProjects.length),
total: filteredProjects.length
})}
</p>
<div className="flex space-x-2">
<Button
type="secondary"
size="sm"
disabled={currentPage === 1}
onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))}
>
<Icon name="chevronLeft" size="sm" />
</Button>
{Array.from({ length: totalPages }, (_, i) => i + 1).map(page => (
<Button
key={page}
type={currentPage === page ? 'primary' : 'secondary'}
size="sm"
onClick={() => setCurrentPage(page)}
>
{page}
</Button>
))}
<Button
type="secondary"
size="sm"
disabled={currentPage === totalPages}
onClick={() => setCurrentPage(prev => Math.min(prev + 1, totalPages))}
>
<Icon name="chevronRight" size="sm" />
</Button>
</div>
</div>
)}
</div>
{/* 创建新项目弹窗 */}
{showCreateModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-[20px] p-6 max-w-md w-full">
<h2 className="text-xl font-medium text-dark-charcoal font-optimistic mb-4">
{t('projectList.createProject')}
</h2>
<div className="space-y-4">
<Input
type="text"
label={t('projectList.projectName')}
placeholder={t('projectList.projectName')}
value={newProject.name}
onChange={(e) => setNewProject({ ...newProject, name: e.target.value })}
name="name"
/>
<Input
type="text"
label={t('projectList.projectDescription')}
placeholder={t('projectList.projectDescription')}
value={newProject.description}
onChange={(e) => setNewProject({ ...newProject, description: e.target.value })}
name="description"
/>
<div className="flex space-x-4 mt-6">
<Button
type="secondary"
onClick={() => setShowCreateModal(false)}
fullWidth
>
{t('projectList.cancel')}
</Button>
<Button
type="primary"
onClick={handleCreateProject}
fullWidth
>
{t('projectList.save')}
</Button>
</div>
</div>
</div>
</div>
)}
{/* 删除项目弹窗 */}
{showDeleteModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-[20px] p-6 max-w-md w-full">
<h2 className="text-xl font-medium text-dark-charcoal font-optimistic mb-4">
{t('projectList.confirmDelete')}
</h2>
<p className="text-sm text-slate-gray font-optimistic mb-6">
{t('projectList.confirmDeleteMessage')}
</p>
<div className="flex space-x-4">
<Button
type="secondary"
onClick={() => setShowDeleteModal(false)}
fullWidth
>
{t('projectList.cancel')}
</Button>
<Button
type="danger"
onClick={confirmDeleteProject}
fullWidth
>
{t('projectList.delete')}
</Button>
</div>
</div>
</div>
)}
{/* 批量删除弹窗 */}
{showBatchDeleteModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-[20px] p-6 max-w-md w-full">
<h2 className="text-xl font-medium text-dark-charcoal font-optimistic mb-4">
{t('projectList.confirmBatchDelete')}
</h2>
<p className="text-sm text-slate-gray font-optimistic mb-6">
{t('projectList.confirmBatchDeleteMessage', { count: selectedProjects.length })}
</p>
<div className="flex space-x-4">
<Button
type="secondary"
onClick={() => setShowBatchDeleteModal(false)}
fullWidth
>
{t('projectList.cancel')}
</Button>
<Button
type="danger"
onClick={confirmBatchDelete}
fullWidth
>
{t('projectList.delete')}
</Button>
</div>
</div>
</div>
)}
</div>
);
};
export default ProjectList;

View File

@ -0,0 +1,334 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import Input from '../../components/ui/Input';
import Button from '../../components/ui/Button';
interface SettingFormData {
username: string;
email: string;
oldPassword: string;
newPassword: string;
confirmPassword: string;
language: 'zh' | 'en';
emailNotification: boolean;
systemNotification: boolean;
}
const Settings: React.FC = () => {
const { t, i18n } = useTranslation();
const [activeTab, setActiveTab] = useState('profile');
const [formData, setFormData] = useState<SettingFormData>({
username: 'User',
email: 'user@example.com',
oldPassword: '',
newPassword: '',
confirmPassword: '',
language: i18n.language as 'zh' | 'en',
emailNotification: true,
systemNotification: true,
});
const [errors, setErrors] = useState<Partial<SettingFormData>>({});
const [loading, setLoading] = useState(false);
const [successMessage, setSuccessMessage] = useState('');
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value, type, checked } = e.target;
setFormData({
...formData,
[name]: type === 'checkbox' ? checked : value,
});
// 清除对应字段的错误信息
if (errors[name as keyof SettingFormData]) {
setErrors({
...errors,
[name]: undefined,
});
}
};
const validateProfileForm = (): boolean => {
const newErrors: Partial<SettingFormData> = {};
if (!formData.username) {
newErrors.username = t('settings.usernameRequired');
}
if (!formData.email) {
newErrors.email = t('settings.emailRequired');
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
newErrors.email = t('settings.emailInvalid');
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const validatePasswordForm = (): boolean => {
const newErrors: Partial<SettingFormData> = {};
if (!formData.oldPassword) {
newErrors.oldPassword = t('settings.oldPasswordRequired');
}
if (!formData.newPassword) {
newErrors.newPassword = t('settings.newPasswordRequired');
} else if (formData.newPassword.length < 6) {
newErrors.newPassword = t('settings.passwordMinLength');
}
if (!formData.confirmPassword) {
newErrors.confirmPassword = t('settings.confirmPasswordRequired');
} else if (formData.confirmPassword !== formData.newPassword) {
newErrors.confirmPassword = t('settings.passwordMismatch');
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
let isValid = false;
switch (activeTab) {
case 'profile':
isValid = validateProfileForm();
break;
case 'password':
isValid = validatePasswordForm();
break;
case 'language':
case 'notifications':
isValid = true;
break;
default:
isValid = false;
}
if (!isValid) {
return;
}
setLoading(true);
setSuccessMessage('');
// 模拟API请求
try {
await new Promise((resolve) => setTimeout(resolve, 1000));
if (activeTab === 'language') {
i18n.changeLanguage(formData.language);
}
setSuccessMessage(t('settings.saveSuccess'));
// 3秒后清除成功消息
setTimeout(() => setSuccessMessage(''), 3000);
} catch (error) {
console.error('保存失败:', error);
} finally {
setLoading(false);
}
};
const renderTabContent = () => {
switch (activeTab) {
case 'profile':
return (
<form onSubmit={handleSubmit} className="space-y-6">
<Input
type="text"
label={t('settings.username')}
placeholder={t('settings.username')}
value={formData.username}
onChange={handleInputChange}
error={errors.username}
required
name="username"
/>
<Input
type="email"
label={t('settings.email')}
placeholder={t('settings.email')}
value={formData.email}
onChange={handleInputChange}
error={errors.email}
required
name="email"
/>
<div className="pt-4">
<Button type="primary" onClick={() => handleSubmit({ preventDefault: () => {} } as React.FormEvent)} loading={loading} fullWidth>
{t('settings.save')}
</Button>
</div>
</form>
);
case 'password':
return (
<form onSubmit={handleSubmit} className="space-y-6">
<Input
type="password"
label={t('settings.oldPassword')}
placeholder={t('settings.oldPassword')}
value={formData.oldPassword}
onChange={handleInputChange}
error={errors.oldPassword}
required
name="oldPassword"
/>
<Input
type="password"
label={t('settings.newPassword')}
placeholder={t('settings.newPassword')}
value={formData.newPassword}
onChange={handleInputChange}
error={errors.newPassword}
required
name="newPassword"
/>
<Input
type="password"
label={t('settings.confirmPassword')}
placeholder={t('settings.confirmPassword')}
value={formData.confirmPassword}
onChange={handleInputChange}
error={errors.confirmPassword}
required
name="confirmPassword"
/>
<div className="pt-4">
<Button type="primary" onClick={() => handleSubmit({ preventDefault: () => {} } as React.FormEvent)} loading={loading} fullWidth>
{t('settings.save')}
</Button>
</div>
</form>
);
case 'language':
return (
<form onSubmit={handleSubmit} className="space-y-6">
<div className="space-y-4">
<div className="flex items-center">
<input
type="radio"
id="chinese"
name="language"
value="zh"
checked={formData.language === 'zh'}
onChange={handleInputChange}
className="w-4 h-4 text-meta-blue border-gray-300 focus:ring-meta-blue"
/>
<label htmlFor="chinese" className="ml-3 text-sm font-medium text-dark-charcoal font-optimistic">
{t('settings.chinese')}
</label>
</div>
<div className="flex items-center">
<input
type="radio"
id="english"
name="language"
value="en"
checked={formData.language === 'en'}
onChange={handleInputChange}
className="w-4 h-4 text-meta-blue border-gray-300 focus:ring-meta-blue"
/>
<label htmlFor="english" className="ml-3 text-sm font-medium text-dark-charcoal font-optimistic">
{t('settings.english')}
</label>
</div>
</div>
<div className="pt-4">
<Button type="primary" onClick={() => handleSubmit({ preventDefault: () => {} } as React.FormEvent)} loading={loading} fullWidth>
{t('settings.save')}
</Button>
</div>
</form>
);
case 'notifications':
return (
<form onSubmit={handleSubmit} className="space-y-6">
<div className="space-y-4">
<div className="flex items-center justify-between">
<label htmlFor="emailNotification" className="text-sm font-medium text-dark-charcoal font-optimistic">
{t('settings.emailNotification')}
</label>
<div className="relative inline-block w-12 align-middle select-none">
<input
type="checkbox"
id="emailNotification"
name="emailNotification"
checked={formData.emailNotification}
onChange={handleInputChange}
className="sr-only"
/>
<div className="block bg-gray-300 w-12 h-6 rounded-full"></div>
<div className={`absolute left-1 top-1 bg-white w-4 h-4 rounded-full transition-transform duration-200 ${formData.emailNotification ? 'transform translate-x-6' : ''}`}></div>
</div>
</div>
<div className="flex items-center justify-between">
<label htmlFor="systemNotification" className="text-sm font-medium text-dark-charcoal font-optimistic">
{t('settings.systemNotification')}
</label>
<div className="relative inline-block w-12 align-middle select-none">
<input
type="checkbox"
id="systemNotification"
name="systemNotification"
checked={formData.systemNotification}
onChange={handleInputChange}
className="sr-only"
/>
<div className="block bg-gray-300 w-12 h-6 rounded-full"></div>
<div className={`absolute left-1 top-1 bg-white w-4 h-4 rounded-full transition-transform duration-200 ${formData.systemNotification ? 'transform translate-x-6' : ''}`}></div>
</div>
</div>
</div>
<div className="pt-4">
<Button type="primary" onClick={() => handleSubmit({ preventDefault: () => {} } as React.FormEvent)} loading={loading} fullWidth>
{t('settings.save')}
</Button>
</div>
</form>
);
default:
return null;
}
};
return (
<div className="space-y-6">
<div className="bg-white rounded-[20px] p-6 shadow-[0_12px_28px_0_rgba(0,0,0,0.2),0_2px_4px_0_rgba(0,0,0,0.1)]">
<h1 className="text-2xl font-medium text-dark-charcoal font-optimistic mb-6">{t('settings.title')}</h1>
<div className="border-b border-divider mb-6">
<nav className="flex space-x-8">
{[
{ id: 'profile', label: t('settings.profile') },
{ id: 'password', label: t('settings.changePassword') },
{ id: 'language', label: t('settings.language') },
{ id: 'notifications', label: t('settings.notifications') },
].map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`py-4 px-1 border-b-2 font-medium text-sm transition-colors duration-200 ${activeTab === tab.id ? 'border-meta-blue text-meta-blue' : 'border-transparent text-slate-gray hover:text-dark-charcoal hover:border-gray-300'}`}
>
{tab.label}
</button>
))}
</nav>
</div>
{successMessage && (
<div className="p-3 bg-positive-bg text-success-green rounded-[8px] mb-6">
{successMessage}
</div>
)}
{renderTabContent()}
</div>
</div>
);
};
export default Settings;

21
src/routes.tsx Normal file
View File

@ -0,0 +1,21 @@
import React from 'react';
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import Login from './pages/auth/Login';
import Register from './pages/auth/Register';
import ForgotPassword from './pages/auth/ForgotPassword';
const AppRoutes: React.FC = () => {
return (
<Router>
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route path="/forgot-password" element={<ForgotPassword />} />
<Route path="/" element={<Navigate to="/login" replace />} />
<Route path="*" element={<Navigate to="/login" replace />} />
</Routes>
</Router>
);
};
export default AppRoutes;

View File

@ -1,45 +0,0 @@
import axios from 'axios'
const API_BASE_URL = 'http://localhost:3000/api'
const apiClient = axios.create({
baseURL: API_BASE_URL,
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
})
apiClient.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error) => {
return Promise.reject(error)
}
)
apiClient.interceptors.response.use(
(response) => {
return response.data
},
(error) => {
if (error.response) {
const { status, data } = error.response
if (status === 401) {
localStorage.removeItem('token')
window.location.href = '/login'
}
return Promise.reject(data || error.message)
}
return Promise.reject(error.message || '网络错误')
}
)
export default apiClient

View File

@ -1,62 +0,0 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import apiClient from './api'
import { DesignFile } from '../types/canvas.types'
export const useDesigns = () => {
return useQuery({
queryKey: ['designs'],
queryFn: async () => {
return apiClient.get('/designs') as Promise<DesignFile[]>
},
})
}
export const useDesign = (id: string) => {
return useQuery({
queryKey: ['design', id],
queryFn: async () => {
return apiClient.get(`/designs/${id}`) as Promise<DesignFile>
},
enabled: !!id,
})
}
export const useCreateDesign = () => {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (design: Partial<DesignFile>) => {
return apiClient.post('/designs', design) as Promise<DesignFile>
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['designs'] })
},
})
}
export const useUpdateDesign = () => {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async ({ id, design }: { id: string; design: Partial<DesignFile> }) => {
return apiClient.put(`/designs/${id}`, design) as Promise<DesignFile>
},
onSuccess: (_, { id }) => {
queryClient.invalidateQueries({ queryKey: ['design', id] })
queryClient.invalidateQueries({ queryKey: ['designs'] })
},
})
}
export const useDeleteDesign = () => {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (id: string) => {
await apiClient.delete(`/designs/${id}`)
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['designs'] })
},
})
}

View File

@ -1,15 +0,0 @@
import { QueryClient } from '@tanstack/react-query'
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: 2,
staleTime: 5 * 60 * 1000, // 5分钟
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
},
},
})
export default queryClient

View File

@ -1,128 +0,0 @@
export interface GridPosition {
x: number;
y: number;
}
export interface Dimensions {
width: number;
height: number;
}
export type FrameDimensions = Dimensions;
export type ViewportMode = 'desktop' | 'tablet' | 'mobile';
export type LayoutMode = 'grid' | 'hierarchy';
export interface DesignFile {
name: string;
path: string;
modified: Date;
content?: string;
fileType?: 'html' | 'svg' | 'png' | 'jpg';
size?: number;
version?: string;
generation?: number;
branchIndex?: number;
parentDesign?: string;
children?: string[];
relationships?: {
parent?: string;
children?: string[];
};
}
export interface CanvasConfig {
frameSize: Dimensions;
gridSpacing: number;
framesPerRow: number;
minZoom: number;
maxZoom: number;
responsive: {
enableScaling: boolean;
minFrameSize: Dimensions;
maxFrameSize: Dimensions;
scaleWithZoom: boolean;
};
viewports: {
desktop: Dimensions;
tablet: Dimensions;
mobile: Dimensions;
};
hierarchy: {
horizontalSpacing: number;
verticalSpacing: number;
connectionLineWidth: number;
connectionLineColor: string;
showConnections: boolean;
};
}
export interface FrameViewportState {
[fileName: string]: ViewportMode;
}
export interface FramePositionState {
[fileName: string]: GridPosition;
}
export interface DragState {
isDragging: boolean;
draggedFrame: string | null;
startPosition: GridPosition;
currentPosition: GridPosition;
offset: GridPosition;
}
export interface HierarchyNode {
id: string;
fileName: string;
position: GridPosition;
generation?: number;
branchIndex?: number;
children: string[];
parent?: string;
}
export interface ConnectionLine {
id: string;
fromFrame: string;
toFrame: string;
fromPosition: GridPosition;
toPosition: GridPosition;
type: 'parent' | 'child' | 'sibling';
color?: string;
width?: number;
}
export interface HierarchyTree {
roots: string[];
nodes: Map<string, HierarchyNode>;
connections: ConnectionLine[];
bounds: {
minX: number;
minY: number;
maxX: number;
maxY: number;
width?: number;
height?: number;
};
}
export interface CanvasState {
designFiles: DesignFile[];
selectedFrames: string[];
isInitialized: boolean;
isLoading: boolean;
error: string | null;
currentZoom: number;
currentConfig: CanvasConfig;
globalViewportMode: ViewportMode;
frameViewports: FrameViewportState;
useGlobalViewport: boolean;
customPositions: FramePositionState;
dragState: DragState;
layoutMode: LayoutMode;
hierarchyTree: HierarchyTree | null;
showConnections: boolean;
}

View File

@ -1,434 +0,0 @@
import { GridPosition, CanvasConfig, DesignFile, HierarchyTree, HierarchyNode, ConnectionLine } from '../types/canvas.types';
/**
*
*/
export function calculateGridPosition(
index: number,
config: CanvasConfig
): GridPosition {
const row = Math.floor(index / config.framesPerRow);
const col = index % config.framesPerRow;
const x = col * (config.frameSize.width + config.gridSpacing);
const y = row * (config.frameSize.height + config.gridSpacing);
return { x, y };
}
/**
*
*/
export function calculateCanvasBounds(
itemCount: number,
config: CanvasConfig
): { width: number; height: number } {
if (itemCount === 0) {
return { width: 0, height: 0 };
}
const rows = Math.ceil(itemCount / config.framesPerRow);
const cols = Math.min(itemCount, config.framesPerRow);
const width = cols * config.frameSize.width + (cols - 1) * config.gridSpacing;
const height = rows * config.frameSize.height + (rows - 1) * config.gridSpacing;
return { width, height };
}
/**
*
*/
export function calculateFitToView(
itemCount: number,
config: CanvasConfig,
containerWidth: number,
containerHeight: number,
padding: number = 50
): { scale: number; x: number; y: number } {
if (itemCount === 0) {
return { scale: 1, x: 0, y: 0 };
}
const bounds = calculateCanvasBounds(itemCount, config);
const availableWidth = containerWidth - 2 * padding;
const availableHeight = containerHeight - 2 * padding;
const scaleX = availableWidth / bounds.width;
const scaleY = availableHeight / bounds.height;
const scale = Math.min(scaleX, scaleY, 1);
const scaledWidth = bounds.width * scale;
const scaledHeight = bounds.height * scale;
const x = (containerWidth - scaledWidth) / 2;
const y = (containerHeight - scaledHeight) / 2;
return { scale, x, y };
}
/**
*
*/
export function findNearestFrame(
targetPosition: GridPosition,
itemCount: number,
config: CanvasConfig
): number | null {
if (itemCount === 0) {
return null;
}
let nearestIndex = 0;
let minDistance = Infinity;
for (let i = 0; i < itemCount; i++) {
const framePos = calculateGridPosition(i, config);
const distance = Math.sqrt(
Math.pow(framePos.x - targetPosition.x, 2) +
Math.pow(framePos.y - targetPosition.y, 2)
);
if (distance < minDistance) {
minDistance = distance;
nearestIndex = i;
}
}
return nearestIndex;
}
/**
*
*/
export function generateResponsiveConfig(
baseConfig: CanvasConfig,
containerWidth: number
): CanvasConfig {
let framesPerRow = baseConfig.framesPerRow;
let gridSpacing = baseConfig.gridSpacing;
if (containerWidth < 600) {
framesPerRow = 1;
gridSpacing = 30;
} else if (containerWidth < 900) {
framesPerRow = 2;
gridSpacing = 40;
} else if (containerWidth < 1300) {
framesPerRow = 3;
gridSpacing = 45;
} else if (containerWidth < 1800) {
framesPerRow = 4;
gridSpacing = 50;
} else {
framesPerRow = 5;
gridSpacing = 60;
}
return {
...baseConfig,
framesPerRow,
gridSpacing
};
}
/**
*
*/
export function getGridMetrics(
itemCount: number,
config: CanvasConfig
): {
rows: number;
cols: number;
totalFrames: number;
bounds: { width: number; height: number };
} {
const rows = Math.ceil(itemCount / config.framesPerRow);
const cols = Math.min(itemCount, config.framesPerRow);
const bounds = calculateCanvasBounds(itemCount, config);
return {
rows,
cols,
totalFrames: itemCount,
bounds
};
}
/**
*
*/
export function buildHierarchyTree(designs: DesignFile[]): HierarchyTree {
const nodes = new Map<string, HierarchyNode>();
const roots: string[] = [];
const connections: ConnectionLine[] = [];
designs.forEach(design => {
const node: HierarchyNode = {
id: design.name,
fileName: design.name,
position: { x: 0, y: 0 },
generation: design.generation || 0,
branchIndex: design.branchIndex || 0,
parent: design.parentDesign,
children: design.children || []
};
nodes.set(design.name, node);
if (!design.parentDesign) {
roots.push(design.name);
}
});
nodes.forEach((node, fileName) => {
if (node.parent && nodes.has(node.parent)) {
const parentNode = nodes.get(node.parent)!;
connections.push({
id: `${node.parent}-${fileName}`,
fromFrame: node.parent,
toFrame: fileName,
fromPosition: parentNode.position,
toPosition: node.position,
type: 'child'
});
}
});
return {
roots,
nodes,
connections,
bounds: {
minX: 0,
minY: 0,
maxX: 0,
maxY: 0
}
};
}
/**
*
*/
export function calculateHierarchyPositions(
tree: HierarchyTree,
config: CanvasConfig,
actualFrameDimensions?: { width: number; height: number }
): HierarchyTree {
const { verticalSpacing } = config.hierarchy;
const frameWidth = actualFrameDimensions?.width || Math.max(config.frameSize.width, 400);
const frameHeight = actualFrameDimensions?.height || Math.max(config.frameSize.height, 550);
let currentRootY = 100;
tree.roots.forEach(rootName => {
const rootNode = tree.nodes.get(rootName)!;
rootNode.position = {
x: 50,
y: currentRootY
};
const nextAvailableY = positionChildrenImproved(rootNode, tree.nodes, config, currentRootY, { width: frameWidth, height: frameHeight });
currentRootY = Math.max(
currentRootY + frameHeight + verticalSpacing * 2,
nextAvailableY + verticalSpacing * 2
);
});
tree.connections.forEach(connection => {
const fromNode = tree.nodes.get(connection.fromFrame);
const toNode = tree.nodes.get(connection.toFrame);
if (fromNode && toNode) {
connection.fromPosition = {
x: fromNode.position.x + frameWidth,
y: fromNode.position.y + frameHeight / 2
};
connection.toPosition = {
x: toNode.position.x,
y: toNode.position.y + frameHeight / 2
};
}
});
let minX = 0, minY = 0, maxX = 0, maxY = 0;
tree.nodes.forEach(node => {
minX = Math.min(minX, node.position.x);
minY = Math.min(minY, node.position.y);
maxX = Math.max(maxX, node.position.x + frameWidth + 100);
maxY = Math.max(maxY, node.position.y + frameHeight + 100);
});
tree.bounds = {
minX,
minY,
maxX,
maxY
};
return tree;
}
/**
*
*/
// function calculateSubtreeHeight(
// node: HierarchyNode,
// nodes: Map<string, HierarchyNode>,
// config: CanvasConfig,
// frameDimensions: { width: number; height: number }
// ): number {
// const { verticalSpacing } = config.hierarchy;
// const frameHeight = frameDimensions.height;
//
// const children = node.children
// .map(childName => nodes.get(childName))
// .filter(child => child !== undefined) as HierarchyNode[];
//
// if (children.length === 0) {
// return frameHeight;
// }
//
// let totalChildrenHeight = 0;
// children.forEach(child => {
// totalChildrenHeight += calculateSubtreeHeight(child, nodes, config, frameDimensions);
// });
//
// totalChildrenHeight += (children.length - 1) * verticalSpacing;
//
// return Math.max(frameHeight, totalChildrenHeight);
// }
/**
*
*/
function positionChildrenImproved(
parentNode: HierarchyNode,
nodes: Map<string, HierarchyNode>,
config: CanvasConfig,
startY: number,
frameDimensions: { width: number; height: number }
): number {
const { horizontalSpacing, verticalSpacing } = config.hierarchy;
const frameWidth = frameDimensions.width;
const frameHeight = frameDimensions.height;
const children = parentNode.children
.map(childName => nodes.get(childName))
.filter(child => child !== undefined) as HierarchyNode[];
if (children.length === 0) {return startY + frameHeight;}
let currentY = startY;
children.forEach((child) => {
child.position = {
x: parentNode.position.x + frameWidth + horizontalSpacing,
y: currentY
};
const nextY = positionChildrenImproved(child, nodes, config, currentY, frameDimensions);
currentY = Math.max(currentY + frameHeight + verticalSpacing, nextY + verticalSpacing);
});
return currentY;
}
/**
*
*/
export function getHierarchicalPosition(
fileName: string,
tree: HierarchyTree
): GridPosition {
const node = tree.nodes.get(fileName);
return node ? node.position : { x: 0, y: 0 };
}
/**
* "text_1_3_1.html" -> ["text", "1", "3", "1"]
*/
export function parseHierarchicalPath(filename: string): string[] {
const nameWithoutExt = filename.replace(/\.[^/.]+$/, "");
const parts = nameWithoutExt.split('_');
return parts;
}
/**
* "text_1_3_1" -> "text_1_3"
*/
export function getParentPath(filename: string): string | null {
const parts = parseHierarchicalPath(filename);
if (parts.length <= 2) {
return null;
}
const parentParts = parts.slice(0, -1);
return parentParts.join('_');
}
/**
* "text_1_3_1" -> 2, "text_1" -> 0
*/
export function getGenerationLevel(filename: string): number {
const parts = parseHierarchicalPath(filename);
const numericParts = parts.slice(1);
return Math.max(0, numericParts.length - 1);
}
/**
* "text_1_3_1" -> "1", "text_1_3" -> "3"
*/
export function getCurrentLevelVersion(filename: string): string {
const parts = parseHierarchicalPath(filename);
return parts[parts.length - 1];
}
/**
*
*/
export function detectDesignRelationships(designs: DesignFile[]): DesignFile[] {
const updatedDesigns = designs.map(design => ({ ...design }));
const designMap = new Map<string, DesignFile>();
updatedDesigns.forEach(design => {
const nameWithoutExt = design.name.replace(/\.[^/.]+$/, "");
designMap.set(nameWithoutExt, design);
});
updatedDesigns.forEach(design => {
design.version = getCurrentLevelVersion(design.name);
design.generation = getGenerationLevel(design.name);
const parentPath = getParentPath(design.name);
if (parentPath) {
const parentDesign = designMap.get(parentPath);
if (parentDesign) {
design.parentDesign = parentDesign.name;
if (!parentDesign.children) {
parentDesign.children = [];
}
if (!parentDesign.children.includes(design.name)) {
parentDesign.children.push(design.name);
}
}
}
if (design.parentDesign) {
const parentDesign = designMap.get(getParentPath(design.name)!);
if (parentDesign && parentDesign.children) {
design.branchIndex = parentDesign.children.indexOf(design.name);
}
} else {
design.branchIndex = parseInt(design.version) - 1;
}
});
return updatedDesigns;
}

6
src/vite-env.d.ts vendored
View File

@ -1,6 +0,0 @@
/// <reference types="vite/client" />
declare module '*.svg' {
const src: string;
export default src;
}

76
tailwind.config.js Normal file
View File

@ -0,0 +1,76 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {
colors: {
// Primary
'meta-blue': '#0064E0',
'meta-blue-hover': '#0143B5',
'meta-blue-pressed': '#004BB9',
'meta-blue-light': '#47A5FA',
'facebook-blue': '#1877F2',
// Secondary & Accent
'ray-ban-red': '#D6311F',
'oculus-purple': '#A121CE',
'work-purple': '#6441D2',
'portal-blue': '#1B365D',
'portal-hero-blue': '#C8E4E8',
'portal-light-blue': '#ADD4E0',
// Surface & Background
'soft-gray': '#F1F4F7',
'warm-gray': '#F7F8FA',
'web-wash': '#F0F2F5',
'linen': '#F2F0E6',
'baby-blue': '#E8F3FF',
'near-black': '#1C1E21',
'oculus-light': '#181A1B',
'oculus-dark': '#000000',
// Neutrals & Text
'primary-text': '#050505',
'dark-charcoal': '#1C2B33',
'icon-secondary': '#465A69',
'secondary-text': '#65676B',
'slate-gray': '#5D6C7B',
'section-header': '#4B4C4F',
'button-text-gray': '#444950',
'disabled-text': '#BCC0C4',
'cta-disabled-text': '#8595A4',
'divider': '#CED0D4',
'divider-gray': '#DEE3E9',
'cta-gray-border': '#CBD2D9',
'dark-gray-border': '#909396',
// Semantic & Accent
'success-green': '#31A24C',
'store-success': '#007D1E',
'error-red': '#E41E3F',
'store-error': '#C80A28',
'warning-amber': '#F7B928',
'positive-bg': 'rgba(36, 228, 0, 0.15)',
'error-bg': 'rgba(255, 123, 145, 0.15)',
'warning-bg': 'rgba(255, 226, 0, 0.15)',
'info-bg': 'rgba(0, 145, 255, 0.15)',
// Base Color Spectrum
'cherry': '#F3425F',
'grape': '#9360F7',
'lime': '#45BD62',
'seafoam': '#54C7EC',
'teal': '#2ABBA7',
'tomato': '#FB724B',
'pink': '#FF66BF',
},
fontFamily: {
'optimistic': ['Optimistic VF', 'Montserrat', 'Helvetica', 'Arial', 'sans-serif'],
},
},
},
plugins: [],
}

View File

@ -9,6 +9,7 @@
- 样式方案Tailwind CSS
- 代码质量ESLint + Prettier
- 测试Jest + React Testing Library
- 图标radix-ui/react-icons
## 项目结构
```
@ -47,6 +48,7 @@ project-root/
│ ├── App.tsx # 应用入口组件
│ ├── main.tsx # 入口文件
│ └── routes.tsx # 路由配置
├── .gitignore # 忽略文件
├── .eslintrc.js # ESLint 配置
├── .prettierrc.js # Prettier 配置
├── tsconfig.json # TypeScript 配置
@ -64,112 +66,7 @@ project-root/
- 设计规范严格按照design/DESIGN.md中的设计规范
## 页面组成
### 1. 登录页面
- 元素:
- 系统 logo
- 用户名输入框(带校验)
- 密码输入框(带校验,显示/隐藏密码功能)
- 记住我 checkbox
- 登录按钮
- 注册链接
- 忘记密码链接
- 交互:
- 用户名:必填,格式校验
- 密码:必填,长度至少 6 位
- 点击登录:提交表单,显示加载状态
- 登录失败:显示错误信息
- 注册链接:跳转到注册页面
### 2. 注册页面
- 元素:
- 系统 logo
- 用户名输入框(带校验)
- 密码输入框(带校验)
- 确认密码输入框(带校验)
- 取消按钮
- 注册按钮
- 返回登录链接
- 交互:
- 用户名:必填,格式校验,唯一性检查
- 密码:必填,长度至少 6 位,包含字母和数字
- 确认密码:必须与密码一致
- 点击注册:提交表单,显示加载状态
- 注册失败:显示错误信息
- 取消按钮:返回登录页面
### 3. 系统布局
- 左侧菜单树:
- 菜单项:首页、项目管理、设置
- 展开/收起功能
- 顶部导航栏:
- 系统标题
- 右侧:通知图标、用户菜单
- 主体区域:显示当前选中的页面内容
- 响应式:在小屏幕设备上,左侧菜单可折叠
### 4. 首页
- 欢迎信息:显示当前用户名
- 项目统计:
- 总项目数
- 活跃项目数
- 最近创建的项目
- 快捷操作:创建新项目、查看所有项目
- 系统状态:显示系统运行状态
### 5. 项目管理
- 项目列表:
- 表格形式展示
- 列:项目名称、项目描述、创建时间、操作
- 操作:查看、编辑、删除
- 搜索和筛选功能
- 分页功能
- 批量操作:批量删除
- 创建新项目按钮
### 6. 项目详情
- 画布区域:
- 以固定外框将html以iframe嵌套的方式显示出来
- iframe内的元素可在画布区域内选择和高亮
- 添加、删除、修改元素
- 拖拽功能
- 预览功能
- 左侧工具栏(绝对定位,距离屏幕左侧 10px顶部 10px抽取为单独组件
- 工具栏可展开/收起
- 原型库:查看项目内所有原型,点击弹窗显示详情
- 备注:添加、删除、修改项目元素的备注
- 资源管理:查看项目内所有资源,点击弹窗显示详情
- 全屏预览:查看设计稿的全屏预览
- 提示词库:查看项目内所有提示词,点击弹窗显示详情
- 右侧AI对话框绝对定位距离屏幕右侧10px底部10px抽取为单独组件
- 对话框可展开/收起
- 中间输出框显示AI回复
- 底部输入框用户输入问题AI回复会显示在输出框中
- 底部输入框按钮组(相对于底部输入框绝对定位,距离底部 10px
- 上传附件按钮:点击后调用浏览器上传文件功能,支持上传图片、doc、docx、pdf、PDF、MD等类型文件上传后显示在输出框中。
- 快捷命令按钮:点击后显示快捷命令,图标为“/”
- 语言输入图标:点击后切换语言,图标为“语言图标”
- 发送按钮点击后触发AI回复图标为“发送图标”
- 右侧顶部工具栏绝对定位距离屏幕右侧10px顶部10px抽取为单独组件
- 顶部工具栏可展开/收起
- 新建页面:提供新建画布的能力
- 保存页面:提供保存画布的能力
- 导入页面提供导入html创建画布的能力
- 导出页面提供导出为html、导出为XD文件的能力
### 7. 设置页面
- 个人信息:
- 用户名、邮箱、头像等
- 保存按钮
- 密码修改:
- 旧密码输入框
- 新密码输入框
- 确认新密码输入框
- 保存按钮
- 语言设置:
- 中文、英语切换
- 通知设置:
- 邮件通知开关
- 系统通知开关
详见design/Page.md
## 数据流转
- 认证流程:
@ -206,8 +103,7 @@ project-root/
- 路由:`npm install react-router-dom`
- HTTP 客户端:`npm install axios`
- 国际化:`npm install i18next react-i18next`
- 状态管理:`npm install @reduxjs/toolkit react-redux`(可选)
- 样式:`npm install tailwindcss postcss autoprefixer`(可选)
- 样式:`npm install tailwindcss postcss autoprefixer`
4. 配置:
- Tailwind CSS`npx tailwindcss init -p`
- ESLint 和 Prettier按照官方文档配置