Themes: Dracula, Material, Adwaita, Flat Remix, Paper, Nordic, each light, dark or Auto

The theme picker lists Modern (the old Dark and Light), Classic and six new
palettes, from Dracula's spec (with Alucard), Material 3's baseline scheme,
libadwaita's CSS variables, Flat Remix's _colors.scss, Paper and Nord. A second
setting picks Light, Dark or Auto where a theme has both; Classic and Paper do
not, so it is hidden for them.

The page gets data-mode, light or dark, and Auto is worked out in theme.ts from
the system, so each palette is written once instead of again under a media
query. Every new palette clears WCAG AA for text on its backgrounds. An old
ipx.theme of dark, light or auto carries over as Modern.

Closes #27.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-18 12:53:25 +00:00
parent 26802d2b23
commit e2969bcee8
6 changed files with 294 additions and 51 deletions

View File

@@ -20,28 +20,54 @@ test('the page loads and lists the configured feeds', async ({ page }) => {
expect(errors, 'the page script must not throw at load').toEqual([]);
});
test('the theme dropdown in Settings jumps straight to a theme, including Auto', async ({ page }) => {
const theme = () => page.evaluate(() => document.documentElement.dataset.theme);
test('Settings picks a theme and, where it has both, light, dark or Auto', async ({ page }) => {
const root = () => page.evaluate(() => [document.documentElement.dataset.theme, document.documentElement.dataset.mode]);
const bg = () => page.evaluate(() => getComputedStyle(document.body).backgroundColor);
await page.locator('#prefs').click();
await expect(page.locator('#stheme')).toHaveValue(await theme());
await expect(page.locator('#stheme')).toHaveValue((await root())[0]);
await page.locator('#stheme').selectOption('auto');
await expect.poll(theme).toBe('auto');
// Auto follows the system; emulating a light system must show the light palette live,
// no reload needed, since it is a media query rather than something JS picks per click.
await page.locator('#stheme').selectOption('modern');
await page.locator('#smode').selectOption('auto');
// Auto follows the system, live, with no reload.
await page.emulateMedia({ colorScheme: 'light' });
await expect.poll(() => page.evaluate(() => getComputedStyle(document.body).backgroundColor))
.toBe('rgb(242, 244, 247)'); // --bg in the light palette
await expect.poll(root).toEqual(['modern', 'light']);
await expect.poll(bg).toBe('rgb(242, 244, 247)'); // Modern's light --bg
await page.emulateMedia({ colorScheme: 'dark' });
await expect.poll(() => page.evaluate(() => getComputedStyle(document.body).backgroundColor))
.toBe('rgb(14, 19, 27)'); // the bare :root is already dark; Auto adds nothing here
await expect.poll(root).toEqual(['modern', 'dark']);
await expect.poll(bg).toBe('rgb(14, 19, 27)'); // Modern's dark --bg
// Dracula, then its light half, Alucard.
await page.locator('#stheme').selectOption('dracula');
await page.locator('#smode').selectOption('dark');
await expect.poll(bg).toBe('rgb(40, 42, 54)'); // #282A36
await page.locator('#smode').selectOption('light');
await expect.poll(bg).toBe('rgb(255, 251, 235)'); // #FFFBEB
// Classic and Paper come one way only, so there is nothing to choose.
await page.locator('#stheme').selectOption('paper');
await expect(page.locator('#smode')).toBeHidden();
await expect.poll(bg).toBe('rgb(242, 238, 222)'); // #F2EEDE
await page.locator('#stheme').selectOption('classic');
await expect(page.locator('#smode')).toBeHidden();
await page.reload();
await expect.poll(theme).toBe('classic');
await expect.poll(root).toEqual(['classic', 'light']);
// The 2004 Mac app set its type in Lucida Grande.
expect(await page.evaluate(() => getComputedStyle(document.body).fontFamily)).toContain('Lucida Grande');
expect(await page.locator('#theme').count(), 'the theme lives in Settings only').toBe(0);
// Back to Nordic, dark: the mode chosen before Classic is kept for the themes that have one.
await page.locator('#prefs').click();
await page.locator('#stheme').selectOption('nordic');
await expect(page.locator('#smode')).toHaveValue('light');
await page.locator('#smode').selectOption('dark');
await expect.poll(bg).toBe('rgb(46, 52, 64)'); // nord0
});
test('a theme saved before there was light and dark carries over', async ({ page }) => {
await page.evaluate(() => { localStorage.setItem('ipx.theme', 'light'); localStorage.removeItem('ipx.mode'); });
await page.reload();
expect(await page.evaluate(() => [document.documentElement.dataset.theme, document.documentElement.dataset.mode]))
.toEqual(['modern', 'light']);
});
test('settings opens and saves the global schedule', async ({ page }) => {