@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 +279 -125
- package/dist/{clsx-CxjWSCcU.d.ts → clsx-BLLSnqAa.d.ts} +17 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3 -1
- package/dist/index.web.d.ts +1 -1
- package/dist/index.web.js +3 -1
- package/dist/index.web.mjs +3 -1
- package/package.json +1 -1
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
|
-
##
|
|
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
|
-
|
|
17
|
-
the app — each is a genuinely different setup, not layered options:
|
|
28
|
+
## 2. Set up your project
|
|
18
29
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
-
|
|
25
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
```
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
106
|
-
|
|
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
|
-
//
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
119
|
-
|
|
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
|
-
|
|
122
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
142
|
-
app and `@kbach/react-native` share a workspace root.
|
|
214
|
+
### `theme` (replace instead of extend)
|
|
143
215
|
|
|
144
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
318
|
-
|
|
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
|
-
|
|
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
|
|
352
|
-
|
|
353
|
-
|
|
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-
|
|
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
|
-
|
|
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));
|
package/dist/index.web.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, h as applyKbachConfig, i as clsx, r as resolveKbachConfig, u as useColors, j as useDynamicToken, k as useTheme } from './clsx-
|
|
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
|
-
|
|
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));
|
package/dist/index.web.mjs
CHANGED
|
@@ -211,11 +211,13 @@ function makeColorsProxy(theme, isDark) {
|
|
|
211
211
|
}
|
|
212
212
|
});
|
|
213
213
|
}
|
|
214
|
-
|
|
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.
|
|
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",
|