@oxy.so/bloom 7.1.0 → 7.1.2

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 (84) hide show
  1. package/assets/character-runtime/managed-avatar.mjs +258 -0
  2. package/assets/character-runtime/render-budget.mjs +144 -0
  3. package/assets/character-runtime/runtime.mjs +49 -18
  4. package/docs/adoption-matrix.mdx +3 -3
  5. package/docs/agent-avatar.mdx +11 -2
  6. package/docs/bottom-sheet.mdx +1 -1
  7. package/lib/commonjs/agent-avatar/AgentAvatarProvider.js +8 -3
  8. package/lib/commonjs/agent-avatar/AgentAvatarProvider.js.map +1 -1
  9. package/lib/commonjs/agent-avatar/CharacterAvatar.js +3 -2
  10. package/lib/commonjs/agent-avatar/CharacterAvatar.js.map +1 -1
  11. package/lib/commonjs/agent-avatar/context.js +4 -1
  12. package/lib/commonjs/agent-avatar/context.js.map +1 -1
  13. package/lib/commonjs/agent-creator/AgentCreatorBase.js +7 -5
  14. package/lib/commonjs/agent-creator/AgentCreatorBase.js.map +1 -1
  15. package/lib/commonjs/agent-creator/CharacterControls.js +4 -2
  16. package/lib/commonjs/agent-creator/CharacterControls.js.map +1 -1
  17. package/lib/commonjs/bottom-sheet/BottomSheetBase.js +33 -49
  18. package/lib/commonjs/bottom-sheet/BottomSheetBase.js.map +1 -1
  19. package/lib/commonjs/dialog/DialogBottomSheet.js +1 -1
  20. package/lib/commonjs/dialog/DialogBottomSheet.js.map +1 -1
  21. package/lib/commonjs/surface/SurfacePaint.js +17 -4
  22. package/lib/commonjs/surface/SurfacePaint.js.map +1 -1
  23. package/lib/module/agent-avatar/AgentAvatarProvider.js +9 -4
  24. package/lib/module/agent-avatar/AgentAvatarProvider.js.map +1 -1
  25. package/lib/module/agent-avatar/CharacterAvatar.js +3 -2
  26. package/lib/module/agent-avatar/CharacterAvatar.js.map +1 -1
  27. package/lib/module/agent-avatar/context.js +3 -0
  28. package/lib/module/agent-avatar/context.js.map +1 -1
  29. package/lib/module/agent-creator/AgentCreatorBase.js +9 -7
  30. package/lib/module/agent-creator/AgentCreatorBase.js.map +1 -1
  31. package/lib/module/agent-creator/CharacterControls.js +5 -3
  32. package/lib/module/agent-creator/CharacterControls.js.map +1 -1
  33. package/lib/module/bottom-sheet/BottomSheetBase.js +33 -49
  34. package/lib/module/bottom-sheet/BottomSheetBase.js.map +1 -1
  35. package/lib/module/dialog/DialogBottomSheet.js +1 -1
  36. package/lib/module/dialog/DialogBottomSheet.js.map +1 -1
  37. package/lib/module/surface/SurfacePaint.js +18 -5
  38. package/lib/module/surface/SurfacePaint.js.map +1 -1
  39. package/lib/typescript/commonjs/agent-avatar/AgentAvatarProvider.d.ts.map +1 -1
  40. package/lib/typescript/commonjs/agent-avatar/CharacterAvatar.d.ts.map +1 -1
  41. package/lib/typescript/commonjs/agent-avatar/context.d.ts +4 -1
  42. package/lib/typescript/commonjs/agent-avatar/context.d.ts.map +1 -1
  43. package/lib/typescript/commonjs/agent-creator/AgentCreatorBase.d.ts.map +1 -1
  44. package/lib/typescript/commonjs/agent-creator/CharacterControls.d.ts.map +1 -1
  45. package/lib/typescript/commonjs/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
  46. package/lib/typescript/commonjs/bottom-sheet/types.d.ts +1 -0
  47. package/lib/typescript/commonjs/bottom-sheet/types.d.ts.map +1 -1
  48. package/lib/typescript/commonjs/surface/SurfacePaint.d.ts.map +1 -1
  49. package/lib/typescript/module/agent-avatar/AgentAvatarProvider.d.ts.map +1 -1
  50. package/lib/typescript/module/agent-avatar/CharacterAvatar.d.ts.map +1 -1
  51. package/lib/typescript/module/agent-avatar/context.d.ts +4 -1
  52. package/lib/typescript/module/agent-avatar/context.d.ts.map +1 -1
  53. package/lib/typescript/module/agent-creator/AgentCreatorBase.d.ts.map +1 -1
  54. package/lib/typescript/module/agent-creator/CharacterControls.d.ts.map +1 -1
  55. package/lib/typescript/module/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
  56. package/lib/typescript/module/bottom-sheet/types.d.ts +1 -0
  57. package/lib/typescript/module/bottom-sheet/types.d.ts.map +1 -1
  58. package/lib/typescript/module/surface/SurfacePaint.d.ts.map +1 -1
  59. package/package.json +1 -1
  60. package/src/__tests__/support/adoption-matrix.ts +683 -0
  61. package/src/__tests__/support/card-surface.ts +25 -0
  62. package/src/__tests__/support/collision-fixture-barrel.ts +20 -0
  63. package/src/__tests__/support/commerce-harness.tsx +97 -0
  64. package/src/__tests__/support/composite.ts +68 -0
  65. package/src/__tests__/support/constructed-style-sheets.ts +68 -0
  66. package/src/__tests__/support/messages-in.ts +10 -0
  67. package/src/__tests__/support/press-host.ts +30 -0
  68. package/src/__tests__/support/rendered-style.ts +99 -0
  69. package/src/__tests__/support/unread-hook-fixture.ts +33 -0
  70. package/src/__tests__/support/worklet-capture-fixture.tsx +28 -0
  71. package/src/agent-avatar/AgentAvatarProvider.tsx +11 -4
  72. package/src/agent-avatar/CharacterAvatar.tsx +9 -4
  73. package/src/agent-avatar/__tests__/artwork-golden.json +774 -0
  74. package/src/agent-avatar/context.ts +5 -1
  75. package/src/agent-creator/AgentCreatorBase.tsx +13 -5
  76. package/src/agent-creator/CharacterControls.tsx +7 -3
  77. package/src/agent-creator/__tests__/support/character-runtime-fixture.tsx +26 -0
  78. package/src/bottom-sheet/BottomSheetBase.tsx +24 -34
  79. package/src/bottom-sheet/types.ts +1 -0
  80. package/src/dialog/DialogBottomSheet.tsx +1 -1
  81. package/src/surface/SurfacePaint.tsx +14 -4
  82. package/src/theme/__tests__/__fixtures__/golden-resolved-tokens.json +9346 -0
  83. package/src/theme/__tests__/__fixtures__/tonal-glass-primary-failures.json +69 -0
  84. package/src/theme/__tests__/fixtures/color-engine-golden.json +1 -0
@@ -14,7 +14,11 @@ export type CharacterCapabilities = {
14
14
  /** The application hosts the optional renderer assets at this module URL. */
15
15
  export const CharacterRuntimeContext = createContext<{
16
16
  runtimeUrl?: string;
17
+ reportCapabilities?: (value: CharacterCapabilities) => void;
18
+ }>({});
19
+
20
+ /** Only editors subscribe to catalog updates; renderer input stays stable. */
21
+ export const CharacterCapabilitiesContext = createContext<{
17
22
  capabilities?: CharacterCapabilities;
18
23
  capabilitiesByKey?: ReadonlyMap<string, CharacterCapabilities>;
19
- reportCapabilities?: (value: CharacterCapabilities) => void;
20
24
  }>({});
@@ -1,8 +1,11 @@
1
- import { useContext, useState } from 'react';
1
+ import { useContext, useMemo, useState } from 'react';
2
2
  import { Platform } from 'react-native';
3
3
  import { AgentAvatar, type AvatarConfig } from '../agent-avatar';
4
4
  import type { AvatarCharacterCategory } from '../agent-avatar/config-character';
5
- import { CharacterRuntimeContext } from '../agent-avatar/context';
5
+ import {
6
+ CharacterRuntimeContext,
7
+ CharacterCapabilitiesContext,
8
+ } from '../agent-avatar/context';
6
9
  import {
7
10
  legacyCharacterRecipe,
8
11
  legacyNativeShape,
@@ -46,13 +49,18 @@ export function AgentCreatorBase(props: AgentCreatorProps) {
46
49
  const [reactionKey, setReactionKey] = useState(0);
47
50
  const [workingKey, setWorkingKey] = useState(0);
48
51
  const runtime = useContext(CharacterRuntimeContext);
52
+ const catalog = useContext(CharacterCapabilitiesContext);
53
+ const previewConfig = useMemo(
54
+ () => ({ ...c, lookAt: 'wander' as const }),
55
+ [c],
56
+ );
49
57
  const { runtimeUrl } = runtime;
50
58
  const beta = Boolean(c.character && c.character.preset !== 'bloom');
51
59
  const nativeShape = legacyNativeShape(c);
52
60
  const key = JSON.stringify(beta ? c.character : legacyCharacterRecipe(c));
53
61
  const capabilities =
54
- runtime.capabilitiesByKey?.get(key) ??
55
- (runtime.capabilities?.key === key ? runtime.capabilities : undefined);
62
+ catalog.capabilitiesByKey?.get(key) ??
63
+ (catalog.capabilities?.key === key ? catalog.capabilities : undefined);
56
64
  const betaColor =
57
65
  capabilities?.selected.color ?? c.character?.selections?.color;
58
66
  const customColorValue = beta
@@ -315,7 +323,7 @@ export function AgentCreatorBase(props: AgentCreatorProps) {
315
323
  }}
316
324
  >
317
325
  <AgentAvatar
318
- config={{ ...c, lookAt: 'wander' }}
326
+ config={previewConfig}
319
327
  size={162}
320
328
  interactive={Boolean(runtimeUrl)}
321
329
  reactionKey={reactionKey}
@@ -1,5 +1,8 @@
1
1
  import { useContext } from 'react';
2
- import { CharacterRuntimeContext } from '../agent-avatar/context';
2
+ import {
3
+ CharacterRuntimeContext,
4
+ CharacterCapabilitiesContext,
5
+ } from '../agent-avatar/context';
3
6
  import type {
4
7
  AvatarCharacterCategory,
5
8
  AvatarCharacterConfig,
@@ -20,8 +23,9 @@ export function CharacterControls({
20
23
  onChange: (character: AvatarCharacterConfig | undefined) => void;
21
24
  }) {
22
25
  const messages = useAgentCreatorMessages();
23
- const { runtimeUrl, capabilities, capabilitiesByKey } = useContext(
24
- CharacterRuntimeContext,
26
+ const { runtimeUrl } = useContext(CharacterRuntimeContext);
27
+ const { capabilities, capabilitiesByKey } = useContext(
28
+ CharacterCapabilitiesContext,
25
29
  );
26
30
  const {
27
31
  Select,
@@ -0,0 +1,26 @@
1
+ import type { ContextType, ReactNode } from 'react';
2
+ import {
3
+ CharacterCapabilitiesContext,
4
+ CharacterRuntimeContext,
5
+ } from '../../../agent-avatar/context';
6
+
7
+ /** Supply both internal channels without starting the browser engine in UI tests. */
8
+ export function CharacterRuntimeFixture({
9
+ value,
10
+ children,
11
+ }: {
12
+ value: ContextType<typeof CharacterRuntimeContext> &
13
+ ContextType<typeof CharacterCapabilitiesContext>;
14
+ children: ReactNode;
15
+ }) {
16
+ const { runtimeUrl, reportCapabilities, ...catalog } = value;
17
+ return (
18
+ <CharacterRuntimeContext.Provider
19
+ value={{ runtimeUrl, reportCapabilities }}
20
+ >
21
+ <CharacterCapabilitiesContext.Provider value={catalog}>
22
+ {children}
23
+ </CharacterCapabilitiesContext.Provider>
24
+ </CharacterRuntimeContext.Provider>
25
+ );
26
+ }
@@ -133,14 +133,11 @@ export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: Rea
133
133
  const bodyPanRef = useRef<GestureType | undefined>(undefined);
134
134
  const handlePanRef = useRef<GestureType | undefined>(undefined);
135
135
 
136
- // Dismiss callbacks
136
+ // Completion is not a user request: once closed, every owner must hear it.
137
+ // Vetoes are checked before a user-triggered close starts, never after it.
137
138
  const safeClose = useCallback(() => {
138
- if (onDismissAttempt?.()) {
139
- onDismiss?.();
140
- } else if (!onDismissAttempt) {
141
- onDismiss?.();
142
- }
143
- }, [onDismissAttempt, onDismiss]);
139
+ onDismiss?.();
140
+ }, [onDismiss]);
144
141
 
145
142
  // Mirror `safeClose` and `rendered` into refs so the unmount cleanup can
146
143
  // fire the latest dismiss callback when needed, without re-binding the
@@ -251,6 +248,20 @@ export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: Rea
251
248
  };
252
249
  }, [colors.border]);
253
250
 
251
+ // Gesture callbacks cross to JS before consulting the consumer's veto.
252
+ // A refused drag springs back; it never hides the Modal or strands its owner.
253
+ const requestPanDismiss = useCallback((generation: number, velocity: number) => {
254
+ if (closeGenerationRef.current !== generation || hasClosedRef.current) return;
255
+ if (onDismissAttempt && !onDismissAttempt()) {
256
+ translateY.value = withSpring(0, { ...SPRING_CONFIG, velocity });
257
+ return;
258
+ }
259
+ translateY.value = withSpring(screenHeightSV.value, { ...SPRING_CONFIG, velocity });
260
+ opacity.value = withTiming(0, { ...MOTION_RECIPES.dismiss, duration: 250 }, (finished) => {
261
+ if (finished) runOnJS(finishClose)(generation);
262
+ });
263
+ }, [onDismissAttempt, translateY, screenHeightSV, opacity, finishClose]);
264
+
254
265
  const present = useCallback(() => {
255
266
  setRendered(true);
256
267
  setVisible(true);
@@ -336,15 +347,7 @@ export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: Rea
336
347
  (distance > closeThreshold && velocity > -300);
337
348
 
338
349
  if (shouldClose) {
339
- // Snapshot the generation on the UI thread at the
340
- // moment the close gesture commits. The completion
341
- // callback only fires `finishClose` if no reopen
342
- // bumped the generation in between.
343
- const generation = closeGeneration.value;
344
- translateY.value = withSpring(screenHeightSV.value, { ...SPRING_CONFIG, velocity });
345
- opacity.value = withTiming(0, { ...MOTION_RECIPES.dismiss, duration: 250 }, (finished) => {
346
- if (finished) runOnJS(finishClose)(generation);
347
- });
350
+ runOnJS(requestPanDismiss)(closeGeneration.value, velocity);
348
351
  } else {
349
352
  translateY.value = withSpring(0, { ...SPRING_CONFIG, velocity });
350
353
  }
@@ -396,16 +399,7 @@ export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: Rea
396
399
  (distance > closeThreshold && velocity > -300);
397
400
 
398
401
  if (shouldClose) {
399
- const generation = closeGeneration.value;
400
- translateY.value = withSpring(screenHeightSV.value, {
401
- ...SPRING_CONFIG,
402
- velocity: velocity,
403
- });
404
- opacity.value = withTiming(0, { ...MOTION_RECIPES.dismiss, duration: 250 }, (finished) => {
405
- if (finished) {
406
- runOnJS(finishClose)(generation);
407
- }
408
- });
402
+ runOnJS(requestPanDismiss)(closeGeneration.value, velocity);
409
403
  } else {
410
404
  translateY.value = withSpring(0, {
411
405
  ...SPRING_CONFIG,
@@ -417,7 +411,7 @@ export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: Rea
417
411
  // values that change the gesture's behavior. `finishClose` is stable
418
412
  // (useCallback with stable deps).
419
413
  // eslint-disable-next-line react-hooks/exhaustive-deps
420
- }, [enablePanDownToClose, detached, manualActivation, nativeGesture, finishClose]);
414
+ }, [enablePanDownToClose, detached, manualActivation, nativeGesture, requestPanDismiss]);
421
415
 
422
416
  // Dedicated handle pan — only built in `manualActivation` mode. Always
423
417
  // active so users can drag the handle even while content is mid-scroll.
@@ -456,17 +450,13 @@ export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: Rea
456
450
  (distance > closeThreshold && velocity > -300);
457
451
 
458
452
  if (shouldClose) {
459
- const generation = closeGeneration.value;
460
- translateY.value = withSpring(screenHeightSV.value, { ...SPRING_CONFIG, velocity });
461
- opacity.value = withTiming(0, { ...MOTION_RECIPES.dismiss, duration: 250 }, (finished) => {
462
- if (finished) runOnJS(finishClose)(generation);
463
- });
453
+ runOnJS(requestPanDismiss)(closeGeneration.value, velocity);
464
454
  } else {
465
455
  translateY.value = withSpring(0, { ...SPRING_CONFIG, velocity });
466
456
  }
467
457
  });
468
458
  // eslint-disable-next-line react-hooks/exhaustive-deps
469
- }, [manualActivation, enablePanDownToClose, enableHandlePanningGesture, detached, finishClose]);
459
+ }, [manualActivation, enablePanDownToClose, enableHandlePanningGesture, detached, requestPanDismiss]);
470
460
 
471
461
  // CRITICAL — the shared values each `useAnimatedStyle` READS (translateY,
472
462
  // opacity, screenHeightSV, keyboardHeight) MUST be listed in its dependency
@@ -698,7 +688,7 @@ export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: Rea
698
688
  : <View style={[styles.nonScrollableContent, surfaceCorners, { overflow: 'hidden' }]}>{surfaceChildren}</View>;
699
689
 
700
690
  return (
701
- <Shell visible={rendered} onRequestClose={dismiss} keyboardHeight={keyboardHeight}>
691
+ <Shell visible={rendered} onRequestClose={handleBackdropPress} keyboardHeight={keyboardHeight}>
702
692
  {/* Web keyboard and focus: in on open, back on close, Tab kept in
703
693
  the sheet, Escape a USER dismissal — through `onDismissAttempt`,
704
694
  exactly like the backdrop. The sheet had no keyboard path at
@@ -38,6 +38,7 @@ export interface BottomSheetProps {
38
38
  */
39
39
  style?: StyleProp<AnimatedStyle<ViewStyle>>;
40
40
  enableHandlePanningGesture?: boolean;
41
+ /** Veto user Back/backdrop/Escape/pan requests before closing; imperative dismiss bypasses it. */
41
42
  onDismissAttempt?: () => boolean;
42
43
  detached?: boolean; // If true, shows with margins and rounded corners. If false, full width with rounded top only.
43
44
  /**
@@ -197,7 +197,7 @@ export function DialogBottomSheet({
197
197
  const maxHeightPercent: `${number}%` = `${Math.round(maxHeightRatio * 100)}%`;
198
198
  return [
199
199
  {
200
- backgroundColor: 'transparent',
200
+ backgroundColor: theme.colors.background,
201
201
  ...surfaceStyle(SURFACE_SHAPES.sheet),
202
202
  maxHeight: maxHeightPercent,
203
203
  },
@@ -1,7 +1,7 @@
1
1
  import { surfaceStyle } from '../shapes/surface-style';
2
2
  import type { SurfaceShape } from '../shapes/corner-types';
3
- import React, { memo, useEffect } from 'react';
4
- import { Platform, StyleSheet, View, type ViewStyle } from 'react-native';
3
+ import React, { memo, useCallback, useEffect, useState } from 'react';
4
+ import { Platform, StyleSheet, View, type LayoutChangeEvent, type ViewStyle } from 'react-native';
5
5
  import Svg, { Defs, LinearGradient, Rect, Stop } from 'react-native-svg';
6
6
  import { useSvgIdPrefix } from '../styles/svg-id';
7
7
  import { StyledView } from '../styles/styled-primitives';
@@ -47,6 +47,13 @@ export const SurfacePaint = memo(function SurfacePaint({
47
47
  const resolvedFill = resolveSurfaceTint(fill ?? 'transparent');
48
48
  const id = useSvgIdPrefix('bloom-surface');
49
49
  const isWeb = Platform.OS === 'web';
50
+ const [nativeSize, setNativeSize] = useState<{ width: number; height: number } | null>(null);
51
+ const onNativeLayout = useCallback((event: LayoutChangeEvent) => {
52
+ const { width, height } = event.nativeEvent.layout;
53
+ if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) return;
54
+ setNativeSize(previous => previous?.width === width && previous.height === height
55
+ ? previous : { width, height });
56
+ }, []);
50
57
  useSurfaceRefraction(isWeb);
51
58
  useEffect(() => {
52
59
  if (isWeb) adoptStyleSheet('bloom-surface-paint-web-css', WEB_PAINT_CSS);
@@ -67,17 +74,20 @@ export const SurfacePaint = memo(function SurfacePaint({
67
74
  return (
68
75
  <View
69
76
  testID={testID}
77
+ onLayout={onNativeLayout}
70
78
  pointerEvents="none"
71
79
  style={[StyleSheet.absoluteFill, geometry, { overflow: 'hidden' }]}
72
80
  >
81
+ {/* Android SVG percentage paths cache canvas dimensions after first paint.
82
+ Update the actual Rect geometry when this backing View changes size. */}
73
83
  <Svg width="100%" height="100%" style={StyleSheet.absoluteFill}>
74
84
  <Defs>
75
85
  {sheen ? <LinearGradient id={`${id}-sheen`} x1="0" y1="0" x2="0" y2="1">
76
86
  {optics.sheen.map(stop => <Stop key={stop.offset} offset={String(stop.offset)} stopColor={stop.color} stopOpacity={stop.opacity} />)}
77
87
  </LinearGradient> : null}
78
88
  </Defs>
79
- {fill !== undefined ? <Rect x="0" y="0" width="100%" height="100%" fill={base.color} fillOpacity={base.opacity} /> : null}
80
- {sheen ? <Rect x="0" y="0" width="100%" height="100%" fill={`url(#${id}-sheen)`} /> : null}
89
+ {fill !== undefined ? <Rect x="0" y="0" width={nativeSize?.width ?? '100%'} height={nativeSize?.height ?? '100%'} fill={base.color} fillOpacity={base.opacity} /> : null}
90
+ {sheen ? <Rect x="0" y="0" width={nativeSize?.width ?? '100%'} height={nativeSize?.height ?? '100%'} fill={`url(#${id}-sheen)`} /> : null}
81
91
  </Svg>
82
92
  <View pointerEvents="none" style={[StyleSheet.absoluteFill, geometry, { boxShadow: optics.rim }]} />
83
93
  </View>