# Metapi 前端架构约定 这份约定把管理端的技术入口收敛成一条可检查的路径,作为新增页面和重构时 的维护者参考。它不改变现有页面能力、路由、协议或主题。 ## Canonical stack ```text pages/** → pages//** → components/ui/** → Radix behavior primitives └──────────→ index.css design tokens/classes ``` - 生产页面使用 React 18 + TypeScript + Vite,并通过 `src/web/api.ts` 发起 认证 HTTP/SSE 请求。 - `src/web/components/ui/**` 是项目拥有的 UI 边界。只有这一层可以直接 import `@radix-ui/*`(以及其他交互原语库);页面和领域组件只接收 Metapi 的语义 props。 - `CenteredModal`、`MobileDrawer`、`ModernSelect` 是兼容 facade。新代码优先 使用 `components/ui` 的 wrapper;旧导出在迁移完成前继续有效。 - 浏览器 renderer 的 Radix modal overlay 已负责 `RemoveScroll`;`components/ui` 只在 SSR/test/不完整 DOM fallback 使用引用计数 body lock,避免多个 owner 在卸载顺序不同步时留下页面滚动锁。 - 视觉来源是 `src/web/index.css` 的 CSS variables 和语义 class。不要为单个 页面引入 CSS module、styled-components 或另一套 UI framework;复用布局时 先扩展已有 class/primitive。 - `@import "tailwindcss"` 只允许出现在 `src/web/index.css`。当前它作为 Vite 的单一 utility/preflight 入口保留;页面不得再创建第二个样式 runtime。 ## 异步与状态边界 - `src/web/api.ts` 负责认证、超时、错误提取、取消和 SSE 解析;页面只拥有 自己的 server state、URL intent 和领域 mutation。 - 请求 effect 必须在 target 改变和卸载时 abort 或 stale-guard,并清理 timer、 listener、animation frame、portal 和 body-scroll lock。 - loading、empty、error、cancel 状态使用共享窄职责 primitive 或既有语义 class;用户可见文本通过 `t()`/`tr()` 翻译。 ## 维护检查 提交前运行: ```bash npm run typecheck:web npm run typecheck:web:test npm run repo:drift-check npm run build:web git diff --check ``` `repo:drift-check` 会报告以下架构漂移: - 页面/领域组件直接 import 第三方 UI 原语; - 顶层 page 互相 import;以及 - `proxy-core`、transformer 和整包响应读取等既有边界债务。 新的 wrapper 只能放入 `src/web/components/ui/**`,不能通过新增 allowlist 隐藏违规。若确有浏览器能力例外,应在拥有该能力的 helper 中记录原因、测试 和后续迁移边界。 ## 迁移策略 优先迁移低耦合的 modal/drawer、异步按钮和反馈状态,再处理大型领域页面。 每个切片都应保留旧 props/class、桌面与移动断点、键盘行为和翻译,并以聚焦 测试证明 focus return、Escape/backdrop、body-scroll 多实例清理和 stale 请求 语义没有回归。不要为了“统一”机械替换所有历史 `