-
-
Notifications
You must be signed in to change notification settings - Fork 16
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Scaffold initial components * Add Eyedropper tool, implement colors lib * Finish drafting color picker * Add value, defaultValue and onChange * Allow editing formatter * Try / catch color manipulation errors * Remove ability to modify inputs, misc fixes * Remove unused dep * Update color-picker.mdx
- Loading branch information
1 parent
4859be9
commit 84ffe02
Showing
7 changed files
with
680 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
--- | ||
title: Color Picker | ||
description: A color picker is a UI component that allows users to select a color. It is modeled after the color picker in Figma, considered to be best in class. | ||
icon: Palette | ||
--- | ||
|
||
import { AutoTypeTable } from 'fumadocs-typescript/ui'; | ||
|
||
<PoweredBy packages={[ | ||
{ name: 'Lucide', url: 'https://lucide.dev/' }, | ||
{ name: 'color', url: 'https://github.com/Qix-/color' }, | ||
]} /> | ||
|
||
<Preview name="color-picker" code={`'use client'; | ||
import { | ||
ColorPicker, | ||
ColorPickerSelection, | ||
ColorPickerHue, | ||
ColorPickerAlpha, | ||
ColorPickerOutput, | ||
ColorPickerEyeDropper, | ||
ColorPickerFormat, | ||
} from '@/components/ui/kibo-ui/color-picker'; | ||
const Example = () => ( | ||
<div className="flex w-full h-screen items-center justify-center gap-4 bg-secondary"> | ||
<ColorPicker className="w-full max-w-[300px] rounded-md border bg-background p-4 shadow-sm"> | ||
<ColorPickerSelection /> | ||
<div className="flex items-center gap-4"> | ||
<ColorPickerEyeDropper /> | ||
<div className="w-full grid gap-1"> | ||
<ColorPickerHue /> | ||
<ColorPickerAlpha /> | ||
</div> | ||
</div> | ||
<div className="flex items-center gap-2"> | ||
<ColorPickerOutput /> | ||
<ColorPickerFormat /> | ||
</div> | ||
</ColorPicker> | ||
</div> | ||
); | ||
export default Example;`} /> | ||
|
||
## Installation | ||
|
||
<Installer packageName="color-picker" /> | ||
|
||
## Features | ||
|
||
- Interactive color selection with drag and drop functionality | ||
- Hue and alpha sliders for precise color adjustments | ||
- EyeDropper tool for picking colors from anywhere on screen | ||
- Multiple color format outputs (HEX, RGB, CSS, HSL) | ||
- Real-time color preview | ||
- Fully customizable through className and props | ||
|
||
## Props | ||
|
||
The color picker is made up of the following subcomponents: | ||
|
||
### ColorPicker | ||
|
||
The `ColorPicker` component is used to display a color picker. | ||
|
||
<AutoTypeTable path="node_modules/@repo/color-picker/index.tsx" name="ColorPickerProps" /> | ||
|
||
### ColorPickerSelection | ||
|
||
The `ColorPickerSelection` component is used to display the selected color. | ||
|
||
<AutoTypeTable path="node_modules/@repo/color-picker/index.tsx" name="ColorPickerSelectionProps" /> | ||
|
||
### ColorPickerHue | ||
|
||
The `ColorPickerHue` component is used to display the hue slider. | ||
|
||
<AutoTypeTable path="node_modules/@repo/color-picker/index.tsx" name="ColorPickerHueProps" /> | ||
|
||
### ColorPickerAlpha | ||
|
||
The `ColorPickerAlpha` component is used to display the alpha slider. | ||
|
||
<AutoTypeTable path="node_modules/@repo/color-picker/index.tsx" name="ColorPickerAlphaProps" /> | ||
|
||
### ColorPickerOutput | ||
|
||
The `ColorPickerOutput` component is used to display the output of the selected color. | ||
|
||
<AutoTypeTable path="node_modules/@repo/color-picker/index.tsx" name="ColorPickerOutputProps" /> | ||
|
||
### ColorPickerFormat | ||
|
||
The `ColorPickerFormat` component is used to display the format of the selected color. | ||
|
||
<AutoTypeTable path="node_modules/@repo/color-picker/index.tsx" name="ColorPickerFormatProps" /> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
{ | ||
"$schema": "https://ui.shadcn.com/schema/registry.json", | ||
"homepage": "https://www.kibo-ui.com/color-picker", | ||
"name": "color-picker", | ||
"type": "registry:ui", | ||
"author": "Hayden Bleasel <hello@haydenbleasel.com>", | ||
"registryDependencies": ["button", "input", "select"], | ||
"dependencies": ["@radix-ui/react-slider", "color", "lucide-react"], | ||
"devDependencies": ["@types/color"], | ||
"files": [ | ||
{ | ||
"type": "registry:ui", | ||
"path": "index.tsx", | ||
"content": "import { Button } from '@/components/ui/button';\nimport { Input } from '@/components/ui/input';\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@/components/ui/select';\nimport { cn } from '@/lib/utils';\nimport { Range, Root, Thumb, Track } from '@radix-ui/react-slider';\nimport Color from 'color';\nimport { PipetteIcon } from 'lucide-react';\nimport {\n type ChangeEventHandler,\n type ComponentProps,\n type HTMLAttributes,\n useCallback,\n useEffect,\n useRef,\n useState,\n} from 'react';\nimport { createContext, useContext } from 'react';\n\ninterface ColorPickerContextValue {\n hue: number;\n saturation: number;\n lightness: number;\n alpha: number;\n mode: string;\n setHue: (hue: number) => void;\n setSaturation: (saturation: number) => void;\n setLightness: (lightness: number) => void;\n setAlpha: (alpha: number) => void;\n setMode: (mode: string) => void;\n}\n\nconst ColorPickerContext = createContext<ColorPickerContextValue | undefined>(\n undefined\n);\n\nexport const useColorPicker = () => {\n const context = useContext(ColorPickerContext);\n\n if (!context) {\n throw new Error('useColorPicker must be used within a ColorPickerProvider');\n }\n\n return context;\n};\n\nexport type ColorPickerProps = HTMLAttributes<HTMLDivElement> & {\n value?: Parameters<typeof Color>[0];\n defaultValue?: Parameters<typeof Color>[0];\n onChange?: (value: Parameters<typeof Color.rgb>[0]) => void;\n};\n\nexport const ColorPicker = ({\n value,\n defaultValue = '#000000',\n onChange,\n className,\n ...props\n}: ColorPickerProps) => {\n const selectedColor = Color(value);\n const defaultColor = Color(defaultValue);\n\n const [hue, setHue] = useState(\n selectedColor.hue() || defaultColor.hue() || 0\n );\n const [saturation, setSaturation] = useState(\n selectedColor.saturationl() || defaultColor.saturationl() || 100\n );\n const [lightness, setLightness] = useState(\n selectedColor.lightness() || defaultColor.lightness() || 50\n );\n const [alpha, setAlpha] = useState(\n selectedColor.alpha() * 100 || defaultColor.alpha() * 100\n );\n const [mode, setMode] = useState('hex');\n\n // Update color when controlled value changes\n useEffect(() => {\n if (value) {\n const color = Color.rgb(value).rgb().object();\n\n setHue(color.r);\n setSaturation(color.g);\n setLightness(color.b);\n setAlpha(color.a);\n }\n }, [value]);\n\n // Notify parent of changes\n useEffect(() => {\n if (onChange) {\n const color = Color.hsl(hue, saturation, lightness).alpha(alpha / 100);\n const rgba = color.rgb().array();\n\n onChange([rgba[0], rgba[1], rgba[2], alpha / 100]);\n }\n }, [hue, saturation, lightness, alpha, onChange]);\n\n return (\n <ColorPickerContext.Provider\n value={{\n hue,\n saturation,\n lightness,\n alpha,\n mode,\n setHue,\n setSaturation,\n setLightness,\n setAlpha,\n setMode,\n }}\n >\n <div className={cn('grid w-full gap-4', className)} {...props} />\n </ColorPickerContext.Provider>\n );\n};\n\nexport type ColorPickerSelectionProps = HTMLAttributes<HTMLDivElement>;\n\nexport const ColorPickerSelection = ({\n className,\n ...props\n}: ColorPickerSelectionProps) => {\n const containerRef = useRef<HTMLDivElement>(null);\n const [isDragging, setIsDragging] = useState(false);\n const [position, setPosition] = useState({ x: 0, y: 0 });\n const { hue, setSaturation } = useColorPicker();\n\n const handlePointerMove = useCallback(\n (event: PointerEvent) => {\n if (!isDragging || !containerRef.current) {\n return;\n }\n\n const rect = containerRef.current.getBoundingClientRect();\n const x = Math.max(\n 0,\n Math.min(1, (event.clientX - rect.left) / rect.width)\n );\n const y = Math.max(\n 0,\n Math.min(1, (event.clientY - rect.top) / rect.height)\n );\n\n setPosition({ x, y });\n setSaturation((1 - y) * 100);\n },\n [isDragging, setSaturation]\n );\n\n useEffect(() => {\n if (isDragging) {\n window.addEventListener('pointermove', handlePointerMove);\n window.addEventListener('pointerup', () => setIsDragging(false));\n }\n return () => {\n window.removeEventListener('pointermove', handlePointerMove);\n window.removeEventListener('pointerup', () => setIsDragging(false));\n };\n }, [isDragging, handlePointerMove]);\n\n return (\n <div\n ref={containerRef}\n className={cn(\n 'relative aspect-[4/3] w-full cursor-crosshair rounded',\n `bg-[linear-gradient(0deg,rgb(0,0,0),transparent),linear-gradient(90deg,rgb(255,255,255),hsl(${hue},100%,50%))]`,\n className\n )}\n onPointerDown={(e) => {\n e.preventDefault();\n setIsDragging(true);\n handlePointerMove(e.nativeEvent);\n }}\n {...props}\n >\n <div\n className=\"-translate-x-1/2 -translate-y-1/2 pointer-events-none absolute h-4 w-4 rounded-full border-2 border-white\"\n style={{\n left: `${position.x * 100}%`,\n top: `${position.y * 100}%`,\n boxShadow: '0 0 0 1px rgba(0,0,0,0.5)',\n }}\n />\n </div>\n );\n};\n\nexport type ColorPickerHueProps = HTMLAttributes<HTMLDivElement>;\n\nexport const ColorPickerHue = ({\n className,\n ...props\n}: ColorPickerHueProps) => {\n const { hue, setHue } = useColorPicker();\n\n return (\n <Root\n value={[hue]}\n max={360}\n step={1}\n className={cn('relative flex h-4 w-full touch-none', className)}\n onValueChange={([hue]) => setHue(hue)}\n {...props}\n >\n <Track className=\"relative my-0.5 h-3 w-full grow rounded-full bg-[linear-gradient(90deg,#FF0000,#FFFF00,#00FF00,#00FFFF,#0000FF,#FF00FF,#FF0000)]\">\n <Range className=\"absolute h-full\" />\n </Track>\n <Thumb className=\"block h-4 w-4 rounded-full border border-primary/50 bg-background shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50\" />\n </Root>\n );\n};\n\nexport type ColorPickerAlphaProps = HTMLAttributes<HTMLDivElement>;\n\nexport const ColorPickerAlpha = ({\n className,\n ...props\n}: ColorPickerAlphaProps) => {\n const { alpha, setAlpha } = useColorPicker();\n\n return (\n <Root\n value={[alpha]}\n max={100}\n step={1}\n className={cn('relative flex h-4 w-full touch-none', className)}\n onValueChange={([alpha]) => setAlpha(alpha)}\n {...props}\n >\n <Track\n className=\"relative my-0.5 h-3 w-full grow rounded-full\"\n style={{\n background:\n 'url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAMUlEQVQ4T2NkYGAQYcAP3uCTZhw1gGGYhAGBZIA/nYDCgBDAm9BGDWAAJyRCgLaBCAAgXwixzAS0pgAAAABJRU5ErkJggg==\") left center',\n }}\n >\n <div className=\"absolute inset-0 rounded-full bg-gradient-to-r from-transparent to-primary/50\" />\n <Range className=\"absolute h-full rounded-full bg-transparent\" />\n </Track>\n <Thumb className=\"block h-4 w-4 rounded-full border border-primary/50 bg-background shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50\" />\n </Root>\n );\n};\n\nexport type ColorPickerEyeDropperProps = ComponentProps<typeof Button>;\n\nexport const ColorPickerEyeDropper = ({\n className,\n ...props\n}: ColorPickerEyeDropperProps) => {\n const { setHue, setSaturation, setLightness, setAlpha } = useColorPicker();\n\n const handleEyeDropper = async () => {\n try {\n // @ts-ignore - EyeDropper API is experimental\n const eyeDropper = new EyeDropper();\n const result = await eyeDropper.open();\n const color = Color(result.sRGBHex);\n const [h, s, l] = color.hsl().array();\n\n setHue(h);\n setSaturation(s);\n setLightness(l);\n setAlpha(100);\n } catch (error) {\n console.error('EyeDropper failed:', error);\n }\n };\n\n return (\n <Button\n variant=\"outline\"\n size=\"icon\"\n onClick={handleEyeDropper}\n className={cn('shrink-0 text-muted-foreground', className)}\n {...props}\n >\n <PipetteIcon size={16} />\n </Button>\n );\n};\n\nexport type ColorPickerOutputProps = ComponentProps<typeof SelectTrigger>;\n\nconst formats = ['hex', 'rgb', 'css', 'hsl'];\n\nexport const ColorPickerOutput = ({\n className,\n ...props\n}: ColorPickerOutputProps) => {\n const { mode, setMode } = useColorPicker();\n\n return (\n <Select value={mode} onValueChange={setMode}>\n <SelectTrigger className=\"h-8 w-[4.5rem] shrink-0 text-xs\" {...props}>\n <SelectValue placeholder=\"Mode\" />\n </SelectTrigger>\n <SelectContent>\n {formats.map((format) => (\n <SelectItem key={format} value={format} className=\"text-xs\">\n {format.toUpperCase()}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n );\n};\n\ntype PercentageInputProps = ComponentProps<typeof Input>;\n\nconst PercentageInput = ({ className, ...props }: PercentageInputProps) => {\n return (\n <div className=\"relative\">\n <Input\n type=\"text\"\n {...props}\n className={cn(\n 'h-8 w-[3.25rem] rounded-l-none bg-secondary px-2 text-xs shadow-none',\n className\n )}\n />\n <span className=\"-translate-y-1/2 absolute top-1/2 right-2 text-muted-foreground text-xs\">\n %\n </span>\n </div>\n );\n};\n\nexport type ColorPickerFormatProps = HTMLAttributes<HTMLDivElement>;\n\nexport const ColorPickerFormat = ({\n className,\n ...props\n}: ColorPickerFormatProps) => {\n const {\n hue,\n saturation,\n lightness,\n alpha,\n mode,\n setHue,\n setSaturation,\n setLightness,\n setAlpha,\n } = useColorPicker();\n const color = Color.hsl(hue, saturation, lightness, alpha / 100);\n\n if (mode === 'hex') {\n const hex = color.hex();\n\n const handleChange: ChangeEventHandler<HTMLInputElement> = (event) => {\n try {\n const newColor = Color(event.target.value);\n\n setHue(newColor.hue());\n setSaturation(newColor.saturationl());\n setLightness(newColor.lightness());\n setAlpha(newColor.alpha() * 100);\n } catch (error) {\n console.error('Invalid hex color:', error);\n }\n };\n\n return (\n <div\n className={cn(\n '-space-x-px relative flex items-center shadow-sm',\n className\n )}\n {...props}\n >\n <span className=\"-translate-y-1/2 absolute top-1/2 left-2 text-xs\">\n #\n </span>\n <Input\n type=\"text\"\n value={hex}\n className=\"h-8 rounded-r-none bg-secondary px-2 text-xs shadow-none\"\n />\n <PercentageInput value={alpha} />\n </div>\n );\n }\n\n if (mode === 'rgb') {\n const rgb = color\n .rgb()\n .array()\n .map((value) => Math.round(value));\n\n return (\n <div\n className={cn('-space-x-px flex items-center shadow-sm', className)}\n {...props}\n >\n {rgb.map((value, index) => (\n <Input\n key={index}\n type=\"text\"\n value={value}\n readOnly\n className={cn(\n 'h-8 rounded-r-none bg-secondary px-2 text-xs shadow-none',\n index && 'rounded-l-none',\n className\n )}\n />\n ))}\n <PercentageInput value={alpha} />\n </div>\n );\n }\n\n if (mode === 'css') {\n const rgb = color\n .rgb()\n .array()\n .map((value) => Math.round(value));\n\n return (\n <div className={cn('w-full shadow-sm', className)} {...props}>\n <Input\n type=\"text\"\n className=\"h-8 w-full bg-secondary px-2 text-xs shadow-none\"\n value={`rgba(${rgb.join(', ')}, ${alpha}%)`}\n readOnly\n {...props}\n />\n </div>\n );\n }\n\n if (mode === 'hsl') {\n const hsl = color\n .hsl()\n .array()\n .map((value) => Math.round(value));\n\n return (\n <div\n className={cn('-space-x-px flex items-center shadow-sm', className)}\n {...props}\n >\n {hsl.map((value, index) => (\n <Input\n key={index}\n type=\"text\"\n value={value}\n readOnly\n className={cn(\n 'h-8 rounded-r-none bg-secondary px-2 text-xs shadow-none',\n index && 'rounded-l-none',\n className\n )}\n />\n ))}\n <PercentageInput value={alpha} />\n </div>\n );\n }\n\n return null;\n};\n", | ||
"target": "components/ui/kibo-ui/color-picker.tsx" | ||
} | ||
] | ||
} |
Oops, something went wrong.