Completed color picker, value editor, desktop layout.

This commit is contained in:
Jay
2025-08-13 18:05:29 -04:00
parent ae02e49ce2
commit 7a2e4cf2ae
22 changed files with 533 additions and 339 deletions
@@ -1,36 +1,110 @@
.container {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
padding: 40px;
display: grid;
grid-template-columns: 25px 1fr 25px;
grid-template-rows: 50px 1fr 25px;
grid-template-areas:
". preview ."
"leftGrip square rightGrip"
". bottomGrip ."
". bar .";
}
.pickerSquare {
flex: 1;
grid-area: square;
position: relative;
aspect-ratio: 1/1;
border: 2px solid #7a7a7a;
}
.pickerBar {
flex: 1;
grid-area: bar;
position: relative;
height: 25px;
margin-top: 15px;
border: 2px solid #7a7a7a;
}
/* Large - Landscape Tablets / Desktops */
/* Medium - Portrait Tablets */
/* Horizontal layout, vertically scrolling picker and palette content */
@media (min-width: 992px),
(min-width: 568px) and (max-width: 991px) and (orientation: portrait) {
.verticalGripLeft {
grid-area: leftGrip;
}
/* Medium - Landscape Phones */
/* Horizontal layout, side menu, vertical tabbed picker */
@media (min-width: 568px) and (max-width: 991px) and (orientation: landscape) {
.container {
flex-direction: row;
}
.verticalGripRight {
grid-area: rightGrip;
}
/* Small - Portrait Phones*/
/* Vertical layout, side menu, horizontal tabbed picker */
@media (max-width: 567px) {
.horizontalGrip {
grid-area: bottomGrip;
}
/* Color Square */
.colorSquareWrapper {
height: 100%;
width: 100%;
}
.colorSquare {
cursor: crosshair;
}
/* Color Bar */
.colorBarWrapper {
height: 100%;
width: 100%;
}
.colorBar {
cursor: crosshair;
}
/* Crosshairs */
.crosshairWrapper {
position: absolute;
top: 0;
right: 0;
left: 0;
bottom: 0;
pointer-events: none;
}
.crosshair {
position: absolute;
transition:
background-color 400ms,
box-shadow 400ms;
pointer-events: none;
}
.crossEye {
position: absolute;
width: 9px;
height: 9px;
z-index: 2;
border-radius: 9px;
border-width: 2px;
border-style: solid;
transition:
border-color 350ms,
box-shadow 400ms;
pointer-events: none;
}
/* Square Grips */
.gripSlider {
position: relative;
width: 100%;
height: 100%;
}
.grip {
position: absolute;
width: 0;
height: 0;
}
/* Preview */
.preview {
grid-area: preview;
margin-bottom: 15px;
border: 2px solid #7a7a7a;
}
+102 -5
View File
@@ -1,10 +1,107 @@
import styles from "./ColorPicker.module.css";
import { useEffect, useRef, useState } from "react";
import * as colorlib from "colorlib";
import type { ColorActions } from "@/hooks/color";
import { useResize } from "@/hooks/window";
import { Direction } from "@/types";
import type { CartesianSpace } from "@/types";
import { formatCssRgb, setMeasurements } from "@/util";
import ColorBar from "./ColorBar";
import styles from "./ColorPicker.module.css";
import ColorSquare from "./ColorSquare";
import { BarCrosshair, SquareCrosshair } from "./Crosshair";
import GripSlider from "./GripSlider";
function ColorPicker({
color,
actions,
}: {
color: colorlib.Color;
actions: ColorActions;
}) {
const containerRef = useRef<HTMLDivElement>(null);
const hueRange = { min: 0, max: 359 };
const lumRange = { min: 0, max: 1 };
const [_origin, setOrigin] = useState<CartesianSpace>({ x: 0, y: 0 });
const [dimensions, setDimensions] = useState<CartesianSpace>({ x: 0, y: 0 });
// Get measurements
useEffect(() => {
if (containerRef.current) {
setMeasurements(containerRef, setOrigin, setDimensions);
}
return useResize(() => {
setMeasurements(containerRef, setOrigin, setDimensions);
});
}, [containerRef.current]);
function ColorPicker() {
return (
<div className={styles.container}>
<div className={styles.pickerSquare}>Square</div>
<div className={styles.pickerBar}>Bar</div>
<div className={styles.container} ref={containerRef}>
<div
className={styles.preview}
style={{
backgroundColor: formatCssRgb(color.hex),
}}
></div>
<div className={styles.verticalGripLeft}>
<GripSlider
direction={Direction.VERTICAL}
value={color.hcl.l}
setValue={actions.hcl.setL}
valueRange={lumRange}
arrowDirection="right"
invert={true}
/>
</div>
<div className={styles.pickerSquare}>
<SquareCrosshair
hue={color.hcl.h}
luminance={color.hcl.l}
hex={color.hex}
/>
<ColorSquare
chroma={color.hcl.c}
actions={actions.hcl}
parentDimensions={dimensions}
/>
</div>
<div className={styles.verticalGripRight}>
<GripSlider
direction={Direction.VERTICAL}
value={color.hcl.l}
setValue={actions.hcl.setL}
valueRange={lumRange}
arrowDirection="left"
invert={true}
/>
</div>
<div className={styles.horizontalGrip}>
<GripSlider
direction={Direction.HORIZONTAL}
value={color.hcl.h}
setValue={actions.hcl.setH}
valueRange={hueRange}
arrowDirection="up"
/>
</div>
<div className={styles.pickerBar}>
<ColorBar
hue={color.hcl.h}
chroma={color.hcl.c}
luminance={color.hcl.l}
setChroma={actions.hcl.setC}
parentDimensions={dimensions}
/>
<BarCrosshair
chroma={color.hcl.c}
luminance={color.hcl.l}
hex={color.hex}
/>
</div>
</div>
);
}
@@ -1,6 +1,137 @@
.colorValuesWrapper {
width: 100%;
height: 100%;
padding: 40px;
display: flex;
flex-direction: column;
gap: 16px;
}
.spaceWrapper {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
border: 2px solid #7a7a7a;
}
.componentWrapper {
display: flex;
align-items: stretch;
width: 100%;
height: 25px;
font-family: monospace;
border-top: 1px solid #7a7a7a;
border-bottom: 1px solid #7a7a7a;
}
.componentWrapper:first-of-type {
border-top: none;
}
.componentWrapper:last-of-type {
border-bottom: none;
}
.section {
display: flex;
align-items: center;
justify-content: center;
border-right: 2px solid #7a7a7a;
}
.section:last-of-type {
border-right: none;
}
.symbol {
font-family: monospace;
font-size: 14px;
aspect-ratio: 1 / 1;
}
.sliderSection {
position: relative;
flex-grow: 1;
}
.sliderWrapper {
width: 100%;
height: 100%;
}
.sliderBar {
position: absolute;
top: 0;
left: 0;
height: 100%;
background-color: #aaa;
pointer-events: none;
}
.buttonWrapper {
aspect-ratio: 1 / 1;
}
.button {
height: 100%;
width: 100%;
padding: 0;
cursor: pointer;
user-select: none;
background: none;
border: none;
}
.button:hover {
background-color: #ddd;
}
.button:active {
background-color: #bbb;
}
.valueWrapper {
aspect-ratio: 1.5 / 1;
min-width: 40px;
}
.value {
height: 100%;
width: 100%;
background: none;
border: none;
padding: 0 5px;
font-family: monospace;
font-size: 14px;
text-align: right;
}
.hexEditor {
display: flex;
align-items: stretch;
font-family: monospace;
border: 2px solid #7a7a7a;
height: 25px;
max-width: 150px;
}
.hexLabel {
font-family: monospace;
font-size: 14px;
padding: 0 10px;
}
.hexValueWrapper {
flex: 1;
}
.hexValueWrapper input {
height: 100%;
width: 100%;
background: none;
border: none;
padding: 0 5px;
font-family: monospace;
font-size: 14px;
letter-spacing: 0.1em;
text-align: center;
}
+1 -1
View File
@@ -3,7 +3,7 @@ import type { KeyboardEvent } from "react";
import * as colorlib from "colorlib";
import type { ColorActions } from "@hooks/color";
import type { ColorActions } from "@/hooks/color";
import styles from "./ColorValues.module.css";
import SpaceEditor from "./SpaceEditor";
@@ -2,7 +2,7 @@ import { useReducer } from "react";
import { Color } from "colorlib";
import { colorReducer, createColorActions } from "@hooks/color";
import { colorReducer, createColorActions } from "@/hooks/color";
import ColorValues from "./ColorValues";
@@ -2,7 +2,7 @@ import { useReducer } from "react";
import { Color } from "colorlib";
import { colorReducer, createColorActions } from "@hooks/color";
import { colorReducer, createColorActions } from "@/hooks/color";
import { HexEditor } from "./ValueEditor";
@@ -1,6 +0,0 @@
.spaceWrapper {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
}
+2 -2
View File
@@ -4,9 +4,9 @@ import type {
HCLColorActions,
HSVColorActions,
RGBColorActions,
} from "@hooks/color";
} from "@/hooks/color";
import styles from "./SpaceEditor.module.css";
import styles from "./ColorValues.module.css";
import { ValueEditor } from "./ValueEditor";
type ColorSpaceProps =
@@ -2,8 +2,8 @@ import { useReducer } from "react";
import { Color } from "colorlib";
import { colorReducer, createColorActions } from "@/hooks/color";
import { roundTo } from "@/util";
import { colorReducer, createColorActions } from "@hooks/color";
import SpaceEditor from "./SpaceEditor";
@@ -1,118 +0,0 @@
.componentWrapper {
display: flex;
align-items: stretch;
width: 100%;
height: 25px;
font-family: monospace;
border: 1px solid black;
border-top: none;
}
.componentWrapper:first-of-type {
border-top: 1px solid black;
}
.section {
display: flex;
align-items: center;
justify-content: center;
border-right: 1px solid black;
}
.section:last-of-type {
border-right: none;
}
.symbol {
font-family: monospace;
font-size: 14px;
aspect-ratio: 1 / 1;
}
.sliderSection {
position: relative;
flex-grow: 1;
}
.sliderWrapper {
width: 100%;
height: 100%;
}
.sliderBar {
position: absolute;
top: 0;
left: 0;
height: 100%;
background-color: #aaa;
pointer-events: none;
}
.buttonWrapper {
aspect-ratio: 1 / 1;
}
.button {
height: 100%;
width: 100%;
padding: 0;
cursor: pointer;
user-select: none;
background: none;
border: none;
}
.button:hover {
background-color: #ddd;
}
.button:active {
background-color: #bbb;
}
.valueWrapper {
aspect-ratio: 1.5 / 1;
min-width: 40px;
}
.value {
height: 100%;
width: 100%;
background: none;
border: none;
padding: 0 5px;
font-family: monospace;
font-size: 14px;
text-align: right;
}
.hexEditor {
display: flex;
align-items: stretch;
font-family: monospace;
border: 1px solid black;
height: 25px;
max-width: 150px;
}
.hexLabel {
font-family: monospace;
font-size: 14px;
padding: 0 10px;
}
.hexValueWrapper {
flex: 1;
}
.hexValueWrapper input {
height: 100%;
width: 100%;
background: none;
border: none;
padding: 0 5px;
font-family: monospace;
font-size: 14px;
letter-spacing: 0.1em;
text-align: center;
}
+7 -7
View File
@@ -10,15 +10,15 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import clsx from "clsx";
import * as colorlib from "colorlib";
import type { HexColorActions } from "@/hooks/color";
import { useScroll } from "@/hooks/scroll";
import { useSlider } from "@/hooks/slider";
import { useResize } from "@/hooks/window";
import type { CartesianSpace, Range, Setter, Timeout } from "@/types";
import { Direction } from "@/types";
import { minmax, setMeasurements, valueToPosition } from "@/util";
import type { HexColorActions } from "@hooks/color";
import { useScroll } from "@hooks/scroll";
import { useSlider } from "@hooks/slider";
import { useResize } from "@hooks/window";
import { minmax, roundTo, setMeasurements, valueToPosition } from "@/util";
import styles from "./ValueEditor.module.css";
import styles from "./ColorValues.module.css";
// ------------ //
// Value Editor //
@@ -68,7 +68,7 @@ export function ValueEditor({
setValue((prev) => {
const scaledStep = step / scale;
const newValue = minmax(
Math.floor(prev * scale) / scale + scaledStep,
roundTo(Math.floor(roundTo(prev * scale, 6)) / scale + scaledStep, 6),
valueRange.min,
valueRange.max,
);
@@ -2,7 +2,7 @@ import { useReducer } from "react";
import { Color } from "colorlib";
import { colorReducer, createColorActions } from "@hooks/color";
import { colorReducer, createColorActions } from "@/hooks/color";
import { ValueEditor } from "./ValueEditor";