收口前端平台组件能力

新增 PlatformAsyncStatePanel 统一 profile 异步状态骨架
扩展 PlatformSegmentedTabs 支持滚动 tab 并接入创作入口与发现页
统一 PixelCloseButton 复用 PlatformModalCloseButton 像素关闭能力
抽取平台入口泥点前置提示弹层并收紧阻断语义
补充组件收口文档与共享决策记录
This commit is contained in:
2026-06-11 01:06:31 +08:00
parent edf37d97a7
commit 94122583ac
22 changed files with 897 additions and 445 deletions

View File

@@ -0,0 +1,49 @@
/* @vitest-environment jsdom */
import { fireEvent, render, screen } from '@testing-library/react';
import { expect, test, vi } from 'vitest';
import {
PlatformDraftGenerationPointNoticeDialog,
} from './PlatformDraftGenerationPointNoticeDialog';
test('renders the insufficient-points notice with the shared blocking copy', () => {
const onClose = vi.fn();
render(
<PlatformDraftGenerationPointNoticeDialog
notice={{
kind: 'insufficient-points',
requiredPoints: 30,
currentPoints: 12,
}}
onClose={onClose}
/>,
);
expect(screen.getByRole('dialog', { name: '泥点不足' })).toBeTruthy();
expect(screen.getByText('本次需要 30 泥点,当前 12 泥点。')).toBeTruthy();
expect(
screen.getByText('当前表单不会丢失,关闭后可继续编辑或补足泥点再继续。'),
).toBeTruthy();
fireEvent.click(screen.getByRole('button', { name: '知道了' }));
expect(onClose).toHaveBeenCalledTimes(1);
});
test('renders the balance-load-failed notice without the amber icon override', () => {
render(
<PlatformDraftGenerationPointNoticeDialog
notice={{ kind: 'balance-load-failed' }}
onClose={() => {}}
/>,
);
const dialog = screen.getByRole('dialog', { name: '读取泥点余额失败' });
expect(screen.getByText('请稍后重试。')).toBeTruthy();
expect(
screen.getByText('当前表单不会丢失,关闭后可继续编辑,稍后再试。'),
).toBeTruthy();
expect(dialog.innerHTML).not.toContain('bg-amber-100/80');
});

View File

@@ -0,0 +1,79 @@
import { PlatformAcknowledgeStatusDialog } from '../common/PlatformAcknowledgeStatusDialog';
export type DraftGenerationPointNotice =
| {
kind: 'insufficient-points';
requiredPoints: number;
currentPoints: number;
}
| {
kind: 'balance-load-failed';
};
type PlatformDraftGenerationPointNoticeDialogProps = {
notice: DraftGenerationPointNotice | null;
onClose: () => void;
overlayClassName?: string;
panelClassName?: string;
zIndexClassName?: string;
};
function resolveDraftGenerationPointNoticeTitle(
notice: DraftGenerationPointNotice,
) {
return notice.kind === 'balance-load-failed' ? '读取泥点余额失败' : '泥点不足';
}
function resolveDraftGenerationPointNoticeDescription(
notice: DraftGenerationPointNotice,
) {
return notice.kind === 'balance-load-failed'
? '当前表单不会丢失,关闭后可继续编辑,稍后再试。'
: '当前表单不会丢失,关闭后可继续编辑或补足泥点再继续。';
}
function resolveDraftGenerationPointNoticeMessage(
notice: DraftGenerationPointNotice,
) {
return notice.kind === 'balance-load-failed'
? '请稍后重试。'
: `本次需要 ${notice.requiredPoints} 泥点,当前 ${notice.currentPoints} 泥点。`;
}
/**
* 创作前置泥点提示弹层。
* 只承接平台入口里“泥点不足 / 读取余额失败”这类阻断提示,避免 FlowShell 直接拼底层状态弹窗。
*/
export function PlatformDraftGenerationPointNoticeDialog({
notice,
onClose,
overlayClassName,
panelClassName,
zIndexClassName,
}: PlatformDraftGenerationPointNoticeDialogProps) {
if (!notice) {
return null;
}
return (
<PlatformAcknowledgeStatusDialog
status="error"
title={resolveDraftGenerationPointNoticeTitle(notice)}
description={resolveDraftGenerationPointNoticeDescription(notice)}
onClose={onClose}
showHeader
showCloseButton
closeOnBackdrop
overlayClassName={overlayClassName}
panelClassName={panelClassName}
zIndexClassName={zIndexClassName}
iconClassName={
notice.kind === 'balance-load-failed'
? undefined
: 'bg-amber-100/80 text-amber-600'
}
>
{resolveDraftGenerationPointNoticeMessage(notice)}
</PlatformAcknowledgeStatusDialog>
);
}

View File

@@ -498,6 +498,10 @@ import {
EDUTAINMENT_HIDDEN_MESSAGE,
filterGeneralPublicWorks,
} from './platformEdutainmentVisibility';
import {
PlatformDraftGenerationPointNoticeDialog,
type DraftGenerationPointNotice,
} from './PlatformDraftGenerationPointNoticeDialog';
import { PlatformEntryCreationTypeModal } from './PlatformEntryCreationTypeModal';
import type { PlatformCreationTypeId } from './platformEntryCreationTypes';
import {
@@ -1419,10 +1423,8 @@ export function PlatformEntryFlowShellImpl({
: 'platform-theme--light';
const isDesktopLayout = usePlatformDesktopLayout();
const [showCreationTypeModal, setShowCreationTypeModal] = useState(false);
const [draftGenerationPointNotice, setDraftGenerationPointNotice] = useState<{
title: string;
message: string;
} | null>(null);
const [draftGenerationPointNotice, setDraftGenerationPointNotice] =
useState<DraftGenerationPointNotice | null>(null);
const [selectedDetailEntry, setSelectedDetailEntry] =
useState<CustomWorldLibraryEntry<CustomWorldProfile> | null>(null);
const [selectedPublicWorkDetail, setSelectedPublicWorkDetail] =
@@ -2246,14 +2248,14 @@ export function PlatformEntryFlowShellImpl({
}
setDraftGenerationPointNotice({
title: '泥点不足',
message: `本次需要 ${pointsCost} 泥点,当前 ${walletBalance} 泥点。`,
kind: 'insufficient-points',
requiredPoints: pointsCost,
currentPoints: walletBalance,
});
return false;
} catch {
setDraftGenerationPointNotice({
title: '读取泥点余额失败',
message: '请稍后重试。',
kind: 'balance-load-failed',
});
return false;
}
@@ -4364,11 +4366,6 @@ export function PlatformEntryFlowShellImpl({
barkBattleDraftGenerationPointCost,
ensureEnoughDraftGenerationPointsFromServer,
]);
const draftGenerationPointNoticeDescription = draftGenerationPointNotice
? draftGenerationPointNotice.title === '读取泥点余额失败'
? '当前表单不会丢失,关闭后可继续编辑,稍后再试。'
: '当前表单不会丢失,关闭后可继续编辑或补足泥点再继续。'
: undefined;
const recoverCompletedPuzzleDraftGeneration = useCallback(
async ({
sessionId,
@@ -16997,25 +16994,12 @@ export function PlatformEntryFlowShellImpl({
}}
/>
) : null}
<PlatformAcknowledgeStatusDialog
open={Boolean(draftGenerationPointNotice)}
status="error"
title={draftGenerationPointNotice?.title ?? '泥点提示'}
description={draftGenerationPointNoticeDescription}
<PlatformDraftGenerationPointNoticeDialog
notice={draftGenerationPointNotice}
onClose={() => setDraftGenerationPointNotice(null)}
showHeader
showCloseButton
closeOnBackdrop
overlayClassName={`platform-theme ${platformThemeClass} !items-center`}
panelClassName="platform-remap-surface rounded-[1.75rem]"
iconClassName={
draftGenerationPointNotice?.title === '读取泥点余额失败'
? undefined
: 'bg-amber-100/80 text-amber-600'
}
>
{draftGenerationPointNotice?.message}
</PlatformAcknowledgeStatusDialog>
/>
<PublishShareModal
open={Boolean(publishSharePayload)}
payload={publishSharePayload}

View File

@@ -5,6 +5,7 @@ import type {
ProfilePlayStatsResponse,
ProfileSaveArchiveSummary,
} from '../../../packages/shared/src/contracts/runtime';
import { PlatformAsyncStatePanel } from '../common/PlatformAsyncStatePanel';
import { PlatformEmptyState } from '../common/PlatformEmptyState';
import { PlatformFieldLabel } from '../common/PlatformFieldLabel';
import { PlatformPillBadge } from '../common/PlatformPillBadge';
@@ -167,13 +168,30 @@ export function PlatformProfilePlayedWorksModal({
</PlatformStatusMessage>
) : null}
{isLoading ? (
<div className="mt-5 space-y-3">
{Array.from({ length: 4 }).map((_, index) => (
<div key={index} className="h-20 animate-pulse rounded-xl bg-zinc-100" />
))}
</div>
) : hasArchiveEntries || hasPlayedWorks ? (
<PlatformAsyncStatePanel
isLoading={isLoading}
loadingState={
<div className="mt-5 space-y-3">
{Array.from({ length: 4 }).map((_, index) => (
<div
key={index}
className="h-20 animate-pulse rounded-xl bg-zinc-100"
/>
))}
</div>
}
isEmpty={!hasArchiveEntries && !hasPlayedWorks}
emptyState={
<PlatformEmptyState
surface="subpanel"
size="inline"
tone="base"
className="mt-5 text-left"
>
</PlatformEmptyState>
}
>
<div className="mt-5 space-y-5">
{hasArchiveEntries ? (
<section>
@@ -245,16 +263,7 @@ export function PlatformProfilePlayedWorksModal({
</section>
) : null}
</div>
) : (
<PlatformEmptyState
surface="subpanel"
size="inline"
tone="base"
className="mt-5 text-left"
>
</PlatformEmptyState>
)}
</PlatformAsyncStatePanel>
</PlatformProfileSecondaryModalShell>
);
}

View File

@@ -6,6 +6,7 @@ import type {
ProfileRechargeProduct,
} from '../../../packages/shared/src/contracts/runtime';
import { PlatformActionButton } from '../common/PlatformActionButton';
import { PlatformAsyncStatePanel } from '../common/PlatformAsyncStatePanel';
import { PlatformEmptyState } from '../common/PlatformEmptyState';
import { PlatformPillBadge } from '../common/PlatformPillBadge';
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
@@ -183,35 +184,49 @@ export function PlatformProfileRechargeModal({
</button>
</div>
{error ? (
<PlatformStatusMessage
tone="error"
surface="profile"
size="xs"
className="mt-4 rounded-2xl font-semibold"
>
<div>{error}</div>
<PlatformActionButton
surface="profile"
size="xs"
className="mt-3"
onClick={onRetry}
<PlatformAsyncStatePanel
errorState={
error ? (
<PlatformStatusMessage
tone="error"
surface="profile"
size="xs"
className="mt-4 rounded-2xl font-semibold"
>
<div>{error}</div>
<PlatformActionButton
surface="profile"
size="xs"
className="mt-3"
onClick={onRetry}
>
</PlatformActionButton>
</PlatformStatusMessage>
) : null
}
isLoading={isLoading}
loadingState={
<div className="mt-4 grid gap-3 sm:grid-cols-2">
{Array.from({ length: 4 }).map((_, index) => (
<div
key={index}
className="h-28 animate-pulse rounded-[1.15rem] bg-white/10"
/>
))}
</div>
}
isEmpty={products.length === 0}
emptyState={
<PlatformEmptyState
surface="subpanel"
size="inline"
className="mt-4"
>
</PlatformActionButton>
</PlatformStatusMessage>
) : null}
{isLoading ? (
<div className="mt-4 grid gap-3 sm:grid-cols-2">
{Array.from({ length: 4 }).map((_, index) => (
<div
key={index}
className="h-28 animate-pulse rounded-[1.15rem] bg-white/10"
/>
))}
</div>
) : products.length > 0 ? (
</PlatformEmptyState>
}
>
<div className="mt-4 grid gap-3 sm:grid-cols-2">
{products.map((product) => (
<RechargeProductCard
@@ -222,15 +237,7 @@ export function PlatformProfileRechargeModal({
/>
))}
</div>
) : (
<PlatformEmptyState
surface="subpanel"
size="inline"
className="mt-4"
>
</PlatformEmptyState>
)}
</PlatformAsyncStatePanel>
{nativePayment ? (
<PlatformSubpanel

View File

@@ -4,6 +4,7 @@ import type { ReactNode } from 'react';
import communityQqQrImage from '../../../media/social-media-group/qq.png';
import communityWechatQrImage from '../../../media/social-media-group/wechat.png';
import type { ProfileReferralInviteCenterResponse } from '../../../packages/shared/src/contracts/runtime';
import { PlatformAsyncStatePanel } from '../common/PlatformAsyncStatePanel';
import { CopyFeedbackButton } from '../common/CopyFeedbackButton';
import { PlatformActionButton } from '../common/PlatformActionButton';
import { PlatformEmptyState } from '../common/PlatformEmptyState';
@@ -134,144 +135,154 @@ export function PlatformProfileReferralModal({
</div>
);
} else if (panel === 'redeem') {
content = isLoading ? (
<div className="mt-5 space-y-3">
<div className="h-12 animate-pulse rounded-xl bg-zinc-100" />
<div className="h-11 animate-pulse rounded-xl bg-zinc-100" />
</div>
) : center?.hasRedeemedCode ? (
<PlatformEmptyState
surface="subpanel"
size="inline"
tone="base"
className="mt-5"
>
</PlatformEmptyState>
) : (
<form
className="mt-5 space-y-3"
onSubmit={(event) => {
event.preventDefault();
onSubmitRedeemCode();
}}
>
<PlatformTextField
value={redeemCode}
onChange={(event) => onRedeemCodeChange(event.target.value)}
size="lg"
density="roomy"
tone="rose"
className="rounded-xl text-center font-black uppercase tracking-[0.16em]"
placeholder="邀请码"
aria-label="邀请码"
autoComplete="off"
autoFocus
/>
<PlatformActionButton
type="submit"
surface="profile"
fullWidth
size="md"
className="rounded-xl"
disabled={isSubmittingRedeem || !normalizedRedeemCode}
>
{isSubmittingRedeem ? '提交中' : '提交'}
</PlatformActionButton>
</form>
);
} else if (isLoading) {
content = (
<div className="mt-5 space-y-3">
<div className="h-20 animate-pulse rounded-xl bg-zinc-100" />
<div className="h-10 animate-pulse rounded-xl bg-zinc-100" />
</div>
<PlatformAsyncStatePanel
isLoading={isLoading}
loadingState={
<div className="mt-5 space-y-3">
<div className="h-12 animate-pulse rounded-xl bg-zinc-100" />
<div className="h-11 animate-pulse rounded-xl bg-zinc-100" />
</div>
}
isEmpty={Boolean(center?.hasRedeemedCode)}
emptyState={
<PlatformEmptyState
surface="subpanel"
size="inline"
tone="base"
className="mt-5"
>
</PlatformEmptyState>
}
>
<form
className="mt-5 space-y-3"
onSubmit={(event) => {
event.preventDefault();
onSubmitRedeemCode();
}}
>
<PlatformTextField
value={redeemCode}
onChange={(event) => onRedeemCodeChange(event.target.value)}
size="lg"
density="roomy"
tone="rose"
className="rounded-xl text-center font-black uppercase tracking-[0.16em]"
placeholder="邀请码"
aria-label="邀请码"
autoComplete="off"
autoFocus
/>
<PlatformActionButton
type="submit"
surface="profile"
fullWidth
size="md"
className="rounded-xl"
disabled={isSubmittingRedeem || !normalizedRedeemCode}
>
{isSubmittingRedeem ? '提交中' : '提交'}
</PlatformActionButton>
</form>
</PlatformAsyncStatePanel>
);
} else {
content = (
<div className="mt-5 space-y-3">
<PlatformSubpanel
as="div"
surface="flat"
radius="xs"
padding="md"
className="text-center"
>
<PlatformFieldLabel
variant="section"
className="block text-[11px] text-zinc-500"
>
</PlatformFieldLabel>
<div className="mt-1 text-3xl font-black tracking-[0.16em] text-[#ff4056]">
{center?.inviteCode ?? '--------'}
<PlatformAsyncStatePanel
isLoading={isLoading}
loadingState={
<div className="mt-5 space-y-3">
<div className="h-20 animate-pulse rounded-xl bg-zinc-100" />
<div className="h-10 animate-pulse rounded-xl bg-zinc-100" />
</div>
</PlatformSubpanel>
<PlatformStatusMessage
tone="warning"
surface="profile"
size="md"
className="space-y-0.5 px-3.5 font-semibold"
>
<div>
{`邀请一个用户注册,双方都可以获得${center?.rewardPoints ?? 30}泥点。`}
</div>
<div></div>
</PlatformStatusMessage>
<CopyFeedbackButton
state={copyInviteState}
onClick={onCopyInvite}
disabled={!center?.inviteCode}
idleLabel="复制邀请"
copiedLabel="已复制"
failedLabel="复制失败"
idleIcon={<Copy className="h-4 w-4" />}
actionSurface="profile"
actionSize="md"
actionFullWidth
className="gap-2 rounded-xl"
/>
<PlatformSubpanel as="div" surface="flat" radius="xs" padding="sm">
<PlatformFieldLabel
variant="section"
className="block text-zinc-900"
}
>
<div className="mt-5 space-y-3">
<PlatformSubpanel
as="div"
surface="flat"
radius="xs"
padding="md"
className="text-center"
>
</PlatformFieldLabel>
{center?.invitedUsers?.length ? (
<div className="mt-3 max-h-44 space-y-2 overflow-y-auto pr-1">
{center.invitedUsers.map((user) => (
<PlatformSubpanel
as="div"
key={`${user.userId}-${user.boundAt}`}
surface="soft"
radius="xs"
padding="row"
className="flex items-center gap-3"
>
<ProfileReferralUserAvatar
name={user.displayName}
avatarUrl={user.avatarUrl}
/>
<div className="min-w-0 flex-1">
<div className="truncate text-sm font-bold text-zinc-900">
{user.displayName || '玩家'}
</div>
</div>
</PlatformSubpanel>
))}
</div>
) : (
<PlatformEmptyState
surface="subpanel"
size="compact"
className="mt-3 text-center text-xs font-semibold leading-normal"
<PlatformFieldLabel
variant="section"
className="block text-[11px] text-zinc-500"
>
</PlatformEmptyState>
)}
</PlatformSubpanel>
</div>
</PlatformFieldLabel>
<div className="mt-1 text-3xl font-black tracking-[0.16em] text-[#ff4056]">
{center?.inviteCode ?? '--------'}
</div>
</PlatformSubpanel>
<PlatformStatusMessage
tone="warning"
surface="profile"
size="md"
className="space-y-0.5 px-3.5 font-semibold"
>
<div>
{`邀请一个用户注册,双方都可以获得${center?.rewardPoints ?? 30}泥点。`}
</div>
<div></div>
</PlatformStatusMessage>
<CopyFeedbackButton
state={copyInviteState}
onClick={onCopyInvite}
disabled={!center?.inviteCode}
idleLabel="复制邀请"
copiedLabel="已复制"
failedLabel="复制失败"
idleIcon={<Copy className="h-4 w-4" />}
actionSurface="profile"
actionSize="md"
actionFullWidth
className="gap-2 rounded-xl"
/>
<PlatformSubpanel as="div" surface="flat" radius="xs" padding="sm">
<PlatformFieldLabel
variant="section"
className="block text-zinc-900"
>
</PlatformFieldLabel>
{center?.invitedUsers?.length ? (
<div className="mt-3 max-h-44 space-y-2 overflow-y-auto pr-1">
{center.invitedUsers.map((user) => (
<PlatformSubpanel
as="div"
key={`${user.userId}-${user.boundAt}`}
surface="soft"
radius="xs"
padding="row"
className="flex items-center gap-3"
>
<ProfileReferralUserAvatar
name={user.displayName}
avatarUrl={user.avatarUrl}
/>
<div className="min-w-0 flex-1">
<div className="truncate text-sm font-bold text-zinc-900">
{user.displayName || '玩家'}
</div>
</div>
</PlatformSubpanel>
))}
</div>
) : (
<PlatformEmptyState
surface="subpanel"
size="compact"
className="mt-3 text-center text-xs font-semibold leading-normal"
>
</PlatformEmptyState>
)}
</PlatformSubpanel>
</div>
</PlatformAsyncStatePanel>
);
}

View File

@@ -1,5 +1,6 @@
import type { ProfileTaskCenterResponse } from '../../../packages/shared/src/contracts/runtime';
import { PlatformActionButton } from '../common/PlatformActionButton';
import { PlatformAsyncStatePanel } from '../common/PlatformAsyncStatePanel';
import { PlatformEmptyState } from '../common/PlatformEmptyState';
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
import { PlatformSubpanel } from '../common/PlatformSubpanel';
@@ -50,24 +51,6 @@ export function PlatformProfileTaskCenterModal({
panelClassName="platform-recharge-modal !max-w-md rounded-[1.4rem]"
bodyClassName="space-y-3 px-5 py-5"
>
{error ? (
<PlatformStatusMessage
tone="error"
surface="profile"
size="xs"
className="rounded-2xl font-semibold"
>
<div>{error}</div>
<PlatformActionButton
surface="profile"
size="xs"
className="mt-3"
onClick={onRetry}
>
</PlatformActionButton>
</PlatformStatusMessage>
) : null}
{success ? (
<PlatformStatusMessage
tone="success"
@@ -78,20 +61,45 @@ export function PlatformProfileTaskCenterModal({
{success}
</PlatformStatusMessage>
) : null}
{isLoading ? (
<div className="space-y-3">
{Array.from({ length: 2 }).map((_, index) => (
<div
key={index}
className="h-20 animate-pulse rounded-2xl bg-white/10"
/>
))}
</div>
) : tasks.length === 0 ? (
<PlatformEmptyState surface="subpanel" size="inline">
</PlatformEmptyState>
) : (
<PlatformAsyncStatePanel
errorState={
error ? (
<PlatformStatusMessage
tone="error"
surface="profile"
size="xs"
className="rounded-2xl font-semibold"
>
<div>{error}</div>
<PlatformActionButton
surface="profile"
size="xs"
className="mt-3"
onClick={onRetry}
>
</PlatformActionButton>
</PlatformStatusMessage>
) : null
}
isLoading={isLoading}
loadingState={
<div className="space-y-3">
{Array.from({ length: 2 }).map((_, index) => (
<div
key={index}
className="h-20 animate-pulse rounded-2xl bg-white/10"
/>
))}
</div>
}
isEmpty={tasks.length === 0}
emptyState={
<PlatformEmptyState surface="subpanel" size="inline">
</PlatformEmptyState>
}
>
<div className="space-y-3">
{tasks.map((task) => {
const isClaimable = task.status === 'claimable';
@@ -137,7 +145,7 @@ export function PlatformProfileTaskCenterModal({
);
})}
</div>
)}
</PlatformAsyncStatePanel>
</PlatformProfileModalShell>
);
}

View File

@@ -2,6 +2,7 @@ import { Coins } from 'lucide-react';
import type { ProfileWalletLedgerResponse } from '../../../packages/shared/src/contracts/runtime';
import { PlatformActionButton } from '../common/PlatformActionButton';
import { PlatformAsyncStatePanel } from '../common/PlatformAsyncStatePanel';
import { PlatformEmptyState } from '../common/PlatformEmptyState';
import { PlatformPillBadge } from '../common/PlatformPillBadge';
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
@@ -60,37 +61,48 @@ export function PlatformProfileWalletLedgerModal({
</PlatformPillBadge>
</div>
{error ? (
<PlatformStatusMessage tone="error" className="mt-4 rounded-xl py-3">
<div>{error}</div>
<PlatformActionButton
surface="profile"
shape="pill"
size="xs"
className="mt-3"
onClick={onRetry}
<PlatformAsyncStatePanel
errorState={
error ? (
<PlatformStatusMessage
tone="error"
className="mt-4 rounded-xl py-3"
>
<div>{error}</div>
<PlatformActionButton
surface="profile"
shape="pill"
size="xs"
className="mt-3"
onClick={onRetry}
>
</PlatformActionButton>
</PlatformStatusMessage>
) : null
}
isLoading={isLoading}
loadingState={
<div className="mt-5 space-y-3">
{Array.from({ length: 5 }).map((_, index) => (
<div
key={index}
className="h-16 animate-pulse rounded-xl bg-zinc-100"
/>
))}
</div>
}
isEmpty={entries.length === 0}
emptyState={
<PlatformEmptyState
surface="subpanel"
size="inline"
className="mt-5 py-8"
>
</PlatformActionButton>
</PlatformStatusMessage>
) : isLoading ? (
<div className="mt-5 space-y-3">
{Array.from({ length: 5 }).map((_, index) => (
<div
key={index}
className="h-16 animate-pulse rounded-xl bg-zinc-100"
/>
))}
</div>
) : entries.length === 0 ? (
<PlatformEmptyState
surface="subpanel"
size="inline"
className="mt-5 py-8"
>
</PlatformEmptyState>
) : (
</PlatformEmptyState>
}
>
<div className="mt-5 space-y-2.5">
{entries.map((entry) => (
<PlatformSubpanel
@@ -124,7 +136,7 @@ export function PlatformProfileWalletLedgerModal({
</PlatformSubpanel>
))}
</div>
)}
</PlatformAsyncStatePanel>
</PlatformProfileSecondaryModalShell>
);
}