# Frontend

Pages, components, design system, state management, landing pages

# Frontend Architecture

# Pages

# Drop Frontend — Pages

> All pages are in `src/drop-app/src/app/` using Next.js App Router file-based routing.

---

## Page Index

| Route | File | Type | Auth Required | BottomNav |
|-------|------|------|---------------|-----------|
| `/` | `page.tsx` | Server | No | No |
| `/login` | `login/page.tsx` | Client | No | No |
| `/register` | `register/page.tsx` | Client | No | No |
| `/dashboard` | `dashboard/page.tsx` | Client | Yes | Yes |
| `/accounts` | `accounts/page.tsx` | Client | Yes | Yes |
| `/transactions` | `transactions/page.tsx` | Client | Yes | Yes |
| `/scan` | `scan/page.tsx` | Client | Yes | Yes |
| `/send` | `send/page.tsx` | Client | Yes | No |
| `/profile` | `profile/page.tsx` | Client | Yes | Yes |
| `/profile/personal` | `profile/personal/page.tsx` | Client | Yes | No |
| `/profile/security` | `profile/security/page.tsx` | Client | Yes | No |
| `/profile/notifications` | `profile/notifications/page.tsx` | Client | Yes | No |
| `/profile/language` | `profile/language/page.tsx` | Client | Yes | No |
| `/notifications` | `notifications/page.tsx` | Client | Yes | Yes |
| `/cards` | `cards/page.tsx` | Client | Yes | No |
| `/complaints` | `complaints/page.tsx` | Client | No | No |
| `/fees` | `fees/page.tsx` | Client | No | No |
| `/privacy` | `privacy/page.tsx` | Client | No | No |
| `/terms` | `terms/page.tsx` | Client | No | No |
| `/withdrawal` | `withdrawal/page.tsx` | Client | No | No |

---

## Page Details

### `/` — Home / Marketing Page
- **File:** `app/page.tsx`
- **Type:** Server component (no "use client")
- **Auth:** None
- **Components used:** DropLogoFull, DropAppIcon, Link, Image, custom icons (IconSendMoney, IconQrScan, IconVirtualCard, IconShield, IconFastTransfer, IconCorridors)
- **Data:** Static arrays `features` (3 items) and `stats` (3 items) defined inline
- **Sections:**
  1. Header with DropLogoFull + nav links (Tjenester, Priser, Om oss, Logg inn, Kom i gang)
  2. Hero with headline, subtext, CTA buttons, phone mockup placeholder
  3. Features grid (Send penger, Betal med QR, Virtuelt kort)
  4. Stats bar (0.5% Gebyr, <2t Leveringstid, 30+ Land)
  5. Trust section (BankID verified, Rask overføring, 30+ land)
  6. Merchant CTA section
  7. Footer with ALAI Holding AS credit

### `/login` — Login
- **File:** `app/login/page.tsx`
- **Type:** Client component
- **Auth:** None (entry point)
- **Components used:** Image, Link, Button, Mail/Lock/Eye/EyeOff/ArrowRight (lucide)
- **State:** email, password, showPassword, error, loading
- **Data fetching:** POST `/api/auth/login` with `{ email, password }`
- **Validation:** Email regex `^[^\s@]+@[^\s@]+\.[^\s@]+$`, required fields check
- **On success:** `router.push("/dashboard")`
- **Social login buttons:** BankID, Vipps (UI only, not functional)
- **Dev mode:** Shows demo credentials `amir@example.com / demo1234`

### `/register` — Registration
- **File:** `app/register/page.tsx`
- **Type:** Client component
- **Auth:** None
- **Components used:** ArrowLeft/ArrowRight/Check/Eye/EyeOff/Phone/Mail/User/Calendar/Lock (lucide), Button
- **State:** step (1-4), form fields, otp, pin, errors
- **Steps:**
  1. **Info** — firstName, lastName, email, phone (+47 prefix), dateOfBirth, password
  2. **Verify** — 6-digit OTP input (MVP: any 6 digits accepted)
  3. **PIN** — 4-digit PIN with custom numpad UI
  4. **Success** — Welcome message, redirect to /dashboard
- **Validation:**
  - Age >= 18 (calculated from dateOfBirth)
  - XSS protection: names reject `< > " ' & ; ( ) { } [ ]`
  - Password: min 8 chars, must contain letters AND numbers
  - Phone: must be 8 digits (Norwegian format)
- **Data fetching:** POST `/api/auth/register`

### `/dashboard` — Main Dashboard
- **File:** `app/dashboard/page.tsx`
- **Type:** Client component
- **Auth:** Yes (`useAuth()` with redirect)
- **Components used:** DropLogo, BottomNav, ScrollArea, Bell/LogOut (lucide)
- **State:** transactions, loading
- **Data fetching:** GET `/api/transactions?limit=10`
- **Interfaces:** `Transaction { id, type, status, amount, currency, recipientName, createdAt }`
- **Layout:**
  1. Header: DropLogo + notification bell + logout + avatar initials
  2. Balance card: primary account balance, formatted NOK
  3. Action buttons: Send penger (→ /send), Skann QR (→ /scan)
  4. Recent transactions list in ScrollArea
  5. BottomNav

### `/accounts` — Bank Accounts
- **File:** `app/accounts/page.tsx`
- **Type:** Client component
- **Auth:** Yes (`useAuth()`)
- **Components used:** BottomNav, Card, ArrowLeft/Landmark/Plus/ChevronRight (lucide)
- **Data:** Reads `user.bankAccounts` from auth hook (no separate fetch)
- **Layout:**
  1. PSD2/Open Banking info banner (blue)
  2. Account cards: bankName, masked accountNumber, balance, currency, isPrimary badge
  3. Total balance summary
  4. "Legg til bankkonto" button (BankID connection note)

### `/transactions` — Transaction History
- **File:** `app/transactions/page.tsx`
- **Type:** Client component
- **Auth:** Yes (`useAuth()`)
- **Components used:** BottomNav, Tabs/TabsList/TabsTrigger, ArrowLeft/Clock (lucide)
- **State:** transactions, filter, loading
- **Data fetching:** GET `/api/transactions?type={filter}&limit=50`
- **Filters:** Alle (all), Overforinger (remittance), QR-betalinger (qr_payment)
- **Grouping:** `groupByDate()` function groups into: I dag, I gar, Denne uken, Eldre
- **Display:** Amount with +/- prefix and color coding (green for received, red for sent)

### `/scan` — QR Scanner
- **File:** `app/scan/page.tsx`
- **Type:** Client component
- **Auth:** Yes (`useAuth()`)
- **Components used:** BottomNav, Button, ArrowLeft/Camera/Check/X/Store (lucide)
- **State:** scanState (scanning | payment | paying | success), scannedMerchant, amount, paymentResult
- **Interfaces:** `ScannedMerchant { id, name, category }`, `PaymentResult { id, status, amount, fee, merchant }`
- **Flow:**
  1. **Scanning** — Camera viewfinder UI with scan frame, "Simuler skanning" button (demo)
  2. **Payment** — Shows merchant info, amount input, 1% fee calculation
  3. **Paying** — Loading spinner
  4. **Success** — Confirmation with transaction details
- **Data fetching:** POST `/api/transactions/qr-payment` with `{ merchantId, amount }`
- **Demo merchant:** "Ahmetov Kebab" (id: "merchant_001", category: "Restaurant")

### `/send` — Send Money (Remittance)
- **File:** `app/send/page.tsx`
- **Type:** Client component
- **Auth:** Yes (`useAuth()`)
- **Components used:** Button, ArrowLeft/ArrowRight/Check/ChevronDown/Globe/User (lucide)
- **State:** step (1-4), selectedRecipient, amount, recipients, rates, sending, txResult
- **Steps:**
  1. **Select Recipient** — List from GET `/api/recipients`, shows name + country flag
  2. **Enter Amount** — NOK input, real-time conversion with exchange rate, 0.5% fee display
  3. **Review** — Summary of recipient, amount, rate, fee, total
  4. **Success** — Confirmation with reference number
- **Data fetching:**
  - GET `/api/recipients` (on mount)
  - GET `/api/rates` (on mount)
  - POST `/api/transactions/remittance` with `{ recipientId, amountNOK, targetCurrency }`
- **Country flags:** RS (Serbia), BA (Bosnia), TR (Turkey), PK (Pakistan), PL (Poland)
- **Interface:** `TxResult { id, status, amount, fee, rate, recipientName, targetAmount, targetCurrency }`

### `/profile` — User Profile
- **File:** `app/profile/page.tsx`
- **Type:** Client component
- **Auth:** Yes (`useAuth()`)
- **Components used:** BottomNav, ArrowLeft/ChevronRight/LogOut/Settings/Shield/HelpCircle/Bell/CreditCard/Landmark (lucide)
- **Data:** Reads `user` from auth hook
- **Layout:**
  1. User info card with initials avatar (green bg), full name, email
  2. Menu items: Mine kontoer (→ /accounts), Varsler, Innstillinger, Sikkerhet, Hjelp og stotte
  3. Logout button with confirmation
  4. Version: "Drop v0.1.0 · ALAI Holding AS"

### `/withdrawal` — Angrerett (Right of Withdrawal)
- **File:** `app/withdrawal/page.tsx`
- **Type:** Client component
- **Auth:** No
- **Components used:** ChevronLeft, RotateCcw, CheckCircle (lucide)
- **State:** submitted, loading
- **Purpose:** Norwegian angrerettloven compliance — 14-day right of withdrawal form
- **Layout:**
  1. Info section explaining angrerett (right to cancel service agreement within 14 days)
  2. Warning banner: Angrerett does not apply to completed payment transactions, only to the service agreement itself
  3. Form with optional reason dropdown (not_needed, alternative, not_satisfied, other) and comment textarea
  4. Submit button (red) with AML retention notice (data kept for 5 years per hvitvaskingsloven)
  5. Success screen with confirmation message (14-day processing time)

---

### `/complaints` — Send Complaint
- **File:** `app/complaints/page.tsx`
- **Type:** Client component
- **Auth:** Yes (`useAuth()`)
- **Components used:** MessageSquare, CheckCircle, ChevronLeft, ExternalLink (lucide)
- **State:** submitted, loading, formData (category, subject, description)
- **Data fetching:** POST `/api/complaints` with `{ category, subject, description }`
- **Purpose:** Finansavtaleloven §3-53 compliance — formal complaint submission with 15 business day response requirement
- **Layout:**
  1. Info text: All complaints taken seriously, up to 15 business days processing time
  2. Form with required fields:
     - Category dropdown: transaction, fees, service, privacy, other
     - Subject text input (max 200 chars)
     - Description textarea (max 2000 chars)
  3. Submit button with POST to `/api/complaints`
  4. External complaint authority section: Finansklagenemnda (FinKN) contact info with link to finansklagenemnda.no
  5. Success screen: Complaint received, 15 business day review commitment

---

### `/privacy` — Privacy Policy
- **File:** `app/privacy/page.tsx`
- **Type:** Client component
- **Auth:** None
- **Components used:** ChevronLeft, Shield (lucide)
- **Purpose:** GDPR-compliant privacy policy page (Norwegian language)
- **Sections:**
  1. **Behandlingsansvarlig:** ALAI Holding AS as data controller
  2. **Hvilke opplysninger vi samler inn:** Identification (name, email, phone, BankID), Financial data (bank accounts via Open Banking, transaction history), Technical data (IP, device, app version)
  3. **Formaal med behandlingen:** Transaction processing (PSD2/PISP), KYC/AML compliance, AML/terrorism prevention, service improvement
  4. **Rettslig grunnlag:** Contract (GDPR 6(1)(b)), Legal obligation (6(1)(c)) for AML/KYC, Legitimate interest (6(1)(f)) for service improvement
  5. **Dine rettigheter:** Innsyn (access), Retting (rectification), Sletting (erasure with 5-year AML retention), Dataportabilitet (portability)
  6. **Oppbevaring:** Minimum 5 years per hvitvaskingsloven, anonymization on account deletion but AML data retained
  7. **Kontakt:** personvern@getdrop.no, complaint to Datatilsynet

---

### `/terms` — Terms of Service
- **File:** `app/terms/page.tsx`
- **Type:** Client component
- **Auth:** None
- **Components used:** ChevronLeft, FileText (lucide)
- **Purpose:** Legal terms of service (Norwegian language)
- **Sections:**
  1. **Om tjenesten:** Drop as PISP/AISP under PSD2, provided by ALAI Holding AS
  2. **Krav til brukere:** 18+ age, Norwegian residency with BankID, KYC required, Norwegian phone (+47)
  3. **Betalingsmodell:** Drop never holds customer money, pass-through model via Open Banking
  4. **Gebyrer:** All fees shown before transaction confirmation, see fees page for full list
  5. **Ansvar:** Drop responsible for correct payment execution per betalingstjenesteloven, refund rights per law, not liable for bank/recipient delays
  6. **Misbruk og sperring:** Right to block accounts for suspected AML/fraud, mandatory STR reporting to Økokrim/EFE
  7. **Angrerett:** 14-day withdrawal right per angrerettloven (does not apply to completed transactions)
  8. **Tvister:** Norwegian law, Oslo tingrett jurisdiction, complaint to Finansklagenemnda

---

### `/fees` — Fee Overview
- **File:** `app/fees/page.tsx`
- **Type:** Client component
- **Auth:** None
- **Components used:** ChevronLeft, Receipt (lucide)
- **Purpose:** Transparent fee disclosure page (Norwegian language)
- **Sections:**
  1. **Overføring til utlandet:**
     - Transaction fee: 1.5% per transfer
     - Currency markup: 0.5% on mid-market rate
     - Typical total: ~2% (compare with banks at 3-7%)
  2. **QR-betaling i butikk:**
     - For customer: Free (no charge to payer)
     - For merchant: 0.5% (lower than card terminals)
  3. **Kontotjenester:**
     - Account creation: Free
     - Monthly fee: Free
     - Bank account linking (AISP): Free
  4. **Viktig informasjon:**
     - Fees can change with 30-day notice
     - Exchange rates updated in real-time from market
     - Always see final amount before confirming

---

### `/profile/personal` — Personal Information
- **File:** `app/profile/personal/page.tsx`
- **Type:** Client component
- **Auth:** Yes (`useAuth()`)
- **Components used:** ChevronLeft, ShieldCheck (lucide), BottomNav
- **State:** Reads `user` from auth hook
- **Layout:**
  1. User avatar with initials (green gradient)
  2. Full name and email display
  3. Read-only form fields: firstName, lastName, email, phone (+47 987 65 432), dateOfBirth (15. mars 1995)
  4. BankID verification badge (green banner with ShieldCheck icon)
- **Note:** All fields are disabled (cannot edit) — verified via BankID

---

### `/profile/security` — Security Settings
- **File:** `app/profile/security/page.tsx`
- **Type:** Client component
- **Auth:** Yes (`useAuth()`)
- **Components used:** ChevronLeft, ChevronRight, Lock, Smartphone, Laptop (lucide), BottomNav
- **Sections:**
  1. **Passord:** Change password button (shows "Sist endret: Aldri")
  2. **To-faktor autentisering:**
     - BankID verification: Active (green badge)
     - Vipps verification: Not activated (gray badge)
  3. **Aktive enheter:**
     - iPhone 15 Pro: Oslo, Norge — Aktiv nå (green dot indicator)
     - MacBook Pro: Oslo, Norge — I går kl. 18:45
  4. Footer: Support contact (support@getdrop.no)
- **Note:** UI only, no actual functionality connected

---

### `/profile/notifications` — Notification Settings
- **File:** `app/profile/notifications/page.tsx`
- **Type:** Client component
- **Auth:** Yes (`useAuth()`)
- **Components used:** ChevronLeft, Bell, Mail (lucide), BottomNav
- **State:** pushEnabled, emailEnabled, settingsLoaded
- **Data fetching:**
  - GET `/api/settings` (on mount)
  - PATCH `/api/settings` with `{ pushEnabled: boolean }` or `{ emailEnabled: boolean }` (on toggle)
- **Layout:**
  1. Push-varsler toggle switch (Bell icon)
  2. E-postvarsler toggle switch (Mail icon)
- **Behavior:** Toggles immediately update state and send PATCH request, revert on failure

---

### `/profile/language` — Language Settings
- **File:** `app/profile/language/page.tsx`
- **Type:** Client component
- **Auth:** Yes (`useAuth()`)
- **Components used:** ChevronLeft, Check (lucide), BottomNav
- **State:** selected, saving
- **Data fetching:**
  - GET `/api/settings` (on mount)
  - PATCH `/api/settings` with `{ language: string }` (on save)
- **Languages:** nb (Norsk Bokmål), en (English), bs (Bosanski), sq (Shqip)
- **Layout:**
  1. Language list with radio selection (green checkmark for selected)
  2. "Lagre" button (green) to save selection
- **Behavior:** Selection updates local state, user must click "Lagre" to persist

---

### `/notifications` — Notifications Center
- **File:** `app/notifications/page.tsx`
- **Type:** Client component
- **Auth:** Yes (`useAuth()`)
- **Components used:** BottomNav, ArrowLeft, Bell, ArrowUpRight, ScanLine, Smartphone, TrendingUp (lucide)
- **State:** notifications, fetching
- **Data fetching:**
  - GET `/api/notifications` (on mount)
  - PATCH `/api/notifications` with `{ notificationIds: [ids] }` (mark read, fire-and-forget)
- **Interface:** `Notification { id, type, title, body, read, createdAt }`
- **Layout:**
  1. Header with back button + "Varsler" title
  2. Empty state: Bell icon + "Ingen varsler enna" message
  3. Grouped notifications: I DAG / I GÅR / date groups
  4. Notification cards: icon based on type, title, body, timestamp, unread dot indicator
  5. BottomNav
- **Notification types:** transaction_complete (green), qr_payment (yellow), security (blue), rate_update (yellow), default (gray)
- **Auto-read:** Automatically marks all unread notifications as read on page load
- **Time formatting:** "I dag kl. HH:MM", "I går kl. HH:MM", or "DD.MM.YYYY kl. HH:MM"

---

### `/cards` — Card Management (FUTURE — feature-flagged)

> **Note:** Cards are a FUTURE feature, gated behind feature flags (all default to `false`). Requires a card issuing partner before activation.

- **File:** `app/cards/page.tsx`
- **Type:** Client component
- **Auth:** Yes (`useAuth()`)
- **Feature flags:** `virtualCards` (gate), `physicalCards`, `cardPin`, `spendingLimits` — all default to `false`
- **Components used:** Button, Dialog, various lucide icons (CreditCard, Plus, ArrowLeft, Smartphone, Eye, EyeOff, Lock, X, Check, Copy, RefreshCw)
- **State:** cards, selectedCard, showDetails, showOrderForm, orderForm, loading
- **Data fetching:**
  - GET `/api/cards` (list)
  - POST `/api/cards` (create virtual)
  - POST `/api/cards` with type "physical" + address (order physical)
  - PATCH `/api/cards/{id}` with `{status: "frozen" | "active"}`
  - DELETE `/api/cards/{id}`
- **Card visual:** Green gradient background, masked card number (•••• •••• •••• 4242), expiry, cardholder name

# Component Inventory

# Component Inventory

> **Project:** {{PROJECT_NAME}}
> **Version:** {{VERSION}}
> **Date:** {{DATE}}
> **Author:** {{AUTHOR}}
> **Status:** Draft | In Review | Approved
> **Reviewers:** {{REVIEWERS}}

## Document History
| Version | Date | Author | Changes |
|---------|------|--------|---------|
| 0.1     | {{DATE}} | {{AUTHOR}} | Initial draft |

---

## 1. Overview

<!-- GUIDANCE: Summarize the component library. How many components total? What's the coverage across categories? Who owns it? -->

**Total components:** {{N}}
**Library location:** `src/components/`
**Storybook:** `{{https://storybook.PROJECT_NAME.example.com}}`
**Design source:** `{{Figma file URL}}`
**Owner team:** `{{TEAM_NAME}}`

This inventory tracks every reusable component in **{{PROJECT_NAME}}**. It follows Atomic Design categorization: atoms → molecules → organisms → templates → pages.

---

## 2. Component Hierarchy Diagram

<!-- GUIDANCE: Update this diagram to show the actual component composition relationships in your system. -->

```mermaid
graph TB
    subgraph Pages
        PageLogin["LoginPage"]
        PageDashboard["DashboardPage"]
    end
    subgraph Templates
        TplAuth["AuthLayout"]
        TplApp["AppLayout"]
    end
    subgraph Organisms
        OrgNavbar["Navbar"]
        OrgSidebar["Sidebar"]
        OrgDataTable["DataTable"]
        OrgUserForm["UserForm"]
    end
    subgraph Molecules
        MolFormField["FormField"]
        MolCard["Card"]
        MolSearchBar["SearchBar"]
        MolDropdown["Dropdown"]
        MolPagination["Pagination"]
    end
    subgraph Atoms
        AtomButton["Button"]
        AtomInput["Input"]
        AtomBadge["Badge"]
        AtomSpinner["Spinner"]
        AtomAvatar["Avatar"]
        AtomIcon["Icon"]
    end

    Pages --> Templates
    Templates --> Organisms
    Organisms --> Molecules
    Molecules --> Atoms
```

**TODO:** Update diagram to reflect actual component tree.

---

## 3. Atoms (Primitive Components)

<!-- GUIDANCE: Add one entry per primitive component. Props table must be complete before marking as Done. -->

### 3.1 Button

| Property | Value |
|----------|-------|
| **Category** | Atom |
| **Status** | `{{Done | WIP | Planned | Deprecated}}` |
| **File path** | `src/components/ui/Button/Button.tsx` |
| **Storybook** | `{{URL}}` |
| **Design ref** | `{{Figma frame URL}}` |

**Props API:**

| Prop | Type | Default | Required | Description |
|------|------|---------|----------|-------------|
| `variant` | `'primary' \| 'secondary' \| 'ghost' \| 'danger' \| 'link'` | `'primary'` | No | Visual style variant |
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | No | Button size |
| `disabled` | `boolean` | `false` | No | Disables interaction |
| `loading` | `boolean` | `false` | No | Shows spinner, disables click |
| `leftIcon` | `ReactNode` | `undefined` | No | Icon before label |
| `rightIcon` | `ReactNode` | `undefined` | No | Icon after label |
| `onClick` | `(e: MouseEvent) => void` | `undefined` | No | Click handler |
| `type` | `'button' \| 'submit' \| 'reset'` | `'button'` | No | HTML button type |
| `fullWidth` | `boolean` | `false` | No | 100% width |

**Variants & States:** primary, secondary, ghost, danger, link × default, hover, focus, active, disabled, loading

**Accessibility:**
- Renders as `<button>` — never `<div>` or `<span>`
- `loading` state: `aria-busy="true"`, spinner has `aria-label="Loading"`
- `disabled`: `aria-disabled="true"`, not removed from tab order
- Focus ring: 2px offset, brand color

**Dependencies:** Icon component, Spinner component

---

### 3.2 Input

| Property | Value |
|----------|-------|
| **Category** | Atom |
| **Status** | `{{Status}}` |
| **File path** | `src/components/ui/Input/Input.tsx` |
| **Storybook** | `{{URL}}` |

**Props API:**

| Prop | Type | Default | Required | Description |
|------|------|---------|----------|-------------|
| `type` | `'text' \| 'email' \| 'password' \| 'number' \| 'search' \| 'tel' \| 'url'` | `'text'` | No | Input type |
| `value` | `string` | — | Yes (controlled) | Input value |
| `onChange` | `(e: ChangeEvent) => void` | — | Yes | Change handler |
| `placeholder` | `string` | `''` | No | Placeholder text |
| `disabled` | `boolean` | `false` | No | Disabled state |
| `error` | `boolean` | `false` | No | Triggers error visual state |
| `errorMessage` | `string` | `undefined` | No | Error text (also sets `aria-describedby`) |
| `leftAdornment` | `ReactNode` | `undefined` | No | Icon or element left of input |
| `rightAdornment` | `ReactNode` | `undefined` | No | Icon or element right of input |
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | No | Input height/font size |

**Variants & States:** default, focused, error, disabled, with-left-icon, with-right-icon

**Accessibility:**
- Must always be paired with `<label>` (use FormField molecule)
- Error: `aria-invalid="true"` + `aria-describedby` pointing to error message id
- Password: toggle visibility button must have `aria-label`

---

### 3.3 Badge

| Property | Value |
|----------|-------|
| **Category** | Atom |
| **Status** | `{{Status}}` |
| **File path** | `src/components/ui/Badge/Badge.tsx` |

**Props API:**

| Prop | Type | Default | Required | Description |
|------|------|---------|----------|-------------|
| `variant` | `'success' \| 'warning' \| 'error' \| 'info' \| 'neutral'` | `'neutral'` | No | Semantic color variant |
| `size` | `'sm' \| 'md'` | `'md'` | No | Badge size |
| `dot` | `boolean` | `false` | No | Shows colored dot instead of text |
| `children` | `ReactNode` | — | Yes | Badge content |

**TODO:** Add remaining atom components following the same pattern (Select, Checkbox, Radio, Toggle, Textarea, Avatar, Tooltip, Spinner, Divider).

---

## 4. Molecules (Composite Components)

<!-- GUIDANCE: More complex components composed of atoms. Focus on the composition and API surface. -->

### 4.1 FormField

| Property | Value |
|----------|-------|
| **Category** | Molecule |
| **Status** | `{{Status}}` |
| **File path** | `src/components/ui/FormField/FormField.tsx` |
| **Composes** | Input, Label, ErrorMessage, HelpText |

**Props API:**

| Prop | Type | Default | Required | Description |
|------|------|---------|----------|-------------|
| `label` | `string` | — | Yes | Visible label text |
| `htmlFor` | `string` | — | Yes | Links label to input id |
| `error` | `string` | `undefined` | No | Error message text |
| `helpText` | `string` | `undefined` | No | Helper text below input |
| `required` | `boolean` | `false` | No | Shows required indicator |
| `children` | `ReactNode` | — | Yes | Input component |

**Accessibility:** Label always associated with input via `htmlFor`/`id` pair.

---

### 4.2 Card

| Property | Value |
|----------|-------|
| **Category** | Molecule |
| **Status** | `{{Status}}` |
| **File path** | `src/components/ui/Card/Card.tsx` |

**Props API:**

| Prop | Type | Default | Required | Description |
|------|------|---------|----------|-------------|
| `variant` | `'default' \| 'bordered' \| 'elevated'` | `'default'` | No | Visual style |
| `padding` | `'sm' \| 'md' \| 'lg' \| 'none'` | `'md'` | No | Internal padding |
| `as` | `ElementType` | `'div'` | No | Polymorphic render element |
| `children` | `ReactNode` | — | Yes | Card content |

**Sub-components:** `Card.Header`, `Card.Body`, `Card.Footer`

**TODO:** Add remaining molecule components: Modal, Dropdown, Table, Pagination, Toast, SearchBar, Breadcrumb, Tabs, Accordion, DatePicker.

---

## 5. Organisms (Complex Components)

<!-- GUIDANCE: Full UI sections composed of molecules and atoms. These are feature-aware but not page-specific. -->

### 5.1 DataTable

| Property | Value |
|----------|-------|
| **Category** | Organism |
| **Status** | `{{Status}}` |
| **File path** | `src/components/features/DataTable/DataTable.tsx` |
| **Dependencies** | Table (molecule), Pagination, SearchBar, Spinner, Badge |

**Features:**
- Sortable columns (client + server-side)
- Pagination (configurable page sizes)
- Row selection (single + multi)
- Column visibility toggle
- Export action slot
- Loading skeleton state
- Empty state slot
- Row action slot (per-row dropdown)

**TODO:** Document full props API for DataTable.

---

### 5.2 Navigation / Sidebar

| Property | Value |
|----------|-------|
| **Category** | Organism |
| **Status** | `{{Status}}` |
| **File path** | `src/components/layouts/Sidebar/Sidebar.tsx` |

**TODO:** Add remaining organism components.

---

## 6. Shared Hooks / Composables Inventory

<!-- GUIDANCE: List all shared hooks that are used across multiple components or features. -->

| Hook | File | Purpose | Used By |
|------|------|---------|---------|
| `useDebounce` | `src/hooks/useDebounce.ts` | Debounce value changes | SearchBar, Input |
| `useLocalStorage` | `src/hooks/useLocalStorage.ts` | Persistent local state | Theme, preferences |
| `useMediaQuery` | `src/hooks/useMediaQuery.ts` | Responsive breakpoint checks | Layout components |
| `useClickOutside` | `src/hooks/useClickOutside.ts` | Close on outside click | Dropdown, Modal |
| `useFocusTrap` | `src/hooks/useFocusTrap.ts` | Trap focus inside element | Modal, Drawer |
| `useToast` | `src/hooks/useToast.ts` | Trigger toast notifications | Global |
| `usePermission` | `src/hooks/usePermission.ts` | Check user permissions | Auth-gated components |
| `{{HOOK_NAME}}` | `{{PATH}}` | `{{PURPOSE}}` | `{{CONSUMERS}}` |

---

## 7. Third-Party Component Usage

<!-- GUIDANCE: Document any third-party UI libraries used and the wrapping strategy. -->

| Package | Version | Components Used | Wrapping Strategy |
|---------|---------|-----------------|-------------------|
| `{{@radix-ui/react-dialog}}` | `{{1.x}}` | Modal base | Wrapped in `src/components/ui/Modal` — custom styling |
| `{{@radix-ui/react-select}}` | `{{2.x}}` | Select base | Wrapped in `src/components/ui/Select` |
| `{{react-hook-form}}` | `{{7.x}}` | Form state | Used directly + FormField wrapper |
| `{{recharts}}` | `{{2.x}}` | Charts | Wrapped in `src/components/charts/` |

**Policy:** Never use third-party components directly in feature code — always wrap in local component to control API surface and allow future swap.

---

## 8. Component Deprecation Process

<!-- GUIDANCE: Define the lifecycle stages and how deprecation is communicated to the team. -->

```
Active → Deprecated (soft) → Deprecated (hard) → Removed
```

| Stage | Action Required |
|-------|----------------|
| **Deprecated (soft)** | Add `@deprecated` JSDoc comment, console warning in dev, migration guide in Storybook |
| **Deprecated (hard)** | TypeScript `@deprecated` annotation triggers IDE warning, added to removal milestone |
| **Removed** | Delete component, update CHANGELOG, run codemod if available |

**Deprecation notice minimum period:** 2 sprint cycles before hard removal.

**TODO:** Link to CHANGELOG.md for tracked deprecations.

---

## Approval
| Role | Name | Date | Signature |
|------|------|------|-----------|
| Author | | | |
| Frontend Lead | | | |
| Design System Owner | | | |

# Design System Documentation

# Design System Documentation

> **Project:** Drop — Fintech Payment App
> **Version:** 0.1.0
> **Date:** 2026-02-23
> **Author:** John (AI Director, ALAI)
> **Status:** In Review
> **Reviewers:** Alem Bašić (CEO)

## Document History
| Version | Date | Author | Changes |
|---------|------|--------|---------|
| 0.1     | 2026-02-23 | John | Initial draft from source code + brand guide analysis |

---

## 1. Design Principles

| Principle | Description |
|-----------|-------------|
| **Clarity first** | Every UI element must communicate its purpose without explanation. Amounts are large and legible. Actions are labeled. |
| **Scandinavian minimal** | Generous whitespace, no visual noise. Forest green + white — nothing extra. |
| **Trust through transparency** | Fees visible before every transaction. No hidden costs. PSD2 disclosures shown explicitly. |
| **Mobile-first** | The app is a mobile PWA. `max-w-sm` containers, `pb-24` for BottomNav clearance, touch targets ≥ 44×44px. |
| **Accessible by default** | WCAG AA compliance is a baseline requirement, not an optional enhancement. |
| **Light mode only (Phase 1)** | Background is always off-white (#FAFCF8) or white. Dark mode is Phase 2. |

---

## 2. Color System

### 2.1 Primitive Palette (Raw Values)

```css
/* Brand primitives — defined in brand/colors.css and globals.css */

/* Green scale */
--color-green-primary:   #0B6E35;   /* Forest Green — primary brand */
--color-green-dark:      #095C2C;   /* Hover/pressed state */
--color-green-light:     #E8F5E9;   /* Light green tint, badges */

/* Gold scale */
--color-gold-primary:    #D4A017;   /* Gold accent, logo arrow, premium elements */
--color-gold-light:      #FFF8E1;   /* Gold tint */

/* Neutral scale */
--color-neutral-0:       #FFFFFF;   /* White — cards, elevated surfaces */
--color-neutral-50:      #FAFCF8;   /* Off-white — page background */
--color-neutral-100:     #F9FAFB;   /* Input field backgrounds */
--color-neutral-200:     #F3F4F6;   /* Section backgrounds, shadcn secondary */
--color-neutral-300:     #E5E7EB;   /* Input borders, card borders, dividers */
--color-neutral-400:     #D1D5DB;   /* Horizontal rule dividers */
--color-neutral-500:     #9CA3AF;   /* Muted text, inactive nav items */
--color-neutral-600:     #6B7280;   /* Secondary text, placeholders */
--color-neutral-700:     #374151;   /* Dark text on light backgrounds */
--color-neutral-900:     #1A1A1A;   /* Near-black — headings, primary text */
--color-neutral-1000:    #000000;

/* Semantic */
--color-success:         #10B981;   /* Emerald — success, positive amounts */
--color-error:           #EF4444;   /* Red — error states, negative amounts */
--color-warning:         #D97706;   /* Warning — pending transactions */
--color-info:            #2563EB;   /* Info — PSD2 banners */
```

### 2.2 Semantic Tokens (Light Mode — from globals.css :root)

```css
:root {
  /* shadcn/ui CSS variable tokens */
  --background:           #FAFCF8;
  --foreground:           #1A1A1A;
  --primary:              #0B6E35;
  --primary-foreground:   #FFFFFF;
  --secondary:            #F3F4F6;
  --secondary-foreground: #1A1A1A;
  --accent:               #F3F4F6;
  --accent-foreground:    #1A1A1A;
  --muted:                #F3F4F6;
  --muted-foreground:     #6B7280;
  --destructive:          #EF4444;
  --border:               #E5E7EB;
  --input:                #E5E7EB;
  --ring:                 #0B6E35;
  --radius:               0.75rem;

  /* Drop custom semantic tokens */
  --color-drop-primary:   #0B6E35;
  --color-drop-secondary: #D4A017;
  --color-drop-accent:    #10B981;
  --color-drop-dark:      #1A1A1A;
  --color-drop-light:     #FAFCF8;
  --color-drop-error:     #EF4444;
}
```

### 2.3 Semantic Tokens (Dark Mode)

```css
/* Phase 2 — Dark mode TBD */
[data-theme="dark"] {
  /* TBD — requires design review */
  /* Drop is light-mode-first. Dark mode planned for Phase 2. */
}
```

### 2.4 Brand Gradient

```css
.bg-gradient-brand {
  background: linear-gradient(135deg, #0B6E35 0%, #D4A017 100%);
}
```

### 2.5 Contrast Ratios (WCAG)

| Pair | Text Color | Background | Ratio | WCAG AA (4.5:1) | WCAG AAA (7:1) |
|------|-----------|------------|-------|-----------------|----------------|
| Primary text on page bg | `#1A1A1A` | `#FAFCF8` | ~18.6:1 | Pass | Pass |
| Secondary text on page bg | `#6B7280` | `#FAFCF8` | ~4.7:1 | Pass | Fail |
| Primary button text | `#FFFFFF` | `#0B6E35` | ~5.0:1 | Pass | Fail |
| Success text | `#10B981` | `#FFFFFF` | ~3.0:1 | Fail (large text only) | Fail |
| Error text | `#EF4444` | `#FFFFFF` | ~3.9:1 | Fail (large text only) | Fail |
| Muted text | `#9CA3AF` | `#FFFFFF` | ~2.9:1 | Fail | Fail |

**Action items:** Success (#10B981), error (#EF4444), and muted (#9CA3AF) colors fail WCAG AA for small text. These are used for semantic indicators (amount colors, hints) — always paired with a non-color indicator. Full audit in `accessibility-audit.md`.

---

## 3. Typography

### 3.1 Font Families

| Token | Value | Usage |
|-------|-------|-------|
| `--font-fraunces` | `Fraunces, Georgia, "Times New Roman", serif` | Display/headings, logo wordmark, brand text |
| `--font-dm-sans` | `"DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif` | Body text, UI labels, inputs (default body font) |
| `--font-geist-mono` | `"Geist Mono", "JetBrains Mono", "Fira Code", "Courier New", monospace` | Code, monospace elements |

**Why these fonts:**
- **Fraunces** — Variable serif with "wonky" optical size. Gives character and warmth. Differentiates Drop from every other fintech app (which all use sans-serif). Signals: "we are different, we are human."
- **DM Sans** — Clean geometric sans-serif. Readable at all screen sizes. Friendly without being playful. Good number legibility (critical for fintech).

### 3.2 Type Scale

| Token | Size | Weight | Line Height | Letter Spacing | Usage |
|-------|------|--------|-------------|----------------|-------|
| Display | 56px | 700 (Fraunces) | 1.1 | -0.02em | Hero headlines (landing page) |
| H1 | 40px | 600 (Fraunces) | 1.2 | -0.01em | Page titles |
| H2 | 32px | 600 (Fraunces) | 1.3 | -0.005em | Section headers |
| H3 | 24px | 500 (Fraunces) | 1.3 | normal | Sub-sections |
| H4 | 20px | 600 (DM Sans) | 1.4 | normal | Card titles |
| Body Large | 18px | 400 (DM Sans) | 1.7 | normal | Lead paragraphs |
| Body | 16px | 400 (DM Sans) | 1.6 | normal | Default text |
| Body Small | 14px | 400 (DM Sans) | 1.5 | normal | Secondary content, captions |
| Label | 14px | 500 (DM Sans) | 1.4 | 0.01em | Form labels, UI labels |
| Caption | 12px | 500 (DM Sans) | 1.4 | 0.02em | Timestamps, meta, hints |

### 3.3 In-App Tailwind Typography Patterns

| Context | Tailwind Class | Example |
|---------|---------------|---------|
| Logo wordmark | `font-[family-name:var(--font-fraunces)] text-3xl font-bold` | "drop" on login |
| Page heading | `text-xl font-semibold text-[#1A1A1A]` | Dashboard greeting |
| Section heading | `text-lg font-semibold text-[#1A1A1A]` | "Siste transaksjoner" |
| Form label | `text-sm font-medium text-[#1A1A1A]` | "E-post" |
| Body text | Inherited DM Sans | General content |
| Muted text | `text-sm text-[#6B7280]` | Descriptions, timestamps |
| Small text | `text-xs text-[#9CA3AF]` | Footers, hints |
| Balance (large) | `text-2xl font-bold text-[#1A1A1A]` | Balance display |
| Amount (list) | `text-sm font-semibold` + color | Transaction amounts |

---

## 4. Spacing & Layout

### 4.1 Spacing Scale (4px Base Unit — Tailwind defaults)

| Tailwind | Value | Usage |
|----------|-------|-------|
| `p-1` | 4px | Micro gaps |
| `p-2` | 8px | Tight inline spacing |
| `p-3` | 12px | Compact elements |
| `p-4` | 16px | Default content spacing |
| `p-6` | 24px | Card padding, section padding |
| `p-8` | 32px | Large section gaps |
| `px-6` | 24px | Page horizontal padding (standard) |
| `pb-24` | 96px | Bottom padding (BottomNav clearance) |

### 4.2 Page Structure Patterns

```css
/* Standard authenticated page */
min-h-screen bg-[#FAFCF8]      /* Off-white background */
px-6 pb-24 pt-6               /* Padding: horizontal 24px, bottom 96px (nav), top 24px */

/* Login / onboarding (centered) */
min-h-screen bg-[#EEEEEE]      /* Slightly darker bg for auth pages */
max-w-sm mx-auto               /* 384px max width, centered */

/* Card pattern (standard) */
rounded-2xl bg-white p-6 shadow-sm

/* Card pattern (compact) */
rounded-xl bg-white p-4 shadow-sm
```

### 4.3 Responsive Breakpoints (Tailwind defaults)

| Name | Min Width | Usage |
|------|-----------|-------|
| Default | 0px | Mobile — primary design target |
| `sm` | 640px | Larger phones |
| `md` | 768px | Tablets — landing page 2-column grid |
| `lg` | 1024px | Desktop — landing page 3-column grid |

**Note:** The app is mobile-first. `max-w-sm` containers keep content readable on wider screens.

### 4.4 Bottom Nav

The BottomNav is `fixed bottom-0 left-0 right-0` with `h-16 border-t bg-white`. All pages that include BottomNav must add `pb-24` to their content container.

---

## 5. Component Library

### 5.1 Custom Drop Components

| Component | File | Status | Description |
|-----------|------|--------|-------------|
| BottomNav | `components/bottom-nav.tsx` | Done | Fixed 5-tab navigation |
| DropLogo | `components/drop-logo.tsx` | Done | Forward-D SVG mark |
| DropWordmark | `components/drop-logo.tsx` | Done | "drop" in Fraunces |
| DropLogoFull | `components/drop-logo.tsx` | Done | Logo mark + wordmark |
| DropAppIcon | `components/drop-logo.tsx` | Done | App icon — rounded green square |
| CookieConsent | `components/cookie-consent.tsx` | Done | GDPR consent banner + modal |
| PrePaymentDisclosure | `components/pre-payment-disclosure.tsx` | Done | PSD2 pre-payment modal |
| PWARegister | `components/pwa-register.tsx` | Done | Service Worker registration |
| drop-icons | `components/drop-icons.tsx` | Done | 9 custom fintech icons |

### 5.2 shadcn/ui Primitive Components (Atoms)

| Component | File | Radix Primitive | Status |
|-----------|------|-----------------|--------|
| Alert | `ui/alert.tsx` | — (div-based) | Done |
| Avatar | `ui/avatar.tsx` | @radix-ui/react-avatar | Done |
| Badge | `ui/badge.tsx` | — (cva variants) | Done |
| Button | `ui/button.tsx` | @radix-ui/react-slot | Done |
| Card | `ui/card.tsx` | — (div-based) | Done |
| Dialog | `ui/dialog.tsx` | @radix-ui/react-dialog | Done |
| Input | `ui/input.tsx` | — (input element) | Done |
| ScrollArea | `ui/scroll-area.tsx` | @radix-ui/react-scroll-area | Done |
| Select | `ui/select.tsx` | @radix-ui/react-select | Done |
| Separator | `ui/separator.tsx` | @radix-ui/react-separator | Done |
| Sheet | `ui/sheet.tsx` | @radix-ui/react-dialog | Done |
| Skeleton | `ui/skeleton.tsx` | — (pulse animation) | Done |
| Sonner | `ui/sonner.tsx` | sonner toast library | Done |
| Tabs | `ui/tabs.tsx` | @radix-ui/react-tabs | Done |

### 5.3 Layout Components

| Component | Description |
|-----------|-------------|
| Page wrapper | `min-h-screen bg-[#FAFCF8] px-6 pb-24 pt-6` |
| Auth wrapper | `min-h-screen flex items-center justify-center bg-[#EEEEEE]` |
| Card | `rounded-2xl bg-white p-6 shadow-sm` |
| Scroll area | `ScrollArea` from shadcn/ui for transaction lists |

---

## 6. Iconography Guidelines

| Item | Standard |
|------|----------|
| Primary library | `lucide-react` |
| Delivery | Inline SVG via React component |
| Sizes | `h-4 w-4` (inline/small), `h-5 w-5` (nav/buttons), `h-6 w-6` (feature icons) |
| Stroke width | 1.5px at 24px (Lucide default) |
| Color | `currentColor` — inherits from parent text color |
| Custom icons | `components/drop-icons.tsx` — 9 domain-specific icons |
| Social auth | Inline SVG — BankID (green rounded rect, "ID"), Vipps (orange circle, "V") |

**Custom Drop Icons:**

| Export | Description |
|--------|-------------|
| `IconSendMoney` | Arrow going up-right from horizontal line |
| `IconQrScan` | QR code frame with scan corners |
| `IconVirtualCard` | Credit card outline |
| `IconShield` | Shield with checkmark |
| `IconFastTransfer` | Lightning bolt |
| `IconCorridors` | Globe with meridians |
| `IconWallet` | Wallet outline (unused — no wallet in pass-through model) |
| `IconHistory` | Clock with arrow |
| `IconTopUp` | Plus inside circle (unused — no top-up in pass-through model) |

**Accessibility rule:** Icons conveying meaning must have `aria-label`. Decorative icons: `aria-hidden="true"`.

---

## 7. Motion & Animation Standards

### 7.1 Duration Tokens

| Usage | Value |
|-------|-------|
| Button hover | `transition-colors` (CSS default ~150ms) |
| Focus states | `transition-colors` on border/ring |
| Loading states | Text replacement ("Logger inn...", "Sender...") or Skeleton |
| Scanner frame | `animate-pulse` on scan page corners |
| Complex animations | None in Phase 1 — simplicity-first |

### 7.2 Easing Tokens

| Token | Value | Usage |
|-------|-------|-------|
| `--ease-default` | `cubic-bezier(0.4, 0, 0.2, 1)` | General UI transitions |
| `--ease-enter` | `cubic-bezier(0, 0, 0.2, 1)` | Elements entering |
| `--ease-exit` | `cubic-bezier(0.4, 0, 1, 1)` | Elements leaving |

### 7.3 Reduced Motion

```css
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}
```

**Rule:** Every animated component MUST respect `prefers-reduced-motion`.

---

## 8. Accessibility Requirements Per Component

| Requirement | Buttons | Inputs | Modals | BottomNav | Transaction List |
|-------------|---------|--------|--------|-----------|-----------------|
| Keyboard accessible | Required | Required | Required | Required | Required |
| Focus visible | `ring-[#0B6E35]` | `focus:border-[#0B6E35]` | Focus trap | Required | Required |
| ARIA role | `button` | `textbox` | `dialog` | `nav` | `list`/`listitem` |
| Screen reader label | Visible text or `aria-label` | `<label>` associated | `aria-labelledby` | `aria-label="Navigasjon"` | — |
| Error state | Text message | `text-[#EF4444]` | — | — | — |
| Touch target | `h-12` (48px) | `h-11` (44px) | — | `h-16` (64px) | `min-h-[44px]` |

---

## 9. Design Token Format

### 9.1 CSS Custom Properties (Source of Truth)

```css
/* src/app/globals.css */
:root {
  --color-drop-primary:   #0B6E35;
  --color-drop-secondary: #D4A017;
  --color-drop-accent:    #10B981;
  --color-drop-dark:      #1A1A1A;
  --color-drop-light:     #FAFCF8;
  --color-drop-error:     #EF4444;
  /* shadcn/ui tokens... */
}
```

### 9.2 Tailwind Config Extension

```js
// tailwind.config.ts
export default {
  theme: {
    extend: {
      colors: {
        drop: {
          primary:   '#0B6E35',
          secondary: '#D4A017',
          accent:    '#10B981',
          dark:      '#1A1A1A',
          light:     '#FAFCF8',
          error:     '#EF4444',
        }
      },
      fontFamily: {
        fraunces: ['var(--font-fraunces)', 'Georgia', 'serif'],
        sans:     ['var(--font-dm-sans)', 'system-ui', 'sans-serif'],
        mono:     ['var(--font-geist-mono)', 'monospace'],
      }
    }
  }
}
```

### 9.3 JavaScript/TypeScript Constants (for charting/canvas)

```ts
// For use in non-CSS contexts (future charts, canvas)
export const dropColors = {
  primary:   '#0B6E35',
  secondary: '#D4A017',
  accent:    '#10B981',
  dark:      '#1A1A1A',
  light:     '#FAFCF8',
  error:     '#EF4444',
} as const;
```

**Token update process:** Figma → update `globals.css` CSS variables → update Tailwind config → PR review.

---

## 10. Component Code Patterns

### Primary Button
```tsx
<Button className="h-12 w-full rounded-xl bg-[#0B6E35] text-sm font-semibold text-white shadow-sm hover:bg-[#095C2C]">
  Send penger
</Button>
```

### Outline Button
```tsx
<button className="flex h-12 flex-1 items-center justify-center gap-2 rounded-xl border border-[#E5E7EB] bg-white text-sm font-medium text-[#1A1A1A] hover:bg-[#F9FAFB]">
  Avbryt
</button>
```

### Text Input with Left Icon
```tsx
<div className="relative">
  <Mail className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-[#6B7280]" />
  <input
    className="h-11 w-full rounded-lg border border-[#E5E7EB] bg-[#F9FAFB] pl-10 pr-3 text-sm outline-none focus:border-[#0B6E35] focus:ring-1 focus:ring-[#0B6E35]"
    type="email"
    placeholder="din@epost.no"
  />
</div>
```

### Error Message
```tsx
<p className="rounded-md bg-[#EF4444]/10 p-2 text-sm text-[#EF4444]">
  {error}
</p>
```

### Primary Badge
```tsx
<span className="rounded-full bg-[#0B6E35]/10 px-2 py-0.5 text-xs font-medium text-[#0B6E35]">
  Primær konto
</span>
```

### Avatar (Initials)
```tsx
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-[#0B6E35] text-sm font-bold text-white">
  {initials}
</div>
```

### Transaction Amount (Positive / Negative)
```tsx
// Positive (received)
<span className="text-sm font-semibold text-[#10B981]">+1 250,00 NOK</span>

// Negative (sent)
<span className="text-sm font-semibold text-[#EF4444]">-500,00 NOK</span>
```

---

## Approval
| Role | Name | Date | Signature |
|------|------|------|-----------|
| Author | John (AI Director) | 2026-02-23 | |
| Lead Designer | | | |
| Frontend Lead | | | |
| Accessibility Reviewer | | | |

# State Management Architecture

# State Management Architecture

> **Project:** Drop — Fintech Payment App
> **Version:** 0.1.0
> **Date:** 2026-02-23
> **Author:** John (AI Director, ALAI)
> **Status:** In Review
> **Reviewers:** Alem Bašić (CEO)

## Document History
| Version | Date | Author | Changes |
|---------|------|--------|---------|
| 0.1     | 2026-02-23 | John | Initial draft from source code analysis |

---

## 1. State Architecture Overview

**Drop** uses a deliberately simple, lightweight state management approach. There are no global state libraries (Redux, Zustand, Jotai, etc.). All state is local component state.

| Layer | Mechanism | Scope |
|-------|-----------|-------|
| Auth / user state | `useAuth()` custom hook | Per-component (fetches `/api/auth/me` on each mount) |
| Feature flags | `useFeatureFlag()` hook | Per-component (reads `NEXT_PUBLIC_FF_*` env vars) |
| Page / API data | `useState` + `useEffect` fetch | Component-local |
| Form state | `useState` per field | Component-local |
| UI state (modals, tabs, steps) | `useState` | Component-local |
| Navigation | Next.js `useRouter` / `usePathname` | Framework-provided |
| Auth token (web) | httpOnly cookie | Server-set, never in JS |
| Auth token (mobile) | Bearer token in-memory (`let token`) + `AsyncStorage` | Module-level in `lib/api.js` |

**Guiding principle:** Keep it simple. No global state = no accidental shared state bugs. API data is fetched fresh on each page mount. User authentication is the only cross-cutting concern, handled by the `useAuth()` hook.

---

## 2. Data Flow Diagram

```mermaid
flowchart TD
    User["User Interaction"] --> Component["Component"]

    Component -->|"Auth check (every mount)"| AuthHook["useAuth() Hook\nGET /api/auth/me"]
    Component -->|"Data fetch (useEffect)"| LocalState["useState + useEffect\nLocal component state"]
    Component -->|"Form input"| FormState["useState per field\nComponent-local"]
    Component -->|"Navigate"| Router["Next.js Router\nuseRouter / usePathname"]
    Component -->|"Feature check"| FlagHook["useFeatureFlag()\nReads NEXT_PUBLIC_FF_*"]

    AuthHook -->|"cookie auth"| BFF["BFF API Routes\n/api/auth/me"]
    LocalState -->|"fetch with credentials"| BFF
    BFF -->|"User object + data"| Component

    FlagHook -->|"env var read"| EnvVars["process.env\nNEXT_PUBLIC_FF_*"]
```

---

## 3. State Categories

### 3.1 Auth State — `useAuth()` Hook

**File:** `src/lib/use-auth.ts`

**Interface:**
```typescript
function useAuth(redirectIfUnauthenticated?: boolean): {
  user: User | null;
  loading: boolean;
  logout: () => Promise<void>;
  refreshUser: () => Promise<void>;
}
// Default: redirectIfUnauthenticated = true
```

**User Model:**
```typescript
interface User {
  id: string;
  email: string;
  firstName: string;
  lastName: string;
  totalBalance: number;
  bankAccounts: BankAccount[];
  kycStatus: string;
}

interface BankAccount {
  id: string;
  bankName: string;
  accountNumber: string;
  balance: number;
  currency: string;
  isPrimary: boolean;
}
```

**Behavior:**
1. On mount: fetches `GET /api/auth/me` with `credentials: "include"` (httpOnly cookie auth)
2. If 401 and `redirectIfUnauthenticated` is true: redirects to `/login`
3. `logout()`: calls `POST /api/auth/logout`, clears cookie server-side, redirects to `/login`
4. `refreshUser()`: re-fetches `/api/auth/me` to update user state

**Auth flow:**
```
Login page → POST /api/auth/login → httpOnly cookie set → router.push("/dashboard")
Dashboard → useAuth() → GET /api/auth/me → User object
Logout → POST /api/auth/logout → cookie cleared → redirect /login
```

**Usage patterns:**
```tsx
// Standard protected page (redirects if unauthenticated)
const { user, loading } = useAuth();
if (loading) return <Skeleton />;
// user is guaranteed non-null after loading

// Page that checks auth without redirect
const { user } = useAuth(false);
```

**Pages using this hook (data source):**
- `accounts/page.tsx` — reads `user.bankAccounts` (no separate fetch)
- `profile/page.tsx` — reads `user.firstName`, `user.lastName`, `user.email`

---

### 3.2 Feature Flags — `useFeatureFlag()` Hook

**File:** `src/lib/feature-flags.ts`

**Available Flags:**

| Flag Name | Default | Used In |
|-----------|---------|---------|
| `virtualCards` | `false` | cards page (gate — shows "not available" if false) |
| `physicalCards` | `false` | cards page (order physical card option) |
| `cardDetails` | `false` | cards page (show full card details) |
| `cardFreeze` | `false` | cards page (freeze/unfreeze) |
| `cardPin` | `false` | cards page (change PIN) |
| `spendingLimits` | `false` | cards page (spending limits) |
| `notifications` | `true` | notification features |
| `merchantDashboard` | `true` | merchant page (gate) |

**Environment Variable Pattern:**
```
NEXT_PUBLIC_FF_VIRTUAL_CARDS=true
NEXT_PUBLIC_FF_PHYSICAL_CARDS=false
NEXT_PUBLIC_FF_NOTIFICATIONS=true
NEXT_PUBLIC_FF_MERCHANT_DASHBOARD=true
```
Convention: `NEXT_PUBLIC_FF_` + `SCREAMING_SNAKE_CASE` version of flag name.

**API:**
```typescript
// Server-side (API routes / middleware)
isEnabled(flagName: string): boolean
getAllFlags(): Record<string, boolean>
featureGate(flagName: string): middleware  // Returns 404 if flag disabled

// Client-side (React hooks)
useFeatureFlag(flagName: string): boolean
useFeatureFlags(): Record<string, boolean>
```

**Usage pattern:**
```tsx
// Page-level gate
const cardsEnabled = useFeatureFlag("virtualCards");
if (!cardsEnabled) return <div>Feature ikke tilgjengelig</div>;

// Conditional rendering
const physicalEnabled = useFeatureFlag("physicalCards");
{physicalEnabled && <OrderPhysicalCard />}
```

---

### 3.3 Page Data — `useState` + `useEffect` Fetch (Pattern 1)

Most pages fetch data in a `useEffect` on mount. No SWR, React Query, or other caching library is used.

```tsx
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);

useEffect(() => {
  fetch("/api/endpoint", { credentials: "include" })
    .then(res => res.json())
    .then(json => setData(json.data))
    .catch(() => {})  // Silent error — empty state
    .finally(() => setLoading(false));
}, []);
```

**Pages using this pattern:**
- `dashboard/page.tsx` — fetches `GET /api/transactions?limit=10`
- `transactions/page.tsx` — fetches `GET /api/transactions?type={filter}&limit=50`
- `send/page.tsx` — fetches `GET /api/recipients` and `GET /api/rates`
- `notifications/page.tsx` — fetches `GET /api/notifications`
- `profile/notifications/page.tsx` — fetches `GET /api/settings`
- `profile/language/page.tsx` — fetches `GET /api/settings`
- `merchant/page.tsx` — fetches `/api/merchants/dashboard`, `/api/merchants/transactions`, `/api/merchants/qr`
- `cards/page.tsx` — fetches `GET /api/cards` (feature-flagged)

---

### 3.4 Form State — `useState` Per Field (Pattern 3)

Form pages use async handlers that POST data and handle success/error states. No form library.

```tsx
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);

const handleSubmit = async () => {
  setLoading(true);
  setError("");
  const res = await fetch("/api/auth/login", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    credentials: "include",
    body: JSON.stringify({ email, password }),
  });
  if (res.ok) {
    router.push("/dashboard");
  } else {
    const data = await res.json();
    setError(data.message || "Noe gikk galt");
  }
  setLoading(false);
};
```

**Pages using this pattern:**
- `login/page.tsx` — POST `/api/auth/login`
- `register/page.tsx` — POST `/api/auth/register` (4-step: info, OTP, PIN, success)
- `send/page.tsx` — POST `/api/transactions/remittance`
- `scan/page.tsx` — POST `/api/transactions/qr-payment`
- `complaints/page.tsx` — POST `/api/complaints`
- `withdrawal/page.tsx` — POST (withdrawal request)
- `cards/page.tsx` — POST/PATCH/DELETE `/api/cards/*` (feature-flagged)

---

### 3.5 Filter-Driven Refetch (Pattern 4)

Transactions and notification pages refetch when filter changes via `useEffect` dependency.

```tsx
const [filter, setFilter] = useState("all");

useEffect(() => {
  fetch(`/api/transactions?type=${filter}&limit=50`, { credentials: "include" })
    .then(res => res.json())
    .then(json => setData(json.transactions))
    .catch(() => {})
    .finally(() => setLoading(false));
}, [filter]);
```

**Pages using this pattern:**
- `transactions/page.tsx` — filters: "all", "remittance", "qr_payment"
- `notifications/page.tsx` — auto-marks read via PATCH on mount

---

## 4. API Routes (BFF Layer)

All API routes are under `src/app/api/`. The frontend calls these endpoints via the Next.js BFF (which translates httpOnly cookie auth to Bearer token for the Hono backend).

| Endpoint | Method | Called From |
|----------|--------|------------|
| `/api/auth/login` | POST | login page |
| `/api/auth/logout` | POST | useAuth hook, profile page |
| `/api/auth/me` | GET | useAuth hook (every protected page mount) |
| `/api/auth/register` | POST | register page |
| `/api/transactions` | GET | dashboard, transactions pages |
| `/api/transactions/remittance` | POST | send page |
| `/api/transactions/qr-payment` | POST | scan page |
| `/api/recipients` | GET | send page |
| `/api/rates` | GET | send page |
| `/api/notifications` | GET | notifications page |
| `/api/notifications` | PATCH | notifications page (mark read) |
| `/api/settings` | GET | profile/notifications, profile/language |
| `/api/settings` | PATCH | profile/notifications, profile/language |
| `/api/complaints` | POST | complaints page |
| `/api/consents` | POST | cookie-consent component |
| `/api/cards` | GET/POST | cards page (feature-flagged) |
| `/api/cards/{id}` | PATCH | cards page (freeze/unfreeze, feature-flagged) |
| `/api/cards/{id}` | DELETE | cards page (delete card, feature-flagged) |
| `/api/merchants/dashboard` | GET | merchant page |
| `/api/merchants/transactions` | GET | merchant page |
| `/api/merchants/qr` | GET | merchant page |

---

## 5. Mobile State Management

**File:** `src/drop-mobile/lib/api.js`

The mobile app uses a different auth mechanism — Bearer token instead of httpOnly cookie.

```javascript
// Token stored at module level (in-memory)
let token = null;

export const setToken = (t) => { token = t; };
export const getToken = () => token;

// All requests include auth header
const request = async (endpoint, options = {}) => {
  const headers = {
    "Content-Type": "application/json",
    ...(token && { Authorization: `Bearer ${token}` }),
    ...options.headers,
  };
  // ...
};
```

**Token persistence:** On login, token stored in `AsyncStorage` for 7-day lifetime. On app launch, token loaded from `AsyncStorage` and set via `setToken()`.

**Mobile state patterns:**
- All screens use `useState` (same as web)
- Auth data read from `api.getMe()` on dashboard mount
- Pull-to-refresh: `RefreshControl` on `FlatList` components
- No global state library — same philosophy as web

---

## 6. Persistent State

| Data | Storage | Notes |
|------|---------|-------|
| Auth session (web) | httpOnly cookie | Server-set, 7-day lifetime, never in JS |
| Auth token (mobile) | Bearer token, `AsyncStorage` | 7-day lifetime, device-bound |
| Cookie preferences | `localStorage` | CookieConsent component |
| Language preference | Server-side (`/api/settings`) | Synced on login |
| Push notification prefs | Server-side (`/api/settings`) | Synced on page load |

**RULE:** Auth tokens NEVER in localStorage. httpOnly cookies on web, `AsyncStorage` (module-level) on mobile.

---

## 7. State Debugging

| Tool | Usage | Enabled In |
|------|-------|-----------|
| React DevTools | Component state tree inspection | Dev only |
| `__DEV__` flag | Enables demo credentials on login | Dev only |
| Network tab | Inspect `/api/auth/me` response | Dev only |
| Console logs | API client logs (mobile) | Dev only |

**No state management devtools** — not needed without a global store.

---

## 8. Performance Considerations

### No Unnecessary Re-renders
- Each `useEffect` has explicit dependency arrays
- `useState` updates are batched by React 19
- No derived state that needs memoization in current implementation

### Fresh Data on Navigation
- `useAuth()` re-fetches `/api/auth/me` on every page mount — ensures fresh user data
- Page data re-fetches on every mount — no stale cache issues
- Trade-off: more network requests, but always fresh data for financial app accuracy

### Future Optimization Path (if needed)
1. Add `SWR` or `TanStack Query` for caching with stale-while-revalidate
2. Persist query cache across navigation (currently re-fetches on each visit)
3. Optimistic updates for settings toggles (partially implemented in notification settings)

---

## Approval
| Role | Name | Date | Signature |
|------|------|------|-----------|
| Author | John (AI Director) | 2026-02-23 | |
| Frontend Lead | | | |
| Tech Lead | | | |

# Landing Pages

# Drop Frontend — Landing Pages

> Covers the static marketing site (`landing/`) and the standalone web prototype (`src/drop-web/`).

---

## Marketing Landing Page

**File:** `landing/index.html` (~646 lines)

### Tech Stack
- Pure HTML/CSS/JS (no framework)
- Fonts: Fraunces + DM Sans (Google Fonts CDN)
- CSS custom properties matching brand system
- Responsive (mobile-first)

### Page Sections

| # | Section | Description |
|---|---------|-------------|
| 1 | **Nav** | DropLogoFull SVG + links (Tjenester, Priser, Om oss) + Logg inn / Kom i gang buttons |
| 2 | **Hero** | Headline "Enklere betalinger for alle i Norge", subtext, 2 CTA buttons, phone mockup placeholder |
| 3 | **Trust Bar** | 3 stats: "0.5% gebyr", "<2 timer leveringstid", "30+ land" |
| 4 | **Features** | 3 cards: Send penger (globe icon), QR-betaling (QR icon), Virtuelt kort (card icon) |
| 5 | **Calculator** | Transfer calculator with amount input, country dropdown, live conversion display |
| 6 | **How It Works** | 4 steps: Last ned → Koble bank → Velg mottaker → Send |
| 7 | **Social Proof / Early Access** | Waitlist signup form with email input |
| 8 | **CTA** | Final call-to-action with app store buttons (placeholder) |
| 9 | **Footer** | Logo, company info (ALAI Holding AS, Org.nr 932 516 136), nav links to sub-pages |

### CSS Animations
```css
@keyframes fadeUp    /* Scroll-triggered fade in from below */
@keyframes float     /* Gentle floating on phone mockup */
@keyframes shimmer   /* Shimmer effect on CTA buttons */
@keyframes pulse     /* Subtle pulse on trust badges */
```

### Colors Used
```css
--drop-green: #0B6E35;
--drop-gold: #D4A017;
--drop-dark: #1A1A1A;
--drop-light: #FAFCF8;
--drop-gray: #6B7280;
--drop-border: #E5E7EB;
```

### Responsive Breakpoints
- Mobile: default (single column)
- Tablet: `@media (min-width: 768px)` — 2-column grids
- Desktop: `@media (min-width: 1024px)` — 3-column grids, wider hero

---

## Sub-Pages

**Directory:** `landing/pages/`

12 static HTML pages linked from the footer:

| File | Route (relative) | Content |
|------|-------------------|---------|
| `cookies.html` | /pages/cookies | Cookie policy |
| `karriere.html` | /pages/karriere | Careers page |
| `kontakt.html` | /pages/kontakt | Contact form / info |
| `lisenser.html` | /pages/lisenser | Licenses and regulatory info |
| `om-drop.html` | /pages/om-drop | About Drop |
| `personvern.html` | /pages/personvern | Privacy policy |
| `presse.html` | /pages/presse | Press / media kit |
| `priser.html` | /pages/priser | Pricing |
| `qr-betaling.html` | /pages/qr-betaling | QR payment feature page |
| `send-penger.html` | /pages/send-penger | Send money feature page |
| `sikkerhet.html` | /pages/sikkerhet | Security overview |
| `vilkar.html` | /pages/vilkar | Terms and conditions |

### Waitlist Script
**File:** `landing/pages/waitlist.js`
- Handles email collection for early access signup
- Connected to the waitlist form in the landing page hero/social proof section

---

## Standalone Web Prototype (`drop-web`)

**File:** `src/drop-web/index.html` (~1305 lines)

### Overview
This is an **older/alternative prototype** with a different design system. It predates the main Next.js app and serves as a standalone demo.

### Key Differences from Main App

| Aspect | Main App (`drop-app`) | Web Prototype (`drop-web`) |
|--------|----------------------|---------------------------|
| Framework | Next.js (React) | Vanilla JS (single file) |
| Theme | Light (#FAFCF8 bg) | Dark (#0D1117 bg) |
| Primary green | `#0B6E35` (Forest Green) | `#00C853` (Bright Green) |
| Fonts | Fraunces + DM Sans | Outfit + Plus Jakarta Sans |
| API port | localhost:3000 | localhost:3001 |
| Routing | File-based (App Router) | JS function `showScreen()` |

### Screens

| Screen | Description |
|--------|-------------|
| Welcome | Dark bg, "drop." wordmark, "Penger uten grenser" tagline, login/register buttons |
| Login | Email + password form, BankID/Vipps buttons, validation |
| App (Dashboard) | 4 tabs: Hjem, Send, QR, Profil |
| Hjem tab | Balance card (dark green gradient), recent transactions |
| Send tab | Recipient selection, amount input, country/currency picker |
| QR tab | Scanner placeholder with merchant list |
| Profil tab | User info, contacts, settings, logout |

### Architecture
- Single HTML file with embedded CSS and JS
- `showScreen(name)` function handles "routing" by showing/hiding divs
- `switchTab(name)` for tab navigation within the app screen
- API calls to `localhost:3001/api` with Bearer token auth
- Functions: `doLogin()`, `doRegister()`, `doSend()`, `doQRPay()`, `renderTransactions()`

### Status
This prototype appears to be an **earlier iteration** or **demo showcase**. The main `drop-app` (Next.js) is the production codebase. The `drop-web` prototype uses different colors, fonts, and a dark theme that does not match the current brand guide.

# Frontend Architecture Document

# Frontend Architecture Document

> **Project:** Drop — Fintech Payment App
> **Version:** 0.1.0
> **Date:** 2026-02-23
> **Author:** John (AI Director, ALAI)
> **Status:** In Review
> **Reviewers:** Alem Bašić (CEO)

## Document History
| Version | Date | Author | Changes |
|---------|------|--------|---------|
| 0.1     | 2026-02-23 | John | Initial draft from source code analysis |

---

## 1. Purpose & Scope

This document defines the frontend architecture for **Drop**, a fintech payment app for all residents of Norway/Scandinavia. It covers the Next.js web application (`src/drop-app/`) and the static marketing site (`landing/`).

**Intended readers:** Frontend engineers, mobile engineers, architects, product managers.

**In scope:**
- Next.js App Router web application (`src/drop-app/`)
- Static HTML marketing landing page (`landing/index.html`)
- Expo React Native mobile app (`src/drop-mobile/`) — details in `mobile-architecture.md`

**Out of scope:** Backend API (Hono v4), infrastructure, CI/CD pipelines.

---

## 2. Framework Choice & Rationale

| Criterion | Weight | Next.js 15 | Nuxt 3 | SvelteKit | Remix |
|-----------|--------|------------|--------|-----------|-------|
| Team expertise | 30% | 9 | 5 | 4 | 6 |
| Ecosystem maturity | 25% | 9 | 7 | 6 | 7 |
| Performance | 20% | 9 | 8 | 9 | 8 |
| DX / tooling | 15% | 9 | 7 | 7 | 7 |
| License / cost | 10% | 10 | 10 | 10 | 10 |

**Selected Framework:** `Next.js 15 (App Router) + React 19`

**Rationale:**
Next.js was selected because the entire team (AI-driven) operates in TypeScript + React. The App Router supports a hybrid rendering model essential for Drop: Server Components for the marketing landing page (fast, SEO-friendly), and Client Components for the authenticated payment flows (real-time, user-specific). Code sharing between the Next.js web app and the Expo mobile app is maximized — shared hooks, types, and validation logic. The Vercel deployment model provides zero-config CDN, edge middleware for auth protection, and ISR for marketing pages without managing infrastructure.

**Node/runtime version:** `Node.js 20 LTS`
**Package manager:** `pnpm`

---

## 3. Project Folder Structure

```
src/drop-app/
├── src/
│   ├── app/                        # Next.js App Router (file-based routing)
│   │   ├── page.tsx                # / — Marketing home (Server Component)
│   │   ├── layout.tsx              # Root layout — fonts, PWA, cookie consent
│   │   ├── login/page.tsx          # /login — Client Component
│   │   ├── register/page.tsx       # /register — Multi-step onboarding
│   │   ├── dashboard/page.tsx      # /dashboard — Auth required
│   │   ├── accounts/page.tsx       # /accounts — Bank account linking
│   │   ├── transactions/page.tsx   # /transactions — Transaction history
│   │   ├── scan/page.tsx           # /scan — QR payment
│   │   ├── send/page.tsx           # /send — Remittance flow
│   │   ├── profile/                # /profile and sub-routes
│   │   ├── notifications/page.tsx  # /notifications
│   │   ├── cards/page.tsx          # /cards — Feature-flagged (future)
│   │   ├── fees/page.tsx           # /fees — Public fee disclosure
│   │   ├── privacy/page.tsx        # /privacy — GDPR policy
│   │   ├── terms/page.tsx          # /terms — Terms of service
│   │   ├── complaints/page.tsx     # /complaints — Finansavtaleloven §3-53
│   │   ├── withdrawal/page.tsx     # /withdrawal — Angrerettloven
│   │   └── api/                    # API routes (BFF layer)
│   │       ├── auth/               # login, logout, me, register
│   │       ├── transactions/       # remittance, qr-payment
│   │       ├── recipients/         # CRUD recipients
│   │       ├── rates/              # Exchange rates
│   │       ├── cards/              # Feature-flagged
│   │       ├── merchants/          # Merchant dashboard
│   │       ├── settings/           # User preferences
│   │       ├── notifications/      # Push notifications
│   │       └── consents/           # Cookie consent
│   ├── components/
│   │   ├── ui/                     # shadcn/ui primitives (Radix-based)
│   │   ├── bottom-nav.tsx          # Fixed bottom navigation (5 tabs)
│   │   ├── drop-logo.tsx           # DropLogo, DropWordmark, DropLogoFull, DropAppIcon
│   │   ├── drop-icons.tsx          # 9 custom domain-specific icons
│   │   ├── cookie-consent.tsx      # GDPR cookie consent banner + modal
│   │   ├── pre-payment-disclosure.tsx  # PSD2 pre-payment disclosure modal
│   │   └── pwa-register.tsx        # Service Worker registration
│   ├── lib/
│   │   ├── use-auth.ts             # useAuth() — auth hook with redirect
│   │   ├── feature-flags.ts        # Feature flag system (env-var based)
│   │   └── middleware/             # auth-middleware, error-handler, validation
│   └── middleware.ts               # Next.js middleware (auth protection)
├── public/
│   ├── sw.js                       # Service worker (PWA)
│   └── manifest.json               # Web app manifest
├── landing/                        # Static marketing site (separate from Next.js)
│   ├── index.html                  # Main landing page (~646 lines, pure HTML/CSS/JS)
│   └── pages/                      # 12 sub-pages (priser, personvern, vilkar, etc.)
├── globals.css                     # CSS custom properties (brand tokens)
├── next.config.ts
└── package.json
```

---

## 4. Rendering Strategy

| Page Type | Strategy | Rationale |
|-----------|----------|-----------|
| `/` — Marketing home | Server Component (SSG) | SEO-critical, no auth, static content |
| `/fees`, `/privacy`, `/terms`, `/complaints`, `/withdrawal` | Client Component (CSR) | Public pages, no auth required |
| `/login`, `/register` | Client Component (CSR) | Form state, client-side validation |
| `/dashboard`, `/accounts`, `/transactions` | Client Component (CSR) | Auth-gated, personalized, real-time data |
| `/scan`, `/send` | Client Component (CSR) | Multi-step flows, camera access, form state |
| `/profile`, `/notifications`, `/cards` | Client Component (CSR) | Auth-gated, user-specific |
| `/api/**` | API Routes | BFF layer — cookie-to-bearer proxy |

**Hydration approach:** Full hydration. Server Components used for the marketing page (`page.tsx`). All authenticated app pages are Client Components using `"use client"` directive.

**Note:** The marketing landing page (`landing/index.html`) is a completely separate pure HTML/CSS/JS file served statically. It does not use the Next.js framework.

---

## 5. Routing Architecture

### 5.1 Route Organization

```
/                           → Marketing home (Server Component)
/login                      → Login (Client)
/register                   → Registration / Onboarding (Client, 4-step)
/dashboard                  → Dashboard (Client, auth required)
/accounts                   → Bank accounts via AISP (Client, auth required)
/transactions               → Transaction history (Client, auth required)
/scan                       → QR scanner (Client, auth required)
/send                       → Remittance flow (Client, auth required)
/profile                    → Profile (Client, auth required)
/profile/personal           → Personal info — BankID-verified, read-only
/profile/security           → Security settings — 2FA, active devices
/profile/notifications      → Notification preferences
/profile/language           → Language selection (nb, en, bs, sq)
/notifications              → Notifications center (Client, auth required)
/cards                      → Card management (Client, auth required, feature-flagged)
/fees                       → Fee disclosure (Public)
/privacy                    → Privacy policy / GDPR (Public)
/terms                      → Terms of service (Public)
/complaints                 → Complaint form — Finansavtaleloven §3-53 (Public)
/withdrawal                 → Right of withdrawal — Angrerettloven (Public)
/api/**                     → API routes (BFF — cookie auth proxy to Hono backend)
```

### 5.2 Route Guards / Middleware

Auth is enforced at two levels:

**Level 1 — `useAuth()` hook (per-page):**
```ts
// src/lib/use-auth.ts
// Default: redirectIfUnauthenticated = true
const { user, loading } = useAuth();
// On 401: redirects to /login
```

**Level 2 — Middleware (future):**
```
Middleware execution order:
1. Request logging
2. Authentication check (JWT cookie validation)
3. Redirect unauthenticated → /login
4. Feature flag gate (returns 404 if flag disabled)
```

**Protected routes:** All routes under `/dashboard`, `/accounts`, `/transactions`, `/scan`, `/send`, `/profile/**`, `/notifications`, `/cards`.

**Public routes:** `/`, `/login`, `/register`, `/fees`, `/privacy`, `/terms`, `/complaints`, `/withdrawal`.

---

## 6. Build & Bundle Configuration

### 6.1 Key Config Options

| Option | Value | Reason |
|--------|-------|--------|
| Output | `standalone` | Docker-optimized deployment |
| Image optimization | `next/image` | WebP conversion, lazy loading |
| Bundle analyzer | `ANALYZE=true` | On-demand local analysis |
| Source maps | Production: hidden | Upload to Sentry only |
| Compression | gzip + brotli | CDN-level compression |
| PWA | Service Worker at `/sw.js` | Offline capability (basic) |

### 6.2 Code Splitting Strategy

- **Route-level splitting:** Automatic per App Router segment
- **Component-level splitting:** `dynamic()` for heavy components (Dialog, QR scanner UI)
- **Feature flags:** Cards page components lazy-loaded (all flags default to `false`)

---

## 7. Performance Budget

| Metric | Target | Tool |
|--------|--------|------|
| LCP (Largest Contentful Paint) | < 2.5s | Lighthouse, CrUX |
| INP (Interaction to Next Paint) | < 200ms | CrUX |
| CLS (Cumulative Layout Shift) | < 0.1 | Lighthouse |
| TTFB (Time to First Byte) | < 600ms | WebPageTest |
| Total JS bundle (compressed) | < 200 KB | Bundlesize CI check |
| First page load (mobile 4G) | < 3s | WebPageTest |
| Lighthouse Performance Score | ≥ 90 | Lighthouse CI |

**Note:** The marketing page (`/`) is the primary SEO and performance target. The app pages (dashboard, send) accept slightly higher bundle sizes due to shadcn/ui + Radix dependencies.

---

## 8. Asset Management

### 8.1 Images
- Format: WebP primary, JPEG fallback, SVG for vectors (logos, icons)
- Optimization: `next/image` with defined domains
- Lazy loading: Native `loading="lazy"` via `next/image`

### 8.2 Fonts
- **Fraunces** (variable, 400-900) — loaded via `next/font/google` in `layout.tsx`
- **DM Sans** (variable, 400-700) — loaded via `next/font/google` in `layout.tsx`
- **Geist Mono** — loaded via `next/font/google` in `layout.tsx`
- CSS variables: `--font-fraunces`, `--font-dm-sans`, `--font-geist-mono`
- `font-display: swap` on all faces (handled by `next/font`)

### 8.3 Icons
- **Primary library:** `lucide-react` (inline SVG via React components)
- **Custom icons:** `components/drop-icons.tsx` — 9 domain-specific icons (IconSendMoney, IconQrScan, IconVirtualCard, IconShield, IconFastTransfer, IconCorridors, IconWallet, IconHistory, IconTopUp)
- **Social auth icons:** Inline SVG (BankID green, Vipps orange)
- Delivery: Inline SVG via component (no icon font, no sprite)

---

## 9. Internationalization (i18n) Strategy

| Item | Decision |
|------|----------|
| Library | None (Phase 1 — Norwegian only) |
| Default locale | `nb` (Norwegian Bokmål) |
| Supported locales | `nb` (launch), `en`, `bs`, `sq` (Phase 2) |
| Language selection | `/profile/language` — PATCH `/api/settings` with `{ language: string }` |
| Translation format | TBD — requires i18n library selection |
| RTL support | No |

**Phase 1:** All UI text hardcoded in Norwegian Bokmål. Language setting stored in user preferences (`/api/settings`) but not yet applied to UI translation.

**Phase 2:** Introduce `next-intl` or `i18next` with per-locale JSON translation files for `nb`, `en`, `bs`, `sq`.

---

## 10. Error Boundary Strategy

| Level | Scope | Behavior |
|-------|-------|----------|
| Global `error.tsx` | Full page crash | Show branded error page |
| Auth failure | API 401 | `useAuth()` redirects to `/login` |
| Form submission | Mutation failure | Inline error message (`text-[#EF4444]`) + retry |
| Data fetch | `useEffect` fetch failure | Caught in `.catch()`, silent or empty state |
| Feature flag | Flag disabled | Returns 404 or "Feature not available" message |

**Error reporting:** TBD — Sentry integration planned for production.

---

## 11. Environment Configuration

| Variable | Dev | Staging | Prod | Description |
|----------|-----|---------|------|-------------|
| `NEXT_PUBLIC_API_URL` | `http://localhost:3000/api` | `https://drop-app-staging.vercel.app/api` | `https://drop-app.vercel.app/api` | Backend API base URL (mobile uses this) |
| `NEXT_PUBLIC_APP_ENV` | `development` | `staging` | `production` | Runtime environment flag |
| `NEXT_PUBLIC_FF_VIRTUAL_CARDS` | `false` | `false` | `false` | Cards feature flag |
| `NEXT_PUBLIC_FF_PHYSICAL_CARDS` | `false` | `false` | `false` | Physical cards flag |
| `NEXT_PUBLIC_FF_NOTIFICATIONS` | `true` | `true` | `true` | Notifications feature flag |
| `NEXT_PUBLIC_FF_MERCHANT_DASHBOARD` | `true` | `true` | `true` | Merchant dashboard flag |
| `SENTRY_DSN` | optional | required | required | Error reporting (server-side) |

**Feature flag convention:** `NEXT_PUBLIC_FF_` + `SCREAMING_SNAKE_CASE` flag name.

**Secrets management:** All non-public secrets in Vaultwarden (`vault.basicconsulting.no`). Never committed.

---

## 12. Dependency Management Policy

| Rule | Detail |
|------|--------|
| Allowed licenses | MIT, Apache-2.0, ISC, BSD-2, BSD-3 |
| Security audit | `pnpm audit` in CI — fail on high/critical |
| Update cadence | Monthly Renovate PRs for minor/patch; major = manual |
| Banned patterns | No `moment.js` (use `date-fns`), no `lodash` (use native) |

**Key dependencies:**
- `next@15`, `react@19` — framework
- `shadcn/ui` + `@radix-ui/*` — component primitives
- `lucide-react` — icon library
- `tailwindcss` — styling
- `class-variance-authority`, `clsx`, `tailwind-merge` — class utilities

---

## 13. Architecture Diagram

```mermaid
graph TB
    subgraph "Client Browser / Mobile"
        Browser["User Browser (PWA)"]
        Mobile["Expo Mobile App"]
    end

    subgraph "Frontend — Next.js 15 App Router"
        CDN["Vercel CDN (Static Assets + Edge)"]
        Marketing["Marketing Page (Server Component, SSG)"]
        AppPages["App Pages (Client Components, CSR)"]
        BFF["BFF API Routes (/api/*)\nCookie → Bearer proxy"]
        Middleware["Next.js Middleware\n(Auth guard)"]
        Components["shadcn/ui + Custom Components\nBottomNav, DropLogo, drop-icons"]
        AuthHook["useAuth() Hook\nGET /api/auth/me"]
        FeatureFlags["Feature Flags\nenv-var based"]
    end

    subgraph "Backend — Hono v4"
        HonoAPI["Hono REST API\n/v1/* — Bearer auth"]
        OpenBanking["Open Banking (PSD2)\nAISP + PISP"]
        BankID["BankID OIDC\nAuthentication"]
    end

    Browser --> CDN
    Browser --> Marketing
    Browser --> Middleware
    Middleware --> AppPages
    AppPages --> Components
    AppPages --> AuthHook
    AppPages --> FeatureFlags
    AuthHook --> BFF
    BFF --> HonoAPI
    HonoAPI --> OpenBanking
    HonoAPI --> BankID
    Mobile --> HonoAPI
```

---

## Approval
| Role | Name | Date | Signature |
|------|------|------|-----------|
| Author | John (AI Director) | 2026-02-23 | |
| Tech Lead | | | |
| Architect | | | |
| Engineering Manager | | | |

# Accessibility Audit

# Accessibility Audit

> **Project:** {{PROJECT_NAME}}
> **Version:** {{VERSION}}
> **Date:** {{DATE}}
> **Author:** {{AUTHOR}}
> **Status:** Draft | In Review | Approved
> **Reviewers:** {{REVIEWERS}}

## Document History
| Version | Date | Author | Changes |
|---------|------|--------|---------|
| 0.1     | {{DATE}} | {{AUTHOR}} | Initial draft |

---

## 1. Compliance Target

<!-- GUIDANCE: State the WCAG target level. Most products target AA. AAA is aspirational. -->

| Standard | Level | Notes |
|----------|-------|-------|
| WCAG 2.1 | **AA** | Minimum required |
| WCAG 2.1 | AAA | Target for critical flows (checkout, forms) |
| EN 301 549 | Applicable | If serving EU public sector |
| Section 508 | `{{Yes/No}}` | If US federal contract |

**Audit date:** `{{DATE}}`
**Auditor:** `{{NAME}}`
**Next scheduled audit:** `{{DATE}}`

---

## 2. Audit Methodology

<!-- GUIDANCE: Define how the audit was conducted — automated tools, manual testing, user testing. -->

### 2.1 Automated Testing
- **axe-core:** Integrated in Storybook + CI (via `@axe-core/playwright`)
- **Lighthouse:** Accessibility score tracked in CI (target: ≥ 95)
- **pa11y:** Run on full page URLs in staging environment

### 2.2 Manual Testing
- Keyboard-only navigation on all critical user flows
- Screen reader testing (see matrix in Section 6)
- Zoom test: 200% browser zoom — no loss of content or functionality
- Reflow test: 320px viewport width

### 2.3 User Testing
- `{{Yes — included users with disabilities in user testing | No — planned for future}}`: `{{Notes}}`

---

## 3. Perceivable

<!-- GUIDANCE: Check each criterion. Status: Pass / Fail / N/A / Partial. Add notes for failures and link to remediation tasks. -->

### 1.1 Text Alternatives

| Criterion | Status | Notes | Issue Ref |
|-----------|--------|-------|-----------|
| 1.1.1 Non-text Content (A) | `{{Pass}}` | All `<img>` have `alt`; decorative images use `alt=""` | |

**Checklist:**
- [ ] All `<img>` elements have descriptive `alt` text or `alt=""`
- [ ] Icon buttons have `aria-label`
- [ ] Complex images (charts, diagrams) have long descriptions
- [ ] CAPTCHAs have audio alternative
- [ ] SVG icons: meaningful → `role="img" aria-label="..."`, decorative → `aria-hidden="true"`

**TODO:** Run `axe` and list all violations.

---

### 1.2 Time-based Media

| Criterion | Status | Notes | Issue Ref |
|-----------|--------|-------|-----------|
| 1.2.1 Audio-only / Video-only (A) | `{{Pass/Fail/N/A}}` | | |
| 1.2.2 Captions (A) | `{{Pass/Fail/N/A}}` | | |
| 1.2.3 Audio Description (A) | `{{Pass/Fail/N/A}}` | | |
| 1.2.4 Captions Live (AA) | `{{Pass/Fail/N/A}}` | | |
| 1.2.5 Audio Description Prerecorded (AA) | `{{Pass/Fail/N/A}}` | | |

**TODO:** Audit all video/audio content on the platform.

---

### 1.3 Adaptable

| Criterion | Status | Notes | Issue Ref |
|-----------|--------|-------|-----------|
| 1.3.1 Info and Relationships (A) | `{{Pass/Fail}}` | Semantic HTML for tables, lists, headings | |
| 1.3.2 Meaningful Sequence (A) | `{{Pass/Fail}}` | DOM order matches visual order | |
| 1.3.3 Sensory Characteristics (A) | `{{Pass/Fail}}` | No "click the red button" instructions | |
| 1.3.4 Orientation (AA) | `{{Pass/Fail}}` | No locked orientation | |
| 1.3.5 Identify Input Purpose (AA) | `{{Pass/Fail}}` | `autocomplete` on all personal data fields | |

**Checklist:**
- [ ] Tables use `<th>` with `scope` attributes
- [ ] Heading hierarchy is logical (H1 → H2 → H3, no skipped levels)
- [ ] Lists use `<ul>` / `<ol>` — not styled `<div>`
- [ ] Form inputs have `autocomplete` attributes for name, email, phone, address

---

### 1.4 Distinguishable

| Criterion | Status | Notes | Issue Ref |
|-----------|--------|-------|-----------|
| 1.4.1 Use of Color (A) | `{{Pass/Fail}}` | Color not sole differentiator | |
| 1.4.2 Audio Control (A) | `{{Pass/Fail}}` | Auto-play audio has stop mechanism | |
| 1.4.3 Contrast Minimum (AA) | `{{Pass/Fail}}` | See contrast table below | |
| 1.4.4 Resize Text (AA) | `{{Pass/Fail}}` | 200% zoom — no content loss | |
| 1.4.5 Images of Text (AA) | `{{Pass/Fail}}` | Real text used, not images | |
| 1.4.10 Reflow (AA) | `{{Pass/Fail}}` | 320px width — no horizontal scroll | |
| 1.4.11 Non-text Contrast (AA) | `{{Pass/Fail}}` | UI components 3:1 ratio | |
| 1.4.12 Text Spacing (AA) | `{{Pass/Fail}}` | Custom spacing doesn't break layout | |
| 1.4.13 Content on Hover/Focus (AA) | `{{Pass/Fail}}` | Tooltips can be hovered/dismissed | |

---

## 4. Operable

### 2.1 Keyboard Accessible

| Criterion | Status | Notes | Issue Ref |
|-----------|--------|-------|-----------|
| 2.1.1 Keyboard (A) | `{{Pass/Fail}}` | All functionality keyboard-operable | |
| 2.1.2 No Keyboard Trap (A) | `{{Pass/Fail}}` | Focus can always be moved away | |
| 2.1.4 Character Key Shortcuts (A) | `{{Pass/Fail}}` | Single-key shortcuts can be disabled | |

**Checklist:**
- [ ] All interactive elements reachable by Tab key
- [ ] All actions triggerable by Enter/Space
- [ ] No keyboard traps (except intentional: modal focus trap with Escape exit)
- [ ] Custom widgets follow ARIA Authoring Practices patterns

---

### 2.2 Enough Time

| Criterion | Status | Notes |
|-----------|--------|-------|
| 2.2.1 Timing Adjustable (A) | `{{Pass/Fail}}` | Session timeout warns with option to extend |
| 2.2.2 Pause, Stop, Hide (A) | `{{Pass/Fail}}` | Auto-updating content has pause control |

---

### 2.3 Seizures and Physical Reactions

| Criterion | Status | Notes |
|-----------|--------|-------|
| 2.3.1 Three Flashes (A) | `{{Pass/Fail}}` | No content flashes >3 times/sec |

---

### 2.4 Navigable

| Criterion | Status | Notes |
|-----------|--------|-------|
| 2.4.1 Bypass Blocks (A) | `{{Pass/Fail}}` | Skip-to-content link implemented |
| 2.4.2 Page Titled (A) | `{{Pass/Fail}}` | Unique descriptive page titles |
| 2.4.3 Focus Order (A) | `{{Pass/Fail}}` | Focus order matches reading order |
| 2.4.4 Link Purpose (A) | `{{Pass/Fail}}` | Link text descriptive in context |
| 2.4.6 Headings and Labels (AA) | `{{Pass/Fail}}` | Descriptive heading and label text |
| 2.4.7 Focus Visible (AA) | `{{Pass/Fail}}` | Focus indicator visible on all elements |

---

## 5. Understandable

### 3.1 Readable

| Criterion | Status | Notes |
|-----------|--------|-------|
| 3.1.1 Language of Page (A) | `{{Pass/Fail}}` | `<html lang="en">` set |
| 3.1.2 Language of Parts (AA) | `{{Pass/Fail}}` | `lang` on foreign language sections |

---

### 3.2 Predictable

| Criterion | Status | Notes |
|-----------|--------|-------|
| 3.2.1 On Focus (A) | `{{Pass/Fail}}` | Focus does not trigger unexpected change |
| 3.2.2 On Input (A) | `{{Pass/Fail}}` | Input change doesn't auto-submit form |
| 3.2.3 Consistent Navigation (AA) | `{{Pass/Fail}}` | Nav consistent across pages |
| 3.2.4 Consistent Identification (AA) | `{{Pass/Fail}}` | Same function = same label |

---

### 3.3 Input Assistance

| Criterion | Status | Notes |
|-----------|--------|-------|
| 3.3.1 Error Identification (A) | `{{Pass/Fail}}` | Errors identified in text, not just color |
| 3.3.2 Labels or Instructions (A) | `{{Pass/Fail}}` | All fields have labels |
| 3.3.3 Error Suggestion (AA) | `{{Pass/Fail}}` | Error messages suggest correction |
| 3.3.4 Error Prevention (AA) | `{{Pass/Fail}}` | Destructive actions require confirmation |

---

## 6. Robust

| Criterion | Status | Notes |
|-----------|--------|-------|
| 4.1.1 Parsing (A) | `{{Pass/Fail}}` | Valid HTML, no duplicate IDs |
| 4.1.2 Name, Role, Value (A) | `{{Pass/Fail}}` | All UI components have accessible name + role |
| 4.1.3 Status Messages (AA) | `{{Pass/Fail}}` | Status messages announced via ARIA live regions |

---

## 7. Screen Reader Testing Matrix

<!-- GUIDANCE: Test on combinations of OS + screen reader + browser. Priority combos are marked. -->

| Screen Reader | OS | Browser | Priority | Status | Tester | Date |
|---------------|----|---------|----------|--------|--------|------|
| VoiceOver | macOS 14 | Safari | HIGH | `{{Pass/Fail/Untested}}` | | |
| VoiceOver | iOS 17 | Safari | HIGH | `{{Pass/Fail/Untested}}` | | |
| NVDA | Windows 11 | Chrome | HIGH | `{{Pass/Fail/Untested}}` | | |
| NVDA | Windows 11 | Firefox | MEDIUM | `{{Pass/Fail/Untested}}` | | |
| JAWS | Windows 11 | Chrome | HIGH | `{{Pass/Fail/Untested}}` | | |
| TalkBack | Android 14 | Chrome | MEDIUM | `{{Pass/Fail/Untested}}` | | |

**Tested user flows:**
1. [ ] User registration / login
2. [ ] Main navigation
3. [ ] Form submission with validation errors
4. [ ] Data table — sorting, pagination, row actions
5. [ ] Modal open/close/action
6. [ ] Toast / notification announced

---

## 8. Keyboard Navigation Map

<!-- GUIDANCE: Document the keyboard interaction model for the full application. -->

| Context | Key | Action |
|---------|-----|--------|
| Global | `Tab` | Next focusable element |
| Global | `Shift+Tab` | Previous focusable element |
| Global | `Escape` | Close modal, dropdown, toast |
| Global | `Alt+S` | Skip to main content (custom shortcut) |
| Dropdown | `Arrow Down/Up` | Navigate options |
| Dropdown | `Enter/Space` | Select option |
| Table | `Arrow keys` | Navigate cells (if grid role) |
| Modal | `Tab` | Cycle within modal (focus trapped) |
| Date picker | `Arrow keys` | Navigate days |
| Tabs | `Arrow keys` | Switch tab |

---

## 9. Color Contrast Verification Table

<!-- GUIDANCE: List every text/background combination and measured contrast ratio. -->

| Element | Text Color | Background | Ratio | AA Pass | AAA Pass |
|---------|-----------|------------|-------|---------|----------|
| Body text | `#0F172A` | `#FFFFFF` | `{{X:1}}` | `{{Y/N}}` | `{{Y/N}}` |
| Secondary text | `#334155` | `#FFFFFF` | `{{X:1}}` | `{{Y/N}}` | `{{Y/N}}` |
| Disabled text | `#94A3B8` | `#FFFFFF` | `{{X:1}}` | `{{Y/N}}` | `{{Y/N}}` |
| Primary button text | `#FFFFFF` | `#0EA5E9` | `{{X:1}}` | `{{Y/N}}` | `{{Y/N}}` |
| Link color | `#0284C7` | `#FFFFFF` | `{{X:1}}` | `{{Y/N}}` | `{{Y/N}}` |
| Error text | `#EF4444` | `#FFFFFF` | `{{X:1}}` | `{{Y/N}}` | `{{Y/N}}` |
| Placeholder | `#94A3B8` | `#FFFFFF` | `{{X:1}}` | `{{Y/N}}` | `{{Y/N}}` |

**TODO:** Measure all values using `{{Colour Contrast Analyser | axe DevTools}}`.

---

## 10. ARIA Usage Audit

<!-- GUIDANCE: List non-trivial ARIA usage and verify it follows ARIA Authoring Practices Guide. -->

| Pattern | ARIA Used | Correct? | Notes |
|---------|-----------|----------|-------|
| Modal dialog | `role="dialog"`, `aria-modal="true"`, `aria-labelledby` | `{{Yes/No}}` | |
| Navigation | `role="navigation"`, `aria-label` | `{{Yes/No}}` | |
| Alert/toast | `role="alert"` or `aria-live="polite"` | `{{Yes/No}}` | |
| Tab panel | `role="tablist"`, `role="tab"`, `role="tabpanel"` | `{{Yes/No}}` | |
| Accordion | `aria-expanded`, `aria-controls` | `{{Yes/No}}` | |
| Custom select | `role="listbox"`, `role="option"`, `aria-selected` | `{{Yes/No}}` | |
| Loading states | `aria-busy="true"`, live region | `{{Yes/No}}` | |

---

## 11. Remediation Priority & Timeline

<!-- GUIDANCE: List all failures with priority and assigned owner. -->

| Issue | WCAG Criterion | Severity | Priority | Assigned To | Target Date | Status |
|-------|----------------|----------|----------|-------------|-------------|--------|
| `{{Issue description}}` | `{{1.4.3}}` | `{{Critical/High/Medium/Low}}` | P1 | `{{NAME}}` | `{{DATE}}` | `{{Open/In Progress/Fixed}}` |

**Severity definitions:**
- **Critical:** Blocks users with disabilities from core functionality
- **High:** Significant barrier, workaround exists
- **Medium:** Inconvenience, does not block task completion
- **Low:** Minor enhancement opportunity

---

## 12. Testing Tools

| Tool | Type | Usage | Version |
|------|------|-------|---------|
| axe-core | Automated | CI integration + Storybook | `{{4.x}}` |
| `@axe-core/playwright` | Automated | E2E accessibility checks | `{{4.x}}` |
| Lighthouse | Automated | CI performance + a11y score | Built-in Chrome |
| pa11y | Automated | Staged URL scanning | `{{6.x}}` |
| Colour Contrast Analyser | Manual | Color contrast spot checks | TPGi |
| Screen readers | Manual | See matrix above | — |
| Accessibility Insights | Manual | Guided manual testing | Microsoft |

---

## Approval
| Role | Name | Date | Signature |
|------|------|------|-----------|
| Author | | | |
| Accessibility Lead | | | |
| QA Lead | | | |
| Product Owner | | | |

# Design System

# Design System Documentation

> **Project:** {{PROJECT_NAME}}
> **Version:** {{VERSION}}
> **Date:** {{DATE}}
> **Author:** {{AUTHOR}}
> **Status:** Draft | In Review | Approved
> **Reviewers:** {{REVIEWERS}}

## Document History
| Version | Date | Author | Changes |
|---------|------|--------|---------|
| 0.1     | {{DATE}} | {{AUTHOR}} | Initial draft |

---

## 1. Design Principles

<!-- GUIDANCE: Define 3-6 core principles that guide every design decision. These should be opinionated and actionable, not generic platitudes. -->

| Principle | Description |
|-----------|-------------|
| **Clarity first** | Every UI element must communicate its purpose without explanation |
| **Consistent over clever** | Prefer familiar patterns over novel interactions |
| **Accessible by default** | WCAG AA compliance is a baseline, not a feature |
| **Density-aware** | Support comfortable and compact density modes |
| **TODO: Add principle** | {{DESCRIPTION}} |

---

## 2. Color System

<!-- GUIDANCE: Define every color token. Use semantic naming (not color names like "blue-500") for usage tokens. -->

### 2.1 Primitive Palette (Raw Values)

```css
/* Brand primitives — do NOT use directly in components */
--color-brand-50:  {{#F0F9FF}};
--color-brand-100: {{#E0F2FE}};
--color-brand-500: {{#0EA5E9}};
--color-brand-600: {{#0284C7}};
--color-brand-900: {{#0C4A6E}};

--color-neutral-0:   #FFFFFF;
--color-neutral-50:  {{#F8FAFC}};
--color-neutral-100: {{#F1F5F9}};
--color-neutral-400: {{#94A3B8}};
--color-neutral-700: {{#334155}};
--color-neutral-900: {{#0F172A}};
--color-neutral-1000: #000000;
```

### 2.2 Semantic Tokens (Light Mode)

```css
/* Background */
--color-bg-primary:   var(--color-neutral-0);
--color-bg-secondary: var(--color-neutral-50);
--color-bg-elevated:  var(--color-neutral-0);

/* Text */
--color-text-primary:   var(--color-neutral-900);
--color-text-secondary: var(--color-neutral-700);
--color-text-disabled:  var(--color-neutral-400);
--color-text-inverse:   var(--color-neutral-0);

/* Interactive */
--color-interactive-primary:        var(--color-brand-500);
--color-interactive-primary-hover:  var(--color-brand-600);
--color-interactive-primary-active: var(--color-brand-700);

/* Semantic */
--color-success:  {{#10B981}};
--color-warning:  {{#F59E0B}};
--color-error:    {{#EF4444}};
--color-info:     {{#3B82F6}};
```

### 2.3 Semantic Tokens (Dark Mode)

```css
[data-theme="dark"] {
  --color-bg-primary:   var(--color-neutral-900);
  --color-bg-secondary: var(--color-neutral-800);
  --color-text-primary: var(--color-neutral-50);
  /* TODO: Complete dark mode token mapping */
}
```

### 2.4 Contrast Ratios (WCAG)

| Pair | Ratio | WCAG AA (4.5:1) | WCAG AAA (7:1) |
|------|-------|-----------------|----------------|
| Text primary on bg primary | {{X:1}} | {{Pass/Fail}} | {{Pass/Fail}} |
| Text secondary on bg primary | {{X:1}} | {{Pass/Fail}} | {{Pass/Fail}} |
| Interactive on bg primary | {{X:1}} | {{Pass/Fail}} | {{Pass/Fail}} |
| White on brand-500 | {{X:1}} | {{Pass/Fail}} | {{Pass/Fail}} |

**TODO:** Run contrast checks with `{{axe | Colour Contrast Analyser}}` and fill table.

---

## 3. Typography

<!-- GUIDANCE: Define all type properties as tokens. Every text style should be composable from these base tokens. -->

### 3.1 Font Families

| Token | Value | Usage |
|-------|-------|-------|
| `--font-heading` | `{{Inter, sans-serif}}` | H1–H4, display text |
| `--font-body` | `{{Inter, sans-serif}}` | Body copy, labels, UI |
| `--font-mono` | `{{JetBrains Mono, monospace}}` | Code, technical data |

### 3.2 Type Scale

| Token | Size | Weight | Line Height | Letter Spacing | Usage |
|-------|------|--------|-------------|----------------|-------|
| `--text-display` | 48px | 700 | 1.1 | -0.02em | Hero headings |
| `--text-h1` | 36px | 700 | 1.2 | -0.01em | Page titles |
| `--text-h2` | 28px | 600 | 1.25 | -0.01em | Section headings |
| `--text-h3` | 22px | 600 | 1.3 | 0 | Subsections |
| `--text-h4` | 18px | 600 | 1.4 | 0 | Card headings |
| `--text-body-lg` | 18px | 400 | 1.6 | 0 | Lead paragraphs |
| `--text-body` | 16px | 400 | 1.6 | 0 | Default body copy |
| `--text-body-sm` | 14px | 400 | 1.5 | 0 | Secondary text, captions |
| `--text-label` | 14px | 500 | 1.4 | 0.01em | Form labels, UI labels |
| `--text-caption` | 12px | 400 | 1.4 | 0.02em | Timestamps, meta |
| `--text-code` | 14px | 400 | 1.6 | 0 | Inline code |

**TODO:** Verify scale against Figma — update values if mismatched.

---

## 4. Spacing & Layout

<!-- GUIDANCE: All spacing must derive from the base unit. No magic numbers in components. -->

### 4.1 Spacing Scale (4px Base Unit)

| Token | Value | Usage |
|-------|-------|-------|
| `--space-0` | 0px | |
| `--space-1` | 4px | Micro gaps, icon padding |
| `--space-2` | 8px | Tight inline spacing |
| `--space-3` | 12px | Compact form elements |
| `--space-4` | 16px | Default content spacing |
| `--space-5` | 20px | Card padding (compact) |
| `--space-6` | 24px | Card padding, section padding |
| `--space-8` | 32px | Large section gaps |
| `--space-10` | 40px | Feature section padding |
| `--space-12` | 48px | Section separation |
| `--space-16` | 64px | Page-level padding |
| `--space-20` | 80px | Hero sections |

### 4.2 Grid System

| Property | Value |
|----------|-------|
| Column count | 12 |
| Column gutter | 24px (mobile: 16px) |
| Container max-width | 1280px |
| Container side padding | 24px (mobile: 16px) |

### 4.3 Responsive Breakpoints

| Name | Min Width | Target Devices |
|------|-----------|----------------|
| `xs` | 0px | Small phones |
| `sm` | 480px | Large phones |
| `md` | 768px | Tablets |
| `lg` | 1024px | Small laptops |
| `xl` | 1280px | Desktops |
| `2xl` | 1536px | Large displays |

---

## 5. Component Library

<!-- GUIDANCE: Categorize all components. Each primitive/composite must have its own entry — reference the component-inventory.md for per-component detail. -->

### 5.1 Primitive Components (Atoms)

| Component | Status | Variants | Storybook |
|-----------|--------|----------|-----------|
| Button | `{{Done | WIP | Planned}}` | primary, secondary, ghost, danger, link | {{URL}} |
| Input | `{{Done | WIP | Planned}}` | default, error, disabled, with-icon | {{URL}} |
| Select | `{{Done | WIP | Planned}}` | single, multi, searchable | {{URL}} |
| Checkbox | `{{Done | WIP | Planned}}` | default, indeterminate, disabled | {{URL}} |
| Radio | `{{Done | WIP | Planned}}` | default, disabled | {{URL}} |
| Toggle/Switch | `{{Done | WIP | Planned}}` | default, disabled | {{URL}} |
| Textarea | `{{Done | WIP | Planned}}` | default, resizable, fixed | {{URL}} |
| Badge | `{{Done | WIP | Planned}}` | success, warning, error, info, neutral | {{URL}} |
| Avatar | `{{Done | WIP | Planned}}` | image, initials, icon | {{URL}} |
| Tooltip | `{{Done | WIP | Planned}}` | top, right, bottom, left | {{URL}} |
| Spinner | `{{Done | WIP | Planned}}` | sm, md, lg | {{URL}} |
| Divider | `{{Done | WIP | Planned}}` | horizontal, vertical | {{URL}} |

### 5.2 Composite Components (Molecules)

| Component | Status | Storybook |
|-----------|--------|-----------|
| Card | `{{Status}}` | {{URL}} |
| Modal / Dialog | `{{Status}}` | {{URL}} |
| Dropdown Menu | `{{Status}}` | {{URL}} |
| Table | `{{Status}}` | {{URL}} |
| Pagination | `{{Status}}` | {{URL}} |
| Toast / Notification | `{{Status}}` | {{URL}} |
| Form Field (label + input + error) | `{{Status}}` | {{URL}} |
| Search Bar | `{{Status}}` | {{URL}} |
| Breadcrumb | `{{Status}}` | {{URL}} |
| Tabs | `{{Status}}` | {{URL}} |
| Accordion | `{{Status}}` | {{URL}} |
| Date Picker | `{{Status}}` | {{URL}} |

### 5.3 Layout Components

| Component | Description |
|-----------|-------------|
| `Container` | Max-width wrapper with responsive padding |
| `Stack` | Vertical flex stack with configurable gap |
| `Inline` | Horizontal flex row with configurable gap/alignment |
| `Grid` | CSS grid layout wrapper |
| `PageLayout` | Full-page layout with sidebar/header/main/footer slots |
| `Section` | Content section with standard vertical rhythm |

---

## 6. Iconography Guidelines

<!-- GUIDANCE: Define the icon library, sizing standards, and usage rules. -->

| Item | Standard |
|------|----------|
| Library | `{{Lucide React | Heroicons | Phosphor}}` |
| Delivery | Inline SVG via component (no sprite, no icon font) |
| Sizes | 16px (sm), 20px (md, default), 24px (lg), 32px (xl) |
| Stroke width | 1.5px at 24px, scaled proportionally |
| Color | Inherits `currentColor` — never hardcoded |
| Interactive icons | Must have visible focus ring + 44×44px touch target |
| Custom icons | SVG optimized via SVGO, placed in `src/components/icons/` |

**Accessibility rule:** Icons conveying meaning must have `aria-label`. Decorative icons: `aria-hidden="true"`.

---

## 7. Motion & Animation Standards

<!-- GUIDANCE: Define animation tokens and rules for when/how motion should be used. -->

### 7.1 Duration Tokens

| Token | Value | Usage |
|-------|-------|-------|
| `--duration-instant` | 50ms | Micro-feedback (checkbox check) |
| `--duration-fast` | 100ms | Button states, hover |
| `--duration-normal` | 200ms | Modals, dropdowns |
| `--duration-slow` | 300ms | Page transitions, large panels |
| `--duration-slower` | 500ms | Onboarding, loading states |

### 7.2 Easing Tokens

| Token | Value | Usage |
|-------|-------|-------|
| `--ease-default` | `cubic-bezier(0.4, 0, 0.2, 1)` | General UI |
| `--ease-enter` | `cubic-bezier(0, 0, 0.2, 1)` | Elements entering |
| `--ease-exit` | `cubic-bezier(0.4, 0, 1, 1)` | Elements leaving |
| `--ease-spring` | `cubic-bezier(0.34, 1.56, 0.64, 1)` | Playful, emphasis |

### 7.3 Reduced Motion

```css
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}
```

**Rule:** Every animated component MUST respect `prefers-reduced-motion`.

---

## 8. Accessibility Requirements Per Component

<!-- GUIDANCE: This is a summary — full audit detail is in accessibility-audit.md. Each component family must meet these baseline requirements. -->

| Requirement | Buttons | Inputs | Modals | Tables | Navigation |
|-------------|---------|--------|--------|--------|------------|
| Keyboard accessible | ✓ | ✓ | ✓ | ✓ | ✓ |
| Focus visible | ✓ | ✓ | Focus trap | ✓ | ✓ |
| ARIA role | `button` | `textbox`/`combobox` | `dialog` | `table`/`grid` | `nav` |
| Screen reader label | aria-label or visible text | `<label>` associated | `aria-labelledby` | Caption + headers | `aria-label` |
| Error state | `aria-describedby` error msg | `aria-invalid` | — | — | — |
| Touch target | 44×44px min | 44px height | — | Row: 44px min | 44×44px |

---

## 9. Design Token Format

<!-- GUIDANCE: Define how tokens are exported and consumed. Choose ONE canonical format. -->

### 9.1 CSS Custom Properties (Source of Truth)

```css
/* tokens/colors.css */
:root {
  --color-brand-500: #0EA5E9;
  /* ... */
}
```

### 9.2 Tailwind Config Extension

```js
// tailwind.config.ts
export default {
  theme: {
    extend: {
      colors: {
        brand: {
          500: 'var(--color-brand-500)',
          // ...
        }
      }
    }
  }
}
```

### 9.3 JavaScript/TypeScript Constants

```ts
// tokens/colors.ts — for use in charting libraries, canvas, etc.
export const colors = {
  brand500: '#0EA5E9',
  // ...
} as const;
```

**Token update process:** Figma → `Style Dictionary` export → CSS/JS/Tailwind files → PR review.

---

## 10. Component Documentation Standard

<!-- GUIDANCE: Every component in the library must follow this documentation format in Storybook. -->

Each component story must include:
1. **Default** story — basic render with minimal props
2. **All Variants** story — every visual variant displayed
3. **All States** story — hover, focus, disabled, error, loading
4. **Responsive** story — behavior at each breakpoint
5. **Accessibility story** — keyboard navigation, screen reader notes

Component JSDoc minimum:

```tsx
/**
 * Button component for primary user actions.
 *
 * @example
 * <Button variant="primary" onClick={handleSubmit}>Save Changes</Button>
 */
```

**Props table:** Every prop must have: type, default, required flag, description.

---

## Approval
| Role | Name | Date | Signature |
|------|------|------|-----------|
| Author | | | |
| Lead Designer | | | |
| Frontend Lead | | | |
| Accessibility Reviewer | | | |

# Forms

# Bilko Forms Documentation

**Current State:** Native HTML forms with React state
**Validation:** Client-side JavaScript validation (no schema validation yet)
**Future State:** Zod schemas for validation, react-hook-form for form management

---

## Invoice Creation Wizard (6-Step Multi-Page Form)

**Route:** `/invoices/new`
**File:** `app/(dashboard)/invoices/new/page.tsx`

### Form Structure

#### Step 1: Customer Selection

**Fields:**

- **Customer** (required)
  - Type: Select (dropdown)
  - Options: All customers from contacts (type='customer')
  - Can add new customer via dialog
  - Validation: Required before proceeding to step 2

**Add Customer Dialog:**

- **Name** (required)
  - Type: Text
  - Validation: Required
- **Email** (required)
  - Type: Email
  - Validation: Required, valid email format
- **Phone** (optional)
  - Type: Tel
  - Validation: None
- **Tax ID** (optional)
  - Type: Text
  - Validation: None

**Validation:**

- Alert shown if user tries to proceed without selecting customer
- Form submission triggers inline alert (no schema validation)

---

#### Step 2: Invoice Details

**Fields:**

- **Invoice Number**
  - Type: Text
  - Default: Auto-generated (e.g., "INV-2026-009")
  - Validation: None (can be edited)

- **Issue Date**
  - Type: Date
  - Default: Today's date
  - Validation: None

- **Due Date**
  - Type: Date
  - Default: 30 days from issue date
  - Validation: None

- **Net Terms** (shortcut selector)
  - Type: Select
  - Options: Net 15, Net 30, Net 60
  - Behavior: Auto-calculates due date when selected
  - Validation: None

- **Currency**
  - Type: Select
  - Options: EUR, RSD, BAM
  - Default: EUR
  - Validation: None

**Behavior:**

- Net terms selector auto-updates due date field
- All fields can be manually overridden

---

#### Step 3: Line Items

**Repeating Fields (Line Items):**

Each line item contains:

- **Description** (required)
  - Type: Text
  - Placeholder: "Service or product description"
  - Validation: At least one item must have description

- **Quantity**
  - Type: Number
  - Default: 1
  - Min: 1
  - Validation: Positive number

- **Unit Price**
  - Type: Number
  - Default: 0
  - Min: 0
  - Step: 0.01
  - Validation: Non-negative

- **VAT Rate**
  - Type: Select
  - Options: 0%, 10%, 17%, 20%, 25%
  - Default: 20%
  - Validation: None

- **Total** (calculated, read-only)
  - Type: Text (disabled input)
  - Calculation: `quantity * unitPrice * (1 + vatRate/100)`
  - Display: Formatted currency

**Actions:**

- **Add Item** button: Appends new empty line item
- **Remove Item** button (X): Removes line item (disabled if only 1 item)

**Totals Display (read-only):**

- Subtotal (before VAT)
- VAT Total
- Grand Total (with VAT)

**Validation:**

- Alert shown if user tries to proceed with all empty descriptions
- Form submission requires at least one item with description

---

#### Step 4: Customization

**Fields:**

- **Notes** (optional)
  - Type: Textarea
  - Default: "Thank you for your business!"
  - Placeholder: "Add a note for your customer..."
  - Validation: None

- **Terms** (optional)
  - Type: Textarea
  - Default: "Payment due within 30 days."
  - Placeholder: "Payment terms and conditions..."
  - Validation: None

**Behavior:**

- Both fields are optional
- Default values pre-populated but can be cleared

---

#### Step 5: Preview (Read-Only)

**No form fields.** Displays formatted invoice preview with all data from previous steps.

**Preview Elements:**

- Invoice title ("INVOICE")
- From/To addresses
- Invoice number, date, due date
- Line items table
- Subtotal, VAT, Total
- Notes (if provided)
- Terms (if provided)

**No validation.** Step is purely visual review.

---

#### Step 6: Send/Save

**Email Form:**

- **To** (required)
  - Type: Email
  - Default: Pre-filled with customer email
  - Validation: Valid email format (no schema yet)

- **Subject** (required)
  - Type: Text
  - Default: "Invoice {invoiceNumber}"
  - Validation: Required

- **Message** (required)
  - Type: Textarea
  - Default: Pre-filled template
  - Rows: 6
  - Validation: Required

- **Send Me a Copy** (optional)
  - Type: Checkbox
  - Default: Unchecked
  - Validation: None

**Action Buttons:**

- **Save as Draft** — Alert placeholder (no API)
- **Download PDF** — Alert placeholder (no API)
- **Send Invoice** — Alert + redirect to `/invoices` (no API)

**Validation:**

- No schema validation
- Form submission triggers alert "Invoice sent!"

---

### Form State Management

**Local State:**

```typescript
const [step, setStep] = useState(1)
const [customer, setCustomer] = useState<Contact | null>(null)
const [showAddCustomer, setShowAddCustomer] = useState(false)
const [invoiceDetails, setInvoiceDetails] = useState<InvoiceDetails>({
  number: 'INV-2026-009',
  issueDate: '2026-02-20',
  dueDate: '2026-03-22',
  currency: 'EUR',
})
const [lineItems, setLineItems] = useState<LineItem[]>([
  { description: '', quantity: 1, unitPrice: 0, vatRate: 20, total: 0 },
])
const [notes, setNotes] = useState('Thank you for your business!')
const [terms, setTerms] = useState('Payment due within 30 days.')
const [emailData, setEmailData] = useState({
  to: '',
  subject: '',
  message: '',
  sendCopy: false,
})
```

**No persistence:** All state lost on page refresh or navigation away.

**No Zod schemas:** Validation is inline JavaScript (alert boxes).

---

## Expense Form (Dialog)

**Route:** `/expenses`
**Component:** Dialog triggered by "Add Expense" button

### Form Fields

- **Amount** (required)
  - Type: Number
  - Placeholder: "0.00"
  - Validation: Required (no schema)

- **Currency** (required)
  - Type: Select
  - Options: EUR, RSD, BAM
  - Default: EUR
  - Width: 24px (narrow select next to amount)
  - Validation: None

- **Category** (required)
  - Type: Select
  - Options: Office, Travel, Meals, Utilities, Marketing, Infrastructure, Software, Professional Services
  - Placeholder: "Select category"
  - Validation: Required

- **Date** (required)
  - Type: Date
  - Default: Today's date
  - Validation: None

- **Vendor** (optional)
  - Type: Text
  - Placeholder: "Search vendor..."
  - Validation: None
  - Note: Not a searchable autocomplete yet — plain text input

- **Payment Method** (optional)
  - Type: Select
  - Options: Cash, Card, Bank Transfer
  - Placeholder: "Select method"
  - Validation: None

- **Receipt** (optional)
  - Type: File upload (placeholder UI only)
  - Display: Dashed border div with "📷 Upload or Drag" text
  - Behavior: No actual upload implemented
  - Validation: None

- **Description** (optional)
  - Type: Text
  - Placeholder: "Additional notes..."
  - Validation: None

### Form Actions

- **Cancel** — Closes dialog, resets form
- **Save Expense** — Logs form data to console, closes dialog, resets form

**No API submission.** Form data not persisted.

**No Zod schemas.** Validation is JavaScript logic in form submit handler.

---

## Settings Forms

**Route:** `/settings`
**File:** `app/(dashboard)/settings/page.tsx`

### Company Profile Form

**Fields:**

- **Company Name** (required)
  - Type: Text
  - Default: "SnowIT d.o.o."

- **Legal Form**
  - Type: Select
  - Options: d.o.o., a.d., Preduzetnik
  - Default: "d.o.o."

- **Address**
  - Type: Text
  - Default: "Zmaja od Bosne"

- **City**
  - Type: Text
  - Default: "Sarajevo"

- **Postal Code**
  - Type: Text
  - Default: "71000"

- **Country**
  - Type: Text
  - Default: "BiH"

- **Tax ID / PIB / JIB**
  - Type: Text
  - Default: "4200000000"

- **Base Currency**
  - Type: Select
  - Options: EUR, RSD, BAM
  - Default: "EUR"

- **Fiscal Year Start**
  - Type: Select
  - Options: Jan 1, Apr 1, Jul 1, Oct 1
  - Default: "Jan 1"

**Action:**

- **Save Changes** — Alert placeholder (no API)

**Validation:** None (no required fields enforced)

---

### Tax & Compliance Form

**Fields:**

- **Country**
  - Type: Select
  - Options: Serbia, BiH, Croatia
  - Default: "Serbia"

- **VAT Registered**
  - Type: Checkbox
  - Default: Checked

- **VAT Number** (conditional, shown only if VAT registered)
  - Type: Text
  - Default: "RS123456789"
  - Placeholder: "Enter VAT number"

- **VAT Rate** (conditional, shown only if VAT registered)
  - Type: Select
  - Options: 17% (BiH), 20% (Serbia), 25% (Croatia)
  - Default: "20"

**Compliance Reminders:**

- **VAT filing deadlines** — Checkbox (default: checked)
- **Annual tax returns** — Checkbox (default: checked)
- **Payroll tax deadlines** — Checkbox (default: unchecked)

**Action:**

- **Save Settings** — Alert placeholder (no API)

**Validation:** None

---

### Notification Preferences

**Email Notifications:**

- Invoice paid — Checkbox (default: checked)
- Invoice overdue — Checkbox (default: checked)
- Expense approved — Checkbox (default: unchecked)
- Bank account synced — Checkbox (default: checked)

**In-App Notifications:**

- Invoice updates — Checkbox (default: checked)
- Expense updates — Checkbox (default: checked)
- Reconciliation matches — Checkbox (default: unchecked)

**Action:**

- **Save Preferences** — Alert placeholder (no API)

**Validation:** None

---

### Security Settings

**Two-Factor Authentication:**

- **Enable 2FA** button — No functionality yet

**Session Timeout:**

- Type: Select
- Options: 15 minutes, 30 minutes, 1 hour, 4 hours
- Default: 30 minutes

**Password Policy:**

- Minimum 12 characters — Checkbox (default: checked)
- Require special characters — Checkbox (default: checked)
- Expire passwords after 90 days — Checkbox (default: unchecked)

**Actions:**

- **View Audit Log** — No functionality yet
- **Request Data Export** — No functionality yet
- **Delete Company** (Danger Zone) — No functionality yet

**Validation:** None

---

## Future Form Enhancements (Phase 2)

### Zod Schema Validation

**Planned:** Replace inline validation with Zod schemas

**Example (Invoice Wizard Step 1):**

```typescript
import { z } from 'zod'

const customerSchema = z.object({
  id: z.string(),
  name: z.string().min(1, 'Customer name required'),
  email: z.string().email('Valid email required'),
  phone: z.string().optional(),
  taxId: z.string().optional(),
})
```

**Benefits:**

- Type-safe validation
- Reusable schemas for API/DB
- Better error messages
- Centralized validation logic

---

### react-hook-form Integration

**Planned:** Replace useState with react-hook-form

**Example (Expense Form):**

```typescript
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'

const expenseSchema = z.object({
  amount: z.number().positive('Amount must be positive'),
  currency: z.enum(['EUR', 'RSD', 'BAM']),
  category: z.string().min(1, 'Category required'),
  date: z.string(),
  vendor: z.string().optional(),
  paymentMethod: z.string().optional(),
  description: z.string().optional(),
})

const {
  register,
  handleSubmit,
  formState: { errors },
} = useForm({
  resolver: zodResolver(expenseSchema),
})
```

**Benefits:**

- Automatic error handling
- Less boilerplate
- Better performance (no re-renders on every keystroke)
- Built-in dirty/touched state

---

### Field-Level Validation

**Planned:** Real-time validation as user types

**Example (Email Field):**

```typescript
<Input
  {...register("email")}
  type="email"
  error={errors.email?.message}
/>
{errors.email && (
  <span className="text-error text-sm">{errors.email.message}</span>
)}
```

**Current State:** No real-time validation, only on form submit.

---

### Form Persistence

**Planned:** Save draft forms to localStorage

**Use Cases:**

- Invoice wizard state saved between page refreshes
- Expense form data saved if user closes dialog accidentally

**Implementation:**

```typescript
// Save to localStorage on every state change
useEffect(() => {
  localStorage.setItem('invoice-draft', JSON.stringify(invoiceState))
}, [invoiceState])

// Load from localStorage on mount
useEffect(() => {
  const draft = localStorage.getItem('invoice-draft')
  if (draft) setInvoiceState(JSON.parse(draft))
}, [])
```

---

### File Upload (Receipt Attachment)

**Current State:** Placeholder UI only (dashed border div)

**Future Implementation:**

- Drag-and-drop file upload
- File size validation (max 5MB)
- File type validation (PDF, JPG, PNG)
- Preview uploaded file
- Remove uploaded file
- Upload to backend API

**API Endpoint (planned):**

```
POST /api/expenses/:id/receipt
Content-Type: multipart/form-data
```

---

### Autocomplete/Search Fields

**Current State:** Plain text inputs

**Future Enhancement (Vendor Field):**

- Searchable dropdown
- Autocomplete from existing vendors
- Create new vendor inline
- Match by partial name

**Library:** Radix UI Combobox or react-select

---

### Multi-Currency Conversion

**Current State:** User manually selects currency

**Future Enhancement:**

- Fetch live exchange rates
- Auto-convert amounts for display
- Store both original currency and base currency
- Show converted amounts in tooltips

---

## Summary

**Current Forms:**

1. Invoice Wizard (6-step) — Customer, Details, Line Items, Customization, Preview, Send
2. Expense Form (dialog) — Amount, Category, Date, Vendor, Receipt, etc.
3. Company Profile — All company settings
4. Tax & Compliance — VAT settings
5. Notification Preferences — Email/in-app notification toggles
6. Security Settings — 2FA, session timeout, password policy

**Validation:**

- Inline JavaScript (alert boxes)
- No schema validation
- No real-time validation
- No error state styling

**State Management:**

- React useState for all forms
- No persistence (lost on refresh)
- No form libraries (native HTML forms)

**Future (Phase 2):**

- Zod schemas for validation
- react-hook-form for form management
- Field-level validation
- Form persistence (localStorage)
- File upload functionality
- Autocomplete/search fields
- API integration for submission

# State Management

# State Management Architecture

> **Project:** {{PROJECT_NAME}}
> **Version:** {{VERSION}}
> **Date:** {{DATE}}
> **Author:** {{AUTHOR}}
> **Status:** Draft | In Review | Approved
> **Reviewers:** {{REVIEWERS}}

## Document History
| Version | Date | Author | Changes |
|---------|------|--------|---------|
| 0.1     | {{DATE}} | {{AUTHOR}} | Initial draft |

---

## 1. State Architecture Overview

<!-- GUIDANCE: Define the high-level state strategy. Which library handles which type of state? Why were these libraries chosen over alternatives? -->

**{{PROJECT_NAME}}** uses a layered state management approach:

| Layer | Library | Scope |
|-------|---------|-------|
| Server state | `{{TanStack Query / SWR / Apollo}}` | API data, caching, synchronization |
| Client / UI state | `{{Zustand / Redux Toolkit / Pinia}}` | Application-wide UI state |
| URL state | Native router APIs | Filters, pagination, search |
| Form state | `{{React Hook Form / Formik / VeeValidate}}` | Form data, validation |
| Persistent state | `{{localStorage / cookies}}` | User preferences, tokens |

**Guiding principle:** Server state is NOT stored in client state. API data lives in the query cache — client state holds only UI concerns (sidebar open, selected theme, modal visibility).

---

## 2. Data Flow Diagram

<!-- GUIDANCE: Update this diagram to reflect your actual libraries and data sources. -->

```mermaid
flowchart TD
    User["User Interaction"] --> Component["Component"]

    Component -->|"API call"| QueryCache["Query Cache\n(TanStack Query)"]
    Component -->|"UI action"| ClientStore["Client Store\n(Zustand)"]
    Component -->|"Navigate"| URLState["URL State\n(Router)"]
    Component -->|"Form input"| FormState["Form State\n(RHF)"]

    QueryCache -->|"fetch / mutation"| API["Backend API"]
    QueryCache -->|"cached data"| Component
    ClientStore -->|"state slice"| Component
    URLState -->|"params"| Component
    FormState -->|"values / errors"| Component

    ClientStore -->|"user prefs"| LocalStorage["localStorage\n(Persistent)"]
    LocalStorage -->|"hydrate on load"| ClientStore
```

---

## 3. State Categories

### 3.1 Server State (API Data)

<!-- GUIDANCE: Define how API data is fetched, cached, and invalidated. -->

**Library:** `{{TanStack Query v5}}`

**Configuration:**

```ts
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60 * 5,        // 5 minutes
      gcTime: 1000 * 60 * 30,          // 30 minutes garbage collection
      retry: 2,
      refetchOnWindowFocus: true,
      refetchOnReconnect: true,
    },
    mutations: {
      retry: 0,
    },
  },
});
```

**Query key convention:**

```ts
// Hierarchical keys for precise invalidation
['users']                          // all user queries
['users', { page: 1, search: '' }] // paginated user list
['users', userId]                  // single user
['users', userId, 'posts']         // user's posts
```

**Stale time per resource type:**

| Resource | Stale Time | GC Time | Rationale |
|----------|-----------|---------|-----------|
| User profile | 5 min | 30 min | Changes infrequently |
| Dashboard stats | 30 sec | 5 min | Near-realtime |
| Config / enums | 1 hour | 24 hours | Rarely changes |
| Notifications | 0 (always fresh) | 2 min | Time-sensitive |

---

### 3.2 Client State (UI State)

<!-- GUIDANCE: Define what belongs in client state. This should be UI-only concerns. Map each slice to its store file. -->

**Library:** `{{Zustand}}`

**Store slices:**

| Slice | File | Responsibility |
|-------|------|----------------|
| `uiStore` | `src/stores/ui.store.ts` | Sidebar open, active modal, theme |
| `authStore` | `src/stores/auth.store.ts` | Current user, roles, session token |
| `notificationStore` | `src/stores/notification.store.ts` | Toast queue, unread count |
| `{{featureStore}}` | `src/stores/{{feature}}.store.ts` | {{Feature-specific UI state}} |

**Slice template:**

```ts
// src/stores/ui.store.ts
import { create } from 'zustand';

interface UIState {
  sidebarOpen: boolean;
  activeModal: string | null;
  theme: 'light' | 'dark' | 'system';
}

interface UIActions {
  setSidebarOpen: (open: boolean) => void;
  openModal: (id: string) => void;
  closeModal: () => void;
  setTheme: (theme: UIState['theme']) => void;
}

export const useUIStore = create<UIState & UIActions>((set) => ({
  sidebarOpen: true,
  activeModal: null,
  theme: 'system',

  setSidebarOpen: (open) => set({ sidebarOpen: open }),
  openModal: (id) => set({ activeModal: id }),
  closeModal: () => set({ activeModal: null }),
  setTheme: (theme) => set({ theme }),
}));
```

---

### 3.3 URL State

<!-- GUIDANCE: Define what lives in the URL. This makes the app linkable and shareable. -->

URL state is the source of truth for:
- Search / filter queries
- Pagination (page, pageSize)
- Sort column and direction
- Active tab / view mode
- Modal ID (when deep-linkable)

**Convention:**

```
/users?page=2&pageSize=25&search=john&sort=name&dir=asc&status=active
```

**Library:** Native `URLSearchParams` + router `useSearchParams` hook

**Serialization helper:** `src/lib/url-state.ts`

```ts
// Type-safe URL param parsing with fallbacks
export function parseListParams(params: URLSearchParams): ListParams {
  return {
    page: Number(params.get('page') ?? 1),
    pageSize: Number(params.get('pageSize') ?? 25),
    search: params.get('search') ?? '',
    sort: params.get('sort') ?? 'createdAt',
    dir: (params.get('dir') as 'asc' | 'desc') ?? 'desc',
  };
}
```

---

### 3.4 Form State

<!-- GUIDANCE: Define the form library and validation approach. -->

**Library:** `{{React Hook Form v7}}`
**Validation:** `{{Zod}}`

**Pattern:**

```ts
const schema = z.object({
  email: z.string().email('Invalid email'),
  name: z.string().min(2, 'Name must be at least 2 characters'),
});

const form = useForm<z.infer<typeof schema>>({
  resolver: zodResolver(schema),
  defaultValues: { email: '', name: '' },
});
```

**Rules:**
- Form state NEVER leaks into global store
- Schema validation lives in `src/schemas/` — reused for API validation
- Complex multi-step forms use form context + Stepper component

---

### 3.5 Persistent State

<!-- GUIDANCE: Define what is persisted and where. Security-sensitive data must not go in localStorage. -->

| Data | Storage | Library | Encryption |
|------|---------|---------|------------|
| Theme preference | `localStorage` | Zustand persist middleware | No |
| Sidebar collapsed | `localStorage` | Zustand persist middleware | No |
| Language preference | `localStorage` | Native | No |
| Auth token | `httpOnly cookie` | Server-set | Yes (TLS) |
| Refresh token | `httpOnly cookie` | Server-set | Yes (TLS) |

**RULE:** Auth tokens NEVER in `localStorage`. HttpOnly cookies only.

**Zustand persistence example:**

```ts
import { persist } from 'zustand/middleware';

export const usePrefsStore = create(
  persist<PrefsState>(
    (set) => ({ theme: 'system', /* ... */ }),
    { name: 'user-preferences' }
  )
);
```

---

## 4. Caching Strategy

<!-- GUIDANCE: Define cache behavior for each resource type and how cache is invalidated on mutations. -->

### 4.1 Optimistic Updates

```ts
// Optimistic update pattern with rollback
const mutation = useMutation({
  mutationFn: updateUser,
  onMutate: async (newData) => {
    // Cancel outgoing refetches
    await queryClient.cancelQueries({ queryKey: ['users', userId] });
    // Snapshot current state for rollback
    const snapshot = queryClient.getQueryData(['users', userId]);
    // Optimistically update cache
    queryClient.setQueryData(['users', userId], (old) => ({ ...old, ...newData }));
    return { snapshot };
  },
  onError: (err, vars, context) => {
    // Rollback on error
    queryClient.setQueryData(['users', userId], context?.snapshot);
  },
  onSettled: () => {
    // Always refetch to sync with server
    queryClient.invalidateQueries({ queryKey: ['users', userId] });
  },
});
```

### 4.2 Cache Invalidation Rules

| Mutation | Invalidates |
|----------|------------|
| Create user | `['users']` (list) |
| Update user | `['users', userId]` |
| Delete user | `['users']` (list) |
| Update user role | `['users', userId]`, `['permissions']` |

---

## 5. Real-Time State

<!-- GUIDANCE: Define the strategy for WebSocket or SSE connections and how they update state. -->

**Protocol:** `{{WebSocket | Server-Sent Events | None}}`
**Library:** `{{Socket.io | native WebSocket | @microsoft/signalr}}`

**Pattern — WebSocket to Query Cache:**

```ts
// On incoming WS event, update query cache directly
socket.on('user.updated', (user: User) => {
  queryClient.setQueryData(['users', user.id], user);
  queryClient.invalidateQueries({ queryKey: ['users'], exact: false });
});
```

**Connection management:**
- Reconnect with exponential backoff: 1s, 2s, 4s, 8s, 16s, max 30s
- Show "reconnecting" banner in UI after 5s disconnect
- Batch updates: max 50ms batching window to prevent UI thrashing

**TODO:** Document specific WebSocket event schema — reference `event-schema-documentation.md`.

---

## 6. Hydration Strategy (SSR ↔ Client)

<!-- GUIDANCE: Define how server-fetched data is hydrated into the client without unnecessary refetches. -->

**Approach:** `{{Dehydrate/Hydrate (TanStack Query) | getServerSideProps | prefetchQuery}}`

```ts
// Server: prefetch and dehydrate
export async function getServerSideProps() {
  const queryClient = new QueryClient();
  await queryClient.prefetchQuery({
    queryKey: ['users'],
    queryFn: fetchUsers,
  });
  return {
    props: { dehydratedState: dehydrate(queryClient) },
  };
}

// Client: hydrate — no refetch until staleTime expires
export default function Page({ dehydratedState }) {
  return (
    <HydrationBoundary state={dehydratedState}>
      <UserList />
    </HydrationBoundary>
  );
}
```

**Rule:** Prefetch all critical page data on server. No loading spinners on initial navigation.

---

## 7. State Debugging Tools

<!-- GUIDANCE: List tools available to developers for inspecting state during development. -->

| Tool | Usage | Enabled In |
|------|-------|-----------|
| TanStack Query Devtools | Inspect cache, queries, mutations | Dev only |
| Zustand devtools middleware | Redux DevTools integration | Dev only |
| React DevTools | Component state tree | Dev only |
| Redux DevTools Extension | If using Redux | Dev only |

**Setup:**

```ts
// Devtools enabled only in development
const devtools = process.env.NODE_ENV === 'development'
  ? (await import('zustand/middleware')).devtools
  : (f: any) => f;
```

---

## 8. Performance Considerations

<!-- GUIDANCE: Define rules for avoiding unnecessary re-renders and optimizing state subscriptions. -->

### 8.1 Selector Pattern (Zustand)

```ts
// BAD — subscribes to entire store, re-renders on any change
const { sidebarOpen, theme } = useUIStore();

// GOOD — subscribe to only what the component needs
const sidebarOpen = useUIStore((s) => s.sidebarOpen);
const theme = useUIStore((s) => s.theme);
```

### 8.2 Memoization Rules

| Scenario | Tool | When to Use |
|----------|------|-------------|
| Expensive derived data | `useMemo` | Only if profiling shows issue |
| Stable callback refs | `useCallback` | Only if passed to memoized child |
| Stable component output | `React.memo` | Only if parent re-renders frequently |

**Rule:** Do NOT pre-emptively memoize. Profile first, optimize second.

### 8.3 Query Deduplication

TanStack Query automatically deduplicates identical queries rendered simultaneously. No additional work needed.

**TODO:** Run React Profiler on critical paths and document findings.

---

## Approval
| Role | Name | Date | Signature |
|------|------|------|-----------|
| Author | | | |
| Frontend Lead | | | |
| Tech Lead | | | |

# Frontend — Status & Architecture

# Bilko Web — Next.js Frontend

## BookStack — Provjeri PRVO

Prije traženja bilo čega — provjeri BookStack (https://docs.basicconsulting.no). Centralna baza znanja za tools, skills, hooks, agents, rules, projekte, klijente, dokumentaciju. Ako odgovor postoji tamo — NE TRAŽI dalje.

## Tech Stack

- **Framework:** Next.js 15 (App Router)
- **React:** 19.0.0
- **TypeScript:** 5.3.0
- **Styling:** Tailwind CSS 4 + shadcn/ui
- **State:** Zustand 4.5.0 (installed but mostly React hooks)
- **Charts:** Recharts 2.15.0
- **Icons:** Lucide React

## Pages (App Router)

All pages under `app/(dashboard)/`:

- `dashboard/page.tsx` — Revenue, expenses, charts
- `invoices/page.tsx` — Invoice list
- `invoices/new/page.tsx` — 6-step invoice wizard
- `expenses/page.tsx` — Expense list
- `purchases/page.tsx` — Alias to expenses
- `banking/page.tsx` — Placeholder
- `reports/page.tsx` — Reports hub (4 active report cards)
- `reports/vat/page.tsx` — VAT report (real API + date filter)
- `reports/profit-loss/page.tsx` — P&L statement (real API + date filter)
- `reports/balance-sheet/page.tsx` — Balance sheet (real API + date filter)
- `reports/trial-balance/page.tsx` — Trial balance (real API + date filter)
- `settings/page.tsx` — User settings

## Components

**UI (shadcn/ui):** 17 components in `components/ui/`

- avatar, button, card, dialog, dropdown-menu, input, label, select, separator, sheet, skeleton, table, tabs

**Layout:**

- `components/sidebar.tsx` — Dark navigation sidebar
- `components/top-bar.tsx` — Header with user menu

## Design System

**Embedded in `tailwind.config.ts`:** 73 tokens

- **Colors:** primary (#8B6BBF Plum), secondary (#5B3E8A), accent (#F2C87A Gold), surface (#F9F7FC), sidebar white, chart colors
- **Typography:** Work Sans (body), National Park (headings), DM Mono (mono), 8 sizes (xs to 4xl)
- **Spacing:** 8px grid (xs, sm, md, lg, xl, 2xl, 3xl)
- **Radius:** 4 values (sm: 6px, md: 8px, lg: 12px, full: 9999px)
- **Shadows:** card, modal, dropdown
- **Breakpoints:** sm (640px), md (768px), lg (1024px), xl (1280px)

## API Integration

All data fetched from real backend API via `lib/api.ts` and Zustand stores in `lib/stores/`.

- `lib/api.ts` — Typed API client with auth, token refresh, and all endpoint methods
- `lib/stores/` — Zustand stores per resource (dashboard, invoices, expenses, contacts, banking, settings)
- `lib/mock-data.ts` — DELETED (2026-03-05). All pages now use real API.
- Pages use stores via hooks (useDashboardStore, useInvoiceStore, etc.)
- Loading and error states handled in every page and store

## State Management

- **Zustand installed** but not yet used
- Currently: React hooks (useState, useEffect)
- **Future:** Migrate to Zustand stores for global state (user, org, auth)

## Development Rules

1. **No production mock data** — Always flag mock data usage
2. **Design system tokens** — Use tokens from tailwind.config.ts, NEVER hardcode colors
3. **Responsive** — Mobile-first, test at all breakpoints
4. **Accessibility** — Use shadcn/ui primitives (Radix UI), semantic HTML
5. **TypeScript strict** — No `any` types without explicit justification

## API Integration (Future)

When backend ready:

- Create `lib/api.ts` with fetch wrappers
- Replace mock-data imports with API calls
- Add loading states, error handling
- Implement auth token management (JWT)

# Frontend Architecture

# Frontend Architecture Document

> **Project:** {{PROJECT_NAME}}
> **Version:** {{VERSION}}
> **Date:** {{DATE}}
> **Author:** {{AUTHOR}}
> **Status:** Draft | In Review | Approved
> **Reviewers:** {{REVIEWERS}}

## Document History
| Version | Date | Author | Changes |
|---------|------|--------|---------|
| 0.1     | {{DATE}} | {{AUTHOR}} | Initial draft |

---

## 1. Purpose & Scope

<!-- GUIDANCE: Describe the purpose of this document. What frontend system does it cover? Who are the intended readers (engineers, architects, PMs)? -->

This document defines the frontend architecture for **{{PROJECT_NAME}}**. It covers framework decisions, folder structure, rendering strategy, routing, build configuration, performance targets, and operational considerations.

**TODO:** Define scope boundaries — which client applications are covered (web app, admin panel, marketing site, etc.).

---

## 2. Framework Choice & Rationale

<!-- GUIDANCE: Document which framework was selected and WHY. Include alternatives considered and the decision matrix. -->

| Criterion | Weight | Next.js | Nuxt 3 | SvelteKit | Remix |
|-----------|--------|---------|--------|-----------|-------|
| Team expertise | 30% | {{SCORE}} | {{SCORE}} | {{SCORE}} | {{SCORE}} |
| Ecosystem maturity | 25% | {{SCORE}} | {{SCORE}} | {{SCORE}} | {{SCORE}} |
| Performance | 20% | {{SCORE}} | {{SCORE}} | {{SCORE}} | {{SCORE}} |
| DX / tooling | 15% | {{SCORE}} | {{SCORE}} | {{SCORE}} | {{SCORE}} |
| License / cost | 10% | {{SCORE}} | {{SCORE}} | {{SCORE}} | {{SCORE}} |

**Selected Framework:** `{{FRAMEWORK}}`

**Rationale:**
> TODO: Write 3-5 sentences explaining the final decision.

**Node/runtime version:** `{{NODE_VERSION}}`
**Package manager:** `{{npm | yarn | pnpm | bun}}`

---

## 3. Project Folder Structure

<!-- GUIDANCE: Paste the actual tree output or describe the intended structure. Explain the purpose of non-obvious directories. -->

```
{{PROJECT_NAME}}/
├── src/
│   ├── app/              # Route segments (App Router) or pages/
│   ├── components/
│   │   ├── ui/           # Primitive design system components
│   │   ├── features/     # Feature-scoped composite components
│   │   └── layouts/      # Page layout wrappers
│   ├── hooks/            # Shared custom hooks / composables
│   ├── lib/              # Pure utilities, third-party wrappers
│   ├── stores/           # Client state (Zustand / Pinia slices)
│   ├── services/         # API client, data-fetching layer
│   ├── styles/           # Global CSS, theme tokens
│   ├── types/            # TypeScript interfaces & enums
│   └── constants/        # App-wide constants
├── public/               # Static assets served at root
├── tests/
│   ├── unit/
│   ├── integration/
│   └── e2e/
├── .env.example
├── next.config.ts        # (or framework config file)
└── package.json
```

**TODO:** Update tree to match actual project structure.

---

## 4. Rendering Strategy

<!-- GUIDANCE: Define which rendering mode is used for which type of page/route. Explain the rationale for each choice. -->

| Page Type | Strategy | Rationale |
|-----------|----------|-----------|
| Marketing / landing | SSG | Maximum caching, no auth dependency |
| Dashboard / app views | SSR | Fresh data, auth-gated |
| Listing pages | ISR (revalidate: 60s) | Balance freshness vs performance |
| User-specific views | CSR | Dynamic, personalized data |
| API routes | Server | Thin BFF layer |

**Hydration approach:** `{{Partial hydration | Full hydration | Islands}}`

**TODO:** Map every top-level route to a rendering strategy.

---

## 5. Routing Architecture

<!-- GUIDANCE: Describe the routing system — file-based, hash, history API. Cover dynamic segments, catch-all routes, and route guards/middleware. -->

### 5.1 Route Organization

```
/                        → Home (SSG)
/app/                    → App shell (SSR, auth-required)
/app/dashboard           → Dashboard
/app/[resource]/[id]     → Dynamic resource detail
/api/                    → API routes (BFF)
/auth/login              → Login (CSR)
/[...catchAll]           → 404 handler
```

### 5.2 Route Guards / Middleware

<!-- GUIDANCE: Describe how protected routes are secured — middleware file, HOC, layout wrappers. -->

```
Middleware execution order:
1. Request logging
2. Authentication check (JWT validation / session lookup)
3. Redirect unauthenticated → /auth/login
4. Role-based route protection
5. Locale detection / redirect
```

**TODO:** List all protected route patterns and their required roles.

---

## 6. Build & Bundle Configuration

<!-- GUIDANCE: Document key build configuration decisions — code splitting, tree shaking, compression, source maps policy. -->

### 6.1 Key Config Options

| Option | Value | Reason |
|--------|-------|--------|
| Output | `standalone` | Docker-optimized deployment |
| Image optimization | Enabled | Next/Image with defined domains |
| Bundle analyzer | `ANALYZE=true` | On-demand local analysis |
| Source maps | Production: hidden | Security — upload to Sentry only |
| Compression | gzip + brotli | CDN-level, not server-level |

### 6.2 Code Splitting Strategy

- **Route-level splitting:** Automatic per page/route segment
- **Component-level splitting:** `dynamic()` / `defineAsyncComponent()` for modals, heavy widgets
- **Third-party splitting:** Vendor chunk isolation for large deps (charts, editors)

**TODO:** Identify and document specific lazy-loaded components.

---

## 7. Performance Budget

<!-- GUIDANCE: Define measurable performance targets. These become acceptance criteria for CI gates. -->

| Metric | Target | Tool |
|--------|--------|------|
| LCP (Largest Contentful Paint) | < 2.5s | Lighthouse, CrUX |
| FID / INP (Interaction to Next Paint) | < 200ms | CrUX |
| CLS (Cumulative Layout Shift) | < 0.1 | Lighthouse |
| TTFB (Time to First Byte) | < 600ms | WebPageTest |
| Total JS bundle (compressed) | < 200 KB | Bundlesize CI check |
| First page load (mobile 4G) | < 3s | WebPageTest |
| Lighthouse Performance Score | ≥ 90 | Lighthouse CI |

**CI enforcement:** `TODO: link to bundlesize config or Lighthouse CI workflow`

---

## 8. Asset Management

<!-- GUIDANCE: Cover how images, fonts, icons, and other static assets are handled. -->

### 8.1 Images
- Format: WebP primary, JPEG fallback, SVG for vector
- Optimization: Framework image component (`next/image` / `nuxt/image`)
- CDN: `{{CDN_PROVIDER}}` — origin: `{{STORAGE_BUCKET}}`
- Lazy loading: Native `loading="lazy"` + framework component

### 8.2 Fonts
- Self-hosted via `/public/fonts/` (no Google Fonts runtime requests)
- `font-display: swap` on all faces
- Subset to used characters if possible

### 8.3 Icons
- Library: `{{Lucide | Heroicons | custom SVG sprite}}`
- Delivery: Inline SVG via component (no icon font)

**TODO:** Finalize CDN domain and storage bucket configuration.

---

## 9. Internationalization (i18n) Strategy

<!-- GUIDANCE: Define i18n library, locale routing, translation file format, and update process. -->

| Item | Decision |
|------|----------|
| Library | `{{next-intl | i18next | vue-i18n}}` |
| Locale routing | `/[locale]/path` prefix |
| Default locale | `{{en}}` |
| Supported locales | `{{en, nb, de}}` |
| Translation format | JSON key-value (per locale, per namespace) |
| Translation storage | `src/messages/[locale]/[namespace].json` |
| Pluralization | ICU message format |
| RTL support | `{{Yes | No | Planned}}` |

**Translation workflow:**
1. Developer adds key with English string
2. `TODO: extraction tool` generates translation keys
3. Translator fills missing keys in Phrase/Lokalise/manual JSON
4. CI validates no missing keys before deploy

---

## 10. Error Boundary Strategy

<!-- GUIDANCE: Define how errors are caught and surfaced at different levels of the component tree. -->

| Level | Scope | Behavior |
|-------|-------|----------|
| Global `error.tsx` | Full page crash | Show branded error page, report to Sentry |
| Layout boundary | Section crash | Isolate — rest of page remains usable |
| Async component | Data fetch failure | Skeleton → error state UI |
| Form submission | Mutation failure | Inline error message + retry |

**Error reporting:** `{{Sentry | Datadog | custom}}`
**DSN:** `{{SENTRY_DSN}}` (environment variable — never hardcode)

**TODO:** Implement and test each boundary level.

---

## 11. Environment Configuration

<!-- GUIDANCE: List all environment variables, their purpose, and which environments they apply to. Never store secrets here — reference vault locations. -->

| Variable | Dev | Staging | Prod | Description |
|----------|-----|---------|------|-------------|
| `NEXT_PUBLIC_API_URL` | `http://localhost:4000` | `https://api-staging.{{DOMAIN}}` | `https://api.{{DOMAIN}}` | Backend API base URL |
| `NEXT_PUBLIC_APP_ENV` | `development` | `staging` | `production` | Runtime environment flag |
| `SENTRY_DSN` | optional | required | required | Error reporting (server-side) |
| `NEXT_PUBLIC_SENTRY_DSN` | optional | required | required | Error reporting (client-side) |
| `ANALYZE` | `true/false` | — | — | Enable bundle analyzer |

**Secrets management:** All non-public secrets stored in `{{Vault / AWS Secrets Manager / Vercel env}}`.
**`.env.example`:** Must be kept up to date — CI validates no undocumented variables exist.

---

## 12. Dependency Management Policy

<!-- GUIDANCE: Define rules for adding, updating, and auditing dependencies. -->

| Rule | Detail |
|------|--------|
| Approval required for new deps | PR must include: bundle size impact, license check, last release date |
| Allowed licenses | MIT, Apache-2.0, ISC, BSD-2, BSD-3 |
| Security audit | `npm audit` / `pnpm audit` in CI — fail on high/critical |
| Update cadence | Minor/patch: monthly automated PR (Renovate); Major: manual + reviewed |
| Banned patterns | No `moment.js` (use `date-fns`), no `lodash` (use native / `lodash-es`) |

**TODO:** Configure Renovate or Dependabot with appropriate grouping rules.

---

## 13. Architecture Diagram

<!-- GUIDANCE: Replace the placeholder below with an actual Mermaid diagram reflecting the system's frontend architecture. -->

```mermaid
graph TB
    subgraph "Client Browser"
        Browser["User Browser"]
    end

    subgraph "Frontend Application — {{FRAMEWORK}}"
        CDN["CDN Edge (Static Assets)"]
        SSR["SSR Server / Edge Runtime"]
        Pages["Route Segments / Pages"]
        Components["Component Tree"]
        State["State Layer (Server + Client)"]
        Services["API Service Layer"]
    end

    subgraph "Backend"
        API["REST / GraphQL API"]
        Auth["Auth Service"]
    end

    Browser --> CDN
    Browser --> SSR
    SSR --> Pages
    Pages --> Components
    Components --> State
    State --> Services
    Services --> API
    SSR --> Auth
```

**TODO:** Refine diagram to reflect actual deployed infrastructure (Vercel, AWS, self-hosted, etc.).

---

## Approval
| Role | Name | Date | Signature |
|------|------|------|-----------|
| Author | | | |
| Tech Lead | | | |
| Architect | | | |
| Engineering Manager | | | |