--- phase: 15 slug: modul-berechtigungen-gruppen-user-grants status: draft shadcn_initialized: false preset: none created: 2026-08-04 --- # Phase 15 — UI Design Contract > Visual and interaction contract for frontend phases. Generated by gsd-ui-researcher, verified by gsd-ui-checker. --- ## Design System | Property | Value | |----------|-------| | Tool | none — kein `components.json`, keine shadcn-CLI im Repo (verifiziert: `find` liefert keinen Treffer) | | Preset | not applicable | | Component library | none — keine Radix-/Base-UI-Abhängigkeit im Projekt (`grep radix\|lucide\|cva\|clsx` in `apps/web/package.json` liefert nichts). Alle fünf bestehenden Admin-Seiten (`ldap`, `users`, `modules`, `tenants`, `smtp`) sind handgebaute Tailwind-Komponenten mit einem konsistenten, projekteigenen Vokabular (Tabellen, Modal-Dialoge, Toggle-Switches, Badge-Chips) | | Icon library | none — Icons sind handgeschriebene Inline-`` im Lucide-Stil (`viewBox="0 0 24 24"`, `stroke="currentColor"`, `strokeWidth="2"`, `strokeLinecap="round"`, `strokeLinejoin="round"`, kein `fill`), siehe `apps/web/src/components/admin/admin-sidebar.tsx` | | Font | Inter via `--font-sans` (`apps/web/src/app/globals.css:33`), system-ui-Fallback | **Shadcn-Gate-Entscheidung:** Kein `components.json` gefunden, Stack ist Next.js — der Gate würde normalerweise fragen, ob shadcn initialisiert werden soll. Diese Frage ist hier bereits beantwortet: der ``-Auftrag verlangt explizit "Reuse existing components and tokens; do not introduce a new component library or a second styling approach", und 14 vorherige Phasen haben ein durchgängiges, funktionierendes handgebautes System etabliert (5 Admin-Seiten, Marketplace, Modul-Shells). shadcn jetzt einzuführen würde einen zweiten, inkonsistenten Styling-Ansatz neben den bestehenden ~40 Seiten schaffen. **Tool bleibt `none`, Registry-Safety-Gate ist damit nicht anwendbar.** --- ## Spacing Scale Declared values (must be multiples of 4), identisch mit dem bereits im Projekt etablierten Raster (verifiziert in allen fünf Admin-Seiten): | Token | Value | Usage | |-------|-------|-------| | xs | 4px | Icon-Text-Abstand in Badges/Chips (`gap-1`), Checkbox-zu-Label-Feinabstand | | sm | 8px | Kompakte Element-Abstände (`gap-2`), Badge-Innenpolsterung (`px-2 py-0.5`) | | md | 16px | Standard-Formularabstand (`space-y-4`), Tabellenzellen-Polsterung (`px-4 py-3`) | | lg | 24px | Card-/Section-Innenpolsterung (`p-6`), Abstand zwischen Seitenabschnitten (`space-y-6`/`space-y-8`) | | xl | 32px | reserviert, in dieser Phase nicht benötigt | | 2xl | 48px | reserviert, in dieser Phase nicht benötigt | | 3xl | 64px | reserviert, in dieser Phase nicht benötigt | **Exceptions:** - Icon-only Aktionsbuttons in Tabellenzeilen (Standardgruppen-Stern-Toggle, Mitglied-Entfernen-Button) übernehmen das bestehende Projektmaß `px-2 py-1` (kein 44px-Touch-Target) — Admin-Oberflächen sind Desktop-Maus-Bedienung, identisch zu den bereits ausgelieferten "Bearbeiten"/"Löschen"-Buttons in `admin/users/page.tsx`. Kein neuer Präzedenzfall, reine Fortführung. - Matrix-Checkboxen (Permission-Matrix-Seite) nutzen die native ``-Rahmen ohne jeden Hinweis erscheint | | loading | Matrix-Zellen-Toggle, Gruppen-Modal-Speichern | ✅ covered | Identisches Inline-Spinner-Muster wie bestehender `isToggling`-Zustand in `AdminModulesPage` — kein neues Ladeverhalten | | error | Grant-Speicherfehler | ✅ covered | Dokumentierte Fehlermeldung im bestehenden `error`-Div-Muster | | error | Gruppe löschen schlägt fehl (z. B. Netzwerkfehler) | 🧪 backstop | Kein bestehendes Netzwerkfehler-Muster für Lösch-Dialoge im Projekt (aktuelle Lösch-Dialoge scheitern "silently" laut Code-Kommentar) — Planner muss entscheiden, ob dieser Präzedenzfall (stiller Fail) hier ebenfalls gilt oder eine sichtbare Fehlermeldung nötig ist, da PERM-01 konkrete Zahlen verspricht, die bei Fehlschlag nicht veralten dürfen | | populated | Matrix mit vielen Modulen/Gruppen | ✅ covered | Sticky Header + sticky erste Spalte + Kategorie-Gruppierung + Suchfeld, siehe Surface Contract 2 | | overflow | Lange Gruppennamen in der Matrix-Kopfzeile | 🧪 backstop | Kein expliziter Truncate-/Tooltip-Mechanismus dokumentiert — Planner sollte `truncate` + `title`-Attribut auf den Spaltenkopf anwenden, analog zum bestehenden `truncate`-Einsatz in `MarketplaceCard.tsx` (`h3.truncate`) | | zero-one-many | AD-Bindung: Gruppe ohne/mit gebundener AD-Gruppe | ✅ covered | Zwei explizite Zustände dokumentiert (ungebunden zeigt Auswahl-UI, gebunden zeigt Chip + Entfernen-Link) | | zero-one-many | Standardgruppe: keine/eine markierte Gruppe pro Mandant | ✅ covered | D-13 erzwingt DB-seitig genau 0 oder 1 — UI zeigt Stern-Toggle je Zeile, "Sofort freigeben" wird `disabled`, wenn keine Standardgruppe existiert (Surface Contract 6) | | long-text | Modulnamen in der Matrix-ersten-Spalte | ✅ covered | `sticky left-0`-Spalte erhält feste Mindestbreite + `truncate` mit `title`-Attribut, identisch zur Overflow-Behandlung in Zeile darüber | | partial | Matrix-Zelle (E2) und „Direkt"-Checkbox im User-Detail (E3), wenn der PATCH nach dem optimistischen Toggle fehlschlägt | ✅ covered | Die Checkbox springt auf den Serverzustand zurück, zusätzlich zur dokumentierten Fehlermeldung. Es darf keinen Zustand geben, in dem die Oberfläche eine Freigabe anzeigt, die in der Datenbank nicht existiert — das bestehende `toggleModule`-Muster in `AdminModulesPage` setzt beim Fehlerfall ebenfalls zurück | | partial | Gruppenliste (E1): Mitgliederzahl je Zeile | ✅ covered | Kein Teilzustand möglich — die Zahl kommt im selben Response wie die Gruppenzeile; es gibt keinen Nachlade-Pfad, der eine Zeile ohne Zahl rendern könnte | | partial | AD-Gruppensuche im Create/Rename-Modal (E1), wenn das LDAP nur einen Teil der Treffer liefert oder der Bind fehlschlägt | 🧪 backstop | Kein dokumentiertes Verhalten für Teilergebnisse der Gruppensuche. Verifikation prüft, dass die Ergebnisliste bei fehlgeschlagener oder unvollständiger Suche nicht stillschweigend leer erscheint, sondern den bestehenden Fehler-Div-Pfad aus `admin/ldap/page.tsx` nutzt | | partial | Sidebar und Dashboard-Grid (E7) | ✅ covered | Kein Teilzustand — beide erhalten vom Server die fertig gefilterte Liste; ein teilweise gefiltertes Ergebnis kann strukturell nicht entstehen | | empty · loading · error · populated · zero-one-many | Marketplace-Katalog (E5) in seinen Bestandszuständen | ✅ covered | Unverändert gegenüber dem ausgelieferten Marketplace — diese Phase ergänzt ausschließlich das Badge und den nicht-klickbaren Zustand einer einzelnen Karte, sie fasst weder Katalog-Laden, Katalog-Fehler noch Leerzustand an | | long-text · overflow | Marketplace-Karte (E5) mit langem Modulnamen plus zusätzlichem „Kein Zugriff"-Badge | 🧪 backstop | Die Badge-Reihe (`flex items-center gap-2`) wird durch das zweite Badge breiter; `h3.truncate` deckt den Titel ab, aber nicht die Badge-Reihe. Verifikation prüft, dass Status-Badge und „Kein Zugriff"-Badge bei schmaler Karte umbrechen statt aus der Karte zu laufen | | partial | Marketplace-Karte (E5): Karte gerendert, bevor der Freigabe-Status bekannt ist | 🧪 backstop | Nicht dokumentiert, ob Aktivierungs- und Freigabestatus im selben Response kommen. Verifikation prüft, dass keine Karte kurzzeitig ohne „Kein Zugriff"-Badge klickbar erscheint und dann nachträglich sperrt — im Zweifel Badge-Status serverseitig mitliefern | | empty · loading · error | 403-Seite (E4) und Aktivierungs-Dialog (E6) | ✅ covered | Beide sind reine Zustandsanzeigen ohne eigenen Datenabruf: die 403-Seite rendert serverseitig fertig, der Dialog arbeitet auf bereits geladenen Moduldaten. Fehler beim Aktivieren laufen über den bestehenden Fehler-Div-Pfad von `AdminModulesPage` | | long-text | Modulname im Aktivierungs-Dialog (E6) | 🧪 backstop | Der interpolierte Modulname steht in Titel und Fließtext des `max-w-sm`-Dialogs; kein Truncate dokumentiert. Verifikation prüft, dass ein langer Name den Dialog umbricht statt ihn zu verbreitern | --- ## Registry Safety | Registry | Blocks Used | Safety Gate | |----------|-------------|--------------| | shadcn official | keine — Tool ist `none` | not required | | Drittanbieter | keine | not required | Kein Registry-Zugriff in dieser Phase — alle Komponenten sind handgebaut nach den in "Surface Contracts" referenzierten, bereits im Repo vorhandenen Mustern. --- ## Checker Sign-Off - [ ] Dimension 1 Copywriting: PASS - [ ] Dimension 2 Visuals: PASS - [ ] Dimension 3 Color: PASS - [ ] Dimension 4 Typography: PASS - [ ] Dimension 5 Spacing: PASS - [ ] Dimension 6 Registry Safety: PASS **Approval:** pending