docs(agents): rewrite AGENTS.md as the single authoritative adaptive-layout spec
Unify the layered-decoupling approach (art in the 9:16 canvas, UI anchored to the device-stage screen container) from docs/adaptive-architecture.md, fold in the dialog fix (cd83c35), drop the wrong 'canvas is deprecated' framing, add a 3-step element-placement decision and the dialog anti-pattern. Codex/teammates read this file to adapt the remaining pages consistently.
This commit is contained in:
@@ -1,43 +1,73 @@
|
||||
# AGENTS.md — VTuber Mini
|
||||
# AGENTS.md — VTuber Mini 自适应规则(权威 · 统一)
|
||||
|
||||
> 给 codex / AI agent:在本项目调整页面**自适应布局**时,遵循本文件。
|
||||
> 目标:任何同事用 codex 微调任意页面,做法都统一、不再各搞一套。
|
||||
> 给 codex / AI agent / 同事:本项目调整**任意页面**的自适应布局,**以本文件为唯一权威**。
|
||||
> 本文件 = `docs/adaptive-architecture.md` 的分层方案 + 实战修正(对话框 `cd83c35`)整合后的最终版。
|
||||
> 旧版"别用 9:16 画布 / 架构文档已废弃"的说法**作废**——画布要用(放美术),只是 UI 别挂进去。
|
||||
|
||||
## 这是什么项目
|
||||
540px 宽的移动端 H5(单 VTuber 粉丝养成 app)。React 19 + Vite + Tailwind 4 + Capacitor(Android)。
|
||||
在桌面浏览器里看 = 一个 540 宽的竖条居中显示。
|
||||
## 项目形态
|
||||
540px 宽移动端 H5(单 VTuber 养成 app)。React 19 + Vite + Tailwind 4 + Capacitor(Android)。
|
||||
真实像素、无 rem 缩放;桌面浏览器里 = 一条 ≤540 宽竖屏居中。包管理用 **pnpm**。
|
||||
|
||||
## 自适应三条规则(就这么简单,别搞复杂)
|
||||
1. **容器**:页面根容器宽 = `min(100vw, 540px)`,水平居中,高 = `100dvh`。窄屏(<540)自动占满 100%。
|
||||
- 全局 `.device-stage` 已是这个规则,页面挂在它里面即可。
|
||||
2. **不要用百分比绝对定位**。元素位置别用 `style={{ left: 'X%', top: 'Y%' }}` 这种绝对定位——手机宽度一变就错乱(这是本项目布局乱的根因)。
|
||||
- 用正常 flex / 文档流;或贴边的 `absolute`(`bottom-0` / `top-0` / `inset-x-0`)。
|
||||
3. **导航 / tab 贴屏幕底**:`absolute inset-x-0 bottom-0` + 内部 flex 等分。背景美术用 `object-cover` 铺满容器。
|
||||
## 一、核心心法:分层解耦(整套体系就这一句)
|
||||
把页面里每个元素归到两类之一,**各用各的坐标系,别焊死在一起**:
|
||||
|
||||
## 两个必避的坑(亲测,栽过)
|
||||
1. **Tailwind 4 会静默丢弃自定义 CSS 类里的 `cqw / cqh / container-type`**(Lightning CSS 把整条规则删掉,不报错)。
|
||||
→ 关键尺寸**别写自定义 CSS 类,直接写 inline style,用 `vw/vh`**。例:
|
||||
```tsx
|
||||
<div style={{ width: 'min(100vw, 540px)', aspectRatio: '9 / 16' }} />
|
||||
```
|
||||
2. **`npm run build` 通过 ≠ 生效**。自定义 CSS 可能编译过但运行时根本没渲染(本项目曾让画布 `height` 塌成 0 却一直没发现,误以为成功)。
|
||||
→ 改完**必须实测元素几何**,不能只看编译过。
|
||||
| 层 | 是什么 | 挂哪个容器 | 怎么定位 |
|
||||
|---|---|---|---|
|
||||
| **美术层** | 背景视频 / 底图 / 立绘 | **9:16 画布**:`.reference-fit-frame` 或 inline `aspectRatio:'9 / 16'` | `object-cover` 铺满,边缘允许裁切 |
|
||||
| **UI 层** | 对话框 / 底栏 tab / 图标 / 头像 / 按钮——自带完整视觉的独立 PNG、要可点 | **真实屏幕容器**:`.device-stage`(`height:100dvh`)或页面级 `h-full`/`100dvh` 容器 | 锚屏幕边(`absolute inset-x-0 bottom-0` 等)+ `safe-area`,**绝不挂进画布** |
|
||||
|
||||
## 改完怎么验证(别跳过)
|
||||
一句话:**背景管「好看」(可裁),UI 管「可用」(锚屏幕、永不裁)。**
|
||||
|
||||
## 二、为什么必须这样(第一性 · 记牢这条因果)
|
||||
- 9:16 画布高度 = `宽 × 16/9`,**随宽度变**。
|
||||
- 元素挂画布里、用百分比纵向定位(`bottom-[X%]`)→ 实际像素 = `X% × 画布高` → **随宽度上下漂**。
|
||||
- `.device-stage` 高 = `100dvh`(视口高),**不随宽度联动** → UI 挂这里才钉得住。
|
||||
|
||||
## 三、三类页面 → 三种做法
|
||||
| 类型 | 页面 | 做法 |
|
||||
|---|---|---|
|
||||
| 沉浸·交互页 | `home`、`anniversary` | 美术留画布 + UI 锚屏幕(见 §一)← 重点 |
|
||||
| 沉浸·演出页 | `gacha`、`heart-moments` | 整段全屏演出,按设计稿**整体等比 `scale`**,可接受少量裁切 |
|
||||
| 功能·流式页 | `my`/`shop`/`album`/各 ledger…(约 14 个) | flex/grid 文档流 + `safe-area` + `clamp()` 字号,天然较好,只补 token 死角 |
|
||||
|
||||
## 四、铁律
|
||||
|
||||
**✅ 要这么做**
|
||||
- UI 元素挂屏幕容器(`.device-stage` 或页面级 `h-full`),贴边 `absolute inset-x-0 bottom-0` / flex 等分。
|
||||
- 背景 `object-cover` 铺满画布(**不是 `object-fill`**,那会把图标拉胖)。
|
||||
- 关键尺寸写 **inline style**,用 `vw/vh/dvh/min()`。
|
||||
|
||||
**❌ 绝不这么做**
|
||||
- **UI 挂 9:16 画布里用百分比纵向定位**(`bottom-[15.8%]`)→ 必随宽度漂。← 本项目布局乱、对话框漂的根因。
|
||||
- 自定义 CSS 类里写 `cqw / cqh / container-type` → Tailwind 4(Lightning CSS)**静默删整条规则、不报错**。改用 inline `vw/vh`。
|
||||
- 在有 `transform` 的祖先内用 `position: fixed` → fixed 被该祖先接管、相对它而非屏幕,照样漂。
|
||||
|
||||
## 五、判断一个元素该放哪(3 步 · 套到任意页任意元素)
|
||||
1. **它是美术还是 UI?** 自带完整视觉的独立 PNG / 要可点 = UI;只是画面氛围 = 美术。
|
||||
2. **查它的定位祖先链**:最近一个 `position:relative/absolute` 的祖先,高度是 **9:16 画布派生**(`aspectRatio` / 套在 `.reference-fit-frame` 里)还是 **视口高**(`100dvh` / `.device-stage`)?
|
||||
3. **若是 UI 却挂在画布派生高度上 → 移出来挂屏幕容器层。** 定位类基本不用动——`bottom-[X%]` 移到屏幕容器后,X% 就相对视口高,自然不漂。
|
||||
|
||||
## 六、反面教材(真实案例 · 别重蹈)
|
||||
首页对话框原本 `<div absolute inset-x-[4%] bottom-[15.8%]>` 挂在 9:16 画布的 `flex-1` 容器里 → 拖窄屏对话框下移、和 tab 重叠。
|
||||
**修复 = 把整个 `{activeDialog ? …}` 块移出画布、挪到 tab(`aspectRatio:108/25`)旁边(屏幕容器层)**,它就和 tab 共享屏幕锚定、不再随宽度动。见 `home-page.tsx` + commit `cd83c35`。
|
||||
(注:`docs/adaptive-architecture.md` §4.1 曾说「对话框为对齐角色保留画布百分比」——此条已被本案例推翻,对话框也要锚屏幕。)
|
||||
|
||||
## 七、验证(别跳过——build 过 ≠ 生效)
|
||||
本项目栽过「画布 height 塌成 0、编译却过、一直没发现」。改完**必须实测几何**:
|
||||
```bash
|
||||
npm run build # 1) 先编译过
|
||||
CI=true npm run dev # 2) 跑起来(CI=true 避免后台进程因 stdin 退出)
|
||||
pnpm build # 1) 先编译过
|
||||
pnpm dev # 2) 跑起来
|
||||
```
|
||||
然后在浏览器实测关键元素的 `getBoundingClientRect()`,确认:
|
||||
- 容器宽 = `min(屏宽, 540)`、`height ≠ 0`;
|
||||
- tab bar 的 `bottom` ≈ 屏幕高(真的贴底)、宽 = 屏幕宽;
|
||||
- 换几个宽度(360 / 400 / 540)布局都不乱。
|
||||
浏览器换 **360 / 400 / 540** 三个宽度,实测关键元素 `getBoundingClientRect()`:
|
||||
- UI 距屏幕底的 px(`innerHeight - rect.bottom`)**三个宽度下不变** = 没漂 ✓
|
||||
- tab 贴屏幕底、宽 = 屏幕宽;
|
||||
- 整体不乱、UI 不重叠。
|
||||
|
||||
> 提示:首页有登录 gate(`!isLoggedIn` → LoginPage,见 `App.tsx`),临时验证可把 `isLoggedIn` 初值改 `true`,**验证后还原**。
|
||||
> 首页有登录 gate(`!isLoggedIn`→LoginPage,见 `App.tsx`)。临时验证可把 `isLoggedIn` 初值改 `true`,**验证完还原**。
|
||||
|
||||
## 参考实现
|
||||
- `src/pages/home-page.tsx`:首页已按上面三条改过(540 容器、去手机外壳、底部 tab 贴屏幕底)——可作为其他页的样板,但**以本文件的规则为准,不要照搬其中的临时痕迹**。
|
||||
- 其他页(anniversary / shop / my / album / pet …)尚未逐页核对,按本规则调整即可。
|
||||
## 八、参考实现 & 现状
|
||||
- `src/pages/home-page.tsx` = **已改对的样板**:背景视频 + 立绘在 9:16 画布;tab + 对话框已锚屏幕(画布外)。可照抄分层方式,但**以本文件规则为准,别照搬临时痕迹**。
|
||||
- 其余 18 页:沉浸页(`anniversary`)按 §三①;演出页(`gacha`/`heart-moments`)按 §三②;流式页维持。**凡发现 UI 还挂在画布内用百分比定位的,按 §五 移出。**
|
||||
|
||||
## 已废弃
|
||||
`docs/adaptive-architecture.md` 是早期的复杂方案(9:16 固定画布 / scale 缩放 / 分层 / cqw),**已废弃、勿参考**,一切以本文件为准。
|
||||
---
|
||||
*来源整合:`docs/adaptive-architecture.md`(分层方案调研 2026-06-29)+ 对话框实战修正(`cd83c35`,2026-06-30)。本文件取代旧 AGENTS.md。*
|
||||
|
||||
Reference in New Issue
Block a user