fix(tailwind): load tailwind.config.js via @config - #2628
Merged
Conversation
🎩 PreviewA preview build has been created at: |
morgan-wowk
reviewed
Aug 17, 2026
morgan-wowk
left a comment
There was a problem hiding this comment.
🤖 Agent review. Correct and byte-identical output today; one latent footgun worth weighing (inline).
camielvs
force-pushed
the
fix/tailwind-config-directive
branch
from
August 18, 2026 20:14
32b08bd to
32b1a10
Compare
Tailwind v4 does not read tailwind.config.js unless a CSS file asks for
it, so the theme.extend.fontSize entries in that file had never been
registered: text-2xs and text-3xs compiled to nothing.
Verified by building the app twice — with the directive the compiler emits
.text-2xs{font-size:.625rem} and .text-3xs{font-size:.5rem} for a probe
usage; without it, neither rule appears.
No call site uses either class today, so the emitted CSS is byte-identical
to master (161,345 bytes both ways). This only makes the two steps
available; nothing changes size.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
camielvs
force-pushed
the
fix/tailwind-config-directive
branch
from
August 18, 2026 20:17
32b1a10 to
de45cd2
Compare
morgan-wowk
approved these changes
Aug 18, 2026
morgan-wowk
left a comment
There was a problem hiding this comment.
🤖 Agent-assisted re-review (posted by Morgan). Approving. You took the v4-idiomatic path — --text-2xs/--text-3xs now live in the existing @theme inline block and the @config/tailwind.config.js reintroduction is gone. That removes the latent footgun entirely: there's no content glob to silently drop classes authored outside src/**, and source detection stays on v4's default scan. Exactly the fix I hoped for.
Collaborator
Author
camielvs
added a commit
that referenced
this pull request
Aug 19, 2026
…#2629) Resolves **B2** of #2626. ## The bug `textVariants` in `src/components/ui/typography.tsx` mapped `size="md"` to `text-md` and `weight="regular"` to `font-regular`. Neither is a Tailwind v4 token — the default theme defines `--text-xs/sm/base/lg/xl/2xl` and `--font-weight-light/normal/semibold/bold`. There is no `--text-md` and no `--font-weight-regular`. A class with no matching token compiles to **nothing**. So every `<Text>`, `<Paragraph>`, `<Heading level={1}>` and `<Link>` on the default size/weight emitted a class that did not exist, and inherited its font-size and weight from an ancestor instead. It failed silently, and was copied from `typography.tsx` into `link.tsx` before anyone noticed. ## The fix | | before | after | |---|---|---| | `textVariants.size.md` | `text-md` | `text-base` | | `textVariants.weight.regular` | `font-regular` | `font-normal` | | `linkVariants.size.md` | `text-md` | `text-base` | | `AppMenu` CopyText | `text-md` | `text-base` | `text-base` is 1rem/1.5 and `font-normal` is 400 — identical to what Preflight already gives an element that inherits (`html { line-height: 1.5 }`, `h1..h6 { font-size: inherit; font-weight: inherit }`, no `body` font-size). So on its own this change is a visual no-op **except** where an ancestor overrode font-size or weight; there, the element used to pick up the ancestor's value and would now snap to 1rem/400. ## Preserving current rendering I walked every `.tsx` in `src` with the TypeScript AST, tracking each typography primitive's ancestor chain — including ancestors contributed by wrapper components that put a font utility on the element wrapping `{children}` (`InfoBox`'s body is `text-sm`, `TooltipContent` is `text-xs`, `DialogTitle` is `text-lg font-semibold`, `TableHead` is `font-medium`, …). 13 elements across 9 files would have changed. Each now states the value it previously inherited: - **`InfoBox` body (`text-sm`) → `size="sm"`** — `PipelineValidationList` ×2, `ExamplePipelines` ×2, `FeaturedExamples`, `IOSection` ×2, `RemoteAuthErrorView`, `PipelineRun` ×2, `RunViewV2` ×2. (A sibling in `PipelineValidationList` already wrote `size="sm"` explicitly, confirming the intent.) - **`InfoBox` with `className="text-xs"` → `size="xs"`** — the three inline `<Link>`s in `ManualSubmissionInstructions`. - **`TooltipContent` (`text-xs`) → `size="xs"`** — `BetaFeatureWrapper`, matching its two siblings. - **`DialogTitle` (`text-lg font-semibold`) → `size="lg" weight="semibold"`** — `ComponentQuickDetailsDialog`. - **`TableHead` (`font-medium`) → `className="font-medium"`** — `TableVisualizer` ×2. `Text`'s weight scale has no `medium` step, so this one cannot be expressed as a prop. Cases that look like nesting but are not, and were verified to need no change: - **`asChild`** (`<Button asChild><Link>`, `<DialogDescription asChild><Paragraph>`) — Radix merges onto the *same* element, and `cn(variants, className)` puts the incoming class last, so twMerge lets the wrapper's `text-xs`/`text-sm` win. - **`CopyText`** forwards `className` into its inner `<Text>`, so all 9 call sites are same-element merges; verified per-site with a `twMerge` probe. - **`[&_.text-sm]:text-xs!`** (`FlexNodeDetails`, `RecentRunsContent`) keys off the emitted class name and only matches `.text-sm`. Confirmed no selector anywhere in `src` keys on `.text-base` or `.font-normal`. ## The guard `src/components/ui/typography.test.tsx` reads the token tables Tailwind actually compiles from (`node_modules/tailwindcss/theme.css` + `src/styles/global.css`, plus `tailwind.config.js` only when `global.css` carries an `@config` directive — see #2628), renders every step of `Text`, `Paragraph`, `Heading` and `Link`, and asserts each emits a font-size/weight class that resolves to a real token. A final pair of cases bans the two dead class names from `src` outright. Mutation-tested: restoring `text-md`/`font-regular` in `typography.tsx` fails 6 of the 23 cases — both the scale assertions and the ban, independently. > [!NOTE] > The issue proposed enforcing this with an ESLint `no-restricted-syntax` rule. That would not have worked: `no-restricted-syntax` is already configured for `REACT_COMPILER_ENABLED_GLOBS`, which explicitly includes `src/components/ui/typography.tsx` and `src/routes/**`, and flat config *replaces* a rule's options rather than merging them — so the new rule would be silently disabled in the very file it protects. CI also runs `pnpm run lint` without `--max-warnings`, so its `"warn"` severity could not fail the build. A test achieves the intent without either hole. ## Verification - `pnpm run typecheck` — clean - `pnpm run lint` — clean - `pnpm run test` — 192 files, 1989 passed - AST scan re-run post-fix: no unintended nested sites remain 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

tailwind.config.jshas been dead since the v4 upgrade. Tailwind v4 is CSS-first and does notauto-load a JS config — it only reads one when a stylesheet asks for it with
@config.src/styles/global.cssnever did, so the
theme.extend.fontSizeentries in that file were never registered andtext-2xs/text-3xscompiled to nothing.Proof the directive takes effect
A byte-identical build is ambiguous on its own — it is also what a silently-ignored directive looks
like. So I added a probe usage (
className="… text-2xs text-3xs") to a real component and built theapp both ways:
.text-2xs.text-3xs@configfont-size:.625rem✅font-size:.5rem✅@configThe probe was reverted; it is not part of this diff.
Proof nothing changes today
No file under
srcusestext-2xsortext-3xs— zero occurrences. With the probe removed, the builtstylesheet is byte-identical to
master:That also settles the one real risk in loading a legacy config: its
content: ["./src/**/*.{js,jsx,ts,tsx}"]key narrows source detection from v4's automatic scanning. Identical output over a full app build means
no currently-used class lives outside that glob. Worth knowing if a class is ever added to a
.html,.mdx, or.mjsfile — it would need adding tocontent.Why
@configrather than moving the values into@themeChosen per request. The alternative — deleting
tailwind.config.jsand adding--text-2xs: 0.625rem/--text-3xs: 0.5remto the existing@theme inlineblock — would be the more idiomatic v4 shape andwould leave one place to look for theme values instead of two. Happy to switch if preferred.
Context
Found while reviewing #2624, which independently discovered the same class of bug:
text-mdandfont-regularintypography.tsxalso emit no CSS, because--text-mdand--font-weight-regulararenot defined either. That one is a separate issue — those variants are referenced by every
<Text>in theapp, so defining them would change rendered output, unlike this change.
Reviewer checklist
@configis the direction, rather than folding the two steps into@theme inlinecontentglob is acceptable (see above)Validation
pnpm vite build✅ — output byte-identical tomasterpnpm prettier --check src/styles/global.css✅