Completed color picker, value editor, desktop layout.
This commit is contained in:
+2
-2
@@ -55,9 +55,9 @@
|
||||
"^react$",
|
||||
"^react-dom(.*)$",
|
||||
"^react(.*)$",
|
||||
"^@(?!(components|hooks|providers|/))(.*)$",
|
||||
"^@(?!(/))(.*)$",
|
||||
"^(?!@|[.])(.*)$",
|
||||
"^@(/|components|hooks|providers)(.*)$",
|
||||
"^@(/)(.*)$",
|
||||
"^[./]"
|
||||
],
|
||||
"importOrderSeparation": true,
|
||||
|
||||
+48
-107
@@ -1,7 +1,42 @@
|
||||
.appWrapper {
|
||||
background-color: white;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
overflow: hidden;
|
||||
margin: 0 auto;
|
||||
box-shadow: 0 0 40px #7a7a7a;
|
||||
border-left: 2px solid #7a7a7a;
|
||||
border-right: 2px solid #7a7a7a;
|
||||
}
|
||||
|
||||
.appHeader {
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
border-bottom: 2px solid #7a7a7a;
|
||||
padding: 20px 30px 12px;
|
||||
}
|
||||
|
||||
.appHeader .title {
|
||||
/* Typography */
|
||||
font-family: "Inter", "Roboto", sans-serif;
|
||||
font-size: 2rem;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.4rem;
|
||||
text-transform: uppercase;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.appHeader .subtitle {
|
||||
margin-left: 1.5rem;
|
||||
font-family: "Inter", "Roboto", sans-serif;
|
||||
font-style: italic;
|
||||
font-size: 1rem;
|
||||
font-weight: 300;
|
||||
letter-spacing: 0.08rem;
|
||||
line-height: 1;
|
||||
color: #7a7a7a;
|
||||
}
|
||||
|
||||
.mobileContent,
|
||||
@@ -17,12 +52,7 @@
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
/* Large */
|
||||
@media (min-width: 992px),
|
||||
(min-width: 568px) and (max-width: 991px) and (orientation: portrait) {
|
||||
.appWrapper {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.mobileContent {
|
||||
@@ -30,18 +60,24 @@
|
||||
}
|
||||
|
||||
.mainContent {
|
||||
display: flex;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2fr;
|
||||
}
|
||||
|
||||
.firstZone {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-right: 2px solid #7a7a7a;
|
||||
}
|
||||
|
||||
.secondZone {
|
||||
flex: 1;
|
||||
padding: 40px;
|
||||
color: #555;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.colorPickerWrapper {
|
||||
border-bottom: 2px solid #7a7a7a;
|
||||
}
|
||||
|
||||
.colorValuesWrapper {
|
||||
@@ -52,111 +88,16 @@
|
||||
|
||||
.paletteLibraryWrapper {
|
||||
}
|
||||
|
||||
/* Large */
|
||||
@media (min-width: 992px),
|
||||
(min-width: 568px) and (max-width: 991px) and (orientation: portrait) {
|
||||
}
|
||||
|
||||
/* Landscape Phone */
|
||||
@media (min-width: 568px) and (max-width: 991px) and (orientation: landscape) {
|
||||
.appWrapper {
|
||||
}
|
||||
|
||||
.mainContent,
|
||||
.mobileTopNav {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mobileContent {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.mobileLeftNav {
|
||||
}
|
||||
|
||||
.mobileRightNav {
|
||||
}
|
||||
|
||||
.mobileFirstZone {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.mobileSecondZone {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.tabWrapper {
|
||||
max-height: 100%;
|
||||
overflow-y: scroll;
|
||||
scroll-snap-type: y mandatory;
|
||||
}
|
||||
|
||||
.tabWrapper .tab {
|
||||
scroll-snap-align: start;
|
||||
}
|
||||
|
||||
.colorPickerWrapper {
|
||||
}
|
||||
|
||||
.colorValuesWrapper {
|
||||
}
|
||||
|
||||
.paletteEditorWrapper {
|
||||
}
|
||||
|
||||
.paletteLibraryWrapper {
|
||||
}
|
||||
}
|
||||
|
||||
/* Portrait Phone */
|
||||
@media (max-width: 567px) {
|
||||
.appWrapper {
|
||||
}
|
||||
|
||||
.mainContent,
|
||||
.mobileRightNav,
|
||||
.mobileLeftNav {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mobileContent {
|
||||
}
|
||||
|
||||
.mobileTopNav {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.leftMenuButton {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.rightMenuButton {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.mobileFirstZone {
|
||||
}
|
||||
|
||||
.mobileSecondZone {
|
||||
}
|
||||
|
||||
.tabWrapper {
|
||||
display: flex;
|
||||
overflow-x: scroll;
|
||||
scroll-snap-type: x mandatory;
|
||||
}
|
||||
|
||||
.tabWrapper .tab {
|
||||
min-width: 100vw;
|
||||
scroll-snap-align: start;
|
||||
}
|
||||
|
||||
.colorPickerWrapper {
|
||||
}
|
||||
|
||||
.colorValuesWrapper {
|
||||
}
|
||||
|
||||
.paletteEditorWrapper {
|
||||
}
|
||||
|
||||
.paletteLibraryWrapper {
|
||||
}
|
||||
}
|
||||
|
||||
+49
-9
@@ -1,14 +1,16 @@
|
||||
import { useReducer, useState } from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
import clsx from "clsx";
|
||||
import { Color } from "colorlib";
|
||||
|
||||
import ColorPicker from "@components/ColorPicker/ColorPicker";
|
||||
import ColorValues from "@components/ColorValues/ColorValues";
|
||||
import { LeftMenu, RightMenu } from "@components/SideMenu";
|
||||
import { useMediaQuery } from "@providers/hooks";
|
||||
import { useSelectedColor } from "@providers/hooks";
|
||||
import ColorPicker from "@/components/ColorPicker/ColorPicker";
|
||||
import ColorValues from "@/components/ColorValues/ColorValues";
|
||||
import { LeftMenu, RightMenu } from "@/components/SideMenu";
|
||||
import { useMediaQuery } from "@/providers/hooks";
|
||||
import { useSelectedColor } from "@/providers/hooks";
|
||||
|
||||
import styles from "./App.module.css";
|
||||
import { formatCssRgb } from "./util";
|
||||
|
||||
// Menu Button Components
|
||||
|
||||
@@ -106,7 +108,7 @@ function MobileFirstZone() {
|
||||
aria-roledescription="slide"
|
||||
aria-label="Color Picker"
|
||||
>
|
||||
<ColorPicker />
|
||||
<ColorPicker color={selectedColor} actions={selectedColorActions} />
|
||||
</div>
|
||||
<div
|
||||
className={clsx(styles.tab, styles.colorValuesWrapper)}
|
||||
@@ -191,13 +193,17 @@ function MobileContent({
|
||||
|
||||
// Desktop Layout Components
|
||||
|
||||
function TitleZone() {
|
||||
return <section className={styles.TitleZone}></section>;
|
||||
}
|
||||
|
||||
function FirstZone() {
|
||||
const { selectedColor, selectedColorActions } = useSelectedColor();
|
||||
|
||||
return (
|
||||
<section className={styles.firstZone} aria-label="Color tools">
|
||||
<div className={styles.colorPickerWrapper} aria-label="Color picker">
|
||||
<ColorPicker />
|
||||
<ColorPicker color={selectedColor} actions={selectedColorActions} />
|
||||
</div>
|
||||
|
||||
<div className={styles.colorValuesWrapper} aria-label="Color values">
|
||||
@@ -210,6 +216,7 @@ function FirstZone() {
|
||||
function SecondZone() {
|
||||
return (
|
||||
<section className={styles.secondZone} aria-label="Palette tools">
|
||||
Palette Creator Coming Soon.
|
||||
<div
|
||||
className={styles.paletteEditorWrapper}
|
||||
aria-label="Palette editor"
|
||||
@@ -224,10 +231,16 @@ function SecondZone() {
|
||||
|
||||
function DesktopContent() {
|
||||
return (
|
||||
<>
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -236,9 +249,35 @@ function DesktopContent() {
|
||||
function App() {
|
||||
const [isRightMenuOpen, setIsRightMenuOpen] = useState(false);
|
||||
const [isLeftMenuOpen, setIsLeftMenuOpen] = useState(false);
|
||||
const { isDesktop } = useMediaQuery();
|
||||
// const { isDesktop } = useMediaQuery();
|
||||
const isDesktop = true;
|
||||
|
||||
const lum = 0.75;
|
||||
const chr = 0.8;
|
||||
const steps = 8;
|
||||
|
||||
const colors = Array.from({ length: steps }, (_, index) => {
|
||||
const hue = (index * 360) / (steps - 1);
|
||||
return Color.from_hcl(hue, chr, lum);
|
||||
});
|
||||
|
||||
const colorGradient = colors
|
||||
.map((color, index) => {
|
||||
const colorString = formatCssRgb(color.hex);
|
||||
const percentage = (index / (colors.length - 1)) * 100;
|
||||
return `${colorString} ${percentage}%`;
|
||||
})
|
||||
.join(", ");
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.background}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
background: `linear-gradient(180deg, ${colorGradient})`,
|
||||
}}
|
||||
>
|
||||
<div className={styles.appWrapper} role="application">
|
||||
{!isDesktop && (
|
||||
<MobileContent
|
||||
@@ -251,6 +290,7 @@ function App() {
|
||||
|
||||
{isDesktop && <DesktopContent />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
+1
-1
@@ -62,7 +62,7 @@ export function colorReducer(
|
||||
}
|
||||
}
|
||||
|
||||
type Setter = (valOrCallback: SetterValueOrCallback<number>) => void;
|
||||
export type Setter = (valOrCallback: SetterValueOrCallback<number>) => void;
|
||||
|
||||
export interface CommonColorActions {
|
||||
setColor: (color: colorlib.Color) => void;
|
||||
|
||||
+22
-3
@@ -9,6 +9,8 @@ import {
|
||||
positionToValue,
|
||||
} from "@/util";
|
||||
|
||||
import { useSmoothAnimation } from "./animation";
|
||||
|
||||
if (typeof TouchEvent === "undefined") {
|
||||
// @ts-ignore - intentionally creating global
|
||||
window.TouchEvent = window.MouseEvent;
|
||||
@@ -21,6 +23,8 @@ export function useCrosshair({
|
||||
setYValue,
|
||||
xValueRange,
|
||||
yValueRange,
|
||||
invertX,
|
||||
invertY,
|
||||
}: {
|
||||
origin: CartesianSpace;
|
||||
dimensions: CartesianSpace;
|
||||
@@ -28,6 +32,8 @@ export function useCrosshair({
|
||||
setYValue: Setter<number>;
|
||||
xValueRange: Range;
|
||||
yValueRange: Range;
|
||||
invertX?: boolean;
|
||||
invertY?: boolean;
|
||||
}) {
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const crosshairRef = useRef<HTMLDivElement>(null);
|
||||
@@ -42,6 +48,9 @@ export function useCrosshair({
|
||||
const xValueRangeRef = useRef(xValueRange);
|
||||
const yValueRangeRef = useRef(yValueRange);
|
||||
|
||||
// Hooks
|
||||
const smoothAnimation = useSmoothAnimation();
|
||||
|
||||
useEffect(() => {
|
||||
originRef.current = origin;
|
||||
dimensionsRef.current = dimensions;
|
||||
@@ -55,13 +64,23 @@ export function useCrosshair({
|
||||
const calculatePositions = useCallback((event: MouseEvent | TouchEvent) => {
|
||||
const orig = originRef.current;
|
||||
const dims = dimensionsRef.current;
|
||||
const xRange = xValueRangeRef.current;
|
||||
const yRange = yValueRangeRef.current;
|
||||
|
||||
const { clientX, clientY } = extractEventCoordinates(event);
|
||||
|
||||
const xPos = minmax(clientX - orig.x, 0, dims.x - 1);
|
||||
const yPos = minmax(clientY - orig.y, 0, dims.y - 1);
|
||||
const newXValue = positionToValue(xPos, dims.x - 1, xValueRangeRef.current);
|
||||
const newYValue = positionToValue(yPos, dims.y - 1, yValueRangeRef.current);
|
||||
let newXValue = positionToValue(xPos, dims.x - 1, xRange);
|
||||
let newYValue = positionToValue(yPos, dims.y - 1, yRange);
|
||||
|
||||
if (invertX) {
|
||||
newXValue = xRange.max - newXValue;
|
||||
}
|
||||
|
||||
if (invertY) {
|
||||
newYValue = yRange.max - newYValue;
|
||||
}
|
||||
|
||||
setXValueRef.current(newXValue);
|
||||
setYValueRef.current(newYValue);
|
||||
@@ -70,7 +89,7 @@ export function useCrosshair({
|
||||
const handleMove = useCallback(
|
||||
(event: MouseEvent | TouchEvent) => {
|
||||
event.preventDefault();
|
||||
calculatePositions(event);
|
||||
smoothAnimation(() => calculatePositions(event));
|
||||
},
|
||||
[calculatePositions],
|
||||
);
|
||||
|
||||
+18
-2
@@ -12,6 +12,7 @@ import {
|
||||
valueToPosition,
|
||||
} from "@/util";
|
||||
|
||||
import { useSmoothAnimation } from "./animation";
|
||||
import { useScroll } from "./scroll";
|
||||
|
||||
if (typeof TouchEvent === "undefined") {
|
||||
@@ -34,6 +35,7 @@ export function useSlider({
|
||||
valueRange,
|
||||
value,
|
||||
setValue,
|
||||
invert = false,
|
||||
}: {
|
||||
direction: Direction;
|
||||
origin: CartesianSpace;
|
||||
@@ -41,6 +43,7 @@ export function useSlider({
|
||||
valueRange: Range;
|
||||
value: number;
|
||||
setValue: Setter<number>;
|
||||
invert?: boolean;
|
||||
}) {
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const sliderRef = useRef<HTMLDivElement>(null);
|
||||
@@ -59,6 +62,9 @@ export function useSlider({
|
||||
const [position, setPosition] = useState(0);
|
||||
const positionRef = useRef(position);
|
||||
|
||||
// Hooks
|
||||
const smoothAnimation = useSmoothAnimation();
|
||||
|
||||
useEffect(() => {
|
||||
directionRef.current = direction;
|
||||
originRef.current = origin;
|
||||
@@ -68,6 +74,11 @@ export function useSlider({
|
||||
dimensions.x,
|
||||
dimensions.y,
|
||||
);
|
||||
positionRef.current = valueToPosition(
|
||||
value,
|
||||
maxPosition.current,
|
||||
valueRangeRef.current,
|
||||
);
|
||||
}, [direction, origin, dimensions]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -94,19 +105,23 @@ export function useSlider({
|
||||
0,
|
||||
chooseValueByDirection(dir, dims.x, dims.y),
|
||||
);
|
||||
const newValue = positionToValue(
|
||||
let newValue = positionToValue(
|
||||
newPosition,
|
||||
maxPosition.current,
|
||||
valueRangeRef.current,
|
||||
);
|
||||
|
||||
if (invert) {
|
||||
newValue = valueRangeRef.current.max - newValue;
|
||||
}
|
||||
|
||||
setValueRef.current(newValue);
|
||||
}, []);
|
||||
|
||||
const handleMove = useCallback(
|
||||
(event: MouseEvent | TouchEvent) => {
|
||||
event.preventDefault();
|
||||
calculatePosition(event);
|
||||
smoothAnimation(() => calculatePosition(event));
|
||||
},
|
||||
[calculatePosition],
|
||||
);
|
||||
@@ -154,6 +169,7 @@ export function useSlider({
|
||||
maxPosition.current,
|
||||
valueRangeRef.current,
|
||||
);
|
||||
|
||||
setValueRef.current(newValue);
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { createContext, useContext, useReducer } from "react";
|
||||
import { createContext, useReducer } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import * as colorlib from "colorlib";
|
||||
|
||||
import { colorReducer, createColorActions } from "@hooks/color";
|
||||
import type { ColorActions } from "@hooks/color";
|
||||
import { colorReducer, createColorActions } from "@/hooks/color";
|
||||
import type { ColorActions } from "@/hooks/color";
|
||||
|
||||
interface SelectedColorContextType {
|
||||
selectedColor: colorlib.Color;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { RefObject } from "react";
|
||||
|
||||
import { Hex } from "colorlib";
|
||||
|
||||
import type { CartesianSpace, Range } from "./types";
|
||||
import { Direction } from "./types";
|
||||
|
||||
@@ -94,3 +96,7 @@ export function roundTo(value: number, decimals: number = 0) {
|
||||
const factor = Math.pow(10, decimals);
|
||||
return Math.round(value * factor) / factor;
|
||||
}
|
||||
|
||||
export function formatCssRgb(hex: Hex) {
|
||||
return `rgb(${hex.r},${hex.g},${hex.b})`;
|
||||
}
|
||||
|
||||
@@ -26,9 +26,6 @@
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@components/*": ["./src/components/*"],
|
||||
"@hooks/*": ["./src/hooks/*"],
|
||||
"@providers/*": ["./src/providers/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src", "cypress"]
|
||||
|
||||
@@ -11,9 +11,6 @@ export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
"@components": path.resolve(__dirname, "./src/components"),
|
||||
"@hooks": path.resolve(__dirname, "./src/hooks"),
|
||||
"@providers": path.resolve(__dirname, "./src/providers"),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
|
||||
Reference in New Issue
Block a user