MotirBuilding in public
MOTIR · moooon
onMotir
You’re viewing a public project. Anyone can view it — no account needed. Sign in to submit, upvote, or comment on requests.View-only — you can’t edit work items
MOTIR-2306

(motir-core) Raw / Tier-0 RADIUS escapes from the shape-swap layer are repo-wide — route them, then GUARD the radius axis like `swapLayerLint` guards colour

Done
Description

Repo: motir-core. One PR. Surfaced while fixing MOTIR-2301 (the ChildList row's rounded-md px-2 py-2), which deliberately scoped the sweep out. Logged as its own card rather than absorbed (notes.html #27 / the drive-by-fix rule).

⚠️ RE-SCOPED to the RADIUS axis by THE REPLAN ACTION, 2026-08-07. This card originally also owned the own-box PADDING / HEIGHT axis and sized both from a one-directory sample. Measured on origin/main, that half is 447 literals in 190 files and — more decisively — has nowhere to go: --spacing-card-padding is 24px and 140 of 145 card surfaces pad at something else, so the instruction "route it to the token CLAUDE.md maps its surface to" is unsatisfiable until the token set grows a density scale. That axis now lives in MOTIR-2335, whose first subtask is that decision; the story is blocked_by this card because it extends the guard this card creates. Full evidence: notes.html #232 ("a sweep card whose routing instruction pointed at a destination that could not hold the values" — locate by title, the numbering shifts on append collisions), planning bug MOTIR-2337.

The defect class

CLAUDE.md's shape rule: a surface's own radius MUST name an element-semantic shape token — --radius-{btn,card,input,modal,badge,control,kbd} — because only those are overridden by a [data-style] block. A raw rounded-md, a bare rounded, or a generic rounded-(--radius-sm) does not participate in the style swap.

Why it stayed invisible

Measured on packages/design-system/theme.css: of the ten bare [data-style] token blocks, nine also override the Tier-0 --radius-xs/sm/md/lg/xl scale. So an escaped surface still moves when the style changes — just to the wrong value (the container radius instead of the control radius, differing under most styles). Only soft-playful leaves the generic scale alone, which is the one style where the escape is plainly visible. A visual spot-check does not find this class; the wrong-but-moving value looks fine. That is what makes a mechanical guard the deliverable, not a one-off sweep.

Inventory (measured origin/main @ 7ffa2ba4 — a sample of the shape, not the assertion)

62 offenders in 34 files. By token: rounded-(--radius-sm) ×18 · rounded-md ×15 · bare rounded ×15 · rounded-(--radius-xs) ×9 · rounded-none ×1 · rounded-lg ×1 · rounded-xl ×1 · rounded-[calc(var(--radius-btn)-2px)] ×2. Concentrations: the app shell's menus and switchers (ProjectSwitcher, WorkspaceSwitcher, UserMenu, SidebarHeader, ThemeToggle, CommandPaletteTrigger, CreateIssueButton — rows and <kbd> chips on --radius-sm/-xs), the work-item detail panels (AddLinkControl, ContentSectionCard, RelationshipsPanel, RemoveLinkButton, FieldCard, ParentBreadcrumb), the skeletons (IssueTreeSkeleton, BoardSkeleton, BacklogSkeleton — bare rounded), WorkflowEditor, LinkAddForm, CreateIssueLinksField, MarkdownEditor, TreeTable.

Do not treat that list as the specification — it goes stale on every merge. The guard IS the specification: run it, and fix what it names (notes.html's site-count-goes-stale lesson).

Three known allowlist candidates, each to be judged on its merits, not inherited:

  • app/tokens/page.tsx — the design-system specimen route, whose job is to render the Tier-0 scale as labelled swatches. The same exemption swapLayerLint grants it for --color-*.
  • packages/design-system/src/components/ui/Segmented.tsx and .../theme/AppearancePickers.tsxrounded-[calc(var(--radius-btn)-2px)], a value DERIVED from a semantic token so the inner pill fits its shell. It swaps correctly; it is not an escape.
  • app/(authed)/items/[key]/_components/AttachmentPreview.tsxrounded-none, if the intent is a deliberately square full-bleed image. Verify before allowlisting; a wrong reason is worse than a fixed site.

Acceptance criteria

  • A structural guard tests/theme/shapeSwapLint.test.ts exists, mirroring tests/theme/swapLayerLint.test.ts's exact shape: the same git ls-files tracked-source set, comments stripped before matching, and the offender list asserted EQUAL to an enumerated allowlist — so a fixed exception must be deleted from the test and a new violation cannot hide behind an old one.
  • Its source set is asserted non-empty and containing known files (swapLayerLint's SOURCES.length > 1000 + named-file checks), so it can never silently pass by scanning nothing (notes.html #195).
  • The radius check flags every radius utility that is NOT an element-semantic role: the raw scale (rounded-none|sm|md|lg|xl|2xl|3xl), a bare rounded, the generic rounded-(--radius-xs|sm|md|lg|xl), and an arbitrary rounded-[…] that does not derive from a --radius-* token. rounded-full and rounded-(--radius-pill) pass — a genuinely circular thing is not style-dependent.
  • Every site the guard flags is either routed to the role CLAUDE.md's radius-by-surface table maps its surface to, or carries an allowlist entry stating why it is legitimate. The allowlist entries are the ONLY exceptions; there is no third disposition.
  • The guard's comment states which axis it owns and which it does not — radius here, own-box padding / height deferred to MOTIR-2335 — so the next reader does not mistake its silence on padding for permission.
  • pnpm test tests/theme/shapeSwapLint.test.ts is green, and reverting any one migrated site turns it red (state the negative control you ran in the PR body).
  • The change is a token substitution, not a redesign: every migrated site keeps its surface's role, and any site whose default-style pixel value moves is named in the PR body with its before → after.

Context refs

  • motir-core/CLAUDE.md — "Shape (radius + spacing + sizing) flows through element-semantic shape tokens", with the radius-by-surface table this sweep routes to.
  • tests/theme/swapLayerLint.test.ts — the colour-axis guard this mirrors: the renderedSources() git ls-files set, stripComments, the exact-allowlist discipline, and the reason each is written that way.
  • packages/design-system/theme.css:159-174 — the Tier-0 radius scale and the seven element-semantic roles; :360+ — the ten [data-style] blocks, nine of which override the generic scale.
  • MOTIR-2301 (merged, PR #1900) — the one-row instance and the token pair a list row takes.
  • MOTIR-2335 — the own-box padding / height axis carved out of this card; it extends this card's guard file.
Status
Done
Type
Bug