Skip to content

Commit f3bbde9

Browse files
committed
feat: Add Color Picker
1 parent 0b6fbee commit f3bbde9

4 files changed

Lines changed: 193 additions & 3 deletions

File tree

package-lock.json

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@
9595
"pyodide": "^0.29.3",
9696
"random-words": "^2.0.1",
9797
"react": "^19.2.4",
98+
"react-colorful": "^5.6.1",
9899
"react-day-picker": "^9.13.2",
99100
"react-dom": "^19.2.4",
100101
"react-error-boundary": "^6.1.1",

src/components/shared/ReactFlow/FlowCanvas/FlexNode/FlexNodeEditor.tsx

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { type ChangeEvent, useEffect, useState } from "react";
33
import { ContentBlock } from "@/components/shared/ContextPanel/Blocks/ContentBlock";
44
import { KeyValueList } from "@/components/shared/ContextPanel/Blocks/KeyValueList";
55
import { CopyText } from "@/components/shared/CopyText/CopyText";
6+
import { ColorPicker } from "@/components/ui/color";
67
import { Input } from "@/components/ui/input";
78
import { Label } from "@/components/ui/label";
89
import { BlockStack, InlineStack } from "@/components/ui/layout";
@@ -199,8 +200,47 @@ const ColorEditor = ({
199200
flexNode: FlexNodeData;
200201
readOnly: boolean;
201202
}) => {
203+
const {
204+
componentSpec,
205+
currentSubgraphSpec,
206+
currentSubgraphPath,
207+
setComponentSpec,
208+
} = useComponentSpec();
209+
202210
const { properties } = flexNode;
203211

212+
const [backgroundColor, setBackgroundColor] = useState(properties.color);
213+
214+
const handleBackgroundColorChange = (newColor: string) => {
215+
setBackgroundColor(newColor);
216+
saveColors(newColor);
217+
};
218+
219+
const saveColors = (newBackgroundColor: string) => {
220+
const updatedSubgraphSpec = updateFlexNodeInComponentSpec(
221+
currentSubgraphSpec,
222+
{
223+
...flexNode,
224+
properties: {
225+
...properties,
226+
color: newBackgroundColor,
227+
},
228+
},
229+
);
230+
231+
const newRootSpec = updateSubgraphSpec(
232+
componentSpec,
233+
currentSubgraphPath,
234+
updatedSubgraphSpec,
235+
);
236+
237+
setComponentSpec(newRootSpec);
238+
};
239+
240+
useEffect(() => {
241+
setBackgroundColor(properties.color);
242+
}, [properties]);
243+
204244
if (readOnly) {
205245
return (
206246
<KeyValueList
@@ -221,9 +261,10 @@ const ColorEditor = ({
221261
<BlockStack gap="1">
222262
<InlineStack gap="4" blockAlign="center">
223263
<Paragraph size="xs">Background</Paragraph>
224-
<div
225-
className="aspect-square h-4 rounded-full border border-muted-foreground"
226-
style={{ backgroundColor: properties.color }}
264+
<ColorPicker
265+
title="Background Color"
266+
color={backgroundColor}
267+
setColor={handleBackgroundColorChange}
227268
/>
228269
<CopyText className="text-xs font-mono">{properties.color}</CopyText>
229270
</InlineStack>

src/components/ui/color.tsx

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import { useState } from "react";
2+
import { HexColorPicker } from "react-colorful";
3+
4+
import { useDebouncedState } from "@/hooks/useDebouncedState";
5+
import { cn } from "@/lib/utils";
6+
7+
import { Button } from "./button";
8+
import {
9+
Collapsible,
10+
CollapsibleContent,
11+
CollapsibleTrigger,
12+
} from "./collapsible";
13+
import { Icon } from "./icon";
14+
import { Input } from "./input";
15+
import { BlockStack, InlineStack } from "./layout";
16+
import { Popover, PopoverContent, PopoverTrigger } from "./popover";
17+
import { Heading } from "./typography";
18+
19+
const PRESET_COLORS = [
20+
"#FFF9C4",
21+
"#C8E6C9",
22+
"#BBDEFB",
23+
"#D1C4E9",
24+
"#FFE0B2",
25+
"#EF9A9A",
26+
"#FFCCBC",
27+
"#D7CCC8",
28+
"#F5F5F5",
29+
"#CFD8DC",
30+
"#B0BEC5",
31+
"transparent",
32+
];
33+
34+
interface ColorPickerProps {
35+
title?: string;
36+
color: string;
37+
debounceMs?: number;
38+
setColor: (color: string) => void;
39+
onClose?: () => void;
40+
}
41+
42+
export const ColorPicker = ({
43+
color,
44+
title,
45+
debounceMs = 300,
46+
setColor,
47+
onClose,
48+
}: ColorPickerProps) => {
49+
const [open, setOpen] = useState(false);
50+
const [localColor, setLocalColor] = useState(color);
51+
52+
const { clearDebounce, updatePreviousState } = useDebouncedState(
53+
localColor,
54+
setColor,
55+
() => false,
56+
{ debounceMs },
57+
);
58+
59+
const handleOpenChange = (isOpen: boolean) => {
60+
setOpen(isOpen);
61+
if (!isOpen) {
62+
onClose?.();
63+
}
64+
};
65+
66+
const handlePresetClick = (preset: string) => {
67+
clearDebounce();
68+
setLocalColor(preset);
69+
setColor(preset);
70+
updatePreviousState(preset);
71+
};
72+
73+
return (
74+
<Popover open={open} onOpenChange={handleOpenChange}>
75+
<PopoverTrigger>
76+
<div
77+
className="aspect-square h-4 rounded-full border border-muted-foreground cursor-pointer"
78+
style={{ backgroundColor: color }}
79+
/>
80+
</PopoverTrigger>
81+
<PopoverContent className="w-fit">
82+
<BlockStack gap="4" align="center">
83+
<Heading level={3}>{title ?? "Pick a color"}</Heading>
84+
<InlineStack gap="2" className="grid grid-cols-6 px-2">
85+
{PRESET_COLORS.map((preset) => (
86+
<div
87+
key={preset}
88+
className={cn(
89+
"aspect-square w-6 rounded-sm border border-muted-foreground cursor-pointer relative overflow-hidden",
90+
{
91+
"ring-2 ring-offset-2 ring-black": preset === color,
92+
},
93+
)}
94+
style={{ backgroundColor: preset }}
95+
onClick={() => handlePresetClick(preset)}
96+
>
97+
{preset === "transparent" && (
98+
<div className="absolute inset-0 flex items-center justify-center">
99+
<div className="h-px w-full bg-red-500 rotate-45 origin-center" />
100+
</div>
101+
)}
102+
</div>
103+
))}
104+
</InlineStack>
105+
<div className="relative w-full">
106+
<Input
107+
value={localColor}
108+
onChange={(e) => setLocalColor(e.target.value)}
109+
/>
110+
<Collapsible>
111+
<InlineStack blockAlign="center" gap="1">
112+
<CollapsibleTrigger asChild>
113+
<Button
114+
size="icon"
115+
variant="ghost"
116+
className="absolute right-2 top-0 hover:bg-transparent hover:text-black!"
117+
style={{
118+
color:
119+
localColor === "transparent" ? "lightgray" : localColor,
120+
filter: "brightness(0.8)",
121+
}}
122+
>
123+
<Icon name="Palette" />
124+
<span className="sr-only">Toggle</span>
125+
</Button>
126+
</CollapsibleTrigger>
127+
</InlineStack>
128+
<CollapsibleContent className="mt-2">
129+
<HexColorPicker color={localColor} onChange={setLocalColor} />
130+
</CollapsibleContent>
131+
</Collapsible>
132+
</div>
133+
</BlockStack>
134+
</PopoverContent>
135+
</Popover>
136+
);
137+
};

0 commit comments

Comments
 (0)