setMeasurements(sliderRef, setOrigin, setDimensions));
}, [sliderRef, 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 {};
- }
- })();
+ const isVertical = direction === Direction.VERTICAL;
return (
{
+ if (position === "right") {
+ return 6;
+ } else if (position === "left") {
+ return -4;
+ }
+ return 0;
+ })(),
),
}}
- />
+ >
+ {isVertical ? (
+
+ ) : (
+
+ )}
+
);
}
diff --git a/src/components/ColorValues/ColorValues.module.css b/src/components/ColorValues/ColorValues.module.css
index 2d3bfac..a8d681b 100644
--- a/src/components/ColorValues/ColorValues.module.css
+++ b/src/components/ColorValues/ColorValues.module.css
@@ -11,7 +11,12 @@
max-height: 94px;
flex: 1;
min-height: 0;
- border: 2px solid #7a7a7a;
+ background-color: #f7f7f7;
+ border: 1px solid #c9c9c9;
+ border-radius: 4px;
+ box-shadow:
+ rgba(0, 0, 0, 0.1) 0px 2px 4px,
+ rgba(0, 0, 0, 0.15) 0px 2px 4px;
}
.componentWrapper {
@@ -22,7 +27,6 @@
min-height: 0;
font-family: monospace;
border-top: 1px solid #7a7a7a;
- border-bottom: 1px solid #7a7a7a;
}
.componentWrapper:first-of-type {
@@ -37,7 +41,7 @@
display: flex;
align-items: center;
justify-content: center;
- border-right: 2px solid #7a7a7a;
+ border-right: 1px solid #7a7a7a;
}
.section:last-of-type {
@@ -65,7 +69,7 @@
top: 0;
left: 0;
height: 100%;
- background-color: #aaa;
+ background-color: #c1c1c1;
pointer-events: none;
}
@@ -84,6 +88,9 @@
user-select: none;
background: none;
border: none;
+ transition-property: background-color;
+ transition-duration: 150ms;
+ transition-timing-function: ease-out;
}
.button:hover {
@@ -114,9 +121,14 @@
display: flex;
align-items: stretch;
font-family: monospace;
- border: 2px solid #7a7a7a;
+ background-color: #f7f7f7;
+ border: 1px solid #c9c9c9;
+ border-radius: 4px;
height: 25px;
max-width: 150px;
+ box-shadow:
+ rgba(0, 0, 0, 0.1) 0px 2px 4px,
+ rgba(0, 0, 0, 0.15) 0px 2px 4px;
}
.hexLabel {
diff --git a/src/components/ColorValues/ColorValues.tsx b/src/components/ColorValues/ColorValues.tsx
index 888f5d2..5c01f71 100644
--- a/src/components/ColorValues/ColorValues.tsx
+++ b/src/components/ColorValues/ColorValues.tsx
@@ -45,6 +45,11 @@ function ColorValues({
return (
+
-
);
}
diff --git a/src/components/ColorValues/HexEditor.test.cy.tsx b/src/components/ColorValues/HexEditor.test.cy.tsx
index e0e88a1..27a29cc 100644
--- a/src/components/ColorValues/HexEditor.test.cy.tsx
+++ b/src/components/ColorValues/HexEditor.test.cy.tsx
@@ -49,11 +49,12 @@ describe("hex editor tests", () => {
cy.get("@color").should("have.text", "000000");
cy.get("@value").blur();
- cy.get("@value").should("have.value", "#000000");
+ cy.get("@value").should("have.value", "#000");
cy.get("@color").should("have.text", "000000");
// Type a new value
- cy.get("@value").focus().type("{backspace}");
+ cy.get("@value").focus();
+ cy.get("@value").type("{backspace}");
cy.get("@value").should("have.value", "");
cy.get("@color").should("have.text", "000000");
@@ -62,11 +63,11 @@ describe("hex editor tests", () => {
cy.get("@color").should("have.text", "000000");
cy.get("@value").type("c");
- cy.get("@value").should("have.value", "#ABC");
- cy.get("@color").should("have.text", "AABBCC");
+ cy.get("@value").should("have.value", "abc");
+ cy.get("@color").should("have.text", "000000");
cy.get("@value").blur();
- cy.get("@value").should("have.value", "#AABBCC");
+ cy.get("@value").should("have.value", "#ABC");
cy.get("@color").should("have.text", "AABBCC");
// Invalid blur resets to last valid color
diff --git a/src/components/ColorValues/SpaceEditor.test.cy.tsx b/src/components/ColorValues/SpaceEditor.test.cy.tsx
index e7d9acc..dfcaeca 100644
--- a/src/components/ColorValues/SpaceEditor.test.cy.tsx
+++ b/src/components/ColorValues/SpaceEditor.test.cy.tsx
@@ -102,8 +102,8 @@ describe("space editor tests", () => {
});
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("hsv-value").should("have.text", "HSV (179, 0.78, 0.29)");
+ cy.dataCy("hcl-value").should("have.text", "HCL (179, 0.78, 0.25)");
cy.dataCy("hex-value").should("have.text", "HEX: #104B4A");
});
});
diff --git a/src/components/ColorValues/ValueEditor.test.cy.tsx b/src/components/ColorValues/ValueEditor.test.cy.tsx
index dd3b068..76f2706 100644
--- a/src/components/ColorValues/ValueEditor.test.cy.tsx
+++ b/src/components/ColorValues/ValueEditor.test.cy.tsx
@@ -55,7 +55,7 @@ describe("component editor tests", () => {
cy.dataCy("R-slider")
.click()
.dataCy("R-slider-bar")
- .should("have.css", "width", "138px")
+ .should("have.css", "width", "140px")
.dataCy("R-value-input")
.should("have.value", "127");
@@ -80,7 +80,7 @@ describe("component editor tests", () => {
.type("100")
.should("have.value", "100")
.dataCy("R-slider-bar")
- .should("have.css", "width", "109px");
+ .should("have.css", "width", "110px");
// Scrolling input should update value
cy.dataCy("R-value-input")
@@ -134,7 +134,7 @@ describe("component editor tests", () => {
cy.dataCy("R-slider")
.click()
.dataCy("R-slider-bar")
- .should("have.css", "width", "138px")
+ .should("have.css", "width", "140px")
.dataCy("R-value-input")
.should("have.value", "127");
diff --git a/src/components/ColorValues/ValueEditor.tsx b/src/components/ColorValues/ValueEditor.tsx
index 01b958f..bcb9e3a 100644
--- a/src/components/ColorValues/ValueEditor.tsx
+++ b/src/components/ColorValues/ValueEditor.tsx
@@ -6,6 +6,7 @@ import * as colorlib from "colorlib";
import { ChevronLeft, ChevronRight } from "lucide-react";
import type { HexColorActions } from "@/hooks/color";
+import { extractHexValue, formatHexString } from "@/hooks/hex";
import { useScroll } from "@/hooks/scroll";
import { useSlider } from "@/hooks/slider";
import { onResize } from "@/hooks/window";
@@ -340,30 +341,6 @@ function useLongPressRepeat(
// Hex Editor //
// ---------- //
-const extractHexValue = (value: string): string | null => {
- const match = value.match(/^#?([0-9A-Fa-f]{6}|[0-9A-Fa-f]{3})$/);
- return match ? match[1] : null;
-};
-
-const formatHexString = (
- color: colorlib.Hex,
- preserveShortFormat: boolean = false,
-): string => {
- const hexValue = color.to_code();
-
- if (preserveShortFormat) {
- if (
- hexValue[0] === hexValue[1] &&
- hexValue[2] === hexValue[3] &&
- hexValue[4] === hexValue[5]
- ) {
- return `#${hexValue[0]}${hexValue[2]}${hexValue[4]}`;
- }
- }
-
- return `#${color.to_code()}`;
-};
-
export function HexEditor({
color,
actions,
@@ -375,24 +352,34 @@ export function HexEditor({
}) {
const [inputValue, setInputValue] = useState(formatHexString(color));
const [isShortHex, setIsShortHex] = useState(false);
+ const isFocused = useRef(false);
useEffect(() => {
- setInputValue(formatHexString(color, isShortHex));
+ if (!isFocused.current) {
+ setInputValue(formatHexString(color, isShortHex));
+ }
}, [color, isShortHex]);
+ const onFocus = (e: ChangeEvent
) => {
+ isFocused.current = true;
+ e.target.select();
+ };
+
const onChange = (e: ChangeEvent) => {
const value = e.target.value;
setInputValue(value);
+ };
- const hex = extractHexValue(value);
+ const onBlur = () => {
+ isFocused.current = false;
+ const hex = extractHexValue(inputValue);
if (hex) {
setIsShortHex(hex.length === 3);
const newColor = colorlib.Hex.from_code(hex);
actions.setHex(newColor);
+ setInputValue(formatHexString(newColor, isShortHex));
+ return;
}
- };
-
- const onBlur = () => {
setInputValue(formatHexString(color));
};
@@ -414,7 +401,7 @@ export function HexEditor({
value={inputValue}
onChange={onChange}
onBlur={onBlur}
- onFocus={(e) => e.target.select()}
+ onFocus={onFocus}
onKeyDown={handleKeyDown}
/>
diff --git a/src/components/PaletteEditor/PaletteEditor.module.css b/src/components/PaletteEditor/PaletteEditor.module.css
index 0b6b2b6..8075943 100644
--- a/src/components/PaletteEditor/PaletteEditor.module.css
+++ b/src/components/PaletteEditor/PaletteEditor.module.css
@@ -6,32 +6,461 @@
}
.actionBar {
- height: 40px;
+ padding: 10px 11px;
+ border-bottom: 1px solid #c9c9c9;
+ display: flex;
+}
+
+.actionBar .actionButton {
+ cursor: pointer;
+ margin: 4px;
+ background-color: #e7e7e7;
+ border-radius: 0;
+ border: 1px solid #e2e2e2;
+ box-shadow:
+ rgba(60, 64, 67, 0.3) 0px 1px 2px 0px,
+ rgba(60, 64, 67, 0.05) 0px 2px 6px 1px;
+ transition-property: background-color, box-shadow;
+ transition-duration: 150ms;
+ transition-timing-function: ease-out;
+}
+
+.actionBar .actionButton:hover {
+ background-color: #f0f0f0;
+ box-shadow:
+ rgba(60, 64, 67, 0.25) 0px 3px 6px 0px,
+ rgba(60, 64, 67, 0.1) 0px 4px 8px 2px;
+ transform: translateY(-1px);
+}
+
+.actionBar .actionButton:active {
+ background-color: #d1d1d1;
+ box-shadow:
+ rgba(60, 64, 67, 0.3) 0px 1px 1px 0px,
+ inset 0px 1px 2px rgba(0, 0, 0, 0.2);
+ transform: translateY(1px);
+ transition-duration: 50ms;
+}
+
+.actionBar .iconButton {
+ aspect-ratio: 1;
+}
+
+.actionBar .wordButton {
+ font-size: 14px;
+ padding: 4px 4px;
+}
+
+.actionBar .activeButton {
+ background-color: #f8b800;
+ border-color: #e1964b;
+ box-shadow:
+ rgba(60, 64, 67, 0.25) 0px 1px 2px 0px,
+ 0 0 8px 1px rgba(255, 163, 56, 0.5),
+ inset 0 0 4px rgba(255, 255, 255, 0.4);
+}
+
+.actionBar .activeButton:hover {
+ background-color: #fea944;
+ box-shadow:
+ rgba(60, 64, 67, 0.25) 0px 2px 4px 0px,
+ 0 0 12px 2px rgba(255, 163, 56, 0.6),
+ inset 0 0 4px rgba(255, 255, 255, 0.5);
+ transform: translateY(-0.5px);
+}
+
+.actionBar .activeButton:active {
+ background-color: #eb8d16;
+ box-shadow:
+ rgba(60, 64, 67, 0.2) 0px 0px 1px 0px,
+ 0 0 4px 0px rgba(255, 163, 56, 0.4),
+ inset 0px 1px 2px rgba(0, 0, 0, 0.2);
+ transform: translateY(0.5px);
+ transition-duration: 50ms;
}
.cardWrapper {
flex: 1;
- padding: 20px;
+ margin: 32px;
display: grid;
+ border-radius: 8px;
grid-template-columns: 1fr 1fr 4fr;
- grid-template-rows: 40px 1fr;
+ grid-template-rows: auto 1fr;
grid-template-areas:
- ". . card-header"
+ "sync sync card-header"
"preview selection palette";
+ overflow-y: auto;
+ /* border: 1px solid #ddd; */
+ box-shadow:
+ rgba(0, 0, 0, 0.19) 0px 10px 20px,
+ rgba(0, 0, 0, 0.23) 0px 6px 6px;
+}
+
+.sync {
+ grid-area: sync;
+ cursor: pointer;
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ align-items: center;
+ background-color: #f6f6f6;
+ border-right: 2px solid #aaa;
+ border-bottom: 2px solid #aaa;
+ transition-property: background-color;
+ transition-duration: 150ms;
+ transition-timing-function: ease-out;
+}
+
+.sync:hover {
+ background-color: #f1f1f1;
+}
+
+.sync:active {
+ background-color: #e7e7e7;
+}
+
+.sync .leftSpan {
+ flex: 1;
+ text-align: right;
+}
+
+.sync .middleSpan {
+ flex: 0 0 auto;
+ display: flex;
+ align-items: middle;
+ margin: 0px 8px;
+}
+
+.sync .rightSpan {
+ flex: 1;
+ text-align: left;
}
.cardHeader {
grid-area: card-header;
+ display: flex;
+ padding: 4px 8px 8px;
+ color: #292929;
+ background-color: #f6f6f6;
+ font-size: 20px;
+ font-weight: 700;
+ border-bottom: 2px solid #aaa;
+}
+
+.cardHeader .editableFieldWrapper {
+ display: flex;
+ align-items: center;
+}
+
+.cardHeader .editableField {
+}
+
+.cardHeader .editingField {
+ outline: none;
+}
+
+.cardHeader .editableFieldButton {
+ cursor: pointer;
+ color: black;
+ background-color: rgba(0, 0, 0, 0.1);
+ margin: 2px -2px 2px 8px;
+ padding: 5px 6px;
+ border: none;
+ border-radius: 4px;
+ transition-property: background-color;
+ transition-duration: 150ms;
+ transition-timing-function: ease-out;
+}
+
+.cardHeader .editableFieldButton:hover {
+ background-color: rgba(0, 0, 0, 0.15);
+}
+
+.cardHeader .editableFieldButton:active {
+ background-color: rgba(0, 0, 0, 0.2);
}
.pickerColor {
grid-area: preview;
+ display: flex;
+ align-items: center;
+ padding: 10px;
}
.paletteColor {
grid-area: selection;
+ display: flex;
+ align-items: center;
+ padding: 10px;
+}
+
+.pickerColor .arrowIndicator {
+ margin-left: auto;
+}
+
+@keyframes slideRight {
+ 0% {
+ transform: translateX(0);
+ }
+ 50% {
+ transform: translateX(3px);
+ }
+ 100% {
+ transform: translateX(0);
+ }
+}
+
+@keyframes slideLeft {
+ 0% {
+ transform: translateX(0);
+ }
+ 50% {
+ transform: translateX(-3px);
+ }
+ 100% {
+ transform: translateX(0);
+ }
+}
+
+.pickerColor:hover .arrowIndicator {
+ animation: slideRight 1.5s ease-in-out infinite;
+}
+
+.paletteColor:hover .arrowIndicator {
+ animation: slideLeft 1.5s ease-in-out infinite;
+}
+
+.previewPane .arrowIndicator {
+ transform: translateX(0);
+}
+
+.previewPane .arrowIndicatorDark {
+ color: rgba(0, 0, 0, 0.4);
+}
+
+.previewPane:hover .arrowIndicatorDark {
+ color: rgba(0, 0, 0, 0.7);
+}
+
+.previewPane:active .arrowIndicatorDark {
+ color: rgba(0, 0, 0, 0.5);
+}
+
+.previewPane .arrowIndicatorLight {
+ color: rgba(255, 255, 255, 0.4);
+}
+
+.previewPane:hover .arrowIndicatorLight {
+ color: rgba(255, 255, 255, 0.7);
+}
+
+.previewPane:active .arrowIndicatorLight {
+ color: rgba(255, 255, 255, 0.5);
}
.palette {
grid-area: palette;
+ display: flex;
+ flex-direction: column;
+ overflow-y: auto;
+ min-height: 0;
+ background-color: #f7f7f7;
+}
+
+.paletteRowWrapper {
+ flex: 1;
+ cursor: pointer;
+ min-height: 3em;
+}
+
+.paletteRowWrapper.draggable {
+ cursor: grab;
+}
+
+.paletteRowWrapper.dragging {
+ cursor: grabbing;
+}
+
+.paletteRowWrapper.dragging .paletteRow,
+.paletteRowWrapper.multiSelected .paletteRow {
+ transform: scale(0.96, 0.9);
+ border-radius: 8px;
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
+ z-index: 10;
+}
+
+.paletteRow {
+ width: 100%;
+ height: 100%;
+ position: relative;
+ transition:
+ transform 200ms ease-out,
+ border-radius 200ms ease-out;
+}
+
+.paletteRow .selectedIndicator {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+}
+
+.paletteRow .indicatorDark {
+ color: rgba(0, 0, 0, 0.7);
+}
+
+.paletteRow .indicatorLight {
+ color: rgba(255, 255, 255, 0.7);
+}
+
+.paletteRow .targettedIndicator {
+ opacity: 0;
+ visibility: hidden;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ transition: opacity 150ms ease-out;
+}
+
+.paletteRow:hover .targettedIndicator {
+ opacity: 1;
+ visibility: visible;
+}
+
+.modeDecorator {
+ position: absolute;
+ right: 8px;
+ top: 50%;
+ transform: translateY(-50%);
+ pointer-events: none;
+}
+
+.checkDecorator {
+ width: 20px;
+ height: 20px;
+ border-radius: 3px;
+ border: 2px solid;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.checkDecoratorDark {
+ color: rgba(0, 0, 0, 0.8);
+ border-color: rgba(0, 0, 0, 0.5);
+ background-color: rgba(255, 255, 255, 0.15);
+}
+.checkDecoratorLight {
+ color: rgba(255, 255, 255, 0.8);
+ border-color: rgba(255, 255, 255, 0.5);
+ background-color: rgba(0, 0, 0, 0.2);
+}
+
+.gripDark {
+ color: rgba(0, 0, 0, 0.4);
+}
+.gripLight {
+ color: rgba(255, 255, 255, 0.4);
+}
+
+.paletteRowDataWithDecorator {
+ padding-right: 36px;
+}
+
+.paletteRowData {
+ display: flex;
+ padding: 10px;
+ align-items: center;
+}
+
+.paletteRowData .colorName {
+ font-size: 14px;
+}
+
+.paletteRowData .colorHex {
+ font-size: 14px;
+ font-family: monospace;
+ margin-left: auto;
+}
+
+.paletteRow .editableFieldWrapper {
+ display: flex;
+ align-items: center;
+}
+
+.paletteRow .editableField,
+.paletteRow .field {
+ cursor: text;
+ border-radius: 4px;
+ margin-right: 2px;
+}
+
+.paletteRow .editableFieldDark,
+.paletteRow .fieldDark {
+ color: rgba(0, 0, 0, 0.95);
+ background-color: rgba(255, 255, 255, 0.4);
+}
+
+.paletteRow .editableFieldLight,
+.paletteRow .fieldLight {
+ color: rgba(255, 255, 255, 0.95);
+ background-color: rgba(0, 0, 0, 0.2);
+}
+
+.paletteRow .editableFieldButton {
+ transition-property: color;
+ transition-duration: 150ms;
+ transition-timing-function: ease-out;
+}
+
+.paletteRow .editableFieldButtonDark {
+ color: rgba(0, 0, 0, 0.5);
+ background-color: rgba(0, 0, 0, 0);
+}
+
+.paletteRow .editableFieldButtonDark:hover {
+ color: rgba(0, 0, 0, 0.8);
+}
+
+.paletteRow .editableFieldButtonDark:active {
+ color: rgba(0, 0, 0, 0.6);
+}
+
+.paletteRow .editableFieldButtonLight {
+ color: rgba(255, 255, 255, 0.5);
+ background-color: rgba(0, 0, 0, 0);
+}
+
+.paletteRow .editableFieldButtonLight:hover {
+ color: rgba(255, 255, 255, 0.8);
+}
+
+.paletteRow .editableFieldButtonLight:active {
+ color: rgba(255, 255, 255, 0.6);
+}
+
+.paletteRow .editingField {
+ outline: none;
+}
+
+.paletteRow .colorHex {
+ width: 124px;
+}
+
+.colorName .editableField,
+.colorName .field {
+ padding: 1px 8px;
+}
+
+.colorHex .editableField,
+.colorHex .field {
+ padding: 3px 8px;
+}
+
+.paletteRow .editableFieldButton {
+ cursor: pointer;
+ padding-left: 4px;
+ border: none;
+ border-radius: 4px;
}
diff --git a/src/components/PaletteEditor/PaletteEditor.test.cy.tsx b/src/components/PaletteEditor/PaletteEditor.test.cy.tsx
index 53949a7..deffed2 100644
--- a/src/components/PaletteEditor/PaletteEditor.test.cy.tsx
+++ b/src/components/PaletteEditor/PaletteEditor.test.cy.tsx
@@ -1,18 +1,40 @@
import { useReducer } from "react";
-import { Color } from "colorlib";
+import { Color, Hex as HexColor } from "colorlib";
import { HexEditor } from "@/components/ColorValues/ValueEditor";
import { colorReducer, createColorActions } from "@/hooks/color";
+import type { PaletteCardState } from "@/hooks/paletteCard";
import PaletteEditor from "./PaletteEditor";
-const initialState = {
+const initialPickerState = {
color: Color.from_hex("000"),
};
-function TestWrapper() {
- const [state, dispatch] = useReducer(colorReducer, initialState);
+const defaultPaletteCard = {
+ id: "card_id",
+ name: "Test Palette",
+ colors: [
+ { id: "red", name: "Red", hex: HexColor.from_code("FF0000") },
+ { id: "green", name: "Green", hex: HexColor.from_code("00FF00") },
+ { id: "blue", name: "Blue", hex: HexColor.from_code("0000FF") },
+ ],
+ selectedColorIds: [],
+};
+
+const defaultPaletteCardState = {
+ present: defaultPaletteCard,
+ history: [],
+ future: [],
+};
+
+function TestWrapper({
+ initialCardState,
+}: {
+ initialCardState?: PaletteCardState;
+}) {
+ const [state, dispatch] = useReducer(colorReducer, initialPickerState);
const actions = createColorActions(dispatch);
return (
@@ -21,6 +43,7 @@ function TestWrapper() {