1134 lines
34 KiB
Markdown
1134 lines
34 KiB
Markdown
# 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.
|