feat(t-paliad-083): dark mode — auto + manual toggle, system-pref default (mAi/paliad#2)
Two-palette swap at :root and :root[data-theme="dark"]; FOUC-prevention
inline <script> in PWAHead reads paliad-theme + paliad-sidebar-pinned +
paliad-sidebar-width from localStorage before the stylesheet loads, so
the page paints in the persisted state from frame one. New theme.ts
client owns the runtime side: cycles auto → light → dark → auto, listens
to prefers-color-scheme while pref="auto", broadcasts change events to
the sidebar toggle so the sun/moon/auto icon stays in sync (incl. on
OS-level theme flips). Sidebar gains a sun/moon toggle below the lang
item with localized aria-label/tooltip describing the next click action.
Surface tokens introduced (--color-surface-{2,muted}, --color-input-bg,
--color-overlay-{faint,subtle,strong,modal}, --color-border-strong,
--shadow-{lg,xl}, --status-{red,amber,green,blue,neutral}-{bg,fg,...},
--tree-icon-{client,litigation,patent,case,project},
--sidebar-scrollbar-{thumb,...,width}); status pills, dashboard cards,
agenda urgency markers, frist-due-chip, akten-status-chip, termin-type
badges all read tokens or get a class-level dark override at the bottom
of global.css. Form inputs render on white in light mode (m: 2026-04-30)
and on a value below --color-surface in dark mode so the well still
reads as depressed below the card panel.
Sidebar scrollbar themed thin + cream-channel alpha with
scrollbar-gutter: stable so the collapsed icon column doesn't shift
when the nav overflows on tall (admin) layouts; .sidebar-icon width
shrinks by var(--sidebar-scrollbar-width) to keep icons centered in
the visible content area.
The pre-paint script also fixes the sidebar-pinned FOUC (maria's add):
sets <html class="sidebar-pinned"> from localStorage before paint, with
sidebar.ts mirroring the class on <html> on every pin toggle so the
new selector :root.sidebar-pinned .has-sidebar tracks the existing
.has-sidebar.sidebar-pinned (body) selector. width is also pre-applied
when within clamp.
Build: bun run build clean (1224 i18n keys, 36 pages).
Smoke: Playwright on /login in both modes — body bg/fg/cards/inputs
read from the right tokens, FOUC script lands in <head> before the
stylesheet, dark→light→auto cycle toggles via the sidebar button.
This commit is contained in:
@@ -1,14 +1,17 @@
|
||||
// app.ts — universal client bundle injected on every page. Three jobs:
|
||||
// app.ts — universal client bundle injected on every page. Four jobs:
|
||||
// 1. Wire the BottomNav (was previously written but never bundled — m
|
||||
// reproduced the broken [+] and Menü buttons in production).
|
||||
// 2. Register the service worker so the site qualifies for PWA install.
|
||||
// 3. Surface the install prompt (Chromium banner / iOS share-sheet hint).
|
||||
// 4. Init the theme listener so the OS-level prefers-color-scheme change
|
||||
// flips the page when the user's pref is "auto" (mAi/paliad#2).
|
||||
//
|
||||
// Per-page bundles still register their own behaviour; this script is
|
||||
// orthogonal and only touches DOM nodes it owns.
|
||||
|
||||
import { initBottomNav } from "./bottom-nav";
|
||||
import { initInstallPrompt } from "./pwa-install";
|
||||
import { initTheme } from "./theme";
|
||||
|
||||
function registerServiceWorker(): void {
|
||||
if (!("serviceWorker" in navigator)) return;
|
||||
@@ -26,6 +29,7 @@ function registerServiceWorker(): void {
|
||||
function boot(): void {
|
||||
initBottomNav();
|
||||
initInstallPrompt();
|
||||
initTheme();
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
|
||||
@@ -43,6 +43,17 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
"nav.neuigkeiten": "Neuigkeiten",
|
||||
"nav.soon.tooltip": "Bald verf\u00fcgbar",
|
||||
|
||||
// Theme toggle (mAi/paliad#2). The button cycles auto \u2192 light \u2192 dark
|
||||
// \u2192 auto. The "toggle.<pref>" keys show the *current* pref next to
|
||||
// the icon; the "cycle.<pref>" keys describe WHAT the next click will
|
||||
// do (read by aria-label / tooltip).
|
||||
"theme.toggle.auto": "Auto",
|
||||
"theme.toggle.light": "Hell",
|
||||
"theme.toggle.dark": "Dunkel",
|
||||
"theme.toggle.cycle.auto": "Auf Hell-Modus wechseln",
|
||||
"theme.toggle.cycle.light": "Auf Dunkel-Modus wechseln",
|
||||
"theme.toggle.cycle.dark": "Auf Auto wechseln (System)",
|
||||
|
||||
// BottomNav (mobile)
|
||||
"bottomnav.add": "Anlegen",
|
||||
"bottomnav.menu": "Menü",
|
||||
@@ -1353,6 +1364,14 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
"nav.neuigkeiten": "What's New",
|
||||
"nav.soon.tooltip": "Coming soon",
|
||||
|
||||
// Theme toggle (mAi/paliad#2)
|
||||
"theme.toggle.auto": "Auto",
|
||||
"theme.toggle.light": "Light",
|
||||
"theme.toggle.dark": "Dark",
|
||||
"theme.toggle.cycle.auto": "Switch to light theme",
|
||||
"theme.toggle.cycle.light": "Switch to dark theme",
|
||||
"theme.toggle.cycle.dark": "Switch to auto (system)",
|
||||
|
||||
// BottomNav (mobile)
|
||||
"bottomnav.add": "New",
|
||||
"bottomnav.menu": "Menu",
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
import { initGlobalSearch } from "./search";
|
||||
import { getChangelogSeen } from "./changelog-seen";
|
||||
import { cycleTheme, getThemePref, onThemeChange, type ThemePref } from "./theme";
|
||||
import { t } from "./i18n";
|
||||
const PIN_KEY = "paliad-sidebar-pinned";
|
||||
const LEGACY_PIN_KEY = "patholo-sidebar-pinned";
|
||||
const WIDTH_KEY = "paliad-sidebar-width";
|
||||
@@ -69,6 +71,7 @@ export function initSidebar() {
|
||||
initGlobalSearch();
|
||||
initChangelogBadge();
|
||||
initAdminGroup();
|
||||
initThemeToggle();
|
||||
const sidebar = document.querySelector<HTMLElement>(".sidebar");
|
||||
if (!sidebar) return;
|
||||
initSidebarResize(sidebar);
|
||||
@@ -83,10 +86,22 @@ export function initSidebar() {
|
||||
return window.innerWidth < 1024;
|
||||
}
|
||||
|
||||
// Restore pin state on desktop
|
||||
// Restore pin state on desktop. We mirror the `sidebar-pinned` class on
|
||||
// both <body> and <html>: the runtime path historically set it on body,
|
||||
// but the pre-paint FOUC script in PWAHead.tsx can only reach <html>
|
||||
// (body doesn't exist yet at that point). Keeping both in sync from
|
||||
// every site that toggles pin state means the CSS selectors
|
||||
// `.has-sidebar.sidebar-pinned` (body-rooted) and
|
||||
// `:root.sidebar-pinned .has-sidebar` (html-rooted) never disagree.
|
||||
if (localStorage.getItem(PIN_KEY) === "true" && !isMobile()) {
|
||||
sidebar.classList.add("pinned");
|
||||
document.body.classList.add("sidebar-pinned");
|
||||
document.documentElement.classList.add("sidebar-pinned");
|
||||
} else {
|
||||
// Pre-paint script may have added it (e.g. last visit was wide enough
|
||||
// and pinned) but current viewport is mobile — clear so the CSS rule
|
||||
// doesn't fight the responsive @media block.
|
||||
document.documentElement.classList.remove("sidebar-pinned");
|
||||
}
|
||||
|
||||
// Desktop: hover expand with 150ms delay
|
||||
@@ -117,11 +132,13 @@ export function initSidebar() {
|
||||
if (wasPinned) {
|
||||
sidebar.classList.remove("pinned");
|
||||
document.body.classList.remove("sidebar-pinned");
|
||||
document.documentElement.classList.remove("sidebar-pinned");
|
||||
localStorage.setItem(PIN_KEY, "false");
|
||||
} else {
|
||||
sidebar.classList.add("pinned");
|
||||
sidebar.classList.remove("expanded");
|
||||
document.body.classList.add("sidebar-pinned");
|
||||
document.documentElement.classList.add("sidebar-pinned");
|
||||
localStorage.setItem(PIN_KEY, "true");
|
||||
}
|
||||
});
|
||||
@@ -167,11 +184,13 @@ export function initSidebar() {
|
||||
if (nowMobile) {
|
||||
sidebar.classList.remove("expanded", "pinned");
|
||||
document.body.classList.remove("sidebar-pinned");
|
||||
document.documentElement.classList.remove("sidebar-pinned");
|
||||
} else {
|
||||
closeMobile();
|
||||
if (localStorage.getItem(PIN_KEY) === "true") {
|
||||
sidebar.classList.add("pinned");
|
||||
document.body.classList.add("sidebar-pinned");
|
||||
document.documentElement.classList.add("sidebar-pinned");
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -295,6 +314,64 @@ function initChangelogBadge(): void {
|
||||
});
|
||||
}
|
||||
|
||||
// initThemeToggle wires the sun/moon button at the bottom of the sidebar
|
||||
// (mAi/paliad#2). The pre-paint inline script in PWAHead.tsx already set
|
||||
// the data-theme attribute on <html>; this function only owns the post-
|
||||
// hydration UI: cycling on click, swapping the icon/label to match the
|
||||
// current preference, and re-rendering when the OS-level scheme flips
|
||||
// while the user is on "auto".
|
||||
const THEME_ICONS: Record<ThemePref, string> = {
|
||||
auto: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 3a9 9 0 0 0 0 18z" fill="currentColor"/></svg>',
|
||||
light: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="M4.93 4.93l1.41 1.41"/><path d="M17.66 17.66l1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="M4.93 19.07l1.41-1.41"/><path d="M17.66 6.34l1.41-1.41"/></svg>',
|
||||
dark: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>',
|
||||
};
|
||||
|
||||
const THEME_LABEL_KEYS: Record<ThemePref, "theme.toggle.auto" | "theme.toggle.light" | "theme.toggle.dark"> = {
|
||||
auto: "theme.toggle.auto",
|
||||
light: "theme.toggle.light",
|
||||
dark: "theme.toggle.dark",
|
||||
};
|
||||
|
||||
function initThemeToggle(): void {
|
||||
const btn = document.getElementById("sidebar-theme-toggle") as HTMLButtonElement | null;
|
||||
const icon = document.getElementById("sidebar-theme-icon") as HTMLElement | null;
|
||||
const label = document.getElementById("sidebar-theme-label") as HTMLElement | null;
|
||||
if (!btn || !icon || !label) return;
|
||||
|
||||
function render(): void {
|
||||
const pref = getThemePref();
|
||||
if (icon) icon.innerHTML = THEME_ICONS[pref];
|
||||
if (label) {
|
||||
const key = THEME_LABEL_KEYS[pref];
|
||||
label.setAttribute("data-i18n", key);
|
||||
label.textContent = t(key);
|
||||
}
|
||||
if (btn) {
|
||||
btn.setAttribute("aria-label", t(key_for_aria(pref)));
|
||||
btn.setAttribute("title", t(key_for_aria(pref)));
|
||||
}
|
||||
}
|
||||
|
||||
function key_for_aria(pref: ThemePref): "theme.toggle.cycle.auto" | "theme.toggle.cycle.light" | "theme.toggle.cycle.dark" {
|
||||
// The aria-label describes WHAT happens on click, not the current
|
||||
// state — assistive tech reads "Switch to dark theme" rather than
|
||||
// just "Auto". Cycle order: auto → light → dark → auto.
|
||||
if (pref === "auto") return "theme.toggle.cycle.auto"; // → light
|
||||
if (pref === "light") return "theme.toggle.cycle.light"; // → dark
|
||||
return "theme.toggle.cycle.dark"; // → auto
|
||||
}
|
||||
|
||||
btn.addEventListener("click", () => {
|
||||
cycleTheme();
|
||||
});
|
||||
|
||||
// theme.ts notifies on user-cycle AND on OS-level prefers-color-scheme
|
||||
// changes while the user is on "auto" — a single subscription covers
|
||||
// both, so the icon/label always tracks the live preference.
|
||||
onThemeChange(render);
|
||||
render();
|
||||
}
|
||||
|
||||
// initAdminGroup reveals the Admin section in the sidebar when the caller's
|
||||
// /api/me lookup confirms global_role='global_admin'. The markup is in the
|
||||
// DOM with display:none for everyone — flipping it on after the fetch lands
|
||||
|
||||
112
frontend/src/client/theme.ts
Normal file
112
frontend/src/client/theme.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
// theme.ts — owns the runtime side of the dark-mode toggle.
|
||||
//
|
||||
// The pre-paint inline <script> in PWAHead.tsx already sets
|
||||
// `<html data-theme="dark">` from localStorage[paliad-theme] before the
|
||||
// stylesheet loads (FOUC-prevention, mAi/paliad#2). This module owns the
|
||||
// post-hydration behaviour:
|
||||
// - exposes the user-facing toggle helpers (cycleTheme, setThemePref)
|
||||
// - re-applies the data-theme attribute when the user changes preference
|
||||
// - listens for system prefers-color-scheme changes when pref === "auto"
|
||||
//
|
||||
// The pre-paint script and this module MUST share the same storage key
|
||||
// and the same "auto + system → dark" decision logic. If you change one,
|
||||
// change the other.
|
||||
|
||||
const STORAGE_KEY = "paliad-theme";
|
||||
|
||||
export type ThemePref = "auto" | "light" | "dark";
|
||||
|
||||
export function getThemePref(): ThemePref {
|
||||
const v = localStorage.getItem(STORAGE_KEY);
|
||||
if (v === "light" || v === "dark") return v;
|
||||
// Anything else (null, "auto", legacy values) collapses to "auto" so a
|
||||
// stale or hand-edited entry can't desync the toggle from the rest of
|
||||
// the system.
|
||||
return "auto";
|
||||
}
|
||||
|
||||
export function setThemePref(pref: ThemePref): void {
|
||||
if (pref === "auto") {
|
||||
// Remove the key entirely on "auto" so the next read sees a clean
|
||||
// null (matching the FOUC script's "no entry → auto" fallback). This
|
||||
// also keeps localStorage tidy between users on shared machines.
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
} else {
|
||||
localStorage.setItem(STORAGE_KEY, pref);
|
||||
}
|
||||
applyTheme();
|
||||
notifyChange();
|
||||
}
|
||||
|
||||
function effectiveTheme(): "light" | "dark" {
|
||||
const pref = getThemePref();
|
||||
if (pref === "light" || pref === "dark") return pref;
|
||||
return matchMedia && matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light";
|
||||
}
|
||||
|
||||
function applyTheme(): void {
|
||||
const t = effectiveTheme();
|
||||
if (t === "dark") {
|
||||
document.documentElement.dataset.theme = "dark";
|
||||
} else {
|
||||
delete document.documentElement.dataset.theme;
|
||||
}
|
||||
}
|
||||
|
||||
const listeners: Array<() => void> = [];
|
||||
|
||||
function notifyChange(): void {
|
||||
for (const cb of listeners) cb();
|
||||
}
|
||||
|
||||
// onThemeChange returns an unsubscribe function. Used by Sidebar.tsx's
|
||||
// theme-toggle button to re-render its icon + label without polling.
|
||||
export function onThemeChange(cb: () => void): () => void {
|
||||
listeners.push(cb);
|
||||
return () => {
|
||||
const i = listeners.indexOf(cb);
|
||||
if (i !== -1) listeners.splice(i, 1);
|
||||
};
|
||||
}
|
||||
|
||||
// cycleTheme advances the preference: auto → light → dark → auto.
|
||||
// Returns the new pref so callers can update aria-label / tooltip.
|
||||
export function cycleTheme(): ThemePref {
|
||||
const next: Record<ThemePref, ThemePref> = {
|
||||
auto: "light",
|
||||
light: "dark",
|
||||
dark: "auto",
|
||||
};
|
||||
const n = next[getThemePref()];
|
||||
setThemePref(n);
|
||||
return n;
|
||||
}
|
||||
|
||||
let initialized = false;
|
||||
|
||||
export function initTheme(): void {
|
||||
if (initialized) return;
|
||||
initialized = true;
|
||||
// Re-apply on hydration. The pre-paint script already covered the
|
||||
// initial frame, but matchMedia may not have been available there
|
||||
// (very old browsers, deep iframes), so we settle the state again.
|
||||
applyTheme();
|
||||
// Listen for OS-level changes — only matters when pref === "auto",
|
||||
// but the listener is cheap and the guard inside the handler keeps
|
||||
// explicit user choices stable.
|
||||
const mq = matchMedia("(prefers-color-scheme: dark)");
|
||||
const handler = () => {
|
||||
if (getThemePref() === "auto") {
|
||||
applyTheme();
|
||||
notifyChange();
|
||||
}
|
||||
};
|
||||
if (typeof mq.addEventListener === "function") {
|
||||
mq.addEventListener("change", handler);
|
||||
} else if (typeof (mq as MediaQueryList & { addListener?: (h: () => void) => void }).addListener === "function") {
|
||||
// Older Safari: the deprecated addListener still works.
|
||||
(mq as MediaQueryList & { addListener: (h: () => void) => void }).addListener(handler);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,27 @@
|
||||
import { h, Fragment } from "../jsx";
|
||||
|
||||
// FOUC-prevention bootstrap (mAi/paliad#2 + maria's instruction 2026-04-30).
|
||||
//
|
||||
// Runs synchronously in <head> BEFORE the stylesheet starts loading. Its
|
||||
// only job is to set classes/attributes on <html> that change first-frame
|
||||
// layout, so the page paints in the user's persisted state instead of
|
||||
// flickering from default → JS-applied state.
|
||||
//
|
||||
// Three pieces of state are restored:
|
||||
// 1. paliad-theme — light / dark / auto-with-system-pref
|
||||
// 2. paliad-sidebar-pinned — desktop-only padding-left swap
|
||||
// 3. paliad-sidebar-width — user-resized rail width
|
||||
//
|
||||
// Body class for `sidebar-pinned` is owned by client/sidebar.ts post-
|
||||
// hydration (we can't touch document.body from <head>). The CSS uses two
|
||||
// selectors — `.has-sidebar.sidebar-pinned` (body class, runtime) and
|
||||
// `:root.sidebar-pinned .has-sidebar` (html class, this script) — so
|
||||
// either source pre-paints the correct padding.
|
||||
//
|
||||
// Storage keys and decision logic mirror frontend/src/client/theme.ts and
|
||||
// client/sidebar.ts. If you change one, change the others.
|
||||
const FOUC_BOOTSTRAP = `(function(){try{var d=document.documentElement,ls=localStorage;var p=ls.getItem("paliad-theme");if(p==="dark"||((!p||p==="auto")&&window.matchMedia&&matchMedia("(prefers-color-scheme: dark)").matches))d.setAttribute("data-theme","dark");if(window.innerWidth>=1024&&ls.getItem("paliad-sidebar-pinned")==="true")d.classList.add("sidebar-pinned");var w=parseInt(ls.getItem("paliad-sidebar-width")||"",10);if(w>=180&&w<=480)d.style.setProperty("--sidebar-width",w+"px");}catch(e){}})();`;
|
||||
|
||||
// PWAHead emits the head fragment that turns Paliad into an installable PWA.
|
||||
// Add it to every page's <head> alongside the existing viewport / theme-color
|
||||
// metas. The <script src="/assets/app.js"> registers the service worker,
|
||||
@@ -9,6 +31,7 @@ import { h, Fragment } from "../jsx";
|
||||
export function PWAHead(): string {
|
||||
return (
|
||||
<Fragment>
|
||||
<script dangerouslySetInnerHTML={{ __html: FOUC_BOOTSTRAP }} />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png" />
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="/icons/icon-192.png" />
|
||||
|
||||
@@ -25,6 +25,14 @@ const ICON_SPARKLE = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
const ICON_USERS = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>';
|
||||
const ICON_SHIELD = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>';
|
||||
const ICON_AUDIT_LOG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="9" y1="13" x2="15" y2="13"/><line x1="9" y1="17" x2="15" y2="17"/></svg>';
|
||||
// Theme-toggle icons. The button cycles auto → light → dark → auto, and
|
||||
// the icon swaps to reflect the *current* preference (auto/light/dark)
|
||||
// — not the eventual click target. SSR renders the auto variant; the
|
||||
// runtime client (sidebar.ts) re-renders the matching icon on hydration
|
||||
// after reading localStorage.
|
||||
const ICON_THEME_AUTO = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 3a9 9 0 0 0 0 18z" fill="currentColor"/></svg>';
|
||||
const ICON_THEME_LIGHT = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="M4.93 4.93l1.41 1.41"/><path d="M17.66 17.66l1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="M4.93 19.07l1.41-1.41"/><path d="M17.66 6.34l1.41-1.41"/></svg>';
|
||||
const ICON_THEME_DARK = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>';
|
||||
|
||||
interface SidebarProps {
|
||||
currentPath: string;
|
||||
@@ -172,6 +180,18 @@ export function Sidebar({ currentPath, authenticated = true }: SidebarProps): st
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
{/* Theme toggle (mAi/paliad#2). SSR renders the auto/system variant;
|
||||
client/sidebar.ts swaps the icon + label to match the persisted
|
||||
preference on hydration, and re-renders on every cycle. The
|
||||
data-theme-toggle attribute is the click handle so the wiring
|
||||
survives any future markup churn. */}
|
||||
<button type="button" className="sidebar-item sidebar-theme-toggle"
|
||||
id="sidebar-theme-toggle" data-theme-toggle="cycle"
|
||||
aria-label="Theme">
|
||||
<span className="sidebar-icon" id="sidebar-theme-icon"
|
||||
dangerouslySetInnerHTML={{ __html: ICON_THEME_AUTO }} />
|
||||
<span className="sidebar-label" id="sidebar-theme-label" data-i18n="theme.toggle.auto">Auto</span>
|
||||
</button>
|
||||
<a href="/logout" className="sidebar-item sidebar-logout">
|
||||
<span className="sidebar-icon" dangerouslySetInnerHTML={{ __html: ICON_LOGOUT }} />
|
||||
<span className="sidebar-label" data-i18n="nav.logout">Abmelden</span>
|
||||
|
||||
@@ -1226,4 +1226,10 @@ export type I18nKey =
|
||||
| "team.partner_unit.unassigned"
|
||||
| "team.search.placeholder"
|
||||
| "team.subtitle"
|
||||
| "team.title";
|
||||
| "team.title"
|
||||
| "theme.toggle.auto"
|
||||
| "theme.toggle.cycle.auto"
|
||||
| "theme.toggle.cycle.dark"
|
||||
| "theme.toggle.cycle.light"
|
||||
| "theme.toggle.dark"
|
||||
| "theme.toggle.light";
|
||||
|
||||
@@ -19,25 +19,80 @@
|
||||
--hlc-cyan-rgb: 159 227 217;
|
||||
--hlc-cream-rgb: 238 229 225;
|
||||
|
||||
/* Surface + semantic tokens — re-pointed onto the brand palette. */
|
||||
/* Surface + semantic tokens — re-pointed onto the brand palette.
|
||||
Light mode is the default. Dark mode overrides every token below
|
||||
in the :root[data-theme="dark"] block further down. Components
|
||||
that read these tokens (instead of palette/hex literals) get the
|
||||
theme swap for free. */
|
||||
--color-bg: var(--hlc-cream);
|
||||
--color-bg-subtle: #f7f3f0; /* slightly off-cream for table headers / soft surfaces */
|
||||
--color-bg-lime-tint: rgb(var(--hlc-lime-rgb) / 0.10);
|
||||
--color-surface: #ffffff; /* cards stay white for contrast */
|
||||
--color-surface-2: #fafafa; /* slightly raised surface (alt rows, code blocks) */
|
||||
--color-surface-muted: #f3f4f6; /* nested muted surfaces (chips, soft fills) */
|
||||
/* Form inputs render on white in light mode (m: 2026-04-30 — the
|
||||
cream `--color-bg` made inputs feel like background blocks, not
|
||||
wells). In dark mode we drop slightly *below* the card surface
|
||||
so the input still reads as a depressed well rather than merging
|
||||
into the card panel. */
|
||||
--color-input-bg: #ffffff;
|
||||
--color-text: var(--hlc-midnight);
|
||||
--color-text-muted: #5a6573;
|
||||
--color-text-subtle: #6b7280; /* third-tier text (placeholders, helper) */
|
||||
--color-accent: var(--hlc-lime);
|
||||
--color-accent-light: #d8f78a; /* lighter lime for hover */
|
||||
--color-accent-dark: var(--hlc-midnight); /* foreground on lime — WCAG AA */
|
||||
/* Accent text colour. Lime fails WCAG on the cream/white BGs we use
|
||||
in light mode, so accent emphasis in body copy is rendered in
|
||||
midnight here. Dark-mode (mAi/paliad#2) flips this back to lime,
|
||||
and the .sidebar scope below already keeps lime today since the
|
||||
sidebar lives on a midnight BG. */
|
||||
midnight here. Dark-mode flips this to lime (lime on midnight is
|
||||
WCAG AA); the .sidebar scope below already keeps lime today since
|
||||
the sidebar lives on a midnight BG, so it's a no-op there. */
|
||||
--color-accent-fg: var(--hlc-midnight);
|
||||
--color-border: #e1dcd6; /* derived from cream, neutral warm */
|
||||
--color-border-strong: #d4d4d8; /* stronger border for tab/select boundaries */
|
||||
--color-hero-bg: var(--hlc-midnight);
|
||||
--color-hero-text: #ffffff;
|
||||
/* Overlay tints for hover/stripe/divider — opacities tuned so they read
|
||||
on cream backgrounds. Dark mode swaps to white-channel alphas. */
|
||||
--color-overlay-faint: rgba(0, 0, 0, 0.04);
|
||||
--color-overlay-subtle: rgba(0, 0, 0, 0.06);
|
||||
--color-overlay-strong: rgba(0, 0, 0, 0.10);
|
||||
--color-overlay-modal: rgba(0, 0, 0, 0.4); /* modal/drawer scrim */
|
||||
|
||||
/* Status palette — five buckets (red/amber/green/blue/neutral) shared
|
||||
across dashboard cards, frist-due-chips, agenda urgency, termin
|
||||
badges, login forms. Light values match the existing pastel-on-dark
|
||||
Tailwind-100/700 pattern; dark values invert to a tinted alpha
|
||||
backdrop with bright foreground for AA contrast on dark surfaces. */
|
||||
--status-red-bg: #fee2e2;
|
||||
--status-red-fg: #b91c1c;
|
||||
--status-red-border: #fecaca;
|
||||
--status-amber-bg: #fef3c7;
|
||||
--status-amber-fg: #92400e;
|
||||
--status-amber-fg-2: #b45309;
|
||||
--status-amber-border: #fcd34d;
|
||||
--status-green-bg: #dcfce7;
|
||||
--status-green-fg: #166534;
|
||||
--status-green-border: #bbf7d0;
|
||||
--status-green-soft-bg: #ecfccb;
|
||||
--status-green-soft-fg: #365314;
|
||||
--status-blue-bg: #dbeafe;
|
||||
--status-blue-fg: #1e40af;
|
||||
--status-blue-fg-2: #2563eb;
|
||||
--status-blue-soft-bg: #eef2ff;
|
||||
--status-blue-soft-fg: #4338ca;
|
||||
--status-neutral-bg: #f3f4f6;
|
||||
--status-neutral-fg: #6b7280;
|
||||
--status-neutral-fg-2: #475569;
|
||||
--status-neutral-fg-3: #374151;
|
||||
/* Project-tree icon palette — five identity colours by node type.
|
||||
Light values are saturated-mid; dark values are brightened pastels
|
||||
so they remain legible on midnight without losing identity. */
|
||||
--tree-icon-client: #4338ca;
|
||||
--tree-icon-litigation: #9d174d;
|
||||
--tree-icon-patent: #075985;
|
||||
--tree-icon-case: #92400e;
|
||||
--tree-icon-project: #166534;
|
||||
|
||||
/* Sidebar — dark midnight surface with cream text + lime active.
|
||||
Built from cream-channel alphas so contrast stays consistent
|
||||
@@ -54,6 +109,16 @@
|
||||
--radius: 8px;
|
||||
--shadow: 0 1px 3px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.04);
|
||||
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
--shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.15);
|
||||
--shadow-xl: 0 20px 60px rgba(0, 0, 0, 0.15);
|
||||
/* Sidebar scrollbar — themed thin scrollbar so the auto-overflow
|
||||
inside the nav reads as part of the dark sidebar surface and
|
||||
doesn't clash visually with the page background. Cream-channel
|
||||
alpha keeps it consistent in both light and dark mode (the
|
||||
sidebar itself is always midnight). */
|
||||
--sidebar-scrollbar-thumb: rgb(var(--hlc-cream-rgb) / 0.20);
|
||||
--sidebar-scrollbar-thumb-hover: rgb(var(--hlc-cream-rgb) / 0.35);
|
||||
--sidebar-scrollbar-width: 6px;
|
||||
--max-width: 1080px;
|
||||
--sidebar-collapsed: 64px;
|
||||
--sidebar-expanded: 240px;
|
||||
@@ -66,6 +131,88 @@
|
||||
--bottom-nav-height: 56px;
|
||||
}
|
||||
|
||||
/* Dark mode (mAi/paliad#2). Activated by the pre-paint inline script in
|
||||
PWAHead.tsx setting <html data-theme="dark"> from localStorage[paliad-theme]
|
||||
("dark" | "auto"+system-prefers-dark). The sidebar already lives on
|
||||
midnight in light mode — its --sidebar-* tokens stay unchanged here, so
|
||||
the sidebar reads identically across themes. The body palette flips to
|
||||
midnight-bg + cream-fg, and accent text flips back to lime (passes WCAG
|
||||
AA on midnight). Status pills, dashboard cards, and tree-icon colours
|
||||
are tinted-alpha-on-dark for readability. */
|
||||
:root[data-theme="dark"] {
|
||||
--color-bg: var(--hlc-midnight);
|
||||
--color-bg-subtle: #00304a; /* slightly raised for table headers */
|
||||
--color-bg-lime-tint: rgb(var(--hlc-lime-rgb) / 0.12);
|
||||
--color-surface: #0a3047; /* card surface — distinct from body */
|
||||
--color-surface-2: #0d3a55; /* one step further raised */
|
||||
--color-surface-muted: rgb(var(--hlc-cream-rgb) / 0.05);
|
||||
/* Input wells render BELOW card surface so the depression reads. */
|
||||
--color-input-bg: #00192a;
|
||||
--color-text: var(--hlc-cream);
|
||||
--color-text-muted: rgb(var(--hlc-cream-rgb) / 0.66);
|
||||
--color-text-subtle: rgb(var(--hlc-cream-rgb) / 0.50);
|
||||
--color-accent-light: #9bd926; /* darker lime for hover on dark */
|
||||
--color-accent-dark: var(--hlc-midnight); /* foreground on lime stays midnight */
|
||||
--color-accent-fg: var(--hlc-lime); /* lime text on midnight is AA */
|
||||
--color-border: rgb(var(--hlc-cream-rgb) / 0.12);
|
||||
--color-border-strong: rgb(var(--hlc-cream-rgb) / 0.22);
|
||||
--color-hero-bg: #0a3047; /* hero panel one step lighter than bg */
|
||||
--color-hero-text: var(--hlc-cream);
|
||||
|
||||
--color-overlay-faint: rgba(255, 255, 255, 0.04);
|
||||
--color-overlay-subtle: rgba(255, 255, 255, 0.07);
|
||||
--color-overlay-strong: rgba(255, 255, 255, 0.12);
|
||||
--color-overlay-modal: rgba(0, 0, 0, 0.65);
|
||||
|
||||
--shadow: 0 1px 3px rgba(0, 0, 0, 0.4), 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.45);
|
||||
--shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.55);
|
||||
--shadow-xl: 0 20px 60px rgba(0, 0, 0, 0.6);
|
||||
|
||||
/* Status palette — alpha-tinted backdrops so the pill reads as a
|
||||
coloured swatch rather than a saturated rectangle. Foregrounds are
|
||||
the Tailwind 300-shades for AA contrast on the alpha bg. */
|
||||
--status-red-bg: rgb(239 68 68 / 0.18);
|
||||
--status-red-fg: #fca5a5;
|
||||
--status-red-border: rgb(239 68 68 / 0.35);
|
||||
--status-amber-bg: rgb(245 158 11 / 0.18);
|
||||
--status-amber-fg: #fcd34d;
|
||||
--status-amber-fg-2: #fbbf24;
|
||||
--status-amber-border: rgb(245 158 11 / 0.35);
|
||||
--status-green-bg: rgb(34 197 94 / 0.18);
|
||||
--status-green-fg: #86efac;
|
||||
--status-green-border: rgb(34 197 94 / 0.35);
|
||||
--status-green-soft-bg: rgb(132 204 22 / 0.18);
|
||||
--status-green-soft-fg: #bef264;
|
||||
--status-blue-bg: rgb(59 130 246 / 0.18);
|
||||
--status-blue-fg: #93c5fd;
|
||||
--status-blue-fg-2: #60a5fa;
|
||||
--status-blue-soft-bg: rgb(99 102 241 / 0.18);
|
||||
--status-blue-soft-fg: #a5b4fc;
|
||||
--status-neutral-bg: rgb(var(--hlc-cream-rgb) / 0.08);
|
||||
--status-neutral-fg: rgb(var(--hlc-cream-rgb) / 0.66);
|
||||
--status-neutral-fg-2: rgb(var(--hlc-cream-rgb) / 0.55);
|
||||
--status-neutral-fg-3: rgb(var(--hlc-cream-rgb) / 0.78);
|
||||
|
||||
/* Tree-icon colours — brighter shades so they remain identifiable
|
||||
on midnight without losing their colour identity. */
|
||||
--tree-icon-client: #818cf8;
|
||||
--tree-icon-litigation: #f472b6;
|
||||
--tree-icon-patent: #38bdf8;
|
||||
--tree-icon-case: #fbbf24;
|
||||
--tree-icon-project: #86efac;
|
||||
}
|
||||
|
||||
/* Smooth the body-background swap so theme toggles don't snap. Surface
|
||||
and text colors are read by individual elements and inherit a similar
|
||||
transition where they apply their own background/color shorthand. */
|
||||
:root {
|
||||
color-scheme: light;
|
||||
}
|
||||
:root[data-theme="dark"] {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
*, *::before, *::after {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@@ -398,9 +545,9 @@ main {
|
||||
}
|
||||
|
||||
.login-error {
|
||||
background: #fef2f2;
|
||||
color: #991b1b;
|
||||
border: 1px solid #fecaca;
|
||||
background: var(--status-red-bg);
|
||||
color: var(--status-red-fg);
|
||||
border: 1px solid var(--status-red-border);
|
||||
border-radius: var(--radius);
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 0.85rem;
|
||||
@@ -409,9 +556,9 @@ main {
|
||||
}
|
||||
|
||||
.login-success {
|
||||
background: #f0fdf4;
|
||||
color: #166534;
|
||||
border: 1px solid #bbf7d0;
|
||||
background: var(--status-green-bg);
|
||||
color: var(--status-green-fg);
|
||||
border: 1px solid var(--status-green-border);
|
||||
border-radius: var(--radius);
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 0.85rem;
|
||||
@@ -445,7 +592,7 @@ main {
|
||||
outline: none;
|
||||
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
||||
color: var(--color-text);
|
||||
background: var(--color-bg);
|
||||
background: var(--color-input-bg);
|
||||
}
|
||||
|
||||
.login-input:focus {
|
||||
@@ -657,6 +804,31 @@ main {
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
/* Themed thin scrollbar — cream-channel alpha so it reads as part of
|
||||
the midnight sidebar surface in both light and dark mode. The
|
||||
gutter is reserved (`stable`) so the sidebar's icon column doesn't
|
||||
shift left when the scrollbar appears (admin nav can be tall
|
||||
enough to overflow on shorter viewports). */
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--sidebar-scrollbar-thumb) transparent;
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
/* WebKit fallback (Safari, older Chromium): explicit thumb styling. The
|
||||
thumb width matches --sidebar-scrollbar-width so the visual gutter is
|
||||
the same on Firefox (`scrollbar-width: thin`) and Webkit. */
|
||||
.sidebar-nav::-webkit-scrollbar {
|
||||
width: var(--sidebar-scrollbar-width);
|
||||
}
|
||||
.sidebar-nav::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
.sidebar-nav::-webkit-scrollbar-thumb {
|
||||
background: var(--sidebar-scrollbar-thumb);
|
||||
border-radius: 3px;
|
||||
}
|
||||
.sidebar-nav::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--sidebar-scrollbar-thumb-hover);
|
||||
}
|
||||
|
||||
.sidebar-item {
|
||||
@@ -699,7 +871,13 @@ main {
|
||||
}
|
||||
|
||||
.sidebar-icon {
|
||||
width: var(--sidebar-collapsed);
|
||||
/* Width accounts for the reserved scrollbar gutter on .sidebar-nav so
|
||||
icons stay visually centered inside the 64px collapsed rail rather
|
||||
than drifting left when the scrollbar appears (or appearing left of
|
||||
center even when it doesn't, since `scrollbar-gutter: stable`
|
||||
always reserves the gutter). The other sidebar items (header,
|
||||
bottom, hamburger) sit outside .sidebar-nav and are unaffected. */
|
||||
width: calc(var(--sidebar-collapsed) - var(--sidebar-scrollbar-width));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -789,7 +967,14 @@ main {
|
||||
transition: padding-left 150ms ease;
|
||||
}
|
||||
|
||||
.has-sidebar.sidebar-pinned {
|
||||
/* Two selectors apply the pinned padding because the pre-paint inline
|
||||
script in PWAHead.tsx sets `<html class="sidebar-pinned">` (body
|
||||
doesn't exist yet at that point), while the runtime sidebar.ts later
|
||||
adds `sidebar-pinned` to `<body>` too. Either source is sufficient
|
||||
and they don't conflict. Keeping both keeps the legacy DOM contract
|
||||
intact (sidebar.ts continues to manage the body class). */
|
||||
.has-sidebar.sidebar-pinned,
|
||||
:root.sidebar-pinned .has-sidebar {
|
||||
padding-left: var(--sidebar-width);
|
||||
}
|
||||
|
||||
@@ -861,7 +1046,7 @@ main {
|
||||
padding: 0.45rem 0.7rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius);
|
||||
background: var(--color-bg);
|
||||
background: var(--color-input-bg);
|
||||
color: var(--color-text);
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
@@ -1590,23 +1775,23 @@ input[type="range"]::-moz-range-thumb {
|
||||
}
|
||||
|
||||
.party-claimant {
|
||||
background: #dbeafe;
|
||||
color: #1e40af;
|
||||
background: var(--status-blue-bg);
|
||||
color: var(--status-blue-fg);
|
||||
}
|
||||
|
||||
.party-defendant {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
background: var(--status-amber-bg);
|
||||
color: var(--status-amber-fg);
|
||||
}
|
||||
|
||||
.party-court {
|
||||
background: #f3e8ff;
|
||||
color: #6b21a8;
|
||||
background: var(--status-blue-soft-bg);
|
||||
color: var(--status-blue-soft-fg);
|
||||
}
|
||||
|
||||
.party-both {
|
||||
background: #e5e7eb;
|
||||
color: #374151;
|
||||
background: var(--status-neutral-bg);
|
||||
color: var(--status-neutral-fg-3);
|
||||
}
|
||||
|
||||
.optional-badge {
|
||||
@@ -1614,8 +1799,8 @@ input[type="range"]::-moz-range-thumb {
|
||||
font-weight: 500;
|
||||
padding: 0.05rem 0.4rem;
|
||||
border-radius: 99px;
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
background: var(--status-amber-bg);
|
||||
color: var(--status-amber-fg);
|
||||
}
|
||||
|
||||
.timeline-rule {
|
||||
@@ -1626,7 +1811,7 @@ input[type="range"]::-moz-range-thumb {
|
||||
|
||||
.timeline-adjusted {
|
||||
font-size: 0.78rem;
|
||||
color: #d97706;
|
||||
color: var(--status-amber-fg-2);
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
|
||||
@@ -1670,7 +1855,7 @@ input[type="range"]::-moz-range-thumb {
|
||||
display: block;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
background: var(--color-overlay-modal);
|
||||
z-index: 35;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
@@ -1683,7 +1868,8 @@ input[type="range"]::-moz-range-thumb {
|
||||
}
|
||||
|
||||
.has-sidebar,
|
||||
.has-sidebar.sidebar-pinned {
|
||||
.has-sidebar.sidebar-pinned,
|
||||
:root.sidebar-pinned .has-sidebar {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
@@ -1903,7 +2089,7 @@ input[type="range"]::-moz-range-thumb {
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
background: var(--color-overlay-modal);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -1914,7 +2100,7 @@ input[type="range"]::-moz-range-thumb {
|
||||
.modal-card {
|
||||
background: var(--color-surface);
|
||||
border-radius: calc(var(--radius) * 1.5);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
|
||||
box-shadow: var(--shadow-lg);
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
max-height: 90vh;
|
||||
@@ -1972,7 +2158,12 @@ input[type="range"]::-moz-range-thumb {
|
||||
font-size: 0.88rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius);
|
||||
background: var(--color-bg);
|
||||
/* Form fields render on --color-input-bg, which is #ffffff in light
|
||||
mode (m: 2026-04-30 — the cream `--color-bg` made inputs blur into
|
||||
the body; white reads as a proper input well) and a value below
|
||||
--color-surface in dark mode so the well is depressed below the
|
||||
card panel rather than merging into it. */
|
||||
background: var(--color-input-bg);
|
||||
color: var(--color-text);
|
||||
font-family: var(--font-sans);
|
||||
outline: none;
|
||||
@@ -2425,7 +2616,7 @@ input[type="range"]::-moz-range-thumb {
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
background: var(--color-overlay-modal);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -2439,7 +2630,7 @@ input[type="range"]::-moz-range-thumb {
|
||||
padding: 2rem;
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
|
||||
box-shadow: var(--shadow-xl);
|
||||
}
|
||||
|
||||
.modal-content-sm {
|
||||
@@ -2473,7 +2664,8 @@ input[type="range"]::-moz-range-thumb {
|
||||
font-family: var(--font-sans);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius);
|
||||
background: var(--color-bg);
|
||||
/* See .akten-form input rule above for the --color-input-bg rationale. */
|
||||
background: var(--color-input-bg);
|
||||
color: var(--color-text);
|
||||
transition: border-color 0.15s ease;
|
||||
}
|
||||
@@ -3824,7 +4016,7 @@ input[type="range"]::-moz-range-thumb {
|
||||
font-size: 0.85rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius);
|
||||
background: #fff;
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -4093,11 +4285,11 @@ input[type="range"]::-moz-range-thumb {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.projekt-tree-icon-client { color: #4338ca; }
|
||||
.projekt-tree-icon-litigation { color: #9d174d; }
|
||||
.projekt-tree-icon-patent { color: #075985; }
|
||||
.projekt-tree-icon-case { color: #92400e; }
|
||||
.projekt-tree-icon-project { color: #166534; }
|
||||
.projekt-tree-icon-client { color: var(--tree-icon-client); }
|
||||
.projekt-tree-icon-litigation { color: var(--tree-icon-litigation); }
|
||||
.projekt-tree-icon-patent { color: var(--tree-icon-patent); }
|
||||
.projekt-tree-icon-case { color: var(--tree-icon-case); }
|
||||
.projekt-tree-icon-project { color: var(--tree-icon-project); }
|
||||
|
||||
.projekt-tree-title {
|
||||
font-weight: 600;
|
||||
@@ -4257,7 +4449,7 @@ input[type="range"]::-moz-range-thumb {
|
||||
flex-wrap: wrap;
|
||||
gap: 0.3rem;
|
||||
align-items: center;
|
||||
background: #fff;
|
||||
background: var(--color-surface);
|
||||
}
|
||||
|
||||
.akten-collab input {
|
||||
@@ -4302,7 +4494,7 @@ input[type="range"]::-moz-range-thumb {
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin-top: 0.25rem;
|
||||
background: #fff;
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow-md);
|
||||
@@ -4319,7 +4511,7 @@ input[type="range"]::-moz-range-thumb {
|
||||
align-items: center;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: none;
|
||||
background: #fff;
|
||||
background: var(--color-surface);
|
||||
text-align: left;
|
||||
font-size: 0.85rem;
|
||||
cursor: pointer;
|
||||
@@ -4391,7 +4583,7 @@ input[type="range"]::-moz-range-thumb {
|
||||
height: 36px;
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid var(--color-border);
|
||||
background: #fff;
|
||||
background: var(--color-surface);
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -6446,7 +6638,7 @@ input[type="range"]::-moz-range-thumb {
|
||||
gap: 0.85rem;
|
||||
align-items: flex-start;
|
||||
padding: 0.9rem 1rem;
|
||||
background: #fff;
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border, #e5e5ed);
|
||||
border-radius: 12px;
|
||||
transition: border-color 0.15s, box-shadow 0.15s;
|
||||
@@ -6720,7 +6912,7 @@ dialog.quick-add-sheet[open] {
|
||||
}
|
||||
|
||||
dialog.quick-add-sheet::backdrop {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
background: var(--color-overlay-modal);
|
||||
}
|
||||
|
||||
.quick-add-card {
|
||||
@@ -6728,7 +6920,7 @@ dialog.quick-add-sheet::backdrop {
|
||||
max-width: 480px;
|
||||
background: var(--color-surface);
|
||||
border-radius: 16px 16px 0 0;
|
||||
box-shadow: 0 -4px 24px rgba(0, 0, 0, 0.12);
|
||||
box-shadow: var(--shadow-md);
|
||||
padding: 0.5rem 1rem calc(1rem + env(safe-area-inset-bottom));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -7049,7 +7241,7 @@ dialog.quick-add-sheet::backdrop {
|
||||
font-size: 0.85rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius);
|
||||
background: #fff;
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
@@ -7279,7 +7471,7 @@ dialog.quick-add-sheet::backdrop {
|
||||
font-size: 0.9rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius);
|
||||
background: #fff;
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
@@ -7692,3 +7884,116 @@ dialog.quick-add-sheet::backdrop {
|
||||
height: 480px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===================================================================
|
||||
Dark mode — class-level overrides for sites that still hold their
|
||||
own hardcoded colours (status pills, dashboard cards, agenda urgency,
|
||||
termin types). The bulk of the swap is handled by the token redef
|
||||
at :root[data-theme="dark"] earlier in this file; this block targets
|
||||
the long-tail of class-scoped colour pairs that are too local to
|
||||
warrant a full token. Keeping them in one section makes the dark-
|
||||
mode palette greppable when tweaking contrast.
|
||||
=================================================================== */
|
||||
:root[data-theme="dark"] {
|
||||
/* Surface elevations — tables/alt-rows/code-blocks read flat against
|
||||
the body background otherwise. */
|
||||
}
|
||||
|
||||
/* Soft amber surfaces (warning callouts, akten-unavailable, etc) */
|
||||
:root[data-theme="dark"] .akten-unavailable {
|
||||
background: var(--status-amber-bg);
|
||||
color: var(--status-amber-fg);
|
||||
}
|
||||
|
||||
/* Dashboard card counts + left-borders. Light mode keeps the saturated
|
||||
Tailwind 700-shades; dark mode uses brightened pastels for AA. */
|
||||
:root[data-theme="dark"] .dashboard-card-red .dashboard-card-count { color: var(--status-red-fg); }
|
||||
:root[data-theme="dark"] .dashboard-card-red { border-left-color: var(--status-red-fg); }
|
||||
:root[data-theme="dark"] .dashboard-card-amber .dashboard-card-count { color: var(--status-amber-fg-2); }
|
||||
:root[data-theme="dark"] .dashboard-card-amber { border-left-color: var(--status-amber-fg-2); }
|
||||
:root[data-theme="dark"] .dashboard-card-green .dashboard-card-count { color: var(--status-green-fg); }
|
||||
:root[data-theme="dark"] .dashboard-card-done .dashboard-card-count { color: var(--status-neutral-fg-2); }
|
||||
:root[data-theme="dark"] .dashboard-card-done { border-left-color: var(--status-neutral-fg-2); }
|
||||
|
||||
/* Dashboard urgency chips */
|
||||
:root[data-theme="dark"] .dashboard-urgency-overdue { background: var(--status-red-bg); color: var(--status-red-fg); }
|
||||
:root[data-theme="dark"] .dashboard-urgency-today { background: var(--status-amber-bg); color: var(--status-amber-fg); }
|
||||
:root[data-theme="dark"] .dashboard-urgency-urgent { background: var(--status-amber-bg); color: var(--status-amber-fg-2); }
|
||||
:root[data-theme="dark"] .dashboard-urgency-soon { background: var(--status-green-soft-bg); color: var(--status-green-soft-fg); }
|
||||
|
||||
/* Dashboard termin dots */
|
||||
:root[data-theme="dark"] .dashboard-termin-hearing { background: var(--status-red-fg); }
|
||||
:root[data-theme="dark"] .dashboard-termin-meeting { background: var(--status-blue-fg-2); }
|
||||
:root[data-theme="dark"] .dashboard-termin-deadline_hearing { background: var(--status-amber-fg-2); }
|
||||
|
||||
/* Termin type colour-codes (card border-left + dot) */
|
||||
:root[data-theme="dark"] .termin-type-hearing { background: var(--status-red-fg); color: var(--status-red-fg); }
|
||||
:root[data-theme="dark"] .termin-type-meeting { background: var(--status-blue-fg-2); color: var(--status-blue-fg-2); }
|
||||
:root[data-theme="dark"] .termin-type-deadline_hearing { background: var(--status-amber-fg-2); color: var(--status-amber-fg-2); }
|
||||
:root[data-theme="dark"] .termin-type-default { background: var(--status-neutral-fg-2); color: var(--status-neutral-fg-2); }
|
||||
|
||||
/* Termin pill badges */
|
||||
:root[data-theme="dark"] .termin-type-badge.termin-type-hearing { background: var(--status-red-bg); color: var(--status-red-fg); }
|
||||
:root[data-theme="dark"] .termin-type-badge.termin-type-meeting { background: var(--status-blue-bg); color: var(--status-blue-fg-2); }
|
||||
:root[data-theme="dark"] .termin-type-badge.termin-type-deadline_hearing { background: var(--status-amber-bg); color: var(--status-amber-fg); }
|
||||
|
||||
/* Termin card border-left colour cues */
|
||||
:root[data-theme="dark"] .termin-card-today { border-left-color: var(--status-amber-fg-2); color: var(--status-amber-fg-2); }
|
||||
:root[data-theme="dark"] .termin-card-week { border-left-color: var(--status-blue-fg-2); color: var(--status-blue-fg-2); }
|
||||
:root[data-theme="dark"] .termin-card-later { border-left-color: var(--status-neutral-fg-2); color: var(--status-neutral-fg-2); }
|
||||
:root[data-theme="dark"] .termin-card-today .frist-summary-dot { background: var(--status-amber-fg-2); }
|
||||
:root[data-theme="dark"] .termin-card-week .frist-summary-dot { background: var(--status-blue-fg-2); }
|
||||
:root[data-theme="dark"] .termin-card-later .frist-summary-dot { background: var(--status-neutral-fg-2); }
|
||||
|
||||
/* Frist due-date chips */
|
||||
:root[data-theme="dark"] .frist-due-chip.frist-urgency-overdue { background: var(--status-red-bg); color: var(--status-red-fg); }
|
||||
:root[data-theme="dark"] .frist-due-chip.frist-urgency-soon { background: var(--status-amber-bg); color: var(--status-amber-fg); }
|
||||
:root[data-theme="dark"] .frist-due-chip.frist-urgency-later { background: var(--status-green-bg); color: var(--status-green-fg); }
|
||||
:root[data-theme="dark"] .frist-due-chip.frist-urgency-done { background: var(--status-neutral-bg); color: var(--status-neutral-fg); }
|
||||
|
||||
/* Akten-status chips (pending / cancelled / waived) */
|
||||
:root[data-theme="dark"] .akten-status-chip.akten-status-pending { background: var(--status-amber-bg); color: var(--status-amber-fg); }
|
||||
:root[data-theme="dark"] .akten-status-chip.akten-status-cancelled { background: var(--status-neutral-bg); color: var(--status-neutral-fg); }
|
||||
:root[data-theme="dark"] .akten-status-chip.akten-status-waived { background: var(--status-neutral-bg); color: var(--status-neutral-fg); }
|
||||
|
||||
/* Agenda urgency markers */
|
||||
:root[data-theme="dark"] .agenda-item-overdue .agenda-item-urgency,
|
||||
:root[data-theme="dark"] .agenda-item-overdue .agenda-item-icon { background: var(--status-red-bg); color: var(--status-red-fg); }
|
||||
:root[data-theme="dark"] .agenda-item-today .agenda-item-urgency,
|
||||
:root[data-theme="dark"] .agenda-item-today .agenda-item-icon { background: var(--status-red-bg); color: var(--status-red-fg); }
|
||||
:root[data-theme="dark"] .agenda-item-tomorrow .agenda-item-urgency,
|
||||
:root[data-theme="dark"] .agenda-item-tomorrow .agenda-item-icon { background: var(--status-amber-bg); color: var(--status-amber-fg); }
|
||||
:root[data-theme="dark"] .agenda-item-this_week .agenda-item-urgency,
|
||||
:root[data-theme="dark"] .agenda-item-this_week .agenda-item-icon { background: var(--status-amber-bg); color: var(--status-amber-fg-2); }
|
||||
:root[data-theme="dark"] .agenda-item-later .agenda-item-urgency,
|
||||
:root[data-theme="dark"] .agenda-item-later .agenda-item-icon { background: var(--status-green-soft-bg); color: var(--status-green-soft-fg); }
|
||||
|
||||
/* Tab-style selection borders (gebuehren-tab, akten-tab, etc.) read on
|
||||
the lime accent in both themes — no override needed. */
|
||||
|
||||
/* Code blocks / soft alt-row backgrounds — these specific Tailwind-grey
|
||||
shades land too washed-out on midnight; pin them to surface-2 instead. */
|
||||
:root[data-theme="dark"] .akten-table thead th,
|
||||
:root[data-theme="dark"] .gebuehren-table th {
|
||||
background: var(--color-bg-subtle);
|
||||
}
|
||||
|
||||
/* Project breadcrumb pill (was background: #eef2ff; color: #4338ca;) */
|
||||
:root[data-theme="dark"] .projekt-breadcrumb-pill,
|
||||
:root[data-theme="dark"] .projekt-tree-pill {
|
||||
background: var(--status-blue-soft-bg);
|
||||
color: var(--status-blue-soft-fg);
|
||||
}
|
||||
|
||||
/* Generic .frist-card hover/alt rows — neutralize the rgba(0,0,0,...)
|
||||
stripes and dividers so the dark surface doesn't get a black wash. */
|
||||
:root[data-theme="dark"] .frist-card:hover,
|
||||
:root[data-theme="dark"] .akten-table tbody tr:hover {
|
||||
background: var(--color-bg-lime-tint);
|
||||
}
|
||||
|
||||
/* Light-mode placeholder scrollbar tinting — already cream-channel alpha
|
||||
on the sidebar (which is midnight in both themes). On the body in dark
|
||||
mode, the browser-default scrollbar is dark via color-scheme, so no
|
||||
per-element override is needed here. */
|
||||
|
||||
|
||||
Reference in New Issue
Block a user