Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/replace-react-color-with-uiw.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@sanity/color-input": patch
---

Replace deprecated react-color with @uiw/react-color
3 changes: 1 addition & 2 deletions plugins/@sanity/color-input/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
"dependencies": {
"@sanity/icons": "^3.7.4",
"@sanity/ui": "catalog:",
"react-color": "^2.19.3",
"@uiw/react-color": "^2.9.3",
"tinycolor2": "^1.6.0"
},
"devDependencies": {
Expand All @@ -58,7 +58,6 @@
"@repo/tsconfig": "workspace:*",
"@sanity/pkg-utils": "catalog:",
"@types/react": "catalog:",
"@types/react-color": "^2.17.12",
"@types/tinycolor2": "^1.4.6",
"babel-plugin-react-compiler": "catalog:",
"babel-plugin-styled-components": "catalog:",
Expand Down
109 changes: 77 additions & 32 deletions plugins/@sanity/color-input/src/ColorInput.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import type {CustomPickerInjectedProps} from 'react-color/lib/components/common/ColorWrap'

import {AddIcon, TrashIcon} from '@sanity/icons'
import {Box, Button, Card, Flex, Inline, Stack, Text} from '@sanity/ui'
import {
Alpha,
Hue,
type HsvaColor,
Saturation,
hsvaToHex,
hsvaToHsla,
hsvaToRgba,
} from '@uiw/react-color'
import {startTransition, useOptimistic, useRef} from 'react'
import {type Color, CustomPicker} from 'react-color'
import {Alpha, Checkboard, Hue, Saturation} from 'react-color/lib/components/common'
import {type ObjectInputProps, set, setIfMissing, unset} from 'sanity'
import {styled} from 'styled-components'

Expand All @@ -28,38 +33,76 @@ const ReadOnlyContainer = styled(Flex)`
width: 100%;
`

interface ColorPickerProps extends CustomPickerInjectedProps<Color> {
// Checkboard pattern matching react-color original: 8x8 pixel squares, white and rgba(0,0,0,.08)
const Checkboard = styled.div`
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: repeating-conic-gradient(rgba(0, 0, 0, 0.08) 0% 25%, #fff 0% 50%) 0 0 / 16px 16px;
`

// Custom pointer to match the original react-color vertical bar style
const BarPointer = styled.div<{$left: string}>`
position: absolute;
left: ${(props) => props.$left};
width: 4px;
height: 100%;
border-radius: 1px;
background: #fff;
box-shadow: rgba(0, 0, 0, 0.6) 0px 0px 2px;
transform: translateX(-50%);
`

interface PointerProps {
left?: string
}

const Pointer = ({left = '0%'}: PointerProps) => <BarPointer $left={left} />

interface ColorPickerProps {
width?: string
disableAlpha: boolean
colorList?: Array<Color> | undefined
colorList?: Array<string | ColorValue> | undefined
readOnly?: boolean
onUnset: () => void
color: ColorValue
onChange: (color: ColorValue) => void
}

const ColorPickerInner = (props: ColorPickerProps) => {
const {
width,
color: {rgb, hex, hsv, hsl},
onChange,
onUnset,
disableAlpha,
colorList,
readOnly,
} = props
const {width, color, onChange, onUnset, disableAlpha, colorList, readOnly} = props
const {rgb, hex, hsv, hsl} = color

if (!hsl || !hsv) {
return null
}

const handleHsvaChange = (newHsva: HsvaColor) => {
const newRgba = hsvaToRgba(newHsva)
const newHex = hsvaToHex(newHsva)
const newHsla = hsvaToHsla(newHsva)
onChange({
hex: newHex,
rgb: newRgba,
hsv: newHsva,
hsl: newHsla,
})
}

return (
<div style={{width}}>
<Card padding={1} border radius={1}>
<Stack space={2}>
{!readOnly && (
<>
<Card overflow="hidden" style={{position: 'relative', height: '5em'}}>
<Saturation onChange={onChange} hsl={hsl} hsv={hsv} />
<Saturation
hsva={hsv}
onChange={handleHsvaChange}
style={{width: '100%', height: '100%'}}
/>
</Card>

<Card
Expand All @@ -68,7 +111,12 @@ const ColorPickerInner = (props: ColorPickerProps) => {
overflow="hidden"
style={{position: 'relative', height: '10px'}}
>
<Hue hsl={hsl} onChange={!readOnly && onChange} />
<Hue
hue={hsv.h}
onChange={(newHue) => handleHsvaChange({...hsv, ...newHue})}
pointer={Pointer}
style={{width: '100%', height: '100%'}}
/>
</Card>

{!disableAlpha && (
Expand All @@ -78,7 +126,13 @@ const ColorPickerInner = (props: ColorPickerProps) => {
overflow="hidden"
style={{position: 'relative', height: '10px', background: '#fff'}}
>
<Alpha rgb={rgb} hsl={hsl} onChange={onChange} />
<Checkboard />
<Alpha
hsva={hsv}
onChange={(newAlpha) => handleHsvaChange({...hsv, ...newAlpha})}
pointer={Pointer}
style={{width: '100%', height: '100%'}}
/>
</Card>
)}
</>
Expand All @@ -90,13 +144,7 @@ const ColorPickerInner = (props: ColorPickerProps) => {
overflow="hidden"
style={{position: 'relative', minWidth: '4em', background: '#fff'}}
>
<Checkboard
size={8}
white="transparent"
grey="rgba(0,0,0,.08)"
// oxlint-disable-next-line no-unsafe-type-assertion
renderers={{} as {canvas: unknown}}
/>
<Checkboard />
<ColorBox
style={{
backgroundColor: `rgba(${rgb?.r},${rgb?.g},${rgb?.b},${rgb?.a})`,
Expand Down Expand Up @@ -137,7 +185,7 @@ const ColorPickerInner = (props: ColorPickerProps) => {
rgb={rgb}
hsl={hsl}
hex={hex}
onChange={onChange}
onChange={handleHsvaChange}
disableAlpha={disableAlpha}
/>
</Box>
Expand All @@ -147,21 +195,18 @@ const ColorPickerInner = (props: ColorPickerProps) => {
</Flex>
)}
</Flex>
{colorList && <ColorList colors={colorList} onChange={onChange} />}
{colorList && <ColorList colors={colorList} onChange={handleHsvaChange} />}
</Stack>
</Card>
</div>
)
}

const ColorPicker = CustomPicker(ColorPickerInner)

const DEFAULT_COLOR: ColorValue & {source: string} = {
const DEFAULT_COLOR: ColorValue = {
hex: '#24a3e3',
hsl: {h: 200, s: 0.7732, l: 0.5156, a: 1},
hsv: {h: 200, s: 0.8414, v: 0.8901, a: 1},
rgb: {r: 46, g: 163, b: 227, a: 1},
source: 'hex',
}

export default function ColorInput(props: ObjectInputProps): React.JSX.Element {
Expand Down Expand Up @@ -196,7 +241,7 @@ export default function ColorInput(props: ObjectInputProps): React.JSX.Element {
return (
<>
{value && value.hex ? (
<ColorPicker
<ColorPickerInner
color={value}
onChange={(nextColor) =>
startTransition(() => {
Expand Down
79 changes: 66 additions & 13 deletions plugins/@sanity/color-input/src/ColorList.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import type {Color, ColorChangeHandler} from 'react-color'
import type {HsvaColor} from '@uiw/react-color'

import {Flex} from '@sanity/ui'
import {hexToHsva} from '@uiw/react-color'
import {styled} from 'styled-components'
import tinycolor from 'tinycolor2'

import type {ColorValue} from './types'

const ColorListWrap = styled(Flex)`
gap: 0.25em;
`
Expand All @@ -15,8 +18,7 @@ const ColorBoxContainer = styled.div`
position: relative;
overflow: hidden;
border-radius: 3px;
background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAADFJREFUOE9jZGBgEGHAD97gk2YcNYBhmIQBgWSAP52AwoAQwJvQRg1gACckQoC2gQgAIF8IscwEtKYAAAAASUVORK5CYII=')
left center #fff;
background: repeating-conic-gradient(rgba(0, 0, 0, 0.08) 0% 25%, #fff 0% 50%) 0 0 / 16px 16px;
`

const ColorBox = styled.div`
Expand All @@ -28,24 +30,75 @@ const ColorBox = styled.div`
z-index: 1;
`

// Color format interfaces for preset colors
interface RgbColor {
r: number
g: number
b: number
a?: number
}

interface HslColor {
h: number
s: number
l: number
a?: number
}

interface HsvColor {
h: number
s: number
v: number
a?: number
}

interface HexColor {
hex: string
}

type PresetColor = string | ColorValue | RgbColor | HslColor | HsvColor | HexColor

interface ValidatedColor {
color: Color
color: PresetColor
backgroundColor: string
hex: string
}

interface ColorListProps {
colors?: Array<Color>
onChange: ColorChangeHandler<Color>
colors?: PresetColor[]
onChange: (color: HsvaColor) => void
}

const validateColors = (colors: Array<Color>) =>
colors.reduce((cls: Array<ValidatedColor>, c) => {
// @ts-expect-error fix types later
const color = c.hex ? tinycolor(c.hex) : tinycolor(c)
if (color.isValid()) {
const validateColors = (colors: PresetColor[]) =>
colors.reduce((cls: ValidatedColor[], c) => {
// Handle various color formats: hex string, {hex}, {r,g,b}, {h,s,l}, {h,s,v}
let color
if (typeof c === 'string') {
color = tinycolor(c)
} else if ('hex' in c && typeof c.hex === 'string') {
color = tinycolor(c.hex)
} else if ('r' in c) {
// RGB(A) format
color = tinycolor({r: c.r, g: c.g, b: c.b, a: c.a})
} else if ('h' in c && 's' in c) {
if ('v' in c) {
// HSV(A) format
color = tinycolor.fromRatio({h: c.h / 360, s: c.s / 100, v: c.v / 100, a: c.a ?? 1})
} else if ('l' in c) {
// HSL(A) format
color = tinycolor.fromRatio({h: c.h / 360, s: c.s / 100, l: c.l / 100, a: c.a ?? 1})
} else {
return cls
}
} else {
return cls
}

if (color && color.isValid()) {
cls.push({
color: c,
backgroundColor: color.toRgbString(),
hex: color.toHexString(),
})
}
return cls
Expand All @@ -55,11 +108,11 @@ export function ColorList({colors, onChange}: ColorListProps): React.JSX.Element
if (!colors) return null
return (
<ColorListWrap wrap="wrap">
{validateColors(colors).map(({color, backgroundColor}, idx) => (
{validateColors(colors).map(({backgroundColor, hex}, idx) => (
<ColorBoxContainer
key={`${backgroundColor}-${idx}`}
onClick={() => {
onChange(color)
onChange(hexToHsva(hex))
}}
>
<ColorBox style={{background: backgroundColor}} />
Expand Down
Loading