Files
Genarrative/src/components/platform-entry/PlatformProfilePrimitives.tsx
kdletters 051fd6156c 继续扩展共享可导航行
扩展 PlatformNavigableListItem 接入 profile 设置行
补充 profile 设置行的组件级与首页集成回归测试
更新 PlatformUiKit 收口计划与共享决策记录
2026-06-11 03:22:34 +08:00

181 lines
5.3 KiB
TypeScript

import { ChevronRight } from 'lucide-react';
import type { ComponentType, ReactNode } from 'react';
import type { ProfileDashboardCardKey } from '../../../packages/shared/src/contracts/runtime';
import {
ICP_RECORD_NUMBER,
ICP_RECORD_URL,
LEGAL_DOCUMENTS,
type LegalDocumentId,
} from '../common/legalDocuments';
import { PlatformNavigableListItem } from '../common/PlatformNavigableListItem';
type ProfileStatCardProps = {
cardKey: ProfileDashboardCardKey;
label: string;
value: string;
onClick?: ((cardKey: ProfileDashboardCardKey) => void) | null;
icon: ComponentType<{ className?: string }>;
imageSrc?: string;
};
export function ProfileStatCard({
cardKey,
label,
value,
onClick,
icon,
imageSrc,
}: ProfileStatCardProps) {
const Icon = icon;
return (
<button
type="button"
onClick={onClick ? () => onClick(cardKey) : undefined}
aria-label={`${label} ${value}`}
className="platform-profile-stat-card flex min-h-[5.25rem] items-center justify-center gap-2 px-2.5 py-2.5 text-center transition"
>
<div className="platform-profile-stat-card__icon">
{imageSrc ? (
<img src={imageSrc} alt="" className="h-full w-full object-contain" />
) : (
<Icon className="h-5 w-5" />
)}
</div>
<div className="min-w-0 text-left">
<div className="platform-profile-stat-card__value whitespace-nowrap text-[16px] font-black leading-none text-[var(--platform-text-strong)]">
{value}
</div>
<div className="platform-profile-stat-card__label mt-1 whitespace-nowrap text-[11px] font-medium text-[var(--platform-text-soft)]">
{label}
</div>
</div>
</button>
);
}
export function ProfileStatCardSkeleton() {
return (
<div className="platform-subpanel flex min-h-[5.75rem] flex-col items-center justify-center rounded-[1.35rem] px-3 py-3 text-center">
<div className="h-4 w-20 animate-pulse rounded-full bg-[var(--platform-subpanel-border)]" />
<div className="mt-2 h-7 w-16 animate-pulse rounded-full bg-[var(--platform-line-soft)]" />
</div>
);
}
type ProfileShortcutButtonProps = {
label: string;
subLabel?: ReactNode;
icon: ComponentType<{ className?: string }>;
onClick?: (() => void) | null;
imageSrc?: string;
};
export function ProfileShortcutButton({
label,
subLabel,
icon,
onClick,
imageSrc,
}: ProfileShortcutButtonProps) {
const Icon = icon;
return (
<button
type="button"
onClick={onClick ?? undefined}
className="platform-profile-shortcut-button flex min-h-[4.75rem] w-full flex-col items-center justify-center gap-1.5 px-2 py-2.5 text-center transition"
>
<div className="platform-profile-shortcut-button__icon">
{imageSrc ? (
<img src={imageSrc} alt="" className="h-full w-full object-contain" />
) : (
<Icon className="h-[1.125rem] w-[1.125rem]" />
)}
</div>
<div className="platform-profile-shortcut-button__label whitespace-nowrap text-[12px] font-semibold text-[var(--platform-text-strong)]">
{label}
</div>
{subLabel ? (
<div className="platform-profile-shortcut-button__sub-label flex min-h-4 items-center justify-center gap-1 whitespace-nowrap text-[10px] font-medium text-[var(--platform-text-soft)]">
{subLabel}
</div>
) : null}
</button>
);
}
type ProfileSettingsRowProps = {
label: string;
icon: ComponentType<{ className?: string }>;
onClick: () => void;
};
export function ProfileSettingsRow({
label,
icon,
onClick,
}: ProfileSettingsRowProps) {
const Icon = icon;
return (
<PlatformNavigableListItem
onClick={onClick}
className="platform-profile-settings-row px-4 py-3 transition"
leading={
<span className="platform-profile-settings-row__icon">
<Icon className="h-4 w-4" />
</span>
}
bodyClassName="flex min-w-0 items-center"
trailing={
<ChevronRight className="h-4 w-4 shrink-0 text-[var(--platform-text-soft)]" />
}
>
<span className="truncate text-[14px] font-semibold text-[var(--platform-text-strong)]">
{label}
</span>
</PlatformNavigableListItem>
);
}
type ProfileLegalSectionProps = {
onOpenDocument: (documentId: LegalDocumentId) => void;
};
export function ProfileLegalSection({
onOpenDocument,
}: ProfileLegalSectionProps) {
return (
<section className="platform-profile-legal-strip" aria-label="法律信息">
<div className="platform-profile-legal-strip__links">
{LEGAL_DOCUMENTS.map((document, index) => (
<button
key={document.id}
type="button"
onClick={() => onOpenDocument(document.id)}
className="platform-profile-legal-strip__link"
>
{document.title}
{index < LEGAL_DOCUMENTS.length - 1 ? (
<span
aria-hidden="true"
className="platform-profile-legal-strip__divider"
/>
) : null}
</button>
))}
</div>
<a
href={ICP_RECORD_URL}
target="_blank"
rel="noreferrer"
className="platform-profile-legal-strip__record"
>
{ICP_RECORD_NUMBER}
</a>
</section>
);
}