@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
@@ -9,20 +9,17 @@
9
9
  // these in the adapter, not in shared, like Keyboard / StatusBar).
10
10
  import { getNativeModule } from '../native-modules.js';
11
11
  import { dlog } from '../debug.js';
12
+ import { isRecord } from '../type-guards.js';
12
13
  // ---- native module routing ----------------------------------------------
13
- // DEVICE-VERIFY-PENDING: on bridgeless Fabric the layout-animation configure call
14
- // is exposed by RN through the UIManager surface (RN's non-Fabric path calls
15
- // `UIManager.configureNextLayoutAnimation`; the Fabric path routes the same args
16
- // onto `global.nativeFabricUIManager.configureNextLayoutAnimation`). The native
17
- // MODULE NAME is platform-specific and a headless fake answers to ANY name, so
18
- // the name below is the most plausible bridgeless candidate, NOT proven. Only the
19
- // simulator/device resolution log can confirm it; the fallback list is tried in
20
- // order so a wrong primary still resolves on a real host. Verify on-device before
21
- // trusting either name. See the `dlog` at the resolution seam below.
22
- const NATIVE_UI_MANAGER_NAME = {
23
- primary: 'UIManager',
24
- fallback: 'FabricUIManager',
25
- };
14
+ // RN's actual dual-path (LayoutAnimation.js): the Fabric global slot
15
+ // (`global.nativeFabricUIManager`, a JSI slot read directly, not a TurboModule
16
+ // lookup) is tried FIRST; only when it lacks `configureNextLayoutAnimation` does
17
+ // RN fall back to the TurboModule `UIManager.configureNextLayoutAnimation`
18
+ // (`TurboModuleRegistry.getEnforcing('UIManager')` in NativeUIManager.js; iOS's
19
+ // RCTUIManager.mm registers under the same bare "UIManager" name via
20
+ // RCT_EXPORT_MODULE()). There is only ONE correct TurboModule name; RN never
21
+ // registers a module under "FabricUIManager".
22
+ const NATIVE_UI_MANAGER_MODULE_NAME = 'UIManager';
26
23
  // ---- public type surface (ported from RN's ReactNativeTypes) -------------
27
24
  const ANIMATION_TYPE = {
28
25
  spring: 'spring',
@@ -38,28 +35,39 @@ const ANIMATION_PROPERTY = {
38
35
  scaleY: 'scaleY',
39
36
  scaleXY: 'scaleXY',
40
37
  };
41
- // Resolved FRESH on every configureNext, deliberately NOT memoized. The native module can
42
- // be absent at one moment and linked later, and a cached answer would pin the first result;
43
- // getNativeModule is a cheap proxy lookup, so per-call resolution costs nothing and stays
44
- // correct. (Memoizing here also broke the headless smoke, which flips a fake module on/off
45
- // in one process: a cached module survived the flip-off, so configureNext kept calling native
46
- // when the module was meant to be absent.)
38
+ // Narrows the Fabric global slot down to whether it ALSO carries the
39
+ // layout-animation hook. `fabric.ts`'s `IFabricSlot` deliberately omits this
40
+ // method (out of scope for the engine's own mutation API), so the slot's static
41
+ // type never has it either; this is the runtime feature-detect RN itself does
42
+ // (`FabricUIManager?.configureNextLayoutAnimation` in LayoutAnimation.js) before
43
+ // calling it, no `as` cast needed.
44
+ function hasConfigureNextLayoutAnimation(value) {
45
+ return isRecord(value) && typeof value.configureNextLayoutAnimation === 'function';
46
+ }
47
+ // Resolved FRESH on every configureNext, deliberately NOT memoized. Either mechanism can
48
+ // come and go at runtime (the Fabric global installs during bootstrap; a TurboModule can be
49
+ // absent at one moment and linked later), and a cached answer would pin the first result;
50
+ // both lookups are cheap, so per-call resolution costs nothing and stays correct.
51
+ // (Memoizing here also broke the headless smoke, which flips a fake module on/off in one
52
+ // process: a cached module survived the flip-off, so configureNext kept calling native when
53
+ // the module was meant to be absent.)
47
54
  function resolveUIManager() {
48
- // Try the most plausible bridgeless name first, then fall back. A wrong primary
49
- // resolves null on a real host (the bridgeless proxy returns null for an
50
- // unlinked name), so the fallback covers a misnamed primary.
51
- const candidates = [NATIVE_UI_MANAGER_NAME.primary, NATIVE_UI_MANAGER_NAME.fallback];
52
- for (const name of candidates) {
53
- const module = getNativeModule(name);
54
- if (module !== null) {
55
- // DEVICE-VERIFY-PENDING seam: this line on the simulator/device is the only
56
- // proof the chosen name is the real one. Keep it permanently (P0 logging gate).
57
- dlog(`LayoutAnimation: resolved native UIManager via "${name}"`);
58
- return module;
59
- }
55
+ // Mechanism 1: the Fabric global slot, read directly: a JSI global, not a
56
+ // TurboModule lookup.
57
+ const fabricUIManager = globalThis.nativeFabricUIManager;
58
+ if (hasConfigureNextLayoutAnimation(fabricUIManager)) {
59
+ dlog('LayoutAnimation: resolved native UIManager via the Fabric global slot');
60
+ return fabricUIManager;
60
61
  }
61
- dlog(`LayoutAnimation: no native UIManager resolved (tried ${candidates.join(', ')}); ` +
62
- `configureNext is a no-op (headless or module not linked)`);
62
+ // Mechanism 2: the TurboModule fallback (RN's non-Fabric path).
63
+ const module = getNativeModule(NATIVE_UI_MANAGER_MODULE_NAME);
64
+ if (module !== null) {
65
+ dlog(`LayoutAnimation: resolved native UIManager via "${NATIVE_UI_MANAGER_MODULE_NAME}"`);
66
+ return module;
67
+ }
68
+ dlog('LayoutAnimation: no native UIManager resolved (no Fabric global slot, ' +
69
+ `"${NATIVE_UI_MANAGER_MODULE_NAME}" module not linked); configureNext is a no-op ` +
70
+ '(headless or module not linked)');
63
71
  return null;
64
72
  }
65
73
  // ---- config builder ------------------------------------------------------
@@ -106,7 +114,7 @@ function setLayoutAnimationEnabled(value) {
106
114
  // ---- configureNext -------------------------------------------------------
107
115
  // Configures the next commit to be animated. NATIVE drives completion:
108
116
  // `onAnimationDidEnd` is passed straight through as the native success callback,
109
- // so it fires exactly when the native animation actually finishes, including
117
+ // so it fires exactly when the native animation finishes, including
110
118
  // when native extends it past `duration` (spring overshoot, OS slowdown,
111
119
  // reduce-motion). `onAnimationDidFail` fires only if native config parsing fails.
112
120
  // When no native module is linked (headless), this is a logged no-op; an app
@@ -140,7 +148,7 @@ function configureNext(config, onAnimationDidEnd, onAnimationDidFail) {
140
148
  onAnimationDidEnd?.();
141
149
  };
142
150
  dlog(`LayoutAnimation.configureNext: dispatching config (duration=${config.duration})`);
143
- // onError only fires if native config parsing fails; default to a no-op.
151
+ // Default to a no-op when the caller doesn't supply one.
144
152
  manager.configureNextLayoutAnimation(config, onComplete, onAnimationDidFail ?? (() => { }));
145
153
  }
146
154
  // ---- the LayoutAnimation facade ------------------------------------------
@@ -178,5 +186,15 @@ class LayoutAnimationImpl {
178
186
  checkConfig(..._args) {
179
187
  dlog('LayoutAnimation.checkConfig(...) has been disabled.');
180
188
  }
189
+ // Coerce an arbitrary string (e.g. a keyboard event's `easing` field) onto a known
190
+ // ILayoutAnimationType, falling back to 'keyboard' when it isn't a key of `Types`.
191
+ // Owned here, not by a caller: `Types` is this class's own frozen table, so by
192
+ // Information Expert the coercion belongs on the type it reads (Keyboard's
193
+ // scheduleLayoutAnimation is the first caller; RN's own
194
+ // `LayoutAnimation.Types[easing] || 'keyboard'` is the same rule).
195
+ coerceType(easing) {
196
+ const types = this.Types;
197
+ return types[easing] ?? ANIMATION_TYPE.keyboard;
198
+ }
181
199
  }
182
200
  export const LayoutAnimation = new LayoutAnimationImpl();
@@ -1,6 +1,18 @@
1
+ import { NativeEventEmitter } from './native-events';
1
2
  declare global {
2
3
  var __turboModuleProxy: (<T>(name: string) => T | null) | undefined;
3
4
  var nativeModuleProxy: Record<string, unknown> | undefined;
4
5
  }
5
6
  export declare function getNativeModule<T>(name: string): T | null;
6
7
  export declare function getEnforcingNativeModule<T>(name: string): T;
8
+ export interface IDeviceEventModuleConfig<TModule> {
9
+ moduleName: string;
10
+ moduleLogPrefix: string;
11
+ bindModuleToEmitter?: boolean;
12
+ onEmitterCreated?: (emitter: NativeEventEmitter, module: TModule | null) => void;
13
+ }
14
+ export interface IDeviceEventModule<TModule> {
15
+ getModule(): TModule | null;
16
+ getEmitter(): NativeEventEmitter;
17
+ }
18
+ export declare function createDeviceEventModule<TModule>(config: IDeviceEventModuleConfig<TModule>): IDeviceEventModule<TModule>;
@@ -5,10 +5,12 @@
5
5
  // one more client of the same global, exactly as `getSlot` is for the view tree.
6
6
  //
7
7
  // This is first-party access, for symbiote's own modules (StatusBarManager,
8
- // KeyboardObserver, …). Third-party RN packages import `TurboModuleRegistry` from
8
+ // KeyboardObserver, ...). Third-party RN packages import `TurboModuleRegistry` from
9
9
  // `'react-native'` and read the same global themselves; they do not go through
10
10
  // here.
11
11
  import { dlog } from './debug.js';
12
+ import { installDeviceEventHub, NativeEventEmitter, } from './native-events.js';
13
+ import { isRecord } from './type-guards.js';
12
14
  // The native module value crosses from an untyped HostObject into our types here;
13
15
  // the caller vouches for its shape via T (the single trust-boundary narrowing, no
14
16
  // per-call `as`). Native modules are always non-null objects.
@@ -55,3 +57,64 @@ export function getEnforcingNativeModule(name) {
55
57
  }
56
58
  return module;
57
59
  }
60
+ // ---- device-event module factory ------------------------------------------
61
+ //
62
+ // AccessibilityInfo (iOS + Android), AppState, Appearance, BackHandler, Keyboard,
63
+ // and Dimensions each hand-rolled the identical plumbing: lazily resolve a native
64
+ // module, lazily build a NativeEventEmitter bound to it, install the device-event
65
+ // hub on first subscribe. `createLinking` (linking/shared.ts) proved this factors
66
+ // out safely for Linking's iOS/Android split; this is the general form for every
67
+ // other lazy-module + emitter pair in the runtime-module layer.
68
+ //
69
+ // Each caller's DEGRADE POLICY stays its own, via config - the factory owns only
70
+ // the plumbing:
71
+ // - `bindModuleToEmitter` (default true): whether the resolved module is wired
72
+ // into the emitter so its addListener/removeListeners observe-counters get
73
+ // pinged. Dimensions' DeviceInfo module has no observe-counters, so it opts out
74
+ // (matching its original `new NativeEventEmitter(undefined)`).
75
+ // - `onEmitterCreated`: runs exactly once, right after the emitter is built -
76
+ // the hook for a caller's own permanent self-subscription (AppState/Appearance/
77
+ // BackHandler/Keyboard each keep a cache fresh or dispatch a chain this way) or
78
+ // one-time hydration from the module's constants (AppState's initial state,
79
+ // Dimensions' initial metrics - the latter also relies on this hook running
80
+ // `addListener` BEFORE the constants read, preserving the original
81
+ // subscribe-before-resolve ordering that guards against missing an update).
82
+ // A structural check, not a cast: narrows an arbitrary resolved module down to
83
+ // IEventEmitterModule only when it actually carries both observe-counter methods.
84
+ // Needed because `TModule` is unconstrained (Dimensions' INativeDeviceInfo doesn't
85
+ // extend IEventEmitterModule at all) - NativeEventEmitter itself re-checks the same
86
+ // shape internally, so this exists to satisfy the type system, not to change
87
+ // behavior.
88
+ function hasEventEmitterShape(value) {
89
+ return (isRecord(value) &&
90
+ typeof value.addListener === 'function' &&
91
+ typeof value.removeListeners === 'function');
92
+ }
93
+ // Build one module's lazy-resolve + lazy-emitter pair. Each call owns its own
94
+ // cache, so two callers (or two platform builds loaded together in one smoke) stay
95
+ // independent - the same guarantee `createLinking` documents.
96
+ export function createDeviceEventModule(config) {
97
+ let module;
98
+ let emitter;
99
+ function getModule() {
100
+ if (module === undefined) {
101
+ module = getNativeModule(config.moduleName);
102
+ dlog(`${config.moduleLogPrefix} ${module ? 'resolved' : 'NOT resolved (null)'}`);
103
+ }
104
+ return module;
105
+ }
106
+ function getEmitter() {
107
+ if (emitter === undefined) {
108
+ // WHY lazy: install on first subscribe so the hub exists before native
109
+ // emits, without a hard bootstrap-order dependency. Idempotent.
110
+ installDeviceEventHub();
111
+ const resolved = getModule();
112
+ const bindModule = config.bindModuleToEmitter ?? true;
113
+ const boundModule = bindModule && resolved !== null && hasEventEmitterShape(resolved) ? resolved : undefined;
114
+ emitter = new NativeEventEmitter(boundModule);
115
+ config.onEmitterCreated?.(emitter, resolved);
116
+ }
117
+ return emitter;
118
+ }
119
+ return { getModule, getEmitter };
120
+ }
package/build/node.d.ts CHANGED
@@ -10,6 +10,7 @@ export interface ISymbioteEvent {
10
10
  stopPropagation: () => void;
11
11
  }
12
12
  export type IListener = (event: ISymbioteEvent) => unknown;
13
+ export declare function isSymbioteEvent(value: unknown): value is ISymbioteEvent;
13
14
  export interface ISymbioteNode {
14
15
  readonly [BRAND]: true;
15
16
  readonly component: string;
package/build/node.js CHANGED
@@ -15,6 +15,15 @@ const BRAND = Symbol('symbiote.node');
15
15
  export const RAW_TEXT_COMPONENT = 'RCTRawText';
16
16
  export const TEXT_COMPONENT = 'RCTText';
17
17
  export const VIRTUAL_TEXT_COMPONENT = 'RCTVirtualText';
18
+ // Runtime guard narrowing `unknown` to ISymbioteEvent (no `as` cast). Lives with the interface
19
+ // it tests, so every adapter checking for a Symbiote event shares one guard instead of writing
20
+ // its own copy.
21
+ export function isSymbioteEvent(value) {
22
+ if (typeof value !== 'object' || value === null)
23
+ return false;
24
+ const nativeEvent = Reflect.get(value, 'nativeEvent');
25
+ return typeof nativeEvent === 'object' && nativeEvent !== null;
26
+ }
18
27
  export function createElement(component, isText = false) {
19
28
  return {
20
29
  [BRAND]: true,
@@ -12,12 +12,10 @@
12
12
  // changed ones on `changedTouches`), so the centroid/velocity math here reads
13
13
  // those directly while keeping RN's accumulate-deltas-over-time behavior.
14
14
  import { dlog } from '../debug.js';
15
+ import { isRecord } from '../type-guards.js';
15
16
  const SINGLE_TOUCH_COUNT = 1;
16
17
  // onShouldBlockNativeResponder defaults to true (RN: block native by default).
17
18
  const DEFAULT_BLOCK_NATIVE_RESPONDER = true;
18
- function isRecord(value) {
19
- return typeof value === 'object' && value !== null;
20
- }
21
19
  function toFiniteNumber(value) {
22
20
  return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
23
21
  }
@@ -19,6 +19,7 @@
19
19
  // requestMultiplePermissions(permissions): Promise<{[permission]: string}>
20
20
  import { getNativeModule } from '../native-modules.js';
21
21
  import { dlog } from '../debug.js';
22
+ import { isBoolean } from '../type-guards.js';
22
23
  // The native module name RN registers this TurboModule under
23
24
  // (`TurboModuleRegistry.get<Spec>('PermissionsAndroid')`). NOTE: a module name is
24
25
  // only provable on a real host (a headless fake answers to any name); this name
@@ -79,9 +80,6 @@ export const PERMISSIONS = Object.freeze({
79
80
  POST_NOTIFICATIONS: 'android.permission.POST_NOTIFICATIONS',
80
81
  NEARBY_WIFI_DEVICES: 'android.permission.NEARBY_WIFI_DEVICES',
81
82
  });
82
- function isBoolean(value) {
83
- return typeof value === 'boolean';
84
- }
85
83
  // Narrow the native string result into a known RESULTS value. Any unrecognized
86
84
  // string is passed through as a IPermissionStatus only when it matches; otherwise
87
85
  // we fall back to DENIED: a runtime guard at the trust boundary, not an `as`.
@@ -17,3 +17,6 @@ export type IColorValue = string | IOpaqueColorValue;
17
17
  export declare function PlatformColor(...names: string[]): IOpaqueColorValue;
18
18
  export declare function DynamicColorIOS(tuple: IDynamicColorIOSTuple): IOpaqueColorValue;
19
19
  export declare function isOpaqueColorValue(value: unknown): value is IOpaqueColorValue;
20
+ export declare function setColorProcessor(process: (value: IColorValue) => unknown): void;
21
+ export declare function processColor(color: IColorValue): unknown;
22
+ export declare function isProcessableColor(value: unknown): value is IColorValue;
@@ -23,3 +23,27 @@ export function DynamicColorIOS(tuple) {
23
23
  export function isOpaqueColorValue(value) {
24
24
  return typeof value === 'object' && value !== null && ('semantic' in value || 'dynamic' in value);
25
25
  }
26
+ // Color props must reach Fabric as platform ints, not CSS strings. Fabric's C++ color
27
+ // parser silently drops strings. The actual conversion (processColor) is RN-platform-specific,
28
+ // so it is injected here rather than imported, keeping shared free of a react-native dependency
29
+ // (and the headless harness working). This module is the sole owner of "run a value through the
30
+ // platform color processor" - every color-touching consumer (commit's fabricProps,
31
+ // process-box-shadow, process-filter, process-background-image, StatusBar android) imports
32
+ // processColor/isProcessableColor from here, never from commit.ts.
33
+ let colorProcessor = value => value;
34
+ export function setColorProcessor(process) {
35
+ colorProcessor = process;
36
+ }
37
+ // Public mirror of RN's processColor: run a color through the injected platform processor (the
38
+ // canary wires RN's own). Off a real host it resolves CSS strings and opaque PlatformColor
39
+ // objects to the platform ints Fabric expects; headless (no processor wired) it is the identity,
40
+ // so smokes see the input unchanged.
41
+ export function processColor(color) {
42
+ return colorProcessor(color);
43
+ }
44
+ // A color-keyed value the platform processor must convert before Fabric: a CSS string, or an
45
+ // opaque PlatformColor / DynamicColorIOS object. Numbers (already platform ints) and undefined
46
+ // are left untouched.
47
+ export function isProcessableColor(value) {
48
+ return typeof value === 'string' || isOpaqueColorValue(value);
49
+ }
@@ -1,15 +1,11 @@
1
1
  // JS-side port of RN's processBackgroundImage (Libraries/StyleSheet/processBackgroundImage.js).
2
2
  // Same root cause as boxShadow/filter/transform: `experimental_backgroundImage` registers with
3
3
  // enableNativeCSSParsing(), which DEFAULTS TO FALSE, so RN's stock path parses the CSS gradient
4
- // string / structured array in JS and sends only the processed array to native — Fabric's C++
4
+ // string / structured array in JS and sends only the processed array to native - Fabric's C++
5
5
  // never sees the raw string. This restores that missing JS parse.
6
- //
7
- // processColor is referenced from ../commit at RUNTIME only (inside function bodies), never at
8
- // module-init, so the cyclic import (commit -> here -> commit) has no TDZ hazard — same pattern
9
- // as process-box-shadow/process-filter.
10
- import { processColor } from '../commit.js';
11
- import { isOpaqueColorValue } from '../platform-color.js';
6
+ import { isOpaqueColorValue, processColor } from '../platform-color.js';
12
7
  import { dlog } from '../debug.js';
8
+ import { isRecord } from '../type-guards.js';
13
9
  // RN processBackgroundImage.js: pre-compiled patterns.
14
10
  const NEWLINE_REGEX = /\n/g;
15
11
  const GRADIENT_REGEX = /^(linear|radial)-gradient\(((?:\([^)]*\)|[^()])*)\)/;
@@ -23,9 +19,6 @@ const LINEAR_GRADIENT_DEFAULT_DIRECTION = { type: 'angle', value: 180 };
23
19
  const DEFAULT_RADIAL_SHAPE = 'ellipse';
24
20
  const DEFAULT_RADIAL_SIZE = 'farthest-corner';
25
21
  const DEFAULT_RADIAL_POSITION = { top: '50%', left: '50%' };
26
- function isRecord(value) {
27
- return typeof value === 'object' && value !== null;
28
- }
29
22
  function isStringOrNumber(value) {
30
23
  return typeof value === 'string' || typeof value === 'number';
31
24
  }
@@ -221,7 +214,7 @@ function processBackgroundImageArray(rawList) {
221
214
  //#region CSS string form
222
215
  // Split a `background-image` value on its TOP-LEVEL commas only (multiple gradients, or a
223
216
  // color function's internal commas like `rgba(0, 0, 0, .3)`, must not be confused for one
224
- // another) — depth-tracking instead of a lookahead regex because a gradient's own arg list can
217
+ // another) - depth-tracking instead of a lookahead regex because a gradient's own arg list can
225
218
  // itself contain nested parens (`linear-gradient(to right, rgba(0,0,0,.5), blue)`).
226
219
  function splitGradients(input) {
227
220
  const result = [];
@@ -320,7 +313,7 @@ function parseLinearGradientCSSString(gradientContent) {
320
313
  }
321
314
  // The `at <position>` clause of `radial-gradient(... at <position>, <color-stops>)`. Drains
322
315
  // `tokens` (the SAME queue the caller is walking) via `shift()`, mirroring RN's in-place mutation
323
- // of `firstPartTokens` — not a copy, so the caller sees it emptied after this returns.
316
+ // of `firstPartTokens` - not a copy, so the caller sees it emptied after this returns.
324
317
  function parseRadialGradientPositionTokens(tokens) {
325
318
  if (tokens.length === 0)
326
319
  return null;
@@ -511,7 +504,7 @@ function parseRadialGradientCSSString(gradientContent) {
511
504
  position = parsedPosition;
512
505
  break;
513
506
  }
514
- // No shape/size/position token found in this iteration — the rest of the first part is a
507
+ // No shape/size/position token found in this iteration - the rest of the first part is a
515
508
  // color stop, not gradient config.
516
509
  if (!hasShapeSizeOrPositionString)
517
510
  break;
@@ -545,7 +538,7 @@ function parseBackgroundImageCSSString(cssString) {
545
538
  return gradients;
546
539
  }
547
540
  //#endregion CSS string form
548
- // RN processBackgroundImage.js's default export. Returns [] on any invalid gradient — web
541
+ // RN processBackgroundImage.js's default export. Returns [] on any invalid gradient - web
549
542
  // semantics: an invalid `background-image` paints none of it rather than a partial gradient.
550
543
  export function processBackgroundImage(backgroundImage) {
551
544
  if (backgroundImage == null)
@@ -4,11 +4,7 @@
4
4
  // processed array to native; Fabric's C++ never sees the raw string. symbiote was
5
5
  // forwarding the raw value, which native (CSS parsing off) silently ignores: shadow
6
6
  // rendered nothing. This restores the missing JS parse + per-color processColor.
7
- //
8
- // processColor is referenced from ./commit at RUNTIME only (inside the function bodies),
9
- // never at module-init, so the cyclic import (commit -> here -> commit) has no TDZ hazard.
10
- import { processColor } from '../commit.js';
11
- import { isOpaqueColorValue } from '../platform-color.js';
7
+ import { isOpaqueColorValue, processColor } from '../platform-color.js';
12
8
  import { dlog } from '../debug.js';
13
9
  // RN processBoxShadow.js:16-19: split args only on the delimiters that are NOT inside
14
10
  // a parenthesized color like rgba(0,0,0,1).
@@ -4,12 +4,9 @@
4
4
  // structured result. symbiote forwarded the raw value; the array form already worked
5
5
  // (Fabric accepts it raw, `filter:[{brightness:0.5}]` was device-verified), but the
6
6
  // CSS-string form and drop-shadow color processing were missing. This restores them.
7
- //
8
- // processColor is referenced from ./commit at RUNTIME only (inside function bodies),
9
- // never at module-init, so the cyclic import (commit -> here -> commit) has no TDZ hazard.
10
- import { processColor } from './commit.js';
11
- import { isOpaqueColorValue } from './platform-color.js';
7
+ import { isOpaqueColorValue, processColor } from './platform-color.js';
12
8
  import { dlog } from './debug.js';
9
+ import { isRecord } from './type-guards.js';
13
10
  // RN processFilter.js:19-24: pre-compiled patterns.
14
11
  const NEWLINE_REGEX = /\n/g;
15
12
  const FILTER_FUNCTION_REGEX = /([\w-]+)\(([^()]*|\([^()]*\)|[^()]*\([^()]*\)[^()]*)\)/g;
@@ -17,9 +14,6 @@ const ARGS_WITH_UNITS_REGEX = /([+-]?\d*(\.\d+)?)([a-zA-Z%]+)?/g;
17
14
  const WHITESPACE_SPLIT_REGEX = /\s+(?![^(]*\))/;
18
15
  const LENGTH_PARSE_REGEX = /([+-]?\d*(\.\d+)?)([\w\W]+)?/g;
19
16
  const RADIANS_TO_DEGREES = 180 / Math.PI;
20
- function isRecord(value) {
21
- return typeof value === 'object' && value !== null;
22
- }
23
17
  // A length field may be a CSS string or a number; resolve to a number or null.
24
18
  function resolveLength(value) {
25
19
  if (typeof value === 'string')
package/build/registry.js CHANGED
@@ -11,13 +11,14 @@
11
11
  // shared must stay react-native-free (the headless harness runs in plain Node), so
12
12
  // the ViewConfig lookup is INJECTED, exactly like the color processor: the adapter
13
13
  // wires `setNativeViewConfigSource(ReactNativeViewConfigRegistry.get)` on a real
14
- // host (on a real host that one source covers BOTH RN core and every library).
14
+ // host, where that one source covers BOTH RN core and every library.
15
15
  //
16
16
  // The ONLY explicit list is OUR OWN built-in primitives (BUILTIN_COMPONENTS): a
17
17
  // finite set we own, which keep their hand-tuned tables (view-config events, commit
18
18
  // COLOR_PROPS) and are never read from the source, so they can't drift. Everything
19
19
  // NOT in that set derives. The list never grows with the community; it grows only
20
20
  // when we add a core primitive of our own.
21
+ import { isRecord } from './type-guards.js';
21
22
  const EMPTY = { listeners: new Set(), byRaw: new Map(), processors: new Map() };
22
23
  // OUR own primitives: the finite set shared hand-tunes (view-config events,
23
24
  // commit COLOR_PROPS). The source is never consulted for these, so they can't
@@ -59,9 +60,6 @@ export function registerComponent(name, registration = {}) {
59
60
  list.push(registration);
60
61
  resolvedCache.delete(name);
61
62
  }
62
- function isRecord(value) {
63
- return typeof value === 'object' && value !== null;
64
- }
65
63
  // onChange -> change (mirrors node.ts listenerName; the split of the handler prop).
66
64
  function splitListener(handlerProp) {
67
65
  return handlerProp.charAt(2).toLowerCase() + handlerProp.slice(3);
@@ -92,7 +90,7 @@ function deriveFromConfig(config, into) {
92
90
  const attribute = validAttributes[key];
93
91
  if (isRecord(attribute)) {
94
92
  const process = attribute.process;
95
- // The codegen config already carries the right processor (processColor, …);
93
+ // The codegen config already carries the right processor (processColor, ...);
96
94
  // wrap it so the typed Function becomes a PropProcessor without a cast.
97
95
  if (typeof process === 'function')
98
96
  into.processors.set(key, value => process(value));
@@ -8,6 +8,7 @@
8
8
  import { installDeviceEventHub, NativeEventEmitter, } from '../native-events.js';
9
9
  import { getNativeModule } from '../native-modules.js';
10
10
  import { dlog } from '../debug.js';
11
+ import { isRecord } from '../type-guards.js';
11
12
  // The iOS native module name RN registers this under. NOTE: this is the name the
12
13
  // iOS JS wrapper resolves via `TurboModuleRegistry.getEnforcing('SettingsManager')`.
13
14
  // The spec filename is `INativeSettingsManager`. A module name is only provable on a
@@ -19,9 +20,6 @@ const SETTINGS_UPDATED_EVENT = 'settingsUpdated';
19
20
  // watchIds are handed out as the registry index, so the first watcher is 0. A
20
21
  // cleared slot keeps its index (the array never shrinks) so later ids stay stable.
21
22
  const FIRST_WATCH_ID = 0;
22
- function isRecord(value) {
23
- return typeof value === 'object' && value !== null;
24
- }
25
23
  // Lazily resolved so importing this module has no native side effect. `null` when
26
24
  // the module isn't linked (headless): the snapshot then starts empty and `set`
27
25
  // updates only JS state + fires watchers.
@@ -6,8 +6,8 @@
6
6
  // web/headless.
7
7
  import { dlog } from '../debug.js';
8
8
  import { getNativeModule } from '../native-modules.js';
9
+ import { ACTION_SHEET_MANAGER, } from '../action-sheet-ios/index.js';
9
10
  import { validateContent, shareActions, SHARED_ACTION, DISMISSED_ACTION } from './shared.js';
10
- const ACTION_SHEET_MANAGER = 'ActionSheetManager';
11
11
  export const Share = {
12
12
  ...shareActions,
13
13
  // Open the iOS share sheet for `content`. Resolves with the user's action
@@ -11,8 +11,8 @@
11
11
  // relayout survives and the bar updates without blanking (verified on device: show/hide +
12
12
  // light/dark text). See render.ts's installStopSurfaceGlobal.
13
13
  import { getNativeModule } from '../native-modules.js';
14
- import { processColor } from '../commit.js';
15
14
  import { dlog } from '../debug.js';
15
+ import { processColor } from '../platform-color.js';
16
16
  import { STATUS_BAR_MANAGER } from './shared.js';
17
17
  // processColor returns `unknown` (its result is platform-dependent); narrow to the
18
18
  // number Fabric/native expects, like RN's invariant before setColor. A non-number
@@ -7,9 +7,7 @@
7
7
  // POSITION only, never on a property VALUE. `transform: [{translateX: 5}]` is an
8
8
  // array-valued prop and `shadowOffset: {width, height}` an object-valued prop; both
9
9
  // are copied through untouched. Only the top-level style slot is flattened.
10
- function isPlainObject(value) {
11
- return typeof value === 'object' && value !== null && !Array.isArray(value);
12
- }
10
+ import { isRecord } from '../type-guards.js';
13
11
  export function flattenStyle(style) {
14
12
  if (Array.isArray(style)) {
15
13
  const result = {};
@@ -22,7 +20,7 @@ export function flattenStyle(style) {
22
20
  }
23
21
  return result;
24
22
  }
25
- if (isPlainObject(style)) {
23
+ if (isRecord(style)) {
26
24
  // Shallow copy of own enumerable keys: values (arrays, nested objects) pass through.
27
25
  return { ...style };
28
26
  }
@@ -5,7 +5,5 @@ export declare function registerStyles(styles: Record<string, IResolvedStyle>):
5
5
  export declare function clearGlobalStyles(): void;
6
6
  export declare function isClassNameValue(value: unknown): value is IClassNameValue;
7
7
  export declare function resolveClassName(className: IClassNameValue): IResolvedStyle;
8
- export type IClassToggleMap = Record<string, boolean | undefined>;
9
- export type IScopableClassValue = string | IClassToggleMap | Array<string | IClassToggleMap> | undefined | null;
10
- export declare function scopeClassName(value: IScopableClassValue, localNames: ReadonlySet<string>, scopeId: string): IScopableClassValue;
8
+ export declare function kebabToCamel(value: string): string;
11
9
  export {};