Styling updates, more robust dynamic/userpref light/dark switching. Sync with paskia-js.

This commit is contained in:
Leo Vasanko
2026-02-05 15:34:02 +00:00
parent 9a6614f057
commit df8bbef8de
7 changed files with 100 additions and 143 deletions
+2 -1
View File
@@ -3,7 +3,8 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Admin</title> <title>Admin</title>
<script>(localStorage.getItem('paskia-theme')==='dark'||localStorage.getItem('paskia-theme')!=='light'&&matchMedia('(prefers-color-scheme:dark)').matches)&&document.documentElement.classList.add('dark')</script>
</head> </head>
<body> <body>
<div id="admin-app"></div> <div id="admin-app"></div>
+1
View File
@@ -4,6 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Auth Profile</title> <title>Auth Profile</title>
<script>(localStorage.getItem('paskia-theme')==='dark'||localStorage.getItem('paskia-theme')!=='light'&&matchMedia('(prefers-color-scheme:dark)').matches)&&document.documentElement.classList.add('dark')</script>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
+1
View File
@@ -3,6 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<script>{let t=localStorage.getItem('paskia-theme');if(t!=='light'&&t!=='dark')t=new URLSearchParams(location.hash.slice(1)).get('theme');(t==='dark'||t!=='light'&&matchMedia('(prefers-color-scheme:dark)').matches)&&document.documentElement.classList.add('dark')}</script>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
+4 -4
View File
@@ -1,11 +1,11 @@
// Early theme for restricted app - first URL param wins, then localStorage // Early theme for restricted app - first URL param wins, then localStorage
import { themeColors, applyTheme, getCachedTheme } from '@/utils/theme.js' import { applyTheme, getCachedTheme } from '@/utils/theme.js'
function getTheme() { function getTheme() {
const params = new URLSearchParams(location.hash.slice(1)) const params = new URLSearchParams(location.hash.slice(1))
return params.get('theme') || getCachedTheme() || '' return params.get('theme') || getCachedTheme() || ''
} }
// Use .surface selector to preserve transparent background // Apply theme class to document root
applyTheme(getTheme(), '.surface') applyTheme(getTheme())
addEventListener('hashchange', () => applyTheme(getTheme(), '.surface')) addEventListener('hashchange', () => applyTheme(getTheme()))
+84 -75
View File
@@ -1,28 +1,37 @@
@property --hue {
syntax: '<angle>';
inherits: true;
initial-value: 0.72turn;
}
:root { :root {
--font-sans: "Inter", "Inter var", "Segoe UI", system-ui, -apple-system, "Helvetica Neue", sans-serif; --font-sans: "Inter", "Inter var", "Segoe UI", system-ui, -apple-system, "Helvetica Neue", sans-serif;
--font-mono: "DM Mono", "JetBrains Mono", "SFMono-Regular", Menlo, Monaco, Consolas, "Liberation Mono", monospace; --font-mono: "DM Mono", "JetBrains Mono", "SFMono-Regular", Menlo, Monaco, Consolas, "Liberation Mono", monospace;
--color-canvas: #ffffff; --color-canvas: white;
--color-surface: #eff6ff; --color-surface: oklch(0.97 0.005 var(--hue));
--color-surface-subtle: #dbeafe; --color-surface-subtle: oklch(0.94 0.01 var(--hue));
--color-border: #2563eb; --color-dialog: white;
--color-border-strong: #1e40af; --color-border: oklch(0.8 0.02 var(--hue));
--color-heading: #1e3a8a; --color-border-strong: oklch(0.55 0.15 var(--hue));
--color-text: #1e293b; --color-heading: oklch(0.3 0.05 var(--hue));
--color-text-muted: #475569; --color-text: oklch(0.25 0.02 var(--hue));
--color-link: #1d4ed8; --color-text-muted: oklch(0.5 0.02 var(--hue));
--color-link-hover: #1e40af; --color-link: oklch(0.5 0.18 var(--hue));
--color-accent: #2563eb; --color-link-hover: oklch(0.45 0.2 var(--hue));
--color-accent-strong: #1e40af; --color-accent: oklch(0.55 0.2 var(--hue));
--color-accent-contrast: #ffffff; --color-accent-strong: oklch(0.45 0.2 var(--hue));
--color-success-text: #166534; --color-accent-contrast: white;
--color-success-bg: #dcfce7; --color-secondary: oklch(0.55 0.05 var(--hue));
--color-error-text: #b91c1c; --color-secondary-strong: oklch(0.45 0.05 var(--hue));
--color-error-bg: #fee2e2; --color-success-text: oklch(0.4 0.15 0.4turn);
--color-info-text: #1e40af; --color-success-bg: oklch(0.95 0.03 0.4turn);
--color-info-bg: #dbeafe; --color-error-text: oklch(0.45 0.2 0.07turn);
--color-danger: #dc2626; --color-error-bg: oklch(0.95 0.03 0.07turn);
--shadow-soft: 0 10px 30px rgba(30, 64, 175, 0.15); --color-info-text: oklch(0.45 0.15 var(--hue));
--color-info-bg: oklch(0.95 0.02 var(--hue));
--color-danger: oklch(0.55 0.22 0.07turn);
--shadow-soft: 0 0 .2rem black;
--radius-none: 0; --radius-none: 0;
--radius-sm: 4px; --radius-sm: 4px;
--radius-md: 6px; --radius-md: 6px;
@@ -39,30 +48,31 @@
--focus-ring: 0 0 0 2px var(--color-accent); --focus-ring: 0 0 0 2px var(--color-accent);
} }
@media (prefers-color-scheme: dark) { :root.dark {
:root { --color-canvas: oklch(0.15 0.03 var(--hue));
--color-canvas: #0f172a; --color-surface: oklch(0.18 0.03 var(--hue));
--color-surface: #141b2f; --color-surface-subtle: oklch(0.22 0.03 var(--hue));
--color-surface-subtle: #1b243b; --color-dialog: oklch(0.22 0.03 var(--hue));
--color-border: #25304a; --color-border: oklch(0.3 0.03 var(--hue));
--color-border-strong: #3d4d6b; --color-border-strong: oklch(0.4 0.04 var(--hue));
--color-heading: #fff; --color-heading: white;
--color-text: #e2e8f0; --color-text: oklch(0.9 0.01 var(--hue));
--color-text-muted: #94a3b8; --color-text-muted: oklch(0.7 0.02 var(--hue));
--color-link: #60a5fa; --color-link: oklch(0.7 0.15 var(--hue));
--color-link-hover: #93c5fd; --color-link-hover: oklch(0.8 0.12 var(--hue));
--color-accent: #60a5fa; --color-accent: oklch(0.7 0.15 var(--hue));
--color-accent-strong: #3b82f6; --color-accent-strong: oklch(0.6 0.18 var(--hue));
--color-accent-contrast: #0b1120; --color-accent-contrast: oklch(0.12 0.03 var(--hue));
--color-success-text: #34d399; --color-secondary: oklch(0.6 0.05 var(--hue));
--color-success-bg: #1a4d2e; --color-secondary-strong: oklch(0.5 0.05 var(--hue));
--color-error-text: #fca5a5; --color-success-text: oklch(0.75 0.15 0.4turn);
--color-error-bg: #4a1f1f; --color-success-bg: oklch(0.3 0.08 0.4turn);
--color-info-text: #bae6fd; --color-error-text: oklch(0.8 0.12 0.07turn);
--color-info-bg: #1e3a5f; --color-error-bg: oklch(0.3 0.08 0.07turn);
--color-danger: #f87171; --color-info-text: oklch(0.8 0.1 var(--hue));
--shadow-soft: 0 0 0 #000000; --color-info-bg: oklch(0.3 0.05 var(--hue));
} --color-danger: oklch(0.7 0.18 0.07turn);
--shadow-soft: 0 0 0 black;
} }
*, *,
@@ -229,6 +239,7 @@ button {
gap: 0.4rem; gap: 0.4rem;
background: var(--color-surface); background: var(--color-surface);
color: var(--color-text); color: var(--color-text);
transition: box-shadow var(--transition-base), background var(--transition-base), border-color var(--transition-base);
} }
button:disabled { button:disabled {
@@ -237,43 +248,41 @@ button:disabled {
} }
.btn-primary { .btn-primary {
background: var(--color-accent); background: linear-gradient(to bottom, oklch(1 0 0 / 0.15), transparent 60%) var(--color-accent);
color: var(--color-accent-contrast); color: var(--color-accent-contrast);
border-color: var(--color-accent); border-color: var(--color-accent);
}
.btn-primary:hover:not(:disabled),
.btn-primary:focus-visible {
background: linear-gradient(to bottom, oklch(1 0 0 / 0.15), transparent 60%) var(--color-accent-strong);
border-color: var(--color-accent-strong);
box-shadow: var(--shadow-soft); box-shadow: var(--shadow-soft);
} }
.btn-primary:hover:not(:disabled) {
background: var(--color-accent-strong);
border-color: var(--color-accent-strong);
}
.btn-secondary { .btn-secondary {
background: transparent; background: linear-gradient(to bottom, oklch(1 0 0 / 0.15), transparent 60%) var(--color-secondary);
color: var(--color-text);
border-color: var(--color-border);
}
.btn-secondary:hover:not(:disabled) {
border-color: var(--color-border-strong);
background: var(--color-surface-subtle);
}
.btn-danger {
background: var(--color-danger);
color: var(--color-accent-contrast); color: var(--color-accent-contrast);
border-color: transparent; border-color: transparent;
} }
.btn-danger:hover:not(:disabled) { .btn-secondary:hover:not(:disabled),
filter: brightness(0.92); .btn-secondary:focus-visible {
background: linear-gradient(to bottom, oklch(1 0 0 / 0.15), transparent 60%) var(--color-secondary-strong);
box-shadow: var(--shadow-soft);
} }
/* Focus-visible outlines for buttons */ .btn-danger {
.btn-primary:focus-visible, background: linear-gradient(to bottom, oklch(1 0 0 / 0.15), transparent 60%) var(--color-danger);
.btn-secondary:focus-visible, color: var(--color-accent-contrast);
border-color: transparent;
}
.btn-danger:hover:not(:disabled),
.btn-danger:focus-visible { .btn-danger:focus-visible {
outline: 1px solid var(-webkit-focus-ring-color); background: linear-gradient(to bottom, oklch(1 0 0 / 0.15), transparent 60%) var(--color-danger);
filter: brightness(0.92);
box-shadow: var(--shadow-soft);
} }
input[type="text"], input[type="text"],
@@ -407,7 +416,7 @@ th {
.device-dialog, .device-dialog,
.modal { .modal {
background: var(--color-surface); background: var(--color-dialog);
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
border-radius: var(--radius-md); border-radius: var(--radius-md);
width: min(520px, 100%); width: min(520px, 100%);
@@ -429,7 +438,7 @@ th {
.qr-code { .qr-code {
padding: 1rem; padding: 1rem;
background: #fff; background: white;
box-shadow: var(--shadow-soft); box-shadow: var(--shadow-soft);
} }
@@ -567,7 +576,7 @@ th {
} }
.btn-card-delete { background: transparent; border: none; color: var(--color-danger); padding: 0.35rem 0.5rem; font-size: 1.05rem; line-height: 1; border-radius: var(--radius-sm); cursor: pointer; display: inline-flex; align-items: center; justify-content: center; } .btn-card-delete { background: transparent; border: none; color: var(--color-danger); padding: 0.35rem 0.5rem; font-size: 1.05rem; line-height: 1; border-radius: var(--radius-sm); cursor: pointer; display: inline-flex; align-items: center; justify-content: center; }
.btn-card-delete:hover:not(:disabled) { background: #fee; } .btn-card-delete:hover:not(:disabled) { filter: brightness(0.85); }
.btn-card-delete:disabled { filter: opacity(0.4); cursor: not-allowed; } .btn-card-delete:disabled { filter: opacity(0.4); cursor: not-allowed; }
@@ -692,8 +701,8 @@ th {
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
background: transparent; background: transparent;
backdrop-filter: blur(.1rem) brightness(0.7); backdrop-filter: blur(.2rem) brightness(0.7);
-webkit-backdrop-filter: blur(.1rem) brightness(0.7); -webkit-backdrop-filter: blur(.2rem) brightness(0.7);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -711,7 +720,7 @@ th {
width: 100%; width: 100%;
max-width: 480px; max-width: 480px;
padding: 2rem; padding: 2rem;
background: var(--color-surface); background: var(--color-dialog);
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
box-shadow: 0 20px 60px #1e293b; box-shadow: 0 20px 60px #1e293b;
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
+6 -61
View File
@@ -1,61 +1,11 @@
// Theme override utilities - shared across apps // Theme override utilities - shared across apps
// User preference or URL hash can force light/dark mode // User preference or URL hash can force light/dark mode
export const themeColors = {
light: {
'color-canvas': '#ffffff',
'color-surface': '#eff6ff',
'color-surface-subtle': '#dbeafe',
'color-border': '#2563eb',
'color-border-strong': '#1e40af',
'color-heading': '#1e3a8a',
'color-text': '#1e293b',
'color-text-muted': '#475569',
'color-link': '#1d4ed8',
'color-link-hover': '#1e40af',
'color-accent': '#2563eb',
'color-accent-strong': '#1e40af',
'color-accent-contrast': '#ffffff',
'color-success-text': '#166534',
'color-success-bg': '#dcfce7',
'color-error-text': '#b91c1c',
'color-error-bg': '#fee2e2',
'color-info-text': '#1e40af',
'color-info-bg': '#dbeafe',
'color-danger': '#dc2626',
'shadow-soft': '0 10px 30px rgba(30, 64, 175, 0.15)',
},
dark: {
'color-canvas': '#0f172a',
'color-surface': '#141b2f',
'color-surface-subtle': '#1b243b',
'color-border': '#25304a',
'color-border-strong': '#3d4d6b',
'color-heading': '#fff',
'color-text': '#e2e8f0',
'color-text-muted': '#94a3b8',
'color-link': '#60a5fa',
'color-link-hover': '#93c5fd',
'color-accent': '#60a5fa',
'color-accent-strong': '#3b82f6',
'color-accent-contrast': '#0b1120',
'color-success-text': '#34d399',
'color-success-bg': '#1a4d2e',
'color-error-text': '#fca5a5',
'color-error-bg': '#4a1f1f',
'color-info-text': '#bae6fd',
'color-info-bg': '#1e3a5f',
'color-danger': '#f87171',
'shadow-soft': '0 0 0 #000000',
}
}
const STYLE_ID = 'theme-override'
const TRANSITION_ID = 'theme-transition' const TRANSITION_ID = 'theme-transition'
const STORAGE_KEY = 'paskia-theme' const STORAGE_KEY = 'paskia-theme'
/** Apply theme override CSS - selector targets .surface for restricted app, :root for main apps */ /** Apply theme by setting class on documentElement */
export function applyTheme(theme, selector = ':root', animate = false) { export function applyTheme(theme, element = document.documentElement, animate = false) {
// Add temporary transition for smooth theme change // Add temporary transition for smooth theme change
if (animate) { if (animate) {
let transitionStyle = document.getElementById(TRANSITION_ID) let transitionStyle = document.getElementById(TRANSITION_ID)
@@ -67,14 +17,9 @@ export function applyTheme(theme, selector = ':root', animate = false) {
} }
setTimeout(() => document.getElementById(TRANSITION_ID)?.remove(), 350) setTimeout(() => document.getElementById(TRANSITION_ID)?.remove(), 350)
} }
document.getElementById(STYLE_ID)?.remove() // If no explicit theme, check system preference
if (theme && themeColors[theme]) { const isDark = theme === 'dark' || (theme !== 'light' && matchMedia('(prefers-color-scheme:dark)').matches)
const css = `${selector} { ${Object.entries(themeColors[theme]).map(([k, v]) => `--${k}: ${v}`).join('; ')}; }` element.classList.toggle('dark', isDark)
const style = document.createElement('style')
style.id = STYLE_ID
style.textContent = css
document.head.appendChild(style)
}
} }
/** Get theme from localStorage cache */ /** Get theme from localStorage cache */
@@ -97,5 +42,5 @@ export function initThemeFromCache() {
export function updateThemeFromSession(ctx, animate = false) { export function updateThemeFromSession(ctx, animate = false) {
const theme = ctx?.user?.theme || '' const theme = ctx?.user?.theme || ''
setCachedTheme(theme) setCachedTheme(theme)
applyTheme(theme, ':root', animate) applyTheme(theme, document.documentElement, animate)
} }
+2 -2
View File
@@ -14,8 +14,8 @@ body::before {
transition: all 0.2s ease-out; transition: all 0.2s ease-out;
} }
body.paskia-backdrop::before { body.paskia-backdrop::before {
-webkit-backdrop-filter: blur(.2rem) brightness(0.5); backdrop-filter: blur(.2rem) brightness(0.7);
backdrop-filter: blur(.2rem) brightness(0.5); -webkit-backdrop-filter: blur(.2rem) brightness(0.7);
visibility: visible; visibility: visible;
} }
body.paskia-backdrop { body.paskia-backdrop {