Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/src/content/docs/reference/transforms.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Apply 2D and 3D transformations to views with React Native's transform API. All
```tsx
<View className="scale-0" /> // scale: 0
<View className="scale-50" /> // scale: 0.5
<View className="scale-80" /> // scale: 0.8
<View className="scale-100" /> // scale: 1
<View className="scale-110" /> // scale: 1.1
<View className="scale-150" /> // scale: 1.5
Expand All @@ -22,12 +23,14 @@ Apply 2D and 3D transformations to views with React Native's transform API. All
```tsx
<View className="scale-x-110" /> // scaleX: 1.1
<View className="scale-y-90" /> // scaleY: 0.9
<View className="-scale-x-80" /> // scaleX: -0.8
```

### Arbitrary Values

```tsx
<View className="scale-[1.23]" /> // scale: 1.23
<View className="scale-[80%]" /> // scale: 0.8
<View className="scale-x-[0.5]" /> // scaleX: 0.5
<View className="scale-y-[2.5]" /> // scaleY: 2.5
```
Expand Down
17 changes: 16 additions & 1 deletion src/parser/transforms.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ describe("parseTransform - scale utilities", () => {
expect(parseTransform("scale-0")).toEqual({ transform: [{ scale: 0 }] });
expect(parseTransform("scale-50")).toEqual({ transform: [{ scale: 0.5 }] });
expect(parseTransform("scale-75")).toEqual({ transform: [{ scale: 0.75 }] });
expect(parseTransform("scale-80")).toEqual({ transform: [{ scale: 0.8 }] });
expect(parseTransform("scale-90")).toEqual({ transform: [{ scale: 0.9 }] });
expect(parseTransform("scale-95")).toEqual({ transform: [{ scale: 0.95 }] });
expect(parseTransform("scale-100")).toEqual({ transform: [{ scale: 1 }] });
Expand All @@ -40,38 +41,52 @@ describe("parseTransform - scale utilities", () => {
expect(parseTransform("scale-125")).toEqual({ transform: [{ scale: 1.25 }] });
expect(parseTransform("scale-150")).toEqual({ transform: [{ scale: 1.5 }] });
expect(parseTransform("scale-200")).toEqual({ transform: [{ scale: 2 }] });
expect(parseTransform("scale-102.5")).toEqual({ transform: [{ scale: 1.025 }] });
expect(parseTransform("scale-999")).toEqual({ transform: [{ scale: 9.99 }] });
});

it("should parse scaleX values", () => {
expect(parseTransform("scale-x-0")).toEqual({ transform: [{ scaleX: 0 }] });
expect(parseTransform("scale-x-50")).toEqual({ transform: [{ scaleX: 0.5 }] });
expect(parseTransform("scale-x-80")).toEqual({ transform: [{ scaleX: 0.8 }] });
expect(parseTransform("scale-x-100")).toEqual({ transform: [{ scaleX: 1 }] });
expect(parseTransform("scale-x-150")).toEqual({ transform: [{ scaleX: 1.5 }] });
});

it("should parse scaleY values", () => {
expect(parseTransform("scale-y-0")).toEqual({ transform: [{ scaleY: 0 }] });
expect(parseTransform("scale-y-75")).toEqual({ transform: [{ scaleY: 0.75 }] });
expect(parseTransform("scale-y-80")).toEqual({ transform: [{ scaleY: 0.8 }] });
expect(parseTransform("scale-y-100")).toEqual({ transform: [{ scaleY: 1 }] });
expect(parseTransform("scale-y-125")).toEqual({ transform: [{ scaleY: 1.25 }] });
});

it("should parse arbitrary scale values", () => {
expect(parseTransform("scale-[1.23]")).toEqual({ transform: [{ scale: 1.23 }] });
expect(parseTransform("scale-[80]")).toEqual({ transform: [{ scale: 80 }] });
expect(parseTransform("scale-[0.5]")).toEqual({ transform: [{ scale: 0.5 }] });
expect(parseTransform("scale-[2.5]")).toEqual({ transform: [{ scale: 2.5 }] });
expect(parseTransform("scale-x-[1.5]")).toEqual({ transform: [{ scaleX: 1.5 }] });
expect(parseTransform("scale-y-[0.8]")).toEqual({ transform: [{ scaleY: 0.8 }] });
expect(parseTransform("scale-[80%]")).toEqual({ transform: [{ scale: 0.8 }] });
expect(parseTransform("scale-x-[125%]")).toEqual({ transform: [{ scaleX: 1.25 }] });
expect(parseTransform("scale-y-[80%]")).toEqual({ transform: [{ scaleY: 0.8 }] });
});

it("should parse negative arbitrary scale values", () => {
expect(parseTransform("scale-[-1]")).toEqual({ transform: [{ scale: -1 }] });
expect(parseTransform("scale-x-[-0.5]")).toEqual({ transform: [{ scaleX: -0.5 }] });
});

it("should parse negative numeric scale values", () => {
expect(parseTransform("-scale-80")).toEqual({ transform: [{ scale: -0.8 }] });
expect(parseTransform("-scale-x-125")).toEqual({ transform: [{ scaleX: -1.25 }] });
expect(parseTransform("-scale-y-50")).toEqual({ transform: [{ scaleY: -0.5 }] });
});

it("should return null for invalid scale values", () => {
expect(parseTransform("scale-999")).toBeNull();
expect(parseTransform("scale-invalid")).toBeNull();
expect(parseTransform("scale-10x")).toBeNull();
});
});

Expand Down
92 changes: 49 additions & 43 deletions src/parser/transforms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,21 +59,22 @@ export const PERSPECTIVE_SCALE: Record<string, number> = {
};

/**
* Parse arbitrary scale value: [1.23], [0.5]
* Parse arbitrary scale value: [1.23], [0.5], [80%]
* Returns number for valid scale, null otherwise
*/
function parseArbitraryScale(value: string): number | null {
const scaleMatch = value.match(/^\[(-?\d+(?:\.\d+)?)\]$/);
const scaleMatch = value.match(/^\[(-?\d+(?:\.\d+)?)(%)?\]$/);
if (scaleMatch) {
return parseFloat(scaleMatch[1]);
const parsedValue = Number.parseFloat(scaleMatch[1]);
return scaleMatch[2] === "%" ? parsedValue / 100 : parsedValue;
}

// Unsupported format
if (value.startsWith("[") && value.endsWith("]")) {
/* v8 ignore next 5 */
if (process.env.NODE_ENV !== "production") {
console.warn(
`[react-native-tailwind] Invalid arbitrary scale value: ${value}. Only numbers are supported (e.g., [1.5], [0.75]).`,
`[react-native-tailwind] Invalid arbitrary scale value: ${value}. Only numbers and percentages are supported (e.g., [1.5], [0.75], [80%]).`,
);
}
return null;
Expand All @@ -82,6 +83,29 @@ function parseArbitraryScale(value: string): number | null {
return null;
}

/**
* Parse a Tailwind scale value.
* Named numeric utilities are percentages (scale-80 -> 0.8), while bracketed
* arbitrary numbers are raw React Native multipliers (scale-[1.25] -> 1.25).
*/
function parseScaleValue(value: string): number | null {
const arbitraryScale = parseArbitraryScale(value);
if (arbitraryScale !== null) {
return arbitraryScale;
}

const presetScale = SCALE_MAP[value];
if (presetScale !== undefined) {
return presetScale;
}

if (/^\d+(?:\.\d+)?$/.test(value)) {
return Number.parseFloat(value) / 100;
}

return null;
}

/**
* Parse arbitrary rotation value: [37deg], [-15deg]
* Returns string for valid rotation, null otherwise
Expand Down Expand Up @@ -181,51 +205,33 @@ export function parseTransform(cls: string, customSpacing?: Record<string, numbe
return null;
}

// Scale: scale-{value}
if (cls.startsWith("scale-")) {
const scaleKey = cls.substring(6);

// Arbitrary values: scale-[1.23]
const arbitraryScale = parseArbitraryScale(scaleKey);
if (arbitraryScale !== null) {
return { transform: [{ scale: arbitraryScale }] };
}

const scaleValue = SCALE_MAP[scaleKey];
if (scaleValue !== undefined) {
return { transform: [{ scale: scaleValue }] };
// Scale: scale-{value}, -scale-{value}
const scaleMatch = cls.match(/^(-?)scale-(.+)$/);
if (scaleMatch) {
const [, negativePrefix, scaleKey] = scaleMatch;
const scaleValue = parseScaleValue(scaleKey);
if (scaleValue !== null) {
return { transform: [{ scale: negativePrefix === "-" ? -scaleValue : scaleValue }] };
}
}

// Scale X: scale-x-{value}
if (cls.startsWith("scale-x-")) {
const scaleKey = cls.substring(8);

// Arbitrary values: scale-x-[1.5]
const arbitraryScale = parseArbitraryScale(scaleKey);
if (arbitraryScale !== null) {
return { transform: [{ scaleX: arbitraryScale }] };
}

const scaleValue = SCALE_MAP[scaleKey];
if (scaleValue !== undefined) {
return { transform: [{ scaleX: scaleValue }] };
// Scale X: scale-x-{value}, -scale-x-{value}
const scaleXMatch = cls.match(/^(-?)scale-x-(.+)$/);
if (scaleXMatch) {
const [, negativePrefix, scaleKey] = scaleXMatch;
const scaleValue = parseScaleValue(scaleKey);
if (scaleValue !== null) {
return { transform: [{ scaleX: negativePrefix === "-" ? -scaleValue : scaleValue }] };
}
}

// Scale Y: scale-y-{value}
if (cls.startsWith("scale-y-")) {
const scaleKey = cls.substring(8);

// Arbitrary values: scale-y-[2.5]
const arbitraryScale = parseArbitraryScale(scaleKey);
if (arbitraryScale !== null) {
return { transform: [{ scaleY: arbitraryScale }] };
}

const scaleValue = SCALE_MAP[scaleKey];
if (scaleValue !== undefined) {
return { transform: [{ scaleY: scaleValue }] };
// Scale Y: scale-y-{value}, -scale-y-{value}
const scaleYMatch = cls.match(/^(-?)scale-y-(.+)$/);
if (scaleYMatch) {
const [, negativePrefix, scaleKey] = scaleYMatch;
const scaleValue = parseScaleValue(scaleKey);
if (scaleValue !== null) {
return { transform: [{ scaleY: negativePrefix === "-" ? -scaleValue : scaleValue }] };
}
}

Expand Down
Loading