From d170326a6b58f81b42810102dc9840655af6ddbb Mon Sep 17 00:00:00 2001 From: Linghong Date: Tue, 25 Aug 2026 07:55:22 +0000 Subject: [PATCH] =?UTF-8?q?=E6=81=A2=E5=A4=8D=E8=A2=AB=20#193=20=E8=AF=AF?= =?UTF-8?q?=E5=88=A0=E7=9A=84=E7=AD=96=E5=88=92=E5=89=8D=E7=AB=AF=E5=85=A8?= =?UTF-8?q?=E9=83=A8=E6=A0=B7=E5=BC=8F=EF=BC=8C=E5=B9=B6=E5=8A=A0=E4=B8=8A?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=E5=88=B0=E6=A0=B7=E5=BC=8F=E7=9A=84=E5=AE=88?= =?UTF-8?q?=E9=97=A8=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 症状:立项策划界面整体裸奔——审批卡、阶段进度条、交付行没有任何框和布局, 「查看 GDD 正文」弹层因 backdrop 丢掉 fixed 定位而内联平铺进消息流。 成因:策划前端经 squash PR #159 完整进入 master,组件(TSX)至今与源分支最终态 逐字节一致;随后 #193「Codex/agent chat layout fix」重写聊天区样式时,把 styles.css 里策划前端的选择器整段删除——gdd-approval-card 34 条、plan-gdd* 17 条、planning-lane-runtime-strip 上限——未搬往任何其他文件(全仓检索为 0)。同一 个 PR 把 project-development.suite.ts 大幅重写,原有的 CSS 守门断言一并消失, 所以没有任何测试变红。 修复:从 5802048c3^ 原样取回全部 42 个规则块,整段追加在 styles.css 末尾并注明 来历。这些选择器在当前文件中出现次数为 0,纯加法、零冲突,不触碰 #193/#194 有意 重构的聊天区规则;追加在末尾也保持了原有的同权重覆盖关系(策划窄条的上限仍压过 调试面板的 240px 天花板)。 守门:plan-gdd.suite.ts(两轮样式重构都未被触碰的文件)新增一条测试,类名清单 直接从 GddApprovalCard / PlanningLaneRuntimeStrip 源码推导,逐个断言 styles.css 里有对应规则,另单独锁弹层 backdrop 的 fixed 定位。A/B 验证:抽掉恢复段即红,报 「styles.css 缺少 .plan-gdd-surface 的规则」。组件加新类而样式缺失、或样式再次 被顺手清理,这条都会拦住。 Co-Authored-By: Claude Opus 5 --- apps/ai-game-creator-shell/src/styles.css | 310 ++++++++++++++++++ .../tests/appSurface/plan-gdd.suite.ts | 49 +++ 2 files changed, 359 insertions(+) diff --git a/apps/ai-game-creator-shell/src/styles.css b/apps/ai-game-creator-shell/src/styles.css index 1e7936569..c87664904 100644 --- a/apps/ai-game-creator-shell/src/styles.css +++ b/apps/ai-game-creator-shell/src/styles.css @@ -7209,3 +7209,313 @@ iframe.preview-frame { grid-column: 1 / -1; } } + + +/* ============================================================ + 立项策划前端样式(PlanGddSurface / 阶段进度 / GDD 审批卡 / 交付行 / + planning-lane-runtime-strip)。 + 这一段曾在 #193「Codex/agent chat layout fix」重写聊天样式时被整体误删: + 组件(TSX)原样保留、选择器全数消失,策划界面裸奔、正文弹层失去 fixed 定位 + 变成内联平铺。从 5802048c3^ 原样恢复。聊天区样式重构时请勿顺手清理本段—— + 这些选择器的使用方在 features/project-workspace/GddApprovalCard.tsx 与 + PlanningLaneRuntimeStrip.tsx。 + ============================================================ */ + +/* 策划区是一个面:阶段进度是标题栏,审批卡是正文。边框和圆角只画在外壳上,里面两块 + 不再各自带框。 */ +.plan-gdd-surface{ + display: grid; + min-width: 0; + border: 1px solid #cfd7e6; + border-radius: 10px; + background: #fff; + overflow: hidden; +} + +.gdd-approval-card{ + display: grid; + gap: 14px; + padding: 16px 18px 18px; + background: #f8fbff; +} + +.plan-gdd-surface--with-card .plan-gdd-stage-progress{ + border-bottom: 1px solid #dbe4f1; +} + +.plan-gdd-stage-progress{ + display: grid; + gap: 6px; + padding: 10px 12px; + background: #fff; + color: #526173; + font-size: 12px; +} + +.plan-gdd-stage-progress__header, +.plan-gdd-stage-progress__meta{ + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 8px 14px; +} + +/* 批准后的交付行。它是策划阶段唯一的产物出口,所以给一条分隔线把它和上面的状态 + 区分开,而不是混成第三行元信息。 */ +.plan-gdd-stage-progress__delivery{ + display: grid; + gap: 8px; + margin-top: 4px; + padding-top: 9px; + border-top: 1px solid #e6ecf5; +} + +.plan-gdd-stage-progress__delivery code{ + min-width: 0; + color: #3c4a5c; + font-size: 11px; + overflow-wrap: anywhere; +} + +.plan-gdd-stage-progress__delivery-actions{ + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.plan-gdd-stage-progress__delivery-actions button{ + min-height: 30px; + padding: 0 12px; + border: 1px solid #cfd7e6; + border-radius: 6px; + color: #27364a; + background: #fff; + font-size: 12px; +} + +.plan-gdd-stage-progress__delivery-actions button:hover:not(:disabled), +.plan-gdd-stage-progress__delivery-actions button:focus-visible{ + border-color: #1f6feb; + color: #1f6feb; +} + +.plan-gdd-stage-progress__delivery-actions button:disabled{ + opacity: 0.58; +} + +.plan-gdd-stage-progress__delivery-error{ + color: #b42323; + overflow-wrap: anywhere; +} + +.plan-gdd-stage-progress__header strong{ + color: #27364a; +} + +.plan-gdd-stage-progress__header span{ + padding: 3px 8px; + border-radius: 999px; + background: #eef6ff; + color: #1f6feb; +} + +.gdd-approval-card__header{ + display: grid; + gap: 5px; + min-width: 0; +} + +.gdd-approval-card h2, +.gdd-approval-card h3, +.gdd-approval-card p{ + margin: 0; +} + +.gdd-approval-card h2{ + font-size: 18px; +} + +.gdd-approval-card__header p{ + color: #526173; + overflow-wrap: anywhere; +} + +.gdd-approval-card__details-trace{ + color: #6b7a8c; + font-size: 11px; + overflow-wrap: anywhere; +} + +.gdd-approval-card__details-trigger{ + justify-self: start; + min-height: 30px; + padding: 0 10px; + border: 1px solid #cfd7e6; + border-radius: 6px; + color: #27364a; + background: #fff; +} + +.gdd-approval-card__decisions{ + display: grid; + gap: 8px; +} + +.gdd-approval-card__decisions article{ + display: grid; + gap: 3px; + padding: 9px 10px; + border: 1px solid #e2e8f0; + border-radius: 7px; + background: #fff; +} + +.gdd-approval-card__decisions article span{ + color: #1f6feb; + font-size: 12px; +} + +.gdd-approval-card__decisions article small{ + color: #526173; + overflow-wrap: anywhere; +} + +.gdd-approval-card__actions, +.gdd-approval-card__dialog-actions, +.gdd-approval-card__recovery{ + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; +} + +.gdd-approval-card__actions button, +.gdd-approval-card__dialog-actions button, +.gdd-approval-card__recovery button{ + min-height: 32px; + padding: 0 12px; + border: 1px solid #cfd7e6; + border-radius: 6px; + color: #fff; + background: #1f6feb; +} + +.gdd-approval-card__actions button:nth-child(n + 2), +.gdd-approval-card__dialog-actions button:first-child{ + color: #27364a; + background: #fff; +} + +.gdd-approval-card button:disabled{ + cursor: not-allowed; + opacity: 0.55; +} + +.gdd-approval-card__recovery{ + justify-content: space-between; + padding: 9px 10px; + border-radius: 7px; + color: #854d0e; + background: #fff7df; + font-size: 13px; +} + +.gdd-approval-card__error{ + padding: 9px 10px; + border-radius: 7px; + color: #991b1b; + background: #fff1f2; + overflow-wrap: anywhere; +} + +.gdd-approval-card__dialog-backdrop{ + position: fixed; + inset: 0; + z-index: 220; + display: grid; + padding: 24px; + background: rgb(0 0 0 / 56%); + place-items: center; +} + +.gdd-approval-card__dialog{ + display: grid; + gap: 12px; + width: min(520px, 100%); + padding: 20px; + border: 1px solid #e5e7eb; + border-radius: 8px; + background: #fff; + box-shadow: 0 18px 52px rgb(0 0 0 / 18%); +} + +.gdd-approval-card__details{ + max-height: min(760px, calc(100vh - 32px)); + overflow: auto; +} + +.gdd-approval-card__details header, +.gdd-approval-card__details article{ + display: grid; + gap: 5px; +} + +.gdd-approval-card__details article{ + padding-top: 10px; + border-top: 1px solid #e2e8f0; +} + +.gdd-approval-card__details p{ + white-space: pre-line; + overflow-wrap: anywhere; +} + +.gdd-approval-card__dialog textarea{ + width: 100%; + min-height: 110px; + padding: 9px; + border: 1px solid #cfd7e6; + border-radius: 6px; + resize: vertical; +} + +.gdd-approval-card__dialog-hint{ + padding: 9px 10px; + border-radius: 7px; + color: #854d0e; + background: #fff7df; + font-size: 13px; +} + +/* 审批卡是这一列里唯一没有天花板的成员:Fast GDD 的决定项越多它越高,能把消息列表和运行 + 时面板一起挤出可视区。和 `.agent-runtime-status` 一样给它自己的上限加内部滚动,而不是 + 让它去挤别人。 */ +/* 这个面不参与 flex 压缩。它自己不会长高:标题栏是固定几行,审批卡下面有自己的 + max-height 和内滚,所以高度天然有界。让它可压缩的话,`overflow: hidden`(画圆角 + 要的)会把底部切掉——批准后交付行正好在那儿,表现是路径和两个按钮凭空消失。 */ +.game-workbench-chat .plan-gdd-surface{ + flex: 0 0 auto; +} + +/* 只有审批卡在场时才需要「标题栏定高 + 正文占余下」的两行轨道。批准后卡片收掉, + 面里只剩标题栏,这条声明会给它套上一个不存在的第二行。 */ +.game-workbench-chat .plan-gdd-surface--with-card{ + min-height: 0; + grid-template-rows: auto minmax(0, 1fr); +} + +.game-workbench-chat .gdd-approval-card{ + min-height: 0; + max-height: clamp(240px, 52dvh, 640px); + overflow-y: auto; + overscroll-behavior: contain; + scrollbar-gutter: stable; +} + +/* 上面 24dvh/240px 的天花板是给做游戏链路常驻的调试面板设的。策划链路的窄条只在 + 需要用户动手时出现(澄清卡 / 失败恢复),内容是要读完再回答的题面,给它和审批 + 卡同级的空间。 */ +.game-workbench-chat .planning-lane-runtime-strip{ + max-height: clamp(240px, 52dvh, 640px); +} diff --git a/apps/ai-game-creator-shell/tests/appSurface/plan-gdd.suite.ts b/apps/ai-game-creator-shell/tests/appSurface/plan-gdd.suite.ts index 9ee3e9fd8..14f38e183 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/plan-gdd.suite.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/plan-gdd.suite.ts @@ -1,3 +1,6 @@ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; + import { agentRuntimeUserInputRequest, createPlanGddStateView, @@ -521,4 +524,50 @@ export function registerPlanGddApprovalTests() { expectSupervisorRuntimePanelAbsent(); expect(screen.queryByText('策划子 Run 退出')).toBeNull(); }); + + it('keeps a stylesheet rule for every class the planning components reference', () => { + // #193「Codex/agent chat layout fix」重写聊天样式时,把策划前端的选择器整段 + // 误删:组件(TSX)原样保留、样式全数消失,审批卡/阶段条/交付行裸奔,正文弹层 + // 失去 fixed 定位变成内联平铺。组件和它的样式分居两个文件,重构样式的人看不见 + // 使用方——这条测试就是那根缺失的连线:类名清单直接从组件源码里推导,组件加了 + // 新类而样式没跟上、或样式又被顺手清掉,这里都会红。 + const componentSources = [ + 'src/features/project-workspace/GddApprovalCard.tsx', + 'src/features/project-workspace/PlanningLaneRuntimeStrip.tsx', + ] + .map((path) => + readFileSync( + resolve(process.cwd(), 'apps/ai-game-creator-shell', path), + 'utf8', + ), + ) + .join('\n'); + const referencedClasses = new Set(); + for (const match of componentSources.matchAll( + /className=(?:"([^"]+)"|\{`([^`]+)`\})/g, + )) { + const literal = (match[1] ?? match[2] ?? '').replace(/\$\{[^}]*\}/g, ' '); + for (const name of literal.split(/\s+/)) { + if (name) { + referencedClasses.add(name); + } + } + } + expect(referencedClasses.size).toBeGreaterThan(10); + const styles = readFileSync( + resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'), + 'utf8', + ); + for (const name of referencedClasses) { + expect( + styles.includes(`.${name}`), + `styles.css 缺少 .${name} 的规则`, + ).toBe(true); + } + // 正文弹层必须是浮层:backdrop 一旦丢掉 fixed 定位,整个 GDD 会内联平铺进 + // 消息流里——这正是误删当时最刺眼的症状。 + expect(styles).toMatch( + /\.gdd-approval-card__dialog-backdrop\s*\{[^}]*position:\s*fixed/s, + ); + }); }