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