diff --git a/src/__tests__/vendor/tailwind.test.tsx b/src/__tests__/vendor/tailwind.test.tsx index d02df0f..275fb84 100644 --- a/src/__tests__/vendor/tailwind.test.tsx +++ b/src/__tests__/vendor/tailwind.test.tsx @@ -23,7 +23,7 @@ test("transition", () => { } `); - expect(compiled).toStrictEqual({ + expect(compiled.stylesheet()).toStrictEqual({ s: [ [ "transition", @@ -84,10 +84,10 @@ test("transition", () => { ], ], vr: [ - ["default-transition-duration", [150]], + ["default-transition-duration", [[150]]], [ "default-transition-timing-function", - [[{}, "cubic-bezier", [0.4, 0, 0.2, 1]]], + [[[{}, "cubic-bezier", [0.4, 0, 0.2, 1]]]], ], ], }); @@ -107,7 +107,7 @@ test("box-shadow", () => { } `); - expect(compiled).toStrictEqual({ + expect(compiled.stylesheet()).toStrictEqual({ s: [ [ "shadow-xl", @@ -215,7 +215,7 @@ test("filter", () => { } `); - expect(compiled).toStrictEqual({ + expect(compiled.stylesheet()).toStrictEqual({ s: [ [ "brightness-50", @@ -286,7 +286,7 @@ test("filter", () => { ], ], ], - vr: [["drop-shadow-md", [[0, 3, 3, "#0000001f"]]]], + vr: [["drop-shadow-md", [[[0, 3, 3, "#0000001f"]]]]], }); render(); diff --git a/src/compiler/__tests__/@prop.test.tsx b/src/compiler/__tests__/@prop.test.tsx index 1b124f6..69d0fee 100644 --- a/src/compiler/__tests__/@prop.test.tsx +++ b/src/compiler/__tests__/@prop.test.tsx @@ -9,7 +9,7 @@ test("@prop single", () => { } `); - expect(compiled).toStrictEqual({ + expect(compiled.stylesheet()).toStrictEqual({ s: [ [ "test", @@ -39,7 +39,7 @@ test("@prop single, nested value", () => { } `); - expect(compiled).toStrictEqual({ + expect(compiled.stylesheet()).toStrictEqual({ s: [ [ "test", @@ -69,7 +69,7 @@ test("@prop single, top level", () => { } `); - expect(compiled).toStrictEqual({ + expect(compiled.stylesheet()).toStrictEqual({ s: [ [ "test", @@ -99,7 +99,7 @@ test("@prop single, top level, nested", () => { } `); - expect(compiled).toStrictEqual({ + expect(compiled.stylesheet()).toStrictEqual({ s: [ [ "test", @@ -129,7 +129,7 @@ test("@prop single, top level, nested", () => { } `); - expect(compiled).toStrictEqual({ + expect(compiled.stylesheet()).toStrictEqual({ s: [ [ "test", @@ -162,7 +162,7 @@ test("@prop multiple", () => { } `); - expect(compiled).toStrictEqual({ + expect(compiled.stylesheet()).toStrictEqual({ s: [ [ "test", diff --git a/src/compiler/__tests__/compiler.test.tsx b/src/compiler/__tests__/compiler.test.tsx index ac9434d..eede862 100644 --- a/src/compiler/__tests__/compiler.test.tsx +++ b/src/compiler/__tests__/compiler.test.tsx @@ -6,7 +6,7 @@ test("hello world", () => { color: red; }`); - expect(compiled).toStrictEqual({ + expect(compiled.stylesheet()).toStrictEqual({ s: [ [ "my-class", @@ -34,8 +34,8 @@ test("reads global CSS variables", () => { } }`); - expect(compiled).toStrictEqual({ - vr: [["color-red-500", ["#fb2c36"]]], + expect(compiled.stylesheet()).toStrictEqual({ + vr: [["color-red-500", [["#fb2c36"]]]], }); }); @@ -49,7 +49,7 @@ test.skip("removes unused CSS variables", () => { } `); - expect(compiled).toStrictEqual({ + expect(compiled.stylesheet()).toStrictEqual({ s: [ [ "test", @@ -84,7 +84,7 @@ test.skip("preserves unused CSS variables with preserve-variables", () => { } `); - expect(compiled).toStrictEqual({ + expect(compiled.stylesheet()).toStrictEqual({ s: [ [ "test", @@ -117,7 +117,7 @@ test("multiple rules with same selector", () => { } `); - expect(compiled).toStrictEqual({ + expect(compiled.stylesheet()).toStrictEqual({ s: [ [ "redOrGreen", @@ -157,7 +157,7 @@ test.skip("transitions", () => { } `); - expect(compiled).toStrictEqual({ + expect(compiled.stylesheet()).toStrictEqual({ s: [ [ "test", @@ -195,7 +195,7 @@ test.skip("animations", () => { } `); - expect(compiled).toStrictEqual({ + expect(compiled.stylesheet()).toStrictEqual({ k: [ [ "spin", @@ -242,8 +242,8 @@ test("breaks apart comma separated variables", () => { } `); - expect(compiled).toStrictEqual({ - vr: [["test", [["blue", "green"]]]], + expect(compiled.stylesheet()).toStrictEqual({ + vr: [["test", [[["blue", "green"]]]]], }); }); @@ -253,7 +253,7 @@ test("light-dark()", () => { background-color: light-dark(#333b3c, #efefec); }`); - expect(compiled).toStrictEqual({ + expect(compiled.stylesheet()).toStrictEqual({ s: [ [ "my-class", @@ -295,20 +295,17 @@ test("media query nested in rules", () => { @media (min-width: 100px) { background-color: yellow; + } }`); - expect(compiled).toStrictEqual({ + expect(compiled.stylesheet()).toStrictEqual({ s: [ [ "my-class", [ { - d: [ - { - color: "#f00", - }, - ], + d: [{ color: "#f00" }], s: [1, 1], v: [["__rn-css-color", "#f00"]], }, @@ -323,51 +320,43 @@ test("media query nested in rules", () => { v: [["__rn-css-color", "#00f"]], }, { - d: [ - { - backgroundColor: "#008000", - }, + d: [{ backgroundColor: "#008000" }], + m: [ + [">=", "width", 600], + [">=", "width", 400], ], - m: [[">=", "width", 400]], s: [3, 1], }, { - d: [ - { - backgroundColor: "#ff0", - }, - ], + d: [{ backgroundColor: "#ff0" }], m: [[">=", "width", 100]], s: [4, 1], }, + ], + ], + ], + }); +}); + +test("container queries", () => { + const compiled = compile(` + @container (width > 400px) { + .child { + color: blue; + } + }`); + + expect(compiled.stylesheet()).toStrictEqual({ + s: [ + [ + "child", + [ { - d: [ - { - color: "#00f", - }, - ], - m: [[">=", "width", 600]], - s: [2, 1, 1], + cq: [{ m: [">", "width", 400] }], + d: [{ color: "#00f" }], + s: [2, 1], v: [["__rn-css-color", "#00f"]], }, - { - d: [ - { - backgroundColor: "#008000", - }, - ], - m: [[">=", "width", 400]], - s: [3, 1, 1], - }, - { - d: [ - { - backgroundColor: "#ff0", - }, - ], - m: [[">=", "width", 100]], - s: [4, 1, 1], - }, ], ], ], diff --git a/src/compiler/__tests__/media-query.test.ts b/src/compiler/__tests__/media-query.test.ts index 3fb5c2f..a9a3ff1 100644 --- a/src/compiler/__tests__/media-query.test.ts +++ b/src/compiler/__tests__/media-query.test.ts @@ -8,7 +8,7 @@ describe.skip("platform media queries", () => { } `); - expect(compiled).toStrictEqual({ + expect(compiled.stylesheet()).toStrictEqual({ s: [ [ "my-class", @@ -39,7 +39,7 @@ describe.skip("platform media queries", () => { } `); - expect(compiled).toStrictEqual({ + expect(compiled.stylesheet()).toStrictEqual({ s: [ [ "my-class", diff --git a/src/compiler/__tests__/unstable_animations.test.ts b/src/compiler/__tests__/unstable_animations.test.ts index 0036feb..7b7e0a9 100644 --- a/src/compiler/__tests__/unstable_animations.test.ts +++ b/src/compiler/__tests__/unstable_animations.test.ts @@ -19,7 +19,7 @@ test.skip("test compiler", () => { `, ); - expect(compiled).toStrictEqual({ + expect(compiled.stylesheet()).toStrictEqual({ k: [ [ "slide-in", diff --git a/src/compiler/attributes.test.ts b/src/compiler/attributes.test.ts index 211ea5b..c444b14 100644 --- a/src/compiler/attributes.test.ts +++ b/src/compiler/attributes.test.ts @@ -6,7 +6,7 @@ test("multiple classes", () => { color: red; }`); - expect(compiled).toStrictEqual({ + expect(compiled.stylesheet()).toStrictEqual({ s: [ [ "test", diff --git a/src/compiler/compiler.ts b/src/compiler/compiler.ts index 19bcb7d..2fa2ffe 100644 --- a/src/compiler/compiler.ts +++ b/src/compiler/compiler.ts @@ -1,4 +1,6 @@ /* eslint-disable */ +import { inspect } from "node:util"; + import { debug } from "debug"; import { transform as lightningcss, @@ -12,11 +14,7 @@ import { } from "lightningcss"; import { maybeMutateReactNativeOptions, parsePropAtRule } from "./atRules"; -import type { - CompilerOptions, - ContainerQuery, - ReactNativeCssStyleSheet, -} from "./compiler.types"; +import type { CompilerOptions, ContainerQuery } from "./compiler.types"; import { parseContainerCondition } from "./container-query"; import { parseDeclaration } from "./declarations"; import { extractKeyFrames } from "./keyframes"; @@ -33,11 +31,11 @@ const defaultLogger = debug("react-native-css:compiler"); * @param options - Compiler options * @returns A `ReactNativeCssStyleSheet` that can be passed to `StyleSheet.register` or used with a custom runtime */ -export function compile( - code: Buffer | string, - options: CompilerOptions = {}, -): ReactNativeCssStyleSheet { +export function compile(code: Buffer | string, options: CompilerOptions = {}) { const { logger = defaultLogger } = options; + const isLoggerEnabled = + "enabled" in logger ? logger.enabled : Boolean(logger); + const features = Object.assign({}, options.features); if (options.selectorPrefix && options.selectorPrefix.startsWith(".")) { @@ -67,8 +65,12 @@ export function compile( maybeMutateReactNativeOptions(rule, builder); }, StyleSheetExit(sheet) { - logger(`Found ${sheet.rules.length} rules to process`); - logger(JSON.stringify(sheet.rules, null, 2)); + if (isLoggerEnabled) { + logger(`Found ${sheet.rules.length} rules to process`); + logger( + inspect(sheet.rules, { depth: null, colors: true, compact: false }), + ); + } for (const rule of sheet.rules) { // Extract the style declarations and animations from the current rule @@ -108,7 +110,10 @@ export function compile( visitor, }); - return builder.getNativeStyleSheet(); + return { + stylesheet: () => builder.getNativeStyleSheet(), + warnings: () => builder.getWarnings(), + }; } /** @@ -137,16 +142,16 @@ function extractRule(rule: Rule, builder: StylesheetBuilder) { const declarationBlock = value.declarations; if (declarationBlock) { - if (declarationBlock.declarations) { - builder.newRuleFork(); + if (declarationBlock.declarations?.length) { + builder.newNestedRule(); for (const declaration of declarationBlock.declarations) { parseDeclaration(declaration, builder); } builder.applyRuleToSelectors(); } - if (declarationBlock.importantDeclarations) { - builder.newRuleFork({ important: true }); + if (declarationBlock.importantDeclarations?.length) { + builder.newNestedRule({ important: true }); for (const declaration of declarationBlock.importantDeclarations) { parseDeclaration(declaration, builder); } @@ -283,17 +288,17 @@ function extractContainer( builder = builder.fork("container"); // Iterate over all rules inside the containerRule and extract their styles using the updated CompilerCollection - for (const rule of containerRule.rules) { - const query: ContainerQuery = { - m: parseContainerCondition(containerRule.condition, builder), - }; + const query: ContainerQuery = { + m: parseContainerCondition(containerRule.condition, builder), + }; - if (containerRule.name) { - query.n = containerRule.name; - } + if (containerRule.name) { + query.n = containerRule.name; + } - builder.addContainerQuery(query); + builder.addContainerQuery(query); + for (const rule of containerRule.rules) { extractRule(rule, builder); } } diff --git a/src/compiler/compiler.types.ts b/src/compiler/compiler.types.ts index 0ba16c8..cfe927d 100644 --- a/src/compiler/compiler.types.ts +++ b/src/compiler/compiler.types.ts @@ -35,9 +35,9 @@ export interface ReactNativeCssStyleSheet_V2 { /** KeyFrames */ k?: Animation_V2[]; /** Root Variables */ - vr?: [string, LightDarkVariable][]; + vr?: RootVariables; /** Universal Variables */ - vu?: [string, LightDarkVariable][]; + vu?: RootVariables; } /******************************** Styles ********************************/ @@ -147,10 +147,12 @@ export type StyleFunction = /****************************** Variables *******************************/ export type VariableDescriptor = [string, StyleDescriptor]; -export type VariableRecord = Record; -export type LightDarkVariable = +export type VariableRecord = Record; +export type VariableValue = | [StyleDescriptor] - | [StyleDescriptor, StyleDescriptor]; + | [StyleDescriptor, MediaCondition[]]; + +export type RootVariables = [string, VariableValue[]][]; export type InlineVariable = { [VAR_SYMBOL]: "inline"; diff --git a/src/compiler/declarations.ts b/src/compiler/declarations.ts index 78210b7..5ac9deb 100644 --- a/src/compiler/declarations.ts +++ b/src/compiler/declarations.ts @@ -1272,7 +1272,7 @@ export function parseAngle(angle: Angle | number, builder: StylesheetBuilder) { case "rad": return `${angle.value}${angle.type}`; default: - builder.addWarning("value", angle.value); + builder.addWarning("value", "angle", angle.value); return undefined; } } @@ -1981,6 +1981,7 @@ export function parseLineHeight( case "calc": builder.addWarning( "value", + "line-height", typeof length.value === "number" ? length.value : JSON.stringify(length.value), diff --git a/src/compiler/inheritance.test.ts b/src/compiler/inheritance.test.ts index 3b1b7d8..021787b 100644 --- a/src/compiler/inheritance.test.ts +++ b/src/compiler/inheritance.test.ts @@ -6,7 +6,7 @@ test("nested classes", () => { color: red; }`); - expect(compiled).toStrictEqual({ + expect(compiled.stylesheet()).toStrictEqual({ s: [ [ "my-class", @@ -38,7 +38,7 @@ test("multiple tiers classes", () => { color: red; }`); - expect(compiled).toStrictEqual({ + expect(compiled.stylesheet()).toStrictEqual({ s: [ [ "one", @@ -79,7 +79,7 @@ test("tiers with multiple classes", () => { color: red; }`); - expect(compiled).toStrictEqual({ + expect(compiled.stylesheet()).toStrictEqual({ s: [ [ "one", diff --git a/src/compiler/stylesheet.ts b/src/compiler/stylesheet.ts index 39558f8..0804c85 100644 --- a/src/compiler/stylesheet.ts +++ b/src/compiler/stylesheet.ts @@ -17,6 +17,7 @@ import type { StyleRuleMapping, StyleRuleSet, VariableRecord, + VariableValue, } from "./compiler.types"; import { toRNProperty, type NormalizeSelector } from "./selectors"; @@ -56,7 +57,17 @@ export class StylesheetBuilder { animations?: AnimationRecord; rem: number; ruleOrder: number; - } = { ruleSets: {}, rem: 14, ruleOrder: 0 }, + warningProperties: string[]; + warningValues: [string, unknown][]; + warningFunctions: string[]; + } = { + ruleSets: {}, + rem: 14, + ruleOrder: 0, + warningProperties: [], + warningValues: [], + warningFunctions: [], + }, private selectors?: NormalizeSelector[], ) {} @@ -76,7 +87,7 @@ export class StylesheetBuilder { ); } - cloneRule({ ...rule } = this.rule): StyleRule { + cloneRule({ ...rule } = this.ruleTemplate): StyleRule { rule.s = [...rule.s]; rule.aq &&= [...rule.aq]; rule.c &&= [...rule.c]; @@ -128,11 +139,17 @@ export class StylesheetBuilder { } if (this.shared.rootVariables) { - stylesheetOptions.vr = Object.entries(this.shared.rootVariables); + stylesheetOptions.vr = Object.entries(this.shared.rootVariables).map( + // Reverse these so the most specific variables are first + ([key, value]) => [key, value.reverse()] as const, + ); } if (this.shared.universalVariables) { - stylesheetOptions.vu = Object.entries(this.shared.universalVariables); + stylesheetOptions.vu = Object.entries(this.shared.universalVariables).map( + // Reverse these so the most specific variables are first + ([key, value]) => [key, value.reverse()] as const, + ); } if (this.shared.animations) { @@ -156,10 +173,29 @@ export class StylesheetBuilder { } addWarning( - _type: "property" | "value" | "function", - _property: string | number, + type: "property" | "value" | "function", + property: string, + value?: unknown, ): void { - // TODO + switch (type) { + case "property": + this.shared.warningProperties.push(property); + break; + case "value": + this.shared.warningValues.push([property, value]); + break; + case "function": + this.shared.warningFunctions.push(property); + break; + } + } + + getWarnings() { + return { + properties: this.shared.warningProperties, + values: this.shared.warningValues, + functions: this.shared.warningFunctions, + }; } newRule(mapping = this.mapping, { important = false } = {}) { @@ -171,14 +207,12 @@ export class StylesheetBuilder { } } - newRuleFork({ important = false } = {}) { - this.rule = this.cloneRule(this.rule); - this.rule.s[Specificity.Order] = this.shared.ruleOrder; - if (important) { - this.rule.s[Specificity.Important] = 1; - } + /** Used by nested declarations (for example @media inside a RuleSet) */ + newNestedRule({ important = false } = {}) { + this.newRule(this.mapping, { important }); } + /** Hack for light-dark, which requires adding a new rule without changing the current rule */ addExtraRule(rule: Partial) { let extraRuleArray = extraRules.get(this.rule); if (!extraRuleArray) { @@ -197,8 +231,8 @@ export class StylesheetBuilder { } addMediaQuery(condition: MediaCondition) { - this.rule.m ??= []; - this.rule.m.push(condition); + this.ruleTemplate.m ??= []; + this.ruleTemplate.m.push(condition); } addContainer(value: string[] | false) { @@ -355,7 +389,8 @@ export class StylesheetBuilder { } for (const selector of selectorList) { - const rule = this.cloneRule(); + // We are going to be apply the current rule to n selectors, so we clone the rule + const rule = this.cloneRule(this.rule); if (selector.type === "className") { const { @@ -431,16 +466,28 @@ export class StylesheetBuilder { for (const [name, value] of this.rule.v) { this.shared[type] ??= {}; - this.shared[type][name] ??= [undefined]; - this.shared[type][name][subtype === "light" ? 0 : 1] = value; + this.shared[type][name] ??= []; + + const variableValue: VariableValue = + subtype === "light" + ? [value] + : [value, [["=", "prefers-color-scheme", "dark"]]]; + + // Append extra media queries if they exist + if (this.rule.m) { + variableValue[1] ??= []; + variableValue[1].push(...this.rule.m); + } + + this.shared[type][name].push(variableValue); } } } } addContainerQuery(query: ContainerQuery) { - this.rule.cq ??= []; - this.rule.cq.push(query); + this.ruleTemplate.cq ??= []; + this.ruleTemplate.cq.push(query); } newAnimationFrames(name: string) { diff --git a/src/jest/index.ts b/src/jest/index.ts index 9d34544..6e933a6 100644 --- a/src/jest/index.ts +++ b/src/jest/index.ts @@ -1,6 +1,6 @@ import { Appearance, Dimensions } from "react-native"; -import util from "node:util"; +import { inspect } from "node:util"; import { compile, type CompilerOptions } from "../compiler"; import { StyleCollection } from "../runtime/native/injection"; @@ -37,14 +37,29 @@ export function registerCSS( ...options }: CompilerOptions & { debug?: boolean } = {}, ) { - const compiled = compile(css, options); + const logger = debug + ? (text: string) => { + // Just log the rules + if (text.startsWith("[")) { + console.log(`Rules:\n---\n${text}`); + } + } + : undefined; + + const compiled = compile(css, { ...options, logger }); if (debug) { console.log( - `Compiled:\n---\n${util.inspect(compiled, { depth: null, colors: true, compact: false })}`, + `Compiled:\n---\n${inspect( + { + stylesheet: compiled.stylesheet(), + warnings: compiled.warnings(), + }, + { depth: null, colors: true, compact: false }, + )}`, ); } - StyleCollection.inject(compiled); + StyleCollection.inject(compiled.stylesheet()); return compiled; } diff --git a/src/runtime/native/__tests__/media-query.test.tsx b/src/runtime/native/__tests__/media-query.test.tsx index 7ac82e5..bc52e47 100644 --- a/src/runtime/native/__tests__/media-query.test.tsx +++ b/src/runtime/native/__tests__/media-query.test.tsx @@ -7,6 +7,37 @@ import { registerCSS, testID } from "react-native-css/jest"; import { colorScheme } from "../api"; import { dimensions } from "../reactivity"; +jest.mock("react-native", () => { + const RN = jest.requireActual("react-native"); + RN.Platform.OS = "ios"; + return RN as unknown; +}); + +test(":root MediaQueries", () => { + registerCSS(` + :root { + @media ios { + --my-var: System; + } + @media android { + --my-var: SystemAndroid; + } + } + + @layer utilities { + .my-class { + font-family: var(--my-var); + } + }`); + + render(); + const component = screen.getByTestId(testID); + + expect(component.props.style).toStrictEqual({ + fontFamily: "System", + }); +}); + test("color scheme", () => { registerCSS(` .my-class { color: blue; } diff --git a/src/runtime/native/conditions/container-query.ts b/src/runtime/native/conditions/container-query.ts index 8459b27..a165627 100644 --- a/src/runtime/native/conditions/container-query.ts +++ b/src/runtime/native/conditions/container-query.ts @@ -14,7 +14,7 @@ import { focusFamily, hoverFamily, type ContainerContextValue, - type Effect, + type Getter, } from "../reactivity"; import type { RenderGuard } from "./guards"; @@ -24,10 +24,10 @@ export function testContainerQueries( queries: ContainerQuery[], inheritedContainers: ContainerContextValue, guards: RenderGuard[], - effect: Effect, + get: Getter, ) { return queries.every((query) => { - return testContainerQuery(query, inheritedContainers, guards, effect); + return testContainerQuery(query, inheritedContainers, guards, get); }); } @@ -35,7 +35,7 @@ export function testContainerQuery( query: ContainerQuery, inheritedContainers: ContainerContextValue, guards: RenderGuard[], - effect?: Effect, + get: Getter, ): boolean { const name = query.n ?? DEFAULT_CONTAINER_NAME; const container = inheritedContainers[name]!; @@ -46,11 +46,11 @@ export function testContainerQuery( return false; } - if (query.m && !testContainerMediaCondition(query.m, container, effect)) { + if (query.m && !testContainerMediaCondition(query.m, container, get)) { return false; } - if (query.p && !testContainerPseudoCondition(query.p, container, effect)) { + if (query.p && !testContainerPseudoCondition(query.p, container, get)) { return false; } @@ -60,15 +60,15 @@ export function testContainerQuery( function testContainerPseudoCondition( query: PseudoClassesQuery, containerKey: WeakKey, - effect?: Effect, + get: Getter, ): boolean { - if (query.h && !hoverFamily(containerKey).get(effect)) { + if (query.h && !get(hoverFamily(containerKey))) { return false; } - if (query.a && !activeFamily(containerKey).get(effect)) { + if (query.a && !get(activeFamily(containerKey))) { return false; } - if (query.f && !focusFamily(containerKey).get(effect)) { + if (query.f && !get(focusFamily(containerKey))) { return false; } return true; @@ -77,18 +77,18 @@ function testContainerPseudoCondition( function testContainerMediaCondition( condition: MediaCondition, containerKey: WeakKey, - effect?: Effect, + get: Getter, ): boolean { switch (condition[0]) { case "!": - return !testContainerMediaCondition(condition[1], containerKey, effect); + return !testContainerMediaCondition(condition[1], containerKey, get); case "&": return condition[1].every((query) => { - return testContainerMediaCondition(query, containerKey, effect); + return testContainerMediaCondition(query, containerKey, get); }); case "|": return condition[1].some((query) => { - return testContainerMediaCondition(query, containerKey, effect); + return testContainerMediaCondition(query, containerKey, get); }); case "!!": return false; @@ -99,7 +99,7 @@ function testContainerMediaCondition( case "<": case "<=": case "=": { - const left = getContainerFeatureValue(condition[1], containerKey, effect); + const left = getContainerFeatureValue(condition[1], containerKey, get); const right = condition[2]; if (condition[0] === "=") { @@ -133,21 +133,21 @@ function testContainerMediaCondition( function getContainerFeatureValue( name: MediaFeatureNameFor_ContainerSizeFeatureId, containerKey: WeakKey, - effect?: Effect, + get: Getter, ): StyleDescriptor { switch (name) { case "width": - return containerWidthFamily(containerKey).get(effect); + return get(containerWidthFamily(containerKey)); case "height": - return containerHeightFamily(containerKey).get(effect); + return get(containerHeightFamily(containerKey)); case "aspect-ratio": { - const width = containerWidthFamily(containerKey).get(effect); - const height = containerWidthFamily(containerKey).get(effect); + const width = get(containerWidthFamily(containerKey)); + const height = get(containerHeightFamily(containerKey)); return width / height; } case "orientation": - const width = containerWidthFamily(containerKey).get(effect); - const height = containerWidthFamily(containerKey).get(effect); + const width = get(containerWidthFamily(containerKey)); + const height = get(containerHeightFamily(containerKey)); return width > height ? "landscape" : "portrait"; case "inline-size": case "block-size": diff --git a/src/runtime/native/conditions/guards.ts b/src/runtime/native/conditions/guards.ts index 558ec70..d3c9dd8 100644 --- a/src/runtime/native/conditions/guards.ts +++ b/src/runtime/native/conditions/guards.ts @@ -2,7 +2,6 @@ import type { ComponentState } from "../react/useNativeCss"; import type { ContainerContextValue, - Effect, VariableContextValue, } from "../reactivity"; @@ -10,7 +9,7 @@ export type RenderGuard = | ["a", string, any] | ["d", string, any] | ["v", string, any] - | ["c", string, Effect]; + | ["c", string, WeakKey]; export function testGuards( state: ComponentState, diff --git a/src/runtime/native/conditions/index.ts b/src/runtime/native/conditions/index.ts index f1870d2..4ee0829 100644 --- a/src/runtime/native/conditions/index.ts +++ b/src/runtime/native/conditions/index.ts @@ -10,7 +10,7 @@ import { focusFamily, hoverFamily, type ContainerContextValue, - type Effect, + type Getter, } from "../reactivity"; import { testContainerQueries } from "./container-query"; import type { RenderGuard } from "./guards"; @@ -18,15 +18,15 @@ import { testMediaQuery } from "./media-query"; export function testRule( rule: StyleRule, - effect: Effect, + get: Getter, props: Props, guards: RenderGuard[], containerContext: ContainerContextValue, ) { - if (rule.p && !pseudoClasses(rule.p, effect)) { + if (rule.p && !pseudoClasses(rule.p, get)) { return false; } - if (rule.m && !testMediaQuery(rule.m, effect)) { + if (rule.m && !testMediaQuery(rule.m, get)) { return false; } if (rule.aq && !attributes(rule.aq, props, guards)) { @@ -34,7 +34,7 @@ export function testRule( } if ( rule.cq && - !testContainerQueries(rule.cq, containerContext, guards, effect) + !testContainerQueries(rule.cq, containerContext, guards, get) ) { return false; } @@ -42,14 +42,14 @@ export function testRule( return true; } -function pseudoClasses(query: PseudoClassesQuery, effect: Effect) { - if (query.h && !hoverFamily(effect).get(effect)) { +function pseudoClasses(query: PseudoClassesQuery, get: Getter) { + if (query.h && !get(hoverFamily(get))) { return false; } - if (query.a && !activeFamily(effect).get(effect)) { + if (query.a && !get(activeFamily(get))) { return false; } - if (query.f && !focusFamily(effect).get(effect)) { + if (query.f && !get(focusFamily(get))) { return false; } return true; diff --git a/src/runtime/native/conditions/media-query.ts b/src/runtime/native/conditions/media-query.ts index 3011d36..7d4d0c7 100644 --- a/src/runtime/native/conditions/media-query.ts +++ b/src/runtime/native/conditions/media-query.ts @@ -2,38 +2,38 @@ import { PixelRatio, Platform } from "react-native"; import type { MediaCondition } from "../../../compiler"; -import { colorScheme, vh, vw, type Effect } from "../reactivity"; +import { colorScheme, vh, vw, type Getter } from "../reactivity"; -export function testMediaQuery(mediaQueries: MediaCondition[], effect: Effect) { - return mediaQueries.every((query) => test(query, effect)); +export function testMediaQuery(mediaQueries: MediaCondition[], get: Getter) { + return mediaQueries.every((query) => test(query, get)); } -function test(mediaQuery: MediaCondition, effect: Effect): Boolean { +function test(mediaQuery: MediaCondition, get: Getter): Boolean { switch (mediaQuery[0]) { case "[]": case "!!": return false; case "!": - return !test(mediaQuery[1], effect); + return !test(mediaQuery[1], get); case "&": return mediaQuery[1].every((query) => { - return test(query, effect); + return test(query, get); }); case "|": return mediaQuery[1].some((query) => { - return test(query, effect); + return test(query, get); }); case ">": case ">=": case "<": case "<=": case "=": { - return testComparison(mediaQuery, effect); + return testComparison(mediaQuery, get); } } } -function testComparison(mediaQuery: MediaCondition, effect: Effect): Boolean { +function testComparison(mediaQuery: MediaCondition, get: Getter): Boolean { let left: number | undefined; const right = mediaQuery[2]; @@ -41,22 +41,20 @@ function testComparison(mediaQuery: MediaCondition, effect: Effect): Boolean { case "platform": return right === Platform.OS; case "prefers-color-scheme": { - return right === colorScheme.get(effect); + return right === get(colorScheme); } case "display-mode": return right === "native" || Platform.OS === right; case "min-width": - return typeof right === "number" && vw.get(effect) >= right; + return typeof right === "number" && get(vw) >= right; case "max-width": - return typeof right === "number" && vw.get(effect) <= right; + return typeof right === "number" && get(vw) <= right; case "min-height": - return typeof right === "number" && vh.get(effect) >= right; + return typeof right === "number" && get(vh) >= right; case "max-height": - return typeof right === "number" && vh.get(effect) <= right; + return typeof right === "number" && get(vh) <= right; case "orientation": - return right === "landscape" - ? vh.get(effect) < vw.get(effect) - : vh.get(effect) >= vw.get(effect); + return right === "landscape" ? get(vh) < get(vw) : get(vh) >= get(vw); } if (typeof right !== "number") { @@ -65,10 +63,10 @@ function testComparison(mediaQuery: MediaCondition, effect: Effect): Boolean { switch (mediaQuery[1]) { case "width": - left = vw.get(effect); + left = get(vw); break; case "height": - left = vh.get(effect); + left = get(vh); break; case "resolution": left = PixelRatio.get(); diff --git a/src/runtime/native/react/interaction.ts b/src/runtime/native/react/interaction.ts index ebcba1e..f6e1ad3 100644 --- a/src/runtime/native/react/interaction.ts +++ b/src/runtime/native/react/interaction.ts @@ -6,11 +6,11 @@ import { containerLayoutFamily, focusFamily, hoverFamily, - type Effect, + type Getter as WeakKey, } from "../reactivity"; const mainCache = new WeakMap< - Effect, + WeakKey, WeakMap void> >(); @@ -38,14 +38,14 @@ const defaultHandlers: Record = { }; export function getInteractionHandler( - effect: Effect, + weakKey: WeakKey, type: InteractionType, handler = defaultHandlers[type], ) { - let cache = mainCache.get(effect); + let cache = mainCache.get(weakKey); if (!cache) { cache = new WeakMap(); - mainCache.set(effect, cache); + mainCache.set(weakKey, cache); } let cached = cache.get(handler); @@ -57,29 +57,29 @@ export function getInteractionHandler( switch (type) { case "onLayout": - containerLayoutFamily(effect).set( + containerLayoutFamily(weakKey).set( (event as LayoutChangeEvent).nativeEvent.layout, ); break; case "onHoverIn": - hoverFamily(effect).set(true); + hoverFamily(weakKey).set(true); break; case "onHoverOut": - hoverFamily(effect).set(false); + hoverFamily(weakKey).set(false); break; case "onPress": break; case "onPressIn": - activeFamily(effect).set(true); + activeFamily(weakKey).set(true); break; case "onPressOut": - activeFamily(effect).set(false); + activeFamily(weakKey).set(false); break; case "onFocus": - focusFamily(effect).set(true); + focusFamily(weakKey).set(true); break; case "onBlur": - focusFamily(effect).set(false); + focusFamily(weakKey).set(false); break; } }; diff --git a/src/runtime/native/react/rules.ts b/src/runtime/native/react/rules.ts index 6694d51..cbe33fb 100644 --- a/src/runtime/native/react/rules.ts +++ b/src/runtime/native/react/rules.ts @@ -104,7 +104,7 @@ export function updateRules( if ( !testRule( rule, - state.ruleEffect, + state.ruleEffectGetter, currentProps, guards, inheritedContainers, @@ -132,20 +132,20 @@ export function updateRules( containers = { ...inheritedContainers, // This container becomes the default container - [DEFAULT_CONTAINER_NAME]: state.ruleEffect, + [DEFAULT_CONTAINER_NAME]: state.ruleEffectGetter, }; } // This this component as the named container for (const name of rule.c) { - containers![name] = state.ruleEffect; + containers![name] = state.ruleEffectGetter; } // Enable hover/active/focus/layout handlers - hoverFamily(state.ruleEffect); - activeFamily(state.ruleEffect); - focusFamily(state.ruleEffect); - containerLayoutFamily(state.ruleEffect); + hoverFamily(state.ruleEffectGetter); + activeFamily(state.ruleEffectGetter); + focusFamily(state.ruleEffectGetter); + containerLayoutFamily(state.ruleEffectGetter); } if (rule.a) { @@ -163,7 +163,7 @@ export function updateRules( if (process.env.NODE_ENV !== "production") { if (isRerender) { - let pressable = activeFamily.has(state.ruleEffect); + let pressable = activeFamily.has(state.ruleEffectGetter); if (Boolean(variables) !== Boolean(state.variables)) { console.log( @@ -190,7 +190,7 @@ export function updateRules( let pressable = process.env.NODE_ENV === "production" ? undefined - : activeFamily.has(state.ruleEffect); + : activeFamily.has(state.ruleEffectGetter); if (!rules.size && !state.stylesObs && !inlineVariables.size) { return { diff --git a/src/runtime/native/react/useNativeCss.ts b/src/runtime/native/react/useNativeCss.ts index 0132813..da48cf3 100644 --- a/src/runtime/native/react/useNativeCss.ts +++ b/src/runtime/native/react/useNativeCss.ts @@ -17,6 +17,7 @@ import { VariableContext, type ContainerContextValue, type Effect, + type Getter, type VariableContextValue, } from "../reactivity"; import { getStyledProps, stylesFamily } from "../styles"; @@ -35,6 +36,7 @@ export type ComponentState = { /** Reactive tracking */ ruleEffect: Effect; + ruleEffectGetter: Getter; styleEffect: Effect; /** The components props */ @@ -94,6 +96,7 @@ export function useNativeCss( return updateRules( { ruleEffect, + ruleEffectGetter: (observable) => observable.get(ruleEffect), styleEffect, configs, inheritedContainers, diff --git a/src/runtime/native/reactivity.ts b/src/runtime/native/reactivity.ts index 9ed16fb..c80154a 100644 --- a/src/runtime/native/reactivity.ts +++ b/src/runtime/native/reactivity.ts @@ -7,7 +7,8 @@ import { type LayoutRectangle, } from "react-native"; -import type { LightDarkVariable, StyleDescriptor } from "../../compiler"; +import type { StyleDescriptor, VariableValue } from "../../compiler"; +import { testMediaQuery } from "./conditions/media-query"; export type Effect = { observers: Set; @@ -189,24 +190,32 @@ export const VariableContext = createContext({ [VAR_SYMBOL]: true, }); -const lightDarkFamily = () => { - return family>(() => { - const obs = observable( - (read, lightDark = [undefined, undefined]) => { - const colorSchemeName = - read(colorScheme) ?? Appearance.getColorScheme() ?? "light"; +const rootVariableFamily = () => { + return family>(() => { + const obs = observable( + (read, variableValue) => { + if (!variableValue) return undefined; - return colorSchemeName === "dark" - ? (lightDark[1] ?? lightDark[0]) - : lightDark[0]; + for (const [value, mediaQuery] of variableValue) { + if (!mediaQuery) { + return value; + } + + if (testMediaQuery(mediaQuery, read)) { + return value; + } + } + + return undefined; }, ); return obs; }); }; -export const rootVariables = lightDarkFamily(); -export const universalVariables = lightDarkFamily(); + +export const rootVariables = rootVariableFamily(); +export const universalVariables = rootVariableFamily(); /** Units *********************************************************************/ @@ -237,7 +246,7 @@ Appearance.addChangeListener((event) => colorScheme.set(event.colorScheme)); /** Containers ****************************************************************/ -export type ContainerContextValue = Record; +export type ContainerContextValue = Record; export const ContainerContext = createContext({}); export const containerLayoutFamily = weakFamily(() => { diff --git a/src/runtime/native/styles/index.ts b/src/runtime/native/styles/index.ts index e22e2df..0a2f553 100644 --- a/src/runtime/native/styles/index.ts +++ b/src/runtime/native/styles/index.ts @@ -55,57 +55,57 @@ export function getStyledProps( } // Apply the handlers - if (hoverFamily.has(state.ruleEffect)) { + if (hoverFamily.has(state.ruleEffectGetter)) { result ??= {}; result.onHoverIn = getInteractionHandler( - state.ruleEffect, + state.ruleEffectGetter, "onHoverIn", inline?.onHoverIn, ); result.onHoverOut = getInteractionHandler( - state.ruleEffect, + state.ruleEffectGetter, "onHoverOut", inline?.onHoverOut, ); } - if (activeFamily.has(state.ruleEffect)) { + if (activeFamily.has(state.ruleEffectGetter)) { result ??= {}; result.onPress = getInteractionHandler( - state.ruleEffect, + state.ruleEffectGetter, "onPress", inline?.onPress, ); result.onPressIn = getInteractionHandler( - state.ruleEffect, + state.ruleEffectGetter, "onPressIn", inline?.onPressIn, ); result.onPressOut = getInteractionHandler( - state.ruleEffect, + state.ruleEffectGetter, "onPressOut", inline?.onPressOut, ); } - if (focusFamily.has(state.ruleEffect)) { + if (focusFamily.has(state.ruleEffectGetter)) { result ??= {}; result.onBlur = getInteractionHandler( - state.ruleEffect, + state.ruleEffectGetter, "onBlur", inline?.onBlur, ); result.onFocus = getInteractionHandler( - state.ruleEffect, + state.ruleEffectGetter, "onFocus", inline?.onFocus, ); } - if (containerLayoutFamily.has(state.ruleEffect)) { + if (containerLayoutFamily.has(state.ruleEffectGetter)) { result ??= {}; result.onLayout = getInteractionHandler( - state.ruleEffect, + state.ruleEffectGetter, "onLayout", inline?.onLayout, );