复用空态原语收口项目新建卡片

PlatformEmptyState 增加 button 形态,支持空列表 CTA 复用同一套空态 chrome。

项目页空列表新建项目卡片改为复用 PlatformEmptyState,保留项目页局部尺寸样式。

补充共享空态按钮形态和项目页空态创建测试,并更新 TRACKING。
This commit is contained in:
2026-06-14 15:21:23 +08:00
parent d1f1cfdcca
commit d4ceb084c9
5 changed files with 124 additions and 22 deletions

View File

@@ -101,3 +101,4 @@
- 2026-06-14 组件复用修正:编辑器生成图右上角元数据角标改为复用 `PlatformIconButton asChild="spanButton"`,保留嵌套在图层按钮内的合法 DOM 结构,同时把 Enter / Space 键盘触发和 `darkMini` chrome 收口到共享组件;验证命令:`npm run test -- src/components/common/PlatformIconButton.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx``npm run typecheck` - 2026-06-14 组件复用修正:编辑器生成图右上角元数据角标改为复用 `PlatformIconButton asChild="spanButton"`,保留嵌套在图层按钮内的合法 DOM 结构,同时把 Enter / Space 键盘触发和 `darkMini` chrome 收口到共享组件;验证命令:`npm run test -- src/components/common/PlatformIconButton.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx``npm run typecheck`
- 2026-06-14 组件复用修正:编辑器画布内生成输入框和“修改图片”弹窗提示词改为复用 `PlatformTextField variant="textarea"`,删除编辑器里按标签选择器手写 textarea 基础输入 chrome 的做法,仅保留生成器局部尺寸和 Lovart 式覆盖;验证命令:`npm run test -- src/components/image-editor/ImageCanvasEditorView.test.tsx src/components/common/PlatformTextField.test.tsx``npm run typecheck` - 2026-06-14 组件复用修正:编辑器画布内生成输入框和“修改图片”弹窗提示词改为复用 `PlatformTextField variant="textarea"`,删除编辑器里按标签选择器手写 textarea 基础输入 chrome 的做法,仅保留生成器局部尺寸和 Lovart 式覆盖;验证命令:`npm run test -- src/components/image-editor/ImageCanvasEditorView.test.tsx src/components/common/PlatformTextField.test.tsx``npm run typecheck`
- 2026-06-14 组件复用修正:新增 `PlatformInlineOptionButton`,编辑器生成输入框里的比例和模型选择 pill 改为复用平台内联选项按钮原语,删除两个局部按钮重复维护基础 inline chrome 的做法;验证命令:`npm run test -- src/components/common/PlatformInlineOptionButton.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx``npm run typecheck` - 2026-06-14 组件复用修正:新增 `PlatformInlineOptionButton`,编辑器生成输入框里的比例和模型选择 pill 改为复用平台内联选项按钮原语,删除两个局部按钮重复维护基础 inline chrome 的做法;验证命令:`npm run test -- src/components/common/PlatformInlineOptionButton.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx``npm run typecheck`
- 2026-06-14 组件复用修正:`PlatformEmptyState` 增加 `asChild="button"` 形态,项目页空列表“新建项目”卡片改为复用平台空态原语,避免项目页单独维护可点击空态卡片基础 chrome验证命令`npm run test -- src/components/common/PlatformEmptyState.test.tsx src/components/project/ProjectGalleryView.test.tsx``npm run typecheck`

View File

@@ -1,7 +1,7 @@
/* @vitest-environment jsdom */ /* @vitest-environment jsdom */
import { render, screen } from '@testing-library/react'; import { fireEvent, render, screen } from '@testing-library/react';
import { expect, test } from 'vitest'; import { expect, test, vi } from 'vitest';
import { PlatformEmptyState } from './PlatformEmptyState'; import { PlatformEmptyState } from './PlatformEmptyState';
@@ -71,3 +71,29 @@ test('supports dark editor dashed empty state', () => {
expect(emptyState.className).toContain('bg-black/20'); expect(emptyState.className).toContain('bg-black/20');
expect(emptyState.className).toContain('text-[var(--platform-text-soft)]'); expect(emptyState.className).toContain('text-[var(--platform-text-soft)]');
}); });
test('supports button empty state for empty collection actions', () => {
const onClick = vi.fn();
render(
<PlatformEmptyState
asChild="button"
surface="subpanel"
size="panel"
className="local-empty-action"
onClick={onClick}
>
</PlatformEmptyState>,
);
const button = screen.getByRole('button', { name: '新建项目' });
expect(button.tagName).toBe('BUTTON');
expect(button.getAttribute('type')).toBe('button');
expect(button.className).toContain('platform-empty-state');
expect(button.className).toContain('local-empty-action');
fireEvent.click(button);
expect(onClick).toHaveBeenCalledTimes(1);
});

View File

@@ -1,19 +1,41 @@
import type { HTMLAttributes, ReactNode } from 'react'; import type { ButtonHTMLAttributes, HTMLAttributes, ReactNode } from 'react';
type PlatformEmptyStateSurface = 'soft' | 'dashed' | 'subpanel' | 'editorDark'; type PlatformEmptyStateSurface = 'soft' | 'dashed' | 'subpanel' | 'editorDark';
type PlatformEmptyStateSize = 'compact' | 'panel' | 'inline'; type PlatformEmptyStateSize = 'compact' | 'panel' | 'inline';
type PlatformEmptyStateTone = 'base' | 'soft'; type PlatformEmptyStateTone = 'base' | 'soft';
type PlatformEmptyStateProps = Omit< type PlatformEmptyStateBaseProps = {
HTMLAttributes<HTMLDivElement>,
'children'
> & {
children: ReactNode; children: ReactNode;
surface?: PlatformEmptyStateSurface; surface?: PlatformEmptyStateSurface;
size?: PlatformEmptyStateSize; size?: PlatformEmptyStateSize;
tone?: PlatformEmptyStateTone; tone?: PlatformEmptyStateTone;
}; };
type PlatformEmptyStateDivProps = Omit<
HTMLAttributes<HTMLDivElement>,
'children'
> & {
asChild?: false;
} & PlatformEmptyStateBaseProps;
type PlatformEmptyStateButtonProps = Omit<
ButtonHTMLAttributes<HTMLButtonElement>,
'children'
> & {
asChild: 'button';
} & PlatformEmptyStateBaseProps;
type PlatformEmptyStateProps =
| PlatformEmptyStateDivProps
| PlatformEmptyStateButtonProps;
type PlatformEmptyStateClassNameOptions = {
surface: PlatformEmptyStateSurface;
size: PlatformEmptyStateSize;
tone: PlatformEmptyStateTone;
className?: string;
};
const PLATFORM_EMPTY_STATE_SURFACE_CLASS: Record< const PLATFORM_EMPTY_STATE_SURFACE_CLASS: Record<
PlatformEmptyStateSurface, PlatformEmptyStateSurface,
string string
@@ -44,30 +66,57 @@ const PLATFORM_EMPTY_STATE_TONE_CLASS: Record<PlatformEmptyStateTone, string> =
* 平台通用空态和轻量加载态。 * 平台通用空态和轻量加载态。
* 收口平台列表、作品架和素材选择弹窗中重复的空面板外观。 * 收口平台列表、作品架和素材选择弹窗中重复的空面板外观。
*/ */
function getPlatformEmptyStateClassName({
surface,
size,
tone,
className,
}: PlatformEmptyStateClassNameOptions) {
return [
'min-w-0',
'platform-empty-state',
PLATFORM_EMPTY_STATE_SURFACE_CLASS[surface],
PLATFORM_EMPTY_STATE_SIZE_CLASS[size],
PLATFORM_EMPTY_STATE_TONE_CLASS[tone],
className,
]
.filter(Boolean)
.join(' ');
}
export function PlatformEmptyState({ export function PlatformEmptyState({
children, children,
surface = 'soft', surface = 'soft',
size = 'compact', size = 'compact',
tone, tone,
className, className,
...divProps asChild,
...emptyStateProps
}: PlatformEmptyStateProps) { }: PlatformEmptyStateProps) {
const resolvedTone = const resolvedTone =
tone ?? (surface === 'subpanel' || size === 'inline' ? 'soft' : 'base'); tone ?? (surface === 'subpanel' || size === 'inline' ? 'soft' : 'base');
const emptyStateClassName = getPlatformEmptyStateClassName({
surface,
size,
tone: resolvedTone,
className,
});
if (asChild === 'button') {
const { type = 'button', ...buttonProps } =
emptyStateProps as ButtonHTMLAttributes<HTMLButtonElement>;
return (
<button {...buttonProps} type={type} className={emptyStateClassName}>
{children}
</button>
);
}
return ( return (
<div <div
{...divProps} {...(emptyStateProps as HTMLAttributes<HTMLDivElement>)}
className={[ className={emptyStateClassName}
'min-w-0',
'platform-empty-state',
PLATFORM_EMPTY_STATE_SURFACE_CLASS[surface],
PLATFORM_EMPTY_STATE_SIZE_CLASS[size],
PLATFORM_EMPTY_STATE_TONE_CLASS[resolvedTone],
className,
]
.filter(Boolean)
.join(' ')}
> >
{children} {children}
</div> </div>

View File

@@ -134,4 +134,28 @@ describe('ProjectGalleryView', () => {
expect(deleteEditorProjectMock).toHaveBeenCalledWith('editor-project-1'); expect(deleteEditorProjectMock).toHaveBeenCalledWith('editor-project-1');
expect(deleteEditorProjectMock).toHaveBeenCalledWith('editor-project-2'); expect(deleteEditorProjectMock).toHaveBeenCalledWith('editor-project-2');
}); });
it('renders the empty project action with shared empty state chrome', async () => {
const onOpenProject = vi.fn();
listEditorProjectsMock.mockResolvedValueOnce([]);
createEditorProjectMock.mockResolvedValueOnce({
...projectItems[0],
projectId: 'editor-project-new',
});
const user = userEvent.setup();
render(<ProjectGalleryView onOpenProject={onOpenProject} />);
const newProjectButton = await screen.findByRole('button', {
name: '新建项目',
});
expect(newProjectButton.className).toContain('platform-empty-state');
expect(newProjectButton.className).toContain('project-gallery__new-card');
await user.click(newProjectButton);
expect(createEditorProjectMock).toHaveBeenCalledTimes(1);
expect(onOpenProject).toHaveBeenCalledWith('editor-project-new');
});
}); });

View File

@@ -327,14 +327,16 @@ export function ProjectGalleryView({ onOpenProject }: ProjectGalleryViewProps) {
</PlatformEmptyState> </PlatformEmptyState>
) : projects.length === 0 ? ( ) : projects.length === 0 ? (
<button <PlatformEmptyState
type="button" asChild="button"
surface="subpanel"
size="panel"
className="project-gallery__new-card" className="project-gallery__new-card"
onClick={createProject} onClick={createProject}
> >
<Plus className="h-6 w-6" /> <Plus className="h-6 w-6" />
<span></span> <span></span>
</button> </PlatformEmptyState>
) : ( ) : (
<section className="project-gallery__grid">{projectCards}</section> <section className="project-gallery__grid">{projectCards}</section>
)} )}