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 (auchanon, für Login-Screen)INSERT/UPDATE/DELETE: nurADMINoder 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.theme—CHECK 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)
- User öffnet Profil-Menü (Bottom-Tab "Konto" auf Mobile, Sidebar-Footer auf Desktop)
- Wählt Sprache aus Segmented-Control: DE | ES | EN
- Caja speichert
app_users.preferred_localevia Supabase-Update (F-10) - React-Context
LocaleProviderre-rendert alle Labels aus den Inline-label_*/locale_strings-Caches - Stabile interne Codes (
EXPENSE,CASHetc.) bleiben unverändert in Daten - Edge-Case: Erstaufruf ohne Session → Browser-Sprache (
navigator.language) als Fallback, Defaultes(ES-priorisiert); Cookiepref_langwie auf der Website
4.2 Theme wechseln (Light/Dark)
- User öffnet Profil oder nutzt ⌘K → „Dark Mode"
- Caja speichert
app_users.theme(F-10) ThemeProvidersetztdata-theme="dark"auf<html>, alle CSS Custom Properties schalten um- Edge-Case:
system→prefers-color-schemewird gefolgt, kein Supabase-Write bei jedem Systemwechsel
4.3 Command-Palette (⌘K / Ctrl+K)
- User drückt ⌘K (Desktop/Tablet) oder tippt in Suchfeld (Mobile)
CommandPalette-Overlay öffnet sich (shadcn/ui<Command>+ Radix Dialog)- Echtzeit-Filterung über:
- Navigation (Modul-Links)
- Kontakte (Suche in
contacts) - Aufträge (Suche in
orders) - Aktionen: „Neue Offerte", „Box scannen", „Zahlung erfassen"
- Tastatur-Navigation (↑↓ Enter Esc) vollständig
- Edge-Case: Netzwerkausfall → zeigt gecachte Navigation-Items, Datei-Suche deaktiviert mit Hinweis
4.4 QR-Code scannen (Box-UUID)
- User tippt „Box scannen"-FAB (Floating Action Button, 56px, unten rechts auf Mobile)
- Kamera-Permission-Request (einmalig, Browser-API)
BarcodeDetectorAPI (Native) oder ZXing-WASM-Fallback öffnet Kamera-Preview (Vollbild)- UUID wird erkannt → Vibrations-Feedback (50ms) + visueller Rahmen grün
- Caja navigiert zu
boxes/:uuidDetail-Screen - 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
- Browser-Banner „App installieren" erscheint (automatisch, wenn Manifest + SW vorhanden)
- User tippt „Installieren" → Homescreen-Icon erstellt
- Beim nächsten Start: Standalone-Modus (
display: standalone), kein Browser-Chrome - Service-Worker aktiv: cached Assets + API-Responses
- Offline: Lese-Operationen aus Cache; Schreib-Operationen in Write-Queue (
IndexedDB) - Sync: Bei Netzwerkrückkehr: Queue wird automatisch geleert (Background-Sync API)
- 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 × 2 │ 480.00 │ 🟢 Bezahlt │ ✎ ⋯ │
│ ☐ │ Roberto Pér. │ Mega × 1 │ 320.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)
ResponsiveContainervon 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
PopoveroderSelect(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:
BarcodeDetectorAPI (Chrome/Android nativ, kein JS-Bundle-Overhead)- Polyfill:
@zxing/browsernur 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 separaterocr_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 Formular
→ Speichern: 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/uiSkeleton) - 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 Formularvat_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_*viaLocaleProvider+useT()Hook (stabiler Code → übersetzter Label)app_users.preferred_locale/app_users.themefü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 Labelstheme:changed→ CSS-Variables wechseln, kein Re-Render nötignetwork:offline/network:online→ Offline-Banner + Queue-Flushqr:detected→ Navigation-Event zu gescannter Boxocr:complete→ Formular-Vorausfüll-Event
7. Validierungen & Edge-Cases
7.1 Sprache & i18n
- Fehlender Label: wenn für
(namespace, key, locale)inlocale_strings(bzw. die Inline-label_*-Spalte) kein Eintrag → Fallback: zuerstlocale='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'); immercode === 'EXPENSE' - Neue Codes: Inline-
label_de/es/enam Lookup setzen (bzw.locale_strings-Zeilen für alle 3 Sprachen anlegen), bevor der Code inmovement_typesetc. erscheint
7.2 Touch & Responsivität
- Minimale Tap-Target-Verletzung: automatischer CI-Check via
eslint-plugin-jsx-a11y+ Axe-Tests; Komponenten-Template schreibtmin-h-[44px] min-w-[44px]vor - Horizontaler Scroll auf Mobile verboten: kein
overflow-x-autoauf 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):StaleWhileRevalidatemit 5min TTL; Mutationen:NetworkFirstmit 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
requestAnimationFrameohne Drosselung
7.5 Performance-Budget (Mittelklasse-Android, Ziel-Gerät)
| Metrik | Ziel |
|---|---|
| First Contentful Paint (3G-sim) | < 2.5s |
| Largest Contentful Paint | < 4.0s |
| Time to Interactive | < 5.0s |
| JS-Bundle (initial, gzip) | < 200KB |
| Bilder/Icons | SVG 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 separaterocr_temp-Bucket — Dok 30 §6.3 kennt nurkyc/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 incontactsgespeichert - Supabase Storage:
documents-Bucket aufprivate(kein öffentlicher Zugriff); Download nur via signierte URLs (1h Ablauf) - Audit-Log: alle
contacts-Änderungen durchaudit_log(Modul 8) erfasst; Pflicht bei OCR-Vorausfüllung (Quelle =ocr)
8.2 Barrierefreiheit (WCAG 2.1 AA)
- Kontrast: Primärfarbe Navy
#00205Bauf Weiß: Kontrast 12.5:1 (AA + AAA); Akzent-Rot#CE1126auf 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: noneentfernt; shadcn/ui verwendetfocus-visible:ring-2 - Screenreader: Bottom-Sheets mit
aria-modal,aria-labelledby,role="dialog"; Kamera-Preview mitaria-livefür Status-Meldungen - Reduce-Motion:
prefers-reduced-motion: reduce→ alleanimate-*Klassen deaktiviert; Kamera-Viewfinder-Animation entfernt - Sprachdeklaration:
<html lang>Attribut dynamisch ausapp_users.preferred_localegesetzt
8.3 PWA-Sicherheit
- HTTPS erzwungen: kein HTTP-Zugriff (Vercel erzwingt HTTPS)
- Service-Worker:
scopeauf/caja/begrenzt; kein Cachen von Auth-Tokens im SW-Cache - Content-Security-Policy: Camera-Quelle explizit erlaubt (
media-src 'self'); keineunsafe-evalfür WASM →wasm-unsafe-evalstattdessen - 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: swapverhindert 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