From b06f60a74c32618746a35cffc2d4be9362dd8756 Mon Sep 17 00:00:00 2001 From: huyan Date: Tue, 8 Sep 2026 23:35:36 +0800 Subject: [PATCH 01/10] fix(brand): render the mark with inherited text color External marks can retain stale colors. Add an inline mark with isolated masks. The mark follows surrounding text. --- frontend/src/shared/ui/windup-mark.tsx | 44 ++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 frontend/src/shared/ui/windup-mark.tsx diff --git a/frontend/src/shared/ui/windup-mark.tsx b/frontend/src/shared/ui/windup-mark.tsx new file mode 100644 index 00000000..7bbaff42 --- /dev/null +++ b/frontend/src/shared/ui/windup-mark.tsx @@ -0,0 +1,44 @@ +import { useId, type SVGProps } from 'react' + +/** 内联标志跟随文字颜色;独立遮罩避免同页多个实例互相引用。 */ +export function WindupMark(props: SVGProps) { + const maskId = useId() + return ( + + ) +} From 8171af3fe9db3f13aaa8791cae2c4a79eb5e092e Mon Sep 17 00:00:00 2001 From: huyan Date: Tue, 8 Sep 2026 23:35:36 +0800 Subject: [PATCH 02/10] fix(navigation): correct theme mark presentation Navigation retains external marks and a bright inset line. Use inline marks and remove the inset shadow. Navigation follows the page palette. --- frontend/src/app/layout/app-header.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/frontend/src/app/layout/app-header.tsx b/frontend/src/app/layout/app-header.tsx index dfe556d2..e6e9a93e 100644 --- a/frontend/src/app/layout/app-header.tsx +++ b/frontend/src/app/layout/app-header.tsx @@ -1,3 +1,4 @@ +import { WindupMark } from '@/shared/ui/windup-mark' import { useEffect, useRef, useState } from 'react' import { Link, useLocation, useNavigate } from 'react-router' @@ -227,7 +228,7 @@ export function AppHeader({
@@ -240,7 +241,7 @@ export function AppHeader({ wave.entry === 'brand' ? 'app-header-text-wave' : '' }`} > - + @@ -430,10 +431,9 @@ export function AppHeader({ accountMenu.expanded ? 'scale-110' : 'scale-100' }`} > - @@ -550,7 +550,7 @@ function MarketingHeaderView({ session, wave, onWave }: MarketingHeaderViewProps wave.entry === 'brand' ? 'app-header-text-wave' : '' }`} > - + From 793696f44e3919678b22309df268f48acfd76792 Mon Sep 17 00:00:00 2001 From: huyan Date: Tue, 8 Sep 2026 23:35:36 +0800 Subject: [PATCH 03/10] fix(account-panel): use the inline brand mark The panel loads a cached external mark. Render the shared inline mark. The mark inherits panel text color. --- frontend/src/features/account-panel/index.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/features/account-panel/index.tsx b/frontend/src/features/account-panel/index.tsx index 083ee886..b1a40bc4 100644 --- a/frontend/src/features/account-panel/index.tsx +++ b/frontend/src/features/account-panel/index.tsx @@ -1,3 +1,4 @@ +import { WindupMark } from '@/shared/ui/windup-mark' import { useEffect, useId, @@ -668,7 +669,7 @@ function AccountPanelDialog({ className="auth-screen-brand fixed z-[2] flex items-center gap-[0.7rem]" aria-label="Windup" > - + Windup
From e0334bd8f4e0e3d6b40c785c992db7a4af90bcee Mon Sep 17 00:00:00 2001 From: huyan Date: Tue, 8 Sep 2026 23:35:36 +0800 Subject: [PATCH 04/10] fix(account): use the inline profile mark The default avatar loads a cached external mark. Render the shared inline mark. The avatar follows account text color. --- frontend/src/pages/account/index.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/pages/account/index.tsx b/frontend/src/pages/account/index.tsx index e1a3a5ee..1a50baff 100644 --- a/frontend/src/pages/account/index.tsx +++ b/frontend/src/pages/account/index.tsx @@ -1,3 +1,4 @@ +import { WindupMark } from '@/shared/ui/windup-mark' import { Gift, X } from '@phosphor-icons/react' import { useEffect, @@ -734,10 +735,9 @@ export function AccountPage() {
- From 2dbdd0096df5e56c9ed52571ac57c1b267dffb6e Mon Sep 17 00:00:00 2001 From: huyan Date: Tue, 8 Sep 2026 23:35:36 +0800 Subject: [PATCH 05/10] fix(landing): use the inline footer mark The footer loads a cached external mark. Render the shared inline mark. The footer mark follows its text color. --- frontend/src/pages/landing/index.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/pages/landing/index.tsx b/frontend/src/pages/landing/index.tsx index 15e88e73..8d9cc095 100644 --- a/frontend/src/pages/landing/index.tsx +++ b/frontend/src/pages/landing/index.tsx @@ -1,3 +1,4 @@ +import { WindupMark } from '@/shared/ui/windup-mark' import { useEffect, useRef, useState } from 'react' import type { CSSProperties } from 'react' import { Link } from 'react-router' @@ -749,7 +750,7 @@ export function LandingPage() { aria-label="返回 Windup 宣传页" className="inline-flex items-center gap-3 rounded-sm focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-app-accent" > - + Windup

From 737de5bfd839f84201ad716f82954e04e90ceb6d Mon Sep 17 00:00:00 2001 From: huyan Date: Tue, 8 Sep 2026 23:35:37 +0800 Subject: [PATCH 06/10] fix(favicon): refresh the theme asset URL The favicon URL has an immutable cache lifetime. Version the updated favicon reference. Browsers request the theme-aware asset. --- frontend/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/index.html b/frontend/index.html index 59c6a4a6..e6603f32 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -3,7 +3,7 @@ - + Date: Tue, 8 Sep 2026 23:35:37 +0800 Subject: [PATCH 07/10] fix(previews): handle unavailable source images Failed sources leave broken images in preview cards. Show a status after the original image fails. Thumbnail fallback and changed URLs remain usable. --- frontend/src/shared/ui/asset-preview-card.tsx | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/frontend/src/shared/ui/asset-preview-card.tsx b/frontend/src/shared/ui/asset-preview-card.tsx index 77dedbd2..6d862300 100644 --- a/frontend/src/shared/ui/asset-preview-card.tsx +++ b/frontend/src/shared/ui/asset-preview-card.tsx @@ -73,14 +73,22 @@ export function AssetPreviewSurface({ className = '', thumbnail = false, }: AssetPreviewSurfaceProps) { + const [failedUrl, setFailedUrl] = useState(null) + const unavailable = !!previewUrl && failedUrl === previewUrl + const handleError: ImgHTMLAttributes['onError'] = (event) => { + // 缩略图失败仍要尝试原图;只有原图也失败才显示缺图状态。 + if (event.currentTarget.getAttribute('src') === previewUrl) setFailedUrl(previewUrl) + } + return (

- {previewUrl ? ( + {previewUrl && !unavailable ? ( thumbnail ? ( ) ) : ( -
+
)} From a52977faffa4ede8ddf5e7b332304b4bf3d70816 Mon Sep 17 00:00:00 2001 From: huyan Date: Tue, 8 Sep 2026 23:35:37 +0800 Subject: [PATCH 08/10] test(previews): cover unavailable image recovery Failed sources need explicit recovery coverage. Exercise fallback and replacement URLs. Regressions in failure handling are detected. --- .../src/shared/ui/asset-preview-card.test.tsx | 39 ++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/frontend/src/shared/ui/asset-preview-card.test.tsx b/frontend/src/shared/ui/asset-preview-card.test.tsx index 6bda0c94..08dfe7c8 100644 --- a/frontend/src/shared/ui/asset-preview-card.test.tsx +++ b/frontend/src/shared/ui/asset-preview-card.test.tsx @@ -3,7 +3,7 @@ import { cleanup, fireEvent, render, screen } from '@testing-library/react' import { afterEach, describe, expect, it, vi } from 'vitest' import { MemoryRouter } from 'react-router' -import { AssetPreviewCard, AssetThumbnailImage } from './asset-preview-card' +import { AssetPreviewCard, AssetPreviewSurface, AssetThumbnailImage } from './asset-preview-card' afterEach(cleanup) @@ -203,3 +203,40 @@ describe('AssetThumbnailImage', () => { ) }) }) + +describe('unavailable preview images', () => { + it('replaces a failed source with a status and retries a different URL', () => { + const { rerender } = render( + , + ) + fireEvent.error(screen.getByRole('img', { name: '角色预览' })) + expect(screen.queryByRole('img')).toBeNull() + expect(screen.getByRole('status').textContent).toContain('无法加载') + rerender( + , + ) + expect(screen.getByRole('img').getAttribute('src')).toBe('https://storage.windup.test/new.png') + expect(screen.queryByRole('status')).toBeNull() + }) + + it('tries the original after thumbnail failure before showing unavailable', () => { + render( + , + ) + fireEvent.error(screen.getByRole('img')) + expect(screen.queryByRole('status')).toBeNull() + expect(screen.getByRole('img').getAttribute('src')).toBe( + 'https://media.windup.test/media/outfit-preview/old.source.png', + ) + fireEvent.error(screen.getByRole('img')) + expect(screen.queryByRole('img')).toBeNull() + expect(screen.getByRole('status')).toBeTruthy() + }) +}) From 5f20f9b934ae6e01665b946bc6e59fdb1cf462cc Mon Sep 17 00:00:00 2001 From: huyan Date: Tue, 8 Sep 2026 23:35:37 +0800 Subject: [PATCH 09/10] test(account): recognize inline decorative marks Default account marks now render inline. Assert their decorative accessibility contract. Existing account behavior remains covered. --- frontend/src/app/layout/app-header.test.tsx | 6 +++--- frontend/src/pages/account/index.test.tsx | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/layout/app-header.test.tsx b/frontend/src/app/layout/app-header.test.tsx index c023da86..5cbf91e5 100644 --- a/frontend/src/app/layout/app-header.test.tsx +++ b/frontend/src/app/layout/app-header.test.tsx @@ -481,9 +481,9 @@ describe('AppHeader', () => { const accountMenu = await screen.findByRole('button', { name: '打开账号菜单' }) expect(accountMenu.textContent).toContain('reader@example.com') - expect(within(accountMenu).getByTestId('default-account-avatar').getAttribute('src')).toBe( - '/windup-mark.svg', - ) + expect( + within(accountMenu).getByTestId('default-account-avatar').getAttribute('aria-hidden'), + ).toBe('true') }) it('账号菜单在点击外部或按 Escape 时关闭', async () => { diff --git a/frontend/src/pages/account/index.test.tsx b/frontend/src/pages/account/index.test.tsx index 3dc1d755..995d4109 100644 --- a/frontend/src/pages/account/index.test.tsx +++ b/frontend/src/pages/account/index.test.tsx @@ -137,8 +137,8 @@ describe('AccountPage', () => { ) expect(screen.getByRole('heading', { name: '个人资料' })).toBeTruthy() expect(screen.getByLabelText('昵称').getAttribute('placeholder')).toBe('输入昵称') - expect(screen.getByTestId('account-profile-default-avatar').getAttribute('src')).toBe( - '/windup-mark.svg', + expect(screen.getByTestId('account-profile-default-avatar').getAttribute('aria-hidden')).toBe( + 'true', ) expect(screen.queryByLabelText('邮箱验证码')).toBeNull() From c7ede56042467d512010241a05b0e8dae36d410b Mon Sep 17 00:00:00 2001 From: huyan Date: Sat, 19 Sep 2026 21:12:37 +0800 Subject: [PATCH 10/10] test(quick-start): wait for active run persistence The header pointer is written by an asynchronous effect. Wait for the stored run identifier instead of transcript rendering. The assertion now observes the state it actually verifies. --- frontend/src/pages/quick-start/index.test.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/src/pages/quick-start/index.test.tsx b/frontend/src/pages/quick-start/index.test.tsx index 6e1b605c..cbc675a5 100644 --- a/frontend/src/pages/quick-start/index.test.tsx +++ b/frontend/src/pages/quick-start/index.test.tsx @@ -1468,8 +1468,7 @@ describe('QuickStartPage', () => { it('生成结束后等待用户选择时仍保留返回入口', async () => { renderStateFixture('template-selecting') - await screen.findByTestId('quick-start-transcript') - expect(readActiveRun('7')).toBe('run-1') + await waitFor(() => expect(readActiveRun('7')).toBe('run-1')) }) it('renders Agent replies with the animated bot and Markdown structure', async () => {