Styling updates, more robust dynamic/userpref light/dark switching. Sync with paskia-js.
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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()))
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user