@kbach/react-native 1.0.0-beta.36 → 1.0.0-beta.38
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
|
@@ -1,510 +1,125 @@
|
|
|
1
1
|
# @kbach/react-native
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Utility-class styling for React Native — the same class names as
|
|
4
|
+
[`@kbach/react`](https://www.npmjs.com/package/@kbach/react), resolved to a
|
|
5
|
+
`StyleSheet`-ready style object instead of CSS.
|
|
6
6
|
|
|
7
|
-
> Beta
|
|
8
|
-
> module — no Rust toolchain or manual
|
|
7
|
+
> **Beta.** Native is Android-only (no iOS bridge yet). Ships a prebuilt
|
|
8
|
+
> native module — no Rust toolchain or manual linking.
|
|
9
9
|
|
|
10
|
-
##
|
|
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
|
|
10
|
+
## Install
|
|
23
11
|
|
|
24
12
|
```sh
|
|
25
|
-
npm install @kbach/react-native
|
|
13
|
+
npm install @kbach/react-native@beta
|
|
26
14
|
```
|
|
27
15
|
|
|
28
|
-
##
|
|
16
|
+
## Setup
|
|
29
17
|
|
|
30
|
-
|
|
31
|
-
running an RN/Expo app — read only the one that matches you, not all four.
|
|
32
|
-
|
|
33
|
-
| How you run your app | Read |
|
|
18
|
+
| Target | What to do |
|
|
34
19
|
| :-- | :-- |
|
|
35
|
-
|
|
|
36
|
-
|
|
|
37
|
-
|
|
|
38
|
-
|
|
|
39
|
-
|
|
40
|
-
### React Native CLI
|
|
41
|
-
|
|
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
|
-
```
|
|
51
|
-
|
|
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).
|
|
55
|
-
|
|
56
|
-
### Expo Go
|
|
57
|
-
|
|
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
|
-
|
|
62
|
-
Expo Go can't load third-party native code, so this falls back to a pure-JS
|
|
63
|
-
engine automatically. Covers layout, spacing, border/radius, plain colors,
|
|
64
|
-
font-weight, text-transform/decoration, line-height, letter-spacing,
|
|
65
|
-
shadows, and transforms (`translate-x`/`translate-y`, `scale`/`scale-x`/
|
|
66
|
-
`scale-y`, `rotate`/`rotate-x`/`rotate-y`/`rotate-z`, `skew-x`/`skew-y`,
|
|
67
|
-
`transform-none`, `backface-visible`/`backface-hidden`). Doesn't cover grid,
|
|
68
|
-
filters, gradients, DOM-only typography, or the rest of the transform family
|
|
69
|
-
(`translate-z`/`scale-z`, `perspective`/`perspective-origin`/`origin`,
|
|
70
|
-
`transform-gpu`/`transform-cpu` — no RN equivalent for any of these) — same
|
|
71
|
-
scope native itself excludes.
|
|
72
|
-
|
|
73
|
-
### Expo (dev client / prebuild)
|
|
74
|
-
|
|
75
|
-
Gets you the full native engine instead of the JS fallback.
|
|
76
|
-
|
|
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:
|
|
81
|
-
|
|
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
|
-
```
|
|
101
|
-
|
|
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).
|
|
104
|
-
|
|
105
|
-
### Expo Web
|
|
106
|
-
|
|
107
|
-
**Nothing to configure at all.** Resolves through the same real-CSS engine
|
|
108
|
-
`@kbach/react` uses on the DOM, with the FULL modifier set (`group-hover:`,
|
|
109
|
-
`peer-hover:`, container queries, `has-[...]:`, etc.) — everything real
|
|
110
|
-
Tailwind supports. Native supports a smaller, explicitly-scoped subset; see
|
|
111
|
-
[Modifiers on native](#5-modifiers-on-native) for exactly what that covers.
|
|
112
|
-
|
|
113
|
-
**Static rendering (SSR) caveat:** `dark:`/responsive classes on content
|
|
114
|
-
that's part of the initial pre-rendered HTML resolve against the *server*,
|
|
115
|
-
not the visiting browser — so a user in dark mode can briefly see the light
|
|
116
|
-
version on first paint. Defer preference-dependent classes until mounted:
|
|
20
|
+
| **Expo Go** | Nothing. A pure-JS fallback engine loads automatically. |
|
|
21
|
+
| **Expo Web** / `react-native-web` | Nothing. Resolves through the same CSS engine `@kbach/react` uses. |
|
|
22
|
+
| **Expo dev client / prebuild / EAS** | Add the babel plugin (below), then `npx expo prebuild && npx expo run:android`. |
|
|
23
|
+
| **React Native CLI** | Add the babel plugin (below), then rebuild the app. |
|
|
117
24
|
|
|
118
|
-
|
|
119
|
-
const [mounted, setMounted] = useState(false);
|
|
120
|
-
useEffect(() => setMounted(true), []);
|
|
121
|
-
|
|
122
|
-
<View className={mounted ? 'bg-gray-1 dark:bg-gray-12' : 'bg-gray-1'} />;
|
|
123
|
-
```
|
|
124
|
-
|
|
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):
|
|
25
|
+
The babel plugin, added to your existing `babel.config.js`:
|
|
164
26
|
|
|
165
27
|
```js
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
}
|
|
28
|
+
// Expo — keep the function form your project already has:
|
|
29
|
+
module.exports = function (api) {
|
|
30
|
+
api.cache(true);
|
|
31
|
+
return {
|
|
32
|
+
presets: ['babel-preset-expo'],
|
|
33
|
+
plugins: ['@kbach/react-native/babel-plugin'],
|
|
34
|
+
};
|
|
35
|
+
};
|
|
173
36
|
```
|
|
174
37
|
|
|
175
|
-
**Mode-aware colors** (a different value per light/dark) — two equivalent
|
|
176
|
-
ways to write one:
|
|
177
|
-
|
|
178
38
|
```js
|
|
179
|
-
//
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
}
|
|
39
|
+
// React Native CLI:
|
|
40
|
+
module.exports = {
|
|
41
|
+
presets: ['module:@react-native/babel-preset'],
|
|
42
|
+
plugins: ['@kbach/react-native/babel-plugin'],
|
|
43
|
+
};
|
|
193
44
|
```
|
|
194
45
|
|
|
195
|
-
|
|
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).
|
|
46
|
+
Then write classes:
|
|
200
47
|
|
|
201
|
-
|
|
48
|
+
```tsx
|
|
49
|
+
import { View, Text } from 'react-native';
|
|
202
50
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
};
|
|
51
|
+
<View className="flex-1 items-center justify-center bg-white dark:bg-black">
|
|
52
|
+
<Text className="text-lg font-bold">Hello Kbach</Text>
|
|
53
|
+
</View>;
|
|
212
54
|
```
|
|
213
55
|
|
|
214
|
-
|
|
56
|
+
## Theme
|
|
215
57
|
|
|
216
|
-
`
|
|
217
|
-
|
|
218
|
-
you want to drop the default palette/scale altogether:
|
|
58
|
+
Optional. Create `kbach.config.js` at your project root — the babel plugin
|
|
59
|
+
picks it up automatically, no import or `applyKbachConfig()` call needed:
|
|
219
60
|
|
|
220
61
|
```js
|
|
62
|
+
// kbach.config.js
|
|
221
63
|
module.exports = {
|
|
222
|
-
|
|
223
|
-
colors: {
|
|
64
|
+
extend: {
|
|
65
|
+
colors: {
|
|
66
|
+
brand: '#ff6b35',
|
|
67
|
+
surface: 'gray-2',
|
|
68
|
+
dark: { surface: 'gray-11' }, // makes `bg-surface` mode-aware
|
|
69
|
+
},
|
|
224
70
|
},
|
|
225
71
|
};
|
|
226
72
|
```
|
|
227
73
|
|
|
228
|
-
|
|
74
|
+
It's plain JavaScript — **quote any key containing a hyphen**
|
|
75
|
+
(`'surface-dim': '#121318'`).
|
|
229
76
|
|
|
230
|
-
|
|
231
|
-
|
|
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).
|
|
77
|
+
Full config reference (colors, spacing, breakpoints, fonts, dark mode) is
|
|
78
|
+
in [AGENTS.md](https://github.com/semsakadanupol/kbach/blob/main/AGENTS.md).
|
|
239
79
|
|
|
240
|
-
##
|
|
80
|
+
## Dark mode
|
|
241
81
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
there's no build-time CSS step here to keep in sync with the runtime:
|
|
245
|
-
`dark:` is resolved fresh from a live parameter on native (no selectors at
|
|
246
|
-
all to generate ahead of time), and Expo Web's default strategy is already
|
|
247
|
-
`'attribute'` — a manual toggle just works, no config file, no
|
|
248
|
-
`applyKbachConfig()` call:
|
|
82
|
+
Zero setup on every target. `dark:` classes react to the OS setting;
|
|
83
|
+
`useTheme()` gives you a manual toggle:
|
|
249
84
|
|
|
250
85
|
```tsx
|
|
251
86
|
import { useTheme } from '@kbach/react-native';
|
|
252
87
|
|
|
253
|
-
|
|
254
|
-
const { mode, isDark, setMode, toggle } = useTheme();
|
|
255
|
-
return <Pressable onPress={toggle}><Text>{mode}</Text></Pressable>;
|
|
256
|
-
}
|
|
88
|
+
const { mode, isDark, setMode, toggle } = useTheme();
|
|
257
89
|
```
|
|
258
90
|
|
|
259
|
-
Works anywhere, no provider
|
|
260
|
-
|
|
261
|
-
`getGlobalDarkMode()`/`toggleGlobalDarkMode()` instead.
|
|
91
|
+
Works anywhere, no provider. `<ThemeProvider>` is optional — for seeding a
|
|
92
|
+
startup default and opt-in persistence (`<ThemeProvider persist={AsyncStorage}>`).
|
|
262
93
|
|
|
263
|
-
|
|
264
|
-
app launches by default (no forced storage dependency). To persist, pass
|
|
265
|
-
any async key-value store exposing `getItem`/`setItem` as `<ThemeProvider
|
|
266
|
-
persist>` — `@react-native-async-storage/async-storage`'s default export
|
|
267
|
-
already matches this shape exactly:
|
|
94
|
+
## Colors as values
|
|
268
95
|
|
|
269
|
-
|
|
270
|
-
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
271
|
-
import { ThemeProvider } from '@kbach/react-native';
|
|
272
|
-
|
|
273
|
-
<ThemeProvider persist={AsyncStorage}>
|
|
274
|
-
<App />
|
|
275
|
-
</ThemeProvider>;
|
|
276
|
-
```
|
|
277
|
-
|
|
278
|
-
The persisted mode (if any) is read once on mount and wins over
|
|
279
|
-
`defaultMode`; every later explicit choice is written back automatically.
|
|
280
|
-
`false` by default — this package still never imports a storage library
|
|
281
|
-
itself, so nothing changes for apps that don't opt in. Prefer wiring your
|
|
282
|
-
own storage manually instead? `setGlobalThemeMode()` before your first
|
|
283
|
-
render still works exactly as before.
|
|
284
|
-
|
|
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
|
|
318
|
-
|
|
319
|
-
For anywhere you need a real color, not a `className` — a chart prop, a
|
|
320
|
-
native shadow color:
|
|
96
|
+
For a real color string rather than a class — a chart prop, a shadow color:
|
|
321
97
|
|
|
322
98
|
```tsx
|
|
323
99
|
import { useColors } from '@kbach/react-native';
|
|
324
100
|
|
|
325
101
|
const colors = useColors();
|
|
326
|
-
colors.blue[6];
|
|
327
|
-
colors.blue['6/50'];
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
A shade-family lookup (`colors.blue[n]`) auto-mirrors to shade `13 - n` in
|
|
331
|
-
dark mode — this palette's own convention is 1 = lightest, 12 = darkest, so
|
|
332
|
-
that's the same visual weight relative to its own background, no manual
|
|
333
|
-
`colors.blue[isDark ? 7 : 6]` needed.
|
|
334
|
-
|
|
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()`
|
|
356
|
-
|
|
357
|
-
```tsx
|
|
358
|
-
<View className="p-[calc(16px+8px)]" /> // -> padding: 24
|
|
359
|
-
<View className="w-[clamp(1rem,2rem,3rem)]" /> // -> width: 32
|
|
360
|
-
```
|
|
361
|
-
|
|
362
|
-
Resolves at **build/resolve time**, not runtime — every operand must be a
|
|
363
|
-
constant `px`/`rem` length (or a bare number). This works because native has
|
|
364
|
-
no CSS engine to evaluate a real `calc()` string at paint time, so the only
|
|
365
|
-
way to support it at all is to compute the answer ourselves.
|
|
366
|
-
|
|
367
|
-
A **viewport-relative** expression (`calc(10vw+8px)`) can't be reduced this
|
|
368
|
-
way — no such unit exists on native at all. It already works fine on web
|
|
369
|
-
(real CSS); on native the declaration is dropped and a `console.warn`
|
|
370
|
-
explains why (dev builds only).
|
|
371
|
-
|
|
372
|
-
**Percentage-relative width/height** (`calc(100%-3rem)`, `calc(50%+16px)`,
|
|
373
|
-
`min(50%,20rem)`, `max(1rem,5%)`, `clamp(16rem,50%,32rem)`) IS supported,
|
|
374
|
-
differently: since native has no way to know "100% of what" ahead of time,
|
|
375
|
-
this measures the element's own real layout instead —
|
|
376
|
-
|
|
377
|
-
```tsx
|
|
378
|
-
<View className="w-[calc(100%_-_3rem)]" />
|
|
379
|
-
<View className="w-[min(50%,20rem)]" /> // half the parent, capped at a fixed size
|
|
380
|
-
<View className="w-[clamp(16rem,50%,32rem)]" />
|
|
102
|
+
colors.blue[6]; // '#2563eb'
|
|
103
|
+
colors.blue['6/50']; // same at 50% opacity
|
|
104
|
+
colors.get('brand'); // a custom color, typed as plain `string`
|
|
381
105
|
```
|
|
382
106
|
|
|
383
|
-
|
|
384
|
-
RN's own layout engine against the parent) purely to trigger a layout
|
|
385
|
-
event, then immediately corrects to the actual computed number once
|
|
386
|
-
measured — a genuine one-frame measure-then-snap, not instant math like the
|
|
387
|
-
constant-only case above, and it keeps re-measuring on every layout (screen
|
|
388
|
-
rotation, parent resize, ...) rather than freezing after the first value.
|
|
389
|
-
Scoped narrowly, same "reduce fully or bail" philosophy as the constant-only
|
|
390
|
-
case: `calc()` takes exactly one percentage term plus one constant term;
|
|
391
|
-
`min()`/`max()` take two or more comma-separated arguments, each EITHER a
|
|
392
|
-
plain percentage or a constant px/rem expression (not both mixed in one
|
|
393
|
-
argument — `min(50%-1rem,20rem)` is out of scope); `clamp()` takes exactly
|
|
394
|
-
three. `width`/`height` only in every case — other properties, or anything
|
|
395
|
-
that doesn't reduce fully, falls through to the same drop-and-warn behavior
|
|
396
|
-
as before.
|
|
397
|
-
|
|
398
|
-
**Arbitrary values can't contain literal spaces** — this is a pre-existing
|
|
399
|
-
rule (real Tailwind has the same one), not specific to `calc()`: a space
|
|
400
|
-
inside `[...]` is indistinguishable from the whitespace that separates
|
|
401
|
-
class tokens in the first place, so `w-[calc(100% - 3rem)]` silently splits
|
|
402
|
-
into three broken tokens before `calc()` is ever even recognized. Write
|
|
403
|
-
`w-[calc(100%_-_3rem)]` (underscores) instead — this is exactly what the
|
|
404
|
-
unknown-class warning below is for: it'll tell you when this happens.
|
|
405
|
-
|
|
406
|
-
### Unknown-class warnings (typo detection)
|
|
407
|
-
|
|
408
|
-
```tsx
|
|
409
|
-
<View className="flexx-center" /> // real Kbach classes never have to guess — a typo warns
|
|
410
|
-
```
|
|
411
|
-
|
|
412
|
-
```
|
|
413
|
-
Kbach: "flexx-center" doesn't match any known Kbach utility — typo? (skipped) (from className "flexx-center")
|
|
414
|
-
```
|
|
107
|
+
## What differs on native
|
|
415
108
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
never warns — that's an intentional, documented platform gap, not a typo,
|
|
423
|
-
so it stays silent the same way it always has.
|
|
109
|
+
React Native has no CSS cascade, so a fixed subset of modifiers is
|
|
110
|
+
supported — `dark:`, `sm:`–`2xl:`, `active:`, `hover:`, `focus:`,
|
|
111
|
+
`disabled:`, and `data-[…]:`/`aria-[…]:`. `group-*`/`peer-*`/`has-[…]` and
|
|
112
|
+
container queries parse without error but do not apply. When two classes
|
|
113
|
+
set the same property the more-specific one wins regardless of source
|
|
114
|
+
order (`dark:bg-black bg-white` → black in dark mode).
|
|
424
115
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
distinguish a genuine typo from a real-but-native-unsupported class
|
|
429
|
-
without a much larger port. A real dev-client/CLI build always gets this
|
|
430
|
-
check.
|
|
431
|
-
|
|
432
|
-
### Dynamic tokens
|
|
433
|
-
|
|
434
|
-
The native counterpart to a real CSS custom property — `var(--x)` already
|
|
435
|
-
works in a `className` on Expo Web with zero help from this package (real
|
|
436
|
-
CSS, resolved by the browser). This gives native the same className syntax:
|
|
437
|
-
|
|
438
|
-
```tsx
|
|
439
|
-
import { setDynamicToken, useDynamicToken } from '@kbach/react-native';
|
|
440
|
-
|
|
441
|
-
setDynamicToken('sidebar-width', '240px'); // updates every subscribed element, on both platforms
|
|
442
|
-
<View className="w-[var(--sidebar-width)]" />
|
|
443
|
-
|
|
444
|
-
// Reading the raw value in JS, reactively:
|
|
445
|
-
function Sidebar() {
|
|
446
|
-
const width = useDynamicToken('sidebar-width'); // '240px' | undefined
|
|
447
|
-
}
|
|
448
|
-
```
|
|
449
|
-
|
|
450
|
-
On web, `setDynamicToken` writes the real CSS custom property to the DOM —
|
|
451
|
-
every element using it repaints instantly via the browser's own CSS engine,
|
|
452
|
-
no React re-render involved. On native, the className is substituted with
|
|
453
|
-
the token's current value before every resolve, and an element referencing
|
|
454
|
-
a token re-renders/repaints on its own when that token changes (same
|
|
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
|
-
```
|
|
116
|
+
Expo Go covers a documented subset of utilities (no grid, filters,
|
|
117
|
+
gradients, or DOM-only typography). Expo Web and a real dev-client build
|
|
118
|
+
cover the full set.
|
|
505
119
|
|
|
506
|
-
|
|
507
|
-
|
|
120
|
+
See [AGENTS.md](https://github.com/semsakadanupol/kbach/blob/main/AGENTS.md)
|
|
121
|
+
for the complete reference — modifier list, `calc()`/`min()`/`max()`
|
|
122
|
+
support, dynamic tokens, monorepo setup, and how the engine works.
|
|
508
123
|
|
|
509
124
|
## License
|
|
510
125
|
|
|
Binary file
|
|
Binary file
|
package/dist/index.js
CHANGED
|
@@ -1605,7 +1605,9 @@ var Evaluator = class {
|
|
|
1605
1605
|
|
|
1606
1606
|
// src/jsEngine/resolveStyle.ts
|
|
1607
1607
|
var RESPONSIVE_BREAKPOINTS = /* @__PURE__ */ new Set(["sm", "md", "lg", "xl", "2xl"]);
|
|
1608
|
+
var STATE_HOLD_MARKER = "_kbon";
|
|
1608
1609
|
function nativeModifierState(modifier, theme, colorScheme, pressed, width) {
|
|
1610
|
+
if (modifier === STATE_HOLD_MARKER) return true;
|
|
1609
1611
|
if (modifier === "dark") return colorScheme === "dark";
|
|
1610
1612
|
if (modifier === "active") return pressed;
|
|
1611
1613
|
if (RESPONSIVE_BREAKPOINTS.has(modifier)) {
|
|
@@ -1744,6 +1746,8 @@ function resolveStyleJsWithWarnings(classString, theme, colorScheme, pressed, wi
|
|
|
1744
1746
|
const warnings = [];
|
|
1745
1747
|
let shadowOffset = null;
|
|
1746
1748
|
const transformOps = /* @__PURE__ */ new Map();
|
|
1749
|
+
const keySpecificity = /* @__PURE__ */ new Map();
|
|
1750
|
+
const transformSpecificity = /* @__PURE__ */ new Map();
|
|
1747
1751
|
for (const rawToken of classString.split(/\s+/).filter(Boolean)) {
|
|
1748
1752
|
const token = substituteModeAwareColorToken(rawToken, theme, colorScheme);
|
|
1749
1753
|
const parsed = parseClass(token);
|
|
@@ -1753,31 +1757,44 @@ function resolveStyleJsWithWarnings(classString, theme, colorScheme, pressed, wi
|
|
|
1753
1757
|
if (!allModifiersHold) continue;
|
|
1754
1758
|
const decls = resolveUtilityNative(parsed, theme);
|
|
1755
1759
|
if (decls === null) continue;
|
|
1760
|
+
const spec = parsed.modifiers.length;
|
|
1756
1761
|
for (const d of decls) {
|
|
1757
1762
|
if (d.property.startsWith("__")) {
|
|
1758
1763
|
continue;
|
|
1759
1764
|
}
|
|
1760
1765
|
if (d.property === "shadow-offset-x" || d.property === "shadow-offset-y") {
|
|
1766
|
+
if (spec < (keySpecificity.get("shadowOffset") ?? 0)) continue;
|
|
1761
1767
|
const n = Number(d.value);
|
|
1762
1768
|
if (!Number.isFinite(n)) continue;
|
|
1763
1769
|
if (shadowOffset === null) shadowOffset = {};
|
|
1764
1770
|
shadowOffset[d.property === "shadow-offset-x" ? "width" : "height"] = n;
|
|
1771
|
+
keySpecificity.set("shadowOffset", spec);
|
|
1765
1772
|
continue;
|
|
1766
1773
|
}
|
|
1767
1774
|
if (d.property === "transform-op-none") {
|
|
1768
1775
|
transformOps.clear();
|
|
1776
|
+
transformSpecificity.clear();
|
|
1769
1777
|
continue;
|
|
1770
1778
|
}
|
|
1771
1779
|
const opKey = TRANSFORM_OP_KEYS.get(d.property);
|
|
1772
1780
|
if (opKey !== void 0) {
|
|
1781
|
+
if (spec < (transformSpecificity.get(opKey) ?? 0)) continue;
|
|
1773
1782
|
const { value: value2, warning: warning2 } = rnStyleValue(d.property, d.value);
|
|
1774
1783
|
if (warning2 !== null) warnings.push(warning2);
|
|
1775
|
-
if (value2 !== null)
|
|
1784
|
+
if (value2 !== null) {
|
|
1785
|
+
transformOps.set(opKey, value2);
|
|
1786
|
+
transformSpecificity.set(opKey, spec);
|
|
1787
|
+
}
|
|
1776
1788
|
continue;
|
|
1777
1789
|
}
|
|
1790
|
+
const key = kebabToCamel(d.property);
|
|
1791
|
+
if (spec < (keySpecificity.get(key) ?? 0)) continue;
|
|
1778
1792
|
const { value, warning } = rnStyleValue(d.property, d.value);
|
|
1779
1793
|
if (warning !== null) warnings.push(warning);
|
|
1780
|
-
if (value !== null)
|
|
1794
|
+
if (value !== null) {
|
|
1795
|
+
style[key] = value;
|
|
1796
|
+
keySpecificity.set(key, spec);
|
|
1797
|
+
}
|
|
1781
1798
|
}
|
|
1782
1799
|
}
|
|
1783
1800
|
if (shadowOffset !== null) {
|
package/dist/jsx-dev-runtime.js
CHANGED
|
@@ -725,9 +725,10 @@ function propBasedModifierState(name, states, hostRest) {
|
|
|
725
725
|
if (ariaInner !== null) return evalAttrCondition(hostRest, "aria-", ariaInner);
|
|
726
726
|
return null;
|
|
727
727
|
}
|
|
728
|
-
|
|
728
|
+
var STATE_HOLD_MARKER = "_kbon";
|
|
729
|
+
function markSatisfiedStateModifiers(parts, namesToReplace) {
|
|
729
730
|
const utility = parts[parts.length - 1] ?? "";
|
|
730
|
-
const modifiers = parts.slice(0, -1).
|
|
731
|
+
const modifiers = parts.slice(0, -1).map((m) => namesToReplace.has(m) ? STATE_HOLD_MARKER : m);
|
|
731
732
|
return [...modifiers, utility].join(":");
|
|
732
733
|
}
|
|
733
734
|
function substituteStateModifiers(classStrRaw, states, hostRest) {
|
|
@@ -745,7 +746,7 @@ function substituteStateModifiers(classStrRaw, states, hostRest) {
|
|
|
745
746
|
}
|
|
746
747
|
if (required.every(([, v]) => v === true)) {
|
|
747
748
|
const requiredNames = new Set(required.map(([m]) => m));
|
|
748
|
-
kept.push(
|
|
749
|
+
kept.push(markSatisfiedStateModifiers(parts, requiredNames));
|
|
749
750
|
}
|
|
750
751
|
}
|
|
751
752
|
return kept.join(" ");
|
|
@@ -1988,7 +1989,9 @@ function resolveUtilityNative(parsed, theme) {
|
|
|
1988
1989
|
|
|
1989
1990
|
// src/jsEngine/resolveStyle.ts
|
|
1990
1991
|
var RESPONSIVE_BREAKPOINTS = /* @__PURE__ */ new Set(["sm", "md", "lg", "xl", "2xl"]);
|
|
1992
|
+
var STATE_HOLD_MARKER2 = "_kbon";
|
|
1991
1993
|
function nativeModifierState(modifier, theme, colorScheme, pressed, width) {
|
|
1994
|
+
if (modifier === STATE_HOLD_MARKER2) return true;
|
|
1992
1995
|
if (modifier === "dark") return colorScheme === "dark";
|
|
1993
1996
|
if (modifier === "active") return pressed;
|
|
1994
1997
|
if (RESPONSIVE_BREAKPOINTS.has(modifier)) {
|
|
@@ -2127,6 +2130,8 @@ function resolveStyleJsWithWarnings(classString, theme, colorScheme, pressed, wi
|
|
|
2127
2130
|
const warnings = [];
|
|
2128
2131
|
let shadowOffset = null;
|
|
2129
2132
|
const transformOps = /* @__PURE__ */ new Map();
|
|
2133
|
+
const keySpecificity = /* @__PURE__ */ new Map();
|
|
2134
|
+
const transformSpecificity = /* @__PURE__ */ new Map();
|
|
2130
2135
|
for (const rawToken of classString.split(/\s+/).filter(Boolean)) {
|
|
2131
2136
|
const token = substituteModeAwareColorToken(rawToken, theme, colorScheme);
|
|
2132
2137
|
const parsed = parseClass(token);
|
|
@@ -2136,31 +2141,44 @@ function resolveStyleJsWithWarnings(classString, theme, colorScheme, pressed, wi
|
|
|
2136
2141
|
if (!allModifiersHold) continue;
|
|
2137
2142
|
const decls = resolveUtilityNative(parsed, theme);
|
|
2138
2143
|
if (decls === null) continue;
|
|
2144
|
+
const spec = parsed.modifiers.length;
|
|
2139
2145
|
for (const d of decls) {
|
|
2140
2146
|
if (d.property.startsWith("__")) {
|
|
2141
2147
|
continue;
|
|
2142
2148
|
}
|
|
2143
2149
|
if (d.property === "shadow-offset-x" || d.property === "shadow-offset-y") {
|
|
2150
|
+
if (spec < (keySpecificity.get("shadowOffset") ?? 0)) continue;
|
|
2144
2151
|
const n = Number(d.value);
|
|
2145
2152
|
if (!Number.isFinite(n)) continue;
|
|
2146
2153
|
if (shadowOffset === null) shadowOffset = {};
|
|
2147
2154
|
shadowOffset[d.property === "shadow-offset-x" ? "width" : "height"] = n;
|
|
2155
|
+
keySpecificity.set("shadowOffset", spec);
|
|
2148
2156
|
continue;
|
|
2149
2157
|
}
|
|
2150
2158
|
if (d.property === "transform-op-none") {
|
|
2151
2159
|
transformOps.clear();
|
|
2160
|
+
transformSpecificity.clear();
|
|
2152
2161
|
continue;
|
|
2153
2162
|
}
|
|
2154
2163
|
const opKey = TRANSFORM_OP_KEYS.get(d.property);
|
|
2155
2164
|
if (opKey !== void 0) {
|
|
2165
|
+
if (spec < (transformSpecificity.get(opKey) ?? 0)) continue;
|
|
2156
2166
|
const { value: value2, warning: warning2 } = rnStyleValue(d.property, d.value);
|
|
2157
2167
|
if (warning2 !== null) warnings.push(warning2);
|
|
2158
|
-
if (value2 !== null)
|
|
2168
|
+
if (value2 !== null) {
|
|
2169
|
+
transformOps.set(opKey, value2);
|
|
2170
|
+
transformSpecificity.set(opKey, spec);
|
|
2171
|
+
}
|
|
2159
2172
|
continue;
|
|
2160
2173
|
}
|
|
2174
|
+
const key = kebabToCamel(d.property);
|
|
2175
|
+
if (spec < (keySpecificity.get(key) ?? 0)) continue;
|
|
2161
2176
|
const { value, warning } = rnStyleValue(d.property, d.value);
|
|
2162
2177
|
if (warning !== null) warnings.push(warning);
|
|
2163
|
-
if (value !== null)
|
|
2178
|
+
if (value !== null) {
|
|
2179
|
+
style[key] = value;
|
|
2180
|
+
keySpecificity.set(key, spec);
|
|
2181
|
+
}
|
|
2164
2182
|
}
|
|
2165
2183
|
}
|
|
2166
2184
|
if (shadowOffset !== null) {
|
package/dist/jsx-runtime.js
CHANGED
|
@@ -726,9 +726,10 @@ function propBasedModifierState(name, states, hostRest) {
|
|
|
726
726
|
if (ariaInner !== null) return evalAttrCondition(hostRest, "aria-", ariaInner);
|
|
727
727
|
return null;
|
|
728
728
|
}
|
|
729
|
-
|
|
729
|
+
var STATE_HOLD_MARKER = "_kbon";
|
|
730
|
+
function markSatisfiedStateModifiers(parts, namesToReplace) {
|
|
730
731
|
const utility = parts[parts.length - 1] ?? "";
|
|
731
|
-
const modifiers = parts.slice(0, -1).
|
|
732
|
+
const modifiers = parts.slice(0, -1).map((m) => namesToReplace.has(m) ? STATE_HOLD_MARKER : m);
|
|
732
733
|
return [...modifiers, utility].join(":");
|
|
733
734
|
}
|
|
734
735
|
function substituteStateModifiers(classStrRaw, states, hostRest) {
|
|
@@ -746,7 +747,7 @@ function substituteStateModifiers(classStrRaw, states, hostRest) {
|
|
|
746
747
|
}
|
|
747
748
|
if (required.every(([, v]) => v === true)) {
|
|
748
749
|
const requiredNames = new Set(required.map(([m]) => m));
|
|
749
|
-
kept.push(
|
|
750
|
+
kept.push(markSatisfiedStateModifiers(parts, requiredNames));
|
|
750
751
|
}
|
|
751
752
|
}
|
|
752
753
|
return kept.join(" ");
|
|
@@ -1979,7 +1980,9 @@ function resolveUtilityNative(parsed, theme) {
|
|
|
1979
1980
|
|
|
1980
1981
|
// src/jsEngine/resolveStyle.ts
|
|
1981
1982
|
var RESPONSIVE_BREAKPOINTS = /* @__PURE__ */ new Set(["sm", "md", "lg", "xl", "2xl"]);
|
|
1983
|
+
var STATE_HOLD_MARKER2 = "_kbon";
|
|
1982
1984
|
function nativeModifierState(modifier, theme, colorScheme, pressed, width) {
|
|
1985
|
+
if (modifier === STATE_HOLD_MARKER2) return true;
|
|
1983
1986
|
if (modifier === "dark") return colorScheme === "dark";
|
|
1984
1987
|
if (modifier === "active") return pressed;
|
|
1985
1988
|
if (RESPONSIVE_BREAKPOINTS.has(modifier)) {
|
|
@@ -2118,6 +2121,8 @@ function resolveStyleJsWithWarnings(classString, theme, colorScheme, pressed, wi
|
|
|
2118
2121
|
const warnings = [];
|
|
2119
2122
|
let shadowOffset = null;
|
|
2120
2123
|
const transformOps = /* @__PURE__ */ new Map();
|
|
2124
|
+
const keySpecificity = /* @__PURE__ */ new Map();
|
|
2125
|
+
const transformSpecificity = /* @__PURE__ */ new Map();
|
|
2121
2126
|
for (const rawToken of classString.split(/\s+/).filter(Boolean)) {
|
|
2122
2127
|
const token = substituteModeAwareColorToken(rawToken, theme, colorScheme);
|
|
2123
2128
|
const parsed = parseClass(token);
|
|
@@ -2127,31 +2132,44 @@ function resolveStyleJsWithWarnings(classString, theme, colorScheme, pressed, wi
|
|
|
2127
2132
|
if (!allModifiersHold) continue;
|
|
2128
2133
|
const decls = resolveUtilityNative(parsed, theme);
|
|
2129
2134
|
if (decls === null) continue;
|
|
2135
|
+
const spec = parsed.modifiers.length;
|
|
2130
2136
|
for (const d of decls) {
|
|
2131
2137
|
if (d.property.startsWith("__")) {
|
|
2132
2138
|
continue;
|
|
2133
2139
|
}
|
|
2134
2140
|
if (d.property === "shadow-offset-x" || d.property === "shadow-offset-y") {
|
|
2141
|
+
if (spec < (keySpecificity.get("shadowOffset") ?? 0)) continue;
|
|
2135
2142
|
const n = Number(d.value);
|
|
2136
2143
|
if (!Number.isFinite(n)) continue;
|
|
2137
2144
|
if (shadowOffset === null) shadowOffset = {};
|
|
2138
2145
|
shadowOffset[d.property === "shadow-offset-x" ? "width" : "height"] = n;
|
|
2146
|
+
keySpecificity.set("shadowOffset", spec);
|
|
2139
2147
|
continue;
|
|
2140
2148
|
}
|
|
2141
2149
|
if (d.property === "transform-op-none") {
|
|
2142
2150
|
transformOps.clear();
|
|
2151
|
+
transformSpecificity.clear();
|
|
2143
2152
|
continue;
|
|
2144
2153
|
}
|
|
2145
2154
|
const opKey = TRANSFORM_OP_KEYS.get(d.property);
|
|
2146
2155
|
if (opKey !== void 0) {
|
|
2156
|
+
if (spec < (transformSpecificity.get(opKey) ?? 0)) continue;
|
|
2147
2157
|
const { value: value2, warning: warning2 } = rnStyleValue(d.property, d.value);
|
|
2148
2158
|
if (warning2 !== null) warnings.push(warning2);
|
|
2149
|
-
if (value2 !== null)
|
|
2159
|
+
if (value2 !== null) {
|
|
2160
|
+
transformOps.set(opKey, value2);
|
|
2161
|
+
transformSpecificity.set(opKey, spec);
|
|
2162
|
+
}
|
|
2150
2163
|
continue;
|
|
2151
2164
|
}
|
|
2165
|
+
const key = kebabToCamel(d.property);
|
|
2166
|
+
if (spec < (keySpecificity.get(key) ?? 0)) continue;
|
|
2152
2167
|
const { value, warning } = rnStyleValue(d.property, d.value);
|
|
2153
2168
|
if (warning !== null) warnings.push(warning);
|
|
2154
|
-
if (value !== null)
|
|
2169
|
+
if (value !== null) {
|
|
2170
|
+
style[key] = value;
|
|
2171
|
+
keySpecificity.set(key, spec);
|
|
2172
|
+
}
|
|
2155
2173
|
}
|
|
2156
2174
|
}
|
|
2157
2175
|
if (shadowOffset !== null) {
|
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.38",
|
|
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",
|