@kbach/react-native 1.0.0-beta.3 → 1.0.0-beta.31
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 +303 -95
- package/android/src/main/jniLibs/arm64-v8a/libkbach_core_engine.so +0 -0
- package/android/src/main/jniLibs/x86_64/libkbach_core_engine.so +0 -0
- package/dist/chunk-FLPPVP5G.mjs +40 -0
- package/dist/chunk-XPVSILI4.mjs +112 -0
- package/dist/chunk-ZYE4VIZE.mjs +564 -0
- package/dist/clsx-BCXALPGh.d.ts +239 -0
- package/dist/darkModeStore.d.ts +73 -0
- package/dist/darkModeStore.js +118 -0
- package/dist/dynamicTokens.d.ts +36 -0
- package/dist/dynamicTokens.js +75 -0
- package/dist/index.d.ts +11 -156
- package/dist/index.js +1994 -393
- package/dist/index.web.d.ts +18 -0
- package/dist/index.web.js +505 -0
- package/dist/index.web.mjs +266 -0
- package/dist/jsx-dev-runtime.d.ts +3 -12
- package/dist/jsx-dev-runtime.js +2143 -350
- package/dist/jsx-dev-runtime.web.d.ts +10 -0
- package/dist/jsx-dev-runtime.web.js +282 -0
- package/dist/jsx-dev-runtime.web.mjs +29 -0
- package/dist/jsx-runtime.d.ts +3 -26
- package/dist/jsx-runtime.js +2134 -342
- package/dist/jsx-runtime.web.d.ts +7 -0
- package/dist/jsx-runtime.web.js +264 -0
- package/dist/{jsx-runtime.mjs → jsx-runtime.web.mjs} +2 -2
- package/dist/theme.d.ts +54 -0
- package/dist/{chunk-7KUNKZBM.mjs → theme.js} +87 -72
- package/package.json +56 -11
- package/src/babel-plugin.js +74 -2
- package/dist/chunk-UP5ZN7CN.mjs +0 -44
- package/dist/index.d.mts +0 -161
- package/dist/index.mjs +0 -80
- package/dist/jsx-dev-runtime.d.mts +0 -19
- package/dist/jsx-dev-runtime.mjs +0 -20
- package/dist/jsx-runtime.d.mts +0 -30
package/README.md
CHANGED
|
@@ -2,32 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
Kbach's utility-class styling engine for React Native — the same class
|
|
4
4
|
names as [`@kbach/react`](https://www.npmjs.com/package/@kbach/react),
|
|
5
|
-
resolved to a
|
|
5
|
+
resolved to a `StyleSheet`-ready style object instead of injected CSS.
|
|
6
6
|
|
|
7
|
-
>
|
|
8
|
-
>
|
|
9
|
-
> identical across web and native. The native module ships prebuilt (see
|
|
10
|
-
> Setup below) — no Rust toolchain or manual native wiring required — but
|
|
11
|
-
> there's no iOS bridge yet.
|
|
7
|
+
> Beta. Native is Android-only (no iOS bridge yet). Ships a prebuilt native
|
|
8
|
+
> module — no Rust toolchain or manual native wiring required.
|
|
12
9
|
|
|
13
|
-
##
|
|
10
|
+
## Install
|
|
14
11
|
|
|
15
12
|
```sh
|
|
16
13
|
npm install @kbach/react-native
|
|
17
14
|
```
|
|
18
15
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
folder. Android's autolinking discovers it automatically the moment it's
|
|
22
|
-
in `node_modules`; no manual `MainApplication.kt` edit needed. Works the
|
|
23
|
-
same way whether your app was created with the React Native CLI or with
|
|
24
|
-
Expo — **except Expo Go**, which can never load this (or any) custom
|
|
25
|
-
native module; see below.
|
|
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:
|
|
26
18
|
|
|
27
|
-
|
|
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
|
|
28
26
|
|
|
29
|
-
|
|
30
|
-
instead of `react/jsx-runtime`:
|
|
27
|
+
### React Native CLI
|
|
31
28
|
|
|
32
29
|
```js
|
|
33
30
|
// babel.config.js
|
|
@@ -37,112 +34,323 @@ module.exports = {
|
|
|
37
34
|
};
|
|
38
35
|
```
|
|
39
36
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
[repo](https://github.com/semsakadanupol/kbach) for a full working
|
|
44
|
-
reference project, including the Metro config a monorepo setup like this
|
|
45
|
-
one needs (a plain single-app install doesn't need any of that — it's
|
|
46
|
-
only relevant if you're linking packages locally instead of installing
|
|
47
|
-
from npm).
|
|
37
|
+
Rebuild the Android app afterward so Gradle picks up the native module.
|
|
38
|
+
|
|
39
|
+
### Expo Go
|
|
48
40
|
|
|
49
|
-
|
|
41
|
+
Works out of the box — no prebuild, no dev client, nothing to configure.
|
|
42
|
+
Expo Go can't load third-party native code, so this falls back to a pure-JS
|
|
43
|
+
engine automatically. Covers layout, spacing, border/radius, plain colors,
|
|
44
|
+
font-weight, text-transform/decoration, line-height, letter-spacing,
|
|
45
|
+
shadows, and transforms (`translate-x`/`translate-y`, `scale`/`scale-x`/
|
|
46
|
+
`scale-y`, `rotate`/`rotate-x`/`rotate-y`/`rotate-z`, `skew-x`/`skew-y`,
|
|
47
|
+
`transform-none`, `backface-visible`/`backface-hidden`). Doesn't cover grid,
|
|
48
|
+
filters, gradients, DOM-only typography, or the rest of the transform family
|
|
49
|
+
(`translate-z`/`scale-z`, `perspective`/`perspective-origin`/`origin`,
|
|
50
|
+
`transform-gpu`/`transform-cpu` — no RN equivalent for any of these) — same
|
|
51
|
+
scope native itself excludes.
|
|
50
52
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
functionally the same "real native project" setup as the CLI path above,
|
|
55
|
-
just generated and driven through Expo's own tooling:
|
|
53
|
+
### Expo (dev client / prebuild)
|
|
54
|
+
|
|
55
|
+
Gets you the full native engine instead of the JS fallback:
|
|
56
56
|
|
|
57
57
|
```js
|
|
58
58
|
// babel.config.js
|
|
59
59
|
module.exports = {
|
|
60
|
-
presets: ['babel-preset-expo'],
|
|
60
|
+
presets: ['babel-preset-expo'],
|
|
61
61
|
plugins: ['@kbach/react-native/babel-plugin'],
|
|
62
62
|
};
|
|
63
63
|
```
|
|
64
64
|
|
|
65
65
|
```sh
|
|
66
|
-
npx expo prebuild
|
|
67
|
-
npx expo run:android
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
Also requires the New Architecture enabled (Expo's default in current SDK
|
|
71
|
-
versions) — the native module is a TurboModule, which doesn't exist under
|
|
72
|
-
the legacy bridge at all. If you're on an older SDK/config with the New
|
|
73
|
-
Architecture off, turn it on in `app.json`
|
|
74
|
-
(`"newArchEnabled": true` under `expo`) before prebuilding.
|
|
75
|
-
|
|
76
|
-
EAS Build works the same way — it runs the same `prebuild` step, so no
|
|
77
|
-
extra configuration beyond the two steps above.
|
|
78
|
-
|
|
79
|
-
## What this is
|
|
80
|
-
|
|
81
|
-
- A JSX runtime (`@kbach/react-native/jsx-runtime`) that intercepts
|
|
82
|
-
`className`/`kb` props on every element and calls into the native bridge
|
|
83
|
-
to resolve them to a style object, synchronously, on every render.
|
|
84
|
-
- A Babel plugin (`@kbach/react-native/babel-plugin`) that routes your
|
|
85
|
-
app's own files (never `node_modules`) through that JSX runtime via a
|
|
86
|
-
per-file `@jsxImportSource` pragma.
|
|
87
|
-
- `setTheme()`/`getTheme()` for customizing the color palette and spacing
|
|
88
|
-
scale — the same `ThemeConfig` shape `@kbach/react` uses, kept as an
|
|
89
|
-
independent type here so a native-only project isn't forced to depend on
|
|
90
|
-
a web-oriented package.
|
|
91
|
-
- `shadow-*` resolves to real native shadows — RN's discrete
|
|
92
|
-
`shadowColor`/`shadowOffset`/`shadowOpacity`/`shadowRadius` props plus
|
|
93
|
-
Android's `elevation` (both emitted together; `elevation` is what
|
|
94
|
-
actually renders a shadow on Android — the `shadow*` props are iOS-only
|
|
95
|
-
in RN itself, ready for whenever this package gets an iOS bridge),
|
|
96
|
-
derived from the same named tiers (`sm`/`DEFAULT`/`md`/`lg`/`xl`/`2xl`)
|
|
97
|
-
`@kbach/react`'s CSS `box-shadow` scale uses. `shadow-inner` and
|
|
98
|
-
arbitrary `shadow-[...]` values aren't supported — RN's native shadow
|
|
99
|
-
props can't express an inset shadow, and an arbitrary value is a raw CSS
|
|
100
|
-
string with no mechanical translation into discrete native props.
|
|
101
|
-
- Responsive breakpoints (`sm:`/`md:`/`lg:`/`xl:`/`2xl:`) — gated live
|
|
102
|
-
against the current window width (`Dimensions.get('window').width`),
|
|
103
|
-
same mobile-first "min-width and up" semantics as web, and freely
|
|
104
|
-
combinable with `dark:`/`active:` (e.g. `dark:md:bg-blue-8`). Call RN's
|
|
105
|
-
own `useWindowDimensions()` somewhere in your tree to re-render on
|
|
106
|
-
rotation/resize — same role `useColorScheme()` plays for `dark:` below.
|
|
66
|
+
npx expo prebuild
|
|
67
|
+
npx expo run:android
|
|
68
|
+
```
|
|
107
69
|
|
|
108
|
-
|
|
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.
|
|
73
|
+
|
|
74
|
+
### Expo Web
|
|
75
|
+
|
|
76
|
+
Works out of the box — resolves through the same real-CSS engine
|
|
77
|
+
`@kbach/react` uses on the DOM, with the FULL modifier set (`group-hover:`,
|
|
78
|
+
`peer-hover:`, container queries, `has-[...]:`, etc.) — everything real
|
|
79
|
+
Tailwind supports. Native supports a smaller, explicitly-scoped subset; see
|
|
80
|
+
"Modifiers on native" below for exactly what that covers.
|
|
109
81
|
|
|
110
|
-
|
|
82
|
+
**Static rendering (SSR) caveat:** `dark:`/responsive classes on content
|
|
83
|
+
that's part of the initial pre-rendered HTML resolve against the *server*,
|
|
84
|
+
not the visiting browser — so a user in dark mode can briefly see the light
|
|
85
|
+
version on first paint. Defer preference-dependent classes until mounted:
|
|
111
86
|
|
|
112
87
|
```tsx
|
|
113
|
-
|
|
88
|
+
const [mounted, setMounted] = useState(false);
|
|
89
|
+
useEffect(() => setMounted(true), []);
|
|
114
90
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
91
|
+
<View className={mounted ? 'bg-gray-1 dark:bg-gray-12' : 'bg-gray-1'} />;
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Monorepo installs (npm/yarn/pnpm workspaces)
|
|
95
|
+
|
|
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.
|
|
104
|
+
|
|
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`:
|
|
111
|
+
|
|
112
|
+
```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');
|
|
117
|
+
|
|
118
|
+
const projectRoot = __dirname;
|
|
119
|
+
const workspaceRoot = path.resolve(projectRoot, '../..'); // -> your monorepo root
|
|
120
|
+
|
|
121
|
+
const config = getDefaultConfig(projectRoot);
|
|
122
|
+
config.watchFolders = [workspaceRoot];
|
|
123
|
+
|
|
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
|
+
};
|
|
137
|
+
|
|
138
|
+
module.exports = config; // (React Native CLI: mergeConfig(getDefaultConfig(projectRoot), config))
|
|
119
139
|
```
|
|
120
140
|
|
|
121
|
-
|
|
122
|
-
|
|
141
|
+
A single, non-monorepo `npm install` never hits this — only relevant if your
|
|
142
|
+
app and `@kbach/react-native` share a workspace root.
|
|
143
|
+
|
|
144
|
+
## Modifiers on native
|
|
145
|
+
|
|
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:
|
|
150
|
+
|
|
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).
|
|
168
|
+
|
|
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.
|
|
175
|
+
|
|
176
|
+
## Dark mode
|
|
177
|
+
|
|
178
|
+
**Zero setup, on every platform this package supports** — real native
|
|
179
|
+
*and* Expo Web. Unlike [`@kbach/react`](https://www.npmjs.com/package/@kbach/react),
|
|
180
|
+
there's no build-time CSS step here to keep in sync with the runtime:
|
|
181
|
+
`dark:` is resolved fresh from a live parameter on native (no selectors at
|
|
182
|
+
all to generate ahead of time), and Expo Web's default strategy is already
|
|
183
|
+
`'attribute'` — a manual toggle just works, no config file, no
|
|
184
|
+
`applyKbachConfig()` call:
|
|
123
185
|
|
|
124
186
|
```tsx
|
|
125
|
-
import {
|
|
187
|
+
import { useTheme } from '@kbach/react-native';
|
|
126
188
|
|
|
127
|
-
function
|
|
189
|
+
function ThemeToggle() {
|
|
128
190
|
const { mode, isDark, setMode, toggle } = useTheme();
|
|
129
191
|
return <Pressable onPress={toggle}><Text>{mode}</Text></Pressable>;
|
|
130
192
|
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Works anywhere, no provider needed. `<ThemeProvider defaultMode="dark">` is
|
|
196
|
+
optional, for seeding a startup default. Outside React, use
|
|
197
|
+
`getGlobalDarkMode()`/`toggleGlobalDarkMode()` instead.
|
|
131
198
|
|
|
132
|
-
|
|
133
|
-
|
|
199
|
+
Follows the OS setting live until overridden, and isn't persisted across
|
|
200
|
+
app launches by default (no forced storage dependency). To persist, pass
|
|
201
|
+
any async key-value store exposing `getItem`/`setItem` as `<ThemeProvider
|
|
202
|
+
persist>` — `@react-native-async-storage/async-storage`'s default export
|
|
203
|
+
already matches this shape exactly:
|
|
204
|
+
|
|
205
|
+
```tsx
|
|
206
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
207
|
+
import { ThemeProvider } from '@kbach/react-native';
|
|
208
|
+
|
|
209
|
+
<ThemeProvider persist={AsyncStorage}>
|
|
210
|
+
<App />
|
|
134
211
|
</ThemeProvider>;
|
|
135
212
|
```
|
|
136
213
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
214
|
+
The persisted mode (if any) is read once on mount and wins over
|
|
215
|
+
`defaultMode`; every later explicit choice is written back automatically.
|
|
216
|
+
`false` by default — this package still never imports a storage library
|
|
217
|
+
itself, so nothing changes for apps that don't opt in. Prefer wiring your
|
|
218
|
+
own storage manually instead? `setGlobalThemeMode()` before your first
|
|
219
|
+
render still works exactly as before.
|
|
220
|
+
|
|
221
|
+
## Reading colors as values
|
|
222
|
+
|
|
223
|
+
For anywhere you need a real color, not a `className` — a chart prop, a
|
|
224
|
+
native shadow color:
|
|
225
|
+
|
|
226
|
+
```tsx
|
|
227
|
+
import { useColors } from '@kbach/react-native';
|
|
228
|
+
|
|
229
|
+
const colors = useColors();
|
|
230
|
+
colors.blue[6]; // '#2563eb' — static, same value in both modes
|
|
231
|
+
colors.blue['6/50']; // same, at 50% opacity
|
|
232
|
+
```
|
|
233
|
+
|
|
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
|
+
A shade-family lookup (`colors.blue[n]`) auto-mirrors to shade `13 - n` in
|
|
246
|
+
dark mode — this palette's own convention is 1 = lightest, 12 = darkest, so
|
|
247
|
+
that's the same visual weight relative to its own background, no manual
|
|
248
|
+
`colors.blue[isDark ? 7 : 6]` needed.
|
|
249
|
+
|
|
250
|
+
## Arbitrary math: `calc()`, `clamp()`, `min()`, `max()`
|
|
251
|
+
|
|
252
|
+
```tsx
|
|
253
|
+
<View className="p-[calc(16px+8px)]" /> // -> padding: 24
|
|
254
|
+
<View className="w-[clamp(1rem,2rem,3rem)]" /> // -> width: 32
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Resolves at **build/resolve time**, not runtime — every operand must be a
|
|
258
|
+
constant `px`/`rem` length (or a bare number). This works because native has
|
|
259
|
+
no CSS engine to evaluate a real `calc()` string at paint time, so the only
|
|
260
|
+
way to support it at all is to compute the answer ourselves.
|
|
261
|
+
|
|
262
|
+
A **viewport-relative** expression (`calc(10vw+8px)`) can't be reduced this
|
|
263
|
+
way — no such unit exists on native at all. It already works fine on web
|
|
264
|
+
(real CSS); on native the declaration is dropped and a `console.warn`
|
|
265
|
+
explains why (dev builds only).
|
|
266
|
+
|
|
267
|
+
**Percentage-relative width/height** (`calc(100%-3rem)`, `calc(50%+16px)`,
|
|
268
|
+
`min(50%,20rem)`, `max(1rem,5%)`, `clamp(16rem,50%,32rem)`) IS supported,
|
|
269
|
+
differently: since native has no way to know "100% of what" ahead of time,
|
|
270
|
+
this measures the element's own real layout instead —
|
|
271
|
+
|
|
272
|
+
```tsx
|
|
273
|
+
<View className="w-[calc(100%_-_3rem)]" />
|
|
274
|
+
<View className="w-[min(50%,20rem)]" /> // half the parent, capped at a fixed size
|
|
275
|
+
<View className="w-[clamp(16rem,50%,32rem)]" />
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Renders once at a plain `100%` (a real RN percentage, resolved correctly by
|
|
279
|
+
RN's own layout engine against the parent) purely to trigger a layout
|
|
280
|
+
event, then immediately corrects to the actual computed number once
|
|
281
|
+
measured — a genuine one-frame measure-then-snap, not instant math like the
|
|
282
|
+
constant-only case above, and it keeps re-measuring on every layout (screen
|
|
283
|
+
rotation, parent resize, ...) rather than freezing after the first value.
|
|
284
|
+
Scoped narrowly, same "reduce fully or bail" philosophy as the constant-only
|
|
285
|
+
case: `calc()` takes exactly one percentage term plus one constant term;
|
|
286
|
+
`min()`/`max()` take two or more comma-separated arguments, each EITHER a
|
|
287
|
+
plain percentage or a constant px/rem expression (not both mixed in one
|
|
288
|
+
argument — `min(50%-1rem,20rem)` is out of scope); `clamp()` takes exactly
|
|
289
|
+
three. `width`/`height` only in every case — other properties, or anything
|
|
290
|
+
that doesn't reduce fully, falls through to the same drop-and-warn behavior
|
|
291
|
+
as before.
|
|
292
|
+
|
|
293
|
+
**Arbitrary values can't contain literal spaces** — this is a pre-existing
|
|
294
|
+
rule (real Tailwind has the same one), not specific to `calc()`: a space
|
|
295
|
+
inside `[...]` is indistinguishable from the whitespace that separates
|
|
296
|
+
class tokens in the first place, so `w-[calc(100% - 3rem)]` silently splits
|
|
297
|
+
into three broken tokens before `calc()` is ever even recognized. Write
|
|
298
|
+
`w-[calc(100%_-_3rem)]` (underscores) instead — this is exactly what the
|
|
299
|
+
unknown-class warning below is for: it'll tell you when this happens.
|
|
300
|
+
|
|
301
|
+
## Unknown-class warnings (typo detection)
|
|
302
|
+
|
|
303
|
+
```tsx
|
|
304
|
+
<View className="flexx-center" /> // real Kbach classes never have to guess — a typo warns
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
```
|
|
308
|
+
Kbach: "flexx-center" doesn't match any known Kbach utility — typo? (skipped) (from className "flexx-center")
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
On native/dev-client builds, a class that doesn't match *any* Kbach
|
|
312
|
+
utility on *any* platform gets a `console.warn` (dev builds only, printed
|
|
313
|
+
once per unique message) instead of silently doing nothing — the same
|
|
314
|
+
signal `@kbach/react`'s Vite plugin already gives you at build time on
|
|
315
|
+
web, just at resolve time here instead. A real utility this engine simply
|
|
316
|
+
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.
|
|
320
|
+
|
|
321
|
+
Not available in Expo Go's pure-JS fallback engine today — that engine has
|
|
322
|
+
no web-shaped resolver to check "is this a real Kbach utility at all"
|
|
323
|
+
against, only the (smaller) native-supported subset, so it can't
|
|
324
|
+
distinguish a genuine typo from a real-but-native-unsupported class
|
|
325
|
+
without a much larger port. A real dev-client/CLI build always gets this
|
|
326
|
+
check.
|
|
327
|
+
|
|
328
|
+
## Dynamic tokens
|
|
329
|
+
|
|
330
|
+
The native counterpart to a real CSS custom property — `var(--x)` already
|
|
331
|
+
works in a `className` on Expo Web with zero help from this package (real
|
|
332
|
+
CSS, resolved by the browser). This gives native the same className syntax:
|
|
333
|
+
|
|
334
|
+
```tsx
|
|
335
|
+
import { setDynamicToken, useDynamicToken } from '@kbach/react-native';
|
|
336
|
+
|
|
337
|
+
setDynamicToken('sidebar-width', '240px'); // updates every subscribed element, on both platforms
|
|
338
|
+
<View className="w-[var(--sidebar-width)]" />
|
|
339
|
+
|
|
340
|
+
// Reading the raw value in JS, reactively:
|
|
341
|
+
function Sidebar() {
|
|
342
|
+
const width = useDynamicToken('sidebar-width'); // '240px' | undefined
|
|
343
|
+
}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
On web, `setDynamicToken` writes the real CSS custom property to the DOM —
|
|
347
|
+
every element using it repaints instantly via the browser's own CSS engine,
|
|
348
|
+
no React re-render involved. On native, the className is substituted with
|
|
349
|
+
the token's current value before every resolve, and an element referencing
|
|
350
|
+
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.
|
|
146
354
|
|
|
147
355
|
## License
|
|
148
356
|
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveClassName
|
|
3
|
+
} from "./chunk-ZYE4VIZE.mjs";
|
|
4
|
+
|
|
5
|
+
// src/jsxRuntimeCoreWeb.ts
|
|
6
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
7
|
+
import { Fragment } from "react";
|
|
8
|
+
function makeElement(isStaticChildren, type, props, key) {
|
|
9
|
+
return (isStaticChildren ? _jsxs : _jsx)(type, props, key);
|
|
10
|
+
}
|
|
11
|
+
function processElement(resolveClassName2, type, rawProps, key, isStaticChildren) {
|
|
12
|
+
if (type === null || typeof type === "symbol" || !rawProps) {
|
|
13
|
+
return makeElement(isStaticChildren, type, rawProps ?? {}, key);
|
|
14
|
+
}
|
|
15
|
+
const { className: classStrRaw, dataSet: userDataSet, ...rest } = rawProps;
|
|
16
|
+
if (typeof classStrRaw !== "string") {
|
|
17
|
+
return makeElement(isStaticChildren, type, rawProps, key);
|
|
18
|
+
}
|
|
19
|
+
const resolved = resolveClassName2(classStrRaw);
|
|
20
|
+
const dataSet = { ...userDataSet, kb: resolved };
|
|
21
|
+
return makeElement(isStaticChildren, type, { ...rest, className: classStrRaw, dataSet }, key);
|
|
22
|
+
}
|
|
23
|
+
function createJsxFunctionsWeb(resolveClassName2) {
|
|
24
|
+
function jsx2(type, props, key) {
|
|
25
|
+
return processElement(resolveClassName2, type, props, key, false);
|
|
26
|
+
}
|
|
27
|
+
function jsxs2(type, props, key) {
|
|
28
|
+
return processElement(resolveClassName2, type, props, key, true);
|
|
29
|
+
}
|
|
30
|
+
return { jsx: jsx2, jsxs: jsxs2 };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// src/jsx-runtime.web.tsx
|
|
34
|
+
var { jsx, jsxs } = createJsxFunctionsWeb(resolveClassName);
|
|
35
|
+
|
|
36
|
+
export {
|
|
37
|
+
Fragment,
|
|
38
|
+
jsx,
|
|
39
|
+
jsxs
|
|
40
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getTheme
|
|
3
|
+
} from "./chunk-ZYE4VIZE.mjs";
|
|
4
|
+
|
|
5
|
+
// src/darkModeStore.ts
|
|
6
|
+
import { Appearance } from "react-native";
|
|
7
|
+
var listeners = /* @__PURE__ */ new Set();
|
|
8
|
+
function systemPrefersDark() {
|
|
9
|
+
return Appearance.getColorScheme() === "dark";
|
|
10
|
+
}
|
|
11
|
+
function resolveIsDark(m) {
|
|
12
|
+
return m === "system" ? systemPrefersDark() : m === "dark";
|
|
13
|
+
}
|
|
14
|
+
function applyToDom(dark) {
|
|
15
|
+
if (typeof document === "undefined") return;
|
|
16
|
+
const root = document.documentElement;
|
|
17
|
+
const strategy = getTheme().darkMode;
|
|
18
|
+
root.classList.toggle("dark", strategy === "class" && dark);
|
|
19
|
+
root.classList.toggle("light", strategy === "class" && !dark);
|
|
20
|
+
if (strategy === "attribute") {
|
|
21
|
+
root.setAttribute("data-theme", dark ? "dark" : "light");
|
|
22
|
+
} else {
|
|
23
|
+
root.removeAttribute("data-theme");
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
var mode = "system";
|
|
27
|
+
var isDark = resolveIsDark(mode);
|
|
28
|
+
applyToDom(isDark);
|
|
29
|
+
var hasExplicitChoice = false;
|
|
30
|
+
var subscription = Appearance.addChangeListener(({ colorScheme }) => {
|
|
31
|
+
if (mode !== "system") return;
|
|
32
|
+
isDark = colorScheme === "dark";
|
|
33
|
+
applyToDom(isDark);
|
|
34
|
+
notify();
|
|
35
|
+
});
|
|
36
|
+
function notify() {
|
|
37
|
+
for (const listener of listeners) listener();
|
|
38
|
+
}
|
|
39
|
+
function resyncDomWithActiveTheme() {
|
|
40
|
+
applyToDom(isDark);
|
|
41
|
+
}
|
|
42
|
+
function getGlobalDarkMode() {
|
|
43
|
+
return isDark;
|
|
44
|
+
}
|
|
45
|
+
function getGlobalThemeMode() {
|
|
46
|
+
return mode;
|
|
47
|
+
}
|
|
48
|
+
function setGlobalThemeMode(nextMode) {
|
|
49
|
+
mode = nextMode;
|
|
50
|
+
isDark = resolveIsDark(nextMode);
|
|
51
|
+
hasExplicitChoice = true;
|
|
52
|
+
applyToDom(isDark);
|
|
53
|
+
notify();
|
|
54
|
+
}
|
|
55
|
+
function toggleGlobalDarkMode() {
|
|
56
|
+
setGlobalThemeMode(isDark ? "light" : "dark");
|
|
57
|
+
}
|
|
58
|
+
function subscribeGlobalDarkMode(listener) {
|
|
59
|
+
listeners.add(listener);
|
|
60
|
+
return () => listeners.delete(listener);
|
|
61
|
+
}
|
|
62
|
+
function seedDefaultMode(fallback) {
|
|
63
|
+
if (hasExplicitChoice || mode === fallback) return;
|
|
64
|
+
mode = fallback;
|
|
65
|
+
isDark = resolveIsDark(fallback);
|
|
66
|
+
applyToDom(isDark);
|
|
67
|
+
notify();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// src/dynamicTokens.ts
|
|
71
|
+
var tokens = /* @__PURE__ */ new Map();
|
|
72
|
+
var listeners2 = /* @__PURE__ */ new Set();
|
|
73
|
+
var version = 0;
|
|
74
|
+
function applyToDom2(name, value) {
|
|
75
|
+
if (typeof document === "undefined") return;
|
|
76
|
+
document.documentElement.style.setProperty(`--${name}`, value);
|
|
77
|
+
}
|
|
78
|
+
function notify2() {
|
|
79
|
+
version++;
|
|
80
|
+
for (const listener of listeners2) listener();
|
|
81
|
+
}
|
|
82
|
+
function setDynamicToken(name, value) {
|
|
83
|
+
tokens.set(name, value);
|
|
84
|
+
applyToDom2(name, value);
|
|
85
|
+
notify2();
|
|
86
|
+
}
|
|
87
|
+
function getDynamicToken(name) {
|
|
88
|
+
return tokens.get(name);
|
|
89
|
+
}
|
|
90
|
+
function deleteDynamicToken(name) {
|
|
91
|
+
if (!tokens.delete(name)) return;
|
|
92
|
+
if (typeof document !== "undefined") document.documentElement.style.removeProperty(`--${name}`);
|
|
93
|
+
notify2();
|
|
94
|
+
}
|
|
95
|
+
function subscribeDynamicTokens(listener) {
|
|
96
|
+
listeners2.add(listener);
|
|
97
|
+
return () => listeners2.delete(listener);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export {
|
|
101
|
+
resyncDomWithActiveTheme,
|
|
102
|
+
getGlobalDarkMode,
|
|
103
|
+
getGlobalThemeMode,
|
|
104
|
+
setGlobalThemeMode,
|
|
105
|
+
toggleGlobalDarkMode,
|
|
106
|
+
subscribeGlobalDarkMode,
|
|
107
|
+
seedDefaultMode,
|
|
108
|
+
setDynamicToken,
|
|
109
|
+
getDynamicToken,
|
|
110
|
+
deleteDynamicToken,
|
|
111
|
+
subscribeDynamicTokens
|
|
112
|
+
};
|