Design-System & Mobile/Tablet-First-Patterns

1. Zweck & Scope

Dieses Modul definiert das verbindliche Design-System von Caja: Branding-Tokens, Breakpoints, adaptive Layouts, Navigations-Muster und Kamera-Flows. Es ist kein eigenständiges Fachmodul, sondern das technische Fundament, auf dem alle 8 Fachmodule aufbauen. Ziel ist eine einheitliche, konsistente Benutzererfahrung auf allen Formfaktoren — vom Mittelklasse-Android-Telefon des Fahrers Arkys über das Tablet im Büro bis zum Desktop von Marcel. Alle UI-Entscheidungen priorisieren Mobile/Tablet als Haupt-Formfaktor (Feldteam am Depot, Container, unterwegs), sind WCAG-konform und folgen dem Atlas-Stack (Next.js App Router + Tailwind 4 + shadcn/ui + Radix). Das Design-System ist das einzige Modul ohne eigene Supabase-Tabellen — es definiert jedoch die globale i18n-Infrastruktur, die Audit-Log-UI und gemeinsame UI-Tokens.


2. Domänenmodell

Das Design-System hat kein eigenes Fachdomänenmodell, aber es definiert folgende querschnittliche Strukturen:

Design-Tokens (CSS Custom Properties, zentral):

  • Farb-Tokens: Brand-Farben, semantische Farben (success, warning, error, info), Neutral-Skala
  • Typografie-Tokens: Schriftfamilie, Schriftgrößen-Skala, Zeilenhöhen
  • Spacing-Tokens: konsistente Abstands-Skala (4px-Raster)
  • Radius-Tokens: Eckenradien für Cards, Buttons, Inputs
  • Shadow-Tokens: Elevations für Cards, Sheets, Modals
  • Motion-Tokens: Transitions, Easing-Kurven

Breakpoint-Modell:

Mobile:  375px–639px   (sm-: default, keine Klasse nötig)
Tablet:  640px–1023px  (sm:..md: via `sm:` Tailwind-Prefix)
Desktop: 1024px+       (lg: via `lg:` Tailwind-Prefix)

Navigations-Architektur:

  • Mobile: Bottom-Tab-Bar (5 Tabs max) + Drawer-Navigation für sekundäre Bereiche
  • Tablet: kombinierte Sidebar (kollabierbar) + Bottom-Tab-Bar optional
  • Desktop: persistente Sidebar (240px) + Command-Palette (⌘K)

Screen-Typ-Kategorien (adaptiv):

  • Daten-Liste/Tabelle (alle Fachmodule)
  • Dashboard / KPI-Übersicht
  • Erfassungs-/Bearbeitungs-Formular
  • Kamera-Flow (QR, OCR, Beleg)
  • Detail-Sheet / Bottom-Sheet

ER-Skizze (querschnittlich, keine eigenen Tabellen):

Design-System
  └── definiert: breakpoints, tokens, patterns
  └── konsumiert von: allen 8 Fachmodulen
  └── referenziert: locale_strings + Inline-label_* (aus Modul 8 Plattform)
  └── präsentiert: audit_log (read-only View)

3. Supabase-Schema

Das Design-System hat keine eigenen Supabase-Tabellen. Es nutzt jedoch zwei querschnittliche Schema-Strukturen, die hier definiert werden, weil sie UI-weit gelten:

3.1 i18n: locale_strings (Hybrid — i18n_labels entfällt)

i18n_labels wird nicht gebaut (Dok 30 K-16, F-11). Stattdessen gilt der Hybrid: Fach-Lookups tragen Inline-label_de/es/en (z. B. movement_types, payment_methods, alle *_statuses); freie UI-Texte (Buttons, Empty-States, Toasts, Formfehler, E-Mail-Templates) leben in locale_strings (Schema-Heimat: Modul 17 / Dok 30 §6.2). Beide werden aus GLOSSAR-ERP_DE-ES-EN.csv geseedet.

-- Kanonische Tabelle (Dok 30 §6.2 / Modul 17 §3.6):
CREATE TABLE locale_strings (
  namespace  text NOT NULL,    -- 'ui.common','email_template','contact_role','tracking_status',…
  key        text NOT NULL,    -- stabiler Schlüssel (oft = Fachcode, z.B. 'EXPENSE')
  locale     text NOT NULL CHECK (locale IN ('de','es','en')),
  value      text NOT NULL,
  updated_at timestamptz NOT NULL DEFAULT now(),
  PRIMARY KEY (namespace, key, locale)
);
CREATE INDEX idx_locale_ns ON locale_strings (namespace, locale);

-- Inline-Labels an Lookups (Beispiel movement_types) — NICHT in locale_strings:
--   movement_types.label_de/es/en = 'Ausgabe' / 'Gasto' / 'Expense' für code 'EXPENSE'
-- Freie UI-Texte in locale_strings:
--   ('ui.common','save','de','Speichern'), (...,'es','Guardar'), (...,'en','Save')

RLS locale_strings:

  • SELECT: alle (auch anon, für Login-Screen)
  • INSERT/UPDATE/DELETE: nur ADMIN oder via Migrations-/Seed-Skript

3.2 Spracheinstellung + UI-Prefs — app_users (keine user_preferences-Tabelle)

user_preferences wird nicht gebaut (F-10). Einzige Quelle für Sprache und Theme ist app_users (Dok 30 §6.1 / Modul 17):

  • app_users.preferred_locale — Sprache, CHECK in ('de','es','en'), Default 'es' (ES-priorisiert, nicht 'de').
  • app_users.themeCHECK in ('light','dark','system'), Default 'system'.
-- (Definition in Dok 30 §6.1 — hier nur referenziert)
-- app_users.preferred_locale text not null default 'es' check (preferred_locale in ('de','es','en'))
-- app_users.theme            text not null default 'system' check (theme in ('light','dark','system'))

Die UI liest/schreibt diese beiden Spalten am eigenen app_users-Satz (id = auth.uid()). sidebar_collapsed ist eine reine Client-Präferenz (localStorage), kein DB-Feld.

3.3 Enum-Definitionen (querschnittlich)

-- Sprache/Theme sind TEXT-Spalten mit CHECK an app_users (siehe §3.2), KEINE Postgres-Enums.

-- KEIN caja_role-ENUM (F-09 / K-20): Rollen sind der TEXT-Lookup `roles`
--   (Dok 30 §6.1) mit den kanonischen Codes
--   ADMIN, BUCHHALTUNG, OPERATIONS, FAHRER, AFFILIATE, READONLY.
--   Zuweisung über user_roles; RLS-Prüfung via has_role('CODE').
--   (admin_office → BUCHHALTUNG, operations → OPERATIONS, driver → FAHRER gemappt.)

4. Kern-Workflows

4.1 Sprache wechseln (i18n)

  1. User öffnet Profil-Menü (Bottom-Tab "Konto" auf Mobile, Sidebar-Footer auf Desktop)
  2. Wählt Sprache aus Segmented-Control: DE | ES | EN
  3. Caja speichert app_users.preferred_locale via Supabase-Update (F-10)
  4. React-Context LocaleProvider re-rendert alle Labels aus den Inline-label_*/locale_strings-Caches
  5. Stabile interne Codes (EXPENSE, CASH etc.) bleiben unverändert in Daten
  6. Edge-Case: Erstaufruf ohne Session → Browser-Sprache (navigator.language) als Fallback, Default es (ES-priorisiert); Cookie pref_lang wie auf der Website

4.2 Theme wechseln (Light/Dark)

  1. User öffnet Profil oder nutzt ⌘K → „Dark Mode"
  2. Caja speichert app_users.theme (F-10)
  3. ThemeProvider setzt data-theme="dark" auf <html>, alle CSS Custom Properties schalten um
  4. Edge-Case: systemprefers-color-scheme wird gefolgt, kein Supabase-Write bei jedem Systemwechsel

4.3 Command-Palette (⌘K / Ctrl+K)

  1. User drückt ⌘K (Desktop/Tablet) oder tippt in Suchfeld (Mobile)
  2. CommandPalette-Overlay öffnet sich (shadcn/ui <Command> + Radix Dialog)
  3. Echtzeit-Filterung über:
    • Navigation (Modul-Links)
    • Kontakte (Suche in contacts)
    • Aufträge (Suche in orders)
    • Aktionen: „Neue Offerte", „Box scannen", „Zahlung erfassen"
  4. Tastatur-Navigation (↑↓ Enter Esc) vollständig
  5. Edge-Case: Netzwerkausfall → zeigt gecachte Navigation-Items, Datei-Suche deaktiviert mit Hinweis

4.4 QR-Code scannen (Box-UUID)

  1. User tippt „Box scannen"-FAB (Floating Action Button, 56px, unten rechts auf Mobile)
  2. Kamera-Permission-Request (einmalig, Browser-API)
  3. BarcodeDetector API (Native) oder ZXing-WASM-Fallback öffnet Kamera-Preview (Vollbild)
  4. UUID wird erkannt → Vibrations-Feedback (50ms) + visueller Rahmen grün
  5. Caja navigiert zu boxes/:uuid Detail-Screen
  6. Edge-Case: Permission verweigert → Anleitung mit direktem Link zu Browser-Einstellungen; Kein QR-Code nach 30s → Scan-Abbruch mit Retry-Option; Falsche UUID → Fehlermeldung Toast

4.5 PWA installieren

  1. Browser-Banner „App installieren" erscheint (automatisch, wenn Manifest + SW vorhanden)
  2. User tippt „Installieren" → Homescreen-Icon erstellt
  3. Beim nächsten Start: Standalone-Modus (display: standalone), kein Browser-Chrome
  4. Service-Worker aktiv: cached Assets + API-Responses
  5. Offline: Lese-Operationen aus Cache; Schreib-Operationen in Write-Queue (IndexedDB)
  6. Sync: Bei Netzwerkrückkehr: Queue wird automatisch geleert (Background-Sync API)
  7. Edge-Case: Konflikt (Offline-Write + inzwischen geänderter Server-Stand) → Conflict-Toast mit Merge-Dialog

5. UI-Screens (Mobile/Tablet-First)

5.1 Navigationssystem

Mobile (375–639px): Bottom-Tab-Bar + Drawer

┌─────────────────────────────────────┐
│  [Header: Caja Logo] [⌘K] [Avatar]  │  ← 56px, sticky top
│                                     │
│         [Screen-Inhalt]             │
│                                     │
│                                     │
│                                     │
├─────────────────────────────────────┤
│  🏠    📦    ➕    💰    👤         │  ← Bottom-Tab-Bar, 56px
│ Home  Box  Neu  Fin  Konto         │  ← Icons + Label, ≥44px Tap-Target
└─────────────────────────────────────┘
  • 5 Tabs max: Home (Dashboard), Sendungen, Neu (FAB-ähnlich, primäre Aktion), Finanzen, Konto
  • Sekundäre Bereiche (CRM, Affiliate, Admin) über Drawer: Tab „Konto" → „Mehr" → Drawer von links
  • Drawer-Item-Höhe: ≥56px, Icons links, Labels rechts, Chevron für Untermenüs
  • Active-Tab: Primärfarbe Navy + Farbiger Indikator-Balken oben

Tablet (640–1023px): Mini-Sidebar + Content

┌──────┬──────────────────────────────┐
│ 🏠   │                              │
│ 📦   │   [Screen-Inhalt]            │
│ 💰   │                              │
│ 👤   │                              │
│      │                              │
│ ⚙️   │                              │
└──────┴──────────────────────────────┘
  • Sidebar kollabiert (64px, nur Icons) oder expandiert (240px, Icons + Labels)
  • Toggle-Button (Hamburger) oben links im Header
  • Overlay-Drawer bei Tab-Tap auf Sidebar-Icon für Untermenüs

Desktop (1024px+): Persistente Sidebar

┌────────────┬────────────────────────┐
│ CAJA  ◀▶  │  [Breadcrumb]  [⌘K]   │
│────────────│                        │
│ Dashboard  │  [Screen-Inhalt]       │
│ Kontakte   │                        │
│ Offerten   │                        │
│ Aufträge   │                        │
│ Sendungen  │                        │
│ Finanzen   │                        │
│ Affiliates │                        │
│────────────│                        │
│ [Avatar]   │                        │
│ [Settings] │                        │
└────────────┴────────────────────────┘
  • Sidebar 240px, kollabierbar auf 64px
  • Command-Palette ⌘K als primäre Suchfunktion

5.2 Daten-Tabelle → adaptive Karten-/Stapelliste

Prinzip: Dieselbe Datenmenge, zwei Präsentations-Formen. TanStack Table als Datenschicht, Rendering je nach Viewport.

Mobile: Karten-Stack

┌─────────────────────────────────────┐
│ [Filter-Chips: Alle | Offen | Paid] │  ← horizontal scrollbar
├─────────────────────────────────────┤
│ ┌───────────────────────────────┐   │
│ │ Carmen Álvarez         🟢 PAID│   │  ← Name + Status-Badge
│ │ Caja Jumbo × 2   CHF 480.00  │   │  ← Produkt + Betrag
│ │ 12.06.2026         Ref: 4821  │   │  ← Datum + Referenz
│ │ [Zahlung]  [Detail]  [···]    │   │  ← Action-Buttons (≥44px)
│ └───────────────────────────────┘   │
│ ┌───────────────────────────────┐   │
│ │ Roberto Pérez          🔴 OPEN│   │
│ │ Caja Mega × 1    CHF 320.00  │   │
│ │ 08.06.2026         Ref: 4820  │   │
│ │ [Zahlung]  [Detail]  [···]    │   │
│ └───────────────────────────────┘   │
└─────────────────────────────────────┘
  • 3–4 wichtigste Felder sichtbar (Name, Betrag/Produkt, Status, Datum)
  • Swipe-Links: primäre Aktion (z.B. „Zahlung erfassen")
  • Swipe-Rechts: sekundäre Aktion (z.B. „Archivieren") oder destruktive Aktion mit Rot
  • Tap auf Karte: öffnet Detail-Bottom-Sheet (nicht neue Seite!)
  • Unendliches Scroll oder Pagination (50 Items pro Seite)
  • Pull-to-Refresh (native Geste)

Tablet/Desktop: TanStack-Tabelle

┌──────┬───────────────┬──────────────┬───────────┬──────────────┬──────────┐
│  ☐   │ Kunde         │ Produkt      │ Betrag    │ Status       │ Aktionen │
├──────┼───────────────┼──────────────┼───────────┼──────────────┼──────────┤
│  ☐   │ Carmen Álv.   │ Jumbo × 2480.00    │ 🟢 Bezahlt   │ ✎ ⋯    │
│  ☐   │ Roberto Pér.  │ Mega × 1320.00    │ 🔴 Offen     │ ✎ ⋯    │
└──────┴───────────────┴──────────────┴───────────┴──────────────┴──────────┘
  • Sticky-Header, Column-Sorting, Multi-Select für Batch-Aktionen
  • Zeilen-Hover → Action-Buttons sichtbar
  • Klick auf Zeile: Slide-Over-Panel rechts (Detail), kein Seitenwechsel
  • Spalten konfigurierbar (show/hide via ColumnVisibility-Dropdown)

TypeScript-Interface (Illustration):

interface DataTableConfig<TData> {
  columns: ColumnDef<TData>[];
  mobileCardRenderer: (row: TData) => React.ReactNode; // Karten-Darstellung
  swipeActions?: {
    left?: SwipeAction;   // primäre Aktion
    right?: SwipeAction;  // sekundäre/destruktive Aktion
  };
  detailPanel?: (row: TData) => React.ReactNode; // Tablet: Slide-Over, Mobile: Bottom-Sheet
}

5.3 Dashboard — KPI-Kacheln + Charts

Mobile: vertikaler Stack

┌─────────────────────────────────────┐
│  Juni 2026     [< Monat >]          │  ← Monatsselektor
├─────────────────────────────────────┤
│  ┌────────────┐  ┌────────────┐     │
│  │ Einnahmen  │  │ Ausgaben   │     │  ← 2-Spalten-Grid, volle Breite
│  │ CHF 12'400 │  │ CHF 3'200  │     │
│  │ ↑ +8% Mom  │  │ ↓ -2% Mom  │     │
│  └────────────┘  └────────────┘     │
│  ┌────────────┐  ┌────────────┐     │
│  │ Offene DB  │  │ Kasse      │     │
│  │ CHF 1'800  │  │ CHF 2'340  │     │
│  └────────────┘  └────────────┘     │
├─────────────────────────────────────┤
│  [Balkendiagramm Einnahmen/Ausgaben]│  ← 100% Breite, 180px Höhe
│  (Recharts ResponsiveContainer)     │
├─────────────────────────────────────┤
│  Offene Posten (3)                  │  ← Karten-Liste
│  ...                                │
└─────────────────────────────────────┘
  • KPI-Kacheln: 2-Spalten-Grid auf Mobile (je 50% Breite minus Gap)
  • Charts: 100% Containerbreite, fixe Höhe (180px Mobile, 240px Tablet, 320px Desktop)
  • ResponsiveContainer von Recharts für automatische Breitenanpassung
  • Legende unterhalb des Charts auf Mobile (nicht seitlich)

Tablet/Desktop: horizontale Kacheln + Seitenpanel

  • KPI-Kacheln: 4-Spalten-Grid (je 25%)
  • Charts: 60% Hauptbereich + 40% Seitenpanel (Offene Posten, Quick-Actions)

5.4 Erfassungs-/Bearbeitungs-Formular — Quick-Entry

Ziel: Minimale Tipp-Arbeit, maximale Daumen-Optimierung auf Mobile.

Bottom-Sheet-Selects (statt nativer <select>)

[Feld: Vorgangsart]  [EXPENSE — Ausgabe  ▾]
  → Tap öffnet Bottom-Sheet:
  ┌─────────────────────────────────────┐
  │  ▔▔▔ (Drag-Handle)                 │
  │  Vorgangsart wählen                 │
  ├─────────────────────────────────────┤
  │  ○ Ausgabe (EXPENSE)                │
  │  ○ Einnahme (INCOME)                │
  │  ○ Rechnung (INVOICE)               │
  │  ● Anzahlung Kasse (DEPOSIT_CASH)   │  ← ausgewählt
  │  ○ Rechnungszahlung (INVOICE_PAYMENT)│
  │  ...                                │
  │  [Bestätigen]                       │
  └─────────────────────────────────────┘
  • shadcn/ui Drawer (Vaul-basiert) für alle Select-Felder auf Mobile
  • Tablet/Desktop: shadcn/ui Popover oder Select (native UX)
  • Suchfeld im Drawer für lange Listen (>8 Optionen)

Numerische Eingaben

// Betrag-Feld immer mit inputmode="decimal" (öffnet Zahlentastatur auf iOS/Android)
<Input
  type="text"
  inputMode="decimal"
  pattern="[0-9]*[.,]?[0-9]*"
  placeholder="0.00"
  className="text-right text-2xl font-mono h-16" // Großes Target
/>

Touch-Target-Regeln:

  • Alle interaktiven Elemente: Mindest-Tap-Target 44×44px (WCAG 2.5.5)
  • Buttons in Forms: 48px Höhe auf Mobile
  • Checkboxen/Radios: 44×44px klickbare Fläche (auch wenn visuell kleiner)
  • Spacing zwischen Tap-Targets: mindestens 8px

Form-Layout Mobile vs. Desktop

Mobile: alle Felder stacked (1 Spalte, 100% Breite)
Tablet: 2-Spalten-Grid für kurze Felder (Datum + Betrag nebeneinander)
Desktop: 2–3-Spalten-Grid + Seitenvorschau

[Datum]          [Betrag CHF]
[Kunde ▾]        [Zahlweise ▾]
[Vorgangsart ▾]
[Referenz]
[Notiz (textarea, 3 Zeilen)]
[Speichern]   [Abbrechen]

Action-State-Forms (shadcn/ui + React Server Actions):

  • Submit-Button zeigt Spinner während Pending
  • Erfolg: Toast grün + Form-Reset oder Navigation
  • Fehler: Toast rot + Felder mit Fehler hervorgehoben (red border + Fehlertext)
  • Validierungsfehler inline unterhalb des Feldes

5.5 Kamera-Flows

(a) Box-QR-Scan

[FAB "📷 Scannen" unten rechts, 56px, Navy]Tap:
  ┌─────────────────────────────────────┐
  │                                     │
  │       [Kamera-Live-Preview]         │
  │                                     │
  │   ┌─────────────────────────┐       │
  │   │  Viewfinder-Rahmen      │       │
  │   │  (animiert, gold)       │       │
  │   └─────────────────────────┘       │
  │                                     │
  │  Halte den QR-Code in den Rahmen   │
  │  [✕ Abbrechen]                     │
  └─────────────────────────────────────┘
  → UUID erkannt: Rahmen grün, Vibration, Navigation zu boxes/:uuid

Technisch:

  • BarcodeDetector API (Chrome/Android nativ, kein JS-Bundle-Overhead)
  • Polyfill: @zxing/browser nur wenn !('BarcodeDetector' in window)
  • getUserMedia({ video: { facingMode: 'environment' } }) → Rückkamera bevorzugt
  • Frame-Rate: 10fps für Scan-Loop reicht, spart Akku

(b) Ausweis-/Cédula-Scan (OCR)

[In CRM: "Ausweis scannen"-Button]
  → Kamera öffnet sich im Dokumenten-Modus (kein Viewfinder-Rahmen, volle Kamera)
  → Foto aufnehmen (Shutter-Button 72px)
  → Preview + "Verwenden" / "Wiederholen"
  → OCR-API-Call (Supabase Edge Function → Tesseract.js oder externe OCR)
  → Extrahierte Felder: Vorname, Nachname, Cédula-Nummer, Geburtsdatum, Ablaufdatum
  → Vorausfüll-Dialog: User sieht extrahierte Felder, kann korrigieren
  → "Übernehmen" → füllt contacts-Formular vor

Edge-Cases:

  • Unlesbare ID: OCR-Confidence < 0.7 → „Felder manuell eingeben" mit vorausgefüllten best-effort Werten
  • Netzwerkausfall: OCR nicht möglich → Bild lokal zwischenspeichern (IndexedDB), nach Netzwerkrückkehr nachverarbeiten
  • Datenschutz: Rohbild liegt im privaten kyc-Bucket (kein separater ocr_temp-Bucket — Dok 30 §6.3); Aufbewahrung/Löschzeitpunkt folgt Geschäftsentscheid OE-11 (F-21, pending Rechtsgutachten; Default minimal nach KYC-Zweckerfüllung, Compliance §4.6). UI-Hinweis auf zweckgebundene, kurze Speicherung

(c) Belegfoto (Rechnung, Lieferschein)

[In Finanzen/Aufträge: "Beleg anhängen" Button]Auswahl: [Foto aufnehmen] | [Aus Galerie] | [PDF auswählen]Foto: Kamera-Modus (native Capture oder custom)
  → Kompression: Canvas-Resize auf max 1920px, JPEG 80% (target <500KB)
  → Upload zu Supabase Storage: Bucket `documents`, Pfad: `{user_id}/{year}/{movement_id}.jpg`
  → Thumbnail-Vorschau in FormularSpeichern: URL in `movements.document_url` oder eigene `attachments`-Tabelle

5.6 Empty-States & Skeletons

Empty-State-Muster:

┌─────────────────────────────────────┐
│                                     │
│          [Icon: Inbox/Box]          │
│     Noch keine Aufträge             │
│  Erstelle deinen ersten Auftrag     │
│                                     │
│      [+ Neuer Auftrag]              │
│                                     │
└─────────────────────────────────────┘
  • Icon: lucide-react, 48px, text-muted-foreground
  • Titel: text-lg font-medium
  • Beschreibung: text-sm text-muted-foreground
  • CTA: primärer Button (wenn User Schreibrecht hat)

Skeleton-Muster (Loading):

  • Karten-Liste: 3 Skeleton-Karten mit animate-pulse (shadcn/ui Skeleton)
  • Tabellen: Skeleton-Zeilen gleicher Höhe wie echte Zeilen
  • KPI-Kacheln: Skeleton-Rechtecke in KPI-Größe
  • Charts: Skeleton-Rechteck in Chart-Höhe

5.7 Ergänzende Mobile-/Tablet-Patterns (F-17)

Diese Fach-Screens hatten bisher nur ein Desktop-Dialog-Muster; hier das verbindliche <sm-Pattern (Bottom-Sheet/Karten gemäss §5.1–§5.4):

(a) Treuhänder-Export (Modul 6 §5.7)

  • Handy: Kein Desktop-Dialog, sondern Bottom-Sheet „Export" — Felder Periode (Monats-Select als Drawer), Format-Chips (CSV · Excel · PDF), grosser Primär-Button „Exportieren" (≥56px, thumb-zone).
  • Datei-Download auf PWA: Generierung serverseitig (Edge-Function), Ergebnis als signierte URL; UI öffnet sie via Share-Sheet / „In Dateien sichern" (iOS) bzw. Download-Manager (Android) statt Inline-Render. Fortschritts-Spinner während Generierung; bei grossen CSV/PDF Hinweis „Wird vorbereitet …" (Perf-Risiko Mittelklasse-Android: serverseitig rendern, nie im Client zusammenbauen).
  • Tablet/Desktop: unverändert Dialog/Slide-Over.

(b) MWST-Aktivierung / Settings (Modul 6 §F)

  • Handy: Settings als gestapelte Karten-Liste (eine Karte je Schalter): „MWST aktivieren" (Toggle), „Methode" (Saldosteuersatz/effektiv — Bottom-Sheet-Select), „Satz" (Eingabe inputmode=decimal). Bestätigung kritischer Schalter (vat_enabled) via Bottom-Sheet-Confirm mit Klartext-Folgehinweis („ab jetzt verlangt jedes Formular vat_code").
  • Tablet/Desktop: zweispaltiges Settings-Formular.

(c) Container-Detail Bulk-Aktionen am Handy (Modul 5 §5.5)

  • Multi-Select: Long-Press auf eine Box-Karte aktiviert den Auswahlmodus (Checkbox-Overlay je Karte, Auswahl-Zähler im Header „7 ausgewählt").
  • Bulk-Bar: als sticky Bottom-Bar über der Tab-Bar — Aktionen „Alle → SHIPPED", „Alle → DR_CUSTOMS", „Aus Container entfernen" als ≥44px-Buttons; destruktive Aktion rot + Confirm-Sheet.
  • Kapazitäts-Warnung: als Inline-Alert-Karte oben in der Liste (nicht als modaler Dialog), wenn v_container_load.load_pct > 100.
  • Verladen-Flow: CTA „Boxen verladen" springt in den Scan-Screen im Mehrfach-Modus (Kamera bleibt offen, Live-Zähler „12 / 40").

(d) Affiliate-Mini-Portal (Modul 7 §9, (portal))

  • Eigene reduzierte Mobile-Shell: kein internes Bottom-Tab-Set (CRM/Finanzen), sondern nur 3 Tabs — „Übersicht" (eigene Provisionen-KPIs), „Sendungen" (eigene vermittelte), „Abrechnung" (eigene Payouts). Header mit Affiliate-Name, kein ⌘K (nur Suche innerhalb eigener Daten).
  • Listen: ausschliesslich Karten-Stack (Provision je Karte: Betrag · Status-Badge · Auftrag); RLS stellt sicher, dass nur current_affiliate_id()-Daten sichtbar sind.
  • Tablet/Desktop: schmale Sidebar mit denselben 3 Einträgen; bewusst minimal gehalten (Externer-Zugriff).

Hinweis Preis-Resolver-Testscreen (Modul 3 §5.5): bewusst nur Desktop/Tablet (Entwickler-/Pflege-Werkzeug) — kein Handy-Pattern nötig, hier als bewusste Lücke dokumentiert.


6. Integrationen & Verbindungen zu anderen Modulen

Das Design-System ist das Fundament für alle anderen Module. Konkrete Verbindungen:

Alle 8 Module konsumieren:

  • locale_strings + Inline-label_* via LocaleProvider + useT() Hook (stabiler Code → übersetzter Label)
  • app_users.preferred_locale / app_users.theme für Sprache/Theme
  • Breakpoint-Hooks (useBreakpoint()) für adaptive Rendering
  • Adaptive DataTable-Komponente
  • Bottom-Sheet / Drawer-Komponenten für Select-Felder

Modul 1 CRM: Ausweis-Scan-Flow (OCR), Kontaktkarten-Pattern Modul 2 WhatsApp+OCR: Kamera-Flow (b), OCR-Confidence-Handling Modul 3 Produkte: Preislisten-Tabelle → adaptive DataTable Modul 4 Offerten: PDF-Vorschau in Bottom-Sheet / Slide-Over Modul 5 Logistik: QR-Scan-Flow (a), 10-Schritt-Tracking-Timeline-Komponente Modul 6 Finanzen: Belegfoto-Flow (c), Monatsabschluss-Dashboard-Charts Modul 7 Affiliate: Provisions-Tabelle → adaptive DataTable Modul 8 Plattform: Auth (Magic-Link-Screen), Audit-Log (read-only DataTable), Rollen → RLS

Events (keine DB-Events, aber UI-Events):

  • locale:changed → alle Komponenten re-rendern Labels
  • theme:changed → CSS-Variables wechseln, kein Re-Render nötig
  • network:offline / network:online → Offline-Banner + Queue-Flush
  • qr:detected → Navigation-Event zu gescannter Box
  • ocr:complete → Formular-Vorausfüll-Event

7. Validierungen & Edge-Cases

7.1 Sprache & i18n

  • Fehlender Label: wenn für (namespace, key, locale) in locale_strings (bzw. die Inline-label_*-Spalte) kein Eintrag → Fallback: zuerst locale='es', dann roher Code in Klammern [EXPENSE]; nie leer darstellen
  • Stabiler Code in Logik: Kein Modul darf label-Text für Logik-Entscheidungen verwenden (z.B. if label === 'Ausgabe'); immer code === 'EXPENSE'
  • Neue Codes: Inline-label_de/es/en am Lookup setzen (bzw. locale_strings-Zeilen für alle 3 Sprachen anlegen), bevor der Code in movement_types etc. erscheint

7.2 Touch & Responsivität

  • Minimale Tap-Target-Verletzung: automatischer CI-Check via eslint-plugin-jsx-a11y + Axe-Tests; Komponenten-Template schreibt min-h-[44px] min-w-[44px] vor
  • Horizontaler Scroll auf Mobile verboten: kein overflow-x-auto auf Root-Level; Tabellen MÜSSEN auf Mobile in Karten-Modus wechseln
  • Bottom-Sheet auf Tastatur: wenn Software-Tastatur öffnet, Sheet scrolbar und nicht verdeckt (CSS env(keyboard-insets-bottom))

7.3 PWA & Offline

  • Write-Queue-Konflikt: Offline-erstelltes movement mit inzwischen geändertem contacts-Eintrag → beim Sync: Konflikt-Dialog zeigt beide Versionen, User entscheidet
  • Service-Worker-Update: neues Deploy → SW zeigt Update-Toast „Neue Version verfügbar [Aktualisieren]"; kein Auto-Reload ohne Benutzerbestätigung
  • Cache-Strategie: Navigation + JS/CSS: CacheFirst; API-Responses (Lesen): StaleWhileRevalidate mit 5min TTL; Mutationen: NetworkFirst mit Queue-Fallback

7.4 Kamera

  • Kein BarcodeDetector + kein WASM-Support: QR-Scan durch manuelle UUID-Eingabe ersetzen (immer als Fallback sichtbar)
  • Kamera-Permission dauerhaft verweigert: Link zu Browser-Einstellungen + Anleitung per Screenshot (je iOS/Android)
  • Sehr schwacher Prozessor (Mittelklasse-Android): WASM-QR-Decode-Loop throttlen auf 5fps; kein requestAnimationFrame ohne Drosselung

7.5 Performance-Budget (Mittelklasse-Android, Ziel-Gerät)

MetrikZiel
First Contentful Paint (3G-sim)< 2.5s
Largest Contentful Paint< 4.0s
Time to Interactive< 5.0s
JS-Bundle (initial, gzip)< 200KB
Bilder/IconsSVG oder WebP, lazy loaded
Fonts (Inter)nur woff2, subset latin+latin-ext

8. Compliance- und Sicherheits-Hinweise

8.1 Datenschutz (DSG/revDSG)

  • Belegfotos und Ausweisscans sind besonders schützenswerte Personendaten (Art. 5 lit. c revDSG); gesonderte Einwilligung dokumentieren
  • OCR-Rohbild — Aufbewahrung folgt Geschäftsentscheid OE-11 (F-21): Rohbild liegt im privaten kyc-Bucket (kein separater ocr_temp-Bucket — Dok 30 §6.3 kennt nur kyc/documents/receipts/public-assets). Der konkrete Löschzeitpunkt ist pending Rechtsgutachten (Zoll/Geldwäsche DR+CH vs. revDSG-Minimierung, Compliance §4.6 / OCR §8.1); bis dahin Default = minimal (Löschung nach KYC-Zweckerfüllung, Lifecycle-Job + audit_log). Extrahierte Textfelder werden in contacts gespeichert
  • Supabase Storage: documents-Bucket auf private (kein öffentlicher Zugriff); Download nur via signierte URLs (1h Ablauf)
  • Audit-Log: alle contacts-Änderungen durch audit_log (Modul 8) erfasst; Pflicht bei OCR-Vorausfüllung (Quelle = ocr)

8.2 Barrierefreiheit (WCAG 2.1 AA)

  • Kontrast: Primärfarbe Navy #00205B auf Weiß: Kontrast 12.5:1 (AA + AAA); Akzent-Rot #CE1126 auf Weiß: 5.3:1 (AA) — beide bestehen; in Dark-Mode gesondert prüfen
  • Touch-Targets: ≥44×44px (WCAG 2.5.5 AAA); ≥24×24px minimum (WCAG 2.5.8 AA)
  • Focus-Ring: sichtbar, nicht durch outline: none entfernt; shadcn/ui verwendet focus-visible:ring-2
  • Screenreader: Bottom-Sheets mit aria-modal, aria-labelledby, role="dialog"; Kamera-Preview mit aria-live für Status-Meldungen
  • Reduce-Motion: prefers-reduced-motion: reduce → alle animate-* Klassen deaktiviert; Kamera-Viewfinder-Animation entfernt
  • Sprachdeklaration: <html lang> Attribut dynamisch aus app_users.preferred_locale gesetzt

8.3 PWA-Sicherheit

  • HTTPS erzwungen: kein HTTP-Zugriff (Vercel erzwingt HTTPS)
  • Service-Worker: scope auf /caja/ begrenzt; kein Cachen von Auth-Tokens im SW-Cache
  • Content-Security-Policy: Camera-Quelle explizit erlaubt (media-src 'self'); keine unsafe-eval für WASM → wasm-unsafe-eval stattdessen
  • IndexedDB Write-Queue: verschlüsselt mit crypto.subtle (AES-GCM) vor Speicherung (Offline-Queue enthält ggf. Finanzdaten)

8.4 Inter-Font (self-hosted)

  • Font-Dateien im Repo unter public/fonts/inter/; kein Google-Fonts-CDN-Aufruf (DSGVO/DSG: kein Drittland-Transfer für Font-Requests)
  • font-display: swap verhindert FOIT

9. Offene Punkte

🔲 Dark-Mode-Kontrast-Audit: Akzent-Rot #CE1126 auf dunklem Hintergrund muss gesondert geprüft werden (ggf. hellere Variante #E8374A im Dark-Mode nötig). Entscheidung ausstehend.

🔲 OCR-Provider für Ausweis-Scan: Tesseract.js (lokal, datenschutzfreundlich, aber langsamer auf Mittelklasse-Android) vs. externe OCR-API (schneller, aber Drittlandtransfer → DSG-Einwilligung erforderlich). Entscheidung von Marcel/Mariela benötigt.

🔲 Bottom-Tab-Labels auf sehr kleinen Displays (320px): 5 Tabs mit Text könnten zu eng werden. Optionen: (a) nur Icons ohne Label, (b) 4 Tabs + „Mehr"-Tab. Entscheidung ausstehend.

🔲 Offline-Write-Queue-Scope: Welche Operationen sollen offline-fähig sein? Vorschlag: nur movements-Erfassung (Depot/Zahlung) und boxes-Status-Update; alles andere requires-network. Bestätigung ausstehend.

🔲 Inter-Subset-Umfang: Lateinische Zeichen reichen für DE/EN. ES (Spanisch DR) benötigt latin-ext (á é í ó ú ñ ü ¿ ¡). Prüfen ob latin-ext ausreicht oder full für Eigennamen aus DR nötig. Wahrscheinlich latin-ext genügt.

🔲 Command-Palette auf Mobile: ⌘K gilt nur für Desktop/Tastatur. Mobile-Äquivalent: Suchfeld im Header oder eigener „Suche"-Tab in der Bottom-Tab-Bar. Welches Muster bevorzugt? Entscheidung von Marcel ausstehend.

🔲 PWA-Icon-Set: Offizielle Caja-/Dominicano-Express-App-Icons (512×512, 192×192, maskable) müssen noch erstellt werden (Grafik-Asset, kein Code-Problem). Auftrag an: Grafiker oder Mariela.


Anhang A: Branding-Tokens (CSS Custom Properties)

/* Dominicano Express — Caja Design Tokens */
:root {
  /* === Brand === */
  --color-brand-navy:      #00205B;   /* Primärfarbe, Sidebar, Buttons */
  --color-brand-navy-700:  #001A4A;   /* Hover-State Navy */
  --color-brand-navy-300:  #3356A0;   /* Muted Navy (z.B. Icons) */
  --color-brand-red:       #CE1126;   /* Akzent, CTA, Alerts */
  --color-brand-red-700:   #A80D1F;   /* Hover-State Rot */
  --color-brand-red-100:   #FDECEA;   /* Error-Background */
  --color-brand-gold:      #D4A800;   /* QR-Scan-Viewfinder, Premium-Badge */

  /* === Semantic (Light Mode) === */
  --color-background:      #FFFFFF;
  --color-surface:         #F8F9FA;   /* Card-Background */
  --color-surface-raised:  #FFFFFF;   /* Modal/Sheet-Background */
  --color-border:          #E2E8F0;
  --color-text-primary:    #0F172A;
  --color-text-secondary:  #64748B;
  --color-text-disabled:   #CBD5E1;

  /* === Status === */
  --color-success:         #16A34A;   /* PAID 🟢 */
  --color-warning:         #D97706;   /* PARTIAL 🟠 */
  --color-error:           #DC2626;   /* OPEN 🔴 */
  --color-info:            #2563EB;   /* DEPOSIT_CASH ohne Abono 🟡 = gold alternativ */
  --color-neutral:         #94A3B8;   /* INTERNAL_TRANSFER ⚪ */

  /* === Spacing (4px-Raster) === */
  --space-1:  4px;
  --space-2:  8px;
  --space-3:  12px;
  --space-4:  16px;
  --space-6:  24px;
  --space-8:  32px;
  --space-12: 48px;
  --space-16: 64px;

  /* === Radius === */
  --radius-sm:  4px;
  --radius-md:  8px;
  --radius-lg:  12px;
  --radius-xl:  16px;
  --radius-full: 9999px;  /* Badges, Chips */

  /* === Typography === */
  --font-sans: 'Inter', system-ui, -apple-system, sans-serif;
  --font-mono: 'JetBrains Mono', 'Courier New', monospace;  /* Beträge/IDs */

  /* === Shadows === */
  --shadow-card:   0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.04);
  --shadow-sheet:  0 -4px 24px rgba(0,0,0,0.12);
  --shadow-modal:  0 8px 32px rgba(0,0,0,0.16);

  /* === Motion === */
  --duration-fast:   150ms;
  --duration-normal: 250ms;
  --duration-slow:   400ms;
  --easing-standard: cubic-bezier(0.4, 0, 0.2, 1);
  --easing-spring:   cubic-bezier(0.34, 1.56, 0.64, 1);
}

/* Dark Mode Override */
[data-theme="dark"] {
  --color-background:      #0F172A;
  --color-surface:         #1E293B;
  --color-surface-raised:  #334155;
  --color-border:          #334155;
  --color-text-primary:    #F1F5F9;
  --color-text-secondary:  #94A3B8;
  --color-text-disabled:   #475569;
  --color-brand-red:       #E8374A;  /* 🔲 Kontrast auf Dark prüfen */
}

/* Reduce Motion */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

Anhang B: Tailwind-4-Konfiguration (Auszug)

// tailwind.config.ts
import type { Config } from 'tailwindcss';

export default {
  darkMode: ['selector', '[data-theme="dark"]'],
  theme: {
    extend: {
      screens: {
        // Mobile-First: kein 'sm' nötig (default = mobile)
        'sm':  '640px',   // Tablet
        'lg':  '1024px',  // Desktop
        // KEIN 'md' — 3-Stufen-Modell: Mobile | Tablet | Desktop
      },
      colors: {
        brand: {
          navy:    'var(--color-brand-navy)',
          'navy-700': 'var(--color-brand-navy-700)',
          red:     'var(--color-brand-red)',
          'red-700': 'var(--color-brand-red-700)',
          gold:    'var(--color-brand-gold)',
        },
      },
      fontFamily: {
        sans: ['Inter', 'system-ui', 'sans-serif'],
        mono: ['JetBrains Mono', 'monospace'],
      },
      minHeight: {
        'touch': '44px',   // WCAG-Mindest-Touch-Target
        'touch-lg': '56px', // empfohlenes Touch-Target
      },
      minWidth: {
        'touch': '44px',
      },
    },
  },
} satisfies Config;

Anhang C: PWA-Manifest (public/manifest.json, Auszug)

{
  "name": "Caja – Dominicano Express",
  "short_name": "Caja",
  "description": "All-in-One Backend für Dominicano Express GmbH",
  "start_url": "/",
  "display": "standalone",
  "orientation": "portrait-primary",
  "background_color": "#00205B",
  "theme_color": "#00205B",
  "lang": "de",
  "icons": [
    { "src": "/icons/icon-192.png",  "sizes": "192x192",  "type": "image/png" },
    { "src": "/icons/icon-512.png",  "sizes": "512x512",  "type": "image/png" },
    { "src": "/icons/icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
  ],
  "screenshots": [
    { "src": "/screenshots/mobile-dashboard.png", "sizes": "390x844", "type": "image/png", "form_factor": "narrow" },
    { "src": "/screenshots/tablet-dashboard.png",  "sizes": "1024x768","type": "image/png", "form_factor": "wide"   }
  ],
  "categories": ["business", "productivity"],
  "prefer_related_applications": false
}

Spec-Version 1.0 · Modul 18 · 2026-06-26 · Dominicano Express GmbH