shadcn-ui / ui

Beautifully designed components that you can copy and paste into your apps. Accessible. Customizable. Open Source.
https://ui.shadcn.com
MIT License
72.23k stars 4.36k forks source link

[feat]: Dynamic add new variants #5420

Open doiya46 opened 1 day ago

doiya46 commented 1 day ago

Feature description

Problem

Currently some component have fixed variant. For example: const badgeVariants = cva(, const buttonVariants = cva( so I find some solution to add dynamic variant with minimum cost to modify ShadCN UI code.

I don't know if this is a good idea before proceeding to modify the code and create a pull request.

This solution need update any component in /components/ui that used = cva(. So the end-user (developer) cant override variant by update ONLY 1 file custom-vars.ts. It need shadcn tool (npx shadcn@latest) create/update (question) /components/ui/custom-vars.ts if file not exits.

This is example for update component button

Add merge variant function / customVars

For example customVars.button is override button variants. I use chat GPT, so maybe _deepMerge fn work not correctly.

// /src/components/ui/custom-vars.ts
type GetObjDifferentKeys<
  T,
  U,
  T0 = Omit<T, keyof U> & Omit<U, keyof T>,
  T1 = {
    [K in keyof T0]: T0[K];
  },
> = T1;

type GetObjSameKeys<T, U> = Omit<T | U, keyof GetObjDifferentKeys<T, U>>;

type MergeTwoObjects<
  T,
  U,
  T0 = GetObjDifferentKeys<T, U> & { [K in keyof GetObjSameKeys<T, U>]: DeepMergeTwoTypes<T[K], U[K]> },
  T1 = { [K in keyof T0]: T0[K] },
> = T1;

export type DeepMergeTwoTypes<T, U> = [T, U] extends [{ [key: string]: unknown }, { [key: string]: unknown }]
  ? MergeTwoObjects<NonNullable<T>, NonNullable<U>>
  : NonNullable<T> | NonNullable<U>;

function _deepMerge<T extends object, U extends object>(target: T, source: U): DeepMergeTwoTypes<T, U> {
  for (const key of Object.keys(source) as Array<keyof U>) {
    if (source[key] instanceof Object && key in target) {
      (target as any)[key] = _deepMerge((target as any)[key], source[key] as any);
    } else {
      (target as any)[key] = source[key];
    }
  }

  return target as any;
}

export function mergeVariants<T, U>(baseConfig: T, customConfig: U): DeepMergeTwoTypes<T, U> {
  return _deepMerge(baseConfig as any, customConfig as any) as any;
}

export const customVars = {
  button: {
    variants: {
      variant: {
        success: 'bg-success text-white hover:bg-success/80',
      },
    },
  },
};

Modify button.tsx

import { cn } from '@/lib/utils';
import { Slot } from '@radix-ui/react-slot';
import { cva, type VariantProps } from 'class-variance-authority';
import * as React from 'react';

// UPDATE: Import fn
import { customVars, mergeVariants } from './custom-vars';

const buttonVariants = cva(
  'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
  // UPDATED: do mergeVariants
  mergeVariants(
    {
      variants: {
        variant: {
          default: 'bg-primary text-primary-foreground hover:bg-primary/90',
          destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
          outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
          secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
          ghost: 'hover:bg-accent hover:text-accent-foreground',
          link: 'text-primary underline-offset-4 hover:underline',
        },
        size: {
          default: 'h-10 px-4 py-2',
          sm: 'h-9 rounded-md px-3',
          lg: 'h-11 rounded-md px-8',
          icon: 'h-10 w-10',
        },
      },
      defaultVariants: {
        variant: 'default',
        size: 'default',
      },
    },
    customVars.button || {},
  ),
);

Usage button

Expected: Code should not throw error for variant='success'

<Button variant='success'>Success</Button>

Update CSS

@layer base {
  :root {
    /* Define new variables */   
    --success: 100 77% 44%;
    --success-foreground: 102 85% 34%;
  }
}

Update tailwind.config.js

/** @type {import('tailwindcss').Config} */
export default {
  theme: {
    extend: {
      colors: {

        success: {
          DEFAULT: 'hsl(var(--success))',
          foreground: 'hsl(var(--success-foreground))',
        },
      }
    }
  }
}

Affected component/components

Alert, Badge, Button, Label, Sheet, Toast, Toggle

Additional Context

Additional details here...

Before submitting

victorhs98 commented 15 hours ago

i need this

imMatheus commented 15 hours ago

You can just change the variants in the cva, you own the code after you install a component, thats the point of ShadcnUI.