feat: 全面重构网站工程化体系与 UI 架构
- 将单体 style.css 拆分为 tokens/reset/fonts/layout/responsive/组件级 CSS 模块 - 从 Google Fonts CDN 迁移至本地自托管字体(JetBrainsMono、NotoSansSC) - 引入 Vitest + Testing Library 测试体系,新增单元测试 - 添加 GitHub Actions CI 流水线(lint → build → test) - 新增 Prettier 格式化与 ESLint 规则强化 - 重构全部 YAML 数据文件,完善项目详情页(截图轮播、更新日志) - 新增项目文档编写指南(docs/create-project.md)
This commit is contained in:
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"version": "0.0.1",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "dev",
|
||||||
|
"runtimeExecutable": "npm",
|
||||||
|
"runtimeArgs": ["run", "dev"],
|
||||||
|
"port": 5173
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(pnpm add *)",
|
||||||
|
"Bash(pnpm format *)",
|
||||||
|
"Bash(pnpm build *)",
|
||||||
|
"Bash(rtk lint *)",
|
||||||
|
"Bash(rtk ls *)",
|
||||||
|
"Bash(rtk pnpm *)",
|
||||||
|
"Bash(pnpm test *)",
|
||||||
|
"mcp__Claude_Preview__preview_start",
|
||||||
|
"Bash(pnpm dev *)",
|
||||||
|
"Bash(rtk read *)",
|
||||||
|
"Bash(rtk grep *)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: pnpm/action-setup@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
cache: pnpm
|
||||||
|
|
||||||
|
- run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- run: pnpm lint
|
||||||
|
|
||||||
|
- run: pnpm build
|
||||||
|
|
||||||
|
- run: pnpm test
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
dist
|
||||||
|
pnpm-lock.yaml
|
||||||
|
public
|
||||||
|
*.svg
|
||||||
|
*.png
|
||||||
|
*.ico
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"printWidth": 100,
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"jsxSingleQuote": false
|
||||||
|
}
|
||||||
@@ -40,15 +40,15 @@ export default defineConfig([
|
|||||||
// other options...
|
// other options...
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
])
|
]);
|
||||||
```
|
```
|
||||||
|
|
||||||
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
// eslint.config.js
|
// eslint.config.js
|
||||||
import reactX from 'eslint-plugin-react-x'
|
import reactX from 'eslint-plugin-react-x';
|
||||||
import reactDom from 'eslint-plugin-react-dom'
|
import reactDom from 'eslint-plugin-react-dom';
|
||||||
|
|
||||||
export default defineConfig([
|
export default defineConfig([
|
||||||
globalIgnores(['dist']),
|
globalIgnores(['dist']),
|
||||||
@@ -69,5 +69,5 @@ export default defineConfig([
|
|||||||
// other options...
|
// other options...
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
])
|
]);
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -0,0 +1,316 @@
|
|||||||
|
# 新增项目详细指南
|
||||||
|
|
||||||
|
本指南说明如何在 SoftwareWorkspace 网站中新增一个完整的项目条目。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 涉及文件总览
|
||||||
|
|
||||||
|
| 步骤 | 文件 | 是否必须 |
|
||||||
|
|------|------|---------|
|
||||||
|
| 1 | `src/data/projects/<项目id>.yaml` | **必须** |
|
||||||
|
| 2 | `src/utils/iconRegistry.ts` | 仅当使用新图标时 |
|
||||||
|
| 3 | `src/data/categories.yaml` | 仅当新增分类时 |
|
||||||
|
| 4 | `src/data/statuses.yaml` | 仅当新增状态时 |
|
||||||
|
| 5 | `public/screenshots/` | 仅当有截图时 |
|
||||||
|
|
||||||
|
**核心原则:** 只需创建一个 YAML 文件,项目就会自动出现在网站上。`loader.ts` 使用 `import.meta.glob('./projects/*.yaml')` 自动扫描该目录,无需手动注册。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 步骤一:创建项目 YAML 文件
|
||||||
|
|
||||||
|
在 `src/data/projects/` 下新建文件,文件名即项目 ID(小写,用连字符分隔)。
|
||||||
|
|
||||||
|
例如:`src/data/projects/my-new-app.yaml`
|
||||||
|
|
||||||
|
### 完整字段模板
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# ========== 基础信息 ==========
|
||||||
|
|
||||||
|
# 项目唯一标识(与文件名一致,用于 URL 路由 /projects/:id)
|
||||||
|
id: 'my-new-app'
|
||||||
|
|
||||||
|
# 项目内部名称(英文,小写)
|
||||||
|
name: 'my-new-app'
|
||||||
|
|
||||||
|
# 显示名称(双语,通常首字母大写)
|
||||||
|
displayName:
|
||||||
|
zh: '我的新应用'
|
||||||
|
en: 'My New App'
|
||||||
|
|
||||||
|
# 一句话简介(显示在卡片和详情页标题下方)
|
||||||
|
slogan:
|
||||||
|
zh: '一句话中文描述'
|
||||||
|
en: 'A one-line English description'
|
||||||
|
|
||||||
|
# 详细描述(显示在详情页"概览"区域)
|
||||||
|
description:
|
||||||
|
zh: '详细的中文介绍,说明项目功能、技术特点和适用场景。'
|
||||||
|
en: 'Detailed English introduction explaining features, technical highlights and use cases.'
|
||||||
|
|
||||||
|
# ========== 分类与状态 ==========
|
||||||
|
|
||||||
|
# 项目分类,可选多个(对应 categories.yaml 中的 id)
|
||||||
|
# 可用值:desktop / mobile / devtool / library / backend / selfhosted / ai
|
||||||
|
type:
|
||||||
|
- 'desktop'
|
||||||
|
|
||||||
|
# 开发状态(对应 statuses.yaml 中的 key)
|
||||||
|
# 可用值:active / maintained / beta / experimental / archived
|
||||||
|
status: 'active'
|
||||||
|
|
||||||
|
# ========== 技术信息 ==========
|
||||||
|
|
||||||
|
# 支持平台(对应 platforms.yaml 中的 key)
|
||||||
|
# 可用值:windows / macos / linux / android / ios / web / docker / npm / cli
|
||||||
|
platforms:
|
||||||
|
- 'windows'
|
||||||
|
- 'macos'
|
||||||
|
- 'linux'
|
||||||
|
|
||||||
|
# 技术栈标签(自由填写,会出现在筛选下拉和项目卡片上)
|
||||||
|
techStack:
|
||||||
|
- 'Rust'
|
||||||
|
- 'TypeScript'
|
||||||
|
- 'React'
|
||||||
|
|
||||||
|
# 核心功能列表(双语,显示在详情页"核心功能"区域)
|
||||||
|
features:
|
||||||
|
zh:
|
||||||
|
- '功能一'
|
||||||
|
- '功能二'
|
||||||
|
- '功能三'
|
||||||
|
en:
|
||||||
|
- 'Feature one'
|
||||||
|
- 'Feature two'
|
||||||
|
- 'Feature three'
|
||||||
|
|
||||||
|
# 搜索标签(用于搜索框模糊匹配,界面上不直接展示)
|
||||||
|
tags:
|
||||||
|
- 'Keyword1'
|
||||||
|
- 'Keyword2'
|
||||||
|
- 'Keyword3'
|
||||||
|
|
||||||
|
# ========== 图标与链接 ==========
|
||||||
|
|
||||||
|
# 项目图标(lucide-react 图标名称)
|
||||||
|
# 已注册的图标:NotebookPen / Terminal / MapPin / SmartPhone / BookOpen /
|
||||||
|
# Monitor / Brain / KeyRound / Wrench / Package / Cloud / Server / FlaskConical
|
||||||
|
# 如果需要新图标,见"步骤二"
|
||||||
|
icon: 'Terminal'
|
||||||
|
|
||||||
|
# GitHub 仓库地址(必须)
|
||||||
|
repoUrl: 'https://github.com/shenjianZ/my-new-app'
|
||||||
|
|
||||||
|
# 在线文档地址(可选,不填则不显示文档按钮)
|
||||||
|
docsUrl: 'https://github.com/shenjianZ/my-new-app#readme'
|
||||||
|
|
||||||
|
# npm 包地址(可选,仅 NPM 包类项目填写)
|
||||||
|
# npmUrl: 'https://www.npmjs.com/package/my-new-app'
|
||||||
|
|
||||||
|
# ========== 版本信息 ==========
|
||||||
|
|
||||||
|
latestVersion: 'v1.0.0'
|
||||||
|
releaseDate: '2026-05-22'
|
||||||
|
license: 'MIT'
|
||||||
|
stars: 10
|
||||||
|
forks: 2
|
||||||
|
language: 'TypeScript'
|
||||||
|
lastUpdated: '2026-05-22'
|
||||||
|
|
||||||
|
# ========== 首页展示控制 ==========
|
||||||
|
|
||||||
|
# 是否在首页"重点项目"区域展示(true/false)
|
||||||
|
recommended: false
|
||||||
|
|
||||||
|
# 是否在首页"Featured"大卡片展示(true/false,通常只给 1-2 个项目)
|
||||||
|
featured: false
|
||||||
|
|
||||||
|
# 排序权重(数字越小越靠前)
|
||||||
|
order: 9
|
||||||
|
|
||||||
|
# 卡片主题色(十六进制色值)
|
||||||
|
color: '#3B82F6'
|
||||||
|
|
||||||
|
# ========== 下载信息(可选) ==========
|
||||||
|
|
||||||
|
# 如果没有下载功能,设为空数组 []
|
||||||
|
downloads:
|
||||||
|
- platform: 'Windows'
|
||||||
|
arch: 'x64'
|
||||||
|
url: 'https://github.com/shenjianZ/my-new-app/releases/download/v1.0.0/my-new-app-setup.exe'
|
||||||
|
size: '15 MB'
|
||||||
|
sha256: ''
|
||||||
|
- platform: 'macOS'
|
||||||
|
arch: 'universal'
|
||||||
|
url: 'https://github.com/shenjianZ/my-new-app/releases/download/v1.0.0/my-new-app.dmg'
|
||||||
|
size: '18 MB'
|
||||||
|
sha256: ''
|
||||||
|
- platform: 'Linux'
|
||||||
|
arch: 'x64'
|
||||||
|
url: 'https://github.com/shenjianZ/my-new-app/releases/download/v1.0.0/my-new-app.AppImage'
|
||||||
|
size: '14 MB'
|
||||||
|
sha256: ''
|
||||||
|
|
||||||
|
# ========== 路线图(可选) ==========
|
||||||
|
|
||||||
|
roadmap:
|
||||||
|
done:
|
||||||
|
- '已完成的功能'
|
||||||
|
doing:
|
||||||
|
- '正在开发的功能'
|
||||||
|
planned:
|
||||||
|
- '计划中的功能'
|
||||||
|
|
||||||
|
# ========== 更新日志(可选) ==========
|
||||||
|
|
||||||
|
changelog:
|
||||||
|
- version: 'v1.0.0'
|
||||||
|
date: '2026-05-22'
|
||||||
|
changes:
|
||||||
|
zh:
|
||||||
|
- '初始发布'
|
||||||
|
- '核心功能上线'
|
||||||
|
en:
|
||||||
|
- 'Initial release'
|
||||||
|
- 'Core features launched'
|
||||||
|
|
||||||
|
# ========== 架构说明(可选) ==========
|
||||||
|
|
||||||
|
architecture:
|
||||||
|
zh: '前端 (React) → API 层 → 后端 (Rust)'
|
||||||
|
en: 'Frontend (React) → API Layer → Backend (Rust)'
|
||||||
|
|
||||||
|
# ========== 截图(可选) ==========
|
||||||
|
|
||||||
|
# 截图文件放在 public/screenshots/<项目id>/ 目录下
|
||||||
|
# screenshots:
|
||||||
|
# - '/screenshots/my-new-app/main.png'
|
||||||
|
# - '/screenshots/my-new-app/settings.png'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 步骤二(仅在需要新图标时):注册图标
|
||||||
|
|
||||||
|
如果 `icon` 字段需要使用一个尚未注册的 lucide-react 图标,编辑 `src/utils/iconRegistry.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// 1. 在 import 中添加新图标
|
||||||
|
import {
|
||||||
|
// ... 已有图标
|
||||||
|
Zap, // 新增
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
// 2. 在 iconRegistry 对象中注册
|
||||||
|
const iconRegistry: Record<string, ComponentType<{ size?: number }>> = {
|
||||||
|
// ... 已有图标
|
||||||
|
Zap, // 新增
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
然后在 YAML 中 `icon: 'Zap'` 即可。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 步骤三(仅在需要新分类时):添加分类
|
||||||
|
|
||||||
|
编辑 `src/data/categories.yaml`,追加新分类:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- id: 'new-category-id'
|
||||||
|
label:
|
||||||
|
zh: '新分类中文名'
|
||||||
|
en: 'New Category Name'
|
||||||
|
icon: 'IconName' # 需要在 iconRegistry 中已注册
|
||||||
|
```
|
||||||
|
|
||||||
|
然后在项目的 `type` 字段中使用该 `id`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 步骤四(仅在需要新状态时):添加状态
|
||||||
|
|
||||||
|
编辑 `src/data/statuses.yaml`,追加新状态:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
new-status:
|
||||||
|
label:
|
||||||
|
zh: '中文状态名'
|
||||||
|
en: 'English Status Name'
|
||||||
|
color: '#HEXCOLOR'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 步骤五(仅在有截图时):添加截图
|
||||||
|
|
||||||
|
1. 在 `public/` 下创建目录:`public/screenshots/<项目id>/`
|
||||||
|
2. 放入截图文件(推荐 PNG 格式)
|
||||||
|
3. 在 YAML 中引用:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
screenshots:
|
||||||
|
- '/screenshots/my-new-app/main.png'
|
||||||
|
- '/screenshots/my-new-app/feature.png'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 步骤六:验证
|
||||||
|
|
||||||
|
1. 启动开发服务器:`pnpm dev`
|
||||||
|
2. 检查以下页面:
|
||||||
|
- **首页**:项目出现在"最新发布"区域;若 `featured: true` 则出现在 Featured 大卡片
|
||||||
|
- **项目列表页**:项目出现在列表中,技术栈/平台/状态筛选均生效
|
||||||
|
- **项目详情页**:访问 `/projects/<项目id>`,所有信息正确展示
|
||||||
|
- **搜索**:在搜索框输入 tags 中的关键词,能搜索到该项目
|
||||||
|
3. 运行测试:`pnpm test`(siteData 测试会验证数据结构完整性)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 可用值速查
|
||||||
|
|
||||||
|
### type(分类)
|
||||||
|
| id | 中文 | 英文 |
|
||||||
|
|----|------|------|
|
||||||
|
| desktop | 桌面软件 | Desktop Apps |
|
||||||
|
| mobile | 移动应用 | Mobile Apps |
|
||||||
|
| devtool | 开发者工具 | Dev Tools |
|
||||||
|
| library | NPM / 组件库 | NPM / Libraries |
|
||||||
|
| backend | 后端服务 | Backend |
|
||||||
|
| selfhosted | 自托管 | Self-hosted |
|
||||||
|
| ai | AI / 实验项目 | AI / Experiments |
|
||||||
|
|
||||||
|
### status(状态)
|
||||||
|
| key | 中文 | 英文 | 颜色 |
|
||||||
|
|-----|------|------|------|
|
||||||
|
| active | 活跃开发 | Active | #22C55E |
|
||||||
|
| maintained | 维护中 | Maintained | #3B82F6 |
|
||||||
|
| beta | 测试版 | Beta | #F59E0B |
|
||||||
|
| experimental | 实验性 | Experimental | #A855F7 |
|
||||||
|
| archived | 已归档 | Archived | #6B7280 |
|
||||||
|
|
||||||
|
### platforms(平台)
|
||||||
|
`windows` / `macos` / `linux` / `android` / `ios` / `web` / `docker` / `npm` / `cli`
|
||||||
|
|
||||||
|
### 已注册图标
|
||||||
|
`NotebookPen` / `Terminal` / `MapPin` / `SmartPhone` / `BookOpen` / `Monitor` / `Brain` / `KeyRound` / `Wrench` / `Package` / `Cloud` / `Server` / `FlaskConical`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 数据流示意
|
||||||
|
|
||||||
|
```
|
||||||
|
src/data/projects/<id>.yaml
|
||||||
|
↓ import.meta.glob 自动扫描
|
||||||
|
src/data/loader.ts(解析为 Project 对象,按 order 排序)
|
||||||
|
↓ 导出 siteData
|
||||||
|
src/pages/HomePage.tsx → 首页卡片、Featured、最新发布
|
||||||
|
src/pages/ProjectsPage.tsx → 项目列表 + 筛选
|
||||||
|
src/pages/ProjectDetailPage.tsx → 详情页(/projects/:id)
|
||||||
|
```
|
||||||
|
|
||||||
|
**只需创建 YAML 文件,无需修改任何 TypeScript 代码,项目即自动上线。**
|
||||||
+12
-7
@@ -1,9 +1,9 @@
|
|||||||
import js from '@eslint/js'
|
import js from '@eslint/js';
|
||||||
import globals from 'globals'
|
import globals from 'globals';
|
||||||
import reactHooks from 'eslint-plugin-react-hooks'
|
import reactHooks from 'eslint-plugin-react-hooks';
|
||||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
import reactRefresh from 'eslint-plugin-react-refresh';
|
||||||
import tseslint from 'typescript-eslint'
|
import tseslint from 'typescript-eslint';
|
||||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
import { defineConfig, globalIgnores } from 'eslint/config';
|
||||||
|
|
||||||
export default defineConfig([
|
export default defineConfig([
|
||||||
globalIgnores(['dist']),
|
globalIgnores(['dist']),
|
||||||
@@ -18,5 +18,10 @@ export default defineConfig([
|
|||||||
languageOptions: {
|
languageOptions: {
|
||||||
globals: globals.browser,
|
globals: globals.browser,
|
||||||
},
|
},
|
||||||
|
rules: {
|
||||||
|
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
|
||||||
|
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
|
||||||
|
'no-console': ['warn', { allow: ['warn', 'error'] }],
|
||||||
},
|
},
|
||||||
])
|
},
|
||||||
|
]);
|
||||||
|
|||||||
+15
-12
@@ -1,19 +1,22 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="zh">
|
<html lang="zh">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>ZUJ OL Apps — 开源软件聚合站</title>
|
<title>ZUJ OL Apps — 开源软件聚合站</title>
|
||||||
<meta name="description" content="构建轻量、高效、开源的软件工具。涵盖桌面软件、移动应用、开发者工具、笔记系统、SSH 客户端、远程控制、文档组件库与全栈应用。">
|
<meta
|
||||||
<meta name="keywords" content="开源软件,桌面应用,移动应用,开发者工具,Tauri,Rust,React,React Native">
|
name="description"
|
||||||
<meta property="og:title" content="ZUJ OL Apps — 开源软件聚合站">
|
content="构建轻量、高效、开源的软件工具。涵盖桌面软件、移动应用、开发者工具、笔记系统、SSH 客户端、远程控制、文档组件库与全栈应用。"
|
||||||
<meta property="og:description" content="构建轻量、高效、开源的软件工具">
|
/>
|
||||||
<meta property="og:type" content="website">
|
<meta
|
||||||
<meta name="color-scheme" content="dark light">
|
name="keywords"
|
||||||
<meta name="twitter:card" content="summary">
|
content="开源软件,桌面应用,移动应用,开发者工具,Tauri,Rust,React,React Native"
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
/>
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<meta property="og:title" content="ZUJ OL Apps — 开源软件聚合站" />
|
||||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Noto+Sans+SC:wght@400;500;600;700&display=swap" rel="stylesheet">
|
<meta property="og:description" content="构建轻量、高效、开源的软件工具" />
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta name="color-scheme" content="dark light" />
|
||||||
|
<meta name="twitter:card" content="summary" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
+10
-1
@@ -7,6 +7,9 @@
|
|||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc -b && vite build",
|
"build": "tsc -b && vite build",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"test": "vitest run",
|
||||||
|
"test:watch": "vitest",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -18,6 +21,9 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
|
"@testing-library/react": "^16.3.2",
|
||||||
|
"@testing-library/user-event": "^14.6.1",
|
||||||
"@types/node": "^24.12.3",
|
"@types/node": "^24.12.3",
|
||||||
"@types/react": "^19.2.14",
|
"@types/react": "^19.2.14",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
@@ -26,9 +32,12 @@
|
|||||||
"eslint-plugin-react-hooks": "^7.1.1",
|
"eslint-plugin-react-hooks": "^7.1.1",
|
||||||
"eslint-plugin-react-refresh": "^0.5.2",
|
"eslint-plugin-react-refresh": "^0.5.2",
|
||||||
"globals": "^17.6.0",
|
"globals": "^17.6.0",
|
||||||
|
"jsdom": "^29.1.1",
|
||||||
|
"prettier": "^3.8.3",
|
||||||
"typescript": "~6.0.2",
|
"typescript": "~6.0.2",
|
||||||
"typescript-eslint": "^8.59.2",
|
"typescript-eslint": "^8.59.2",
|
||||||
"vite": "^8.0.12"
|
"vite": "^8.0.12",
|
||||||
|
"vitest": "^4.1.7"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.33.2+sha512.a90faf6feeab71ad6c6e57f94e0fe1a12f5dcc22cd754db40ae9593eb6a3e0b6b12e3540218bb37ae083404b1f2ce6db2a4121e979829b4aff94b99f49da1cf8"
|
"packageManager": "pnpm@10.33.2+sha512.a90faf6feeab71ad6c6e57f94e0fe1a12f5dcc22cd754db40ae9593eb6a3e0b6b12e3540218bb37ae083404b1f2ce6db2a4121e979829b4aff94b99f49da1cf8"
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+763
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 456 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 72 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,24 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<symbol id="bluesky-icon" viewBox="0 0 16 17">
|
|
||||||
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
|
|
||||||
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
|
|
||||||
</symbol>
|
|
||||||
<symbol id="discord-icon" viewBox="0 0 20 19">
|
|
||||||
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
|
|
||||||
</symbol>
|
|
||||||
<symbol id="documentation-icon" viewBox="0 0 21 20">
|
|
||||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
|
|
||||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
|
|
||||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
|
|
||||||
</symbol>
|
|
||||||
<symbol id="github-icon" viewBox="0 0 19 19">
|
|
||||||
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
|
|
||||||
</symbol>
|
|
||||||
<symbol id="social-icon" viewBox="0 0 20 20">
|
|
||||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
|
|
||||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
|
|
||||||
</symbol>
|
|
||||||
<symbol id="x-icon" viewBox="0 0 19 19">
|
|
||||||
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
|
|
||||||
</symbol>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 9.3 KiB |
+12
-7
@@ -1,14 +1,17 @@
|
|||||||
import { useEffect } from 'react';
|
import { lazy, Suspense, useEffect } from 'react';
|
||||||
import { Routes, Route } from 'react-router-dom';
|
import { Routes, Route } from 'react-router-dom';
|
||||||
import { useScrollToTop } from './hooks/useScrollToTop';
|
import { useScrollToTop } from './hooks/useScrollToTop';
|
||||||
import Nav from './components/Nav';
|
import Nav from './components/Nav';
|
||||||
import Footer from './components/Footer';
|
import Footer from './components/Footer';
|
||||||
import HomePage from './pages/HomePage';
|
import LoadingFallback from './components/LoadingFallback';
|
||||||
import ProjectsPage from './pages/ProjectsPage';
|
|
||||||
import ProjectDetailPage from './pages/ProjectDetailPage';
|
|
||||||
import AboutPage from './pages/AboutPage';
|
|
||||||
|
|
||||||
const REVEAL_SELECTORS = '.section, .stat-item, .project-card, .category-card, .contact-card, .changelog-entry, .about-header, .focus-item';
|
const HomePage = lazy(() => import('./pages/HomePage'));
|
||||||
|
const ProjectsPage = lazy(() => import('./pages/ProjectsPage'));
|
||||||
|
const ProjectDetailPage = lazy(() => import('./pages/ProjectDetailPage'));
|
||||||
|
const AboutPage = lazy(() => import('./pages/AboutPage'));
|
||||||
|
|
||||||
|
const REVEAL_SELECTORS =
|
||||||
|
'.section, .stat-item, .project-card, .category-card, .contact-card, .changelog-entry, .about-header, .focus-item';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
useScrollToTop();
|
useScrollToTop();
|
||||||
@@ -19,7 +22,7 @@ function App() {
|
|||||||
|
|
||||||
const observer = new IntersectionObserver(
|
const observer = new IntersectionObserver(
|
||||||
(entries) => {
|
(entries) => {
|
||||||
entries.forEach(entry => {
|
entries.forEach((entry) => {
|
||||||
if (entry.isIntersecting) {
|
if (entry.isIntersecting) {
|
||||||
entry.target.classList.add('revealed');
|
entry.target.classList.add('revealed');
|
||||||
observer.unobserve(entry.target);
|
observer.unobserve(entry.target);
|
||||||
@@ -70,6 +73,7 @@ function App() {
|
|||||||
<>
|
<>
|
||||||
<Nav />
|
<Nav />
|
||||||
<main className="main-content">
|
<main className="main-content">
|
||||||
|
<Suspense fallback={<LoadingFallback />}>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<HomePage />} />
|
<Route path="/" element={<HomePage />} />
|
||||||
<Route path="/projects" element={<ProjectsPage />} />
|
<Route path="/projects" element={<ProjectsPage />} />
|
||||||
@@ -77,6 +81,7 @@ function App() {
|
|||||||
<Route path="/about" element={<AboutPage />} />
|
<Route path="/about" element={<AboutPage />} />
|
||||||
<Route path="/contact" element={<AboutPage />} />
|
<Route path="/contact" element={<AboutPage />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
</Suspense>
|
||||||
</main>
|
</main>
|
||||||
<Footer />
|
<Footer />
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,17 +1,7 @@
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { Monitor, Smartphone, Wrench, Package, Cloud, Server, FlaskConical } from 'lucide-react';
|
|
||||||
import { useI18n } from '../hooks/useI18n';
|
import { useI18n } from '../hooks/useI18n';
|
||||||
import { siteData } from '../data/siteData';
|
import { siteData } from '../data/siteData';
|
||||||
|
import { getIcon } from '../utils/iconRegistry';
|
||||||
const iconMap: Record<string, React.ComponentType<{ size?: number }>> = {
|
|
||||||
Monitor,
|
|
||||||
Smartphone,
|
|
||||||
Wrench,
|
|
||||||
Package,
|
|
||||||
Cloud,
|
|
||||||
Server,
|
|
||||||
FlaskConical,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function CategoryGrid() {
|
export default function CategoryGrid() {
|
||||||
const { bi } = useI18n();
|
const { bi } = useI18n();
|
||||||
@@ -19,16 +9,10 @@ export default function CategoryGrid() {
|
|||||||
return (
|
return (
|
||||||
<div className="category-grid">
|
<div className="category-grid">
|
||||||
{siteData.categories.map((cat) => {
|
{siteData.categories.map((cat) => {
|
||||||
const IconComponent = iconMap[cat.icon];
|
const IconComponent = getIcon(cat.icon);
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link key={cat.id} to={`/projects?cat=${cat.id}`} className="category-card">
|
||||||
key={cat.id}
|
<div className="category-icon">{IconComponent && <IconComponent size={28} />}</div>
|
||||||
to={`/projects?cat=${cat.id}`}
|
|
||||||
className="category-card"
|
|
||||||
>
|
|
||||||
<div className="category-icon">
|
|
||||||
{IconComponent && <IconComponent size={28} />}
|
|
||||||
</div>
|
|
||||||
<div className="category-label">{bi(cat.label)}</div>
|
<div className="category-label">{bi(cat.label)}</div>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -27,16 +27,9 @@ export default function DownloadTable({ downloads, showChecksum = false }: Downl
|
|||||||
<td>{dl.platform}</td>
|
<td>{dl.platform}</td>
|
||||||
<td>{dl.arch}</td>
|
<td>{dl.arch}</td>
|
||||||
<td>{dl.size}</td>
|
<td>{dl.size}</td>
|
||||||
{showChecksum && (
|
{showChecksum && <td className="sha256">{dl.sha256}</td>}
|
||||||
<td className="sha256">{dl.sha256}</td>
|
|
||||||
)}
|
|
||||||
<td>
|
<td>
|
||||||
<a
|
<a href={dl.url} className="btn btn-sm" target="_blank" rel="noopener noreferrer">
|
||||||
href={dl.url}
|
|
||||||
className="btn btn-sm"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<Download size={14} /> {t('common.download')}
|
<Download size={14} /> {t('common.download')}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -1,44 +1,31 @@
|
|||||||
import type { ComponentType } from 'react';
|
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { Star, NotebookPen, Terminal, MapPin, Smartphone, BookOpen, Monitor, Brain, KeyRound } from 'lucide-react';
|
import { Star } from 'lucide-react';
|
||||||
import { useI18n } from '../hooks/useI18n';
|
import { useI18n } from '../hooks/useI18n';
|
||||||
import { siteData } from '../data/siteData';
|
import { siteData } from '../data/siteData';
|
||||||
|
import { getIcon } from '../utils/iconRegistry';
|
||||||
import type { Project } from '../types';
|
import type { Project } from '../types';
|
||||||
|
|
||||||
const iconMap: Record<string, ComponentType<{ size?: number }>> = {
|
|
||||||
NotebookPen,
|
|
||||||
Terminal,
|
|
||||||
MapPin,
|
|
||||||
Smartphone,
|
|
||||||
BookOpen,
|
|
||||||
Monitor,
|
|
||||||
Brain,
|
|
||||||
KeyRound,
|
|
||||||
};
|
|
||||||
|
|
||||||
interface FeaturedCardProps {
|
interface FeaturedCardProps {
|
||||||
project: Project;
|
project: Project;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function FeaturedCard({ project }: FeaturedCardProps) {
|
export default function FeaturedCard({ project }: FeaturedCardProps) {
|
||||||
const { bi } = useI18n();
|
const { bi, lang } = useI18n();
|
||||||
const status = siteData.statuses[project.status];
|
const status = siteData.statuses[project.status];
|
||||||
const IconComponent = iconMap[project.icon];
|
const IconComponent = getIcon(project.icon);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link to={`/projects/${project.id}`} className="project-card" style={{ textDecoration: 'none' }}>
|
<Link
|
||||||
|
to={`/projects/${project.id}`}
|
||||||
|
className="project-card project-card-link"
|
||||||
|
>
|
||||||
<div className="project-card-header">
|
<div className="project-card-header">
|
||||||
<div className="project-icon">
|
<div className="project-icon">{IconComponent && <IconComponent size={22} />}</div>
|
||||||
{IconComponent && <IconComponent size={22} />}
|
|
||||||
</div>
|
|
||||||
<div className="project-card-info">
|
<div className="project-card-info">
|
||||||
<div className="project-name">
|
<div className="project-name">
|
||||||
{project.name}
|
{project.displayName[lang] || project.name}
|
||||||
{status && (
|
{status && (
|
||||||
<span
|
<span className="badge badge-status" style={{ '--status-color': status.color } as React.CSSProperties}>
|
||||||
className="badge badge-status"
|
|
||||||
style={{ color: status.color }}
|
|
||||||
>
|
|
||||||
{bi(status.label)}
|
{bi(status.label)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -49,7 +36,9 @@ export default function FeaturedCard({ project }: FeaturedCardProps) {
|
|||||||
|
|
||||||
<div className="project-card-meta">
|
<div className="project-card-meta">
|
||||||
{project.techStack.slice(0, 3).map((tech) => (
|
{project.techStack.slice(0, 3).map((tech) => (
|
||||||
<span key={tech} className="badge">{tech}</span>
|
<span key={tech} className="badge">
|
||||||
|
{tech}
|
||||||
|
</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useI18n } from '../hooks/useI18n';
|
|||||||
import { siteData } from '../data/siteData';
|
import { siteData } from '../data/siteData';
|
||||||
|
|
||||||
export default function Footer() {
|
export default function Footer() {
|
||||||
const { t, bi } = useI18n();
|
const { t, bi, lang } = useI18n();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className="footer">
|
<footer className="footer">
|
||||||
@@ -16,25 +16,40 @@ export default function Footer() {
|
|||||||
<div>
|
<div>
|
||||||
<div className="footer-col-title">{t('footer.projects')}</div>
|
<div className="footer-col-title">{t('footer.projects')}</div>
|
||||||
<ul className="footer-links footer-project-links">
|
<ul className="footer-links footer-project-links">
|
||||||
{siteData.projects.slice(0, 5).map(p => (
|
{siteData.projects.slice(0, 6).map((p) => (
|
||||||
<li key={p.id}><Link to={`/projects/${p.id}`}>{p.name}</Link></li>
|
<li key={p.id}>
|
||||||
|
<Link to={`/projects/${p.id}`}>{p.displayName[lang] || p.name}</Link>
|
||||||
|
</li>
|
||||||
))}
|
))}
|
||||||
|
<li>
|
||||||
|
<Link to="/projects" className="footer-view-all">{t('footer.viewAllProjects')} →</Link>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="footer-col-title">{t('footer.community')}</div>
|
<div className="footer-col-title">{t('footer.community')}</div>
|
||||||
<ul className="footer-links footer-community-links">
|
<ul className="footer-links footer-community-links">
|
||||||
<li><a href={siteData.brand.github} target="_blank">GitHub</a></li>
|
<li>
|
||||||
<li><Link to="/about">{t('about.title')}</Link></li>
|
<a href={siteData.brand.github} target="_blank">
|
||||||
|
GitHub
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link to="/about">{t('about.title')}</Link>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="footer-bottom">
|
<div className="footer-bottom">
|
||||||
<span>{t('footer.copyright')}</span>
|
<span>{t('footer.copyright')}</span>
|
||||||
<span className="footer-policy-links">
|
<span className="footer-policy-links">
|
||||||
<Link to="/about" style={{ color: 'var(--muted)' }}>{t('footer.license')}</Link>
|
<Link to="/about" className="footer-policy-link">
|
||||||
|
{t('footer.license')}
|
||||||
|
</Link>
|
||||||
{' · '}
|
{' · '}
|
||||||
<Link to="/about" style={{ color: 'var(--muted)' }}>{t('footer.privacy')}</Link>
|
<Link to="/about" className="footer-policy-link">
|
||||||
|
{t('footer.privacy')}
|
||||||
|
</Link>
|
||||||
</span>
|
</span>
|
||||||
<span className="footer-legal">
|
<span className="footer-legal">
|
||||||
{siteData.brand.icp && (
|
{siteData.brand.icp && (
|
||||||
|
|||||||
@@ -14,12 +14,7 @@ export default function Hero() {
|
|||||||
<Link to="/projects" className="btn btn-primary">
|
<Link to="/projects" className="btn btn-primary">
|
||||||
<FolderKanban size={16} /> {t('hero.cta.projects')}
|
<FolderKanban size={16} /> {t('hero.cta.projects')}
|
||||||
</Link>
|
</Link>
|
||||||
<a
|
<a href={siteData.brand.github} target="_blank" rel="noopener noreferrer" className="btn">
|
||||||
href={siteData.brand.github}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="btn"
|
|
||||||
>
|
|
||||||
<ExternalLink size={16} /> {t('hero.cta.github')}
|
<ExternalLink size={16} /> {t('hero.cta.github')}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export default function LoadingFallback() {
|
||||||
|
return (
|
||||||
|
<div className="container loading-fallback">
|
||||||
|
<div className="loading-spinner" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,7 +5,8 @@ import { useI18n } from '../hooks/useI18n';
|
|||||||
import { siteData } from '../data/siteData';
|
import { siteData } from '../data/siteData';
|
||||||
|
|
||||||
export default function Nav() {
|
export default function Nav() {
|
||||||
const { theme, mobileMenuOpen, toggleTheme, toggleLang, openMobileMenu, closeMobileMenu } = useApp();
|
const { theme, mobileMenuOpen, toggleTheme, toggleLang, openMobileMenu, closeMobileMenu } =
|
||||||
|
useApp();
|
||||||
const { t, bi } = useI18n();
|
const { t, bi } = useI18n();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const currentPath = '/' + location.pathname.slice(1);
|
const currentPath = '/' + location.pathname.slice(1);
|
||||||
@@ -15,14 +16,17 @@ export default function Nav() {
|
|||||||
<nav className="nav">
|
<nav className="nav">
|
||||||
<div className="nav-inner">
|
<div className="nav-inner">
|
||||||
<Link to="/" className="nav-brand">
|
<Link to="/" className="nav-brand">
|
||||||
|
{siteData.brand.logo ? (
|
||||||
|
<img src={siteData.brand.logo} alt={bi(siteData.brand.name)} className="nav-brand-icon" />
|
||||||
|
) : (
|
||||||
<span className="nav-brand-icon">Z</span>
|
<span className="nav-brand-icon">Z</span>
|
||||||
|
)}
|
||||||
{bi(siteData.brand.name)}
|
{bi(siteData.brand.name)}
|
||||||
</Link>
|
</Link>
|
||||||
<div className="nav-links">
|
<div className="nav-links">
|
||||||
{siteData.nav.map(n => {
|
{siteData.nav.map((n) => {
|
||||||
const navPath = n.hash.slice(2) || '/';
|
const navPath = n.hash.slice(2) || '/';
|
||||||
const isActive = currentPath === navPath ||
|
const isActive = currentPath === navPath || (navPath === '/' && currentPath === '');
|
||||||
(navPath === '/' && currentPath === '');
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
key={n.id}
|
key={n.id}
|
||||||
@@ -56,7 +60,7 @@ export default function Nav() {
|
|||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<div className={`nav-mobile-menu${mobileMenuOpen ? ' open' : ''}`}>
|
<div className={`nav-mobile-menu${mobileMenuOpen ? ' open' : ''}`}>
|
||||||
{siteData.nav.map(n => {
|
{siteData.nav.map((n) => {
|
||||||
const navPath = n.hash.slice(2) || '/';
|
const navPath = n.hash.slice(2) || '/';
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
|
|||||||
@@ -1,44 +1,28 @@
|
|||||||
import type { ComponentType } from 'react';
|
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { ExternalLink, Download, BookOpen, Star, NotebookPen, Terminal, MapPin, Smartphone, Monitor, Brain, KeyRound } from 'lucide-react';
|
import { ExternalLink, Download, BookOpen, Star } from 'lucide-react';
|
||||||
import { useI18n } from '../hooks/useI18n';
|
import { useI18n } from '../hooks/useI18n';
|
||||||
import { siteData } from '../data/siteData';
|
import { siteData } from '../data/siteData';
|
||||||
|
import { getIcon } from '../utils/iconRegistry';
|
||||||
import type { Project } from '../types';
|
import type { Project } from '../types';
|
||||||
|
|
||||||
const iconMap: Record<string, ComponentType<{ size?: number }>> = {
|
|
||||||
NotebookPen,
|
|
||||||
Terminal,
|
|
||||||
MapPin,
|
|
||||||
Smartphone,
|
|
||||||
BookOpen,
|
|
||||||
Monitor,
|
|
||||||
Brain,
|
|
||||||
KeyRound,
|
|
||||||
};
|
|
||||||
|
|
||||||
interface ProjectCardProps {
|
interface ProjectCardProps {
|
||||||
project: Project;
|
project: Project;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ProjectCard({ project }: ProjectCardProps) {
|
export default function ProjectCard({ project }: ProjectCardProps) {
|
||||||
const { t, bi } = useI18n();
|
const { t, bi, lang } = useI18n();
|
||||||
const status = siteData.statuses[project.status];
|
const status = siteData.statuses[project.status];
|
||||||
const IconComponent = iconMap[project.icon];
|
const IconComponent = getIcon(project.icon);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="project-card">
|
<div className="project-card">
|
||||||
<div className="project-card-header">
|
<div className="project-card-header">
|
||||||
<div className="project-icon">
|
<div className="project-icon">{IconComponent && <IconComponent size={22} />}</div>
|
||||||
{IconComponent && <IconComponent size={22} />}
|
|
||||||
</div>
|
|
||||||
<div className="project-card-info">
|
<div className="project-card-info">
|
||||||
<div className="project-name">
|
<div className="project-name">
|
||||||
<Link to={`/projects/${project.id}`}>{project.name}</Link>
|
<Link to={`/projects/${project.id}`}>{project.displayName[lang] || project.name}</Link>
|
||||||
{status && (
|
{status && (
|
||||||
<span
|
<span className="badge badge-status" style={{ '--status-color': status.color } as React.CSSProperties}>
|
||||||
className="badge badge-status"
|
|
||||||
style={{ color: status.color }}
|
|
||||||
>
|
|
||||||
{bi(status.label)}
|
{bi(status.label)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -49,7 +33,9 @@ export default function ProjectCard({ project }: ProjectCardProps) {
|
|||||||
|
|
||||||
<div className="project-card-meta">
|
<div className="project-card-meta">
|
||||||
{project.techStack.slice(0, 4).map((tech) => (
|
{project.techStack.slice(0, 4).map((tech) => (
|
||||||
<span key={tech} className="badge">{tech}</span>
|
<span key={tech} className="badge">
|
||||||
|
{tech}
|
||||||
|
</span>
|
||||||
))}
|
))}
|
||||||
{project.platforms.slice(0, 3).map((platform) => (
|
{project.platforms.slice(0, 3).map((platform) => (
|
||||||
<span key={platform} className="badge">
|
<span key={platform} className="badge">
|
||||||
@@ -65,12 +51,7 @@ export default function ProjectCard({ project }: ProjectCardProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="project-card-actions">
|
<div className="project-card-actions">
|
||||||
<a
|
<a href={project.repoUrl} target="_blank" rel="noopener noreferrer" className="btn btn-sm">
|
||||||
href={project.repoUrl}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="btn btn-sm"
|
|
||||||
>
|
|
||||||
<ExternalLink size={14} /> GitHub
|
<ExternalLink size={14} /> GitHub
|
||||||
</a>
|
</a>
|
||||||
{project.downloads.length > 0 && (
|
{project.downloads.length > 0 && (
|
||||||
|
|||||||
@@ -38,9 +38,7 @@ export default function RoadmapGrid({ roadmap, showCounts = false }: RoadmapGrid
|
|||||||
<span>{item}</span>
|
<span>{item}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{roadmap[tab].length === 0 && (
|
{roadmap[tab].length === 0 && <div className="roadmap-empty">—</div>}
|
||||||
<div className="roadmap-empty">—</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { useState, useRef, useEffect, useCallback } from 'react';
|
||||||
|
|
||||||
|
interface ScreenshotCarouselProps {
|
||||||
|
screenshots?: string[];
|
||||||
|
placeholder?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ScreenshotCarousel({ screenshots }: ScreenshotCarouselProps) {
|
||||||
|
const [active, setActive] = useState(0);
|
||||||
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const scrollTo = useCallback((i: number) => {
|
||||||
|
setActive(i);
|
||||||
|
scrollRef.current?.children[i]?.scrollIntoView({
|
||||||
|
behavior: 'smooth',
|
||||||
|
block: 'nearest',
|
||||||
|
inline: 'center',
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const el = scrollRef.current;
|
||||||
|
if (!el) return;
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
entries.forEach((e) => {
|
||||||
|
if (e.isIntersecting) {
|
||||||
|
const idx = Array.from(el.children).indexOf(e.target);
|
||||||
|
if (idx >= 0) setActive(idx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ root: el, threshold: 0.6 }
|
||||||
|
);
|
||||||
|
Array.from(el.children).forEach((c) => observer.observe(c));
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!screenshots || screenshots.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="screenshot-carousel">
|
||||||
|
<div className="screenshot-scroll" ref={scrollRef}>
|
||||||
|
{screenshots.map((src, i) => (
|
||||||
|
<div key={i} className="screenshot-slide">
|
||||||
|
<img src={src} alt={`Screenshot ${i + 1}`} className="screenshot-image" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{screenshots.length > 1 && (
|
||||||
|
<div className="screenshot-dots">
|
||||||
|
{screenshots.map((_, i) => (
|
||||||
|
<button
|
||||||
|
key={i}
|
||||||
|
className={`screenshot-dot ${i === active ? 'active' : ''}`}
|
||||||
|
onClick={() => scrollTo(i)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -28,7 +28,8 @@ export default function SelectControl({
|
|||||||
const [query, setQuery] = useState('');
|
const [query, setQuery] = useState('');
|
||||||
|
|
||||||
const selected = options.find((option) => option.value === value) ?? options[0];
|
const selected = options.find((option) => option.value === value) ?? options[0];
|
||||||
const visibleOptions = searchable && query
|
const visibleOptions =
|
||||||
|
searchable && query
|
||||||
? options.filter((option) => option.label.toLowerCase().includes(query.toLowerCase()))
|
? options.filter((option) => option.label.toLowerCase().includes(query.toLowerCase()))
|
||||||
: options;
|
: options;
|
||||||
|
|
||||||
@@ -101,7 +102,11 @@ export default function SelectControl({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span>{option.label}</span>
|
<span>{option.label}</span>
|
||||||
{active && <span className="select-check" aria-hidden="true">✓</span>}
|
{active && (
|
||||||
|
<span className="select-check" aria-hidden="true">
|
||||||
|
✓
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -18,11 +18,11 @@ export function AppProvider({ children }: { children: ReactNode }) {
|
|||||||
}, [theme]);
|
}, [theme]);
|
||||||
|
|
||||||
const toggleTheme = useCallback(() => {
|
const toggleTheme = useCallback(() => {
|
||||||
setTheme(prev => prev === 'dark' ? 'light' : 'dark');
|
setTheme((prev) => (prev === 'dark' ? 'light' : 'dark'));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const toggleLang = useCallback(() => {
|
const toggleLang = useCallback(() => {
|
||||||
setLang(prev => {
|
setLang((prev) => {
|
||||||
const next = prev === 'zh' ? 'en' : 'zh';
|
const next = prev === 'zh' ? 'en' : 'zh';
|
||||||
localStorage.setItem('lang', next);
|
localStorage.setItem('lang', next);
|
||||||
return next;
|
return next;
|
||||||
@@ -33,7 +33,17 @@ export function AppProvider({ children }: { children: ReactNode }) {
|
|||||||
const closeMobileMenu = useCallback(() => setMobileMenuOpen(false), []);
|
const closeMobileMenu = useCallback(() => setMobileMenuOpen(false), []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppContext.Provider value={{ lang, theme, mobileMenuOpen, toggleTheme, toggleLang, openMobileMenu, closeMobileMenu }}>
|
<AppContext.Provider
|
||||||
|
value={{
|
||||||
|
lang,
|
||||||
|
theme,
|
||||||
|
mobileMenuOpen,
|
||||||
|
toggleTheme,
|
||||||
|
toggleLang,
|
||||||
|
openMobileMenu,
|
||||||
|
closeMobileMenu,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</AppContext.Provider>
|
</AppContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
+31
-31
@@ -1,36 +1,36 @@
|
|||||||
bio:
|
bio:
|
||||||
zh: "独立开发者,专注于桌面软件、移动应用和开发者工具。热爱开源,相信好的工具能让开发和创作更高效。"
|
zh: '独立开发者,专注于桌面软件、移动应用和开发者工具。热爱开源,相信好的工具能让开发和创作更高效。'
|
||||||
en: "Independent developer focused on desktop apps, mobile apps, and developer tools. Passionate about open source and building tools that make development and creation more efficient."
|
en: 'Independent developer focused on desktop apps, mobile apps, and developer tools. Passionate about open source and building tools that make development and creation more efficient.'
|
||||||
focus:
|
focus:
|
||||||
zh:
|
zh:
|
||||||
- "Tauri 桌面软件"
|
- 'Tauri 桌面软件'
|
||||||
- "Rust 后端"
|
- 'Rust 后端'
|
||||||
- "React / TypeScript 前端"
|
- 'React / TypeScript 前端'
|
||||||
- "React Native 移动端"
|
- 'React Native 移动端'
|
||||||
- "开发者工具"
|
- '开发者工具'
|
||||||
- "自托管服务"
|
- '自托管服务'
|
||||||
- "AI Agent 工具链"
|
- 'AI Agent 工具链'
|
||||||
- "开源软件产品化"
|
- '开源软件产品化'
|
||||||
en:
|
en:
|
||||||
- "Tauri desktop apps"
|
- 'Tauri desktop apps'
|
||||||
- "Rust backend"
|
- 'Rust backend'
|
||||||
- "React / TypeScript frontend"
|
- 'React / TypeScript frontend'
|
||||||
- "React Native mobile"
|
- 'React Native mobile'
|
||||||
- "Developer tools"
|
- 'Developer tools'
|
||||||
- "Self-hosted services"
|
- 'Self-hosted services'
|
||||||
- "AI Agent toolchain"
|
- 'AI Agent toolchain'
|
||||||
- "Open-source software productization"
|
- 'Open-source software productization'
|
||||||
techStack:
|
techStack:
|
||||||
- "Tauri"
|
- 'Tauri'
|
||||||
- "Rust"
|
- 'Rust'
|
||||||
- "React"
|
- 'React'
|
||||||
- "TypeScript"
|
- 'TypeScript'
|
||||||
- "React Native"
|
- 'React Native'
|
||||||
- "Expo"
|
- 'Expo'
|
||||||
- "Node.js"
|
- 'Node.js'
|
||||||
- "Python"
|
- 'Python'
|
||||||
- "SQLite"
|
- 'SQLite'
|
||||||
- "PostgreSQL"
|
- 'PostgreSQL'
|
||||||
- "Docker"
|
- 'Docker'
|
||||||
- "TailwindCSS"
|
- 'TailwindCSS'
|
||||||
github: "https://github.com/shenjianZ"
|
github: 'https://github.com/shenjianZ'
|
||||||
|
|||||||
+10
-9
@@ -1,11 +1,12 @@
|
|||||||
name:
|
name:
|
||||||
zh: "ZUJ OL 软件工坊"
|
zh: 'ZUJ OL 软件工坊'
|
||||||
en: "ZUJ OL Apps"
|
en: 'ZUJ OL Apps'
|
||||||
slogan:
|
slogan:
|
||||||
zh: "构建轻量、高效、开源的软件工具"
|
zh: '构建轻量、高效、开源的软件工具'
|
||||||
en: "Building lightweight, efficient, open-source software tools"
|
en: 'Building lightweight, efficient, open-source software tools'
|
||||||
author: "shenjianZ"
|
logo: '/logo.svg'
|
||||||
github: "https://github.com/shenjianZ"
|
author: 'shenjianZ'
|
||||||
email: "15202078626@163.com"
|
github: 'https://github.com/shenjianZ'
|
||||||
icp: "ICP备案号待填写"
|
email: '15202078626@163.com'
|
||||||
policeRecord: "公网安备号待填写"
|
icp: '豫ICP备2023019300号'
|
||||||
|
policeRecord: '豫公网安备41102502000221号'
|
||||||
|
|||||||
+28
-28
@@ -1,41 +1,41 @@
|
|||||||
- id: "desktop"
|
- id: 'desktop'
|
||||||
label:
|
label:
|
||||||
zh: "桌面软件"
|
zh: '桌面软件'
|
||||||
en: "Desktop Apps"
|
en: 'Desktop Apps'
|
||||||
icon: "Monitor"
|
icon: 'Monitor'
|
||||||
|
|
||||||
- id: "mobile"
|
- id: 'mobile'
|
||||||
label:
|
label:
|
||||||
zh: "移动应用"
|
zh: '移动应用'
|
||||||
en: "Mobile Apps"
|
en: 'Mobile Apps'
|
||||||
icon: "Smartphone"
|
icon: 'Smartphone'
|
||||||
|
|
||||||
- id: "devtool"
|
- id: 'devtool'
|
||||||
label:
|
label:
|
||||||
zh: "开发者工具"
|
zh: '开发者工具'
|
||||||
en: "Dev Tools"
|
en: 'Dev Tools'
|
||||||
icon: "Wrench"
|
icon: 'Wrench'
|
||||||
|
|
||||||
- id: "library"
|
- id: 'library'
|
||||||
label:
|
label:
|
||||||
zh: "NPM / 组件库"
|
zh: 'NPM / 组件库'
|
||||||
en: "NPM / Libraries"
|
en: 'NPM / Libraries'
|
||||||
icon: "Package"
|
icon: 'Package'
|
||||||
|
|
||||||
- id: "backend"
|
- id: 'backend'
|
||||||
label:
|
label:
|
||||||
zh: "后端服务"
|
zh: '后端服务'
|
||||||
en: "Backend"
|
en: 'Backend'
|
||||||
icon: "Cloud"
|
icon: 'Cloud'
|
||||||
|
|
||||||
- id: "selfhosted"
|
- id: 'selfhosted'
|
||||||
label:
|
label:
|
||||||
zh: "自托管"
|
zh: '自托管'
|
||||||
en: "Self-hosted"
|
en: 'Self-hosted'
|
||||||
icon: "Server"
|
icon: 'Server'
|
||||||
|
|
||||||
- id: "ai"
|
- id: 'ai'
|
||||||
label:
|
label:
|
||||||
zh: "AI / 实验项目"
|
zh: 'AI / 实验项目'
|
||||||
en: "AI / Experiments"
|
en: 'AI / Experiments'
|
||||||
icon: "FlaskConical"
|
icon: 'FlaskConical'
|
||||||
|
|||||||
+127
-125
@@ -1,125 +1,127 @@
|
|||||||
nav.search: "Search projects..."
|
nav.search: 'Search projects...'
|
||||||
nav.theme: "Toggle theme"
|
nav.theme: 'Toggle theme'
|
||||||
nav.lang: "中文"
|
nav.lang: '中文'
|
||||||
hero.title: "Building lightweight, efficient, open-source software tools"
|
hero.title: 'Building lightweight, efficient, open-source software tools'
|
||||||
hero.subtitle: "Desktop apps, mobile apps, developer tools, note-taking systems, SSH clients, remote desktop, documentation libraries, and full-stack applications."
|
hero.subtitle: 'Desktop apps, mobile apps, developer tools, note-taking systems, SSH clients, remote desktop, documentation libraries, and full-stack applications.'
|
||||||
hero.cta.projects: "View All Projects"
|
hero.cta.projects: 'View All Projects'
|
||||||
hero.cta.github: "Visit GitHub"
|
hero.cta.github: 'Visit GitHub'
|
||||||
hero.cta.download: "Download Software"
|
hero.cta.download: 'Download Software'
|
||||||
hero.cta.docs: "View Docs"
|
hero.cta.docs: 'View Docs'
|
||||||
stats.projects: "Open Source Projects"
|
stats.projects: 'Open Source Projects'
|
||||||
stats.stars: "GitHub Stars"
|
stats.stars: 'GitHub Stars'
|
||||||
stats.techStack: "Tech Stacks"
|
stats.techStack: 'Tech Stacks'
|
||||||
stats.platforms: "Platforms"
|
stats.platforms: 'Platforms'
|
||||||
featured.title: "Featured Projects"
|
featured.title: 'Featured Projects'
|
||||||
featured.subtitle: "Core projects under active development"
|
featured.subtitle: 'Core projects under active development'
|
||||||
categories.title: "Categories"
|
featured.viewAll: 'View All Projects'
|
||||||
categories.subtitle: "Browse all open-source projects by type"
|
categories.title: 'Categories'
|
||||||
latest.title: "Latest Releases"
|
categories.subtitle: 'Browse all open-source projects by type'
|
||||||
latest.subtitle: "Recent version updates"
|
latest.title: 'Latest Releases'
|
||||||
techstack.title: "Tech Stack"
|
latest.subtitle: 'Recent version updates'
|
||||||
techstack.subtitle: "Primary technologies used across projects"
|
techstack.title: 'Tech Stack'
|
||||||
cta.title: "Open Source Philosophy"
|
techstack.subtitle: 'Primary technologies used across projects'
|
||||||
cta.subtitle: "Great software should be transparent, auditable, and customizable. All project source code is available on GitHub. Contributions welcome."
|
cta.title: 'Open Source Philosophy'
|
||||||
cta.button: "Explore on GitHub"
|
cta.subtitle: 'Great software should be transparent, auditable, and customizable. All project source code is available on GitHub. Contributions welcome.'
|
||||||
projects.title: "All Projects"
|
cta.button: 'Explore on GitHub'
|
||||||
projects.subtitle: "Browse and filter all open-source software projects"
|
projects.title: 'All Projects'
|
||||||
projects.filter.all: "All"
|
projects.subtitle: 'Browse and filter all open-source software projects'
|
||||||
projects.filter.tech: "Tech Stack"
|
projects.filter.all: 'All'
|
||||||
projects.filter.platform: "Platform"
|
projects.filter.tech: 'Tech Stack'
|
||||||
projects.filter.status: "Status"
|
projects.filter.platform: 'Platform'
|
||||||
projects.sort.updated: "Recently Updated"
|
projects.filter.status: 'Status'
|
||||||
projects.sort.stars: "Most Stars"
|
projects.sort.updated: 'Recently Updated'
|
||||||
projects.sort.name: "By Name"
|
projects.sort.stars: 'Most Stars'
|
||||||
projects.noResults: "No matching projects"
|
projects.sort.name: 'By Name'
|
||||||
projects.search: "Search project name, description, or tags..."
|
projects.noResults: 'No matching projects'
|
||||||
detail.overview: "Overview"
|
projects.search: 'Search project name, description, or tags...'
|
||||||
detail.features: "Features"
|
detail.overview: 'Overview'
|
||||||
detail.screenshots: "Screenshots"
|
detail.features: 'Features'
|
||||||
detail.downloads: "Downloads"
|
detail.screenshots: 'Screenshots'
|
||||||
detail.techstack: "Tech Stack"
|
detail.downloads: 'Downloads'
|
||||||
detail.architecture: "Architecture"
|
detail.techstack: 'Tech Stack'
|
||||||
detail.roadmap: "Roadmap"
|
detail.architecture: 'Architecture'
|
||||||
detail.changelog: "Changelog"
|
detail.roadmap: 'Roadmap'
|
||||||
detail.info: "Project Info"
|
detail.changelog: 'Changelog'
|
||||||
detail.version: "Version"
|
detail.info: 'Project Info'
|
||||||
detail.license: "License"
|
detail.version: 'Version'
|
||||||
detail.platforms: "Platforms"
|
detail.license: 'License'
|
||||||
detail.status: "Status"
|
detail.platforms: 'Platforms'
|
||||||
detail.lastUpdate: "Last Updated"
|
detail.status: 'Status'
|
||||||
detail.repo: "GitHub Repo"
|
detail.lastUpdate: 'Last Updated'
|
||||||
detail.docs: "Online Docs"
|
detail.repo: 'GitHub Repo'
|
||||||
detail.release: "Download Release"
|
detail.docs: 'Online Docs'
|
||||||
detail.installGuide: "Installation Guide"
|
detail.release: 'Download Release'
|
||||||
detail.install.windows: "Windows: Double-click the installer and follow the prompts"
|
detail.installGuide: 'Installation Guide'
|
||||||
detail.install.macos: "macOS: If you see \"unidentified developer\", go to System Settings → Privacy & Security → Open Anyway"
|
detail.install.windows: 'Windows: Double-click the installer and follow the prompts'
|
||||||
detail.install.linux: "Linux: chmod +x and run, or install via package manager"
|
detail.install.macos: 'macOS: If you see "unidentified developer", go to System Settings → Privacy & Security → Open Anyway'
|
||||||
detail.install.android: "Android: Download the APK and enable unknown sources"
|
detail.install.linux: 'Linux: chmod +x and run, or install via package manager'
|
||||||
detail.roadmap.done: "Completed"
|
detail.install.android: 'Android: Download the APK and enable unknown sources'
|
||||||
detail.roadmap.doing: "In Progress"
|
detail.roadmap.done: 'Completed'
|
||||||
detail.roadmap.planned: "Planned"
|
detail.roadmap.doing: 'In Progress'
|
||||||
detail.screenshotPlaceholder: "Screenshot Preview"
|
detail.roadmap.planned: 'Planned'
|
||||||
downloads.title: "Download Center"
|
detail.screenshotPlaceholder: 'Screenshot Preview'
|
||||||
downloads.subtitle: "Download the latest versions of all open-source software"
|
downloads.title: 'Download Center'
|
||||||
downloads.fileSize: "File Size"
|
downloads.subtitle: 'Download the latest versions of all open-source software'
|
||||||
downloads.checksum: "SHA256 Checksum"
|
downloads.fileSize: 'File Size'
|
||||||
downloads.allReleases: "View All Releases"
|
downloads.checksum: 'SHA256 Checksum'
|
||||||
downloads.installGuide: "Installation Guide"
|
downloads.allReleases: 'View All Releases'
|
||||||
downloads.trustNote: "Built with Tauri for small installer size. Some versions may not be commercially code-signed, so Windows or macOS may show security warnings. All source code is available on GitHub."
|
downloads.installGuide: 'Installation Guide'
|
||||||
docs.title: "Documentation"
|
downloads.trustNote: 'Built with Tauri for small installer size. Some versions may not be commercially code-signed, so Windows or macOS may show security warnings. All source code is available on GitHub.'
|
||||||
docs.subtitle: "Quick start, usage guides, and developer documentation"
|
docs.title: 'Documentation'
|
||||||
docs.quickstart: "Quick Start"
|
docs.subtitle: 'Quick start, usage guides, and developer documentation'
|
||||||
docs.install: "Installation"
|
docs.quickstart: 'Quick Start'
|
||||||
docs.usage: "Basic Usage"
|
docs.install: 'Installation'
|
||||||
docs.advanced: "Advanced Features"
|
docs.usage: 'Basic Usage'
|
||||||
docs.config: "Configuration"
|
docs.advanced: 'Advanced Features'
|
||||||
docs.faq: "FAQ"
|
docs.config: 'Configuration'
|
||||||
docs.dev: "Developer Guide"
|
docs.faq: 'FAQ'
|
||||||
docs.deploy: "Deployment Guide"
|
docs.dev: 'Developer Guide'
|
||||||
docs.api: "API Reference"
|
docs.deploy: 'Deployment Guide'
|
||||||
docs.contribute: "Contributing"
|
docs.api: 'API Reference'
|
||||||
docs.selectProject: "Select a project to view docs"
|
docs.contribute: 'Contributing'
|
||||||
changelog.title: "Changelog"
|
docs.selectProject: 'Select a project to view docs'
|
||||||
changelog.subtitle: "Version update history for all projects"
|
changelog.title: 'Changelog'
|
||||||
changelog.all: "All Projects"
|
changelog.subtitle: 'Version update history for all projects'
|
||||||
roadmap.title: "Roadmap"
|
changelog.all: 'All Projects'
|
||||||
roadmap.subtitle: "Development plans and progress for all projects"
|
roadmap.title: 'Roadmap'
|
||||||
roadmap.all: "All Projects"
|
roadmap.subtitle: 'Development plans and progress for all projects'
|
||||||
about.title: "About"
|
roadmap.all: 'All Projects'
|
||||||
about.subtitle: "Learn about the developer and open-source philosophy"
|
about.title: 'About'
|
||||||
about.bio: "About"
|
about.subtitle: 'Learn about the developer and open-source philosophy'
|
||||||
about.focus: "Focus Areas"
|
about.bio: 'About'
|
||||||
about.techStack: "Tech Stack"
|
about.focus: 'Focus Areas'
|
||||||
about.links: "Links"
|
about.techStack: 'Tech Stack'
|
||||||
about.github: "GitHub Profile"
|
about.links: 'Links'
|
||||||
about.opensource: "Open Source Philosophy"
|
about.github: 'GitHub Profile'
|
||||||
contact.title: "Contact & Feedback"
|
about.opensource: 'Open Source Philosophy'
|
||||||
contact.subtitle: "Bug reports, feature requests, and community discussion"
|
contact.title: 'Contact & Feedback'
|
||||||
contact.issues: "Report Issues"
|
contact.subtitle: 'Bug reports, feature requests, and community discussion'
|
||||||
contact.issues.desc: "Report bugs or submit feature requests on GitHub Issues"
|
contact.issues: 'Report Issues'
|
||||||
contact.discussions: "Discussions"
|
contact.issues.desc: 'Report bugs or submit feature requests on GitHub Issues'
|
||||||
contact.discussions.desc: "Join the conversation on GitHub Discussions"
|
contact.discussions: 'Discussions'
|
||||||
contact.email: "Email"
|
contact.discussions.desc: 'Join the conversation on GitHub Discussions'
|
||||||
contact.email.desc: "Contact the author directly via email"
|
contact.email: 'Email'
|
||||||
contact.security: "Security"
|
contact.email.desc: 'Contact the author directly via email'
|
||||||
contact.security.desc: "Report security vulnerabilities through private channels"
|
contact.security: 'Security'
|
||||||
footer.slogan: "Building lightweight, efficient, open-source software tools"
|
contact.security.desc: 'Report security vulnerabilities through private channels'
|
||||||
footer.projects: "Projects"
|
footer.slogan: 'Building lightweight, efficient, open-source software tools'
|
||||||
footer.resources: "Resources"
|
footer.projects: 'Projects'
|
||||||
footer.community: "Community"
|
footer.resources: 'Resources'
|
||||||
footer.license: "License"
|
footer.community: 'Community'
|
||||||
footer.privacy: "Privacy Policy"
|
footer.license: 'License'
|
||||||
footer.security: "Security Policy"
|
footer.privacy: 'Privacy Policy'
|
||||||
footer.copyright: "© 2026 ZUJ OL. All rights reserved."
|
footer.security: 'Security Policy'
|
||||||
common.viewAll: "View All"
|
footer.copyright: '© 2026 ZUJ OL. All rights reserved.'
|
||||||
common.learnMore: "Learn More"
|
footer.viewAllProjects: 'View All'
|
||||||
common.download: "Download"
|
common.viewAll: 'View All'
|
||||||
common.docs: "Docs"
|
common.learnMore: 'Learn More'
|
||||||
common.demo: "Live Demo"
|
common.download: 'Download'
|
||||||
common.back: "Back"
|
common.docs: 'Docs'
|
||||||
common.stars: "Stars"
|
common.demo: 'Live Demo'
|
||||||
common.forks: "Forks"
|
common.back: 'Back'
|
||||||
common.version: "Version"
|
common.stars: 'Stars'
|
||||||
common.platform: "Platform"
|
common.forks: 'Forks'
|
||||||
common.size: "Size"
|
common.version: 'Version'
|
||||||
common.arch: "Arch"
|
common.platform: 'Platform'
|
||||||
|
common.size: 'Size'
|
||||||
|
common.arch: 'Arch'
|
||||||
|
|||||||
+126
-124
@@ -1,125 +1,127 @@
|
|||||||
nav.search: "搜索项目..."
|
nav.search: '搜索项目...'
|
||||||
nav.theme: "切换主题"
|
nav.theme: '切换主题'
|
||||||
nav.lang: "English"
|
nav.lang: 'English'
|
||||||
hero.title: "构建轻量、高效、开源的软件工具"
|
hero.title: '构建轻量、高效、开源的软件工具'
|
||||||
hero.subtitle: "涵盖桌面软件、移动应用、开发者工具、笔记系统、SSH 客户端、远程控制、文档组件库与全栈应用。"
|
hero.subtitle: '涵盖桌面软件、移动应用、开发者工具、笔记系统、SSH 客户端、远程控制、文档组件库与全栈应用。'
|
||||||
hero.cta.projects: "查看所有项目"
|
hero.cta.projects: '查看所有项目'
|
||||||
hero.cta.github: "访问 GitHub"
|
hero.cta.github: '访问 GitHub'
|
||||||
hero.cta.download: "下载软件"
|
hero.cta.download: '下载软件'
|
||||||
hero.cta.docs: "查看文档"
|
hero.cta.docs: '查看文档'
|
||||||
stats.projects: "开源项目"
|
stats.projects: '开源项目'
|
||||||
stats.stars: "GitHub Stars"
|
stats.stars: 'GitHub Stars'
|
||||||
stats.techStack: "技术栈"
|
stats.techStack: '技术栈'
|
||||||
stats.platforms: "支持平台"
|
stats.platforms: '支持平台'
|
||||||
featured.title: "重点项目"
|
featured.title: '重点项目'
|
||||||
featured.subtitle: "正在积极开发和推荐使用的核心项目"
|
featured.subtitle: '正在积极开发和推荐使用的核心项目'
|
||||||
categories.title: "项目分类"
|
featured.viewAll: '查看全部项目'
|
||||||
categories.subtitle: "按类型浏览所有开源项目"
|
categories.title: '项目分类'
|
||||||
latest.title: "最新发布"
|
categories.subtitle: '按类型浏览所有开源项目'
|
||||||
latest.subtitle: "最近的版本更新"
|
latest.title: '最新发布'
|
||||||
techstack.title: "技术栈"
|
latest.subtitle: '最近的版本更新'
|
||||||
techstack.subtitle: "项目中使用的主要技术"
|
techstack.title: '技术栈'
|
||||||
cta.title: "开源理念"
|
techstack.subtitle: '项目中使用的主要技术'
|
||||||
cta.subtitle: "相信好的软件应该是透明、可审计、可定制的。所有项目源代码均可在 GitHub 查看,欢迎参与贡献。"
|
cta.title: '开源理念'
|
||||||
cta.button: "在 GitHub 上探索"
|
cta.subtitle: '相信好的软件应该是透明、可审计、可定制的。所有项目源代码均可在 GitHub 查看,欢迎参与贡献。'
|
||||||
projects.title: "所有项目"
|
cta.button: '在 GitHub 上探索'
|
||||||
projects.subtitle: "浏览和筛选所有开源软件项目"
|
projects.title: '所有项目'
|
||||||
projects.filter.all: "全部"
|
projects.subtitle: '浏览和筛选所有开源软件项目'
|
||||||
projects.filter.tech: "技术栈"
|
projects.filter.all: '全部'
|
||||||
projects.filter.platform: "平台"
|
projects.filter.tech: '技术栈'
|
||||||
projects.filter.status: "状态"
|
projects.filter.platform: '平台'
|
||||||
projects.sort.updated: "最近更新"
|
projects.filter.status: '状态'
|
||||||
projects.sort.stars: "Star 最多"
|
projects.sort.updated: '最近更新'
|
||||||
projects.sort.name: "名称排序"
|
projects.sort.stars: 'Star 最多'
|
||||||
projects.noResults: "没有匹配的项目"
|
projects.sort.name: '名称排序'
|
||||||
projects.search: "搜索项目名称、描述或标签..."
|
projects.noResults: '没有匹配的项目'
|
||||||
detail.overview: "概览"
|
projects.search: '搜索项目名称、描述或标签...'
|
||||||
detail.features: "核心功能"
|
detail.overview: '概览'
|
||||||
detail.screenshots: "截图预览"
|
detail.features: '核心功能'
|
||||||
detail.downloads: "下载安装"
|
detail.screenshots: '截图预览'
|
||||||
detail.techstack: "技术栈"
|
detail.downloads: '下载安装'
|
||||||
detail.architecture: "系统架构"
|
detail.techstack: '技术栈'
|
||||||
detail.roadmap: "开发路线图"
|
detail.architecture: '系统架构'
|
||||||
detail.changelog: "更新日志"
|
detail.roadmap: '开发路线图'
|
||||||
detail.info: "项目信息"
|
detail.changelog: '更新日志'
|
||||||
detail.version: "当前版本"
|
detail.info: '项目信息'
|
||||||
detail.license: "开源协议"
|
detail.version: '当前版本'
|
||||||
detail.platforms: "支持平台"
|
detail.license: '开源协议'
|
||||||
detail.status: "开发状态"
|
detail.platforms: '支持平台'
|
||||||
detail.lastUpdate: "最后更新"
|
detail.status: '开发状态'
|
||||||
detail.repo: "GitHub 仓库"
|
detail.lastUpdate: '最后更新'
|
||||||
detail.docs: "在线文档"
|
detail.repo: 'GitHub 仓库'
|
||||||
detail.release: "下载 Release"
|
detail.docs: '在线文档'
|
||||||
detail.installGuide: "安装说明"
|
detail.release: '下载 Release'
|
||||||
detail.install.windows: "Windows:双击安装包,按提示完成安装"
|
detail.installGuide: '安装说明'
|
||||||
|
detail.install.windows: 'Windows:双击安装包,按提示完成安装'
|
||||||
detail.install.macos: 'macOS:如提示"无法验证开发者",请前往 系统设置 → 隐私与安全性 → 仍要打开'
|
detail.install.macos: 'macOS:如提示"无法验证开发者",请前往 系统设置 → 隐私与安全性 → 仍要打开'
|
||||||
detail.install.linux: "Linux:chmod +x 后运行,或使用包管理器安装"
|
detail.install.linux: 'Linux:chmod +x 后运行,或使用包管理器安装'
|
||||||
detail.install.android: "Android:下载 APK 文件,允许安装未知来源应用"
|
detail.install.android: 'Android:下载 APK 文件,允许安装未知来源应用'
|
||||||
detail.roadmap.done: "已完成"
|
detail.roadmap.done: '已完成'
|
||||||
detail.roadmap.doing: "开发中"
|
detail.roadmap.doing: '开发中'
|
||||||
detail.roadmap.planned: "计划中"
|
detail.roadmap.planned: '计划中'
|
||||||
detail.screenshotPlaceholder: "截图预览区"
|
detail.screenshotPlaceholder: '截图预览区'
|
||||||
downloads.title: "下载中心"
|
downloads.title: '下载中心'
|
||||||
downloads.subtitle: "下载所有开源软件的最新版本"
|
downloads.subtitle: '下载所有开源软件的最新版本'
|
||||||
downloads.fileSize: "文件大小"
|
downloads.fileSize: '文件大小'
|
||||||
downloads.checksum: "SHA256 校验"
|
downloads.checksum: 'SHA256 校验'
|
||||||
downloads.allReleases: "查看全部 Release"
|
downloads.allReleases: '查看全部 Release'
|
||||||
downloads.installGuide: "安装说明"
|
downloads.installGuide: '安装说明'
|
||||||
downloads.trustNote: "此软件使用 Tauri 构建,安装包体积较小。部分版本可能未进行商业代码签名,因此 Windows 或 macOS 可能出现安全提示。所有源码均可在 GitHub 查看。"
|
downloads.trustNote: '此软件使用 Tauri 构建,安装包体积较小。部分版本可能未进行商业代码签名,因此 Windows 或 macOS 可能出现安全提示。所有源码均可在 GitHub 查看。'
|
||||||
docs.title: "文档中心"
|
docs.title: '文档中心'
|
||||||
docs.subtitle: "快速开始、使用指南和开发文档"
|
docs.subtitle: '快速开始、使用指南和开发文档'
|
||||||
docs.quickstart: "快速开始"
|
docs.quickstart: '快速开始'
|
||||||
docs.install: "安装指南"
|
docs.install: '安装指南'
|
||||||
docs.usage: "基础使用"
|
docs.usage: '基础使用'
|
||||||
docs.advanced: "高级功能"
|
docs.advanced: '高级功能'
|
||||||
docs.config: "配置说明"
|
docs.config: '配置说明'
|
||||||
docs.faq: "常见问题"
|
docs.faq: '常见问题'
|
||||||
docs.dev: "开发指南"
|
docs.dev: '开发指南'
|
||||||
docs.deploy: "部署指南"
|
docs.deploy: '部署指南'
|
||||||
docs.api: "API 文档"
|
docs.api: 'API 文档'
|
||||||
docs.contribute: "贡献指南"
|
docs.contribute: '贡献指南'
|
||||||
docs.selectProject: "选择一个项目查看文档"
|
docs.selectProject: '选择一个项目查看文档'
|
||||||
changelog.title: "更新日志"
|
changelog.title: '更新日志'
|
||||||
changelog.subtitle: "所有项目的版本更新记录"
|
changelog.subtitle: '所有项目的版本更新记录'
|
||||||
changelog.all: "全部项目"
|
changelog.all: '全部项目'
|
||||||
roadmap.title: "开发路线图"
|
roadmap.title: '开发路线图'
|
||||||
roadmap.subtitle: "所有项目的开发计划和进度"
|
roadmap.subtitle: '所有项目的开发计划和进度'
|
||||||
roadmap.all: "全部项目"
|
roadmap.all: '全部项目'
|
||||||
about.title: "关于作者"
|
about.title: '关于作者'
|
||||||
about.subtitle: "了解开发者和开源理念"
|
about.subtitle: '了解开发者和开源理念'
|
||||||
about.bio: "个人简介"
|
about.bio: '个人简介'
|
||||||
about.focus: "技术方向"
|
about.focus: '技术方向'
|
||||||
about.techStack: "常用技术栈"
|
about.techStack: '常用技术栈'
|
||||||
about.links: "联系方式"
|
about.links: '联系方式'
|
||||||
about.github: "GitHub 主页"
|
about.github: 'GitHub 主页'
|
||||||
about.opensource: "开源理念"
|
about.opensource: '开源理念'
|
||||||
contact.title: "反馈与联系"
|
contact.title: '反馈与联系'
|
||||||
contact.subtitle: "问题反馈、功能建议和社区讨论"
|
contact.subtitle: '问题反馈、功能建议和社区讨论'
|
||||||
contact.issues: "提交问题"
|
contact.issues: '提交问题'
|
||||||
contact.issues.desc: "在 GitHub Issues 中报告 Bug 或提交功能请求"
|
contact.issues.desc: '在 GitHub Issues 中报告 Bug 或提交功能请求'
|
||||||
contact.discussions: "社区讨论"
|
contact.discussions: '社区讨论'
|
||||||
contact.discussions.desc: "在 GitHub Discussions 中参与讨论"
|
contact.discussions.desc: '在 GitHub Discussions 中参与讨论'
|
||||||
contact.email: "邮件联系"
|
contact.email: '邮件联系'
|
||||||
contact.email.desc: "通过邮件直接联系作者"
|
contact.email.desc: '通过邮件直接联系作者'
|
||||||
contact.security: "安全问题"
|
contact.security: '安全问题'
|
||||||
contact.security.desc: "发现安全漏洞请通过私密渠道报告"
|
contact.security.desc: '发现安全漏洞请通过私密渠道报告'
|
||||||
footer.slogan: "构建轻量、高效、开源的软件工具"
|
footer.slogan: '构建轻量、高效、开源的软件工具'
|
||||||
footer.projects: "项目"
|
footer.projects: '项目'
|
||||||
footer.resources: "资源"
|
footer.resources: '资源'
|
||||||
footer.community: "社区"
|
footer.community: '社区'
|
||||||
footer.license: "开源协议"
|
footer.license: '开源协议'
|
||||||
footer.privacy: "隐私政策"
|
footer.privacy: '隐私政策'
|
||||||
footer.security: "安全政策"
|
footer.security: '安全政策'
|
||||||
footer.copyright: "© 2026 ZUJ OL. All rights reserved."
|
footer.copyright: '© 2026 ZUJ OL. All rights reserved.'
|
||||||
common.viewAll: "查看全部"
|
footer.viewAllProjects: '查看全部'
|
||||||
common.learnMore: "了解更多"
|
common.viewAll: '查看全部'
|
||||||
common.download: "下载"
|
common.learnMore: '了解更多'
|
||||||
common.docs: "文档"
|
common.download: '下载'
|
||||||
common.demo: "在线演示"
|
common.docs: '文档'
|
||||||
common.back: "返回"
|
common.demo: '在线演示'
|
||||||
common.stars: "Stars"
|
common.back: '返回'
|
||||||
common.forks: "Forks"
|
common.stars: 'Stars'
|
||||||
common.version: "版本"
|
common.forks: 'Forks'
|
||||||
common.platform: "平台"
|
common.version: '版本'
|
||||||
common.size: "大小"
|
common.platform: '平台'
|
||||||
common.arch: "架构"
|
common.size: '大小'
|
||||||
|
common.arch: '架构'
|
||||||
|
|||||||
+5
-4
@@ -10,10 +10,11 @@ import aboutRaw from './about.yaml?raw';
|
|||||||
import zhRaw from './i18n/zh.yaml?raw';
|
import zhRaw from './i18n/zh.yaml?raw';
|
||||||
import enRaw from './i18n/en.yaml?raw';
|
import enRaw from './i18n/en.yaml?raw';
|
||||||
|
|
||||||
const projectFiles = import.meta.glob(
|
const projectFiles = import.meta.glob('./projects/*.yaml', {
|
||||||
'./projects/*.yaml',
|
eager: true,
|
||||||
{ eager: true, query: '?raw', import: 'default' },
|
query: '?raw',
|
||||||
) as Record<string, string>;
|
import: 'default',
|
||||||
|
}) as Record<string, string>;
|
||||||
|
|
||||||
const projects: Project[] = Object.values(projectFiles)
|
const projects: Project[] = Object.values(projectFiles)
|
||||||
.map((raw) => yamlParse(raw) as Project)
|
.map((raw) => yamlParse(raw) as Project)
|
||||||
|
|||||||
+12
-12
@@ -1,17 +1,17 @@
|
|||||||
- id: "home"
|
- id: 'home'
|
||||||
label:
|
label:
|
||||||
zh: "首页"
|
zh: '首页'
|
||||||
en: "Home"
|
en: 'Home'
|
||||||
hash: "#/"
|
hash: '#/'
|
||||||
|
|
||||||
- id: "projects"
|
- id: 'projects'
|
||||||
label:
|
label:
|
||||||
zh: "项目"
|
zh: '项目'
|
||||||
en: "Projects"
|
en: 'Projects'
|
||||||
hash: "#/projects"
|
hash: '#/projects'
|
||||||
|
|
||||||
- id: "about"
|
- id: 'about'
|
||||||
label:
|
label:
|
||||||
zh: "关于"
|
zh: '关于'
|
||||||
en: "About"
|
en: 'About'
|
||||||
hash: "#/about"
|
hash: '#/about'
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
windows: "Windows"
|
windows: 'Windows'
|
||||||
macos: "macOS"
|
macos: 'macOS'
|
||||||
linux: "Linux"
|
linux: 'Linux'
|
||||||
android: "Android"
|
android: 'Android'
|
||||||
ios: "iOS"
|
ios: 'iOS'
|
||||||
web: "Web"
|
web: 'Web'
|
||||||
docker: "Docker"
|
docker: 'Docker'
|
||||||
npm: "NPM"
|
npm: 'NPM'
|
||||||
cli: "CLI"
|
cli: 'CLI'
|
||||||
|
|||||||
@@ -1,93 +1,94 @@
|
|||||||
id: "billddesk"
|
id: 'billddesk'
|
||||||
name: "billddesk"
|
name: 'billddesk'
|
||||||
displayName:
|
displayName:
|
||||||
zh: "BilldDesk"
|
zh: 'BilldDesk'
|
||||||
en: "BilldDesk"
|
en: 'BilldDesk'
|
||||||
slogan:
|
slogan:
|
||||||
zh: "开源远程桌面控制方案"
|
zh: '开源远程桌面控制方案'
|
||||||
en: "Open-source remote desktop control solution"
|
en: 'Open-source remote desktop control solution'
|
||||||
description:
|
description:
|
||||||
zh: "BilldDesk 是一个基于 WebRTC 的开源远程桌面项目,支持浏览器端远程控制、文件传输和剪贴板共享。可通过 Docker 一键部署,适合需要轻量级远程支持的场景。"
|
zh: 'BilldDesk 是一个基于 WebRTC 的开源远程桌面项目,支持浏览器端远程控制、文件传输和剪贴板共享。可通过 Docker 一键部署,适合需要轻量级远程支持的场景。'
|
||||||
en: "BilldDesk is an open-source remote desktop project based on WebRTC. Supports browser-based remote control, file transfer, and clipboard sharing. Deploy with Docker in one click for lightweight remote support scenarios."
|
en: 'BilldDesk is an open-source remote desktop project based on WebRTC. Supports browser-based remote control, file transfer, and clipboard sharing. Deploy with Docker in one click for lightweight remote support scenarios.'
|
||||||
type:
|
type:
|
||||||
- "backend"
|
- 'backend'
|
||||||
- "selfhosted"
|
- 'selfhosted'
|
||||||
status: "maintained"
|
status: 'maintained'
|
||||||
platforms:
|
platforms:
|
||||||
- "web"
|
- 'web'
|
||||||
- "docker"
|
- 'docker'
|
||||||
techStack:
|
techStack:
|
||||||
- "WebRTC"
|
- 'WebRTC'
|
||||||
- "TypeScript"
|
- 'TypeScript'
|
||||||
- "Node.js"
|
- 'Node.js'
|
||||||
- "Docker"
|
- 'Docker'
|
||||||
- "React"
|
- 'React'
|
||||||
features:
|
features:
|
||||||
zh:
|
zh:
|
||||||
- "WebRTC 远程控制"
|
- 'WebRTC 远程控制'
|
||||||
- "浏览器访问"
|
- '浏览器访问'
|
||||||
- "文件传输"
|
- '文件传输'
|
||||||
- "剪贴板共享"
|
- '剪贴板共享'
|
||||||
- "Docker 部署"
|
- 'Docker 部署'
|
||||||
- "多用户管理"
|
- '多用户管理'
|
||||||
- "连接加密"
|
- '连接加密'
|
||||||
en:
|
en:
|
||||||
- "WebRTC remote control"
|
- 'WebRTC remote control'
|
||||||
- "Browser access"
|
- 'Browser access'
|
||||||
- "File transfer"
|
- 'File transfer'
|
||||||
- "Clipboard sharing"
|
- 'Clipboard sharing'
|
||||||
- "Docker deployment"
|
- 'Docker deployment'
|
||||||
- "Multi-user management"
|
- 'Multi-user management'
|
||||||
- "Encrypted connections"
|
- 'Encrypted connections'
|
||||||
tags:
|
tags:
|
||||||
- "Remote Desktop"
|
- 'Remote Desktop'
|
||||||
- "WebRTC"
|
- 'WebRTC'
|
||||||
- "Self-hosted"
|
- 'Self-hosted'
|
||||||
icon: "Monitor"
|
icon: 'Monitor'
|
||||||
repoUrl: "https://github.com/shenjianZ62/billddesk"
|
repoUrl: 'https://github.com/shenjianZ/billddesk'
|
||||||
latestVersion: "v0.8.0"
|
docsUrl: 'https://github.com/shenjianZ/billddesk#readme'
|
||||||
releaseDate: "2026-02-14"
|
latestVersion: 'v0.8.0'
|
||||||
license: "Apache-2.0"
|
releaseDate: '2026-02-14'
|
||||||
|
license: 'Apache-2.0'
|
||||||
stars: 45
|
stars: 45
|
||||||
forks: 9
|
forks: 9
|
||||||
language: "TypeScript"
|
language: 'TypeScript'
|
||||||
lastUpdated: "2026-04-20"
|
lastUpdated: '2026-04-20'
|
||||||
recommended: false
|
recommended: false
|
||||||
featured: false
|
featured: false
|
||||||
order: 6
|
order: 6
|
||||||
color: "#EF4444"
|
color: '#EF4444'
|
||||||
downloads:
|
downloads:
|
||||||
- platform: "Docker"
|
- platform: 'Docker'
|
||||||
arch: ""
|
arch: 'multi-arch'
|
||||||
url: "#"
|
url: 'https://github.com/shenjianZ/billddesk/releases/download/v0.8.0/docker-compose.yml'
|
||||||
size: "156 MB"
|
size: '156 MB'
|
||||||
sha256: "abc123"
|
sha256: ''
|
||||||
roadmap:
|
roadmap:
|
||||||
done:
|
done:
|
||||||
- "WebRTC 连接"
|
- 'WebRTC 连接'
|
||||||
- "基础远程控制"
|
- '基础远程控制'
|
||||||
- "Docker 镜像"
|
- 'Docker 镜像'
|
||||||
- "用户认证"
|
- '用户认证'
|
||||||
doing:
|
doing:
|
||||||
- "文件传输优化"
|
- '文件传输优化'
|
||||||
- "剪贴板同步"
|
- '剪贴板同步'
|
||||||
- "多显示器支持"
|
- '多显示器支持'
|
||||||
planned:
|
planned:
|
||||||
- "移动端客户端"
|
- '移动端客户端'
|
||||||
- "录制功能"
|
- '录制功能'
|
||||||
- "白板协作"
|
- '白板协作'
|
||||||
changelog:
|
changelog:
|
||||||
- version: "v0.8.0"
|
- version: 'v0.8.0'
|
||||||
date: "2026-02-14"
|
date: '2026-02-14'
|
||||||
changes:
|
changes:
|
||||||
zh:
|
zh:
|
||||||
- "新增 Docker Compose 部署"
|
- '新增 Docker Compose 部署'
|
||||||
- "优化连接稳定性"
|
- '优化连接稳定性'
|
||||||
- "新增用户管理界面"
|
- '新增用户管理界面'
|
||||||
en:
|
en:
|
||||||
- "Added Docker Compose deploy"
|
- 'Added Docker Compose deploy'
|
||||||
- "Improved connection stability"
|
- 'Improved connection stability'
|
||||||
- "Added user management UI"
|
- 'Added user management UI'
|
||||||
architecture:
|
architecture:
|
||||||
zh: "浏览器客户端 → WebRTC → 信令服务器 (Node.js) → TURN/STUN → 被控端"
|
zh: '浏览器客户端 → WebRTC → 信令服务器 (Node.js) → TURN/STUN → 被控端'
|
||||||
en: "Browser Client → WebRTC → Signaling Server (Node.js) → TURN/STUN → Controlled End"
|
en: 'Browser Client → WebRTC → Signaling Server (Node.js) → TURN/STUN → Controlled End'
|
||||||
|
|||||||
@@ -1,100 +1,100 @@
|
|||||||
id: "codex-manager"
|
id: 'codex-manager'
|
||||||
name: "codex-manager"
|
name: 'codex-manager'
|
||||||
displayName:
|
displayName:
|
||||||
zh: "Codex-Manager"
|
zh: 'Codex-Manager'
|
||||||
en: "Codex-Manager"
|
en: 'Codex-Manager'
|
||||||
slogan:
|
slogan:
|
||||||
zh: "本地账号池管理与 API 网关工具"
|
zh: '本地账号池管理与 API 网关工具'
|
||||||
en: "Local account pool manager & API gateway tool"
|
en: 'Local account pool manager & API gateway tool'
|
||||||
description:
|
description:
|
||||||
zh: "Codex-Manager 是一个基于 Tauri 构建的本地账号池管理器,用于集中管理多个 API 密钥、Token 和账号凭证。支持自动轮转、健康检查、用量统计和限额告警。"
|
zh: 'Codex-Manager 是一个基于 Tauri 构建的本地账号池管理器,用于集中管理多个 API 密钥、Token 和账号凭证。支持自动轮转、健康检查、用量统计和限额告警。'
|
||||||
en: "Codex-Manager is a Tauri-based local account pool manager for centralized management of API keys, tokens, and credentials. Supports auto-rotation, health checks, usage stats, and quota alerts."
|
en: 'Codex-Manager is a Tauri-based local account pool manager for centralized management of API keys, tokens, and credentials. Supports auto-rotation, health checks, usage stats, and quota alerts.'
|
||||||
type:
|
type:
|
||||||
- "desktop"
|
- 'desktop'
|
||||||
- "devtool"
|
- 'devtool'
|
||||||
status: "beta"
|
status: 'beta'
|
||||||
platforms:
|
platforms:
|
||||||
- "windows"
|
- 'windows'
|
||||||
- "macos"
|
- 'macos'
|
||||||
- "linux"
|
|
||||||
techStack:
|
techStack:
|
||||||
- "Tauri"
|
- 'Tauri'
|
||||||
- "React"
|
- 'React'
|
||||||
- "Rust"
|
- 'Rust'
|
||||||
- "SQLite"
|
- 'SQLite'
|
||||||
- "TypeScript"
|
- 'TypeScript'
|
||||||
features:
|
features:
|
||||||
zh:
|
zh:
|
||||||
- "账号池管理"
|
- '账号池管理'
|
||||||
- "密钥轮转"
|
- '密钥轮转'
|
||||||
- "健康检查"
|
- '健康检查'
|
||||||
- "用量统计"
|
- '用量统计'
|
||||||
- "限额告警"
|
- '限额告警'
|
||||||
- "代理配置"
|
- '代理配置'
|
||||||
- "导入导出"
|
- '导入导出'
|
||||||
en:
|
en:
|
||||||
- "Account pool management"
|
- 'Account pool management'
|
||||||
- "Key rotation"
|
- 'Key rotation'
|
||||||
- "Health checks"
|
- 'Health checks'
|
||||||
- "Usage statistics"
|
- 'Usage statistics'
|
||||||
- "Quota alerts"
|
- 'Quota alerts'
|
||||||
- "Proxy config"
|
- 'Proxy config'
|
||||||
- "Import/export"
|
- 'Import/export'
|
||||||
tags:
|
tags:
|
||||||
- "API"
|
- 'API'
|
||||||
- "Gateway"
|
- 'Gateway'
|
||||||
- "Account Management"
|
- 'Account Management'
|
||||||
- "Desktop"
|
- 'Desktop'
|
||||||
icon: "KeyRound"
|
icon: 'KeyRound'
|
||||||
repoUrl: "https://github.com/shenjianZ62/codex-manager"
|
repoUrl: 'https://github.com/shenjianZ/codex-manager'
|
||||||
latestVersion: "v0.1.0-beta"
|
docsUrl: 'https://github.com/shenjianZ/codex-manager#readme'
|
||||||
releaseDate: "2026-04-28"
|
latestVersion: 'v0.1.0-beta'
|
||||||
license: "MIT"
|
releaseDate: '2026-04-28'
|
||||||
|
license: 'MIT'
|
||||||
stars: 34
|
stars: 34
|
||||||
forks: 2
|
forks: 2
|
||||||
language: "Rust"
|
language: 'Rust'
|
||||||
lastUpdated: "2026-05-05"
|
lastUpdated: '2026-05-05'
|
||||||
recommended: false
|
recommended: false
|
||||||
featured: false
|
featured: false
|
||||||
order: 8
|
order: 8
|
||||||
color: "#EC4899"
|
color: '#EC4899'
|
||||||
downloads:
|
downloads:
|
||||||
- platform: "Windows"
|
- platform: 'Windows'
|
||||||
arch: "x64"
|
arch: 'x64'
|
||||||
url: "#"
|
url: 'https://github.com/shenjianZ/codex-manager/releases/download/v0.1.0-beta/Codex-Manager_0.1.0-beta_x64-setup.exe'
|
||||||
size: "18.7 MB"
|
size: '18.7 MB'
|
||||||
sha256: "abc123"
|
sha256: ''
|
||||||
- platform: "macOS"
|
- platform: 'macOS'
|
||||||
arch: "Apple Silicon"
|
arch: 'Apple Silicon'
|
||||||
url: "#"
|
url: 'https://github.com/shenjianZ/codex-manager/releases/download/v0.1.0-beta/Codex-Manager_0.1.0-beta_aarch64.dmg'
|
||||||
size: "15.2 MB"
|
size: '15.2 MB'
|
||||||
sha256: "def456"
|
sha256: ''
|
||||||
roadmap:
|
roadmap:
|
||||||
done:
|
done:
|
||||||
- "基础账号管理"
|
- '基础账号管理'
|
||||||
- "SQLite 存储"
|
- 'SQLite 存储'
|
||||||
- "API 密钥添加/删除"
|
- 'API 密钥添加/删除'
|
||||||
doing:
|
doing:
|
||||||
- "自动轮转策略"
|
- '自动轮转策略'
|
||||||
- "健康检查"
|
- '健康检查'
|
||||||
- "用量仪表板"
|
- '用量仪表板'
|
||||||
planned:
|
planned:
|
||||||
- "代理池集成"
|
- '代理池集成'
|
||||||
- "团队协作"
|
- '团队协作'
|
||||||
- "Web Dashboard"
|
- 'Web Dashboard'
|
||||||
- "API 网关模式"
|
- 'API 网关模式'
|
||||||
changelog:
|
changelog:
|
||||||
- version: "v0.1.0-beta"
|
- version: 'v0.1.0-beta'
|
||||||
date: "2026-04-28"
|
date: '2026-04-28'
|
||||||
changes:
|
changes:
|
||||||
zh:
|
zh:
|
||||||
- "首个测试版本"
|
- '首个测试版本'
|
||||||
- "基础账号管理"
|
- '基础账号管理'
|
||||||
- "本地存储"
|
- '本地存储'
|
||||||
en:
|
en:
|
||||||
- "First beta release"
|
- 'First beta release'
|
||||||
- "Basic account management"
|
- 'Basic account management'
|
||||||
- "Local storage"
|
- 'Local storage'
|
||||||
architecture:
|
architecture:
|
||||||
zh: "React 前端 → Tauri Commands → Rust 核心层 → SQLite → 加密凭证存储"
|
zh: 'React 前端 → Tauri Commands → Rust 核心层 → SQLite → 加密凭证存储'
|
||||||
en: "React Frontend → Tauri Commands → Rust Core → SQLite → Encrypted Credential Store"
|
en: 'React Frontend → Tauri Commands → Rust Core → SQLite → Encrypted Credential Store'
|
||||||
|
|||||||
@@ -1,120 +1,121 @@
|
|||||||
id: "devicedeck"
|
id: 'devicedeck'
|
||||||
name: "devicedeck"
|
name: 'devicedeck'
|
||||||
displayName:
|
displayName:
|
||||||
zh: "DeviceDeck"
|
zh: 'DeviceDeck'
|
||||||
en: "DeviceDeck"
|
en: 'DeviceDeck'
|
||||||
slogan:
|
slogan:
|
||||||
zh: "Android 投屏与设备调试工作台"
|
zh: 'Android 投屏与设备调试工作台'
|
||||||
en: "Android screen casting & device debugging workstation"
|
en: 'Android screen casting & device debugging workstation'
|
||||||
description:
|
description:
|
||||||
zh: "DeviceDeck 是一个基于 Tauri 和 scrcpy 构建的 Android 设备管理工具。提供设备投屏、ADB 调试、文件传输、截图录制和多设备工作台功能,帮助开发者高效管理测试设备。"
|
zh: 'DeviceDeck 是一个基于 Tauri 和 scrcpy 构建的 Android 设备管理工具。提供设备投屏、ADB 调试、文件传输、截图录制和多设备工作台功能,帮助开发者高效管理测试设备。'
|
||||||
en: "DeviceDeck is an Android device management tool built with Tauri and scrcpy. Provides screen casting, ADB debugging, file transfer, screenshot/recording, and multi-device workstation for efficient test device management."
|
en: 'DeviceDeck is an Android device management tool built with Tauri and scrcpy. Provides screen casting, ADB debugging, file transfer, screenshot/recording, and multi-device workstation for efficient test device management.'
|
||||||
type:
|
type:
|
||||||
- "desktop"
|
- 'desktop'
|
||||||
- "devtool"
|
- 'devtool'
|
||||||
status: "active"
|
status: 'active'
|
||||||
platforms:
|
platforms:
|
||||||
- "windows"
|
- 'windows'
|
||||||
- "macos"
|
- 'macos'
|
||||||
- "linux"
|
- 'linux'
|
||||||
techStack:
|
techStack:
|
||||||
- "Tauri"
|
- 'Tauri'
|
||||||
- "React"
|
- 'React'
|
||||||
- "Rust"
|
- 'Rust'
|
||||||
- "ADB"
|
- 'ADB'
|
||||||
- "scrcpy"
|
- 'scrcpy'
|
||||||
- "TypeScript"
|
- 'TypeScript'
|
||||||
features:
|
features:
|
||||||
zh:
|
zh:
|
||||||
- "scrcpy 投屏"
|
- 'scrcpy 投屏'
|
||||||
- "ADB 调试"
|
- 'ADB 调试'
|
||||||
- "文件传输"
|
- '文件传输'
|
||||||
- "截图录制"
|
- '截图录制'
|
||||||
- "多设备管理"
|
- '多设备管理'
|
||||||
- "设备信息查看"
|
- '设备信息查看'
|
||||||
- "日志查看"
|
- '日志查看'
|
||||||
- "参数配置"
|
- '参数配置'
|
||||||
en:
|
en:
|
||||||
- "scrcpy screen casting"
|
- 'scrcpy screen casting'
|
||||||
- "ADB debugging"
|
- 'ADB debugging'
|
||||||
- "File transfer"
|
- 'File transfer'
|
||||||
- "Screenshot & recording"
|
- 'Screenshot & recording'
|
||||||
- "Multi-device management"
|
- 'Multi-device management'
|
||||||
- "Device info viewer"
|
- 'Device info viewer'
|
||||||
- "Log viewer"
|
- 'Log viewer'
|
||||||
- "Parameter config"
|
- 'Parameter config'
|
||||||
tags:
|
tags:
|
||||||
- "Android"
|
- 'Android'
|
||||||
- "ADB"
|
- 'ADB'
|
||||||
- "Scrcpy"
|
- 'Scrcpy'
|
||||||
- "Debug"
|
- 'Debug'
|
||||||
icon: "Smartphone"
|
icon: 'Smartphone'
|
||||||
repoUrl: "https://github.com/shenjianZ62/devicedeck"
|
repoUrl: 'https://github.com/shenjianZ/devicedeck'
|
||||||
latestVersion: "v0.3.0"
|
docsUrl: 'https://github.com/shenjianZ/devicedeck#readme'
|
||||||
releaseDate: "2026-04-20"
|
latestVersion: 'v0.3.0'
|
||||||
license: "MIT"
|
releaseDate: '2026-04-20'
|
||||||
|
license: 'MIT'
|
||||||
stars: 72
|
stars: 72
|
||||||
forks: 6
|
forks: 6
|
||||||
language: "Rust"
|
language: 'Rust'
|
||||||
lastUpdated: "2026-05-12"
|
lastUpdated: '2026-05-12'
|
||||||
recommended: true
|
recommended: true
|
||||||
featured: true
|
featured: true
|
||||||
order: 4
|
order: 4
|
||||||
color: "#8B5CF6"
|
color: '#8B5CF6'
|
||||||
downloads:
|
downloads:
|
||||||
- platform: "Windows"
|
- platform: 'Windows'
|
||||||
arch: "x64"
|
arch: 'x64'
|
||||||
url: "#"
|
url: 'https://github.com/shenjianZ/devicedeck/releases/download/v0.3.0/DeviceDeck_0.3.0_x64-setup.exe'
|
||||||
size: "28.5 MB"
|
size: '28.5 MB'
|
||||||
sha256: "abc123"
|
sha256: ''
|
||||||
- platform: "macOS"
|
- platform: 'macOS'
|
||||||
arch: "Apple Silicon"
|
arch: 'Apple Silicon'
|
||||||
url: "#"
|
url: 'https://github.com/shenjianZ/devicedeck/releases/download/v0.3.0/DeviceDeck_0.3.0_aarch64.dmg'
|
||||||
size: "24.1 MB"
|
size: '24.1 MB'
|
||||||
sha256: "def456"
|
sha256: ''
|
||||||
- platform: "Linux"
|
- platform: 'Linux'
|
||||||
arch: "x64"
|
arch: 'x64'
|
||||||
url: "#"
|
url: 'https://github.com/shenjianZ/devicedeck/releases/download/v0.3.0/DeviceDeck_0.3.0_amd64.AppImage'
|
||||||
size: "25.8 MB"
|
size: '25.8 MB'
|
||||||
sha256: "jkl012"
|
sha256: ''
|
||||||
roadmap:
|
roadmap:
|
||||||
done:
|
done:
|
||||||
- "scrcpy 投屏"
|
- 'scrcpy 投屏'
|
||||||
- "ADB 命令执行"
|
- 'ADB 命令执行'
|
||||||
- "文件拖放传输"
|
- '文件拖放传输'
|
||||||
- "设备列表"
|
- '设备列表'
|
||||||
- "截图功能"
|
- '截图功能'
|
||||||
doing:
|
doing:
|
||||||
- "多设备同时投屏"
|
- '多设备同时投屏'
|
||||||
- "脚本录制回放"
|
- '脚本录制回放'
|
||||||
- "设备配置模板"
|
- '设备配置模板'
|
||||||
planned:
|
planned:
|
||||||
- "iOS 设备支持"
|
- 'iOS 设备支持'
|
||||||
- "无线调试"
|
- '无线调试'
|
||||||
- "自动化测试集成"
|
- '自动化测试集成'
|
||||||
changelog:
|
changelog:
|
||||||
- version: "v0.3.0"
|
- version: 'v0.3.0'
|
||||||
date: "2026-04-20"
|
date: '2026-04-20'
|
||||||
changes:
|
changes:
|
||||||
zh:
|
zh:
|
||||||
- "新增文件拖放传输"
|
- '新增文件拖放传输'
|
||||||
- "优化投屏延迟"
|
- '优化投屏延迟'
|
||||||
- "新增设备信息面板"
|
- '新增设备信息面板'
|
||||||
en:
|
en:
|
||||||
- "Added drag-drop file transfer"
|
- 'Added drag-drop file transfer'
|
||||||
- "Reduced casting latency"
|
- 'Reduced casting latency'
|
||||||
- "Added device info panel"
|
- 'Added device info panel'
|
||||||
- version: "v0.2.0"
|
- version: 'v0.2.0'
|
||||||
date: "2026-03-05"
|
date: '2026-03-05'
|
||||||
changes:
|
changes:
|
||||||
zh:
|
zh:
|
||||||
- "新增截图功能"
|
- '新增截图功能'
|
||||||
- "修复 Windows 投屏黑屏"
|
- '修复 Windows 投屏黑屏'
|
||||||
- "新增快捷键支持"
|
- '新增快捷键支持'
|
||||||
en:
|
en:
|
||||||
- "Added screenshot feature"
|
- 'Added screenshot feature'
|
||||||
- "Fixed Windows black screen"
|
- 'Fixed Windows black screen'
|
||||||
- "Added keyboard shortcuts"
|
- 'Added keyboard shortcuts'
|
||||||
architecture:
|
architecture:
|
||||||
zh: "Tauri 桌面前端 → Rust 核心层 → ADB/scrcpy 进程管理 → Android 设备"
|
zh: 'Tauri 桌面前端 → Rust 核心层 → ADB/scrcpy 进程管理 → Android 设备'
|
||||||
en: "Tauri Desktop Frontend → Rust Core → ADB/scrcpy Process Manager → Android Device"
|
en: 'Tauri Desktop Frontend → Rust Core → ADB/scrcpy Process Manager → Android Device'
|
||||||
|
|||||||
@@ -1,92 +1,93 @@
|
|||||||
id: "news-classifier"
|
id: 'news-classifier'
|
||||||
name: "news-classifier"
|
name: 'news-classifier'
|
||||||
displayName:
|
displayName:
|
||||||
zh: "news-classifier"
|
zh: 'News Classifier'
|
||||||
en: "news-classifier"
|
en: 'News Classifier'
|
||||||
slogan:
|
slogan:
|
||||||
zh: "基于机器学习的新闻自动分类系统"
|
zh: '基于机器学习的新闻自动分类系统'
|
||||||
en: "ML-powered automatic news classification system"
|
en: 'ML-powered automatic news classification system'
|
||||||
description:
|
description:
|
||||||
zh: "一个使用 Python 和 TensorFlow 构建的新闻分类系统,能够自动将新闻文章分类到预定义类别。提供 REST API 接口,支持批量处理和模型微调。"
|
zh: '一个使用 Python 和 TensorFlow 构建的新闻分类系统,能够自动将新闻文章分类到预定义类别。提供 REST API 接口,支持批量处理和模型微调。'
|
||||||
en: "A news classification system built with Python and TensorFlow that automatically categorizes news articles into predefined categories. Provides REST API, batch processing, and model fine-tuning."
|
en: 'A news classification system built with Python and TensorFlow that automatically categorizes news articles into predefined categories. Provides REST API, batch processing, and model fine-tuning.'
|
||||||
type:
|
type:
|
||||||
- "ai"
|
- 'ai'
|
||||||
- "backend"
|
- 'backend'
|
||||||
status: "experimental"
|
status: 'experimental'
|
||||||
platforms:
|
platforms:
|
||||||
- "docker"
|
- 'docker'
|
||||||
- "cli"
|
- 'cli'
|
||||||
techStack:
|
techStack:
|
||||||
- "Python"
|
- 'Python'
|
||||||
- "TensorFlow"
|
- 'TensorFlow'
|
||||||
- "FastAPI"
|
- 'FastAPI'
|
||||||
- "Docker"
|
- 'Docker'
|
||||||
- "Redis"
|
- 'Redis'
|
||||||
features:
|
features:
|
||||||
zh:
|
zh:
|
||||||
- "新闻分类"
|
- '新闻分类'
|
||||||
- "多类别支持"
|
- '多类别支持'
|
||||||
- "REST API"
|
- 'REST API'
|
||||||
- "批量处理"
|
- '批量处理'
|
||||||
- "模型微调"
|
- '模型微调'
|
||||||
- "实时预测"
|
- '实时预测'
|
||||||
- "缓存优化"
|
- '缓存优化'
|
||||||
en:
|
en:
|
||||||
- "News classification"
|
- 'News classification'
|
||||||
- "Multi-category support"
|
- 'Multi-category support'
|
||||||
- "REST API"
|
- 'REST API'
|
||||||
- "Batch processing"
|
- 'Batch processing'
|
||||||
- "Model fine-tuning"
|
- 'Model fine-tuning'
|
||||||
- "Real-time prediction"
|
- 'Real-time prediction'
|
||||||
- "Cache optimization"
|
- 'Cache optimization'
|
||||||
tags:
|
tags:
|
||||||
- "ML"
|
- 'ML'
|
||||||
- "NLP"
|
- 'NLP'
|
||||||
- "Classification"
|
- 'Classification'
|
||||||
- "API"
|
- 'API'
|
||||||
icon: "Brain"
|
icon: 'Brain'
|
||||||
repoUrl: "https://github.com/shenjianZ62/news-classifier"
|
repoUrl: 'https://github.com/shenjianZ/news-classifier'
|
||||||
latestVersion: "v0.1.0"
|
docsUrl: 'https://github.com/shenjianZ/news-classifier#readme'
|
||||||
releaseDate: "2025-12-01"
|
latestVersion: 'v0.1.0'
|
||||||
license: "MIT"
|
releaseDate: '2025-12-01'
|
||||||
|
license: 'MIT'
|
||||||
stars: 23
|
stars: 23
|
||||||
forks: 3
|
forks: 3
|
||||||
language: "Python"
|
language: 'Python'
|
||||||
lastUpdated: "2026-03-15"
|
lastUpdated: '2026-03-15'
|
||||||
recommended: false
|
recommended: false
|
||||||
featured: false
|
featured: false
|
||||||
order: 7
|
order: 7
|
||||||
color: "#F97316"
|
color: '#F97316'
|
||||||
downloads:
|
downloads:
|
||||||
- platform: "Docker"
|
- platform: 'Docker'
|
||||||
arch: ""
|
arch: 'multi-arch'
|
||||||
url: "#"
|
url: 'https://github.com/shenjianZ/news-classifier/releases/download/v0.1.0/docker-compose.yml'
|
||||||
size: "890 MB"
|
size: '890 MB'
|
||||||
sha256: "abc123"
|
sha256: ''
|
||||||
roadmap:
|
roadmap:
|
||||||
done:
|
done:
|
||||||
- "基础分类模型"
|
- '基础分类模型'
|
||||||
- "FastAPI 接口"
|
- 'FastAPI 接口'
|
||||||
- "Docker 镜像"
|
- 'Docker 镜像'
|
||||||
doing:
|
doing:
|
||||||
- "模型精度优化"
|
- '模型精度优化'
|
||||||
- "新增类别"
|
- '新增类别'
|
||||||
planned:
|
planned:
|
||||||
- "中文新闻支持"
|
- '中文新闻支持'
|
||||||
- "可视化训练面板"
|
- '可视化训练面板'
|
||||||
- "多模型对比"
|
- '多模型对比'
|
||||||
changelog:
|
changelog:
|
||||||
- version: "v0.1.0"
|
- version: 'v0.1.0'
|
||||||
date: "2025-12-01"
|
date: '2025-12-01'
|
||||||
changes:
|
changes:
|
||||||
zh:
|
zh:
|
||||||
- "首个版本"
|
- '首个版本'
|
||||||
- "基础分类 API"
|
- '基础分类 API'
|
||||||
- "预训练模型"
|
- '预训练模型'
|
||||||
en:
|
en:
|
||||||
- "First release"
|
- 'First release'
|
||||||
- "Basic classification API"
|
- 'Basic classification API'
|
||||||
- "Pre-trained model"
|
- 'Pre-trained model'
|
||||||
architecture:
|
architecture:
|
||||||
zh: "REST API (FastAPI) → 分类服务 → TensorFlow 模型 → Redis 缓存 → 数据源"
|
zh: 'REST API (FastAPI) → 分类服务 → TensorFlow 模型 → Redis 缓存 → 数据源'
|
||||||
en: "REST API (FastAPI) → Classification Service → TensorFlow Model → Redis Cache → Data Source"
|
en: 'REST API (FastAPI) → Classification Service → TensorFlow Model → Redis Cache → Data Source'
|
||||||
|
|||||||
+111
-110
@@ -1,138 +1,139 @@
|
|||||||
id: "quantanote"
|
id: 'quantanote'
|
||||||
name: "QuantaNote"
|
name: 'QuantaNote'
|
||||||
displayName:
|
displayName:
|
||||||
zh: "QuantaNote"
|
zh: 'QuantaNote'
|
||||||
en: "QuantaNote"
|
en: 'QuantaNote'
|
||||||
slogan:
|
slogan:
|
||||||
zh: "本地优先的跨平台桌面笔记与知识管理工具"
|
zh: '本地优先的跨平台桌面笔记与知识管理工具'
|
||||||
en: "Local-first cross-platform desktop note & knowledge management tool"
|
en: 'Local-first cross-platform desktop note & knowledge management tool'
|
||||||
description:
|
description:
|
||||||
zh: "QuantaNote 是一个基于 Tauri 2、Rust 和 React 构建的本地优先桌面笔记软件。面向需要离线使用、Markdown 编辑、资料归档、快速搜索和长期保存笔记的用户。相比传统云笔记,它更强调本地数据控制、轻量启动和跨平台桌面体验。"
|
zh: 'QuantaNote 是一个基于 Tauri 2、Rust 和 React 构建的本地优先桌面笔记软件。面向需要离线使用、Markdown 编辑、资料归档、快速搜索和长期保存笔记的用户。相比传统云笔记,它更强调本地数据控制、轻量启动和跨平台桌面体验。'
|
||||||
en: "QuantaNote is a local-first desktop note-taking app built with Tauri 2, Rust, and React. Designed for users who need offline Markdown editing, knowledge archiving, fast search, and long-term note storage. Emphasizes local data control, lightweight startup, and cross-platform desktop experience."
|
en: 'QuantaNote is a local-first desktop note-taking app built with Tauri 2, Rust, and React. Designed for users who need offline Markdown editing, knowledge archiving, fast search, and long-term note storage. Emphasizes local data control, lightweight startup, and cross-platform desktop experience.'
|
||||||
type:
|
type:
|
||||||
- "desktop"
|
- 'desktop'
|
||||||
- "devtool"
|
- 'devtool'
|
||||||
status: "active"
|
status: 'active'
|
||||||
platforms:
|
platforms:
|
||||||
- "windows"
|
- 'windows'
|
||||||
- "macos"
|
- 'macos'
|
||||||
- "linux"
|
- 'linux'
|
||||||
techStack:
|
techStack:
|
||||||
- "Tauri 2"
|
- 'Tauri 2'
|
||||||
- "Rust"
|
- 'Rust'
|
||||||
- "React"
|
- 'React'
|
||||||
- "TypeScript"
|
- 'TypeScript'
|
||||||
- "SQLite"
|
- 'SQLite'
|
||||||
- "TailwindCSS"
|
- 'TailwindCSS'
|
||||||
features:
|
features:
|
||||||
zh:
|
zh:
|
||||||
- "Markdown 编辑"
|
- 'Markdown 编辑'
|
||||||
- "本地 SQLite 存储"
|
- '本地 SQLite 存储'
|
||||||
- "全文搜索"
|
- '全文搜索'
|
||||||
- "标签管理"
|
- '标签管理'
|
||||||
- "附件预览"
|
- '附件预览'
|
||||||
- "版本历史"
|
- '版本历史'
|
||||||
- "导入导出"
|
- '导入导出'
|
||||||
- "自动备份"
|
- '自动备份'
|
||||||
- "主题切换"
|
- '主题切换'
|
||||||
- "系统托盘"
|
- '系统托盘'
|
||||||
- "云同步(开发中)"
|
- '云同步(开发中)'
|
||||||
en:
|
en:
|
||||||
- "Markdown editing"
|
- 'Markdown editing'
|
||||||
- "Local SQLite storage"
|
- 'Local SQLite storage'
|
||||||
- "Full-text search"
|
- 'Full-text search'
|
||||||
- "Tag management"
|
- 'Tag management'
|
||||||
- "Attachment preview"
|
- 'Attachment preview'
|
||||||
- "Version history"
|
- 'Version history'
|
||||||
- "Import/export"
|
- 'Import/export'
|
||||||
- "Auto backup"
|
- 'Auto backup'
|
||||||
- "Theme switching"
|
- 'Theme switching'
|
||||||
- "System tray"
|
- 'System tray'
|
||||||
- "Cloud sync (WIP)"
|
- 'Cloud sync (WIP)'
|
||||||
tags:
|
tags:
|
||||||
- "Markdown"
|
- 'Markdown'
|
||||||
- "Notes"
|
- 'Notes'
|
||||||
- "Knowledge Management"
|
- 'Knowledge Management'
|
||||||
- "Desktop"
|
- 'Desktop'
|
||||||
icon: "NotebookPen"
|
icon: 'NotebookPen'
|
||||||
repoUrl: "https://github.com/shenjianZ62/quantanote"
|
repoUrl: 'https://github.com/shenjianZ/quantanote'
|
||||||
latestVersion: "v0.2.0"
|
docsUrl: 'https://github.com/shenjianZ/quantanote#readme'
|
||||||
releaseDate: "2026-04-15"
|
latestVersion: 'v0.2.0'
|
||||||
license: "MIT"
|
releaseDate: '2026-04-15'
|
||||||
|
license: 'MIT'
|
||||||
stars: 128
|
stars: 128
|
||||||
forks: 12
|
forks: 12
|
||||||
language: "Rust"
|
language: 'Rust'
|
||||||
lastUpdated: "2026-05-10"
|
lastUpdated: '2026-05-10'
|
||||||
recommended: true
|
recommended: true
|
||||||
featured: true
|
featured: true
|
||||||
order: 1
|
order: 1
|
||||||
color: "#3B82F6"
|
color: '#3B82F6'
|
||||||
downloads:
|
downloads:
|
||||||
- platform: "Windows"
|
- platform: 'Windows'
|
||||||
arch: "x64"
|
arch: 'x64'
|
||||||
url: "#"
|
url: 'https://github.com/shenjianZ/quantanote/releases/download/v0.2.0/QuantaNote_0.2.0_x64-setup.exe'
|
||||||
size: "22.6 MB"
|
size: '22.6 MB'
|
||||||
sha256: "abc123"
|
sha256: ''
|
||||||
- platform: "macOS"
|
- platform: 'macOS'
|
||||||
arch: "Apple Silicon"
|
arch: 'Apple Silicon'
|
||||||
url: "#"
|
url: 'https://github.com/shenjianZ/quantanote/releases/download/v0.2.0/QuantaNote_0.2.0_aarch64.dmg'
|
||||||
size: "18.3 MB"
|
size: '18.3 MB'
|
||||||
sha256: "def456"
|
sha256: ''
|
||||||
- platform: "macOS"
|
- platform: 'macOS'
|
||||||
arch: "Intel"
|
arch: 'Intel'
|
||||||
url: "#"
|
url: 'https://github.com/shenjianZ/quantanote/releases/download/v0.2.0/QuantaNote_0.2.0_x64.dmg'
|
||||||
size: "19.1 MB"
|
size: '19.1 MB'
|
||||||
sha256: "ghi789"
|
sha256: ''
|
||||||
- platform: "Linux"
|
- platform: 'Linux'
|
||||||
arch: "x64"
|
arch: 'x64'
|
||||||
url: "#"
|
url: 'https://github.com/shenjianZ/quantanote/releases/download/v0.2.0/QuantaNote_0.2.0_amd64.AppImage'
|
||||||
size: "20.2 MB"
|
size: '20.2 MB'
|
||||||
sha256: "jkl012"
|
sha256: ''
|
||||||
roadmap:
|
roadmap:
|
||||||
done:
|
done:
|
||||||
- "基础笔记管理"
|
- '基础笔记管理'
|
||||||
- "Markdown 编辑"
|
- 'Markdown 编辑'
|
||||||
- "本地存储"
|
- '本地存储'
|
||||||
- "标签系统"
|
- '标签系统'
|
||||||
- "全文搜索"
|
- '全文搜索'
|
||||||
doing:
|
doing:
|
||||||
- "云同步"
|
- '云同步'
|
||||||
- "多端同步"
|
- '多端同步'
|
||||||
- "账号管理"
|
- '账号管理'
|
||||||
planned:
|
planned:
|
||||||
- "插件系统"
|
- '插件系统'
|
||||||
- "MCP 接入"
|
- 'MCP 接入'
|
||||||
- "移动端查看"
|
- '移动端查看'
|
||||||
- "AI 辅助写作"
|
- 'AI 辅助写作'
|
||||||
changelog:
|
changelog:
|
||||||
- version: "v0.2.0"
|
- version: 'v0.2.0'
|
||||||
date: "2026-04-15"
|
date: '2026-04-15'
|
||||||
changes:
|
changes:
|
||||||
zh:
|
zh:
|
||||||
- "新增账号管理模块"
|
- '新增账号管理模块'
|
||||||
- "修复 Token 刷新竞态"
|
- '修复 Token 刷新竞态'
|
||||||
- "优化同步状态显示"
|
- '优化同步状态显示'
|
||||||
- "新增附件预览支持"
|
- '新增附件预览支持'
|
||||||
en:
|
en:
|
||||||
- "Added account management"
|
- 'Added account management'
|
||||||
- "Fixed token refresh race condition"
|
- 'Fixed token refresh race condition'
|
||||||
- "Improved sync status display"
|
- 'Improved sync status display'
|
||||||
- "Added attachment preview"
|
- 'Added attachment preview'
|
||||||
- version: "v0.1.0"
|
- version: 'v0.1.0'
|
||||||
date: "2026-02-20"
|
date: '2026-02-20'
|
||||||
changes:
|
changes:
|
||||||
zh:
|
zh:
|
||||||
- "首个公开版本"
|
- '首个公开版本'
|
||||||
- "基础笔记 CRUD"
|
- '基础笔记 CRUD'
|
||||||
- "Markdown 编辑器"
|
- 'Markdown 编辑器'
|
||||||
- "本地 SQLite 存储"
|
- '本地 SQLite 存储'
|
||||||
- "标签管理"
|
- '标签管理'
|
||||||
en:
|
en:
|
||||||
- "First public release"
|
- 'First public release'
|
||||||
- "Basic note CRUD"
|
- 'Basic note CRUD'
|
||||||
- "Markdown editor"
|
- 'Markdown editor'
|
||||||
- "Local SQLite storage"
|
- 'Local SQLite storage'
|
||||||
- "Tag management"
|
- 'Tag management'
|
||||||
architecture:
|
architecture:
|
||||||
zh: "前端 (React + TypeScript) → Tauri Commands → Rust 核心层 → SQLite 数据库 → 本地文件存储"
|
zh: '前端 (React + TypeScript) → Tauri Commands → Rust 核心层 → SQLite 数据库 → 本地文件存储'
|
||||||
en: "Frontend (React + TypeScript) → Tauri Commands → Rust Core → SQLite Database → Local File Storage"
|
en: 'Frontend (React + TypeScript) → Tauri Commands → Rust Core → SQLite Database → Local File Storage'
|
||||||
|
|||||||
@@ -1,106 +1,106 @@
|
|||||||
id: "react-docs-ui"
|
id: 'react-docs-ui'
|
||||||
name: "react-docs-ui"
|
name: 'react-docs-ui'
|
||||||
displayName:
|
displayName:
|
||||||
zh: "react-docs-ui"
|
zh: 'React Docs UI'
|
||||||
en: "react-docs-ui"
|
en: 'React Docs UI'
|
||||||
slogan:
|
slogan:
|
||||||
zh: "基于 React 的文档组件库与站点生成器"
|
zh: '基于 React 的文档组件库与站点生成器'
|
||||||
en: "React-based documentation component library & site generator"
|
en: 'React-based documentation component library & site generator'
|
||||||
description:
|
description:
|
||||||
zh: "react-docs-ui 是一套用于构建文档站点的 React 组件库,配合 create-react-docs-ui 脚手架可以快速搭建类似 Nextra / Docusaurus 风格的文档网站。支持 MDX、全文搜索、版本切换和主题定制。"
|
zh: 'react-docs-ui 是一套用于构建文档站点的 React 组件库,配合 create-react-docs-ui 脚手架可以快速搭建类似 Nextra / Docusaurus 风格的文档网站。支持 MDX、全文搜索、版本切换和主题定制。'
|
||||||
en: "react-docs-ui is a React component library for building documentation sites. Paired with create-react-docs-ui scaffolding, it quickly sets up Nextra/Docusaurus-style docs sites. Supports MDX, full-text search, version switching, and theme customization."
|
en: 'react-docs-ui is a React component library for building documentation sites. Paired with create-react-docs-ui scaffolding, it quickly sets up Nextra/Docusaurus-style docs sites. Supports MDX, full-text search, version switching, and theme customization.'
|
||||||
type:
|
type:
|
||||||
- "library"
|
- 'library'
|
||||||
- "devtool"
|
- 'devtool'
|
||||||
status: "active"
|
status: 'active'
|
||||||
platforms:
|
platforms:
|
||||||
- "web"
|
- 'web'
|
||||||
- "npm"
|
- 'npm'
|
||||||
techStack:
|
techStack:
|
||||||
- "React"
|
- 'React'
|
||||||
- "TypeScript"
|
- 'TypeScript'
|
||||||
- "MDX"
|
- 'MDX'
|
||||||
- "TailwindCSS"
|
- 'TailwindCSS'
|
||||||
- "Vite"
|
- 'Vite'
|
||||||
features:
|
features:
|
||||||
zh:
|
zh:
|
||||||
- "MDX 支持"
|
- 'MDX 支持'
|
||||||
- "全文搜索"
|
- '全文搜索'
|
||||||
- "版本切换"
|
- '版本切换'
|
||||||
- "主题定制"
|
- '主题定制'
|
||||||
- "响应式布局"
|
- '响应式布局'
|
||||||
- "API 文档生成"
|
- 'API 文档生成'
|
||||||
- "代码高亮"
|
- '代码高亮'
|
||||||
- "国际化"
|
- '国际化'
|
||||||
en:
|
en:
|
||||||
- "MDX support"
|
- 'MDX support'
|
||||||
- "Full-text search"
|
- 'Full-text search'
|
||||||
- "Version switching"
|
- 'Version switching'
|
||||||
- "Theme customization"
|
- 'Theme customization'
|
||||||
- "Responsive layout"
|
- 'Responsive layout'
|
||||||
- "API doc generation"
|
- 'API doc generation'
|
||||||
- "Code highlighting"
|
- 'Code highlighting'
|
||||||
- "i18n"
|
- 'i18n'
|
||||||
tags:
|
tags:
|
||||||
- "Documentation"
|
- 'Documentation'
|
||||||
- "React"
|
- 'React'
|
||||||
- "NPM"
|
- 'NPM'
|
||||||
- "MDX"
|
- 'MDX'
|
||||||
icon: "BookOpen"
|
icon: 'BookOpen'
|
||||||
repoUrl: "https://github.com/shenjianZ62/react-docs-ui"
|
repoUrl: 'https://github.com/shenjianZ/react-docs-ui'
|
||||||
docsUrl: "#"
|
docsUrl: 'https://github.com/shenjianZ/react-docs-ui#readme'
|
||||||
npmUrl: "https://www.npmjs.com/package/react-docs-ui"
|
npmUrl: 'https://www.npmjs.com/package/react-docs-ui'
|
||||||
latestVersion: "v0.5.2"
|
latestVersion: 'v0.5.2'
|
||||||
releaseDate: "2026-05-10"
|
releaseDate: '2026-05-10'
|
||||||
license: "MIT"
|
license: 'MIT'
|
||||||
stars: 203
|
stars: 203
|
||||||
forks: 24
|
forks: 24
|
||||||
language: "TypeScript"
|
language: 'TypeScript'
|
||||||
lastUpdated: "2026-05-18"
|
lastUpdated: '2026-05-18'
|
||||||
recommended: true
|
recommended: true
|
||||||
featured: true
|
featured: true
|
||||||
order: 5
|
order: 5
|
||||||
color: "#06B6D4"
|
color: '#06B6D4'
|
||||||
downloads: []
|
downloads: []
|
||||||
roadmap:
|
roadmap:
|
||||||
done:
|
done:
|
||||||
- "基础组件库"
|
- '基础组件库'
|
||||||
- "MDX 渲染"
|
- 'MDX 渲染'
|
||||||
- "侧边栏导航"
|
- '侧边栏导航'
|
||||||
- "搜索功能"
|
- '搜索功能'
|
||||||
- "主题系统"
|
- '主题系统'
|
||||||
doing:
|
doing:
|
||||||
- "API 文档自动生成"
|
- 'API 文档自动生成'
|
||||||
- "版本切换"
|
- '版本切换'
|
||||||
- "性能优化"
|
- '性能优化'
|
||||||
planned:
|
planned:
|
||||||
- "插件系统"
|
- '插件系统'
|
||||||
- "评论集成"
|
- '评论集成'
|
||||||
- "多语言路由"
|
- '多语言路由'
|
||||||
- "CLI 工具"
|
- 'CLI 工具'
|
||||||
changelog:
|
changelog:
|
||||||
- version: "v0.5.2"
|
- version: 'v0.5.2'
|
||||||
date: "2026-05-10"
|
date: '2026-05-10'
|
||||||
changes:
|
changes:
|
||||||
zh:
|
zh:
|
||||||
- "修复搜索索引构建错误"
|
- '修复搜索索引构建错误'
|
||||||
- "新增代码块复制按钮"
|
- '新增代码块复制按钮'
|
||||||
- "优化移动端导航"
|
- '优化移动端导航'
|
||||||
en:
|
en:
|
||||||
- "Fixed search index build error"
|
- 'Fixed search index build error'
|
||||||
- "Added code copy button"
|
- 'Added code copy button'
|
||||||
- "Improved mobile navigation"
|
- 'Improved mobile navigation'
|
||||||
- version: "v0.5.0"
|
- version: 'v0.5.0'
|
||||||
date: "2026-04-01"
|
date: '2026-04-01'
|
||||||
changes:
|
changes:
|
||||||
zh:
|
zh:
|
||||||
- "新增主题定制"
|
- '新增主题定制'
|
||||||
- "支持 MDX 嵌入组件"
|
- '支持 MDX 嵌入组件'
|
||||||
- "新增面包屑导航"
|
- '新增面包屑导航'
|
||||||
en:
|
en:
|
||||||
- "Added theme customization"
|
- 'Added theme customization'
|
||||||
- "MDX embedded components"
|
- 'MDX embedded components'
|
||||||
- "Breadcrumb navigation"
|
- 'Breadcrumb navigation'
|
||||||
architecture:
|
architecture:
|
||||||
zh: "MDX 源文件 → Vite 构建 → React 组件渲染 → 静态文档站点"
|
zh: 'MDX 源文件 → Vite 构建 → React 组件渲染 → 静态文档站点'
|
||||||
en: "MDX Sources → Vite Build → React Component Rendering → Static Documentation Site"
|
en: 'MDX Sources → Vite Build → React Component Rendering → Static Documentation Site'
|
||||||
|
|||||||
@@ -1,126 +1,127 @@
|
|||||||
id: "ssh-terminal"
|
id: 'ssh-terminal'
|
||||||
name: "ssh-terminal"
|
name: 'ssh-terminal'
|
||||||
displayName:
|
displayName:
|
||||||
zh: "ssh-terminal"
|
zh: 'SSH Terminal'
|
||||||
en: "ssh-terminal"
|
en: 'SSH Terminal'
|
||||||
slogan:
|
slogan:
|
||||||
zh: "轻量级跨平台 SSH 客户端与终端工具"
|
zh: '轻量级跨平台 SSH 客户端与终端工具'
|
||||||
en: "Lightweight cross-platform SSH client & terminal tool"
|
en: 'Lightweight cross-platform SSH client & terminal tool'
|
||||||
description:
|
description:
|
||||||
zh: "一个基于 Tauri 和 xterm.js 构建的轻量级 SSH 客户端,支持多会话管理、内置 SFTP 文件传输、低内存占用和快速启动。适合需要频繁连接远程服务器的开发者和运维人员。"
|
zh: '一个基于 Tauri 和 xterm.js 构建的轻量级 SSH 客户端,支持多会话管理、内置 SFTP 文件传输、低内存占用和快速启动。适合需要频繁连接远程服务器的开发者和运维人员。'
|
||||||
en: "A lightweight SSH client built with Tauri and xterm.js. Supports multi-session management, built-in SFTP file transfer, low memory usage, and fast startup. Ideal for developers and ops who frequently connect to remote servers."
|
en: 'A lightweight SSH client built with Tauri and xterm.js. Supports multi-session management, built-in SFTP file transfer, low memory usage, and fast startup. Ideal for developers and ops who frequently connect to remote servers.'
|
||||||
type:
|
type:
|
||||||
- "desktop"
|
- 'desktop'
|
||||||
- "devtool"
|
- 'devtool'
|
||||||
status: "active"
|
status: 'active'
|
||||||
platforms:
|
platforms:
|
||||||
- "windows"
|
- 'windows'
|
||||||
- "macos"
|
- 'macos'
|
||||||
- "linux"
|
- 'linux'
|
||||||
techStack:
|
techStack:
|
||||||
- "Tauri"
|
- 'Tauri'
|
||||||
- "React"
|
- 'React'
|
||||||
- "Rust"
|
- 'Rust'
|
||||||
- "xterm.js"
|
- 'xterm.js'
|
||||||
- "TypeScript"
|
- 'TypeScript'
|
||||||
features:
|
features:
|
||||||
zh:
|
zh:
|
||||||
- "SSH 连接管理"
|
- 'SSH 连接管理'
|
||||||
- "多会话终端"
|
- '多会话终端'
|
||||||
- "内置 SFTP"
|
- '内置 SFTP'
|
||||||
- "轻量启动"
|
- '轻量启动'
|
||||||
- "低内存占用"
|
- '低内存占用'
|
||||||
- "跨平台桌面"
|
- '跨平台桌面'
|
||||||
- "自定义快捷键"
|
- '自定义快捷键'
|
||||||
- "终端录制"
|
- '终端录制'
|
||||||
- "主题定制"
|
- '主题定制'
|
||||||
en:
|
en:
|
||||||
- "SSH connection management"
|
- 'SSH connection management'
|
||||||
- "Multi-session terminal"
|
- 'Multi-session terminal'
|
||||||
- "Built-in SFTP"
|
- 'Built-in SFTP'
|
||||||
- "Lightweight startup"
|
- 'Lightweight startup'
|
||||||
- "Low memory usage"
|
- 'Low memory usage'
|
||||||
- "Cross-platform desktop"
|
- 'Cross-platform desktop'
|
||||||
- "Custom shortcuts"
|
- 'Custom shortcuts'
|
||||||
- "Terminal recording"
|
- 'Terminal recording'
|
||||||
- "Theme customization"
|
- 'Theme customization'
|
||||||
tags:
|
tags:
|
||||||
- "SSH"
|
- 'SSH'
|
||||||
- "Terminal"
|
- 'Terminal'
|
||||||
- "SFTP"
|
- 'SFTP'
|
||||||
- "DevOps"
|
- 'DevOps'
|
||||||
icon: "Terminal"
|
icon: 'Terminal'
|
||||||
repoUrl: "https://github.com/shenjianZ62/ssh-terminal"
|
repoUrl: 'https://github.com/shenjianZ/ssh-terminal'
|
||||||
latestVersion: "v0.1.5"
|
docsUrl: 'https://github.com/shenjianZ/ssh-terminal#readme'
|
||||||
releaseDate: "2026-03-28"
|
latestVersion: 'v0.1.5'
|
||||||
license: "MIT"
|
releaseDate: '2026-03-28'
|
||||||
|
license: 'MIT'
|
||||||
stars: 89
|
stars: 89
|
||||||
forks: 8
|
forks: 8
|
||||||
language: "Rust"
|
language: 'Rust'
|
||||||
lastUpdated: "2026-05-08"
|
lastUpdated: '2026-05-08'
|
||||||
recommended: true
|
recommended: true
|
||||||
featured: true
|
featured: true
|
||||||
order: 2
|
order: 2
|
||||||
color: "#10B981"
|
color: '#10B981'
|
||||||
downloads:
|
downloads:
|
||||||
- platform: "Windows"
|
- platform: 'Windows'
|
||||||
arch: "x64"
|
arch: 'x64'
|
||||||
url: "#"
|
url: 'https://github.com/shenjianZ/ssh-terminal/releases/download/v0.1.5/ssh-terminal_0.1.5_x64-setup.exe'
|
||||||
size: "15.2 MB"
|
size: '15.2 MB'
|
||||||
sha256: "abc123"
|
sha256: ''
|
||||||
- platform: "macOS"
|
- platform: 'macOS'
|
||||||
arch: "Apple Silicon"
|
arch: 'Apple Silicon'
|
||||||
url: "#"
|
url: 'https://github.com/shenjianZ/ssh-terminal/releases/download/v0.1.5/ssh-terminal_0.1.5_aarch64.dmg'
|
||||||
size: "12.8 MB"
|
size: '12.8 MB'
|
||||||
sha256: "def456"
|
sha256: ''
|
||||||
- platform: "macOS"
|
- platform: 'macOS'
|
||||||
arch: "Intel"
|
arch: 'Intel'
|
||||||
url: "#"
|
url: 'https://github.com/shenjianZ/ssh-terminal/releases/download/v0.1.5/ssh-terminal_0.1.5_x64.dmg'
|
||||||
size: "13.4 MB"
|
size: '13.4 MB'
|
||||||
sha256: "ghi789"
|
sha256: ''
|
||||||
- platform: "Linux"
|
- platform: 'Linux'
|
||||||
arch: "x64"
|
arch: 'x64'
|
||||||
url: "#"
|
url: 'https://github.com/shenjianZ/ssh-terminal/releases/download/v0.1.5/ssh-terminal_0.1.5_amd64.AppImage'
|
||||||
size: "14.1 MB"
|
size: '14.1 MB'
|
||||||
sha256: "jkl012"
|
sha256: ''
|
||||||
roadmap:
|
roadmap:
|
||||||
done:
|
done:
|
||||||
- "SSH 连接"
|
- 'SSH 连接'
|
||||||
- "多会话"
|
- '多会话'
|
||||||
- "SFTP 浏览器"
|
- 'SFTP 浏览器'
|
||||||
- "快捷键"
|
- '快捷键'
|
||||||
- "主题切换"
|
- '主题切换'
|
||||||
doing:
|
doing:
|
||||||
- "终端录制回放"
|
- '终端录制回放'
|
||||||
- "连接云同步"
|
- '连接云同步'
|
||||||
- "AI 命令建议"
|
- 'AI 命令建议'
|
||||||
planned:
|
planned:
|
||||||
- "端口转发 UI"
|
- '端口转发 UI'
|
||||||
- "Snippet 库"
|
- 'Snippet 库'
|
||||||
- "RDP/VNC 支持"
|
- 'RDP/VNC 支持'
|
||||||
changelog:
|
changelog:
|
||||||
- version: "v0.1.5"
|
- version: 'v0.1.5'
|
||||||
date: "2026-03-28"
|
date: '2026-03-28'
|
||||||
changes:
|
changes:
|
||||||
zh:
|
zh:
|
||||||
- "新增 SFTP 文件浏览器"
|
- '新增 SFTP 文件浏览器'
|
||||||
- "优化终端渲染性能"
|
- '优化终端渲染性能'
|
||||||
- "修复大文件传输中断"
|
- '修复大文件传输中断'
|
||||||
en:
|
en:
|
||||||
- "Added SFTP file browser"
|
- 'Added SFTP file browser'
|
||||||
- "Improved terminal rendering"
|
- 'Improved terminal rendering'
|
||||||
- "Fixed large file transfer interruption"
|
- 'Fixed large file transfer interruption'
|
||||||
- version: "v0.1.0"
|
- version: 'v0.1.0'
|
||||||
date: "2026-01-10"
|
date: '2026-01-10'
|
||||||
changes:
|
changes:
|
||||||
zh:
|
zh:
|
||||||
- "首个版本"
|
- '首个版本'
|
||||||
- "基础 SSH 连接"
|
- '基础 SSH 连接'
|
||||||
- "多会话标签页"
|
- '多会话标签页'
|
||||||
en:
|
en:
|
||||||
- "First release"
|
- 'First release'
|
||||||
- "Basic SSH connection"
|
- 'Basic SSH connection'
|
||||||
- "Multi-session tabs"
|
- 'Multi-session tabs'
|
||||||
architecture:
|
architecture:
|
||||||
zh: "前端 (React + xterm.js) → Tauri Commands → Rust SSH 层 (russh) → 远程服务器"
|
zh: '前端 (React + xterm.js) → Tauri Commands → Rust SSH 层 (russh) → 远程服务器'
|
||||||
en: "Frontend (React + xterm.js) → Tauri Commands → Rust SSH Layer (russh) → Remote Server"
|
en: 'Frontend (React + xterm.js) → Tauri Commands → Rust SSH Layer (russh) → Remote Server'
|
||||||
|
|||||||
@@ -1,99 +1,99 @@
|
|||||||
id: "streetmoment"
|
id: 'streetmoment'
|
||||||
name: "streetmoment"
|
name: 'streetmoment'
|
||||||
displayName:
|
displayName:
|
||||||
zh: "市井拾光"
|
zh: '市井拾光'
|
||||||
en: "StreetMoment"
|
en: 'StreetMoment'
|
||||||
slogan:
|
slogan:
|
||||||
zh: "记录城市生活中的美好瞬间"
|
zh: '记录城市生活中的美好瞬间'
|
||||||
en: "Capture beautiful moments in city life"
|
en: 'Capture beautiful moments in city life'
|
||||||
description:
|
description:
|
||||||
zh: "市井拾光是一款基于 React Native 和 Expo 构建的城市生活记录 App。用户可以基于地理位置发现和分享身边的美食、店铺、景点和生活瞬间,支持地图浏览、分类筛选和社区互动。"
|
zh: '市井拾光是一款基于 React Native 和 Expo 构建的城市生活记录 App。用户可以基于地理位置发现和分享身边的美食、店铺、景点和生活瞬间,支持地图浏览、分类筛选和社区互动。'
|
||||||
en: "StreetMoment is a city life recording app built with React Native and Expo. Users can discover and share food, shops, attractions, and life moments based on geographic location, with map browsing, category filtering, and community interaction."
|
en: 'StreetMoment is a city life recording app built with React Native and Expo. Users can discover and share food, shops, attractions, and life moments based on geographic location, with map browsing, category filtering, and community interaction.'
|
||||||
type:
|
type:
|
||||||
- "mobile"
|
- 'mobile'
|
||||||
status: "active"
|
status: 'active'
|
||||||
platforms:
|
platforms:
|
||||||
- "android"
|
- 'android'
|
||||||
- "ios"
|
|
||||||
techStack:
|
techStack:
|
||||||
- "React Native"
|
- 'React Native'
|
||||||
- "Expo"
|
- 'Expo'
|
||||||
- "TypeScript"
|
- 'TypeScript'
|
||||||
- "MapLibre"
|
- 'MapLibre'
|
||||||
- "Node.js"
|
- 'Node.js'
|
||||||
- "PostgreSQL"
|
- 'PostgreSQL'
|
||||||
features:
|
features:
|
||||||
zh:
|
zh:
|
||||||
- "地图探索"
|
- '地图探索'
|
||||||
- "地点发现"
|
- '地点发现'
|
||||||
- "分类浏览"
|
- '分类浏览'
|
||||||
- "拍照记录"
|
- '拍照记录'
|
||||||
- "社区分享"
|
- '社区分享'
|
||||||
- "个人主页"
|
- '个人主页'
|
||||||
- "收藏管理"
|
- '收藏管理'
|
||||||
- "搜索筛选"
|
- '搜索筛选'
|
||||||
en:
|
en:
|
||||||
- "Map exploration"
|
- 'Map exploration'
|
||||||
- "Place discovery"
|
- 'Place discovery'
|
||||||
- "Category browsing"
|
- 'Category browsing'
|
||||||
- "Photo recording"
|
- 'Photo recording'
|
||||||
- "Community sharing"
|
- 'Community sharing'
|
||||||
- "Personal profile"
|
- 'Personal profile'
|
||||||
- "Favorites"
|
- 'Favorites'
|
||||||
- "Search & filter"
|
- 'Search & filter'
|
||||||
tags:
|
tags:
|
||||||
- "Lifestyle"
|
- 'Lifestyle'
|
||||||
- "Map"
|
- 'Map'
|
||||||
- "Social"
|
- 'Social'
|
||||||
- "Mobile"
|
- 'Mobile'
|
||||||
icon: "MapPin"
|
icon: 'MapPin'
|
||||||
repoUrl: "https://github.com/shenjianZ62/streetmoment"
|
repoUrl: 'https://github.com/shenjianZ/streetmoment'
|
||||||
latestVersion: "v1.0.0"
|
docsUrl: 'https://github.com/shenjianZ/streetmoment#readme'
|
||||||
releaseDate: "2026-05-01"
|
latestVersion: 'v1.0.0'
|
||||||
license: "MIT"
|
releaseDate: '2026-05-01'
|
||||||
|
license: 'MIT'
|
||||||
stars: 56
|
stars: 56
|
||||||
forks: 5
|
forks: 5
|
||||||
language: "TypeScript"
|
language: 'TypeScript'
|
||||||
lastUpdated: "2026-05-15"
|
lastUpdated: '2026-05-15'
|
||||||
recommended: false
|
recommended: false
|
||||||
featured: true
|
featured: true
|
||||||
order: 3
|
order: 3
|
||||||
color: "#F59E0B"
|
color: '#F59E0B'
|
||||||
downloads:
|
downloads:
|
||||||
- platform: "Android"
|
- platform: 'Android'
|
||||||
arch: "arm64"
|
arch: 'arm64'
|
||||||
url: "#"
|
url: 'https://github.com/shenjianZ/streetmoment/releases/download/v1.0.0/streetmoment-1.0.0-arm64.apk'
|
||||||
size: "32.4 MB"
|
size: '32.4 MB'
|
||||||
sha256: "abc123"
|
sha256: ''
|
||||||
roadmap:
|
roadmap:
|
||||||
done:
|
done:
|
||||||
- "地图浏览"
|
- '地图浏览'
|
||||||
- "地点标记"
|
- '地点标记'
|
||||||
- "分类系统"
|
- '分类系统'
|
||||||
- "用户注册"
|
- '用户注册'
|
||||||
- "拍照上传"
|
- '拍照上传'
|
||||||
doing:
|
doing:
|
||||||
- "评论互动"
|
- '评论互动'
|
||||||
- "消息通知"
|
- '消息通知'
|
||||||
- "个性化推荐"
|
- '个性化推荐'
|
||||||
planned:
|
planned:
|
||||||
- "iOS 版发布"
|
- 'iOS 版发布'
|
||||||
- "小程序版本"
|
- '小程序版本'
|
||||||
- "AR 探索模式"
|
- 'AR 探索模式'
|
||||||
changelog:
|
changelog:
|
||||||
- version: "v1.0.0"
|
- version: 'v1.0.0'
|
||||||
date: "2026-05-01"
|
date: '2026-05-01'
|
||||||
changes:
|
changes:
|
||||||
zh:
|
zh:
|
||||||
- "正式发布"
|
- '正式发布'
|
||||||
- "地图浏览优化"
|
- '地图浏览优化'
|
||||||
- "新增分类筛选"
|
- '新增分类筛选'
|
||||||
- "修复定位偏差"
|
- '修复定位偏差'
|
||||||
en:
|
en:
|
||||||
- "Official release"
|
- 'Official release'
|
||||||
- "Map browsing optimization"
|
- 'Map browsing optimization'
|
||||||
- "Added category filter"
|
- 'Added category filter'
|
||||||
- "Fixed location offset"
|
- 'Fixed location offset'
|
||||||
architecture:
|
architecture:
|
||||||
zh: "React Native App → REST API → Node.js 服务 → PostgreSQL + Map Tiles → 对象存储"
|
zh: 'React Native App → REST API → Node.js 服务 → PostgreSQL + Map Tiles → 对象存储'
|
||||||
en: "React Native App → REST API → Node.js Service → PostgreSQL + Map Tiles → Object Storage"
|
en: 'React Native App → REST API → Node.js Service → PostgreSQL + Map Tiles → Object Storage'
|
||||||
|
|||||||
+15
-15
@@ -1,29 +1,29 @@
|
|||||||
active:
|
active:
|
||||||
label:
|
label:
|
||||||
zh: "活跃开发"
|
zh: '活跃开发'
|
||||||
en: "Active"
|
en: 'Active'
|
||||||
color: "#b0b0b0"
|
color: '#22C55E'
|
||||||
|
|
||||||
maintained:
|
maintained:
|
||||||
label:
|
label:
|
||||||
zh: "维护中"
|
zh: '维护中'
|
||||||
en: "Maintained"
|
en: 'Maintained'
|
||||||
color: "#909090"
|
color: '#3B82F6'
|
||||||
|
|
||||||
beta:
|
beta:
|
||||||
label:
|
label:
|
||||||
zh: "测试版"
|
zh: '测试版'
|
||||||
en: "Beta"
|
en: 'Beta'
|
||||||
color: "#707070"
|
color: '#F59E0B'
|
||||||
|
|
||||||
experimental:
|
experimental:
|
||||||
label:
|
label:
|
||||||
zh: "实验性"
|
zh: '实验性'
|
||||||
en: "Experimental"
|
en: 'Experimental'
|
||||||
color: "#555555"
|
color: '#A855F7'
|
||||||
|
|
||||||
archived:
|
archived:
|
||||||
label:
|
label:
|
||||||
zh: "已归档"
|
zh: '已归档'
|
||||||
en: "Archived"
|
en: 'Archived'
|
||||||
color: "#404040"
|
color: '#6B7280'
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { siteData } from '../data/siteData';
|
|||||||
import { useI18n } from './useI18n';
|
import { useI18n } from './useI18n';
|
||||||
|
|
||||||
export function useProjectFilters() {
|
export function useProjectFilters() {
|
||||||
const { bi } = useI18n();
|
const { bi, lang } = useI18n();
|
||||||
const projects = siteData.projects;
|
const projects = siteData.projects;
|
||||||
|
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
@@ -12,40 +12,54 @@ export function useProjectFilters() {
|
|||||||
const [status, setStatus] = useState('');
|
const [status, setStatus] = useState('');
|
||||||
const [sort, setSort] = useState('updated');
|
const [sort, setSort] = useState('updated');
|
||||||
|
|
||||||
const allTech = useMemo(() => [...new Set(projects.flatMap(p => p.techStack))].sort(), [projects]);
|
const allTech = useMemo(
|
||||||
const allPlatforms = useMemo(() => [...new Set(projects.flatMap(p => p.platforms))].sort(), [projects]);
|
() => [...new Set(projects.flatMap((p) => p.techStack))].sort(),
|
||||||
const allStatuses = useMemo(() => [...new Set(projects.map(p => p.status))], [projects]);
|
[projects]
|
||||||
|
);
|
||||||
|
const allPlatforms = useMemo(
|
||||||
|
() => [...new Set(projects.flatMap((p) => p.platforms))].sort(),
|
||||||
|
[projects]
|
||||||
|
);
|
||||||
|
const allStatuses = useMemo(() => [...new Set(projects.map((p) => p.status))], [projects]);
|
||||||
|
|
||||||
const filteredProjects = useMemo(() => {
|
const filteredProjects = useMemo(() => {
|
||||||
let result = [...projects];
|
let result = [...projects];
|
||||||
|
|
||||||
if (search) {
|
if (search) {
|
||||||
const q = search.toLowerCase();
|
const q = search.toLowerCase();
|
||||||
result = result.filter(p =>
|
result = result.filter(
|
||||||
|
(p) =>
|
||||||
p.name.toLowerCase().includes(q) ||
|
p.name.toLowerCase().includes(q) ||
|
||||||
bi(p.slogan).toLowerCase().includes(q) ||
|
bi(p.slogan).toLowerCase().includes(q) ||
|
||||||
p.tags.some(tag => tag.toLowerCase().includes(q)) ||
|
p.tags.some((tag) => tag.toLowerCase().includes(q)) ||
|
||||||
p.techStack.some(ts => ts.toLowerCase().includes(q))
|
p.techStack.some((ts) => ts.toLowerCase().includes(q))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (tech) result = result.filter(p => p.techStack.includes(tech));
|
if (tech) result = result.filter((p) => p.techStack.includes(tech));
|
||||||
if (platform) result = result.filter(p => p.platforms.includes(platform));
|
if (platform) result = result.filter((p) => p.platforms.includes(platform));
|
||||||
if (status) result = result.filter(p => p.status === status);
|
if (status) result = result.filter((p) => p.status === status);
|
||||||
|
|
||||||
if (sort === 'stars') result.sort((a, b) => b.stars - a.stars);
|
if (sort === 'stars') result.sort((a, b) => b.stars - a.stars);
|
||||||
else if (sort === 'name') result.sort((a, b) => a.name.localeCompare(b.name));
|
else if (sort === 'name') result.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
else result.sort((a, b) => b.lastUpdated.localeCompare(a.lastUpdated));
|
else result.sort((a, b) => b.lastUpdated.localeCompare(a.lastUpdated));
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}, [projects, search, tech, platform, status, sort, bi]);
|
}, [projects, search, tech, platform, status, sort, lang, bi]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
search, setSearch,
|
search,
|
||||||
tech, setTech,
|
setSearch,
|
||||||
platform, setPlatform,
|
tech,
|
||||||
status, setStatus,
|
setTech,
|
||||||
sort, setSort,
|
platform,
|
||||||
allTech, allPlatforms, allStatuses,
|
setPlatform,
|
||||||
|
status,
|
||||||
|
setStatus,
|
||||||
|
sort,
|
||||||
|
setSort,
|
||||||
|
allTech,
|
||||||
|
allPlatforms,
|
||||||
|
allStatuses,
|
||||||
filteredProjects,
|
filteredProjects,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-8
@@ -1,9 +1,9 @@
|
|||||||
import { StrictMode } from 'react'
|
import { StrictMode } from 'react';
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client';
|
||||||
import { HashRouter } from 'react-router-dom'
|
import { HashRouter } from 'react-router-dom';
|
||||||
import { AppProvider } from './contexts/AppContext'
|
import { AppProvider } from './contexts/AppContext';
|
||||||
import App from './App.tsx'
|
import App from './App.tsx';
|
||||||
import './style.css'
|
import './style.css';
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
@@ -12,5 +12,5 @@ createRoot(document.getElementById('root')!).render(
|
|||||||
<App />
|
<App />
|
||||||
</AppProvider>
|
</AppProvider>
|
||||||
</HashRouter>
|
</HashRouter>
|
||||||
</StrictMode>,
|
</StrictMode>
|
||||||
)
|
);
|
||||||
|
|||||||
+16
-59
@@ -1,40 +1,7 @@
|
|||||||
import { Bug, ExternalLink, MessageCircle, Shield } from 'lucide-react';
|
import { ExternalLink, Mail } from 'lucide-react';
|
||||||
import type { LucideIcon } from 'lucide-react';
|
|
||||||
import { useI18n } from '../hooks/useI18n';
|
import { useI18n } from '../hooks/useI18n';
|
||||||
import { siteData } from '../data/siteData';
|
import { siteData } from '../data/siteData';
|
||||||
|
|
||||||
interface ContactCard {
|
|
||||||
icon: LucideIcon;
|
|
||||||
titleKey: string;
|
|
||||||
descKey: string;
|
|
||||||
href: string;
|
|
||||||
label: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const contactCards: ContactCard[] = [
|
|
||||||
{
|
|
||||||
icon: Bug,
|
|
||||||
titleKey: 'contact.issues',
|
|
||||||
descKey: 'contact.issues.desc',
|
|
||||||
href: `${siteData.brand.github}/issues`,
|
|
||||||
label: 'GitHub Issues',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: MessageCircle,
|
|
||||||
titleKey: 'contact.discussions',
|
|
||||||
descKey: 'contact.discussions.desc',
|
|
||||||
href: `${siteData.brand.github}/discussions`,
|
|
||||||
label: 'GitHub Discussions',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: Shield,
|
|
||||||
titleKey: 'contact.security',
|
|
||||||
descKey: 'contact.security.desc',
|
|
||||||
href: `${siteData.brand.github}/security`,
|
|
||||||
label: 'Security',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function AboutPage() {
|
export default function AboutPage() {
|
||||||
const { t, bi, biArray } = useI18n();
|
const { t, bi, biArray } = useI18n();
|
||||||
const { about, brand } = siteData;
|
const { about, brand } = siteData;
|
||||||
@@ -47,7 +14,9 @@ export default function AboutPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="about-header">
|
<div className="about-header">
|
||||||
<div className="about-avatar">SZ</div>
|
<div className="about-avatar">
|
||||||
|
<img src="/avatar.jpg" alt={brand.author} className="about-avatar-img" />
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="about-name">{brand.author}</div>
|
<div className="about-name">{brand.author}</div>
|
||||||
<div className="about-bio">{bi(about.bio)}</div>
|
<div className="about-bio">{bi(about.bio)}</div>
|
||||||
@@ -60,7 +29,9 @@ export default function AboutPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="focus-grid">
|
<div className="focus-grid">
|
||||||
{biArray(about.focus).map((item) => (
|
{biArray(about.focus).map((item) => (
|
||||||
<div key={item} className="focus-item">{item}</div>
|
<div key={item} className="focus-item">
|
||||||
|
{item}
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -71,7 +42,9 @@ export default function AboutPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="techstack-grid">
|
<div className="techstack-grid">
|
||||||
{about.techStack.map((tech) => (
|
{about.techStack.map((tech) => (
|
||||||
<span key={tech} className="tech-tag">{tech}</span>
|
<span key={tech} className="tech-tag">
|
||||||
|
{tech}
|
||||||
|
</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -88,31 +61,15 @@ export default function AboutPage() {
|
|||||||
<h2 className="section-title">{t('contact.title')}</h2>
|
<h2 className="section-title">{t('contact.title')}</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className="contact-grid">
|
<div className="contact-grid">
|
||||||
{contactCards.map((card) => {
|
<a href={`mailto:${brand.email}`} className="contact-card">
|
||||||
const Icon = card.icon;
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
key={card.titleKey}
|
|
||||||
href={card.href}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="contact-card"
|
|
||||||
>
|
|
||||||
<h3>
|
<h3>
|
||||||
<Icon size={18} />
|
<Mail size={18} />
|
||||||
{t(card.titleKey)}
|
{t('contact.email')}
|
||||||
</h3>
|
</h3>
|
||||||
<p>{t(card.descKey)}</p>
|
<p>{t('contact.email.desc')}</p>
|
||||||
<span className="contact-card-link">{card.label}</span>
|
<span className="contact-card-link">{brand.email}</span>
|
||||||
</a>
|
</a>
|
||||||
);
|
<a href={about.github} target="_blank" rel="noopener noreferrer" className="contact-card">
|
||||||
})}
|
|
||||||
<a
|
|
||||||
href={about.github}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="contact-card"
|
|
||||||
>
|
|
||||||
<h3>
|
<h3>
|
||||||
<ExternalLink size={18} />
|
<ExternalLink size={18} />
|
||||||
{t('about.github')}
|
{t('about.github')}
|
||||||
|
|||||||
+16
-5
@@ -1,3 +1,4 @@
|
|||||||
|
import { Link } from 'react-router-dom';
|
||||||
import { ExternalLink } from 'lucide-react';
|
import { ExternalLink } from 'lucide-react';
|
||||||
import { useI18n } from '../hooks/useI18n';
|
import { useI18n } from '../hooks/useI18n';
|
||||||
import { siteData } from '../data/siteData';
|
import { siteData } from '../data/siteData';
|
||||||
@@ -9,7 +10,9 @@ import FeaturedCard from '../components/FeaturedCard';
|
|||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const featuredProjects = siteData.projects.filter((p) => p.featured);
|
const featuredProjects = siteData.projects
|
||||||
|
.filter((p) => p.featured)
|
||||||
|
.slice(0, 6);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fade-in">
|
<div className="fade-in">
|
||||||
@@ -18,7 +21,15 @@ export default function HomePage() {
|
|||||||
<StatsBar />
|
<StatsBar />
|
||||||
|
|
||||||
<div className="section">
|
<div className="section">
|
||||||
<SectionHeader title={t('featured.title')} subtitle={t('featured.subtitle')} />
|
<SectionHeader
|
||||||
|
title={t('featured.title')}
|
||||||
|
subtitle={t('featured.subtitle')}
|
||||||
|
action={
|
||||||
|
<Link to="/projects" className="section-action">
|
||||||
|
{t('featured.viewAll')} →
|
||||||
|
</Link>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<div className="card-grid">
|
<div className="card-grid">
|
||||||
{featuredProjects.map((p) => (
|
{featuredProjects.map((p) => (
|
||||||
<FeaturedCard key={p.id} project={p} />
|
<FeaturedCard key={p.id} project={p} />
|
||||||
@@ -26,12 +37,12 @@ export default function HomePage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="section section-compact" style={{ textAlign: 'center' }}>
|
<div className="section section-compact cta-section">
|
||||||
<h2 className="section-title">{t('cta.title')}</h2>
|
<h2 className="section-title">{t('cta.title')}</h2>
|
||||||
<p className="section-subtitle" style={{ margin: '8px auto 0', maxWidth: 600 }}>
|
<p className="section-subtitle cta-subtitle">
|
||||||
{t('cta.subtitle')}
|
{t('cta.subtitle')}
|
||||||
</p>
|
</p>
|
||||||
<div style={{ marginTop: 24 }}>
|
<div className="cta-actions">
|
||||||
<a
|
<a
|
||||||
href={siteData.brand.github}
|
href={siteData.brand.github}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
|||||||
@@ -4,36 +4,40 @@ import { siteData } from '../data/siteData';
|
|||||||
import DownloadTable from '../components/DownloadTable';
|
import DownloadTable from '../components/DownloadTable';
|
||||||
import RoadmapGrid from '../components/RoadmapGrid';
|
import RoadmapGrid from '../components/RoadmapGrid';
|
||||||
import ChangelogList from '../components/ChangelogList';
|
import ChangelogList from '../components/ChangelogList';
|
||||||
import { NotebookPen, Terminal, MapPin, Smartphone, BookOpen, Monitor, Brain, KeyRound, ExternalLink, Download } from 'lucide-react';
|
import ScreenshotCarousel from '../components/ScreenshotCarousel';
|
||||||
import { useState, useRef, useEffect, useCallback } from 'react';
|
import { getIcon } from '../utils/iconRegistry';
|
||||||
import type { ComponentType } from 'react';
|
import { ExternalLink, Download, BookOpen } from 'lucide-react';
|
||||||
|
|
||||||
const iconMap: Record<string, ComponentType<{ size?: number }>> = {
|
|
||||||
NotebookPen, Terminal, MapPin, Smartphone, BookOpen, Monitor, Brain, KeyRound,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function ProjectDetailPage() {
|
export default function ProjectDetailPage() {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const { t, bi, biArray, lang } = useI18n();
|
const { t, bi, biArray, lang } = useI18n();
|
||||||
const p = siteData.projects.find(pr => pr.id === id);
|
const p = siteData.projects.find((pr) => pr.id === id);
|
||||||
|
|
||||||
if (!p) {
|
if (!p) {
|
||||||
return <div className="container"><div className="empty-state">Project not found</div></div>;
|
return (
|
||||||
|
<div className="container">
|
||||||
|
<div className="empty-state">Project not found</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const statusDef = siteData.statuses[p.status];
|
const statusDef = siteData.statuses[p.status];
|
||||||
const hasDownloads = p.downloads && p.downloads.length > 0;
|
const hasDownloads = p.downloads && p.downloads.length > 0;
|
||||||
const IconComponent = iconMap[p.icon];
|
const IconComponent = getIcon(p.icon);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container fade-in">
|
<div className="container fade-in">
|
||||||
{/* Breadcrumb */}
|
{/* Breadcrumb */}
|
||||||
<div style={{ padding: '8px 0', fontSize: 13, color: 'var(--muted)' }}>
|
<div className="breadcrumb">
|
||||||
<Link to="/" style={{ color: 'var(--muted)' }}>{bi(siteData.nav[0].label)}</Link>
|
<Link to="/">
|
||||||
|
{bi(siteData.nav[0].label)}
|
||||||
|
</Link>
|
||||||
{' / '}
|
{' / '}
|
||||||
<Link to="/projects" style={{ color: 'var(--muted)' }}>{bi(siteData.nav[1].label)}</Link>
|
<Link to="/projects">
|
||||||
|
{bi(siteData.nav[1].label)}
|
||||||
|
</Link>
|
||||||
{' / '}
|
{' / '}
|
||||||
<span style={{ color: 'var(--fg)' }}>{p.name}</span>
|
<span className="breadcrumb-current">{p.name}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@@ -48,15 +52,47 @@ export default function ProjectDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="detail-badges">
|
<div className="detail-badges">
|
||||||
{p.techStack.map(ts => <span key={ts} className="badge badge-accent">{ts}</span>)}
|
{p.techStack.map((ts) => (
|
||||||
{p.platforms.map(pl => <span key={pl} className="badge">{pl}</span>)}
|
<span key={ts} className="badge badge-accent">
|
||||||
<span className="badge badge-status" style={{ background: statusDef?.color || '#6B6B6B' }}>{bi(statusDef?.label)}</span>
|
{ts}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
{p.platforms.map((pl) => (
|
||||||
|
<span key={pl} className="badge">
|
||||||
|
{pl}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
<span
|
||||||
|
className="badge badge-status"
|
||||||
|
style={{ background: statusDef?.color || '#6B6B6B' }}
|
||||||
|
>
|
||||||
|
{bi(statusDef?.label)}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="detail-actions">
|
<div className="detail-actions">
|
||||||
<a href={p.repoUrl} target="_blank" className="btn btn-primary"><ExternalLink size={16} />GitHub</a>
|
<a href={p.repoUrl} target="_blank" className="btn btn-primary">
|
||||||
{hasDownloads && <Link to="/downloads" className="btn"><Download size={16} />{t('common.download')}</Link>}
|
<ExternalLink size={16} />
|
||||||
{p.docsUrl && <a href={p.docsUrl} className="btn"><BookOpen size={16} />{t('common.docs')}</a>}
|
GitHub
|
||||||
<a href={`${p.repoUrl}/issues`} target="_blank" className="btn btn-ghost"><ExternalLink size={16} />{t('contact.issues')}</a>
|
</a>
|
||||||
|
{hasDownloads && (
|
||||||
|
<a href="#downloads" className="btn" onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
document.getElementById('downloads')?.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
}}>
|
||||||
|
<Download size={16} />
|
||||||
|
{t('common.download')}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
{p.docsUrl && (
|
||||||
|
<a href={p.docsUrl} target="_blank" rel="noopener noreferrer" className="btn">
|
||||||
|
<BookOpen size={16} />
|
||||||
|
{t('common.docs')}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
<a href={`${p.repoUrl}/issues`} target="_blank" className="btn btn-ghost">
|
||||||
|
<ExternalLink size={16} />
|
||||||
|
{t('contact.issues')}
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -73,26 +109,31 @@ export default function ProjectDetailPage() {
|
|||||||
<div className="detail-section">
|
<div className="detail-section">
|
||||||
<h2 className="detail-section-title">{t('detail.features')}</h2>
|
<h2 className="detail-section-title">{t('detail.features')}</h2>
|
||||||
<div className="feature-tags">
|
<div className="feature-tags">
|
||||||
{biArray(p.features).map((f, i) => <span key={i} className="feature-tag">{f}</span>)}
|
{biArray(p.features).map((f, i) => (
|
||||||
|
<span key={i} className="feature-tag">
|
||||||
|
{f}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Screenshots */}
|
{/* Screenshots */}
|
||||||
|
{p.screenshots && p.screenshots.length > 0 && (
|
||||||
<div className="detail-section">
|
<div className="detail-section">
|
||||||
<h2 className="detail-section-title">{t('detail.screenshots')}</h2>
|
<h2 className="detail-section-title">{t('detail.screenshots')}</h2>
|
||||||
<ScreenshotCarousel count={3} placeholder={t('detail.screenshotPlaceholder')} />
|
<ScreenshotCarousel screenshots={p.screenshots} />
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Downloads */}
|
{/* Downloads */}
|
||||||
{hasDownloads && (
|
{hasDownloads && (
|
||||||
<div className="detail-section">
|
<div className="detail-section" id="downloads">
|
||||||
<h2 className="detail-section-title">{t('detail.downloads')}</h2>
|
<h2 className="detail-section-title">{t('detail.downloads')}</h2>
|
||||||
<DownloadTable downloads={p.downloads} />
|
<DownloadTable downloads={p.downloads} />
|
||||||
<div className="trust-note">{t('downloads.trustNote')}</div>
|
<div className="trust-note">{t('downloads.trustNote')}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
{/* Roadmap */}
|
{/* Roadmap */}
|
||||||
{p.roadmap && (
|
{p.roadmap && (
|
||||||
<div className="detail-section">
|
<div className="detail-section">
|
||||||
@@ -113,10 +154,24 @@ export default function ProjectDetailPage() {
|
|||||||
<div className="detail-section">
|
<div className="detail-section">
|
||||||
<h2 className="detail-section-title">{t('detail.installGuide')}</h2>
|
<h2 className="detail-section-title">{t('detail.installGuide')}</h2>
|
||||||
<div className="install-list">
|
<div className="install-list">
|
||||||
<div className="install-item"><strong>Windows</strong>{t('detail.install.windows')}</div>
|
<div className="install-item">
|
||||||
<div className="install-item"><strong>macOS</strong>{t('detail.install.macos')}</div>
|
<strong>Windows</strong>
|
||||||
<div className="install-item"><strong>Linux</strong>{t('detail.install.linux')}</div>
|
{t('detail.install.windows')}
|
||||||
{p.platforms.includes('android') && <div className="install-item"><strong>Android</strong>{t('detail.install.android')}</div>}
|
</div>
|
||||||
|
<div className="install-item">
|
||||||
|
<strong>macOS</strong>
|
||||||
|
{t('detail.install.macos')}
|
||||||
|
</div>
|
||||||
|
<div className="install-item">
|
||||||
|
<strong>Linux</strong>
|
||||||
|
{t('detail.install.linux')}
|
||||||
|
</div>
|
||||||
|
{p.platforms.includes('android') && (
|
||||||
|
<div className="install-item">
|
||||||
|
<strong>Android</strong>
|
||||||
|
{t('detail.install.android')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -154,49 +209,19 @@ export default function ProjectDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="detail-link-grid">
|
<div className="detail-link-grid">
|
||||||
<a href={p.repoUrl} target="_blank" className="detail-link-btn"><ExternalLink size={15} />{t('detail.repo')}</a>
|
<a href={p.repoUrl} target="_blank" className="detail-link-btn">
|
||||||
{p.docsUrl && <a href={p.docsUrl} className="detail-link-btn"><BookOpen size={15} />{t('detail.docs')}</a>}
|
<ExternalLink size={15} />
|
||||||
|
{t('detail.repo')}
|
||||||
|
</a>
|
||||||
|
{p.docsUrl && (
|
||||||
|
<a href={p.docsUrl} target="_blank" rel="noopener noreferrer" className="detail-link-btn">
|
||||||
|
<BookOpen size={15} />
|
||||||
|
{t('detail.docs')}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ScreenshotCarousel({ count, placeholder }: { count: number; placeholder: string }) {
|
|
||||||
const [active, setActive] = useState(0);
|
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const scrollTo = useCallback((i: number) => {
|
|
||||||
setActive(i);
|
|
||||||
scrollRef.current?.children[i]?.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const el = scrollRef.current;
|
|
||||||
if (!el) return;
|
|
||||||
const observer = new IntersectionObserver(
|
|
||||||
(entries) => { entries.forEach(e => { if (e.isIntersecting) { const idx = Array.from(el.children).indexOf(e.target); if (idx >= 0) setActive(idx); } }); },
|
|
||||||
{ root: el, threshold: 0.6 }
|
|
||||||
);
|
|
||||||
Array.from(el.children).forEach(c => observer.observe(c));
|
|
||||||
return () => observer.disconnect();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="screenshot-carousel">
|
|
||||||
<div className="screenshot-scroll" ref={scrollRef}>
|
|
||||||
{Array.from({ length: count }, (_, i) => (
|
|
||||||
<div key={i} className="screenshot-slide">
|
|
||||||
<div className="screenshot-placeholder">{placeholder} {i + 1}</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="screenshot-dots">
|
|
||||||
{Array.from({ length: count }, (_, i) => (
|
|
||||||
<button key={i} className={`screenshot-dot ${i === active ? 'active' : ''}`} onClick={() => scrollTo(i)} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -11,17 +11,24 @@ export default function ProjectsPage() {
|
|||||||
const catParam = searchParams.get('cat');
|
const catParam = searchParams.get('cat');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
search, setSearch,
|
search,
|
||||||
tech, setTech,
|
setSearch,
|
||||||
platform, setPlatform,
|
tech,
|
||||||
status, setStatus,
|
setTech,
|
||||||
sort, setSort,
|
platform,
|
||||||
allTech, allPlatforms, allStatuses,
|
setPlatform,
|
||||||
|
status,
|
||||||
|
setStatus,
|
||||||
|
sort,
|
||||||
|
setSort,
|
||||||
|
allTech,
|
||||||
|
allPlatforms,
|
||||||
|
allStatuses,
|
||||||
filteredProjects,
|
filteredProjects,
|
||||||
} = useProjectFilters();
|
} = useProjectFilters();
|
||||||
|
|
||||||
const displayedProjects = catParam
|
const displayedProjects = catParam
|
||||||
? filteredProjects.filter(p => p.type.includes(catParam))
|
? filteredProjects.filter((p) => p.type.includes(catParam))
|
||||||
: filteredProjects;
|
: filteredProjects;
|
||||||
|
|
||||||
const techOptions = [
|
const techOptions = [
|
||||||
@@ -54,7 +61,7 @@ export default function ProjectsPage() {
|
|||||||
type="text"
|
type="text"
|
||||||
className="search-input"
|
className="search-input"
|
||||||
value={search}
|
value={search}
|
||||||
onChange={e => setSearch(e.target.value)}
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
placeholder={t('projects.search')}
|
placeholder={t('projects.search')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -90,7 +97,7 @@ export default function ProjectsPage() {
|
|||||||
{displayedProjects.length === 0 ? (
|
{displayedProjects.length === 0 ? (
|
||||||
<div className="empty-state">{t('projects.noResults')}</div>
|
<div className="empty-state">{t('projects.noResults')}</div>
|
||||||
) : (
|
) : (
|
||||||
displayedProjects.map(p => <ProjectCard key={p.id} project={p} />)
|
displayedProjects.map((p) => <ProjectCard key={p.id} project={p} />)
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+29
-2575
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,221 @@
|
|||||||
|
/* ═══════════════════════════════════════════════════════
|
||||||
|
Scroll Reveal System
|
||||||
|
Progressive: hidden only when JS is confirmed ready
|
||||||
|
═══════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
html.reveal-active .section,
|
||||||
|
html.reveal-active .stat-item,
|
||||||
|
html.reveal-active .project-card,
|
||||||
|
html.reveal-active .category-card,
|
||||||
|
html.reveal-active .contact-card,
|
||||||
|
html.reveal-active .changelog-entry,
|
||||||
|
html.reveal-active .about-header,
|
||||||
|
html.reveal-active .focus-item {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(28px);
|
||||||
|
transition:
|
||||||
|
opacity 0.7s var(--ease-out-expo),
|
||||||
|
transform 0.7s var(--ease-out-expo),
|
||||||
|
border-color var(--transition),
|
||||||
|
box-shadow var(--transition),
|
||||||
|
background var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.reveal-active .section.revealed,
|
||||||
|
html.reveal-active .stat-item.revealed,
|
||||||
|
html.reveal-active .project-card.revealed,
|
||||||
|
html.reveal-active .category-card.revealed,
|
||||||
|
html.reveal-active .contact-card.revealed,
|
||||||
|
html.reveal-active .changelog-entry.revealed,
|
||||||
|
html.reveal-active .about-header.revealed,
|
||||||
|
html.reveal-active .focus-item.revealed {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stagger delays for grid items */
|
||||||
|
html.reveal-active .project-card:nth-child(2),
|
||||||
|
html.reveal-active .stat-item:nth-child(2),
|
||||||
|
html.reveal-active .category-card:nth-child(2),
|
||||||
|
html.reveal-active .focus-item:nth-child(2) {
|
||||||
|
transition-delay: 0.06s;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.reveal-active .project-card:nth-child(3),
|
||||||
|
html.reveal-active .stat-item:nth-child(3),
|
||||||
|
html.reveal-active .category-card:nth-child(3),
|
||||||
|
html.reveal-active .focus-item:nth-child(3) {
|
||||||
|
transition-delay: 0.12s;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.reveal-active .project-card:nth-child(4),
|
||||||
|
html.reveal-active .stat-item:nth-child(4),
|
||||||
|
html.reveal-active .category-card:nth-child(4),
|
||||||
|
html.reveal-active .focus-item:nth-child(4) {
|
||||||
|
transition-delay: 0.18s;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.reveal-active .project-card:nth-child(5),
|
||||||
|
html.reveal-active .stat-item:nth-child(5),
|
||||||
|
html.reveal-active .category-card:nth-child(5),
|
||||||
|
html.reveal-active .focus-item:nth-child(5) {
|
||||||
|
transition-delay: 0.24s;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.reveal-active .project-card:nth-child(6),
|
||||||
|
html.reveal-active .category-card:nth-child(6) {
|
||||||
|
transition-delay: 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.reveal-active .project-card:nth-child(7),
|
||||||
|
html.reveal-active .category-card:nth-child(7) {
|
||||||
|
transition-delay: 0.36s;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.reveal-active .project-card:nth-child(8),
|
||||||
|
html.reveal-active .category-card:nth-child(8) {
|
||||||
|
transition-delay: 0.42s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Changelog stagger */
|
||||||
|
html.reveal-active .changelog-entry:nth-child(2) {
|
||||||
|
transition-delay: 0.08s;
|
||||||
|
}
|
||||||
|
html.reveal-active .changelog-entry:nth-child(3) {
|
||||||
|
transition-delay: 0.16s;
|
||||||
|
}
|
||||||
|
html.reveal-active .changelog-entry:nth-child(4) {
|
||||||
|
transition-delay: 0.24s;
|
||||||
|
}
|
||||||
|
html.reveal-active .changelog-entry:nth-child(5) {
|
||||||
|
transition-delay: 0.32s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════════════════
|
||||||
|
Keyframe Animations
|
||||||
|
═══════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
@keyframes fadeInUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(24px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(8px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-in {
|
||||||
|
animation: fadeIn 0.4s var(--ease-out-expo) forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes selectMenuIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-4px) scale(0.98);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes meshFloat1 {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: translate(0, 0) scale(1);
|
||||||
|
}
|
||||||
|
33% {
|
||||||
|
transform: translate(40px, -30px) scale(1.08);
|
||||||
|
}
|
||||||
|
66% {
|
||||||
|
transform: translate(-20px, 20px) scale(0.95);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes meshFloat2 {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: translate(0, 0) scale(1);
|
||||||
|
}
|
||||||
|
33% {
|
||||||
|
transform: translate(-30px, 25px) scale(1.05);
|
||||||
|
}
|
||||||
|
66% {
|
||||||
|
transform: translate(25px, -15px) scale(0.92);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes glowDot {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0 6px currentColor;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
box-shadow:
|
||||||
|
0 0 12px currentColor,
|
||||||
|
0 0 20px currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% {
|
||||||
|
background-position: -200% center;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 200% center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes gradientShift {
|
||||||
|
0% {
|
||||||
|
background-position: 0% 50%;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background-position: 100% 50%;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 0% 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes avatarPulse {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0 30px oklch(74% 0.2 45 / 10%);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
box-shadow:
|
||||||
|
0 0 40px oklch(74% 0.2 45 / 20%),
|
||||||
|
0 0 60px oklch(70% 0.24 20 / 10%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Loading Spinner ──────────────────────────────────── */
|
||||||
|
.loading-spinner {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border: 3px solid var(--border);
|
||||||
|
border-top-color: var(--accent);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
/* About page */
|
||||||
|
.about-header {
|
||||||
|
display: flex;
|
||||||
|
gap: 28px;
|
||||||
|
align-items: center;
|
||||||
|
margin: 28px 0 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-avatar {
|
||||||
|
width: 88px;
|
||||||
|
height: 88px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: oklch(74% 0.2 45 / 8%);
|
||||||
|
box-shadow: 0 0 0 2px oklch(74% 0.2 45 / 25%);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: var(--accent);
|
||||||
|
flex-shrink: 0;
|
||||||
|
box-shadow: 0 0 30px oklch(74% 0.2 45 / 10%);
|
||||||
|
position: relative;
|
||||||
|
animation: avatarPulse 4s ease-in-out infinite;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-avatar-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-avatar::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: -4px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--gradient-aurora);
|
||||||
|
z-index: -1;
|
||||||
|
opacity: 0.3;
|
||||||
|
filter: blur(8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-name {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-bio {
|
||||||
|
font-size: 15px;
|
||||||
|
color: var(--muted);
|
||||||
|
margin-top: 6px;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.focus-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.focus-item {
|
||||||
|
padding: 12px 16px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-size: 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
background: oklch(15% 0.025 270 / 40%);
|
||||||
|
transition: all var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.light .focus-item {
|
||||||
|
background: oklch(98% 0.005 270);
|
||||||
|
}
|
||||||
|
|
||||||
|
.focus-item:hover {
|
||||||
|
border-color: oklch(74% 0.2 45 / 20%);
|
||||||
|
background: oklch(74% 0.2 45 / 5%);
|
||||||
|
transform: translateX(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.focus-item::before {
|
||||||
|
content: '\2192';
|
||||||
|
color: var(--accent);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Contact cards */
|
||||||
|
.contact-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-card {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
padding: 16px;
|
||||||
|
background: oklch(15% 0.025 270 / 50%);
|
||||||
|
backdrop-filter: blur(16px);
|
||||||
|
-webkit-backdrop-filter: blur(16px);
|
||||||
|
transition: all var(--transition);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.light .contact-card {
|
||||||
|
background: oklch(100% 0 0 / 70%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-card::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: var(--gradient-glow);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-card:hover {
|
||||||
|
border-color: oklch(74% 0.2 45 / 25%);
|
||||||
|
box-shadow: 0 0 16px oklch(74% 0.2 45 / 8%);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-card:hover::before {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-card h3 {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-card p {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--muted);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-card-link {
|
||||||
|
display: inline-flex;
|
||||||
|
margin-top: 12px;
|
||||||
|
color: var(--accent);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tech stack tags */
|
||||||
|
.techstack-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-tag {
|
||||||
|
padding: 6px 14px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-size: 13px;
|
||||||
|
background: oklch(74% 0.2 45 / 6%);
|
||||||
|
border: 1px solid oklch(74% 0.2 45 / 12%);
|
||||||
|
color: var(--accent);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-tag:hover {
|
||||||
|
border-color: oklch(74% 0.2 45 / 30%);
|
||||||
|
background: oklch(74% 0.2 45 / 14%);
|
||||||
|
box-shadow: 0 0 16px oklch(74% 0.2 45 / 12%);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
/* ── Buttons ─────────────────────────────────────────── */
|
||||||
|
.btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--fg);
|
||||||
|
transition: all var(--transition);
|
||||||
|
white-space: nowrap;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: var(--gradient-glow);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
border-color: oklch(74% 0.2 45 / 25%);
|
||||||
|
box-shadow: var(--shadow-glow);
|
||||||
|
text-decoration: none;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover::before {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:active {
|
||||||
|
transform: translateY(0) scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: var(--gradient-primary);
|
||||||
|
color: var(--accent-fg);
|
||||||
|
border-color: transparent;
|
||||||
|
font-weight: 600;
|
||||||
|
box-shadow:
|
||||||
|
0 0 24px oklch(74% 0.2 45 / 20%),
|
||||||
|
0 2px 8px oklch(0% 0 0 / 20%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary::before {
|
||||||
|
background: linear-gradient(135deg, oklch(100% 0 0 / 15%) 0%, transparent 60%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
box-shadow: var(--shadow-glow);
|
||||||
|
border-color: transparent;
|
||||||
|
filter: brightness(1.1);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:active {
|
||||||
|
transform: translateY(0) scale(0.98);
|
||||||
|
filter: brightness(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm {
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-ghost {
|
||||||
|
border-color: transparent;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-ghost:hover {
|
||||||
|
background: oklch(74% 0.2 45 / 6%);
|
||||||
|
border-color: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-ghost::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
@@ -0,0 +1,338 @@
|
|||||||
|
/* ── Section ─────────────────────────────────────────── */
|
||||||
|
.section {
|
||||||
|
padding: 40px 0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-compact {
|
||||||
|
padding: 32px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section + .section::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 120px;
|
||||||
|
height: 1px;
|
||||||
|
background: linear-gradient(90deg, transparent, oklch(74% 0.2 45 / 20%), transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
position: relative;
|
||||||
|
padding-left: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 1px;
|
||||||
|
bottom: 1px;
|
||||||
|
width: 4px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: var(--gradient-primary);
|
||||||
|
box-shadow: 0 0 12px oklch(74% 0.2 45 / 40%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-action {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--accent);
|
||||||
|
text-decoration: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-action:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Card grid ───────────────────────────────────────── */
|
||||||
|
.card-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-grid-sm {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-grid > .empty-state {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Project card ────────────────────────────────────── */
|
||||||
|
.project-card-link,
|
||||||
|
.project-card-link:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-card {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-xl);
|
||||||
|
padding: 24px;
|
||||||
|
background: oklch(15% 0.025 270 / 50%);
|
||||||
|
backdrop-filter: blur(16px);
|
||||||
|
-webkit-backdrop-filter: blur(16px);
|
||||||
|
transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 14px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-card::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 2px;
|
||||||
|
background: var(--gradient-aurora);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-card:hover {
|
||||||
|
border-color: oklch(74% 0.2 45 / 25%);
|
||||||
|
box-shadow:
|
||||||
|
var(--shadow-glow),
|
||||||
|
0 8px 32px oklch(0% 0 0 / 20%);
|
||||||
|
transform: translateY(-6px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-card:hover::before {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-card:hover::after {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-card:active {
|
||||||
|
transform: translateY(-2px) scale(0.99);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.light .project-card {
|
||||||
|
background: oklch(100% 0 0 / 70%);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.light .project-card:hover {
|
||||||
|
box-shadow:
|
||||||
|
var(--shadow-glow),
|
||||||
|
0 8px 32px oklch(13% 0.02 270 / 6%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-icon {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 22px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border: 1px solid oklch(74% 0.2 45 / 12%);
|
||||||
|
background: oklch(74% 0.2 45 / 8%);
|
||||||
|
color: var(--accent);
|
||||||
|
transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-card:hover .project-icon {
|
||||||
|
border-color: oklch(74% 0.2 45 / 30%);
|
||||||
|
background: oklch(74% 0.2 45 / 15%);
|
||||||
|
box-shadow: 0 0 24px oklch(74% 0.2 45 / 20%);
|
||||||
|
transform: scale(1.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-card-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-name {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-name a {
|
||||||
|
color: var(--fg);
|
||||||
|
transition: color var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-name a:hover {
|
||||||
|
color: var(--accent);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-slogan {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--muted);
|
||||||
|
margin-top: 3px;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-card-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-card-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Badges ──────────────────────────────────────────── */
|
||||||
|
.badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 3px 10px;
|
||||||
|
border-radius: 99px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.5;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: oklch(15% 0.025 270 / 60%);
|
||||||
|
color: var(--muted);
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: all var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.light .badge {
|
||||||
|
background: oklch(97% 0.005 270);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-accent {
|
||||||
|
background: oklch(74% 0.2 45 / 8%);
|
||||||
|
border-color: oklch(74% 0.2 45 / 20%);
|
||||||
|
color: var(--blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-status {
|
||||||
|
border: none;
|
||||||
|
color: var(--fg);
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 3px 10px 3px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-status::before {
|
||||||
|
content: '';
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--status-color, currentColor);
|
||||||
|
margin-inline-end: 3px;
|
||||||
|
box-shadow: 0 0 8px var(--status-color, currentColor);
|
||||||
|
animation: glowDot 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Category cards ──────────────────────────────────── */
|
||||||
|
.category-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-card {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: 24px 16px;
|
||||||
|
background: oklch(15% 0.025 270 / 40%);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
-webkit-backdrop-filter: blur(8px);
|
||||||
|
text-align: center;
|
||||||
|
transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-card::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: radial-gradient(ellipse at 50% 100%, oklch(74% 0.2 45 / 6%) 0%, transparent 70%);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.35s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-card:hover {
|
||||||
|
border-color: oklch(74% 0.2 45 / 30%);
|
||||||
|
box-shadow: var(--shadow-glow);
|
||||||
|
transform: translateY(-6px);
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-card:hover::before {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-card:active {
|
||||||
|
transform: translateY(-2px) scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.light .category-card {
|
||||||
|
background: oklch(100% 0 0 / 60%);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.light .category-card:hover {
|
||||||
|
box-shadow:
|
||||||
|
var(--shadow-glow),
|
||||||
|
0 8px 24px oklch(13% 0.02 270 / 5%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-icon {
|
||||||
|
font-size: 28px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: var(--accent);
|
||||||
|
transition:
|
||||||
|
transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1),
|
||||||
|
color var(--transition);
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-card:hover .category-icon {
|
||||||
|
transform: scale(1.2);
|
||||||
|
filter: drop-shadow(0 0 8px oklch(74% 0.2 45 / 30%));
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-label {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
.changelog-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Changelog */
|
||||||
|
.changelog-entry {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background: oklch(15% 0.025 270 / 35%);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.light .changelog-entry {
|
||||||
|
background: oklch(100% 0 0 / 55%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.changelog-summary {
|
||||||
|
min-height: 44px;
|
||||||
|
padding: 9px 12px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(82px, auto) 1fr auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.changelog-summary::-webkit-details-marker {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.changelog-summary::before {
|
||||||
|
content: '';
|
||||||
|
width: 7px;
|
||||||
|
height: 7px;
|
||||||
|
border-right: 2px solid var(--accent);
|
||||||
|
border-bottom: 2px solid var(--accent);
|
||||||
|
transform: rotate(-45deg);
|
||||||
|
transition: transform var(--transition);
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 1;
|
||||||
|
justify-self: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.changelog-entry[open] .changelog-summary::before {
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.changelog-version {
|
||||||
|
padding-left: 16px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.changelog-date {
|
||||||
|
min-width: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--muted);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.changelog-count {
|
||||||
|
min-width: 24px;
|
||||||
|
height: 22px;
|
||||||
|
padding: 0 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: oklch(74% 0.2 45 / 9%);
|
||||||
|
color: var(--accent);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
}
|
||||||
|
|
||||||
|
.changelog-changes {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 14px 12px 34px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--muted);
|
||||||
|
line-height: 1.65;
|
||||||
|
}
|
||||||
|
|
||||||
|
.changelog-changes li {
|
||||||
|
padding: 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.changelog-changes li::marker {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
@@ -0,0 +1,414 @@
|
|||||||
|
/* ── Breadcrumb ──────────────────────────────────────── */
|
||||||
|
.breadcrumb {
|
||||||
|
padding: 8px 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb a {
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-current {
|
||||||
|
color: var(--fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Detail page ─────────────────────────────────────── */
|
||||||
|
.detail-header {
|
||||||
|
padding-top: 48px;
|
||||||
|
padding-bottom: 36px;
|
||||||
|
padding-inline: 24px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-header::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background:
|
||||||
|
radial-gradient(ellipse 60% 80% at 10% 50%, oklch(74% 0.2 45 / 8%) 0%, transparent 70%),
|
||||||
|
radial-gradient(ellipse 40% 60% at 90% 30%, oklch(70% 0.24 20 / 6%) 0%, transparent 70%);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.light .detail-header::before {
|
||||||
|
background:
|
||||||
|
radial-gradient(ellipse 60% 80% at 10% 50%, oklch(58% 0.22 45 / 5%) 0%, transparent 70%),
|
||||||
|
radial-gradient(ellipse 40% 60% at 90% 30%, oklch(55% 0.24 20 / 4%) 0%, transparent 70%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-header-top {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-icon {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 28px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border: 1px solid oklch(74% 0.2 45 / 15%);
|
||||||
|
background: oklch(74% 0.2 45 / 8%);
|
||||||
|
color: var(--accent);
|
||||||
|
box-shadow: 0 0 24px oklch(74% 0.2 45 / 10%);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-icon::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border-radius: inherit;
|
||||||
|
background: linear-gradient(135deg, oklch(100% 0 0 / 10%) 0%, transparent 60%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-title {
|
||||||
|
font-size: 36px;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: -0.03em;
|
||||||
|
background: var(--gradient-primary);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-slogan {
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--muted);
|
||||||
|
margin-top: 6px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-badges {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 24px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-body {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 300px;
|
||||||
|
gap: 40px;
|
||||||
|
padding-top: 40px;
|
||||||
|
padding-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-sidebar {
|
||||||
|
position: sticky;
|
||||||
|
top: calc(var(--nav-height) + 32px);
|
||||||
|
align-self: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-section {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
scroll-margin-top: calc(var(--nav-height) + 24px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-section-title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
position: relative;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-section-title::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: -1px;
|
||||||
|
left: 0;
|
||||||
|
width: 60px;
|
||||||
|
height: 2px;
|
||||||
|
background: var(--gradient-primary);
|
||||||
|
box-shadow: 0 0 8px oklch(74% 0.2 45 / 25%);
|
||||||
|
border-radius: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-prose {
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.8;
|
||||||
|
color: var(--fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-prose + .detail-prose {
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar compact metadata */
|
||||||
|
.detail-meta-panel {
|
||||||
|
width: 100%;
|
||||||
|
font-size: 13px;
|
||||||
|
background: oklch(15% 0.025 270 / 50%);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
-webkit-backdrop-filter: blur(12px);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
padding: 10px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.light .detail-meta-panel {
|
||||||
|
background: oklch(100% 0 0 / 70%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-meta-item {
|
||||||
|
min-width: 0;
|
||||||
|
padding: 9px 10px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: oklch(74% 0.2 45 / 4%);
|
||||||
|
border: 1px solid oklch(74% 0.2 45 / 8%);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.light .detail-meta-item {
|
||||||
|
background: oklch(98% 0.005 270 / 70%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-meta-label {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-meta-value {
|
||||||
|
min-width: 0;
|
||||||
|
color: var(--fg);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.35;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-meta-value.mono {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-link-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-link-btn {
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 38px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: oklch(15% 0.025 270 / 45%);
|
||||||
|
color: var(--fg);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
transition:
|
||||||
|
color var(--transition),
|
||||||
|
border-color var(--transition),
|
||||||
|
background var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-link-btn:hover {
|
||||||
|
color: var(--accent);
|
||||||
|
border-color: oklch(74% 0.2 45 / 25%);
|
||||||
|
background: oklch(74% 0.2 45 / 7%);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.light .detail-link-btn {
|
||||||
|
background: oklch(100% 0 0 / 65%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Feature tags — compact highlight */
|
||||||
|
.feature-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-tag {
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 99px;
|
||||||
|
font-size: 13px;
|
||||||
|
background: oklch(74% 0.2 45 / 10%);
|
||||||
|
color: var(--accent);
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: background var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-tag::before {
|
||||||
|
content: '\2713';
|
||||||
|
margin-inline-end: 6px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-tag:hover {
|
||||||
|
background: oklch(74% 0.2 45 / 18%);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.light .feature-tag {
|
||||||
|
background: oklch(58% 0.22 45 / 8%);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.light .feature-tag:hover {
|
||||||
|
background: oklch(58% 0.22 45 / 14%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Screenshot carousel */
|
||||||
|
.screenshot-carousel {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.screenshot-scroll {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
overflow-x: auto;
|
||||||
|
scroll-snap-type: x mandatory;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
scrollbar-width: none;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.screenshot-scroll::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.screenshot-slide {
|
||||||
|
flex: 0 0 85%;
|
||||||
|
max-width: 400px;
|
||||||
|
scroll-snap-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.screenshot-slide:first-child {
|
||||||
|
margin-inline-start: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.screenshot-placeholder {
|
||||||
|
aspect-ratio: 16/10;
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 14px;
|
||||||
|
background: oklch(15% 0.025 270 / 60%);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
-webkit-backdrop-filter: blur(8px);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.screenshot-placeholder::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: var(--gradient-glow);
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.light .screenshot-placeholder {
|
||||||
|
background: oklch(96% 0.008 270 / 70%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.screenshot-dots {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.screenshot-dot {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
background: var(--border);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.screenshot-dot.active {
|
||||||
|
background: var(--accent);
|
||||||
|
width: 18px;
|
||||||
|
border-radius: 3px;
|
||||||
|
box-shadow: 0 0 8px oklch(74% 0.2 45 / 30%);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.screenshot-slide {
|
||||||
|
flex: 0 0 calc(33.333% - 8px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Install guide ───────────────────────────────────── */
|
||||||
|
.install-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.install-item {
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
font-size: 13px;
|
||||||
|
background: oklch(15% 0.025 270 / 40%);
|
||||||
|
transition: all var(--transition);
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.light .install-item {
|
||||||
|
background: oklch(98% 0.005 270);
|
||||||
|
}
|
||||||
|
|
||||||
|
.install-item:hover {
|
||||||
|
background: oklch(74% 0.2 45 / 6%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.install-item strong {
|
||||||
|
display: inline;
|
||||||
|
margin-inline-end: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--accent);
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Project selector (for roadmap/changelog) ────────── */
|
||||||
|
.project-selector {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
/* Download table */
|
||||||
|
.download-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-table th {
|
||||||
|
text-align: start;
|
||||||
|
padding: 10px 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--muted);
|
||||||
|
border-bottom: 2px solid oklch(74% 0.2 45 / 15%);
|
||||||
|
font-size: 12px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-table td {
|
||||||
|
padding: 12px 14px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
transition: background var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-table tr:hover td {
|
||||||
|
background: oklch(74% 0.2 45 / 4%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sha256 {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--muted);
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Trust note */
|
||||||
|
.trust-note {
|
||||||
|
padding: 18px 22px;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
border: 1px solid oklch(74% 0.2 45 / 12%);
|
||||||
|
background: oklch(74% 0.2 45 / 4%);
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--muted);
|
||||||
|
margin-top: 20px;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trust-note::before {
|
||||||
|
content: '\2139';
|
||||||
|
margin-inline-end: 10px;
|
||||||
|
color: var(--accent);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Docs hub */
|
||||||
|
.docs-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-link {
|
||||||
|
padding: 14px 18px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
font-size: 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
transition: all var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-link:hover {
|
||||||
|
border-color: oklch(74% 0.2 45 / 25%);
|
||||||
|
box-shadow: 0 0 16px oklch(74% 0.2 45 / 6%);
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--accent);
|
||||||
|
transform: translateX(4px);
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
/* Footer ─────────────────────────────────────────────── */
|
||||||
|
.footer {
|
||||||
|
position: relative;
|
||||||
|
padding: 48px 0;
|
||||||
|
margin-top: auto;
|
||||||
|
background: oklch(12% 0.02 270);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.light .footer {
|
||||||
|
background: oklch(96% 0.008 270);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Gradient top border */
|
||||||
|
.footer::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 2px;
|
||||||
|
background: var(--gradient-aurora);
|
||||||
|
opacity: 0.6;
|
||||||
|
box-shadow: 0 0 16px oklch(74% 0.2 45 / 15%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(260px, 2fr) minmax(180px, 1fr) minmax(160px, 1fr);
|
||||||
|
gap: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-brand {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-slogan {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--muted);
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-col-title {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
color: var(--muted);
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-links {
|
||||||
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-links a {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--muted);
|
||||||
|
transition:
|
||||||
|
color var(--transition),
|
||||||
|
padding-inline-start var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-links a:hover {
|
||||||
|
color: var(--accent);
|
||||||
|
text-decoration: none;
|
||||||
|
padding-inline-start: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-bottom {
|
||||||
|
margin-top: 36px;
|
||||||
|
padding-top: 20px;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--muted);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-policy-links,
|
||||||
|
.footer-legal {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-legal a {
|
||||||
|
color: var(--muted);
|
||||||
|
transition: color var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-legal a:hover {
|
||||||
|
color: var(--accent);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-policy-link {
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
/* ── Hero ─────────────────────────────────────────────── */
|
||||||
|
.hero {
|
||||||
|
padding-top: 120px;
|
||||||
|
padding-bottom: 80px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animated gradient mesh — blob 1 */
|
||||||
|
.hero::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -80px;
|
||||||
|
left: -10%;
|
||||||
|
width: 600px;
|
||||||
|
height: 600px;
|
||||||
|
background: radial-gradient(
|
||||||
|
circle,
|
||||||
|
oklch(74% 0.2 45 / 28%) 0%,
|
||||||
|
oklch(70% 0.24 20 / 16%) 40%,
|
||||||
|
transparent 70%
|
||||||
|
);
|
||||||
|
pointer-events: none;
|
||||||
|
filter: blur(60px);
|
||||||
|
animation: meshFloat1 12s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animated gradient mesh — blob 2 */
|
||||||
|
.hero::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -30px;
|
||||||
|
right: -5%;
|
||||||
|
width: 500px;
|
||||||
|
height: 500px;
|
||||||
|
background: radial-gradient(
|
||||||
|
circle,
|
||||||
|
oklch(68% 0.22 350 / 22%) 0%,
|
||||||
|
oklch(80% 0.17 85 / 12%) 40%,
|
||||||
|
transparent 70%
|
||||||
|
);
|
||||||
|
pointer-events: none;
|
||||||
|
filter: blur(50px);
|
||||||
|
animation: meshFloat2 10s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.light .hero::before {
|
||||||
|
background: radial-gradient(
|
||||||
|
circle,
|
||||||
|
oklch(58% 0.22 45 / 12%) 0%,
|
||||||
|
oklch(55% 0.24 20 / 6%) 40%,
|
||||||
|
transparent 70%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.light .hero::after {
|
||||||
|
background: radial-gradient(
|
||||||
|
circle,
|
||||||
|
oklch(52% 0.22 350 / 10%) 0%,
|
||||||
|
oklch(65% 0.18 85 / 5%) 40%,
|
||||||
|
transparent 70%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title {
|
||||||
|
font-size: clamp(36px, 5.5vw, 64px);
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1.08;
|
||||||
|
letter-spacing: -0.04em;
|
||||||
|
max-width: 720px;
|
||||||
|
background: var(--gradient-primary);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
position: relative;
|
||||||
|
animation: fadeInUp 0.8s var(--ease-out-expo) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-subtitle {
|
||||||
|
font-size: 17px;
|
||||||
|
color: oklch(75% 0.02 270);
|
||||||
|
margin-top: 20px;
|
||||||
|
max-width: 600px;
|
||||||
|
line-height: 1.8;
|
||||||
|
animation: fadeInUp 0.8s var(--ease-out-expo) 0.1s both;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.light .hero-subtitle {
|
||||||
|
color: oklch(40% 0.028 270);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 14px;
|
||||||
|
margin-top: 40px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
animation: fadeInUp 0.8s var(--ease-out-expo) 0.2s both;
|
||||||
|
}
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
/* ── Navigation ──────────────────────────────────────── */
|
||||||
|
.nav {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
inset-inline: 0;
|
||||||
|
height: var(--nav-height);
|
||||||
|
background: oklch(11% 0.02 270 / 20%);
|
||||||
|
backdrop-filter: blur(32px) saturate(1.8);
|
||||||
|
-webkit-backdrop-filter: blur(32px) saturate(1.8);
|
||||||
|
border-bottom: 1px solid oklch(74% 0.2 45 / 6%);
|
||||||
|
z-index: 300;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
transition:
|
||||||
|
background 0.4s ease,
|
||||||
|
border-color 0.4s ease,
|
||||||
|
box-shadow 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav.scrolled,
|
||||||
|
.nav:has(~ .main-content) {
|
||||||
|
background: oklch(11% 0.02 270 / 35%);
|
||||||
|
box-shadow:
|
||||||
|
0 4px 30px oklch(0% 0 0 / 20%),
|
||||||
|
0 1px 0 oklch(74% 0.2 45 / 8%);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.light .nav {
|
||||||
|
background: oklch(98% 0.005 270 / 35%);
|
||||||
|
border-bottom: 1px solid oklch(58% 0.22 45 / 6%);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.light .nav.scrolled,
|
||||||
|
:root.light .nav:has(~ .main-content) {
|
||||||
|
background: oklch(98% 0.005 270 / 55%);
|
||||||
|
box-shadow:
|
||||||
|
0 4px 30px oklch(13% 0.02 270 / 6%),
|
||||||
|
0 1px 0 oklch(58% 0.22 45 / 8%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Gradient line under nav */
|
||||||
|
.nav::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: -2px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 2px;
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
transparent 0%,
|
||||||
|
oklch(74% 0.2 45 / 40%) 15%,
|
||||||
|
oklch(70% 0.24 20 / 50%) 35%,
|
||||||
|
oklch(68% 0.22 350 / 50%) 55%,
|
||||||
|
oklch(80% 0.17 85 / 40%) 75%,
|
||||||
|
transparent 100%
|
||||||
|
);
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-inner {
|
||||||
|
max-width: var(--max-width);
|
||||||
|
margin: 0 auto;
|
||||||
|
padding-inline: var(--gutter);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-brand {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 15px;
|
||||||
|
color: var(--fg);
|
||||||
|
margin-inline-end: 24px;
|
||||||
|
white-space: nowrap;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
transition: opacity var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-brand:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-brand-icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: var(--gradient-primary);
|
||||||
|
color: var(--accent-fg);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 800;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
box-shadow: 0 0 16px oklch(74% 0.2 45 / 20%);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-brand-icon::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: linear-gradient(135deg, oklch(100% 0 0 / 20%) 0%, transparent 60%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links {
|
||||||
|
display: flex;
|
||||||
|
gap: 2px;
|
||||||
|
flex: 1;
|
||||||
|
overflow-x: auto;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
padding: 7px 14px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--muted);
|
||||||
|
white-space: nowrap;
|
||||||
|
position: relative;
|
||||||
|
transition:
|
||||||
|
color var(--transition),
|
||||||
|
background var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link:hover {
|
||||||
|
color: var(--fg);
|
||||||
|
background: oklch(74% 0.2 45 / 6%);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link.active {
|
||||||
|
color: var(--accent);
|
||||||
|
background: oklch(74% 0.2 45 / 10%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link.active::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 16px;
|
||||||
|
height: 2px;
|
||||||
|
border-radius: 1px;
|
||||||
|
background: var(--gradient-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
align-items: center;
|
||||||
|
margin-inline-start: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-btn {
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: transparent;
|
||||||
|
color: var(--muted);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition:
|
||||||
|
color var(--transition),
|
||||||
|
background var(--transition),
|
||||||
|
transform var(--transition-fast);
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-btn:hover {
|
||||||
|
color: var(--accent);
|
||||||
|
background: oklch(74% 0.2 45 / 8%);
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-btn:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Mobile nav overlay ──────────────────────────────── */
|
||||||
|
.nav-mobile-toggle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-mobile-menu {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: var(--nav-height);
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
background: oklch(11% 0.02 270 / 95%);
|
||||||
|
backdrop-filter: blur(24px);
|
||||||
|
-webkit-backdrop-filter: blur(24px);
|
||||||
|
z-index: 290;
|
||||||
|
padding: 24px var(--gutter);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.light .nav-mobile-menu {
|
||||||
|
background: oklch(98% 0.005 270 / 95%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-mobile-menu.open {
|
||||||
|
animation: fadeIn 0.25s var(--ease-out-expo);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-mobile-menu .nav-link {
|
||||||
|
padding: 14px 18px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.nav-links {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.nav-mobile-toggle {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.nav-mobile-menu.open {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
/* Roadmap compact tabs */
|
||||||
|
.roadmap-compact {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
overflow: hidden;
|
||||||
|
background: oklch(15% 0.025 270 / 40%);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
-webkit-backdrop-filter: blur(8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.light .roadmap-compact {
|
||||||
|
background: oklch(100% 0 0 / 60%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.roadmap-tabs {
|
||||||
|
display: flex;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.roadmap-tab {
|
||||||
|
flex: 1;
|
||||||
|
padding: 10px 14px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--muted);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all var(--transition);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.roadmap-tab::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: -1px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 2px;
|
||||||
|
background: transparent;
|
||||||
|
transition: background var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.roadmap-tab.active {
|
||||||
|
color: var(--fg);
|
||||||
|
background: oklch(15% 0.025 270 / 30%);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.light .roadmap-tab.active {
|
||||||
|
background: oklch(97% 0.005 270);
|
||||||
|
}
|
||||||
|
|
||||||
|
.roadmap-tab.done.active {
|
||||||
|
color: var(--emerald);
|
||||||
|
}
|
||||||
|
.roadmap-tab.done.active::after {
|
||||||
|
background: var(--emerald);
|
||||||
|
}
|
||||||
|
.roadmap-tab.doing.active {
|
||||||
|
color: var(--amber);
|
||||||
|
}
|
||||||
|
.roadmap-tab.doing.active::after {
|
||||||
|
background: var(--amber);
|
||||||
|
}
|
||||||
|
.roadmap-tab.planned.active {
|
||||||
|
color: var(--cyan);
|
||||||
|
}
|
||||||
|
.roadmap-tab.planned.active::after {
|
||||||
|
background: var(--cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
.roadmap-tab-content {
|
||||||
|
padding: 12px 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.roadmap-item-inline {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 3px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.roadmap-item-inline .dot {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.roadmap-item-inline .dot.done {
|
||||||
|
background: var(--emerald);
|
||||||
|
box-shadow: 0 0 6px oklch(74% 0.2 165 / 40%);
|
||||||
|
}
|
||||||
|
.roadmap-item-inline .dot.doing {
|
||||||
|
background: var(--amber);
|
||||||
|
box-shadow: 0 0 6px oklch(83% 0.19 85 / 40%);
|
||||||
|
animation: glowDot 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
.roadmap-item-inline .dot.planned {
|
||||||
|
background: var(--cyan);
|
||||||
|
box-shadow: 0 0 6px oklch(77% 0.16 200 / 40%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.roadmap-empty {
|
||||||
|
color: var(--muted);
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,282 @@
|
|||||||
|
/* ── Filter bar ──────────────────────────────────────── */
|
||||||
|
.filter-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
padding: 14px;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background: oklch(15% 0.025 270 / 46%);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
-webkit-backdrop-filter: blur(12px);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
box-shadow: 0 1px 0 oklch(100% 0 0 / 4%) inset;
|
||||||
|
position: relative;
|
||||||
|
z-index: 30;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-bar:has(.select-control.open) {
|
||||||
|
z-index: 260;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.light .filter-bar {
|
||||||
|
background: oklch(100% 0 0 / 72%);
|
||||||
|
box-shadow: 0 1px 0 oklch(100% 0 0 / 70%) inset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 240px;
|
||||||
|
min-height: 40px;
|
||||||
|
padding: 9px 13px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: oklch(11% 0.02 270 / 45%);
|
||||||
|
color: var(--fg);
|
||||||
|
font-size: 14px;
|
||||||
|
outline: none;
|
||||||
|
transition:
|
||||||
|
border-color var(--transition),
|
||||||
|
box-shadow var(--transition),
|
||||||
|
background var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.light .search-input {
|
||||||
|
background: oklch(98% 0.005 270 / 80%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input:focus {
|
||||||
|
border-color: oklch(74% 0.2 45 / 40%);
|
||||||
|
box-shadow: 0 0 0 3px oklch(74% 0.2 45 / 8%);
|
||||||
|
background: var(--surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input::placeholder {
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-group-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
margin-inline-end: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-btn {
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 13px;
|
||||||
|
transition: all var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-btn:hover {
|
||||||
|
border-color: oklch(74% 0.2 45 / 25%);
|
||||||
|
color: var(--fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-btn.active {
|
||||||
|
background: oklch(74% 0.2 45 / 10%);
|
||||||
|
border-color: oklch(74% 0.2 45 / 30%);
|
||||||
|
color: var(--accent);
|
||||||
|
box-shadow: 0 0 12px oklch(74% 0.2 45 / 8%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-control {
|
||||||
|
position: relative;
|
||||||
|
min-width: 132px;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-control.open {
|
||||||
|
z-index: 280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-trigger {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 40px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: oklch(11% 0.02 270 / 45%);
|
||||||
|
color: var(--fg);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
border-color var(--transition),
|
||||||
|
box-shadow var(--transition),
|
||||||
|
background var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.light .select-trigger {
|
||||||
|
background: oklch(98% 0.005 270 / 80%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-trigger:hover,
|
||||||
|
.select-control.open .select-trigger {
|
||||||
|
border-color: oklch(74% 0.2 45 / 25%);
|
||||||
|
background: oklch(74% 0.2 45 / 6%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-trigger:focus-visible,
|
||||||
|
.select-control.open .select-trigger {
|
||||||
|
border-color: oklch(74% 0.2 45 / 40%);
|
||||||
|
box-shadow: 0 0 0 3px oklch(74% 0.2 45 / 8%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-chevron {
|
||||||
|
width: 7px;
|
||||||
|
height: 7px;
|
||||||
|
border-right: 2px solid var(--muted);
|
||||||
|
border-bottom: 2px solid var(--muted);
|
||||||
|
transform: translateY(-2px) rotate(45deg);
|
||||||
|
transition:
|
||||||
|
border-color var(--transition),
|
||||||
|
transform var(--transition);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-trigger:hover .select-chevron,
|
||||||
|
.select-trigger:focus-visible .select-chevron,
|
||||||
|
.select-control.open .select-chevron {
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-control.open .select-chevron {
|
||||||
|
transform: translateY(2px) rotate(225deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-menu {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 6px);
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 300;
|
||||||
|
padding: 6px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: oklch(13% 0.018 270 / 98%);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
max-height: min(320px, calc(100vh - 220px));
|
||||||
|
box-shadow:
|
||||||
|
0 14px 36px oklch(0% 0 0 / 32%),
|
||||||
|
0 0 0 1px oklch(100% 0 0 / 3%) inset;
|
||||||
|
backdrop-filter: blur(18px);
|
||||||
|
-webkit-backdrop-filter: blur(18px);
|
||||||
|
animation: selectMenuIn 120ms var(--ease-out-expo);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.light .select-menu {
|
||||||
|
background: oklch(100% 0 0 / 98%);
|
||||||
|
box-shadow:
|
||||||
|
0 16px 36px oklch(13% 0.02 270 / 12%),
|
||||||
|
0 0 0 1px oklch(100% 0 0 / 80%) inset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-search {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 32px;
|
||||||
|
padding: 7px 10px;
|
||||||
|
border: 1px solid oklch(74% 0.2 45 / 18%);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: oklch(11% 0.02 270 / 55%);
|
||||||
|
color: var(--fg);
|
||||||
|
font-size: 12px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.light .select-search {
|
||||||
|
background: oklch(98% 0.005 270 / 90%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-search:focus {
|
||||||
|
border-color: oklch(74% 0.2 45 / 42%);
|
||||||
|
box-shadow: 0 0 0 2px oklch(74% 0.2 45 / 8%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-options {
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
padding-right: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-options::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-options::-webkit-scrollbar-thumb {
|
||||||
|
background: oklch(74% 0.2 45 / 28%);
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-option {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 34px;
|
||||||
|
padding: 7px 9px 7px 12px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--fg);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
text-align: left;
|
||||||
|
transition:
|
||||||
|
color var(--transition-fast),
|
||||||
|
background var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-option:hover {
|
||||||
|
background: oklch(74% 0.2 45 / 8%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-option.active {
|
||||||
|
background: oklch(74% 0.2 45 / 13%);
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.light .select-option:hover {
|
||||||
|
background: oklch(58% 0.22 45 / 8%);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.light .select-option.active {
|
||||||
|
background: oklch(58% 0.22 45 / 12%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-check {
|
||||||
|
color: var(--accent);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-empty {
|
||||||
|
padding: 10px 12px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
/* ── Stats bar ───────────────────────────────────────── */
|
||||||
|
.stats-bar {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
padding: 48px 0;
|
||||||
|
border-bottom: 1px solid oklch(24% 0.035 270);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
text-align: center;
|
||||||
|
padding: 24px 16px;
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
background: oklch(15% 0.025 270 / 60%);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
-webkit-backdrop-filter: blur(12px);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
transition:
|
||||||
|
border-color var(--transition),
|
||||||
|
box-shadow var(--transition),
|
||||||
|
transform var(--transition),
|
||||||
|
background var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: radial-gradient(ellipse at 50% 0%, oklch(74% 0.2 45 / 5%) 0%, transparent 70%);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.light .stat-item {
|
||||||
|
background: oklch(100% 0 0 / 70%);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.light .stat-item::before {
|
||||||
|
background: radial-gradient(ellipse at 50% 0%, oklch(58% 0.22 45 / 4%) 0%, transparent 70%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Gradient accent line at bottom */
|
||||||
|
.stat-item::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 10%;
|
||||||
|
right: 10%;
|
||||||
|
height: 2px;
|
||||||
|
border-radius: 1px;
|
||||||
|
opacity: 0.6;
|
||||||
|
transition:
|
||||||
|
opacity var(--transition),
|
||||||
|
left var(--transition),
|
||||||
|
right var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item:nth-child(1)::after {
|
||||||
|
background: var(--blue);
|
||||||
|
}
|
||||||
|
.stat-item:nth-child(2)::after {
|
||||||
|
background: var(--emerald);
|
||||||
|
}
|
||||||
|
.stat-item:nth-child(3)::after {
|
||||||
|
background: var(--violet);
|
||||||
|
}
|
||||||
|
.stat-item:nth-child(4)::after {
|
||||||
|
background: var(--amber);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item:hover {
|
||||||
|
border-color: oklch(74% 0.2 45 / 25%);
|
||||||
|
box-shadow: var(--shadow-glow);
|
||||||
|
transform: translateY(-4px);
|
||||||
|
background: oklch(15% 0.025 270 / 80%);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.light .stat-item:hover {
|
||||||
|
background: oklch(100% 0 0 / 90%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item:hover::after {
|
||||||
|
opacity: 1;
|
||||||
|
left: 5%;
|
||||||
|
right: 5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 36px;
|
||||||
|
font-weight: 800;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item:nth-child(1) .stat-value {
|
||||||
|
color: var(--blue);
|
||||||
|
text-shadow: 0 0 20px oklch(74% 0.2 45 / 15%);
|
||||||
|
}
|
||||||
|
.stat-item:nth-child(2) .stat-value {
|
||||||
|
color: var(--emerald);
|
||||||
|
text-shadow: 0 0 20px oklch(74% 0.2 165 / 15%);
|
||||||
|
}
|
||||||
|
.stat-item:nth-child(3) .stat-value {
|
||||||
|
color: var(--violet);
|
||||||
|
text-shadow: 0 0 20px oklch(70% 0.24 20 / 15%);
|
||||||
|
}
|
||||||
|
.stat-item:nth-child(4) .stat-value {
|
||||||
|
color: var(--amber);
|
||||||
|
text-shadow: 0 0 20px oklch(83% 0.19 85 / 15%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value .accent {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--muted);
|
||||||
|
margin-top: 6px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: 'JetBrains Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400 600;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('/fonts/JetBrainsMono-Latin.woff2') format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
|
||||||
|
U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122,
|
||||||
|
U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Noto Sans SC';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('/fonts/NotoSansSC-400.woff2') format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Noto Sans SC';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('/fonts/NotoSansSC-500.woff2') format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Noto Sans SC';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('/fonts/NotoSansSC-600.woff2') format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Noto Sans SC';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('/fonts/NotoSansSC-700.woff2') format('woff2');
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
/* ── Layout ──────────────────────────────────────────── */
|
||||||
|
.container {
|
||||||
|
max-width: var(--max-width);
|
||||||
|
margin: 0 auto;
|
||||||
|
padding-inline: var(--gutter);
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
padding-top: calc(var(--nav-height) + 32px);
|
||||||
|
padding-bottom: 80px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Empty state ─────────────────────────────────────── */
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 56px 0;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Page header ─────────────────────────────────────── */
|
||||||
|
.page-header {
|
||||||
|
padding: 24px 0;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: -0.03em;
|
||||||
|
background: var(--gradient-primary);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-subtitle {
|
||||||
|
font-size: 15px;
|
||||||
|
color: var(--muted);
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── CTA section ─────────────────────────────────────── */
|
||||||
|
.cta-section {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-subtitle {
|
||||||
|
margin: 8px auto 0;
|
||||||
|
max-width: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-actions {
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Loading fallback ────────────────────────────────── */
|
||||||
|
.loading-fallback {
|
||||||
|
padding: 80px 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
/* ── Reset ───────────────────────────────────────────── */
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
font-size: 16px;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: var(--font-body);
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--fg);
|
||||||
|
line-height: 1.6;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
position: relative;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ambient aurora gradients on body */
|
||||||
|
body::before {
|
||||||
|
content: '';
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background:
|
||||||
|
radial-gradient(ellipse 80% 60% at 8% 15%, oklch(74% 0.2 45 / 14%) 0%, transparent 60%),
|
||||||
|
radial-gradient(ellipse 60% 80% at 92% 65%, oklch(70% 0.24 20 / 10%) 0%, transparent 70%),
|
||||||
|
radial-gradient(ellipse 70% 70% at 50% 40%, oklch(68% 0.22 350 / 7%) 0%, transparent 55%);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.light body::before {
|
||||||
|
background:
|
||||||
|
radial-gradient(ellipse 80% 60% at 8% 15%, oklch(58% 0.22 45 / 8%) 0%, transparent 60%),
|
||||||
|
radial-gradient(ellipse 60% 80% at 92% 65%, oklch(55% 0.24 20 / 6%) 0%, transparent 70%),
|
||||||
|
radial-gradient(ellipse 70% 70% at 50% 40%, oklch(52% 0.22 350 / 4%) 0%, transparent 55%);
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--fg);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
input,
|
||||||
|
textarea,
|
||||||
|
select {
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
@@ -0,0 +1,355 @@
|
|||||||
|
/* ── Responsive ──────────────────────────────────────── */
|
||||||
|
@media (max-width: 1023px) {
|
||||||
|
.detail-body {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.detail-sidebar {
|
||||||
|
position: static;
|
||||||
|
}
|
||||||
|
.footer-grid {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
:root {
|
||||||
|
--gutter: 20px;
|
||||||
|
--nav-height: 56px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero {
|
||||||
|
padding-top: 64px;
|
||||||
|
padding-bottom: 40px;
|
||||||
|
}
|
||||||
|
.hero-title {
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
.hero-subtitle {
|
||||||
|
font-size: 15px;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
.hero-actions {
|
||||||
|
margin-top: 24px;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.hero::before {
|
||||||
|
width: 300px;
|
||||||
|
height: 300px;
|
||||||
|
}
|
||||||
|
.hero::after {
|
||||||
|
width: 250px;
|
||||||
|
height: 250px;
|
||||||
|
}
|
||||||
|
.section {
|
||||||
|
padding: 28px 0;
|
||||||
|
}
|
||||||
|
.section-compact {
|
||||||
|
padding: 24px 0;
|
||||||
|
}
|
||||||
|
.section-header {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.stats-bar {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
padding: 32px 0;
|
||||||
|
}
|
||||||
|
.stat-value {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
.stat-item {
|
||||||
|
padding: 16px 12px;
|
||||||
|
}
|
||||||
|
.card-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.category-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Detail page compact */
|
||||||
|
.detail-header {
|
||||||
|
padding-top: 24px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
padding-inline: 16px;
|
||||||
|
}
|
||||||
|
.detail-title {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
.detail-header-top {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.detail-icon {
|
||||||
|
width: 52px;
|
||||||
|
height: 52px;
|
||||||
|
}
|
||||||
|
.detail-slogan {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
.detail-badges {
|
||||||
|
gap: 5px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.detail-badges .badge {
|
||||||
|
padding: 3px 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
.detail-actions {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
margin-top: 14px;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.detail-actions .btn {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 36px;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 8px 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.detail-body {
|
||||||
|
padding-top: 20px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
.detail-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.detail-section-title {
|
||||||
|
font-size: 17px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
.detail-prose {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Feature tags compact on mobile */
|
||||||
|
.feature-tags {
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
.feature-tag {
|
||||||
|
padding: 3px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Screenshot carousel mobile */
|
||||||
|
.screenshot-slide {
|
||||||
|
flex: 0 0 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer compact */
|
||||||
|
.footer {
|
||||||
|
padding: 28px 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.footer-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 18px;
|
||||||
|
}
|
||||||
|
.footer-brand {
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.footer-col-title {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.footer-links {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.footer-project-links {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, max-content);
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px 22px;
|
||||||
|
}
|
||||||
|
.footer-project-links li:last-child:nth-child(odd) {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
.footer-community-links {
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 8px 18px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.footer-links a:hover {
|
||||||
|
padding-inline-start: 0;
|
||||||
|
}
|
||||||
|
.footer-bottom {
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 22px;
|
||||||
|
padding-top: 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.footer-policy-links,
|
||||||
|
.footer-legal {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Nav */
|
||||||
|
.nav-links {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.nav-mobile-toggle {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.page-title {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
.filter-bar {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
.search-input {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.filter-group {
|
||||||
|
width: 100%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
.filter-group-label {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
.select-control {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.select-trigger {
|
||||||
|
min-height: 36px;
|
||||||
|
padding-top: 7px;
|
||||||
|
padding-bottom: 7px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.select-menu {
|
||||||
|
top: calc(100% + 5px);
|
||||||
|
padding: 5px;
|
||||||
|
max-height: min(260px, calc(100vh - 180px));
|
||||||
|
}
|
||||||
|
.select-search {
|
||||||
|
min-height: 30px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.select-option {
|
||||||
|
min-height: 32px;
|
||||||
|
padding: 6px 8px 6px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Install guide compact */
|
||||||
|
.install-item {
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.install-item strong {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Download table scroll */
|
||||||
|
.download-table {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.download-table th,
|
||||||
|
.download-table td {
|
||||||
|
padding: 8px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar compact metadata */
|
||||||
|
.detail-meta-panel {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
padding: 8px;
|
||||||
|
gap: 6px;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
}
|
||||||
|
.detail-meta-item {
|
||||||
|
padding: 7px 8px;
|
||||||
|
}
|
||||||
|
.detail-meta-label {
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
.detail-meta-value {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.detail-link-grid {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
.detail-link-btn {
|
||||||
|
min-height: 34px;
|
||||||
|
padding: 7px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Trust note */
|
||||||
|
.trust-note {
|
||||||
|
padding: 12px 14px;
|
||||||
|
font-size: 13px;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Changelog */
|
||||||
|
.changelog-list {
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.changelog-summary {
|
||||||
|
min-height: 38px;
|
||||||
|
padding: 7px 10px;
|
||||||
|
grid-template-columns: minmax(76px, auto) 1fr auto;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.changelog-version {
|
||||||
|
font-size: 13px;
|
||||||
|
padding-left: 14px;
|
||||||
|
}
|
||||||
|
.changelog-date {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
.changelog-count {
|
||||||
|
min-width: 22px;
|
||||||
|
height: 20px;
|
||||||
|
padding: 0 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
.changelog-changes {
|
||||||
|
padding: 0 10px 10px 28px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-header {
|
||||||
|
gap: 16px;
|
||||||
|
margin: 22px 0 18px;
|
||||||
|
}
|
||||||
|
.about-avatar {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
.about-name {
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
.about-bio {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.focus-grid,
|
||||||
|
.contact-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.contact-card {
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
/* ── Scrollbar ───────────────────────────────────────── */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: oklch(28% 0.04 270);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: oklch(74% 0.2 45 / 35%);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.light ::-webkit-scrollbar-thumb {
|
||||||
|
background: oklch(80% 0.02 270);
|
||||||
|
}
|
||||||
|
:root.light ::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: oklch(58% 0.22 45 / 35%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Selection ───────────────────────────────────────── */
|
||||||
|
::selection {
|
||||||
|
background: oklch(74% 0.2 45 / 25%);
|
||||||
|
color: var(--fg);
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
/* ═══════════════════════════════════════════════════════
|
||||||
|
ZUJ OL Apps — Warm Aurora Design System v2
|
||||||
|
Palette: Deep space + Warm amber-coral aurora gradients
|
||||||
|
Font: Noto Sans SC + JetBrains Mono
|
||||||
|
Color Space: OKLCH
|
||||||
|
═══════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
/* ── Tokens (Dark-first) ──────────────────────────────── */
|
||||||
|
:root {
|
||||||
|
--bg: oklch(11% 0.02 270);
|
||||||
|
--surface: oklch(15% 0.025 270);
|
||||||
|
--surface-2: oklch(19% 0.028 270);
|
||||||
|
--fg: oklch(97% 0.005 270);
|
||||||
|
--muted: oklch(58% 0.028 270);
|
||||||
|
--border: oklch(24% 0.035 270);
|
||||||
|
--accent: oklch(74% 0.2 45);
|
||||||
|
--accent-fg: oklch(99% 0.005 270);
|
||||||
|
|
||||||
|
--blue: oklch(74% 0.2 45);
|
||||||
|
--emerald: oklch(74% 0.2 165);
|
||||||
|
--amber: oklch(83% 0.19 85);
|
||||||
|
--violet: oklch(70% 0.24 20);
|
||||||
|
--cyan: oklch(77% 0.16 200);
|
||||||
|
--red: oklch(69% 0.28 25);
|
||||||
|
--orange: oklch(76% 0.22 55);
|
||||||
|
--pink: oklch(72% 0.26 340);
|
||||||
|
|
||||||
|
--gradient-primary: linear-gradient(135deg, oklch(74% 0.2 45), oklch(70% 0.24 20));
|
||||||
|
--gradient-aurora: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
oklch(74% 0.2 45),
|
||||||
|
oklch(70% 0.24 20),
|
||||||
|
oklch(68% 0.22 350),
|
||||||
|
oklch(80% 0.17 85)
|
||||||
|
);
|
||||||
|
--gradient-warm: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
oklch(69% 0.28 25),
|
||||||
|
oklch(76% 0.22 55),
|
||||||
|
oklch(83% 0.19 85)
|
||||||
|
);
|
||||||
|
--gradient-shine: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
oklch(80% 0.17 85),
|
||||||
|
oklch(70% 0.24 20),
|
||||||
|
oklch(68% 0.22 350)
|
||||||
|
);
|
||||||
|
--gradient-glow: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
oklch(74% 0.2 45 / 30%),
|
||||||
|
oklch(70% 0.24 20 / 30%),
|
||||||
|
oklch(68% 0.22 350 / 20%)
|
||||||
|
);
|
||||||
|
|
||||||
|
--font-display: 'Noto Sans SC', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
|
||||||
|
--font-body: 'Noto Sans SC', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
|
||||||
|
--font-mono: 'JetBrains Mono', 'IBM Plex Mono', ui-monospace, Menlo, monospace;
|
||||||
|
|
||||||
|
--radius-sm: 8px;
|
||||||
|
--radius-md: 12px;
|
||||||
|
--radius-lg: 16px;
|
||||||
|
--radius-xl: 20px;
|
||||||
|
|
||||||
|
--shadow-sm: 0 1px 3px oklch(0% 0 0 / 20%);
|
||||||
|
--shadow-raised: 0 4px 24px oklch(0% 0 0 / 30%), 0 1px 4px oklch(0% 0 0 / 20%);
|
||||||
|
--shadow-glow: 0 0 30px oklch(74% 0.2 45 / 12%), 0 0 60px oklch(70% 0.24 20 / 8%);
|
||||||
|
--shadow-glow-lg: 0 0 40px oklch(74% 0.2 45 / 15%), 0 0 80px oklch(70% 0.24 20 / 10%);
|
||||||
|
|
||||||
|
--nav-height: 64px;
|
||||||
|
--max-width: 1200px;
|
||||||
|
--gutter: 40px;
|
||||||
|
|
||||||
|
--ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
|
--transition: 250ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
--transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
|
||||||
|
color-scheme: dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Light mode ───────────────────────────────────────── */
|
||||||
|
:root.light {
|
||||||
|
--bg: oklch(98% 0.005 270);
|
||||||
|
--surface: oklch(100% 0 0);
|
||||||
|
--surface-2: oklch(96% 0.008 270);
|
||||||
|
--fg: oklch(13% 0.025 270);
|
||||||
|
--muted: oklch(46% 0.028 270);
|
||||||
|
--border: oklch(88% 0.018 270);
|
||||||
|
--accent: oklch(58% 0.22 45);
|
||||||
|
--accent-fg: oklch(98% 0.005 270);
|
||||||
|
|
||||||
|
--blue: oklch(58% 0.22 45);
|
||||||
|
--emerald: oklch(52% 0.2 165);
|
||||||
|
--amber: oklch(68% 0.2 85);
|
||||||
|
--violet: oklch(55% 0.24 20);
|
||||||
|
--cyan: oklch(52% 0.18 200);
|
||||||
|
--red: oklch(55% 0.28 25);
|
||||||
|
--orange: oklch(60% 0.24 55);
|
||||||
|
--pink: oklch(52% 0.26 340);
|
||||||
|
|
||||||
|
--gradient-primary: linear-gradient(135deg, oklch(55% 0.24 20), oklch(58% 0.22 45));
|
||||||
|
--gradient-aurora: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
oklch(55% 0.24 20),
|
||||||
|
oklch(58% 0.22 45),
|
||||||
|
oklch(52% 0.22 350),
|
||||||
|
oklch(65% 0.18 85)
|
||||||
|
);
|
||||||
|
--gradient-warm: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
oklch(55% 0.28 25),
|
||||||
|
oklch(60% 0.24 55),
|
||||||
|
oklch(68% 0.2 85)
|
||||||
|
);
|
||||||
|
--gradient-shine: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
oklch(65% 0.18 85),
|
||||||
|
oklch(55% 0.24 20),
|
||||||
|
oklch(52% 0.26 340)
|
||||||
|
);
|
||||||
|
--gradient-glow: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
oklch(58% 0.22 45 / 10%),
|
||||||
|
oklch(55% 0.24 20 / 10%),
|
||||||
|
oklch(52% 0.22 350 / 6%)
|
||||||
|
);
|
||||||
|
|
||||||
|
--shadow-sm: 0 1px 3px oklch(13% 0.02 270 / 6%);
|
||||||
|
--shadow-raised: 0 4px 24px oklch(13% 0.02 270 / 8%), 0 1px 4px oklch(13% 0.02 270 / 4%);
|
||||||
|
--shadow-glow: 0 0 30px oklch(58% 0.22 45 / 8%), 0 0 60px oklch(55% 0.24 20 / 5%);
|
||||||
|
--shadow-glow-lg: 0 0 40px oklch(58% 0.22 45 / 10%), 0 0 80px oklch(55% 0.24 20 / 6%);
|
||||||
|
|
||||||
|
color-scheme: light;
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
import '@testing-library/jest-dom';
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { siteData } from '../data/siteData';
|
||||||
|
|
||||||
|
describe('siteData', () => {
|
||||||
|
it('has brand data with name and slogan', () => {
|
||||||
|
expect(siteData.brand).toBeDefined();
|
||||||
|
expect(siteData.brand.name).toHaveProperty('zh');
|
||||||
|
expect(siteData.brand.name).toHaveProperty('en');
|
||||||
|
expect(siteData.brand.slogan).toHaveProperty('zh');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has nav items', () => {
|
||||||
|
expect(siteData.nav.length).toBeGreaterThan(0);
|
||||||
|
expect(siteData.nav[0]).toHaveProperty('id');
|
||||||
|
expect(siteData.nav[0]).toHaveProperty('label');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has categories', () => {
|
||||||
|
expect(siteData.categories.length).toBeGreaterThan(0);
|
||||||
|
expect(siteData.categories[0]).toHaveProperty('id');
|
||||||
|
expect(siteData.categories[0]).toHaveProperty('label');
|
||||||
|
expect(siteData.categories[0]).toHaveProperty('icon');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has statuses', () => {
|
||||||
|
expect(Object.keys(siteData.statuses).length).toBeGreaterThan(0);
|
||||||
|
const firstStatus = Object.values(siteData.statuses)[0];
|
||||||
|
expect(firstStatus).toHaveProperty('label');
|
||||||
|
expect(firstStatus).toHaveProperty('color');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has platforms', () => {
|
||||||
|
expect(Object.keys(siteData.platforms).length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has projects sorted by order', () => {
|
||||||
|
expect(siteData.projects.length).toBeGreaterThan(0);
|
||||||
|
for (let i = 1; i < siteData.projects.length; i++) {
|
||||||
|
expect(siteData.projects[i].order).toBeGreaterThanOrEqual(
|
||||||
|
siteData.projects[i - 1].order
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('each project has required fields', () => {
|
||||||
|
for (const p of siteData.projects) {
|
||||||
|
expect(p.id).toBeTruthy();
|
||||||
|
expect(p.name).toBeTruthy();
|
||||||
|
expect(p.displayName).toHaveProperty('zh');
|
||||||
|
expect(p.displayName).toHaveProperty('en');
|
||||||
|
expect(p.slogan).toHaveProperty('zh');
|
||||||
|
expect(p.techStack).toBeInstanceOf(Array);
|
||||||
|
expect(p.platforms).toBeInstanceOf(Array);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has i18n data for zh and en', () => {
|
||||||
|
expect(siteData.i18n.zh).toBeDefined();
|
||||||
|
expect(siteData.i18n.en).toBeDefined();
|
||||||
|
expect(Object.keys(siteData.i18n.zh).length).toBeGreaterThan(0);
|
||||||
|
expect(Object.keys(siteData.i18n.en).length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has about data', () => {
|
||||||
|
expect(siteData.about).toBeDefined();
|
||||||
|
expect(siteData.about.bio).toHaveProperty('zh');
|
||||||
|
expect(siteData.about.bio).toHaveProperty('en');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { renderHook } from '@testing-library/react';
|
||||||
|
import { type ReactNode } from 'react';
|
||||||
|
import { AppProvider } from '../contexts/AppContext';
|
||||||
|
import { useI18n } from '../hooks/useI18n';
|
||||||
|
|
||||||
|
function wrapper({ children }: { children: ReactNode }) {
|
||||||
|
return <AppProvider>{children}</AppProvider>;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('useI18n', () => {
|
||||||
|
it('returns zh as default lang', () => {
|
||||||
|
const { result } = renderHook(() => useI18n(), { wrapper });
|
||||||
|
expect(result.current.lang).toBe('zh');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('t() returns translated text for known key', () => {
|
||||||
|
const { result } = renderHook(() => useI18n(), { wrapper });
|
||||||
|
const text = result.current.t('hero.title');
|
||||||
|
expect(text).toBeTruthy();
|
||||||
|
expect(typeof text).toBe('string');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('t() returns key itself for unknown key', () => {
|
||||||
|
const { result } = renderHook(() => useI18n(), { wrapper });
|
||||||
|
const text = result.current.t('nonexistent.key');
|
||||||
|
expect(text).toBe('nonexistent.key');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('bi() returns zh value for bilingual text', () => {
|
||||||
|
const { result } = renderHook(() => useI18n(), { wrapper });
|
||||||
|
const text = result.current.bi({ zh: '你好', en: 'Hello' });
|
||||||
|
expect(text).toBe('你好');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('bi() returns empty string for undefined', () => {
|
||||||
|
const { result } = renderHook(() => useI18n(), { wrapper });
|
||||||
|
expect(result.current.bi(undefined)).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('biArray() returns zh array for bilingual array', () => {
|
||||||
|
const { result } = renderHook(() => useI18n(), { wrapper });
|
||||||
|
const arr = result.current.biArray({ zh: ['甲', '乙'], en: ['A', 'B'] });
|
||||||
|
expect(arr).toEqual(['甲', '乙']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('biArray() returns empty array for undefined', () => {
|
||||||
|
const { result } = renderHook(() => useI18n(), { wrapper });
|
||||||
|
expect(result.current.biArray(undefined)).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -11,6 +11,7 @@ export interface BilingualArray {
|
|||||||
export interface BrandData {
|
export interface BrandData {
|
||||||
name: BilingualText;
|
name: BilingualText;
|
||||||
slogan: BilingualText;
|
slogan: BilingualText;
|
||||||
|
logo?: string;
|
||||||
author: string;
|
author: string;
|
||||||
github: string;
|
github: string;
|
||||||
email: string;
|
email: string;
|
||||||
@@ -86,6 +87,7 @@ export interface Project {
|
|||||||
roadmap?: RoadmapItem;
|
roadmap?: RoadmapItem;
|
||||||
changelog?: ChangelogEntry[];
|
changelog?: ChangelogEntry[];
|
||||||
architecture?: BilingualText;
|
architecture?: BilingualText;
|
||||||
|
screenshots?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AboutData {
|
export interface AboutData {
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import type { ComponentType } from 'react';
|
||||||
|
import {
|
||||||
|
NotebookPen,
|
||||||
|
Terminal,
|
||||||
|
MapPin,
|
||||||
|
Smartphone,
|
||||||
|
BookOpen,
|
||||||
|
Monitor,
|
||||||
|
Brain,
|
||||||
|
KeyRound,
|
||||||
|
Wrench,
|
||||||
|
Package,
|
||||||
|
Cloud,
|
||||||
|
Server,
|
||||||
|
FlaskConical,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
const iconRegistry: Record<string, ComponentType<{ size?: number }>> = {
|
||||||
|
NotebookPen,
|
||||||
|
Terminal,
|
||||||
|
MapPin,
|
||||||
|
Smartphone,
|
||||||
|
BookOpen,
|
||||||
|
Monitor,
|
||||||
|
Brain,
|
||||||
|
KeyRound,
|
||||||
|
Wrench,
|
||||||
|
Package,
|
||||||
|
Cloud,
|
||||||
|
Server,
|
||||||
|
FlaskConical,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getIcon(name: string): ComponentType<{ size?: number }> | undefined {
|
||||||
|
return iconRegistry[name];
|
||||||
|
}
|
||||||
+1
-4
@@ -1,7 +1,4 @@
|
|||||||
{
|
{
|
||||||
"files": [],
|
"files": [],
|
||||||
"references": [
|
"references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }]
|
||||||
{ "path": "./tsconfig.app.json" },
|
|
||||||
{ "path": "./tsconfig.node.json" }
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
+30
-4
@@ -1,7 +1,33 @@
|
|||||||
import { defineConfig } from 'vite'
|
/// <reference types="vitest/config" />
|
||||||
import react from '@vitejs/plugin-react'
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
// https://vite.dev/config/
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
})
|
build: {
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
manualChunks(id) {
|
||||||
|
if (id.includes('node_modules/react') || id.includes('node_modules/react-router')) {
|
||||||
|
return 'vendor-react';
|
||||||
|
}
|
||||||
|
if (id.includes('node_modules/lucide')) {
|
||||||
|
return 'vendor-lucide';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
assetsInlineLimit: 4096,
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
warmup: {
|
||||||
|
clientFiles: ['./src/data/loader.ts'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
globals: true,
|
||||||
|
environment: 'jsdom',
|
||||||
|
setupFiles: './src/test/setup.ts',
|
||||||
|
css: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user