Skip to content

Commit

Permalink
Add shave config to editor
Browse files Browse the repository at this point in the history
  • Loading branch information
tomkennedy22 committed May 13, 2024
1 parent 924d58d commit 544ac1c
Show file tree
Hide file tree
Showing 8 changed files with 586 additions and 9 deletions.
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
66 changes: 66 additions & 0 deletions public/editor/ColorPicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { type CSSProperties } from "react";
import { Sketch } from "./Sketch";
import {
Button,
PopoverTrigger,
PopoverContent,
Popover,
} from "@nextui-org/react";

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",
allowAlpha = false,
presetColors,
}: {
onClick?: () => void;
onChange: (hex: string) => void;
style?: CSSProperties;
value: string;
colorFormat: "hex" | "rgba";
allowAlpha: boolean;
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}
allowAlpha={allowAlpha}
presetColors={presetColors}
onChange={(color) => {
console.log("Sketch change", { color });

if (colorFormat === "rgba") {
onChange(rgbaObjToRgbaStr(color.rgba));
} else {
onChange(color.hex);
}
}}
/>
</PopoverContent>
</Popover>
);
};
26 changes: 20 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,10 @@ const FeatureSelector = ({
});
};

let colorFormat = gallerySectionConfig.colorFormat

Check failure on line 247 in public/editor/FeatureGallery.tsx

View workflow job for this annotation

GitHub Actions / build

'colorFormat' is never reassigned. Use 'const' instead
? gallerySectionConfig.colorFormat
: "hex";

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

let presetColors = hasMultipleColors

Check failure on line 263 in public/editor/FeatureGallery.tsx

View workflow job for this annotation

GitHub Actions / build

'presetColors' is never reassigned. Use 'const' instead
? gallerySectionConfig.renderOptions.valuesToRender.map(
(colorList: string[]) => colorList[colorIndex],
)
: 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}
allowAlpha={gallerySectionConfig.allowAlpha}
value={selectedColor}
/>
<Input
value={selectedColor}
isInvalid={!inputValidationArr[colorIndex]}
className="min-w-52"
onChange={(e) => {
colorInputOnChange({
newColorValue: e.target.value,
Expand Down
248 changes: 248 additions & 0 deletions public/editor/Sketch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
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,
hexToHsva,
color as handleColor,
type ColorResult,
} from "@uiw/color-convert";
import Swatch from "@uiw/react-color-swatch";
import { useEffect } from "react";

// 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-0 ps-2 pe-1"
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[];
allowAlpha: boolean;
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,
allowAlpha,
presetColors,
...other
} = props;

console.log("Sketch", { color, props });

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) => {
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>
{allowAlpha && (
<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) => {
console.log("newAlpha", { 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={allowAlpha ? undefined : false}
onChange={(result) => handleChange(result.hsva)}
/>
<EyeDropperButton onChange={handleHex} />
</div>
<Swatch
style={styleSwatch}
colors={presetColors}
color={hsvaToHex(hsva)}
onChange={(hsvColor) => handleChange(hsvColor)}
rectProps={{
style: styleSwatchRect,
}}
/>
</div>
);
},
);
Loading

0 comments on commit 544ac1c

Please sign in to comment.