Skip to content

chore: inline css -> tailwind, font tweaks (sizing, spacing, face, line height)#1878

Merged
jonathanlab merged 10 commits intomainfrom
default-font
Apr 24, 2026
Merged

chore: inline css -> tailwind, font tweaks (sizing, spacing, face, line height)#1878
jonathanlab merged 10 commits intomainfrom
default-font

Conversation

@corywatilo
Copy link
Copy Markdown
Contributor

@corywatilo corywatilo commented Apr 24, 2026

Two big things on an initial styling pass:

  1. Moved most styles from inline CSS to Tailwind. Needed to do this to have much more control over font sizes and things.
  2. Adjusted sizing of a bunch of little things (nav, descriptions, paragraph line heights, padding in main column)

Aside from a little bit of right-sizing (labels, paragraph treatment, etc), it shouldn't look like much has changed... which is intentional. This was mostly a chore to get stuff in a place they'll be easier to tweak in the coming weeks.

Note: This was a moderately-moderated Claude task. It was intentionally pretty conservative so it shouldn't cause any visual regressions.

Before / after

(The after has more padding in the main column on the left/right sides of the main description.)

2026-04-24 01 22 58

I also asked it to add some rules to CLAUDE.md to follow some best practices around this stuff.

Original plan and summary of output is below, for those interested.


Inline Styles Migration & Line Height Tightening

Scope

  • ~145 files in apps/code/src/renderer/ contain style={{}} (~600+ instances)
  • Hybrid execution: codemod the mechanical patterns, hand-edit ~8-10 hard files
  • Static only: leave dynamic values (width: someNumber, transform: \…${x}px…``), library config (CodeMirror/xterm theme objects), and complex computed cases alone — convert only what's truly static
  • Tighten leading globally: extend the List.tsx change (leading-5leading-snug) across the codebase via a Tailwind v4 theme override + a sweep of arbitrary text sizes

Phase 1 — Global line-height tightening

Two-pronged so it works for both named utilities (text-sm, text-base) and arbitrary sizes (text-[13px]).

1a. Override Tailwind v4 font-size token line-heights in globals.css

Tailwind v4 ships paired font-size + line-height tokens. Override the line-height side so every <element className="text-sm"> automatically gets the tighter leading. In apps/code/src/renderer/styles/globals.css, add a @theme block:

@theme {
  /* Tighten the leading paired with each named font-size utility.
   * Body sizes use snug (1.375), heading sizes stay tight (1.25).
   * This auto-applies to <p className="text-sm"> etc. without requiring
   * a leading-* class at every call site. */
  --text-xs--line-height: calc(0.75rem * 1.375);   /* 12 → 16.5px */
  --text-sm--line-height: calc(0.875rem * 1.375);  /* 14 → 19.25px */
  --text-base--line-height: calc(1rem * 1.375);    /* 16 → 22px */
  --text-lg--line-height: calc(1.125rem * 1.25);   /* 18 → 22.5px */
  --text-xl--line-height: calc(1.25rem * 1.25);    /* 20 → 25px */
  --text-2xl--line-height: calc(1.5rem * 1.25);    /* 24 → 30px (matches existing leading-tight usage) */
}

1b. Sweep text-[Npx] leading-5text-[Npx]

text-[13px] etc. don't pair with theme tokens, so each call site needs an explicit leading. Sweep across the codebase:

  • text-[13px] leading-5text-[13px] (~50 files, all from the size="1" codemod)
  • text-[12px] leading-5text-[12px] (handful)
  • text-[11px] leading-5text-[11px] (handful)
  • Leave leading-5 standalone (without text-[Npx]) alone — those have intentional 20px values
  • Leave the leading-tight / leading-relaxed / leading-[1.35] etc. cases alone — those are explicit overrides

1c. Update List.tsx sizeStyles

Now that named utilities ship with the right defaults, simplify:

  • "2": "text-sm""text-sm" (theme provides snug)
  • "3": "text-base""text-base" (theme provides snug)

Phase 2 — Inline-style codemod

Add apps/code/scripts/migrate-inline-styles.ts (next to the existing migrate-radix-text-to-tailwind.ts) using ts-morph. The codemod walks every style={{...}} JSX attribute, examines each property, and:

  1. Convert known static properties to Tailwind classes via a mapping table
  2. Remove the converted properties from the style object literal
  3. Delete the entire style={...} attribute if empty after removal
  4. Merge new classes into existing className (handle StringLiteral, TemplateLiteral, JsxExpression — same logic as the previous codemod)
  5. Skip the property (and log) if the value is non-literal (variable, expression, ternary, function call), or if the property isn't in the mapping table

Mapping table (a non-exhaustive set, focused on what's actually used)

Conversions only apply when the property value is a literal (string or number) matching a recognized pattern:

Inline Tailwind
color: "var(--gray-N)" text-(--gray-N) (Tailwind v4 CSS-var shorthand)
color: "var(--<scale>-<N>)" (red, green, blue, accent, …) text-(--<scale>-<N>)
backgroundColor: "var(--gray-N)" bg-(--gray-N) (and other scales)
padding: "var(--space-N)" p-{1,2,3,4,6,8,10,12,16} (Radix space-N → Tailwind, see scale below)
paddingTop/Right/Bottom/Left (var) pt/pr/pb/pl-N
padding: <Npx> or <N> p-[Npx] arbitrary
margin/marginTop/... (var or px) same idea,m-N
flex: 1 flex-1
flexShrink: 0 shrink-0
flexGrow: 0 / 1 grow-0 / grow
minWidth: 0 min-w-0
minHeight: 0 min-h-0
width: "100%" w-full
height: "100%" h-full
overflowY: "auto" / overflowX: "auto" / overflow: "auto" overflow-y-auto etc.
display: "flex" / "inline-flex" / "none" / "block" flex / inline-flex / hidden / block
cursor: "pointer" / "default" / "help" / "not-allowed" cursor-pointer etc.
textAlign: "left" / "center" / "right" text-left/center/right
textTransform: "uppercase" / "lowercase" uppercase / lowercase
fontWeight: 400 / 500 / 600 / 700 font-normal / font-medium / font-semibold / font-bold
opacity: 0.5 / 0.6 / 1 opacity-50 / opacity-60 / opacity-100
userSelect: "none" select-none
pointerEvents: "none" pointer-events-none
position: "absolute" / "relative" / "fixed" / "sticky" absolute / relative / fixed / sticky
borderRadius: "var(--radius-N)" rounded-(--radius-N)
border: "1px solid var(--gray-N)" border border-(--gray-N)
borderTop/Bottom/...: "1px solid var(--gray-N)" border-t border-t-(--gray-N) etc.
whiteSpace: "nowrap" / "pre" / "pre-wrap" whitespace-nowrap/pre/pre-wrap
wordBreak: "break-word" / "break-all" break-words / break-all
boxSizing: "border-box" box-border
listStyleType: "disc" / "decimal" list-disc / list-decimal

Radix space → Tailwind spacing mapping

Radix var px Tailwind Match?
--space-1 4 1 exact
--space-2 8 2 exact
--space-3 12 3 exact
--space-4 16 4 exact
--space-5 24 6 exact
--space-6 32 8 exact
--space-7 40 10 exact
--space-8 48 12 exact
--space-9 64 16 exact

What the codemod will NOT touch

  • Style object spreads (style={{ ...style, foo: 1 }}) — leave as-is
  • Conditional values (color: cond ? "x" : "y") — leave as-is unless trivially literal
  • Computed values (width: \${n}px`, transform: ...`) — leave as-is
  • fontFamily — leave alone (deferred; mostly already handled or library-specific)
  • fontSize — already migrated in the previous pass
  • lineHeight — already migrated in the previous pass
  • Files under node_modules/, tests/, scripts/
  • Library config files (editorTheme.ts, useCodePreviewExtensions.ts, TerminalManager.ts)

Codemod flow

flowchart TD
  Walk[Walk every JsxAttribute name='style'] --> Object{ObjectLiteralExpression?}
  Object -->|no| Skip
  Object -->|yes| ForEachProp[For each PropertyAssignment]
  ForEachProp --> Lookup{Property in MAPPING_TABLE?}
  Lookup -->|no| KeepProp[Keep property in style]
  Lookup -->|yes| Literal{Value is literal?}
  Literal -->|no| KeepProp
  Literal -->|yes| AddClass[Add Tailwind class to pending list]
  AddClass --> RemoveProp[Remove property from style]
  RemoveProp --> NextProp
  KeepProp --> NextProp
  NextProp --> Done{All props done?}
  Done -->|no| ForEachProp
  Done -->|yes| MergeClasses[Merge pending classes into className]
  MergeClasses --> CheckEmpty{style now empty?}
  CheckEmpty -->|yes| RemoveStyleAttr[Delete style attribute]
  CheckEmpty -->|no| Save
  RemoveStyleAttr --> Save[Save file]
Loading

After codemod

  • Run biome check --write --unsafe on touched files (sorts merged Tailwind classes)
  • Run tsc --noEmit to confirm no type regressions

Phase 3 — Hand-edit the hard files

The codemod's skip log will list every property it couldn't convert. For these ~8-10 files, hand-edit the parts that the codemod skipped where conversion is still safe:

File Approach
InboxEmptyStates.tsx Convert numeric image widths andmeta.color ternaries that the codemod skips. Static parts get codemodded automatically.
SignalSourceToggles.tsx statusInfo.color patterns → CSS-var-driven className using style={{ "--bg": color }} + bg-(--bg). Or accept staying inline since color is data-driven.
McpInstalledRail.tsx gridTemplateColumnsgrid-cols-[28px_1fr_auto] (static). Pulse-dot dynamic color stays inline.
TreeDirectoryRow.tsx Depth-basedpaddingLeft: depth * Nstyle={{ paddingLeft: depth * 12 }} stays (dynamic). Caret rotate stays.
OptionRow.tsx Background ternaries → conditional className. Margin ternaries → conditional className.
ReviewShell.tsx Static panel chrome (border, background) gets codemodded; dynamic sidebarwidth: \${w}px`` stays.
ReportTaskLogs.tsx Dynamic heights, scrim,calc(100% - ...) all stay inline. Static parts get codemodded.
InboxSignalsTab.tsx Resizable sidebar width stays. Sticky headers + gradients stay (gradient is hard to express compactly).
TourTooltip.tsx Viewporttop: y / left: x stay. flexShrink: 0 and other static parts get codemodded.
KeyboardShortcutsSheet.tsx Keycap is a small design system bundled in one style block. Extract to a single component with internal Tailwind classes.
OnboardingHogTip.tsx Speech-bubble triangle uses transparent borders — leave the CSS triangle inline (utilities form would be ugly).

For each: I read the current state, apply the conversions the codemod missed, and confirm visually-equivalent output.

Phase 4 — Strengthen CLAUDE.md

Replace the existing minimal section at lines 175-177 of CLAUDE.md:

### Tailwind over inline styles

Always write Tailwind classes for styling wherever possible.

…with a fuller version that defines the boundary clearly:

### Tailwind over inline styles

Always reach for Tailwind utility classes first. The codebase uses Tailwind v4
with CSS variables from Radix Themes (e.g. `--gray-12`, `--space-3`,
`--radius-2`); use Tailwind v4's CSS-var shorthand to bridge them — `text-(--gray-12)`,
`bg-(--gray-2)`, `rounded-(--radius-2)`, `border-(--gray-5)`. Use arbitrary values
(`text-[13px]`, `pl-[18px]`) when the design token doesn't have a named match.

Inline `style={{}}` is acceptable in three cases only:

1. **Genuinely dynamic values** computed at runtime that can't be a class —
   e.g. `style={{ width: \`${pxFromHook}px\` }}`, `style={{ transform: \`translateY(${y}px)\` }}`,
   pixel positions from measurement.
2. **Library configuration** passed to non-React libraries (CodeMirror's
   `EditorView.theme(...)`, xterm.js options, etc.).
3. **CSS variables set from JS** that downstream classes consume —
   `style={{ "--row-color": item.color }}` paired with `className="bg-(--row-color)"`.

Do NOT use inline `style` for:

- Color tokens (use `text-(--gray-12)`)
- Spacing (use `p-3`, `mt-2`, `pl-4`)
- Layout primitives (`shrink-0`, `min-w-0`, `flex-1`, `overflow-y-auto`, `w-full`)
- Borders, radii, cursors, opacity, text-align, text-transform, white-space
- Conditional values that can be `className={cond ? "x" : "y"}`

Default line-heights have been tightened (`text-sm` ships with etc.)
in [globals.css](./apps/code/src/renderer/styles/globals.css). Don't add a
`leading-*` class for body text unless you specifically want a non-default
line-height. For arbitrary sizes (`text-[13px]`), pair with `leading-snug` for
body text or `leading-tight` for titles.

Order of execution

flowchart TD
  P1A[Phase 1a: globals.css @theme line-height overrides]
  P1B[Phase 1b: sweep text-Npx leading-5 →]
  P1C[Phase 1c: simplify List.tsx sizeStyles]
  Build1[Quick visual smoke test]
  P2[Phase 2: write codemod, dry-run, apply]
  Biome[biome check --write --unsafe touched files]
  Typecheck[tsc --noEmit]
  P3[Phase 3: hand-edit ~10 hard files]
  P4[Phase 4: update CLAUDE.md]
  Final[Final biome + tsc]

  P1A --> P1B --> P1C --> Build1 --> P2 --> Biome --> Typecheck --> P3 --> P4 --> Final
Loading

What stays inline (intentionally)

Risk & mitigation

  • Visual regressions: line-height tightening is the most visible change. Suggested smoke test after Phase 1: view each route once (onboarding, settings, inbox, sessions, code review). The change in List.tsx already validated the direction.
  • Codemod over-converts: the mapping table is conservative — only literal values in the table get touched. Skip log captures everything else for review.
  • Class merging edge cases: same shape as the previous size/weight codemod, which had a 98% hit rate (10 manual fixes out of 510). Expect similar.
  • Style attribute deletion: only delete when object is empty after property removal — never strip a non-empty style block.

Migration Result

Final stats

  • 191 files modified
  • 1018 insertions, 2151 deletions — net 1133 lines removed
  • 640 → 163 inline style={{}} instances (~70% reduction)
  • 0 new TypeScript errors (only pre-existing 3 unrelated errors remain)
  • All Tailwind classes biome-sorted

What got done

Phase 1 — Line-height tightening

  • globals.css @theme block overrides Tailwind v4's font-size line-height tokens (--text-{xs,sm,base,lg,xl,2xl}--line-height) so every <element className="text-sm"> automatically gets the tighter leading. Body sizes use snug (1.375), heading sizes stay tight (1.25).
  • Swept all text-[Npx] leading-5text-[Npx] across the renderer (108 files via per-line awk that required both patterns on the same line).
  • Simplified List.tsx sizeStyles to drop redundant leading-snug from sizes 2 and 3 since the theme override now provides it.

Phase 2 — Inline-style codemod

  • New apps/code/scripts/migrate-inline-styles.ts — ts-morph codemod that walks every style={{...}} attribute, converts known static properties via a mapping table, removes them from the style object, merges new classes into className, and deletes the style attribute when emptied.
  • Mapping handles ~50 CSS properties: colors (Radix var → text-(--gray-N) shorthand), spacing (margins/paddings/gaps/inset/top/right/bottom/left), sizing (with px/percent/vw/vh support), flex/grid/display/overflow/position/cursor/text/white-space/word-break/font-weight/opacity/userSelect/pointerEvents/visibility/box-sizing/list-style/object-fit/z-index, plus border / borderRadius / background tokens.
  • Multi-value padding/margin shorthand parser ("6px 10px"px-[10px] py-[6px]).
  • Codemod converted 1205 of 1528 examined properties (~79%) across 141 files in one pass; remaining 323 were genuine skips (dynamic values, library config, multi-value gradients, etc.).
  • Auto-fixed DotPatternBackground.tsx to accept className after the codemod added one to a call site.

Phase 3 — Hand-edits for the hard files

  • Cleaned up the codemod's leftover skipped-but-fixable cases in InboxEmptyStates, SignalSourceToggles, McpInstalledRail, OptionRow, ReviewShell, TourTooltip, KeyboardShortcutsSheet, SettingsDialog, TaskDetail, Divider, FullScreenLayout, and 6 fontFamily-using files.
  • Notable conversions:
    • tracking-[0.06em], italic, grid-cols-[28px_1fr_auto], rounded-full
    • Conditional backgrounds: ${isCancel ? "bg-(--gray-3)" : ...} instead of inline ternaries
    • font-mono and font-[var(--code-font-family)] for monospace text
    • [all:unset] for the rare CSS-reset case
    • last:border-b-0 even:bg-(--gray-1) odd:bg-(--gray-2) to replace index < ... && index % 2
    • animate-spin to replace inline animation: "spin 1s linear infinite"
    • m-auto instead of margin: "auto auto"
    • max-h-[calc(80vh-120px)] for arbitrary calc expressions

Phase 4 — CLAUDE.md rule

  • Replaced the minimal 3-line "Tailwind over inline styles" section in CLAUDE.md with a full guide that defines the allowed cases (dynamic values, library config, CSS-var bridges), explicit deny list (color tokens, spacing, layout, conditional values, etc.), Radix→Tailwind spacing scale mapping, line-height defaults note, and guidance for custom components to accept both className and style props.

What remains inline (intentional, ~163 instances)

  • Library config: editorTheme.ts, useCodePreviewExtensions.ts, TerminalManager.ts (CodeMirror / xterm options — not React style)
  • Dynamic measurements: pixel positions from JS measurement (sidebar widths, tooltip x/y, log heights, indent-from-depth padding, transforms)
  • Data-driven colors: style={{ color: meta.color }}, background: PULSE_COLOR[status], boxShadow: 0 0 0 3px color-mix(...) — values from data
  • fontSize: "inherit" in custom buttons inside text — intentional cascade
  • Multi-value gradients / shadows / mask-image — too varied for utility classes
  • CSS triangles (OnboardingHogTip speech bubble) — leaving as transparent borders is more legible than utilities
  • Style spreads (style={{ ...callerStyle, foo: 1 }}) — caller-passed style needs to merge

Codemod artifact

apps/code/scripts/migrate-inline-styles.ts is kept as a reusable artifact next to the existing migrate-radix-text-to-tailwind.ts. It supports --dry-run, --verbose (skip log with grouping + samples), and --files=<glob> for narrowed runs. If new inline styles creep in despite the CLAUDE.md rule, you can re-run it any time.

Copy link
Copy Markdown
Contributor

@jonathanlab jonathanlab left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks!
qq: if we're using text-[13px] everywhere, why not add a custom tailwind token for it?

@jonathanlab jonathanlab merged commit a6628fb into main Apr 24, 2026
15 checks passed
@jonathanlab jonathanlab deleted the default-font branch April 24, 2026 11:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants