mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 01:45:10 +03:00
chore: migrate from clsx + tailwind-merge to shadcn-ui/cn (#49938)
Migrates the repo off `clsx` + `tailwind-merge` to [shadcn-ui/cn](https://github.com/shadcn-ui/cn). Every app and package already gets `cn` from `packages/ui`, so the swap happens in that one helper and flows through to Studio, docs, www, and the rest. **Changed:** - `packages/ui` `cn` helper now uses `createCn` from `cn/config`, keeping the custom `card`/`content` spacing scale so `p-card` still overrides `p-4`. It has an explicit signature and re-exports `ClassValue`. - The four www Launch Week files that imported the `ClassValue` type from `clsx` now import it from `ui`. - `blocks/vue` local `lib/utils.ts` re-exports `cn` from the package. - Comments/README that referenced tailwind-merge. **Removed:** - Direct `clsx` and `tailwind-merge` deps from `ui`, `ui-patterns`, `www`, and `blocks/vue`. `ui-patterns` and `www` declared them without importing. **Added:** - `packages/ui/src/lib/utils/cn.test.ts` covering clsx-style joining, conflict resolution, the custom spacing scale, and variant handling. Not migrated: the standalone apps under `examples/`. They're outside the workspace and mostly on Tailwind v3, which `cn` doesn't support. Lockfile note: after merging master, the lockfile diff is only the intended swap (`clsx` and `tailwind-merge` out, `cn@0.2.5` in). `tailwind-merge` stays in the lockfile as a transitive dep of a third-party package. Release-age note: this sat in draft with a temporary `minimumReleaseAgeExclude` entry for `cn` while `cn` was inside the workspace's 3-day `minimumReleaseAge` window. That window has closed, so the exclusion is gone and nothing bypasses the release-age gate. ## To test - `pnpm install --frozen-lockfile` succeeds with no `minimumReleaseAgeExclude` entry for `cn`. - `pnpm --filter ui test` – new `cn.test.ts` passes, including `cn('p-4', 'p-card')` → `p-card`. - Typecheck passes for studio, ui, ui-patterns, vue-blocks. www typecheck panics under tsgo on master already (pre-existing, unrelated); it passes with the JS `tsc` binary. - Spot-check Studio locally: class overrides still win in the usual places (e.g. `CodeEditor` height, `Button` variants with a custom `className`). https://claude.ai/code/session_01MkAt16tsPRDTm9oB5Jr8Ub <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Improvements** * Standardized Tailwind class merging across shared UI utilities while preserving conditional classes, custom spacing classes, and variant behavior. * Updated related components and examples to use the standardized class-merging utility. * **Tests** * Added coverage for conditional class handling, conflicting utility resolution, custom spacing classes, and variant separation. * **Documentation** * Updated usage guidance to reflect the standardized Tailwind class-merging approach. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Alaister Young <10985857+alaister@users.noreply.github.com>
This commit is contained in:
1 parent
b0601f9ab7
commit
ec1029dff0
15 files changed
+54
-45
No files matched your search
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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'
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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))
|
||||
}
|
||||
Generated
+13
-18
@@ -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
|
||||
|
||||
Reference in new issue
Block a user