diff --git a/packages/@react-stately/color/README.md b/packages/@react-stately/color/README.md new file mode 100644 index 00000000000..e93ca7420ae --- /dev/null +++ b/packages/@react-stately/color/README.md @@ -0,0 +1,3 @@ +# @react-stately/color + +This package is part of [react-spectrum](https://github.com/adobe/react-spectrum). See the repo for more details. diff --git a/packages/@react-stately/color/index.ts b/packages/@react-stately/color/index.ts new file mode 100644 index 00000000000..1210ae1e402 --- /dev/null +++ b/packages/@react-stately/color/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +export * from './src'; diff --git a/packages/@react-stately/color/package.json b/packages/@react-stately/color/package.json new file mode 100644 index 00000000000..7383c657606 --- /dev/null +++ b/packages/@react-stately/color/package.json @@ -0,0 +1,26 @@ +{ + "name": "@react-stately/color", + "version": "3.0.0-alpha.1", + "description": "Spectrum UI components in React", + "license": "Apache-2.0", + "private": true, + "main": "dist/main.js", + "module": "dist/module.js", + "types": "dist/types.d.ts", + "source": "src/index.ts", + "files": ["dist", "src"], + "sideEffects": false, + "repository": { + "type": "git", + "url": "https://github.com/adobe/react-spectrum" + }, + "dependencies": { + "@babel/runtime": "^7.6.2" + }, + "peerDependencies": { + "react": "^16.8.0" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/@react-stately/color/src/Color.ts b/packages/@react-stately/color/src/Color.ts new file mode 100644 index 00000000000..6c2fcac229c --- /dev/null +++ b/packages/@react-stately/color/src/Color.ts @@ -0,0 +1,203 @@ +/* + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +/** A list of supported color formats. */ +type ColorFormat = 'hex' | 'hexa' | 'rgb' | 'rgba' | 'hsl' | 'hsla' | 'hsb' | 'hsba'; + +/** A list of color channels. */ +type ColorChannel = 'hue' | 'saturation' | 'brightness' | 'lightness' | 'red' | 'green' | 'blue' | 'alpha'; + +export class Color { + private value: ColorValue; + + constructor(value: string) { + let parsed: ColorValue | void = RGBColor.parse(value); + if (parsed) { + this.value = parsed; + } else { + throw new Error('Invalid color value: ' + value); + } + } + + private static fromColorValue(value: ColorValue): Color { + let x: Color = Object.create(Color.prototype); + x.value = value; + return x; + } + + toFormat(format: ColorFormat): Color { + switch (format) { + case 'hex': + case 'hexa': + case 'rgb': + case 'rgba': + return Color.fromColorValue(this.value.toRGB()); + case 'hsl': + case 'hsla': + return Color.fromColorValue(this.value.toHSL()); + case 'hsb': + case 'hsba': + return Color.fromColorValue(this.value.toHSB()); + default: + throw new Error('Invalid color format: ' + format); + } + } + + toString(format: ColorFormat) { + switch (format) { + case 'hex': + case 'hexa': + case 'rgb': + case 'rgba': + return this.value.toRGB().toString(format); + case 'hsl': + case 'hsla': + return this.value.toHSL().toString(format); + case 'hsb': + case 'hsba': + return this.value.toHSB().toString(format); + default: + throw new Error('Invalid color format: ' + format); + } + } + + getChannelValue(channel: ColorChannel): number { + if (channel in this.value) { + return this.value[channel]; + } + + throw new Error('Unsupported color channel: ' + channel); + } +} + +interface ColorValue { + toRGB(): ColorValue, + toHSB(): ColorValue, + toHSL(): ColorValue, + toString(format: ColorFormat): string +} + +const HEX_REGEX = /^#(?:([0-9a-f]{3})|([0-9a-f]{6}))$/i; + +class RGBColor implements ColorValue { + constructor(private red: number, private green: number, private blue: number, private alpha: number) {} + + static parse(value: string): RGBColor | void { + let m; + if ((m = value.match(HEX_REGEX))) { + if (m[1]) { + let r = parseInt(m[1][0] + m[1][0], 16); + let g = parseInt(m[1][1] + m[1][1], 16); + let b = parseInt(m[1][2] + m[1][2], 16); + return new RGBColor(r, g, b, 1); + } else if (m[2]) { + let r = parseInt(m[2][0] + m[2][1], 16); + let g = parseInt(m[2][2] + m[2][3], 16); + let b = parseInt(m[2][4] + m[2][5], 16); + return new RGBColor(r, g, b, 1); + } + } else { + // TODO: check rgb and rgba strings + } + } + + toString(format: ColorFormat) { + switch (format) { + case 'hex': + return '#' + (1 << 24 | this.red << 16 | this.green << 8 | this.blue).toString(16).slice(1).toUpperCase(); + case 'rgb': + return `rgb(${this.red}, ${this.green}, ${this.blue})`; + case 'rgba': + return `rgba(${this.red}, ${this.green}, ${this.blue}, ${this.alpha})`; + default: + throw new Error('Unsupported color format: ' + format); + } + } + + toRGB(): ColorValue { + return this; + } + + toHSB(): ColorValue { + throw new Error('Not implemented'); + } + + toHSL(): ColorValue { + throw new Error('Not implemented'); + } +} + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +class HSBColor implements ColorValue { + constructor(private hue: number, private saturation: number, private brightness: number, private alpha: number) {} + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + static parse(value: string): HSBColor | void { + // TODO + } + + toString(format: ColorFormat) { + switch (format) { + case 'hsb': + return `hsb(${this.hue}, ${this.saturation}%, ${this.brightness}%)`; + case 'hsba': + return `hsba(${this.hue}, ${this.saturation}%, ${this.brightness}%, ${this.alpha})`; + default: + throw new Error('Unsupported color format: ' + format); + } + } + + toRGB(): ColorValue { + throw new Error('Not implemented'); + } + + toHSB(): ColorValue { + return this; + } + + toHSL(): ColorValue { + throw new Error('Not implemented'); + } +} + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +class HSLColor implements ColorValue { + constructor(private hue: number, private saturation: number, private lightness: number, private alpha: number) {} + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + static parse(value: string): HSLColor | void { + // TODO + } + + toString(format: ColorFormat) { + switch (format) { + case 'hsl': + return `hsl(${this.hue}, ${this.saturation}%, ${this.lightness}%)`; + case 'hsla': + return `hsla(${this.hue}, ${this.saturation}%, ${this.lightness}%, ${this.alpha})`; + default: + throw new Error('Unsupported color format: ' + format); + } + } + + toRGB(): ColorValue { + throw new Error('Not implemented'); + } + + toHSB(): ColorValue { + throw new Error('Not implemented'); + } + + toHSL(): ColorValue { + return this; + } +} diff --git a/packages/@react-stately/color/src/index.ts b/packages/@react-stately/color/src/index.ts new file mode 100644 index 00000000000..ea4b501621e --- /dev/null +++ b/packages/@react-stately/color/src/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +export * from './Color'; diff --git a/packages/@react-stately/color/test/Color.test.js b/packages/@react-stately/color/test/Color.test.js new file mode 100644 index 00000000000..4b4c23a028f --- /dev/null +++ b/packages/@react-stately/color/test/Color.test.js @@ -0,0 +1,43 @@ +/* + * Copyright 2020 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import {Color} from '../src/Color'; + +describe('Color', function () { + describe('hex', function () { + it('should parse a short hex color', function () { + let color = new Color('#abc'); + expect(color.getChannelValue('red')).toBe(170); + expect(color.getChannelValue('green')).toBe(187); + expect(color.getChannelValue('blue')).toBe(204); + expect(color.getChannelValue('alpha')).toBe(1); + expect(color.toString('hex')).toBe('#AABBCC'); + expect(color.toString('rgb')).toBe('rgb(170, 187, 204)'); + expect(color.toString('rgba')).toBe('rgba(170, 187, 204, 1)'); + }); + + it('should parse a long hex color', function () { + let color = new Color('#abcdef'); + expect(color.getChannelValue('red')).toBe(171); + expect(color.getChannelValue('green')).toBe(205); + expect(color.getChannelValue('blue')).toBe(239); + expect(color.getChannelValue('alpha')).toBe(1); + expect(color.toString('hex')).toBe('#ABCDEF'); + expect(color.toString('rgb')).toBe('rgb(171, 205, 239)'); + expect(color.toString('rgba')).toBe('rgba(171, 205, 239, 1)'); + }); + + it('should throw on invalid hex value', function () { + expect(() => new Color('#ggg')).toThrow('Invalid color value: #ggg'); + }); + }); +}); diff --git a/plop-templates/@react-spectrum/package.json.hbs b/plop-templates/@react-spectrum/package.json.hbs index 42c57366f5c..2a23c2ef233 100644 --- a/plop-templates/@react-spectrum/package.json.hbs +++ b/plop-templates/@react-spectrum/package.json.hbs @@ -8,7 +8,7 @@ "module": "dist/module.js", "types": "dist/types.d.ts", "source": "src/index.ts", - "files": ["dist"], + "files": ["dist", "src"], "sideEffects": [ "*.css" ], diff --git a/plop-templates/@react-stately/package.json.hbs b/plop-templates/@react-stately/package.json.hbs index dcf5999b976..d2d5fba50c6 100644 --- a/plop-templates/@react-stately/package.json.hbs +++ b/plop-templates/@react-stately/package.json.hbs @@ -8,7 +8,7 @@ "module": "dist/module.js", "types": "dist/types.d.ts", "source": "src/index.ts", - "files": ["dist"], + "files": ["dist", "src"], "sideEffects": false, "repository": { "type": "git",