统一音频素材封面缩略图

新增音频素材统一封面图。
素材库、创作主页精选和后台缩略图改为使用音频封面。
补充音频封面展示回归测试。
This commit is contained in:
2026-07-06 14:10:16 +08:00
parent b288b091b2
commit 0b766c8bf9
12 changed files with 717 additions and 361 deletions
@@ -1,14 +1,20 @@
/* @vitest-environment jsdom */
import {fireEvent, render, screen, waitFor, within} from '@testing-library/react';
import {beforeEach, expect, test, vi} from 'vitest';
import {
fireEvent,
render,
screen,
waitFor,
within,
} from '@testing-library/react';
import { beforeEach, expect, test, vi } from 'vitest';
import {
getAdminAssetReadUrl,
listAdminEditorAssets,
} from '../api/adminApiClient';
import type {AdminEditorAssetPayload} from '../api/adminApiTypes';
import {AdminEditorAssetQueryPage} from './AdminEditorAssetQueryPage';
import type { AdminEditorAssetPayload } from '../api/adminApiTypes';
import { AdminEditorAssetQueryPage } from './AdminEditorAssetQueryPage';
vi.mock('../api/adminApiClient', () => ({
getAdminAssetReadUrl: vi.fn(),
@@ -34,7 +40,7 @@ const generatedAsset: AdminEditorAssetPayload = {
provider: 'character',
taskId: 'task-1',
assetKind: 'character',
generationInputs: {style: 'clay'},
generationInputs: { style: 'clay' },
sourceResourceId: 'resource-1',
thumbnailSrc: null,
generationCostMudPoints: 12,
@@ -60,10 +66,7 @@ beforeEach(() => {
test('后台素材查询展示作者昵称和陶泥号', async () => {
render(
<AdminEditorAssetQueryPage
token="admin-token"
onUnauthorized={vi.fn()}
/>,
<AdminEditorAssetQueryPage token="admin-token" onUnauthorized={vi.fn()} />,
);
expect(await screen.findByText('作者昵称')).toBeTruthy();
@@ -73,25 +76,22 @@ test('后台素材查询展示作者昵称和陶泥号', async () => {
test('后台素材查询按用户、搜索和时间调用查询接口', async () => {
render(
<AdminEditorAssetQueryPage
token="admin-token"
onUnauthorized={vi.fn()}
/>,
<AdminEditorAssetQueryPage token="admin-token" onUnauthorized={vi.fn()} />,
);
await screen.findByRole('img', {name: '素材:角色形象 1'});
await screen.findByRole('img', { name: '素材:角色形象 1' });
fireEvent.change(screen.getByLabelText('用户 ID'), {
target: {value: 'user-1'},
target: { value: 'user-1' },
});
fireEvent.change(screen.getByLabelText('搜索'), {
target: {value: '陶泥角色'},
target: { value: '陶泥角色' },
});
fireEvent.change(screen.getByLabelText('开始时间'), {
target: {value: '2026-07-01'},
target: { value: '2026-07-01' },
});
fireEvent.change(screen.getByLabelText('结束时间'), {
target: {value: '2026-07-04'},
target: { value: '2026-07-04' },
});
await waitFor(() => {
@@ -107,26 +107,20 @@ test('后台素材查询按用户、搜索和时间调用查询接口', async ()
test('后台素材查询不展示分类筛选和分类列', async () => {
render(
<AdminEditorAssetQueryPage
token="admin-token"
onUnauthorized={vi.fn()}
/>,
<AdminEditorAssetQueryPage token="admin-token" onUnauthorized={vi.fn()} />,
);
expect(await screen.findByText('角色形象 1')).toBeTruthy();
expect(screen.queryByLabelText('分类')).toBeNull();
expect(screen.queryByRole('columnheader', {name: '分类'})).toBeNull();
expect(screen.queryByRole('columnheader', { name: '分类' })).toBeNull();
});
test('后台素材查询缩略图使用 objectKey 换签后展示', async () => {
render(
<AdminEditorAssetQueryPage
token="admin-token"
onUnauthorized={vi.fn()}
/>,
<AdminEditorAssetQueryPage token="admin-token" onUnauthorized={vi.fn()} />,
);
const image = await screen.findByRole('img', {name: '素材:角色形象 1'});
const image = await screen.findByRole('img', { name: '素材:角色形象 1' });
await waitFor(() => {
expect(image.getAttribute('src')).toBe(
@@ -139,6 +133,32 @@ test('后台素材查询缩略图使用 objectKey 换签后展示', async () =>
});
});
test('后台素材查询音频素材使用统一封面缩略图', async () => {
vi.mocked(listAdminEditorAssets).mockResolvedValueOnce({
entries: [
{
...generatedAsset,
assetId: 'asset-audio-1',
label: '胜利音效',
imageSrc: '/generated-editor-audios/sfx.mp3',
objectKey: 'generated-editor-audios/sfx.mp3',
assetKind: 'sound-effect',
},
],
nextCursor: null,
});
render(
<AdminEditorAssetQueryPage token="admin-token" onUnauthorized={vi.fn()} />,
);
const image = await screen.findByRole('img', { name: '素材:胜利音效' });
expect(image.getAttribute('src')).toBe(
'/creation-home/audio-asset-cover.svg',
);
expect(getAdminAssetReadUrl).not.toHaveBeenCalled();
});
test('后台素材查询格式化微秒时间文本', async () => {
vi.mocked(listAdminEditorAssets).mockResolvedValueOnce({
entries: [
@@ -152,10 +172,7 @@ test('后台素材查询格式化微秒时间文本', async () => {
});
render(
<AdminEditorAssetQueryPage
token="admin-token"
onUnauthorized={vi.fn()}
/>,
<AdminEditorAssetQueryPage token="admin-token" onUnauthorized={vi.fn()} />,
);
expect(await screen.findByText('角色形象 1')).toBeTruthy();
@@ -164,15 +181,12 @@ test('后台素材查询格式化微秒时间文本', async () => {
test('后台素材查询可查看素材详情', async () => {
render(
<AdminEditorAssetQueryPage
token="admin-token"
onUnauthorized={vi.fn()}
/>,
<AdminEditorAssetQueryPage token="admin-token" onUnauthorized={vi.fn()} />,
);
fireEvent.click(await screen.findByRole('button', {name: '详情'}));
fireEvent.click(await screen.findByRole('button', { name: '详情' }));
const dialog = screen.getByRole('dialog', {name: '素材详情'});
const dialog = screen.getByRole('dialog', { name: '素材详情' });
expect(within(dialog).getByText('asset-1')).toBeTruthy();
expect(within(dialog).getByText('1024 x 1024')).toBeTruthy();
expect(within(dialog).getByText('12 泥点')).toBeTruthy();
@@ -181,20 +195,21 @@ test('后台素材查询可查看素材详情', async () => {
test('后台素材查询可打开弹窗查看完整提示词', async () => {
render(
<AdminEditorAssetQueryPage
token="admin-token"
onUnauthorized={vi.fn()}
/>,
<AdminEditorAssetQueryPage token="admin-token" onUnauthorized={vi.fn()} />,
);
fireEvent.click(await screen.findByRole('button', {name: '完整提示词内容'}));
fireEvent.click(
await screen.findByRole('button', { name: '完整提示词内容' }),
);
const dialog = screen.getByRole('dialog', {name: '完整提示词'});
const dialog = screen.getByRole('dialog', { name: '完整提示词' });
expect(within(dialog).getByText('完整提示词内容')).toBeTruthy();
fireEvent.click(within(dialog).getByRole('button', {name: '关闭完整提示词'}));
fireEvent.click(
within(dialog).getByRole('button', { name: '关闭完整提示词' }),
);
await waitFor(() => {
expect(screen.queryByRole('dialog', {name: '完整提示词'})).toBeNull();
expect(screen.queryByRole('dialog', { name: '完整提示词' })).toBeNull();
});
});
@@ -1,17 +1,17 @@
import {Eye, FileText, RefreshCcw, X} from 'lucide-react';
import type {ReactNode} from 'react';
import {useEffect, useState} from 'react';
import { Eye, FileText, RefreshCcw, X } from 'lucide-react';
import type { ReactNode } from 'react';
import { useEffect, useState } from 'react';
import {
getAdminAssetReadUrl,
listAdminEditorAssets,
} from '../api/adminApiClient';
import type {AdminAssetReadUrlResponse} from '../api/adminApiClient';
import type { AdminAssetReadUrlResponse } from '../api/adminApiClient';
import type {
AdminEditorAssetListQuery,
AdminEditorAssetPayload,
} from '../api/adminApiTypes';
import {handlePageError} from './pageUtils';
import { handlePageError } from './pageUtils';
interface AdminEditorAssetQueryPageProps {
token: string;
@@ -19,6 +19,7 @@ interface AdminEditorAssetQueryPageProps {
}
const ADMIN_ASSET_READ_EXPIRE_SECONDS = 300;
const AUDIO_ASSET_COVER_SRC = '/creation-home/audio-asset-cover.svg';
export function AdminEditorAssetQueryPage({
token,
@@ -274,10 +275,11 @@ export function AdminEditorAssetQueryPage({
);
}
function AdminAssetThumbnail({entry}: {entry: AdminEditorAssetPayload}) {
function AdminAssetThumbnail({ entry }: { entry: AdminEditorAssetPayload }) {
const isAudio = isAdminAudioAsset(entry);
const imageSrc = useAdminResolvedAssetImageSrc(
entry.thumbnailSrc || entry.imageSrc,
entry.objectKey,
isAudio ? AUDIO_ASSET_COVER_SRC : entry.thumbnailSrc || entry.imageSrc,
isAudio ? null : entry.objectKey,
);
const alt = `素材:${entry.label || entry.assetId}`;
@@ -288,6 +290,16 @@ function AdminAssetThumbnail({entry}: {entry: AdminEditorAssetPayload}) {
);
}
function isAdminAudioAsset(entry: AdminEditorAssetPayload) {
const assetKind = entry.assetKind?.trim() ?? '';
return (
assetKind === 'sound-effect' ||
assetKind === 'background-music' ||
assetKind === 'editor_uploaded_audio' ||
/\.(?:mp3|wav|m4a|aac|ogg)(?:$|[?#])/iu.test(entry.imageSrc.trim())
);
}
function AdminAssetDetailDialog({
entry,
onClose,
@@ -334,7 +346,9 @@ function AdminAssetDetailDialog({
{entry.generationCostMudPoints}
</AdminInfoItem>
<AdminInfoItem label="模型">{entry.model || '-'}</AdminInfoItem>
<AdminInfoItem label="Provider">{entry.provider || '-'}</AdminInfoItem>
<AdminInfoItem label="Provider">
{entry.provider || '-'}
</AdminInfoItem>
<AdminInfoItem label="Task ID">{entry.taskId || '-'}</AdminInfoItem>
<AdminInfoItem label="Object Key">
{entry.objectKey || '-'}
@@ -463,7 +477,8 @@ function mergeAssetEntries(
[...current, ...incoming].forEach((entry) => byId.set(entry.assetId, entry));
return [...byId.values()].sort(
(left, right) =>
parseAdminTimestamp(right.createdAt) - parseAdminTimestamp(left.createdAt) ||
parseAdminTimestamp(right.createdAt) -
parseAdminTimestamp(left.createdAt) ||
right.assetId.localeCompare(left.assetId),
);
}
@@ -505,13 +520,13 @@ function parseAdminTimestamp(value: string | null | undefined) {
function authorDisplayName(entry: AdminEditorAssetPayload) {
return (
entry.authorDisplayName?.trim() ||
entry.authorPublicUserCode?.trim() ||
'-'
entry.authorDisplayName?.trim() || entry.authorPublicUserCode?.trim() || '-'
);
}
function formatGenerationInputs(value: Record<string, unknown> | null | undefined) {
function formatGenerationInputs(
value: Record<string, unknown> | null | undefined,
) {
if (!value) {
return '-';
}
@@ -1,7 +1,13 @@
/* @vitest-environment jsdom */
import {fireEvent, render, screen, waitFor, within} from '@testing-library/react';
import {beforeEach, expect, test, vi} from 'vitest';
import {
fireEvent,
render,
screen,
waitFor,
within,
} from '@testing-library/react';
import { beforeEach, expect, test, vi } from 'vitest';
import {
getAdminAssetReadUrl,
@@ -11,8 +17,8 @@ import {
updateAdminEditorShowcaseDisplay,
upsertAdminEditorShowcaseCampaign,
} from '../api/adminApiClient';
import type {AdminEditorShowcaseAssetPayload} from '../api/adminApiTypes';
import {AdminEditorShowcaseReviewPage} from './AdminEditorShowcaseReviewPage';
import type { AdminEditorShowcaseAssetPayload } from '../api/adminApiTypes';
import { AdminEditorShowcaseReviewPage } from './AdminEditorShowcaseReviewPage';
vi.mock('../api/adminApiClient', () => ({
getAdminAssetReadUrl: vi.fn(),
@@ -40,7 +46,7 @@ const pendingShowcaseAsset: AdminEditorShowcaseAssetPayload = {
provider: 'character',
taskId: 'task-1',
assetKind: 'character',
generationInputs: {style: 'clay'},
generationInputs: { style: 'clay' },
generationCostMudPoints: 12,
refundMudPoints: 6,
reviewStatus: 'pending',
@@ -169,6 +175,36 @@ test('后台精选审核格式化微秒时间并显示素材名', async () => {
expect(screen.queryByText('1783231493.573727Z')).toBeNull();
});
test('后台精选审核音频素材使用统一封面缩略图', async () => {
vi.mocked(listAdminEditorShowcaseAssets).mockResolvedValueOnce({
entries: [
{
...pendingShowcaseAsset,
showcaseId: 'showcase-audio-1',
assetId: 'asset-audio-1',
label: '胜利音效',
imageSrc: '/generated-editor-audios/sfx.mp3',
objectKey: 'generated-editor-audios/sfx.mp3',
assetKind: 'sound-effect',
},
],
nextCursor: null,
});
render(
<AdminEditorShowcaseReviewPage
token="admin-token"
onUnauthorized={vi.fn()}
/>,
);
const image = await screen.findByRole('img', { name: '精选素材:胜利音效' });
expect(image.getAttribute('src')).toBe(
'/creation-home/audio-asset-cover.svg',
);
expect(getAdminAssetReadUrl).not.toHaveBeenCalled();
});
test('后台精选审核可以通过素材并查看完整提示词', async () => {
render(
<AdminEditorShowcaseReviewPage
@@ -177,11 +213,10 @@ test('后台精选审核可以通过素材并查看完整提示词', async () =>
/>,
);
fireEvent.change(
await screen.findByLabelText('审核备注:角色形象 1'),
{target: {value: '质量通过'}},
);
fireEvent.click(screen.getByRole('button', {name: '通过'}));
fireEvent.change(await screen.findByLabelText('审核备注:角色形象 1'), {
target: { value: '质量通过' },
});
fireEvent.click(screen.getByRole('button', { name: '通过' }));
await waitFor(() => {
expect(reviewAdminEditorShowcaseAsset).toHaveBeenCalledWith('admin-token', {
@@ -191,8 +226,8 @@ test('后台精选审核可以通过素材并查看完整提示词', async () =>
});
});
fireEvent.click(screen.getByRole('button', {name: '完整提示词内容'}));
const dialog = screen.getByRole('dialog', {name: '完整提示词'});
fireEvent.click(screen.getByRole('button', { name: '完整提示词内容' }));
const dialog = screen.getByRole('dialog', { name: '完整提示词' });
expect(within(dialog).getByText('完整提示词内容')).toBeTruthy();
});
@@ -209,7 +244,7 @@ test('后台精选审核可以切换展示和保存活动卡', async () => {
/>,
);
fireEvent.click(await screen.findByRole('button', {name: '隐藏'}));
fireEvent.click(await screen.findByRole('button', { name: '隐藏' }));
await waitFor(() => {
expect(updateAdminEditorShowcaseDisplay).toHaveBeenCalledWith(
'admin-token',
@@ -222,15 +257,15 @@ test('后台精选审核可以切换展示和保存活动卡', async () => {
});
fireEvent.change(await screen.findByLabelText('标题'), {
target: {value: '新活动卡'},
target: { value: '新活动卡' },
});
fireEvent.change(screen.getByLabelText('图片地址'), {
target: {value: '/new-campaign.png'},
target: { value: '/new-campaign.png' },
});
fireEvent.change(screen.getByLabelText('成本文案'), {
target: {value: '6 泥点'},
target: { value: '6 泥点' },
});
fireEvent.click(screen.getByRole('button', {name: '保存活动卡'}));
fireEvent.click(screen.getByRole('button', { name: '保存活动卡' }));
await waitFor(() => {
expect(upsertAdminEditorShowcaseCampaign).toHaveBeenCalledWith(
@@ -271,7 +306,7 @@ test('后台精选审核已通过素材可以设置精选分类', async () => {
);
fireEvent.change(await screen.findByLabelText('精选分类:角色形象 2'), {
target: {value: 'ui'},
target: { value: 'ui' },
});
await waitFor(() => {
@@ -307,7 +342,7 @@ test('后台精选审核清空精选分类时自动隐藏素材', async () => {
);
fireEvent.change(await screen.findByLabelText('精选分类:角色形象 2'), {
target: {value: ''},
target: { value: '' },
});
await waitFor(() => {
@@ -1,6 +1,6 @@
import {Eye, FileText, RefreshCcw, X} from 'lucide-react';
import type {ReactNode} from 'react';
import {useEffect, useState} from 'react';
import { Eye, FileText, RefreshCcw, X } from 'lucide-react';
import type { ReactNode } from 'react';
import { useEffect, useState } from 'react';
import {
getAdminAssetReadUrl,
@@ -10,13 +10,13 @@ import {
updateAdminEditorShowcaseDisplay,
upsertAdminEditorShowcaseCampaign,
} from '../api/adminApiClient';
import type {AdminAssetReadUrlResponse} from '../api/adminApiClient';
import type { AdminAssetReadUrlResponse } from '../api/adminApiClient';
import type {
AdminEditorShowcaseAssetPayload,
AdminEditorShowcaseCampaignPayload,
AdminEditorShowcaseListQuery,
} from '../api/adminApiTypes';
import {handlePageError} from './pageUtils';
import { handlePageError } from './pageUtils';
interface AdminEditorShowcaseReviewPageProps {
token: string;
@@ -24,20 +24,21 @@ interface AdminEditorShowcaseReviewPageProps {
}
const ADMIN_SHOWCASE_READ_EXPIRE_SECONDS = 300;
const AUDIO_ASSET_COVER_SRC = '/creation-home/audio-asset-cover.svg';
const showcaseCategoryOptions = [
{value: 'packs', label: '素材包'},
{value: 'characters', label: '角色'},
{value: 'ui', label: 'UI'},
{value: 'music', label: '音乐'},
{value: 'marketing', label: '美宣'},
{ value: 'packs', label: '素材包' },
{ value: 'characters', label: '角色' },
{ value: 'ui', label: 'UI' },
{ value: 'music', label: '音乐' },
{ value: 'marketing', label: '美宣' },
];
const reviewStatusOptions = [
{value: '', label: '全部'},
{value: 'pending', label: '待审核'},
{value: 'approved', label: '已通过'},
{value: 'rejected', label: '已拒绝'},
{ value: '', label: '全部' },
{ value: 'pending', label: '待审核' },
{ value: 'approved', label: '已通过' },
{ value: 'rejected', label: '已拒绝' },
];
export function AdminEditorShowcaseReviewPage({
@@ -314,9 +315,7 @@ export function AdminEditorShowcaseReviewPage({
</button>
<small>{entry.label || '-'}</small>
</td>
<td>
{formatDateTime(entry.submittedAt)}
</td>
<td>{formatDateTime(entry.submittedAt)}</td>
<td>
{authorDisplayName(entry)}
<small>{entry.authorPublicUserCode?.trim() || '-'}</small>
@@ -342,7 +341,9 @@ export function AdminEditorShowcaseReviewPage({
)}
</td>
<td>
<span className={reviewStatusClassName(entry.reviewStatus)}>
<span
className={reviewStatusClassName(entry.reviewStatus)}
>
{reviewStatusLabel(entry.reviewStatus)}
</span>
{entry.reviewStatus === 'approved' ? (
@@ -589,7 +590,11 @@ function AdminShowcaseThumbnail({
}: {
entry: AdminEditorShowcaseAssetPayload;
}) {
const imageSrc = useAdminResolvedAssetImageSrc(entry.imageSrc, entry.objectKey);
const isAudio = isAdminShowcaseAudioAsset(entry);
const imageSrc = useAdminResolvedAssetImageSrc(
isAudio ? AUDIO_ASSET_COVER_SRC : entry.imageSrc,
isAudio ? null : entry.objectKey,
);
const alt = `精选素材:${entry.label || entry.showcaseId}`;
return imageSrc ? (
@@ -599,6 +604,16 @@ function AdminShowcaseThumbnail({
);
}
function isAdminShowcaseAudioAsset(entry: AdminEditorShowcaseAssetPayload) {
const assetKind = entry.assetKind?.trim() ?? '';
return (
assetKind === 'sound-effect' ||
assetKind === 'background-music' ||
assetKind === 'editor_uploaded_audio' ||
/\.(?:mp3|wav|m4a|aac|ogg)(?:$|[?#])/iu.test(entry.imageSrc.trim())
);
}
function AdminShowcaseDetailDialog({
entry,
onClose,
@@ -659,7 +674,9 @@ function AdminShowcaseDetailDialog({
</AdminInfoItem>
<AdminInfoItem label="点赞数">{entry.likeCount}</AdminInfoItem>
<AdminInfoItem label="模型">{entry.model || '-'}</AdminInfoItem>
<AdminInfoItem label="Provider">{entry.provider || '-'}</AdminInfoItem>
<AdminInfoItem label="Provider">
{entry.provider || '-'}
</AdminInfoItem>
<AdminInfoItem label="Task ID">{entry.taskId || '-'}</AdminInfoItem>
<AdminInfoItem label="Object Key">
{entry.objectKey || '-'}
@@ -869,13 +886,13 @@ function reviewStatusClassName(value: string) {
function authorDisplayName(entry: AdminEditorShowcaseAssetPayload) {
return (
entry.authorDisplayName?.trim() ||
entry.authorPublicUserCode?.trim() ||
'-'
entry.authorDisplayName?.trim() || entry.authorPublicUserCode?.trim() || '-'
);
}
function formatGenerationInputs(value: Record<string, unknown> | null | undefined) {
function formatGenerationInputs(
value: Record<string, unknown> | null | undefined,
) {
if (!value) {
return '-';
}
@@ -0,0 +1,19 @@
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 256 256"
role="img"
aria-label="音频素材"
>
<g
fill="none"
stroke="#dc7f58"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="16"
>
<path d="M92 185V35l152-24v149" />
<path d="M92 87l152-24" />
<ellipse cx="53" cy="207" rx="41" ry="24" transform="rotate(4 53 207)" />
<ellipse cx="201" cy="193" rx="41" ry="24" transform="rotate(-4 201 193)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 475 B

@@ -125,12 +125,10 @@ function installIntersectionObserverMock() {
return observers;
}
function triggerIntersection(
observer: {
callback: IntersectionObserverCallback;
instance: IntersectionObserver;
},
) {
function triggerIntersection(observer: {
callback: IntersectionObserverCallback;
instance: IntersectionObserver;
}) {
act(() => {
observer.callback(
[{ isIntersecting: true } as IntersectionObserverEntry],
@@ -157,15 +155,15 @@ describe('CreationLandingView', () => {
renderCreationLanding();
expect(
screen.getByRole('main', { name: '陶泥儿创作主页' }),
).toBeTruthy();
expect(screen.getByRole('main', { name: '陶泥儿创作主页' })).toBeTruthy();
expect(
screen.getByRole('heading', {
name: '陶泥儿 - 开启全民精品游戏创作',
}),
).toBeTruthy();
expect(screen.getByText('登录即送100泥点,可以免费制作50个素材')).toBeTruthy();
expect(
screen.getByText('登录即送100泥点,可以免费制作50个素材'),
).toBeTruthy();
expect(screen.getByRole('heading', { name: '创作工具' })).toBeTruthy();
expect(screen.getByText('游戏视觉规范')).toBeTruthy();
expect(screen.getByRole('heading', { name: '陶泥儿精选' })).toBeTruthy();
@@ -193,7 +191,9 @@ describe('CreationLandingView', () => {
</AuthUiContext.Provider>,
);
expect(await screen.findByRole('heading', { name: '最近项目' })).toBeTruthy();
expect(
await screen.findByRole('heading', { name: '最近项目' }),
).toBeTruthy();
expect(screen.getByText('最新项目')).toBeTruthy();
expect(screen.getByText('旧项目')).toBeTruthy();
await user.click(screen.getByRole('button', { name: '查看全部' }));
@@ -302,10 +302,7 @@ describe('CreationLandingView', () => {
render(
<AuthUiContext.Provider value={createAuthValue()}>
<CreationLandingView
onOpenProject={vi.fn()}
onOpenProjects={vi.fn()}
/>
<CreationLandingView onOpenProject={vi.fn()} onOpenProjects={vi.fn()} />
</AuthUiContext.Provider>,
);
@@ -375,9 +372,13 @@ describe('CreationLandingView', () => {
const assetCard = await screen.findByText('角色英雄');
const card = assetCard.closest('.creation-landing__asset-card');
expect(card).toBeTruthy();
expect(within(card as HTMLElement).getByText('character hero prompt')).toBeTruthy();
expect(
within(card as HTMLElement).getByText('character hero prompt'),
).toBeTruthy();
expect(within(card as HTMLElement).getByText('创作者A')).toBeTruthy();
expect(within(card as HTMLElement).queryByText('user-internal-author')).toBeNull();
expect(
within(card as HTMLElement).queryByText('user-internal-author'),
).toBeNull();
expect(within(card as HTMLElement).getByText('20泥点')).toBeTruthy();
});
@@ -500,18 +501,56 @@ describe('CreationLandingView', () => {
expect(within(dialog).getByText('生成总成本')).toBeTruthy();
expect(within(dialog).getByText('60泥点')).toBeTruthy();
expect(within(dialog).getByText('创作者A')).toBeTruthy();
expect(
within(dialog).getByLabelText('素材缩略图导航'),
).toBeTruthy();
expect(within(dialog).getByLabelText('素材缩略图导航')).toBeTruthy();
await user.click(within(dialog).getByRole('button', { name: '查看待机动画' }));
await user.click(
within(dialog).getByRole('button', { name: '查看待机动画' }),
);
expect(
within(dialog).getByRole('button', { name: '查看待机动画' }).getAttribute(
'aria-current',
),
within(dialog)
.getByRole('button', { name: '查看待机动画' })
.getAttribute('aria-current'),
).toBe('true');
});
it('uses the unified audio cover image for featured audio assets', async () => {
const user = userEvent.setup();
listEditorProjectsMock.mockResolvedValueOnce(projectItems);
listPublicEditorProjectResourcesMock.mockResolvedValueOnce([
{
resourceId: 'resource-music',
projectId: 'project-newest',
label: '背景音乐',
imageSrc: '/generated-editor-audios/music.mp3',
objectKey: 'generated-editor-audios/music.mp3',
width: 1,
height: 1,
sourceType: 'generated',
prompt: '舒缓背景音乐',
actualPrompt: null,
model: 'audio1.0',
provider: 'ark',
taskId: 'task-audio-1',
assetKind: 'background-music',
priceMudPoints: 5,
},
]);
renderCreationLanding();
await user.click(screen.getByRole('tab', { name: '音乐' }));
const card = (await screen.findByText('背景音乐')).closest(
'.creation-landing__asset-card',
);
expect(card).toBeTruthy();
const image = within(card as HTMLElement).getByRole('img', {
name: '背景音乐',
});
expect(image.getAttribute('src')).toBe(
'/creation-home/audio-asset-cover.svg',
);
});
it('uses Taonier featured as the generated project resource waterfall instead of creation entries', async () => {
listEditorProjectsMock.mockResolvedValueOnce(projectItems);
listPublicEditorProjectResourcesMock.mockResolvedValueOnce([
@@ -564,9 +603,9 @@ describe('CreationLandingView', () => {
expect(await screen.findByText('素材包 A')).toBeTruthy();
expect(screen.getByText('素材包 B')).toBeTruthy();
expect(screen.queryByText('上传素材')).toBeNull();
expect(
screen.getByLabelText('用户素材瀑布流').className,
).toContain('creation-landing__asset-waterfall');
expect(screen.getByLabelText('用户素材瀑布流').className).toContain(
'creation-landing__asset-waterfall',
);
const firstPreview = container.querySelector(
'.creation-landing__asset-preview',
) as HTMLElement | null;
@@ -665,7 +704,9 @@ describe('CreationLandingView', () => {
await waitFor(() => expect(observers).toHaveLength(1));
triggerIntersection(observers[0]!);
expect(await screen.findByRole('button', { name: '读取更多素材失败,点击重试' })).toBeTruthy();
expect(
await screen.findByRole('button', { name: '读取更多素材失败,点击重试' }),
).toBeTruthy();
expect(screen.getByText('素材包 A')).toBeTruthy();
expect(screen.getByLabelText('用户素材瀑布流')).toBeTruthy();
});
@@ -61,8 +61,7 @@ const CREATION_HOME_ASSETS = {
projectNew: '/creation-home/project-new.png',
} as const;
const SHOWCASE_LIKE_ICON_SRC =
'/creation-home/showcase-like-thumb.png';
const SHOWCASE_LIKE_ICON_SRC = '/creation-home/showcase-like-thumb.png';
const CREATION_FEATURES: CreationFeatureItem[] = [
{
@@ -137,7 +136,9 @@ function mergeShowcaseResources(
current: EditorProjectResourceSnapshot[],
incoming: EditorProjectResourceSnapshot[],
) {
const seenResourceIds = new Set(current.map((resource) => resource.resourceId));
const seenResourceIds = new Set(
current.map((resource) => resource.resourceId),
);
return [
...current,
...incoming.filter((resource) => {
@@ -189,10 +190,17 @@ function ShowcaseAudioPreview({
const { resolvedUrl } = useResolvedAssetReadUrl(preview.src, {
objectKey: preview.objectKey,
});
const coverSrc =
preview.coverSrc?.trim() || '/creation-home/audio-asset-cover.svg';
return (
<div className="creation-landing__asset-audio-preview">
<Music2 aria-hidden="true" />
<img
alt={preview.label}
className="creation-landing__asset-audio-cover"
draggable={false}
src={coverSrc}
/>
{controls && resolvedUrl ? (
<audio controls preload="metadata" src={resolvedUrl} />
) : null}
@@ -271,11 +279,7 @@ function ShowcaseVideoPreview({
);
}
function ShowcaseBundlePreview({
item,
}: {
item: ShowcaseAssetItem;
}) {
function ShowcaseBundlePreview({ item }: { item: ShowcaseAssetItem }) {
const visiblePreviews = item.previews.slice(0, 4);
const primaryPreview = visiblePreviews[0];
@@ -396,7 +400,9 @@ function CreationShowcaseModal({
alt=""
aria-hidden="true"
/>
<span>{isLikePending ? '处理中' : isLiked ? '已点赞' : '点赞'}</span>
<span>
{isLikePending ? '处理中' : isLiked ? '已点赞' : '点赞'}
</span>
</PlatformActionButton>
) : null}
</div>
@@ -418,7 +424,8 @@ function CreationShowcaseModal({
onClick={() => onSelectIndex(index)}
>
<span className="creation-landing__showcase-thumb-media">
{preview.mediaType === 'image' ? (
{preview.mediaType === 'image' ||
preview.mediaType === 'audio' ? (
<ShowcasePreview preview={preview} variant="thumb" />
) : (
getPreviewIcon(preview)
@@ -455,9 +462,13 @@ export function CreationLandingView({
>([]);
const [showcaseCampaign, setShowcaseCampaign] =
useState<EditorShowcaseCampaignSnapshot | null>(null);
const [showcaseNextCursor, setShowcaseNextCursor] = useState<string | null>(null);
const [showcaseNextCursor, setShowcaseNextCursor] = useState<string | null>(
null,
);
const [assetError, setAssetError] = useState<string | null>(null);
const [showcaseLoadMoreError, setShowcaseLoadMoreError] = useState<string | null>(null);
const [showcaseLoadMoreError, setShowcaseLoadMoreError] = useState<
string | null
>(null);
const [activeShowcaseTab, setActiveShowcaseTab] =
useState<ShowcaseTabId>('packs');
const [selectedShowcaseItem, setSelectedShowcaseItem] =
@@ -499,9 +510,7 @@ export function CreationLandingView({
}
const message =
error instanceof Error ? error.message : '读取最近项目失败';
setProjectError(
message,
);
setProjectError(message);
})
.finally(() => setIsLoadingProjects(false));
}, [authUi, isAuthenticated]);
@@ -540,47 +549,58 @@ export function CreationLandingView({
refreshShowcaseResources();
}, [refreshShowcaseResources]);
const loadMoreShowcaseResources = useCallback((options: { force?: boolean } = {}) => {
const cursor = showcaseNextCursor;
if (!cursor || isLoadingShowcase || isLoadingMoreShowcase) {
return;
}
if (showcaseLoadingMoreCursorRef.current === cursor) {
return;
}
if (!options.force && showcaseFailedLoadMoreCursorRef.current === cursor) {
return;
}
showcaseLoadingMoreCursorRef.current = cursor;
setIsLoadingMoreShowcase(true);
setShowcaseLoadMoreError(null);
listPublicEditorProjectResources({ cursor })
.then((page) => {
const normalizedPage = normalizeShowcaseResourcePage(page);
setProjectShowcaseResources((current) =>
mergeShowcaseResources(current, normalizedPage.resources),
);
setShowcaseCampaign((current) => normalizedPage.campaign ?? current);
setShowcaseNextCursor(normalizedPage.nextCursor);
showcaseFailedLoadMoreCursorRef.current = null;
})
.catch((error: unknown) => {
showcaseFailedLoadMoreCursorRef.current = cursor;
setShowcaseLoadMoreError(
error instanceof Error ? error.message : '读取陶泥儿精选失败',
);
})
.finally(() => {
if (showcaseLoadingMoreCursorRef.current === cursor) {
showcaseLoadingMoreCursorRef.current = null;
}
setIsLoadingMoreShowcase(false);
});
}, [isLoadingMoreShowcase, isLoadingShowcase, showcaseNextCursor]);
const loadMoreShowcaseResources = useCallback(
(options: { force?: boolean } = {}) => {
const cursor = showcaseNextCursor;
if (!cursor || isLoadingShowcase || isLoadingMoreShowcase) {
return;
}
if (showcaseLoadingMoreCursorRef.current === cursor) {
return;
}
if (
!options.force &&
showcaseFailedLoadMoreCursorRef.current === cursor
) {
return;
}
showcaseLoadingMoreCursorRef.current = cursor;
setIsLoadingMoreShowcase(true);
setShowcaseLoadMoreError(null);
listPublicEditorProjectResources({ cursor })
.then((page) => {
const normalizedPage = normalizeShowcaseResourcePage(page);
setProjectShowcaseResources((current) =>
mergeShowcaseResources(current, normalizedPage.resources),
);
setShowcaseCampaign((current) => normalizedPage.campaign ?? current);
setShowcaseNextCursor(normalizedPage.nextCursor);
showcaseFailedLoadMoreCursorRef.current = null;
})
.catch((error: unknown) => {
showcaseFailedLoadMoreCursorRef.current = cursor;
setShowcaseLoadMoreError(
error instanceof Error ? error.message : '读取陶泥儿精选失败',
);
})
.finally(() => {
if (showcaseLoadingMoreCursorRef.current === cursor) {
showcaseLoadingMoreCursorRef.current = null;
}
setIsLoadingMoreShowcase(false);
});
},
[isLoadingMoreShowcase, isLoadingShowcase, showcaseNextCursor],
);
useEffect(() => {
const element = showcaseLoadMoreRef.current;
if (!element || !showcaseNextCursor || isLoadingShowcase || isLoadingMoreShowcase) {
if (
!element ||
!showcaseNextCursor ||
isLoadingShowcase ||
isLoadingMoreShowcase
) {
return;
}
if (showcaseFailedLoadMoreCursorRef.current === showcaseNextCursor) {
@@ -608,28 +628,31 @@ export function CreationLandingView({
showcaseNextCursor,
]);
const createProject = useCallback((tool?: CreationFeatureTool) => {
if (authUi && !authUi.user) {
authUi.openLoginModal(() => createProject(tool));
return;
}
setIsCreatingProject(true);
setProjectError(null);
createEditorProject()
.then((project) =>
onOpenProject(project.projectId, tool ? { tool } : { guide: true }),
)
.catch((error: unknown) => {
if (isUnauthorizedError(error)) {
authUi?.openLoginModal(() => createProject(tool));
return;
}
setProjectError(
error instanceof Error ? error.message : '创建项目失败',
);
})
.finally(() => setIsCreatingProject(false));
}, [authUi, onOpenProject]);
const createProject = useCallback(
(tool?: CreationFeatureTool) => {
if (authUi && !authUi.user) {
authUi.openLoginModal(() => createProject(tool));
return;
}
setIsCreatingProject(true);
setProjectError(null);
createEditorProject()
.then((project) =>
onOpenProject(project.projectId, tool ? { tool } : { guide: true }),
)
.catch((error: unknown) => {
if (isUnauthorizedError(error)) {
authUi?.openLoginModal(() => createProject(tool));
return;
}
setProjectError(
error instanceof Error ? error.message : '创建项目失败',
);
})
.finally(() => setIsCreatingProject(false));
},
[authUi, onOpenProject],
);
const handleFeatureClick = useCallback(
(feature: CreationFeatureItem) => {
@@ -646,13 +669,16 @@ export function CreationLandingView({
(showcaseId: string, likeCount: number) => {
setProjectShowcaseResources((current) =>
current.map((resource) =>
resource.showcaseId === showcaseId || resource.resourceId === showcaseId
resource.showcaseId === showcaseId ||
resource.resourceId === showcaseId
? { ...resource, likeCount }
: resource,
),
);
setSelectedShowcaseItem((current) =>
current?.showcaseId === showcaseId ? { ...current, likeCount } : current,
current?.showcaseId === showcaseId
? { ...current, likeCount }
: current,
);
},
[],
@@ -783,7 +809,10 @@ export function CreationLandingView({
return (
<>
<div className="creation-landing__asset-waterfall" aria-label="用户素材瀑布流">
<div
className="creation-landing__asset-waterfall"
aria-label="用户素材瀑布流"
>
{showcaseItems.map((item) => {
const showcaseId = item.showcaseId?.trim();
const isLiked = Boolean(
@@ -830,7 +859,11 @@ export function CreationLandingView({
disabled={isLikePending}
onClick={() => handleToggleShowcaseLike(item)}
>
<img src={SHOWCASE_LIKE_ICON_SRC} alt="" aria-hidden="true" />
<img
src={SHOWCASE_LIKE_ICON_SRC}
alt=""
aria-hidden="true"
/>
<span>{item.likeCount ?? 0}</span>
</button>
) : null}
@@ -1015,7 +1048,11 @@ export function CreationLandingView({
<h2></h2>
</div>
</div>
<div className="creation-landing__tabs" role="tablist" aria-label="用户素材分类">
<div
className="creation-landing__tabs"
role="tablist"
aria-label="用户素材分类"
>
{SHOWCASE_TABS.map((tab) => (
<button
key={tab.id}
@@ -458,6 +458,9 @@ describe('creationShowcaseModel', () => {
expect(items).toHaveLength(1);
expect(items[0]?.previews[0]?.mediaType).toBe('audio');
expect(items[0]?.previews[0]?.coverSrc).toBe(
'/creation-home/audio-asset-cover.svg',
);
expect(items[0]?.author).toBe('当前用户');
expect(items[0]?.cost).toBe('5泥点');
});
@@ -30,6 +30,7 @@ export type ShowcaseAssetPreview = {
id: string;
label: string;
src: string;
coverSrc?: string | null;
objectKey?: string | null;
mediaType: ShowcaseAssetMediaType;
width?: number;
@@ -55,6 +56,7 @@ type ShowcaseAssetGroup = {
};
const UNKNOWN_META_VALUE = '-';
const AUDIO_ASSET_COVER_SRC = '/creation-home/audio-asset-cover.svg';
const SHOWCASE_COST_KEYS = [
'priceMudPoints',
@@ -63,7 +65,10 @@ const SHOWCASE_COST_KEYS = [
'mudPointCost',
] as const;
const SHOWCASE_AUTHOR_KEYS = ['authorDisplayName', 'authorPublicUserCode'] as const;
const SHOWCASE_AUTHOR_KEYS = [
'authorDisplayName',
'authorPublicUserCode',
] as const;
export const SHOWCASE_TABS: Array<{ id: ShowcaseTabId; label: string }> = [
{ id: 'packs', label: '素材包' },
@@ -183,7 +188,10 @@ function projectResourceToShowcaseAsset(
return {
...record,
assetId: resource.assetId?.trim() || resource.showcaseId?.trim() || resource.resourceId,
assetId:
resource.assetId?.trim() ||
resource.showcaseId?.trim() ||
resource.resourceId,
folderId: `project:${resource.projectId}`,
label: fallbackLabel,
imageSrc: resource.imageSrc,
@@ -216,7 +224,9 @@ function normalizeShowcaseCategory(value: string): ShowcaseTabId | null {
}
function resolveAssetShowcaseCategory(asset: EditorAssetSnapshot) {
return normalizeShowcaseCategory(normalizeText(assetRecord(asset).showcaseCategory));
return normalizeShowcaseCategory(
normalizeText(assetRecord(asset).showcaseCategory),
);
}
function normalizeShowcaseMediaRef(value: string | null | undefined) {
@@ -307,11 +317,15 @@ function findSpecReference(asset: EditorAssetSnapshot) {
}
function findCharacterReference(asset: EditorAssetSnapshot) {
return findReference(asset, (reference) => /|/u.test(reference.title));
return findReference(asset, (reference) =>
/|/u.test(reference.title),
);
}
function findUiDesignReference(asset: EditorAssetSnapshot) {
return findReference(asset, (reference) => /UI|UI |UI/u.test(reference.title));
return findReference(asset, (reference) =>
/UI|UI |UI/u.test(reference.title),
);
}
function isAssetKind(asset: EditorAssetSnapshot, kinds: readonly string[]) {
@@ -336,7 +350,9 @@ function isVideoAsset(asset: EditorAssetSnapshot) {
);
}
function resolveAssetMediaType(asset: EditorAssetSnapshot): ShowcaseAssetMediaType {
function resolveAssetMediaType(
asset: EditorAssetSnapshot,
): ShowcaseAssetMediaType {
if (isAudioAsset(asset)) {
return 'audio';
}
@@ -435,11 +451,16 @@ function mergeAssetAliasGroups(
function sortGroupAssets(assets: EditorAssetSnapshot[]) {
return [...assets].sort((left, right) => {
const leftRank = isSpecAsset(left) || isCharacterAsset(left) || isAssetKind(left, ['ui-design'])
? 0
: 1;
const leftRank =
isSpecAsset(left) ||
isCharacterAsset(left) ||
isAssetKind(left, ['ui-design'])
? 0
: 1;
const rightRank =
isSpecAsset(right) || isCharacterAsset(right) || isAssetKind(right, ['ui-design'])
isSpecAsset(right) ||
isCharacterAsset(right) ||
isAssetKind(right, ['ui-design'])
? 0
: 1;
if (leftRank !== rightRank) {
@@ -454,7 +475,12 @@ function groupPackAssets(assets: EditorAssetSnapshot[]) {
assets.forEach((asset) => {
const specReference = findSpecReference(asset);
if (specReference) {
addAssetToGroup(groups, getReferenceKey(specReference), asset, specReference.label);
addAssetToGroup(
groups,
getReferenceKey(specReference),
asset,
specReference.label,
);
return;
}
if (isSpecAsset(asset)) {
@@ -466,7 +492,10 @@ function groupPackAssets(assets: EditorAssetSnapshot[]) {
const specAssets = assets.filter(isSpecAsset);
specAssets.forEach((asset) => {
const group = mergeAssetAliasGroups(groups, asset, asset.label);
if (group && !group.assets.some((groupAsset) => groupAsset.assetId === asset.assetId)) {
if (
group &&
!group.assets.some((groupAsset) => groupAsset.assetId === asset.assetId)
) {
group.assets.unshift(asset);
}
});
@@ -500,12 +529,20 @@ function groupCharacterAssets(assets: EditorAssetSnapshot[]) {
);
return;
}
addAssetToGroup(groups, `standalone:${asset.assetId}`, asset, asset.label);
});
addAssetToGroup(
groups,
`standalone:${asset.assetId}`,
asset,
asset.label,
);
});
const characterAssets = assets.filter(isCharacterAsset);
characterAssets.forEach((asset) => {
const group = mergeAssetAliasGroups(groups, asset, asset.label);
if (group && !group.assets.some((groupAsset) => groupAsset.assetId === asset.assetId)) {
if (
group &&
!group.assets.some((groupAsset) => groupAsset.assetId === asset.assetId)
) {
group.assets.unshift(asset);
}
});
@@ -518,23 +555,43 @@ function groupCharacterAssets(assets: EditorAssetSnapshot[]) {
function groupUiAssets(assets: EditorAssetSnapshot[]) {
const groups = new Map<string, ShowcaseAssetGroup>();
assets
.filter((asset) => isUiAsset(asset) || /\bui\b|||icon/u.test(getAssetHaystack(asset)))
.filter(
(asset) =>
isUiAsset(asset) ||
/\bui\b|||icon/u.test(getAssetHaystack(asset)),
)
.forEach((asset) => {
if (isAssetKind(asset, ['ui-design'])) {
addAssetToGroup(groups, getSelfAssetKey(asset), asset, asset.label);
return;
}
const uiReference = findUiDesignReference(asset) ?? findSpecReference(asset);
const uiReference =
findUiDesignReference(asset) ?? findSpecReference(asset);
if (uiReference) {
addAssetToGroup(groups, getReferenceKey(uiReference), asset, uiReference.label);
addAssetToGroup(
groups,
getReferenceKey(uiReference),
asset,
uiReference.label,
);
return;
}
addAssetToGroup(groups, `standalone:${asset.assetId}`, asset, asset.label);
});
const uiDesignAssets = assets.filter((asset) => isAssetKind(asset, ['ui-design']));
addAssetToGroup(
groups,
`standalone:${asset.assetId}`,
asset,
asset.label,
);
});
const uiDesignAssets = assets.filter((asset) =>
isAssetKind(asset, ['ui-design']),
);
uiDesignAssets.forEach((asset) => {
const group = mergeAssetAliasGroups(groups, asset, asset.label);
if (group && !group.assets.some((groupAsset) => groupAsset.assetId === asset.assetId)) {
if (
group &&
!group.assets.some((groupAsset) => groupAsset.assetId === asset.assetId)
) {
group.assets.unshift(asset);
}
});
@@ -588,13 +645,18 @@ function resolveAssetPrompt(asset: EditorAssetSnapshot) {
const generationFields = asset.generationInputs?.fields ?? [];
const firstPromptField =
generationFields.find((field) => /|||prompt/u.test(field.title)) ??
generationFields[0];
generationFields.find((field) =>
/|||prompt/u.test(field.title),
) ?? generationFields[0];
return firstPromptField?.value?.trim() || UNKNOWN_META_VALUE;
}
function resolveGroupPrompt(assets: EditorAssetSnapshot[]) {
return assets.map(resolveAssetPrompt).find((value) => value !== UNKNOWN_META_VALUE) ?? UNKNOWN_META_VALUE;
return (
assets
.map(resolveAssetPrompt)
.find((value) => value !== UNKNOWN_META_VALUE) ?? UNKNOWN_META_VALUE
);
}
function resolveAssetAuthor(
@@ -672,7 +734,12 @@ function inferAssetCost(asset: EditorAssetSnapshot) {
| '480p'
| '720p'
| '1080p',
normalizeInt(asset.durationSeconds, DEFAULT_VIDEO_DURATION_SECONDS, 4, 15),
normalizeInt(
asset.durationSeconds,
DEFAULT_VIDEO_DURATION_SECONDS,
4,
15,
),
);
}
if (asset.sourceType === 'generated' || kind) {
@@ -715,12 +782,14 @@ function resolveGroupCost(assets: EditorAssetSnapshot[]) {
}
function toAssetPreview(asset: EditorAssetSnapshot): ShowcaseAssetPreview {
const mediaType = resolveAssetMediaType(asset);
return {
id: asset.assetId,
label: asset.label,
src: asset.imageSrc,
coverSrc: mediaType === 'audio' ? AUDIO_ASSET_COVER_SRC : null,
objectKey: asset.objectKey,
mediaType: resolveAssetMediaType(asset),
mediaType,
width: asset.width,
height: asset.height,
};
@@ -731,7 +800,9 @@ function toLibraryShowcaseItem(
fallbackAuthorName?: string | null,
): ShowcaseAssetItem | null {
const sortedAssets = sortGroupAssets(group.assets);
const previews = sortedAssets.map(toAssetPreview).filter((preview) => preview.src.trim());
const previews = sortedAssets
.map(toAssetPreview)
.filter((preview) => preview.src.trim());
if (!previews.length) {
return null;
}
@@ -27,25 +27,30 @@ vi.mock('../common/PlatformMediaFrame', () => ({
}) => {
const imageSrc =
src?.trim() ||
(objectKey?.trim() ? `https://signed.example.com/${objectKey.trim()}` : '');
(objectKey?.trim()
? `https://signed.example.com/${objectKey.trim()}`
: '');
return (
<div className="platform-media-frame">
{imageSrc ? (
<img
src={imageSrc}
alt={alt}
data-object-key={objectKey?.trim() || undefined}
/>
) : null}
{previewOverlay}
</div>
<img
src={imageSrc}
alt={alt}
data-object-key={objectKey?.trim() || undefined}
/>
) : null}
{previewOverlay}
</div>
);
},
}));
beforeEach(() => {
useResolvedAssetReadUrlMock.mockImplementation(
(source: string | null | undefined, options?: { objectKey?: string | null }) => ({
(
source: string | null | undefined,
options?: { objectKey?: string | null },
) => ({
resolvedUrl: options?.objectKey
? `https://signed.example.com/${options.objectKey}`
: (source ?? ''),
@@ -138,9 +143,7 @@ describe('ImageCanvasAssetRowView', () => {
const onDownloadAsset = vi.fn();
renderAssetRow({ asset, onDownloadAsset });
fireEvent.click(
screen.getByRole('button', { name: '下载素材账号素材A' }),
);
fireEvent.click(screen.getByRole('button', { name: '下载素材账号素材A' }));
expect(onDownloadAsset).toHaveBeenCalledWith(asset);
});
@@ -154,20 +157,26 @@ describe('ImageCanvasAssetRowView', () => {
const onDownloadAsset = vi.fn();
renderAssetRow({ asset, addAssetLayer, onDownloadAsset });
fireEvent.contextMenu(screen.getByRole('button', { name: '添加账号素材A' }), {
clientX: 120,
clientY: 80,
});
fireEvent.contextMenu(
screen.getByRole('button', { name: '添加账号素材A' }),
{
clientX: 120,
clientY: 80,
},
);
expect(screen.getByRole('menu')).toBeTruthy();
fireEvent.click(screen.getByRole('button', { name: '下载素材账号素材A' }));
expect(onDownloadAsset).toHaveBeenCalledWith(asset);
expect(screen.queryByRole('menu')).toBeNull();
fireEvent.contextMenu(screen.getByRole('button', { name: '添加账号素材A' }), {
clientX: 120,
clientY: 80,
});
fireEvent.contextMenu(
screen.getByRole('button', { name: '添加账号素材A' }),
{
clientX: 120,
clientY: 80,
},
);
fireEvent.click(screen.getByRole('button', { name: '添加账号素材A' }));
expect(addAssetLayer).toHaveBeenCalledWith(asset);
expect(screen.queryByRole('menu')).toBeNull();
@@ -199,7 +208,9 @@ describe('ImageCanvasAssetRowView', () => {
fireEvent.keyDown(input, { key: 'Enter' });
expect(input.className).toContain('platform-text-field');
expect(input.className).toContain('image-canvas-editor__asset-rename-input');
expect(input.className).toContain(
'image-canvas-editor__asset-rename-input',
);
expect(commitAssetRename).toHaveBeenCalledWith(
expect.objectContaining({ id: 'asset-1' }),
);
@@ -226,7 +237,7 @@ describe('ImageCanvasAssetRowView', () => {
).toBe('42');
});
it('uses icon thumbnails for audio assets instead of rendering media as images', () => {
it('uses the audio cover thumbnail for audio assets', () => {
renderAssetRow({
asset: createAsset({
label: '胜利音效.mp3',
@@ -236,13 +247,13 @@ describe('ImageCanvasAssetRowView', () => {
});
expect(screen.getByText('音频')).toBeTruthy();
expect(screen.queryByRole('img', { name: '素材:胜利音效.mp3' })).toBeNull();
const image = screen.getByRole('img', { name: '素材:胜利音效.mp3' });
expect(image.getAttribute('src')).toBe(
'/creation-home/audio-asset-cover.svg',
);
expect(
document.querySelector('.image-canvas-editor__asset-media-overlay'),
).toBeTruthy();
expect(
document.querySelector('.image-canvas-editor__media-preview--audio'),
).toBeTruthy();
).toBeNull();
});
it('uses a stable visual preview for video asset cards without image thumbnails', () => {
@@ -250,13 +261,16 @@ describe('ImageCanvasAssetRowView', () => {
asset: createAsset({
label: '生成视频.mp4',
src: '/generated-character-drafts/editor/asset-library/video/demo.mp4',
objectKey: 'generated-character-drafts/editor/asset-library/video/demo.mp4',
objectKey:
'generated-character-drafts/editor/asset-library/video/demo.mp4',
mediaType: 'video',
}),
});
expect(screen.getByText('视频')).toBeTruthy();
expect(screen.queryByRole('img', { name: '素材:生成视频.mp4' })).toBeNull();
expect(
screen.queryByRole('img', { name: '素材:生成视频.mp4' }),
).toBeNull();
const video = screen.getByLabelText(
'视频素材预览:生成视频.mp4',
) as HTMLVideoElement;
@@ -315,10 +329,13 @@ describe('ImageCanvasAssetRowView', () => {
screen.queryByRole('button', { name: '删除素材账号素材A' }),
).toBeNull();
fireEvent.contextMenu(screen.getByRole('button', { name: '添加账号素材A' }), {
clientX: 120,
clientY: 80,
});
fireEvent.contextMenu(
screen.getByRole('button', { name: '添加账号素材A' }),
{
clientX: 120,
clientY: 80,
},
);
const submitMenuItem = screen.getByRole('menuitem', {
name: //u,
@@ -327,10 +344,13 @@ describe('ImageCanvasAssetRowView', () => {
fireEvent.click(submitMenuItem);
expect(submitAssetToShowcase).toHaveBeenCalledWith(asset);
fireEvent.contextMenu(screen.getByRole('button', { name: '添加账号素材A' }), {
clientX: 120,
clientY: 80,
});
fireEvent.contextMenu(
screen.getByRole('button', { name: '添加账号素材A' }),
{
clientX: 120,
clientY: 80,
},
);
fireEvent.click(screen.getByRole('menuitem', { name: '删除' }));
expect(deleteUploadedAsset).toHaveBeenCalledWith(asset);
});
@@ -339,10 +359,13 @@ describe('ImageCanvasAssetRowView', () => {
const submitAssetToShowcase = vi.fn();
renderAssetRow({ submitAssetToShowcase });
fireEvent.contextMenu(screen.getByRole('button', { name: '添加账号素材A' }), {
clientX: 120,
clientY: 80,
});
fireEvent.contextMenu(
screen.getByRole('button', { name: '添加账号素材A' }),
{
clientX: 120,
clientY: 80,
},
);
const submitMenuItem = screen.getByRole('menuitem', {
name: //u,
@@ -360,10 +383,13 @@ describe('ImageCanvasAssetRowView', () => {
const submitAssetToShowcase = vi.fn();
renderAssetRow({ asset, submitAssetToShowcase });
fireEvent.contextMenu(screen.getByRole('button', { name: '添加账号素材A' }), {
clientX: 120,
clientY: 80,
});
fireEvent.contextMenu(
screen.getByRole('button', { name: '添加账号素材A' }),
{
clientX: 120,
clientY: 80,
},
);
const submitMenuItem = screen.getByRole('menuitem', {
name: //u,
@@ -1,4 +1,4 @@
import { Check, Download, Music2, Pencil, Video, X } from 'lucide-react';
import { Check, Download, Pencil, Video, X } from 'lucide-react';
import { useCallback, useEffect, useRef, useState } from 'react';
import type { Dispatch, SetStateAction } from 'react';
@@ -24,6 +24,8 @@ import {
getCanvasMediaPreviewMarker,
} from './ImageCanvasMediaModel';
const AUDIO_ASSET_COVER_SRC = '/creation-home/audio-asset-cover.svg';
export type ImageCanvasAssetRowViewProps = {
asset: EditorAsset;
assetPointerDragRef: { current: AssetPointerDragState | null };
@@ -38,7 +40,10 @@ export type ImageCanvasAssetRowViewProps = {
setAssetPointerDrag: Dispatch<SetStateAction<AssetPointerDragState | null>>;
setSelectedAssetIds: Dispatch<SetStateAction<Set<string>>>;
updateAssetMoveDropFolder: (folderId: string | null) => void;
addUploadedFiles: (files: FileList | File[], options?: UploadFilesOptions) => void;
addUploadedFiles: (
files: FileList | File[],
options?: UploadFilesOptions,
) => void;
moveAssetToFolder: (assetId: string, folderId: string) => void;
startRenamingAsset: (asset: EditorAsset) => void;
commitAssetRename: (asset: EditorAsset) => void;
@@ -81,7 +86,9 @@ export function ImageCanvasAssetRowView({
const isFailedUpload = asset.uploadStatus === 'failed';
const showcaseReviewStatus = asset.showcaseReviewStatus ?? null;
const canSubmitShowcaseAsset =
asset.sourceType === 'generated' && asset.persisted && !showcaseReviewStatus;
asset.sourceType === 'generated' &&
asset.persisted &&
!showcaseReviewStatus;
const showcaseMenuLabel =
showcaseReviewStatus === 'pending'
? '精选审核中'
@@ -153,13 +160,7 @@ export function ImageCanvasAssetRowView({
? '视频'
: `${asset.width} x ${asset.height}`;
const mediaOverlay =
asset.mediaType === 'audio' ? (
<div
className={`image-canvas-editor__asset-media-overlay ${mediaPreviewClassName}`}
>
<Music2 className="h-5 w-5" />
</div>
) : asset.mediaType === 'video' ? (
asset.mediaType === 'video' ? (
<div
className={`image-canvas-editor__asset-media-overlay ${mediaPreviewClassName}`}
>
@@ -279,9 +280,7 @@ export function ImageCanvasAssetRowView({
rowClassName={[
'image-canvas-editor__asset-row',
isUploadingAsset ? 'image-canvas-editor__asset-row--uploading' : '',
isFailedUpload
? 'image-canvas-editor__asset-row--upload-failed'
: '',
isFailedUpload ? 'image-canvas-editor__asset-row--upload-failed' : '',
]
.filter(Boolean)
.join(' ')}
@@ -291,12 +290,18 @@ export function ImageCanvasAssetRowView({
imageSrc={
asset.mediaType === 'image'
? asset.src
: (asset.thumbnailSrc ?? '')
: asset.mediaType === 'audio'
? AUDIO_ASSET_COVER_SRC
: (asset.thumbnailSrc ?? '')
}
objectKey={asset.mediaType === 'image' ? asset.objectKey : undefined}
previewNode={videoPreview}
titleNode={
isUploadingAsset || isFailedUpload ? <span>{asset.label}</span> : titleNode
isUploadingAsset || isFailedUpload ? (
<span>{asset.label}</span>
) : (
titleNode
)
}
actions={actions}
draggable={!isRenaming && !isUploadingAsset && !isFailedUpload}
+132 -60
View File
@@ -1585,7 +1585,9 @@ html[data-mobile-keyboard-open='true'] #root {
background: rgba(255, 252, 248, 0.97);
box-shadow: 0 18px 36px rgba(74, 34, 15, 0.18);
pointer-events: none;
transform: translateX(calc(-50% + var(--floating-feedback-contact-offset, 0px)));
transform: translateX(
calc(-50% + var(--floating-feedback-contact-offset, 0px))
);
}
.floating-feedback-entry-contact-panel img {
@@ -3183,8 +3185,7 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
width: 100%;
height: 100%;
overflow: hidden;
background:
linear-gradient(135deg, rgba(15, 23, 42, 0.06), transparent 56%),
background: linear-gradient(135deg, rgba(15, 23, 42, 0.06), transparent 56%),
#f3f4f6;
}
@@ -3397,7 +3398,8 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
overflow: auto;
background:
linear-gradient(rgba(255, 250, 244, 0.56), rgba(255, 250, 244, 0.56)),
url('/creation-home/background-clay-workspace.png') center top / 64rem 64rem repeat,
url('/creation-home/background-clay-workspace.png') center top / 64rem 64rem
repeat,
linear-gradient(180deg, #fff9f3 0%, #fffdf9 52%, #fff8f0 100%);
color: var(--creation-ink);
}
@@ -3434,8 +3436,7 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
margin: 0;
max-width: 62rem;
color: var(--creation-ink);
font-family:
"Songti SC", "STSong", "Noto Serif SC", "SimSun", serif;
font-family: 'Songti SC', 'STSong', 'Noto Serif SC', 'SimSun', serif;
font-size: clamp(2.35rem, 4.25vw, 3.85rem);
font-weight: 950;
letter-spacing: 0;
@@ -3512,8 +3513,7 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
top: 14%;
width: 58%;
aspect-ratio: 4 / 3;
background:
linear-gradient(135deg, rgba(75, 181, 170, 0.24), transparent 52%),
background: linear-gradient(135deg, rgba(75, 181, 170, 0.24), transparent 52%),
#ffffff;
}
@@ -3522,8 +3522,7 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
top: 28%;
width: 34%;
aspect-ratio: 1;
background:
linear-gradient(135deg, rgba(246, 171, 76, 0.24), transparent 56%),
background: linear-gradient(135deg, rgba(246, 171, 76, 0.24), transparent 56%),
#ffffff;
}
@@ -3532,8 +3531,7 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
bottom: 10%;
width: 36%;
aspect-ratio: 16 / 9;
background:
linear-gradient(135deg, rgba(129, 140, 248, 0.2), transparent 56%),
background: linear-gradient(135deg, rgba(129, 140, 248, 0.2), transparent 56%),
#ffffff;
}
@@ -3585,9 +3583,16 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
min-height: 9.55rem;
border: 1px solid rgba(220, 189, 163, 0.78);
border-radius: 1.12rem;
background:
radial-gradient(circle at 21% 42%, rgba(222, 130, 66, 0.12), transparent 9rem),
linear-gradient(135deg, rgba(255, 254, 251, 0.92), rgba(255, 247, 238, 0.82));
background: radial-gradient(
circle at 21% 42%,
rgba(222, 130, 66, 0.12),
transparent 9rem
),
linear-gradient(
135deg,
rgba(255, 254, 251, 0.92),
rgba(255, 247, 238, 0.82)
);
padding: 1.05rem 1.3rem;
color: inherit;
font: inherit;
@@ -3646,7 +3651,7 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
border-radius: 999px;
background: rgba(112, 62, 32, 0.22);
filter: blur(0.16rem);
content: "";
content: '';
}
.creation-landing__feature-art {
@@ -3961,15 +3966,25 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
place-items: center;
gap: 1rem;
padding: 1rem;
background:
linear-gradient(135deg, rgba(244, 187, 129, 0.4), rgba(255, 247, 236, 0.92)),
radial-gradient(circle at 30% 24%, rgba(255, 255, 255, 0.9), transparent 42%);
color: #9a5428;
background: linear-gradient(
135deg,
rgba(244, 187, 129, 0.4),
rgba(255, 247, 236, 0.92)
),
radial-gradient(
circle at 30% 24%,
rgba(255, 255, 255, 0.9),
transparent 42%
);
}
.creation-landing__asset-audio-preview svg {
width: 2.6rem;
height: 2.6rem;
.creation-landing__asset-audio-cover {
display: block;
width: min(48%, 11rem);
min-width: 3.2rem;
height: auto;
object-fit: contain;
object-position: center;
}
.creation-landing__asset-audio-preview audio {
@@ -4054,8 +4069,11 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
min-height: 0;
grid-template-rows: minmax(0, 1fr) auto;
overflow: hidden;
background:
radial-gradient(circle at 18% 12%, rgba(186, 117, 65, 0.18), transparent 32%),
background: radial-gradient(
circle at 18% 12%,
rgba(186, 117, 65, 0.18),
transparent 32%
),
#100d0b;
}
@@ -4376,7 +4394,10 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
--platform-button-primary-fill,
linear-gradient(135deg, #df7f40, #b95d3a)
);
--image-canvas-brand-text-on-fill: var(--platform-button-primary-text, #fffaf5);
--image-canvas-brand-text-on-fill: var(
--platform-button-primary-text,
#fffaf5
);
--image-canvas-brand-border: rgba(204, 117, 76, 0.34);
--image-canvas-brand-border-strong: rgba(199, 101, 61, 0.64);
--image-canvas-brand-border-soft: rgba(226, 203, 184, 0.82);
@@ -4881,8 +4902,11 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
.image-canvas-editor__media-preview {
position: relative;
isolation: isolate;
background:
radial-gradient(circle at 25% 24%, rgba(255, 255, 255, 0.22), transparent 0.72rem),
background: radial-gradient(
circle at 25% 24%,
rgba(255, 255, 255, 0.22),
transparent 0.72rem
),
linear-gradient(135deg, rgba(15, 23, 42, 0.84), rgba(20, 83, 45, 0.58));
}
@@ -4895,8 +4919,7 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
}
.image-canvas-editor__media-preview--audio::before {
background:
repeating-linear-gradient(
background: repeating-linear-gradient(
90deg,
rgba(103, 232, 249, 0.34) 0 0.18rem,
transparent 0.18rem 0.52rem
@@ -4905,8 +4928,12 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
}
.image-canvas-editor__media-preview--video::before {
background:
linear-gradient(90deg, rgba(15, 23, 42, 0.42) 0 14%, transparent 14% 86%, rgba(15, 23, 42, 0.42) 86%),
background: linear-gradient(
90deg,
rgba(15, 23, 42, 0.42) 0 14%,
transparent 14% 86%,
rgba(15, 23, 42, 0.42) 86%
),
repeating-linear-gradient(
0deg,
rgba(248, 250, 252, 0.34) 0 0.2rem,
@@ -5840,7 +5867,11 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
rgba(250, 245, 255, 0.94),
transparent 30%
),
linear-gradient(135deg, rgba(250, 245, 255, 0.84), rgba(243, 232, 255, 0.72));
linear-gradient(
135deg,
rgba(250, 245, 255, 0.84),
rgba(243, 232, 255, 0.72)
);
}
.image-canvas-editor__generation-frame--character.image-canvas-editor__generation-frame--focused {
@@ -5853,9 +5884,16 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
}
.image-canvas-editor__generation-frame--character-animation {
background:
radial-gradient(circle at 50% 36%, rgba(255, 247, 237, 0.96), transparent 32%),
linear-gradient(135deg, rgba(255, 247, 237, 0.92), rgba(254, 215, 170, 0.74));
background: radial-gradient(
circle at 50% 36%,
rgba(255, 247, 237, 0.96),
transparent 32%
),
linear-gradient(
135deg,
rgba(255, 247, 237, 0.92),
rgba(254, 215, 170, 0.74)
);
}
.image-canvas-editor__generation-frame--character-animation.image-canvas-editor__generation-frame--focused {
@@ -5868,8 +5906,12 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
}
.image-canvas-editor__generation-frame--video {
background:
linear-gradient(90deg, rgba(15, 23, 42, 0.12) 10%, transparent 10% 90%, rgba(15, 23, 42, 0.12) 90%),
background: linear-gradient(
90deg,
rgba(15, 23, 42, 0.12) 10%,
transparent 10% 90%,
rgba(15, 23, 42, 0.12) 90%
),
linear-gradient(135deg, rgba(239, 246, 255, 0.9), rgba(219, 234, 254, 0.82));
}
@@ -5884,8 +5926,7 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
.image-canvas-editor__generation-frame--audio-sound-effect,
.image-canvas-editor__generation-frame--audio-background-music {
background:
repeating-linear-gradient(
background: repeating-linear-gradient(
90deg,
rgba(79, 70, 229, 0.14) 0 0.22rem,
transparent 0.22rem 0.58rem
@@ -6091,8 +6132,11 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
.image-canvas-editor__generation-frame--quick-edit {
border-color: var(--image-canvas-brand-border);
background:
linear-gradient(135deg, rgba(204, 117, 76, 0.12), rgba(240, 203, 169, 0.12)),
background: linear-gradient(
135deg,
rgba(204, 117, 76, 0.12),
rgba(240, 203, 169, 0.12)
),
rgba(255, 255, 255, 0.72);
}
@@ -6109,8 +6153,11 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
.image-canvas-editor__generation-frame--publication {
border-color: rgba(236, 72, 153, 0.28);
background:
radial-gradient(circle at 30% 20%, rgba(244, 114, 182, 0.18), transparent 38%),
background: radial-gradient(
circle at 30% 20%,
rgba(244, 114, 182, 0.18),
transparent 38%
),
linear-gradient(135deg, rgba(236, 72, 153, 0.12), rgba(251, 146, 60, 0.08)),
rgba(255, 247, 252, 0.78);
}
@@ -6311,7 +6358,8 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
transform: translate(-50%, -100%);
}
.image-canvas-editor__floating-toolbar button.image-canvas-editor__floating-toolbar-text-button {
.image-canvas-editor__floating-toolbar
button.image-canvas-editor__floating-toolbar-text-button {
width: auto;
min-width: 0;
gap: 0.35rem;
@@ -8779,8 +8827,11 @@ button.image-canvas-editor__reference-chip:disabled {
z-index: 999;
box-sizing: border-box;
border: 2px solid var(--image-canvas-brand-accent);
background:
linear-gradient(90deg, rgba(204, 117, 76, 0.2) 1px, transparent 1px),
background: linear-gradient(
90deg,
rgba(204, 117, 76, 0.2) 1px,
transparent 1px
),
linear-gradient(rgba(204, 117, 76, 0.2) 1px, transparent 1px);
background-size: 33.333% 33.333%;
box-shadow:
@@ -9335,7 +9386,8 @@ button.image-canvas-editor__reference-chip:disabled {
height: 2rem;
}
.image-canvas-editor__floating-toolbar button.image-canvas-editor__floating-toolbar-text-button {
.image-canvas-editor__floating-toolbar
button.image-canvas-editor__floating-toolbar-text-button {
width: auto;
}
@@ -9632,8 +9684,11 @@ button.image-canvas-editor__reference-chip:disabled {
}
.platform-mobile-home-welcome-overlay {
background:
radial-gradient(circle at 50% 18%, rgba(255, 254, 252, 0.54), transparent 32%),
background: radial-gradient(
circle at 50% 18%,
rgba(255, 254, 252, 0.54),
transparent 32%
),
rgba(61, 31, 16, 0.24);
}
@@ -9642,8 +9697,11 @@ button.image-canvas-editor__reference-chip:disabled {
overflow: hidden;
border-radius: 1.65rem;
border: 1px solid rgba(255, 255, 255, 0.68);
background:
radial-gradient(circle at 18% 0%, rgba(240, 203, 169, 0.28), transparent 38%),
background: radial-gradient(
circle at 18% 0%,
rgba(240, 203, 169, 0.28),
transparent 38%
),
var(--platform-modal-fill);
box-shadow:
0 24px 58px rgba(112, 57, 30, 0.2),
@@ -12521,7 +12579,8 @@ button.image-canvas-editor__reference-chip:disabled {
padding: 0 !important;
background:
linear-gradient(rgba(255, 250, 244, 0.72), rgba(255, 250, 244, 0.72)),
url('/creation-home/background-clay-workspace.png') center top / 64rem 64rem repeat,
url('/creation-home/background-clay-workspace.png') center top / 64rem 64rem
repeat,
linear-gradient(180deg, #fffaf4 0%, #fffdf9 52%, #fff8f0 100%);
box-shadow: 0 24px 58px rgba(112, 57, 30, 0.08);
}
@@ -12656,7 +12715,7 @@ button.image-canvas-editor__reference-chip:disabled {
width: 1px;
height: 1.66rem;
background: rgba(217, 190, 166, 0.82);
content: "";
content: '';
transform: translateY(-50%);
}
@@ -12691,7 +12750,9 @@ button.image-canvas-editor__reference-chip:disabled {
display: none;
}
.platform-desktop-shell--workbench .platform-desktop-topbar button.platform-desktop-search {
.platform-desktop-shell--workbench
.platform-desktop-topbar
button.platform-desktop-search {
width: auto;
max-width: none !important;
min-width: 7.35rem;
@@ -12702,12 +12763,17 @@ button.image-canvas-editor__reference-chip:disabled {
gap: 0.38rem !important;
}
.platform-desktop-shell--workbench .platform-desktop-topbar button.platform-desktop-search:hover {
.platform-desktop-shell--workbench
.platform-desktop-topbar
button.platform-desktop-search:hover {
border-color: rgba(214, 184, 159, 0.52);
background: rgba(255, 250, 244, 0.56);
}
.platform-desktop-shell--workbench .platform-desktop-topbar button.platform-desktop-search > span:first-child {
.platform-desktop-shell--workbench
.platform-desktop-topbar
button.platform-desktop-search
> span:first-child {
width: 2.06rem;
height: 2.06rem;
}
@@ -12750,12 +12816,16 @@ button.image-canvas-editor__reference-chip:disabled {
box-shadow: none;
}
.platform-desktop-shell--workbench .platform-desktop-rail__button--active .platform-desktop-rail__icon-shell {
.platform-desktop-shell--workbench
.platform-desktop-rail__button--active
.platform-desktop-rail__icon-shell {
background: #fff2e7;
box-shadow: none;
}
.platform-desktop-shell--workbench .platform-desktop-rail__button--active .platform-desktop-rail__icon {
.platform-desktop-shell--workbench
.platform-desktop-rail__button--active
.platform-desktop-rail__icon {
color: #c7653d;
}
@@ -12795,7 +12865,9 @@ button.image-canvas-editor__reference-chip:disabled {
box-shadow: none;
}
.platform-desktop-shell--workbench .platform-desktop-rail__button--active .platform-desktop-rail__label {
.platform-desktop-shell--workbench
.platform-desktop-rail__button--active
.platform-desktop-rail__label {
color: #c7653d;
}