新增 PlatformUiKit 通用弹窗、按钮、状态、空态、媒体、表单和标签等公共组件 迁移结果页、创作工作台、认证入口、RPG 暗色面板和运行态弹窗的重复 UI chrome 补充组件测试、页面回归测试、技术文档和 Hermes 共享决策记录
164 lines
4.1 KiB
TypeScript
164 lines
4.1 KiB
TypeScript
import {
|
|
forwardRef,
|
|
type HTMLAttributes,
|
|
type ImgHTMLAttributes,
|
|
type ReactNode,
|
|
} from 'react';
|
|
|
|
import { ResolvedAssetImage } from '../ResolvedAssetImage';
|
|
|
|
type PlatformMediaFrameAspect =
|
|
| 'auto'
|
|
| 'square'
|
|
| 'standard'
|
|
| 'landscape'
|
|
| 'wide'
|
|
| 'portrait'
|
|
| 'video';
|
|
type PlatformMediaFrameSurface =
|
|
| 'warm'
|
|
| 'editorDark'
|
|
| 'plain'
|
|
| 'soft'
|
|
| 'bright'
|
|
| 'none'
|
|
| 'bare';
|
|
|
|
type PlatformMediaFrameProps = Omit<
|
|
HTMLAttributes<HTMLDivElement>,
|
|
'children' | 'className'
|
|
> & {
|
|
src?: string | null;
|
|
fallbackSrc?: string | null;
|
|
alt: string;
|
|
fallbackLabel: string;
|
|
aspect?: PlatformMediaFrameAspect;
|
|
surface?: PlatformMediaFrameSurface;
|
|
loading?: 'eager' | 'lazy';
|
|
refreshKey?: string | number | null;
|
|
imageClassName?: string;
|
|
imageProps?: Omit<
|
|
ImgHTMLAttributes<HTMLImageElement>,
|
|
'alt' | 'className' | 'loading' | 'src'
|
|
>;
|
|
className?: string;
|
|
fallbackClassName?: string;
|
|
fallbackShellClassName?: string;
|
|
fallbackContent?: ReactNode;
|
|
children?: ReactNode;
|
|
previewOverlay?: ReactNode;
|
|
overlayInteractive?: boolean;
|
|
};
|
|
|
|
const PLATFORM_MEDIA_FRAME_ASPECT_CLASS: Record<
|
|
PlatformMediaFrameAspect,
|
|
string
|
|
> = {
|
|
auto: '',
|
|
square: 'aspect-square',
|
|
standard: 'aspect-[4/3]',
|
|
landscape: 'aspect-[16/9]',
|
|
wide: 'aspect-[9/5]',
|
|
portrait: 'aspect-[9/16]',
|
|
video: 'aspect-video',
|
|
};
|
|
|
|
const PLATFORM_MEDIA_FRAME_SURFACE_CLASS: Record<
|
|
PlatformMediaFrameSurface,
|
|
string
|
|
> = {
|
|
warm: 'border border-[var(--platform-subpanel-border)] bg-[radial-gradient(circle_at_top,rgba(255,255,255,0.22),transparent_42%),linear-gradient(180deg,rgba(204,117,76,0.9),rgba(223,127,64,0.82))]',
|
|
editorDark:
|
|
'border border-white/10 bg-[radial-gradient(circle_at_top,rgba(56,189,248,0.16),transparent_48%),linear-gradient(180deg,rgba(19,24,39,0.95),rgba(8,10,17,0.92))]',
|
|
plain:
|
|
'border border-[var(--platform-subpanel-border)] bg-[var(--platform-subpanel-fill)]',
|
|
soft: 'border border-[var(--platform-subpanel-border)] bg-white/68',
|
|
bright: 'border border-[var(--platform-subpanel-border)] bg-white/82',
|
|
none: '',
|
|
bare: 'bg-[var(--platform-subpanel-fill)]',
|
|
};
|
|
|
|
/**
|
|
* 平台媒体预览框。
|
|
* 统一承接图片预览、固定比例、fallback 文案和可选 overlay。
|
|
*/
|
|
export const PlatformMediaFrame = forwardRef<
|
|
HTMLDivElement,
|
|
PlatformMediaFrameProps
|
|
>(function PlatformMediaFrame(
|
|
{
|
|
src,
|
|
fallbackSrc,
|
|
alt,
|
|
fallbackLabel,
|
|
aspect = 'square',
|
|
surface = 'warm',
|
|
loading,
|
|
refreshKey,
|
|
imageClassName = 'h-full w-full object-cover',
|
|
imageProps,
|
|
className,
|
|
fallbackClassName,
|
|
fallbackShellClassName,
|
|
fallbackContent,
|
|
children,
|
|
previewOverlay,
|
|
overlayInteractive = false,
|
|
...containerProps
|
|
},
|
|
ref,
|
|
) {
|
|
const imageSrc = src?.trim() || fallbackSrc?.trim() || '';
|
|
const hasOverlay = Boolean(children) || Boolean(previewOverlay);
|
|
|
|
return (
|
|
<div
|
|
{...containerProps}
|
|
ref={ref}
|
|
className={[
|
|
'platform-media-frame relative overflow-hidden rounded-2xl',
|
|
PLATFORM_MEDIA_FRAME_SURFACE_CLASS[surface],
|
|
PLATFORM_MEDIA_FRAME_ASPECT_CLASS[aspect],
|
|
className,
|
|
]
|
|
.filter(Boolean)
|
|
.join(' ')}
|
|
>
|
|
{imageSrc ? (
|
|
<ResolvedAssetImage
|
|
{...imageProps}
|
|
src={src?.trim() || undefined}
|
|
fallbackSrc={fallbackSrc?.trim() || undefined}
|
|
alt={alt}
|
|
loading={loading}
|
|
refreshKey={refreshKey}
|
|
className={imageClassName}
|
|
/>
|
|
) : (
|
|
<div
|
|
className={[
|
|
'flex h-full w-full items-center justify-center px-4 text-center text-sm font-semibold tracking-[0.18em] text-zinc-400',
|
|
fallbackShellClassName,
|
|
fallbackClassName,
|
|
]
|
|
.filter(Boolean)
|
|
.join(' ')}
|
|
>
|
|
{fallbackContent ?? fallbackLabel}
|
|
</div>
|
|
)}
|
|
{hasOverlay ? (
|
|
<div
|
|
className={[
|
|
overlayInteractive ? 'pointer-events-auto' : 'pointer-events-none',
|
|
'absolute inset-0',
|
|
].join(' ')}
|
|
>
|
|
{previewOverlay}
|
|
{children}
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
);
|
|
});
|