406 lines
16 KiB
Markdown
406 lines
16 KiB
Markdown
---
|
||
name: css-framework
|
||
description: Erstellt und bearbeitet HTML/CSS/JS für CSS-Framework — ein classless-first CSS-Framework für Jinja2/HTMX-Webapps. Verwende diesen Skill für alle UI-Aufgaben: Layouts, Navigation, Buttons, Cards, Formulare (inkl. Switch, File Upload), Tabellen (inkl. sortierbar, sticky, mobile-stack), Modals, Dropdowns, Tabs, Alerts (inkl. dismissible), Accordion, Filter Chips, Timeline, Stepper, List Group, Meta List, Tooltip, Toast, Pagination, Feedback-Komponenten (Spinner, Skeleton, Stat, Avatar) sowie Alignment- und Spacing-Utilities. Auch für Design Tokens, Dark Mode, HTMX-Integration und die JavaScript-API (showToast, dismissAlert, sortTable, openModal, switchTab, toggleDropdown).
|
||
metadata:
|
||
author: tom
|
||
version: "2.1"
|
||
---
|
||
|
||
## Überblick
|
||
|
||
CSS-Framework ist ein schlankes, tokenbasiertes CSS-Framework für Flask/Jinja2-Webapps mit HTMX. Kein Build-Tool, keine Abhängigkeiten.
|
||
|
||
**Einbinden:**
|
||
```html
|
||
<script src="/static/js/theme.js"></script> <!-- im <head>, ohne defer -->
|
||
<link rel="stylesheet" href="/static/css/main.css">
|
||
<script src="/static/js/framework.js" defer></script>
|
||
```
|
||
|
||
**Datei-Architektur:**
|
||
| Datei | Zweck |
|
||
|---|---|
|
||
| `tokens.css` | CSS Custom Properties — einzige Source of Truth |
|
||
| `base.css` | Classless Reset — semantisches HTML braucht keine Klassen |
|
||
| `components.css` | Opt-in Klassen für Layouts & Komponenten |
|
||
| `utilities.css` | Minimale Helfer-Klassen |
|
||
| `main.css` | Entry Point |
|
||
| `framework.js` | JavaScript API — `data-action` und globale Funktionen |
|
||
| `theme.js` | Hell/Dunkel vor dem Rendern setzen und umschalten |
|
||
|
||
Vollständige Referenzen: [tokens.md](references/tokens.md) · [components.md](references/components.md) · [jinja2.md](references/jinja2.md)
|
||
|
||
---
|
||
|
||
## Kernprinzipien
|
||
|
||
1. **Classless first** — `h1–h6`, `p`, `a`, `table`, `input`, `select`, `details` werden ohne Klassen gestylt
|
||
2. **Opt-in Klassen** — nur wo semantisches HTML nicht ausreicht
|
||
3. **Token first** — alle Werte über `tokens.css`, nie hartverdrahten
|
||
4. **Dark Mode** via `data-theme="dark"` auf `<html>`
|
||
5. **Minimales JS** — framework-unabhängig, in `framework.js` via `<script src>` eingebunden
|
||
6. **CSP-fest** — Aktionen über `data-action`, Styles über Klassen, kein Inline-Code
|
||
|
||
---
|
||
|
||
## Design Tokens (Kurzübersicht)
|
||
|
||
```css
|
||
/* Hintergründe */ --bg --bg-subtle --bg-muted
|
||
/* Surfaces */ --surface --surface-raised --surface-overlay
|
||
/* Rahmen */ --border --border-strong --border-focus
|
||
/* Text */ --text --text-muted --text-subtle --text-on-accent
|
||
/* Akzentfarbe */ --accent --accent-hover --accent-subtle --accent-muted
|
||
/* Status */ --color-success --color-warning --color-danger --color-info
|
||
/* Spacing (4px-Grid) */ --space-1(4px) … --space-24(96px)
|
||
/* Typografie */ --font-sans --font-mono | --text-xs … --text-4xl
|
||
```
|
||
|
||
---
|
||
|
||
## Layout-Klassen
|
||
|
||
```html
|
||
<div class="container"> <!-- max-width 1280px -->
|
||
<div class="container container-md"> <!-- max-width 768px -->
|
||
<div class="stack"> <!-- flex-col, gap 1rem -->
|
||
<div class="stack stack-sm"> <!-- gap 0.5rem -->
|
||
<div class="stack stack-lg"> <!-- gap 2rem -->
|
||
<div class="cluster"> <!-- flex-wrap, gap 0.75rem, align center -->
|
||
<div class="grid grid-2"> <!-- 2 Spalten -->
|
||
<div class="grid grid-3"> <!-- 3 Spalten -->
|
||
<div class="grid grid-4"> <!-- 4 Spalten -->
|
||
<div class="with-sidebar"> <!-- sidebar-width + 1fr -->
|
||
```
|
||
|
||
---
|
||
|
||
## Komponenten (Schnellreferenz)
|
||
|
||
Vollständige Beispiele: [references/components.md](references/components.md)
|
||
|
||
### Navigation
|
||
```html
|
||
<nav class="topnav">...</nav> <!-- sticky, mit burger + dropdown -->
|
||
<aside class="sidebar">...</aside> <!-- mit .with-sidebar kombinieren -->
|
||
<nav class="breadcrumbs"><ol>...</ol></nav>
|
||
<footer class="footer">...</footer> <!-- .footer-full für mehrspaltig -->
|
||
```
|
||
|
||
### Buttons
|
||
```html
|
||
<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>
|
||
<!-- Größen: btn-xs btn-sm btn-lg btn-xl | Modifiers: btn-full btn-icon -->
|
||
<div class="button-group">...</div> <!-- bündige Gruppe -->
|
||
<div class="segmented">...</div> <!-- Radio-Basis, kein JS -->
|
||
```
|
||
|
||
### Badges & Cards
|
||
```html
|
||
<span class="badge badge-success badge-dot">Live</span>
|
||
<!-- badge-default/primary/success/warning/danger/info/outline -->
|
||
|
||
<div class="card">
|
||
<div class="card-header"><h3>Titel</h3></div>
|
||
<div class="card-body">...</div>
|
||
<div class="card-footer">...</div>
|
||
</div>
|
||
<!-- card-flat / card-raised / card-ghost / card-hover -->
|
||
```
|
||
|
||
### Formulare
|
||
```html
|
||
<div class="form-group">
|
||
<label for="x">Label</label>
|
||
<input type="text" id="x">
|
||
<span class="form-hint">Hinweis</span>
|
||
</div>
|
||
<div class="form-row">...</div> <!-- nebeneinander -->
|
||
<input class="error"> <span class="form-error"> <!-- Fehler-State -->
|
||
<div class="input-group"><span class="input-group-prefix">https://</span><input></div>
|
||
<label class="file-upload">...</label> <!-- .file-upload-dropzone -->
|
||
<label class="switch"><input class="switch-input" type="checkbox">...</label>
|
||
```
|
||
|
||
### Tabellen
|
||
```html
|
||
<table>...</table> <!-- classless -->
|
||
<div class="table-wrap"><table>...</table></div> <!-- mit Rahmen + Scroll -->
|
||
<table class="table-striped">
|
||
<table class="table-compact">
|
||
<table class="table-sticky"> <!-- sticky thead -->
|
||
<table class="table-sticky-col"> <!-- sticky erste Spalte -->
|
||
<table class="table-stack"> <!-- Mobile: Key-Value (data-label auf td) -->
|
||
<table class="table-sortable"> <!-- data-action="sort" data-sort="" auf th -->
|
||
```
|
||
|
||
### Alerts & Feedback
|
||
```html
|
||
<div class="alert alert-success">
|
||
<div class="alert-content"><p class="alert-title">Titel</p><p>Text</p></div>
|
||
<button class="alert-dismiss" data-action="alert-dismiss">✕</button>
|
||
</div>
|
||
<!-- alert-info / alert-success / alert-warning / alert-danger -->
|
||
|
||
<div class="spinner"></div> <!-- spinner-sm / spinner-lg -->
|
||
<div class="progress"><div class="progress-bar" data-progress="75"></div></div>
|
||
<div class="skeleton skeleton-line is-short"></div> <!-- auch skeleton-title, skeleton-block -->
|
||
<div class="stat"><span class="stat-label">...</span><span class="stat-value">...</span></div>
|
||
<div class="avatar">AM</div> <!-- avatar-sm / avatar-lg -->
|
||
<div class="empty"><p class="empty-title">...</p></div>
|
||
<div class="divider">oder</div>
|
||
```
|
||
|
||
### Modals & Dropdowns & Tabs
|
||
```html
|
||
<div class="modal-backdrop" id="m" data-action="modal-backdrop">
|
||
<div class="modal modal-lg">
|
||
<div class="modal-header">...</div>
|
||
<div class="modal-body">...</div>
|
||
<div class="modal-footer">...</div>
|
||
</div>
|
||
</div>
|
||
<button data-action="modal-open" data-target="m">Öffnen</button>
|
||
<!-- modal-sm / modal-lg / modal-xl -->
|
||
|
||
<div class="dropdown" id="dd"><button data-action="dropdown" data-target="dd">...</button>
|
||
<div class="dropdown-menu"><button class="dropdown-item">...</button></div>
|
||
</div>
|
||
|
||
<div class="tabs">
|
||
<ul class="tab-list" role="tablist">
|
||
<li><button class="active" role="tab" data-action="tab" data-target="p1">Tab</button></li>
|
||
</ul>
|
||
<div id="p1" class="tab-panel active">...</div>
|
||
</div>
|
||
```
|
||
|
||
### Neue Komponenten
|
||
```html
|
||
<!-- Filter Chips (Checkbox/Radio, kein JS) -->
|
||
<div class="chip-group">
|
||
<label class="chip"><input class="chip-input" type="checkbox" checked><span class="chip-label">Design</span></label>
|
||
</div>
|
||
|
||
<!-- Timeline -->
|
||
<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">Titel</p><p class="timeline-meta">Meta</p></div>
|
||
</div>
|
||
</div>
|
||
<!-- timeline-accent / timeline-success / timeline-warning / timeline-danger -->
|
||
|
||
<!-- Stepper -->
|
||
<div class="stepper">
|
||
<div class="step step-done"><div class="step-indicator">✓</div><span class="step-label">Schritt</span></div>
|
||
<div class="step step-active"><div class="step-indicator">2</div><span class="step-label">Aktiv</span></div>
|
||
<div class="step"><div class="step-indicator">3</div><span class="step-label">Offen</span></div>
|
||
</div>
|
||
<!-- .stepper-vertical für vertikale Variante -->
|
||
|
||
<!-- List Group / Action List -->
|
||
<ul class="list-group">
|
||
<li class="list-item list-item-active">Aktiv</li>
|
||
<li class="list-item">Eintrag</li>
|
||
</ul>
|
||
<div class="action-list">
|
||
<a href="#" class="action-item">
|
||
<div class="action-item-content"><p class="action-item-title">Titel</p></div>
|
||
</a>
|
||
</div>
|
||
<!-- list-group-compact / action-list-flush -->
|
||
|
||
<!-- Meta List -->
|
||
<dl class="meta-list">
|
||
<div class="meta-item"><dt class="meta-label">Status</dt><dd class="meta-value">Aktiv</dd></div>
|
||
</dl>
|
||
<!-- meta-list-stacked / meta-list-compact / meta-list-flush -->
|
||
|
||
<!-- Tooltip (CSS-only) -->
|
||
<button data-tooltip="Erklärung">Hover</button>
|
||
<button data-tooltip="Unten" data-tooltip-pos="bottom">...</button>
|
||
<!-- data-tooltip-pos: top (default) / bottom / left / right -->
|
||
|
||
<!-- Accordion -->
|
||
<div class="accordion">
|
||
<details class="accordion-item" open>
|
||
<summary>Frage</summary>
|
||
<div class="accordion-body"><p>Antwort</p></div>
|
||
</details>
|
||
</div>
|
||
<!-- accordion-flush / accordion-muted -->
|
||
```
|
||
|
||
---
|
||
|
||
## Utility-Klassen (Auswahl)
|
||
|
||
```
|
||
Display: block inline flex hidden
|
||
Flex: flex-center flex-between flex-start flex-end flex-col flex-wrap flex-1 flex-shrink-0
|
||
Alignment: items-start/center/end/stretch/baseline
|
||
justify-start/center/end/between
|
||
self-start/center/end/stretch
|
||
Gap: gap-1/2/3/4/6/8
|
||
Margin: mt-0/2/4/6/8/auto mb-0/2/4/6/8 ml-auto mr-auto mx-auto
|
||
Padding: p-0/2/4/6/8 px-4/6 py-4/6/8
|
||
Text: text-xs/sm/base/md/lg/xl text-left/center/right
|
||
text-muted text-subtle text-accent text-success text-warning text-danger
|
||
Font: font-normal/medium/semibold/bold font-mono uppercase capitalize truncate nowrap
|
||
Border: border border-t border-b rounded rounded-lg rounded-xl rounded-full
|
||
BG: bg-subtle bg-muted bg-surface
|
||
Shadow: shadow-xs shadow-sm shadow-md shadow-lg
|
||
Other: w-full h-full min-h-screen overflow-hidden overflow-auto overflow-x-auto
|
||
relative absolute sticky inset-0 cursor-pointer opacity-50 transition
|
||
filter-bar sr-only hide-mobile hide-desktop
|
||
```
|
||
|
||
---
|
||
|
||
## JavaScript API
|
||
|
||
Aktionen deklarativ über `data-action` — CSP-fest, funktioniert auch in HTMX-Fragmenten. **Nie `onclick`, `style=` oder Inline-`<script>`** (Consumer-Apps laufen mit strenger CSP).
|
||
|
||
| `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`) | – |
|
||
|
||
Für eigene Skripte gibt es dieselben Funktionen global: `openModal(id)`, `closeModal(id)`, `switchTab(btn, id)`, `toggleDropdown(id)`, `showToast(msg, type)`, `dismissAlert(btn)`, `sortTable(th)`. `showToast` setzt die Meldung als Text (kein HTML).
|
||
|
||
---
|
||
|
||
## Häufige Muster
|
||
|
||
### Seitenstruktur mit Sidebar
|
||
```html
|
||
<div class="mobile-topbar"><button data-action="sidebar-open">☰</button><span>App</span></div>
|
||
<div id="sidebar-backdrop" class="sidebar-backdrop" data-action="sidebar-close"></div>
|
||
<div class="with-sidebar">
|
||
<aside class="sidebar">...</aside>
|
||
<main class="stack stack-lg">...</main>
|
||
</div>
|
||
```
|
||
|
||
### Dashboard-Kacheln
|
||
```html
|
||
<div class="grid grid-3">
|
||
<div class="card"><div class="card-body">
|
||
<div class="stat"><span class="stat-label">Umsatz</span><span class="stat-value">€ 48.291</span></div>
|
||
</div></div>
|
||
</div>
|
||
```
|
||
|
||
### Detailseite mit Meta-Infos
|
||
```html
|
||
<div class="grid grid-2">
|
||
<div class="card"><div class="card-body">
|
||
<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>
|
||
</dl>
|
||
</div></div>
|
||
</div>
|
||
```
|
||
|
||
### Aktionsliste mit Zeilen-Alignment
|
||
```html
|
||
<div class="action-list">
|
||
<a href="/einstellungen" class="action-item">
|
||
<div class="action-item-content">
|
||
<p class="action-item-title">Einstellungen</p>
|
||
<p class="action-item-desc">Profil und Konto</p>
|
||
</div>
|
||
<svg class="action-item-end">›</svg>
|
||
</a>
|
||
</div>
|
||
```
|
||
|
||
### Filter-Leiste
|
||
```html
|
||
<div class="filter-bar">
|
||
<div class="form-group"><label>Monat</label><input type="month"></div>
|
||
<div class="chip-group"><label class="chip"><input class="chip-input" type="checkbox" checked><span class="chip-label">Aktiv</span></label></div>
|
||
<button class="btn btn-secondary btn-sm">Filtern</button>
|
||
</div>
|
||
```
|
||
|
||
---
|
||
|
||
## HTMX-Muster
|
||
|
||
Vollständige Referenz inkl. Basis-Template und Flask-Patterns: [references/jinja2.md](references/jinja2.md)
|
||
|
||
### Button Loading State
|
||
```html
|
||
<button class="btn btn-primary"
|
||
hx-post="/api/speichern"
|
||
hx-disabled-elt="this">
|
||
<span class="htmx-indicator spinner spinner-sm"></span>
|
||
Speichern
|
||
</button>
|
||
```
|
||
|
||
### Inline-Validation (422)
|
||
Server antwortet mit HTTP 422 + rendert Formular mit `.error`-Klassen auf Inputs:
|
||
```html
|
||
<form hx-post="/api/formular" hx-swap="outerHTML">
|
||
<div class="form-group">
|
||
<input type="email" name="email" class="error">
|
||
<span class="form-error">Ungültige E-Mail</span>
|
||
</div>
|
||
</form>
|
||
```
|
||
|
||
### Skeleton als Ladeindikator
|
||
```html
|
||
<div hx-get="/api/daten" hx-trigger="load" hx-swap="outerHTML">
|
||
<div class="stack">
|
||
<div class="skeleton skeleton-title"></div>
|
||
<div class="skeleton skeleton-line"></div>
|
||
</div>
|
||
</div>
|
||
```
|
||
|
||
### Modal via HTMX befüllen
|
||
```html
|
||
<button class="btn btn-secondary"
|
||
hx-get="/api/detail/{{ id }}"
|
||
hx-target="#modal-body"
|
||
hx-swap="innerHTML"
|
||
data-action="modal-open" data-target="m">Details</button>
|
||
<div class="modal-backdrop" id="m" data-action="modal-backdrop">
|
||
<div class="modal">
|
||
<div class="modal-body" id="modal-body">
|
||
<div class="skeleton skeleton-block"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
```
|
||
|
||
### Toast über HX-Trigger-Header
|
||
```python
|
||
# Flask: HX-Trigger im Response-Header setzen
|
||
response.headers['HX-Trigger'] = '{"showToast":{"message":"Gespeichert","type":"success"}}'
|
||
```
|
||
```html
|
||
<!-- body-Attribut im Basis-Template -->
|
||
<body hx-on::after-request="
|
||
var h=event.detail.xhr.getResponseHeader('HX-Trigger');
|
||
if(h){try{var t=JSON.parse(h);if(t.showToast)showToast(t.showToast.message,t.showToast.type);}catch(e){}}
|
||
">
|
||
```
|