Skip to content

refactor(website): substrate migration batch 1 — ui primitives off inline styles - #848

Merged
blove merged 7 commits into
mainfrom
blove/style-substrate-batch-1
Aug 29, 2026
Merged

refactor(website): substrate migration batch 1 — ui primitives off inline styles#848
blove merged 7 commits into
mainfrom
blove/style-substrate-batch-1

Conversation

@blove

@blove blove commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

What

Batch 1 of the inline-style substrate migration (spec · plan in this PR): the 11 components/ui primitives move their static presentation from inline style props into src/styles/ui.css, plus the migration infrastructure (six scope files imported by global.css, an advisory value-equality checker, a computed-style snapshot snippet). Deletes the unused page.module.css stub.

This PR is the pattern-setter — its review calibrates batches 2–7. The conventions it establishes:

  • Migrated rules are unlayered (never @layer): Tailwind v4 layers everything, unlayered author CSS beats all layers — the precedence inline styles had.
  • Bounded variants become data-* modifiers: data-variant/data-size (Button), data-padding (Card), data-tight (Section), data-active (TabGroup), data-elevation (BrowserFrame).
  • Caller style passthroughs stay, so callers still win.
  • ClipPlayer and TabGroup gain their first data-ui hooks.

Verification — zero visual change, measured

Before migrating, a computed-style baseline of the homepage was captured: 143 hooked elements × 26 properties. After migration the diff is 0 real changes (4 diffs remain, all heights of the hero clip's lazy-mounted video — no height property was migrated).

Also: vitest exactly at the pre-existing baseline (5 failed | 341 passed — PostCard/Differentiator/thanks content drift, untouched), 0 lint errors, production build green.

The bug the measurement caught

The first migration pass moved fontFamily: tokens.typography.fontSans onto var(--font-inter) per the token↔CSS parity map — and the diff showed button widths shifting ~2.5px. Cause: layout.tsx loads fonts via next/font with the same variable names as the token vars, so var(--font-inter) resolves to next/font's stack at runtime, not the token value. The parity guard is value-true against theme.css but runtime-false for the three font vars.

Fix in the final commit: sites whose inline value was the raw stack keep it as a verbatim literal; only sites already using a .family value (which is 'var(--font-*)') keep the var. Recorded as findings §10 and as a locked exception in the migration plan — batches 2–6 inherit the rule. Unifying fonts onto next/font would be a real (arguably desirable) visual change and belongs to the polish arc as its own decision.

Two verbatim oddities preserved on purpose

  • TabGroup's Inter, sans-serif and border-radius: 8px are hardcoded literals that match no token (--radius-md is 10px). Copied through unchanged with a comment — fixing them is restyling.
  • FAQ's marker/chevron/focus rules stay in global.css for now; only resting styles moved.

🤖 Generated with Claude Code

@vercel

vercel Bot commented Aug 29, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
threadplane Ready Ready Preview Aug 29, 2026 7:53pm

Request Review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Automated approval: this PR received an intelligent (AI) code review. See the review comments on this PR.

@github-actions

github-actions Bot commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Claude finished @blove's task in 0s —— View job


I'll analyze this and get back to you.

blove and others added 7 commits August 29, 2026 12:50
Extends the data-ui/data-mdx hook pattern already shipping in 21 places rather
than introducing CSS Modules; splits the CSS into six files so each batch maps
1:1 to a file and can be reverted alone.

Corrects a count carried through project 1: the real figure is 908 style-prop
sites, not 802. `grep 'style={{'` sees only object literals at the call site and
misses 106 sites that pass a named CSSProperties variable — Button.tsx does
this and was miscounted as already migrated. The 1,188 token-reference figure is
unaffected, having been counted from `tokens.*` directly.

Records the no-visual-harness decision as an explicitly accepted risk, with two
cheap non-harness mitigations: a text-level value-equality check per batch, and
a one-page computed-style spot check.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Seven migration PRs plus an ESLint guard, each batch mapping 1:1 to a CSS file
so it reverts alone. Locks the conventions every batch follows: data-ui hooks
for primitives, component-prefixed classes elsewhere, four worked shape
archetypes (static literal, variant map, hover handler, reused variable), and
the custom-property escape hatch for dynamic values.

Planning corrected the spec in four places, each verified rather than assumed:

- The @layer components suggestion was backwards. Tailwind v4 layers
  everything and unlayered author CSS beats all layers - the precedence inline
  styles had. Migrated rules stay unlayered, like global.css today.
- A third substrate: 12 components embed <style> tags interpolating tokens.*.
- Satori OG images and the dev-only primitives route are excluded (inline
  styles are load-bearing there); in-scope is ~877 sites, not 908.
- 33 onMouseEnter presentation handlers (23 in Footer) become :hover rules.

Ships an advisory value-equality checker and a computed-style snapshot snippet
as the agreed non-harness mitigations.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Card, Container, Eyebrow, Pill, Section move static presentation from
inline style props into unlayered rules in ui.css, keyed off the
existing data-ui/data-variant/data-surface hooks (plus new
data-padding on Card and data-tight on Section). Also cut-pastes the
existing [data-ui="card"] and [data-ui="faq-item"] blocks out of
global.css into ui.css unchanged. Zero visual change: every value
moved verbatim, token references resolved via CSS_VAR_BY_PATH.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Button, FAQ, LogoMark move static presentation from inline style props
(VARIANT_STYLES/SIZE_STYLES records, per-item inline objects) into
unlayered rules in ui.css, keyed off data-ui/data-variant/data-size
(new data-size on LogoMark, new data-ui="logo-mark-icon" hook).
Caller `style` passthrough is preserved on Button and LogoMark. Zero
visual change: every value moved verbatim.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
BrowserFrame, ClipPlayer, TabGroup move static presentation from
inline style props into unlayered rules in ui.css. BrowserFrame gains
data-elevation plus new data-ui hooks on its title bar/dots/url
pill/spacer (transform-rotate and max-width stay inline, genuinely
dynamic per-instance values); ClipPlayer and TabGroup get their first
data-ui hooks (data-ui="clip-player", data-ui="tab-group"/"tab-group-
list"/"tab-group-tab" with data-active). TabGroup's hardcoded
`Inter, sans-serif` font stack and `8px` radius are copied verbatim
rather than mapped to the (different-valued) --font-inter/--radius-md
tokens. Zero visual change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…s the token vars

layout.tsx loads fonts via next/font with variable names identical to the
theme.css font vars, so var(--font-inter) resolves to next/font's stack at
runtime, not tokens.typography.fontSans. The batch-1 migration onto the vars
changed Button's rendered font (widths shifted ~2.5px, measured by
computed-style diff against the pre-migration baseline). Sites whose inline
value was the raw stack keep it as a literal; Eyebrow, whose inline value was
already 'var(--font-mono)', keeps the var.

After this fix the baseline diff is 0 real changes across 143 elements x 26
properties (4 remaining diffs are the hero clip's lazy-mount heights).

Recorded as findings §10 and as a locked exception in the migration plan.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@blove
blove force-pushed the blove/style-substrate-batch-1 branch from f49247d to ed81369 Compare August 29, 2026 19:50
@github-actions

github-actions Bot commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Claude finished @blove's task in 0s —— View job


I'll analyze this and get back to you.

@blove
blove merged commit d418b38 into main Aug 29, 2026
23 checks passed
blove added a commit that referenced this pull request Aug 29, 2026
Lands only now that all seven migration batches (#848-#857) are merged, so it
never sees legacy code. Flags identifier-keyed members of a style object
literal in apps/website/src; the style={{ '--x': value }} escape hatch uses
string-literal keys and passes. Excluded: the two Satori OG-image files
(inline-only by design - note the [slug] segment must be matched by wildcard,
brackets are a glob character class), the dev-only primitives route, and specs.

Verified with raw eslint: fires on an identifier-key probe, silent on a
custom-property probe, zero hits on the OG file. Baseline is 18 warnings -
exactly the documented dynamic-value sites from the batch reports - and 0
errors, so CI (which fails on errors only) stays green. Ships as 'warn'; the
escalation to 'error' is a deliberate follow-up one release later.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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.

1 participant