From 928a0ea13e45aa5e436b7313d3bd67d90eb2cccf Mon Sep 17 00:00:00 2001 From: Suzumiya Date: Mon, 14 Sep 2026 18:16:56 +0800 Subject: [PATCH] =?UTF-8?q?=E7=B4=A0=E6=9D=90=E7=B1=BB=E5=9E=8B=E9=80=89?= =?UTF-8?q?=E9=A1=B9=E6=94=B9=E6=88=90=E7=BA=B5=E5=90=91=E5=8D=95=E9=80=89?= =?UTF-8?q?=E5=88=97=E8=A1=A8=EF=BC=8C=E4=B8=8D=E5=86=8D=E6=A8=AA=E6=8E=92?= =?UTF-8?q?=E6=88=90=E4=B8=80=E6=9D=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ResourceTypePanel:选项区从 PlatformSegmentedTabs(3~6 列网格,6 项正好挤成一行)换成纵向单选列表,复用共享的 PlatformNavigableListItem 作行骨架 - ResourceTypePanel:容器改为 role="radiogroup" + aria-label,每项 role="radio" + aria-checked,选中态与读屏读的是同一个属性 - ResourceTypePanel:roving tabindex(只有选中项 tabIndex=0)与方向键在选项间移动焦点,走到头不越界;方向键不顺手选中,Enter/Space 才落盘(选中=一次 CAS 写盘,浏览选项不该连环写盘) - ResourceTypePanel:选中行额外渲染勾选图标作为第二视觉线索,图标 aria-hidden 不污染可读名字 - resourceTypePanel.css:选项容器改成单列网格(grid-template-columns: minmax(0, 1fr) + grid-auto-flow: row + align-content: start),一行一个选项 - resourceTypePanel.css:选项行补 width/min-width/min-height: 44px 与 overflow-wrap,移动端热区与窄屏换行都不依赖共享件的工具类 - resourceTypePanel.css:选中态由 [aria-checked='true'] 驱动并显式提权到 (0,3,0) 以上(含覆盖 :hover:not(:disabled)),避免被共享列表行的悬停底色顶掉 - resourceTypePanel.css:滚动从 body 挪到选项列表(列表现有 max-height: min(320px, 40dvh) + overflow-y: auto),body 改为 auto/minmax(0,1fr)/auto 三段,标题、素材名与错误提示不跟着滚 - 测试:新增「选项区是纵向单列列表」用例,按 styleCascade 解析真实生效声明(360/390/1440 三档宽度都是单列、行满宽、热区 44px),并断言 6 项都是 radiogroup 的直接子元素 - 测试:新增「单选语义与键盘」用例(radiogroup/radio + aria-checked + roving tabindex,方向键只移焦点不落盘,Enter 才写盘) - 测试:新增「选中态由 aria-checked 驱动并压过共享列表行悬停底色」用例 - 测试:把「面板有高度上界且中间行可滚动」改成「选项区有上界并独立滚动,标题与错误提示不滚」 - 测试:集成用例的类型选项查询从 button/aria-pressed 改为 radio/aria-checked --- .../project-workspace/resourceTypePanel.css | 75 ++++- .../project-development/ResourceTypePanel.tsx | 87 ++++- .../projectResourceLiveIntegration.test.tsx | 14 +- .../tests/resourceTypePanel.test.tsx | 305 +++++++++++++++--- 4 files changed, 412 insertions(+), 69 deletions(-) diff --git a/apps/ai-game-creator-shell/src/features/project-workspace/resourceTypePanel.css b/apps/ai-game-creator-shell/src/features/project-workspace/resourceTypePanel.css index f582f3885..fb9260aa6 100644 --- a/apps/ai-game-creator-shell/src/features/project-workspace/resourceTypePanel.css +++ b/apps/ai-game-creator-shell/src/features/project-workspace/resourceTypePanel.css @@ -1,12 +1,12 @@ /* - * 「设置素材类型」面板的弹窗骨架与信息浮层的类型入口。 + * 「设置素材类型」面板的弹窗骨架、纵向单选列表与信息浮层的类型入口。 * * 单独一个文件而不是塞进 styles.css:与「编辑素材标签」面板当初同样的理由 —— * 这份样式只服务本次的素材类型入口,与工作台其它区块没有共享选择器,独立文件让改动 * 边界更清楚,也不会与同一时段其它 Agent 在 styles.css 里的编辑互相踩。 * * 骨架沿用「编辑素材标签」那套三段式契约(`auto / minmax(0, 1fr)`):标题常驻、 - * 中间一行可压缩可滚动、`max-height` 兜住上界。类型面板没有底部按钮,所以只有两行。 + * 中间一行可压缩、`max-height` 兜住上界。类型面板没有底部按钮,所以只有两行。 */ .game-resource-type-dialog { width: min(480px, 100%); @@ -15,16 +15,16 @@ } /* - * 滚动落在 body 这一行:`min-height: 0` 是网格项能被 `1fr` 压缩的前提, - * 否则内容高度会顶回轨道、`overflow-y` 永远不触发。 + * body 分三段:提示 / 选项列表 / 错误提示。 + * + * `min-height: 0` 是网格项能被 `1fr` 压缩的前提;**滚动不在这里**——滚动权交给选项列表 + * (见下),否则往下滚时素材名和错误提示会跟着跑掉,用户看不到"改的是哪件素材、为什么失败"。 */ .game-resource-type-body { display: grid; + grid-template-rows: auto minmax(0, 1fr) auto; gap: 10px; min-height: 0; - overflow-y: auto; - overscroll-behavior: contain; - scrollbar-gutter: stable; } /* @@ -36,6 +36,67 @@ font-size: 12px; } +/* + * 纵向单选列表(`role="radiogroup"`):**一行一个选项**。 + * + * 之前 6 项横排在一条里(`PlatformSegmentedTabs` 的 3~6 列网格),窄屏上互相叠字读不出来。 + * 单列网格 + 按行流向是"每项一行、互不重叠"的充分条件:只声明一列,6 个子元素必然上下排 6 行, + * 不存在两项挤一行的可能。选项多时列表自己滚(`max-height` + `overflow-y: auto`), + * 面板不会被撑高。移动端优先:360px 宽的窄屏同样是这一套声明(没有按宽度改列数的媒体查询)。 + */ +.game-resource-type-options { + display: grid; + grid-template-columns: minmax(0, 1fr); + grid-auto-flow: row; + align-content: start; + gap: 6px; + min-height: 0; + max-height: min(320px, 40dvh); + overflow-y: auto; + overscroll-behavior: contain; + scrollbar-gutter: stable; +} + +/* + * 单个选项行:复用共享的 `PlatformNavigableListItem` 骨架(w-full / flex / text-left / + * 圆角 / 悬停 / 焦点环都由它给),这里只补"整行可点 + 明确选中态"的表现。 + * + * `width/min-width` 显式写出来,不依赖共享件里的 Tailwind `w-full`:这一行是不是满宽 + * 决定了"一项一行"能不能成立,不能挂在另一份文件的工具类上。 + * `min-height: 44px` 是移动端点击热区下限;`overflow-wrap` 让长选项名在窄屏换行而不是溢出。 + */ +.game-resource-type-option { + width: 100%; + min-width: 0; + min-height: 44px; + padding: 8px 12px; + border: 1px solid var(--platform-subpanel-border); + background: rgb(255 255 255 / 62%); + color: var(--platform-text-base); + font-size: 13px; + font-weight: 600; + overflow-wrap: anywhere; +} + +.game-resource-type-option:hover:not(:disabled) { + border-color: var(--platform-surface-hover-border); +} + +/* + * 选中态完全由 `aria-checked="true"` 驱动:视觉与读屏读的是同一个属性,不会各说一套。 + * + * 选择器显式提权到 (0,3,0) 以上:共享列表行自带的 `.platform-navigable-list-item:hover:not(:disabled)` + * 也是 (0,3,0),只写 `.game-resource-type-option[aria-checked='true']`((0,2,0))会在悬停时 + * 被它的底色顶掉;带 `:hover:not(:disabled)` 的那条 (0,5,0) 保证选中行悬停时也不变色。 + */ +.game-resource-type-options .game-resource-type-option[aria-checked='true'], +.game-resource-type-options + .game-resource-type-option[aria-checked='true']:hover:not(:disabled) { + border-color: var(--platform-warm-border); + background: var(--platform-warm-bg); + color: var(--platform-text-strong); +} + .game-resource-type-error { margin: 0; color: #b3261e; diff --git a/apps/ai-game-creator-shell/src/view/project-development/ResourceTypePanel.tsx b/apps/ai-game-creator-shell/src/view/project-development/ResourceTypePanel.tsx index 07b254267..3a7144160 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/ResourceTypePanel.tsx +++ b/apps/ai-game-creator-shell/src/view/project-development/ResourceTypePanel.tsx @@ -1,8 +1,13 @@ import '../../features/project-workspace/resourceTypePanel.css'; -import { useState } from 'react'; +import { Check } from 'lucide-react'; +import { + type KeyboardEvent as ReactKeyboardEvent, + useRef, + useState, +} from 'react'; -import { PlatformSegmentedTabs } from '../../../../../packages/shared/src/components/PlatformSegmentedTabs'; +import { PlatformNavigableListItem } from '../../../../../packages/shared/src/components/PlatformNavigableListItem'; import { GAME_CREATION_APP_ASSET_CATEGORIES, type GameCreationAppAssetCategory, @@ -81,6 +86,36 @@ export function ResourceTypePanel({ const [saving, setSaving] = useState(false); const [error, setError] = useState(null); const activeCategory = pendingCategory ?? gameCreationAppAssetCategory(asset); + const optionsRef = useRef(null); + + /** + * 单选组的键盘口径:Tab 进组只停一次(roving tabindex,见下面的 `tabIndex`); + * 方向键在选项之间移动**焦点**,Enter/Space(`