@kbach/react-native 1.0.0-beta.21 → 1.0.0-beta.23

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
 
@@ -173,8 +178,26 @@ optional, for seeding a startup default. Outside React, use
173
178
  `getGlobalDarkMode()`/`toggleGlobalDarkMode()` instead.
174
179
 
175
180
  Follows the OS setting live until overridden, and isn't persisted across
176
- app launches (no built-in storage dependency). To persist, read your own
177
- storage at startup and call `setGlobalThemeMode()` before your first render.
181
+ app launches by default (no forced storage dependency). To persist, pass
182
+ any async key-value store exposing `getItem`/`setItem` as `<ThemeProvider
183
+ persist>` — `@react-native-async-storage/async-storage`'s default export
184
+ already matches this shape exactly:
185
+
186
+ ```tsx
187
+ import AsyncStorage from '@react-native-async-storage/async-storage';
188
+ import { ThemeProvider } from '@kbach/react-native';
189
+
190
+ <ThemeProvider persist={AsyncStorage}>
191
+ <App />
192
+ </ThemeProvider>;
193
+ ```
194
+
195
+ The persisted mode (if any) is read once on mount and wins over
196
+ `defaultMode`; every later explicit choice is written back automatically.
197
+ `false` by default — this package still never imports a storage library
198
+ itself, so nothing changes for apps that don't opt in. Prefer wiring your
199
+ own storage manually instead? `setGlobalThemeMode()` before your first
200
+ render still works exactly as before.
178
201
 
179
202
  ## Reading colors as values
180
203
 
@@ -222,12 +245,15 @@ way — no such unit exists on native at all. It already works fine on web
222
245
  (real CSS); on native the declaration is dropped and a `console.warn`
223
246
  explains why (dev builds only).
224
247
 
225
- **Percentage-relative width/height** (`calc(100%-3rem)`, `calc(50%+16px)`)
226
- IS supported, differently: since native has no way to know "100% of what"
227
- ahead of time, this measures the element's own real layout instead —
248
+ **Percentage-relative width/height** (`calc(100%-3rem)`, `calc(50%+16px)`,
249
+ `min(50%,20rem)`, `max(1rem,5%)`, `clamp(16rem,50%,32rem)`) IS supported,
250
+ differently: since native has no way to know "100% of what" ahead of time,
251
+ this measures the element's own real layout instead —
228
252
 
229
253
  ```tsx
230
254
  <View className="w-[calc(100%_-_3rem)]" />
255
+ <View className="w-[min(50%,20rem)]" /> // half the parent, capped at a fixed size
256
+ <View className="w-[clamp(16rem,50%,32rem)]" />
231
257
  ```
232
258
 
233
259
  Renders once at a plain `100%` (a real RN percentage, resolved correctly by
@@ -236,9 +262,14 @@ event, then immediately corrects to the actual computed number once
236
262
  measured — a genuine one-frame measure-then-snap, not instant math like the
237
263
  constant-only case above, and it keeps re-measuring on every layout (screen
238
264
  rotation, parent resize, ...) rather than freezing after the first value.
239
- Scoped to exactly one percentage term plus one constant term on `width`/
240
- `height` only — `calc(50%-50%)`, other properties, or anything with `*`/`/`
241
- falls through to the same drop-and-warn behavior as before.
265
+ Scoped narrowly, same "reduce fully or bail" philosophy as the constant-only
266
+ case: `calc()` takes exactly one percentage term plus one constant term;
267
+ `min()`/`max()` take two or more comma-separated arguments, each EITHER a
268
+ plain percentage or a constant px/rem expression (not both mixed in one
269
+ argument — `min(50%-1rem,20rem)` is out of scope); `clamp()` takes exactly
270
+ three. `width`/`height` only in every case — other properties, or anything
271
+ that doesn't reduce fully, falls through to the same drop-and-warn behavior
272
+ as before.
242
273
 
243
274
  **Arbitrary values can't contain literal spaces** — this is a pre-existing
244
275
  rule (real Tailwind has the same one), not specific to `calc()`: a space
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { ColorEntry, ContainerConfig, ThemeConfig, defaultTheme, getTheme, setTheme } from './theme';
2
- export { C as ColorScale, a as ColorsAPI, K as KbachConfig, T as ThemeProvider, b as ThemeProviderProps, c as ThemeState, d as applyKbachConfig, r as resolveKbachConfig, u as useColors, e as useDynamicToken, f as useTheme } from './useDynamicToken-Dr15c88t.js';
2
+ export { C as ColorScale, a as ColorsAPI, K as KbachConfig, T as ThemeProvider, b as ThemeProviderProps, c as ThemeState, d as ThemeStorageAdapter, e as applyKbachConfig, r as resolveKbachConfig, u as useColors, f as useDynamicToken, g as useTheme } from './useDynamicToken-DAHQob1u.js';
3
3
  export { ThemeMode, getGlobalDarkMode, getGlobalThemeMode, setGlobalThemeMode, subscribeGlobalDarkMode, toggleGlobalDarkMode } from './darkModeStore';
4
4
  export { deleteDynamicToken, getDynamicToken, setDynamicToken, subscribeDynamicTokens } from './dynamicTokens';
5
5
  import 'react';
@@ -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"
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"]
1505
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
 
@@ -1680,9 +1799,13 @@ function useTheme() {
1680
1799
  var import_react2 = require("react");
1681
1800
  var import_darkModeStore3 = require("./darkModeStore");
1682
1801
  var import_jsx_runtime = require("react/jsx-runtime");
1802
+ var PERSIST_STORAGE_KEY = "kbach-theme";
1803
+ function isThemeMode(value) {
1804
+ return value === "light" || value === "dark" || value === "system";
1805
+ }
1683
1806
  var mountedProviderCount = 0;
1684
1807
  var warnedMultipleProviders = false;
1685
- function ThemeProvider({ children, defaultMode = "system" }) {
1808
+ function ThemeProvider({ children, defaultMode = "system", persist = false }) {
1686
1809
  (0, import_react2.useEffect)(() => {
1687
1810
  mountedProviderCount++;
1688
1811
  if (mountedProviderCount > 1 && !warnedMultipleProviders) {
@@ -1699,8 +1822,29 @@ function ThemeProvider({ children, defaultMode = "system" }) {
1699
1822
  (0, import_react2.useEffect)(() => {
1700
1823
  if (seeded.current) return;
1701
1824
  seeded.current = true;
1702
- (0, import_darkModeStore3.seedDefaultMode)(defaultMode);
1825
+ if (!persist) {
1826
+ (0, import_darkModeStore3.seedDefaultMode)(defaultMode);
1827
+ return;
1828
+ }
1829
+ let cancelled = false;
1830
+ persist.getItem(PERSIST_STORAGE_KEY).then((stored) => {
1831
+ if (cancelled) return;
1832
+ if (isThemeMode(stored)) (0, import_darkModeStore3.seedDefaultMode)(stored);
1833
+ else (0, import_darkModeStore3.seedDefaultMode)(defaultMode);
1834
+ }).catch(() => {
1835
+ if (!cancelled) (0, import_darkModeStore3.seedDefaultMode)(defaultMode);
1836
+ });
1837
+ return () => {
1838
+ cancelled = true;
1839
+ };
1703
1840
  }, []);
1841
+ (0, import_react2.useEffect)(() => {
1842
+ if (!persist) return;
1843
+ return (0, import_darkModeStore3.subscribeGlobalDarkMode)(() => {
1844
+ persist.setItem(PERSIST_STORAGE_KEY, (0, import_darkModeStore3.getGlobalThemeMode)()).catch(() => {
1845
+ });
1846
+ });
1847
+ }, [persist]);
1704
1848
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children });
1705
1849
  }
1706
1850
 
@@ -1,5 +1,5 @@
1
1
  export { ColorEntry, ContainerConfig, ThemeConfig, defaultTheme, getTheme, setTheme } from './theme';
2
- export { C as ColorScale, a as ColorsAPI, K as KbachConfig, T as ThemeProvider, b as ThemeProviderProps, c as ThemeState, d as applyKbachConfig, r as resolveKbachConfig, u as useColors, e as useDynamicToken, f as useTheme } from './useDynamicToken-Dr15c88t.js';
2
+ export { C as ColorScale, a as ColorsAPI, K as KbachConfig, T as ThemeProvider, b as ThemeProviderProps, c as ThemeState, e as applyKbachConfig, r as resolveKbachConfig, u as useColors, f as useDynamicToken, g as useTheme } from './useDynamicToken-DAHQob1u.js';
3
3
  export { ThemeMode, getGlobalDarkMode, getGlobalThemeMode, setGlobalThemeMode, subscribeGlobalDarkMode, toggleGlobalDarkMode } from './darkModeStore';
4
4
  export { deleteDynamicToken, getDynamicToken, setDynamicToken, subscribeDynamicTokens } from './dynamicTokens';
5
5
  import 'react';
package/dist/index.web.js CHANGED
@@ -311,9 +311,13 @@ function useTheme() {
311
311
  var import_react2 = require("react");
312
312
  var import_darkModeStore2 = require("./darkModeStore");
313
313
  var import_jsx_runtime = require("react/jsx-runtime");
314
+ var PERSIST_STORAGE_KEY = "kbach-theme";
315
+ function isThemeMode(value) {
316
+ return value === "light" || value === "dark" || value === "system";
317
+ }
314
318
  var mountedProviderCount = 0;
315
319
  var warnedMultipleProviders = false;
316
- function ThemeProvider({ children, defaultMode = "system" }) {
320
+ function ThemeProvider({ children, defaultMode = "system", persist = false }) {
317
321
  (0, import_react2.useEffect)(() => {
318
322
  mountedProviderCount++;
319
323
  if (mountedProviderCount > 1 && !warnedMultipleProviders) {
@@ -330,8 +334,29 @@ function ThemeProvider({ children, defaultMode = "system" }) {
330
334
  (0, import_react2.useEffect)(() => {
331
335
  if (seeded.current) return;
332
336
  seeded.current = true;
333
- (0, import_darkModeStore2.seedDefaultMode)(defaultMode);
337
+ if (!persist) {
338
+ (0, import_darkModeStore2.seedDefaultMode)(defaultMode);
339
+ return;
340
+ }
341
+ let cancelled = false;
342
+ persist.getItem(PERSIST_STORAGE_KEY).then((stored) => {
343
+ if (cancelled) return;
344
+ if (isThemeMode(stored)) (0, import_darkModeStore2.seedDefaultMode)(stored);
345
+ else (0, import_darkModeStore2.seedDefaultMode)(defaultMode);
346
+ }).catch(() => {
347
+ if (!cancelled) (0, import_darkModeStore2.seedDefaultMode)(defaultMode);
348
+ });
349
+ return () => {
350
+ cancelled = true;
351
+ };
334
352
  }, []);
353
+ (0, import_react2.useEffect)(() => {
354
+ if (!persist) return;
355
+ return (0, import_darkModeStore2.subscribeGlobalDarkMode)(() => {
356
+ persist.setItem(PERSIST_STORAGE_KEY, (0, import_darkModeStore2.getGlobalThemeMode)()).catch(() => {
357
+ });
358
+ });
359
+ }, [persist]);
335
360
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children });
336
361
  }
337
362
 
@@ -94,9 +94,13 @@ function useTheme() {
94
94
  // src/ThemeProvider.tsx
95
95
  import { useEffect, useRef } from "react";
96
96
  import { Fragment, jsx } from "react/jsx-runtime";
97
+ var PERSIST_STORAGE_KEY = "kbach-theme";
98
+ function isThemeMode(value) {
99
+ return value === "light" || value === "dark" || value === "system";
100
+ }
97
101
  var mountedProviderCount = 0;
98
102
  var warnedMultipleProviders = false;
99
- function ThemeProvider({ children, defaultMode = "system" }) {
103
+ function ThemeProvider({ children, defaultMode = "system", persist = false }) {
100
104
  useEffect(() => {
101
105
  mountedProviderCount++;
102
106
  if (mountedProviderCount > 1 && !warnedMultipleProviders) {
@@ -113,8 +117,29 @@ function ThemeProvider({ children, defaultMode = "system" }) {
113
117
  useEffect(() => {
114
118
  if (seeded.current) return;
115
119
  seeded.current = true;
116
- seedDefaultMode(defaultMode);
120
+ if (!persist) {
121
+ seedDefaultMode(defaultMode);
122
+ return;
123
+ }
124
+ let cancelled = false;
125
+ persist.getItem(PERSIST_STORAGE_KEY).then((stored) => {
126
+ if (cancelled) return;
127
+ if (isThemeMode(stored)) seedDefaultMode(stored);
128
+ else seedDefaultMode(defaultMode);
129
+ }).catch(() => {
130
+ if (!cancelled) seedDefaultMode(defaultMode);
131
+ });
132
+ return () => {
133
+ cancelled = true;
134
+ };
117
135
  }, []);
136
+ useEffect(() => {
137
+ if (!persist) return;
138
+ return subscribeGlobalDarkMode(() => {
139
+ persist.setItem(PERSIST_STORAGE_KEY, getGlobalThemeMode()).catch(() => {
140
+ });
141
+ });
142
+ }, [persist]);
118
143
  return /* @__PURE__ */ jsx(Fragment, { children });
119
144
  }
120
145
 
@@ -205,6 +205,46 @@ function parsePercentRelativeCalc(token) {
205
205
  function resolvePercentRelativeCalc(basisPx, calc) {
206
206
  return basisPx * calc.percentCoefficient / 100 + calc.constantPx;
207
207
  }
208
+ var PERCENT_MIN_MAX_CLAMP_TOKEN_RE = /^(w|h)-\[(min|max|clamp)\(([^)]+)\)\]$/;
209
+ function parseOperand(arg) {
210
+ const trimmed = arg.trim();
211
+ const pct = /^(-?[\d.]+)%$/.exec(trimmed);
212
+ if (pct) {
213
+ const percentCoefficient = Number(pct[1]);
214
+ return Number.isFinite(percentCoefficient) ? { percentCoefficient, constantPx: 0 } : null;
215
+ }
216
+ const constantPx = reduceConstantMath(`calc(${trimmed})`);
217
+ return constantPx === null ? null : { percentCoefficient: 0, constantPx };
218
+ }
219
+ function parsePercentRelativeMinMaxClamp(token) {
220
+ const m = PERCENT_MIN_MAX_CLAMP_TOKEN_RE.exec(token);
221
+ if (!m) return null;
222
+ const property = m[1] === "w" ? "width" : "height";
223
+ const kind = m[2];
224
+ const args = splitTopLevelCommas(m[3].replace(/_/g, " "));
225
+ if (kind === "clamp" && args.length !== 3) return null;
226
+ if (kind !== "clamp" && args.length < 1) return null;
227
+ const operands = args.map(parseOperand);
228
+ if (operands.some((o) => o === null)) return null;
229
+ const resolved = operands;
230
+ if (!resolved.some((o) => o.percentCoefficient !== 0)) return null;
231
+ const resolveOperand = (o, basisPx) => basisPx * o.percentCoefficient / 100 + o.constantPx;
232
+ return {
233
+ property,
234
+ resolve(basisPx) {
235
+ const values = resolved.map((o) => resolveOperand(o, basisPx));
236
+ if (kind === "min") return Math.min(...values);
237
+ if (kind === "max") return Math.max(...values);
238
+ const [min, preferred, max] = values;
239
+ return Math.max(min, Math.min(preferred, max));
240
+ }
241
+ };
242
+ }
243
+ function parsePercentRelativeExpr(token) {
244
+ const calc = parsePercentRelativeCalc(token);
245
+ if (calc) return { property: calc.property, resolve: (basisPx) => resolvePercentRelativeCalc(basisPx, calc) };
246
+ return parsePercentRelativeMinMaxClamp(token);
247
+ }
208
248
 
209
249
  // src/jsEngine/parser.ts
210
250
  var VALUE_PREFIXES = [
@@ -335,12 +375,19 @@ var VALUE_PREFIXES = [
335
375
  "justify-self-",
336
376
  "scale-x-",
337
377
  "scale-y-",
378
+ "scale-z-",
338
379
  "scale-",
380
+ "rotate-x-",
381
+ "rotate-y-",
382
+ "rotate-z-",
339
383
  "rotate-",
340
384
  "translate-x-",
341
385
  "translate-y-",
386
+ "translate-z-",
342
387
  "skew-x-",
343
388
  "skew-y-",
389
+ "perspective-origin-",
390
+ "perspective-",
344
391
  "origin-",
345
392
  "blur-",
346
393
  "brightness-",
@@ -471,17 +518,17 @@ function substituteDynamicTokens(classStrRaw) {
471
518
  return value ?? whole;
472
519
  });
473
520
  }
474
- var PERCENT_RELATIVE_CALC_HINT_RE = /[wh]-\[calc\([^)]*%[^)]*\)\]/;
521
+ var PERCENT_RELATIVE_CALC_HINT_RE = /[wh]-\[(calc|min|max|clamp)\([^)]*%[^)]*\)\]/;
475
522
  function extractPercentRelativeCalcs(classStrRaw) {
476
523
  const found = [];
477
524
  for (const token of classStrRaw.split(/\s+/)) {
478
- const parsed = parsePercentRelativeCalc(token);
525
+ const parsed = parsePercentRelativeExpr(token);
479
526
  if (parsed) found.push(parsed);
480
527
  }
481
528
  return found;
482
529
  }
483
530
  function stripPercentRelativeCalcTokens(classStrRaw) {
484
- return classStrRaw.split(/\s+/).filter((token) => token && parsePercentRelativeCalc(token) === null).join(" ");
531
+ return classStrRaw.split(/\s+/).filter((token) => token && parsePercentRelativeExpr(token) === null).join(" ");
485
532
  }
486
533
  var STATE_MODIFIER_NAMES = /* @__PURE__ */ new Set(["hover", "focus", "disabled"]);
487
534
  var STATE_MODIFIER_HINT_RE = /(^|\s|:)(hover|focus|disabled):/;
@@ -556,7 +603,7 @@ function ReactiveElement({
556
603
  layoutOverrides = {};
557
604
  for (const calc of percentCalcs) {
558
605
  const basis = measuredBasis?.[calc.property];
559
- layoutOverrides[calc.property] = basis === void 0 ? "100%" : resolvePercentRelativeCalc(basis, calc);
606
+ layoutOverrides[calc.property] = basis === void 0 ? "100%" : calc.resolve(basis);
560
607
  }
561
608
  const userOnLayout = hostRest.onLayout;
562
609
  hostRestWithLayout = {
@@ -1543,6 +1590,77 @@ function nativeShadowDeclarations(parsed) {
1543
1590
  ];
1544
1591
  }
1545
1592
 
1593
+ // src/jsEngine/resolvers/transform.ts
1594
+ function scaleFactor(parsed) {
1595
+ if (parsed.value === null) return null;
1596
+ const n = Number(parsed.value);
1597
+ if (!Number.isFinite(n)) return null;
1598
+ return parsed.isArbitrary ? n : n / 100;
1599
+ }
1600
+ function angle(parsed) {
1601
+ if (parsed.value === null) return null;
1602
+ if (parsed.isArbitrary) return parsed.value;
1603
+ const n = Number(parsed.value);
1604
+ if (!Number.isFinite(n)) return null;
1605
+ return `${parsed.negative ? -n : n}deg`;
1606
+ }
1607
+ function resolveNative(parsed, theme) {
1608
+ switch (parsed.utility) {
1609
+ case "transform-none":
1610
+ return [decl("transform-op-none", "1")];
1611
+ case "backface-visible":
1612
+ return [decl("backface-visibility", "visible")];
1613
+ case "backface-hidden":
1614
+ return [decl("backface-visibility", "hidden")];
1615
+ case "translate-x": {
1616
+ const v = resolveNegatableLength(theme, parsed);
1617
+ return v === null ? null : [decl("transform-op-translate-x", v)];
1618
+ }
1619
+ case "translate-y": {
1620
+ const v = resolveNegatableLength(theme, parsed);
1621
+ return v === null ? null : [decl("transform-op-translate-y", v)];
1622
+ }
1623
+ case "scale": {
1624
+ const n = scaleFactor(parsed);
1625
+ return n === null ? null : [decl("transform-op-scale-x", `${n}`), decl("transform-op-scale-y", `${n}`)];
1626
+ }
1627
+ case "scale-x": {
1628
+ const n = scaleFactor(parsed);
1629
+ return n === null ? null : [decl("transform-op-scale-x", `${n}`)];
1630
+ }
1631
+ case "scale-y": {
1632
+ const n = scaleFactor(parsed);
1633
+ return n === null ? null : [decl("transform-op-scale-y", `${n}`)];
1634
+ }
1635
+ case "rotate": {
1636
+ const deg = angle(parsed);
1637
+ return deg === null ? null : [decl("transform-op-rotate", deg)];
1638
+ }
1639
+ case "rotate-x": {
1640
+ const deg = angle(parsed);
1641
+ return deg === null ? null : [decl("transform-op-rotate-x", deg)];
1642
+ }
1643
+ case "rotate-y": {
1644
+ const deg = angle(parsed);
1645
+ return deg === null ? null : [decl("transform-op-rotate-y", deg)];
1646
+ }
1647
+ case "rotate-z": {
1648
+ const deg = angle(parsed);
1649
+ return deg === null ? null : [decl("transform-op-rotate-z", deg)];
1650
+ }
1651
+ case "skew-x": {
1652
+ const deg = angle(parsed);
1653
+ return deg === null ? null : [decl("transform-op-skew-x", deg)];
1654
+ }
1655
+ case "skew-y": {
1656
+ const deg = angle(parsed);
1657
+ return deg === null ? null : [decl("transform-op-skew-y", deg)];
1658
+ }
1659
+ default:
1660
+ return null;
1661
+ }
1662
+ }
1663
+
1546
1664
  // src/jsEngine/resolveUtilityNative.ts
1547
1665
  function firstFontName(stack) {
1548
1666
  const first = stack.split(",")[0] ?? stack;
@@ -1624,7 +1742,7 @@ function resolveOpacityNative(parsed) {
1624
1742
  return v === null ? null : [decl("opacity", v)];
1625
1743
  }
1626
1744
  function resolveUtilityNative(parsed, theme) {
1627
- 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);
1745
+ 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);
1628
1746
  }
1629
1747
 
1630
1748
  // src/jsEngine/resolveStyle.ts
@@ -1695,8 +1813,32 @@ var NUMERIC_LENGTH_PROPS = /* @__PURE__ */ new Set([
1695
1813
  "border-bottom-right-radius",
1696
1814
  "border-bottom-left-radius",
1697
1815
  "outline-width",
1698
- "outline-offset"
1816
+ "outline-offset",
1817
+ // The transform ops whose RN value is a plain JS number — translate takes
1818
+ // a length (px/percentage, same as top/left/etc. above) and scale takes a
1819
+ // bare decimal factor (parsed by the same "raw value" fallback branch
1820
+ // below). transform-op-rotate*/transform-op-skew* stay OUT of this set on
1821
+ // purpose, since RN wants those as literal "45deg"-shaped strings, not
1822
+ // numbers. See the transform-ops accumulator below for how these markers
1823
+ // get collected into the final array.
1824
+ "transform-op-translate-x",
1825
+ "transform-op-translate-y",
1826
+ "transform-op-scale-x",
1827
+ "transform-op-scale-y"
1699
1828
  ]);
1829
+ var TRANSFORM_OP_KEYS = /* @__PURE__ */ new Map([
1830
+ ["transform-op-translate-x", "translateX"],
1831
+ ["transform-op-translate-y", "translateY"],
1832
+ ["transform-op-rotate", "rotate"],
1833
+ ["transform-op-rotate-x", "rotateX"],
1834
+ ["transform-op-rotate-y", "rotateY"],
1835
+ ["transform-op-rotate-z", "rotateZ"],
1836
+ ["transform-op-skew-x", "skewX"],
1837
+ ["transform-op-skew-y", "skewY"],
1838
+ ["transform-op-scale-x", "scaleX"],
1839
+ ["transform-op-scale-y", "scaleY"]
1840
+ ]);
1841
+ var TRANSFORM_OP_ORDER = ["translateX", "translateY", "rotate", "rotateX", "rotateY", "rotateZ", "skewX", "skewY", "scaleX", "scaleY"];
1700
1842
  function isPlainPercentage(value) {
1701
1843
  if (!value.endsWith("%")) return false;
1702
1844
  let digits = value.slice(0, -1);
@@ -1733,6 +1875,7 @@ function resolveStyleJsWithWarnings(classString, theme, colorScheme, pressed, wi
1733
1875
  const style = {};
1734
1876
  const warnings = [];
1735
1877
  let shadowOffset = null;
1878
+ const transformOps = /* @__PURE__ */ new Map();
1736
1879
  for (const token of classString.split(/\s+/).filter(Boolean)) {
1737
1880
  const parsed = parseClass(token);
1738
1881
  const allModifiersHold = parsed.modifiers.every(
@@ -1752,6 +1895,17 @@ function resolveStyleJsWithWarnings(classString, theme, colorScheme, pressed, wi
1752
1895
  shadowOffset[d.property === "shadow-offset-x" ? "width" : "height"] = n;
1753
1896
  continue;
1754
1897
  }
1898
+ if (d.property === "transform-op-none") {
1899
+ transformOps.clear();
1900
+ continue;
1901
+ }
1902
+ const opKey = TRANSFORM_OP_KEYS.get(d.property);
1903
+ if (opKey !== void 0) {
1904
+ const { value: value2, warning: warning2 } = rnStyleValue(d.property, d.value);
1905
+ if (warning2 !== null) warnings.push(warning2);
1906
+ if (value2 !== null) transformOps.set(opKey, value2);
1907
+ continue;
1908
+ }
1755
1909
  const { value, warning } = rnStyleValue(d.property, d.value);
1756
1910
  if (warning !== null) warnings.push(warning);
1757
1911
  if (value !== null) style[kebabToCamel(d.property)] = value;
@@ -1760,6 +1914,11 @@ function resolveStyleJsWithWarnings(classString, theme, colorScheme, pressed, wi
1760
1914
  if (shadowOffset !== null) {
1761
1915
  style.shadowOffset = { width: shadowOffset.width ?? 0, height: shadowOffset.height ?? 0 };
1762
1916
  }
1917
+ if (transformOps.size > 0) {
1918
+ style.transform = TRANSFORM_OP_ORDER.filter((key) => transformOps.has(key)).map((key) => ({
1919
+ [key]: transformOps.get(key)
1920
+ }));
1921
+ }
1763
1922
  return { style, warnings };
1764
1923
  }
1765
1924
 
@@ -206,6 +206,46 @@ function parsePercentRelativeCalc(token) {
206
206
  function resolvePercentRelativeCalc(basisPx, calc) {
207
207
  return basisPx * calc.percentCoefficient / 100 + calc.constantPx;
208
208
  }
209
+ var PERCENT_MIN_MAX_CLAMP_TOKEN_RE = /^(w|h)-\[(min|max|clamp)\(([^)]+)\)\]$/;
210
+ function parseOperand(arg) {
211
+ const trimmed = arg.trim();
212
+ const pct = /^(-?[\d.]+)%$/.exec(trimmed);
213
+ if (pct) {
214
+ const percentCoefficient = Number(pct[1]);
215
+ return Number.isFinite(percentCoefficient) ? { percentCoefficient, constantPx: 0 } : null;
216
+ }
217
+ const constantPx = reduceConstantMath(`calc(${trimmed})`);
218
+ return constantPx === null ? null : { percentCoefficient: 0, constantPx };
219
+ }
220
+ function parsePercentRelativeMinMaxClamp(token) {
221
+ const m = PERCENT_MIN_MAX_CLAMP_TOKEN_RE.exec(token);
222
+ if (!m) return null;
223
+ const property = m[1] === "w" ? "width" : "height";
224
+ const kind = m[2];
225
+ const args = splitTopLevelCommas(m[3].replace(/_/g, " "));
226
+ if (kind === "clamp" && args.length !== 3) return null;
227
+ if (kind !== "clamp" && args.length < 1) return null;
228
+ const operands = args.map(parseOperand);
229
+ if (operands.some((o) => o === null)) return null;
230
+ const resolved = operands;
231
+ if (!resolved.some((o) => o.percentCoefficient !== 0)) return null;
232
+ const resolveOperand = (o, basisPx) => basisPx * o.percentCoefficient / 100 + o.constantPx;
233
+ return {
234
+ property,
235
+ resolve(basisPx) {
236
+ const values = resolved.map((o) => resolveOperand(o, basisPx));
237
+ if (kind === "min") return Math.min(...values);
238
+ if (kind === "max") return Math.max(...values);
239
+ const [min, preferred, max] = values;
240
+ return Math.max(min, Math.min(preferred, max));
241
+ }
242
+ };
243
+ }
244
+ function parsePercentRelativeExpr(token) {
245
+ const calc = parsePercentRelativeCalc(token);
246
+ if (calc) return { property: calc.property, resolve: (basisPx) => resolvePercentRelativeCalc(basisPx, calc) };
247
+ return parsePercentRelativeMinMaxClamp(token);
248
+ }
209
249
 
210
250
  // src/jsEngine/parser.ts
211
251
  var VALUE_PREFIXES = [
@@ -336,12 +376,19 @@ var VALUE_PREFIXES = [
336
376
  "justify-self-",
337
377
  "scale-x-",
338
378
  "scale-y-",
379
+ "scale-z-",
339
380
  "scale-",
381
+ "rotate-x-",
382
+ "rotate-y-",
383
+ "rotate-z-",
340
384
  "rotate-",
341
385
  "translate-x-",
342
386
  "translate-y-",
387
+ "translate-z-",
343
388
  "skew-x-",
344
389
  "skew-y-",
390
+ "perspective-origin-",
391
+ "perspective-",
345
392
  "origin-",
346
393
  "blur-",
347
394
  "brightness-",
@@ -472,17 +519,17 @@ function substituteDynamicTokens(classStrRaw) {
472
519
  return value ?? whole;
473
520
  });
474
521
  }
475
- var PERCENT_RELATIVE_CALC_HINT_RE = /[wh]-\[calc\([^)]*%[^)]*\)\]/;
522
+ var PERCENT_RELATIVE_CALC_HINT_RE = /[wh]-\[(calc|min|max|clamp)\([^)]*%[^)]*\)\]/;
476
523
  function extractPercentRelativeCalcs(classStrRaw) {
477
524
  const found = [];
478
525
  for (const token of classStrRaw.split(/\s+/)) {
479
- const parsed = parsePercentRelativeCalc(token);
526
+ const parsed = parsePercentRelativeExpr(token);
480
527
  if (parsed) found.push(parsed);
481
528
  }
482
529
  return found;
483
530
  }
484
531
  function stripPercentRelativeCalcTokens(classStrRaw) {
485
- return classStrRaw.split(/\s+/).filter((token) => token && parsePercentRelativeCalc(token) === null).join(" ");
532
+ return classStrRaw.split(/\s+/).filter((token) => token && parsePercentRelativeExpr(token) === null).join(" ");
486
533
  }
487
534
  var STATE_MODIFIER_NAMES = /* @__PURE__ */ new Set(["hover", "focus", "disabled"]);
488
535
  var STATE_MODIFIER_HINT_RE = /(^|\s|:)(hover|focus|disabled):/;
@@ -557,7 +604,7 @@ function ReactiveElement({
557
604
  layoutOverrides = {};
558
605
  for (const calc of percentCalcs) {
559
606
  const basis = measuredBasis?.[calc.property];
560
- layoutOverrides[calc.property] = basis === void 0 ? "100%" : resolvePercentRelativeCalc(basis, calc);
607
+ layoutOverrides[calc.property] = basis === void 0 ? "100%" : calc.resolve(basis);
561
608
  }
562
609
  const userOnLayout = hostRest.onLayout;
563
610
  hostRestWithLayout = {
@@ -1534,6 +1581,77 @@ function nativeShadowDeclarations(parsed) {
1534
1581
  ];
1535
1582
  }
1536
1583
 
1584
+ // src/jsEngine/resolvers/transform.ts
1585
+ function scaleFactor(parsed) {
1586
+ if (parsed.value === null) return null;
1587
+ const n = Number(parsed.value);
1588
+ if (!Number.isFinite(n)) return null;
1589
+ return parsed.isArbitrary ? n : n / 100;
1590
+ }
1591
+ function angle(parsed) {
1592
+ if (parsed.value === null) return null;
1593
+ if (parsed.isArbitrary) return parsed.value;
1594
+ const n = Number(parsed.value);
1595
+ if (!Number.isFinite(n)) return null;
1596
+ return `${parsed.negative ? -n : n}deg`;
1597
+ }
1598
+ function resolveNative(parsed, theme) {
1599
+ switch (parsed.utility) {
1600
+ case "transform-none":
1601
+ return [decl("transform-op-none", "1")];
1602
+ case "backface-visible":
1603
+ return [decl("backface-visibility", "visible")];
1604
+ case "backface-hidden":
1605
+ return [decl("backface-visibility", "hidden")];
1606
+ case "translate-x": {
1607
+ const v = resolveNegatableLength(theme, parsed);
1608
+ return v === null ? null : [decl("transform-op-translate-x", v)];
1609
+ }
1610
+ case "translate-y": {
1611
+ const v = resolveNegatableLength(theme, parsed);
1612
+ return v === null ? null : [decl("transform-op-translate-y", v)];
1613
+ }
1614
+ case "scale": {
1615
+ const n = scaleFactor(parsed);
1616
+ return n === null ? null : [decl("transform-op-scale-x", `${n}`), decl("transform-op-scale-y", `${n}`)];
1617
+ }
1618
+ case "scale-x": {
1619
+ const n = scaleFactor(parsed);
1620
+ return n === null ? null : [decl("transform-op-scale-x", `${n}`)];
1621
+ }
1622
+ case "scale-y": {
1623
+ const n = scaleFactor(parsed);
1624
+ return n === null ? null : [decl("transform-op-scale-y", `${n}`)];
1625
+ }
1626
+ case "rotate": {
1627
+ const deg = angle(parsed);
1628
+ return deg === null ? null : [decl("transform-op-rotate", deg)];
1629
+ }
1630
+ case "rotate-x": {
1631
+ const deg = angle(parsed);
1632
+ return deg === null ? null : [decl("transform-op-rotate-x", deg)];
1633
+ }
1634
+ case "rotate-y": {
1635
+ const deg = angle(parsed);
1636
+ return deg === null ? null : [decl("transform-op-rotate-y", deg)];
1637
+ }
1638
+ case "rotate-z": {
1639
+ const deg = angle(parsed);
1640
+ return deg === null ? null : [decl("transform-op-rotate-z", deg)];
1641
+ }
1642
+ case "skew-x": {
1643
+ const deg = angle(parsed);
1644
+ return deg === null ? null : [decl("transform-op-skew-x", deg)];
1645
+ }
1646
+ case "skew-y": {
1647
+ const deg = angle(parsed);
1648
+ return deg === null ? null : [decl("transform-op-skew-y", deg)];
1649
+ }
1650
+ default:
1651
+ return null;
1652
+ }
1653
+ }
1654
+
1537
1655
  // src/jsEngine/resolveUtilityNative.ts
1538
1656
  function firstFontName(stack) {
1539
1657
  const first = stack.split(",")[0] ?? stack;
@@ -1615,7 +1733,7 @@ function resolveOpacityNative(parsed) {
1615
1733
  return v === null ? null : [decl("opacity", v)];
1616
1734
  }
1617
1735
  function resolveUtilityNative(parsed, theme) {
1618
- 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);
1736
+ 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);
1619
1737
  }
1620
1738
 
1621
1739
  // src/jsEngine/resolveStyle.ts
@@ -1686,8 +1804,32 @@ var NUMERIC_LENGTH_PROPS = /* @__PURE__ */ new Set([
1686
1804
  "border-bottom-right-radius",
1687
1805
  "border-bottom-left-radius",
1688
1806
  "outline-width",
1689
- "outline-offset"
1807
+ "outline-offset",
1808
+ // The transform ops whose RN value is a plain JS number — translate takes
1809
+ // a length (px/percentage, same as top/left/etc. above) and scale takes a
1810
+ // bare decimal factor (parsed by the same "raw value" fallback branch
1811
+ // below). transform-op-rotate*/transform-op-skew* stay OUT of this set on
1812
+ // purpose, since RN wants those as literal "45deg"-shaped strings, not
1813
+ // numbers. See the transform-ops accumulator below for how these markers
1814
+ // get collected into the final array.
1815
+ "transform-op-translate-x",
1816
+ "transform-op-translate-y",
1817
+ "transform-op-scale-x",
1818
+ "transform-op-scale-y"
1690
1819
  ]);
1820
+ var TRANSFORM_OP_KEYS = /* @__PURE__ */ new Map([
1821
+ ["transform-op-translate-x", "translateX"],
1822
+ ["transform-op-translate-y", "translateY"],
1823
+ ["transform-op-rotate", "rotate"],
1824
+ ["transform-op-rotate-x", "rotateX"],
1825
+ ["transform-op-rotate-y", "rotateY"],
1826
+ ["transform-op-rotate-z", "rotateZ"],
1827
+ ["transform-op-skew-x", "skewX"],
1828
+ ["transform-op-skew-y", "skewY"],
1829
+ ["transform-op-scale-x", "scaleX"],
1830
+ ["transform-op-scale-y", "scaleY"]
1831
+ ]);
1832
+ var TRANSFORM_OP_ORDER = ["translateX", "translateY", "rotate", "rotateX", "rotateY", "rotateZ", "skewX", "skewY", "scaleX", "scaleY"];
1691
1833
  function isPlainPercentage(value) {
1692
1834
  if (!value.endsWith("%")) return false;
1693
1835
  let digits = value.slice(0, -1);
@@ -1724,6 +1866,7 @@ function resolveStyleJsWithWarnings(classString, theme, colorScheme, pressed, wi
1724
1866
  const style = {};
1725
1867
  const warnings = [];
1726
1868
  let shadowOffset = null;
1869
+ const transformOps = /* @__PURE__ */ new Map();
1727
1870
  for (const token of classString.split(/\s+/).filter(Boolean)) {
1728
1871
  const parsed = parseClass(token);
1729
1872
  const allModifiersHold = parsed.modifiers.every(
@@ -1743,6 +1886,17 @@ function resolveStyleJsWithWarnings(classString, theme, colorScheme, pressed, wi
1743
1886
  shadowOffset[d.property === "shadow-offset-x" ? "width" : "height"] = n;
1744
1887
  continue;
1745
1888
  }
1889
+ if (d.property === "transform-op-none") {
1890
+ transformOps.clear();
1891
+ continue;
1892
+ }
1893
+ const opKey = TRANSFORM_OP_KEYS.get(d.property);
1894
+ if (opKey !== void 0) {
1895
+ const { value: value2, warning: warning2 } = rnStyleValue(d.property, d.value);
1896
+ if (warning2 !== null) warnings.push(warning2);
1897
+ if (value2 !== null) transformOps.set(opKey, value2);
1898
+ continue;
1899
+ }
1746
1900
  const { value, warning } = rnStyleValue(d.property, d.value);
1747
1901
  if (warning !== null) warnings.push(warning);
1748
1902
  if (value !== null) style[kebabToCamel(d.property)] = value;
@@ -1751,6 +1905,11 @@ function resolveStyleJsWithWarnings(classString, theme, colorScheme, pressed, wi
1751
1905
  if (shadowOffset !== null) {
1752
1906
  style.shadowOffset = { width: shadowOffset.width ?? 0, height: shadowOffset.height ?? 0 };
1753
1907
  }
1908
+ if (transformOps.size > 0) {
1909
+ style.transform = TRANSFORM_OP_ORDER.filter((key) => transformOps.has(key)).map((key) => ({
1910
+ [key]: transformOps.get(key)
1911
+ }));
1912
+ }
1754
1913
  return { style, warnings };
1755
1914
  }
1756
1915
 
@@ -80,18 +80,49 @@ interface ThemeState {
80
80
  */
81
81
  declare function useTheme(): ThemeState;
82
82
 
83
+ /**
84
+ * The exact shape `@react-native-async-storage/async-storage`'s default
85
+ * export already has — pass it directly as `persist`. Any other async
86
+ * key-value store (MMKV, SecureStore, a custom backend, ...) works too, as
87
+ * long as it exposes these two methods with this signature. Kept minimal
88
+ * on purpose: `ThemeProvider` never imports a storage library itself (see
89
+ * `darkModeStore.ts`'s own doc comment on why this package forces no real
90
+ * native dependency on consumers who don't want persistence) — deliberately
91
+ * NOT a `boolean`-only option that reaches for AsyncStorage internally,
92
+ * since Metro resolves every `require()`/`import` target at BUNDLE time
93
+ * regardless of try/catch, so a library-internal "if installed, use it"
94
+ * check can't actually degrade gracefully when the package is absent — an
95
+ * injected adapter sidesteps that entirely.
96
+ */
97
+ interface ThemeStorageAdapter {
98
+ getItem(key: string): Promise<string | null>;
99
+ setItem(key: string, value: string): Promise<void>;
100
+ }
83
101
  interface ThemeProviderProps {
84
102
  children: ReactNode;
85
103
  /**
86
104
  * Applied once, on this provider's first mount, but ONLY if nothing has
87
105
  * made an explicit mode choice yet (see
88
106
  * `darkModeStore.ts::seedDefaultMode`) — an explicit prior choice always
89
- * wins over a component's own default. Unlike @kbach/react's web
90
- * version, there's no persisted value to check here — see
91
- * darkModeStore.ts's own doc comment on why this package doesn't persist
92
- * the mode across app launches.
107
+ * wins over a component's own default. If `persist` is also set and a
108
+ * persisted mode is found, the persisted mode wins over this fallback.
93
109
  */
94
110
  defaultMode?: ThemeMode;
111
+ /**
112
+ * Opt-in persistence across app launches — `false` by default (this
113
+ * package never persists on its own; see `darkModeStore.ts`'s doc
114
+ * comment). Pass a storage adapter to enable it:
115
+ * ```tsx
116
+ * import AsyncStorage from '@react-native-async-storage/async-storage';
117
+ * <ThemeProvider persist={AsyncStorage}>...
118
+ * ```
119
+ * The persisted value (if any) is read once on mount and applied via the
120
+ * same "don't clobber a real choice already in flight" guard
121
+ * `seedDefaultMode`/`defaultMode` uses; every later explicit mode change
122
+ * is written back automatically for as long as this provider stays
123
+ * mounted.
124
+ */
125
+ persist?: ThemeStorageAdapter | false;
95
126
  }
96
127
  /**
97
128
  * Optional — `useTheme()` reads `darkModeStore.ts`'s global store directly
@@ -107,7 +138,7 @@ interface ThemeProviderProps {
107
138
  * state — mounting more than one is very likely a mistake, so this warns
108
139
  * once in dev.
109
140
  */
110
- declare function ThemeProvider({ children, defaultMode }: ThemeProviderProps): react.JSX.Element;
141
+ declare function ThemeProvider({ children, defaultMode, persist }: ThemeProviderProps): react.JSX.Element;
111
142
 
112
143
  interface ColorScale {
113
144
  /** `colors.blue[6]` -> raw value */
@@ -157,4 +188,4 @@ declare function useColors(): ColorsAPI;
157
188
  */
158
189
  declare function useDynamicToken(name: string): string | undefined;
159
190
 
160
- export { type ColorScale as C, type KbachConfig as K, ThemeProvider as T, type ColorsAPI as a, type ThemeProviderProps as b, type ThemeState as c, applyKbachConfig as d, useDynamicToken as e, useTheme as f, resolveKbachConfig as r, useColors as u };
191
+ export { type ColorScale as C, type KbachConfig as K, ThemeProvider as T, type ColorsAPI as a, type ThemeProviderProps as b, type ThemeState as c, type ThemeStorageAdapter as d, applyKbachConfig as e, useDynamicToken as f, useTheme as g, resolveKbachConfig as r, useColors as u };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kbach/react-native",
3
- "version": "1.0.0-beta.21",
3
+ "version": "1.0.0-beta.23",
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",