From f0e63de09b6ce3b8216779e658c3080cb5f4ce56 Mon Sep 17 00:00:00 2001 From: Bernd Rodler Date: Mon, 3 Aug 2026 19:13:07 +0200 Subject: [PATCH] =?UTF-8?q?feat(theme):=20SRC=20theme=20v1.1.0=20=E2=80=94?= =?UTF-8?q?=20MD3=20components=20(shape=20scale,=20buttons,=20cards,=20dia?= =?UTF-8?q?logs,=20state=20layers)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/builtin-themes.ts | 230 ++++++++++++++++++++++++++++++++++- lib/stalwart/auth-context.ts | 5 +- vnc/VNC-CHANGES.md | 3 + 3 files changed, 234 insertions(+), 4 deletions(-) diff --git a/lib/builtin-themes.ts b/lib/builtin-themes.ts index 1c9f380f..fd8a70e2 100644 --- a/lib/builtin-themes.ts +++ b/lib/builtin-themes.ts @@ -1014,6 +1014,11 @@ body[data-theme-skin="builtin-vnclagoon"] .rounded-2xl.border.bg-background\\/80 // near-black stone-grey neutral. Dark variant brightens the red (#EF4444) on a // warm near-black. Info stays blue so it never collides with the red accent. const srcCSS = ` +@font-face { font-family: 'DM Sans'; font-style: normal; font-weight: 400; font-display: swap; src: url('/fonts/dmsans-400.woff2') format('woff2'); } +@font-face { font-family: 'DM Sans'; font-style: normal; font-weight: 500; font-display: swap; src: url('/fonts/dmsans-500.woff2') format('woff2'); } +@font-face { font-family: 'DM Sans'; font-style: normal; font-weight: 700; font-display: swap; src: url('/fonts/dmsans-700.woff2') format('woff2'); } +@font-face { font-family: 'Syne'; font-style: normal; font-weight: 700; font-display: swap; src: url('/fonts/syne-700.woff2') format('woff2'); } +@font-face { font-family: 'Syne'; font-style: normal; font-weight: 800; font-display: swap; src: url('/fonts/syne-800.woff2') format('woff2'); } :root { --color-border: #e7e5e4; --color-input: #e7e5e4; @@ -1095,6 +1100,225 @@ const srcCSS = ` --color-chart-5: #a78bfa; }`; +// MD3 component overrides for the SRC theme — shape scale, filled buttons, +// text fields, cards, dialogs, state layers, switches, login card treatment. +// All scoped under the skin body attribute so they detach cleanly on switch-off. +const srcSkin = ` +body[data-theme-skin="builtin-src"] { + font-family: "DM Sans", system-ui, -apple-system, "Segoe UI", sans-serif; +} +body[data-theme-skin="builtin-src"] h1, +body[data-theme-skin="builtin-src"] h2, +body[data-theme-skin="builtin-src"] h3 { + font-family: "Syne", "DM Sans", sans-serif; + font-weight: 700; + letter-spacing: -0.01em; +} + +/* ── MD3 Shape scale ─────────────────────────────────────────── */ +/* Remap Tailwind rounded-* to M3 shape tokens. rounded-full (pill */ +/* avatars, badges, toggles) is intentionally left untouched. */ +body[data-theme-skin="builtin-src"] .rounded-sm { border-radius: 4px !important; } +body[data-theme-skin="builtin-src"] .rounded { border-radius: 4px !important; } +body[data-theme-skin="builtin-src"] .rounded-md { border-radius: 8px !important; } +body[data-theme-skin="builtin-src"] .rounded-lg { border-radius: 12px !important; } +body[data-theme-skin="builtin-src"] .rounded-xl { border-radius: 16px !important; } +body[data-theme-skin="builtin-src"] .rounded-2xl { border-radius: 28px !important; } +body[data-theme-skin="builtin-src"] .rounded-3xl { border-radius: 28px !important; } + +/* ── MD3 Buttons: full shape (20 dp) ─────────────────────────── */ +/* All button variants (filled, tonal, outlined, text) use 20 dp */ +/* corners per M3. Circle icon buttons (.rounded-full) are skipped; */ +/* switches ([role="switch"]) are handled separately. */ +body[data-theme-skin="builtin-src"] button:not(.rounded-full):not([role="switch"]) { + border-radius: 20px !important; +} + +/* MD3 filled button — primary surface, M3 label-large, state layers */ +body[data-theme-skin="builtin-src"] .bg-primary.text-primary-foreground { + border-radius: 20px !important; + padding-inline: 24px !important; + min-height: 40px !important; + font-weight: 500 !important; + letter-spacing: 0.0063em !important; + border: none !important; + box-shadow: none !important; + transition: box-shadow 200ms ease, filter 200ms ease; +} +/* hover: M3 elevation 1 + 8 % on-primary state layer */ +body[data-theme-skin="builtin-src"] .bg-primary.text-primary-foreground:hover { + box-shadow: + 0 1px 2px rgba(0, 0, 0, 0.30), + 0 2px 6px 2px rgba(0, 0, 0, 0.15) !important; + filter: brightness(1.06); +} +/* focus: +12 % tint + M3 focus ring */ +body[data-theme-skin="builtin-src"] .bg-primary.text-primary-foreground:focus-visible { + filter: brightness(1.10) !important; + outline: 3px solid var(--color-ring) !important; + outline-offset: 2px !important; +} +/* pressed: +12 % darker, no shadow */ +body[data-theme-skin="builtin-src"] .bg-primary.text-primary-foreground:active { + box-shadow: none !important; + filter: brightness(0.94) !important; +} + +/* ── MD3 Text fields: outlined style, extra-small (4 dp) ─────── */ +body[data-theme-skin="builtin-src"] input:not([type="checkbox"]):not([type="radio"]):not([type="range"]), +body[data-theme-skin="builtin-src"] textarea, +body[data-theme-skin="builtin-src"] select { + border-radius: 4px !important; + transition: outline 150ms ease; +} +body[data-theme-skin="builtin-src"] input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):focus-visible, +body[data-theme-skin="builtin-src"] textarea:focus-visible, +body[data-theme-skin="builtin-src"] select:focus-visible { + outline: 2px solid var(--color-primary) !important; + outline-offset: -2px !important; +} + +/* ── MD3 Cards: elevated (level 1), medium shape (12 dp) ─────── */ +body[data-theme-skin="builtin-src"] .bg-card { + border-radius: 12px !important; + box-shadow: + 0 1px 2px rgba(0, 0, 0, 0.10), + 0 1px 3px 1px rgba(0, 0, 0, 0.06) !important; +} +.dark body[data-theme-skin="builtin-src"] .bg-card { + box-shadow: + 0 1px 2px rgba(0, 0, 0, 0.35), + 0 1px 3px 1px rgba(0, 0, 0, 0.20) !important; +} + +/* ── MD3 Menus / popovers / listboxes: extra-small (4 dp) ───── */ +body[data-theme-skin="builtin-src"] .bg-popover, +body[data-theme-skin="builtin-src"] [role="listbox"] { + border-radius: 4px !important; + border: none !important; + box-shadow: + 0 2px 6px 2px rgba(0, 0, 0, 0.15), + 0 1px 2px rgba(0, 0, 0, 0.30) !important; +} +.dark body[data-theme-skin="builtin-src"] .bg-popover, +.dark body[data-theme-skin="builtin-src"] [role="listbox"] { + box-shadow: + 0 2px 8px 2px rgba(0, 0, 0, 0.50), + 0 1px 2px rgba(0, 0, 0, 0.60) !important; +} + +/* ── MD3 Dialogs: extra-large shape (28 dp) ──────────────────── */ +body[data-theme-skin="builtin-src"] [role="dialog"] { + border-radius: 28px !important; + border: none !important; + box-shadow: + 0 6px 10px 4px rgba(0, 0, 0, 0.15), + 0 2px 3px rgba(0, 0, 0, 0.30) !important; +} +.dark body[data-theme-skin="builtin-src"] [role="dialog"] { + box-shadow: + 0 6px 10px 4px rgba(0, 0, 0, 0.50), + 0 2px 3px rgba(0, 0, 0, 0.60) !important; +} + +/* ── MD3 Menu items: 8 % on-surface state layer on hover ─────── */ +body[data-theme-skin="builtin-src"] [role="menu"] [role="menuitem"]:hover, +body[data-theme-skin="builtin-src"] [role="menu"] [role="menuitem"]:focus, +body[data-theme-skin="builtin-src"] [role="menu"] [role="menuitem"]:focus-visible { + background-color: color-mix(in srgb, var(--color-foreground) 8%, transparent) !important; + color: var(--color-foreground) !important; +} +.dark body[data-theme-skin="builtin-src"] [role="menu"] [role="menuitem"]:hover, +.dark body[data-theme-skin="builtin-src"] [role="menu"] [role="menuitem"]:focus { + background-color: color-mix(in srgb, var(--color-foreground) 10%, transparent) !important; +} + +/* ── MD3 Switch ──────────────────────────────────────────────── */ +/* M3 switch: unselected = outline + icon; selected = primary fill */ +body[data-theme-skin="builtin-src"] [role="switch"] { + background-color: var(--color-input) !important; + border: 2px solid var(--color-muted-foreground) !important; + transition: background-color 150ms ease, border-color 150ms ease; +} +body[data-theme-skin="builtin-src"] [role="switch"] > span { + background-color: var(--color-muted-foreground) !important; +} +body[data-theme-skin="builtin-src"] [role="switch"][aria-checked="true"] { + background-color: var(--color-primary) !important; + border-color: var(--color-primary) !important; +} +body[data-theme-skin="builtin-src"] [role="switch"][aria-checked="true"] > span { + background-color: var(--color-primary-foreground) !important; +} +.dark body[data-theme-skin="builtin-src"] [role="switch"] { + background-color: #3a2524 !important; + border-color: var(--color-muted-foreground) !important; +} +.dark body[data-theme-skin="builtin-src"] [role="switch"][aria-checked="true"] { + background-color: var(--color-primary) !important; + border-color: var(--color-primary) !important; +} +.dark body[data-theme-skin="builtin-src"] [role="switch"][aria-checked="true"] > span { + background-color: #1c1917 !important; +} + +/* ── Selected folder: M3 active-indicator treatment ─────────── */ +/* M3 uses a pill-shaped tonal container for the active nav item. */ +/* Bulwark's left-border accent becomes a 3 dp primary accent line */ +/* + secondary-container (--color-accent) fill + gentle corner. */ +body[data-theme-skin="builtin-src"] .bg-secondary.border-r .border-l-2.border-primary { + border-left-width: 3px !important; + border-left-color: var(--color-primary) !important; + background-color: var(--color-accent) !important; + border-radius: 0 12px 12px 0 !important; +} + +/* ── Login card: MD3 extra-large + SRC red top accent strip ───── */ +body[data-theme-skin="builtin-src"] .rounded-2xl.border.bg-background\\/80.backdrop-blur-sm { + position: relative; + background-color: var(--color-card) !important; + border-color: rgba(213, 43, 30, 0.14) !important; + border-radius: 28px !important; + box-shadow: + 0 2px 8px rgba(0, 0, 0, 0.08), + 0 8px 32px rgba(0, 0, 0, 0.06) !important; + overflow: hidden; +} +body[data-theme-skin="builtin-src"] .rounded-2xl.border.bg-background\\/80.backdrop-blur-sm::before { + content: ""; + position: absolute; + left: 0; right: 0; top: 0; + height: 3px; + background: linear-gradient(90deg, transparent, #d52b1e 30%, #d52b1e 70%, transparent); + pointer-events: none; +} +/* Login page background: faint SRC red ambient wash */ +body[data-theme-skin="builtin-src"] .min-h-screen.bg-gradient-to-br { + background-color: #f9f7f7 !important; + background-image: radial-gradient(56rem 38rem at 50% -12%, rgba(213, 43, 30, 0.05), transparent 60%) !important; +} +.dark body[data-theme-skin="builtin-src"] .rounded-2xl.border.bg-background\\/80.backdrop-blur-sm { + border-color: rgba(239, 68, 68, 0.18) !important; + box-shadow: + 0 4px 16px rgba(0, 0, 0, 0.45), + 0 0 40px -20px rgba(239, 68, 68, 0.18) !important; +} +.dark body[data-theme-skin="builtin-src"] .rounded-2xl.border.bg-background\\/80.backdrop-blur-sm::before { + background: linear-gradient(90deg, transparent, #ef4444 30%, #ef4444 70%, transparent); +} +.dark body[data-theme-skin="builtin-src"] .min-h-screen.bg-gradient-to-br { + background-color: var(--color-background) !important; + background-image: radial-gradient(56rem 38rem at 50% -12%, rgba(239, 68, 68, 0.07), transparent 60%) !important; +} + +/* ── Single-surface panes (M3 has no gradient empty states) ───── */ +body[data-theme-skin="builtin-src"] .bg-gradient-to-br.from-muted\\/30.to-muted\\/50 { + background: var(--color-background) !important; +} +body[data-theme-skin="builtin-src"] .bg-muted\\/30 { + background-color: var(--color-background) !important; +}`; + export const BUILTIN_THEMES: InstalledTheme[] = [ { id: 'builtin-vnclagoon', @@ -1114,13 +1338,15 @@ export const BUILTIN_THEMES: InstalledTheme[] = [ { id: 'builtin-src', name: 'SRC', - version: '1.0.0', + version: '1.1.0', author: 'VNC', - description: 'SRC Advisory brand theme — Swiss red on white, light-first', + description: 'SRC Advisory brand theme — Swiss red on white, MD3 components, light-first', css: srcCSS, + skin: srcSkin, logoLightUrl: '/branding/src-logo.svg', logoDarkUrl: '/branding/src-logo.svg', variants: ['light', 'dark'], + typography: { fontSans: '"DM Sans", system-ui, -apple-system, "Segoe UI", sans-serif' }, enabled: true, builtIn: true, }, diff --git a/lib/stalwart/auth-context.ts b/lib/stalwart/auth-context.ts index f2879d34..874448f8 100644 --- a/lib/stalwart/auth-context.ts +++ b/lib/stalwart/auth-context.ts @@ -27,9 +27,10 @@ function isValidContext(payload: unknown): payload is StalwartAuthContext { && typeof candidate.authHeader === 'string'; } +const AUTH_CONTEXT_MAX_AGE = 6 * 60 * 60; // 6 hours + function getSessionCookieOptions() { - const { maxAge: _maxAge, ...cookieOptions } = getCookieOptions(); - return cookieOptions; + return { ...getCookieOptions(), maxAge: AUTH_CONTEXT_MAX_AGE }; } export function readStalwartAuthContextFromStore( diff --git a/vnc/VNC-CHANGES.md b/vnc/VNC-CHANGES.md index c394a4bb..d0376d5d 100644 --- a/vnc/VNC-CHANGES.md +++ b/vnc/VNC-CHANGES.md @@ -45,4 +45,7 @@ data dir (`/app/data/*`); Vercel serverless has a read-only filesystem → crash `vncmail.sandbox.vnc.de`, with 4 persistent volumes — see `deploy/k8s/`. A microfrontends integration was also added and reverted the same day._ +| 2026-08-03 | `lib/stalwart/auth-context.ts` | give `jmap_stalwart_ctx` a 6-hour maxAge (was session-cookie → expired on tab close) | session survival across browser restarts | +| 2026-08-03 | `lib/builtin-themes.ts` | add `srcSkin` (MD3 component overrides: shape scale, filled buttons, text fields, cards, dialogs, state layers, switches, login card); add @font-face + typography to `builtin-src`; bump to v1.1.0 | SRC theme: keep colors + fonts, apply MD3 design system | + _(append new rows as you diverge)_