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:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
})();
|
||||
Reference in New Issue
Block a user