refactor(design): lock the border-radius ladder, one radius per role (#1607)

Seven radii were in circulation (4/5/6/8/12/16px + pill) with no rule for
which went where; one toolbar row on /transactions mixed four shape
languages. This locks a 4-tier ladder (design.md convention 16):

- pill: interactive toolbar controls (buttons, chips, pickers, segmented
  controls, toolbar search, count nubs)
- rounded-xl (12px): overlay tier: page panel, dialogs, slide-overs
- rounded-lg (8px): cards, form fields, popover/menu content, boxes
- rounded-sm (4px): nested leaves (menu items, checkboxes, kbd/code nubs)

Changes:
- New SegmentedControl primitive (pill-in-pill tablist, h-8) replaces the
  hand-rolled bg-muted/70 tablist copied across 11 files
- New ToolbarSearch primitive (pill, h-8) adopted on 9 page toolbars;
  dialog/picker searches keep the rounded-lg Input
- dialog.tsx 8px -> 12px, matching SettingsModal/slide-over/CommandPalette
- ContextPicker chips at the shared h-8 toolbar height
- ~300 rounded-md / bare rounded call sites remapped by role; auth icon
  tiles and the mobile nav sheet come down from 16px to 12px
- rounded-md, bare rounded, rounded-2xl and rounded-[Npx] are dead
  vocabulary, enforced by a new off-ladder-radius check in check:guards

Verified: lint 0 errors, 14422 unit tests pass, check:guards green, tsc
clean on all changed files, sandbox screenshots of transactions/
bookkeeping/granskning toolbars and the Ny verifikation dialog.

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-08-14 08:55:37 +02:00
committed by GitHub
co-authored by Jakob Wennberg Claude Fable 5
parent 18c20e68e6
commit 9686b54b41
130 changed files with 633 additions and 652 deletions
@@ -75,7 +75,7 @@ export default function BankSyncSinceLastVisit() {
}
return (
<div className="inline-flex items-center gap-2 rounded-md border border-border bg-secondary px-2.5 py-1 text-xs text-foreground">
<div className="inline-flex items-center gap-2 rounded-full border border-border bg-secondary px-2.5 py-1 text-xs text-foreground">
<span>
{count === 1
? t('bank_sync_new_since_last_visit_one')
@@ -110,7 +110,7 @@ export default function BankSyncStatusChip() {
return (
<Link
href="/settings/banking"
className="inline-flex items-center gap-1.5 rounded-md border border-destructive/40 bg-destructive/5 px-2.5 py-1 text-xs text-destructive transition-colors hover:bg-destructive/10"
className="inline-flex items-center gap-1.5 rounded-full border border-destructive/40 bg-destructive/5 px-2.5 py-1 text-xs text-destructive transition-colors hover:bg-destructive/10"
>
<AlertTriangle className="h-3.5 w-3.5" />
<span>
@@ -128,7 +128,7 @@ export default function BankSyncStatusChip() {
return (
<Link
href="/settings/banking"
className="inline-flex items-center gap-1.5 rounded-md border border-border bg-muted/30 px-2.5 py-1 text-xs text-attn transition-colors hover:bg-muted/50"
className="inline-flex items-center gap-1.5 rounded-full border border-border bg-muted/30 px-2.5 py-1 text-xs text-attn transition-colors hover:bg-muted/50"
>
<AlertTriangle className="h-3.5 w-3.5" />
<span>
@@ -143,7 +143,7 @@ export default function BankSyncStatusChip() {
return (
<Tooltip>
<TooltipTrigger asChild>
<span className="inline-flex cursor-help items-center gap-1.5 rounded-md border border-border bg-muted/30 px-2.5 py-1 text-xs text-muted-foreground">
<span className="inline-flex cursor-help items-center gap-1.5 rounded-full border border-border bg-muted/30 px-2.5 py-1 text-xs text-muted-foreground">
<RefreshCw className="h-3.5 w-3.5" />
<span>
{t('bank_sync_auto_nightly')}
@@ -212,7 +212,7 @@ export default function DuplicateBookingDialog({
{canIgnore && <> {t('dialog_duplicate_ignore_hint')}</>}
</p>
{candidate && (
<div className="space-y-1 rounded-md border bg-muted/30 p-3">
<div className="space-y-1 rounded-lg border bg-muted/30 p-3">
<div className="flex items-center justify-between gap-3 text-sm">
<span className="font-medium">
{candidate.voucher_label
@@ -879,7 +879,7 @@ export default function InvoiceMatchDialog({
{/* Side toggle: segmented control. Clicking either
button picks that side; the amount stays the
same. */}
<div className="inline-flex rounded-md border bg-background overflow-hidden h-9">
<div className="inline-flex rounded-lg border bg-background overflow-hidden h-9">
<button
type="button"
onClick={() => updateEditLine(i, { side: 'debit' })}
@@ -159,7 +159,7 @@ export default function TransactionAttachDocumentDialog({
disabled={isAttaching}
/>
{pickedDoc && (
<div className="flex items-center gap-2 text-sm py-1.5 px-2 rounded bg-muted/50">
<div className="flex items-center gap-2 text-sm py-1.5 px-2 rounded-sm bg-muted/50">
<FileText className="h-4 w-4 text-muted-foreground shrink-0" />
<span className="truncate flex-1">
{pickedDoc.supplier_name ?? pickedDoc.file_name}
@@ -311,7 +311,7 @@ export default function TransactionBookingDialog({
{pickedInboxDocs.map((doc) => (
<div
key={doc.document_id}
className="flex items-center gap-2 text-sm py-1.5 px-2 rounded bg-muted/50"
className="flex items-center gap-2 text-sm py-1.5 px-2 rounded-sm bg-muted/50"
>
<FileText className="h-4 w-4 text-muted-foreground shrink-0" />
<span className="truncate flex-1">
@@ -6,6 +6,7 @@ import Link from 'next/link'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { DataListEmpty } from '@/components/ui/data-list'
import { SegmentedControl } from '@/components/ui/segmented-control'
import { TH_CLASS, TD_CLASS, QUIET_LINK_CLASS } from '@/components/ui/dry-table'
import {
DropdownMenu,
@@ -148,25 +149,11 @@ export default function TransactionHistoryList({
<div className="space-y-4">
{/* Business/private seg; the source chip lives in the page toolbar. */}
<div className="flex flex-wrap items-center gap-2">
<div className="inline-flex shrink-0 gap-0.5 rounded-lg bg-muted/70 p-[3px]" role="tablist">
{FILTERS.map(({ key, labelKey }) => (
<button
key={key}
type="button"
role="tab"
aria-selected={filter === key}
onClick={() => setFilter(key)}
className={cn(
'rounded-md px-3.5 py-[5px] text-[12.5px] transition-colors duration-150',
filter === key
? 'border border-border bg-card font-medium text-foreground'
: 'text-muted-foreground hover:text-foreground',
)}
>
{t(labelKey)}
</button>
))}
</div>
<SegmentedControl
value={filter}
onChange={setFilter}
options={FILTERS.map(({ key, labelKey }) => ({ value: key, label: t(labelKey) }))}
/>
</div>
{filtered.length === 0 ? (
@@ -46,7 +46,7 @@ export default function VatTreatmentSelect<T extends VatSelectValue>({
key={opt.value}
value={opt.value}
className={cn(
'relative flex w-full cursor-default select-none items-start rounded-md py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-secondary focus:text-secondary-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50'
'relative flex w-full cursor-default select-none items-start rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-secondary focus:text-secondary-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50'
)}
>
<span className="absolute left-2 top-2 flex h-3.5 w-3.5 items-center justify-center">