fix(desktop): Dateidialog folgt dem Dunkelmodus (GTK prefer-dark via set_theme)
Tessera CI/CD / Lint & Type Check (push) Successful in 1m0s
Tessera CI/CD / Tests (push) Successful in 1m49s
Tessera CI/CD / Desktop-Pakete bauen (push) Successful in 15m0s
Tessera CI/CD / Build & Publish Images (push) Successful in 3m33s

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-10-02 23:31:26 +02:00
parent 9829228726
commit fffb7ffbec
4 changed files with 108 additions and 2 deletions
@@ -0,0 +1,65 @@
import { act, cleanup, render } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { DESKTOP_COOKIE_NAME } from '@/lib/desktop-client';
import { DesktopThemeSync } from './desktop-theme-sync';
/**
* desktop-theme-sync.test (quick-261002) — meldet hell/dunkel per
* `plugin:window|set_theme` an das Desktop-Fenster, im Browser nichts.
*/
const theme = vi.hoisted(() => ({ resolvedTheme: 'dark' as string | undefined }));
vi.mock('next-themes', () => ({ useTheme: () => theme }));
const invoke = vi.fn(() => Promise.resolve());
function setCookie() {
document.cookie = `${DESKTOP_COOKIE_NAME}=1; path=/`;
}
beforeEach(() => {
invoke.mockClear();
(window as unknown as Record<string, unknown>).__TAURI_INTERNALS__ = { invoke };
theme.resolvedTheme = 'dark';
});
afterEach(() => {
cleanup();
document.cookie = `${DESKTOP_COOKIE_NAME}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`;
delete (window as unknown as Record<string, unknown>).__TAURI_INTERNALS__;
});
describe('DesktopThemeSync', () => {
it('meldet im Desktop-Client das dunkle Erscheinungsbild', async () => {
setCookie();
await act(async () => {
render(<DesktopThemeSync />);
});
expect(invoke).toHaveBeenCalledWith('plugin:window|set_theme', { value: 'dark' });
});
it('meldet einen Wechsel auf hell erneut', async () => {
setCookie();
const view = await act(async () => render(<DesktopThemeSync />));
theme.resolvedTheme = 'light';
await act(async () => {
view.rerender(<DesktopThemeSync />);
});
expect(invoke).toHaveBeenLastCalledWith('plugin:window|set_theme', { value: 'light' });
});
it('ruft im Browser nichts auf', async () => {
await act(async () => {
render(<DesktopThemeSync />);
});
expect(invoke).not.toHaveBeenCalled();
});
it('wartet, solange das Erscheinungsbild noch nicht feststeht', async () => {
setCookie();
theme.resolvedTheme = undefined;
await act(async () => {
render(<DesktopThemeSync />);
});
expect(invoke).not.toHaveBeenCalled();
});
});
@@ -0,0 +1,30 @@
'use client';
import { useTheme } from 'next-themes';
import { useEffect } from 'react';
import { useIsDesktopClient } from '@/lib/desktop-client';
type TauriInvoke = (cmd: string, args?: Record<string, unknown>) => Promise<unknown>;
/**
* DesktopThemeSync (quick-261002) — meldet dem Desktop-Fenster, ob Tessera
* hell oder dunkel dargestellt wird. Unter Linux schaltet das GTK auf die
* dunkle Variante, damit z. B. der Dateidialog eines Uploads nicht grell weiss
* aufgeht; unter Windows folgt die Titelleiste. Im Browser ohne Wirkung.
* Fehler (aeltere Desktop-App ohne Berechtigung) werden still ignoriert.
*/
export function DesktopThemeSync() {
const isDesktop = useIsDesktopClient();
const { resolvedTheme } = useTheme();
useEffect(() => {
if (!isDesktop || (resolvedTheme !== 'dark' && resolvedTheme !== 'light')) return;
const internals = (window as unknown as Record<string, unknown>).__TAURI_INTERNALS__ as
| { invoke?: TauriInvoke }
| undefined;
if (typeof internals?.invoke !== 'function') return;
internals.invoke('plugin:window|set_theme', { value: resolvedTheme }).catch(() => {});
}, [isDesktop, resolvedTheme]);
return null;
}