Skip to content

Commit bdfd54d

Browse files
committed
feat: added context api for changing colors on components
1 parent c9b7f93 commit bdfd54d

File tree

17 files changed

+432
-39
lines changed

17 files changed

+432
-39
lines changed

README.md

+72-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ yarn add @wedevs/tail-react
1414

1515
## Usage
1616

17-
On your `tailwind.config.js` file, update the content entry:
17+
### Tailwind CSS v3
18+
19+
If you're using Tailwind CSS v3, update your `tailwind.config.js` file:
1820

1921
```diff
2022
/** @type {import('tailwindcss').Config} */
@@ -33,6 +35,75 @@ export default {
3335
}
3436
```
3537

38+
### Tailwind CSS v4
39+
40+
For Tailwind CSS v4, configuration is done in your CSS file:
41+
42+
```css
43+
@import 'tailwindcss';
44+
45+
/* Import tail-react components */
46+
@source "node_modules/@wedevs/tail-react/dist/index.js";
47+
48+
@theme {
49+
/* Your theme customizations */
50+
}
51+
```
52+
53+
### Customizing the theme color
54+
55+
By default, the component library uses `indigo` as the primary color. You can customize this using the `TailReactBaseColorProvider`:
56+
57+
```jsx
58+
import { TailReactBaseColorProvider, Button } from '@wedevs/tail-react';
59+
60+
function App() {
61+
return (
62+
<TailReactBaseColorProvider color="blue">
63+
{/* All components inside will use blue as the primary color */}
64+
<Button variant="primary">Primary Blue Button</Button>
65+
</TailReactBaseColorProvider>
66+
);
67+
}
68+
```
69+
70+
Available color options include all Tailwind CSS colors:
71+
72+
- `slate`, `gray`, `zinc`, `neutral`, `stone`
73+
- `red`, `orange`, `amber`, `yellow`, `lime`
74+
- `green`, `emerald`, `teal`, `cyan`, `sky`
75+
- `blue`, `indigo` (default), `violet`, `purple`, `fuchsia`
76+
- `pink`, `rose`
77+
78+
#### Important: Prevent color classes from being purged
79+
80+
##### Tailwind CSS v3
81+
82+
When using dynamic color classes with TailReactBaseColor context in Tailwind CSS v3, you need to safelist these classes in your Tailwind config to prevent them from being purged in production:
83+
84+
```js
85+
// tailwind.config.js
86+
module.exports = {
87+
// ... your existing config
88+
safelist: [
89+
// Safelist color variants that you plan to use dynamically
90+
{
91+
pattern: /bg-(red|blue|green|purple|indigo|etc)-(500|600|700)/,
92+
},
93+
{
94+
pattern: /text-(red|blue|green|purple|indigo|etc)-(500|600)/,
95+
},
96+
{
97+
pattern: /ring-(red|blue|green|purple|indigo|etc)-(400|500|600)/,
98+
},
99+
// Add other patterns based on which color utilities you use
100+
],
101+
// ... rest of your config
102+
};
103+
```
104+
105+
This approach allows you to safelist utility classes using patterns, which is much more concise than listing each class individually.
106+
36107
## Development
37108

38109
To get started with development:

src/Components/Button/Button.tsx

+65-24
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { ArrowPathIcon } from '@heroicons/react/24/outline';
22
import React from 'react';
33
import { twMerge } from 'tailwind-merge';
4+
import { getColorClasses } from '../../utils/colorUtils';
45

56
interface ButtonProps {
67
children: React.ReactNode;
@@ -18,23 +19,6 @@ interface ButtonProps {
1819
rel?: string;
1920
}
2021

21-
const Styles = {
22-
'primary:fill':
23-
'bg-indigo-600 dark:bg-indigo-500 text-white shadow-sm hover:bg-indigo-700 dark:hover:bg-indigo-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600',
24-
'primary:outline':
25-
'bg-white dark:bg-transparent ring-1 ring-inset ring-indigo-600 dark:ring-indigo-400 text-indigo-600 dark:text-indigo-400 hover:bg-indigo-600 dark:hover:bg-indigo-400 hover:text-white dark:hover:text-indigo-700 shadow-sm',
26-
'primary:link': 'text-indigo-600 dark:text-indigo-400 hover:text-indigo-500',
27-
'secondary:fill':
28-
'bg-white dark:bg-white/10 ring-1 ring-inset ring-gray-300 dark:ring-white/10 hover:bg-gray-50 hover:bg-white/5 text-gray-900 dark:text-white shadow-sm',
29-
'secondary:outline':
30-
'bg-white dark:bg-transparent dark:ring-1 dark:ring-gray-400 hover:bg-gray-50 text-gray-900 dark:text-gray-400 shadow-sm',
31-
'secondary:link': 'text-gray-900 dark:text-gray-400 hover:text-gray-500',
32-
'danger:fill': 'bg-red-600 dark:bg-red-500 hover:bg-red-500 text-white',
33-
'danger:outline':
34-
'bg-white ring-1 ring-inset ring-red-600 hover:bg-red-600 text-red-600 hover:text-white shadow-sm',
35-
'danger:link': 'text-red-600 dark:text-red-500 hover:text-red-500 dark:hover:text-red-400',
36-
};
37-
3822
const Button: React.FC<ButtonProps> = ({
3923
children,
4024
variant = 'primary',
@@ -50,6 +34,50 @@ const Button: React.FC<ButtonProps> = ({
5034
rel,
5135
size = 'medium',
5236
}) => {
37+
const getStyleClasses = () => {
38+
if (variant === 'primary') {
39+
if (style === 'fill') {
40+
return twMerge(
41+
getColorClasses({
42+
bg: '600',
43+
'dark:bg': '500',
44+
'hover:bg': '700',
45+
'dark:hover:bg': '400',
46+
'focus-visible:outline': '',
47+
'focus-visible:outline-offset': '',
48+
'focus-visible:outline-2': '',
49+
'focus-visible:outline-': '600',
50+
}),
51+
'text-white shadow-sm'
52+
);
53+
} else if (style === 'outline') {
54+
return twMerge(
55+
getColorClasses({
56+
ring: '600',
57+
'dark:ring': '400',
58+
text: '600',
59+
'dark:text': '400',
60+
'hover:bg': '600',
61+
'dark:hover:bg': '400',
62+
'dark:hover:text': '700',
63+
}),
64+
'bg-white dark:bg-transparent ring-1 ring-inset hover:text-white shadow-sm'
65+
);
66+
} else if (style === 'link') {
67+
return twMerge(
68+
getColorClasses({
69+
text: '600',
70+
'dark:text': '400',
71+
'hover:text': '500',
72+
})
73+
);
74+
}
75+
}
76+
77+
// If not primary or a different variant, use the existing styles
78+
return Styles[`${variant}:${style}`];
79+
};
80+
5381
const getSizeStyles = () => {
5482
switch (size) {
5583
case 'small':
@@ -77,19 +105,14 @@ const Button: React.FC<ButtonProps> = ({
77105
const renderButton = () => {
78106
const commonStyles = 'font-semibold focus:outline-none';
79107
const sizeStyles = getSizeStyles();
108+
const styleClasses = getStyleClasses();
80109

81110
const disabledStyles = disabled ? 'opacity-50 cursor-not-allowed' : '';
82111

83112
return React.createElement(
84113
as,
85114
{
86-
className: twMerge(
87-
commonStyles,
88-
Styles[`${variant}:${style}`],
89-
sizeStyles,
90-
disabledStyles,
91-
className
92-
),
115+
className: twMerge(commonStyles, styleClasses, sizeStyles, disabledStyles, className),
93116
type,
94117
disabled: disabled,
95118
onClick: handleClick,
@@ -105,4 +128,22 @@ const Button: React.FC<ButtonProps> = ({
105128
return renderButton();
106129
};
107130

131+
// Keep the original Styles object for secondary and danger variants
132+
const Styles = {
133+
'primary:fill':
134+
'bg-indigo-600 dark:bg-indigo-500 text-white shadow-sm hover:bg-indigo-700 dark:hover:bg-indigo-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600',
135+
'primary:outline':
136+
'bg-white dark:bg-transparent ring-1 ring-inset ring-indigo-600 dark:ring-indigo-400 text-indigo-600 dark:text-indigo-400 hover:bg-indigo-600 dark:hover:bg-indigo-400 hover:text-white dark:hover:text-indigo-700 shadow-sm',
137+
'primary:link': 'text-indigo-600 dark:text-indigo-400 hover:text-indigo-500',
138+
'secondary:fill':
139+
'bg-white dark:bg-white/10 ring-1 ring-inset ring-gray-300 dark:ring-white/10 hover:bg-gray-50 hover:bg-white/5 text-gray-900 dark:text-white shadow-sm',
140+
'secondary:outline':
141+
'bg-white dark:bg-transparent dark:ring-1 dark:ring-gray-400 hover:bg-gray-50 text-gray-900 dark:text-gray-400 shadow-sm',
142+
'secondary:link': 'text-gray-900 dark:text-gray-400 hover:text-gray-500',
143+
'danger:fill': 'bg-red-600 dark:bg-red-500 hover:bg-red-500 text-white',
144+
'danger:outline':
145+
'bg-white ring-1 ring-inset ring-red-600 hover:bg-red-600 text-red-600 hover:text-white shadow-sm',
146+
'danger:link': 'text-red-600 dark:text-red-500 hover:text-red-500 dark:hover:text-red-400',
147+
};
148+
108149
export { Button };

src/Components/Checkbox/Checkbox.tsx

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React from 'react';
22
import { twMerge } from 'tailwind-merge';
3+
import { getColorClass } from '../../utils/colorUtils';
34

45
interface CheckboxProps {
56
label: string;
@@ -22,6 +23,11 @@ const Checkbox: React.FC<CheckboxProps> = ({
2223
}) => {
2324
const id = `input-${Math.random().toString(36).substr(2, 9)}`;
2425

26+
// Get color classes
27+
const textColor = getColorClass('text', '600');
28+
const darkTextColor = getColorClass('dark:text', '500');
29+
const focusRingColor = getColorClass('focus:ring', '600');
30+
2531
return (
2632
<div className="relative flex gap-x-3 mb-4">
2733
<div className="flex h-6 items-center">
@@ -32,7 +38,7 @@ const Checkbox: React.FC<CheckboxProps> = ({
3238
disabled={disabled}
3339
{...props}
3440
className={twMerge(
35-
'h-4 w-4 rounded form-checkbox border-gray-300 text-indigo-600 dark:text-indigo-500 focus:ring-indigo-600',
41+
`h-4 w-4 rounded form-checkbox border-gray-300 ${textColor} ${darkTextColor} ${focusRingColor}`,
3642
className,
3743
disabled && 'disabled:opacity-50 cursor-not-allowed'
3844
)}

src/Components/RadioGroup/RadioGroup.tsx

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { twMerge } from 'tailwind-merge';
2+
import { getColorClass } from '../../utils/colorUtils';
23

34
interface Option {
45
key: string;
@@ -22,6 +23,10 @@ const RadioGroup: React.FC<RadioGroupProps> = ({
2223
help,
2324
...props
2425
}) => {
26+
// Get color classes
27+
const textColor = getColorClass('text', '600');
28+
const focusRingColor = getColorClass('focus:ring', '600');
29+
2530
return (
2631
<div className="mb-4">
2732
{props.label && (
@@ -48,7 +53,7 @@ const RadioGroup: React.FC<RadioGroupProps> = ({
4853
value={option.key}
4954
checked={value === option.key}
5055
onChange={() => onChange(option.key)}
51-
className="h-4 w-4 border-gray-300 text-indigo-600 focus:ring-indigo-600"
56+
className={`h-4 w-4 border-gray-300 ${textColor} ${focusRingColor}`}
5257
/>
5358
</div>
5459

src/Components/SelectCard/SelectCard.tsx

+9-4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React, { useState } from 'react';
22
import classNames from 'classnames';
33
import { twMerge } from 'tailwind-merge';
44
import { CheckCircleIcon } from '@heroicons/react/24/solid';
5+
import { getColorClass } from '../../utils/colorUtils';
56

67
interface Option {
78
key: string;
@@ -41,6 +42,12 @@ const SelectCard = ({
4142
return options.find((option: { key: string }) => option.key === selectedKey) || options[0];
4243
});
4344

45+
// Get color classes
46+
const borderColor = getColorClass('border', '600');
47+
const darkBorderColor = getColorClass('dark:border', '300');
48+
const textColor = getColorClass('text', '600');
49+
const darkTextColor = getColorClass('dark:text', '400');
50+
4451
const handleChange = (option: Option) => {
4552
if (option.disabled) {
4653
return;
@@ -71,7 +78,7 @@ const SelectCard = ({
7178
key={index}
7279
className={twMerge(
7380
selectedOption.key === option.key
74-
? 'border-indigo-600 dark:border-indigo-300'
81+
? `${borderColor} ${darkBorderColor}`
7582
: 'border-gray-200 dark:border-gray-600',
7683
'relative flex cursor-pointer text-center rounded-lg border-2 p-4 bg-white dark:bg-white/10 dark:text-gray-200 focus:outline-none',
7784
option.disabled ? 'opacity-75 cursor-not-allowed grayscale' : ''
@@ -83,9 +90,7 @@ const SelectCard = ({
8390
<div
8491
className={classNames(
8592
'absolute top-0 right-0 p-1 rounded-full',
86-
selectedOption.key === option.key
87-
? ' text-indigo-600 dark:text-indigo-400'
88-
: 'invisible'
93+
selectedOption.key === option.key ? `${textColor} ${darkTextColor}` : 'invisible'
8994
)}
9095
>
9196
<CheckCircleIcon className="h-5 w-5" />

src/Components/SelectInput/SelectInput.tsx

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React, { useEffect, useState } from 'react';
22
import { twMerge } from 'tailwind-merge';
3+
import { getColorClass } from '../../utils/colorUtils';
34

45
interface Option {
56
key: string;
@@ -45,6 +46,9 @@ const SelectInput: React.FC<SelectProps> = ({
4546

4647
const id = `input-${Math.random().toString(36).substr(2, 9)}`;
4748

49+
// Get color classes
50+
const focusRingColor = getColorClass('focus:ring', '600');
51+
4852
const handleChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
4953
const selectedOption = options.find((option) => option.key == event.target.value);
5054

@@ -71,7 +75,7 @@ const SelectInput: React.FC<SelectProps> = ({
7175
)}
7276
<select
7377
className={twMerge(
74-
'block w-full dark:bg-white/5 rounded-md border-0 py-1.5 px-2 text-gray-900 dark:text-gray-300 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-500 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6 outline-none',
78+
`block w-full dark:bg-white/5 rounded-md border-0 py-1.5 px-2 text-gray-900 dark:text-gray-300 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-500 focus:ring-2 focus:ring-inset ${focusRingColor} sm:text-sm sm:leading-6 outline-none`,
7579
className,
7680
error && 'ring-red-300 text-red-900 placeholder:text-red-300 focus:ring-red-500'
7781
)}

src/Components/SwitchInput/SwitchInput.tsx

+8-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { useState, useEffect, ReactNode } from 'react';
22
import { Switch } from '@headlessui/react';
33
import classNames from 'classnames';
44
import { twMerge } from 'tailwind-merge';
5+
import { getColorClass } from '../../utils/colorUtils';
56

67
type Props = {
78
label: string;
@@ -37,15 +38,20 @@ const SwitchInput = ({
3738
}
3839
};
3940

41+
// Get color classes
42+
const bgColor = getColorClass('bg', '600');
43+
const darkBgColor = getColorClass('dark:bg', '500');
44+
const focusRingColor = getColorClass('focus:ring', '500');
45+
4046
return (
4147
<Switch.Group as="div" className={twMerge('relative flex items-start mb-4', className)}>
4248
<Switch
4349
checked={enabled}
4450
disabled={disabled}
4551
onChange={toggleInput}
4652
className={classNames(
47-
enabled ? 'bg-indigo-600 dark:bg-indigo-500' : 'bg-gray-200 dark:bg-gray-600',
48-
'relative mt-1 inline-flex h-5 w-10 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2',
53+
enabled ? `${bgColor} ${darkBgColor}` : 'bg-gray-200 dark:bg-gray-600',
54+
`relative mt-1 inline-flex h-5 w-10 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 ${focusRingColor} focus:ring-offset-2`,
4955
disabled ? 'cursor-not-allowed opacity-50' : ''
5056
)}
5157
>

0 commit comments

Comments
 (0)