Repository: hxf31891/react-gradient-color-picker
Branch: main
Commit: e97eba085d67
Files: 42
Total size: 149.8 KB
Directory structure:
gitextract__r_hbhn9/
├── .github/
│ └── workflows/
│ ├── pull-request.yml
│ └── release-package.yml
├── .gitignore
├── .npmignore
├── .nvmrc
├── .prettierrc
├── LICENSE
├── README.md
├── biome.json
├── package.json
├── src/
│ ├── components/
│ │ ├── AdvancedControls.tsx
│ │ ├── ComparibleColors.tsx
│ │ ├── Controls.tsx
│ │ ├── EyeDropper.tsx
│ │ ├── GradientBar.tsx
│ │ ├── GradientControls.tsx
│ │ ├── Hue.tsx
│ │ ├── Inputs.tsx
│ │ ├── Opacity.tsx
│ │ ├── Picker.tsx
│ │ ├── Portal.tsx
│ │ ├── Presets.tsx
│ │ ├── Square.tsx
│ │ ├── icon.tsx
│ │ └── index.tsx
│ ├── constants.ts
│ ├── context.tsx
│ ├── hooks/
│ │ ├── useColorPicker.ts
│ │ ├── usePaintHue.ts
│ │ └── usePaintSquare.ts
│ ├── index.ts
│ ├── shared/
│ │ └── types.ts
│ ├── styles/
│ │ ├── darkStyles.ts
│ │ └── styles.ts
│ └── utils/
│ ├── converters.ts
│ ├── formatters.ts
│ ├── gradientParser.ts
│ └── utils.ts
├── test/
│ ├── gradientParser.spec.js
│ └── utils.spec.js
├── tsconfig.build.json
└── tsconfig.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/pull-request.yml
================================================
name: Run tests
on: pull_request
jobs:
run-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 16
- run: npm i -g npm@latest
- run: npm ci
- run: npm test
run-eslint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 16
- run: npm i -g npm@latest
- run: npm ci
- run: npm run eslint
================================================
FILE: .github/workflows/release-package.yml
================================================
name: Release Package
on:
release:
types: [created]
jobs:
run-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 16
- run: npm i -g npm@latest
- run: npm ci
- run: npm test
run-eslint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 16
- run: npm i -g npm@latest
- run: npm ci
- run: npm run eslint
publish-gpr:
needs:
- run-tests
- run-eslint
runs-on: ubuntu-latest
permissions:
packages: write
contents: read
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 16
registry-url: https://npm.pkg.github.com/
- run: npm i -g npm@latest
- run: npm ci
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}}
================================================
FILE: .gitignore
================================================
# OS
.DS_Store
# Cache
.cache
.playwright
.tmp
*.tsbuildinfo
.eslintcache
# Yarn
.pnp.*
**/.yarn/*
!**/.yarn/patches
!**/.yarn/plugins
!**/.yarn/releases
!**/.yarn/sdks
!**/.yarn/versions
# Project-generated directories and files
coverage
dist
node_modules
playwright-report
test-results
package.tgz
# Logs
npm-debug.log
yarn-error.log
# .env files
**/.env
**/.env.*
!**/.env.example
================================================
FILE: .npmignore
================================================
/*.log
/*.DS_Store
/.idea
/.gitignore
/node_modules/
/.github/
/src/
/babel.config.js
**/*.spec.js
/.eslintignore
/.prettierrc
/.eslintrc.js
================================================
FILE: .nvmrc
================================================
v16
================================================
FILE: .prettierrc
================================================
{
"endOfLine": "lf",
"semi": false,
"singleQuote": true,
"useTabs": false,
"trailingComma": "es5"
}
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2022 Harry Fox
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
[![Npm Version][npm-version-image]][npm-version-url]
[![Downloads][downloads-image]][downloads-url]
[![License][license-image]][license-url]
# react-best-gradient-color-picker
- Customizable, easy to use color and gradient picker for React.js
- Simply pass in an rgba or css gradient string as value and an onChange handler
- Variety of optional tools like eye dropper, advanced color settings, and color guide
- use the useColorPicker hook for complete control over of the picker
- You can completly customize the UI by hiding the included elements and using the hook to build your own
- You can also customize preset options by passing in an array of rgba colors (see custom presets below)
<br />
<img alt="" src="https://gradient-package-demo.web.app/gradientPickerImg.png" width="100%" />
<br />
<a id="item-one"></a>
## Install
```
npm install react-best-gradient-color-picker
```
```
yarn add react-best-gradient-color-picker
```
<a id="item-two"></a>
## Demo
See the picker in action [here](https://gradient-package-demo.web.app/)
<br />
**Table of Contents**
- [Basic Example](#item-three)
- [Props](#item-four)
- [API](#item-five)
- [useColorPicker Hook](#item-six)
- [Hook Basic Example](#item-seven)
- [Hook Functions](#item-eight)
- [Hook State](#item-nine)
- [More Hook Examples](#item-ten)
- [Styling](#item-fifteen)
- [Legacy Options](#item-eleven)
- [Roadmap](#item-twelve)
- [License](#item-thirteen)
- [Acknowledgments](#item-fourteen)
<br />
<a id="item-three"></a>
## Basic Example
```js
import React from 'react'
import ColorPicker from 'react-best-gradient-color-picker'
function MyApp() {
const [color, setColor] = useState('rgba(255,255,255,1)');
return <ColorPicker value={color} onChange={setColor} />
}
```
<br />
<a id="item-four"></a>
### Props
| Name | Type | Default | Description |
|---------------------|--------------| ----------------------- |---------------------------------------------------------------------------|
| value | `string` | 'rgba(175, 51, 242, 1)' | The starting color |
| width | `int` | 294 | (optional) The width of the picker |
| height | `int` | 294 | (optional) The height of the picker |
| hideInputs | `boolean` | `false` | (optional) hide the hex and rgba inputs |
| hideOpacity | `boolean` | `false` | (optional) hide the opacity bar |
| hideHue | `boolean` | `false` | (optional) hide the hue bar |
| hideControls | `boolean` | `false` | (optional) hide the entire top row of various control btns |
| hideColorTypeBtns | `boolean` | `false` | (optional) hide the solid/gradient buttons |
| hidePresets | `boolean` | `false` | (optional) hide the preset color options |
| hideEyeDrop | `boolean` | `false` | (optional) hide (and disable the eye dropper tool |
| hideAdvancedSliders | `boolean` | `false` | (optional) hide the additional sliders (saturation, luminence, brightness |
| hideColorGuide | `boolean` | `false` | (optional) hide the color guide, a tool that shows color pairings |
| hideInputType | `boolean` | `false` | (optional) hide the input type selector, looking the type |
| hideGradientType | `boolean` | `false` | (optional) hide the linear/circular gradient type toggle (only relevant in gradient mode)|
| hideGradientAngle | `boolean` | `false` | (optional) hide the gradient angle input (only relevant in gradient mode with a linear gradient)|
| hideGradientStop | `boolean` | `false` | (optional) hide the gradient point stop input (only relevant in gradient mode)|
| hideGradientControls| `boolean` | `false` | (optional) hide the all gradient controls (the bar that appears below top controls when in gradient mode)|
| hidePickerSquare | `boolean` | `false` | (optional) hide the main picker color swatch (the square that appears at the top)|
| showHexAlpha | `boolean` | `false` | (optional) add alpha (AA) channel to hex value which represents the opacity of the color|
| presets | `array` | ['rgba(0,0,0,1)', ...] | (optional) pass in custom preset options ['rgba()', 'rgba()', ..] |
| locales | `object` | { CONTROLS: { SOLID: 'Solid', GRADIENT: 'Gradient' }} | (optional) pass in custom locales |
| disableDarkMode | `boolean` | false | (optional) disable automatic dark mode style adjustments |
| disableLightMode | `boolean` | false | (optional) force the component to only use the dark mode styles |
| className | `string` | '' | (optional) a CSS class for the picker parent (see styling for more options)|
| idSuffix | `string` | '' | (optional) Adds a suffix to all the ids of the picker elements, useful when using multiple pickers on the same page|
<a id="item-five"></a>
### API
| Name | Description |
| ---------------- | ---------------------------------------------------------------- |
| onChange | A function to update color value |
<br />
<a id="item-six"></a>
# useColorPicker
- Take complete control of the picker
- Get current state
- Convert between color types
<a id="item-seven"></a>
## Basic Example
- Initialize the hook by passing in the same color value and onChange handler
```js
import React from 'react'
import ColorPicker, { useColorPicker } from 'react-best-gradient-color-picker'
function MyApp() {
const [color, setColor] = useState('linear-gradient(90deg, rgba(96,93,93,1) 0%, rgba(255,255,255,1) 100%)');
const { setSolid, setGradient } = useColorPicker(color, setColor);
return(
<div>
<button onClick={setSolid}>Solid</button>
<button onClick={setGradient}>Gradient</button>
<ColorPicker value={color} onChange={setColor} />
</div>
)
}
```
<a id="item-eight"></a>
### Included Functions
| Name | Arguments | Description |
| ---------------- | ---------------- | ---------------------------------------------------------------- |
| handleChange | value (rgba string)| Most useful for setting color value of the selectedPoint without overwriting entire gradient string. Only pass this function a single color value, not a gradient |
| setLinear | | Change the type of gradient to linear |
| setRadial | | Change the type of gradient to radial |
| setDegrees | degrees (num, 0 - 360)| Change the degrees of a linear gradient |
| setSolid | (optional) new solid color (rgba string) | Change the pickers color mode from gradient to solid |
| setGradient | (optional) new gradient (CSS gradient) | Change the pickers color mode from solid to gradient |
| setR | value (num, 0 - 255) | Update the red value of the color |
| setG | value (num, 0 - 255) | Update the green value of the color |
| setB | value (num, 0 - 255) | Update the blue value of the color |
| setA | value (num, 0 - 100) | Update the opacity (alpha) of a color |
| setHue | value (num, 0 - 360) | Update the hue of a color |
| setSaturation | value (num, 0 - 100) | Update the saturation of a color |
| setLightness | value (num, 0 - 100) | Update the lightness of a color |
| valueToHSL | | Get the current value in HSL |
| valueToHSV | | Get the current value in HSV |
| valueToHex | | Get the current value in HEX |
| valueToCmyk | | Get the current value in CMYK |
| setSelectedPoint | index of point (num) | Update which individual color of a gradient is in focus |
| deletePoint | index of point (num) | Delete one of the gradients colors |
| addPoint | position of point (num, 0 - 100) | Add a new color to the gradient |
| setPointLeft | value (num, 0 - 100) | Update the position (left) of the currently selected gradient color |
| getGradientObject| | get the gradients value parsed into a key/value object (see example below)|
<a id="item-nine"></a>
### Available State
| Name | Description |
| ---------------- | ---------------------------------------------------------------- |
| selectedPoint | returns index of which color point of a gradient is currently selected |
| isGradient | returns which mode the picker is in, solid or gradient |
| gradientType | which gradient type is currently selected, linear or radial |
| degrees | current degrees of a radial gradient |
| currentLeft | the position of the selected gradient color |
| rgbaArr | get the current rgba values in an array |
| hslArr | get the current hsl values in an array |
<a id="item-ten"></a>
## Various Customization Examples
### Custom Gradient Controls
```js
import React from 'react'
import ColorPicker, { useColorPicker } from 'react-best-gradient-color-picker'
function MyApp() {
const [color, setColor] = useState('linear-gradient(90deg, rgba(96,93,93,1) 0%, rgba(255,255,255,1) 100%)');
const { gradientType, setLinear, setRadial, addPoint, deletePoint, degrees, setDegrees, setPointLeft, currentLeft, selectedPoint } = useColorPicker(color, setColor);
return(
<div>
<button onClick={setLinear}>Linear</button>
<button onClick={setRadial}>Radial</button>
{gradientType === 'linear-gradient' && <input value={degrees} onChange={(e) => setDegrees(e.target.value)} />}
<input value={currentLeft} onChange={(e) => setPointLeft(e.target.value)} />
<button onClick={() => addPoint(50)}>Add Color</button>
<button onClick={() => deletePoint(selectedPoint)}>Delete Color</button>
<ColorPicker value={color} onChange={setColor} hideControls={true} />
</div>
)
}
```
### Custom RGBA Inputs
```js
import React from 'react'
import ColorPicker, { useColorPicker } from 'react-best-gradient-color-picker'
function MyApp() {
const [color, setColor] = useState('linear-gradient(90deg, rgba(96,93,93,1) 0%, rgba(255,255,255,1) 100%)');
const { setR, setG, setB, setA, rgbaArr } = useColorPicker(color, setColor);
return(
<div>
<input value={rgbaArr[0]} onChange={(e) => setR(e.target.value)} />
<input value={rgbaArr[1]} onChange={(e) => setG(e.target.value)} />
<input value={rgbaArr[2]} onChange={(e) => setB(e.target.value)} />
<input value={rgbaArr[3]} onChange={(e) => setA(e.target.value)} />
<ColorPicker value={color} onChange={setColor} hideInputs={true} />
</div>
)
}
```
### Conversions
```js
import React from 'react'
import ColorPicker, { useColorPicker } from 'react-best-gradient-color-picker'
function MyApp() {
const [color, setColor] = useState('linear-gradient(90deg, rgba(96,93,93,1) 0%, rgba(255,255,255,1) 100%)');
const { valueToHSL, valueToHSV, valueToHex, valueToCmyk, rgbaArr, hslArr } = useColorPicker(color, setColor);
const hslString = valueToHSL();
const hsvString = valueToHSV();
const hexString = valueToHex();
const cmykString = valueToCmyk();
const rgbaArray = rgbaArr;
const hslArray = hslArr;
return(
<div>
<ColorPicker value={color} onChange={setColor} />
</div>
)
}
```
### Custom Presets Example
```js
import React from 'react'
import ColorPicker from 'react-best-gradient-color-picker'
const customPresets = [
'rgba(34, 164, 65, 1)',
'rgba(210, 18, 40, .5)',
'rgba(90, 110, 232, 1)',
'rgba(65, 89, 56, 1)',
'rgba(98, 189, 243, 1)',
'rgba(255, 210, 198, 1)',
'rgba(94, 94, 94, 1)'
] //max 18 colors, you can pass in more but the array will be sliced to the first 18
function MyApp() {
const [color, setColor] = useState('rgba(255,255,255,1');
return <ColorPicker value={color} onChange={setColor} presets={customPresets} />
}
```
You may also want to provide the users recently used colors in lieu of preset options. This can be easily accomplished use the hook.
```js
import React from 'react'
import ColorPicker, { useColorPicker } from 'react-best-gradient-color-picker'
function MyApp() {
const [color, setColor] = useState('linear-gradient(90deg, rgba(96,93,93,1) 0%, rgba(255,255,255,1) 100%)');
const { previousColors } = useColorPicker(color, setColor);
return(
<div>
<ColorPicker value={color} onChange={setColor} presets={previousColors} />
</div>
)
}
```
### Custom Locales Example
You can pass custom locales via `locales` prop.
```js
import React from 'react'
import ColorPicker, { useColorPicker } from 'react-best-gradient-color-picker'
function MyApp() {
const customLocales = {
CONTROLS: {
SOLID: 'Obične',
GRADIENT: 'Gradijent',
},
}
return (
<div>
<ColorPicker locales={customLocales} />
</div>
)
}
```
### Getting Value in Object Form
The picker returns the new value as a css gradient string but you may need it parsed as an object. This can easily be accomplised by using the getGradientObject function returned by the useColorPicker hook like so:
```js
import React from 'react'
import ColorPicker, { useColorPicker } from 'react-best-gradient-color-picker'
function MyApp() {
const [color, setColor] = useState('linear-gradient(90deg, rgba(96,93,93,1) 0%, rgba(255,255,255,1) 100%)');
const { getGradientObject } = useColorPicker(color, setColor);
const gradientObject = getGradientObject();
// example value
// {
// "isGradient": true,
// "gradientType": "linear-gradient",
// "degrees": "40deg",
// "colors": [
// {
// "value": "rgba(27,107,235,1)",
// "left": 0
// },
// {
// "value": "rgba(25,245,157,1)",
// "left": 100
// }
// ]
// }
return(
<div>
<ColorPicker value={color} onChange={setColor} presets={previousColors} />
</div>
)
}
```
### Only Gradients Example
If you would like to not allow selection of solid colors disable the color type buttons and feed in the initial value as a gradient like below:
NOTE: the same can be done in reverse to only allow selection of solid colors
```js
import React from 'react'
import ColorPicker, { useColorPicker } from 'react-best-gradient-color-picker'
function MyApp() {
const [color, setColor] = useState('linear-gradient(90deg, rgba(96,93,93,1) 0%, rgba(255,255,255,1) 100%)');
return(
<div>
<ColorPicker
value={color}
onChange={setColor}
hideColorTypeBtns={true}
/>
</div>
)
}
```
<br />
<a id="item-fifteen"></a>
## Styling
Most of the pickers components have inline styles applied to them, these essential function as classNames. In order to update, identify the key being applied to the desired component and add any styles as a nested object to the `style` object which can be passed into the picker. See below list of style keys and example.
<br />
| Key | Description |
| -------------------------------- | ------------------------------------------------ |
| body | ComingSoon |
| rbgcpControlBtnWrapper | ComingSoon |
| rbgcpControlBtn | ComingSoon |
| rbgcpControlIconBtn | ComingSoon |
| rbgcpControlIcon | ComingSoon |
| rbgcpColorModelDropdown | ComingSoon |
| rbgcpEyedropperCover | ComingSoon |
| rbgcpControlInputWrap | ComingSoon |
| rbgcpControlInput | ComingSoon |
| rbgcpInputLabel | ComingSoon |
| rbgcpInput | ComingSoon |
| rbgcpHandle | ComingSoon |
| rbgcpCanvasWrapper | ComingSoon |
| rbgcpOpacityOverlay | ComingSoon |
| rbgcpGradientHandleWrap | ComingSoon |
| rbgcpGradientHandle | ComingSoon |
| rbgcpControlIcon2 | ComingSoon |
| rbgcpControlBtnSelected | ComingSoon |
| rbgcpComparibleLabel | ComingSoon |
| rbgcpColorModelDropdownBtn | ComingSoon |
| rbgcpStopInputWrap | ComingSoon |
| rbgcpStopInput | ComingSoon |
| rbgcpDegreeInputWrap | ComingSoon |
| rbgcpDegreeInput | ComingSoon |
| rbgcpDegreeIcon | ComingSoon |
| rbgcpEyedropperBtn | ComingSoon |
| rbgcpHexInput | ComingSoon |
| rbgcpInputsWrap | ComingSoon |
<br />
<a id="item-eleven"></a>
## LEGACY V1 - Manual Control - Customizing UI
This still works, although most functions are available through the useColorPicker hook, if there is something you need that is not available you could use the below methods to create your desired functionality.
The state of the picker is determined by parsing the value string. You can update props like colorType (solid/gradient), gradientType (linear/radial), gradientDegrees, hex, rgba, opacity and hue simply by updating the value you are passing into the component. Let's say you want to change the colorType from gradient to solid:
```js
import React from 'react'
import ColorPicker from 'react-best-gradient-color-picker'
function MyApp() {
const [color, setColor] = useState('linear-gradient(90deg, rgba(96,93,93,1) 0%, rgba(255,255,255,1) 100%)');
const setSolid = () => {
setColor('rgba(255,255,255,1)') //color could be any rgba value
}
return(
<div>
<button onClick={setSolid}>Solid</button>
<ColorPicker value={color} onChange={setColor} />
</div>
)
}
```
The same can be done in inverse to change colorType from solid to gradient:
```js
const setGradient = () => {
setColor('linear-gradient(90deg, rgba(96,93,93,1) 0%, rgba(255,255,255,1) 100%)')
}
```
Example toggling gradientType
```js
const setLinear = () => {
setColor('linear-gradient(90deg, rgba(96,93,93,1) 0%, rgba(255,255,255,1) 100%)')
}
const setRadial = () => {
setColor('radial-gradient(circle, rgba(96,93,93,1) 0%, rgba(255,255,255,1) 100%)')
}
```
Custom linear-gradient degrees input
```js
import React from 'react'
import ColorPicker from 'react-best-gradient-color-picker'
function MyApp() {
const [color, setColor] = useState('linear-gradient(90deg, rgba(96,93,93,1) 0%, rgba(255,255,255,1) 100%)');
const degrees = parseInt(value?.split(',')[0]?.split('(')[1])
const handleDegrees = (val) => {
let num = parseInt(val)
let nans = isNaN(num) ? 0 : num
let min = Math.max(nans, 0)
let max = Math.min(min, 360)
const remaining = value.split(/,(.+)/)[1]
setColor(`linear-gradient(${max}deg, ${remaining}`)
}
return(
<div>
<input value={degrees} onChange={(e) => handleDegrees(e.target.value)} />
<ColorPicker value={color} onChange={setColor} />
</div>
)
}
```
<a id="item-twelve"></a>
## Roadmap
1. enhanced mobile support
2. cross browser eye dropper issue
3. enhanced gradient parsing to allow additional gradient types
<a id="item-thirteen"></a>
## License
Code released under the [MIT](https://github.com/hxf31891/react-gradient-color-picker/blob/main/LICENSE) license.
[build-image]: https://img.shields.io/github/checks-status/hxf31891/react-gradient-color-picker/main?color=%23498af2
[license-image]: https://img.shields.io/npm/l/react-best-gradient-color-picker.svg?color=%23498af2
[license-url]: LICENSE
[downloads-image]: http://img.shields.io/npm/dm/react-best-gradient-color-picker.svg?color=%23498af2
[downloads-url]: http://npm-stat.com/charts.html?package=react-best-gradient-color-picker
[npm-version-image]: https://img.shields.io/npm/v/react-best-gradient-color-picker.svg?color=%23498af2
[npm-version-url]: https://www.npmjs.com/package/react-best-gradient-color-picker
<a id="item-fourteen"></a>
## Acknowledgments
Very special thank you to [Rafael Carício](https://github.com/rafaelcaricio) for his amazing work parsing gradient strings.
================================================
FILE: biome.json
================================================
{
"$schema": "https://biomejs.dev/schemas/1.8.3/schema.json",
"files": {
"ignore": [
".tsimp",
".yarn",
"coverage",
"dist",
".pnp.cjs",
".pnp.loader.mjs"
]
},
"formatter": {
"lineWidth": 100,
"indentStyle": "space"
},
"linter": {
"rules": {
"complexity": {
"noUselessSwitchCase": "off"
},
"suspicious": {
"noConsoleLog": "warn"
}
}
},
"css": {
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 100,
"quoteStyle": "single"
}
},
"javascript": {
"formatter": {
"quoteStyle": "single"
}
},
"overrides": [
{
"include": ["**/package.json"],
"formatter": {
"lineWidth": 1
}
},
{
"include": ["**/vite.config.ts"],
"linter": {
"rules": {
"suspicious": {
"noConsoleLog": "off"
}
}
}
}
]
}
================================================
FILE: package.json
================================================
{
"name": "react-best-gradient-color-picker",
"version": "3.0.14",
"description": "An easy to use color/gradient picker for React.js",
"type": "module",
"sideEffects": [
"*.css"
],
"main": "./dist/cjs/index.js",
"module": "./dist/esm/index.js",
"source": "./src/index.ts",
"types": "./dist/cjs/index.d.ts",
"exports": {
".": {
"import": "./dist/esm/index.js",
"require": "./dist/cjs/index.js"
}
},
"scripts": {
"build": "yarn build-js",
"build-js": "yarn build-js-esm && yarn build-js-cjs && yarn build-js-cjs-package",
"build-js-esm": "tsc --project tsconfig.build.json --outDir dist/esm",
"build-js-cjs": "tsc --project tsconfig.build.json --outDir dist/cjs --module commonjs --moduleResolution node --verbatimModuleSyntax false",
"build-js-cjs-package": "echo '{\n \"type\": \"commonjs\"\n}' > dist/cjs/package.json",
"clean": "rimraf dist",
"format": "biome format",
"lint": "biome lint",
"prepack": "yarn clean && yarn build",
"test": "yarn lint && yarn tsc && yarn format && yarn unit",
"tsc": "tsc",
"unit": "vitest",
"watch": "yarn build-js-esm --watch & yarn build-js-cjs --watch & node --eval"
},
"repository": {
"type": "git",
"url": "git+https://github.com/hxf31891/react-gradient-color-picker.git"
},
"keywords": [
"gradient",
"react",
"color",
"picker",
"react.js",
"tool",
"editor"
],
"author": {
"name": "Harry Fox",
"email": "hxfox1@gmail.com"
},
"dependencies": {
"html2canvas": "^1.4.1",
"lodash.throttle": "^4.1.1",
"tinycolor2": "1.4.2"
},
"devDependencies": {
"@testing-library/jest-dom": "^6.0.0",
"@testing-library/react": "^14.0.0",
"@types/lodash.throttle": "*",
"@types/node": "*",
"@types/react": "*",
"@types/reactcss": "^1.2.11",
"@types/tinycolor2": "^1.4.6",
"cpy-cli": "^3.1.1",
"happy-dom": "^12.6.0",
"nodemon": "^3.0.0",
"prettier": "^3.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"rimraf": "^3.0.0",
"typescript": "^5.3.2",
"vitest": "^1.0.2"
},
"peerDependencies": {
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
},
"files": [
"dist",
"src"
],
"license": "MIT",
"bugs": {
"url": "https://github.com/hxf31891/react-gradient-color-picker/issues"
},
"homepage": "https://gradient-package-demo.web.app/",
"publishConfig": {
"@hxf31891:registry": "https://npm.pkg.github.com"
}
}
================================================
FILE: src/components/AdvancedControls.tsx
================================================
import React, { useState, useRef, useEffect } from 'react'
import { Styles, Config } from '../shared/types.js'
import { getHandleValue } from '../utils/utils.js'
import { usePicker } from '../context.js'
import {
usePaintSat,
usePaintLight,
usePaintBright,
} from '../hooks/usePaintHue.js'
import tinycolor from 'tinycolor2'
const AdvBar = ({
value,
reffy,
label,
config,
callback,
squareWidth,
openAdvanced,
defaultStyles,
pickerIdSuffix,
}: {
reffy: any
value: number
label: string
config: Config
squareWidth: number
openAdvanced: boolean
defaultStyles: Styles
pickerIdSuffix: string
callback: (arg0: number) => void
}) => {
const { barSize } = config
const [dragging, setDragging] = useState<boolean>(false)
const [handleTop, setHandleTop] = useState<number>(2)
const left = value * (squareWidth - 18)
useEffect(() => {
setHandleTop(reffy?.current?.offsetTop - 2)
}, [openAdvanced, reffy])
const stopDragging = () => {
setDragging(false)
}
const handleMove = (e: any) => {
if (dragging) {
callback(getHandleValue(e, barSize))
}
}
const handleClick = (e: any) => {
if (!dragging) {
callback(getHandleValue(e, barSize))
}
}
const handleDown = () => {
setDragging(true)
}
useEffect(() => {
const handleUp = () => {
stopDragging()
}
window.addEventListener('mouseup', handleUp)
return () => {
window.removeEventListener('mouseup', handleUp)
}
}, [])
return (
<div style={{ width: '100%', padding: '3px 0px 3px 0px' }}>
<div
onMouseMove={(e) => handleMove(e)}
// className="rbgcp-advanced-bar-wrap"
style={{ cursor: 'resize', position: 'relative' }}
id={`rbgcp-advanced-bar-${label}-wrapper${pickerIdSuffix}`}
>
<div
style={{ left, top: handleTop, ...defaultStyles.rbgcpHandle }}
id={`rbgcp-advanced-bar-${label}-handle${pickerIdSuffix}`}
// className="rbgcp-advanced-bar-handle"
onMouseDown={handleDown}
role="button"
tabIndex={0}
/>
<div
style={{
textAlign: 'center',
color: '#fff',
fontSize: 12,
fontWeight: 500,
lineHeight: 1,
position: 'absolute',
left: '50%',
transform: 'translate(-50%, 0%)',
top: handleTop + 2,
zIndex: 10,
textShadow: '1px 1px 1px rgba(0,0,0,.6)',
}}
id={`rbgcp-advanced-bar-${label}-label${pickerIdSuffix}`}
// className="rbgcp-advanced-bar-label"
onMouseMove={(e) => handleMove(e)}
onClick={(e) => handleClick(e)}
tabIndex={0}
role="button"
onKeyDown={() => {
return
}}
>
{label}
</div>
<canvas
ref={reffy}
height="14px"
width={`${squareWidth}px`}
onClick={(e) => handleClick(e)}
// className="rbgcp-advanced-bar-canvas"
style={{ position: 'relative', borderRadius: 14 }}
id={`rbgcp-advanced-bar-${label}-canvas${pickerIdSuffix}`}
/>
</div>
</div>
)
}
const AdvancedControls = ({ openAdvanced }: { openAdvanced: boolean }) => {
const { config, tinyColor, handleChange, squareWidth, hc, defaultStyles, pickerIdSuffix } = usePicker()
const { s, l } = tinyColor.toHsl()
const satRef = useRef(null)
const lightRef = useRef(null)
const brightRef = useRef(null)
usePaintSat(satRef, hc?.h, l * 100, squareWidth)
usePaintLight(lightRef, hc?.h, s * 100, squareWidth)
usePaintBright(brightRef, hc?.h, s * 100, squareWidth)
const satDesat = (value: number) => {
const { r, g, b } = tinycolor({ h: hc?.h, s: value / 100, l }).toRgb()
handleChange(`rgba(${r},${g},${b},${hc?.a})`)
}
const setLight = (value: number) => {
const { r, g, b } = tinycolor({ h: hc?.h, s, l: value / 100 }).toRgb()
handleChange(`rgba(${r},${g},${b},${hc?.a})`)
}
const setBright = (value: number) => {
const { r, g, b } = tinycolor({
h: hc?.h,
s: hc?.s * 100,
v: value,
}).toRgb()
handleChange(`rgba(${r},${g},${b},${hc?.a})`)
}
return (
<div
style={{
width: '100%',
height: openAdvanced ? 98 : 0,
transition: 'all 120ms linear',
}}
id={`rbgcp-advanced-controls-wrapper${pickerIdSuffix}`}
// className="rbgcp-advanced-controls-wrap"
>
<div
style={{
paddingTop: 11,
display: openAdvanced ? 'flex' : 'none',
flexDirection: 'column',
justifyContent: 'space-between',
height: openAdvanced ? 98 : 0,
overflow: 'hidden',
transition: 'height 100ms linear',
}}
id={`rbgcp-advanced-controls-inner${pickerIdSuffix}`}
// className="rbgcp-advanced-controls-inner"
>
<AdvBar
value={s}
reffy={satRef}
config={config}
label="Saturation"
callback={satDesat}
squareWidth={squareWidth}
openAdvanced={openAdvanced}
defaultStyles={defaultStyles}
pickerIdSuffix={pickerIdSuffix}
/>
<AdvBar
value={l}
config={config}
reffy={lightRef}
label="Lightness"
callback={setLight}
squareWidth={squareWidth}
openAdvanced={openAdvanced}
defaultStyles={defaultStyles}
pickerIdSuffix={pickerIdSuffix}
/>
<AdvBar
value={hc?.v}
config={config}
reffy={brightRef}
label="Brightness"
callback={setBright}
squareWidth={squareWidth}
openAdvanced={openAdvanced}
defaultStyles={defaultStyles}
pickerIdSuffix={pickerIdSuffix}
/>
</div>
</div>
)
}
export default AdvancedControls
================================================
FILE: src/components/ComparibleColors.tsx
================================================
import React from 'react'
import { usePicker } from '../context.js'
const ComparibleColors = ({
openComparibles,
}: {
openComparibles: boolean
}) => {
const { tinyColor, handleChange, defaultStyles, pickerIdSuffix } = usePicker()
const analogous = tinyColor.analogous()
const monochromatic = tinyColor.monochromatic()
const triad = tinyColor.triad()
const tetrad = tinyColor.tetrad()
const handleClick = (tiny: any) => {
const { r, g, b, a } = tiny.toRgb()
handleChange(`rgba(${r},${g},${b},${a})`)
}
return (
<div
style={{
width: '100%',
transition: 'all 120ms linear',
height: openComparibles ? 216 : 0,
}}
id={`rbgcp-comparible-colors-wrapper${pickerIdSuffix}`}
// className="rbgcp-comparible-colors-wrap"
>
<div
style={{
paddingTop: 11,
display: openComparibles ? '' : 'none',
position: 'relative',
}}
id={`rbgcp-comparible-colors-inner${pickerIdSuffix}`}
// className="rbgcp-comparible-colors-inner"
>
<div
style={{
textAlign: 'center',
fontSize: 13,
fontWeight: 600,
position: 'absolute',
top: 6.5,
left: 2,
...defaultStyles.rbgcpComparibleLabel,
}}
id={`rbgcp-comparible-color-guide-label${pickerIdSuffix}`}
// className="rbgcp-comparible-colors-label"
>
Color Guide
</div>
<div
style={{
textAlign: 'center',
fontSize: 12,
fontWeight: 500,
marginTop: 3,
...defaultStyles.rbgcpComparibleLabel,
}}
id={`rbgcp-comparible-analogous-colors-label${pickerIdSuffix}`}
// className="rbgcp-comparible-colors-label"
>
Analogous
</div>
<div
style={{ borderRadius: 5, overflow: 'hidden', display: 'flex' }}
id={`rbgcp-comparible-analogous-colors${pickerIdSuffix}`}
// className="rbgcp-comparible-colors-colors"
>
{analogous?.map((c: any, key: number) => (
<div
key={key}
id={`rbgcp-comparible-analogous-color-${key}${pickerIdSuffix}`}
style={{ width: '20%', height: 30, background: c.toHexString() }}
onClick={() => handleClick(c)}
/>
))}
</div>
<div
style={{
textAlign: 'center',
fontSize: 12,
fontWeight: 500,
marginTop: 3,
...defaultStyles.rbgcpComparibleLabel,
}}
id={`rbgcp-comparible-monochromatic-colors-label${pickerIdSuffix}`}
// className="rbgcp-comparible-colors-label"
>
Monochromatic
</div>
<div
style={{
borderRadius: 5,
overflow: 'hidden',
display: 'flex',
justifyContent: 'flex-end',
}}
id={`rbgcp-comparible-monochromatic-colors${pickerIdSuffix}`}
// className="rbgcp-comparible-colors-colors"
>
{monochromatic?.map((c: any, key: number) => (
<div
key={key}
id={`rbgcp-comparible-monochromatic-color-${key}${pickerIdSuffix}`}
style={{ width: '20%', height: 30, background: c.toHexString() }}
onClick={() => handleClick(c)}
/>
))}
</div>
<div
style={{
textAlign: 'center',
fontSize: 12,
fontWeight: 500,
marginTop: 3,
...defaultStyles.rbgcpComparibleLabel,
}}
id={`rbgcp-comparible-triad-colors-label${pickerIdSuffix}`}
// className="rbgcp-comparible-colors-label"
>
Triad
</div>
<div
style={{
borderRadius: 5,
overflow: 'hidden',
display: 'flex',
justifyContent: 'flex-end',
}}
id={`rbgcp-comparible-triad-colors${pickerIdSuffix}`}
// className="rbgcp-comparible-colors-colors"
>
{triad?.map((c: any, key: number) => (
<div
key={key}
id={`rbgcp-comparible-triad-color-${key}${pickerIdSuffix}`}
style={{
width: 'calc(100% / 3)',
height: 28,
background: c.toHexString(),
}}
onClick={() => handleClick(c)}
/>
))}
</div>
<div
style={{
textAlign: 'center',
fontSize: 12,
fontWeight: 500,
marginTop: 3,
...defaultStyles.rbgcpComparibleLabel,
}}
id={`rbgcp-comparible-tetrad-colors-label${pickerIdSuffix}`}
// className="rbgcp-comparible-colors-label"
>
Tetrad
</div>
<div
style={{
borderRadius: 5,
overflow: 'hidden',
display: 'flex',
justifyContent: 'flex-end',
}}
id={`rbgcp-comparible-tetrad-colors${pickerIdSuffix}`}
// className="rbgcp-comparible-colors-colors"
>
{tetrad?.map((c: any, key: number) => (
<div
key={key}
id={`rbgcp-comparible-tetrad-color-${key}${pickerIdSuffix}`}
style={{ width: '25%', height: 28, background: c.toHexString() }}
onClick={() => handleClick(c)}
/>
))}
</div>
</div>
</div>
)
}
export default ComparibleColors
================================================
FILE: src/components/Controls.tsx
================================================
/* eslint-disable react/jsx-no-leaked-render */
/* eslint-disable jsx-a11y/no-static-element-interactions */
import React, { useState } from 'react'
import { SlidersIcon, InputsIcon, PaletteIcon } from './icon.js'
import { usePicker } from '../context.js'
import EyeDropper from './EyeDropper.js'
import AdvancedControls from './AdvancedControls.js'
import ComparibleColors from './ComparibleColors.js'
import GradientControls from './GradientControls.js'
import { LocalesProps } from '../shared/types.js'
import { colorTypeBtnStyles, controlBtnStyles, modalBtnStyles } from '../styles/styles.js'
const ColorTypeBtns = ({
hideColorTypeBtns,
setGradient,
isGradient,
setSolid,
locales,
}: {
hideColorTypeBtns?: boolean
isGradient?: boolean
setSolid: () => void
setGradient: () => void
locales?: LocalesProps
}) => {
const { defaultStyles, pickerIdSuffix } = usePicker()
if (hideColorTypeBtns) {
return <div style={{ width: 1 }} />
} else {
return (
<div
style={{
display: 'flex',
alignItems: 'center',
...defaultStyles.rbgcpControlBtnWrapper,
}}
id={`rbgcp-color-type-btns${pickerIdSuffix}`}
>
<div
onClick={setSolid}
id={`rbgcp-solid-btn${pickerIdSuffix}`}
style={colorTypeBtnStyles(!isGradient, defaultStyles)}
// className="rbgcp-control-btn rbgcp-solid-btn"
>
{locales?.CONTROLS?.SOLID}
</div>
<div
onClick={setGradient}
id={`rbgcp-gradient-btn${pickerIdSuffix}`}
style={colorTypeBtnStyles(isGradient ?? false, defaultStyles)}
// className="rbgcp-control-btn rbgcp-gradient-btn"
>
{locales?.CONTROLS?.GRADIENT}
</div>
</div>
)
}
}
const InputTypeDropdown = ({
openInputType,
setOpenInputType,
}: {
openInputType?: boolean
setOpenInputType: (arg0: boolean) => void
}) => {
const { inputType, setInputType, defaultStyles, pickerIdSuffix } = usePicker()
const vTrans = openInputType
? 'visibility 0ms linear'
: 'visibility 100ms linear 150ms'
const zTrans = openInputType
? 'z-index 0ms linear'
: 'z-index 100ms linear 150ms'
const oTrans = openInputType
? 'opacity 120ms linear'
: 'opacity 150ms linear 50ms'
const handleInputType = (e: any, val: string) => {
if (openInputType) {
e.stopPropagation()
setInputType(val)
setOpenInputType(false)
}
}
return (
<div
// className="rbgcp-color-model-dropdown"
style={{
visibility: openInputType ? 'visible' : 'hidden',
zIndex: openInputType ? '' : -100,
opacity: openInputType ? 1 : 0,
transition: `${oTrans}, ${vTrans}, ${zTrans}`,
...defaultStyles.rbgcpColorModelDropdown,
}}
id={`rbgcp-color-model-dropdown${pickerIdSuffix}`}
>
<div
id={`rbgcp-color-model-rgb-btn${pickerIdSuffix}`}
onClick={(e) => handleInputType(e, 'rgb')}
style={modalBtnStyles(inputType === 'rgb', defaultStyles)}
>
RGB
</div>
<div
id={`rbgcp-color-model-hsl-btn${pickerIdSuffix}`}
onClick={(e) => handleInputType(e, 'hsl')}
style={modalBtnStyles(inputType === 'hsl', defaultStyles)}
>
HSL
</div>
<div
id={`rbgcp-color-model-hsv-btn${pickerIdSuffix}`}
onClick={(e) => handleInputType(e, 'hsv')}
style={modalBtnStyles(inputType === 'hsv', defaultStyles)}
>
HSV
</div>
<div
id={`rbgcp-color-model-cmyk-btn${pickerIdSuffix}`}
onClick={(e) => handleInputType(e, 'cmyk')}
style={modalBtnStyles(inputType === 'cmyk', defaultStyles)}
>
CMYK
</div>
</div>
)
}
const Controls = ({
locales,
hideEyeDrop = false,
hideAdvancedSliders = false,
hideColorGuide = false,
hideInputType = false,
hideColorTypeBtns = false,
hideGradientControls = false,
hideGradientType = false,
hideGradientAngle = false,
hideGradientStop = false,
}: {
locales?: LocalesProps
hideEyeDrop?: boolean
hideAdvancedSliders?: boolean
hideColorGuide?: boolean
hideInputType?: boolean
hideColorTypeBtns?: boolean
hideGradientControls?: boolean
hideGradientType?: boolean
hideGradientAngle?: boolean
hideGradientStop?: boolean
}) => {
const { config, onChange, isGradient, handleChange, previous, defaultStyles, pickerIdSuffix } =
usePicker()
const { defaultColor, defaultGradient } = config
const [openComparibles, setOpenComparibles] = useState(false)
const [openInputType, setOpenInputType] = useState(false)
const [openAdvanced, setOpenAdvanced] = useState(false)
const noTools =
hideEyeDrop && hideAdvancedSliders && hideColorGuide && hideInputType
const solidColor = previous?.color ?? defaultColor
const gradientColor = previous?.gradient ?? defaultGradient
const setSolid = () => {
onChange(solidColor)
}
const setGradient = () => {
onChange(gradientColor)
}
const allRightControlsHidden =
hideEyeDrop && hideAdvancedSliders && hideColorGuide && hideInputType
const allControlsHidden = allRightControlsHidden && hideColorTypeBtns
if (allControlsHidden) {
if (isGradient && !hideGradientControls) {
return (
<GradientControls
hideGradientType={hideGradientType}
hideGradientAngle={hideGradientAngle}
hideGradientStop={hideGradientStop}
/>
)
} else {
return null
}
} else {
return (
<div style={{ paddingBottom: 4 }}>
<div
style={{
width: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
id={`rbgcp-controls-wrapper${pickerIdSuffix}`}
// className="rbgcp-controls-wrapper"
>
<ColorTypeBtns
hideColorTypeBtns={hideColorTypeBtns}
setGradient={setGradient}
isGradient={isGradient}
setSolid={setSolid}
locales={locales}
/>
{!allRightControlsHidden && (
<div
style={{
display: noTools ? 'none' : '',
...defaultStyles.rbgcpControlBtnWrapper,
}}
id={`rbgcp-control-rightside-wrapper${pickerIdSuffix}`}
// className="rbgcp-control-btn-wrapper"
>
{!hideEyeDrop && <EyeDropper onSelect={handleChange} />}
{!hideAdvancedSliders && (
<div
id={`rbgcp-advanced-btn${pickerIdSuffix}`}
onClick={() => setOpenAdvanced(!openAdvanced)}
// className="rbgcp-control-btn rbgcp-advanced-btn"
style={controlBtnStyles(openAdvanced, defaultStyles)}
>
<SlidersIcon color={openAdvanced ? '#568CF5' : ''} />
</div>
)}
{!hideColorGuide && (
<div
style={controlBtnStyles(openComparibles, defaultStyles)}
onClick={() => setOpenComparibles(!openComparibles)}
// className="rbgcp-control-btn rbgcp-comparibles-btn"
id={`rbgcp-comparibles-btn${pickerIdSuffix}`}
>
<PaletteIcon color={openComparibles ? '#568CF5' : ''} />
</div>
)}
{!hideInputType && (
<div
id={`rbgcp-color-model-btn${pickerIdSuffix}`}
onClick={() => setOpenInputType(!openInputType)}
// className="rbgcp-control-btn rbgcp-color-model-btn"
style={controlBtnStyles(openInputType, defaultStyles)}
>
<InputsIcon color={openInputType ? '#568CF5' : ''} />
<InputTypeDropdown
openInputType={openInputType}
setOpenInputType={setOpenInputType}
/>
</div>
)}
</div>
)}
</div>
{!hideAdvancedSliders && (
<AdvancedControls openAdvanced={openAdvanced} />
)}
{!hideColorGuide && (
<ComparibleColors openComparibles={openComparibles} />
)}
{isGradient && !hideGradientControls && (
<GradientControls
hideGradientType={hideGradientType}
hideGradientAngle={hideGradientAngle}
hideGradientStop={hideGradientStop}
/>
)}
</div>
)
}
}
export default Controls;
================================================
FILE: src/components/EyeDropper.tsx
================================================
/* eslint-disable react/jsx-no-leaked-render */
/* eslint-disable jsx-a11y/no-static-element-interactions */
import React, { useState } from 'react'
import Portal from './Portal.js'
import html2canvas from 'html2canvas'
import { controlBtnStyles } from '../styles/styles.js'
import tc from 'tinycolor2'
import { usePicker } from '../context.js'
const DropperIcon = ({ color }: { color: string }) => {
const { defaultStyles } = usePicker()
const col = color ?? ''
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
style={{ width: 16 }}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
style={{
fill: 'none',
strokeWidth: '1.4px',
...defaultStyles.rbgcpControlIcon,
...(col && { stroke: col }),
}}
d="M15.6,7h0L7.78,14.86c-.37.37-1.61.38-2,.75s-.5,1.53-.76,2a3.53,3.53,0,0,1-.52.52,1.6,1.6,0,0,1-2.27-.06l-.32-.32a1.61,1.61,0,0,1-.06-2.27A3.25,3.25,0,0,1,2.4,15c.47-.26,1.65-.35,2-.73s.34-1.64.71-2c1.68-1.73,5.61-5.65,7.91-7.93h0l1.14,1.38L15.6,7Z"
/>
<polygon
strokeLinecap="round"
strokeLinejoin="round"
style={{
strokeWidth: '1.4px',
...defaultStyles.rbgcpControlIcon2,
...(col && { stroke: col, fill: col }),
}}
points="15.7 8.87 11.13 4.29 12.69 2.73 17.25 7.31 15.7 8.87"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
style={{
strokeWidth: '1.4px',
...defaultStyles.rbgcpControlIcon2,
...(col && { stroke: col, fill: col }),
}}
d="M18.18,3.71,16.36,5.53a1.33,1.33,0,0,1-1.88,0h0a1.34,1.34,0,0,1,0-1.89l1.81-1.82a1.34,1.34,0,0,1,1.89,0h0A1.34,1.34,0,0,1,18.18,3.71Z"
/>
</svg>
)
}
const Dropper = ({ onSelect }: { onSelect: (arg0: string) => void }) => {
const { defaultStyles } = usePicker()
const [pickerCanvas, setPickerCanvas] =
useState<CanvasRenderingContext2D | null>(null)
const [coverUp, setCoverUp] = useState(false)
const [isPicking, setIsPicking] = useState(false)
const takePick = () => {
const root = document.getElementById('root')
setCoverUp(true)
// @ts-expect-error some error with this imported packages types
html2canvas(root).then((canvas: any) => {
const blankCanvas = document.createElement('canvas')
const ctx = blankCanvas.getContext('2d', { willReadFrequently: true })
if (root && ctx) {
blankCanvas.width = root.offsetWidth * 2
blankCanvas.height = root.offsetHeight * 2
ctx.drawImage(canvas, 0, 0)
}
setPickerCanvas(ctx)
})
}
const getColorLegacy = (e: any) => {
e.stopPropagation()
if (pickerCanvas) {
const { pageX, pageY } = e
const x1 = pageX * 2
const y1 = pageY * 2
const rgb = pickerCanvas.getImageData(x1, y1, 1, 1).data
onSelect(`rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 1)`)
}
setIsPicking(false)
setCoverUp(false)
}
const getEyeDrop = () => {
setIsPicking(true)
// @ts-expect-error - ts does not evaluate for window.EyeDropper
if (!window.EyeDropper) {
takePick()
} else {
// @ts-expect-error - ts does not evaluate for window.EyeDropper
const eyeDropper = new window.EyeDropper()
const abortController = new window.AbortController()
eyeDropper
.open({ signal: abortController.signal })
.then((result: any) => {
const tinyHex = tc(result.sRGBHex)
const { r, g, b } = tinyHex.toRgb()
onSelect(`rgba(${r}, ${g}, ${b}, 1)`)
setIsPicking(false)
})
.catch((e: any) => {
console.log(e)
setIsPicking(false)
})
}
}
return (
<div>
<div
onClick={getEyeDrop}
id="rbgcp-eyedropper-btn"
style={{
...defaultStyles.rbgcpEyedropperBtn,
...controlBtnStyles(coverUp, defaultStyles),
}}
>
<DropperIcon color={isPicking ? 'rgb(86, 140, 245)' : ''} />
</div>
{coverUp && (
<Portal>
<div
onClick={(e) => getColorLegacy(e)}
style={defaultStyles.rbgcpEyedropperCover}
/>
</Portal>
)}
</div>
)
}
export default Dropper
================================================
FILE: src/components/GradientBar.tsx
================================================
/* eslint-disable react/no-array-index-key */
/* eslint-disable react/jsx-no-leaked-render */
/* eslint-disable jsx-a11y/no-static-element-interactions */
import React, { useState, useEffect } from 'react'
import { getHandleValue } from '../utils/utils.js'
import { usePicker } from '../context.js'
import { low, high } from '../utils/formatters.js'
import { GradientProps } from '../shared/types.js'
export const Handle = ({
left,
i,
setDragging,
}: {
left?: number
i: number
setDragging: (arg0: boolean) => void
}) => {
const {
colors,
squareWidth,
selectedColor,
defaultStyles,
pickerIdSuffix,
createGradientStr,
} = usePicker()
const isSelected = selectedColor === i
const leftMultiplyer = (squareWidth - 18) / 100
const setSelectedColor = (index: number) => {
const newGradStr = colors?.map((cc: GradientProps, i: number) => ({
...cc,
value: i === index ? high(cc) : low(cc),
}))
createGradientStr(newGradStr)
}
const handleDown = (e: any) => {
e.stopPropagation()
setSelectedColor(i)
setDragging(true)
}
// const handleFocus = () => {
// setInFocus('gpoint')
// setSelectedColor(i)
// }
// const handleBlur = () => {
// setInFocus(null)
// }
return (
<div
// tabIndex={0}
// onBlur={handleBlur}
// onFocus={handleFocus}
onMouseDown={(e) => handleDown(e)}
id={`rbgcp-gradient-handle-${i}${pickerIdSuffix}`}
// className="rbgcp-gradient-handle-wrap"
style={{
...defaultStyles.rbgcpGradientHandleWrap,
left: (left ?? 0) * leftMultiplyer,
}}
>
<div
// className="rbgcp-gradient-handle"
style={{
...defaultStyles.rbgcpGradientHandle,
...(isSelected
? {
boxShadow: '0px 0px 5px 1px rgba(86, 140, 245,.95)',
border: '2px solid white',
}
: {}),
}}
id={`rbgcp-gradient-handle-${i}-dot${pickerIdSuffix}`}
>
{isSelected && (
<div
style={{
width: 5,
height: 5,
borderRadius: '50%',
background: 'white',
}}
id={`rbgcp-gradient-handle-${i}-selected-dot${pickerIdSuffix}`}
/>
)}
</div>
</div>
)
}
const GradientBar = () => {
const {
value,
colors,
config,
squareWidth,
currentColor,
handleGradient,
pickerIdSuffix,
createGradientStr,
} = usePicker()
const { barSize } = config
const [dragging, setDragging] = useState(false)
// const [inFocus, setInFocus] = useState<string | null>(null)
function force90degLinear(color: string) {
return color.replace(
/(radial|linear)-gradient\([^,]+,/,
'linear-gradient(90deg,'
)
}
const addPoint = (e: any) => {
const left = getHandleValue(e, barSize)
const newColors = [
...colors.map((c: any) => ({ ...c, value: low(c) })),
{ value: currentColor, left: left },
]?.sort((a, b) => a.left - b.left)
createGradientStr(newColors)
}
// useEffect(() => {
// const selectedEl = window?.document?.getElementById(
// `gradient-handle-${selectedColor}`
// )
// if (selectedEl) selectedEl.focus()
// }, [selectedColor])
const stopDragging = () => {
setDragging(false)
}
const handleDown = (e: any) => {
if (dragging) return;
addPoint(e)
setDragging(true)
}
const handleMove = (e: any) => {
if (dragging) handleGradient(currentColor, getHandleValue(e, barSize))
}
// const handleKeyboard = (e: any) => {
// if (isGradient) {
// if (e.keyCode === 8) {
// if (inFocus === 'gpoint') {
// deletePoint()
// }
// }
// }
// }
const handleUp = () => {
stopDragging()
}
useEffect(() => {
window.addEventListener('mouseup', handleUp)
// window?.addEventListener('keydown', handleKeyboard)
return () => {
window.removeEventListener('mouseup', handleUp)
// window?.removeEventListener('keydown', handleKeyboard)
}
})
return (
<div
style={{
width: '100%',
marginTop: 17,
marginBottom: 4,
position: 'relative',
}}
id={`rbgcp-gradient-bar${pickerIdSuffix}`}
// className="rbgcp-gradient-bar"
>
<div
style={{
height: 14,
borderRadius: 10,
width: squareWidth,
backgroundImage: force90degLinear(value),
}}
onMouseDown={(e) => handleDown(e)}
onMouseMove={(e) => handleMove(e)}
id={`rbgcp-gradient-bar-canvas${pickerIdSuffix}`}
// className="rbgcp-gradient-bar-canvas"
/>
{colors?.map((c: any, i) => (
<Handle
i={i}
left={c.left}
key={`${i}-${c}`}
setDragging={setDragging}
/>
))}
</div>
)
}
export default GradientBar
================================================
FILE: src/components/GradientControls.tsx
================================================
import React from 'react'
import { usePicker } from '../context.js'
import { formatInputValues, low, high } from '../utils/formatters.js'
import { controlBtnStyles } from '../styles/styles.js'
import TrashIcon, {
LinearIcon,
RadialIcon,
DegreesIcon,
StopIcon,
} from './icon.js'
const GradientType = () => {
const { gradientType, onChange, value, defaultStyles, pickerIdSuffix } =
usePicker()
const isLinear = gradientType === 'linear-gradient'
const isRadial = gradientType === 'radial-gradient'
const handleLinear = () => {
const remaining = value.split(/,(.+)/)[1]
onChange(`linear-gradient(90deg, ${remaining}`)
}
const handleRadial = () => {
const remaining = value.split(/,(.+)/)[1]
onChange(`radial-gradient(circle, ${remaining}`)
}
return (
<div style={defaultStyles.rbgcpControlBtnWrapper}>
<div
onClick={handleLinear}
id={`rbgcp-linear-btn${pickerIdSuffix}`}
// className="rbgcp-control-icon-btn rbgcp-linear-btn"
style={{
...defaultStyles.rbgcpControlBtn,
...(isLinear && defaultStyles.rbgcpControlBtnSelected),
}}
tabIndex={0}
role="button"
onKeyDown={() => {
return
}}
>
<LinearIcon color={isLinear ? '#568CF5' : ''} />
</div>
<div
onClick={handleRadial}
id={`rbgcp-radial-btn${pickerIdSuffix}`}
// className="rbgcp-control-icon-btn rbgcp-radial-btn"
style={{
...defaultStyles.rbgcpControlBtn,
...(isRadial && defaultStyles.rbgcpControlBtnSelected),
}}
tabIndex={0}
role="button"
onKeyDown={() => {
return
}}
>
<RadialIcon color={isRadial ? '#568CF5' : ''} />
</div>
</div>
)
}
const StopPicker = () => {
const {
currentLeft,
currentColor,
defaultStyles,
handleGradient,
pickerIdSuffix,
} = usePicker()
const handleMove = (newVal: string) => {
handleGradient(currentColor, formatInputValues(parseInt(newVal), 0, 100))
}
return (
<div
// className="rbgcp-stop-input-wrap"
style={{
...defaultStyles.rbgcpControlBtnWrapper,
...defaultStyles.rbgcpControlInputWrap,
...defaultStyles.rbgcpStopInputWrap,
paddingLeft: 8,
}}
id={`rbgcp-stop-input-wrapper${pickerIdSuffix}`}
>
<StopIcon />
<input
value={currentLeft}
id={`rbgcp-stop-input${pickerIdSuffix}`}
onChange={(e) => handleMove(e.target.value)}
style={{
...defaultStyles.rbgcpControlInput,
...defaultStyles.rbgcpStopInput,
}}
// className="rbgcp-control-input rbgcp-stop-input"
/>
</div>
)
}
const DegreePicker = () => {
const { degrees, onChange, value, defaultStyles, pickerIdSuffix } =
usePicker()
const handleDegrees = (e: any) => {
const newValue = formatInputValues(e.target.value, 0, 360)
const remaining = value.split(/,(.+)/)[1]
onChange(`linear-gradient(${newValue ?? 0}deg, ${remaining}`)
}
return (
<div
// className="rbgcp-degree-input-wrap"
style={{
...defaultStyles.rbgcpControlBtnWrapper,
...defaultStyles.rbgcpControlInputWrap,
...defaultStyles.rbgcpDegreeInputWrap,
}}
id={`rbgcp-degree-input-wrapper${pickerIdSuffix}`}
>
<DegreesIcon />
<input
value={degrees}
onChange={(e) => handleDegrees(e)}
id={`rbgcp-degree-input${pickerIdSuffix}`}
// className="rbgcp-control-input rbgcp-degree-input"
style={{
...defaultStyles.rbgcpControlInput,
...defaultStyles.rbgcpDegreeInput,
}}
/>
<div
// className="rbgcp-degree-circle-icon"
style={{
...defaultStyles.rbgcpDegreeIcon,
position: 'absolute',
right: degrees > 99 ? 0 : degrees < 10 ? 7 : 3,
top: 1,
fontWeight: 400,
fontSize: 13,
}}
>
°
</div>
</div>
)
}
const DeleteBtn = () => {
const { colors, selectedColor, createGradientStr, defaultStyles, pickerIdSuffix } =
usePicker()
const deletePoint = () => {
if (colors?.length > 2) {
const formatted = colors?.map((fc: any, i: number) => ({
...fc,
value: i === selectedColor - 1 ? high(fc) : low(fc),
}))
const remaining = formatted?.filter(
(_: any, i: number) => i !== selectedColor
)
createGradientStr(remaining)
}
}
return (
<div
onClick={deletePoint}
style={{ ...controlBtnStyles(false, defaultStyles), width: 28 }}
id={`rbgcp-point-delete-btn${pickerIdSuffix}`}
// className="rbgcp-control-btn rbgcp-point-delete-btn"
tabIndex={0}
role="button"
onKeyDown={() => {
return
}}
>
<TrashIcon />
</div>
)
}
const GradientControls = ({
hideGradientType,
hideGradientAngle,
hideGradientStop,
}: {
hideGradientType?: boolean
hideGradientAngle?: boolean
hideGradientStop?: boolean
}) => {
const { gradientType, defaultStyles, pickerIdSuffix } = usePicker()
return (
<div
style={{
...defaultStyles.rbgcpControlBtnWrapper,
marginTop: 12,
marginBottom: -4,
justifyContent: 'space-between',
paddingLeft: hideGradientType ? 4 : 0,
}}
id={`rbgcp-gradient-controls-wrap${pickerIdSuffix}`}
// className="rbgcp-gradient-controls-wrap"
>
{!hideGradientType && <GradientType />}
<div style={{ width: 53 }}>
{!hideGradientAngle && gradientType === 'linear-gradient' && (
<DegreePicker />
)}
</div>
{!hideGradientStop && <StopPicker />}
<DeleteBtn />
</div>
)
}
export default GradientControls
================================================
FILE: src/components/Hue.tsx
================================================
import React, { useRef, useState, useEffect } from 'react'
import { usePicker } from '../context.js'
import usePaintHue from '../hooks/usePaintHue.js'
import { getHandleValue } from '../utils/utils.js'
import tinycolor from 'tinycolor2'
const Hue = () => {
const barRef = useRef<HTMLCanvasElement>(null)
const { config, handleChange, squareWidth, hc, setHc, pickerIdSuffix } = usePicker()
const [dragging, setDragging] = useState(false)
const { barSize } = config
usePaintHue(barRef, squareWidth)
const stopDragging = () => {
setDragging(false)
}
const handleDown = () => {
setDragging(true)
}
const handleHue = (e: any) => {
const newHue = getHandleValue(e, barSize) * 3.6
const tinyHsv = tinycolor({ h: newHue, s: hc?.s, v: hc?.v })
const { r, g, b } = tinyHsv.toRgb()
handleChange(`rgba(${r}, ${g}, ${b}, ${hc.a})`)
setHc({ ...hc, h: newHue })
}
const handleMove = (e: any) => {
if (dragging) {
handleHue(e)
}
}
const handleClick = (e: any) => {
if (!dragging) {
handleHue(e)
}
}
useEffect(() => {
const handleUp = () => {
stopDragging()
}
window.addEventListener('mouseup', handleUp)
return () => {
window.removeEventListener('mouseup', handleUp)
}
}, [])
return (
<div
style={{
height: 14,
marginTop: 17,
marginBottom: 4,
cursor: 'ew-resize',
position: 'relative',
}}
onMouseMove={(e) => handleMove(e)}
id={`rbgcp-hue-wrap${pickerIdSuffix}`}
// className="rbgcp-hue-wrap"
>
<div
tabIndex={0}
role="button"
// className="rbgcp-handle rbgcp-handle-hue"
style={{
border: '2px solid white',
borderRadius: '50%',
boxShadow: '0px 0px 3px rgba(0, 0, 0, 0.5)',
width: '18px',
height: '18px',
zIndex: 1000,
transition: 'all 10ms linear',
position: 'absolute',
left: hc?.h * ((squareWidth - 18) / 360),
top: -2,
cursor: 'ew-resize',
boxSizing: 'border-box',
}}
onMouseDown={handleDown}
id={`rbgcp-hue-handle${pickerIdSuffix}`}
/>
<canvas
ref={barRef}
height="14px"
// className="rbgcp-hue-bar"
width={`${squareWidth}px`}
onClick={(e) => handleClick(e)}
id={`rbgcp-hue-bar${pickerIdSuffix}`}
style={{
borderRadius: 14,
position: 'relative',
verticalAlign: 'top',
}}
/>
</div>
)
}
export default Hue
================================================
FILE: src/components/Inputs.tsx
================================================
import React, { useState, useEffect } from 'react'
import { Styles } from '../shared/types.js'
import { formatInputValues, round } from '../utils/formatters.js'
import { rgb2cmyk, cmykToRgb, getHexAlpha } from '../utils/converters.js'
import { usePicker } from '../context.js'
import tc from 'tinycolor2'
const Input = ({
label,
value,
callback,
max = 100,
hideOpacity,
defaultStyles,
pickerIdSuffix,
}: {
max?: number
label: string
value: number
hideOpacity: boolean
defaultStyles: Styles
pickerIdSuffix: string
callback: (arg0: number) => void
}) => {
const [temp, setTemp] = useState(value)
const width = hideOpacity ? '25%' : '20%'
useEffect(() => {
setTemp(value)
}, [value])
const onChange = (e: any) => {
const newVal = formatInputValues(parseFloat(e.target.value), 0, max)
setTemp(newVal)
callback(newVal)
}
return (
<div
style={{ width: width, flexShrink: 1 }}
id={`rbgcp-${label}-input-wrapper${pickerIdSuffix}`}
>
<input
value={temp}
onChange={(e) => onChange(e)}
style={{ ...defaultStyles.rbgcpInput }}
id={`rbgcp-${label}-input${pickerIdSuffix}`}
// className="rbgcp-input"
/>
<div style={{ ...defaultStyles.rbgcpInputLabel }}>{label}</div>
</div>
)
}
const HexInput = ({
opacity,
tinyColor,
showHexAlpha,
handleChange,
defaultStyles,
pickerIdSuffix,
}: {
tinyColor: any
opacity: number
showHexAlpha: boolean
defaultStyles: Styles
pickerIdSuffix: string
handleChange: (arg0: string) => void
}) => {
const [disable, setDisable] = useState('')
const hex = tinyColor.toHex()
const [newHex, setNewHex] = useState(hex)
useEffect(() => {
if (disable !== 'hex') {
setNewHex(hex)
}
}, [tinyColor, disable, hex])
const hexFocus = () => {
setDisable('hex')
}
const hexBlur = () => {
setDisable('')
}
const handleHex = (e: any) => {
const tinyHex = tc(e.target.value)
setNewHex(e.target.value)
if (tinyHex.isValid()) {
const { r, g, b } = tinyHex.toRgb()
const newColor = `rgba(${r}, ${g}, ${b}, ${opacity})`
handleChange(newColor)
}
}
const displayValue = showHexAlpha ? `${newHex}${getHexAlpha(opacity)}` : newHex
const label = showHexAlpha ? 'HEXA' : 'HEX'
const width = showHexAlpha ? 88 : 76
return (
<div
style={{ width, flexShrink: 0 }}
id={`rbgcp-hex-input-wrapper${pickerIdSuffix}`}
>
<input
onBlur={hexBlur}
onFocus={hexFocus}
onChange={(e) => handleHex(e)}
value={displayValue?.toUpperCase()}
id={`rbgcp-hex-input${pickerIdSuffix}`}
style={{ ...defaultStyles.rbgcpInput, ...defaultStyles.rbgcpHexInput }}
/>
<div style={{ ...defaultStyles.rbgcpInputLabel }}>{label}</div>
</div>
)
}
const RGBInputs = ({
hc,
hideOpacity,
handleChange,
defaultStyles,
pickerIdSuffix,
}: {
hc: any
hideOpacity: boolean
defaultStyles: Styles
pickerIdSuffix: string
handleChange: (arg0: string) => void
}) => {
const handleRgb = ({ r, g, b }: { r: number; g: number; b: number }) => {
handleChange(`rgba(${r}, ${g}, ${b}, ${hc?.a})`)
}
return (
<>
<Input
label="R"
max={255}
value={hc?.r}
hideOpacity={hideOpacity}
defaultStyles={defaultStyles}
pickerIdSuffix={pickerIdSuffix}
callback={(newVal) => handleRgb({ r: newVal, g: hc?.g, b: hc?.b })}
/>
<Input
label="G"
max={255}
value={hc?.g}
hideOpacity={hideOpacity}
defaultStyles={defaultStyles}
pickerIdSuffix={pickerIdSuffix}
callback={(newVal) => handleRgb({ r: hc?.r, g: newVal, b: hc?.b })}
/>
<Input
label="B"
max={255}
value={hc?.b}
hideOpacity={hideOpacity}
defaultStyles={defaultStyles}
pickerIdSuffix={pickerIdSuffix}
callback={(newVal) => handleRgb({ r: hc?.r, g: hc?.g, b: newVal })}
/>
</>
)
}
const HSLInputs = ({
hc,
setHc,
tinyColor,
hideOpacity,
handleChange,
defaultStyles,
pickerIdSuffix,
}: {
hc: any
tinyColor: any
hideOpacity: boolean
defaultStyles: Styles
pickerIdSuffix: string
setHc: (arg0: any) => void
handleChange: (arg0: string) => void
}) => {
const { s, l } = tinyColor.toHsl()
const handleH = (h: number, s: number, l: number) => {
const { r, g, b } = tc({ h: h, s: s, l: l }).toRgb()
handleChange(`rgba(${r}, ${g}, ${b}, ${hc?.a})`)
setHc({ ...hc, h })
}
const handleSl = (value: any) => {
const { r, g, b } = tc(value).toRgb()
handleChange(`rgba(${r}, ${g}, ${b}, ${hc?.a})`)
}
return (
<>
<Input
label="H"
max={360}
value={round(hc?.h)}
hideOpacity={hideOpacity}
defaultStyles={defaultStyles}
pickerIdSuffix={pickerIdSuffix}
callback={(newVal) => handleH(newVal, s, l)}
/>
<Input
label="S"
value={round(s * 100)}
hideOpacity={hideOpacity}
defaultStyles={defaultStyles}
pickerIdSuffix={pickerIdSuffix}
callback={(newVal) => handleSl({ h: hc?.h, s: newVal, l: l })}
/>
<Input
label="L"
value={round(l * 100)}
hideOpacity={hideOpacity}
defaultStyles={defaultStyles}
pickerIdSuffix={pickerIdSuffix}
callback={(newVal) => handleSl({ h: hc?.h, s: s, l: newVal })}
/>
</>
)
}
const HSVInputs = ({
hc,
setHc,
hideOpacity,
handleChange,
defaultStyles,
pickerIdSuffix,
}: {
hc: any
hideOpacity: boolean
defaultStyles: Styles
pickerIdSuffix: string
setHc: (arg0: any) => void
handleChange: (arg0: string) => void
}) => {
const handleH = (h: number, s: number, v: number) => {
const { r, g, b } = tc({ h: h, s: s, v: v }).toRgb()
handleChange(`rgba(${r}, ${g}, ${b}, ${hc?.a})`)
setHc({ ...hc, h })
}
const handleSV = (value: any) => {
const { r, g, b } = tc(value).toRgb()
handleChange(`rgba(${r}, ${g}, ${b}, ${hc?.a})`)
}
return (
<>
<Input
label="H"
max={360}
value={round(hc?.h)}
hideOpacity={hideOpacity}
defaultStyles={defaultStyles}
pickerIdSuffix={pickerIdSuffix}
callback={(newVal) => handleH(newVal, hc?.s, hc?.v)}
/>
<Input
label="S"
hideOpacity={hideOpacity}
value={round(hc?.s * 100)}
defaultStyles={defaultStyles}
pickerIdSuffix={pickerIdSuffix}
callback={(newVal) => handleSV({ h: hc?.h, s: newVal, v: hc?.v })}
/>
<Input
label="V"
hideOpacity={hideOpacity}
value={round(hc?.v * 100)}
defaultStyles={defaultStyles}
pickerIdSuffix={pickerIdSuffix}
callback={(newVal) => handleSV({ h: hc?.h, s: hc?.s, v: newVal })}
/>
</>
)
}
const CMKYInputs = ({
hc,
hideOpacity,
handleChange,
defaultStyles,
pickerIdSuffix,
}: {
hc: any
hideOpacity: boolean
defaultStyles: Styles
pickerIdSuffix: string
handleChange: (arg0: string) => void
}) => {
const { c, m, y, k } = rgb2cmyk(hc?.r, hc?.g, hc?.b)
const handleCmyk = (value: any) => {
const { r, g, b } = cmykToRgb(value)
handleChange(`rgba(${r}, ${g}, ${b}, ${hc?.a})`)
}
return (
<>
<Input
label="C"
value={round(c * 100)}
hideOpacity={hideOpacity}
defaultStyles={defaultStyles}
pickerIdSuffix={pickerIdSuffix}
callback={(newVal) => handleCmyk({ c: newVal / 100, m: m, y: y, k: k })}
/>
<Input
label="M"
value={round(m * 100)}
hideOpacity={hideOpacity}
defaultStyles={defaultStyles}
pickerIdSuffix={pickerIdSuffix}
callback={(newVal) => handleCmyk({ c: c, m: newVal / 100, y: y, k: k })}
/>
<Input
label="Y"
value={round(y * 100)}
hideOpacity={hideOpacity}
defaultStyles={defaultStyles}
pickerIdSuffix={pickerIdSuffix}
callback={(newVal) => handleCmyk({ c: c, m: m, y: newVal / 100, k: k })}
/>
<Input
label="K"
value={round(k * 100)}
hideOpacity={hideOpacity}
defaultStyles={defaultStyles}
pickerIdSuffix={pickerIdSuffix}
callback={(newVal) => handleCmyk({ c: c, m: m, y: y, k: newVal / 100 })}
/>
</>
)
}
const Inputs = () => {
const {
hc,
setHc,
inputType,
tinyColor,
hideOpacity,
showHexAlpha,
handleChange,
defaultStyles,
pickerIdSuffix,
} = usePicker()
return (
<div
style={{
columnGap: 6,
paddingTop: 14,
display: 'flex',
justifyContent: 'space-between',
...defaultStyles.rbgcpInputsWrap,
}}
id={`rbgcp-inputs-wrap${pickerIdSuffix}`}
>
{inputType !== 'cmyk' && (
<HexInput
opacity={hc?.a}
tinyColor={tinyColor}
showHexAlpha={showHexAlpha}
handleChange={handleChange}
defaultStyles={defaultStyles}
pickerIdSuffix={pickerIdSuffix}
/>
)}
{inputType === 'hsl' && (
<HSLInputs
hc={hc}
setHc={setHc}
tinyColor={tinyColor}
hideOpacity={hideOpacity}
handleChange={handleChange}
defaultStyles={defaultStyles}
pickerIdSuffix={pickerIdSuffix}
/>
)}
{inputType === 'rgb' && (
<RGBInputs
hc={hc}
hideOpacity={hideOpacity}
handleChange={handleChange}
defaultStyles={defaultStyles}
pickerIdSuffix={pickerIdSuffix}
/>
)}
{inputType === 'hsv' && (
<HSVInputs
hc={hc}
setHc={setHc}
hideOpacity={hideOpacity}
handleChange={handleChange}
defaultStyles={defaultStyles}
pickerIdSuffix={pickerIdSuffix}
/>
)}
{inputType === 'cmyk' && (
<CMKYInputs
hc={hc}
hideOpacity={hideOpacity}
handleChange={handleChange}
defaultStyles={defaultStyles}
pickerIdSuffix={pickerIdSuffix}
/>
)}
{!hideOpacity && (
<Input
label="A"
hideOpacity={hideOpacity}
defaultStyles={defaultStyles}
value={Math.round(hc?.a * 100)}
pickerIdSuffix={pickerIdSuffix}
callback={(newVal: number) =>
handleChange(`rgba(${hc?.r}, ${hc?.g}, ${hc?.b}, ${newVal / 100})`)
}
/>
)}
</div>
)
}
export default Inputs
================================================
FILE: src/components/Opacity.tsx
================================================
/* eslint-disable jsx-a11y/no-static-element-interactions */
import React, { useState, useEffect } from 'react'
import { usePicker } from '../context.js'
import { getHandleValue } from '../utils/utils.js'
const Opacity = () => {
const {
config,
hc = {},
squareWidth,
handleChange,
defaultStyles,
pickerIdSuffix,
} = usePicker()
const [dragging, setDragging] = useState(false)
const { r, g, b } = hc
const bg = `linear-gradient(90deg, rgba(255,255,255,0) 0%, rgba(${r},${g},${b},.5) 100%)`
const { barSize } = config
const stopDragging = () => {
setDragging(false)
}
const handleDown = () => {
setDragging(true)
}
const handleOpacity = (e: any) => {
const newO = getHandleValue(e, barSize) / 100
const newColor = `rgba(${r}, ${g}, ${b}, ${newO})`
handleChange(newColor)
}
const handleMove = (e: any) => {
if (dragging) {
handleOpacity(e)
}
}
const handleClick = (e: any) => {
if (!dragging) {
handleOpacity(e)
}
}
const left = squareWidth - 18
useEffect(() => {
const handleUp = () => {
stopDragging()
}
window.addEventListener('mouseup', handleUp)
return () => {
window.removeEventListener('mouseup', handleUp)
}
}, [])
return (
<div
onMouseDown={handleDown}
onMouseMove={(e) => handleMove(e)}
style={{
height: 14,
marginTop: 17,
marginBottom: 4,
cursor: 'ew-resize',
position: 'relative',
}}
id={`rbgcp-opacity-wrapper${pickerIdSuffix}`}
// className="rbgcp-opacity-wrap"
>
<div
// className="rbgcp-opacity-checkered"
id={`rbgcp-opacity-checkered-bg${pickerIdSuffix}`}
style={{ ...defaultStyles.rbgcpCheckered, width: '100%', height: 14 }}
/>
<div
// className="rbgcp-handle rbgcp-handle-opacity"
id={`rbgcp-opacity-handle${pickerIdSuffix}`}
style={{ ...defaultStyles.rbgcpHandle, left: left * hc?.a, top: -2 }}
/>
<div
style={{ ...defaultStyles.rbgcpOpacityOverlay, background: bg }}
id={`rbgcp-opacity-overlay${pickerIdSuffix}`}
// className="rbgcp-opacity-overlay"
onClick={(e) => handleClick(e)}
/>
</div>
)
}
export default Opacity
================================================
FILE: src/components/Picker.tsx
================================================
import React from 'react'
import Hue from './Hue.js'
import Inputs from './Inputs.js'
import Square from './Square.js'
import Opacity from './Opacity.js'
import Presets from './Presets.js'
import Controls from './Controls.js'
import { usePicker } from '../context.js'
import GradientBar from './GradientBar.js'
import { LocalesProps } from '../shared/types.js'
const Picker = ({
locales,
presets,
hideHue,
hideInputs,
hidePresets,
hideOpacity,
hideEyeDrop,
hideControls,
hideInputType,
hideColorGuide,
hidePickerSquare,
hideGradientType,
hideGradientStop,
hideGradientAngle,
hideColorTypeBtns,
hideAdvancedSliders,
hideGradientControls,
}: PickerProps) => {
const { isGradient, pickerIdSuffix } = usePicker()
return (
<div style={{ userSelect: 'none' }} id={`rbgcp-color-picker${pickerIdSuffix}`}>
{!hidePickerSquare && <Square />}
{!hideControls && (
<Controls
locales={locales}
hideEyeDrop={hideEyeDrop}
hideInputType={hideInputType}
hideColorGuide={hideColorGuide}
hideGradientType={hideGradientType}
hideGradientStop={hideGradientStop}
hideColorTypeBtns={hideColorTypeBtns}
hideGradientAngle={hideGradientAngle}
hideAdvancedSliders={hideAdvancedSliders}
hideGradientControls={hideGradientControls}
/>
)}
{isGradient && <GradientBar />}
{!hideHue && <Hue />}
{!hideOpacity && <Opacity />}
{!hideInputs && <Inputs />}
{!hidePresets && <Presets presets={presets} />}
</div>
)
}
export default Picker
type PickerProps = {
hideControls?: boolean
hideInputs?: boolean
hidePresets?: boolean
hideOpacity?: boolean
hideHue?: boolean
presets?: string[]
hideEyeDrop?: boolean
hideAdvancedSliders?: boolean
hideColorGuide?: boolean
hideInputType?: boolean
hideColorTypeBtns?: boolean
hideGradientType?: boolean
hideGradientAngle?: boolean
hideGradientStop?: boolean
hideGradientControls?: boolean
locales?: LocalesProps
hidePickerSquare?: boolean
}
================================================
FILE: src/components/Portal.tsx
================================================
import { memo, useEffect, useRef, useState, ReactNode } from 'react'
import { createPortal } from 'react-dom'
const Portal = ({ children }: { children: ReactNode }) => {
const id = 'id' + Math.random().toString(16).slice(2)
const el = useRef(
document.getElementById(id) ?? document.createElement('div')
)
const [dynamic] = useState(!el.current.parentElement)
useEffect(() => {
const refValue = el.current
if (dynamic) {
el.current.id = id
document.body.appendChild(el.current)
}
return () => {
if (dynamic && refValue.parentElement) {
refValue.parentElement.removeChild(refValue)
}
}
//eslint-disable-next-line
}, [id])
return createPortal(children, el.current)
}
export default memo(Portal)
================================================
FILE: src/components/Presets.tsx
================================================
/* eslint-disable react/no-array-index-key */
/* eslint-disable jsx-a11y/no-static-element-interactions */
import React from 'react'
import { usePicker } from '../context.js'
import { fakePresets } from '../constants.js'
const Presets = ({ presets = [] }: { presets?: string[] }) => {
const {
value,
onChange,
isDarkMode,
squareWidth,
handleChange,
pickerIdSuffix,
} = usePicker()
const getPresets = () => {
if (presets?.length > 0) {
return presets?.slice(0, 18)
} else {
return fakePresets
}
}
const handlePresetClick = (preset: string) => {
if (preset?.includes('gradient')) {
onChange(preset)
} else {
handleChange(preset)
}
}
const getBorder = (p: string) => {
if (!p || isDarkMode) return ''
const c = p?.replace(' ', '');
if (c === 'rgba(255,255,255,1)') {
return '1px solid #96959c'
}
return ''
}
return (
<div
style={{
marginTop: 14,
display: 'flex',
justifyContent: 'space-between',
}}
id={`rbgcp-footer-wrapper${pickerIdSuffix}`}
// className="rbgcp-presets-wrap"
>
<div
style={{
width: 50,
height: 50,
flexShrink: 0,
borderRadius: 6,
background: value,
border: getBorder(value),
}}
id={`rbgcp-preview${pickerIdSuffix}`}
// className="rbgcp-preset-color-preview"
/>
<div
style={{
rowGap: 3,
display: 'flex',
flexWrap: 'wrap',
width: squareWidth - 57,
justifyContent: 'space-between',
}}
id={`rbgcp-presets-wrapper${pickerIdSuffix}`}
// className="rbgcp-presets-list"
>
{getPresets().map((p: any, key: number) => (
<div
key={`${p}-${key}`}
id={`rbgcp-preset-${key}-wrapper${pickerIdSuffix}`}
style={{ width: `calc(100% / 9)`, paddingLeft: 3 }}
>
<div
style={{
height: 23.5,
width: '100%',
background: p,
borderRadius: 4,
border: getBorder(p),
}}
// className="rbgcp-preset-color"
onClick={() => handlePresetClick(p)}
id={`rbgcp-preset-${key}${pickerIdSuffix}`}
/>
</div>
))}
</div>
</div>
)
}
export default Presets
================================================
FILE: src/components/Square.tsx
================================================
/* eslint-disable jsx-a11y/no-static-element-interactions */
import { computePickerPosition, computeSquareXY } from '../utils/utils.js'
import React, { useRef, useState, useEffect } from 'react'
import usePaintSquare from '../hooks/usePaintSquare.js'
import { usePicker } from '../context.js'
import throttle from 'lodash.throttle'
import tinycolor from 'tinycolor2'
const Square = () => {
const {
hc,
config,
squareWidth,
squareHeight,
handleChange,
defaultStyles,
pickerIdSuffix,
} = usePicker()
const { crossSize } = config
const [dragging, setDragging] = useState(false)
const canvas = useRef<HTMLCanvasElement>(null)
const [x, y] = computeSquareXY(hc?.s, hc?.v * 100, squareWidth, squareHeight, crossSize)
const [dragPos, setDragPos] = useState({ x, y })
usePaintSquare(canvas, hc?.h, squareWidth, squareHeight)
useEffect(() => {
if (!dragging) {
setDragPos({ x: hc?.v === 0 ? dragPos.x : x, y })
}
}, [x, y])
const handleColor = (e: any) => {
const onMouseMove = throttle(() => {
const [x, y] = computePickerPosition(e, crossSize)
if (x && y) {
const x1 = Math.min(x + crossSize / 2, squareWidth - 1)
const y1 = Math.min(y + crossSize / 2, squareHeight - 1)
const newS = (x1 / squareWidth) * 100
const newY = 100 - (y1 / squareHeight) * 100
setDragPos({ x: newY === 0 ? dragPos?.x : x, y })
const updated = tinycolor(
`hsva(${hc?.h}, ${newS}%, ${newY}%, ${hc?.a})`
)
handleChange(updated.toRgbString())
}
}, 250)
onMouseMove()
}
const stopDragging = () => {
setDragging(false)
}
const handleMove = (e: any) => {
if (dragging) {
handleColor(e)
}
}
// const handleTouchMove = (e: any) => {
// if (dragging && isMobile) {
// document.body.style.overflow = 'hidden'
// handleColor(e)
// }
// }
const handleClick = (e: any) => {
if (!dragging) {
handleColor(e)
}
}
const handleMouseDown = () => {
setDragging(true)
}
const handleCanvasDown = (e: any) => {
setDragging(true)
handleColor(e)
}
useEffect(() => {
const handleUp = () => {
stopDragging()
}
window.addEventListener('mouseup', handleUp)
return () => {
window.removeEventListener('mouseup', handleUp)
}
}, [])
return (
<div
style={{ position: 'relative', marginBottom: 12 }}
id={`rbgcp-square-wrapper${pickerIdSuffix}`}
>
<div
onMouseUp={stopDragging}
onTouchEnd={stopDragging}
onMouseDown={handleCanvasDown}
onTouchStart={handleCanvasDown}
onMouseMove={(e) => handleMove(e)}
id={`rbgcp-square${pickerIdSuffix}`}
style={{ position: 'relative', cursor: 'ew-cross' }}
// className="rbgcp-square-wrap"
>
<div
style={{
...defaultStyles.rbgcpHandle,
transform: `translate(${dragPos?.x ?? 0}px, ${dragPos?.y ?? 0}px)`,
...(dragging ? { transition: '' } : {}),
}}
onMouseDown={handleMouseDown}
id={`rbgcp-square-handle${pickerIdSuffix}`}
// className="rbgcp-handle rbgcp-handle-square"
/>
<div
style={{ ...defaultStyles.rbgcpCanvasWrapper, height: squareHeight }}
id={`rbgcp-square-canvas-wrapper${pickerIdSuffix}`}
// className="rbgcp-canvas-wrapper"
onClick={(e) => handleClick(e)}
>
<canvas
ref={canvas}
// className="rbgcp-canvas"
width={`${squareWidth}px`}
height={`${squareHeight}px`}
id={`rbgcp-square-canvas${pickerIdSuffix}`}
/>
</div>
</div>
</div>
)
}
export default Square
================================================
FILE: src/components/icon.tsx
================================================
import React from 'react'
import { usePicker } from '../context.js'
type ColorProps = {
color: string
}
const TrashIcon = () => {
const { defaultStyles } = usePicker()
const styles = {
fill: 'none',
strokeWidth: '1.8px',
}
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
style={{ width: 15 }}
>
<polyline
strokeLinecap="round"
strokeLinejoin="round"
style={{ ...styles, ...defaultStyles.rbgcpControlIcon }}
points="17.96 4.31 2.04 4.3 3.75 4.3 4.81 17.29 5.16 17.96 5.74 18.47 6.59 18.62 13.64 18.62 14.52 18.32 15.07 17.68 15.29 17.12 16.28 4.3 12.87 4.3 12.87 2.38 12.48 1.75 11.83 1.46 8.4 1.46 7.64 1.68 7.26 2.21 7.16 2.52 7.17 4.23"
/>
</svg>
)
}
export default TrashIcon
export const LinearIcon = ({ color }: ColorProps) => {
const { defaultStyles } = usePicker()
const col = color ?? ''
const styles = {
fill: 'none',
strokeWidth: '1.8px',
}
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
style={{ width: 14 }}
>
<polyline
strokeLinecap="round"
strokeLinejoin="round"
style={{
...styles,
...defaultStyles.rbgcpControlIcon,
...(col && { stroke: col }),
}}
points="0.9 12.73 0.9 19.1 7.27 19.1 0.9 19.1 19.1 0.9 12.73 0.9 19.1 0.9 19.1 7.27"
/>
</svg>
)
}
export const RadialIcon = ({ color }: ColorProps) => {
const { defaultStyles } = usePicker()
const col = color ?? ''
const styles = {
fill: 'none',
strokeMiterlimit: 10,
strokeWidth: '1.8px',
}
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
style={{ width: 15 }}
>
<circle
style={{
...styles,
...defaultStyles.rbgcpControlIcon,
...(col && { stroke: col }),
}}
cx="10"
cy="10"
r="9"
/>
<circle
style={{
...styles,
...defaultStyles.rbgcpControlIcon,
...(col && { stroke: col }),
}}
cx="10"
cy="10"
r="5"
/>
</svg>
)
}
export const SlidersIcon = ({ color }: ColorProps) => {
const { defaultStyles } = usePicker()
const col = color ?? ''
const style1 = {
fill: 'none',
strokeWidth: '1.8px',
}
const style2 = {
strokeWidth: '1.8px',
}
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
style={{ width: 17 }}
>
<polyline
fillRule="evenodd"
strokeLinecap="round"
strokeLinejoin="round"
style={{
...style1,
...defaultStyles.rbgcpControlIcon,
...(col && { stroke: col }),
}}
points="3.74 2.75 3.74 12.69 0.9 12.71 6.59 12.71"
/>
<line
strokeLinecap="round"
strokeLinejoin="round"
style={{
...style2,
...defaultStyles.rbgcpControlIcon,
...(col && { stroke: col, fill: col }),
}}
x1="3.74"
y1="17.26"
x2="3.74"
y2="15.21"
/>
<polyline
fillRule="evenodd"
strokeLinecap="round"
strokeLinejoin="round"
style={{
...style1,
...defaultStyles.rbgcpControlIcon,
...(col && { stroke: col }),
}}
points="10.1 17.25 10.1 7.31 12.95 7.29 7.26 7.29"
/>
<line
strokeLinecap="round"
strokeLinejoin="round"
style={{
...style2,
...defaultStyles.rbgcpControlIcon,
...(col && { stroke: col, fill: col }),
}}
x1="10.1"
y1="2.74"
x2="10.1"
y2="4.79"
/>
<polyline
fillRule="evenodd"
strokeLinecap="round"
strokeLinejoin="round"
style={{
...style1,
...defaultStyles.rbgcpControlIcon,
...(col && { stroke: col }),
}}
points="16.26 2.75 16.26 12.69 13.41 12.71 19.1 12.71"
/>
<line
strokeLinecap="round"
strokeLinejoin="round"
style={{
...style2,
...defaultStyles.rbgcpControlIcon,
...(col && { stroke: col, fill: col }),
}}
x1="16.26"
y1="17.26"
x2="16.26"
y2="15.21"
/>
</svg>
)
}
export const InputsIcon = ({ color }: ColorProps) => {
const { defaultStyles } = usePicker()
const col = color ?? ''
const style1 = {
fill: 'none',
strokeWidth: '1.8px',
}
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
style={{ width: 17 }}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
style={{
...style1,
...defaultStyles.rbgcpControlIcon,
...(col && { stroke: col }),
}}
d="M6.35,2.72a4.85,4.85,0,0,1,1.86.16,7.94,7.94,0,0,1,.88.43,3.66,3.66,0,0,0,.85.49c.25,0,.58-.27.81-.39A8.25,8.25,0,0,1,11.7,3a4,4,0,0,1,1.79-.23,3.21,3.21,0,0,0-1.34.09,6.39,6.39,0,0,0-1.47.63c-.45.25-.7.3-.7.86s0,1.18,0,1.78c0,1.3,0,2.61,0,3.92h0v5.63a2.46,2.46,0,0,1,0,.47c-.07.28-.43.42-.7.57a5.29,5.29,0,0,1-2.94.61A9.3,9.3,0,0,0,8,17.15l1.09-.37.89-.52c.06,0,.48.21.56.25.32.14.64.27,1,.38a8.54,8.54,0,0,0,2.12.4"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
style={{
...style1,
...defaultStyles.rbgcpControlIcon,
...(col && { stroke: col }),
}}
d="M7.57,5.73C6,5.7,4.5,5.65,3,5.77a2.28,2.28,0,0,0-1.76.74A2.3,2.3,0,0,0,.94,7.83l0,3.82A4.73,4.73,0,0,0,1,12.9a1.64,1.64,0,0,0,.68,1,2.44,2.44,0,0,0,1,.27,25,25,0,0,0,4.74.09"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
style={{
...style1,
...defaultStyles.rbgcpControlIcon,
...(col && { stroke: col }),
}}
d="M12.43,14.32a44.12,44.12,0,0,0,4.6,0,2.24,2.24,0,0,0,1.76-.74,2.29,2.29,0,0,0,.27-1.32l0-3.81A4.81,4.81,0,0,0,19,7.15a1.62,1.62,0,0,0-.68-1,2.31,2.31,0,0,0-1-.28,26.8,26.8,0,0,0-4.74-.09"
/>
</svg>
)
}
export const PaletteIcon = ({ color }: ColorProps) => {
const { defaultStyles } = usePicker()
const col = color ?? ''
const style2 = {
strokeMiterlimit: 10,
strokeWidth: '0.5px',
}
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
style={{ width: 17 }}
>
<circle
style={{
...defaultStyles.rbgcpControlIcon2,
...(col && { fill: col }),
}}
cx="9.36"
cy="5.07"
r="1.71"
/>
<circle
style={{
...defaultStyles.rbgcpControlIcon2,
...(col && { fill: col }),
}}
cx="13.93"
cy="6.91"
r="1.71"
/>
<circle
style={{
...defaultStyles.rbgcpControlIcon2,
...(col && { fill: col }),
}}
cx="5.8"
cy="7.55"
r="1.71"
/>
<circle
style={{
...defaultStyles.rbgcpControlIcon2,
...(col && { fill: col }),
}}
cx="5.45"
cy="12.04"
r="1.71"
/>
<path
style={{
...style2,
...defaultStyles.rbgcpControlIcon,
...defaultStyles.rbgcpControlIcon2,
...(col && { fill: col, stroke: col }),
}}
d="M19.1,10c0,3.58-2.12,2.94-4.06,2.35-1.15-.34-2.24-.67-2.77-.08-.68.78-.54,2.07-.39,3.33.2,1.79.39,3.5-1.88,3.5A9.1,9.1,0,1,1,19.1,10ZM10,18c.7,0,.74-.19.75-.2a2.67,2.67,0,0,0,.07-1.27c0-.19,0-.42-.06-.67-.06-.53-.13-1.15-.14-1.67a3.82,3.82,0,0,1,.8-2.63,2.14,2.14,0,0,1,1.45-.7,4.36,4.36,0,0,1,1.32.12c.39.08.8.21,1.16.32h0c.39.12.74.23,1.08.3.74.17,1,.1,1.13,0S18,11.32,18,10a8,8,0,1,0-8,8Z"
/>
</svg>
)
}
export const DegreesIcon = ({ color }: { color?: string }) => {
const { defaultStyles } = usePicker()
const col = color ?? ''
const style2 = {
fill: 'none',
strokeMiterlimit: 10,
strokeWidth: '1.8px',
}
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
style={{ width: 15 }}
>
<polyline
strokeLinecap="round"
style={{
...style2,
...defaultStyles.rbgcpControlIcon,
...(col && { stroke: col }),
}}
points="13.86 2.01 1.7 16.99 18.77 16.99"
/>
<polyline
strokeLinecap="round"
style={{
...style2,
...defaultStyles.rbgcpControlIcon,
...(col && { stroke: col }),
}}
points="10.96 16.38 10.96 16.38 10.74 15.7 10.44 14.97 10.06 14.21 9.72 13.63 9.21 12.89 8.85 12.44 8.41 11.95 7.91 11.45 7.51 11.1"
/>
</svg>
)
}
export const StopIcon = () => {
const { defaultStyles } = usePicker()
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
style={{ width: 20, marginRight: 1 }}
>
<path
style={{ ...defaultStyles.rbgcpControlIcon2 }}
d="M2.39,8c-.63,0-1,.21-1,.63A.49.49,0,0,0,1.67,9a6.48,6.48,0,0,0,1.11.43A3,3,0,0,1,4,10.09a1.47,1.47,0,0,1,.35,1.09,1.75,1.75,0,0,1-.57,1.42,2.21,2.21,0,0,1-1.48.48,8.32,8.32,0,0,1-1.68-.21l-.31-.06.12-.94a13.7,13.7,0,0,0,1.8.16c.61,0,.92-.26.92-.77a.52.52,0,0,0-.21-.44,3.13,3.13,0,0,0-.85-.34A3.32,3.32,0,0,1,.66,9.79a1.43,1.43,0,0,1-.42-1.1A1.6,1.6,0,0,1,.78,7.36a2.32,2.32,0,0,1,1.49-.44,10.46,10.46,0,0,1,1.64.17l.32.07-.1.95C3.31,8,2.73,8,2.39,8Z"
/>
<path
style={{ ...defaultStyles.rbgcpControlIcon2 }}
d="M4.79,8.09V7H9.16V8.09H7.59V13H6.38V8.09Z"
/>
<path
style={{ ...defaultStyles.rbgcpControlIcon2 }}
d="M14,12.34a2.25,2.25,0,0,1-1.91.74,2.24,2.24,0,0,1-1.91-.74A3.85,3.85,0,0,1,9.61,10a4,4,0,0,1,.56-2.34,2.2,2.2,0,0,1,1.91-.77A2.21,2.21,0,0,1,14,7.69,4,4,0,0,1,14.55,10,3.85,3.85,0,0,1,14,12.34Zm-2.88-.77a1,1,0,0,0,1,.46,1,1,0,0,0,1-.46A3.25,3.25,0,0,0,13.3,10,3.45,3.45,0,0,0,13,8.46a1,1,0,0,0-1-.49,1,1,0,0,0-1,.49A3.43,3.43,0,0,0,10.85,10,3.38,3.38,0,0,0,11.11,11.57Z"
/>
<path
style={{ ...defaultStyles.rbgcpControlIcon2 }}
d="M17.77,11.24h-1V13H15.58V7h2.19a1.85,1.85,0,0,1,2.11,2.07,2.21,2.21,0,0,1-.54,1.6A2.07,2.07,0,0,1,17.77,11.24Zm-1-1h1c.6,0,.9-.37.9-1.12a1.18,1.18,0,0,0-.22-.79.88.88,0,0,0-.68-.24h-1Z"
/>
</svg>
)
}
================================================
FILE: src/components/index.tsx
================================================
'use client'
import React from 'react'
import PickerContextWrapper from '../context.js'
import Picker from './Picker.js'
import { ColorPickerProps } from '../shared/types.js'
import { defaultLocales } from '../constants.js'
import { objectToString } from '../utils/utils.js'
import { getStyles } from '../styles/styles.js'
export function ColorPicker({
idSuffix,
value = 'rgba(175, 51, 242, 1)',
onChange,
hideControls = false,
hideInputs = false,
hideOpacity = false,
hidePresets = false,
hideHue = false,
presets = [],
hideEyeDrop = false,
hideAdvancedSliders = false,
hideColorGuide = false,
hideInputType = false,
hideColorTypeBtns = false,
hideGradientType = false,
hideGradientAngle = false,
hideGradientStop = false,
hideGradientControls = false,
locales = defaultLocales,
width = 294,
height = 294,
style = {},
className,
disableDarkMode = false,
disableLightMode = false,
hidePickerSquare = false,
showHexAlpha = false,
config = {},
}: ColorPickerProps) {
const safeValue = objectToString(value)
const isDarkMode =
typeof window === 'undefined' || disableDarkMode
? false
: window.matchMedia('(prefers-color-scheme: dark)').matches ||
disableLightMode
? true
: false
// const contRef = useRef<HTMLDivElement>(null)
const defaultStyles = getStyles(isDarkMode, style)
const pickerIdSuffix = isDarkMode
? `-dark${idSuffix ? `-${idSuffix}` : ''}`
: idSuffix
? `-${idSuffix}`
: ''
return (
<div
// ref={contRef}
className={className}
style={{ ...defaultStyles.body, width: width }}
>
<PickerContextWrapper
value={safeValue}
onChange={onChange}
squareWidth={width}
passedConfig={config}
squareHeight={height}
isDarkMode={isDarkMode}
hideOpacity={hideOpacity}
showHexAlpha={showHexAlpha}
defaultStyles={defaultStyles}
pickerIdSuffix={pickerIdSuffix}
>
<Picker
hideControls={hideControls}
hideInputs={hideInputs}
hidePresets={hidePresets}
hideOpacity={hideOpacity}
hideHue={hideHue}
presets={presets}
hideEyeDrop={hideEyeDrop}
hideAdvancedSliders={hideAdvancedSliders}
hideColorGuide={hideColorGuide}
hideInputType={hideInputType}
hideColorTypeBtns={hideColorTypeBtns}
hideGradientType={hideGradientType}
hideGradientAngle={hideGradientAngle}
hideGradientStop={hideGradientStop}
hideGradientControls={hideGradientControls}
hidePickerSquare={hidePickerSquare}
locales={locales}
/>
</PickerContextWrapper>
</div>
)
}
================================================
FILE: src/constants.ts
================================================
export const defaultLocales = {
CONTROLS: {
SOLID: 'Solid',
GRADIENT: 'Gradient',
},
}
export const fakePresets = [
'rgba(0,0,0,1)',
'rgba(128,128,128, 1)',
'rgba(192,192,192, 1)',
'rgba(255,255,255, 1)',
'rgba(0,0,128,1)',
'rgba(0,0,255,1)',
'rgba(0,255,255, 1)',
'rgba(0,128,0,1)',
'rgba(128,128,0, 1)',
'rgba(0,128,128,1)',
'rgba(0,255,0, 1)',
'rgba(128,0,0, 1)',
'rgba(128,0,128, 1)',
'rgba(175, 51, 242, 1)',
'rgba(255,0,255, 1)',
'rgba(255,0,0, 1)',
'rgba(240, 103, 46, 1)',
'rgba(255,255,0, 1)',
]
================================================
FILE: src/context.tsx
================================================
import React, {
createContext,
useContext,
ReactNode,
useEffect,
useState,
} from 'react'
import { GradientProps, Styles, PassedConfig, Config } from './shared/types.js'
import { isUpperCase, getColorObj, getDetails } from './utils/utils.js'
import { low, high, getColors } from './utils/formatters.js'
import tinycolor from 'tinycolor2'
const PickerContext = createContext<PickerContextProps | null>(null)
export default function PickerContextWrapper({
value,
children,
onChange,
isDarkMode,
squareWidth,
hideOpacity,
showHexAlpha,
squareHeight,
passedConfig,
defaultStyles,
pickerIdSuffix,
}: PCWProps) {
const config: Config = {
barSize: passedConfig.barSize ?? defaultConfig.barSize,
crossSize: passedConfig.crossSize ?? defaultConfig.crossSize,
defaultColor: passedConfig.defaultColor ?? defaultConfig.defaultColor,
defaultGradient:
passedConfig.defaultGradient ?? defaultConfig.defaultGradient,
}
const colors = getColors(value, config.defaultColor, config.defaultGradient)
const { degrees, degreeStr, isGradient, gradientType } = getDetails(value)
const { currentColor, selectedColor, currentLeft } = getColorObj(colors, config.defaultGradient)
const [inputType, setInputType] = useState('rgb')
const [previous, setPrevious] = useState({})
const tinyColor = tinycolor(currentColor)
const rgba = tinyColor.toRgb()
const hsv = tinyColor.toHsv()
const [hc, setHc] = useState({ ...rgba, ...hsv })
useEffect(() => {
if (hsv?.s === 0) {
setHc({ ...rgba, ...hsv, h: hc?.h })
} else {
setHc({ ...rgba, ...hsv })
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentColor])
const createGradientStr = (newColors: GradientProps[]) => {
const sorted = newColors.sort(
(a: GradientProps, b: GradientProps) => a.left - b.left
)
const colorString = sorted?.map((cc: any) => `${cc?.value} ${cc.left}%`)
const newGrade = `${gradientType}(${degreeStr}, ${colorString.join(', ')})`
setPrevious({ ...previous, gradient: newGrade })
onChange(newGrade)
}
const handleGradient = (newColor: string, left?: number) => {
const remaining = colors?.filter(
(c: GradientProps) => !isUpperCase(c.value)
)
const newColors = [
{ value: newColor.toUpperCase(), left: left ?? currentLeft },
...remaining,
]
createGradientStr(newColors)
}
const handleChange = (newColor: string) => {
if (isGradient) {
handleGradient(newColor)
} else {
setPrevious({ ...previous, color: newColor })
onChange(newColor)
}
}
const deletePoint = () => {
if (colors?.length > 2) {
const formatted = colors?.map((fc: GradientProps, i: number) => ({
...fc,
value: i === selectedColor - 1 ? high(fc) : low(fc),
}))
const remaining = formatted?.filter(
(_: any, i: number) => i !== selectedColor
)
createGradientStr(remaining)
}
}
const pickerContext = {
hc,
setHc,
value,
colors,
config,
degrees,
onChange,
previous,
inputType,
tinyColor,
isDarkMode,
isGradient,
squareWidth,
hideOpacity,
currentLeft,
deletePoint,
showHexAlpha,
squareHeight,
setInputType,
gradientType,
handleChange,
currentColor,
selectedColor,
defaultStyles,
handleGradient,
pickerIdSuffix,
createGradientStr,
}
return (
<PickerContext.Provider value={pickerContext}>
{children}
</PickerContext.Provider>
)
}
export function usePicker() {
const pickerContext = useContext(PickerContext)
if (!pickerContext) {
throw new Error('usePicker has to be used within <PickerContext.Provider>')
}
return pickerContext
}
type PCWProps = {
value: string
squareWidth: number
children: ReactNode
squareHeight: number
hideOpacity: boolean
onChange: (arg0: string) => void
defaultStyles: Styles
isDarkMode: boolean
pickerIdSuffix: string
showHexAlpha: boolean
passedConfig: PassedConfig
}
export type PickerContextProps = {
hc: any
config: Config
value: string
colors: GradientProps[]
degrees: number
onChange: (arg0: string) => void
inputType: string
tinyColor: any
isGradient: boolean
squareWidth: number
hideOpacity: boolean
currentLeft: number
deletePoint: () => void
squareHeight: number
setInputType: (arg0: string) => void
gradientType?: string
handleChange: (arg0: string) => void
currentColor: string
selectedColor: number
setHc: (arg0: any) => void
handleGradient: (arg0: string, arg1?: number) => void
createGradientStr: (arg0: GradientProps[]) => void
defaultStyles: Styles
previous: {
color?: string
gradient?: string
}
isDarkMode: boolean
pickerIdSuffix: string
showHexAlpha: boolean
}
const defaultConfig = {
barSize: 18,
crossSize: 18,
inputSize: 40,
delay: 150,
defaultColor: 'rgba(175, 51, 242, 1)',
defaultGradient:
'linear-gradient(90deg, rgba(2,0,36,1) 0%, rgba(9,9,121,1) 35%, rgba(0,212,255,1) 100%)',
}
================================================
FILE: src/hooks/useColorPicker.ts
================================================
import tc from 'tinycolor2'
import { useState, useEffect } from 'react'
import { rgb2cmyk } from '../utils/converters.js'
import { ColorsProps, GradientProps, Config } from '../shared/types.js'
import { isUpperCase, getDetails, getColorObj } from '../utils/utils.js'
import { low, high, getColors, formatInputValues } from '../utils/formatters.js'
export const useColorPicker = (
value: string,
onChange: (arg0: string) => void,
config?: Config
) => {
const {
defaultColor = 'rgba(175, 51, 242, 1)',
defaultGradient = 'linear-gradient(90deg, rgba(2,0,36,1) 0%, rgba(9,9,121,1) 35%, rgba(0,212,255,1) 100%)',
} = config ?? {}
let colors = getColors(value, defaultColor, defaultGradient)
const { degrees, degreeStr, isGradient, gradientType } = getDetails(value)
const { currentColor, selectedColor, currentLeft } = getColorObj(
colors,
defaultGradient
)
const [previousColors, setPreviousColors] = useState([])
const getGradientObject = (currentValue: string) => {
if (currentValue) {
colors = getColors(currentValue, defaultColor, defaultGradient)
}
if (value) {
if (isGradient) {
return {
isGradient: true,
gradientType: gradientType,
degrees: degreeStr,
colors: colors?.map((c: ColorsProps) => ({
...c,
value: c.value?.toLowerCase(),
})),
}
} else {
return {
isGradient: false,
gradientType: null,
degrees: null,
colors: colors?.map((c: ColorsProps) => ({
...c,
value: c.value?.toLowerCase(),
})),
}
}
} else {
console.log(
'RBGCP ERROR - YOU MUST PASS A VALUE AND CALLBACK TO THE useColorPicker HOOK'
)
}
}
const tiny = tc(currentColor)
const { r, g, b, a } = tiny.toRgb()
const { h, s, l } = tiny.toHsl()
useEffect(() => {
if (tc(currentColor)?.isValid() && previousColors[0] !== currentColor) {
// @ts-expect-error - currentColor type issue
setPreviousColors([currentColor, ...previousColors.slice(0, 19)])
}
}, [currentColor, previousColors])
const setLinear = () => {
const remaining = value.split(/,(.+)/)[1]
onChange(`linear-gradient(90deg, ${remaining}`)
}
const setRadial = () => {
const remaining = value.split(/,(.+)/)[1]
onChange(`radial-gradient(circle, ${remaining}`)
}
const setDegrees = (newDegrees: number) => {
const remaining = value.split(/,(.+)/)[1]
onChange(
`linear-gradient(${formatInputValues(
newDegrees,
0,
360
)}deg, ${remaining}`
)
if (gradientType !== 'linear-gradient') {
console.log(
'Warning: you are updating degrees when the gradient type is not linear. This will change the gradients type which may be undesired'
)
}
}
const setSolid = (startingColor: string) => {
const newValue = startingColor ?? defaultColor ?? 'rgba(175, 51, 242, 1)'
onChange(newValue)
}
const setGradient = (startingGradiant: string) => {
const newValue =
startingGradiant ??
defaultGradient ??
'linear-gradient(90deg, rgba(2,0,36,1) 0%, rgba(9,9,121,1) 35%, rgba(0,212,255,1) 100%)'
onChange(newValue)
}
const createGradientStr = (newColors: GradientProps[]) => {
const sorted = newColors.sort(
(a: GradientProps, b: GradientProps) => a.left - b.left
)
const colorString = sorted?.map(
(cc: ColorsProps) => `${cc?.value} ${cc.left}%`
)
onChange(`${gradientType}(${degreeStr}, ${colorString.join(', ')})`)
}
const handleGradient = (newColor: string, left?: number) => {
const remaining = colors?.filter((c: ColorsProps) => !isUpperCase(c.value))
const newColors = [
{ value: newColor.toUpperCase(), left: left ?? currentLeft },
...remaining,
]
createGradientStr(newColors)
}
const handleChange = (newColor: string) => {
newColor = newColor?.replace(/\s+/g, '')
if (isGradient) {
handleGradient(newColor)
} else {
onChange(newColor)
}
}
const setR = (newR: number) => {
const newVal = formatInputValues(newR, 0, 255)
handleChange(`rgba(${newVal}, ${g}, ${b}, ${a})`)
}
const setG = (newG: number) => {
const newVal = formatInputValues(newG, 0, 255)
handleChange(`rgba(${r}, ${newVal}, ${b}, ${a})`)
}
const setB = (newB: number) => {
const newVal = formatInputValues(newB, 0, 255)
handleChange(`rgba(${r}, ${g}, ${newVal}, ${a})`)
}
const setA = (newA: number) => {
const newVal = formatInputValues(newA, 0, 100)
handleChange(`rgba(${r}, ${g}, ${b}, ${newVal / 100})`)
}
const setHue = (newHue: number) => {
const newVal = formatInputValues(newHue, 0, 360)
const tinyNew = tc({ h: newVal, s: s, l: l })
const { r, g, b } = tinyNew.toRgb()
handleChange(`rgba(${r}, ${g}, ${b}, ${a})`)
}
const setSaturation = (newSat: number) => {
const newVal = formatInputValues(newSat, 0, 100)
const tinyNew = tc({ h: h, s: newVal / 100, l: l })
const { r, g, b } = tinyNew.toRgb()
handleChange(`rgba(${r}, ${g}, ${b}, ${a})`)
}
const setLightness = (newLight: number) => {
const newVal = formatInputValues(newLight, 0, 100)
const tinyNew = tc({ h: h, s: s, l: newVal / 100 })
if (tinyNew?.isValid()) {
const { r, g, b } = tinyNew.toRgb()
handleChange(`rgba(${r}, ${g}, ${b}, ${a})`)
} else {
console.log(
'The new color was invalid, perhaps the lightness you passed in was a decimal? Please pass the new value between 0 - 100'
)
}
}
const valueToHSL = () => {
return tiny.toHslString()
}
const valueToHSV = () => {
return tiny.toHsvString()
}
const valueToHex = () => {
return tiny.toHexString()
}
const valueToCmyk = () => {
const { c, m, y, k } = rgb2cmyk(r, g, b)
return `cmyk(${c}, ${m}, ${y}, ${k})`
}
const setSelectedPoint = (index: number) => {
if (isGradient) {
const newGradStr = colors?.map((cc: GradientProps, i: number) => ({
...cc,
value: i === index ? high(cc) : low(cc),
}))
createGradientStr(newGradStr)
} else {
console.log(
'This function is only relevant when the picker is in gradient mode'
)
}
}
const addPoint = (left: number) => {
const newColors = [
...colors.map((c: GradientProps) => ({ ...c, value: low(c) })),
{ value: currentColor, left: left },
]
createGradientStr(newColors)
if (!left) {
console.log(
'You did not pass a stop value (left amount) for the new color point so it defaulted to 50'
)
}
}
const deletePoint = (index: number) => {
if (colors?.length > 2) {
const pointToDelete = index ?? selectedColor
const remaining = colors?.filter(
(rc: ColorsProps, i: number) => i !== pointToDelete
)
createGradientStr(remaining)
if (!index) {
console.log(
'You did not pass in the index of the point you wanted to delete so the function default to the currently selected point'
)
}
} else {
console.log(
'A gradient must have atleast two colors, disable your delete button when necessary'
)
}
}
const setPointLeft = (left: number) => {
handleGradient(currentColor, formatInputValues(left, 0, 100))
}
const rgbaArr = [r, g, b, a]
const hslArr = [h, s, l]
return {
setR,
setG,
setB,
setA,
setHue,
addPoint,
setSolid,
setLinear,
setRadial,
valueToHSL,
valueToHSV,
valueToHex,
valueToCmyk,
setDegrees,
setGradient,
setLightness,
setSaturation,
setSelectedPoint,
deletePoint,
isGradient,
gradientType,
degrees,
setPointLeft,
currentLeft,
rgbaArr,
hslArr,
handleChange,
previousColors,
getGradientObject,
selectedPoint: selectedColor,
}
}
================================================
FILE: src/hooks/usePaintHue.ts
================================================
import { useEffect, RefObject } from 'react'
import tinycolor from 'tinycolor2'
const usePaintHue = (
canvas: RefObject<HTMLCanvasElement>,
squareWidth: number
) => {
useEffect(() => {
const ctx = canvas?.current?.getContext('2d', { willReadFrequently: true })
if (ctx) {
ctx.rect(0, 0, squareWidth, 14)
const gradient = ctx.createLinearGradient(0, 0, squareWidth, 0)
for (let i = 0; i <= 360; i += 30) {
gradient.addColorStop(i / 360, `hsl(${i}, 100%, 50%)`)
}
ctx.fillStyle = gradient
ctx.fill()
}
}, [canvas, squareWidth])
}
export default usePaintHue
export const usePaintSat = (
canvas: RefObject<HTMLCanvasElement>,
h: number,
l: number,
squareWidth: number
) => {
useEffect(() => {
const ctx = canvas?.current?.getContext('2d', { willReadFrequently: true })
if (ctx) {
ctx.rect(0, 0, squareWidth, 14)
const gradient = ctx.createLinearGradient(0, 0, squareWidth, 0)
for (let i = 0; i <= 100; i += 10) {
gradient.addColorStop(i / 100, `hsl(${h}, ${i}%, ${l}%)`)
}
ctx.fillStyle = gradient
ctx.fill()
}
}, [canvas, h, l, squareWidth])
}
export const usePaintLight = (
canvas: RefObject<HTMLCanvasElement>,
h: number,
s: number,
squareWidth: number
) => {
useEffect(() => {
const ctx = canvas?.current?.getContext('2d', { willReadFrequently: true })
if (ctx) {
ctx.rect(0, 0, squareWidth, 14)
const gradient = ctx.createLinearGradient(0, 0, squareWidth, 0)
for (let i = 0; i <= 100; i += 10) {
gradient.addColorStop(i / 100, `hsl(${h}, ${s}%, ${i}%)`)
}
ctx.fillStyle = gradient
ctx.fill()
}
}, [canvas, h, s, squareWidth])
}
export const usePaintBright = (
canvas: RefObject<HTMLCanvasElement>,
h: number,
s: number,
squareWidth: number
) => {
useEffect(() => {
const ctx = canvas?.current?.getContext('2d', { willReadFrequently: true })
if (ctx) {
ctx.rect(0, 0, squareWidth, 14)
const gradient = ctx.createLinearGradient(0, 0, squareWidth, 0)
for (let i = 0; i <= 100; i += 10) {
const hsl = tinycolor({ h: h, s: s, v: i })
gradient.addColorStop(i / 100, hsl.toHslString())
}
ctx.fillStyle = gradient
ctx.fill()
}
}, [canvas, h, s, squareWidth])
}
================================================
FILE: src/hooks/usePaintSquare.ts
================================================
import { useEffect, RefObject } from 'react'
const usePaintSquare = (
canvas: RefObject<HTMLCanvasElement>,
hue: number,
squareWidth: number,
squareHeight: number
) => {
useEffect(() => {
if (canvas.current) {
const ctx = canvas.current.getContext('2d', { willReadFrequently: true })
if (ctx) {
ctx.fillStyle = `hsl(${hue}, 100%, 50%)`
ctx.fillRect(0, 0, squareWidth, squareHeight)
const gradientWhite = ctx.createLinearGradient(0, 0, squareWidth, 0)
gradientWhite.addColorStop(0, `rgba(255, 255, 255, 1)`)
gradientWhite.addColorStop(1, `rgba(255, 255, 255, 0)`)
ctx.fillStyle = gradientWhite
ctx.fillRect(0, 0, squareWidth, squareHeight)
const gradientBlack = ctx.createLinearGradient(0, 0, 0, squareHeight)
gradientBlack.addColorStop(0, `rgba(0, 0, 0, 0)`)
gradientBlack.addColorStop(1, `rgba(0, 0, 0, 1)`)
ctx.fillStyle = gradientBlack
ctx.fillRect(0, 0, squareWidth, squareHeight)
}
}
}, [canvas, hue, squareWidth, squareHeight])
}
export default usePaintSquare
================================================
FILE: src/index.ts
================================================
import { ColorPicker } from './components/index.js'
export { useColorPicker } from './hooks/useColorPicker.js'
export type {
Styles,
ColorsProps,
PassedConfig,
LocalesProps,
GradientProps,
ColorPickerProps,
} from './shared/types.js'
export default ColorPicker
================================================
FILE: src/shared/types.ts
================================================
export type ColorPickerProps = {
idSuffix?: string
value?: string
onChange: (value: string) => void
hideControls?: boolean
hideInputs?: boolean
hideOpacity?: boolean
hidePresets?: boolean
hideHue?: boolean
presets?: string[]
hideEyeDrop?: boolean
hideAdvancedSliders?: boolean
hideColorGuide?: boolean
hideInputType?: boolean
hideColorTypeBtns?: boolean
hideGradientType?: boolean
hideGradientAngle?: boolean
hideGradientStop?: boolean
hideGradientControls?: boolean
width?: number
height?: number
style?: Styles
className?: any
locales?: LocalesProps
disableDarkMode?: boolean
disableLightMode?: boolean
hidePickerSquare?: boolean
showHexAlpha?: boolean
config?: PassedConfig
}
export type ColorsProps = {
value: string
index?: number
left?: number
}
export type GradientProps = {
value: string
index: number
left: number
}
export type LocalesProps = {
CONTROLS: controlsProps
}
type controlsProps = {
SOLID: string
GRADIENT: string
}
export type ThemeProps = {
light: ThemeMode
dark: ThemeMode
}
export type ThemeMode = {
color?: string
background?: string
highlights?: string
accent?: string
}
export type Styles = Partial<{
body: React.CSSProperties
rbgcpControlBtn: React.CSSProperties
rbgcpControlIcon: React.CSSProperties
rbgcpControlIconBtn: React.CSSProperties
rbgcpControlBtnWrapper: React.CSSProperties
rbgcpColorModelDropdown: React.CSSProperties
rbgcpEyedropperCover: React.CSSProperties
rbgcpControlInput: React.CSSProperties
rbgcpInputLabel: React.CSSProperties
rbgcpInput: React.CSSProperties
rbgcpHandle: React.CSSProperties
rbgcpCanvasWrapper: React.CSSProperties
rbgcpCheckered: React.CSSProperties
rbgcpOpacityOverlay: React.CSSProperties
rbgcpGradientHandleWrap: React.CSSProperties
rbgcpGradientHandle: React.CSSProperties
rbgcpControlIcon2: React.CSSProperties
rbgcpControlBtnSelected: React.CSSProperties
rbgcpComparibleLabel: React.CSSProperties
rbgcpColorModelDropdownBtn: React.CSSProperties
rbgcpControlInputWrap: React.CSSProperties
rbgcpStopInputWrap: React.CSSProperties
rbgcpStopInput: React.CSSProperties
rbgcpDegreeInputWrap: React.CSSProperties
rbgcpDegreeInput: React.CSSProperties
rbgcpDegreeIcon: React.CSSProperties
rbgcpEyedropperBtn: React.CSSProperties
rbgcpHexInput: React.CSSProperties
rbgcpInputsWrap: React.CSSProperties
}>
export type PassedConfig = {
barSize?: number
crossSize?: number
defaultColor?: string
defaultGradient?: string
}
export type Config = {
barSize: number
crossSize: number
defaultColor: string
defaultGradient: string
}
================================================
FILE: src/styles/darkStyles.ts
================================================
export const darkStyles: Record<string, Record<string, string | number>> = {
body: {
background: 'rgb(32, 32, 32)',
},
rbgcpInputLabel: {
color: 'rgb(212, 212, 212)',
},
rbgcpControlBtnWrapper: {
background: 'rgb(54, 54, 54)',
},
rbgcpInput: {
border: 'none',
color: 'white',
background: 'rgb(54, 54, 54)',
},
rbgcpControlBtn: {
color: 'rgb(212, 212, 212)',
},
rbgcpControlIcon: {
stroke: 'rgb(212, 212, 212)',
},
rbgcpControlIcon2: {
fill: 'rgb(212, 212, 212)',
},
rbgcpControlInput: {
color: 'white',
},
rbgcpControlBtnSelected: {
background: 'black',
color: '#568cf5',
},
rbgcpDegreeIcon: {
color: 'rgb(212, 212, 212)',
},
rbgcpColorModelDropdown: {
background: 'rgb(32, 32, 32)',
},
rbgcpComparibleLabel: {
color: 'rgb(212, 212, 212)',
}
}
================================================
FILE: src/styles/styles.ts
================================================
import { darkStyles } from './darkStyles.js';
import { Styles } from '../shared/types.js';
const styles: Styles = {
body: {
boxSizing: 'border-box',
background: 'rgb(255, 255, 255)',
},
rbgcpControlBtn: {
paddingLeft: '8px',
paddingRight: '8px',
lineHeight: '1',
borderRadius: '4px',
fontWeight: 700,
fontSize: '12px',
height: '24px',
transition: 'all 160ms ease',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: 'rgba(255, 255, 255, 0)',
boxShadow: '1px 1px 3px rgba(0, 0, 0, 0)',
color: 'rgb(86, 86, 86)',
},
rbgcpControlIcon: {
stroke: 'rgb(50, 49, 54)',
},
rbgcpControlIconBtn: {
width: '30px',
height: '24px',
borderRadius: '4px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
position: 'relative',
},
rbgcpControlBtnWrapper: {
height: '28px',
background: '#e9e9f5',
borderRadius: '6px',
padding: '2px',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
position: 'relative',
},
rbgcpColorModelDropdown: {
position: 'absolute',
right: '-2px',
top: '34px',
padding: '5px',
background: '#e9e9f5',
zIndex: 100000000,
borderRadius: '6px',
boxShadow: '1px 1px 14px 1px rgba(0, 0, 0, 0.25)',
},
rbgcpEyedropperCover: {
position: 'fixed',
left: '0px',
top: '0px',
zIndex: 100000000,
width: '100vw',
height: '100vh',
cursor: 'copy',
},
rbgcpControlInput: {
height: '24px',
borderRadius: '4px',
border: 'none',
outline: 'none',
textAlign: 'center',
width: '34px',
fontWeight: 500,
color: 'rgb(50, 49, 54)',
fontSize: '13px',
background: 'transparent',
},
rbgcpInputLabel: {
textAlign: 'center',
lineHeight: '1.2',
fontWeight: 700,
color: 'rgb(86, 86, 86)',
fontSize: '11px',
},
rbgcpInput: {
height: '32px',
borderRadius: '6px',
border: '1px solid #bebebe',
width: '100%',
padding: '2px',
outline: 'none',
color: 'black',
fontWeight: 400,
textAlign: 'center',
background: 'transparent',
fontSize: '15px',
},
rbgcpHandle: {
position: 'absolute',
border: '2px solid white',
borderRadius: '50%',
boxShadow: '0px 0px 3px rgba(0, 0, 0, 0.5)',
width: '18px',
height: '18px',
zIndex: 1000,
transition: 'all 30ms linear',
boxSizing: 'border-box',
willChange: 'transform',
outline: 'none',
},
rbgcpCanvasWrapper: {
borderRadius: '6px',
overflow: 'hidden',
height: '294px',
},
rbgcpCheckered: {
background: `linear-gradient(
45deg,
rgba(0, 0, 0, 0.18) 25%,
transparent 25%,
transparent 75%,
rgba(0, 0, 0, 0.18) 75%,
rgba(0, 0, 0, 0.18) 0
),
linear-gradient(
45deg,
rgba(0, 0, 0, 0.18) 25%,
transparent 25%,
transparent 75%,
rgba(0, 0, 0, 0.18) 75%,
rgba(0, 0, 0, 0.18) 0
),
white`,
backgroundRepeat: 'repeat, repeat',
backgroundPosition: '0px 0, 7px 7px',
transformOrigin: '0 0 0',
backgroundOrigin: 'padding-box, padding-box',
backgroundClip: 'border-box, border-box',
backgroundSize: '14px 14px, 14px 14px',
boxShadow: 'none',
textShadow: 'none',
transition: 'none',
transform: 'scaleX(1) scaleY(1) scaleZ(1)',
borderRadius: '10px',
},
rbgcpOpacityOverlay: {
position: 'absolute',
left: '0px',
top: '0px',
width: '100%',
height: '100%',
borderRadius: '10px',
},
rbgcpGradientHandleWrap: {
position: 'absolute',
zIndex: 10000,
top: '-2px',
outline: 'none',
},
rbgcpGradientHandle: {
border: '2px solid white',
borderRadius: '50%',
boxShadow: '0px 0px 3px rgba(0, 0, 0, 0.5)',
width: '18px',
height: '18px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
},
rbgcpControlIcon2: {
fill: '#323136',
},
rbgcpControlBtnSelected: {
background: 'white',
color: '#568cf5',
boxShadow: '1px 1px 3px rgba(0, 0, 0, 0.2)',
},
rbgcpComparibleLabel: {
color: '#323136',
}
};
export const getStyles = (isDarkMode: boolean, passedStyles: Styles) => {
const mergedStyles = { ...styles }
if (isDarkMode) {
for (const key in darkStyles) {
if (Object.prototype.hasOwnProperty.call(darkStyles, key)) {
;(mergedStyles as Record<string, any>)[key] = {
...(Object.prototype.hasOwnProperty.call(mergedStyles, key)
? (mergedStyles as Record<string, any>)[key]
: {}),
...(darkStyles as Record<string, any>)[key],
}
}
}
}
for (const key in passedStyles) {
if (Object.prototype.hasOwnProperty.call(passedStyles, key)) {
;(mergedStyles as Record<string, any>)[key] = {
...(Object.prototype.hasOwnProperty.call(mergedStyles, key)
? (mergedStyles as Record<string, any>)[key]
: {}),
...(passedStyles as Record<string, any>)[key],
}
}
}
return mergedStyles
}
export const colorTypeBtnStyles = (selected: boolean, styles: Styles): React.CSSProperties => {
if (selected) {
return {...styles.rbgcpControlBtn, ...styles.rbgcpControlBtnSelected}
} else {
return { ...styles.rbgcpControlBtn }
}
}
export const controlBtnStyles = (selected: boolean, styles: Styles): React.CSSProperties => {
if (selected) {
return { ...styles.rbgcpControlIconBtn, ...styles.rbgcpControlBtnSelected }
} else {
return { ...styles.rbgcpControlIconBtn }
}
}
export const modalBtnStyles = (selected: boolean, styles: Styles): React.CSSProperties => {
if (selected) {
return { ...styles.rbgcpControlBtn, ...styles.rbgcpColorModelDropdownBtn, ...styles.rbgcpControlBtnSelected }
} else {
return { ...styles.rbgcpControlBtn, ...styles.rbgcpColorModelDropdownBtn }
}
}
================================================
FILE: src/utils/converters.ts
================================================
export function rgb2cmyk(r: number, g: number, b: number) {
let computedC = 0
let computedM = 0
let computedY = 0
let computedK = 0
if (
r === null ||
g === null ||
b === null ||
isNaN(r) ||
isNaN(g) ||
isNaN(b)
) {
console.log('Please enter numeric RGB values!')
return { c: 0, m: 0, k: 0, y: 1 }
}
if (r < 0 || g < 0 || b < 0 || r > 255 || g > 255 || b > 255) {
console.log('RGB values must be in the range 0 to 255.')
return { c: 0, m: 0, k: 0, y: 1 }
}
if (r === 0 && g === 0 && b === 0) {
computedK = 1
return { c: 0, m: 0, k: 0, y: 1 }
}
computedC = 1 - r / 255
computedM = 1 - g / 255
computedY = 1 - b / 255
const minCMY = Math.min(computedC, Math.min(computedM, computedY))
computedC = (computedC - minCMY) / (1 - minCMY)
computedM = (computedM - minCMY) / (1 - minCMY)
computedY = (computedY - minCMY) / (1 - minCMY)
computedK = minCMY
return { c: computedC, m: computedM, y: computedY, k: computedK }
}
export const cmykToRgb = ({
c,
m,
y,
k,
}: {
c: number
m: number
y: number
k: number
}) => {
const r = 255 * (1 - c) * (1 - k)
const g = 255 * (1 - m) * (1 - k)
const b = 255 * (1 - y) * (1 - k)
return { r: r, g: g, b: b }
}
export const getHexAlpha = (opacityPercent: number): string => {
if (typeof opacityPercent !== 'number') {
return 'FF'
}
if (opacityPercent < 0) {
return '00'
}
if (opacityPercent > 1) {
return 'FF'
}
return Math.round(opacityPercent * 255)
.toString(16)
.padStart(2, '0')
.toUpperCase()
}
================================================
FILE: src/utils/formatters.ts
================================================
import { ColorsProps } from '../shared/types.js'
import { gradientParser } from './gradientParser.js'
export const low = (color: ColorsProps) => {
return color.value.toLowerCase()
}
export const high = (color: ColorsProps) => {
return color.value.toUpperCase()
}
export const getColors = (value: string, defaultColor: string, defaultGradient: string) => {
const isGradient = value?.includes('gradient')
if (isGradient) {
const isConic = value?.includes('conic')
const safeValue = !isConic ? value : defaultGradient
if (isConic) {
console.log('Sorry we cant handle conic gradients yet')
}
const obj = gradientParser(safeValue)
return obj?.colorStops
} else {
const safeValue = value || defaultColor
return [{ value: safeValue }]
}
}
export const formatInputValues = (value: number, min: number, max: number) => {
return isNaN(value) ? min : value < min ? min : value > max ? max : value
}
export const round = (val: number) => {
return Math.round(val)
}
================================================
FILE: src/utils/gradientParser.ts
================================================
import { high, low } from './formatters.js'
import { isUpperCase } from './utils.js'
import tinycolor from 'tinycolor2'
export const gradientParser = (input = '') => {
const tokens = {
linearGradient: /^(-(webkit|o|ms|moz)-)?(linear-gradient)/i,
repeatingLinearGradient:
/^(-(webkit|o|ms|moz)-)?(repeating-linear-gradient)/i,
radialGradient: /^(-(webkit|o|ms|moz)-)?(radial-gradient)/i,
repeatingRadialGradient:
/^(-(webkit|o|ms|moz)-)?(repeating-radial-gradient)/i,
sideOrCorner:
/^to (left (top|bottom)|right (top|bottom)|top (left|right)|bottom (left|right)|left|right|top|bottom)/i,
extentKeywords:
/^(closest-side|closest-corner|farthest-side|farthest-corner|contain|cover)/,
positionKeywords: /^(left|center|right|top|bottom)/i,
pixelValue: /^(-?(([0-9]*\.[0-9]+)|([0-9]+\.?)))px/,
percentageValue: /^(-?(([0-9]*\.[0-9]+)|([0-9]+\.?)))%/,
emValue: /^(-?(([0-9]*\.[0-9]+)|([0-9]+\.?)))em/,
angleValue: /^(-?(([0-9]*\.[0-9]+)|([0-9]+\.?)))deg/,
startCall: /^\(/,
endCall: /^\)/,
comma: /^,/,
hexColor: /^#([0-9a-fA-F]+)/,
literalColor: /^([a-zA-Z]+)/,
rgbColor: /^rgb/i,
spacedRgbColor: /^(\d{1,3})\s+(\d{1,3})\s+(\d{1,3})\s+\/\s+([0-1](\.\d+)?)/,
rgbaColor: /^rgba/i,
hslColor: /^hsl/i,
hsvColor: /^hsv/i,
number: /^(([0-9]*\.[0-9]+)|([0-9]+\.?))/,
}
function error(msg: any) {
const err = new Error(input + ': ' + msg)
// err.source = input
throw err
}
function consume(size: any) {
input = input.substr(size)
}
function scan(regexp: any) {
const blankCaptures = /^[\n\r\t\s]+/.exec(input)
if (blankCaptures) {
consume(blankCaptures[0].length)
}
const captures = regexp.exec(input)
if (captures) {
consume(captures[0].length)
}
return captures
}
function matchListing(matcher: any) {
let captures = matcher()
const result = []
if (captures) {
result.push(captures)
while (scan(tokens.comma)) {
captures = matcher()
if (captures) {
result.push(captures)
} else {
error('One extra comma')
}
}
}
return result
}
function match(type: any, pattern: any, captureIndex: any) {
const captures = scan(pattern)
if (captures) {
return {
type: type,
value: captures[captureIndex],
}
}
}
function matchHexColor() {
const hexObj = match('hex', tokens.hexColor, 1)
if (hexObj?.value) {
const { r, g, b, a } = tinycolor(hexObj?.value).toRgb()
return {
value: `rgba(${r}, ${g}, ${b}, ${a})`,
}
}
}
const checkCaps = (val: any) => {
const capIt = isUpperCase(val?.[0])
return {
value: `${capIt ? 'RGBA' : 'rgba'}(${matchListing(matchNumber)})`,
}
}
function matchCall(pattern: any, callback: any) {
const captures = scan(pattern)
if (captures) {
if (!scan(tokens.startCall)) {
error('Missing (')
}
const result = callback(captures)
if (!scan(tokens.endCall)) {
error('Missing )')
}
return result
}
}
function matchHSLColor() {
return matchCall(tokens.hslColor, convertHsl)
}
function matchRGBAColor() {
return matchCall(tokens.rgbaColor, checkCaps)
}
function matchRGBColor() {
return matchCall(tokens.rgbColor, convertRgb)
}
function matchLiteralColor() {
const litObj = match('literal', tokens.literalColor, 0)
if (litObj?.value) {
const { r, g, b, a } = tinycolor(litObj?.value).toRgb()
return {
value: `rgba(${r}, ${g}, ${b}, ${a})`,
}
}
}
function matchHSVColor() {
return matchCall(tokens.hsvColor, convertHsv)
}
function matchColor() {
return (
matchHexColor() ||
matchHSLColor() ||
matchRGBAColor() ||
matchRGBColor() ||
matchLiteralColor() ||
matchHSVColor()
)
}
function matchColorStop() {
const color = matchColor()
if (!color) {
error('Expected color definition')
}
color.left = parseInt(matchDistance()?.value)
return color
}
function matchGradient(
gradientType: any,
pattern: any,
orientationMatcher: any
) {
return matchCall(pattern, function () {
const orientation = orientationMatcher()
if (orientation) {
if (!scan(tokens.comma)) {
error('Missing comma before color stops')
}
}
return {
type: gradientType,
orientation: orientation,
colorStops: matchListing(matchColorStop),
}
})
}
function matchLinearOrientation() {
return matchSideOrCorner() || matchAngle()
}
function matchDefinition() {
return (
matchGradient(
'linear-gradient',
tokens.linearGradient,
matchLinearOrientation
) ||
matchGradient(
'repeating-linear-gradient',
tokens.repeatingLinearGradient,
matchLinearOrientation
) ||
matchGradient(
'radial-gradient',
tokens.radialGradient,
matchListRadialOrientations
) ||
matchGradient(
'repeating-radial-gradient',
tokens.repeatingRadialGradient,
matchListRadialOrientations
)
)
}
function matchListDefinitions() {
return matchListing(matchDefinition)
}
function getAST() {
const ast = matchListDefinitions()
if (input.length > 0) {
error('Invalid input not EOF')
}
const ast0 = ast[0]
const checkSelected = ast0?.colorStops?.filter((c: any) =>
isUpperCase(c.value)
).length
const getGradientObj = () => {
if (checkSelected > 0) {
return ast0
} else {
const val = (c: any, i: number) => (i === 0 ? high(c) : low(c))
return {
...ast0,
colorStops: ast0.colorStops.map((c: any, i: number) => ({
...c,
value: val(c, i),
})),
}
}
}
return getGradientObj()
}
function matchSideOrCorner() {
return match('directional', tokens.sideOrCorner, 1)
}
function matchAngle() {
return match('angular', tokens.angleValue, 1)
}
function matchListRadialOrientations() {
var radialOrientations,
radialOrientation = matchRadialOrientation(),
lookaheadCache
if (radialOrientation) {
radialOrientations = []
radialOrientations.push(radialOrientation)
lookaheadCache = input
if (scan(tokens.comma)) {
radialOrientation = matchRadialOrientation()
if (radialOrientation) {
radialOrientations.push(radialOrientation)
} else {
input = lookaheadCache
}
}
}
return radialOrientations
}
function matchRadialOrientation() {
let radialType = matchCircle() || matchEllipse()
if (radialType) {
// @ts-expect-error - need to circle back for these types
radialType.at = matchAtPosition()
} else {
const extent = matchExtentKeyword()
if (extent) {
radialType = extent
const positionAt = matchAtPosition()
if (positionAt) {
// @ts-expect-error - need to circle back for these types
radialType.at = positionAt
}
} else {
const defaultPosition = matchPositioning()
if (defaultPosition) {
radialType = {
type: 'default-radial',
// @ts-expect-error - need to circle back for these types
at: defaultPosition,
}
}
}
}
return radialType
}
function matchLength() {
return match('px', tokens.pixelValue, 1) || match('em', tokens.emValue, 1)
}
function matchCircle() {
const circle = match('shape', /^(circle)/i, 0)
if (circle) {
// @ts-expect-error - need to circle back for these types
circle.style = matchLength() || matchExtentKeyword()
}
return circle
}
function matchEllipse() {
const ellipse = match('shape', /^(ellipse)/i, 0)
if (ellipse) {
// @ts-expect-error - need to circle back for these types
ellipse.style = matchDistance() || matchExtentKeyword()
}
return ellipse
}
function matchExtentKeyword() {
return match('extent-keyword', tokens.extentKeywords, 1)
}
function matchAtPosition() {
if (match('position', /^at/, 0)) {
const positioning = matchPositioning()
if (!positioning) {
error('Missing positioning value')
}
return positioning
}
}
function matchPositioning() {
const location = matchCoordinates()
if (location.x || location.y) {
return {
type: 'position',
value: location,
}
}
}
function matchCoordinates() {
return {
x: matchDistance(),
y: matchDistance(),
}
}
function matchNumber() {
return scan(tokens.number)[1]
}
const convertHsl = (val: any) => {
const capIt = isUpperCase(val?.[0])
const hsl = matchListing(matchNumber)
const { r, g, b, a } = tinycolor({
h: hsl[0],
s: hsl[1],
l: hsl[2],
a: hsl[3] || 1,
}).toRgb()
return {
value: `${capIt ? 'RGBA' : 'rgba'}(${r}, ${g}, ${b}, ${a})`,
}
}
const convertHsv = (val: any) => {
const capIt = isUpperCase(val?.[0])
const hsv = matchListing(matchNumber)
const { r, g, b, a } = tinycolor({
h: hsv[0],
s: hsv[1],
v: hsv[2],
a: hsv[3] || 1,
}).toRgb()
return {
value: `${capIt ? 'RGBA' : 'rgba'}(${r}, ${g}, ${b}, ${a})`,
}
}
const convertRgb = (val: any) => {
const capIt = isUpperCase(val?.[0])
const captures = scan(tokens.spacedRgbColor)
const [, r, g, b, a = 1] = captures || [null, ...matchListing(matchNumber)]
return {
value: `${capIt ? 'RGBA' : 'rgba'}(${r}, ${g}, ${b}, ${a})`,
}
}
function matchDistance() {
return (
match('%', tokens.percentageValue, 1) ||
matchPositionKeyword() ||
matchLength()
)
}
function matchPositionKeyword() {
return match('position-keyword', tokens.positionKeywords, 1)
}
return getAST()
}
================================================
FILE: src/utils/utils.ts
================================================
import { formatInputValues } from './formatters.js'
import { ColorsProps } from '../shared/types.js'
export const safeBounds = (e: any) => {
const client = e.target.parentNode.getBoundingClientRect()
const className = e.target.className
const adjuster = className === 'c-resize ps-rl' ? 15 : 0
return {
offsetLeft: client?.x + adjuster,
offsetTop: client?.y,
clientWidth: client?.width,
clientHeight: client?.height,
}
}
export function getHandleValue(e: any, barSize: number) {
const { offsetLeft, clientWidth } = safeBounds(e)
const pos = e.clientX - offsetLeft - barSize / 2
const adjuster = clientWidth - 18
const bounded = formatInputValues(pos, 0, adjuster)
return Math.round(bounded / (adjuster / 100))
}
export function computeSquareXY(
s: number,
v: number,
squareWidth: number,
squareHeight: number,
crossSize: number
) {
const x = s * squareWidth - crossSize / 2
const y = ((100 - v) / 100) * squareHeight - crossSize / 2
return [x, y]
}
const getClientXY = (e: any) => {
if (e.clientX) {
return { clientX: e.clientX, clientY: e.clientY }
} else {
const touch = e.touches[0] || {}
return { clientX: touch.clientX, clientY: touch.clientY }
}
}
export function computePickerPosition(e: any, crossSize: number) {
const { offsetLeft, offsetTop, clientWidth, clientHeight } = safeBounds(e)
const { clientX, clientY } = getClientXY(e)
const getX = () => {
const xPos = clientX - offsetLeft - crossSize / 2
return formatInputValues(xPos, -9, clientWidth - 10)
}
const getY = () => {
const yPos = clientY - offsetTop - crossSize / 2
return formatInputValues(yPos, -9, clientHeight - 10)
}
return [getX(), getY()]
}
// export const getGradientType = (value: string) => {
// return value?.split('(')[0]
// }
export const isUpperCase = (str: string) => {
return str?.[0] === str?.[0]?.toUpperCase()
}
// export const compareGradients = (g1: string, g2: string) => {
// const ng1 = g1?.toLowerCase()?.replaceAll(' ', '')
// const ng2 = g2?.toLowerCase()?.replaceAll(' ', '')
// if (ng1 === ng2) {
// return true
// } else {
// return false
// }
// }
const convertShortHandDeg = (dir: any) => {
if (dir === 'to top') {
return 0
} else if (dir === 'to bottom') {
return 180
} else if (dir === 'to left') {
return 270
} else if (dir === 'to right') {
return 90
} else if (dir === 'to top right') {
return 45
} else if (dir === 'to bottom right') {
return 135
} else if (dir === 'to bottom left') {
return 225
} else if (dir === 'to top left') {
return 315
} else {
const safeDir = dir || 0
return parseInt(safeDir)
}
}
export const objectToString = (value: any) => {
if (typeof value === 'string') {
return value
} else {
if (value?.type?.includes('gradient')) {
const sorted = value?.colorStops?.sort(
(a: any, b: any) => a?.left - b?.left
)
const string = sorted
?.map((c: any) => `${c?.value} ${c?.left}%`)
?.join(', ')
const type = value?.type
const degs = convertShortHandDeg(value?.orientation?.value)
const gradientStr = type === 'linear-gradient' ? `${degs}deg` : 'circle'
return `${type}(${gradientStr}, ${string})`
} else {
const color = value?.colorStops[0]?.value || 'rgba(175, 51, 242, 1)'
return color
}
}
}
export const getColorObj = (colors: ColorsProps[], defaultGradient: string) => {
const idxCols = colors?.map((c: ColorsProps, i: number) => ({
...c,
index: i,
}))
const upperObj = idxCols?.find((c: ColorsProps) => isUpperCase(c.value))
const ccObj = upperObj || idxCols[0]
return {
currentColor: ccObj?.value || defaultGradient,
selectedColor: ccObj?.index || 0,
currentLeft: ccObj?.left || 0,
}
}
const getDegrees = (value: string) => {
const s1 = value?.split(',')[0]
const s2 = s1?.split('(')[1]?.replace('deg', '')
return convertShortHandDeg(s2)
}
export const getDetails = (value: string) => {
const isGradient = value?.includes('gradient')
const gradientType = value?.split('(')[0]
const degrees = getDegrees(value)
const degreeStr =
gradientType === 'linear-gradient' ? `${degrees}deg` : 'circle'
return {
degrees,
degreeStr,
isGradient,
gradientType,
}
}
================================================
FILE: test/gradientParser.spec.js
================================================
import { gradientParser } from '../src/utils/gradientParser'
import { describe, expect, it } from 'vitest'
describe('gradientParser', () => {
it('should parse linear gradient with hex colors', () => {
const gradient = 'linear-gradient(45deg, #012345 0%, #6789AB 100%)'
expect(gradientParser(gradient)).toEqual({
colorStops: [
{ left: 0, type: 'hex', value: '012345' },
{ left: 100, type: 'hex', value: '6789AB' },
],
orientation: { type: 'angular', value: '45' },
type: 'linear-gradient',
})
})
it('should parse linear gradient with comma-separated rgba colors', () => {
const gradient =
'linear-gradient(45deg, rgba(1, 2, 3, 0.123) 0%, rgba(4, 5, 6, 0.456) 100%)'
expect(gradientParser(gradient)).toEqual({
colorStops: [
{ left: 0, value: 'RGBA(1,2,3,0.123)' },
{ left: 100, value: 'rgba(4,5,6,0.456)' },
],
orientation: { type: 'angular', value: '45' },
type: 'linear-gradient',
})
})
it('should parse linear gradient with rgb + alpha colors', () => {
const gradient =
'linear-gradient(45deg, rgb(1 2 3 / 0.123) 0%, rgb(4 5 6 / 0.456) 100%)'
expect(gradientParser(gradient)).toEqual({
colorStops: [
{ left: 0, value: 'RGBA(1, 2, 3, 0.123)' },
{ left: 100, value: 'rgba(4, 5, 6, 0.456)' },
],
orientation: { type: 'angular', value: '45' },
type: 'linear-gradient',
})
})
it('should parse linear gradient with comma-separated rgb colors', () => {
const gradient =
'linear-gradient(45deg, rgb(1, 2, 3) 0%, rgb(4, 5, 6) 100%)'
expect(gradientParser(gradient)).toEqual({
colorStops: [
{ left: 0, value: 'RGBA(1, 2, 3, 1)' },
{ left: 100, value: 'rgba(4, 5, 6, 1)' },
],
orientation: { type: 'angular', value: '45' },
type: 'linear-gradient',
})
})
// TODO: Figure out pattern of "HSV" colors
// TODO: Make it accept percentages
// it('should parse linear gradient with hsl colors', () => {
// const gradient = 'linear-gradient(45deg, hsl(0, 100%, 50%) 0%, hsl(100, 50%, 85%) 100%)'
//
// expect(gradientParser(gradient)).toEqual({})
// })
})
================================================
FILE: test/utils.spec.js
================================================
import {
isUpperCase,
getNewHsl,
getGradientType,
getDegrees,
} from '../src/utils/utils'
import { describe, expect, it } from 'vitest'
describe('isUpperCase', () => {
it('should return true when the first letter of the string is upper-cased', () => {
expect(isUpperCase('Aloha oe')).toBe(true)
})
it('should return false when the first letter of the string is lower-cased', () => {
expect(isUpperCase('aLOHA OE')).toBe(false)
})
})
describe('getNewHsl', () => {
it('should return correct RGBA color for given HSL color', () => {
const callback = () => {}
const output = getNewHsl(116, 79, 19, 0.5, callback)
expect(output).toEqual('rgba(15, 87, 10, 0.5)')
})
it('should trigger callback with correct arguments', () => {
const callback = jest.fn()
getNewHsl(116, 79, 19, 0.5, callback)
expect(callback).toHaveBeenCalledWith(116)
})
})
describe('getGradientType', () => {
it('should pick the correct prefix of gradient values', () => {
const assertionMap = [
['linear-gradient(30deg, #6789AB 0%, #012345 100%)', 'linear-gradient'],
[
'-webkit-linear-gradient(30deg, #6789AB 0%, #012345 100%)',
'-webkit-linear-gradient',
],
[
'-o-linear-gradient(30deg, #6789AB 0%, #012345 100%)',
'-o-linear-gradient',
],
[
'-ms-linear-gradient(30deg, #6789AB 0%, #012345 100%)',
'-ms-linear-gradient',
],
[
'-moz-linear-gradient(30deg, #6789AB 0%, #012345 100%)',
'-moz-linear-gradient',
],
[
'repeating-linear-gradient(30deg, #6789AB 0%, #012345 100%)',
'repeating-linear-gradient',
],
[
'-webkit-repeating-linear-gradient(30deg, #6789AB 0%, #012345 100%)',
'-webkit-repeating-linear-gradient',
],
[
'-o-repeating-linear-gradient(30deg, #6789AB 0%, #012345 100%)',
'-o-repeating-linear-gradient',
],
[
'-ms-repeating-linear-gradient(30deg, #6789AB 0%, #012345 100%)',
'-ms-repeating-linear-gradient',
],
[
'-moz-repeating-linear-gradient(30deg, #6789AB 0%, #012345 100%)',
'-moz-repeating-linear-gradient',
],
['radial-gradient(#6789AB 0%, #012345 100%)', 'radial-gradient'],
[
'-webkit-radial-gradient(#6789AB 0%, #012345 100%)',
'-webkit-radial-gradient',
],
['-o-radial-gradient(#6789AB 0%, #012345 100%)', '-o-radial-gradient'],
['-ms-radial-gradient(#6789AB 0%, #012345 100%)', '-ms-radial-gradient'],
[
'-moz-radial-gradient(#6789AB 0%, #012345 100%)',
'-moz-radial-gradient',
],
[
'repeating-radial-gradient(#6789AB 0%, #012345 100%)',
'repeating-radial-gradient',
],
[
'-webkit-repeating-radial-gradient(#6789AB 0%, #012345 100%)',
'-webkit-repeating-radial-gradient',
],
[
'-o-repeating-radial-gradient(#6789AB 0%, #012345 100%)',
'-o-repeating-radial-gradient',
],
[
'-ms-repeating-radial-gradient(#6789AB 0%, #012345 100%)',
'-ms-repeating-radial-gradient',
],
[
'-moz-repeating-radial-gradient(#6789AB 0%, #012345 100%)',
'-moz-repeating-radial-gradient',
],
]
const outputs = []
const expected = []
assertionMap.forEach(([value, expectedValue]) => {
outputs.push(getGradientType(value))
expected.push(expectedValue)
})
expect(outputs).toEqual(expected)
})
})
describe('getDegrees', () => {
it('should pick the correct degree from linear gradient values', () => {
const assertionMap = [
['linear-gradient(0deg, #6789AB 0%, #012345 100%)', 0],
['-webkit-linear-gradient(1deg, #6789AB 0%, #012345 100%)', 1],
['-o-linear-gradient(2deg, #6789AB 0%, #012345 100%)', 2],
['-ms-linear-gradient(3deg, #6789AB 0%, #012345 100%)', 3],
['-moz-linear-gradient(4deg, #6789AB 0%, #012345 100%)', 4],
['repeating-linear-gradient(5deg, #6789AB 0%, #012345 100%)', 5],
['-webkit-repeating-linear-gradient(6deg, #6789AB 0%, #012345 100%)', 6],
['-o-repeating-linear-gradient(7deg, #6789AB 0%, #012345 100%)', 7],
['-ms-repeating-linear-gradient(8deg, #6789AB 0%, #012345 100%)', 8],
['-moz-repeating-linear-gradient(9deg, #6789AB 0%, #012345 100%)', 9],
]
const outputs = []
const expected = []
assertionMap.forEach(([value, expectedValue]) => {
outputs.push(getDegrees(value))
expected.push(expectedValue)
})
expect(outputs).toEqual(expected)
})
})
================================================
FILE: tsconfig.build.json
================================================
{
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": false,
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"],
"exclude": ["src/**/*.spec.ts", "src/**/*.spec.tsx"]
}
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"allowJs": true,
"declaration": true,
"esModuleInterop": true,
"isolatedModules": true,
"jsx": "react",
"module": "nodenext",
"noEmit": true,
"noUncheckedIndexedAccess": true,
"outDir": "dist",
"skipLibCheck": true,
"strict": true,
"target": "es5",
"lib": ["es2015", "dom"],
"downlevelIteration": false,
},
"exclude": ["dist"]
}
gitextract__r_hbhn9/ ├── .github/ │ └── workflows/ │ ├── pull-request.yml │ └── release-package.yml ├── .gitignore ├── .npmignore ├── .nvmrc ├── .prettierrc ├── LICENSE ├── README.md ├── biome.json ├── package.json ├── src/ │ ├── components/ │ │ ├── AdvancedControls.tsx │ │ ├── ComparibleColors.tsx │ │ ├── Controls.tsx │ │ ├── EyeDropper.tsx │ │ ├── GradientBar.tsx │ │ ├── GradientControls.tsx │ │ ├── Hue.tsx │ │ ├── Inputs.tsx │ │ ├── Opacity.tsx │ │ ├── Picker.tsx │ │ ├── Portal.tsx │ │ ├── Presets.tsx │ │ ├── Square.tsx │ │ ├── icon.tsx │ │ └── index.tsx │ ├── constants.ts │ ├── context.tsx │ ├── hooks/ │ │ ├── useColorPicker.ts │ │ ├── usePaintHue.ts │ │ └── usePaintSquare.ts │ ├── index.ts │ ├── shared/ │ │ └── types.ts │ ├── styles/ │ │ ├── darkStyles.ts │ │ └── styles.ts │ └── utils/ │ ├── converters.ts │ ├── formatters.ts │ ├── gradientParser.ts │ └── utils.ts ├── test/ │ ├── gradientParser.spec.js │ └── utils.spec.js ├── tsconfig.build.json └── tsconfig.json
SYMBOL INDEX (55 symbols across 9 files)
FILE: src/components/GradientBar.tsx
function force90degLinear (line 110) | function force90degLinear(color: string) {
FILE: src/components/Picker.tsx
type PickerProps (line 61) | type PickerProps = {
FILE: src/components/icon.tsx
type ColorProps (line 4) | type ColorProps = {
FILE: src/components/index.tsx
function ColorPicker (line 10) | function ColorPicker({
FILE: src/context.tsx
function PickerContextWrapper (line 15) | function PickerContextWrapper({
function usePicker (line 135) | function usePicker() {
type PCWProps (line 145) | type PCWProps = {
type PickerContextProps (line 159) | type PickerContextProps = {
FILE: src/shared/types.ts
type ColorPickerProps (line 1) | type ColorPickerProps = {
type ColorsProps (line 32) | type ColorsProps = {
type GradientProps (line 38) | type GradientProps = {
type LocalesProps (line 44) | type LocalesProps = {
type controlsProps (line 48) | type controlsProps = {
type ThemeProps (line 53) | type ThemeProps = {
type ThemeMode (line 58) | type ThemeMode = {
type Styles (line 65) | type Styles = Partial<{
type PassedConfig (line 97) | type PassedConfig = {
type Config (line 104) | type Config = {
FILE: src/utils/converters.ts
function rgb2cmyk (line 1) | function rgb2cmyk(r: number, g: number, b: number) {
FILE: src/utils/gradientParser.ts
function error (line 35) | function error(msg: any) {
function consume (line 41) | function consume(size: any) {
function scan (line 45) | function scan(regexp: any) {
function matchListing (line 59) | function matchListing(matcher: any) {
function match (line 78) | function match(type: any, pattern: any, captureIndex: any) {
function matchHexColor (line 88) | function matchHexColor() {
function matchCall (line 105) | function matchCall(pattern: any, callback: any) {
function matchHSLColor (line 123) | function matchHSLColor() {
function matchRGBAColor (line 127) | function matchRGBAColor() {
function matchRGBColor (line 131) | function matchRGBColor() {
function matchLiteralColor (line 135) | function matchLiteralColor() {
function matchHSVColor (line 145) | function matchHSVColor() {
function matchColor (line 149) | function matchColor() {
function matchColorStop (line 160) | function matchColorStop() {
function matchGradient (line 171) | function matchGradient(
function matchLinearOrientation (line 192) | function matchLinearOrientation() {
function matchDefinition (line 196) | function matchDefinition() {
function matchListDefinitions (line 221) | function matchListDefinitions() {
function getAST (line 225) | function getAST() {
function matchSideOrCorner (line 255) | function matchSideOrCorner() {
function matchAngle (line 259) | function matchAngle() {
function matchListRadialOrientations (line 263) | function matchListRadialOrientations() {
function matchRadialOrientation (line 286) | function matchRadialOrientation() {
function matchLength (line 316) | function matchLength() {
function matchCircle (line 320) | function matchCircle() {
function matchEllipse (line 331) | function matchEllipse() {
function matchExtentKeyword (line 342) | function matchExtentKeyword() {
function matchAtPosition (line 346) | function matchAtPosition() {
function matchPositioning (line 358) | function matchPositioning() {
function matchCoordinates (line 369) | function matchCoordinates() {
function matchNumber (line 376) | function matchNumber() {
function matchDistance (line 417) | function matchDistance() {
function matchPositionKeyword (line 425) | function matchPositionKeyword() {
FILE: src/utils/utils.ts
function getHandleValue (line 16) | function getHandleValue(e: any, barSize: number) {
function computeSquareXY (line 24) | function computeSquareXY(
function computePickerPosition (line 45) | function computePickerPosition(e: any, crossSize: number) {
Condensed preview — 42 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (162K chars).
[
{
"path": ".github/workflows/pull-request.yml",
"chars": 523,
"preview": "name: Run tests\n\non: pull_request\n\njobs:\n run-tests:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checko"
},
{
"path": ".github/workflows/release-package.yml",
"chars": 1017,
"preview": "name: Release Package\n\non:\n release:\n types: [created]\n\njobs:\n run-tests:\n runs-on: ubuntu-latest\n steps:\n "
},
{
"path": ".gitignore",
"chars": 389,
"preview": "# OS\n.DS_Store\n\n# Cache\n.cache\n.playwright\n.tmp\n*.tsbuildinfo\n.eslintcache\n\n# Yarn\n.pnp.*\n**/.yarn/*\n!**/.yarn/patches\n!"
},
{
"path": ".npmignore",
"chars": 153,
"preview": "/*.log\r\n/*.DS_Store\r\n/.idea\r\n/.gitignore\r\n/node_modules/\r\n/.github/\r\n/src/\r\n/babel.config.js\r\n**/*.spec.js\r\n/.eslintigno"
},
{
"path": ".nvmrc",
"chars": 4,
"preview": "v16\n"
},
{
"path": ".prettierrc",
"chars": 112,
"preview": "{\r\n\t\"endOfLine\": \"lf\",\r\n\t\"semi\": false,\r\n\t\"singleQuote\": true,\r\n\t\"useTabs\": false,\r\n\t\"trailingComma\": \"es5\"\r\n}\r\n"
},
{
"path": "LICENSE",
"chars": 1066,
"preview": "MIT License\n\nCopyright (c) 2022 Harry Fox\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\n"
},
{
"path": "README.md",
"chars": 21601,
"preview": "[![Npm Version][npm-version-image]][npm-version-url]\n[![Downloads][downloads-image]][downloads-url]\n[![License][license-"
},
{
"path": "biome.json",
"chars": 1002,
"preview": "{\n \"$schema\": \"https://biomejs.dev/schemas/1.8.3/schema.json\",\n \"files\": {\n \"ignore\": [\n \".tsimp\",\n \".yar"
},
{
"path": "package.json",
"chars": 2718,
"preview": "{\n \"name\": \"react-best-gradient-color-picker\",\n \"version\": \"3.0.14\",\n \"description\": \"An easy to use color/gradient p"
},
{
"path": "src/components/AdvancedControls.tsx",
"chars": 5965,
"preview": "import React, { useState, useRef, useEffect } from 'react'\nimport { Styles, Config } from '../shared/types.js'\nimport { "
},
{
"path": "src/components/ComparibleColors.tsx",
"chars": 5672,
"preview": "import React from 'react'\nimport { usePicker } from '../context.js'\n\nconst ComparibleColors = ({\n openComparibles,\n}: {"
},
{
"path": "src/components/Controls.tsx",
"chars": 8648,
"preview": "/* eslint-disable react/jsx-no-leaked-render */\n/* eslint-disable jsx-a11y/no-static-element-interactions */\nimport Reac"
},
{
"path": "src/components/EyeDropper.tsx",
"chars": 4331,
"preview": "/* eslint-disable react/jsx-no-leaked-render */\n/* eslint-disable jsx-a11y/no-static-element-interactions */\nimport Reac"
},
{
"path": "src/components/GradientBar.tsx",
"chars": 4973,
"preview": "/* eslint-disable react/no-array-index-key */\n/* eslint-disable react/jsx-no-leaked-render */\n/* eslint-disable jsx-a11y"
},
{
"path": "src/components/GradientControls.tsx",
"chars": 5847,
"preview": "import React from 'react'\nimport { usePicker } from '../context.js'\nimport { formatInputValues, low, high } from '../uti"
},
{
"path": "src/components/Hue.tsx",
"chars": 2606,
"preview": "import React, { useRef, useState, useEffect } from 'react'\nimport { usePicker } from '../context.js'\nimport usePaintHue "
},
{
"path": "src/components/Inputs.tsx",
"chars": 10643,
"preview": "import React, { useState, useEffect } from 'react'\nimport { Styles } from '../shared/types.js'\nimport { formatInputValue"
},
{
"path": "src/components/Opacity.tsx",
"chars": 2300,
"preview": "/* eslint-disable jsx-a11y/no-static-element-interactions */\nimport React, { useState, useEffect } from 'react'\nimport {"
},
{
"path": "src/components/Picker.tsx",
"chars": 2091,
"preview": "import React from 'react'\nimport Hue from './Hue.js'\nimport Inputs from './Inputs.js'\nimport Square from './Square.js'\ni"
},
{
"path": "src/components/Portal.tsx",
"chars": 770,
"preview": "import { memo, useEffect, useRef, useState, ReactNode } from 'react'\nimport { createPortal } from 'react-dom'\n\nconst Por"
},
{
"path": "src/components/Presets.tsx",
"chars": 2470,
"preview": "/* eslint-disable react/no-array-index-key */\n/* eslint-disable jsx-a11y/no-static-element-interactions */\nimport React "
},
{
"path": "src/components/Square.tsx",
"chars": 3803,
"preview": "/* eslint-disable jsx-a11y/no-static-element-interactions */\nimport { computePickerPosition, computeSquareXY } from '../"
},
{
"path": "src/components/icon.tsx",
"chars": 10439,
"preview": "import React from 'react'\nimport { usePicker } from '../context.js'\n\ntype ColorProps = {\n color: string\n}\n\nconst TrashI"
},
{
"path": "src/components/index.tsx",
"chars": 2764,
"preview": "'use client'\nimport React from 'react'\nimport PickerContextWrapper from '../context.js'\nimport Picker from './Picker.js'"
},
{
"path": "src/constants.ts",
"chars": 554,
"preview": "export const defaultLocales = {\n CONTROLS: {\n SOLID: 'Solid',\n GRADIENT: 'Gradient',\n },\n}\n\nexport const fakePre"
},
{
"path": "src/context.tsx",
"chars": 5087,
"preview": "import React, {\n createContext,\n useContext,\n ReactNode,\n useEffect,\n useState,\n} from 'react'\nimport { GradientPro"
},
{
"path": "src/hooks/useColorPicker.ts",
"chars": 7998,
"preview": "import tc from 'tinycolor2'\nimport { useState, useEffect } from 'react'\nimport { rgb2cmyk } from '../utils/converters.js"
},
{
"path": "src/hooks/usePaintHue.ts",
"chars": 2339,
"preview": "import { useEffect, RefObject } from 'react'\nimport tinycolor from 'tinycolor2'\n\nconst usePaintHue = (\n canvas: RefObje"
},
{
"path": "src/hooks/usePaintSquare.ts",
"chars": 1102,
"preview": "import { useEffect, RefObject } from 'react'\n\nconst usePaintSquare = (\n canvas: RefObject<HTMLCanvasElement>,\n hue: nu"
},
{
"path": "src/index.ts",
"chars": 275,
"preview": "import { ColorPicker } from './components/index.js'\nexport { useColorPicker } from './hooks/useColorPicker.js'\n\nexport t"
},
{
"path": "src/shared/types.ts",
"chars": 2657,
"preview": "export type ColorPickerProps = {\n idSuffix?: string\n value?: string\n onChange: (value: string) => void\n hideControls"
},
{
"path": "src/styles/darkStyles.ts",
"chars": 929,
"preview": "export const darkStyles: Record<string, Record<string, string | number>> = {\n body: {\n background: 'rgb(32, 32, "
},
{
"path": "src/styles/styles.ts",
"chars": 5919,
"preview": "import { darkStyles } from './darkStyles.js';\nimport { Styles } from '../shared/types.js';\n\nconst styles: Styles = {\n b"
},
{
"path": "src/utils/converters.ts",
"chars": 1590,
"preview": "export function rgb2cmyk(r: number, g: number, b: number) {\n let computedC = 0\n let computedM = 0\n let computedY = 0\n"
},
{
"path": "src/utils/formatters.ts",
"chars": 1012,
"preview": "import { ColorsProps } from '../shared/types.js'\nimport { gradientParser } from './gradientParser.js'\n\nexport const low "
},
{
"path": "src/utils/gradientParser.ts",
"chars": 10183,
"preview": "import { high, low } from './formatters.js'\nimport { isUpperCase } from './utils.js'\nimport tinycolor from 'tinycolor2'\n"
},
{
"path": "src/utils/utils.ts",
"chars": 4351,
"preview": "import { formatInputValues } from './formatters.js'\nimport { ColorsProps } from '../shared/types.js'\n\nexport const safeB"
},
{
"path": "test/gradientParser.spec.js",
"chars": 2209,
"preview": "import { gradientParser } from '../src/utils/gradientParser'\nimport { describe, expect, it } from 'vitest'\n\ndescribe('gr"
},
{
"path": "test/utils.spec.js",
"chars": 4622,
"preview": "import {\n isUpperCase,\n getNewHsl,\n getGradientType,\n getDegrees,\n} from '../src/utils/utils'\nimport { describe, exp"
},
{
"path": "tsconfig.build.json",
"chars": 205,
"preview": "{\n \"extends\": \"./tsconfig.json\",\n \"compilerOptions\": {\n \"noEmit\": false,\n \"outDir\": \"dist\",\n \"rootDir\": \"src\""
},
{
"path": "tsconfig.json",
"chars": 416,
"preview": "{\n \"compilerOptions\": {\n \"allowJs\": true,\n \"declaration\": true,\n \"esModuleInterop\": true,\n \"isolatedModules"
}
]
About this extraction
This page contains the full source code of the hxf31891/react-gradient-color-picker GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 42 files (149.8 KB), approximately 44.8k tokens, and a symbol index with 55 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.