From 9b4ffbb75425cd8d5cf656a82dff377134ddbb3d Mon Sep 17 00:00:00 2001 From: xpltd Date: Sun, 22 Mar 2026 00:51:00 -0500 Subject: [PATCH] 6 new themes + grouped theme picker dropdown Dark themes: - Midnight: ultra-minimal, near-black, zero effects - Hacker: green-on-black terminal, monospace, CRT scanlines - Neon: hot pink + cyan on purple-black, synthwave, heavy glow Light themes: - Paper: warm cream/sepia, serif fonts, book-like - Arctic: cool whites and icy blues, crisp and modern - Solarized: Ethan Schoonover's solarized-light palette Theme picker: - Replaced simple dark/light toggle with grouped dropdown - Themes organized by Dark / Light sections with active checkmark - Remembers last dark and light theme separately for quick toggle - Theme metadata now includes variant field for proper grouping - Custom themes default to dark variant --- frontend/src/components/DarkModeToggle.vue | 170 ++++++++++++++++++--- frontend/src/main.ts | 6 + frontend/src/stores/theme.ts | 39 ++++- frontend/src/tests/stores/theme.test.ts | 9 +- frontend/src/themes/arctic.css | 35 +++++ frontend/src/themes/hacker.css | 43 ++++++ frontend/src/themes/midnight.css | 40 +++++ frontend/src/themes/neon.css | 42 +++++ frontend/src/themes/paper.css | 40 +++++ frontend/src/themes/solarized.css | 36 +++++ 10 files changed, 424 insertions(+), 36 deletions(-) create mode 100644 frontend/src/themes/arctic.css create mode 100644 frontend/src/themes/hacker.css create mode 100644 frontend/src/themes/midnight.css create mode 100644 frontend/src/themes/neon.css create mode 100644 frontend/src/themes/paper.css create mode 100644 frontend/src/themes/solarized.css diff --git a/frontend/src/components/DarkModeToggle.vue b/frontend/src/components/DarkModeToggle.vue index aaae9c9..81122d4 100644 --- a/frontend/src/components/DarkModeToggle.vue +++ b/frontend/src/components/DarkModeToggle.vue @@ -1,37 +1,88 @@ diff --git a/frontend/src/main.ts b/frontend/src/main.ts index 548d803..3a397b5 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -8,6 +8,12 @@ import './assets/base.css' import './themes/cyberpunk.css' import './themes/dark.css' import './themes/light.css' +import './themes/midnight.css' +import './themes/hacker.css' +import './themes/neon.css' +import './themes/paper.css' +import './themes/arctic.css' +import './themes/solarized.css' import App from './App.vue' diff --git a/frontend/src/stores/theme.ts b/frontend/src/stores/theme.ts index 7a4fe6b..be5d454 100644 --- a/frontend/src/stores/theme.ts +++ b/frontend/src/stores/theme.ts @@ -17,15 +17,24 @@ export interface ThemeMeta { author?: string description?: string builtin: boolean + variant: 'dark' | 'light' } const STORAGE_KEY = 'mrip-theme' const DEFAULT_THEME = 'cyberpunk' const BUILTIN_THEMES: ThemeMeta[] = [ - { id: 'cyberpunk', name: 'Cyberpunk', author: 'media.rip()', description: 'Electric blue + orange, scanlines, grid overlay', builtin: true }, - { id: 'dark', name: 'Dark', author: 'media.rip()', description: 'Clean neutral dark theme', builtin: true }, - { id: 'light', name: 'Light', author: 'media.rip()', description: 'Clean light theme for daylight use', builtin: true }, + // Dark themes + { id: 'cyberpunk', name: 'Cyberpunk', author: 'media.rip()', description: 'Electric blue + orange, scanlines, grid overlay', builtin: true, variant: 'dark' }, + { id: 'dark', name: 'Dark', author: 'media.rip()', description: 'Clean neutral dark theme', builtin: true, variant: 'dark' }, + { id: 'midnight', name: 'Midnight', author: 'media.rip()', description: 'Ultra-minimal, near-black, zero effects', builtin: true, variant: 'dark' }, + { id: 'hacker', name: 'Hacker', author: 'media.rip()', description: 'Green-on-black terminal aesthetic', builtin: true, variant: 'dark' }, + { id: 'neon', name: 'Neon', author: 'media.rip()', description: 'Hot pink + cyan on deep purple, synthwave vibes', builtin: true, variant: 'dark' }, + // Light themes + { id: 'light', name: 'Light', author: 'media.rip()', description: 'Clean light theme for daylight use', builtin: true, variant: 'light' }, + { id: 'paper', name: 'Paper', author: 'media.rip()', description: 'Warm cream and sepia, book-like', builtin: true, variant: 'light' }, + { id: 'arctic', name: 'Arctic', author: 'media.rip()', description: 'Cool whites and icy blues, crisp and sharp', builtin: true, variant: 'light' }, + { id: 'solarized', name: 'Solarized', author: 'media.rip()', description: 'Solarized Light — easy on the eyes', builtin: true, variant: 'light' }, ] export const useThemeStore = defineStore('theme', () => { @@ -33,8 +42,14 @@ export const useThemeStore = defineStore('theme', () => { const customThemes = ref([]) const customThemeCSS = ref>(new Map()) - /** Whether the current theme is a dark variant (cyberpunk and dark are dark; light is light). */ - const isDark = computed(() => currentTheme.value !== 'light') + /** Whether the current theme is a dark variant. */ + const isDark = computed(() => { + const meta = allThemes.value.find(t => t.id === currentTheme.value) + return meta ? meta.variant === 'dark' : true + }) + + const darkThemes = computed(() => allThemes.value.filter(t => t.variant === 'dark')) + const lightThemes = computed(() => allThemes.value.filter(t => t.variant === 'light')) const allThemes = computed(() => [ ...BUILTIN_THEMES, @@ -64,11 +79,13 @@ export const useThemeStore = defineStore('theme', () => { */ function toggleDarkMode(): void { if (isDark.value) { - setTheme('light') + // Switch to last used light theme, or first available + const lastLight = localStorage.getItem(STORAGE_KEY + '-light') || 'light' + setTheme(lastLight) } else { // Return to the last dark theme, defaulting to cyberpunk const lastDark = localStorage.getItem(STORAGE_KEY + '-dark') || DEFAULT_THEME - setTheme(lastDark === 'light' ? DEFAULT_THEME : lastDark) + setTheme(lastDark) } } @@ -82,8 +99,11 @@ export const useThemeStore = defineStore('theme', () => { currentTheme.value = themeId localStorage.setItem(STORAGE_KEY, themeId) // Remember the last dark theme for toggle - if (themeId !== 'light') { + const meta = allThemes.value.find(t => t.id === themeId) + if (meta?.variant === 'dark') { localStorage.setItem(STORAGE_KEY + '-dark', themeId) + } else { + localStorage.setItem(STORAGE_KEY + '-light', themeId) } _apply(themeId) } @@ -104,6 +124,7 @@ export const useThemeStore = defineStore('theme', () => { author: t.author, description: t.description, builtin: false, + variant: t.variant || 'dark', // default custom themes to dark })) // If saved theme is a custom theme, validate it still exists @@ -159,6 +180,8 @@ export const useThemeStore = defineStore('theme', () => { currentTheme, customThemes, allThemes, + darkThemes, + lightThemes, currentMeta, isDark, init, diff --git a/frontend/src/tests/stores/theme.test.ts b/frontend/src/tests/stores/theme.test.ts index a7b6333..5f7e5eb 100644 --- a/frontend/src/tests/stores/theme.test.ts +++ b/frontend/src/tests/stores/theme.test.ts @@ -73,10 +73,13 @@ describe('theme store', () => { expect(store.currentTheme).toBe('cyberpunk') }) - it('lists 3 built-in themes', () => { + it('lists 9 built-in themes', () => { const store = useThemeStore() - expect(store.allThemes).toHaveLength(3) - expect(store.allThemes.map(t => t.id)).toEqual(['cyberpunk', 'dark', 'light']) + expect(store.allThemes).toHaveLength(9) + expect(store.allThemes.map(t => t.id)).toEqual([ + 'cyberpunk', 'dark', 'midnight', 'hacker', 'neon', + 'light', 'paper', 'arctic', 'solarized', + ]) }) it('all built-in themes are marked builtin: true', () => { diff --git a/frontend/src/themes/arctic.css b/frontend/src/themes/arctic.css new file mode 100644 index 0000000..4683d55 --- /dev/null +++ b/frontend/src/themes/arctic.css @@ -0,0 +1,35 @@ +/* media.rip() — Arctic Theme + * + * Cool whites, icy blues, crisp edges. + * Modern, clean, and sharp — like fresh snow. + */ + +:root[data-theme="arctic"] { + --color-bg: #f0f4f8; + --color-surface: #ffffff; + --color-surface-hover: #e8eef4; + --color-border: #c8d6e5; + + --color-text: #1a2a3a; + --color-text-muted: #5a7a94; + + --color-accent: #0088cc; + --color-accent-hover: #006da6; + --color-accent-secondary: #0066aa; + + --color-success: #00a878; + --color-warning: #e8a317; + --color-error: #d63031; + + --font-display: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; + + --effect-scanlines: none; + --effect-grid: none; + --effect-grid-size: 0px 0px; + --effect-glow: none; + + --shadow-sm: 0 1px 3px rgba(26, 42, 58, 0.06); + --shadow-md: 0 4px 12px rgba(26, 42, 58, 0.08); + --shadow-lg: 0 8px 24px rgba(26, 42, 58, 0.12); + --shadow-glow: none; +} diff --git a/frontend/src/themes/hacker.css b/frontend/src/themes/hacker.css new file mode 100644 index 0000000..19c9661 --- /dev/null +++ b/frontend/src/themes/hacker.css @@ -0,0 +1,43 @@ +/* media.rip() — Hacker Theme + * + * Green-on-black terminal aesthetic. Monospace everything, + * phosphor glow, CRT scanlines. You're in the Matrix. + */ + +:root[data-theme="hacker"] { + --color-bg: #0a0a0a; + --color-surface: #0f1a0f; + --color-surface-hover: #152215; + --color-border: #1a3a1a; + + --color-text: #33ff33; + --color-text-muted: #1a8c1a; + + --color-accent: #00ff41; + --color-accent-hover: #33ff66; + --color-accent-secondary: #ffcc00; + + --color-success: #00ff41; + --color-warning: #ffcc00; + --color-error: #ff3333; + + --font-ui: 'Cascadia Code', 'Fira Code', 'JetBrains Mono', 'Courier New', monospace; + --font-mono: 'Cascadia Code', 'Fira Code', 'JetBrains Mono', 'Courier New', monospace; + --font-display: 'Cascadia Code', 'Fira Code', 'JetBrains Mono', 'Courier New', monospace; + + --effect-scanlines: repeating-linear-gradient( + 0deg, + transparent, + transparent 1px, + rgba(0, 255, 65, 0.03) 1px, + rgba(0, 255, 65, 0.03) 2px + ); + --effect-grid: none; + --effect-grid-size: 0px 0px; + --effect-glow: 0 0 12px rgba(0, 255, 65, 0.2); + + --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.4); + --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.5); + --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.6); + --shadow-glow: 0 0 15px rgba(0, 255, 65, 0.12); +} diff --git a/frontend/src/themes/midnight.css b/frontend/src/themes/midnight.css new file mode 100644 index 0000000..b2254bf --- /dev/null +++ b/frontend/src/themes/midnight.css @@ -0,0 +1,40 @@ +/* media.rip() — Midnight Theme + * + * Ultra-minimal dark theme. Near-black backgrounds, + * muted steel blue accents, zero visual effects. + * For people who want the UI to disappear. + */ + +:root[data-theme="midnight"] { + --color-bg: #060608; + --color-surface: #0e0e12; + --color-surface-hover: #16161c; + --color-border: #1c1c24; + + --color-text: #c8ccd0; + --color-text-muted: #5c6370; + + --color-accent: #6b8aaf; + --color-accent-hover: #8aa4c4; + --color-accent-secondary: #af6b8a; + + --color-success: #5faa7c; + --color-warning: #c4a35a; + --color-error: #c45a5a; + + --font-display: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; + + --effect-scanlines: none; + --effect-grid: none; + --effect-grid-size: 0px 0px; + --effect-glow: none; + + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.5); + --shadow-md: 0 4px 8px rgba(0, 0, 0, 0.6); + --shadow-lg: 0 8px 16px rgba(0, 0, 0, 0.7); + --shadow-glow: none; + + --radius-sm: 3px; + --radius-md: 6px; + --radius-lg: 8px; +} diff --git a/frontend/src/themes/neon.css b/frontend/src/themes/neon.css new file mode 100644 index 0000000..a6dca0f --- /dev/null +++ b/frontend/src/themes/neon.css @@ -0,0 +1,42 @@ +/* media.rip() — Neon Theme + * + * Hot pink + cyan on deep purple-black. Vibrant, edgy, + * synthwave-inspired. Glow effects cranked up. + */ + +:root[data-theme="neon"] { + --color-bg: #0d0014; + --color-surface: #150022; + --color-surface-hover: #1e0033; + --color-border: #2a0044; + + --color-text: #f0e0ff; + --color-text-muted: #9966bb; + + --color-accent: #ff2d95; + --color-accent-hover: #ff5cae; + --color-accent-secondary: #00e5ff; + + --color-success: #00e676; + --color-warning: #ffab00; + --color-error: #ff1744; + + --font-display: 'JetBrains Mono', 'Cascadia Code', 'Fira Code', monospace; + + --effect-scanlines: repeating-linear-gradient( + 0deg, + transparent, + transparent 3px, + rgba(255, 45, 149, 0.04) 3px, + rgba(255, 45, 149, 0.04) 4px + ); + --effect-grid: linear-gradient(rgba(0, 229, 255, 0.02) 1px, transparent 1px), + linear-gradient(90deg, rgba(0, 229, 255, 0.02) 1px, transparent 1px); + --effect-grid-size: 40px 40px; + --effect-glow: 0 0 25px rgba(255, 45, 149, 0.2); + + --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.4); + --shadow-md: 0 4px 12px rgba(13, 0, 20, 0.6); + --shadow-lg: 0 8px 24px rgba(13, 0, 20, 0.8); + --shadow-glow: 0 0 20px rgba(255, 45, 149, 0.15); +} diff --git a/frontend/src/themes/paper.css b/frontend/src/themes/paper.css new file mode 100644 index 0000000..cc5d500 --- /dev/null +++ b/frontend/src/themes/paper.css @@ -0,0 +1,40 @@ +/* media.rip() — Paper Theme + * + * Warm cream and sepia tones. Book-like reading feel. + * Soft, easy on the eyes in bright environments. + */ + +:root[data-theme="paper"] { + --color-bg: #f5f0e8; + --color-surface: #fffdf7; + --color-surface-hover: #f0ebe0; + --color-border: #d6cebf; + + --color-text: #2c2416; + --color-text-muted: #7a705e; + + --color-accent: #b85c38; + --color-accent-hover: #a04e2e; + --color-accent-secondary: #2e6b5e; + + --color-success: #3a7d44; + --color-warning: #c48a1a; + --color-error: #b83838; + + --font-ui: 'Georgia', 'Palatino Linotype', 'Book Antiqua', serif; + --font-display: 'Georgia', 'Palatino Linotype', 'Book Antiqua', serif; + + --effect-scanlines: none; + --effect-grid: none; + --effect-grid-size: 0px 0px; + --effect-glow: none; + + --shadow-sm: 0 1px 2px rgba(44, 36, 22, 0.06); + --shadow-md: 0 3px 8px rgba(44, 36, 22, 0.08); + --shadow-lg: 0 8px 20px rgba(44, 36, 22, 0.1); + --shadow-glow: none; + + --radius-sm: 3px; + --radius-md: 6px; + --radius-lg: 10px; +} diff --git a/frontend/src/themes/solarized.css b/frontend/src/themes/solarized.css new file mode 100644 index 0000000..2212842 --- /dev/null +++ b/frontend/src/themes/solarized.css @@ -0,0 +1,36 @@ +/* media.rip() — Solarized Light Theme + * + * Based on Ethan Schoonover's Solarized palette. + * Developer favorite — designed for extended use + * with carefully selected contrast ratios. + */ + +:root[data-theme="solarized"] { + --color-bg: #fdf6e3; + --color-surface: #eee8d5; + --color-surface-hover: #e8e1cc; + --color-border: #d3cbaf; + + --color-text: #586e75; + --color-text-muted: #93a1a1; + + --color-accent: #268bd2; + --color-accent-hover: #1a6da3; + --color-accent-secondary: #d33682; + + --color-success: #859900; + --color-warning: #b58900; + --color-error: #dc322f; + + --font-display: 'Cascadia Code', 'Fira Code', 'JetBrains Mono', monospace; + + --effect-scanlines: none; + --effect-grid: none; + --effect-grid-size: 0px 0px; + --effect-glow: none; + + --shadow-sm: 0 1px 2px rgba(88, 110, 117, 0.08); + --shadow-md: 0 3px 8px rgba(88, 110, 117, 0.1); + --shadow-lg: 0 8px 20px rgba(88, 110, 117, 0.12); + --shadow-glow: none; +}