diff --git a/apps/studio/components/ui/CodeEditor/CodeEditor.test.tsx b/apps/studio/components/ui/CodeEditor/CodeEditor.test.tsx index d4f6d10fea7..a6e5fcc0bf6 100644 --- a/apps/studio/components/ui/CodeEditor/CodeEditor.test.tsx +++ b/apps/studio/components/ui/CodeEditor/CodeEditor.test.tsx @@ -9,7 +9,7 @@ import { render } from '@/tests/helpers' * win — otherwise the editor collapses to a single line. * * This regressed once already: #47339 appended the default as `cn(className, 'monaco-editor', - * 'h-full')`, and tailwind-merge keeps the *last* conflicting height utility, so the trailing + * 'h-full')`, and `cn` keeps the *last* conflicting height utility, so the trailing * `h-full` clobbered caller heights. #47350 fixed it by passing `className` last. These tests * lock that ordering in. * diff --git a/apps/studio/components/ui/CodeEditor/CodeEditor.tsx b/apps/studio/components/ui/CodeEditor/CodeEditor.tsx index cf36124858a..ad3e522f2c9 100644 --- a/apps/studio/components/ui/CodeEditor/CodeEditor.tsx +++ b/apps/studio/components/ui/CodeEditor/CodeEditor.tsx @@ -260,7 +260,7 @@ export const CodeEditor = ({ // injects a second copy of Monaco's CSS onto the shared instance). Without an explicit // height, an absolutely-positioned wrapper collapses to 0 and Monaco lays out at ~5px. // Order matters: `h-full` is a default, so a caller-supplied height in `className` - // (e.g. `h-96`) wins via tailwind-merge instead of being clobbered. + // (e.g. `h-96`) wins via `cn` class merging instead of being clobbered. className={cn('monaco-editor', 'h-full', className)} wrapperProps={{ className: wrapperClassName }} value={value ?? undefined} diff --git a/apps/www/components/LaunchWeek/12/Releases/data/lw12_build_stage.tsx b/apps/www/components/LaunchWeek/12/Releases/data/lw12_build_stage.tsx index 83a20c6f45d..13b763cc696 100644 --- a/apps/www/components/LaunchWeek/12/Releases/data/lw12_build_stage.tsx +++ b/apps/www/components/LaunchWeek/12/Releases/data/lw12_build_stage.tsx @@ -1,7 +1,7 @@ // see apps/www/components/LaunchWeek/X/Releases/data/lwx_advent_days.tsx for reference -import { type ClassValue } from 'clsx' import { ReactNode } from 'react' +import type { ClassValue } from 'ui' export interface AdventDay { icon?: ReactNode // use svg jsx with 34x34px viewport diff --git a/apps/www/components/LaunchWeek/13/Releases/data/lw13_build_stage.tsx b/apps/www/components/LaunchWeek/13/Releases/data/lw13_build_stage.tsx index 5b3d6e4313d..dcf210466af 100644 --- a/apps/www/components/LaunchWeek/13/Releases/data/lw13_build_stage.tsx +++ b/apps/www/components/LaunchWeek/13/Releases/data/lw13_build_stage.tsx @@ -1,8 +1,8 @@ // see apps/www/components/LaunchWeek/X/Releases/data/lwx_advent_days.tsx for reference -import { type ClassValue } from 'clsx' import { BookCopy, Flag, GitBranch, HardDrive } from 'lucide-react' import { ReactNode } from 'react' +import type { ClassValue } from 'ui' export interface AdventDay { icon?: ReactNode // use svg jsx with 34x34px viewport diff --git a/apps/www/components/LaunchWeek/14/Releases/data/lw14_build_stage.tsx b/apps/www/components/LaunchWeek/14/Releases/data/lw14_build_stage.tsx index 50435fa5c6f..59ab5636e6b 100644 --- a/apps/www/components/LaunchWeek/14/Releases/data/lw14_build_stage.tsx +++ b/apps/www/components/LaunchWeek/14/Releases/data/lw14_build_stage.tsx @@ -1,9 +1,9 @@ // see apps/www/components/LaunchWeek/13/Releases/data/lw13_build_stage.tsx for reference -import { type ClassValue } from 'clsx' import { AppWindow, Database, Globe } from 'lucide-react' import { ReactNode } from 'react' import { PRODUCT_MODULES } from 'shared-data/products' +import type { ClassValue } from 'ui' export interface AdventDay { icon?: ReactNode // use svg jsx with 34x34px viewport diff --git a/apps/www/components/LaunchWeek/15/data/lw15_build_stage.tsx b/apps/www/components/LaunchWeek/15/data/lw15_build_stage.tsx index ced89da85d5..39d6d2406fa 100644 --- a/apps/www/components/LaunchWeek/15/data/lw15_build_stage.tsx +++ b/apps/www/components/LaunchWeek/15/data/lw15_build_stage.tsx @@ -1,5 +1,5 @@ -import type { ClassValue } from 'clsx' import type { ReactNode } from 'react' +import type { ClassValue } from 'ui' export interface BuildDay { icon?: ReactNode // use svg jsx with 34x34px viewport diff --git a/apps/www/package.json b/apps/www/package.json index 2dbec8f9552..bdc93fa0681 100644 --- a/apps/www/package.json +++ b/apps/www/package.json @@ -44,7 +44,6 @@ "animejs": "^4.0.2", "class-variance-authority": "^0.7.1", "classnames": "^2.3.1", - "clsx": "^2.1.1", "cobe": "^0.6.5", "common": "workspace:*", "common-tags": "^1.8.2", diff --git a/blocks/vue/package.json b/blocks/vue/package.json index 4ca3008fd4c..fa47f83d062 100644 --- a/blocks/vue/package.json +++ b/blocks/vue/package.json @@ -15,11 +15,10 @@ "@supabase/supabase-js": "catalog:", "@vueuse/core": "^14.1.0", "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", + "cn": "^0.2.4", "h3": "^1.15.10", "lucide-vue-next": "^0.562.0", "nuxt": "^4.4.6", - "tailwind-merge": "^3.5.0", "vue": "^3.5.35", "vue-router": "^4.5.1" }, diff --git a/blocks/vue/registry/default/lib/utils.ts b/blocks/vue/registry/default/lib/utils.ts index abba253f04c..ae9e5a7c376 100644 --- a/blocks/vue/registry/default/lib/utils.ts +++ b/blocks/vue/registry/default/lib/utils.ts @@ -1,7 +1 @@ -import type { ClassValue } from 'clsx' -import { clsx } from 'clsx' -import { twMerge } from 'tailwind-merge' - -export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)) -} +export { cn } from 'cn' diff --git a/packages/ui-patterns/package.json b/packages/ui-patterns/package.json index 9acb4eed9a0..dd40894f18c 100644 --- a/packages/ui-patterns/package.json +++ b/packages/ui-patterns/package.json @@ -774,7 +774,6 @@ "@supabase/supabase-js": "catalog:", "@tanstack/react-table": "catalog:", "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", "cmdk": "^1.1.1", "common": "workspace:*", "common-tags": "^1.8.2", diff --git a/packages/ui/README.md b/packages/ui/README.md index 02a7da1ac4a..999e7ad9dd2 100644 --- a/packages/ui/README.md +++ b/packages/ui/README.md @@ -16,7 +16,7 @@ Some of the components have the `_Shadcn_` suffix. These components should be pr ```tsx // deep object merge (used for themes) -import { clipboard, cn, mergeDeep } from 'ui' // clsx + tailwind-merge +import { clipboard, cn, mergeDeep } from 'ui' // Tailwind class merging (shadcn-ui/cn) // copy-to-clipboard helper ``` diff --git a/packages/ui/package.json b/packages/ui/package.json index f87bfb5a4a5..676ce5ee370 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -18,7 +18,7 @@ "@tailwindcss/forms": "^0.5.11", "@tailwindcss/typography": "^0.5.19", "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", + "cn": "^0.2.4", "cmdk": "^1.1.1", "color": "^4.2.3", "date-fns": "^2.30.0", @@ -38,7 +38,6 @@ "react-resizable-panels": "^4.6.5", "recharts": "catalog:", "sonner": "^1.5.0", - "tailwind-merge": "^3.5.0", "tailwindcss": "catalog:", "vaul": "^1.1.2" }, diff --git a/packages/ui/src/lib/utils/cn.test.ts b/packages/ui/src/lib/utils/cn.test.ts new file mode 100644 index 00000000000..eb625823025 --- /dev/null +++ b/packages/ui/src/lib/utils/cn.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from 'vitest' + +import { cn } from './cn' + +describe('cn', () => { + it('joins conditional inputs like clsx', () => { + expect(cn('a', ['b', { c: true, d: false }], undefined, null, false && 'e')).toBe('a b c') + }) + + it('resolves conflicting Tailwind utilities, last one wins', () => { + expect(cn('p-2 text-foreground', 'p-4', 'text-foreground-light')).toBe( + 'p-4 text-foreground-light' + ) + expect(cn('h-full', 'h-96')).toBe('h-96') + }) + + it('treats the custom card/content spacing scale as spacing utilities', () => { + expect(cn('p-4', 'p-card')).toBe('p-card') + expect(cn('px-content', 'px-6')).toBe('px-6') + expect(cn('gap-2', 'gap-card')).toBe('gap-card') + }) + + it('keeps variants separate from base utilities', () => { + expect(cn('p-2 hover:p-4', 'md:p-6')).toBe('p-2 hover:p-4 md:p-6') + }) +}) diff --git a/packages/ui/src/lib/utils/cn.ts b/packages/ui/src/lib/utils/cn.ts index a2a546b06c5..bab7d6b4646 100644 --- a/packages/ui/src/lib/utils/cn.ts +++ b/packages/ui/src/lib/utils/cn.ts @@ -1,14 +1,12 @@ -import { ClassValue, clsx } from 'clsx' -import { extendTailwindMerge } from 'tailwind-merge' +import type { ClassValue } from 'cn' +import { createCn } from 'cn/config' -const twMerge = extendTailwindMerge({ +export type { ClassValue } from 'cn' + +export const cn: (...inputs: ClassValue[]) => string = createCn({ extend: { theme: { spacing: ['card', 'content'], }, }, }) - -export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)) -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5176ef80536..619b39d6aa7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1787,9 +1787,6 @@ importers: classnames: specifier: ^2.3.1 version: 2.3.2 - clsx: - specifier: ^2.1.1 - version: 2.1.1 cobe: specifier: ^0.6.5 version: 0.6.5 @@ -2058,9 +2055,9 @@ importers: class-variance-authority: specifier: ^0.7.1 version: 0.7.1 - clsx: - specifier: ^2.1.1 - version: 2.1.1 + cn: + specifier: ^0.2.4 + version: 0.2.5 h3: specifier: 1.15.11 version: 1.15.11 @@ -2070,9 +2067,6 @@ importers: nuxt: specifier: ^4.4.6 version: 4.5.2(@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.29.7(supports-color@8.1.1)))(@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@8.1.1)))(@electric-sql/pglite@0.4.5)(@oxc-project/types@0.143.0)(@parcel/watcher@2.5.6)(@types/node@22.13.14)(@vue/compiler-sfc@3.5.41)(aws4fetch@1.0.20)(cac@6.7.14)(commander@14.0.1)(db0@0.3.4(@electric-sql/pglite@0.4.5))(encoding@0.1.13)(esbuild@0.28.1)(eslint@9.37.0(jiti@2.7.0)(supports-color@8.1.1))(ioredis@5.10.1(supports-color@8.1.1))(lightningcss@1.33.0)(magicast@0.5.4)(oxc-parser@0.143.0)(rolldown@1.2.3)(rollup-plugin-visualizer@7.0.1(rolldown@1.2.3)(rollup@4.60.3))(rollup@4.60.3)(sass@1.77.4)(supports-color@8.1.1)(terser@5.48.0)(tsx@4.22.4)(typescript@6.0.2)(vite@7.3.5(@types/node@22.13.14)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.77.4)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0))(webpack@5.105.4(esbuild@0.28.1))(yaml@2.9.0) - tailwind-merge: - specifier: ^3.5.0 - version: 3.5.0 vue: specifier: ^3.5.35 version: 3.5.35(typescript@6.0.2) @@ -2619,12 +2613,12 @@ importers: class-variance-authority: specifier: ^0.7.1 version: 0.7.1 - clsx: - specifier: ^2.1.1 - version: 2.1.1 cmdk: specifier: ^1.1.1 version: 1.1.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + cn: + specifier: ^0.2.4 + version: 0.2.5 color: specifier: ^4.2.3 version: 4.2.3 @@ -2679,9 +2673,6 @@ importers: sonner: specifier: ^1.5.0 version: 1.5.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - tailwind-merge: - specifier: ^3.5.0 - version: 3.5.0 tailwindcss: specifier: 'catalog:' version: 4.2.4 @@ -2761,9 +2752,6 @@ importers: class-variance-authority: specifier: ^0.7.1 version: 0.7.1 - clsx: - specifier: ^2.1.1 - version: 2.1.1 cmdk: specifier: ^1.1.1 version: 1.1.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) @@ -10388,6 +10376,11 @@ packages: react: ^18 || ^19 || ^19.0.0-rc react-dom: ^18 || ^19 || ^19.0.0-rc + cn@0.2.5: + resolution: {integrity: sha512-OCjZtMeQfXbI4Es1+EIjkd77gvWzaE689gD8KhfexlqjClC06qR1MQBR+Z35ZMSPNEBWyHiItW1Soy0UvwNv9w==} + engines: {node: '>=20'} + hasBin: true + cobe@0.6.5: resolution: {integrity: sha512-MA8bu81EFY6JjQpj+FovEuhyJ25khx2Q7Lh+ot/UkCJe5yKyDgzdc6u2lGZIOmsZTXK6Itg1i4lQZIJZbPWnAg==} @@ -27335,6 +27328,8 @@ snapshots: - '@types/react' - '@types/react-dom' + cn@0.2.5: {} + cobe@0.6.5: dependencies: phenomenon: 1.6.0