Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add shave config to editor, and update all color pickers #47

Merged
merged 5 commits into from
May 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@
"@types/react": "^18.2.73",
"@types/react-dom": "^18.2.23",
"@vitejs/plugin-react-swc": "^3.6.0",
"@uiw/react-color-alpha": "^2.3.0",
"@uiw/react-color-editable-input": "^2.3.0",
"@uiw/react-color-editable-input-rgba": "^2.3.0",
"@uiw/react-color-hue": "^2.3.0",
"@uiw/react-color-saturation": "^2.3.0",
"@uiw/react-color-swatch": "^2.3.0",
"autoprefixer": "^10.4.19",
"babel-plugin-add-module-exports": "^1.0.4",
"chokidar": "^3.6.0",
Expand Down
63 changes: 63 additions & 0 deletions public/editor/ColorPicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { type CSSProperties } from "react";
import { Sketch } from "./Sketch";
import {
Button,
PopoverTrigger,
PopoverContent,
Popover,
} from "@nextui-org/react";
import { ColorFormat } from "./types";

const rgbaObjToRgbaStr = (rgbaObj: {
r: number;
g: number;
b: number;
a: number;
}): string => {
return `rgba(${rgbaObj.r}, ${rgbaObj.g}, ${rgbaObj.b}, ${rgbaObj.a})`;
};

export const ColorPicker = ({
onClick,
onChange,
style,
value,
colorFormat = "hex",
presetColors,
}: {
onClick?: () => void;
onChange: (hex: string) => void;
style?: CSSProperties;
value: string;
colorFormat: ColorFormat;
presetColors: string[];
}) => {
return (
<Popover showArrow placement="bottom">
<PopoverTrigger>
<Button
onClick={onClick}
className="border-2"
style={{
...style,
backgroundColor: value,
}}
/>
</PopoverTrigger>
<PopoverContent className="p-0">
<Sketch
color={value}
presetColors={presetColors}
colorFormat={colorFormat}
onChange={(color) => {
if (colorFormat === "rgba") {
onChange(rgbaObjToRgbaStr(color.rgba));
} else {
onChange(color.hex);
}
}}
/>
</PopoverContent>
</Popover>
);
};
23 changes: 17 additions & 6 deletions public/editor/FeatureGallery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
import { CombinedState, GallerySectionConfig, OverrideListItem } from "./types";
import { Face } from "../../src/Face";
import { deepCopy } from "../../src/utils";
import { ColorPicker } from "./ColorPicker";

const inputOnChange = ({
chosenValue,
Expand Down Expand Up @@ -243,6 +244,8 @@ const FeatureSelector = ({
});
};

const colorFormat = gallerySectionConfig.colorFormat || "hex";

return (
<div
key={sectionIndex}
Expand All @@ -255,28 +258,36 @@ const FeatureSelector = ({
// @ts-expect-error TS doesnt like conditional array vs string
hasMultipleColors ? selectedVal[colorIndex] : selectedVal;

let presetColors = hasMultipleColors
? gallerySectionConfig.renderOptions.valuesToRender.map(
(colorList: string[]) => colorList[colorIndex],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll fix this when I merge in a minute, but there's no need for this type annotation, TypeScript already knows it's string[]

)
: gallerySectionConfig.renderOptions.valuesToRender;

return (
<div key={colorIndex} className="w-48">
<div key={colorIndex} className="w-fit">
{colorIndex === 0 ? (
<label className="text-xs text-foreground-600 mb-2">
{gallerySectionConfig.text}
</label>
) : null}
<div key={colorIndex} className="flex gap-2">
<Input
type="color"
value={selectedColor}
onValueChange={(e) => {
<ColorPicker
onChange={(color) => {
colorInputOnChange({
newColorValue: e,
newColorValue: color,
hasMultipleColors,
colorIndex,
});
}}
colorFormat={colorFormat}
presetColors={presetColors}
value={selectedColor}
/>
<Input
value={selectedColor}
isInvalid={!inputValidationArr[colorIndex]}
className="min-w-52"
onChange={(e) => {
colorInputOnChange({
newColorValue: e.target.value,
Expand Down
258 changes: 258 additions & 0 deletions public/editor/Sketch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
import React, { useState, type CSSProperties } from "react";
import Saturation from "@uiw/react-color-saturation";
import Alpha from "@uiw/react-color-alpha";
import EditableInput from "@uiw/react-color-editable-input";
import RGBA from "@uiw/react-color-editable-input-rgba";
import Hue from "@uiw/react-color-hue";
import {
validHex,
type HsvaColor,
rgbaStringToHsva,
hsvaToHex,
hsvaToHexa,
hexToHsva,
color as handleColor,
type ColorResult,
} from "@uiw/color-convert";
import Swatch from "@uiw/react-color-swatch";
import { useEffect } from "react";
import { roundTwoDecimals } from "./utils";
import { ColorFormat } from "./types";

// Similar to https://github.com/uiwjs/react-color/blob/632d4e9201e26b42ee7d5bfeda407144e9a6e2f3/packages/color-sketch/src/index.tsx but with EyeDropper added

// https://gist.github.com/bkrmendy/f4582173f50fab209ddfef1377ab31e3
interface ColorSelectionOptions {
signal?: AbortSignal;
}
interface ColorSelectionResult {
sRGBHex: string;
}
interface EyeDropper {
open: (options?: ColorSelectionOptions) => Promise<ColorSelectionResult>;
}
interface EyeDropperConstructor {
new (): EyeDropper;
}
declare global {
interface Window {
EyeDropper?: EyeDropperConstructor | undefined;
}
}

const EyeDropperButton = ({
onChange,
}: {
onChange: (hex: string) => void;
}) => {
if (!window.EyeDropper) {
return null;
}

// https://icons.getbootstrap.com/icons/eyedropper/ v1.11.3
const eyedropperIcon = (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
viewBox="0 0 16 16"
>
<path d="M13.354.646a1.207 1.207 0 0 0-1.708 0L8.5 3.793l-.646-.647a.5.5 0 1 0-.708.708L8.293 5l-7.147 7.146A.5.5 0 0 0 1 12.5v1.793l-.854.853a.5.5 0 1 0 .708.707L1.707 15H3.5a.5.5 0 0 0 .354-.146L11 7.707l1.146 1.147a.5.5 0 0 0 .708-.708l-.647-.646 3.147-3.146a1.207 1.207 0 0 0 0-1.708zM2 12.707l7-7L10.293 7l-7 7H2z" />
</svg>
);

return (
<button
className="btn pt-1 ps-2 pe-1 h-fit"
type="button"
onClick={async () => {
const eyeDropper = new window.EyeDropper!();
try {
const result = await eyeDropper.open();
onChange(result.sRGBHex.slice(1));
} catch (err) {
// The user escaped the eyedropper mode, do nothing
}
}}
>
{eyedropperIcon}
</button>
);
};

export interface SketchProps
extends Omit<React.HTMLAttributes<HTMLDivElement>, "onChange" | "color"> {
prefixCls?: string;
width?: number;
color?: string | HsvaColor;
presetColors: string[];
colorFormat: ColorFormat;
onChange?: (newShade: ColorResult) => void;
}

const Bar = (props: { left?: string }) => (
<div
style={{
boxShadow: "rgb(0 0 0 / 60%) 0px 0px 2px",
width: 4,
top: 1,
bottom: 1,
left: props.left,
borderRadius: 1,
position: "absolute",
backgroundColor: "#fff",
}}
/>
);

export const Sketch = React.forwardRef<HTMLDivElement, SketchProps>(
(props, ref) => {
const {
prefixCls = "w-color-sketch",
className,
onChange,
width = 240,
color,
style,
colorFormat,
presetColors,
...other
} = props;

let formattedPresetColors = presetColors;
if (colorFormat === "rgba") {
formattedPresetColors = presetColors.map((rgbaColor) =>
hsvaToHexa(rgbaStringToHsva(rgbaColor)),
);
}

const [hsva, setHsva] = useState({ h: 209, s: 36, v: 90, a: 1 });
useEffect(() => {
if (typeof color === "string" && validHex(color)) {
setHsva(hexToHsva(color));
}
if (typeof color === "string" && color.startsWith("rgba")) {
setHsva(rgbaStringToHsva(color));
}
if (typeof color === "object") {
setHsva(color);
}
}, [color]);

const handleChange = (hsv: HsvaColor) => {
for (const [key, value] of Object.entries(hsv)) {
hsv[key as keyof HsvaColor] = roundTwoDecimals(value);
}
setHsva(hsv);
onChange && onChange(handleColor(hsv));
};

const handleHex = (value: string | number) => {
if (
typeof value === "string" &&
validHex(value) &&
/(3|6)/.test(String(value.length))
) {
handleChange(hexToHsva(value));
}
};
const handleSaturationChange = (newColor: HsvaColor) =>
handleChange({ ...hsva, ...newColor, a: hsva.a });
const styleMain = {
"--sketch-background": "rgb(255, 255, 255)",
"--sketch-box-shadow":
"rgb(0 0 0 / 15%) 0px 0px 0px 1px, rgb(0 0 0 / 15%) 0px 8px 16px",
"--sketch-swatch-box-shadow": "rgb(0 0 0 / 15%) 0px 0px 0px 1px inset",
"--sketch-swatch-border-top": "1px solid rgb(238, 238, 238)",
background: "var(--sketch-background)",
borderRadius: 4,
boxShadow: "var(--sketch-box-shadow)",
width,
...style,
} as CSSProperties;
const styleSwatch = {
borderTop: "var(--sketch-swatch-border-top)",
paddingTop: 10,
paddingLeft: 10,
} as CSSProperties;
const styleSwatchRect = {
marginRight: 10,
marginBottom: 10,
borderRadius: 3,
boxShadow: "var(--sketch-swatch-box-shadow)",
} as CSSProperties;
return (
<div
{...other}
className={`${prefixCls} ${className || ""}`}
ref={ref}
style={styleMain}
>
<div style={{ padding: "10px 10px 8px" }}>
<Saturation
hsva={hsva}
style={{ width: "auto", height: 150 }}
onChange={handleSaturationChange}
/>
<div style={{ display: "flex", marginTop: 4 }}>
<div style={{ flex: 1 }}>
<Hue
width="auto"
height={10}
hue={hsva.h}
pointer={Bar}
innerProps={{
style: { marginLeft: 1, marginRight: 5 },
}}
onChange={(newHue) => handleChange({ ...hsva, ...newHue })}
/>
</div>
</div>
{colorFormat === "rgba" && (
<div style={{ display: "flex", marginTop: 4 }}>
<div style={{ flex: 1 }}>
<Alpha
width="auto"
height={10}
hsva={hsva}
pointer={Bar}
innerProps={{
style: { marginLeft: 1, marginRight: 5 },
}}
onChange={(newHvsa) => {
handleChange({ ...hsva, ...newHvsa });
}}
/>
</div>
</div>
)}
</div>
<div style={{ display: "flex", margin: "0 10px 3px 10px" }}>
<EditableInput
label="Hex"
value={hsvaToHex(hsva).replace(/^#/, "").toLocaleUpperCase()}
onChange={(_, val) => handleHex(val)}
style={{ minWidth: 58 }}
/>
<RGBA
hsva={hsva}
style={{ marginLeft: 6 }}
aProps={colorFormat === "rgba" ? undefined : false}
onChange={(result) => handleChange(result.hsva)}
/>
<EyeDropperButton onChange={handleHex} />
tomkennedy22 marked this conversation as resolved.
Show resolved Hide resolved
</div>
<Swatch
style={styleSwatch}
colors={formattedPresetColors}
color={hsvaToHex(hsva)}
onChange={(hsvColor) => handleChange(hsvColor)}
rectProps={{
style: styleSwatchRect,
}}
/>
</div>
);
},
);
Loading