diff --git a/html-router/app.css b/html-router/app.css index c6afe6e..7111451 100644 --- a/html-router/app.css +++ b/html-router/app.css @@ -97,6 +97,60 @@ --border: 2px; } + /* ========================================================================== + THEME: Obsidian Prism + A forward-looking neobrutalist dark theme. Cool obsidian base, + prismatic violet shadows, dual-accent system (Signal + Ember). + ========================================================================== */ + [data-theme="obsidian-prism"] { + color-scheme: dark; + + /* --- Canvas & Surfaces --- */ + --color-base-100: oklch(12% 0.015 260); + --color-base-200: oklch(9% 0.018 262); + --color-base-300: oklch(6% 0.02 265); + --color-base-content: oklch(95% 0.008 260); + + /* --- Primary: Electric Violet Signal --- */ + --color-primary: oklch(62% 0.28 290); + --color-primary-content: oklch(98% 0.01 290); + + /* --- Secondary: Cyan Edge --- */ + --color-secondary: oklch(68% 0.18 220); + --color-secondary-content: oklch(98% 0.01 220); + + /* --- Accent: Ember (warm counterpoint) --- */ + --color-accent: oklch(78% 0.19 55); + --color-accent-content: oklch(18% 0.04 55); + + /* --- Neutral: Cold Steel --- */ + --color-neutral: oklch(24% 0.02 260); + --color-neutral-content: oklch(92% 0.01 260); + + /* --- Semantic Colors --- */ + --color-info: oklch(72% 0.14 230); + --color-info-content: oklch(25% 0.06 230); + --color-success: oklch(74% 0.16 155); + --color-success-content: oklch(25% 0.06 155); + --color-warning: oklch(82% 0.18 75); + --color-warning-content: oklch(25% 0.08 75); + --color-error: oklch(68% 0.22 15); + --color-error-content: oklch(98% 0.02 15); + + /* --- Radii (NB Law: Zero) --- */ + --radius-selector: 0rem; + --radius-field: 0rem; + --radius-box: 0rem; + --size-selector: 0.25rem; + --size-field: 0.25rem; + --border: 2px; + + /* --- Prismatic Shadow System --- */ + --nb-shadow-hue: 290; + --nb-shadow: 4px 4px 0 0 oklch(8% 0.06 var(--nb-shadow-hue)); + --nb-shadow-hover: 6px 6px 0 0 oklch(6% 0.08 calc(var(--nb-shadow-hue) + 15)); + } + body { background-color: var(--color-base-100); color: var(--color-base-content); @@ -900,4 +954,109 @@ .toast-alert-title { @apply text-lg font-bold; } +} + +/* ========================================================================== + OBSIDIAN PRISM: Component Overrides & Delight Features + ========================================================================== */ + +/* Prismatic shadow hue shift on hover */ +[data-theme="obsidian-prism"] .nb-panel:hover, +[data-theme="obsidian-prism"] .nb-card:hover, +[data-theme="obsidian-prism"] .nb-btn:hover { + --nb-shadow-hue: 305; +} + +/* Focus state: breathing shadow pulse */ +@keyframes shadow-breathe { + 0%, 100% { box-shadow: 6px 6px 0 0 oklch(8% 0.08 305); } + 50% { box-shadow: 7px 7px 0 0 oklch(10% 0.10 310); } +} + +[data-theme="obsidian-prism"] .nb-btn:focus-visible, +[data-theme="obsidian-prism"] .nb-input:focus-visible, +[data-theme="obsidian-prism"] .nb-select:focus-visible { + animation: shadow-breathe 1.5s ease-in-out infinite; + outline: 2px solid oklch(62% 0.28 290); + outline-offset: 2px; +} + +/* Selection color: Prismatic violet */ +[data-theme="obsidian-prism"] ::selection { + background: oklch(62% 0.28 290 / 0.35); + color: oklch(98% 0.01 290); +} + +/* Prose adjustments for Obsidian Prism */ +[data-theme="obsidian-prism"] .prose-tufte, +[data-theme="obsidian-prism"] .prose-tufte-compact { + color: var(--color-base-content); + --tw-prose-body: oklch(92% 0.008 260); + --tw-prose-headings: oklch(98% 0.01 260); + --tw-prose-lead: oklch(88% 0.01 260); + --tw-prose-links: oklch(78% 0.19 55); + --tw-prose-bold: oklch(98% 0.01 260); + --tw-prose-counters: oklch(70% 0.01 260); + --tw-prose-bullets: oklch(50% 0.01 260); + --tw-prose-hr: oklch(24% 0.02 260); + --tw-prose-quotes: oklch(88% 0.01 260); + --tw-prose-quote-borders: oklch(40% 0.04 290); + --tw-prose-captions: oklch(70% 0.01 260); + --tw-prose-code: oklch(95% 0.008 260); + --tw-prose-pre-code: inherit; + --tw-prose-pre-bg: oklch(8% 0.02 262); + --tw-prose-th-borders: oklch(30% 0.02 260); + --tw-prose-td-borders: oklch(24% 0.02 260); +} + +[data-theme="obsidian-prism"] .prose-tufte a, +[data-theme="obsidian-prism"] .prose-tufte-compact a { + color: oklch(78% 0.19 55); +} + +/* Code blocks: deeper well */ +[data-theme="obsidian-prism"] .markdown-content pre { + background-color: oklch(7% 0.018 262); + border-color: oklch(20% 0.03 290); +} + +[data-theme="obsidian-prism"] .markdown-content :not(pre)>code { + background-color: oklch(18% 0.025 265); +} + +/* Tables in Obsidian Prism */ +[data-theme="obsidian-prism"] .markdown-content th, +[data-theme="obsidian-prism"] .markdown-content td { + border-color: oklch(24% 0.02 260); +} + +/* Blockquotes */ +[data-theme="obsidian-prism"] .markdown-content blockquote { + border-color: oklch(40% 0.04 290); + color: oklch(85% 0.01 260); +} + +/* HR */ +[data-theme="obsidian-prism"] .markdown-content hr { + border-top-color: oklch(24% 0.02 260); +} + +/* Checkbox in Obsidian Prism (white tick) */ +[data-theme="obsidian-prism"] .nb-checkbox:checked { + background-image: url("data:image/svg+xml;utf8,"); +} + +/* Placeholder text */ +[data-theme="obsidian-prism"] .nb-input::placeholder, +[data-theme="obsidian-prism"] .input::placeholder, +[data-theme="obsidian-prism"] .textarea::placeholder, +[data-theme="obsidian-prism"] textarea::placeholder, +[data-theme="obsidian-prism"] input::placeholder { + color: oklch(70% 0.01 260) !important; + opacity: 0.85; +} + +/* Nav shadow uses prismatic color */ +[data-theme="obsidian-prism"] nav { + box-shadow: 4px calc(4px + var(--scroll-depth, 0) * 4px) 0 0 oklch(8% 0.06 290); } \ No newline at end of file diff --git a/html-router/assets/theme-toggle.js b/html-router/assets/theme-toggle.js index 227d878..de4e7dc 100644 --- a/html-router/assets/theme-toggle.js +++ b/html-router/assets/theme-toggle.js @@ -7,6 +7,7 @@ const handleSystemThemeChange = (e) => { if (themePreference === 'system') { document.documentElement.setAttribute('data-theme', e.matches ? 'dark' : 'light'); } + // For explicit themes like 'obsidian-prism', 'light', 'dark' - do nothing on system change }; const initializeTheme = () => { @@ -57,11 +58,12 @@ const initializeTheme = () => { isSystemListenerAttached = true; } } else { + // Explicit theme: 'light', 'dark', 'obsidian-prism', etc. if (isSystemListenerAttached) { systemMediaQuery.removeEventListener('change', handleSystemThemeChange); isSystemListenerAttached = false; } - // Ensure data-theme matches preference + // Ensure data-theme matches preference exactly if (themePreference && document.documentElement.getAttribute('data-theme') !== themePreference) { document.documentElement.setAttribute('data-theme', themePreference); } diff --git a/html-router/src/middlewares/response_middleware.rs b/html-router/src/middlewares/response_middleware.rs index 6ebcaba..1c1f6da 100644 --- a/html-router/src/middlewares/response_middleware.rs +++ b/html-router/src/middlewares/response_middleware.rs @@ -124,7 +124,11 @@ where if let Some(auth) = req.extensions().get::() { if let Some(user) = &auth.current_user { let theme = user.theme.as_str(); - let initial = if theme == "dark" { "dark" } else { "light" }; + // For explicit themes (not "system"), use the theme directly as initial_theme + let initial = match theme { + "system" => "light", + other => other, // "light", "dark", "obsidian-prism", etc. + }; (theme.to_string(), initial.to_string(), true) } else { ("system".to_string(), "light".to_string(), false) diff --git a/html-router/src/routes/account/handlers.rs b/html-router/src/routes/account/handlers.rs index d865854..eb1e431 100644 --- a/html-router/src/routes/account/handlers.rs +++ b/html-router/src/routes/account/handlers.rs @@ -32,6 +32,7 @@ pub async fn show_account_page( let theme_options = vec![ "light".to_string(), "dark".to_string(), + "obsidian-prism".to_string(), "system".to_string(), ]; let conversation_archive = User::get_user_conversations(&user.id, &state.db).await?; @@ -156,6 +157,7 @@ pub async fn update_theme( let theme_options = vec![ "light".to_string(), "dark".to_string(), + "obsidian-prism".to_string(), "system".to_string(), ];