继续收口暗色弹窗底部动作区
新增 PlatformDarkModalFooter 统一 dark modal footer 的布局壳 接入 NPC 弹窗、选择定制弹窗、任务更新弹层与物品详情 footer 补充组件级与弹窗集成测试并更新收口计划和共享决策记录
This commit is contained in:
@@ -54,6 +54,7 @@
|
|||||||
- 2026-06-10 追加:`PlatformStatusDialog` 继续支持 header notice 布局、body content、close button、backdrop / Escape 关闭路径,用来承接“提示 / 规则阻断 / 作品不可用 / 泥点不足”这类带标题栏的状态 notice;平台入口的 `draftGenerationPointNotice`、`workNotFoundRecoveryDialog` 和 RPG 大编辑器里的 `EditorNoticeDialog` 已迁移到这套共享组件,不再各自维护 `UnifiedConfirmDialog` 壳层和关闭策略。
|
- 2026-06-10 追加:`PlatformStatusDialog` 继续支持 header notice 布局、body content、close button、backdrop / Escape 关闭路径,用来承接“提示 / 规则阻断 / 作品不可用 / 泥点不足”这类带标题栏的状态 notice;平台入口的 `draftGenerationPointNotice`、`workNotFoundRecoveryDialog` 和 RPG 大编辑器里的 `EditorNoticeDialog` 已迁移到这套共享组件,不再各自维护 `UnifiedConfirmDialog` 壳层和关闭策略。
|
||||||
- 2026-06-10 追加:`CustomWorldEntityCatalog` 的 `minimum-playable` 规则阻断提示也统一迁到 `PlatformStatusDialog`,不再和删除角色 / 批量删除共用 `UnifiedConfirmDialog` 配置;同日平台入口公开编号搜索把 error 分支从用户摘要 modal 中拆出,未命中结果单独走 `PlatformStatusDialog`,命中用户继续保留 `UnifiedModal + PlatformSubpanel` 信息布局。
|
- 2026-06-10 追加:`CustomWorldEntityCatalog` 的 `minimum-playable` 规则阻断提示也统一迁到 `PlatformStatusDialog`,不再和删除角色 / 批量删除共用 `UnifiedConfirmDialog` 配置;同日平台入口公开编号搜索把 error 分支从用户摘要 modal 中拆出,未命中结果单独走 `PlatformStatusDialog`,命中用户继续保留 `UnifiedModal + PlatformSubpanel` 信息布局。
|
||||||
- 2026-06-11 追加:`PlatformAsyncStatePanel` 继续从 profile modal 与作品架扩展到 RPG 首页公开分区;`RpgEntryHomeView.tsx` 的移动端排行、发现页寓教于乐 / 默认公开 feed、桌面首页“今日游戏 / 推荐”、桌面发现页寓教于乐 / 默认公开 feed,以及“我的创作”分区已统一改成 `loadingState / emptyState / children` 三态 slot。页面级 `platformError` 继续留在状态壳外层,保证错误提示可以和内容并存;`recommend runtime`、分类筛选等含运行态或二级筛选语义的分支暂不硬并入这一轮。
|
- 2026-06-11 追加:`PlatformAsyncStatePanel` 继续从 profile modal 与作品架扩展到 RPG 首页公开分区;`RpgEntryHomeView.tsx` 的移动端排行、发现页寓教于乐 / 默认公开 feed、桌面首页“今日游戏 / 推荐”、桌面发现页寓教于乐 / 默认公开 feed,以及“我的创作”分区已统一改成 `loadingState / emptyState / children` 三态 slot。页面级 `platformError` 继续留在状态壳外层,保证错误提示可以和内容并存;`recommend runtime`、分类筛选等含运行态或二级筛选语义的分支暂不硬并入这一轮。
|
||||||
|
- 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-09 追加:通用输入 Composer 的上传参考图、发送和移除参考图已迁移到 `PlatformIconButton`;图标上传仍使用 `asChild="label"` 保留 label + file input 语义,公共组件会自动写入隐藏文本,确保内嵌 file input 继承可访问名称。
|
- 2026-06-09 追加:通用输入 Composer 的上传参考图、发送和移除参考图已迁移到 `PlatformIconButton`;图标上传仍使用 `asChild="label"` 保留 label + file input 语义,公共组件会自动写入隐藏文本,确保内嵌 file input 继承可访问名称。
|
||||||
- 2026-06-10 追加:creation-agent composer 的上传文档 / 上传参考图入口使用 `PlatformIconButton` 默认 `platformIcon`;工作台只保留动态 label、title、busy 状态和 picker 回调,发送按钮继续保留主题色动作布局。验证命令:`npm run test -- src/components/creation-agent/CreationAgentWorkspace.test.tsx src/components/common/PlatformIconButton.test.tsx`。
|
- 2026-06-10 追加:creation-agent composer 的上传文档 / 上传参考图入口使用 `PlatformIconButton` 默认 `platformIcon`;工作台只保留动态 label、title、busy 状态和 picker 回调,发送按钮继续保留主题色动作布局。验证命令:`npm run test -- src/components/creation-agent/CreationAgentWorkspace.test.tsx src/components/common/PlatformIconButton.test.tsx`。
|
||||||
- 2026-06-10 追加:作品详情顶部返回 / 分享和封面轮播上一张 / 下一张入口使用 `PlatformIconButton variant="platformIcon"`;详情页保留原 `platform-work-detail__*` 局部 class 控制位置和尺寸,点赞、复制三态等专用动作暂不迁移。验证命令:`npm run test -- src/components/platform-entry/PlatformWorkDetailView.test.tsx src/components/common/PlatformIconButton.test.tsx`。
|
- 2026-06-10 追加:作品详情顶部返回 / 分享和封面轮播上一张 / 下一张入口使用 `PlatformIconButton variant="platformIcon"`;详情页保留原 `platform-work-detail__*` 局部 class 控制位置和尺寸,点赞、复制三态等专用动作暂不迁移。验证命令:`npm run test -- src/components/platform-entry/PlatformWorkDetailView.test.tsx src/components/common/PlatformIconButton.test.tsx`。
|
||||||
|
|||||||
@@ -269,6 +269,7 @@
|
|||||||
22.1. RPG 创作侧标准 dark header / footer 动作继续向共享按钮收口:`RpgCreationRoleAssetStudioModalImpl.tsx` 的 header“关闭”、`RpgCreationEntityEditorShared.tsx` 的 footer“取消”、`RpgCreationRoleAssetStudioFooter.tsx` 的“保存到当前角色”都改为委托 `PlatformActionButton surface="editorDark"`。局部壳层只继续保留 `stopPropagation`、tone 映射、布局和极少量字号/宽度贴合;标准暗色编辑器里的 close / cancel / save CTA 不再各自手写原生 `<button>` 基础 chrome。
|
22.1. RPG 创作侧标准 dark header / footer 动作继续向共享按钮收口:`RpgCreationRoleAssetStudioModalImpl.tsx` 的 header“关闭”、`RpgCreationEntityEditorShared.tsx` 的 footer“取消”、`RpgCreationRoleAssetStudioFooter.tsx` 的“保存到当前角色”都改为委托 `PlatformActionButton surface="editorDark"`。局部壳层只继续保留 `stopPropagation`、tone 映射、布局和极少量字号/宽度贴合;标准暗色编辑器里的 close / cancel / save CTA 不再各自手写原生 `<button>` 基础 chrome。
|
||||||
22.2. RPG runtime overlay 里的标准 dark CTA 和可点击 dark row 继续向共享原子收口:`RpgAdventurePanelOverlays.tsx` 的 goal panel“知道了”、任务详情里的“领取任务 / 返回交付”、任务完成提示里的“打开任务日志”都改为委托 `PlatformActionButton surface="editorDark"`;设置面板里的“运行统计”入口改为 `PlatformSubpanel as="button" surface="dark"`。像素风 choice button、HUD launcher、奖励物品格和输入 composer 这类 runtime 专属控件继续保留独立语义,不并回普通平台按钮。
|
22.2. RPG runtime overlay 里的标准 dark CTA 和可点击 dark row 继续向共享原子收口:`RpgAdventurePanelOverlays.tsx` 的 goal panel“知道了”、任务详情里的“领取任务 / 返回交付”、任务完成提示里的“打开任务日志”都改为委托 `PlatformActionButton surface="editorDark"`;设置面板里的“运行统计”入口改为 `PlatformSubpanel as="button" surface="dark"`。像素风 choice button、HUD launcher、奖励物品格和输入 composer 这类 runtime 专属控件继续保留独立语义,不并回普通平台按钮。
|
||||||
22.3. NPC dark modal footer 与暗色明细空态继续向共享原子收口:`NpcModals.tsx` 里的交易 / 赠礼 / 招募弹窗 footer 按钮和物品详情“关闭”按钮都改为委托 `PlatformActionButton surface="editorDark"`,交易右侧“请选择一件物品”提示改为 `PlatformEmptyState surface="editorDark"`;`CharacterInfoShared.tsx` 里的 `BuildContributionDetailPanel` 空明细也改为 `PlatformEmptyState surface="editorDark"`。数量 stepper、赠礼 / 招募 option card、标签强度按钮这类带更强业务语义的控件继续保留局部实现。
|
22.3. NPC dark modal footer 与暗色明细空态继续向共享原子收口:`NpcModals.tsx` 里的交易 / 赠礼 / 招募弹窗 footer 按钮和物品详情“关闭”按钮都改为委托 `PlatformActionButton surface="editorDark"`,交易右侧“请选择一件物品”提示改为 `PlatformEmptyState surface="editorDark"`;`CharacterInfoShared.tsx` 里的 `BuildContributionDetailPanel` 空明细也改为 `PlatformEmptyState surface="editorDark"`。数量 stepper、赠礼 / 招募 option card、标签强度按钮这类带更强业务语义的控件继续保留局部实现。
|
||||||
|
22.4. 暗色 / 像素 modal 的标准 footer 布局统一收口到 `src/components/common/PlatformDarkModalFooter.tsx`;该 Module 只承接 dark footer 的顶部分隔线、padding 和常见动作区排布,不承接“取消 / 确认”业务语义。`NpcModals.tsx` 的交易 / 赠礼 / 招募 footer、`SelectionCustomizationModals.tsx` 的 `SelectionModal` footer、`RpgAdventurePanelOverlays.tsx` 的 goal panel footer,以及 `InventoryItemViews.tsx` 的详情 footer wrapper 已接入。sticky 工作台 footer、正文里的单独 CTA 收尾和 runtime HUD 工具条继续保留局部布局;后续 dark / pixel modal 若只是同构 footer chrome,优先直接复用这个 Module,不再重复手写 `border-t border-white/10 + px/py + justify-end gap-*` 组合。验证命令:`npx vitest run src/components/common/PlatformDarkModalFooter.test.tsx src/components/NpcModals.test.tsx src/components/SelectionCustomizationModals.test.tsx src/components/rpg-runtime-panels/RpgAdventurePanel.questOffer.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`。
|
||||||
|
|
||||||
## 验证
|
## 验证
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
getNineSliceStyle,
|
getNineSliceStyle,
|
||||||
UI_CHROME,
|
UI_CHROME,
|
||||||
} from '../uiAssets';
|
} from '../uiAssets';
|
||||||
|
import { PlatformDarkModalFooter } from './common/PlatformDarkModalFooter';
|
||||||
import { PlatformQuantityBadge } from './common/PlatformQuantityBadge';
|
import { PlatformQuantityBadge } from './common/PlatformQuantityBadge';
|
||||||
import { PixelCloseButton } from './PixelCloseButton';
|
import { PixelCloseButton } from './PixelCloseButton';
|
||||||
import { PixelIcon } from './PixelIcon';
|
import { PixelIcon } from './PixelIcon';
|
||||||
@@ -237,9 +238,9 @@ export function InventoryItemDetailModal({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{footer != null ? (
|
{footer != null ? (
|
||||||
<div className="border-t border-white/10 px-4 py-3 sm:px-5">
|
<PlatformDarkModalFooter layout="content">
|
||||||
{footer}
|
{footer}
|
||||||
</div>
|
</PlatformDarkModalFooter>
|
||||||
) : null}
|
) : null}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|||||||
@@ -276,6 +276,9 @@ test('NPC 弹窗标准 dark footer CTA 复用 PlatformActionButton', async () =>
|
|||||||
const { unmount } = render(
|
const { unmount } = render(
|
||||||
<NpcModals gameState={createEmptyGameState()} npcUi={createEmptyNpcUi()} />,
|
<NpcModals gameState={createEmptyGameState()} npcUi={createEmptyNpcUi()} />,
|
||||||
);
|
);
|
||||||
|
const tradeFooter = screen.getByTestId('npc-trade-footer');
|
||||||
|
const giftFooter = screen.getByTestId('npc-gift-footer');
|
||||||
|
const recruitFooter = screen.getByTestId('npc-recruit-footer');
|
||||||
|
|
||||||
const cancelButtons = screen.getAllByRole('button', { name: '取消' });
|
const cancelButtons = screen.getAllByRole('button', { name: '取消' });
|
||||||
const tradeConfirmButton = screen.getByRole('button', { name: '确认购买' });
|
const tradeConfirmButton = screen.getByRole('button', { name: '确认购买' });
|
||||||
@@ -291,6 +294,12 @@ test('NPC 弹窗标准 dark footer CTA 复用 PlatformActionButton', async () =>
|
|||||||
].filter((button): button is HTMLElement => Boolean(button));
|
].filter((button): button is HTMLElement => Boolean(button));
|
||||||
|
|
||||||
expect(footerButtons).toHaveLength(6);
|
expect(footerButtons).toHaveLength(6);
|
||||||
|
expect(tradeFooter.className).toContain('platform-dark-modal-footer');
|
||||||
|
expect(tradeFooter.className).toContain('border-t');
|
||||||
|
expect(giftFooter.className).toContain('platform-dark-modal-footer');
|
||||||
|
expect(giftFooter.className).toContain('pb-5');
|
||||||
|
expect(recruitFooter.className).toContain('platform-dark-modal-footer');
|
||||||
|
expect(recruitFooter.className).toContain('pb-5');
|
||||||
|
|
||||||
footerButtons.forEach((button) => {
|
footerButtons.forEach((button) => {
|
||||||
expect(button.className).toContain('platform-action-button--editor-dark');
|
expect(button.className).toContain('platform-action-button--editor-dark');
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import {
|
|||||||
UI_CHROME,
|
UI_CHROME,
|
||||||
} from '../uiAssets';
|
} from '../uiAssets';
|
||||||
import { PlatformActionButton } from './common/PlatformActionButton';
|
import { PlatformActionButton } from './common/PlatformActionButton';
|
||||||
|
import { PlatformDarkModalFooter } from './common/PlatformDarkModalFooter';
|
||||||
import { PlatformDarkOptionCard } from './common/PlatformDarkOptionCard';
|
import { PlatformDarkOptionCard } from './common/PlatformDarkOptionCard';
|
||||||
import { PlatformEmptyState } from './common/PlatformEmptyState';
|
import { PlatformEmptyState } from './common/PlatformEmptyState';
|
||||||
import { PlatformPillBadge } from './common/PlatformPillBadge';
|
import { PlatformPillBadge } from './common/PlatformPillBadge';
|
||||||
@@ -427,7 +428,7 @@ export function NpcModals({ gameState, npcUi }: NpcModalsProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-end gap-3 border-t border-white/10 px-4 py-3 sm:px-5 sm:py-4">
|
<PlatformDarkModalFooter data-testid="npc-trade-footer">
|
||||||
<PlatformActionButton
|
<PlatformActionButton
|
||||||
surface="editorDark"
|
surface="editorDark"
|
||||||
tone="secondary"
|
tone="secondary"
|
||||||
@@ -445,7 +446,7 @@ export function NpcModals({ gameState, npcUi }: NpcModalsProps) {
|
|||||||
>
|
>
|
||||||
{tradeMode === 'buy' ? '确认购买' : '确认出售'}
|
{tradeMode === 'buy' ? '确认购买' : '确认出售'}
|
||||||
</PlatformActionButton>
|
</PlatformActionButton>
|
||||||
</div>
|
</PlatformDarkModalFooter>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
@@ -665,7 +666,11 @@ export function NpcModals({ gameState, npcUi }: NpcModalsProps) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end gap-3 px-5 pb-5">
|
<PlatformDarkModalFooter
|
||||||
|
bordered={false}
|
||||||
|
padding="bottom"
|
||||||
|
data-testid="npc-gift-footer"
|
||||||
|
>
|
||||||
<PlatformActionButton
|
<PlatformActionButton
|
||||||
surface="editorDark"
|
surface="editorDark"
|
||||||
tone="secondary"
|
tone="secondary"
|
||||||
@@ -683,7 +688,7 @@ export function NpcModals({ gameState, npcUi }: NpcModalsProps) {
|
|||||||
>
|
>
|
||||||
确认赠礼
|
确认赠礼
|
||||||
</PlatformActionButton>
|
</PlatformActionButton>
|
||||||
</div>
|
</PlatformDarkModalFooter>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
@@ -763,7 +768,11 @@ export function NpcModals({ gameState, npcUi }: NpcModalsProps) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end gap-3 px-5 pb-5">
|
<PlatformDarkModalFooter
|
||||||
|
bordered={false}
|
||||||
|
padding="bottom"
|
||||||
|
data-testid="npc-recruit-footer"
|
||||||
|
>
|
||||||
<PlatformActionButton
|
<PlatformActionButton
|
||||||
surface="editorDark"
|
surface="editorDark"
|
||||||
tone="secondary"
|
tone="secondary"
|
||||||
@@ -781,7 +790,7 @@ export function NpcModals({ gameState, npcUi }: NpcModalsProps) {
|
|||||||
>
|
>
|
||||||
确认招募
|
确认招募
|
||||||
</PlatformActionButton>
|
</PlatformActionButton>
|
||||||
</div>
|
</PlatformDarkModalFooter>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ test('角色自定义错误提示复用暗色 PlatformStatusMessage chrome', ()
|
|||||||
const currentCharacterPanel = screen.getByText('当前角色:试剑客');
|
const currentCharacterPanel = screen.getByText('当前角色:试剑客');
|
||||||
const nameInput = screen.getByLabelText('角色名字');
|
const nameInput = screen.getByLabelText('角色名字');
|
||||||
const backstoryTextarea = screen.getByLabelText('背景补充');
|
const backstoryTextarea = screen.getByLabelText('背景补充');
|
||||||
|
const footer = screen.getByTestId('selection-modal-footer');
|
||||||
|
|
||||||
expect(errorMessage.className).toContain('platform-status-message');
|
expect(errorMessage.className).toContain('platform-status-message');
|
||||||
expect(errorMessage.className).toContain('border-rose-300/15');
|
expect(errorMessage.className).toContain('border-rose-300/15');
|
||||||
@@ -53,6 +54,8 @@ test('角色自定义错误提示复用暗色 PlatformStatusMessage chrome', ()
|
|||||||
expect(closeButton.className).toContain(
|
expect(closeButton.className).toContain(
|
||||||
'platform-modal-close-button--editor-dark',
|
'platform-modal-close-button--editor-dark',
|
||||||
);
|
);
|
||||||
|
expect(footer.className).toContain('platform-dark-modal-footer');
|
||||||
|
expect(footer.className).toContain('border-t');
|
||||||
expect(currentCharacterPanel.className).toContain('border-white/10');
|
expect(currentCharacterPanel.className).toContain('border-white/10');
|
||||||
expect(currentCharacterPanel.className).toContain('bg-black/25');
|
expect(currentCharacterPanel.className).toContain('bg-black/25');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type {
|
|||||||
CustomWorldGenerationMode,
|
CustomWorldGenerationMode,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { PlatformActionButton } from './common/PlatformActionButton';
|
import { PlatformActionButton } from './common/PlatformActionButton';
|
||||||
|
import { PlatformDarkModalFooter } from './common/PlatformDarkModalFooter';
|
||||||
import { PlatformModalCloseButton } from './common/PlatformModalCloseButton';
|
import { PlatformModalCloseButton } from './common/PlatformModalCloseButton';
|
||||||
import { PlatformProgressBar } from './common/PlatformProgressBar';
|
import { PlatformProgressBar } from './common/PlatformProgressBar';
|
||||||
import { PlatformStatusMessage } from './common/PlatformStatusMessage';
|
import { PlatformStatusMessage } from './common/PlatformStatusMessage';
|
||||||
@@ -46,9 +47,13 @@ function SelectionModal({
|
|||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
{footer ? (
|
{footer ? (
|
||||||
<div className="flex flex-wrap items-center justify-end gap-3 border-t border-white/10 px-5 py-4">
|
<PlatformDarkModalFooter
|
||||||
|
wrap
|
||||||
|
padding="roomy"
|
||||||
|
data-testid="selection-modal-footer"
|
||||||
|
>
|
||||||
{footer}
|
{footer}
|
||||||
</div>
|
</PlatformDarkModalFooter>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
65
src/components/common/PlatformDarkModalFooter.test.tsx
Normal file
65
src/components/common/PlatformDarkModalFooter.test.tsx
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
/* @vitest-environment jsdom */
|
||||||
|
|
||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import { describe, expect, test } from 'vitest';
|
||||||
|
|
||||||
|
import { PlatformDarkModalFooter } from './PlatformDarkModalFooter';
|
||||||
|
|
||||||
|
describe('PlatformDarkModalFooter', () => {
|
||||||
|
test('renders bordered action footer with shared action row chrome', () => {
|
||||||
|
render(
|
||||||
|
<PlatformDarkModalFooter data-testid="footer">
|
||||||
|
<button type="button">取消</button>
|
||||||
|
<button type="button">确认</button>
|
||||||
|
</PlatformDarkModalFooter>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const footer = screen.getByTestId('footer');
|
||||||
|
const actions = footer.querySelector('.platform-dark-modal-footer__actions');
|
||||||
|
|
||||||
|
expect(footer.className).toContain('platform-dark-modal-footer');
|
||||||
|
expect(footer.className).toContain('border-t');
|
||||||
|
expect(footer.className).toContain('border-white/10');
|
||||||
|
expect(footer.className).toContain('px-4');
|
||||||
|
expect(footer.className).toContain('py-3');
|
||||||
|
expect(actions?.className).toContain('justify-end');
|
||||||
|
expect(actions?.className).toContain('gap-3');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('supports unbordered bottom padding actions for modal footer tails', () => {
|
||||||
|
render(
|
||||||
|
<PlatformDarkModalFooter
|
||||||
|
bordered={false}
|
||||||
|
padding="bottom"
|
||||||
|
gap="sm"
|
||||||
|
data-testid="footer"
|
||||||
|
>
|
||||||
|
<button type="button">关闭</button>
|
||||||
|
</PlatformDarkModalFooter>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const footer = screen.getByTestId('footer');
|
||||||
|
const actions = footer.querySelector('.platform-dark-modal-footer__actions');
|
||||||
|
|
||||||
|
expect(footer.className).not.toContain('border-t');
|
||||||
|
expect(footer.className).toContain('px-5');
|
||||||
|
expect(footer.className).toContain('pb-5');
|
||||||
|
expect(actions?.className).toContain('gap-2');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('supports content layout without wrapping children in an actions row', () => {
|
||||||
|
render(
|
||||||
|
<PlatformDarkModalFooter layout="content" data-testid="footer">
|
||||||
|
<div data-testid="content">自定义内容</div>
|
||||||
|
</PlatformDarkModalFooter>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const footer = screen.getByTestId('footer');
|
||||||
|
|
||||||
|
expect(screen.getByTestId('content')).toBeTruthy();
|
||||||
|
expect(
|
||||||
|
footer.querySelector('.platform-dark-modal-footer__actions'),
|
||||||
|
).toBeNull();
|
||||||
|
expect(footer.className).toContain('platform-dark-modal-footer');
|
||||||
|
});
|
||||||
|
});
|
||||||
86
src/components/common/PlatformDarkModalFooter.tsx
Normal file
86
src/components/common/PlatformDarkModalFooter.tsx
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import type { ComponentPropsWithoutRef, ReactNode } from 'react';
|
||||||
|
|
||||||
|
type PlatformDarkModalFooterLayout = 'actions' | 'content';
|
||||||
|
type PlatformDarkModalFooterPadding = 'compact' | 'roomy' | 'bottom';
|
||||||
|
type PlatformDarkModalFooterGap = 'sm' | 'md';
|
||||||
|
type PlatformDarkModalFooterAlign = 'start' | 'center' | 'end' | 'between';
|
||||||
|
|
||||||
|
type PlatformDarkModalFooterProps = ComponentPropsWithoutRef<'div'> & {
|
||||||
|
children: ReactNode;
|
||||||
|
bordered?: boolean;
|
||||||
|
layout?: PlatformDarkModalFooterLayout;
|
||||||
|
padding?: PlatformDarkModalFooterPadding;
|
||||||
|
gap?: PlatformDarkModalFooterGap;
|
||||||
|
wrap?: boolean;
|
||||||
|
align?: PlatformDarkModalFooterAlign;
|
||||||
|
contentClassName?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PADDING_CLASS: Record<PlatformDarkModalFooterPadding, string> = {
|
||||||
|
compact: 'px-4 py-3 sm:px-5 sm:py-4',
|
||||||
|
roomy: 'px-5 py-4',
|
||||||
|
bottom: 'px-5 pb-5',
|
||||||
|
};
|
||||||
|
|
||||||
|
const GAP_CLASS: Record<PlatformDarkModalFooterGap, string> = {
|
||||||
|
sm: 'gap-2',
|
||||||
|
md: 'gap-3',
|
||||||
|
};
|
||||||
|
|
||||||
|
const ALIGN_CLASS: Record<PlatformDarkModalFooterAlign, string> = {
|
||||||
|
start: 'justify-start',
|
||||||
|
center: 'justify-center',
|
||||||
|
end: 'justify-end',
|
||||||
|
between: 'justify-between',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 暗色 / 像素弹层底部 footer 骨架。
|
||||||
|
* 统一承接 border、padding 和常见的动作按钮排布,避免业务页重复手写同一套 chrome。
|
||||||
|
*/
|
||||||
|
export function PlatformDarkModalFooter({
|
||||||
|
children,
|
||||||
|
bordered = true,
|
||||||
|
layout = 'actions',
|
||||||
|
padding = 'compact',
|
||||||
|
gap = 'md',
|
||||||
|
wrap = false,
|
||||||
|
align = 'end',
|
||||||
|
className,
|
||||||
|
contentClassName,
|
||||||
|
...props
|
||||||
|
}: PlatformDarkModalFooterProps) {
|
||||||
|
const frameClassName = [
|
||||||
|
'platform-dark-modal-footer',
|
||||||
|
bordered ? 'border-t border-white/10' : null,
|
||||||
|
PADDING_CLASS[padding],
|
||||||
|
className ?? null,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ');
|
||||||
|
|
||||||
|
if (layout === 'content') {
|
||||||
|
return (
|
||||||
|
<div className={frameClassName} {...props}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const actionsClassName = [
|
||||||
|
'platform-dark-modal-footer__actions',
|
||||||
|
'flex items-center',
|
||||||
|
ALIGN_CLASS[align],
|
||||||
|
GAP_CLASS[gap],
|
||||||
|
wrap ? 'flex-wrap' : null,
|
||||||
|
contentClassName ?? null,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={frameClassName} {...props}>
|
||||||
|
<div className={actionsClassName}>{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -46,6 +46,7 @@ import {
|
|||||||
UI_CHROME,
|
UI_CHROME,
|
||||||
} from '../../uiAssets';
|
} from '../../uiAssets';
|
||||||
import { PlatformActionButton } from '../common/PlatformActionButton';
|
import { PlatformActionButton } from '../common/PlatformActionButton';
|
||||||
|
import { PlatformDarkModalFooter } from '../common/PlatformDarkModalFooter';
|
||||||
import { PlatformEmptyState } from '../common/PlatformEmptyState';
|
import { PlatformEmptyState } from '../common/PlatformEmptyState';
|
||||||
import { PlatformPillBadge } from '../common/PlatformPillBadge';
|
import { PlatformPillBadge } from '../common/PlatformPillBadge';
|
||||||
import type { PlatformPillBadgeTone } from '../common/platformPillBadgeModel';
|
import type { PlatformPillBadgeTone } from '../common/platformPillBadgeModel';
|
||||||
@@ -1025,7 +1026,10 @@ export function RpgAdventurePanelOverlays({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-end gap-2 border-t border-white/10 px-4 py-3 sm:px-5">
|
<PlatformDarkModalFooter
|
||||||
|
gap="sm"
|
||||||
|
data-testid="adventure-goal-panel-footer"
|
||||||
|
>
|
||||||
{goalStack.activeGoal?.sourceKind === 'quest' ||
|
{goalStack.activeGoal?.sourceKind === 'quest' ||
|
||||||
goalStack.immediateStepGoal?.sourceKind === 'quest' ? (
|
goalStack.immediateStepGoal?.sourceKind === 'quest' ? (
|
||||||
<PlatformActionButton
|
<PlatformActionButton
|
||||||
@@ -1052,7 +1056,7 @@ export function RpgAdventurePanelOverlays({
|
|||||||
>
|
>
|
||||||
知道了
|
知道了
|
||||||
</PlatformActionButton>
|
</PlatformActionButton>
|
||||||
</div>
|
</PlatformDarkModalFooter>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user