diff --git a/.hermes/shared-memory/decision-log.md b/.hermes/shared-memory/decision-log.md index eefd804d..af8001fd 100644 --- a/.hermes/shared-memory/decision-log.md +++ b/.hermes/shared-memory/decision-log.md @@ -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 追加:`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 追加:暗色 / 像素 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-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`。 diff --git a/docs/technical/【前端架构】PlatformUiKit弹窗组件收口计划-2026-06-08.md b/docs/technical/【前端架构】PlatformUiKit弹窗组件收口计划-2026-06-08.md index 3a332713..4a47ed40 100644 --- a/docs/technical/【前端架构】PlatformUiKit弹窗组件收口计划-2026-06-08.md +++ b/docs/technical/【前端架构】PlatformUiKit弹窗组件收口计划-2026-06-08.md @@ -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 不再各自手写原生 ` + + , + ); + + 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( + + + , + ); + + 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( + +
自定义内容
+
, + ); + + 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'); + }); +}); diff --git a/src/components/common/PlatformDarkModalFooter.tsx b/src/components/common/PlatformDarkModalFooter.tsx new file mode 100644 index 00000000..128e7dfc --- /dev/null +++ b/src/components/common/PlatformDarkModalFooter.tsx @@ -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 = { + 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 = { + sm: 'gap-2', + md: 'gap-3', +}; + +const ALIGN_CLASS: Record = { + 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 ( +
+ {children} +
+ ); + } + + 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 ( +
+
{children}
+
+ ); +} diff --git a/src/components/rpg-runtime-panels/RpgAdventurePanelOverlays.tsx b/src/components/rpg-runtime-panels/RpgAdventurePanelOverlays.tsx index 445ac19f..e503494a 100644 --- a/src/components/rpg-runtime-panels/RpgAdventurePanelOverlays.tsx +++ b/src/components/rpg-runtime-panels/RpgAdventurePanelOverlays.tsx @@ -46,6 +46,7 @@ import { UI_CHROME, } from '../../uiAssets'; import { PlatformActionButton } from '../common/PlatformActionButton'; +import { PlatformDarkModalFooter } from '../common/PlatformDarkModalFooter'; import { PlatformEmptyState } from '../common/PlatformEmptyState'; import { PlatformPillBadge } from '../common/PlatformPillBadge'; import type { PlatformPillBadgeTone } from '../common/platformPillBadgeModel'; @@ -1025,7 +1026,10 @@ export function RpgAdventurePanelOverlays({ /> -
+ {goalStack.activeGoal?.sourceKind === 'quest' || goalStack.immediateStepGoal?.sourceKind === 'quest' ? ( 知道了 -
+ )}