UNPKG

react-color-palette

Version:

🎨 Lightweight Color Picker component for React.

525 lines (505 loc) • 20 kB
"use client" "use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/rcp.ts var rcp_exports = {}; __export(rcp_exports, { Alpha: () => Alpha, ColorPicker: () => ColorPicker, ColorService: () => ColorService, Hue: () => Hue, Saturation: () => Saturation, useColor: () => useColor }); module.exports = __toCommonJS(rcp_exports); // src/components/color-picker/color-picker.component.tsx var import_react7 = __toESM(require("react")); // src/utils/is-field-hide/is-field-hide.util.ts function isFieldHide(hideInput, field) { return Array.isArray(hideInput) ? hideInput.includes(field) : hideInput; } // src/components/alpha/alpha.component.tsx var import_react3 = __toESM(require("react")); // src/hooks/use-bounding-client-rect/use-bounding-client-rect.hook.ts var import_react = require("react"); var getElementDimensions = (element) => { const rect = element.getBoundingClientRect(); return { width: rect.width, height: rect.height }; }; function useBoundingClientRect() { const ref = (0, import_react.useRef)(null); const [size, setSize] = (0, import_react.useState)({ width: 1, height: 1 }); (0, import_react.useLayoutEffect)(() => { const onWindowResize = () => { if (!ref.current) return; setSize(getElementDimensions(ref.current)); }; const onElementResize = ([{ contentBoxSize }]) => { setSize({ height: contentBoxSize[0].blockSize, width: contentBoxSize[0].inlineSize }); }; window.addEventListener("resize", onWindowResize, false); const observer = new ResizeObserver(onElementResize); if (ref.current) observer.observe(ref.current); return () => { window.removeEventListener("resize", onWindowResize, false); observer.disconnect(); }; }, []); const getPosition = (0, import_react.useCallback)(() => { const { left = 1, right = 1, top = 1, bottom = 1 } = ref.current?.getBoundingClientRect() ?? {}; return { left, right, top, bottom }; }, []); return [ref, size, getPosition]; } // src/utils/clamp/clamp.util.ts function clamp(value, min, max) { return value < min ? min : value > max ? max : value; } // src/services/color/color.service.ts var ColorServiceStatic = class { convert(model, color) { let hex = this.toHex("#000000"); let rgb = this.hex2rgb(hex); let hsv = this.rgb2hsv(rgb); if (model === "hex") { const value = color; hex = this.toHex(value); rgb = this.hex2rgb(hex); if (hex.startsWith("rgba")) { rgb = this.toRgb(hex); hex = this.rgb2hex(rgb); } hsv = this.rgb2hsv(rgb); } else if (model === "rgb") { const value = color; rgb = value; hex = this.rgb2hex(rgb); hsv = this.rgb2hsv(rgb); } else if (model === "hsv") { const value = color; hsv = value; rgb = this.hsv2rgb(hsv); hex = this.rgb2hex(rgb); } return { hex, rgb, hsv }; } toHex(value) { if (!value.startsWith("#")) { const ctx = document.createElement("canvas").getContext("2d"); if (!ctx) throw new Error("2d context not supported or canvas already initialized"); ctx.fillStyle = value; return ctx.fillStyle; } else if (value.length === 4 || value.length === 5) { value = value.split("").map((v, i) => i ? i < 4 ? v + v : v === "f" ? void 0 : v + v : "#").join(""); return value; } else if (value.length === 7) { return value; } else if (value.length === 9) { return value.endsWith("ff") ? value.slice(0, 7) : value; } return "#000000"; } toRgb(value) { const rgb = value.match(/\d+(\.\d+)?/gu) ?? []; const [r, g, b, a] = Array.from({ length: 4 }).map( (_, i) => clamp(+(rgb[i] ?? (i < 3 ? 0 : 1)), 0, i < 3 ? 255 : 1) ); return { r, g, b, a }; } toHsv(value) { const hsv = value.match(/\d+(\.\d+)?/gu) ?? []; const [h, s, v, a] = Array.from({ length: 4 }).map( (_, i) => clamp(+(hsv[i] ?? (i < 3 ? 0 : 1)), 0, i ? i < 3 ? 100 : 1 : 360) ); return { h, s, v, a }; } hex2rgb(hex) { hex = hex.slice(1); let [r, g, b, a] = Array.from({ length: 4 }).map((_, i) => parseInt(hex.slice(i * 2, i * 2 + 2), 16)); a = Number.isNaN(a) ? 1 : a / 255; return { r, g, b, a }; } rgb2hsv({ r, g, b, a }) { r /= 255; g /= 255; b /= 255; const max = Math.max(r, g, b); const d = max - Math.min(r, g, b); const h = d ? (max === r ? (g - b) / d + (g < b ? 6 : 0) : max === g ? 2 + (b - r) / d : 4 + (r - g) / d) * 60 : 0; const s = max ? d / max * 100 : 0; const v = max * 100; return { h, s, v, a }; } hsv2rgb({ h, s, v, a }) { s /= 100; v /= 100; const i = ~~(h / 60); const f = h / 60 - i; const p = v * (1 - s); const q = v * (1 - s * f); const t = v * (1 - s * (1 - f)); const index = i % 6; const r = [v, q, p, p, t, v][index] * 255; const g = [t, v, v, q, p, p][index] * 255; const b = [p, p, t, v, v, q][index] * 255; return { r, g, b, a }; } rgb2hex({ r, g, b, a }) { const [rr, gg, bb, aa] = [r, g, b, a].map( (v, i) => Math.round(i < 3 ? v : v * 255).toString(16).padStart(2, "0") ); return ["#", rr, gg, bb, aa === "ff" ? "" : aa].join(""); } }; var ColorService = new ColorServiceStatic(); // src/components/interactive/interactive.component.tsx var import_react2 = __toESM(require("react")); // src/utils/is-touch/is-touch.util.ts function isTouch(event) { return "touches" in event; } // src/components/interactive/interactive.component.tsx var Interactive = (0, import_react2.memo)(({ onCoordinateChange, children, disabled }) => { const [interactiveRef, { width, height }, getPosition] = useBoundingClientRect(); const move = (0, import_react2.useCallback)( (event, final = false) => { const { left, top } = getPosition(); const x = clamp(event.clientX - left, 0, width); const y = clamp(event.clientY - top, 0, height); onCoordinateChange(final, x, y); }, [width, height, getPosition, onCoordinateChange] ); const onStart = (0, import_react2.useCallback)( (event) => { if (!isTouch(event) && event.button !== 0) return; const onMove = (event2) => { move(isTouch(event2) ? event2.touches[0] : event2); }; const onEnd = (event2) => { move(isTouch(event2) ? event2.changedTouches[0] : event2, true); document.removeEventListener(isTouch(event2) ? "touchmove" : "mousemove", onMove, false); document.removeEventListener(isTouch(event2) ? "touchend" : "mouseup", onEnd, false); }; onMove(event); document.addEventListener(isTouch(event) ? "touchmove" : "mousemove", onMove, false); document.addEventListener(isTouch(event) ? "touchend" : "mouseup", onEnd, false); }, [move] ); return /* @__PURE__ */ import_react2.default.createElement( "div", { ref: interactiveRef, className: "rcp-interactive", onMouseDown: onStart, onTouchStart: onStart, "aria-disabled": disabled }, children ); }); // src/components/alpha/alpha.component.tsx var Alpha = (0, import_react3.memo)(({ color, disabled, onChange, onChangeComplete }) => { const [alphaRef, { width }] = useBoundingClientRect(); const position = (0, import_react3.useMemo)(() => { const x = color.hsv.a * width; return { x }; }, [color.hsv.a, width]); const updateColor = (0, import_react3.useCallback)( (final, x) => { const nextColor = ColorService.convert("hsv", { ...color.hsv, a: x / width }); onChange(nextColor); if (final) onChangeComplete?.(nextColor); }, [color.hsv, width, onChange, onChangeComplete] ); const rgb = (0, import_react3.useMemo)(() => [color.rgb.r, color.rgb.g, color.rgb.b].join(" "), [color.rgb.r, color.rgb.g, color.rgb.b]); const rgba = (0, import_react3.useMemo)(() => [rgb, color.rgb.a].join(" / "), [rgb, color.rgb.a]); return /* @__PURE__ */ import_react3.default.createElement(Interactive, { disabled, onCoordinateChange: updateColor }, /* @__PURE__ */ import_react3.default.createElement( "div", { ref: alphaRef, style: { background: `linear-gradient(to right, rgb(${rgb} / 0), rgb(${rgb} / 1)) top left / auto auto, conic-gradient(#666 0.25turn, #999 0.25turn 0.5turn, #666 0.5turn 0.75turn, #999 0.75turn) top left / 12px 12px repeat` }, className: "rcp-alpha" }, /* @__PURE__ */ import_react3.default.createElement( "div", { style: { left: position.x, background: `linear-gradient(to right, rgb(${rgba}), rgb(${rgba})) top left / auto auto, conic-gradient(#666 0.25turn, #999 0.25turn 0.5turn, #666 0.5turn 0.75turn, #999 0.75turn) ${-position.x - 4}px 2px / 12px 12px repeat` }, className: "rcp-alpha-cursor" } ) )); }); // src/components/fields/fields.component.tsx var import_react4 = __toESM(require("react")); // src/utils/float/float.util.ts function float(value, decimalPlaces) { return Math.round(value * 10 ** decimalPlaces) / 10 ** decimalPlaces; } // src/utils/format/format.util.ts function formatRgb({ r, g, b, a }) { const rgb = [Math.round(r), Math.round(g), Math.round(b)]; const alpha = float(a, 3); if (alpha < 1) rgb.push(alpha); return rgb.join(", "); } function formatHsv({ h, s, v, a }) { const hsv = [`${Math.round(h)}\xB0`, `${Math.round(s)}%`, `${Math.round(v)}%`]; const alpha = float(a, 3); if (alpha < 1) hsv.push(alpha); return hsv.join(", "); } // src/components/fields/fields.component.tsx var Fields = (0, import_react4.memo)(({ hideInput, color, disabled, onChange, onChangeComplete }) => { const [fields, setFields] = (0, import_react4.useState)({ hex: { value: color.hex, inputted: false }, rgb: { value: formatRgb(color.rgb), inputted: false }, hsv: { value: formatHsv(color.hsv), inputted: false } }); (0, import_react4.useEffect)(() => { if (!fields.hex.inputted) { setFields((fields2) => ({ ...fields2, hex: { ...fields2.hex, value: color.hex } })); } }, [fields.hex.inputted, color.hex]); (0, import_react4.useEffect)(() => { if (!fields.rgb.inputted) { setFields((fields2) => ({ ...fields2, rgb: { ...fields2.rgb, value: formatRgb(color.rgb) } })); } }, [fields.rgb.inputted, color.rgb]); (0, import_react4.useEffect)(() => { if (!fields.hsv.inputted) { setFields((fields2) => ({ ...fields2, hsv: { ...fields2.hsv, value: formatHsv(color.hsv) } })); } }, [fields.hsv.inputted, color.hsv]); const onInputChange = (0, import_react4.useCallback)( (field) => (event) => { const { value } = event.target; setFields((fields2) => ({ ...fields2, [field]: { ...fields2[field], value } })); if (field === "hsv") onChange(ColorService.convert("hsv", ColorService.toHsv(value))); else if (field === "rgb") onChange(ColorService.convert("rgb", ColorService.toRgb(value))); else onChange(ColorService.convert("hex", value)); }, [onChange] ); const onInputFocus = (0, import_react4.useCallback)( (field) => () => { setFields((fields2) => ({ ...fields2, [field]: { ...fields2[field], inputted: true } })); }, [] ); const onInputBlur = (0, import_react4.useCallback)( (field) => (event) => { const { value } = event.target; setFields((fields2) => ({ ...fields2, [field]: { ...fields2[field], inputted: false } })); if (field === "hsv") onChangeComplete?.(ColorService.convert("hsv", ColorService.toHsv(value))); else if (field === "rgb") onChangeComplete?.(ColorService.convert("rgb", ColorService.toRgb(value))); else onChangeComplete?.(ColorService.convert("hex", value)); }, [onChangeComplete] ); return /* @__PURE__ */ import_react4.default.createElement("div", { className: "rcp-fields" }, !isFieldHide(hideInput, "hex") && /* @__PURE__ */ import_react4.default.createElement("div", { className: "rcp-fields-floor" }, /* @__PURE__ */ import_react4.default.createElement("div", { className: "rcp-field" }, /* @__PURE__ */ import_react4.default.createElement( "input", { id: "hex", className: "rcp-field-input", readOnly: disabled, value: fields.hex.value, onChange: onInputChange("hex"), onFocus: onInputFocus("hex"), onBlur: onInputBlur("hex") } ), /* @__PURE__ */ import_react4.default.createElement("label", { htmlFor: "hex", className: "rcp-field-label" }, "HEX"))), (!isFieldHide(hideInput, "rgb") || !isFieldHide(hideInput, "hsv")) && /* @__PURE__ */ import_react4.default.createElement("div", { className: "rcp-fields-floor" }, !isFieldHide(hideInput, "rgb") && /* @__PURE__ */ import_react4.default.createElement("div", { className: "rcp-field" }, /* @__PURE__ */ import_react4.default.createElement( "input", { id: "rgb", className: "rcp-field-input", readOnly: disabled, value: fields.rgb.value, onChange: onInputChange("rgb"), onFocus: onInputFocus("rgb"), onBlur: onInputBlur("rgb") } ), /* @__PURE__ */ import_react4.default.createElement("label", { htmlFor: "rgb", className: "rcp-field-label" }, "RGB")), !isFieldHide(hideInput, "hsv") && /* @__PURE__ */ import_react4.default.createElement("div", { className: "rcp-field" }, /* @__PURE__ */ import_react4.default.createElement( "input", { id: "hsv", className: "rcp-field-input", readOnly: disabled, value: fields.hsv.value, onChange: onInputChange("hsv"), onFocus: onInputFocus("hsv"), onBlur: onInputBlur("hsv") } ), /* @__PURE__ */ import_react4.default.createElement("label", { htmlFor: "hsv", className: "rcp-field-label" }, "HSV")))); }); // src/components/hue/hue.component.tsx var import_react5 = __toESM(require("react")); var Hue = (0, import_react5.memo)(({ color, disabled, onChange, onChangeComplete }) => { const [hueRef, { width }] = useBoundingClientRect(); const position = (0, import_react5.useMemo)(() => { const x = color.hsv.h / 360 * width; return { x }; }, [color.hsv.h, width]); const updateColor = (0, import_react5.useCallback)( (final, x) => { const nextColor = ColorService.convert("hsv", { ...color.hsv, h: x / width * 360 }); onChange(nextColor); if (final) onChangeComplete?.(nextColor); }, [color.hsv, width, onChange, onChangeComplete] ); const hsl = (0, import_react5.useMemo)(() => [color.hsv.h, "100%", "50%"].join(" "), [color.hsv.h]); return /* @__PURE__ */ import_react5.default.createElement(Interactive, { disabled, onCoordinateChange: updateColor }, /* @__PURE__ */ import_react5.default.createElement("div", { ref: hueRef, className: "rcp-hue" }, /* @__PURE__ */ import_react5.default.createElement("div", { style: { left: position.x, backgroundColor: `hsl(${hsl})` }, className: "rcp-hue-cursor" }))); }); // src/components/saturation/saturation.component.tsx var import_react6 = __toESM(require("react")); var Saturation = (0, import_react6.memo)(({ height, color, disabled, onChange, onChangeComplete }) => { const [saturationRef, { width }] = useBoundingClientRect(); const position = (0, import_react6.useMemo)(() => { const x = color.hsv.s / 100 * width; const y = (100 - color.hsv.v) / 100 * height; return { x, y }; }, [color.hsv.s, color.hsv.v, width, height]); const updateColor = (0, import_react6.useCallback)( (final, x, y) => { const nextColor = ColorService.convert("hsv", { ...color.hsv, s: x / width * 100, v: 100 - y / height * 100 }); onChange(nextColor); if (final) onChangeComplete?.(nextColor); }, [color.hsv, width, height, onChange, onChangeComplete] ); const hsl = (0, import_react6.useMemo)(() => [color.hsv.h, "100%", "50%"].join(" "), [color.hsv.h]); const rgb = (0, import_react6.useMemo)(() => [color.rgb.r, color.rgb.g, color.rgb.b].join(" "), [color.rgb.r, color.rgb.g, color.rgb.b]); return /* @__PURE__ */ import_react6.default.createElement(Interactive, { disabled, onCoordinateChange: updateColor }, /* @__PURE__ */ import_react6.default.createElement("div", { ref: saturationRef, style: { height, backgroundColor: `hsl(${hsl})` }, className: "rcp-saturation" }, /* @__PURE__ */ import_react6.default.createElement( "div", { style: { left: position.x, top: position.y, backgroundColor: `rgb(${rgb})` }, className: "rcp-saturation-cursor" } ))); }); // src/components/color-picker/color-picker.component.tsx var ColorPicker = (0, import_react7.memo)( ({ height = 200, hideAlpha = false, hideInput = false, color, disabled = false, onChange, onChangeComplete }) => /* @__PURE__ */ import_react7.default.createElement("div", { className: "rcp-root rcp" }, /* @__PURE__ */ import_react7.default.createElement( Saturation, { height, color, disabled, onChange, onChangeComplete } ), /* @__PURE__ */ import_react7.default.createElement("div", { className: "rcp-body" }, /* @__PURE__ */ import_react7.default.createElement("section", { className: "rcp-section" }, /* @__PURE__ */ import_react7.default.createElement(Hue, { color, disabled, onChange, onChangeComplete }), !hideAlpha && /* @__PURE__ */ import_react7.default.createElement(Alpha, { color, disabled, onChange, onChangeComplete })), (!isFieldHide(hideInput, "hex") || !isFieldHide(hideInput, "rgb") || !isFieldHide(hideInput, "hsv")) && /* @__PURE__ */ import_react7.default.createElement("section", { className: "rcp-section" }, /* @__PURE__ */ import_react7.default.createElement( Fields, { hideInput, color, disabled, onChange, onChangeComplete } )))) ); // src/hooks/use-color/use-color.hook.ts var import_react8 = require("react"); function useColor(initialColor) { const [color, setColor] = (0, import_react8.useState)(ColorService.convert("hex", initialColor)); (0, import_react8.useEffect)(() => { setColor(ColorService.convert("hex", initialColor)); }, [initialColor]); return [color, setColor]; } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { Alpha, ColorPicker, ColorService, Hue, Saturation, useColor });