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:
2026-05-22 13:34:41 +08:00
parent 5e79c96364
commit 6b58b55c32
83 changed files with 5890 additions and 3955 deletions
+11
View File
@@ -0,0 +1,11 @@
{
"version": "0.0.1",
"configurations": [
{
"name": "dev",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "dev"],
"port": 5173
}
]
}
+17
View File
@@ -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 *)"
]
}
}
+29
View File
@@ -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
+6
View File
@@ -0,0 +1,6 @@
dist
pnpm-lock.yaml
public
*.svg
*.png
*.ico
+9
View File
@@ -0,0 +1,9 @@
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"printWidth": 100,
"bracketSpacing": true,
"jsxSingleQuote": false
}
+4 -4
View File
@@ -40,15 +40,15 @@ export default defineConfig([
// 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:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
import reactX from 'eslint-plugin-react-x';
import reactDom from 'eslint-plugin-react-dom';
export default defineConfig([
globalIgnores(['dist']),
@@ -69,5 +69,5 @@ export default defineConfig([
// other options...
},
},
])
]);
```
+316
View File
@@ -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
View File
@@ -1,9 +1,9 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'
import js from '@eslint/js';
import globals from 'globals';
import reactHooks from 'eslint-plugin-react-hooks';
import reactRefresh from 'eslint-plugin-react-refresh';
import tseslint from 'typescript-eslint';
import { defineConfig, globalIgnores } from 'eslint/config';
export default defineConfig([
globalIgnores(['dist']),
@@ -18,5 +18,10 @@ export default defineConfig([
languageOptions: {
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
View File
@@ -1,19 +1,22 @@
<!doctype html>
<html lang="zh">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>ZUJ OL Apps — 开源软件聚合站</title>
<meta name="description" content="构建轻量、高效、开源的软件工具。涵盖桌面软件、移动应用、开发者工具、笔记系统、SSH 客户端、远程控制、文档组件库与全栈应用。">
<meta name="keywords" content="开源软件,桌面应用,移动应用,开发者工具,Tauri,Rust,React,React Native">
<meta property="og:title" content="ZUJ OL Apps — 开源软件聚合站">
<meta property="og:description" content="构建轻量、高效、开源的软件工具">
<meta property="og:type" content="website">
<meta name="color-scheme" content="dark light">
<meta name="twitter:card" content="summary">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Noto+Sans+SC:wght@400;500;600;700&display=swap" rel="stylesheet">
<meta
name="description"
content="构建轻量、高效、开源软件工具。涵盖桌面软件、移动应用、开发者工具、笔记系统、SSH 客户端、远程控制、文档组件库与全栈应用。"
/>
<meta
name="keywords"
content="开源软件,桌面应用,移动应用,开发者工具,Tauri,Rust,React,React Native"
/>
<meta property="og:title" content="ZUJ OL Apps — 开源软件聚合站" />
<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>
<body>
<div id="root"></div>
+10 -1
View File
@@ -7,6 +7,9 @@
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"format": "prettier --write .",
"test": "vitest run",
"test:watch": "vitest",
"preview": "vite preview"
},
"dependencies": {
@@ -18,6 +21,9 @@
},
"devDependencies": {
"@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/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
@@ -26,9 +32,12 @@
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.6.0",
"jsdom": "^29.1.1",
"prettier": "^3.8.3",
"typescript": "~6.0.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"
}
+763
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 456 KiB

BIN
View File
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.
-24
View File
@@ -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

+19 -14
View File
@@ -1,14 +1,17 @@
import { useEffect } from 'react';
import { lazy, Suspense, useEffect } from 'react';
import { Routes, Route } from 'react-router-dom';
import { useScrollToTop } from './hooks/useScrollToTop';
import Nav from './components/Nav';
import Footer from './components/Footer';
import HomePage from './pages/HomePage';
import ProjectsPage from './pages/ProjectsPage';
import ProjectDetailPage from './pages/ProjectDetailPage';
import AboutPage from './pages/AboutPage';
import LoadingFallback from './components/LoadingFallback';
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() {
useScrollToTop();
@@ -19,7 +22,7 @@ function App() {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add('revealed');
observer.unobserve(entry.target);
@@ -70,13 +73,15 @@ function App() {
<>
<Nav />
<main className="main-content">
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/projects" element={<ProjectsPage />} />
<Route path="/projects/:id" element={<ProjectDetailPage />} />
<Route path="/about" element={<AboutPage />} />
<Route path="/contact" element={<AboutPage />} />
</Routes>
<Suspense fallback={<LoadingFallback />}>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/projects" element={<ProjectsPage />} />
<Route path="/projects/:id" element={<ProjectDetailPage />} />
<Route path="/about" element={<AboutPage />} />
<Route path="/contact" element={<AboutPage />} />
</Routes>
</Suspense>
</main>
<Footer />
</>
+4 -20
View File
@@ -1,17 +1,7 @@
import { Link } from 'react-router-dom';
import { Monitor, Smartphone, Wrench, Package, Cloud, Server, FlaskConical } from 'lucide-react';
import { useI18n } from '../hooks/useI18n';
import { siteData } from '../data/siteData';
const iconMap: Record<string, React.ComponentType<{ size?: number }>> = {
Monitor,
Smartphone,
Wrench,
Package,
Cloud,
Server,
FlaskConical,
};
import { getIcon } from '../utils/iconRegistry';
export default function CategoryGrid() {
const { bi } = useI18n();
@@ -19,16 +9,10 @@ export default function CategoryGrid() {
return (
<div className="category-grid">
{siteData.categories.map((cat) => {
const IconComponent = iconMap[cat.icon];
const IconComponent = getIcon(cat.icon);
return (
<Link
key={cat.id}
to={`/projects?cat=${cat.id}`}
className="category-card"
>
<div className="category-icon">
{IconComponent && <IconComponent size={28} />}
</div>
<Link key={cat.id} 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>
</Link>
);
+2 -9
View File
@@ -27,16 +27,9 @@ export default function DownloadTable({ downloads, showChecksum = false }: Downl
<td>{dl.platform}</td>
<td>{dl.arch}</td>
<td>{dl.size}</td>
{showChecksum && (
<td className="sha256">{dl.sha256}</td>
)}
{showChecksum && <td className="sha256">{dl.sha256}</td>}
<td>
<a
href={dl.url}
className="btn btn-sm"
target="_blank"
rel="noopener noreferrer"
>
<a href={dl.url} className="btn btn-sm" target="_blank" rel="noopener noreferrer">
<Download size={14} /> {t('common.download')}
</a>
</td>
+14 -25
View File
@@ -1,44 +1,31 @@
import type { ComponentType } from 'react';
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 { siteData } from '../data/siteData';
import { getIcon } from '../utils/iconRegistry';
import type { Project } from '../types';
const iconMap: Record<string, ComponentType<{ size?: number }>> = {
NotebookPen,
Terminal,
MapPin,
Smartphone,
BookOpen,
Monitor,
Brain,
KeyRound,
};
interface FeaturedCardProps {
project: Project;
}
export default function FeaturedCard({ project }: FeaturedCardProps) {
const { bi } = useI18n();
const { bi, lang } = useI18n();
const status = siteData.statuses[project.status];
const IconComponent = iconMap[project.icon];
const IconComponent = getIcon(project.icon);
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-icon">
{IconComponent && <IconComponent size={22} />}
</div>
<div className="project-icon">{IconComponent && <IconComponent size={22} />}</div>
<div className="project-card-info">
<div className="project-name">
{project.name}
{project.displayName[lang] || project.name}
{status && (
<span
className="badge badge-status"
style={{ color: status.color }}
>
<span className="badge badge-status" style={{ '--status-color': status.color } as React.CSSProperties}>
{bi(status.label)}
</span>
)}
@@ -49,7 +36,9 @@ export default function FeaturedCard({ project }: FeaturedCardProps) {
<div className="project-card-meta">
{project.techStack.slice(0, 3).map((tech) => (
<span key={tech} className="badge">{tech}</span>
<span key={tech} className="badge">
{tech}
</span>
))}
</div>
+22 -7
View File
@@ -3,7 +3,7 @@ import { useI18n } from '../hooks/useI18n';
import { siteData } from '../data/siteData';
export default function Footer() {
const { t, bi } = useI18n();
const { t, bi, lang } = useI18n();
return (
<footer className="footer">
@@ -16,25 +16,40 @@ export default function Footer() {
<div>
<div className="footer-col-title">{t('footer.projects')}</div>
<ul className="footer-links footer-project-links">
{siteData.projects.slice(0, 5).map(p => (
<li key={p.id}><Link to={`/projects/${p.id}`}>{p.name}</Link></li>
{siteData.projects.slice(0, 6).map((p) => (
<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>
</div>
<div>
<div className="footer-col-title">{t('footer.community')}</div>
<ul className="footer-links footer-community-links">
<li><a href={siteData.brand.github} target="_blank">GitHub</a></li>
<li><Link to="/about">{t('about.title')}</Link></li>
<li>
<a href={siteData.brand.github} target="_blank">
GitHub
</a>
</li>
<li>
<Link to="/about">{t('about.title')}</Link>
</li>
</ul>
</div>
</div>
<div className="footer-bottom">
<span>{t('footer.copyright')}</span>
<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 className="footer-legal">
{siteData.brand.icp && (
+1 -6
View File
@@ -14,12 +14,7 @@ export default function Hero() {
<Link to="/projects" className="btn btn-primary">
<FolderKanban size={16} /> {t('hero.cta.projects')}
</Link>
<a
href={siteData.brand.github}
target="_blank"
rel="noopener noreferrer"
className="btn"
>
<a href={siteData.brand.github} target="_blank" rel="noopener noreferrer" className="btn">
<ExternalLink size={16} /> {t('hero.cta.github')}
</a>
</div>
+7
View File
@@ -0,0 +1,7 @@
export default function LoadingFallback() {
return (
<div className="container loading-fallback">
<div className="loading-spinner" />
</div>
);
}
+10 -6
View File
@@ -5,7 +5,8 @@ import { useI18n } from '../hooks/useI18n';
import { siteData } from '../data/siteData';
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 location = useLocation();
const currentPath = '/' + location.pathname.slice(1);
@@ -15,14 +16,17 @@ export default function Nav() {
<nav className="nav">
<div className="nav-inner">
<Link to="/" className="nav-brand">
<span className="nav-brand-icon">Z</span>
{siteData.brand.logo ? (
<img src={siteData.brand.logo} alt={bi(siteData.brand.name)} className="nav-brand-icon" />
) : (
<span className="nav-brand-icon">Z</span>
)}
{bi(siteData.brand.name)}
</Link>
<div className="nav-links">
{siteData.nav.map(n => {
{siteData.nav.map((n) => {
const navPath = n.hash.slice(2) || '/';
const isActive = currentPath === navPath ||
(navPath === '/' && currentPath === '');
const isActive = currentPath === navPath || (navPath === '/' && currentPath === '');
return (
<Link
key={n.id}
@@ -56,7 +60,7 @@ export default function Nav() {
</div>
</nav>
<div className={`nav-mobile-menu${mobileMenuOpen ? ' open' : ''}`}>
{siteData.nav.map(n => {
{siteData.nav.map((n) => {
const navPath = n.hash.slice(2) || '/';
return (
<Link
+11 -30
View File
@@ -1,44 +1,28 @@
import type { ComponentType } from 'react';
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 { siteData } from '../data/siteData';
import { getIcon } from '../utils/iconRegistry';
import type { Project } from '../types';
const iconMap: Record<string, ComponentType<{ size?: number }>> = {
NotebookPen,
Terminal,
MapPin,
Smartphone,
BookOpen,
Monitor,
Brain,
KeyRound,
};
interface ProjectCardProps {
project: Project;
}
export default function ProjectCard({ project }: ProjectCardProps) {
const { t, bi } = useI18n();
const { t, bi, lang } = useI18n();
const status = siteData.statuses[project.status];
const IconComponent = iconMap[project.icon];
const IconComponent = getIcon(project.icon);
return (
<div className="project-card">
<div className="project-card-header">
<div className="project-icon">
{IconComponent && <IconComponent size={22} />}
</div>
<div className="project-icon">{IconComponent && <IconComponent size={22} />}</div>
<div className="project-card-info">
<div className="project-name">
<Link to={`/projects/${project.id}`}>{project.name}</Link>
<Link to={`/projects/${project.id}`}>{project.displayName[lang] || project.name}</Link>
{status && (
<span
className="badge badge-status"
style={{ color: status.color }}
>
<span className="badge badge-status" style={{ '--status-color': status.color } as React.CSSProperties}>
{bi(status.label)}
</span>
)}
@@ -49,7 +33,9 @@ export default function ProjectCard({ project }: ProjectCardProps) {
<div className="project-card-meta">
{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) => (
<span key={platform} className="badge">
@@ -65,12 +51,7 @@ export default function ProjectCard({ project }: ProjectCardProps) {
</div>
<div className="project-card-actions">
<a
href={project.repoUrl}
target="_blank"
rel="noopener noreferrer"
className="btn btn-sm"
>
<a href={project.repoUrl} target="_blank" rel="noopener noreferrer" className="btn btn-sm">
<ExternalLink size={14} /> GitHub
</a>
{project.downloads.length > 0 && (
+1 -3
View File
@@ -38,9 +38,7 @@ export default function RoadmapGrid({ roadmap, showCounts = false }: RoadmapGrid
<span>{item}</span>
</div>
))}
{roadmap[tab].length === 0 && (
<div className="roadmap-empty"></div>
)}
{roadmap[tab].length === 0 && <div className="roadmap-empty"></div>}
</div>
</div>
);
+65
View File
@@ -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>
);
}
+9 -4
View File
@@ -28,9 +28,10 @@ export default function SelectControl({
const [query, setQuery] = useState('');
const selected = options.find((option) => option.value === value) ?? options[0];
const visibleOptions = searchable && query
? options.filter((option) => option.label.toLowerCase().includes(query.toLowerCase()))
: options;
const visibleOptions =
searchable && query
? options.filter((option) => option.label.toLowerCase().includes(query.toLowerCase()))
: options;
useEffect(() => {
if (!open) return;
@@ -101,7 +102,11 @@ export default function SelectControl({
}}
>
<span>{option.label}</span>
{active && <span className="select-check" aria-hidden="true"></span>}
{active && (
<span className="select-check" aria-hidden="true">
</span>
)}
</button>
);
})
+13 -3
View File
@@ -18,11 +18,11 @@ export function AppProvider({ children }: { children: ReactNode }) {
}, [theme]);
const toggleTheme = useCallback(() => {
setTheme(prev => prev === 'dark' ? 'light' : 'dark');
setTheme((prev) => (prev === 'dark' ? 'light' : 'dark'));
}, []);
const toggleLang = useCallback(() => {
setLang(prev => {
setLang((prev) => {
const next = prev === 'zh' ? 'en' : 'zh';
localStorage.setItem('lang', next);
return next;
@@ -33,7 +33,17 @@ export function AppProvider({ children }: { children: ReactNode }) {
const closeMobileMenu = useCallback(() => setMobileMenuOpen(false), []);
return (
<AppContext.Provider value={{ lang, theme, mobileMenuOpen, toggleTheme, toggleLang, openMobileMenu, closeMobileMenu }}>
<AppContext.Provider
value={{
lang,
theme,
mobileMenuOpen,
toggleTheme,
toggleLang,
openMobileMenu,
closeMobileMenu,
}}
>
{children}
</AppContext.Provider>
);
+33 -33
View File
@@ -1,36 +1,36 @@
bio:
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."
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.'
focus:
zh:
- "Tauri 桌面软件"
- "Rust 后端"
- "React / TypeScript 前端"
- "React Native 移动端"
- "开发者工具"
- "自托管服务"
- "AI Agent 工具链"
- "开源软件产品化"
en:
- "Tauri desktop apps"
- "Rust backend"
- "React / TypeScript frontend"
- "React Native mobile"
- "Developer tools"
- "Self-hosted services"
- "AI Agent toolchain"
- "Open-source software productization"
zh:
- 'Tauri 桌面软件'
- 'Rust 后端'
- 'React / TypeScript 前端'
- 'React Native 移动端'
- '开发者工具'
- '自托管服务'
- 'AI Agent 工具链'
- '开源软件产品化'
en:
- 'Tauri desktop apps'
- 'Rust backend'
- 'React / TypeScript frontend'
- 'React Native mobile'
- 'Developer tools'
- 'Self-hosted services'
- 'AI Agent toolchain'
- 'Open-source software productization'
techStack:
- "Tauri"
- "Rust"
- "React"
- "TypeScript"
- "React Native"
- "Expo"
- "Node.js"
- "Python"
- "SQLite"
- "PostgreSQL"
- "Docker"
- "TailwindCSS"
github: "https://github.com/shenjianZ"
- 'Tauri'
- 'Rust'
- 'React'
- 'TypeScript'
- 'React Native'
- 'Expo'
- 'Node.js'
- 'Python'
- 'SQLite'
- 'PostgreSQL'
- 'Docker'
- 'TailwindCSS'
github: 'https://github.com/shenjianZ'
+10 -9
View File
@@ -1,11 +1,12 @@
name:
zh: "ZUJ OL 软件工坊"
en: "ZUJ OL Apps"
zh: 'ZUJ OL 软件工坊'
en: 'ZUJ OL Apps'
slogan:
zh: "构建轻量、高效、开源的软件工具"
en: "Building lightweight, efficient, open-source software tools"
author: "shenjianZ"
github: "https://github.com/shenjianZ"
email: "15202078626@163.com"
icp: "ICP备案号待填写"
policeRecord: "公网安备号待填写"
zh: '构建轻量、高效、开源的软件工具'
en: 'Building lightweight, efficient, open-source software tools'
logo: '/logo.svg'
author: 'shenjianZ'
github: 'https://github.com/shenjianZ'
email: '15202078626@163.com'
icp: '豫ICP备2023019300号'
policeRecord: '豫公网安备41102502000221号'
+28 -28
View File
@@ -1,41 +1,41 @@
- id: "desktop"
- id: 'desktop'
label:
zh: "桌面软件"
en: "Desktop Apps"
icon: "Monitor"
zh: '桌面软件'
en: 'Desktop Apps'
icon: 'Monitor'
- id: "mobile"
- id: 'mobile'
label:
zh: "移动应用"
en: "Mobile Apps"
icon: "Smartphone"
zh: '移动应用'
en: 'Mobile Apps'
icon: 'Smartphone'
- id: "devtool"
- id: 'devtool'
label:
zh: "开发者工具"
en: "Dev Tools"
icon: "Wrench"
zh: '开发者工具'
en: 'Dev Tools'
icon: 'Wrench'
- id: "library"
- id: 'library'
label:
zh: "NPM / 组件库"
en: "NPM / Libraries"
icon: "Package"
zh: 'NPM / 组件库'
en: 'NPM / Libraries'
icon: 'Package'
- id: "backend"
- id: 'backend'
label:
zh: "后端服务"
en: "Backend"
icon: "Cloud"
zh: '后端服务'
en: 'Backend'
icon: 'Cloud'
- id: "selfhosted"
- id: 'selfhosted'
label:
zh: "自托管"
en: "Self-hosted"
icon: "Server"
zh: '自托管'
en: 'Self-hosted'
icon: 'Server'
- id: "ai"
- id: 'ai'
label:
zh: "AI / 实验项目"
en: "AI / Experiments"
icon: "FlaskConical"
zh: 'AI / 实验项目'
en: 'AI / Experiments'
icon: 'FlaskConical'
+127 -125
View File
@@ -1,125 +1,127 @@
nav.search: "Search projects..."
nav.theme: "Toggle theme"
nav.lang: "中文"
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.cta.projects: "View All Projects"
hero.cta.github: "Visit GitHub"
hero.cta.download: "Download Software"
hero.cta.docs: "View Docs"
stats.projects: "Open Source Projects"
stats.stars: "GitHub Stars"
stats.techStack: "Tech Stacks"
stats.platforms: "Platforms"
featured.title: "Featured Projects"
featured.subtitle: "Core projects under active development"
categories.title: "Categories"
categories.subtitle: "Browse all open-source projects by type"
latest.title: "Latest Releases"
latest.subtitle: "Recent version updates"
techstack.title: "Tech Stack"
techstack.subtitle: "Primary technologies used across projects"
cta.title: "Open Source Philosophy"
cta.subtitle: "Great software should be transparent, auditable, and customizable. All project source code is available on GitHub. Contributions welcome."
cta.button: "Explore on GitHub"
projects.title: "All Projects"
projects.subtitle: "Browse and filter all open-source software projects"
projects.filter.all: "All"
projects.filter.tech: "Tech Stack"
projects.filter.platform: "Platform"
projects.filter.status: "Status"
projects.sort.updated: "Recently Updated"
projects.sort.stars: "Most Stars"
projects.sort.name: "By Name"
projects.noResults: "No matching projects"
projects.search: "Search project name, description, or tags..."
detail.overview: "Overview"
detail.features: "Features"
detail.screenshots: "Screenshots"
detail.downloads: "Downloads"
detail.techstack: "Tech Stack"
detail.architecture: "Architecture"
detail.roadmap: "Roadmap"
detail.changelog: "Changelog"
detail.info: "Project Info"
detail.version: "Version"
detail.license: "License"
detail.platforms: "Platforms"
detail.status: "Status"
detail.lastUpdate: "Last Updated"
detail.repo: "GitHub Repo"
detail.docs: "Online Docs"
detail.release: "Download Release"
detail.installGuide: "Installation Guide"
detail.install.windows: "Windows: Double-click the installer and follow the prompts"
detail.install.macos: "macOS: If you see \"unidentified developer\", go to System Settings → Privacy & Security → Open Anyway"
detail.install.linux: "Linux: chmod +x and run, or install via package manager"
detail.install.android: "Android: Download the APK and enable unknown sources"
detail.roadmap.done: "Completed"
detail.roadmap.doing: "In Progress"
detail.roadmap.planned: "Planned"
detail.screenshotPlaceholder: "Screenshot Preview"
downloads.title: "Download Center"
downloads.subtitle: "Download the latest versions of all open-source software"
downloads.fileSize: "File Size"
downloads.checksum: "SHA256 Checksum"
downloads.allReleases: "View All Releases"
downloads.installGuide: "Installation Guide"
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.title: "Documentation"
docs.subtitle: "Quick start, usage guides, and developer documentation"
docs.quickstart: "Quick Start"
docs.install: "Installation"
docs.usage: "Basic Usage"
docs.advanced: "Advanced Features"
docs.config: "Configuration"
docs.faq: "FAQ"
docs.dev: "Developer Guide"
docs.deploy: "Deployment Guide"
docs.api: "API Reference"
docs.contribute: "Contributing"
docs.selectProject: "Select a project to view docs"
changelog.title: "Changelog"
changelog.subtitle: "Version update history for all projects"
changelog.all: "All Projects"
roadmap.title: "Roadmap"
roadmap.subtitle: "Development plans and progress for all projects"
roadmap.all: "All Projects"
about.title: "About"
about.subtitle: "Learn about the developer and open-source philosophy"
about.bio: "About"
about.focus: "Focus Areas"
about.techStack: "Tech Stack"
about.links: "Links"
about.github: "GitHub Profile"
about.opensource: "Open Source Philosophy"
contact.title: "Contact & Feedback"
contact.subtitle: "Bug reports, feature requests, and community discussion"
contact.issues: "Report Issues"
contact.issues.desc: "Report bugs or submit feature requests on GitHub Issues"
contact.discussions: "Discussions"
contact.discussions.desc: "Join the conversation on GitHub Discussions"
contact.email: "Email"
contact.email.desc: "Contact the author directly via email"
contact.security: "Security"
contact.security.desc: "Report security vulnerabilities through private channels"
footer.slogan: "Building lightweight, efficient, open-source software tools"
footer.projects: "Projects"
footer.resources: "Resources"
footer.community: "Community"
footer.license: "License"
footer.privacy: "Privacy Policy"
footer.security: "Security Policy"
footer.copyright: "© 2026 ZUJ OL. All rights reserved."
common.viewAll: "View All"
common.learnMore: "Learn More"
common.download: "Download"
common.docs: "Docs"
common.demo: "Live Demo"
common.back: "Back"
common.stars: "Stars"
common.forks: "Forks"
common.version: "Version"
common.platform: "Platform"
common.size: "Size"
common.arch: "Arch"
nav.search: 'Search projects...'
nav.theme: 'Toggle theme'
nav.lang: '中文'
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.cta.projects: 'View All Projects'
hero.cta.github: 'Visit GitHub'
hero.cta.download: 'Download Software'
hero.cta.docs: 'View Docs'
stats.projects: 'Open Source Projects'
stats.stars: 'GitHub Stars'
stats.techStack: 'Tech Stacks'
stats.platforms: 'Platforms'
featured.title: 'Featured Projects'
featured.subtitle: 'Core projects under active development'
featured.viewAll: 'View All Projects'
categories.title: 'Categories'
categories.subtitle: 'Browse all open-source projects by type'
latest.title: 'Latest Releases'
latest.subtitle: 'Recent version updates'
techstack.title: 'Tech Stack'
techstack.subtitle: 'Primary technologies used across projects'
cta.title: 'Open Source Philosophy'
cta.subtitle: 'Great software should be transparent, auditable, and customizable. All project source code is available on GitHub. Contributions welcome.'
cta.button: 'Explore on GitHub'
projects.title: 'All Projects'
projects.subtitle: 'Browse and filter all open-source software projects'
projects.filter.all: 'All'
projects.filter.tech: 'Tech Stack'
projects.filter.platform: 'Platform'
projects.filter.status: 'Status'
projects.sort.updated: 'Recently Updated'
projects.sort.stars: 'Most Stars'
projects.sort.name: 'By Name'
projects.noResults: 'No matching projects'
projects.search: 'Search project name, description, or tags...'
detail.overview: 'Overview'
detail.features: 'Features'
detail.screenshots: 'Screenshots'
detail.downloads: 'Downloads'
detail.techstack: 'Tech Stack'
detail.architecture: 'Architecture'
detail.roadmap: 'Roadmap'
detail.changelog: 'Changelog'
detail.info: 'Project Info'
detail.version: 'Version'
detail.license: 'License'
detail.platforms: 'Platforms'
detail.status: 'Status'
detail.lastUpdate: 'Last Updated'
detail.repo: 'GitHub Repo'
detail.docs: 'Online Docs'
detail.release: 'Download Release'
detail.installGuide: 'Installation Guide'
detail.install.windows: 'Windows: Double-click the installer and follow the prompts'
detail.install.macos: 'macOS: If you see "unidentified developer", go to System Settings → Privacy & Security → Open Anyway'
detail.install.linux: 'Linux: chmod +x and run, or install via package manager'
detail.install.android: 'Android: Download the APK and enable unknown sources'
detail.roadmap.done: 'Completed'
detail.roadmap.doing: 'In Progress'
detail.roadmap.planned: 'Planned'
detail.screenshotPlaceholder: 'Screenshot Preview'
downloads.title: 'Download Center'
downloads.subtitle: 'Download the latest versions of all open-source software'
downloads.fileSize: 'File Size'
downloads.checksum: 'SHA256 Checksum'
downloads.allReleases: 'View All Releases'
downloads.installGuide: 'Installation Guide'
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.title: 'Documentation'
docs.subtitle: 'Quick start, usage guides, and developer documentation'
docs.quickstart: 'Quick Start'
docs.install: 'Installation'
docs.usage: 'Basic Usage'
docs.advanced: 'Advanced Features'
docs.config: 'Configuration'
docs.faq: 'FAQ'
docs.dev: 'Developer Guide'
docs.deploy: 'Deployment Guide'
docs.api: 'API Reference'
docs.contribute: 'Contributing'
docs.selectProject: 'Select a project to view docs'
changelog.title: 'Changelog'
changelog.subtitle: 'Version update history for all projects'
changelog.all: 'All Projects'
roadmap.title: 'Roadmap'
roadmap.subtitle: 'Development plans and progress for all projects'
roadmap.all: 'All Projects'
about.title: 'About'
about.subtitle: 'Learn about the developer and open-source philosophy'
about.bio: 'About'
about.focus: 'Focus Areas'
about.techStack: 'Tech Stack'
about.links: 'Links'
about.github: 'GitHub Profile'
about.opensource: 'Open Source Philosophy'
contact.title: 'Contact & Feedback'
contact.subtitle: 'Bug reports, feature requests, and community discussion'
contact.issues: 'Report Issues'
contact.issues.desc: 'Report bugs or submit feature requests on GitHub Issues'
contact.discussions: 'Discussions'
contact.discussions.desc: 'Join the conversation on GitHub Discussions'
contact.email: 'Email'
contact.email.desc: 'Contact the author directly via email'
contact.security: 'Security'
contact.security.desc: 'Report security vulnerabilities through private channels'
footer.slogan: 'Building lightweight, efficient, open-source software tools'
footer.projects: 'Projects'
footer.resources: 'Resources'
footer.community: 'Community'
footer.license: 'License'
footer.privacy: 'Privacy Policy'
footer.security: 'Security Policy'
footer.copyright: '© 2026 ZUJ OL. All rights reserved.'
footer.viewAllProjects: 'View All'
common.viewAll: 'View All'
common.learnMore: 'Learn More'
common.download: 'Download'
common.docs: 'Docs'
common.demo: 'Live Demo'
common.back: 'Back'
common.stars: 'Stars'
common.forks: 'Forks'
common.version: 'Version'
common.platform: 'Platform'
common.size: 'Size'
common.arch: 'Arch'
+126 -124
View File
@@ -1,125 +1,127 @@
nav.search: "搜索项目..."
nav.theme: "切换主题"
nav.lang: "English"
hero.title: "构建轻量、高效、开源的软件工具"
hero.subtitle: "涵盖桌面软件、移动应用、开发者工具、笔记系统、SSH 客户端、远程控制、文档组件库与全栈应用。"
hero.cta.projects: "查看所有项目"
hero.cta.github: "访问 GitHub"
hero.cta.download: "下载软件"
hero.cta.docs: "查看文档"
stats.projects: "开源项目"
stats.stars: "GitHub Stars"
stats.techStack: "技术栈"
stats.platforms: "支持平台"
featured.title: "重点项目"
featured.subtitle: "正在积极开发和推荐使用的核心项目"
categories.title: "项目分类"
categories.subtitle: "按类型浏览所有开源项目"
latest.title: "最新发布"
latest.subtitle: "最近的版本更新"
techstack.title: "技术栈"
techstack.subtitle: "项目中使用的主要技术"
cta.title: "开源理念"
cta.subtitle: "相信好的软件应该是透明、可审计、可定制的。所有项目源代码均可在 GitHub 查看,欢迎参与贡献。"
cta.button: "在 GitHub 上探索"
projects.title: "所有项目"
projects.subtitle: "浏览和筛选所有开源软件项目"
projects.filter.all: "全部"
projects.filter.tech: "技术栈"
projects.filter.platform: "平台"
projects.filter.status: "状态"
projects.sort.updated: "最近更新"
projects.sort.stars: "Star 最多"
projects.sort.name: "名称排序"
projects.noResults: "没有匹配的项目"
projects.search: "搜索项目名称、描述或标签..."
detail.overview: "概览"
detail.features: "核心功能"
detail.screenshots: "截图预览"
detail.downloads: "下载安装"
detail.techstack: "技术栈"
detail.architecture: "系统架构"
detail.roadmap: "开发路线图"
detail.changelog: "更新日志"
detail.info: "项目信息"
detail.version: "当前版本"
detail.license: "开源协议"
detail.platforms: "支持平台"
detail.status: "开发状态"
detail.lastUpdate: "最后更新"
detail.repo: "GitHub 仓库"
detail.docs: "在线文档"
detail.release: "下载 Release"
detail.installGuide: "安装说明"
detail.install.windows: "Windows:双击安装包,按提示完成安装"
nav.search: '搜索项目...'
nav.theme: '切换主题'
nav.lang: 'English'
hero.title: '构建轻量、高效、开源的软件工具'
hero.subtitle: '涵盖桌面软件、移动应用、开发者工具、笔记系统、SSH 客户端、远程控制、文档组件库与全栈应用。'
hero.cta.projects: '查看所有项目'
hero.cta.github: '访问 GitHub'
hero.cta.download: '下载软件'
hero.cta.docs: '查看文档'
stats.projects: '开源项目'
stats.stars: 'GitHub Stars'
stats.techStack: '技术栈'
stats.platforms: '支持平台'
featured.title: '重点项目'
featured.subtitle: '正在积极开发和推荐使用的核心项目'
featured.viewAll: '查看全部项目'
categories.title: '项目分类'
categories.subtitle: '按类型浏览所有开源项目'
latest.title: '最新发布'
latest.subtitle: '最近的版本更新'
techstack.title: '技术栈'
techstack.subtitle: '项目中使用的主要技术'
cta.title: '开源理念'
cta.subtitle: '相信好的软件应该是透明、可审计、可定制的。所有项目源代码均可在 GitHub 查看,欢迎参与贡献。'
cta.button: '在 GitHub 上探索'
projects.title: '所有项目'
projects.subtitle: '浏览和筛选所有开源软件项目'
projects.filter.all: '全部'
projects.filter.tech: '技术栈'
projects.filter.platform: '平台'
projects.filter.status: '状态'
projects.sort.updated: '最近更新'
projects.sort.stars: 'Star 最多'
projects.sort.name: '名称排序'
projects.noResults: '没有匹配的项目'
projects.search: '搜索项目名称、描述或标签...'
detail.overview: '概览'
detail.features: '核心功能'
detail.screenshots: '截图预览'
detail.downloads: '下载安装'
detail.techstack: '技术栈'
detail.architecture: '系统架构'
detail.roadmap: '开发路线图'
detail.changelog: '更新日志'
detail.info: '项目信息'
detail.version: '当前版本'
detail.license: '开源协议'
detail.platforms: '支持平台'
detail.status: '开发状态'
detail.lastUpdate: '最后更新'
detail.repo: 'GitHub 仓库'
detail.docs: '在线文档'
detail.release: '下载 Release'
detail.installGuide: '安装说明'
detail.install.windows: 'Windows:双击安装包,按提示完成安装'
detail.install.macos: 'macOS:如提示"无法验证开发者",请前往 系统设置 → 隐私与安全性 → 仍要打开'
detail.install.linux: "Linuxchmod +x 后运行,或使用包管理器安装"
detail.install.android: "Android:下载 APK 文件,允许安装未知来源应用"
detail.roadmap.done: "已完成"
detail.roadmap.doing: "开发中"
detail.roadmap.planned: "计划中"
detail.screenshotPlaceholder: "截图预览区"
downloads.title: "下载中心"
downloads.subtitle: "下载所有开源软件的最新版本"
downloads.fileSize: "文件大小"
downloads.checksum: "SHA256 校验"
downloads.allReleases: "查看全部 Release"
downloads.installGuide: "安装说明"
downloads.trustNote: "此软件使用 Tauri 构建,安装包体积较小。部分版本可能未进行商业代码签名,因此 Windows 或 macOS 可能出现安全提示。所有源码均可在 GitHub 查看。"
docs.title: "文档中心"
docs.subtitle: "快速开始、使用指南和开发文档"
docs.quickstart: "快速开始"
docs.install: "安装指南"
docs.usage: "基础使用"
docs.advanced: "高级功能"
docs.config: "配置说明"
docs.faq: "常见问题"
docs.dev: "开发指南"
docs.deploy: "部署指南"
docs.api: "API 文档"
docs.contribute: "贡献指南"
docs.selectProject: "选择一个项目查看文档"
changelog.title: "更新日志"
changelog.subtitle: "所有项目的版本更新记录"
changelog.all: "全部项目"
roadmap.title: "开发路线图"
roadmap.subtitle: "所有项目的开发计划和进度"
roadmap.all: "全部项目"
about.title: "关于作者"
about.subtitle: "了解开发者和开源理念"
about.bio: "个人简介"
about.focus: "技术方向"
about.techStack: "常用技术栈"
about.links: "联系方式"
about.github: "GitHub 主页"
about.opensource: "开源理念"
contact.title: "反馈与联系"
contact.subtitle: "问题反馈、功能建议和社区讨论"
contact.issues: "提交问题"
contact.issues.desc: "在 GitHub Issues 中报告 Bug 或提交功能请求"
contact.discussions: "社区讨论"
contact.discussions.desc: "在 GitHub Discussions 中参与讨论"
contact.email: "邮件联系"
contact.email.desc: "通过邮件直接联系作者"
contact.security: "安全问题"
contact.security.desc: "发现安全漏洞请通过私密渠道报告"
footer.slogan: "构建轻量、高效、开源的软件工具"
footer.projects: "项目"
footer.resources: "资源"
footer.community: "社区"
footer.license: "开源协议"
footer.privacy: "隐私政策"
footer.security: "安全政策"
footer.copyright: "© 2026 ZUJ OL. All rights reserved."
common.viewAll: "查看全部"
common.learnMore: "了解更多"
common.download: "下载"
common.docs: "文档"
common.demo: "在线演示"
common.back: "返回"
common.stars: "Stars"
common.forks: "Forks"
common.version: "版本"
common.platform: "平台"
common.size: "大小"
common.arch: "架构"
detail.install.linux: 'Linuxchmod +x 后运行,或使用包管理器安装'
detail.install.android: 'Android:下载 APK 文件,允许安装未知来源应用'
detail.roadmap.done: '已完成'
detail.roadmap.doing: '开发中'
detail.roadmap.planned: '计划中'
detail.screenshotPlaceholder: '截图预览区'
downloads.title: '下载中心'
downloads.subtitle: '下载所有开源软件的最新版本'
downloads.fileSize: '文件大小'
downloads.checksum: 'SHA256 校验'
downloads.allReleases: '查看全部 Release'
downloads.installGuide: '安装说明'
downloads.trustNote: '此软件使用 Tauri 构建,安装包体积较小。部分版本可能未进行商业代码签名,因此 Windows 或 macOS 可能出现安全提示。所有源码均可在 GitHub 查看。'
docs.title: '文档中心'
docs.subtitle: '快速开始、使用指南和开发文档'
docs.quickstart: '快速开始'
docs.install: '安装指南'
docs.usage: '基础使用'
docs.advanced: '高级功能'
docs.config: '配置说明'
docs.faq: '常见问题'
docs.dev: '开发指南'
docs.deploy: '部署指南'
docs.api: 'API 文档'
docs.contribute: '贡献指南'
docs.selectProject: '选择一个项目查看文档'
changelog.title: '更新日志'
changelog.subtitle: '所有项目的版本更新记录'
changelog.all: '全部项目'
roadmap.title: '开发路线图'
roadmap.subtitle: '所有项目的开发计划和进度'
roadmap.all: '全部项目'
about.title: '关于作者'
about.subtitle: '了解开发者和开源理念'
about.bio: '个人简介'
about.focus: '技术方向'
about.techStack: '常用技术栈'
about.links: '联系方式'
about.github: 'GitHub 主页'
about.opensource: '开源理念'
contact.title: '反馈与联系'
contact.subtitle: '问题反馈、功能建议和社区讨论'
contact.issues: '提交问题'
contact.issues.desc: '在 GitHub Issues 中报告 Bug 或提交功能请求'
contact.discussions: '社区讨论'
contact.discussions.desc: '在 GitHub Discussions 中参与讨论'
contact.email: '邮件联系'
contact.email.desc: '通过邮件直接联系作者'
contact.security: '安全问题'
contact.security.desc: '发现安全漏洞请通过私密渠道报告'
footer.slogan: '构建轻量、高效、开源的软件工具'
footer.projects: '项目'
footer.resources: '资源'
footer.community: '社区'
footer.license: '开源协议'
footer.privacy: '隐私政策'
footer.security: '安全政策'
footer.copyright: '© 2026 ZUJ OL. All rights reserved.'
footer.viewAllProjects: '查看全部'
common.viewAll: '查看全部'
common.learnMore: '了解更多'
common.download: '下载'
common.docs: '文档'
common.demo: '在线演示'
common.back: '返回'
common.stars: 'Stars'
common.forks: 'Forks'
common.version: '版本'
common.platform: '平台'
common.size: '大小'
common.arch: '架构'
+5 -4
View File
@@ -10,10 +10,11 @@ import aboutRaw from './about.yaml?raw';
import zhRaw from './i18n/zh.yaml?raw';
import enRaw from './i18n/en.yaml?raw';
const projectFiles = import.meta.glob(
'./projects/*.yaml',
{ eager: true, query: '?raw', import: 'default' },
) as Record<string, string>;
const projectFiles = import.meta.glob('./projects/*.yaml', {
eager: true,
query: '?raw',
import: 'default',
}) as Record<string, string>;
const projects: Project[] = Object.values(projectFiles)
.map((raw) => yamlParse(raw) as Project)
+12 -12
View File
@@ -1,17 +1,17 @@
- id: "home"
- id: 'home'
label:
zh: "首页"
en: "Home"
hash: "#/"
zh: '首页'
en: 'Home'
hash: '#/'
- id: "projects"
- id: 'projects'
label:
zh: "项目"
en: "Projects"
hash: "#/projects"
zh: '项目'
en: 'Projects'
hash: '#/projects'
- id: "about"
- id: 'about'
label:
zh: "关于"
en: "About"
hash: "#/about"
zh: '关于'
en: 'About'
hash: '#/about'
+9 -9
View File
@@ -1,9 +1,9 @@
windows: "Windows"
macos: "macOS"
linux: "Linux"
android: "Android"
ios: "iOS"
web: "Web"
docker: "Docker"
npm: "NPM"
cli: "CLI"
windows: 'Windows'
macos: 'macOS'
linux: 'Linux'
android: 'Android'
ios: 'iOS'
web: 'Web'
docker: 'Docker'
npm: 'NPM'
cli: 'CLI'
+69 -68
View File
@@ -1,93 +1,94 @@
id: "billddesk"
name: "billddesk"
id: 'billddesk'
name: 'billddesk'
displayName:
zh: "BilldDesk"
en: "BilldDesk"
zh: 'BilldDesk'
en: 'BilldDesk'
slogan:
zh: "开源远程桌面控制方案"
en: "Open-source remote desktop control solution"
zh: '开源远程桌面控制方案'
en: 'Open-source remote desktop control solution'
description:
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."
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.'
type:
- "backend"
- "selfhosted"
status: "maintained"
- 'backend'
- 'selfhosted'
status: 'maintained'
platforms:
- "web"
- "docker"
- 'web'
- 'docker'
techStack:
- "WebRTC"
- "TypeScript"
- "Node.js"
- "Docker"
- "React"
- 'WebRTC'
- 'TypeScript'
- 'Node.js'
- 'Docker'
- 'React'
features:
zh:
- "WebRTC 远程控制"
- "浏览器访问"
- "文件传输"
- "剪贴板共享"
- "Docker 部署"
- "多用户管理"
- "连接加密"
- 'WebRTC 远程控制'
- '浏览器访问'
- '文件传输'
- '剪贴板共享'
- 'Docker 部署'
- '多用户管理'
- '连接加密'
en:
- "WebRTC remote control"
- "Browser access"
- "File transfer"
- "Clipboard sharing"
- "Docker deployment"
- "Multi-user management"
- "Encrypted connections"
- 'WebRTC remote control'
- 'Browser access'
- 'File transfer'
- 'Clipboard sharing'
- 'Docker deployment'
- 'Multi-user management'
- 'Encrypted connections'
tags:
- "Remote Desktop"
- "WebRTC"
- "Self-hosted"
icon: "Monitor"
repoUrl: "https://github.com/shenjianZ62/billddesk"
latestVersion: "v0.8.0"
releaseDate: "2026-02-14"
license: "Apache-2.0"
- 'Remote Desktop'
- 'WebRTC'
- 'Self-hosted'
icon: 'Monitor'
repoUrl: 'https://github.com/shenjianZ/billddesk'
docsUrl: 'https://github.com/shenjianZ/billddesk#readme'
latestVersion: 'v0.8.0'
releaseDate: '2026-02-14'
license: 'Apache-2.0'
stars: 45
forks: 9
language: "TypeScript"
lastUpdated: "2026-04-20"
language: 'TypeScript'
lastUpdated: '2026-04-20'
recommended: false
featured: false
order: 6
color: "#EF4444"
color: '#EF4444'
downloads:
- platform: "Docker"
arch: ""
url: "#"
size: "156 MB"
sha256: "abc123"
- platform: 'Docker'
arch: 'multi-arch'
url: 'https://github.com/shenjianZ/billddesk/releases/download/v0.8.0/docker-compose.yml'
size: '156 MB'
sha256: ''
roadmap:
done:
- "WebRTC 连接"
- "基础远程控制"
- "Docker 镜像"
- "用户认证"
- 'WebRTC 连接'
- '基础远程控制'
- 'Docker 镜像'
- '用户认证'
doing:
- "文件传输优化"
- "剪贴板同步"
- "多显示器支持"
- '文件传输优化'
- '剪贴板同步'
- '多显示器支持'
planned:
- "移动端客户端"
- "录制功能"
- "白板协作"
- '移动端客户端'
- '录制功能'
- '白板协作'
changelog:
- version: "v0.8.0"
date: "2026-02-14"
- version: 'v0.8.0'
date: '2026-02-14'
changes:
zh:
- "新增 Docker Compose 部署"
- "优化连接稳定性"
- "新增用户管理界面"
- '新增 Docker Compose 部署'
- '优化连接稳定性'
- '新增用户管理界面'
en:
- "Added Docker Compose deploy"
- "Improved connection stability"
- "Added user management UI"
- 'Added Docker Compose deploy'
- 'Improved connection stability'
- 'Added user management UI'
architecture:
zh: "浏览器客户端 → WebRTC → 信令服务器 (Node.js) → TURN/STUN → 被控端"
en: "Browser Client → WebRTC → Signaling Server (Node.js) → TURN/STUN → Controlled End"
zh: '浏览器客户端 → WebRTC → 信令服务器 (Node.js) → TURN/STUN → 被控端'
en: 'Browser Client → WebRTC → Signaling Server (Node.js) → TURN/STUN → Controlled End'
+75 -75
View File
@@ -1,100 +1,100 @@
id: "codex-manager"
name: "codex-manager"
id: 'codex-manager'
name: 'codex-manager'
displayName:
zh: "Codex-Manager"
en: "Codex-Manager"
zh: 'Codex-Manager'
en: 'Codex-Manager'
slogan:
zh: "本地账号池管理与 API 网关工具"
en: "Local account pool manager & API gateway tool"
zh: '本地账号池管理与 API 网关工具'
en: 'Local account pool manager & API gateway tool'
description:
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."
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.'
type:
- "desktop"
- "devtool"
status: "beta"
- 'desktop'
- 'devtool'
status: 'beta'
platforms:
- "windows"
- "macos"
- "linux"
- 'windows'
- 'macos'
techStack:
- "Tauri"
- "React"
- "Rust"
- "SQLite"
- "TypeScript"
- 'Tauri'
- 'React'
- 'Rust'
- 'SQLite'
- 'TypeScript'
features:
zh:
- "账号池管理"
- "密钥轮转"
- "健康检查"
- "用量统计"
- "限额告警"
- "代理配置"
- "导入导出"
- '账号池管理'
- '密钥轮转'
- '健康检查'
- '用量统计'
- '限额告警'
- '代理配置'
- '导入导出'
en:
- "Account pool management"
- "Key rotation"
- "Health checks"
- "Usage statistics"
- "Quota alerts"
- "Proxy config"
- "Import/export"
- 'Account pool management'
- 'Key rotation'
- 'Health checks'
- 'Usage statistics'
- 'Quota alerts'
- 'Proxy config'
- 'Import/export'
tags:
- "API"
- "Gateway"
- "Account Management"
- "Desktop"
icon: "KeyRound"
repoUrl: "https://github.com/shenjianZ62/codex-manager"
latestVersion: "v0.1.0-beta"
releaseDate: "2026-04-28"
license: "MIT"
- 'API'
- 'Gateway'
- 'Account Management'
- 'Desktop'
icon: 'KeyRound'
repoUrl: 'https://github.com/shenjianZ/codex-manager'
docsUrl: 'https://github.com/shenjianZ/codex-manager#readme'
latestVersion: 'v0.1.0-beta'
releaseDate: '2026-04-28'
license: 'MIT'
stars: 34
forks: 2
language: "Rust"
lastUpdated: "2026-05-05"
language: 'Rust'
lastUpdated: '2026-05-05'
recommended: false
featured: false
order: 8
color: "#EC4899"
color: '#EC4899'
downloads:
- platform: "Windows"
arch: "x64"
url: "#"
size: "18.7 MB"
sha256: "abc123"
- platform: "macOS"
arch: "Apple Silicon"
url: "#"
size: "15.2 MB"
sha256: "def456"
- platform: 'Windows'
arch: 'x64'
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'
sha256: ''
- platform: 'macOS'
arch: 'Apple Silicon'
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'
sha256: ''
roadmap:
done:
- "基础账号管理"
- "SQLite 存储"
- "API 密钥添加/删除"
- '基础账号管理'
- 'SQLite 存储'
- 'API 密钥添加/删除'
doing:
- "自动轮转策略"
- "健康检查"
- "用量仪表板"
- '自动轮转策略'
- '健康检查'
- '用量仪表板'
planned:
- "代理池集成"
- "团队协作"
- "Web Dashboard"
- "API 网关模式"
- '代理池集成'
- '团队协作'
- 'Web Dashboard'
- 'API 网关模式'
changelog:
- version: "v0.1.0-beta"
date: "2026-04-28"
- version: 'v0.1.0-beta'
date: '2026-04-28'
changes:
zh:
- "首个测试版本"
- "基础账号管理"
- "本地存储"
- '首个测试版本'
- '基础账号管理'
- '本地存储'
en:
- "First beta release"
- "Basic account management"
- "Local storage"
- 'First beta release'
- 'Basic account management'
- 'Local storage'
architecture:
zh: "React 前端 → Tauri Commands → Rust 核心层 → SQLite → 加密凭证存储"
en: "React Frontend → Tauri Commands → Rust Core → SQLite → Encrypted Credential Store"
zh: 'React 前端 → Tauri Commands → Rust 核心层 → SQLite → 加密凭证存储'
en: 'React Frontend → Tauri Commands → Rust Core → SQLite → Encrypted Credential Store'
+93 -92
View File
@@ -1,120 +1,121 @@
id: "devicedeck"
name: "devicedeck"
id: 'devicedeck'
name: 'devicedeck'
displayName:
zh: "DeviceDeck"
en: "DeviceDeck"
zh: 'DeviceDeck'
en: 'DeviceDeck'
slogan:
zh: "Android 投屏与设备调试工作台"
en: "Android screen casting & device debugging workstation"
zh: 'Android 投屏与设备调试工作台'
en: 'Android screen casting & device debugging workstation'
description:
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."
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.'
type:
- "desktop"
- "devtool"
status: "active"
- 'desktop'
- 'devtool'
status: 'active'
platforms:
- "windows"
- "macos"
- "linux"
- 'windows'
- 'macos'
- 'linux'
techStack:
- "Tauri"
- "React"
- "Rust"
- "ADB"
- "scrcpy"
- "TypeScript"
- 'Tauri'
- 'React'
- 'Rust'
- 'ADB'
- 'scrcpy'
- 'TypeScript'
features:
zh:
- "scrcpy 投屏"
- "ADB 调试"
- "文件传输"
- "截图录制"
- "多设备管理"
- "设备信息查看"
- "日志查看"
- "参数配置"
- 'scrcpy 投屏'
- 'ADB 调试'
- '文件传输'
- '截图录制'
- '多设备管理'
- '设备信息查看'
- '日志查看'
- '参数配置'
en:
- "scrcpy screen casting"
- "ADB debugging"
- "File transfer"
- "Screenshot & recording"
- "Multi-device management"
- "Device info viewer"
- "Log viewer"
- "Parameter config"
- 'scrcpy screen casting'
- 'ADB debugging'
- 'File transfer'
- 'Screenshot & recording'
- 'Multi-device management'
- 'Device info viewer'
- 'Log viewer'
- 'Parameter config'
tags:
- "Android"
- "ADB"
- "Scrcpy"
- "Debug"
icon: "Smartphone"
repoUrl: "https://github.com/shenjianZ62/devicedeck"
latestVersion: "v0.3.0"
releaseDate: "2026-04-20"
license: "MIT"
- 'Android'
- 'ADB'
- 'Scrcpy'
- 'Debug'
icon: 'Smartphone'
repoUrl: 'https://github.com/shenjianZ/devicedeck'
docsUrl: 'https://github.com/shenjianZ/devicedeck#readme'
latestVersion: 'v0.3.0'
releaseDate: '2026-04-20'
license: 'MIT'
stars: 72
forks: 6
language: "Rust"
lastUpdated: "2026-05-12"
language: 'Rust'
lastUpdated: '2026-05-12'
recommended: true
featured: true
order: 4
color: "#8B5CF6"
color: '#8B5CF6'
downloads:
- platform: "Windows"
arch: "x64"
url: "#"
size: "28.5 MB"
sha256: "abc123"
- platform: "macOS"
arch: "Apple Silicon"
url: "#"
size: "24.1 MB"
sha256: "def456"
- platform: "Linux"
arch: "x64"
url: "#"
size: "25.8 MB"
sha256: "jkl012"
- platform: 'Windows'
arch: 'x64'
url: 'https://github.com/shenjianZ/devicedeck/releases/download/v0.3.0/DeviceDeck_0.3.0_x64-setup.exe'
size: '28.5 MB'
sha256: ''
- platform: 'macOS'
arch: 'Apple Silicon'
url: 'https://github.com/shenjianZ/devicedeck/releases/download/v0.3.0/DeviceDeck_0.3.0_aarch64.dmg'
size: '24.1 MB'
sha256: ''
- platform: 'Linux'
arch: 'x64'
url: 'https://github.com/shenjianZ/devicedeck/releases/download/v0.3.0/DeviceDeck_0.3.0_amd64.AppImage'
size: '25.8 MB'
sha256: ''
roadmap:
done:
- "scrcpy 投屏"
- "ADB 命令执行"
- "文件拖放传输"
- "设备列表"
- "截图功能"
- 'scrcpy 投屏'
- 'ADB 命令执行'
- '文件拖放传输'
- '设备列表'
- '截图功能'
doing:
- "多设备同时投屏"
- "脚本录制回放"
- "设备配置模板"
- '多设备同时投屏'
- '脚本录制回放'
- '设备配置模板'
planned:
- "iOS 设备支持"
- "无线调试"
- "自动化测试集成"
- 'iOS 设备支持'
- '无线调试'
- '自动化测试集成'
changelog:
- version: "v0.3.0"
date: "2026-04-20"
- version: 'v0.3.0'
date: '2026-04-20'
changes:
zh:
- "新增文件拖放传输"
- "优化投屏延迟"
- "新增设备信息面板"
- '新增文件拖放传输'
- '优化投屏延迟'
- '新增设备信息面板'
en:
- "Added drag-drop file transfer"
- "Reduced casting latency"
- "Added device info panel"
- version: "v0.2.0"
date: "2026-03-05"
- 'Added drag-drop file transfer'
- 'Reduced casting latency'
- 'Added device info panel'
- version: 'v0.2.0'
date: '2026-03-05'
changes:
zh:
- "新增截图功能"
- "修复 Windows 投屏黑屏"
- "新增快捷键支持"
- '新增截图功能'
- '修复 Windows 投屏黑屏'
- '新增快捷键支持'
en:
- "Added screenshot feature"
- "Fixed Windows black screen"
- "Added keyboard shortcuts"
- 'Added screenshot feature'
- 'Fixed Windows black screen'
- 'Added keyboard shortcuts'
architecture:
zh: "Tauri 桌面前端 → Rust 核心层 → ADB/scrcpy 进程管理 → Android 设备"
en: "Tauri Desktop Frontend → Rust Core → ADB/scrcpy Process Manager → Android Device"
zh: 'Tauri 桌面前端 → Rust 核心层 → ADB/scrcpy 进程管理 → Android 设备'
en: 'Tauri Desktop Frontend → Rust Core → ADB/scrcpy Process Manager → Android Device'
+68 -67
View File
@@ -1,92 +1,93 @@
id: "news-classifier"
name: "news-classifier"
id: 'news-classifier'
name: 'news-classifier'
displayName:
zh: "news-classifier"
en: "news-classifier"
zh: 'News Classifier'
en: 'News Classifier'
slogan:
zh: "基于机器学习的新闻自动分类系统"
en: "ML-powered automatic news classification system"
zh: '基于机器学习的新闻自动分类系统'
en: 'ML-powered automatic news classification system'
description:
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."
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.'
type:
- "ai"
- "backend"
status: "experimental"
- 'ai'
- 'backend'
status: 'experimental'
platforms:
- "docker"
- "cli"
- 'docker'
- 'cli'
techStack:
- "Python"
- "TensorFlow"
- "FastAPI"
- "Docker"
- "Redis"
- 'Python'
- 'TensorFlow'
- 'FastAPI'
- 'Docker'
- 'Redis'
features:
zh:
- "新闻分类"
- "多类别支持"
- "REST API"
- "批量处理"
- "模型微调"
- "实时预测"
- "缓存优化"
- '新闻分类'
- '多类别支持'
- 'REST API'
- '批量处理'
- '模型微调'
- '实时预测'
- '缓存优化'
en:
- "News classification"
- "Multi-category support"
- "REST API"
- "Batch processing"
- "Model fine-tuning"
- "Real-time prediction"
- "Cache optimization"
- 'News classification'
- 'Multi-category support'
- 'REST API'
- 'Batch processing'
- 'Model fine-tuning'
- 'Real-time prediction'
- 'Cache optimization'
tags:
- "ML"
- "NLP"
- "Classification"
- "API"
icon: "Brain"
repoUrl: "https://github.com/shenjianZ62/news-classifier"
latestVersion: "v0.1.0"
releaseDate: "2025-12-01"
license: "MIT"
- 'ML'
- 'NLP'
- 'Classification'
- 'API'
icon: 'Brain'
repoUrl: 'https://github.com/shenjianZ/news-classifier'
docsUrl: 'https://github.com/shenjianZ/news-classifier#readme'
latestVersion: 'v0.1.0'
releaseDate: '2025-12-01'
license: 'MIT'
stars: 23
forks: 3
language: "Python"
lastUpdated: "2026-03-15"
language: 'Python'
lastUpdated: '2026-03-15'
recommended: false
featured: false
order: 7
color: "#F97316"
color: '#F97316'
downloads:
- platform: "Docker"
arch: ""
url: "#"
size: "890 MB"
sha256: "abc123"
- platform: 'Docker'
arch: 'multi-arch'
url: 'https://github.com/shenjianZ/news-classifier/releases/download/v0.1.0/docker-compose.yml'
size: '890 MB'
sha256: ''
roadmap:
done:
- "基础分类模型"
- "FastAPI 接口"
- "Docker 镜像"
- '基础分类模型'
- 'FastAPI 接口'
- 'Docker 镜像'
doing:
- "模型精度优化"
- "新增类别"
- '模型精度优化'
- '新增类别'
planned:
- "中文新闻支持"
- "可视化训练面板"
- "多模型对比"
- '中文新闻支持'
- '可视化训练面板'
- '多模型对比'
changelog:
- version: "v0.1.0"
date: "2025-12-01"
- version: 'v0.1.0'
date: '2025-12-01'
changes:
zh:
- "首个版本"
- "基础分类 API"
- "预训练模型"
- '首个版本'
- '基础分类 API'
- '预训练模型'
en:
- "First release"
- "Basic classification API"
- "Pre-trained model"
- 'First release'
- 'Basic classification API'
- 'Pre-trained model'
architecture:
zh: "REST API (FastAPI) → 分类服务 → TensorFlow 模型 → Redis 缓存 → 数据源"
en: "REST API (FastAPI) → Classification Service → TensorFlow Model → Redis Cache → Data Source"
zh: 'REST API (FastAPI) → 分类服务 → TensorFlow 模型 → Redis 缓存 → 数据源'
en: 'REST API (FastAPI) → Classification Service → TensorFlow Model → Redis Cache → Data Source'
+111 -110
View File
@@ -1,138 +1,139 @@
id: "quantanote"
name: "QuantaNote"
id: 'quantanote'
name: 'QuantaNote'
displayName:
zh: "QuantaNote"
en: "QuantaNote"
zh: 'QuantaNote'
en: 'QuantaNote'
slogan:
zh: "本地优先的跨平台桌面笔记与知识管理工具"
en: "Local-first cross-platform desktop note & knowledge management tool"
zh: '本地优先的跨平台桌面笔记与知识管理工具'
en: 'Local-first cross-platform desktop note & knowledge management tool'
description:
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."
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.'
type:
- "desktop"
- "devtool"
status: "active"
- 'desktop'
- 'devtool'
status: 'active'
platforms:
- "windows"
- "macos"
- "linux"
- 'windows'
- 'macos'
- 'linux'
techStack:
- "Tauri 2"
- "Rust"
- "React"
- "TypeScript"
- "SQLite"
- "TailwindCSS"
- 'Tauri 2'
- 'Rust'
- 'React'
- 'TypeScript'
- 'SQLite'
- 'TailwindCSS'
features:
zh:
- "Markdown 编辑"
- "本地 SQLite 存储"
- "全文搜索"
- "标签管理"
- "附件预览"
- "版本历史"
- "导入导出"
- "自动备份"
- "主题切换"
- "系统托盘"
- "云同步(开发中)"
- 'Markdown 编辑'
- '本地 SQLite 存储'
- '全文搜索'
- '标签管理'
- '附件预览'
- '版本历史'
- '导入导出'
- '自动备份'
- '主题切换'
- '系统托盘'
- '云同步(开发中)'
en:
- "Markdown editing"
- "Local SQLite storage"
- "Full-text search"
- "Tag management"
- "Attachment preview"
- "Version history"
- "Import/export"
- "Auto backup"
- "Theme switching"
- "System tray"
- "Cloud sync (WIP)"
- 'Markdown editing'
- 'Local SQLite storage'
- 'Full-text search'
- 'Tag management'
- 'Attachment preview'
- 'Version history'
- 'Import/export'
- 'Auto backup'
- 'Theme switching'
- 'System tray'
- 'Cloud sync (WIP)'
tags:
- "Markdown"
- "Notes"
- "Knowledge Management"
- "Desktop"
icon: "NotebookPen"
repoUrl: "https://github.com/shenjianZ62/quantanote"
latestVersion: "v0.2.0"
releaseDate: "2026-04-15"
license: "MIT"
- 'Markdown'
- 'Notes'
- 'Knowledge Management'
- 'Desktop'
icon: 'NotebookPen'
repoUrl: 'https://github.com/shenjianZ/quantanote'
docsUrl: 'https://github.com/shenjianZ/quantanote#readme'
latestVersion: 'v0.2.0'
releaseDate: '2026-04-15'
license: 'MIT'
stars: 128
forks: 12
language: "Rust"
lastUpdated: "2026-05-10"
language: 'Rust'
lastUpdated: '2026-05-10'
recommended: true
featured: true
order: 1
color: "#3B82F6"
color: '#3B82F6'
downloads:
- platform: "Windows"
arch: "x64"
url: "#"
size: "22.6 MB"
sha256: "abc123"
- platform: "macOS"
arch: "Apple Silicon"
url: "#"
size: "18.3 MB"
sha256: "def456"
- platform: "macOS"
arch: "Intel"
url: "#"
size: "19.1 MB"
sha256: "ghi789"
- platform: "Linux"
arch: "x64"
url: "#"
size: "20.2 MB"
sha256: "jkl012"
- platform: 'Windows'
arch: 'x64'
url: 'https://github.com/shenjianZ/quantanote/releases/download/v0.2.0/QuantaNote_0.2.0_x64-setup.exe'
size: '22.6 MB'
sha256: ''
- platform: 'macOS'
arch: 'Apple Silicon'
url: 'https://github.com/shenjianZ/quantanote/releases/download/v0.2.0/QuantaNote_0.2.0_aarch64.dmg'
size: '18.3 MB'
sha256: ''
- platform: 'macOS'
arch: 'Intel'
url: 'https://github.com/shenjianZ/quantanote/releases/download/v0.2.0/QuantaNote_0.2.0_x64.dmg'
size: '19.1 MB'
sha256: ''
- platform: 'Linux'
arch: 'x64'
url: 'https://github.com/shenjianZ/quantanote/releases/download/v0.2.0/QuantaNote_0.2.0_amd64.AppImage'
size: '20.2 MB'
sha256: ''
roadmap:
done:
- "基础笔记管理"
- "Markdown 编辑"
- "本地存储"
- "标签系统"
- "全文搜索"
- '基础笔记管理'
- 'Markdown 编辑'
- '本地存储'
- '标签系统'
- '全文搜索'
doing:
- "云同步"
- "多端同步"
- "账号管理"
- '云同步'
- '多端同步'
- '账号管理'
planned:
- "插件系统"
- "MCP 接入"
- "移动端查看"
- "AI 辅助写作"
- '插件系统'
- 'MCP 接入'
- '移动端查看'
- 'AI 辅助写作'
changelog:
- version: "v0.2.0"
date: "2026-04-15"
- version: 'v0.2.0'
date: '2026-04-15'
changes:
zh:
- "新增账号管理模块"
- "修复 Token 刷新竞态"
- "优化同步状态显示"
- "新增附件预览支持"
- '新增账号管理模块'
- '修复 Token 刷新竞态'
- '优化同步状态显示'
- '新增附件预览支持'
en:
- "Added account management"
- "Fixed token refresh race condition"
- "Improved sync status display"
- "Added attachment preview"
- version: "v0.1.0"
date: "2026-02-20"
- 'Added account management'
- 'Fixed token refresh race condition'
- 'Improved sync status display'
- 'Added attachment preview'
- version: 'v0.1.0'
date: '2026-02-20'
changes:
zh:
- "首个公开版本"
- "基础笔记 CRUD"
- "Markdown 编辑器"
- "本地 SQLite 存储"
- "标签管理"
- '首个公开版本'
- '基础笔记 CRUD'
- 'Markdown 编辑器'
- '本地 SQLite 存储'
- '标签管理'
en:
- "First public release"
- "Basic note CRUD"
- "Markdown editor"
- "Local SQLite storage"
- "Tag management"
- 'First public release'
- 'Basic note CRUD'
- 'Markdown editor'
- 'Local SQLite storage'
- 'Tag management'
architecture:
zh: "前端 (React + TypeScript) → Tauri Commands → Rust 核心层 → SQLite 数据库 → 本地文件存储"
en: "Frontend (React + TypeScript) → Tauri Commands → Rust Core → SQLite Database → Local File Storage"
zh: '前端 (React + TypeScript) → Tauri Commands → Rust 核心层 → SQLite 数据库 → 本地文件存储'
en: 'Frontend (React + TypeScript) → Tauri Commands → Rust Core → SQLite Database → Local File Storage'
+78 -78
View File
@@ -1,106 +1,106 @@
id: "react-docs-ui"
name: "react-docs-ui"
id: 'react-docs-ui'
name: 'react-docs-ui'
displayName:
zh: "react-docs-ui"
en: "react-docs-ui"
zh: 'React Docs UI'
en: 'React Docs UI'
slogan:
zh: "基于 React 的文档组件库与站点生成器"
en: "React-based documentation component library & site generator"
zh: '基于 React 的文档组件库与站点生成器'
en: 'React-based documentation component library & site generator'
description:
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."
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.'
type:
- "library"
- "devtool"
status: "active"
- 'library'
- 'devtool'
status: 'active'
platforms:
- "web"
- "npm"
- 'web'
- 'npm'
techStack:
- "React"
- "TypeScript"
- "MDX"
- "TailwindCSS"
- "Vite"
- 'React'
- 'TypeScript'
- 'MDX'
- 'TailwindCSS'
- 'Vite'
features:
zh:
- "MDX 支持"
- "全文搜索"
- "版本切换"
- "主题定制"
- "响应式布局"
- "API 文档生成"
- "代码高亮"
- "国际化"
- 'MDX 支持'
- '全文搜索'
- '版本切换'
- '主题定制'
- '响应式布局'
- 'API 文档生成'
- '代码高亮'
- '国际化'
en:
- "MDX support"
- "Full-text search"
- "Version switching"
- "Theme customization"
- "Responsive layout"
- "API doc generation"
- "Code highlighting"
- "i18n"
- 'MDX support'
- 'Full-text search'
- 'Version switching'
- 'Theme customization'
- 'Responsive layout'
- 'API doc generation'
- 'Code highlighting'
- 'i18n'
tags:
- "Documentation"
- "React"
- "NPM"
- "MDX"
icon: "BookOpen"
repoUrl: "https://github.com/shenjianZ62/react-docs-ui"
docsUrl: "#"
npmUrl: "https://www.npmjs.com/package/react-docs-ui"
latestVersion: "v0.5.2"
releaseDate: "2026-05-10"
license: "MIT"
- 'Documentation'
- 'React'
- 'NPM'
- 'MDX'
icon: 'BookOpen'
repoUrl: 'https://github.com/shenjianZ/react-docs-ui'
docsUrl: 'https://github.com/shenjianZ/react-docs-ui#readme'
npmUrl: 'https://www.npmjs.com/package/react-docs-ui'
latestVersion: 'v0.5.2'
releaseDate: '2026-05-10'
license: 'MIT'
stars: 203
forks: 24
language: "TypeScript"
lastUpdated: "2026-05-18"
language: 'TypeScript'
lastUpdated: '2026-05-18'
recommended: true
featured: true
order: 5
color: "#06B6D4"
color: '#06B6D4'
downloads: []
roadmap:
done:
- "基础组件库"
- "MDX 渲染"
- "侧边栏导航"
- "搜索功能"
- "主题系统"
- '基础组件库'
- 'MDX 渲染'
- '侧边栏导航'
- '搜索功能'
- '主题系统'
doing:
- "API 文档自动生成"
- "版本切换"
- "性能优化"
- 'API 文档自动生成'
- '版本切换'
- '性能优化'
planned:
- "插件系统"
- "评论集成"
- "多语言路由"
- "CLI 工具"
- '插件系统'
- '评论集成'
- '多语言路由'
- 'CLI 工具'
changelog:
- version: "v0.5.2"
date: "2026-05-10"
- version: 'v0.5.2'
date: '2026-05-10'
changes:
zh:
- "修复搜索索引构建错误"
- "新增代码块复制按钮"
- "优化移动端导航"
- '修复搜索索引构建错误'
- '新增代码块复制按钮'
- '优化移动端导航'
en:
- "Fixed search index build error"
- "Added code copy button"
- "Improved mobile navigation"
- version: "v0.5.0"
date: "2026-04-01"
- 'Fixed search index build error'
- 'Added code copy button'
- 'Improved mobile navigation'
- version: 'v0.5.0'
date: '2026-04-01'
changes:
zh:
- "新增主题定制"
- "支持 MDX 嵌入组件"
- "新增面包屑导航"
- '新增主题定制'
- '支持 MDX 嵌入组件'
- '新增面包屑导航'
en:
- "Added theme customization"
- "MDX embedded components"
- "Breadcrumb navigation"
- 'Added theme customization'
- 'MDX embedded components'
- 'Breadcrumb navigation'
architecture:
zh: "MDX 源文件 → Vite 构建 → React 组件渲染 → 静态文档站点"
en: "MDX Sources → Vite Build → React Component Rendering → Static Documentation Site"
zh: 'MDX 源文件 → Vite 构建 → React 组件渲染 → 静态文档站点'
en: 'MDX Sources → Vite Build → React Component Rendering → Static Documentation Site'
+99 -98
View File
@@ -1,126 +1,127 @@
id: "ssh-terminal"
name: "ssh-terminal"
id: 'ssh-terminal'
name: 'ssh-terminal'
displayName:
zh: "ssh-terminal"
en: "ssh-terminal"
zh: 'SSH Terminal'
en: 'SSH Terminal'
slogan:
zh: "轻量级跨平台 SSH 客户端与终端工具"
en: "Lightweight cross-platform SSH client & terminal tool"
zh: '轻量级跨平台 SSH 客户端与终端工具'
en: 'Lightweight cross-platform SSH client & terminal tool'
description:
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."
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.'
type:
- "desktop"
- "devtool"
status: "active"
- 'desktop'
- 'devtool'
status: 'active'
platforms:
- "windows"
- "macos"
- "linux"
- 'windows'
- 'macos'
- 'linux'
techStack:
- "Tauri"
- "React"
- "Rust"
- "xterm.js"
- "TypeScript"
- 'Tauri'
- 'React'
- 'Rust'
- 'xterm.js'
- 'TypeScript'
features:
zh:
- "SSH 连接管理"
- "多会话终端"
- "内置 SFTP"
- "轻量启动"
- "低内存占用"
- "跨平台桌面"
- "自定义快捷键"
- "终端录制"
- "主题定制"
- 'SSH 连接管理'
- '多会话终端'
- '内置 SFTP'
- '轻量启动'
- '低内存占用'
- '跨平台桌面'
- '自定义快捷键'
- '终端录制'
- '主题定制'
en:
- "SSH connection management"
- "Multi-session terminal"
- "Built-in SFTP"
- "Lightweight startup"
- "Low memory usage"
- "Cross-platform desktop"
- "Custom shortcuts"
- "Terminal recording"
- "Theme customization"
- 'SSH connection management'
- 'Multi-session terminal'
- 'Built-in SFTP'
- 'Lightweight startup'
- 'Low memory usage'
- 'Cross-platform desktop'
- 'Custom shortcuts'
- 'Terminal recording'
- 'Theme customization'
tags:
- "SSH"
- "Terminal"
- "SFTP"
- "DevOps"
icon: "Terminal"
repoUrl: "https://github.com/shenjianZ62/ssh-terminal"
latestVersion: "v0.1.5"
releaseDate: "2026-03-28"
license: "MIT"
- 'SSH'
- 'Terminal'
- 'SFTP'
- 'DevOps'
icon: 'Terminal'
repoUrl: 'https://github.com/shenjianZ/ssh-terminal'
docsUrl: 'https://github.com/shenjianZ/ssh-terminal#readme'
latestVersion: 'v0.1.5'
releaseDate: '2026-03-28'
license: 'MIT'
stars: 89
forks: 8
language: "Rust"
lastUpdated: "2026-05-08"
language: 'Rust'
lastUpdated: '2026-05-08'
recommended: true
featured: true
order: 2
color: "#10B981"
color: '#10B981'
downloads:
- platform: "Windows"
arch: "x64"
url: "#"
size: "15.2 MB"
sha256: "abc123"
- platform: "macOS"
arch: "Apple Silicon"
url: "#"
size: "12.8 MB"
sha256: "def456"
- platform: "macOS"
arch: "Intel"
url: "#"
size: "13.4 MB"
sha256: "ghi789"
- platform: "Linux"
arch: "x64"
url: "#"
size: "14.1 MB"
sha256: "jkl012"
- platform: 'Windows'
arch: 'x64'
url: 'https://github.com/shenjianZ/ssh-terminal/releases/download/v0.1.5/ssh-terminal_0.1.5_x64-setup.exe'
size: '15.2 MB'
sha256: ''
- platform: 'macOS'
arch: 'Apple Silicon'
url: 'https://github.com/shenjianZ/ssh-terminal/releases/download/v0.1.5/ssh-terminal_0.1.5_aarch64.dmg'
size: '12.8 MB'
sha256: ''
- platform: 'macOS'
arch: 'Intel'
url: 'https://github.com/shenjianZ/ssh-terminal/releases/download/v0.1.5/ssh-terminal_0.1.5_x64.dmg'
size: '13.4 MB'
sha256: ''
- platform: 'Linux'
arch: 'x64'
url: 'https://github.com/shenjianZ/ssh-terminal/releases/download/v0.1.5/ssh-terminal_0.1.5_amd64.AppImage'
size: '14.1 MB'
sha256: ''
roadmap:
done:
- "SSH 连接"
- "多会话"
- "SFTP 浏览器"
- "快捷键"
- "主题切换"
- 'SSH 连接'
- '多会话'
- 'SFTP 浏览器'
- '快捷键'
- '主题切换'
doing:
- "终端录制回放"
- "连接云同步"
- "AI 命令建议"
- '终端录制回放'
- '连接云同步'
- 'AI 命令建议'
planned:
- "端口转发 UI"
- "Snippet 库"
- "RDP/VNC 支持"
- '端口转发 UI'
- 'Snippet 库'
- 'RDP/VNC 支持'
changelog:
- version: "v0.1.5"
date: "2026-03-28"
- version: 'v0.1.5'
date: '2026-03-28'
changes:
zh:
- "新增 SFTP 文件浏览器"
- "优化终端渲染性能"
- "修复大文件传输中断"
- '新增 SFTP 文件浏览器'
- '优化终端渲染性能'
- '修复大文件传输中断'
en:
- "Added SFTP file browser"
- "Improved terminal rendering"
- "Fixed large file transfer interruption"
- version: "v0.1.0"
date: "2026-01-10"
- 'Added SFTP file browser'
- 'Improved terminal rendering'
- 'Fixed large file transfer interruption'
- version: 'v0.1.0'
date: '2026-01-10'
changes:
zh:
- "首个版本"
- "基础 SSH 连接"
- "多会话标签页"
- '首个版本'
- '基础 SSH 连接'
- '多会话标签页'
en:
- "First release"
- "Basic SSH connection"
- "Multi-session tabs"
- 'First release'
- 'Basic SSH connection'
- 'Multi-session tabs'
architecture:
zh: "前端 (React + xterm.js) → Tauri Commands → Rust SSH 层 (russh) → 远程服务器"
en: "Frontend (React + xterm.js) → Tauri Commands → Rust SSH Layer (russh) → Remote Server"
zh: '前端 (React + xterm.js) → Tauri Commands → Rust SSH 层 (russh) → 远程服务器'
en: 'Frontend (React + xterm.js) → Tauri Commands → Rust SSH Layer (russh) → Remote Server'
+74 -74
View File
@@ -1,99 +1,99 @@
id: "streetmoment"
name: "streetmoment"
id: 'streetmoment'
name: 'streetmoment'
displayName:
zh: "市井拾光"
en: "StreetMoment"
zh: '市井拾光'
en: 'StreetMoment'
slogan:
zh: "记录城市生活中的美好瞬间"
en: "Capture beautiful moments in city life"
zh: '记录城市生活中的美好瞬间'
en: 'Capture beautiful moments in city life'
description:
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."
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.'
type:
- "mobile"
status: "active"
- 'mobile'
status: 'active'
platforms:
- "android"
- "ios"
- 'android'
techStack:
- "React Native"
- "Expo"
- "TypeScript"
- "MapLibre"
- "Node.js"
- "PostgreSQL"
- 'React Native'
- 'Expo'
- 'TypeScript'
- 'MapLibre'
- 'Node.js'
- 'PostgreSQL'
features:
zh:
- "地图探索"
- "地点发现"
- "分类浏览"
- "拍照记录"
- "社区分享"
- "个人主页"
- "收藏管理"
- "搜索筛选"
- '地图探索'
- '地点发现'
- '分类浏览'
- '拍照记录'
- '社区分享'
- '个人主页'
- '收藏管理'
- '搜索筛选'
en:
- "Map exploration"
- "Place discovery"
- "Category browsing"
- "Photo recording"
- "Community sharing"
- "Personal profile"
- "Favorites"
- "Search & filter"
- 'Map exploration'
- 'Place discovery'
- 'Category browsing'
- 'Photo recording'
- 'Community sharing'
- 'Personal profile'
- 'Favorites'
- 'Search & filter'
tags:
- "Lifestyle"
- "Map"
- "Social"
- "Mobile"
icon: "MapPin"
repoUrl: "https://github.com/shenjianZ62/streetmoment"
latestVersion: "v1.0.0"
releaseDate: "2026-05-01"
license: "MIT"
- 'Lifestyle'
- 'Map'
- 'Social'
- 'Mobile'
icon: 'MapPin'
repoUrl: 'https://github.com/shenjianZ/streetmoment'
docsUrl: 'https://github.com/shenjianZ/streetmoment#readme'
latestVersion: 'v1.0.0'
releaseDate: '2026-05-01'
license: 'MIT'
stars: 56
forks: 5
language: "TypeScript"
lastUpdated: "2026-05-15"
language: 'TypeScript'
lastUpdated: '2026-05-15'
recommended: false
featured: true
order: 3
color: "#F59E0B"
color: '#F59E0B'
downloads:
- platform: "Android"
arch: "arm64"
url: "#"
size: "32.4 MB"
sha256: "abc123"
- platform: 'Android'
arch: 'arm64'
url: 'https://github.com/shenjianZ/streetmoment/releases/download/v1.0.0/streetmoment-1.0.0-arm64.apk'
size: '32.4 MB'
sha256: ''
roadmap:
done:
- "地图浏览"
- "地点标记"
- "分类系统"
- "用户注册"
- "拍照上传"
- '地图浏览'
- '地点标记'
- '分类系统'
- '用户注册'
- '拍照上传'
doing:
- "评论互动"
- "消息通知"
- "个性化推荐"
- '评论互动'
- '消息通知'
- '个性化推荐'
planned:
- "iOS 版发布"
- "小程序版本"
- "AR 探索模式"
- 'iOS 版发布'
- '小程序版本'
- 'AR 探索模式'
changelog:
- version: "v1.0.0"
date: "2026-05-01"
- version: 'v1.0.0'
date: '2026-05-01'
changes:
zh:
- "正式发布"
- "地图浏览优化"
- "新增分类筛选"
- "修复定位偏差"
- '正式发布'
- '地图浏览优化'
- '新增分类筛选'
- '修复定位偏差'
en:
- "Official release"
- "Map browsing optimization"
- "Added category filter"
- "Fixed location offset"
- 'Official release'
- 'Map browsing optimization'
- 'Added category filter'
- 'Fixed location offset'
architecture:
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"
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'
+15 -15
View File
@@ -1,29 +1,29 @@
active:
label:
zh: "活跃开发"
en: "Active"
color: "#b0b0b0"
zh: '活跃开发'
en: 'Active'
color: '#22C55E'
maintained:
label:
zh: "维护中"
en: "Maintained"
color: "#909090"
zh: '维护中'
en: 'Maintained'
color: '#3B82F6'
beta:
label:
zh: "测试版"
en: "Beta"
color: "#707070"
zh: '测试版'
en: 'Beta'
color: '#F59E0B'
experimental:
label:
zh: "实验性"
en: "Experimental"
color: "#555555"
zh: '实验性'
en: 'Experimental'
color: '#A855F7'
archived:
label:
zh: "已归档"
en: "Archived"
color: "#404040"
zh: '已归档'
en: 'Archived'
color: '#6B7280'
+33 -19
View File
@@ -3,7 +3,7 @@ import { siteData } from '../data/siteData';
import { useI18n } from './useI18n';
export function useProjectFilters() {
const { bi } = useI18n();
const { bi, lang } = useI18n();
const projects = siteData.projects;
const [search, setSearch] = useState('');
@@ -12,40 +12,54 @@ export function useProjectFilters() {
const [status, setStatus] = useState('');
const [sort, setSort] = useState('updated');
const allTech = useMemo(() => [...new Set(projects.flatMap(p => p.techStack))].sort(), [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 allTech = useMemo(
() => [...new Set(projects.flatMap((p) => p.techStack))].sort(),
[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(() => {
let result = [...projects];
if (search) {
const q = search.toLowerCase();
result = result.filter(p =>
p.name.toLowerCase().includes(q) ||
bi(p.slogan).toLowerCase().includes(q) ||
p.tags.some(tag => tag.toLowerCase().includes(q)) ||
p.techStack.some(ts => ts.toLowerCase().includes(q))
result = result.filter(
(p) =>
p.name.toLowerCase().includes(q) ||
bi(p.slogan).toLowerCase().includes(q) ||
p.tags.some((tag) => tag.toLowerCase().includes(q)) ||
p.techStack.some((ts) => ts.toLowerCase().includes(q))
);
}
if (tech) result = result.filter(p => p.techStack.includes(tech));
if (platform) result = result.filter(p => p.platforms.includes(platform));
if (status) result = result.filter(p => p.status === status);
if (tech) result = result.filter((p) => p.techStack.includes(tech));
if (platform) result = result.filter((p) => p.platforms.includes(platform));
if (status) result = result.filter((p) => p.status === status);
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 result.sort((a, b) => b.lastUpdated.localeCompare(a.lastUpdated));
return result;
}, [projects, search, tech, platform, status, sort, bi]);
}, [projects, search, tech, platform, status, sort, lang, bi]);
return {
search, setSearch,
tech, setTech,
platform, setPlatform,
status, setStatus,
sort, setSort,
allTech, allPlatforms, allStatuses,
search,
setSearch,
tech,
setTech,
platform,
setPlatform,
status,
setStatus,
sort,
setSort,
allTech,
allPlatforms,
allStatuses,
filteredProjects,
};
}
+8 -8
View File
@@ -1,9 +1,9 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { HashRouter } from 'react-router-dom'
import { AppProvider } from './contexts/AppContext'
import App from './App.tsx'
import './style.css'
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { HashRouter } from 'react-router-dom';
import { AppProvider } from './contexts/AppContext';
import App from './App.tsx';
import './style.css';
createRoot(document.getElementById('root')!).render(
<StrictMode>
@@ -12,5 +12,5 @@ createRoot(document.getElementById('root')!).render(
<App />
</AppProvider>
</HashRouter>
</StrictMode>,
)
</StrictMode>
);
+19 -62
View File
@@ -1,40 +1,7 @@
import { Bug, ExternalLink, MessageCircle, Shield } from 'lucide-react';
import type { LucideIcon } from 'lucide-react';
import { ExternalLink, Mail } from 'lucide-react';
import { useI18n } from '../hooks/useI18n';
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() {
const { t, bi, biArray } = useI18n();
const { about, brand } = siteData;
@@ -47,7 +14,9 @@ export default function AboutPage() {
</div>
<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 className="about-name">{brand.author}</div>
<div className="about-bio">{bi(about.bio)}</div>
@@ -60,7 +29,9 @@ export default function AboutPage() {
</div>
<div className="focus-grid">
{biArray(about.focus).map((item) => (
<div key={item} className="focus-item">{item}</div>
<div key={item} className="focus-item">
{item}
</div>
))}
</div>
</div>
@@ -71,7 +42,9 @@ export default function AboutPage() {
</div>
<div className="techstack-grid">
{about.techStack.map((tech) => (
<span key={tech} className="tech-tag">{tech}</span>
<span key={tech} className="tech-tag">
{tech}
</span>
))}
</div>
</div>
@@ -88,31 +61,15 @@ export default function AboutPage() {
<h2 className="section-title">{t('contact.title')}</h2>
</div>
<div className="contact-grid">
{contactCards.map((card) => {
const Icon = card.icon;
return (
<a
key={card.titleKey}
href={card.href}
target="_blank"
rel="noopener noreferrer"
className="contact-card"
>
<h3>
<Icon size={18} />
{t(card.titleKey)}
</h3>
<p>{t(card.descKey)}</p>
<span className="contact-card-link">{card.label}</span>
</a>
);
})}
<a
href={about.github}
target="_blank"
rel="noopener noreferrer"
className="contact-card"
>
<a href={`mailto:${brand.email}`} className="contact-card">
<h3>
<Mail size={18} />
{t('contact.email')}
</h3>
<p>{t('contact.email.desc')}</p>
<span className="contact-card-link">{brand.email}</span>
</a>
<a href={about.github} target="_blank" rel="noopener noreferrer" className="contact-card">
<h3>
<ExternalLink size={18} />
{t('about.github')}
+16 -5
View File
@@ -1,3 +1,4 @@
import { Link } from 'react-router-dom';
import { ExternalLink } from 'lucide-react';
import { useI18n } from '../hooks/useI18n';
import { siteData } from '../data/siteData';
@@ -9,7 +10,9 @@ import FeaturedCard from '../components/FeaturedCard';
export default function HomePage() {
const { t } = useI18n();
const featuredProjects = siteData.projects.filter((p) => p.featured);
const featuredProjects = siteData.projects
.filter((p) => p.featured)
.slice(0, 6);
return (
<div className="fade-in">
@@ -18,7 +21,15 @@ export default function HomePage() {
<StatsBar />
<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">
{featuredProjects.map((p) => (
<FeaturedCard key={p.id} project={p} />
@@ -26,12 +37,12 @@ export default function HomePage() {
</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>
<p className="section-subtitle" style={{ margin: '8px auto 0', maxWidth: 600 }}>
<p className="section-subtitle cta-subtitle">
{t('cta.subtitle')}
</p>
<div style={{ marginTop: 24 }}>
<div className="cta-actions">
<a
href={siteData.brand.github}
target="_blank"
+98 -73
View File
@@ -4,43 +4,47 @@ import { siteData } from '../data/siteData';
import DownloadTable from '../components/DownloadTable';
import RoadmapGrid from '../components/RoadmapGrid';
import ChangelogList from '../components/ChangelogList';
import { NotebookPen, Terminal, MapPin, Smartphone, BookOpen, Monitor, Brain, KeyRound, ExternalLink, Download } from 'lucide-react';
import { useState, useRef, useEffect, useCallback } from 'react';
import type { ComponentType } from 'react';
const iconMap: Record<string, ComponentType<{ size?: number }>> = {
NotebookPen, Terminal, MapPin, Smartphone, BookOpen, Monitor, Brain, KeyRound,
};
import ScreenshotCarousel from '../components/ScreenshotCarousel';
import { getIcon } from '../utils/iconRegistry';
import { ExternalLink, Download, BookOpen } from 'lucide-react';
export default function ProjectDetailPage() {
const { id } = useParams();
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) {
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 hasDownloads = p.downloads && p.downloads.length > 0;
const IconComponent = iconMap[p.icon];
const IconComponent = getIcon(p.icon);
return (
<div className="container fade-in">
{/* Breadcrumb */}
<div style={{ padding: '8px 0', fontSize: 13, color: 'var(--muted)' }}>
<Link to="/" style={{ color: 'var(--muted)' }}>{bi(siteData.nav[0].label)}</Link>
<div className="breadcrumb">
<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>
{/* Header */}
<div className="detail-header">
<div className="detail-header-top">
<div className="detail-icon">
{IconComponent ? <IconComponent size={28} /> : <span>{p.icon}</span>}
{IconComponent ? <IconComponent size={28} /> : <span>{p.icon}</span>}
</div>
<div>
<h1 className="detail-title">{p.displayName[lang] || p.name}</h1>
@@ -48,15 +52,47 @@ export default function ProjectDetailPage() {
</div>
</div>
<div className="detail-badges">
{p.techStack.map(ts => <span key={ts} className="badge badge-accent">{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>
{p.techStack.map((ts) => (
<span key={ts} className="badge badge-accent">
{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 className="detail-actions">
<a href={p.repoUrl} target="_blank" className="btn btn-primary"><ExternalLink size={16} />GitHub</a>
{hasDownloads && <Link to="/downloads" className="btn"><Download size={16} />{t('common.download')}</Link>}
{p.docsUrl && <a href={p.docsUrl} 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>
<a href={p.repoUrl} target="_blank" className="btn btn-primary">
<ExternalLink size={16} />
GitHub
</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>
@@ -73,26 +109,31 @@ export default function ProjectDetailPage() {
<div className="detail-section">
<h2 className="detail-section-title">{t('detail.features')}</h2>
<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>
{/* Screenshots */}
<div className="detail-section">
<h2 className="detail-section-title">{t('detail.screenshots')}</h2>
<ScreenshotCarousel count={3} placeholder={t('detail.screenshotPlaceholder')} />
</div>
{p.screenshots && p.screenshots.length > 0 && (
<div className="detail-section">
<h2 className="detail-section-title">{t('detail.screenshots')}</h2>
<ScreenshotCarousel screenshots={p.screenshots} />
</div>
)}
{/* Downloads */}
{hasDownloads && (
<div className="detail-section">
<div className="detail-section" id="downloads">
<h2 className="detail-section-title">{t('detail.downloads')}</h2>
<DownloadTable downloads={p.downloads} />
<div className="trust-note">{t('downloads.trustNote')}</div>
</div>
)}
{/* Roadmap */}
{p.roadmap && (
<div className="detail-section">
@@ -113,10 +154,24 @@ export default function ProjectDetailPage() {
<div className="detail-section">
<h2 className="detail-section-title">{t('detail.installGuide')}</h2>
<div className="install-list">
<div className="install-item"><strong>Windows</strong>{t('detail.install.windows')}</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 className="install-item">
<strong>Windows</strong>
{t('detail.install.windows')}
</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>
@@ -154,49 +209,19 @@ export default function ProjectDetailPage() {
</div>
</div>
<div className="detail-link-grid">
<a href={p.repoUrl} target="_blank" className="detail-link-btn"><ExternalLink size={15} />{t('detail.repo')}</a>
{p.docsUrl && <a href={p.docsUrl} className="detail-link-btn"><BookOpen size={15} />{t('detail.docs')}</a>}
<a href={p.repoUrl} target="_blank" className="detail-link-btn">
<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>
</aside>
</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>
);
}
+16 -9
View File
@@ -11,17 +11,24 @@ export default function ProjectsPage() {
const catParam = searchParams.get('cat');
const {
search, setSearch,
tech, setTech,
platform, setPlatform,
status, setStatus,
sort, setSort,
allTech, allPlatforms, allStatuses,
search,
setSearch,
tech,
setTech,
platform,
setPlatform,
status,
setStatus,
sort,
setSort,
allTech,
allPlatforms,
allStatuses,
filteredProjects,
} = useProjectFilters();
const displayedProjects = catParam
? filteredProjects.filter(p => p.type.includes(catParam))
? filteredProjects.filter((p) => p.type.includes(catParam))
: filteredProjects;
const techOptions = [
@@ -54,7 +61,7 @@ export default function ProjectsPage() {
type="text"
className="search-input"
value={search}
onChange={e => setSearch(e.target.value)}
onChange={(e) => setSearch(e.target.value)}
placeholder={t('projects.search')}
/>
@@ -90,7 +97,7 @@ export default function ProjectsPage() {
{displayedProjects.length === 0 ? (
<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>
+29 -2575
View File
File diff suppressed because it is too large Load Diff
+221
View File
@@ -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);
}
}
+183
View File
@@ -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);
}
+90
View File
@@ -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;
}
+338
View File
@@ -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;
}
+98
View File
@@ -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);
}
+414
View File
@@ -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;
}
+79
View File
@@ -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);
}
+108
View File
@@ -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);
}
+98
View File
@@ -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;
}
+241
View File
@@ -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;
}
}
+109
View File
@@ -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;
}
+282
View File
@@ -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;
}
+125
View File
@@ -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;
}
+42
View File
@@ -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');
}
+66
View File
@@ -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;
}
+68
View File
@@ -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;
}
+355
View File
@@ -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;
}
}
+28
View File
@@ -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);
}
+134
View File
@@ -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;
}
+1
View File
@@ -0,0 +1 @@
import '@testing-library/jest-dom';
+69
View File
@@ -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');
});
});
+51
View File
@@ -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([]);
});
});
+2
View File
@@ -11,6 +11,7 @@ export interface BilingualArray {
export interface BrandData {
name: BilingualText;
slogan: BilingualText;
logo?: string;
author: string;
github: string;
email: string;
@@ -86,6 +87,7 @@ export interface Project {
roadmap?: RoadmapItem;
changelog?: ChangelogEntry[];
architecture?: BilingualText;
screenshots?: string[];
}
export interface AboutData {
+36
View File
@@ -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
View File
@@ -1,7 +1,4 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
"references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }]
}
+30 -4
View File
@@ -1,7 +1,33 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
/// <reference types="vitest/config" />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
// https://vite.dev/config/
export default defineConfig({
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,
},
});