Wrote space editor component and test.
This commit is contained in:
Generated
+13
@@ -9,6 +9,7 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-svg-core": "^7.0.0",
|
"@fortawesome/fontawesome-svg-core": "^7.0.0",
|
||||||
|
"@fortawesome/free-regular-svg-icons": "^7.0.0",
|
||||||
"@fortawesome/free-solid-svg-icons": "^7.0.0",
|
"@fortawesome/free-solid-svg-icons": "^7.0.0",
|
||||||
"@fortawesome/react-fontawesome": "^0.2.3",
|
"@fortawesome/react-fontawesome": "^0.2.3",
|
||||||
"motion": "^12.23.12",
|
"motion": "^12.23.12",
|
||||||
@@ -1145,6 +1146,18 @@
|
|||||||
"node": ">=6"
|
"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": {
|
"node_modules/@fortawesome/free-solid-svg-icons": {
|
||||||
"version": "7.0.0",
|
"version": "7.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-7.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-7.0.0.tgz",
|
||||||
|
|||||||
+4
-3
@@ -7,17 +7,18 @@
|
|||||||
"build:wasm": "wasm-pack build colorlib -t bundler -d pkg --release",
|
"build:wasm": "wasm-pack build colorlib -t bundler -d pkg --release",
|
||||||
"clean": "rm -rf dist colorlib/pkg*",
|
"clean": "rm -rf dist colorlib/pkg*",
|
||||||
"cypress:open": "1>/dev/null 2>/dev/null cypress open -d &",
|
"cypress:open": "1>/dev/null 2>/dev/null cypress open -d &",
|
||||||
"cypress:component": "cypress run --component",
|
|
||||||
"cypress:e2e": "cypress run --e2e",
|
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"test:wasm": "cargo test --lib --manifest-path colorlib/Cargo.toml",
|
"test:wasm": "cargo test --lib --manifest-path colorlib/Cargo.toml",
|
||||||
"test:wasmdoc": "cargo test --doc --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": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-svg-core": "^7.0.0",
|
"@fortawesome/fontawesome-svg-core": "^7.0.0",
|
||||||
|
"@fortawesome/free-regular-svg-icons": "^7.0.0",
|
||||||
"@fortawesome/free-solid-svg-icons": "^7.0.0",
|
"@fortawesome/free-solid-svg-icons": "^7.0.0",
|
||||||
"@fortawesome/react-fontawesome": "^0.2.3",
|
"@fortawesome/react-fontawesome": "^0.2.3",
|
||||||
"motion": "^12.23.12",
|
"motion": "^12.23.12",
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: 300,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: "20px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SpaceEditor
|
||||||
|
space="HCL"
|
||||||
|
color={state.color.hcl}
|
||||||
|
actions={actions.hcl}
|
||||||
|
/>
|
||||||
|
<SpaceEditor
|
||||||
|
space="HSV"
|
||||||
|
color={state.color.hsv}
|
||||||
|
actions={actions.hsv}
|
||||||
|
/>
|
||||||
|
<SpaceEditor
|
||||||
|
space="RGB"
|
||||||
|
color={state.color.rgb}
|
||||||
|
actions={actions.rgb}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ fontFamily: "monospace" }}>
|
||||||
|
<p data-cy="hcl-value">
|
||||||
|
HCL ({roundTo(state.color.hcl.h, 0)}, {roundTo(state.color.hcl.c, 2)},{" "}
|
||||||
|
{roundTo(state.color.hcl.l, 2)})
|
||||||
|
</p>
|
||||||
|
<p data-cy="hsv-value">
|
||||||
|
HSV ({roundTo(state.color.hsv.h, 0)}, {roundTo(state.color.hsv.s, 2)},{" "}
|
||||||
|
{roundTo(state.color.hsv.v, 2)})
|
||||||
|
</p>
|
||||||
|
<p data-cy="rgb-value">
|
||||||
|
RGB ({roundTo(state.color.rgb.r, 0)}, {roundTo(state.color.rgb.g, 0)},{" "}
|
||||||
|
{roundTo(state.color.rgb.b, 0)})
|
||||||
|
</p>
|
||||||
|
<p data-cy="hex-value">HEX: #{state.color.hex.to_code()}</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("space editor tests", () => {
|
||||||
|
it("can edit color values", () => {
|
||||||
|
cy.mount(<TestWrapper />);
|
||||||
|
|
||||||
|
// 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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
.spaceWrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,165 @@
|
|||||||
import * as colorlib from "colorlib";
|
import * as colorlib from "colorlib";
|
||||||
|
|
||||||
import styles from "./SpaceEditor.module.css";
|
import type {
|
||||||
|
HCLColorActions,
|
||||||
|
HSVColorActions,
|
||||||
|
RGBColorActions,
|
||||||
|
} from "@hooks/color";
|
||||||
|
|
||||||
function SpaceEditor({}: {}) {
|
import styles from "./SpaceEditor.module.css";
|
||||||
return;
|
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 <RGBSpaceEditor color={color} actions={actions} />;
|
||||||
|
|
||||||
|
case "HSV":
|
||||||
|
return <HSVSpaceEditor color={color} actions={actions} />;
|
||||||
|
|
||||||
|
case "HCL":
|
||||||
|
return <HCLSpaceEditor color={color} actions={actions} />;
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div data-cy="RGB-editor" className={styles.spaceWrapper}>
|
||||||
|
<ValueEditor
|
||||||
|
componentSymbol={COLOR_SPACES.RGB.symbols.r}
|
||||||
|
valueRange={COLOR_SPACES.RGB.ranges.r}
|
||||||
|
value={color.r}
|
||||||
|
setValue={actions.setR}
|
||||||
|
/>
|
||||||
|
<ValueEditor
|
||||||
|
componentSymbol={COLOR_SPACES.RGB.symbols.g}
|
||||||
|
valueRange={COLOR_SPACES.RGB.ranges.g}
|
||||||
|
value={color.g}
|
||||||
|
setValue={actions.setG}
|
||||||
|
/>
|
||||||
|
<ValueEditor
|
||||||
|
componentSymbol={COLOR_SPACES.RGB.symbols.b}
|
||||||
|
valueRange={COLOR_SPACES.RGB.ranges.b}
|
||||||
|
value={color.b}
|
||||||
|
setValue={actions.setB}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function HSVSpaceEditor({
|
||||||
|
color,
|
||||||
|
actions,
|
||||||
|
}: {
|
||||||
|
color: colorlib.HSV;
|
||||||
|
actions: HSVColorActions;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div data-cy="HSV-editor" className={styles.spaceWrapper}>
|
||||||
|
<ValueEditor
|
||||||
|
componentSymbol={COLOR_SPACES.HSV.symbols.h}
|
||||||
|
valueRange={COLOR_SPACES.HSV.ranges.h}
|
||||||
|
value={color.h}
|
||||||
|
setValue={actions.setH}
|
||||||
|
/>
|
||||||
|
<ValueEditor
|
||||||
|
componentSymbol={COLOR_SPACES.HSV.symbols.s}
|
||||||
|
valueRange={COLOR_SPACES.HSV.ranges.s}
|
||||||
|
value={color.s}
|
||||||
|
setValue={actions.setS}
|
||||||
|
scale={COLOR_SPACES.HSV.scales.s}
|
||||||
|
/>
|
||||||
|
<ValueEditor
|
||||||
|
componentSymbol={COLOR_SPACES.HSV.symbols.v}
|
||||||
|
valueRange={COLOR_SPACES.HSV.ranges.v}
|
||||||
|
value={color.v}
|
||||||
|
setValue={actions.setV}
|
||||||
|
scale={COLOR_SPACES.HSV.scales.v}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function HCLSpaceEditor({
|
||||||
|
color,
|
||||||
|
actions,
|
||||||
|
}: {
|
||||||
|
color: colorlib.HCL;
|
||||||
|
actions: HCLColorActions;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div data-cy="HCL-editor" className={styles.spaceWrapper}>
|
||||||
|
<ValueEditor
|
||||||
|
componentSymbol={COLOR_SPACES.HCL.symbols.h}
|
||||||
|
valueRange={COLOR_SPACES.HCL.ranges.h}
|
||||||
|
value={color.h}
|
||||||
|
setValue={actions.setH}
|
||||||
|
/>
|
||||||
|
<ValueEditor
|
||||||
|
componentSymbol={COLOR_SPACES.HCL.symbols.c}
|
||||||
|
valueRange={COLOR_SPACES.HCL.ranges.c}
|
||||||
|
value={color.c}
|
||||||
|
setValue={actions.setC}
|
||||||
|
scale={COLOR_SPACES.HCL.scales.c}
|
||||||
|
/>
|
||||||
|
<ValueEditor
|
||||||
|
componentSymbol={COLOR_SPACES.HCL.symbols.l}
|
||||||
|
valueRange={COLOR_SPACES.HCL.ranges.l}
|
||||||
|
value={color.l}
|
||||||
|
setValue={actions.setL}
|
||||||
|
scale={COLOR_SPACES.HCL.scales.l}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SpaceEditor;
|
export default SpaceEditor;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: var(--height, 25px);
|
min-height: 0;
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
border: 1px solid black;
|
border: 1px solid black;
|
||||||
border-top: none;
|
border-top: none;
|
||||||
@@ -84,5 +84,4 @@
|
|||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import type { ChangeEvent, RefObject } from "react";
|
import type { CSSProperties, ChangeEvent, RefObject } from "react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
faChevronLeft,
|
faChevronLeft,
|
||||||
@@ -36,17 +36,18 @@ function ValueEditor({
|
|||||||
const direction = Direction.HORIZONTAL;
|
const direction = Direction.HORIZONTAL;
|
||||||
const [origin, setOrigin] = useState<CartesianSpace>({ x: 0, y: 0 });
|
const [origin, setOrigin] = useState<CartesianSpace>({ x: 0, y: 0 });
|
||||||
const [dimensions, setDimensions] = useState<CartesianSpace>({ x: 0, y: 0 });
|
const [dimensions, setDimensions] = useState<CartesianSpace>({ x: 0, y: 0 });
|
||||||
const position = useRef(0);
|
const [position, setPosition] = useState(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
position.current = valueToPosition(value, dimensions.x, valueRange);
|
const newPosition = valueToPosition(value, dimensions.x, valueRange);
|
||||||
|
setPosition(newPosition);
|
||||||
}, [value, dimensions, valueRange]);
|
}, [value, dimensions, valueRange]);
|
||||||
|
|
||||||
// Handler functions
|
// Handler functions
|
||||||
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
|
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
const inputValue = parseInt(e.target.value, 10);
|
const inputValue = parseInt(e.target.value, 10);
|
||||||
if (!isNaN(inputValue)) {
|
if (!isNaN(inputValue)) {
|
||||||
const actualValue = inputValue / scale;
|
const actualValue = Math.floor(inputValue) / scale;
|
||||||
const newValue = minmax(actualValue, valueRange.min, valueRange.max);
|
const newValue = minmax(actualValue, valueRange.min, valueRange.max);
|
||||||
|
|
||||||
setValue(newValue);
|
setValue(newValue);
|
||||||
@@ -59,7 +60,7 @@ function ValueEditor({
|
|||||||
setValue((prev) => {
|
setValue((prev) => {
|
||||||
const scaledStep = step / scale;
|
const scaledStep = step / scale;
|
||||||
const newValue = minmax(
|
const newValue = minmax(
|
||||||
prev + scaledStep,
|
Math.floor(prev * scale) / scale + scaledStep,
|
||||||
valueRange.min,
|
valueRange.min,
|
||||||
valueRange.max,
|
valueRange.max,
|
||||||
);
|
);
|
||||||
@@ -96,7 +97,7 @@ function ValueEditor({
|
|||||||
|
|
||||||
<Slider
|
<Slider
|
||||||
sliderRef={sliderRef}
|
sliderRef={sliderRef}
|
||||||
position={position.current}
|
position={position}
|
||||||
dimensions={dimensions}
|
dimensions={dimensions}
|
||||||
componentSymbol={componentSymbol}
|
componentSymbol={componentSymbol}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -15,7 +15,14 @@ function TestWrapper() {
|
|||||||
const actions = createColorActions(dispatch);
|
const actions = createColorActions(dispatch);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ width: 400 }}>
|
<div
|
||||||
|
style={{
|
||||||
|
width: 400,
|
||||||
|
height: 27,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
}}
|
||||||
|
>
|
||||||
<ValueEditor
|
<ValueEditor
|
||||||
componentSymbol="R"
|
componentSymbol="R"
|
||||||
valueRange={{ min: 0, max: 255 }}
|
valueRange={{ min: 0, max: 255 }}
|
||||||
@@ -36,7 +43,7 @@ describe("component editor tests", () => {
|
|||||||
cy.clock().then((clock) => clock.restore());
|
cy.clock().then((clock) => clock.restore());
|
||||||
});
|
});
|
||||||
|
|
||||||
it("works with mouse events", () => {
|
it.only("works with mouse events", () => {
|
||||||
// Check initial state
|
// Check initial state
|
||||||
cy.dataCy("R-slider-bar")
|
cy.dataCy("R-slider-bar")
|
||||||
.should("have.css", "width", "0px")
|
.should("have.css", "width", "0px")
|
||||||
|
|||||||
@@ -89,3 +89,8 @@ export function chooseValueByDirection(
|
|||||||
) {
|
) {
|
||||||
return direction === Direction.HORIZONTAL ? xValue : yValue;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user