Started palette editor.
Test and Build / test-and-build (push) Failing after 1m47s

Cleaned up tests and lint errors.
Upgraded npm packages.
This commit is contained in:
Jay
2026-03-19 18:54:44 -04:00
parent 6be2d9e41a
commit 9fec89949b
36 changed files with 1484 additions and 1229 deletions
Executable
+14
View File
@@ -0,0 +1,14 @@
#!/bin/bash
latest=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
IFS='.' read -r major minor patch <<< "${latest#v}"
case ${1:-patch} in
major) new="v$((major+1)).0.0" ;;
minor) new="v${major}.$((minor+1)).0" ;;
patch) new="v${major}.${minor}.$((patch+1))" ;;
*) echo "Usage: bump.sh [major|minor|patch]" >&2; exit 1 ;;
esac
git tag -a "$new"
Executable
+1
View File
@@ -0,0 +1 @@
code2prompt -c -e package-lock.json -e example.json -e "colorlib/*"
+16 -2
View File
@@ -167,6 +167,21 @@ impl Color {
Component::HCL_L => Color::from_hcl(self.hcl.h, self.hcl.c, value), Component::HCL_L => Color::from_hcl(self.hcl.h, self.hcl.c, value),
} }
} }
/// Checks if two Color objects are equal
///
/// # Example:
///
/// ```
/// use colorlib::Color;
///
/// let color1 = Color::from_hex("FF0000");
/// let color2 = Color::from_hex("FF0000");
/// assert!(color1.equals(&color2));
/// ```
pub fn equals(&self, other: &Color) -> bool {
self == other
}
} }
#[cfg(test)] #[cfg(test)]
@@ -252,8 +267,7 @@ mod tests {
fn color_from_hcl() { fn color_from_hcl() {
let hex_code = "4B964B"; let hex_code = "4B964B";
let (hr, hg, hb) = (75u8, 150u8, 75u8); let (hr, hg, hb) = (75u8, 150u8, 75u8);
let (r, g, b) = let (r, g, b) = (75.19744022437494, 150.3948804487499, 75.19744022437494);
(75.19744022437494, 150.3948804487499, 75.19744022437494);
let (h1, s, v) = (120.0, 0.5, 0.5897838448970584); let (h1, s, v) = (120.0, 0.5, 0.5897838448970584);
let (h2, c, l) = (120.0, 0.5, 0.49); let (h2, c, l) = (120.0, 0.5, 0.49);
+16 -9
View File
@@ -85,6 +85,21 @@ impl HCL {
let c = calc::chroma(h, s, l); let c = calc::chroma(h, s, l);
HCL::new(h, c, l) HCL::new(h, c, l)
} }
/// Checks if two HCL colors are equal
///
/// # Example:
///
/// ```
/// use colorlib::hcl::HCL;
///
/// let hcl1 = HCL::new(0.0, 1.0, 0.55);
/// let hcl2 = HCL::new(0.0, 1.0, 0.55);
/// assert!(hcl1.equals(&hcl2));
/// ```
pub fn equals(&self, other: &HCL) -> bool {
self == other
}
} }
#[cfg(test)] #[cfg(test)]
@@ -185,15 +200,7 @@ mod tests {
0.864996, 0.864996,
0.521301 0.521301
); );
from_hsv!( from_hsv!(from_hsv_dark_magenta, 300.0, 0.9, 0.5, 300.0, 0.9, 0.323601);
from_hsv_dark_magenta,
300.0,
0.9,
0.5,
300.0,
0.9,
0.323601
);
from_hsv!( from_hsv!(
from_hsv_light_magenta, from_hsv_light_magenta,
300.0, 300.0,
+15
View File
@@ -138,6 +138,21 @@ impl Hex {
b: decoded[2], b: decoded[2],
} }
} }
/// Checks if two Hex colors are equal
///
/// # Example:
///
/// ```
/// use colorlib::hex::Hex;
///
/// let hex1 = Hex::from_code("FF0000");
/// let hex2 = Hex::from_code("FF0000");
/// assert!(hex1.equals(&hex2));
/// ```
pub fn equals(&self, other: &Hex) -> bool {
self == other
}
} }
#[cfg(test)] #[cfg(test)]
+15
View File
@@ -114,6 +114,21 @@ impl HSV {
let v = calc::value(h, s, l); let v = calc::value(h, s, l);
HSV::new(h, s, v) HSV::new(h, s, v)
} }
/// Checks if two HSV colors are equal
///
/// # Example:
///
/// ```
/// use colorlib::hsv::HSV;
///
/// let hsv1 = HSV::new(0.0, 1.0, 1.0);
/// let hsv2 = HSV::new(0.0, 1.0, 1.0);
/// assert!(hsv1.equals(&hsv2));
/// ```
pub fn equals(&self, other: &HSV) -> bool {
self == other
}
} }
#[cfg(test)] #[cfg(test)]
+15
View File
@@ -93,6 +93,21 @@ impl RGB {
let hsv = HSV::from_hcl(h, c, l); let hsv = HSV::from_hcl(h, c, l);
RGB::from_hsv(hsv.h, hsv.s, hsv.v) RGB::from_hsv(hsv.h, hsv.s, hsv.v)
} }
/// Checks if two RGB colors are equal
///
/// # Example:
///
/// ```
/// use colorlib::rgb::RGB;
///
/// let rgb1 = RGB::new(255.0, 0.0, 0.0);
/// let rgb2 = RGB::new(255.0, 0.0, 0.0);
/// assert!(rgb1.equals(&rgb2));
/// ```
pub fn equals(&self, other: &RGB) -> bool {
self == other
}
} }
#[cfg(test)] #[cfg(test)]
-2
View File
@@ -1,2 +0,0 @@
#!/bin/bash
git tag -a "$(cat VERSION)" -m "Version $(cat VERSION)"
+811 -962
View File
File diff suppressed because it is too large Load Diff
+3 -5
View File
@@ -7,7 +7,8 @@
"build:wasm": "wasm-pack build colorlib -t bundler -d pkg --release", "build:wasm": "wasm-pack build colorlib -t bundler -d pkg --release",
"check": "tsc --noEmit -p tsconfig.app.json", "check": "tsc --noEmit -p tsconfig.app.json",
"clean": "rm -rf dist colorlib/pkg*", "clean": "rm -rf dist colorlib/pkg*",
"cypress": "1>/dev/null 2>/dev/null cypress open -d &", "cypress:component": "cypress run --component",
"cypress:open": "1>/dev/null 2>/dev/null cypress open -d &",
"dev": "vite", "dev": "vite",
"lint": "eslint .", "lint": "eslint .",
"preview": "vite preview", "preview": "vite preview",
@@ -20,10 +21,7 @@
"test:e2e:firefox": "cypress run --e2e -b firefox" "test:e2e:firefox": "cypress run --e2e -b firefox"
}, },
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-svg-core": "^7.0.0", "lucide-react": "^0.542.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", "motion": "^12.23.12",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0" "react-dom": "^19.1.0"
+12 -5
View File
@@ -6,7 +6,7 @@ import { memory } from "colorlib/colorlib_bg.wasm";
import { useSmoothAnimation } from "@/hooks/animation"; import { useSmoothAnimation } from "@/hooks/animation";
import type { Setter } from "@/hooks/color"; import type { Setter } from "@/hooks/color";
import { useSlider } from "@/hooks/slider"; import { useSlider } from "@/hooks/slider";
import { useResize } from "@/hooks/window"; import { onResize } from "@/hooks/window";
import type { CartesianSpace } from "@/types"; import type { CartesianSpace } from "@/types";
import { Direction } from "@/types"; import { Direction } from "@/types";
import { setMeasurements } from "@/util"; import { setMeasurements } from "@/util";
@@ -56,7 +56,7 @@ function ColorBar({
refreshColorBar(canvasRef.current!, colorBar); refreshColorBar(canvasRef.current!, colorBar);
}); });
} }
}, [hue, luminance, colorBar]); }, [hue, luminance, colorBar, smoothAnimation]);
// Get measurements // Get measurements
useEffect(() => { useEffect(() => {
@@ -64,10 +64,10 @@ function ColorBar({
setMeasurements(containerRef, setOrigin, setDimensions); setMeasurements(containerRef, setOrigin, setDimensions);
} }
return useResize(() => return onResize(() =>
setMeasurements(containerRef, setOrigin, setDimensions), setMeasurements(containerRef, setOrigin, setDimensions),
); );
}, [containerRef.current]); }, [containerRef]);
// Resize color bar // Resize color bar
useEffect(() => { useEffect(() => {
@@ -87,7 +87,14 @@ function ColorBar({
}); });
} }
} }
}, [containerRef.current, canvasRef.current, parentDimensions]); }, [
containerRef,
canvasRef,
parentDimensions,
hue,
luminance,
smoothAnimation,
]);
return ( return (
<div className={styles.colorBarWrapper} ref={containerRef}> <div className={styles.colorBarWrapper} ref={containerRef}>
+4 -4
View File
@@ -3,7 +3,7 @@ import { useEffect, useRef, useState } from "react";
import * as colorlib from "colorlib"; import * as colorlib from "colorlib";
import type { ColorActions } from "@/hooks/color"; import type { ColorActions } from "@/hooks/color";
import { useResize } from "@/hooks/window"; import { onResize } from "@/hooks/window";
import { Direction } from "@/types"; import { Direction } from "@/types";
import type { CartesianSpace } from "@/types"; import type { CartesianSpace } from "@/types";
import { formatCssRgb, setMeasurements } from "@/util"; import { formatCssRgb, setMeasurements } from "@/util";
@@ -25,7 +25,7 @@ function ColorPicker({
const hueRange = { min: 0, max: 359 }; const hueRange = { min: 0, max: 359 };
const lumRange = { min: 0, max: 1 }; const lumRange = { min: 0, max: 1 };
const [_origin, setOrigin] = useState<CartesianSpace>({ x: 0, y: 0 }); const [, 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 });
// Get measurements // Get measurements
@@ -34,10 +34,10 @@ function ColorPicker({
setMeasurements(containerRef, setOrigin, setDimensions); setMeasurements(containerRef, setOrigin, setDimensions);
} }
return useResize(() => { return onResize(() => {
setMeasurements(containerRef, setOrigin, setDimensions); setMeasurements(containerRef, setOrigin, setDimensions);
}); });
}, [containerRef.current]); }, []);
return ( return (
<div className={styles.container} ref={containerRef}> <div className={styles.container} ref={containerRef}>
+6 -6
View File
@@ -7,7 +7,7 @@ import { useSmoothAnimation } from "@/hooks/animation";
import type { HCLColorActions } from "@/hooks/color"; import type { HCLColorActions } from "@/hooks/color";
import { useCrosshair } from "@/hooks/crosshair"; import { useCrosshair } from "@/hooks/crosshair";
import { useScroll } from "@/hooks/scroll"; import { useScroll } from "@/hooks/scroll";
import { useResize } from "@/hooks/window"; import { onResize } from "@/hooks/window";
import type { CartesianSpace } from "@/types"; import type { CartesianSpace } from "@/types";
import { setMeasurements } from "@/util"; import { setMeasurements } from "@/util";
@@ -62,12 +62,12 @@ function ColorSquare({
refreshColorSquare(canvasRef.current!, colorSquare); refreshColorSquare(canvasRef.current!, colorSquare);
}); });
} }
}, [chroma, colorSquare]); }, [chroma, colorSquare, smoothAnimation]);
// Add event listeners // Add event listeners
useEffect(() => { useEffect(() => {
if (canvasRef.current) addScrollListener(); if (canvasRef.current) addScrollListener();
}, []); }, [addScrollListener]);
// Get measurements // Get measurements
useEffect(() => { useEffect(() => {
@@ -75,10 +75,10 @@ function ColorSquare({
setMeasurements(containerRef, setOrigin, setDimensions); setMeasurements(containerRef, setOrigin, setDimensions);
} }
return useResize(() => return onResize(() =>
setMeasurements(containerRef, setOrigin, setDimensions), setMeasurements(containerRef, setOrigin, setDimensions),
); );
}, [containerRef.current, parentDimensions]); }, [containerRef, parentDimensions]);
// Resize square // Resize square
useEffect(() => { useEffect(() => {
@@ -97,7 +97,7 @@ function ColorSquare({
}); });
} }
} }
}, [containerRef.current, canvasRef.current, parentDimensions]); }, [containerRef, canvasRef, parentDimensions, chroma, smoothAnimation]);
return ( return (
<div className={styles.colorSquareWrapper} ref={containerRef}> <div className={styles.colorSquareWrapper} ref={containerRef}>
+7 -7
View File
@@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from "react";
import * as colorlib from "colorlib"; import * as colorlib from "colorlib";
import { useResize } from "@/hooks/window"; import { onResize } from "@/hooks/window";
import type { CartesianSpace } from "@/types"; import type { CartesianSpace } from "@/types";
import { formatCssRgb, setMeasurements, valueToPosition } from "@/util"; import { formatCssRgb, setMeasurements, valueToPosition } from "@/util";
@@ -19,7 +19,7 @@ export function SquareCrosshair({
hex: colorlib.Hex; hex: colorlib.Hex;
parentDimensions: CartesianSpace; parentDimensions: CartesianSpace;
}) { }) {
const [_origin, setOrigin] = useState<CartesianSpace>({ x: 0, y: 0 }); const [, 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 [darkCrosshairs, setDarkCrosshairs] = useState(true); const [darkCrosshairs, setDarkCrosshairs] = useState(true);
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
@@ -32,10 +32,10 @@ export function SquareCrosshair({
useEffect(() => { useEffect(() => {
setMeasurements(containerRef, setOrigin, setDimensions); setMeasurements(containerRef, setOrigin, setDimensions);
return useResize(() => return onResize(() =>
setMeasurements(containerRef, setOrigin, setDimensions), setMeasurements(containerRef, setOrigin, setDimensions),
); );
}, [containerRef.current, parentDimensions]); }, [containerRef, parentDimensions]);
return ( return (
<div className={styles.crosshairWrapper} ref={containerRef}> <div className={styles.crosshairWrapper} ref={containerRef}>
@@ -86,7 +86,7 @@ export function BarCrosshair({
hex: colorlib.Hex; hex: colorlib.Hex;
parentDimensions: CartesianSpace; parentDimensions: CartesianSpace;
}) { }) {
const [_origin, setOrigin] = useState<CartesianSpace>({ x: 0, y: 0 }); const [, 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 [darkCrosshairs, setDarkCrosshairs] = useState(true); const [darkCrosshairs, setDarkCrosshairs] = useState(true);
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
@@ -98,10 +98,10 @@ export function BarCrosshair({
useEffect(() => { useEffect(() => {
setMeasurements(containerRef, setOrigin, setDimensions); setMeasurements(containerRef, setOrigin, setDimensions);
return useResize(() => return onResize(() =>
setMeasurements(containerRef, setOrigin, setDimensions), setMeasurements(containerRef, setOrigin, setDimensions),
); );
}, [containerRef.current, parentDimensions]); }, [containerRef, parentDimensions]);
return ( return (
<div className={styles.crosshairWrapper} ref={containerRef}> <div className={styles.crosshairWrapper} ref={containerRef}>
+3 -5
View File
@@ -2,7 +2,7 @@ import { useEffect, useState } from "react";
import type { Setter } from "@/hooks/color"; import type { Setter } from "@/hooks/color";
import { useSlider } from "@/hooks/slider"; import { useSlider } from "@/hooks/slider";
import { useResize } from "@/hooks/window"; import { onResize } from "@/hooks/window";
import { Direction } from "@/types"; import { Direction } from "@/types";
import type { CartesianSpace, Range } from "@/types"; import type { CartesianSpace, Range } from "@/types";
import { import {
@@ -49,10 +49,8 @@ function GripSlider({
setMeasurements(sliderRef, setOrigin, setDimensions); setMeasurements(sliderRef, setOrigin, setDimensions);
} }
return useResize(() => return onResize(() => setMeasurements(sliderRef, setOrigin, setDimensions));
setMeasurements(sliderRef, setOrigin, setDimensions), }, [sliderRef, parentDimensions]);
);
}, [sliderRef.current, parentDimensions]);
const upArrowStyle = { const upArrowStyle = {
borderLeft: "12px solid transparent", borderLeft: "12px solid transparent",
@@ -74,6 +74,9 @@
} }
.button { .button {
display: flex;
align-items: center;
justify-content: center;
height: 100%; height: 100%;
width: 100%; width: 100%;
padding: 0; padding: 0;
+8 -15
View File
@@ -1,19 +1,14 @@
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import type { ChangeEvent, KeyboardEvent, RefObject } from "react"; import type { ChangeEvent, KeyboardEvent, RefObject } from "react";
import {
faChevronLeft,
faChevronRight,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import clsx from "clsx"; import clsx from "clsx";
import * as colorlib from "colorlib"; import * as colorlib from "colorlib";
import { ChevronLeft, ChevronRight } from "lucide-react";
import type { HexColorActions } from "@/hooks/color"; import type { HexColorActions } from "@/hooks/color";
import { useScroll } from "@/hooks/scroll"; import { useScroll } from "@/hooks/scroll";
import { useSlider } from "@/hooks/slider"; import { useSlider } from "@/hooks/slider";
import { useResize } from "@/hooks/window"; import { onResize } from "@/hooks/window";
import type { CartesianSpace, Range, Setter, Timeout } from "@/types"; import type { CartesianSpace, Range, Setter, Timeout } from "@/types";
import { Direction } from "@/types"; import { Direction } from "@/types";
import { minmax, roundTo, setMeasurements, valueToPosition } from "@/util"; import { minmax, roundTo, setMeasurements, valueToPosition } from "@/util";
@@ -89,9 +84,7 @@ export function ValueEditor({
// Set component dimensions for slider hook // Set component dimensions for slider hook
useEffect(() => { useEffect(() => {
setMeasurements(sliderRef, setOrigin, setDimensions); setMeasurements(sliderRef, setOrigin, setDimensions);
return useResize(() => return onResize(() => setMeasurements(sliderRef, setOrigin, setDimensions));
setMeasurements(sliderRef, setOrigin, setDimensions),
);
}, [sliderRef, setOrigin, setDimensions]); }, [sliderRef, setOrigin, setDimensions]);
return ( return (
@@ -209,7 +202,7 @@ function Button({
}) { }) {
const isIncrease = direction === "increase"; const isIncrease = direction === "increase";
const label = isIncrease ? "Increase" : "Decrease"; const label = isIncrease ? "Increase" : "Decrease";
const icon = isIncrease ? faChevronRight : faChevronLeft; const Icon = isIncrease ? ChevronRight : ChevronLeft;
const dataCy = `${componentSymbol}-${isIncrease ? "increment" : "decrement"}-button`; const dataCy = `${componentSymbol}-${isIncrease ? "increment" : "decrement"}-button`;
const step = isIncrease ? 1 : -1; const step = isIncrease ? 1 : -1;
@@ -234,7 +227,7 @@ function Button({
data-cy={dataCy} data-cy={dataCy}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
> >
<FontAwesomeIcon icon={icon} transform="shrink-2 down-1" /> <Icon size={18} />
</button> </button>
</div> </div>
); );
@@ -338,8 +331,8 @@ function useLongPressRepeat(
onMouseLeave: stop, onMouseLeave: stop,
onTouchStart: start, onTouchStart: start,
onTouchEnd: stop, onTouchEnd: stop,
// Intentional 'any' to avoid overly complex typing // @ts-expect-error: Avoid overly complex typing
onContextMenu: (e: Event | any) => e.preventDefault(), onContextMenu: (e) => e.preventDefault(),
}; };
} }
@@ -385,7 +378,7 @@ export function HexEditor({
useEffect(() => { useEffect(() => {
setInputValue(formatHexString(color, isShortHex)); setInputValue(formatHexString(color, isShortHex));
}, [color]); }, [color, isShortHex]);
const onChange = (e: ChangeEvent<HTMLInputElement>) => { const onChange = (e: ChangeEvent<HTMLInputElement>) => {
const value = e.target.value; const value = e.target.value;
@@ -1,16 +1,37 @@
/* Large - Landscape Tablets / Desktops */ .paletteEditor {
/* Medium - Portrait Tablets */ height: 100%;
/* Horizontal layout, vertically scrolling picker and palette content */ width: 100%;
@media (min-width: 992px), display: flex;
(min-width: 568px) and (max-width: 991px) and (orientation: portrait) { flex-direction: column;
} }
/* Medium - Landscape Phones */ .actionBar {
/* Horizontal layout, side menu, vertical tabbed picker */ height: 40px;
@media (min-width: 568px) and (max-width: 991px) and (orientation: landscape) {
} }
/* Small - Portrait Phones*/ .cardWrapper {
/* Vertical layout, side menu, horizontal tabbed picker */ flex: 1;
@media (max-width: 567px) { padding: 20px;
display: grid;
grid-template-columns: 1fr 1fr 4fr;
grid-template-rows: 40px 1fr;
grid-template-areas:
". . card-header"
"preview selection palette";
}
.cardHeader {
grid-area: card-header;
}
.pickerColor {
grid-area: preview;
}
.paletteColor {
grid-area: selection;
}
.palette {
grid-area: palette;
} }
@@ -0,0 +1,39 @@
import { useReducer } from "react";
import { Color } from "colorlib";
import { HexEditor } from "@/components/ColorValues/ValueEditor";
import { colorReducer, createColorActions } from "@/hooks/color";
import PaletteEditor from "./PaletteEditor";
const initialState = {
color: Color.from_hex("000"),
};
function TestWrapper() {
const [state, dispatch] = useReducer(colorReducer, initialState);
const actions = createColorActions(dispatch);
return (
<>
<div style={{ width: "100%", height: 400 }}>
<PaletteEditor
pickerColor={state.color.hex}
setPickerColor={actions.hex.setHex}
/>
</div>
<HexEditor color={state.color.hex} actions={actions.hex} />
</>
);
}
describe("palette editor tests", () => {
beforeEach(() => {
cy.mount(<TestWrapper />);
});
it("renders the palette editor", () => {
cy.dataCy("palette-editor").should("exist");
});
});
@@ -0,0 +1,51 @@
import { Hex as HexColor } from "colorlib";
import styles from "./PaletteEditor.module.css";
function PaletteEditor({
pickerColor,
setPickerColor,
}: {
pickerColor: HexColor;
setPickerColor: (hex: HexColor) => void;
}) {
return (
<div className={styles.paletteEditor} data-cy="palette-editor">
<ActionBar />
<PaletteCard />
</div>
);
}
function ActionBar() {
return <div className={styles.actionBar}>actions</div>;
}
function PaletteCard() {
return (
<div className={styles.cardWrapper}>
<CardHeader />
<PickerColor />
<PaletteColor />
<Palette />
</div>
);
}
function CardHeader() {
return <div className={styles.cardHeader}>header</div>;
}
function PickerColor() {
return <div className={styles.pickerColor}>picker color</div>;
}
function PaletteColor() {
return <div className={styles.paletteColor}>palette color</div>;
}
function Palette() {
return <div className={styles.palette}>palette</div>;
}
export default PaletteEditor;
+7 -7
View File
@@ -24,34 +24,34 @@ export function colorReducer(
state: ColorState, state: ColorState,
action: ColorAction, action: ColorAction,
): ColorState { ): ColorState {
let comp; let comp, rgb, hsv, hcl, hex, valOrFn, prev;
switch (action.type) { switch (action.type) {
case "SET_COLOR": case "SET_COLOR":
return { ...state, color: action.payload }; return { ...state, color: action.payload };
case "SET_RGB": case "SET_RGB":
let rgb = action.payload; rgb = action.payload;
return { ...state, color: colorlib.Color.from_rgb(rgb.r, rgb.g, rgb.b) }; return { ...state, color: colorlib.Color.from_rgb(rgb.r, rgb.g, rgb.b) };
case "SET_HSV": case "SET_HSV":
let hsv = action.payload; hsv = action.payload;
return { ...state, color: colorlib.Color.from_hsv(hsv.h, hsv.s, hsv.v) }; return { ...state, color: colorlib.Color.from_hsv(hsv.h, hsv.s, hsv.v) };
case "SET_HCL": case "SET_HCL":
let hcl = action.payload; hcl = action.payload;
return { ...state, color: colorlib.Color.from_hcl(hcl.h, hcl.c, hcl.l) }; return { ...state, color: colorlib.Color.from_hcl(hcl.h, hcl.c, hcl.l) };
case "SET_HEX": case "SET_HEX":
let hex = action.payload; hex = action.payload;
return { ...state, color: colorlib.Color.from_hex(hex.to_code()) }; return { ...state, color: colorlib.Color.from_hex(hex.to_code()) };
case "SET_VALUE": case "SET_VALUE":
comp = action.component; comp = action.component;
let valOrFn = action.payload; valOrFn = action.payload;
if (typeof valOrFn === "function") { if (typeof valOrFn === "function") {
let prev = state.color.get(comp); prev = state.color.get(comp);
return { ...state, color: state.color.update(comp, valOrFn(prev)) }; return { ...state, color: state.color.update(comp, valOrFn(prev)) };
} else { } else {
return { ...state, color: state.color.update(comp, valOrFn) }; return { ...state, color: state.color.update(comp, valOrFn) };
+7 -4
View File
@@ -12,7 +12,7 @@ import {
import { useSmoothAnimation } from "./animation"; import { useSmoothAnimation } from "./animation";
if (typeof TouchEvent === "undefined") { if (typeof TouchEvent === "undefined") {
// @ts-ignore - intentionally creating global // @ts-expect-error - intentionally creating global
window.TouchEvent = window.MouseEvent; window.TouchEvent = window.MouseEvent;
} }
@@ -61,7 +61,8 @@ export function useCrosshair({
yValueRangeRef.current = yValueRange; yValueRangeRef.current = yValueRange;
}, [xValueRange, yValueRange]); }, [xValueRange, yValueRange]);
const calculatePositions = useCallback((event: MouseEvent | TouchEvent) => { const calculatePositions = useCallback(
(event: MouseEvent | TouchEvent) => {
const orig = originRef.current; const orig = originRef.current;
const dims = dimensionsRef.current; const dims = dimensionsRef.current;
const xRange = xValueRangeRef.current; const xRange = xValueRangeRef.current;
@@ -84,14 +85,16 @@ export function useCrosshair({
setXValueRef.current(newXValue); setXValueRef.current(newXValue);
setYValueRef.current(newYValue); setYValueRef.current(newYValue);
}, []); },
[invertX, invertY],
);
const handleMove = useCallback( const handleMove = useCallback(
(event: MouseEvent | TouchEvent) => { (event: MouseEvent | TouchEvent) => {
event.preventDefault(); event.preventDefault();
smoothAnimation(() => calculatePositions(event)); smoothAnimation(() => calculatePositions(event));
}, },
[calculatePositions], [calculatePositions, smoothAnimation],
); );
const handleEnd = useCallback( const handleEnd = useCallback(
+24 -11
View File
@@ -22,9 +22,11 @@ interface DragState<T> {
} }
function reducer<T>(state: DragState<T>, action: DragAction<T>) { function reducer<T>(state: DragState<T>, action: DragAction<T>) {
let items, cursor, rects, newTargetIndex, newPreviewItems, movedItem;
switch (action.type) { switch (action.type) {
case "resetItems": case "resetItems":
const items = action.items; items = action.items;
return { return {
...state, ...state,
items: [...items], items: [...items],
@@ -44,8 +46,9 @@ function reducer<T>(state: DragState<T>, action: DragAction<T>) {
case "processMove": case "processMove":
if (!state.isDragging) return state; if (!state.isDragging) return state;
const { cursor, rects } = action; cursor = action.cursor;
let newTargetIndex = state.targetIndex; rects = action.rects;
newTargetIndex = state.targetIndex;
for (let i = 0; i < rects.length; i++) { for (let i = 0; i < rects.length; i++) {
const rect = rects[i]; const rect = rects[i];
@@ -62,8 +65,8 @@ function reducer<T>(state: DragState<T>, action: DragAction<T>) {
if (newTargetIndex === state.targetIndex) return state; if (newTargetIndex === state.targetIndex) return state;
const newPreviewItems = [...state.items]; newPreviewItems = [...state.items];
const [movedItem] = newPreviewItems.splice(state.sourceIndex, 1); [movedItem] = newPreviewItems.splice(state.sourceIndex, 1);
newPreviewItems.splice(newTargetIndex, 0, movedItem); newPreviewItems.splice(newTargetIndex, 0, movedItem);
return { return {
@@ -136,18 +139,21 @@ export function useDragAndDrop<T extends { id: string }>({
return itemElement; return itemElement;
} }
function getItemIndex(el: HTMLElement) { const getItemIndex = useCallback(
(el: HTMLElement) => {
const itemId = el.dataset.itemId; const itemId = el.dataset.itemId;
const index = items.findIndex((item) => item.id === itemId); const index = items.findIndex((item) => item.id === itemId);
return index; return index;
} },
[items],
);
function captureItemBoundaries() { const captureItemBoundaries = useCallback(() => {
itemBoundingRects.current = items.map((item) => { itemBoundingRects.current = items.map((item) => {
const el = itemRefs.current[item.id]?.current; const el = itemRefs.current[item.id]?.current;
return el ? el.getBoundingClientRect() : new DOMRect(); return el ? el.getBoundingClientRect() : new DOMRect();
}); });
} }, [itemRefs, items]);
const handleDragMove = useCallback( const handleDragMove = useCallback(
(event: MouseEvent | TouchEvent) => { (event: MouseEvent | TouchEvent) => {
@@ -195,7 +201,14 @@ export function useDragAndDrop<T extends { id: string }>({
document.addEventListener("touchend", handleDragEnd); document.addEventListener("touchend", handleDragEnd);
document.addEventListener("touchcancel", handleDragEnd); document.addEventListener("touchcancel", handleDragEnd);
}, },
[items, dispatch, handleDragEnd, handleDragMove], [
items,
dispatch,
handleDragEnd,
handleDragMove,
captureItemBoundaries,
getItemIndex,
],
); );
// Set/cleanup event handlers // Set/cleanup event handlers
@@ -221,7 +234,7 @@ export function useDragAndDrop<T extends { id: string }>({
}); });
}; };
} }
}, [items, handleDragStart, disabled]); }, [items, handleDragStart, disabled, itemRefs]);
return { return {
containerRef, containerRef,
+118
View File
@@ -0,0 +1,118 @@
import type { Dispatch } from "react";
export interface PaletteColor {
id: string;
name: string;
hex: string;
}
export interface PaletteCard {
id: string;
name: string;
colors: PaletteColor[];
selectedColorId: string | null;
inToolkitMode: boolean;
}
export interface PaletteCardState {
present: PaletteCard;
history: PaletteCard[];
future: PaletteCard[];
}
export type PaletteCardAction =
| { type: "SET_CARD_NAME"; payload: string }
| { type: "SET_SELECTED_COLOR"; payload: string | null }
| { type: "ADD_COLOR" }
| { type: "DELETE_SELECTED_COLOR" }
| { type: "DUPLICATE_SELECTED_COLOR" }
| { type: "REORDER_COLORS"; payload: PaletteColor[] }
| { type: "TOGGLE_TOOLKIT_MODE" }
| { type: "UNDO" }
| { type: "REDO" };
export function paletteCardReducer(
state: PaletteCardState,
action: PaletteCardAction,
): PaletteCardState {
const pushToHistory = (state: PaletteCardState, newPresent: PaletteCard) => {
return {
...state,
history: [state.present, ...state.history],
future: [],
present: newPresent,
};
};
switch (action.type) {
case "SET_CARD_NAME":
state = pushToHistory(state, { ...state.present, name: action.payload });
return state;
case "SET_SELECTED_COLOR":
// TODO: Implement
return state;
case "ADD_COLOR":
// TODO: Implement
return state;
case "DELETE_SELECTED_COLOR":
// TODO: Implement
return state;
case "DUPLICATE_SELECTED_COLOR":
// TODO: Implement
return state;
case "REORDER_COLORS":
// TODO: Implement
return state;
case "TOGGLE_TOOLKIT_MODE":
// TODO: Implement
return state;
case "UNDO":
// TODO: Implement
return state;
case "REDO":
// TODO: Implement
return state;
default:
return state;
}
}
export interface PaletteCardActions {
setCardName: (name: string) => void;
setSelectedColor: (id: string | null) => void;
addColor: () => void;
deleteSelectedColor: () => void;
duplicateSelectedColor: () => void;
reorderColors: (colors: PaletteColor[]) => void;
toggleToolkitMode: () => void;
undo: () => void;
redo: () => void;
}
export function createPaletteCardActions(
dispatch: Dispatch<PaletteCardAction>,
): PaletteCardActions {
return {
setCardName: (name) => dispatch({ type: "SET_CARD_NAME", payload: name }),
setSelectedColor: (id) =>
dispatch({ type: "SET_SELECTED_COLOR", payload: id }),
addColor: () => dispatch({ type: "ADD_COLOR" }),
deleteSelectedColor: () => dispatch({ type: "DELETE_SELECTED_COLOR" }),
duplicateSelectedColor: () =>
dispatch({ type: "DUPLICATE_SELECTED_COLOR" }),
reorderColors: (colors) =>
dispatch({ type: "REORDER_COLORS", payload: colors }),
toggleToolkitMode: () => dispatch({ type: "TOGGLE_TOOLKIT_MODE" }),
undo: () => dispatch({ type: "UNDO" }),
redo: () => dispatch({ type: "REDO" }),
};
}
+9 -6
View File
@@ -16,7 +16,7 @@ import { useSmoothAnimation } from "./animation";
import { useScroll } from "./scroll"; import { useScroll } from "./scroll";
if (typeof TouchEvent === "undefined") { if (typeof TouchEvent === "undefined") {
// @ts-ignore - intentionally creating global // @ts-expect-error - intentionally creating global
window.TouchEvent = window.MouseEvent; window.TouchEvent = window.MouseEvent;
} }
@@ -79,11 +79,11 @@ export function useSlider({
maxPosition.current, maxPosition.current,
valueRangeRef.current, valueRangeRef.current,
); );
}, [direction, origin, dimensions]); }, [direction, origin, dimensions, value]);
useEffect(() => { useEffect(() => {
valueRangeRef.current = valueRange; valueRangeRef.current = valueRange;
}, [valueRangeRef]); }, [valueRange, valueRangeRef]);
useEffect(() => { useEffect(() => {
setValueRef.current = setValue; setValueRef.current = setValue;
@@ -94,7 +94,8 @@ export function useSlider({
}, [position]); }, [position]);
// Setup drag handlers // Setup drag handlers
const calculatePosition = useCallback((event: MouseEvent | TouchEvent) => { const calculatePosition = useCallback(
(event: MouseEvent | TouchEvent) => {
const dir = directionRef.current; const dir = directionRef.current;
const orig = originRef.current; const orig = originRef.current;
const dims = dimensionsRef.current; const dims = dimensionsRef.current;
@@ -116,14 +117,16 @@ export function useSlider({
} }
setValueRef.current(newValue); setValueRef.current(newValue);
}, []); },
[invert],
);
const handleMove = useCallback( const handleMove = useCallback(
(event: MouseEvent | TouchEvent) => { (event: MouseEvent | TouchEvent) => {
event.preventDefault(); event.preventDefault();
smoothAnimation(() => calculatePosition(event)); smoothAnimation(() => calculatePosition(event));
}, },
[calculatePosition], [calculatePosition, smoothAnimation],
); );
const handleEnd = useCallback(() => { const handleEnd = useCallback(() => {
+62 -95
View File
@@ -1,44 +1,11 @@
import * as colorlib from "colorlib"; import * as colorlib from "colorlib";
import { beforeEach, describe, expect, test } from "vitest"; import { beforeEach, describe, expect, test } from "vitest";
import { expectEqualColor, mockUseReducer } from "@/testUtil";
import { colorReducer, createColorActions } from "../color"; import { colorReducer, createColorActions } from "../color";
import type { ColorAction, ColorActions, ColorState } from "../color"; import type { ColorAction, ColorActions, ColorState } from "../color";
const mockUseReducer = <T extends object, U>(
reducer: (state: T, action: U) => T,
initialArg: T,
): [T, (value: U) => void] => {
let currentState = initialArg;
const state = new Proxy({} as T, {
get: (_, prop) => currentState[prop as keyof T],
});
const dispatch = (value: U) => {
const nextState = reducer(currentState, value);
currentState = nextState;
};
return [state, dispatch];
};
const expectRGB = (value: colorlib.RGB, expected: colorlib.RGB) => {
expect(value.r).toBe(expected.r);
expect(value.g).toBe(expected.g);
expect(value.b).toBe(expected.b);
};
const expectHSV = (value: colorlib.HSV, expected: colorlib.HSV) => {
expect(value.h).toBe(expected.h);
expect(value.s).toBe(expected.s);
expect(value.v).toBe(expected.v);
};
const expectHCL = (value: colorlib.HCL, expected: colorlib.HCL) => {
expect(value.h).toBe(expected.h);
expect(value.c).toBe(expected.c);
expect(value.l).toBe(expected.l);
};
describe("color reducer", () => { describe("color reducer", () => {
const initialState = { color: colorlib.Color.from_hex("000") }; const initialState = { color: colorlib.Color.from_hex("000") };
@@ -61,7 +28,7 @@ describe("color reducer", () => {
payload: nextColor, payload: nextColor,
}); });
expectRGB(nextState.color.rgb, nextColor); expectEqualColor(nextState.color.rgb, nextColor);
}); });
test("set hsv", () => { test("set hsv", () => {
@@ -71,7 +38,7 @@ describe("color reducer", () => {
payload: nextColor, payload: nextColor,
}); });
expectHSV(nextState.color.hsv, nextColor); expectEqualColor(nextState.color.hsv, nextColor);
}); });
test("set hcl", () => { test("set hcl", () => {
@@ -81,7 +48,7 @@ describe("color reducer", () => {
payload: nextColor, payload: nextColor,
}); });
expectHCL(nextState.color.hcl, nextColor); expectEqualColor(nextState.color.hcl, nextColor);
}); });
test("set hex", () => { test("set hex", () => {
@@ -104,7 +71,7 @@ describe("color reducer", () => {
component: colorlib.Component.RGB_R, component: colorlib.Component.RGB_R,
payload: nextColor.r, payload: nextColor.r,
}); });
expectRGB(nextState.color.rgb, nextColor); expectEqualColor(nextState.color.rgb, nextColor);
}); });
test("set rgb g", () => { test("set rgb g", () => {
@@ -114,7 +81,7 @@ describe("color reducer", () => {
component: colorlib.Component.RGB_G, component: colorlib.Component.RGB_G,
payload: nextColor.g, payload: nextColor.g,
}); });
expectRGB(nextState.color.rgb, nextColor); expectEqualColor(nextState.color.rgb, nextColor);
}); });
test("set rgb b", () => { test("set rgb b", () => {
@@ -124,7 +91,7 @@ describe("color reducer", () => {
component: colorlib.Component.RGB_B, component: colorlib.Component.RGB_B,
payload: nextColor.b, payload: nextColor.b,
}); });
expectRGB(nextState.color.rgb, nextColor); expectEqualColor(nextState.color.rgb, nextColor);
}); });
}); });
@@ -136,7 +103,7 @@ describe("color reducer", () => {
component: colorlib.Component.HSV_H, component: colorlib.Component.HSV_H,
payload: nextColor.h, payload: nextColor.h,
}); });
expectHSV(nextState.color.hsv, nextColor); expectEqualColor(nextState.color.hsv, nextColor);
}); });
test("set hsv s", () => { test("set hsv s", () => {
@@ -146,7 +113,7 @@ describe("color reducer", () => {
component: colorlib.Component.HSV_S, component: colorlib.Component.HSV_S,
payload: nextColor.s, payload: nextColor.s,
}); });
expectHSV(nextState.color.hsv, nextColor); expectEqualColor(nextState.color.hsv, nextColor);
}); });
test("set hsv v", () => { test("set hsv v", () => {
@@ -156,7 +123,7 @@ describe("color reducer", () => {
component: colorlib.Component.HSV_V, component: colorlib.Component.HSV_V,
payload: nextColor.v, payload: nextColor.v,
}); });
expectHSV(nextState.color.hsv, nextColor); expectEqualColor(nextState.color.hsv, nextColor);
}); });
}); });
@@ -168,7 +135,7 @@ describe("color reducer", () => {
component: colorlib.Component.HCL_H, component: colorlib.Component.HCL_H,
payload: nextColor.h, payload: nextColor.h,
}); });
expectHCL(nextState.color.hcl, nextColor); expectEqualColor(nextState.color.hcl, nextColor);
}); });
test("set hcl c", () => { test("set hcl c", () => {
@@ -178,7 +145,7 @@ describe("color reducer", () => {
component: colorlib.Component.HCL_C, component: colorlib.Component.HCL_C,
payload: nextColor.c, payload: nextColor.c,
}); });
expectHCL(nextState.color.hcl, nextColor); expectEqualColor(nextState.color.hcl, nextColor);
}); });
test("set hcl l", () => { test("set hcl l", () => {
@@ -188,7 +155,7 @@ describe("color reducer", () => {
component: colorlib.Component.HCL_L, component: colorlib.Component.HCL_L,
payload: nextColor.l, payload: nextColor.l,
}); });
expectHCL(nextState.color.hcl, nextColor); expectEqualColor(nextState.color.hcl, nextColor);
}); });
}); });
}); });
@@ -202,7 +169,7 @@ describe("color reducer", () => {
component: colorlib.Component.RGB_R, component: colorlib.Component.RGB_R,
payload: (prev) => prev + 100, payload: (prev) => prev + 100,
}); });
expectRGB(nextState.color.rgb, nextColor); expectEqualColor(nextState.color.rgb, nextColor);
nextColor = colorlib.RGB.new(50, 0, 0); nextColor = colorlib.RGB.new(50, 0, 0);
nextState = colorReducer(nextState, { nextState = colorReducer(nextState, {
@@ -210,7 +177,7 @@ describe("color reducer", () => {
component: colorlib.Component.RGB_R, component: colorlib.Component.RGB_R,
payload: (prev) => prev - 50, payload: (prev) => prev - 50,
}); });
expectRGB(nextState.color.rgb, nextColor); expectEqualColor(nextState.color.rgb, nextColor);
}); });
test("adjust rgb g", () => { test("adjust rgb g", () => {
@@ -220,7 +187,7 @@ describe("color reducer", () => {
component: colorlib.Component.RGB_G, component: colorlib.Component.RGB_G,
payload: (prev) => prev + 100, payload: (prev) => prev + 100,
}); });
expectRGB(nextState.color.rgb, nextColor); expectEqualColor(nextState.color.rgb, nextColor);
nextColor = colorlib.RGB.new(0, 50, 0); nextColor = colorlib.RGB.new(0, 50, 0);
nextState = colorReducer(nextState, { nextState = colorReducer(nextState, {
@@ -228,7 +195,7 @@ describe("color reducer", () => {
component: colorlib.Component.RGB_G, component: colorlib.Component.RGB_G,
payload: (prev) => prev - 50, payload: (prev) => prev - 50,
}); });
expectRGB(nextState.color.rgb, nextColor); expectEqualColor(nextState.color.rgb, nextColor);
}); });
test("adjust rgb b", () => { test("adjust rgb b", () => {
@@ -238,7 +205,7 @@ describe("color reducer", () => {
component: colorlib.Component.RGB_B, component: colorlib.Component.RGB_B,
payload: (prev) => prev + 100, payload: (prev) => prev + 100,
}); });
expectRGB(nextState.color.rgb, nextColor); expectEqualColor(nextState.color.rgb, nextColor);
nextColor = colorlib.RGB.new(0, 0, 50); nextColor = colorlib.RGB.new(0, 0, 50);
nextState = colorReducer(nextState, { nextState = colorReducer(nextState, {
@@ -246,7 +213,7 @@ describe("color reducer", () => {
component: colorlib.Component.RGB_B, component: colorlib.Component.RGB_B,
payload: (prev) => prev - 50, payload: (prev) => prev - 50,
}); });
expectRGB(nextState.color.rgb, nextColor); expectEqualColor(nextState.color.rgb, nextColor);
}); });
}); });
@@ -258,7 +225,7 @@ describe("color reducer", () => {
component: colorlib.Component.HSV_H, component: colorlib.Component.HSV_H,
payload: (prev) => prev + 100, payload: (prev) => prev + 100,
}); });
expectHSV(nextState.color.hsv, nextColor); expectEqualColor(nextState.color.hsv, nextColor);
nextColor = colorlib.HSV.new(50, 0, 0); nextColor = colorlib.HSV.new(50, 0, 0);
nextState = colorReducer(nextState, { nextState = colorReducer(nextState, {
@@ -266,7 +233,7 @@ describe("color reducer", () => {
component: colorlib.Component.HSV_H, component: colorlib.Component.HSV_H,
payload: (prev) => prev - 50, payload: (prev) => prev - 50,
}); });
expectHSV(nextState.color.hsv, nextColor); expectEqualColor(nextState.color.hsv, nextColor);
}); });
test("adjust hsv s", () => { test("adjust hsv s", () => {
@@ -276,7 +243,7 @@ describe("color reducer", () => {
component: colorlib.Component.HSV_S, component: colorlib.Component.HSV_S,
payload: (prev) => prev + 1, payload: (prev) => prev + 1,
}); });
expectHSV(nextState.color.hsv, nextColor); expectEqualColor(nextState.color.hsv, nextColor);
nextColor = colorlib.HSV.new(0, 0.5, 0); nextColor = colorlib.HSV.new(0, 0.5, 0);
nextState = colorReducer(nextState, { nextState = colorReducer(nextState, {
@@ -284,7 +251,7 @@ describe("color reducer", () => {
component: colorlib.Component.HSV_S, component: colorlib.Component.HSV_S,
payload: (prev) => prev - 0.5, payload: (prev) => prev - 0.5,
}); });
expectHSV(nextState.color.hsv, nextColor); expectEqualColor(nextState.color.hsv, nextColor);
}); });
test("adjust hsv v", () => { test("adjust hsv v", () => {
@@ -294,7 +261,7 @@ describe("color reducer", () => {
component: colorlib.Component.HSV_V, component: colorlib.Component.HSV_V,
payload: (prev) => prev + 1, payload: (prev) => prev + 1,
}); });
expectHSV(nextState.color.hsv, nextColor); expectEqualColor(nextState.color.hsv, nextColor);
nextColor = colorlib.HSV.new(0, 0, 0.5); nextColor = colorlib.HSV.new(0, 0, 0.5);
nextState = colorReducer(nextState, { nextState = colorReducer(nextState, {
@@ -302,7 +269,7 @@ describe("color reducer", () => {
component: colorlib.Component.HSV_V, component: colorlib.Component.HSV_V,
payload: (prev) => prev - 0.5, payload: (prev) => prev - 0.5,
}); });
expectHSV(nextState.color.hsv, nextColor); expectEqualColor(nextState.color.hsv, nextColor);
}); });
}); });
@@ -314,7 +281,7 @@ describe("color reducer", () => {
component: colorlib.Component.HCL_H, component: colorlib.Component.HCL_H,
payload: (prev) => prev + 100, payload: (prev) => prev + 100,
}); });
expectHCL(nextState.color.hcl, nextColor); expectEqualColor(nextState.color.hcl, nextColor);
nextColor = colorlib.HCL.new(50, 0, 0); nextColor = colorlib.HCL.new(50, 0, 0);
nextState = colorReducer(nextState, { nextState = colorReducer(nextState, {
@@ -322,7 +289,7 @@ describe("color reducer", () => {
component: colorlib.Component.HCL_H, component: colorlib.Component.HCL_H,
payload: (prev) => prev - 50, payload: (prev) => prev - 50,
}); });
expectHCL(nextState.color.hcl, nextColor); expectEqualColor(nextState.color.hcl, nextColor);
}); });
test("adjust hcl c", () => { test("adjust hcl c", () => {
@@ -332,7 +299,7 @@ describe("color reducer", () => {
component: colorlib.Component.HCL_C, component: colorlib.Component.HCL_C,
payload: (prev) => prev + 1, payload: (prev) => prev + 1,
}); });
expectHCL(nextState.color.hcl, nextColor); expectEqualColor(nextState.color.hcl, nextColor);
nextColor = colorlib.HCL.new(0, 0.5, 0); nextColor = colorlib.HCL.new(0, 0.5, 0);
nextState = colorReducer(nextState, { nextState = colorReducer(nextState, {
@@ -340,7 +307,7 @@ describe("color reducer", () => {
component: colorlib.Component.HCL_C, component: colorlib.Component.HCL_C,
payload: (prev) => prev - 0.5, payload: (prev) => prev - 0.5,
}); });
expectHCL(nextState.color.hcl, nextColor); expectEqualColor(nextState.color.hcl, nextColor);
}); });
test("adjust hcl l", () => { test("adjust hcl l", () => {
@@ -350,7 +317,7 @@ describe("color reducer", () => {
component: colorlib.Component.HCL_L, component: colorlib.Component.HCL_L,
payload: (prev) => prev + 1, payload: (prev) => prev + 1,
}); });
expectHCL(nextState.color.hcl, nextColor); expectEqualColor(nextState.color.hcl, nextColor);
nextColor = colorlib.HCL.new(0, 0, 0.5); nextColor = colorlib.HCL.new(0, 0, 0.5);
nextState = colorReducer(nextState, { nextState = colorReducer(nextState, {
@@ -358,7 +325,7 @@ describe("color reducer", () => {
component: colorlib.Component.HCL_L, component: colorlib.Component.HCL_L,
payload: (prev) => prev - 0.5, payload: (prev) => prev - 0.5,
}); });
expectHCL(nextState.color.hcl, nextColor); expectEqualColor(nextState.color.hcl, nextColor);
}); });
}); });
}); });
@@ -388,19 +355,19 @@ describe("color actions", () => {
test("set rgb", () => { test("set rgb", () => {
const nextColor = colorlib.RGB.new(1, 2, 3); const nextColor = colorlib.RGB.new(1, 2, 3);
actions.rgb.setRGB(nextColor); actions.rgb.setRGB(nextColor);
expectRGB(state.color.rgb, nextColor); expectEqualColor(state.color.rgb, nextColor);
}); });
test("set hsv", () => { test("set hsv", () => {
const nextColor = colorlib.HSV.new(1, 2, 3); const nextColor = colorlib.HSV.new(1, 2, 3);
actions.hsv.setHSV(nextColor); actions.hsv.setHSV(nextColor);
expectHSV(state.color.hsv, nextColor); expectEqualColor(state.color.hsv, nextColor);
}); });
test("set hcl", () => { test("set hcl", () => {
const nextColor = colorlib.HCL.new(1, 2, 3); const nextColor = colorlib.HCL.new(1, 2, 3);
actions.hcl.setHCL(nextColor); actions.hcl.setHCL(nextColor);
expectHCL(state.color.hcl, nextColor); expectEqualColor(state.color.hcl, nextColor);
}); });
test("set hex", () => { test("set hex", () => {
@@ -415,19 +382,19 @@ describe("color actions", () => {
test("set rgb r", () => { test("set rgb r", () => {
const nextColor = colorlib.RGB.new(100, 0, 0); const nextColor = colorlib.RGB.new(100, 0, 0);
actions.rgb.setR(nextColor.r); actions.rgb.setR(nextColor.r);
expectRGB(state.color.rgb, nextColor); expectEqualColor(state.color.rgb, nextColor);
}); });
test("set rgb g", () => { test("set rgb g", () => {
const nextColor = colorlib.RGB.new(0, 100, 0); const nextColor = colorlib.RGB.new(0, 100, 0);
actions.rgb.setG(nextColor.g); actions.rgb.setG(nextColor.g);
expectRGB(state.color.rgb, nextColor); expectEqualColor(state.color.rgb, nextColor);
}); });
test("set rgb b", () => { test("set rgb b", () => {
const nextColor = colorlib.RGB.new(0, 0, 100); const nextColor = colorlib.RGB.new(0, 0, 100);
actions.rgb.setB(nextColor.b); actions.rgb.setB(nextColor.b);
expectRGB(state.color.rgb, nextColor); expectEqualColor(state.color.rgb, nextColor);
}); });
}); });
@@ -435,19 +402,19 @@ describe("color actions", () => {
test("set hsv h", () => { test("set hsv h", () => {
const nextColor = colorlib.HSV.new(100, 0, 0); const nextColor = colorlib.HSV.new(100, 0, 0);
actions.hsv.setH(nextColor.h); actions.hsv.setH(nextColor.h);
expectHSV(state.color.hsv, nextColor); expectEqualColor(state.color.hsv, nextColor);
}); });
test("set hsv s", () => { test("set hsv s", () => {
const nextColor = colorlib.HSV.new(0, 0.5, 0); const nextColor = colorlib.HSV.new(0, 0.5, 0);
actions.hsv.setS(nextColor.s); actions.hsv.setS(nextColor.s);
expectHSV(state.color.hsv, nextColor); expectEqualColor(state.color.hsv, nextColor);
}); });
test("set hsv v", () => { test("set hsv v", () => {
const nextColor = colorlib.HSV.new(0, 0, 0.5); const nextColor = colorlib.HSV.new(0, 0, 0.5);
actions.hsv.setV(nextColor.v); actions.hsv.setV(nextColor.v);
expectHSV(state.color.hsv, nextColor); expectEqualColor(state.color.hsv, nextColor);
}); });
}); });
@@ -455,19 +422,19 @@ describe("color actions", () => {
test("set hcl h", () => { test("set hcl h", () => {
const nextColor = colorlib.HCL.new(100, 0, 0); const nextColor = colorlib.HCL.new(100, 0, 0);
actions.hcl.setH(nextColor.h); actions.hcl.setH(nextColor.h);
expectHCL(state.color.hcl, nextColor); expectEqualColor(state.color.hcl, nextColor);
}); });
test("set hcl c", () => { test("set hcl c", () => {
const nextColor = colorlib.HCL.new(0, 0.5, 0); const nextColor = colorlib.HCL.new(0, 0.5, 0);
actions.hcl.setC(nextColor.c); actions.hcl.setC(nextColor.c);
expectHCL(state.color.hcl, nextColor); expectEqualColor(state.color.hcl, nextColor);
}); });
test("set hcl l", () => { test("set hcl l", () => {
const nextColor = colorlib.HCL.new(0, 0, 0.5); const nextColor = colorlib.HCL.new(0, 0, 0.5);
actions.hcl.setL(nextColor.l); actions.hcl.setL(nextColor.l);
expectHCL(state.color.hcl, nextColor); expectEqualColor(state.color.hcl, nextColor);
}); });
}); });
}); });
@@ -477,31 +444,31 @@ describe("color actions", () => {
test("adjust rgb r", () => { test("adjust rgb r", () => {
let nextColor = colorlib.RGB.new(100, 0, 0); let nextColor = colorlib.RGB.new(100, 0, 0);
actions.rgb.setR((prev) => prev + 100); actions.rgb.setR((prev) => prev + 100);
expectRGB(state.color.rgb, nextColor); expectEqualColor(state.color.rgb, nextColor);
nextColor = colorlib.RGB.new(50, 0, 0); nextColor = colorlib.RGB.new(50, 0, 0);
actions.rgb.setR((prev) => prev - 50); actions.rgb.setR((prev) => prev - 50);
expectRGB(state.color.rgb, nextColor); expectEqualColor(state.color.rgb, nextColor);
}); });
test("adjust rgb g", () => { test("adjust rgb g", () => {
let nextColor = colorlib.RGB.new(0, 100, 0); let nextColor = colorlib.RGB.new(0, 100, 0);
actions.rgb.setG((prev) => prev + 100); actions.rgb.setG((prev) => prev + 100);
expectRGB(state.color.rgb, nextColor); expectEqualColor(state.color.rgb, nextColor);
nextColor = colorlib.RGB.new(0, 50, 0); nextColor = colorlib.RGB.new(0, 50, 0);
actions.rgb.setG((prev) => prev - 50); actions.rgb.setG((prev) => prev - 50);
expectRGB(state.color.rgb, nextColor); expectEqualColor(state.color.rgb, nextColor);
}); });
test("adjust rgb b", () => { test("adjust rgb b", () => {
let nextColor = colorlib.RGB.new(0, 0, 100); let nextColor = colorlib.RGB.new(0, 0, 100);
actions.rgb.setB((prev) => prev + 100); actions.rgb.setB((prev) => prev + 100);
expectRGB(state.color.rgb, nextColor); expectEqualColor(state.color.rgb, nextColor);
nextColor = colorlib.RGB.new(0, 0, 50); nextColor = colorlib.RGB.new(0, 0, 50);
actions.rgb.setB((prev) => prev - 50); actions.rgb.setB((prev) => prev - 50);
expectRGB(state.color.rgb, nextColor); expectEqualColor(state.color.rgb, nextColor);
}); });
}); });
@@ -509,31 +476,31 @@ describe("color actions", () => {
test("adjust hsv h", () => { test("adjust hsv h", () => {
let nextColor = colorlib.HSV.new(100, 0, 0); let nextColor = colorlib.HSV.new(100, 0, 0);
actions.hsv.setH((prev) => prev + 100); actions.hsv.setH((prev) => prev + 100);
expectHSV(state.color.hsv, nextColor); expectEqualColor(state.color.hsv, nextColor);
nextColor = colorlib.HSV.new(50, 0, 0); nextColor = colorlib.HSV.new(50, 0, 0);
actions.hsv.setH((prev) => prev - 50); actions.hsv.setH((prev) => prev - 50);
expectHSV(state.color.hsv, nextColor); expectEqualColor(state.color.hsv, nextColor);
}); });
test("adjust hsv s", () => { test("adjust hsv s", () => {
let nextColor = colorlib.HSV.new(0, 1, 0); let nextColor = colorlib.HSV.new(0, 1, 0);
actions.hsv.setS((prev) => prev + 1); actions.hsv.setS((prev) => prev + 1);
expectHSV(state.color.hsv, nextColor); expectEqualColor(state.color.hsv, nextColor);
nextColor = colorlib.HSV.new(0, 0.5, 0); nextColor = colorlib.HSV.new(0, 0.5, 0);
actions.hsv.setS((prev) => prev - 0.5); actions.hsv.setS((prev) => prev - 0.5);
expectHSV(state.color.hsv, nextColor); expectEqualColor(state.color.hsv, nextColor);
}); });
test("adjust hsv v", () => { test("adjust hsv v", () => {
let nextColor = colorlib.HSV.new(0, 0, 1); let nextColor = colorlib.HSV.new(0, 0, 1);
actions.hsv.setV((prev) => prev + 1); actions.hsv.setV((prev) => prev + 1);
expectHSV(state.color.hsv, nextColor); expectEqualColor(state.color.hsv, nextColor);
nextColor = colorlib.HSV.new(0, 0, 0.5); nextColor = colorlib.HSV.new(0, 0, 0.5);
actions.hsv.setV((prev) => prev - 0.5); actions.hsv.setV((prev) => prev - 0.5);
expectHSV(state.color.hsv, nextColor); expectEqualColor(state.color.hsv, nextColor);
}); });
}); });
@@ -541,31 +508,31 @@ describe("color actions", () => {
test("adjust hcl h", () => { test("adjust hcl h", () => {
let nextColor = colorlib.HCL.new(100, 0, 0); let nextColor = colorlib.HCL.new(100, 0, 0);
actions.hcl.setH((prev) => prev + 100); actions.hcl.setH((prev) => prev + 100);
expectHCL(state.color.hcl, nextColor); expectEqualColor(state.color.hcl, nextColor);
nextColor = colorlib.HCL.new(50, 0, 0); nextColor = colorlib.HCL.new(50, 0, 0);
actions.hcl.setH((prev) => prev - 50); actions.hcl.setH((prev) => prev - 50);
expectHCL(state.color.hcl, nextColor); expectEqualColor(state.color.hcl, nextColor);
}); });
test("adjust hcl c", () => { test("adjust hcl c", () => {
let nextColor = colorlib.HCL.new(0, 1, 0); let nextColor = colorlib.HCL.new(0, 1, 0);
actions.hcl.setC((prev) => prev + 1); actions.hcl.setC((prev) => prev + 1);
expectHCL(state.color.hcl, nextColor); expectEqualColor(state.color.hcl, nextColor);
nextColor = colorlib.HCL.new(0, 0.5, 0); nextColor = colorlib.HCL.new(0, 0.5, 0);
actions.hcl.setC((prev) => prev - 0.5); actions.hcl.setC((prev) => prev - 0.5);
expectHCL(state.color.hcl, nextColor); expectEqualColor(state.color.hcl, nextColor);
}); });
test("adjust hcl l", () => { test("adjust hcl l", () => {
let nextColor = colorlib.HCL.new(0, 0, 1); let nextColor = colorlib.HCL.new(0, 0, 1);
actions.hcl.setL((prev) => prev + 1); actions.hcl.setL((prev) => prev + 1);
expectHCL(state.color.hcl, nextColor); expectEqualColor(state.color.hcl, nextColor);
nextColor = colorlib.HCL.new(0, 0, 0.5); nextColor = colorlib.HCL.new(0, 0, 0.5);
actions.hcl.setL((prev) => prev - 0.5); actions.hcl.setL((prev) => prev - 0.5);
expectHCL(state.color.hcl, nextColor); expectEqualColor(state.color.hcl, nextColor);
}); });
}); });
}); });
+4 -4
View File
@@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import type { CartesianSpace } from "@/types"; import type { CartesianSpace } from "@/types";
import { valueToPosition } from "@/util"; import { valueToPosition } from "@/util";
@@ -12,8 +12,8 @@ function TestSquare() {
const [dimensions, setDimensions] = useState<CartesianSpace>({ x: 0, y: 0 }); const [dimensions, setDimensions] = useState<CartesianSpace>({ x: 0, y: 0 });
const [xValue, setXValue] = useState(0); const [xValue, setXValue] = useState(0);
const [yValue, setYValue] = useState(0); const [yValue, setYValue] = useState(0);
const xValueRange = { min: 0, max: 100 }; const xValueRange = useMemo(() => ({ min: 0, max: 100 }), []);
const yValueRange = { min: 0, max: 100 }; const yValueRange = useMemo(() => ({ min: 0, max: 100 }), []);
const [xPosition, setXPosition] = useState(0); const [xPosition, setXPosition] = useState(0);
const [yPosition, setYPosition] = useState(0); const [yPosition, setYPosition] = useState(0);
@@ -46,7 +46,7 @@ function TestSquare() {
const newYPos = valueToPosition(yValue, dimensions.y - 1, yValueRange); const newYPos = valueToPosition(yValue, dimensions.y - 1, yValueRange);
setXPosition(newXPos); setXPosition(newXPos);
setYPosition(newYPos); setYPosition(newYPos);
}, [xValue, yValue]); }, [xValue, yValue, dimensions.x, dimensions.y, xValueRange, yValueRange]);
return ( return (
<> <>
+1
View File
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { useState } from "react"; import { useState } from "react";
import clsx from "clsx"; import clsx from "clsx";
+52
View File
@@ -0,0 +1,52 @@
import { Hex as HexColor } from "colorlib";
import { beforeEach, describe, expect, test } from "vitest";
import { mockUseReducer } from "@/testUtil";
import type {
PaletteCard,
PaletteCardAction,
PaletteCardActions,
PaletteCardState,
} from "../paletteCard";
import { createPaletteCardActions, paletteCardReducer } from "../paletteCard";
const createPaletteState = (
present: PaletteCard,
history: PaletteCard[] = [],
future: PaletteCard[] = [],
) => ({ present: { ...present }, history, future });
const testPaletteCard = {
id: "palette_id",
name: "Test Palette",
colors: [],
selectedColorId: null,
inToolkitMode: false,
};
const testState = createPaletteState(testPaletteCard);
const WHITE = HexColor.from_code("#fff");
const GREY = HexColor.from_code("#777");
const BLACK = HexColor.from_code("#000");
describe("palette card actions", () => {
let state: PaletteCardState;
let dispatch: (value: PaletteCardAction) => void;
let actions: PaletteCardActions;
beforeEach(() => {
[state, dispatch] = mockUseReducer(paletteCardReducer, testState);
actions = createPaletteCardActions(dispatch);
});
test("sets card name", () => {
actions.setCardName("New Name");
expect(state.present.name).toBe("New Name");
expect(state.history.length).toBe(1);
expect(state.future.length).toBe(0);
expect(state.history[0].name).toBe("Test Palette");
});
});
+3 -3
View File
@@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import type { CartesianSpace } from "@/types"; import type { CartesianSpace } from "@/types";
import { Direction } from "@/types"; import { Direction } from "@/types";
@@ -17,7 +17,7 @@ function TestSlider({
const [dimensions, setDimensions] = useState<CartesianSpace>({ x: 0, y: 0 }); const [dimensions, setDimensions] = useState<CartesianSpace>({ x: 0, y: 0 });
const [value, setValue] = useState(0); const [value, setValue] = useState(0);
const [position, setPosition] = useState(0); const [position, setPosition] = useState(0);
const valueRange = { min: 0, max: 100 }; const valueRange = useMemo(() => ({ min: 0, max: 100 }), []);
const { sliderRef, isDragging } = useSlider({ const { sliderRef, isDragging } = useSlider({
direction, direction,
origin, origin,
@@ -51,7 +51,7 @@ function TestSlider({
} else { } else {
setValue(0); setValue(0);
} }
}, [dimensions, direction]); }, [dimensions, direction, position]);
useEffect(() => { useEffect(() => {
const maxPosition = chooseValueByDirection( const maxPosition = chooseValueByDirection(
+1 -1
View File
@@ -1,4 +1,4 @@
export function useResize(callback: () => void): () => void { export function onResize(callback: () => void): () => void {
window.addEventListener("resize", callback); window.addEventListener("resize", callback);
return () => window.removeEventListener("resize", callback); return () => window.removeEventListener("resize", callback);
} }
+2 -17
View File
@@ -1,22 +1,7 @@
import { createContext, useEffect, useState } from "react"; import { useEffect, useState } from "react";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
enum ViewportMode { import { MediaQueryContext, ViewportMode } from "./context";
DESKTOP = "desktop",
MOBILE_LANDSCAPE = "mobile-landscape",
MOBILE_PORTRAIT = "mobile-portrait",
}
interface MediaQueryContextType {
viewportMode: ViewportMode;
isDesktop: boolean;
isMobileLandscape: boolean;
isMobilePortrait: boolean;
}
export const MediaQueryContext = createContext<
MediaQueryContextType | undefined
>(undefined);
export const MediaQueryProvider = ({ children }: { children: ReactNode }) => { export const MediaQueryProvider = ({ children }: { children: ReactNode }) => {
const [viewportMode, setViewportMode] = useState<ViewportMode>( const [viewportMode, setViewportMode] = useState<ViewportMode>(
+2 -10
View File
@@ -1,19 +1,11 @@
import { createContext, useReducer } from "react"; import { useReducer } from "react";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import * as colorlib from "colorlib"; import * as colorlib from "colorlib";
import { colorReducer, createColorActions } from "@/hooks/color"; import { colorReducer, createColorActions } from "@/hooks/color";
import type { ColorActions } from "@/hooks/color";
interface SelectedColorContextType { import { SelectedColorContext } from "./context";
selectedColor: colorlib.Color;
selectedColorActions: ColorActions;
}
export const SelectedColorContext = createContext<
SelectedColorContextType | undefined
>(undefined);
export const SelectedColorProvider = ({ export const SelectedColorProvider = ({
children, children,
+31
View File
@@ -0,0 +1,31 @@
import { createContext } from "react";
import * as colorlib from "colorlib";
import type { ColorActions } from "@/hooks/color";
export enum ViewportMode {
DESKTOP = "desktop",
MOBILE_LANDSCAPE = "mobile-landscape",
MOBILE_PORTRAIT = "mobile-portrait",
}
interface MediaQueryContextType {
viewportMode: ViewportMode;
isDesktop: boolean;
isMobileLandscape: boolean;
isMobilePortrait: boolean;
}
export const MediaQueryContext = createContext<
MediaQueryContextType | undefined
>(undefined);
interface SelectedColorContextType {
selectedColor: colorlib.Color;
selectedColorActions: ColorActions;
}
export const SelectedColorContext = createContext<
SelectedColorContextType | undefined
>(undefined);
+46
View File
@@ -0,0 +1,46 @@
import { Color, HCL, HSV, Hex, RGB } from "colorlib";
import { expect } from "vitest";
export const mockUseReducer = <T extends object, U>(
reducer: (state: T, action: U) => T,
initialArg: T,
): [T, (value: U) => void] => {
let currentState = initialArg;
const state = new Proxy({} as T, {
get: (_, prop) => currentState[prop as keyof T],
});
const dispatch = (value: U) => {
const nextState = reducer(currentState, value);
currentState = nextState;
};
return [state, dispatch];
};
export const expectEqualColor = <T extends { equals(other: T): boolean }>(
value: T,
expected: T,
) => {
if (!value.equals(expected)) {
if (value instanceof Color && expected instanceof Color) {
expect(value.hex.to_code()).toBe(expected.hex.to_code());
} else if (value instanceof HCL && expected instanceof HCL) {
expect(value.h).toBe(expected.h);
expect(value.c).toBe(expected.c);
expect(value.l).toBe(expected.l);
} else if (value instanceof HSV && expected instanceof HSV) {
expect(value.h).toBe(expected.h);
expect(value.s).toBe(expected.s);
expect(value.v).toBe(expected.v);
} else if (
(value instanceof RGB && expected instanceof RGB) ||
(value instanceof Hex && expected instanceof Hex)
) {
expect(value.r).toBe(expected.r);
expect(value.g).toBe(expected.g);
expect(value.b).toBe(expected.b);
}
}
expect(value.equals(expected)).toBe(true);
};
+6
View File
@@ -1,9 +1,15 @@
import * as path from "path";
import topLevelAwait from "vite-plugin-top-level-await"; import topLevelAwait from "vite-plugin-top-level-await";
import wasm from "vite-plugin-wasm"; import wasm from "vite-plugin-wasm";
import { defineConfig } from "vitest/config"; import { defineConfig } from "vitest/config";
export default defineConfig({ export default defineConfig({
plugins: [wasm(), topLevelAwait()], plugins: [wasm(), topLevelAwait()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
test: { test: {
environment: "jsdom", environment: "jsdom",
}, },