@kbach/react-native 1.0.0-beta.35 → 1.0.0-beta.36

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
@@ -7,38 +7,58 @@ resolved to a `StyleSheet`-ready style object instead of injected CSS.
7
7
  > Beta. Native is Android-only (no iOS bridge yet). Ships a prebuilt native
8
8
  > module — no Rust toolchain or manual native wiring required.
9
9
 
10
- ## Install
10
+ ## Contents
11
+
12
+ - [1. Install](#1-install)
13
+ - [2. Set up your project](#2-set-up-your-project) — pick the ONE section matching how you run your app
14
+ - [3. Configure your theme (`kbach.config.js`)](#3-configure-your-theme-kbachconfigjs)
15
+ - [4. Dark mode](#4-dark-mode)
16
+ - [5. Modifiers on native](#5-modifiers-on-native)
17
+ - [6. Reading colors as values](#6-reading-colors-as-values)
18
+ - [Reference](#reference): [arbitrary math](#arbitrary-math-calc-clamp-min-max) · [unknown-class warnings](#unknown-class-warnings-typo-detection) · [dynamic tokens](#dynamic-tokens) · [monorepo installs](#monorepo-installs-npmyarnpnpm-workspaces)
19
+
20
+ ---
21
+
22
+ ## 1. Install
11
23
 
12
24
  ```sh
13
25
  npm install @kbach/react-native
14
26
  ```
15
27
 
16
- Then, pick the one section below that matches how you're actually running
17
- the app — each is a genuinely different setup, not layered options:
28
+ ## 2. Set up your project
18
29
 
19
- - **React Native CLI** (`npx react-native ...`) → [React Native CLI](#react-native-cli)
20
- - **Expo, using Expo Go** (`npx expo start`, scanning the QR code in the Expo
21
- Go app) → [Expo Go](#expo-go) — nothing to configure at all
22
- - **Expo, using a dev client or a bare/prebuilt project** (`npx expo run:android`,
23
- or EAS Build) → [Expo (dev client / prebuild)](#expo-dev-client--prebuild)
24
- - **Expo Web** (`npx expo start --web`, or any `react-native-web` target) →
25
- [Expo Web](#expo-web) — also nothing to configure
30
+ Each subsection below is a **complete, standalone setup** for one way of
31
+ running an RN/Expo app — read only the one that matches you, not all four.
32
+
33
+ | How you run your app | Read |
34
+ | :-- | :-- |
35
+ | `npx react-native ...` (React Native CLI) | [React Native CLI](#react-native-cli) |
36
+ | `npx expo start`, scanning the QR code in the **Expo Go** app | [Expo Go](#expo-go) — nothing to configure |
37
+ | `npx expo run:android`, a dev client, or EAS Build | [Expo (dev client / prebuild)](#expo-dev-client--prebuild) |
38
+ | `npx expo start --web`, or any `react-native-web` target | [Expo Web](#expo-web) — nothing to configure |
26
39
 
27
40
  ### React Native CLI
28
41
 
29
- ```js
30
- // babel.config.js
31
- module.exports = {
32
- presets: ['module:@react-native/babel-preset'],
33
- plugins: ['@kbach/react-native/babel-plugin'],
34
- };
35
- ```
42
+ 1. Add the babel plugin:
43
+
44
+ ```js
45
+ // babel.config.js
46
+ module.exports = {
47
+ presets: ['module:@react-native/babel-preset'],
48
+ plugins: ['@kbach/react-native/babel-plugin'],
49
+ };
50
+ ```
36
51
 
37
- Rebuild the Android app afterward so Gradle picks up the native module.
52
+ 2. Rebuild the Android app (`npx react-native run-android`) so Gradle picks
53
+ up the native module.
54
+ 3. Continue to [Configure your theme](#3-configure-your-theme-kbachconfigjs) (optional, but almost always what you want next).
38
55
 
39
56
  ### Expo Go
40
57
 
41
- Works out of the box — no prebuild, no dev client, nothing to configure.
58
+ **Nothing to configure at all.** Skip straight to
59
+ [Configure your theme](#3-configure-your-theme-kbachconfigjs) — or just
60
+ start writing `className` right away using the defaults.
61
+
42
62
  Expo Go can't load third-party native code, so this falls back to a pure-JS
43
63
  engine automatically. Covers layout, spacing, border/radius, plain colors,
44
64
  font-weight, text-transform/decoration, line-height, letter-spacing,
@@ -52,32 +72,43 @@ scope native itself excludes.
52
72
 
53
73
  ### Expo (dev client / prebuild)
54
74
 
55
- Gets you the full native engine instead of the JS fallback:
75
+ Gets you the full native engine instead of the JS fallback.
56
76
 
57
- ```js
58
- // babel.config.js
59
- module.exports = {
60
- presets: ['babel-preset-expo'],
61
- plugins: ['@kbach/react-native/babel-plugin'],
62
- };
63
- ```
77
+ 1. Add the babel plugin — a fresh Expo project's generated `babel.config.js`
78
+ uses the function form below (`api.cache(true)`), not a plain exported
79
+ object; add the plugin to the array **inside** the returned config,
80
+ keep everything else as-is:
64
81
 
65
- ```sh
66
- npx expo prebuild
67
- npx expo run:android
68
- ```
82
+ ```js
83
+ // babel.config.js
84
+ module.exports = function (api) {
85
+ api.cache(true);
86
+ return {
87
+ presets: ['babel-preset-expo'],
88
+ plugins: ['@kbach/react-native/babel-plugin'], // <- add this line
89
+ };
90
+ };
91
+ ```
92
+
93
+ 2. Confirm the New Architecture is enabled — `"newArchEnabled": true` in
94
+ `app.json` (on by default in current Expo SDKs).
95
+ 3. Prebuild and run:
96
+
97
+ ```sh
98
+ npx expo prebuild
99
+ npx expo run:android
100
+ ```
69
101
 
70
- Requires the New Architecture enabled (`"newArchEnabled": true` in
71
- `app.json`, on by default in current Expo SDKs). EAS Build works the same
72
- way, no extra config.
102
+ EAS Build works the same way, no extra config.
103
+ 4. Continue to [Configure your theme](#3-configure-your-theme-kbachconfigjs) (optional, but almost always what you want next).
73
104
 
74
105
  ### Expo Web
75
106
 
76
- Works out of the box — resolves through the same real-CSS engine
107
+ **Nothing to configure at all.** Resolves through the same real-CSS engine
77
108
  `@kbach/react` uses on the DOM, with the FULL modifier set (`group-hover:`,
78
109
  `peer-hover:`, container queries, `has-[...]:`, etc.) — everything real
79
110
  Tailwind supports. Native supports a smaller, explicitly-scoped subset; see
80
- "Modifiers on native" below for exactly what that covers.
111
+ [Modifiers on native](#5-modifiers-on-native) for exactly what that covers.
81
112
 
82
113
  **Static rendering (SSR) caveat:** `dark:`/responsive classes on content
83
114
  that's part of the initial pre-rendered HTML resolve against the *server*,
@@ -91,89 +122,122 @@ useEffect(() => setMounted(true), []);
91
122
  <View className={mounted ? 'bg-gray-1 dark:bg-gray-12' : 'bg-gray-1'} />;
92
123
  ```
93
124
 
94
- ### Monorepo installs (npm/yarn/pnpm workspaces)
125
+ ## 3. Configure your theme (`kbach.config.js`)
126
+
127
+ This step is **optional** — every class already works against a built-in
128
+ default palette/spacing/breakpoint scale with no config file at all. Add a
129
+ `kbach.config.js` when you want your own color names, custom spacing, or
130
+ extra breakpoints.
131
+
132
+ 1. Create a **`kbach.config.js` file at your project root** (same folder as
133
+ `package.json` — the one you run `expo start`/`react-native start` from):
134
+
135
+ ```js
136
+ // kbach.config.js
137
+ module.exports = {
138
+ extend: {
139
+ colors: {
140
+ brand: '#ff6b35',
141
+ },
142
+ },
143
+ };
144
+ ```
145
+
146
+ 2. That's it — **no import, no `applyKbachConfig()` call needed.** If you
147
+ completed step 2 above (the babel plugin is installed), it detects
148
+ `kbach.config.js` automatically and applies it before your app's first
149
+ render. You only need to call `applyKbachConfig()`/`resolveKbachConfig()`
150
+ yourself if you're changing the theme again at *runtime* (e.g. loading a
151
+ different config after login) — see the exports below.
152
+ 3. Use it: `<View className="bg-brand" />`.
153
+
154
+ > **Common mistake:** `kbach.config.js` is a plain **JavaScript** file, not
155
+ > JSON — an object key containing a hyphen (`surface-dim`) must be quoted
156
+ > as a string (`'surface-dim': '#121318'`), or it's invalid JS syntax and
157
+ > the whole config file fails to load. Any key that's a valid identifier
158
+ > (letters/digits/underscore, e.g. `brand`, `surface2`) can stay unquoted.
159
+
160
+ ### `extend.colors`
161
+
162
+ Every value is either a literal color, or a **reference** to another color
163
+ name (optionally with an opacity suffix):
95
164
 
96
- If your app lives in a monorepo where `@kbach/react-native` is a workspace
97
- package (a `file:`/`link:` dependency, or hoisted rather than nested), watch
98
- for **two physical copies of `react-native`/`react`** ending up in your
99
- dependency tree — one this package would otherwise resolve up to from its
100
- own directory, and a different one your app's own files resolve to. Metro
101
- walks `node_modules` hierarchically per-file, so it can silently hand two
102
- different files two different copies of the *same* module, each with its
103
- own separate module state.
165
+ ```js
166
+ colors: {
167
+ brand: '#ff6b35', // literal hex
168
+ accent: 'orange-5', // references a BUILT-IN palette color
169
+ accentSoft: 'orange-5/50', // ...at 50% opacity
170
+ brandSoft: 'brand/30', // references a color defined EARLIER in this object, at 30% opacity
171
+ danger: 'red', // not a known color name -> used as a literal CSS value
172
+ }
173
+ ```
104
174
 
105
- This isn't hypothetical: it's the root cause of a real dark-mode bug this
106
- package shipped and fixed — `useColorScheme()`/`Appearance` in one copy
107
- never saw changes made through the other, so an explicit theme toggle
108
- appeared to do nothing. The fix is a Metro config override that forces a
109
- single instance, **required in your app**, not something this package can
110
- do on your behalf from inside `node_modules`:
175
+ **Mode-aware colors** (a different value per light/dark) — two equivalent
176
+ ways to write one:
111
177
 
112
178
  ```js
113
- // metro.config.js
114
- const path = require('path');
115
- // React Native CLI: const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');
116
- // Expo: const { getDefaultConfig } = require('expo/metro-config');
179
+ // Per-color pair — good for one or two mode-aware colors
180
+ colors: {
181
+ surface: { light: 'gray-2', dark: 'gray-11' },
182
+ }
117
183
 
118
- const projectRoot = __dirname;
119
- const workspaceRoot = path.resolve(projectRoot, '../..'); // -> your monorepo root
184
+ // Grouped "dark" block — good when MOST of your colors are mode-aware
185
+ colors: {
186
+ surface: 'gray-2',
187
+ card: 'white',
188
+ dark: {
189
+ surface: 'gray-11',
190
+ card: 'gray-9',
191
+ },
192
+ }
193
+ ```
120
194
 
121
- const config = getDefaultConfig(projectRoot);
122
- config.watchFolders = [workspaceRoot];
195
+ Both produce the same result: `bg-surface` (no `dark:` prefix needed)
196
+ automatically resolves to the right shade for the current mode. `dark` is a
197
+ reserved key here, not a color name — a color never mentioned under `dark`
198
+ stays a plain, mode-independent color. A name written under `dark` with no
199
+ matching top-level entry is skipped (mode-aware colors need both sides).
123
200
 
124
- const forcedSingleInstance = {
125
- 'react-native': path.resolve(projectRoot, 'node_modules/react-native'),
126
- react: path.resolve(projectRoot, 'node_modules/react'),
127
- };
128
- const defaultResolveRequest = config.resolver.resolveRequest;
129
- config.resolver.resolveRequest = (context, moduleName, platform) => {
130
- if (Object.prototype.hasOwnProperty.call(forcedSingleInstance, moduleName)) {
131
- return { type: 'sourceFile', filePath: require.resolve(forcedSingleInstance[moduleName]) };
132
- }
133
- return defaultResolveRequest
134
- ? defaultResolveRequest(context, moduleName, platform)
135
- : context.resolveRequest(context, moduleName, platform);
136
- };
201
+ ### `extend.spacing` / `extend.screens` / `extend.fontFamily` / `extend.container`
137
202
 
138
- module.exports = config; // (React Native CLI: mergeConfig(getDefaultConfig(projectRoot), config))
203
+ ```js
204
+ module.exports = {
205
+ extend: {
206
+ spacing: { 128: 512 }, // p-128 -> 512
207
+ screens: { '3xl': 1920 }, // 3xl:flex-row
208
+ fontFamily: { display: '"Cal Sans", sans-serif' }, // font-display
209
+ container: { center: true, padding: '2rem' },
210
+ },
211
+ };
139
212
  ```
140
213
 
141
- A single, non-monorepo `npm install` never hits this — only relevant if your
142
- app and `@kbach/react-native` share a workspace root.
214
+ ### `theme` (replace instead of extend)
143
215
 
144
- ## Modifiers on native
216
+ `theme.colors`/`theme.spacing`/`theme.screens`/`theme.fontFamily` **replace**
217
+ the built-in defaults entirely instead of adding to them — use this only if
218
+ you want to drop the default palette/scale altogether:
145
219
 
146
- Native has no CSS selector/cascade engine, so only a fixed, explicitly
147
- supported set of modifiers actually applies — everything else (`group-*:`,
148
- `peer-*:`, `has-[...]:`, `aria-*:`, `data-*:`, container queries, ...)
149
- parses without error but never takes effect. Supported today:
220
+ ```js
221
+ module.exports = {
222
+ theme: {
223
+ colors: { brand: '#ff6b35' }, // ONLY color available now — the default 22-family palette is gone
224
+ },
225
+ };
226
+ ```
150
227
 
151
- - **`dark:`** — reacts to the OS/app color scheme, see "Dark mode" below.
152
- - **`sm:`/`md:`/`lg:`/`xl:`/`2xl:`** — reacts to `useWindowDimensions()`
153
- crossing the matching `theme.screens` breakpoint (rotation/resize/fold).
154
- - **`active:`** — `Pressable`'s own press state, via its style-callback API.
155
- - **`hover:`** — tracked via `onHoverIn`/`onHoverOut`, which RN only ever
156
- fires on a platform with real pointer input (iOS 13+/Android/macOS/
157
- Windows via a mouse/trackpad/Apple Pencil hover) — inert (never fires) on
158
- a touch-only device, the same way real Tailwind's `hover:` already
159
- degrades on a touch-only *browser*. Composing it onto any host type is
160
- harmless even where it never fires.
161
- - **`focus:`** — tracked via `onFocus`/`onBlur` (keyboard/external-keyboard/
162
- TV-remote/tab navigation, and `TextInput` focus).
163
- - **`disabled:`** — reads the element's own `disabled` prop directly; no
164
- event tracking needed, since a prop change already re-renders normally.
165
- - Any combination of the above chained on one class (`sm:dark:bg-blue-8`,
166
- `hover:focus:opacity-100`) requires every named condition to hold at once
167
- — chain order never matters (`dark:sm:` and `sm:dark:` are equivalent).
228
+ ### `darkMode`
168
229
 
169
- `group-hover:`/`peer-hover:` and friends are NOT implemented on native —
170
- doing so needs a React Context bridge (propagating an ancestor's hover/
171
- press/focus state down to descendants, since there's no `.group`/`.peer`
172
- selector relationship to lean on) that doesn't exist today. If you need
173
- group/peer-style coordination on native right now, lift the shared state
174
- into a parent component yourself and pass it down as an explicit prop.
230
+ ```js
231
+ module.exports = {
232
+ darkMode: 'attribute', // 'attribute' (default) | 'class' | 'media'
233
+ };
234
+ ```
235
+
236
+ Controls how Expo Web reflects dark mode in the DOM; has no effect on real
237
+ native, which always resolves `dark:` from a live parameter (see
238
+ [Dark mode](#4-dark-mode) below).
175
239
 
176
- ## Dark mode
240
+ ## 4. Dark mode
177
241
 
178
242
  **Zero setup, on every platform this package supports** — real native
179
243
  *and* Expo Web. Unlike [`@kbach/react`](https://www.npmjs.com/package/@kbach/react),
@@ -218,7 +282,39 @@ itself, so nothing changes for apps that don't opt in. Prefer wiring your
218
282
  own storage manually instead? `setGlobalThemeMode()` before your first
219
283
  render still works exactly as before.
220
284
 
221
- ## Reading colors as values
285
+ ## 5. Modifiers on native
286
+
287
+ Native has no CSS selector/cascade engine, so only a fixed, explicitly
288
+ supported set of modifiers actually applies — everything else (`group-*:`,
289
+ `peer-*:`, `has-[...]:`, `aria-*:`, `data-*:`, container queries, ...)
290
+ parses without error but never takes effect. Supported today:
291
+
292
+ - **`dark:`** — reacts to the OS/app color scheme, see [Dark mode](#4-dark-mode).
293
+ - **`sm:`/`md:`/`lg:`/`xl:`/`2xl:`** — reacts to `useWindowDimensions()`
294
+ crossing the matching `theme.screens` breakpoint (rotation/resize/fold).
295
+ - **`active:`** — `Pressable`'s own press state, via its style-callback API.
296
+ - **`hover:`** — tracked via `onHoverIn`/`onHoverOut`, which RN only ever
297
+ fires on a platform with real pointer input (iOS 13+/Android/macOS/
298
+ Windows via a mouse/trackpad/Apple Pencil hover) — inert (never fires) on
299
+ a touch-only device, the same way real Tailwind's `hover:` already
300
+ degrades on a touch-only *browser*. Composing it onto any host type is
301
+ harmless even where it never fires.
302
+ - **`focus:`** — tracked via `onFocus`/`onBlur` (keyboard/external-keyboard/
303
+ TV-remote/tab navigation, and `TextInput` focus).
304
+ - **`disabled:`** — reads the element's own `disabled` prop directly; no
305
+ event tracking needed, since a prop change already re-renders normally.
306
+ - Any combination of the above chained on one class (`sm:dark:bg-blue-8`,
307
+ `hover:focus:opacity-100`) requires every named condition to hold at once
308
+ — chain order never matters (`dark:sm:` and `sm:dark:` are equivalent).
309
+
310
+ `group-hover:`/`peer-hover:` and friends are NOT implemented on native —
311
+ doing so needs a React Context bridge (propagating an ancestor's hover/
312
+ press/focus state down to descendants, since there's no `.group`/`.peer`
313
+ selector relationship to lean on) that doesn't exist today. If you need
314
+ group/peer-style coordination on native right now, lift the shared state
315
+ into a parent component yourself and pass it down as an explicit prop.
316
+
317
+ ## 6. Reading colors as values
222
318
 
223
319
  For anywhere you need a real color, not a `className` — a chart prop, a
224
320
  native shadow color:
@@ -231,23 +327,32 @@ colors.blue[6]; // '#2563eb' — static, same value in both modes
231
327
  colors.blue['6/50']; // same, at 50% opacity
232
328
  ```
233
329
 
234
- Any color can be defined in the theme as a plain string (static) or a
235
- `{ light, dark }` pair — fully manual, resolved per the current dark-mode
236
- state:
237
-
238
- ```ts
239
- setTheme({
240
- ...defaultTheme,
241
- colors: { ...defaultTheme.colors, brand: { light: '#2563eb', dark: '#93c5fd' } },
242
- });
243
- ```
244
-
245
330
  A shade-family lookup (`colors.blue[n]`) auto-mirrors to shade `13 - n` in
246
331
  dark mode — this palette's own convention is 1 = lightest, 12 = darkest, so
247
332
  that's the same visual weight relative to its own background, no manual
248
333
  `colors.blue[isDark ? 7 : 6]` needed.
249
334
 
250
- ## Arbitrary math: `calc()`, `clamp()`, `min()`, `max()`
335
+ **TypeScript error on a custom color?** `colors.brand` (any custom color
336
+ from `kbach.config.js`) types as `string | ColorScale`, not plain `string`
337
+ — `colors` serves both a flat color AND a shade family (`colors.blue`)
338
+ through the same object, and TypeScript can't statically tell which one a
339
+ given name is (your config is plain runtime JS, not a type declaration).
340
+ If that union causes an error somewhere expecting a real `string` (a
341
+ native style prop, a chart library), use `colors.get('brand')` instead —
342
+ same value, typed as plain `string`:
343
+
344
+ ```tsx
345
+ colors.brand; // string | ColorScale — errors if assigned to a `string`-typed prop
346
+ colors.get('brand'); // string — same value, always typed correctly
347
+ ```
348
+
349
+ ---
350
+
351
+ ## Reference
352
+
353
+ Deeper/less-common material — you likely won't need this on your first pass.
354
+
355
+ ### Arbitrary math: `calc()`, `clamp()`, `min()`, `max()`
251
356
 
252
357
  ```tsx
253
358
  <View className="p-[calc(16px+8px)]" /> // -> padding: 24
@@ -298,7 +403,7 @@ into three broken tokens before `calc()` is ever even recognized. Write
298
403
  `w-[calc(100%_-_3rem)]` (underscores) instead — this is exactly what the
299
404
  unknown-class warning below is for: it'll tell you when this happens.
300
405
 
301
- ## Unknown-class warnings (typo detection)
406
+ ### Unknown-class warnings (typo detection)
302
407
 
303
408
  ```tsx
304
409
  <View className="flexx-center" /> // real Kbach classes never have to guess — a typo warns
@@ -314,9 +419,8 @@ once per unique message) instead of silently doing nothing — the same
314
419
  signal `@kbach/react`'s Vite plugin already gives you at build time on
315
420
  web, just at resolve time here instead. A real utility this engine simply
316
421
  doesn't support on native yet (`grid-cols-3`, `scale-150`, `blur`, ...)
317
- never warns — that's an intentional, documented platform gap (see
318
- `resolve_style.rs`'s own doc comment), not a typo, so it stays silent the
319
- same way it always has.
422
+ never warns — that's an intentional, documented platform gap, not a typo,
423
+ so it stays silent the same way it always has.
320
424
 
321
425
  Not available in Expo Go's pure-JS fallback engine today — that engine has
322
426
  no web-shaped resolver to check "is this a real Kbach utility at all"
@@ -325,7 +429,7 @@ distinguish a genuine typo from a real-but-native-unsupported class
325
429
  without a much larger port. A real dev-client/CLI build always gets this
326
430
  check.
327
431
 
328
- ## Dynamic tokens
432
+ ### Dynamic tokens
329
433
 
330
434
  The native counterpart to a real CSS custom property — `var(--x)` already
331
435
  works in a `className` on Expo Web with zero help from this package (real
@@ -348,9 +452,59 @@ every element using it repaints instantly via the browser's own CSS engine,
348
452
  no React re-render involved. On native, the className is substituted with
349
453
  the token's current value before every resolve, and an element referencing
350
454
  a token re-renders/repaints on its own when that token changes (same
351
- subscription mechanism `dark:` and `sm:`/`md:`/... already use — see
352
- `jsxRuntimeCore.ts`'s own doc comments). An unregistered token name falls
353
- through to the same native invalid-value warning above.
455
+ subscription mechanism `dark:` and `sm:`/`md:`/... already use). An
456
+ unregistered token name falls through to the same native invalid-value
457
+ warning above.
458
+
459
+ ### Monorepo installs (npm/yarn/pnpm workspaces)
460
+
461
+ If your app lives in a monorepo where `@kbach/react-native` is a workspace
462
+ package (a `file:`/`link:` dependency, or hoisted rather than nested), watch
463
+ for **two physical copies of `react-native`/`react`** ending up in your
464
+ dependency tree — one this package would otherwise resolve up to from its
465
+ own directory, and a different one your app's own files resolve to. Metro
466
+ walks `node_modules` hierarchically per-file, so it can silently hand two
467
+ different files two different copies of the *same* module, each with its
468
+ own separate module state.
469
+
470
+ This isn't hypothetical: it's the root cause of a real dark-mode bug this
471
+ package shipped and fixed — `useColorScheme()`/`Appearance` in one copy
472
+ never saw changes made through the other, so an explicit theme toggle
473
+ appeared to do nothing. The fix is a Metro config override that forces a
474
+ single instance, **required in your app**, not something this package can
475
+ do on your behalf from inside `node_modules`:
476
+
477
+ ```js
478
+ // metro.config.js
479
+ const path = require('path');
480
+ // React Native CLI: const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');
481
+ // Expo: const { getDefaultConfig } = require('expo/metro-config');
482
+
483
+ const projectRoot = __dirname;
484
+ const workspaceRoot = path.resolve(projectRoot, '../..'); // -> your monorepo root
485
+
486
+ const config = getDefaultConfig(projectRoot);
487
+ config.watchFolders = [workspaceRoot];
488
+
489
+ const forcedSingleInstance = {
490
+ 'react-native': path.resolve(projectRoot, 'node_modules/react-native'),
491
+ react: path.resolve(projectRoot, 'node_modules/react'),
492
+ };
493
+ const defaultResolveRequest = config.resolver.resolveRequest;
494
+ config.resolver.resolveRequest = (context, moduleName, platform) => {
495
+ if (Object.prototype.hasOwnProperty.call(forcedSingleInstance, moduleName)) {
496
+ return { type: 'sourceFile', filePath: require.resolve(forcedSingleInstance[moduleName]) };
497
+ }
498
+ return defaultResolveRequest
499
+ ? defaultResolveRequest(context, moduleName, platform)
500
+ : context.resolveRequest(context, moduleName, platform);
501
+ };
502
+
503
+ module.exports = config; // (React Native CLI: mergeConfig(getDefaultConfig(projectRoot), config))
504
+ ```
505
+
506
+ A single, non-monorepo `npm install` never hits this — only relevant if your
507
+ app and `@kbach/react-native` share a workspace root.
354
508
 
355
509
  ## License
356
510
 
@@ -179,6 +179,22 @@ type ColorsAPI = {
179
179
  } & {
180
180
  /** Applies an opacity (0–100) to any color string this hook can return. */
181
181
  readonly alpha: (color: string, opacity?: number) => string;
182
+ /**
183
+ * Same lookup dot/bracket access does (`colors.get('brand')` ===
184
+ * `colors.brand`), typed as plain `string` instead of `string |
185
+ * ColorScale` — use this whenever the result goes somewhere that expects
186
+ * a real `string` (a native style prop, a chart library) and hits a type
187
+ * error otherwise. The wider union on every OTHER property here isn't a
188
+ * bug: a flat custom color (`brand`) and a shade-family prefix (`blue`)
189
+ * are served by the exact same object, and TypeScript has no way to
190
+ * statically know which one any given key names — `kbach.config.js`'s
191
+ * colors are plain runtime JS, not a type declaration this package could
192
+ * inspect ahead of time. A name with no matching theme entry falls back
193
+ * to the name itself, unresolved — the same "not a known color name ->
194
+ * literal value" convention `kbach.config.js`'s own color aliasing
195
+ * already uses (see `config.ts`'s `resolveColorEntry`), never an error.
196
+ */
197
+ readonly get: (name: string) => string;
182
198
  };
183
199
  /**
184
200
  * Resolved theme colors as real values — for anywhere you need an actual
@@ -198,6 +214,7 @@ type ColorsAPI = {
198
214
  * colors.blue[6]; // "#2563eb" — static, same value in both modes
199
215
  * colors.blue['6/50']; // same, at 50% opacity
200
216
  * colors.brand; // resolves per isDark if defined as { light, dark }
217
+ * colors.get('brand'); // same value as colors.brand, typed as plain `string`
201
218
  * ```
202
219
  */
203
220
  declare function useColors(): ColorsAPI;
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 ClassArray, a as ClassDictionary, b as ClassValue, c as ColorScale, d as ColorsAPI, K as KbachConfig, T as ThemeProvider, e as ThemeProviderProps, f as ThemeState, g as ThemeStorageAdapter, h as applyKbachConfig, i as clsx, r as resolveKbachConfig, u as useColors, j as useDynamicToken, k as useTheme } from './clsx-CxjWSCcU.js';
2
+ export { C as ClassArray, a as ClassDictionary, b as ClassValue, c as ColorScale, d as ColorsAPI, K as KbachConfig, T as ThemeProvider, e as ThemeProviderProps, f as ThemeState, g as ThemeStorageAdapter, h as applyKbachConfig, i as clsx, r as resolveKbachConfig, u as useColors, j as useDynamicToken, k as useTheme } from './clsx-BLLSnqAa.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
@@ -2041,11 +2041,13 @@ function makeColorsProxy(theme, isDark) {
2041
2041
  }
2042
2042
  });
2043
2043
  }
2044
- return new Proxy({ alpha }, {
2044
+ const get = (name) => resolveNamed(name) ?? name;
2045
+ return new Proxy({ alpha, get }, {
2045
2046
  get(_, prop) {
2046
2047
  const key = String(prop);
2047
2048
  if (key === "then") return void 0;
2048
2049
  if (key === "alpha") return alpha;
2050
+ if (key === "get") return get;
2049
2051
  const direct = resolveNamed(key);
2050
2052
  if (direct !== void 0) return direct;
2051
2053
  if (!cache.has(key)) cache.set(key, makeFamilyProxy(key));
@@ -1,5 +1,5 @@
1
1
  export { ColorEntry, ContainerConfig, ThemeConfig, defaultTheme, getTheme, setTheme } from './theme';
2
- export { C as ClassArray, a as ClassDictionary, b as ClassValue, c as ColorScale, d as ColorsAPI, K as KbachConfig, T as ThemeProvider, e as ThemeProviderProps, f as ThemeState, h as applyKbachConfig, i as clsx, r as resolveKbachConfig, u as useColors, j as useDynamicToken, k as useTheme } from './clsx-CxjWSCcU.js';
2
+ export { C as ClassArray, a as ClassDictionary, b as ClassValue, c as ColorScale, d as ColorsAPI, K as KbachConfig, T as ThemeProvider, e as ThemeProviderProps, f as ThemeState, h as applyKbachConfig, i as clsx, r as resolveKbachConfig, u as useColors, j as useDynamicToken, k as useTheme } from './clsx-BLLSnqAa.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
@@ -445,11 +445,13 @@ function makeColorsProxy(theme, isDark) {
445
445
  }
446
446
  });
447
447
  }
448
- return new Proxy({ alpha }, {
448
+ const get = (name) => resolveNamed(name) ?? name;
449
+ return new Proxy({ alpha, get }, {
449
450
  get(_, prop) {
450
451
  const key = String(prop);
451
452
  if (key === "then") return void 0;
452
453
  if (key === "alpha") return alpha;
454
+ if (key === "get") return get;
453
455
  const direct = resolveNamed(key);
454
456
  if (direct !== void 0) return direct;
455
457
  if (!cache.has(key)) cache.set(key, makeFamilyProxy(key));
@@ -211,11 +211,13 @@ function makeColorsProxy(theme, isDark) {
211
211
  }
212
212
  });
213
213
  }
214
- return new Proxy({ alpha }, {
214
+ const get = (name) => resolveNamed(name) ?? name;
215
+ return new Proxy({ alpha, get }, {
215
216
  get(_, prop) {
216
217
  const key = String(prop);
217
218
  if (key === "then") return void 0;
218
219
  if (key === "alpha") return alpha;
220
+ if (key === "get") return get;
219
221
  const direct = resolveNamed(key);
220
222
  if (direct !== void 0) return direct;
221
223
  if (!cache.has(key)) cache.set(key, makeFamilyProxy(key));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kbach/react-native",
3
- "version": "1.0.0-beta.35",
3
+ "version": "1.0.0-beta.36",
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",