Style v2 with tailwind-variants/lite
The numbered v2 token scales (text-1…9, rounded-1…6, shadow-1…6) share a prefix with the color/utility namespaces, and tailwind-merge (which tailwind-variants runs by default) cannot tell a numeric text-3 from a color step, so it silently dropped the size whenever a text-<color> was present. Switch the v2 kit to the merge-free tailwind-variants/lite entrypoint (added in v3): upgrade tailwind-variants to ^3.2.2, import tv/VariantProps from tailwind-variants/lite in the typography primitives, and drop the per-call twMerge:false. The legacy v1 kit stays on tailwind-variants (merge intact). Document the rationale in ui.md and add a packages/ui/src/v2 ESLint guard that forbids tailwind-variants, tailwind-merge, and clsx. Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
@@ -20,7 +20,7 @@ These rules are the **source of truth**. The legacy tree (`Atoms/`, `Molecules/`
|
||||
|------|------------|
|
||||
| Package | **`@probo/ui`** — v2 components under `src/v2`. Apps opt into v2 by importing the v2 theme (see [`v2-tokens.md`](v2-tokens.md)). |
|
||||
| Styling | **Tailwind v4** with the Radix-scale tokens (`bg-sand-3`, `text-sand-12`, `rounded-3`, `text-4`, …). |
|
||||
| Variants API | **`tailwind-variants`** only — `import { tv } from "tailwind-variants"`. |
|
||||
| Variants API | **`tailwind-variants/lite`** only — `import { tv } from "tailwind-variants/lite"`. The `/lite` entrypoint ships **without `tailwind-merge`**, which is required: the numbered scales (`text-1…9`, `rounded-1…6`, `shadow-1…6`) collide with the color/utility namespaces and tailwind-merge would silently drop the scale class (e.g. `text-3` next to `text-sand-11`). The legacy v1 kit stays on `tailwind-variants` (with merge). |
|
||||
| Class composition | **Do not use `clsx` or `tailwind-merge`.** All conditional styling goes through `tv` variants and slots. |
|
||||
| Headless primitives | **Base UI** (`@base-ui/react`). We **style** these primitives; we do not re-implement their behavior. |
|
||||
|
||||
@@ -58,7 +58,7 @@ export function Dialog({ trigger, ref, children }: Props) {
|
||||
```tsx
|
||||
// Good — thin styling over Base UI; consumers use the lib's open/onOpenChange directly
|
||||
import { Dialog as BaseDialog } from "@base-ui/react/dialog";
|
||||
import { tv } from "tailwind-variants";
|
||||
import { tv } from "tailwind-variants/lite";
|
||||
|
||||
const dialog = tv({
|
||||
slots: {
|
||||
@@ -111,7 +111,7 @@ For **compound / multi-slot** components, define `tv` in a **dedicated `variants
|
||||
```tsx
|
||||
// Bad — same file mixes tv() output with ad-hoc Tailwind / clsx on className
|
||||
import { clsx } from "clsx";
|
||||
import { tv } from "tailwind-variants";
|
||||
import { tv } from "tailwind-variants/lite";
|
||||
|
||||
const row = tv({ base: "flex items-center gap-2" });
|
||||
export function Row({ children }: { children: ReactNode }) {
|
||||
@@ -121,7 +121,7 @@ export function Row({ children }: { children: ReactNode }) {
|
||||
|
||||
```tsx
|
||||
// Good — layout and look live in tv
|
||||
import { tv } from "tailwind-variants";
|
||||
import { tv } from "tailwind-variants/lite";
|
||||
|
||||
const row = tv({
|
||||
base: "flex items-center gap-2 rounded-3 border border-sand-6",
|
||||
@@ -133,7 +133,7 @@ export function Row({ children }: { children: ReactNode }) {
|
||||
|
||||
```tsx
|
||||
// Good — optional styling toggles use tv variants, not extra className strings
|
||||
import { tv } from "tailwind-variants";
|
||||
import { tv } from "tailwind-variants/lite";
|
||||
|
||||
const row = tv({
|
||||
base: "flex items-center gap-2",
|
||||
@@ -398,7 +398,7 @@ Model regions with `tv` `slots` named after the layout:
|
||||
|
||||
```ts
|
||||
// ImageCard/variants.ts
|
||||
import { tv } from "tailwind-variants";
|
||||
import { tv } from "tailwind-variants/lite";
|
||||
|
||||
export const imageCard = tv({
|
||||
slots: {
|
||||
|
||||
@@ -68,6 +68,34 @@ export default defineConfig([
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
// The v2 kit styles with tailwind-variants/lite (no tailwind-merge): the
|
||||
// numbered scales (text-1…9, rounded-1…6, shadow-1…6) collide with the
|
||||
// color/utility namespaces and tailwind-merge would drop the scale class.
|
||||
files: ["packages/ui/src/v2/**"],
|
||||
rules: {
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
paths: [
|
||||
{
|
||||
name: "tailwind-variants",
|
||||
message:
|
||||
"Import from tailwind-variants/lite (no tailwind-merge) in v2.",
|
||||
},
|
||||
{
|
||||
name: "tailwind-merge",
|
||||
message: "The v2 kit does not use tailwind-merge.",
|
||||
},
|
||||
{
|
||||
name: "clsx",
|
||||
message: "The v2 kit does not use clsx; style via tailwind-variants/lite.",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: reactDirs,
|
||||
ignores: ["packages/ui/tailwind.config.js"],
|
||||
|
||||
27
package-lock.json
generated
27
package-lock.json
generated
@@ -18989,29 +18989,22 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tailwind-variants": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tailwind-variants/-/tailwind-variants-1.0.0.tgz",
|
||||
"integrity": "sha512-2WSbv4ulEEyuBKomOunut65D8UZwxrHoRfYnxGcQNnHqlSCp2+B7Yz2W+yrNDrxRodOXtGD/1oCcKGNBnUqMqA==",
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/tailwind-variants/-/tailwind-variants-3.2.2.tgz",
|
||||
"integrity": "sha512-Mi4kHeMTLvKlM98XPnK+7HoBPmf4gygdFmqQPaDivc3DpYS6aIY6KiG/PgThrGvii5YZJqRsPz0aPyhoFzmZgg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tailwind-merge": "3.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.x",
|
||||
"pnpm": ">=7.x"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"tailwind-merge": ">=3.0.0",
|
||||
"tailwindcss": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwind-variants/node_modules/tailwind-merge": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.0.2.tgz",
|
||||
"integrity": "sha512-l7z+OYZ7mu3DTqrL88RiKrKIqO3NcpEO8V/Od04bNpvk0kiIFndGEoqfuzvj4yuhRkHKjRkII2z+KS2HfPcSxw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/dcastil"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"tailwind-merge": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
@@ -21497,7 +21490,7 @@
|
||||
"rehype-raw": "^7.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwind-variants": "^1.0.0",
|
||||
"tailwind-variants": "^3.2.2",
|
||||
"tailwindcss": "^4.3.1",
|
||||
"tw-animate-css": "^1.3.0",
|
||||
"zustand": "^5.0.4"
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
"rehype-raw": "^7.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwind-variants": "^1.0.0",
|
||||
"tailwind-variants": "^3.2.2",
|
||||
"tailwindcss": "^4.3.1",
|
||||
"tw-animate-css": "^1.3.0",
|
||||
"zustand": "^5.0.4"
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
import type { ComponentProps } from "react";
|
||||
import type { VariantProps } from "tailwind-variants";
|
||||
import type { VariantProps } from "tailwind-variants/lite";
|
||||
|
||||
import { text } from "./variants";
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
import type { ComponentProps } from "react";
|
||||
import type { VariantProps } from "tailwind-variants";
|
||||
import type { VariantProps } from "tailwind-variants/lite";
|
||||
|
||||
import { textSkeleton } from "./variants";
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
import { tv } from "tailwind-variants";
|
||||
import { tv } from "tailwind-variants/lite";
|
||||
|
||||
// Numbered type scale (text-1 … text-9). Each utility carries its paired
|
||||
// font-size, line-height, and letter-spacing from the v2 theme. Shared between
|
||||
|
||||
Reference in New Issue
Block a user