diff --git a/example/src/App.tsx b/example/src/App.tsx
index 23149e7..4cfb29e 100644
--- a/example/src/App.tsx
+++ b/example/src/App.tsx
@@ -6,7 +6,7 @@ export default function App() {
return (
<>
- Test Component
+ Test Component
>
);
diff --git a/src/compiler/compiler.types.ts b/src/compiler/compiler.types.ts
index 137c45e..cbfd25c 100644
--- a/src/compiler/compiler.types.ts
+++ b/src/compiler/compiler.types.ts
@@ -1,4 +1,5 @@
/* eslint-disable */
+import type { Debugger } from "debug";
import type {
AnimationDirection,
AnimationFillMode,
@@ -14,7 +15,7 @@ export interface CompilerOptions {
selectorPrefix?: string;
stylesheetOrder?: number;
features?: FeatureFlagRecord;
- logger?: (message: string) => void;
+ logger?: (message: string) => void | Debugger;
/** Strip unused variables declarations. Defaults: false */
stripUnusedVariables?: boolean;
/** @internal */
diff --git a/src/compiler/declarations.ts b/src/compiler/declarations.ts
index b9d7740..79df06e 100644
--- a/src/compiler/declarations.ts
+++ b/src/compiler/declarations.ts
@@ -974,6 +974,8 @@ export function parseUnparsed(
case "hsla":
case "linear-gradient":
case "radial-gradient":
+ case "cubic-bezier":
+ case "steps":
return unparsedFunction(tokenOrValue, builder);
case "hairlineWidth":
return [{}, tokenOrValue.value.name, []];
@@ -999,31 +1001,29 @@ export function parseUnparsed(
case "token":
switch (tokenOrValue.value.type) {
case "string":
- case "number":
case "ident": {
const value = tokenOrValue.value.value;
- if (typeof value === "string") {
- if (!allowAuto && value === "auto") {
- builder.addWarning("value", value);
- return;
- }
+ if (!allowAuto && value === "auto") {
+ builder.addWarning("value", value);
+ return;
+ }
- if (value === "inherit") {
- builder.addWarning("value", value);
- return;
- }
+ if (value === "inherit") {
+ builder.addWarning("value", value);
+ return;
+ }
- if (value === "true") {
- return true;
- } else if (value === "false") {
- return false;
- } else {
- return value;
- }
+ if (value === "true") {
+ return true;
+ } else if (value === "false") {
+ return false;
} else {
return value;
}
}
+ case "number": {
+ return round(tokenOrValue.value.value);
+ }
case "function":
builder.addWarning("value", tokenOrValue.value.value);
return;
diff --git a/src/compiler/keyframes.ts b/src/compiler/keyframes.ts
index a999207..1c719e2 100644
--- a/src/compiler/keyframes.ts
+++ b/src/compiler/keyframes.ts
@@ -53,7 +53,7 @@ export function extractKeyFrames(
switch (selector.type) {
case "percentage":
return frame.selectors.length > 1
- ? `${selector.value}%`
+ ? `${selector.value * 100}%`
: selector.value;
case "from":
case "to":
diff --git a/src/compiler/stylesheet.ts b/src/compiler/stylesheet.ts
index 86a82a5..64280b2 100644
--- a/src/compiler/stylesheet.ts
+++ b/src/compiler/stylesheet.ts
@@ -196,6 +196,9 @@ export class StylesheetBuilder {
const [delayed, usesVariables] = postProcessStyleFunction(value);
this.rule.d ??= [];
+ if (value[1] === "@animation") {
+ this.rule.a ??= true;
+ }
if (usesVariables) {
this.rule.dv = 1;
@@ -209,6 +212,10 @@ export class StylesheetBuilder {
delayed || usesVariables,
);
} else {
+ if (property.startsWith("animation-")) {
+ this.rule.a ??= true;
+ }
+
this.rule.d ??= [];
this.pushDescriptor(property, value, this.rule.d);
}
@@ -370,7 +377,7 @@ export class StylesheetBuilder {
}
this.animationDeclarations = [];
- this.staticDeclarations = {};
+ this.staticDeclarations = undefined;
this.animationFrames.push([progress, this.animationDeclarations]);
}
}
diff --git a/src/runtime/native/conditions/guards.ts b/src/runtime/native/conditions/guards.ts
index b926ed3..558ec70 100644
--- a/src/runtime/native/conditions/guards.ts
+++ b/src/runtime/native/conditions/guards.ts
@@ -40,10 +40,6 @@ export function testGuards(
break;
}
- // if (result) {
- // console.log(`Guard ${guard[0]}:${guard[1]} failed`);
- // }
-
return result;
});
}
diff --git a/src/runtime/native/react/rules.ts b/src/runtime/native/react/rules.ts
index ef21d3b..63f5507 100644
--- a/src/runtime/native/react/rules.ts
+++ b/src/runtime/native/react/rules.ts
@@ -221,7 +221,7 @@ export function updateRules(
}
// Generate a StyleObservable for this unique set of rules / variables
- const stylesObs = stylesFamily(generateHash(state, rules), rules);
+ const stylesObs = stylesFamily(generateStateHash(state, rules), rules);
// Get the guards without subscribing to the observable
// We will subscribe within the render using the StyleEffect
@@ -277,21 +277,12 @@ function pushInlineRule(
let hashKeyCount = 0;
const hashKeyFamily = weakFamily(() => hashKeyCount++);
-/**
- * Quickly generate a unique hash for a set of numbers.
- * This is not a cryptographic hash, but it is fast and has a low chance of collision.
- */
-const MOD = 9007199254740871; // Largest prime within safe integer range 2^53
-const PRIME = 31; // A smaller prime for mixing
-export function generateHash(
+export function generateStateHash(
state: ComponentState,
iterableKeys?: Iterable,
variables?: WeakKey,
inlineVars?: Set,
): string {
- let hash = 0;
- let product = 1; // Used for mixing to enhance uniqueness
-
if (!iterableKeys) {
return "";
}
@@ -306,6 +297,19 @@ export function generateHash(
keys.push(...inlineVars);
}
+ return generateHash(keys);
+}
+
+/**
+ * Quickly generate a unique hash for a set of numbers.
+ * This is not a cryptographic hash, but it is fast and has a low chance of collision.
+ */
+const MOD = 9007199254740871; // Largest prime within safe integer range 2^53
+const PRIME = 31; // A smaller prime for mixing
+export function generateHash(keys: WeakKey[]): string {
+ let hash = 0;
+ let product = 1; // Used for mixing to enhance uniqueness
+
for (const key of keys) {
if (!key) continue; // Skip if key is undefined
diff --git a/src/runtime/native/styles/animation.ts b/src/runtime/native/styles/animation.ts
index a985237..d49e307 100644
--- a/src/runtime/native/styles/animation.ts
+++ b/src/runtime/native/styles/animation.ts
@@ -1,26 +1,37 @@
/* eslint-disable */
import type { ComponentType } from "react";
-import type { StyleDescriptor } from "../../../compiler";
import { StyleCollection } from "../injection";
-import { observable, weakFamily, type Getter } from "../reactivity";
-import type { SimpleResolveValue, StyleFunctionResolver } from "./resolve";
+import { weakFamily } from "../reactivity";
+import type { StyleFunctionResolver } from "./resolve";
import { shorthandHandler } from "./shorthand";
-const name = ["n", "string", "none"] as const;
-const delay = ["de", "number", 0] as const;
-const duration = ["du", "number", 0] as const;
-const fill = ["f", ["none", "forwards", "backwards", "both"], "none"] as const;
-const iteration = ["i", "number", 1] as const;
-const playState = ["p", ["running", "paused"], "running"] as const;
+const name = ["animationName", "string", "none"] as const;
+const delay = ["animationDelay", "number", 0] as const;
+const duration = ["animationDuration", "number", 0] as const;
+const iteration = [
+ "animationIterationCount",
+ ["number", "infinite"],
+ 1,
+] as const;
+const fill = [
+ "animationFillMode",
+ ["none", "forwards", "backwards", "both"],
+ "none",
+] as const;
+const playState = [
+ "animationPlayState",
+ ["running", "paused"],
+ "running",
+] as const;
const direction = [
- "di",
+ "animationDirection",
["normal", "reverse", "alternate", "alternate-reverse"],
"normal",
] as const;
-const easing = [
- "e",
- ["linear", "ease", "ease-in", "ease-out", "ease-in-out"],
+const timingFunction = [
+ "animationTimingFunction",
+ ["linear", "ease", "ease-in", "ease-out", "ease-in-out", "object"],
"ease",
] as const;
@@ -29,100 +40,23 @@ export const animationShorthand = shorthandHandler(
[name],
[duration, name],
[name, duration],
+ [name, duration, iteration],
+ [name, duration, timingFunction, iteration],
[duration, delay, name],
[duration, delay, iteration, name],
- [duration, delay, iteration, easing, name],
- [name, duration, easing, delay, iteration, fill],
+ [duration, delay, iteration, timingFunction, name],
+ [name, duration, timingFunction, delay, iteration, fill],
+ ],
+ [
+ name,
+ delay,
+ direction,
+ duration,
+ fill,
+ iteration,
+ playState,
+ timingFunction,
],
- [name, delay, direction, duration, fill, iteration, playState, easing],
-);
-
-export const animation: StyleFunctionResolver = (
- resolveValue,
- value,
- get,
- { inheritedVariables },
-) => {
- const name = resolveValue(value[2]);
-
- /**
- * Get a stable reference to the StyleProp observer.
- * We can use that as a WeakKey
- */
- return get(
- animationFamily(get, {
- name,
- resolveValue,
- inheritedVariables,
- }),
- );
-};
-
-type AnimationFamilyOptions = {
- name: StyleDescriptor;
- resolveValue: SimpleResolveValue;
- inheritedVariables: any;
-};
-
-const animationFamily = weakFamily(
- (_: Getter, options: AnimationFamilyOptions) => {
- const { name: nameDescriptor, resolveValue } = options;
-
- return observable((get) => {
- const names = resolveValue(nameDescriptor);
-
- if (!Array.isArray(names)) {
- return;
- }
-
- return names.map((name: string) => {
- const keyframes = get(StyleCollection.keyframes(name));
-
- const animation: Record = {};
-
- for (const [progress, declarations] of keyframes) {
- const result: Record = {};
-
- // This code needs to match calculateProps
- // TODO: Refactor this to use the same code
- for (const declaration of declarations) {
- let target = result;
-
- if (!Array.isArray(declaration)) {
- // Static styles
- Object.assign(target, declaration);
- } else {
- // Dynamic styles
- let value: any = declaration[0];
- let propPath = declaration[1];
- let prop: string | undefined = "";
-
- if (typeof propPath === "string") {
- prop = propPath;
- } else {
- prop = propPath[0];
-
- for (
- let i = 0;
- i < propPath.length - 2 && typeof prop === "string";
- i++
- ) {
- target = target[prop] ??= {};
- prop = propPath[i + 1];
- }
- }
-
- value = resolveValue(value);
- }
- }
-
- animation[progress] = result;
- }
-
- return animation;
- });
- });
- },
);
export const animatedComponentFamily = weakFamily(
@@ -140,3 +74,96 @@ export const animatedComponentFamily = weakFamily(
return createAnimatedComponent(component);
},
);
+
+export const animation: StyleFunctionResolver = (
+ resolveValue,
+ value,
+ get,
+ options,
+) => {
+ const animationShortHandTuples: [unknown, string][] | undefined =
+ animationShorthand(resolveValue, value, get, options);
+
+ if (!animationShortHandTuples) {
+ return;
+ }
+
+ const nameTuple = animationShortHandTuples.find(
+ (tuple) => tuple[1] === "animationName",
+ );
+
+ const name = nameTuple?.[0];
+
+ if (!nameTuple || typeof name !== "string") {
+ return;
+ }
+
+ const keyframes = get(StyleCollection.keyframes(name));
+
+ const animation: Record = {};
+ for (const [progress, declarations] of keyframes) {
+ animation[progress] ??= {};
+
+ const props = options.calculateProps?.(
+ get,
+ // Cast this into a StyleRule[]
+ [{ s: [0], d: declarations }],
+ options.renderGuards,
+ options.inheritedVariables,
+ options.inlineVariables,
+ );
+
+ if (!props) {
+ continue;
+ }
+
+ if (props.normal) {
+ Object.assign(animation[progress], props.normal);
+ }
+ if (props.important) {
+ Object.assign(animation[progress], props.important);
+ }
+
+ animation[progress] = animation[progress].style;
+ }
+
+ nameTuple[0] = animation;
+
+ return animationShortHandTuples;
+};
+
+const advancedTimingFunctions: Record<
+ string,
+ () => (...args: any[]) => unknown
+> = {
+ "cubic-bezier": () => {
+ return (
+ require("react-native-reanimated") as typeof import("react-native-reanimated")
+ ).cubicBezier;
+ },
+ "steps": () => {
+ return (
+ require("react-native-reanimated") as typeof import("react-native-reanimated")
+ ).steps;
+ },
+};
+
+export const timingFunctionResolver: StyleFunctionResolver = (
+ resolveValue,
+ value,
+) => {
+ const name = value[1];
+ const resolver = advancedTimingFunctions[name];
+
+ if (!resolver) {
+ return;
+ }
+
+ const args: unknown[] = resolveValue(value[2]);
+
+ const fn = resolver();
+
+ const result = fn(...args);
+
+ return result;
+};
diff --git a/src/runtime/native/styles/calculate-props.ts b/src/runtime/native/styles/calculate-props.ts
new file mode 100644
index 0000000..96b6a2e
--- /dev/null
+++ b/src/runtime/native/styles/calculate-props.ts
@@ -0,0 +1,165 @@
+/* eslint-disable */
+import type {
+ InlineVariable,
+ StyleDeclaration,
+ StyleRule,
+} from "../../../compiler";
+import { applyValue, Specificity as S } from "../../utils";
+import type { RenderGuard } from "../conditions/guards";
+import {
+ VAR_SYMBOL,
+ type Getter,
+ type VariableContextValue,
+} from "../reactivity";
+import { resolveValue } from "./resolve";
+
+export function calculateProps(
+ get: Getter,
+ rules: (StyleRule | InlineVariable | VariableContextValue)[],
+ guards: RenderGuard[] = [],
+ inheritedVariables: VariableContextValue = {
+ [VAR_SYMBOL]: true,
+ },
+ inlineVariables: InlineVariable = {
+ [VAR_SYMBOL]: "inline",
+ },
+) {
+ let normal: Record | undefined;
+ let important: Record | undefined;
+
+ const delayedStyles: (() => void)[] = [];
+
+ for (const rule of rules) {
+ if (VAR_SYMBOL in rule) {
+ if (typeof rule[VAR_SYMBOL] === "string") {
+ Object.assign(inlineVariables, rule);
+ } else {
+ Object.assign(inheritedVariables, rule);
+ }
+ continue;
+ }
+
+ if (rule.v) {
+ for (const variable of rule.v) {
+ inlineVariables[variable[0]] = variable[1];
+ }
+ }
+
+ if (rule.d) {
+ let topLevelTarget = rule.s?.[S.Important]
+ ? (important ??= {})
+ : (normal ??= {});
+ let target = topLevelTarget;
+
+ const ruleTarget = rule.target || "style";
+
+ if (typeof ruleTarget === "string") {
+ target = target[ruleTarget] ??= {};
+ } else if (ruleTarget) {
+ for (const path of ruleTarget) {
+ target = target[path] ??= {};
+ }
+ }
+
+ applyDeclarations(
+ get,
+ rule.d,
+ inlineVariables,
+ inheritedVariables,
+ delayedStyles,
+ guards,
+ target,
+ topLevelTarget,
+ );
+ }
+ }
+
+ for (const delayedStyle of delayedStyles) {
+ delayedStyle();
+ }
+
+ return {
+ normal,
+ guards,
+ important,
+ };
+}
+
+export function applyDeclarations(
+ get: Getter,
+ declarations: StyleDeclaration[],
+ inlineVariables: InlineVariable,
+ inheritedVariables: VariableContextValue,
+ delayedStyles: (() => void)[] = [],
+ guards: RenderGuard[] = [],
+ target: Record = {},
+ topLevelTarget = target,
+) {
+ for (const declaration of declarations) {
+ if (!Array.isArray(declaration)) {
+ // Static styles
+ Object.assign(target, declaration);
+ } else {
+ // Dynamic styles
+ let value: any = declaration[0];
+ let propPath = declaration[1];
+ let prop = "";
+
+ if (typeof propPath === "string") {
+ if (propPath.startsWith("^")) {
+ propPath = propPath.slice(1);
+ target = topLevelTarget[propPath] ??= {};
+ }
+ prop = propPath;
+ } else {
+ for (prop of propPath) {
+ if (prop.startsWith("^")) {
+ prop = prop.slice(1);
+ target = topLevelTarget[prop] ??= {};
+ } else {
+ target = target[prop] ??= {};
+ }
+ }
+ }
+
+ if (Array.isArray(value)) {
+ const shouldDelay = declaration[2];
+
+ if (shouldDelay) {
+ /**
+ * We need to delay the resolution of this value until after all
+ * styles have been calculated. But another style might override
+ * this value. So we set a placeholder value and only override
+ * if the placeholder is preserved
+ *
+ * This also ensures the props exist, so setValue will properly
+ * mutate the props object and not create a new one
+ */
+ const originalValue = value;
+ value = {};
+ delayedStyles.push(() => {
+ if (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,
+ inheritedVariables,
+ renderGuards: guards,
+ calculateProps,
+ });
+ }
+
+ applyValue(target, prop, value);
+ }
+ }
+ }
+}
diff --git a/src/runtime/native/styles/index.ts b/src/runtime/native/styles/index.ts
index 491f691..e22e2df 100644
--- a/src/runtime/native/styles/index.ts
+++ b/src/runtime/native/styles/index.ts
@@ -1,11 +1,6 @@
/* eslint-disable */
import type { InlineVariable, StyleRule } from "../../../compiler";
-import {
- applyValue,
- Specificity as S,
- specificityCompareFn,
-} from "../../utils";
-import type { RenderGuard } from "../conditions/guards";
+import { specificityCompareFn } from "../../utils";
import { getInteractionHandler } from "../react/interaction";
import type { ComponentState, Config } from "../react/useNativeCss";
import {
@@ -17,10 +12,9 @@ import {
observable,
VAR_SYMBOL,
type Effect,
- type Getter,
type VariableContextValue,
} from "../reactivity";
-import { resolveValue } from "./resolve";
+import { calculateProps } from "./calculate-props";
export const stylesFamily = family(
(
@@ -45,136 +39,6 @@ export const stylesFamily = family(
},
);
-function calculateProps(
- get: Getter,
- rules: Array,
-) {
- let normal: Record | undefined;
- let important: Record | undefined;
-
- const delayedStyles: (() => void)[] = [];
-
- const guards: RenderGuard[] = [];
-
- const inheritedVariables: VariableContextValue = {
- [VAR_SYMBOL]: true,
- };
-
- const inlineVariables: InlineVariable = {
- [VAR_SYMBOL]: "inline",
- };
-
- for (const rule of rules) {
- if (VAR_SYMBOL in rule) {
- if (typeof rule[VAR_SYMBOL] === "string") {
- Object.assign(inlineVariables, rule);
- } else {
- Object.assign(inheritedVariables, rule);
- }
- continue;
- }
-
- if (rule.v) {
- for (const variable of rule.v) {
- inlineVariables[variable[0]] = variable[1];
- }
- }
-
- if (rule.d) {
- let topLevelTarget = rule.s?.[S.Important]
- ? (important ??= {})
- : (normal ??= {});
- let target = topLevelTarget;
-
- const ruleTarget = rule.target || "style";
-
- if (typeof ruleTarget === "string") {
- target = target[ruleTarget] ??= {};
- } else if (ruleTarget) {
- for (const path of ruleTarget) {
- target = target[path] ??= {};
- }
- }
-
- for (const declaration of rule.d) {
- if (!Array.isArray(declaration)) {
- // Static styles
- Object.assign(target, declaration);
- } else {
- // Dynamic styles
- let value: any = declaration[0];
- let propPath = declaration[1];
- let prop = "";
-
- if (typeof propPath === "string") {
- if (propPath.startsWith("^")) {
- propPath = propPath.slice(1);
- target = topLevelTarget[propPath] ??= {};
- }
- prop = propPath;
- } else {
- for (prop of propPath) {
- if (prop.startsWith("^")) {
- prop = prop.slice(1);
- target = topLevelTarget[prop] ??= {};
- } else {
- target = target[prop] ??= {};
- }
- }
- }
-
- if (Array.isArray(value)) {
- const shouldDelay = declaration[2];
-
- if (shouldDelay) {
- /**
- * We need to delay the resolution of this value until after all
- * styles have been calculated. But another style might override
- * this value. So we set a placeholder value and only override
- * if the placeholder is preserved
- *
- * This also ensures the props exist, so setValue will properly
- * mutate the props object and not create a new one
- */
- const originalValue = value;
- value = {};
- delayedStyles.push(() => {
- if (target[prop] === value) {
- delete target[prop];
- value = resolveValue(originalValue, get, {
- inlineVariables,
- inheritedVariables,
- renderGuards: guards,
- });
- applyValue(target, prop, value);
- }
- });
- } else {
- value = resolveValue(value, get, {
- inlineVariables,
- inheritedVariables,
- renderGuards: guards,
- });
- }
-
- applyValue(target, prop, value);
- }
- }
- }
- }
- }
-
- for (const delayedStyle of delayedStyles) {
- delayedStyle();
- }
-
- return {
- normal,
- guards,
- important,
- };
-}
-
export function getStyledProps(
state: ComponentState,
inline: Record | undefined | null,
diff --git a/src/runtime/native/styles/resolve.ts b/src/runtime/native/styles/resolve.ts
index 04288d1..096187e 100644
--- a/src/runtime/native/styles/resolve.ts
+++ b/src/runtime/native/styles/resolve.ts
@@ -6,10 +6,11 @@ import type {
} from "../../../compiler";
import type { RenderGuard } from "../conditions/guards";
import { type Getter, type VariableContextValue } from "../reactivity";
-import { animation } from "./animation";
+import { animation, timingFunctionResolver } from "./animation";
import { border } from "./border";
import { boxShadow } from "./box-shadow";
import { calc } from "./calc";
+import type { calculateProps } from "./calculate-props";
import { transformKeys } from "./defaults";
import {
fontScale,
@@ -37,6 +38,7 @@ export type StyleFunctionResolver = (
) => any;
const shorthands: Record<`@${string}`, StyleFunctionResolver> = {
+ "@animation": animation,
"@textShadow": textShadow,
"@transform": transform,
"@boxShadow": boxShadow,
@@ -55,7 +57,9 @@ const functions: Record = {
fontScale,
pixelSizeForLayoutSize,
roundToNearestPixel,
- animationName: animation,
+ "animationName": animation,
+ "cubic-bezier": timingFunctionResolver,
+ "steps": timingFunctionResolver,
...shorthands,
};
@@ -65,6 +69,8 @@ export type ResolveValueOptions = {
inlineVariables?: InlineVariable | undefined;
renderGuards?: RenderGuard[];
variableHistory?: Set;
+ /** Pass down to perform recursive calculations and avoid circular dependencies */
+ calculateProps?: typeof calculateProps;
};
export function resolveValue(
@@ -94,7 +100,7 @@ export function resolveValue(
}
if (isDescriptorArray(value)) {
- value = value.map((d) => {
+ value = value.flatMap((d) => {
const value = resolveValue(d, get, options);
return value === undefined ? [] : value;
}) as StyleDescriptor[];
diff --git a/src/runtime/native/styles/shorthand.ts b/src/runtime/native/styles/shorthand.ts
index 45be3f3..8976976 100644
--- a/src/runtime/native/styles/shorthand.ts
+++ b/src/runtime/native/styles/shorthand.ts
@@ -8,7 +8,7 @@ type ShorthandType =
| "number"
| "length"
| "color"
- | Readonly;
+ | Readonly<(string | Function)[]>;
type ShorthandRequiredValue =
| readonly [string | readonly string[], ShorthandType]
@@ -46,7 +46,7 @@ export function shorthandHandler(
const value = resolved[index];
if (Array.isArray(type)) {
- return type.includes(value);
+ return type.includes(value) || type.includes(typeof value);
}
switch (type) {