c5200c7d45
## 变更内容 - 新增基于 shadcn open-code 模式的共享 UI canonical 组件、样式与导出。 - 新增 `/components` 共享组件展示页,并覆盖平台组件、Token、状态和移动端布局。 - 补齐平台展示页的筛选、排序、上传预览、标签、开关和异步状态交互。 - 修复排序导致预览主题变化、筛选弹窗误关闭和入口状态不更新的问题。 - 同步网站与客户端构建别名、依赖、路由测试和项目文档。 ## 验证 - `npm run test -- src/components/shared-components/SharedComponentsShowcasePage.test.tsx` - `npm run typecheck` - `npm run check:encoding` - `npm run build:raw` - `git diff --check` - pre-push 门禁通过 Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/202 Co-authored-by: kdletters <kdletters@qq.com> Co-committed-by: kdletters <kdletters@qq.com>
224 lines
5.8 KiB
TypeScript
224 lines
5.8 KiB
TypeScript
import type { ButtonHTMLAttributes, HTMLAttributes, ReactNode } from 'react';
|
|
|
|
import { PlatformFieldLabel } from './PlatformFieldLabel';
|
|
|
|
export type PlatformSubpanelStaticElement =
|
|
| 'section'
|
|
| 'div'
|
|
| 'article'
|
|
| 'aside';
|
|
export type PlatformSubpanelElement = PlatformSubpanelStaticElement | 'button';
|
|
export type PlatformSubpanelPadding =
|
|
| 'tight'
|
|
| 'row'
|
|
| 'xs'
|
|
| 'sm'
|
|
| 'md'
|
|
| 'lg'
|
|
| 'none';
|
|
export type PlatformSubpanelRadius = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
|
export type PlatformSubpanelSurface =
|
|
| 'platform'
|
|
| 'flat'
|
|
| 'soft'
|
|
| 'dark'
|
|
| 'darkSky'
|
|
| 'darkEmerald'
|
|
| 'darkAmber'
|
|
| 'darkRose'
|
|
| 'danger';
|
|
export 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';
|
|
};
|
|
|
|
export 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>
|
|
);
|
|
}
|
|
|
|
/** 平台白底子面板,统一承接结果页和创作工作台里的子面板外壳。 */
|
|
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>
|
|
);
|
|
}
|