mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-11-09 05:26:24 +09:00
experimental: show color picker in advanced panel (#4206)
Ref https://github.com/webstudio-is/webstudio/issues/3399 Now users can tinker colors in css variables 
This commit is contained in:
1 parent
5031292cca
commit
58df4bc3a9
4 files changed
+112
-60
No files matched your search
@@ -1,3 +1,4 @@
|
||||
import { colord } from "colord";
|
||||
import { useEffect, useMemo, useRef, useState, type ReactNode } from "react";
|
||||
import { computed } from "nanostores";
|
||||
import { matchSorter } from "match-sorter";
|
||||
@@ -22,6 +23,7 @@ import {
|
||||
import { useStore } from "@nanostores/react";
|
||||
import {
|
||||
hyphenateProperty,
|
||||
toValue,
|
||||
type StyleProperty,
|
||||
} from "@webstudio-is/css-engine";
|
||||
import {
|
||||
@@ -39,6 +41,7 @@ import {
|
||||
import { getDots } from "../../shared/style-section";
|
||||
import { PropertyInfo } from "../../property-label";
|
||||
import { sections } from "../sections";
|
||||
import { ColorPopover } from "../../shared/color-picker";
|
||||
|
||||
// Only here to keep the same section module interface
|
||||
export const properties = [];
|
||||
@@ -229,6 +232,7 @@ const AdvancedPropertyValue = ({
|
||||
inputRef.current?.focus();
|
||||
}
|
||||
}, [autoFocus]);
|
||||
const isColor = colord(toValue(styleDecl.usedValue)).isValid();
|
||||
return (
|
||||
<CssValueInputContainer
|
||||
inputRef={inputRef}
|
||||
@@ -236,6 +240,25 @@ const AdvancedPropertyValue = ({
|
||||
size="2"
|
||||
text="mono"
|
||||
fieldSizing="content"
|
||||
prefix={
|
||||
isColor && (
|
||||
<ColorPopover
|
||||
size={1}
|
||||
value={styleDecl.usedValue}
|
||||
onChange={(styleValue) => {
|
||||
const options = { isEphemeral: true, listed: true };
|
||||
if (styleValue) {
|
||||
setProperty(property)(styleValue, options);
|
||||
} else {
|
||||
deleteProperty(property, options);
|
||||
}
|
||||
}}
|
||||
onChangeComplete={(styleValue) => {
|
||||
setProperty(property)(styleValue);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
property={property}
|
||||
styleSource={styleDecl.source.name}
|
||||
keywords={[
|
||||
|
||||
@@ -176,25 +176,21 @@ type ColorPickerProps = {
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const ColorPicker = ({
|
||||
export const ColorPopover = ({
|
||||
size,
|
||||
value,
|
||||
currentColor,
|
||||
keywords,
|
||||
property,
|
||||
disabled,
|
||||
onChange,
|
||||
onChangeComplete,
|
||||
onAbort,
|
||||
}: ColorPickerProps) => {
|
||||
}: {
|
||||
size?: 1 | 2;
|
||||
value: StyleValue;
|
||||
onChange: (value: undefined | StyleValue) => void;
|
||||
onChangeComplete: (value: StyleValue) => void;
|
||||
}) => {
|
||||
const [displayColorPicker, setDisplayColorPicker] = useState(false);
|
||||
const { enableCanvasPointerEvents, disableCanvasPointerEvents } =
|
||||
useDisableCanvasPointerEvents();
|
||||
|
||||
const [intermediateValue, setIntermediateValue] = useState<
|
||||
StyleValue | IntermediateStyleValue
|
||||
>();
|
||||
const currentValue = intermediateValue ?? value;
|
||||
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
setDisplayColorPicker(open);
|
||||
if (open) {
|
||||
@@ -209,7 +205,7 @@ export const ColorPicker = ({
|
||||
enableCanvasPointerEvents();
|
||||
};
|
||||
|
||||
const prefix = (
|
||||
return (
|
||||
<Popover modal open={displayColorPicker} onOpenChange={handleOpenChange}>
|
||||
<PopoverTrigger
|
||||
asChild
|
||||
@@ -217,7 +213,8 @@ export const ColorPicker = ({
|
||||
onClick={() => setDisplayColorPicker((shown) => !shown)}
|
||||
>
|
||||
<ColorThumb
|
||||
color={styleValueToRgbaColor(currentColor)}
|
||||
color={styleValueToRgbaColor(value)}
|
||||
size={size}
|
||||
css={{ margin: theme.spacing[2] }}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
@@ -230,7 +227,36 @@ export const ColorPicker = ({
|
||||
}}
|
||||
>
|
||||
<ColorPickerPopoverContent
|
||||
value={currentValue}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onChangeComplete={onChangeComplete}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export const ColorPicker = ({
|
||||
value,
|
||||
currentColor,
|
||||
keywords,
|
||||
property,
|
||||
disabled,
|
||||
onChange,
|
||||
onChangeComplete,
|
||||
onAbort,
|
||||
}: ColorPickerProps) => {
|
||||
const [intermediateValue, setIntermediateValue] = useState<
|
||||
StyleValue | IntermediateStyleValue
|
||||
>();
|
||||
|
||||
return (
|
||||
<CssValueInput
|
||||
aria-disabled={disabled}
|
||||
styleSource="default"
|
||||
prefix={
|
||||
<ColorPopover
|
||||
value={currentColor}
|
||||
onChange={(styleValue) => {
|
||||
setIntermediateValue(styleValue);
|
||||
if (styleValue) {
|
||||
@@ -244,15 +270,7 @@ export const ColorPicker = ({
|
||||
onChangeComplete(value);
|
||||
}}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
|
||||
return (
|
||||
<CssValueInput
|
||||
aria-disabled={disabled}
|
||||
styleSource="default"
|
||||
prefix={prefix}
|
||||
}
|
||||
showSuffix={false}
|
||||
property={property}
|
||||
value={value}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { rawTheme, theme, css, type CSS } from "@webstudio-is/design-system";
|
||||
import { colord, type RgbaColor } from "colord";
|
||||
import { forwardRef, type ElementRef, type ComponentProps } from "react";
|
||||
import {
|
||||
forwardRef,
|
||||
type ElementRef,
|
||||
type ComponentProps,
|
||||
type CSSProperties,
|
||||
} from "react";
|
||||
import { clamp } from "~/shared/math-utils";
|
||||
|
||||
const whiteColor: RgbaColor = { r: 255, g: 255, b: 255, a: 1 };
|
||||
@@ -44,40 +49,48 @@ const lerpColor = (a: RgbaColor, b: RgbaColor, t: number) => {
|
||||
};
|
||||
};
|
||||
|
||||
const colorThumbSize = "--color-thumb-size";
|
||||
|
||||
const thumbStyle = css({
|
||||
display: "block",
|
||||
width: theme.spacing[10],
|
||||
height: theme.spacing[10],
|
||||
width: `var(${colorThumbSize}, 20px)`,
|
||||
height: `var(${colorThumbSize}, 20px)`,
|
||||
backgroundBlendMode: "difference",
|
||||
borderRadius: theme.borderRadius[2],
|
||||
borderWidth: 0,
|
||||
borderStyle: "solid",
|
||||
});
|
||||
|
||||
export const ColorThumb = forwardRef<
|
||||
ElementRef<"button">,
|
||||
Omit<ComponentProps<"button">, "color"> & { color?: RgbaColor; css?: CSS }
|
||||
>(({ color = transparentColor, css, ...rest }, ref) => {
|
||||
const background =
|
||||
color === undefined || color.a < 1
|
||||
? // Chessboard pattern 5x5
|
||||
`repeating-conic-gradient(rgba(0,0,0,0.22) 0% 25%, transparent 0% 50%) 0% 33.33% / 40% 40%, ${colord(color).toRgbString()}`
|
||||
: colord(color).toRgbString();
|
||||
const borderColor = calcBorderColor(color);
|
||||
type Props = Omit<ComponentProps<"button">, "color"> & {
|
||||
color?: RgbaColor;
|
||||
css?: CSS;
|
||||
size?: 1 | 2;
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
{...rest}
|
||||
ref={ref}
|
||||
style={{
|
||||
background,
|
||||
borderColor: borderColor.toRgbString(),
|
||||
// Border becomes visible when color is close to white so that the thumb is visible in the white input.
|
||||
borderWidth: borderColor.alpha() === 0 ? 0 : 1,
|
||||
}}
|
||||
className={thumbStyle({ css })}
|
||||
/>
|
||||
);
|
||||
});
|
||||
export const ColorThumb = forwardRef<ElementRef<"button">, Props>(
|
||||
({ color = transparentColor, size = 2, css, ...rest }, ref) => {
|
||||
const background =
|
||||
color === undefined || color.a < 1
|
||||
? // Chessboard pattern 5x5
|
||||
`repeating-conic-gradient(rgba(0,0,0,0.22) 0% 25%, transparent 0% 50%) 0% 33.33% / 40% 40%, ${colord(color).toRgbString()}`
|
||||
: colord(color).toRgbString();
|
||||
const borderColor = calcBorderColor(color);
|
||||
|
||||
return (
|
||||
<button
|
||||
{...rest}
|
||||
ref={ref}
|
||||
style={{
|
||||
background,
|
||||
borderColor: borderColor.toRgbString(),
|
||||
// Border becomes visible when color is close to white so that the thumb is visible in the white input.
|
||||
borderWidth: borderColor.alpha() === 0 ? 0 : 1,
|
||||
[colorThumbSize as keyof CSSProperties]: size === 1 ? "16px" : "20px",
|
||||
}}
|
||||
className={thumbStyle({ css })}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ColorThumb.displayName = "ColorThumb";
|
||||
+7
-9
@@ -51,12 +51,11 @@ import { convertUnits } from "./convert-units";
|
||||
import { mergeRefs } from "@react-aria/utils";
|
||||
|
||||
// We need to enable scrub on properties that can have numeric value.
|
||||
const canBeNumber = (property: StyleProperty) => {
|
||||
if (property in properties === false) {
|
||||
return true;
|
||||
}
|
||||
const { unitGroups } = properties[property as keyof typeof properties];
|
||||
return unitGroups.length !== 0;
|
||||
const canBeNumber = (property: StyleProperty, value: CssValueInputValue) => {
|
||||
const unitGroups =
|
||||
properties[property as keyof typeof properties]?.unitGroups ?? [];
|
||||
// allow scrubbing css variables with unit value
|
||||
return unitGroups.length !== 0 || value.type === "unit";
|
||||
};
|
||||
|
||||
// Subjective adjust ment based on how it feels on macbook/trackpad.
|
||||
@@ -111,7 +110,7 @@ const useScrub = ({
|
||||
if (
|
||||
inputRefCurrent === null ||
|
||||
scrubRefCurrent === null ||
|
||||
canBeNumber(property) === false
|
||||
canBeNumber(property, valueRef.current) === false
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -446,7 +445,6 @@ export const CssValueInput = ({
|
||||
highlightedIndex,
|
||||
} = useCombobox<CssValueInputValue>({
|
||||
// Used for description to match the item when nothing is highlighted yet and value is still in non keyword mode
|
||||
defaultHighlightedIndex: 0,
|
||||
items: keywords,
|
||||
value,
|
||||
selectedItem: props.value,
|
||||
@@ -718,7 +716,7 @@ export const CssValueInput = ({
|
||||
color={value.type === "invalid" ? "error" : undefined}
|
||||
prefix={finalPrefix}
|
||||
suffix={suffix}
|
||||
css={{ cursor: "default", minWidth: "2em" }}
|
||||
css={{ cursor: "default", minWidth: "3em" }}
|
||||
text={text}
|
||||
/>
|
||||
</ComboboxAnchor>
|
||||
|
||||
Reference in new issue
Block a user