Skip to content

Commit 39307bd

Browse files
authored
chore(tailwind): upgrade apps/sim to Tailwind v4 (#7419)
* chore(tailwind): upgrade to v4 and swap clsx/tailwind-merge for cn Migrates apps/sim from Tailwind v3.4 to v4.3 (CSS-first config) and replaces clsx + tailwind-merge with the cn package in @sim/emcn and apps/docs. The upgrade is intended to be visually inert. Parity was verified by building the stylesheet both ways and diffing computed styles in a real browser: - Preflight: 112/112 bare elements identical, after restoring seven v3 behaviours v4 dropped (button cursor, form-control background, placeholder colour, table/option padding, search-field normalisation, dialog centring) - Utilities: every class the app uses renders pixel-identically - Border widths: 10/10 combinations identical - Class strings: 699 changed strings run through both merge engines, 0 diffs Pinned against v4 defaults that would otherwise shift rendering: --font-sans (v4 changed its own default stack), --text-xs--line-height, and the 24 stock palette colours the app uses (v4 re-authored the palette in oklch, which is visibly more saturated on P3 displays). borderWidth.DEFAULT has no v4 theme key, so a PostCSS pass rewrites Tailwind's own border-width output through --border-width. Re-declaring the utilities in a trailing @layer instead would make `border` beat `border-2` and `border-t-0`. rounded-sm and rounded-md are deliberately NOT renamed: this app overrides that radius scale, so the standard v3->v4 rename would change their values. cn is compiled ahead of time (cn build --full) so the config compiler stays out of the browser bundle; check:cn-tables fails CI if the tables drift. * chore(tailwind): anchor the cn-tables scratch dir ignore Was an unanchored `.cn-check/`, which would ignore a directory of that name anywhere in the tree; the check script only ever creates it under packages/emcn. * fix(tailwind): close the remaining v3/v4 rendering differences Found by rebuilding the v3 stylesheet from this same tree and diffing computed styles for all 4,674 class strings the app actually uses — combinations, not just individual utilities, which is where these only show up. - leading-*/text-* precedence: v4 routes a text-* utility's line-height through --tw-leading, so a leading-* class now wins at every breakpoint. v3 let a responsive text-lg reassert its own 28px leading; restate that explicitly. - overflow-wrap: v4 emits the arbitrary property before `break-words`, flipping the winner from `anywhere` to `break-word`. Drop the redundant utility. - z-9999999 and duration-[30ms] never took effect under v3 (outside the generated scale; rejected as ambiguous). v4 honours both, which would change stacking order and a transition from 150ms to 30ms. Removed. - note-block focus ring: outline-<n> set width only in v3, so this drew nothing; v4 also sets outline-style. Removed to keep v3 rendering. Every remaining computed-style difference is now verified non-visual: colour notation (oklch/oklab, pixel-identical), gradient interpolation, mask-composite keyword aliasing, and outline-none vs outline-hidden. * fix(tailwind): restore v3 hover, blur and breakpoint behaviour An independent parity audit across light, dark and touch contexts found four user-visible differences the earlier desktop-only checks could not see. - hover: v4 wraps `hover:`/`group-hover:`/`peer-hover:` in `@media (hover: hover)`, so 64 hover-family classes were dead on touch devices — row highlights, reveal-on-hover icons, nav menus. This codebase already has `hover-hover` for the cases that want that gating, so plain `hover:` was meant to apply everywhere. Restored with `@custom-variant hover`. - blur-0 no longer exists in v4. Two sites used `group-hover:blur-0` to clear a base `blur-[2px]`, leaving the icon permanently blurred. Renamed to `blur-none`. - space-y-* dropped from specificity 0-3-0 to `:where()` 0-0-0 and moved to the preceding sibling, so a child's own `mt-*` now wins. Two layouts grew 4-8px; removed the child margins v3 was overriding. - Breakpoints: v4 emits rem, which resolves against the browser's initial font size rather than `html { font-size }`, so a reader on a larger default text size got the mobile layout at desktop widths. Pinned back to v3's px. Hardening from the same pass: - `::before`/`::after`/`::backdrop` border-color — v4 leaves them `currentColor` and the app's `*` rule did not cover pseudo-elements. - apps/docs was missing the `cn-tables.ts` source exclusion, so its bundle carried phantom `float-left`, `clear-end` and every `mix-blend-*`. - Hairline plugin: narrowed to border-width longhands (it could rewrite an `outline-width` sharing a rule), stopped `\b1px\b` splicing into `0.1px`, and skipped node_modules stylesheets. Added 18 fixture tests. - 23 golden-case tests for `cn`, pinning the font-size class group. - The tables drift check now derives its flags from `cn:build` so the two cannot diverge, uses the local bin instead of `bun x`, and writes to tmpdir. * refactor(emcn): finish the cn migration by dropping clsx apps/sim still imported clsx directly in 8 files, so the repo carried two class-name paths — one that resolves Tailwind conflicts and one that does not. Converts them all to `cn` and removes the dependency. clsx joins; cn merges. The two differ only where a call emits conflicting classes, so every call site was checked rather than assumed. Of 19 sites with 2+ class tokens, 6 could conflict: - 4 already resolve the same way, because cn keeps the class CSS source order was picking anyway (cursor-grab/pointer, opacity-0/100, text-secondary/tertiary) - 2 are mutually exclusive ternaries in output-panel; only one branch ships The seventh is a latent bug rather than a merge difference: workflow-item's overlay sets `pointer-events-none` in its base and `pointer-events-auto` when the context menu is open, but Tailwind emits `pointer-events-none` last, so it always won — that button has never been clickable while the menu is open, only visible. Left rendering as-is with a comment; enabling it is a real fix but not a rendering-neutral one. Verified all four runtime branches of that call still yield `pointer-events-none`, and the built stylesheet is byte-identical. * refactor(emcn): use tailwind-merge v3 instead of the cn package The cn swap did not earn its keep. Benchmarked on this repo's own 4,673 class strings, with both libraries' caches warm — which is what a re-rendering UI is almost always in — cn is 4.2x faster per call (4.7ns vs 19.5ns). On a heavy 5,000-call render that is a 0.074ms saving: below anything perceivable, and far below React's own reconciliation. The 13.7x cold-path win only applies to strings neither cache has seen. Against that it cost 2.1 KB gzip over tailwind-merge v3, paid on every cold load on the critical path to first paint. cn trades gzip-compressibility for parse speed: its tables are pre-encoded data that gzip cannot squeeze, where tailwind-merge's config is repetitive JS that it can. Reverting also drops a dependency published eight days ago onto a recycled npm name, and lets the 7-day supply-chain gate go back on for the whole tree. Keeps everything the migration was actually worth: Tailwind v4, and a single class-name path now that apps/sim's direct clsx imports are gone. The 23 golden cases pass unchanged against tailwind-merge, so the merge semantics are identical; the built stylesheet is byte-for-byte the same.
1 parent 17bfb0b commit 39307bd

279 files changed

Lines changed: 1658 additions & 1285 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.agents/skills/design-taste-frontend/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ source: https://github.com/leonxlnx/taste-skill — skills/taste-skill/SKILL.md
44
description: Anti-slop frontend skill for landing pages, portfolios, and redesigns. The agent reads the brief, infers the right design direction, and ships interfaces that do not look templated. Real design systems when applicable, audit-first on redesigns, strict pre-flight check.
55
---
66

7-
> **In this repo:** Tailwind 3.4 (`apps/sim/tailwind.config.ts`); animation via `import { motion } from 'framer-motion'` (not `motion/react` — rewrite every `motion/react` import in the samples below); icons from `@sim/emcn/icons`; colors through the CSS-variable tokens in `.claude/rules/sim-styling.md` (no hardcoded `text-gray-*`/hex/`zinc` utilities, no paired `dark:` utilities). This note overrides any conflicting guidance or code sample anywhere in this file.
7+
> **In this repo:** Tailwind 4 (CSS-first config in `apps/sim/app/_styles/globals.css`); animation via `import { motion } from 'framer-motion'` (not `motion/react` — rewrite every `motion/react` import in the samples below); icons from `@sim/emcn/icons`; colors through the CSS-variable tokens in `.claude/rules/sim-styling.md` (no hardcoded `text-gray-*`/hex/`zinc` utilities, no paired `dark:` utilities). This note overrides any conflicting guidance or code sample anywhere in this file.
88
99
# tasteskill: Anti-Slop Frontend Skill
1010

.claude/rules/sim-settings-pages.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ Adding a new settings page:
113113
## Text-scale tokens (no literal pixel sizes)
114114

115115
Settings pages never use a literal `text-[Npx]` class — always the named Tailwind
116-
scale token from `apps/sim/tailwind.config.ts`'s `fontSize` extension (`text-micro`
116+
scale token from the `@theme` block in `apps/sim/app/_styles/globals.css` (`text-micro`
117117
10px, `text-xs` 11px, `text-caption` 12px, `text-small` 13px, `text-sm` 14px
118118
[Tailwind default, unmodified], `text-base` 15px, `text-md` 16px, `text-lg` 18px
119119
[Tailwind default]). A literal size is either a straight rename to the equivalent

.claude/rules/sim-styling.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ setWidth: (width) => {
4646

4747
## Text Scale
4848

49-
Custom font sizes (`apps/sim/tailwind.config.ts`): `text-micro`=10px, `text-xs`=11px, `text-caption`=12px, `text-small`=13px, `text-base`=15px. `text-sm` is Tailwind default 14px. Field titles use `text-small` (13px); hints/errors use `text-caption` (12px).
49+
Custom font sizes (the `@theme` block in `apps/sim/app/_styles/globals.css`): `text-micro`=10px, `text-xs`=11px, `text-caption`=12px, `text-small`=13px, `text-base`=15px. `text-sm` is Tailwind default 14px. Field titles use `text-small` (13px); hints/errors use `text-caption` (12px).
5050

5151
Icons default `size-[14px]`. Equal h/w → `size-*` (`size-[14px]`, `size-4`), never `h-N w-N`.
5252

.cursor/rules/sim-styling.mdc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ setWidth: (width) => {
4040

4141
## Text Scale
4242

43-
Custom font sizes (`apps/sim/tailwind.config.ts`): `text-micro`=10px, `text-xs`=11px, `text-caption`=12px, `text-small`=13px, `text-base`=15px. `text-sm` is Tailwind default 14px. Field titles use `text-small` (13px); hints/errors use `text-caption` (12px).
43+
Custom font sizes (the `@theme` block in `apps/sim/app/_styles/globals.css`): `text-micro`=10px, `text-xs`=11px, `text-caption`=12px, `text-small`=13px, `text-base`=15px. `text-sm` is Tailwind default 14px. Field titles use `text-small` (13px); hints/errors use `text-caption` (12px).
4444

4545
Icons default `size-[14px]`. Equal h/w → `size-*` (`size-[14px]`, `size-4`), never `h-N w-N`.
4646

apps/docs/app/global.css

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
/* Every @sim/emcn component expresses hover through `hover-hover:` so touch
99
devices never latch a sticky hover state. The app registers it as a plugin
10-
variant in apps/sim/tailwind.config.ts; docs is CSS-first Tailwind v4 with no
10+
variant in apps/sim/app/_styles/globals.css; docs is CSS-first Tailwind v4 with no
1111
config, so without this declaration the variant compiles to nothing and every
1212
emcn hover state silently no-ops here. */
1313
@custom-variant hover-hover {
@@ -35,7 +35,7 @@ body {
3535
--color-fd-primary: var(--color-fd-foreground);
3636
/* Sim's custom type scale — emcn components (Label, Badge, the shared block
3737
views) use these names, so they must resolve here or text falls back to the
38-
inherited size. Mirrors apps/sim/tailwind.config.ts. */
38+
inherited size. Mirrors apps/sim/app/_styles/globals.css. */
3939
--text-micro: 10px;
4040
--text-xs: 11px;
4141
--text-caption: 12px;
@@ -47,7 +47,7 @@ body {
4747
--text-caption and --text-small, because the mono face reads small at 12px. */
4848
--text-code: 0.78125rem;
4949

50-
/* The platform's code face. `apps/sim/tailwind.config.ts` lists `var(--font-martian-mono)`
50+
/* The platform's code face. `apps/sim/app/_styles/globals.css` lists `var(--font-martian-mono)`
5151
first, but nothing in the app ever defines that variable — `apps/sim/app/layout.tsx`
5252
applies only `season.variable` — so every code surface in the product resolves to the
5353
system stack below. Docs match what the app actually renders rather than the token it

apps/docs/lib/utils.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1-
import { type ClassValue, clsx } from 'clsx'
2-
import { twMerge } from 'tailwind-merge'
3-
41
/**
5-
* Combines multiple class names into a single string, merging Tailwind classes properly
2+
* Re-exported from the design system rather than built here, so docs runs one
3+
* merge engine instead of two: its own components already import `cn` from
4+
* `@sim/emcn` indirectly, and a second `cn` entry point would ship a duplicate
5+
* engine and lookup tables.
6+
*
7+
* It also carries emcn's `font-size` class group, which docs needs — its
8+
* `global.css` defines the same `--text-micro/caption/small/md` scale, and the
9+
* stock merger does not treat those as font sizes, so `cn('text-small',
10+
* 'text-sm')` emitted both and let CSS source order pick the winner.
611
*/
7-
export function cn(...inputs: ClassValue[]) {
8-
return twMerge(clsx(inputs))
9-
}
12+
export { cn } from '@sim/emcn'
1013

1114
/**
1215
* Get the full URL for an asset stored in Vercel Blob

apps/docs/package.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
"@sim/workflow-renderer": "workspace:*",
2424
"@xyflow/react": "12.11.3",
2525
"class-variance-authority": "^0.7.1",
26-
"clsx": "^2.1.1",
2726
"drizzle-orm": "^0.45.2",
2827
"fumadocs-core": "16.8.5",
2928
"fumadocs-mdx": "14.3.2",
@@ -35,7 +34,6 @@
3534
"react-dom": "19.2.4",
3635
"remark-breaks": "^4.0.0",
3736
"shiki": "4.3.1",
38-
"tailwind-merge": "^3.0.2",
3937
"framer-motion": "^12.5.0",
4038
"zod": "4.3.6"
4139
},

apps/sim/app/(interfaces)/chat/components/input/input.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ export const ChatInput: React.FC<{
124124
const canSubmit = (inputValue.trim().length > 0 || attachedFiles.length > 0) && !isStreaming
125125

126126
return (
127-
<div className='fixed right-0 bottom-0 left-0 flex w-full items-center justify-center bg-gradient-to-t from-[var(--bg)] to-transparent px-4 pb-4 md:px-0 md:pb-4'>
127+
<div className='fixed right-0 bottom-0 left-0 flex w-full items-center justify-center bg-linear-to-t from-[var(--bg)] to-transparent px-4 pb-4 md:px-0 md:pb-4'>
128128
<div className='w-full max-w-3xl md:max-w-[748px]'>
129129
{uploadErrors.length > 0 && (
130130
<div className='mb-3 flex flex-col gap-2'>
@@ -171,7 +171,7 @@ export const ChatInput: React.FC<{
171171
{attachedFiles.map((file) => (
172172
<Tooltip.Root key={file.id}>
173173
<Tooltip.Trigger asChild>
174-
<div className='group relative size-[56px] flex-shrink-0 cursor-pointer overflow-hidden rounded-[8px] border border-[var(--border-1)] bg-[var(--surface-3)]'>
174+
<div className='group relative size-[56px] shrink-0 cursor-pointer overflow-hidden rounded-[8px] border border-[var(--border-1)] bg-[var(--surface-3)]'>
175175
{file.dataUrl ? (
176176
<img
177177
src={file.dataUrl}
@@ -214,7 +214,7 @@ export const ChatInput: React.FC<{
214214
onKeyDown={handleKeyDown}
215215
placeholder={isDragOver ? 'Drop files here...' : 'Enter a message...'}
216216
rows={1}
217-
className='m-0 h-auto min-h-[24px] w-full resize-none overflow-y-auto overflow-x-hidden border-0 bg-transparent p-1 text-[15px] text-[var(--text-primary)] leading-[24px] caret-[var(--text-primary)] outline-none [-ms-overflow-style:none] [scrollbar-width:none] placeholder:text-[var(--text-muted)] focus-visible:ring-0 focus-visible:ring-offset-0 [&::-webkit-scrollbar]:hidden'
217+
className='m-0 h-auto min-h-[24px] w-full resize-none overflow-y-auto overflow-x-hidden border-0 bg-transparent p-1 text-[15px] text-[var(--text-primary)] leading-[24px] caret-[var(--text-primary)] outline-hidden [-ms-overflow-style:none] [scrollbar-width:none] placeholder:text-[var(--text-muted)] focus-visible:ring-0 focus-visible:ring-offset-0 [&::-webkit-scrollbar]:hidden'
218218
/>
219219

220220
<div className='flex items-center justify-between'>

apps/sim/app/(interfaces)/chat/components/message-container/message-container.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export function ChatMessageContainer({
6868
</div>
6969

7070
{showScrollButton && (
71-
<div className='-translate-x-1/2 absolute bottom-16 left-1/2 z-20 transform'>
71+
<div className='-translate-x-1/2 absolute bottom-16 left-1/2 z-20'>
7272
<Button
7373
onClick={scrollToBottom}
7474
size='sm'

apps/sim/app/(interfaces)/chat/components/message/components/file-download.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,12 +115,12 @@ export function ChatFileDownload({ file }: ChatFileDownloadProps) {
115115
disabled={isDownloading}
116116
className='group flex h-auto w-[200px] items-center gap-2 rounded-lg px-3 py-2'
117117
>
118-
<div className='flex size-8 flex-shrink-0 items-center justify-center'>{renderIcon()}</div>
118+
<div className='flex size-8 shrink-0 items-center justify-center'>{renderIcon()}</div>
119119
<div className='min-w-0 flex-1 text-left'>
120120
<div className='w-[100px] truncate text-xs'>{file.name}</div>
121121
<div className='text-[var(--text-muted)] text-micro'>{formatFileSize(file.size)}</div>
122122
</div>
123-
<div className='flex-shrink-0'>
123+
<div className='shrink-0'>
124124
{isDownloading ? (
125125
<Loader className='size-3.5' animate />
126126
) : (

0 commit comments

Comments
 (0)