From ee81f25942b4bd9da6adf0e8013029fbac5a7303 Mon Sep 17 00:00:00 2001 From: Sven Carstensen Date: Fri, 28 Aug 2026 14:32:58 +0200 Subject: [PATCH] Umschaltbares Erscheinungsbild: STADTBOTE oder HANSETRANS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nach dem HANSETRANS Brandbook (2025/10): Grün #68B022 als Akzent, HANSETRANS Grau als Wortmarken-Farbe, Überschriften gemischt geschrieben mit negativer Laufweite, Bildmarke (Achteck mit Fahrbahn-Schwung) als SVG angenähert. Umschalter in der Kopfzeile neben dem Verbindungsstatus, per data-brand am Wurzelelement; Wahl samt Favicon und Titel bleibt über Neustarts erhalten. Co-Authored-By: Claude Fable 5 --- frontend/src/brand.ts | 50 +++++++++++ frontend/src/components/HansetransSignet.tsx | 18 ++++ frontend/src/index.css | 91 +++++++++++++++++--- frontend/src/main.tsx | 4 + frontend/src/pages/HomePage.tsx | 29 ++++++- 5 files changed, 179 insertions(+), 13 deletions(-) create mode 100644 frontend/src/brand.ts create mode 100644 frontend/src/components/HansetransSignet.tsx diff --git a/frontend/src/brand.ts b/frontend/src/brand.ts new file mode 100644 index 0000000..029874b --- /dev/null +++ b/frontend/src/brand.ts @@ -0,0 +1,50 @@ +/** + * Umschaltbares Erscheinungsbild (CI/CD): STADTBOTE (Vorgabe) oder HANSETRANS. + * + * Beide Vorgaben stammen aus dem HANSETRANS-Brandbook (Stand 2025/10): gleiche + * Schriftfamilie (Red Hat Display/Text), aber eigene Farben, Wortmarke und + * Bildmarke je Marke. Die Umschaltung läuft über `data-brand` am Wurzelelement – + * die Farb-Token dazu stehen in der index.css. + */ +export type Brand = 'stadtbote' | 'hansetrans' + +const STORAGE_KEY = 'swyxweb.brand' + +export function loadBrand(): Brand { + try { + return localStorage.getItem(STORAGE_KEY) === 'hansetrans' ? 'hansetrans' : 'stadtbote' + } catch { + // Privater Modus o. Ä. – dann eben immer die Vorgabe. + return 'stadtbote' + } +} + +const TITLES: Record = { + stadtbote: 'SwyxWeb · STADTBOTE', + hansetrans: 'SwyxWeb · HANSETRANS', +} + +/** + * Favicons als Daten-URI: das STADTBOTE-Signet (roter Ring mit „S", + * Markenrot #CA0D38) und die HANSETRANS-Bildmarke (grünes Achteck #68B022 + * mit „Fahrbahn"-Schwung), jeweils wie in den Signet-Komponenten. + */ +const FAVICONS: Record = { + stadtbote: + "data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20100%20100'%3E%3Cpath%20d='M%2055%2014.4%20A%2036%2036%200%201%200%2081.8%2033.1'%20fill='none'%20stroke='%23CA0D38'%20stroke-width='13'/%3E%3Ctext%20x='50'%20y='52'%20text-anchor='middle'%20dominant-baseline='central'%20font-family='Arial,Helvetica,sans-serif'%20font-weight='bold'%20font-size='54'%20fill='%23CA0D38'%3ES%3C/text%3E%3C/svg%3E", + hansetrans: + "data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20100%20100'%3E%3Cpath%20fill='%2368B022'%20fill-rule='evenodd'%20d='M29.3%200%20H70.7%20L100%2029.3%20V70.7%20L70.7%20100%20H29.3%20L0%2070.7%20V29.3%20Z%20M4%2052%20C38%2046%2070%2040%2096%2031%20C66%2044%2036%2054%206%2062%20Z%20M10%2076%20C46%2066%2074%2052%2096%2035%20C76%2056%2048%2072%2016%2084%20Z'/%3E%3C/svg%3E", +} + +/** Stellt das Erscheinungsbild um und merkt es sich für den nächsten Start. */ +export function applyBrand(brand: Brand): void { + document.documentElement.dataset.brand = brand + document.title = TITLES[brand] + const icon = document.querySelector("link[rel='icon']") + if (icon) icon.href = FAVICONS[brand] + try { + localStorage.setItem(STORAGE_KEY, brand) + } catch { + // absichtlich still – dann gilt die Wahl nur für diese Sitzung. + } +} diff --git a/frontend/src/components/HansetransSignet.tsx b/frontend/src/components/HansetransSignet.tsx new file mode 100644 index 0000000..d9988ac --- /dev/null +++ b/frontend/src/components/HansetransSignet.tsx @@ -0,0 +1,18 @@ +/** + * Die HANSETRANS-Bildmarke aus dem Brandbook: ein geöffnetes Achteck mit + * „Fahrbahn"-Schwung. Als Inline-SVG angenähert, weil das Brandbook keine + * Vektordaten enthält; die Farbe kommt über currentColor vom Umfeld, der + * Schwung ist ein echtes Loch (evenodd) und zeigt den Seitenhintergrund – + * so, wie das Brandbook die Marke auf hellen und dunklen Flächen zeigt. + */ +export default function HansetransSignet({ className }: { className?: string }) { + return ( + + ) +} diff --git a/frontend/src/index.css b/frontend/src/index.css index 4ad9b7f..fa54a27 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,10 +1,12 @@ /* - * Farb- und Schriftvorgaben aus dem STADTBOTE-Styleguide (HANSETRANS Brandbook): - * Primär Rot #CA0D38 (Pantone 185 C) und Grau #7C7F7E (Pantone 424 C), - * Off-Black #222222 für Fließtext, Grauabstufungen als Flächen. - * Schrift: Red Hat Display (Überschriften, Versalien, Laufweite 4 %) und - * Red Hat Text (Fließtext). Buttons: primär vollflächig, sekundär mit Rahmen, - * Kästen und Linien mit leichter Abrundung. + * Farb- und Schriftvorgaben aus dem HANSETRANS Brandbook. Vorgabe ist das + * STADTBOTE-Erscheinungsbild: Primär Rot #CA0D38 (Pantone 185 C) und Grau + * #7C7F7E (Pantone 424 C), Off-Black #222222 für Fließtext, Grauabstufungen + * als Flächen. Schrift: Red Hat Display (Überschriften, Versalien, Laufweite + * 4 %) und Red Hat Text (Fließtext). Buttons: primär vollflächig, sekundär mit + * Rahmen, Kästen und Linien mit leichter Abrundung. + * Das HANSETRANS-Erscheinungsbild wird weiter unten über data-brand + * am Wurzelelement zugeschaltet. */ :root { --bg: #ffffff; @@ -20,6 +22,9 @@ --brand-strong: #a30a2d; --brand-soft: rgb(202 13 56 / 8%); --accent: #ca0d38; + /* Kopfzeile: Bild- und Wortmarke; bei STADTBOTE beide im Markenrot. */ + --logo-mark: #ca0d38; + --logo-word: #ca0d38; --ok: #2f9e6e; --warn: #b98a1c; --err: #d92b1f; @@ -43,6 +48,32 @@ } } +/* + * HANSETRANS-Erscheinungsbild (HANSETRANS Brandbook 2025/10, Styleguide): + * HANSETRANS Grün #68B022 (Pantone 361 C) als zentrale Akzentfarbe, + * Dunkelgrün #3D821C, HANSETRANS Grau #4B4A4D als Farbe der Wortmarke, + * Off-Black #222222 für Fließtext, Grauabstufungen #B4B4B5/#DCDCDD/#F6F6F6. + * Umgeschaltet über data-brand am Wurzelelement (Bereich „Verbindung"). + */ +:root[data-brand='hansetrans'] { + --brand: #68b022; + --brand-strong: #3d821c; + --brand-soft: rgb(104 176 34 / 10%); + --accent: #68b022; + --text-muted: #4b4a4d; + --logo-mark: #68b022; + --logo-word: #4b4a4d; +} + +@media (prefers-color-scheme: dark) { + :root[data-brand='hansetrans'] { + --brand-soft: rgb(104 176 34 / 16%); + --text-muted: #a4a7a8; + /* Auf dunklen Flächen bleibt das Achteck grün, der Schriftzug wird weiß. */ + --logo-word: #f1f1f2; + } +} + * { box-sizing: border-box; } @@ -59,8 +90,8 @@ body { h1, h2, h3 { - /* Überschriften laut Styleguide: Red Hat Display Semibold in Versalien, - Laufweite 4 %, Zeilenabstand 100 %. */ + /* Überschriften laut STADTBOTE-Styleguide: Red Hat Display Semibold in + Versalien, Laufweite 4 %, Zeilenabstand 100 %. */ font-family: 'Red Hat Display', 'Red Hat Text', system-ui, sans-serif; font-weight: 600; text-transform: uppercase; @@ -68,6 +99,15 @@ h3 { line-height: 1; } +/* HANSETRANS setzt Überschriften gemischt geschrieben mit leicht negativer + Laufweite (Red Hat Display Semibold, Laufweite −20). */ +:root[data-brand='hansetrans'] h1, +:root[data-brand='hansetrans'] h2, +:root[data-brand='hansetrans'] h3 { + text-transform: none; + letter-spacing: -0.02em; +} + code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.9em; @@ -99,11 +139,35 @@ code { background: var(--brand); } +.page__header-tools { + display: flex; + align-items: center; + gap: 10px; +} + +/* Marken-Umschalter im Badge-Stil, damit er neben dem Status nicht aus der + Reihe fällt. Die Wahl gilt sofort und bleibt über Neustarts erhalten. */ +.brand-switch { + appearance: none; + padding: 6px 12px; + border-radius: 999px; + border: 1px solid var(--border); + background: var(--surface); + color: var(--text); + font: inherit; + font-size: 13px; + cursor: pointer; +} + +.brand-switch:hover { + border-color: var(--border-strong); +} + .brand { display: flex; align-items: center; gap: 14px; - color: var(--accent); + color: var(--logo-mark); } .brand__signet { @@ -117,7 +181,14 @@ code { font-weight: 700; font-size: 24px; letter-spacing: 0.06em; - color: var(--accent); + color: var(--logo-word); +} + +/* Die HANSETRANS-Wortmarke steht kursiv („Brandon Grotesque Bold Italic" – + hier mit der Hausschrift angenähert) in HANSETRANS Grau bzw. Weiß. */ +:root[data-brand='hansetrans'] .brand__name { + font-style: italic; + letter-spacing: 0.02em; } .page__subtitle { diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 683d930..b8ba35a 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -1,8 +1,12 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import App from './App.tsx' +import { applyBrand, loadBrand } from './brand' import './index.css' +// Vor dem ersten Rendern, damit die Seite nicht kurz in der falschen Marke aufblitzt. +applyBrand(loadBrand()) + const container = document.getElementById('root') if (!container) { throw new Error('Root-Element #root nicht gefunden') diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx index b5f5d4d..d3441dd 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/HomePage.tsx @@ -4,8 +4,10 @@ import { useSwyxTray } from '../hooks/useSwyxTray' import { useCallNotifications } from '../hooks/useCallNotifications' import { useWebhook } from '../hooks/useWebhook' import { countUnseen } from '../webhook' +import { applyBrand, loadBrand, type Brand } from '../brand' import StatusBadge from '../components/StatusBadge' import StadtboteSignet from '../components/StadtboteSignet' +import HansetransSignet from '../components/HansetransSignet' import MessageLog from '../components/MessageLog' import IncomingCallCard from '../components/IncomingCallCard' import DialPanel from '../components/DialPanel' @@ -38,6 +40,9 @@ export default function HomePage() { const [url, setUrl] = useState(DEFAULT_WS_URL) const [configNote, setConfigNote] = useState(null) const [tab, setTab] = useState('calls') + // Erscheinungsbild (CI/CD): STADTBOTE oder HANSETRANS, gemerkt über Neustarts. + const [brand, setBrand] = useState(loadBrand) + useEffect(() => applyBrand(brand), [brand]) const tray = useSwyxTray() const { status, error, log, history, calls, ringingCall, busy, tray: appState } = tray @@ -123,13 +128,31 @@ export default function HomePage() {
- + {brand === 'hansetrans' ? ( + + ) : ( + + )}
-

Stadtbote

+

{brand === 'hansetrans' ? 'HANSETRANS' : 'Stadtbote'}

SwyxWeb

- +
+ {/* Erscheinungsbild (CI/CD) umschalten – im Badge-Stil, damit es + neben dem Verbindungsstatus nicht aus der Reihe fällt. */} + + +