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(theme): improving component customization using twMerge #800

Closed
wants to merge 15 commits into from
Closed
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,10 @@
},
"dependencies": {
"@floating-ui/react": "^0.24.1",
"classnames": "^2.3.2",
"flowbite": "^1.6.5",
"react-icons": "^4.8.0",
"react-indiana-drag-scroll": "^2.2.0"
"react-indiana-drag-scroll": "^2.2.0",
"tailwind-merge": "^1.13.1"
},
"devDependencies": {
"@mdx-js/loader": "^2.3.0",
Expand Down
3 changes: 2 additions & 1 deletion src/components/Accordion/Accordion.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,8 @@ describe('Components / Accordion', () => {
expect(title).toHaveClass('text-3xl');
});
openTitles.forEach((title) => {
expect(title).toHaveClass('text-gray-600');
// Note: it is being overwrited by the className prop which is expected
expect(title).toHaveClass('text-cyan-300');
});
closedTitles.forEach((title) => {
expect(title).toHaveClass('text-gray-400');
Expand Down
4 changes: 2 additions & 2 deletions src/components/Accordion/Accordion.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import classNames from 'classnames';
import type { ComponentProps, FC, PropsWithChildren, ReactElement } from 'react';
import { Children, cloneElement, useMemo, useState } from 'react';
import { HiChevronDown } from 'react-icons/hi';
import { twMerge } from 'tailwind-merge';
import type { DeepPartial, FlowbiteBoolean } from '../../';
import { useTheme } from '../../';
import { mergeDeep } from '../../helpers/merge-deep';
Expand Down Expand Up @@ -62,7 +62,7 @@ const AccordionComponent: FC<AccordionProps> = ({

return (
<div
className={classNames(theme.base, theme.flush[flush ? 'on' : 'off'], className)}
className={twMerge(theme.base, theme.flush[flush ? 'on' : 'off'], className)}
data-testid="flowbite-accordion"
{...props}
>
Expand Down
4 changes: 2 additions & 2 deletions src/components/Accordion/AccordionContent.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import classNames from 'classnames';
import type { ComponentProps, FC, PropsWithChildren } from 'react';
import { twMerge } from 'tailwind-merge';
import type { DeepPartial } from '../../';
import { useTheme } from '../../';
import { mergeDeep } from '../../helpers/merge-deep';
Expand All @@ -25,7 +25,7 @@ export const AccordionContent: FC<AccordionContentProps> = ({

return (
<div
className={classNames(theme.base, className)}
className={twMerge(theme.base, className)}
data-testid="flowbite-accordion-content"
hidden={!isOpen}
{...props}
Expand Down
11 changes: 3 additions & 8 deletions src/components/Accordion/AccordionTitle.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import classNames from 'classnames';
import type { ComponentProps, FC } from 'react';
import { twMerge } from 'tailwind-merge';
import type { DeepPartial, FlowbiteBoolean, FlowbiteHeadingLevel } from '../../';
import { useTheme } from '../../';
import { mergeDeep } from '../../helpers/merge-deep';
Expand Down Expand Up @@ -36,12 +36,7 @@ export const AccordionTitle: FC<AccordionTitleProps> = ({

return (
<button
className={classNames(
theme.base,
theme.flush[flush ? 'on' : 'off'],
theme.open[isOpen ? 'on' : 'off'],
className,
)}
className={twMerge(theme.base, theme.flush[flush ? 'on' : 'off'], theme.open[isOpen ? 'on' : 'off'], className)}
onClick={onClick}
type="button"
{...props}
Expand All @@ -52,7 +47,7 @@ export const AccordionTitle: FC<AccordionTitleProps> = ({
{ArrowIcon && (
<ArrowIcon
aria-hidden
className={classNames(theme.arrow.base, theme.arrow.open[isOpen ? 'on' : 'off'])}
className={twMerge(theme.arrow.base, theme.arrow.open[isOpen ? 'on' : 'off'])}
data-testid="flowbite-accordion-arrow"
/>
)}
Expand Down
33 changes: 33 additions & 0 deletions src/components/Accordion/theme.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { FlowbiteAccordionTheme } from './Accordion';

export const accordionTheme: FlowbiteAccordionTheme = {
root: {
base: 'divide-y divide-gray-200 border-gray-200 dark:divide-gray-700 dark:border-gray-700',
flush: {
off: 'rounded-lg border',
on: 'border-b',
},
},
content: {
base: 'py-5 px-5 last:rounded-b-lg dark:bg-gray-900 first:rounded-t-lg',
},
title: {
arrow: {
base: 'h-6 w-6 shrink-0',
open: {
off: '',
on: 'rotate-180',
},
},
base: 'flex w-full items-center justify-between first:rounded-t-lg last:rounded-b-lg py-5 px-5 text-left font-medium text-gray-500 dark:text-gray-400',
flush: {
off: 'hover:bg-gray-100 focus:ring-4 focus:ring-gray-200 dark:hover:bg-gray-800 dark:focus:ring-gray-800',
on: 'bg-transparent dark:bg-transparent',
},
heading: '',
open: {
off: '',
on: 'text-gray-900 bg-gray-100 dark:bg-gray-800 dark:text-white',
},
},
};
4 changes: 3 additions & 1 deletion src/components/Alert/Alert.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ describe.concurrent('Components / Alert', () => {
it('should use custom `base` classes', () => {
const theme = {
alert: {
base: 'text-purple-100',
color: {
info: 'text-purple-100',
},
},
};
render(
Expand Down
6 changes: 3 additions & 3 deletions src/components/Alert/Alert.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import classNames from 'classnames';
import type { ComponentProps, FC, PropsWithChildren, ReactNode } from 'react';
import { HiX } from 'react-icons/hi';
import { twMerge } from 'tailwind-merge';
import type { DeepPartial, FlowbiteColors } from '../../';
import { useTheme } from '../../';
import { mergeDeep } from '../../helpers/merge-deep';
Expand Down Expand Up @@ -47,7 +47,7 @@ export const Alert: FC<AlertProps> = ({

return (
<div
className={classNames(
className={twMerge(
theme.base,
theme.color[color],
rounded && theme.rounded,
Expand All @@ -63,7 +63,7 @@ export const Alert: FC<AlertProps> = ({
{typeof onDismiss === 'function' && (
<button
aria-label="Dismiss"
className={classNames(theme.closeButton.base, theme.closeButton.color[color])}
className={twMerge(theme.closeButton.base, theme.closeButton.color[color])}
onClick={onDismiss}
type="button"
>
Expand Down
59 changes: 59 additions & 0 deletions src/components/Alert/theme.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type { FlowbiteAlertTheme } from './Alert';

export const alertTheme: FlowbiteAlertTheme = {
base: 'flex flex-col gap-2 p-4 text-sm',
borderAccent: 'border-t-4',
closeButton: {
base: '-mx-1.5 -my-1.5 ml-auto inline-flex h-8 w-8 rounded-lg p-1.5 focus:ring-2',
icon: 'w-5 h-5',
color: {
info: 'bg-cyan-100 text-cyan-500 hover:bg-cyan-200 focus:ring-cyan-400 dark:bg-cyan-200 dark:text-cyan-600 dark:hover:bg-cyan-300',
gray: 'bg-gray-100 text-gray-500 hover:bg-gray-200 focus:ring-gray-400 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-white',
failure:
'bg-red-100 text-red-500 hover:bg-red-200 focus:ring-red-400 dark:bg-red-200 dark:text-red-600 dark:hover:bg-red-300',
success:
'bg-green-100 text-green-500 hover:bg-green-200 focus:ring-green-400 dark:bg-green-200 dark:text-green-600 dark:hover:bg-green-300',
warning:
'bg-yellow-100 text-yellow-500 hover:bg-yellow-200 focus:ring-yellow-400 dark:bg-yellow-200 dark:text-yellow-600 dark:hover:bg-yellow-300',
red: 'bg-red-100 text-red-500 hover:bg-red-200 focus:ring-red-400 dark:bg-red-200 dark:text-red-600 dark:hover:bg-red-300',
green:
'bg-green-100 text-green-500 hover:bg-green-200 focus:ring-green-400 dark:bg-green-200 dark:text-green-600 dark:hover:bg-green-300',
yellow:
'bg-yellow-100 text-yellow-500 hover:bg-yellow-200 focus:ring-yellow-400 dark:bg-yellow-200 dark:text-yellow-600 dark:hover:bg-yellow-300',
blue: 'bg-cyan-100 text-cyan-500 hover:bg-cyan-200 focus:ring-cyan-400 dark:bg-cyan-200 dark:text-cyan-600 dark:hover:bg-cyan-300',
cyan: 'bg-cyan-100 text-cyan-500 hover:bg-cyan-200 focus:ring-cyan-400 dark:bg-cyan-200 dark:text-cyan-600 dark:hover:bg-cyan-300',
pink: 'bg-pink-100 text-pink-500 hover:bg-pink-200 focus:ring-pink-400 dark:bg-pink-200 dark:text-pink-600 dark:hover:bg-pink-300',
lime: 'bg-lime-100 text-lime-500 hover:bg-lime-200 focus:ring-lime-400 dark:bg-lime-200 dark:text-lime-600 dark:hover:bg-lime-300',
dark: 'bg-gray-100 text-gray-500 hover:bg-gray-200 focus:ring-gray-400 dark:bg-gray-200 dark:text-gray-600 dark:hover:bg-gray-300',
indigo:
'bg-indigo-100 text-indigo-500 hover:bg-indigo-200 focus:ring-indigo-400 dark:bg-indigo-200 dark:text-indigo-600 dark:hover:bg-indigo-300',
purple:
'bg-purple-100 text-purple-500 hover:bg-purple-200 focus:ring-purple-400 dark:bg-purple-200 dark:text-purple-600 dark:hover:bg-purple-300',
teal: 'bg-teal-100 text-teal-500 hover:bg-teal-200 focus:ring-teal-400 dark:bg-teal-200 dark:text-teal-600 dark:hover:bg-teal-300',
light:
'bg-gray-50 text-gray-500 hover:bg-gray-100 focus:ring-gray-200 dark:bg-gray-600 dark:text-gray-200 dark:hover:bg-gray-700 dark:hover:text-white',
},
},
color: {
info: 'text-cyan-700 bg-cyan-100 border-cyan-500 dark:bg-cyan-200 dark:text-cyan-800',
gray: 'text-gray-700 bg-gray-100 border-gray-500 dark:bg-gray-700 dark:text-gray-300',
failure: 'text-red-700 bg-red-100 border-red-500 dark:bg-red-200 dark:text-red-800',
success: 'text-green-700 bg-green-100 border-green-500 dark:bg-green-200 dark:text-green-800',
warning: 'text-yellow-700 bg-yellow-100 border-yellow-500 dark:bg-yellow-200 dark:text-yellow-800',
red: 'text-red-700 bg-red-100 border-red-500 dark:bg-red-200 dark:text-red-800',
green: 'text-green-700 bg-green-100 border-green-500 dark:bg-green-200 dark:text-green-800',
yellow: 'text-yellow-700 bg-yellow-100 border-yellow-500 dark:bg-yellow-200 dark:text-yellow-800',
blue: 'text-cyan-700 bg-cyan-100 border-cyan-500 dark:bg-cyan-200 dark:text-cyan-800',
cyan: 'text-cyan-700 bg-cyan-100 border-cyan-500 dark:bg-cyan-200 dark:text-cyan-800',
pink: 'text-pink-700 bg-pink-100 border-pink-500 dark:bg-pink-200 dark:text-pink-800',
lime: 'text-lime-700 bg-lime-100 border-lime-500 dark:bg-lime-200 dark:text-lime-800',
dark: 'text-gray-200 bg-gray-800 border-gray-600 dark:bg-gray-900 dark:text-gray-300',
indigo: 'text-indigo-700 bg-indigo-100 border-indigo-500 dark:bg-indigo-200 dark:text-indigo-800',
purple: 'text-purple-700 bg-purple-100 border-purple-500 dark:bg-purple-200 dark:text-purple-800',
teal: 'text-teal-700 bg-teal-100 border-teal-500 dark:bg-teal-200 dark:text-teal-800',
light: 'text-gray-600 bg-gray-50 border-gray-400 dark:bg-gray-500 dark:text-gray-200',
},
icon: 'mr-3 inline h-5 w-5 flex-shrink-0',
rounded: 'rounded-lg',
wrapper: 'flex items-center',
};
23 changes: 11 additions & 12 deletions src/components/Avatar/Avatar.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import classNames from 'classnames';
import type { ComponentProps, FC, PropsWithChildren, ReactElement } from 'react';
import { twMerge } from 'tailwind-merge';
import type { DeepPartial, FlowbiteBoolean, FlowbiteColors, FlowbitePositions, FlowbiteSizes } from '../../';
import { useTheme } from '../../';
import { mergeDeep } from '../../helpers/merge-deep';
Expand Down Expand Up @@ -28,6 +28,7 @@ export interface FlowbiteAvatarRootTheme {
}

export interface FlowbiteAvatarImageTheme extends FlowbiteBoolean {
base: string;
placeholder: string;
}

Expand Down Expand Up @@ -91,7 +92,8 @@ const AvatarComponent: FC<AvatarProps> = ({
}) => {
const theme = mergeDeep(useTheme().theme.avatar, customTheme);

const imgClassName = classNames(
const imgClassName = twMerge(
theme.root.img.base,
bordered && theme.root.bordered,
bordered && theme.root.color[color],
rounded && theme.root.rounded,
Expand All @@ -101,11 +103,11 @@ const AvatarComponent: FC<AvatarProps> = ({
);

const imgProps = {
className: classNames(imgClassName, theme.root.img.on),
className: twMerge(imgClassName, theme.root.img.on),
'data-testid': 'flowbite-avatar-img',
};
return (
<div className={classNames(theme.root.base, className)} data-testid="flowbite-avatar" {...props}>
<div className={twMerge(theme.root.base, className)} data-testid="flowbite-avatar" {...props}>
<div className="relative">
{img ? (
typeof img === 'string' ? (
Expand All @@ -115,26 +117,23 @@ const AvatarComponent: FC<AvatarProps> = ({
)
) : placeholderInitials ? (
<div
className={classNames(
className={twMerge(
theme.root.img.off,
theme.root.initials.base,
rounded && theme.root.rounded,
stacked && theme.root.stacked,
bordered && theme.root.bordered,
bordered && theme.root.color[color],
theme.root.size[size],
rounded && theme.root.rounded,
)}
data-testid="flowbite-avatar-initials-placeholder"
>
<span
className={classNames(theme.root.initials.text)}
data-testid="flowbite-avatar-initials-placeholder-text"
>
<span className={twMerge(theme.root.initials.text)} data-testid="flowbite-avatar-initials-placeholder-text">
{placeholderInitials}
</span>
</div>
) : (
<div className={classNames(imgClassName, theme.root.img.off)} data-testid="flowbite-avatar-img">
<div className={twMerge(imgClassName, theme.root.img.off)} data-testid="flowbite-avatar-img">
<svg
className={theme.root.img.placeholder}
fill="currentColor"
Expand All @@ -148,7 +147,7 @@ const AvatarComponent: FC<AvatarProps> = ({
{status && (
<span
data-testid="flowbite-avatar-status"
className={classNames(
className={twMerge(
theme.root.status.base,
theme.root.status[status],
theme.root.statusPosition[statusPosition],
Expand Down
4 changes: 2 additions & 2 deletions src/components/Avatar/AvatarGroup.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import classNames from 'classnames';
import type { ComponentProps, PropsWithChildren } from 'react';
import React from 'react';
import { twMerge } from 'tailwind-merge';
import type { DeepPartial } from '../../';
import { useTheme } from '../../';
import { mergeDeep } from '../../helpers/merge-deep';
Expand All @@ -17,7 +17,7 @@ export const AvatarGroup: React.FC<AvatarGroupProps> = ({ children, className, t
const theme = mergeDeep(useTheme().theme.avatar.group, customTheme);

return (
<div data-testid="avatar-group-element" className={classNames(theme.base, className)} {...props}>
<div data-testid="avatar-group-element" className={twMerge(theme.base, className)} {...props}>
{children}
</div>
);
Expand Down
4 changes: 2 additions & 2 deletions src/components/Avatar/AvatarGroupCounter.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import classNames from 'classnames';
import type { ComponentProps, FC, PropsWithChildren } from 'react';
import { twMerge } from 'tailwind-merge';
import type { DeepPartial } from '../../';
import { useTheme } from '../../';
import { mergeDeep } from '../../helpers/merge-deep';
Expand All @@ -23,7 +23,7 @@ export const AvatarGroupCounter: FC<AvatarGroupCounterProps> = ({
const theme = mergeDeep(useTheme().theme.avatar.groupCounter, customTheme);

return (
<a href={href} className={classNames(theme.base, className)} {...props}>
<a href={href} className={twMerge(theme.base, className)} {...props}>
+{total}
</a>
);
Expand Down
62 changes: 62 additions & 0 deletions src/components/Avatar/theme.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import type { FlowbiteAvatarTheme } from './Avatar';

export const avatarTheme: FlowbiteAvatarTheme = {
root: {
base: 'flex justify-center items-center space-x-4 rounded',
bordered: 'p-1 ring-2',
rounded: 'rounded-full',
color: {
dark: 'ring-gray-800 dark:ring-gray-800',
failure: 'ring-red-500 dark:ring-red-700',
gray: 'ring-gray-500 dark:ring-gray-400',
info: 'ring-cyan-400 dark:ring-cyan-800',
light: 'ring-gray-300 dark:ring-gray-500',
purple: 'ring-purple-500 dark:ring-purple-600',
success: 'ring-green-500 dark:ring-green-500',
warning: 'ring-yellow-300 dark:ring-yellow-500',
pink: 'ring-pink-500 dark:ring-pink-500',
},
img: {
base: 'rounded',
off: 'relative overflow-hidden bg-gray-100 dark:bg-gray-600',
on: '',
placeholder: 'absolute w-auto h-auto text-gray-400 -bottom-1',
},
size: {
xs: 'w-6 h-6',
sm: 'w-8 h-8',
md: 'w-10 h-10',
lg: 'w-20 h-20',
xl: 'w-36 h-36',
},
stacked: 'ring-2 ring-gray-300 dark:ring-gray-500',
statusPosition: {
'bottom-left': '-bottom-1 -left-1',
'bottom-center': '-bottom-1 center',
'bottom-right': '-bottom-1 -right-1',
'top-left': '-top-1 -left-1',
'top-center': '-top-1 center',
'top-right': '-top-1 -right-1',
'center-right': 'center -right-1',
center: 'center center',
'center-left': 'center -left-1',
},
status: {
away: 'bg-yellow-400',
base: 'absolute h-3.5 w-3.5 rounded-full border-2 border-white dark:border-gray-800',
busy: 'bg-red-400',
offline: 'bg-gray-400',
online: 'bg-green-400',
},
initials: {
text: 'font-medium text-gray-600 dark:text-gray-300',
base: 'inline-flex overflow-hidden relative justify-center items-center bg-gray-100 dark:bg-gray-600',
},
},
group: {
base: 'flex -space-x-4',
},
groupCounter: {
base: 'relative flex items-center justify-center w-10 h-10 text-xs font-medium text-white bg-gray-700 rounded-full ring-2 ring-gray-300 hover:bg-gray-600 dark:ring-gray-500',
},
};
Loading