feat: add the Roundcube Elastic and Aurora Glass themes

The Themes pane offered four of the webmail's built-in themes. The
webmail also ships Roundcube Elastic and Aurora Glass, so their colour
tokens are now mapped onto the native palette the same way as the
other four, for both light and dark.

Aurora Glass dark uses translucent surfaces that the webmail frosts
with a backdrop blur. RN has no blur, so those fill tokens are
composited onto the theme background instead of letting the content
underneath show through. The skins (fonts, radii, Elastic's dark task
rail, Aurora's gradient mesh) and the "Flat fields" theme are not
ported.
This commit is contained in:
Linus Rath
2026-09-24 19:40:08 +02:00
parent 008bff29f8
commit 7964e646c4
2 changed files with 299 additions and 0 deletions
@@ -0,0 +1,75 @@
import { describe, it, expect } from 'vitest';
import { BUILTIN_THEMES, getBuiltinTheme } from '../builtin-themes';
import { resolvePalette } from '../colors';
import { DARK_COLORS, LIGHT_COLORS } from '../tokens';
// Solid fills: RN paints screens, sheets and cards with these and has no
// backdrop blur, so a translucent value would show the content underneath.
const SOLID_FILLS = [
'background', 'surface', 'surfaceHover', 'surfaceActive', 'card',
'secondary', 'muted', 'accent', 'primaryBg', 'primaryBgHover', 'popover',
] as const;
describe('built-in themes', () => {
it('ships every webmail built-in theme except Flat fields, in webmail order', () => {
expect(BUILTIN_THEMES.map((t) => t.id)).toEqual([
'builtin-qui',
'builtin-nord',
'builtin-catppuccin',
'builtin-solarized',
'builtin-roundcube-elastic',
'builtin-aurora-glass',
]);
});
it('defines the same tokens for light and dark in every theme', () => {
for (const theme of BUILTIN_THEMES) {
expect(Object.keys(theme.light).sort(), theme.id).toEqual(Object.keys(theme.dark).sort());
}
});
it('keeps solid-fill tokens opaque', () => {
for (const theme of BUILTIN_THEMES) {
for (const scheme of ['light', 'dark'] as const) {
for (const key of SOLID_FILLS) {
expect(theme[scheme][key], `${theme.id} ${scheme} ${key}`).toMatch(/^#[0-9a-f]{6}$/i);
}
}
}
});
it('applies the Roundcube Elastic tokens in light and dark', () => {
const light = resolvePalette('light', 'builtin-roundcube-elastic');
expect(light.primary).toBe('#37beff');
expect(light.text).toBe('#27353a');
expect(light.background).toBe('#ffffff');
const dark = resolvePalette('dark', 'builtin-roundcube-elastic');
expect(dark.background).toBe('#21292c');
expect(dark.surface).toBe('#2c373a');
expect(dark.unread).toBe('#b88a00');
// Tokens the webmail theme doesn't define keep the base palette.
expect(dark.starred).toBe(DARK_COLORS.starred);
expect(dark.calendar).toBe(DARK_COLORS.calendar);
});
it('flattens the Aurora Glass dark glass tokens onto its background', () => {
const dark = resolvePalette('dark', 'builtin-aurora-glass');
expect(dark.primary).toBe('#8b7bff');
expect(dark.background).toBe('#06070f');
// rgba(255, 255, 255, 0.06) over #06070f
expect(dark.surface).toBe('#15161d');
// rgba(139, 123, 255, 0.16) over #06070f
expect(dark.accent).toBe('#1b1a35');
// Overlays may stay translucent, as in the other themes.
expect(dark.selection).toBe('rgba(139, 123, 255, 0.22)');
const light = resolvePalette('light', 'builtin-aurora-glass');
expect(light.background).toBe('#eef1fb');
expect(light.tags).toBe(LIGHT_COLORS.tags);
});
it('falls back to the base palette for an unknown or cleared theme id', () => {
expect(getBuiltinTheme('builtin-flat-fields')).toBeNull();
expect(resolvePalette('dark', 'builtin-flat-fields')).toBe(DARK_COLORS);
expect(resolvePalette('light', null)).toBe(LIGHT_COLORS);
});
});
+224
View File
@@ -2,6 +2,20 @@
// scripts in the parity work (2026-08-29). Each theme overrides the subset of
// ThemePalette that the CSS defines; everything else keeps the base palette.
// Regenerate rather than hand-edit when the webmail themes change.
//
// Mapping (webmail --color-* -> ThemePalette): primary -> primary,
// primaryDark, textLink, navActive; ring -> primaryLight, borderFocus;
// accent -> primaryBg, primaryBgHover, surfaceHover; secondary -> surface;
// muted -> surfaceActive; foreground -> text; background -> textInverse;
// muted-foreground -> textSecondary, textMuted, read, navInactive;
// destructive -> error, navBadge; border -> borderLight; the rest by name.
// Translucent values of the solid-fill tokens (surfaces, cards, accent,
// popover) are composited onto the theme background, because RN has no
// backdrop blur and would show whatever lies underneath (Aurora Glass dark).
//
// Only the colour tokens are ported. The webmail's theme skins (fonts,
// radii, Elastic's dark task rail, Aurora's gradient mesh and frosted panes)
// and the "Flat fields" design-system theme have no native counterpart.
import type { ThemePalette } from './tokens';
export type PaletteOverride = Partial<Omit<ThemePalette, 'calendar' | 'tags'>>;
@@ -435,6 +449,216 @@ export const BUILTIN_THEMES: BuiltinTheme[] = [
chart5: '#6c71c4',
},
},
{
id: 'builtin-roundcube-elastic',
name: 'Roundcube Elastic',
description: "Colours of Roundcube's Elastic skin: the #37beff blue on white or dark slate",
light: {
primary: '#37beff',
primaryDark: '#37beff',
primaryLight: '#37beff',
primaryBg: '#e6f6ff',
primaryBgHover: '#e6f6ff',
primaryForeground: '#ffffff',
background: '#ffffff',
surface: '#f4f4f4',
surfaceHover: '#e6f6ff',
surfaceActive: '#f4f4f4',
card: '#ffffff',
cardForeground: '#27353a',
text: '#27353a',
textSecondary: '#737677',
textMuted: '#737677',
textInverse: '#ffffff',
textLink: '#37beff',
border: '#dddddd',
borderLight: '#dddddd',
borderFocus: '#37beff',
success: '#41b849',
successForeground: '#ffffff',
warning: '#ffd452',
warningForeground: '#27353a',
error: '#ff5552',
errorForeground: '#ffffff',
info: '#37beff',
infoForeground: '#ffffff',
unread: '#ffd452',
read: '#737677',
navActive: '#37beff',
navInactive: '#737677',
navBadge: '#ff5552',
secondary: '#f4f4f4',
secondaryForeground: '#27353a',
muted: '#f4f4f4',
mutedForeground: '#737677',
accent: '#e6f6ff',
accentForeground: '#0070a8',
selection: '#ebf8ff',
selectionForeground: '#27353a',
popover: '#ffffff',
popoverForeground: '#27353a',
chart1: '#37beff',
chart2: '#41b849',
chart3: '#ffd452',
chart4: '#ff5552',
chart5: '#9b59b6',
},
dark: {
primary: '#37beff',
primaryDark: '#37beff',
primaryLight: '#37beff',
primaryBg: '#374549',
primaryBgHover: '#374549',
primaryForeground: '#ffffff',
background: '#21292c',
surface: '#2c373a',
surfaceHover: '#374549',
surfaceActive: '#2c373a',
card: '#1a2225',
cardForeground: '#ffffff',
text: '#ffffff',
textSecondary: '#b3bec1',
textMuted: '#b3bec1',
textInverse: '#21292c',
textLink: '#37beff',
border: '#4d6066',
borderLight: '#4d6066',
borderFocus: '#37beff',
success: '#41b849',
successForeground: '#ffffff',
warning: '#ffd452',
warningForeground: '#21292c',
error: '#ff5552',
errorForeground: '#ffffff',
info: '#37beff',
infoForeground: '#ffffff',
unread: '#b88a00',
read: '#b3bec1',
navActive: '#37beff',
navInactive: '#b3bec1',
navBadge: '#ff5552',
secondary: '#2c373a',
secondaryForeground: '#ffffff',
muted: '#2c373a',
mutedForeground: '#b3bec1',
accent: '#374549',
accentForeground: '#37beff',
selection: '#374549',
selectionForeground: '#37beff',
popover: '#161b1d',
popoverForeground: '#ffffff',
chart1: '#37beff',
chart2: '#41b849',
chart3: '#ffd452',
chart4: '#ff5552',
chart5: '#b48ead',
},
},
{
id: 'builtin-aurora-glass',
name: 'Aurora Glass',
description: 'Vibrant indigo and cyan accents on a cool lavender or near-black canvas',
light: {
primary: '#6d5cff',
primaryDark: '#6d5cff',
primaryLight: '#6d5cff',
primaryBg: '#e0e7ff',
primaryBgHover: '#e0e7ff',
primaryForeground: '#ffffff',
background: '#eef1fb',
surface: '#e3e7f7',
surfaceHover: '#e0e7ff',
surfaceActive: '#e7eaf6',
card: '#ffffff',
cardForeground: '#14162e',
text: '#14162e',
textSecondary: '#5b6080',
textMuted: '#5b6080',
textInverse: '#eef1fb',
textLink: '#6d5cff',
border: 'rgba(13, 18, 45, 0.10)',
borderLight: 'rgba(13, 18, 45, 0.10)',
borderFocus: '#6d5cff',
success: '#10b981',
successForeground: '#ffffff',
warning: '#f59e0b',
warningForeground: '#14162e',
error: '#f43f5e',
errorForeground: '#ffffff',
info: '#06b6d4',
infoForeground: '#ffffff',
unread: '#6d5cff',
read: '#5b6080',
navActive: '#6d5cff',
navInactive: '#5b6080',
navBadge: '#f43f5e',
secondary: '#e3e7f7',
secondaryForeground: '#14162e',
muted: '#e7eaf6',
mutedForeground: '#5b6080',
accent: '#e0e7ff',
accentForeground: '#4338ca',
selection: 'rgba(109, 92, 255, 0.14)',
selectionForeground: '#4338ca',
popover: '#ffffff',
popoverForeground: '#14162e',
chart1: '#6d5cff',
chart2: '#06b6d4',
chart3: '#ec4899',
chart4: '#f59e0b',
chart5: '#10b981',
},
dark: {
primary: '#8b7bff',
primaryDark: '#8b7bff',
primaryLight: '#8b7bff',
primaryBg: '#1b1a35',
primaryBgHover: '#1b1a35',
primaryForeground: '#ffffff',
background: '#06070f',
surface: '#15161d',
surfaceHover: '#1b1a35',
surfaceActive: '#12131b',
card: '#101119',
cardForeground: '#eef0ff',
text: '#eef0ff',
textSecondary: '#9aa0c4',
textMuted: '#9aa0c4',
textInverse: '#06070f',
textLink: '#8b7bff',
border: 'rgba(255, 255, 255, 0.08)',
borderLight: 'rgba(255, 255, 255, 0.08)',
borderFocus: '#8b7bff',
success: '#34d399',
successForeground: '#052e1f',
warning: '#fbbf24',
warningForeground: '#1a1405',
error: '#fb7185',
errorForeground: '#1a0b10',
info: '#22d3ee',
infoForeground: '#04222a',
unread: '#8b7bff',
read: '#9aa0c4',
navActive: '#8b7bff',
navInactive: '#9aa0c4',
navBadge: '#fb7185',
secondary: '#15161d',
secondaryForeground: '#eef0ff',
muted: '#12131b',
mutedForeground: '#9aa0c4',
accent: '#1b1a35',
accentForeground: '#c4bbff',
selection: 'rgba(139, 123, 255, 0.22)',
selectionForeground: '#c4bbff',
popover: '#12132a',
popoverForeground: '#eef0ff',
chart1: '#8b7bff',
chart2: '#22d3ee',
chart3: '#f472b6',
chart4: '#fbbf24',
chart5: '#34d399',
},
},
];
export function getBuiltinTheme(id: string | null | undefined): BuiltinTheme | null {