继续收口平台空态与动作按钮
作品架异步状态切换复用 PlatformAsyncStatePanel 复制反馈动作外观改为组合 PlatformActionButton 结果页与调试面板空态继续收口到 PlatformEmptyState 暗色私聊与工坊按钮改为复用 PlatformActionButton 更新 PlatformUiKit 收口文档与团队决策记录
This commit is contained in:
@@ -2075,7 +2075,10 @@
|
|||||||
- 决策:简单泥点确认流的开关状态机统一收口到 `src/components/common/useMudPointConfirmController.ts`,只暴露 `open / requestOpen / close / confirm`,不持有点数、标题、描述或禁用态等业务字段;`PuzzleCreationWorkspace.tsx`、`Match3DCreationWorkspace.tsx` 与 `Match3DResultView.tsx` 的两个批量素材面板已接入。`PuzzleResultView.tsx` 和 `RpgCreationRoleAssetStudioModalImpl.tsx` 这类节奏不同或携带 pending payload 的场景继续保留本地状态机,避免把简单 hook 扩成泛型动作路由器。
|
- 决策:简单泥点确认流的开关状态机统一收口到 `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"`;`PuzzleResultView.tsx` 的关卡详情 / 发布弹窗、`RpgCreationResultActionBar.tsx` 的发布检查弹窗,以及 `PuzzleHistoryAssetPickerDialog.tsx` 的历史素材弹窗已迁移。像素风 runtime、drawer collapse、玩法规则面板和运行态 overlay 不跟这条线混收,继续保留局部 close 语义。
|
- 决策:标准平台 modal header 的关闭入口继续统一到 `PlatformModalCloseButton variant="platformIcon"`;`PuzzleResultView.tsx` 的关卡详情 / 发布弹窗、`RpgCreationResultActionBar.tsx` 的发布检查弹窗,以及 `PuzzleHistoryAssetPickerDialog.tsx` 的历史素材弹窗已迁移。像素风 runtime、drawer collapse、玩法规则面板和运行态 overlay 不跟这条线混收,继续保留局部 close 语义。
|
||||||
- 决策:平台入口的创作前置泥点阻断提示只在 `platform-entry` 局部抽成 `src/components/platform-entry/PlatformDraftGenerationPointNoticeDialog.tsx`,并使用 `DraftGenerationPointNotice` union(`insufficient-points` / `balance-load-failed`)承接业务真相;不要在 `common/` 再抽一个泛化 `BlockingNoticeDialog`,否则会把 `PlatformAcknowledgeStatusDialog` 的样式透传再包装一层而不缩小调用面。
|
- 决策:平台入口的创作前置泥点阻断提示只在 `platform-entry` 局部抽成 `src/components/platform-entry/PlatformDraftGenerationPointNoticeDialog.tsx`,并使用 `DraftGenerationPointNotice` union(`insufficient-points` / `balance-load-failed`)承接业务真相;不要在 `common/` 再抽一个泛化 `BlockingNoticeDialog`,否则会把 `PlatformAcknowledgeStatusDialog` 的样式透传再包装一层而不缩小调用面。
|
||||||
- 验证方式:`npm run test -- src/components/common/PlatformAsyncStatePanel.test.tsx src/components/platform-entry/PlatformProfileReferralModal.test.tsx src/components/platform-entry/PlatformProfileWalletLedgerModal.test.tsx src/components/platform-entry/PlatformProfilePlayedWorksModal.test.tsx src/components/platform-entry/PlatformProfileTaskCenterModal.test.tsx src/components/platform-entry/PlatformProfileRechargeModal.test.tsx src/components/common/PlatformSegmentedTabs.test.tsx src/components/custom-world-home/CustomWorldCreationHub.test.tsx src/components/custom-world-home/CustomWorldCreationHub.interaction.test.tsx`、`npm run test -- src/components/common/PlatformModalCloseButton.test.tsx src/components/PixelCloseButton.test.tsx src/components/CharacterChatModal.test.tsx src/components/MapModal.test.tsx`、`npm run test -- src/components/common/useMudPointConfirmController.test.tsx src/components/match3d-result/Match3DResultView.test.tsx src/components/unified-creation/workspaces/PuzzleCreationWorkspace.interaction.test.tsx src/components/unified-creation/workspaces/Match3DCreationWorkspace.interaction.test.tsx src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx src/components/platform-entry/PlatformProfileRechargeModal.test.tsx src/components/CustomWorldEntityEditorModal.test.tsx src/components/rpg-creation-result/RpgCreationResultActionBar.test.tsx src/components/unified-creation/shared/PuzzleHistoryAssetPickerDialog.test.tsx src/components/puzzle-result/PuzzleResultView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`。
|
- 决策:`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。
|
||||||
|
- 决策:白底 / 暗色面板里的轻量空态和普通 CTA 继续向共享组件收口。`PuzzleResultView.tsx` 的缺草稿提示、`RpgCreationAssetDebugPanel.tsx` 的空诊断提示改为 `PlatformEmptyState`,`Match3DResultView.tsx` 的引用素材列表直接复用 `PlatformAssetPickerGrid` 自己的空态;`AdventureEntityModal.tsx` 的私聊按钮和 `InventoryPanel.tsx` 的锻造 / 合成按钮改为 `PlatformActionButton surface="editorDark"`,业务页只贴局部 sky / emerald 皮肤。后续白底子面板里的只读空态优先使用 `PlatformEmptyState surface="subpanel" size="inline"`,暗色编辑 / 运行面板里的普通动作优先使用 `PlatformActionButton surface="editorDark"`。
|
||||||
|
- 验证方式:`npm run test -- src/components/common/PlatformAsyncStatePanel.test.tsx src/components/platform-entry/PlatformProfileReferralModal.test.tsx src/components/platform-entry/PlatformProfileWalletLedgerModal.test.tsx src/components/platform-entry/PlatformProfilePlayedWorksModal.test.tsx src/components/platform-entry/PlatformProfileTaskCenterModal.test.tsx src/components/platform-entry/PlatformProfileRechargeModal.test.tsx src/components/common/PlatformSegmentedTabs.test.tsx src/components/custom-world-home/CustomWorldCreationHub.test.tsx src/components/custom-world-home/CustomWorldCreationHub.interaction.test.tsx`、`npm run test -- src/components/common/PlatformModalCloseButton.test.tsx src/components/PixelCloseButton.test.tsx src/components/CharacterChatModal.test.tsx src/components/MapModal.test.tsx`、`npm run test -- src/components/common/useMudPointConfirmController.test.tsx src/components/match3d-result/Match3DResultView.test.tsx src/components/unified-creation/workspaces/PuzzleCreationWorkspace.interaction.test.tsx src/components/unified-creation/workspaces/Match3DCreationWorkspace.interaction.test.tsx src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx src/components/platform-entry/PlatformProfileRechargeModal.test.tsx src/components/CustomWorldEntityEditorModal.test.tsx src/components/rpg-creation-result/RpgCreationResultActionBar.test.tsx src/components/unified-creation/shared/PuzzleHistoryAssetPickerDialog.test.tsx src/components/puzzle-result/PuzzleResultView.test.tsx`、`npm run test -- src/components/common/CopyFeedbackButton.test.tsx src/components/common/PlatformActionButton.test.tsx src/components/AdventureEntityModal.test.tsx src/components/InventoryPanel.test.tsx src/components/rpg-creation-result/RpgCreationAssetDebugPanel.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`。
|
||||||
|
|
||||||
## 2026-05-26 敲木鱼发布后作品架与推荐流刷新口径
|
## 2026-05-26 敲木鱼发布后作品架与推荐流刷新口径
|
||||||
|
|
||||||
|
|||||||
@@ -254,6 +254,9 @@
|
|||||||
19.3.30. `PlatformSegmentedTabs` 继续承接首页 / 结果页里剩余的横向 rail 与二选一切换:`RpgEntryHomeView.tsx` 的 discover channel bar、移动端 / 桌面端分类 chip rail,`CustomWorldEntityCatalog.tsx` 的 `RESULT_TABS` sticky rail,以及 `PlatformProfileRechargeModal.tsx` 的“泥点充值 / 会员卡”切换条已迁移。`CustomWorldEntityCatalog` 通过 `ReactNode label` 保留“标题 + count”两行内容,`RpgEntryHomeView` 和个人中心切换条继续通过 `itemClassName` 贴回本地皮肤;同类 rail 优先直接复用 `PlatformSegmentedTabs`,测试也应按 `role="tablist" / "tab"` 查询,不再把这些切换项当普通 button。
|
19.3.30. `PlatformSegmentedTabs` 继续承接首页 / 结果页里剩余的横向 rail 与二选一切换:`RpgEntryHomeView.tsx` 的 discover channel bar、移动端 / 桌面端分类 chip rail,`CustomWorldEntityCatalog.tsx` 的 `RESULT_TABS` sticky rail,以及 `PlatformProfileRechargeModal.tsx` 的“泥点充值 / 会员卡”切换条已迁移。`CustomWorldEntityCatalog` 通过 `ReactNode label` 保留“标题 + count”两行内容,`RpgEntryHomeView` 和个人中心切换条继续通过 `itemClassName` 贴回本地皮肤;同类 rail 优先直接复用 `PlatformSegmentedTabs`,测试也应按 `role="tablist" / "tab"` 查询,不再把这些切换项当普通 button。
|
||||||
19.3.31. 简单泥点确认流的开关状态机收口到 `src/components/common/useMudPointConfirmController.ts`;该 hook 只承接 `open / requestOpen / close / confirm` 四个动作,`confirm` 固定先关弹窗再执行回调,不持有 `points / title / description / confirmDisabled` 之类业务字段。`PuzzleCreationWorkspace.tsx`、`Match3DCreationWorkspace.tsx` 和 `Match3DResultView.tsx` 的两个批量素材面板已接入;`PuzzleCreationWorkspace` 仍在业务页判断“只有 `aiRedraw` 才弹确认”。`PuzzleResultView.tsx` 与 `RpgCreationRoleAssetStudioModalImpl.tsx` 这类要么节奏不同、要么携带 pending payload 的场景先保留本地状态机,不把 hook 扩成泛型动作路由器。
|
19.3.31. 简单泥点确认流的开关状态机收口到 `src/components/common/useMudPointConfirmController.ts`;该 hook 只承接 `open / requestOpen / close / confirm` 四个动作,`confirm` 固定先关弹窗再执行回调,不持有 `points / title / description / confirmDisabled` 之类业务字段。`PuzzleCreationWorkspace.tsx`、`Match3DCreationWorkspace.tsx` 和 `Match3DResultView.tsx` 的两个批量素材面板已接入;`PuzzleCreationWorkspace` 仍在业务页判断“只有 `aiRedraw` 才弹确认”。`PuzzleResultView.tsx` 与 `RpgCreationRoleAssetStudioModalImpl.tsx` 这类要么节奏不同、要么携带 pending payload 的场景先保留本地状态机,不把 hook 扩成泛型动作路由器。
|
||||||
19.3.32. 标准平台 modal header 的关闭入口继续统一到 `PlatformModalCloseButton variant="platformIcon"`;`PuzzleResultView.tsx` 的关卡详情 / 发布弹窗、`RpgCreationResultActionBar.tsx` 的发布检查弹窗,以及 `PuzzleHistoryAssetPickerDialog.tsx` 的历史素材弹窗已迁移。像素风 runtime、drawer collapse、玩法规则面板和运行态专属 overlay 继续保留本地 close 语义,不把 `PlatformModalCloseButton` 硬塞进非平台 modal header 场景。
|
19.3.32. 标准平台 modal header 的关闭入口继续统一到 `PlatformModalCloseButton variant="platformIcon"`;`PuzzleResultView.tsx` 的关卡详情 / 发布弹窗、`RpgCreationResultActionBar.tsx` 的发布检查弹窗,以及 `PuzzleHistoryAssetPickerDialog.tsx` 的历史素材弹窗已迁移。像素风 runtime、drawer collapse、玩法规则面板和运行态专属 overlay 继续保留本地 close 语义,不把 `PlatformModalCloseButton` 硬塞进非平台 modal header 场景。
|
||||||
|
19.3.33. `PlatformAsyncStatePanel` 从 profile modal 扩展到作品架:`CustomWorldCreationHub.tsx` 的作品架主体现在也统一通过 `loadingState / emptyState / children` 三个 slot 切换,保留外层 error + 重试提示不并入共享状态骨架。后续白底作品架或列表 panel 若只是互斥的 `loading / empty / content`,优先直接复用 `PlatformAsyncStatePanel`,不要再在业务 JSX 中重复拼 skeleton 和“当前筛选下没有内容”的分支。验证命令:`npx vitest run src/components/custom-world-home/CustomWorldCreationHub.test.tsx src/components/custom-world-home/CustomWorldCreationHub.interaction.test.tsx`、`npm run check:encoding`。
|
||||||
|
19.3.34. `CopyFeedbackButton.tsx` 的 `actionSurface` 分支继续向共享按钮收口:带平台动作外观的复制按钮现在直接组合 `PlatformActionButton`,仅保留 `pill` 分支继续复用 `PlatformPillBadge` 风格。复制反馈按钮不再手动调用 `getPlatformActionButtonClassName` 拼平台按钮基础 chrome;后续同类“复制状态机 + 平台动作按钮”组合也优先走 `CopyFeedbackButton + PlatformActionButton`,不要在业务页或按钮组件里重新混写图标、文案、aria 和 class。验证命令:`npm run test -- src/components/common/CopyFeedbackButton.test.tsx src/components/common/PlatformActionButton.test.tsx`。
|
||||||
|
19.3.35. 白底 / 暗色面板里的轻量空态和普通 CTA 继续按共享组件收口:`PuzzleResultView.tsx` 的“还没有可编辑的拼图草稿”和 `RpgCreationAssetDebugPanel.tsx` 的“没有可诊断项”改为 `PlatformEmptyState`,`Match3DResultView.tsx` 的引用素材列表直接交给 `PlatformAssetPickerGrid` 自己处理空态;`AdventureEntityModal.tsx` 的私聊按钮与 `InventoryPanel.tsx` 的锻造 / 合成按钮改为 `PlatformActionButton surface="editorDark"`,业务页只贴回 sky / emerald 局部皮肤。后续白底子面板里的只读空态优先使用 `PlatformEmptyState surface="subpanel" size="inline"`,暗色编辑 / 运行面板里的普通动作优先使用 `PlatformActionButton surface="editorDark"`,不要再手写基础按钮壳或空态面板 chrome。验证命令:`npm run test -- src/components/puzzle-result/PuzzleResultView.test.tsx src/components/rpg-creation-result/RpgCreationAssetDebugPanel.test.tsx src/components/AdventureEntityModal.test.tsx src/components/InventoryPanel.test.tsx`、`npm run check:encoding`。
|
||||||
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`。
|
||||||
|
|||||||
@@ -397,6 +397,7 @@ test('私聊和队友收束复用暗色 tint PlatformSubpanel chrome', () => {
|
|||||||
const companionResolutionEcho = screen.getByTestId(
|
const companionResolutionEcho = screen.getByTestId(
|
||||||
'companion-resolution-echo',
|
'companion-resolution-echo',
|
||||||
);
|
);
|
||||||
|
const privateChatButton = screen.getByRole('button', { name: '聊天' });
|
||||||
|
|
||||||
expect(privateChatPanel.className).toContain('border-sky-400/18');
|
expect(privateChatPanel.className).toContain('border-sky-400/18');
|
||||||
expect(privateChatPanel.className).toContain('bg-sky-500/8');
|
expect(privateChatPanel.className).toContain('bg-sky-500/8');
|
||||||
@@ -404,6 +405,12 @@ test('私聊和队友收束复用暗色 tint PlatformSubpanel chrome', () => {
|
|||||||
expect(companionResolutionEcho.className).toContain('border-emerald-400/18');
|
expect(companionResolutionEcho.className).toContain('border-emerald-400/18');
|
||||||
expect(companionResolutionEcho.className).toContain('bg-emerald-500/8');
|
expect(companionResolutionEcho.className).toContain('bg-emerald-500/8');
|
||||||
expect(companionResolutionEcho.className).toContain('rounded-xl');
|
expect(companionResolutionEcho.className).toContain('rounded-xl');
|
||||||
|
expect(privateChatButton.className).toContain(
|
||||||
|
'platform-action-button--editor-dark',
|
||||||
|
);
|
||||||
|
expect(privateChatButton.className).toContain('rounded-xl');
|
||||||
|
expect(privateChatButton.className).toContain('bg-sky-400/15');
|
||||||
|
expect(privateChatButton.className).toContain('disabled:bg-black/20');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('技能详情静态标签复用暗色 PlatformPillBadge chrome', () => {
|
test('技能详情静态标签复用暗色 PlatformPillBadge chrome', () => {
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ import {
|
|||||||
StatusRow,
|
StatusRow,
|
||||||
} from './CharacterInfoShared';
|
} from './CharacterInfoShared';
|
||||||
import { PlatformEmptyState } from './common/PlatformEmptyState';
|
import { PlatformEmptyState } from './common/PlatformEmptyState';
|
||||||
|
import { PlatformActionButton } from './common/PlatformActionButton';
|
||||||
import { PlatformPillBadge } from './common/PlatformPillBadge';
|
import { PlatformPillBadge } from './common/PlatformPillBadge';
|
||||||
import { PlatformSubpanel } from './common/PlatformSubpanel';
|
import { PlatformSubpanel } from './common/PlatformSubpanel';
|
||||||
import { GENERIC_NPC_SCENE_SCALE } from './game-canvas/GameCanvasShared';
|
import { GENERIC_NPC_SCENE_SCALE } from './game-canvas/GameCanvasShared';
|
||||||
@@ -1106,8 +1107,9 @@ export function AdventureEntityModal({
|
|||||||
: `好感达到 ${privateChatUnlockAffinity ?? DEFAULT_PRIVATE_CHAT_UNLOCK_AFFINITY} 后解锁,当前 ${companionNpcState?.affinity ?? 0}。`}
|
: `好感达到 ${privateChatUnlockAffinity ?? DEFAULT_PRIVATE_CHAT_UNLOCK_AFFINITY} 后解锁,当前 ${companionNpcState?.affinity ?? 0}。`}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<PlatformActionButton
|
||||||
type="button"
|
surface="editorDark"
|
||||||
|
tone="ghost"
|
||||||
disabled={
|
disabled={
|
||||||
!privateChatUnlocked || !onOpenCharacterChat
|
!privateChatUnlocked || !onOpenCharacterChat
|
||||||
}
|
}
|
||||||
@@ -1121,16 +1123,12 @@ export function AdventureEntityModal({
|
|||||||
onClose();
|
onClose();
|
||||||
onOpenCharacterChat(companionChatTarget);
|
onOpenCharacterChat(companionChatTarget);
|
||||||
}}
|
}}
|
||||||
className={`rounded-xl px-4 py-2 text-sm font-medium transition-colors ${
|
className="rounded-xl border-sky-300/40 bg-sky-400/15 text-sky-50 hover:bg-sky-400/22 disabled:cursor-not-allowed disabled:border-white/8 disabled:bg-black/20 disabled:text-zinc-500 disabled:opacity-100"
|
||||||
privateChatUnlocked && onOpenCharacterChat
|
|
||||||
? 'border border-sky-300/40 bg-sky-400/15 text-sky-50 hover:bg-sky-400/22'
|
|
||||||
: 'cursor-not-allowed border border-white/8 bg-black/20 text-zinc-500'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{privateChatUnlocked
|
{privateChatUnlocked
|
||||||
? '聊天'
|
? '聊天'
|
||||||
: `聊天(${privateChatUnlockAffinity ?? DEFAULT_PRIVATE_CHAT_UNLOCK_AFFINITY} 解锁)`}
|
: `聊天(${privateChatUnlockAffinity ?? DEFAULT_PRIVATE_CHAT_UNLOCK_AFFINITY} 解锁)`}
|
||||||
</button>
|
</PlatformActionButton>
|
||||||
</div>
|
</div>
|
||||||
</PlatformSubpanel>
|
</PlatformSubpanel>
|
||||||
</Section>
|
</Section>
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ test('背包工坊材料需求状态复用暗色平台胶囊标签', () => {
|
|||||||
const missingRequirement = screen.getByText('木材 0/1');
|
const missingRequirement = screen.getByText('木材 0/1');
|
||||||
const forgePanel = screen.getByText('工坊').closest('section');
|
const forgePanel = screen.getByText('工坊').closest('section');
|
||||||
const recipePanel = screen.getByText('潮汐护符').closest('section');
|
const recipePanel = screen.getByText('潮汐护符').closest('section');
|
||||||
|
const forgeButton = screen.getByRole('button', { name: '锻造' });
|
||||||
|
|
||||||
expect(metRequirement.className).toContain('rounded-full');
|
expect(metRequirement.className).toContain('rounded-full');
|
||||||
expect(metRequirement.className).toContain('font-black');
|
expect(metRequirement.className).toContain('font-black');
|
||||||
@@ -95,6 +96,10 @@ test('背包工坊材料需求状态复用暗色平台胶囊标签', () => {
|
|||||||
expect(forgePanel?.className).toContain('bg-black/25');
|
expect(forgePanel?.className).toContain('bg-black/25');
|
||||||
expect(recipePanel?.className).toContain('border-white/10');
|
expect(recipePanel?.className).toContain('border-white/10');
|
||||||
expect(recipePanel?.className).toContain('bg-black/25');
|
expect(recipePanel?.className).toContain('bg-black/25');
|
||||||
|
expect(forgeButton.className).toContain('platform-action-button--editor-dark');
|
||||||
|
expect(forgeButton.className).toContain('rounded-lg');
|
||||||
|
expect(forgeButton.className).toContain('bg-emerald-500/10');
|
||||||
|
expect(forgeButton.className).toContain('disabled:bg-black/20');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('背包文书和故事档案区块复用暗色 PlatformSubpanel chrome', () => {
|
test('背包文书和故事档案区块复用暗色 PlatformSubpanel chrome', () => {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
WorldType,
|
WorldType,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { PlatformPillBadge } from './common/PlatformPillBadge';
|
import { PlatformPillBadge } from './common/PlatformPillBadge';
|
||||||
|
import { PlatformActionButton } from './common/PlatformActionButton';
|
||||||
import { PlatformStatusMessage } from './common/PlatformStatusMessage';
|
import { PlatformStatusMessage } from './common/PlatformStatusMessage';
|
||||||
import { PlatformSubpanel } from './common/PlatformSubpanel';
|
import { PlatformSubpanel } from './common/PlatformSubpanel';
|
||||||
import {
|
import {
|
||||||
@@ -216,8 +217,10 @@ export function InventoryPanel({
|
|||||||
花费:{recipe.currencyText}
|
花费:{recipe.currencyText}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<PlatformActionButton
|
||||||
type="button"
|
surface="editorDark"
|
||||||
|
tone="ghost"
|
||||||
|
size="xxs"
|
||||||
disabled={
|
disabled={
|
||||||
!recipe.canCraft ||
|
!recipe.canCraft ||
|
||||||
!recipe.action.enabled ||
|
!recipe.action.enabled ||
|
||||||
@@ -232,18 +235,14 @@ export function InventoryPanel({
|
|||||||
setSelectedItem(null);
|
setSelectedItem(null);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className={`rounded-lg border px-3 py-1.5 text-xs transition ${
|
className="rounded-lg border-emerald-400/30 bg-emerald-500/10 text-emerald-100 hover:bg-emerald-500/20 disabled:border-white/8 disabled:bg-black/20 disabled:text-zinc-500 disabled:opacity-100"
|
||||||
recipe.canCraft && recipe.action.enabled && !inBattle
|
|
||||||
? 'border-emerald-400/30 bg-emerald-500/10 text-emerald-100 hover:bg-emerald-500/20'
|
|
||||||
: 'border-white/8 bg-black/20 text-zinc-500'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{forgeActionKey === recipe.id
|
{forgeActionKey === recipe.id
|
||||||
? '制作中...'
|
? '制作中...'
|
||||||
: recipe.kind === 'forge'
|
: recipe.kind === 'forge'
|
||||||
? '锻造'
|
? '锻造'
|
||||||
: '合成'}
|
: '合成'}
|
||||||
</button>
|
</PlatformActionButton>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 flex flex-wrap gap-2">
|
<div className="mt-2 flex flex-wrap gap-2">
|
||||||
{recipe.requirements.map((requirement) => {
|
{recipe.requirements.map((requirement) => {
|
||||||
|
|||||||
@@ -105,15 +105,19 @@ test('can opt into platform action button chrome', () => {
|
|||||||
actionSurface="platform"
|
actionSurface="platform"
|
||||||
actionShape="pill"
|
actionShape="pill"
|
||||||
actionFullWidth
|
actionFullWidth
|
||||||
|
aria-label="复制错误详情"
|
||||||
|
title="复制错误详情"
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
const button = screen.getByRole('button', { name: '复制报错' });
|
const button = screen.getByRole('button', { name: '复制错误详情' });
|
||||||
|
|
||||||
expect(button.className).toContain('platform-button--primary');
|
expect(button.className).toContain('platform-button--primary');
|
||||||
expect(button.className).toContain('w-full');
|
expect(button.className).toContain('w-full');
|
||||||
expect(button.className).toContain('rounded-full');
|
expect(button.className).toContain('rounded-full');
|
||||||
expect(button.className).toContain('disabled:cursor-not-allowed');
|
expect(button.className).toContain('disabled:cursor-not-allowed');
|
||||||
|
expect(button.getAttribute('title')).toBe('复制错误详情');
|
||||||
|
expect(button.textContent).toContain('复制报错');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('can opt into shared pill action chrome', () => {
|
test('can opt into shared pill action chrome', () => {
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ import { Check, Copy } from 'lucide-react';
|
|||||||
import type { ButtonHTMLAttributes, ReactNode } from 'react';
|
import type { ButtonHTMLAttributes, ReactNode } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getPlatformActionButtonClassName,
|
|
||||||
type PlatformActionButtonSize,
|
type PlatformActionButtonSize,
|
||||||
type PlatformActionButtonShape,
|
type PlatformActionButtonShape,
|
||||||
type PlatformActionButtonSurface,
|
type PlatformActionButtonSurface,
|
||||||
type PlatformActionButtonTone,
|
type PlatformActionButtonTone,
|
||||||
} from './platformActionButtonModel';
|
} from './platformActionButtonModel';
|
||||||
|
import { PlatformActionButton } from './PlatformActionButton';
|
||||||
import {
|
import {
|
||||||
getPlatformPillBadgeClassName,
|
getPlatformPillBadgeClassName,
|
||||||
type PlatformPillBadgeSize,
|
type PlatformPillBadgeSize,
|
||||||
@@ -105,38 +105,53 @@ export function CopyFeedbackButton({
|
|||||||
: typeof idleLabel === 'string'
|
: typeof idleLabel === 'string'
|
||||||
? idleLabel
|
? idleLabel
|
||||||
: undefined);
|
: undefined);
|
||||||
|
const resolvedAriaLabel = ariaLabel ?? accessibleLabel;
|
||||||
|
const resolvedTitle =
|
||||||
|
title ?? (typeof accessibleLabel === 'string' ? accessibleLabel : undefined);
|
||||||
|
const content = (
|
||||||
|
<>
|
||||||
|
{showIcon ? icon : null}
|
||||||
|
{showLabel ? <span className={labelClassName}>{label}</span> : null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (actionSurface) {
|
||||||
|
return (
|
||||||
|
<PlatformActionButton
|
||||||
|
surface={actionSurface}
|
||||||
|
tone={actionTone}
|
||||||
|
size={actionSize}
|
||||||
|
shape={actionShape}
|
||||||
|
fullWidth={actionFullWidth}
|
||||||
|
className={className}
|
||||||
|
{...buttonProps}
|
||||||
|
aria-label={resolvedAriaLabel}
|
||||||
|
title={resolvedTitle}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</PlatformActionButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={[
|
className={[
|
||||||
actionSurface
|
actionAppearance === 'pill'
|
||||||
? getPlatformActionButtonClassName({
|
? getPlatformPillBadgeClassName({
|
||||||
surface: actionSurface,
|
tone: actionPillTone,
|
||||||
tone: actionTone,
|
size: actionPillSize,
|
||||||
size: actionSize,
|
|
||||||
shape: actionShape,
|
|
||||||
fullWidth: actionFullWidth,
|
|
||||||
})
|
})
|
||||||
: actionAppearance === 'pill'
|
: null,
|
||||||
? getPlatformPillBadgeClassName({
|
|
||||||
tone: actionPillTone,
|
|
||||||
size: actionPillSize,
|
|
||||||
})
|
|
||||||
: null,
|
|
||||||
className,
|
className,
|
||||||
]
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(' ')}
|
.join(' ')}
|
||||||
{...buttonProps}
|
{...buttonProps}
|
||||||
aria-label={ariaLabel ?? accessibleLabel}
|
aria-label={resolvedAriaLabel}
|
||||||
title={
|
title={resolvedTitle}
|
||||||
title ??
|
|
||||||
(typeof accessibleLabel === 'string' ? accessibleLabel : undefined)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{showIcon ? icon : null}
|
{content}
|
||||||
{showLabel ? <span className={labelClassName}>{label}</span> : null}
|
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -847,6 +847,31 @@ test('creation hub works-only tab filters bark battle draft and published works'
|
|||||||
expect(onOpenBarkBattleDetail).toHaveBeenCalledWith(barkBattlePublishedItem);
|
expect(onOpenBarkBattleDetail).toHaveBeenCalledWith(barkBattlePublishedItem);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('creation hub keeps filtered empty copy when selected tab has no works', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
|
||||||
|
render(
|
||||||
|
<CustomWorldCreationHub
|
||||||
|
mode="works-only"
|
||||||
|
items={[]}
|
||||||
|
barkBattleItems={[barkBattlePublishedItem]}
|
||||||
|
loading={false}
|
||||||
|
error={null}
|
||||||
|
onRetry={() => {}}
|
||||||
|
onCreateType={noopCreateType}
|
||||||
|
onOpenDraft={() => {}}
|
||||||
|
onEnterPublished={() => {}}
|
||||||
|
entryConfig={testEntryConfig}
|
||||||
|
creationTypes={testCreationTypes}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('tab', { name: '草稿 0' }));
|
||||||
|
|
||||||
|
expect(screen.getByText('当前筛选下没有内容')).toBeTruthy();
|
||||||
|
expect(screen.queryByText('还没有作品')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
test('creation hub published work delete action stays in revealed side actions', async () => {
|
test('creation hub published work delete action stays in revealed side actions', async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
const onDeletePuzzle = vi.fn();
|
const onDeletePuzzle = vi.fn();
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useEffect, useMemo, useState } from 'react';
|
|||||||
import { resolveSelectionStageFromPath } from '../../routing/appPageRoutes';
|
import { resolveSelectionStageFromPath } from '../../routing/appPageRoutes';
|
||||||
import type { CreationEntryConfig } from '../../services/creationEntryConfigService';
|
import type { CreationEntryConfig } from '../../services/creationEntryConfigService';
|
||||||
import { PlatformActionButton } from '../common/PlatformActionButton';
|
import { PlatformActionButton } from '../common/PlatformActionButton';
|
||||||
|
import { PlatformAsyncStatePanel } from '../common/PlatformAsyncStatePanel';
|
||||||
import { PlatformEmptyState } from '../common/PlatformEmptyState';
|
import { PlatformEmptyState } from '../common/PlatformEmptyState';
|
||||||
import { PlatformSubpanel } from '../common/PlatformSubpanel';
|
import { PlatformSubpanel } from '../common/PlatformSubpanel';
|
||||||
import type { PublishShareModalPayload } from '../common/publishShareModalModel';
|
import type { PublishShareModalPayload } from '../common/publishShareModalModel';
|
||||||
@@ -206,6 +207,7 @@ export function CustomWorldCreationHub({
|
|||||||
const recentCreationTypeIds = [
|
const recentCreationTypeIds = [
|
||||||
...new Set(recentWorkItems.map((item) => item.kind)),
|
...new Set(recentWorkItems.map((item) => item.kind)),
|
||||||
];
|
];
|
||||||
|
const isWorkShelfEmpty = !loading && filteredItems.length === 0;
|
||||||
|
|
||||||
function handleOpenShelfItem(item: CreationWorkShelfItem) {
|
function handleOpenShelfItem(item: CreationWorkShelfItem) {
|
||||||
onOpenShelfItem?.(item);
|
onOpenShelfItem?.(item);
|
||||||
@@ -238,6 +240,55 @@ export function CustomWorldCreationHub({
|
|||||||
|
|
||||||
const showStartCard = mode !== 'works-only';
|
const showStartCard = mode !== 'works-only';
|
||||||
const showWorkShelf = mode !== 'start-only';
|
const showWorkShelf = mode !== 'start-only';
|
||||||
|
const workShelfLoadingState = (
|
||||||
|
<div className={WORK_GRID_CLASS}>
|
||||||
|
{Array.from({ length: 3 }).map((_, index) => (
|
||||||
|
<PlatformSubpanel
|
||||||
|
as="div"
|
||||||
|
key={`skeleton-${index}`}
|
||||||
|
padding="sm"
|
||||||
|
radius="md"
|
||||||
|
className="min-h-[10.5rem] sm:min-h-[12rem] sm:p-5"
|
||||||
|
>
|
||||||
|
<div className="h-4 w-20 rounded-full bg-[var(--platform-track-fill)]" />
|
||||||
|
<div className="mt-5 h-6 w-24 rounded-full bg-[var(--platform-track-fill)] sm:mt-6 sm:h-8 sm:w-36" />
|
||||||
|
<div className="mt-3 h-3 w-full rounded-full bg-[var(--platform-track-fill)] sm:mt-4 sm:h-4" />
|
||||||
|
<div className="mt-2 h-4 w-4/5 rounded-full bg-[var(--platform-track-fill)]" />
|
||||||
|
<div className="mt-6 flex flex-col gap-2 sm:mt-8 sm:flex-row">
|
||||||
|
<div className="h-6 w-16 rounded-full bg-[var(--platform-track-fill)] sm:h-7 sm:w-20" />
|
||||||
|
<div className="h-6 w-16 rounded-full bg-[var(--platform-track-fill)] sm:h-7 sm:w-20" />
|
||||||
|
</div>
|
||||||
|
</PlatformSubpanel>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
const workShelfEmptyState = (
|
||||||
|
<EmptyState
|
||||||
|
title={shelfItems.length === 0 ? '还没有作品' : '当前筛选下没有内容'}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
const workShelfContent = (
|
||||||
|
<div className={WORK_GRID_CLASS}>
|
||||||
|
{filteredItems.map((item) => (
|
||||||
|
<CustomWorldWorkCard
|
||||||
|
key={`${item.kind}-${item.id}`}
|
||||||
|
item={item}
|
||||||
|
previousMetricValues={metricSnapshot[buildWorkMetricCacheItemKey(item)]}
|
||||||
|
onOpen={() => {
|
||||||
|
handleOpenShelfItem(item);
|
||||||
|
}}
|
||||||
|
onDelete={buildDeleteAction(item)}
|
||||||
|
deleteBusy={deletingWorkId === item.id}
|
||||||
|
onShare={buildShareAction(item)}
|
||||||
|
onClaimPointIncentive={buildPointIncentiveAction(item)}
|
||||||
|
pointIncentiveBusy={
|
||||||
|
item.source.kind === 'puzzle' &&
|
||||||
|
claimingPuzzleProfileId === item.source.item.profileId
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="platform-remap-surface w-full space-y-4 px-3 pb-4 pt-3 sm:px-4 sm:pt-4 xl:px-5 xl:pb-5 xl:pt-5">
|
<div className="platform-remap-surface w-full space-y-4 px-3 pb-4 pt-3 sm:px-4 sm:pt-4 xl:px-5 xl:pb-5 xl:pt-5">
|
||||||
@@ -276,55 +327,14 @@ export function CustomWorldCreationHub({
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{showWorkShelf ? (
|
{showWorkShelf ? (
|
||||||
loading ? (
|
<PlatformAsyncStatePanel
|
||||||
<div className={WORK_GRID_CLASS}>
|
isLoading={loading}
|
||||||
{Array.from({ length: 3 }).map((_, index) => (
|
loadingState={workShelfLoadingState}
|
||||||
<PlatformSubpanel
|
isEmpty={isWorkShelfEmpty}
|
||||||
as="div"
|
emptyState={workShelfEmptyState}
|
||||||
key={`skeleton-${index}`}
|
>
|
||||||
padding="sm"
|
{workShelfContent}
|
||||||
radius="md"
|
</PlatformAsyncStatePanel>
|
||||||
className="min-h-[10.5rem] sm:min-h-[12rem] sm:p-5"
|
|
||||||
>
|
|
||||||
<div className="h-4 w-20 rounded-full bg-[var(--platform-track-fill)]" />
|
|
||||||
<div className="mt-5 h-6 w-24 rounded-full bg-[var(--platform-track-fill)] sm:mt-6 sm:h-8 sm:w-36" />
|
|
||||||
<div className="mt-3 h-3 w-full rounded-full bg-[var(--platform-track-fill)] sm:mt-4 sm:h-4" />
|
|
||||||
<div className="mt-2 h-4 w-4/5 rounded-full bg-[var(--platform-track-fill)]" />
|
|
||||||
<div className="mt-6 flex flex-col gap-2 sm:mt-8 sm:flex-row">
|
|
||||||
<div className="h-6 w-16 rounded-full bg-[var(--platform-track-fill)] sm:h-7 sm:w-20" />
|
|
||||||
<div className="h-6 w-16 rounded-full bg-[var(--platform-track-fill)] sm:h-7 sm:w-20" />
|
|
||||||
</div>
|
|
||||||
</PlatformSubpanel>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : filteredItems.length > 0 ? (
|
|
||||||
<div className={WORK_GRID_CLASS}>
|
|
||||||
{filteredItems.map((item) => (
|
|
||||||
<CustomWorldWorkCard
|
|
||||||
key={`${item.kind}-${item.id}`}
|
|
||||||
item={item}
|
|
||||||
previousMetricValues={
|
|
||||||
metricSnapshot[buildWorkMetricCacheItemKey(item)]
|
|
||||||
}
|
|
||||||
onOpen={() => {
|
|
||||||
handleOpenShelfItem(item);
|
|
||||||
}}
|
|
||||||
onDelete={buildDeleteAction(item)}
|
|
||||||
deleteBusy={deletingWorkId === item.id}
|
|
||||||
onShare={buildShareAction(item)}
|
|
||||||
onClaimPointIncentive={buildPointIncentiveAction(item)}
|
|
||||||
pointIncentiveBusy={
|
|
||||||
item.source.kind === 'puzzle' &&
|
|
||||||
claimingPuzzleProfileId === item.source.item.profileId
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : shelfItems.length === 0 ? (
|
|
||||||
<EmptyState title="还没有作品" />
|
|
||||||
) : (
|
|
||||||
<EmptyState title="当前筛选下没有内容" />
|
|
||||||
)
|
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1673,30 +1673,28 @@ function Match3DCoverImageEditor({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{sourceAssets.length > 0 ? (
|
<PlatformAssetPickerGrid
|
||||||
<PlatformAssetPickerGrid
|
items={sourceAssets}
|
||||||
items={sourceAssets}
|
loadingLabel="读取中..."
|
||||||
loadingLabel="读取中..."
|
emptyLabel="暂无可引用素材"
|
||||||
emptyLabel="暂无可引用素材"
|
disabled={isGenerating}
|
||||||
disabled={isGenerating}
|
getKey={(asset) => asset.id}
|
||||||
getKey={(asset) => asset.id}
|
getImageSrc={(asset) => asset.imageSrc}
|
||||||
getImageSrc={(asset) => asset.imageSrc}
|
getImageAlt={() => ''}
|
||||||
getImageAlt={() => ''}
|
getTitle={(asset) => asset.label}
|
||||||
getTitle={(asset) => asset.label}
|
getAriaLabel={(asset) => `引用${asset.label}`}
|
||||||
getAriaLabel={(asset) => `引用${asset.label}`}
|
isSelected={(asset) =>
|
||||||
isSelected={(asset) =>
|
referenceImages.some(
|
||||||
referenceImages.some(
|
(reference) => reference.imageSrc === asset.imageSrc,
|
||||||
(reference) => reference.imageSrc === asset.imageSrc,
|
)
|
||||||
)
|
}
|
||||||
}
|
onSelect={(asset) => onReferenceSelect(asset.imageSrc)}
|
||||||
onSelect={(asset) => onReferenceSelect(asset.imageSrc)}
|
gridClassName="grid grid-cols-3 gap-2 sm:grid-cols-4"
|
||||||
gridClassName="grid grid-cols-3 gap-2 sm:grid-cols-4"
|
cardClassName="bg-white/74"
|
||||||
cardClassName="bg-white/74"
|
cardRadiusClassName="rounded-[1rem]"
|
||||||
cardRadiusClassName="rounded-[1rem]"
|
imageShellClassName="aspect-square"
|
||||||
imageShellClassName="aspect-square"
|
bodyClassName="truncate px-2 py-2 text-[11px] font-semibold text-[var(--platform-text-base)]"
|
||||||
bodyClassName="truncate px-2 py-2 text-[11px] font-semibold text-[var(--platform-text-base)]"
|
/>
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ function stubReferenceImageUpload(dataUrl: string) {
|
|||||||
vi.stubGlobal('FileReader', MockFileReader as unknown as typeof FileReader);
|
vi.stubGlobal('FileReader', MockFileReader as unknown as typeof FileReader);
|
||||||
}
|
}
|
||||||
|
|
||||||
test('renders missing draft notice with shared PlatformSubpanel chrome', () => {
|
test('renders missing draft notice with shared PlatformEmptyState chrome', () => {
|
||||||
render(
|
render(
|
||||||
<PuzzleResultView
|
<PuzzleResultView
|
||||||
session={{ ...createSession(), draft: null }}
|
session={{ ...createSession(), draft: null }}
|
||||||
@@ -92,11 +92,12 @@ test('renders missing draft notice with shared PlatformSubpanel chrome', () => {
|
|||||||
|
|
||||||
const noticePanel = screen
|
const noticePanel = screen
|
||||||
.getByText('还没有可编辑的拼图草稿')
|
.getByText('还没有可编辑的拼图草稿')
|
||||||
.closest('.platform-subpanel');
|
.closest('.platform-empty-state');
|
||||||
|
|
||||||
|
expect(noticePanel?.className).toContain('platform-empty-state');
|
||||||
expect(noticePanel?.className).toContain('rounded-[1rem]');
|
expect(noticePanel?.className).toContain('rounded-[1rem]');
|
||||||
expect(noticePanel?.className).toContain('sm:p-5');
|
expect(noticePanel?.className).toContain('py-5');
|
||||||
expect(noticePanel?.className).toContain('text-[var(--platform-text-base)]');
|
expect(noticePanel?.className).toContain('text-[var(--platform-text-soft)]');
|
||||||
});
|
});
|
||||||
|
|
||||||
function createSession(
|
function createSession(
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { readPuzzleReferenceImageAsDataUrl } from '../../services/puzzleReferenc
|
|||||||
import { useAuthUi } from '../auth/AuthUiContext';
|
import { useAuthUi } from '../auth/AuthUiContext';
|
||||||
import { CreativeImageInputPanel } from '../common/CreativeImageInputPanel';
|
import { CreativeImageInputPanel } from '../common/CreativeImageInputPanel';
|
||||||
import { PlatformActionButton } from '../common/PlatformActionButton';
|
import { PlatformActionButton } from '../common/PlatformActionButton';
|
||||||
|
import { PlatformEmptyState } from '../common/PlatformEmptyState';
|
||||||
import { PlatformFieldLabel } from '../common/PlatformFieldLabel';
|
import { PlatformFieldLabel } from '../common/PlatformFieldLabel';
|
||||||
import { PlatformIconBadge } from '../common/PlatformIconBadge';
|
import { PlatformIconBadge } from '../common/PlatformIconBadge';
|
||||||
import { PlatformIconButton } from '../common/PlatformIconButton';
|
import { PlatformIconButton } from '../common/PlatformIconButton';
|
||||||
@@ -1556,14 +1557,13 @@ export function PuzzleResultView({
|
|||||||
if (!draft || !editState || !syncedDraft) {
|
if (!draft || !editState || !syncedDraft) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full items-center justify-center">
|
<div className="flex h-full items-center justify-center">
|
||||||
<PlatformSubpanel
|
<PlatformEmptyState
|
||||||
as="div"
|
surface="subpanel"
|
||||||
radius="sm"
|
size="inline"
|
||||||
padding="lg"
|
className="w-full max-w-md"
|
||||||
className="text-sm text-[var(--platform-text-base)]"
|
|
||||||
>
|
>
|
||||||
还没有可编辑的拼图草稿
|
还没有可编辑的拼图草稿
|
||||||
</PlatformSubpanel>
|
</PlatformEmptyState>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ test('RPG asset debug panel uses PlatformSubpanel shells for summary and entries
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('RPG asset debug panel uses PlatformSubpanel shell for empty state', () => {
|
test('RPG asset debug panel uses PlatformEmptyState shell for empty state', () => {
|
||||||
const emptyProfile = {
|
const emptyProfile = {
|
||||||
...createProfileWithAssets(),
|
...createProfileWithAssets(),
|
||||||
playableNpcs: [],
|
playableNpcs: [],
|
||||||
@@ -187,14 +187,15 @@ test('RPG asset debug panel uses PlatformSubpanel shell for empty state', () =>
|
|||||||
|
|
||||||
const emptyPanel = screen
|
const emptyPanel = screen
|
||||||
.getByText('当前结果页 profile 里没有拿到任何可诊断的图片地址。')
|
.getByText('当前结果页 profile 里没有拿到任何可诊断的图片地址。')
|
||||||
.closest('section');
|
.closest('.platform-empty-state');
|
||||||
|
|
||||||
expect(screen.getByText('0项')).toBeTruthy();
|
expect(screen.getByText('0项')).toBeTruthy();
|
||||||
expect(emptyPanel?.className).toContain('platform-subpanel');
|
expect(emptyPanel?.className).toContain('platform-empty-state');
|
||||||
expect(emptyPanel?.className).toContain('rounded-2xl');
|
expect(emptyPanel?.className).toContain('rounded-2xl');
|
||||||
|
expect(emptyPanel?.className).toContain('bg-black/20');
|
||||||
expect(
|
expect(
|
||||||
container.querySelectorAll(
|
container.querySelectorAll(
|
||||||
'section.platform-subpanel, div.platform-subpanel',
|
'section.platform-subpanel, div.platform-subpanel, div.platform-empty-state',
|
||||||
),
|
),
|
||||||
).toHaveLength(5);
|
).toHaveLength(5);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useState } from 'react';
|
|||||||
|
|
||||||
import { resolveAssetReadUrl } from '../../services/assetReadUrlService';
|
import { resolveAssetReadUrl } from '../../services/assetReadUrlService';
|
||||||
import type { CustomWorldProfile } from '../../types';
|
import type { CustomWorldProfile } from '../../types';
|
||||||
|
import { PlatformEmptyState } from '../common/PlatformEmptyState';
|
||||||
import { PlatformPillBadge } from '../common/PlatformPillBadge';
|
import { PlatformPillBadge } from '../common/PlatformPillBadge';
|
||||||
import { PlatformSubpanel } from '../common/PlatformSubpanel';
|
import { PlatformSubpanel } from '../common/PlatformSubpanel';
|
||||||
|
|
||||||
@@ -303,12 +304,14 @@ export function RpgCreationAssetDebugPanel({
|
|||||||
);
|
);
|
||||||
})
|
})
|
||||||
) : (
|
) : (
|
||||||
<PlatformSubpanel
|
<PlatformEmptyState
|
||||||
padding="none"
|
surface="editorDark"
|
||||||
className="rounded-2xl px-3 py-3 text-sm text-zinc-400"
|
size="compact"
|
||||||
|
tone="soft"
|
||||||
|
className="rounded-2xl px-3 py-3"
|
||||||
>
|
>
|
||||||
当前结果页 profile 里没有拿到任何可诊断的图片地址。
|
当前结果页 profile 里没有拿到任何可诊断的图片地址。
|
||||||
</PlatformSubpanel>
|
</PlatformEmptyState>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user