diff --git a/.config/jest.config.cjs b/.config/jest.config.cjs index a0f28bc4..3f321fc9 100644 --- a/.config/jest.config.cjs +++ b/.config/jest.config.cjs @@ -7,6 +7,6 @@ process.env.REACT_NATIVE_CSS_TEST_DEBUG = true; module.exports = { ...jestExpo, - testPathIgnorePatterns: ["dist/"], + testPathIgnorePatterns: ["dist/", ".*/_[a-zA-Z]"], setupFilesAfterEnv: ["./.config/jest.setup.js"], }; diff --git a/package.json b/package.json index 422a77ae..08011389 100644 --- a/package.json +++ b/package.json @@ -157,6 +157,7 @@ "@commitlint/config-conventional": "^19.8.1", "@eslint/js": "^9.30.1", "@ianvs/prettier-plugin-sort-imports": "^4.4.2", + "@tailwindcss/postcss": "^4.1.12", "@testing-library/react-native": "^13.2.0", "@tsconfig/react-native": "^3.0.6", "@types/babel__core": "^7", @@ -179,6 +180,7 @@ "lefthook": "^1.12.2", "lightningcss": "^1.30.1", "metro-runtime": "^0.83.0", + "postcss": "^8.5.6", "prettier": "^3.6.2", "react": "19.1.0", "react-native": "0.80.1", diff --git a/src/__tests__/vendor/tailwind/_tailwind.tsx b/src/__tests__/vendor/tailwind/_tailwind.tsx new file mode 100644 index 00000000..c8307473 --- /dev/null +++ b/src/__tests__/vendor/tailwind/_tailwind.tsx @@ -0,0 +1,192 @@ +import type { PropsWithChildren, ReactElement } from "react"; + +import tailwind from "@tailwindcss/postcss"; +import { + screen, + render as tlRender, + type RenderOptions, +} from "@testing-library/react-native"; +import postcss from "postcss"; +import { registerCSS } from "react-native-css/jest"; + +import type { compile } from "../../../compiler"; +import { View } from "../../../components"; + +const testID = "tailwind"; + +export type NativewindRenderOptions = RenderOptions & { + /** Replace the generated CSS*/ + css?: string; + /** Appended after the generated CSS */ + extraCss?: string; + /** Specify the className to use for the component @default sourceInline */ + className?: string; + /** Add `@source inline('')` to the CSS. @default Values are extracted from the component's className */ + sourceInline?: string[]; + /** Whether to include the theme in the generated CSS @default true */ + theme?: boolean; + /** Whether to include the preflight in the generated CSS @default false */ + preflight?: boolean; + /** Whether to include the plugin in the generated CSS. @default true */ + plugin?: boolean; + /** Enable debug logging. @default false - Set process.env.NATIVEWIND_TEST_AUTO_DEBUG and run tests with the node inspector */ + debug?: boolean; +}; + +const debugDefault = Boolean(process.env.NODE_OPTIONS?.includes("--inspect")); + +export async function render( + component: ReactElement, + { + css, + sourceInline = Array.from(getClassNames(component)), + debug = debugDefault, + theme = true, + preflight = false, + extraCss, + ...options + }: NativewindRenderOptions = {}, +): Promise & ReturnType> { + if (!css) { + css = ``; + + if (theme) { + css += `@import "tailwindcss/theme.css" layer(theme);\n`; + } + + if (preflight) { + css += `@import "tailwindcss/preflight.css" layer(base);\n`; + } + + css += `@import "tailwindcss/utilities.css" layer(utilities) source(none);\n`; + } + + css += sourceInline + .map((source) => `@source inline("${source}");`) + .join("\n"); + + if (extraCss) { + css += `\n${extraCss}`; + } + + if (debug) { + console.log(`Input CSS:\n---\n${css}\n---\n`); + } + + // Process the TailwindCSS + const { css: output } = await postcss([ + /* Tailwind seems to internally cache things, so we need a random value to cache bust */ + tailwind({ base: Date.now().toString() }), + ]).process(css, { + from: __dirname, + }); + + if (debug) { + console.log(`Output CSS:\n---\n${output}\n---\n`); + } + + const compiled = registerCSS(output, { debug }); + + return Object.assign( + {}, + tlRender(component, { + ...options, + }), + compiled, + ); +} + +render.debug = ( + component: ReactElement, + options: RenderOptions = {}, +) => { + return render(component, { ...options, debug: true }); +}; + +function getClassNames( + component: ReactElement, + classNames = new Set(), +) { + if ( + typeof component.props === "object" && + "className" in component.props && + typeof component.props.className === "string" + ) { + classNames.add(component.props.className); + } + + if (component.props.children) { + const children: ReactElement[] = Array.isArray(component.props.children) + ? component.props.children + : [component.props.children]; + + for (const child of children) { + getClassNames(child as ReactElement, classNames); + } + } + + return classNames; +} + +export async function renderSimple({ + className, + ...options +}: NativewindRenderOptions & { className: string }) { + const { warnings: warningFn } = await render( + , + options, + ); + const component = screen.getByTestId(testID, { hidden: true }); + + // Strip the testID and the children + const { testID: _testID, children, ...props } = component.props; + + const compilerWarnings = warningFn(); + + let warnings: Record | undefined; + + if (compilerWarnings.properties) { + warnings ??= {}; + warnings.properties = compilerWarnings.properties; + } + + const warningValues = compilerWarnings.values; + + if (warningValues) { + warnings ??= {}; + warnings.values = Object.fromEntries( + Object.entries(warningValues).map(([key, value]) => [ + key, + value.length > 1 ? value : value[0], + ]), + ); + } + + return warnings ? { props, warnings } : { props }; +} + +renderSimple.debug = ( + options: NativewindRenderOptions & { className: string }, +) => { + return renderSimple({ ...options, debug: true }); +}; + +/** + * Helper method that uses the current test name to render the component + * Doesn't not support multiple components or changing the component type + */ +export async function renderCurrentTest({ + sourceInline = [expect.getState().currentTestName?.split(/\s+/).at(-1) ?? ""], + className = sourceInline.join(" "), + ...options +}: NativewindRenderOptions = {}) { + return renderSimple({ + ...options, + sourceInline, + className, + }); +} + +renderCurrentTest.debug = (options: NativewindRenderOptions = {}) => { + return renderCurrentTest({ ...options, debug: true }); +}; diff --git a/src/__tests__/vendor/tailwind/transform.test.ts b/src/__tests__/vendor/tailwind/transform.test.ts new file mode 100644 index 00000000..5c9a17a3 --- /dev/null +++ b/src/__tests__/vendor/tailwind/transform.test.ts @@ -0,0 +1,279 @@ +import { renderCurrentTest, renderSimple } from "./_tailwind"; + +describe("Transforms - Scale", () => { + test("scale-0", async () => { + expect(await renderCurrentTest()).toStrictEqual({ + props: { + style: { + transform: [{ scale: "0%" }], + }, + }, + }); + }); + test("scale-x-50", async () => { + expect(await renderCurrentTest()).toStrictEqual({ + props: { + style: { + transform: [{ scaleX: "50%" }, { scaleY: 1 }], + }, + }, + }); + }); + test("scale-y-50", async () => { + expect(await renderCurrentTest()).toStrictEqual({ + props: { + style: { + transform: [{ scaleX: 1 }, { scaleY: "50%" }], + }, + }, + }); + }); + test("scale-50", async () => { + expect(await renderCurrentTest()).toStrictEqual({ + props: { + style: { + transform: [{ scale: "50%" }], + }, + }, + }); + }); +}); + +describe("Transforms - Rotate", () => { + test("rotate-0", async () => { + expect(await renderCurrentTest()).toStrictEqual({ + props: { + style: { + transform: [{ rotateZ: "0deg" }], + }, + }, + }); + }); + test("rotate-180", async () => { + expect(await renderCurrentTest()).toStrictEqual({ + props: { + style: { + transform: [{ rotateZ: "180deg" }], + }, + }, + }); + }); + test("rotate-[30deg]", async () => { + expect(await renderCurrentTest()).toStrictEqual({ + props: { + style: { + transform: [{ rotateZ: "30deg" }], + }, + }, + }); + }); +}); + +describe("Transforms - Translate", () => { + test("translate-x-0", async () => { + expect(await renderCurrentTest()).toStrictEqual({ + props: { + style: { + transform: [{ translateX: 0 }], + }, + }, + }); + }); + test("translate-y-0", async () => { + expect(await renderCurrentTest()).toStrictEqual({ + props: { + style: { + transform: [{ translateY: 0 }], + }, + }, + }); + }); + test("translate-x-px", async () => { + expect(await renderCurrentTest()).toStrictEqual({ + props: { + style: { + transform: [{ translateX: 1 }], + }, + }, + }); + }); + test("translate-y-px", async () => { + expect(await renderCurrentTest()).toStrictEqual({ + props: { + style: { + transform: [{ translateY: 1 }], + }, + }, + }); + }); + test("translate-x-1", async () => { + expect(await renderCurrentTest()).toStrictEqual({ + props: { + style: { + transform: [{ translateX: 3.5 }], + }, + }, + }); + }); + test("translate-y-1", async () => { + expect(await renderCurrentTest()).toStrictEqual({ + props: { + style: { + transform: [{ translateY: 3.5 }], + }, + }, + }); + }); +}); + +describe("Transforms - Translate (%)", () => { + test("translate-x-1/2", async () => { + expect(await renderCurrentTest()).toStrictEqual({ + props: { style: { transform: [{ translateX: "50%" }] } }, + }); + }); + + test("translate-y-1/2", async () => { + expect(await renderCurrentTest()).toStrictEqual({ + props: { + style: { + transform: [{ translateY: "50%" }], + }, + }, + }); + }); + test("translate-x-full", async () => { + expect(await renderCurrentTest()).toStrictEqual({ + props: { + style: { + transform: [{ translateX: "100%" }], + }, + }, + }); + }); + test("translate-y-full", async () => { + expect(await renderCurrentTest()).toStrictEqual({ + props: { + style: { + transform: [{ translateY: "100%" }], + }, + }, + }); + }); +}); + +describe("Transforms - Skew", () => { + test("skew-x-0", async () => { + expect(await renderCurrentTest()).toStrictEqual({ + props: { + style: { + transform: [{ skewX: "0deg" }], + }, + }, + }); + }); + test("skew-y-0", async () => { + expect(await renderCurrentTest()).toStrictEqual({ + props: { + style: { + transform: [{ skewY: "0deg" }], + }, + }, + }); + }); + test("skew-x-1", async () => { + expect(await renderCurrentTest()).toStrictEqual({ + props: { + style: { + transform: [{ skewX: "1deg" }], + }, + }, + }); + }); + test("skew-y-1", async () => { + expect(await renderCurrentTest()).toStrictEqual({ + props: { + style: { + transform: [{ skewY: "1deg" }], + }, + }, + }); + }); +}); + +describe("Transforms - Mixed", () => { + test("rotate-90 skew-y-1 translate-x-1", async () => { + expect( + await renderSimple({ + className: "rotate-90 skew-y-1 translate-x-1", + }), + ).toStrictEqual({ + props: { + style: { + transform: [ + { skewY: "1deg" }, + { translateX: 3.5 }, + { rotateZ: "90deg" }, + ], + }, + }, + }); + }); + + describe("Transforms - Transform Origin", () => { + test("origin-center", async () => { + expect(await renderCurrentTest()).toStrictEqual({ + props: {}, + warnings: { properties: ["transform-origin"] }, + }); + }); + test("origin-top", async () => { + expect(await renderCurrentTest()).toStrictEqual({ + props: {}, + warnings: { properties: ["transform-origin"] }, + }); + }); + test("origin-top-right", async () => { + expect(await renderCurrentTest()).toStrictEqual({ + props: {}, + warnings: { properties: ["transform-origin"] }, + }); + }); + test("origin-right", async () => { + expect(await renderCurrentTest()).toStrictEqual({ + props: {}, + warnings: { properties: ["transform-origin"] }, + }); + }); + test("origin-bottom-right", async () => { + expect(await renderCurrentTest()).toStrictEqual({ + props: {}, + warnings: { properties: ["transform-origin"] }, + }); + }); + test("origin-bottom", async () => { + expect(await renderCurrentTest()).toStrictEqual({ + props: {}, + warnings: { properties: ["transform-origin"] }, + }); + }); + test("origin-bottom-left", async () => { + expect(await renderCurrentTest()).toStrictEqual({ + props: {}, + warnings: { properties: ["transform-origin"] }, + }); + }); + test("origin-left", async () => { + expect(await renderCurrentTest()).toStrictEqual({ + props: {}, + warnings: { properties: ["transform-origin"] }, + }); + }); + test("origin-top-left", async () => { + expect(await renderCurrentTest()).toStrictEqual({ + props: {}, + warnings: { properties: ["transform-origin"] }, + }); + }); + }); +}); diff --git a/src/compiler/compiler.ts b/src/compiler/compiler.ts index 24dfea9c..aea5dc45 100644 --- a/src/compiler/compiler.ts +++ b/src/compiler/compiler.ts @@ -20,6 +20,7 @@ import { parseDeclaration } from "./declarations"; import { extractKeyFrames } from "./keyframes"; import { parseMediaQuery } from "./media-query"; import { StylesheetBuilder } from "./stylesheet"; +import { supportsConditionValid } from "./supports"; const defaultLogger = debug("react-native-css:compiler"); @@ -199,6 +200,13 @@ function extractRule(rule: Rule, builder: StylesheetBuilder) { extractRule(layerRule, builder); } break; + case "supports": + if (supportsConditionValid(rule.value.condition)) { + for (const layerRule of rule.value.rules) { + extractRule(layerRule, builder); + } + } + break; case "custom": case "font-face": case "font-palette-values": @@ -211,7 +219,6 @@ function extractRule(rule: Rule, builder: StylesheetBuilder) { case "unknown": case "import": case "page": - case "supports": case "counter-style": case "moz-document": case "nesting": diff --git a/src/compiler/declarations.ts b/src/compiler/declarations.ts index 6c455e94..b0415292 100644 --- a/src/compiler/declarations.ts +++ b/src/compiler/declarations.ts @@ -2546,9 +2546,7 @@ export function parseCalcArguments( ) { const parsed: StyleDescriptor[] = []; - let mode: "number" | "percentage" | undefined; - - for (const [currentIndex, arg] of args.entries()) { + for (const arg of args) { switch (arg.type) { case "env": { parsed.push(parseEnv(arg.value, builder)); @@ -2568,11 +2566,9 @@ export function parseCalcArguments( } case "length": { const value = parseLength(arg.value, builder); - if (value !== undefined) { parsed.push(value); } - break; } case "color": @@ -2595,49 +2591,19 @@ export function parseCalcArguments( } break; case "percentage": - mode ??= "percentage"; - if (mode !== "percentage") return; - parsed.push(`${arg.value.value * 100}%`); + parsed.push(`${round(arg.value.value * 100)}%`); break; case "number": { - mode ??= "number"; - if (mode !== "number") return; - parsed.push(arg.value.value); + parsed.push(round(arg.value.value)); break; } case "parenthesis-block": { - /** - * If we have a parenthesis block, we just treat it as a nested calc function - * Because there could be multiple parenthesis blocks, this is recursive - */ - let closeParenthesisIndex = -1; - for (let index = args.length - 1; index > 0; index--) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const value = args[index]!; - if ( - value.type === "token" && - value.value.type === "close-parenthesis" - ) { - closeParenthesisIndex = index; - break; - } - } - - if (closeParenthesisIndex === -1) { - return; - } - - const innerCalcArgs = args - // Extract the inner calcArgs including the parenthesis. This mutates args - .splice(currentIndex, closeParenthesisIndex - currentIndex + 1) - // Then drop the surrounding parenthesis - .slice(1, -1); - - parsed.push(parseCalcFn("calc", innerCalcArgs, builder, property)); - + parsed.push("("); break; } case "close-parenthesis": + parsed.push(")"); + break; case "string": case "function": case "ident": diff --git a/src/compiler/selector-builder.ts b/src/compiler/selector-builder.ts index 35c77ed6..5fd8c0ee 100644 --- a/src/compiler/selector-builder.ts +++ b/src/compiler/selector-builder.ts @@ -55,6 +55,8 @@ export function getClassNameSelectors( (selector): (ReactNativeGlobalSelector | PartialSelector)[] => { if (isRootVariableSelector(selector)) { return [{ type: "rootVariables" }]; + } else if (isUniversalSelector(selector)) { + return [{ type: "universalVariables" }]; } else { return ( parseComponents( @@ -519,6 +521,10 @@ function isRootVariableSelector([first, second]: Selector) { ); } +function isUniversalSelector([first, second]: Selector) { + return first && first.type === "universal" && !second; +} + export function toRNProperty(str: T) { return str .replace(/^-rn-/, "") diff --git a/src/compiler/supports.ts b/src/compiler/supports.ts new file mode 100644 index 00000000..1558d616 --- /dev/null +++ b/src/compiler/supports.ts @@ -0,0 +1,26 @@ +import type { SupportsCondition } from "lightningcss"; + +export function supportsConditionValid(condition: SupportsCondition): boolean { + if (condition.type === "and") { + return condition.value.every((condition) => { + return supportsConditionValid(condition); + }); + } else if (condition.type === "or") { + return condition.value.some((condition) => { + return supportsConditionValid(condition); + }); + } else if (condition.type === "not") { + return !supportsConditionValid(condition.value); + } else if (condition.type === "declaration") { + return Boolean( + declarations[condition.propertyId.property]?.includes(condition.value), + ); + } + + return false; +} + +const declarations: Record = { + // We don't actually support this, but its needed for Tailwind CSS + "-moz-orient": ["inline"], +}; diff --git a/src/runtime/native/styles/calculate-props.ts b/src/runtime/native/styles/calculate-props.ts index c9c6aa21..d5a8c326 100644 --- a/src/runtime/native/styles/calculate-props.ts +++ b/src/runtime/native/styles/calculate-props.ts @@ -11,6 +11,7 @@ import { type Getter, type VariableContextValue, } from "../reactivity"; +import { transformKeys } from "./defaults"; import { resolveValue } from "./resolve"; export function calculateProps( @@ -28,6 +29,7 @@ export function calculateProps( let important: Record | undefined; const delayedStyles: (() => void)[] = []; + const transformStyles: (() => void)[] = []; for (const rule of rules) { if (VAR_SYMBOL in rule) { @@ -67,6 +69,7 @@ export function calculateProps( inlineVariables, inheritedVariables, delayedStyles, + transformStyles, guards, target, topLevelTarget, @@ -78,6 +81,10 @@ export function calculateProps( delayedStyle(); } + for (const transformStyle of transformStyles) { + transformStyle(); + } + return { normal, guards, @@ -91,6 +98,7 @@ export function applyDeclarations( inlineVariables: InlineVariable, inheritedVariables: VariableContextValue, delayedStyles: (() => void)[] = [], + transformStyles: (() => void)[] = [], guards: RenderGuard[] = [], target: Record = {}, topLevelTarget = target, @@ -136,7 +144,7 @@ export function applyDeclarations( const shouldDelay = declaration[2]; - if (shouldDelay) { + if (shouldDelay || transformKeys.has(prop)) { /** * We need to delay the resolution of this value until after all * styles have been calculated. But another style might override @@ -149,9 +157,9 @@ export function applyDeclarations( const originalValue = value; // This needs to be a object with the [prop] so we can discover in transform arrays value = { [prop]: true }; - delayedStyles.push(() => { - if (getDeepPath(target, prop) === value) { - delete target[prop]; + + if (transformKeys.has(prop)) { + transformStyles.push(() => { value = resolveValue(originalValue, get, { inlineVariables, inheritedVariables, @@ -159,8 +167,21 @@ export function applyDeclarations( calculateProps, }); applyValue(target, prop, value); - } - }); + }); + } else { + delayedStyles.push(() => { + if (getDeepPath(target, prop) === value) { + delete target[prop]; + value = resolveValue(originalValue, get, { + inlineVariables, + inheritedVariables, + renderGuards: guards, + calculateProps, + }); + applyValue(target, prop, value); + } + }); + } } else { value = resolveValue(value, get, { inlineVariables, diff --git a/src/runtime/native/styles/functions/calc.ts b/src/runtime/native/styles/functions/calc.ts index 555679af..9c267746 100644 --- a/src/runtime/native/styles/functions/calc.ts +++ b/src/runtime/native/styles/functions/calc.ts @@ -1,97 +1,121 @@ -/* eslint-disable */ -import { isStyleDescriptorArray } from "../../../utils"; import type { StyleFunctionResolver } from "../resolve"; -const calcPrecedence: Record = { - "+": 1, - "-": 1, - "*": 2, - "/": 2, -}; +interface Value { + value: number; + isPercent: boolean; +} + +const precedence: Record = { "+": 1, "-": 1, "*": 2, "/": 2 }; export const calc: StyleFunctionResolver = (resolveValue, func) => { - let mode: "number" | "percentage" | undefined; - const values: number[] = []; - const ops: string[] = []; + const tokens = resolveValue(func[2]); - const args = resolveValue(func[2]); + if (!Array.isArray(tokens)) { + return tokens; + } - if (!isStyleDescriptorArray(args)) return; + // --- Step 1: Convert to RPN (Shunting-Yard) --- + const output: (string | Value)[] = []; + const ops: string[] = []; - for (let token of args) { - if (typeof token === "number") { - if (!mode) mode = "number"; - if (mode !== "number") return; - values.push(token); - continue; - } else if (typeof token === "string") { - if (token === "(") { - ops.push(token); - } else if (token === ")") { - // Resolve all values within the brackets - while (ops.length && ops[ops.length - 1] !== "(") { - applyCalcOperator(ops.pop()!, values.pop(), values.pop(), values); - } - ops.pop(); - } else if (token.endsWith("%")) { - if (!mode) mode = "percentage"; - if (mode !== "percentage") return; - values.push(Number.parseFloat(token.slice(0, -1))); - } else { - // This means we have an operator + for (const t of tokens) { + if (typeof t === "number") { + output.push({ value: round(t), isPercent: false }); + } else if (typeof t === "string") { + if (t.endsWith("%")) { + const num = parseFloat(t.slice(0, -1)); + if (Number.isNaN(num)) return undefined; + output.push({ value: round(num / 100), isPercent: true }); + } else if (t === "+" || t === "-" || t === "*" || t === "/") { while ( ops.length && - ops[ops.length - 1] && - // @ts-ignore - calcPrecedence[ops[ops.length - 1]] >= calcPrecedence[token] + (ops[ops.length - 1] === "+" || + ops[ops.length - 1] === "-" || + ops[ops.length - 1] === "*" || + ops[ops.length - 1] === "/") && + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + (precedence[ops[ops.length - 1]!] ?? 0) >= (precedence[t] ?? 0) ) { - applyCalcOperator(ops.pop()!, values.pop(), values.pop(), values); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + output.push(ops.pop()!); + } + ops.push(t); + } else if (t === "(") { + ops.push(t); + } else if (t === ")") { + while (ops.length && ops[ops.length - 1] !== "(") { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + output.push(ops.pop()!); } - ops.push(token); + if (ops.pop() !== "(") return undefined; // mismatched parens + } else { + return undefined; // invalid token } } else { - // We got something unexpected - return; + return undefined; } } - while (ops.length) { - applyCalcOperator(ops.pop()!, values.pop(), values.pop(), values); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const op = ops.pop()!; + if (op === "(" || op === ")") return undefined; + output.push(op); } - if (!mode) return; + // --- Step 2: Evaluate RPN --- + const stack: Value[] = []; + for (const t of output) { + if (typeof t !== "string") { + stack.push(t); + } else { + const b = stack.pop(); + const a = stack.pop(); + if (!a || !b) return undefined; - const num = values[0]; + const res: Value = { value: 0, isPercent: false }; - if (typeof num !== "number") { - return; + switch (t) { + case "+": + case "-": { + if (a.isPercent !== b.isPercent) return undefined; // cannot mix + res.isPercent = a.isPercent; + res.value = + t === "+" ? round(a.value + b.value) : round(a.value - b.value); + break; + } + case "*": { + if (a.isPercent && b.isPercent) return undefined; // ambiguous + res.isPercent = a.isPercent || b.isPercent; + res.value = round(a.value * b.value); + break; + } + case "/": { + if (b.value === 0) return undefined; + if (a.isPercent && b.isPercent) { + // % / % → plain number ratio + res.isPercent = false; + res.value = round(a.value / b.value); + } else { + res.isPercent = a.isPercent || b.isPercent; + res.value = round(a.value / b.value); + } + break; + } + } + stack.push(res); + } } - const value = Math.round((num + Number.EPSILON) * 100) / 100; + if (stack.length !== 1) return undefined; - if (mode === "percentage") { - return `${value}%`; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const final = stack[0]!; + if (final.isPercent) { + return `${final.value * 100}%`; } - - return value; + return final.value; }; -function applyCalcOperator( - operator: string, - b = 0, // These are reversed because we pop them off the stack - a = 0, - values: number[], -) { - switch (operator) { - case "+": - return values.push(a + b); - case "-": - return values.push(a - b); - case "*": - return values.push(a * b); - case "/": - return values.push(a / b); - } - - return; +function round(number: number) { + return Math.round((number + Number.EPSILON) * 10000) / 10000; } diff --git a/src/runtime/native/styles/functions/transform-functions.ts b/src/runtime/native/styles/functions/transform-functions.ts index f6802d37..3ae1f127 100644 --- a/src/runtime/native/styles/functions/transform-functions.ts +++ b/src/runtime/native/styles/functions/transform-functions.ts @@ -1,51 +1,84 @@ +import { isStyleDescriptorArray } from "../../../utils"; import type { StyleFunctionResolver } from "../resolve"; export const scale: StyleFunctionResolver = (resolveValue, descriptor) => { - const values = resolveValue(descriptor[2]); - - if (Array.isArray(values)) { - const [x, y] = values as [string | number][]; - - if (values.length === 2 && x === y) { - return { scale: x }; - } else if (values.length === 2) { - return [{ scaleX: x }, { scaleY: y }]; - } else { - return { scale: x }; - } - } else { - return { scale: values }; + const args = descriptor[2]; + + if (!isStyleDescriptorArray(args)) { + return { scale: resolveValue(args) }; + } + + const x = resolveValue(args[0]); + const y = resolveValue(args[1]); + + const isXValid = typeof x === "string" || typeof x === "number"; + const isYValid = typeof y === "string" || typeof y === "number"; + + if (isXValid && isYValid) { + return x === y ? { scale: x } : [{ scaleX: x }, { scaleY: y }]; + } else if (isXValid) { + return { scaleX: x }; + } else if (isYValid) { + return { scaleY: y }; } + + return; }; export const rotate: StyleFunctionResolver = (resolveValue, descriptor) => { - const values = resolveValue(descriptor[2]); - - if (Array.isArray(values)) { - const [x, y, z] = values as [string | number][]; - - if (values.length === 3 && x === y && x === z) { - return { rotate: values }; - } else if (values.length === 3) { - return [{ rotateX: x }, { rotateY: y }, { rotateZ: z }]; - } else if (values.length === 2) { - return [{ rotateX: x }, { rotateY: y }]; - } else { - return { rotate: values }; - } - } else { - return { rotate: values }; + const args = descriptor[2]; + + if (!isStyleDescriptorArray(args)) { + return { rotate: resolveValue(args) }; + } + + const x = resolveValue(args[0]); + const y = resolveValue(args[1]); + const z = resolveValue(args[2]); + + const isXValid = typeof x === "string" || typeof x === "number"; + const isYValid = typeof y === "string" || typeof y === "number"; + const isZValid = typeof z === "string" || typeof z === "number"; + + if (isXValid && isYValid && isZValid) { + return [{ rotateX: x }, { rotateY: y }, { rotateZ: z }]; + } else if (isXValid && isYValid) { + return [{ rotateX: x }, { rotateY: y }]; + } else if (isXValid && isZValid) { + return [{ rotateX: x }, { rotateZ: z }]; + } else if (isYValid && isZValid) { + return [{ rotateY: y }, { rotateZ: z }]; + } else if (isXValid) { + return { rotateX: x }; + } else if (isYValid) { + return { rotateY: y }; + } else if (isZValid) { + return { rotateZ: z }; } + + return; }; export const translate: StyleFunctionResolver = (resolveValue, descriptor) => { - const values = resolveValue(descriptor[2]); + const args = descriptor[2]; - if (Array.isArray(values)) { - const [x, y] = values as [string | number][]; + if (!isStyleDescriptorArray(args)) { + return; + } + + const x = resolveValue(args[0]); + const y = resolveValue(args[1]); + const isXValid = typeof x === "string" || typeof x === "number"; + const isYValid = typeof y === "string" || typeof y === "number"; + + if (isXValid && isYValid) { return [{ translateX: x }, { translateY: y }]; - } else { - return { translateX: values }; + } else if (isXValid) { + return { translateX: x }; + } else if (isYValid) { + return { translateY: y }; } + + return; }; diff --git a/src/runtime/native/styles/shorthands/transform.ts b/src/runtime/native/styles/shorthands/transform.ts index f9046225..ef603dd8 100644 --- a/src/runtime/native/styles/shorthands/transform.ts +++ b/src/runtime/native/styles/shorthands/transform.ts @@ -11,7 +11,9 @@ export const transform: StyleFunctionResolver = ( const transforms = resolveValue(transformDescriptor[2]); if (Array.isArray(transforms)) { - return transforms.filter((transform) => transform !== undefined) as unknown; + return transforms.filter( + (transform) => transform !== undefined && transform !== "initial", + ) as unknown; } else if (transforms) { // If it's a single transform, wrap it in an array return [transforms]; diff --git a/src/runtime/utils/objects.ts b/src/runtime/utils/objects.ts index d7c5d21b..12e69ccd 100644 --- a/src/runtime/utils/objects.ts +++ b/src/runtime/utils/objects.ts @@ -24,7 +24,9 @@ export function getDeepPath(source: any, paths: string | string[] | false) { return target; } else if (transformKeys.has(paths)) { - return source?.transform?.find((t: any) => t[paths] !== undefined); + return Array.isArray(source?.transform) + ? source.transform.find((t: any) => t[paths] !== undefined) + : source.transform; } else { return source?.[paths]; } diff --git a/yarn.lock b/yarn.lock index b45b8fc1..c7011006 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1805,7 +1805,7 @@ __metadata: languageName: node linkType: hard -"@emnapi/core@npm:^1.4.3": +"@emnapi/core@npm:^1.4.3, @emnapi/core@npm:^1.4.5": version: 1.4.5 resolution: "@emnapi/core@npm:1.4.5" dependencies: @@ -1815,7 +1815,7 @@ __metadata: languageName: node linkType: hard -"@emnapi/runtime@npm:^1.4.3": +"@emnapi/runtime@npm:^1.4.3, @emnapi/runtime@npm:^1.4.5": version: 1.4.5 resolution: "@emnapi/runtime@npm:1.4.5" dependencies: @@ -1824,7 +1824,7 @@ __metadata: languageName: node linkType: hard -"@emnapi/wasi-threads@npm:1.0.4, @emnapi/wasi-threads@npm:^1.0.2": +"@emnapi/wasi-threads@npm:1.0.4, @emnapi/wasi-threads@npm:^1.0.2, @emnapi/wasi-threads@npm:^1.0.4": version: 1.0.4 resolution: "@emnapi/wasi-threads@npm:1.0.4" dependencies: @@ -3023,6 +3023,16 @@ __metadata: languageName: node linkType: hard +"@jridgewell/remapping@npm:^2.3.4": + version: 2.3.5 + resolution: "@jridgewell/remapping@npm:2.3.5" + dependencies: + "@jridgewell/gen-mapping": "npm:^0.3.5" + "@jridgewell/trace-mapping": "npm:^0.3.24" + checksum: 10c0/3de494219ffeb2c5c38711d0d7bb128097edf91893090a2dbc8ee0b55d092bb7347b1fd0f478486c5eab010e855c73927b1666f2107516d472d24a73017d1194 + languageName: node + linkType: hard + "@jridgewell/resolve-uri@npm:^3.1.0": version: 3.1.2 resolution: "@jridgewell/resolve-uri@npm:3.1.2" @@ -3057,7 +3067,7 @@ __metadata: languageName: node linkType: hard -"@napi-rs/wasm-runtime@npm:^0.2.11": +"@napi-rs/wasm-runtime@npm:^0.2.11, @napi-rs/wasm-runtime@npm:^0.2.12": version: 0.2.12 resolution: "@napi-rs/wasm-runtime@npm:0.2.12" dependencies: @@ -3569,6 +3579,21 @@ __metadata: languageName: node linkType: hard +"@tailwindcss/node@npm:4.1.12": + version: 4.1.12 + resolution: "@tailwindcss/node@npm:4.1.12" + dependencies: + "@jridgewell/remapping": "npm:^2.3.4" + enhanced-resolve: "npm:^5.18.3" + jiti: "npm:^2.5.1" + lightningcss: "npm:1.30.1" + magic-string: "npm:^0.30.17" + source-map-js: "npm:^1.2.1" + tailwindcss: "npm:4.1.12" + checksum: 10c0/8dcf3658126fd9bbd95391226022c1f480beacd7a1304a6afb416361bfab4e09b2c89733061e28d3b7429d3c3f77934c56da9d824aa34433d973adccd2080253 + languageName: node + linkType: hard + "@tailwindcss/oxide-android-arm64@npm:4.1.11": version: 4.1.11 resolution: "@tailwindcss/oxide-android-arm64@npm:4.1.11" @@ -3576,6 +3601,13 @@ __metadata: languageName: node linkType: hard +"@tailwindcss/oxide-android-arm64@npm:4.1.12": + version: 4.1.12 + resolution: "@tailwindcss/oxide-android-arm64@npm:4.1.12" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + "@tailwindcss/oxide-darwin-arm64@npm:4.1.11": version: 4.1.11 resolution: "@tailwindcss/oxide-darwin-arm64@npm:4.1.11" @@ -3583,6 +3615,13 @@ __metadata: languageName: node linkType: hard +"@tailwindcss/oxide-darwin-arm64@npm:4.1.12": + version: 4.1.12 + resolution: "@tailwindcss/oxide-darwin-arm64@npm:4.1.12" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + "@tailwindcss/oxide-darwin-x64@npm:4.1.11": version: 4.1.11 resolution: "@tailwindcss/oxide-darwin-x64@npm:4.1.11" @@ -3590,6 +3629,13 @@ __metadata: languageName: node linkType: hard +"@tailwindcss/oxide-darwin-x64@npm:4.1.12": + version: 4.1.12 + resolution: "@tailwindcss/oxide-darwin-x64@npm:4.1.12" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + "@tailwindcss/oxide-freebsd-x64@npm:4.1.11": version: 4.1.11 resolution: "@tailwindcss/oxide-freebsd-x64@npm:4.1.11" @@ -3597,6 +3643,13 @@ __metadata: languageName: node linkType: hard +"@tailwindcss/oxide-freebsd-x64@npm:4.1.12": + version: 4.1.12 + resolution: "@tailwindcss/oxide-freebsd-x64@npm:4.1.12" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + "@tailwindcss/oxide-linux-arm-gnueabihf@npm:4.1.11": version: 4.1.11 resolution: "@tailwindcss/oxide-linux-arm-gnueabihf@npm:4.1.11" @@ -3604,6 +3657,13 @@ __metadata: languageName: node linkType: hard +"@tailwindcss/oxide-linux-arm-gnueabihf@npm:4.1.12": + version: 4.1.12 + resolution: "@tailwindcss/oxide-linux-arm-gnueabihf@npm:4.1.12" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + "@tailwindcss/oxide-linux-arm64-gnu@npm:4.1.11": version: 4.1.11 resolution: "@tailwindcss/oxide-linux-arm64-gnu@npm:4.1.11" @@ -3611,6 +3671,13 @@ __metadata: languageName: node linkType: hard +"@tailwindcss/oxide-linux-arm64-gnu@npm:4.1.12": + version: 4.1.12 + resolution: "@tailwindcss/oxide-linux-arm64-gnu@npm:4.1.12" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + "@tailwindcss/oxide-linux-arm64-musl@npm:4.1.11": version: 4.1.11 resolution: "@tailwindcss/oxide-linux-arm64-musl@npm:4.1.11" @@ -3618,6 +3685,13 @@ __metadata: languageName: node linkType: hard +"@tailwindcss/oxide-linux-arm64-musl@npm:4.1.12": + version: 4.1.12 + resolution: "@tailwindcss/oxide-linux-arm64-musl@npm:4.1.12" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + "@tailwindcss/oxide-linux-x64-gnu@npm:4.1.11": version: 4.1.11 resolution: "@tailwindcss/oxide-linux-x64-gnu@npm:4.1.11" @@ -3625,6 +3699,13 @@ __metadata: languageName: node linkType: hard +"@tailwindcss/oxide-linux-x64-gnu@npm:4.1.12": + version: 4.1.12 + resolution: "@tailwindcss/oxide-linux-x64-gnu@npm:4.1.12" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + "@tailwindcss/oxide-linux-x64-musl@npm:4.1.11": version: 4.1.11 resolution: "@tailwindcss/oxide-linux-x64-musl@npm:4.1.11" @@ -3632,6 +3713,13 @@ __metadata: languageName: node linkType: hard +"@tailwindcss/oxide-linux-x64-musl@npm:4.1.12": + version: 4.1.12 + resolution: "@tailwindcss/oxide-linux-x64-musl@npm:4.1.12" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + "@tailwindcss/oxide-wasm32-wasi@npm:4.1.11": version: 4.1.11 resolution: "@tailwindcss/oxide-wasm32-wasi@npm:4.1.11" @@ -3646,6 +3734,20 @@ __metadata: languageName: node linkType: hard +"@tailwindcss/oxide-wasm32-wasi@npm:4.1.12": + version: 4.1.12 + resolution: "@tailwindcss/oxide-wasm32-wasi@npm:4.1.12" + dependencies: + "@emnapi/core": "npm:^1.4.5" + "@emnapi/runtime": "npm:^1.4.5" + "@emnapi/wasi-threads": "npm:^1.0.4" + "@napi-rs/wasm-runtime": "npm:^0.2.12" + "@tybys/wasm-util": "npm:^0.10.0" + tslib: "npm:^2.8.0" + conditions: cpu=wasm32 + languageName: node + linkType: hard + "@tailwindcss/oxide-win32-arm64-msvc@npm:4.1.11": version: 4.1.11 resolution: "@tailwindcss/oxide-win32-arm64-msvc@npm:4.1.11" @@ -3653,6 +3755,13 @@ __metadata: languageName: node linkType: hard +"@tailwindcss/oxide-win32-arm64-msvc@npm:4.1.12": + version: 4.1.12 + resolution: "@tailwindcss/oxide-win32-arm64-msvc@npm:4.1.12" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + "@tailwindcss/oxide-win32-x64-msvc@npm:4.1.11": version: 4.1.11 resolution: "@tailwindcss/oxide-win32-x64-msvc@npm:4.1.11" @@ -3660,6 +3769,13 @@ __metadata: languageName: node linkType: hard +"@tailwindcss/oxide-win32-x64-msvc@npm:4.1.12": + version: 4.1.12 + resolution: "@tailwindcss/oxide-win32-x64-msvc@npm:4.1.12" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@tailwindcss/oxide@npm:4.1.11": version: 4.1.11 resolution: "@tailwindcss/oxide@npm:4.1.11" @@ -3707,6 +3823,53 @@ __metadata: languageName: node linkType: hard +"@tailwindcss/oxide@npm:4.1.12": + version: 4.1.12 + resolution: "@tailwindcss/oxide@npm:4.1.12" + dependencies: + "@tailwindcss/oxide-android-arm64": "npm:4.1.12" + "@tailwindcss/oxide-darwin-arm64": "npm:4.1.12" + "@tailwindcss/oxide-darwin-x64": "npm:4.1.12" + "@tailwindcss/oxide-freebsd-x64": "npm:4.1.12" + "@tailwindcss/oxide-linux-arm-gnueabihf": "npm:4.1.12" + "@tailwindcss/oxide-linux-arm64-gnu": "npm:4.1.12" + "@tailwindcss/oxide-linux-arm64-musl": "npm:4.1.12" + "@tailwindcss/oxide-linux-x64-gnu": "npm:4.1.12" + "@tailwindcss/oxide-linux-x64-musl": "npm:4.1.12" + "@tailwindcss/oxide-wasm32-wasi": "npm:4.1.12" + "@tailwindcss/oxide-win32-arm64-msvc": "npm:4.1.12" + "@tailwindcss/oxide-win32-x64-msvc": "npm:4.1.12" + detect-libc: "npm:^2.0.4" + tar: "npm:^7.4.3" + dependenciesMeta: + "@tailwindcss/oxide-android-arm64": + optional: true + "@tailwindcss/oxide-darwin-arm64": + optional: true + "@tailwindcss/oxide-darwin-x64": + optional: true + "@tailwindcss/oxide-freebsd-x64": + optional: true + "@tailwindcss/oxide-linux-arm-gnueabihf": + optional: true + "@tailwindcss/oxide-linux-arm64-gnu": + optional: true + "@tailwindcss/oxide-linux-arm64-musl": + optional: true + "@tailwindcss/oxide-linux-x64-gnu": + optional: true + "@tailwindcss/oxide-linux-x64-musl": + optional: true + "@tailwindcss/oxide-wasm32-wasi": + optional: true + "@tailwindcss/oxide-win32-arm64-msvc": + optional: true + "@tailwindcss/oxide-win32-x64-msvc": + optional: true + checksum: 10c0/30ea0c63e2e024636c607c37fadd9a093168d39ffa816f8113183a085595443d533bfb1a62d8f315800b07f5f8e88fd303b4242505cc65d0cfd622ffd50abbe3 + languageName: node + linkType: hard + "@tailwindcss/postcss@npm:^4.1.11": version: 4.1.11 resolution: "@tailwindcss/postcss@npm:4.1.11" @@ -3720,6 +3883,19 @@ __metadata: languageName: node linkType: hard +"@tailwindcss/postcss@npm:^4.1.12": + version: 4.1.12 + resolution: "@tailwindcss/postcss@npm:4.1.12" + dependencies: + "@alloc/quick-lru": "npm:^5.2.0" + "@tailwindcss/node": "npm:4.1.12" + "@tailwindcss/oxide": "npm:4.1.12" + postcss: "npm:^8.4.41" + tailwindcss: "npm:4.1.12" + checksum: 10c0/25f6229bca22bb20513bb75896ff7d195052380a72cb534691860daeca5d5e3a9b80dc66ceb74998f7614293bf0a62c10d85d4ba5d39b6d820faafec9ab1d134 + languageName: node + linkType: hard + "@testing-library/react-native@npm:^13.2.0": version: 13.2.0 resolution: "@testing-library/react-native@npm:13.2.0" @@ -5947,6 +6123,16 @@ __metadata: languageName: node linkType: hard +"enhanced-resolve@npm:^5.18.3": + version: 5.18.3 + resolution: "enhanced-resolve@npm:5.18.3" + dependencies: + graceful-fs: "npm:^4.2.4" + tapable: "npm:^2.2.0" + checksum: 10c0/d413c23c2d494e4c1c9c9ac7d60b812083dc6d446699ed495e69c920988af0a3c66bf3f8d0e7a45cb1686c2d4c1df9f4e7352d973f5b56fe63d8d711dd0ccc54 + languageName: node + linkType: hard + "entities@npm:^6.0.0": version: 6.0.1 resolution: "entities@npm:6.0.1" @@ -8548,6 +8734,15 @@ __metadata: languageName: node linkType: hard +"jiti@npm:^2.5.1": + version: 2.5.1 + resolution: "jiti@npm:2.5.1" + bin: + jiti: lib/jiti-cli.mjs + checksum: 10c0/f0a38d7d8842cb35ffe883038166aa2d52ffd21f1a4fc839ae4076ea7301c22a1f11373f8fc52e2667de7acde8f3e092835620dd6f72a0fbe9296b268b0874bb + languageName: node + linkType: hard + "js-tokens@npm:^3.0.0 || ^4.0.0, js-tokens@npm:^4.0.0": version: 4.0.0 resolution: "js-tokens@npm:4.0.0" @@ -10679,7 +10874,7 @@ __metadata: languageName: node linkType: hard -"postcss@npm:^8.4.41": +"postcss@npm:^8.4.41, postcss@npm:^8.5.6": version: 8.5.6 resolution: "postcss@npm:8.5.6" dependencies: @@ -11039,6 +11234,7 @@ __metadata: "@commitlint/config-conventional": "npm:^19.8.1" "@eslint/js": "npm:^9.30.1" "@ianvs/prettier-plugin-sort-imports": "npm:^4.4.2" + "@tailwindcss/postcss": "npm:^4.1.12" "@testing-library/react-native": "npm:^13.2.0" "@tsconfig/react-native": "npm:^3.0.6" "@types/babel__core": "npm:^7" @@ -11065,6 +11261,7 @@ __metadata: lefthook: "npm:^1.12.2" lightningcss: "npm:^1.30.1" metro-runtime: "npm:^0.83.0" + postcss: "npm:^8.5.6" prettier: "npm:^3.6.2" react: "npm:19.1.0" react-native: "npm:0.80.1" @@ -12247,6 +12444,13 @@ __metadata: languageName: node linkType: hard +"tailwindcss@npm:4.1.12": + version: 4.1.12 + resolution: "tailwindcss@npm:4.1.12" + checksum: 10c0/0e43375d8de91e1c97a60ed7855f1bf02d5cac61a909439afd54462604862ee71706d812c0447a639f2ef98051a8817840b3df6847c7a1ed015f7a910240ffef + languageName: node + linkType: hard + "tapable@npm:^2.2.0": version: 2.2.2 resolution: "tapable@npm:2.2.2"