@oxyhq/bloom 0.7.0 → 0.7.1

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.
Files changed (64) hide show
  1. package/lib/commonjs/react-native-css.d.js +2 -0
  2. package/lib/commonjs/react-native-css.d.js.map +1 -0
  3. package/lib/commonjs/theme/BloomThemeProvider.js +39 -7
  4. package/lib/commonjs/theme/BloomThemeProvider.js.map +1 -1
  5. package/lib/commonjs/theme/color-presets.js +118 -118
  6. package/lib/commonjs/theme/color-presets.js.map +1 -1
  7. package/lib/commonjs/theme/color-scope/index.js +28 -6
  8. package/lib/commonjs/theme/color-scope/index.js.map +1 -1
  9. package/lib/commonjs/theme/color-scope/style-builder.js +35 -0
  10. package/lib/commonjs/theme/color-scope/style-builder.js.map +1 -1
  11. package/lib/commonjs/theme/native-root-vars.js +105 -0
  12. package/lib/commonjs/theme/native-root-vars.js.map +1 -0
  13. package/lib/commonjs/theme/native-root-vars.web.js +27 -0
  14. package/lib/commonjs/theme/native-root-vars.web.js.map +1 -0
  15. package/lib/commonjs/theme/preset-vars.js +4 -1
  16. package/lib/commonjs/theme/preset-vars.js.map +1 -1
  17. package/lib/module/react-native-css.d.js +2 -0
  18. package/lib/module/react-native-css.d.js.map +1 -0
  19. package/lib/module/theme/BloomThemeProvider.js +40 -8
  20. package/lib/module/theme/BloomThemeProvider.js.map +1 -1
  21. package/lib/module/theme/color-presets.js +118 -118
  22. package/lib/module/theme/color-presets.js.map +1 -1
  23. package/lib/module/theme/color-scope/index.js +29 -7
  24. package/lib/module/theme/color-scope/index.js.map +1 -1
  25. package/lib/module/theme/color-scope/style-builder.js +35 -0
  26. package/lib/module/theme/color-scope/style-builder.js.map +1 -1
  27. package/lib/module/theme/native-root-vars.js +101 -0
  28. package/lib/module/theme/native-root-vars.js.map +1 -0
  29. package/lib/module/theme/native-root-vars.web.js +23 -0
  30. package/lib/module/theme/native-root-vars.web.js.map +1 -0
  31. package/lib/module/theme/preset-vars.js +4 -1
  32. package/lib/module/theme/preset-vars.js.map +1 -1
  33. package/lib/typescript/commonjs/icons/common.d.ts +8 -8
  34. package/lib/typescript/commonjs/theme/BloomThemeProvider.d.ts.map +1 -1
  35. package/lib/typescript/commonjs/theme/color-scope/index.d.ts +9 -2
  36. package/lib/typescript/commonjs/theme/color-scope/index.d.ts.map +1 -1
  37. package/lib/typescript/commonjs/theme/color-scope/style-builder.d.ts +32 -0
  38. package/lib/typescript/commonjs/theme/color-scope/style-builder.d.ts.map +1 -1
  39. package/lib/typescript/commonjs/theme/native-root-vars.d.ts +89 -0
  40. package/lib/typescript/commonjs/theme/native-root-vars.d.ts.map +1 -0
  41. package/lib/typescript/commonjs/theme/native-root-vars.web.d.ts +20 -0
  42. package/lib/typescript/commonjs/theme/native-root-vars.web.d.ts.map +1 -0
  43. package/lib/typescript/commonjs/theme/preset-vars.d.ts.map +1 -1
  44. package/lib/typescript/module/icons/common.d.ts +8 -8
  45. package/lib/typescript/module/theme/BloomThemeProvider.d.ts.map +1 -1
  46. package/lib/typescript/module/theme/color-scope/index.d.ts +9 -2
  47. package/lib/typescript/module/theme/color-scope/index.d.ts.map +1 -1
  48. package/lib/typescript/module/theme/color-scope/style-builder.d.ts +32 -0
  49. package/lib/typescript/module/theme/color-scope/style-builder.d.ts.map +1 -1
  50. package/lib/typescript/module/theme/native-root-vars.d.ts +89 -0
  51. package/lib/typescript/module/theme/native-root-vars.d.ts.map +1 -0
  52. package/lib/typescript/module/theme/native-root-vars.web.d.ts +20 -0
  53. package/lib/typescript/module/theme/native-root-vars.web.d.ts.map +1 -0
  54. package/lib/typescript/module/theme/preset-vars.d.ts.map +1 -1
  55. package/package.json +1 -1
  56. package/src/__tests__/native-root-vars.test.ts +125 -0
  57. package/src/react-native-css.d.ts +69 -0
  58. package/src/theme/BloomThemeProvider.tsx +32 -6
  59. package/src/theme/color-presets.ts +118 -118
  60. package/src/theme/color-scope/index.tsx +30 -8
  61. package/src/theme/color-scope/style-builder.ts +40 -0
  62. package/src/theme/native-root-vars.ts +102 -0
  63. package/src/theme/native-root-vars.web.ts +22 -0
  64. package/src/theme/preset-vars.ts +4 -1
@@ -0,0 +1,89 @@
1
+ import { type AppColorName } from './color-presets';
2
+ /**
3
+ * Publish the active color preset's CSS custom properties into react-native-css's
4
+ * GLOBAL root-variable family so they reach the ENTIRE native app tree — including
5
+ * content rendered outside the React subtree of `BloomThemeProvider`: expo-router
6
+ * navigator screens, `Portal`s, `Dialog`/`BottomSheet` overlays, and any other host
7
+ * that paints through a separate React root.
8
+ *
9
+ * Why a global write is required (native only)
10
+ * --------------------------------------------
11
+ * react-native-css resolves a `var(--x)` reference (its `varResolver`) in this order:
12
+ * 1. `name in inheritedVariables` — the React-context-inherited vars. This is what
13
+ * `VariableContextProvider` populates, but it is React-context-scoped, so it only
14
+ * reaches *direct descendants* of the provider in the React tree.
15
+ * 2. `inlineVariables[name]` — same-component inline vars.
16
+ * 3. `variables[name]` — resolved inherited vars.
17
+ * 4. `universalVariables(name)` — a GLOBAL observable family.
18
+ * 5. `rootVariables(name)` — a GLOBAL observable family.
19
+ * 6. the declared fallback.
20
+ *
21
+ * `BloomThemeProvider`'s `VariableContextProvider` only feeds path #1, so the navigator
22
+ * host and every portal/modal — which render outside that subtree — never see the
23
+ * preset vars and fall through to `rootVariables`/`universalVariables`. By default those
24
+ * families hold only react-native-css's own `__rn-css-rem`/`__rn-css-color` entries
25
+ * (Bloom's `global.css` defines the `hsl(var(--primary))` indirection but not the HSL
26
+ * triples themselves — those are injected at runtime), so the whole tree below the
27
+ * navigator renders monochrome. Writing the triples into `rootVariables` here is the
28
+ * exact same mechanism react-native-css uses for compiled `:root {}` / `@theme` vars
29
+ * (`StyleCollection.inject` does `rootVariables(name).set(valueArray)`), so portaled
30
+ * content resolves the palette identically regardless of React-tree position.
31
+ *
32
+ * Why a STATIC `import`, not a runtime `require` (critical)
33
+ * ---------------------------------------------------------
34
+ * react-native-css@3.0.7 ships both a `dist/commonjs` and a `dist/module` build, and
35
+ * Metro bundles BOTH. In `native-internal/root.js` the variable families are plain
36
+ * module-local consts — NOT `globalThis`-guarded the way `StyleCollection` is — so the
37
+ * commonjs `root.js` and the module `root.js` each instantiate their OWN separate
38
+ * `rootVariables` family. The react-native-css renderer (`react-native-css/src/components/View.tsx`
39
+ * → `varResolver`) imports its `root.js` via ESM `import`, i.e. the MODULE build. A
40
+ * runtime `require('react-native-css/native-internal')` resolves the package's `require`
41
+ * export condition → the COMMONJS build → a DIFFERENT family the renderer never
42
+ * subscribes to, so the writes are a silent no-op on device. A static top-level
43
+ * `import` resolves the `import` condition → the MODULE build → the renderer's instance.
44
+ *
45
+ * This plain file is the NATIVE/default variant (Metro selects it on iOS/Android,
46
+ * which have no `.native` override here); the web variant lives in the sibling
47
+ * `native-root-vars.web.ts` no-op, which web bundlers pick over this one. So the
48
+ * static `react-native-css/native-internal` import below is never pulled into a web
49
+ * bundle — only into native. This mirrors Bloom's existing platform-split convention
50
+ * (`color-scope/index.tsx` + `color-scope/index.web.tsx`, `FontLoader.native.tsx`).
51
+ *
52
+ * Keying contract (verified against react-native-css@3.0.7)
53
+ * ---------------------------------------------------------
54
+ * The `rootVariables` family is keyed by the BARE variable name, WITHOUT the leading
55
+ * `--`. The compiler strips it uniformly: a `:root { --primary: ... }` declaration is
56
+ * stored via `rule.v.push([property.slice(2), value])` then routed into
57
+ * `shared.rootVariables[name]`, and `@property --x` goes through
58
+ * `name.startsWith("--") ? name.slice(2) : name`. The lookup side matches: a `var(--x)`
59
+ * reference compiles to `[{}, "var", ident.slice(2)]`, so `varResolver` looks up the
60
+ * bare name. `buildScopeVars` returns keys WITH the `--` prefix, so we strip it before
61
+ * writing each entry.
62
+ *
63
+ * Value shape
64
+ * -----------
65
+ * Each entry is a `VariableValue[]` — an array of `[value, mediaCondition?]` tuples.
66
+ * Bloom's preset vars are unconditional, so each is a single-element `[[value]]`, e.g.
67
+ * `rootVariables('primary').set([['205 87% 53%']])`. This is the same shape the compiler
68
+ * emits for unconditional `:root` vars.
69
+ *
70
+ * This is native-only and additive: the `VariableContextProvider` wrapper stays (it is
71
+ * the correct, scoped mechanism for `BloomColorScope` subtree overrides and harmlessly
72
+ * covers direct descendants). The web fork (`native-root-vars.web.ts`) no-ops — web
73
+ * writes the same vars to `document.documentElement` via `applyColorPresetVars`.
74
+ */
75
+ /**
76
+ * Write the preset's CSS vars to react-native-css's global `rootVariables` family
77
+ * so the whole native tree (navigator screens + portals/modals/bottom-sheets)
78
+ * resolves the active palette.
79
+ *
80
+ * Each `.set` notifies its own observers synchronously (react-native-css only
81
+ * batches notifications inside its own `StyleCollection.inject`; outside it,
82
+ * `observableBatch.current` is unset so every `.set` runs observers immediately),
83
+ * so sequential writes correctly trigger a re-render when the preset/mode changes.
84
+ *
85
+ * Defensive guard: if a host ships a react-native-css build that doesn't expose
86
+ * `rootVariables` as a callable (it always does on @3), we bail rather than throw.
87
+ */
88
+ export declare function applyNativeRootVars(colorPreset: AppColorName, mode: 'light' | 'dark'): void;
89
+ //# sourceMappingURL=native-root-vars.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"native-root-vars.d.ts","sourceRoot":"","sources":["../../../../src/theme/native-root-vars.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAEpD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwEG;AAEH;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,YAAY,EAAE,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAS3F"}
@@ -0,0 +1,20 @@
1
+ import { type AppColorName } from './color-presets';
2
+ /**
3
+ * Web fork of `applyNativeRootVars` — a no-op.
4
+ *
5
+ * On web, `BloomThemeProvider` already writes the active preset's CSS vars to
6
+ * `document.documentElement` via `applyColorPresetVars`, which the browser /
7
+ * react-native-web resolve natively. The native/default variant — the plain
8
+ * sibling `native-root-vars.ts` — instead publishes those vars into
9
+ * react-native-css's GLOBAL `rootVariables` family.
10
+ *
11
+ * Keeping the native variant's `react-native-css/native-internal` import out of
12
+ * the web bundle is the whole reason this is a platform-split module: web (and
13
+ * any non-NativeWind host) picks this `.web` fork over the plain file and so
14
+ * never statically pulls react-native-css internals.
15
+ *
16
+ * The signature is intentionally identical to the plain variant so the import
17
+ * site in `BloomThemeProvider` is platform-agnostic.
18
+ */
19
+ export declare function applyNativeRootVars(_colorPreset: AppColorName, _mode: 'light' | 'dark'): void;
20
+ //# sourceMappingURL=native-root-vars.web.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"native-root-vars.web.d.ts","sourceRoot":"","sources":["../../../../src/theme/native-root-vars.web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAEpD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,mBAAmB,CAAC,YAAY,EAAE,YAAY,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAE7F"}
@@ -1 +1 @@
1
- {"version":3,"file":"preset-vars.d.ts","sourceRoot":"","sources":["../../../../src/theme/preset-vars.ts"],"names":[],"mappings":"AACA,OAAO,EAAqB,KAAK,YAAY,EAAE,KAAK,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAiC1F,MAAM,WAAW,iBAAiB;IAChC;;;;;;OAMG;IACH,wBAAwB,CAAC,EAAE,OAAO,CAAC;CACpC;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAC3B,SAAS,EAAE,YAAY,EACvB,IAAI,EAAE,OAAO,GAAG,MAAM,EACtB,OAAO,GAAE,iBAAsB,GAC9B,YAAY,CAqCd;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,YAAY,EACvB,IAAI,EAAE,OAAO,GAAG,MAAM,EACtB,OAAO,CAAC,EAAE,iBAAiB,GAC1B,IAAI,CAQN"}
1
+ {"version":3,"file":"preset-vars.d.ts","sourceRoot":"","sources":["../../../../src/theme/preset-vars.ts"],"names":[],"mappings":"AACA,OAAO,EAAqB,KAAK,YAAY,EAAE,KAAK,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAiC1F,MAAM,WAAW,iBAAiB;IAChC;;;;;;OAMG;IACH,wBAAwB,CAAC,EAAE,OAAO,CAAC;CACpC;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAC3B,SAAS,EAAE,YAAY,EACvB,IAAI,EAAE,OAAO,GAAG,MAAM,EACtB,OAAO,GAAE,iBAAsB,GAC9B,YAAY,CAwCd;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,YAAY,EACvB,IAAI,EAAE,OAAO,GAAG,MAAM,EACtB,OAAO,CAAC,EAAE,iBAAiB,GAC1B,IAAI,CAQN"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxyhq/bloom",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "Bloom UI — Oxy ecosystem component library for React Native + Expo + Web",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "module": "lib/module/index.js",
@@ -0,0 +1,125 @@
1
+ type SetCall = ReadonlyArray<readonly [string]>;
2
+
3
+ /**
4
+ * Verifies the native/default variant `theme/native-root-vars.ts` publishes the
5
+ * active preset into react-native-css's GLOBAL `rootVariables` family with the
6
+ * exact keying and value shape react-native-css itself uses for compiled `:root`
7
+ * vars, and that the web fork `theme/native-root-vars.web.ts` is an inert no-op.
8
+ *
9
+ * - keys are written WITHOUT the leading `--` (the family is keyed by the bare
10
+ * name — confirmed against react-native-css@3.0.7's compiler, which stores
11
+ * `:root { --x }` via `slice(2)` and looks up `var(--x)` by the bare name);
12
+ * - each value is a single-element value-array `[[value]]` (`VariableValue[]`);
13
+ * - the native variant reaches react-native-css via a STATIC ESM import (so
14
+ * Metro resolves the `module` build — the SAME family the renderer reads);
15
+ * - the web variant never imports react-native-css and writes nothing.
16
+ *
17
+ * react-native-css is NOT a Bloom dependency (it arrives transitively via the
18
+ * host's NativeWind install), so the subpath is mocked virtually here — both for
19
+ * the runtime `.set` capture AND so ts-jest can compile the static import. The
20
+ * ambient `src/react-native-css.d.ts` declares the subpath's types for `tsc`.
21
+ */
22
+ describe('applyNativeRootVars (native variant)', () => {
23
+ beforeEach(() => {
24
+ jest.resetModules();
25
+ });
26
+
27
+ it('writes preset vars to rootVariables with the leading -- stripped', () => {
28
+ const sets: Record<string, SetCall> = {};
29
+
30
+ jest.isolateModules(() => {
31
+ jest.doMock(
32
+ 'react-native-css/native-internal',
33
+ () => ({
34
+ rootVariables: (name: string) => ({
35
+ set: (value: SetCall) => {
36
+ sets[name] = value;
37
+ },
38
+ }),
39
+ }),
40
+ { virtual: true },
41
+ );
42
+
43
+ const { applyNativeRootVars } = require('../theme/native-root-vars') as {
44
+ applyNativeRootVars: (preset: string, mode: 'light' | 'dark') => void;
45
+ };
46
+ applyNativeRootVars('blue', 'light');
47
+ });
48
+
49
+ // Keys are bare (no `--`), and the raw HSL triple is present unwrapped.
50
+ expect(sets).toHaveProperty('primary');
51
+ expect(sets).not.toHaveProperty('--primary');
52
+ expect(sets.primary).toEqual([['205 87% 53%']]);
53
+
54
+ // Resolved Tailwind v4 `--color-*` vars are written too, also bare-keyed.
55
+ expect(sets).toHaveProperty('color-primary');
56
+ expect(sets['color-primary']).toEqual([['hsl(205 87% 53%)']]);
57
+
58
+ // Every value is a single-element value-array `[[value]]` of strings.
59
+ for (const value of Object.values(sets)) {
60
+ expect(Array.isArray(value)).toBe(true);
61
+ expect(value).toHaveLength(1);
62
+ expect(value[0]).toHaveLength(1);
63
+ expect(typeof value[0]?.[0]).toBe('string');
64
+ }
65
+ });
66
+
67
+ it('reflects the resolved mode (dark preset differs from light)', () => {
68
+ const collect = (mode: 'light' | 'dark'): Record<string, SetCall> => {
69
+ const sets: Record<string, SetCall> = {};
70
+ jest.isolateModules(() => {
71
+ jest.doMock(
72
+ 'react-native-css/native-internal',
73
+ () => ({
74
+ rootVariables: (name: string) => ({
75
+ set: (value: SetCall) => {
76
+ sets[name] = value;
77
+ },
78
+ }),
79
+ }),
80
+ { virtual: true },
81
+ );
82
+ const { applyNativeRootVars } = require('../theme/native-root-vars') as {
83
+ applyNativeRootVars: (preset: string, mode: 'light' | 'dark') => void;
84
+ };
85
+ applyNativeRootVars('teal', mode);
86
+ });
87
+ return sets;
88
+ };
89
+
90
+ expect(collect('light').background).not.toEqual(collect('dark').background);
91
+ });
92
+
93
+ it('guards when rootVariables is not callable (defensive, never throws)', () => {
94
+ jest.isolateModules(() => {
95
+ jest.doMock(
96
+ 'react-native-css/native-internal',
97
+ () => ({ rootVariables: undefined }),
98
+ { virtual: true },
99
+ );
100
+ const { applyNativeRootVars } = require('../theme/native-root-vars') as {
101
+ applyNativeRootVars: (preset: string, mode: 'light' | 'dark') => void;
102
+ };
103
+ expect(() => applyNativeRootVars('blue', 'light')).not.toThrow();
104
+ });
105
+ });
106
+ });
107
+
108
+ describe('applyNativeRootVars (web fork)', () => {
109
+ beforeEach(() => {
110
+ jest.resetModules();
111
+ });
112
+
113
+ it('no-ops on web (vars are written to documentElement there instead)', () => {
114
+ // The web fork must never touch react-native-css. We assert it neither
115
+ // imports the subpath nor throws — it is a pure no-op with the same
116
+ // signature as the native variant.
117
+ jest.isolateModules(() => {
118
+ const { applyNativeRootVars } = require('../theme/native-root-vars.web') as {
119
+ applyNativeRootVars: (preset: string, mode: 'light' | 'dark') => void;
120
+ };
121
+ expect(() => applyNativeRootVars('blue', 'light')).not.toThrow();
122
+ expect(applyNativeRootVars('blue', 'light')).toBeUndefined();
123
+ });
124
+ });
125
+ });
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Ambient declaration for `react-native-css/native-internal`.
3
+ *
4
+ * react-native-css is NOT a Bloom dependency — it arrives transitively via the
5
+ * host app's NativeWind install (NativeWind 5 is built on react-native-css@3).
6
+ * Bloom's `native-root-vars.native.ts` statically imports `rootVariables` from
7
+ * this subpath so Metro resolves the `import` export condition to the package's
8
+ * `module` build — the SAME family instance the react-native-css renderer
9
+ * (`varResolver`) subscribes to. A runtime `require()` would resolve the
10
+ * `require` condition to the separate `commonjs` build, whose `rootVariables`
11
+ * is a distinct, un-observed family instance (see the file-level comment in
12
+ * `native-root-vars.native.ts`).
13
+ *
14
+ * Because the package isn't installed in Bloom, `tsc` can't see its published
15
+ * types, so we declare the slice we use here — typed to match react-native-css
16
+ * @3.0.7's `dist/typescript/.../native-internal/root.d.ts` exactly (no `any`,
17
+ * no shims). This file is `.native`-only in spirit; the web fork never imports
18
+ * the subpath, so on web bundlers the static import is never reached.
19
+ */
20
+ declare module 'react-native-css/native-internal' {
21
+ /**
22
+ * A leaf style value. Mirrors react-native-css's `compiler.types`
23
+ * `StyleDescriptor`. Bloom only ever writes string HSL triples / `hsl(...)`
24
+ * strings, but the family is typed against the full descriptor union.
25
+ */
26
+ type StyleDescriptor =
27
+ | string
28
+ | number
29
+ | boolean
30
+ | undefined
31
+ | StyleDescriptor[];
32
+
33
+ /**
34
+ * A media-conditioned predicate guarding a variable value. Bloom never emits
35
+ * conditioned values (its preset vars are unconditional), so the inner shape
36
+ * is intentionally opaque here — declaring the full recursive union would
37
+ * duplicate a large compiler type we never construct.
38
+ */
39
+ type MediaCondition = readonly unknown[];
40
+
41
+ /**
42
+ * A single variable value: a `[value]` tuple, optionally guarded by media
43
+ * conditions. Mirrors react-native-css's `VariableValue`. `rootVariables(...).set`
44
+ * takes a `VariableValue[]` (the variable's full ordered value list).
45
+ */
46
+ type VariableValue =
47
+ | readonly [StyleDescriptor]
48
+ | readonly [StyleDescriptor, readonly MediaCondition[]];
49
+
50
+ /**
51
+ * The reactive cell react-native-css renders from. `rootVariables(name)`
52
+ * returns one of these; `.set` replaces its value list and notifies the
53
+ * renderer's subscribed effects. Mirrors `Observable<StyleDescriptor, VariableValue[]>`.
54
+ */
55
+ interface Observable {
56
+ set: (value: readonly VariableValue[]) => void;
57
+ }
58
+
59
+ /**
60
+ * The GLOBAL root-variable family — `rootVariables(name)` returns the
61
+ * observable for the bare variable `name` (no leading `--`). This is the
62
+ * exact callable react-native-css's compiled `:root {}` / `@theme` vars are
63
+ * written through (`StyleCollection.inject` → `rootVariables(name).set(...)`).
64
+ */
65
+ export const rootVariables: ((name: string) => Observable) & {
66
+ delete(name: string): boolean;
67
+ clear(): void;
68
+ };
69
+ }
@@ -27,7 +27,8 @@ import { FontLoader } from '../fonts/FontLoader';
27
27
  import { applyDarkClass, applyColorPresetVars } from './apply-dark-class';
28
28
  import { buildTheme } from './build-theme';
29
29
  import { type AppColorName } from './color-presets';
30
- import { buildNativePresetStyle } from './color-scope/style-builder';
30
+ import { buildScopeVars, getVariableContextProvider } from './color-scope/style-builder';
31
+ import { applyNativeRootVars } from './native-root-vars';
31
32
  import {
32
33
  readPersistedTheme,
33
34
  readPersistedThemeSync,
@@ -295,6 +296,17 @@ export function BloomThemeProvider({
295
296
  applyColorPresetVars(colorPreset, resolved);
296
297
  }, [effectiveMode, resolved, colorPreset]);
297
298
 
299
+ // Native only: publish the preset vars into react-native-css's GLOBAL root
300
+ // variable family so they reach the ENTIRE app tree — navigator screens and
301
+ // every portal/modal/bottom-sheet that paints outside the provider's React
302
+ // subtree (and therefore never sees the `VariableContextProvider` vars below).
303
+ // Web writes the same vars to `document.documentElement` above, so this no-ops
304
+ // there. Runs before paint via `useIsomorphicLayoutEffect`, matching the
305
+ // sibling `applyColorPresetVars` side effect.
306
+ useIsomorphicLayoutEffect(() => {
307
+ applyNativeRootVars(colorPreset, resolved);
308
+ }, [colorPreset, resolved]);
309
+
298
310
  const contextValue = useMemo<BloomThemeContextValue>(
299
311
  () => ({
300
312
  theme: buildTheme(colorPreset, resolved, isAdaptive),
@@ -310,10 +322,15 @@ export function BloomThemeProvider({
310
322
  const shouldAwait = awaitHydration ?? Boolean(persistKey && storage);
311
323
  const isGated = shouldAwait && !hydrated;
312
324
 
313
- const nativeVarsStyle = useMemo(
314
- () => (Platform.OS === 'web' ? undefined : buildNativePresetStyle(colorPreset, resolved)),
325
+ // The full `--name -> value` record for the active preset. On native this is
326
+ // handed to `VariableContextProvider` so descendants resolve `var(--primary)`
327
+ // etc. through react-native-css's real VariableContext. Web writes these to
328
+ // `document.documentElement` via `applyColorPresetVars`, so it's unused there.
329
+ const nativeVars = useMemo(
330
+ () => (Platform.OS === 'web' ? null : buildScopeVars(colorPreset, resolved)),
315
331
  [colorPreset, resolved],
316
332
  );
333
+ const VariableProvider = getVariableContextProvider();
317
334
 
318
335
  const content = (
319
336
  <FontLoader enabled={fonts} fallback={onFontsLoading}>
@@ -321,12 +338,21 @@ export function BloomThemeProvider({
321
338
  </FontLoader>
322
339
  );
323
340
 
341
+ if (Platform.OS === 'web') {
342
+ return <BloomThemeContext.Provider value={contextValue}>{content}</BloomThemeContext.Provider>;
343
+ }
344
+
345
+ // The flex wrapper is still needed for layout; the preset vars now flow
346
+ // through `VariableContextProvider` rather than an (inert) inline-vars style.
347
+ // If NativeWind isn't installed the wrapper still renders so we never crash.
348
+ const nativeTree = <View style={[{ flex: 1 }, nativeWrapperStyle]}>{content}</View>;
349
+
324
350
  return (
325
351
  <BloomThemeContext.Provider value={contextValue}>
326
- {Platform.OS === 'web' ? (
327
- content
352
+ {VariableProvider && nativeVars ? (
353
+ <VariableProvider value={nativeVars}>{nativeTree}</VariableProvider>
328
354
  ) : (
329
- <View style={[{ flex: 1 }, nativeVarsStyle, nativeWrapperStyle]}>{content}</View>
355
+ nativeTree
330
356
  )}
331
357
  </BloomThemeContext.Provider>
332
358
  );