@oxyhq/bloom 0.55.0 → 0.56.0

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 (30) hide show
  1. package/README.md +32 -1
  2. package/lib/commonjs/provider/index.js +67 -0
  3. package/lib/commonjs/provider/index.js.map +1 -0
  4. package/lib/commonjs/provider/scroll-provider.js +13 -0
  5. package/lib/commonjs/provider/scroll-provider.js.map +1 -0
  6. package/lib/commonjs/provider/scroll-provider.web.js +13 -0
  7. package/lib/commonjs/provider/scroll-provider.web.js.map +1 -0
  8. package/lib/module/provider/index.js +63 -0
  9. package/lib/module/provider/index.js.map +1 -0
  10. package/lib/module/provider/scroll-provider.js +16 -0
  11. package/lib/module/provider/scroll-provider.js.map +1 -0
  12. package/lib/module/provider/scroll-provider.web.js +5 -0
  13. package/lib/module/provider/scroll-provider.web.js.map +1 -0
  14. package/lib/typescript/commonjs/provider/index.d.ts +45 -0
  15. package/lib/typescript/commonjs/provider/index.d.ts.map +1 -0
  16. package/lib/typescript/commonjs/provider/scroll-provider.d.ts +14 -0
  17. package/lib/typescript/commonjs/provider/scroll-provider.d.ts.map +1 -0
  18. package/lib/typescript/commonjs/provider/scroll-provider.web.d.ts +3 -0
  19. package/lib/typescript/commonjs/provider/scroll-provider.web.d.ts.map +1 -0
  20. package/lib/typescript/module/provider/index.d.ts +45 -0
  21. package/lib/typescript/module/provider/index.d.ts.map +1 -0
  22. package/lib/typescript/module/provider/scroll-provider.d.ts +14 -0
  23. package/lib/typescript/module/provider/scroll-provider.d.ts.map +1 -0
  24. package/lib/typescript/module/provider/scroll-provider.web.d.ts +3 -0
  25. package/lib/typescript/module/provider/scroll-provider.web.d.ts.map +1 -0
  26. package/package.json +12 -1
  27. package/src/__tests__/BloomProvider.web.test.tsx +114 -0
  28. package/src/provider/index.tsx +68 -0
  29. package/src/provider/scroll-provider.ts +13 -0
  30. package/src/provider/scroll-provider.web.ts +2 -0
package/README.md CHANGED
@@ -24,9 +24,40 @@ Also required:
24
24
 
25
25
  ## Usage
26
26
 
27
+ ### App root
28
+
29
+ Mount `BloomProvider` once, at the very top of the app. It composes every piece of
30
+ app-wide Bloom state — theme, haptics, image resolution, scroll restoration and the
31
+ tab-bar minimize progress — so none of them can end up at the wrong depth. It takes
32
+ all of `BloomThemeProvider`'s props plus `imageResolver` and `haptics`.
33
+
34
+ ```tsx
35
+ import { BloomProvider } from '@oxyhq/bloom/provider';
36
+
37
+ <BloomProvider
38
+ defaultMode="system"
39
+ defaultColorPreset="blue"
40
+ persistKey="app.theme"
41
+ storage={storage}
42
+ imageResolver={(id, variant) => oxyServices.getFileDownloadUrl(id, variant)}
43
+ >
44
+ <App />
45
+ </BloomProvider>
46
+ ```
47
+
48
+ Everything scrollable must be **under** it: on web `useScrollRestoration()` throws
49
+ outside its provider, so a list rendered beside the root (a right rail, an overlay)
50
+ crashes the screen.
51
+
52
+ Outlets are **not** included — their position in the tree is a real app decision, and
53
+ a second mount duplicates every surface they render. Mount these yourself, under
54
+ `BloomProvider`: `<ToastOutlet>`, `<Portal.Provider>`/`<Portal.Outlet>`,
55
+ `<SurfaceHost>`, `<BloomDialogProvider>`, `<AlertDialogHost>`.
56
+
27
57
  ### Theme
28
58
 
29
- Wrap your app with `BloomThemeProvider`. It accepts controlled `mode` and `colorPreset` props — persist them however you like (AsyncStorage, Zustand, etc.).
59
+ `BloomProvider` already mounts `BloomThemeProvider`; mount it directly only when you
60
+ need a nested/scoped theme. It accepts controlled `mode` and `colorPreset` props — persist them however you like (AsyncStorage, Zustand, etc.).
30
61
 
31
62
  ```tsx
32
63
  import { BloomThemeProvider } from '@oxyhq/bloom/theme';
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.BloomProvider = BloomProvider;
7
+ var _index = require("../image-resolver/index.js");
8
+ var _useHaptics = require("../hooks/useHaptics.js");
9
+ var _minimizeContext = require("../tab-bar/minimize-context.js");
10
+ var _index2 = require("../theme/index.js");
11
+ var _scrollProvider = require("./scroll-provider");
12
+ var _jsxRuntime = require("react/jsx-runtime");
13
+ /**
14
+ * `BloomProvider` — the ONE Bloom root an app mounts.
15
+ *
16
+ * Bloom's app-wide state used to be a handful of separate providers that every
17
+ * consumer wired by hand (theme, haptics, image resolution, scroll restoration,
18
+ * tab-bar minimize progress). Mounting them separately means each one can end
19
+ * up at a different depth, and a provider mounted too low fails in ways that
20
+ * are hard to trace:
21
+ *
22
+ * - `useScrollRestoration()` THROWS on web outside `ScrollRestorationProvider`,
23
+ * so any scrollable rendered beside the provider (a right rail, an overlay)
24
+ * crashes the screen.
25
+ * - `useMinimizeState()` silently hands each caller a private fallback, so a
26
+ * tab bar below the provider just never minimizes — no error anywhere.
27
+ *
28
+ * Mounting this single provider at the app root makes both classes of mistake
29
+ * impossible: everything Bloom renders is under all of them, at the same depth.
30
+ * Nesting extra contexts costs nothing at runtime — the win is that scope is no
31
+ * longer a per-app decision.
32
+ *
33
+ * NOT included, on purpose — these are OUTLETS, not state, and their placement
34
+ * in the tree is a real app decision (z-order, safe areas, and mounting a
35
+ * second one duplicates every surface it renders):
36
+ * `<ToastOutlet>`, `<Portal.Provider>`/`<Portal.Outlet>`, `<SurfaceHost>`,
37
+ * `<BloomDialogProvider>`, `<AlertDialogHost>`.
38
+ */
39
+
40
+ function BloomProvider({
41
+ children,
42
+ imageResolver,
43
+ haptics = true,
44
+ ...themeProps
45
+ }) {
46
+ return (
47
+ /*#__PURE__*/
48
+ // `value` is passed unconditionally (null when unset) so toggling a resolver
49
+ // never changes the tree shape and remounts everything below it.
50
+ (0, _jsxRuntime.jsx)(_index.ImageResolverProvider, {
51
+ value: imageResolver ?? null,
52
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_index2.BloomThemeProvider, {
53
+ ...themeProps,
54
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_scrollProvider.ScrollRestorationProvider, {
55
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_useHaptics.BloomHapticsProvider, {
56
+ enabled: haptics,
57
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_minimizeContext.TabBarMinimizeProvider, {
58
+ children: children
59
+ })
60
+ })
61
+ })
62
+ })
63
+ })
64
+ );
65
+ }
66
+ BloomProvider.displayName = 'BloomProvider';
67
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["_index","require","_useHaptics","_minimizeContext","_index2","_scrollProvider","_jsxRuntime","BloomProvider","children","imageResolver","haptics","themeProps","jsx","ImageResolverProvider","value","BloomThemeProvider","ScrollRestorationProvider","BloomHapticsProvider","enabled","TabBarMinimizeProvider","displayName"],"sourceRoot":"../../../src","sources":["provider/index.tsx"],"mappings":";;;;;;AA4BA,IAAAA,MAAA,GAAAC,OAAA;AACA,IAAAC,WAAA,GAAAD,OAAA;AACA,IAAAE,gBAAA,GAAAF,OAAA;AACA,IAAAG,OAAA,GAAAH,OAAA;AACA,IAAAI,eAAA,GAAAJ,OAAA;AAA8D,IAAAK,WAAA,GAAAL,OAAA;AAhC9D;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;;AAqBO,SAASM,aAAaA,CAAC;EAC5BC,QAAQ;EACRC,aAAa;EACbC,OAAO,GAAG,IAAI;EACd,GAAGC;AACe,CAAC,EAAE;EACrB;IAAA;IACE;IACA;IACA,IAAAL,WAAA,CAAAM,GAAA,EAACZ,MAAA,CAAAa,qBAAqB;MAACC,KAAK,EAAEL,aAAa,IAAI,IAAK;MAAAD,QAAA,eAClD,IAAAF,WAAA,CAAAM,GAAA,EAACR,OAAA,CAAAW,kBAAkB;QAAA,GAAKJ,UAAU;QAAAH,QAAA,eAChC,IAAAF,WAAA,CAAAM,GAAA,EAACP,eAAA,CAAAW,yBAAyB;UAAAR,QAAA,eACxB,IAAAF,WAAA,CAAAM,GAAA,EAACV,WAAA,CAAAe,oBAAoB;YAACC,OAAO,EAAER,OAAQ;YAAAF,QAAA,eACrC,IAAAF,WAAA,CAAAM,GAAA,EAACT,gBAAA,CAAAgB,sBAAsB;cAAAX,QAAA,EAAEA;YAAQ,CAAyB;UAAC,CACvC;QAAC,CACE;MAAC,CACV;IAAC,CACA;EAAC;AAE5B;AAEAD,aAAa,CAACa,WAAW,GAAG,eAAe","ignoreList":[]}
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ Object.defineProperty(exports, "ScrollRestorationProvider", {
7
+ enumerable: true,
8
+ get: function () {
9
+ return _index.ScrollRestorationProvider;
10
+ }
11
+ });
12
+ var _index = require("../scroll/index.js");
13
+ //# sourceMappingURL=scroll-provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["_index","require"],"sourceRoot":"../../../src","sources":["provider/scroll-provider.ts"],"mappings":";;;;;;;;;;;AAYA,IAAAA,MAAA,GAAAC,OAAA","ignoreList":[]}
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ Object.defineProperty(exports, "ScrollRestorationProvider", {
7
+ enumerable: true,
8
+ get: function () {
9
+ return _indexWeb.ScrollRestorationProvider;
10
+ }
11
+ });
12
+ var _indexWeb = require("../scroll/index.web.js");
13
+ //# sourceMappingURL=scroll-provider.web.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["_indexWeb","require"],"sourceRoot":"../../../src","sources":["provider/scroll-provider.web.ts"],"mappings":";;;;;;;;;;;AACA,IAAAA,SAAA,GAAAC,OAAA","ignoreList":[]}
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * `BloomProvider` — the ONE Bloom root an app mounts.
5
+ *
6
+ * Bloom's app-wide state used to be a handful of separate providers that every
7
+ * consumer wired by hand (theme, haptics, image resolution, scroll restoration,
8
+ * tab-bar minimize progress). Mounting them separately means each one can end
9
+ * up at a different depth, and a provider mounted too low fails in ways that
10
+ * are hard to trace:
11
+ *
12
+ * - `useScrollRestoration()` THROWS on web outside `ScrollRestorationProvider`,
13
+ * so any scrollable rendered beside the provider (a right rail, an overlay)
14
+ * crashes the screen.
15
+ * - `useMinimizeState()` silently hands each caller a private fallback, so a
16
+ * tab bar below the provider just never minimizes — no error anywhere.
17
+ *
18
+ * Mounting this single provider at the app root makes both classes of mistake
19
+ * impossible: everything Bloom renders is under all of them, at the same depth.
20
+ * Nesting extra contexts costs nothing at runtime — the win is that scope is no
21
+ * longer a per-app decision.
22
+ *
23
+ * NOT included, on purpose — these are OUTLETS, not state, and their placement
24
+ * in the tree is a real app decision (z-order, safe areas, and mounting a
25
+ * second one duplicates every surface it renders):
26
+ * `<ToastOutlet>`, `<Portal.Provider>`/`<Portal.Outlet>`, `<SurfaceHost>`,
27
+ * `<BloomDialogProvider>`, `<AlertDialogHost>`.
28
+ */
29
+
30
+ import { ImageResolverProvider } from "../image-resolver/index.js";
31
+ import { BloomHapticsProvider } from "../hooks/useHaptics.js";
32
+ import { TabBarMinimizeProvider } from "../tab-bar/minimize-context.js";
33
+ import { BloomThemeProvider } from "../theme/index.js";
34
+ import { ScrollRestorationProvider } from './scroll-provider';
35
+ import { jsx as _jsx } from "react/jsx-runtime";
36
+ export function BloomProvider({
37
+ children,
38
+ imageResolver,
39
+ haptics = true,
40
+ ...themeProps
41
+ }) {
42
+ return (
43
+ /*#__PURE__*/
44
+ // `value` is passed unconditionally (null when unset) so toggling a resolver
45
+ // never changes the tree shape and remounts everything below it.
46
+ _jsx(ImageResolverProvider, {
47
+ value: imageResolver ?? null,
48
+ children: /*#__PURE__*/_jsx(BloomThemeProvider, {
49
+ ...themeProps,
50
+ children: /*#__PURE__*/_jsx(ScrollRestorationProvider, {
51
+ children: /*#__PURE__*/_jsx(BloomHapticsProvider, {
52
+ enabled: haptics,
53
+ children: /*#__PURE__*/_jsx(TabBarMinimizeProvider, {
54
+ children: children
55
+ })
56
+ })
57
+ })
58
+ })
59
+ })
60
+ );
61
+ }
62
+ BloomProvider.displayName = 'BloomProvider';
63
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["ImageResolverProvider","BloomHapticsProvider","TabBarMinimizeProvider","BloomThemeProvider","ScrollRestorationProvider","jsx","_jsx","BloomProvider","children","imageResolver","haptics","themeProps","value","enabled","displayName"],"sourceRoot":"../../../src","sources":["provider/index.tsx"],"mappings":";;AAAA;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;;AAGA,SAASA,qBAAqB,QAA4B,4BAAmB;AAC7E,SAASC,oBAAoB,QAAQ,wBAAqB;AAC1D,SAASC,sBAAsB,QAAQ,gCAA6B;AACpE,SAASC,kBAAkB,QAAsC,mBAAU;AAC3E,SAASC,yBAAyB,QAAQ,mBAAmB;AAAC,SAAAC,GAAA,IAAAC,IAAA;AAc9D,OAAO,SAASC,aAAaA,CAAC;EAC5BC,QAAQ;EACRC,aAAa;EACbC,OAAO,GAAG,IAAI;EACd,GAAGC;AACe,CAAC,EAAE;EACrB;IAAA;IACE;IACA;IACAL,IAAA,CAACN,qBAAqB;MAACY,KAAK,EAAEH,aAAa,IAAI,IAAK;MAAAD,QAAA,eAClDF,IAAA,CAACH,kBAAkB;QAAA,GAAKQ,UAAU;QAAAH,QAAA,eAChCF,IAAA,CAACF,yBAAyB;UAAAI,QAAA,eACxBF,IAAA,CAACL,oBAAoB;YAACY,OAAO,EAAEH,OAAQ;YAAAF,QAAA,eACrCF,IAAA,CAACJ,sBAAsB;cAAAM,QAAA,EAAEA;YAAQ,CAAyB;UAAC,CACvC;QAAC,CACE;MAAC,CACV;IAAC,CACA;EAAC;AAE5B;AAEAD,aAAa,CAACO,WAAW,GAAG,eAAe","ignoreList":[]}
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Native/default binding for the scroll-restoration provider used by
5
+ * {@link BloomProvider}.
6
+ *
7
+ * `@oxyhq/bloom/scroll` is web-forked, so its `browser` export condition hands
8
+ * web consumers the real implementation while native gets the no-op. A compiled
9
+ * `lib/module/provider/index.js` cannot benefit from that condition (it imports
10
+ * a relative path, not the package subpath), so the platform choice is made
11
+ * here by FILENAME instead: Metro picks `scroll-provider.web.ts` on web, and
12
+ * every web bundler that resolves `.web.js` picks the compiled sibling. Same
13
+ * mechanism the toast engine uses for `ToastHost.native.tsx`.
14
+ */
15
+ export { ScrollRestorationProvider } from "../scroll/index.js";
16
+ //# sourceMappingURL=scroll-provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["ScrollRestorationProvider"],"sourceRoot":"../../../src","sources":["provider/scroll-provider.ts"],"mappings":";;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAASA,yBAAyB,QAAQ,oBAAW","ignoreList":[]}
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+
3
+ /** Web binding for {@link BloomProvider}'s scroll-restoration provider — see `./scroll-provider.ts`. */
4
+ export { ScrollRestorationProvider } from "../scroll/index.web.js";
5
+ //# sourceMappingURL=scroll-provider.web.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["ScrollRestorationProvider"],"sourceRoot":"../../../src","sources":["provider/scroll-provider.web.ts"],"mappings":";;AAAA;AACA,SAASA,yBAAyB,QAAQ,wBAAqB","ignoreList":[]}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * `BloomProvider` — the ONE Bloom root an app mounts.
3
+ *
4
+ * Bloom's app-wide state used to be a handful of separate providers that every
5
+ * consumer wired by hand (theme, haptics, image resolution, scroll restoration,
6
+ * tab-bar minimize progress). Mounting them separately means each one can end
7
+ * up at a different depth, and a provider mounted too low fails in ways that
8
+ * are hard to trace:
9
+ *
10
+ * - `useScrollRestoration()` THROWS on web outside `ScrollRestorationProvider`,
11
+ * so any scrollable rendered beside the provider (a right rail, an overlay)
12
+ * crashes the screen.
13
+ * - `useMinimizeState()` silently hands each caller a private fallback, so a
14
+ * tab bar below the provider just never minimizes — no error anywhere.
15
+ *
16
+ * Mounting this single provider at the app root makes both classes of mistake
17
+ * impossible: everything Bloom renders is under all of them, at the same depth.
18
+ * Nesting extra contexts costs nothing at runtime — the win is that scope is no
19
+ * longer a per-app decision.
20
+ *
21
+ * NOT included, on purpose — these are OUTLETS, not state, and their placement
22
+ * in the tree is a real app decision (z-order, safe areas, and mounting a
23
+ * second one duplicates every surface it renders):
24
+ * `<ToastOutlet>`, `<Portal.Provider>`/`<Portal.Outlet>`, `<SurfaceHost>`,
25
+ * `<BloomDialogProvider>`, `<AlertDialogHost>`.
26
+ */
27
+ import type { ReactNode } from 'react';
28
+ import { type ImageResolver } from '../image-resolver';
29
+ import { type BloomThemeProviderProps } from '../theme';
30
+ export interface BloomProviderProps extends Omit<BloomThemeProviderProps, 'children'> {
31
+ children: ReactNode;
32
+ /**
33
+ * Resolves bare media identifiers (Oxy file ids) to loadable URLs for every
34
+ * Bloom surface that takes a `source` — `<Avatar>`, image galleries, cards.
35
+ * Typically `(id, variant) => oxyServices.getFileDownloadUrl(id, variant)`.
36
+ */
37
+ imageResolver?: ImageResolver;
38
+ /** `false` disables haptic feedback app-wide (honored by every `useHaptics()` call). */
39
+ haptics?: boolean;
40
+ }
41
+ export declare function BloomProvider({ children, imageResolver, haptics, ...themeProps }: BloomProviderProps): import("react/jsx-runtime").JSX.Element;
42
+ export declare namespace BloomProvider {
43
+ var displayName: string;
44
+ }
45
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/provider/index.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,OAAO,EAAyB,KAAK,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAG9E,OAAO,EAAsB,KAAK,uBAAuB,EAAE,MAAM,UAAU,CAAC;AAG5E,MAAM,WAAW,kBAAmB,SAAQ,IAAI,CAAC,uBAAuB,EAAE,UAAU,CAAC;IACnF,QAAQ,EAAE,SAAS,CAAC;IACpB;;;;OAIG;IACH,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,wFAAwF;IACxF,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,wBAAgB,aAAa,CAAC,EAC5B,QAAQ,EACR,aAAa,EACb,OAAc,EACd,GAAG,UAAU,EACd,EAAE,kBAAkB,2CAcpB;yBAnBe,aAAa"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Native/default binding for the scroll-restoration provider used by
3
+ * {@link BloomProvider}.
4
+ *
5
+ * `@oxyhq/bloom/scroll` is web-forked, so its `browser` export condition hands
6
+ * web consumers the real implementation while native gets the no-op. A compiled
7
+ * `lib/module/provider/index.js` cannot benefit from that condition (it imports
8
+ * a relative path, not the package subpath), so the platform choice is made
9
+ * here by FILENAME instead: Metro picks `scroll-provider.web.ts` on web, and
10
+ * every web bundler that resolves `.web.js` picks the compiled sibling. Same
11
+ * mechanism the toast engine uses for `ToastHost.native.tsx`.
12
+ */
13
+ export { ScrollRestorationProvider } from '../scroll';
14
+ //# sourceMappingURL=scroll-provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scroll-provider.d.ts","sourceRoot":"","sources":["../../../../src/provider/scroll-provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,yBAAyB,EAAE,MAAM,WAAW,CAAC"}
@@ -0,0 +1,3 @@
1
+ /** Web binding for {@link BloomProvider}'s scroll-restoration provider — see `./scroll-provider.ts`. */
2
+ export { ScrollRestorationProvider } from '../scroll/index.web';
3
+ //# sourceMappingURL=scroll-provider.web.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scroll-provider.web.d.ts","sourceRoot":"","sources":["../../../../src/provider/scroll-provider.web.ts"],"names":[],"mappings":"AAAA,wGAAwG;AACxG,OAAO,EAAE,yBAAyB,EAAE,MAAM,qBAAqB,CAAC"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * `BloomProvider` — the ONE Bloom root an app mounts.
3
+ *
4
+ * Bloom's app-wide state used to be a handful of separate providers that every
5
+ * consumer wired by hand (theme, haptics, image resolution, scroll restoration,
6
+ * tab-bar minimize progress). Mounting them separately means each one can end
7
+ * up at a different depth, and a provider mounted too low fails in ways that
8
+ * are hard to trace:
9
+ *
10
+ * - `useScrollRestoration()` THROWS on web outside `ScrollRestorationProvider`,
11
+ * so any scrollable rendered beside the provider (a right rail, an overlay)
12
+ * crashes the screen.
13
+ * - `useMinimizeState()` silently hands each caller a private fallback, so a
14
+ * tab bar below the provider just never minimizes — no error anywhere.
15
+ *
16
+ * Mounting this single provider at the app root makes both classes of mistake
17
+ * impossible: everything Bloom renders is under all of them, at the same depth.
18
+ * Nesting extra contexts costs nothing at runtime — the win is that scope is no
19
+ * longer a per-app decision.
20
+ *
21
+ * NOT included, on purpose — these are OUTLETS, not state, and their placement
22
+ * in the tree is a real app decision (z-order, safe areas, and mounting a
23
+ * second one duplicates every surface it renders):
24
+ * `<ToastOutlet>`, `<Portal.Provider>`/`<Portal.Outlet>`, `<SurfaceHost>`,
25
+ * `<BloomDialogProvider>`, `<AlertDialogHost>`.
26
+ */
27
+ import type { ReactNode } from 'react';
28
+ import { type ImageResolver } from '../image-resolver';
29
+ import { type BloomThemeProviderProps } from '../theme';
30
+ export interface BloomProviderProps extends Omit<BloomThemeProviderProps, 'children'> {
31
+ children: ReactNode;
32
+ /**
33
+ * Resolves bare media identifiers (Oxy file ids) to loadable URLs for every
34
+ * Bloom surface that takes a `source` — `<Avatar>`, image galleries, cards.
35
+ * Typically `(id, variant) => oxyServices.getFileDownloadUrl(id, variant)`.
36
+ */
37
+ imageResolver?: ImageResolver;
38
+ /** `false` disables haptic feedback app-wide (honored by every `useHaptics()` call). */
39
+ haptics?: boolean;
40
+ }
41
+ export declare function BloomProvider({ children, imageResolver, haptics, ...themeProps }: BloomProviderProps): import("react/jsx-runtime").JSX.Element;
42
+ export declare namespace BloomProvider {
43
+ var displayName: string;
44
+ }
45
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/provider/index.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,OAAO,EAAyB,KAAK,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAG9E,OAAO,EAAsB,KAAK,uBAAuB,EAAE,MAAM,UAAU,CAAC;AAG5E,MAAM,WAAW,kBAAmB,SAAQ,IAAI,CAAC,uBAAuB,EAAE,UAAU,CAAC;IACnF,QAAQ,EAAE,SAAS,CAAC;IACpB;;;;OAIG;IACH,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,wFAAwF;IACxF,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,wBAAgB,aAAa,CAAC,EAC5B,QAAQ,EACR,aAAa,EACb,OAAc,EACd,GAAG,UAAU,EACd,EAAE,kBAAkB,2CAcpB;yBAnBe,aAAa"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Native/default binding for the scroll-restoration provider used by
3
+ * {@link BloomProvider}.
4
+ *
5
+ * `@oxyhq/bloom/scroll` is web-forked, so its `browser` export condition hands
6
+ * web consumers the real implementation while native gets the no-op. A compiled
7
+ * `lib/module/provider/index.js` cannot benefit from that condition (it imports
8
+ * a relative path, not the package subpath), so the platform choice is made
9
+ * here by FILENAME instead: Metro picks `scroll-provider.web.ts` on web, and
10
+ * every web bundler that resolves `.web.js` picks the compiled sibling. Same
11
+ * mechanism the toast engine uses for `ToastHost.native.tsx`.
12
+ */
13
+ export { ScrollRestorationProvider } from '../scroll';
14
+ //# sourceMappingURL=scroll-provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scroll-provider.d.ts","sourceRoot":"","sources":["../../../../src/provider/scroll-provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,yBAAyB,EAAE,MAAM,WAAW,CAAC"}
@@ -0,0 +1,3 @@
1
+ /** Web binding for {@link BloomProvider}'s scroll-restoration provider — see `./scroll-provider.ts`. */
2
+ export { ScrollRestorationProvider } from '../scroll/index.web';
3
+ //# sourceMappingURL=scroll-provider.web.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scroll-provider.web.d.ts","sourceRoot":"","sources":["../../../../src/provider/scroll-provider.web.ts"],"names":[],"mappings":"AAAA,wGAAwG;AACxG,OAAO,EAAE,yBAAyB,EAAE,MAAM,qBAAqB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxyhq/bloom",
3
- "version": "0.55.0",
3
+ "version": "0.56.0",
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",
@@ -28,6 +28,17 @@
28
28
  "default": "./lib/commonjs/index.js"
29
29
  }
30
30
  },
31
+ "./provider": {
32
+ "react-native": "./src/provider/index.tsx",
33
+ "import": {
34
+ "types": "./lib/typescript/module/provider/index.d.ts",
35
+ "default": "./lib/module/provider/index.js"
36
+ },
37
+ "require": {
38
+ "types": "./lib/typescript/commonjs/provider/index.d.ts",
39
+ "default": "./lib/commonjs/provider/index.js"
40
+ }
41
+ },
31
42
  "./surfaces": {
32
43
  "react-native": "./src/surfaces/index.ts",
33
44
  "browser": {
@@ -0,0 +1,114 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
4
+
5
+ // `BloomProvider` exists so an app mounts ONE Bloom root instead of five, and
6
+ // the concrete bug that motivated it is web-only: `useScrollRestoration()`
7
+ // THROWS outside `ScrollRestorationProvider`, so anything scrollable that an
8
+ // app renders beside — rather than under — that provider crashes the screen.
9
+ //
10
+ // These tests therefore assert the WEB binding: `provider/scroll-provider` is
11
+ // mapped to the web implementation the same way a web bundler resolves the
12
+ // `.web` sibling (jest has no platform-extension resolution of its own, so
13
+ // without this mapping the suite would silently exercise the native no-op and
14
+ // pass no matter what).
15
+
16
+ import { createElement, type ReactNode } from 'react';
17
+ import { act } from 'react';
18
+ import { createRoot, type Root } from 'react-dom/client';
19
+
20
+ (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT =
21
+ true;
22
+
23
+ // Only the two navigation hooks `scroll/index.web` consumes; `virtual` keeps
24
+ // the real (native-heavy) expo-router off the resolver.
25
+ jest.mock(
26
+ 'expo-router',
27
+ () => {
28
+ const react = jest.requireActual<typeof import('react')>('react');
29
+ return {
30
+ useFocusEffect: (effect: () => undefined | (() => void)) => {
31
+ react.useEffect(effect, [effect]);
32
+ },
33
+ useRoute: () => ({ key: 'route-test', name: 'Test', params: {} }),
34
+ };
35
+ },
36
+ { virtual: true },
37
+ );
38
+
39
+ // What a web bundler does with `provider/scroll-provider.web.ts`.
40
+ jest.mock('../provider/scroll-provider', () => ({
41
+ ScrollRestorationProvider:
42
+ jest.requireActual<typeof import('../scroll/index.web')>('../scroll/index.web')
43
+ .ScrollRestorationProvider,
44
+ }));
45
+
46
+ // Imported AFTER the mocks are registered.
47
+ import { BloomProvider } from '../provider';
48
+ import { useImageResolver } from '../image-resolver';
49
+ import { useScrollRestoration } from '../scroll/index.web';
50
+
51
+ /**
52
+ * Stands in for any Bloom-consuming scrollable an app renders under the root —
53
+ * a feed, a right-rail replies list, an overlay.
54
+ */
55
+ function Scrollable({ onResolve }: { onResolve?: (url: string | undefined) => void }): ReactNode {
56
+ useScrollRestoration('window');
57
+ const resolver = useImageResolver();
58
+ onResolve?.(resolver?.('file-id', 'avatar'));
59
+ return null;
60
+ }
61
+
62
+ function render(node: ReactNode): { root: Root; container: HTMLElement } {
63
+ const container = document.createElement('div');
64
+ document.body.appendChild(container);
65
+ const root = createRoot(container);
66
+ act(() => {
67
+ root.render(node);
68
+ });
69
+ return { root, container };
70
+ }
71
+
72
+ function unmount({ root, container }: { root: Root; container: HTMLElement }): void {
73
+ act(() => {
74
+ root.unmount();
75
+ });
76
+ container.remove();
77
+ }
78
+
79
+ describe('BloomProvider (web)', () => {
80
+ it('provides scroll restoration to everything below it', () => {
81
+ let resolved: string | undefined;
82
+ let mounted: { root: Root; container: HTMLElement } | undefined;
83
+
84
+ expect(() => {
85
+ mounted = render(
86
+ createElement(BloomProvider, {
87
+ fonts: false,
88
+ imageResolver: (id: string, variant?: string) => `${id}:${variant}`,
89
+ children: createElement(Scrollable, { onResolve: (url) => { resolved = url; } }),
90
+ }),
91
+ );
92
+ }).not.toThrow();
93
+
94
+ // The image resolver reaches the same subtree — one root, every context.
95
+ expect(resolved).toBe('file-id:avatar');
96
+
97
+ if (mounted) unmount(mounted);
98
+ });
99
+
100
+ // Vacuity guard: proves the assertion above is actually load-bearing. If the
101
+ // provider ever stops mounting the WEB scroll provider (a bad platform
102
+ // binding, a dropped nesting level) this control case is what makes the test
103
+ // above meaningful — remove the provider and the very same child throws.
104
+ it('is what keeps the hook from throwing — the same child crashes without it', () => {
105
+ const consoleError = jest.spyOn(console, 'error').mockImplementation(() => undefined);
106
+ try {
107
+ expect(() => render(createElement(Scrollable, {}))).toThrow(
108
+ /useScrollRestoration must be used within a <ScrollRestorationProvider>/,
109
+ );
110
+ } finally {
111
+ consoleError.mockRestore();
112
+ }
113
+ });
114
+ });
@@ -0,0 +1,68 @@
1
+ /**
2
+ * `BloomProvider` — the ONE Bloom root an app mounts.
3
+ *
4
+ * Bloom's app-wide state used to be a handful of separate providers that every
5
+ * consumer wired by hand (theme, haptics, image resolution, scroll restoration,
6
+ * tab-bar minimize progress). Mounting them separately means each one can end
7
+ * up at a different depth, and a provider mounted too low fails in ways that
8
+ * are hard to trace:
9
+ *
10
+ * - `useScrollRestoration()` THROWS on web outside `ScrollRestorationProvider`,
11
+ * so any scrollable rendered beside the provider (a right rail, an overlay)
12
+ * crashes the screen.
13
+ * - `useMinimizeState()` silently hands each caller a private fallback, so a
14
+ * tab bar below the provider just never minimizes — no error anywhere.
15
+ *
16
+ * Mounting this single provider at the app root makes both classes of mistake
17
+ * impossible: everything Bloom renders is under all of them, at the same depth.
18
+ * Nesting extra contexts costs nothing at runtime — the win is that scope is no
19
+ * longer a per-app decision.
20
+ *
21
+ * NOT included, on purpose — these are OUTLETS, not state, and their placement
22
+ * in the tree is a real app decision (z-order, safe areas, and mounting a
23
+ * second one duplicates every surface it renders):
24
+ * `<ToastOutlet>`, `<Portal.Provider>`/`<Portal.Outlet>`, `<SurfaceHost>`,
25
+ * `<BloomDialogProvider>`, `<AlertDialogHost>`.
26
+ */
27
+ import type { ReactNode } from 'react';
28
+
29
+ import { ImageResolverProvider, type ImageResolver } from '../image-resolver';
30
+ import { BloomHapticsProvider } from '../hooks/useHaptics';
31
+ import { TabBarMinimizeProvider } from '../tab-bar/minimize-context';
32
+ import { BloomThemeProvider, type BloomThemeProviderProps } from '../theme';
33
+ import { ScrollRestorationProvider } from './scroll-provider';
34
+
35
+ export interface BloomProviderProps extends Omit<BloomThemeProviderProps, 'children'> {
36
+ children: ReactNode;
37
+ /**
38
+ * Resolves bare media identifiers (Oxy file ids) to loadable URLs for every
39
+ * Bloom surface that takes a `source` — `<Avatar>`, image galleries, cards.
40
+ * Typically `(id, variant) => oxyServices.getFileDownloadUrl(id, variant)`.
41
+ */
42
+ imageResolver?: ImageResolver;
43
+ /** `false` disables haptic feedback app-wide (honored by every `useHaptics()` call). */
44
+ haptics?: boolean;
45
+ }
46
+
47
+ export function BloomProvider({
48
+ children,
49
+ imageResolver,
50
+ haptics = true,
51
+ ...themeProps
52
+ }: BloomProviderProps) {
53
+ return (
54
+ // `value` is passed unconditionally (null when unset) so toggling a resolver
55
+ // never changes the tree shape and remounts everything below it.
56
+ <ImageResolverProvider value={imageResolver ?? null}>
57
+ <BloomThemeProvider {...themeProps}>
58
+ <ScrollRestorationProvider>
59
+ <BloomHapticsProvider enabled={haptics}>
60
+ <TabBarMinimizeProvider>{children}</TabBarMinimizeProvider>
61
+ </BloomHapticsProvider>
62
+ </ScrollRestorationProvider>
63
+ </BloomThemeProvider>
64
+ </ImageResolverProvider>
65
+ );
66
+ }
67
+
68
+ BloomProvider.displayName = 'BloomProvider';
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Native/default binding for the scroll-restoration provider used by
3
+ * {@link BloomProvider}.
4
+ *
5
+ * `@oxyhq/bloom/scroll` is web-forked, so its `browser` export condition hands
6
+ * web consumers the real implementation while native gets the no-op. A compiled
7
+ * `lib/module/provider/index.js` cannot benefit from that condition (it imports
8
+ * a relative path, not the package subpath), so the platform choice is made
9
+ * here by FILENAME instead: Metro picks `scroll-provider.web.ts` on web, and
10
+ * every web bundler that resolves `.web.js` picks the compiled sibling. Same
11
+ * mechanism the toast engine uses for `ToastHost.native.tsx`.
12
+ */
13
+ export { ScrollRestorationProvider } from '../scroll';
@@ -0,0 +1,2 @@
1
+ /** Web binding for {@link BloomProvider}'s scroll-restoration provider — see `./scroll-provider.ts`. */
2
+ export { ScrollRestorationProvider } from '../scroll/index.web';