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

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
@@ -173,8 +173,26 @@ optional, for seeding a startup default. Outside React, use
173
173
  `getGlobalDarkMode()`/`toggleGlobalDarkMode()` instead.
174
174
 
175
175
  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.
176
+ app launches by default (no forced storage dependency). To persist, pass
177
+ any async key-value store exposing `getItem`/`setItem` as `<ThemeProvider
178
+ persist>` — `@react-native-async-storage/async-storage`'s default export
179
+ already matches this shape exactly:
180
+
181
+ ```tsx
182
+ import AsyncStorage from '@react-native-async-storage/async-storage';
183
+ import { ThemeProvider } from '@kbach/react-native';
184
+
185
+ <ThemeProvider persist={AsyncStorage}>
186
+ <App />
187
+ </ThemeProvider>;
188
+ ```
189
+
190
+ The persisted mode (if any) is read once on mount and wins over
191
+ `defaultMode`; every later explicit choice is written back automatically.
192
+ `false` by default — this package still never imports a storage library
193
+ itself, so nothing changes for apps that don't opt in. Prefer wiring your
194
+ own storage manually instead? `setGlobalThemeMode()` before your first
195
+ render still works exactly as before.
178
196
 
179
197
  ## Reading colors as values
180
198
 
@@ -222,12 +240,15 @@ way — no such unit exists on native at all. It already works fine on web
222
240
  (real CSS); on native the declaration is dropped and a `console.warn`
223
241
  explains why (dev builds only).
224
242
 
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 —
243
+ **Percentage-relative width/height** (`calc(100%-3rem)`, `calc(50%+16px)`,
244
+ `min(50%,20rem)`, `max(1rem,5%)`, `clamp(16rem,50%,32rem)`) IS supported,
245
+ differently: since native has no way to know "100% of what" ahead of time,
246
+ this measures the element's own real layout instead —
228
247
 
229
248
  ```tsx
230
249
  <View className="w-[calc(100%_-_3rem)]" />
250
+ <View className="w-[min(50%,20rem)]" /> // half the parent, capped at a fixed size
251
+ <View className="w-[clamp(16rem,50%,32rem)]" />
231
252
  ```
232
253
 
233
254
  Renders once at a plain `100%` (a real RN percentage, resolved correctly by
@@ -236,9 +257,14 @@ event, then immediately corrects to the actual computed number once
236
257
  measured — a genuine one-frame measure-then-snap, not instant math like the
237
258
  constant-only case above, and it keeps re-measuring on every layout (screen
238
259
  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.
260
+ Scoped narrowly, same "reduce fully or bail" philosophy as the constant-only
261
+ case: `calc()` takes exactly one percentage term plus one constant term;
262
+ `min()`/`max()` take two or more comma-separated arguments, each EITHER a
263
+ plain percentage or a constant px/rem expression (not both mixed in one
264
+ argument — `min(50%-1rem,20rem)` is out of scope); `clamp()` takes exactly
265
+ three. `width`/`height` only in every case — other properties, or anything
266
+ that doesn't reduce fully, falls through to the same drop-and-warn behavior
267
+ as before.
242
268
 
243
269
  **Arbitrary values can't contain literal spaces** — this is a pre-existing
244
270
  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';
package/dist/index.js CHANGED
@@ -1680,9 +1680,13 @@ function useTheme() {
1680
1680
  var import_react2 = require("react");
1681
1681
  var import_darkModeStore3 = require("./darkModeStore");
1682
1682
  var import_jsx_runtime = require("react/jsx-runtime");
1683
+ var PERSIST_STORAGE_KEY = "kbach-theme";
1684
+ function isThemeMode(value) {
1685
+ return value === "light" || value === "dark" || value === "system";
1686
+ }
1683
1687
  var mountedProviderCount = 0;
1684
1688
  var warnedMultipleProviders = false;
1685
- function ThemeProvider({ children, defaultMode = "system" }) {
1689
+ function ThemeProvider({ children, defaultMode = "system", persist = false }) {
1686
1690
  (0, import_react2.useEffect)(() => {
1687
1691
  mountedProviderCount++;
1688
1692
  if (mountedProviderCount > 1 && !warnedMultipleProviders) {
@@ -1699,8 +1703,29 @@ function ThemeProvider({ children, defaultMode = "system" }) {
1699
1703
  (0, import_react2.useEffect)(() => {
1700
1704
  if (seeded.current) return;
1701
1705
  seeded.current = true;
1702
- (0, import_darkModeStore3.seedDefaultMode)(defaultMode);
1706
+ if (!persist) {
1707
+ (0, import_darkModeStore3.seedDefaultMode)(defaultMode);
1708
+ return;
1709
+ }
1710
+ let cancelled = false;
1711
+ persist.getItem(PERSIST_STORAGE_KEY).then((stored) => {
1712
+ if (cancelled) return;
1713
+ if (isThemeMode(stored)) (0, import_darkModeStore3.seedDefaultMode)(stored);
1714
+ else (0, import_darkModeStore3.seedDefaultMode)(defaultMode);
1715
+ }).catch(() => {
1716
+ if (!cancelled) (0, import_darkModeStore3.seedDefaultMode)(defaultMode);
1717
+ });
1718
+ return () => {
1719
+ cancelled = true;
1720
+ };
1703
1721
  }, []);
1722
+ (0, import_react2.useEffect)(() => {
1723
+ if (!persist) return;
1724
+ return (0, import_darkModeStore3.subscribeGlobalDarkMode)(() => {
1725
+ persist.setItem(PERSIST_STORAGE_KEY, (0, import_darkModeStore3.getGlobalThemeMode)()).catch(() => {
1726
+ });
1727
+ });
1728
+ }, [persist]);
1704
1729
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children });
1705
1730
  }
1706
1731
 
@@ -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 = [
@@ -471,17 +511,17 @@ function substituteDynamicTokens(classStrRaw) {
471
511
  return value ?? whole;
472
512
  });
473
513
  }
474
- var PERCENT_RELATIVE_CALC_HINT_RE = /[wh]-\[calc\([^)]*%[^)]*\)\]/;
514
+ var PERCENT_RELATIVE_CALC_HINT_RE = /[wh]-\[(calc|min|max|clamp)\([^)]*%[^)]*\)\]/;
475
515
  function extractPercentRelativeCalcs(classStrRaw) {
476
516
  const found = [];
477
517
  for (const token of classStrRaw.split(/\s+/)) {
478
- const parsed = parsePercentRelativeCalc(token);
518
+ const parsed = parsePercentRelativeExpr(token);
479
519
  if (parsed) found.push(parsed);
480
520
  }
481
521
  return found;
482
522
  }
483
523
  function stripPercentRelativeCalcTokens(classStrRaw) {
484
- return classStrRaw.split(/\s+/).filter((token) => token && parsePercentRelativeCalc(token) === null).join(" ");
524
+ return classStrRaw.split(/\s+/).filter((token) => token && parsePercentRelativeExpr(token) === null).join(" ");
485
525
  }
486
526
  var STATE_MODIFIER_NAMES = /* @__PURE__ */ new Set(["hover", "focus", "disabled"]);
487
527
  var STATE_MODIFIER_HINT_RE = /(^|\s|:)(hover|focus|disabled):/;
@@ -556,7 +596,7 @@ function ReactiveElement({
556
596
  layoutOverrides = {};
557
597
  for (const calc of percentCalcs) {
558
598
  const basis = measuredBasis?.[calc.property];
559
- layoutOverrides[calc.property] = basis === void 0 ? "100%" : resolvePercentRelativeCalc(basis, calc);
599
+ layoutOverrides[calc.property] = basis === void 0 ? "100%" : calc.resolve(basis);
560
600
  }
561
601
  const userOnLayout = hostRest.onLayout;
562
602
  hostRestWithLayout = {
@@ -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 = [
@@ -472,17 +512,17 @@ function substituteDynamicTokens(classStrRaw) {
472
512
  return value ?? whole;
473
513
  });
474
514
  }
475
- var PERCENT_RELATIVE_CALC_HINT_RE = /[wh]-\[calc\([^)]*%[^)]*\)\]/;
515
+ var PERCENT_RELATIVE_CALC_HINT_RE = /[wh]-\[(calc|min|max|clamp)\([^)]*%[^)]*\)\]/;
476
516
  function extractPercentRelativeCalcs(classStrRaw) {
477
517
  const found = [];
478
518
  for (const token of classStrRaw.split(/\s+/)) {
479
- const parsed = parsePercentRelativeCalc(token);
519
+ const parsed = parsePercentRelativeExpr(token);
480
520
  if (parsed) found.push(parsed);
481
521
  }
482
522
  return found;
483
523
  }
484
524
  function stripPercentRelativeCalcTokens(classStrRaw) {
485
- return classStrRaw.split(/\s+/).filter((token) => token && parsePercentRelativeCalc(token) === null).join(" ");
525
+ return classStrRaw.split(/\s+/).filter((token) => token && parsePercentRelativeExpr(token) === null).join(" ");
486
526
  }
487
527
  var STATE_MODIFIER_NAMES = /* @__PURE__ */ new Set(["hover", "focus", "disabled"]);
488
528
  var STATE_MODIFIER_HINT_RE = /(^|\s|:)(hover|focus|disabled):/;
@@ -557,7 +597,7 @@ function ReactiveElement({
557
597
  layoutOverrides = {};
558
598
  for (const calc of percentCalcs) {
559
599
  const basis = measuredBasis?.[calc.property];
560
- layoutOverrides[calc.property] = basis === void 0 ? "100%" : resolvePercentRelativeCalc(basis, calc);
600
+ layoutOverrides[calc.property] = basis === void 0 ? "100%" : calc.resolve(basis);
561
601
  }
562
602
  const userOnLayout = hostRest.onLayout;
563
603
  hostRestWithLayout = {
@@ -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.22",
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",