diff --git a/CLAUDE.md b/CLAUDE.md
index 78f7ff7..180ac0c 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -598,6 +598,40 @@ wie die Formular-Abschnitte angehoben (600/1rem statt 500/0.9375rem) —
für eine einheitliche Überschriften-Hierarchie statt Zufallsgrößen je
nach UA-Stylesheet.
+**enconf-Patterns übertragen (2026-08-31):** enconf (Schwesterprojekt)
+ist React + Ant Design (`ProTable`/`StatCard`/`PageHeader`-Komponenten)
+— nicht 1:1 übernehmbar, da Deklarix bewusst kein SPA ist, aber die
+*Muster* wurden als reines CSS/HTML nachgebaut (recherchiert über die
+Architect-RAG-Suche, `ac_search_code_global`, da das enconf-Repo selbst
+außerhalb des Arbeitsverzeichnisses liegt):
+
+- **`.table-responsive`** (ersetzt `.table-scroll` schrittweise) —
+ enconfs `ProTable.tsx` schaltet auf schmalen Screens von Tabelle auf
+ eine Card-Liste um (`renderMobileCard` via `Grid.useBreakpoint()`),
+ statt horizontal zu scrollen. Nachgebaut rein in CSS: unter 640px
+ werden `
` zu Karten, `| ` zu Label/Wert-Zeilen (`content:
+ attr(data-label)` im `::before`) — jedes ` | ` braucht dafür ein
+ `data-label="Spaltenname"`-Attribut. Ab 640px normale Tabelle mit
+ Scroll-Fade als Sicherheitsnetz. Bisher angewendet auf beide
+ Werkzeugkatalog-Tabellen; `.table-scroll` (nur Scroll-Fade, keine
+ Card-Umschaltung) bleibt für noch nicht migrierte Tabellen (Register,
+ Nutzer-Import-Ergebnis) bestehen.
+- **`.page-header`** (Titel+Unterzeile links, primäre Aktion rechts,
+ stapelt auf Mobile) — ersetzt einen nackten `` mit einem Link
+ irgendwo unten auf der Seite. Bisher auf den Werkzeugkatalog
+ angewendet (Betreiber + Mandant, inkl. "Eigene Werkzeuge"-Abschnitt).
+- **`.stat-cards`/`.stat-card`** — Kennzahl-Kacheln mit farbigem oberem
+ Akzentrand (blau normal, gelb/rot nur wenn der Wert > 0 ist — keine
+ Farbe ohne Grund) statt einfacher Text-Links; `.stat-card-action` für
+ die primäre Handlungs-Kachel (z. B. "Antrag stellen"). Kein
+ Icon-Set/keine Icon-Bibliothek eingeführt (enconf nutzt
+ `@ant-design/icons`, das ist React-spezifisch) — bewusste
+ Vereinfachung. Angewendet auf die Startseite (`index.html`).
+
+Noch nicht auf weitere Tabellen/Seiten übertragen (Register,
+Nutzerliste, Abteilungen, Fälle-Posteingang, Betreiber-Dashboard) —
+nächste Kandidaten, falls das Muster fortgesetzt werden soll.
+
**Verifikationsmethode für CSS-Änderungen:** Live-Cookie-Auth per
Chromium-Headless/CDP ist im Sandbox-Environment nicht möglich (kein
websocket-Python-Modul) — stattdessen: Seite per `curl -b cookies.txt`
diff --git a/internal/web/handlers_test.go b/internal/web/handlers_test.go
index c329aba..4af0dde 100644
--- a/internal/web/handlers_test.go
+++ b/internal/web/handlers_test.go
@@ -14,8 +14,9 @@ func TestIndexZeigtEigeneOffeneAntraege(t *testing.T) {
if resp.Code != http.StatusOK {
t.Fatalf("status = %d, body: %s", resp.Code, resp.Body.String())
}
- if !strings.Contains(resp.Body.String(), `href="/antraege">Meine offenen Anträge 1`) {
- t.Errorf("expected 1 offenen Antrag auf der Startseite, got: %s", resp.Body.String())
+ body := resp.Body.String()
+ if !strings.Contains(body, `href="/antraege"`) || !strings.Contains(body, `stat-card-value">1`) || !strings.Contains(body, "Meine offenen Anträge") {
+ t.Errorf("expected 1 offenen Antrag auf der Startseite, got: %s", body)
}
}
@@ -28,8 +29,9 @@ func TestIndexZeigtPosteingangNurFuerFachebene(t *testing.T) {
if resp.Code != http.StatusOK {
t.Fatalf("status = %d, body: %s", resp.Code, resp.Body.String())
}
- if !strings.Contains(resp.Body.String(), `href="/faelle">Posteingang 1`) {
- t.Errorf("expected 1 offenen Fall im Posteingang auf der Startseite, got: %s", resp.Body.String())
+ body := resp.Body.String()
+ if !strings.Contains(body, `href="/faelle"`) || !strings.Contains(body, `stat-card-value">1`) || !strings.Contains(body, "Posteingang") {
+ t.Errorf("expected 1 offenen Fall im Posteingang auf der Startseite, got: %s", body)
}
}
@@ -54,7 +56,7 @@ func TestIndexZeigtPlattformLinkFuerBetreiber(t *testing.T) {
if resp.Code != http.StatusOK {
t.Fatalf("status = %d, body: %s", resp.Code, resp.Body.String())
}
- if !strings.Contains(resp.Body.String(), `href="/betreiber">Zur Plattform`) {
+ if !strings.Contains(resp.Body.String(), `href="/betreiber"`) || !strings.Contains(resp.Body.String(), "Zur Plattform") {
t.Errorf("expected a Plattform link for betreiber, got: %s", resp.Body.String())
}
if strings.Contains(resp.Body.String(), "Antrag stellen") {
diff --git a/internal/web/static/app.css b/internal/web/static/app.css
index 70dcd24..5d467d1 100644
--- a/internal/web/static/app.css
+++ b/internal/web/static/app.css
@@ -792,6 +792,183 @@ tbody tr:hover {
font-size: 0.875rem;
}
+/* ── Responsive Tabellen — Card-Ansicht auf dem Handy ────────────────
+ enconf-Pattern (ProTable.tsx): auf schmalen Screens wird nicht
+ horizontal gescrollt, sondern jede Zeile zu einer eigenen Karte mit
+ Label/Wert-Paaren. Braucht data-label="Spaltenname" auf jedem .
+ Ab 640px bleibt es eine normale Tabelle (mit Scroll-Fade als
+ Sicherheitsnetz bei vielen Spalten). */
+.table-responsive {
+ overflow-x: auto;
+ box-shadow: var(--shadow);
+ border-radius: var(--radius-md);
+ margin-bottom: 16px;
+ -webkit-mask-image: linear-gradient(to right, black calc(100% - 24px), transparent 100%);
+ mask-image: linear-gradient(to right, black calc(100% - 24px), transparent 100%);
+}
+
+@media (max-width: 639px) {
+ .table-responsive {
+ overflow-x: visible;
+ box-shadow: none;
+ border-radius: 0;
+ -webkit-mask-image: none;
+ mask-image: none;
+ }
+
+ .table-responsive table,
+ .table-responsive tbody,
+ .table-responsive tr,
+ .table-responsive td {
+ display: block;
+ width: 100%;
+ }
+
+ .table-responsive thead {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ }
+
+ .table-responsive tbody tr {
+ margin-bottom: 12px;
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius-md);
+ background: #fff;
+ box-shadow: var(--shadow-sm);
+ }
+
+ .table-responsive td {
+ display: flex;
+ justify-content: space-between;
+ align-items: baseline;
+ gap: 12px;
+ text-align: right;
+ padding: 8px 12px;
+ border-bottom: 1px solid var(--color-border);
+ white-space: normal;
+ }
+
+ .table-responsive td:last-child {
+ border-bottom: none;
+ }
+
+ .table-responsive td::before {
+ content: attr(data-label);
+ font-weight: 600;
+ color: var(--color-heading);
+ text-align: left;
+ flex-shrink: 0;
+ }
+}
+
+/* ── Seitenkopf (PageHeader-Pattern, enconf) ─────────────────────────
+ Titel + Unterzeile links, primäre Aktion rechts — statt eines
+ nackten mit einem Link irgendwo unten auf der Seite. */
+.page-header {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 12px;
+ margin-bottom: 20px;
+}
+
+.page-header h1 {
+ margin-bottom: 4px;
+}
+
+.page-header .hinweis {
+ margin-top: 0;
+}
+
+.page-header .page-header-action {
+ margin-top: 4px;
+ white-space: nowrap;
+ display: inline-block;
+ padding: 10px 18px;
+ border-radius: var(--radius);
+ background: var(--branding-primary);
+ color: #fff;
+ font-weight: 500;
+ font-size: 0.9375rem;
+ text-decoration: none;
+ box-shadow: var(--shadow-sm);
+}
+
+.page-header .page-header-action:hover {
+ filter: brightness(0.94);
+}
+
+/* ── Kennzahl-Kacheln (StatCard-Pattern, enconf Dashboard) ───────────
+ Vorher: einfache Text-Links in einer Liste ohne visuelle Priorität.
+ Jetzt: farbiger oberer Akzentrand + große Zahl + Klick navigiert zur
+ Liste, wie enconfs StatCard-Komponente — nur ohne Icon-Bibliothek. */
+.stat-cards {
+ list-style: none;
+ margin: 0 0 24px;
+ padding: 0;
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
+ gap: 12px;
+}
+
+.stat-card {
+ display: block;
+ background: #fff;
+ border: 1px solid var(--color-border);
+ border-top: 3px solid var(--branding-primary);
+ border-radius: var(--radius-md);
+ box-shadow: var(--shadow-sm);
+ padding: 16px 18px;
+ text-decoration: none;
+ transition: box-shadow 0.15s ease;
+}
+
+.stat-card:hover {
+ box-shadow: var(--shadow-md);
+}
+
+.stat-card-value {
+ display: block;
+ font-size: 1.75rem;
+ font-weight: 700;
+ color: var(--color-heading);
+ line-height: 1.2;
+}
+
+.stat-card-label {
+ display: block;
+ font-size: 0.875rem;
+ color: var(--color-muted);
+ margin-top: 4px;
+}
+
+.stat-card-mittel {
+ border-top-color: var(--color-mittel);
+}
+
+.stat-card-hoch {
+ border-top-color: var(--color-hoch);
+}
+
+.stat-card-action {
+ background: var(--branding-primary);
+ border-color: var(--branding-primary);
+ display: flex;
+ align-items: center;
+ height: 100%;
+}
+
+.stat-card-action .stat-card-label {
+ color: #fff;
+ font-weight: 600;
+ font-size: 0.9375rem;
+ margin-top: 0;
+}
+
/* Ab hier mehr Platz (Tablet) — der Container bekommt spürbaren
Rand statt volle Breite, sonst bleibt alles identisch. */
@media (min-width: 640px) {
diff --git a/internal/web/templates/betreiber_werkzeuge.html b/internal/web/templates/betreiber_werkzeuge.html
index 8bfea07..3d3e1a8 100644
--- a/internal/web/templates/betreiber_werkzeuge.html
+++ b/internal/web/templates/betreiber_werkzeuge.html
@@ -5,7 +5,13 @@
{{template "nav" .Nav}}
← Plattform
- Werkzeugkatalog
+
| |