diff --git a/CLAUDE.md b/CLAUDE.md index 844596d8..6f94f202 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -25,70 +25,7 @@ npm run setup:extensions # Regenerate extension registry from extensions.config. --- -## Architecture - -``` -app/ - (auth)/ Login, register, auth callback, MFA enroll/verify, password reset - (onboarding)/ 6-step setup wizard - (dashboard)/ Authenticated routes (invoices, customers, transactions, - bookkeeping, reports, suppliers, supplier-invoices, - receipts, deadlines, settings, help, import, extensions) - (public)/ Public invoice action links (no auth), DPA, privacy policy - api/ API routes organized by domain - -components/ - ui/ shadcn/ui primitives - bookkeeping/ Chart of accounts, journal entry form/list/review - chat/ ChatWidget, ChatPanel - extensions/ Extension marketplace UI, general/ workspace components - transactions/ Transaction list, categorization, booking, VAT treatment - (+ customers/, dashboard/, deadlines/, import/, invoices/, - onboarding/, reports/, settings/, suppliers/) - -extensions/general/ Config-driven extensions (see Extension System below) - -lib/ - api/ Zod validation schemas (schemas.ts) and helpers (validate.ts) - bookkeeping/ Core journal entry engine and all entry generators - engine.ts Draft/commit workflow, balance validation, voucher numbering - invoice-entries.ts Sales invoice journal entries (per-line VAT rates) - transaction-entries.ts Bank transaction journal entries - supplier-invoice-entries.ts Purchase invoice journal entries - category-mapping.ts Category-to-BAS-account mapping - mapping-engine.ts Rule-based auto-categorization (MCC, merchant patterns) - bas-reference.ts BAS account catalog (~180 accounts) - handlers/ Booking handler functions - core/ - bookkeeping/ Period service, storno reversal, year-end closing - documents/ Document archive (upload, versioning, SHA-256 integrity) - audit/ Audit trail service - tax/ Tax code service - email/ EmailService interface + NoopEmailService default - events/ Event bus (bus.ts, types.ts) — singleton, core emits, extensions subscribe - extensions/ Extension system (loader, registry, types, hooks, context-factory) - ai-consent.ts AI consent gate for extensions using third-party AI providers - _generated/ Code-generated files (DO NOT EDIT) - import/ SIE parser, bank file parser (10 Swedish bank formats) - invoices/ VAT rules, invoice matching, PDF template, reminders - reconciliation/ Bank reconciliation engine (4-pass matching) - reports/ Financial reports (trial balance, income statement, balance sheet, - VAT declaration, SIE export, general ledger, NE-bilaga, INK2, SRU export, - full archive ZIP export) - auth/ MFA helpers (mfa.ts) and API auth guard (require-auth.ts) - supabase/ Client setup (client.ts = browser, server.ts = server) - tax/ Tax calculations, deadlines, Swedish holidays - vat/ VIES validation, moms box mapping - init.ts Extension loader (idempotent, called by API routes) - -types/index.ts Canonical type definitions (single source of truth) -types/chat.ts Chat types -tests/helpers.ts Mock factories and fixture builders -supabase/migrations/ SQL migration files (63 files) -extensions.config.json Extension opt-in configuration -``` - -### Key Relationships +## Key Architectural Relationships - **All journal entry creation** routes through `lib/bookkeeping/engine.ts` via `createJournalEntry()`. - **API routes** that emit events must call `ensureInitialized()` (from `lib/init.ts`) at module level. @@ -96,47 +33,19 @@ extensions.config.json Extension opt-in configuration - **Supabase clients**: browser (`lib/supabase/client.ts`), server with cookies (`createClient()` from `server.ts`), service role (`createServiceClient()`). - **Extension system**: Opt-in via `extensions.config.json`. Core builds and runs with zero extensions. - **NE-bilaga, INK2 declaration, SRU export, and full archive export** are core reports (in `lib/reports/`), not extensions. -- **AI consent gate** (`lib/extensions/ai-consent.ts`): AI extensions (`receipt-ocr`, `ai-categorization`, `ai-chat`) require user consent before API calls. The extension catch-all route checks consent and returns `403 AI_CONSENT_REQUIRED` if missing. +- **AI consent gate** (`lib/extensions/ai-consent.ts`): AI extensions (`receipt-ocr`, `ai-categorization`, `ai-chat`) require user consent before API calls. Returns `403 AI_CONSENT_REQUIRED` if missing. +- **Types**: All shared types in `types/index.ts` (single source of truth). Import via `import type { T } from '@/types'`. Event types live in `lib/events/types.ts`. --- ## Authentication -Authentication uses Supabase Auth with **email+password** (primary) and **magic link** (fallback). MFA via TOTP is supported. +Supabase Auth with **email+password** (primary) and **magic link** (fallback). MFA via TOTP is supported. -### Auth Flow +MFA is enforced **application-side** (middleware + API routes), **not** in RLS policies. Controlled by two env vars: -1. **Login** (`/login`) — Email+password with magic link toggle. After password login, checks MFA status. -2. **Register** (`/register`) — Email+password signup. Supabase sends confirmation email. Strong password required (8+ chars, uppercase, lowercase, number, special character). -3. **Password reset** (`/reset-password`) — Via recovery link from login page. -4. **MFA verify** (`/mfa/verify`) — 6-digit TOTP code input after login when MFA is enrolled. -5. **MFA enroll** (`/mfa/enroll`) — QR code + manual secret for authenticator app setup. - -### MFA Enforcement - -MFA is enforced **application-side** (middleware + API routes), **not** in RLS policies. This is controlled by two env vars: - -| Env Var | Hosted (Vercel) | Self-hosted (Docker) | -|---------|-----------------|----------------------| -| `NEXT_PUBLIC_REQUIRE_MFA` | `true` | `false` (default) | -| `NEXT_PUBLIC_SELF_HOSTED` | not set | `true` | - -- **Self-hosted**: MFA is never enforced (`NEXT_PUBLIC_SELF_HOSTED=true` overrides). Users can still enable it voluntarily via Settings → Säkerhet. -- **Hosted**: When `NEXT_PUBLIC_REQUIRE_MFA=true`, middleware redirects users without MFA to `/mfa/enroll` after onboarding, and users with MFA to `/mfa/verify` until AAL2 is achieved. - -### Key Files - -| File | Purpose | -|------|---------| -| `lib/auth/mfa.ts` | `isMfaRequired()` — checks env vars | -| `lib/auth/require-auth.ts` | `requireAuth()` — API route guard (auth + MFA) | -| `lib/supabase/middleware.ts` | Session refresh + MFA enforcement | -| `app/(auth)/auth/callback/route.ts` | PKCE/magic link callback with MFA redirect | -| `components/settings/SecuritySettings.tsx` | Password change + MFA enable/disable UI | - -### Supabase MFA Setup - -Enable TOTP in Supabase Dashboard → `Authentication` → `Multi-Factor Authentication`. No database migration needed — Supabase manages `auth.mfa_factors`, `auth.mfa_challenges`, and `auth.mfa_amr_claims` internally. +- `NEXT_PUBLIC_SELF_HOSTED=true` → MFA never enforced (users can enable voluntarily) +- `NEXT_PUBLIC_REQUIRE_MFA=true` (hosted/Vercel) → middleware redirects to `/mfa/enroll` or `/mfa/verify` until AAL2 --- @@ -144,23 +53,7 @@ Enable TOTP in Supabase Dashboard → `Authentication` → `Multi-Factor Authent The engine (`lib/bookkeeping/engine.ts`) is the most critical system. All accounting flows route through it. -### Journal Entry Lifecycle - -1. **`createDraftEntry(userId, input)`** — Creates `status: 'draft'`, `voucher_number: 0`. Validates balance. Emits `journal_entry.drafted`. -2. **`commitEntry(userId, entryId)`** — Assigns voucher number via DB RPC (concurrent-safe). Sets `status: 'posted'`. Emits `journal_entry.committed`. -3. **`createJournalEntry(userId, input)`** — Draft + commit in one call. This is what all entry generators use. -4. **`reverseEntry(userId, entryId)`** — Storno reversal: swaps debit/credit, links via `reverses_id`/`reversed_by_id`. - -### Entry Generators - -| Function | File | Purpose | -|----------|------|---------| -| `createInvoiceJournalEntry()` | `invoice-entries.ts` | Debit 1510, Credit 30xx + 26xx VAT (per-line VAT rates) | -| `createInvoicePaymentJournalEntry()` | `invoice-entries.ts` | Debit 1930, Credit 1510 | -| `createCreditNoteJournalEntry()` | `invoice-entries.ts` | Reverses original invoice entry | -| `createTransactionJournalEntry()` | `transaction-entries.ts` | Maps bank transactions via MappingResult | -| `createSupplierInvoiceRegistrationEntry()` | `supplier-invoice-entries.ts` | Debit expense + 2641, Credit 2440 | -| `createSupplierInvoicePaymentEntry()` | `supplier-invoice-entries.ts` | Debit 2440, Credit 1930 | +**Lifecycle**: `createDraftEntry()` → `commitEntry()` (assigns voucher number via DB RPC). Convenience: `createJournalEntry()` does both in one call. Reversal via `reverseEntry()` (storno). Correction via `correctEntry()` in `lib/core/bookkeeping/storno-service.ts`. ### Key BAS Accounts @@ -183,16 +76,7 @@ The `VatDeclarationRutor` type maps to the Swedish tax authority's momsdeklarati - **Ruta 48**: Ingående moms — input VAT (from 2641/2645) - **Ruta 49**: Moms att betala/återfå = (ruta 10 + 11 + 12) - ruta 48 -The `VatDeclaration.breakdown.invoices` also includes `base25`/`base12`/`base6` for per-rate revenue breakdown in the UI. - -### Bank Reconciliation - -`lib/reconciliation/bank-reconciliation.ts` — 4-pass matching on account 1930: - -1. `auto_exact` (0.95) — exact amount + exact date -2. `auto_reference` (0.90) — exact amount + reference match -3. `auto_date_range` (0.85) — exact amount + date ±3 days -4. `auto_fuzzy` (0.75) — fuzzy amount (±0.01) + exact date +`VatDeclaration.breakdown.invoices` also includes `base25`/`base12`/`base6` for per-rate revenue breakdown in the UI. --- @@ -215,150 +99,19 @@ These rules exist for legal compliance, enforced by database triggers. **Never v ## Extension System -Extensions are opt-in plugins controlled by `extensions.config.json`. Core builds and runs with zero extensions. +Extensions are opt-in plugins in `extensions/general//`, controlled by `extensions.config.json`. Core builds and runs with zero extensions. `npm run setup:extensions` generates static imports in `lib/extensions/_generated/` (runs automatically via `predev`/`prebuild`). Extensions **cannot** use dynamic imports (Next.js bundling). -### How It Works +**API routes**: Dispatched via catch-all at `app/api/extensions/ext/[...path]/route.ts`. URL: `/api/extensions/ext/{extensionId}/{routePath}`. Path params extracted as `_paramName` search params. -1. Each extension has a `manifest.json` in `extensions/general//`. -2. `extensions.config.json` lists enabled extension IDs. -3. `npm run setup:extensions` generates files in `lib/extensions/_generated/` (static imports, workspace map, definitions). -4. `predev`/`prebuild` hooks run this automatically. +**Service provider patterns**: +- *Interface registration* (email): Core defines noop default in `lib/email/service.ts`, extension calls `registerEmailService()`, core uses `getEmailService()`. +- *Services record* (ai-categorization): Extension exposes via `services` property, core looks up via `extensionRegistry.get('id')?.services?.method(...)`. -### Available Extensions - -| Extension | Category | Env Vars Required | -|-----------|----------|-------------------| -| `receipt-ocr` | import | `ANTHROPIC_API_KEY` | -| `ai-categorization` | operations | `ANTHROPIC_API_KEY`, `OPENAI_API_KEY` | -| `ai-chat` | operations | `ANTHROPIC_API_KEY`, `OPENAI_API_KEY` | -| `invoice-inbox` | import | `ANTHROPIC_API_KEY` | -| `calendar` | operations | — | -| `enable-banking` | import | Enable Banking keys | -| `email` | operations | `RESEND_API_KEY`, `RESEND_FROM_EMAIL` | - -### Creating Extensions - -```bash -npx tsx scripts/create-extension.ts --name my-ext --sector general --category operations --description "..." -``` - -Then add `"my-ext"` to `extensions.config.json` and run `npm run setup:extensions`. - -**Constraints**: Extensions **cannot** use dynamic imports (Next.js bundling). - -### Extension Interface - -```typescript -interface Extension { - id: string; name: string; version: string; sector?: SectorSlug - // All optional surfaces: - apiRoutes?: ApiRouteDefinition[] - eventHandlers?: ExtensionEventHandler[] - services?: Record Promise> - sidebarItems?: SidebarItem[] - mappingRuleTypes?: MappingRuleTypeDefinition[] - settingsPanel?: SettingsPanelDefinition - onInstall?(ctx: ExtensionContext): Promise - onUninstall?(ctx: ExtensionContext): Promise -} -``` - -### Extension Context - -Handlers receive an `ExtensionContext` with: `userId`, `extensionId`, `supabase` (pre-authenticated), `emit()`, `settings` (JSONB key-value), `storage` (Supabase Storage), `log` (scoped logger), `services`. - -### Extension API Routes - -Dispatched via catch-all at `app/api/extensions/ext/[...path]/route.ts`. -URL scheme: `/api/extensions/ext/{extensionId}/{routePath}` - -Path params extracted as `_paramName` search params (e.g., `/:id` → `searchParams.get('_id')`). - -### Service Provider Patterns - -**Interface registration** (email pattern): Core defines interface with noop default in `lib/email/service.ts`. Extension calls `registerEmailService()` at load time. Core uses `getEmailService()` — degrades gracefully. - -**Services record** (ai-categorization pattern): Extension exposes functions via `services` property. Core looks up via registry: `extensionRegistry.get('ai-categorization')?.services?.findSimilarTemplates(...)`. - -### Event Types (lib/events/types.ts) - -`journal_entry.drafted/committed/corrected` | `document.uploaded` | `invoice.created/sent` | `credit_note.created` | `transaction.synced/categorized/reconciled` | `period.locked/year_closed` | `customer.created` | `receipt.extracted/matched/confirmed` | `supplier_invoice.received/extracted/confirmed` +**Creating extensions**: `npx tsx scripts/create-extension.ts --name my-ext --sector general --category operations --description "..."`, then add to `extensions.config.json`. --- -## Testing - -**Framework**: Vitest 4, `globals: true`, `environment: 'node'`. Tests colocated in `__tests__/` directories. - -**Scope**: Business logic in `lib/` and API routes in `app/api/`. No component or E2E tests. - -**Test helpers** (`tests/helpers.ts`): -- `createMockSupabase()` / `createQueuedMockSupabase()` — Supabase mocks -- `createMockRequest()`, `parseJsonResponse()`, `createMockRouteParams()` — API route testing -- Fixture factories: `makeTransaction()`, `makeJournalEntry()`, `makeInvoice()`, `makeCustomer()`, `makeSupplier()`, `makeFiscalPeriod()`, `makeReceipt()`, `makeDocumentAttachment()`, `makeCompanySettings()`, etc. - -**Patterns**: -- Always mock `@/lib/supabase/server` -- Use `vi.clearAllMocks()` and `eventBus.clear()` in `beforeEach` -- API route tests: mock `@/lib/init` and lib functions, test auth (401), validation (400), not found (404), errors (500), happy path - ---- - -## Database & Migrations - -**Location**: `supabase/migrations/` — 63 files. Early migrations use sequential numbering (`20240101000001`–`20240101000038`), later ones use real timestamps (`20260223150836`+). -**Next migration**: Use `mcp__plugin_supabase_supabase__apply_migration` which assigns timestamps automatically. - -### Placeholder Migrations - -Some migrations are no-op placeholders to preserve the numbering sequence: -- **012** (`tax_codes_placeholder`) — Planned but never deployed. The system operates without the `tax_codes` table. -- **023** (`document_version_chain_placeholder`) — Planned but never deployed. Document versioning columns/functions do not exist in production. - -### Migration Rules - -1. **Always enable RLS** and create `SELECT/INSERT/UPDATE` policies using `auth.uid() = user_id` -2. **Always add `updated_at` trigger** using `update_updated_at_column()` -3. **UUID primary keys**: `DEFAULT uuid_generate_v4()` -4. **User ownership**: `user_id UUID REFERENCES auth.users ON DELETE CASCADE NOT NULL` -5. **Never modify existing migrations** — create new ones -6. **Never modify enforcement triggers** (migration 017) — legally required -7. **Apply via Supabase MCP tool**: `mcp__plugin_supabase_supabase__apply_migration` - -### Key Enforcement Triggers (migration 017) - -- `enforce_journal_entry_immutability` — Blocks edits/deletes on posted/reversed entries -- `enforce_journal_entry_line_immutability` — Blocks line modifications on committed entries -- `enforce_period_lock` — Blocks writes to closed/locked fiscal periods -- `block_document_deletion` — Prevents deletion of documents linked to committed entries -- `enforce_retention_journal_entries` — 7-year retention enforcement -- `set_committed_at` — Auto-sets timestamp on draft-to-posted transition -- `calculate_retention_expiry` — Auto-sets `retention_expires_at = period_end + 7 years` - -### Recent Migrations - -- **`20260223150836_invoice_inbox`** — Invoice inbox table with document type classification, AI extraction, supplier/transaction matching, and receipt linking. -- **`20260224101905_booking_template_embeddings`** — Booking templates with AI embeddings for suggestion matching. -- **`20260224132419_user_description_matching`** — User description matching for transaction categorization. -- **`20260224165254_prevent_overlapping_fiscal_periods`** — Exclusion constraint preventing overlapping fiscal periods per user. -- **`20260224190818_enforce_fiscal_period_month_boundaries`** — Ensures fiscal periods start/end on month boundaries. -- **`20260225103139_full_bas_2026`** — Full BAS 2026 account catalog, K2-excluded flag, and SRU code backfill. -- **`20260226120553_expand_account_type_untaxed_reserves`** — Adds `untaxed_reserves` to `chart_of_accounts.account_type` CHECK constraint for BAS 21xx accounts (obeskattade reserver). -- **`20260304191528_set_search_path_on_functions`** — Pins `search_path = public` on all 24 custom functions to prevent search_path injection. -- **`20260306084837_invoice_delivery_note_sequences`** — Adds delivery note number sequence and `generate_invoice_number`/`generate_delivery_note_number` RPC functions. - ---- - -## Type System - -- All shared types live in `types/index.ts` — this is the single source of truth -- Import via `import type { TypeName } from '@/types'` -- When adding new domain types, add them to `types/index.ts` -- Event types are the exception — they live in `lib/events/types.ts` (since they reference domain types) - ---- - -## API Route Patterns +## API Route Pattern ```typescript import { createClient } from '@/lib/supabase/server' @@ -388,6 +141,32 @@ export async function POST(request: Request) { --- +## Testing + +**Framework**: Vitest 4, `globals: true`, `environment: 'node'`. Tests colocated in `__tests__/` directories. Scope: business logic in `lib/` and API routes in `app/api/`. No component or E2E tests. + +**Test helpers** (`tests/helpers.ts`): `createMockSupabase()`, `createQueuedMockSupabase()`, `createMockRequest()`, `parseJsonResponse()`, `createMockRouteParams()`, and fixture factories (`makeTransaction()`, `makeJournalEntry()`, `makeInvoice()`, `makeCustomer()`, `makeSupplier()`, `makeSupplierInvoice()`, `makeFiscalPeriod()`, `makeReceipt()`, `makeDocumentAttachment()`, `makeCompanySettings()`, `makeInvoiceInboxItem()`, `makeExtensionToggle()`, etc.). + +**Patterns**: Always mock `@/lib/supabase/server`. Use `vi.clearAllMocks()` and `eventBus.clear()` in `beforeEach`. API route tests: mock `@/lib/init` and lib functions, test auth (401), validation (400), not found (404), errors (500), happy path. + +--- + +## Database & Migrations + +**Location**: `supabase/migrations/` — 65 files. Early migrations use sequential numbering (`20240101000001`–`20240101000038`), later ones use real timestamps. + +### Migration Rules + +1. **Always enable RLS** and create `SELECT/INSERT/UPDATE` policies using `auth.uid() = user_id` +2. **Always add `updated_at` trigger** using `update_updated_at_column()` +3. **UUID primary keys**: `DEFAULT uuid_generate_v4()` +4. **User ownership**: `user_id UUID REFERENCES auth.users ON DELETE CASCADE NOT NULL` +5. **Never modify existing migrations** — create new ones +6. **Never modify enforcement triggers** (migration 017) — legally required +7. **Apply via Supabase MCP tool**: `mcp__plugin_supabase_supabase__apply_migration` + +--- + ## Skills, Git & CI **Skills**: Always use `/frontend-design` for new UI. Use `langchain` for AI features. Use `vercel:deploy` for deployment. @@ -400,7 +179,7 @@ export async function POST(request: Request) { ## Deployment -Hosted on **Vercel**. Cron jobs in `vercel.json` (banking sync daily 05:00, deadlines 06:00, reminders 08:00, push notifications 09:00, tax deadlines yearly Jan 2, document verify weekly Sunday 03:00). +Hosted on **Vercel**. Cron jobs defined in `vercel.json` (banking sync, deadlines, reminders, tax deadlines, document verification, sandbox cleanup). **Core env vars**: `NEXT_PUBLIC_SUPABASE_URL`, `NEXT_PUBLIC_SUPABASE_ANON_KEY`, `SUPABASE_SERVICE_ROLE_KEY`, `NEXT_PUBLIC_APP_URL`, `CRON_SECRET`. **Auth env vars**: `NEXT_PUBLIC_REQUIRE_MFA` (set `true` on hosted), `NEXT_PUBLIC_SELF_HOSTED` (set `true` for Docker). Extension env vars only needed when that extension is enabled. @@ -415,39 +194,28 @@ Never create a NUL/nul file: \gnubok\NUL Swedish sole traders (enskild firma) and small business owners (aktiebolag) who need to manage their own bookkeeping. They are not accountants — they are professionals (consultants, freelancers, shop owners) who want to stay compliant without hiring one. They use gnubok in short, focused sessions: sending an invoice, categorizing bank transactions, filing a VAT declaration. Speed and clarity matter — every second spent in the app is a second away from their real work. -### Brand Personality +### Brand & Aesthetic -**Minimal. Sharp. Efficient.** +**Minimal. Sharp. Efficient.** The interface should feel like a well-made instrument: considered, quiet, and confident. Reference: Mercury (banking). Anti-reference: enterprise software (SAP/Oracle density). -gnubok is the tool that makes accounting feel handled. It doesn't try to be fun — it earns trust through precision, clarity, and speed. The interface should feel like a well-made instrument: considered, quiet, and confident. Think Mercury's calm authority applied to Swedish bookkeeping. - -### Aesthetic Direction - -- **Palette**: Grayscale foundation with restrained semantic colors — sage green (success/balance), terracotta (errors/overdue), ochre (warnings/attention). No loud brand color. The absence of color *is* the brand. -- **Typography**: Fraunces (serif) for display headings — adds warmth and distinction to an otherwise minimal interface. Geist (sans) for body — clean, modern, excellent for tabular data. Tabular numbers everywhere financial data appears. -- **Surfaces**: White/near-white cards on light gray backgrounds. Subtle borders (60% opacity). Soft shadows that suggest depth without drama. Dark mode follows the same restraint. -- **Spacing**: Generous whitespace. Let content breathe. Dense data (tables, ledgers) should use tighter spacing but never feel cramped. -- **Motion**: Subtle and purposeful. Stagger animations for list entry, spring easing for interactive feedback. Never decorative — always communicates state change. -- **Icons**: Lucide — consistent 15px in navigation, slightly larger in empty states and actions. -- **Reference**: Mercury (banking) — calm, trustworthy, clean data presentation, financial precision. -- **Anti-reference**: Enterprise software (SAP/Oracle density, overwhelming dashboards with 50 widgets, toolbar soup). +- **Palette**: Grayscale foundation with restrained semantic colors — sage green (success/balance), terracotta (errors/overdue), ochre (warnings/attention). No loud brand color. +- **Typography**: Fraunces (serif) for display headings, Geist (sans) for body. Tabular numbers everywhere financial data appears. +- **Surfaces**: White/near-white cards on light gray backgrounds. Subtle borders (60% opacity). Soft shadows. Dark mode follows the same restraint. +- **Spacing**: Generous whitespace. Dense data (tables, ledgers) uses tighter spacing but never feels cramped. +- **Motion**: Subtle and purposeful. Stagger animations for list entry, spring easing for feedback. Never decorative. +- **Icons**: Lucide — 15px in navigation, slightly larger in empty states. ### Design Principles -1. **Clarity over cleverness.** Every element should be immediately understandable. Accounting is complex enough — the UI must never add confusion. Use familiar patterns, clear labels (in Swedish), and obvious hierarchy. - -2. **Earned minimalism.** Remove everything that doesn't serve the user's task. But don't strip away context that prevents errors — in accounting, a missing detail can mean a compliance violation. Minimal means *considered*, not *sparse*. - -3. **Numbers are first-class.** Financial data deserves typographic care: tabular-nums, proper alignment, adequate contrast, clear positive/negative distinction. A journal entry grid should feel as precise as a printed ledger. - -4. **Trust through consistency.** Same patterns, same spacing, same behavior everywhere. Buttons work the same way. Cards look the same way. When the user learns one screen, they've learned them all. - -5. **Speed is a feature.** Fast load, fast interaction, fast comprehension. Users are here to get a job done and leave. Optimize for the 90-second session: open app, categorize three transactions, close app. +1. **Clarity over cleverness.** Every element immediately understandable. Clear labels (in Swedish), obvious hierarchy. +2. **Earned minimalism.** Remove what doesn't serve the task, but don't strip context that prevents compliance errors. +3. **Numbers are first-class.** Tabular-nums, proper alignment, adequate contrast, clear positive/negative distinction. +4. **Trust through consistency.** Same patterns, spacing, and behavior everywhere. +5. **Speed is a feature.** Optimize for the 90-second session. ### Accessibility -- Target: **WCAG AA** compliance -- Minimum contrast ratio: 4.5:1 for text, 3:1 for UI components -- All interactive elements keyboard-navigable with visible focus rings -- Respect `prefers-reduced-motion` for users who disable animations -- Color is never the sole indicator of state — always pair with icons, text, or shape +- **WCAG AA**: 4.5:1 text contrast, 3:1 UI components +- Keyboard-navigable with visible focus rings +- Respect `prefers-reduced-motion` +- Color never sole indicator of state — always pair with icons, text, or shape