diff --git a/.hermes/shared-memory/decision-log.md b/.hermes/shared-memory/decision-log.md index 636fd197..16752357 100644 --- a/.hermes/shared-memory/decision-log.md +++ b/.hermes/shared-memory/decision-log.md @@ -53,6 +53,7 @@ - 2026-06-10 追加:RPG 大编辑器里的当前角色、可选角色、预设背景和场景连接关系等暗色信息面板通过本地 `EditorInfoPanel` 复用 `PlatformSubpanel surface="dark"`;有右侧动作的面板也只向适配器传 actions,不再在业务 JSX 中重复手写暗色面板边框、底色、圆角、标题行和内容间距。验证命令:`npm run test -- src/components/CustomWorldEntityEditorModal.test.tsx -t "场景编辑器会在场景内展示槽位化多幕配置并保存"`。 - 2026-06-09 追加:大鱼吃小鱼结果页白底平台动作迁移到 `PlatformActionButton shape="pill" size="xs"`;资产工坊关闭 / 生成正式图、关卡主图 / 待机 / 移动入口和场地背景生成只保留业务回调,深色 hero 返回 / 测试 / 发布按钮继续保留玩法品牌布局。 - 2026-06-09 追加:敲木鱼 fallback 返回、跳一跳结算、拼消消 runtime header / 结算弹窗等白底 HUD 动作使用 `PlatformActionButton`,拼消消 runtime 白底错误条使用 `PlatformStatusMessage surface="platform"`;深色半透明游戏提示和强品牌按钮仍可保留 runtime 专用布局。 +- 2026-06-10 追加:运行态短错误 / 成功 / 命中反馈 chip 使用 `PlatformRuntimeStatusToast` 承接圆角、字号、阴影、色值和 `role="alert/status"` 语义;跳一跳、拼图、敲木鱼、方洞和宝贝爱画运行态短 toast 已迁移。玩法专属返回按钮、计分牌、蓄力提示和强品牌主按钮仍留在 runtime 壳层,不把位置和玩法资产耦合进公共 Module。验证命令:`npm run test -- src/components/common/PlatformRuntimeStatusToast.test.tsx src/components/jump-hop-runtime/JumpHopRuntimeShell.test.tsx src/components/puzzle-runtime/PuzzleRuntimeShell.test.tsx src/components/wooden-fish-runtime/WoodenFishRuntimeShell.test.tsx src/components/square-hole-runtime/SquareHoleRuntimeShell.test.tsx src/components/edutainment-runtime/BabyLoveDrawingRuntimeShell.test.tsx`。 - 2026-06-09 追加:历史图片 / 历史素材 / 可引用素材选择统一使用 `src/components/common/PlatformAssetPickerCard.tsx` 中的 `PlatformAssetPickerCard` 与 `PlatformAssetPickerGrid`,由该 Module 承载缩略图、禁用态、选中态、边框、hover、主副文案、`ResolvedAssetImage` 壳层、错误态、读取态、空态和网格布局;拼图历史图片弹窗、方洞历史生成、视觉小说历史素材选择器、RPG 大编辑器历史素材弹窗和抓大鹅封面编辑可引用素材网格已先迁移,业务页只传素材数组、素材地址、文案、可访问名称、surface、选中判断和选择回调。RPG 大编辑器等暗色弹窗使用 `surface="editorDark"`,不混用白底平台卡片视觉;场景横图通过 `imageShellClassName` 保留 16:9。 - 2026-06-09 追加:平台白底圆角输入框和文本域统一使用 `src/components/common/PlatformTextField.tsx` 承载 input / textarea 语义、基础边框、背景、内边距、字号 / 行高、密度和禁用态;同组下拉框使用 `PlatformSelectField` 复用同一输入 chrome。抓大鹅结果页作品名称 / 描述、封面描述、素材名称、批量新增 / 批量重生成物品名称,方洞结果页主信息表单和形状 / 洞口选项字段,拼图结果页作品信息 / 关卡名称 / 智能修订输入,敲木鱼结果页作品标题 / 简介,敲木鱼创作工作台功德词条输入,creative-agent 模板确认调整弹层关卡数输入,拼消消创作工作台作品标题 / 简介 / 主题词、跳一跳创作工作台主题,以及视觉小说结果页音乐生成、作品信息、开场、运行配置、角色、场景、阶段和世界观普通文本 / 下拉字段已先迁移,业务页只保留受控值、事件、可访问名称、占位符、选项和局部布局 class。同一面板内的主图上传和提示词参考图上传必须使用不同可访问名称,避免多个同名“上传参考图”入口让测试和读屏语义混淆;拼图关卡编辑中的描述参考图入口使用“上传描述参考图”。 - 2026-06-09 追加:通用创作图片输入面板的提示词文本域也使用 `PlatformTextField variant="textarea" density="roomy"`;图片面板只通过局部 class 保留高度、`pb-14` 和浮动参考图上传按钮避让,不再自己维护白底 textarea 边框、背景、字号和禁用态。 diff --git a/docs/README.md b/docs/README.md index ab91c1e5..a56edf70 100644 --- a/docs/README.md +++ b/docs/README.md @@ -83,7 +83,7 @@ RPG Agent 结果页发布门禁展示和预览来源 label 收口到 `src/compon 平台入口错误 / 完成弹窗的文案归一、来源格式、候选择一、dismiss key 与任务完成文案收口到 `src/components/platform-entry/platformDialogStateModel.ts`,规则见 [【前端架构】PlatformDialogStateModel收口计划-2026-06-03.md](./technical/【前端架构】PlatformDialogStateModel收口计划-2026-06-03.md)。 -平台 UI Kit 的提示 / 确认弹窗收口到 `src/components/common/UnifiedConfirmDialog.tsx`,复制反馈收口到 `src/components/common/useCopyFeedback.ts`、`src/components/common/CopyFeedbackButton.tsx`、`src/components/common/CopyCodeButton.tsx` 与 `src/components/common/CopyFeedbackMessage.tsx`,基础状态提示收口到 `src/components/common/PlatformStatusMessage.tsx`,平台空态 / 轻量加载态收口到 `src/components/common/PlatformEmptyState.tsx`,平台动作按钮收口到 `src/components/common/PlatformActionButton.tsx`,平台白底子面板 / 小型列表卡片收口到 `src/components/common/PlatformSubpanel.tsx`,平台媒体预览框收口到 `src/components/common/PlatformMediaFrame.tsx`,平台胶囊状态标签收口到 `src/components/common/PlatformPillBadge.tsx`,平台 / 个人中心弹窗关闭按钮收口到 `src/components/common/PlatformModalCloseButton.tsx`,底层继续复用 `UnifiedModal`;普通提示、确认 / 取消、危险确认、复制状态机、短代码复制 chip、复制按钮表现、白底 / 个人中心 / 认证入口 token 状态条、无操作空态、主动作按钮、白底子面板、白底交互列表卡片、图片源 / fallback / 固定比例媒体预览、单个状态 / 标签 chip 和圆形关闭按钮优先使用公共 Module,规则见 [【前端架构】PlatformUiKit弹窗组件收口计划-2026-06-08.md](./technical/【前端架构】PlatformUiKit弹窗组件收口计划-2026-06-08.md)。 +平台 UI Kit 的提示 / 确认弹窗收口到 `src/components/common/UnifiedConfirmDialog.tsx`,复制反馈收口到 `src/components/common/useCopyFeedback.ts`、`src/components/common/CopyFeedbackButton.tsx`、`src/components/common/CopyCodeButton.tsx` 与 `src/components/common/CopyFeedbackMessage.tsx`,基础状态提示收口到 `src/components/common/PlatformStatusMessage.tsx`,运行态短错误 / 成功 / 反馈 toast 收口到 `src/components/common/PlatformRuntimeStatusToast.tsx`,平台空态 / 轻量加载态收口到 `src/components/common/PlatformEmptyState.tsx`,平台动作按钮收口到 `src/components/common/PlatformActionButton.tsx`,平台白底子面板 / 小型列表卡片收口到 `src/components/common/PlatformSubpanel.tsx`,平台媒体预览框收口到 `src/components/common/PlatformMediaFrame.tsx`,平台胶囊状态标签收口到 `src/components/common/PlatformPillBadge.tsx`,平台 / 个人中心弹窗关闭按钮收口到 `src/components/common/PlatformModalCloseButton.tsx`,底层继续复用 `UnifiedModal`;普通提示、确认 / 取消、危险确认、复制状态机、短代码复制 chip、复制按钮表现、白底 / 个人中心 / 认证入口 token 状态条、运行态状态 toast、无操作空态、主动作按钮、白底子面板、白底交互列表卡片、图片源 / fallback / 固定比例媒体预览、单个状态 / 标签 chip 和圆形关闭按钮优先使用公共 Module,规则见 [【前端架构】PlatformUiKit弹窗组件收口计划-2026-06-08.md](./technical/【前端架构】PlatformUiKit弹窗组件收口计划-2026-06-08.md)。 平台入口受保护数据失效后的 stage 去留判定,以及缺失草稿 / 作品 / run 时的阶段回退,收口到 `src/components/platform-entry/platformSelectionStageModel.ts`,壳层只执行缓存清空、布尔事实汇总和必要跳转,规则见 [【前端架构】PlatformSelectionStageModel收口计划-2026-06-04.md](./technical/【前端架构】PlatformSelectionStageModel收口计划-2026-06-04.md)。 diff --git a/docs/technical/【前端架构】PlatformUiKit弹窗组件收口计划-2026-06-08.md b/docs/technical/【前端架构】PlatformUiKit弹窗组件收口计划-2026-06-08.md index 4f60e7dd..5763532d 100644 --- a/docs/technical/【前端架构】PlatformUiKit弹窗组件收口计划-2026-06-08.md +++ b/docs/technical/【前端架构】PlatformUiKit弹窗组件收口计划-2026-06-08.md @@ -62,6 +62,7 @@ - `CopyCodeButton`:接收 `state`、`code`、`codeLabel`、`copiedSuffix`、`failedSuffix`、`codeClassName`、`suffixClassName`、`actionAppearance="pill"`、`actionPillTone`、`actionPillSize` 和复制按钮透传属性;作品号、用户号等短代码 chip 优先用它,不在业务 JSX 中重复写 `{code} + 已复制 / 复制失败` fragments,也不直接传 `platform-pill` class。 - `CopyFeedbackMessage`:接收 `state`、`copiedLabel` 和 `failedLabel`;toast 或行内状态只展示成功 / 失败时使用,不在业务页手写三态分支。 - `PlatformStatusMessage`:接收 `tone="error" | "success" | "info" | "warning" | "neutral"`、`surface="light" | "tinted" | "platform" | "profile" | "editorDark"`、`size="xs" | "sm" | "md"`、`remapSurface`、`children` 和 `className`;根节点固定带 `platform-status-message` 稳定类名,业务测试可断言公共状态条接入。局部可覆盖圆角、外边距和网格布局,但状态色值、基础内边距和字号由 Module 统一控制。结果页、发布检查、素材生成面板等需要复用旧 `platform-banner--danger / success / info / warning / neutral` token 外观时使用 `surface="platform"`;需要在局部 platform token 作用域内重映射 CSS 变量的提示条传 `remapSurface`,不在业务 JSX 手写 `platform-remap-surface platform-banner`。个人中心弹窗、认证入口、验证码提示、统一创作工作台和通用创作输入区需要 profile token 外观时使用 `surface="profile"`,RPG 暗色编辑 / 运行面板内的普通状态提示使用 `surface="editorDark"`;背包故事档案 QA、NPC 叙事提示、角色聊天错误提示、营地编组战斗中提示、自定义选择弹窗错误 / 生成中提示等暗色状态条已迁移。旧 `platform-profile-error` / `platform-profile-success`、暗色手写 `border-*-300/15 bg-*-500/10 text-*-50/90` 和 `platform-banner--danger / success / info / warning / neutral` 不再作为业务 JSX 接口。 +- `PlatformRuntimeStatusToast`:接收 `tone="error" | "success" | "info" | "warning" | "neutral"`、`surface="light" | "dark" | "solid"`、`size="xs" | "sm" | "md"`、`shape="pill" | "rounded"`、`children` 和 `className`;根节点固定带 `platform-runtime-status-toast` 稳定类名,默认按 `tone` 写入 `role="alert/status"` 与 `aria-live`。它只承接运行态 HUD 中短错误、成功和反馈 chip 的圆角、字号、阴影、色值和可访问语义,具体浮层位置、玩法资产按钮、计分牌、蓄力提示、强品牌 primary 按钮仍由玩法 runtime 控制。跳一跳、拼图、敲木鱼、方洞和宝贝爱画运行态的短错误 / 成功 / 投放反馈已先迁移;后续同类短 toast 不再手写 `rounded-full bg-white/* text-*`、暗色 `border-rose/emerald bg-*/text-*` 或单玩法 `*-runtime-error-chip`。 - `PlatformDarkOptionCard`:接收 `selected`、`tone="emerald" | "sky" | "rose" | "amber"`、`radius="sm" | "md" | "lg"`、`padding="sm" | "md" | "lg"`、`children`、`className` 和原生 button props;根节点固定带 `platform-dark-option-card` 稳定类名,统一承接 RPG 暗色弹窗 / 面板中的 selected / idle / hover / disabled 可选项卡按钮外观。NPC 交易模式、交易物品行、赠礼候选、招募替换候选、角色素材工作室动作预览格、营地编组替换位按钮和角色聊天建议按钮已先迁移;业务页只保留选中判断、点击回调和内容布局,不再重复手写 `rounded-* border px-3 py-*`、`border-*-400/* bg-*-500/10` 或 `border-white/* bg-black/20 hover:border-white/15`。 - `PlatformEmptyState`:接收 `surface="soft" | "dashed" | "subpanel" | "editorDark"`、`size="compact" | "panel" | "inline"`、`tone="base" | "soft"`、`children` 和 `className`;根节点固定带 `platform-empty-state` 稳定类名,业务测试可断言公共空态接入。`soft + compact` 用于公开广场、排行和作品架内的轻量空态,`soft + panel` 用于创作中心作品架整块空态,`dashed + panel` 用于素材选择、历史资源等弹窗的大面积空态或读取态,`subpanel + inline` 用于视觉小说 runtime 等白底子面板内的无操作空态,`editorDark + compact/inline` 用于 RPG 大编辑器、实体详情弹窗、营地编组、角色聊天和运行态设置弹窗等暗色面板里的纯展示空态 / 禁用提示。组件只承接外观,不内置业务文案。 - `PlatformTextField`:接收 `variant="input" | "textarea"`、`surface="platform" | "editorDark"`、`size="xs" | "sm" | "md" | "lg"`、`density="default" | "compact" | "roomy"`、`tone="warm" | "rose" | "emerald" | "sky"`、`className` 和原生 input / textarea props;统一承接平台白底与 RPG 暗色弹窗里的圆角输入框、文本域、禁用态、密度、字号 / 行高和焦点色,暗色 surface 根节点固定带 `platform-text-field--editor-dark` 稳定类名。`PlatformSelectField` 复用同一套输入 chrome 承接下拉框。业务页继续持有 `value`、`onChange`、`aria-label`、`rows`、`placeholder`、`option` 等语义,不再重复拼 `rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/86 px-3 py-3`、`rounded-[0.85rem] bg-white/90 px-3 py-2`、`bg-white/90 px-4 py-3`、暗色 `border-white/10 bg-black/30 px-4 py-3` 或 `focus:border-* focus:ring-*`。抓大鹅结果页作品信息、封面描述、素材名称和批量物品名称,方洞结果页主信息表单和形状 / 洞口选项字段,拼图 / 敲木鱼结果页作品信息字段,视觉小说结果页的音乐生成、作品信息、开场、运行配置、角色、场景、阶段和世界观普通文本 / 下拉字段,以及视觉小说 / 抓大鹅 / 汪汪声浪 / 宝贝识物 / 拼消消 / 跳一跳创作工作台普通输入字段已先迁移;自定义选择弹窗角色名字 / 背景补充 / 生成模式 / 世界描述和角色聊天草稿等暗色字段使用 `surface="editorDark"`。通用创作图片输入面板的提示词文本域也使用该 Module,只通过局部 class 保留高度和底部浮动上传按钮避让。 @@ -120,7 +121,7 @@ 12. RPG 场景背景和作品封面生成结果未保存时,退出确认也使用 `UnifiedConfirmDialog`;像素风场景生成弹窗通过 `variant="pixel"` 适配视觉。 13. 公开作品详情或运行态深链失效时,由平台入口壳展示 `UnifiedConfirmDialog` 的“作品不可用”提示;用户确认后再回到首页,错误处理分支不再调用浏览器原生 `window.alert`。 14. 带复杂内容的专用 Module 可以保留自己的布局,但复制反馈仍应复用 `useCopyFeedback`;如果有可点击复制按钮,优先复用 `CopyFeedbackButton`;如果只展示复制结果提示,优先复用 `CopyFeedbackMessage`。 -15. 白底平台弹窗、详情页、结果页、目录页、个人页、认证入口、统一创作工作台和通用创作输入区的基础错误 / 成功 / 信息 / 警告 / 中性状态提示逐步迁移到 `PlatformStatusMessage`;RPG 结果页、拼图结果页、抓大鹅结果页、跳一跳结果页、敲木鱼结果页、拼消消结果页、宝贝识物结果页、方洞结果页、汪汪声浪结果页、视觉小说结果页、拼消消创作工作台、宝贝识物创作工作台、视觉小说创作工作台、汪汪声浪创作工作台、creative-agent 工作台、creation-agent operation banner、自定义世界实体目录和拼消消 runtime 白底错误条已使用 `surface="platform"` 承接发布检查、错误提示、进度提示、素材生成提示、资源未就绪提示和主线目标提示;个人中心、认证入口、统一创作工作台和创作输入区需要 profile token 外观时使用 `surface="profile"`;RPG 暗色编辑 / 运行面板里的普通错误 / 成功 / 信息 / 警告 / 中性提示使用 `surface="editorDark"`,背包故事档案 QA 提示、NPC 交易 / 赠礼 / 招募叙事提示和角色聊天错误提示已先迁移。深色半透明游戏内提示和强品牌样式可以暂保留专用布局,避免状态条组件过早承接游戏视觉。 +15. 白底平台弹窗、详情页、结果页、目录页、个人页、认证入口、统一创作工作台和通用创作输入区的基础错误 / 成功 / 信息 / 警告 / 中性状态提示逐步迁移到 `PlatformStatusMessage`;RPG 结果页、拼图结果页、抓大鹅结果页、跳一跳结果页、敲木鱼结果页、拼消消结果页、宝贝识物结果页、方洞结果页、汪汪声浪结果页、视觉小说结果页、拼消消创作工作台、宝贝识物创作工作台、视觉小说创作工作台、汪汪声浪创作工作台、creative-agent 工作台、creation-agent operation banner、自定义世界实体目录和拼消消 runtime 白底错误条已使用 `surface="platform"` 承接发布检查、错误提示、进度提示、素材生成提示、资源未就绪提示和主线目标提示;个人中心、认证入口、统一创作工作台和创作输入区需要 profile token 外观时使用 `surface="profile"`;RPG 暗色编辑 / 运行面板里的普通错误 / 成功 / 信息 / 警告 / 中性提示使用 `surface="editorDark"`,背包故事档案 QA 提示、NPC 交易 / 赠礼 / 招募叙事提示和角色聊天错误提示已先迁移。运行态里的短错误 / 成功 / 命中反馈 chip 使用 `PlatformRuntimeStatusToast`,位置和玩法强品牌 HUD 仍留在 runtime 壳层;深色半透明游戏内提示和强品牌样式可以暂保留专用布局,避免状态条组件过早承接游戏视觉。 16. 正方形图片裁剪的初始居中、边界 clamp 和裁剪矩形类型统一从 `squareImageCropModel` 导入,避免头像裁剪、拼图参考图裁剪等业务页面依赖弹窗组件文件里的 helper。 17. 个人中心的账户充值、泥点账单、每日任务、兑换码、扫码、存档、玩过作品、邀请 / 社区、昵称修改、头像裁剪,以及平台筛选、创作图片预览、认证入口、邀请码弹窗、公开编号搜索结果弹窗、方洞结果页图片素材弹窗、视觉小说结果页资产 / 音频 / 编辑器弹窗、视觉小说 runtime 普通面板、creative-agent 模板确认弹窗和自定义选择弹窗等圆形关闭按钮迁移到 `PlatformModalCloseButton`;后续新增弹窗关闭按钮先判断是否属于 `profile`、`profileCompact`、`floating`、`floatingPlain`、`platformIcon` 或 `editorDark` 六类,确有品牌化或运行态 HUD 语义时才保留专用按钮。 17.1. 平台弹窗 header 和普通工具栏里的 `platform-icon-button` 迁移到 `PlatformIconButton`;历史图片选择弹窗、RPG 发布检查弹窗、creative-agent 侧边栏关闭 / 外观 / 设置入口、通用输入 Composer 上传 / 发送 / 移除参考图、creation-agent 参考图移除、敲木鱼结果页新增主题标签入口、敲木鱼创作工作台功德词条删除入口、拼图结果页标签生成 / 标签新增 / 关卡详情关闭 / 发布弹窗关闭 / 删除关卡入口、视觉小说结果页素材选择 / 音频生成 / 保存草稿 / 运行配置入口、RPG 首页搜索结果清空入口、方洞结果页形状 / 洞口选项删除入口,以及抓大鹅结果页标签生成 / 标签新增 / 物品素材删除 / 参考图上传入口已先迁移。结果页内的普通平台弹窗关闭入口使用 `PlatformModalCloseButton variant="platformIcon"`;图标上传控件使用 `PlatformIconButton asChild="label"` 保留 label + file input 语义,不改成普通按钮;`PlatformIconButton` 的 label 模式会自动写入隐藏文本,保证内嵌 file input 仍能继承可访问名称。通用创作图片面板中覆盖在图片上的更换主图、移除主图、历史入口短标签按钮和提示词参考图上传入口,抓大鹅封面编辑中覆盖在封面图上的移除入口,以及敲木鱼创作工作台功德词条删除入口使用 `PlatformIconButton variant="surfaceFloating"`,不再手写白底圆形 / 短标签浮动按钮 chrome。运行态 HUD、带复制状态或需要专用交互禁用语义的图标按钮,先保留专用布局,等对应场景验证时再迁移。 @@ -197,6 +198,7 @@ - `npm run test -- src/components/common/PlatformPillBadge.test.tsx` - `npm run test -- src/components/common/CopyFeedbackMessage.test.tsx` - `npm run test -- src/components/common/PlatformStatusMessage.test.tsx` +- `npm run test -- src/components/common/PlatformRuntimeStatusToast.test.tsx src/components/jump-hop-runtime/JumpHopRuntimeShell.test.tsx src/components/puzzle-runtime/PuzzleRuntimeShell.test.tsx src/components/wooden-fish-runtime/WoodenFishRuntimeShell.test.tsx src/components/square-hole-runtime/SquareHoleRuntimeShell.test.tsx src/components/edutainment-runtime/BabyLoveDrawingRuntimeShell.test.tsx` - `npm run test -- src/components/CharacterChatModal.test.tsx src/components/common/PlatformStatusMessage.test.tsx src/components/common/PlatformSubpanel.test.tsx src/components/common/PlatformEmptyState.test.tsx src/components/common/PlatformDarkOptionCard.test.tsx` - `npm run test -- src/components/CompanionCampModal.test.tsx src/components/common/PlatformStatusMessage.test.tsx src/components/common/PlatformSubpanel.test.tsx src/components/common/PlatformPillBadge.test.tsx src/components/common/PlatformEmptyState.test.tsx src/components/common/PlatformDarkOptionCard.test.tsx` - `npm run test -- src/components/CompanionCampModal.test.tsx src/components/common/PlatformMediaFrame.test.tsx` diff --git a/src/components/common/PlatformRuntimeStatusToast.test.tsx b/src/components/common/PlatformRuntimeStatusToast.test.tsx new file mode 100644 index 00000000..b871c75b --- /dev/null +++ b/src/components/common/PlatformRuntimeStatusToast.test.tsx @@ -0,0 +1,46 @@ +/* @vitest-environment jsdom */ + +import { render, screen } from '@testing-library/react'; +import { expect, test } from 'vitest'; + +import { PlatformRuntimeStatusToast } from './PlatformRuntimeStatusToast'; + +test('renders error runtime toast with alert semantics', () => { + render( + + 操作失败 + , + ); + + const toast = screen.getByRole('alert'); + + expect(toast.className).toContain('platform-runtime-status-toast'); + expect(toast.className).toContain('rounded-full'); + expect(toast.className).toContain( + 'text-[var(--platform-button-danger-text)]', + ); + expect(toast.className).toContain('mt-2'); + expect(toast.getAttribute('aria-live')).toBe('assertive'); +}); + +test('supports dark and solid runtime surfaces', () => { + const { rerender } = render( + + 命中 + , + ); + + expect(screen.getByRole('status').className).toContain( + 'border-emerald-200/35', + ); + expect(screen.getByRole('status').className).toContain('rounded-[1.2rem]'); + + rerender( + + 运行失败 + , + ); + + expect(screen.getByRole('alert').className).toContain('bg-rose-600'); + expect(screen.getByRole('alert').className).toContain('px-4'); +}); diff --git a/src/components/common/PlatformRuntimeStatusToast.tsx b/src/components/common/PlatformRuntimeStatusToast.tsx new file mode 100644 index 00000000..0232c96c --- /dev/null +++ b/src/components/common/PlatformRuntimeStatusToast.tsx @@ -0,0 +1,104 @@ +import type { HTMLAttributes, ReactNode } from 'react'; + +type PlatformRuntimeStatusTone = + | 'error' + | 'success' + | 'info' + | 'warning' + | 'neutral'; +type PlatformRuntimeStatusSurface = 'light' | 'dark' | 'solid'; +type PlatformRuntimeStatusSize = 'xs' | 'sm' | 'md'; +type PlatformRuntimeStatusShape = 'pill' | 'rounded'; + +type PlatformRuntimeStatusToastProps = Omit< + HTMLAttributes, + 'children' +> & { + tone: PlatformRuntimeStatusTone; + surface?: PlatformRuntimeStatusSurface; + size?: PlatformRuntimeStatusSize; + shape?: PlatformRuntimeStatusShape; + children: ReactNode; +}; + +const PLATFORM_RUNTIME_STATUS_SURFACE_CLASS: Record< + PlatformRuntimeStatusSurface, + Record +> = { + light: { + error: + 'border-white/70 bg-white/86 text-[var(--platform-button-danger-text)]', + success: + 'border-white/70 bg-white/86 text-[var(--platform-success-text)]', + info: 'border-white/70 bg-white/86 text-[var(--platform-cool-text)]', + warning: 'border-white/70 bg-white/86 text-[var(--platform-warm-text)]', + neutral: 'border-white/70 bg-white/86 text-[var(--platform-text-base)]', + }, + dark: { + error: 'border-rose-200/35 bg-rose-400/18 text-rose-50', + success: 'border-emerald-200/35 bg-emerald-400/18 text-emerald-50', + info: 'border-sky-200/30 bg-sky-400/16 text-sky-50', + warning: 'border-amber-200/35 bg-amber-400/18 text-amber-50', + neutral: 'border-white/15 bg-white/12 text-white/82', + }, + solid: { + error: 'border-transparent bg-rose-600 text-white', + success: 'border-transparent bg-emerald-600 text-white', + info: 'border-transparent bg-sky-600 text-white', + warning: 'border-transparent bg-amber-500 text-amber-950', + neutral: 'border-transparent bg-slate-800 text-white', + }, +}; + +const PLATFORM_RUNTIME_STATUS_SIZE_CLASS: Record< + PlatformRuntimeStatusSize, + string +> = { + xs: 'px-3 py-1 text-xs leading-5', + sm: 'px-3 py-2 text-sm leading-5', + md: 'px-4 py-3 text-sm leading-6', +}; + +const PLATFORM_RUNTIME_STATUS_SHAPE_CLASS: Record< + PlatformRuntimeStatusShape, + string +> = { + pill: 'rounded-full', + rounded: 'rounded-[1.2rem]', +}; + +/** + * 运行态 HUD 状态提示。 + * 收口游戏内短错误、成功和反馈 chip,位置与玩法强品牌视觉仍由调用方控制。 + */ +export function PlatformRuntimeStatusToast({ + tone, + surface = 'light', + size = 'sm', + shape = 'pill', + children, + className, + role, + ...divProps +}: PlatformRuntimeStatusToastProps) { + const ariaLive = divProps['aria-live']; + + return ( +
+ {children} +
+ ); +} diff --git a/src/components/edutainment-runtime/BabyLoveDrawingRuntimeShell.test.tsx b/src/components/edutainment-runtime/BabyLoveDrawingRuntimeShell.test.tsx index 6e45230f..3d67c3da 100644 --- a/src/components/edutainment-runtime/BabyLoveDrawingRuntimeShell.test.tsx +++ b/src/components/edutainment-runtime/BabyLoveDrawingRuntimeShell.test.tsx @@ -117,7 +117,11 @@ test('finish then save stores original drawing in local demo service', () => { magicImageSrc: null, }), ); - expect(screen.getByText('已保存')).toBeTruthy(); + const savedToast = screen.getByText('已保存'); + expect(savedToast.className).toContain('platform-runtime-status-toast'); + expect(savedToast.className).toContain( + 'baby-love-drawing-runtime__status--saved', + ); }); test('back button calls onBack callback', () => { diff --git a/src/components/edutainment-runtime/BabyLoveDrawingRuntimeShell.tsx b/src/components/edutainment-runtime/BabyLoveDrawingRuntimeShell.tsx index f288725a..37019c93 100644 --- a/src/components/edutainment-runtime/BabyLoveDrawingRuntimeShell.tsx +++ b/src/components/edutainment-runtime/BabyLoveDrawingRuntimeShell.tsx @@ -31,6 +31,7 @@ import { } from '../../services/edutainment-baby-drawing'; import type { MocapHandInput } from '../../services/useMocapInput'; import { useMocapInput } from '../../services/useMocapInput'; +import { PlatformRuntimeStatusToast } from '../common/PlatformRuntimeStatusToast'; import { appendPointToStroke, BABY_LOVE_DRAWING_BRUSH_SIZE, @@ -884,14 +885,23 @@ export function BabyLoveDrawingRuntimeShell({ {error ? ( -
{error}
+ + {error} + ) : null} {savedRecord ? ( -
+ 已保存 -
+ ) : null} {leftHandPoint ? ( diff --git a/src/components/jump-hop-runtime/JumpHopRuntimeShell.test.tsx b/src/components/jump-hop-runtime/JumpHopRuntimeShell.test.tsx index 3d5e3c68..61734a3d 100644 --- a/src/components/jump-hop-runtime/JumpHopRuntimeShell.test.tsx +++ b/src/components/jump-hop-runtime/JumpHopRuntimeShell.test.tsx @@ -10,10 +10,10 @@ import type { import { useResolvedAssetReadUrl } from '../../hooks/useResolvedAssetReadUrl'; import { useJumpHopLeaderboard } from '../../services/jump-hop/useJumpHopLeaderboard'; import { - JUMP_HOP_THREE_CAMERA_UP_Y, - JumpHopRuntimeShell, getJumpHopThreeProjectedY, getJumpHopTileTextureSignature, + JUMP_HOP_THREE_CAMERA_UP_Y, + JumpHopRuntimeShell, } from './JumpHopRuntimeShell'; vi.mock('../../hooks/useResolvedAssetReadUrl', () => ({ @@ -360,6 +360,23 @@ test('跳一跳运行态背景和游戏舞台覆盖全部界面且 HUD 使用独 expect(scoreCard.className).toContain('text-center'); }); +test('跳一跳运行态错误提示使用公共运行态 toast', () => { + render( + {}} + />, + ); + + const toast = screen.getByRole('alert'); + + expect(toast.textContent).toBe('跳跃失败'); + expect(toast.className).toContain('platform-runtime-status-toast'); +}); + test('跳一跳运行态失败后在弹窗中展示排行榜', () => { const runtimeRequestOptions = { runtimeGuestToken: 'runtime-guest-token', diff --git a/src/components/jump-hop-runtime/JumpHopRuntimeShell.tsx b/src/components/jump-hop-runtime/JumpHopRuntimeShell.tsx index 2c2eb1a2..138bb50f 100644 --- a/src/components/jump-hop-runtime/JumpHopRuntimeShell.tsx +++ b/src/components/jump-hop-runtime/JumpHopRuntimeShell.tsx @@ -14,8 +14,8 @@ import { import jumpHopRuntimeLevelLogo from '../../../media/logo.png'; import type { JumpHopRuntimeRunSnapshotResponse, - JumpHopTileFaceAsset, JumpHopTileAsset, + JumpHopTileFaceAsset, JumpHopWorkProfileResponse, } from '../../../packages/shared/src/contracts/jumpHop'; import { useResolvedAssetReadUrl } from '../../hooks/useResolvedAssetReadUrl'; @@ -27,9 +27,9 @@ import type { JumpHopRuntimeRequestOptions } from '../../services/jump-hop/jumpH import { buildJumpHopVisiblePlatforms, formatJumpHopDurationLabel, + getJumpHopBackendDragVector, getJumpHopCharacterVisualPosition, getJumpHopJumpFeedbackLabel, - getJumpHopBackendDragVector, getJumpHopLandingAssistVisualPosition, getJumpHopPlatformVisualSize, getJumpHopRunDurationMs, @@ -42,6 +42,7 @@ import { } from '../../services/jump-hop/jumpHopRuntimeModel'; import { useJumpHopLeaderboard } from '../../services/jump-hop/useJumpHopLeaderboard'; import { PlatformActionButton } from '../common/PlatformActionButton'; +import { PlatformRuntimeStatusToast } from '../common/PlatformRuntimeStatusToast'; import { RuntimeResourcePendingMarker } from '../common/RuntimeResourcePendingMarker'; type JumpHopRuntimeJumpPayload = { @@ -2374,9 +2375,9 @@ export function JumpHopRuntimeShell({ {error ? (
-
+ {error} -
+
) : null} diff --git a/src/components/puzzle-runtime/PuzzleRuntimeShell.test.tsx b/src/components/puzzle-runtime/PuzzleRuntimeShell.test.tsx index a12fe475..f2996dd0 100644 --- a/src/components/puzzle-runtime/PuzzleRuntimeShell.test.tsx +++ b/src/components/puzzle-runtime/PuzzleRuntimeShell.test.tsx @@ -200,6 +200,30 @@ test('拼图界面不调用 mocap,也不渲染 mocap 光标或调试面板', ( expect(screen.queryByTestId('puzzle-mocap-cursor')).toBeNull(); }); +test('拼图运行态错误提示使用公共运行态 toast', () => { + renderPuzzleRuntime( + , + ); + + const toast = screen.getByRole('alert'); + + expect(toast.textContent).toBe('交换失败'); + expect(toast.className).toContain('platform-runtime-status-toast'); +}); + test('指针拖拽时会触发拖拽提交并在松开时落子', () => { const originalRequestAnimationFrame = window.requestAnimationFrame; const onDragPiece = vi.fn(); diff --git a/src/components/puzzle-runtime/PuzzleRuntimeShell.tsx b/src/components/puzzle-runtime/PuzzleRuntimeShell.tsx index cfbd941d..155cc408 100644 --- a/src/components/puzzle-runtime/PuzzleRuntimeShell.tsx +++ b/src/components/puzzle-runtime/PuzzleRuntimeShell.tsx @@ -55,6 +55,7 @@ import { resolveRuntimeCountdownSecondBucket, } from '../../services/runtimeAudioFeedback'; import { useAuthUi } from '../auth/AuthUiContext'; +import { PlatformRuntimeStatusToast } from '../common/PlatformRuntimeStatusToast'; import { RuntimeResourcePendingMarker } from '../common/RuntimeResourcePendingMarker'; import { ResolvedAssetImage } from '../ResolvedAssetImage'; import { @@ -1987,9 +1988,9 @@ export function PuzzleRuntimeShell({
{error ? ( -
+ {error} -
+ ) : null} {selectedPieceId && shouldDisplaySelectedState && @@ -2202,9 +2203,14 @@ export function PuzzleRuntimeShell({
消耗 1 泥点 {propConfirmError ? ( -
+ {propConfirmError} -
+ ) : null}
diff --git a/src/components/square-hole-runtime/SquareHoleRuntimeShell.test.tsx b/src/components/square-hole-runtime/SquareHoleRuntimeShell.test.tsx index e51dd9af..c125d4c2 100644 --- a/src/components/square-hole-runtime/SquareHoleRuntimeShell.test.tsx +++ b/src/components/square-hole-runtime/SquareHoleRuntimeShell.test.tsx @@ -103,6 +103,25 @@ test('点击洞口会提交该洞口选择', async () => { }); }); +test('错误提示使用公共运行态 toast', () => { + const run = buildRun(); + + render( + , + ); + + const toast = screen.getByRole('alert'); + + expect(toast.textContent).toBe('投放失败'); + expect(toast.className).toContain('platform-runtime-status-toast'); +}); + test('引导高亮不会默认指向当前正确洞口', () => { renderRuntime(); diff --git a/src/components/square-hole-runtime/SquareHoleRuntimeShell.tsx b/src/components/square-hole-runtime/SquareHoleRuntimeShell.tsx index 53043fe7..6629ba2f 100644 --- a/src/components/square-hole-runtime/SquareHoleRuntimeShell.tsx +++ b/src/components/square-hole-runtime/SquareHoleRuntimeShell.tsx @@ -23,6 +23,7 @@ import type { SquareHoleDropResponse, SquareHoleRunSnapshot, } from '../../../packages/shared/src/contracts/squareHoleRuntime'; +import { PlatformRuntimeStatusToast } from '../common/PlatformRuntimeStatusToast'; import { ResolvedAssetImage } from '../ResolvedAssetImage'; type SquareHoleRuntimeShellProps = { @@ -531,19 +532,23 @@ export function SquareHoleRuntimeShell({
{feedback ? ( -
{feedback.message} -
+ ) : dropError || error ? ( -
+ {dropError ?? error} -
+ ) : null}
diff --git a/src/components/wooden-fish-runtime/WoodenFishRuntimeShell.test.tsx b/src/components/wooden-fish-runtime/WoodenFishRuntimeShell.test.tsx index 8c05a8cc..fd67fda7 100644 --- a/src/components/wooden-fish-runtime/WoodenFishRuntimeShell.test.tsx +++ b/src/components/wooden-fish-runtime/WoodenFishRuntimeShell.test.tsx @@ -195,6 +195,22 @@ test('木鱼运行态使用生成的主题返回按钮图', () => { expect(backButton.className).toContain('w-10'); }); +test('木鱼运行态错误提示使用公共运行态 toast', () => { + render( + , + ); + + const toast = screen.getByRole('alert'); + + expect(toast.textContent).toBe('同步失败'); + expect(toast.className).toContain('platform-runtime-status-toast'); + expect(toast.getAttribute('data-wooden-fish-functional')).toBe('true'); +}); + test('木鱼运行态飘字去掉底板并放大字号', () => { const { container } = render( , diff --git a/src/components/wooden-fish-runtime/WoodenFishRuntimeShell.tsx b/src/components/wooden-fish-runtime/WoodenFishRuntimeShell.tsx index 732e7548..b87cc8ff 100644 --- a/src/components/wooden-fish-runtime/WoodenFishRuntimeShell.tsx +++ b/src/components/wooden-fish-runtime/WoodenFishRuntimeShell.tsx @@ -20,6 +20,7 @@ import { WOODEN_FISH_DEFAULT_HIT_SOUND_ASSET, } from '../../services/wooden-fish/woodenFishDefaults'; import { PlatformActionButton } from '../common/PlatformActionButton'; +import { PlatformRuntimeStatusToast } from '../common/PlatformRuntimeStatusToast'; import { RuntimeResourcePendingMarker } from '../common/RuntimeResourcePendingMarker'; import { ResolvedAssetImage } from '../ResolvedAssetImage'; import { @@ -410,12 +411,16 @@ export function WoodenFishRuntimeShell({ {error ? ( -
{error} -
+ ) : null}