Fix/统一修复modal样式失效 (#122)
之前遇到几次弹窗的样式不生效,bug 现在在画布agent的删除对话弹窗又出现了, 这个pr把类似的修改集中到最下层的UnifiedModal,适用于所有的modal, 一些特殊的modal有特殊处理. 画布agent的删除对话弹窗  --------- Co-authored-by: 段舒康 <kdletters@qq.com> Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/122 Reviewed-by: 段舒康 <kdletters@qq.com> Co-authored-by: 王德宇 <kvtodev@outlook.com> Co-committed-by: 王德宇 <kvtodev@outlook.com>
This commit was merged in pull request #122.
This commit is contained in:
@@ -3638,6 +3638,8 @@
|
||||
- 决策:`PlatformSegmentedTabs` 继续承接首页 / 结果页剩余的横向 rail 与二选一切换;`RpgEntryHomeView.tsx` 的 discover channel bar、移动端 / 桌面端分类 chip rail,`CustomWorldEntityCatalog.tsx` 的 `RESULT_TABS` sticky rail,以及 `PlatformProfileRechargeModal.tsx` 的“泥点充值 / 会员卡”切换条已迁移。像 `CustomWorldEntityCatalog` 这种“标题 + count”内容直接走 `ReactNode label`;首页 / 创作入口 / 作品架 / 个人中心里稳定复用的频道下划线、创作 pill rail、二列 option segment 皮肤走 `PlatformSegmentedTabPresets`。同类切换在测试里应优先按 `role="tablist" / "tab"` 查询,而不是把它们继续当普通 button。
|
||||
- 决策:简单泥点确认流的开关状态机统一收口到 `src/components/common/useMudPointConfirmController.ts`,只暴露 `open / requestOpen / close / confirm`,不持有点数、标题、描述或禁用态等业务字段;`PuzzleCreationWorkspace.tsx`、`Match3DCreationWorkspace.tsx` 与 `Match3DResultView.tsx` 的两个批量素材面板已接入。`PuzzleResultView.tsx` 和 `RpgCreationRoleAssetStudioModalImpl.tsx` 这类节奏不同或携带 pending payload 的场景继续保留本地状态机,避免把简单 hook 扩成泛型动作路由器。
|
||||
- 决策:标准平台 modal header 的关闭入口继续统一到 `PlatformModalCloseButton variant="platformIcon"`;结果页 / 工具页重复的白底 portal 弹窗壳层收口到 `src/components/common/PlatformToolModalShell.tsx`,由它统一承接平台主题 overlay、白底 remap panel、标准 header/body/footer spacing、关闭按钮和遮罩 / Escape 关闭策略。`PuzzleResultView.tsx` 的关卡详情 / 发布弹窗、`Match3DResultView.tsx` 的封面 / 发布工具弹窗,以及 `PuzzleHistoryAssetPickerDialog.tsx` 的历史素材弹窗已迁移;`UnifiedModal` 新增 `ariaLabel` 支持可见标题动态、可访问名称固定的场景。像素风 runtime、drawer collapse、玩法规则面板和运行态 overlay 不跟这条线混收,继续保留局部 close 语义。
|
||||
- 决策:平台 portal 主题恢复下沉到 `UnifiedModal`;`portal=true` 默认从 `AuthUiContext` 注入当前 light / dark 主题,已显式给出主题的调用保留原选择,无 Provider 回退 light。`portalTheme="none"` 只用于全黑图片预览等完全自绘弹层,`portal=false` 仍使用原 DOM 主题作用域。图片信息、修改图片与画布快捷键弹窗在完整支持暗色样式前显式使用 `portalTheme="light"`,不将固定白底面板与暗色文本变量混用。共享业务壳不再重复读取 AuthUi 只为 portal 补 class,画布私有变量则继续通过 `ImageCanvasEditorPortal` 桥接。已退役玩法不因该底层修复恢复入口或维护范围。
|
||||
- 决策:`PlatformUtilityInfoModal` 未显式传主题时必须沿用 `UnifiedModal` 的 auto 主题,不在共享壳里默认锁定 light。`PublishShareModal` 跟随当前 light / dark 主题;`PlatformReportDialog` 因包含二维码 / 扫码展示区,显式固定 light 以保证白底对比度和识别率。
|
||||
- 决策:平台入口的创作前置泥点阻断提示只在 `platform-entry` 局部抽成 `src/components/platform-entry/PlatformDraftGenerationPointNoticeDialog.tsx`,并使用 `DraftGenerationPointNotice` union(`insufficient-points` / `balance-load-failed`)承接业务真相;不要在 `common/` 再抽一个泛化 `BlockingNoticeDialog`,否则会把 `PlatformAcknowledgeStatusDialog` 的样式透传再包装一层而不缩小调用面。
|
||||
- 决策:`PlatformAsyncStatePanel` 从 profile modal 扩展到作品架类白底 panel;`CustomWorldCreationHub.tsx` 的作品架主体现在也统一走 `loadingState / emptyState / children` 三段 slot,但 error + 重试继续留在业务层外侧,不把共享组件扩成“banner + retry + content”全能状态机。后续白底作品架或列表 panel 若只是互斥的 `loading / empty / content`,优先直接复用这套骨架。
|
||||
- 决策:`CopyFeedbackButton.tsx` 的 `actionSurface` 分支继续收口到 `PlatformActionButton`,`pill` 分支继续保留 `PlatformPillBadge` 风格;复制反馈按钮不再直接调用 `getPlatformActionButtonClassName` 手拼平台按钮基础 chrome。后续同类“复制状态机 + 平台动作按钮”组合优先直接复用 `CopyFeedbackButton`,不要在业务页重新混写图标、文案、aria 和动作按钮 class。
|
||||
|
||||
@@ -3579,8 +3579,8 @@
|
||||
|
||||
- 现象:项目库点击“重命名”后,标题、输入框和按钮仍显示,但弹窗面板及遮罩背景变透明,看起来像“改名界面的背景没了”。
|
||||
- 原因:`UnifiedModal` 默认 portal 到 `document.body`;若业务入口只在页面内层继承 `platform-theme`,portal 根节点不会继承该容器的 CSS 变量。此时 `.platform-modal-shell` 的 `background: var(--platform-modal-fill)` 和 `.platform-overlay` 的背景声明都会失效。
|
||||
- 处理:平台白底工具弹窗优先复用 `PlatformToolModalShell`,由共享壳读取当前 `AuthUiContext.platformTheme`,并把 `platform-theme platform-theme--<light|dark>` 挂到 portal overlay;不要用硬编码白底掩盖主题变量缺失。必须直接使用 `UnifiedModal` 的特殊场景,也要在 `overlayClassName` 显式传递当前平台主题。
|
||||
- 验证:在 light / dark 主题下打开 portal 弹窗,断言 dialog 的 overlay 携带对应主题类,并在真实浏览器核对 panel 与遮罩的 computed background 均非透明。
|
||||
- 处理:`UnifiedModal` 在 `portal=true` 时默认把 `AuthUiContext.platformTheme` 注入 overlay,共享白底弹窗和直接调用都不应再手工拼接主题 class。完全自绘的黑底预览显式使用 `portalTheme="none"`;已明确固定主题的弹窗使用 `light` / `dark`;局部 CSS 仍固定白底且未完成暗色样式的弹窗,必须暂时显式固定 `light`,否则会出现白底白字或深浅样式混杂;`portal=false` 继续依赖原 DOM 主题作用域。裸 `createPortal` 若使用平台或画布 CSS 变量,必须改用相应的主题 portal 壳,不要用硬编码白底掩盖主题变量缺失。
|
||||
- 验证:在真实 `AuthUiContext.platformTheme="dark"` Provider 下打开 portal 弹窗,断言 auto 弹窗的 overlay 携带暗色主题类,固定浅色弹窗只携带浅色主题类,panel 与遮罩的 computed background 均非透明;同时断言 `portalTheme="none"` 的黑底预览不被平台 remap。
|
||||
- 关联:`src/components/project/ProjectGalleryView.tsx`、`src/components/common/PlatformToolModalShell.tsx`、`src/components/common/UnifiedModal.tsx`。
|
||||
|
||||
## 自主试玩失败后的修复责任不能同时落给总控和专业 Agent
|
||||
|
||||
@@ -283,6 +283,7 @@
|
||||
19.3.53. 认证入口白底弹窗壳层收口到 `src/components/auth/PlatformAuthModalShell.tsx`;该 Module 只承接平台主题 overlay、`platform-auth-card`、标准标题栏、关闭按钮、点击遮罩关闭和禁用 Escape 的认证弹窗策略,不持有短信 / 密码登录、重置密码、邀请码规范化、法律协议或错误状态。`LoginScreen.tsx` 与 `RegistrationInviteModal.tsx` 已接入,业务组件只保留表单状态与提交流程。后续认证域新增同形态白底弹窗时优先复用该壳层;账号安全详情和绑定手机号这类布局差异较大的卡片先独立评估,不把 auth shell 扩成万能认证容器。验证命令:`npx vitest run src/components/auth/PlatformAuthModalShell.test.tsx src/components/auth/AuthGate.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`。
|
||||
19.3.54. 账号 / 运行态 / onboarding 这轮继续分场景收口:`AccountModal.tsx` 的设置入口外层 overlay 与 auth card 壳层复用 `PlatformAuthModalShell`,并通过 `overlaySpacing`、`overlayStyle`、`showHeader` 和尺寸透传保留账号弹窗的 safe-area 与 direct account 唯一 dialog 语义;拼图运行态新增 `src/components/puzzle-runtime/PuzzleRuntimeModalShell.tsx`,只在 `puzzle-runtime` 内承接道具确认、设置、退出改造提示、失败弹窗和通关结算的 overlay / dialog / footer / button 骨架,原图查看、拖拽 ghost、飞行动画和全屏 runtime 容器不纳入 modal 收口;抓大鹅与跳一跳结算弹窗分别在 `Match3DRuntimeShell.tsx` 和 `JumpHopRuntimeShell.tsx` 内提取本地结算壳层 / summary / actions,保留玩法视觉身份;拼图 onboarding 首屏继续保留沉浸式全屏体验,只把登录保存覆盖层迁入 `UnifiedModal`,保持无关闭按钮、禁用遮罩关闭和禁用 Escape。后续 runtime 专属弹窗优先先抽玩法目录内薄壳;只有出现跨玩法稳定同构接口时再上升到 `common/`,不要把 `PlatformToolModalShell` 强行套到像素 / 游戏运行态 overlay。验证命令:`npm run test -- src/components/auth/AccountModal.test.tsx src/components/auth/PlatformAuthModalShell.test.tsx src/components/platform-entry/PlatformEntryFlowShellImpl/PuzzleOnboardingView.test.tsx src/components/match3d-runtime/Match3DRuntimeShell.test.tsx src/components/jump-hop-runtime/JumpHopRuntimeShell.test.tsx src/components/puzzle-runtime/PuzzleRuntimeShell.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`。
|
||||
19.3.55. 拼图 / 拼消消运行态的剩余阻断层继续按玩法目录局部收口:`src/components/platform-entry/PlatformEntryFlowShellImpl/PuzzleRuntimeBlockingOverlay.tsx` 只承接平台入口里拼图“正在准备下一关”的短暂阻断层,继续复用 `UnifiedModal` 的遮罩、dialog 语义和关闭禁用策略,但不把这类运行态等待面板直接提升到 `common/`;`src/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.tsx` 则在玩法目录内新增 `PuzzleClearRuntimeOverlayShell`、`PuzzleClearRuntimePendingOverlay` 与 `PuzzleClearRuntimeSettlementDialog`,把 `!activeRun` 的等待层和 `level_cleared / finished / level_failed` 的结算层统一成一条本地结构线,同时保留拼消消自己的视觉和动作分流。拖拽 ghost、swap flight、补牌 / 消除动画、全屏 runtime 容器和其它强玩法视觉层不算旧 modal 债务,不跟这条线混收。验证命令:`npm run test -- src/components/platform-entry/PlatformEntryFlowShellImpl/PuzzleRuntimeBlockingOverlay.test.tsx src/components/platform-entry/PlatformEntryFlowShellImpl.test.ts src/components/puzzle-clear-runtime/PuzzleClearRuntimeShell.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`。
|
||||
19.3.56. `UnifiedModal` 默认在 `portal=true` 时读取 `AuthUiContext.platformTheme`,并把 `platform-theme platform-theme--<light|dark>` 注入 portal overlay,从底层保证 `--platform-modal-fill`、`--platform-overlay-fill` 等 CSS 变量不因挂到 `document.body` 而丢失。新增 `portalTheme="auto|light|dark|none"`:默认 `auto` 优先保留调用方已给出的明确主题,否则使用当前主题并在无 Provider 时回退 light;纯黑底图片 / 素材预览使用 `none` 保持完全自绘;`portal=false` 不改变原 DOM 继承。`PlatformToolModalShell`、`PlatformDangerConfirmDialog`、认证与工具信息壳不再重复维护 portal 主题桥,但继续保留各自的 panel、间距、按钮和层级语义。编辑器裸 portal 菜单应复用 `ImageCanvasEditorPortal` 同时获得平台与画布变量;图片信息、修改图片和画布快捷键三个面板仍使用固定浅色 CSS,在完整支持暗色样式前显式传入 `portalTheme="light"`;已退役玩法目录不因本次收口重新进入维护范围。验证命令:`npm run test -- src/components/image-editor/UnifiedModalPortalTheme.test.tsx src/components/image-editor/ImageCanvasShortcutDialogView.test.tsx src/components/image-editor/ImageCanvasEditGenerationModalView.test.tsx src/components/image-editor/ImageCanvasMetadataModalView.test.tsx src/components/image-editor/EditorAgentConversation/MessageBubble.test.tsx src/components/image-editor/EditorAgentConversation/EditorAgentConversationPanelView.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.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`。
|
||||
|
||||
@@ -65,6 +65,7 @@
|
||||
- Enter 发送必须同时排除 `isComposing` 和旧 Safari / WebKit 候选词确认事件的 `keyCode === 229`,避免输入法选词时误发送。
|
||||
- 用户消息必须包含去除首尾空白后的非空文本;附件只能随文本消息发送,前端发送门禁与后端 `module-editor-agent` 领域校验必须同时拒绝纯附件消息。
|
||||
- 会话管理入口在对话框头部:当前会话标题 + 历史会话下拉(按更新时间倒序)+ 新建对话按钮,全部包在对话框内。
|
||||
- 右上角删除当前对话的危险确认框继续使用 `PlatformDangerConfirmDialog`;其 portal 主题由 `UnifiedModal` 统一恢复,panel 使用 `platform-remap-surface`,且层级与附件选择弹窗一致,避免背景透明、错色或被画布控件遮挡。
|
||||
- 当前会话没有任何已发送消息时,新建对话按钮置灰且不可点击;输入框草稿和未发送附件不算会话内容。当前会话已有消息时可新建,新建成功后只切换到返回的空白会话,输入文字、附件及附件选择状态与切换历史会话时一样原样保留,旧会话继续保留在历史会话下拉中;创建失败同样不修改草稿。
|
||||
- 新会话创建请求 pending 时禁用历史会话下拉和发送动作,但输入框与附件仍可编辑;会话列表或历史消息加载期间同样禁用发送。表单提交处理器必须复用相同门禁,不能先清空草稿再由 hook 静默跳过发送。
|
||||
- 快速切换会话或会话轮询刷新产生并发详情请求时,每个请求必须获得唯一且单调递增的请求序号;前端只允许最后发起且有权生效的请求更新当前会话、消息、错误和加载态。被正在进行的会话切换压制的旧会话 refresh 不得提前结束新切换的加载态,旧响应也不得覆盖用户最新选择。
|
||||
@@ -78,7 +79,7 @@
|
||||
- 「素材库」页签:账号级素材库(复用 `ImageCanvasAssetLibrary` 数据源);
|
||||
- 多选 + 底部「取消 / 应用」。
|
||||
- 网格末尾上传格为后续补齐项;在上传格未落地前,对话附件只从已有画布资源和账号素材库选择。后续若从对话入口上传图片,必须复用素材库 / 画布资源登记链路,不新增对话私有图片类型。
|
||||
- 附件选择弹窗使用 `PlatformToolModalShell` 承接 portal 主题变量和不透明 panel 背景;不能直接把未注入 `platform-theme` 的 `UnifiedModal` portal 到 `document.body`,否则 `--platform-modal-fill` 失效后面板会变透明。
|
||||
- 附件选择弹窗使用 `PlatformToolModalShell` 承接白底 panel 和标准间距;底层 `UnifiedModal` 会把当前 `platform-theme` 自动注入 portal overlay,保证 `--platform-modal-fill` 在 `document.body` 下仍有效。
|
||||
- 应用后附件以胶囊 chip 挂在输入框上方;发出的消息内附件渲染为纯文本胶囊 chip(名称 + 小图标),**默认无缩略图,鼠标悬浮才浮出缩略图预览**。
|
||||
- 附件领域形状:统一为画布资源 / 素材库对象引用(`resourceId` / `assetId` + 可选 `objectKey`),不存在只属于对话的第三种图;单条消息上限 9 张(前后端共同校验)。前端可携带展示用 `imageSrc` / `thumbnailSrc`,后端必须按当前工程和当前账号重新归一、校验归属与 `objectKey`。
|
||||
- 附件 `label` 是人类可读的展示元数据,统一限制为最多 24 个 Unicode 码点。归一化时先去掉首尾空白,删除控制字符以及除 `-`、`_`、`.` 之外的 ASCII 标点,把连续空白折叠为一个半角空格,再按 24 码点截断;只含被过滤字符的 label 视为缺失。中文等非 ASCII 标点不属于本轮过滤范围。
|
||||
|
||||
@@ -57,9 +57,10 @@ export function PlatformAuthModalShell({
|
||||
closeOnEscape={false}
|
||||
size={size}
|
||||
showHeader={showHeader}
|
||||
portalTheme={platformTheme}
|
||||
zIndexClassName={zIndexClassName}
|
||||
overlayClassName={joinClassNames(
|
||||
`platform-theme platform-theme--${platformTheme} text-[var(--platform-text-strong)]`,
|
||||
'text-[var(--platform-text-strong)]',
|
||||
overlaySpacing === 'default' && '!px-3 !py-4 sm:!p-4',
|
||||
overlayClassName,
|
||||
)}
|
||||
|
||||
@@ -289,6 +289,7 @@ test('creative image input panel confirms before removing uploaded image', () =>
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '移除拼图图片' }));
|
||||
const dialog = screen.getByRole('dialog', { name: '移除拼图图片?' });
|
||||
expect(dialog.parentElement?.className).toContain('platform-theme--light');
|
||||
expect(within(dialog).getByText('移除后需要重新上传图片。')).toBeTruthy();
|
||||
fireEvent.click(within(dialog).getByRole('button', { name: '移除' }));
|
||||
expect(onMainImageRemove).toHaveBeenCalledTimes(1);
|
||||
|
||||
@@ -88,9 +88,7 @@ export function LegalDocumentModal({
|
||||
size="md"
|
||||
closeLabel="关闭法律信息"
|
||||
zIndexClassName={zIndexClassName ?? 'z-[150]'}
|
||||
overlayClassName={`platform-theme ${
|
||||
platformTheme ? `platform-theme--${platformTheme}` : ''
|
||||
}`}
|
||||
portalTheme={platformTheme ?? 'auto'}
|
||||
panelClassName="platform-remap-surface rounded-t-[1.4rem] sm:rounded-[1.4rem]"
|
||||
headerClassName="items-center"
|
||||
bodyClassName="px-4 py-0 sm:px-5"
|
||||
|
||||
@@ -24,6 +24,10 @@ test('renders a standard danger confirmation with cancel and confirm actions', (
|
||||
|
||||
const dialog = screen.getByRole('dialog', { name: '删除作品' });
|
||||
|
||||
expect(dialog.parentElement?.className).toContain('platform-theme--light');
|
||||
expect(dialog.parentElement?.className).toContain('z-[140]');
|
||||
expect(dialog.className).toContain('platform-remap-surface');
|
||||
expect(dialog.className).toContain('shadow-[0_24px_80px_rgba(0,0,0,0.55)]');
|
||||
expect(within(dialog).getByText('确认删除《潮雾列岛》吗?')).toBeTruthy();
|
||||
expect(within(dialog).getByText('删除后不可恢复。')).toBeTruthy();
|
||||
|
||||
|
||||
@@ -21,8 +21,13 @@ type PlatformDangerConfirmDialogProps = {
|
||||
panelClassName?: string;
|
||||
footerClassName?: string;
|
||||
confirmClassName?: string;
|
||||
zIndexClassName?: string;
|
||||
};
|
||||
|
||||
function joinClassNames(...classNames: Array<string | undefined>) {
|
||||
return classNames.filter(Boolean).join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* 平台危险确认弹窗。
|
||||
* 统一承接需要“确认 / 取消 + 危险主动作”语义的标准弹窗壳层。
|
||||
@@ -46,6 +51,7 @@ export function PlatformDangerConfirmDialog({
|
||||
panelClassName,
|
||||
footerClassName,
|
||||
confirmClassName,
|
||||
zIndexClassName = 'z-[140]',
|
||||
}: PlatformDangerConfirmDialogProps) {
|
||||
return (
|
||||
<UnifiedConfirmDialog
|
||||
@@ -65,9 +71,13 @@ export function PlatformDangerConfirmDialog({
|
||||
portal={portal}
|
||||
size={size}
|
||||
overlayClassName={overlayClassName}
|
||||
panelClassName={panelClassName}
|
||||
panelClassName={joinClassNames(
|
||||
'platform-remap-surface shadow-[0_24px_80px_rgba(0,0,0,0.55)]',
|
||||
panelClassName,
|
||||
)}
|
||||
footerClassName={footerClassName}
|
||||
confirmClassName={confirmClassName}
|
||||
zIndexClassName={zIndexClassName}
|
||||
>
|
||||
{children}
|
||||
</UnifiedConfirmDialog>
|
||||
|
||||
@@ -103,6 +103,7 @@ test('renders full-screen image preview with zoom controls and dark backdrop', (
|
||||
expect(
|
||||
dialog.parentElement?.className.includes('!bg-black'),
|
||||
).toBe(true);
|
||||
expect(dialog.parentElement?.className).not.toContain('platform-theme--');
|
||||
expect(screen.getByRole('button', { name: '放大图片' })).toBeTruthy();
|
||||
expect(screen.getByRole('button', { name: '缩小图片' })).toBeTruthy();
|
||||
expect(screen.getByRole('button', { name: '重置图片缩放' })).toBeTruthy();
|
||||
|
||||
@@ -243,6 +243,8 @@ export function PlatformImagePreviewModal({
|
||||
showHeader={false}
|
||||
showCloseButton={false}
|
||||
size="fullscreen"
|
||||
// 全黑自绘查看器不需要平台变量;避免 light / dark remap 改写黑底、白字和暗色控件。
|
||||
portalTheme="none"
|
||||
zIndexClassName={zIndexClassName}
|
||||
overlayClassName="!items-stretch !justify-stretch !bg-black !p-0 !backdrop-blur-none"
|
||||
panelClassName="platform-image-preview-modal !h-[100dvh] !max-h-none !max-w-none !rounded-none border-0 bg-black text-white shadow-none"
|
||||
|
||||
@@ -10,6 +10,10 @@ vi.mock('../../services/clipboard', () => ({
|
||||
copyTextToClipboard: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../auth/AuthUiContext', () => ({
|
||||
useAuthUi: () => ({ platformTheme: 'dark' }),
|
||||
}));
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
@@ -31,6 +35,8 @@ test('renders report fields and copies the joined report lines', async () => {
|
||||
);
|
||||
|
||||
const dialog = screen.getByRole('dialog', { name: '统一报告' });
|
||||
expect(dialog.parentElement?.className).toContain('platform-theme--light');
|
||||
expect(dialog.parentElement?.className).not.toContain('platform-theme--dark');
|
||||
expect(within(dialog).getByText('拼图草稿 puzzle-session-1')).toBeTruthy();
|
||||
expect(within(dialog).getByText('已完成')).toBeTruthy();
|
||||
|
||||
|
||||
@@ -54,6 +54,8 @@ export function PlatformReportDialog({
|
||||
open={open}
|
||||
title={title}
|
||||
onClose={onClose}
|
||||
// 报告中的二维码 / 扫码区固定使用浅色背景,保证对比度和识别率。
|
||||
platformTheme="light"
|
||||
overlayClassName={overlayClassName}
|
||||
panelClassName={panelClassName}
|
||||
bodyClassName="space-y-3"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import { useAuthUi } from '../auth/AuthUiContext';
|
||||
import { UnifiedModal } from './UnifiedModal';
|
||||
|
||||
type PlatformToolModalShellProps = {
|
||||
@@ -50,9 +49,6 @@ export function PlatformToolModalShell({
|
||||
bodyClassName,
|
||||
footerClassName,
|
||||
}: PlatformToolModalShellProps) {
|
||||
const resolvedPlatformTheme =
|
||||
useAuthUi()?.platformTheme ?? 'light';
|
||||
|
||||
return (
|
||||
<UnifiedModal
|
||||
open={open}
|
||||
@@ -68,7 +64,6 @@ export function PlatformToolModalShell({
|
||||
closeOnBackdrop={closeOnBackdrop}
|
||||
closeOnEscape={closeOnEscape}
|
||||
zIndexClassName={zIndexClassName}
|
||||
overlayClassName={`platform-theme platform-theme--${resolvedPlatformTheme}`}
|
||||
panelClassName={joinClassNames(
|
||||
'platform-remap-surface shadow-[0_24px_80px_rgba(0,0,0,0.55)]',
|
||||
panelClassName,
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { render, screen, within } from '@testing-library/react';
|
||||
import { expect, test } from 'vitest';
|
||||
import { expect, test, vi } from 'vitest';
|
||||
|
||||
import { PlatformUtilityInfoModal } from './PlatformUtilityInfoModal';
|
||||
|
||||
vi.mock('../auth/AuthUiContext', () => ({
|
||||
useAuthUi: () => ({ platformTheme: 'dark' }),
|
||||
}));
|
||||
|
||||
test('renders platform utility info modal shell with default platform styling', () => {
|
||||
render(
|
||||
<PlatformUtilityInfoModal
|
||||
@@ -19,7 +23,7 @@ test('renders platform utility info modal shell with default platform styling',
|
||||
);
|
||||
|
||||
const dialog = screen.getByRole('dialog', { name: '工具信息' });
|
||||
expect(dialog.parentElement?.className).toContain('platform-theme--light');
|
||||
expect(dialog.parentElement?.className).toContain('platform-theme--dark');
|
||||
expect(dialog.parentElement?.className).toContain('!items-center');
|
||||
expect(dialog.className).toContain('platform-remap-surface');
|
||||
expect(dialog.className).toContain('rounded-[1.5rem]');
|
||||
|
||||
@@ -29,7 +29,7 @@ export function PlatformUtilityInfoModal({
|
||||
onClose,
|
||||
children,
|
||||
footer,
|
||||
platformTheme = 'light',
|
||||
platformTheme,
|
||||
overlayClassName,
|
||||
panelClassName,
|
||||
bodyClassName,
|
||||
@@ -41,11 +41,8 @@ export function PlatformUtilityInfoModal({
|
||||
title={title}
|
||||
onClose={onClose}
|
||||
size="sm"
|
||||
overlayClassName={joinClassNames(
|
||||
`platform-theme platform-theme--${platformTheme}`,
|
||||
'!items-center',
|
||||
overlayClassName,
|
||||
)}
|
||||
portalTheme={platformTheme}
|
||||
overlayClassName={joinClassNames('!items-center', overlayClassName)}
|
||||
panelClassName={joinClassNames('platform-remap-surface', panelClassName)}
|
||||
bodyClassName={joinClassNames(
|
||||
'space-y-4 px-4 py-4 sm:px-5 sm:py-5',
|
||||
|
||||
@@ -29,6 +29,10 @@ vi.mock('../../services/clipboard', () => ({
|
||||
copyTextToClipboard: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../auth/AuthUiContext', () => ({
|
||||
useAuthUi: () => ({ platformTheme: 'dark' }),
|
||||
}));
|
||||
|
||||
const payload: PublishShareModalPayload = {
|
||||
title: '暖灯猫街',
|
||||
publicWorkCode: 'PZ-00000001',
|
||||
@@ -119,7 +123,7 @@ describe('PublishShareModal', () => {
|
||||
|
||||
const dialog = screen.getByRole('dialog', { name: '分享给朋友' });
|
||||
expect(dialog.parentElement?.className).toContain('!items-center');
|
||||
expect(dialog.parentElement?.className).toContain('platform-theme--light');
|
||||
expect(dialog.parentElement?.className).toContain('platform-theme--dark');
|
||||
expect(dialog.className).toContain('platform-modal-shell');
|
||||
expect(dialog.className).toContain('rounded-[1.75rem]');
|
||||
expect(dialog.getAttribute('style')).toBeNull();
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
openHostShare,
|
||||
openHostShareGrid,
|
||||
} from '../../services/host-bridge/hostBridge';
|
||||
import { useAuthUi } from '../auth/AuthUiContext';
|
||||
import { ResolvedAssetImage } from '../ResolvedAssetImage';
|
||||
import { PlatformUtilityInfoModal } from './PlatformUtilityInfoModal';
|
||||
import { downloadPublishShareCardImage } from './publishShareCardImage';
|
||||
@@ -77,7 +76,6 @@ export function PublishShareModal({
|
||||
payload,
|
||||
onClose,
|
||||
}: PublishShareModalProps) {
|
||||
const platformTheme = useAuthUi()?.platformTheme ?? 'light';
|
||||
const [copyState, setCopyState] = useState<ActionState>('idle');
|
||||
const [downloadState, setDownloadState] = useState<ActionState>('idle');
|
||||
const [gridState, setGridState] = useState<ActionState>('idle');
|
||||
@@ -233,7 +231,6 @@ export function PublishShareModal({
|
||||
open={open && Boolean(payload)}
|
||||
title="分享给朋友"
|
||||
onClose={onClose}
|
||||
platformTheme={platformTheme}
|
||||
panelClassName="rounded-[1.75rem]"
|
||||
footerClassName="border-t-0 px-4 pb-5 pt-0 sm:px-5"
|
||||
footer={
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import { PlatformActionButton } from './PlatformActionButton';
|
||||
import { UnifiedModal } from './UnifiedModal';
|
||||
import { UnifiedModal, type UnifiedModalPortalTheme } from './UnifiedModal';
|
||||
|
||||
type UnifiedConfirmDialogTone = 'primary' | 'danger';
|
||||
|
||||
@@ -24,6 +24,7 @@ type UnifiedConfirmDialogProps = {
|
||||
closeOnBackdrop?: boolean;
|
||||
showCloseButton?: boolean;
|
||||
portal?: boolean;
|
||||
portalTheme?: UnifiedModalPortalTheme;
|
||||
size?: 'sm' | 'md';
|
||||
overlayClassName?: string;
|
||||
panelClassName?: string;
|
||||
@@ -54,6 +55,7 @@ export function UnifiedConfirmDialog({
|
||||
closeOnBackdrop = true,
|
||||
showCloseButton = true,
|
||||
portal = true,
|
||||
portalTheme = 'auto',
|
||||
size = 'sm',
|
||||
overlayClassName,
|
||||
panelClassName,
|
||||
@@ -74,6 +76,7 @@ export function UnifiedConfirmDialog({
|
||||
closeOnBackdrop={closeOnBackdrop && !busy}
|
||||
showCloseButton={showCloseButton}
|
||||
portal={portal}
|
||||
portalTheme={portalTheme}
|
||||
size={size}
|
||||
overlayClassName={overlayClassName}
|
||||
panelClassName={panelClassName}
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
} from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import type { PlatformTheme } from '../../../packages/shared/src/contracts/runtime';
|
||||
import { useAuthUi } from '../auth/AuthUiContext';
|
||||
import { PlatformModalCloseButton } from './PlatformModalCloseButton';
|
||||
|
||||
type UnifiedModalSize = 'sm' | 'md' | 'lg' | 'xl' | 'fullscreen';
|
||||
@@ -18,6 +20,8 @@ type UnifiedModalCloseIcon = ComponentProps<
|
||||
typeof PlatformModalCloseButton
|
||||
>['icon'];
|
||||
|
||||
export type UnifiedModalPortalTheme = PlatformTheme | 'auto' | 'none';
|
||||
|
||||
type UnifiedModalProps = {
|
||||
open: boolean;
|
||||
title: string;
|
||||
@@ -37,6 +41,7 @@ type UnifiedModalProps = {
|
||||
closeVariant?: UnifiedModalCloseVariant;
|
||||
closeIcon?: UnifiedModalCloseIcon;
|
||||
portal?: boolean;
|
||||
portalTheme?: UnifiedModalPortalTheme;
|
||||
zIndexClassName?: string;
|
||||
overlayClassName?: string;
|
||||
overlayStyle?: CSSProperties;
|
||||
@@ -63,6 +68,44 @@ function joinClassNames(
|
||||
return classNames.filter(Boolean).join(' ');
|
||||
}
|
||||
|
||||
function resolvePortalOverlayClassName({
|
||||
overlayClassName,
|
||||
portalTheme,
|
||||
contextualTheme,
|
||||
}: {
|
||||
overlayClassName?: string;
|
||||
portalTheme: UnifiedModalPortalTheme;
|
||||
contextualTheme: PlatformTheme;
|
||||
}) {
|
||||
if (portalTheme === 'none') {
|
||||
return overlayClassName;
|
||||
}
|
||||
|
||||
const classNames = overlayClassName?.split(/\s+/u).filter(Boolean) ?? [];
|
||||
const explicitTheme = classNames.find(
|
||||
(className): className is `platform-theme--${PlatformTheme}` =>
|
||||
className === 'platform-theme--light' ||
|
||||
className === 'platform-theme--dark',
|
||||
);
|
||||
const resolvedTheme =
|
||||
portalTheme === 'auto'
|
||||
? ((explicitTheme?.replace('platform-theme--', '') as
|
||||
PlatformTheme | undefined) ?? contextualTheme)
|
||||
: portalTheme;
|
||||
const remainingClassNames = classNames.filter(
|
||||
(className) =>
|
||||
className !== 'platform-theme' &&
|
||||
className !== 'platform-theme--light' &&
|
||||
className !== 'platform-theme--dark',
|
||||
);
|
||||
|
||||
return joinClassNames(
|
||||
'platform-theme',
|
||||
`platform-theme--${resolvedTheme}`,
|
||||
...remainingClassNames,
|
||||
);
|
||||
}
|
||||
|
||||
function UnifiedModalContent({
|
||||
open,
|
||||
title,
|
||||
@@ -91,7 +134,7 @@ function UnifiedModalContent({
|
||||
bodyClassName,
|
||||
footerClassName,
|
||||
panelStyle,
|
||||
}: Omit<UnifiedModalProps, 'portal'>) {
|
||||
}: Omit<UnifiedModalProps, 'portal' | 'portalTheme'>) {
|
||||
const generatedTitleId = useId();
|
||||
const descriptionId = useId();
|
||||
const titleId = titleIdProp ?? generatedTitleId;
|
||||
@@ -231,10 +274,30 @@ function UnifiedModalContent({
|
||||
* 统一模态窗口外壳。
|
||||
* 业务组件只传入标题、内容和操作区;遮罩、无障碍属性、Escape 与移动端布局在这里收口。
|
||||
*/
|
||||
export function UnifiedModal({ portal = true, ...props }: UnifiedModalProps) {
|
||||
export function UnifiedModal({
|
||||
portal = true,
|
||||
portalTheme = 'auto',
|
||||
overlayClassName,
|
||||
...props
|
||||
}: UnifiedModalProps) {
|
||||
const contextualTheme = useAuthUi()?.platformTheme ?? 'light';
|
||||
const resolvedProps = {
|
||||
...props,
|
||||
overlayClassName: portal
|
||||
? resolvePortalOverlayClassName({
|
||||
overlayClassName,
|
||||
portalTheme,
|
||||
contextualTheme,
|
||||
})
|
||||
: overlayClassName,
|
||||
};
|
||||
|
||||
if (!portal || typeof document === 'undefined') {
|
||||
return <UnifiedModalContent {...props} />;
|
||||
return <UnifiedModalContent {...resolvedProps} />;
|
||||
}
|
||||
|
||||
return createPortal(<UnifiedModalContent {...props} />, document.body);
|
||||
return createPortal(
|
||||
<UnifiedModalContent {...resolvedProps} />,
|
||||
document.body,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -569,6 +569,10 @@ describe('CreationLandingView', () => {
|
||||
);
|
||||
|
||||
const dialog = await screen.findByRole('dialog', { name: '角色英雄' });
|
||||
expect(dialog.parentElement?.className).not.toContain('platform-theme--');
|
||||
expect(dialog.parentElement?.className).toContain(
|
||||
'creation-landing__showcase-modal-overlay',
|
||||
);
|
||||
expect(within(dialog).getByText('赞')).toBeTruthy();
|
||||
expect(within(dialog).getByText('3')).toBeTruthy();
|
||||
expect(within(dialog).queryByRole('button', { name: /点赞/ })).toBeNull();
|
||||
|
||||
@@ -409,6 +409,8 @@ function CreationShowcaseModal({
|
||||
showHeader={false}
|
||||
showCloseButton={false}
|
||||
size="fullscreen"
|
||||
// 素材舞台是独立黑底视觉;避免平台主题 remap 把它改成普通白底工具弹窗。
|
||||
portalTheme="none"
|
||||
zIndexClassName="z-[120]"
|
||||
overlayClassName="creation-landing__showcase-modal-overlay"
|
||||
panelClassName="creation-landing__showcase-modal-panel"
|
||||
@@ -1174,7 +1176,8 @@ export function CreationLandingView({
|
||||
showHeader={false}
|
||||
showCloseButton={false}
|
||||
size="sm"
|
||||
overlayClassName="platform-theme platform-theme--light platform-mobile-home-welcome-overlay !items-center !p-4"
|
||||
portalTheme="light"
|
||||
overlayClassName="platform-mobile-home-welcome-overlay !items-center !p-4"
|
||||
panelClassName="platform-remap-surface platform-mobile-home-welcome-dialog"
|
||||
bodyClassName="platform-mobile-home-welcome-dialog__body"
|
||||
footerClassName="platform-mobile-home-welcome-dialog__footer"
|
||||
|
||||
+12
-6
@@ -338,9 +338,7 @@ describe('EditorAgentConversationPanelView', () => {
|
||||
expect(
|
||||
screen.getByText('一二三四五六七八九十甲乙丙丁戊己庚辛壬癸子丑寅卯'),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
screen.getByRole('option', { name: '角色参考' }),
|
||||
).toBeTruthy();
|
||||
expect(screen.getByRole('option', { name: '角色参考' })).toBeTruthy();
|
||||
expect(screen.getByRole('option', { name: '新对话' })).toBeTruthy();
|
||||
|
||||
fireEvent.change(screen.getByLabelText('发送给画布 Agent'), {
|
||||
@@ -530,9 +528,9 @@ describe('EditorAgentConversationPanelView', () => {
|
||||
(screen.getByLabelText('发送给画布 Agent') as HTMLTextAreaElement).value,
|
||||
).toBe('需要保留的草稿');
|
||||
expect(screen.getByText('已经看到画布内容')).toBeTruthy();
|
||||
expect(
|
||||
(screen.getByLabelText('当前对话') as HTMLSelectElement).value,
|
||||
).toBe('conversation-1');
|
||||
expect((screen.getByLabelText('当前对话') as HTMLSelectElement).value).toBe(
|
||||
'conversation-1',
|
||||
);
|
||||
});
|
||||
|
||||
it('preserves newer draft edits while creating a conversation', async () => {
|
||||
@@ -1989,6 +1987,14 @@ describe('EditorAgentConversationPanelView', () => {
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '删除当前对话' }));
|
||||
const confirmDialog = screen.getByRole('dialog', { name: '删除对话' });
|
||||
expect(confirmDialog.parentElement?.className).toContain(
|
||||
'platform-theme--light',
|
||||
);
|
||||
expect(confirmDialog.parentElement?.className).toContain('z-[140]');
|
||||
expect(confirmDialog.className).toContain('platform-remap-surface');
|
||||
expect(
|
||||
within(confirmDialog).getByText('确认删除这个对话吗?'),
|
||||
).toBeTruthy();
|
||||
fireEvent.click(
|
||||
within(confirmDialog).getByRole('button', { name: '确认删除' }),
|
||||
);
|
||||
|
||||
+3
-1
@@ -391,7 +391,9 @@ export function EditorAgentConversationPanelView({
|
||||
setDeleteConfirmOpen(false),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
>
|
||||
确认删除这个对话吗?
|
||||
</PlatformDangerConfirmDialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -118,9 +118,11 @@ describe('MessageBubble', () => {
|
||||
clientX: 30,
|
||||
clientY: 40,
|
||||
});
|
||||
const menu = screen.getByRole('menu', { name: '消息右键菜单' });
|
||||
expect(menu.style.zIndex).toBe('60');
|
||||
expect(
|
||||
screen.getByRole('menu', { name: '消息右键菜单' }).style.zIndex,
|
||||
).toBe('60');
|
||||
menu.closest('.image-canvas-editor__portal-theme')?.className,
|
||||
).toContain('platform-theme--light');
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: '复制文本' }));
|
||||
|
||||
await waitFor(() =>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user