close
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
b5c717f
fix: emit valid CSS color space names in toValue()
kof Mar 24, 2026
59517ce
feat: support color-mix() in color inputs
kof Mar 24, 2026
a326d01
feat: support relative color syntax in color inputs
kof Mar 24, 2026
1b759b7
refactor: replace string heuristics in isValidDeclaration with AST-ba…
kof Mar 24, 2026
6abfbac
feat: replace react-colorful with hdr-color-input web component in co…
kof Mar 24, 2026
230da8f
feat: use hdr-color-input native trigger, hide text input via ::part,…
kof Mar 25, 2026
e4c6e5d
refactor: consolidate ColorPickerPopover into ColorPicker, inline hoo…
kof Mar 25, 2026
1aa21f1
refactor: rename variables, add css prop to ColorPicker, remove unuse…
kof Mar 25, 2026
a01b1d0
feat(css-engine): add hex as first-class ColorValue color space
kof Mar 25, 2026
d8ba492
refactor(color-picker): use ref-based callbacks, AbortController, poi…
kof Mar 25, 2026
6e2d56a
fix: mock hdr-color-input in builder tests + update hex colorSpace as…
kof Mar 25, 2026
584a04d
feat: open color picker by default in story for snapshotting; remove …
kof Mar 25, 2026
3020895
refactor: lazy-load hdr-color-input via dynamic import, remove alias …
kof Mar 25, 2026
8f9eaf7
refactor: remove vi.mock from color-picker.test (dynamic import makes…
kof Mar 25, 2026
868e29b
fix(color-picker): fix curly and exhaustive-deps lint errors
kof Mar 25, 2026
d06e5c9
fix(color-picker): await customElements.whenDefined before show/close…
kof Mar 25, 2026
903daa2
fix(tests): polyfill browser globals so hdr-color-input can be import…
kof Mar 25, 2026
d0a5026
refactor(tests): single shared test-setup via @webstudio-is/design-sy…
kof Mar 25, 2026
2f16b1e
fix(test-setup): use any cast to avoid stub prototype mismatch type e…
kof Mar 25, 2026
0ed1e0e
Merge branch 'main' into color-formats
kof Mar 26, 2026
bb253be
test
kof Mar 26, 2026
7344ed1
fix(tests): use jsdom as default builder env, @vitest-environment nod…
kof Mar 26, 2026
9772ee0
refactor(tests): use environmentMatchGlobs for *.server.test.* instea…
kof Mar 26, 2026
9ed7d72
refactor(pubsub): run create.test.ts in jsdom, replace manual window …
kof Mar 26, 2026
ae176a4
test(pubsub): replace async timer waits with vi.useFakeTimers / vi.ru…
kof Mar 26, 2026
ae9224c
increase waitBeforeScreenshot to 2000ms for tall stories
kof Mar 26, 2026
e662745
fix: gradient color picker not applying color values
kof Mar 26, 2026
ad071f6
fix: attempt to fix native select GPU rendering glitch on Windows
kof Mar 26, 2026
58c00a9
fix: force color-scheme: light on color-input to fix native select vi…
kof Mar 26, 2026
af17707
font-size/dark theme
kof Mar 26, 2026
6c08773
light mode
kof Mar 26, 2026
4f3e041
fix: override --contrast on color-input to account for alpha transpar…
kof Mar 26, 2026
44bd940
fix color picker inputs
kof Mar 26, 2026
76377dc
fix: use optional chaining for shadow DOM preview access in updateCon…
kof Mar 26, 2026
bf788f9
fix: remove unused React import
kof Mar 26, 2026
bdd8f93
refactor: centralize colorjs into css-engine color module
kof Mar 26, 2026
1595495
refactor: extract overrideContrast into shared useCallback
kof Mar 27, 2026
ff70df5
test
kof Mar 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -638,7 +638,7 @@ describe("styleValueToColor", () => {
};
expect(styleValueToColor(style)).toEqual({
type: "color",
colorSpace: "srgb",
colorSpace: "hex",
components: [0, 0, 1],
alpha: 1,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
type VarValue,
type CssProperty,
} from "@webstudio-is/css-engine";
import { Box, ColorPickerPopover } from "@webstudio-is/design-system";
import { Box, ColorPicker } from "@webstudio-is/design-system";
import { CssValueInput } from "./css-value-input";
import type { IntermediateStyleValue } from "./css-value-input/css-value-input";

Expand Down Expand Up @@ -43,7 +43,7 @@ export const ColorPickerControl = ({
styleSource="default"
prefix={
<Box css={{ paddingLeft: 2 }}>
<ColorPickerPopover
<ColorPicker
value={currentColor}
onChange={(styleValue) => {
setIntermediateValue(styleValue);
Expand Down Expand Up @@ -80,6 +80,7 @@ export const ColorPickerControl = ({
styleValue.type === "color" ||
styleValue.type === "keyword" ||
styleValue.type === "var" ||
styleValue.type === "unparsed" ||
styleValue.type === "invalid"
) {
setIntermediateValue(styleValue);
Expand All @@ -104,7 +105,8 @@ export const ColorPickerControl = ({
value.type === "rgb" ||
value.type === "color" ||
value.type === "keyword" ||
value.type === "var"
value.type === "var" ||
value.type === "unparsed"
) {
setIntermediateValue(undefined);
onChangeComplete(value);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,7 @@ test("fallback further to valid values", () => {
["background-attachment", parseCssValue("background-attachment", "scroll")],
["background-origin", parseCssValue("background-origin", "padding-box")],
["background-clip", parseCssValue("background-clip", "border-box")],
[
"background-color",
parseCssValue("background-color", "rgba(255, 255, 255, 1)"),
],
["background-color", parseCssValue("background-color", "#fff")],
]);
expect(parseCssFragment("#fff", ["background-image", "background"])).toEqual(
result
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -470,7 +470,7 @@ describe("Colors", () => {
expect(result).toEqual({
type: "color",
alpha: 1,
colorSpace: "srgb",
colorSpace: "hex",
components: [1, 0, 0],
});
});
Expand All @@ -484,7 +484,7 @@ describe("Colors", () => {
expect(result).toEqual({
type: "color",
alpha: 1,
colorSpace: "srgb",
colorSpace: "hex",
components: [0.9412, 0.9333, 0.0588],
});
});
Expand All @@ -498,7 +498,7 @@ describe("Colors", () => {
expect(result).toEqual({
type: "color",
alpha: 1,
colorSpace: "srgb",
colorSpace: "hex",
components: [1, 0, 0],
});
});
Expand All @@ -512,7 +512,7 @@ describe("Colors", () => {
expect(result).toEqual({
type: "color",
alpha: 1,
colorSpace: "srgb",
colorSpace: "hex",
components: [0.9412, 0.9333, 0.0588],
});
});
Expand All @@ -531,6 +531,188 @@ describe("Colors", () => {
components: [0.0392, 0.0784, 0.1176],
});
});

test("hsl color", () => {
expect(
parseIntermediateOrInvalidValue("color", {
type: "intermediate",
value: "hsl(120 100% 50%)",
})
).toEqual({
type: "color",
colorSpace: "hsl",
alpha: 1,
components: [120, 100, 50],
});
});

test("hwb color", () => {
expect(
parseIntermediateOrInvalidValue("color", {
type: "intermediate",
value: "hwb(120 0% 0%)",
})
).toEqual({
type: "color",
colorSpace: "hwb",
alpha: 1,
components: [120, 0, 0],
});
});

test("lab color", () => {
expect(
parseIntermediateOrInvalidValue("color", {
type: "intermediate",
value: "lab(50 20 30)",
})
).toEqual({
type: "color",
colorSpace: "lab",
alpha: 1,
components: [50, 20, 30],
});
});

test("lch color", () => {
expect(
parseIntermediateOrInvalidValue("color", {
type: "intermediate",
value: "lch(50 40 120)",
})
).toEqual({
type: "color",
colorSpace: "lch",
alpha: 1,
components: [50, 40, 120],
});
});

test("oklab color", () => {
expect(
parseIntermediateOrInvalidValue("color", {
type: "intermediate",
value: "oklab(0.7 0.1 -0.1)",
})
).toEqual({
type: "color",
colorSpace: "oklab",
alpha: 1,
components: [0.7, 0.1, -0.1],
});
});

test("oklch color", () => {
expect(
parseIntermediateOrInvalidValue("color", {
type: "intermediate",
value: "oklch(0.7 0.1 180)",
})
).toEqual({
type: "color",
colorSpace: "oklch",
alpha: 1,
components: [0.7, 0.1, 180],
});
});

test("srgb-linear color", () => {
expect(
parseIntermediateOrInvalidValue("color", {
type: "intermediate",
value: "color(srgb-linear 1 0 0)",
})
).toEqual({
type: "color",
colorSpace: "srgb-linear",
alpha: 1,
components: [1, 0, 0],
});
});

test("display-p3 color", () => {
expect(
parseIntermediateOrInvalidValue("color", {
type: "intermediate",
value: "color(display-p3 0.4 0.6 0.3)",
})
).toEqual({
type: "color",
colorSpace: "p3",
alpha: 1,
components: [0.4, 0.6, 0.3],
});
});

test("a98-rgb color", () => {
expect(
parseIntermediateOrInvalidValue("color", {
type: "intermediate",
value: "color(a98-rgb 0.4 0.6 0.3)",
})
).toEqual({
type: "color",
colorSpace: "a98rgb",
alpha: 1,
components: [0.4, 0.6, 0.3],
});
});

test("prophoto-rgb color", () => {
expect(
parseIntermediateOrInvalidValue("color", {
type: "intermediate",
value: "color(prophoto-rgb 0.4 0.6 0.3)",
})
).toEqual({
type: "color",
colorSpace: "prophoto",
alpha: 1,
components: [0.4, 0.6, 0.3],
});
});

test("rec2020 color", () => {
expect(
parseIntermediateOrInvalidValue("color", {
type: "intermediate",
value: "color(rec2020 0.4 0.6 0.3)",
})
).toEqual({
type: "color",
colorSpace: "rec2020",
alpha: 1,
components: [0.4, 0.6, 0.3],
});
});

test("xyz-d65 color", () => {
expect(
parseIntermediateOrInvalidValue("color", {
type: "intermediate",
value: "color(xyz-d65 0.4 0.6 0.3)",
})
).toEqual({
type: "color",
colorSpace: "xyz-d65",
alpha: 1,
components: [0.4, 0.6, 0.3],
});
});

test("xyz-d50 color", () => {
expect(
parseIntermediateOrInvalidValue("color", {
type: "intermediate",
value: "color(xyz-d50 0.4 0.6 0.3)",
})
).toEqual({
type: "color",
colorSpace: "xyz-d50",
alpha: 1,
components: [0.4, 0.6, 0.3],
});
});
});

test("parse css variable reference", () => {
Expand Down Expand Up @@ -595,7 +777,7 @@ test("parse color in css variable", () => {
})
).toEqual({
type: "color",
colorSpace: "srgb",
colorSpace: "hex",
components: [0.0588, 0.0588, 0.0588],
alpha: 1,
});
Expand Down
8 changes: 4 additions & 4 deletions apps/builder/app/builder/shared/css-editor/css-editor.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { mergeRefs } from "@react-aria/utils";
import * as colorjs from "colorjs.io/fn";
import { color } from "@webstudio-is/css-engine";
import {
memo,
useEffect,
Expand Down Expand Up @@ -35,7 +35,7 @@ import {
import { CssValueInputContainer } from "../../features/style-panel/shared/css-value-input";
import { $availableVariables } from "../../features/style-panel/shared/model";
import { PropertyInfo } from "../../features/style-panel/property-label";
import { ColorPickerPopover } from "@webstudio-is/design-system";
import { ColorPicker } from "@webstudio-is/design-system";
import { useClientSupports } from "~/shared/client-supports";
import { CssEditorContextMenu, copyAttribute } from "./css-editor-context-menu";
import { AddStyleInput } from "./add-style-input";
Expand Down Expand Up @@ -134,7 +134,7 @@ const AdvancedPropertyValue = ({
const inputRef = useRef<HTMLInputElement>(null);
let isColor = false;
try {
colorjs.parse(toValue(styleDecl.usedValue));
color.parse(toValue(styleDecl.usedValue));
isColor = true;
} catch {
isColor = false;
Expand All @@ -148,7 +148,7 @@ const AdvancedPropertyValue = ({
fieldSizing="content"
prefix={
isColor && (
<ColorPickerPopover
<ColorPicker
value={styleDecl.usedValue}
onChange={(styleValue) => {
const options = { isEphemeral: true, listed: true };
Expand Down
6 changes: 3 additions & 3 deletions apps/builder/app/canvas/features/text-editor/text-editor.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as colorjs from "colorjs.io/fn";
import { color } from "@webstudio-is/css-engine";
import {
useState,
useEffect,
Expand Down Expand Up @@ -146,8 +146,8 @@ const CaretColorPlugin = () => {

let isLightBackground = false;
try {
const color = colorjs.parse(elementColor);
const alpha = color.alpha ?? 1;
const parsed = color.parse(elementColor);
const alpha = parsed.alpha ?? 1;
isLightBackground = alpha < 0.1;
} catch {
// If we can't parse the color, assume it's not light
Expand Down
6 changes: 4 additions & 2 deletions apps/builder/app/routes/_ui.(builder).tsx
Original file line number Diff line number Diff line change
Expand Up @@ -189,9 +189,11 @@ export const loader = async (loaderArgs: LoaderFunctionArgs) => {
// Disallowing iframes from loading any content except the canvas
// Still possible create iframes on canvas itself (but we use credentialless attribute)
// Still possible create iframe without src attribute
// Disable workers on builder
// Allow blob: workers so hdr-color-input can spawn its inline canvas-rendering worker.
// blob: workers can only be created from JS already running in this page, so the
// attack surface is no wider than allowing eval.
"Content-Security-Policy",
`frame-src ${url.origin}/canvas https://app.goentri.com/ https://help.webstudio.is/; worker-src 'none'`
`frame-src ${url.origin}/canvas https://app.goentri.com/ https://help.webstudio.is/; worker-src blob:`
);

return json(
Expand Down
2 changes: 1 addition & 1 deletion apps/builder/app/shared/commands-emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ export const createCommandsEmitter = <CommandName extends string>({
if (disableOnInputLikeControls) {
const element = event.target as HTMLElement;
const isOnInputLikeControl =
["input", "select", "textarea"].includes(
["input", "select", "textarea", "color-input"].includes(
element.tagName.toLowerCase()
) ||
element.isContentEditable ||
Expand Down
Loading
Loading