feat: add Aurora Glass built-in theme
This commit is contained in:
@@ -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', () => {
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user