Skip to content

Commit bb13c29

Browse files
marklawlorCopilot
andauthored
feat: add runtime color-mix() (#208)
* feat: add runtime color-mix() * Update src/compiler/supports.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/native/styles/functions/color-mix.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 7cd11a8 commit bb13c29

File tree

5 files changed

+214
-0
lines changed

5 files changed

+214
-0
lines changed
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { render, screen } from "@testing-library/react-native";
2+
import { View } from "react-native-css/components/View";
3+
import { registerCSS, testID } from "react-native-css/jest";
4+
5+
test("color-mix() - keyword", () => {
6+
registerCSS(
7+
`.test {
8+
--bg: red;
9+
@supports (color: color-mix(in lab, red, red)) {
10+
background-color: color-mix(in oklab, var(--bg) 50%, transparent);
11+
}
12+
}
13+
`,
14+
{
15+
inlineVariables: false,
16+
},
17+
);
18+
19+
render(<View testID={testID} className="test" />);
20+
const component = screen.getByTestId(testID);
21+
22+
expect(component.props.style).toStrictEqual({
23+
backgroundColor: "rgba(255, 0, 0, 0.5)",
24+
});
25+
});
26+
27+
test("color-mix() - oklch", () => {
28+
registerCSS(
29+
`.test {
30+
--bg: oklch(0.577 0.245 27.325);
31+
@supports (color: color-mix(in lab, red, red)) {
32+
background-color: color-mix(in oklab, var(--bg) 50%, transparent);
33+
}
34+
}
35+
`,
36+
{
37+
inlineVariables: false,
38+
},
39+
);
40+
41+
render(<View testID={testID} className="test" />);
42+
const component = screen.getByTestId(testID);
43+
44+
expect(component.props.style).toStrictEqual({
45+
backgroundColor: "rgba(231, 0, 11, 0.5)",
46+
});
47+
});

src/compiler/declarations.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1228,6 +1228,8 @@ export function parseUnparsed(
12281228
builder,
12291229
property,
12301230
);
1231+
case "color-mix":
1232+
return parseColorMix(tokenOrValue.value.arguments, builder, property);
12311233
default: {
12321234
builder.addWarning("value", `${tokenOrValue.value.name}()`);
12331235
return;
@@ -2633,6 +2635,100 @@ export function parseCalcFn(
26332635
return;
26342636
}
26352637

2638+
export function parseColorMix(
2639+
tokens: TokenOrValue[],
2640+
builder: StylesheetBuilder,
2641+
property: string,
2642+
): StyleDescriptor {
2643+
const [inToken, whitespace, colorSpace, comma, ...rest] = tokens;
2644+
if (
2645+
typeof inToken !== "object" ||
2646+
inToken.type !== "token" ||
2647+
inToken.value.type !== "ident" ||
2648+
inToken.value.value !== "in"
2649+
) {
2650+
return;
2651+
}
2652+
2653+
if (
2654+
typeof whitespace !== "object" ||
2655+
whitespace.type !== "token" ||
2656+
whitespace.value.type !== "white-space"
2657+
) {
2658+
return;
2659+
}
2660+
2661+
if (
2662+
typeof comma !== "object" ||
2663+
comma.type !== "token" ||
2664+
comma.value.type !== "comma"
2665+
) {
2666+
return;
2667+
}
2668+
2669+
const colorSpaceArg = parseUnparsed(colorSpace, builder, property);
2670+
if (typeof colorSpaceArg !== "string") {
2671+
return;
2672+
}
2673+
2674+
let nextToken = rest.shift();
2675+
2676+
const leftColorArg = parseUnparsed(nextToken, builder, property);
2677+
2678+
if (!leftColorArg) {
2679+
return;
2680+
}
2681+
2682+
nextToken = rest.shift();
2683+
2684+
let leftColorPercentage: StyleDescriptor | undefined;
2685+
if (nextToken?.type !== "token" || nextToken.value.type !== "comma") {
2686+
leftColorPercentage = parseUnparsed(nextToken, builder, property);
2687+
nextToken = rest.shift();
2688+
}
2689+
2690+
if (
2691+
typeof nextToken !== "object" ||
2692+
nextToken.type !== "token" ||
2693+
nextToken.value.type !== "comma"
2694+
) {
2695+
return;
2696+
}
2697+
2698+
nextToken = rest.shift();
2699+
2700+
const rightColorArg = parseUnparsed(nextToken, builder, property);
2701+
2702+
if (rightColorArg === "transparent") {
2703+
// Ignore the rest, treat as single color with alpha
2704+
return [{}, "colorMix", [colorSpaceArg, leftColorArg, leftColorPercentage]];
2705+
}
2706+
2707+
nextToken = rest.shift();
2708+
let rightColorPercentage: StyleDescriptor | undefined;
2709+
if (nextToken?.type !== "token" || nextToken.value.type !== "comma") {
2710+
rightColorPercentage = parseUnparsed(nextToken, builder, property);
2711+
nextToken = rest.shift();
2712+
}
2713+
2714+
// We should have expired all tokens now
2715+
if (nextToken) {
2716+
return;
2717+
}
2718+
2719+
return [
2720+
{},
2721+
"colorMix",
2722+
[
2723+
colorSpaceArg,
2724+
leftColorArg,
2725+
leftColorPercentage,
2726+
rightColorArg,
2727+
rightColorPercentage,
2728+
],
2729+
];
2730+
}
2731+
26362732
export function parseCalcArguments(
26372733
[...args]: TokenOrValue[],
26382734
builder: StylesheetBuilder,

src/compiler/supports.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,6 @@ export function supportsConditionValid(condition: SupportsCondition): boolean {
2323
const declarations: Record<string, string[]> = {
2424
// We don't actually support this, but its needed for Tailwind CSS
2525
"-moz-orient": ["inline"],
26+
// Special text used by TailwindCSS. We should probably change this to all color-mix
27+
"color": ["color-mix(in lab, red, red)"],
2628
};
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
2+
import type { PlainColorObject } from "colorjs.io";
3+
import {
4+
ColorSpace,
5+
to as convert,
6+
mix,
7+
OKLab,
8+
P3,
9+
parse,
10+
sRGB,
11+
type ColorConstructor,
12+
} from "colorjs.io/fn";
13+
14+
import type { StyleFunctionResolver } from "../resolve";
15+
16+
ColorSpace.register(sRGB);
17+
ColorSpace.register(P3);
18+
ColorSpace.register(OKLab);
19+
20+
export const colorMix: StyleFunctionResolver = (resolveValue, value) => {
21+
const args = resolveValue(value[2]);
22+
23+
if (!Array.isArray(args) || args.length < 3) {
24+
return;
25+
}
26+
27+
try {
28+
const space = args.shift();
29+
30+
let left: ColorConstructor | PlainColorObject = parse(
31+
args.shift() as string,
32+
);
33+
34+
let next = args.shift();
35+
36+
if (typeof next === "string" && next.endsWith("%")) {
37+
left.alpha = parseFloat(next) / 100;
38+
next = args.shift();
39+
}
40+
41+
if (next === undefined) {
42+
if (left.spaceId !== "srgb") {
43+
left = convert(left, "srgb");
44+
}
45+
46+
return `rgba(${(left.coords[0] ?? 0) * 255}, ${(left.coords[1] ?? 0) * 255}, ${(left.coords[2] ?? 0) * 255}, ${left.alpha})`;
47+
}
48+
49+
if (typeof next !== "string") {
50+
return;
51+
}
52+
const right = parse(next);
53+
54+
next = args.shift();
55+
if (next && typeof next === "string" && next.endsWith("%")) {
56+
right.alpha = parseFloat(next) / 100;
57+
}
58+
59+
const result = mix(left, right, {
60+
space,
61+
outputSpace: "srgb",
62+
});
63+
64+
return `rgba(${(result.coords[0] ?? 0) * 255}, ${(result.coords[1] ?? 0) * 255}, ${(result.coords[2] ?? 0) * 255}, ${result.alpha})`;
65+
} catch {
66+
return;
67+
}
68+
};

src/native/styles/functions/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ export * from "./numeric-functions";
55
export * from "./platform-functions";
66
export * from "./string-functions";
77
export * from "./transform-functions";
8+
export * from "./color-mix";

0 commit comments

Comments
 (0)