@kbach/react-native 1.0.0-beta.19 → 1.0.0-beta.20

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
@@ -184,14 +184,36 @@ constant `px`/`rem` length (or a bare number). This works because native has
184
184
  no CSS engine to evaluate a real `calc()` string at paint time, so the only
185
185
  way to support it at all is to compute the answer ourselves.
186
186
 
187
- A **percentage- or viewport-relative** expression (`calc(50%-0.5rem)`,
188
- `calc(10vw+8px)`) can't be reduced this way — it genuinely needs the
189
- parent's actual layout size, which doesn't exist until paint time on
190
- native. These already work fine on web (real CSS); on native the
191
- declaration is dropped and a `console.warn` explains why (dev builds only),
192
- rather than silently shipping an invalid value into the style object. Use a
193
- fraction utility (`w-1/2`) for relative sizing on native instead — see
194
- `resolvers/spacing.rs`'s `fraction_percent` — or resolve the value in JS.
187
+ A **viewport-relative** expression (`calc(10vw+8px)`) can't be reduced this
188
+ way — no such unit exists on native at all. It already works fine on web
189
+ (real CSS); on native the declaration is dropped and a `console.warn`
190
+ explains why (dev builds only).
191
+
192
+ **Percentage-relative width/height** (`calc(100%-3rem)`, `calc(50%+16px)`)
193
+ IS supported, differently: since native has no way to know "100% of what"
194
+ ahead of time, this measures the element's own real layout instead —
195
+
196
+ ```tsx
197
+ <View className="w-[calc(100%_-_3rem)]" />
198
+ ```
199
+
200
+ Renders once at a plain `100%` (a real RN percentage, resolved correctly by
201
+ RN's own layout engine against the parent) purely to trigger a layout
202
+ event, then immediately corrects to the actual computed number once
203
+ measured — a genuine one-frame measure-then-snap, not instant math like the
204
+ constant-only case above, and it keeps re-measuring on every layout (screen
205
+ rotation, parent resize, ...) rather than freezing after the first value.
206
+ Scoped to exactly one percentage term plus one constant term on `width`/
207
+ `height` only — `calc(50%-50%)`, other properties, or anything with `*`/`/`
208
+ falls through to the same drop-and-warn behavior as before.
209
+
210
+ **Arbitrary values can't contain literal spaces** — this is a pre-existing
211
+ rule (real Tailwind has the same one), not specific to `calc()`: a space
212
+ inside `[...]` is indistinguishable from the whitespace that separates
213
+ class tokens in the first place, so `w-[calc(100% - 3rem)]` silently splits
214
+ into three broken tokens before `calc()` is ever even recognized. Write
215
+ `w-[calc(100%_-_3rem)]` (underscores) instead — this is exactly what the
216
+ unknown-class warning below is for: it'll tell you when this happens.
195
217
 
196
218
  ## Unknown-class warnings (typo detection)
197
219
 
@@ -32,6 +32,182 @@ var import_react_native = require("react-native");
32
32
  var import_darkModeStore = require("./darkModeStore");
33
33
  var import_dynamicTokens = require("./dynamicTokens");
34
34
  var import_theme = require("./theme");
35
+
36
+ // src/jsEngine/calc.ts
37
+ function reduceConstantMath(raw) {
38
+ const trimmed = raw.trim();
39
+ const calc = stripCall(trimmed, "calc");
40
+ if (calc !== null) {
41
+ return new Evaluator(calc).parseExprToEnd();
42
+ }
43
+ const clamp = stripCall(trimmed, "clamp");
44
+ if (clamp !== null) {
45
+ const args = splitTopLevelCommas(clamp);
46
+ if (args.length !== 3) return null;
47
+ const [min2, preferred, max2] = args.map((a) => new Evaluator(a).parseExprToEnd());
48
+ if (min2 === null || preferred === null || max2 === null) return null;
49
+ return Math.max(min2, Math.min(preferred, max2));
50
+ }
51
+ const min = stripCall(trimmed, "min");
52
+ if (min !== null) {
53
+ const values = splitTopLevelCommas(min).map((a) => new Evaluator(a).parseExprToEnd());
54
+ return values.some((v) => v === null) ? null : Math.min(...values);
55
+ }
56
+ const max = stripCall(trimmed, "max");
57
+ if (max !== null) {
58
+ const values = splitTopLevelCommas(max).map((a) => new Evaluator(a).parseExprToEnd());
59
+ return values.some((v) => v === null) ? null : Math.max(...values);
60
+ }
61
+ return null;
62
+ }
63
+ function stripCall(raw, name) {
64
+ if (!raw.startsWith(name)) return null;
65
+ const afterName = raw.slice(name.length).trimStart();
66
+ if (!afterName.startsWith("(") || !afterName.endsWith(")")) return null;
67
+ return afterName.slice(1, -1);
68
+ }
69
+ function splitTopLevelCommas(s) {
70
+ const parts = [];
71
+ let depth = 0;
72
+ let start = 0;
73
+ for (let i = 0; i < s.length; i++) {
74
+ const ch = s[i];
75
+ if (ch === "(") depth++;
76
+ else if (ch === ")") depth--;
77
+ else if (ch === "," && depth === 0) {
78
+ parts.push(s.slice(start, i).trim());
79
+ start = i + 1;
80
+ }
81
+ }
82
+ parts.push(s.slice(start).trim());
83
+ return parts;
84
+ }
85
+ var Evaluator = class {
86
+ constructor(s) {
87
+ this.pos = 0;
88
+ this.s = s;
89
+ }
90
+ parseExprToEnd() {
91
+ const value = this.expr();
92
+ if (value === null) return null;
93
+ this.skipWs();
94
+ return this.pos === this.s.length ? value : null;
95
+ }
96
+ skipWs() {
97
+ while (this.pos < this.s.length && /\s/.test(this.s[this.pos])) this.pos++;
98
+ }
99
+ peek() {
100
+ this.skipWs();
101
+ return this.s[this.pos];
102
+ }
103
+ expr() {
104
+ let value = this.term();
105
+ if (value === null) return null;
106
+ for (; ; ) {
107
+ const ch = this.peek();
108
+ if (ch === "+") {
109
+ this.pos++;
110
+ const rhs = this.term();
111
+ if (rhs === null) return null;
112
+ value += rhs;
113
+ } else if (ch === "-") {
114
+ this.pos++;
115
+ const rhs = this.term();
116
+ if (rhs === null) return null;
117
+ value -= rhs;
118
+ } else {
119
+ return value;
120
+ }
121
+ }
122
+ }
123
+ term() {
124
+ let value = this.factor();
125
+ if (value === null) return null;
126
+ for (; ; ) {
127
+ const ch = this.peek();
128
+ if (ch === "*") {
129
+ this.pos++;
130
+ const rhs = this.factor();
131
+ if (rhs === null) return null;
132
+ value *= rhs;
133
+ } else if (ch === "/") {
134
+ this.pos++;
135
+ const divisor = this.factor();
136
+ if (divisor === null || divisor === 0) return null;
137
+ value /= divisor;
138
+ } else {
139
+ return value;
140
+ }
141
+ }
142
+ }
143
+ factor() {
144
+ const ch = this.peek();
145
+ if (ch === void 0) return null;
146
+ if (ch === "(") {
147
+ this.pos++;
148
+ const value = this.expr();
149
+ if (value === null || this.peek() !== ")") return null;
150
+ this.pos++;
151
+ return value;
152
+ }
153
+ if (ch === "-") {
154
+ this.pos++;
155
+ const value = this.factor();
156
+ return value === null ? null : -value;
157
+ }
158
+ return this.numberWithUnit();
159
+ }
160
+ numberWithUnit() {
161
+ this.skipWs();
162
+ const start = this.pos;
163
+ while (this.pos < this.s.length && (/[0-9]/.test(this.s[this.pos]) || this.s[this.pos] === ".")) this.pos++;
164
+ if (this.pos === start) return null;
165
+ const n = Number(this.s.slice(start, this.pos));
166
+ if (!Number.isFinite(n)) return null;
167
+ const unitStart = this.pos;
168
+ while (this.pos < this.s.length && /[a-zA-Z]/.test(this.s[this.pos])) this.pos++;
169
+ const unit = this.s.slice(unitStart, this.pos);
170
+ switch (unit) {
171
+ case "":
172
+ return n;
173
+ case "px":
174
+ return n;
175
+ case "rem":
176
+ return n * 16;
177
+ default:
178
+ return null;
179
+ }
180
+ }
181
+ };
182
+
183
+ // src/layoutCalc.ts
184
+ var PERCENT_CALC_TOKEN_RE = /^(w|h)-\[calc\(([^)]+)\)\]$/;
185
+ function parsePercentRelativeCalc(token) {
186
+ const m = PERCENT_CALC_TOKEN_RE.exec(token);
187
+ if (!m) return null;
188
+ const property = m[1] === "w" ? "width" : "height";
189
+ const inner = m[2].replace(/_/g, " ").trim();
190
+ let mm = /^(-?[\d.]+)%\s*([+-])\s*(.+)$/.exec(inner);
191
+ if (mm) {
192
+ const percentCoefficient = Number(mm[1]);
193
+ const magnitude = reduceConstantMath(`calc(${mm[3].trim()})`);
194
+ if (!Number.isFinite(percentCoefficient) || magnitude === null) return null;
195
+ return { property, percentCoefficient, constantPx: mm[2] === "+" ? magnitude : -magnitude };
196
+ }
197
+ mm = /^(.+?)\s*([+-])\s*(-?[\d.]+)%$/.exec(inner);
198
+ if (mm) {
199
+ const constantPx = reduceConstantMath(`calc(${mm[1].trim()})`);
200
+ const percentMagnitude = Number(mm[3]);
201
+ if (constantPx === null || !Number.isFinite(percentMagnitude)) return null;
202
+ return { property, percentCoefficient: mm[2] === "+" ? percentMagnitude : -percentMagnitude, constantPx };
203
+ }
204
+ return null;
205
+ }
206
+ function resolvePercentRelativeCalc(basisPx, calc) {
207
+ return basisPx * calc.percentCoefficient / 100 + calc.constantPx;
208
+ }
209
+
210
+ // src/jsxRuntimeCore.ts
35
211
  var DYNAMIC_TOKEN_MATCH_RE = /var\(--([a-zA-Z0-9-]+)\)/g;
36
212
  function substituteDynamicTokens(classStrRaw) {
37
213
  if (!classStrRaw.includes("var(--")) return classStrRaw;
@@ -40,11 +216,27 @@ function substituteDynamicTokens(classStrRaw) {
40
216
  return value ?? whole;
41
217
  });
42
218
  }
219
+ var PERCENT_RELATIVE_CALC_HINT_RE = /[wh]-\[calc\([^)]*%[^)]*\)\]/;
220
+ function extractPercentRelativeCalcs(classStrRaw) {
221
+ const found = [];
222
+ for (const token of classStrRaw.split(/\s+/)) {
223
+ const parsed = parsePercentRelativeCalc(token);
224
+ if (parsed) found.push(parsed);
225
+ }
226
+ return found;
227
+ }
228
+ function stripPercentRelativeCalcTokens(classStrRaw) {
229
+ return classStrRaw.split(/\s+/).filter((token) => token && parsePercentRelativeCalc(token) === null).join(" ");
230
+ }
43
231
  function makeElement(isStaticChildren, type, props, key) {
44
232
  return (isStaticChildren ? import_jsx_runtime.jsxs : import_jsx_runtime.jsx)(type, props, key);
45
233
  }
46
- function resolvedStyleFor(resolveStyle2, classStrRaw, userStyle, pressed) {
47
- const resolved = resolveStyle2(substituteDynamicTokens(classStrRaw), pressed);
234
+ function resolvedStyleFor(resolveStyle2, classStrRaw, userStyle, pressed, layoutOverrides) {
235
+ const cleaned = stripPercentRelativeCalcTokens(substituteDynamicTokens(classStrRaw));
236
+ const resolved = resolveStyle2(cleaned, pressed);
237
+ if (layoutOverrides) {
238
+ Object.assign(resolved, layoutOverrides);
239
+ }
48
240
  if (userStyle === void 0) {
49
241
  return resolved;
50
242
  }
@@ -75,6 +267,14 @@ function dynamicTokenKeySuffix(classStrRaw) {
75
267
  const values = matches.map((m) => (0, import_dynamicTokens.getDynamicToken)(m.slice("var(--".length, -1)) ?? "");
76
268
  return `:kb-dt-${values.join("|")}`;
77
269
  }
270
+ function percentCalcKeySuffix(percentCalcs, measuredBasis) {
271
+ if (percentCalcs.length === 0) return "";
272
+ const values = percentCalcs.map((calc) => {
273
+ const basis = measuredBasis?.[calc.property];
274
+ return basis === void 0 ? "pending" : resolvePercentRelativeCalc(basis, calc);
275
+ });
276
+ return `:kb-pc-${values.join("|")}`;
277
+ }
78
278
  function ReactiveElement({
79
279
  hostType,
80
280
  hostRest,
@@ -87,10 +287,33 @@ function ReactiveElement({
87
287
  (0, import_react.useSyncExternalStore)(import_darkModeStore.subscribeGlobalDarkMode, import_darkModeStore.getGlobalDarkMode);
88
288
  (0, import_react.useSyncExternalStore)(import_dynamicTokens.subscribeDynamicTokens, import_dynamicTokens.getDynamicTokensVersion);
89
289
  const { width } = (0, import_react_native.useWindowDimensions)();
90
- const finalStyle = hostType === import_react_native.Pressable ? (state) => resolvedStyleFor(resolveStyle2, classStrRaw, userStyle, state.pressed) : resolvedStyleFor(resolveStyle2, classStrRaw, userStyle, false);
91
- const suffix = darkModeKeySuffix(classStrRaw) + breakpointKeySuffix(classStrRaw, width) + dynamicTokenKeySuffix(classStrRaw);
290
+ const [measuredBasis, setMeasuredBasis] = (0, import_react.useState)(null);
291
+ const measure = (0, import_react.useCallback)((e) => {
292
+ const { width: w, height: h } = e.nativeEvent.layout;
293
+ setMeasuredBasis((prev) => prev && prev.width === w && prev.height === h ? prev : { width: w, height: h });
294
+ }, []);
295
+ const percentCalcs = extractPercentRelativeCalcs(classStrRaw);
296
+ let layoutOverrides;
297
+ let hostRestWithLayout = hostRest;
298
+ if (percentCalcs.length > 0) {
299
+ layoutOverrides = {};
300
+ for (const calc of percentCalcs) {
301
+ const basis = measuredBasis?.[calc.property];
302
+ layoutOverrides[calc.property] = basis === void 0 ? "100%" : resolvePercentRelativeCalc(basis, calc);
303
+ }
304
+ const userOnLayout = hostRest.onLayout;
305
+ hostRestWithLayout = {
306
+ ...hostRest,
307
+ onLayout: userOnLayout ? (e) => {
308
+ measure(e);
309
+ userOnLayout(e);
310
+ } : measure
311
+ };
312
+ }
313
+ const finalStyle = hostType === import_react_native.Pressable ? (state) => resolvedStyleFor(resolveStyle2, classStrRaw, userStyle, state.pressed, layoutOverrides) : resolvedStyleFor(resolveStyle2, classStrRaw, userStyle, false, layoutOverrides);
314
+ const suffix = darkModeKeySuffix(classStrRaw) + breakpointKeySuffix(classStrRaw, width) + dynamicTokenKeySuffix(classStrRaw) + percentCalcKeySuffix(percentCalcs, measuredBasis);
92
315
  const finalKey = suffix ? `${elementKey ?? ""}${suffix}` : elementKey;
93
- return makeElement(isStaticChildren, hostType, { ...hostRest, style: finalStyle }, finalKey);
316
+ return makeElement(isStaticChildren, hostType, { ...hostRestWithLayout, style: finalStyle }, finalKey);
94
317
  }
95
318
  function processElement(resolveStyle2, type, rawProps, key, isStaticChildren) {
96
319
  if (type === null || typeof type === "symbol" || !rawProps) {
@@ -100,7 +323,7 @@ function processElement(resolveStyle2, type, rawProps, key, isStaticChildren) {
100
323
  if (typeof classStrRaw !== "string") {
101
324
  return makeElement(isStaticChildren, type, rawProps, key);
102
325
  }
103
- if (DARK_MODIFIER_RE.test(classStrRaw) || BREAKPOINT_MODIFIER_RE.test(classStrRaw) || classStrRaw.includes("var(--")) {
326
+ if (DARK_MODIFIER_RE.test(classStrRaw) || BREAKPOINT_MODIFIER_RE.test(classStrRaw) || classStrRaw.includes("var(--") || PERCENT_RELATIVE_CALC_HINT_RE.test(classStrRaw)) {
104
327
  return (0, import_jsx_runtime.jsx)(
105
328
  ReactiveElement,
106
329
  { hostType: type, hostRest: rest, classStrRaw, userStyle, resolveStyle: resolveStyle2, elementKey: key, isStaticChildren },
@@ -1231,153 +1454,6 @@ function resolveUtilityNative(parsed, theme) {
1231
1454
  return resolveFlex(parsed, false) ?? resolve(parsed, theme) ?? resolve2(parsed, theme) ?? resolve3(parsed, theme) ?? resolveColorNative(parsed, theme) ?? resolveLeadingTrackingNative(parsed) ?? resolveTypographyNative(parsed) ?? nativeShadowDeclarations(parsed);
1232
1455
  }
1233
1456
 
1234
- // src/jsEngine/calc.ts
1235
- function reduceConstantMath(raw) {
1236
- const trimmed = raw.trim();
1237
- const calc = stripCall(trimmed, "calc");
1238
- if (calc !== null) {
1239
- return new Evaluator(calc).parseExprToEnd();
1240
- }
1241
- const clamp = stripCall(trimmed, "clamp");
1242
- if (clamp !== null) {
1243
- const args = splitTopLevelCommas(clamp);
1244
- if (args.length !== 3) return null;
1245
- const [min2, preferred, max2] = args.map((a) => new Evaluator(a).parseExprToEnd());
1246
- if (min2 === null || preferred === null || max2 === null) return null;
1247
- return Math.max(min2, Math.min(preferred, max2));
1248
- }
1249
- const min = stripCall(trimmed, "min");
1250
- if (min !== null) {
1251
- const values = splitTopLevelCommas(min).map((a) => new Evaluator(a).parseExprToEnd());
1252
- return values.some((v) => v === null) ? null : Math.min(...values);
1253
- }
1254
- const max = stripCall(trimmed, "max");
1255
- if (max !== null) {
1256
- const values = splitTopLevelCommas(max).map((a) => new Evaluator(a).parseExprToEnd());
1257
- return values.some((v) => v === null) ? null : Math.max(...values);
1258
- }
1259
- return null;
1260
- }
1261
- function stripCall(raw, name) {
1262
- if (!raw.startsWith(name)) return null;
1263
- const afterName = raw.slice(name.length).trimStart();
1264
- if (!afterName.startsWith("(") || !afterName.endsWith(")")) return null;
1265
- return afterName.slice(1, -1);
1266
- }
1267
- function splitTopLevelCommas(s) {
1268
- const parts = [];
1269
- let depth = 0;
1270
- let start = 0;
1271
- for (let i = 0; i < s.length; i++) {
1272
- const ch = s[i];
1273
- if (ch === "(") depth++;
1274
- else if (ch === ")") depth--;
1275
- else if (ch === "," && depth === 0) {
1276
- parts.push(s.slice(start, i).trim());
1277
- start = i + 1;
1278
- }
1279
- }
1280
- parts.push(s.slice(start).trim());
1281
- return parts;
1282
- }
1283
- var Evaluator = class {
1284
- constructor(s) {
1285
- this.pos = 0;
1286
- this.s = s;
1287
- }
1288
- parseExprToEnd() {
1289
- const value = this.expr();
1290
- if (value === null) return null;
1291
- this.skipWs();
1292
- return this.pos === this.s.length ? value : null;
1293
- }
1294
- skipWs() {
1295
- while (this.pos < this.s.length && /\s/.test(this.s[this.pos])) this.pos++;
1296
- }
1297
- peek() {
1298
- this.skipWs();
1299
- return this.s[this.pos];
1300
- }
1301
- expr() {
1302
- let value = this.term();
1303
- if (value === null) return null;
1304
- for (; ; ) {
1305
- const ch = this.peek();
1306
- if (ch === "+") {
1307
- this.pos++;
1308
- const rhs = this.term();
1309
- if (rhs === null) return null;
1310
- value += rhs;
1311
- } else if (ch === "-") {
1312
- this.pos++;
1313
- const rhs = this.term();
1314
- if (rhs === null) return null;
1315
- value -= rhs;
1316
- } else {
1317
- return value;
1318
- }
1319
- }
1320
- }
1321
- term() {
1322
- let value = this.factor();
1323
- if (value === null) return null;
1324
- for (; ; ) {
1325
- const ch = this.peek();
1326
- if (ch === "*") {
1327
- this.pos++;
1328
- const rhs = this.factor();
1329
- if (rhs === null) return null;
1330
- value *= rhs;
1331
- } else if (ch === "/") {
1332
- this.pos++;
1333
- const divisor = this.factor();
1334
- if (divisor === null || divisor === 0) return null;
1335
- value /= divisor;
1336
- } else {
1337
- return value;
1338
- }
1339
- }
1340
- }
1341
- factor() {
1342
- const ch = this.peek();
1343
- if (ch === void 0) return null;
1344
- if (ch === "(") {
1345
- this.pos++;
1346
- const value = this.expr();
1347
- if (value === null || this.peek() !== ")") return null;
1348
- this.pos++;
1349
- return value;
1350
- }
1351
- if (ch === "-") {
1352
- this.pos++;
1353
- const value = this.factor();
1354
- return value === null ? null : -value;
1355
- }
1356
- return this.numberWithUnit();
1357
- }
1358
- numberWithUnit() {
1359
- this.skipWs();
1360
- const start = this.pos;
1361
- while (this.pos < this.s.length && (/[0-9]/.test(this.s[this.pos]) || this.s[this.pos] === ".")) this.pos++;
1362
- if (this.pos === start) return null;
1363
- const n = Number(this.s.slice(start, this.pos));
1364
- if (!Number.isFinite(n)) return null;
1365
- const unitStart = this.pos;
1366
- while (this.pos < this.s.length && /[a-zA-Z]/.test(this.s[this.pos])) this.pos++;
1367
- const unit = this.s.slice(unitStart, this.pos);
1368
- switch (unit) {
1369
- case "":
1370
- return n;
1371
- case "px":
1372
- return n;
1373
- case "rem":
1374
- return n * 16;
1375
- default:
1376
- return null;
1377
- }
1378
- }
1379
- };
1380
-
1381
1457
  // src/jsEngine/resolveStyle.ts
1382
1458
  var RESPONSIVE_BREAKPOINTS = /* @__PURE__ */ new Set(["sm", "md", "lg", "xl", "2xl"]);
1383
1459
  function nativeModifierState(modifier, theme, colorScheme, pressed, width) {
@@ -8,7 +8,7 @@ import "./chunk-OGFB3OXS.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, useSyncExternalStore } from "react";
11
+ import { Fragment as Fragment2, useCallback, 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) {
@@ -33,6 +33,182 @@ var import_react_native = require("react-native");
33
33
  var import_darkModeStore = require("./darkModeStore");
34
34
  var import_dynamicTokens = require("./dynamicTokens");
35
35
  var import_theme = require("./theme");
36
+
37
+ // src/jsEngine/calc.ts
38
+ function reduceConstantMath(raw) {
39
+ const trimmed = raw.trim();
40
+ const calc = stripCall(trimmed, "calc");
41
+ if (calc !== null) {
42
+ return new Evaluator(calc).parseExprToEnd();
43
+ }
44
+ const clamp = stripCall(trimmed, "clamp");
45
+ if (clamp !== null) {
46
+ const args = splitTopLevelCommas(clamp);
47
+ if (args.length !== 3) return null;
48
+ const [min2, preferred, max2] = args.map((a) => new Evaluator(a).parseExprToEnd());
49
+ if (min2 === null || preferred === null || max2 === null) return null;
50
+ return Math.max(min2, Math.min(preferred, max2));
51
+ }
52
+ const min = stripCall(trimmed, "min");
53
+ if (min !== null) {
54
+ const values = splitTopLevelCommas(min).map((a) => new Evaluator(a).parseExprToEnd());
55
+ return values.some((v) => v === null) ? null : Math.min(...values);
56
+ }
57
+ const max = stripCall(trimmed, "max");
58
+ if (max !== null) {
59
+ const values = splitTopLevelCommas(max).map((a) => new Evaluator(a).parseExprToEnd());
60
+ return values.some((v) => v === null) ? null : Math.max(...values);
61
+ }
62
+ return null;
63
+ }
64
+ function stripCall(raw, name) {
65
+ if (!raw.startsWith(name)) return null;
66
+ const afterName = raw.slice(name.length).trimStart();
67
+ if (!afterName.startsWith("(") || !afterName.endsWith(")")) return null;
68
+ return afterName.slice(1, -1);
69
+ }
70
+ function splitTopLevelCommas(s) {
71
+ const parts = [];
72
+ let depth = 0;
73
+ let start = 0;
74
+ for (let i = 0; i < s.length; i++) {
75
+ const ch = s[i];
76
+ if (ch === "(") depth++;
77
+ else if (ch === ")") depth--;
78
+ else if (ch === "," && depth === 0) {
79
+ parts.push(s.slice(start, i).trim());
80
+ start = i + 1;
81
+ }
82
+ }
83
+ parts.push(s.slice(start).trim());
84
+ return parts;
85
+ }
86
+ var Evaluator = class {
87
+ constructor(s) {
88
+ this.pos = 0;
89
+ this.s = s;
90
+ }
91
+ parseExprToEnd() {
92
+ const value = this.expr();
93
+ if (value === null) return null;
94
+ this.skipWs();
95
+ return this.pos === this.s.length ? value : null;
96
+ }
97
+ skipWs() {
98
+ while (this.pos < this.s.length && /\s/.test(this.s[this.pos])) this.pos++;
99
+ }
100
+ peek() {
101
+ this.skipWs();
102
+ return this.s[this.pos];
103
+ }
104
+ expr() {
105
+ let value = this.term();
106
+ if (value === null) return null;
107
+ for (; ; ) {
108
+ const ch = this.peek();
109
+ if (ch === "+") {
110
+ this.pos++;
111
+ const rhs = this.term();
112
+ if (rhs === null) return null;
113
+ value += rhs;
114
+ } else if (ch === "-") {
115
+ this.pos++;
116
+ const rhs = this.term();
117
+ if (rhs === null) return null;
118
+ value -= rhs;
119
+ } else {
120
+ return value;
121
+ }
122
+ }
123
+ }
124
+ term() {
125
+ let value = this.factor();
126
+ if (value === null) return null;
127
+ for (; ; ) {
128
+ const ch = this.peek();
129
+ if (ch === "*") {
130
+ this.pos++;
131
+ const rhs = this.factor();
132
+ if (rhs === null) return null;
133
+ value *= rhs;
134
+ } else if (ch === "/") {
135
+ this.pos++;
136
+ const divisor = this.factor();
137
+ if (divisor === null || divisor === 0) return null;
138
+ value /= divisor;
139
+ } else {
140
+ return value;
141
+ }
142
+ }
143
+ }
144
+ factor() {
145
+ const ch = this.peek();
146
+ if (ch === void 0) return null;
147
+ if (ch === "(") {
148
+ this.pos++;
149
+ const value = this.expr();
150
+ if (value === null || this.peek() !== ")") return null;
151
+ this.pos++;
152
+ return value;
153
+ }
154
+ if (ch === "-") {
155
+ this.pos++;
156
+ const value = this.factor();
157
+ return value === null ? null : -value;
158
+ }
159
+ return this.numberWithUnit();
160
+ }
161
+ numberWithUnit() {
162
+ this.skipWs();
163
+ const start = this.pos;
164
+ while (this.pos < this.s.length && (/[0-9]/.test(this.s[this.pos]) || this.s[this.pos] === ".")) this.pos++;
165
+ if (this.pos === start) return null;
166
+ const n = Number(this.s.slice(start, this.pos));
167
+ if (!Number.isFinite(n)) return null;
168
+ const unitStart = this.pos;
169
+ while (this.pos < this.s.length && /[a-zA-Z]/.test(this.s[this.pos])) this.pos++;
170
+ const unit = this.s.slice(unitStart, this.pos);
171
+ switch (unit) {
172
+ case "":
173
+ return n;
174
+ case "px":
175
+ return n;
176
+ case "rem":
177
+ return n * 16;
178
+ default:
179
+ return null;
180
+ }
181
+ }
182
+ };
183
+
184
+ // src/layoutCalc.ts
185
+ var PERCENT_CALC_TOKEN_RE = /^(w|h)-\[calc\(([^)]+)\)\]$/;
186
+ function parsePercentRelativeCalc(token) {
187
+ const m = PERCENT_CALC_TOKEN_RE.exec(token);
188
+ if (!m) return null;
189
+ const property = m[1] === "w" ? "width" : "height";
190
+ const inner = m[2].replace(/_/g, " ").trim();
191
+ let mm = /^(-?[\d.]+)%\s*([+-])\s*(.+)$/.exec(inner);
192
+ if (mm) {
193
+ const percentCoefficient = Number(mm[1]);
194
+ const magnitude = reduceConstantMath(`calc(${mm[3].trim()})`);
195
+ if (!Number.isFinite(percentCoefficient) || magnitude === null) return null;
196
+ return { property, percentCoefficient, constantPx: mm[2] === "+" ? magnitude : -magnitude };
197
+ }
198
+ mm = /^(.+?)\s*([+-])\s*(-?[\d.]+)%$/.exec(inner);
199
+ if (mm) {
200
+ const constantPx = reduceConstantMath(`calc(${mm[1].trim()})`);
201
+ const percentMagnitude = Number(mm[3]);
202
+ if (constantPx === null || !Number.isFinite(percentMagnitude)) return null;
203
+ return { property, percentCoefficient: mm[2] === "+" ? percentMagnitude : -percentMagnitude, constantPx };
204
+ }
205
+ return null;
206
+ }
207
+ function resolvePercentRelativeCalc(basisPx, calc) {
208
+ return basisPx * calc.percentCoefficient / 100 + calc.constantPx;
209
+ }
210
+
211
+ // src/jsxRuntimeCore.ts
36
212
  var DYNAMIC_TOKEN_MATCH_RE = /var\(--([a-zA-Z0-9-]+)\)/g;
37
213
  function substituteDynamicTokens(classStrRaw) {
38
214
  if (!classStrRaw.includes("var(--")) return classStrRaw;
@@ -41,11 +217,27 @@ function substituteDynamicTokens(classStrRaw) {
41
217
  return value ?? whole;
42
218
  });
43
219
  }
220
+ var PERCENT_RELATIVE_CALC_HINT_RE = /[wh]-\[calc\([^)]*%[^)]*\)\]/;
221
+ function extractPercentRelativeCalcs(classStrRaw) {
222
+ const found = [];
223
+ for (const token of classStrRaw.split(/\s+/)) {
224
+ const parsed = parsePercentRelativeCalc(token);
225
+ if (parsed) found.push(parsed);
226
+ }
227
+ return found;
228
+ }
229
+ function stripPercentRelativeCalcTokens(classStrRaw) {
230
+ return classStrRaw.split(/\s+/).filter((token) => token && parsePercentRelativeCalc(token) === null).join(" ");
231
+ }
44
232
  function makeElement(isStaticChildren, type, props, key) {
45
233
  return (isStaticChildren ? import_jsx_runtime.jsxs : import_jsx_runtime.jsx)(type, props, key);
46
234
  }
47
- function resolvedStyleFor(resolveStyle2, classStrRaw, userStyle, pressed) {
48
- const resolved = resolveStyle2(substituteDynamicTokens(classStrRaw), pressed);
235
+ function resolvedStyleFor(resolveStyle2, classStrRaw, userStyle, pressed, layoutOverrides) {
236
+ const cleaned = stripPercentRelativeCalcTokens(substituteDynamicTokens(classStrRaw));
237
+ const resolved = resolveStyle2(cleaned, pressed);
238
+ if (layoutOverrides) {
239
+ Object.assign(resolved, layoutOverrides);
240
+ }
49
241
  if (userStyle === void 0) {
50
242
  return resolved;
51
243
  }
@@ -76,6 +268,14 @@ function dynamicTokenKeySuffix(classStrRaw) {
76
268
  const values = matches.map((m) => (0, import_dynamicTokens.getDynamicToken)(m.slice("var(--".length, -1)) ?? "");
77
269
  return `:kb-dt-${values.join("|")}`;
78
270
  }
271
+ function percentCalcKeySuffix(percentCalcs, measuredBasis) {
272
+ if (percentCalcs.length === 0) return "";
273
+ const values = percentCalcs.map((calc) => {
274
+ const basis = measuredBasis?.[calc.property];
275
+ return basis === void 0 ? "pending" : resolvePercentRelativeCalc(basis, calc);
276
+ });
277
+ return `:kb-pc-${values.join("|")}`;
278
+ }
79
279
  function ReactiveElement({
80
280
  hostType,
81
281
  hostRest,
@@ -88,10 +288,33 @@ function ReactiveElement({
88
288
  (0, import_react.useSyncExternalStore)(import_darkModeStore.subscribeGlobalDarkMode, import_darkModeStore.getGlobalDarkMode);
89
289
  (0, import_react.useSyncExternalStore)(import_dynamicTokens.subscribeDynamicTokens, import_dynamicTokens.getDynamicTokensVersion);
90
290
  const { width } = (0, import_react_native.useWindowDimensions)();
91
- const finalStyle = hostType === import_react_native.Pressable ? (state) => resolvedStyleFor(resolveStyle2, classStrRaw, userStyle, state.pressed) : resolvedStyleFor(resolveStyle2, classStrRaw, userStyle, false);
92
- const suffix = darkModeKeySuffix(classStrRaw) + breakpointKeySuffix(classStrRaw, width) + dynamicTokenKeySuffix(classStrRaw);
291
+ const [measuredBasis, setMeasuredBasis] = (0, import_react.useState)(null);
292
+ const measure = (0, import_react.useCallback)((e) => {
293
+ const { width: w, height: h } = e.nativeEvent.layout;
294
+ setMeasuredBasis((prev) => prev && prev.width === w && prev.height === h ? prev : { width: w, height: h });
295
+ }, []);
296
+ const percentCalcs = extractPercentRelativeCalcs(classStrRaw);
297
+ let layoutOverrides;
298
+ let hostRestWithLayout = hostRest;
299
+ if (percentCalcs.length > 0) {
300
+ layoutOverrides = {};
301
+ for (const calc of percentCalcs) {
302
+ const basis = measuredBasis?.[calc.property];
303
+ layoutOverrides[calc.property] = basis === void 0 ? "100%" : resolvePercentRelativeCalc(basis, calc);
304
+ }
305
+ const userOnLayout = hostRest.onLayout;
306
+ hostRestWithLayout = {
307
+ ...hostRest,
308
+ onLayout: userOnLayout ? (e) => {
309
+ measure(e);
310
+ userOnLayout(e);
311
+ } : measure
312
+ };
313
+ }
314
+ const finalStyle = hostType === import_react_native.Pressable ? (state) => resolvedStyleFor(resolveStyle2, classStrRaw, userStyle, state.pressed, layoutOverrides) : resolvedStyleFor(resolveStyle2, classStrRaw, userStyle, false, layoutOverrides);
315
+ const suffix = darkModeKeySuffix(classStrRaw) + breakpointKeySuffix(classStrRaw, width) + dynamicTokenKeySuffix(classStrRaw) + percentCalcKeySuffix(percentCalcs, measuredBasis);
93
316
  const finalKey = suffix ? `${elementKey ?? ""}${suffix}` : elementKey;
94
- return makeElement(isStaticChildren, hostType, { ...hostRest, style: finalStyle }, finalKey);
317
+ return makeElement(isStaticChildren, hostType, { ...hostRestWithLayout, style: finalStyle }, finalKey);
95
318
  }
96
319
  function processElement(resolveStyle2, type, rawProps, key, isStaticChildren) {
97
320
  if (type === null || typeof type === "symbol" || !rawProps) {
@@ -101,7 +324,7 @@ function processElement(resolveStyle2, type, rawProps, key, isStaticChildren) {
101
324
  if (typeof classStrRaw !== "string") {
102
325
  return makeElement(isStaticChildren, type, rawProps, key);
103
326
  }
104
- if (DARK_MODIFIER_RE.test(classStrRaw) || BREAKPOINT_MODIFIER_RE.test(classStrRaw) || classStrRaw.includes("var(--")) {
327
+ if (DARK_MODIFIER_RE.test(classStrRaw) || BREAKPOINT_MODIFIER_RE.test(classStrRaw) || classStrRaw.includes("var(--") || PERCENT_RELATIVE_CALC_HINT_RE.test(classStrRaw)) {
105
328
  return (0, import_jsx_runtime.jsx)(
106
329
  ReactiveElement,
107
330
  { hostType: type, hostRest: rest, classStrRaw, userStyle, resolveStyle: resolveStyle2, elementKey: key, isStaticChildren },
@@ -1222,153 +1445,6 @@ function resolveUtilityNative(parsed, theme) {
1222
1445
  return resolveFlex(parsed, false) ?? resolve(parsed, theme) ?? resolve2(parsed, theme) ?? resolve3(parsed, theme) ?? resolveColorNative(parsed, theme) ?? resolveLeadingTrackingNative(parsed) ?? resolveTypographyNative(parsed) ?? nativeShadowDeclarations(parsed);
1223
1446
  }
1224
1447
 
1225
- // src/jsEngine/calc.ts
1226
- function reduceConstantMath(raw) {
1227
- const trimmed = raw.trim();
1228
- const calc = stripCall(trimmed, "calc");
1229
- if (calc !== null) {
1230
- return new Evaluator(calc).parseExprToEnd();
1231
- }
1232
- const clamp = stripCall(trimmed, "clamp");
1233
- if (clamp !== null) {
1234
- const args = splitTopLevelCommas(clamp);
1235
- if (args.length !== 3) return null;
1236
- const [min2, preferred, max2] = args.map((a) => new Evaluator(a).parseExprToEnd());
1237
- if (min2 === null || preferred === null || max2 === null) return null;
1238
- return Math.max(min2, Math.min(preferred, max2));
1239
- }
1240
- const min = stripCall(trimmed, "min");
1241
- if (min !== null) {
1242
- const values = splitTopLevelCommas(min).map((a) => new Evaluator(a).parseExprToEnd());
1243
- return values.some((v) => v === null) ? null : Math.min(...values);
1244
- }
1245
- const max = stripCall(trimmed, "max");
1246
- if (max !== null) {
1247
- const values = splitTopLevelCommas(max).map((a) => new Evaluator(a).parseExprToEnd());
1248
- return values.some((v) => v === null) ? null : Math.max(...values);
1249
- }
1250
- return null;
1251
- }
1252
- function stripCall(raw, name) {
1253
- if (!raw.startsWith(name)) return null;
1254
- const afterName = raw.slice(name.length).trimStart();
1255
- if (!afterName.startsWith("(") || !afterName.endsWith(")")) return null;
1256
- return afterName.slice(1, -1);
1257
- }
1258
- function splitTopLevelCommas(s) {
1259
- const parts = [];
1260
- let depth = 0;
1261
- let start = 0;
1262
- for (let i = 0; i < s.length; i++) {
1263
- const ch = s[i];
1264
- if (ch === "(") depth++;
1265
- else if (ch === ")") depth--;
1266
- else if (ch === "," && depth === 0) {
1267
- parts.push(s.slice(start, i).trim());
1268
- start = i + 1;
1269
- }
1270
- }
1271
- parts.push(s.slice(start).trim());
1272
- return parts;
1273
- }
1274
- var Evaluator = class {
1275
- constructor(s) {
1276
- this.pos = 0;
1277
- this.s = s;
1278
- }
1279
- parseExprToEnd() {
1280
- const value = this.expr();
1281
- if (value === null) return null;
1282
- this.skipWs();
1283
- return this.pos === this.s.length ? value : null;
1284
- }
1285
- skipWs() {
1286
- while (this.pos < this.s.length && /\s/.test(this.s[this.pos])) this.pos++;
1287
- }
1288
- peek() {
1289
- this.skipWs();
1290
- return this.s[this.pos];
1291
- }
1292
- expr() {
1293
- let value = this.term();
1294
- if (value === null) return null;
1295
- for (; ; ) {
1296
- const ch = this.peek();
1297
- if (ch === "+") {
1298
- this.pos++;
1299
- const rhs = this.term();
1300
- if (rhs === null) return null;
1301
- value += rhs;
1302
- } else if (ch === "-") {
1303
- this.pos++;
1304
- const rhs = this.term();
1305
- if (rhs === null) return null;
1306
- value -= rhs;
1307
- } else {
1308
- return value;
1309
- }
1310
- }
1311
- }
1312
- term() {
1313
- let value = this.factor();
1314
- if (value === null) return null;
1315
- for (; ; ) {
1316
- const ch = this.peek();
1317
- if (ch === "*") {
1318
- this.pos++;
1319
- const rhs = this.factor();
1320
- if (rhs === null) return null;
1321
- value *= rhs;
1322
- } else if (ch === "/") {
1323
- this.pos++;
1324
- const divisor = this.factor();
1325
- if (divisor === null || divisor === 0) return null;
1326
- value /= divisor;
1327
- } else {
1328
- return value;
1329
- }
1330
- }
1331
- }
1332
- factor() {
1333
- const ch = this.peek();
1334
- if (ch === void 0) return null;
1335
- if (ch === "(") {
1336
- this.pos++;
1337
- const value = this.expr();
1338
- if (value === null || this.peek() !== ")") return null;
1339
- this.pos++;
1340
- return value;
1341
- }
1342
- if (ch === "-") {
1343
- this.pos++;
1344
- const value = this.factor();
1345
- return value === null ? null : -value;
1346
- }
1347
- return this.numberWithUnit();
1348
- }
1349
- numberWithUnit() {
1350
- this.skipWs();
1351
- const start = this.pos;
1352
- while (this.pos < this.s.length && (/[0-9]/.test(this.s[this.pos]) || this.s[this.pos] === ".")) this.pos++;
1353
- if (this.pos === start) return null;
1354
- const n = Number(this.s.slice(start, this.pos));
1355
- if (!Number.isFinite(n)) return null;
1356
- const unitStart = this.pos;
1357
- while (this.pos < this.s.length && /[a-zA-Z]/.test(this.s[this.pos])) this.pos++;
1358
- const unit = this.s.slice(unitStart, this.pos);
1359
- switch (unit) {
1360
- case "":
1361
- return n;
1362
- case "px":
1363
- return n;
1364
- case "rem":
1365
- return n * 16;
1366
- default:
1367
- return null;
1368
- }
1369
- }
1370
- };
1371
-
1372
1448
  // src/jsEngine/resolveStyle.ts
1373
1449
  var RESPONSIVE_BREAKPOINTS = /* @__PURE__ */ new Set(["sm", "md", "lg", "xl", "2xl"]);
1374
1450
  function nativeModifierState(modifier, theme, colorScheme, pressed, width) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kbach/react-native",
3
- "version": "1.0.0-beta.19",
3
+ "version": "1.0.0-beta.20",
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",