继续收口首页导航行与暗色弹窗底栏

将首页排行行与分类行切换为 PlatformNavigableListItem
将 NPC 交易详情与地图切场确认底栏收口到 PlatformDarkModalFooter
补充首页与弹窗回归测试并更新 PlatformUiKit 收口计划与共享决策记录
This commit is contained in:
2026-06-11 05:06:07 +08:00
parent ce13bdbb02
commit 193e4f0e96
8 changed files with 111 additions and 83 deletions

View File

@@ -57,8 +57,10 @@
- 2026-06-11 追加:暗色 / 像素 modal 的标准 footer 布局统一抽到 `src/components/common/PlatformDarkModalFooter.tsx`;该组件只负责 dark footer 的分隔线、padding 和常见动作区排布,不持有“取消 / 确认”业务语义。`NpcModals.tsx` 的交易 / 赠礼 / 招募 footer、`SelectionCustomizationModals.tsx``SelectionModal` footer、`RpgAdventurePanelOverlays.tsx` 的 goal panel footer以及 `InventoryItemViews.tsx` 的详情 footer wrapper 已接入sticky 工作台 footer、正文内单 CTA 收尾和 runtime HUD 工具条暂不并入这一抽象。 - 2026-06-11 追加:暗色 / 像素 modal 的标准 footer 布局统一抽到 `src/components/common/PlatformDarkModalFooter.tsx`;该组件只负责 dark footer 的分隔线、padding 和常见动作区排布,不持有“取消 / 确认”业务语义。`NpcModals.tsx` 的交易 / 赠礼 / 招募 footer、`SelectionCustomizationModals.tsx``SelectionModal` footer、`RpgAdventurePanelOverlays.tsx` 的 goal panel footer以及 `InventoryItemViews.tsx` 的详情 footer wrapper 已接入sticky 工作台 footer、正文内单 CTA 收尾和 runtime HUD 工具条暂不并入这一抽象。
- 2026-06-11 追加:桌面首页里的轻量可点击扁平行开始统一收口到 `src/components/common/PlatformNavigableListItem.tsx`;目前已覆盖 `RpgEntryHomeView.tsx` 的搜索结果行、桌面“最近作品”、桌面“最近浏览”以及桌面“今日游戏”趋势行。组件只承接 `button + left content + right affordance` 结构、默认 `type="button"``leading / trailing` 插槽,暂不扩成覆盖教培 promo card、分类卡片、世界卡或 runtime 列表项的万能 row primitive。 - 2026-06-11 追加:桌面首页里的轻量可点击扁平行开始统一收口到 `src/components/common/PlatformNavigableListItem.tsx`;目前已覆盖 `RpgEntryHomeView.tsx` 的搜索结果行、桌面“最近作品”、桌面“最近浏览”以及桌面“今日游戏”趋势行。组件只承接 `button + left content + right affordance` 结构、默认 `type="button"``leading / trailing` 插槽,暂不扩成覆盖教培 promo card、分类卡片、世界卡或 runtime 列表项的万能 row primitive。
- 2026-06-11 追加:`PlatformNavigableListItem` 继续扩展到 profile 设置行;`src/components/platform-entry/PlatformProfilePrimitives.tsx``ProfileSettingsRow` 已改成委托共享 `button + leading + trailing` 骨架,继续保留本地 `platform-profile-settings-row` class 承接分隔线、icon 胶囊和字号微调。后续 profile / 账户中心里的同类轻量导航行优先直接复用共享行骨架,不再回退成原生 `<button>` 手写布局。 - 2026-06-11 追加:`PlatformNavigableListItem` 继续扩展到 profile 设置行;`src/components/platform-entry/PlatformProfilePrimitives.tsx``ProfileSettingsRow` 已改成委托共享 `button + leading + trailing` 骨架,继续保留本地 `platform-profile-settings-row` class 承接分隔线、icon 胶囊和字号微调。后续 profile / 账户中心里的同类轻量导航行优先直接复用共享行骨架,不再回退成原生 `<button>` 手写布局。
- 2026-06-11 追加:`PlatformNavigableListItem` 继续扩展到 RPG 首页公开列表里的排行行与分类行;`RpgEntryHomeView.tsx``PlatformRankingItem``PlatformCategoryGameItem` 已改成委托共享 `button + leading + body + trailing` 骨架,同时保留 `platform-ranking-item__*``platform-category-game-item__*` 局部 class 承接封面、metric、badge、摘要和右侧 `试玩 / 进入` affordance。后续首页 / 发现页里同类浅色导航行优先沿“共享骨架 + 本地皮肤 class”推进不再为了这类 row 回退成原生 `<button>` 手写布局。
- 2026-06-11 追加:`PlatformAsyncStatePanel` 继续补齐 RPG 首页分类分支;移动端“发现 -> 分类”、桌面发现页“分类”和桌面首页“作品分类”模块现在都统一委托共享状态壳切换外层 `loading / empty / content`,分类控制条与排序按钮继续留在内容 slot 中。筛选后无结果的“当前筛选下没有作品。”也统一改成内层 `PlatformAsyncStatePanel` 切换,不再在三处 JSX 中各自维护嵌套 ternary。 - 2026-06-11 追加:`PlatformAsyncStatePanel` 继续补齐 RPG 首页分类分支;移动端“发现 -> 分类”、桌面发现页“分类”和桌面首页“作品分类”模块现在都统一委托共享状态壳切换外层 `loading / empty / content`,分类控制条与排序按钮继续留在内容 slot 中。筛选后无结果的“当前筛选下没有作品。”也统一改成内层 `PlatformAsyncStatePanel` 切换,不再在三处 JSX 中各自维护嵌套 ternary。
- 2026-06-11 追加:`PlatformDarkModalFooter` 不只收动作按钮区,也继续覆盖纯内容 footer`CompanionCampModal.tsx` 底部“营地气氛”区域已改成 `layout="content"` + `padding="roomy"` 的共享 footer frame保留原有文案和卡片布局不再单独手写 `border-t border-white/10 px-5 py-4` - 2026-06-11 追加:`PlatformDarkModalFooter` 不只收动作按钮区,也继续覆盖纯内容 footer`CompanionCampModal.tsx` 底部“营地气氛”区域已改成 `layout="content"` + `padding="roomy"` 的共享 footer frame保留原有文案和卡片布局不再单独手写 `border-t border-white/10 px-5 py-4`
- 2026-06-11 追加:`PlatformDarkModalFooter` 继续从标准双按钮 footer 扩到 detail / confirm 收尾;`NpcModals.tsx` 的交易详情 footer 和 `MapModal.tsx` 的场景切换确认 footer 已改成复用同一个 dark footer frame即使只有单个“关闭”按钮也不再手写 `flex justify-end`。这条抽象继续只覆盖 dark / pixel modal 里的底部分隔线与常规动作区排布,不向白底 profile 弹窗 footer、sticky 工作台 footer 或运行态 HUD 工具条扩张。
- 2026-06-11 追加:`PlatformProfileModalShell` 继续补齐标准 footer 插槽,直接透传 `UnifiedModal.footer``footerClassName``RpgEntryHomeView.tsx` 的昵称修改弹窗已改成标准 profile footer不再把双按钮动作区手写在 body 末尾。后续个人中心里同类“表单内容 + 底部双按钮”弹窗优先走壳层 footer 接法。 - 2026-06-11 追加:`PlatformProfileModalShell` 继续补齐标准 footer 插槽,直接透传 `UnifiedModal.footer``footerClassName``RpgEntryHomeView.tsx` 的昵称修改弹窗已改成标准 profile footer不再把双按钮动作区手写在 body 末尾。后续个人中心里同类“表单内容 + 底部双按钮”弹窗优先走壳层 footer 接法。
- 2026-06-11 追加:`PlatformProfileModalShell` 的标准 footer 接法继续扩展到单 CTA 表单收尾;`PlatformProfileRewardCodeRedeemModal.tsx` 的兑换按钮已迁到壳层 footerbody 只保留输入和反馈消息。`PlatformAsyncStatePanel` 同日继续扩展到 `PlatformAssetPickerGrid``VisualNovelSavePanel.tsx``AccountModal.tsx` 的账号安全三个子区块;其中公共素材网格继续把 `error` banner 放在状态壳外层,保持错误提示可与加载态或内容并存的原语义。 - 2026-06-11 追加:`PlatformProfileModalShell` 的标准 footer 接法继续扩展到单 CTA 表单收尾;`PlatformProfileRewardCodeRedeemModal.tsx` 的兑换按钮已迁到壳层 footerbody 只保留输入和反馈消息。`PlatformAsyncStatePanel` 同日继续扩展到 `PlatformAssetPickerGrid``VisualNovelSavePanel.tsx``AccountModal.tsx` 的账号安全三个子区块;其中公共素材网格继续把 `error` banner 放在状态壳外层,保持错误提示可与加载态或内容并存的原语义。
- 2026-06-11 追加:按钮层继续补齐轻量漏网项。`PlatformTagEditor.tsx` 的标签 chip 删除入口已改成紧凑 `PlatformIconButton`,保留透明背景和原 chip 高度;`RpgEntryCharacterSelectView.tsx` 的两处“返回”按钮统一沉到局部 `CharacterSelectBackButton`,底层委托 `PlatformActionButton surface="editorDark"`。同日 `GenerationProgressHero.tsx` 新增 `GenerationHeaderBackButton``CustomWorldGenerationView.tsx``BarkBattleGeneratingView.tsx` 已开始复用这套暖色生成页返回入口骨架;后续同类轻量返回按钮与 chip 删除按钮优先继续沿共享按钮 + 薄包装的方向推进。 - 2026-06-11 追加:按钮层继续补齐轻量漏网项。`PlatformTagEditor.tsx` 的标签 chip 删除入口已改成紧凑 `PlatformIconButton`,保留透明背景和原 chip 高度;`RpgEntryCharacterSelectView.tsx` 的两处“返回”按钮统一沉到局部 `CharacterSelectBackButton`,底层委托 `PlatformActionButton surface="editorDark"`。同日 `GenerationProgressHero.tsx` 新增 `GenerationHeaderBackButton``CustomWorldGenerationView.tsx``BarkBattleGeneratingView.tsx` 已开始复用这套暖色生成页返回入口骨架;后续同类轻量返回按钮与 chip 删除按钮优先继续沿共享按钮 + 薄包装的方向推进。

View File

@@ -269,6 +269,8 @@
19.3.43. 轻量按钮漏网继续向共享按钮收口:`PlatformTagEditor.tsx` 的标签 chip 删除入口已改成紧凑 `PlatformIconButton`,保留 `label="删除标签 ${tag}"`、透明背景和原 chip 高度,不再手写裸 `<button>``RpgEntryCharacterSelectView.tsx` 两处重复的“返回”按钮已统一沉到文件内 `CharacterSelectBackButton`,底层委托 `PlatformActionButton surface="editorDark"`,保留原有暗色视觉与文案。后续同类“局部 chip 删除按钮”优先先用 `PlatformIconButton` 压缩尺寸和视觉;暗色轻量返回 / 返回上一级 CTA 则优先用 `PlatformActionButton surface="editorDark"` 包一层局部 helper不再复制原生 `<button>` class。验证命令`npx vitest run src/components/common/PlatformTagEditor.test.tsx src/components/rpg-entry/RpgEntryCharacterSelectView.test.tsx``npm run typecheck``npm run check:encoding``git diff --check` 19.3.43. 轻量按钮漏网继续向共享按钮收口:`PlatformTagEditor.tsx` 的标签 chip 删除入口已改成紧凑 `PlatformIconButton`,保留 `label="删除标签 ${tag}"`、透明背景和原 chip 高度,不再手写裸 `<button>``RpgEntryCharacterSelectView.tsx` 两处重复的“返回”按钮已统一沉到文件内 `CharacterSelectBackButton`,底层委托 `PlatformActionButton surface="editorDark"`,保留原有暗色视觉与文案。后续同类“局部 chip 删除按钮”优先先用 `PlatformIconButton` 压缩尺寸和视觉;暗色轻量返回 / 返回上一级 CTA 则优先用 `PlatformActionButton surface="editorDark"` 包一层局部 helper不再复制原生 `<button>` class。验证命令`npx vitest run src/components/common/PlatformTagEditor.test.tsx src/components/rpg-entry/RpgEntryCharacterSelectView.test.tsx``npm run typecheck``npm run check:encoding``git diff --check`
19.3.44. 暖色生成页顶部返回入口开始沉淀共享壳:`GenerationProgressHero.tsx` 新增 `GenerationHeaderBackButton`,统一承接 `ArrowLeft + 文案 + 透明背景` 的暖色生成页返回按钮骨架,并底层复用 `PlatformIconButton variant="darkMini"``CustomWorldGenerationView.tsx``BarkBattleGeneratingView.tsx` 已接入,继续保留各自 `backLabel`、禁用态和局部暖色文字样式。后续同类生成页、等待页或暖色 hero 顶栏若只是“左箭头 + 返回文案”的轻量返回入口,优先复用这个小组件,不再各自手写 `ArrowLeft`、透明按钮背景和字号间距。验证命令:`npx vitest run src/components/CustomWorldGenerationView.test.tsx src/components/bark-battle-creation/BarkBattleGeneratingView.test.tsx src/components/common/PlatformIconButton.test.tsx``npm run typecheck``npm run check:encoding``git diff --check` 19.3.44. 暖色生成页顶部返回入口开始沉淀共享壳:`GenerationProgressHero.tsx` 新增 `GenerationHeaderBackButton`,统一承接 `ArrowLeft + 文案 + 透明背景` 的暖色生成页返回按钮骨架,并底层复用 `PlatformIconButton variant="darkMini"``CustomWorldGenerationView.tsx``BarkBattleGeneratingView.tsx` 已接入,继续保留各自 `backLabel`、禁用态和局部暖色文字样式。后续同类生成页、等待页或暖色 hero 顶栏若只是“左箭头 + 返回文案”的轻量返回入口,优先复用这个小组件,不再各自手写 `ArrowLeft`、透明按钮背景和字号间距。验证命令:`npx vitest run src/components/CustomWorldGenerationView.test.tsx src/components/bark-battle-creation/BarkBattleGeneratingView.test.tsx src/components/common/PlatformIconButton.test.tsx``npm run typecheck``npm run check:encoding``git diff --check`
19.3.45. 白底 / 浅色结果页与工作台顶部的轻量返回入口继续收口到 `src/components/common/PlatformBackActionButton.tsx`;该 Module 固定承接 `PlatformActionButton tone="ghost" size="xs"` 上的 `ArrowLeft + 返回文案` 骨架,并只暴露 `compact / regular` 两档尺寸,分别覆盖紧凑结果页 header 与标准白底结果页顶栏。当前 `PuzzleResultView.tsx``SquareHoleResultView.tsx``Match3DResultView.tsx``VisualNovelResultView.tsx` 四个结果页已接入 `variant="compact"``PuzzleClearResultView.tsx``JumpHopResultView.tsx``WoodenFishResultView.tsx` 三个结果页已接入 `variant="regular"``BabyObjectMatchResultView.tsx` 继续使用紧凑款并保留 `className="px-3"` 贴合原横向留白。暖色生成页顶部返回入口继续走 `GenerationHeaderBackButton``BigFishResultView.tsx` 这类 dark hero / 强品牌 special case 继续保留 `PlatformIconButton variant="darkMini"` 路线,不强行并入同一个白底返回按钮基元。后续白底结果页、浅色工作台或普通 platform 顶栏里若只是“左箭头 + 返回”轻量返回入口,优先直接复用 `PlatformBackActionButton`,只在局部补尺寸和少量外边距,不再各页重复手写 ghost button class。 19.3.45. 白底 / 浅色结果页与工作台顶部的轻量返回入口继续收口到 `src/components/common/PlatformBackActionButton.tsx`;该 Module 固定承接 `PlatformActionButton tone="ghost" size="xs"` 上的 `ArrowLeft + 返回文案` 骨架,并只暴露 `compact / regular` 两档尺寸,分别覆盖紧凑结果页 header 与标准白底结果页顶栏。当前 `PuzzleResultView.tsx``SquareHoleResultView.tsx``Match3DResultView.tsx``VisualNovelResultView.tsx` 四个结果页已接入 `variant="compact"``PuzzleClearResultView.tsx``JumpHopResultView.tsx``WoodenFishResultView.tsx` 三个结果页已接入 `variant="regular"``BabyObjectMatchResultView.tsx` 继续使用紧凑款并保留 `className="px-3"` 贴合原横向留白。暖色生成页顶部返回入口继续走 `GenerationHeaderBackButton``BigFishResultView.tsx` 这类 dark hero / 强品牌 special case 继续保留 `PlatformIconButton variant="darkMini"` 路线,不强行并入同一个白底返回按钮基元。后续白底结果页、浅色工作台或普通 platform 顶栏里若只是“左箭头 + 返回”轻量返回入口,优先直接复用 `PlatformBackActionButton`,只在局部补尺寸和少量外边距,不再各页重复手写 ghost button class。
19.3.46. `PlatformNavigableListItem` 继续从桌面 flat row 扩展到首页公开列表里的排行行与分类行;`RpgEntryHomeView.tsx``PlatformRankingItem``PlatformCategoryGameItem` 现在都统一委托共享 `button + leading + body + trailing` 骨架,同时保留原有 `platform-ranking-item__*``platform-category-game-item__*` 局部 class 承接封面尺寸、标题摘要、公开 badge、metric 和右侧 `试玩 / 进入` affordance。后续首页、发现页或 profile 侧同类“封面 + 文本主体 + 右侧动作提示”的浅色导航行,优先先尝试复用 `PlatformNavigableListItem` 并把局部视觉挂在业务 class 上,不要为了这类 row 再回退成原生 `<button>` 手写布局;但教培 promo card、runtime 列表项和带复杂手势的卡片仍保留本地语义,不把共享行骨架扩成万能作品卡。验证命令:`npx vitest run src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx``npm run typecheck``npm run check:encoding``git diff --check`
19.3.47. `PlatformDarkModalFooter` 继续从标准双按钮 footer 扩展到 detail / confirm 收尾:`NpcModals.tsx` 的交易详情单按钮 footer 与 `MapModal.tsx` 的场景切换确认 footer 已接入共享 dark footer frame分别保留“关闭”单 CTA 和“取消 / 确认前往”双 CTA 的业务语义、按钮 tone 与禁用态。后续 dark / pixel modal 里若只是标准底部分隔线 + 常规动作区排布,优先直接复用 `PlatformDarkModalFooter`,即使只有单个按钮也不再手写 `flex justify-end`;但像 `SquareImageCropModal.tsx` 这类白底弹窗 footer、sticky 工作台 footer 和运行态 HUD 工具条继续留在各自语义壳层,不强行混到 dark footer 抽象里。验证命令:`npx vitest run src/components/NpcModals.test.tsx src/components/MapModal.test.tsx``npm run typecheck``npm run check:encoding``git diff --check`
19.3. creative-agent 首页的侧边栏菜单、账号入口、开启新对话、我的创作、首页激励 CTA 和 prompt suggestion 按钮迁移到 `PlatformIconButton` / `PlatformActionButton`;首页继续保留 `creative-agent-home__*` 本地 class 承接透明顶栏、抽屉和品牌化胶囊视觉,不把视觉回收和语义收口绑成一次大改。`Beta` 徽标和历史记录纯文本行暂保留本地实现,等出现更多同构轻量列表行后再评估是否抽新的共享 row primitive。 19.3. creative-agent 首页的侧边栏菜单、账号入口、开启新对话、我的创作、首页激励 CTA 和 prompt suggestion 按钮迁移到 `PlatformIconButton` / `PlatformActionButton`;首页继续保留 `creative-agent-home__*` 本地 class 承接透明顶栏、抽屉和品牌化胶囊视觉,不把视觉回收和语义收口绑成一次大改。`Beta` 徽标和历史记录纯文本行暂保留本地实现,等出现更多同构轻量列表行后再评估是否抽新的共享 row primitive。
19.4. 大鱼吃小鱼结果页 hero 的返回入口迁移到 `PlatformIconButton variant="darkMini"`,测试 / 发布动作迁移到 `PlatformActionButton surface="editorDark"`;结果页只保留测试运行、发布提交和文案状态语义,不再手写 hero 顶栏按钮壳。 19.4. 大鱼吃小鱼结果页 hero 的返回入口迁移到 `PlatformIconButton variant="darkMini"`,测试 / 发布动作迁移到 `PlatformActionButton surface="editorDark"`;结果页只保留测试运行、发布提交和文案状态语义,不再手写 hero 顶栏按钮壳。
19.4.1. 大鱼吃小鱼结果页的发布失败弹层迁移到 `src/components/common/PlatformStatusDialog.tsx``PlatformStatusDialog` 补充自定义图标、可访问标签和动作按钮样式透传后,`BigFishResultView` 不再保留 `BigFishResultErrorModal` 内联的 `UnifiedConfirmDialog + PlatformIconBadge` 组合。结果页只保留失败文案和关闭回调,发布失败的状态图标、遮罩、白底面板和“知道了”主动作统一由共享状态弹层承接。验证命令:`npm run test -- src/components/common/PlatformStatusDialog.test.tsx src/components/big-fish-result/BigFishResultView.test.tsx``npm run typecheck` 19.4.1. 大鱼吃小鱼结果页的发布失败弹层迁移到 `src/components/common/PlatformStatusDialog.tsx``PlatformStatusDialog` 补充自定义图标、可访问标签和动作按钮样式透传后,`BigFishResultView` 不再保留 `BigFishResultErrorModal` 内联的 `UnifiedConfirmDialog + PlatformIconBadge` 组合。结果页只保留失败文案和关闭回调,发布失败的状态图标、遮罩、白底面板和“知道了”主动作统一由共享状态弹层承接。验证命令:`npm run test -- src/components/common/PlatformStatusDialog.test.tsx src/components/big-fish-result/BigFishResultView.test.tsx``npm run typecheck`

View File

@@ -82,6 +82,7 @@ test('目标场景确认面板复用暗色琥珀 PlatformSubpanel 和胶囊标
const panel = screen.getByTestId('map-target-scene-panel'); const panel = screen.getByTestId('map-target-scene-panel');
const currentSummary = screen.getByTestId('map-current-scene-summary'); const currentSummary = screen.getByTestId('map-current-scene-summary');
const nextSummary = screen.getByTestId('map-next-scene-summary'); const nextSummary = screen.getByTestId('map-next-scene-summary');
const footer = screen.getByTestId('map-travel-footer');
const labelBadge = within(panel).getByText(destinationLabel); const labelBadge = within(panel).getByText(destinationLabel);
const cancelButton = screen.getByRole('button', { name: '取消' }); const cancelButton = screen.getByRole('button', { name: '取消' });
const confirmButton = screen.getByRole('button', { name: '确认前往' }); const confirmButton = screen.getByRole('button', { name: '确认前往' });
@@ -96,6 +97,8 @@ test('目标场景确认面板复用暗色琥珀 PlatformSubpanel 和胶囊标
expect(currentSummary.className).toContain('bg-black/25'); expect(currentSummary.className).toContain('bg-black/25');
expect(nextSummary.className).toContain('border-white/10'); expect(nextSummary.className).toContain('border-white/10');
expect(nextSummary.className).toContain('bg-black/25'); expect(nextSummary.className).toContain('bg-black/25');
expect(footer.className).toContain('platform-dark-modal-footer');
expect(footer.className).toContain('border-t');
expect(cancelButton.className).toContain( expect(cancelButton.className).toContain(
'platform-action-button--editor-dark', 'platform-action-button--editor-dark',
); );

View File

@@ -7,6 +7,7 @@ import { useResolvedAssetReadUrl } from '../hooks/useResolvedAssetReadUrl';
import { ScenePresetInfo, WorldType } from '../types'; import { ScenePresetInfo, WorldType } from '../types';
import { CHROME_ICONS, getNineSliceStyle, UI_CHROME } from '../uiAssets'; import { CHROME_ICONS, getNineSliceStyle, UI_CHROME } from '../uiAssets';
import { PlatformActionButton } from './common/PlatformActionButton'; import { PlatformActionButton } from './common/PlatformActionButton';
import { PlatformDarkModalFooter } from './common/PlatformDarkModalFooter';
import { PlatformPillBadge } from './common/PlatformPillBadge'; import { PlatformPillBadge } from './common/PlatformPillBadge';
import { PlatformSubpanel } from './common/PlatformSubpanel'; import { PlatformSubpanel } from './common/PlatformSubpanel';
import { PixelCloseButton } from './PixelCloseButton'; import { PixelCloseButton } from './PixelCloseButton';
@@ -441,26 +442,26 @@ export function MapModal({
</PlatformSubpanel> </PlatformSubpanel>
</div> </div>
<div className="flex justify-end gap-2">
<PlatformActionButton
surface="editorDark"
tone="ghost"
size="xs"
onClick={() => setPendingScene(null)}
>
</PlatformActionButton>
<PlatformActionButton
surface="editorDark"
tone={isTraveling || !canTravel ? 'ghost' : 'warning'}
size="xs"
disabled={isTraveling || !canTravel}
onClick={confirmTravel}
>
{isTraveling ? '切换中...' : canTravel ? '确认前往' : '当前不可切换'}
</PlatformActionButton>
</div>
</div> </div>
<PlatformDarkModalFooter data-testid="map-travel-footer">
<PlatformActionButton
surface="editorDark"
tone="ghost"
size="xs"
onClick={() => setPendingScene(null)}
>
</PlatformActionButton>
<PlatformActionButton
surface="editorDark"
tone={isTraveling || !canTravel ? 'ghost' : 'warning'}
size="xs"
disabled={isTraveling || !canTravel}
onClick={confirmTravel}
>
{isTraveling ? '切换中...' : canTravel ? '确认前往' : '当前不可切换'}
</PlatformActionButton>
</PlatformDarkModalFooter>
</motion.div> </motion.div>
</motion.div> </motion.div>
)} )}

View File

@@ -311,8 +311,12 @@ test('NPC 弹窗标准 dark footer CTA 复用 PlatformActionButton', async () =>
await user.click(screen.getByRole('button', { name: /月壳/ })); await user.click(screen.getByRole('button', { name: /月壳/ }));
const tradeDetailFooter = screen.getByTestId('npc-trade-detail-footer');
const closeButton = screen.getByRole('button', { name: '关闭' }); const closeButton = screen.getByRole('button', { name: '关闭' });
expect(tradeDetailFooter.className).toContain('platform-dark-modal-footer');
expect(tradeDetailFooter.className).toContain('px-5');
expect(tradeDetailFooter.className).toContain('py-4');
expect(closeButton.className).toContain('platform-action-button--editor-dark'); expect(closeButton.className).toContain('platform-action-button--editor-dark');
expect(closeButton.className).toContain('rounded-2xl'); expect(closeButton.className).toContain('rounded-2xl');
}); });

View File

@@ -560,17 +560,21 @@ export function NpcModals({ gameState, npcUi }: NpcModalsProps) {
</PlatformStatusMessage> </PlatformStatusMessage>
)} )}
<div className="flex justify-end">
<PlatformActionButton
surface="editorDark"
tone="secondary"
size="xs"
onClick={() => setTradeDetail(null)}
>
</PlatformActionButton>
</div>
</div> </div>
<PlatformDarkModalFooter
padding="roomy"
data-testid="npc-trade-detail-footer"
>
<PlatformActionButton
surface="editorDark"
tone="secondary"
size="xs"
onClick={() => setTradeDetail(null)}
>
</PlatformActionButton>
</PlatformDarkModalFooter>
</motion.div> </motion.div>
</motion.div> </motion.div>
)} )}

View File

@@ -5416,7 +5416,11 @@ test('mobile home moves category shelf into game category channel', async () =>
expect(screen.getAllByText('分类').length).toBeGreaterThan(0); expect(screen.getAllByText('分类').length).toBeGreaterThan(0);
expect(screen.getByRole('button', { name: //u })).toBeTruthy(); expect(screen.getByRole('button', { name: //u })).toBeTruthy();
expect(screen.getByRole('button', { name: '奇幻' })).toBeTruthy(); expect(screen.getByRole('button', { name: '奇幻' })).toBeTruthy();
expect(screen.getByRole('button', { name: //u })).toBeTruthy(); const categoryEntryButton = screen.getByRole('button', {
name: //u,
});
expect(categoryEntryButton).toBeTruthy();
expect(categoryEntryButton.className).toContain('platform-navigable-list-item');
expect(container.querySelector('.platform-category-game-list')).toBeTruthy(); expect(container.querySelector('.platform-category-game-list')).toBeTruthy();
expect(container.querySelector('.platform-category-game-item')).toBeTruthy(); expect(container.querySelector('.platform-category-game-item')).toBeTruthy();
expect( expect(
@@ -5590,4 +5594,8 @@ test('ranking rows limit displayed work name and show two short tags on the thir
expect(within(rankingPanel!).getByText('古风机关')).toBeTruthy(); expect(within(rankingPanel!).getByText('古风机关')).toBeTruthy();
expect(within(rankingPanel!).queryByText(/2026-04-29/u)).toBeNull(); expect(within(rankingPanel!).queryByText(/2026-04-29/u)).toBeNull();
expect(within(rankingPanel!).queryByText('拼图玩家')).toBeNull(); expect(within(rankingPanel!).queryByText('拼图玩家')).toBeNull();
expect(
within(rankingPanel!).getByRole('button', { name: //u })
.className,
).toContain('platform-navigable-list-item');
}); });

View File

@@ -2003,24 +2003,32 @@ function PlatformRankingItem({
: '进入'; : '进入';
return ( return (
<button <PlatformNavigableListItem
type="button"
onClick={onClick} onClick={onClick}
className="platform-ranking-item w-full text-left" align="center"
className="platform-ranking-item"
leadingClassName="flex items-center gap-3"
leading={
<>
<div className="platform-ranking-item__rank">{rank}</div>
<div className="platform-ranking-item__cover">
{coverImage ? (
<PlatformWorkCoverArtwork
entry={entry}
imageSrc={coverImage}
alt={entry.worldName}
className="h-full w-full object-cover"
/>
) : (
<div className="h-full w-full bg-[radial-gradient(circle_at_24%_18%,rgba(255,255,255,0.24),transparent_30%),linear-gradient(135deg,rgba(255,118,117,0.42),rgba(89,164,255,0.34))]" />
)}
</div>
</>
}
trailing={
<span className="platform-ranking-item__action">{actionLabel}</span>
}
> >
<div className="platform-ranking-item__rank">{rank}</div>
<div className="platform-ranking-item__cover">
{coverImage ? (
<PlatformWorkCoverArtwork
entry={entry}
imageSrc={coverImage}
alt={entry.worldName}
className="h-full w-full object-cover"
/>
) : (
<div className="h-full w-full bg-[radial-gradient(circle_at_24%_18%,rgba(255,255,255,0.24),transparent_30%),linear-gradient(135deg,rgba(255,118,117,0.42),rgba(89,164,255,0.34))]" />
)}
</div>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="line-clamp-1 break-words text-base font-black leading-tight text-[var(--platform-text-strong)]"> <div className="line-clamp-1 break-words text-base font-black leading-tight text-[var(--platform-text-strong)]">
{displayName} {displayName}
@@ -2041,8 +2049,7 @@ function PlatformRankingItem({
))} ))}
</div> </div>
</div> </div>
<span className="platform-ranking-item__action">{actionLabel}</span> </PlatformNavigableListItem>
</button>
); );
} }
@@ -2071,49 +2078,46 @@ function PlatformCategoryGameItem({
entry.summaryText || entry.subtitle || `${displayName} 正在等待摘要。`; entry.summaryText || entry.subtitle || `${displayName} 正在等待摘要。`;
return ( return (
<button <PlatformNavigableListItem
type="button"
onClick={onClick} onClick={onClick}
aria-label={`${entry.worldName}${actionLabel}`} aria-label={`${entry.worldName}${actionLabel}`}
align="center"
className="platform-category-game-item" className="platform-category-game-item"
bodyClassName="platform-category-game-item__body"
leading={
<div className="platform-category-game-item__cover">
{coverImage ? (
<PlatformWorkCoverArtwork
entry={entry}
imageSrc={coverImage}
alt={entry.worldName}
className="h-full w-full object-cover"
/>
) : (
<div className="h-full w-full bg-[radial-gradient(circle_at_24%_18%,rgba(255,255,255,0.26),transparent_32%),linear-gradient(135deg,rgba(255,118,117,0.44),rgba(89,164,255,0.36))]" />
)}
</div>
}
trailing={
<span className="platform-category-game-item__action">{actionLabel}</span>
}
> >
<div className="platform-category-game-item__cover"> <div className="platform-category-game-item__title-row">
{coverImage ? ( <span className="platform-category-game-item__title">{displayName}</span>
<PlatformWorkCoverArtwork <span className="platform-category-game-item__badge"></span>
entry={entry}
imageSrc={coverImage}
alt={entry.worldName}
className="h-full w-full object-cover"
/>
) : (
<div className="h-full w-full bg-[radial-gradient(circle_at_24%_18%,rgba(255,255,255,0.26),transparent_32%),linear-gradient(135deg,rgba(255,118,117,0.44),rgba(89,164,255,0.36))]" />
)}
</div> </div>
<div className="platform-category-game-item__body"> <div className="platform-category-game-item__meta">
<div className="platform-category-game-item__title-row"> <span className="platform-category-game-item__metric">
<span className="platform-category-game-item__title"> <Star className="h-3.5 w-3.5 fill-current" />
{displayName} <span>{formatPlatformCompactCount(metric.value)}</span>
</span> </span>
<span className="platform-category-game-item__badge"></span> <span>{metric.label}</span>
</div> {metaParts.length > 0 ? <span>{metaParts.join(' · ')}</span> : null}
<div className="platform-category-game-item__meta">
<span className="platform-category-game-item__metric">
<Star className="h-3.5 w-3.5 fill-current" />
<span>{formatPlatformCompactCount(metric.value)}</span>
</span>
<span>{metric.label}</span>
{metaParts.length > 0 ? <span>{metaParts.join(' · ')}</span> : null}
</div>
<div className="platform-category-game-item__summary">
{summaryText}
</div>
</div> </div>
<span className="platform-category-game-item__action">{actionLabel}</span> <div className="platform-category-game-item__summary">{summaryText}</div>
</button> </PlatformNavigableListItem>
); );
} }