@symbiote-native/engine 0.1.4 → 0.1.6

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 (79) 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/index.d.ts +1 -1
  13. package/build/animated/index.js +1 -1
  14. package/build/animated/interpolation-node.d.ts +0 -1
  15. package/build/animated/interpolation-node.js +5 -4
  16. package/build/animated/native/native-animated.js +1 -3
  17. package/build/animated/operators.d.ts +14 -34
  18. package/build/animated/operators.js +19 -95
  19. package/build/animated/value.d.ts +2 -4
  20. package/build/animated/value.js +17 -5
  21. package/build/app-state/index.js +17 -22
  22. package/build/appearance/index.js +17 -25
  23. package/build/back-handler/index.js +15 -18
  24. package/build/commit.d.ts +1 -3
  25. package/build/commit.js +26 -209
  26. package/build/dimensions/index.js +25 -20
  27. package/build/events/index.js +6 -151
  28. package/build/fabric-props.d.ts +3 -0
  29. package/build/fabric-props.js +186 -0
  30. package/build/i18n-manager/index.js +3 -5
  31. package/build/image-loader/index.d.ts +24 -0
  32. package/build/image-loader/index.js +175 -0
  33. package/build/image-loader.d.ts +24 -0
  34. package/build/image-loader.js +175 -0
  35. package/build/image-source-resolver/index.d.ts +9 -0
  36. package/build/image-source-resolver/index.js +16 -0
  37. package/build/image-source-resolver.d.ts +9 -0
  38. package/build/image-source-resolver.js +16 -0
  39. package/build/index.d.ts +10 -3
  40. package/build/index.js +12 -3
  41. package/build/keyboard/index.js +19 -34
  42. package/build/layout-animation/index.d.ts +1 -0
  43. package/build/layout-animation/index.js +53 -35
  44. package/build/native-modules/index.d.ts +18 -0
  45. package/build/native-modules/index.js +120 -0
  46. package/build/native-modules.d.ts +12 -0
  47. package/build/native-modules.js +64 -1
  48. package/build/node.d.ts +2 -0
  49. package/build/node.js +36 -1
  50. package/build/pan-responder/index.js +1 -3
  51. package/build/permissions-android/index.js +1 -3
  52. package/build/{platform-color.d.ts → platform-color/index.d.ts} +3 -0
  53. package/build/platform-color/index.js +49 -0
  54. package/build/process-background-image/index.js +7 -14
  55. package/build/process-box-shadow/index.js +1 -5
  56. package/build/process-filter.js +2 -8
  57. package/build/registry.js +3 -5
  58. package/build/settings/index.js +1 -3
  59. package/build/share/index.ios.js +1 -1
  60. package/build/status-bar/index.android.js +1 -1
  61. package/build/style/index.js +2 -4
  62. package/build/style-registry/index.d.ts +1 -3
  63. package/build/style-registry/index.js +15 -48
  64. package/build/style-registry/scope/index.d.ts +3 -0
  65. package/build/style-registry/scope/index.js +41 -0
  66. package/build/style-registry/scope.d.ts +3 -0
  67. package/build/style-registry/scope.js +41 -0
  68. package/build/touch-history/index.d.ts +23 -0
  69. package/build/touch-history/index.js +150 -0
  70. package/build/touch-history.d.ts +23 -0
  71. package/build/touch-history.js +150 -0
  72. package/build/type-guards/index.d.ts +4 -0
  73. package/build/type-guards/index.js +15 -0
  74. package/build/type-guards.d.ts +4 -0
  75. package/build/type-guards.js +15 -0
  76. package/package.json +1 -1
  77. package/build/platform-color.js +0 -25
  78. /package/build/animated/{animated-component-shared.d.ts → shared.d.ts} +0 -0
  79. /package/build/animated/{animated-component-shared.js → shared.js} +0 -0
package/build/commit.js CHANGED
@@ -13,26 +13,20 @@
13
13
  // references to specific child handles. That bubble is inherent to a persistent
14
14
  // tree and is exactly what React's own Fabric renderer does.
15
15
  import { getSlot, } from './fabric.js';
16
- import { createElement, isAnchor, RAW_TEXT_COMPONENT, VIRTUAL_TEXT_COMPONENT, } from './node.js';
16
+ import { createElement, debugNodeId, isAnchor, VIRTUAL_TEXT_COMPONENT, } from './node.js';
17
17
  import { dlog, isDebug } from './debug.js';
18
18
  import { flattenStyle } from './style/index.js';
19
- import { registeredProcessor } from './registry.js';
20
19
  import { nextTag } from './tags.js';
21
- import { isOpaqueColorValue } from './platform-color.js';
22
- import { processBoxShadow } from './process-box-shadow/index.js';
23
20
  import { registerPostCommit, runPostCommitHooks } from './post-commit.js';
24
- import { processFilter } from './process-filter.js';
25
- import { processTransformOrigin } from './process-transform-origin/index.js';
26
- import { processTransform } from './process-transform/index.js';
27
- import { processAspectRatio } from './process-aspect-ratio.js';
28
- import { processFontVariant } from './process-font-variant.js';
29
- import { processBackgroundImage } from './process-background-image/index.js';
21
+ import { fabricProps } from './fabric-props.js';
22
+ import { isRecord } from './type-guards.js';
23
+ // processColor/setColorProcessor now live in ./platform-color (the stable color-processing
24
+ // leaf every color-touching module imports from); re-exported here so nothing outside this
25
+ // module needs to change its import path.
26
+ export { processColor, setColorProcessor } from './platform-color/index.js';
30
27
  // Per-commit work counters, surfaced via dlog so a device run can prove the
31
28
  // engine is incremental (created=0 with clones after the first mount).
32
29
  const stats = { created: 0, cloneProps: 0, cloneChildren: 0, reused: 0 };
33
- function isRecord(value) {
34
- return typeof value === 'object' && value !== null;
35
- }
36
30
  // Diagnostic (gated): Fabric serializes props to folly::dynamic, which rejects a JS
37
31
  // Symbol or function with "JS Symbols are not convertible to dynamic". A hard native
38
32
  // throw deep in cloneNode*. Walk a props payload and return the dotted path of the
@@ -84,202 +78,11 @@ function guardSerializable(propsDiff, viewName, tag) {
84
78
  if (bad !== undefined)
85
79
  dlog(`NON-SERIALIZABLE prop on ${viewName}#${tag}: ${bad}`);
86
80
  }
87
- // Color props must reach Fabric as platform ints, not CSS strings. Fabric's C++
88
- // color parser silently drops strings. The actual conversion (processColor) is
89
- // RN-platform-specific, so it is injected here rather than imported, keeping
90
- // shared free of a react-native dependency (and the headless harness working).
91
- const COLOR_PROPS = new Set([
92
- 'backgroundColor',
93
- 'color',
94
- 'borderColor',
95
- 'borderTopColor',
96
- 'borderRightColor',
97
- 'borderBottomColor',
98
- 'borderLeftColor',
99
- // Logical (writing-direction-relative) border colors + the block axis, all wired to
100
- // processColor in RN's ReactNativeStyleAttributes. borderStartColor/borderEndColor are
101
- // even publicly typed ColorValue, so they silently dropped on iOS / threw on Android.
102
- 'borderStartColor',
103
- 'borderEndColor',
104
- 'borderBlockColor',
105
- 'borderBlockStartColor',
106
- 'borderBlockEndColor',
107
- 'shadowColor',
108
- // Text shadow + the W3C `outline`/image `overlay` colors, also processColor in RN.
109
- 'textShadowColor',
110
- 'overlayColor',
111
- 'outlineColor',
112
- 'tintColor',
113
- // TextInput color props. iOS's native input accepts a CSS string, but Android's
114
- // AndroidTextInput is strict ("ColorValue: the value must be a number or Object"),
115
- // so these must be processColor'd here too, same as any other color reaching Fabric.
116
- 'placeholderTextColor',
117
- 'selectionColor',
118
- 'cursorColor',
119
- 'underlineColorAndroid',
120
- // Text decoration color (underline/strike): same Fabric strictness as any color.
121
- 'textDecorationColor',
122
- 'selectionHandleColor',
123
- // Switch track/thumb colors. RN processColors each via the Switch ViewConfig
124
- // (SwitchNativeComponent / AndroidSwitchNativeComponent validAttributes). iOS takes
125
- // onTintColor (ON) / tintColor (OFF); Android takes trackColorForTrue/False +
126
- // trackTintColor, and Android's ColorPropConverter is strict ("the value must be a
127
- // number or Object"), so a raw CSS string crashes. thumbTintColor reaches both.
128
- 'onTintColor',
129
- 'thumbTintColor',
130
- 'trackColorForTrue',
131
- 'trackColorForFalse',
132
- 'trackTintColor',
133
- ]);
134
- // Accepts a CSS string or an opaque PlatformColor / DynamicColorIOS object. RN's
135
- // processColor (the value the canary injects) handles both, resolving the opaque
136
- // shapes to the platform ints/dicts iOS UIColor expects.
137
- let colorProcessor = value => value;
138
- export function setColorProcessor(process) {
139
- colorProcessor = process;
140
- }
141
- // Public mirror of RN's processColor: run a color through the injected platform
142
- // processor (the canary wires RN's own). Off a real host it resolves CSS strings
143
- // and opaque PlatformColor objects to the platform ints Fabric expects; headless
144
- // (no processor wired) it is the identity, so smokes see the input unchanged.
145
- export function processColor(color) {
146
- return colorProcessor(color);
147
- }
148
- // A color-keyed value the platform processor must convert before Fabric: a CSS
149
- // string, or an opaque PlatformColor / DynamicColorIOS object. Numbers (already
150
- // platform ints) and undefined are left untouched.
151
- function isProcessableColor(value) {
152
- return typeof value === 'string' || isOpaqueColorValue(value);
153
- }
154
- // Structured CSS-style keys RN parses in JS before native (boxShadow/filter register
155
- // with enableNativeCSSParsing(), which DEFAULTS TO FALSE, so native CSS parsing is off
156
- // and the raw string is dropped). Each runs on the hoisted top-level style key, turning
157
- // a CSS string or structured array into the processed array Fabric's C++ expects.
158
- const STYLE_PROCESSORS = new Map([
159
- ['boxShadow', value => processBoxShadow(asBoxShadowInput(value))],
160
- ['filter', value => processFilter(asFilterInput(value))],
161
- ['transformOrigin', value => processTransformOrigin(asTransformOriginInput(value))],
162
- ['transform', processTransformValue],
163
- ['aspectRatio', value => processAspectRatio(asAspectRatioInput(value))],
164
- ['fontVariant', value => processFontVariant(asFontVariantInput(value))],
165
- ['experimental_backgroundImage', value => processBackgroundImage(asBackgroundImageInput(value))],
166
- ]);
167
- // boxShadow accepts a CSS string or an array of shadow objects; anything else is
168
- // undefined to processBoxShadow (which returns []). Narrowing avoids an `as` cast.
169
- function asBoxShadowInput(value) {
170
- if (typeof value === 'string')
171
- return value;
172
- if (Array.isArray(value))
173
- return value.filter(isRecord);
174
- return undefined;
175
- }
176
- // filter accepts a CSS string or an array of single-key filter objects; same narrowing.
177
- function asFilterInput(value) {
178
- if (typeof value === 'string')
179
- return value;
180
- if (Array.isArray(value))
181
- return value.filter(isRecord);
182
- return undefined;
183
- }
184
- // experimental_backgroundImage accepts a CSS string (gradient functions) or an array of
185
- // structured gradient objects; same narrowing as boxShadow/filter.
186
- function asBackgroundImageInput(value) {
187
- if (typeof value === 'string')
188
- return value;
189
- if (Array.isArray(value))
190
- return value.filter(isRecord);
191
- return undefined;
192
- }
193
- // transformOrigin accepts a CSS string or a [x, y, z] array of strings/numbers; anything
194
- // else is undefined to processTransformOrigin (which defaults to center/center/0).
195
- function asTransformOriginInput(value) {
196
- if (typeof value === 'string')
197
- return value;
198
- if (Array.isArray(value))
199
- return value.filter(isStringOrNumber);
200
- return undefined;
201
- }
202
- // aspectRatio accepts a number (the common, working form) or a ratio string; otherwise
203
- // undefined, which processAspectRatio drops.
204
- function asAspectRatioInput(value) {
205
- if (typeof value === 'number' || typeof value === 'string')
206
- return value;
207
- return undefined;
208
- }
209
- // fontVariant accepts an array of variant strings (the common, working form) or a
210
- // space-separated string; anything else becomes an empty string, which yields [].
211
- function asFontVariantInput(value) {
212
- if (typeof value === 'string')
213
- return value;
214
- if (Array.isArray(value))
215
- return value.filter(isString);
216
- return '';
217
- }
218
- // transform accepts a CSS string (processTransform parses it) or an array of single-key
219
- // transform records (the hot animated / sticky-header path, passed through unchanged).
220
- // A non-string non-array value is NOT dropped: it may already be processed, so it passes
221
- // through verbatim rather than being coerced to [] (which would erase a valid transform).
222
- function processTransformValue(value) {
223
- if (typeof value === 'string')
224
- return processTransform(value);
225
- if (Array.isArray(value))
226
- return processTransform(value.filter(isRecord));
227
- return value;
228
- }
229
- function isStringOrNumber(value) {
230
- return typeof value === 'string' || typeof value === 'number';
231
- }
232
- function isString(value) {
233
- return typeof value === 'string';
234
- }
235
- // Convert a prop to the shape Fabric's C++ expects. A third-party view contributes
236
- // its own processors, auto-derived from its ViewConfig (validAttributes[*].process,
237
- // e.g. processColor for a slider's track tints); those run first. Then the structured
238
- // CSS-style processors (boxShadow/filter). Built-ins are never in the registry, so they
239
- // fall through to the global color path, where any CSS-string color is run through the
240
- // injected platform processor (Fabric's C++ color parser silently drops strings).
241
- function processValue(component, key, value) {
242
- const processor = registeredProcessor(component, key);
243
- if (processor !== undefined)
244
- return processor(value);
245
- const styleProcessor = STYLE_PROCESSORS.get(key);
246
- if (styleProcessor !== undefined)
247
- return styleProcessor(value);
248
- if (COLOR_PROPS.has(key) && isProcessableColor(value))
249
- return colorProcessor(value);
250
- return value;
251
- }
252
81
  function viewNameFor(node, hasTextAncestor) {
253
82
  // The only position-dependent name: a <Text> inside another <Text> becomes a
254
83
  // virtual span. Everything else is the component string the adapter chose.
255
84
  return node.isText && hasTextAncestor ? VIRTUAL_TEXT_COMPONENT : node.component;
256
85
  }
257
- // Translate the retained node's logical props into the flat payload Fabric's C++
258
- // props expect: `style` keys are hoisted to the top level, event handlers and
259
- // undefined values are dropped.
260
- function fabricProps(node) {
261
- if (node.component === RAW_TEXT_COMPONENT) {
262
- return { text: node.props.text };
263
- }
264
- const out = {};
265
- for (const [key, value] of Object.entries(node.props)) {
266
- if (key === 'style')
267
- continue;
268
- if (typeof value === 'function')
269
- continue;
270
- if (value === undefined)
271
- continue;
272
- out[key] = processValue(node.component, key, value);
273
- }
274
- // Collapse style (object | array | nested arrays) into one flat payload before
275
- // hoisting: `style={[base, override]}` is RN's idiom and Fabric wants it flat.
276
- const style = flattenStyle(node.props.style);
277
- for (const [key, value] of Object.entries(style)) {
278
- if (value !== undefined)
279
- out[key] = processValue(node.component, key, value);
280
- }
281
- return out;
282
- }
283
86
  // Fabric's clone*WithNewProps MERGES the raw payload onto the node's existing props,
284
87
  // so the payload must be a MINIMAL diff: only the keys that actually changed, plus any
285
88
  // key the node held last time but no longer has, sent as `null` so Fabric resets it to
@@ -407,6 +210,13 @@ function reconcile(slot, node, rootTag, hasTextAncestor, renderableParent, force
407
210
  slot.appendChild(handle, reconcile(slot, child, rootTag, childInText, node, true).handle);
408
211
  }
409
212
  logScrollChildren(node, viewName, tag);
213
+ // Investigation instrumentation (search-bar-ref "node not committed" bug): scoped to RNS* so
214
+ // it can be directly compared against the ref-attach log in stack.ts and the dispatch-miss
215
+ // log below — same debugNodeId on both sides proves/disproves an identity mismatch. Kept
216
+ // behind DEBUG per <keep_logs_gate_behind_DEBUG>, never removed.
217
+ if (viewName.startsWith('RNS')) {
218
+ dlog(`mirror.set (create) node=${debugNodeId(node)} tag=${tag} view=${viewName}`);
219
+ }
410
220
  mirror.set(node, {
411
221
  handle,
412
222
  tag,
@@ -456,6 +266,11 @@ function reconcile(slot, node, rootTag, hasTextAncestor, renderableParent, force
456
266
  guardSerializable(propsDiff, viewName, committed.tag);
457
267
  handle = slot.cloneNodeWithNewProps(committed.handle, propsDiff);
458
268
  }
269
+ // Investigation instrumentation (search-bar-ref "node not committed" bug): see the create-path
270
+ // dlog above. Kept behind DEBUG per <keep_logs_gate_behind_DEBUG>, never removed.
271
+ if (viewName.startsWith('RNS')) {
272
+ dlog(`mirror.set (update) node=${debugNodeId(node)} tag=${committed.tag} view=${viewName}`);
273
+ }
459
274
  // The clone keeps the node's family, so its reactTag is unchanged; carry it.
460
275
  mirror.set(node, {
461
276
  handle,
@@ -500,7 +315,7 @@ function rootContainerFor(rootTag) {
500
315
  // from scratch (fresh tags, fresh mirror) instead of cloning handles that belonged to a
501
316
  // now-stopped surface. Called from unmount (the bridgeless surface-stop path): the host stops then restarts a
502
317
  // surface (Fast Refresh, focus/lifecycle) reusing the same rootTag, and a stale root
503
- // container would re-clone dead handles into the new surface → a blank screen. The old
318
+ // container would re-clone dead handles into the new surface -> a blank screen. The old
504
319
  // container's descendants fall out of every reference and their mirror entries GC.
505
320
  export function disposeRoot(rootTag) {
506
321
  if (rootContainers.delete(rootTag))
@@ -514,7 +329,7 @@ export function commitChildren(rootTag, children) {
514
329
  }
515
330
  // Re-run the scoped commit for a surface from its synthetic root container, reusing
516
331
  // whatever top-level children it currently holds. The shared half of the engine: both
517
- // a full mutation→commit and a single-node Animated frame (setNativeProps) funnel here.
332
+ // a full mutation->commit and a single-node Animated frame (setNativeProps) funnel here.
518
333
  function commitContainer(rootTag) {
519
334
  const slot = getSlot();
520
335
  const container = rootContainerFor(rootTag);
@@ -598,7 +413,7 @@ registerPostCommit(() => {
598
413
  pendingCommitWaiters.delete(waiter);
599
414
  }
600
415
  });
601
- // Run `action` once `node` has a committed Fabric tag — immediately if it already does, else after
416
+ // Run `action` once `node` has a committed Fabric tag - immediately if it already does, else after
602
417
  // the commit that assigns it. The canonical fix for the Vue async-commit race: defer instead of
603
418
  // silently no-opping. Returns a cancel fn (drop the pending retry, e.g. on unmount).
604
419
  export function whenCommitted(node, action) {
@@ -625,10 +440,12 @@ export function getNativeNode(node) {
625
440
  export function dispatchViewCommand(node, commandName, args) {
626
441
  const record = mirror.get(node);
627
442
  if (record === undefined) {
628
- dlog(`dispatchViewCommand "${commandName}" skipped: node not committed`);
443
+ // node=... compares directly against the mirror.set logs above (same debugNodeId scheme) to
444
+ // prove/disprove a node-identity mismatch — see the search-bar-ref investigation note there.
445
+ dlog(`dispatchViewCommand "${commandName}" skipped: node not committed (node=${debugNodeId(node)} component=${node.component})`);
629
446
  return;
630
447
  }
631
- dlog(`dispatchViewCommand "${commandName}"`);
448
+ dlog(`dispatchViewCommand "${commandName}" (node=${debugNodeId(node)})`);
632
449
  getSlot().dispatchCommand(record.handle, commandName, args);
633
450
  }
634
451
  // Emit an accessibility event (focus/click/viewHoverEnter/windowStateChange) at a node's
@@ -11,9 +11,10 @@
11
11
  // We resolve it through the same generic native-module bridge as Platform
12
12
  // (getNativeModule), so this module stays importable headless before a fake proxy
13
13
  // is installed.
14
- import { getNativeModule } from '../native-modules.js';
15
- import { installDeviceEventHub, NativeEventEmitter, } from '../native-events.js';
14
+ import { createDeviceEventModule } from '../native-modules.js';
15
+ import {} from '../native-events.js';
16
16
  import { dlog } from '../debug.js';
17
+ import { isRecord } from '../type-guards.js';
17
18
  // The native module name RN registers device metrics under.
18
19
  const DEVICE_INFO_MODULE = 'DeviceInfo';
19
20
  // The device event native emits when metrics change; its payload is a fresh
@@ -32,9 +33,6 @@ const ZERO_METRICS = {
32
33
  // The trust boundary: native getConstants() crosses from an untyped HostObject into
33
34
  // our types here, behind a structural guard (no per-call cast). A shape that fails
34
35
  // the guard is treated as "module absent".
35
- function isRecord(value) {
36
- return typeof value === 'object' && value !== null;
37
- }
38
36
  function isDimensionsConstants(value) {
39
37
  return isRecord(value) && isRecord(value.Dimensions);
40
38
  }
@@ -86,20 +84,19 @@ function setDimensions(payload) {
86
84
  for (const listener of [...changeListeners])
87
85
  listener(cached);
88
86
  }
89
- // Resolve initial metrics lazily and subscribe to native updates once. Re-attempts
90
- // on each call until a valid module is cached, so a later-installed DeviceInfo still
91
- // gets picked up (same pattern as platform.ts).
92
- let updateSubscription;
93
- function ensureResolved() {
94
- if (cached !== undefined)
95
- return cached;
96
- // Subscribe BEFORE reading constants so an update fired in between isn't missed
97
- // (RN orders the addListener before the getConstants call for the same reason).
98
- if (updateSubscription === undefined) {
99
- const module = getNativeModule(DEVICE_INFO_MODULE);
100
- installDeviceEventHub();
101
- const emitter = new NativeEventEmitter(undefined);
102
- updateSubscription = emitter.addListener(DID_UPDATE_DIMENSIONS, payload => {
87
+ // The policy that diverges from a plain lazy-resolve+emitter: DeviceInfo carries no
88
+ // observe-counters (only getConstants), so it never binds into the emitter -
89
+ // bindModuleToEmitter stays false, matching the original `new
90
+ // NativeEventEmitter(undefined)`. And the subscribe-BEFORE-reading-constants order
91
+ // (native could push an update between the two, RN orders addListener before
92
+ // getConstants for the same reason) lives in onEmitterCreated, which runs addListener
93
+ // first and only then touches getConstants() - same order as before the extraction.
94
+ const deviceEventModule = createDeviceEventModule({
95
+ moduleName: DEVICE_INFO_MODULE,
96
+ moduleLogPrefix: 'Dimensions: module',
97
+ bindModuleToEmitter: false,
98
+ onEmitterCreated: (emitter, module) => {
99
+ emitter.addListener(DID_UPDATE_DIMENSIONS, payload => {
103
100
  if (isDimensionsPayload(payload))
104
101
  setDimensions(payload);
105
102
  });
@@ -116,7 +113,15 @@ function ensureResolved() {
116
113
  dlog('Dimensions: DeviceInfo.getConstants() returned an unexpected shape — using zero metrics');
117
114
  }
118
115
  }
119
- }
116
+ },
117
+ });
118
+ // Resolve initial metrics lazily and subscribe to native updates once. Re-attempts
119
+ // on each call until a valid module is cached, so a later-installed DeviceInfo still
120
+ // gets picked up (same pattern as platform.ts).
121
+ function ensureResolved() {
122
+ if (cached !== undefined)
123
+ return cached;
124
+ deviceEventModule.getEmitter();
120
125
  return cached ?? { window: ZERO_METRICS, screen: ZERO_METRICS };
121
126
  }
122
127
  export const Dimensions = {
@@ -10,11 +10,10 @@ import { runWrapped } from '../dispatch.js';
10
10
  import { getSlot } from '../fabric.js';
11
11
  import { isAnchor, isSymbioteNode } from '../node.js';
12
12
  import { registeredNativeEvent } from '../registry.js';
13
- // Raw Fabric event name -> listener name. Generic bubbling events live here; press
14
- // is synthesized from a touch sequence and layout is direct, so both are handled
15
- // outside this table.
13
+ import { attachTouchHistory, recordTouchTrack, resetTouchHistory, touchHistory, } from '../touch-history.js';
14
+ import { isRecord } from '../type-guards.js';
16
15
  // Raw Fabric event -> listener name, split by dispatch phase. Press is synthesized
17
- // from a touch sequence (handled separately below); everything else is table-driven.
16
+ // from a touch sequence and layout is direct, so neither lives in this table.
18
17
  // Bubbling events walk target -> root; direct events fire only on the target.
19
18
  const BUBBLING_EVENTS = {
20
19
  topFocus: 'focus',
@@ -92,146 +91,6 @@ const DEFAULT_LONG_PRESS_MS = 500;
92
91
  // Pressability cancels the pending long press when the touch drifts past this many
93
92
  // points from where it started (Pressability.DEFAULT_LONG_PRESS_DEACTIVATION_DISTANCE).
94
93
  const LONG_PRESS_DEACTIVATION_DISTANCE = 10;
95
- // RN's bank is indexed by touch identifier and warns above 20; we never warn (headless
96
- // events may carry larger or absent ids), we just skip anything out of a sane range.
97
- const MAX_TOUCH_BANK = 20;
98
- const touchBank = [];
99
- const touchHistory = {
100
- touchBank,
101
- numberActiveTouches: 0,
102
- indexOfSingleActiveTouch: -1,
103
- mostRecentTimeStamp: 0,
104
- };
105
- function toFiniteNumber(value) {
106
- return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
107
- }
108
- // Pull a recordable touch out of an untyped entry, or undefined when it lacks a usable
109
- // identifier or coordinates. RN's getTouchIdentifier throws on a null id; we skip
110
- // instead, so events without touch geometry leave the bank untouched.
111
- function normalizeTouch(raw) {
112
- if (!isRecord(raw))
113
- return undefined;
114
- const identifier = toFiniteNumber(raw.identifier);
115
- const pageX = toFiniteNumber(raw.pageX);
116
- const pageY = toFiniteNumber(raw.pageY);
117
- if (identifier === undefined || pageX === undefined || pageY === undefined)
118
- return undefined;
119
- if (identifier < 0 || identifier > MAX_TOUCH_BANK)
120
- return undefined;
121
- return { identifier, pageX, pageY, timestamp: toFiniteNumber(raw.timestamp) ?? 0 };
122
- }
123
- // The changed touches for this frame (start/move/end), defensively read.
124
- function changedTouchesOf(nativeEvent) {
125
- const raw = nativeEvent.changedTouches;
126
- if (!Array.isArray(raw))
127
- return [];
128
- const out = [];
129
- for (const entry of raw) {
130
- const touch = normalizeTouch(entry);
131
- if (touch !== undefined)
132
- out.push(touch);
133
- }
134
- return out;
135
- }
136
- // Count of all touches still down (RN reads nativeEvent.touches.length directly).
137
- function activeTouchCount(nativeEvent) {
138
- const raw = nativeEvent.touches;
139
- return Array.isArray(raw) ? raw.length : 0;
140
- }
141
- function recordTouchStart(touch) {
142
- const record = touchBank[touch.identifier];
143
- if (record) {
144
- record.touchActive = true;
145
- record.startPageX = touch.pageX;
146
- record.startPageY = touch.pageY;
147
- record.startTimeStamp = touch.timestamp;
148
- record.currentPageX = touch.pageX;
149
- record.currentPageY = touch.pageY;
150
- record.currentTimeStamp = touch.timestamp;
151
- record.previousPageX = touch.pageX;
152
- record.previousPageY = touch.pageY;
153
- record.previousTimeStamp = touch.timestamp;
154
- }
155
- else {
156
- touchBank[touch.identifier] = {
157
- touchActive: true,
158
- startPageX: touch.pageX,
159
- startPageY: touch.pageY,
160
- startTimeStamp: touch.timestamp,
161
- currentPageX: touch.pageX,
162
- currentPageY: touch.pageY,
163
- currentTimeStamp: touch.timestamp,
164
- previousPageX: touch.pageX,
165
- previousPageY: touch.pageY,
166
- previousTimeStamp: touch.timestamp,
167
- };
168
- }
169
- touchHistory.mostRecentTimeStamp = touch.timestamp;
170
- }
171
- // Move and end share the previous<-current shift; only `touchActive` differs.
172
- function shiftTouchRecord(touch, active) {
173
- const record = touchBank[touch.identifier];
174
- if (!record)
175
- return;
176
- record.touchActive = active;
177
- record.previousPageX = record.currentPageX;
178
- record.previousPageY = record.currentPageY;
179
- record.previousTimeStamp = record.currentTimeStamp;
180
- record.currentPageX = touch.pageX;
181
- record.currentPageY = touch.pageY;
182
- record.currentTimeStamp = touch.timestamp;
183
- touchHistory.mostRecentTimeStamp = touch.timestamp;
184
- }
185
- // Maintain the bank as a touch frame flows. Mirrors RN's recordTouchTrack: moveish
186
- // shifts records, startish records + recomputes numberActiveTouches, endish marks the
187
- // record inactive + rescans for the single remaining touch. `kind` is the touch phase.
188
- function recordTouchTrack(kind, nativeEvent) {
189
- if (kind === 'move') {
190
- for (const touch of changedTouchesOf(nativeEvent))
191
- shiftTouchRecord(touch, true);
192
- return;
193
- }
194
- if (kind === 'start') {
195
- for (const touch of changedTouchesOf(nativeEvent))
196
- recordTouchStart(touch);
197
- touchHistory.numberActiveTouches = activeTouchCount(nativeEvent);
198
- if (touchHistory.numberActiveTouches === 1) {
199
- const first = normalizeTouch(arrayFirst(nativeEvent.touches));
200
- touchHistory.indexOfSingleActiveTouch = first?.identifier ?? -1;
201
- }
202
- return;
203
- }
204
- for (const touch of changedTouchesOf(nativeEvent))
205
- shiftTouchRecord(touch, false);
206
- touchHistory.numberActiveTouches = activeTouchCount(nativeEvent);
207
- if (touchHistory.numberActiveTouches === 1) {
208
- for (let i = 0; i < touchBank.length; i++) {
209
- const record = touchBank[i];
210
- if (record !== undefined && record.touchActive) {
211
- touchHistory.indexOfSingleActiveTouch = i;
212
- break;
213
- }
214
- }
215
- }
216
- }
217
- function arrayFirst(value) {
218
- return Array.isArray(value) ? value[0] : undefined;
219
- }
220
- // Drop all touch state. Called on a fully-released / cancelled gesture so a stale bank
221
- // never leaks geometry into the next gesture's first frame.
222
- function resetTouchHistory() {
223
- touchBank.length = 0;
224
- touchHistory.numberActiveTouches = 0;
225
- touchHistory.indexOfSingleActiveTouch = -1;
226
- touchHistory.mostRecentTimeStamp = 0;
227
- }
228
- // Attach the live touch history onto the event the responder handlers receive, matching
229
- // ResponderEventPlugin.js (`grantEvent.touchHistory = ...`, etc.). PanResponder reads
230
- // it for the per-touch dx/vx math; handlers that ignore it are unaffected.
231
- function attachTouchHistory(nativeEvent) {
232
- nativeEvent.touchHistory = touchHistory;
233
- }
234
- // #endregion
235
94
  let installed = false;
236
95
  // Target of the in-flight touch, remembered at topTouchStart and consumed (or
237
96
  // cleared) at topTouchEnd / topTouchCancel.
@@ -279,14 +138,10 @@ function readTouchPoint(nativeEvent) {
279
138
  }
280
139
  return undefined;
281
140
  }
282
- // Narrow an unknown to a plain object so its properties can be read without a cast.
283
- function isRecord(value) {
284
- return typeof value === 'object' && value !== null;
285
- }
286
141
  // Whether any touch still down started inside the responder (its target IS the
287
142
  // responder or a descendant). RN's noResponderTouches walks nativeEvent.touches and
288
143
  // returns false the moment one is found; a release fires only when none remain. The
289
- // headless smokes fire with an empty `{}` event (no `touches`) → no remaining touch →
144
+ // headless smokes fire with an empty `{}` event (no `touches`) -> no remaining touch ->
290
145
  // release fires, preserving single-touch behavior. (ResponderEventPlugin.noResponder-
291
146
  // Touches + isAncestor.)
292
147
  function hasRemainingResponderTouch(responder, nativeEvent) {
@@ -635,7 +490,7 @@ function bubble(target, listenerName, nativeEvent) {
635
490
  const path = pathToRoot(target);
636
491
  for (let i = path.length - 1; i >= 0; i--) {
637
492
  const node = path[i];
638
- // Anchors (Angular's #anchor component hosts) never paint and have no native view — a
493
+ // Anchors (Angular's #anchor component hosts) never paint and have no native view. A
639
494
  // listener registered on one only exists because a framework's own output-binding
640
495
  // machinery (already delivered directly, e.g. Angular's EventEmitter.subscribe) also
641
496
  // registered it through Renderer2.listen. Bubbling into it would refire that same
@@ -655,7 +510,7 @@ function bubble(target, listenerName, nativeEvent) {
655
510
  }
656
511
  }
657
512
  // Bubble phase: target -> root, invoking each ancestor's plain listener. Anchors are
658
- // transparent here too (see the capture-phase comment above) — same event, same reason.
513
+ // transparent here too, same reason (see the capture-phase comment above).
659
514
  let node = target;
660
515
  while (node) {
661
516
  const listener = isAnchor(node) ? undefined : node.listeners?.get(listenerName);
@@ -0,0 +1,3 @@
1
+ import type { IFabricProps } from './fabric';
2
+ import { type ISymbioteNode } from './node';
3
+ export declare function fabricProps(node: ISymbioteNode): IFabricProps;