feat: add page shell, preferences and translations

Base layout with top bar, role-based menu, profile menu and phone layout; theme, language and German form of address kept in cookies; German formal, German informal and English text; Fluent avatar bundled in the image by a Node build stage; placeholder pages for the Publisher and Admin groups.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N3t4WL7bd9bdHHNjEsxzqC
This commit is contained in:
2026-10-17 21:05:40 +02:00
co-authored by Claude Sonnet 5.5
parent 3c69c2a156
commit 0dc079a8ac
34 changed files with 2132 additions and 24 deletions
+432
View File
@@ -0,0 +1,432 @@
/* Page shell: design tokens from DESIGN.md, light and dark, wide and phone layout (AD-15). */
:root {
--brand: #0f6cbd;
--on-brand: #fff;
--top-bar: var(--brand);
--on-top-bar: var(--on-brand);
--avatar-bg: #fff;
--avatar-fg: var(--brand);
--bg: #fafafa;
--surface: #fff;
--nav-surface: #f5f5f5;
--text: #242424;
--text-secondary: #616161;
--line: #e0e0e0;
--control-line: #8a8a8a;
--font: "Segoe UI Variable", "Segoe UI", system-ui, sans-serif;
--radius-sm: 4px;
--radius-lg: 8px;
--touch: 44px;
--nav-width: 220px;
--bar-height: 48px;
color-scheme: light;
}
:root[data-theme="dark"] {
--brand: #479ef5;
--on-brand: #0a0a0a;
--top-bar: #0f2a43;
--on-top-bar: #fff;
--avatar-bg: #479ef5;
--avatar-fg: #0a0a0a;
--bg: #1f1f1f;
--surface: #292929;
--nav-surface: #242424;
--text: #fff;
--text-secondary: #c8c8c8;
--line: #3d3d3d;
--control-line: #9a9a9a;
color-scheme: dark;
}
@media (prefers-color-scheme: dark) {
:root[data-theme="system"] {
--brand: #479ef5;
--on-brand: #0a0a0a;
--top-bar: #0f2a43;
--on-top-bar: #fff;
--avatar-bg: #479ef5;
--avatar-fg: #0a0a0a;
--bg: #1f1f1f;
--surface: #292929;
--nav-surface: #242424;
--text: #fff;
--text-secondary: #c8c8c8;
--line: #3d3d3d;
--control-line: #9a9a9a;
color-scheme: dark;
}
}
/* The tokens the Fluent avatar reads, mapped to the portal's own. */
:root {
--colorBrandBackgroundStatic: var(--avatar-bg);
--colorNeutralForegroundStaticInverted: var(--avatar-fg);
--colorNeutralBackground1: var(--surface);
--colorNeutralBackground6: var(--nav-surface);
--colorNeutralForeground3: var(--text-secondary);
--fontFamilyBase: var(--font);
--fontWeightRegular: 400;
--fontWeightSemibold: 600;
--fontSizeBase100: 10px;
--fontSizeBase200: 12px;
--fontSizeBase300: 14px;
--fontSizeBase400: 16px;
--fontSizeBase500: 20px;
--fontSizeBase600: 24px;
--borderRadiusSmall: 2px;
--borderRadiusMedium: 4px;
--borderRadiusLarge: 6px;
--borderRadiusXLarge: 8px;
--borderRadiusCircular: 9999px;
--strokeWidthThin: 1px;
--strokeWidthThick: 2px;
--strokeWidthThicker: 3px;
--strokeWidthThickest: 4px;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font: 400 14px/1.45 var(--font);
background: var(--bg);
color: var(--text);
min-height: 100vh;
display: flex;
flex-direction: column;
}
a {
color: var(--brand);
}
:focus-visible {
outline: 2px solid var(--brand);
outline-offset: 2px;
}
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
.skip-link {
position: absolute;
left: 8px;
top: -100px;
z-index: 20;
padding: 10px 14px;
background: var(--surface);
color: var(--text);
border: 2px solid var(--brand);
border-radius: var(--radius-sm);
}
.skip-link:focus {
top: 8px;
}
/* Top bar */
.top-bar {
position: relative;
z-index: 10;
display: flex;
align-items: center;
gap: 12px;
height: var(--bar-height);
padding: 0 16px;
background: var(--top-bar);
color: var(--on-top-bar);
}
.portal-name {
flex: 1;
color: inherit;
font-weight: 600;
text-decoration: none;
}
.menu-button {
display: none;
width: var(--touch);
height: var(--touch);
margin-left: -10px;
border: 0;
border-radius: var(--radius-sm);
background: transparent;
color: inherit;
cursor: pointer;
place-items: center;
}
.burger,
.burger::before,
.burger::after {
display: block;
width: 18px;
height: 2px;
background: currentcolor;
content: "";
}
.burger {
position: relative;
}
.burger::before {
position: absolute;
top: -6px;
}
.burger::after {
position: absolute;
top: 6px;
}
/* Profile menu */
.profile {
position: relative;
}
.profile summary {
display: grid;
place-items: center;
width: var(--touch);
height: var(--touch);
margin-right: -6px;
list-style: none;
cursor: pointer;
border-radius: 50%;
}
.profile summary::-webkit-details-marker {
display: none;
}
/* The plain initials show until the Fluent avatar is defined. */
.profile fluent-avatar:not(:defined) {
display: none;
}
.profile fluent-avatar:defined + .avatar-fallback {
display: none;
}
.avatar-fallback {
display: grid;
place-items: center;
width: 32px;
height: 32px;
border-radius: 50%;
background: var(--avatar-bg);
color: var(--avatar-fg);
font-size: 12px;
font-weight: 700;
}
.profile-menu {
position: absolute;
right: 0;
top: calc(100% + 2px);
width: 260px;
max-width: calc(100vw - 24px);
padding: 14px;
background: var(--surface);
color: var(--text);
border: 1px solid var(--line);
border-radius: var(--radius-lg);
box-shadow: 0 6px 20px rgb(0 0 0 / 25%);
}
.profile-name {
margin: 0;
font-weight: 600;
overflow-wrap: anywhere;
}
.profile-user {
margin: 0 0 8px;
font-size: 13px;
color: var(--text-secondary);
overflow-wrap: anywhere;
}
.prefs fieldset {
margin: 0;
padding: 8px 0 0;
border: 0;
border-top: 1px solid var(--line);
}
.prefs legend {
padding: 0;
font-size: 13px;
color: var(--text-secondary);
}
.choices {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin: 6px 0 8px;
}
.profile-menu button {
min-height: var(--touch);
padding: 0 14px;
border: 1px solid var(--control-line);
border-radius: var(--radius-sm);
background: transparent;
color: var(--text);
font: inherit;
cursor: pointer;
}
.profile-menu button[aria-pressed="true"] {
background: var(--brand);
border-color: var(--brand);
color: var(--on-brand);
font-weight: 600;
}
.sign-out {
margin-top: 4px;
padding-top: 10px;
border-top: 1px solid var(--line);
}
/* Layout */
.layout {
display: flex;
flex: 1;
min-height: 0;
}
#menu {
width: var(--nav-width);
flex: none;
padding: 12px 8px;
background: var(--nav-surface);
border-right: 1px solid var(--line);
}
.menu-heading {
margin: 14px 12px 4px;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--text-secondary);
}
#menu a {
display: flex;
align-items: center;
min-height: 38px;
padding: 0 12px;
margin-bottom: 2px;
border-radius: var(--radius-sm);
color: var(--text);
text-decoration: none;
}
#menu a:hover {
background: var(--surface);
}
#menu a.current {
background: var(--surface);
font-weight: 600;
box-shadow: inset 3px 0 0 var(--brand);
}
main {
flex: 1;
min-width: 0;
padding: 24px 28px;
}
main:focus {
outline: none;
}
h1 {
margin: 0 0 4px;
font-size: 24px;
font-weight: 600;
line-height: 1.25;
}
.lead {
max-width: 640px;
margin: 0 0 18px;
color: var(--text-secondary);
}
main p,
main li {
max-width: 640px;
}
main button {
min-height: var(--touch);
padding: 0 16px;
border: 1px solid var(--control-line);
border-radius: var(--radius-sm);
background: transparent;
color: var(--text);
font: inherit;
cursor: pointer;
}
/* Phone: content only, the menu opens from the menu button. */
@media (width <= 720px) {
main {
padding: 16px;
}
h1 {
font-size: 20px;
}
.layout {
flex-direction: column;
}
#menu {
width: auto;
border-right: 0;
border-bottom: 1px solid var(--line);
}
#menu a {
min-height: var(--touch);
}
/* Without JavaScript the menu stays visible above the content. */
.js .menu-button {
display: grid;
}
.js #menu {
display: none;
}
.js.menu-open #menu {
display: block;
}
}
+73
View File
@@ -0,0 +1,73 @@
// Page shell behaviour: menu button, profile menu and preferences. Everything also works
// without this script (links, a plain form), so it only makes the page quicker.
(function () {
"use strict";
var root = document.documentElement;
root.classList.add("js");
var YEAR = 365 * 24 * 60 * 60;
var COOKIES = { theme: "pm_theme", language: "pm_lang", form: "pm_form" };
var ALLOWED = {
theme: ["light", "dark", "system"],
language: ["de", "en"],
form: ["sie", "du"],
};
function writeCookie(field, value) {
var secure = location.protocol === "https:" ? "; Secure" : "";
document.cookie =
COOKIES[field] + "=" + value + "; Max-Age=" + YEAR + "; Path=/; SameSite=Lax" + secure;
}
var profile = document.querySelector(".profile");
var menuButton = document.querySelector(".menu-button");
document.addEventListener("click", function (event) {
var button = event.target.closest("button[data-pref]");
if (button) {
var field = button.getAttribute("data-pref");
var value = button.value;
if (ALLOWED[field] && ALLOWED[field].indexOf(value) !== -1) {
event.preventDefault();
writeCookie(field, value);
if (field === "theme") {
root.setAttribute("data-theme", value);
button
.closest(".choices")
.querySelectorAll("button")
.forEach(function (other) {
other.setAttribute("aria-pressed", other === button ? "true" : "false");
});
} else {
location.reload(); // language and form change the text, which the server renders
}
}
return;
}
if (profile && profile.open && !profile.contains(event.target)) {
profile.open = false;
}
});
if (menuButton) {
menuButton.addEventListener("click", function () {
var open = root.classList.toggle("menu-open");
menuButton.setAttribute("aria-expanded", open ? "true" : "false");
});
}
document.addEventListener("keydown", function (event) {
if (event.key !== "Escape") {
return;
}
if (profile && profile.open) {
profile.open = false;
profile.querySelector("summary").focus();
} else if (menuButton && root.classList.contains("menu-open")) {
root.classList.remove("menu-open");
menuButton.setAttribute("aria-expanded", "false");
menuButton.focus();
}
});
})();