@oxyhq/bloom 0.3.3 → 0.3.5
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/lib/commonjs/fonts/FontLoader.native.js +60 -2
- package/lib/commonjs/fonts/FontLoader.native.js.map +1 -1
- package/lib/commonjs/theme/set-color-scheme-safe.js +34 -12
- package/lib/commonjs/theme/set-color-scheme-safe.js.map +1 -1
- package/lib/module/fonts/FontLoader.native.js +59 -1
- package/lib/module/fonts/FontLoader.native.js.map +1 -1
- package/lib/module/theme/set-color-scheme-safe.js +34 -12
- package/lib/module/theme/set-color-scheme-safe.js.map +1 -1
- package/lib/typescript/commonjs/fonts/FontLoader.native.d.ts +10 -0
- package/lib/typescript/commonjs/fonts/FontLoader.native.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/set-color-scheme-safe.d.ts +27 -9
- package/lib/typescript/commonjs/theme/set-color-scheme-safe.d.ts.map +1 -1
- package/lib/typescript/module/fonts/FontLoader.native.d.ts +10 -0
- package/lib/typescript/module/fonts/FontLoader.native.d.ts.map +1 -1
- package/lib/typescript/module/theme/set-color-scheme-safe.d.ts +27 -9
- package/lib/typescript/module/theme/set-color-scheme-safe.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/FontLoader.native.test.tsx +72 -1
- package/src/fonts/FontLoader.native.tsx +74 -1
- package/src/theme/set-color-scheme-safe.ts +36 -12
|
@@ -4,11 +4,54 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.FontLoader = FontLoader;
|
|
7
|
-
var _react =
|
|
7
|
+
var _react = _interopRequireWildcard(require("react"));
|
|
8
|
+
var _reactNative = require("react-native");
|
|
8
9
|
var _expoFont = require("expo-font");
|
|
9
10
|
var _fontAssets = require("./font-assets.js");
|
|
10
11
|
var _jsxRuntime = require("react/jsx-runtime");
|
|
11
|
-
function
|
|
12
|
+
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
|
|
13
|
+
/**
|
|
14
|
+
* React Native's `Text` exposes a static `defaultProps` for app-wide prop
|
|
15
|
+
* defaults. The public type definition omits it (it's an implementation
|
|
16
|
+
* detail, not part of the stable API), so we describe it locally rather
|
|
17
|
+
* than casting to `any`.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Mutates `Text.defaultProps.style` so every native `<Text>` in the consuming
|
|
22
|
+
* app inherits Bloom's sans font (Inter). The Bloom family name is prepended
|
|
23
|
+
* to any existing default style so caller-provided `style` (which React
|
|
24
|
+
* applies AFTER `defaultProps.style`) still wins for overrides.
|
|
25
|
+
*
|
|
26
|
+
* Idempotent: re-invoking it after the first apply is a no-op because the
|
|
27
|
+
* first style entry will already be the Bloom default we added.
|
|
28
|
+
*/
|
|
29
|
+
function applyDefaultTextFont() {
|
|
30
|
+
const TextWithDefaults = _reactNative.Text;
|
|
31
|
+
const existing = TextWithDefaults.defaultProps?.style;
|
|
32
|
+
const bloomDefault = {
|
|
33
|
+
fontFamily: 'Inter'
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Detect whether we've already prepended ourselves. `existing` may be an
|
|
37
|
+
// array (multiple style fragments), a single object, a registered style
|
|
38
|
+
// number, or undefined. Only the array/object cases need inspection — if
|
|
39
|
+
// it's a number, we can't read the contents so we just prepend.
|
|
40
|
+
if (Array.isArray(existing)) {
|
|
41
|
+
const first = existing[0];
|
|
42
|
+
if (first && typeof first === 'object' && !Array.isArray(first) && first.fontFamily === bloomDefault.fontFamily) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
} else if (existing && typeof existing === 'object' && existing.fontFamily === bloomDefault.fontFamily) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const nextStyle = existing == null ? bloomDefault : [bloomDefault, existing];
|
|
49
|
+
TextWithDefaults.defaultProps = {
|
|
50
|
+
...TextWithDefaults.defaultProps,
|
|
51
|
+
style: nextStyle
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
12
55
|
/**
|
|
13
56
|
* Native font loader. Calls `expo-font`'s `useFonts` with the Bloom font
|
|
14
57
|
* asset map and gates `children` on the load result. The hook is invoked
|
|
@@ -16,6 +59,16 @@ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e
|
|
|
16
59
|
* fonts get loaded. This keeps Hook order stable across renders and means
|
|
17
60
|
* `<BloomThemeProvider fonts={false}>` still gets fonts pre-loaded if the
|
|
18
61
|
* provider tree ever flips `fonts` back on.
|
|
62
|
+
*
|
|
63
|
+
* Once the bundled fonts report `loaded === true`, this component also
|
|
64
|
+
* mutates `Text.defaultProps.style` to prepend `{ fontFamily: 'Inter' }`
|
|
65
|
+
* so every native `<Text>` in the consuming app inherits Bloom's sans
|
|
66
|
+
* family without callers needing to import `<Text>` from `@oxyhq/bloom`.
|
|
67
|
+
* The mutation is gated by a `useRef` so it runs at most once per
|
|
68
|
+
* FontLoader instance, and the underlying apply is idempotent — the
|
|
69
|
+
* Bloom default is prepended to any caller `defaultProps.style`, so
|
|
70
|
+
* per-call `style` overrides (which React applies after `defaultProps`)
|
|
71
|
+
* still take precedence.
|
|
19
72
|
*/
|
|
20
73
|
function FontLoader({
|
|
21
74
|
enabled,
|
|
@@ -23,6 +76,11 @@ function FontLoader({
|
|
|
23
76
|
children
|
|
24
77
|
}) {
|
|
25
78
|
const [loaded] = (0, _expoFont.useFonts)(_fontAssets.FONT_ASSETS);
|
|
79
|
+
const defaultsApplied = (0, _react.useRef)(false);
|
|
80
|
+
if (loaded && !defaultsApplied.current) {
|
|
81
|
+
defaultsApplied.current = true;
|
|
82
|
+
applyDefaultTextFont();
|
|
83
|
+
}
|
|
26
84
|
if (!enabled) return /*#__PURE__*/(0, _jsxRuntime.jsx)(_jsxRuntime.Fragment, {
|
|
27
85
|
children: children
|
|
28
86
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["_react","
|
|
1
|
+
{"version":3,"names":["_react","_interopRequireWildcard","require","_reactNative","_expoFont","_fontAssets","_jsxRuntime","e","t","WeakMap","r","n","__esModule","o","i","f","__proto__","default","has","get","set","hasOwnProperty","call","Object","defineProperty","getOwnPropertyDescriptor","applyDefaultTextFont","TextWithDefaults","Text","existing","defaultProps","style","bloomDefault","fontFamily","Array","isArray","first","nextStyle","FontLoader","enabled","fallback","children","loaded","useFonts","FONT_ASSETS","defaultsApplied","useRef","current","jsx","Fragment"],"sourceRoot":"../../../src","sources":["fonts/FontLoader.native.tsx"],"mappings":";;;;;;AAAA,IAAAA,MAAA,GAAAC,uBAAA,CAAAC,OAAA;AACA,IAAAC,YAAA,GAAAD,OAAA;AACA,IAAAE,SAAA,GAAAF,OAAA;AACA,IAAAG,WAAA,GAAAH,OAAA;AAA4C,IAAAI,WAAA,GAAAJ,OAAA;AAAA,SAAAD,wBAAAM,CAAA,EAAAC,CAAA,6BAAAC,OAAA,MAAAC,CAAA,OAAAD,OAAA,IAAAE,CAAA,OAAAF,OAAA,YAAAR,uBAAA,YAAAA,CAAAM,CAAA,EAAAC,CAAA,SAAAA,CAAA,IAAAD,CAAA,IAAAA,CAAA,CAAAK,UAAA,SAAAL,CAAA,MAAAM,CAAA,EAAAC,CAAA,EAAAC,CAAA,KAAAC,SAAA,QAAAC,OAAA,EAAAV,CAAA,iBAAAA,CAAA,uBAAAA,CAAA,yBAAAA,CAAA,SAAAQ,CAAA,MAAAF,CAAA,GAAAL,CAAA,GAAAG,CAAA,GAAAD,CAAA,QAAAG,CAAA,CAAAK,GAAA,CAAAX,CAAA,UAAAM,CAAA,CAAAM,GAAA,CAAAZ,CAAA,GAAAM,CAAA,CAAAO,GAAA,CAAAb,CAAA,EAAAQ,CAAA,gBAAAP,CAAA,IAAAD,CAAA,gBAAAC,CAAA,OAAAa,cAAA,CAAAC,IAAA,CAAAf,CAAA,EAAAC,CAAA,OAAAM,CAAA,IAAAD,CAAA,GAAAU,MAAA,CAAAC,cAAA,KAAAD,MAAA,CAAAE,wBAAA,CAAAlB,CAAA,EAAAC,CAAA,OAAAM,CAAA,CAAAK,GAAA,IAAAL,CAAA,CAAAM,GAAA,IAAAP,CAAA,CAAAE,CAAA,EAAAP,CAAA,EAAAM,CAAA,IAAAC,CAAA,CAAAP,CAAA,IAAAD,CAAA,CAAAC,CAAA,WAAAO,CAAA,KAAAR,CAAA,EAAAC,CAAA;AAc5C;AACA;AACA;AACA;AACA;AACA;;AAKA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAASkB,oBAAoBA,CAAA,EAAS;EACpC,MAAMC,gBAAgB,GAAGC,iBAAwB;EACjD,MAAMC,QAAQ,GAAGF,gBAAgB,CAACG,YAAY,EAAEC,KAAK;EACrD,MAAMC,YAAuB,GAAG;IAAEC,UAAU,EAAE;EAAQ,CAAC;;EAEvD;EACA;EACA;EACA;EACA,IAAIC,KAAK,CAACC,OAAO,CAACN,QAAQ,CAAC,EAAE;IAC3B,MAAMO,KAAK,GAAGP,QAAQ,CAAC,CAAC,CAAC;IACzB,IACEO,KAAK,IACL,OAAOA,KAAK,KAAK,QAAQ,IACzB,CAACF,KAAK,CAACC,OAAO,CAACC,KAAK,CAAC,IACpBA,KAAK,CAAeH,UAAU,KAAKD,YAAY,CAACC,UAAU,EAC3D;MACA;IACF;EACF,CAAC,MAAM,IACLJ,QAAQ,IACR,OAAOA,QAAQ,KAAK,QAAQ,IAC3BA,QAAQ,CAAeI,UAAU,KAAKD,YAAY,CAACC,UAAU,EAC9D;IACA;EACF;EAEA,MAAMI,SAA+B,GACnCR,QAAQ,IAAI,IAAI,GAAGG,YAAY,GAAG,CAACA,YAAY,EAAEH,QAAQ,CAAC;EAE5DF,gBAAgB,CAACG,YAAY,GAAG;IAC9B,GAAGH,gBAAgB,CAACG,YAAY;IAChCC,KAAK,EAAEM;EACT,CAAC;AACH;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAASC,UAAUA,CAAC;EAAEC,OAAO;EAAEC,QAAQ;EAAEC;AAA0B,CAAC,EAAE;EAC3E,MAAM,CAACC,MAAM,CAAC,GAAG,IAAAC,kBAAQ,EAACC,uBAAW,CAAC;EACtC,MAAMC,eAAe,GAAG,IAAAC,aAAM,EAAC,KAAK,CAAC;EAErC,IAAIJ,MAAM,IAAI,CAACG,eAAe,CAACE,OAAO,EAAE;IACtCF,eAAe,CAACE,OAAO,GAAG,IAAI;IAC9BrB,oBAAoB,CAAC,CAAC;EACxB;EAEA,IAAI,CAACa,OAAO,EAAE,oBAAO,IAAAjC,WAAA,CAAA0C,GAAA,EAAA1C,WAAA,CAAA2C,QAAA;IAAAR,QAAA,EAAGA;EAAQ,CAAG,CAAC;EACpC,IAAI,CAACC,MAAM,EAAE,oBAAO,IAAApC,WAAA,CAAA0C,GAAA,EAAA1C,WAAA,CAAA2C,QAAA;IAAAR,QAAA,EAAGD,QAAQ,IAAI;EAAI,CAAG,CAAC;EAC3C,oBAAO,IAAAlC,WAAA,CAAA0C,GAAA,EAAA1C,WAAA,CAAA2C,QAAA;IAAAR,QAAA,EAAGA;EAAQ,CAAG,CAAC;AACxB","ignoreList":[]}
|
|
@@ -7,15 +7,33 @@ exports.setColorSchemeSafe = setColorSchemeSafe;
|
|
|
7
7
|
var _reactNative = require("react-native");
|
|
8
8
|
/**
|
|
9
9
|
* Safely set the color scheme via Appearance API.
|
|
10
|
-
* On Android (RN 0.83+), Appearance.setColorScheme has a Kotlin non-null
|
|
11
|
-
* annotation on `style`. Passing null for 'system' crashes.
|
|
12
|
-
* Workaround: resolve the system preference and pass 'light'/'dark' instead.
|
|
13
10
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
11
|
+
* Behavior by mode:
|
|
12
|
+
* - 'light' / 'dark': set the explicit override.
|
|
13
|
+
* - 'system' / 'adaptive': leave the OS in control. We must NOT call
|
|
14
|
+
* Appearance.setColorScheme(resolved) here — doing so installs an
|
|
15
|
+
* app-level override that masks the OS preference. Once that override
|
|
16
|
+
* is set, useColorScheme() / Appearance.getColorScheme() return the
|
|
17
|
+
* frozen override instead of the live OS value, and the app stops
|
|
18
|
+
* following dark↔light OS toggles until a cold restart.
|
|
19
|
+
*
|
|
20
|
+
* On iOS we additionally pass 'unspecified' to clear any prior
|
|
21
|
+
* override that may have been installed by a previous explicit mode.
|
|
22
|
+
* 'unspecified' is the documented sentinel that tells the native
|
|
23
|
+
* Appearance module to fall back to the OS preference. (RN's JS
|
|
24
|
+
* implementation forwards this straight through to the native
|
|
25
|
+
* bridge; on iOS this clears the override.)
|
|
26
|
+
*
|
|
27
|
+
* On Android (RN 0.83+) the native Kotlin signature has @NonNull on
|
|
28
|
+
* `style` and rejects null, and 'unspecified' is not honored as a
|
|
29
|
+
* clear-override sentinel on Android either. As a result, if a user
|
|
30
|
+
* previously selected 'light' or 'dark' and then switches back to
|
|
31
|
+
* 'system' on Android, the override remains until the next cold
|
|
32
|
+
* restart. Users who never explicitly overrode are unaffected because
|
|
33
|
+
* we never install an override in system mode in the first place.
|
|
34
|
+
*
|
|
35
|
+
* On react-native-web, Appearance.setColorScheme is not implemented at
|
|
36
|
+
* all; the browser controls the color scheme, so we bail out on web.
|
|
19
37
|
*/
|
|
20
38
|
function setColorSchemeSafe(mode) {
|
|
21
39
|
if (_reactNative.Platform.OS === 'web') {
|
|
@@ -23,10 +41,14 @@ function setColorSchemeSafe(mode) {
|
|
|
23
41
|
}
|
|
24
42
|
const effectiveMode = mode === 'adaptive' ? 'system' : mode;
|
|
25
43
|
if (effectiveMode === 'system') {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
_reactNative.
|
|
44
|
+
// Clear any prior app-level override so useColorScheme() tracks the
|
|
45
|
+
// OS. iOS honors 'unspecified' as a sentinel to fall back to the
|
|
46
|
+
// system preference; Android does not (see note above).
|
|
47
|
+
if (_reactNative.Platform.OS === 'ios') {
|
|
48
|
+
_reactNative.Appearance.setColorScheme('unspecified');
|
|
49
|
+
}
|
|
50
|
+
return;
|
|
30
51
|
}
|
|
52
|
+
_reactNative.Appearance.setColorScheme(effectiveMode);
|
|
31
53
|
}
|
|
32
54
|
//# sourceMappingURL=set-color-scheme-safe.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["_reactNative","require","setColorSchemeSafe","mode","Platform","OS","effectiveMode","
|
|
1
|
+
{"version":3,"names":["_reactNative","require","setColorSchemeSafe","mode","Platform","OS","effectiveMode","Appearance","setColorScheme"],"sourceRoot":"../../../src","sources":["theme/set-color-scheme-safe.ts"],"mappings":";;;;;;AAAA,IAAAA,YAAA,GAAAC,OAAA;AAGA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAASC,kBAAkBA,CAACC,IAAe,EAAE;EAClD,IAAIC,qBAAQ,CAACC,EAAE,KAAK,KAAK,EAAE;IACzB;EACF;EAEA,MAAMC,aAAa,GAAGH,IAAI,KAAK,UAAU,GAAG,QAAQ,GAAGA,IAAI;EAE3D,IAAIG,aAAa,KAAK,QAAQ,EAAE;IAC9B;IACA;IACA;IACA,IAAIF,qBAAQ,CAACC,EAAE,KAAK,KAAK,EAAE;MACzBE,uBAAU,CAACC,cAAc,CAAC,aAAa,CAAC;IAC1C;IACA;EACF;EAEAD,uBAAU,CAACC,cAAc,CAACF,aAAa,CAAC;AAC1C","ignoreList":[]}
|
|
@@ -1,9 +1,52 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
import React from 'react';
|
|
3
|
+
import React, { useRef } from 'react';
|
|
4
|
+
import { Text } from 'react-native';
|
|
4
5
|
import { useFonts } from 'expo-font';
|
|
5
6
|
import { FONT_ASSETS } from "./font-assets.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* React Native's `Text` exposes a static `defaultProps` for app-wide prop
|
|
10
|
+
* defaults. The public type definition omits it (it's an implementation
|
|
11
|
+
* detail, not part of the stable API), so we describe it locally rather
|
|
12
|
+
* than casting to `any`.
|
|
13
|
+
*/
|
|
6
14
|
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
15
|
+
/**
|
|
16
|
+
* Mutates `Text.defaultProps.style` so every native `<Text>` in the consuming
|
|
17
|
+
* app inherits Bloom's sans font (Inter). The Bloom family name is prepended
|
|
18
|
+
* to any existing default style so caller-provided `style` (which React
|
|
19
|
+
* applies AFTER `defaultProps.style`) still wins for overrides.
|
|
20
|
+
*
|
|
21
|
+
* Idempotent: re-invoking it after the first apply is a no-op because the
|
|
22
|
+
* first style entry will already be the Bloom default we added.
|
|
23
|
+
*/
|
|
24
|
+
function applyDefaultTextFont() {
|
|
25
|
+
const TextWithDefaults = Text;
|
|
26
|
+
const existing = TextWithDefaults.defaultProps?.style;
|
|
27
|
+
const bloomDefault = {
|
|
28
|
+
fontFamily: 'Inter'
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Detect whether we've already prepended ourselves. `existing` may be an
|
|
32
|
+
// array (multiple style fragments), a single object, a registered style
|
|
33
|
+
// number, or undefined. Only the array/object cases need inspection — if
|
|
34
|
+
// it's a number, we can't read the contents so we just prepend.
|
|
35
|
+
if (Array.isArray(existing)) {
|
|
36
|
+
const first = existing[0];
|
|
37
|
+
if (first && typeof first === 'object' && !Array.isArray(first) && first.fontFamily === bloomDefault.fontFamily) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
} else if (existing && typeof existing === 'object' && existing.fontFamily === bloomDefault.fontFamily) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const nextStyle = existing == null ? bloomDefault : [bloomDefault, existing];
|
|
44
|
+
TextWithDefaults.defaultProps = {
|
|
45
|
+
...TextWithDefaults.defaultProps,
|
|
46
|
+
style: nextStyle
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
7
50
|
/**
|
|
8
51
|
* Native font loader. Calls `expo-font`'s `useFonts` with the Bloom font
|
|
9
52
|
* asset map and gates `children` on the load result. The hook is invoked
|
|
@@ -11,6 +54,16 @@ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
|
11
54
|
* fonts get loaded. This keeps Hook order stable across renders and means
|
|
12
55
|
* `<BloomThemeProvider fonts={false}>` still gets fonts pre-loaded if the
|
|
13
56
|
* provider tree ever flips `fonts` back on.
|
|
57
|
+
*
|
|
58
|
+
* Once the bundled fonts report `loaded === true`, this component also
|
|
59
|
+
* mutates `Text.defaultProps.style` to prepend `{ fontFamily: 'Inter' }`
|
|
60
|
+
* so every native `<Text>` in the consuming app inherits Bloom's sans
|
|
61
|
+
* family without callers needing to import `<Text>` from `@oxyhq/bloom`.
|
|
62
|
+
* The mutation is gated by a `useRef` so it runs at most once per
|
|
63
|
+
* FontLoader instance, and the underlying apply is idempotent — the
|
|
64
|
+
* Bloom default is prepended to any caller `defaultProps.style`, so
|
|
65
|
+
* per-call `style` overrides (which React applies after `defaultProps`)
|
|
66
|
+
* still take precedence.
|
|
14
67
|
*/
|
|
15
68
|
export function FontLoader({
|
|
16
69
|
enabled,
|
|
@@ -18,6 +71,11 @@ export function FontLoader({
|
|
|
18
71
|
children
|
|
19
72
|
}) {
|
|
20
73
|
const [loaded] = useFonts(FONT_ASSETS);
|
|
74
|
+
const defaultsApplied = useRef(false);
|
|
75
|
+
if (loaded && !defaultsApplied.current) {
|
|
76
|
+
defaultsApplied.current = true;
|
|
77
|
+
applyDefaultTextFont();
|
|
78
|
+
}
|
|
21
79
|
if (!enabled) return /*#__PURE__*/_jsx(_Fragment, {
|
|
22
80
|
children: children
|
|
23
81
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["React","useFonts","FONT_ASSETS","Fragment","_Fragment","jsx","_jsx","FontLoader","enabled","fallback","children","loaded"],"sourceRoot":"../../../src","sources":["fonts/FontLoader.native.tsx"],"mappings":";;AAAA,OAAOA,KAAK,MAAM,OAAO;
|
|
1
|
+
{"version":3,"names":["React","useRef","Text","useFonts","FONT_ASSETS","Fragment","_Fragment","jsx","_jsx","applyDefaultTextFont","TextWithDefaults","existing","defaultProps","style","bloomDefault","fontFamily","Array","isArray","first","nextStyle","FontLoader","enabled","fallback","children","loaded","defaultsApplied","current"],"sourceRoot":"../../../src","sources":["fonts/FontLoader.native.tsx"],"mappings":";;AAAA,OAAOA,KAAK,IAAIC,MAAM,QAAQ,OAAO;AACrC,SAASC,IAAI,QAAwD,cAAc;AACnF,SAASC,QAAQ,QAAQ,WAAW;AACpC,SAASC,WAAW,QAAQ,kBAAe;;AAc3C;AACA;AACA;AACA;AACA;AACA;AALA,SAAAC,QAAA,IAAAC,SAAA,EAAAC,GAAA,IAAAC,IAAA;AAUA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAASC,oBAAoBA,CAAA,EAAS;EACpC,MAAMC,gBAAgB,GAAGR,IAAwB;EACjD,MAAMS,QAAQ,GAAGD,gBAAgB,CAACE,YAAY,EAAEC,KAAK;EACrD,MAAMC,YAAuB,GAAG;IAAEC,UAAU,EAAE;EAAQ,CAAC;;EAEvD;EACA;EACA;EACA;EACA,IAAIC,KAAK,CAACC,OAAO,CAACN,QAAQ,CAAC,EAAE;IAC3B,MAAMO,KAAK,GAAGP,QAAQ,CAAC,CAAC,CAAC;IACzB,IACEO,KAAK,IACL,OAAOA,KAAK,KAAK,QAAQ,IACzB,CAACF,KAAK,CAACC,OAAO,CAACC,KAAK,CAAC,IACpBA,KAAK,CAAeH,UAAU,KAAKD,YAAY,CAACC,UAAU,EAC3D;MACA;IACF;EACF,CAAC,MAAM,IACLJ,QAAQ,IACR,OAAOA,QAAQ,KAAK,QAAQ,IAC3BA,QAAQ,CAAeI,UAAU,KAAKD,YAAY,CAACC,UAAU,EAC9D;IACA;EACF;EAEA,MAAMI,SAA+B,GACnCR,QAAQ,IAAI,IAAI,GAAGG,YAAY,GAAG,CAACA,YAAY,EAAEH,QAAQ,CAAC;EAE5DD,gBAAgB,CAACE,YAAY,GAAG;IAC9B,GAAGF,gBAAgB,CAACE,YAAY;IAChCC,KAAK,EAAEM;EACT,CAAC;AACH;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,UAAUA,CAAC;EAAEC,OAAO;EAAEC,QAAQ;EAAEC;AAA0B,CAAC,EAAE;EAC3E,MAAM,CAACC,MAAM,CAAC,GAAGrB,QAAQ,CAACC,WAAW,CAAC;EACtC,MAAMqB,eAAe,GAAGxB,MAAM,CAAC,KAAK,CAAC;EAErC,IAAIuB,MAAM,IAAI,CAACC,eAAe,CAACC,OAAO,EAAE;IACtCD,eAAe,CAACC,OAAO,GAAG,IAAI;IAC9BjB,oBAAoB,CAAC,CAAC;EACxB;EAEA,IAAI,CAACY,OAAO,EAAE,oBAAOb,IAAA,CAAAF,SAAA;IAAAiB,QAAA,EAAGA;EAAQ,CAAG,CAAC;EACpC,IAAI,CAACC,MAAM,EAAE,oBAAOhB,IAAA,CAAAF,SAAA;IAAAiB,QAAA,EAAGD,QAAQ,IAAI;EAAI,CAAG,CAAC;EAC3C,oBAAOd,IAAA,CAAAF,SAAA;IAAAiB,QAAA,EAAGA;EAAQ,CAAG,CAAC;AACxB","ignoreList":[]}
|
|
@@ -3,15 +3,33 @@
|
|
|
3
3
|
import { Appearance, Platform } from 'react-native';
|
|
4
4
|
/**
|
|
5
5
|
* Safely set the color scheme via Appearance API.
|
|
6
|
-
* On Android (RN 0.83+), Appearance.setColorScheme has a Kotlin non-null
|
|
7
|
-
* annotation on `style`. Passing null for 'system' crashes.
|
|
8
|
-
* Workaround: resolve the system preference and pass 'light'/'dark' instead.
|
|
9
6
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
7
|
+
* Behavior by mode:
|
|
8
|
+
* - 'light' / 'dark': set the explicit override.
|
|
9
|
+
* - 'system' / 'adaptive': leave the OS in control. We must NOT call
|
|
10
|
+
* Appearance.setColorScheme(resolved) here — doing so installs an
|
|
11
|
+
* app-level override that masks the OS preference. Once that override
|
|
12
|
+
* is set, useColorScheme() / Appearance.getColorScheme() return the
|
|
13
|
+
* frozen override instead of the live OS value, and the app stops
|
|
14
|
+
* following dark↔light OS toggles until a cold restart.
|
|
15
|
+
*
|
|
16
|
+
* On iOS we additionally pass 'unspecified' to clear any prior
|
|
17
|
+
* override that may have been installed by a previous explicit mode.
|
|
18
|
+
* 'unspecified' is the documented sentinel that tells the native
|
|
19
|
+
* Appearance module to fall back to the OS preference. (RN's JS
|
|
20
|
+
* implementation forwards this straight through to the native
|
|
21
|
+
* bridge; on iOS this clears the override.)
|
|
22
|
+
*
|
|
23
|
+
* On Android (RN 0.83+) the native Kotlin signature has @NonNull on
|
|
24
|
+
* `style` and rejects null, and 'unspecified' is not honored as a
|
|
25
|
+
* clear-override sentinel on Android either. As a result, if a user
|
|
26
|
+
* previously selected 'light' or 'dark' and then switches back to
|
|
27
|
+
* 'system' on Android, the override remains until the next cold
|
|
28
|
+
* restart. Users who never explicitly overrode are unaffected because
|
|
29
|
+
* we never install an override in system mode in the first place.
|
|
30
|
+
*
|
|
31
|
+
* On react-native-web, Appearance.setColorScheme is not implemented at
|
|
32
|
+
* all; the browser controls the color scheme, so we bail out on web.
|
|
15
33
|
*/
|
|
16
34
|
export function setColorSchemeSafe(mode) {
|
|
17
35
|
if (Platform.OS === 'web') {
|
|
@@ -19,10 +37,14 @@ export function setColorSchemeSafe(mode) {
|
|
|
19
37
|
}
|
|
20
38
|
const effectiveMode = mode === 'adaptive' ? 'system' : mode;
|
|
21
39
|
if (effectiveMode === 'system') {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
40
|
+
// Clear any prior app-level override so useColorScheme() tracks the
|
|
41
|
+
// OS. iOS honors 'unspecified' as a sentinel to fall back to the
|
|
42
|
+
// system preference; Android does not (see note above).
|
|
43
|
+
if (Platform.OS === 'ios') {
|
|
44
|
+
Appearance.setColorScheme('unspecified');
|
|
45
|
+
}
|
|
46
|
+
return;
|
|
26
47
|
}
|
|
48
|
+
Appearance.setColorScheme(effectiveMode);
|
|
27
49
|
}
|
|
28
50
|
//# sourceMappingURL=set-color-scheme-safe.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["Appearance","Platform","setColorSchemeSafe","mode","OS","effectiveMode","
|
|
1
|
+
{"version":3,"names":["Appearance","Platform","setColorSchemeSafe","mode","OS","effectiveMode","setColorScheme"],"sourceRoot":"../../../src","sources":["theme/set-color-scheme-safe.ts"],"mappings":";;AAAA,SAASA,UAAU,EAAEC,QAAQ,QAAQ,cAAc;AAGnD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,kBAAkBA,CAACC,IAAe,EAAE;EAClD,IAAIF,QAAQ,CAACG,EAAE,KAAK,KAAK,EAAE;IACzB;EACF;EAEA,MAAMC,aAAa,GAAGF,IAAI,KAAK,UAAU,GAAG,QAAQ,GAAGA,IAAI;EAE3D,IAAIE,aAAa,KAAK,QAAQ,EAAE;IAC9B;IACA;IACA;IACA,IAAIJ,QAAQ,CAACG,EAAE,KAAK,KAAK,EAAE;MACzBJ,UAAU,CAACM,cAAc,CAAC,aAAa,CAAC;IAC1C;IACA;EACF;EAEAN,UAAU,CAACM,cAAc,CAACD,aAAa,CAAC;AAC1C","ignoreList":[]}
|
|
@@ -17,6 +17,16 @@ export interface FontLoaderProps {
|
|
|
17
17
|
* fonts get loaded. This keeps Hook order stable across renders and means
|
|
18
18
|
* `<BloomThemeProvider fonts={false}>` still gets fonts pre-loaded if the
|
|
19
19
|
* provider tree ever flips `fonts` back on.
|
|
20
|
+
*
|
|
21
|
+
* Once the bundled fonts report `loaded === true`, this component also
|
|
22
|
+
* mutates `Text.defaultProps.style` to prepend `{ fontFamily: 'Inter' }`
|
|
23
|
+
* so every native `<Text>` in the consuming app inherits Bloom's sans
|
|
24
|
+
* family without callers needing to import `<Text>` from `@oxyhq/bloom`.
|
|
25
|
+
* The mutation is gated by a `useRef` so it runs at most once per
|
|
26
|
+
* FontLoader instance, and the underlying apply is idempotent — the
|
|
27
|
+
* Bloom default is prepended to any caller `defaultProps.style`, so
|
|
28
|
+
* per-call `style` overrides (which React applies after `defaultProps`)
|
|
29
|
+
* still take precedence.
|
|
20
30
|
*/
|
|
21
31
|
export declare function FontLoader({ enabled, fallback, children }: FontLoaderProps): import("react/jsx-runtime").JSX.Element;
|
|
22
32
|
//# sourceMappingURL=FontLoader.native.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FontLoader.native.d.ts","sourceRoot":"","sources":["../../../../src/fonts/FontLoader.native.tsx"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"FontLoader.native.d.ts","sourceRoot":"","sources":["../../../../src/fonts/FontLoader.native.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAiB,MAAM,OAAO,CAAC;AAKtC,MAAM,WAAW,eAAe;IAC9B;;;;OAIG;IACH,OAAO,EAAE,OAAO,CAAC;IACjB,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B;AAyDD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,UAAU,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,EAAE,eAAe,2CAY1E"}
|
|
@@ -1,15 +1,33 @@
|
|
|
1
1
|
import type { ThemeMode } from './types';
|
|
2
2
|
/**
|
|
3
3
|
* Safely set the color scheme via Appearance API.
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* Appearance.getColorScheme()
|
|
11
|
-
*
|
|
12
|
-
*
|
|
4
|
+
*
|
|
5
|
+
* Behavior by mode:
|
|
6
|
+
* - 'light' / 'dark': set the explicit override.
|
|
7
|
+
* - 'system' / 'adaptive': leave the OS in control. We must NOT call
|
|
8
|
+
* Appearance.setColorScheme(resolved) here — doing so installs an
|
|
9
|
+
* app-level override that masks the OS preference. Once that override
|
|
10
|
+
* is set, useColorScheme() / Appearance.getColorScheme() return the
|
|
11
|
+
* frozen override instead of the live OS value, and the app stops
|
|
12
|
+
* following dark↔light OS toggles until a cold restart.
|
|
13
|
+
*
|
|
14
|
+
* On iOS we additionally pass 'unspecified' to clear any prior
|
|
15
|
+
* override that may have been installed by a previous explicit mode.
|
|
16
|
+
* 'unspecified' is the documented sentinel that tells the native
|
|
17
|
+
* Appearance module to fall back to the OS preference. (RN's JS
|
|
18
|
+
* implementation forwards this straight through to the native
|
|
19
|
+
* bridge; on iOS this clears the override.)
|
|
20
|
+
*
|
|
21
|
+
* On Android (RN 0.83+) the native Kotlin signature has @NonNull on
|
|
22
|
+
* `style` and rejects null, and 'unspecified' is not honored as a
|
|
23
|
+
* clear-override sentinel on Android either. As a result, if a user
|
|
24
|
+
* previously selected 'light' or 'dark' and then switches back to
|
|
25
|
+
* 'system' on Android, the override remains until the next cold
|
|
26
|
+
* restart. Users who never explicitly overrode are unaffected because
|
|
27
|
+
* we never install an override in system mode in the first place.
|
|
28
|
+
*
|
|
29
|
+
* On react-native-web, Appearance.setColorScheme is not implemented at
|
|
30
|
+
* all; the browser controls the color scheme, so we bail out on web.
|
|
13
31
|
*/
|
|
14
32
|
export declare function setColorSchemeSafe(mode: ThemeMode): void;
|
|
15
33
|
//# sourceMappingURL=set-color-scheme-safe.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"set-color-scheme-safe.d.ts","sourceRoot":"","sources":["../../../../src/theme/set-color-scheme-safe.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEzC
|
|
1
|
+
{"version":3,"file":"set-color-scheme-safe.d.ts","sourceRoot":"","sources":["../../../../src/theme/set-color-scheme-safe.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEzC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,SAAS,QAkBjD"}
|
|
@@ -17,6 +17,16 @@ export interface FontLoaderProps {
|
|
|
17
17
|
* fonts get loaded. This keeps Hook order stable across renders and means
|
|
18
18
|
* `<BloomThemeProvider fonts={false}>` still gets fonts pre-loaded if the
|
|
19
19
|
* provider tree ever flips `fonts` back on.
|
|
20
|
+
*
|
|
21
|
+
* Once the bundled fonts report `loaded === true`, this component also
|
|
22
|
+
* mutates `Text.defaultProps.style` to prepend `{ fontFamily: 'Inter' }`
|
|
23
|
+
* so every native `<Text>` in the consuming app inherits Bloom's sans
|
|
24
|
+
* family without callers needing to import `<Text>` from `@oxyhq/bloom`.
|
|
25
|
+
* The mutation is gated by a `useRef` so it runs at most once per
|
|
26
|
+
* FontLoader instance, and the underlying apply is idempotent — the
|
|
27
|
+
* Bloom default is prepended to any caller `defaultProps.style`, so
|
|
28
|
+
* per-call `style` overrides (which React applies after `defaultProps`)
|
|
29
|
+
* still take precedence.
|
|
20
30
|
*/
|
|
21
31
|
export declare function FontLoader({ enabled, fallback, children }: FontLoaderProps): import("react/jsx-runtime").JSX.Element;
|
|
22
32
|
//# sourceMappingURL=FontLoader.native.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FontLoader.native.d.ts","sourceRoot":"","sources":["../../../../src/fonts/FontLoader.native.tsx"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"FontLoader.native.d.ts","sourceRoot":"","sources":["../../../../src/fonts/FontLoader.native.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAiB,MAAM,OAAO,CAAC;AAKtC,MAAM,WAAW,eAAe;IAC9B;;;;OAIG;IACH,OAAO,EAAE,OAAO,CAAC;IACjB,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B;AAyDD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,UAAU,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,EAAE,eAAe,2CAY1E"}
|
|
@@ -1,15 +1,33 @@
|
|
|
1
1
|
import type { ThemeMode } from './types';
|
|
2
2
|
/**
|
|
3
3
|
* Safely set the color scheme via Appearance API.
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* Appearance.getColorScheme()
|
|
11
|
-
*
|
|
12
|
-
*
|
|
4
|
+
*
|
|
5
|
+
* Behavior by mode:
|
|
6
|
+
* - 'light' / 'dark': set the explicit override.
|
|
7
|
+
* - 'system' / 'adaptive': leave the OS in control. We must NOT call
|
|
8
|
+
* Appearance.setColorScheme(resolved) here — doing so installs an
|
|
9
|
+
* app-level override that masks the OS preference. Once that override
|
|
10
|
+
* is set, useColorScheme() / Appearance.getColorScheme() return the
|
|
11
|
+
* frozen override instead of the live OS value, and the app stops
|
|
12
|
+
* following dark↔light OS toggles until a cold restart.
|
|
13
|
+
*
|
|
14
|
+
* On iOS we additionally pass 'unspecified' to clear any prior
|
|
15
|
+
* override that may have been installed by a previous explicit mode.
|
|
16
|
+
* 'unspecified' is the documented sentinel that tells the native
|
|
17
|
+
* Appearance module to fall back to the OS preference. (RN's JS
|
|
18
|
+
* implementation forwards this straight through to the native
|
|
19
|
+
* bridge; on iOS this clears the override.)
|
|
20
|
+
*
|
|
21
|
+
* On Android (RN 0.83+) the native Kotlin signature has @NonNull on
|
|
22
|
+
* `style` and rejects null, and 'unspecified' is not honored as a
|
|
23
|
+
* clear-override sentinel on Android either. As a result, if a user
|
|
24
|
+
* previously selected 'light' or 'dark' and then switches back to
|
|
25
|
+
* 'system' on Android, the override remains until the next cold
|
|
26
|
+
* restart. Users who never explicitly overrode are unaffected because
|
|
27
|
+
* we never install an override in system mode in the first place.
|
|
28
|
+
*
|
|
29
|
+
* On react-native-web, Appearance.setColorScheme is not implemented at
|
|
30
|
+
* all; the browser controls the color scheme, so we bail out on web.
|
|
13
31
|
*/
|
|
14
32
|
export declare function setColorSchemeSafe(mode: ThemeMode): void;
|
|
15
33
|
//# sourceMappingURL=set-color-scheme-safe.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"set-color-scheme-safe.d.ts","sourceRoot":"","sources":["../../../../src/theme/set-color-scheme-safe.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEzC
|
|
1
|
+
{"version":3,"file":"set-color-scheme-safe.d.ts","sourceRoot":"","sources":["../../../../src/theme/set-color-scheme-safe.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEzC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,SAAS,QAkBjD"}
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { Text } from 'react-native';
|
|
2
|
+
import { Text, type TextProps } from 'react-native';
|
|
3
3
|
import { render } from '@testing-library/react-native';
|
|
4
4
|
|
|
5
5
|
type UseFontsResult = readonly [boolean, Error | null];
|
|
@@ -16,9 +16,22 @@ function setUseFontsResult(result: UseFontsResult): void {
|
|
|
16
16
|
mockUseFontsResult = result;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
type TextWithDefaults = typeof Text & {
|
|
20
|
+
defaultProps?: Partial<TextProps>;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function resetTextDefaults(): void {
|
|
24
|
+
(Text as TextWithDefaults).defaultProps = undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
19
27
|
describe('FontLoader (native)', () => {
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
resetTextDefaults();
|
|
30
|
+
});
|
|
31
|
+
|
|
20
32
|
afterEach(() => {
|
|
21
33
|
setUseFontsResult([true, null]);
|
|
34
|
+
resetTextDefaults();
|
|
22
35
|
});
|
|
23
36
|
|
|
24
37
|
it('renders children when fonts are loaded and enabled', () => {
|
|
@@ -72,4 +85,62 @@ describe('FontLoader (native)', () => {
|
|
|
72
85
|
);
|
|
73
86
|
expect(getByText('content')).toBeTruthy();
|
|
74
87
|
});
|
|
88
|
+
|
|
89
|
+
it('prepends Inter to Text.defaultProps.style once fonts are loaded', () => {
|
|
90
|
+
setUseFontsResult([true, null]);
|
|
91
|
+
render(
|
|
92
|
+
<FontLoader enabled>
|
|
93
|
+
<Text>content</Text>
|
|
94
|
+
</FontLoader>,
|
|
95
|
+
);
|
|
96
|
+
const defaults = (Text as TextWithDefaults).defaultProps;
|
|
97
|
+
expect(defaults?.style).toEqual({ fontFamily: 'Inter' });
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('does not touch Text.defaultProps until fonts are loaded', () => {
|
|
101
|
+
setUseFontsResult([false, null]);
|
|
102
|
+
render(
|
|
103
|
+
<FontLoader enabled>
|
|
104
|
+
<Text>content</Text>
|
|
105
|
+
</FontLoader>,
|
|
106
|
+
);
|
|
107
|
+
expect((Text as TextWithDefaults).defaultProps?.style).toBeUndefined();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('preserves a pre-existing default style by appending it after Inter', () => {
|
|
111
|
+
const preExisting = { color: '#abcdef' } as const;
|
|
112
|
+
(Text as TextWithDefaults).defaultProps = { style: preExisting };
|
|
113
|
+
setUseFontsResult([true, null]);
|
|
114
|
+
render(
|
|
115
|
+
<FontLoader enabled>
|
|
116
|
+
<Text>content</Text>
|
|
117
|
+
</FontLoader>,
|
|
118
|
+
);
|
|
119
|
+
const style = (Text as TextWithDefaults).defaultProps?.style;
|
|
120
|
+
expect(Array.isArray(style)).toBe(true);
|
|
121
|
+
if (Array.isArray(style)) {
|
|
122
|
+
expect(style[0]).toEqual({ fontFamily: 'Inter' });
|
|
123
|
+
expect(style[1]).toBe(preExisting);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('is idempotent across multiple FontLoader mounts', () => {
|
|
128
|
+
setUseFontsResult([true, null]);
|
|
129
|
+
const first = render(
|
|
130
|
+
<FontLoader enabled>
|
|
131
|
+
<Text>first</Text>
|
|
132
|
+
</FontLoader>,
|
|
133
|
+
);
|
|
134
|
+
first.unmount();
|
|
135
|
+
const second = render(
|
|
136
|
+
<FontLoader enabled>
|
|
137
|
+
<Text>second</Text>
|
|
138
|
+
</FontLoader>,
|
|
139
|
+
);
|
|
140
|
+
second.unmount();
|
|
141
|
+
const style = (Text as TextWithDefaults).defaultProps?.style;
|
|
142
|
+
// Should still be the single, plain Inter object — not nested arrays
|
|
143
|
+
// or duplicated entries.
|
|
144
|
+
expect(style).toEqual({ fontFamily: 'Inter' });
|
|
145
|
+
});
|
|
75
146
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useRef } from 'react';
|
|
2
|
+
import { Text, type TextProps, type TextStyle, type StyleProp } from 'react-native';
|
|
2
3
|
import { useFonts } from 'expo-font';
|
|
3
4
|
import { FONT_ASSETS } from './font-assets';
|
|
4
5
|
|
|
@@ -14,6 +15,61 @@ export interface FontLoaderProps {
|
|
|
14
15
|
children: React.ReactNode;
|
|
15
16
|
}
|
|
16
17
|
|
|
18
|
+
/**
|
|
19
|
+
* React Native's `Text` exposes a static `defaultProps` for app-wide prop
|
|
20
|
+
* defaults. The public type definition omits it (it's an implementation
|
|
21
|
+
* detail, not part of the stable API), so we describe it locally rather
|
|
22
|
+
* than casting to `any`.
|
|
23
|
+
*/
|
|
24
|
+
type TextWithDefaults = typeof Text & {
|
|
25
|
+
defaultProps?: Partial<TextProps>;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Mutates `Text.defaultProps.style` so every native `<Text>` in the consuming
|
|
30
|
+
* app inherits Bloom's sans font (Inter). The Bloom family name is prepended
|
|
31
|
+
* to any existing default style so caller-provided `style` (which React
|
|
32
|
+
* applies AFTER `defaultProps.style`) still wins for overrides.
|
|
33
|
+
*
|
|
34
|
+
* Idempotent: re-invoking it after the first apply is a no-op because the
|
|
35
|
+
* first style entry will already be the Bloom default we added.
|
|
36
|
+
*/
|
|
37
|
+
function applyDefaultTextFont(): void {
|
|
38
|
+
const TextWithDefaults = Text as TextWithDefaults;
|
|
39
|
+
const existing = TextWithDefaults.defaultProps?.style;
|
|
40
|
+
const bloomDefault: TextStyle = { fontFamily: 'Inter' };
|
|
41
|
+
|
|
42
|
+
// Detect whether we've already prepended ourselves. `existing` may be an
|
|
43
|
+
// array (multiple style fragments), a single object, a registered style
|
|
44
|
+
// number, or undefined. Only the array/object cases need inspection — if
|
|
45
|
+
// it's a number, we can't read the contents so we just prepend.
|
|
46
|
+
if (Array.isArray(existing)) {
|
|
47
|
+
const first = existing[0];
|
|
48
|
+
if (
|
|
49
|
+
first &&
|
|
50
|
+
typeof first === 'object' &&
|
|
51
|
+
!Array.isArray(first) &&
|
|
52
|
+
(first as TextStyle).fontFamily === bloomDefault.fontFamily
|
|
53
|
+
) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
} else if (
|
|
57
|
+
existing &&
|
|
58
|
+
typeof existing === 'object' &&
|
|
59
|
+
(existing as TextStyle).fontFamily === bloomDefault.fontFamily
|
|
60
|
+
) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const nextStyle: StyleProp<TextStyle> =
|
|
65
|
+
existing == null ? bloomDefault : [bloomDefault, existing];
|
|
66
|
+
|
|
67
|
+
TextWithDefaults.defaultProps = {
|
|
68
|
+
...TextWithDefaults.defaultProps,
|
|
69
|
+
style: nextStyle,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
17
73
|
/**
|
|
18
74
|
* Native font loader. Calls `expo-font`'s `useFonts` with the Bloom font
|
|
19
75
|
* asset map and gates `children` on the load result. The hook is invoked
|
|
@@ -21,9 +77,26 @@ export interface FontLoaderProps {
|
|
|
21
77
|
* fonts get loaded. This keeps Hook order stable across renders and means
|
|
22
78
|
* `<BloomThemeProvider fonts={false}>` still gets fonts pre-loaded if the
|
|
23
79
|
* provider tree ever flips `fonts` back on.
|
|
80
|
+
*
|
|
81
|
+
* Once the bundled fonts report `loaded === true`, this component also
|
|
82
|
+
* mutates `Text.defaultProps.style` to prepend `{ fontFamily: 'Inter' }`
|
|
83
|
+
* so every native `<Text>` in the consuming app inherits Bloom's sans
|
|
84
|
+
* family without callers needing to import `<Text>` from `@oxyhq/bloom`.
|
|
85
|
+
* The mutation is gated by a `useRef` so it runs at most once per
|
|
86
|
+
* FontLoader instance, and the underlying apply is idempotent — the
|
|
87
|
+
* Bloom default is prepended to any caller `defaultProps.style`, so
|
|
88
|
+
* per-call `style` overrides (which React applies after `defaultProps`)
|
|
89
|
+
* still take precedence.
|
|
24
90
|
*/
|
|
25
91
|
export function FontLoader({ enabled, fallback, children }: FontLoaderProps) {
|
|
26
92
|
const [loaded] = useFonts(FONT_ASSETS);
|
|
93
|
+
const defaultsApplied = useRef(false);
|
|
94
|
+
|
|
95
|
+
if (loaded && !defaultsApplied.current) {
|
|
96
|
+
defaultsApplied.current = true;
|
|
97
|
+
applyDefaultTextFont();
|
|
98
|
+
}
|
|
99
|
+
|
|
27
100
|
if (!enabled) return <>{children}</>;
|
|
28
101
|
if (!loaded) return <>{fallback ?? null}</>;
|
|
29
102
|
return <>{children}</>;
|
|
@@ -3,15 +3,33 @@ import type { ThemeMode } from './types';
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Safely set the color scheme via Appearance API.
|
|
6
|
-
* On Android (RN 0.83+), Appearance.setColorScheme has a Kotlin non-null
|
|
7
|
-
* annotation on `style`. Passing null for 'system' crashes.
|
|
8
|
-
* Workaround: resolve the system preference and pass 'light'/'dark' instead.
|
|
9
6
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
7
|
+
* Behavior by mode:
|
|
8
|
+
* - 'light' / 'dark': set the explicit override.
|
|
9
|
+
* - 'system' / 'adaptive': leave the OS in control. We must NOT call
|
|
10
|
+
* Appearance.setColorScheme(resolved) here — doing so installs an
|
|
11
|
+
* app-level override that masks the OS preference. Once that override
|
|
12
|
+
* is set, useColorScheme() / Appearance.getColorScheme() return the
|
|
13
|
+
* frozen override instead of the live OS value, and the app stops
|
|
14
|
+
* following dark↔light OS toggles until a cold restart.
|
|
15
|
+
*
|
|
16
|
+
* On iOS we additionally pass 'unspecified' to clear any prior
|
|
17
|
+
* override that may have been installed by a previous explicit mode.
|
|
18
|
+
* 'unspecified' is the documented sentinel that tells the native
|
|
19
|
+
* Appearance module to fall back to the OS preference. (RN's JS
|
|
20
|
+
* implementation forwards this straight through to the native
|
|
21
|
+
* bridge; on iOS this clears the override.)
|
|
22
|
+
*
|
|
23
|
+
* On Android (RN 0.83+) the native Kotlin signature has @NonNull on
|
|
24
|
+
* `style` and rejects null, and 'unspecified' is not honored as a
|
|
25
|
+
* clear-override sentinel on Android either. As a result, if a user
|
|
26
|
+
* previously selected 'light' or 'dark' and then switches back to
|
|
27
|
+
* 'system' on Android, the override remains until the next cold
|
|
28
|
+
* restart. Users who never explicitly overrode are unaffected because
|
|
29
|
+
* we never install an override in system mode in the first place.
|
|
30
|
+
*
|
|
31
|
+
* On react-native-web, Appearance.setColorScheme is not implemented at
|
|
32
|
+
* all; the browser controls the color scheme, so we bail out on web.
|
|
15
33
|
*/
|
|
16
34
|
export function setColorSchemeSafe(mode: ThemeMode) {
|
|
17
35
|
if (Platform.OS === 'web') {
|
|
@@ -19,10 +37,16 @@ export function setColorSchemeSafe(mode: ThemeMode) {
|
|
|
19
37
|
}
|
|
20
38
|
|
|
21
39
|
const effectiveMode = mode === 'adaptive' ? 'system' : mode;
|
|
40
|
+
|
|
22
41
|
if (effectiveMode === 'system') {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
42
|
+
// Clear any prior app-level override so useColorScheme() tracks the
|
|
43
|
+
// OS. iOS honors 'unspecified' as a sentinel to fall back to the
|
|
44
|
+
// system preference; Android does not (see note above).
|
|
45
|
+
if (Platform.OS === 'ios') {
|
|
46
|
+
Appearance.setColorScheme('unspecified');
|
|
47
|
+
}
|
|
48
|
+
return;
|
|
27
49
|
}
|
|
50
|
+
|
|
51
|
+
Appearance.setColorScheme(effectiveMode);
|
|
28
52
|
}
|