feat: add Aurora Glass built-in theme

This commit is contained in:
Linus Rath
2026-06-03 19:30:51 +02:00
parent 9953557af0
commit 9863b9d88e
3 changed files with 271 additions and 4 deletions
+3 -2
View File
@@ -2,8 +2,8 @@ import { describe, it, expect } from 'vitest';
import { BUILTIN_THEMES } from '../builtin-themes';
describe('BUILTIN_THEMES', () => {
it('contains exactly 5 themes', () => {
expect(BUILTIN_THEMES).toHaveLength(5);
it('contains exactly 6 themes', () => {
expect(BUILTIN_THEMES).toHaveLength(6);
});
it('all themes have required fields', () => {
@@ -44,6 +44,7 @@ describe('BUILTIN_THEMES', () => {
expect(names).toContain('Catppuccin');
expect(names).toContain('Solarized');
expect(names).toContain('Roundcube Elastic');
expect(names).toContain('Aurora Glass');
});
it('theme IDs are unique', () => {
+252
View File
@@ -635,6 +635,245 @@ body[data-theme-skin="builtin-roundcube-elastic"] .bg-secondary.border-r .border
border-left-width: 3px !important;
}`;
// "Aurora Glass" - an ultra-modern glassmorphism skin. Vibrant indigo→cyan
// accents float over a fixed gradient-mesh canvas; every structural surface
// (nav rail, folder sidebar, cards, popovers, menus, dialogs, toolbars) is a
// translucent frosted pane with heavy backdrop-blur + saturation, hairline
// luminous borders and soft glow on the primary action.
//
// The colour tokens stay near-solid for legible text contrast; the frosted
// translucency + blur all live in the skin, where a single selector can pair
// an rgba background with the matching backdrop-filter.
const auroraCSS = `
:root {
--color-border: rgba(13, 18, 45, 0.10);
--color-input: rgba(13, 18, 45, 0.12);
--color-ring: #6d5cff;
--color-background: #eef1fb;
--color-foreground: #14162e;
--color-primary: #6d5cff;
--color-primary-foreground: #ffffff;
--color-secondary: #e3e7f7;
--color-secondary-foreground: #14162e;
--color-muted: #e7eaf6;
--color-muted-foreground: #5b6080;
--color-accent: #e0e7ff;
--color-accent-foreground: #4338ca;
--color-destructive: #f43f5e;
--color-destructive-foreground: #ffffff;
--color-popover: #ffffff;
--color-popover-foreground: #14162e;
--color-sidebar: #eef1fb;
--color-sidebar-foreground: #14162e;
--color-sidebar-border: rgba(13, 18, 45, 0.08);
--color-sidebar-accent: rgba(109, 92, 255, 0.10);
--color-sidebar-accent-foreground: #4338ca;
--color-card: #ffffff;
--color-card-foreground: #14162e;
--color-success: #10b981;
--color-success-foreground: #ffffff;
--color-warning: #f59e0b;
--color-warning-foreground: #14162e;
--color-info: #06b6d4;
--color-info-foreground: #ffffff;
--color-selection: rgba(109, 92, 255, 0.14);
--color-selection-foreground: #4338ca;
--color-unread: #6d5cff;
--color-chart-1: #6d5cff;
--color-chart-2: #06b6d4;
--color-chart-3: #ec4899;
--color-chart-4: #f59e0b;
--color-chart-5: #10b981;
}
.dark {
--color-border: rgba(255, 255, 255, 0.08);
--color-input: rgba(255, 255, 255, 0.10);
--color-ring: #8b7bff;
--color-background: #06070f;
--color-foreground: #eef0ff;
--color-primary: #8b7bff;
--color-primary-foreground: #ffffff;
--color-secondary: rgba(255, 255, 255, 0.06);
--color-secondary-foreground: #eef0ff;
--color-muted: rgba(255, 255, 255, 0.05);
--color-muted-foreground: #9aa0c4;
--color-accent: rgba(139, 123, 255, 0.16);
--color-accent-foreground: #c4bbff;
--color-destructive: #fb7185;
--color-destructive-foreground: #1a0b10;
--color-popover: #12132a;
--color-popover-foreground: #eef0ff;
--color-sidebar: #08091a;
--color-sidebar-foreground: #eef0ff;
--color-sidebar-border: rgba(255, 255, 255, 0.07);
--color-sidebar-accent: rgba(139, 123, 255, 0.18);
--color-sidebar-accent-foreground: #c4bbff;
--color-card: rgba(255, 255, 255, 0.04);
--color-card-foreground: #eef0ff;
--color-success: #34d399;
--color-success-foreground: #052e1f;
--color-warning: #fbbf24;
--color-warning-foreground: #1a1405;
--color-info: #22d3ee;
--color-info-foreground: #04222a;
--color-selection: rgba(139, 123, 255, 0.22);
--color-selection-foreground: #c4bbff;
--color-unread: #8b7bff;
--color-chart-1: #8b7bff;
--color-chart-2: #22d3ee;
--color-chart-3: #f472b6;
--color-chart-4: #fbbf24;
--color-chart-5: #34d399;
}`;
// Glassmorphism skin: the frosted translucency, backdrop-blur, gradient-mesh
// canvas, luminous borders and glow that the colour tokens alone can't carry.
// Scoped under the skin body attribute so it detaches cleanly on switch-off.
const auroraSkin = `
body[data-theme-skin="builtin-aurora-glass"] {
font-family: "Inter", "SF Pro Display", "Segoe UI", system-ui, -apple-system, sans-serif;
letter-spacing: -0.01em;
}
/* ── Gradient-mesh canvas ──────────────────────────────────────── */
/* A multi-stop radial mesh painted onto the app-shell root (the unique
.bg-background.overflow-hidden container) so the frosted panes have
something luminous to blur. Painting it directly on the shell - rather than
a fixed body::before with z-indexed children - avoids creating stacking
contexts on portaled popups/dialogs (Radix portals render as direct <body>
children), which would otherwise break their layering. */
body[data-theme-skin="builtin-aurora-glass"] .bg-background.overflow-hidden {
background-color: var(--color-background);
background-image:
radial-gradient(60rem 60rem at 12% -10%, rgba(109, 92, 255, 0.22), transparent 60%),
radial-gradient(50rem 50rem at 105% 5%, rgba(6, 182, 212, 0.18), transparent 55%),
radial-gradient(55rem 55rem at 80% 110%, rgba(236, 72, 153, 0.16), transparent 60%),
radial-gradient(45rem 45rem at -5% 95%, rgba(16, 185, 129, 0.12), transparent 55%);
}
.dark body[data-theme-skin="builtin-aurora-glass"] .bg-background.overflow-hidden {
background-image:
radial-gradient(60rem 60rem at 12% -10%, rgba(109, 92, 255, 0.16), transparent 58%),
radial-gradient(50rem 50rem at 105% 5%, rgba(6, 182, 212, 0.11), transparent 52%),
radial-gradient(55rem 55rem at 80% 110%, rgba(236, 72, 153, 0.10), transparent 58%),
radial-gradient(45rem 45rem at -5% 95%, rgba(16, 185, 129, 0.07), transparent 52%);
}
/* ── Pill-soft radii everywhere ───────────────────────────────── */
body[data-theme-skin="builtin-aurora-glass"] .rounded-md,
body[data-theme-skin="builtin-aurora-glass"] .rounded-lg {
border-radius: 0.85rem !important;
}
body[data-theme-skin="builtin-aurora-glass"] .rounded-xl,
body[data-theme-skin="builtin-aurora-glass"] .rounded-2xl {
border-radius: 1.25rem !important;
}
/* ── Frosted panes: the shared glass recipe ───────────────────── */
/* Nav rail, folder sidebar, content backing, cards, popovers, dialogs and
menus all share one frosted treatment - translucent fill + heavy backdrop
blur + saturation so the mesh and neighbours bleed through softly. */
body[data-theme-skin="builtin-aurora-glass"] .w-14.bg-secondary,
body[data-theme-skin="builtin-aurora-glass"] .bg-secondary.border-r {
background-color: rgba(255, 255, 255, 0.55) !important;
backdrop-filter: blur(26px) saturate(180%);
-webkit-backdrop-filter: blur(26px) saturate(180%);
border-color: rgba(13, 18, 45, 0.08) !important;
}
.dark body[data-theme-skin="builtin-aurora-glass"] .w-14.bg-secondary,
.dark body[data-theme-skin="builtin-aurora-glass"] .bg-secondary.border-r {
background-color: rgba(8, 9, 22, 0.82) !important;
border-color: rgba(255, 255, 255, 0.07) !important;
}
/* The "no conversation selected" empty pane sits inside an opaque
bg-background column, so it can't inherit the shell mesh - paint the same
gradient mesh directly onto it so the placeholder reads as glass too. */
body[data-theme-skin="builtin-aurora-glass"] .bg-gradient-to-br.from-muted\\/30.to-muted\\/50 {
background-color: var(--color-background) !important;
background-image:
radial-gradient(50rem 50rem at 15% 0%, rgba(109, 92, 255, 0.22), transparent 60%),
radial-gradient(45rem 45rem at 100% 10%, rgba(6, 182, 212, 0.18), transparent 55%),
radial-gradient(50rem 50rem at 85% 105%, rgba(236, 72, 153, 0.16), transparent 60%) !important;
}
.dark body[data-theme-skin="builtin-aurora-glass"] .bg-gradient-to-br.from-muted\\/30.to-muted\\/50 {
background-image:
radial-gradient(50rem 50rem at 15% 0%, rgba(109, 92, 255, 0.18), transparent 58%),
radial-gradient(45rem 45rem at 100% 10%, rgba(6, 182, 212, 0.12), transparent 52%),
radial-gradient(50rem 50rem at 85% 105%, rgba(236, 72, 153, 0.11), transparent 58%) !important;
}
/* The reading-pane message backing goes clear glass so the frosted panes and
gradient bleed through softly. */
body[data-theme-skin="builtin-aurora-glass"] .bg-muted\\/30 {
background: transparent !important;
}
/* Cards, popovers and dialogs: brighter frosted glass with a luminous edge. */
body[data-theme-skin="builtin-aurora-glass"] .bg-card,
body[data-theme-skin="builtin-aurora-glass"] .bg-popover,
body[data-theme-skin="builtin-aurora-glass"] [role="menu"],
body[data-theme-skin="builtin-aurora-glass"] [role="dialog"],
body[data-theme-skin="builtin-aurora-glass"] [role="listbox"] {
background-color: rgba(255, 255, 255, 0.72) !important;
backdrop-filter: blur(28px) saturate(190%);
-webkit-backdrop-filter: blur(28px) saturate(190%);
border: 1px solid rgba(255, 255, 255, 0.6) !important;
box-shadow: 0 12px 40px -12px rgba(20, 22, 46, 0.28), inset 0 1px 0 rgba(255, 255, 255, 0.7) !important;
}
.dark body[data-theme-skin="builtin-aurora-glass"] .bg-card,
.dark body[data-theme-skin="builtin-aurora-glass"] .bg-popover,
.dark body[data-theme-skin="builtin-aurora-glass"] [role="menu"],
.dark body[data-theme-skin="builtin-aurora-glass"] [role="dialog"],
.dark body[data-theme-skin="builtin-aurora-glass"] [role="listbox"] {
background-color: rgba(16, 17, 38, 0.86) !important;
border: 1px solid rgba(255, 255, 255, 0.10) !important;
box-shadow: 0 16px 48px -12px rgba(0, 0, 0, 0.6), inset 0 1px 0 rgba(255, 255, 255, 0.08) !important;
}
/* ── Primary action: gradient fill + glow ─────────────────────── */
body[data-theme-skin="builtin-aurora-glass"] .bg-primary.text-primary-foreground {
background-image: linear-gradient(135deg, #7c5cff 0%, #6d5cff 45%, #4f9bff 100%) !important;
border: none !important;
color: #ffffff !important;
box-shadow: 0 6px 20px -4px rgba(109, 92, 255, 0.55), inset 0 1px 0 rgba(255, 255, 255, 0.35) !important;
transition: box-shadow 160ms ease, filter 160ms ease;
}
body[data-theme-skin="builtin-aurora-glass"] .bg-primary.text-primary-foreground:hover {
filter: brightness(1.08);
box-shadow: 0 10px 30px -4px rgba(109, 92, 255, 0.7), inset 0 1px 0 rgba(255, 255, 255, 0.4) !important;
}
.dark body[data-theme-skin="builtin-aurora-glass"] .bg-primary.text-primary-foreground {
background-image: linear-gradient(135deg, #8b7bff 0%, #6d5cff 50%, #22d3ee 130%) !important;
box-shadow: 0 6px 24px -4px rgba(139, 123, 255, 0.6), inset 0 1px 0 rgba(255, 255, 255, 0.2) !important;
}
/* ── Selected folder / row: soft tinted glass + accent bar ────── */
body[data-theme-skin="builtin-aurora-glass"] .bg-secondary.border-r .border-l-2.border-primary {
border-left-width: 3px !important;
}
/* ── On/off switch: gradient when active ──────────────────────── */
body[data-theme-skin="builtin-aurora-glass"] [role="switch"][aria-checked="true"] {
background-image: linear-gradient(135deg, #7c5cff, #4f9bff) !important;
border-color: transparent !important;
box-shadow: 0 2px 10px -1px rgba(109, 92, 255, 0.6) !important;
}
.dark body[data-theme-skin="builtin-aurora-glass"] [role="switch"][aria-checked="true"] {
background-image: linear-gradient(135deg, #8b7bff, #22d3ee) !important;
}
/* ── Menu hover: tinted accent slab (keeps glass legible) ─────── */
body[data-theme-skin="builtin-aurora-glass"] [role="menu"] [role="menuitem"]:hover,
body[data-theme-skin="builtin-aurora-glass"] [role="menu"] [role="menuitem"]:focus,
body[data-theme-skin="builtin-aurora-glass"] [role="menu"] [role="menuitem"]:focus-visible {
background-color: rgba(109, 92, 255, 0.14) !important;
color: var(--color-foreground) !important;
}
.dark body[data-theme-skin="builtin-aurora-glass"] [role="menu"] [role="menuitem"]:hover,
.dark body[data-theme-skin="builtin-aurora-glass"] [role="menu"] [role="menuitem"]:focus,
.dark body[data-theme-skin="builtin-aurora-glass"] [role="menu"] [role="menuitem"]:focus-visible {
background-color: rgba(139, 123, 255, 0.22) !important;
}`;
export const BUILTIN_THEMES: InstalledTheme[] = [
{
id: 'builtin-qui',
@@ -693,4 +932,17 @@ export const BUILTIN_THEMES: InstalledTheme[] = [
enabled: true,
builtIn: true,
},
{
id: 'builtin-aurora-glass',
name: 'Aurora Glass',
version: '1.0.0',
author: 'Built-in',
description: 'Ultra-modern glassmorphism - frosted blurred panes floating over a vibrant indigo→cyan gradient mesh, with glowing gradient actions',
css: auroraCSS,
skin: auroraSkin,
variants: ['light', 'dark'],
typography: { fontSans: '"Inter", "SF Pro Display", "Segoe UI", system-ui, sans-serif' },
enabled: true,
builtIn: true,
},
];
+16 -2
View File
@@ -451,11 +451,25 @@ export const useThemeStore = create<ThemeState>()(
onRehydrateStorage: () => {
return (state) => {
if (state) {
// Ensure built-in themes are always present after rehydration
// Ensure built-in themes are always present after rehydration.
// Drop persisted themes that were flagged builtIn but no longer
// exist in BUILTIN_THEMES (e.g. a built-in removed from source) so
// stale built-ins don't survive as phantom "user" themes.
const builtInIds = new Set(BUILTIN_THEMES.map(t => t.id));
const userThemes = state.installedThemes.filter(t => !builtInIds.has(t.id));
const userThemes = state.installedThemes.filter(
t => !builtInIds.has(t.id) && !t.builtIn
);
state.installedThemes = [...BUILTIN_THEMES, ...userThemes];
// If the active theme pointed at a now-removed built-in, fall back
// to the default so the UI doesn't reference a missing theme.
if (
state.activeThemeId &&
!state.installedThemes.some(t => t.id === state.activeThemeId)
) {
state.activeThemeId = null;
}
// Re-apply theme immediately after rehydration
const resolvedTheme = state.theme === 'system' ? getSystemTheme() : state.theme;
applyTheme(resolvedTheme);