Completed picker / history. Needs styling refactor.
This commit is contained in:
@@ -1 +1,2 @@
|
||||
node_modules
|
||||
dist
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<!-- <link rel="icon" type="image/svg+xml" href="/vite.svg" /> -->
|
||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Luminance</title>
|
||||
</head>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 6.3 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
+27
-30
@@ -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 {
|
||||
|
||||
+12
-10
@@ -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 <section className={styles.TitleZone}></section>;
|
||||
}
|
||||
|
||||
function FirstZone() {
|
||||
const { selectedColor, selectedColorActions } = useSelectedColor();
|
||||
|
||||
@@ -205,7 +202,6 @@ function FirstZone() {
|
||||
<div className={styles.colorPickerWrapper} aria-label="Color picker">
|
||||
<ColorPicker color={selectedColor} actions={selectedColorActions} />
|
||||
</div>
|
||||
|
||||
<div className={styles.colorValuesWrapper} aria-label="Color values">
|
||||
<ColorValues color={selectedColor} actions={selectedColorActions} />
|
||||
</div>
|
||||
@@ -214,9 +210,17 @@ function FirstZone() {
|
||||
}
|
||||
|
||||
function SecondZone() {
|
||||
const { selectedColor, selectedColorActions } = useSelectedColor();
|
||||
|
||||
return (
|
||||
<section className={styles.secondZone} aria-label="Palette tools">
|
||||
Palette Creator Coming Soon.
|
||||
<div className={styles.colorHistoryWrapper} aria-label="Color History">
|
||||
<ColorHistory
|
||||
color={selectedColor}
|
||||
setColor={selectedColorActions.common.setColor}
|
||||
disabled={false}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={styles.paletteEditorWrapper}
|
||||
aria-label="Palette editor"
|
||||
@@ -231,16 +235,14 @@ function SecondZone() {
|
||||
|
||||
function DesktopContent() {
|
||||
return (
|
||||
<>
|
||||
<div className={styles.mainLayout}>
|
||||
<header className={styles.appHeader}>
|
||||
<span className={styles.title}>LUMINANCE</span>
|
||||
<span className={styles.subtitle}>A color picker for humans.</span>
|
||||
</header>
|
||||
<main className={styles.mainContent}>
|
||||
<FirstZone />
|
||||
<SecondZone />
|
||||
</main>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<Color[]>([]);
|
||||
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 (
|
||||
<div data-cy="color-history" className={styles.colorHistory}>
|
||||
{history.map((historyColor, index) => (
|
||||
<motion.div
|
||||
key={`${historyColor.hex.to_code()}-${index}`}
|
||||
data-cy={`history-color-${index}`}
|
||||
className={styles.historyColor}
|
||||
style={{ backgroundColor: formatCssRgb(historyColor.hex) }}
|
||||
onClick={() => handleClick(historyColor)}
|
||||
{...getAnimationProps(index === 0)}
|
||||
></motion.div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ColorHistory;
|
||||
@@ -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<HTMLInputElement>) =>
|
||||
setDisabled(e.target.checked);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ColorHistory
|
||||
color={state.color}
|
||||
setColor={actions.common.setColor}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<HexEditor color={state.color.hex} actions={actions.hex} />
|
||||
<label>
|
||||
<input
|
||||
data-cy="disabled-checkbox"
|
||||
type="checkbox"
|
||||
checked={disabled}
|
||||
onChange={handleDisabledChange}
|
||||
/>
|
||||
Disabled
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
describe("color history", () => {
|
||||
beforeEach(() => {
|
||||
cy.disableTransitions();
|
||||
cy.clock();
|
||||
cy.mount(<TestWrapper />);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -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<colorlib.ColorBar | null>(null);
|
||||
const [origin, setOrigin] = useState<CartesianSpace>({ x: 0, y: 0 });
|
||||
const [dimensions, setDimensions] = useState<CartesianSpace>({ x: 0, y: 0 });
|
||||
|
||||
// Refs
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const canvasRef = useRef<HTMLCanvasElement>(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 (
|
||||
<div className={styles.colorBarWrapper} ref={containerRef}>
|
||||
<div
|
||||
className={styles.colorBar}
|
||||
ref={sliderRef}
|
||||
style={{
|
||||
width: colorBar?.get_width(),
|
||||
height: colorBar?.get_height(),
|
||||
}}
|
||||
>
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
width={colorBar?.get_width()}
|
||||
height={colorBar?.get_height()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@ function ColorPicker({
|
||||
valueRange={lumRange}
|
||||
arrowDirection="right"
|
||||
invert={true}
|
||||
parentDimensions={dimensions}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.pickerSquare}>
|
||||
@@ -62,6 +63,7 @@ function ColorPicker({
|
||||
hue={color.hcl.h}
|
||||
luminance={color.hcl.l}
|
||||
hex={color.hex}
|
||||
parentDimensions={dimensions}
|
||||
/>
|
||||
<ColorSquare
|
||||
chroma={color.hcl.c}
|
||||
@@ -77,6 +79,7 @@ function ColorPicker({
|
||||
valueRange={lumRange}
|
||||
arrowDirection="left"
|
||||
invert={true}
|
||||
parentDimensions={dimensions}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.horizontalGrip}>
|
||||
@@ -86,6 +89,7 @@ function ColorPicker({
|
||||
setValue={actions.hcl.setH}
|
||||
valueRange={hueRange}
|
||||
arrowDirection="up"
|
||||
parentDimensions={dimensions}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.pickerBar}>
|
||||
@@ -100,6 +104,7 @@ function ColorPicker({
|
||||
chroma={color.hcl.c}
|
||||
luminance={color.hcl.l}
|
||||
hex={color.hex}
|
||||
parentDimensions={dimensions}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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<colorlib.ColorSquare | null>(
|
||||
null,
|
||||
);
|
||||
const [origin, setOrigin] = useState<CartesianSpace>({ x: 0, y: 0 });
|
||||
const [dimensions, setDimensions] = useState<CartesianSpace>({ x: 0, y: 0 });
|
||||
|
||||
// Refs
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const canvasRef = useRef<HTMLCanvasElement>(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 (
|
||||
<div className={styles.colorSquareWrapper} ref={containerRef}>
|
||||
<div
|
||||
className={styles.colorSquare}
|
||||
ref={crosshairRef}
|
||||
style={{
|
||||
width: colorSquare?.get_size(),
|
||||
height: colorSquare?.get_size(),
|
||||
}}
|
||||
>
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
width={colorSquare?.get_size()}
|
||||
height={colorSquare?.get_size()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -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<CartesianSpace>({ x: 0, y: 0 });
|
||||
const [dimensions, setDimensions] = useState<CartesianSpace>({ x: 0, y: 0 });
|
||||
const [darkCrosshairs, setDarkCrosshairs] = useState(true);
|
||||
const containerRef = useRef<HTMLDivElement>(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 (
|
||||
<div className={styles.crosshairWrapper} ref={containerRef}>
|
||||
<div
|
||||
className={styles.crosshair}
|
||||
style={{
|
||||
width: 1,
|
||||
height: dimensions.y,
|
||||
backgroundColor: darkCrosshairs ? "black" : "white",
|
||||
boxShadow: darkCrosshairs ? "0 0 2px black" : "0 0 2px white",
|
||||
left: valueToPosition(hue, dimensions.x - 1, hueRange),
|
||||
top: 0,
|
||||
}}
|
||||
></div>
|
||||
<div
|
||||
className={styles.crosshair}
|
||||
style={{
|
||||
width: dimensions.x,
|
||||
height: 1,
|
||||
backgroundColor: darkCrosshairs ? "black" : "white",
|
||||
boxShadow: darkCrosshairs ? "0 0 2px black" : "0 0 2px white",
|
||||
left: 0,
|
||||
top: valueToPosition(1 - luminance, dimensions.y - 1, lumRange),
|
||||
}}
|
||||
></div>
|
||||
<div
|
||||
className={styles.crossEye}
|
||||
style={{
|
||||
borderColor: darkCrosshairs ? "black" : "white",
|
||||
boxShadow: darkCrosshairs ? "0 0 1px black" : "0 0 1px white",
|
||||
backgroundColor: formatCssRgb(hex),
|
||||
top: valueToPosition(1 - luminance, dimensions.y - 1, lumRange) - 6,
|
||||
left: valueToPosition(hue, dimensions.x - 1, hueRange) - 6,
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function BarCrosshair({
|
||||
chroma,
|
||||
luminance,
|
||||
hex,
|
||||
parentDimensions,
|
||||
}: {
|
||||
chroma: number;
|
||||
luminance: number;
|
||||
hex: colorlib.Hex;
|
||||
parentDimensions: CartesianSpace;
|
||||
}) {
|
||||
const [_origin, setOrigin] = useState<CartesianSpace>({ x: 0, y: 0 });
|
||||
const [dimensions, setDimensions] = useState<CartesianSpace>({ x: 0, y: 0 });
|
||||
const [darkCrosshairs, setDarkCrosshairs] = useState(true);
|
||||
const containerRef = useRef<HTMLDivElement>(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 (
|
||||
<div className={styles.crosshairWrapper} ref={containerRef}>
|
||||
<div
|
||||
className={styles.crosshair}
|
||||
style={{
|
||||
width: 1,
|
||||
height: dimensions.y,
|
||||
backgroundColor: darkCrosshairs ? "black" : "white",
|
||||
boxShadow: darkCrosshairs ? "0 0 2px black" : "0 0 2px white",
|
||||
left: valueToPosition(chroma, dimensions.x - 1, chromaRange),
|
||||
top: 0,
|
||||
}}
|
||||
></div>
|
||||
<div
|
||||
className={styles.crossEye}
|
||||
style={{
|
||||
borderColor: darkCrosshairs ? "black" : "white",
|
||||
boxShadow: darkCrosshairs ? "0 0 1px black" : "0 0 1px white",
|
||||
backgroundColor: formatCssRgb(hex),
|
||||
top: 6,
|
||||
left: valueToPosition(chroma, dimensions.x - 1, chromaRange) - 6,
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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<CartesianSpace>({ x: 0, y: 0 });
|
||||
const [dimensions, setDimensions] = useState<CartesianSpace>({ 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 (
|
||||
<div className={styles.gripSlider} ref={sliderRef}>
|
||||
<div
|
||||
className={styles.grip}
|
||||
style={{
|
||||
...arrowStyle,
|
||||
cursor: isDragging ? "grabbing" : "grab",
|
||||
top: chooseValueByDirection(
|
||||
direction,
|
||||
0,
|
||||
-12 +
|
||||
valueToPosition(
|
||||
valueRange.max - value,
|
||||
dimensions.y - 1,
|
||||
valueRange,
|
||||
),
|
||||
),
|
||||
left: chooseValueByDirection(
|
||||
direction,
|
||||
-12 + valueToPosition(value, dimensions.x - 1, valueRange),
|
||||
0,
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default GripSlider;
|
||||
@@ -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;
|
||||
|
||||
@@ -20,9 +20,10 @@ function TestWrapper() {
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
height: 300,
|
||||
height: 275,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 16,
|
||||
}}
|
||||
>
|
||||
<SpaceEditor
|
||||
@@ -44,16 +45,19 @@ function TestWrapper() {
|
||||
|
||||
<div style={{ fontFamily: "monospace" }}>
|
||||
<p data-cy="hcl-value">
|
||||
HCL ({roundTo(state.color.hcl.h, 0)}, {roundTo(state.color.hcl.c, 2)},{" "}
|
||||
{roundTo(state.color.hcl.l, 2)})
|
||||
HCL ({roundTo(state.color.hcl.h, 0, "down")},{" "}
|
||||
{roundTo(state.color.hcl.c, 2, "down")},{" "}
|
||||
{roundTo(state.color.hcl.l, 2, "down")})
|
||||
</p>
|
||||
<p data-cy="hsv-value">
|
||||
HSV ({roundTo(state.color.hsv.h, 0)}, {roundTo(state.color.hsv.s, 2)},{" "}
|
||||
{roundTo(state.color.hsv.v, 2)})
|
||||
HSV ({roundTo(state.color.hsv.h, 0, "down")},{" "}
|
||||
{roundTo(state.color.hsv.s, 2, "down")},{" "}
|
||||
{roundTo(state.color.hsv.v, 2, "down")})
|
||||
</p>
|
||||
<p data-cy="rgb-value">
|
||||
RGB ({roundTo(state.color.rgb.r, 0)}, {roundTo(state.color.rgb.g, 0)},{" "}
|
||||
{roundTo(state.color.rgb.b, 0)})
|
||||
RGB ({roundTo(state.color.rgb.r, 0, "down")},{" "}
|
||||
{roundTo(state.color.rgb.g, 0, "down")},{" "}
|
||||
{roundTo(state.color.rgb.b, 0, "down")})
|
||||
</p>
|
||||
<p data-cy="hex-value">HEX: #{state.color.hex.to_code()}</p>
|
||||
</div>
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -31,14 +31,14 @@ export function ValueEditor({
|
||||
value,
|
||||
setValue,
|
||||
scale = 1,
|
||||
onKeyDown,
|
||||
onKeyDown = null,
|
||||
}: {
|
||||
componentSymbol: string;
|
||||
valueRange: Range;
|
||||
value: number;
|
||||
setValue: Setter<number>;
|
||||
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({
|
||||
<input
|
||||
type="text"
|
||||
ref={valueRef}
|
||||
value={Math.round(value * scale)}
|
||||
value={Math.floor(value * scale)}
|
||||
onChange={onChange}
|
||||
className={styles.value}
|
||||
onFocus={(e) => e.target.select()}
|
||||
|
||||
@@ -18,8 +18,10 @@ function TestWrapper() {
|
||||
<div
|
||||
style={{
|
||||
width: 400,
|
||||
height: 25,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
border: "2px solid #7a7a7a",
|
||||
}}
|
||||
>
|
||||
<ValueEditor
|
||||
@@ -53,7 +55,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");
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
+12
-1
@@ -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);
|
||||
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) {
|
||||
|
||||
Vendored
+5
@@ -1 +1,6 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface Window {
|
||||
framerMotionTestOverride?: boolean;
|
||||
originalRequestAnimationFrame?: typeof requestAnimationFrame;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user