diff --git a/apps/web/index.html b/apps/web/index.html index 75c4e5bac6..f5c2ffac06 100644 --- a/apps/web/index.html +++ b/apps/web/index.html @@ -4,7 +4,8 @@ - + + DSH Local Build diff --git a/apps/web/public/favicon-dark.svg b/apps/web/public/favicon-dark.svg new file mode 100644 index 0000000000..648fe4f2b9 --- /dev/null +++ b/apps/web/public/favicon-dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/public/favicon.svg b/apps/web/public/favicon.svg index c92f15d43b..754615a37a 100644 --- a/apps/web/public/favicon.svg +++ b/apps/web/public/favicon.svg @@ -1,8 +1,3 @@ - diff --git a/apps/web/src/favicon.ts b/apps/web/src/favicon.ts deleted file mode 100644 index ac08a9f84f..0000000000 --- a/apps/web/src/favicon.ts +++ /dev/null @@ -1,26 +0,0 @@ -/** Browser-tab icons follow the system scheme independently of the application theme. */ -import favicon from '../public/favicon.svg?raw' - -/** - * Install fixed-color icons and a document-lifetime system-scheme listener. - * Browser favicon rasterizers can cache SVG media-query results across scheme changes. - * Distinct data URLs select explicit colors without duplicating the source artwork. - */ -export function installFavicon(): void { - const link = document.querySelector('link[rel="icon"]') - if (link === null) throw new Error('web app: missing favicon link') - const svg = new DOMParser().parseFromString(favicon, 'image/svg+xml') - svg.querySelector('style')?.remove() - const path = svg.querySelector('path') - if (path === null) throw new Error('web app: missing favicon path') - const icon = (fill: string): string => { - path.setAttribute('fill', fill) - return `data:image/svg+xml,${encodeURIComponent(new XMLSerializer().serializeToString(svg))}` - } - const light = icon('#000') - const dark = icon('#fff') - const scheme = window.matchMedia('(prefers-color-scheme: dark)') - const update = (): void => { link.href = scheme.matches ? dark : light } - update() - scheme.addEventListener('change', update) -} diff --git a/apps/web/src/main.ts b/apps/web/src/main.ts index 9f1fa593a1..1a84cd5f63 100644 --- a/apps/web/src/main.ts +++ b/apps/web/src/main.ts @@ -1,6 +1,5 @@ /** Browser entry for the Web client. */ import { AppWebEntry, applyIndexInjections } from '@deepseek-ai/dsh-client-web' -import { installFavicon } from './favicon.ts' interface DesktopBootGlobal { dshDesktopBoot?: { @@ -15,7 +14,6 @@ const reportFailure = (reason: unknown): void => { } try { - installFavicon() const el = document.getElementById('root') if (el === null) throw new Error('web app: missing #root') const entry = new AppWebEntry(el) diff --git a/apps/web/tests/desktop-boot.spec.ts b/apps/web/tests/desktop-boot.spec.ts index 9bea3def5f..cac91ad7e7 100644 --- a/apps/web/tests/desktop-boot.spec.ts +++ b/apps/web/tests/desktop-boot.spec.ts @@ -1,5 +1,5 @@ // @vitest-environment jsdom -import { afterEach, beforeEach, expect, it, vi } from 'vitest' +import { afterEach, expect, it, vi } from 'vitest' const boot = vi.hoisted(() => ({ run: vi.fn(), @@ -10,16 +10,10 @@ vi.mock('@deepseek-ai/dsh-client-web', () => ({ applyIndexInjections: boot.applyIndexInjections, })) -beforeEach(() => { - document.head.innerHTML = '' - vi.stubGlobal('matchMedia', () => ({ matches: false, addEventListener: vi.fn() })) -}) - afterEach(() => { vi.unstubAllGlobals() vi.clearAllMocks() vi.resetModules() - document.head.replaceChildren() document.body.replaceChildren() }) diff --git a/apps/web/tests/favicon.spec.ts b/apps/web/tests/favicon.spec.ts deleted file mode 100644 index af932bc4fc..0000000000 --- a/apps/web/tests/favicon.spec.ts +++ /dev/null @@ -1,55 +0,0 @@ -// @vitest-environment jsdom -import { afterEach, expect, it, vi } from 'vitest' -import { installFavicon } from '../src/favicon.ts' - -const originalHead = document.head.innerHTML - -afterEach(() => { - document.head.innerHTML = originalHead - document.documentElement.style.removeProperty('color-scheme') - vi.unstubAllGlobals() -}) - -it.each([false, true])('selects fixed-color tab icons on boot and system changes (dark=%s)', (initialDark) => { - const link = document.createElement('link') - link.rel = 'icon' - link.href = '/favicon.svg' - document.head.append(link) - let dark = initialDark - let onChange = (): void => { throw new Error('missing scheme listener') } - const matchMedia = vi.fn(() => ({ - get matches() { return dark }, - addEventListener: (event: string, listener: () => void) => { - expect(event).toBe('change') - onChange = listener - }, - })) - vi.stubGlobal('matchMedia', matchMedia) - const readIcon = (): string => decodeURIComponent(link.href.slice('data:image/svg+xml,'.length)) - const expectFill = (fill: string): void => { - expect(link.href).toMatch(/^data:image\/svg\+xml,/) - const svg = new DOMParser().parseFromString(readIcon(), 'image/svg+xml') - expect(svg.querySelector('parsererror')).toBeNull() - expect(svg.querySelector('style')).toBeNull() - expect(svg.querySelector('path')?.getAttribute('fill')).toBe(fill) - expect(svg.documentElement.getAttribute('viewBox')).toBe('0 0 50 50') - } - - document.documentElement.style.colorScheme = initialDark ? 'light' : 'dark' - installFavicon() - expect(matchMedia).toHaveBeenCalledWith('(prefers-color-scheme: dark)') - expectFill(initialDark ? '#fff' : '#000') - const initial = link.href - dark = !initialDark - onChange() - expectFill(initialDark ? '#000' : '#fff') - expect(link.href).not.toBe(initial) - dark = initialDark - onChange() - expect(link.href).toBe(initial) -}) - -it('requires the document favicon link', () => { - document.querySelector('link[rel="icon"]')?.remove() - expect(installFavicon).toThrow('web app: missing favicon link') -}) diff --git a/apps/web/tests/pwa-manifest.e2e.ts b/apps/web/tests/pwa-manifest.e2e.ts index db5e4c66bf..2cfc21e2c9 100644 --- a/apps/web/tests/pwa-manifest.e2e.ts +++ b/apps/web/tests/pwa-manifest.e2e.ts @@ -28,10 +28,14 @@ it('ships install metadata with the built web application', async () => { }) }) -it('ships a favicon that switches to a light mark under dark color scheme', async () => { - const favicon = await readFile(join(DIST_ROOT, 'favicon.svg'), 'utf8') - // The light fill must live inside the dark-scheme media query, so the icon - // stays black in light mode and only turns white under a dark scheme. - expect(favicon).toMatch(/@media \(prefers-color-scheme: dark\)\s*{\s*path\s*{[^}]*fill:\s*#fff/i) - expect(favicon).toContain('fill="#000"') +it('ships fixed-color favicons selected by document media queries', async () => { + const index = await readFile(join(DIST_ROOT, 'index.html'), 'utf8') + expect(index).toContain('') + expect(index).toContain('') + const light = await readFile(join(DIST_ROOT, 'favicon.svg'), 'utf8') + const dark = await readFile(join(DIST_ROOT, 'favicon-dark.svg'), 'utf8') + expect(light).not.toContain('