bifroest/app/static/css-framework/README.md
2026-10-27 08:34:17 +01:00

1134 lines
34 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# CSS-Framework — CSS Framework für Jinja2 / HTMX Webapps
Ein schlankes CSS-Framework für konsistente, moderne Webapplikationen. Classless wo möglich, opt-in Klassen wo nötig. Gebaut für Jinja2-Templates und HTMX-Interaktionen.
---
## Dateien & Struktur
```
framework/
├── tokens.css — Design Tokens (CSS Custom Properties)
├── base.css — Classless Reset + semantische HTML-Elemente
├── components.css — Opt-in Komponenten-Klassen
├── utilities.css — Minimale Helfer-Klassen
├── main.css — Entry Point (importiert alle vier)
└── fonts.css — Template für lokale Google Fonts (optional)
```
**Einbinden in Projekten:**
```html
<link rel="stylesheet" href="/static/css/main.css">
```
---
## Design Tokens (`tokens.css`)
Alle Designentscheidungen sind als CSS Custom Properties definiert. **Zentrale Anlaufstelle für alle Anpassungen.**
### Farbsystem
Zweischichtig aufgebaut: Rohe Palette + Semantische Aliases.
```css
/* Nie direkt verwenden — nur als Rohwerte */
--color-accent-500: #6366f1;
/* Diese verwenden — semantische Bedeutung */
--accent: var(--color-accent-500);
--accent-hover: var(--color-accent-600);
--accent-subtle: var(--color-accent-50);
```
**Primärfarbe anpassen:** Nur `--color-accent-*` in `tokens.css` ändern — alle Komponenten folgen automatisch.
| Token | Verwendung |
|---|---|
| `--bg`, `--bg-subtle`, `--bg-muted` | Seitenhintergründe |
| `--surface`, `--surface-raised`, `--surface-overlay` | Karten, Modals, Dropdowns |
| `--border`, `--border-strong`, `--border-focus` | Rahmenlinien |
| `--text`, `--text-muted`, `--text-subtle` | Texthierarchie |
| `--accent`, `--accent-hover`, `--accent-active` | Primärfarbe + Zustände |
| `--color-success/warning/danger/info` | Semantische Status-Farben |
### Dark Mode
Nur semantische Aliases werden überschrieben — Rohpalette bleibt unverändert.
```html
<html data-theme="light"> <!-- oder "dark" -->
```
```css
[data-theme="dark"] {
--bg: var(--color-black);
--surface: #161920;
--text: var(--color-gray-100);
/* ... alle Aliases werden überschrieben -->
}
```
Umschalten und Speichern übernimmt `theme.js` (siehe „JavaScript API → Theme“), ohne Inline-Skript.
### Spacing
4px-Grid, 12 Stufen: `--space-1` (4px) bis `--space-24` (96px).
```css
--space-1: 0.25rem; /* 4px */
--space-2: 0.5rem; /* 8px */
--space-4: 1rem; /* 16px */
--space-6: 1.5rem; /* 24px */
--space-8: 2rem; /* 32px */
```
### Typografie
```css
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
--font-mono: "JetBrains Mono", ui-monospace, monospace;
--text-xs: 0.75rem; /* 12px */
--text-sm: 0.875rem; /* 14px */
--text-base: 1rem; /* 16px */
--text-lg: 1.25rem; /* 20px */
--text-xl: 1.5rem; /* 24px */
--text-2xl: 1.875rem; /* 30px */
--text-3xl: 2.25rem; /* 36px */
--text-4xl: 3rem; /* 48px */
```
### Weitere Token-Gruppen
| Gruppe | Tokens |
|---|---|
| Border Radius | `--radius-sm/md/lg/xl/2xl/full` |
| Schatten | `--shadow-xs/sm/md/lg/xl`, `--shadow-focus` |
| Übergänge | `--duration-fast/base/slow`, `--ease/ease-out/ease-in`, `--transition` |
| Layout | `--container-sm/md/lg/xl/2xl`, `--nav-height`, `--sidebar-width` |
| Z-Index | `--z-base/raised/dropdown/sticky/overlay/modal/toast` |
---
## Classless Base (`base.css`)
Semantisches HTML benötigt **keine Klassen** für grundlegendes Styling. Einfach valides HTML schreiben:
```html
<!-- Alles automatisch gestylt: -->
<h1>Überschrift</h1>
<p>Text mit <strong>fett</strong>, <em>kursiv</em>, <a href="#">Link</a>, <code>Code</code>.</p>
<blockquote>Zitat<cite>— Autor</cite></blockquote>
<table>...</table>
<input type="text" placeholder="...">
<select>...</select>
<details><summary>Akkordeon</summary>Inhalt</details>
```
**Gestylt ohne Klassen:** `h1–h6`, `p`, `a`, `strong`, `em`, `small`, `mark`, `kbd`, `code`, `pre`, `blockquote`, `hr`, `ul`, `ol`, `dl`, `table`, `thead`, `tbody`, `tfoot`, `th`, `td`, `input`, `textarea`, `select`, `fieldset`, `legend`, `label`, `details`, `summary`, `figure`, `figcaption`, `img`.
Checkboxen und Radios behalten die native Darstellung des Browsers und werden über `accent-color` in `--accent` eingefärbt (hell wie dunkel).
---
## Komponenten (`components.css`)
Opt-in Klassen für alles was semantisches HTML nicht abdeckt.
### Layout
```html
<!-- Container -->
<div class="container"> <!-- max-width: 1280px, zentriert -->
<div class="container container-md"> <!-- max-width: 768px -->
<!-- Vertikaler Stack -->
<div class="stack"> <!-- flex-direction: column, gap: 1rem -->
<div class="stack stack-sm"> <!-- gap: 0.5rem -->
<div class="stack stack-lg"> <!-- gap: 2rem -->
<!-- Horizontaler Cluster (umbrechend) -->
<div class="cluster"> <!-- flex-wrap, gap: 0.75rem, align-items: center -->
<!-- Grid -->
<div class="grid"> <!-- auto-fill, min 20rem pro Spalte -->
<div class="grid grid-2"> <!-- 2 Spalten -->
<div class="grid grid-3"> <!-- 3 Spalten -->
<div class="grid grid-4"> <!-- 4 Spalten -->
<!-- Sidebar-Layout -->
<div class="with-sidebar"> <!-- sidebar-width + 1fr -->
```
### Navigation
#### Breadcrumbs (`.breadcrumbs`)
```html
<nav class="breadcrumbs" aria-label="Breadcrumb">
<ol>
<li><a href="/dashboard">Dashboard</a></li>
<li><a href="/projekte">Projekte</a></li>
<li><a href="/projekte/acme">Acme Redesign</a></li>
<li><span aria-current="page">Einstellungen</span></li>
</ol>
</nav>
```
Semantischer Navigationspfad für hierarchische Seiten. Nutzt vorhandene Text-, Radius- und Surface-Tokens, trennt Einträge per Slash und bleibt auf schmalen Viewports horizontal scrollbar.
#### Top Navigation (`.topnav`)
```html
<nav class="topnav">
<div class="topnav-inner">
<a class="topnav-brand" href="/">Brand</a>
<ul class="topnav-links">
<li><a href="/" class="active">Dashboard</a></li>
<li>
<button data-action="nav-dropdown">
Mehr <svg class="nav-chevron">...</svg>
</button>
<div class="topnav-dropdown">
<a href="/x">Link</a>
</div>
</li>
</ul>
<div class="topnav-end">
<!-- Buttons, User-Dropdown, Theme-Toggle -->
</div>
<button class="topnav-burger" ...>☰</button>
</div>
<div class="topnav-mobile">...</div>
</nav>
```
**Modifier:** `.topnav-static` (nicht sticky)
#### Sidebar (`.sidebar`)
```html
<aside class="sidebar">
<div class="sidebar-header">...</div>
<nav class="sidebar-nav">
<div class="sidebar-section">Abschnitt</div>
<a href="/" class="active">
<svg>...</svg>
Dashboard
<span class="nav-count">4</span>
</a>
<details class="sidebar-group">
<summary><svg>...</svg> Gruppe</summary>
<div class="sidebar-group-items">
<a href="/x">Unterlink</a>
</div>
</details>
</nav>
<div class="sidebar-footer">
<a href="/profil" class="sidebar-user">
<div class="avatar avatar-sm">AM</div>
<div class="sidebar-user-info">
<div class="sidebar-user-name">Anna Müller</div>
<div class="sidebar-user-role">Admin</div>
</div>
</a>
</div>
</aside>
```
**Mobile Sidebar-Overlay (< 64rem):**
Bei schmalen Viewports bricht `.with-sidebar` das Grid um. Die Sidebar wird dabei per JavaScript als festes Overlay eingeblendet. Benötigte HTML-Elemente:
```html
<!-- Direkt vor .with-sidebar -->
<div class="mobile-topbar">
<button id="sidebar-toggle" aria-label="Menü öffnen">☰</button>
<span>Meine App</span>
</div>
<div id="sidebar-backdrop" class="sidebar-backdrop"></div>
<div class="with-sidebar">
<aside class="sidebar">...</aside>
<main>...</main>
</div>
```
JavaScript (im Basis-Template eintragen):
```js
document.getElementById('sidebar-toggle')
.addEventListener('click', () => document.body.classList.add('sidebar-open'));
document.getElementById('sidebar-backdrop')
.addEventListener('click', () => document.body.classList.remove('sidebar-open'));
```
#### Footer (`.footer`)
```html
<!-- Einfach (eine Zeile) -->
<footer class="footer">
<div class="footer-inner">
<a class="footer-brand" href="/">Brand</a>
<ul class="footer-links">
<li><a href="/impressum">Impressum</a></li>
</ul>
<p class="footer-copy">© 2025</p>
</div>
</footer>
<!-- Mehrspaltig -->
<footer class="footer footer-full">
<div class="footer-inner">
<div class="footer-col">
<p class="footer-col-title">Produkt</p>
<a href="/features">Features</a>
</div>
...
</div>
<div class="footer-bottom">
<span>© 2025 Acme GmbH</span>
<ul class="footer-social">
<li><a href="https://github.com/..."><svg>...</svg></a></li>
</ul>
</div>
</footer>
```
### Buttons (`.btn`)
```html
<!-- Varianten -->
<button class="btn btn-primary">Primary</button>
<button class="btn btn-secondary">Secondary</button>
<button class="btn btn-ghost">Ghost</button>
<button class="btn btn-danger">Danger</button>
<button class="btn btn-success">Success</button>
<!-- Größen -->
<button class="btn btn-primary btn-xs">XSmall</button>
<button class="btn btn-primary btn-sm">Small</button>
<button class="btn btn-primary btn-lg">Large</button>
<button class="btn btn-primary btn-xl">XLarge</button>
<!-- Modifiers -->
<button class="btn btn-primary btn-full">Volle Breite</button>
<button class="btn btn-ghost btn-icon"><svg>...</svg></button>
<!-- Als Link -->
<a href="/ziel" class="btn btn-primary">Link-Button</a>
<!-- Disabled -->
<button class="btn btn-primary" disabled>Gesperrt</button>
<!-- Verbundene Button-Leiste -->
<div class="button-group" role="group" aria-label="Ansicht wechseln">
<button class="btn btn-secondary">Liste</button>
<button class="btn btn-secondary">Board</button>
<button class="btn btn-secondary">Kalender</button>
</div>
<!-- Segmented Control auf Radio-Basis -->
<div class="segmented" role="radiogroup" aria-label="Zeitraum wählen">
<label class="segmented-option" for="range-day">
<input class="segmented-input" type="radio" name="range" id="range-day">
<span class="segmented-button">Tag</span>
</label>
<label class="segmented-option" for="range-week">
<input class="segmented-input" type="radio" name="range" id="range-week" checked>
<span class="segmented-button">Woche</span>
</label>
<label class="segmented-option" for="range-month">
<input class="segmented-input" type="radio" name="range" id="range-month">
<span class="segmented-button">Monat</span>
</label>
</div>
```
Die Button Group fasst normale .btn-Elemente bündig zusammen. Die Segmented Control bleibt technisch eine Gruppe nativer Radio-Inputs und eignet sich für Filter, Ansichten und Moduswechsel ohne JavaScript.
### Badges (`.badge`)
```html
<span class="badge badge-default">Standard</span>
<span class="badge badge-primary">Primär</span>
<span class="badge badge-success">Erfolg</span>
<span class="badge badge-warning">Warnung</span>
<span class="badge badge-danger">Fehler</span>
<span class="badge badge-info">Info</span>
<span class="badge badge-outline">Outline</span>
<!-- Mit Dot-Indikator -->
<span class="badge badge-success badge-dot">Live</span>
```
### Cards (`.card`)
```html
<div class="card">
<div class="card-header">
<h3>Titel</h3>
<span class="badge badge-success">Aktiv</span>
</div>
<div class="card-body">
Inhalt
</div>
<div class="card-footer">
<button class="btn btn-primary btn-sm">Aktion</button>
</div>
</div>
```
**Varianten:** `.card-flat`, `.card-raised`, `.card-ghost`
**Hover-Effekt:** `.card.card-hover`
**Image Card:**
```html
<div class="card">
<div class="card-media">
<img src="..." alt="...">
</div>
<div class="card-body">...</div>
</div>
```
### Formulare
```html
<!-- Opt-in Gruppen-Klassen -->
<div class="form-group">
<label for="email">E-Mail</label>
<input type="email" id="email" name="email">
<span class="form-hint">Wird für Login genutzt.</span>
</div>
<!-- Fehler-State -->
<div class="form-group">
<label for="pwd">Passwort</label>
<input type="password" id="pwd" class="error">
<span class="form-error">Mindestens 8 Zeichen.</span>
</div>
<!-- Mehrere Felder in einer Zeile -->
<div class="form-row">
<div class="form-group">...</div>
<div class="form-group">...</div>
</div>
<!-- Input mit Prefix/Suffix -->
<div class="input-group">
<span class="input-group-prefix">https://</span>
<input type="text" placeholder="beispiel.de">
</div>
<!-- File Upload mit Hint -->
<div class="form-group">
<label for="contract-upload">Vertragsdokument</label>
<label class="file-upload" for="contract-upload">
<input class="file-upload-input" type="file" id="contract-upload">
<span class="file-upload-head">
<span class="file-upload-body">
<span class="file-upload-title">PDF oder DOCX hochladen</span>
<span class="file-upload-description">Für Angebote, Verträge oder Briefings.</span>
</span>
<span class="file-upload-button">Datei wählen</span>
</span>
<span class="file-upload-meta" aria-hidden="true">
<span>Max. 10 MB</span>
<span>PDF, DOCX</span>
</span>
</label>
<span class="form-hint">Die Datei wird beim Absenden des Formulars normal übertragen.</span>
</div>
<!-- Dropzone-Optik mit Fehler-State -->
<div class="form-group">
<label for="asset-upload">Brand Assets</label>
<label class="file-upload file-upload-dropzone is-error" for="asset-upload">
<input class="file-upload-input" type="file" id="asset-upload">
<span class="file-upload-head">
<span class="file-upload-body">
<span class="file-upload-title">Datei hier ablegen oder auswählen</span>
<span class="file-upload-description">Optionaler Dropzone-Look ohne Preview-Logik.</span>
</span>
<span class="file-upload-button">Asset auswählen</span>
</span>
</label>
<span class="form-error">Bitte nur Logos im SVG- oder PNG-Format verwenden.</span>
</div>
<!-- Switch auf nativer Checkbox-Basis -->
<label class="switch" for="notify-comments">
<input class="switch-input" type="checkbox" id="notify-comments" checked>
<span class="switch-control" aria-hidden="true"></span>
<span class="switch-body">
<span class="switch-label">Kommentar-Benachrichtigungen</span>
<span class="switch-description">Informiert bei Antworten und Erwähnungen.</span>
</span>
</label>
```
Die Formular-Helfer decken damit einfache Gruppen, Hinweise, Fehlerstates, Switches und auch ein natives File-Upload-Pattern ab. `.file-upload` kapselt den klickbaren Bereich, `.file-upload-dropzone` schaltet auf eine gestrichelte Dropzone-Optik um und `.is-error` markiert visuell fehlerhafte Upload-Felder, ohne dass dafür JavaScript nötig ist.
### Accordion
```html
<div class="accordion">
<details class="accordion-item" open>
<summary>Was ist ein classless Framework?</summary>
<div class="accordion-body">
<p>Ein classless Framework stylt HTML-Elemente direkt.</p>
<p>Die Accordion-Komponente organisiert mehrere details-Elemente als zusammengehörige Gruppe.</p>
</div>
</details>
<details class="accordion-item">
<summary>Wie passe ich Farben an?</summary>
<div class="accordion-body">
<p>Farben und semantische Aliases werden in tokens.css gepflegt.</p>
</div>
</details>
</div>
<!-- Varianten -->
<div class="accordion accordion-flush">...</div>
<div class="accordion accordion-muted">...</div>
```
Das Pattern baut bewusst auf nativen details- und summary-Elementen auf. .accordion organisiert Abstände und Gruppierung, .accordion-body kapselt den Inhaltsbereich, und die Varianten .accordion-flush sowie .accordion-muted decken kompaktere oder weichere Darstellungen ab.
### Sortierbare Tabellen
```html
<table class="table-sortable">
<thead>
<tr>
<th data-action="sort" data-sort="">Name</th>
<th data-action="sort" data-sort="">Datum</th>
<th>Status</th> <!-- nicht sortierbar: kein data-action/data-sort -->
</tr>
</thead>
<tbody>...</tbody>
</table>
```
Erkennt automatisch numerische vs. alphabetische Werte. Deutsche Sortierung via `localeCompare`.
### Tabellen
```html
<!-- Classless (kein Wrapper nötig) -->
<table>
<thead><tr><th>Name</th><th>Status</th></tr></thead>
<tbody><tr><td>Anna</td><td>Aktiv</td></tr></tbody>
</table>
<!-- Mit Rahmen und Scroll -->
<div class="table-wrap">
<table>...</table>
</div>
<!-- Modifier -->
<table class="table-striped">...</table>
<table class="table-compact">...</table>
<!-- Sticky Header -->
<div class="table-wrap is-scroll">
<table class="table-sticky">...</table>
</div>
<!-- Sticky erste Spalte -->
<div class="table-wrap">
<table class="table-sticky-col">...</table>
</div>
<!-- Mobile Key-Value-Stacking (data-label auf jedem td erforderlich) -->
<table class="table-stack">
<thead><tr><th>Name</th><th>Status</th></tr></thead>
<tbody>
<tr>
<td data-label="Name">Anna</td>
<td data-label="Status">Aktiv</td>
</tr>
</tbody>
</table>
```
### Alerts
```html
<div class="alert alert-info">
<svg class="alert-icon">...</svg>
<div class="alert-content">
<p class="alert-title">Hinweis</p>
<p>Deine Daten werden gespeichert.</p>
</div>
</div>
```
**Varianten:** `alert-info`, `alert-success`, `alert-warning`, `alert-danger`
**Dismissible Alert:**
```html
<div class="alert alert-success">
<div class="alert-content">
<p class="alert-title">Erfolgreich</p>
<p>Deine Änderungen wurden gespeichert.</p>
</div>
<button class="alert-dismiss" data-action="alert-dismiss" aria-label="Schließen">✕</button>
</div>
```
```js
function dismissAlert(btn) {
const alert = btn.closest('.alert');
alert.classList.add('is-dismissing');
alert.addEventListener('transitionend', () => alert.remove(), { once: true });
}
```
### Modals
```html
<!-- Struktur -->
<div class="modal-backdrop" id="mein-modal"
data-action="modal-backdrop">
<div class="modal">
<div class="modal-header">
<h3>Titel</h3>
<button class="btn btn-ghost btn-icon btn-sm"
data-action="modal-close" data-target="mein-modal">✕</button>
</div>
<div class="modal-body">Inhalt</div>
<div class="modal-footer">
<button class="btn btn-ghost" data-action="modal-close" data-target="mein-modal">Abbrechen</button>
<button class="btn btn-primary">Bestätigen</button>
</div>
</div>
</div>
<button data-action="modal-open" data-target="mein-modal">Öffnen</button>
```
**Größen:** `.modal-sm`, `.modal-lg`, `.modal-xl`
### Tabs
```html
<div class="tabs">
<ul class="tab-list" role="tablist">
<li><button class="active" role="tab"
data-action="tab" data-target="panel-1">Tab 1</button></li>
<li><button role="tab"
data-action="tab" data-target="panel-2">Tab 2</button></li>
</ul>
<div id="panel-1" class="tab-panel active">Inhalt 1</div>
<div id="panel-2" class="tab-panel">Inhalt 2</div>
</div>
```
### Dropdowns
```html
<div class="dropdown" id="dd-1">
<button class="btn btn-secondary" data-action="dropdown" data-target="dd-1">
Aktionen
</button>
<div class="dropdown-menu">
<div class="dropdown-label">Aktionen</div>
<button class="dropdown-item">Bearbeiten</button>
<div class="dropdown-divider"></div>
<button class="dropdown-item danger">Löschen</button>
</div>
</div>
```
**Varianten:** `.dropdown-menu-left`, `.dropdown-menu-top`, `.dropdown-wide`
**Spezielle Item-Typen:**
```html
<!-- Mit Beschreibung -->
<button class="dropdown-item dropdown-item-rich">
<div class="dropdown-item-title">Projekt</div>
<div class="dropdown-item-desc">Neue Projektmappe erstellen</div>
</button>
<!-- Checkmark (Filter) -->
<button class="dropdown-item dropdown-item-check checked"
data-action="toggle-class" data-class="checked">
Aktiv
</button>
<!-- User-Header -->
<div class="dropdown-header">
<div class="avatar">AM</div>
<div>
<div class="dropdown-header-name">Anna Müller</div>
<div class="dropdown-header-sub">anna@beispiel.de</div>
</div>
</div>
```
### Pagination
```html
<ul class="pagination">
<li class="disabled"><a>←</a></li>
<li><a class="active" href="?page=1">1</a></li>
<li><a href="?page=2">2</a></li>
<li><a href="?page=3">3</a></li>
<li><a href="?page=2">→</a></li>
</ul>
```
### Feedback & Status
```html
<!-- Spinner -->
<div class="spinner"></div>
<div class="spinner spinner-sm"></div>
<div class="spinner spinner-lg"></div>
<!-- Fortschrittsbalken -->
<div class="progress">
<div class="progress-bar" data-progress="75"></div>
</div>
<div class="progress">
<div class="progress-bar success" data-progress="100"></div>
</div>
<!-- Skeleton (Lade-Platzhalter) -->
<div class="skeleton skeleton-line is-short"></div>
<div class="skeleton skeleton-title"></div>
<div class="skeleton skeleton-block"></div>
<!-- KPI / Stat -->
<div class="stat">
<span class="stat-label">Umsatz</span>
<span class="stat-value">€ 48.291</span>
<span class="stat-change up">↑ +12.4%</span>
</div>
<!-- Stat mit trend-basierter Einfärbung des Hauptwerts -->
<!-- data-trend="positive" → grün, data-trend="negative" → rot -->
<div class="stat" data-trend="positive">
<span class="stat-label">Saldo</span>
<span class="stat-value">+ € 1.240</span>
</div>
<div class="stat" data-trend="negative">
<span class="stat-label">Saldo</span>
<span class="stat-value">− € 320</span>
</div>
<!-- Avatar -->
<div class="avatar">AM</div>
<div class="avatar avatar-sm">AM</div>
<div class="avatar avatar-lg"><img src="..."></div>
<!-- Avatar Group -->
<div class="avatar-group">
<div class="avatar avatar-sm">AM</div>
<div class="avatar avatar-sm">BS</div>
</div>
<!-- Empty State -->
<div class="empty">
<svg class="empty-icon">...</svg>
<p class="empty-title">Keine Einträge</p>
<p class="empty-description">Erstelle deinen ersten Eintrag.</p>
<button class="btn btn-primary btn-sm">Erstellen</button>
</div>
<!-- Toast Container (einmalig im Basis-Template, direkt nach <body>) -->
<div class="toast-container" id="toast-container"></div>
<!-- Divider mit Label -->
<div class="divider">oder</div>
```
### Tooltip
```html
<!-- Standard: oben -->
<button data-tooltip="Erklärungstext">Hover mich</button>
<!-- Positionsvarianten -->
<button data-tooltip="Unten" data-tooltip-pos="bottom">...</button>
<button data-tooltip="Links" data-tooltip-pos="left">...</button>
<button data-tooltip="Rechts" data-tooltip-pos="right">...</button>
```
CSS-only via `data-tooltip`-Attribut. Kein JavaScript nötig. Funktioniert auf jedem Element, reagiert auch auf `:focus-visible`.
### Meta List / Definition List (`.meta-list`)
```html
<dl class="meta-list">
<div class="meta-item">
<dt class="meta-label">Status</dt>
<dd class="meta-value"><span class="badge badge-success">Aktiv</span></dd>
</div>
<div class="meta-item">
<dt class="meta-label">Erstellt am</dt>
<dd class="meta-value">12. Januar 2025</dd>
</div>
</dl>
```
**Varianten:** `.meta-list-stacked` (Label über Wert), `.meta-list-compact`, `.meta-list-flush` (kein oberer Border)
### Filter Chips (`.chip-group`)
```html
<!-- Mehrfachauswahl (Checkbox) -->
<div class="chip-group">
<label class="chip">
<input class="chip-input" type="checkbox" checked>
<span class="chip-label">Design</span>
</label>
<label class="chip">
<input class="chip-input" type="checkbox">
<span class="chip-label">Entwicklung</span>
</label>
</div>
<!-- Einzelauswahl (Radio) -->
<div class="chip-group">
<label class="chip">
<input class="chip-input" type="radio" name="filter" checked>
<span class="chip-label">Alle</span>
</label>
<label class="chip">
<input class="chip-input" type="radio" name="filter">
<span class="chip-label">Aktiv</span>
</label>
</div>
<!-- Klein -->
<div class="chip-group chip-sm">...</div>
```
### Timeline / Activity Feed (`.timeline`)
```html
<div class="timeline">
<div class="timeline-item timeline-success">
<div class="timeline-marker">
<div class="timeline-dot"></div>
<div class="timeline-connector"></div>
</div>
<div class="timeline-content">
<p class="timeline-title">Deployment abgeschlossen</p>
<p class="timeline-meta">vor 2 Stunden</p>
<p class="timeline-body">Version 2.4.1 ist live.</p>
</div>
</div>
<!-- letztes Item: .timeline-connector kann weggelassen werden -->
</div>
```
**Dot-Varianten:** `timeline-accent`, `timeline-success`, `timeline-warning`, `timeline-danger`
### Stepper / Progress Steps (`.stepper`)
```html
<!-- Horizontal -->
<div class="stepper">
<div class="step step-done">
<div class="step-indicator">✓</div>
<span class="step-label">Konto</span>
</div>
<div class="step step-active">
<div class="step-indicator">2</div>
<span class="step-label">Profil</span>
</div>
<div class="step">
<div class="step-indicator">3</div>
<span class="step-label">Fertig</span>
</div>
</div>
<!-- Vertikal -->
<div class="stepper stepper-vertical">
<div class="step step-done">
<div class="step-indicator">✓</div>
<div class="step-body">
<p class="step-title">Konto anlegen</p>
<p class="step-desc">E-Mail und Passwort festlegen.</p>
</div>
</div>
...
</div>
```
**Step-Modifier:** `.step-done`, `.step-active`
### List Group / Action List
```html
<!-- List Group (statisch) -->
<ul class="list-group">
<li class="list-item list-item-active">Dashboard</li>
<li class="list-item">Projekte</li>
<li class="list-item list-item-muted">Archiv</li>
</ul>
<!-- Action List (klickbar) -->
<div class="action-list">
<a href="/profil" class="action-item">
<svg class="action-item-icon">...</svg>
<div class="action-item-content">
<p class="action-item-title">Profil bearbeiten</p>
<p class="action-item-desc">Name, Avatar und Kontakt</p>
</div>
<svg class="action-item-end">›</svg>
</a>
<button class="action-item action-item-danger">Konto löschen</button>
</div>
```
**Varianten:** `.list-group-compact`, `.action-list-compact`, `.list-group-flush`, `.action-list-flush`
---
## Utilities (`utilities.css`)
Minimale Helfer — bewusst schlank gehalten.
```html
<!-- Filter-Leiste: Flex-Row, auf Mobile gestapelt (< 40rem) -->
<div class="filter-bar">
<div class="form-group"><label>Monat</label><input type="month"></div>
<div class="form-group"><label>Typ</label><select>...</select></div>
<button class="btn btn-secondary btn-sm">Filtern</button>
</div>
<!-- Flex -->
<div class="flex-center"> <!-- align + justify: center -->
<div class="flex-between"> <!-- space-between -->
<div class="flex-1"> <!-- flex: 1 -->
<!-- Alignment (standalone, kombinierbar mit .flex / .grid-u) -->
<div class="flex items-center justify-between">...</div>
<div class="flex flex-col items-center">...</div>
<!-- align-items -->
<div class="items-start"> <!-- flex-start -->
<div class="items-center">
<div class="items-end"> <!-- flex-end -->
<div class="items-stretch">
<div class="items-baseline">
<!-- justify-content -->
<div class="justify-start">
<div class="justify-center">
<div class="justify-end">
<div class="justify-between">
<!-- align-self (auf Kind-Elementen) -->
<div class="self-start">
<div class="self-center">
<div class="self-end">
<div class="self-stretch">
<!-- Text -->
<p class="text-sm text-muted">Kleiner Hilfstext</p>
<span class="font-semibold truncate">Langer Text wird abgeschnitten</span>
<p class="text-accent">Akzentfarbe</p>
<p class="text-success/warning/danger">Status-Farben</p>
<!-- Spacing -->
<div class="mt-4 mb-8 px-6">...</div>
<div class="ml-auto">Schiebt nach rechts</div>
<!-- Verschiedenes -->
<div class="rounded-xl shadow-md border">...</div>
<div class="bg-subtle overflow-x-auto">...</div>
<span class="sr-only">Nur für Screenreader</span>
```
---
## JavaScript API
`framework.js` bietet jede Funktion auf zwei Wegen an:
1. **Deklarativ über `data-action`** (empfohlen). Ein einziger Listener am `document` wertet die Attribute aus. Das funktioniert unter einer strengen Content Security Policy und auch für Inhalte, die HTMX später nachlädt.
2. **Als globale Funktion**, z. B. `openModal('m')`, für eigene Skripte. Inline-Handler wie `onclick="…"` gehen zwar weiterhin, verlangen aber `'unsafe-inline'` in der CSP.
```html
<button class="btn" data-action="modal-open" data-target="mein-modal">Öffnen</button>
```
### Aktionen
| `data-action` | Weitere Attribute | Wirkung | Entspricht |
|---|---|---|---|
| `modal-open` | `data-target="id"` | Modal öffnen, Seite nicht mehr scrollen | `openModal(id)` |
| `modal-close` | `data-target="id"` (optional, sonst umgebendes Modal) | Modal schließen | `closeModal(id)` |
| `modal-backdrop` | auf `.modal-backdrop` mit `id` | Klick auf den Hintergrund schließt | `closeOnBackdrop(e, id)` |
| `tab` | `data-target="panel-id"` | Tab wechseln | `switchTab(btn, id)` |
| `dropdown` | `data-target="id"` (optional, sonst umgebendes `.dropdown`) | Dropdown öffnen/schließen | `toggleDropdown(id)` |
| `nav-dropdown` | – | Nav-Dropdown öffnen/schließen | `toggleNavDd(btn)` |
| `sidebar-open` / `sidebar-close` | – | Mobile Sidebar ein-/ausblenden | `toggleSidebar()` / `closeSidebar()` |
| `alert-dismiss` | – | Alert mit Transition entfernen | `dismissAlert(btn)` |
| `sort` | auf `th`, mit `data-sort=""` | Spalte sortieren | `sortTable(th)` |
| `toggle-class` | `data-class="checked"` | Klasse am Element umschalten | – |
| `toast` | `data-message="…"`, `data-type="success"` | Toast einblenden | `showToast(msg, type)` |
| `toast-close` | – | Toast schließen (setzt `showToast` selbst) | `removeToast(id)` |
| `theme-toggle` | – | Hell/Dunkel umschalten (`theme.js`) | – |
### Funktionsübersicht
| Funktion | Beschreibung |
|---|---|
| `openModal(id)` | Modal öffnen, Scroll sperren (`body.modal-open`) |
| `closeModal(id)` | Modal schließen |
| `closeOnBackdrop(e, id)` | Modal schließen, wenn der Klick auf dem Hintergrund lag |
| `switchTab(btn, panelId)` | Tab wechseln |
| `toggleDropdown(id)` | Dropdown öffnen/schließen |
| `toggleNavDd(btn)` | Nav-Dropdown öffnen/schließen |
| `showToast(message, type)` | Toast einblenden (`type`: `'success'`, `'warning'`, `'danger'`, `'info'`). Die Meldung wird als Text gesetzt, HTML darin nicht ausgeführt |
| `removeToast(id)` | Toast ausblenden und entfernen |
| `dismissAlert(btn)` | Alert mit Transition entfernen |
| `sortTable(th)` | Tabellenspalte auf-/absteigend sortieren |
| `toggleSidebar()` | Mobile Sidebar einblenden |
| `closeSidebar()` | Mobile Sidebar ausblenden |
ESC schließt automatisch alle offenen Modals. Klick außerhalb schließt Dropdowns.
### Theme (`theme.js`)
`theme.js` setzt Hell/Dunkel vor dem ersten Rendern (gespeicherter Wert, sonst `prefers-color-scheme`) und schaltet bei `data-action="theme-toggle"` um. Im `<head>` **ohne** `defer` einbinden. Die Icons wechseln per CSS: `.icon-theme-light` ist im hellen, `.icon-theme-dark` im dunklen Modus sichtbar.
```html
<head>
<script src="/static/js/theme.js"></script>
<link rel="stylesheet" href="/static/css/main.css">
</head>
…
<button class="btn btn-ghost btn-icon" data-action="theme-toggle" aria-label="Hell/Dunkel">
<svg class="icon-theme-light">…Mond…</svg>
<svg class="icon-theme-dark">…Sonne…</svg>
</button>
```
### Content Security Policy
Das Framework läuft unter einer strengen CSP ohne `'unsafe-inline'` und ohne `'unsafe-eval'`:
```
default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:
```
Dafür gilt:
- Keine `on…`-Attribute, sondern `data-action`.
- Keine `style`-Attribute, sondern Klassen (Tokens, Utilities, z. B. `min-w-0`, `skeleton-line`).
- Kein Inline-`<script>`, sondern Dateien (`framework.js`, `theme.js`, eigene Skripte).
- HTMX ohne eigene Inline-Styles: `<meta name="htmx-config" content='{"includeIndicatorStyles": false, "allowEval": false}'>`. Die Styles für `.htmx-indicator` liefert `components.css`. `hx-on` nicht verwenden (braucht `eval`).
**Nicht in `framework.js` enthalten — markup-abhängig, gehört in eine eigene Datei des Projekts:**
- Scroll-basierte aktive Nav-Markierung (abhängig von der Seitenstruktur)
---
---
## Fonts lokal hosten (`fonts.css`)
```css
/* fonts.css — Vorlage, Pfade anpassen */
@font-face {
font-family: "Inter";
font-weight: 400;
font-display: swap;
src: url("/static/fonts/inter-regular.woff2") format("woff2");
}
```
Empfohlene Quelle: [gwfh.mranftl.com](https://gwfh.mranftl.com)
Aktivieren in `main.css`:
```css
@import "fonts.css"; /* vor allen anderen Imports */
@import "tokens.css";
/* ... */
```
---
## Projektstruktur (empfohlen)
```
projekt/
└── static/
└── css/
├── main.css — Entry Point
├── fonts.css — Lokale Fonts (optional)
├── tokens.css
├── base.css
├── components.css
└── utilities.css
```
---
## Framework in ein bestehendes Projekt integrieren
### 1. CSS-Dateien kopieren
```
static/css/
├── main.css
├── tokens.css
├── base.css
├── components.css
└── utilities.css
```
Optional: `fonts.css` für lokal gehostete Schriften.
### 2. CSS und JS einbinden
```html
<head>
<script src="/static/js/theme.js"></script>
<link rel="stylesheet" href="/static/css/main.css">
<script src="/static/js/framework.js" defer></script>
</head>
```
`framework.js` enthält alle Funktionen und den Listener für `data-action` (Modal, Toast, Tabs, Dropdown usw.), `theme.js` das Umschalten von Hell/Dunkel — kein weiterer Setup-Schritt nötig.
### 3. KI-Guidance mitgeben (OpenCode)
Den `.opencode/`-Ordner aus diesem Repo ins Consumer-Projekt kopieren:
```
.opencode/
├── commands/ — Prompt-Checklisten für neue Komponenten, Tokens, Sync
└── skills/
├── SKILL.md — Schnellreferenz (wird beim Skill-Aufruf geladen)
└── references/ — Vollständige Referenzen für Komponenten, Tokens, Jinja2
```
Außerdem `AGENTS.md` ins Projektstamm-Verzeichnis kopieren — sie enthält die verbindlichen Arbeitsregeln für KI-Agenten.
### 4. Basis-Template einrichten
Das fertige `base.html` für FastAPI + Jinja2 + HTMX liegt in `.opencode/skills/references/jinja2.md`. Es enthält:
- Theme-Toggle über `theme.js` (CSP-fest, ohne Inline-Skript)
- Sidebar-Layout mit Mobile-Overlay
- Alle Aktionen über `data-action` (CSP-fest)
- HTMX-Einbindung
### 5. Version prüfen
Die aktuelle Framework-Version steht im Kommentar von `main.css`. Beim Aktualisieren des Frameworks einfach die CSS-Dateien ersetzen — keine Build-Schritte nötig.