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:
co-authored by
Jakob Wennberg
Claude Fable 5
parent
18c20e68e6
commit
9686b54b41
@@ -58,6 +58,14 @@
|
||||
* the four Skatteverket-bound org-number paths disagreed outright about
|
||||
* what "valid" meant, which is the kind of drift a customer only discovers
|
||||
* when a filing fails at the deadline. Tracked as a count.
|
||||
* 9. off-ladder-radius: a border-radius class outside the locked ladder
|
||||
* (pill / rounded-xl overlays / rounded-lg surfaces / rounded-sm leaves;
|
||||
* see .claude/rules/design.md). Before the 2026-08 migration the UI had
|
||||
* seven radii in circulation (4/5/6/8/12/16px + pill) and one toolbar row
|
||||
* could mix four of them. `rounded-md`, bare `rounded`, `rounded-2xl`+
|
||||
* and arbitrary `rounded-[Npx]` are dead vocabulary in app/ and
|
||||
* components/. No baseline: the count is 0, any new one is a hard
|
||||
* failure.
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/checks/no-new-antipatterns.mjs # check (CI)
|
||||
@@ -282,6 +290,59 @@ function countHandRolledInvariants() {
|
||||
return count
|
||||
}
|
||||
|
||||
// 9. off-ladder-radius. The radius ladder (.claude/rules/design.md) allows
|
||||
// exactly: rounded-full (interactive toolbar controls, chips, dots),
|
||||
// rounded-xl (page panel, dialogs, slide-overs, hero surfaces), rounded-lg
|
||||
// (cards, form fields, popover/menu content, bordered boxes), rounded-sm
|
||||
// (nested leaf elements), rounded-none, and directional variants of those.
|
||||
// Everything else is off-ladder. Bare `rounded` is banned as vocabulary: it
|
||||
// renders the same 4px as rounded-sm but hides from a rounded-sm grep.
|
||||
const OFF_LADDER_RADIUS_RES = [
|
||||
// rounded-md and any directional variant (rounded-t-md, rounded-bl-md, ...)
|
||||
/\brounded(?:-[trbl]{1,2})?-(?:md|2xl|3xl|4xl)\b/,
|
||||
// arbitrary radius values: rounded-[5px], rounded-t-[10px], ...
|
||||
/\brounded(?:-[trbl]{1,2})?-\[/,
|
||||
]
|
||||
|
||||
// Bare `rounded` as a class token (not rounded-*): renders the same 4px as
|
||||
// rounded-sm but hides from a rounded-sm grep. `rounded` is also a common
|
||||
// variable name and an ordinary English word, so this one only counts inside
|
||||
// a quoted string that looks like a Tailwind class list (contains at least
|
||||
// one other utility-class token).
|
||||
const BARE_ROUNDED_RE = /(?<![-\w])rounded(?![-\w])/
|
||||
const CLASS_LIST_HINT_RE =
|
||||
/(?:^|\s)(?:[a-z-]+:)*(?:flex|inline-flex|grid|hidden|absolute|relative|sticky|fixed|bg-\S|text-\S|border\b|border-\S|shadow\S*|p-\d|px-\S|py-\S|pl-\S|pr-\S|pt-\S|pb-\S|h-\S|w-\S|gap-\S|items-\S|justify-\S|font-\S|overflow-\S|transition\S*|animate-\S)/
|
||||
|
||||
function lineHasBareRoundedClass(line) {
|
||||
const strings = line.match(/"[^"]*"|'[^']*'|`[^`]*`/g)
|
||||
if (!strings) return false
|
||||
return strings.some(
|
||||
(s) => BARE_ROUNDED_RE.test(s) && CLASS_LIST_HINT_RE.test(s.slice(1, -1)),
|
||||
)
|
||||
}
|
||||
|
||||
/** Off-ladder border-radius classes in UI code. */
|
||||
function findOffLadderRadii() {
|
||||
const files = [
|
||||
...walk(path.join(ROOT, 'app'), ['.ts', '.tsx']),
|
||||
...walk(path.join(ROOT, 'components'), ['.ts', '.tsx']),
|
||||
]
|
||||
const findings = []
|
||||
for (const f of files) {
|
||||
const lines = fs.readFileSync(f, 'utf8').split('\n')
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i]
|
||||
// Prose mentions of "rounded" in comments are not class tokens.
|
||||
const trimmed = line.trim()
|
||||
if (trimmed.startsWith('//') || trimmed.startsWith('*') || trimmed.startsWith('/*')) continue
|
||||
if (OFF_LADDER_RADIUS_RES.some((re) => re.test(line)) || lineHasBareRoundedClass(line)) {
|
||||
findings.push(`${rel(f)}:${i + 1}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
return findings.sort()
|
||||
}
|
||||
|
||||
// Dependencies pinned to an EXACT version on purpose, because a bump broke prod
|
||||
// and must not silently return via `npm update`, a dependabot bump, or a manual
|
||||
// install. Any drift (in package.json OR the lockfile) fails CI. See DECISIONS.md.
|
||||
@@ -621,6 +682,7 @@ const current = {
|
||||
rawUserErrors: findRawUserErrors(),
|
||||
sekLabelledAmounts: findSekLabelledFxAmounts(ROOT),
|
||||
extensionRoutes: findExtensionRouteFindings(ROOT),
|
||||
offLadderRadii: findOffLadderRadii(),
|
||||
}
|
||||
|
||||
const isUpdate = process.argv.includes('--update')
|
||||
@@ -728,6 +790,21 @@ if (current.sekLabelledAmounts.length) {
|
||||
)
|
||||
}
|
||||
|
||||
// 1e1. off-ladder-radius: no baseline, the count is 0 after the 2026-08
|
||||
// migration and any new off-ladder radius class is a hard failure.
|
||||
if (current.offLadderRadii.length) {
|
||||
failed = true
|
||||
console.error(
|
||||
`\n✗ off-ladder-radius: ${current.offLadderRadii.length} border-radius class(es) outside the locked ladder:`,
|
||||
)
|
||||
current.offLadderRadii.forEach((f) => console.error(` ${f}`))
|
||||
console.error(
|
||||
' → use the radius ladder (.claude/rules/design.md): rounded-full for toolbar controls/chips,\n' +
|
||||
' rounded-xl for overlays, rounded-lg for cards/fields/menu content, rounded-sm for nested\n' +
|
||||
' leaves. rounded-md, bare `rounded`, rounded-2xl and rounded-[Npx] are dead vocabulary.',
|
||||
)
|
||||
}
|
||||
|
||||
// 1e2. hand-rolled-invariant: counted, may only go down.
|
||||
if (current.handRolledInvariants > (baseline.handRolledInvariants?.count ?? Infinity)) {
|
||||
failed = true
|
||||
@@ -839,5 +916,5 @@ if (failed) {
|
||||
process.exit(1)
|
||||
}
|
||||
console.log(
|
||||
`\n✓ Antipattern guard passed (raw-route-auth: ${current.rawRouteAuth.length}, naive-ore-round: ${current.naiveOreRound}, hand-rolled-invariant: ${current.handRolledInvariants}, ledger-scanning-report: ${current.ledgerScanningReports.length}, direct-jel-insert: 0, pinned-dep: 0, raw-user-error: 0, sek-labelled-amount: 0, cross-extension-import: 0, ungated-extension-route: ${current.extensionRoutes.ungated.length}/${UNGATED_EXTENSION_ROUTES.size} allowlisted).`,
|
||||
`\n✓ Antipattern guard passed (raw-route-auth: ${current.rawRouteAuth.length}, naive-ore-round: ${current.naiveOreRound}, hand-rolled-invariant: ${current.handRolledInvariants}, ledger-scanning-report: ${current.ledgerScanningReports.length}, direct-jel-insert: 0, pinned-dep: 0, raw-user-error: 0, sek-labelled-amount: 0, off-ladder-radius: 0, cross-extension-import: 0, ungated-extension-route: ${current.extensionRoutes.ungated.length}/${UNGATED_EXTENSION_ROUTES.size} allowlisted).`,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user