Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feat]: Dynamic add new variants #5420

Open
2 tasks done
doiya46 opened this issue Oct 18, 2024 · 0 comments
Open
2 tasks done

[feat]: Dynamic add new variants #5420

doiya46 opened this issue Oct 18, 2024 · 0 comments

Comments

@doiya46
Copy link

doiya46 commented Oct 18, 2024

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

  • I've made research efforts and searched the documentation
  • I've searched for existing issues and PRs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant