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
+1 -1
View File
@@ -389,7 +389,7 @@ export function AgentMemoryPanel() {
onClick={() => patch(row.id, { is_pinned: !row.is_pinned })}
disabled={isBusy}
className={cn(
'mt-0.5 shrink-0 rounded-md p-1.5 transition-colors duration-150',
'mt-0.5 shrink-0 rounded-sm p-1.5 transition-colors duration-150',
row.is_pinned
? 'bg-secondary text-foreground'
: 'text-muted-foreground hover:bg-secondary/60 hover:text-foreground',
+1 -1
View File
@@ -50,7 +50,7 @@ const PROSE =
'prose-h1:text-lg prose-h2:text-base prose-h3:text-sm prose-p:my-2 prose-p:leading-6 ' +
'prose-strong:font-semibold prose-strong:text-foreground prose-ul:my-2 prose-li:my-0.5 ' +
'prose-a:text-foreground prose-a:underline prose-a:underline-offset-2 ' +
'prose-code:bg-secondary prose-code:rounded prose-code:px-1 prose-code:py-0.5 prose-code:text-xs ' +
'prose-code:bg-secondary prose-code:rounded-sm prose-code:px-1 prose-code:py-0.5 prose-code:text-xs ' +
'prose-code:before:content-none prose-code:after:content-none ' +
'prose-table:my-2 prose-table:text-xs [&_table]:w-full ' +
'[&_th]:border-b [&_th]:border-border [&_th]:py-1.5 [&_th]:px-2 [&_th]:text-left [&_th]:font-medium ' +
+6 -6
View File
@@ -192,7 +192,7 @@ function CopyBlock({ text, copyAriaLabel }: { text: string; copyAriaLabel: strin
return (
<div className="relative group">
<pre className="select-all rounded-md bg-muted p-4 pr-12 text-xs font-mono overflow-x-auto whitespace-pre-wrap break-all">
<pre className="select-all rounded-lg bg-muted p-4 pr-12 text-xs font-mono overflow-x-auto whitespace-pre-wrap break-all">
{text}
</pre>
<Button
@@ -240,7 +240,7 @@ function ScopeCard({
return (
<label
className={cn(
'flex min-h-[68px] cursor-pointer flex-col gap-1 rounded-md border p-2 transition-colors',
'flex min-h-[68px] cursor-pointer flex-col gap-1 rounded-lg border p-2 transition-colors',
checked
? 'border-border bg-secondary'
: 'border-border hover:bg-secondary/60'
@@ -580,7 +580,7 @@ export function ApiKeysPanel() {
{/* Create key dialog */}
<Dialog open={showCreateDialog} onOpenChange={setShowCreateDialog}>
<DialogContent className="max-w-[calc(100vw-2rem)] rounded-2xl p-4 sm:max-w-3xl sm:p-6">
<DialogContent className="max-w-[calc(100vw-2rem)] rounded-xl p-4 sm:max-w-3xl sm:p-6">
<DialogHeader>
<DialogTitle>{t('create_dialog_title')}</DialogTitle>
<DialogDescription>
@@ -600,7 +600,7 @@ export function ApiKeysPanel() {
</div>
<div className="space-y-2">
<Label>{t('mode_label')}</Label>
<div className="inline-flex rounded-md border p-0.5" role="radiogroup" aria-label={t('mode_label')}>
<div className="inline-flex rounded-full border p-0.5" role="radiogroup" aria-label={t('mode_label')}>
{(['live', 'test'] as const).map((m) => (
<button
key={m}
@@ -609,7 +609,7 @@ export function ApiKeysPanel() {
aria-checked={newKeyMode === m}
onClick={() => setNewKeyMode(m)}
className={cn(
'rounded-[5px] px-3 py-1.5 text-xs transition-colors',
'rounded-full px-3 py-1.5 text-xs transition-colors',
newKeyMode === m
? 'bg-secondary text-foreground'
: 'text-muted-foreground hover:text-foreground',
@@ -683,7 +683,7 @@ export function ApiKeysPanel() {
{hasSodConflict && (
<div
role="alert"
className="flex items-start gap-2 rounded-md border border-border bg-muted/30 p-3 text-xs text-foreground"
className="flex items-start gap-2 rounded-lg border border-border bg-muted/30 p-3 text-xs text-foreground"
>
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-warning" />
<p className="leading-snug">{t('sod_warning')}</p>
+2 -2
View File
@@ -277,7 +277,7 @@ export function BackupDownloadForm() {
id="backup-period"
value={selectedPeriodId}
onChange={(e) => setSelectedPeriodId(e.target.value)}
className="flex h-10 w-full max-w-xs rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
className="flex h-10 w-full max-w-xs rounded-lg border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
disabled={periods === null || periods.length === 0}
>
{/* The confirmed-empty option only after a confirmed empty
@@ -308,7 +308,7 @@ export function BackupDownloadForm() {
/>
</div>
<div className="rounded-md border border-border bg-muted/30 p-3 text-sm">
<div className="rounded-lg border border-border bg-muted/30 p-3 text-sm">
<div className="flex items-center gap-2 text-muted-foreground">
<Info className="h-3.5 w-3.5" />
{isLoadingEstimate ? (
+2 -2
View File
@@ -155,7 +155,7 @@ export function BankNameCombobox({ defaultValue = '', value: controlledValue, on
ref={listRef}
id="bank-name-listbox"
role="listbox"
className="absolute z-50 mt-1 max-h-56 w-full overflow-auto rounded-md border border-border bg-popover shadow-md"
className="absolute z-50 mt-1 max-h-56 w-full overflow-auto rounded-lg border border-border bg-popover shadow-md"
>
{filtered.map((bank, i) => (
<li
@@ -176,7 +176,7 @@ export function BankNameCombobox({ defaultValue = '', value: controlledValue, on
<img
src={bank.logo}
alt=""
className="h-5 w-5 flex-shrink-0 rounded object-contain"
className="h-5 w-5 flex-shrink-0 rounded-sm object-contain"
/>
) : (
<svg
@@ -150,7 +150,7 @@ export function InvoiceEmailTextsSettings({ settings, onUpdate }: InvoiceEmailTe
<p>
{t('placeholders_help')}{' '}
{INVOICE_EMAIL_PLACEHOLDER_KEYS.map((key) => (
<code key={key} className="mr-1 rounded bg-muted px-1 text-xs">{`{${key}}`}</code>
<code key={key} className="mr-1 rounded-sm bg-muted px-1 text-xs">{`{${key}}`}</code>
))}
</p>
<p>{t('firstname_note')}</p>
+1 -1
View File
@@ -177,7 +177,7 @@ export function OAuthClientsPanel() {
<DialogDescription>
{t.rich('register_dialog_description', {
bold: (chunks) => <span className="font-medium">{chunks}</span>,
code: (chunks) => <code className="rounded bg-muted px-1 py-0.5 font-mono text-[11px]">{chunks}</code>,
code: (chunks) => <code className="rounded-sm bg-muted px-1 py-0.5 font-mono text-[11px]">{chunks}</code>,
})}
</DialogDescription>
</DialogHeader>
+1 -1
View File
@@ -242,7 +242,7 @@ export function SettingsSeg<T extends string>({
aria-pressed={o.value === value}
onClick={() => onChange(o.value)}
className={cn(
'rounded-md px-3 py-1 text-xs transition-colors duration-150 disabled:cursor-not-allowed disabled:opacity-60',
'rounded-sm px-3 py-1 text-xs transition-colors duration-150 disabled:cursor-not-allowed disabled:opacity-60',
o.value === value
? 'border border-border bg-card font-medium text-foreground'
: 'text-muted-foreground hover:text-foreground',
+1 -1
View File
@@ -237,7 +237,7 @@ export function TemplateForm({
{lines.map((line, i) => {
const showRatioInput = showRatio && line.type === 'business'
return (
<div key={i} className="rounded-md border border-border p-2 space-y-1.5">
<div key={i} className="rounded-lg border border-border p-2 space-y-1.5">
<div className="flex items-center gap-2">
<Input
value={line.account}
@@ -115,7 +115,7 @@ export function BankingSettingsContent() {
setIsAccessDenied(false)
setShowHbPoaHint(false)
}}
className="shrink-0 rounded-md p-1 text-muted-foreground transition-colors duration-150 hover:text-foreground"
className="shrink-0 rounded-sm p-1 text-muted-foreground transition-colors duration-150 hover:text-foreground"
aria-label={t('dismiss_aria')}
>
<span className="text-lg leading-none">&times;</span>