模型下拉弹层改回以触发钮为锚点,输入区高度变化不再牵动它
- styles.css:direct-codex 控制排里的 .conversation-model-select 由 position: static !important 改为 position: relative !important(只改这一个词,另补一段注释说明它同时是弹层锚点)。这颗容器是 .conversation-model-menu(position: absolute; right: 0; bottom: calc(100% + 8px))与模型提示([role='alert'] / [role='status'],position: absolute; bottom: 36px)的包含块;写成 static 时包含块回落到最近的有定位祖先——也就是 position: absolute 的 composer 盒子,菜单于是出现在整个 composer 上方、与触发钮之间隔着整个输入区,输入区一变高(多行、引用 chip、AI 润色)菜单与提示跟着往上飘:这就是"编辑框把元素挤开"的形态。 - styles.css:right/bottom: auto、flex: 0 1 auto、min-width: 0、max-width: 180px 全部保持原样——relative 不会把模型选择钮移出控制排,也不改它被压缩换行的行为;交互语义零改动(打开/选择/刷新模型、发送、@ 引用、快速模式全在原位)。 - project-development.suite.ts:新增一条声明级守卫。① 弹层不参与父容器流式布局(菜单与模型提示都是 absolute,菜单 bottom: calc(100% + …)、right: 0);② 锚点必须是有定位的容器(relative 且不是 static),同时仍在原位(right/bottom auto、flex 0 1 auto、min-width 0);③ 输入区高度变化不得推动同排操作元素(composer 是底边锚定的两行网格:position: absolute + bottom: 12px + grid-template-rows: auto auto;控制排是单行 flex 且不换行,@ 与发送钮 flex: 0 0 30px 不可压缩,窄屏下被压缩的是可收缩的模型选择钮)。 - conversationModelSelect.test.tsx:新增一条 DOM 守卫——菜单必须是 .conversation-model-select 的直接子节点,且不得成为控制排容器 .project-supervisor-composer-controls-right 的流式子节点。 - 变异验证(均已实测红灯):① 锚点改回 static → "expected … to match /position:\s*relative\s*!important/";② 菜单改成 position: static(弹层回流式)→ "expected … to match /position:\s*absolute/";③ 把菜单渲染挪出触发钮容器(变成同排容器的流式子节点)→ DOM 守卫 "expected null not to be null"。 - 门禁:typecheck exit 0;AGC 子集 1205 passed / 4 skipped / 0 failed(基线 1203 + 本次新增 2 条);src/components/image-editor 1385 passed;check:encoding 4379 文件通过;本次三个文件单独 `git diff --check` exit 0(全量 `git diff --check` 目前报的是另一位 Agent 在途修改的 packages/shared/src/components/styles.css:1281 EOF 空行,与本次无关)。
This commit is contained in:
@@ -9897,6 +9897,14 @@ iframe.preview-frame {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* 模型选择钮在控制排里按流式排布,但它同时是**弹层锚点**:`.conversation-model-menu`
|
||||
(`bottom: calc(100% + 8px)`)与模型提示(`[role='alert'] / [role='status']`,
|
||||
`bottom: 36px`)都是 `position: absolute`,包含块必须是这颗容器。这里原来写的是
|
||||
`position: static !important`(当初为抵消上面那条遗留的「模型选择钮绝对定位」规则),
|
||||
锚点于是回落到最近的有定位祖先——也就是 `position: absolute` 的 composer 盒子:
|
||||
菜单跑到**整个 composer 上方**、和触发钮之间隔着整个输入区,输入区一变高(多行、
|
||||
引用 chip、AI 润色)菜单与提示就跟着往上飘,看起来就是"编辑框把弹层挤开了"。
|
||||
`relative` 不会把它移出控制排(`right/bottom: auto` 仍在原位),只补回锚点。 */
|
||||
.game-workbench-chat
|
||||
.project-supervisor-surface.is-direct-codex
|
||||
.project-supervisor-composer.is-direct-codex
|
||||
@@ -9904,7 +9912,7 @@ iframe.preview-frame {
|
||||
.conversation-model-select {
|
||||
flex: 0 1 auto;
|
||||
min-width: 0;
|
||||
position: static !important;
|
||||
position: relative !important;
|
||||
right: auto !important;
|
||||
bottom: auto !important;
|
||||
max-width: 180px;
|
||||
|
||||
@@ -5763,6 +5763,66 @@ export function registerProjectSupervisorSurfaceTests() {
|
||||
await waitFor(() => expect(trigger.textContent).toContain('快速'));
|
||||
});
|
||||
|
||||
it('anchors the model dropdown to its own trigger instead of the composer box', () => {
|
||||
const styles = readFileSync(
|
||||
resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
// 弹层自己不参与父容器流式布局:菜单与模型提示都是 absolute,打开它不会给控制排
|
||||
// 添一个流式子节点("弹层撑高容器 / 把同排元素挤开"这一类问题的前提)。
|
||||
const menu = styleRuleBody(styles, '\\.conversation-model-menu');
|
||||
expect(menu).toMatch(/position:\s*absolute/u);
|
||||
expect(menu).toMatch(/bottom:\s*calc\(100%\s*\+\s*\d+px\)/u);
|
||||
expect(menu).toMatch(/right:\s*0/u);
|
||||
const actionNotice = styleRuleBody(
|
||||
styles,
|
||||
"\\.conversation-model-select \\[role='status'\\]",
|
||||
);
|
||||
expect(actionNotice).toMatch(/position:\s*absolute/u);
|
||||
|
||||
// 锚点:direct-codex 控制排里的模型选择钮容器必须是**有定位**的,否则上面两个
|
||||
// absolute 弹层的包含块会回落到 `position: absolute` 的 composer 盒子——菜单就会
|
||||
// 跑到整个 composer 上方,和触发钮之间隔着输入区,输入区一变高菜单跟着往上飘。
|
||||
const anchor = styleRuleBody(
|
||||
styles,
|
||||
'\\.project-supervisor-composer-controls\\s+\\n?\\s*\\.conversation-model-select',
|
||||
);
|
||||
expect(anchor).toMatch(/position:\s*relative\s*!important/u);
|
||||
expect(anchor).not.toMatch(/position:\s*static/u);
|
||||
// 补锚点不能把它移出控制排:仍在原位(right/bottom auto)、仍可被压缩换行。
|
||||
expect(anchor).toMatch(/right:\s*auto/u);
|
||||
expect(anchor).toMatch(/bottom:\s*auto/u);
|
||||
expect(anchor).toMatch(/flex:\s*0 1 auto/u);
|
||||
expect(anchor).toMatch(/min-width:\s*0/u);
|
||||
|
||||
// 输入区高度变化不得推动同排操作元素:composer 是底边锚定的两行网格,输入区占第一
|
||||
// 行、控制排占第二行;输入区从 96px 长到 140px 只把 composer 顶边抬高,控制排仍钉在
|
||||
// composer 底边(`bottom: 12px`),发送钮与模型选择钮不动。
|
||||
const composer = styleRuleBody(
|
||||
styles,
|
||||
'\\.game-workbench-chat\\s+\\.project-supervisor-surface\\.is-direct-codex\\s+\\.project-supervisor-composer\\.is-direct-codex',
|
||||
);
|
||||
expect(composer).toMatch(/position:\s*absolute/u);
|
||||
expect(styleNumber(composer, 'bottom')).toBeGreaterThan(0);
|
||||
expect(composer).toMatch(/grid-template-rows:\s*auto auto/u);
|
||||
// 控制排是单行 flex、不换行:窄屏下靠可压缩的模型选择钮(flex 0 1 auto + min-width 0)
|
||||
// 收窄,而不是把模型选择钮/发送钮挤到第二行。
|
||||
const controls = styleRuleBody(
|
||||
styles,
|
||||
'\\.project-supervisor-composer-controls',
|
||||
);
|
||||
expect(controls).toMatch(/display:\s*flex/u);
|
||||
expect(controls).not.toMatch(/flex-wrap/u);
|
||||
// `@` 引用钮与发送钮共用一条尺寸规则(同一规则体里两个选择器),都必须不可压缩,
|
||||
// 这样窄屏下被压的是可收缩的模型选择钮,而不是把这两个方钮挤出这一行。
|
||||
const controlsButtons = styleRuleBody(
|
||||
styles,
|
||||
'\\.project-supervisor-composer-controls\\s+\\n?\\s*\\.project-supervisor-submit-button',
|
||||
);
|
||||
expect(controlsButtons).toMatch(/flex:\s*0 0 30px/u);
|
||||
});
|
||||
|
||||
it('runs a top workbench play request without a second confirmation', async () => {
|
||||
const projectPath = '/tmp/top-play-request';
|
||||
const supervisorHarness = createProjectSupervisorRuntimeHarness({
|
||||
|
||||
@@ -127,6 +127,32 @@ test('a failed save keeps submission unavailable', async () => {
|
||||
expect(onReady).toHaveBeenLastCalledWith(false);
|
||||
});
|
||||
|
||||
test('keeps the open menu inside its own trigger container, not the control row', async () => {
|
||||
const onReady = vi.fn();
|
||||
const { container } = render(
|
||||
<>
|
||||
<div className="project-supervisor-composer-controls-right">
|
||||
<ConversationModelSelect disabled={false} onReady={onReady} />
|
||||
</div>
|
||||
</>,
|
||||
);
|
||||
await screen.findByRole('button', { name: '对话模型' });
|
||||
fireEvent.click(screen.getByRole('button', { name: '对话模型' }));
|
||||
|
||||
// 弹层必须留在触发钮自己那颗容器里:它是 absolute + bottom: 100% 的浮层,锚点由这颗
|
||||
// 容器提供;一旦被挪到控制排(或其它同排容器)当流式子节点,就会参与那一排的布局。
|
||||
const modelSelect = container.querySelector('.conversation-model-select');
|
||||
expect(modelSelect).not.toBeNull();
|
||||
expect(
|
||||
modelSelect?.querySelector(':scope > .conversation-model-menu'),
|
||||
).not.toBeNull();
|
||||
expect(
|
||||
container.querySelector(
|
||||
'.project-supervisor-composer-controls-right > .conversation-model-menu',
|
||||
),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
test('closes the menu when clicking outside', async () => {
|
||||
const onReady = vi.fn();
|
||||
render(<ConversationModelSelect disabled={false} onReady={onReady} />);
|
||||
|
||||
Reference in New Issue
Block a user