diff --git a/apps/admin-web/src/pages/AdminEditorAssetQueryPage.test.tsx b/apps/admin-web/src/pages/AdminEditorAssetQueryPage.test.tsx index 9c7612f3d..3d075d2ed 100644 --- a/apps/admin-web/src/pages/AdminEditorAssetQueryPage.test.tsx +++ b/apps/admin-web/src/pages/AdminEditorAssetQueryPage.test.tsx @@ -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( - , + , ); expect(await screen.findByText('作者昵称')).toBeTruthy(); @@ -73,25 +76,22 @@ test('后台素材查询展示作者昵称和陶泥号', async () => { test('后台素材查询按用户、搜索和时间调用查询接口', async () => { render( - , + , ); - 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( - , + , ); 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( - , + , ); - 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( + , + ); + + 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( - , + , ); expect(await screen.findByText('角色形象 1')).toBeTruthy(); @@ -164,15 +181,12 @@ test('后台素材查询格式化微秒时间文本', async () => { test('后台素材查询可查看素材详情', async () => { render( - , + , ); - 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( - , + , ); - 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(); }); }); diff --git a/apps/admin-web/src/pages/AdminEditorAssetQueryPage.tsx b/apps/admin-web/src/pages/AdminEditorAssetQueryPage.tsx index b5b705e9a..50f588c25 100644 --- a/apps/admin-web/src/pages/AdminEditorAssetQueryPage.tsx +++ b/apps/admin-web/src/pages/AdminEditorAssetQueryPage.tsx @@ -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} 泥点 {entry.model || '-'} - {entry.provider || '-'} + + {entry.provider || '-'} + {entry.taskId || '-'} {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 | null | undefined) { +function formatGenerationInputs( + value: Record | null | undefined, +) { if (!value) { return '-'; } diff --git a/apps/admin-web/src/pages/AdminEditorShowcaseReviewPage.test.tsx b/apps/admin-web/src/pages/AdminEditorShowcaseReviewPage.test.tsx index f67e00914..ac11c94bd 100644 --- a/apps/admin-web/src/pages/AdminEditorShowcaseReviewPage.test.tsx +++ b/apps/admin-web/src/pages/AdminEditorShowcaseReviewPage.test.tsx @@ -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( + , + ); + + 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( />, ); - 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(() => { diff --git a/apps/admin-web/src/pages/AdminEditorShowcaseReviewPage.tsx b/apps/admin-web/src/pages/AdminEditorShowcaseReviewPage.tsx index f3fbd561c..302d4d074 100644 --- a/apps/admin-web/src/pages/AdminEditorShowcaseReviewPage.tsx +++ b/apps/admin-web/src/pages/AdminEditorShowcaseReviewPage.tsx @@ -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({ {entry.label || '-'} - - {formatDateTime(entry.submittedAt)} - + {formatDateTime(entry.submittedAt)} {authorDisplayName(entry)} {entry.authorPublicUserCode?.trim() || '-'} @@ -342,7 +341,9 @@ export function AdminEditorShowcaseReviewPage({ )} - + {reviewStatusLabel(entry.reviewStatus)} {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({ {entry.likeCount} {entry.model || '-'} - {entry.provider || '-'} + + {entry.provider || '-'} + {entry.taskId || '-'} {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 | null | undefined) { +function formatGenerationInputs( + value: Record | null | undefined, +) { if (!value) { return '-'; } diff --git a/public/creation-home/audio-asset-cover.svg b/public/creation-home/audio-asset-cover.svg new file mode 100644 index 000000000..0e96909aa --- /dev/null +++ b/public/creation-home/audio-asset-cover.svg @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/src/components/creation-home/CreationLandingView.test.tsx b/src/components/creation-home/CreationLandingView.test.tsx index 82449027e..b80a8d7f6 100644 --- a/src/components/creation-home/CreationLandingView.test.tsx +++ b/src/components/creation-home/CreationLandingView.test.tsx @@ -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', () => { , ); - 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( - + , ); @@ -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(); }); diff --git a/src/components/creation-home/CreationLandingView.tsx b/src/components/creation-home/CreationLandingView.tsx index 3a50a5a70..833e6f490 100644 --- a/src/components/creation-home/CreationLandingView.tsx +++ b/src/components/creation-home/CreationLandingView.tsx @@ -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 (
-
@@ -418,7 +424,8 @@ function CreationShowcaseModal({ onClick={() => onSelectIndex(index)} > - {preview.mediaType === 'image' ? ( + {preview.mediaType === 'image' || + preview.mediaType === 'audio' ? ( ) : ( getPreviewIcon(preview) @@ -455,9 +462,13 @@ export function CreationLandingView({ >([]); const [showcaseCampaign, setShowcaseCampaign] = useState(null); - const [showcaseNextCursor, setShowcaseNextCursor] = useState(null); + const [showcaseNextCursor, setShowcaseNextCursor] = useState( + null, + ); const [assetError, setAssetError] = useState(null); - const [showcaseLoadMoreError, setShowcaseLoadMoreError] = useState(null); + const [showcaseLoadMoreError, setShowcaseLoadMoreError] = useState< + string | null + >(null); const [activeShowcaseTab, setActiveShowcaseTab] = useState('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 ( <> -
+
{showcaseItems.map((item) => { const showcaseId = item.showcaseId?.trim(); const isLiked = Boolean( @@ -830,7 +859,11 @@ export function CreationLandingView({ disabled={isLikePending} onClick={() => handleToggleShowcaseLike(item)} > - + {item.likeCount ?? 0} ) : null} @@ -1015,7 +1048,11 @@ export function CreationLandingView({

陶泥儿精选

-
+
{SHOWCASE_TABS.map((tab) => (
); }, })); 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, diff --git a/src/components/image-editor/ImageCanvasAssetRowView.tsx b/src/components/image-editor/ImageCanvasAssetRowView.tsx index 393500139..c6bd2093a 100644 --- a/src/components/image-editor/ImageCanvasAssetRowView.tsx +++ b/src/components/image-editor/ImageCanvasAssetRowView.tsx @@ -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>; setSelectedAssetIds: Dispatch>>; 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' ? ( -
- -
- ) : asset.mediaType === 'video' ? ( + asset.mediaType === 'video' ? (
@@ -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 ? {asset.label} : titleNode + isUploadingAsset || isFailedUpload ? ( + {asset.label} + ) : ( + titleNode + ) } actions={actions} draggable={!isRenaming && !isUploadingAsset && !isFailedUpload} diff --git a/src/index.css b/src/index.css index 7d873657e..f1022184c 100644 --- a/src/index.css +++ b/src/index.css @@ -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; }