@kbach/react-native 1.0.0-beta.22 → 1.0.0-beta.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -30,9 +30,14 @@ Rebuild the Android app afterward so Gradle picks up the native module.
30
30
  Works out of the box — no prebuild, no dev client, nothing to configure.
31
31
  Expo Go can't load third-party native code, so this falls back to a pure-JS
32
32
  engine automatically. Covers layout, spacing, border/radius, plain colors,
33
- font-weight, text-transform/decoration, line-height, letter-spacing, and
34
- shadows. Doesn't cover grid, transforms, filters, gradients, or DOM-only
35
- typography — same scope native itself excludes.
33
+ font-weight, text-transform/decoration, line-height, letter-spacing,
34
+ shadows, and transforms (`translate-x`/`translate-y`, `scale`/`scale-x`/
35
+ `scale-y`, `rotate`/`rotate-x`/`rotate-y`/`rotate-z`, `skew-x`/`skew-y`,
36
+ `transform-none`, `backface-visible`/`backface-hidden`). Doesn't cover grid,
37
+ filters, gradients, DOM-only typography, or the rest of the transform family
38
+ (`translate-z`/`scale-z`, `perspective`/`perspective-origin`/`origin`,
39
+ `transform-gpu`/`transform-cpu` — no RN equivalent for any of these) — same
40
+ scope native itself excludes.
36
41
 
37
42
  ### Expo (dev client / prebuild)
38
43
 
package/dist/index.d.ts CHANGED
@@ -7,7 +7,7 @@ import 'react';
7
7
  type StyleValue = string | number | {
8
8
  width: number;
9
9
  height: number;
10
- };
10
+ } | Array<Record<string, string | number>>;
11
11
  interface StyleObject {
12
12
  [key: string]: StyleValue;
13
13
  }
package/dist/index.js CHANGED
@@ -177,12 +177,19 @@ var VALUE_PREFIXES = [
177
177
  "justify-self-",
178
178
  "scale-x-",
179
179
  "scale-y-",
180
+ "scale-z-",
180
181
  "scale-",
182
+ "rotate-x-",
183
+ "rotate-y-",
184
+ "rotate-z-",
181
185
  "rotate-",
182
186
  "translate-x-",
183
187
  "translate-y-",
188
+ "translate-z-",
184
189
  "skew-x-",
185
190
  "skew-y-",
191
+ "perspective-origin-",
192
+ "perspective-",
186
193
  "origin-",
187
194
  "blur-",
188
195
  "brightness-",
@@ -1202,6 +1209,77 @@ function nativeShadowDeclarations(parsed) {
1202
1209
  ];
1203
1210
  }
1204
1211
 
1212
+ // src/jsEngine/resolvers/transform.ts
1213
+ function scaleFactor(parsed) {
1214
+ if (parsed.value === null) return null;
1215
+ const n = Number(parsed.value);
1216
+ if (!Number.isFinite(n)) return null;
1217
+ return parsed.isArbitrary ? n : n / 100;
1218
+ }
1219
+ function angle(parsed) {
1220
+ if (parsed.value === null) return null;
1221
+ if (parsed.isArbitrary) return parsed.value;
1222
+ const n = Number(parsed.value);
1223
+ if (!Number.isFinite(n)) return null;
1224
+ return `${parsed.negative ? -n : n}deg`;
1225
+ }
1226
+ function resolveNative(parsed, theme) {
1227
+ switch (parsed.utility) {
1228
+ case "transform-none":
1229
+ return [decl("transform-op-none", "1")];
1230
+ case "backface-visible":
1231
+ return [decl("backface-visibility", "visible")];
1232
+ case "backface-hidden":
1233
+ return [decl("backface-visibility", "hidden")];
1234
+ case "translate-x": {
1235
+ const v = resolveNegatableLength(theme, parsed);
1236
+ return v === null ? null : [decl("transform-op-translate-x", v)];
1237
+ }
1238
+ case "translate-y": {
1239
+ const v = resolveNegatableLength(theme, parsed);
1240
+ return v === null ? null : [decl("transform-op-translate-y", v)];
1241
+ }
1242
+ case "scale": {
1243
+ const n = scaleFactor(parsed);
1244
+ return n === null ? null : [decl("transform-op-scale-x", `${n}`), decl("transform-op-scale-y", `${n}`)];
1245
+ }
1246
+ case "scale-x": {
1247
+ const n = scaleFactor(parsed);
1248
+ return n === null ? null : [decl("transform-op-scale-x", `${n}`)];
1249
+ }
1250
+ case "scale-y": {
1251
+ const n = scaleFactor(parsed);
1252
+ return n === null ? null : [decl("transform-op-scale-y", `${n}`)];
1253
+ }
1254
+ case "rotate": {
1255
+ const deg = angle(parsed);
1256
+ return deg === null ? null : [decl("transform-op-rotate", deg)];
1257
+ }
1258
+ case "rotate-x": {
1259
+ const deg = angle(parsed);
1260
+ return deg === null ? null : [decl("transform-op-rotate-x", deg)];
1261
+ }
1262
+ case "rotate-y": {
1263
+ const deg = angle(parsed);
1264
+ return deg === null ? null : [decl("transform-op-rotate-y", deg)];
1265
+ }
1266
+ case "rotate-z": {
1267
+ const deg = angle(parsed);
1268
+ return deg === null ? null : [decl("transform-op-rotate-z", deg)];
1269
+ }
1270
+ case "skew-x": {
1271
+ const deg = angle(parsed);
1272
+ return deg === null ? null : [decl("transform-op-skew-x", deg)];
1273
+ }
1274
+ case "skew-y": {
1275
+ const deg = angle(parsed);
1276
+ return deg === null ? null : [decl("transform-op-skew-y", deg)];
1277
+ }
1278
+ default:
1279
+ return null;
1280
+ }
1281
+ }
1282
+
1205
1283
  // src/jsEngine/resolveUtilityNative.ts
1206
1284
  function firstFontName(stack) {
1207
1285
  const first = stack.split(",")[0] ?? stack;
@@ -1283,7 +1361,7 @@ function resolveOpacityNative(parsed) {
1283
1361
  return v === null ? null : [decl("opacity", v)];
1284
1362
  }
1285
1363
  function resolveUtilityNative(parsed, theme) {
1286
- return resolveFlex(parsed, false) ?? resolve(parsed, theme) ?? resolve2(parsed, theme) ?? resolve3(parsed, theme) ?? resolveColorNative(parsed, theme) ?? resolveLeadingTrackingNative(parsed) ?? resolveTypographyNative(parsed, theme) ?? resolveOpacityNative(parsed) ?? nativeShadowDeclarations(parsed);
1364
+ return resolveFlex(parsed, false) ?? resolve(parsed, theme) ?? resolve2(parsed, theme) ?? resolve3(parsed, theme) ?? resolveColorNative(parsed, theme) ?? resolveLeadingTrackingNative(parsed) ?? resolveTypographyNative(parsed, theme) ?? resolveOpacityNative(parsed) ?? nativeShadowDeclarations(parsed) ?? resolveNative(parsed, theme);
1287
1365
  }
1288
1366
 
1289
1367
  // src/jsEngine/calc.ts
@@ -1501,8 +1579,32 @@ var NUMERIC_LENGTH_PROPS = /* @__PURE__ */ new Set([
1501
1579
  "border-bottom-right-radius",
1502
1580
  "border-bottom-left-radius",
1503
1581
  "outline-width",
1504
- "outline-offset"
1582
+ "outline-offset",
1583
+ // The transform ops whose RN value is a plain JS number — translate takes
1584
+ // a length (px/percentage, same as top/left/etc. above) and scale takes a
1585
+ // bare decimal factor (parsed by the same "raw value" fallback branch
1586
+ // below). transform-op-rotate*/transform-op-skew* stay OUT of this set on
1587
+ // purpose, since RN wants those as literal "45deg"-shaped strings, not
1588
+ // numbers. See the transform-ops accumulator below for how these markers
1589
+ // get collected into the final array.
1590
+ "transform-op-translate-x",
1591
+ "transform-op-translate-y",
1592
+ "transform-op-scale-x",
1593
+ "transform-op-scale-y"
1505
1594
  ]);
1595
+ var TRANSFORM_OP_KEYS = /* @__PURE__ */ new Map([
1596
+ ["transform-op-translate-x", "translateX"],
1597
+ ["transform-op-translate-y", "translateY"],
1598
+ ["transform-op-rotate", "rotate"],
1599
+ ["transform-op-rotate-x", "rotateX"],
1600
+ ["transform-op-rotate-y", "rotateY"],
1601
+ ["transform-op-rotate-z", "rotateZ"],
1602
+ ["transform-op-skew-x", "skewX"],
1603
+ ["transform-op-skew-y", "skewY"],
1604
+ ["transform-op-scale-x", "scaleX"],
1605
+ ["transform-op-scale-y", "scaleY"]
1606
+ ]);
1607
+ var TRANSFORM_OP_ORDER = ["translateX", "translateY", "rotate", "rotateX", "rotateY", "rotateZ", "skewX", "skewY", "scaleX", "scaleY"];
1506
1608
  function isPlainPercentage(value) {
1507
1609
  if (!value.endsWith("%")) return false;
1508
1610
  let digits = value.slice(0, -1);
@@ -1539,6 +1641,7 @@ function resolveStyleJsWithWarnings(classString, theme, colorScheme, pressed, wi
1539
1641
  const style = {};
1540
1642
  const warnings = [];
1541
1643
  let shadowOffset = null;
1644
+ const transformOps = /* @__PURE__ */ new Map();
1542
1645
  for (const token of classString.split(/\s+/).filter(Boolean)) {
1543
1646
  const parsed = parseClass(token);
1544
1647
  const allModifiersHold = parsed.modifiers.every(
@@ -1558,6 +1661,17 @@ function resolveStyleJsWithWarnings(classString, theme, colorScheme, pressed, wi
1558
1661
  shadowOffset[d.property === "shadow-offset-x" ? "width" : "height"] = n;
1559
1662
  continue;
1560
1663
  }
1664
+ if (d.property === "transform-op-none") {
1665
+ transformOps.clear();
1666
+ continue;
1667
+ }
1668
+ const opKey = TRANSFORM_OP_KEYS.get(d.property);
1669
+ if (opKey !== void 0) {
1670
+ const { value: value2, warning: warning2 } = rnStyleValue(d.property, d.value);
1671
+ if (warning2 !== null) warnings.push(warning2);
1672
+ if (value2 !== null) transformOps.set(opKey, value2);
1673
+ continue;
1674
+ }
1561
1675
  const { value, warning } = rnStyleValue(d.property, d.value);
1562
1676
  if (warning !== null) warnings.push(warning);
1563
1677
  if (value !== null) style[kebabToCamel(d.property)] = value;
@@ -1566,6 +1680,11 @@ function resolveStyleJsWithWarnings(classString, theme, colorScheme, pressed, wi
1566
1680
  if (shadowOffset !== null) {
1567
1681
  style.shadowOffset = { width: shadowOffset.width ?? 0, height: shadowOffset.height ?? 0 };
1568
1682
  }
1683
+ if (transformOps.size > 0) {
1684
+ style.transform = TRANSFORM_OP_ORDER.filter((key) => transformOps.has(key)).map((key) => ({
1685
+ [key]: transformOps.get(key)
1686
+ }));
1687
+ }
1569
1688
  return { style, warnings };
1570
1689
  }
1571
1690
 
@@ -375,12 +375,19 @@ var VALUE_PREFIXES = [
375
375
  "justify-self-",
376
376
  "scale-x-",
377
377
  "scale-y-",
378
+ "scale-z-",
378
379
  "scale-",
380
+ "rotate-x-",
381
+ "rotate-y-",
382
+ "rotate-z-",
379
383
  "rotate-",
380
384
  "translate-x-",
381
385
  "translate-y-",
386
+ "translate-z-",
382
387
  "skew-x-",
383
388
  "skew-y-",
389
+ "perspective-origin-",
390
+ "perspective-",
384
391
  "origin-",
385
392
  "blur-",
386
393
  "brightness-",
@@ -581,11 +588,18 @@ function ReactiveElement({
581
588
  }) {
582
589
  (0, import_react.useSyncExternalStore)(import_darkModeStore.subscribeGlobalDarkMode, import_darkModeStore.getGlobalDarkMode);
583
590
  (0, import_react.useSyncExternalStore)(import_dynamicTokens.subscribeDynamicTokens, import_dynamicTokens.getDynamicTokensVersion);
584
- (0, import_react_native.useWindowDimensions)();
591
+ const windowSize = (0, import_react_native.useWindowDimensions)();
585
592
  const [measuredBasis, setMeasuredBasis] = (0, import_react.useState)(null);
593
+ const lastWindowSizeRef = (0, import_react.useRef)(windowSize);
594
+ if (lastWindowSizeRef.current.width !== windowSize.width || lastWindowSizeRef.current.height !== windowSize.height) {
595
+ lastWindowSizeRef.current = windowSize;
596
+ if (measuredBasis !== null) {
597
+ setMeasuredBasis(null);
598
+ }
599
+ }
586
600
  const measure = (0, import_react.useCallback)((e) => {
587
601
  const { width: w, height: h } = e.nativeEvent.layout;
588
- setMeasuredBasis((prev) => prev && prev.width === w && prev.height === h ? prev : { width: w, height: h });
602
+ setMeasuredBasis((prev) => prev === null ? { width: w, height: h } : prev);
589
603
  }, []);
590
604
  const [isHovered, setIsHovered] = (0, import_react.useState)(false);
591
605
  const [isFocused, setIsFocused] = (0, import_react.useState)(false);
@@ -1583,6 +1597,77 @@ function nativeShadowDeclarations(parsed) {
1583
1597
  ];
1584
1598
  }
1585
1599
 
1600
+ // src/jsEngine/resolvers/transform.ts
1601
+ function scaleFactor(parsed) {
1602
+ if (parsed.value === null) return null;
1603
+ const n = Number(parsed.value);
1604
+ if (!Number.isFinite(n)) return null;
1605
+ return parsed.isArbitrary ? n : n / 100;
1606
+ }
1607
+ function angle(parsed) {
1608
+ if (parsed.value === null) return null;
1609
+ if (parsed.isArbitrary) return parsed.value;
1610
+ const n = Number(parsed.value);
1611
+ if (!Number.isFinite(n)) return null;
1612
+ return `${parsed.negative ? -n : n}deg`;
1613
+ }
1614
+ function resolveNative(parsed, theme) {
1615
+ switch (parsed.utility) {
1616
+ case "transform-none":
1617
+ return [decl("transform-op-none", "1")];
1618
+ case "backface-visible":
1619
+ return [decl("backface-visibility", "visible")];
1620
+ case "backface-hidden":
1621
+ return [decl("backface-visibility", "hidden")];
1622
+ case "translate-x": {
1623
+ const v = resolveNegatableLength(theme, parsed);
1624
+ return v === null ? null : [decl("transform-op-translate-x", v)];
1625
+ }
1626
+ case "translate-y": {
1627
+ const v = resolveNegatableLength(theme, parsed);
1628
+ return v === null ? null : [decl("transform-op-translate-y", v)];
1629
+ }
1630
+ case "scale": {
1631
+ const n = scaleFactor(parsed);
1632
+ return n === null ? null : [decl("transform-op-scale-x", `${n}`), decl("transform-op-scale-y", `${n}`)];
1633
+ }
1634
+ case "scale-x": {
1635
+ const n = scaleFactor(parsed);
1636
+ return n === null ? null : [decl("transform-op-scale-x", `${n}`)];
1637
+ }
1638
+ case "scale-y": {
1639
+ const n = scaleFactor(parsed);
1640
+ return n === null ? null : [decl("transform-op-scale-y", `${n}`)];
1641
+ }
1642
+ case "rotate": {
1643
+ const deg = angle(parsed);
1644
+ return deg === null ? null : [decl("transform-op-rotate", deg)];
1645
+ }
1646
+ case "rotate-x": {
1647
+ const deg = angle(parsed);
1648
+ return deg === null ? null : [decl("transform-op-rotate-x", deg)];
1649
+ }
1650
+ case "rotate-y": {
1651
+ const deg = angle(parsed);
1652
+ return deg === null ? null : [decl("transform-op-rotate-y", deg)];
1653
+ }
1654
+ case "rotate-z": {
1655
+ const deg = angle(parsed);
1656
+ return deg === null ? null : [decl("transform-op-rotate-z", deg)];
1657
+ }
1658
+ case "skew-x": {
1659
+ const deg = angle(parsed);
1660
+ return deg === null ? null : [decl("transform-op-skew-x", deg)];
1661
+ }
1662
+ case "skew-y": {
1663
+ const deg = angle(parsed);
1664
+ return deg === null ? null : [decl("transform-op-skew-y", deg)];
1665
+ }
1666
+ default:
1667
+ return null;
1668
+ }
1669
+ }
1670
+
1586
1671
  // src/jsEngine/resolveUtilityNative.ts
1587
1672
  function firstFontName(stack) {
1588
1673
  const first = stack.split(",")[0] ?? stack;
@@ -1664,7 +1749,7 @@ function resolveOpacityNative(parsed) {
1664
1749
  return v === null ? null : [decl("opacity", v)];
1665
1750
  }
1666
1751
  function resolveUtilityNative(parsed, theme) {
1667
- return resolveFlex(parsed, false) ?? resolve(parsed, theme) ?? resolve2(parsed, theme) ?? resolve3(parsed, theme) ?? resolveColorNative(parsed, theme) ?? resolveLeadingTrackingNative(parsed) ?? resolveTypographyNative(parsed, theme) ?? resolveOpacityNative(parsed) ?? nativeShadowDeclarations(parsed);
1752
+ return resolveFlex(parsed, false) ?? resolve(parsed, theme) ?? resolve2(parsed, theme) ?? resolve3(parsed, theme) ?? resolveColorNative(parsed, theme) ?? resolveLeadingTrackingNative(parsed) ?? resolveTypographyNative(parsed, theme) ?? resolveOpacityNative(parsed) ?? nativeShadowDeclarations(parsed) ?? resolveNative(parsed, theme);
1668
1753
  }
1669
1754
 
1670
1755
  // src/jsEngine/resolveStyle.ts
@@ -1735,8 +1820,32 @@ var NUMERIC_LENGTH_PROPS = /* @__PURE__ */ new Set([
1735
1820
  "border-bottom-right-radius",
1736
1821
  "border-bottom-left-radius",
1737
1822
  "outline-width",
1738
- "outline-offset"
1823
+ "outline-offset",
1824
+ // The transform ops whose RN value is a plain JS number — translate takes
1825
+ // a length (px/percentage, same as top/left/etc. above) and scale takes a
1826
+ // bare decimal factor (parsed by the same "raw value" fallback branch
1827
+ // below). transform-op-rotate*/transform-op-skew* stay OUT of this set on
1828
+ // purpose, since RN wants those as literal "45deg"-shaped strings, not
1829
+ // numbers. See the transform-ops accumulator below for how these markers
1830
+ // get collected into the final array.
1831
+ "transform-op-translate-x",
1832
+ "transform-op-translate-y",
1833
+ "transform-op-scale-x",
1834
+ "transform-op-scale-y"
1739
1835
  ]);
1836
+ var TRANSFORM_OP_KEYS = /* @__PURE__ */ new Map([
1837
+ ["transform-op-translate-x", "translateX"],
1838
+ ["transform-op-translate-y", "translateY"],
1839
+ ["transform-op-rotate", "rotate"],
1840
+ ["transform-op-rotate-x", "rotateX"],
1841
+ ["transform-op-rotate-y", "rotateY"],
1842
+ ["transform-op-rotate-z", "rotateZ"],
1843
+ ["transform-op-skew-x", "skewX"],
1844
+ ["transform-op-skew-y", "skewY"],
1845
+ ["transform-op-scale-x", "scaleX"],
1846
+ ["transform-op-scale-y", "scaleY"]
1847
+ ]);
1848
+ var TRANSFORM_OP_ORDER = ["translateX", "translateY", "rotate", "rotateX", "rotateY", "rotateZ", "skewX", "skewY", "scaleX", "scaleY"];
1740
1849
  function isPlainPercentage(value) {
1741
1850
  if (!value.endsWith("%")) return false;
1742
1851
  let digits = value.slice(0, -1);
@@ -1773,6 +1882,7 @@ function resolveStyleJsWithWarnings(classString, theme, colorScheme, pressed, wi
1773
1882
  const style = {};
1774
1883
  const warnings = [];
1775
1884
  let shadowOffset = null;
1885
+ const transformOps = /* @__PURE__ */ new Map();
1776
1886
  for (const token of classString.split(/\s+/).filter(Boolean)) {
1777
1887
  const parsed = parseClass(token);
1778
1888
  const allModifiersHold = parsed.modifiers.every(
@@ -1792,6 +1902,17 @@ function resolveStyleJsWithWarnings(classString, theme, colorScheme, pressed, wi
1792
1902
  shadowOffset[d.property === "shadow-offset-x" ? "width" : "height"] = n;
1793
1903
  continue;
1794
1904
  }
1905
+ if (d.property === "transform-op-none") {
1906
+ transformOps.clear();
1907
+ continue;
1908
+ }
1909
+ const opKey = TRANSFORM_OP_KEYS.get(d.property);
1910
+ if (opKey !== void 0) {
1911
+ const { value: value2, warning: warning2 } = rnStyleValue(d.property, d.value);
1912
+ if (warning2 !== null) warnings.push(warning2);
1913
+ if (value2 !== null) transformOps.set(opKey, value2);
1914
+ continue;
1915
+ }
1795
1916
  const { value, warning } = rnStyleValue(d.property, d.value);
1796
1917
  if (warning !== null) warnings.push(warning);
1797
1918
  if (value !== null) style[kebabToCamel(d.property)] = value;
@@ -1800,6 +1921,11 @@ function resolveStyleJsWithWarnings(classString, theme, colorScheme, pressed, wi
1800
1921
  if (shadowOffset !== null) {
1801
1922
  style.shadowOffset = { width: shadowOffset.width ?? 0, height: shadowOffset.height ?? 0 };
1802
1923
  }
1924
+ if (transformOps.size > 0) {
1925
+ style.transform = TRANSFORM_OP_ORDER.filter((key) => transformOps.has(key)).map((key) => ({
1926
+ [key]: transformOps.get(key)
1927
+ }));
1928
+ }
1803
1929
  return { style, warnings };
1804
1930
  }
1805
1931
 
@@ -8,7 +8,7 @@ import "./chunk-AG7ZQ2K4.mjs";
8
8
 
9
9
  // src/jsxRuntimeCore.ts
10
10
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
11
- import { Fragment as Fragment2, useCallback, useState, useSyncExternalStore } from "react";
11
+ import { Fragment as Fragment2, useCallback, useRef, useState, useSyncExternalStore } from "react";
12
12
  import { Pressable, useWindowDimensions } from "react-native";
13
13
  function createJsxDEV(jsx2, jsxs2) {
14
14
  return function jsxDEV2(type, props, key, isStaticChildren, source, self) {
@@ -376,12 +376,19 @@ var VALUE_PREFIXES = [
376
376
  "justify-self-",
377
377
  "scale-x-",
378
378
  "scale-y-",
379
+ "scale-z-",
379
380
  "scale-",
381
+ "rotate-x-",
382
+ "rotate-y-",
383
+ "rotate-z-",
380
384
  "rotate-",
381
385
  "translate-x-",
382
386
  "translate-y-",
387
+ "translate-z-",
383
388
  "skew-x-",
384
389
  "skew-y-",
390
+ "perspective-origin-",
391
+ "perspective-",
385
392
  "origin-",
386
393
  "blur-",
387
394
  "brightness-",
@@ -582,11 +589,18 @@ function ReactiveElement({
582
589
  }) {
583
590
  (0, import_react.useSyncExternalStore)(import_darkModeStore.subscribeGlobalDarkMode, import_darkModeStore.getGlobalDarkMode);
584
591
  (0, import_react.useSyncExternalStore)(import_dynamicTokens.subscribeDynamicTokens, import_dynamicTokens.getDynamicTokensVersion);
585
- (0, import_react_native.useWindowDimensions)();
592
+ const windowSize = (0, import_react_native.useWindowDimensions)();
586
593
  const [measuredBasis, setMeasuredBasis] = (0, import_react.useState)(null);
594
+ const lastWindowSizeRef = (0, import_react.useRef)(windowSize);
595
+ if (lastWindowSizeRef.current.width !== windowSize.width || lastWindowSizeRef.current.height !== windowSize.height) {
596
+ lastWindowSizeRef.current = windowSize;
597
+ if (measuredBasis !== null) {
598
+ setMeasuredBasis(null);
599
+ }
600
+ }
587
601
  const measure = (0, import_react.useCallback)((e) => {
588
602
  const { width: w, height: h } = e.nativeEvent.layout;
589
- setMeasuredBasis((prev) => prev && prev.width === w && prev.height === h ? prev : { width: w, height: h });
603
+ setMeasuredBasis((prev) => prev === null ? { width: w, height: h } : prev);
590
604
  }, []);
591
605
  const [isHovered, setIsHovered] = (0, import_react.useState)(false);
592
606
  const [isFocused, setIsFocused] = (0, import_react.useState)(false);
@@ -1574,6 +1588,77 @@ function nativeShadowDeclarations(parsed) {
1574
1588
  ];
1575
1589
  }
1576
1590
 
1591
+ // src/jsEngine/resolvers/transform.ts
1592
+ function scaleFactor(parsed) {
1593
+ if (parsed.value === null) return null;
1594
+ const n = Number(parsed.value);
1595
+ if (!Number.isFinite(n)) return null;
1596
+ return parsed.isArbitrary ? n : n / 100;
1597
+ }
1598
+ function angle(parsed) {
1599
+ if (parsed.value === null) return null;
1600
+ if (parsed.isArbitrary) return parsed.value;
1601
+ const n = Number(parsed.value);
1602
+ if (!Number.isFinite(n)) return null;
1603
+ return `${parsed.negative ? -n : n}deg`;
1604
+ }
1605
+ function resolveNative(parsed, theme) {
1606
+ switch (parsed.utility) {
1607
+ case "transform-none":
1608
+ return [decl("transform-op-none", "1")];
1609
+ case "backface-visible":
1610
+ return [decl("backface-visibility", "visible")];
1611
+ case "backface-hidden":
1612
+ return [decl("backface-visibility", "hidden")];
1613
+ case "translate-x": {
1614
+ const v = resolveNegatableLength(theme, parsed);
1615
+ return v === null ? null : [decl("transform-op-translate-x", v)];
1616
+ }
1617
+ case "translate-y": {
1618
+ const v = resolveNegatableLength(theme, parsed);
1619
+ return v === null ? null : [decl("transform-op-translate-y", v)];
1620
+ }
1621
+ case "scale": {
1622
+ const n = scaleFactor(parsed);
1623
+ return n === null ? null : [decl("transform-op-scale-x", `${n}`), decl("transform-op-scale-y", `${n}`)];
1624
+ }
1625
+ case "scale-x": {
1626
+ const n = scaleFactor(parsed);
1627
+ return n === null ? null : [decl("transform-op-scale-x", `${n}`)];
1628
+ }
1629
+ case "scale-y": {
1630
+ const n = scaleFactor(parsed);
1631
+ return n === null ? null : [decl("transform-op-scale-y", `${n}`)];
1632
+ }
1633
+ case "rotate": {
1634
+ const deg = angle(parsed);
1635
+ return deg === null ? null : [decl("transform-op-rotate", deg)];
1636
+ }
1637
+ case "rotate-x": {
1638
+ const deg = angle(parsed);
1639
+ return deg === null ? null : [decl("transform-op-rotate-x", deg)];
1640
+ }
1641
+ case "rotate-y": {
1642
+ const deg = angle(parsed);
1643
+ return deg === null ? null : [decl("transform-op-rotate-y", deg)];
1644
+ }
1645
+ case "rotate-z": {
1646
+ const deg = angle(parsed);
1647
+ return deg === null ? null : [decl("transform-op-rotate-z", deg)];
1648
+ }
1649
+ case "skew-x": {
1650
+ const deg = angle(parsed);
1651
+ return deg === null ? null : [decl("transform-op-skew-x", deg)];
1652
+ }
1653
+ case "skew-y": {
1654
+ const deg = angle(parsed);
1655
+ return deg === null ? null : [decl("transform-op-skew-y", deg)];
1656
+ }
1657
+ default:
1658
+ return null;
1659
+ }
1660
+ }
1661
+
1577
1662
  // src/jsEngine/resolveUtilityNative.ts
1578
1663
  function firstFontName(stack) {
1579
1664
  const first = stack.split(",")[0] ?? stack;
@@ -1655,7 +1740,7 @@ function resolveOpacityNative(parsed) {
1655
1740
  return v === null ? null : [decl("opacity", v)];
1656
1741
  }
1657
1742
  function resolveUtilityNative(parsed, theme) {
1658
- return resolveFlex(parsed, false) ?? resolve(parsed, theme) ?? resolve2(parsed, theme) ?? resolve3(parsed, theme) ?? resolveColorNative(parsed, theme) ?? resolveLeadingTrackingNative(parsed) ?? resolveTypographyNative(parsed, theme) ?? resolveOpacityNative(parsed) ?? nativeShadowDeclarations(parsed);
1743
+ return resolveFlex(parsed, false) ?? resolve(parsed, theme) ?? resolve2(parsed, theme) ?? resolve3(parsed, theme) ?? resolveColorNative(parsed, theme) ?? resolveLeadingTrackingNative(parsed) ?? resolveTypographyNative(parsed, theme) ?? resolveOpacityNative(parsed) ?? nativeShadowDeclarations(parsed) ?? resolveNative(parsed, theme);
1659
1744
  }
1660
1745
 
1661
1746
  // src/jsEngine/resolveStyle.ts
@@ -1726,8 +1811,32 @@ var NUMERIC_LENGTH_PROPS = /* @__PURE__ */ new Set([
1726
1811
  "border-bottom-right-radius",
1727
1812
  "border-bottom-left-radius",
1728
1813
  "outline-width",
1729
- "outline-offset"
1814
+ "outline-offset",
1815
+ // The transform ops whose RN value is a plain JS number — translate takes
1816
+ // a length (px/percentage, same as top/left/etc. above) and scale takes a
1817
+ // bare decimal factor (parsed by the same "raw value" fallback branch
1818
+ // below). transform-op-rotate*/transform-op-skew* stay OUT of this set on
1819
+ // purpose, since RN wants those as literal "45deg"-shaped strings, not
1820
+ // numbers. See the transform-ops accumulator below for how these markers
1821
+ // get collected into the final array.
1822
+ "transform-op-translate-x",
1823
+ "transform-op-translate-y",
1824
+ "transform-op-scale-x",
1825
+ "transform-op-scale-y"
1730
1826
  ]);
1827
+ var TRANSFORM_OP_KEYS = /* @__PURE__ */ new Map([
1828
+ ["transform-op-translate-x", "translateX"],
1829
+ ["transform-op-translate-y", "translateY"],
1830
+ ["transform-op-rotate", "rotate"],
1831
+ ["transform-op-rotate-x", "rotateX"],
1832
+ ["transform-op-rotate-y", "rotateY"],
1833
+ ["transform-op-rotate-z", "rotateZ"],
1834
+ ["transform-op-skew-x", "skewX"],
1835
+ ["transform-op-skew-y", "skewY"],
1836
+ ["transform-op-scale-x", "scaleX"],
1837
+ ["transform-op-scale-y", "scaleY"]
1838
+ ]);
1839
+ var TRANSFORM_OP_ORDER = ["translateX", "translateY", "rotate", "rotateX", "rotateY", "rotateZ", "skewX", "skewY", "scaleX", "scaleY"];
1731
1840
  function isPlainPercentage(value) {
1732
1841
  if (!value.endsWith("%")) return false;
1733
1842
  let digits = value.slice(0, -1);
@@ -1764,6 +1873,7 @@ function resolveStyleJsWithWarnings(classString, theme, colorScheme, pressed, wi
1764
1873
  const style = {};
1765
1874
  const warnings = [];
1766
1875
  let shadowOffset = null;
1876
+ const transformOps = /* @__PURE__ */ new Map();
1767
1877
  for (const token of classString.split(/\s+/).filter(Boolean)) {
1768
1878
  const parsed = parseClass(token);
1769
1879
  const allModifiersHold = parsed.modifiers.every(
@@ -1783,6 +1893,17 @@ function resolveStyleJsWithWarnings(classString, theme, colorScheme, pressed, wi
1783
1893
  shadowOffset[d.property === "shadow-offset-x" ? "width" : "height"] = n;
1784
1894
  continue;
1785
1895
  }
1896
+ if (d.property === "transform-op-none") {
1897
+ transformOps.clear();
1898
+ continue;
1899
+ }
1900
+ const opKey = TRANSFORM_OP_KEYS.get(d.property);
1901
+ if (opKey !== void 0) {
1902
+ const { value: value2, warning: warning2 } = rnStyleValue(d.property, d.value);
1903
+ if (warning2 !== null) warnings.push(warning2);
1904
+ if (value2 !== null) transformOps.set(opKey, value2);
1905
+ continue;
1906
+ }
1786
1907
  const { value, warning } = rnStyleValue(d.property, d.value);
1787
1908
  if (warning !== null) warnings.push(warning);
1788
1909
  if (value !== null) style[kebabToCamel(d.property)] = value;
@@ -1791,6 +1912,11 @@ function resolveStyleJsWithWarnings(classString, theme, colorScheme, pressed, wi
1791
1912
  if (shadowOffset !== null) {
1792
1913
  style.shadowOffset = { width: shadowOffset.width ?? 0, height: shadowOffset.height ?? 0 };
1793
1914
  }
1915
+ if (transformOps.size > 0) {
1916
+ style.transform = TRANSFORM_OP_ORDER.filter((key) => transformOps.has(key)).map((key) => ({
1917
+ [key]: transformOps.get(key)
1918
+ }));
1919
+ }
1794
1920
  return { style, warnings };
1795
1921
  }
1796
1922
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kbach/react-native",
3
- "version": "1.0.0-beta.22",
3
+ "version": "1.0.0-beta.24",
4
4
  "description": "Kbach's utility-class styling engine for React Native — the same class names as @kbach/react, resolved to a StyleSheet-ready style object via a Kotlin/JNI TurboModule (native), the same WASM engine @kbach/react uses (Expo Web / react-native-web), or a pure-JS fallback engine (Expo Go). Ships its own native Android module — one npm install, no separate native package. Beta. Native is Android-only (no iOS bridge yet); works with the React Native CLI, an Expo dev client, AND Expo Go (a reduced-coverage pure-JS fallback, since Expo Go can't load any custom native module).",
5
5
  "keywords": [
6
6
  "css",