Frontend

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

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

/login — Login

/register — Registration

/dashboard — Main Dashboard

/accounts — Bank Accounts

/transactions — Transaction History

/scan — QR Scanner

/send — Send Money (Remittance)

/profile — User Profile

/withdrawal — Angrerett (Right of Withdrawal)


/complaints — Send Complaint


/privacy — Privacy Policy


/terms — Terms of Service


/fees — Fee Overview


/profile/personal — Personal Information


/profile/security — Security Settings


/profile/notifications — Notification Settings


/profile/language — Language Settings


/notifications — Notifications Center


/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.

Frontend Architecture

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:


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:

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.


Approval

Role Name Date Signature
Author
Frontend Lead
Design System Owner
Frontend Architecture

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:

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

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


5. Component Library

5.1 Custom Drop Components

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

5.2 shadcn/ui Primitive Components (Atoms)

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

5.3 Layout Components

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

6. Iconography Guidelines

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

Custom Drop Icons:

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

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


7. Motion & Animation Standards

7.1 Duration Tokens

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

7.2 Easing Tokens

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

7.3 Reduced Motion

@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
Frontend Architecture

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:

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

Auth flow:

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

Usage patterns:

// 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):


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:


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:


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:


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:


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

Fresh Data on Navigation

Future Optimization Path (if needed)

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

Approval

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

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

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


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


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

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:

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


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

8.2 Fonts

8.3 Icons


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:


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

2.2 Manual Testing

2.3 User Testing


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:

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:


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:


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:

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

8. Keyboard Navigation Map

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:


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:

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

Component JSDoc minimum:

/**
 * 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:

Add Customer Dialog:

Validation:


Step 2: Invoice Details

Fields:

Behavior:


Step 3: Line Items

Repeating Fields (Line Items):

Each line item contains:

Actions:

Totals Display (read-only):

Validation:


Step 4: Customization

Fields:

Behavior:


Step 5: Preview (Read-Only)

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

Preview Elements:

No validation. Step is purely visual review.


Step 6: Send/Save

Email Form:

Action Buttons:

Validation:


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

Form Actions

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:

Action:

Validation: None (no required fields enforced)


Tax & Compliance Form

Fields:

Compliance Reminders:

Action:

Validation: None


Notification Preferences

Email Notifications:

In-App Notifications:

Action:

Validation: None


Security Settings

Two-Factor Authentication:

Session Timeout:

Password Policy:

Actions:

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:


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:


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:

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:

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):

Library: Radix UI Combobox or react-select


Multi-Currency Conversion

Current State: User manually selects currency

Future Enhancement:


Summary

Current Forms:

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

Validation:

State Management:

Future (Phase 2):

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:

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:


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:

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

Pages (App Router)

All pages under app/(dashboard)/:

Components

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

Layout:

Design System

Embedded in tailwind.config.ts: 73 tokens

API Integration

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

State Management

Development Rules

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

API Integration (Future)

When backend ready:

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

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

8.2 Fonts

8.3 Icons

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:

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

10. Error Boundary Strategy

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