Files
Genarrative/src/components/common/PlatformUploadTile.tsx
kdletters 1ad25e30f8 收口前端平台组件库能力
新增 PlatformUiKit 通用弹窗、按钮、状态、空态、媒体、表单和标签等公共组件
迁移结果页、创作工作台、认证入口、RPG 暗色面板和运行态弹窗的重复 UI chrome
补充组件测试、页面回归测试、技术文档和 Hermes 共享决策记录
2026-06-10 10:24:18 +08:00

147 lines
3.7 KiB
TypeScript

import { ImagePlus } from 'lucide-react';
import type {
ButtonHTMLAttributes,
LabelHTMLAttributes,
ReactNode,
} from 'react';
type PlatformUploadTileBaseProps = {
label: ReactNode;
hint?: ReactNode;
icon?: ReactNode;
disabled?: boolean;
showLabel?: boolean;
size?: 'square' | 'compact' | 'panel';
surface?: 'platform' | 'editorDark';
};
type PlatformUploadTileButtonProps = Omit<
ButtonHTMLAttributes<HTMLButtonElement>,
'children' | 'disabled'
> &
PlatformUploadTileBaseProps & {
asChild?: false;
};
type PlatformUploadTileLabelProps = Omit<
LabelHTMLAttributes<HTMLLabelElement>,
'children'
> &
PlatformUploadTileBaseProps & {
asChild: 'label';
};
type PlatformUploadTileProps =
| PlatformUploadTileButtonProps
| PlatformUploadTileLabelProps;
function getPlatformUploadTileClassName(
size: 'square' | 'compact' | 'panel',
surface: 'platform' | 'editorDark',
className?: string,
disabled?: boolean,
) {
const surfaceClassName = {
platform:
'border-[var(--platform-subpanel-border)] bg-[var(--platform-input-fill)] text-[var(--platform-text-soft)] hover:border-[var(--platform-surface-hover-border)] hover:text-[var(--platform-text-strong)]',
editorDark:
'border-white/12 bg-black/20 text-zinc-300 hover:border-sky-300/45 hover:bg-white/8 hover:text-white',
}[surface];
const sizeClassName = {
square: 'h-[5.75rem] w-[5.75rem] items-center text-center',
compact: 'h-12 w-full items-center text-center',
panel: 'min-h-[7rem] w-full items-start px-4 py-4 text-left',
}[size];
return [
'flex flex-col justify-center rounded-xl border border-dashed transition',
surfaceClassName,
sizeClassName,
disabled ? 'cursor-not-allowed opacity-55' : 'cursor-pointer',
className,
]
.filter(Boolean)
.join(' ');
}
/**
* 平台通用上传方块。
* 统一承载图片 / 附件上传入口的虚线方块、图标、主副文案和可访问语义。
*/
export function PlatformUploadTile({
label,
hint,
icon = <ImagePlus className="h-6 w-6" />,
disabled = false,
showLabel = true,
size = 'square',
surface = 'platform',
className,
asChild,
...actionProps
}: PlatformUploadTileProps) {
const tileClassName = getPlatformUploadTileClassName(
size,
surface,
className,
disabled,
);
const hasIcon = Boolean(icon);
const labelClassName =
size === 'panel'
? [hasIcon ? 'mt-2' : null, 'text-[11px] font-bold tracking-[0.14em]']
.filter(Boolean)
.join(' ')
: [hasIcon ? 'mt-2' : null, 'text-xs font-medium']
.filter(Boolean)
.join(' ');
const hintClassName =
size === 'panel'
? 'mt-2 text-xs leading-5 opacity-80'
: 'mt-0.5 text-[11px] opacity-80';
const content = (
<>
{hasIcon ? icon : null}
<span className={showLabel ? labelClassName : 'sr-only'}>{label}</span>
{hint ? <span className={hintClassName}>{hint}</span> : null}
</>
);
if (asChild === 'label') {
const { onClick, ...labelProps } =
actionProps as LabelHTMLAttributes<HTMLLabelElement>;
return (
<label
{...labelProps}
onClick={(event) => {
if (disabled) {
event.preventDefault();
return;
}
onClick?.(event);
}}
aria-disabled={disabled || undefined}
className={tileClassName}
>
{content}
</label>
);
}
const { type = 'button', ...buttonProps } =
actionProps as ButtonHTMLAttributes<HTMLButtonElement>;
return (
<button
{...buttonProps}
type={type}
disabled={disabled}
className={tileClassName}
>
{content}
</button>
);
}