Skip to content

Commit a79adcf

Browse files
committed
Add color components to tailwind starter
1 parent caa2726 commit a79adcf

16 files changed

+389
-2
lines changed

starters/tailwind/.gitignore

-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1 @@
1-
src
2-
stories
31
storybook-static

starters/tailwind/src/ColorArea.tsx

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import React from 'react';
2+
import {
3+
ColorArea as AriaColorArea,
4+
ColorAreaProps as AriaColorAreaProps
5+
} from 'react-aria-components';
6+
import { composeTailwindRenderProps } from './utils';
7+
import { ColorThumb } from './ColorThumb';
8+
9+
export interface ColorAreaProps extends AriaColorAreaProps {}
10+
11+
export function ColorArea(props: ColorAreaProps) {
12+
return (
13+
<AriaColorArea
14+
{...props}
15+
className={composeTailwindRenderProps(props.className, 'w-56 h-56 rounded-lg bg-gray-300 dark:bg-zinc-800 forced-colors:bg-[GrayText]')}
16+
style={({ defaultStyle, isDisabled }) => ({
17+
...defaultStyle,
18+
background: isDisabled ? undefined : defaultStyle.background
19+
})}>
20+
<ColorThumb />
21+
</AriaColorArea>
22+
);
23+
}

starters/tailwind/src/ColorField.tsx

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import React from 'react';
2+
import {
3+
ColorField as AriaColorField,
4+
ColorFieldProps as AriaColorFieldProps,
5+
ValidationResult
6+
} from 'react-aria-components';
7+
import { tv } from 'tailwind-variants';
8+
import { Description, FieldError, Input, Label, fieldBorderStyles } from './Field';
9+
import { composeTailwindRenderProps, focusRing } from './utils';
10+
11+
const inputStyles = tv({
12+
extend: focusRing,
13+
base: 'border-2 rounded-md',
14+
variants: {
15+
isFocused: fieldBorderStyles.variants.isFocusWithin,
16+
...fieldBorderStyles.variants,
17+
}
18+
});
19+
20+
export interface ColorFieldProps extends AriaColorFieldProps {
21+
label?: string;
22+
description?: string;
23+
errorMessage?: string | ((validation: ValidationResult) => string);
24+
}
25+
26+
export function ColorField(
27+
{ label, description, errorMessage, ...props }: ColorFieldProps
28+
) {
29+
return (
30+
<AriaColorField {...props} className={composeTailwindRenderProps(props.className, 'flex flex-col gap-1')}>
31+
{label && <Label>{label}</Label>}
32+
<Input className={inputStyles} />
33+
{description && <Description>{description}</Description>}
34+
<FieldError>{errorMessage}</FieldError>
35+
</AriaColorField>
36+
);
37+
}

starters/tailwind/src/ColorPicker.tsx

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import React from 'react';
2+
import {Button, ColorPicker as AriaColorPicker, ColorPickerProps as AriaColorPickerProps, DialogTrigger} from 'react-aria-components';
3+
import {ColorSwatch} from './ColorSwatch';
4+
import {ColorArea} from './ColorArea';
5+
import {ColorSlider} from './ColorSlider';
6+
import {ColorField} from './ColorField';
7+
import {Dialog} from './Dialog';
8+
import {Popover} from './Popover';
9+
import { tv } from 'tailwind-variants';
10+
import { focusRing } from './utils';
11+
12+
const buttonStyles = tv({
13+
extend: focusRing,
14+
base: 'flex gap-2 items-center cursor-default rounded text-sm text-gray-800 dark:text-gray-200'
15+
});
16+
17+
export interface ColorPickerProps extends AriaColorPickerProps {
18+
label?: string;
19+
children?: React.ReactNode;
20+
}
21+
22+
export function ColorPicker({ label, children, ...props }: ColorPickerProps) {
23+
return (
24+
<AriaColorPicker {...props}>
25+
<DialogTrigger>
26+
<Button className={buttonStyles}>
27+
<ColorSwatch />
28+
<span>{label}</span>
29+
</Button>
30+
<Popover placement="bottom start">
31+
<Dialog className="flex flex-col gap-2">
32+
{children || (
33+
<>
34+
<ColorArea
35+
colorSpace="hsb"
36+
xChannel="saturation"
37+
yChannel="brightness"
38+
/>
39+
<ColorSlider colorSpace="hsb" channel="hue" />
40+
<ColorField label="Hex" />
41+
</>
42+
)}
43+
</Dialog>
44+
</Popover>
45+
</DialogTrigger>
46+
</AriaColorPicker>
47+
);
48+
}

starters/tailwind/src/ColorSlider.tsx

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import React from 'react';
2+
import {
3+
ColorSlider as AriaColorSlider,
4+
ColorSliderProps as AriaColorSliderProps,
5+
SliderOutput,
6+
SliderTrack
7+
} from 'react-aria-components';
8+
import { tv } from 'tailwind-variants';
9+
import { Label } from './Field';
10+
import { composeTailwindRenderProps } from './utils';
11+
import { ColorThumb } from './ColorThumb';
12+
13+
const trackStyles = tv({
14+
base: 'group col-span-2 orientation-horizontal:h-6 rounded-lg',
15+
variants: {
16+
orientation: {
17+
horizontal: 'w-full h-6',
18+
vertical: 'w-6 h-56 ml-[50%] -translate-x-[50%]'
19+
},
20+
isDisabled: {
21+
true: 'bg-gray-300 dark:bg-zinc-800 forced-colors:bg-[GrayText]'
22+
}
23+
}
24+
});
25+
26+
interface ColorSliderProps extends AriaColorSliderProps {
27+
label?: string;
28+
}
29+
30+
export function ColorSlider({ label, ...props }: ColorSliderProps) {
31+
return (
32+
<AriaColorSlider {...props} className={composeTailwindRenderProps(props.className, 'orientation-horizontal:grid orientation-vertical:flex grid-cols-[1fr_auto] flex-col items-center gap-2 orientation-horizontal:w-56')}>
33+
<Label>{label}</Label>
34+
<SliderOutput className="text-sm text-gray-500 dark:text-zinc-400 font-medium orientation-vertical:hidden" />
35+
<SliderTrack
36+
className={trackStyles}
37+
style={({ defaultStyle, isDisabled }) => ({
38+
...defaultStyle,
39+
background: isDisabled ? undefined : `${defaultStyle.background}, repeating-conic-gradient(#CCC 0% 25%, white 0% 50%) 50% / 16px 16px`
40+
})}
41+
>
42+
<ColorThumb />
43+
</SliderTrack>
44+
</AriaColorSlider>
45+
);
46+
}

starters/tailwind/src/ColorSwatch.tsx

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import React from 'react';
2+
import {ColorSwatch as AriaColorSwatch, ColorSwatchProps} from 'react-aria-components';
3+
import { composeTailwindRenderProps } from './utils';
4+
5+
export function ColorSwatch(props: ColorSwatchProps) {
6+
return (
7+
<AriaColorSwatch
8+
{...props}
9+
className={composeTailwindRenderProps(props.className, 'w-8 h-8 rounded border border-black/10')}
10+
style={({color}) => ({
11+
background: `linear-gradient(${color}, ${color}),
12+
repeating-conic-gradient(#CCC 0% 25%, white 0% 50%) 50% / 16px 16px`
13+
})} />
14+
);
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import React from 'react';
2+
import {
3+
ColorSwatchPicker as AriaColorSwatchPicker,
4+
ColorSwatchPickerItem as AriaColorSwatchPickerItem,
5+
ColorSwatchPickerItemProps,
6+
ColorSwatchPickerProps
7+
} from 'react-aria-components';
8+
import {ColorSwatch} from './ColorSwatch';
9+
import {composeTailwindRenderProps, focusRing} from './utils';
10+
import {tv} from 'tailwind-variants';
11+
12+
export function ColorSwatchPicker(
13+
{ children, ...props }: Omit<ColorSwatchPickerProps, 'layout'>
14+
) {
15+
return (
16+
<AriaColorSwatchPicker {...props} className={composeTailwindRenderProps(props.className, 'flex gap-1')}>
17+
{children}
18+
</AriaColorSwatchPicker>
19+
);
20+
}
21+
22+
const itemStyles = tv({
23+
extend: focusRing,
24+
base: 'relative rounded'
25+
});
26+
27+
export function ColorSwatchPickerItem(props: ColorSwatchPickerItemProps) {
28+
return (
29+
<AriaColorSwatchPickerItem {...props} className={itemStyles}>
30+
{({isSelected}) => <>
31+
<ColorSwatch />
32+
{isSelected && <div className="absolute top-0 left-0 w-full h-full border border-2 border-black dark:border-white outline outline-2 outline-white dark:outline-black -outline-offset-4 rounded forced-color-adjust-none" />}
33+
</>}
34+
</AriaColorSwatchPickerItem>
35+
);
36+
}

starters/tailwind/src/ColorThumb.tsx

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import React from 'react';
2+
import {ColorThumb as AriaColorThumb, ColorThumbProps} from 'react-aria-components';
3+
import { tv } from 'tailwind-variants';
4+
5+
const thumbStyles = tv({
6+
base: 'w-6 h-6 top-[50%] left-[50%] rounded-full border-2 border-white',
7+
variants: {
8+
isFocusVisible: {
9+
true: 'w-8 h-8'
10+
},
11+
isDragging: {
12+
true: 'bg-gray-700 dark:bg-gray-300 forced-colors:bg-[ButtonBorder]'
13+
},
14+
isDisabled: {
15+
true: 'border-gray-300 dark:border-zinc-700 forced-colors:border-[GrayText] bg-gray-300 dark:bg-zinc-800 forced-colors:bg-[GrayText]'
16+
}
17+
}
18+
});
19+
20+
export function ColorThumb(props: ColorThumbProps) {
21+
return (
22+
<AriaColorThumb
23+
{...props}
24+
style={({ defaultStyle, isDisabled }) => ({
25+
...defaultStyle,
26+
backgroundColor: isDisabled ? undefined : defaultStyle.backgroundColor,
27+
boxShadow: '0 0 0 1px black, inset 0 0 0 1px black'}
28+
)}
29+
className={thumbStyles} />
30+
);
31+
}

starters/tailwind/src/ColorWheel.tsx

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import React from 'react';
2+
import {ColorWheel as AriaColorWheel, ColorWheelProps as AriaColorWheelProps, ColorWheelTrack} from 'react-aria-components';
3+
import { ColorThumb } from './ColorThumb';
4+
5+
export interface ColorWheelProps extends Omit<AriaColorWheelProps, 'outerRadius' | 'innerRadius'> {}
6+
7+
export function ColorWheel(props: ColorWheelProps) {
8+
return (
9+
<AriaColorWheel {...props} outerRadius={100} innerRadius={74}>
10+
<ColorWheelTrack
11+
className="disabled:bg-gray-300 disabled:dark:bg-zinc-800 disabled:forced-colors:bg-[GrayText]"
12+
style={({ defaultStyle, isDisabled }) => ({
13+
...defaultStyle,
14+
background: isDisabled ? undefined : `${defaultStyle.background}, repeating-conic-gradient(#CCC 0% 25%, white 0% 50%) 50% / 16px 16px`
15+
})} />
16+
<ColorThumb />
17+
</AriaColorWheel>
18+
);
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import type { Meta } from '@storybook/react';
2+
import React from 'react';
3+
import { ColorArea } from '../src/ColorArea';
4+
5+
const meta: Meta<typeof ColorArea> = {
6+
component: ColorArea,
7+
parameters: {
8+
layout: 'centered'
9+
},
10+
tags: ['autodocs']
11+
};
12+
13+
export default meta;
14+
15+
export const Example = (args: any) => <ColorArea {...args} />;
16+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import type { Meta } from '@storybook/react';
2+
import React from 'react';
3+
import { ColorField } from '../src/ColorField';
4+
5+
const meta: Meta<typeof ColorField> = {
6+
component: ColorField,
7+
parameters: {
8+
layout: 'centered'
9+
},
10+
tags: ['autodocs'],
11+
args: {
12+
label: 'Color',
13+
defaultValue: '#ff0'
14+
}
15+
};
16+
17+
export default meta;
18+
19+
export const Example = (args: any) => <ColorField {...args} />;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import type { Meta } from '@storybook/react';
2+
import React from 'react';
3+
import { ColorPicker } from '../src/ColorPicker';
4+
5+
const meta: Meta<typeof ColorPicker> = {
6+
component: ColorPicker,
7+
parameters: {
8+
layout: 'centered'
9+
},
10+
tags: ['autodocs'],
11+
args: {
12+
label: 'Color',
13+
defaultValue: '#ff0'
14+
}
15+
};
16+
17+
export default meta;
18+
19+
export const Example = (args: any) => <ColorPicker {...args} />;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import type { Meta } from '@storybook/react';
2+
import React from 'react';
3+
import { ColorSlider } from '../src/ColorSlider';
4+
5+
const meta: Meta<typeof ColorSlider> = {
6+
component: ColorSlider,
7+
parameters: {
8+
layout: 'centered'
9+
},
10+
tags: ['autodocs']
11+
};
12+
13+
export default meta;
14+
15+
export const Example = (args: any) => <ColorSlider {...args} />;
16+
17+
Example.args = {
18+
label: 'Fill Color',
19+
channel: 'hue',
20+
colorSpace: 'hsl',
21+
defaultValue: '#f00'
22+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import type { Meta } from '@storybook/react';
2+
import React from 'react';
3+
import { ColorSwatch } from '../src/ColorSwatch';
4+
5+
const meta: Meta<typeof ColorSwatch> = {
6+
component: ColorSwatch,
7+
parameters: {
8+
layout: 'centered'
9+
},
10+
tags: ['autodocs']
11+
};
12+
13+
export default meta;
14+
15+
export const Example = (args: any) => <ColorSwatch {...args} />;
16+
17+
Example.args = {
18+
color: '#f00a'
19+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import type { Meta } from '@storybook/react';
2+
import React from 'react';
3+
import { ColorSwatchPicker, ColorSwatchPickerItem } from '../src/ColorSwatchPicker';
4+
5+
const meta: Meta<typeof ColorSwatchPicker> = {
6+
component: ColorSwatchPicker,
7+
parameters: {
8+
layout: 'centered'
9+
},
10+
tags: ['autodocs']
11+
};
12+
13+
export default meta;
14+
15+
export const Example = (args: any) => (
16+
<ColorSwatchPicker {...args}>
17+
<ColorSwatchPickerItem color="#A00" />
18+
<ColorSwatchPickerItem color="#f80" />
19+
<ColorSwatchPickerItem color="#080" />
20+
<ColorSwatchPickerItem color="#08f" />
21+
<ColorSwatchPickerItem color="#088" />
22+
<ColorSwatchPickerItem color="#008" />
23+
</ColorSwatchPicker>
24+
);

0 commit comments

Comments
 (0)