# web/ 契约(给人和 AI 代理) 本目录是 WebDrop 前端。动手前先读,冲突时以此文件与 `../docs/` 为准。 ## 权威顺序 1. `../docs/threat-model.md`、`../docs/protocol.md`、`../docs/api.md`(协议与接口,不得违背) 2. `../docs/ui-design.md`(视觉与交互) 3. `../docs/frontend.md`(工程约定) 4. 本文件 5. 代码 文档与代码不一致时,**以文档为准**。要改行为,先改文档再改代码。 ## 范围 - 按用户要求修改项目文件,遵循对应目录的约定与测试要求。 - `@webdrop/core` 未就绪时,在 `src/core-bridge/` 按文档接口做适配与假实现,不自行发明协议。 ## 命令 ``` npm run dev 开发 npm run typecheck 类型检查 npm run lint:css 样式检查(颜色、圆角、阴影、blur 必须走 token) npm test Vitest npm run build 生产构建 ``` 提交前 `typecheck`、`lint:css`、`test`、`build` 必须全部通过。 ## 必须遵守 ### 安全 - 取出 URL fragment 后,在任何异步代码之前 `history.replaceState`。 - 密钥 K 导入为不可导出的 `CryptoKey`;不把密钥、令牌、明文写入 `console`、URL、`localStorage`、错误信息。 - 链接含 `k` 就只接受 enc 且认证通过的帧;模式与链接不符直接拒绝。 - 同一 `(item_id, frame_index)` 只能加密一次;重试发送队列里已持久化的相同字节(protocol §5)。 - 不使用 `dangerouslySetInnerHTML`;预览走沙箱 iframe;raw 内容只当文本。 - 不引入第三方脚本、统计、CDN 字体。 ### 分层 - 依赖单向:`app → features → ui`;`features → store → core-bridge`;`lib` 不依赖其他层。 - `ui/` 不依赖业务;`features/` 之间不互相导入,**唯一例外**:`features/channel/`、`features/home/`、`features/share/` 是组合层,可以导入 `features/item/`、`features/scan/`(home、share 还可导入 channel 里的 ActionBar 与 ComposeSheet);组件不直接调用网络或加解密。 - 不写 barrel(`index.ts` 汇总导出)。 - 所有页面跳转经 `app/navigation.ts` 的 `navigate()`,不直接操作 `history`。页面一加载就在 `app/entry.ts` 里同步取走并清除 fragment。**唯一例外**:弹层的返回键关闭,在 `lib/useBackToClose.ts`。 ### 样式 - 颜色字面量只允许出现在 `src/styles/tokens.css`。 - 圆角、阴影、`backdrop-filter` 只能取 token。 - `backdrop-filter` 仅用于顶栏和底部输入栏,同屏不超过 2 个;必须有 `prefers-reduced-transparency` 与不支持时的回退。 - 阴影只用于浮层(菜单、弹窗、Toast);列表用 1px `--line` 分隔。 - 不引入 Tailwind、CSS-in-JS、UI 组件库、图标库、动画库。 ### 状态与 React - 条目状态由日志推导,用判别联合,`switch` 必须穷尽。 - 订阅最小派生值,不订阅整个 store;高频值(进度)不触发整列表重渲染。 - 派生状态在渲染期计算,不用 effect 同步;不在组件内定义组件。 - 条件渲染用三元,不用 `&&` 渲染可能为 `0` 的值。 - 加解密、装配、哈希在 Worker,主线程不处理大文件。 ### 动效 - `ViewTransition` 一律 `default="none"`,`enter` 与 `exit` 成对;只用于有空间关系的变化(层级导航、条目增删)。 - 状态变化、重连、后台同步不加动画。 - 动画 CSS 只放在 `styles/transitions.css`,取自技能配方;必须尊重 `prefers-reduced-motion`。 - 方向性 `ViewTransition` 放在页面组件里,不放布局里。 ### 文案 - 只做中文,文案直接写在组件里,不做 i18n。 - 安全相关文案必须诚实(见 `ui-design.md §7.4`):`ui_restrict_copy_download` 不是安全保证;raw 模式必须明确提示服务端可见;不夸大「匿名」「不可追踪」。 ## 禁止 - 自动加载有下载上限(GATED)的文件内容:缩略图、内联视频一律等用户点击,滚动列表不能消耗下载次数。 - 为了「看起来现代」加装饰性渐变、霓虹、大圆角、pill 容器、眉标签、装饰性副标题。渐变只能用 `tokens.css` 里的渐变 token,且只表达状态或层次。 - 弹层不用 portal 就渲染在带 `content-visibility` 的列表项内(`position: fixed` 会失效)。 - 新增运行时依赖而没有书面理由(解决什么、为何不能自写、体积)。 - 提前抽象。同样的代码第二次出现才抽,第三次必须抽。 - 把颜色、间距、圆角写死在组件里。 - 静默吞掉认证失败或篡改;`tampered` 必须可见。 ## 约定 - 文件单一职责,组件约 150 行为上限。 - 导出面最小化,不被使用的符号不导出。 - 注释只写「为什么」,并指向文档章节,例如 `// protocol §5: 重试必须发相同字节`。 - `store/` 与 `lib/` 的模块配 Vitest;用户可见流程配 Playwright。 - 路径别名 `@/` 指向 `src/`。 - 默认中文界面;提交信息沿用仓库风格。 ## 完成的定义 一项改动完成,需要: 1. 对应文档已更新(如行为有变)。 2. `typecheck`、`lint:css`、`test`、`build` 全部通过。 3. 亮暗两套、手机与桌面宽度都看过。 4. 没有违反上面的「必须遵守」与「禁止」。