From 0d08d805a3df4e861ddc646921c32333bb635043 Mon Sep 17 00:00:00 2001 From: Jay Date: Fri, 8 Aug 2025 11:17:21 -0400 Subject: [PATCH] Wrote space editor component and test. --- package-lock.json | 13 ++ package.json | 7 +- src/components/ColorValues/SpaceEditor.cy.tsx | 106 ++++++++++++ .../ColorValues/SpaceEditor.module.css | 6 + src/components/ColorValues/SpaceEditor.tsx | 162 +++++++++++++++++- .../ColorValues/ValueEditor.module.css | 3 +- src/components/ColorValues/ValueEditor.tsx | 13 +- .../ColorValues/ValueEditorTest.cy.tsx | 11 +- src/util.ts | 5 + 9 files changed, 310 insertions(+), 16 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1187d1e..035c882 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0", "dependencies": { "@fortawesome/fontawesome-svg-core": "^7.0.0", + "@fortawesome/free-regular-svg-icons": "^7.0.0", "@fortawesome/free-solid-svg-icons": "^7.0.0", "@fortawesome/react-fontawesome": "^0.2.3", "motion": "^12.23.12", @@ -1145,6 +1146,18 @@ "node": ">=6" } }, + "node_modules/@fortawesome/free-regular-svg-icons": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-7.0.0.tgz", + "integrity": "sha512-qAh0mTaCY22sQzMK2lKBrtn/aR4keUu5XmtdYR7d702laMe0h+Ab4Kj2pExR9HZkKhjKoq8pbwt8Td+mjW/ipQ==", + "license": "(CC-BY-4.0 AND MIT)", + "dependencies": { + "@fortawesome/fontawesome-common-types": "7.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@fortawesome/free-solid-svg-icons": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-7.0.0.tgz", diff --git a/package.json b/package.json index d3bafcc..f009d34 100644 --- a/package.json +++ b/package.json @@ -7,17 +7,18 @@ "build:wasm": "wasm-pack build colorlib -t bundler -d pkg --release", "clean": "rm -rf dist colorlib/pkg*", "cypress:open": "1>/dev/null 2>/dev/null cypress open -d &", - "cypress:component": "cypress run --component", - "cypress:e2e": "cypress run --e2e", "dev": "vite", "lint": "eslint .", "preview": "vite preview", "test:wasm": "cargo test --lib --manifest-path colorlib/Cargo.toml", "test:wasmdoc": "cargo test --doc --manifest-path colorlib/Cargo.toml", - "test": "vitest" + "test": "vitest", + "test:component:chrome": "cypress run --component -b chromium", + "test:component:fire": "cypress run --component -b firefox" }, "dependencies": { "@fortawesome/fontawesome-svg-core": "^7.0.0", + "@fortawesome/free-regular-svg-icons": "^7.0.0", "@fortawesome/free-solid-svg-icons": "^7.0.0", "@fortawesome/react-fontawesome": "^0.2.3", "motion": "^12.23.12", diff --git a/src/components/ColorValues/SpaceEditor.cy.tsx b/src/components/ColorValues/SpaceEditor.cy.tsx index e69de29..c66875e 100644 --- a/src/components/ColorValues/SpaceEditor.cy.tsx +++ b/src/components/ColorValues/SpaceEditor.cy.tsx @@ -0,0 +1,106 @@ +import { useReducer } from "react"; + +import { Color } from "colorlib"; + +import { roundTo } from "@/util"; +import { colorReducer, createColorActions } from "@hooks/color"; + +import SpaceEditor from "./SpaceEditor"; + +const initialState = { + color: Color.from_hex("2edd9d"), +}; + +function TestWrapper() { + const [state, dispatch] = useReducer(colorReducer, initialState); + const actions = createColorActions(dispatch); + + return ( + <> +
+ + + +
+ +
+

+ HCL ({roundTo(state.color.hcl.h, 0)}, {roundTo(state.color.hcl.c, 2)},{" "} + {roundTo(state.color.hcl.l, 2)}) +

+

+ HSV ({roundTo(state.color.hsv.h, 0)}, {roundTo(state.color.hsv.s, 2)},{" "} + {roundTo(state.color.hsv.v, 2)}) +

+

+ RGB ({roundTo(state.color.rgb.r, 0)}, {roundTo(state.color.rgb.g, 0)},{" "} + {roundTo(state.color.rgb.b, 0)}) +

+

HEX: #{state.color.hex.to_code()}

+
+ + ); +} + +describe("space editor tests", () => { + it("can edit color values", () => { + cy.mount(); + + // Confirm initial values + cy.dataCy("RGB-editor").within(() => { + cy.dataCy("R-value-input").should("have.value", 46); + cy.dataCy("G-value-input").should("have.value", 221); + cy.dataCy("B-value-input").should("have.value", 157); + }); + + cy.dataCy("HSV-editor").within(() => { + cy.dataCy("H-value-input").should("have.value", 158); + cy.dataCy("S-value-input").should("have.value", 79); + cy.dataCy("V-value-input").should("have.value", 87); + }); + + cy.dataCy("HCL-editor").within(() => { + cy.dataCy("H-value-input").should("have.value", 158); + cy.dataCy("C-value-input").should("have.value", 79); + cy.dataCy("L-value-input").should("have.value", 70); + }); + + cy.dataCy("rgb-value").should("have.text", "RGB (46, 221, 157)"); + cy.dataCy("hsv-value").should("have.text", "HSV (158, 0.79, 0.87)"); + cy.dataCy("hcl-value").should("have.text", "HCL (158, 0.79, 0.7)"); + cy.dataCy("hex-value").should("have.text", "HEX: #2EDD9D"); + + // Update the color values + cy.wait(50); // ensure render + cy.dataCy("HCL-editor").within(() => { + cy.dataCy("H-slider").click(); + cy.dataCy("C-decrement-button").click(); + cy.dataCy("L-value-input").type("25"); + }); + + cy.dataCy("rgb-value").should("have.text", "RGB (17, 76, 74)"); + cy.dataCy("hsv-value").should("have.text", "HSV (179, 0.78, 0.3)"); + cy.dataCy("hcl-value").should("have.text", "HCL (179, 0.78, 0.25)"); + cy.dataCy("hex-value").should("have.text", "HEX: #104B4A"); + }); +}); diff --git a/src/components/ColorValues/SpaceEditor.module.css b/src/components/ColorValues/SpaceEditor.module.css index e69de29..616096e 100644 --- a/src/components/ColorValues/SpaceEditor.module.css +++ b/src/components/ColorValues/SpaceEditor.module.css @@ -0,0 +1,6 @@ +.spaceWrapper { + display: flex; + flex-direction: column; + flex: 1; + min-height: 0; +} diff --git a/src/components/ColorValues/SpaceEditor.tsx b/src/components/ColorValues/SpaceEditor.tsx index 7b0940c..500baaf 100644 --- a/src/components/ColorValues/SpaceEditor.tsx +++ b/src/components/ColorValues/SpaceEditor.tsx @@ -1,9 +1,165 @@ import * as colorlib from "colorlib"; -import styles from "./SpaceEditor.module.css"; +import type { + HCLColorActions, + HSVColorActions, + RGBColorActions, +} from "@hooks/color"; -function SpaceEditor({}: {}) { - return; +import styles from "./SpaceEditor.module.css"; +import ValueEditor from "./ValueEditor"; + +type SpaceEditorProps = + | { space: "RGB"; color: colorlib.RGB; actions: RGBColorActions } + | { space: "HSV"; color: colorlib.HSV; actions: HSVColorActions } + | { space: "HCL"; color: colorlib.HCL; actions: HCLColorActions }; + +function SpaceEditor({ space, color, actions }: SpaceEditorProps) { + switch (space) { + case "RGB": + return ; + + case "HSV": + return ; + + case "HCL": + return ; + + default: + return <>; + } +} + +const COLOR_SPACES = { + RGB: { + symbols: { r: "R", g: "G", b: "B" }, + ranges: { + r: { min: 0, max: 255 }, + g: { min: 0, max: 255 }, + b: { min: 0, max: 255 }, + }, + }, + HSV: { + symbols: { h: "H", s: "S", v: "V" }, + ranges: { + h: { min: 0, max: 359 }, + s: { min: 0, max: 1 }, + v: { min: 0, max: 1 }, + }, + scales: { + s: 100, + v: 100, + }, + }, + HCL: { + symbols: { h: "H", c: "C", l: "L" }, + ranges: { + h: { min: 0, max: 359 }, + c: { min: 0, max: 1 }, + l: { min: 0, max: 1 }, + }, + scales: { + c: 100, + l: 100, + }, + }, +}; + +function RGBSpaceEditor({ + color, + actions, +}: { + color: colorlib.RGB; + actions: RGBColorActions; +}) { + return ( +
+ + + +
+ ); +} + +function HSVSpaceEditor({ + color, + actions, +}: { + color: colorlib.HSV; + actions: HSVColorActions; +}) { + return ( +
+ + + +
+ ); +} + +function HCLSpaceEditor({ + color, + actions, +}: { + color: colorlib.HCL; + actions: HCLColorActions; +}) { + return ( +
+ + + +
+ ); } export default SpaceEditor; diff --git a/src/components/ColorValues/ValueEditor.module.css b/src/components/ColorValues/ValueEditor.module.css index a1d35d5..abcb415 100644 --- a/src/components/ColorValues/ValueEditor.module.css +++ b/src/components/ColorValues/ValueEditor.module.css @@ -2,7 +2,7 @@ display: flex; align-items: stretch; width: 100%; - height: var(--height, 25px); + min-height: 0; font-family: monospace; border: 1px solid black; border-top: none; @@ -84,5 +84,4 @@ font-family: monospace; font-size: 14px; text-align: right; - box-sizing: border-box; } diff --git a/src/components/ColorValues/ValueEditor.tsx b/src/components/ColorValues/ValueEditor.tsx index 745e1fc..4ca07c5 100644 --- a/src/components/ColorValues/ValueEditor.tsx +++ b/src/components/ColorValues/ValueEditor.tsx @@ -1,5 +1,5 @@ import { useEffect, useRef, useState } from "react"; -import type { ChangeEvent, RefObject } from "react"; +import type { CSSProperties, ChangeEvent, RefObject } from "react"; import { faChevronLeft, @@ -36,17 +36,18 @@ function ValueEditor({ const direction = Direction.HORIZONTAL; const [origin, setOrigin] = useState({ x: 0, y: 0 }); const [dimensions, setDimensions] = useState({ x: 0, y: 0 }); - const position = useRef(0); + const [position, setPosition] = useState(0); useEffect(() => { - position.current = valueToPosition(value, dimensions.x, valueRange); + const newPosition = valueToPosition(value, dimensions.x, valueRange); + setPosition(newPosition); }, [value, dimensions, valueRange]); // Handler functions const handleInputChange = (e: ChangeEvent) => { const inputValue = parseInt(e.target.value, 10); if (!isNaN(inputValue)) { - const actualValue = inputValue / scale; + const actualValue = Math.floor(inputValue) / scale; const newValue = minmax(actualValue, valueRange.min, valueRange.max); setValue(newValue); @@ -59,7 +60,7 @@ function ValueEditor({ setValue((prev) => { const scaledStep = step / scale; const newValue = minmax( - prev + scaledStep, + Math.floor(prev * scale) / scale + scaledStep, valueRange.min, valueRange.max, ); @@ -96,7 +97,7 @@ function ValueEditor({ diff --git a/src/components/ColorValues/ValueEditorTest.cy.tsx b/src/components/ColorValues/ValueEditorTest.cy.tsx index f5d9ff0..bfed38b 100644 --- a/src/components/ColorValues/ValueEditorTest.cy.tsx +++ b/src/components/ColorValues/ValueEditorTest.cy.tsx @@ -15,7 +15,14 @@ function TestWrapper() { const actions = createColorActions(dispatch); return ( -
+
{ cy.clock().then((clock) => clock.restore()); }); - it("works with mouse events", () => { + it.only("works with mouse events", () => { // Check initial state cy.dataCy("R-slider-bar") .should("have.css", "width", "0px") diff --git a/src/util.ts b/src/util.ts index f85b63e..2645f39 100644 --- a/src/util.ts +++ b/src/util.ts @@ -89,3 +89,8 @@ export function chooseValueByDirection( ) { return direction === Direction.HORIZONTAL ? xValue : yValue; } + +export function roundTo(value: number, decimals: number = 0) { + const factor = Math.pow(10, decimals); + return Math.round(value * factor) / factor; +}