Files
accounted/.claude/rules/design.md
T
f266c386f3 chore: repo-wide bloat sweep, remove dead code and fold duplicate helpers (#2150)
* chore: repo-wide bloat sweep, remove dead code and fold duplicate helpers

Remove 33 dead files, ~270 unreferenced exports/types, 13 dead i18n
namespaces and 4 unused dependencies; fold byte-identical helper copies
into one canonical home each (lib/utils chunk/sleep/utcDateStamp,
lib/dates/iso, lib/invariants/uuid, lib/xml/escape, lib/reports/sru/format,
lib/pdf/number-text, lib/browser/panel-request, lib/api/v1/body +
v1ValidationError rolled out to ~55 v1 routes, booking-template schemas).

No behaviour change: v1 bodies and status codes, MCP tool schemas, DB
writes and money math are untouched. Naive ore rounding was deliberately
not swapped for roundOre; see DECISIONS.md 2026-09-02 for the full list
of things left alone on purpose.

tsc, lint, 19588 unit tests and check:guards green; antipattern baseline
ratcheted (naive-ore-round 622 -> 620, hand-rolled-invariant 115 -> 113).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(transactions): import RawTransaction from @/types after the ingest re-export removal

CI's type ratchet (check:types, full tsconfig) caught the one test file
that still imported the type through lib/transactions/ingest.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 11:51:16 +02:00

16 KiB

paths
paths
app/**
components/**

Design Context & Design System

Always use the /frontend-design skill for new UI. The conventions below are locked: deviating from them on existing pages is a regression.

Users

Swedish sole traders (enskild firma) and small business owners (aktiebolag) who 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 Accounted in short, focused sessions: sending an invoice, categorizing bank transactions, filing a VAT declaration. Speed and clarity matter.

Brand & Aesthetic

Editorial monochrome. Paper-white surfaces, hairline borders, serif headlines. The interface should feel like a well-made instrument, considered, quiet, confident. Anti-references: enterprise software (SAP/Oracle density), neon SaaS coldness.

  • Palette: Achromatic foundation. Pure white background, warm beige (40 11% 89%) for chips / active sidebar / hover / secondary buttons. Achromatic primary (no cool tint). Semantic colors (--success sage, --warning ochre, --destructive terracotta) exist but are data-only: they appear in charts and financial numbers (positive/negative deltas), never as chrome backgrounds. In chrome, only --destructive survives.
  • Typography: Hedvig Letters Serif for display headings, Geist (sans) for body, forms, and tables. Hedvig is single-weight (400): do not apply font-medium to display text; its natural high-contrast strokes carry the weight. Tabular numbers everywhere financial data appears.
  • Surfaces: The page itself is a rounded panel (12px) floating on a warm-toned frame (--frame); the sidebar sits borderless on the frame. Cards sit flat on the page: no shadow, full-opacity hairline border (border-border), rounded-lg (8px). Card background matches page background; the border carries hierarchy. Dark mode drops the warm tint from secondary for a pure-gray mood shift; light mode keeps the beige.
  • Spacing: Generous whitespace. Dense data (tables, ledgers) uses tighter spacing but never feels cramped.
  • Motion: Functional, not decorative. No press-scale, no hover-lift, no spring overshoot. Hover state is a flat background shift (bg-secondary/60). transition-colors duration-150 is the default. Stagger animations on list entry are fine. Respect prefers-reduced-motion (already wired).
  • Icons: Lucide: 15px in navigation, slightly larger in empty states.

Design Principles

  1. Clarity over cleverness: Swedish labels, obvious hierarchy.
  2. Earned minimalism: remove what doesn't serve the task, keep compliance context.
  3. Numbers are first-class: tabular-nums, alignment, positive/negative clarity.
  4. Trust through consistency.
  5. Speed is a feature: optimize for the 90-second session.

Accessibility

WCAG AA (4.5:1 text, 3:1 UI). Keyboard-navigable + visible focus rings. Respect prefers-reduced-motion. Color never sole state indicator. Touch targets ≥40px (44px for mobile-critical). Icon-only buttons need aria-label.

Locked UI-migration conventions

Decided during the 2026-07 concept work (dev_docs/ui_migration_plan.md); they apply system-wide and override anything below that conflicts.

  1. Frame layout. The page is a rounded panel (12px) on a warm-toned frame (--frame). The panel keeps --background; the sidebar is borderless on the frame.
  2. Page title is exactly 24px/32px Hedvig Letters Serif (text-2xl leading-8 via PageHeader).
  3. Buttons are pills. Radius 99px, default padding 7px 16px, 13px text. Set once in components/ui/button.tsx, app-wide, never per page.
  4. Table rows are one line. Secondary info (descriptions, OCR, roles) belongs in the detail view or a click-popup, never as sub-rows in lists.
  5. Chips mark exceptions. Normal states render as muted text; Badge only when the row deviates. Same chip on every row means the chip is wrong.
  6. Attention is one ochre sentence, not a banner: the .attn pattern (12.5px, --warning tone, single line, optionally with an embedded action link). Max one per page. Addendum 2026-08-19: a page may show at most one global notice line sourced from lib/notices (highest priority wins, additional active notices collapse behind a quiet "+N till" inline expander) plus at most one page-domain attn line.
  7. Help text lives behind a "?" right after the H1: a small (17px) circular button opening a popover anchored at the button. No instructional copy in the page flow.
  8. One context picker per page, far right in the toolbar: fiscal year or account/source as a chip-dropdown with a check on the active choice. A chip that looks like a picker must be a picker.
  9. The primary action lives in the page header, right side. Multiple create paths collapse into a split button whose caret menu remembers the last-used mode (persisted in user_preferences, not localStorage).
  10. Confirm up front, don't comment afterwards. Actions that post or send open a small confirm dialog describing the outcome ("Bokförs som verifikat A-217 ...") instead of writing outcome text into the page afterwards.
  11. Content lands with stagger. .stagger-enter is the standard entry for list/table content, on server render and on client-fetch completion alike.
  12. Status colors are data, not chrome: sage/ochre/terracotta only in numbers, exception chips and .attn.
  13. Overlays: centered modal for create/confirm (template, assistant, confirmations, settings); right slide-over for reviewing an object (Granskning detail). Both with veil, Esc, and click-outside.
  14. Manual base, AI as opt-in. Base flows work without AI; AI entry points are clearly labeled discrete choices (e.g. "Skapa med assistenten") and no AI suggestion posts without Granskning.
  15. Settings speak "Fönster" (founder-chosen 2026-07-25, applies to every settings tab). Settings content is flat hairline rows, never boxed cards: SettingsGroup / SettingsRow / SettingsSectionHeader from components/settings/SettingsRows.tsx. Toggles are switches, not checkboxes. Saving is a sticky bar that fades in only when the form is dirty (components/settings/SettingsFormWrapper.tsx); no always-visible save button. The settings surface is a 920x680 modal (components/settings/SettingsModal.tsx); switching tabs inside it updates the URL shallowly via history.replaceState (SettingsRail.tsx), never router.replace, which would remount the modal.
  16. One radius per role (the radius ladder, 2026-08-13). Four tiers, tied to what a thing IS: pills for interactive toolbar controls (buttons, chips, pickers, segmented controls, toolbar search, count badges), rounded-xl (12px) for the overlay/panel tier (page panel, dialogs, slide-overs, command palette), rounded-lg (8px) for surfaces on the panel (cards, form fields, textareas, popover/dropdown/menu content, bordered boxes), rounded-sm (4px) for leaf elements nested inside 8px surfaces (menu items, checkboxes, kbd/code nubs, skeleton text lines). Toolbar controls share one height: h-8. rounded-md, bare rounded, rounded-2xl and arbitrary rounded-[Npx] are dead vocabulary, enforced by check:guards (off-ladder-radius). Nesting is concentric: pill-in-pill by construction, rounded-sm items inside rounded-lg content with ~4px inset.

Design System Tokens

Spacing scale. Only use Tailwind values 1, 2, 3, 4, 6, 8, 10, 12. Forbidden: 2.5, 5, hardcoded pixels in page logic.

Token Tailwind Use for
4 1 icon padding
8 2 tight inline gaps
12 3 dense list rows, badge gaps
16 4 default form / control / grid gap
24 6 card padding default (p-6)
32 8 between page sections (space-y-8 on page root)
40 10 hero spacing
48 12 top of page after header

Compact metric cards (e.g. dashboard tiles, salary KPI row) use p-4. Detail cards use p-6. Never mix p-5.

Radius ladder (convention 16; enforced by check:guards off-ladder-radius). Radius communicates role, and every role has exactly one:

Tier Class Use for
pill rounded-full Buttons, chips, badges, context pickers, segmented controls, toolbar search, count nubs, dots, avatars
12px rounded-xl Page panel, dialogs, slide-overs, command palette, hero surfaces
8px rounded-lg Cards, form inputs, textareas, popover/dropdown/menu content, bordered boxes, toasts
4px rounded-sm Menu items, checkboxes, kbd/code/account-number nubs, skeleton text lines, small nested leaves

Forbidden radii: rounded-md, bare rounded, rounded-2xl+, rounded-[Npx]. A search field is a pill in a toolbar (ToolbarSearch) and rounded-lg in a form or dialog (Input): the row it sits in decides, and mixing shapes in one row is the bug the ladder exists to prevent.

Layout (frame layout).

  • The dashboard wrapper is bg-frame (--frame: 40 18% 96% light, 0 0% 5% dark). <main> is the page panel: bg-background rounded-xl border border-border, 10px margin against the frame, own inner scroll (md:h-[calc(100vh-20px)] md:overflow-y-auto). Defined once as MAIN_PANEL_CLASS in app/(dashboard)/layout.tsx; never restyle per page.
  • The panel is the desktop scroll container: position: sticky binds to it automatically; never assume window scroll on dashboard pages. Scroll reset on navigation lives in MainContainer.
  • Sidebar width: md:w-64 (256px), borderless and transparent on the frame. Panel offset: md:ml-64.
  • Main container: max-w-5xl mx-auto px-5 py-8 md:px-8 md:py-10 (via components/dashboard/MainContainer.tsx).
  • Page root: <div className="space-y-8">.
  • Mobile keeps the pre-frame layout: full-width document flow, bottom nav; the panel styles are md:-gated.

Primitives: always use these, don't hand-roll.

Need Component Notes
Page title + action components/ui/page-header.tsx PageHeader Use this, not bespoke <h1> + <p> blocks. Drop the description prop when it just paraphrases the title.
Data table, page-level list components/ui/dry-table.tsx TH_CLASS / TD_CLASS on a plain <table className="w-full border-collapse text-[13px]">, rows hover:bg-secondary/35 The concept list table: borderless, straight on the panel, 13px rows, hairline heads. This is what every migrated list page uses. Add tabular-nums to numeric cells. Hover-revealed row controls use HOVER_REVEAL_CLASS from the same file, never a hand-rolled opacity-0 group-hover:opacity-100 (coarse pointers never hover, so the control would be unreachable on touch).
Data table, dialog or report view components/ui/table.tsx Table / TableHeader / TableHead / TableRow / TableCell Header style is baked in: text-[11px] font-medium uppercase tracking-wider text-muted-foreground. Wrap in <CardContent className="p-0"> when the table is a card's primary content. TableCell is px-4 py-3 on text-sm, so a page-level list built from this primitive comes out ~15% taller with a different hover tint: use the dry-table row above instead.
Status indicator components/ui/badge.tsx <Badge variant> Chips mark exceptions only: normal states (Aktiv, Bokförd, Betald-i-tid) render as muted text (text-muted-foreground text-xs); Badge is reserved for rows that deviate (Utkast, Förfallen, Ej bokförd). A table where every row carries the same chip is wrong. Variants: default / secondary / success / warning / destructive / outline. Never use raw Tailwind colors (bg-blue-100, bg-emerald-500/10, etc.) for status. Map status → variant via a small Record per feature.
No-data state components/ui/empty-state.tsx EmptyState Don't hand-roll <div className="flex flex-col items-center py-12">…</div>. Preset variants exist (EmptyCustomers, EmptyByraClients).
Loading placeholder components/ui/skeleton.tsx <Skeleton> Don't hand-roll bg-muted rounded animate-pulse divs.
Inline help / formulas components/ui/info-tooltip.tsx InfoTooltip Hover-revealed; don't use always-visible info buttons.
View/mode switcher in a toolbar components/ui/segmented-control.tsx SegmentedControl Pill-in-pill tablist at the shared h-8 toolbar height; options take an optional count for the standard count chip. Never hand-roll the bg-muted/70 tablist div.
Search in a page toolbar components/ui/toolbar-search.tsx ToolbarSearch Pill search at h-8, matching the chips and pickers beside it. Searches inside dialogs/pickers keep the regular Input.
Fiscal year picker components/common/FyPicker.tsx The chip-dropdown context picker of convention 8 (wraps ContextPicker). FiscalYearSelector is the legacy pre-frame control: don't add new uses. Never use a raw <select> for fiscal periods.
Settings rows / save components/settings/SettingsRows.tsx, SettingsFormWrapper.tsx Fönster language (convention 15): flat hairline rows, dirty-only sticky save bar. Don't hand-roll settings cards or per-field save buttons.

Tabular display rules.

  • All financial values get tabular-nums.
  • Dates in tables: tabular-nums for fixed width.
  • Right-align numeric columns (text-right).
  • For group bands inside tables (Resultatrapport-style): <tr className="bg-muted/30"><td colSpan={n} className="px-4 py-2 text-[12px] font-semibold text-muted-foreground">{label}</td></tr>.

Date formatting. Two helpers in lib/utils.ts:

  • formatDate(x) → 2026-05-11 (ISO yyyy-MM-dd). Use for accounting data: transaction dates, invoice dates, payment dates, voucher dates. Aligns in tables, matches SIE/BFL convention.
  • formatDateLong(x) → 11 maj 2026 (Swedish long form). Use for metadata: when something was created, linked, verified, expires. Settings panels and audit displays.

Never render raw {x.invoice_date} directly: always route through formatDate() for code consistency.

Currency. formatCurrency(n, currency?) from lib/utils.ts. Default SEK.

Typography.

  • Page title: use PageHeader (renders font-display text-2xl leading-8 tracking-tight, exactly 24px/32px, locked). Do not hand-roll an <h1>.
  • Card title: <CardTitle className="text-base"> for sections, default for primary cards. The primitive already drops font-medium: do not add it back.
  • Section divider header inside a page: <h2 className="text-sm font-medium uppercase tracking-wider text-muted-foreground">.
  • Headline number: font-display text-xl tabular-nums. No font-medium: Hedvig's natural weight carries the gravitas.
  • Display font (font-display, Hedvig Letters Serif) reserved for h1/h2/h3 and primary financial numbers. If a specific font-display numeral reads weak inside a compact metric card, override that call site with font-sans tabular-nums (Geist), better legibility on small numerals.

Forbidden / dead patterns.

  • Page descriptions that paraphrase the page title (e.g. <PageHeader title="Fakturor" description="Hantera dina fakturor">) → drop the description.
  • Two different status indicators on the same element (e.g. colored card border and Badge for status) → pick one (prefer Badge).
  • Mobile-specific <select> duplicating desktop tabs in code: use a single Tabs primitive or a single grouped Select.
  • Hand-rolled icon buttons smaller than h-10 w-10. Use shadcn Button size="icon".
  • Color-coded status using full-rainbow Tailwind palette (bg-amber-100, bg-emerald-500/10, etc.). Use Badge variants tied to the brand palette.
  • shadow-sm / shadow-md / shadow-lg on cards, buttons, or list items. The aesthetic is flat-with-hairlines: surfaces use border-border, not elevation. Shadows survive only on dialogs/popovers/dropdowns (anything that overlays the page).
  • active:scale-[...] on buttons. Buttons do not bounce.
  • bg-gradient-to-* on page or card backgrounds. Flat surfaces only.
  • font-medium on display elements (font-display, h1/h2/h3, CardTitle, PageHeader title). Hedvig is single-weight by design.
  • rounded-xl (12px) on cards. Cards are rounded-lg (8px). rounded-xl is the overlay/panel tier: page panel, dialogs, slide-overs, hero surfaces.
  • Off-ladder radii: rounded-md, bare rounded, rounded-2xl, rounded-[Npx]. See the radius ladder above; check:guards fails on any of them in app/ or components/.
  • Overriding the Button pill radius per call site (rounded-lg, rounded-md on a <Button>). Buttons are pills app-wide; the radius lives in components/ui/button.tsx alone.
  • Opacity-suffixed border classes (border-border/30, border-border/60) on cards and primary surfaces. Use full-opacity border-border: the new border token is calibrated for that.