@symbiote-native/engine 0.1.4 → 0.1.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.
Files changed (62) hide show
  1. package/build/accessibility-info/index.android.js +17 -29
  2. package/build/accessibility-info/index.ios.js +23 -37
  3. package/build/accessibility-info/shared.d.ts +3 -2
  4. package/build/accessibility-info/shared.js +25 -2
  5. package/build/action-sheet-ios/index.d.ts +6 -0
  6. package/build/action-sheet-ios/index.js +1 -1
  7. package/build/alert/index.android.js +1 -3
  8. package/build/animated/animations/base.js +5 -14
  9. package/build/animated/event.js +7 -9
  10. package/build/animated/graph.d.ts +6 -0
  11. package/build/animated/graph.js +14 -0
  12. package/build/animated/interpolation-node.d.ts +0 -1
  13. package/build/animated/interpolation-node.js +5 -4
  14. package/build/animated/native/native-animated.js +1 -3
  15. package/build/animated/operators.d.ts +14 -34
  16. package/build/animated/operators.js +19 -95
  17. package/build/animated/value.d.ts +2 -4
  18. package/build/animated/value.js +17 -5
  19. package/build/app-state/index.js +17 -22
  20. package/build/appearance/index.js +17 -25
  21. package/build/back-handler/index.js +15 -18
  22. package/build/commit.d.ts +1 -3
  23. package/build/commit.js +10 -207
  24. package/build/dimensions/index.js +25 -20
  25. package/build/events/index.js +6 -151
  26. package/build/fabric-props.d.ts +3 -0
  27. package/build/fabric-props.js +186 -0
  28. package/build/i18n-manager/index.js +3 -5
  29. package/build/image-loader.d.ts +24 -0
  30. package/build/image-loader.js +175 -0
  31. package/build/image-source-resolver.d.ts +9 -0
  32. package/build/image-source-resolver.js +16 -0
  33. package/build/index.d.ts +10 -3
  34. package/build/index.js +11 -2
  35. package/build/keyboard/index.js +19 -34
  36. package/build/layout-animation/index.d.ts +1 -0
  37. package/build/layout-animation/index.js +53 -35
  38. package/build/native-modules.d.ts +12 -0
  39. package/build/native-modules.js +64 -1
  40. package/build/node.d.ts +1 -0
  41. package/build/node.js +9 -0
  42. package/build/pan-responder/index.js +1 -3
  43. package/build/permissions-android/index.js +1 -3
  44. package/build/platform-color.d.ts +3 -0
  45. package/build/platform-color.js +24 -0
  46. package/build/process-background-image/index.js +7 -14
  47. package/build/process-box-shadow/index.js +1 -5
  48. package/build/process-filter.js +2 -8
  49. package/build/registry.js +3 -5
  50. package/build/settings/index.js +1 -3
  51. package/build/share/index.ios.js +1 -1
  52. package/build/status-bar/index.android.js +1 -1
  53. package/build/style/index.js +2 -4
  54. package/build/style-registry/index.d.ts +1 -3
  55. package/build/style-registry/index.js +15 -48
  56. package/build/style-registry/scope.d.ts +3 -0
  57. package/build/style-registry/scope.js +41 -0
  58. package/build/touch-history.d.ts +23 -0
  59. package/build/touch-history.js +150 -0
  60. package/build/type-guards.d.ts +4 -0
  61. package/build/type-guards.js +15 -0
  62. package/package.json +1 -1
@@ -0,0 +1,186 @@
1
+ // Fabric-prop translation: turn a retained node's logical props into the flat payload
2
+ // Fabric's C++ props expect. Split out of commit.ts (which owns the reconciler/mirror walk
3
+ // + the imperative instance API, neither of which this half touches) so the two
4
+ // responsibilities stop sharing one 300+ line file. Color processing itself lives in
5
+ // ./platform-color (the stable leaf every color-touching module imports from); this file only
6
+ // decides WHICH props are color props and wires the structured CSS-style processors.
7
+ import { RAW_TEXT_COMPONENT } from './node.js';
8
+ import { flattenStyle } from './style/index.js';
9
+ import { registeredProcessor } from './registry.js';
10
+ import { isProcessableColor, processColor } from './platform-color.js';
11
+ import { processBoxShadow } from './process-box-shadow/index.js';
12
+ import { processFilter } from './process-filter.js';
13
+ import { processTransformOrigin } from './process-transform-origin/index.js';
14
+ import { processTransform } from './process-transform/index.js';
15
+ import { processAspectRatio } from './process-aspect-ratio.js';
16
+ import { processFontVariant } from './process-font-variant.js';
17
+ import { processBackgroundImage } from './process-background-image/index.js';
18
+ import { isRecord, isString } from './type-guards.js';
19
+ // Color props must reach Fabric as platform ints, not CSS strings. Fabric's C++
20
+ // color parser silently drops strings. The actual conversion (processColor) is
21
+ // RN-platform-specific, so it is injected in platform-color.ts rather than imported,
22
+ // keeping shared free of a react-native dependency (and the headless harness working).
23
+ const COLOR_PROPS = new Set([
24
+ 'backgroundColor',
25
+ 'color',
26
+ 'borderColor',
27
+ 'borderTopColor',
28
+ 'borderRightColor',
29
+ 'borderBottomColor',
30
+ 'borderLeftColor',
31
+ // Logical (writing-direction-relative) border colors + the block axis, all wired to
32
+ // processColor in RN's ReactNativeStyleAttributes. borderStartColor/borderEndColor are
33
+ // even publicly typed ColorValue, so they silently dropped on iOS / threw on Android.
34
+ 'borderStartColor',
35
+ 'borderEndColor',
36
+ 'borderBlockColor',
37
+ 'borderBlockStartColor',
38
+ 'borderBlockEndColor',
39
+ 'shadowColor',
40
+ // Text shadow + the W3C `outline`/image `overlay` colors, also processColor in RN.
41
+ 'textShadowColor',
42
+ 'overlayColor',
43
+ 'outlineColor',
44
+ 'tintColor',
45
+ // TextInput color props. iOS's native input accepts a CSS string, but Android's
46
+ // AndroidTextInput is strict ("ColorValue: the value must be a number or Object"),
47
+ // so these must be processColor'd here too, same as any other color reaching Fabric.
48
+ 'placeholderTextColor',
49
+ 'selectionColor',
50
+ 'cursorColor',
51
+ 'underlineColorAndroid',
52
+ // Text decoration color (underline/strike): same Fabric strictness as any color.
53
+ 'textDecorationColor',
54
+ 'selectionHandleColor',
55
+ // Switch track/thumb colors. RN processColors each via the Switch ViewConfig
56
+ // (SwitchNativeComponent / AndroidSwitchNativeComponent validAttributes). iOS takes
57
+ // onTintColor (ON) / tintColor (OFF); Android takes trackColorForTrue/False +
58
+ // trackTintColor, and Android's ColorPropConverter is strict ("the value must be a
59
+ // number or Object"), so a raw CSS string crashes. thumbTintColor reaches both.
60
+ 'onTintColor',
61
+ 'thumbTintColor',
62
+ 'trackColorForTrue',
63
+ 'trackColorForFalse',
64
+ 'trackTintColor',
65
+ ]);
66
+ // Structured CSS-style keys RN parses in JS before native (boxShadow/filter register
67
+ // with enableNativeCSSParsing(), which DEFAULTS TO FALSE, so native CSS parsing is off
68
+ // and the raw string is dropped). Each runs on the hoisted top-level style key, turning
69
+ // a CSS string or structured array into the processed array Fabric's C++ expects.
70
+ const STYLE_PROCESSORS = new Map([
71
+ ['boxShadow', value => processBoxShadow(asBoxShadowInput(value))],
72
+ ['filter', value => processFilter(asFilterInput(value))],
73
+ ['transformOrigin', value => processTransformOrigin(asTransformOriginInput(value))],
74
+ ['transform', processTransformValue],
75
+ ['aspectRatio', value => processAspectRatio(asAspectRatioInput(value))],
76
+ ['fontVariant', value => processFontVariant(asFontVariantInput(value))],
77
+ ['experimental_backgroundImage', value => processBackgroundImage(asBackgroundImageInput(value))],
78
+ ]);
79
+ // boxShadow accepts a CSS string or an array of shadow objects; anything else is
80
+ // undefined to processBoxShadow (which returns []). Narrowing avoids an `as` cast.
81
+ function asBoxShadowInput(value) {
82
+ if (typeof value === 'string')
83
+ return value;
84
+ if (Array.isArray(value))
85
+ return value.filter(isRecord);
86
+ return undefined;
87
+ }
88
+ // filter accepts a CSS string or an array of single-key filter objects; same narrowing.
89
+ function asFilterInput(value) {
90
+ if (typeof value === 'string')
91
+ return value;
92
+ if (Array.isArray(value))
93
+ return value.filter(isRecord);
94
+ return undefined;
95
+ }
96
+ // experimental_backgroundImage accepts a CSS string (gradient functions) or an array of
97
+ // structured gradient objects; same narrowing as boxShadow/filter.
98
+ function asBackgroundImageInput(value) {
99
+ if (typeof value === 'string')
100
+ return value;
101
+ if (Array.isArray(value))
102
+ return value.filter(isRecord);
103
+ return undefined;
104
+ }
105
+ // transformOrigin accepts a CSS string or a [x, y, z] array of strings/numbers; anything
106
+ // else is undefined to processTransformOrigin (which defaults to center/center/0).
107
+ function asTransformOriginInput(value) {
108
+ if (typeof value === 'string')
109
+ return value;
110
+ if (Array.isArray(value))
111
+ return value.filter(isStringOrNumber);
112
+ return undefined;
113
+ }
114
+ // aspectRatio accepts a number (the common, working form) or a ratio string; otherwise
115
+ // undefined, which processAspectRatio drops.
116
+ function asAspectRatioInput(value) {
117
+ if (typeof value === 'number' || typeof value === 'string')
118
+ return value;
119
+ return undefined;
120
+ }
121
+ // fontVariant accepts an array of variant strings (the common, working form) or a
122
+ // space-separated string; anything else becomes an empty string, which yields [].
123
+ function asFontVariantInput(value) {
124
+ if (typeof value === 'string')
125
+ return value;
126
+ if (Array.isArray(value))
127
+ return value.filter(isString);
128
+ return '';
129
+ }
130
+ // transform accepts a CSS string (processTransform parses it) or an array of single-key
131
+ // transform records (the hot animated / sticky-header path, passed through unchanged).
132
+ // A non-string non-array value is NOT dropped: it may already be processed, so it passes
133
+ // through verbatim rather than being coerced to [] (which would erase a valid transform).
134
+ function processTransformValue(value) {
135
+ if (typeof value === 'string')
136
+ return processTransform(value);
137
+ if (Array.isArray(value))
138
+ return processTransform(value.filter(isRecord));
139
+ return value;
140
+ }
141
+ function isStringOrNumber(value) {
142
+ return typeof value === 'string' || typeof value === 'number';
143
+ }
144
+ // Convert a prop to the shape Fabric's C++ expects. A third-party view contributes
145
+ // its own processors, auto-derived from its ViewConfig (validAttributes[*].process,
146
+ // e.g. processColor for a slider's track tints); those run first. Then the structured
147
+ // CSS-style processors (boxShadow/filter). Built-ins are never in the registry, so they
148
+ // fall through to the global color path, where any CSS-string color is run through the
149
+ // injected platform processor (Fabric's C++ color parser silently drops strings).
150
+ function processValue(component, key, value) {
151
+ const processor = registeredProcessor(component, key);
152
+ if (processor !== undefined)
153
+ return processor(value);
154
+ const styleProcessor = STYLE_PROCESSORS.get(key);
155
+ if (styleProcessor !== undefined)
156
+ return styleProcessor(value);
157
+ if (COLOR_PROPS.has(key) && isProcessableColor(value))
158
+ return processColor(value);
159
+ return value;
160
+ }
161
+ // Translate the retained node's logical props into the flat payload Fabric's C++
162
+ // props expect: `style` keys are hoisted to the top level, event handlers and
163
+ // undefined values are dropped.
164
+ export function fabricProps(node) {
165
+ if (node.component === RAW_TEXT_COMPONENT) {
166
+ return { text: node.props.text };
167
+ }
168
+ const out = {};
169
+ for (const [key, value] of Object.entries(node.props)) {
170
+ if (key === 'style')
171
+ continue;
172
+ if (typeof value === 'function')
173
+ continue;
174
+ if (value === undefined)
175
+ continue;
176
+ out[key] = processValue(node.component, key, value);
177
+ }
178
+ // Collapse style (object | array | nested arrays) into one flat payload before
179
+ // hoisting: `style={[base, override]}` is RN's idiom and Fabric wants it flat.
180
+ const style = flattenStyle(node.props.style);
181
+ for (const [key, value] of Object.entries(style)) {
182
+ if (value !== undefined)
183
+ out[key] = processValue(node.component, key, value);
184
+ }
185
+ return out;
186
+ }
@@ -14,9 +14,10 @@
14
14
  // swapLeftAndRightInRTL(flipStyles: boolean)
15
15
  import { dlog } from '../debug.js';
16
16
  import { getNativeModule } from '../native-modules.js';
17
+ import { isBoolean } from '../type-guards.js';
17
18
  // The iOS native module name RN registers this under (the same name on both
18
- // platforms). A module name is only provable on a real host — a headless fake
19
- // answers to any name — so this name is still pending verification on device.
19
+ // platforms). A module name is only provable on a real host (a headless fake
20
+ // answers to any name), so this name is still pending verification on device.
20
21
  const I18N_MANAGER_MODULE = 'I18nManager';
21
22
  // RN's fallback constants when no native module is linked (headless / not yet on
22
23
  // device): not RTL, and the iOS default of swapping in RTL.
@@ -24,9 +25,6 @@ const DEFAULT_CONSTANTS = {
24
25
  isRTL: false,
25
26
  doLeftAndRightSwapInRTL: true,
26
27
  };
27
- function isBoolean(value) {
28
- return typeof value === 'boolean';
29
- }
30
28
  function isOptionalString(value) {
31
29
  return value === undefined || typeof value === 'string';
32
30
  }
@@ -0,0 +1,24 @@
1
+ import { type IImageSourceProp } from './image-source-resolver';
2
+ export type IImageSize = {
3
+ width: number;
4
+ height: number;
5
+ };
6
+ export type IImageCacheStatus = 'memory' | 'disk' | 'disk/memory';
7
+ type ISizeSuccess = (width: number, height: number) => void;
8
+ type ISizeFailure = (error: unknown) => void;
9
+ declare function getSize(uri: string, success?: ISizeSuccess, failure?: ISizeFailure): Promise<IImageSize>;
10
+ declare function getSizeWithHeaders(uri: string, headers: Record<string, string>, success?: ISizeSuccess, failure?: ISizeFailure): Promise<IImageSize>;
11
+ declare function prefetch(uri: string, callback?: (requestId: number) => void): Promise<boolean>;
12
+ declare function abortPrefetch(requestId: number): void;
13
+ declare function queryCache(uris: string[]): Promise<Record<string, IImageCacheStatus>>;
14
+ declare function resolveAssetSource(source: IImageSourceProp): unknown;
15
+ export type IImageStatics = {
16
+ getSize: typeof getSize;
17
+ getSizeWithHeaders: typeof getSizeWithHeaders;
18
+ prefetch: typeof prefetch;
19
+ abortPrefetch: typeof abortPrefetch;
20
+ queryCache: typeof queryCache;
21
+ resolveAssetSource: typeof resolveAssetSource;
22
+ };
23
+ export declare const imageStatics: IImageStatics;
24
+ export {};
@@ -0,0 +1,175 @@
1
+ // Image static methods (RN's Image.getSize / prefetch / queryCache / etc).
2
+ //
3
+ // These mirror RN's iOS Image statics (Libraries/Image/Image.ios.js), which delegate to the
4
+ // `ImageLoader` native (Turbo)Module declared in NativeImageLoaderIOS.js. The Android spec
5
+ // (NativeImageLoaderAndroid.js) registers under the SAME module name ('ImageLoader'), so this
6
+ // stays a flat, non-platform-split module - only the Android prefetch call signature differs (a
7
+ // second `requestId` arg), branched on Platform.OS below, not on module name. NOTE the asymmetry
8
+ // in the iOS spec: `getSize` resolves a `[width, height]` ARRAY, while `getSizeWithHeaders`
9
+ // resolves a `{width, height}` OBJECT, both are guarded below before reading. The native result
10
+ // crosses the I/O boundary as `unknown`; we never cast it, we narrow its shape.
11
+ //
12
+ // This is a stateful, native-bridge-touching imperative module (module-level ImageLoader cache +
13
+ // prefetch requestId counter) with no view of its own - it belongs in @symbiote-native/engine
14
+ // alongside Alert/Share, not in a view/render-*.ts file (whose contract is zero state / zero
15
+ // native bridge).
16
+ import { dlog } from './debug.js';
17
+ import { resolveImageSource } from './image-source-resolver.js';
18
+ import { getNativeModule } from './native-modules.js';
19
+ import { Platform } from './platform';
20
+ import { isNumber } from './type-guards.js';
21
+ // The iOS native module name RN registers this under (NativeImageLoaderIOS.js resolves
22
+ // `TurboModuleRegistry.getEnforcing<Spec>('ImageLoader')`). A module name like this is only
23
+ // provable on a real host - a headless fake answers to any name - so this iOS name is
24
+ // device-verify-pending.
25
+ const IMAGE_LOADER_MODULE = 'ImageLoader';
26
+ let imageLoaderModule;
27
+ function getImageLoader() {
28
+ if (imageLoaderModule === undefined) {
29
+ imageLoaderModule = getNativeModule(IMAGE_LOADER_MODULE);
30
+ dlog(`Image: ImageLoader module ${imageLoaderModule ? 'resolved' : 'NOT resolved (null)'}`);
31
+ }
32
+ return imageLoaderModule;
33
+ }
34
+ // Narrow native's getSize result. The spec resolves a `[width, height]` array, but tolerate a
35
+ // `{width, height}` object too (getSizeWithHeaders uses that shape).
36
+ function toImageSize(result) {
37
+ if (Array.isArray(result) && isNumber(result[0]) && isNumber(result[1])) {
38
+ return { width: result[0], height: result[1] };
39
+ }
40
+ if (typeof result === 'object' && result !== null) {
41
+ const width = Reflect.get(result, 'width');
42
+ const height = Reflect.get(result, 'height');
43
+ if (isNumber(width) && isNumber(height))
44
+ return { width, height };
45
+ }
46
+ throw new Error(`Image: unexpected size result from native: ${JSON.stringify(result)}`);
47
+ }
48
+ function requireLoader(method) {
49
+ const loader = getImageLoader();
50
+ if (loader === null) {
51
+ throw new Error(`Image.${method}: ImageLoader native module is not available ` +
52
+ '(running headless or not linked on this host).');
53
+ }
54
+ return loader;
55
+ }
56
+ // Resolve image dimensions, optionally via success/failure callbacks. Always returns the Promise
57
+ // too (RN returns void when a callback is passed, but a promise-and-callback shape is friendlier
58
+ // and a strict superset).
59
+ function getSize(uri, success, failure) {
60
+ const promise = Promise.resolve()
61
+ .then(() => requireLoader('getSize').getSize(uri))
62
+ .then(toImageSize);
63
+ if (typeof success === 'function') {
64
+ promise
65
+ .then(size => success(size.width, size.height))
66
+ .catch((error) => {
67
+ if (typeof failure === 'function')
68
+ failure(error);
69
+ else
70
+ dlog(`Image.getSize failed for ${uri}: ${String(error)}`);
71
+ });
72
+ }
73
+ return promise;
74
+ }
75
+ function getSizeWithHeaders(uri, headers, success, failure) {
76
+ const promise = Promise.resolve()
77
+ .then(() => requireLoader('getSizeWithHeaders').getSizeWithHeaders(uri, headers))
78
+ .then(toImageSize);
79
+ if (typeof success === 'function') {
80
+ promise
81
+ .then(size => success(size.width, size.height))
82
+ .catch((error) => {
83
+ if (typeof failure === 'function')
84
+ failure(error);
85
+ else
86
+ dlog(`Image.getSizeWithHeaders failed for ${uri}: ${String(error)}`);
87
+ });
88
+ }
89
+ return promise;
90
+ }
91
+ // Android keys an in-flight prefetch by a monotonic requestId (so abortRequest can cancel it);
92
+ // RN's Image.android.js generates the same way. iOS ignores the arg.
93
+ let prefetchRequestId = 0;
94
+ // Download a remote image into the disk cache. Resolves to whether it succeeded. `callback`
95
+ // receives the requestId (RN's Image.android.js shape) so the caller can later pass it to
96
+ // abortPrefetch.
97
+ async function prefetch(uri, callback) {
98
+ prefetchRequestId += 1;
99
+ const requestId = prefetchRequestId;
100
+ if (typeof callback === 'function')
101
+ callback(requestId);
102
+ const loader = requireLoader('prefetch');
103
+ return (Promise.resolve()
104
+ // Android's prefetchImage keys an abortable request on requestId; iOS takes ONLY the uri and
105
+ // throws on an extra arg (bridgeless TurboModule arg-count check). Match RN's per-platform call.
106
+ .then(() => Platform.OS === 'android'
107
+ ? loader.prefetchImage(uri, requestId)
108
+ : loader.prefetchImage(uri))
109
+ .then(result => result === true)
110
+ .catch((error) => {
111
+ dlog(`Image.prefetch failed for ${uri}: ${String(error)}`);
112
+ throw error;
113
+ }));
114
+ }
115
+ // Cancel an in-flight prefetch by the requestId prefetch handed back. Android only (mirrors
116
+ // Image.android.js -> NativeImageLoaderAndroid.abortRequest); a missing abortRequest (iOS,
117
+ // headless) is a no-op rather than a throw.
118
+ function abortPrefetch(requestId) {
119
+ const loader = getImageLoader();
120
+ if (loader === null || typeof loader.abortRequest !== 'function') {
121
+ dlog(`Image.abortPrefetch(${requestId}): no abortRequest on this host, ignoring`);
122
+ return;
123
+ }
124
+ loader.abortRequest(requestId);
125
+ }
126
+ // Narrow native's queryCache result: an object mapping each known uri to its cache status.
127
+ // Unknown statuses are dropped rather than trusted blindly.
128
+ const CACHE_STATUS = {
129
+ memory: 'memory',
130
+ disk: 'disk',
131
+ 'disk/memory': 'disk/memory',
132
+ };
133
+ function toCacheRecord(result) {
134
+ const record = {};
135
+ if (typeof result !== 'object' || result === null)
136
+ return record;
137
+ for (const key of Object.keys(result)) {
138
+ const value = Reflect.get(result, key);
139
+ if (typeof value === 'string' && Object.hasOwn(CACHE_STATUS, value)) {
140
+ record[key] = CACHE_STATUS[value];
141
+ }
142
+ }
143
+ return record;
144
+ }
145
+ async function queryCache(uris) {
146
+ return Promise.resolve()
147
+ .then(() => {
148
+ const loader = requireLoader('queryCache');
149
+ // The native queryCache never rejects (RCTImageLoader resolves getImageCacheStatus), so a
150
+ // rejection here is a JS/native boundary fault: log whether the method is even callable and
151
+ // the arg shape, to tell "not a function" (interop gap) from a marshalling reject.
152
+ dlog(`Image.queryCache: typeof loader.queryCache=${typeof loader.queryCache} uris=${uris.length}`);
153
+ return loader.queryCache(uris);
154
+ })
155
+ .then(toCacheRecord)
156
+ .catch((error) => {
157
+ dlog(`Image.queryCache failed: ${String(error)}`);
158
+ throw error;
159
+ });
160
+ }
161
+ // PURE JS: run the currently-installed source resolver (the same machinery the Image component
162
+ // uses via resolveImageSource). RN's resolveAssetSource turns a require() asset id into
163
+ // {uri, scale, ...}; the app injects the real one with setImageSourceResolver, and this exposes
164
+ // its output to callers directly.
165
+ function resolveAssetSource(source) {
166
+ return resolveImageSource(source);
167
+ }
168
+ export const imageStatics = {
169
+ getSize,
170
+ getSizeWithHeaders,
171
+ prefetch,
172
+ abortPrefetch,
173
+ queryCache,
174
+ resolveAssetSource,
175
+ };
@@ -0,0 +1,9 @@
1
+ export declare function setImageSourceResolver(resolve: (source: unknown) => unknown): void;
2
+ export declare function resolveImageSource(source: unknown): unknown;
3
+ export type IImageSource = {
4
+ uri?: string;
5
+ scale?: number;
6
+ width?: number;
7
+ height?: number;
8
+ };
9
+ export type IImageSourceProp = IImageSource | IImageSource[] | number;
@@ -0,0 +1,16 @@
1
+ // Image source resolution seam: require('./x.png') asset ids and {uri} sources are resolved by
2
+ // RN's own resolveAssetSource before reaching the shared render fn. The actual resolution is
3
+ // RN-platform-specific, so it is injected here rather than imported, mirroring platform-color.ts's
4
+ // processColor seam - this keeps @symbiote-native/components free of a react-native dependency (and
5
+ // the headless harness working). BOTH the pure renderImage view (@symbiote-native/components) and
6
+ // this package's own image-loader statics (resolveAssetSource) call resolveImageSource; neither
7
+ // reaches into the mutable resolver directly.
8
+ let sourceResolver = source => source;
9
+ export function setImageSourceResolver(resolve) {
10
+ sourceResolver = resolve;
11
+ }
12
+ // Public mirror of RN's Image.resolveAssetSource: run a source through the injected resolver.
13
+ // Headless (no resolver wired) it is the identity, so smokes see the input unchanged.
14
+ export function resolveImageSource(source) {
15
+ return sourceResolver(source);
16
+ }
package/build/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
- export { createElement, createRawText, createAnchor, isAnchor, appendChild, insertBefore, removeChild, setProp, setEventListener, routeProp, getExplicitStyle, setText, isSymbioteNode, RAW_TEXT_COMPONENT, } from './node';
1
+ export { createElement, createRawText, createAnchor, isAnchor, appendChild, insertBefore, removeChild, setProp, setEventListener, routeProp, getExplicitStyle, setText, isSymbioteNode, isSymbioteEvent, RAW_TEXT_COMPONENT, } from './node';
2
2
  export { isEventFor } from './view-config';
3
3
  export { registerComponent, setNativeViewConfigSource } from './registry';
4
+ export { isRecord } from './type-guards';
4
5
  export { InteractionManager, Events as InteractionManagerEvents } from './interaction-manager';
5
6
  export type { IInteractionEvent, ISimpleTask, IPromiseTask, ITask, IHandle, ICancellable, } from './interaction-manager';
6
7
  export { default as PanResponder } from './pan-responder';
@@ -27,8 +28,10 @@ export type { IParsedBackgroundImage } from './process-background-image';
27
28
  export { flattenStyle } from './style';
28
29
  export type { IViewStyle, ITextStyle, IStyleProp, IDimensionValue, IFlexAlign, IFlexJustify, ITransformProp, IBoxShadowValue, IDropShadowValue, IFilterFunction, IColorStopValue, ILinearGradientValue, IRadialGradientValue, IRadialGradientPosition, IRadialGradientShape, IRadialGradientSize, IBackgroundImageValue, IBlendMode, } from './styles';
29
30
  export { StyleSheet, computeHairlineWidth } from './style-sheet';
30
- export { registerStyles, resolveClassName, clearGlobalStyles, scopeClassName, isClassNameValue, } from './style-registry';
31
- export type { IClassNameValue, IClassToggleMap, IScopableClassValue } from './style-registry';
31
+ export { registerStyles, resolveClassName, clearGlobalStyles, isClassNameValue, } from './style-registry';
32
+ export type { IClassNameValue } from './style-registry';
33
+ export { scopeClassName } from './style-registry/scope';
34
+ export type { IClassToggleMap, IScopableClassValue } from './style-registry/scope';
32
35
  export { Platform } from './platform';
33
36
  export type { IPlatformStatic, IPlatformOSType, IPlatformSelectSpec } from './platform';
34
37
  export type { IPlatformConstantsIOS } from './platform/index.ios';
@@ -45,6 +48,10 @@ export { Alert } from './alert';
45
48
  export type { IAlertType, IAlertButtonStyle, IAlertButton, IAlertButtons, IAlertOptions, } from './alert';
46
49
  export { Share } from './share';
47
50
  export type { IShareContent, IShareOptions, IShareAction } from './share';
51
+ export { imageStatics } from './image-loader';
52
+ export type { IImageStatics, IImageSize, IImageCacheStatus } from './image-loader';
53
+ export { setImageSourceResolver, resolveImageSource } from './image-source-resolver';
54
+ export type { IImageSource, IImageSourceProp } from './image-source-resolver';
48
55
  export { ActionSheetIOS } from './action-sheet-ios';
49
56
  export type { IActionSheetIOSOptions, IShareActionSheetIOSOptions, IShareActionSheetError, } from './action-sheet-ios';
50
57
  export { Linking } from './linking';
package/build/index.js CHANGED
@@ -2,9 +2,12 @@
2
2
  // Every framework adapter drives this tiny mutation API; all Fabric-specific
3
3
  // logic (tag allocation, view-name resolution, clone-on-write, event
4
4
  // normalization) lives behind it, in one place.
5
- export { createElement, createRawText, createAnchor, isAnchor, appendChild, insertBefore, removeChild, setProp, setEventListener, routeProp, getExplicitStyle, setText, isSymbioteNode, RAW_TEXT_COMPONENT, } from './node.js';
5
+ export { createElement, createRawText, createAnchor, isAnchor, appendChild, insertBefore, removeChild, setProp, setEventListener, routeProp, getExplicitStyle, setText, isSymbioteNode, isSymbioteEvent, RAW_TEXT_COMPONENT, } from './node.js';
6
6
  export { isEventFor } from './view-config.js';
7
7
  export { registerComponent, setNativeViewConfigSource } from './registry.js';
8
+ // Real cross-package consumer: core/components' KeyboardAvoidingView render narrows
9
+ // raw native payloads with this same guard, so it needs it off the package root.
10
+ export { isRecord } from './type-guards.js';
8
11
  // InteractionManager: pure JS (timers + emitter), framework-agnostic, so it lives
9
12
  // here; every adapter re-exports it.
10
13
  export { InteractionManager, Events as InteractionManagerEvents } from './interaction-manager/index.js';
@@ -30,7 +33,8 @@ export { processFontVariant } from './process-font-variant.js';
30
33
  export { processBackgroundImage } from './process-background-image/index.js';
31
34
  export { flattenStyle } from './style/index.js';
32
35
  export { StyleSheet, computeHairlineWidth } from './style-sheet/index.js';
33
- export { registerStyles, resolveClassName, clearGlobalStyles, scopeClassName, isClassNameValue, } from './style-registry/index.js';
36
+ export { registerStyles, resolveClassName, clearGlobalStyles, isClassNameValue, } from './style-registry/index.js';
37
+ export { scopeClassName } from './style-registry/scope.js';
34
38
  export { Platform } from './platform';
35
39
  export { dlog, isDebug } from './debug.js';
36
40
  export { getNativeModule, getEnforcingNativeModule } from './native-modules.js';
@@ -43,6 +47,11 @@ export { getSlot } from './fabric.js';
43
47
  // real device or simulator, not headless (a headless fake resolves any module name).
44
48
  export { Alert } from './alert';
45
49
  export { Share } from './share';
50
+ // Image statics (getSize/prefetch/queryCache/...): a stateful, native-bridge-touching module with
51
+ // no view of its own, same shape as Alert/Share. The source-resolution seam it shares with
52
+ // @symbiote-native/components' renderImage lives alongside it in image-source-resolver.
53
+ export { imageStatics } from './image-loader.js';
54
+ export { setImageSourceResolver, resolveImageSource } from './image-source-resolver.js';
46
55
  export { ActionSheetIOS } from './action-sheet-ios/index.js';
47
56
  export { Linking } from './linking';
48
57
  export { Vibration } from './vibration';
@@ -3,22 +3,11 @@
3
3
  // subscribes through a NativeEventEmitter bound to the KeyboardObserver native
4
4
  // module, which RN keys its keyboard events off of. Mirrors RN's
5
5
  // Libraries/Components/Keyboard/Keyboard.js, slimmed to the parts we need.
6
- import { getNativeModule } from '../native-modules.js';
7
- import { installDeviceEventHub, NativeEventEmitter, } from '../native-events.js';
6
+ import { createDeviceEventModule } from '../native-modules.js';
7
+ import {} from '../native-events.js';
8
8
  import { dlog } from '../debug.js';
9
9
  import { blurTextInput, currentlyFocusedInput } from '../text-input-state.js';
10
10
  import { LayoutAnimation } from '../layout-animation/index.js';
11
- // RN's scheduleLayoutAnimation falls back to the 'keyboard' animation type when the
12
- // event's easing string isn't a known LayoutAnimation type (Keyboard.js:200).
13
- const KEYBOARD_ANIMATION_TYPE = 'keyboard';
14
- // Map a raw easing string onto a ILayoutAnimationType without a cast: RN does
15
- // `LayoutAnimation.Types[easing] || 'keyboard'`, which only yields a real type when
16
- // the string is a key of the Types table. We mirror that by checking membership in
17
- // the frozen table before trusting the value.
18
- function easingToAnimationType(easing) {
19
- const types = LayoutAnimation.Types;
20
- return types[easing] ?? KEYBOARD_ANIMATION_TYPE;
21
- }
22
11
  // The native module name RN registers the keyboard observer under, confirmed
23
12
  // from its spec (specs_DEPRECATED/modules/INativeKeyboardObserver.js:20,
24
13
  // `TurboModuleRegistry.get('KeyboardObserver')`).
@@ -39,11 +28,6 @@ function isKeyboardEvent(payload) {
39
28
  typeof payload.endCoordinates === 'object' &&
40
29
  payload.endCoordinates !== null);
41
30
  }
42
- // Lazily resolved so importing this module has no native side effect: a headless
43
- // run without a fake __turboModuleProxy still loads it; resolution happens on the
44
- // first addListener. Null when the module isn't linked.
45
- let observer;
46
- let emitter;
47
31
  // The latest keyboardDidShow event, or null when the keyboard is hidden. RN's
48
32
  // `_currentlyShowing`. Kept fresh by an internal self-subscription (see getEmitter):
49
33
  // Keyboard listens to its OWN show/hide events and caches the event so the synchronous
@@ -68,19 +52,18 @@ function trackSubscription(eventType, subscription) {
68
52
  },
69
53
  };
70
54
  }
71
- function getEmitter() {
72
- if (emitter === undefined) {
73
- if (observer === undefined) {
74
- observer = getNativeModule(KEYBOARD_OBSERVER_MODULE);
75
- dlog(`Keyboard: KeyboardObserver module ${observer ? 'resolved' : 'NOT resolved (null)'}`);
76
- }
77
- // WHY lazy: install on the first subscribe rather than at module load, so the
78
- // hub exists before native emits without a hard bootstrap-order dependency for
79
- // this first cut. Idempotent, so repeated subscribes cost one boolean check.
80
- installDeviceEventHub();
81
- emitter = new NativeEventEmitter(observer ?? undefined);
82
- // Self-subscription: cache the latest show event, clear on hide (RN's constructor).
83
- // Bypasses trackSubscription so removeAllListeners never tears down the cache feed.
55
+ // Lazily resolved so importing this module has no native side effect: a headless
56
+ // run without a fake __turboModuleProxy still loads it; resolution happens on the
57
+ // first addListener. Null when the module isn't linked.
58
+ //
59
+ // The self-subscription policy that diverges from a plain lazy-resolve+emitter:
60
+ // Keyboard caches the latest show event, clearing on hide (RN's constructor), so
61
+ // isVisible()/metrics() read synchronously with no native round-trip. Bypasses
62
+ // trackSubscription so removeAllListeners never tears down the cache feed.
63
+ const deviceEventModule = createDeviceEventModule({
64
+ moduleName: KEYBOARD_OBSERVER_MODULE,
65
+ moduleLogPrefix: 'Keyboard: KeyboardObserver module',
66
+ onEmitterCreated: emitter => {
84
67
  emitter.addListener(KEYBOARD_EVENT.didShow, payload => {
85
68
  if (isKeyboardEvent(payload))
86
69
  currentlyShowing = payload;
@@ -88,8 +71,10 @@ function getEmitter() {
88
71
  emitter.addListener(KEYBOARD_EVENT.didHide, () => {
89
72
  currentlyShowing = null;
90
73
  });
91
- }
92
- return emitter;
74
+ },
75
+ });
76
+ function getEmitter() {
77
+ return deviceEventModule.getEmitter();
93
78
  }
94
79
  export const Keyboard = {
95
80
  addListener(eventType, listener) {
@@ -128,7 +113,7 @@ export const Keyboard = {
128
113
  dlog(`Keyboard.scheduleLayoutAnimation -> duration ${duration}, easing ${easing}`);
129
114
  LayoutAnimation.configureNext({
130
115
  duration,
131
- update: { duration, type: easingToAnimationType(easing) },
116
+ update: { duration, type: LayoutAnimation.coerceType(easing) },
132
117
  });
133
118
  },
134
119
  // RN's dismissKeyboard blurs the currently-focused input (TextInputState); blurring
@@ -61,6 +61,7 @@ declare class LayoutAnimationImpl {
61
61
  spring(onAnimationDidEnd?: IOnAnimationDidEndCallback): void;
62
62
  setLayoutAnimationEnabled(enabled: boolean): void;
63
63
  checkConfig(..._args: unknown[]): void;
64
+ coerceType(easing: string): ILayoutAnimationType;
64
65
  }
65
66
  export declare const LayoutAnimation: LayoutAnimationImpl;
66
67
  export {};