Frontend
Pages, components, design system, state management, landing pages
- Frontend Architecture
- Frontend Architecture Document
- Accessibility Audit
- Design System
- Forms
- State Management
- Frontend — Status & Architecture
- Frontend Architecture
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) andstats(3 items) defined inline - Sections:
- Header with DropLogoFull + nav links (Tjenester, Priser, Om oss, Logg inn, Kom i gang)
- Hero with headline, subtext, CTA buttons, phone mockup placeholder
- Features grid (Send penger, Betal med QR, Virtuelt kort)
- Stats bar (0.5% Gebyr, <2t Leveringstid, 30+ Land)
- Trust section (BankID verified, Rask overføring, 30+ land)
- Merchant CTA section
- 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/loginwith{ 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:
- Info — firstName, lastName, email, phone (+47 prefix), dateOfBirth, password
- Verify — 6-digit OTP input (MVP: any 6 digits accepted)
- PIN — 4-digit PIN with custom numpad UI
- 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:
- Header: DropLogo + notification bell + logout + avatar initials
- Balance card: primary account balance, formatted NOK
- Action buttons: Send penger (→ /send), Skann QR (→ /scan)
- Recent transactions list in ScrollArea
- 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.bankAccountsfrom auth hook (no separate fetch) - Layout:
- PSD2/Open Banking info banner (blue)
- Account cards: bankName, masked accountNumber, balance, currency, isPrimary badge
- Total balance summary
- "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:
- Scanning — Camera viewfinder UI with scan frame, "Simuler skanning" button (demo)
- Payment — Shows merchant info, amount input, 1% fee calculation
- Paying — Loading spinner
- Success — Confirmation with transaction details
- Data fetching: POST
/api/transactions/qr-paymentwith{ 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:
- Select Recipient — List from GET
/api/recipients, shows name + country flag - Enter Amount — NOK input, real-time conversion with exchange rate, 0.5% fee display
- Review — Summary of recipient, amount, rate, fee, total
- Success — Confirmation with reference number
- Select Recipient — List from GET
- Data fetching:
- GET
/api/recipients(on mount) - GET
/api/rates(on mount) - POST
/api/transactions/remittancewith{ recipientId, amountNOK, targetCurrency }
- GET
- 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
userfrom auth hook - Layout:
- User info card with initials avatar (green bg), full name, email
- Menu items: Mine kontoer (→ /accounts), Varsler, Innstillinger, Sikkerhet, Hjelp og stotte
- Logout button with confirmation
- 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:
- Info section explaining angrerett (right to cancel service agreement within 14 days)
- Warning banner: Angrerett does not apply to completed payment transactions, only to the service agreement itself
- Form with optional reason dropdown (not_needed, alternative, not_satisfied, other) and comment textarea
- Submit button (red) with AML retention notice (data kept for 5 years per hvitvaskingsloven)
- 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/complaintswith{ category, subject, description } - Purpose: Finansavtaleloven §3-53 compliance — formal complaint submission with 15 business day response requirement
- Layout:
- Info text: All complaints taken seriously, up to 15 business days processing time
- Form with required fields:
- Category dropdown: transaction, fees, service, privacy, other
- Subject text input (max 200 chars)
- Description textarea (max 2000 chars)
- Submit button with POST to
/api/complaints - External complaint authority section: Finansklagenemnda (FinKN) contact info with link to finansklagenemnda.no
- 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:
- Behandlingsansvarlig: ALAI Holding AS as data controller
- 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)
- Formaal med behandlingen: Transaction processing (PSD2/PISP), KYC/AML compliance, AML/terrorism prevention, service improvement
- Rettslig grunnlag: Contract (GDPR 6(1)(b)), Legal obligation (6(1)(c)) for AML/KYC, Legitimate interest (6(1)(f)) for service improvement
- Dine rettigheter: Innsyn (access), Retting (rectification), Sletting (erasure with 5-year AML retention), Dataportabilitet (portability)
- Oppbevaring: Minimum 5 years per hvitvaskingsloven, anonymization on account deletion but AML data retained
- 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:
- Om tjenesten: Drop as PISP/AISP under PSD2, provided by ALAI Holding AS
- Krav til brukere: 18+ age, Norwegian residency with BankID, KYC required, Norwegian phone (+47)
- Betalingsmodell: Drop never holds customer money, pass-through model via Open Banking
- Gebyrer: All fees shown before transaction confirmation, see fees page for full list
- Ansvar: Drop responsible for correct payment execution per betalingstjenesteloven, refund rights per law, not liable for bank/recipient delays
- Misbruk og sperring: Right to block accounts for suspected AML/fraud, mandatory STR reporting to Økokrim/EFE
- Angrerett: 14-day withdrawal right per angrerettloven (does not apply to completed transactions)
- 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:
- 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%)
- QR-betaling i butikk:
- For customer: Free (no charge to payer)
- For merchant: 0.5% (lower than card terminals)
- Kontotjenester:
- Account creation: Free
- Monthly fee: Free
- Bank account linking (AISP): Free
- Viktig informasjon:
- Fees can change with 30-day notice
- Exchange rates updated in real-time from market
- Always see final amount before confirming
- Overføring til utlandet:
/profile/personal — Personal Information
- File:
app/profile/personal/page.tsx - Type: Client component
- Auth: Yes (
useAuth()) - Components used: ChevronLeft, ShieldCheck (lucide), BottomNav
- State: Reads
userfrom auth hook - Layout:
- User avatar with initials (green gradient)
- Full name and email display
- Read-only form fields: firstName, lastName, email, phone (+47 987 65 432), dateOfBirth (15. mars 1995)
- 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:
- Passord: Change password button (shows "Sist endret: Aldri")
- To-faktor autentisering:
- BankID verification: Active (green badge)
- Vipps verification: Not activated (gray badge)
- Aktive enheter:
- iPhone 15 Pro: Oslo, Norge — Aktiv nå (green dot indicator)
- MacBook Pro: Oslo, Norge — I går kl. 18:45
- 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/settingswith{ pushEnabled: boolean }or{ emailEnabled: boolean }(on toggle)
- GET
- Layout:
- Push-varsler toggle switch (Bell icon)
- 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/settingswith{ language: string }(on save)
- GET
- Languages: nb (Norsk Bokmål), en (English), bs (Bosanski), sq (Shqip)
- Layout:
- Language list with radio selection (green checkmark for selected)
- "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/notificationswith{ notificationIds: [ids] }(mark read, fire-and-forget)
- GET
- Interface:
Notification { id, type, title, body, read, createdAt } - Layout:
- Header with back button + "Varsler" title
- Empty state: Bell icon + "Ingen varsler enna" message
- Grouped notifications: I DAG / I GÅR / date groups
- Notification cards: icon based on type, title, body, timestamp, unread dot indicator
- 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 tofalse - 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/cardswith type "physical" + address (order physical) - PATCH
/api/cards/{id}with{status: "frozen" | "active"} - DELETE
/api/cards/{id}
- GET
- 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
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
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)
3.1 Button
| Property | Value |
|---|---|
| Category | Atom |
| Status | `{{Done |
| 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:
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-describedbypointing 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)
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)
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
| 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
| 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
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)
/* 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)
: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)
/* 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
.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
/* 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
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
@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)
/* 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
// 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)
// 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
<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
<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
<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
<p className="rounded-md bg-[#EF4444]/10 p-2 text-sm text-[#EF4444]">
{error}
</p>
Primary Badge
<span className="rounded-full bg-[#0B6E35]/10 px-2 py-0.5 text-xs font-medium text-[#0B6E35]">
Primær konto
</span>
Avatar (Initials)
<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)
// 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
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:
function useAuth(redirectIfUnauthenticated?: boolean): {
user: User | null;
loading: boolean;
logout: () => Promise<void>;
refreshUser: () => Promise<void>;
}
// Default: redirectIfUnauthenticated = true
User Model:
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:
- On mount: fetches
GET /api/auth/mewithcredentials: "include"(httpOnly cookie auth) - If 401 and
redirectIfUnauthenticatedis true: redirects to/login logout(): callsPOST /api/auth/logout, clears cookie server-side, redirects to/loginrefreshUser(): re-fetches/api/auth/meto 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:
// 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— readsuser.bankAccounts(no separate fetch)profile/page.tsx— readsuser.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:
// 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:
// 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.
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— fetchesGET /api/transactions?limit=10transactions/page.tsx— fetchesGET /api/transactions?type={filter}&limit=50send/page.tsx— fetchesGET /api/recipientsandGET /api/ratesnotifications/page.tsx— fetchesGET /api/notificationsprofile/notifications/page.tsx— fetchesGET /api/settingsprofile/language/page.tsx— fetchesGET /api/settingsmerchant/page.tsx— fetches/api/merchants/dashboard,/api/merchants/transactions,/api/merchants/qrcards/page.tsx— fetchesGET /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.
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/loginregister/page.tsx— POST/api/auth/register(4-step: info, OTP, PIN, success)send/page.tsx— POST/api/transactions/remittancescan/page.tsx— POST/api/transactions/qr-paymentcomplaints/page.tsx— POST/api/complaintswithdrawal/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.
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.
// 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:
RefreshControlonFlatListcomponents - 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
useEffecthas explicit dependency arrays useStateupdates are batched by React 19- No derived state that needs memoization in current implementation
Fresh Data on Navigation
useAuth()re-fetches/api/auth/meon 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)
- Add
SWRorTanStack Queryfor caching with stale-while-revalidate - Persist query cache across navigation (currently re-fetches on each visit)
- 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
@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
--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 divsswitchTab(name)for tab navigation within the app screen- API calls to
localhost:3001/apiwith 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 inmobile-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):
// 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/imagewith defined domains - Lazy loading: Native
loading="lazy"vianext/image
8.2 Fonts
- Fraunces (variable, 400-900) — loaded via
next/font/googleinlayout.tsx - DM Sans (variable, 400-700) — loaded via
next/font/googleinlayout.tsx - Geist Mono — loaded via
next/font/googleinlayout.tsx - CSS variables:
--font-fraunces,--font-dm-sans,--font-geist-mono font-display: swapon all faces (handled bynext/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— frameworkshadcn/ui+@radix-ui/*— component primitiveslucide-react— icon librarytailwindcss— stylingclass-variance-authority,clsx,tailwind-merge— class utilities
13. Architecture Diagram
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
| 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
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
2.3 User Testing
{{Yes — included users with disabilities in user testing | No — planned for future}}:{{Notes}}
3. Perceivable
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 descriptivealttext oralt="" - 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>withscopeattributes - Heading hierarchy is logical (H1 → H2 → H3, no skipped levels)
- Lists use
<ul>/<ol>— not styled<div> - Form inputs have
autocompleteattributes 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
| 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:
- User registration / login
- Main navigation
- Form submission with validation errors
- Data table — sorting, pagination, row actions
- Modal open/close/action
- Toast / notification announced
8. Keyboard Navigation Map
| 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
| 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
| 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
| 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
| 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
2.1 Primitive Palette (Raw Values)
/* 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)
/* 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)
[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
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
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
5.1 Primitive Components (Atoms)
| Component | Status | Variants | Storybook |
|---|---|---|---|
| Button | `{{Done | WIP | Planned}}` |
| Input | `{{Done | WIP | Planned}}` |
| Select | `{{Done | WIP | Planned}}` |
| Checkbox | `{{Done | WIP | Planned}}` |
| Radio | `{{Done | WIP | Planned}}` |
| Toggle/Switch | `{{Done | WIP | Planned}}` |
| Textarea | `{{Done | WIP | Planned}}` |
| Badge | `{{Done | WIP | Planned}}` |
| Avatar | `{{Done | WIP | Planned}}` |
| Tooltip | `{{Done | WIP | Planned}}` |
| Spinner | `{{Done | WIP | Planned}}` |
| Divider | `{{Done | WIP | Planned}}` |
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
| Item | Standard |
|---|---|
| Library | `{{Lucide React |
| 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
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
@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 | 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
9.1 CSS Custom Properties (Source of Truth)
/* tokens/colors.css */
:root {
--color-brand-500: #0EA5E9;
/* ... */
}
9.2 Tailwind Config Extension
// tailwind.config.ts
export default {
theme: {
extend: {
colors: {
brand: {
500: 'var(--color-brand-500)',
// ...
}
}
}
}
}
9.3 JavaScript/TypeScript Constants
// 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
Each component story must include:
- Default story — basic render with minimal props
- All Variants story — every visual variant displayed
- All States story — hover, focus, disabled, error, loading
- Responsive story — behavior at each breakpoint
- Accessibility story — keyboard navigation, screen reader notes
Component JSDoc minimum:
/**
* 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:
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
- 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:
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:
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):
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):
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):
<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:
// 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:
- Invoice Wizard (6-step) — Customer, Details, Line Items, Customization, Preview, Send
- Expense Form (dialog) — Amount, Category, Date, Vendor, Receipt, etc.
- Company Profile — All company settings
- Tax & Compliance — VAT settings
- Notification Preferences — Email/in-app notification toggles
- 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
{{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
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)
Library: {{TanStack Query v5}}
Configuration:
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:
// 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)
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:
// 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
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
// 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
Library: {{React Hook Form v7}}
Validation: {{Zod}}
Pattern:
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
| 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:
import { persist } from 'zustand/middleware';
export const usePrefsStore = create(
persist<PrefsState>(
(set) => ({ theme: 'system', /* ... */ }),
{ name: 'user-preferences' }
)
);
4. Caching Strategy
4.1 Optimistic Updates
// 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
Protocol: {{WebSocket | Server-Sent Events | None}}
Library: {{Socket.io | native WebSocket | @microsoft/signalr}}
Pattern — WebSocket to Query Cache:
// 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)
Approach: {{Dehydrate/Hydrate (TanStack Query) | getServerSideProps | prefetchQuery}}
// 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
| 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:
// Devtools enabled only in development
const devtools = process.env.NODE_ENV === 'development'
? (await import('zustand/middleware')).devtools
: (f: any) => f;
8. Performance Considerations
8.1 Selector Pattern (Zustand)
// 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, chartsinvoices/page.tsx— Invoice listinvoices/new/page.tsx— 6-step invoice wizardexpenses/page.tsx— Expense listpurchases/page.tsx— Alias to expensesbanking/page.tsx— Placeholderreports/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/
Layout:
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 methodslib/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
- No production mock data — Always flag mock data usage
- Design system tokens — Use tokens from tailwind.config.ts, NEVER hardcode colors
- Responsive — Mobile-first, test at all breakpoints
- Accessibility — Use shadcn/ui primitives (Radix UI), semantic HTML
- TypeScript strict — No
anytypes without explicit justification
API Integration (Future)
When backend ready:
- Create
lib/api.tswith 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
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
| 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
{{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
| 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
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
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
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
| 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
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: swapon 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
| Item | Decision |
|---|---|
| Library | `{{next-intl |
| 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 |
Translation workflow:
- Developer adds key with English string
TODO: extraction toolgenerates translation keys- Translator fills missing keys in Phrase/Lokalise/manual JSON
- CI validates no missing keys before deploy
10. Error Boundary Strategy
| 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
| 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
| 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
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 |