@symbiote-native/components 0.2.2 → 0.2.4

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.
@@ -1,13 +1,12 @@
1
1
  // Zero-config host bootstrap for @symbiote-native/components' four RN-backed seams
2
2
  // (colorProcessor, imageSourceResolver, deviceEventSource, nativeViewConfigSource), wired from
3
- // real react-native in one call — collapses what every canary example currently hand-wires at
3
+ // real react-native in one call - collapses what every canary example currently hand-wires at
4
4
  // startup. Lives OUTSIDE the package's main barrel (see package.json's separate "./bootstrap"
5
5
  // export): react-native's own source is Flow syntax Vitest's transform can't parse, so anything
6
6
  // importing it directly must stay unreachable from the tested main index.ts.
7
7
  import { processColor, DeviceEventEmitter, Image } from 'react-native';
8
- import { setColorProcessor, setDeviceEventSource, setNativeViewConfigSource, } from '@symbiote-native/engine';
9
- import { setImageSourceResolver } from './view/render-image.js';
10
- // @ts-expect-error react-native ships no types for this internal path (plain .js) — the
8
+ import { setColorProcessor, setDeviceEventSource, setImageSourceResolver, setNativeViewConfigSource, } from '@symbiote-native/engine';
9
+ // @ts-expect-error react-native ships no types for this internal path (plain .js) - the
11
10
  // try/catch below is what actually proves the shape, not TS.
12
11
  import * as ReactNativeViewConfigRegistry from 'react-native/Libraries/Renderer/shims/ReactNativeViewConfigRegistry';
13
12
  // Third-party Fabric views derive their events + prop processors from RN's own ViewConfig
@@ -23,7 +22,7 @@ function defaultNativeViewConfigSource(name) {
23
22
  }
24
23
  // require('./x.png') asset ids and {uri} sources are resolved by RN's own resolver before they
25
24
  // reach the shared render fns. `source` is untyped at the setImageSourceResolver seam by design
26
- // (any component's resolved shape flows through it) — this is the I/O edge where it crosses
25
+ // (any component's resolved shape flows through it) - this is the I/O edge where it crosses
27
26
  // into RN's own typed Image API.
28
27
  function defaultImageSourceResolver(source) {
29
28
  return Image.resolveAssetSource(source);
package/build/index.d.ts CHANGED
@@ -18,8 +18,10 @@ export { buildScrollViewHandle, splitLayoutProps, attachStickyScroll, isSymbiote
18
18
  export type { IScrollViewHandle } from './scroll-view-commands';
19
19
  export { computeStickyInterpolation, nextStickyHeaderY, stickyDebounceMs, readLayoutNumber, STICKY_HEADER_Z_INDEX, } from './view/render-scroll-sticky';
20
20
  export type { IStickyHeaderProps, IStickyInterpolationParams } from './view/render-scroll-sticky';
21
- export type { IImageSource, IImageStatics, IImageSourceProp, IResizeMode, IImageSize, IImageCacheStatus, IImageProps, IImageViewProps, } from './view/render-image';
22
- export { renderImage, imageStatics, setImageSourceResolver } from './view/render-image';
21
+ export type { IImageSource, IImageSourceProp, IResizeMode, IImageProps, IImageViewProps, } from './view/render-image';
22
+ export { renderImage } from './view/render-image';
23
+ export type { IImageStatics, IImageSize, IImageCacheStatus } from '@symbiote-native/engine';
24
+ export { imageStatics, setImageSourceResolver } from '@symbiote-native/engine';
23
25
  export { renderImageBackground } from './view/render-image-background';
24
26
  export type { IImageBackgroundViewProps } from './view/render-image-background';
25
27
  export { renderInputAccessoryView } from './view/render-input-accessory-view';
@@ -32,7 +34,7 @@ export { computeInset, readKeyboardFrame, readLayoutFrame, resolveKeyboardAvoidi
32
34
  export type { IKeyboardAvoidingBehavior, IMeasuredFrame, IKeyboardFrame, IKeyboardAvoidingLayout, IResolveKeyboardAvoidingLayoutParams, } from './view/render-keyboard-avoiding-view';
33
35
  export { createPressHandlers, createPressRuntime, normalizeRect, maxEdge, isTouchWithinRegion, readPoint, computeRegion, rippleProps, DEFAULT_DELAY_LONG_PRESS_MS, DEFAULT_PRESS_RECT_OFFSETS, } from './state/pressable';
34
36
  export type { IPressState, IPressHandler, IPressHandlers, IPressHost, IPressRuntime, IPressMachineConfig, IFrameCallback, IRectOffset, IEdgeInsets, IResponderRegion, IPressableAndroidRippleConfig, IRippleBackground as IPressableRippleBackground, } from './state/pressable';
35
- export { buildPressableListeners, resolveDisabledAccessibilityState, noteHoverNoop, } from './view/render-pressable';
37
+ export { buildPressableListeners, resolveDisabledAccessibilityState, noteHoverNoop, shouldSuppressPress, shouldClaimResponder, isTerminationAllowed, } from './view/render-pressable';
36
38
  export { computePressOutWait, DEFAULT_ACTIVE_OPACITY, OPACITY_ACTIVE_DURATION_MS, OPACITY_INACTIVE_DURATION_MS, RESTING_OPACITY, DEFAULT_HIGHLIGHT_CHILD_OPACITY, DEFAULT_UNDERLAY_COLOR, DEFAULT_MIN_PRESS_DURATION_MS, } from './state/touchable';
37
39
  export type { IPressTimingProps, ITouchableHandler } from './state/touchable';
38
40
  export { backgroundProps, canUseNativeForeground, selectableBackground, selectableBackgroundBorderless, rippleBackground, } from './view/render-touchable-native-feedback';
package/build/index.js CHANGED
@@ -11,7 +11,7 @@ export { resolveAccessibilityProps } from './accessibility-props.js';
11
11
  // names CANNOT drift between them (one engine, one Fabric). The name tables are
12
12
  // Metro-split (.ios/.android, filename selects, no Platform.OS read); the base
13
13
  // re-exports iOS for headless. descriptorFor is the per-platform-bound resolver.
14
- export { descriptorFor, COMPONENT_DESCRIPTORS } from './component-names/index.js';
14
+ export { descriptorFor, COMPONENT_DESCRIPTORS } from './component-names';
15
15
  export { buildDescriptors, makeDescriptorFor } from './component-names/shared.js';
16
16
  export { renderActivityIndicator } from './view/render-activity-indicator.js';
17
17
  export { renderSwitch } from './view/render-switch.js';
@@ -24,7 +24,8 @@ export { switchReducer, createInitialSwitchState, shouldSnapBack, valueFromChang
24
24
  export { resolveDecelerationRate, selectScrollIntrinsics, readLayoutDimension, didContentSizeChange, SCROLL_VIEW_BASE_HORIZONTAL, SCROLL_VIEW_BASE_VERTICAL, } from './view/render-scroll-view.js';
25
25
  export { buildScrollViewHandle, splitLayoutProps, attachStickyScroll, isSymbioteEvent, forwardScrollEvent, } from './scroll-view-commands.js';
26
26
  export { computeStickyInterpolation, nextStickyHeaderY, stickyDebounceMs, readLayoutNumber, STICKY_HEADER_Z_INDEX, } from './view/render-scroll-sticky.js';
27
- export { renderImage, imageStatics, setImageSourceResolver } from './view/render-image.js';
27
+ export { renderImage } from './view/render-image.js';
28
+ export { imageStatics, setImageSourceResolver } from '@symbiote-native/engine';
28
29
  // ImageBackground: render-only composition (absolute-fill image behind, children on top).
29
30
  export { renderImageBackground } from './view/render-image-background.js';
30
31
  // InputAccessoryView: render-only host assembly (nativeID/backgroundColor).
@@ -39,7 +40,7 @@ export { computeInset, readKeyboardFrame, readLayoutFrame, resolveKeyboardAvoidi
39
40
  // accessibilityState + ripple render decisions in view/render-pressable. Every adapter supplies
40
41
  // only its reactive `pressed` cell + the View's measure handle (see the 3-layer split).
41
42
  export { createPressHandlers, createPressRuntime, normalizeRect, maxEdge, isTouchWithinRegion, readPoint, computeRegion, rippleProps, DEFAULT_DELAY_LONG_PRESS_MS, DEFAULT_PRESS_RECT_OFFSETS, } from './state/pressable.js';
42
- export { buildPressableListeners, resolveDisabledAccessibilityState, noteHoverNoop, } from './view/render-pressable.js';
43
+ export { buildPressableListeners, resolveDisabledAccessibilityState, noteHoverNoop, shouldSuppressPress, shouldClaimResponder, isTerminationAllowed, } from './view/render-pressable.js';
43
44
  // Touchable*: shared press-timing constants + the deactivation-floor math (the Animated feedback
44
45
  // itself stays per-adapter), and TouchableNativeFeedback's pure static factories + background map.
45
46
  export { computePressOutWait, DEFAULT_ACTIVE_OPACITY, OPACITY_ACTIVE_DURATION_MS, OPACITY_INACTIVE_DURATION_MS, RESTING_OPACITY, DEFAULT_HIGHLIGHT_CHILD_OPACITY, DEFAULT_UNDERLAY_COLOR, DEFAULT_MIN_PRESS_DURATION_MS, } from './state/touchable.js';
@@ -1,4 +1,4 @@
1
- import { type AnimatedValue, type IStyleProp, type ISymbioteEvent, type ISymbioteNode, type IViewStyle } from '@symbiote-native/engine';
1
+ import { isSymbioteEvent, type AnimatedValue, type IStyleProp, type ISymbioteEvent, type ISymbioteNode, type IViewStyle } from '@symbiote-native/engine';
2
2
  type IScrollHandler = (event: ISymbioteEvent) => void;
3
3
  export interface IScrollViewHandle {
4
4
  scrollTo(options?: {
@@ -16,8 +16,7 @@ export declare function splitLayoutProps(style: IStyleProp<IViewStyle> | undefin
16
16
  outer: Record<string, unknown>;
17
17
  inner: Record<string, unknown>;
18
18
  };
19
- export declare function isSymbioteEvent(value: unknown): value is ISymbioteEvent;
19
+ export { isSymbioteEvent };
20
20
  export declare function forwardScrollEvent(handler: IScrollHandler, args: readonly unknown[]): void;
21
21
  export declare function buildScrollViewHandle(getNode: () => ISymbioteNode | null): IScrollViewHandle;
22
22
  export declare function attachStickyScroll(node: ISymbioteNode, value: AnimatedValue): () => void;
23
- export {};
@@ -3,7 +3,7 @@
3
3
  // the Android RefreshControl wrap, the scroll-event guard/forwarder, and the native sticky
4
4
  // scroll-attach are all platform- and framework-invariant, so they live here. The adapter
5
5
  // supplies the lifecycle (the node getter, the effect) and re-exports these.
6
- import { attachNativeEvent, dispatchViewCommand, dlog, flattenStyle, } from '@symbiote-native/engine';
6
+ import { attachNativeEvent, dispatchViewCommand, dlog, flattenStyle, isSymbioteEvent, } from '@symbiote-native/engine';
7
7
  // RN's splitLayoutProps key partition (StyleSheet/splitLayoutProps.js): the LAYOUT keys
8
8
  // that belong on the OUTER box when a layout-affecting wrapper sits between the laid-out
9
9
  // frame and the visual content. Everything NOT in this set (background*, padding*, border*,
@@ -58,12 +58,9 @@ export function splitLayoutProps(style) {
58
58
  }
59
59
  return { outer, inner };
60
60
  }
61
- export function isSymbioteEvent(value) {
62
- if (typeof value !== 'object' || value === null)
63
- return false;
64
- const nativeEvent = Reflect.get(value, 'nativeEvent');
65
- return typeof nativeEvent === 'object' && nativeEvent !== null;
66
- }
61
+ // Re-exported so the package barrel (index.ts) can still export this guard to
62
+ // '@symbiote-native/components' callers, now that it lives in the engine, next to ISymbioteEvent.
63
+ export { isSymbioteEvent };
67
64
  // Forward a wrapped scroll event to the user's ScrollHandler. The Animated.event listener
68
65
  // hands raw args; the first is the original SymbioteEvent, which we narrow with a runtime
69
66
  // guard (no cast) and pass through unchanged so the user sees the same event RN would deliver.
@@ -0,0 +1,10 @@
1
+ import type { ISymbioteNode } from '@symbiote-native/engine';
2
+ import type { IScrollViewHandle } from '../scroll-view-commands';
3
+ export interface IScrollRoutingHandle {
4
+ flashScrollIndicators(): void;
5
+ getNativeScrollRef(): IScrollViewHandle | null;
6
+ getScrollableNode(): IScrollViewHandle | null;
7
+ getScrollResponder(): IScrollViewHandle | null;
8
+ getScrollNode(): ISymbioteNode | null;
9
+ recordInteraction(): void;
10
+ }
@@ -0,0 +1,6 @@
1
+ // The inner-scroll routing tail every windowed-list imperative handle exposes
2
+ // (VirtualizedList, VirtualizedSectionList, and via those FlatList/SectionList). Each
3
+ // method forwards straight to the underlying ScrollView's own handle/node; neither list
4
+ // adds behavior of its own, so the 6 members live here once and both list handle types
5
+ // extend this instead of hand-duplicating (and risking drift in) the signatures.
6
+ export {};
@@ -1,5 +1,4 @@
1
- import type { ISymbioteNode } from '@symbiote-native/engine';
2
- import type { IScrollViewHandle } from '../scroll-view-commands';
1
+ import type { IScrollRoutingHandle } from './scroll-routing-handle';
3
2
  export interface ISection<ItemT> {
4
3
  title: string;
5
4
  data: readonly ItemT[];
@@ -22,7 +21,7 @@ export type ISectionEntry<ItemT> = {
22
21
  kind: 'section-separator';
23
22
  sectionIndex: number;
24
23
  };
25
- export interface IVirtualizedSectionListHandle {
24
+ export interface IVirtualizedSectionListHandle extends IScrollRoutingHandle {
26
25
  scrollToLocation(params: {
27
26
  sectionIndex: number;
28
27
  itemIndex: number;
@@ -30,12 +29,6 @@ export interface IVirtualizedSectionListHandle {
30
29
  viewPosition?: number;
31
30
  animated?: boolean;
32
31
  }): void;
33
- flashScrollIndicators(): void;
34
- getNativeScrollRef(): IScrollViewHandle | null;
35
- getScrollableNode(): IScrollViewHandle | null;
36
- getScrollResponder(): IScrollViewHandle | null;
37
- getScrollNode(): ISymbioteNode | null;
38
- recordInteraction(): void;
39
32
  }
40
33
  export declare function flattenSections<ItemT>(sections: ReadonlyArray<ISection<ItemT>>, withSeparators: boolean): {
41
34
  entries: ISectionEntry<ItemT>[];
@@ -2,8 +2,8 @@
2
2
  // section contributes a header row, its item rows, then a footer row (RN counts 2 per
3
3
  // section around the items); the flattened sequence feeds VirtualizedList as one tagged
4
4
  // stream, so headers, items, and footers are windowed by the same machinery. All of this
5
- // is pure transform — every adapter reuses it; the adapter supplies only the per-entry
6
- // element creation (renderSectionHeader / renderItem / …) and the ref wiring.
5
+ // is pure transform - every adapter reuses it; the adapter supplies only the per-entry
6
+ // element creation (renderSectionHeader / renderItem / etc.) and the ref wiring.
7
7
  // Flatten sections into entries AND record where each section header lands in the flat
8
8
  // stream, so scrollToLocation can map (sectionIndex, itemIndex) -> flat index without
9
9
  // re-deriving the layout. withSeparators inserts a section separator between adjacent
@@ -1,6 +1,6 @@
1
1
  import type { IViewStyle } from '@symbiote-native/engine';
2
- import type { ISymbioteEvent, ISymbioteNode } from '@symbiote-native/engine';
3
- import type { IScrollViewHandle } from '../scroll-view-commands';
2
+ import type { ISymbioteEvent } from '@symbiote-native/engine';
3
+ import type { IScrollRoutingHandle } from './scroll-routing-handle';
4
4
  export declare const DEFAULT_WINDOW_SIZE = 21;
5
5
  export declare const DEFAULT_INITIAL_NUM_TO_RENDER = 10;
6
6
  export declare const DEFAULT_END_REACHED_THRESHOLD = 2;
@@ -52,7 +52,7 @@ export interface IViewabilityConfigCallbackPair<ItemT> {
52
52
  viewabilityConfig: IViewabilityConfig;
53
53
  onViewableItemsChanged: (info: IViewableItemsChangedInfo<ItemT>) => void;
54
54
  }
55
- export interface IVirtualizedListHandle {
55
+ export interface IVirtualizedListHandle extends IScrollRoutingHandle {
56
56
  scrollToOffset(params: {
57
57
  offset: number;
58
58
  animated?: boolean;
@@ -71,12 +71,6 @@ export interface IVirtualizedListHandle {
71
71
  scrollToEnd(params?: {
72
72
  animated?: boolean;
73
73
  }): void;
74
- flashScrollIndicators(): void;
75
- getNativeScrollRef(): IScrollViewHandle | null;
76
- getScrollableNode(): IScrollViewHandle | null;
77
- getScrollResponder(): IScrollViewHandle | null;
78
- getScrollNode(): ISymbioteNode | null;
79
- recordInteraction(): void;
80
74
  }
81
75
  export declare function readScrollOffset(event: ISymbioteEvent, horizontal: boolean): number | undefined;
82
76
  export declare function readLayoutLength(event: ISymbioteEvent, horizontal: boolean): number | undefined;
@@ -2,7 +2,7 @@
2
2
  // (React hooks, Vue reactivity) drives the SAME math from here, so a windowing /
3
3
  // viewability / edge-reached bug is fixed once for all adapters. The adapter
4
4
  // supplies only its lifecycle (refs/state/effects), the imperative handle
5
- // wiring, and the per-cell element creation (createElement / h) — never the
5
+ // wiring, and the per-cell element creation (createElement / h) - never the
6
6
  // geometry.
7
7
  //
8
8
  // What lives here:
@@ -17,7 +17,7 @@
17
17
  //
18
18
  // What stays in the adapter (genuinely framework-bound): the cell CONTENT is the
19
19
  // framework's own children (renderItem -> ReactNode / VNode), so there is no
20
- // Descriptor render fn for a list — the shared layer for lists is this STATE/logic
20
+ // Descriptor render fn for a list - the shared layer for lists is this STATE/logic
21
21
  // module, not a view/render-*.ts.
22
22
  // Defaults match RN. windowSize is measured in viewport-lengths (21 => ten screens
23
23
  // of buffer on each side of the visible region). onEndReachedThreshold is a multiple
@@ -0,0 +1,2 @@
1
+ import type { ISymbioteEvent } from '@symbiote-native/engine';
2
+ export declare function readLayoutField(event: ISymbioteEvent, key: 'width' | 'height' | 'y'): number | undefined;
@@ -0,0 +1,12 @@
1
+ // Shared across render-scroll-view's content/frame dimension read (width/height) and
2
+ // render-scroll-sticky's header position read (y/height): both pull a single numeric field out of
3
+ // an onLayout event's nativeEvent.layout. SymbioteEvent.nativeEvent is Record<string, unknown>, so
4
+ // the layout box and the requested field are narrowed at runtime, no cast. A malformed event
5
+ // yields undefined (no-op).
6
+ export function readLayoutField(event, key) {
7
+ const layout = event.nativeEvent.layout;
8
+ if (typeof layout !== 'object' || layout === null)
9
+ return undefined;
10
+ const value = Reflect.get(layout, key);
11
+ return typeof value === 'number' ? value : undefined;
12
+ }
@@ -1,15 +1,9 @@
1
- import { type IStyleProp, type ISymbioteEvent, type IViewStyle } from '@symbiote-native/engine';
1
+ import { type IImageSource, type IImageSourceProp, type IStyleProp, type ISymbioteEvent, type IViewStyle } from '@symbiote-native/engine';
2
2
  import type { IAccessibilityProps, IAriaProps } from '../accessibility-props';
3
3
  import { type IDescriptor } from '../descriptor';
4
+ export type { IImageSource, IImageSourceProp };
4
5
  type IImageEventHandler = (event: ISymbioteEvent) => void;
5
6
  export type IResizeMode = 'cover' | 'contain' | 'stretch' | 'repeat' | 'center';
6
- export type IImageSource = {
7
- uri?: string;
8
- scale?: number;
9
- width?: number;
10
- height?: number;
11
- };
12
- export type IImageSourceProp = IImageSource | IImageSource[] | number;
13
7
  export type IImageCapInsets = {
14
8
  top: number;
15
9
  left: number;
@@ -43,29 +37,6 @@ export type IImageProps = IAccessibilityProps & IAriaProps & {
43
37
  onProgress?: IImageEventHandler;
44
38
  onPartialLoad?: IImageEventHandler;
45
39
  };
46
- export declare function setImageSourceResolver(resolve: (source: unknown) => unknown): void;
47
- export type IImageSize = {
48
- width: number;
49
- height: number;
50
- };
51
- export type IImageCacheStatus = 'memory' | 'disk' | 'disk/memory';
52
- type ISizeSuccess = (width: number, height: number) => void;
53
- type ISizeFailure = (error: unknown) => void;
54
- declare function getSize(uri: string, success?: ISizeSuccess, failure?: ISizeFailure): Promise<IImageSize>;
55
- declare function getSizeWithHeaders(uri: string, headers: Record<string, string>, success?: ISizeSuccess, failure?: ISizeFailure): Promise<IImageSize>;
56
- declare function prefetch(uri: string, callback?: (requestId: number) => void): Promise<boolean>;
57
- declare function abortPrefetch(requestId: number): void;
58
- declare function queryCache(uris: string[]): Promise<Record<string, IImageCacheStatus>>;
59
- declare function resolveAssetSource(source: IImageSourceProp): unknown;
60
- export type IImageStatics = {
61
- getSize: typeof getSize;
62
- getSizeWithHeaders: typeof getSizeWithHeaders;
63
- prefetch: typeof prefetch;
64
- abortPrefetch: typeof abortPrefetch;
65
- queryCache: typeof queryCache;
66
- resolveAssetSource: typeof resolveAssetSource;
67
- };
68
- export declare const imageStatics: IImageStatics;
69
40
  export type IImageViewProps = {
70
41
  source?: IImageSourceProp;
71
42
  defaultSource?: IImageSourceProp;
@@ -83,4 +54,3 @@ export type IImageViewProps = {
83
54
  passthrough: Record<string, unknown>;
84
55
  };
85
56
  export declare function renderImage(view: IImageViewProps): IDescriptor;
86
- export {};
@@ -1,15 +1,9 @@
1
- import { dlog, flattenStyle, getNativeModule, Platform, } from '@symbiote-native/engine';
1
+ import { dlog, flattenStyle, resolveImageSource, } from '@symbiote-native/engine';
2
2
  import { el } from '../descriptor.js';
3
- // Default resolver: identity. RN's resolveAssetSource (which turns a require()
4
- // number into {uri, scale, width, height}) is wired in by the app at startup.
5
- let resolveSource = source => source;
6
- export function setImageSourceResolver(resolve) {
7
- resolveSource = resolve;
8
- }
9
3
  // Resolve the source, then normalize to the array shape native expects. A single
10
4
  // object/number becomes a one-element array; an already-array source passes through.
11
5
  function normalizeSource(source) {
12
- const resolved = resolveSource(source);
6
+ const resolved = resolveImageSource(source);
13
7
  const sources = Array.isArray(resolved) ? resolved : [resolved];
14
8
  dlog(`Image source resolved to ${JSON.stringify(sources)}`);
15
9
  return sources;
@@ -108,164 +102,6 @@ function readSourceUri(source) {
108
102
  }
109
103
  return undefined;
110
104
  }
111
- // The iOS native module name RN registers this under (NativeImageLoaderIOS.js
112
- // resolves `TurboModuleRegistry.getEnforcing<Spec>('ImageLoader')`). A module
113
- // name like this is only provable on a real host — a headless fake answers to
114
- // any name — so this iOS name is device-verify-pending.
115
- const IMAGE_LOADER_MODULE = 'ImageLoader';
116
- let imageLoaderModule;
117
- function getImageLoader() {
118
- if (imageLoaderModule === undefined) {
119
- imageLoaderModule = getNativeModule(IMAGE_LOADER_MODULE);
120
- dlog(`Image: ImageLoader module ${imageLoaderModule ? 'resolved' : 'NOT resolved (null)'}`);
121
- }
122
- return imageLoaderModule;
123
- }
124
- function isNumber(value) {
125
- return typeof value === 'number';
126
- }
127
- // Narrow native's getSize result. The spec resolves a `[width, height]` array,
128
- // but tolerate a `{width, height}` object too (getSizeWithHeaders uses that shape).
129
- function toImageSize(result) {
130
- if (Array.isArray(result) && isNumber(result[0]) && isNumber(result[1])) {
131
- return { width: result[0], height: result[1] };
132
- }
133
- if (typeof result === 'object' && result !== null) {
134
- const width = Reflect.get(result, 'width');
135
- const height = Reflect.get(result, 'height');
136
- if (isNumber(width) && isNumber(height))
137
- return { width, height };
138
- }
139
- throw new Error(`Image: unexpected size result from native: ${JSON.stringify(result)}`);
140
- }
141
- function requireLoader(method) {
142
- const loader = getImageLoader();
143
- if (loader === null) {
144
- throw new Error(`Image.${method}: ImageLoader native module is not available ` +
145
- '(running headless or not linked on this host).');
146
- }
147
- return loader;
148
- }
149
- // Resolve image dimensions, optionally via success/failure callbacks. Always
150
- // returns the Promise too (RN returns void when a callback is passed, but a
151
- // promise-and-callback shape is friendlier and a strict superset).
152
- function getSize(uri, success, failure) {
153
- const promise = Promise.resolve()
154
- .then(() => requireLoader('getSize').getSize(uri))
155
- .then(toImageSize);
156
- if (typeof success === 'function') {
157
- promise
158
- .then(size => success(size.width, size.height))
159
- .catch((error) => {
160
- if (typeof failure === 'function')
161
- failure(error);
162
- else
163
- dlog(`Image.getSize failed for ${uri}: ${String(error)}`);
164
- });
165
- }
166
- return promise;
167
- }
168
- function getSizeWithHeaders(uri, headers, success, failure) {
169
- const promise = Promise.resolve()
170
- .then(() => requireLoader('getSizeWithHeaders').getSizeWithHeaders(uri, headers))
171
- .then(toImageSize);
172
- if (typeof success === 'function') {
173
- promise
174
- .then(size => success(size.width, size.height))
175
- .catch((error) => {
176
- if (typeof failure === 'function')
177
- failure(error);
178
- else
179
- dlog(`Image.getSizeWithHeaders failed for ${uri}: ${String(error)}`);
180
- });
181
- }
182
- return promise;
183
- }
184
- // Android keys an in-flight prefetch by a monotonic requestId (so abortRequest can
185
- // cancel it); RN's Image.android.js generates the same way. iOS ignores the arg.
186
- let prefetchRequestId = 0;
187
- // Download a remote image into the disk cache. Resolves to whether it succeeded.
188
- // `callback` receives the requestId (RN's Image.android.js shape) so the caller
189
- // can later pass it to abortPrefetch.
190
- async function prefetch(uri, callback) {
191
- prefetchRequestId += 1;
192
- const requestId = prefetchRequestId;
193
- if (typeof callback === 'function')
194
- callback(requestId);
195
- const loader = requireLoader('prefetch');
196
- return (Promise.resolve()
197
- // Android's prefetchImage keys an abortable request on requestId; iOS takes ONLY the uri and
198
- // throws on an extra arg (bridgeless TurboModule arg-count check). Match RN's per-platform call.
199
- .then(() => Platform.OS === 'android'
200
- ? loader.prefetchImage(uri, requestId)
201
- : loader.prefetchImage(uri))
202
- .then(result => result === true)
203
- .catch((error) => {
204
- dlog(`Image.prefetch failed for ${uri}: ${String(error)}`);
205
- throw error;
206
- }));
207
- }
208
- // Cancel an in-flight prefetch by the requestId prefetch handed back. Android
209
- // only (mirrors Image.android.js -> NativeImageLoaderAndroid.abortRequest); a
210
- // missing abortRequest (iOS, headless) is a no-op rather than a throw.
211
- function abortPrefetch(requestId) {
212
- const loader = getImageLoader();
213
- if (loader === null || typeof loader.abortRequest !== 'function') {
214
- dlog(`Image.abortPrefetch(${requestId}): no abortRequest on this host, ignoring`);
215
- return;
216
- }
217
- loader.abortRequest(requestId);
218
- }
219
- // Narrow native's queryCache result: an object mapping each known uri to its
220
- // cache status. Unknown statuses are dropped rather than trusted blindly.
221
- const CACHE_STATUS = {
222
- memory: 'memory',
223
- disk: 'disk',
224
- 'disk/memory': 'disk/memory',
225
- };
226
- function toCacheRecord(result) {
227
- const record = {};
228
- if (typeof result !== 'object' || result === null)
229
- return record;
230
- for (const key of Object.keys(result)) {
231
- const value = Reflect.get(result, key);
232
- if (typeof value === 'string' && Object.hasOwn(CACHE_STATUS, value)) {
233
- record[key] = CACHE_STATUS[value];
234
- }
235
- }
236
- return record;
237
- }
238
- async function queryCache(uris) {
239
- return Promise.resolve()
240
- .then(() => {
241
- const loader = requireLoader('queryCache');
242
- // The native queryCache never rejects (RCTImageLoader resolves getImageCacheStatus), so a
243
- // rejection here is a JS↔native boundary fault: log whether the method is even callable and
244
- // the arg shape, to tell "not a function" (interop gap) from a marshalling reject.
245
- dlog(`Image.queryCache: typeof loader.queryCache=${typeof loader.queryCache} uris=${uris.length}`);
246
- return loader.queryCache(uris);
247
- })
248
- .then(toCacheRecord)
249
- .catch((error) => {
250
- dlog(`Image.queryCache failed: ${String(error)}`);
251
- throw error;
252
- });
253
- }
254
- // PURE JS: run the currently-installed source resolver (the same machinery the
255
- // Image component uses via normalizeSource). RN's resolveAssetSource turns a
256
- // require() asset id into {uri, scale, …}; the app injects the real one with
257
- // setImageSourceResolver, and this exposes its output to callers directly.
258
- function resolveAssetSource(source) {
259
- return resolveSource(source);
260
- }
261
- export const imageStatics = {
262
- getSize,
263
- getSizeWithHeaders,
264
- prefetch,
265
- abortPrefetch,
266
- queryCache,
267
- resolveAssetSource,
268
- };
269
105
  export function renderImage(view) {
270
106
  // `width` / `height` aliases fold into style (ImageProps.js:195,202); explicit
271
107
  // style keys win, matching RN's `{width, height}, ...style` ordering.
@@ -1,6 +1,6 @@
1
1
  // KeyboardAvoidingView: the pure logic + view-contract half (framework-agnostic). It owns
2
2
  // every piece that does NOT need a framework: the keyboard/frame inset math, the onLayout
3
- // frame read, and the behavior → style/structure decision. Each adapter supplies ONLY the
3
+ // frame read, and the behavior -> style/structure decision. Each adapter supplies ONLY the
4
4
  // lifecycle (subscribe to the Keyboard module, measure via onLayout, hold the inset in its
5
5
  // reactive primitive) and assembles the wrapper element around its own opaque children.
6
6
  //
@@ -9,13 +9,11 @@
9
9
  // carry. Instead we return a layout DESCRIPTION (which styles to apply and whether to nest)
10
10
  // and the adapter builds its own element tree with its children. Mirrors RN's
11
11
  // Libraries/Components/Keyboard/KeyboardAvoidingView.js inset/behavior logic.
12
+ import { isRecord } from '@symbiote-native/engine';
12
13
  // RN's default keyboardVerticalOffset (KeyboardAvoidingView.js).
13
14
  export const DEFAULT_VERTICAL_OFFSET = 0;
14
15
  // RN rounds nothing here; 'height' mode collapses flex so the shrunk height holds.
15
16
  const COLLAPSED_FLEX = 0;
16
- function isRecord(value) {
17
- return typeof value === 'object' && value !== null;
18
- }
19
17
  // Pull the keyboard's top edge (screenY) and height off the raw native payload. The shape is
20
18
  // the consumer's knowledge, so we narrow `unknown` here rather than trust a type, no `as`.
21
19
  // Returns undefined when the payload isn't a keyboard frame.
@@ -1,6 +1,9 @@
1
1
  import type { IAccessibilityStateValue } from '../accessibility-props';
2
2
  import type { IPressHandlers } from '../state/pressable';
3
3
  export declare function resolveDisabledAccessibilityState(accessibilityState: IAccessibilityStateValue | undefined, disabled: boolean | undefined): IAccessibilityStateValue | undefined;
4
+ export declare function shouldSuppressPress(disabled: boolean | undefined): boolean;
5
+ export declare function shouldClaimResponder(disabled: boolean | undefined): boolean;
6
+ export declare function isTerminationAllowed(cancelable: boolean | undefined): boolean;
4
7
  export declare function buildPressableListeners(handlers: IPressHandlers, options: {
5
8
  disabled?: boolean;
6
9
  cancelable?: boolean;
@@ -10,12 +10,33 @@ import { dlog } from '@symbiote-native/engine';
10
10
  export function resolveDisabledAccessibilityState(accessibilityState, disabled) {
11
11
  return disabled !== undefined ? { ...accessibilityState, disabled } : accessibilityState;
12
12
  }
13
- // The listeners the responder View carries. When disabled, leave them off entirely. A press
14
- // never fires and pressed-state never flips, exactly as RN's disabled Pressable. cancelable ===
15
- // false refuses to yield the responder (RN routes cancelable to onResponderTerminationRequest,
16
- // default true when unset).
13
+ // The 3 agnostic gating predicates behind the listener bag below. Angular has no bag to spread -
14
+ // it binds these directly onto template event outputs (see adapters/angular/src/components/
15
+ // pressable/index.ts), so they're exported and shared rather than folded back into
16
+ // buildPressableListeners, keeping both sides on one definition instead of two.
17
+ // A disabled Pressable suppresses a press entirely: it never fires and pressed-state never flips,
18
+ // exactly as RN's disabled Pressable.
19
+ export function shouldSuppressPress(disabled) {
20
+ return disabled === true;
21
+ }
22
+ // Claim the responder so the move stream reaches this View whenever the press isn't suppressed;
23
+ // retention reads it.
24
+ export function shouldClaimResponder(disabled) {
25
+ return disabled !== true;
26
+ }
27
+ // cancelable === false refuses to yield the responder (RN routes cancelable to
28
+ // onResponderTerminationRequest). Unset defers to RN's own default, which is allowed - so this
29
+ // resolves to true for undefined rather than hardcoding a value, mirroring the more deliberate of
30
+ // the two definitions this predicate replaces.
31
+ export function isTerminationAllowed(cancelable) {
32
+ return cancelable !== false;
33
+ }
34
+ // The listeners the responder View carries. When disabled, leave them off entirely - see
35
+ // shouldSuppressPress. onResponderTerminationRequest itself is only attached when cancelable is
36
+ // set at all, so an unset cancelable leaves RN's native default in charge rather than this
37
+ // listener asserting one.
17
38
  export function buildPressableListeners(handlers, options) {
18
- if (options.disabled === true) {
39
+ if (shouldSuppressPress(options.disabled)) {
19
40
  dlog('Pressable disabled — listeners suppressed');
20
41
  return {};
21
42
  }
@@ -23,12 +44,11 @@ export function buildPressableListeners(handlers, options) {
23
44
  onPress: handlers.handlePress,
24
45
  onPressIn: handlers.handlePressIn,
25
46
  onPressOut: handlers.handlePressOut,
26
- // Claim the responder so the move stream reaches this View; retention reads it.
27
- onStartShouldSetResponder: () => true,
47
+ onStartShouldSetResponder: () => shouldClaimResponder(options.disabled),
28
48
  onResponderMove: handlers.handleResponderMove,
29
49
  };
30
50
  if (options.cancelable !== undefined) {
31
- listeners.onResponderTerminationRequest = () => options.cancelable;
51
+ listeners.onResponderTerminationRequest = () => isTerminationAllowed(options.cancelable);
32
52
  }
33
53
  return listeners;
34
54
  }
@@ -6,6 +6,7 @@
6
6
  // piece, the top/inverted inputRange/outputRange math (computeStickyInterpolation), is ported
7
7
  // byte-for-byte from ScrollViewStickyHeader.js's effect. The adapter owns the component shell,
8
8
  // the layout state, and building the interpolation onto its Animated value.
9
+ import { readLayoutField } from './layout-event.js';
9
10
  // RN gives the sticky wrapper zIndex:10 (ScrollViewStickyHeader.js styles.header) so the
10
11
  // pinned header paints OVER the rows that scroll up under it. Without it the next rows (later
11
12
  // siblings) paint on top and bleed through the header.
@@ -19,16 +20,15 @@ const STICKY_DEBOUNCE_IOS_MS = 64;
19
20
  export function stickyDebounceMs(os) {
20
21
  return os === 'android' ? STICKY_DEBOUNCE_ANDROID_MS : STICKY_DEBOUNCE_IOS_MS;
21
22
  }
23
+ // Thin re-export kept for the existing public surface (adapters import this name from
24
+ // `@symbiote-native/components`); the actual field read is shared with render-scroll-view's
25
+ // width/height read in layout-event.ts.
22
26
  export function readLayoutNumber(event, key) {
23
- const layout = event.nativeEvent.layout;
24
- if (typeof layout !== 'object' || layout === null)
25
- return undefined;
26
- const value = Reflect.get(layout, key);
27
- return typeof value === 'number' ? value : undefined;
27
+ return readLayoutField(event, key);
28
28
  }
29
- // Build the scroll-offset → translateY interpolation ranges that keep the header pinned.
29
+ // Build the scroll-offset -> translateY interpolation ranges that keep the header pinned.
30
30
  // Ported byte-for-byte from ScrollViewStickyHeader.js's effect (both branches). The base
31
- // [-1, 0] → [0, 0] stub is the un-measured identity; once measured, the top branch pins at
31
+ // [-1, 0] -> [0, 0] stub is the un-measured identity; once measured, the top branch pins at
32
32
  // layoutY and tracks 1:1 until the next header pushes it off, while the inverted branch pins
33
33
  // at the viewport bottom (stickStartPoint) and tracks up to the collision point.
34
34
  export function computeStickyInterpolation(params) {
@@ -6,15 +6,12 @@
6
6
  // helpers from prepareScrollView. What diverges per platform, and how a RefreshControl
7
7
  // integrates, stays in the adapter's .ios/.android files.
8
8
  import { Platform } from '@symbiote-native/engine';
9
- // Pull a numeric field out of an onLayout event's nativeEvent.layout without a cast:
10
- // SymbioteEvent.nativeEvent is Record<string, unknown>, so the layout box and its
11
- // width/height are narrowed at runtime. A malformed event yields undefined (no-op).
9
+ import { readLayoutField } from './layout-event.js';
10
+ // Thin re-export kept for the existing public surface (adapters import this name from
11
+ // `@symbiote-native/components`); the actual field read is shared with render-scroll-sticky's
12
+ // y/height read in layout-event.ts.
12
13
  export function readLayoutDimension(event, key) {
13
- const layout = event.nativeEvent.layout;
14
- if (typeof layout !== 'object' || layout === null)
15
- return undefined;
16
- const value = Reflect.get(layout, key);
17
- return typeof value === 'number' ? value : undefined;
14
+ return readLayoutField(event, key);
18
15
  }
19
16
  // 'normal'/'fast' resolve to DIFFERENT friction constants per platform: RN's
20
17
  // processDecelerationRate.js Platform.select()s them: iOS glides longer (0.998/0.99),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@symbiote-native/components",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "Framework-agnostic component logic (state machines + render functions) for SymbioteNative — written once, inherited by every adapter (React, Vue, Angular, ...).",
5
5
  "repository": {
6
6
  "type": "git",
@@ -33,7 +33,7 @@
33
33
  "access": "public"
34
34
  },
35
35
  "dependencies": {
36
- "@symbiote-native/engine": "0.1.3"
36
+ "@symbiote-native/engine": "0.1.5"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "react-native": ">=0.86"