Files
tessera-ctl/.planning/phases/15-modul-berechtigungen-gruppen-user-grants/15-UI-SPEC.md
T
schalli 4f78999238
Tessera CI/CD / Lint & Type Check (push) Successful in 43s
Tessera CI/CD / Tests (push) Successful in 45s
Tessera CI/CD / Build & Publish Images (push) Successful in 7s
docs(15): UI design contract
2026-08-04 13:58:33 +02:00

29 KiB
Raw Blame History

phase, slug, status, shadcn_initialized, preset, created
phase slug status shadcn_initialized preset created
15 modul-berechtigungen-gruppen-user-grants draft false none 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-<svg> 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 <hard_constraints>-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 <input type="checkbox">-Größe ohne zusätzliches Padding, identisch zum bestehenden groupFilterDns-Checkbox-Muster in admin/ldap/page.tsx.

Typography

Übernommene, bereits im Projekt etablierte Werte (nicht neu erfunden — verifiziert aus allen fünf Admin-Seiten). Vier Größen und vier Gewichte sind bereits Teil des bestehenden Systems; diese Phase führt keine neue Größe und kein neues Gewicht ein, sondern verwendet ausschließlich die vorhandenen vier Kombinationen:

Role Size Weight Line Height
Label / Badge 12px (text-xs) 500 medium 1.4
Body / Table / Form 14px (text-sm) 400 regular (Fließtext, Tabellenzellen) 1.5
Section Heading 18px (text-lg) 600 semibold 1.3
Page Heading 24px (text-2xl) 700 bold 1.2

OWNER-APPROVED EXCEPTION — genehmigt am 2026-08-04 vom Projekt-Owner.

Die vier Gewichte 400 (Body) / 500 (Label, Badge) / 600 (Section Heading) / 700 (Page Heading) überschreiten das UI-SPEC-Standardlimit von 2 Gewichten. Diese Überschreitung ist explizit vom Projekt-Owner genehmigt, nicht nur durch Prosa-Argumentation begründet:

  • Scope der Ausnahme: genau diese vier Werte — 400/500/600/700 — sind vorbestehendes Projektvokabular, etabliert in Phase 2 und seither auf allen ausgelieferten Oberflächen (5 Admin-Seiten, Marketplace, Dashboard, alle Modul-Shells) im Einsatz. Phase 15 führt kein fünftes Gewicht ein.
  • Begründung: Phase 15 konsumiert ausschließlich die bestehende Skala, ohne sie zu erweitern. Eine Konsolidierung auf 2 Gewichte würde jede bereits ausgelieferte Seite umgestalten — projektweite Bestandsaufnahme zählt 192 font-medium-, 56 font-semibold- und 19 font-bold-Vorkommen über 70 Dateien in apps/web/src. Das ist ein Portal-weites Redesign und liegt außerhalb des Scopes einer Berechtigungs-Phase.
  • Keine Performance-Kosten: apps/web/src/app/globals.css:33 deklariert --font-sans: "Inter", system-ui, … ohne next/font und ohne @font-face — es werden keine Font-Dateien nachgeladen, die Gewichtsanzahl verursacht keine Ladekosten.
  • Bindende Einschränkung für Phase 15: die neuen Oberflächen dieser Phase dürfen kein Gewicht jenseits dieser vier einführen. Formular-Labels, Tabellen-Kopfzeilen und Buttons nutzen font-medium (500) auf 14px-Basis (z. B. <label className="text-sm font-medium text-foreground">) — dieselbe 500er-Stufe wie bei Labels/Badges, nur auf 14px statt 12px angewendet, kein neuer Wert. Planner/Executor übernehmen ausschließlich die vier genehmigten Größe-Gewicht-Paare, erfinden keine neue Kombination.

Color

Role Value Usage
Dominant (60%) --background / --card (OKLCH, hell 0.99 0 0 / 1 0 0, dunkel 0.23 0.01 260 / 0.27 0.01 260) Seitenhintergrund, Card-/Section-Flächen
Secondary (30%) --secondary / --muted / --sidebar (OKLCH 0.96 0 0 hell, 0.30 0.01 260 dunkel) Tabellenkopf (bg-muted/50), Admin-Sidebar, sekundäre Buttons (border border-border)
Accent (10%) --primary (OKLCH 0.91 0.19 102, gelb) ausschließlich: primäre CTA-Buttons ("Gruppe erstellen", "Sofort freigeben", "Speichern"), aktive Toggle-Switch-Füllung (Modul-Aktivierung, Matrix-Häkchen-Fokusring), aktiver Zustand im Admin-Sidebar-Nav-Item (sidebar-accent-foreground), Fokus-Ring (ring-ring)
Destructive --destructive (OKLCH 0.55 0.2 27, rot) ausschließlich: "Löschen"/"Entfernen"/"Freigabe entziehen"-Buttons, Bestätigungs-Button in Lösch-Dialogen, Fehlermeldungstext

Accent reserved for: Primär-Buttons (Gruppe erstellen, Speichern, Sofort freigeben), aktiver Toggle-Zustand, aktives Sidebar-Nav-Item, Fokus-Ring. Niemals für Badges, Tabellenzeilen-Hover oder informative Hinweise — dafür gilt die bestehende informelle Badge-Palette unten.

Bestehende informelle Badge-Palette (kein Token, aber projektweit etabliertes Muster — jede Instanz trägt Light- und Dark-Variante):

Badge-Zweck Klassen Bereits verwendet in
Positiv/Aktiv bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400 Modul-Status "Aktiviert", User-Status "Aktiv"
Neutral/Inaktiv bg-gray-100 text-gray-500 dark:bg-gray-800 dark:text-gray-500 bzw. bg-muted text-muted-foreground User-Status "Inaktiv", Marketplace "Verfügbar"
Info/System bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400 Rolle ADMIN, "System"-Modul-Badge
Kritisch (Rolle) bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400 Rolle SUPER_ADMIN
NEU in dieser Phase — Eingeschränkt bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400 Marketplace-Badge "Kein Zugriff" (D-08). Bewusst nicht destructive (rot) — es ist kein Fehlerzustand, sondern ein Informationszustand, konsistent mit der bestehenden Palette-Familie um eine fünfte, bisher ungenutzte Farbe erweitert (amber ist in Tailwinds Standardpalette bereits verfügbar, kein neues Token nötig)

Copywriting Contract

Element Copy
Primary CTA — Gruppenverwaltung "Gruppe erstellen" (admin.groups.create)
Primary CTA — Matrix-Zugriff "Freigaben-Matrix" (Link/Button auf /admin/modules, adminModules.grantsLink)
Primary CTA — Modul-Aktivierung mit Grant "Sofort freigeben" (adminModules.activationDialog.grantNow)
Sekundär-CTA — Modul-Aktivierung ohne Grant "Später konfigurieren" (adminModules.activationDialog.configureLater)
Empty state heading — Gruppenliste "Keine Gruppen vorhanden" (admin.groups.noGroups)
Empty state body — Gruppenliste "Lege deine erste Gruppe an, um Modulzugriff gezielt zu vergeben." (admin.groups.noGroupsBody)
Empty state — Matrix ohne aktive Module "Es sind noch keine Module für diesen Mandanten aktiviert. Aktiviere zuerst ein Modul unter Module." (adminModules.grants.emptyModules, mit Link zu /admin/modules)
Empty state — User-Detail ohne Gruppenmitgliedschaft "Dieser Benutzer ist keiner Gruppe zugeordnet." (admin.users.grants.noGroups)
Error state — Grant-Speicherfehler "Freigabe konnte nicht gespeichert werden. Bitte erneut versuchen." + Statuscode-Anzeige wie im bestehenden adminModules-Fehlermuster (error-Div mit border-destructive/50 bg-destructive/10)
403-Seite Titel (D-07) "Kein Zugriff auf dieses Modul" (modules.accessDenied.title)
403-Seite Body (D-07) "Du hast für dieses Modul keine Freigabe. Wende dich an deinen Administrator." (modules.accessDenied.body) — Formulierung ist durch D-07 wörtlich vorgegeben, keine Umformulierung
403-Seite Rücklink "Zur Startseite" → / (Dashboard), nicht "Zurück zur Kategorie" — der Benutzer hat für dieses Modul keinen Zugriff, ein Rücksprung in die Modul-Kategorie ist kein sinnvoller nächster Schritt (modules.accessDenied.backToDashboard)
Marketplace-Badge "Kein Zugriff" (D-08) "Kein Zugriff" (marketplace.statusNoAccess)
Marketplace-Klick-Feedback bei gesperrter Karte Toast: "Kein Zugriff auf dieses Modul — wende dich an deinen Administrator." (identischer Wortlaut wie 403-Seite, marketplace.toastNoAccess) — nutzt die bestehende Toast.tsx-Komponente
Destructive confirmation — Gruppe löschen (D-17, PERM-01) Titel: "Gruppe löschen"; Body: "Diese Gruppe hat {memberCount} Mitglieder und {grantCount} Modul-Freigaben. Betroffene Benutzer verlieren den Zugriff, sofern sie ihn nicht anderweitig haben. Diese Aktion kann nicht rückgängig gemacht werden." (admin.groups.deleteConfirm.title / .body, Zahlen als next-intl-Interpolationsparameter, keine generische Warnung ohne Zahlen)
Destructive confirmation — Freigabe entziehen (Matrix/User-Detail) Kein Dialog nötig — Häkchen-Toggle ist sofort wirksam und sofort umkehrbar (kein Datenverlust, reine Klick-Umkehrung), analog zum bestehenden Modul-Aktivierungs-Toggle-Verhalten. Kein Bestätigungsdialog für einzelne Grant-Toggles.
Destructive confirmation — Mitglied aus Gruppe entfernen Kein Dialog — sofortiger Klick-Entfernen-Button analog zum bestehenden userExcludeList-Chip-Entfernen-Muster (kein irreversibler Datenverlust, Mitglied kann sofort wieder hinzugefügt werden)
AD-Bindung — Bindungs-Auswahl-Hinweis "Wähle eine AD-Gruppe aus der Liste, um die Mitgliedschaft automatisch zu synchronisieren." (admin.groups.ldapBind.hint)
AD-Bindung — bereits gebunden "Gebunden an: {ldapDn}" mit "Bindung entfernen"-Link (admin.groups.ldapBind.bound / .unbind)
Standardgruppen-Markierung Icon-Button-Tooltip/aria-label: "Als Standardgruppe festlegen" bzw. "Standardgruppe" (wenn bereits gesetzt) — admin.groups.setDefault / admin.groups.isDefault
Mitglied entfernen (Icon-only, Mitglieder-Modal) Icon-Button-Tooltip/aria-label: "Mitglied entfernen" — admin.groups.members.remove. Gilt nur für MANUAL-Mitgliedschaften (Button ist deaktiviert bei LDAP-Herkunft, siehe Surface Contract 1); der deaktivierte Zustand trägt zusätzlich aria-label/Tooltip "Wird über AD-Sync verwaltet" — admin.groups.members.ldapManaged
Matrix-Häkchen ohne sichtbaren Text (Permission-Matrix) aria-label pro Checkbox, dynamisch zusammengesetzt: "{Modulname} für Gruppe {Gruppenname} freigeben" bzw. "… entziehen" je nach Zustand — admin.groups.grants.matrixCheckboxLabel (Modul- und Gruppenname als next-intl-Interpolationsparameter, da die Zelle selbst keinen sichtbaren Text trägt — nur Zeilen-/Spaltenkopf)
User-Detail "Direkt"-Checkbox (User-Detail-Modul-Zugriff) aria-label pro Checkbox: "{Modulname} direkt für {Benutzername} freigeben" bzw. "… entziehen" — admin.users.grants.directCheckboxLabel (gleiches Muster wie Matrix-Checkbox, da auch hier kein sichtbarer Zellentext existiert)

Surface Contracts

1. /admin/groups — Gruppenverwaltung (PERM-01, D-14, D-17)

Sechster Eintrag im AdminSidebar (apps/web/src/components/admin/admin-sidebar.tsx), zwischen Module und LDAP oder ans Ende — Reihenfolge liegt bei Executor, Icon muss sich vom bestehenden admin.users-Personen-Icon unterscheiden (Vorschlag: ein Roster-/Listen-Icon, 24×24, stroke-width 2, kein Fill — kein bestehendes Icon im Projekt duplizieren).

Seitenaufbau identisch zu admin/users/page.tsx:

  • h1 "Gruppen" (24px/700) + primärer Button "Gruppe erstellen" rechtsbündig im Header (identisches Markup wie openCreate-Button in AdminUsersPage).
  • Tabelle (rounded-md border border-border, thead bg-muted/50, tbody divide-y divide-border) mit Spalten: Name, AD-Bindung (Badge "AD-gebunden" info-blau wenn ldapDn gesetzt, sonst Badge "Manuell" neutral-grau), Standardgruppe (Stern-Icon-Toggle-Button, gefüllt wenn isDefault), Mitglieder (Zahl, {count}), Aktionen (Bearbeiten · Mitglieder · Löschen als drei Text-Buttons im bestehenden px-2 py-1 text-xs-Stil).
  • Create/Rename-Modal: identisches Overlay-Muster (fixed inset-0 bg-black/50 → max-w-md rounded-lg border bg-card p-6 shadow-lg) wie das User-Create-Modal. Feld "Name" (Pflicht), darunter Abschnitt "AD-Bindung (optional)": Suchfeld + Ergebnisliste im 1:1-Muster der bestehenden groupFilter.discover-Sektion aus admin/ldap/page.tsx (Zeilen 705–746), aber mit Radio-Auswahl statt Checkbox-Mehrfachauswahl (genau eine AD-Gruppe pro Tessera-Gruppe, D-05).
  • Mitglieder-Modal (separater Dialog, Button "Mitglieder" aus der Tabellenzeile): zwei Abschnitte — (a) aktuelle Mitglieder als Chip-Liste mit Herkunfts-Badge (MANUAL/LDAP) und Entfernen-Button, identisches Chip-Markup wie userExcludeList in admin/ldap/page.tsx (Zeilen 957–974); LDAP-Mitgliedschaften zeigen keinen Entfernen-Button (D-19: nur der Sync entfernt sie) — stattdessen ein deaktivierter Zustand mit Tooltip "Wird über AD-Sync verwaltet". (b) Suchfeld + Checkbox-Liste bestehender Tenant-Benutzer zum manuellen Hinzufügen, identisches Markup wie die userSearch-Sektion (Zeilen 841–877), Ziel ist hier aber GroupMembership, nicht LDAP-Import.
  • Lösch-Dialog: identisches Overlay-Muster wie deleteConfirm in AdminUsersPage, aber mit den unter Copywriting Contract festgelegten konkreten Zahlen (D-17) statt der generischen Ein-Satz-Warnung.

2. Permission-Matrix — Module × Gruppen (PERM-03, D-15)

Erreichbar über einen Button "Freigaben-Matrix" im Header von /admin/modules (kein siebter Sidebar-Eintrag — bleibt Unterseite von Modules, analog zu dkv-fleet/settings). Route: /admin/modules/grants.

  • Suchfeld oben (filtert Modul- und Gruppennamen client-seitig), identisches Eingabefeld-Markup wie discoverSearch.
  • Tabelle mit sticky erster Spalte (Modulname, sticky left-0 bg-card z-10) und sticky Kopfzeile (Gruppennamen, sticky top-0 bg-card z-10) in einem overflow-x-auto overflow-y-auto max-h-[70vh] rounded-md border border-border-Container — löst die Claude's-Discretion-Frage "Scrollverhalten bei vielen Modulen/Gruppen" aus RESEARCH.md.
  • Zeilen nach Modul-Kategorie gruppiert: volle-Breite-Zwischenzeile bg-muted/50 mit Kategorienamen (identisch zum bestehenden Kategorie-Badge-Text aus AdminModulesPage), darunter die Modulzeilen dieser Kategorie.
  • Jede Zelle: natives <input type="checkbox">, sofortiges Toggle bei Klick (optimistisches UI, PATCH im Hintergrund — identisches Verhalten zum bestehenden Modul-Aktivierungs-Switch in AdminModulesPage.toggleModule), kleiner Inline-Spinner nur in der aktiven Zelle während des Requests (kein Vollseiten-Ladezustand).
  • Fußnote unter der Tabelle (12px, text-muted-foreground): "ADMIN und SUPER_ADMIN haben immer Zugriff auf alle aktiven Module — diese Matrix betrifft nur die Rolle USER." (D-03 transparent machen, verhindert Verwirrung, warum Admin-Accounts hier nicht auftauchen).
  • Leerer Zustand (kein aktives Modul im Mandanten): siehe Copywriting Contract "Empty state — Matrix ohne aktive Module".

3. User-Detail-Grants in /admin/users (PERM-03, D-16)

AdminUsersPage bekommt pro Zeile einen vierten Aktions-Button "Details" (neben Bearbeiten/Löschen, gleicher px-2 py-1 text-xs-Stil), der ein breiteres Modal öffnet (max-w-2xl statt max-w-md — mehr Inhalt als das einfache Edit-Formular, aber weiterhin das etablierte Overlay-Muster, kein neues Slide-over-Primitiv).

Modal-Inhalt, Titel "{username} — Zugriff":

  1. Abschnitt "Gruppenmitgliedschaften" — read-only Chip-Liste (identisches Chip-Markup wie userExcludeList, aber ohne Entfernen-Button — Bearbeitung geschieht ausschließlich unter /admin/groups, keine zweite CRUD-Oberfläche für dieselbe Beziehung). Leerzustand siehe Copywriting Contract.
  2. Abschnitt "Modul-Zugriff" — Tabelle mit Spalten: Modul | Über Gruppe(n) (Chip-Liste der Gruppennamen, die dieses Modul gewähren, oder "–") | Direkt (Checkbox-Toggle für einen direkten ModuleGrant, identisches Sofort-Toggle-Verhalten wie die Matrix). Nur mandantenweit aktive Module werden gelistet — inaktive Module sind hier nicht wählbar, da ein Grant ohne Aktivierung wirkungslos ist (D-02).

4. 403-Seite für gesperrtes Modul (PERM-04, D-07)

apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/page.tsx wird zur async Server Component (Umbau lt. RESEARCH.md Pattern 5); der bisherige Client-Inhalt wandert unverändert in eine neue module-shell.tsx-Client-Component.

Visuelles Muster identisch zum bestehenden "not found"-Zustand derselben Datei (Zeilen 30–65: zentrierter Block, rounded-lg bg-muted p-4-Icon-Container, h2 18px/600, p 14px muted, Link darunter) — nur Icon und Copy ändern sich:

  • Icon: Schloss-Symbol (lock, nicht das bestehende Kreis-mit-Schrägstrich-"nicht gefunden"-Icon — visuelle Unterscheidung zwischen "existiert nicht" und "gesperrt" ist bewusst, verhindert Verwechslung der beiden Zustände).
  • Titel/Body: siehe Copywriting Contract, wörtlich D-07.
  • Kein notFound(), kein Redirect (D-07 explizit) — die Seite rendert das 403-Markup direkt als Server-Response.
  • Rücklink führt zur Startseite (/), nicht zur Modul-Kategorie (siehe Copywriting Contract Begründung).

5. Marketplace-Katalog — "Kein Zugriff"-Badge (PERM-04, D-08)

MarketplaceCard.tsx bekommt einen dritten Status neben isActive/!isActive: hasGrant (nur relevant, wenn isActive === true und Rolle USER). Wenn isActive && !hasGrant:

  • Zusätzliches Badge "Kein Zugriff" (amber, siehe Color-Tabelle) direkt neben dem bestehenden Status-Badge in derselben Badge-Reihe (flex items-center gap-2).
  • Karte bekommt opacity-60 cursor-not-allowed auf dem äußeren div (kein hover:shadow-md/hover:border-primary/30 in diesem Zustand — visuelles Feedback, dass die Karte nicht interaktiv ist).
  • Klick auf die Karte navigiert nicht zum Moduldetail, sondern löst den Toast "Kein Zugriff …" aus (bestehende Toast.tsx-Komponente aus dem Marketplace-Ordner wiederverwenden, kein neues Toast-System).
  • ADMIN/SUPER_ADMIN sehen dieses Badge nie (Bypass, D-03) — für sie ist jede aktivierte Karte normal klickbar.

6. Modul-Aktivierungs-Dialog in /admin/modules (PERM-03, D-10)

AdminModulesPage.toggleModule verzweigt: Deaktivierung bleibt unverändert (bestehendes ActivationDialog.tsx-Warnmuster, falls vorhanden/nachgezogen). Aktivierung öffnet neu ein Dialog im identischen Overlay-Stil wie ActivationDialog.tsx (fixed inset-0 bg-black/50 → max-w-sm rounded-lg border bg-card p-6 shadow-lg), aber mit drei Aktionen statt zwei:

  • Titel: "Modul aktivieren" + Modulname interpoliert.
  • Body: Frage, ob sofort für die Standardgruppe freigegeben werden soll (siehe D-10-Wortlaut in 15-CONTEXT.md).
  • Buttons, von links nach rechts: "Abbrechen" (Border-Button, bricht komplett ab, Modul bleibt inaktiv) — "Später konfigurieren" (Border-Button, aktiviert das Modul OHNE Grant, Admin konfiguriert Freigaben separat über Matrix/Gruppen) — "Sofort freigeben" (Primary-Button bg-primary, aktiviert das Modul UND erzeugt einen ModuleGrant für die als Standard markierte Gruppe des Mandanten).
  • Falls der Mandant keine als Standard markierte Gruppe besitzt (D-13: Markierung kann abgeschaltet sein): "Sofort freigeben" ist disabled mit Hinweistext darunter ("Keine Standardgruppe markiert — lege zuerst eine unter Gruppen fest oder konfiguriere die Freigabe manuell.").

7. Sidebar (apps/web/src/components/layout/sidebar.tsx)

Explizit keine visuelle Änderung. Die Komponente fetcht weiterhin GET /modules/active unverändert und rendert exakt dasselbe Markup — der Endpoint liefert nach dieser Phase serverseitig weniger Module für nicht-freigegebene Benutzer, das ist reine Datenänderung, keine UI-Änderung. Kein neuer Sidebar-Zustand ("gesperrtes Modul ausgegraut anzeigen" o. ä.) — nicht freigegebene Module fehlen schlicht in der Liste, identisch zum bestehenden Verhalten für nicht-aktivierte Module.

Dashboard-Widgets (PERM-07, D-22)

Ebenfalls keine neue UI-Komponente: DashboardService.getWidgets() filtert serverseitig, das Frontend rendert nur die gefilterte Liste — identisch zum bestehenden Grid-Rendering-Pfad. Kein Platzhalter/"gesperrtes Widget"-Zustand, das Widget verschwindet ersatzlos (D-22 verlangt kein Hinweis-Kachel).


UI Considerations

Applicable state considerations resolved: 17 covered, 5 backstop, 0 unresolved

Quelle: ui-consideration-probe.cjs über sieben Surfaces (E1 /admin/groups, E2 Permission-Matrix, E3 User-Detail-Modal, E4 403-Seite, E5 Marketplace-Karte, E6 Aktivierungs-Dialog, E7 Sidebar + Dashboard-Grid). Der Prosa-Klassifikator ordnete E5 zunächst als unclassified ein; nach manueller Einordnung als list-collection + interactive-control + static-content wurden dessen acht Kategorien nachgezogen (Propose-then-confirm, ui-phase §9.5).

Category Element(s) Status Resolution / Reason
empty Gruppenliste (/admin/groups) ✅ covered Leerer Zustand rendert die dokumentierte "Keine Gruppen vorhanden"-Copy mit CTA, identisches Muster zu AdminUsersPage's noUsers-Zustand
empty Permission-Matrix ohne aktive Module ✅ covered Dokumentierte Empty-State-Copy mit Link zu /admin/modules
empty User-Detail ohne Gruppenmitgliedschaft ✅ covered Dokumentierte Empty-State-Copy, Chip-Liste einfach leer
empty User-Detail-Modul-Zugriff-Tabelle, wenn Mandant keine aktiven Module hat 🧪 backstop Kein explizit dokumentierter Text — Tabelle rendert 0 Zeilen; Verifikation prüft, dass kein leerer <table>-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