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

23 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

Exception (dokumentierte Abweichung vom UI-SPEC-Standardvokabular "2 Gewichte max"): Formular-Labels, Tabellen-Kopfzeilen und Buttons nutzen zusätzlich font-medium (500) auf 14px-Basis — z. B. <label className="text-sm font-medium text-foreground">. Das ist kein fünftes Gewicht, sondern dieselbe 500er-Stufe wie bei Labels/Badges, nur auf 14px statt 12px angewendet. Diese vier Größe-Gewicht-Paare sind das etablierte Tessera-Admin-Vokabular seit Phase 2 — der Planner/Executor übernimmt sie unverändert, erfindet 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

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: 9 covered, 3 backstop, 0 unresolved

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

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