diff --git a/.gitignore b/.gitignore index 3c3629e..f06235c 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ node_modules +dist diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index 177a868..c36daa9 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -53,6 +53,19 @@ Cypress.Commands.add("disableTransitions", () => { } `; document.head.appendChild(style); + + // Flag to disable Framer Motion + if (window.framerMotionTestOverride) return; + window.framerMotionTestOverride = true; + window.originalRequestAnimationFrame = window.requestAnimationFrame; + + window.requestAnimationFrame = (callback) => { + return window.setTimeout(() => { + if (callback) callback(0); + }, 0); + }; + + document.body.setAttribute("data-cy-animations-disabled", "true"); }); }); @@ -62,5 +75,17 @@ Cypress.Commands.add("enableTransitions", () => { if (styleElement) { styleElement.remove(); } + + // Remove flags for Framer Motion + if (window.framerMotionTestOverride) { + window.framerMotionTestOverride = false; + + if (window.originalRequestAnimationFrame) { + window.requestAnimationFrame = window.originalRequestAnimationFrame; + delete window.originalRequestAnimationFrame; + } + } + + document.body.removeAttribute("data-cy-animations-disabled"); }); }); diff --git a/index.html b/index.html index cfdbbba..a98b290 100644 --- a/index.html +++ b/index.html @@ -2,7 +2,7 @@ - + Luminance diff --git a/public/favicon.png b/public/favicon.png new file mode 100644 index 0000000..219898b Binary files /dev/null and b/public/favicon.png differ diff --git a/public/vite.svg b/public/vite.svg deleted file mode 100644 index e7b8dfb..0000000 --- a/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/App.module.css b/src/App.module.css index f24bebd..be19978 100644 --- a/src/App.module.css +++ b/src/App.module.css @@ -1,17 +1,26 @@ .appWrapper { background-color: white; height: 100%; - width: 100%; - max-width: 1200px; - overflow: hidden; + width: 1200px; margin: 0 auto; box-shadow: 0 0 40px #7a7a7a; border-left: 2px solid #7a7a7a; border-right: 2px solid #7a7a7a; + overflow: hidden; +} + +.mainLayout { + height: 100%; + display: grid; + grid-template-areas: + "header header" + "picker palette"; + grid-template-columns: 1fr 2fr; + grid-template-rows: 76px 1fr; } .appHeader { - height: 40px; + grid-area: header; display: flex; align-items: baseline; border-bottom: 2px solid #7a7a7a; @@ -39,48 +48,36 @@ color: #7a7a7a; } -.mobileContent, -.mainContent, -.tabWrapper, -.tabWrapper .tab { - height: 100%; -} - -.tabWrapper { - /* hide scrollbar */ - -ms-overflow-style: none; - scrollbar-width: none; -} - -.appWrapper { -} - -.mobileContent { - display: none; -} - -.mainContent { - display: grid; - grid-template-columns: 1fr 2fr; -} - .firstZone { + grid-area: picker; display: flex; flex-direction: column; border-right: 2px solid #7a7a7a; } .secondZone { - padding: 40px; + min-width: 0; + grid-area: palette; color: #555; font-style: italic; } +.colorHistoryWrapper { + box-sizing: border-box; + border-bottom: 2px solid #7a7a7a; + position: relative; +} + .colorPickerWrapper { border-bottom: 2px solid #7a7a7a; + padding: 20px 40px 40px; } .colorValuesWrapper { + padding: 40px; +} + +.colorHistoryWrapper { } .paletteEditorWrapper { diff --git a/src/App.tsx b/src/App.tsx index 2562728..f8ef1c5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,6 +3,7 @@ import { useState } from "react"; import clsx from "clsx"; import { Color } from "colorlib"; +import ColorHistory from "@/components/ColorHistory/ColorHistory"; import ColorPicker from "@/components/ColorPicker/ColorPicker"; import ColorValues from "@/components/ColorValues/ColorValues"; import { LeftMenu, RightMenu } from "@/components/SideMenu"; @@ -193,10 +194,6 @@ function MobileContent({ // Desktop Layout Components -function TitleZone() { - return
; -} - function FirstZone() { const { selectedColor, selectedColorActions } = useSelectedColor(); @@ -205,7 +202,6 @@ function FirstZone() {
-
@@ -214,9 +210,17 @@ function FirstZone() { } function SecondZone() { + const { selectedColor, selectedColorActions } = useSelectedColor(); + return (
- Palette Creator Coming Soon. +
+ +
+
LUMINANCE A color picker for humans.
-
- - -
- + + +
); } diff --git a/src/components/ColorHistory/ColorHistory.module.css b/src/components/ColorHistory/ColorHistory.module.css new file mode 100644 index 0000000..365f005 --- /dev/null +++ b/src/components/ColorHistory/ColorHistory.module.css @@ -0,0 +1,48 @@ +.colorHistory { + height: 74px; + display: flex; + flex-wrap: nowrap; + overflow-x: auto; + width: 100%; + padding: 5px 0px 0px; + + /* Improve scrolling experience */ + -webkit-overflow-scrolling: touch; /* Smooth scrolling on iOS */ + scrollbar-width: thin; /* For Firefox */ +} + +.historyColor { + flex: 0 0 auto; + display: inline-block; + height: 50px; + width: 25px; + margin: 5px; + border: 2px solid #7a7a7a; + transition: + margin 200ms, + height 200ms, + width 200ms; + cursor: pointer; +} + +.historyColor:hover { + height: 56px; + width: 31px; + margin: 2px; +} + +.historyColor:first-of-type:hover { + margin-left: 12px; +} + +.historyColor:first-of-type { + margin-left: 15px; +} + +.historyColor:last-of-type:hover { + margin-right: 12px; +} + +.historyColor:last-of-type { + margin-right: 15px; +} diff --git a/src/components/ColorHistory/ColorHistory.tsx b/src/components/ColorHistory/ColorHistory.tsx new file mode 100644 index 0000000..1915830 --- /dev/null +++ b/src/components/ColorHistory/ColorHistory.tsx @@ -0,0 +1,80 @@ +import { useEffect, useState } from "react"; + +import { Color } from "colorlib"; +import { motion } from "motion/react"; + +import type { Timeout } from "@/types"; +import { formatCssRgb } from "@/util"; + +import styles from "./ColorHistory.module.css"; + +function ColorHistory({ + color, + setColor, + disabled, +}: { + color: Color; + setColor: (newColor: Color) => void; + disabled: boolean; +}) { + const [history, setHistory] = useState([]); + const maxItems = 50; + + useEffect(() => { + if (disabled) return; + + const timer: Timeout = setTimeout(() => { + setHistory((prev) => { + if (prev.length > 0 && prev[0].hex.to_code() === color.hex.to_code()) + return prev; + + const newHistory = [color, ...prev]; + return newHistory.slice(0, maxItems); + }); + }, 1000); + + return () => clearTimeout(timer); + }, [color, disabled]); + + const handleClick = (historyColor: Color) => { + setColor(historyColor); + }; + + // Manage motion props for testing + const isTestEnvironment = + typeof window !== "undefined" && window.framerMotionTestOverride === true; + + const getAnimationProps = (isFirst: boolean) => { + return isTestEnvironment + ? { + initial: false, + animate: {}, + exit: {}, + ease: null, + transition: { duration: 0 }, + } + : { + initial: { opacity: isFirst ? 0 : 1, x: -20 }, + animate: { opacity: 1, x: 0 }, + exit: { opacity: 0 }, + ease: "easeInOut", + transition: { duration: 0.3 }, + }; + }; + return ( +
+ {history.map((historyColor, index) => ( + handleClick(historyColor)} + {...getAnimationProps(index === 0)} + > + ))} +
+ ); +} + +export default ColorHistory; diff --git a/src/components/ColorHistory/ColorHistoryTest.cy.tsx b/src/components/ColorHistory/ColorHistoryTest.cy.tsx new file mode 100644 index 0000000..442f29a --- /dev/null +++ b/src/components/ColorHistory/ColorHistoryTest.cy.tsx @@ -0,0 +1,95 @@ +import { useReducer, useState } from "react"; +import type { ChangeEvent } from "react"; + +import { Color } from "colorlib"; + +import { colorReducer, createColorActions } from "@/hooks/color"; + +import { HexEditor } from "../ColorValues/ValueEditor"; +import ColorHistory from "./ColorHistory"; + +const initialState = { + color: Color.from_hex("000"), +}; + +function TestWrapper() { + const [state, dispatch] = useReducer(colorReducer, initialState); + const actions = createColorActions(dispatch); + + const [disabled, setDisabled] = useState(false); + const handleDisabledChange = (e: ChangeEvent) => + setDisabled(e.target.checked); + + return ( +
+ + + +
+ ); +} + +describe("color history", () => { + beforeEach(() => { + cy.disableTransitions(); + cy.clock(); + cy.mount(); + }); + + afterEach(() => { + cy.clock().then((clock) => clock.restore()); + cy.enableTransitions(); + }); + + it("adds stable color values after 1 second", () => { + // add stable values to history + cy.dataCy("hex-value-input").as("value").clear().type("#00F536"); + cy.tick(1000); + + cy.dataCy("color-history").children().should("have.length", 1); + cy.dataCy("history-color-0").should( + "have.css", + "background-color", + "rgb(0, 245, 54)", + ); + + cy.get("@value").clear().type("#E23AEC"); + cy.tick(1000); + + cy.dataCy("color-history").children().should("have.length", 2); + cy.dataCy("history-color-0").should( + "have.css", + "background-color", + "rgb(226, 58, 236)", + ); + + // click to restore value + cy.dataCy("history-color-1").click(); + cy.get("@value").should("have.value", "#00F536"); + + // disable history + cy.dataCy("disabled-checkbox").click(); + cy.get("@value").clear().type("#00C3EE"); + cy.tick(1000); + + cy.dataCy("color-history").children().should("have.length", 2); + + // re-enable history + cy.dataCy("disabled-checkbox").click(); + cy.tick(1000); + + cy.dataCy("color-history").children().should("have.length", 3); + }); +}); diff --git a/src/components/ColorPicker/ColorBar.tsx b/src/components/ColorPicker/ColorBar.tsx new file mode 100644 index 0000000..8d689fe --- /dev/null +++ b/src/components/ColorPicker/ColorBar.tsx @@ -0,0 +1,133 @@ +import { useEffect, useRef, useState } from "react"; + +import * as colorlib from "colorlib"; +import { memory } from "colorlib/colorlib_bg.wasm"; + +import { useSmoothAnimation } from "@/hooks/animation"; +import type { Setter } from "@/hooks/color"; +import { useSlider } from "@/hooks/slider"; +import { useResize } from "@/hooks/window"; +import type { CartesianSpace } from "@/types"; +import { Direction } from "@/types"; +import { setMeasurements } from "@/util"; + +import styles from "./ColorPicker.module.css"; + +function ColorBar({ + hue, + chroma, + luminance, + setChroma, + parentDimensions, +}: { + hue: number; + chroma: number; + luminance: number; + setChroma: Setter; + parentDimensions: CartesianSpace; +}) { + // State + const [colorBar, setColorBar] = useState(null); + const [origin, setOrigin] = useState({ x: 0, y: 0 }); + const [dimensions, setDimensions] = useState({ x: 0, y: 0 }); + + // Refs + const containerRef = useRef(null); + const canvasRef = useRef(null); + + // Hooks + const smoothAnimation = useSmoothAnimation(); + + // Slider interaction + const { sliderRef } = useSlider({ + direction: Direction.HORIZONTAL, + origin, + dimensions, + valueRange: { min: 0, max: 1 }, + value: chroma, + setValue: setChroma, + }); + + // Update canvas when hue/luminance changes + useEffect(() => { + if (colorBar && canvasRef.current) { + smoothAnimation(() => { + colorBar.fill_color(hue, luminance); + refreshColorBar(canvasRef.current!, colorBar); + }); + } + }, [hue, luminance, colorBar]); + + // Get measurements + useEffect(() => { + if (containerRef.current) { + setMeasurements(containerRef, setOrigin, setDimensions); + } + + return useResize(() => + setMeasurements(containerRef, setOrigin, setDimensions), + ); + }, [containerRef.current]); + + // Resize color bar + useEffect(() => { + if (containerRef.current && canvasRef.current && parentDimensions.x > 0) { + const newHeight = containerRef.current.clientHeight; + const newWidth = parentDimensions.x - 54; + const newColorBar = new colorlib.ColorBar(newWidth, newHeight); + + setColorBar(newColorBar); + + if (newColorBar) { + smoothAnimation(() => { + if (canvasRef.current) { + newColorBar.fill_color(hue, luminance); + refreshColorBar(canvasRef.current!, newColorBar); + } + }); + } + } + }, [containerRef.current, canvasRef.current, parentDimensions]); + + return ( +
+
+ +
+
+ ); +} + +function refreshColorBar( + canvas: HTMLCanvasElement, + colorBar: colorlib.ColorBar, +) { + const ctx = canvas.getContext("2d"); + if (ctx) { + const width = colorBar.get_width(); + const height = colorBar.get_height(); + const imageData = ctx.createImageData(width, height); + const pixelPointer = colorBar.get_pixels_pointer(); + const pixels = new Uint8Array( + memory.buffer, + pixelPointer, + width * height * 4, + ); + + imageData.data.set(pixels); + ctx.putImageData(imageData, 0, 0); + } +} + +export default ColorBar; diff --git a/src/components/ColorPicker/ColorPicker.module.css b/src/components/ColorPicker/ColorPicker.module.css index cafc5a4..81fed13 100644 --- a/src/components/ColorPicker/ColorPicker.module.css +++ b/src/components/ColorPicker/ColorPicker.module.css @@ -1,5 +1,4 @@ .container { - padding: 40px; display: grid; grid-template-columns: 25px 1fr 25px; grid-template-rows: 50px 1fr 25px; @@ -9,6 +8,14 @@ ". bottomGrip ." ". bar ."; } + +.preview { + grid-area: preview; + height: 25px; + margin-bottom: 15px; + border: 2px solid #7a7a7a; +} + .pickerSquare { grid-area: square; position: relative; @@ -100,11 +107,3 @@ width: 0; height: 0; } - -/* Preview */ - -.preview { - grid-area: preview; - margin-bottom: 15px; - border: 2px solid #7a7a7a; -} diff --git a/src/components/ColorPicker/ColorPicker.tsx b/src/components/ColorPicker/ColorPicker.tsx index c07975b..866c2f1 100644 --- a/src/components/ColorPicker/ColorPicker.tsx +++ b/src/components/ColorPicker/ColorPicker.tsx @@ -55,6 +55,7 @@ function ColorPicker({ valueRange={lumRange} arrowDirection="right" invert={true} + parentDimensions={dimensions} />
@@ -62,6 +63,7 @@ function ColorPicker({ hue={color.hcl.h} luminance={color.hcl.l} hex={color.hex} + parentDimensions={dimensions} />
@@ -86,6 +89,7 @@ function ColorPicker({ setValue={actions.hcl.setH} valueRange={hueRange} arrowDirection="up" + parentDimensions={dimensions} />
@@ -100,6 +104,7 @@ function ColorPicker({ chroma={color.hcl.c} luminance={color.hcl.l} hex={color.hex} + parentDimensions={dimensions} />
diff --git a/src/components/ColorPicker/ColorPickerTest.cy.tsx b/src/components/ColorPicker/ColorPickerTest.cy.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/components/ColorPicker/ColorSquare.tsx b/src/components/ColorPicker/ColorSquare.tsx new file mode 100644 index 0000000..4679a25 --- /dev/null +++ b/src/components/ColorPicker/ColorSquare.tsx @@ -0,0 +1,137 @@ +import { useEffect, useRef, useState } from "react"; + +import * as colorlib from "colorlib"; +import { memory } from "colorlib/colorlib_bg.wasm"; + +import { useSmoothAnimation } from "@/hooks/animation"; +import type { HCLColorActions } from "@/hooks/color"; +import { useCrosshair } from "@/hooks/crosshair"; +import { useScroll } from "@/hooks/scroll"; +import { useResize } from "@/hooks/window"; +import type { CartesianSpace } from "@/types"; +import { setMeasurements } from "@/util"; + +import styles from "./ColorPicker.module.css"; + +function ColorSquare({ + chroma, + actions, + parentDimensions, +}: { + chroma: number; + actions: HCLColorActions; + parentDimensions: CartesianSpace; +}) { + // State + const [colorSquare, setColorSquare] = useState( + null, + ); + const [origin, setOrigin] = useState({ x: 0, y: 0 }); + const [dimensions, setDimensions] = useState({ x: 0, y: 0 }); + + // Refs + const containerRef = useRef(null); + const canvasRef = useRef(null); + + // Hooks + const smoothAnimation = useSmoothAnimation(); + + // Crosshair interaction + const { crosshairRef } = useCrosshair({ + origin, + dimensions, + setXValue: actions.setH, + setYValue: actions.setL, + xValueRange: { min: 0, max: 359 }, + yValueRange: { min: 0, max: 1 }, + invertY: true, + }); + + // Handle chroma adjustment with scroll + const { addScrollListener } = useScroll({ + targetRef: canvasRef, + onScrollUp: () => actions.setC((prev) => Math.min(1, prev + 0.01)), + onScrollDown: () => actions.setC((prev) => Math.max(0, prev - 0.01)), + }); + + // Update canvas when chroma changes + useEffect(() => { + if (colorSquare && canvasRef.current) { + smoothAnimation(() => { + colorSquare.fill_chroma(chroma); + refreshColorSquare(canvasRef.current!, colorSquare); + }); + } + }, [chroma, colorSquare]); + + // Add event listeners + useEffect(() => { + if (canvasRef.current) addScrollListener(); + }, []); + + // Get measurements + useEffect(() => { + if (containerRef.current) { + setMeasurements(containerRef, setOrigin, setDimensions); + } + + return useResize(() => + setMeasurements(containerRef, setOrigin, setDimensions), + ); + }, [containerRef.current, parentDimensions]); + + // Resize square + useEffect(() => { + if (containerRef.current && canvasRef.current && parentDimensions.x > 0) { + const newSize = parentDimensions.x - 54; + const newColorSquare = new colorlib.ColorSquare(newSize); + + setColorSquare(newColorSquare); + + if (newColorSquare) { + smoothAnimation(() => { + if (canvasRef.current) { + newColorSquare.fill_chroma(chroma); + refreshColorSquare(canvasRef.current, newColorSquare); + } + }); + } + } + }, [containerRef.current, canvasRef.current, parentDimensions]); + + return ( +
+
+ +
+
+ ); +} + +function refreshColorSquare( + canvas: HTMLCanvasElement, + colorSquare: colorlib.ColorSquare, +) { + const ctx = canvas.getContext("2d"); + if (ctx) { + const size = colorSquare.get_size(); + const imageData = ctx.createImageData(size, size); + const pixelPointer = colorSquare.get_pixels_pointer(); + const pixels = new Uint8Array(memory.buffer, pixelPointer, size * size * 4); + imageData.data.set(pixels); + ctx.putImageData(imageData, 0, 0); + } +} + +export default ColorSquare; diff --git a/src/components/ColorPicker/Crosshair.tsx b/src/components/ColorPicker/Crosshair.tsx new file mode 100644 index 0000000..8306ed2 --- /dev/null +++ b/src/components/ColorPicker/Crosshair.tsx @@ -0,0 +1,133 @@ +import { useEffect, useRef, useState } from "react"; + +import * as colorlib from "colorlib"; + +import { useResize } from "@/hooks/window"; +import type { CartesianSpace } from "@/types"; +import { formatCssRgb, setMeasurements, valueToPosition } from "@/util"; + +import styles from "./ColorPicker.module.css"; + +export function SquareCrosshair({ + hue, + luminance, + hex, + parentDimensions, + isDragging, +}: { + hue: number; + luminance: number; + hex: colorlib.Hex; + parentDimensions: CartesianSpace; + isDragging: boolean; +}) { + const [_origin, setOrigin] = useState({ x: 0, y: 0 }); + const [dimensions, setDimensions] = useState({ x: 0, y: 0 }); + const [darkCrosshairs, setDarkCrosshairs] = useState(true); + const containerRef = useRef(null); + const lumRange = { min: 0, max: 1 }; + const hueRange = { min: 0, max: 359 }; + + useEffect(() => { + setDarkCrosshairs(luminance > 0.5); + }, [luminance]); + + useEffect(() => { + setMeasurements(containerRef, setOrigin, setDimensions); + return useResize(() => + setMeasurements(containerRef, setOrigin, setDimensions), + ); + }, [containerRef.current, parentDimensions]); + + return ( +
+
+
+
+
+ ); +} + +export function BarCrosshair({ + chroma, + luminance, + hex, + parentDimensions, +}: { + chroma: number; + luminance: number; + hex: colorlib.Hex; + parentDimensions: CartesianSpace; +}) { + const [_origin, setOrigin] = useState({ x: 0, y: 0 }); + const [dimensions, setDimensions] = useState({ x: 0, y: 0 }); + const [darkCrosshairs, setDarkCrosshairs] = useState(true); + const containerRef = useRef(null); + const chromaRange = { min: 0, max: 1 }; + + useEffect(() => { + setDarkCrosshairs(luminance > 0.5); + }, [luminance]); + + useEffect(() => { + setMeasurements(containerRef, setOrigin, setDimensions); + return useResize(() => + setMeasurements(containerRef, setOrigin, setDimensions), + ); + }, [containerRef.current, parentDimensions]); + + return ( +
+
+
+
+ ); +} diff --git a/src/components/ColorPicker/GripSlider.tsx b/src/components/ColorPicker/GripSlider.tsx new file mode 100644 index 0000000..a3c22ed --- /dev/null +++ b/src/components/ColorPicker/GripSlider.tsx @@ -0,0 +1,116 @@ +import { useEffect, useState } from "react"; + +import type { Setter } from "@/hooks/color"; +import { useSlider } from "@/hooks/slider"; +import { useResize } from "@/hooks/window"; +import { Direction } from "@/types"; +import type { CartesianSpace, Range } from "@/types"; +import { + chooseValueByDirection, + setMeasurements, + valueToPosition, +} from "@/util"; + +import styles from "./ColorPicker.module.css"; + +function GripSlider({ + direction, + value, + setValue, + valueRange, + arrowDirection, + invert = false, + parentDimensions, +}: { + direction: Direction; + value: number; + setValue: Setter; + valueRange: Range; + arrowDirection: "up" | "left" | "right"; + invert?: boolean; + parentDimensions: CartesianSpace; +}) { + const [origin, setOrigin] = useState({ x: 0, y: 0 }); + const [dimensions, setDimensions] = useState({ x: 0, y: 0 }); + + // Slider interaction + const { sliderRef, isDragging } = useSlider({ + direction, + origin, + dimensions, + valueRange, + value, + setValue, + invert, + }); + + useEffect(() => { + if (sliderRef.current) { + setMeasurements(sliderRef, setOrigin, setDimensions); + } + + return useResize(() => + setMeasurements(sliderRef, setOrigin, setDimensions), + ); + }, [sliderRef.current, parentDimensions]); + + const upArrowStyle = { + borderLeft: "12px solid transparent", + borderRight: "12px solid transparent", + borderBottom: "25px solid black", + }; + + const leftArrowStyle = { + borderTop: "12px solid transparent", + borderBottom: "12px solid transparent", + borderRight: "25px solid black", + }; + + const rightArrowStyle = { + borderTop: "12px solid transparent", + borderBottom: "12px solid transparent", + borderLeft: "25px solid black", + }; + + const arrowStyle = (function () { + switch (arrowDirection) { + case "up": + return upArrowStyle; + case "left": + return leftArrowStyle; + case "right": + return rightArrowStyle; + default: + return {}; + } + })(); + + return ( +
+
+
+ ); +} + +export default GripSlider; diff --git a/src/components/ColorValues/ColorValues.module.css b/src/components/ColorValues/ColorValues.module.css index 45bdf14..671e67a 100644 --- a/src/components/ColorValues/ColorValues.module.css +++ b/src/components/ColorValues/ColorValues.module.css @@ -1,5 +1,5 @@ .colorValuesWrapper { - padding: 40px; + height: 100%; display: flex; flex-direction: column; gap: 16px; @@ -8,6 +8,7 @@ .spaceWrapper { display: flex; flex-direction: column; + max-height: 94px; flex: 1; min-height: 0; border: 2px solid #7a7a7a; @@ -17,7 +18,8 @@ display: flex; align-items: stretch; width: 100%; - height: 25px; + max-height: 25px; + min-height: 0; font-family: monospace; border-top: 1px solid #7a7a7a; border-bottom: 1px solid #7a7a7a; diff --git a/src/components/ColorValues/SpaceEditorTest.cy.tsx b/src/components/ColorValues/SpaceEditorTest.cy.tsx index 6c9c0c4..e7d9acc 100644 --- a/src/components/ColorValues/SpaceEditorTest.cy.tsx +++ b/src/components/ColorValues/SpaceEditorTest.cy.tsx @@ -20,9 +20,10 @@ function TestWrapper() {

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

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

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

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

@@ -75,7 +79,7 @@ describe("space editor tests", () => { 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("V-value-input").should("have.value", 86); }); cy.dataCy("HCL-editor").within(() => { @@ -85,7 +89,7 @@ describe("space editor tests", () => { }); 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("hsv-value").should("have.text", "HSV (158, 0.79, 0.86)"); cy.dataCy("hcl-value").should("have.text", "HCL (158, 0.79, 0.7)"); cy.dataCy("hex-value").should("have.text", "HEX: #2EDD9D"); @@ -97,9 +101,9 @@ describe("space editor tests", () => { cy.dataCy("L-value-input").type("25"); }); - cy.dataCy("rgb-value").should("have.text", "RGB (17, 76, 75)"); - 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("rgb-value").should("have.text", "RGB (16, 75, 74)"); + cy.dataCy("hsv-value").should("have.text", "HSV (178, 0.78, 0.29)"); + cy.dataCy("hcl-value").should("have.text", "HCL (178, 0.78, 0.25)"); cy.dataCy("hex-value").should("have.text", "HEX: #104B4A"); }); }); diff --git a/src/components/ColorValues/ValueEditor.tsx b/src/components/ColorValues/ValueEditor.tsx index 8c83bb9..fb85fa3 100644 --- a/src/components/ColorValues/ValueEditor.tsx +++ b/src/components/ColorValues/ValueEditor.tsx @@ -31,14 +31,14 @@ export function ValueEditor({ value, setValue, scale = 1, - onKeyDown, + onKeyDown = null, }: { componentSymbol: string; valueRange: Range; value: number; setValue: Setter; scale?: number; - onKeyDown?: (e: React.KeyboardEvent) => void; + onKeyDown?: ((e: React.KeyboardEvent) => void) | null; }) { // Set up component state const direction = Direction.HORIZONTAL; @@ -162,7 +162,7 @@ function Slider({ position: number; dimensions: CartesianSpace; componentSymbol: string; - onKeyDown?: (e: React.KeyboardEvent) => void; + onKeyDown?: ((e: React.KeyboardEvent) => void) | null; }) { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "Escape") { @@ -205,7 +205,7 @@ function Button({ direction: "increase" | "decrease"; componentSymbol: string; handleValueStep: (step: number) => void; - onKeyDown?: (e: React.KeyboardEvent) => void; + onKeyDown?: ((e: React.KeyboardEvent) => void) | null; }) { const isIncrease = direction === "increase"; const label = isIncrease ? "Increase" : "Decrease"; @@ -255,7 +255,7 @@ function Value({ componentSymbol: string; handleValueStep: (step: number) => void; scale: number; - onKeyDown?: (e: React.KeyboardEvent) => void; + onKeyDown?: ((e: React.KeyboardEvent) => void) | null; }) { const valueRef = useRef(null); const valueScroller = useScroll({ @@ -287,7 +287,7 @@ function Value({ e.target.select()} diff --git a/src/components/ColorValues/ValueEditorTest.cy.tsx b/src/components/ColorValues/ValueEditorTest.cy.tsx index f4b7304..dd3b068 100644 --- a/src/components/ColorValues/ValueEditorTest.cy.tsx +++ b/src/components/ColorValues/ValueEditorTest.cy.tsx @@ -18,8 +18,10 @@ function TestWrapper() {
{ cy.dataCy("R-slider") .click() .dataCy("R-slider-bar") - .should("have.css", "width", "140px") + .should("have.css", "width", "138px") .dataCy("R-value-input") .should("have.value", "127"); @@ -78,14 +80,14 @@ describe("component editor tests", () => { .type("100") .should("have.value", "100") .dataCy("R-slider-bar") - .should("have.css", "width", "110px"); + .should("have.css", "width", "109px"); // Scrolling input should update value cy.dataCy("R-value-input") .trigger("wheel", { deltaY: -100, eventConstructor: "WheelEvent" }) .should("have.value", "100") .dataCy("R-slider-bar") - .should("have.css", "width", "111px") + .should("have.css", "width", "110px") .wait(50); // Test increment/decrement buttons @@ -132,7 +134,7 @@ describe("component editor tests", () => { cy.dataCy("R-slider") .click() .dataCy("R-slider-bar") - .should("have.css", "width", "140px") + .should("have.css", "width", "138px") .dataCy("R-value-input") .should("have.value", "127"); diff --git a/src/util.ts b/src/util.ts index 43a2a8a..feaa606 100644 --- a/src/util.ts +++ b/src/util.ts @@ -92,9 +92,20 @@ export function chooseValueByDirection( return direction === Direction.HORIZONTAL ? xValue : yValue; } -export function roundTo(value: number, decimals: number = 0) { +export function roundTo( + value: number, + decimals: number = 0, + direction: "up" | "down" | null = null, +) { const factor = Math.pow(10, decimals); - return Math.round(value * factor) / factor; + switch (direction) { + case "up": + return Math.ceil(value * factor) / factor; + case "down": + return Math.floor(value * factor) / factor; + default: + return Math.round(value * factor) / factor; + } } export function formatCssRgb(hex: Hex) { diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 11f02fe..2368b80 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -1 +1,6 @@ /// + +interface Window { + framerMotionTestOverride?: boolean; + originalRequestAnimationFrame?: typeof requestAnimationFrame; +}