@kbach/react-native 1.0.0-beta.7 → 1.0.0-beta.9

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.
@@ -1,12 +1,10 @@
1
1
  import {
2
+ Fragment,
2
3
  jsx,
3
4
  jsxs
4
- } from "./chunk-5WK55Z56.mjs";
5
- import "./chunk-T3HPTH62.mjs";
6
- import {
7
- Fragment
8
- } from "./chunk-DLIXI5OW.mjs";
9
- import "./chunk-AEVOM2NB.mjs";
5
+ } from "./chunk-YOC3SHF5.mjs";
6
+ import "./chunk-H5Z24XX5.mjs";
7
+ import "./chunk-HK6PDJJK.mjs";
10
8
  export {
11
9
  Fragment,
12
10
  jsx,
@@ -0,0 +1,230 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+
4
+ /**
5
+ * A plain hex value (static — same in both modes), or a `{ light, dark }`
6
+ * pair a caller sets manually in their own theme. Fully opt-in per color:
7
+ * nothing here picks a "dark variant" automatically. Only `useColors()`
8
+ * (a JS-value read, not a `className`) understands this shape — utility
9
+ * CLASS resolution (`bg-blue-6`) on native/Expo Go stays plain-string-only,
10
+ * same as it always was (dark mode for a CLASS is the separate `dark:`
11
+ * modifier, gated live at resolve time — see `resolve_style.rs`); a
12
+ * mode-aware entry referenced directly by a class name just doesn't
13
+ * resolve there, mirroring `resolvers/color.rs`'s own `lookup_hex`
14
+ * (`ColorValue::Plain` only, `ModeAware` -> `None`) exactly, same as it
15
+ * would on the web engine too.
16
+ */
17
+ type ColorEntry = string | {
18
+ light: string;
19
+ dark: string;
20
+ };
21
+ /**
22
+ * Real Tailwind's `theme.container` config, mirroring
23
+ * `packages/core-engine/src/theme.rs`'s `ContainerConfig` exactly. `center`/
24
+ * `padding` DO reach native now (unlike the max-width breakpoint ladder,
25
+ * which is a `css.rs`-only concept with nothing to generate on native at
26
+ * all) — `resolvers::layout::resolve`'s `"container"` arm is shared by both
27
+ * dispatchers, and `margin`/`padding` are ordinary RN style properties.
28
+ */
29
+ interface ContainerConfig {
30
+ center?: boolean;
31
+ padding?: string;
32
+ }
33
+ interface ThemeConfig {
34
+ colors: Record<string, ColorEntry>;
35
+ spacing: Record<string, number>;
36
+ screens: Record<string, number>;
37
+ /**
38
+ * e.g. "sans" -> "Inter, sans-serif". Optional, matching the Rust
39
+ * engine's own `#[serde(default)]`. Write it as a real web-shaped
40
+ * fallback stack even on this native-first package — native's own
41
+ * resolver strips it down to a single bare name at resolve time (RN's
42
+ * `fontFamily` prop can't take a fallback list — see
43
+ * `resolvers/mod.rs`'s `first_font_name`), so the SAME config still
44
+ * works unmodified on Expo Web, which needs the full stack.
45
+ */
46
+ fontFamily?: Record<string, string>;
47
+ darkMode: 'attribute' | 'class' | 'media';
48
+ /** Optional, matching `ContainerConfig`'s own `#[serde(default)]` on the Rust side. */
49
+ container?: ContainerConfig;
50
+ }
51
+ declare const defaultTheme: ThemeConfig;
52
+ declare function setTheme(theme: ThemeConfig): void;
53
+ declare function getTheme(): ThemeConfig;
54
+
55
+ /**
56
+ * A `kbach.config.js`-style theme customization — same shape as
57
+ * @kbach/react's `KbachConfig`, minus mode-aware `{ light, dark }` color
58
+ * entries in `extend.colors` specifically: aliasing/opacity math here only
59
+ * ever operates on plain hex (`brand: 'blue-6'`, `brandSoft: 'brand/30'`),
60
+ * matching `jsEngine/resolvers/color.ts`'s `lookupHex`'s own Plain-only
61
+ * convention — a reference to an EXISTING color that happens to be
62
+ * mode-aware (`theme.colors` can hold those now, for `useColors()`; see
63
+ * `theme.ts`'s own `ColorEntry` doc comment) just doesn't resolve as an
64
+ * alias, falling through to "literal CSS value" the same way an unknown
65
+ * name would. A config object goes in, a resolved `ThemeConfig` comes out,
66
+ * via one PURE function (`resolveKbachConfig`) — no plugins, no
67
+ * live-update store, nothing to keep "in sync."
68
+ */
69
+ interface KbachConfig {
70
+ darkMode?: ThemeConfig['darkMode'];
71
+ /**
72
+ * REPLACES the named section of `defaultTheme` entirely — use `extend` to
73
+ * add instead of replace. `container` isn't here (only under `extend`
74
+ * below) — see @kbach/react's identical `KbachConfig.theme` doc comment
75
+ * for why.
76
+ */
77
+ theme?: Partial<Pick<ThemeConfig, 'colors' | 'spacing' | 'screens' | 'fontFamily'>>;
78
+ /** Merges INTO `defaultTheme` (or `theme` above, if also supplied) — the common case. */
79
+ extend?: {
80
+ /**
81
+ * A value is either a literal hex color, or a REFERENCE to another
82
+ * color name, optionally with an opacity suffix. Two things can be
83
+ * referenced: a BUILT-IN palette color, always available regardless of
84
+ * ordering (`accent: 'orange-5'`, `accentSoft: 'orange-5/50'`), or a
85
+ * custom color defined EARLIER in this same object (`{ brand:
86
+ * '#ff6b35', brandSoft: 'brand/30' }` — `brandSoft` resolves to
87
+ * `brand`'s own value at 30% opacity). A string that doesn't match any
88
+ * known color name is treated as a literal CSS value instead.
89
+ */
90
+ colors?: Record<string, string>;
91
+ spacing?: Record<string, number>;
92
+ screens?: Record<string, number>;
93
+ /**
94
+ * Named font stacks, e.g. `{ display: '"Cal Sans", sans-serif' }` —
95
+ * adds to (or overrides one of) `defaultTheme.fontFamily`'s three
96
+ * names. Write it as a real web-shaped fallback stack even though this
97
+ * is the React Native package — native's own resolver strips it down
98
+ * to a single bare name at resolve time (RN's `fontFamily` prop can't
99
+ * take a fallback list), so the SAME config still works unmodified on
100
+ * Expo Web, which needs the full stack.
101
+ */
102
+ fontFamily?: Record<string, string>;
103
+ /** `{ center: true, padding: '2rem' }` — see `ContainerConfig`'s own doc comment. */
104
+ container?: ContainerConfig;
105
+ };
106
+ }
107
+ /** Merges a `KbachConfig` into `defaultTheme` — a pure function, no side effects. */
108
+ declare function resolveKbachConfig(config: KbachConfig): ThemeConfig;
109
+ /** `resolveKbachConfig()` + `setTheme()` in one call — the usual entry point from app code. */
110
+ declare function applyKbachConfig(config: KbachConfig): ThemeConfig;
111
+
112
+ /**
113
+ * Native port of @kbach/react's darkModeStore.ts — same architecture (a
114
+ * single module-level store is the real source of truth;
115
+ * ThemeProvider/useTheme are a thin, OPTIONAL wrapper around it; every
116
+ * function here works standalone with no provider mounted anywhere), with
117
+ * two real platform differences from the web version:
118
+ *
119
+ * 1. No persistence. There's no React-Native-builtin equivalent of
120
+ * localStorage — adding one would mean forcing a real native dependency
121
+ * (e.g. @react-native-async-storage/async-storage) on every consumer,
122
+ * even ones that don't want it. The mode always starts at 'system' on a
123
+ * fresh app launch. If you want persistence, read your own storage at
124
+ * startup (before your first render) and call setGlobalThemeMode() with
125
+ * the result — same "seed once, no built-in write-back" shape as
126
+ * seedDefaultMode() below, just driven by your own storage instead of
127
+ * one this package picks for you.
128
+ * 2. No DOM to "apply" the resolved mode to. On web, applyToDom() flips a
129
+ * class/attribute the generated CSS's `dark:` selectors match against.
130
+ * On native there's no CSS at all — nativeBridge.ts's resolveStyle()
131
+ * reads getGlobalDarkMode() directly on every call instead of querying
132
+ * Appearance.getColorScheme() itself, which is what actually makes an
133
+ * explicit override affect rendering (previously that function always
134
+ * read the raw OS setting, so it had no way to reflect a user choice —
135
+ * see nativeBridge.ts's own comment on this for why that's the one
136
+ * genuinely fixed bug this change introduces, not just a new feature).
137
+ */
138
+ type ThemeMode = 'light' | 'dark' | 'system';
139
+ declare function getGlobalDarkMode(): boolean;
140
+ declare function getGlobalThemeMode(): ThemeMode;
141
+ /** Explicit user choice — takes priority over any later `<ThemeProvider defaultMode>` seed (see `seedDefaultMode` below). Not persisted; see this module's own doc comment. */
142
+ declare function setGlobalThemeMode(nextMode: ThemeMode): void;
143
+ declare function toggleGlobalDarkMode(): void;
144
+ declare function subscribeGlobalDarkMode(listener: () => void): () => void;
145
+
146
+ interface ThemeState {
147
+ mode: ThemeMode;
148
+ isDark: boolean;
149
+ setMode: (mode: ThemeMode) => void;
150
+ toggle: () => void;
151
+ }
152
+ /**
153
+ * The one hook for reading and writing Kbach's dark-mode state. Reads
154
+ * `darkModeStore.ts`'s global store directly via `useSyncExternalStore` —
155
+ * that module-level store, not React Context, is the actual source of
156
+ * truth (every provider used to just mirror it into a Context value), so
157
+ * this works from ANY component, with or without a `<ThemeProvider>`
158
+ * mounted anywhere in the tree. `<ThemeProvider>` still exists, but purely
159
+ * as an optional convenience for seeding an app's initial `defaultMode` —
160
+ * see its own doc comment. No server-snapshot argument (unlike
161
+ * @kbach/react's web version) — React Native has no SSR concept at all,
162
+ * so there's no third argument for `useSyncExternalStore` to need.
163
+ */
164
+ declare function useTheme(): ThemeState;
165
+
166
+ interface ThemeProviderProps {
167
+ children: ReactNode;
168
+ /**
169
+ * Applied once, on this provider's first mount, but ONLY if nothing has
170
+ * made an explicit mode choice yet (see
171
+ * `darkModeStore.ts::seedDefaultMode`) — an explicit prior choice always
172
+ * wins over a component's own default. Unlike @kbach/react's web
173
+ * version, there's no persisted value to check here — see
174
+ * darkModeStore.ts's own doc comment on why this package doesn't persist
175
+ * the mode across app launches.
176
+ */
177
+ defaultMode?: ThemeMode;
178
+ }
179
+ /**
180
+ * Optional — `useTheme()` reads `darkModeStore.ts`'s global store directly
181
+ * and works anywhere without this mounted at all. The only thing this
182
+ * component does is seed that store's initial `defaultMode` once, for apps
183
+ * that want to declare a startup default in JSX rather than calling
184
+ * `setGlobalThemeMode()` imperatively before their first render. It
185
+ * renders `children` as-is — no Context.Provider, since there's no value
186
+ * left to distribute; `useTheme()` never reads from Context.
187
+ *
188
+ * Because the store is global, MULTIPLE mounted providers would each try
189
+ * to seed the exact same store rather than getting their own scoped
190
+ * state — mounting more than one is very likely a mistake, so this warns
191
+ * once in dev.
192
+ */
193
+ declare function ThemeProvider({ children, defaultMode }: ThemeProviderProps): react.JSX.Element;
194
+
195
+ interface ColorScale {
196
+ /** `colors.blue[6]` -> raw value */
197
+ readonly [shade: number]: string;
198
+ /** `colors.blue['6/50']` -> shade 6 at 50% opacity */
199
+ readonly [key: string]: string;
200
+ }
201
+ type ColorsAPI = {
202
+ /** `colors.white`, `colors.blue` (a shade family), ... */
203
+ readonly [name: string]: string | ColorScale;
204
+ } & {
205
+ /** Applies an opacity (0–100) to any color string this hook can return. */
206
+ readonly alpha: (color: string, opacity?: number) => string;
207
+ };
208
+ /**
209
+ * Resolved theme colors as real values — for anywhere you need an actual
210
+ * color (a chart library prop, a raw SVG `fill`), not a `className`. Ported
211
+ * from old-kbach's `useColors()`, adapted to this engine's flat
212
+ * `"blue-6"`-keyed palette (`colors.blue[6]` looks up `"blue-6"`).
213
+ *
214
+ * Any color — a shade-family entry like `"blue-6"` or a standalone name —
215
+ * can be defined in the theme as either a plain string (static, same in
216
+ * both modes) or a `{ light, dark }` pair — fully manual, no automatic
217
+ * shade-shifting or other assumption about what a "dark variant" should
218
+ * be; see `resolveEntry`'s own doc comment for how a `{ light, dark }`
219
+ * entry resolves on this platform specifically.
220
+ *
221
+ * ```ts
222
+ * const colors = useColors();
223
+ * colors.blue[6]; // "#2563eb" — static, same value in both modes
224
+ * colors.blue['6/50']; // same, at 50% opacity
225
+ * colors.brand; // resolves per isDark if defined as { light, dark }
226
+ * ```
227
+ */
228
+ declare function useColors(): ColorsAPI;
229
+
230
+ export { type ColorEntry as C, type KbachConfig as K, type ThemeConfig as T, type ColorScale as a, type ColorsAPI as b, type ContainerConfig as c, type ThemeMode as d, ThemeProvider as e, type ThemeProviderProps as f, type ThemeState as g, applyKbachConfig as h, defaultTheme as i, getGlobalDarkMode as j, getGlobalThemeMode as k, getTheme as l, setTheme as m, subscribeGlobalDarkMode as n, useTheme as o, resolveKbachConfig as r, setGlobalThemeMode as s, toggleGlobalDarkMode as t, useColors as u };
@@ -0,0 +1,230 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+
4
+ /**
5
+ * A plain hex value (static — same in both modes), or a `{ light, dark }`
6
+ * pair a caller sets manually in their own theme. Fully opt-in per color:
7
+ * nothing here picks a "dark variant" automatically. Only `useColors()`
8
+ * (a JS-value read, not a `className`) understands this shape — utility
9
+ * CLASS resolution (`bg-blue-6`) on native/Expo Go stays plain-string-only,
10
+ * same as it always was (dark mode for a CLASS is the separate `dark:`
11
+ * modifier, gated live at resolve time — see `resolve_style.rs`); a
12
+ * mode-aware entry referenced directly by a class name just doesn't
13
+ * resolve there, mirroring `resolvers/color.rs`'s own `lookup_hex`
14
+ * (`ColorValue::Plain` only, `ModeAware` -> `None`) exactly, same as it
15
+ * would on the web engine too.
16
+ */
17
+ type ColorEntry = string | {
18
+ light: string;
19
+ dark: string;
20
+ };
21
+ /**
22
+ * Real Tailwind's `theme.container` config, mirroring
23
+ * `packages/core-engine/src/theme.rs`'s `ContainerConfig` exactly. `center`/
24
+ * `padding` DO reach native now (unlike the max-width breakpoint ladder,
25
+ * which is a `css.rs`-only concept with nothing to generate on native at
26
+ * all) — `resolvers::layout::resolve`'s `"container"` arm is shared by both
27
+ * dispatchers, and `margin`/`padding` are ordinary RN style properties.
28
+ */
29
+ interface ContainerConfig {
30
+ center?: boolean;
31
+ padding?: string;
32
+ }
33
+ interface ThemeConfig {
34
+ colors: Record<string, ColorEntry>;
35
+ spacing: Record<string, number>;
36
+ screens: Record<string, number>;
37
+ /**
38
+ * e.g. "sans" -> "Inter, sans-serif". Optional, matching the Rust
39
+ * engine's own `#[serde(default)]`. Write it as a real web-shaped
40
+ * fallback stack even on this native-first package — native's own
41
+ * resolver strips it down to a single bare name at resolve time (RN's
42
+ * `fontFamily` prop can't take a fallback list — see
43
+ * `resolvers/mod.rs`'s `first_font_name`), so the SAME config still
44
+ * works unmodified on Expo Web, which needs the full stack.
45
+ */
46
+ fontFamily?: Record<string, string>;
47
+ darkMode: 'attribute' | 'class' | 'media';
48
+ /** Optional, matching `ContainerConfig`'s own `#[serde(default)]` on the Rust side. */
49
+ container?: ContainerConfig;
50
+ }
51
+ declare const defaultTheme: ThemeConfig;
52
+ declare function setTheme(theme: ThemeConfig): void;
53
+ declare function getTheme(): ThemeConfig;
54
+
55
+ /**
56
+ * A `kbach.config.js`-style theme customization — same shape as
57
+ * @kbach/react's `KbachConfig`, minus mode-aware `{ light, dark }` color
58
+ * entries in `extend.colors` specifically: aliasing/opacity math here only
59
+ * ever operates on plain hex (`brand: 'blue-6'`, `brandSoft: 'brand/30'`),
60
+ * matching `jsEngine/resolvers/color.ts`'s `lookupHex`'s own Plain-only
61
+ * convention — a reference to an EXISTING color that happens to be
62
+ * mode-aware (`theme.colors` can hold those now, for `useColors()`; see
63
+ * `theme.ts`'s own `ColorEntry` doc comment) just doesn't resolve as an
64
+ * alias, falling through to "literal CSS value" the same way an unknown
65
+ * name would. A config object goes in, a resolved `ThemeConfig` comes out,
66
+ * via one PURE function (`resolveKbachConfig`) — no plugins, no
67
+ * live-update store, nothing to keep "in sync."
68
+ */
69
+ interface KbachConfig {
70
+ darkMode?: ThemeConfig['darkMode'];
71
+ /**
72
+ * REPLACES the named section of `defaultTheme` entirely — use `extend` to
73
+ * add instead of replace. `container` isn't here (only under `extend`
74
+ * below) — see @kbach/react's identical `KbachConfig.theme` doc comment
75
+ * for why.
76
+ */
77
+ theme?: Partial<Pick<ThemeConfig, 'colors' | 'spacing' | 'screens' | 'fontFamily'>>;
78
+ /** Merges INTO `defaultTheme` (or `theme` above, if also supplied) — the common case. */
79
+ extend?: {
80
+ /**
81
+ * A value is either a literal hex color, or a REFERENCE to another
82
+ * color name, optionally with an opacity suffix. Two things can be
83
+ * referenced: a BUILT-IN palette color, always available regardless of
84
+ * ordering (`accent: 'orange-5'`, `accentSoft: 'orange-5/50'`), or a
85
+ * custom color defined EARLIER in this same object (`{ brand:
86
+ * '#ff6b35', brandSoft: 'brand/30' }` — `brandSoft` resolves to
87
+ * `brand`'s own value at 30% opacity). A string that doesn't match any
88
+ * known color name is treated as a literal CSS value instead.
89
+ */
90
+ colors?: Record<string, string>;
91
+ spacing?: Record<string, number>;
92
+ screens?: Record<string, number>;
93
+ /**
94
+ * Named font stacks, e.g. `{ display: '"Cal Sans", sans-serif' }` —
95
+ * adds to (or overrides one of) `defaultTheme.fontFamily`'s three
96
+ * names. Write it as a real web-shaped fallback stack even though this
97
+ * is the React Native package — native's own resolver strips it down
98
+ * to a single bare name at resolve time (RN's `fontFamily` prop can't
99
+ * take a fallback list), so the SAME config still works unmodified on
100
+ * Expo Web, which needs the full stack.
101
+ */
102
+ fontFamily?: Record<string, string>;
103
+ /** `{ center: true, padding: '2rem' }` — see `ContainerConfig`'s own doc comment. */
104
+ container?: ContainerConfig;
105
+ };
106
+ }
107
+ /** Merges a `KbachConfig` into `defaultTheme` — a pure function, no side effects. */
108
+ declare function resolveKbachConfig(config: KbachConfig): ThemeConfig;
109
+ /** `resolveKbachConfig()` + `setTheme()` in one call — the usual entry point from app code. */
110
+ declare function applyKbachConfig(config: KbachConfig): ThemeConfig;
111
+
112
+ /**
113
+ * Native port of @kbach/react's darkModeStore.ts — same architecture (a
114
+ * single module-level store is the real source of truth;
115
+ * ThemeProvider/useTheme are a thin, OPTIONAL wrapper around it; every
116
+ * function here works standalone with no provider mounted anywhere), with
117
+ * two real platform differences from the web version:
118
+ *
119
+ * 1. No persistence. There's no React-Native-builtin equivalent of
120
+ * localStorage — adding one would mean forcing a real native dependency
121
+ * (e.g. @react-native-async-storage/async-storage) on every consumer,
122
+ * even ones that don't want it. The mode always starts at 'system' on a
123
+ * fresh app launch. If you want persistence, read your own storage at
124
+ * startup (before your first render) and call setGlobalThemeMode() with
125
+ * the result — same "seed once, no built-in write-back" shape as
126
+ * seedDefaultMode() below, just driven by your own storage instead of
127
+ * one this package picks for you.
128
+ * 2. No DOM to "apply" the resolved mode to. On web, applyToDom() flips a
129
+ * class/attribute the generated CSS's `dark:` selectors match against.
130
+ * On native there's no CSS at all — nativeBridge.ts's resolveStyle()
131
+ * reads getGlobalDarkMode() directly on every call instead of querying
132
+ * Appearance.getColorScheme() itself, which is what actually makes an
133
+ * explicit override affect rendering (previously that function always
134
+ * read the raw OS setting, so it had no way to reflect a user choice —
135
+ * see nativeBridge.ts's own comment on this for why that's the one
136
+ * genuinely fixed bug this change introduces, not just a new feature).
137
+ */
138
+ type ThemeMode = 'light' | 'dark' | 'system';
139
+ declare function getGlobalDarkMode(): boolean;
140
+ declare function getGlobalThemeMode(): ThemeMode;
141
+ /** Explicit user choice — takes priority over any later `<ThemeProvider defaultMode>` seed (see `seedDefaultMode` below). Not persisted; see this module's own doc comment. */
142
+ declare function setGlobalThemeMode(nextMode: ThemeMode): void;
143
+ declare function toggleGlobalDarkMode(): void;
144
+ declare function subscribeGlobalDarkMode(listener: () => void): () => void;
145
+
146
+ interface ThemeState {
147
+ mode: ThemeMode;
148
+ isDark: boolean;
149
+ setMode: (mode: ThemeMode) => void;
150
+ toggle: () => void;
151
+ }
152
+ /**
153
+ * The one hook for reading and writing Kbach's dark-mode state. Reads
154
+ * `darkModeStore.ts`'s global store directly via `useSyncExternalStore` —
155
+ * that module-level store, not React Context, is the actual source of
156
+ * truth (every provider used to just mirror it into a Context value), so
157
+ * this works from ANY component, with or without a `<ThemeProvider>`
158
+ * mounted anywhere in the tree. `<ThemeProvider>` still exists, but purely
159
+ * as an optional convenience for seeding an app's initial `defaultMode` —
160
+ * see its own doc comment. No server-snapshot argument (unlike
161
+ * @kbach/react's web version) — React Native has no SSR concept at all,
162
+ * so there's no third argument for `useSyncExternalStore` to need.
163
+ */
164
+ declare function useTheme(): ThemeState;
165
+
166
+ interface ThemeProviderProps {
167
+ children: ReactNode;
168
+ /**
169
+ * Applied once, on this provider's first mount, but ONLY if nothing has
170
+ * made an explicit mode choice yet (see
171
+ * `darkModeStore.ts::seedDefaultMode`) — an explicit prior choice always
172
+ * wins over a component's own default. Unlike @kbach/react's web
173
+ * version, there's no persisted value to check here — see
174
+ * darkModeStore.ts's own doc comment on why this package doesn't persist
175
+ * the mode across app launches.
176
+ */
177
+ defaultMode?: ThemeMode;
178
+ }
179
+ /**
180
+ * Optional — `useTheme()` reads `darkModeStore.ts`'s global store directly
181
+ * and works anywhere without this mounted at all. The only thing this
182
+ * component does is seed that store's initial `defaultMode` once, for apps
183
+ * that want to declare a startup default in JSX rather than calling
184
+ * `setGlobalThemeMode()` imperatively before their first render. It
185
+ * renders `children` as-is — no Context.Provider, since there's no value
186
+ * left to distribute; `useTheme()` never reads from Context.
187
+ *
188
+ * Because the store is global, MULTIPLE mounted providers would each try
189
+ * to seed the exact same store rather than getting their own scoped
190
+ * state — mounting more than one is very likely a mistake, so this warns
191
+ * once in dev.
192
+ */
193
+ declare function ThemeProvider({ children, defaultMode }: ThemeProviderProps): react.JSX.Element;
194
+
195
+ interface ColorScale {
196
+ /** `colors.blue[6]` -> raw value */
197
+ readonly [shade: number]: string;
198
+ /** `colors.blue['6/50']` -> shade 6 at 50% opacity */
199
+ readonly [key: string]: string;
200
+ }
201
+ type ColorsAPI = {
202
+ /** `colors.white`, `colors.blue` (a shade family), ... */
203
+ readonly [name: string]: string | ColorScale;
204
+ } & {
205
+ /** Applies an opacity (0–100) to any color string this hook can return. */
206
+ readonly alpha: (color: string, opacity?: number) => string;
207
+ };
208
+ /**
209
+ * Resolved theme colors as real values — for anywhere you need an actual
210
+ * color (a chart library prop, a raw SVG `fill`), not a `className`. Ported
211
+ * from old-kbach's `useColors()`, adapted to this engine's flat
212
+ * `"blue-6"`-keyed palette (`colors.blue[6]` looks up `"blue-6"`).
213
+ *
214
+ * Any color — a shade-family entry like `"blue-6"` or a standalone name —
215
+ * can be defined in the theme as either a plain string (static, same in
216
+ * both modes) or a `{ light, dark }` pair — fully manual, no automatic
217
+ * shade-shifting or other assumption about what a "dark variant" should
218
+ * be; see `resolveEntry`'s own doc comment for how a `{ light, dark }`
219
+ * entry resolves on this platform specifically.
220
+ *
221
+ * ```ts
222
+ * const colors = useColors();
223
+ * colors.blue[6]; // "#2563eb" — static, same value in both modes
224
+ * colors.blue['6/50']; // same, at 50% opacity
225
+ * colors.brand; // resolves per isDark if defined as { light, dark }
226
+ * ```
227
+ */
228
+ declare function useColors(): ColorsAPI;
229
+
230
+ export { type ColorEntry as C, type KbachConfig as K, type ThemeConfig as T, type ColorScale as a, type ColorsAPI as b, type ContainerConfig as c, type ThemeMode as d, ThemeProvider as e, type ThemeProviderProps as f, type ThemeState as g, applyKbachConfig as h, defaultTheme as i, getGlobalDarkMode as j, getGlobalThemeMode as k, getTheme as l, setTheme as m, subscribeGlobalDarkMode as n, useTheme as o, resolveKbachConfig as r, setGlobalThemeMode as s, toggleGlobalDarkMode as t, useColors as u };
package/package.json CHANGED
@@ -1,8 +1,18 @@
1
1
  {
2
2
  "name": "@kbach/react-native",
3
- "version": "1.0.0-beta.7",
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) or the same WASM engine @kbach/react uses (Expo Web / react-native-web). 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 or an Expo dev client (not Expo Go, which cannot load any custom native module).",
5
- "keywords": ["css", "react-native", "expo", "tailwind", "utility-first", "styling", "android", "web", "wasm"],
3
+ "version": "1.0.0-beta.9",
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
+ "keywords": [
6
+ "css",
7
+ "react-native",
8
+ "expo",
9
+ "tailwind",
10
+ "utility-first",
11
+ "styling",
12
+ "android",
13
+ "web",
14
+ "wasm"
15
+ ],
6
16
  "license": "MIT",
7
17
  "author": "semsakadanupol",
8
18
  "repository": {
@@ -17,7 +27,14 @@
17
27
  "publishConfig": {
18
28
  "access": "public"
19
29
  },
20
- "files": ["dist", "src/babel-plugin.js", "android/build.gradle", "android/src", "specs", "react-native.config.js"],
30
+ "files": [
31
+ "dist",
32
+ "src/babel-plugin.js",
33
+ "android/build.gradle",
34
+ "android/src",
35
+ "specs",
36
+ "react-native.config.js"
37
+ ],
21
38
  "source": "./src/index.ts",
22
39
  "main": "./dist/index.js",
23
40
  "module": "./dist/index.mjs",
@@ -79,6 +96,7 @@
79
96
  "devDependencies": {
80
97
  "@types/react": "^19.2.0",
81
98
  "@types/react-test-renderer": "^19.1.0",
99
+ "jsdom": "^30.0.1",
82
100
  "react-native": "^0.87.0",
83
101
  "react-test-renderer": "^19.2.0",
84
102
  "tsup": "^8.0.0",
@@ -1,5 +1,9 @@
1
1
  'use strict';
2
2
 
3
+ const path = require('path');
4
+ const fs = require('fs');
5
+ const template = require('@babel/template').default;
6
+
3
7
  /**
4
8
  * Injects a per-file `@jsxImportSource @kbach/react-native` pragma comment
5
9
  * so Metro's JSX transform routes className resolution through
@@ -19,9 +23,67 @@
19
23
  * Deliberately NOT porting old-kbach's build-time class-resolution half
20
24
  * (the __kbachStyles fast path) — resolution happens at the (already
21
25
  * synchronous) native-bridge call inside the jsx-runtime instead (see
22
- * nativeBridge.ts), so this plugin's only job is the pragma injection.
26
+ * nativeBridge.ts), so this plugin's only job is the pragma injection —
27
+ * PLUS one more, added later: auto-applying `kbach.config.js` if one
28
+ * exists (see `getConfigStatement`'s own doc comment), so a project never
29
+ * needs its own explicit `applyKbachConfig(kbachConfig)` call at all. That
30
+ * addition deliberately still stops short of old-kbach's own version of
31
+ * this (an mtime-polling live-reload of the config, re-checked on every
32
+ * file) — a Babel-plugin-level change needs a dev-server restart to take
33
+ * effect either way, so a one-time check per (root, process) pair is
34
+ * enough, and much simpler to reason about.
35
+ */
36
+
37
+ // Keyed by the root directory checked — undefined = not checked yet for
38
+ // that root, null = checked, no kbach.config.js found there. A Babel/Metro
39
+ // worker process transforms many files over its lifetime — this makes the
40
+ // existsSync + template-build work happen ONCE per root, not on every
41
+ // single file `pre()` runs for.
42
+ const _configStatementCache = new Map();
43
+
44
+ /**
45
+ * Builds the (uncompiled) AST statement that applies `kbach.config.js`,
46
+ * cached per `root` after the first call — `null` if no such file exists
47
+ * there. `root` defaults to `process.cwd()` (how `metro start`/`expo
48
+ * start` are conventionally invoked) at the plugin's own call site below;
49
+ * pass `{ configRoot: '...' }` as this plugin's Babel options if your
50
+ * `kbach.config.js` lives somewhere else (a monorepo where Metro's actual
51
+ * working directory isn't the app's own root, for instance).
52
+ *
53
+ * The injected statement is a plain `require(...).applyKbachConfig(require(...))`
54
+ * — no try/catch around it. `kbach.config.js`'s own path is only ever
55
+ * baked in here after `existsSync` already confirmed it exists, so the
56
+ * only way this could still throw is a genuine bug in that file (a syntax
57
+ * error, a bad nested require) — letting that surface as a real, visible
58
+ * Metro bundling error is far better than silently swallowing it: a
59
+ * developer's real customization would otherwise go missing with zero
60
+ * indication why, exactly the kind of headache a good default should avoid.
61
+ *
62
+ * `@babel/template` builds this from a plain string with the absolute path
63
+ * JSON-escaped directly into it (correct on Windows too, backslashes and
64
+ * all) — simpler than building the equivalent `@babel/types` node graph by
65
+ * hand for one straightforward statement.
23
66
  */
24
- module.exports = function kbachReactNativeBabelPlugin() {
67
+ function getConfigStatement(root) {
68
+ if (_configStatementCache.has(root)) return _configStatementCache.get(root);
69
+
70
+ const configPath = path.join(root, 'kbach.config.js');
71
+ if (!fs.existsSync(configPath)) {
72
+ _configStatementCache.set(root, null);
73
+ return null;
74
+ }
75
+
76
+ const buildStatement = template(
77
+ `require('@kbach/react-native').applyKbachConfig(require(${JSON.stringify(configPath)}));`,
78
+ );
79
+ const statement = buildStatement();
80
+ _configStatementCache.set(root, statement);
81
+ return statement;
82
+ }
83
+
84
+ module.exports = function kbachReactNativeBabelPlugin(_babel, options) {
85
+ const configRoot = (options && options.configRoot) || process.cwd();
86
+
25
87
  return {
26
88
  name: 'kbach-react-native',
27
89
  pre(file) {
@@ -33,7 +95,17 @@ module.exports = function kbachReactNativeBabelPlugin() {
33
95
  if (!alreadySet) {
34
96
  comments.unshift({ type: 'CommentLine', value: ' @jsxImportSource @kbach/react-native' });
35
97
  }
98
+
99
+ const configStatement = getConfigStatement(configRoot);
100
+ if (configStatement) {
101
+ file.ast.program.body.unshift(configStatement);
102
+ }
36
103
  },
37
104
  visitor: {},
38
105
  };
39
106
  };
107
+
108
+ /** Exported for tests only — clears the per-root config-file cache between test cases. */
109
+ module.exports._resetForTests = function resetForTests() {
110
+ _configStatementCache.clear();
111
+ };