收口前端平台组件库能力
新增 PlatformUiKit 通用弹窗、按钮、状态、空态、媒体、表单和标签等公共组件 迁移结果页、创作工作台、认证入口、RPG 暗色面板和运行态弹窗的重复 UI chrome 补充组件测试、页面回归测试、技术文档和 Hermes 共享决策记录
This commit is contained in:
222
src/components/common/PlatformSubpanel.tsx
Normal file
222
src/components/common/PlatformSubpanel.tsx
Normal file
@@ -0,0 +1,222 @@
|
||||
import type { ButtonHTMLAttributes, HTMLAttributes, ReactNode } from 'react';
|
||||
|
||||
import { PlatformFieldLabel } from './PlatformFieldLabel';
|
||||
|
||||
type PlatformSubpanelStaticElement = 'section' | 'div' | 'article' | 'aside';
|
||||
type PlatformSubpanelElement = PlatformSubpanelStaticElement | 'button';
|
||||
type PlatformSubpanelPadding =
|
||||
| 'tight'
|
||||
| 'row'
|
||||
| 'xs'
|
||||
| 'sm'
|
||||
| 'md'
|
||||
| 'lg'
|
||||
| 'none';
|
||||
type PlatformSubpanelRadius = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
||||
type PlatformSubpanelSurface =
|
||||
| 'platform'
|
||||
| 'flat'
|
||||
| 'soft'
|
||||
| 'dark'
|
||||
| 'darkSky'
|
||||
| 'darkEmerald'
|
||||
| 'darkAmber'
|
||||
| 'darkRose'
|
||||
| 'danger';
|
||||
type PlatformSubpanelTitleVariant = 'section' | 'strong';
|
||||
|
||||
type PlatformSubpanelBaseProps = {
|
||||
as?: PlatformSubpanelElement;
|
||||
title?: ReactNode;
|
||||
titleVariant?: PlatformSubpanelTitleVariant;
|
||||
actions?: ReactNode;
|
||||
children: ReactNode;
|
||||
interactive?: boolean;
|
||||
padding?: PlatformSubpanelPadding;
|
||||
radius?: PlatformSubpanelRadius;
|
||||
surface?: PlatformSubpanelSurface;
|
||||
className?: string;
|
||||
headerClassName?: string;
|
||||
titleClassName?: string;
|
||||
actionsClassName?: string;
|
||||
bodyClassName?: string;
|
||||
};
|
||||
|
||||
type PlatformSubpanelStaticProps = PlatformSubpanelBaseProps &
|
||||
Omit<HTMLAttributes<HTMLElement>, 'children' | 'className' | 'title'> & {
|
||||
as?: PlatformSubpanelStaticElement;
|
||||
};
|
||||
|
||||
type PlatformSubpanelButtonProps = PlatformSubpanelBaseProps &
|
||||
Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'children' | 'title'> & {
|
||||
as: 'button';
|
||||
};
|
||||
|
||||
type PlatformSubpanelProps =
|
||||
| PlatformSubpanelStaticProps
|
||||
| PlatformSubpanelButtonProps;
|
||||
|
||||
const PLATFORM_SUBPANEL_PADDING_CLASS: Record<PlatformSubpanelPadding, string> =
|
||||
{
|
||||
tight: 'p-2',
|
||||
row: 'px-3 py-2',
|
||||
xs: 'px-3 py-2.5',
|
||||
sm: 'p-3',
|
||||
md: 'p-4',
|
||||
lg: 'p-4 sm:p-5',
|
||||
none: 'p-0',
|
||||
};
|
||||
|
||||
const PLATFORM_SUBPANEL_RADIUS_CLASS: Record<PlatformSubpanelRadius, string> = {
|
||||
xs: 'rounded-xl',
|
||||
sm: 'rounded-[1rem]',
|
||||
md: 'rounded-[1.25rem]',
|
||||
lg: 'rounded-[1.35rem]',
|
||||
xl: 'rounded-[1.5rem]',
|
||||
};
|
||||
|
||||
const PLATFORM_SUBPANEL_SURFACE_CLASS: Record<PlatformSubpanelSurface, string> =
|
||||
{
|
||||
platform: 'platform-subpanel',
|
||||
flat: 'border border-[var(--platform-subpanel-border)] bg-white/72',
|
||||
soft: 'border border-[var(--platform-subpanel-border)] bg-white/68',
|
||||
dark: 'border border-white/10 bg-black/25 text-zinc-100',
|
||||
darkSky: 'border border-sky-400/18 bg-sky-500/8 text-sky-50',
|
||||
darkEmerald:
|
||||
'border border-emerald-400/18 bg-emerald-500/8 text-emerald-100/85',
|
||||
darkAmber: 'border border-amber-300/18 bg-amber-500/8 text-amber-50',
|
||||
darkRose: 'border border-rose-300/18 bg-rose-500/8 text-rose-50',
|
||||
danger:
|
||||
'border border-[var(--platform-button-danger-border)] bg-[var(--platform-button-danger-fill)]',
|
||||
};
|
||||
|
||||
const PLATFORM_SUBPANEL_INTERACTIVE_CLASS =
|
||||
'text-left transition hover:bg-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--platform-warm-border)] disabled:cursor-not-allowed disabled:opacity-55';
|
||||
|
||||
function renderSubpanelTitle({
|
||||
title,
|
||||
titleClassName,
|
||||
titleVariant,
|
||||
}: {
|
||||
title: ReactNode;
|
||||
titleClassName?: string;
|
||||
titleVariant: PlatformSubpanelTitleVariant;
|
||||
}) {
|
||||
if (titleVariant === 'strong') {
|
||||
return (
|
||||
<div
|
||||
className={[
|
||||
'text-sm font-black text-[var(--platform-text-strong)]',
|
||||
titleClassName,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PlatformFieldLabel variant="section" className={titleClassName}>
|
||||
{title}
|
||||
</PlatformFieldLabel>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 平台白底子面板。
|
||||
* 统一承接结果页和创作工作台里的 subpanel 外壳、标题行和右侧动作区。
|
||||
*/
|
||||
export function PlatformSubpanel({
|
||||
as: Component = 'section',
|
||||
title,
|
||||
titleVariant = 'section',
|
||||
actions,
|
||||
children,
|
||||
interactive = false,
|
||||
padding = 'md',
|
||||
radius = 'md',
|
||||
surface = 'platform',
|
||||
className,
|
||||
headerClassName,
|
||||
titleClassName,
|
||||
actionsClassName,
|
||||
bodyClassName,
|
||||
...elementProps
|
||||
}: PlatformSubpanelProps) {
|
||||
const hasHeader = Boolean(title) || Boolean(actions);
|
||||
const subpanelClassName = [
|
||||
PLATFORM_SUBPANEL_SURFACE_CLASS[surface],
|
||||
PLATFORM_SUBPANEL_RADIUS_CLASS[radius],
|
||||
PLATFORM_SUBPANEL_PADDING_CLASS[padding],
|
||||
interactive ? PLATFORM_SUBPANEL_INTERACTIVE_CLASS : null,
|
||||
className,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
const content = (
|
||||
<>
|
||||
{hasHeader ? (
|
||||
<div
|
||||
className={[
|
||||
'flex items-center justify-between gap-3',
|
||||
headerClassName,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
>
|
||||
<div className="min-w-0">
|
||||
{title
|
||||
? renderSubpanelTitle({
|
||||
title,
|
||||
titleClassName,
|
||||
titleVariant,
|
||||
})
|
||||
: null}
|
||||
</div>
|
||||
{actions ? (
|
||||
<div
|
||||
className={['flex shrink-0 items-center gap-2', actionsClassName]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
>
|
||||
{actions}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{bodyClassName ? (
|
||||
<div className={bodyClassName}>{children}</div>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
if (Component === 'button') {
|
||||
const { type = 'button', ...buttonProps } = elementProps as Omit<
|
||||
ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
'children' | 'title'
|
||||
>;
|
||||
|
||||
return (
|
||||
<button {...buttonProps} type={type} className={subpanelClassName}>
|
||||
{content}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
const StaticComponent = Component;
|
||||
const staticProps = elementProps as Omit<
|
||||
HTMLAttributes<HTMLElement>,
|
||||
'children' | 'className' | 'title'
|
||||
>;
|
||||
|
||||
return (
|
||||
<StaticComponent {...staticProps} className={subpanelClassName}>
|
||||
{content}
|
||||
</StaticComponent>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user