diff --git a/apps/ai-game-creator-shell/src/styles.css b/apps/ai-game-creator-shell/src/styles.css index 2d45b40e7..d2de3cd8b 100644 --- a/apps/ai-game-creator-shell/src/styles.css +++ b/apps/ai-game-creator-shell/src/styles.css @@ -11474,7 +11474,9 @@ button.design-workspace-tree__entry:hover, /* ---- 强度区(组件自带类名;任何宿主共用同一份,没有宿主分支) ---- */ -/* 强度区:菜单顶部,与列表之间只用留白分开(不画横贯分隔线),标题左右层级分明。 */ +/* 强度区:菜单顶部,与列表之间只用留白分开(不画横贯分隔线),标题左右层级分明。 + 左右不留内边距:滑块(轨道 + 圆钮)直接铺满卡片内容宽度,填充段与圆钮两端与轨道两端贴齐 + —— 这条内边距原先只是为档位点排布留位,点删掉后必须归零,否则两端各多 6px 空白。 */ .project-chat-reasoning-effort.is-section { display: grid; /* 单列铺满:`auto` 轨道在这里只按 max-content 定尺,滑块会被压回固有宽度。 */ @@ -11483,12 +11485,12 @@ button.design-workspace-tree__entry:hover, width: 100%; align-self: stretch; gap: 10px; - padding: 4px 6px 2px; + padding: 4px 0 2px; } -/* 滑块与档位点是一个整体:点绝对定位叠在轨道中线上(不受字号 / 行高变化影响)。 */ +/* 滑块:圆钮(20px)与轨道(6px)叠在一起,命中与按 `step` 吸附由原生 range 负责; + 档位圆点删掉后这里不再需要相对定位的容器(点是唯一的绝对定位子元素)。 */ .project-chat-composer-strength-slider { - position: relative; display: grid; grid-template-columns: minmax(0, 1fr); width: 100%; @@ -11634,45 +11636,10 @@ button.design-workspace-tree__entry:hover, 0 2px 6px color-mix(in srgb, var(--platform-accent) 24%, transparent); } -/* 档位点:叠在轨道**之上**(z-index 高于 input),横向跟圆钮用同一套算式—— - 两侧内边距取「圆钮半径 − 半点宽」,`space-between` 让首末点的**中心**正好落在 - 圆钮两端的极限位置,中间各点等距重合;当前档位那一点隐藏(圆钮已经压在那里, - 再画点就会变成"靶心")。 */ -.project-chat-composer-strength-stops { - position: absolute; - top: 50%; - right: 0; - left: 0; - z-index: 2; - display: flex; - align-items: center; - justify-content: space-between; - /* 两侧内边距 = 圆钮半径 10px − 半点宽 3px(点 6px 见下):首末点的**中心**因此正好落在 - 圆钮两端的极限位置,中间各点等距重合。改点宽或钮径时这条与下面的宽高要一起改。 */ - padding: 0 7px; - height: 6px; - transform: translateY(-50%); - pointer-events: none; -} - -.project-chat-composer-strength-stops span { - box-sizing: border-box; - /* 变体 A:点直径 == 轨道高度(6px),不再上下各露 0.5px。 */ - width: 6px; - height: 6px; - border-radius: 999px; - /* 浅暖点:轨道上可见但不抢戏(原来是与圆钮同色的深棕,压在橙色已选段上很跳)。 - 从 `--platform-text-strong` 与轨道色派生,不新造色值;对比度实测见 PR。 */ - background: color-mix( - in srgb, - var(--platform-text-strong) 60%, - var(--platform-subpanel-border) - ); -} - -.project-chat-composer-strength-stops span.is-active { - opacity: 0; -} +/* 档位圆点已删除(用户口径「很丑」,第一选择就是删):连同 `space-between` 排布、 + `z-index: 2` 抬层、`.is-active{opacity:0}` 与点的 `color-mix` 底色一起删掉, + 容器上那条「圆钮半径 − 半点宽」的 `padding: 0 7px` 也随之作废(滑块左右不再留白)。 + 历史成因(点被 6px 轨道盖住、与圆钮两端错位 ±12.2px)由 Git 与 pitfalls 追溯。 */ .project-chat-composer-strength-value { color: var(--platform-text-soft); diff --git a/apps/ai-game-creator-shell/src/view/project-development/chat/components/DirectProjectComposer/ComposerControls.tsx b/apps/ai-game-creator-shell/src/view/project-development/chat/components/DirectProjectComposer/ComposerControls.tsx index 2a4d388a4..bc1ea595a 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/chat/components/DirectProjectComposer/ComposerControls.tsx +++ b/apps/ai-game-creator-shell/src/view/project-development/chat/components/DirectProjectComposer/ComposerControls.tsx @@ -624,21 +624,14 @@ export function ComposerReasoningEffortSelect({ onInput={previewFromEvent} /* 松手 / 抬键 / 失焦才落盘:拖动中写盘会 disable 控件、原生拖拽当场中断。 */ onPointerUp={commitPreview} + /* 触屏取消(pointercancel)与"松手"同一口径:一并提交当前预览, + 否则预览值会滞留到下一次提交或卸载 flush。 */ + onPointerCancel={commitPreview} onKeyUp={commitPreview} onBlur={commitPreview} /> - {/* 档位点叠在轨道上(纯装饰,命中与吸附由原生 range 负责)。 */} - ); diff --git a/apps/ai-game-creator-shell/tests/appSurface/home.suite.ts b/apps/ai-game-creator-shell/tests/appSurface/home.suite.ts index 366e09bee..e0d76bc74 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/home.suite.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/home.suite.ts @@ -225,6 +225,10 @@ export function registerClientHomeTests() { await waitFor(() => expect(slider?.getAttribute('aria-valuetext')).toBe('默认'), ); + // 档位圆点已删除(样式表里与渲染里都不再有那组元素)。 + expect( + document.querySelector('.project-chat-composer-strength-stops'), + ).toBeNull(); const rules = parseStyleSheet( readFileSync( diff --git a/apps/ai-game-creator-shell/tests/chatDialogFrameLayout.test.ts b/apps/ai-game-creator-shell/tests/chatDialogFrameLayout.test.ts index 291947857..0879bb33e 100644 --- a/apps/ai-game-creator-shell/tests/chatDialogFrameLayout.test.ts +++ b/apps/ai-game-creator-shell/tests/chatDialogFrameLayout.test.ts @@ -20,6 +20,10 @@ const CHAT_CONVERSATION_PATH = repoPath( const CHAT_COMPOSER_PATH = repoPath( 'apps/ai-game-creator-shell/src/view/project-development/chat/components/DirectProjectComposer/DirectProjectComposer.tsx', ); +/** 同目录的控件实现(推理强度滑块在这里渲染)。 */ +const CHAT_COMPOSER_CONTROLS_PATH = repoPath( + 'apps/ai-game-creator-shell/src/view/project-development/chat/components/DirectProjectComposer/ComposerControls.tsx', +); function lengthPx(rawValue: string, label: string): number { const value = rawValue.trim(); @@ -164,14 +168,13 @@ const SHARED_CARD = '.conversation-model-select .conversation-model-menu'; const SHARED_SHELL = '.conversation-model-select .conversation-model-menu-shell'; const SHARED_CARD_IN_SHELL = `${SHARED_SHELL} .conversation-model-menu`; -/** 强度区与它那排装饰档位点(issue #600 的复核点:点被轨道压住、横向与圆钮错位)。 - 这些规则与卡片一样已经抬到共享样式面(组件自带类名,不带宿主祖先链)。 */ +/** 强度区与滑块(issue #600 的复核点;档位圆点按用户口径已删除)。 + 这些规则与卡片一样在共享样式面(组件自带类名,不带宿主祖先链)。 */ const STRENGTH_SECTION = '.project-chat-reasoning-effort.is-section'; const STRENGTH_RANGE = '.project-chat-composer-strength-range'; const STRENGTH_THUMB = `${STRENGTH_RANGE}::-webkit-slider-thumb`; +/** 已删除的档位点容器:留着它做反向守卫(谁都别把点写回来)。 */ const STRENGTH_STOPS = '.project-chat-composer-strength-stops'; -const STRENGTH_STOPS_DOT = `${STRENGTH_STOPS} span`; -const STRENGTH_STOPS_DOT_ACTIVE = `${STRENGTH_STOPS} span.is-active`; const TOPBAR = `${CHAT} .project-chat-topbar`; /** 列表实际会命中的全部选择器:direct-codex 直连与基础规则。 */ const LIST_SELECTORS = [MESSAGE_LIST, MESSAGE_LIST_PLAIN]; @@ -395,42 +398,37 @@ describe('陶泥儿对话区:Codex 三段式(顶栏 / 唯一滚动区 / 文 expect(pixelValue(trigger, 'height')).toBe(30); }); - it('档位点叠在轨道之上、横向与圆钮同一套算式,当前档位那一点留给圆钮(宽屏)', () => { + it('档位圆点已删除,滑块契约与「两端贴齐」的强度区内边距仍在(宽屏)', () => { + const section = desktopDeclarations(STRENGTH_SECTION); const range = desktopDeclarations(STRENGTH_RANGE); const thumb = desktopDeclarations(STRENGTH_THUMB); const stops = desktopDeclarations(STRENGTH_STOPS); - const dot = desktopDeclarations(STRENGTH_STOPS_DOT); - const activeDot = desktopDeclarations(STRENGTH_STOPS_DOT_ACTIVE); - // 点必须画在轨道**之上**:range 自己是 `z-index: 1`,点得比它高才不会被 6px 轨道压掉。 - expect(declaration(stops, 'position')).toBe('absolute'); - expect( - Number(declaration(stops, 'z-index')), - '档位点要在轨道(range 的 z-index)之上', - ).toBeGreaterThan(Number(declaration(range, 'z-index'))); - - // 横向跟圆钮共用一套算式:`space-between` + 两侧「圆钮半径 − 半点宽」的内边距, - // 首末点的**中心**落点 = 圆钮两端极限位置(= 轨道两端内缩一个半径)。 - expect(declaration(stops, 'justify-content')).toBe('space-between'); - const knobRadius = pixelValue(thumb, 'width') / 2; - const dotHalf = pixelValue(dot, 'width') / 2; - const stopsPadding = paddingBox(stops); - expect( - stopsPadding.left, - '首点的中心要落在圆钮的左极限位置(改点宽/钮径都要同步这条算式)', - ).toBe(knobRadius - dotHalf); - expect(stopsPadding.right).toBe(knobRadius - dotHalf); - - // 当前档位那一点不画:圆钮已经压在那儿,再画出来就是"靶心"。 - expect(pixelValue(activeDot, 'opacity')).toBe(0); - // 点:直径 == 轨道高度(6px,不再上下各露 0.5px),浅暖色从既有 token 派生(对比度实测见 PR)。 - expect(pixelValue(dot, 'width')).toBe(6); - expect(pixelValue(dot, 'height')).toBe(pixelValue(stops, 'height')); - expect(declaration(dot, 'background')).toContain('color-mix'); - expect(declaration(dot, 'background')).toContain( - 'var(--platform-subpanel-border)', + // 反向守卫①:样式表里**不再有**档位点的规则(谁把点写回来,这条就红)。 + expect(stops.has('position')).toBe(false); + expect(stops.size).toBe(0); + // 反向守卫②:组件源码里也不再渲染那组点。 + const controlsSource = readFileSync(CHAT_COMPOSER_CONTROLS_PATH, 'utf8'); + expect(controlsSource).not.toContain( + 'project-chat-composer-strength-stops', ); - expect(dot.has('border')).toBe(false); + + // 滑块本体契约不变:铺满卡片内容宽度,轨道 6px、圆钮 20px。 + expect(declaration(range, 'width')).toBe('100%'); + expect(pixelValue(range, 'height')).toBe(26); + expect(pixelValue(thumb, 'width')).toBe(20); + expect(pixelValue(thumb, 'height')).toBe(20); + + // 强度区左右不留内边距(那条 padding 原先只服务档位点排布):填充段与圆钮两端与轨道贴齐, + // 中间不出现多余留白。上下留白不动(4 / 2)。 + const sectionPadding = paddingBox(section); + expect( + sectionPadding.left, + '滑块左右不留白:点删掉后这条内边距必须归零', + ).toBe(0); + expect(sectionPadding.right).toBe(0); + expect(sectionPadding.top).toBe(4); + expect(sectionPadding.bottom).toBe(2); }); it('窄屏(390px)输入盒仍在文档流、四周有边距,列表底部没有留白', () => { diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index 13ecd716b..8c5dfd4b1 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -4,7 +4,7 @@ ## 2026-10-05 PR #607 复核修复:档位点对齐/对比度、状态文案也走浮层、键盘去重、卸载 flush -- **档位点被轨道压住且与圆钮错位(D1)**:旧实现把 5 个点放进 `grid-auto-columns: 1fr`,点中心落在 22.19 / 66.59 / 110.98 / 155.39 / 199.80,而圆钮中心只在 `10 + ratio * (W - 20)`(10 / 60.5 / 111 / 161.5 / 212)——两端各差 **±12.2px**;且点画在 range(`z-index: 1`)**下面**,实测点中心像素被 6px 轨道盖成轨道色,点填充与相邻轨道对比度只有 **1.21:1**。现行口径:容器 `display: flex; justify-content: space-between; padding: 0 7px`(7 = 圆钮半径 10 − 半点宽 3),首末点的**中心**因此正好落在圆钮两端极限位置;容器 `z-index: 2`(高于 range)把点画到轨道之上;**当前档位那一点 `opacity: 0`**(圆钮已经压在那儿,再画就是"靶心");点 6px(= 轨道高度)、`background: color-mix(in srgb, var(--platform-text-strong) 60%, var(--platform-subpanel-border))`。实测(无头 Chromium / 222px 轨道):5 个点中心与圆钮算式偏移 **全部 0.00px**(旧实现 +12.19/+6.09/−0.02/−6.11/−12.20),点中心像素 `rgb(126,100,85)`、与相邻轨道对比度 **3.83:1**(旧 1.21~1.23:1;再上一版深棕点是 10.5:1,用户口径"做浅一点"后取这个值,仍 ≥3:1);当前档位那一点被圆钮覆盖(点中心像素 = 圆钮填充色,圆钮中心与点中心差 ≤0.5px)。**边界**:点落在**橙色已选段**(`--platform-accent`)上时对比度只有 1.54:1——"变浅"和"已选段上也 ≥3:1"不可兼得(两边都要 ≥3:1 只能是接近黑棕),本项目按"不抢戏"优先,取值由这条 `color-mix` 的百分比统一调。 +- **档位圆点已删除(D1 的收口)**:用户口径是「圆点很丑,干脆删了吧,要做就做成几乎透明的」——按第一选择**删除**(未做半透明备选)。现在滑块只剩轨道 + 圆钮:轨道 6px 圆头、已选段 `--platform-accent` 由 `--strength-ratio` 驱动(`calc(20px + ratio * (100% - 20px))`)、圆钮 20px 实心暖白(`--platform-panel-fill` + `--platform-subpanel-border` 1px 描边 + `color-mix` 柔影);强度区横向内边距 `4px 6px 2px` → `4px 0 2px`(滑块铺满卡片内容宽度,填充段与圆钮两端与轨道两端贴齐);相关 CSS(`space-between` 排布 / `z-index: 2` 抬层 / `.is-active{opacity:0}` / 点的 `color-mix` 底色 / 只为点对齐的 `padding: 0 7px`)与渲染标记一并删除。**判据**:`chatDialogFrameLayout.test.ts` 反向守卫(样式表里不再有 `.project-chat-composer-strength-stops` 规则、组件源码不再渲染该类名;滑块契约仍在:宽 100% / 高 26 / 圆钮 20×20、强度区左右内边距 0)+ `home.suite.ts` 首页菜单里查不到那组点。实测(447 视口,像素扫描):轨道 90..324(宽 234 = 卡片内容宽),档位 0 时圆钮左缘 90.5(距轨道左端 0.5px)、档位 4 时圆钮右缘 322.3(距右端 1.8px,扫描行不在圆钮正中所以略窄),填充段始终铺到圆钮右缘止。**历史成因(只留一句,细节由 Git 追溯)**:圆点此前被 6px 轨道盖住、且与圆钮两端错位 ±12.2px,曾用「抬到轨道之上 + space-between 对齐 + 浅暖色」修过一轮,最终按用户口径整体删除。 - **状态文案(`role="status"`)也会顶高那一排(D2)**:`notice`(「模型列表已刷新」「正在刷新模型列表」「默认模型已停用」、推理档读取/保存失败)原先无条件渲染成控件里的 `role="status"`,`toast` 形态只接管了 `error`,于是 Direct 输入区仍会被一行状态文字顶成两行高。现行口径:`toast` 形态下失败与状态**分两条通道**(`onUnavailable` / `onNotice`,各自去重、清空后可再报),状态用 `tone="neutral"`、失败用 `tone="error"`;`errorPresentation === 'toast'` 时两种常驻行都不渲染。判据:`chat-composer.suite.ts`「routes model-list notices to the toast…」断言 Direct 输入区子树里 `[role='status']` 与 `[role='alert']` **都为 0**,且浮层在输入区之外。 - **键盘长按每个事件都落盘(D5)**:range 的方向键在 Chromium 里每次重复都会补 `change`,旧行为 ~30 次/秒全量写盘(写链 + 持久化 + 回读)。现行口径:`selectEffort` 先比对「最近一次**落盘成功**的档位」(`persistedEffortRef`,挂载回读时就种上),相同则不写;连续调整只写**每个新档位**一次,钳位后的重复事件 0 次。实测(真实按键序列):单击方向键 **1** 次落盘;长按 1.5s(34 个重复事件)**4** 次(`low/medium/high/max`,即每个新档位一次);已在最高档继续长按 1.2s(28 个事件)**0** 次。 - **拖动中途卸载会丢档位(D6)**:预览值只在 `change`/`keyup`/`pointerup`/`blur` 落盘,拖到一半被 Esc 关掉菜单或宿主卸载时不会提交。现行口径:卸载 cleanup 里 flush 一次(读 ref,不碰已卸载组件的 state)。实测:拖动到「中」未松手时落盘 0 次,`unmount()` 后恰好 **1** 次(`medium`);单次拖动(按下 + 6 步横移 + 松手)全程 **1** 次(`high`),终值 = 松手位置。 @@ -21,7 +21,7 @@ - **判据/取证**:`tests/appSurface/home.suite.ts`「首页模型菜单复用同一套共享样式面」——首页实例的 `.conversation-model-menu` 的祖先链里**没有** `.game-workbench-chat`,而只靠 `.conversation-model-select …` 就能解析出 16px 圆角 / 12px 内边距 / 34px 行,且菜单里确实有原生滑块(`input[type="range"]` 带 `aria-label="思维强度"`);`tests/chatDialogFrameLayout.test.ts` 的外壳用例按新选择器集合更新(锚点与卡片观感都来自共享面,工作台作用域只剩工具栏几何)。真机实测(无头 Chromium,同一夹具同一裁剪框):首页那颗 161×309 / 10px / 5px / 32px → **260×194 / 16px / 12px / 34px**(与工作台卡片逐项一致);工作台那颗外壳 260×262.5、内层列表 234×170、行 34/9px **前后逐值不变**。 - **边界**:策划面与画布也复用同一个组件 → 它们的菜单观感同样变成这张卡片(有意为之;窄面板下卡片最小宽度 260px,仍受 `calc(100vw - 24px)` 与各自锚点约束)。若某个宿主需要另一套尺寸,在**该宿主作用域**里覆盖,不要再把观感抄第二份。 - **后续(同一轮用户追加)**:强度区与外壳锚点也一并抬到共享面——`view/home/index.tsx` 给首页那颗传了**同一个** `ComposerReasoningEffortSelect`(`presentation="section"`,读写仍是组件自己的 `read_game_creator_app_config` / `select_game_creator_reasoning_effort`),首页因此与工作台逐项同形:外壳 260×262.5、强度区 234×60.5、列表 234×170、滑块 222×26(447 与 1200 两档实测一致;工作台回归后逐值不变)。抬升时必须删掉工作台作用域里两条**死重**规则(给旧独立「推理档」控件排版的 `display: inline-flex` 一族),否则它们会以更高特异性盖掉共享面的 `display: grid`,把强度区重新算宽 24px。 -- **滑块两点(用户口径)**:①档位点从深棕改成派生浅暖色(见本文件 2026-10-05 D1 条);②"进度条高度与滑块高度一致"按**变体 A** 落地——填充段本来就是轨道高度(6px),把档位点也收到 6px(不再上下各露 0.5px);变体 B(填充段与点都做成 20px = 圆钮高度)只在夹具里渲染出图,未提交。 +- **滑块两点(用户口径 → 最终收口)**:档位圆点先是改成派生浅暖色、直径收到 6px(= 轨道高度),随后按用户「圆点删了吧」的口径**整体删除**(见上面 D1 条)。「进度条高度与滑块高度一致」保留两种解释对照:**变体 A 落地**(填充段 = 轨道高度 6px),**变体 B**(填充段做成 20px = 圆钮高度)只在夹具里渲染出图、未提交。 ## 2026-10-04 把非素材任务塞进「生成任务」账本:taskType 维度、v1 兼容与 live 集合