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';
|
import { BUILTIN_THEMES } from '../builtin-themes';
|
||||||
|
|
||||||
describe('BUILTIN_THEMES', () => {
|
describe('BUILTIN_THEMES', () => {
|
||||||
it('contains exactly 5 themes', () => {
|
it('contains exactly 6 themes', () => {
|
||||||
expect(BUILTIN_THEMES).toHaveLength(5);
|
expect(BUILTIN_THEMES).toHaveLength(6);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('all themes have required fields', () => {
|
it('all themes have required fields', () => {
|
||||||
@@ -44,6 +44,7 @@ describe('BUILTIN_THEMES', () => {
|
|||||||
expect(names).toContain('Catppuccin');
|
expect(names).toContain('Catppuccin');
|
||||||
expect(names).toContain('Solarized');
|
expect(names).toContain('Solarized');
|
||||||
expect(names).toContain('Roundcube Elastic');
|
expect(names).toContain('Roundcube Elastic');
|
||||||
|
expect(names).toContain('Aurora Glass');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('theme IDs are unique', () => {
|
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;
|
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[] = [
|
export const BUILTIN_THEMES: InstalledTheme[] = [
|
||||||
{
|
{
|
||||||
id: 'builtin-qui',
|
id: 'builtin-qui',
|
||||||
@@ -693,4 +932,17 @@ export const BUILTIN_THEMES: InstalledTheme[] = [
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
builtIn: 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: () => {
|
onRehydrateStorage: () => {
|
||||||
return (state) => {
|
return (state) => {
|
||||||
if (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 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];
|
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
|
// Re-apply theme immediately after rehydration
|
||||||
const resolvedTheme = state.theme === 'system' ? getSystemTheme() : state.theme;
|
const resolvedTheme = state.theme === 'system' ? getSystemTheme() : state.theme;
|
||||||
applyTheme(resolvedTheme);
|
applyTheme(resolvedTheme);
|
||||||
|
|||||||
Reference in New Issue
Block a user