diff --git a/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input.test.ts b/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input.test.ts new file mode 100644 index 000000000000..761a206496cd --- /dev/null +++ b/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input.test.ts @@ -0,0 +1,98 @@ +import { describe, test, expect } from "vitest"; +import { __testing__ } from "./css-value-input"; +const { getItemColor } = __testing__; + +describe("getItemColor", () => { + test("returns undefined for non-color keyword", () => { + expect(getItemColor({ type: "keyword", value: "auto" })).toBeUndefined(); + expect(getItemColor({ type: "keyword", value: "flex" })).toBeUndefined(); + }); + + test("returns the keyword value for a named color keyword", () => { + expect(getItemColor({ type: "keyword", value: "red" })).toBe("red"); + expect(getItemColor({ type: "keyword", value: "transparent" })).toBe( + "transparent" + ); + }); + + test("returns undefined for non-color var fallback types", () => { + expect( + getItemColor({ + type: "var", + value: "spacing", + fallback: { type: "unit", value: 8, unit: "px" }, + }) + ).toBeUndefined(); + expect( + getItemColor({ + type: "var", + value: "display", + fallback: { type: "keyword", value: "flex" }, + }) + ).toBeUndefined(); + }); + + test("returns color string for rgb var fallback", () => { + expect( + getItemColor({ + type: "var", + value: "brand", + fallback: { type: "rgb", r: 255, g: 0, b: 0, alpha: 1 }, + }) + ).toBe("rgb(255 0 0 / 1)"); + }); + + test("returns color string for color var fallback", () => { + const result = getItemColor({ + type: "var", + value: "brand", + fallback: { + type: "color", + colorSpace: "srgb", + components: [1, 0, 0], + alpha: 1, + }, + }); + expect(result).toMatch(/rgb/); + }); + + test("returns value for unparsed var fallback that is a valid color", () => { + expect( + getItemColor({ + type: "var", + value: "brand", + fallback: { type: "unparsed", value: "red" }, + }) + ).toBe("red"); + }); + + test("returns undefined for unparsed var fallback that is not a color", () => { + expect( + getItemColor({ + type: "var", + value: "spacing", + fallback: { type: "unparsed", value: "1rem" }, + }) + ).toBeUndefined(); + }); + + test("returns value for keyword var fallback that is a named color", () => { + expect( + getItemColor({ + type: "var", + value: "brand", + fallback: { type: "keyword", value: "red" }, + }) + ).toBe("red"); + }); + + test("returns undefined for var without fallback", () => { + expect(getItemColor({ type: "var", value: "brand" })).toBeUndefined(); + }); + + test("returns undefined for unit item", () => { + expect( + getItemColor({ type: "unit", value: 16, unit: "px" }) + ).toBeUndefined(); + }); +}); diff --git a/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input.tsx b/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input.tsx index d4175be0b176..3d614f8e2d82 100644 --- a/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input.tsx +++ b/apps/builder/app/builder/features/style-panel/shared/css-value-input/css-value-input.tsx @@ -16,7 +16,6 @@ import { theme, Flex, styled, - Text, ColorThumb, } from "@webstudio-is/design-system"; import type { @@ -45,6 +44,7 @@ import { camelCaseProperty, declarationDescriptions, isValidDeclaration, + parseColor, } from "@webstudio-is/css-data"; import { $selectedInstanceSizes } from "~/shared/nano-states"; import { convertUnits } from "./convert-units"; @@ -363,6 +363,20 @@ const itemToString = (item: CssValueInputValue | null) => { const Description = styled(Box, { width: theme.spacing[27] }); +// Returns the CSS color string to show as a color swatch for a dropdown item, +// or undefined if the item has no meaningful color preview. +const getItemColor = (item: CssValueInputValue): string | undefined => { + let colorString: string | undefined; + if (item.type === "var" && item.fallback !== undefined) { + colorString = toValue(item.fallback); + } else if (item.type === "keyword") { + colorString = item.value; + } + if (colorString !== undefined && parseColor(colorString) !== undefined) { + return colorString; + } +}; + /** * Common: * - Free text editing @@ -943,22 +957,19 @@ export const CssValueInput = ({ {...getItemProps({ item, index })} key={index} > - {item.type === "var" ? ( - - --{item.value} - {item.fallback?.type === "unit" && ( - - {toValue(item.fallback)} - - )} - {(item.fallback?.type === "rgb" || - item.fallback?.type === "color") && ( - - )} - - ) : ( - itemToString(item) - )} + {(() => { + const label = itemToString(item); + const colorValue = getItemColor(item); + if (colorValue === undefined) { + return label; + } + return ( + + {label} + + + ); + })()} ))} @@ -974,3 +985,5 @@ export const CssValueInput = ({ ); }; + +export const __testing__ = { getItemColor }; diff --git a/apps/builder/app/builder/features/style-panel/shared/css-value-input/parse-intermediate-or-invalid-value.ts.test.ts b/apps/builder/app/builder/features/style-panel/shared/css-value-input/parse-intermediate-or-invalid-value.test.ts similarity index 100% rename from apps/builder/app/builder/features/style-panel/shared/css-value-input/parse-intermediate-or-invalid-value.ts.test.ts rename to apps/builder/app/builder/features/style-panel/shared/css-value-input/parse-intermediate-or-invalid-value.test.ts