@oxyhq/bloom 0.44.1 → 0.46.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 (137) hide show
  1. package/lib/commonjs/dialog/Dialog.js +12 -3
  2. package/lib/commonjs/dialog/Dialog.js.map +1 -1
  3. package/lib/commonjs/dialog/Dialog.web.js +23 -8
  4. package/lib/commonjs/dialog/Dialog.web.js.map +1 -1
  5. package/lib/commonjs/index.js +29 -0
  6. package/lib/commonjs/index.js.map +1 -1
  7. package/lib/commonjs/index.web.js +73 -44
  8. package/lib/commonjs/index.web.js.map +1 -1
  9. package/lib/commonjs/surfaces/SurfaceHost.js +189 -0
  10. package/lib/commonjs/surfaces/SurfaceHost.js.map +1 -0
  11. package/lib/commonjs/surfaces/api.js +83 -0
  12. package/lib/commonjs/surfaces/api.js.map +1 -0
  13. package/lib/commonjs/surfaces/index.js +82 -0
  14. package/lib/commonjs/surfaces/index.js.map +1 -0
  15. package/lib/commonjs/surfaces/index.web.js +77 -0
  16. package/lib/commonjs/surfaces/index.web.js.map +1 -0
  17. package/lib/commonjs/surfaces/prompts.js +154 -0
  18. package/lib/commonjs/surfaces/prompts.js.map +1 -0
  19. package/lib/commonjs/surfaces/surfaceStore.js +172 -0
  20. package/lib/commonjs/surfaces/surfaceStore.js.map +1 -0
  21. package/lib/commonjs/surfaces/types.js +6 -0
  22. package/lib/commonjs/surfaces/types.js.map +1 -0
  23. package/lib/commonjs/surfaces/useSurface.js +36 -0
  24. package/lib/commonjs/surfaces/useSurface.js.map +1 -0
  25. package/lib/commonjs/theme/BloomThemeProvider.js +34 -15
  26. package/lib/commonjs/theme/BloomThemeProvider.js.map +1 -1
  27. package/lib/commonjs/theme/ambient-store.js +153 -0
  28. package/lib/commonjs/theme/ambient-store.js.map +1 -0
  29. package/lib/commonjs/theme/index.js +13 -0
  30. package/lib/commonjs/theme/index.js.map +1 -1
  31. package/lib/module/dialog/Dialog.js +13 -4
  32. package/lib/module/dialog/Dialog.js.map +1 -1
  33. package/lib/module/dialog/Dialog.web.js +23 -8
  34. package/lib/module/dialog/Dialog.web.js.map +1 -1
  35. package/lib/module/index.js +5 -0
  36. package/lib/module/index.js.map +1 -1
  37. package/lib/module/index.web.js +5 -0
  38. package/lib/module/index.web.js.map +1 -1
  39. package/lib/module/surfaces/SurfaceHost.js +184 -0
  40. package/lib/module/surfaces/SurfaceHost.js.map +1 -0
  41. package/lib/module/surfaces/api.js +48 -0
  42. package/lib/module/surfaces/api.js.map +1 -0
  43. package/lib/module/surfaces/index.js +24 -0
  44. package/lib/module/surfaces/index.js.map +1 -0
  45. package/lib/module/surfaces/index.web.js +19 -0
  46. package/lib/module/surfaces/index.web.js.map +1 -0
  47. package/lib/module/surfaces/prompts.js +148 -0
  48. package/lib/module/surfaces/prompts.js.map +1 -0
  49. package/lib/module/surfaces/surfaceStore.js +159 -0
  50. package/lib/module/surfaces/surfaceStore.js.map +1 -0
  51. package/lib/module/surfaces/types.js +4 -0
  52. package/lib/module/surfaces/types.js.map +1 -0
  53. package/lib/module/surfaces/useSurface.js +31 -0
  54. package/lib/module/surfaces/useSurface.js.map +1 -0
  55. package/lib/module/theme/BloomThemeProvider.js +34 -15
  56. package/lib/module/theme/BloomThemeProvider.js.map +1 -1
  57. package/lib/module/theme/ambient-store.js +146 -0
  58. package/lib/module/theme/ambient-store.js.map +1 -0
  59. package/lib/module/theme/index.js +1 -0
  60. package/lib/module/theme/index.js.map +1 -1
  61. package/lib/typescript/commonjs/dialog/Dialog.d.ts.map +1 -1
  62. package/lib/typescript/commonjs/dialog/Dialog.web.d.ts.map +1 -1
  63. package/lib/typescript/commonjs/dialog/types.d.ts +10 -0
  64. package/lib/typescript/commonjs/dialog/types.d.ts.map +1 -1
  65. package/lib/typescript/commonjs/index.d.ts +2 -0
  66. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  67. package/lib/typescript/commonjs/index.web.d.ts +2 -0
  68. package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
  69. package/lib/typescript/commonjs/surfaces/SurfaceHost.d.ts +25 -0
  70. package/lib/typescript/commonjs/surfaces/SurfaceHost.d.ts.map +1 -0
  71. package/lib/typescript/commonjs/surfaces/api.d.ts +41 -0
  72. package/lib/typescript/commonjs/surfaces/api.d.ts.map +1 -0
  73. package/lib/typescript/commonjs/surfaces/index.d.ts +25 -0
  74. package/lib/typescript/commonjs/surfaces/index.d.ts.map +1 -0
  75. package/lib/typescript/commonjs/surfaces/index.web.d.ts +20 -0
  76. package/lib/typescript/commonjs/surfaces/index.web.d.ts.map +1 -0
  77. package/lib/typescript/commonjs/surfaces/prompts.d.ts +13 -0
  78. package/lib/typescript/commonjs/surfaces/prompts.d.ts.map +1 -0
  79. package/lib/typescript/commonjs/surfaces/surfaceStore.d.ts +51 -0
  80. package/lib/typescript/commonjs/surfaces/surfaceStore.d.ts.map +1 -0
  81. package/lib/typescript/commonjs/surfaces/types.d.ts +145 -0
  82. package/lib/typescript/commonjs/surfaces/types.d.ts.map +1 -0
  83. package/lib/typescript/commonjs/surfaces/useSurface.d.ts +21 -0
  84. package/lib/typescript/commonjs/surfaces/useSurface.d.ts.map +1 -0
  85. package/lib/typescript/commonjs/theme/BloomThemeProvider.d.ts.map +1 -1
  86. package/lib/typescript/commonjs/theme/ambient-store.d.ts +61 -0
  87. package/lib/typescript/commonjs/theme/ambient-store.d.ts.map +1 -0
  88. package/lib/typescript/commonjs/theme/index.d.ts +2 -0
  89. package/lib/typescript/commonjs/theme/index.d.ts.map +1 -1
  90. package/lib/typescript/module/dialog/Dialog.d.ts.map +1 -1
  91. package/lib/typescript/module/dialog/Dialog.web.d.ts.map +1 -1
  92. package/lib/typescript/module/dialog/types.d.ts +10 -0
  93. package/lib/typescript/module/dialog/types.d.ts.map +1 -1
  94. package/lib/typescript/module/index.d.ts +2 -0
  95. package/lib/typescript/module/index.d.ts.map +1 -1
  96. package/lib/typescript/module/index.web.d.ts +2 -0
  97. package/lib/typescript/module/index.web.d.ts.map +1 -1
  98. package/lib/typescript/module/surfaces/SurfaceHost.d.ts +25 -0
  99. package/lib/typescript/module/surfaces/SurfaceHost.d.ts.map +1 -0
  100. package/lib/typescript/module/surfaces/api.d.ts +41 -0
  101. package/lib/typescript/module/surfaces/api.d.ts.map +1 -0
  102. package/lib/typescript/module/surfaces/index.d.ts +25 -0
  103. package/lib/typescript/module/surfaces/index.d.ts.map +1 -0
  104. package/lib/typescript/module/surfaces/index.web.d.ts +20 -0
  105. package/lib/typescript/module/surfaces/index.web.d.ts.map +1 -0
  106. package/lib/typescript/module/surfaces/prompts.d.ts +13 -0
  107. package/lib/typescript/module/surfaces/prompts.d.ts.map +1 -0
  108. package/lib/typescript/module/surfaces/surfaceStore.d.ts +51 -0
  109. package/lib/typescript/module/surfaces/surfaceStore.d.ts.map +1 -0
  110. package/lib/typescript/module/surfaces/types.d.ts +145 -0
  111. package/lib/typescript/module/surfaces/types.d.ts.map +1 -0
  112. package/lib/typescript/module/surfaces/useSurface.d.ts +21 -0
  113. package/lib/typescript/module/surfaces/useSurface.d.ts.map +1 -0
  114. package/lib/typescript/module/theme/BloomThemeProvider.d.ts.map +1 -1
  115. package/lib/typescript/module/theme/ambient-store.d.ts +61 -0
  116. package/lib/typescript/module/theme/ambient-store.d.ts.map +1 -0
  117. package/lib/typescript/module/theme/index.d.ts +2 -0
  118. package/lib/typescript/module/theme/index.d.ts.map +1 -1
  119. package/package.json +17 -1
  120. package/src/__tests__/ambient-theme.test.tsx +136 -0
  121. package/src/__tests__/surfaceStore.test.ts +219 -0
  122. package/src/dialog/Dialog.tsx +14 -3
  123. package/src/dialog/Dialog.web.tsx +25 -6
  124. package/src/dialog/types.ts +10 -0
  125. package/src/index.ts +15 -0
  126. package/src/index.web.ts +15 -0
  127. package/src/surfaces/SurfaceHost.tsx +207 -0
  128. package/src/surfaces/api.ts +55 -0
  129. package/src/surfaces/index.ts +38 -0
  130. package/src/surfaces/index.web.ts +33 -0
  131. package/src/surfaces/prompts.tsx +178 -0
  132. package/src/surfaces/surfaceStore.ts +168 -0
  133. package/src/surfaces/types.ts +156 -0
  134. package/src/surfaces/useSurface.ts +33 -0
  135. package/src/theme/BloomThemeProvider.tsx +52 -13
  136. package/src/theme/ambient-store.ts +186 -0
  137. package/src/theme/index.ts +7 -0
@@ -24,6 +24,8 @@ import {
24
24
  import { useControllableState } from '../hooks/useControllableState';
25
25
  import { FontLoader } from '../fonts/FontLoader';
26
26
 
27
+ import { useAmbientThemeState } from './ambient-store';
28
+
27
29
  import { applyDarkClass, applyVarsToDocument } from './apply-dark-class';
28
30
  import { buildTheme } from './build-theme';
29
31
  import { buildThemeFromSeed } from './build-theme-from-seed';
@@ -332,16 +334,40 @@ export function BloomThemeProvider({
332
334
  }: BloomThemeProviderProps) {
333
335
  const rnScheme = useRNColorScheme();
334
336
 
337
+ // The app-wide ambient override, driven imperatively via `useAmbientTheme()`
338
+ // from anywhere in the app. Subscribed through the module-level store's
339
+ // `useSyncExternalStore` (stable snapshot ref while unchanged → React-Compiler
340
+ // safe). When an ambient seed is set it OVERRIDES the static `seed` prop and
341
+ // the active preset; clearing it restores them.
342
+ const ambient = useAmbientThemeState();
343
+
344
+ // The EFFECTIVE dynamic seed + accents for this render: ambient wins over the
345
+ // static props. `null` ambient seed means "no override" → fall back to the
346
+ // `seed` prop (which itself may be undefined → preset path). When ambient is
347
+ // active, ambient accents replace the static accent props entirely (a null
348
+ // ambient accent = "no pin for this artwork colour").
349
+ const ambientActive = ambient.seed !== null;
350
+ const effectiveSeed: string | undefined = ambientActive
351
+ ? ambient.seed ?? undefined
352
+ : seed;
353
+ const effectiveSecondary: string | undefined = ambientActive
354
+ ? ambient.secondary ?? undefined
355
+ : secondaryColor;
356
+ const effectiveTertiary: string | undefined = ambientActive
357
+ ? ambient.tertiary ?? undefined
358
+ : tertiaryColor;
359
+
335
360
  // The app-wide pinned accents, if any. Memoized so its identity only changes
336
- // when a prop changes (keeps the effect/memo deps below stable). Left
361
+ // when an accent changes (keeps the effect/memo deps below stable). Left
337
362
  // `undefined` when neither is set so every downstream call is byte-identical
338
- // to the no-accent path.
363
+ // to the no-accent path. Uses the EFFECTIVE accents so ambient accents apply
364
+ // to the preset path too when no ambient seed is present.
339
365
  const explicitAccents = useMemo<ExplicitAccents | undefined>(
340
366
  () =>
341
- secondaryColor === undefined && tertiaryColor === undefined
367
+ effectiveSecondary === undefined && effectiveTertiary === undefined
342
368
  ? undefined
343
- : { secondaryHex: secondaryColor, tertiaryHex: tertiaryColor },
344
- [secondaryColor, tertiaryColor],
369
+ : { secondaryHex: effectiveSecondary, tertiaryHex: effectiveTertiary },
370
+ [effectiveSecondary, effectiveTertiary],
345
371
  );
346
372
 
347
373
  const { mode, colorPreset, setMode, setColorPreset, resetTheme, hydrated } = useThemeState({
@@ -371,20 +397,33 @@ export function BloomThemeProvider({
371
397
  // colour engine, so there is one code path — no seed/preset duplication.
372
398
  const themeVars = useMemo(
373
399
  () =>
374
- seed
375
- ? buildSeedScopeVars({ seed, mode: resolved, secondarySeed: secondaryColor, tertiarySeed: tertiaryColor })
400
+ effectiveSeed
401
+ ? buildSeedScopeVars({
402
+ seed: effectiveSeed,
403
+ mode: resolved,
404
+ secondarySeed: effectiveSecondary,
405
+ tertiarySeed: effectiveTertiary,
406
+ })
376
407
  : buildScopeVars(colorPreset, resolved, explicitAccents),
377
- [seed, secondaryColor, tertiaryColor, colorPreset, resolved, explicitAccents],
408
+ [effectiveSeed, effectiveSecondary, effectiveTertiary, colorPreset, resolved, explicitAccents],
378
409
  );
379
410
  const themeColors = useMemo(
380
411
  () =>
381
- seed
382
- ? buildThemeFromSeed(seed, resolved, undefined, undefined, {
383
- secondarySeed: secondaryColor,
384
- tertiarySeed: tertiaryColor,
412
+ effectiveSeed
413
+ ? buildThemeFromSeed(effectiveSeed, resolved, undefined, undefined, {
414
+ secondarySeed: effectiveSecondary,
415
+ tertiarySeed: effectiveTertiary,
385
416
  })
386
417
  : buildTheme(colorPreset, resolved, isAdaptive, explicitAccents),
387
- [seed, secondaryColor, tertiaryColor, colorPreset, resolved, isAdaptive, explicitAccents],
418
+ [
419
+ effectiveSeed,
420
+ effectiveSecondary,
421
+ effectiveTertiary,
422
+ colorPreset,
423
+ resolved,
424
+ isAdaptive,
425
+ explicitAccents,
426
+ ],
388
427
  );
389
428
 
390
429
  useIsomorphicLayoutEffect(() => {
@@ -0,0 +1,186 @@
1
+ import { useSyncExternalStore } from 'react';
2
+
3
+ /**
4
+ * The current ambient theme override. When `seed` is non-null the whole app is
5
+ * themed from this dynamic seed (same colour engine the `seed` prop uses),
6
+ * overriding the active preset / static `seed` prop. `secondary`/`tertiary` pin
7
+ * this seed's accent families (e.g. the 2nd/3rd colours extracted from artwork).
8
+ */
9
+ export interface AmbientThemeState {
10
+ readonly seed: string | null;
11
+ readonly secondary: string | null;
12
+ readonly tertiary: string | null;
13
+ }
14
+
15
+ export interface AmbientAccents {
16
+ secondary?: string | null;
17
+ tertiary?: string | null;
18
+ }
19
+
20
+ export interface AmbientThemeApi {
21
+ /**
22
+ * Set the ambient seed (and optional accents). Coalesced through an internal
23
+ * debounce so rapid hover/scroll doesn't thrash the theme. The debounce lives
24
+ * HERE — apps never own one.
25
+ */
26
+ setAmbient: (seed: string, accents?: AmbientAccents) => void;
27
+ /** Clear the ambient override, restoring the preset (or the static `seed` prop). */
28
+ clearAmbient: () => void;
29
+ }
30
+
31
+ /** Options for {@link useAmbientTheme}. */
32
+ export interface UseAmbientThemeOptions {
33
+ /**
34
+ * Debounce, in ms, applied to `setAmbient`/`clearAmbient` before the store
35
+ * commits. Default `120`. Pass `0` to apply synchronously.
36
+ */
37
+ debounceMs?: number;
38
+ }
39
+
40
+ const DEFAULT_DEBOUNCE_MS = 120;
41
+
42
+ // A single frozen "no override" snapshot. `getSnapshot` returns this EXACT
43
+ // reference whenever nothing is set, so `useSyncExternalStore` sees a stable
44
+ // identity across renders (React-Compiler-safe: the memoized value never
45
+ // silently goes stale because the snapshot ref only changes on a real commit).
46
+ const EMPTY_STATE: AmbientThemeState = Object.freeze({
47
+ seed: null,
48
+ secondary: null,
49
+ tertiary: null,
50
+ });
51
+
52
+ let state: AmbientThemeState = EMPTY_STATE;
53
+ const listeners = new Set<() => void>();
54
+
55
+ // Pending debounced commit. `null` operation means "clear".
56
+ let pendingTimer: ReturnType<typeof setTimeout> | null = null;
57
+ let pendingCommit: (() => void) | null = null;
58
+
59
+ function emit(): void {
60
+ for (const listener of listeners) listener();
61
+ }
62
+
63
+ function commit(next: AmbientThemeState): void {
64
+ // Skip a no-op commit so subscribers don't re-render for an identical value.
65
+ if (
66
+ next.seed === state.seed &&
67
+ next.secondary === state.secondary &&
68
+ next.tertiary === state.tertiary
69
+ ) {
70
+ return;
71
+ }
72
+ state = next;
73
+ emit();
74
+ }
75
+
76
+ function schedule(next: AmbientThemeState, debounceMs: number): void {
77
+ if (pendingTimer !== null) {
78
+ clearTimeout(pendingTimer);
79
+ pendingTimer = null;
80
+ }
81
+ const run = () => {
82
+ pendingTimer = null;
83
+ pendingCommit = null;
84
+ commit(next);
85
+ };
86
+ if (debounceMs <= 0) {
87
+ run();
88
+ return;
89
+ }
90
+ pendingCommit = run;
91
+ pendingTimer = setTimeout(run, debounceMs);
92
+ pendingTimer.unref?.();
93
+ }
94
+
95
+ function subscribe(listener: () => void): () => void {
96
+ listeners.add(listener);
97
+ return () => {
98
+ listeners.delete(listener);
99
+ };
100
+ }
101
+
102
+ function getSnapshot(): AmbientThemeState {
103
+ return state;
104
+ }
105
+
106
+ /** Internal setter used by the debounced hook API. */
107
+ function setAmbientInternal(seed: string, accents: AmbientAccents | undefined, debounceMs: number): void {
108
+ schedule(
109
+ {
110
+ seed,
111
+ secondary: accents?.secondary ?? null,
112
+ tertiary: accents?.tertiary ?? null,
113
+ },
114
+ debounceMs,
115
+ );
116
+ }
117
+
118
+ /** Internal clear used by the debounced hook API. */
119
+ function clearAmbientInternal(debounceMs: number): void {
120
+ schedule(EMPTY_STATE, debounceMs);
121
+ }
122
+
123
+ /**
124
+ * Read + drive the app-wide ambient theme. Reading returns the current override
125
+ * (via `useSyncExternalStore` — stable snapshot ref while unchanged); the
126
+ * returned `setAmbient`/`clearAmbient` are debounced imperative controls.
127
+ *
128
+ * `BloomThemeProvider` consumes the SAME store internally, so calling
129
+ * `setAmbient(...)` from anywhere themes the whole app through the provider's
130
+ * single apply path — no `seed` prop threading, no app-owned theming store.
131
+ */
132
+ export function useAmbientTheme(
133
+ options?: UseAmbientThemeOptions,
134
+ ): AmbientThemeState & AmbientThemeApi {
135
+ const debounceMs = options?.debounceMs ?? DEFAULT_DEBOUNCE_MS;
136
+ const current = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
137
+ return {
138
+ seed: current.seed,
139
+ secondary: current.secondary,
140
+ tertiary: current.tertiary,
141
+ setAmbient: (seed, accents) => setAmbientInternal(seed, accents, debounceMs),
142
+ clearAmbient: () => clearAmbientInternal(debounceMs),
143
+ };
144
+ }
145
+
146
+ /**
147
+ * Provider-side subscription. Returns the current ambient state with a stable
148
+ * ref while unchanged. Uses the same store, so it stays in lockstep with any
149
+ * `useAmbientTheme()` caller.
150
+ */
151
+ export function useAmbientThemeState(): AmbientThemeState {
152
+ return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
153
+ }
154
+
155
+ /**
156
+ * Non-hook imperative escape hatch (e.g. gesture handlers / worklets bridges /
157
+ * non-React callers). Same debounced store as {@link useAmbientTheme}.
158
+ */
159
+ export const ambientTheme: AmbientThemeApi & { getState: () => AmbientThemeState } = {
160
+ setAmbient: (seed, accents) => setAmbientInternal(seed, accents, DEFAULT_DEBOUNCE_MS),
161
+ clearAmbient: () => clearAmbientInternal(DEFAULT_DEBOUNCE_MS),
162
+ getState: getSnapshot,
163
+ };
164
+
165
+ /** Test-only: flush any pending debounced commit synchronously. */
166
+ export function __flushAmbientForTests(): void {
167
+ if (pendingCommit) {
168
+ if (pendingTimer !== null) {
169
+ clearTimeout(pendingTimer);
170
+ pendingTimer = null;
171
+ }
172
+ const run = pendingCommit;
173
+ pendingCommit = null;
174
+ run();
175
+ }
176
+ }
177
+
178
+ /** Test-only: reset the store to the empty state without notifying via debounce. */
179
+ export function __resetAmbientForTests(): void {
180
+ if (pendingTimer !== null) {
181
+ clearTimeout(pendingTimer);
182
+ pendingTimer = null;
183
+ }
184
+ pendingCommit = null;
185
+ commit(EMPTY_STATE);
186
+ }
@@ -30,6 +30,13 @@ export { buildThemeFromSeed, buildColorsFromSeed } from './build-theme-from-seed
30
30
  export type { SeedAccents } from './build-theme-from-seed';
31
31
  export { THEME_GRADIENTS } from './gradients';
32
32
  export { useTheme, useThemeColor, useBloomTheme } from './use-theme';
33
+ export { useAmbientTheme, ambientTheme } from './ambient-store';
34
+ export type {
35
+ AmbientThemeState,
36
+ AmbientThemeApi,
37
+ AmbientAccents,
38
+ UseAmbientThemeOptions,
39
+ } from './ambient-store';
33
40
  export { useNavigationTheme } from './use-navigation-theme';
34
41
  export type { NavigationTheme, NavigationThemeFont } from './use-navigation-theme';
35
42
  export type { Theme, ThemeColors, ThemeMode, ThemeGradient, ThemeGradients } from './types';