Completed color picker, value editor, desktop layout.
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user