mirror of
https://github.com/bulwarkmail/native.git
synced 2026-10-31 16:06:32 +00:00
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:
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user