diff --git a/src/compiler/__tests__/compiler.test.tsx b/src/compiler/__tests__/compiler.test.tsx
index 4f7cacf..c790998 100644
--- a/src/compiler/__tests__/compiler.test.tsx
+++ b/src/compiler/__tests__/compiler.test.tsx
@@ -246,3 +246,37 @@ test("breaks apart comma separated variables", () => {
vr: [["test", [["blue", "green"]]]],
});
});
+
+test("light-dark()", () => {
+ const compiled = compile(`
+.my-class {
+ background-color: light-dark(#333b3c, #efefec);
+}`);
+
+ expect(compiled).toStrictEqual({
+ s: [
+ [
+ "my-class",
+ [
+ {
+ d: [
+ {
+ backgroundColor: "#333b3c",
+ },
+ ],
+ s: [1, 1],
+ },
+ {
+ d: [
+ {
+ backgroundColor: "#efefec",
+ },
+ ],
+ m: [["=", "prefers-color-scheme", "dark"]],
+ s: [1, 1],
+ },
+ ],
+ ],
+ ],
+ });
+});
diff --git a/src/compiler/declarations.ts b/src/compiler/declarations.ts
index a65a924..3793502 100644
--- a/src/compiler/declarations.ts
+++ b/src/compiler/declarations.ts
@@ -35,7 +35,11 @@ import type {
UnresolvedColor,
} from "lightningcss";
-import type { StyleDescriptor, StyleFunction } from "./compiler.types";
+import type {
+ StyleDescriptor,
+ StyleFunction,
+ StyleRule,
+} from "./compiler.types";
import { parseEasingFunction, parseIterationCount } from "./keyframes";
import { toRNProperty } from "./selectors";
import type { StylesheetBuilder } from "./stylesheet";
@@ -47,9 +51,10 @@ type DeclarationType
= Extract<
{ property: P }
>;
-type Parser = (
+type Parser = (
declaration: Extract,
builder: StylesheetBuilder,
+ propertyName: string,
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
) => StyleDescriptor | void;
@@ -225,8 +230,8 @@ const parsers: {
};
// This is missing LightningCSS types
-(parsers as Record>)["pointer-events"] =
- parsePointerEvents as Parser;
+(parsers as Record)["pointer-events"] =
+ parsePointerEvents as Parser;
const validProperties = new Set(Object.keys(parsers));
@@ -250,34 +255,30 @@ export function parseDeclaration(
if (declaration.property === "unparsed") {
parseDeclarationUnparsed(declaration, builder);
- return;
} else if (declaration.property === "custom") {
parseDeclarationCustom(declaration, builder);
- return;
+ } else {
+ parseWithParser(declaration, builder);
}
+}
+function parseWithParser(declaration: Declaration, builder: StylesheetBuilder) {
if (declaration.property in parsers) {
- const property =
- propertyRename[declaration.property] ?? declaration.property;
+ const parser = parsers[declaration.property] as Parser;
- const parser = parsers[
- declaration.property as keyof typeof parsers
- ] as unknown as (
- b: typeof declaration,
- builder: StylesheetBuilder,
- // eslint-disable-next-line @typescript-eslint/no-invalid-void-type
- ) => StyleDescriptor | void;
+ builder.descriptorProperty = declaration.property;
- const value = parser(declaration, builder);
+ const value = parser(declaration, builder, declaration.property);
if (value !== undefined) {
- builder.addDescriptor(property, value);
+ builder.addDescriptor(
+ propertyRename[declaration.property] ?? declaration.property,
+ value,
+ );
}
} else {
- builder.addWarning("property", (declaration as Declaration).property);
+ builder.addWarning("property", declaration.property);
}
-
- return;
}
function parseInsetBlock(
@@ -803,7 +804,7 @@ export function parseDeclarationUnparsed(
/**
* React Native doesn't support all the logical properties
*/
- const rename = propertyRename[declaration.value.propertyId.property];
+ const rename = propertyRename[property];
if (rename) {
property = rename;
}
@@ -811,6 +812,8 @@ export function parseDeclarationUnparsed(
/**
* Unparsed shorthand properties need to be parsed at runtime
*/
+ builder.descriptorProperty = property;
+
if (unparsedRuntimeParsing.has(property)) {
const args = parseUnparsed(declaration.value.value, builder);
@@ -1369,9 +1372,20 @@ export function parseColor(cssColor: CssColor, builder: StylesheetBuilder) {
switch (cssColor.type) {
case "currentcolor":
return [{}, "var", "__rn-css-current-color"] as const;
- case "light-dark":
- // TODO: Handle light-dark colors
- return;
+ case "light-dark": {
+ const extraRule: StyleRule = {
+ s: [],
+ m: [["=", "prefers-color-scheme", "dark"]],
+ };
+
+ builder.addUnnamedDescriptor(
+ parseColor(cssColor.dark, builder),
+ false,
+ extraRule,
+ );
+ builder.addExtraRule(extraRule);
+ return parseColor(cssColor.light, builder);
+ }
case "rgb": {
color = new Color({
space: "sRGB",
@@ -2451,8 +2465,18 @@ export function parseUnresolvedColor(
color.type,
[color.h, color.s, color.l, parseUnparsed(color.alpha, builder)],
];
- case "light-dark":
- return undefined;
+ case "light-dark": {
+ const extraRule = builder.extendRule({
+ m: [["=", "prefers-color-scheme", "dark"]],
+ });
+ builder.addUnnamedDescriptor(
+ reduceParseUnparsed(color.dark, builder),
+ false,
+ extraRule,
+ );
+ builder.addExtraRule(extraRule);
+ return reduceParseUnparsed(color.light, builder);
+ }
default:
color satisfies never;
}
diff --git a/src/compiler/stylesheet.ts b/src/compiler/stylesheet.ts
index d180c42..42c3e91 100644
--- a/src/compiler/stylesheet.ts
+++ b/src/compiler/stylesheet.ts
@@ -22,10 +22,16 @@ import { toRNProperty, type NormalizeSelector } from "./selectors";
type BuilderMode = "style" | "media" | "container" | "keyframes";
+const staticDeclarations = new WeakMap<
+ WeakKey,
+ Record
+>();
+
+const extraRules = new WeakMap[]>();
+
export class StylesheetBuilder {
animationFrames?: AnimationKeyframes_V2[];
animationDeclarations: StyleDeclaration[] = [];
- staticDeclarations: Record | undefined;
stylesheet: ReactNativeCssStyleSheet = {};
@@ -37,11 +43,12 @@ export class StylesheetBuilder {
constructor(
private options: CompilerOptions,
- private mode: BuilderMode = "style",
+ public mode: BuilderMode = "style",
private ruleTemplate: StyleRule = {
s: [],
},
private mapping: StyleRuleMapping = {},
+ public descriptorProperty?: string,
private shared: {
ruleSets: Record;
rootVariables?: VariableRecord;
@@ -52,19 +59,20 @@ export class StylesheetBuilder {
} = { ruleSets: {}, rem: 14, ruleOrder: 0 },
) {}
- fork(mode: BuilderMode) {
+ fork(mode = this.mode, rule?: Partial): StylesheetBuilder {
this.shared.ruleOrder++;
return new StylesheetBuilder(
this.options,
mode,
- this.cloneRule(),
+ this.cloneRule(rule ? { ...this.rule, ...rule } : undefined),
{ ...this.mapping },
+ this.descriptorProperty,
this.shared,
);
}
cloneRule({ ...rule } = this.rule): StyleRule {
- rule.s = [...this.rule.s];
+ rule.s = [...rule.s];
rule.aq &&= [...rule.aq];
rule.c &&= [...rule.c];
rule.cq &&= [...rule.cq];
@@ -76,6 +84,25 @@ export class StylesheetBuilder {
return rule;
}
+ private createRuleFromPartial(rule: StyleRule, partial: Partial) {
+ rule = this.cloneRule(rule);
+
+ if (partial.m) {
+ rule.m ??= [];
+ rule.m.push(...partial.m);
+ }
+
+ if (partial.d) {
+ rule.d = partial.d;
+ }
+
+ return rule;
+ }
+
+ extendRule(rule: Partial) {
+ return this.cloneRule({ ...this.rule, ...rule });
+ }
+
getOptions(): CompilerOptions {
return this.options;
}
@@ -130,9 +157,8 @@ export class StylesheetBuilder {
// TODO
}
- newRule(mapping: StyleRuleMapping, { important = false } = {}) {
+ newRule(mapping = this.mapping, { important = false } = {}) {
this.mapping = mapping;
- this.staticDeclarations = undefined;
this.rule = this.cloneRule(this.ruleTemplate);
this.rule.s[Specificity.Order] = this.shared.ruleOrder;
if (important) {
@@ -140,7 +166,16 @@ export class StylesheetBuilder {
}
}
- addRuleToRuleSet(name: string, rule = this.rule) {
+ addExtraRule(rule: Partial) {
+ let extraRuleArray = extraRules.get(this.rule);
+ if (!extraRuleArray) {
+ extraRuleArray = [];
+ extraRules.set(this.rule, extraRuleArray);
+ }
+ extraRuleArray.push(rule);
+ }
+
+ private addRuleToRuleSet(name: string, rule = this.rule) {
if (this.shared.ruleSets[name]) {
this.shared.ruleSets[name].push(rule);
} else {
@@ -163,10 +198,23 @@ export class StylesheetBuilder {
}
}
+ addUnnamedDescriptor(
+ value: StyleDescriptor,
+ forceTuple?: boolean,
+ rule = this.rule,
+ ) {
+ if (this.descriptorProperty === undefined) {
+ return;
+ }
+
+ this.addDescriptor(this.descriptorProperty, value, forceTuple, rule);
+ }
+
addDescriptor(
property: string,
value: StyleDescriptor,
forceTuple?: boolean,
+ rule = this.rule,
) {
if (value === undefined) {
return;
@@ -190,34 +238,34 @@ export class StylesheetBuilder {
return;
}
- this.rule.v ??= [];
- this.rule.v.push([property.slice(2), value]);
+ rule.v ??= [];
+ rule.v.push([property.slice(2), value]);
} else if (isStyleFunction(value)) {
const [delayed, usesVariables] = postProcessStyleFunction(value);
- this.rule.d ??= [];
+ rule.d ??= [];
if (value[1] === "@animation") {
- this.rule.a ??= true;
+ rule.a ??= true;
}
if (usesVariables) {
- this.rule.dv = 1;
+ rule.dv = 1;
}
this.pushDescriptor(
property,
value,
- this.rule.d,
+ rule.d,
forceTuple,
delayed || usesVariables,
);
} else {
if (property.startsWith("animation-")) {
- this.rule.a ??= true;
+ rule.a ??= true;
}
- this.rule.d ??= [];
- this.pushDescriptor(property, value, this.rule.d);
+ rule.d ??= [];
+ this.pushDescriptor(property, value, rule.d);
}
}
@@ -269,11 +317,13 @@ export class StylesheetBuilder {
} else if (Array.isArray(value) && value.some(isStyleFunction)) {
declarations.push([value, propPath]);
} else {
- if (!this.staticDeclarations) {
- this.staticDeclarations = {};
- declarations.push(this.staticDeclarations);
+ let staticDeclarationRecord = staticDeclarations.get(declarations);
+ if (!staticDeclarationRecord) {
+ staticDeclarationRecord = {};
+ staticDeclarations.set(declarations, staticDeclarationRecord);
+ declarations.push(staticDeclarationRecord);
}
- this.staticDeclarations[propPath] = value;
+ staticDeclarationRecord[propPath] = value;
}
}
@@ -339,6 +389,16 @@ export class StylesheetBuilder {
}
this.addRuleToRuleSet(className, rule);
+
+ const extraRulesArray = extraRules.get(this.rule);
+ if (extraRulesArray) {
+ for (const extraRule of extraRulesArray) {
+ this.addRuleToRuleSet(
+ className,
+ this.createRuleFromPartial(rule, extraRule),
+ );
+ }
+ }
} else {
// These can only have variable declarations
if (!this.rule.v) {
@@ -379,7 +439,6 @@ export class StylesheetBuilder {
}
this.animationDeclarations = [];
- this.staticDeclarations = undefined;
this.animationFrames.push([progress, this.animationDeclarations]);
}
}