feat(ts): 实现 Polaris 可交互首页

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-02 23:53:28 +08:00
co-authored by Claude Opus 4.8
parent 95c3da0f71
commit c3d2a40c1f
71 changed files with 15613 additions and 46 deletions
@@ -0,0 +1,453 @@
# Polaris 首页实现规格
> 日期:2026-08-26
>
> 状态:用户已批准,可进入实施
>
> 实现分支:`feat/polaris-homepage`
## 1. 背景与目标
Polaris 是一个轻量、安静、高效的个人浏览器起始页,不是后台管理系统或信息密集型 Dashboard。
本次工作只建立推荐的项目结构并实现完整可交互首页。用户验收首页视觉和交互后,才会明确决定是否继续实现分类页及第一阶段其余目标。
## 2. 范围
### 2.1 本次实现
- 初始化 Next.js App Router 项目;
- 建立 Feature-first 混合目录;
- 整理现有设计文档和资产位置;
- 实现低饱和青蓝 CSS 渐变背景与浅色玻璃视觉;
- 实现可折叠且持久化的桌面侧栏;
- 实现实时时钟、日期和分时问候语;
- 实现 Google、Bing、GitHub、Local 四种严格分离的搜索模式;
- 实现 Local 搜索结果、键盘导航和空状态;
- 实现 8 个常用链接;
- 实现统一图标 fallback;
- 建立本地 JSON、service、类型与数据校验边界;
- 完成单元测试、组件交互测试、静态构建和真实页面视觉检查。
### 2.2 本次不实现
- 正式分类页;
- Go API 与 PostgreSQL;
- 管理页或隐藏管理入口;
- 深色模式;
- 移动端与平板布局;
- 自定义背景上传;
- 自定义搜索引擎、URL 直达、AI 搜索;
- 加载骨架和后端错误重试。
## 3. 已确认决策
| 决策项 | 结论 |
| ---------- | ----------------------------------------------------------------- |
| 首页范围 | 完整可交互首页 |
| 目录方案 | Feature-first + 共享基础层 |
| 分支粒度 | 项目骨架与首页位于同一个 `feat/polaris-homepage` 分支 |
| 文档整理 | 三份文档迁入 `docs/design/`,设计图迁入 `docs/assets/` |
| Logo | 现有占位 Logo 迁入 `public/brand/`,不追加品牌设计 |
| 运行时背景 | CSS 低饱和青蓝渐变;设计图只作构图参考 |
| 外部搜索 | Google、Bing、GitHub 在当前标签页跳转 |
| 本地链接 | Local 结果与常用链接在新标签页打开 |
| 排序模型 | `sort` 管理分类内顺序;可选 `pinnedSort` 独立管理首页常用链接顺序 |
| 分类导航 | 首页验收阶段展示但禁用,避免进入未实现路由;分类页完成后启用 |
| 注释 | 关键规则、非显然决策、边界条件和阶段性限制使用简洁中文注释 |
| 提交时机 | 分支全部目标完成并验证后再提交 |
## 4. 技术基线
- Next.js App Router;
- React;
- TypeScript strict;
- Tailwind CSS v4,使用 CSS-first 配置;
- shadcn/ui,只引入实际使用的基础原语;
- lucide-react;
- simple-icons;
- pnpm;
- Vitest;
- Testing Library;
- ESLint;
- Prettier。
`next.config.ts` 使用 `output: "export"`,构建产物必须能作为纯静态文件部署。
不引入 Redux、Zustand、React Query、Zod、Storybook、Repository interface 或依赖注入容器。当前规模下这些抽象没有必要。
## 5. 推荐目录
```text
polaris/
├─ docs/
│ ├─ design/
│ │ ├─ Polaris 项目说明文档.md
│ │ ├─ Polaris 首页设计文档.md
│ │ ├─ Polaris 分类页设计文档.md
│ │ └─ 2026-08-26-Polaris-首页实现规格.md
│ └─ assets/
│ └─ Polaris页面设计图.png
├─ public/
│ ├─ brand/
│ │ └─ logo.svg
│ └─ icons/
├─ src/
│ ├─ app/
│ │ ├─ globals.css
│ │ ├─ layout.tsx
│ │ └─ page.tsx
│ ├─ components/
│ │ ├─ ui/
│ │ └─ shared/
│ ├─ features/
│ │ ├─ home/
│ │ │ ├─ components/
│ │ │ ├─ hooks/
│ │ │ └─ lib/
│ │ └─ navigation/
│ │ └─ components/
│ ├─ data/
│ │ ├─ categories.json
│ │ ├─ links.json
│ │ └─ settings.json
│ ├─ services/
│ │ ├─ categories.ts
│ │ ├─ links.ts
│ │ └─ settings.ts
│ ├─ types/
│ │ ├─ navigation.ts
│ │ ├─ search.ts
│ │ └─ settings.ts
│ ├─ lib/
│ │ └─ utils.ts
│ └─ test/
│ └─ setup.ts
├─ components.json
├─ eslint.config.mjs
├─ next.config.ts
├─ package.json
├─ pnpm-lock.yaml
├─ postcss.config.mjs
├─ prettier.config.mjs
├─ tsconfig.json
└─ vitest.config.ts
```
### 5.1 目录职责
- `app/`:路由、metadata、根布局、全局样式和页面组合;
- `features/`:按功能聚合组件、hooks 和专属纯函数;
- `components/ui/`:shadcn/ui 基础原语;
- `components/shared/`:跨功能复用组件,例如 `GlassPanel`、`SiteIcon`;
- `data/`:第一版本地 JSON;
- `services/`:页面与数据源之间的唯一访问边界;
- `types/`:跨功能共享的数据契约;
- `lib/`:真正通用且不依赖业务功能的工具;
- `test/`:全局测试环境与共享测试辅助代码。
测试文件与被测试实现就近放置,`src/test/` 不作为所有测试的集中目录。
## 6. 渲染与组件边界
`src/app/page.tsx` 保持为 Server Component,在构建时调用 service 层读取数据,再将可序列化数据传给页面组件。
```text
app/page.tsx
└─ AppShell
├─ Sidebar
│ ├─ SidebarHeader
│ └─ SidebarNav
└─ HomeContent
├─ ClockGreeting
├─ SearchBox
│ ├─ SearchProviderMenu
│ └─ LocalSearchResults
└─ QuickLinks
└─ QuickLinkCard
└─ SiteIcon
```
Client Component 边界仅用于浏览器状态:
- `AppShell`:侧栏折叠与持久化;
- `ClockGreeting`:当前时间;
- `SearchBox`:Provider、query、结果选中项和快捷键;
- `SiteIcon`:图标加载失败后的 fallback。
把 Server Component 作为 `AppShell` 的 `children` 传入,不因为外层存在 Client Component 就将全部展示代码改成客户端代码。
不为每个小标签或图标创建组件。只有存在独立状态、复用价值或清晰职责时才拆分。
## 7. 数据模型与数据流
### 7.1 数据规模
- 7 个分类;
- 开发工具 15 条;
- ChatGPT、Gmail、Notion、YouTube、Bilibili 5 条跨分类链接;
- 共 20 条链接;
- 8 条 `pinned` 常用链接;
- `sort` 只表示分类内顺序,可选 `pinnedSort` 只表示首页常用链接顺序。
首页常用链接固定按以下 `pinnedSort` 顺序展示:GitHub、ChatGPT、Gmail、Notion、Figma、YouTube、Vercel、Bilibili。该字段解决开发工具分类中 Vercel 位于 Figma 之前、但首页中 Figma 位于 Vercel 之前的双重排序需求。
设计资源、学习资料、生活服务保持无链接;AI 工具、效率工具和娱乐媒体因常用链接而有少量数据。
### 7.2 访问路径
1. `page.tsx` 调用 `getCategories()`、`getLinks()` 和 `getSettings()`;
2. service 层复制、排序和筛选数据;
3. 页面不直接 import JSON;
4. 首页只把所需的分类、全部搜索候选、8 条常用链接和设置传给客户端;
5. 未来接 Go API 时保持 service 的对外职责不变,只替换其内部数据来源。
### 7.3 数据校验
Vitest 必须校验:
- 必填字段类型;
- 分类和链接 ID 唯一;
- URL 合法;
- `categoryId` 必须指向现有分类;
- `sort` 与可选 `pinnedSort` 合法;
- Provider 配置有效;
- `pinned` 数量为 8,且每条 pinned 链接都有唯一 `pinnedSort`。
## 8. 视觉系统
### 8.1 背景
使用多层 CSS 渐变和柔和光斑模拟晨雾、天空和水面氛围,不把 `Polaris页面设计图.png` 作为运行时背景。
增加浅色遮罩和适度模糊,保证未来替换背景时仍具有可读性。
### 8.2 布局
- 侧栏外边距:`20px`;
- 展开宽度:`240px`;
- 折叠宽度:`72px`;
- 主内容视觉重心位于中上方;
- 使用 `minmax()` 与 `clamp()` 适配 1024px 以上桌面窗口;
- 搜索框最大宽度约 `760px`、高度约 `68px`;
- 常用链接最大四列、共两行,缩窄窗口时允许减少列数;
- 第一版不实现移动端抽屉侧栏。
### 8.3 Design tokens
在 `globals.css` 中定义 Polaris 语义变量,并通过 Tailwind v4 `@theme inline` 映射:
- 主文本、次级文本、弱文本;
- 主色、hover 色、柔和主色;
- 侧栏、搜索框、卡片表面;
- 玻璃边框;
- 软阴影、中等阴影;
- 圆角与模糊强度。
第一版只实现浅色值,但变量命名允许以后通过主题选择器覆盖。
### 8.4 动效与无障碍
- hover、focus 和折叠过渡约为 180ms;
- 不使用夸张缩放或弹跳;
- 尊重 `prefers-reduced-motion`;
- 所有图标按钮有可访问名称;
- 键盘焦点清晰;
- 搜索输入与结果使用合理的 combobox/listbox 语义;
- 禁用分类项使用 `aria-disabled` 并提供“分类页将在下一阶段提供”的轻量提示。
## 9. 首页交互
### 9.1 时钟与问候
问候规则:
```text
05:00 - 11:59 早上好
12:00 - 13:59 中午好
14:00 - 17:59 下午好
18:00 - 23:59 晚上好
00:00 - 04:59 夜深了
```
客户端按分钟更新;跨午夜后日期、星期和问候语同步变化。
### 9.2 Provider 状态
- 初始值来自 `settings.search.defaultProvider`;
- 用户切换后保存到 `localStorage`;
- 无效值或存储不可用时回退到设置默认值;
- 外部 Provider 与 Local 模式严格分离。
### 9.3 外部搜索
Google、Bing、GitHub:
- 输入先 `trim()`;
- 空内容不提交;
- 使用 `URLSearchParams` 编码;
- 在当前标签页跳转;
- 不显示 Local 结果下拉。
### 9.4 Local 搜索
匹配字段:
- 链接标题;
- 关键词;
- 描述;
- 分类名称;
- 分类 ID。
优先级:
```text
标题完全匹配
标题前缀或包含
关键词匹配
描述匹配
分类匹配
```
同分时按链接 `sort` 和标题稳定排序,最多返回 8 条。
交互:
- 默认选中第一项;
- `↑` / `↓` 移动;
- `Enter` 打开选中项;
- 鼠标点击直接打开;
- 结果在新标签页打开;
- `Esc` 清空输入并关闭面板;
- 无结果显示“没有匹配的网站”。
### 9.5 快捷键
- 首页加载后自动聚焦搜索框;
- Windows/Linux:`Ctrl+K`;
- macOS:`⌘K`;
- 快捷键阻止浏览器默认行为后聚焦;
- 监听器只存在于首页生命周期内。
### 9.6 侧栏
- 折叠状态保存到 `localStorage`;
- 存储不可用或值损坏时回退到设置默认值;
- hydration 后同步持久化值;
- 同步完成前禁用布局过渡,减少闪动;
- 首页项为当前选中态;
- 分类项本次显示但禁用,不进入 404。
### 9.7 常用链接
- 对 `pinned` 链接按 `pinnedSort` 排序取前 8 条;
- `pinned: true` 时必须提供唯一的 `pinnedSort`;
- 使用真实 `<a>`;
- `target="_blank"`;
- `rel="noopener noreferrer"`;
- 卡片仅显示图标与名称。
## 10. 图标策略
运行时优先级:
```text
自定义本地图标
明确映射的 Simple Icons
favicon
标题首字符文字占位
```
当前 20 条数据尽量命中显式 Simple Icons 映射,避免正常页面依赖远程 favicon。远程图标失败时不得显示破图。
侧栏分类图标使用 lucide-react,不参与站点图标 fallback。
## 11. 状态与故障处理
- 静态 JSON 无 Loading UI;
- 数据结构错误由测试和构建阶段阻止;
- `localStorage` 读写使用容错封装;
- favicon 失败显示文字占位;
- 空搜索不跳转;
- Local 无结果显示轻量空状态;
- 非法 Provider 回退到配置默认值;
- Server 初始值与 Client 首次渲染保持一致,避免 hydration error。
## 12. 注释规范
必须为以下非显然逻辑添加简洁中文注释,解释“为什么”:
- Local 搜索权重及稳定排序;
- Server/Client 边界选择;
- 图标 fallback 顺序;
- hydration 后同步 `localStorage`;
- 分类导航临时禁用;
- 数据校验中的跨分类引用规则。
普通 JSX、显然的变量赋值和框架惯用代码不添加复述式注释。
## 13. 测试设计
### 13.1 纯函数
- 问候语全部时间边界;
- 日期格式;
- 搜索 URL 与特殊字符编码;
- Local 搜索字段、权重、排序和数量上限;
- 图标引用解析;
- 数据结构及交叉引用。
### 13.2 组件交互
- 自动聚焦;
- Provider 严格分离;
- Provider 持久化与非法值回退;
- Local 键盘导航、Enter、Esc、点击和无结果;
- `Ctrl+K` / `⌘K`;
- 侧栏折叠、恢复和异常存储回退;
- 新标签页链接安全属性;
- 分类项禁用状态。
## 14. 验证与视觉验收
完成后必须运行:
```text
pnpm lint
pnpm typecheck
pnpm test
pnpm build
```
随后实际启动页面并检查:
- `1440 × 900`;
- `1920 × 1080`;
- `1024 × 768`。
检查项:
- 无横向溢出;
- 背景渐变层次和文本对比度;
- 玻璃透明度、边框与阴影一致;
- 侧栏展开/折叠比例;
- 搜索聚焦与 Provider 交互;
- Local 结果定位和键盘交互;
- 卡片排列、hover 和 focus;
- `prefers-reduced-motion`;
- 控制台无 hydration error 或运行时错误。
设计图用于构图和氛围参照,不要求像素级复制图片背景。
## 15. 完成定义
只有以下条件全部满足,`feat/polaris-homepage` 才完成:
1. 推荐目录和工程配置建立;
2. 文档及资产迁移完成,内部链接有效;
3. 完整可交互首页实现;
4. 自动化检查全部通过;
5. 真实页面完成三个尺寸的视觉检查;
6. 实际结果、视觉观察和已知偏差如实报告;
7. 未提前实现正式分类页;
8. 完成后再执行提交操作。