输入区工具条改回唯一一行:润色钮并入工具条,nowrap + 实测 min-width
Project CI / Backend tests (pull_request) Failing after 26s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 5m45s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 6m30s
Project CI / Repository checks (pull_request) Failing after 14s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 7m4s
Project CI / Frontend tests (pull_request) Successful in 2m51s
Project CI / AI game creator shell web tests (pull_request) Failing after 4m12s
Project CI / Native shell tests (pull_request) Successful in 10m32s
Project CI / Backend tests (pull_request) Failing after 26s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 5m45s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 6m30s
Project CI / Repository checks (pull_request) Failing after 14s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 7m4s
Project CI / Frontend tests (pull_request) Successful in 2m51s
Project CI / AI game creator shell web tests (pull_request) Failing after 4m12s
Project CI / Native shell tests (pull_request) Successful in 10m32s
- 口径:工具条只允许一行——`@` / `+` / 「AI 润色 / 恢复原文」/ 模型 / 语音 / 发送 六颗控件平铺,任何面板宽下都不换行、不堆叠、不重叠;宽度不够时按 `min-width` 横向溢出可见,发送钮仍在可视区内 - 换行来源(首轮为「避免重叠」加的 wrap):styles.css:11893 `.project-chat-composer-controls`(6 类)`flex-wrap: wrap`、styles.css:11933 `.project-chat-composer-controls-left` `flex-wrap: wrap`、styles.css:13747 `.conversation-model-select` `flex-wrap: wrap` → 三处都改 `nowrap`;左组补 `flex: 0 0 auto`(固定 28px 动作钮不参与收缩,否则槽位被压窄会把润色钮挤到模型钮上) - 润色钮进工具条:`ResourceReferenceInput` 新增 `actionsPortalTarget`(portal 目标,不传时行为不变,策划面照旧),`DirectProjectComposer` 在左组 `+` 之后放挂点 `.project-chat-composer-controls-actions` 并传入 → 空输入态不再为它留一行;输入盒第二行只剩「润色状态」(无内容时 0 高,`gap` 的 row 分量 6→0、状态自带 `margin-top: 6px`) - min-width 算式(落到两个元素):控件排 `.project-chat-composer-controls` = `@`28 + `+`28 + 润色28 + 模型钮 64(`min-width: 64px`,名字省略号)+ 语音28 + 发送28 = 204,加 5×8px 列间距 = 244,再留 8 余量 → **252px**;外壳 `form.project-chat-composer.is-direct-codex` = 252 + 左右内边距 12×2 + 边框 1×2 → **278px**。夹具实测:自然宽度 290.4、七档(240…900)控件 `top` 差值 ≤1px、两两相交 0 处、`outsideControlsRow` 空 - 低于 min-width:外壳保持 278px、**内容按自身宽度溢出可见**(面板与 composer 都不裁切,选它而不是给工具条 `overflow-x: auto` 是因为工具条里挂着模型菜单浮层,加滚动容器会把它裁掉——#600 的老问题);240px 实测发送钮 rect 仍在窗口内且 `elementFromPoint` 命中它 - 测试:`chatDialogFrameLayout.test.ts` 新增「工具条只有一行…」——七个宽度下 `flex-wrap` 必须 `nowrap`(右组本来就不换行)、控件排 `min-width: 252` / 外壳 `min-width: 278`、组件源码里润色钮渲染在控件排之后的挂点;最高高度算式按新结构更新为 222。**改前红已记录**:把三个源文件 stash 回旧实现后单跑,`240px 下工具条必须保持单行: expected 'wrap' to be 'nowrap'` 与 `expected 228 to be 222` 两条同时红 - 校验:`npx vitest run tests/appSurface.test.ts tests/chatDialogFrameLayout.test.ts` → **225 passed / 9 skipped**;`agc:typecheck` pass 3 / fail 0;eslint、prettier、encoding 5280 files、spacetime-schema 91 tables、`git diff --check` 全绿;对照图与 PR 正文已更新 Refs #600, PR #607
This commit is contained in:
+52
-37
@@ -114,6 +114,13 @@ type ResourceReferenceInputProps = {
|
||||
submitSuppressed?: boolean;
|
||||
/** 输入区操作排(编辑器右侧那一列)里宿主注入的内容,例如素材选择器的 `@` 触发钮。 */
|
||||
inputActions?: ReactNode;
|
||||
/**
|
||||
* 把「AI 润色 / 恢复原文」这一排**渲染到宿主指定的容器里**(`createPortal`)。
|
||||
*
|
||||
* Direct 输入区把它挪进下方那条工具条,让润色钮与 `@` / `+` / 模型 / 语音 / 发送 同一水平线
|
||||
* (否则它会自成一行,窄面板下看起来像"堆叠")。不传时仍然渲染在输入盒内(策划面等保持原样)。
|
||||
*/
|
||||
actionsPortalTarget?: HTMLElement | null;
|
||||
inputRef?: RefObject<HTMLDivElement | null>;
|
||||
/** 输入区容器元素:宿主定位自己的浮层时用它当锚点。 */
|
||||
containerRef?: RefObject<HTMLDivElement | null>;
|
||||
@@ -431,6 +438,7 @@ function ResourceReferenceEditor({
|
||||
showPolishAction = true,
|
||||
submitSuppressed = false,
|
||||
inputActions,
|
||||
actionsPortalTarget,
|
||||
composerRef,
|
||||
rootRef,
|
||||
}: ResourceReferenceInputProps & {
|
||||
@@ -842,48 +850,55 @@ function ResourceReferenceEditor({
|
||||
return () => form.removeEventListener('submit', handleFormSubmit, true);
|
||||
}, [rootRef]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="resource-reference-input-actions">
|
||||
{inputActions}
|
||||
{showPolishAction ? (
|
||||
<>
|
||||
/**
|
||||
* 「输入区操作排」这一块:宿主注入的 `inputActions` + 内置的「AI 润色 / 恢复原文」。
|
||||
* 默认渲染在输入盒里;宿主给了 `actionsPortalTarget` 时改渲染到那个容器(Direct 把它放进
|
||||
* 下方工具条,与 `@` / `+` / 模型 / 语音 / 发送 同一行)。
|
||||
*/
|
||||
const inputActionsBlock = (
|
||||
<div className="resource-reference-input-actions">
|
||||
{inputActions}
|
||||
{showPolishAction ? (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="resource-reference-input-polish"
|
||||
aria-label="AI 润色"
|
||||
title="AI 润色"
|
||||
aria-busy={polishing}
|
||||
disabled={disabled || polishing || draftText.trim() === ''}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
onClick={() => void polishPrompt()}
|
||||
>
|
||||
{polishing ? (
|
||||
<Loader2 size={15} className="animate-spin" aria-hidden="true" />
|
||||
) : (
|
||||
<Sparkles size={15} aria-hidden="true" />
|
||||
)}
|
||||
</button>
|
||||
{polishedOriginalText !== null ? (
|
||||
<button
|
||||
type="button"
|
||||
className="resource-reference-input-polish"
|
||||
aria-label="AI 润色"
|
||||
title="AI 润色"
|
||||
aria-busy={polishing}
|
||||
disabled={disabled || polishing || draftText.trim() === ''}
|
||||
className="resource-reference-input-restore"
|
||||
aria-label="恢复原文"
|
||||
title="恢复原文"
|
||||
disabled={disabled || polishing}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
onClick={() => void polishPrompt()}
|
||||
onClick={restoreOriginalPrompt}
|
||||
>
|
||||
{polishing ? (
|
||||
<Loader2
|
||||
size={15}
|
||||
className="animate-spin"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : (
|
||||
<Sparkles size={15} aria-hidden="true" />
|
||||
)}
|
||||
<RotateCcw size={15} aria-hidden="true" />
|
||||
</button>
|
||||
{polishedOriginalText !== null ? (
|
||||
<button
|
||||
type="button"
|
||||
className="resource-reference-input-restore"
|
||||
aria-label="恢复原文"
|
||||
title="恢复原文"
|
||||
disabled={disabled || polishing}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
onClick={restoreOriginalPrompt}
|
||||
>
|
||||
<RotateCcw size={15} aria-hidden="true" />
|
||||
</button>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{actionsPortalTarget
|
||||
? createPortal(inputActionsBlock, actionsPortalTarget)
|
||||
: inputActionsBlock}
|
||||
{/* 提醒面板打开时错误提示只在面板里出现,输入区不重复显示。 */}
|
||||
{/* 「与原文相同 / 已截断」这类提示也要可见:只报失败会让「润色没变化」看起来像按钮坏了。 */}
|
||||
{showPolishAction &&
|
||||
|
||||
@@ -11799,8 +11799,11 @@ button.design-workspace-tree__entry:hover,
|
||||
.project-chat-composer.is-direct-codex {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
/* 三层(输入内容区 / 工具栏 / 状态提示区)之间靠行间距分开,任何一层都不再用
|
||||
绝对定位去压另一层;`gap` 同时给队列 chip 与输入盒之间留出同一档间距。 */
|
||||
/* 单行工具条下限 252 内缩在盒内(左右各 12px 内边距 + 各 1px 边框)→ 外壳最小宽度
|
||||
252 + 24 + 2 = **278**:低于它时不换行、不重叠,内容溢出可见(面板不裁切)。 */
|
||||
min-width: 278px;
|
||||
/* 控件行 / 状态层之间靠行间距分开(状态层没有内容时为 0 高,不占行);
|
||||
`gap` 同时给队列 chip 与输入盒之间留出同一档间距。 */
|
||||
gap: 8px;
|
||||
align-items: stretch;
|
||||
position: relative;
|
||||
@@ -11878,17 +11881,20 @@ button.design-workspace-tree__entry:hover,
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* Direct 控件排:三层布局下允许换行,两轴间距分开写(行 6 / 列 8)。 */
|
||||
/* Direct 控件排:**唯一一行控件,永远不换行**(`@` / `+` / AI 润色 / 模型 / 语音 / 发送 平铺)。
|
||||
单行下限 = Σ 控件最小宽度 + 列间距:`@` 28 + `+` 28 + 润色 28 + 模型钮 64(`min-width: 64px`,
|
||||
名字走省略号)+ 语音 28 + 发送 28 = 204,加 5 个 8px 列间距 = 244;再留 8px 余量 → **252**
|
||||
(夹具实测:自然宽度 290.4、下限 244)。低于 252 时不换行、不重叠,内容溢出可见。 */
|
||||
.game-workbench-chat
|
||||
.project-chat-surface.is-direct-codex
|
||||
.project-chat-composer.is-direct-codex
|
||||
.project-chat-composer-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 6px 8px;
|
||||
min-width: 0;
|
||||
min-width: 252px;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
@@ -11917,12 +11923,15 @@ button.design-workspace-tree__entry:hover,
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Direct 左侧动作组(`@` 引用 / `+` 附件)允许换行:窄面板下它和右侧控件组各占一行。 */
|
||||
/* Direct 左侧动作组(`@` 引用 / `+` 附件 / AI 润色钮)同样不换行:工具条只有一行。 */
|
||||
.game-workbench-chat
|
||||
.project-chat-surface.is-direct-codex
|
||||
.project-chat-composer.is-direct-codex
|
||||
.project-chat-composer-controls-left {
|
||||
flex-wrap: wrap;
|
||||
/* 左组是固定宽度的动作钮(`@` / `+` / 润色),不参与收缩:收缩会让它们溢出、
|
||||
压到右侧的模型钮上;要收缩就由右组的模型钮(名字走省略号)承担。 */
|
||||
flex: 0 0 auto;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
/* 右侧那一组是**不可再压的控件**(推理档 / 模型 / 语音 / 发送):它原来写死
|
||||
@@ -12549,6 +12558,7 @@ button.design-workspace-tree__entry:hover,
|
||||
grid-column: 1;
|
||||
align-self: center;
|
||||
min-width: 0;
|
||||
margin-top: 6px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
@@ -13230,7 +13240,8 @@ button.design-workspace-tree__entry:hover,
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
grid-template-rows: auto auto;
|
||||
align-content: start;
|
||||
gap: 6px 8px;
|
||||
/* 行间距归 0:状态层没有内容时第二行高度为 0,不留幻影行距(间距给状态元素自己)。 */
|
||||
gap: 0 8px;
|
||||
}
|
||||
|
||||
/* 编辑器横跨两列:文字不再被右侧 28px 的操作列长期挤窄。 */
|
||||
@@ -13255,24 +13266,11 @@ button.design-workspace-tree__entry:hover,
|
||||
grid-column: 1;
|
||||
align-self: center;
|
||||
min-width: 0;
|
||||
margin-top: 6px;
|
||||
margin-top: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.game-workbench-chat
|
||||
.project-chat-surface.is-direct-codex
|
||||
.project-chat-composer.is-direct-codex
|
||||
.resource-reference-input
|
||||
.resource-reference-input-actions {
|
||||
grid-row: 2;
|
||||
grid-column: 2;
|
||||
position: static;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* 工具调用折叠块块头改成两行:第一行图标 + 汇总(超长省略),第二行状态 + 用时,箭头右侧跨两行。
|
||||
窄面板里一行塞不下(汇总会被截断、"进行中"还会折成两行),拆成两行后每段都有自己的宽度。 */
|
||||
.game-workbench-chat
|
||||
@@ -13716,13 +13714,37 @@ details.design-agent-reasoning[open]
|
||||
④ 思维强度并入模型菜单:底行只留模型触发钮一颗控件,强度区(原生 `<input type="range">`)
|
||||
嵌在菜单顶部,与模型列表共用同一张卡(`.conversation-model-menu-shell`)。 */
|
||||
|
||||
/* ① 下拉控件:可收缩 + 组内换行;模型名超长走省略号。 */
|
||||
/* 工具条里的「AI 润色 / 恢复原文」挂点(`ResourceReferenceInput` 用 portal 渲染进来):
|
||||
与 `@` / `+` 同组,排在左组最后一个;空输入态只是不显示按钮,不占额外行。 */
|
||||
.game-workbench-chat
|
||||
.project-chat-surface.is-direct-codex
|
||||
.project-chat-composer.is-direct-codex
|
||||
.project-chat-composer-controls-actions {
|
||||
display: inline-flex;
|
||||
/* 不参与收缩:里面的润色钮是 28px 固定方块,槽位被压窄会让它溢出去压到 `+`。 */
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.game-workbench-chat
|
||||
.project-chat-surface.is-direct-codex
|
||||
.project-chat-composer.is-direct-codex
|
||||
.project-chat-composer-controls-actions
|
||||
.resource-reference-input-actions {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
/* ① 下拉控件:可收缩(模型名超长走省略号);整条工具条不换行,控件排只占一行。 */
|
||||
.game-workbench-chat
|
||||
.project-chat-surface.is-direct-codex
|
||||
.project-chat-composer.is-direct-codex
|
||||
.project-chat-composer-controls
|
||||
.conversation-model-select {
|
||||
flex-wrap: wrap;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
|
||||
+12
@@ -133,6 +133,9 @@ export function DirectProjectComposer({
|
||||
}) {
|
||||
const composerRef = useRef<ResourceReferenceInputHandle | null>(null);
|
||||
const composerRootRef = useRef<HTMLDivElement | null>(null);
|
||||
/** 工具条里给「AI 润色 / 恢复原文」的挂点(`ResourceReferenceInput` 用 portal 渲染进来)。 */
|
||||
const [controlsActionsSlot, setControlsActionsSlot] =
|
||||
useState<HTMLSpanElement | null>(null);
|
||||
const pickerRef = useRef<ResourceReferencePickerHandle | null>(null);
|
||||
const modelSelectRef = useRef<ConversationModelSelectHandle>(null);
|
||||
const modelValidateInFlightRef = useRef(false);
|
||||
@@ -263,6 +266,9 @@ export function DirectProjectComposer({
|
||||
placeholder="描述你的想法,或 @ 引用素材"
|
||||
submitSuppressed={pickerOpen}
|
||||
containerRef={composerRootRef}
|
||||
/* 「AI 润色 / 恢复原文」排进下方工具条:与 `@` / `+` / 模型 / 语音 / 发送 同一行,
|
||||
空输入态也不再自带一行(窄面板下那一行看起来像"堆叠")。 */
|
||||
actionsPortalTarget={controlsActionsSlot}
|
||||
/>
|
||||
<div className="project-chat-composer-controls">
|
||||
<div className="project-chat-composer-controls-left">
|
||||
@@ -281,6 +287,12 @@ export function DirectProjectComposer({
|
||||
onPickFiles={(files) => void uploadFiles(files)}
|
||||
onOpenReferencePicker={openPicker}
|
||||
/>
|
||||
{/* 润色 / 恢复原文的挂点(portal 目标):紧跟在 `+` 之后,与输入类动作同组,
|
||||
右侧留给"生成类"控件(模型 / 语音 / 发送)。 */}
|
||||
<span
|
||||
className="project-chat-composer-controls-actions"
|
||||
ref={setControlsActionsSlot}
|
||||
/>
|
||||
</div>
|
||||
<div className="project-chat-composer-controls-right">
|
||||
<ConversationModelSelect
|
||||
|
||||
@@ -221,9 +221,15 @@ function composerMaxHeight() {
|
||||
const trigger = desktopDeclarations(COMPOSER_TRIGGER, COMPOSER_TRIGGER_BASE);
|
||||
|
||||
const editorMaxHeight = pixelValue(editor, 'max-height');
|
||||
const actionsRowHeight = pixelValue(polishButton, 'height');
|
||||
// 输入盒与输入盒外各有一条行间距:前者是「文字区 / 操作排」之间,后者是
|
||||
// 「输入内容区 / 工具栏」之间(三层布局,见 styles.css 文件末尾的输入区区块)。
|
||||
// 「AI 润色 / 恢复原文」已经排进下方工具条(`.project-chat-composer-controls-actions`
|
||||
// 挂点 + `actionsPortalTarget`),不再是输入盒里的第二行:这里只留它的按钮高度断言,
|
||||
// 输入盒的第二行现在只可能放润色状态(没内容时为 0 高)。
|
||||
expect(
|
||||
pixelValue(polishButton, 'height'),
|
||||
'润色钮是 28px 方块,改了要同步改工具条那一排的高度',
|
||||
).toBe(28);
|
||||
// 输入盒与输入盒外各有一条行间距:前者是「文字区 / 润色状态」之间(状态没内容时为 0),
|
||||
// 后者是「输入内容区 / 工具栏」之间。
|
||||
const inputRowGap = gapBox(input).row;
|
||||
const composerRowGap = gapBox(composer).row;
|
||||
const inputPaddingBottom = paddingBox(input).bottom;
|
||||
@@ -249,22 +255,16 @@ function composerMaxHeight() {
|
||||
pixelValue(editor, 'min-height'),
|
||||
'编辑器最小高度是输入盒高度的下界,改了要同步改这里的算式',
|
||||
).toBe(72);
|
||||
// 操作排的高度就是那三只 28px 方钮自己撑起来的:它自己不设 height / min-height,
|
||||
// 一旦设了,网格行高会被顶起来、编辑器的 min-height 也跟着变,算式随之作废。
|
||||
expect(actions.has('height')).toBe(false);
|
||||
expect(actions.has('min-height')).toBe(false);
|
||||
// 操作排必须是网格里的第二行,不能再绝对定位浮到文字上(issue #600 的回归点)。
|
||||
expect(declaration(actions, 'position')).toBe('static');
|
||||
expect(declaration(actions, 'grid-row')).toBe('2');
|
||||
expect(declaration(actions, 'grid-column')).toBe('2');
|
||||
// 输入盒第二行的润色状态行没内容时为 0 高(`gap` 的 row 分量已归 0,状态自己带 6px 上间距)。
|
||||
const statusRowHeight = 0;
|
||||
|
||||
return {
|
||||
actionsRowHeight,
|
||||
polishButtonHeight: pixelValue(polishButton, 'height'),
|
||||
total:
|
||||
composerPaddingTop +
|
||||
editorMaxHeight +
|
||||
inputRowGap +
|
||||
actionsRowHeight +
|
||||
statusRowHeight +
|
||||
inputPaddingBottom +
|
||||
composerRowGap +
|
||||
controlsRowHeight +
|
||||
@@ -327,14 +327,15 @@ describe('陶泥儿对话区:Codex 三段式(顶栏 / 唯一滚动区 / 文
|
||||
|
||||
it('消息列表底部不留给浮层的空白,滚动到底不会多出一段空白(宽屏)', () => {
|
||||
const list = desktopDeclarations(...LIST_SELECTORS);
|
||||
const { total, actionsRowHeight } = composerMaxHeight();
|
||||
const { total, polishButtonHeight } = composerMaxHeight();
|
||||
|
||||
// 输入盒最高高度由真实声明算出来(三层布局,自上而下):
|
||||
// 盒内上内边距 12 + 编辑器 160 + 输入区行距 6 + 操作排 28 + 输入区下内边距 0
|
||||
// + 输入盒与工具栏的行距 8 + 工具栏 30(模型触发钮撑起来的行高)+ 盒内下内边距 12 = 256。
|
||||
expect(actionsRowHeight, '操作排按钮高度变了就要重算下面的算式').toBe(28);
|
||||
// 输入盒最高高度由真实声明算出来(自上而下):
|
||||
// 盒内上内边距 12 + 编辑器 160 + 输入区行距 0(状态行没内容时为 0 高,不留幻影行距)
|
||||
// + 状态行 0 + 输入区下内边距 0 + 输入盒与工具栏的行距 8
|
||||
// + 工具栏 30(模型触发钮撑起来的行高)+ 盒内下内边距 12 = **222**。
|
||||
expect(polishButtonHeight, '润色钮在工具条里仍是 28px 方块').toBe(28);
|
||||
expect(pixelValue(desktopDeclarations(COMPOSER_SUBMIT), 'height')).toBe(28);
|
||||
expect(total).toBe(256);
|
||||
expect(total).toBe(222);
|
||||
|
||||
const paddingBottom = paddingBox(list).bottom;
|
||||
// 新契约:留白归 0。旧版这里是一条 `inset + 输入区最高高度 + 间距` 的算式,
|
||||
@@ -444,6 +445,63 @@ describe('陶泥儿对话区:Codex 三段式(顶栏 / 唯一滚动区 / 文
|
||||
);
|
||||
});
|
||||
|
||||
it('工具条只有一行:七个宽度下都不换行,控件排与外壳带单行最小宽度(含 AI 润色钮)', () => {
|
||||
// 宽度扫描:任何面板宽下工具条都必须 `nowrap`,且样式表里**不再有** Direct 作用域的
|
||||
// `flex-wrap: wrap`(窄面板曾经靠换行避免重叠,现在改成 min-width + 溢出可见)。
|
||||
const CONTROLS_LEFT = `${COMPOSER} .project-chat-composer-controls-left`;
|
||||
const CONTROLS_RIGHT = `${COMPOSER} .project-chat-composer-controls-right`;
|
||||
const CONTROLS_MODEL_SELECT = `${COMPOSER_CONTROLS} .conversation-model-select`;
|
||||
const WIDTHS = [240, 280, 296, 320, 360, 438, 900] as const;
|
||||
for (const width of WIDTHS) {
|
||||
const controls = resolveDeclarations(rules, [COMPOSER_CONTROLS], width);
|
||||
expect(
|
||||
declaration(controls, 'flex-wrap'),
|
||||
`${width}px 下工具条必须保持单行`,
|
||||
).toBe('nowrap');
|
||||
const left = resolveDeclarations(rules, [CONTROLS_LEFT], width);
|
||||
expect(declaration(left, 'flex-wrap'), `${width}px 下左组不换行`).toBe(
|
||||
'nowrap',
|
||||
);
|
||||
const right = resolveDeclarations(rules, [CONTROLS_RIGHT], width);
|
||||
expect(right.has('flex-wrap'), '右组本来就不换行').toBe(false);
|
||||
const modelSelect = resolveDeclarations(
|
||||
rules,
|
||||
[CONTROLS_MODEL_SELECT, `${CONTROLS_RIGHT} .conversation-model-select`],
|
||||
width,
|
||||
);
|
||||
expect(declaration(modelSelect, 'flex-wrap')).toBe('nowrap');
|
||||
}
|
||||
|
||||
// 单行下限:控件排 252px(Σ 控件最小宽度 204 + 5×8 间距 = 244,再留 8 余量),
|
||||
// 外壳 252 + 左右内边距各 12 + 边框各 1 = 278px。夹具实测:自然宽度 290.4。
|
||||
const controls = desktopDeclarations(COMPOSER_CONTROLS);
|
||||
expect(pixelValue(controls, 'min-width')).toBe(252);
|
||||
const composer = desktopDeclarations(
|
||||
COMPOSER,
|
||||
COMPOSER_PLAIN,
|
||||
CONVERSATION_COMPOSER,
|
||||
);
|
||||
expect(
|
||||
pixelValue(composer, 'min-width'),
|
||||
'外壳最小宽度 = 工具条下限 + 左右内边距 + 边框',
|
||||
).toBe(278);
|
||||
|
||||
// 空输入态:组件源码里「AI 润色 / 恢复原文」是渲染到工具条那个挂点里的
|
||||
// (`actionsPortalTarget`),不再是输入盒里的第二行。
|
||||
const composerSource = readFileSync(CHAT_COMPOSER_PATH, 'utf8');
|
||||
const controlsIndex = composerSource.indexOf(
|
||||
'className="project-chat-composer-controls"',
|
||||
);
|
||||
const slotIndex = composerSource.indexOf(
|
||||
'project-chat-composer-controls-actions',
|
||||
);
|
||||
expect(controlsIndex).toBeGreaterThanOrEqual(0);
|
||||
expect(slotIndex).toBeGreaterThan(controlsIndex);
|
||||
expect(composerSource).toContain(
|
||||
'actionsPortalTarget={controlsActionsSlot}',
|
||||
);
|
||||
});
|
||||
|
||||
it('窄屏(390px)输入盒仍在文档流、四周有边距,列表底部没有留白', () => {
|
||||
const conversation = mobileDeclarations(CONVERSATION);
|
||||
const list = mobileDeclarations(...LIST_SELECTORS);
|
||||
|
||||
@@ -116,8 +116,8 @@
|
||||
|
||||
- **现象**:#600 截图里用户输入区域「元素堆叠」——空草稿时「AI 润色」按钮就压在输入内容区右下角(实测与编辑器相交 28×24px),模型控件的失败提示「本地模型服务不可用」直接画在第 3~4 行文字上(70×36px,长草稿时把整段文字盖住);面板越窄越糟:280px 面板下推理档整颗压在「添加文件」上(28×28px),「插入素材引用」与推理档也相交(20×28px)。
|
||||
- **原因**:两条「靠绝对定位把控件挪出流」的旧写法叠在一起:① `.resource-reference-input-actions` 被写成 `position: absolute; right: 0; bottom: 0`,浮到编辑器右下角(同一份样式早前其实是网格第二行,见文件里 11893 行那条注释,被后写的绝对定位规则盖掉);② `ConversationModelSelect` 的 `[role='alert'] / [role='status']` 是 `bottom: 36px` 的浮层,锚点就在输入盒正下方,提示必然落在文字上。另外 `.project-chat-composer-controls-right` 写死 `flex: 0 0 auto`,宽度不够时既不收缩也不换行,整组向右溢出输入盒(360px 面板下溢出 34px、280px 下 114px)。
|
||||
- **处理(现行口径)**:`.project-chat-composer.is-direct-codex` 是单列网格(`gap: 8px`),三层自上而下——输入内容区 / 工具栏 / 状态提示区;输入内容区自己是两行网格(编辑器横跨两列,第二行左放润色状态、右放「AI 润色 / 恢复原文」操作排),操作排 `position: static` 回到行里;工具栏允许换行、右侧组改 `flex: 1 1 auto`,模型钮封顶 160px 且名字走省略号(收缩由它承担,推理档 `flex: 0 0 auto` 不参与收缩,避免把「默认」截成半个字)。**模型失败提示后来改成走浮层**(见本文件顶部 2026-10-04 / 2026-10-05 两条):控件排里既不留常驻红字、也不留常驻状态文字——「控件排内独占一行」那次快照已退役,行高回到由模型触发钮决定。
|
||||
- **判据/取证**:`apps/ai-game-creator-shell/tests/chatDialogFrameLayout.test.ts` 用层叠求值钉住三层几何与操作排 `grid-row: 2 / position: static`、输入盒最高高度算式(12 盒内上内边距 + 160 编辑器 + 6 输入区行距 + 28 操作排 + 0 输入区下内边距 + 8 输入盒与工具栏行距 + **30 工具栏行高(模型触发钮 30px 撑起来,发送/语音是 28px)** + 12 盒内下内边距 = **256**);`tests/appSurface/project-development.suite.ts` 钉住 composer 的 `gap: 8px`。真实渲染对照见 #600 PR:同一夹具在 495/438/360/296/280px 面板 × 空 / 长多行 / 队列+提示 四态下,改前 2~4 处相交(含文字被提示盖住),改后 0 处相交、0 处溢出。
|
||||
- **处理(现行口径)**:`.project-chat-composer.is-direct-codex` 是单列网格(`gap: 8px`);**控件全部集中在唯一一行工具条**——`@` / `+` / 「AI 润色 / 恢复原文」/ 模型 / 语音 / 发送,六颗 `flex-wrap: nowrap` 平铺(润色钮由 `ResourceReferenceInput` 的 `actionsPortalTarget` portal 进工具条,不再自成一行);状态/提示层只在有内容时占行(空输入态第二行为 0 高、行间距为 0,状态自己带 6px 上间距)。窄宽度用 `min-width` 兜(控件排 252px / 外壳 278px)+ 横向溢出可见:**不换行、不堆叠、不重叠**,发送钮仍可点击(夹具 `elementFromPoint` 命中)。模型钮封顶 160px 且名字走省略号(收缩由它承担),左组是固定 28px 动作钮、`flex: 0 0 auto` 不参与收缩。**模型失败提示后来改成走浮层**(见本文件顶部 2026-10-04 / 2026-10-05 两条):控件排里既不留常驻红字、也不留常驻状态文字——「控件排内独占一行」那次快照已退役,行高回到由模型触发钮决定。
|
||||
- **判据/取证**:`apps/ai-game-creator-shell/tests/chatDialogFrameLayout.test.ts` 用层叠求值钉住单行工具条(240 / 280 / 296 / 320 / 360 / 438 / 900 七个宽度下 `flex-wrap` 都是 `nowrap`、控件排 `min-width: 252px`、外壳 `min-width: 278px`)、输入盒最高高度算式(12 盒内上内边距 + 160 编辑器 + 0 输入区行距 + 0 状态行 + 0 输入区下内边距 + 8 输入盒与工具栏行距 + **30 工具栏行高(模型触发钮 30px 撑起来,发送/语音是 28px)** + 12 盒内下内边距 = **222**),以及组件源码里润色钮渲染在工具条挂点(`actionsPortalTarget={controlsActionsSlot}`、挂点在控件排之后);`tests/appSurface/project-development.suite.ts` 钉住 composer 的 `gap: 8px`。真实渲染对照见 #600 PR:同一夹具在 495/438/360/296/280px 面板 × 空 / 长多行 / 队列+提示 四态下,改前 2~4 处相交(含文字被提示盖住),改后 0 处相交、0 处溢出。
|
||||
- **关联**:`apps/ai-game-creator-shell/src/styles.css`(文件末尾「输入区工具栏与状态提示」区块)、`docs/【功能说明】AGC聊天AI润色与发送前提醒-2026-09-10.md`。
|
||||
|
||||
## 2026-10-03 AGC 随包 plugins 的 feature 档位必须与消费方一致,且门禁会因 build.rs 未重跑而假通过
|
||||
|
||||
Reference in New Issue
Block a user