docs(04): UI design contract for marketplace & portal navigation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-22 13:53:44 +02:00
parent 7fb0ed6646
commit 4ea1b6144f
@@ -0,0 +1,445 @@
---
phase: 4
slug: marketplace-portal-navigation
status: draft
shadcn_initialized: false
preset: none
created: 2026-06-22
---
# Phase 4 — UI Design Contract
> Visual and interaction contract for Marketplace & Portal Navigation. Generated by gsd-ui-researcher, verified by gsd-ui-checker.
---
## Design System
| Property | Value |
|----------|-------|
| Tool | none (hand-rolled components with shadcn-compatible CSS variable naming) |
| Preset | not applicable |
| Component library | none (custom components, Radix-compatible token structure) |
| Icon library | Inline SVG (project convention from Phase 1-3, no external icon library) |
| Font | Inter, system-ui, -apple-system, sans-serif (declared as `--font-sans` in globals.css) |
**Note:** The project uses shadcn-compatible CSS custom property naming (`--primary`, `--card`, `--muted`, etc.) and Tailwind v4 `@theme inline` mapping, but does not have `components.json`. All UI components are hand-rolled. This phase continues that pattern.
---
## Spacing Scale
Declared values (must be multiples of 4):
| Token | Value | Usage |
|-------|-------|-------|
| xs | 4px | Icon gaps, inline badge padding, chip internal spacing |
| sm | 8px | Compact element spacing, card internal gaps, filter chip gaps |
| md | 16px | Default element spacing, card padding, search bar padding |
| lg | 24px | Section padding, space between marketplace header and grid |
| xl | 32px | Layout gaps, space between major page sections |
| 2xl | 48px | Empty state vertical padding, page top/bottom padding |
| 3xl | 64px | Not used in this phase |
Exceptions: Sidebar search input height at 36px (not a spacing token — component-specific dimension for comfortable text input within narrow 240px sidebar). Marketplace card minimum height at 180px for visual consistency in the grid.
---
## Typography
| Role | Size | Weight | Line Height | Usage in Phase 4 |
|------|------|--------|-------------|-------------------|
| Body | 14px | 400 (regular) | 1.5 | Card descriptions, sidebar module names, filter labels |
| Label | 12px | 600 (semibold) | 1.4 | Category badges, status badges, sidebar section headers, version text |
| Heading | 20px | 700 (bold) | 1.2 | Page titles ("Marktplatz"), section headings |
| Display | 28px | 700 (bold) | 1.2 | Not used in this phase (reserved for Dashboard phase) |
**Font weights used:** 400 (regular) and 600 (semibold). 700 (bold) only for page headings per existing codebase convention (`text-2xl font-bold` in admin pages).
---
## Color
All values in OKLCH (project convention from Phase 1, defined in `globals.css`).
| Role | Light Mode | Dark Mode | Usage |
|------|-----------|-----------|-------|
| Dominant (60%) | `oklch(0.99 0 0)` — near-white | `oklch(0.23 0.01 260)` — dark gray-blue | Page background, marketplace grid background |
| Secondary (30%) | `oklch(1 0 0)` / `oklch(0.96 0 0)` — white/light gray | `oklch(0.27 0.01 260)` / `oklch(0.30 0.01 260)` — dark card/muted | Marketplace cards (`bg-card`), sidebar (`bg-sidebar`), filter bar background, tenant selector dropdown |
| Accent (10%) | `oklch(0.91 0.19 102)` — Tessera yellow | `oklch(0.91 0.19 102)` — same yellow | See reserved-for list below |
| Destructive | `oklch(0.55 0.2 27)` — red | `oklch(0.55 0.2 27)` — same red | Deactivation confirmation dialog only |
| Success | `oklch(0.55 0.16 145)` — green | `oklch(0.55 0.16 145)` — green | "Aktiviert" status badge, activation success toast |
**Accent reserved for:**
1. "Aktivieren" / "Modul aktivieren" primary CTA button background
2. Active module card border highlight on hover (`hover:border-primary/30` — existing pattern)
3. Active sidebar item background accent (`bg-sidebar-accent`)
4. Marketplace search input focus ring (`ring-ring` maps to primary)
5. Tenant context selector active state indicator
6. Toggle switch active state (existing pattern from admin/modules)
**Accent explicitly NOT used for:**
- Category badges (use `bg-muted` with `text-muted-foreground`)
- Status text (use semantic green/red)
- Card backgrounds (use `bg-card`)
---
## Component Inventory
### New Components (Phase 4)
| Component | Location | Purpose |
|-----------|----------|---------|
| `MarketplaceCard` | `apps/web/src/app/(portal)/marketplace/components/MarketplaceCard.tsx` | Module card for marketplace grid — extends ModuleCard pattern with activation toggle and status badge |
| `MarketplaceSearch` | `apps/web/src/app/(portal)/marketplace/components/MarketplaceSearch.tsx` | Search input with live filter, debounced 300ms |
| `CategoryFilter` | `apps/web/src/app/(portal)/marketplace/components/CategoryFilter.tsx` | Horizontal chip/pill row for category filtering |
| `StatusFilter` | `apps/web/src/app/(portal)/marketplace/components/StatusFilter.tsx` | Tab bar: "Alle" / "Aktiviert" / "Verfuegbar" for filtering by activation status |
| `TenantContextSelector` | `apps/web/src/app/(portal)/marketplace/components/TenantContextSelector.tsx` | Dropdown for Super-Admin to switch tenant context in marketplace |
| `ModuleDetailView` | `apps/web/src/app/(portal)/marketplace/[slug]/page.tsx` | Compact detail page for a single module |
| `ActivationDialog` | `apps/web/src/app/(portal)/marketplace/components/ActivationDialog.tsx` | Confirmation dialog for deactivation only (activation is immediate with undo toast) |
| `SidebarSearch` | `apps/web/src/components/layout/sidebar-search.tsx` | Inline search/filter field within sidebar categories section |
### Reused Components (from Phase 1-3)
| Component | Source | Reuse |
|-----------|--------|-------|
| `ModuleCard` | `apps/web/src/app/(portal)/modules/[category]/components/ModuleCard.tsx` | Pattern reference for MarketplaceCard — same card structure, extended with activation controls |
| `Sidebar` | `apps/web/src/components/layout/sidebar.tsx` | Extended with SidebarSearch and individual module links per category |
| `Header` | `apps/web/src/components/layout/header.tsx` | Unchanged — breadcrumb updates for marketplace route |
| `AppShell` | `apps/web/src/components/layout/app-shell.tsx` | Unchanged — marketplace pages render within existing shell |
---
## Layout Specifications
### Marketplace Page (`/marketplace`)
```
+----------------------------------------------------------+
| Header (sticky, 60px) |
+--------+-------------------------------------------------+
| Sidebar | Marketplace Page |
| 240px | |
| | [Tenant Selector - Super-Admin only] |
| | |
| | Marktplatz (h1, 20px bold) |
| | Entdecken Sie verfuegbare Module (14px muted) |
| | |
| | [Search input, full width] |
| | [StatusTabs: Alle | Aktiviert | Verfuegbar] |
| | [CategoryChips: Alle | Domain-Tools | Utils...] |
| | |
| | +--------+ +--------+ +--------+ |
| | | Card 1 | | Card 2 | | Card 3 | |
| | +--------+ +--------+ +--------+ |
| | +--------+ +--------+ +--------+ |
| | | Card 4 | | Card 5 | | Card 6 | |
| | +--------+ +--------+ +--------+ |
+--------+-------------------------------------------------+
```
**Grid responsive breakpoints:**
- Mobile (<768px): 1 column, sidebar hidden
- Tablet (768px-1023px): 2 columns, sidebar collapsed or hidden
- Desktop (1024px-1279px): 3 columns, sidebar expanded
- Wide (1280px+): 4 columns, sidebar expanded
**Grid gap:** 16px (`gap-4`)
### MarketplaceCard Anatomy
```
+------------------------------------------+
| [Icon 24x24] Module Name |
| Category Badge Status Badge|
| |
| Localized description text, max 2 lines |
| with line-clamp-2 truncation. |
| |
| v1.0.0 [Aktivieren] (button) |
+------------------------------------------+
```
- Card padding: 20px (`p-5`)
- Card border: `border border-border` (1px)
- Card background: `bg-card`
- Card border-radius: 8px (`rounded-lg`)
- Card hover: `hover:shadow-md hover:border-primary/30` (existing pattern)
- Icon container: `rounded-md bg-muted p-2.5` (40px total, existing pattern)
- Status badge (activated): `bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400` rounded-full px-2 py-0.5 text-xs
- Status badge (available): `bg-muted text-muted-foreground` rounded-full px-2 py-0.5 text-xs
- Category badge: `bg-muted text-muted-foreground` rounded-full px-2 py-0.5 text-xs
- Activation button (activate): `bg-primary text-primary-foreground` rounded-md px-3 py-1.5 text-sm font-medium
- Activation button (deactivate): `border border-border text-foreground hover:bg-muted` rounded-md px-3 py-1.5 text-sm font-medium
### Module Detail Page (`/marketplace/[slug]`)
Compact detail view (appropriate for v1 module count). Single-column layout within main content area.
```
+------------------------------------------+
| < Zurueck zum Marktplatz |
| |
| [Icon 48x48] |
| Module Name (h1, 20px bold) |
| Category Badge v1.0.0 |
| |
| Full localized description text. |
| No line clamping. Renders full text. |
| |
| Status: Aktiviert / Nicht aktiviert |
| [Aktivieren / Deaktivieren] (button) |
+------------------------------------------+
```
- Back link: `text-sm text-muted-foreground hover:text-foreground` with left chevron icon
- Icon container: `rounded-lg bg-muted p-4` (56px total) — larger than card icon
- Max content width: `max-w-2xl` (672px)
### Sidebar Enhancement
```
Sidebar (240px expanded):
+----------------------------------+
| [Dashboard icon] Dashboard |
| [Cart icon] Marktplatz |
+----------------------------------+
| [Search input _______________] | <-- NEW: SidebarSearch
+----------------------------------+
| KATEGORIEN [chevron] |
| [Folder] Domain Tools (1) |
| - Domaincheck |
| [Folder] Utilities (2) |
| - Tool A |
| - Tool B |
+----------------------------------+
| VERWALTUNG |
| [Users icon] Benutzer |
| [Home icon] Mandanten | (Super-Admin only)
| [Box icon] Module |
| [Layers icon] LDAP |
+----------------------------------+
| [Collapse] |
| [User Info] |
+----------------------------------+
Sidebar (64px collapsed):
+--------+
| [Dash] |
| [Cart] |
+--------+
| [Usr] |
| [Home] | (Super-Admin only)
| [Box] |
| [Lyrs] |
+--------+
| [<] |
| [Avtr] |
+--------+
```
**Sidebar Search (SidebarSearch):**
- Position: Between main nav (Dashboard/Marketplace) and Categories section
- Height: 36px
- Padding: 8px horizontal within sidebar's 12px padding (`px-3` within `p-3` nav)
- Placeholder text: "Module suchen..." (14px, muted-foreground)
- Border: `border border-border rounded-md`
- Focus: `focus:ring-2 focus:ring-ring focus:border-transparent`
- Behavior: Filters visible module names and category names in real-time. Categories with no matching modules are hidden.
- Collapsed sidebar: Search field hidden (not enough space at 64px)
**Sidebar collapsed mode decision:** Show main navigation icons (Dashboard, Marketplace) and admin icons. Do NOT show individual module icons -- category/module list is hidden when collapsed. This keeps the collapsed sidebar clean and avoids icon overload.
### Tenant Context Selector
```
+------------------------------------------+
| Mandanten-Kontext: [Dropdown v] |
| +--------------------------------------+ |
| | Firma A GmbH [x] | | <-- currently selected
| | Firma B AG | |
| | Test-Mandant | |
| +--------------------------------------+ |
+------------------------------------------+
```
- Position: Top of marketplace page, above title, only visible for `SUPER_ADMIN` role
- Container: `bg-muted/50 rounded-lg p-3 mb-4 border border-border`
- Label: "Mandanten-Kontext:" 12px semibold text-muted-foreground
- Dropdown: Native `<select>` styled with `bg-card border border-border rounded-md px-3 py-1.5 text-sm`
- Width: auto (fits content), min-width 200px
- Behavior: Changing tenant re-fetches all module activation states. Shows loading spinner during fetch.
---
## Interaction Contracts
### Marketplace Filtering
**Status Tabs (Claude's Discretion decision: Tab bar + badge combination):**
- Three tabs: "Alle", "Aktiviert", "Verfuegbar"
- Tab style: underline indicator, `text-sm font-medium`
- Active tab: `text-foreground border-b-2 border-primary`
- Inactive tab: `text-muted-foreground hover:text-foreground`
- Tab counts shown as inline badge: `(3)` in muted text after label
- Reason for tabs over pure badges: Clear top-level filtering for the most important dimension (activated vs. not). Badges on cards provide per-card status, tabs provide batch filtering.
**Category Chips:**
- Horizontal scrollable row of pill buttons below status tabs
- First chip: "Alle" (always present, default selected)
- Remaining chips: One per unique category from module list
- Selected: `bg-primary text-primary-foreground`
- Unselected: `bg-muted text-foreground hover:bg-muted/80`
- Chip size: `px-3 py-1 text-xs font-medium rounded-full`
- Gap between chips: 8px
- Overflow: horizontal scroll with `overflow-x-auto` and hidden scrollbar
**Search:**
- Full-width input above tabs
- Debounce: 300ms
- Filters module name and description (client-side, all modules loaded)
- Clear button (X icon) appears when text is entered
- Empty search shows all modules (respecting active tab + category filter)
### Module Activation Flow
**Activation (Claude's Discretion decision: Immediate action with success toast):**
1. User clicks "Aktivieren" button on card or detail page
2. Button shows loading spinner (replace text with spinner, button disabled)
3. POST `/modules/{id}/activate` fires
4. On success: Button changes to "Deaktivieren" (outline style), status badge updates to green "Aktiviert", success toast appears: "Modul erfolgreich aktiviert"
5. On error: Error toast: "Aktivierung fehlgeschlagen. Bitte versuchen Sie es erneut."
6. Sidebar refreshes active modules list (re-fetch `/modules/active`)
**Deactivation (Claude's Discretion decision: Confirmation dialog for deactivation only):**
1. User clicks "Deaktivieren" button
2. Confirmation dialog appears (modal overlay):
- Title: "Modul deaktivieren"
- Body: "Moechten Sie {moduleName} fuer diesen Mandanten deaktivieren? Das Modul wird aus der Seitenleiste entfernt."
- Cancel button: "Abbrechen" (outline style)
- Confirm button: "Deaktivieren" (destructive style: `bg-destructive text-destructive-foreground`)
3. On confirm: POST `/modules/{id}/deactivate`, button/badge update, sidebar refresh
4. On cancel: Dialog closes, no action
**Reason for asymmetric UX:** Activation is low-risk and easily reversible. Deactivation could disrupt users who depend on the module, so a confirmation prevents accidental removal.
### Sidebar Module Navigation
**Clicking a module in sidebar:**
1. Navigates to `/modules/{category}/{slug}` (existing route from Phase 3)
2. Module opens in main content area within AppShell
3. Sidebar highlights the active module with `bg-sidebar-accent text-sidebar-accent-foreground`
**Sidebar active state:** Current page URL determines which sidebar item gets the active class. Compare `pathname` against link `href`.
### Toast Notifications
- Position: Bottom-right of viewport
- Duration: 4000ms auto-dismiss
- Style: `bg-card border border-border shadow-lg rounded-lg p-4`
- Success icon: Green checkmark
- Error icon: Red X circle
- Max visible: 3 stacked, newest on top
- Animation: Slide in from right, fade out
---
## Copywriting Contract
All copy in both DE and EN via next-intl. Below shows DE as primary, EN in parentheses.
| Element | DE Copy | EN Copy |
|---------|---------|---------|
| Page title | "Marktplatz" | "Marketplace" |
| Page subtitle | "Entdecken und aktivieren Sie Module fuer Ihren Mandanten" | "Discover and activate modules for your tenant" |
| Primary CTA | "Modul aktivieren" | "Activate Module" |
| Deactivation CTA | "Deaktivieren" | "Deactivate" |
| Search placeholder | "Module suchen..." | "Search modules..." |
| Sidebar search placeholder | "Module suchen..." | "Search modules..." |
| Status tab: All | "Alle" | "All" |
| Status tab: Active | "Aktiviert" | "Activated" |
| Status tab: Available | "Verfuegbar" | "Available" |
| Category chip: All | "Alle" | "All" |
| Empty state heading | "Noch keine Module verfuegbar" | "No modules available yet" |
| Empty state body | "Module werden vom Systemadministrator bereitgestellt. Schauen Sie spaeter noch einmal vorbei." | "Modules are provided by the system administrator. Check back later." |
| Empty state (filtered) heading | "Keine Ergebnisse" | "No results" |
| Empty state (filtered) body | "Versuchen Sie einen anderen Suchbegriff oder aendern Sie die Filter." | "Try a different search term or adjust your filters." |
| Error state | "Module konnten nicht geladen werden. Bitte laden Sie die Seite neu." | "Could not load modules. Please reload the page." |
| Activation success toast | "Modul erfolgreich aktiviert" | "Module activated successfully" |
| Deactivation success toast | "Modul erfolgreich deaktiviert" | "Module deactivated successfully" |
| Activation error toast | "Aktivierung fehlgeschlagen. Bitte versuchen Sie es erneut." | "Activation failed. Please try again." |
| Deactivation dialog title | "Modul deaktivieren" | "Deactivate Module" |
| Deactivation dialog body | "Moechten Sie {moduleName} fuer diesen Mandanten deaktivieren? Das Modul wird aus der Seitenleiste entfernt." | "Do you want to deactivate {moduleName} for this tenant? The module will be removed from the sidebar." |
| Deactivation dialog confirm | "Deaktivieren" | "Deactivate" |
| Deactivation dialog cancel | "Abbrechen" | "Cancel" |
| Tenant selector label | "Mandanten-Kontext" | "Tenant Context" |
| Back to marketplace | "Zurueck zum Marktplatz" | "Back to Marketplace" |
| Detail: version | "Version {version}" | "Version {version}" |
| Detail: status active | "Aktiviert fuer diesen Mandanten" | "Activated for this tenant" |
| Detail: status inactive | "Nicht aktiviert" | "Not activated" |
| Sidebar no results | "Keine Module gefunden" | "No modules found" |
---
## States Matrix
| View | Loading | Empty | Populated | Error | Filtered Empty |
|------|---------|-------|-----------|-------|----------------|
| Marketplace grid | Spinner centered in grid area (existing pattern: `h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent`) | Illustration + heading + body from copywriting contract | Card grid with modules | Error banner (`border-destructive/50 bg-destructive/10 p-3 text-sm text-destructive`) + retry hint | "Keine Ergebnisse" + filter suggestion |
| Module detail | Spinner centered | N/A (404 redirect) | Full detail layout | Error banner | N/A |
| Sidebar modules | No loading state (instant from cache/store) | "Keine Module" (existing) | Category accordion with modules | Silent fail (existing pattern) | "Keine Module gefunden" when search yields nothing |
| Tenant selector | "Laden..." in dropdown | N/A (Super-Admin always has at least 1 tenant) | Tenant list dropdown | Toast error | N/A |
---
## Accessibility
| Requirement | Implementation |
|-------------|----------------|
| Keyboard navigation | All interactive elements reachable via Tab. Cards focusable via `group-focus-visible:ring-2`. Activation buttons in natural tab order. |
| Screen reader labels | `aria-label` on icon-only buttons. Status badges use `role="status"`. Toggle buttons use `role="switch"` with `aria-checked` (existing pattern). |
| Focus management | Dialog traps focus when open (ActivationDialog). Return focus to trigger button on close. |
| Color contrast | All text meets WCAG 2.1 AA (4.5:1 for body, 3:1 for large text). Status badges use text + background, not color alone. |
| Reduced motion | Loading spinner respects `prefers-reduced-motion` with `motion-safe:animate-spin`. Toast slide animation gated on motion preference. |
---
## Registry Safety
| Registry | Blocks Used | Safety Gate |
|----------|-------------|-------------|
| shadcn official | none (project does not use components.json) | not applicable |
| Third-party | none | not applicable |
**Note:** Project uses hand-rolled components following shadcn CSS variable naming conventions but without the shadcn CLI or components.json. No registry dependencies exist. All components for this phase are custom-built.
---
## Discretion Decisions Summary
Decisions made by this design contract for areas marked as "Claude's Discretion" in `04-CONTEXT.md`:
| Area | Decision | Rationale |
|------|----------|-----------|
| Filter/Status UI | Tab bar ("Alle" / "Aktiviert" / "Verfuegbar") + category chips | Tabs provide clear top-level filtering for the primary dimension. Category chips offer secondary filtering. Badges on cards still show per-item status. |
| Activation confirmation | Immediate activation with success toast. Deactivation gets confirmation dialog. | Asymmetric risk: activation is easily reversed, deactivation affects active users. |
| Visual feedback | Toast notification + inline badge/button state update | Consistent with existing admin/modules toggle pattern. Toast adds explicit confirmation. |
| Sidebar categories | Keep existing accordion pattern | Already established in Phase 1, works well, categories expand to show individual modules. |
| Sidebar collapsed mode | Show only main nav icons (Dashboard, Marketplace) and admin icons. Hide module list. | Module list needs text labels to be useful. Icons alone at 64px width would be confusing. |
| Sidebar search | Inline search field within sidebar, between main nav and categories section | PRTAL-05 requires sidebar search. Inline placement keeps it contextual. Header search reserved for future global search. |
| Module detail depth | Compact single-column detail view | Appropriate for v1 module count. Full description, status, activation button. No screenshots/reviews (v2). |
| Navigation pattern | Separate pages: `/marketplace` for browsing, `/marketplace/[slug]` for detail, `/modules/{cat}/{slug}` for module usage | Clean URL separation. Marketplace is for discovery/activation. Module routes are for usage. App Router route groups keep them distinct. |
---
## 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