feat: 新增 QuantaNote 项目展示,重构项目卡片与截图浏览组件
- 新增 QuantaNote 完整项目数据(特性描述、截图、Logo) - 为所有项目添加 logo 和 websiteUrl 字段支持 - 移除 stars/forks 相关展示与排序逻辑 - ScreenshotCarousel 增加左右切换箭头和 Lightbox 全屏预览(支持键盘导航) - 更新项目创建文档,补充 logo 和 installGuide 配置说明
@@ -11,7 +11,14 @@
|
||||
"mcp__Claude_Preview__preview_start",
|
||||
"Bash(pnpm dev *)",
|
||||
"Bash(rtk read *)",
|
||||
"Bash(rtk grep *)"
|
||||
"Bash(rtk grep *)",
|
||||
"Bash(rtk git *)",
|
||||
"Bash(gh repo *)",
|
||||
"Bash(gh release *)",
|
||||
"Bash(gh api *)",
|
||||
"Bash(echo \"exit: $?\")",
|
||||
"Bash(rtk npx *)",
|
||||
"Bash(rtk tsc *)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
| 2 | `src/utils/iconRegistry.ts` | 仅当使用新图标时 |
|
||||
| 3 | `src/data/categories.yaml` | 仅当新增分类时 |
|
||||
| 4 | `src/data/statuses.yaml` | 仅当新增状态时 |
|
||||
| 5 | `public/screenshots/` | 仅当有截图时 |
|
||||
| 5 | `public/logos/` | 仅当有项目 Logo 时 |
|
||||
| 6 | `public/screenshots/` | 仅当有截图时 |
|
||||
|
||||
**核心原则:** 只需创建一个 YAML 文件,项目就会自动出现在网站上。`loader.ts` 使用 `import.meta.glob('./projects/*.yaml')` 自动扫描该目录,无需手动注册。
|
||||
|
||||
@@ -101,6 +102,11 @@ tags:
|
||||
# 如果需要新图标,见"步骤二"
|
||||
icon: 'Terminal'
|
||||
|
||||
# 项目 Logo 图片(可选,支持 SVG/PNG/JPG)
|
||||
# 图片放在 public/logos/ 目录下,路径相对于 public 目录
|
||||
# 若设置了 logo,卡片和详情页会优先展示 Logo 图片,否则回退到上面的 lucide 图标
|
||||
# logo: '/logos/my-new-app.png'
|
||||
|
||||
# GitHub 仓库地址(必须)
|
||||
repoUrl: 'https://github.com/shenjianZ/my-new-app'
|
||||
|
||||
@@ -115,8 +121,6 @@ docsUrl: 'https://github.com/shenjianZ/my-new-app#readme'
|
||||
latestVersion: 'v1.0.0'
|
||||
releaseDate: '2026-05-22'
|
||||
license: 'MIT'
|
||||
stars: 10
|
||||
forks: 2
|
||||
language: 'TypeScript'
|
||||
lastUpdated: '2026-05-22'
|
||||
|
||||
@@ -189,6 +193,39 @@ architecture:
|
||||
# screenshots:
|
||||
# - '/screenshots/my-new-app/main.png'
|
||||
# - '/screenshots/my-new-app/settings.png'
|
||||
|
||||
# ========== 安装指南(可选) ==========
|
||||
# 每个项目生产的安装包不同,按实际包格式填写即可
|
||||
# 会以紧凑的单行提示展示在"下载安装"区域内
|
||||
# 不填写则使用默认的通用安装说明
|
||||
|
||||
installGuide:
|
||||
zh:
|
||||
- platform: 'Windows'
|
||||
icon: '🪟'
|
||||
format: '.exe'
|
||||
tip: 'SmartScreen 拦截?点击"更多信息" → "仍要运行"'
|
||||
- platform: 'macOS'
|
||||
icon: '🍎'
|
||||
format: '.dmg'
|
||||
tip: '提示已损坏?终端运行: xattr -dr com.apple.quarantine /Applications/MyApp.app'
|
||||
- platform: 'Linux'
|
||||
icon: '🐧'
|
||||
format: '.AppImage'
|
||||
tip: 'chmod +x MyApp-*.AppImage && ./MyApp-*.AppImage'
|
||||
en:
|
||||
- platform: 'Windows'
|
||||
icon: '🪟'
|
||||
format: '.exe'
|
||||
tip: 'SmartScreen blocked? Click "More info" → "Run anyway"'
|
||||
- platform: 'macOS'
|
||||
icon: '🍎'
|
||||
format: '.dmg'
|
||||
tip: '"Damaged" error? Run: xattr -dr com.apple.quarantine /Applications/MyApp.app'
|
||||
- platform: 'Linux'
|
||||
icon: '🐧'
|
||||
format: '.AppImage'
|
||||
tip: 'chmod +x MyApp-*.AppImage && ./MyApp-*.AppImage'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 119 KiB |
|
After Width: | Height: | Size: 103 KiB |
|
After Width: | Height: | Size: 131 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 124 KiB |
|
After Width: | Height: | Size: 199 KiB |
|
After Width: | Height: | Size: 130 KiB |
|
After Width: | Height: | Size: 137 KiB |
|
After Width: | Height: | Size: 127 KiB |
|
After Width: | Height: | Size: 141 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 74 KiB |
@@ -1,5 +1,4 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Star } from 'lucide-react';
|
||||
import { useI18n } from '../hooks/useI18n';
|
||||
import { siteData } from '../data/siteData';
|
||||
import { getIcon } from '../utils/iconRegistry';
|
||||
@@ -20,7 +19,12 @@ export default function FeaturedCard({ project }: FeaturedCardProps) {
|
||||
className="project-card project-card-link"
|
||||
>
|
||||
<div className="project-card-header">
|
||||
<div className="project-icon">{IconComponent && <IconComponent size={22} />}</div>
|
||||
<div className="project-icon">
|
||||
{project.logo
|
||||
? <img src={project.logo} alt={project.name} className="project-logo" />
|
||||
: (IconComponent && <IconComponent size={22} />)
|
||||
}
|
||||
</div>
|
||||
<div className="project-card-info">
|
||||
<div className="project-name">
|
||||
{project.displayName[lang] || project.name}
|
||||
@@ -43,9 +47,6 @@ export default function FeaturedCard({ project }: FeaturedCardProps) {
|
||||
</div>
|
||||
|
||||
<div className="project-card-meta">
|
||||
<span className="badge badge-accent">
|
||||
<Star size={12} /> {project.stars}
|
||||
</span>
|
||||
<span className="badge">{project.latestVersion}</span>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { ExternalLink, Download, BookOpen, Star } from 'lucide-react';
|
||||
import { ExternalLink, Download, BookOpen, Globe } from 'lucide-react';
|
||||
import { useI18n } from '../hooks/useI18n';
|
||||
import { siteData } from '../data/siteData';
|
||||
import { getIcon } from '../utils/iconRegistry';
|
||||
@@ -17,7 +17,12 @@ export default function ProjectCard({ project }: ProjectCardProps) {
|
||||
return (
|
||||
<div className="project-card">
|
||||
<div className="project-card-header">
|
||||
<div className="project-icon">{IconComponent && <IconComponent size={22} />}</div>
|
||||
<div className="project-icon">
|
||||
{project.logo
|
||||
? <img src={project.logo} alt={project.name} className="project-logo" />
|
||||
: (IconComponent && <IconComponent size={22} />)
|
||||
}
|
||||
</div>
|
||||
<div className="project-card-info">
|
||||
<div className="project-name">
|
||||
<Link to={`/projects/${project.id}`}>{project.displayName[lang] || project.name}</Link>
|
||||
@@ -44,12 +49,6 @@ export default function ProjectCard({ project }: ProjectCardProps) {
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="project-card-meta">
|
||||
<span className="badge badge-accent">
|
||||
<Star size={12} /> {project.stars}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="project-card-actions">
|
||||
<a href={project.repoUrl} target="_blank" rel="noopener noreferrer" className="btn btn-sm">
|
||||
<ExternalLink size={14} /> GitHub
|
||||
@@ -59,6 +58,16 @@ export default function ProjectCard({ project }: ProjectCardProps) {
|
||||
<Download size={14} /> {t('common.download')}
|
||||
</Link>
|
||||
)}
|
||||
{project.websiteUrl && (
|
||||
<a
|
||||
href={project.websiteUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="btn btn-sm"
|
||||
>
|
||||
<Globe size={14} /> {t('detail.website')}
|
||||
</a>
|
||||
)}
|
||||
{project.docsUrl && (
|
||||
<a
|
||||
href={project.docsUrl}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { useState, useRef, useEffect, useCallback } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { ChevronLeft, ChevronRight, X } from 'lucide-react';
|
||||
|
||||
interface ScreenshotCarouselProps {
|
||||
screenshots?: string[];
|
||||
@@ -7,6 +9,7 @@ interface ScreenshotCarouselProps {
|
||||
|
||||
export default function ScreenshotCarousel({ screenshots }: ScreenshotCarouselProps) {
|
||||
const [active, setActive] = useState(0);
|
||||
const [preview, setPreview] = useState<number | null>(null);
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const scrollTo = useCallback((i: number) => {
|
||||
@@ -18,6 +21,16 @@ export default function ScreenshotCarousel({ screenshots }: ScreenshotCarouselPr
|
||||
});
|
||||
}, []);
|
||||
|
||||
const prev = useCallback(() => {
|
||||
if (!screenshots) return;
|
||||
scrollTo(active > 0 ? active - 1 : screenshots.length - 1);
|
||||
}, [active, screenshots, scrollTo]);
|
||||
|
||||
const next = useCallback(() => {
|
||||
if (!screenshots) return;
|
||||
scrollTo(active < screenshots.length - 1 ? active + 1 : 0);
|
||||
}, [active, screenshots, scrollTo]);
|
||||
|
||||
useEffect(() => {
|
||||
const el = scrollRef.current;
|
||||
if (!el) return;
|
||||
@@ -36,19 +49,51 @@ export default function ScreenshotCarousel({ screenshots }: ScreenshotCarouselPr
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
// 同步 lightbox 切换到轮播
|
||||
useEffect(() => {
|
||||
if (preview === null) return;
|
||||
scrollTo(preview);
|
||||
}, [preview, scrollTo]);
|
||||
|
||||
useEffect(() => {
|
||||
if (preview === null) return;
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') setPreview(null);
|
||||
if (e.key === 'ArrowLeft') setPreview((p) => p !== null ? (p > 0 ? p - 1 : screenshots!.length - 1) : null);
|
||||
if (e.key === 'ArrowRight') setPreview((p) => p !== null ? (p < screenshots!.length - 1 ? p + 1 : 0) : null);
|
||||
};
|
||||
document.addEventListener('keydown', onKey);
|
||||
document.body.style.overflow = 'hidden';
|
||||
return () => {
|
||||
document.removeEventListener('keydown', onKey);
|
||||
document.body.style.overflow = '';
|
||||
};
|
||||
}, [preview, screenshots]);
|
||||
|
||||
if (!screenshots || screenshots.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="screenshot-carousel">
|
||||
{screenshots.length > 1 && (
|
||||
<button className="screenshot-arrow screenshot-arrow-prev" onClick={prev} aria-label="Previous">
|
||||
<ChevronLeft size={18} />
|
||||
</button>
|
||||
)}
|
||||
<div className="screenshot-scroll" ref={scrollRef}>
|
||||
{screenshots.map((src, i) => (
|
||||
<div key={i} className="screenshot-slide">
|
||||
<div key={i} className="screenshot-slide" onClick={() => setPreview(i)}>
|
||||
<img src={src} alt={`Screenshot ${i + 1}`} className="screenshot-image" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{screenshots.length > 1 && (
|
||||
<button className="screenshot-arrow screenshot-arrow-next" onClick={next} aria-label="Next">
|
||||
<ChevronRight size={18} />
|
||||
</button>
|
||||
)}
|
||||
{screenshots.length > 1 && (
|
||||
<div className="screenshot-dots">
|
||||
{screenshots.map((_, i) => (
|
||||
@@ -61,5 +106,40 @@ export default function ScreenshotCarousel({ screenshots }: ScreenshotCarouselPr
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{preview !== null && createPortal(
|
||||
<div className="screenshot-lightbox" onClick={() => setPreview(null)}>
|
||||
<button className="screenshot-lightbox-close" onClick={() => setPreview(null)} aria-label="Close">
|
||||
<X size={24} />
|
||||
</button>
|
||||
{screenshots.length > 1 && (
|
||||
<button
|
||||
className="screenshot-lightbox-arrow screenshot-lightbox-prev"
|
||||
onClick={(e) => { e.stopPropagation(); setPreview(p => p !== null ? (p > 0 ? p - 1 : screenshots.length - 1) : null); }}
|
||||
aria-label="Previous"
|
||||
>
|
||||
<ChevronLeft size={28} />
|
||||
</button>
|
||||
)}
|
||||
<img
|
||||
src={screenshots[preview]}
|
||||
alt={`Screenshot ${preview + 1}`}
|
||||
className="screenshot-lightbox-img"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
{screenshots.length > 1 && (
|
||||
<button
|
||||
className="screenshot-lightbox-arrow screenshot-lightbox-next"
|
||||
onClick={(e) => { e.stopPropagation(); setPreview(p => p !== null ? (p < screenshots.length - 1 ? p + 1 : 0) : null); }}
|
||||
aria-label="Next"
|
||||
>
|
||||
<ChevronRight size={28} />
|
||||
</button>
|
||||
)}
|
||||
<div className="screenshot-lightbox-counter">{preview + 1} / {screenshots.length}</div>
|
||||
</div>,
|
||||
document.body
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,13 +5,11 @@ export default function StatsBar() {
|
||||
const { t } = useI18n();
|
||||
const projects = siteData.projects;
|
||||
|
||||
const totalStars = projects.reduce((sum, p) => sum + p.stars, 0);
|
||||
const uniqueTechStacks = new Set(projects.flatMap((p) => p.techStack)).size;
|
||||
const uniquePlatforms = new Set(projects.flatMap((p) => p.platforms)).size;
|
||||
|
||||
const stats = [
|
||||
{ id: 'projects', value: projects.length, label: t('stats.projects') },
|
||||
{ id: 'stars', value: totalStars, label: t('stats.stars') },
|
||||
{ id: 'techStack', value: uniqueTechStacks, label: t('stats.techStack') },
|
||||
{ id: 'platforms', value: uniquePlatforms, label: t('stats.platforms') },
|
||||
];
|
||||
|
||||
@@ -8,7 +8,6 @@ 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'
|
||||
@@ -30,7 +29,6 @@ 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...'
|
||||
@@ -49,6 +47,7 @@ detail.platforms: 'Platforms'
|
||||
detail.status: 'Status'
|
||||
detail.lastUpdate: 'Last Updated'
|
||||
detail.repo: 'GitHub Repo'
|
||||
detail.website: 'Website'
|
||||
detail.docs: 'Online Docs'
|
||||
detail.release: 'Download Release'
|
||||
detail.installGuide: 'Installation Guide'
|
||||
@@ -119,8 +118,6 @@ 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'
|
||||
|
||||
@@ -8,7 +8,6 @@ hero.cta.github: '访问 GitHub'
|
||||
hero.cta.download: '下载软件'
|
||||
hero.cta.docs: '查看文档'
|
||||
stats.projects: '开源项目'
|
||||
stats.stars: 'GitHub Stars'
|
||||
stats.techStack: '技术栈'
|
||||
stats.platforms: '支持平台'
|
||||
featured.title: '重点项目'
|
||||
@@ -30,7 +29,6 @@ projects.filter.tech: '技术栈'
|
||||
projects.filter.platform: '平台'
|
||||
projects.filter.status: '状态'
|
||||
projects.sort.updated: '最近更新'
|
||||
projects.sort.stars: 'Star 最多'
|
||||
projects.sort.name: '名称排序'
|
||||
projects.noResults: '没有匹配的项目'
|
||||
projects.search: '搜索项目名称、描述或标签...'
|
||||
@@ -49,6 +47,7 @@ detail.platforms: '支持平台'
|
||||
detail.status: '开发状态'
|
||||
detail.lastUpdate: '最后更新'
|
||||
detail.repo: 'GitHub 仓库'
|
||||
detail.website: '官网'
|
||||
detail.docs: '在线文档'
|
||||
detail.release: '下载 Release'
|
||||
detail.installGuide: '安装说明'
|
||||
@@ -119,8 +118,6 @@ common.download: '下载'
|
||||
common.docs: '文档'
|
||||
common.demo: '在线演示'
|
||||
common.back: '返回'
|
||||
common.stars: 'Stars'
|
||||
common.forks: 'Forks'
|
||||
common.version: '版本'
|
||||
common.platform: '平台'
|
||||
common.size: '大小'
|
||||
|
||||
@@ -44,13 +44,12 @@ tags:
|
||||
- 'WebRTC'
|
||||
- 'Self-hosted'
|
||||
icon: 'Monitor'
|
||||
logo: '/logos/billddesk.png'
|
||||
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'
|
||||
recommended: false
|
||||
|
||||
@@ -45,13 +45,12 @@ tags:
|
||||
- 'Account Management'
|
||||
- 'Desktop'
|
||||
icon: 'KeyRound'
|
||||
logo: '/logos/codex-manager.png'
|
||||
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'
|
||||
recommended: false
|
||||
|
||||
@@ -49,13 +49,12 @@ tags:
|
||||
- 'Scrcpy'
|
||||
- 'Debug'
|
||||
icon: 'Smartphone'
|
||||
logo: '/logos/devicedeck.png'
|
||||
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'
|
||||
recommended: true
|
||||
|
||||
@@ -45,13 +45,12 @@ tags:
|
||||
- 'Classification'
|
||||
- 'API'
|
||||
icon: 'Brain'
|
||||
logo: '/logos/news-classifier.png'
|
||||
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'
|
||||
recommended: false
|
||||
|
||||
@@ -4,15 +4,15 @@ displayName:
|
||||
zh: 'QuantaNote'
|
||||
en: 'QuantaNote'
|
||||
slogan:
|
||||
zh: '本地优先的跨平台桌面笔记与知识管理工具'
|
||||
en: 'Local-first cross-platform desktop note & knowledge management tool'
|
||||
zh: '本地优先的跨平台桌面笔记与知识管理工具 — Markdown 编辑、全文搜索、悬浮球快捷操作'
|
||||
en: 'Local-first cross-platform desktop note & knowledge management — Markdown editing, full-text search, floating ball quick actions'
|
||||
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 编辑(Vditor IR 模式)、FTS5 全文搜索(含中文子串)、标签管理、版本历史 Diff 对比、附件预览、导入导出、自动备份、悬浮球快捷操作等功能。所有数据存储在本地 SQLite,强调数据主权和轻量跨平台体验。支持云同步(开发中)。'
|
||||
en: 'QuantaNote is a local-first desktop note app built with Tauri 2, Rust, and React. Features include Markdown editing (Vditor IR mode), FTS5 full-text search with Chinese substring support, tag management, version history with diff comparison, attachment preview, import/export, auto backup, and floating ball quick actions. All data is stored in local SQLite, emphasizing data ownership and lightweight cross-platform experience. Cloud sync is under development.'
|
||||
type:
|
||||
- 'desktop'
|
||||
- 'devtool'
|
||||
status: 'active'
|
||||
status: 'maintained'
|
||||
platforms:
|
||||
- 'windows'
|
||||
- 'macos'
|
||||
@@ -20,50 +20,63 @@ platforms:
|
||||
techStack:
|
||||
- 'Tauri 2'
|
||||
- 'Rust'
|
||||
- 'React'
|
||||
- 'React 19'
|
||||
- 'TypeScript'
|
||||
- 'SQLite'
|
||||
- 'TailwindCSS'
|
||||
- 'SQLite (rusqlite 0.35)'
|
||||
- 'TailwindCSS 4'
|
||||
- 'Vditor'
|
||||
- 'Zustand'
|
||||
- 'i18next'
|
||||
features:
|
||||
zh:
|
||||
- 'Markdown 编辑'
|
||||
- '本地 SQLite 存储'
|
||||
- '全文搜索'
|
||||
- '标签管理'
|
||||
- '附件预览'
|
||||
- '版本历史'
|
||||
- '导入导出'
|
||||
- '自动备份'
|
||||
- '主题切换'
|
||||
- '系统托盘'
|
||||
- 'Markdown 编辑器(Vditor IR 模式)'
|
||||
- '全文搜索(FTS5 + trigram 双引擎,支持中文子串)'
|
||||
- '标签管理(CRUD + 多对多关联)'
|
||||
- '命令面板(Ctrl+K 全局快速搜索)'
|
||||
- '版本历史与 Diff 对比'
|
||||
- '附件管理(图片/音频/视频/PDF 预览)'
|
||||
- '数据导入导出(JSON / ZIP 可选)'
|
||||
- '定时自动备份'
|
||||
- '深色/浅色主题 + 自定义强调色'
|
||||
- '系统托盘 + 开机自启动'
|
||||
- '悬浮球快捷操作(径向菜单)'
|
||||
- '快速笔记独立窗口'
|
||||
- '中英文国际化'
|
||||
- '云同步(开发中)'
|
||||
en:
|
||||
- 'Markdown editing'
|
||||
- 'Local SQLite storage'
|
||||
- 'Full-text search'
|
||||
- 'Tag management'
|
||||
- 'Attachment preview'
|
||||
- 'Version history'
|
||||
- 'Import/export'
|
||||
- 'Auto backup'
|
||||
- 'Theme switching'
|
||||
- 'System tray'
|
||||
- 'Markdown editor (Vditor IR mode)'
|
||||
- 'Full-text search (FTS5 + trigram, Chinese substring)'
|
||||
- 'Tag management (CRUD + many-to-many)'
|
||||
- 'Command palette (Ctrl+K global search)'
|
||||
- 'Version history with diff comparison'
|
||||
- 'Attachment management (image/audio/video/PDF preview)'
|
||||
- 'Import/Export (JSON / ZIP selectable)'
|
||||
- 'Scheduled auto backup'
|
||||
- 'Dark/Light theme + custom accent colors'
|
||||
- 'System tray + auto-start on boot'
|
||||
- 'Floating ball quick actions (radial menu)'
|
||||
- 'Quick note standalone window'
|
||||
- 'i18n (Chinese / English)'
|
||||
- 'Cloud sync (WIP)'
|
||||
tags:
|
||||
- 'Markdown'
|
||||
- 'Notes'
|
||||
- 'Knowledge Management'
|
||||
- 'Desktop'
|
||||
- 'Notebook'
|
||||
- 'Local-first'
|
||||
- 'Tauri'
|
||||
- 'Vditor'
|
||||
icon: 'NotebookPen'
|
||||
repoUrl: 'https://github.com/shenjianZ/quantanote'
|
||||
docsUrl: 'https://github.com/shenjianZ/quantanote#readme'
|
||||
latestVersion: 'v0.2.0'
|
||||
releaseDate: '2026-04-15'
|
||||
logo: '/logos/quantanote.png'
|
||||
repoUrl: 'https://github.com/shenjianZ/QuantaNote'
|
||||
websiteUrl: 'https://shenjianz.github.io/QuantaNote/'
|
||||
docsUrl: 'https://quantanote-docs.shenjianl.cn/'
|
||||
latestVersion: 'v0.2.3'
|
||||
releaseDate: '2026-05-12'
|
||||
license: 'MIT'
|
||||
stars: 128
|
||||
forks: 12
|
||||
language: 'Rust'
|
||||
lastUpdated: '2026-05-10'
|
||||
lastUpdated: '2026-05-12'
|
||||
recommended: true
|
||||
featured: true
|
||||
order: 1
|
||||
@@ -71,23 +84,23 @@ color: '#3B82F6'
|
||||
downloads:
|
||||
- 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'
|
||||
url: 'https://file.shenjianl.cn/softwore/QuantaNote/v0.2.3/QuantaNote-v0.2.3-windows-x64.exe'
|
||||
size: '9.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'
|
||||
url: 'https://file.shenjianl.cn/softwore/QuantaNote/v0.2.3/QuantaNote-v0.2.3-macos-aarch64.dmg'
|
||||
size: '14.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'
|
||||
url: 'https://file.shenjianl.cn/softwore/QuantaNote/v0.2.3/QuantaNote-v0.2.3-macos-x64.dmg'
|
||||
size: '14.7 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'
|
||||
url: 'https://file.shenjianl.cn/softwore/QuantaNote/v0.2.3/QuantaNote-v0.2.3-linux-x64.AppImage'
|
||||
size: '86.1 MB'
|
||||
sha256: ''
|
||||
roadmap:
|
||||
done:
|
||||
@@ -96,44 +109,136 @@ roadmap:
|
||||
- '本地存储'
|
||||
- '标签系统'
|
||||
- '全文搜索'
|
||||
- '版本历史与 Diff'
|
||||
- '附件管理'
|
||||
- '导入导出'
|
||||
- '自动备份'
|
||||
- '主题与自定义强调色'
|
||||
- '系统托盘与开机自启'
|
||||
- '账号管理'
|
||||
- '悬浮球快捷操作'
|
||||
- '快速笔记窗口'
|
||||
doing:
|
||||
- '云同步'
|
||||
- '多端同步'
|
||||
- '账号管理'
|
||||
planned:
|
||||
- '插件系统'
|
||||
- 'MCP 接入'
|
||||
- '移动端查看'
|
||||
- 'AI 辅助写作'
|
||||
changelog:
|
||||
- version: 'v0.2.0'
|
||||
date: '2026-04-15'
|
||||
- version: 'v0.2.3'
|
||||
date: '2026-05-12'
|
||||
changes:
|
||||
zh:
|
||||
- '新增账号管理模块'
|
||||
- '修复 Token 刷新竞态'
|
||||
- '优化同步状态显示'
|
||||
- '新增附件预览支持'
|
||||
- '悬浮球功能:收起时球体、展开时径向菜单,支持快速笔记和搜索'
|
||||
- '悬浮球设置项:透明度调节、窗口置顶、开关控制'
|
||||
- '快速笔记独立窗口,支持 Markdown 编辑'
|
||||
- '完整中英文国际化支持'
|
||||
en:
|
||||
- 'Added account management'
|
||||
- 'Fixed token refresh race condition'
|
||||
- 'Improved sync status display'
|
||||
- 'Added attachment preview'
|
||||
- 'Floating ball: radial menu for quick notes and search'
|
||||
- 'Floating ball settings: opacity, always-on-top, toggle'
|
||||
- 'Quick note standalone window with Markdown editing'
|
||||
- 'Full Chinese/English i18n support'
|
||||
- version: 'v0.2.2'
|
||||
date: '2026-05-11'
|
||||
changes:
|
||||
zh:
|
||||
- '修复更新包公钥配置被占位符覆盖导致签名校验失败'
|
||||
- '更新关于页的更新错误提示'
|
||||
en:
|
||||
- 'Fixed updater public key config overridden by placeholder'
|
||||
- 'Improved update error messages on About page'
|
||||
- version: 'v0.2.1'
|
||||
date: '2026-05-11'
|
||||
changes:
|
||||
zh:
|
||||
- '升级 rusqlite 0.31→0.35、thiserror 1→2'
|
||||
- '同步状态管理改用 Result 替代 unwrap'
|
||||
- '新增密码长度、标签颜色等输入验证'
|
||||
- '修复前端 TopBar 非 Tauri 环境报错'
|
||||
- '编辑器搜索高亮改用 DOM API'
|
||||
en:
|
||||
- 'Upgraded rusqlite 0.31→0.35, thiserror 1→2'
|
||||
- 'Sync state management uses Result instead of unwrap'
|
||||
- 'Added input validation for password length, tag colors'
|
||||
- 'Fixed TopBar errors in non-Tauri environment'
|
||||
- 'Editor search highlight uses DOM API'
|
||||
- version: 'v0.2.0'
|
||||
date: '2026-05-05'
|
||||
changes:
|
||||
zh:
|
||||
- '新增账号管理(个人资料、修改密码、删除账号)'
|
||||
- '云同步增强:Token 刷新竞态修复、同步状态指示器'
|
||||
- 'Docker 多阶段构建 + docker-compose 部署'
|
||||
- '文档站自动部署到 GitHub Pages'
|
||||
en:
|
||||
- 'Account management (profile, password change, account deletion)'
|
||||
- 'Cloud sync: token refresh race fix, sync status indicator'
|
||||
- 'Docker multi-stage build + docker-compose deployment'
|
||||
- 'Docs auto-deploy to GitHub Pages'
|
||||
- version: 'v0.1.0'
|
||||
date: '2026-02-20'
|
||||
date: '2026-05-02'
|
||||
changes:
|
||||
zh:
|
||||
- '首个公开版本'
|
||||
- '基础笔记 CRUD'
|
||||
- 'Markdown 编辑器'
|
||||
- '本地 SQLite 存储'
|
||||
- '标签管理'
|
||||
- 'Markdown 编辑器(Vditor IR 模式)'
|
||||
- '全文搜索(FTS5 + trigram 双引擎)'
|
||||
- '版本历史与 Diff 对比'
|
||||
- '附件管理、导入导出、自动备份'
|
||||
- '主题切换、系统托盘'
|
||||
en:
|
||||
- 'First public release'
|
||||
- 'Basic note CRUD'
|
||||
- 'Markdown editor'
|
||||
- 'Local SQLite storage'
|
||||
- 'Tag management'
|
||||
- 'Markdown editor (Vditor IR mode)'
|
||||
- 'Full-text search (FTS5 + trigram dual engine)'
|
||||
- 'Version history with diff comparison'
|
||||
- 'Attachments, import/export, auto backup'
|
||||
- 'Theme switching, system tray'
|
||||
architecture:
|
||||
zh: '前端 (React + TypeScript) → Tauri Commands → Rust 核心层 → SQLite 数据库 → 本地文件存储'
|
||||
en: 'Frontend (React + TypeScript) → Tauri Commands → Rust Core → SQLite Database → Local File Storage'
|
||||
zh: '前端 (React 19 + Zustand + TailwindCSS 4) → Tauri 2 Commands → Rust 核心层 (rusqlite 0.35) → SQLite (WAL + FTS5) → 本地文件存储'
|
||||
en: 'Frontend (React 19 + Zustand + TailwindCSS 4) → Tauri 2 Commands → Rust Core (rusqlite 0.35) → SQLite (WAL + FTS5) → Local File Storage'
|
||||
|
||||
screenshots:
|
||||
- '/screenshots/quantanote/library.png'
|
||||
- '/screenshots/quantanote/note-preview.png'
|
||||
- '/screenshots/quantanote/note-edit.png'
|
||||
- '/screenshots/quantanote/note-version.png'
|
||||
- '/screenshots/quantanote/workspace.png'
|
||||
- '/screenshots/quantanote/search-cmd.png'
|
||||
- '/screenshots/quantanote/settings-appearance.png'
|
||||
- '/screenshots/quantanote/settings-font.png'
|
||||
- '/screenshots/quantanote/settings-data.png'
|
||||
- '/screenshots/quantanote/settings-sync.png'
|
||||
- '/screenshots/quantanote/settings-about.png'
|
||||
- '/screenshots/quantanote/accoun-login.png'
|
||||
- '/screenshots/quantanote/account-register.png'
|
||||
- '/screenshots/quantanote/account-profile.png'
|
||||
- '/screenshots/quantanote/account.png'
|
||||
- '/screenshots/quantanote/topbar-more.png'
|
||||
|
||||
installGuide:
|
||||
zh:
|
||||
- platform: 'Windows'
|
||||
icon: '🪟'
|
||||
format: '.exe'
|
||||
tip: 'SmartScreen 拦截?点击"更多信息" → "仍要运行"'
|
||||
- platform: 'macOS'
|
||||
icon: '🍎'
|
||||
format: '.dmg'
|
||||
tip: '提示已损坏?终端运行: xattr -dr com.apple.quarantine /Applications/QuantaNote.app'
|
||||
- platform: 'Linux'
|
||||
icon: '🐧'
|
||||
format: '.AppImage'
|
||||
tip: 'chmod +x QuantaNote-*.AppImage && ./QuantaNote-*.AppImage'
|
||||
en:
|
||||
- platform: 'Windows'
|
||||
icon: '🪟'
|
||||
format: '.exe'
|
||||
tip: 'SmartScreen blocked? Click "More info" → "Run anyway"'
|
||||
- platform: 'macOS'
|
||||
icon: '🍎'
|
||||
format: '.dmg'
|
||||
tip: '"Damaged" error? Run: xattr -dr com.apple.quarantine /Applications/QuantaNote.app'
|
||||
- platform: 'Linux'
|
||||
icon: '🐧'
|
||||
format: '.AppImage'
|
||||
tip: 'chmod +x QuantaNote-*.AppImage && ./QuantaNote-*.AppImage'
|
||||
|
||||
@@ -47,14 +47,13 @@ tags:
|
||||
- 'NPM'
|
||||
- 'MDX'
|
||||
icon: 'BookOpen'
|
||||
logo: '/logos/react-docs-ui.png'
|
||||
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'
|
||||
recommended: true
|
||||
|
||||
@@ -50,13 +50,12 @@ tags:
|
||||
- 'SFTP'
|
||||
- 'DevOps'
|
||||
icon: 'Terminal'
|
||||
logo: '/logos/ssh-terminal.png'
|
||||
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'
|
||||
recommended: true
|
||||
|
||||
@@ -46,13 +46,12 @@ tags:
|
||||
- 'Social'
|
||||
- 'Mobile'
|
||||
icon: 'MapPin'
|
||||
logo: '/logos/streetmoment.png'
|
||||
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'
|
||||
recommended: false
|
||||
|
||||
@@ -39,8 +39,7 @@ export function useProjectFilters() {
|
||||
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));
|
||||
if (sort === 'name') result.sort((a, b) => a.name.localeCompare(b.name));
|
||||
else result.sort((a, b) => b.lastUpdated.localeCompare(a.lastUpdated));
|
||||
|
||||
return result;
|
||||
|
||||
@@ -6,7 +6,7 @@ import RoadmapGrid from '../components/RoadmapGrid';
|
||||
import ChangelogList from '../components/ChangelogList';
|
||||
import ScreenshotCarousel from '../components/ScreenshotCarousel';
|
||||
import { getIcon } from '../utils/iconRegistry';
|
||||
import { ExternalLink, Download, BookOpen } from 'lucide-react';
|
||||
import { ExternalLink, Download, BookOpen, Globe } from 'lucide-react';
|
||||
|
||||
export default function ProjectDetailPage() {
|
||||
const { id } = useParams();
|
||||
@@ -44,7 +44,10 @@ export default function ProjectDetailPage() {
|
||||
<div className="detail-header">
|
||||
<div className="detail-header-top">
|
||||
<div className="detail-icon">
|
||||
{IconComponent ? <IconComponent size={28} /> : <span>{p.icon}</span>}
|
||||
{p.logo
|
||||
? <img src={p.logo} alt={p.name} className="project-logo" />
|
||||
: (IconComponent ? <IconComponent size={28} /> : <span>{p.icon}</span>)
|
||||
}
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="detail-title">{p.displayName[lang] || p.name}</h1>
|
||||
@@ -70,10 +73,12 @@ export default function ProjectDetailPage() {
|
||||
</span>
|
||||
</div>
|
||||
<div className="detail-actions">
|
||||
<a href={p.repoUrl} target="_blank" className="btn btn-primary">
|
||||
<ExternalLink size={16} />
|
||||
GitHub
|
||||
{p.websiteUrl && (
|
||||
<a href={p.websiteUrl} target="_blank" rel="noopener noreferrer" className="btn btn-primary">
|
||||
<Globe size={16} />
|
||||
{t('detail.website')}
|
||||
</a>
|
||||
)}
|
||||
{hasDownloads && (
|
||||
<a href="#downloads" className="btn" onClick={(e) => {
|
||||
e.preventDefault();
|
||||
@@ -130,15 +135,18 @@ export default function ProjectDetailPage() {
|
||||
<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>
|
||||
{p.installGuide && (
|
||||
<div className="install-guide-inline">
|
||||
{p.installGuide[lang].map((item) => (
|
||||
<div key={item.platform} className="install-tip-row">
|
||||
<span className="install-tip-icon">{item.icon}</span>
|
||||
<span className="install-tip-format">{item.format}</span>
|
||||
<span className="install-tip-text">{item.tip}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Roadmap */}
|
||||
{p.roadmap && (
|
||||
<div className="detail-section">
|
||||
<h2 className="detail-section-title">{t('detail.roadmap')}</h2>
|
||||
<RoadmapGrid roadmap={p.roadmap} />
|
||||
<div className="trust-note">{t('downloads.trustNote')}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -150,31 +158,14 @@ export default function ProjectDetailPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Install guide */}
|
||||
{/* Roadmap */}
|
||||
{p.roadmap && (
|
||||
<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')}
|
||||
<h2 className="detail-section-title">{t('detail.roadmap')}</h2>
|
||||
<RoadmapGrid roadmap={p.roadmap} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sidebar */}
|
||||
<aside className="detail-sidebar">
|
||||
@@ -191,14 +182,6 @@ export default function ProjectDetailPage() {
|
||||
<span className="detail-meta-label">{t('detail.license')}</span>
|
||||
<span className="detail-meta-value">{p.license}</span>
|
||||
</div>
|
||||
<div className="detail-meta-item">
|
||||
<span className="detail-meta-label">{t('common.stars')}</span>
|
||||
<span className="detail-meta-value mono">{p.stars}</span>
|
||||
</div>
|
||||
<div className="detail-meta-item">
|
||||
<span className="detail-meta-label">{t('common.forks')}</span>
|
||||
<span className="detail-meta-value mono">{p.forks}</span>
|
||||
</div>
|
||||
<div className="detail-meta-item">
|
||||
<span className="detail-meta-label">{t('detail.lastUpdate')}</span>
|
||||
<span className="detail-meta-value">{p.lastUpdated}</span>
|
||||
@@ -213,6 +196,12 @@ export default function ProjectDetailPage() {
|
||||
<ExternalLink size={15} />
|
||||
{t('detail.repo')}
|
||||
</a>
|
||||
{p.websiteUrl && (
|
||||
<a href={p.websiteUrl} target="_blank" rel="noopener noreferrer" className="detail-link-btn">
|
||||
<Globe size={15} />
|
||||
{t('detail.website')}
|
||||
</a>
|
||||
)}
|
||||
{p.docsUrl && (
|
||||
<a href={p.docsUrl} target="_blank" rel="noopener noreferrer" className="detail-link-btn">
|
||||
<BookOpen size={15} />
|
||||
|
||||
@@ -45,7 +45,6 @@ export default function ProjectsPage() {
|
||||
];
|
||||
const sortOptions = [
|
||||
{ value: 'updated', label: t('projects.sort.updated') },
|
||||
{ value: 'stars', label: t('projects.sort.stars') },
|
||||
{ value: 'name', label: t('projects.sort.name') },
|
||||
];
|
||||
|
||||
|
||||
@@ -169,6 +169,13 @@
|
||||
transform: scale(1.08);
|
||||
}
|
||||
|
||||
.project-logo {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
object-fit: contain;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.project-card-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
@@ -70,6 +70,11 @@
|
||||
background: linear-gradient(135deg, oklch(100% 0 0 / 10%) 0%, transparent 60%);
|
||||
}
|
||||
|
||||
.detail-icon .project-logo {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.detail-title {
|
||||
font-size: 36px;
|
||||
font-weight: 800;
|
||||
@@ -290,12 +295,13 @@
|
||||
|
||||
.screenshot-scroll {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
scroll-snap-type: x mandatory;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-width: none;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.screenshot-scroll::-webkit-scrollbar {
|
||||
@@ -305,13 +311,176 @@
|
||||
.screenshot-slide {
|
||||
flex: 0 0 85%;
|
||||
max-width: 400px;
|
||||
aspect-ratio: 16 / 10;
|
||||
scroll-snap-align: center;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.screenshot-slide:first-child {
|
||||
margin-inline-start: 4px;
|
||||
}
|
||||
|
||||
.screenshot-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--border);
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.screenshot-slide:hover .screenshot-image {
|
||||
transform: scale(1.02);
|
||||
box-shadow: 0 4px 20px oklch(0% 0 0 / 30%);
|
||||
}
|
||||
|
||||
/* Desktop arrows — hidden on mobile */
|
||||
.screenshot-arrow {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
z-index: 2;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--border);
|
||||
background: oklch(15% 0.025 270 / 80%);
|
||||
backdrop-filter: blur(8px);
|
||||
color: var(--fg);
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all var(--transition);
|
||||
}
|
||||
|
||||
.screenshot-arrow:hover {
|
||||
background: oklch(74% 0.2 45 / 20%);
|
||||
border-color: oklch(74% 0.2 45 / 30%);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.screenshot-arrow-prev {
|
||||
left: 4px;
|
||||
}
|
||||
|
||||
.screenshot-arrow-next {
|
||||
right: 4px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.screenshot-slide {
|
||||
flex: 0 0 calc(33.333% - 8px);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.screenshot-arrow {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
/* Lightbox overlay */
|
||||
.screenshot-lightbox {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 9999;
|
||||
background: oklch(0% 0 0 / 85%);
|
||||
backdrop-filter: blur(12px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px;
|
||||
cursor: zoom-out;
|
||||
animation: lightbox-in 0.2s ease;
|
||||
}
|
||||
|
||||
@keyframes lightbox-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.screenshot-lightbox-img {
|
||||
max-width: 90vw;
|
||||
max-height: 85vh;
|
||||
object-fit: contain;
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: 0 8px 40px oklch(0% 0 0 / 50%);
|
||||
cursor: default;
|
||||
animation: lightbox-zoom 0.25s ease;
|
||||
}
|
||||
|
||||
@keyframes lightbox-zoom {
|
||||
from { transform: scale(0.92); opacity: 0; }
|
||||
to { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
|
||||
.screenshot-lightbox-close {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid oklch(100% 0 0 / 20%);
|
||||
background: oklch(0% 0 0 / 50%);
|
||||
color: oklch(100% 0 0 / 80%);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.screenshot-lightbox-close:hover {
|
||||
background: oklch(0% 0 0 / 70%);
|
||||
color: oklch(100% 0 0 / 100%);
|
||||
}
|
||||
|
||||
.screenshot-lightbox-arrow {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid oklch(100% 0 0 / 20%);
|
||||
background: oklch(0% 0 0 / 40%);
|
||||
color: oklch(100% 0 0 / 80%);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.screenshot-lightbox-arrow:hover {
|
||||
background: oklch(0% 0 0 / 60%);
|
||||
color: oklch(100% 0 0 / 100%);
|
||||
}
|
||||
|
||||
.screenshot-lightbox-prev {
|
||||
left: 16px;
|
||||
}
|
||||
|
||||
.screenshot-lightbox-next {
|
||||
right: 16px;
|
||||
}
|
||||
|
||||
.screenshot-lightbox-counter {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
color: oklch(100% 0 0 / 60%);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.screenshot-placeholder {
|
||||
aspect-ratio: 16/10;
|
||||
border-radius: var(--radius-lg);
|
||||
@@ -365,12 +534,6 @@
|
||||
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;
|
||||
@@ -405,6 +568,57 @@
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
/* ── Install guide inline (compact) ──────────────────── */
|
||||
.install-guide-inline {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
margin-top: 12px;
|
||||
padding: 8px 10px;
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px dashed oklch(74% 0.2 45 / 15%);
|
||||
background: oklch(74% 0.2 45 / 3%);
|
||||
}
|
||||
|
||||
:root.light .install-guide-inline {
|
||||
border-color: oklch(58% 0.22 45 / 15%);
|
||||
background: oklch(58% 0.22 45 / 3%);
|
||||
}
|
||||
|
||||
.install-tip-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
padding: 3px 4px;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.install-tip-icon {
|
||||
flex-shrink: 0;
|
||||
font-size: 13px;
|
||||
width: 18px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.install-tip-format {
|
||||
flex-shrink: 0;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--accent);
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
background: oklch(74% 0.2 45 / 10%);
|
||||
}
|
||||
|
||||
.install-tip-text {
|
||||
color: var(--muted);
|
||||
min-width: 0;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* ── Project selector (for roadmap/changelog) ────────── */
|
||||
.project-selector {
|
||||
display: flex;
|
||||
|
||||
@@ -56,6 +56,20 @@ export interface ChangelogEntry {
|
||||
changes: BilingualArray;
|
||||
}
|
||||
|
||||
export interface InstallGuideItem {
|
||||
platform: string;
|
||||
icon: string;
|
||||
format: string;
|
||||
tip: string;
|
||||
}
|
||||
|
||||
export type InstallGuide = InstallGuideItem[];
|
||||
|
||||
export interface BilingualInstallGuide {
|
||||
zh: InstallGuide;
|
||||
en: InstallGuide;
|
||||
}
|
||||
|
||||
export interface Project {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -69,14 +83,14 @@ export interface Project {
|
||||
features: BilingualArray;
|
||||
tags: string[];
|
||||
icon: string;
|
||||
logo?: string;
|
||||
repoUrl: string;
|
||||
websiteUrl?: string;
|
||||
docsUrl?: string;
|
||||
npmUrl?: string;
|
||||
latestVersion: string;
|
||||
releaseDate: string;
|
||||
license: string;
|
||||
stars: number;
|
||||
forks: number;
|
||||
language: string;
|
||||
lastUpdated: string;
|
||||
recommended: boolean;
|
||||
@@ -88,6 +102,7 @@ export interface Project {
|
||||
changelog?: ChangelogEntry[];
|
||||
architecture?: BilingualText;
|
||||
screenshots?: string[];
|
||||
installGuide?: BilingualInstallGuide;
|
||||
}
|
||||
|
||||
export interface AboutData {
|
||||
|
||||