Skip to content

Commit 58df4bc

Browse files
authored
experimental: show color picker in advanced panel (#4206)
Ref #3399 Now users can tinker colors in css variables ![Screenshot 2024-10-03 at 14 47 26](https://github.com/user-attachments/assets/fc48334a-fc72-41ee-922b-1ae31039fe7a)
1 parent 5031292 commit 58df4bc

File tree

4 files changed

+112
-60
lines changed

4 files changed

+112
-60
lines changed

apps/builder/app/builder/features/style-panel/sections/advanced/advanced.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { colord } from "colord";
12
import { useEffect, useMemo, useRef, useState, type ReactNode } from "react";
23
import { computed } from "nanostores";
34
import { matchSorter } from "match-sorter";
@@ -22,6 +23,7 @@ import {
2223
import { useStore } from "@nanostores/react";
2324
import {
2425
hyphenateProperty,
26+
toValue,
2527
type StyleProperty,
2628
} from "@webstudio-is/css-engine";
2729
import {
@@ -39,6 +41,7 @@ import {
3941
import { getDots } from "../../shared/style-section";
4042
import { PropertyInfo } from "../../property-label";
4143
import { sections } from "../sections";
44+
import { ColorPopover } from "../../shared/color-picker";
4245

4346
// Only here to keep the same section module interface
4447
export const properties = [];
@@ -229,13 +232,33 @@ const AdvancedPropertyValue = ({
229232
inputRef.current?.focus();
230233
}
231234
}, [autoFocus]);
235+
const isColor = colord(toValue(styleDecl.usedValue)).isValid();
232236
return (
233237
<CssValueInputContainer
234238
inputRef={inputRef}
235239
variant="chromeless"
236240
size="2"
237241
text="mono"
238242
fieldSizing="content"
243+
prefix={
244+
isColor && (
245+
<ColorPopover
246+
size={1}
247+
value={styleDecl.usedValue}
248+
onChange={(styleValue) => {
249+
const options = { isEphemeral: true, listed: true };
250+
if (styleValue) {
251+
setProperty(property)(styleValue, options);
252+
} else {
253+
deleteProperty(property, options);
254+
}
255+
}}
256+
onChangeComplete={(styleValue) => {
257+
setProperty(property)(styleValue);
258+
}}
259+
/>
260+
)
261+
}
239262
property={property}
240263
styleSource={styleDecl.source.name}
241264
keywords={[

apps/builder/app/builder/features/style-panel/shared/color-picker.tsx

Lines changed: 42 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -176,25 +176,21 @@ type ColorPickerProps = {
176176
disabled?: boolean;
177177
};
178178

179-
export const ColorPicker = ({
179+
export const ColorPopover = ({
180+
size,
180181
value,
181-
currentColor,
182-
keywords,
183-
property,
184-
disabled,
185182
onChange,
186183
onChangeComplete,
187-
onAbort,
188-
}: ColorPickerProps) => {
184+
}: {
185+
size?: 1 | 2;
186+
value: StyleValue;
187+
onChange: (value: undefined | StyleValue) => void;
188+
onChangeComplete: (value: StyleValue) => void;
189+
}) => {
189190
const [displayColorPicker, setDisplayColorPicker] = useState(false);
190191
const { enableCanvasPointerEvents, disableCanvasPointerEvents } =
191192
useDisableCanvasPointerEvents();
192193

193-
const [intermediateValue, setIntermediateValue] = useState<
194-
StyleValue | IntermediateStyleValue
195-
>();
196-
const currentValue = intermediateValue ?? value;
197-
198194
const handleOpenChange = (open: boolean) => {
199195
setDisplayColorPicker(open);
200196
if (open) {
@@ -209,15 +205,16 @@ export const ColorPicker = ({
209205
enableCanvasPointerEvents();
210206
};
211207

212-
const prefix = (
208+
return (
213209
<Popover modal open={displayColorPicker} onOpenChange={handleOpenChange}>
214210
<PopoverTrigger
215211
asChild
216212
aria-label="Open color picker"
217213
onClick={() => setDisplayColorPicker((shown) => !shown)}
218214
>
219215
<ColorThumb
220-
color={styleValueToRgbaColor(currentColor)}
216+
color={styleValueToRgbaColor(value)}
217+
size={size}
221218
css={{ margin: theme.spacing[2] }}
222219
tabIndex={-1}
223220
/>
@@ -230,7 +227,36 @@ export const ColorPicker = ({
230227
}}
231228
>
232229
<ColorPickerPopoverContent
233-
value={currentValue}
230+
value={value}
231+
onChange={onChange}
232+
onChangeComplete={onChangeComplete}
233+
/>
234+
</PopoverContent>
235+
</Popover>
236+
);
237+
};
238+
239+
export const ColorPicker = ({
240+
value,
241+
currentColor,
242+
keywords,
243+
property,
244+
disabled,
245+
onChange,
246+
onChangeComplete,
247+
onAbort,
248+
}: ColorPickerProps) => {
249+
const [intermediateValue, setIntermediateValue] = useState<
250+
StyleValue | IntermediateStyleValue
251+
>();
252+
253+
return (
254+
<CssValueInput
255+
aria-disabled={disabled}
256+
styleSource="default"
257+
prefix={
258+
<ColorPopover
259+
value={currentColor}
234260
onChange={(styleValue) => {
235261
setIntermediateValue(styleValue);
236262
if (styleValue) {
@@ -244,15 +270,7 @@ export const ColorPicker = ({
244270
onChangeComplete(value);
245271
}}
246272
/>
247-
</PopoverContent>
248-
</Popover>
249-
);
250-
251-
return (
252-
<CssValueInput
253-
aria-disabled={disabled}
254-
styleSource="default"
255-
prefix={prefix}
273+
}
256274
showSuffix={false}
257275
property={property}
258276
value={value}

apps/builder/app/builder/features/style-panel/shared/color-thumb.tsx

Lines changed: 40 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { rawTheme, theme, css, type CSS } from "@webstudio-is/design-system";
22
import { colord, type RgbaColor } from "colord";
3-
import { forwardRef, type ElementRef, type ComponentProps } from "react";
3+
import {
4+
forwardRef,
5+
type ElementRef,
6+
type ComponentProps,
7+
type CSSProperties,
8+
} from "react";
49
import { clamp } from "~/shared/math-utils";
510

611
const whiteColor: RgbaColor = { r: 255, g: 255, b: 255, a: 1 };
@@ -44,40 +49,48 @@ const lerpColor = (a: RgbaColor, b: RgbaColor, t: number) => {
4449
};
4550
};
4651

52+
const colorThumbSize = "--color-thumb-size";
53+
4754
const thumbStyle = css({
4855
display: "block",
49-
width: theme.spacing[10],
50-
height: theme.spacing[10],
56+
width: `var(${colorThumbSize}, 20px)`,
57+
height: `var(${colorThumbSize}, 20px)`,
5158
backgroundBlendMode: "difference",
5259
borderRadius: theme.borderRadius[2],
5360
borderWidth: 0,
5461
borderStyle: "solid",
5562
});
5663

57-
export const ColorThumb = forwardRef<
58-
ElementRef<"button">,
59-
Omit<ComponentProps<"button">, "color"> & { color?: RgbaColor; css?: CSS }
60-
>(({ color = transparentColor, css, ...rest }, ref) => {
61-
const background =
62-
color === undefined || color.a < 1
63-
? // Chessboard pattern 5x5
64-
`repeating-conic-gradient(rgba(0,0,0,0.22) 0% 25%, transparent 0% 50%) 0% 33.33% / 40% 40%, ${colord(color).toRgbString()}`
65-
: colord(color).toRgbString();
66-
const borderColor = calcBorderColor(color);
64+
type Props = Omit<ComponentProps<"button">, "color"> & {
65+
color?: RgbaColor;
66+
css?: CSS;
67+
size?: 1 | 2;
68+
};
6769

68-
return (
69-
<button
70-
{...rest}
71-
ref={ref}
72-
style={{
73-
background,
74-
borderColor: borderColor.toRgbString(),
75-
// Border becomes visible when color is close to white so that the thumb is visible in the white input.
76-
borderWidth: borderColor.alpha() === 0 ? 0 : 1,
77-
}}
78-
className={thumbStyle({ css })}
79-
/>
80-
);
81-
});
70+
export const ColorThumb = forwardRef<ElementRef<"button">, Props>(
71+
({ color = transparentColor, size = 2, css, ...rest }, ref) => {
72+
const background =
73+
color === undefined || color.a < 1
74+
? // Chessboard pattern 5x5
75+
`repeating-conic-gradient(rgba(0,0,0,0.22) 0% 25%, transparent 0% 50%) 0% 33.33% / 40% 40%, ${colord(color).toRgbString()}`
76+
: colord(color).toRgbString();
77+
const borderColor = calcBorderColor(color);
78+
79+
return (
80+
<button
81+
{...rest}
82+
ref={ref}
83+
style={{
84+
background,
85+
borderColor: borderColor.toRgbString(),
86+
// Border becomes visible when color is close to white so that the thumb is visible in the white input.
87+
borderWidth: borderColor.alpha() === 0 ? 0 : 1,
88+
[colorThumbSize as keyof CSSProperties]: size === 1 ? "16px" : "20px",
89+
}}
90+
className={thumbStyle({ css })}
91+
/>
92+
);
93+
}
94+
);
8295

8396
ColorThumb.displayName = "ColorThumb";

apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input.tsx

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,11 @@ import { convertUnits } from "./convert-units";
5151
import { mergeRefs } from "@react-aria/utils";
5252

5353
// We need to enable scrub on properties that can have numeric value.
54-
const canBeNumber = (property: StyleProperty) => {
55-
if (property in properties === false) {
56-
return true;
57-
}
58-
const { unitGroups } = properties[property as keyof typeof properties];
59-
return unitGroups.length !== 0;
54+
const canBeNumber = (property: StyleProperty, value: CssValueInputValue) => {
55+
const unitGroups =
56+
properties[property as keyof typeof properties]?.unitGroups ?? [];
57+
// allow scrubbing css variables with unit value
58+
return unitGroups.length !== 0 || value.type === "unit";
6059
};
6160

6261
// Subjective adjust ment based on how it feels on macbook/trackpad.
@@ -111,7 +110,7 @@ const useScrub = ({
111110
if (
112111
inputRefCurrent === null ||
113112
scrubRefCurrent === null ||
114-
canBeNumber(property) === false
113+
canBeNumber(property, valueRef.current) === false
115114
) {
116115
return;
117116
}
@@ -446,7 +445,6 @@ export const CssValueInput = ({
446445
highlightedIndex,
447446
} = useCombobox<CssValueInputValue>({
448447
// Used for description to match the item when nothing is highlighted yet and value is still in non keyword mode
449-
defaultHighlightedIndex: 0,
450448
items: keywords,
451449
value,
452450
selectedItem: props.value,
@@ -718,7 +716,7 @@ export const CssValueInput = ({
718716
color={value.type === "invalid" ? "error" : undefined}
719717
prefix={finalPrefix}
720718
suffix={suffix}
721-
css={{ cursor: "default", minWidth: "2em" }}
719+
css={{ cursor: "default", minWidth: "3em" }}
722720
text={text}
723721
/>
724722
</ComboboxAnchor>

0 commit comments

Comments
 (0)