hugin/static/css-framework/.opencode/skills/SKILL.md
2026-10-27 08:33:21 +01:00

16 KiB
Raw Blame History


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:

<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 · components.md · 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)

/* 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

<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

Navigation

<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

<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

<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

<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

<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

<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

<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

<!-- 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

<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

<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

<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

<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

<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

Button Loading State

<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:

<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

<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

<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

# Flask: HX-Trigger im Response-Header setzen
response.headers['HX-Trigger'] = '{"showToast":{"message":"Gespeichert","type":"success"}}'
<!-- 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){}}
">