From 6fbb36d7a0ed0dd9c5ead4dbcad75eb9451c2766 Mon Sep 17 00:00:00 2001 From: Suyi Date: Sun, 2 Aug 2026 14:35:21 +0800 Subject: [PATCH 1/7] docs: propose web ui hardening --- .../harden-web-ui-system/.openspec.yaml | 2 + .../changes/harden-web-ui-system/design.md | 188 ++++++++++++++++++ .../changes/harden-web-ui-system/proposal.md | 44 ++++ .../specs/admin-dashboard/spec.md | 82 ++++++++ .../specs/feedback-command-system/spec.md | 87 ++++++++ .../specs/navigation-shell/spec.md | 74 +++++++ .../specs/topic-detail-experience/spec.md | 78 ++++++++ .../specs/user-management/spec.md | 104 ++++++++++ .../specs/web-ui-components/spec.md | 66 ++++++ .../specs/web-ui-forms/spec.md | 86 ++++++++ .../specs/web-ui-state/spec.md | 74 +++++++ .../specs/web-ui-theme/spec.md | 61 ++++++ .../changes/harden-web-ui-system/tasks.md | 58 ++++++ 13 files changed, 1004 insertions(+) create mode 100644 openspec/changes/harden-web-ui-system/.openspec.yaml create mode 100644 openspec/changes/harden-web-ui-system/design.md create mode 100644 openspec/changes/harden-web-ui-system/proposal.md create mode 100644 openspec/changes/harden-web-ui-system/specs/admin-dashboard/spec.md create mode 100644 openspec/changes/harden-web-ui-system/specs/feedback-command-system/spec.md create mode 100644 openspec/changes/harden-web-ui-system/specs/navigation-shell/spec.md create mode 100644 openspec/changes/harden-web-ui-system/specs/topic-detail-experience/spec.md create mode 100644 openspec/changes/harden-web-ui-system/specs/user-management/spec.md create mode 100644 openspec/changes/harden-web-ui-system/specs/web-ui-components/spec.md create mode 100644 openspec/changes/harden-web-ui-system/specs/web-ui-forms/spec.md create mode 100644 openspec/changes/harden-web-ui-system/specs/web-ui-state/spec.md create mode 100644 openspec/changes/harden-web-ui-system/specs/web-ui-theme/spec.md create mode 100644 openspec/changes/harden-web-ui-system/tasks.md diff --git a/openspec/changes/harden-web-ui-system/.openspec.yaml b/openspec/changes/harden-web-ui-system/.openspec.yaml new file mode 100644 index 0000000..d658936 --- /dev/null +++ b/openspec/changes/harden-web-ui-system/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-08-02 diff --git a/openspec/changes/harden-web-ui-system/design.md b/openspec/changes/harden-web-ui-system/design.md new file mode 100644 index 0000000..21553f4 --- /dev/null +++ b/openspec/changes/harden-web-ui-system/design.md @@ -0,0 +1,188 @@ +## Context + +本设计说明 `proposal.md` 所述系统性 Web UI 整改的实现方式,不重复其动机。当前 `apps/web/components.json` 已把 `~/components/ui` 指向 `apps/web/app/components/ui/`,该目录中的 Button、Dialog、DropdownMenu 等是已进入仓库并经过 CNode 品牌化修改的 shadcn/ui 源码;Dialog 等交互组件以 Radix UI 为基础。它们不是应被 npm 黑盒组件或另一套 primitive 全量替换的临时代码。 + +当前实现存在三类会放大逐页迁移风险的基础差异:`global.css` 中 light/dark token 与主题基础行为尚未形成可靠契约;组件使用了 `animate-in` 等 class,但没有完整的 Tailwind CSS v4 动画支持;路由内仍混用手写 `select`、分页、确认 Dialog、焦点与状态逻辑。话题发布/编辑页需要适合公共表单的自定义选择器,后台 GET 筛选则依赖原生表单提交语义。React Router SSR 还要求首屏 HTML 与 hydration 前后的控件结构、权限入口和主题状态一致。 + +现有 API 已负责话题编辑和内容治理授权,数据库与审计语义也已存在。本次只改变 Web 的呈现、交互和共享组件边界,前端权限矩阵仅决定入口是否展示,不能替代后端校验。 + +```mermaid +flowchart TD + RR[React Router SSR 与 route loader] --> Route[路由/领域组合层] + Route --> UI[仓库内 shadcn 源码层] + UI --> Radix[Radix 交互 primitives] + UI --> Native[浏览器原生控件] + Tokens[语义 token、主题与 Tailwind v4 动画] --> UI + Tokens --> Route + Route --> API[现有 API 与后端权限校验] + API --> PG[(现有 PostgreSQL)] + + Route -.负责.-> RouteOwn[Label、URL 状态、SSR 数据、未保存守卫] + UI -.负责.-> UIOwn[focus、reduced motion、overlay 滚动、safe area] +``` + +## Goals / Non-Goals + +**Goals:** + +- 在迁移路由前固定语义颜色、主题、动画和共享交互基线,使后续页面只组合稳定 primitive,而不重复修复同一问题。 +- 建立可审计的 shadcn registry 引入流程,保留仓库对源码及 CNode 品牌差异的所有权。 +- 为公共表单、后台筛选、危险动作、分页/空状态和命令界面提供职责清晰的共享 primitive。 +- 让话题动作在匿名用户、普通登录用户、作者、版主和管理员下具有确定且可测试的呈现,并保持后端为最终授权边界。 +- 使每一实施阶段都可通过 Web 测试和 SSR build 验证,最终可仅切换不可变 Web 镜像发布或回滚。 + +**Non-Goals:** + +- 不从 Radix UI 迁移到 Base UI,不执行 shadcn 全量重生成,也不覆盖全部现有组件。 +- 不重新设计 CNode 品牌、页面信息架构或全站视觉语言;只处理高影响、可复用、系统性的缺陷。 +- 不改 API 契约、后端权限、审计语义、会话角色模型或内容生命周期。 +- 不进行全站低优先级文案润色、图片尺寸清理、大列表虚拟化或逐像素视觉重做。 +- 不借本次整改重写 Markdown 样式等无关的既有页面样式。 + +## Decisions + +### 1. 保持 shadcn 源码复制与 Radix 基础 + +`apps/web/app/components/ui/` 继续作为项目拥有的 shadcn 源码层。registry 输出是待审查的上游参考,不是可无条件覆盖本地文件的生成产物。现有组件中的语义 token、圆角、阴影、中文无障碍文案和 CNode 品牌 class 均属于需要保留的本地差异。 + +在任何 registry 操作前,先验证一个与当前 Tailwind CSS v4、React 19、`new-york` 样式、非 RSC 和 Radix 组件基础兼容的 shadcn CLI 版本,并将 CLI 以精确版本写入 Web workspace 的 `devDependencies` 和 `pnpm-lock.yaml`。新增的运行时依赖也使用精确版本。之后只从 `apps/web` 通过 workspace 内已锁定的 CLI 执行,不使用未固定版本的 `pnpm dlx shadcn@latest`。 + +每个候选组件分别执行 `shadcn add --dry-run` 和 `shadcn add --diff`,审查文件、依赖和 import 差异后再引入。若 diff 试图切换到 Base UI、覆盖品牌差异或改写无关组件,则拒绝该输出并手工合并所需上游结构。禁止 `add --all`,也禁止以一条命令更新整个 `ui/` 目录。 + +**拒绝的替代方案:** + +- 迁移到 Base UI:会同时改变焦点、portal、状态属性和依赖模型,扩大回归面,且没有当前需求驱动。 +- 全量覆盖现有 shadcn 文件:会丢失已验证的品牌和行为差异,并使 review 无法按组件确认影响。 +- 临时运行 latest CLI:同一提交在不同时间可能得到不同源码或依赖,无法从 lockfile 复现。 +- 将 shadcn 组件改为 npm 黑盒依赖:失去源码级定制和审查能力,违背当前组件所有权模式。 + +### 2. 先固定语义 token 与主题,再迁移路由 + +第一实施阶段先校准 light/dark 下 `background`、`foreground`、`card`、`popover`、`primary`、`muted`、`accent`、`destructive`、`border`、`input`、`ring` 及其 foreground 配对。交互文字和控件边界以 WCAG AA 对比度为验收下限;品牌绿色不能仅因品牌一致性而承担低对比度正文色。组件使用语义 token,品牌 token 只用于确有品牌含义的装饰,不在路由中新增字面量颜色。 + +主题仍由 `root.tsx` 的同步 head script 在 hydration 前确定,客户端 theme store 在 hydration 后接管 `light`、`dark`、`system` 三态及系统主题监听。`color-scheme` 和浏览器 `theme-color` 必须与实际主题同步,但服务端首屏不得读取 `window`、`localStorage` 或 `matchMedia`。主题初始化只允许有一个持久化值来源,避免 React 首次 render 改写服务端结构。 + +Tailwind CSS v4 动画使用精确版本的 `tw-animate-css` CSS-first 支持,并由全局样式入口导入,使 Radix `data-[state]` 动画 class 真正生效。`prefers-reduced-motion: reduce` 下,共享层关闭非必要 transform/transition、取消平滑滚动,并保留即时状态变化。不会引入 Tailwind v3 plugin 配置作为兼容旁路。 + +**拒绝的替代方案:** + +- 先逐路由替换控件、最后修 token:同一组件会在主题修复后再次返工,且中间状态无法可靠做视觉回归。 +- 在各组件内复制 light/dark 字面量:增加主题分叉,无法统一校准对比度。 +- 为每个动画手写 keyframes:会重复 registry 组件已采用的状态 class 契约,维护成本更高。 +- 在 render 阶段读取浏览器主题:会造成 SSR 与客户端首帧不一致及 hydration 风险。 + +### 3. 按依赖与使用场景分四批引入 primitive + +组件按下列顺序引入,每个组件仍单独执行 dry-run/diff,每一批完成源码审查、类型检查和交互测试后才进入下一批: + +1. `Select` + `NativeSelect` + `Textarea`:先形成表单控件基线,并验证 Label、错误态、disabled、focus 和密度。 +2. `AlertDialog` + `Alert`:建立阻断式危险确认和非阻断说明/错误状态的不同语义。 +3. `Pagination` + `Empty`:统一导航语义和空结果表达。`ui/pagination.tsx` 只负责可访问的分页外观;现有领域 `components/Pagination.tsx` 可继续负责根据 `basePath`、page 和筛选参数生成 React Router URL,再组合 UI primitive,避免路由重复 URL 算法。 +4. `Command` + `RadioGroup`:最后迁移 CommandPalette 的键盘交互,并统一互斥选择组。Command 必须支持方向键、Enter、Escape、焦点归还和无结果状态;RadioGroup 必须有可感知组名和选项标签。 + +共享 primitive 统一拥有以下默认行为:一致的 `focus-visible` ring;disabled/pending 的不可重复触发;reduced-motion 降级;Dialog、AlertDialog、Sheet、Command overlay 的 portal、背景滚动锁定、可滚动内容上限和焦点归还;使用 `100dvh` 与 `env(safe-area-inset-*)` 避免移动端内容或操作区被遮挡。具体 route 不得通过删掉 outline、关闭 focus trap 或自行复制 fixed overlay 来覆盖这些默认值。 + +**拒绝的替代方案:** + +- 一次加入全部组件:依赖与源码 diff 混在一起,难以定位 token、动画或交互回归。 +- 让 Pagination primitive 同时读取所有业务 URL:会把列表筛选协议耦合进原子组件。 +- 继续使用通用 Dialog 表达所有破坏性确认:缺少 AlertDialog 的阻断语义,且容易让取消、焦点和提交中关闭行为不一致。 +- 将 safe-area 和 overlay 修复留给各页面:会在公共和后台 shell 中持续产生不同实现。 + +### 4. Select 与 NativeSelect 按交互语义分工 + +公共话题创建和编辑的分类字段使用 shadcn `Select`。该场景选项少但属于主要创作流程,需要一致的品牌弹层、键盘操作、禁用招聘选项说明及表单错误关联。Select 的 trigger 通过 FormControl 与 Label、description、error id 关联,路由负责把选中值写入表单状态和提交 payload。 + +后台 GET 筛选、审计筛选及其他高密度简单筛选使用 `NativeSelect`。原生 `name`、`defaultValue` 和 FormData 行为更适合无 JavaScript 也可表达的 GET form,并可让 URL 保持唯一筛选来源。loader 从 URL 解析并校验值,提交筛选时重置 page,分页继续保留其余 query 参数。NativeSelect 还用于后台表格内需要紧凑密度的简单枚举;不为这类场景引入 portal 和额外客户端状态。 + +路由/领域层拥有 Label 文案与 `htmlFor`/id 关联、autocomplete、字段说明和错误、URL 参数、loader 数据、受控值、提交 pending 以及未保存编辑守卫。创建/编辑页依据 dirty state 使用 React Router 导航阻断和浏览器离页提示;成功提交或显式放弃后解除守卫。所有浏览器 API 只在客户端 effect/事件中访问,SSR 首次 render 使用 loader 和确定性默认值。 + +**拒绝的替代方案:** + +- 全站只用 Select:后台 GET form 会增加不必要的 hydration、portal 和状态同步复杂度。 +- 全站只用原生 select:公共创作表单无法获得一致的弹层、错误和品牌交互。 +- 由 primitive 自动生成业务 Label 或 URL:会把领域文案和路由协议错误地下沉到共享层。 +- 用客户端 store 保存后台筛选:刷新、分享链接和返回导航时会丢失可追踪状态。 + +### 5. 话题动作按身份与风险分层 + +动作区始终优先展示主互动“收藏/取消收藏”和页内导航“查看回复”。普通用户动作与治理动作分开;所有治理动作收进单一“管理”菜单。作者对自己话题的“编辑话题”保持直接可见。管理员编辑他人话题放入“管理”菜单,避免与作者入口重复。版主不能仅凭版主身份编辑他人话题。角色可叠加:管理员或版主同时是作者时,保留作者的直接编辑入口,并额外展示其治理菜单。 + +| 身份/关系 | 直接操作 | 次级区域 | “管理”菜单 | +| --- | --- | --- | --- | +| 匿名用户 | 查看回复;收藏入口引导登录 | 无举报 | 不展示 | +| 普通登录用户,非作者 | 收藏/取消收藏、查看回复 | 举报话题 | 不展示 | +| 作者,无治理角色 | 收藏/取消收藏、查看回复、编辑话题 | 不提供举报自己的话题 | 不展示 | +| 版主,非作者 | 收藏/取消收藏、查看回复 | 举报不作为治理入口 | 置顶/取消置顶、高亮/取消高亮、删除帖子 | +| 版主,同时为作者 | 收藏/取消收藏、查看回复、编辑话题 | 无 | 置顶/取消置顶、高亮/取消高亮、删除帖子 | +| 管理员,非作者 | 收藏/取消收藏、查看回复 | 无 | 编辑话题、置顶/取消置顶、高亮/取消高亮、删除帖子 | +| 管理员,同时为作者 | 收藏/取消收藏、查看回复、编辑话题 | 无 | 置顶/取消置顶、高亮/取消高亮、删除帖子 | + +“普通登录用户”在此指没有作者关系且没有治理能力的登录用户;身份组合按能力并集呈现,不通过互斥的角色优先级丢失作者入口。前端基于 SSR session/loader 已提供的 admin、moderator 与作者关系得到同一首屏结果,但每个 mutation 仍由现有 API 再次授权。 + +删除帖子必须使用 `AlertDialog`,标题、目标话题和后果均明确为“删除帖子”,确认按钮使用 destructive variant。请求 pending 时禁止重复提交和关闭确认框,成功后按现有路由语义 revalidate 或导航,失败保留上下文并通过可感知反馈说明原因。置顶和高亮是可逆治理动作,不强制二次确认,但必须有 pending、成功/失败反馈。后台物理删除、任务级批量删除、用户 block/mute 及其他高风险动作继续按实际后果使用明确目标和影响说明;物理删除不得与软删除共用模糊文案。 + +**拒绝的替代方案:** + +- 将编辑、置顶、高亮、删除继续平铺:高频互动和低频治理同权重,移动端也容易误触。 +- 把作者编辑藏入管理菜单:降低核心内容维护动作的可发现性,并误把作者能力表现成治理能力。 +- 向版主展示编辑他人话题:扩大既有权限边界。 +- 仅用 toast 或浏览器 `confirm()` 确认删除:无法稳定表达目标、后果、焦点和 pending 状态。 +- 依赖隐藏按钮实现授权:客户端呈现不是安全边界,直接请求仍必须由 API 拒绝。 + +### 6. 共享层与路由层保持明确所有权 + +共享层只实现跨页面不应变化的机制:token 消费、控件状态样式、focus-visible、键盘基础交互、reduced motion、overlay scroll lock、safe-area、Alert/Empty 语义和 live region 基础。公共与后台 shell 组合 skip link、main landmark、heading 层级、active navigation 和移动安全间距。 + +路由层保留会随业务变化的责任:字段 Label 与校验关系、URL-backed tabs/filters、loader 数据、权限与对象关系、mutation/revalidate、hydration-safe 默认值,以及编辑器 dirty state 和未保存离开守卫。CommandPalette 的命令集合与导航目标也属于领域层,`Command` primitive 只提供交互模型。 + +**拒绝的替代方案:** + +- 创建一个知道所有 route、角色和表单的全局 UI store:会复制 React Router URL/loader 状态,并增加 SSR 同步源。 +- 让每个 route 自行决定 focus、motion 和 overlay 规则:相同行为会再次漂移。 +- 把权限矩阵写进纯视觉 primitive:primitive 无法获得完整领域关系,也不应成为授权层。 + +## Risks / Trade-offs + +- [语义 token 调整会影响全站而非单一路由] → 先建立 light/dark token 对照和代表性 surface 快照,再迁移 route;禁止在 route 用字面量颜色规避问题。 +- [固定 CLI 后仍可能与 registry 当前输出不同] → lockfile 作为可复现依据,每个组件保留 dry-run/diff 审查,不为追随最新模板而升级。 +- [品牌化合并可能遗漏上游无障碍属性] → review 时分别核对结构/ARIA/键盘行为与视觉 class,测试不得只比较截图。 +- [Select portal 在 SSR、移动键盘或 overlay 中出现定位问题] → 服务端保持确定 trigger 结构,portal 仅在客户端工作;覆盖窄屏、缩放、Dialog 内使用和 safe-area 测试。 +- [NativeSelect 与 Select 外观不可能完全一致] → 统一 token、控件高度、Label、错误和 focus 契约,接受原生下拉菜单由平台渲染以换取 GET/移动端可靠性。 +- [权限入口隐藏与后端能力短暂不一致] → 权限测试覆盖每个矩阵行;API 仍是最终授权边界,403 必须转为可感知失败反馈。 +- [未保存守卫可能阻止成功提交后的导航] → 成功 mutation 先清理 dirty/解除 blocker 再导航;分别测试站内导航、刷新、关闭标签页与取消离开。 +- [reduced-motion 全局规则过强会隐藏必要反馈] → 仅移除非必要时长和位移,保留即时的 open/closed、pending、错误和 focus 状态。 +- [分批实现会短期保留新旧控件并存] → 批次以共享契约和目标 route 清单为完成边界,不新增第三种临时控件;每批通过验证后再继续。 +- [只回滚 Web 可能恢复旧 UI 缺陷] → 这是可接受的短期退化;记录旧 Web digest,并保持 API/DB 未变以确保回滚兼容。 + +## Migration Plan + +实施和发布按“基础先于消费方”排序,每阶段只扩展 Web,且必须保持可构建状态: + +1. **Foundations**:固定并记录 shadcn CLI;逐项审查 registry 配置;修正语义 token、light/dark/system 行为、浏览器主题元数据、Tailwind CSS v4 动画和 reduced-motion 基线。 +2. **Primitives**:按四批顺序引入并品牌化组件,补齐 shared focus、overlay scroll、safe-area 和键盘测试;不迁移无关 route。 +3. **Global shell/forms**:修公共/后台 shell、CommandPalette、公共创建/编辑表单与后台 NativeSelect GET 筛选;URL 继续作为 tabs/filter 真相来源,浏览器状态只在客户端接管。 +4. **Topic/admin actions**:按权限矩阵重组话题动作,用 AlertDialog 处理 destructive 确认,并迁移后台高风险和批量治理确认,不改变 API endpoint 或审计含义。 +5. **Verification**:执行针对性组件和 route 测试、权限矩阵、键盘、focus、移动端 safe-area、light/dark、reduced-motion、SSR/hydration 和未保存守卫检查;随后运行 `pnpm verify`,构建并记录不可变 Web image SHA/digest,完成公开页面、发帖/编辑、话题治理和后台筛选 smoke。 + +```mermaid +flowchart LR + A[固定 CLI 与基础 token/主题/动画] --> B[四批 primitives] + B --> C[全局 shell 与表单] + C --> D[话题与后台动作] + D --> E[pnpm verify 与 Web smoke] + E --> F{验证通过?} + F -- 否 --> G[停止发布并修复当前阶段] + G --> E + F -- 是 --> H[部署新 CNODE_WEB_IMAGE digest] + H --> I{生产 Web smoke 通过?} + I -- 是 --> J[记录 Web digest 与结果] + I -- 否 --> K[仅恢复旧 CNODE_WEB_IMAGE digest] + K --> L[重新验证 Web health/smoke] +``` + +发布前记录当前 Web image SHA tag/digest。由于本 change 不含 API 或数据库变更,生产切换只更新 `CNODE_WEB_IMAGE`;API、worker 和 PostgreSQL 保持原版本。若 Web health、SSR、hydration、关键表单或权限 smoke 失败,恢复上一次成功的 Web image digest 并重新验证,不执行 API 回滚或数据库回滚。不得使用 `latest` 解析回滚目标。 + +## Database Change Audit + +本 change **没有数据库变更**:不修改 PostgreSQL schema、Drizzle schema 或 migration,不新增/修改表、列、索引、约束、seed/bootstrap、backfill、数据修复、数据清理、保留策略或字段语义,也不执行 MongoDB 到 PostgreSQL 迁移步骤。UI 的权限呈现、筛选控件和确认流程继续消费现有 API 与数据字段,不产生新的持久化状态。因此发布不需要 `db:push:pg`、`db:migrate`、数据备份恢复或数据库回滚;发生问题时仅回滚 Web 镜像。 diff --git a/openspec/changes/harden-web-ui-system/proposal.md b/openspec/changes/harden-web-ui-system/proposal.md new file mode 100644 index 0000000..92b5ff5 --- /dev/null +++ b/openspec/changes/harden-web-ui-system/proposal.md @@ -0,0 +1,44 @@ +## Why + +`apps/web` 已采用 shadcn/ui 源码所有权模式,但基础组件版本、语义 token、表单关联、危险操作确认、导航状态和页面组合仍存在系统性漂移;话题详情又将编辑、置顶、高亮和删除平铺展示。需要在同一个 UI 整改 change 中修复共享基础,再按独立 capability 收束前台与后台交互,避免继续逐页复制 Tailwind 控件和不一致行为。 + +## What Changes + +- 明确 `components.json` 与 `apps/web/app/components/ui/` 为现有 shadcn/ui 源码层,不切换组件基础;建立通过 CLI `--dry-run`、`--diff` 引入或更新单个 registry component 的治理方式。 +- 修复亮暗主题下的语义颜色对比度、缺失动画支持、focus、reduced motion、Dialog/Sheet 滚动与移动端 safe area 等共享基础行为。 +- 增加并品牌化 `Select`、`NativeSelect`、`Textarea`、`AlertDialog`、`Alert`、`Pagination`、`Empty`、`Command`、`RadioGroup` 等必要 primitives;不执行 `add --all` 或整体覆盖现有组件。 +- 前台发布与编辑话题的分类使用 shadcn `Select`;后台 GET 筛选和高密度简单筛选使用 `NativeSelect`。补齐 Label 关联、autocomplete、字段错误和异步状态播报。 +- 将话题详情的置顶、高亮和删除收进单一“管理”菜单;作者编辑保持直接可见,管理员编辑他人话题进入管理菜单,举报继续作为普通登录用户动作,删除保留二次确认。 +- 统一后台 block/mute、删除、批量治理等高风险操作的确认或可撤销策略,不改变既有后端权限和审计语义。 +- 为公共与后台 shell 增加 skip link、正确 heading、`aria-current`、URL-backed tabs;将 CommandPalette 改为可用方向键操作的 command interface,并修复 SSR render 阶段读取浏览器状态的问题。 +- 增加共享组件、权限矩阵、键盘、移动端、亮暗主题、SSR/hydration 和危险操作回归测试。 + +## Capabilities + +### New Capabilities + +- 无。 + +### Modified Capabilities + +- `web-ui-components`: 完成 shadcn 源码治理、必要 primitives、语义 token、动画、overlay、Pagination 和 Empty 组件约束。 +- `web-ui-forms`: 规定 `Select`/`NativeSelect` 使用边界、Label 关联、Textarea、autocomplete、字段错误与选择组语义。 +- `web-ui-theme`: 修复主题对比度、浏览器 color scheme/theme color 与 reduced motion 行为。 +- `web-ui-state`: 约束 URL-backed UI、SSR/hydration 安全和未保存编辑状态。 +- `navigation-shell`: 为公共与后台 shell 增加 skip navigation、active state 和移动端安全布局。 +- `feedback-command-system`: 将 CommandPalette 与异步反馈收束为可访问的 command/live-region 模型。 +- `topic-detail-experience`: 重组普通动作、作者编辑和管理员/版主管理菜单的视觉与交互层级。 +- `admin-dashboard`: 统一后台高风险与批量治理动作的确认、反馈和上下文保留。 +- `user-management`: 为后台 block/mute 等独立用户治理动作增加与风险相称的确认要求。 + +## Impact + +**In scope**:`apps/web/app/components/ui/`、全局 CSS/theme、公共与后台 Layout、CommandPalette、Pagination、表单与筛选控件、发布/编辑话题、话题详情管理动作及后台治理入口;对应 Web 测试与 OpenSpec specs。 + +**Out of scope / Non-goals**:不从 Radix 迁移到 Base UI;不重新设计 CNode 品牌;不整体覆盖现有 shadcn 源码;不修改 API、后端权限、审计、PostgreSQL schema 或数据;不在本次处理全部低优先级文案、图片尺寸和大列表虚拟化;不恢复 `../nodeclub/` 的 EJS 控件外观,只保留其内容治理权限与动作语义作为参考。 + +**Affected systems**:React Router SSR、Tailwind CSS v4、shadcn/Radix primitives、主题初始化、前后台表单和 mutation 反馈。高风险类别为共享颜色 token 的全站影响、overlay/focus 行为、SSR hydration、危险操作确认和角色权限下的入口可见性。 + +## Documentation Impact + +更新 OpenSpec 中 UI 组件、表单、主题、状态、导航和治理交互要求。若新增 registry component 或 CLI 版本约束需要维护者操作,则同步 `docs/development.md` 或 `docs/conventions.md`;`wiki/` 不记录前端组件实现细节,无需更新。 diff --git a/openspec/changes/harden-web-ui-system/specs/admin-dashboard/spec.md b/openspec/changes/harden-web-ui-system/specs/admin-dashboard/spec.md new file mode 100644 index 0000000..ef74bba --- /dev/null +++ b/openspec/changes/harden-web-ui-system/specs/admin-dashboard/spec.md @@ -0,0 +1,82 @@ +## ADDED Requirements + +### Requirement: 后台高风险动作保护 + +后台删除、真实删除、批量删除、确认违规并删除、角色或账号安全变更等不可逆或高影响动作 SHALL 在执行前要求确认。可完整恢复的批量状态治理动作 MAY 立即执行,但仅在页面提供明确、限时且可完成恢复的 undo 时免除事前确认;仅显示成功 toast 不构成 undo。 + +#### Scenario: 确认不可逆单项操作 + +- **WHEN** 管理员触发删除内容、重置凭证或其他不可逆单项目标操作 +- **THEN** 确认界面 MUST 明确显示动作、目标和主要影响 +- **AND** 初始焦点 MUST NOT 落在 destructive 确认按钮上 +- **AND** 取消 MUST 不发送 mutation 且焦点返回触发入口。 + +#### Scenario: 确认批量破坏性操作 + +- **WHEN** 管理员触发批量删除、批量确认违规或其他会影响多个对象的破坏性操作 +- **THEN** 确认界面 MUST 显示动作类型和目标数量 +- **AND** 在目标集合可概括时 MUST 显示当前筛选范围或目标摘要 +- **AND** 只有最终确认按钮使用 destructive 语义。 + +#### Scenario: 可恢复批量状态操作提供 undo + +- **WHEN** 后台对可完整恢复的批量状态操作选择免除事前确认 +- **THEN** 操作成功后 MUST 显示明确的 undo 动作、可撤销对象数量和可用时限 +- **AND** 用户在时限内触发 undo MUST 恢复所有成功处理对象的操作前状态 +- **AND** 部分失败时 MUST 说明成功、失败和可撤销的对象数量。 + +#### Scenario: 无可靠 undo 时要求确认 + +- **WHEN** 某项治理动作无法完整恢复、恢复会覆盖后续更改或 undo 时限无法保证 +- **THEN** 页面 MUST 在执行前要求确认 +- **AND** 不得以关闭通知、重新加载列表或手动执行相反动作冒充 undo。 + +### Requirement: 后台治理反馈与列表上下文 + +后台单项和批量治理动作 SHALL 提供可见且可播报的 pending、success、partial success 和 error 状态。操作完成或失败后,页面 MUST 保留当前 URL 表示的 tab、搜索、筛选、排序和分页上下文。 + +#### Scenario: 筛选列表中的单项治理成功 + +- **WHEN** 管理员在筛选后的后台列表执行单项治理并成功 +- **THEN** 页面 MUST 更新当前列表中的对象状态 +- **AND** URL 中的 tab、搜索、筛选、排序和页码 MUST 保持不变 +- **AND** 页面 MUST 播报包含动作结果的成功反馈。 + +#### Scenario: 批量治理部分成功 + +- **WHEN** 批量操作仅成功处理部分目标 +- **THEN** 页面 MUST 明确展示成功数、失败数和可重试范围 +- **AND** 已失败对象 MUST 保持可识别或可重新选择 +- **AND** 当前列表和筛选上下文 MUST 不回退到默认状态。 + +#### Scenario: 治理请求失败 + +- **WHEN** 后台治理请求失败 +- **THEN** 页面 MUST 清除 pending 状态并恢复可操作控件 +- **AND** 当前选择、筛选和分页上下文 MUST 保持可用 +- **AND** 页面 MUST 展示并播报错误,且不得把失败对象显示为成功状态。 + +### Requirement: 后台筛选、分页与空结果协同 + +后台列表的 GET 筛选 SHALL 使用与高密度管理界面相适配的原生选择行为,并 SHALL 与 URL-backed pagination 和 Empty 状态协同工作。 + +#### Scenario: 提交后台筛选 + +- **WHEN** 管理员选择筛选条件并提交 GET 筛选表单 +- **THEN** URL MUST 表示所选筛选条件 +- **AND** 结果 MUST 从第一页开始,除非 URL 明确指定仍有效的页码 +- **AND** 刷新和浏览器后退 MUST 恢复筛选控件与结果。 + +#### Scenario: 当前筛选无结果 + +- **WHEN** 当前后台筛选没有匹配记录 +- **THEN** 页面 MUST 展示说明当前筛选无结果的 Empty 状态 +- **AND** 提供清除筛选或返回完整列表的入口 +- **AND** 不得展示可导航到不存在结果页的分页控件。 + +#### Scenario: 删除当前页最后一项 + +- **WHEN** 治理动作使非第一页的当前页不再有任何结果 +- **THEN** 页面 MUST 保留当前筛选和排序条件 +- **AND** 导航到最近的有效页或展示明确空状态 +- **AND** 不得显示空表格同时保留指向无效页码的 current page 状态。 diff --git a/openspec/changes/harden-web-ui-system/specs/feedback-command-system/spec.md b/openspec/changes/harden-web-ui-system/specs/feedback-command-system/spec.md new file mode 100644 index 0000000..2ecb463 --- /dev/null +++ b/openspec/changes/harden-web-ui-system/specs/feedback-command-system/spec.md @@ -0,0 +1,87 @@ +## MODIFIED Requirements + +### Requirement: Command/search palette + +应用 SHALL 提供具有可访问 command 语义的 command/search 入口,用于搜索或跳转到话题、用户、发布流程、消息、内容页和有权限访问的后台页面。Command palette MUST 支持键盘移动、选择、关闭和焦点恢复,并 MUST 向辅助技术表达当前输入、结果集合、活动项和空状态。 + +#### Scenario: 从 Header 打开 command palette + +- **WHEN** 用户触发搜索/命令入口或键盘快捷键 +- **THEN** 打开包含有名称的搜索输入和快捷 actions 的 command/search 界面 +- **AND** 焦点 MUST 移入搜索输入或当前可操作区域。 + +#### Scenario: 键盘浏览命令 + +- **WHEN** command palette 已打开且存在可用结果 +- **THEN** 用户 MUST 能使用上、下方向键移动活动结果 +- **AND** Enter MUST 执行当前活动命令 +- **AND** 活动项 MUST 在视觉上可辨识并由辅助技术读取。 + +#### Scenario: 关闭后恢复焦点 + +- **WHEN** 用户按 Escape、选择命令或以关闭控件关闭 command palette +- **THEN** palette MUST 关闭 +- **AND** 未发生页面导航时焦点 MUST 返回打开 palette 的触发控件。 + +#### Scenario: 命令权限保持不变 + +- **WHEN** command palette 为匿名用户、普通用户、版主或管理员生成命令 +- **THEN** 结果 MUST 仅包含该用户原本可访问的目的地和动作 +- **AND** command 交互不得扩大任何后台或治理权限。 + +### Requirement: 搜索结果呈现 + +搜索结果 SHALL 使用一致的 results layout,包括 empty、loading、error 和 result 状态;每次异步状态变化 MUST 可见并通过适当的 status 或 live region 播报,且过期请求结果 MUST NOT 覆盖当前查询状态。 + +#### Scenario: 空搜索结果 + +- **WHEN** 当前搜索完成且没有结果 +- **THEN** UI 展示带引导的品牌 Empty 状态,而不是裸文本 +- **AND** 辅助技术 MUST 收到没有匹配结果的状态播报。 + +#### Scenario: 搜索加载中 + +- **WHEN** 用户输入有效查询且搜索请求尚未完成 +- **THEN** 结果区域 MUST 展示 loading 状态 +- **AND** loading 状态 MUST 被非打断式播报 +- **AND** 不得把上一次查询结果表达为当前查询结果。 + +#### Scenario: 搜索失败 + +- **WHEN** 当前搜索请求失败 +- **THEN** 结果区域 MUST 展示可理解的错误状态和可用的重试方式 +- **AND** 错误 MUST 以适当的 live region 播报 +- **AND** palette MUST 保留当前查询文本。 + +#### Scenario: 搜索结果更新 + +- **WHEN** 当前搜索请求成功返回一个或多个结果 +- **THEN** 页面 MUST 展示结果数量或等价状态并使首个结果可通过键盘到达 +- **AND** 辅助技术 MUST 收到结果已更新的播报。 + +## ADDED Requirements + +### Requirement: 全局异步反馈播报 + +创建、编辑、收藏、治理和批量操作的 pending、success 与 error 反馈 SHALL 以可见品牌反馈呈现,并 MUST 通过适当 live region 播报。相同操作进行中 MUST 防止重复提交,反馈文案 MUST 说明操作对象或结果。 + +#### Scenario: 异步动作进行中 + +- **WHEN** 用户触发异步动作且请求尚未完成 +- **THEN** 触发控件 MUST 表达 busy 或 disabled 状态并阻止重复提交 +- **AND** 页面 MUST 播报操作正在进行 +- **AND** 其他不冲突的页面导航和阅读能力 MUST 保持可用。 + +#### Scenario: 异步动作成功 + +- **WHEN** 异步动作成功 +- **THEN** 页面 MUST 更新可见状态并播报成功结果 +- **AND** 成功反馈 MUST 与当前对象和动作一致 +- **AND** pending 状态 MUST 被清除。 + +#### Scenario: 异步动作失败 + +- **WHEN** 异步动作失败 +- **THEN** 页面 MUST 保留操作前可恢复的内容和上下文 +- **AND** 展示并播报可理解的失败原因或重试提示 +- **AND** 触发控件 MUST 恢复为可再次操作状态。 diff --git a/openspec/changes/harden-web-ui-system/specs/navigation-shell/spec.md b/openspec/changes/harden-web-ui-system/specs/navigation-shell/spec.md new file mode 100644 index 0000000..760a8cb --- /dev/null +++ b/openspec/changes/harden-web-ui-system/specs/navigation-shell/spec.md @@ -0,0 +1,74 @@ +## ADDED Requirements + +### Requirement: 公共与后台 Skip navigation + +公共 shell 和后台 shell SHALL 在文档开头提供键盘可用的 skip link,使用户可跳过重复导航并直接到达当前页面主内容。 + +#### Scenario: 公共页面跳到主内容 + +- **WHEN** 键盘用户在公共页面首次按 Tab +- **THEN** 页面 MUST 显示“跳到主要内容”或等价 skip link +- **AND** 激活后焦点 MUST 移到当前页面唯一的 main 内容区域 +- **AND** 主标题或第一项主要内容 MUST 位于后续阅读顺序中。 + +#### Scenario: 后台页面跳过管理导航 + +- **WHEN** 键盘用户在后台页面激活 skip link +- **THEN** 焦点 MUST 跳过顶部导航、侧栏和移动端导航入口 +- **AND** 到达后台当前页面的 main 内容区域。 + +### Requirement: 导航当前项语义 + +公共与后台导航 SHALL 以可见样式和 `aria-current` 或等价语义标识当前页面;仅可展开菜单但不代表当前目的地的触发器 MUST NOT 被错误标记为当前页。 + +#### Scenario: 公共一级导航当前项 + +- **WHEN** 用户位于 `/about` +- **THEN** 指向 `/about` 的“关于”导航链接 MUST 具有当前页语义和可见 active state +- **AND** 其他一级导航链接 MUST NOT 同时标记为当前页。 + +#### Scenario: 后台子页面当前项 + +- **WHEN** 用户位于某个后台子页面 +- **THEN** 对应后台导航入口 MUST 具有当前页语义 +- **AND** 桌面侧栏、顶部导航和移动端导航 MUST 对同一路由表达一致的 active state。 + +#### Scenario: 分页导航当前项 + +- **WHEN** shell 内页面渲染分页导航 +- **THEN** 当前页码 MUST 使用当前页语义 +- **AND** shell 的页面导航 active state MUST 不因页码参数变化而丢失。 + +### Requirement: 页面 Landmark 与标题层级 + +公共与后台 shell SHALL 提供可识别的 header、navigation 和唯一 main landmark;每个页面 MUST 有一个描述当前页面目的的一级标题,后续标题 SHALL 按内容层级排列。 + +#### Scenario: 后台列表页面结构 + +- **WHEN** 辅助技术用户访问后台列表页 +- **THEN** 用户 MUST 能按 landmark 到达后台导航和 main 内容 +- **AND** main 内 MUST 有描述当前列表的一级标题 +- **AND** 筛选区、结果区和批量操作区不得以多个无层级的一级标题表示。 + +### Requirement: 移动端安全区域 + +公共与后台 shell 的固定 header、底部操作、导航 Sheet 和浮动控件 SHALL 避开设备 safe area,并 MUST 在虚拟键盘、窄屏和横屏条件下保持主要内容及关闭操作可触达。 + +#### Scenario: 带底部 safe area 的设备 + +- **WHEN** 页面运行在具有底部 safe-area inset 的移动设备 +- **THEN** 固定底部操作和浮动控件 MUST 与系统手势区域保持安全间距 +- **AND** 页面最后一项内容 MUST 能滚动到不被固定控件遮挡的位置。 + +#### Scenario: 移动导航 Sheet + +- **WHEN** 用户在移动端打开公共或后台导航 Sheet +- **THEN** Sheet 的关闭控件、首个导航项和最后一个导航项 MUST 位于安全区域内 +- **AND** 内容超过 viewport 时 MUST 可在 Sheet 内滚动 +- **AND** 背景页面 MUST 不随 Sheet 内容滚动。 + +#### Scenario: 虚拟键盘打开 + +- **WHEN** 用户在移动端导航或搜索 overlay 中聚焦输入框并打开虚拟键盘 +- **THEN** 输入框、当前结果和关闭方式 MUST 仍可见或可滚动到达 +- **AND** shell 不得产生 viewport 水平溢出。 diff --git a/openspec/changes/harden-web-ui-system/specs/topic-detail-experience/spec.md b/openspec/changes/harden-web-ui-system/specs/topic-detail-experience/spec.md new file mode 100644 index 0000000..31dcb7d --- /dev/null +++ b/openspec/changes/harden-web-ui-system/specs/topic-detail-experience/spec.md @@ -0,0 +1,78 @@ +## MODIFIED Requirements + +### Requirement: Topic action surface 分层 + +话题详情 action surface SHALL 将普通用户动作、作者直接编辑和管理动作分层展示。收藏/取消收藏、查看回复和举报 MUST 保持普通动作语义;作者编辑自己的话题 MUST 直接可见;admin 编辑他人话题 MUST 位于单一“管理”菜单;有既有权限的 mod 或 admin 执行置顶、高亮和删除时,这些动作 MUST 收纳在同一“管理”菜单中。该分层 MUST NOT 改变任何角色原有的动作权限。 + +#### Scenario: 普通互动和页内导航保持可见 + +- **WHEN** 话题详情正文渲染完成 +- **THEN** 收藏/取消收藏 MUST 作为普通互动操作展示 +- **AND** “查看回复” MUST 作为页内导航操作展示 +- **AND** 两者 MUST NOT 与置顶、高亮或删除平铺为同级管理按钮。 + +#### Scenario: 登录用户举报话题 + +- **WHEN** 有举报权限的普通登录用户查看他人话题 +- **THEN** 页面 MUST 提供举报这一普通用户动作 +- **AND** 举报 MUST NOT 被表达为管理权限或放入仅管理人员可见的“管理”菜单 +- **AND** 用户 MUST NOT 因可举报而看到置顶、高亮、删除或编辑他人话题入口。 + +#### Scenario: 作者直接编辑自己的话题 + +- **WHEN** 作者查看自己发布且可编辑的话题 +- **THEN** “编辑话题” MUST 作为直接可见操作展示 +- **AND** 作者不得仅因拥有编辑权限而看到置顶、高亮或删除等管理动作。 + +#### Scenario: 管理员编辑他人话题 + +- **WHEN** admin 查看他人发布且可编辑的话题 +- **THEN** “编辑话题” MUST 位于单一“管理”菜单内 +- **AND** 页面 MUST NOT 在菜单外重复平铺管理员编辑入口。 + +#### Scenario: 版主或管理员打开管理菜单 + +- **WHEN** mod 或 admin 查看其按既有权限可管理的话题并打开“管理”菜单 +- **THEN** 其有权限执行的置顶或取消置顶、高亮或取消高亮、删除动作 MUST 位于同一菜单 +- **AND** 无权限的动作 MUST 不可执行或不展示 +- **AND** 菜单组织方式 MUST NOT 扩大 mod、admin、作者或普通用户的既有权限矩阵。 + +#### Scenario: 删除话题需要确认 + +- **WHEN** 有权限的用户从“管理”菜单触发删除话题 +- **THEN** 页面 MUST 展示说明目标话题和删除影响的确认对话框 +- **AND** 只有最终确认操作 MUST 使用 destructive 语义 +- **AND** 用户取消时 MUST 不改变话题状态并将焦点返回管理入口。 + +#### Scenario: 移动端 action surface 可用 + +- **WHEN** 话题详情页在移动端渲染 +- **THEN** 收藏/取消收藏、查看回复和作者直接编辑 MUST 保持可触达 +- **AND** 举报和“管理”菜单 MAY 按普通动作与管理动作层级折叠,但不得消失或变成不可发现的死控件 +- **AND** 菜单内容 MUST 可滚动且不得被底部 safe area 遮挡。 + +## ADDED Requirements + +### Requirement: 话题动作异步反馈 + +话题收藏、举报、编辑跳转和管理 mutation SHALL 提供与动作一致的 pending、success 和 error 状态;管理操作完成后 MUST 保留用户在当前话题及回复位置的阅读上下文,除非删除成功后该话题不再可访问。 + +#### Scenario: 收藏状态更新 + +- **WHEN** 用户触发收藏或取消收藏且请求进行中 +- **THEN** 对应控件 MUST 防止重复触发并表达进行中状态 +- **AND** 成功后 MUST 更新收藏状态并播报结果 +- **AND** 失败时 MUST 保留操作前状态并播报错误。 + +#### Scenario: 管理操作失败 + +- **WHEN** 置顶、高亮或删除请求失败 +- **THEN** 页面 MUST 保留操作前的话题状态、滚动位置和可用动作 +- **AND** 用户 MUST 收到可见且可由辅助技术感知的错误反馈。 + +#### Scenario: 删除成功 + +- **WHEN** 用户确认删除且请求成功 +- **THEN** 页面 MUST 播报删除成功 +- **AND** 页面 MUST 导航到既有删除后目的地或展示既有 deleted 状态 +- **AND** 不得短暂恢复为可交互的未删除状态。 diff --git a/openspec/changes/harden-web-ui-system/specs/user-management/spec.md b/openspec/changes/harden-web-ui-system/specs/user-management/spec.md new file mode 100644 index 0000000..01eeac4 --- /dev/null +++ b/openspec/changes/harden-web-ui-system/specs/user-management/spec.md @@ -0,0 +1,104 @@ +## ADDED Requirements + +### Requirement: 独立用户治理动作确认 + +后台用户列表和公开用户页中的 block、mute、角色变更、重置密码及删除所有发言 SHALL 使用与风险相称的确认界面。确认 MUST 使用用户可读文案区分“屏蔽用户内容”与“禁言用户”,并 MUST NOT 改变既有权限、状态语义或审计规则。 + +#### Scenario: 确认屏蔽用户内容 + +- **WHEN** admin 对目标用户触发 block +- **THEN** 确认界面 MUST 显示目标用户并说明其公开内容将不再可见 +- **AND** MUST 说明该动作不等同于禁言 +- **AND** 用户取消时不得改变目标用户的 block 或 mute 状态。 + +#### Scenario: 确认禁言用户 + +- **WHEN** admin 对目标用户触发 mute +- **THEN** 确认界面 MUST 显示目标用户并说明其将无法新增话题和回复 +- **AND** MUST 说明已有内容不会仅因 mute 自动隐藏 +- **AND** 用户取消时不得改变目标用户的 mute 或 block 状态。 + +#### Scenario: 确认角色变更 + +- **WHEN** admin 授予或撤销目标用户角色 +- **THEN** 确认界面 MUST 显示目标用户、当前角色和变更后的角色 +- **AND** MUST 说明该角色变化影响的管理访问范围 +- **AND** 不得因确认界面而允许当前操作者执行原本无权限的角色变更。 + +#### Scenario: 确认重置密码 + +- **WHEN** admin 触发目标用户密码重置 +- **THEN** 确认界面 MUST 说明目标用户及现有凭证将受影响 +- **AND** 取消 MUST 不发送重置请求 +- **AND** 新凭证或一次性结果 MUST 仅按既有安全行为在成功后展示。 + +#### Scenario: 确认删除所有发言 + +- **WHEN** admin 从后台用户列表或公开用户页触发删除目标用户所有发言 +- **THEN** 确认界面 MUST 显示目标用户并明确说明将影响其全部话题和回复 +- **AND** 最终确认 MUST 使用 destructive 语义 +- **AND** 取消 MUST 不修改用户、话题、回复或计数状态。 + +#### Scenario: 恢复性操作保持明确 + +- **WHEN** admin 对已 block 或 mute 的用户执行 unblock 或 unmute +- **THEN** 页面 MUST 使用“恢复内容可见”或“解除禁言”等对应文案 +- **AND** 操作结果 MUST 只改变既有业务规则定义的目标状态 +- **AND** 不得把 unblock 与 unmute 合并为一个含义不明的恢复动作。 + +### Requirement: 用户治理反馈与上下文保留 + +用户治理动作 SHALL 提供可见且可播报的 pending、success 和 error 状态;请求进行中 MUST 防止重复提交。动作完成或失败后,后台用户列表 MUST 保留当前搜索、筛选和分页上下文,公开用户页 MUST 保留当前用户上下文。 + +#### Scenario: 搜索结果中治理成功 + +- **WHEN** admin 在带搜索或筛选条件的用户列表中确认治理动作且请求成功 +- **THEN** 页面 MUST 更新目标用户的可见状态并播报成功结果 +- **AND** URL、搜索词、筛选值和页码 MUST 保持不变 +- **AND** 页面 MUST NOT 返回未筛选的默认用户列表。 + +#### Scenario: 用户治理进行中 + +- **WHEN** admin 已确认用户治理动作且请求尚未完成 +- **THEN** 确认控件 MUST 显示进行中状态并禁止重复提交 +- **AND** 页面 MUST 播报操作正在进行 +- **AND** 不得提前显示目标状态已改变。 + +#### Scenario: 用户治理失败 + +- **WHEN** block、mute、角色变更、密码重置或删除所有发言失败 +- **THEN** 页面 MUST 保留操作前的用户状态和当前列表上下文 +- **AND** 展示并播报可理解的错误 +- **AND** 用户 MUST 能在修正条件后重试。 + +#### Scenario: 公开用户页治理完成 + +- **WHEN** admin 在公开用户页执行治理动作并成功 +- **THEN** 页面 MUST 保持在同一用户页并更新可见治理状态 +- **AND** 若动作使内容列表为空,页面 MUST 展示与该状态匹配的 Empty 说明 +- **AND** 不得将无内容误报为加载失败。 + +### Requirement: 用户批量治理确认与结果 + +批量解除禁言、批量恢复内容可见及其他批量用户治理动作 SHALL 在提交前展示目标数量和动作范围;执行后 MUST 分别报告成功、跳过和失败数量,并保持当前列表上下文。 + +#### Scenario: 确认批量解除禁言 + +- **WHEN** admin 选择多个用户并触发批量解除禁言 +- **THEN** 确认界面 MUST 显示所选用户数量并说明仅取消 mute 状态 +- **AND** MUST 说明不会取消 block 或恢复已删除内容 +- **AND** 取消时所选用户状态 MUST 保持不变。 + +#### Scenario: 确认批量恢复内容可见 + +- **WHEN** admin 选择多个用户并触发批量恢复内容可见 +- **THEN** 确认界面 MUST 显示所选用户数量并说明仅取消 block 状态 +- **AND** MUST 说明不会取消 mute 或恢复已删除内容 +- **AND** 取消时所选用户状态 MUST 保持不变。 + +#### Scenario: 批量用户治理部分失败 + +- **WHEN** 批量用户治理包含无权限、自操作或其他无法处理的目标 +- **THEN** 页面 MUST 展示成功、跳过和失败数量 +- **AND** 当前 tab、搜索、筛选和分页 MUST 保持不变 +- **AND** 响应反馈 MUST 允许管理员识别需要重新处理的目标范围。 diff --git a/openspec/changes/harden-web-ui-system/specs/web-ui-components/spec.md b/openspec/changes/harden-web-ui-system/specs/web-ui-components/spec.md new file mode 100644 index 0000000..d0cc870 --- /dev/null +++ b/openspec/changes/harden-web-ui-system/specs/web-ui-components/spec.md @@ -0,0 +1,66 @@ +## MODIFIED Requirements + +### Requirement: shadcn/ui 原子组件层 + +Web UI SHALL 继续使用仓库内源码所有权的 shadcn/ui 原子组件层,并 SHALL 提供 `button`、`input`、`label`、`card`、`badge`、`avatar`、`dropdown-menu`、`dialog`、`sheet`、`tabs`、`table`、`tooltip`、`skeleton`、`sonner`、`form`、`select`、`native-select`、`textarea`、`alert-dialog`、`alert`、`pagination`、`empty`、`command` 和 `radio-group`。这些组件 MUST 共享语义颜色、尺寸、焦点和禁用状态,且不得以另一套不兼容的基础组件替换现有层。 + +#### Scenario: 页面使用一致的语义组件 + +- **WHEN** 页面渲染按钮、输入框、选择器、反馈、分页或空状态 +- **THEN** 用户看到的颜色、圆角、密度、焦点和禁用状态 MUST 与同类品牌组件一致 +- **AND** 键盘与辅助技术用户 MUST 能识别控件的名称、角色和当前状态。 + +#### Scenario: 现有组件行为保持可用 + +- **WHEN** 共享原子组件新增能力或样式更新 +- **THEN** 已使用该组件的页面 MUST 保持原有可执行动作和导航结果 +- **AND** 不得因基础组件整体替换而改变既有业务行为。 + +## ADDED Requirements + +### Requirement: Overlay 焦点与滚动边界 + +Dialog、AlertDialog、Sheet、下拉菜单和 Command overlay SHALL 管理打开后的焦点、背景交互和滚动边界;内容超过 viewport 时,overlay 内容 MUST 可滚动,背景页面 MUST NOT 随 overlay 内滚动而移动。 + +#### Scenario: 键盘用户打开并关闭 Dialog + +- **WHEN** 键盘用户打开 Dialog 或 AlertDialog +- **THEN** 初始焦点 MUST 移入可操作内容或明确的安全默认控件 +- **AND** Tab 焦点 MUST 保持在 overlay 内 +- **AND** 用户按 Escape 或完成关闭后,焦点 MUST 返回触发控件。 + +#### Scenario: 长内容 overlay 在移动端滚动 + +- **WHEN** Dialog、Sheet 或 Command 内容高度超过移动端可视区域 +- **THEN** overlay 内部 MUST 可滚动到所有内容和操作 +- **AND** 页面背景 MUST 保持滚动锁定 +- **AND** 顶部、底部操作和系统安全区域 MUST 不遮挡可操作内容。 + +#### Scenario: 嵌套滚动到达边界 + +- **WHEN** 用户在可滚动 overlay 内到达内容顶部或底部并继续滚动 +- **THEN** 滚动 MUST NOT 穿透到背景页面。 + +### Requirement: Pagination 与 Empty 语义 + +共享 Pagination SHALL 提供当前页、可用页和上一页/下一页的可访问名称及状态;共享 Empty SHALL 说明当前结果为空,并在存在恢复路径时提供与当前上下文相关的操作。 + +#### Scenario: 数字分页表达当前页 + +- **WHEN** 后台列表或用户聚合页渲染数字分页 +- **THEN** 当前页 MUST 以 `aria-current="page"` 或等价语义标记 +- **AND** 不可用的上一页或下一页 MUST 不可触发 +- **AND** 翻页链接 MUST 保留当前搜索和筛选参数。 + +#### Scenario: 筛选结果为空 + +- **WHEN** 列表在当前搜索或筛选条件下没有结果 +- **THEN** 页面 MUST 展示品牌 Empty 状态并说明没有匹配结果 +- **AND** 在可清除筛选时 MUST 提供清除筛选或返回完整列表的可执行入口 +- **AND** 不得同时展示误导性的分页控件。 + +#### Scenario: 数据集本身为空 + +- **WHEN** 列表没有数据且未应用搜索或筛选条件 +- **THEN** Empty 状态 MUST 区分“尚无数据”与“加载失败” +- **AND** 仅在用户有权限且确有创建路径时展示创建操作。 diff --git a/openspec/changes/harden-web-ui-system/specs/web-ui-forms/spec.md b/openspec/changes/harden-web-ui-system/specs/web-ui-forms/spec.md new file mode 100644 index 0000000..fb99365 --- /dev/null +++ b/openspec/changes/harden-web-ui-system/specs/web-ui-forms/spec.md @@ -0,0 +1,86 @@ +## ADDED Requirements + +### Requirement: 选择控件使用边界 + +公开话题创建和编辑表单的分类字段 SHALL 使用品牌化、可访问的 Select 行为;后台以 GET 参数提交的筛选器及高密度简单筛选场景 SHALL 在适合时使用浏览器原生选择行为。两类控件 MUST 保留相同的字段名称、可选值和提交结果。 + +#### Scenario: 创建话题选择分类 + +- **WHEN** 用户在公开话题创建页打开分类选择器 +- **THEN** 选择器 MUST 展示品牌化触发控件、当前值和可用分类 +- **AND** 用户 MUST 能使用键盘打开选项、移动当前选项、确认选择并关闭列表 +- **AND** 焦点与已选择分类 MUST 对辅助技术可识别。 + +#### Scenario: 编辑话题保留分类 + +- **WHEN** 用户打开已有话题的编辑页 +- **THEN** 分类 Select MUST 显示该话题当前分类 +- **AND** 用户未更改分类时提交 MUST 保留原值。 + +#### Scenario: 后台 GET 筛选使用原生选择行为 + +- **WHEN** 管理员在后台列表使用状态、类型、排序或其他高密度简单筛选器 +- **THEN** 页面 SHALL 使用适合 GET 表单提交和浏览器键盘行为的原生选择控件 +- **AND** 提交后 URL MUST 包含所选筛选值 +- **AND** 返回、刷新和翻页后控件 MUST 恢复 URL 表示的当前值。 + +### Requirement: 字段名称、提示与错误关联 + +每个表单控件 SHALL 具有程序化关联的可见 Label;登录、注册和账号表单中的身份、密码及联系字段 MUST 提供正确的 autocomplete 提示。字段错误和补充说明 MUST 与对应控件关联,且无效状态 MUST 可由辅助技术识别。 + +#### Scenario: 点击 Label 聚焦字段 + +- **WHEN** 用户点击输入框、Textarea、Select、NativeSelect 或 RadioGroup 的可见 Label +- **THEN** 焦点 MUST 移到对应字段或选择组 +- **AND** 辅助技术读取该控件时 MUST 同时读取其名称。 + +#### Scenario: 登录字段提供 autocomplete + +- **WHEN** 登录或注册表单渲染用户名、邮箱、当前密码或新密码字段 +- **THEN** 字段 MUST 使用与用途匹配的 autocomplete 语义 +- **AND** 密码管理器不得因缺失或错误的字段用途而把新密码填入当前密码字段。 + +#### Scenario: 校验错误关联到字段 + +- **WHEN** 用户提交无效字段 +- **THEN** 错误信息 MUST 显示在对应字段附近 +- **AND** 控件 MUST 暴露无效状态并关联该错误信息 +- **AND** 键盘焦点 MUST 移到第一个无效字段或通过等价方式立即定位错误。 + +### Requirement: 多行输入与互斥选择语义 + +多行纯文本输入 SHALL 使用品牌 Textarea 行为;一组互斥选项 SHALL 使用单选组语义,并提供组名称、每个选项的名称、选中状态和键盘操作。 + +#### Scenario: Textarea 保持标签和错误状态 + +- **WHEN** 用户编辑签名、原因或其他多行纯文本字段 +- **THEN** Textarea MUST 保持可见 Label、可辨识 focus 状态和字段错误关联 +- **AND** 内容增长时不得遮挡相邻提交操作或产生 viewport 水平溢出。 + +#### Scenario: 键盘操作互斥选择组 + +- **WHEN** 键盘用户进入一组互斥选项 +- **THEN** 辅助技术 MUST 将其识别为一个有名称的单选组 +- **AND** 用户 MUST 能使用方向键在选项间移动并保持仅一个选项被选中。 + +### Requirement: 表单异步状态播报 + +表单提交、保存和服务端校验的 pending、success 与 error 状态 SHALL 以可见文本呈现,并 MUST 通过适当 live status 向辅助技术播报;pending 期间 MUST 防止同一提交重复触发。 + +#### Scenario: 提交进行中 + +- **WHEN** 用户提交表单且请求尚未完成 +- **THEN** 提交控件 MUST 显示进行中状态并禁止重复提交 +- **AND** 辅助技术 MUST 收到非打断式的进行中状态播报。 + +#### Scenario: 服务端返回字段错误 + +- **WHEN** 服务端返回可归属到具体字段的错误 +- **THEN** 页面 MUST 将错误关联到对应字段并保留用户已输入内容 +- **AND** 辅助技术 MUST 收到错误状态播报。 + +#### Scenario: 提交完成 + +- **WHEN** 保存成功或失败 +- **THEN** 页面 MUST 展示并播报对应结果 +- **AND** 成功状态不得与仍在进行中的状态同时存在。 diff --git a/openspec/changes/harden-web-ui-system/specs/web-ui-state/spec.md b/openspec/changes/harden-web-ui-system/specs/web-ui-state/spec.md new file mode 100644 index 0000000..eecef4e --- /dev/null +++ b/openspec/changes/harden-web-ui-system/specs/web-ui-state/spec.md @@ -0,0 +1,74 @@ +## ADDED Requirements + +### Requirement: 可分享 UI 状态由 URL 表示 + +影响当前数据集或页面视图的 tab、搜索、筛选、排序和分页状态 SHALL 由 URL pathname 或 search parameters 表示,而不是仅保存在组件内存中。打开相同 URL MUST 恢复相同的可分享 UI 状态。 + +#### Scenario: 切换列表 tab + +- **WHEN** 用户在公开页面或后台列表切换 tab +- **THEN** URL MUST 更新为所选 tab +- **AND** 刷新页面后 MUST 继续展示该 tab +- **AND** 浏览器后退 MUST 恢复切换前的 tab 和结果。 + +#### Scenario: 修改筛选后翻页 + +- **WHEN** 用户应用筛选、排序或搜索条件后翻页 +- **THEN** 下一页 URL MUST 同时保留这些条件 +- **AND** 复制该 URL 到新会话 MUST 得到相同筛选和页码状态。 + +#### Scenario: URL 包含无效 UI 状态 + +- **WHEN** URL 包含不支持的 tab、筛选值或页码 +- **THEN** 页面 MUST 使用明确的安全默认值或展示可理解的无结果状态 +- **AND** 不得渲染互相矛盾的选中状态。 + +### Requirement: SSR 首次渲染不得依赖浏览器专属状态 + +SSR 页面及其客户端首次 render SHALL 从相同的路由数据、URL 和确定性默认值生成相同结构。组件 MUST NOT 在 render 阶段读取 `window`、`document`、`localStorage`、viewport 或媒体查询来决定首次呈现的分支;浏览器专属增强 MUST 在 hydration 后接管,且不得改变既有业务状态。 + +#### Scenario: 直接请求含筛选参数的页面 + +- **WHEN** 用户直接请求带 tab、筛选或分页参数的 URL +- **THEN** SSR HTML MUST 已反映这些 URL 状态 +- **AND** hydration 后选中项、结果和分页 MUST 与 SSR HTML 一致 +- **AND** 控制台不得出现 hydration mismatch。 + +#### Scenario: 浏览器专属 UI 偏好 + +- **WHEN** 某个非关键 UI 偏好只能从浏览器存储或媒体查询获得 +- **THEN** SSR 与客户端首次 render MUST 使用相同的确定性默认结构 +- **AND** hydration 后应用该偏好时 MUST 不丢失焦点、输入内容或路由状态。 + +#### Scenario: 无 JavaScript 首屏结构 + +- **WHEN** 服务端渲染公共或后台页面 +- **THEN** 主标题、主要内容和基于 URL 的当前状态 MUST 存在于首屏 HTML +- **AND** 不得以仅客户端占位分支替代这些内容。 + +### Requirement: 未保存内容离开保护 + +话题创建、话题编辑、回复编辑及其他会产生长文本草稿的页面 SHALL 在内容相对初始值发生变化且尚未成功保存时保护用户免于意外离开。 + +#### Scenario: 站内导航离开脏表单 + +- **WHEN** 用户修改标题、分类或正文后触发站内导航 +- **THEN** 页面 MUST 在离开前说明存在未保存内容并要求用户确认 +- **AND** 用户取消时 MUST 留在当前页面且输入内容保持不变。 + +#### Scenario: 刷新或关闭含未保存内容的页面 + +- **WHEN** 表单存在未保存内容且用户刷新、关闭标签页或离开站点 +- **THEN** 浏览器 MUST 展示其支持的离开警告 +- **AND** 未确认离开时页面内容 MUST 保持不变。 + +#### Scenario: 无修改或保存成功后离开 + +- **WHEN** 表单未发生变化或最近一次提交已成功保存当前内容 +- **THEN** 后续导航 MUST 不再显示未保存内容警告。 + +#### Scenario: 提交进行中触发离开 + +- **WHEN** 保存请求仍在进行且用户尝试离开 +- **THEN** 页面 MUST 将当前内容视为尚未保存 +- **AND** 只有收到成功结果后才能解除离开保护。 diff --git a/openspec/changes/harden-web-ui-system/specs/web-ui-theme/spec.md b/openspec/changes/harden-web-ui-system/specs/web-ui-theme/spec.md new file mode 100644 index 0000000..0b668b7 --- /dev/null +++ b/openspec/changes/harden-web-ui-system/specs/web-ui-theme/spec.md @@ -0,0 +1,61 @@ +## ADDED Requirements + +### Requirement: 亮暗主题可读对比度 + +light 与 dark 主题中的文本、链接、边框、焦点指示、状态提示和交互控件 SHALL 使用可辨识的语义颜色组合。普通文本与背景的对比度 MUST 至少为 4.5:1,大号文本 MUST 至少为 3:1;交互控件边界、状态图形和 focus 指示与相邻颜色的对比度 MUST 至少为 3:1。 + +#### Scenario: light 主题阅读和操作 + +- **WHEN** 用户以 light 主题查看正文、muted metadata、链接、表单和 destructive 提示 +- **THEN** 每类内容 MUST 达到其适用的最低对比度 +- **AND** 链接、错误和 destructive 状态 MUST NOT 仅依赖难以辨识的浅色差表达。 + +#### Scenario: dark 主题阅读和操作 + +- **WHEN** 用户以 dark 主题查看卡片、popover、表单、disabled 状态和 focus 指示 +- **THEN** 前景与对应背景 MUST 达到其适用的最低对比度 +- **AND** disabled 与 enabled 状态 MUST 可区分,但 disabled 文本不得因此变成不可读。 + +#### Scenario: 键盘焦点在两种主题中可见 + +- **WHEN** 键盘用户在 light 或 dark 主题中移动焦点 +- **THEN** 当前交互元素 MUST 显示连续且可辨识的 focus 指示 +- **AND** focus 指示不得被 overflow 裁切或仅依赖颜色极接近的阴影。 + +### Requirement: 浏览器主题集成 + +页面 SHALL 根据当前生效主题向浏览器声明匹配的 color scheme 和 theme color;system 模式变化后,浏览器 chrome 与原生表单控件 MUST 跟随实际生效的 light 或 dark 主题。 + +#### Scenario: 固定 dark 主题 + +- **WHEN** 用户选择 dark 主题 +- **THEN** 浏览器 color scheme MUST 表示 dark +- **AND** theme color MUST 使用与 dark 页面背景协调且可识别的颜色。 + +#### Scenario: system 主题实时变化 + +- **WHEN** 用户选择 system 且操作系统从 light 切换到 dark +- **THEN** 页面主题、浏览器 color scheme 和 theme color MUST 在不刷新页面的情况下同步为 dark +- **AND** 原生选择器、滚动条及浏览器提供的控件 MUST 使用匹配的配色。 + +### Requirement: Reduced motion + +当用户声明 `prefers-reduced-motion: reduce` 时,Web UI SHALL 移除非必要的平滑滚动、位移、缩放、旋转和长时过渡,同时保留状态变化及操作结果的可理解性。 + +#### Scenario: reduced motion 下打开 overlay + +- **WHEN** 用户启用 reduced motion 并打开 Dialog、Sheet、DropdownMenu 或 Command overlay +- **THEN** overlay MUST 立即出现或仅使用最短的非位移动画 +- **AND** 焦点移动和可见状态 MUST 保持正确。 + +#### Scenario: reduced motion 下页内导航 + +- **WHEN** 用户启用 reduced motion 并触发回到顶部或锚点导航 +- **THEN** 页面 MUST 不执行持续平滑滚动 +- **AND** 最终滚动位置和焦点目标 MUST 与普通模式一致。 + +#### Scenario: 未声明 reduced motion + +- **WHEN** 用户未请求 reduced motion +- **THEN** 状态过渡 MAY 使用品牌动画 +- **AND** 动画 MUST NOT 阻止用户在过渡期间取消、关闭或继续操作。 diff --git a/openspec/changes/harden-web-ui-system/tasks.md b/openspec/changes/harden-web-ui-system/tasks.md new file mode 100644 index 0000000..5266e01 --- /dev/null +++ b/openspec/changes/harden-web-ui-system/tasks.md @@ -0,0 +1,58 @@ +## 1. MVP:shadcn 工具链与主题基础 + +- [ ] 1.1 为 `apps/web` 选择并锁定与 React 19、Tailwind CSS v4、Radix 和当前 `components.json` 兼容的 shadcn CLI 精确版本,修复 `shadcn info` 的 Zod/MCP 解析失败,并验证 CLI 不会切换 Base UI 或改写无关文件。 +- [ ] 1.2 分别对 `select`、`native-select`、`textarea`、`alert-dialog`、`alert`、`pagination`、`empty`、`command`、`radio-group` 执行 registry `--dry-run`/`--diff` 审查,记录依赖、目标文件和必须保留的 CNode 品牌差异。 +- [ ] 1.3 重构 light/dark 语义 token,消除 `text-cnode-ink` 等暗色低对比组合,为正文、链接、muted、状态、边框和 focus 建立可验证的 WCAG AA 配对。 +- [ ] 1.4 让主题初始化、`color-scheme` 和 `theme-color` 在 light/dark/system 下同步,确保 SSR 首屏不读取浏览器 API 且 system 主题变化可实时反映。 +- [ ] 1.5 接入 Tailwind CSS v4 兼容的动画支持,移除共享组件中的 `transition-all`,为 CSS 动画、平滑滚动和程序化滚动增加 reduced-motion 降级。 + +## 2. MVP:共享 UI primitives + +- [ ] 2.1 引入并品牌化 `Select`、`NativeSelect` 和 `Textarea`,统一高度、Label/description/error 关联、disabled、focus 和前后台密度行为。 +- [ ] 2.2 引入并品牌化 `AlertDialog` 和 `Alert`,定义安全默认焦点、destructive 最终确认、pending 禁止关闭以及 status/alert 播报行为。 +- [ ] 2.3 引入并品牌化 `Pagination` 和 `Empty`,让领域 Pagination 保留 URL 生成职责,同时提供 nav、`aria-current`、不可用状态和筛选空结果恢复入口。 +- [ ] 2.4 引入并品牌化 `Command` 和 `RadioGroup`,覆盖方向键/Enter/Escape、焦点归还、空结果、组名和单选状态语义。 +- [ ] 2.5 统一 Dialog、AlertDialog、Sheet、DropdownMenu 和 Command overlay 的滚动锁定、移动端 max-height、overscroll containment、`100dvh` 与 safe-area 默认值。 +- [ ] 2.6 增加共享 primitive 测试,覆盖亮暗主题 class、键盘、focus、disabled/pending、reduced motion、长 overlay 和移动端安全区域。 + +## 3. MVP:表单与编辑状态 + +- [ ] 3.1 将发布话题和编辑话题的分类改为 shadcn `Select`,关联可见 Label,并覆盖当前分类、键盘选择、招聘分类可用/禁用说明及提交值测试。 +- [ ] 3.2 将后台 topics、audit、reports 等 GET/高密度简单筛选迁移到 `NativeSelect`,补齐 id/Label 或 accessible name,并保证 URL、返回、刷新和分页恢复状态。 +- [ ] 3.3 将签名、举报说明和后台普通多行字段迁移到 `Textarea`;保留 MarkdownEditor 领域封装并为其 textarea 增加 focus-within 与可见焦点。 +- [ ] 3.4 补齐登录、注册、找回/重置密码、设置和招聘表单的 Label、name、type、autocomplete、spellcheck、字段错误关联及首个错误定位。 +- [ ] 3.5 为 Turnstile、上传、搜索和表单 mutation 增加可见且可播报的 pending/success/error 状态,防止重复提交并保留失败时输入。 +- [ ] 3.6 为话题创建、话题编辑和回复编辑增加 dirty state、站内导航阻断与浏览器离页提示,并验证保存成功或显式放弃后解除保护。 + +## 4. Feature-complete:Shell、命令与 URL 状态 + +- [ ] 4.1 为公共和后台 Layout 增加首个可聚焦 skip link、唯一 main target 与稳定 focus,修复 AuthShell 移动端一级标题和 Card section heading 层级。 +- [ ] 4.2 为公共导航、后台导航、用户 tabs 和分页增加可见 active state 与 `aria-current`,确保 query 参数变化不破坏当前路由识别。 +- [ ] 4.3 将后台 bans/settings 等可分享 tabs 与列表筛选状态写入 URL,保证刷新、复制链接和浏览器前进/后退恢复同一视图。 +- [ ] 4.4 移除 `zone.jobs` 等 SSR render 阶段的 `window`/viewport 读取,由 loader URL 或确定默认值生成首屏,并增加 hydration 一致性测试。 +- [ ] 4.5 使用 `Command` 重构 CommandPalette 的输入、结果、空状态和关闭控件,覆盖权限过滤、方向键选择、Enter 导航、Escape 和焦点归还。 +- [ ] 4.6 将共享 Pagination 与 Empty 迁移到首页以外的数字分页、搜索、用户聚合和后台空结果场景,验证窄屏无横向溢出且筛选参数不丢失。 + +## 5. Feature-complete:话题动作层级 + +- [ ] 5.1 建立匿名、普通用户、作者、版主、管理员及角色叠加的 topic action presentation helper,并用表格驱动测试锁定既有权限矩阵。 +- [ ] 5.2 重组 TopicActions:收藏/查看回复保持普通动作,举报不进入治理菜单,作者编辑直接可见,管理员编辑他人及置顶/高亮/删除收进单一“管理”菜单。 +- [ ] 5.3 将删除帖子改为 `AlertDialog`,明确目标和影响,pending 时阻止重复提交/关闭,并让取消恢复焦点、失败保留状态、成功遵循既有删除后路由。 +- [ ] 5.4 增加桌面与移动端话题动作测试,覆盖置顶/取消、高亮/取消、管理菜单键盘操作、safe area、async feedback 和非授权入口不可见。 + +## 6. Feature-complete:后台与用户治理确认 + +- [ ] 6.1 盘点后台 topics、mod、keywords、bans、reports、users 的单项/批量 mutation,按不可逆、高影响、可逆三类建立确认矩阵;无可靠 undo 的动作统一要求事前确认。 +- [ ] 6.2 将软删除、真实删除、批量删除、确认违规删除、敏感词/IP 规则删除等破坏性动作迁移到 `AlertDialog`,显示目标或数量并区分软删除与物理删除后果。 +- [ ] 6.3 为后台 block/unblock、mute/unmute、角色变更、密码重置和删除所有发言增加独立确认文案,确保 block 与 mute 不互相推导且恢复动作名称准确。 +- [ ] 6.4 统一后台单项与批量动作的 pending、partial success、error 和可播报反馈,防止重复提交并保留失败对象的可识别/重试范围。 +- [ ] 6.5 让用户与内容治理完成后保留 URL 中的 tab、搜索、筛选、排序和分页;删除当前页最后一项时回到最近有效页或明确空状态。 +- [ ] 6.6 增加后台和公开用户页治理测试,覆盖取消不发请求、目标/数量文案、安全默认焦点、partial failure、上下文保留和后端 403 反馈。 + +## 7. 验证与归档准备 + +- [ ] 7.1 对代表性公共、认证、创作、话题、用户和后台页面执行键盘/焦点/landmark/Label/live-region 自动化检查,并补充缺失回归测试。 +- [ ] 7.2 在 light、dark、system、reduced-motion、375px 移动端和大桌面下检查 token 对比度、overlay、Select、Command、分页、safe area 与无横向溢出。 +- [ ] 7.3 更新 `docs/development.md` 或 `docs/conventions.md` 中的 shadcn 精确版本、registry dry-run/diff 和禁止整体覆盖约定;确认 `wiki/` 无需同步。 +- [ ] 7.4 运行 `pnpm lint`、`pnpm typecheck`、`pnpm test`、`pnpm build`、`pnpm verify` 和 `openspec validate harden-web-ui-system --type change --strict --no-interactive`。 +- [ ] 7.5 对照 proposal、design、9 份 delta specs 与实现检查 diagram/权限矩阵/Select 边界一致性,确认 git diff 不含 API、后端权限、数据库 schema、migration、seed 或数据变更并达到归档条件。 From 438022c5fdcf3cdc19ddec85d9a49421b5daa314 Mon Sep 17 00:00:00 2001 From: Suyi Date: Sun, 2 Aug 2026 14:36:44 +0800 Subject: [PATCH 2/7] chore: add shadcn agent skills --- .agents/skills/migrate-radix-to-base/SKILL.md | 173 +++++++ .../migrate-radix-to-base/class-mapping.md | 62 +++ .../migrate-radix-to-base/consumer-props.md | 58 +++ .../migrate-radix-to-base/disclosure.md | 353 ++++++++++++++ .../migrate-radix-to-base/display-misc.md | 410 ++++++++++++++++ .../migrate-radix-to-base/form-controls.md | 390 +++++++++++++++ .agents/skills/migrate-radix-to-base/menus.md | 409 ++++++++++++++++ .../skills/migrate-radix-to-base/overlays.md | 459 ++++++++++++++++++ .../universal-patterns.md | 286 +++++++++++ .../migrate-radix-to-base/wrapper-shapes.md | 110 +++++ .agents/skills/shadcn/SKILL.md | 277 +++++++++++ .agents/skills/shadcn/agents/openai.yml | 5 + .agents/skills/shadcn/assets/shadcn-small.png | Bin 0 -> 1049 bytes .agents/skills/shadcn/assets/shadcn.png | Bin 0 -> 3852 bytes .agents/skills/shadcn/cli.md | 290 +++++++++++ .agents/skills/shadcn/customization.md | 209 ++++++++ .agents/skills/shadcn/evals/evals.json | 77 +++ .agents/skills/shadcn/mcp.md | 105 ++++ .agents/skills/shadcn/registry.md | 277 +++++++++++ .agents/skills/shadcn/rules/base-vs-radix.md | 306 ++++++++++++ .agents/skills/shadcn/rules/chat.md | 224 +++++++++ .agents/skills/shadcn/rules/composition.md | 213 ++++++++ .agents/skills/shadcn/rules/forms.md | 192 ++++++++ .agents/skills/shadcn/rules/icons.md | 101 ++++ .agents/skills/shadcn/rules/styling.md | 185 +++++++ .claude/skills/migrate-radix-to-base | 1 + .claude/skills/shadcn | 1 + skills-lock.json | 12 + 28 files changed, 5185 insertions(+) create mode 100644 .agents/skills/migrate-radix-to-base/SKILL.md create mode 100644 .agents/skills/migrate-radix-to-base/class-mapping.md create mode 100644 .agents/skills/migrate-radix-to-base/consumer-props.md create mode 100644 .agents/skills/migrate-radix-to-base/disclosure.md create mode 100644 .agents/skills/migrate-radix-to-base/display-misc.md create mode 100644 .agents/skills/migrate-radix-to-base/form-controls.md create mode 100644 .agents/skills/migrate-radix-to-base/menus.md create mode 100644 .agents/skills/migrate-radix-to-base/overlays.md create mode 100644 .agents/skills/migrate-radix-to-base/universal-patterns.md create mode 100644 .agents/skills/migrate-radix-to-base/wrapper-shapes.md create mode 100644 .agents/skills/shadcn/SKILL.md create mode 100644 .agents/skills/shadcn/agents/openai.yml create mode 100644 .agents/skills/shadcn/assets/shadcn-small.png create mode 100644 .agents/skills/shadcn/assets/shadcn.png create mode 100644 .agents/skills/shadcn/cli.md create mode 100644 .agents/skills/shadcn/customization.md create mode 100644 .agents/skills/shadcn/evals/evals.json create mode 100644 .agents/skills/shadcn/mcp.md create mode 100644 .agents/skills/shadcn/registry.md create mode 100644 .agents/skills/shadcn/rules/base-vs-radix.md create mode 100644 .agents/skills/shadcn/rules/chat.md create mode 100644 .agents/skills/shadcn/rules/composition.md create mode 100644 .agents/skills/shadcn/rules/forms.md create mode 100644 .agents/skills/shadcn/rules/icons.md create mode 100644 .agents/skills/shadcn/rules/styling.md create mode 120000 .claude/skills/migrate-radix-to-base create mode 120000 .claude/skills/shadcn diff --git a/.agents/skills/migrate-radix-to-base/SKILL.md b/.agents/skills/migrate-radix-to-base/SKILL.md new file mode 100644 index 0000000..5eb5dc5 --- /dev/null +++ b/.agents/skills/migrate-radix-to-base/SKILL.md @@ -0,0 +1,173 @@ +--- +name: migrate-radix-to-base +description: Migrates React projects and components from Radix UI to Base UI. Use when asked to migrate from radix, move to base-ui, convert radix primitives, or switch a shadcn project's base library. Handles single components ("migrate accordion") and whole projects. +--- + +# Radix UI -> Base UI migration + +You migrate shadcn wrappers, hand-rolled radix compositions, and their +consumers to `@base-ui/react`, keeping the project buildable at every step. +Be precise; never guess a mapping. When a prop or part is not in these +reference files, check `node_modules/@base-ui/react/**/*.d.ts` before +transforming, and record gaps in the report. + +## Preflight (always) + +1. `npx shadcn@latest info --json` (or the project's runner): gives the + current base, STYLE (e.g. `radix-lyra`), tailwind version, aliases, + installed components, and package manager. Trust it over inference. +2. Detect the package manager (packageManager field / lockfile: + pnpm-lock.yaml, bun.lock, yarn.lock, package-lock.json) and use IT for + every install. Never leave a stale lockfile. +3. Require a clean git tree; work on a branch; one commit per component. +4. Baseline check BEFORE touching dependencies: run the project's + typecheck/build so pre-existing failures are never attributed to you. +5. Install `@base-ui/react` alongside radix. Radix packages are removed only + after the LAST component is migrated (both coexist fine). + +## Strategy: golden pair first, transformation engine second + +- **Golden pair via the CLI (preferred).** If the project is shadcn with a + known style (`radix-