chore(landing): update landing

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei
2025-11-11 22:52:35 +08:00
parent 2665778890
commit 16d78812ee
19 changed files with 1824 additions and 1511 deletions

View File

@@ -0,0 +1,341 @@
# 设计系统使用指南
## 🎨 颜色系统
### Pastel Palette Token自动适配暗色模式
```tsx
// ✅ 推荐:使用语义化 token
bg-background // 主背景
bg-background-secondary // 次级背景
bg-fill // 填充色
bg-fill-secondary // 次级填充
bg-material-medium // 材质:中等透明度
bg-accent // 强调色
text-text // 主文本
text-text-secondary // 次级文本
text-text-tertiary // 三级文本(最淡)
border-border // 边框色
```
### 自定义 Accent 混合色
```tsx
// 10% ~ 80% 的 accent 混合色
bg-accent-10 // 极淡强调色背景
bg-accent-20
bg-accent-30
...
bg-accent-80 // 强烈强调色
```
### ❌ 避免硬编码
```tsx
// ❌ 不要这样
className="bg-blue-500 text-white shadow-xl"
// ✅ 应该这样
className="bg-accent text-background shadow-medium"
```
## 📐 阴影系统
`lib/design-tokens.ts` 导入统一阴影:
```tsx
import { shadows } from '~/lib/design-tokens'
// 5 个标准层级
shadows.subtle // 轻微阴影 - 用于悬停效果
shadows.light // 轻度阴影 - 普通卡片
shadows.medium // 中度阴影 - 浮动面板
shadows.strong // 强阴影 - 模态框
shadows.heavy // 重阴影 - 全屏/Hero
```
### 使用示例
```tsx
// 普通卡片
<div className={clsxm('bg-background', shadows.light)}>...</div>
// 浮动面板
<div className={clsxm('bg-background/80 backdrop-blur-xl', shadows.medium)}>
...
</div>
// Hero 区块
<div className={shadows.heavy}>...</div>
```
## 🔲 圆角系统
```tsx
import { radius } from '~/lib/design-tokens'
radius.sm // rounded-xl (12px) - 小元素
radius.md // rounded-2xl (16px) - 按钮、输入框
radius.lg // rounded-3xl (24px) - 卡片
radius.xl // rounded-[28px] - 大卡片
radius['2xl'] // rounded-[32px] - Section 容器
radius['3xl'] // rounded-[40px] - Hero 级别
```
## 💨 模糊度系统
```tsx
import { blur } from '~/lib/design-tokens'
blur.sm // backdrop-blur-sm (4px)
blur.md // backdrop-blur-md (12px)
blur.lg // backdrop-blur-lg (16px)
blur.xl // backdrop-blur-xl (24px)
blur['2xl'] // backdrop-blur-2xl (40px)
blur['3xl'] // backdrop-blur-[60px]
```
## 🎴 Glassmorphic 卡片
### 预设变体
```tsx
import { glassCard } from '~/lib/design-tokens'
// 4 种预设风格
glassCard.default // bg-background/60 + border + backdrop-blur-xl
glassCard.elevated // bg-background/80 + backdrop-blur-2xl (更实)
glassCard.floating // bg-background/50 + backdrop-blur-[30px] (更透)
glassCard.gradient // border-white/15 + backdrop-blur-2xl (配合渐变)
```
### 组合使用
```tsx
import { glassCard, radius, shadows } from '~/lib/design-tokens'
<div className={clsxm(
glassCard.floating,
radius.lg,
shadows.medium,
'p-6'
)}>
</div>
```
## 📏 Typography
```tsx
import { typography } from '~/lib/design-tokens'
typography.hero // text-4xl sm:text-5xl lg:text-6xl font-semibold
typography.h1 // text-3xl lg:text-4xl font-semibold
typography.h2 // text-2xl lg:text-3xl font-semibold
typography.h3 // text-xl lg:text-2xl font-semibold
typography.body // text-base
typography.small // text-sm
typography.tiny // text-xs
typography.label // text-xs tracking-[0.3em] uppercase font-semibold
```
### 使用示例
```tsx
<h1 className={clsxm(typography.hero, 'text-white')}>
</h1>
<p className={clsxm(typography.label, 'text-text-secondary')}>
Section Label
</p>
```
## 📦 间距系统
```tsx
import { spacing } from '~/lib/design-tokens'
spacing.section // space-y-20 - Section 之间
spacing.content // space-y-12 - 内容组之间
spacing.group // space-y-6 - 组内元素
spacing.tight // space-y-3 - 紧密元素
```
## 🎯 图标容器
```tsx
import { iconBox } from '~/lib/design-tokens'
// 3 种尺寸,自带圆角、背景、居中
iconBox.sm // size-8 + rounded-xl
iconBox.md // size-10 + rounded-2xl
iconBox.lg // size-12 + rounded-2xl
// 使用示例
<span className={iconBox.lg}>
<i className="i-lucide-sparkles size-5" />
</span>
```
## ⚡ 过渡动画
```tsx
import { transition } from '~/lib/design-tokens'
transition.fast // duration-200
transition.normal // duration-300
transition.slow // duration-500
// 使用示例
<button className={clsxm('hover:bg-accent/90', transition.normal)}>
</button>
```
## 🎭 Hover 效果
```tsx
import { hover } from '~/lib/design-tokens'
hover.card // border 和 bg 变化
hover.lift // 轻微放大 + 阴影加强
hover.glow // 发光效果
```
## 🧩 组件使用模式
### 标准卡片模式
```tsx
import { Card } from '~/components/landing'
import { shadows } from '~/lib/design-tokens'
<Card variant="floating" size="lg" hoverable>
<div className="flex items-center gap-3">
{/* 内容 */}
</div>
</Card>
```
### Section 标准布局
```tsx
import { spacing, typography } from '~/lib/design-tokens'
<section className={spacing.content}>
<header className={spacing.tight}>
<p className={clsxm(typography.label, 'text-text-secondary')}>
Section Label
</p>
<h2 className={clsxm(typography.h1, 'text-white')}>
</h2>
<p className="text-base text-text-secondary">
</p>
</header>
{/* Section 内容 */}
</section>
```
## 🌈 渐变组合
```tsx
// Hero 渐变背景
<div className="bg-gradient-to-br from-accent/40 via-purple-600/40 to-slate-900/70">
...
</div>
// 文字渐变
<span className="bg-gradient-to-r from-sky-300 via-accent to-purple-400 bg-clip-text text-transparent">
</span>
```
## 📋 完整示例
### 功能卡片
```tsx
import { Card } from '~/components/landing'
import { shadows, radius, spacing, typography, iconBox } from '~/lib/design-tokens'
import { clsxm } from '~/lib/helper'
const FeatureCard = () => (
<Card variant="floating" size="lg" className={spacing.group}>
<div className="flex items-center gap-3">
<span className={clsxm(iconBox.lg, 'bg-accent/15 text-accent')}>
<i className="i-lucide-cpu size-5" />
</span>
<div>
<p className={clsxm(typography.h3, 'text-white')}>
</p>
<p className={clsxm(typography.small, 'text-text-secondary')}>
</p>
</div>
</div>
<ul className={clsxm(spacing.tight, typography.small, 'text-text-secondary')}>
<li className="flex items-start gap-2">
<i className="i-lucide-check size-4 text-accent" />
<span> 1</span>
</li>
</ul>
</Card>
)
```
## 🎨 颜色使用场景指南
| 场景 | 推荐颜色 | 示例 |
|------|----------|------|
| 页面背景 | `bg-background` | `<div className="bg-background">` |
| 卡片背景 | `bg-background/60` ~ `bg-background/80` | 玻璃态透明度 |
| 按钮主色 | `bg-accent text-background` | CTA 按钮 |
| 按钮次级 | `bg-fill text-text` | 次要操作 |
| 强调文字 | `text-accent` | 重要信息 |
| 边框 | `border-border``border-white/10` | 分隔线 |
| 悬浮蒙层 | `bg-background/50 backdrop-blur-xl` | Modal 背景 |
## 🚫 反模式(避免)
```tsx
// ❌ 硬编码阴影
className="shadow-[0_20px_60px_rgba(0,0,0,0.35)]"
// ✅ 使用 token
className={shadows.heavy}
// ❌ 重复的圆角定义
className="rounded-[32px]"
className="rounded-[32px]"
// ✅ 统一使用
className={radius['2xl']}
// ❌ 分散的透明度值
className="bg-white/5"
className="bg-white/8"
className="bg-white/12"
// ✅ 语义化背景
className={glassCard.floating}
```
## 📚 参考资源
- **Pastel Palette**: 颜色系统基础
- **Tailwind CSS v4**: 工具类参考
- **Glassmorphic Design**: UI 设计理念
- **Motion/Framer Motion**: 动画系统
---
**最后更新**: 2025-11-11
**维护者**: Design System Team

270
apps/landing/QUICK_START.md Normal file
View File

@@ -0,0 +1,270 @@
# 快速开始Landing Page 开发指南
## 🚀 5 分钟上手
### 1. 添加新 Section
```tsx
// 1⃣ 创建组件文件components/landing/MySection.tsx
import { spacing, typography } from '~/lib/design-tokens'
import { clsxm } from '~/lib/helper'
import { Card } from './Card'
export const MySection = () => (
<section className={spacing.content}>
<header className={spacing.tight}>
<p className={clsxm(typography.label, 'text-text-secondary')}>
</p>
<h2 className={clsxm(typography.h1, 'text-white')}>
</h2>
</header>
<Card variant="floating" size="lg">
</Card>
</section>
)
// 2⃣ 在 index.ts 导出
export { MySection } from './MySection'
// 3⃣ 在 page.tsx 使用
import { MySection } from '~/components/landing'
export default function Home() {
return (
<div className="...">
<BackgroundDecor />
<div className="...">
{/* ...其他 sections */}
<MySection />
</div>
</div>
)
}
```
### 2. 创建自定义卡片
```tsx
import { Card } from '~/components/landing'
import { iconBox, shadows } from '~/lib/design-tokens'
import { clsxm } from '~/lib/helper'
const ProductCard = ({ icon, title, price }) => (
<Card variant="elevated" size="md" hoverable>
<div className="flex items-center gap-3">
<span className={clsxm(iconBox.lg, 'bg-accent/15 text-accent')}>
<i className={icon} />
</span>
<div className="flex-1">
<h3 className="text-lg font-semibold text-white">{title}</h3>
<p className="text-sm text-text-secondary">{price}</p>
</div>
</div>
</Card>
)
```
### 3. 自定义颜色和阴影
```tsx
import { shadows, radius, blur, glassCard } from '~/lib/design-tokens'
// 组合使用
<div className={clsxm(
glassCard.floating, // 玻璃态背景
radius.lg, // 圆角
shadows.medium, // 阴影
'p-6' // 内边距
)}>
</div>
// 自定义透明度
<div className={clsxm(
'bg-background/70', // 70% 不透明度
blur.xl, // 模糊度
'border border-white/10'
)}>
</div>
```
## 🎨 常用模式
### Pattern 1: 带图标的功能列表
```tsx
const features = [
{ icon: 'i-lucide-zap', title: '快速', desc: '毫秒级响应' },
{ icon: 'i-lucide-shield', title: '安全', desc: '企业级加密' },
]
<div className="space-y-4">
{features.map(item => (
<IconCard
key={item.title}
icon={item.icon}
title={item.title}
description={item.desc}
/>
))}
</div>
```
### Pattern 2: 网格布局的卡片
```tsx
import { FeatureCard } from '~/components/landing'
<div className="grid gap-6 lg:grid-cols-2">
{featureGroups.map(group => (
<FeatureCard
key={group.title}
icon={group.icon}
title={group.title}
description={group.description}
bullets={group.bullets}
/>
))}
</div>
```
### Pattern 3: Hero 渐变背景
```tsx
<div className={clsxm(
'relative overflow-hidden',
radius['3xl'],
shadows.heavy,
'bg-gradient-to-br from-accent/40 via-purple-600/40 to-slate-900/70',
'p-10 text-white'
)}>
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_top,_rgba(255,255,255,0.25),_transparent_55%)] opacity-80" />
<div className="relative space-y-6">
</div>
</div>
```
### Pattern 4: 统计指标条
```tsx
import { MetricCard } from '~/components/landing'
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<MetricCard label="用户" value="10k+" detail="活跃用户" />
<MetricCard label="性能" value="99.9%" detail="可用性" />
{/* 更多指标 */}
</div>
```
## 🎯 设计 Token 速查
### 快速导入
```tsx
import {
shadows, // 阴影
radius, // 圆角
blur, // 模糊
glassCard, // 玻璃态卡片
typography, // 文字层级
spacing, // 间距
iconBox, // 图标容器
transition, // 过渡动画
} from '~/lib/design-tokens'
```
### 常用组合
```tsx
// 玻璃态浮动卡片
className={clsxm(glassCard.floating, radius.lg, shadows.medium)}
// 标题
className={clsxm(typography.h1, 'text-white')}
// Section 间距
className={spacing.content}
// 图标容器
className={clsxm(iconBox.lg, 'bg-accent/15 text-accent')}
```
## 📝 开发检查清单
在提交代码前,确保:
- [ ] 使用了设计 token避免硬编码
- [ ] 单个组件文件 < 500 行
- [ ] 导出的组件已添加到 `index.ts`
- [ ] 使用了语义化的颜色类(`bg-background` 而非 `bg-white`
- [ ] 阴影使用 `shadows.*` token
- [ ] 圆角使用 `radius.*` token
- [ ] 运行 `pnpm lint` 无错误
## 🐛 常见问题
### Q: Linter 提示 `bg-background` 不是有效的 Tailwind 类?
**A**: 这是正常的。`bg-background` 等类来自 Pastel Palette在运行时有效。可以忽略这个警告。
### Q: 如何修改全局阴影样式?
**A**: 编辑 `lib/design-tokens.ts` 中的 `shadows` 对象:
```tsx
export const shadows = {
medium: 'shadow-[0_8px_32px_rgba(0,0,0,0.12)]', // 修改这里
}
```
### Q: 如何添加新的卡片变体?
**A**: 在 `components/landing/Card.tsx` 中扩展:
```tsx
interface CardProps {
variant?: 'default' | 'elevated' | 'floating' | 'gradient' | 'custom'
// ^^^^^^ 新增
}
// 添加到 glassCard 定义
export const glassCard = {
// ...existing
custom: 'bg-custom-color border-custom backdrop-blur-lg',
}
```
### Q: 如何实现暗色模式?
**A**: Pastel Palette 已自动支持。在根元素添加 `data-color-mode` 属性:
```tsx
<html data-color-mode="dark">
{/* 所有颜色 token 自动切换 */}
</html>
```
## 🔗 相关文档
- [重构详情](./REFACTOR.md) - 查看重构历史和对比
- [设计系统](./DESIGN_SYSTEM.md) - 完整的 token 参考
- [AGENTS.md](./AGENTS.md) - AI 开发指南
- [主项目 README](../../README.md) - 项目整体架构
## 💡 提示
1. **优先复用组件**:先查看 `components/landing/` 是否有类似组件
2. **保持一致性**:新组件应遵循现有的设计模式
3. **命名规范**:组件以 `Section``Card` 结尾
4. **Props 类型**:始终定义 TypeScript 接口
---
Happy Coding! 🚀

199
apps/landing/REFACTOR.md Normal file
View File

@@ -0,0 +1,199 @@
# Landing Page 重构文档
## 📋 重构概览
这次重构优化了 Landing Page 的代码结构、设计系统一致性和可维护性。
## 🎨 设计系统统一
### 新增:`lib/design-tokens.ts`
创建了统一的设计 token 系统,包括:
- **阴影层级**`shadows.subtle``shadows.heavy`5 个层级)
- **圆角系统**`radius.sm``radius['3xl']`6 个尺寸)
- **模糊度**`blur.sm``blur['3xl']`6 个级别)
- **Glassmorphic 卡片样式**`glassCard.default/elevated/floating/gradient`
- **内阴影**`innerShadow.subtle/medium/strong`
- **过渡动画**`transition.fast/normal/slow`
- **Typography 层级**`typography.hero/h1/h2/h3/body/small/tiny/label`
- **间距系统**`spacing.section/content/group/tight`
- **图标容器**`iconBox.sm/md/lg`
### 使用示例
```tsx
import { shadows, radius, blur, glassCard } from '~/lib/design-tokens'
// 统一的玻璃态卡片
<div className={clsxm(glassCard.floating, radius.lg, shadows.medium)}>
...
</div>
```
## 🧩 组件拆分
### 原文件结构
- `page.tsx`680 行单文件,包含所有组件和数据
### 新文件结构
```
components/landing/
├── index.ts # 统一导出
├── BackgroundDecor.tsx # 背景装饰层
├── Card.tsx # 通用卡片组件4 个变体)
├── HeroSection.tsx # Hero 区块
├── MetricStrip.tsx # 指标条
├── PreviewSection.tsx # 预览区块
├── FeatureSection.tsx # 功能区块
├── WorkflowSection.tsx # 工作流区块
├── TechSection.tsx # 技术栈区块
└── CTASection.tsx # CTA 行动召唤区块
```
### 主页面(`page.tsx`
- **重构前**680 行
- **重构后**29 行(仅组合导入的组件)
## 🎯 核心改进
### 1. 颜色系统规范化
**重构前**
```tsx
// ❌ 硬编码颜色和不一致的阴影
className="shadow-[0_20px_60px_rgba(0,0,0,0.35)] bg-white/5"
className="shadow-[0_25px_80px_rgba(0,0,0,0.35)]"
className="shadow-[0_30px_80px_rgba(0,0,0,0.35)]"
```
**重构后**
```tsx
// ✅ 使用统一的设计 token
className={shadows.heavy}
className={shadows.medium}
```
### 2. 阴影层级统一
所有阴影现在遵循 5 个标准层级:
- `subtle`:轻微阴影(卡片悬停)
- `light`:轻度阴影(普通卡片)
- `medium`:中度阴影(浮动面板)
- `strong`:强阴影(模态框)
- `heavy`:重阴影(全屏覆盖)
### 3. 卡片组件抽象
创建了 4 个通用卡片组件:
#### `Card`(基础卡片)
```tsx
<Card variant="floating" size="md" hoverable>
</Card>
```
#### `IconCard`(带图标的卡片)
```tsx
<IconCard
icon="i-lucide-aperture"
title="EXIF HUD"
description="完整记录光圈、快门..."
meta="f/1.4 · 1/125s"
/>
```
#### `FeatureCard`(功能卡片)
```tsx
<FeatureCard
icon="i-lucide-cpu"
title="性能与体验"
description="WebGL viewer..."
bullets={['GPU 管线渲染', '...更多']}
/>
```
#### `MetricCard`(指标卡片)
```tsx
<MetricCard
label="WebGL 渲染"
value="60fps"
detail="平移 · 缩放 · HDR"
/>
```
## 📦 组件拆分原则
遵循项目规则:
1.**每个文件 < 500 行**
2.**避免重复代码**(提取通用 Card 组件)
3.**单一职责**(每个 Section 组件只负责一个区块)
4.**可复用性**(设计 token 可在整个项目中使用)
## 🔄 迁移指南
### 如果需要修改样式:
**重构前**:在每个组件中搜索硬编码的阴影/圆角值
**重构后**:只需修改 `lib/design-tokens.ts`
### 如果需要添加新区块:
```tsx
// 1. 创建新组件文件
// components/landing/NewSection.tsx
import { Card } from './Card'
import { shadows, radius } from '~/lib/design-tokens'
export const NewSection = () => (
<section>
<Card variant="floating" className={shadows.medium}>
...
</Card>
</section>
)
// 2. 在 index.ts 导出
export { NewSection } from './NewSection'
// 3. 在 page.tsx 引入使用
import { NewSection } from '~/components/landing'
```
## 🎨 设计哲学
### Glassmorphic Depth 原则
所有组件都遵循「玻璃态深度设计」:
1. **分层透明度**`bg-background/60``bg-background/80`
2. **模糊背景**`backdrop-blur-xl``backdrop-blur-3xl`
3. **精细边框**`border border-white/10`
4. **柔和阴影**:使用 `shadows.*` token
5. **内阴影点缀**`innerShadow.subtle` 营造深度
## 📊 重构效果
| 指标 | 重构前 | 重构后 | 改进 |
|------|--------|--------|------|
| 主文件行数 | 680 行 | 29 行 | ↓ 95.7% |
| 组件文件数 | 1 个 | 10 个 | 模块化 |
| 重复样式 | ~15 处 | 0 处 | 消除重复 |
| 阴影定义 | 12+ 种 | 5 种 | 统一规范 |
| 可维护性 | ⭐⭐ | ⭐⭐⭐⭐⭐ | 显著提升 |
## 🚀 后续优化建议
1. **响应式优化**:将断点也抽取为 token`breakpoints.*`
2. **暗色模式**:基于 Pastel Palette 的 `data-color-mode` 扩展
3. **动画库**:将 motion 动画参数也抽取为 token
4. **性能监控**:添加 React DevTools Profiler 标记
## 📚 相关文档
- 设计系统规范:`apps/landing/AGENTS.md`
- Pastel Palette 文档:`@pastel-palette/tailwindcss`
- 项目整体架构:根目录 `README.md`

View File

@@ -11,7 +11,7 @@ const isProd = process.env.NODE_ENV === 'production'
let nextConfig: NextConfig = {
reactStrictMode: false,
productionBrowserSourceMaps: true,
output: 'standalone',
output: 'export',
assetPrefix: isProd ? env.ASSETPREFIX || undefined : undefined,
compiler: {
// reactRemoveProperties: { properties: ['^data-id$', '^data-(\\w+)-id$'] },

View File

@@ -55,8 +55,8 @@
"@tailwindcss/typography": "0.5.19",
"@tanstack/react-query-devtools": "5.90.2",
"@types/node": "24.10.0",
"@types/react": "npm:types-react@19.0.0-rc.1",
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
"@types/react": "19.2.2",
"@types/react-dom": "19.2.2",
"autoprefixer": "10.4.22",
"code-inspector-plugin": "1.2.10",
"cross-env": "10.1.0",
@@ -78,8 +78,6 @@
},
"pnpm": {
"overrides": {
"@types/react": "npm:types-react@19.0.0-rc.1",
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
"array-includes": "npm:@nolyfill/array-includes@latest",
"array.prototype.findlastindex": "npm:@nolyfill/array.prototype.findlastindex@latest",
"array.prototype.flat": "npm:@nolyfill/array.prototype.flat@latest",

View File

@@ -14,8 +14,6 @@ import { InitInClient } from './InitInClient'
init()
export const revalidate = 60
export function generateViewport(): Viewport {
return {
themeColor: [

View File

@@ -1,211 +1,20 @@
'use client'
'use client'
import Link from 'next/link'
import { m } from 'motion/react'
import { Button } from '~/components/ui/button/Button'
import { clsxm } from '~/lib/helper'
const heroHighlights = [
{ title: 'WebGL Viewer', description: '60fps 缩放 / 漫游 / HDR' },
{ title: 'Manifest 驱动', description: 'window.__MANIFEST__ 即时注入' },
{ title: '地图探索', description: 'MapLibre GPS / Cluster / Heatmap' },
]
const metrics = [
{ label: 'WebGL 渲染', value: '60fps', detail: '平移 · 缩放 · HDR' },
{ label: '增量同步', value: 'S3 · GitHub', detail: '多存储后端' },
{ label: '照片节点', value: '2k+', detail: 'EXIF · Live Photo · Blurhash' },
{ label: '多语言', value: '11', detail: 'i18n · 动态 OG' },
]
const previewHighlights = [
{
icon: 'i-lucide-aperture',
title: 'EXIF HUD',
description: '完整记录光圈、快门、ISO、镜头、配方、HDR 信息。',
meta: 'f/1.4 · 1/125s · ISO 200',
},
{
icon: 'i-lucide-map',
title: '地图探索',
description: 'MapLibre 地图、GPS 聚合、热力探索每一次旅程。',
meta: '84% 带 GPS · Cluster & Pin',
},
{
icon: 'i-lucide-maximize',
title: 'Fullscreen Viewer',
description: 'WebGL 全屏查看器支持手势、Live Photo 与分享。',
meta: '手势 · 分享 · Live',
},
]
const featureGroups = [
{
icon: 'i-lucide-cpu',
title: '性能与体验',
description: 'WebGL viewer、Masonry 布局与全屏手势带来原生级互动。',
bullets: [
'GPU 管线渲染 · Free transform · Tone mapping',
'Blurhash 占位、响应式断点与浮动操作面板',
'Glassmorphic Depth 系统 + Motion spring 动画',
],
},
{
icon: 'i-lucide-database',
title: '数据与同步',
description: 'builder 多进程处理 + manifest 驱动数据流,自动增量同步。',
bullets: [
'S3 / GitHub / Eagle / 本地多源存储抽象',
'EXIF、Live Photo、Fujifilm Recipe 与缩略图生成',
'window.__MANIFEST__ 注入,前端无感刷新',
],
},
{
icon: 'i-lucide-plug',
title: '接入模式',
description: 'SPA、Next.js SSR、be/apps/core 后端三种模式自由切换。',
bullets: [
'静态/SSR 共享 UIapps/web + apps/ssr',
'be/apps/core 以 Hono + Drizzle 提供实时能力',
'OG 渲染 / SEO 元数据 / OpenGraph API',
],
},
{
icon: 'i-lucide-globe',
title: '全球化 & 分享',
description: '多语言、OG、分享嵌入天然适合出海作品集。',
bullets: [
'i18next 平台化11+ 语言文件',
'动态 OG 图 + /og/[photoId] API',
'分享/嵌入组件、一键复制链接或 iframe',
],
},
]
const workflowSteps = [
{
icon: 'i-lucide-rocket',
title: 'Builder Pipeline',
description: 'packages/builder 负责拉取、处理、分析与生成 manifest。',
points: [
'Storage SyncS3 / GitHub / Eagle 增量拉取',
'Format & ThumbnailHEIC/TIFF 转码 + Blurhash',
'EXIF / GPS / Fujifilm Recipe / HDR 元数据提取',
],
},
{
icon: 'i-lucide-plug-zap',
title: 'Manifest 注入',
description: 'apps/ssr 在 Next.js 层注入 window.__MANIFEST__开启 SEO/OG。',
points: [
'index.html 模板内联 manifest',
'OG 动态渲染 + Metadata 替换',
'MapLibre、PhotoLoader、Viewer 共享数据源',
],
},
{
icon: 'i-lucide-monitor-play',
title: 'SPA 消费',
description: 'apps/web 作为 Vite SPA自主渲染 Masonry、Viewer、地图与浮动 UI。',
points: [
'PhotoLoader 单例在客户端初始化',
'Jotai + TanStack Query 状态与数据层',
'Glassmorphic Depth 组件交付最终体验',
],
},
]
const techStacks = [
'React 19',
'Next.js 15',
'Vite',
'Tailwind CSS v4',
'Radix UI',
'Framer Motion 12',
'Jotai',
'TanStack Query',
'MapLibre',
'Sharp',
'Drizzle ORM',
'Hono',
]
const integrationModes = [
{
title: 'Standalone SPA',
description: '预构建 photos-manifest.json纯静态部署即可运行。',
points: [
'Vite + Cloudflare Pages / Vercel / 静态空间',
'CI 中运行 builder增量更新 manifest',
'无需服务器即可获得完整体验',
],
},
{
title: 'Next.js SSR Host',
description: 'apps/ssr 作为 SPA 外壳,负责注入 manifest、OG、SEO。',
points: [
'route.ts 捕获所有路径,返回注入后的 index.html',
'动态 /og/[photoId] 渲染社交卡片',
'保留 SPA 的交互,同时具备 SSR 首屏',
],
},
{
title: 'Full Backend Flow',
description: 'be/apps/core + be/apps/dashboard 驱动实时数据与管理。',
points: [
'Hono + Drizzle + PostgreSQL 全栈能力',
'Manifest 在内存构建后注入页面',
'Dashboard 管理、权限与指标监控',
],
},
]
const heroTiles = [
{
id: 'tile-masonry',
title: 'Masonry Flow',
subtitle: '自适应列 · Blurhash 过渡',
badge: 'GPU',
className: 'col-span-2 row-span-2',
gradient:
'linear-gradient(135deg, rgba(15,76,129,0.65), rgba(0,122,255,0.35))',
},
{
id: 'tile-exif',
title: 'EXIF',
subtitle: 'Fujifilm Recipe · HDR',
badge: 'metadata',
className: 'col-span-1 row-span-1',
gradient:
'linear-gradient(135deg, rgba(47,46,113,0.85), rgba(130,124,252,0.25))',
},
{
id: 'tile-map',
title: 'Map Explorer',
subtitle: 'GPS Cluster / Heatmap',
badge: 'MapLibre',
className: 'col-span-1 row-span-2',
gradient:
'linear-gradient(135deg, rgba(0,150,136,0.8), rgba(0,212,255,0.3))',
},
{
id: 'tile-viewer',
title: 'Fullscreen Viewer',
subtitle: 'Live Photo / 分享',
badge: 'viewer',
className: 'col-span-2 row-span-1',
gradient:
'linear-gradient(135deg, rgba(255,87,34,0.65), rgba(255,193,7,0.25))',
},
]
import { BackgroundDecor } from '~/components/landing/BackgroundDecor'
import { CTASection } from '~/components/landing/CTASection'
import { FeatureSection } from '~/components/landing/FeatureSection'
import { HeroSection } from '~/components/landing/HeroSection'
import { MetricStrip } from '~/components/landing/MetricStrip'
import { PreviewSection } from '~/components/landing/PreviewSection'
import { TechSection } from '~/components/landing/TechSection'
import { WorkflowSection } from '~/components/landing/WorkflowSection'
export default function Home() {
return (
<div className="relative isolate overflow-hidden bg-background pb-32 text-text">
<div className="bg-background text-text relative isolate overflow-hidden pb-32">
<BackgroundDecor />
<div className="relative z-10 mx-auto flex w-full max-w-6xl flex-col gap-20 px-4 pb-16 pt-12 sm:px-6 lg:px-0">
<div className="relative z-10 mx-auto flex w-full max-w-6xl flex-col gap-20 px-4 pt-12 pb-16 sm:px-6 lg:px-0">
<HeroSection />
<MetricStrip />
<PreviewSection />
@@ -217,421 +26,3 @@ export default function Home() {
</div>
)
}
const HeroSection = () => (
<section className="relative flex flex-col gap-12 lg:flex-row lg:items-center">
<div className="flex flex-1 flex-col gap-8">
<span className="inline-flex w-fit items-center gap-2 rounded-full border border-white/10 bg-white/5 px-4 py-1 text-sm text-accent/90 backdrop-blur">
<i className="i-lucide-sparkles size-4" aria-hidden />
Glassmorphic Depth Design System
</span>
<div className="space-y-5">
<h1 className="text-4xl font-semibold leading-tight text-white sm:text-5xl lg:text-6xl">
Afilmory
<span className="block bg-gradient-to-r from-sky-300 via-accent to-purple-400 bg-clip-text text-transparent">
</span>
</h1>
<p className="max-w-2xl text-base text-text-secondary sm:text-lg">
WebGLMotion manifest EXIF
MapLibre Next.js SEO / OG
</p>
</div>
<div className="flex flex-wrap items-center gap-3">
<Button
asChild
className="min-w-[160px] bg-gradient-to-r from-sky-400 to-accent text-background shadow-lg shadow-accent/30"
>
<Link href="https://afilmory.innei.in" target="_blank" rel="noreferrer">
</Link>
</Button>
<Button asChild variant="secondary">
<Link
href="https://github.com/Afilmory/photo-gallery-site#-features"
target="_blank"
rel="noreferrer"
>
README
</Link>
</Button>
<Link
href="https://github.com/Afilmory/photo-gallery-site"
target="_blank"
rel="noreferrer"
className="group inline-flex items-center gap-1.5 text-sm text-text-secondary transition hover:text-text"
>
<span>Star on GitHub</span>
<i className="i-lucide-arrow-up-right size-4 transition group-hover:translate-x-0.5 group-hover:-translate-y-0.5" />
</Link>
</div>
<ul className="mt-4 flex flex-wrap gap-6 text-sm text-text-secondary sm:text-base">
{heroHighlights.map((item) => (
<li key={item.title} className="flex items-center gap-2">
<span className="size-2 rounded-full bg-accent/70 shadow-[0_0_12px_rgba(0,122,255,0.8)]" />
<div>
<p className="font-medium text-text">{item.title}</p>
<p>{item.description}</p>
</div>
</li>
))}
</ul>
</div>
<HeroPreview />
</section>
)
const HeroPreview = () => (
<div className="relative flex flex-1 justify-center">
<div className="relative w-full max-w-xl">
<m.div
className="grid grid-cols-3 gap-3"
initial={{ opacity: 0, y: 32 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, ease: 'easeOut' }}
>
{heroTiles.map((tile, index) => (
<m.div
key={tile.id}
className={clsxm(
'group relative overflow-hidden rounded-[28px] border border-white/15 p-4 text-white shadow-[0_20px_60px_rgba(0,0,0,0.35)] backdrop-blur-2xl',
tile.className,
)}
style={{
backgroundImage: tile.gradient,
}}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 + index * 0.08, duration: 0.6, ease: 'easeOut' }}
>
<div className="flex items-center justify-between text-xs uppercase tracking-wide text-white/80">
<span>{tile.badge}</span>
<i className="i-lucide-chevrons-up size-4 text-white/70" aria-hidden />
</div>
<p className="mt-6 text-lg font-semibold">{tile.title}</p>
<p className="text-sm text-white/80">{tile.subtitle}</p>
<div className="pointer-events-none absolute inset-x-4 bottom-4 h-1 rounded-full bg-white/30 opacity-0 blur-lg transition duration-500 group-hover:opacity-100" />
</m.div>
))}
</m.div>
<m.div
aria-hidden
className="absolute -right-6 top-4 w-56 rounded-3xl border border-white/10 bg-background/80 p-4 text-sm text-text shadow-2xl backdrop-blur-2xl"
initial={{ opacity: 0, y: -20, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
transition={{ delay: 0.35, duration: 0.6, ease: 'easeOut' }}
>
<div className="flex items-center justify-between text-xs font-medium text-text-secondary">
window.__MANIFEST__
<span className="rounded-full bg-accent/20 px-2 py-0.5 text-[10px] text-accent">
hydrated
</span>
</div>
<div className="mt-3 space-y-2 font-mono text-[11px] leading-relaxed text-text-secondary">
<p>{'{ data: 2048 photos }'}</p>
<p>{'cameras: 12 · lenses: 18'}</p>
<p>{'blurhash: true · livePhoto: 86'}</p>
</div>
</m.div>
<m.div
aria-hidden
className="absolute -left-6 bottom-6 w-60 rounded-3xl border border-white/10 bg-gradient-to-br from-emerald-500/20 via-background to-background/80 p-4 text-sm shadow-xl backdrop-blur-2xl"
initial={{ opacity: 0, y: 20, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
transition={{ delay: 0.45, duration: 0.6, ease: 'easeOut' }}
>
<div className="flex items-center gap-2 text-xs font-semibold text-emerald-200">
<i className="i-lucide-map-pin size-4" />
Map Explorer
</div>
<p className="mt-3 text-xs text-white/80">242 geotagged stories online.</p>
<div className="mt-4 flex items-center justify-between text-[11px] text-white/70">
<span>Cluster · Heatmap</span>
<span> fully interactive</span>
</div>
</m.div>
</div>
</div>
)
const MetricStrip = () => (
<section>
<m.div
className="grid gap-4 rounded-[32px] border border-white/10 bg-white/5 p-6 text-sm text-text-secondary shadow-[0_25px_80px_rgba(0,0,0,0.35)] backdrop-blur-[40px] sm:grid-cols-2 lg:grid-cols-4"
initial={{ opacity: 0, y: 24 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, amount: 0.4 }}
transition={{ duration: 0.7, ease: 'easeOut' }}
>
{metrics.map((metric) => (
<div
key={metric.label}
className="rounded-2xl border border-white/5 bg-background/60 px-4 py-5 shadow-inner shadow-black/20"
>
<p className="text-xs uppercase tracking-widest text-text-tertiary">{metric.label}</p>
<p className="mt-3 text-2xl font-semibold text-white">{metric.value}</p>
<p>{metric.detail}</p>
</div>
))}
</m.div>
</section>
)
const PreviewSection = () => (
<section className="grid gap-12 lg:grid-cols-[1.1fr_0.9fr] lg:items-center">
<div className="space-y-6">
<p className="text-sm font-semibold uppercase tracking-[0.3em] text-text-secondary">
</p>
<h2 className="text-3xl font-semibold text-white"></h2>
<p className="text-base text-text-secondary">
Masonry MapLibre WebGL ViewerEXIF HUD
motion spring glassmorphic
</p>
<div className="space-y-4">
{previewHighlights.map((item) => (
<div
key={item.title}
className="group rounded-3xl border border-white/10 bg-background/50 px-5 py-4 shadow-[inset_0_1px_0_rgba(255,255,255,0.08)] backdrop-blur-2xl transition hover:border-accent/40 hover:bg-background/80"
>
<div className="flex items-center gap-3 text-text">
<span className="flex size-10 items-center justify-center rounded-2xl bg-accent/15 text-accent">
<i className={clsxm('size-5', item.icon)} aria-hidden />
</span>
<div className="flex-1">
<p className="text-lg font-medium">{item.title}</p>
<p className="text-sm text-text-secondary">{item.description}</p>
</div>
<span className="text-xs text-text-tertiary">{item.meta}</span>
</div>
</div>
))}
</div>
</div>
<div className="relative">
<div className="absolute inset-x-8 -top-10 -bottom-10 rounded-[40px] bg-gradient-to-b from-white/10 via-white/5 to-transparent blur-3xl" />
<div className="relative rounded-[36px] border border-white/15 bg-white/5 p-6 shadow-[0_25px_80px_rgba(0,0,0,0.45)] backdrop-blur-[35px]">
<div className="flex items-center justify-between text-xs text-text-secondary">
<div className="flex items-center gap-2">
<span className="size-2 rounded-full bg-red-400" />
<span className="size-2 rounded-full bg-yellow-400" />
<span className="size-2 rounded-full bg-emerald-400" />
</div>
<span>gallery.mxte.cc</span>
</div>
<div className="mt-6 grid grid-cols-3 gap-3">
{Array.from({ length: 9 }).map((_, index) => (
<div
key={index}
className="h-24 rounded-2xl bg-gradient-to-br from-white/20 via-white/5 to-white/0 shadow-inner shadow-black/40"
/>
))}
</div>
<div className="mt-6 rounded-2xl border border-white/10 bg-background/70 p-4 text-sm text-text-secondary">
<div className="flex items-center justify-between text-text">
<span className="font-medium">EXIF · X-T5 · XF16mmF1.4</span>
<span className="text-xs text-text-tertiary">Map locked</span>
</div>
<p className="mt-2 text-text-secondary">
GPS 35.6895 / 139.6917 · Fujifilm Classic Chrome · Blurhash ready.
</p>
</div>
</div>
</div>
</section>
)
const FeatureSection = () => (
<section className="space-y-6">
<header className="space-y-3">
<p className="text-sm font-semibold uppercase tracking-[0.3em] text-text-secondary">
</p>
<h2 className="text-3xl font-semibold text-white"></h2>
<p className="max-w-3xl text-base text-text-secondary">
Performance / Data / Integrations / Global Experience
便
</p>
</header>
<div className="grid gap-6 lg:grid-cols-2">
{featureGroups.map((group) => (
<div
key={group.title}
className="flex flex-col gap-4 rounded-[32px] border border-white/10 bg-background/50 p-6 shadow-[0_20px_60px_rgba(0,0,0,0.35)] backdrop-blur-[30px]"
>
<div className="flex items-center gap-3">
<span className="flex size-12 items-center justify-center rounded-2xl bg-accent/15 text-accent">
<i className={clsxm('size-5', group.icon)} aria-hidden />
</span>
<div>
<p className="text-xl font-semibold text-white">{group.title}</p>
<p className="text-sm text-text-secondary">{group.description}</p>
</div>
</div>
<ul className="space-y-2 text-sm text-text-secondary">
{group.bullets.map((point) => (
<li key={point} className="flex items-start gap-2">
<i className="i-lucide-check size-4 text-accent" aria-hidden />
<span>{point}</span>
</li>
))}
</ul>
</div>
))}
</div>
</section>
)
const WorkflowSection = () => (
<section className="space-y-8">
<div>
<p className="text-sm font-semibold uppercase tracking-[0.3em] text-text-secondary">
</p>
<h2 className="text-3xl font-semibold text-white">Builder Manifest SPA</h2>
<p className="text-base text-text-secondary">
README 线
</p>
</div>
<div className="relative rounded-[36px] border border-white/10 bg-white/5 p-8 shadow-[0_30px_80px_rgba(0,0,0,0.35)] backdrop-blur-[45px]">
<div className="absolute left-12 top-10 bottom-10 w-px bg-white/10" />
<ol className="space-y-10">
{workflowSteps.map((step, index) => (
<li key={step.title} className="relative pl-16">
<div className="absolute left-0 top-1 flex size-12 items-center justify-center rounded-full border border-accent/20 bg-accent/10 text-accent">
<i className={clsxm('size-5', step.icon)} aria-hidden />
</div>
<div>
<p className="text-lg font-semibold text-white">
{index + 1}. {step.title}
</p>
<p className="mt-2 text-sm text-text-secondary">{step.description}</p>
<ul className="mt-3 space-y-2 text-sm text-text-secondary">
{step.points.map((point) => (
<li key={point} className="flex items-start gap-2">
<span className="mt-1 size-1.5 rounded-full bg-accent" />
<span>{point}</span>
</li>
))}
</ul>
</div>
</li>
))}
</ol>
</div>
</section>
)
const TechSection = () => (
<section className="space-y-10">
<div className="space-y-3">
<p className="text-sm font-semibold uppercase tracking-[0.3em] text-text-secondary">
&
</p>
<h2 className="text-3xl font-semibold text-white"></h2>
<p className="text-base text-text-secondary">
React 19 + Next.js 15 + Vite + motion + Tailwind v4 + Pastel Palette token
UI / /
</p>
</div>
<div className="rounded-[32px] border border-white/10 bg-background/40 p-6 shadow-[0_25px_70px_rgba(0,0,0,0.4)] backdrop-blur-[30px]">
<div className="flex flex-wrap gap-3">
{techStacks.map((stack, index) => (
<m.span
key={stack}
className="rounded-full border border-white/15 bg-white/5 px-4 py-2 text-sm text-text-secondary"
initial={{ opacity: 0, y: 12 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: index * 0.05, duration: 0.4 }}
>
{stack}
</m.span>
))}
</div>
<div className="mt-8 grid gap-4 lg:grid-cols-3">
{integrationModes.map((mode) => (
<div
key={mode.title}
className="rounded-3xl border border-white/10 bg-white/5 p-5 shadow-inner shadow-black/30"
>
<p className="text-lg font-semibold text-white">{mode.title}</p>
<p className="mt-1 text-sm text-text-secondary">{mode.description}</p>
<ul className="mt-3 space-y-2 text-sm text-text-secondary">
{mode.points.map((point) => (
<li key={point} className="flex items-start gap-2">
<i className="i-lucide-minus size-4 text-accent" aria-hidden />
<span>{point}</span>
</li>
))}
</ul>
</div>
))}
</div>
</div>
</section>
)
const CTASection = () => (
<section>
<div className="relative overflow-hidden rounded-[40px] border border-white/10 bg-gradient-to-br from-accent/40 via-purple-600/40 to-slate-900/70 p-10 text-white shadow-[0_35px_80px_rgba(0,0,0,0.5)]">
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_top,_rgba(255,255,255,0.25),_transparent_55%)] opacity-80" />
<div className="relative space-y-6">
<p className="text-sm uppercase tracking-[0.4em] text-white/70">Ready?</p>
<h2 className="text-4xl font-semibold leading-tight">
Afilmory<span className="text-accent">线</span>
</h2>
<p className="text-lg text-white/80">
builderapps/webapps/ssr be/apps/core
UI
</p>
<div className="flex flex-wrap items-center gap-4">
<Button
asChild
className="min-w-[160px] border border-white/30 bg-white/10 text-white hover:bg-white/20"
>
<Link href="https://github.com/Afilmory/photo-gallery-site" target="_blank" rel="noreferrer">
Fork &
</Link>
</Button>
<Link
href="https://afilmory.innei.in"
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-2 text-sm font-medium text-white/80 hover:text-white"
>
线
<i className="i-lucide-arrow-up-right size-4" />
</Link>
</div>
</div>
</div>
</section>
)
const BackgroundDecor = () => (
<>
<div
aria-hidden
className="pointer-events-none absolute inset-0"
style={{
backgroundImage:
'radial-gradient(circle at 20% 20%, rgba(0,122,255,0.35), transparent 45%), radial-gradient(circle at 80% 0%, rgba(156,39,176,0.25), transparent 40%), radial-gradient(circle at 50% 80%, rgba(0,150,136,0.25), transparent 45%)',
}}
/>
<div className="pointer-events-none absolute inset-0 bg-[linear-gradient(120deg,rgba(255,255,255,0.05)_0%,transparent_35%,rgba(255,255,255,0.05)_70%)] opacity-40" />
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle,rgba(255,255,255,0.07)_1px,transparent_1px)] [background-size:120px_120px]" />
</>
)

View File

@@ -0,0 +1,19 @@
/**
* 背景装饰层
* 提供径向渐变、网格和光线效果
*/
export const BackgroundDecor = () => (
<>
<div
aria-hidden
className="pointer-events-none absolute inset-0"
style={{
backgroundImage:
'radial-gradient(circle at 20% 20%, rgba(0,122,255,0.35), transparent 45%), radial-gradient(circle at 80% 0%, rgba(156,39,176,0.25), transparent 40%), radial-gradient(circle at 50% 80%, rgba(0,150,136,0.25), transparent 45%)',
}}
/>
<div className="pointer-events-none absolute inset-0 bg-[linear-gradient(120deg,rgba(255,255,255,0.05)_0%,transparent_35%,rgba(255,255,255,0.05)_70%)] opacity-40" />
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle,rgba(255,255,255,0.07)_1px,transparent_1px)] [background-size:120px_120px]" />
</>
)

View File

@@ -0,0 +1,57 @@
'use client'
import Link from 'next/link'
import { Button } from '~/components/ui/button/Button'
import { radius, shadows } from '~/lib/design-tokens'
import { clsxm } from '~/lib/helper'
export const CTASection = () => (
<section>
<div
className={clsxm(
'relative overflow-hidden border border-white/10 bg-gradient-to-br from-accent/40 via-purple-600/40 to-slate-900/70 p-10 text-white',
radius['3xl'],
shadows.heavy,
)}
>
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_top,_rgba(255,255,255,0.25),_transparent_55%)] opacity-80" />
<div className="relative space-y-6">
<p className="text-sm tracking-[0.4em] text-white/70 uppercase">
Ready?
</p>
<h2 className="text-4xl leading-tight font-semibold">
Afilmory
<span className="text-accent">线</span>
</h2>
<p className="text-lg text-white/80">
builderapps/webapps/ssr
be/apps/core UI
</p>
<div className="flex flex-wrap items-center gap-4">
<Button
asChild
className="min-w-[160px] border border-white/30 bg-white/10 text-white hover:bg-white/20"
>
<Link
href="https://github.com/Afilmory/photo-gallery-site"
target="_blank"
rel="noreferrer"
>
Fork &
</Link>
</Button>
<Link
href="https://afilmory.innei.in"
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-2 text-sm font-medium text-white/80 hover:text-white"
>
线
<i className="i-lucide-arrow-up-right size-4" />
</Link>
</div>
</div>
</div>
</section>
)

View File

@@ -0,0 +1,134 @@
/**
* 通用卡片组件 - 基于 Glassmorphic Depth Design System
*/
import type { ReactNode } from 'react'
import { glassCard, radius, shadows } from '~/lib/design-tokens'
import { clsxm } from '~/lib/helper'
interface CardProps {
children: ReactNode
className?: string
variant?: 'default' | 'elevated' | 'floating' | 'gradient'
size?: 'sm' | 'md' | 'lg'
hoverable?: boolean
gradient?: string
}
const sizeStyles = {
sm: 'p-4',
md: 'p-6',
lg: 'p-8',
}
export const Card = ({
children,
className,
variant = 'default',
size = 'md',
hoverable = false,
gradient,
}: CardProps) => {
return (
<div
className={clsxm(
glassCard[variant],
radius.lg,
shadows.medium,
sizeStyles[size],
hoverable &&
'hover:border-accent/30 hover:bg-background/90 transition-all duration-300',
className,
)}
style={gradient ? { backgroundImage: gradient } : undefined}
>
{children}
</div>
)
}
interface IconCardProps {
icon: string
title: string
description?: string
meta?: string
hoverable?: boolean
}
export const IconCard = ({
icon,
title,
description,
meta,
hoverable = true,
}: IconCardProps) => {
return (
<Card variant="default" size="md" hoverable={hoverable} className="group">
<div className="text-text flex items-center gap-3">
<span className="bg-accent/15 text-accent flex size-10 items-center justify-center rounded-2xl">
<i className={clsxm('size-5', icon)} aria-hidden />
</span>
<div className="flex-1">
<p className="text-lg font-medium">{title}</p>
{description && (
<p className="text-text-secondary text-sm">{description}</p>
)}
</div>
{meta && <span className="text-text-tertiary text-xs">{meta}</span>}
</div>
</Card>
)
}
interface FeatureCardProps {
icon: string
title: string
description: string
bullets: string[]
}
export const FeatureCard = ({
icon,
title,
description,
bullets,
}: FeatureCardProps) => {
return (
<Card variant="floating" size="lg" className="flex flex-col gap-4">
<div className="flex items-center gap-3">
<span className="bg-accent/15 text-accent flex size-12 items-center justify-center rounded-2xl">
<i className={clsxm('size-5', icon)} aria-hidden />
</span>
<div>
<p className="text-xl font-semibold text-white">{title}</p>
<p className="text-text-secondary text-sm">{description}</p>
</div>
</div>
<ul className="text-text-secondary space-y-2 text-sm">
{bullets.map((point) => (
<li key={point} className="flex items-start gap-2">
<i className="i-lucide-check text-accent size-4" aria-hidden />
<span>{point}</span>
</li>
))}
</ul>
</Card>
)
}
interface MetricCardProps {
label: string
value: string
detail: string
}
export const MetricCard = ({ label, value, detail }: MetricCardProps) => {
return (
<div className="rounded-2xl border border-white/10 bg-white/50 px-4 py-5 shadow-inner shadow-white/30">
<p className="text-xs tracking-widest text-gray-600 uppercase">{label}</p>
<p className="mt-3 text-2xl font-semibold text-gray-900">{value}</p>
<p className="text-gray-700">{detail}</p>
</div>
)
}

View File

@@ -0,0 +1,78 @@
'use client'
import { spacing, typography } from '~/lib/design-tokens'
import { clsxm } from '~/lib/helper'
import { FeatureCard } from './Card'
const featureGroups = [
{
icon: 'i-lucide-cpu',
title: '性能与体验',
description: 'WebGL viewer、Masonry 布局与全屏手势带来原生级互动。',
bullets: [
'GPU 管线渲染 · Free transform · Tone mapping',
'Blurhash 占位、响应式断点与浮动操作面板',
'Glassmorphic Depth 系统 + Motion spring 动画',
],
},
{
icon: 'i-lucide-database',
title: '数据与同步',
description: 'builder 多进程处理 + manifest 驱动数据流,自动增量同步。',
bullets: [
'S3 / GitHub / Eagle / 本地多源存储抽象',
'EXIF、Live Photo、Fujifilm Recipe 与缩略图生成',
'window.__MANIFEST__ 注入,前端无感刷新',
],
},
{
icon: 'i-lucide-plug',
title: '接入模式',
description: 'SPA、Next.js SSR、be/apps/core 后端三种模式自由切换。',
bullets: [
'静态/SSR 共享 UIapps/web + apps/ssr',
'be/apps/core 以 Hono + Drizzle 提供实时能力',
'OG 渲染 / SEO 元数据 / OpenGraph API',
],
},
{
icon: 'i-lucide-globe',
title: '全球化 & 分享',
description: '多语言、OG、分享嵌入天然适合出海作品集。',
bullets: [
'i18next 平台化11+ 语言文件',
'动态 OG 图 + /og/[photoId] API',
'分享/嵌入组件、一键复制链接或 iframe',
],
},
]
export const FeatureSection = () => (
<section className={spacing.content}>
<header className={spacing.tight}>
<p className="text-text-secondary text-sm font-semibold tracking-[0.3em] uppercase">
</p>
<h2 className={clsxm(typography.h1, 'text-white')}>
</h2>
<p className="text-text-secondary max-w-3xl text-base">
Performance / Data / Integrations / Global Experience
便
</p>
</header>
<div className="grid gap-6 lg:grid-cols-2">
{featureGroups.map((group) => (
<FeatureCard
key={group.title}
icon={group.icon}
title={group.title}
description={group.description}
bullets={group.bullets}
/>
))}
</div>
</section>
)

View File

@@ -0,0 +1,215 @@
'use client'
import { m } from 'motion/react'
import Link from 'next/link'
import { Button } from '~/components/ui/button/Button'
import { blur } from '~/lib/design-tokens'
import { clsxm } from '~/lib/helper'
const heroHighlights = [
{ title: 'WebGL Viewer', description: '60fps 缩放 / 漫游 / HDR' },
{ title: 'Manifest 驱动', description: 'window.__MANIFEST__ 即时注入' },
{ title: '地图探索', description: 'MapLibre GPS / Cluster / Heatmap' },
]
const heroTiles = [
{
id: 'tile-masonry',
title: 'Masonry Flow',
subtitle: '自适应列 · Blurhash 过渡',
badge: 'GPU',
className: 'col-span-2 row-span-2',
gradient:
'linear-gradient(135deg, rgba(100,160,220,0.35), rgba(140,200,255,0.2))',
},
{
id: 'tile-exif',
title: 'EXIF',
subtitle: 'Fujifilm Recipe · HDR',
badge: 'metadata',
className: 'col-span-1 row-span-1',
gradient:
'linear-gradient(135deg, rgba(160,140,200,0.35), rgba(200,180,255,0.2))',
},
{
id: 'tile-map',
title: 'Map Explorer',
subtitle: 'GPS Cluster / Heatmap',
badge: 'MapLibre',
className: 'col-span-1 row-span-2',
gradient:
'linear-gradient(135deg, rgba(100,200,180,0.35), rgba(140,240,220,0.2))',
},
{
id: 'tile-viewer',
title: 'Fullscreen Viewer',
subtitle: 'Live Photo / 分享',
badge: 'viewer',
className: 'col-span-2 row-span-1',
gradient:
'linear-gradient(135deg, rgba(255,180,120,0.35), rgba(255,210,160,0.2))',
},
]
export const HeroSection = () => (
<section className="relative flex flex-col gap-12 lg:flex-row lg:items-center">
<div className="flex flex-1 flex-col gap-8">
<span className="text-accent/90 inline-flex w-fit items-center gap-2 rounded-full border border-white/10 bg-white/5 px-4 py-1 text-sm backdrop-blur">
<i className="i-lucide-sparkles size-4" aria-hidden />
Glassmorphic Depth Design System
</span>
<div className="space-y-5">
<h1 className="text-4xl leading-tight font-semibold text-white sm:text-5xl lg:text-6xl">
Afilmory
<span className="via-accent block bg-gradient-to-r from-sky-300 to-purple-400 bg-clip-text text-transparent">
</span>
</h1>
<p className="text-text-secondary max-w-2xl text-base sm:text-lg">
WebGLMotion manifest EXIF
MapLibre Next.js SEO / OG
</p>
</div>
<div className="flex flex-wrap items-center gap-3">
<Button
asChild
className="to-accent text-background shadow-accent/30 min-w-[160px] bg-gradient-to-r from-sky-400 shadow-lg"
>
<Link
href="https://afilmory.innei.in"
target="_blank"
rel="noreferrer"
>
</Link>
</Button>
<Button asChild variant="secondary">
<Link
href="https://github.com/Afilmory/photo-gallery-site#-features"
target="_blank"
rel="noreferrer"
>
README
</Link>
</Button>
<Link
href="https://github.com/Afilmory/photo-gallery-site"
target="_blank"
rel="noreferrer"
className="group text-text-secondary hover:text-text inline-flex items-center gap-1.5 text-sm transition"
>
<span>Star on GitHub</span>
<i className="i-lucide-arrow-up-right size-4 transition group-hover:translate-x-0.5 group-hover:-translate-y-0.5" />
</Link>
</div>
<ul className="text-text-secondary mt-4 flex flex-wrap gap-6 text-sm sm:text-base">
{heroHighlights.map((item) => (
<li key={item.title} className="flex items-center gap-2">
<span className="bg-accent/70 size-2 rounded-full shadow-[0_0_12px_rgba(0,122,255,0.8)]" />
<div>
<p className="text-text font-medium">{item.title}</p>
<p>{item.description}</p>
</div>
</li>
))}
</ul>
</div>
<HeroPreview />
</section>
)
const HeroPreview = () => (
<div className="relative flex flex-1 justify-center">
<div className="relative w-full max-w-xl">
<m.div
className="grid grid-cols-3 gap-3"
initial={{ opacity: 0, y: 32 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, ease: 'easeOut' }}
>
{heroTiles.map((tile, index) => (
<m.div
key={tile.id}
className={clsxm(
'group relative overflow-hidden rounded-[28px] border border-white/20 bg-white/40 p-4 text-gray-800 shadow-[0_8px_32px_rgba(0,0,0,0.06)] backdrop-blur-xl',
tile.className,
)}
style={{ backgroundImage: tile.gradient }}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{
delay: 0.2 + index * 0.08,
duration: 0.6,
ease: 'easeOut',
}}
>
<div className="flex items-center justify-between text-xs tracking-wide text-gray-600 uppercase">
<span>{tile.badge}</span>
<i
className="i-lucide-chevrons-up size-4 text-gray-500"
aria-hidden
/>
</div>
<p className="mt-6 text-lg font-semibold text-gray-900">
{tile.title}
</p>
<p className="text-sm text-gray-700">{tile.subtitle}</p>
<div className="pointer-events-none absolute inset-x-4 bottom-4 h-1 rounded-full bg-white/50 opacity-0 blur-lg transition duration-500 group-hover:opacity-100" />
</m.div>
))}
</m.div>
<m.div
aria-hidden
className={clsxm(
'absolute top-4 -right-6 w-56 rounded-3xl border border-white/20 bg-white/60 p-4 text-sm text-gray-800 shadow-[0_8px_32px_rgba(0,0,0,0.08)]',
blur['2xl'],
)}
initial={{ opacity: 0, y: -20, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
transition={{ delay: 0.35, duration: 0.6, ease: 'easeOut' }}
>
<div className="flex items-center justify-between text-xs font-medium text-gray-600">
window.__MANIFEST__
<span className="rounded-full bg-cyan-100 px-2 py-0.5 text-[10px] text-cyan-700">
hydrated
</span>
</div>
<div className="mt-3 space-y-2 font-mono text-[11px] leading-relaxed text-gray-700">
<p>{'{ data: 2048 photos }'}</p>
<p>{'cameras: 12 · lenses: 18'}</p>
<p>{'blurhash: true · livePhoto: 86'}</p>
</div>
</m.div>
<m.div
aria-hidden
className={clsxm(
'absolute bottom-6 -left-6 w-60 rounded-3xl border border-white/20 bg-gradient-to-br from-emerald-100/60 to-white/60 p-4 text-sm shadow-[0_8px_32px_rgba(0,0,0,0.08)]',
blur['2xl'],
)}
initial={{ opacity: 0, y: 20, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
transition={{ delay: 0.45, duration: 0.6, ease: 'easeOut' }}
>
<div className="flex items-center gap-2 text-xs font-semibold text-emerald-700">
<i className="i-lucide-map-pin size-4" />
Map Explorer
</div>
<p className="mt-3 text-xs text-gray-700">
242 geotagged stories online.
</p>
<div className="mt-4 flex items-center justify-between text-[11px] text-gray-600">
<span>Cluster · Heatmap</span>
<span> fully interactive</span>
</div>
</m.div>
</div>
</div>
)

View File

@@ -0,0 +1,41 @@
'use client'
import { m } from 'motion/react'
import { blur, radius, shadows } from '~/lib/design-tokens'
import { clsxm } from '~/lib/helper'
import { MetricCard } from './Card'
const metrics = [
{ label: 'WebGL 渲染', value: '60fps', detail: '平移 · 缩放 · HDR' },
{ label: '增量同步', value: 'S3 · GitHub', detail: '多存储后端' },
{ label: '照片节点', value: '2k+', detail: 'EXIF · Live Photo · Blurhash' },
{ label: '多语言', value: '11', detail: 'i18n · 动态 OG' },
]
export const MetricStrip = () => (
<section>
<m.div
className={clsxm(
'grid gap-4 border border-white/15 bg-white/40 p-6 text-sm text-gray-700 sm:grid-cols-2 lg:grid-cols-4',
radius['2xl'],
blur.xl,
shadows.medium,
)}
initial={{ opacity: 0, y: 24 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, amount: 0.4 }}
transition={{ duration: 0.7, ease: 'easeOut' }}
>
{metrics.map((metric) => (
<MetricCard
key={metric.label}
label={metric.label}
value={metric.value}
detail={metric.detail}
/>
))}
</m.div>
</section>
)

View File

@@ -0,0 +1,99 @@
'use client'
import { blur, radius, shadows, typography } from '~/lib/design-tokens'
import { clsxm } from '~/lib/helper'
import { IconCard } from './Card'
const previewHighlights = [
{
icon: 'i-lucide-aperture',
title: 'EXIF HUD',
description: '完整记录光圈、快门、ISO、镜头、配方、HDR 信息。',
meta: 'f/1.4 · 1/125s · ISO 200',
},
{
icon: 'i-lucide-map',
title: '地图探索',
description: 'MapLibre 地图、GPS 聚合、热力探索每一次旅程。',
meta: '84% 带 GPS · Cluster & Pin',
},
{
icon: 'i-lucide-maximize',
title: 'Fullscreen Viewer',
description: 'WebGL 全屏查看器支持手势、Live Photo 与分享。',
meta: '手势 · 分享 · Live',
},
]
export const PreviewSection = () => (
<section className="grid gap-12 lg:grid-cols-[1.1fr_0.9fr] lg:items-center">
<div className="space-y-6">
<p className="text-text-secondary text-sm font-semibold tracking-[0.3em] uppercase">
</p>
<h2 className={clsxm(typography.h1, 'text-white')}>
</h2>
<p className="text-text-secondary text-base">
Masonry MapLibre WebGL ViewerEXIF HUD
motion spring glassmorphic
</p>
<div className="space-y-4">
{previewHighlights.map((item) => (
<IconCard
key={item.title}
icon={item.icon}
title={item.title}
description={item.description}
meta={item.meta}
/>
))}
</div>
</div>
<PreviewMockup />
</section>
)
const PreviewMockup = () => (
<div className="relative">
<div className="absolute inset-x-8 -top-10 -bottom-10 rounded-[40px] bg-gradient-to-b from-white/10 via-white/5 to-transparent blur-3xl" />
<div
className={clsxm(
'relative border border-white/15 bg-white/5 p-6',
radius['3xl'],
blur['3xl'],
shadows.heavy,
)}
>
<div className="text-text-secondary flex items-center justify-between text-xs">
<div className="flex items-center gap-2">
<span className="size-2 rounded-full bg-red-400" />
<span className="size-2 rounded-full bg-yellow-400" />
<span className="size-2 rounded-full bg-emerald-400" />
</div>
<span>gallery.mxte.cc</span>
</div>
<div className="mt-6 grid grid-cols-3 gap-3">
{Array.from({ length: 9 }).map((_, index) => (
<div
key={index}
className="h-24 rounded-2xl bg-gradient-to-br from-white/20 via-white/5 to-white/0 shadow-inner shadow-black/40"
/>
))}
</div>
<div className="bg-background/70 text-text-secondary mt-6 rounded-2xl border border-white/10 p-4 text-sm">
<div className="text-text flex items-center justify-between">
<span className="font-medium">EXIF · X-T5 · XF16mmF1.4</span>
<span className="text-text-tertiary text-xs">Map locked</span>
</div>
<p className="text-text-secondary mt-2">
GPS 35.6895 / 139.6917 · Fujifilm Classic Chrome · Blurhash ready.
</p>
</div>
</div>
</div>
)

View File

@@ -0,0 +1,115 @@
'use client'
import { m } from 'motion/react'
import { blur, radius, shadows, spacing, typography } from '~/lib/design-tokens'
import { clsxm } from '~/lib/helper'
const techStacks = [
'React 19',
'Next.js 15',
'Vite',
'Tailwind CSS v4',
'Radix UI',
'Framer Motion 12',
'Jotai',
'TanStack Query',
'MapLibre',
'Sharp',
'Drizzle ORM',
'Hono',
]
const integrationModes = [
{
title: 'Standalone SPA',
description: '预构建 photos-manifest.json纯静态部署即可运行。',
points: [
'Vite + Cloudflare Pages / Vercel / 静态空间',
'CI 中运行 builder增量更新 manifest',
'无需服务器即可获得完整体验',
],
},
{
title: 'Next.js SSR Host',
description: 'apps/ssr 作为 SPA 外壳,负责注入 manifest、OG、SEO。',
points: [
'route.ts 捕获所有路径,返回注入后的 index.html',
'动态 /og/[photoId] 渲染社交卡片',
'保留 SPA 的交互,同时具备 SSR 首屏',
],
},
{
title: 'Full Backend Flow',
description: 'be/apps/core + be/apps/dashboard 驱动实时数据与管理。',
points: [
'Hono + Drizzle + PostgreSQL 全栈能力',
'Manifest 在内存构建后注入页面',
'Dashboard 管理、权限与指标监控',
],
},
]
export const TechSection = () => (
<section className={spacing.content}>
<div className={spacing.tight}>
<p className="text-text-secondary text-sm font-semibold tracking-[0.3em] uppercase">
&
</p>
<h2 className={clsxm(typography.h1, 'text-white')}></h2>
<p className="text-text-secondary text-base">
React 19 + Next.js 15 + Vite + motion + Tailwind v4 + Pastel Palette
token UI / /
</p>
</div>
<div
className={clsxm(
'border border-white/10 bg-background/40 p-6',
radius['2xl'],
blur['3xl'],
shadows.heavy,
)}
>
<div className="flex flex-wrap gap-3">
{techStacks.map((stack, index) => (
<m.span
key={stack}
className="text-text-secondary rounded-full border border-white/15 bg-white/5 px-4 py-2 text-sm"
initial={{ opacity: 0, y: 12 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: index * 0.05, duration: 0.4 }}
>
{stack}
</m.span>
))}
</div>
<div className="mt-8 grid gap-4 lg:grid-cols-3">
{integrationModes.map((mode) => (
<div
key={mode.title}
className="rounded-3xl border border-white/10 bg-white/5 p-5 shadow-inner shadow-black/30"
>
<p className="text-lg font-semibold text-white">{mode.title}</p>
<p className="text-text-secondary mt-1 text-sm">
{mode.description}
</p>
<ul className="text-text-secondary mt-3 space-y-2 text-sm">
{mode.points.map((point) => (
<li key={point} className="flex items-start gap-2">
<i
className="i-lucide-minus text-accent size-4"
aria-hidden
/>
<span>{point}</span>
</li>
))}
</ul>
</div>
))}
</div>
</div>
</section>
)

View File

@@ -0,0 +1,90 @@
'use client'
import { blur, radius, shadows, spacing, typography } from '~/lib/design-tokens'
import { clsxm } from '~/lib/helper'
const workflowSteps = [
{
icon: 'i-lucide-rocket',
title: 'Builder Pipeline',
description: 'packages/builder 负责拉取、处理、分析与生成 manifest。',
points: [
'Storage SyncS3 / GitHub / Eagle 增量拉取',
'Format & ThumbnailHEIC/TIFF 转码 + Blurhash',
'EXIF / GPS / Fujifilm Recipe / HDR 元数据提取',
],
},
{
icon: 'i-lucide-plug-zap',
title: 'Manifest 注入',
description:
'apps/ssr 在 Next.js 层注入 window.__MANIFEST__开启 SEO/OG。',
points: [
'index.html 模板内联 manifest',
'OG 动态渲染 + Metadata 替换',
'MapLibre、PhotoLoader、Viewer 共享数据源',
],
},
{
icon: 'i-lucide-monitor-play',
title: 'SPA 消费',
description:
'apps/web 作为 Vite SPA自主渲染 Masonry、Viewer、地图与浮动 UI。',
points: [
'PhotoLoader 单例在客户端初始化',
'Jotai + TanStack Query 状态与数据层',
'Glassmorphic Depth 组件交付最终体验',
],
},
]
export const WorkflowSection = () => (
<section className={spacing.content}>
<div>
<p className="text-text-secondary text-sm font-semibold tracking-[0.3em] uppercase">
</p>
<h2 className={clsxm(typography.h1, 'text-white')}>
Builder Manifest SPA
</h2>
<p className="text-text-secondary text-base">
README 线
</p>
</div>
<div
className={clsxm(
'relative border border-white/10 bg-white/5 p-8',
radius['3xl'],
blur['3xl'],
shadows.heavy,
)}
>
<div className="absolute top-10 bottom-10 left-12 w-px bg-white/10" />
<ol className="space-y-10">
{workflowSteps.map((step, index) => (
<li key={step.title} className="relative pl-16">
<div className="border-accent/20 bg-accent/10 text-accent absolute top-1 left-0 flex size-12 items-center justify-center rounded-full border">
<i className={clsxm('size-5', step.icon)} aria-hidden />
</div>
<div>
<p className="text-lg font-semibold text-white">
{index + 1}. {step.title}
</p>
<p className="text-text-secondary mt-2 text-sm">
{step.description}
</p>
<ul className="text-text-secondary mt-3 space-y-2 text-sm">
{step.points.map((point) => (
<li key={point} className="flex items-start gap-2">
<span className="bg-accent mt-1 size-1.5 rounded-full" />
<span>{point}</span>
</li>
))}
</ul>
</div>
</li>
))}
</ol>
</div>
</section>
)

View File

@@ -0,0 +1,14 @@
/**
* Landing Page Components
* 统一导出所有 landing 页面组件
*/
export { BackgroundDecor } from './BackgroundDecor'
export { Card, FeatureCard, IconCard, MetricCard } from './Card'
export { CTASection } from './CTASection'
export { FeatureSection } from './FeatureSection'
export { HeroSection } from './HeroSection'
export { MetricStrip } from './MetricStrip'
export { PreviewSection } from './PreviewSection'
export { TechSection } from './TechSection'
export { WorkflowSection } from './WorkflowSection'

View File

@@ -0,0 +1,124 @@
/**
* 设计系统 - 统一的视觉 Token
* 基于 Pastel Palette + Glassmorphic Depth Design System
*/
/**
* 阴影层级系统
* 从 subtle轻微到 heavy强烈
* 针对浅色背景优化,使用更柔和的阴影
*/
export const shadows = {
subtle: 'shadow-[0_2px_8px_rgba(0,0,0,0.02)]',
light: 'shadow-[0_4px_16px_rgba(0,0,0,0.04)]',
medium: 'shadow-[0_8px_24px_rgba(0,0,0,0.06)]',
strong: 'shadow-[0_12px_32px_rgba(0,0,0,0.08)]',
heavy: 'shadow-[0_16px_48px_rgba(0,0,0,0.10)]',
} as const
/**
* 圆角系统
*/
export const radius = {
sm: 'rounded-xl', // 12px
md: 'rounded-2xl', // 16px
lg: 'rounded-3xl', // 24px
xl: 'rounded-[28px]',
'2xl': 'rounded-[32px]',
'3xl': 'rounded-[40px]',
} as const
/**
* 模糊度系统
*/
export const blur = {
sm: 'backdrop-blur-sm', // 4px
md: 'backdrop-blur-md', // 12px
lg: 'backdrop-blur-lg', // 16px
xl: 'backdrop-blur-xl', // 24px
'2xl': 'backdrop-blur-2xl', // 40px
'3xl': 'backdrop-blur-[60px]',
} as const
/**
* Glassmorphic 卡片基础样式
* 统一的玻璃态卡片样式,可通过 variant 调整
*/
export const glassCard = {
// 默认浮动卡片
default: 'bg-background/60 border border-border backdrop-blur-xl',
// 强调卡片(略微透明)
elevated: 'bg-background/80 border border-border backdrop-blur-2xl',
// 悬浮面板
floating: 'bg-background/50 border border-white/10 backdrop-blur-[30px]',
// 带渐变的特殊卡片
gradient: 'border border-white/15 backdrop-blur-2xl',
} as const
/**
* 内阴影样式(用于内陷效果)
*/
export const innerShadow = {
subtle: 'shadow-[inset_0_1px_0_rgba(255,255,255,0.05)]',
medium: 'shadow-[inset_0_2px_4px_rgba(0,0,0,0.1)]',
strong: 'shadow-[inset_0_2px_8px_rgba(0,0,0,0.2)]',
} as const
/**
* 发光效果(用于强调元素)
*/
export const glow = {
accent: 'shadow-[0_0_12px_rgba(var(--color-accent-rgb),0.5)]',
accentStrong: 'shadow-[0_0_24px_rgba(var(--color-accent-rgb),0.7)]',
} as const
/**
* 过渡动画
*/
export const transition = {
fast: 'transition-all duration-200 ease-out',
normal: 'transition-all duration-300 ease-out',
slow: 'transition-all duration-500 ease-out',
} as const
/**
* Hover 状态样式
*/
export const hover = {
card: 'hover:border-accent/30 hover:bg-background/90 transition-all duration-300',
lift: 'hover:scale-[1.02] hover:shadow-strong transition-all duration-300',
glow: 'hover:shadow-[0_0_24px_rgba(var(--color-accent-rgb),0.3)] transition-all duration-300',
} as const
/**
* 间距系统gap/space
*/
export const spacing = {
section: 'space-y-20', // section 之间
content: 'space-y-12', // 内容组之间
group: 'space-y-6', // 组内元素
tight: 'space-y-3', // 紧密元素
} as const
/**
* Typography 层级
*/
export const typography = {
hero: 'text-4xl sm:text-5xl lg:text-6xl font-semibold leading-tight',
h1: 'text-3xl lg:text-4xl font-semibold',
h2: 'text-2xl lg:text-3xl font-semibold',
h3: 'text-xl lg:text-2xl font-semibold',
body: 'text-base',
small: 'text-sm',
tiny: 'text-xs',
label: 'text-xs tracking-[0.3em] uppercase font-semibold',
} as const
/**
* 图标容器样式
*/
export const iconBox = {
sm: 'flex size-8 items-center justify-center rounded-xl bg-accent/10 text-accent',
md: 'flex size-10 items-center justify-center rounded-2xl bg-accent/15 text-accent',
lg: 'flex size-12 items-center justify-center rounded-2xl bg-accent/15 text-accent',
} as const

898
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff