Skip to content

feat(studio): upgrade Studio to Tailwind CSS v4 - #3618

Draft
miguel-heygen wants to merge 1 commit into
mainfrom
feat/studio-u14-tailwind-v4
Draft

feat(studio): upgrade Studio to Tailwind CSS v4#3618
miguel-heygen wants to merge 1 commit into
mainfrom
feat/studio-u14-tailwind-v4

Conversation

@miguel-heygen

Copy link
Copy Markdown
Collaborator

Lands unit U14 (Tailwind v4) of the Studio design-system foundation. No ratchet yet; no color literal added. One PR over the usual size guideline: a half-migrated Tailwind tree does not build, so there is no seam to split at.

Lands U14 of the Studio design-system plan. Ratchet delta: none (the hex ratchet lands in a later unit).

What

Studio moves from Tailwind CSS 3.4 to 4.3.3, loaded through @tailwindcss/vite instead of PostCSS.

  • packages/studio/tailwind.config.js and packages/studio/postcss.config.js are deleted. The JS config's studio.* and panel.* palettes become an @theme block in src/styles/studio.css, byte-for-byte the same hex values.
  • @import "tailwindcss" source(none) plus explicit @source globs for src/**/*.{ts,tsx} and index.html. Automatic source detection would walk up to the repository root and scan data/projects, which is gitignored user content rather than Studio markup.
  • autoprefixer and postcss are dropped from Studio's dev dependencies; @tailwindcss/vite is added.
  • Renamed utilities migrated across Studio's components.

The published ./tailwind-preset subpath is untouched: src/styles/tailwind-preset.shared.js and tailwind-preset.ts still exist and still build to dist/styles/tailwind-preset.js. Turning it into a deprecated shim is a later unit's job.

Why

Tailwind v4 is a prerequisite for the token work in this plan. It makes the token file CSS-first, emits every token as a real CSS custom property, and handles alpha on any color format, which removes most of the hand-built plumbing the token unit would otherwise need.

How

The official @tailwindcss/upgrade tool did the utility renames. Its diff was then reviewed by hand, which mattered more than expected: the tool rewrites bare words inside any string literal, not only inside class attributes. Left as-is it would have shipped these defects, all of which were reverted:

  • removeEventListener("blur", ...) became removeEventListener("blur-sm", ...) in five files, so focus-loss cleanup would never have fired.
  • element.style.setProperty("flex-grow"/"flex-shrink", ...) became setProperty("grow"/"shrink", ...) in the DOM-editing patch and snapshot paths, which are no-op CSS property names.
  • getCssFilterFunctionPx(styles.filter, "blur") became "blur-sm", breaking the inspector's blur sliders.
  • The FrameStatus value "outline" became "outline-solid" in the storyboard status order and its consumers.
  • Effect-spec keys, user-visible labels ("Layer blur"), test names and code comments were rewritten as prose.

Every one of these was outside a className. The rule that caught them: a change the tool makes on a line with no className on it is a false positive until proven otherwise.

Default changes the codemod did not make, and what was done about each

v4 default change Finding in Studio Action
Border color: gray-200 to currentColor The upgrade tool emits a compatibility shim pinning border-color back to gray-200 on *. An audit of every string literal carrying a bare border/border-<side> found 15 hits: all are either border-0, or prose in test names, or already carry an explicit color (an inline borderColor, or a variant class supplying border-emerald-700/border-sky-500/border-neutral-700). Every border: declaration in studio.css names its own color. Shim deleted. Nothing depended on the default, so keeping it would have been dead plumbing.
Ring width 3 to 1, ring color to currentColor Studio has no bare ring utility. Every ring-* usage names both a width (ring-1, ring-2) and a color. The four grep hits for a bare ring are all the English word in comments. No change needed.
divide-* default color One divide-y, and it carries divide-panel-border-input/60. No change needed.
outline-none semantics 93 occurrences migrated to outline-hidden (79 bare, 14 under a focus: variant); bare outline migrated to outline-solid. Handled by the codemod, verified by hand.
Shadow / radius scale shift rounded to rounded-sm, rounded-sm to rounded-xs, shadow to shadow-sm, drop-shadow to drop-shadow-sm, blur to blur-sm, backdrop-blur-sm to backdrop-blur-xs. Handled by the codemod, verified by hand.
bg-opacity-* removed Studio has none. No change needed.
@apply / the v3 theme() function in CSS studio.css uses neither. No change needed.
Pointer cursor on buttons The shared Button and IconButton already set cursor-pointer explicitly. No change needed.
Tailwind's JS PostCSS-plugin API removed One test compiled Tailwind in-process to assert field-sizing: content is emitted. Rewritten onto v4's compile() API; postcss is no longer a Studio dependency.

Test plan

Studio's dev server, suite, typecheck and build all run against the new pipeline.

  • Unit suite: 427 files passed, 1 skipped; 4743 tests passed, 18 todo. No dev server was alive from this session during the run.
  • typecheck: clean.
  • build: clean, including the tailwind-preset subpath still emitting to dist/styles/tailwind-preset.js.
  • oxlint: 0 warnings, 0 errors. oxfmt --check: clean (7 files were reformatted because the longer v4 class names push lines past the print width).
  • Fallow audit against the base branch: passes.
  • Studio load smoke (the same command the CI job runs, against a dev server booted from this branch): PASS: studio loaded with schema-valid API fixtures and no runtime errors.
  • Emitted stylesheet spot-checked: .bg-studio-accent, .bg-panel-surface, .text-panel-text-2, .border-panel-border-input, .bg-panel-bg-inset are all present, and --color-panel-surface:#18181b is emitted as a real custom property. Alpha on a theme color folds correctly (.bg-studio-accent\/3 compiles to #3ce6ac08).

Screenshot and computed-style capture

The plan's capture script was run on the base branch and on this branch.

Screenshots. Reporting a raw before/after difference here would be misleading, because the capture is not deterministic: two runs of the same base-branch code differ by SSIM 0.923 / 0.922 / 0.931 on three of the six states, because the preview canvas and the composition list each race the screenshot. Measured against that noise floor, every before/after delta is at or below same-code run-to-run variation:

State Same-code noise Before vs after
fresh boot 0.9949 0.9998
inspector design 0.9234 0.9983
renders panel 0.9220 0.9974
canvas context menu 0.9305 0.9304
timeline toolbar 0.9988 0.9986
timeline speed menu 0.9988 0.9977

No screenshot difference is attributable to the upgrade.

Computed-style table. Four of the five rows are identical. Two cells changed, both reproducible across repeated runs:

Control Before After Verdict
Timeline toolbar button, background rgba(255, 255, 255, 0.08) oklab(0.999994 0.0000455678 0.0000200868 / 0.08) Same paint, different serialization. v4 resolves bg-white/8 through color-mix(in oklab, ...) because the color comes from a custom property. That oklab triple is white. The screenshot containing this button matches within the noise floor.
Renders Export, font size 14px 11px Real change; see below.

The Renders Export font size

That button carries two competing font-size utilities: text-sm from the shared Button's size styles, and text-[11px] from the caller's className. v3 emitted the named scale after the arbitrary value, so text-sm won and the control rendered at 14px. v4 emits arbitrary values after the named scale, so the caller's text-[11px] now wins.

This is not a regression introduced by the migration: it is the same latent defect the plan already names as the reason for adopting a class-merging helper, and v4 resolves it toward what the caller asked for.

A DOM-wide sweep across four panel states looked for every element carrying two competing utilities of the same property. This button is the only one. (Three other hits were a false positive of the scan: text-[#3CE6AC]-style colors matching a font-size pattern.) So the blast radius of v4's reordering is exactly one control, and it is the control a later unit is already scheduled to fix.

Not covered

  • The @theme block is a direct translation of the old JS palette. Semantic tokens, the type scale, radius/shadow/easing tokens, and clearing Tailwind's default palette with --color-*: initial are a later unit's scope and are deliberately absent.
  • The published ./tailwind-preset JS export still exports the raw v3-shaped preset object. Turning it into a deprecated shim is a later unit.
  • rounded-button and shadow-btn-primary are referenced by the shared Button but are not real utilities and emit nothing, on v3 and on v4 alike. Left as-is: they are pre-existing and the token unit owns them.
  • The two Export buttons still do not match each other. Making them identical is a later unit's stated goal.
  • Storybook was not part of this capture, so the capture script's Storybook state is skipped.
  • The diff is 1016 changed lines, marginally over the 1000-line guideline. It is a single mechanical codemod plus its config swap, and there is no coherent vertical seam to cut: a half-migrated tree does not build.

Studio moves from Tailwind 3.4 to 4.3.3, loaded by @tailwindcss/vite instead
of PostCSS. The JS config becomes an @theme block in studio.css carrying the
same studio.* and panel.* colors, with explicit @source globs so the gitignored
data/projects tree is never scanned.

The official upgrade tool renamed the utilities, but it rewrites bare words
inside any string, so its edits to event names, CSS property names, filter
function names, status enum values and prose were reverted by hand.

Nothing in Studio relied on the v3 default border color, ring width or ring
color, so the tool's compatibility shim was dropped rather than kept.
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