@symbiote-native/components 0.1.0

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 (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +110 -0
  3. package/build/accessibility-props.d.ts +64 -0
  4. package/build/accessibility-props.js +150 -0
  5. package/build/component-names/index.android.d.ts +3 -0
  6. package/build/component-names/index.android.js +32 -0
  7. package/build/component-names/index.d.ts +1 -0
  8. package/build/component-names/index.ios.d.ts +3 -0
  9. package/build/component-names/index.ios.js +26 -0
  10. package/build/component-names/index.js +5 -0
  11. package/build/component-names/shared.d.ts +7 -0
  12. package/build/component-names/shared.js +36 -0
  13. package/build/descriptor.d.ts +11 -0
  14. package/build/descriptor.js +17 -0
  15. package/build/index.d.ts +51 -0
  16. package/build/index.js +63 -0
  17. package/build/responder-props.d.ts +18 -0
  18. package/build/responder-props.js +9 -0
  19. package/build/scroll-view-commands.d.ts +23 -0
  20. package/build/scroll-view-commands.js +123 -0
  21. package/build/state/drawer-layout-android.d.ts +22 -0
  22. package/build/state/drawer-layout-android.js +53 -0
  23. package/build/state/flat-list.d.ts +12 -0
  24. package/build/state/flat-list.js +40 -0
  25. package/build/state/modal.d.ts +11 -0
  26. package/build/state/modal.js +28 -0
  27. package/build/state/pressable.d.ts +90 -0
  28. package/build/state/pressable.js +236 -0
  29. package/build/state/section-list.d.ts +46 -0
  30. package/build/state/section-list.js +51 -0
  31. package/build/state/switch.d.ts +12 -0
  32. package/build/state/switch.js +31 -0
  33. package/build/state/text-input.d.ts +103 -0
  34. package/build/state/text-input.js +205 -0
  35. package/build/state/touchable.d.ts +15 -0
  36. package/build/state/touchable.js +22 -0
  37. package/build/state/virtualized-list.d.ts +161 -0
  38. package/build/state/virtualized-list.js +306 -0
  39. package/build/view/render-activity-indicator.d.ts +25 -0
  40. package/build/view/render-activity-indicator.js +54 -0
  41. package/build/view/render-button.d.ts +19 -0
  42. package/build/view/render-button.js +24 -0
  43. package/build/view/render-drawer-layout-android.d.ts +23 -0
  44. package/build/view/render-drawer-layout-android.js +56 -0
  45. package/build/view/render-image-background.d.ts +9 -0
  46. package/build/view/render-image-background.js +48 -0
  47. package/build/view/render-image.d.ts +86 -0
  48. package/build/view/render-image.js +298 -0
  49. package/build/view/render-input-accessory-view.d.ts +9 -0
  50. package/build/view/render-input-accessory-view.js +18 -0
  51. package/build/view/render-keyboard-avoiding-view.d.ts +30 -0
  52. package/build/view/render-keyboard-avoiding-view.js +75 -0
  53. package/build/view/render-modal.d.ts +23 -0
  54. package/build/view/render-modal.js +70 -0
  55. package/build/view/render-pressable.d.ts +8 -0
  56. package/build/view/render-pressable.js +42 -0
  57. package/build/view/render-scroll-sticky.d.ts +24 -0
  58. package/build/view/render-scroll-sticky.js +81 -0
  59. package/build/view/render-scroll-view.d.ts +18 -0
  60. package/build/view/render-scroll-view.js +85 -0
  61. package/build/view/render-switch.d.ts +29 -0
  62. package/build/view/render-switch.js +33 -0
  63. package/build/view/render-text-input.d.ts +11 -0
  64. package/build/view/render-text-input.js +35 -0
  65. package/build/view/render-touchable-native-feedback.d.ts +17 -0
  66. package/build/view/render-touchable-native-feedback.js +39 -0
  67. package/package.json +38 -0
@@ -0,0 +1,298 @@
1
+ import { dlog, flattenStyle, getNativeModule, Platform, } from '@symbiote-native/engine';
2
+ import { el } from '../descriptor';
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
+ // Resolve the source, then normalize to the array shape native expects. A single
10
+ // object/number becomes a one-element array; an already-array source passes through.
11
+ function normalizeSource(source) {
12
+ const resolved = resolveSource(source);
13
+ const sources = Array.isArray(resolved) ? resolved : [resolved];
14
+ dlog(`Image source resolved to ${JSON.stringify(sources)}`);
15
+ return sources;
16
+ }
17
+ // The HTTP headers the W3C aliases (crossOrigin / referrerPolicy) contribute to
18
+ // every folded source, mirroring ImageSourceUtils.js getImageSourcesFromImageProps:
19
+ // 'use-credentials' adds the credentials header; referrerPolicy adds Referrer-Policy.
20
+ function headersFromAliases(view) {
21
+ const headers = {};
22
+ if (view.crossOrigin === 'use-credentials') {
23
+ headers['Access-Control-Allow-Credentials'] = 'true';
24
+ }
25
+ if (view.referrerPolicy !== undefined) {
26
+ headers['Referrer-Policy'] = view.referrerPolicy;
27
+ }
28
+ return headers;
29
+ }
30
+ // Expand a `srcSet` descriptor list into scaled sources, falling back to `src` for
31
+ // the 1x slot when srcSet omits it. Direct port of getImageSourcesFromImageProps'
32
+ // srcSet branch (ImageSourceUtils.js:48). Invalid scale tokens are skipped, matching
33
+ // RN's parse-and-warn behavior.
34
+ function expandSrcSet(srcSet, view, headers) {
35
+ const sources = [];
36
+ let useSrcForDefaultScale = true;
37
+ for (const entry of srcSet.split(', ')) {
38
+ const [uri, xScale = '1x'] = entry.split(' ');
39
+ if (!xScale.endsWith('x')) {
40
+ dlog(`Image srcSet: unsupported scale token "${xScale}", skipping`);
41
+ continue;
42
+ }
43
+ const scale = parseInt(xScale.slice(0, -1), 10);
44
+ if (Number.isNaN(scale))
45
+ continue;
46
+ if (scale === 1)
47
+ useSrcForDefaultScale = false;
48
+ sources.push({ uri, scale, width: view.width, height: view.height, ...{ headers } });
49
+ }
50
+ if (useSrcForDefaultScale && view.src !== undefined) {
51
+ sources.push({
52
+ uri: view.src,
53
+ scale: 1,
54
+ width: view.width,
55
+ height: view.height,
56
+ ...{ headers },
57
+ });
58
+ }
59
+ if (sources.length === 0)
60
+ dlog('Image srcSet: produced no valid sources');
61
+ return sources;
62
+ }
63
+ // Resolve the native `source` array from whichever of source / src / srcSet the
64
+ // caller provided. Mirrors ImageSourceUtils.js getImageSourcesFromImageProps:
65
+ // srcSet wins, then src, then a header-decorated source, then the plain source.
66
+ // Always returns the array shape native expects (the same contract normalizeSource
67
+ // guarantees), so the component never sends a bare object.
68
+ function resolveSourceArray(view) {
69
+ const headers = headersFromAliases(view);
70
+ if (view.srcSet !== undefined) {
71
+ return expandSrcSet(view.srcSet, view, headers);
72
+ }
73
+ if (view.src !== undefined) {
74
+ return [{ uri: view.src, width: view.width, height: view.height, ...{ headers } }];
75
+ }
76
+ if (view.source === undefined) {
77
+ dlog('Image: no source / src / srcSet provided');
78
+ return [];
79
+ }
80
+ const sources = normalizeSource(view.source);
81
+ // A header-decorated single object source gets the headers merged in, per RN's
82
+ // `source.uri && headers` branch; the array/number shapes pass through untouched.
83
+ if (Object.keys(headers).length > 0 && sources.length === 1) {
84
+ const [only] = sources;
85
+ if (typeof only === 'object' && only !== null && typeof Reflect.get(only, 'uri') === 'string') {
86
+ return [{ ...only, headers }];
87
+ }
88
+ }
89
+ return sources;
90
+ }
91
+ function readStyleString(style, key) {
92
+ if (style === undefined)
93
+ return undefined;
94
+ // style is a StyleProp (possibly a nested array), so flatten before reading a key.
95
+ const flat = flattenStyle(style);
96
+ const value = Object.hasOwn(flat, key) ? flat[key] : undefined;
97
+ return typeof value === 'string' ? value : undefined;
98
+ }
99
+ // Resolve an asset source and read its single uri. RN forwards the Android
100
+ // loading indicator as a bare uri string (`loadingIndicatorSrc`), not the
101
+ // array shape the main source uses, so we resolve and pluck the uri.
102
+ function readSourceUri(source) {
103
+ const [resolved] = normalizeSource(source);
104
+ if (typeof resolved === 'object' && resolved !== null) {
105
+ const uri = Reflect.get(resolved, 'uri');
106
+ if (typeof uri === 'string')
107
+ return uri;
108
+ }
109
+ return undefined;
110
+ }
111
+ // The iOS native module name RN registers this under (NativeImageLoaderIOS.js
112
+ // resolves `TurboModuleRegistry.getEnforcing<Spec>('ImageLoader')`). Per the
113
+ // symbiote invariant, a module name is only provable on a real host (a headless
114
+ // fake answers to any name); this iOS name is device-verify-pending. See
115
+ // .docs/native-module-platform-routing.md.
116
+ const IMAGE_LOADER_MODULE = 'ImageLoader';
117
+ let imageLoaderModule;
118
+ function getImageLoader() {
119
+ if (imageLoaderModule === undefined) {
120
+ imageLoaderModule = getNativeModule(IMAGE_LOADER_MODULE);
121
+ dlog(`Image: ImageLoader module ${imageLoaderModule ? 'resolved' : 'NOT resolved (null)'}`);
122
+ }
123
+ return imageLoaderModule;
124
+ }
125
+ function isNumber(value) {
126
+ return typeof value === 'number';
127
+ }
128
+ // Narrow native's getSize result. The spec resolves a `[width, height]` array,
129
+ // but tolerate a `{width, height}` object too (getSizeWithHeaders uses that shape).
130
+ function toImageSize(result) {
131
+ if (Array.isArray(result) && isNumber(result[0]) && isNumber(result[1])) {
132
+ return { width: result[0], height: result[1] };
133
+ }
134
+ if (typeof result === 'object' && result !== null) {
135
+ const width = Reflect.get(result, 'width');
136
+ const height = Reflect.get(result, 'height');
137
+ if (isNumber(width) && isNumber(height))
138
+ return { width, height };
139
+ }
140
+ throw new Error(`Image: unexpected size result from native: ${JSON.stringify(result)}`);
141
+ }
142
+ function requireLoader(method) {
143
+ const loader = getImageLoader();
144
+ if (loader === null) {
145
+ throw new Error(`Image.${method}: ImageLoader native module is not available ` +
146
+ '(running headless or not linked on this host).');
147
+ }
148
+ return loader;
149
+ }
150
+ // Resolve image dimensions, optionally via success/failure callbacks. Always
151
+ // returns the Promise too (RN returns void when a callback is passed, but a
152
+ // promise-and-callback shape is friendlier and a strict superset).
153
+ function getSize(uri, success, failure) {
154
+ const promise = Promise.resolve()
155
+ .then(() => requireLoader('getSize').getSize(uri))
156
+ .then(toImageSize);
157
+ if (typeof success === 'function') {
158
+ promise
159
+ .then(size => success(size.width, size.height))
160
+ .catch((error) => {
161
+ if (typeof failure === 'function')
162
+ failure(error);
163
+ else
164
+ dlog(`Image.getSize failed for ${uri}: ${String(error)}`);
165
+ });
166
+ }
167
+ return promise;
168
+ }
169
+ function getSizeWithHeaders(uri, headers, success, failure) {
170
+ const promise = Promise.resolve()
171
+ .then(() => requireLoader('getSizeWithHeaders').getSizeWithHeaders(uri, headers))
172
+ .then(toImageSize);
173
+ if (typeof success === 'function') {
174
+ promise
175
+ .then(size => success(size.width, size.height))
176
+ .catch((error) => {
177
+ if (typeof failure === 'function')
178
+ failure(error);
179
+ else
180
+ dlog(`Image.getSizeWithHeaders failed for ${uri}: ${String(error)}`);
181
+ });
182
+ }
183
+ return promise;
184
+ }
185
+ // Android keys an in-flight prefetch by a monotonic requestId (so abortRequest can
186
+ // cancel it); RN's Image.android.js generates the same way. iOS ignores the arg.
187
+ let prefetchRequestId = 0;
188
+ // Download a remote image into the disk cache. Resolves to whether it succeeded.
189
+ // `callback` receives the requestId (RN's Image.android.js shape) so the caller
190
+ // can later pass it to abortPrefetch.
191
+ async function prefetch(uri, callback) {
192
+ prefetchRequestId += 1;
193
+ const requestId = prefetchRequestId;
194
+ if (typeof callback === 'function')
195
+ callback(requestId);
196
+ const loader = requireLoader('prefetch');
197
+ return (Promise.resolve()
198
+ // Android's prefetchImage keys an abortable request on requestId; iOS takes ONLY the uri and
199
+ // throws on an extra arg (bridgeless TurboModule arg-count check). Match RN's per-platform call.
200
+ .then(() => Platform.OS === 'android'
201
+ ? loader.prefetchImage(uri, requestId)
202
+ : loader.prefetchImage(uri))
203
+ .then(result => result === true)
204
+ .catch((error) => {
205
+ dlog(`Image.prefetch failed for ${uri}: ${String(error)}`);
206
+ throw error;
207
+ }));
208
+ }
209
+ // Cancel an in-flight prefetch by the requestId prefetch handed back. Android
210
+ // only (mirrors Image.android.js -> NativeImageLoaderAndroid.abortRequest); a
211
+ // missing abortRequest (iOS, headless) is a no-op rather than a throw.
212
+ function abortPrefetch(requestId) {
213
+ const loader = getImageLoader();
214
+ if (loader === null || typeof loader.abortRequest !== 'function') {
215
+ dlog(`Image.abortPrefetch(${requestId}): no abortRequest on this host, ignoring`);
216
+ return;
217
+ }
218
+ loader.abortRequest(requestId);
219
+ }
220
+ // Narrow native's queryCache result: an object mapping each known uri to its
221
+ // cache status. Unknown statuses are dropped rather than trusted blindly.
222
+ const CACHE_STATUS = {
223
+ memory: 'memory',
224
+ disk: 'disk',
225
+ 'disk/memory': 'disk/memory',
226
+ };
227
+ function toCacheRecord(result) {
228
+ const record = {};
229
+ if (typeof result !== 'object' || result === null)
230
+ return record;
231
+ for (const key of Object.keys(result)) {
232
+ const value = Reflect.get(result, key);
233
+ if (typeof value === 'string' && Object.hasOwn(CACHE_STATUS, value)) {
234
+ record[key] = CACHE_STATUS[value];
235
+ }
236
+ }
237
+ return record;
238
+ }
239
+ async function queryCache(uris) {
240
+ return Promise.resolve()
241
+ .then(() => {
242
+ const loader = requireLoader('queryCache');
243
+ // The native queryCache never rejects (RCTImageLoader resolves getImageCacheStatus), so a
244
+ // rejection here is a JS↔native boundary fault: log whether the method is even callable and
245
+ // the arg shape, to tell "not a function" (interop gap) from a marshalling reject.
246
+ dlog(`Image.queryCache: typeof loader.queryCache=${typeof loader.queryCache} uris=${uris.length}`);
247
+ return loader.queryCache(uris);
248
+ })
249
+ .then(toCacheRecord)
250
+ .catch((error) => {
251
+ dlog(`Image.queryCache failed: ${String(error)}`);
252
+ throw error;
253
+ });
254
+ }
255
+ // PURE JS: run the currently-installed source resolver (the same machinery the
256
+ // Image component uses via normalizeSource). RN's resolveAssetSource turns a
257
+ // require() asset id into {uri, scale, …}; the app injects the real one with
258
+ // setImageSourceResolver, and this exposes its output to callers directly.
259
+ function resolveAssetSource(source) {
260
+ return resolveSource(source);
261
+ }
262
+ export const imageStatics = {
263
+ getSize,
264
+ getSizeWithHeaders,
265
+ prefetch,
266
+ abortPrefetch,
267
+ queryCache,
268
+ resolveAssetSource,
269
+ };
270
+ export function renderImage(view) {
271
+ // `width` / `height` aliases fold into style (ImageProps.js:195,202); explicit
272
+ // style keys win, matching RN's `{width, height}, ...style` ordering.
273
+ const foldedStyle = view.width === undefined && view.height === undefined
274
+ ? view.style
275
+ : [{ width: view.width, height: view.height }, view.style];
276
+ const mapped = {
277
+ ...view.passthrough,
278
+ style: foldedStyle,
279
+ source: resolveSourceArray(view),
280
+ resizeMode: view.resizeMode ?? readStyleString(view.style, 'resizeMode'),
281
+ tintColor: view.tintColor ?? readStyleString(view.style, 'tintColor'),
282
+ };
283
+ // `alt` is the accessibility text: it sets accessibilityLabel and marks the image
284
+ // accessible (Image.ios.js / Image.android.js: alt -> accessibilityLabel + accessible).
285
+ // An explicit accessibilityLabel (already folded into passthrough) still wins.
286
+ if (view.alt !== undefined) {
287
+ if (mapped.accessibilityLabel === undefined)
288
+ mapped.accessibilityLabel = view.alt;
289
+ mapped.accessible = true;
290
+ }
291
+ if (view.defaultSource !== undefined)
292
+ mapped.defaultSource = normalizeSource(view.defaultSource);
293
+ if (view.loadingIndicatorSource !== undefined) {
294
+ mapped.loadingIndicatorSrc = readSourceUri(view.loadingIndicatorSource);
295
+ }
296
+ dlog('Image -> RCTImageView');
297
+ return el('symbiote-image', mapped);
298
+ }
@@ -0,0 +1,9 @@
1
+ import { type IStyleProp, type IViewStyle } from '@symbiote-native/engine';
2
+ import { type IDescriptor } from '../descriptor';
3
+ export type IInputAccessoryViewViewProps = {
4
+ nativeID?: string;
5
+ backgroundColor?: string;
6
+ style?: IStyleProp<IViewStyle>;
7
+ passthrough: Record<string, unknown>;
8
+ };
9
+ export declare function renderInputAccessoryView(view: IInputAccessoryViewViewProps): IDescriptor;
@@ -0,0 +1,18 @@
1
+ // InputAccessoryView: the render half (framework-agnostic, iOS). A real Fabric host node,
2
+ // RCTInputAccessoryView, that docks its content above the keyboard. It is referenced by
3
+ // `nativeID`, which a TextInput points at through its `inputAccessoryViewID` prop; native pairs
4
+ // the two by id. There is no JS-side translation: style / nativeID / backgroundColor map straight
5
+ // onto the intrinsic and the user children (injected by the adapter) nest under it. Shared
6
+ // verbatim across adapters: React and Vue both bridge this Descriptor.
7
+ import { dlog } from '@symbiote-native/engine';
8
+ import { el } from '../descriptor';
9
+ export function renderInputAccessoryView(view) {
10
+ const props = { ...view.passthrough, style: view.style };
11
+ if (view.nativeID !== undefined)
12
+ props.nativeID = view.nativeID;
13
+ if (view.backgroundColor !== undefined)
14
+ props.backgroundColor = view.backgroundColor;
15
+ dlog('InputAccessoryView -> RCTInputAccessoryView');
16
+ // Empty structural children: the adapter appends the user children directly under the host.
17
+ return el('symbiote-input-accessory-view', props, []);
18
+ }
@@ -0,0 +1,30 @@
1
+ import type { IStyleProp, IViewStyle } from '@symbiote-native/engine';
2
+ export type IKeyboardAvoidingBehavior = 'height' | 'position' | 'padding';
3
+ export declare const DEFAULT_VERTICAL_OFFSET = 0;
4
+ export interface IMeasuredFrame {
5
+ y: number;
6
+ height: number;
7
+ }
8
+ export interface IKeyboardFrame {
9
+ screenY: number;
10
+ height: number;
11
+ }
12
+ export declare function readKeyboardFrame(payload: unknown): IKeyboardFrame | undefined;
13
+ export declare function readLayoutFrame(layout: unknown): IMeasuredFrame | undefined;
14
+ export declare function computeInset(frame: IMeasuredFrame | undefined, keyboard: IKeyboardFrame | undefined, verticalOffset: number): number;
15
+ export type IKeyboardAvoidingLayout = {
16
+ kind: 'nested';
17
+ wrapperStyle?: IStyleProp<IViewStyle>;
18
+ innerStyle: IStyleProp<IViewStyle>;
19
+ } | {
20
+ kind: 'wrapper';
21
+ wrapperStyle?: IStyleProp<IViewStyle>;
22
+ };
23
+ export interface IResolveKeyboardAvoidingLayoutParams {
24
+ behavior?: IKeyboardAvoidingBehavior;
25
+ effectiveInset: number;
26
+ initialHeight?: number;
27
+ style?: IStyleProp<IViewStyle>;
28
+ contentContainerStyle?: IStyleProp<IViewStyle>;
29
+ }
30
+ export declare function resolveKeyboardAvoidingLayout(params: IResolveKeyboardAvoidingLayoutParams): IKeyboardAvoidingLayout;
@@ -0,0 +1,75 @@
1
+ // KeyboardAvoidingView: the pure logic + view-contract half (framework-agnostic). It owns
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
4
+ // lifecycle (subscribe to the Keyboard module, measure via onLayout, hold the inset in its
5
+ // reactive primitive) and assembles the wrapper element around its own opaque children.
6
+ //
7
+ // We do NOT emit a Descriptor tree here the way render-switch does, because KAV wraps
8
+ // arbitrary user-provided children (React nodes / Vue slots) that the Descriptor model can't
9
+ // carry. Instead we return a layout DESCRIPTION (which styles to apply and whether to nest)
10
+ // and the adapter builds its own element tree with its children. Mirrors RN's
11
+ // Libraries/Components/Keyboard/KeyboardAvoidingView.js inset/behavior logic.
12
+ // RN's default keyboardVerticalOffset (KeyboardAvoidingView.js).
13
+ export const DEFAULT_VERTICAL_OFFSET = 0;
14
+ // RN rounds nothing here; 'height' mode collapses flex so the shrunk height holds.
15
+ const COLLAPSED_FLEX = 0;
16
+ function isRecord(value) {
17
+ return typeof value === 'object' && value !== null;
18
+ }
19
+ // Pull the keyboard's top edge (screenY) and height off the raw native payload. The shape is
20
+ // the consumer's knowledge, so we narrow `unknown` here rather than trust a type, no `as`.
21
+ // Returns undefined when the payload isn't a keyboard frame.
22
+ export function readKeyboardFrame(payload) {
23
+ if (!isRecord(payload))
24
+ return undefined;
25
+ const end = payload.endCoordinates;
26
+ if (!isRecord(end))
27
+ return undefined;
28
+ const { screenY, height } = end;
29
+ if (typeof screenY !== 'number' || typeof height !== 'number')
30
+ return undefined;
31
+ return { screenY, height };
32
+ }
33
+ // Pull the measured wrapper frame ({ y, height }) off a raw onLayout layout object. Returns
34
+ // undefined when the shape doesn't carry both numbers, so a bad payload never poisons the math.
35
+ export function readLayoutFrame(layout) {
36
+ if (!isRecord(layout))
37
+ return undefined;
38
+ const { y, height } = layout;
39
+ if (typeof y !== 'number' || typeof height !== 'number')
40
+ return undefined;
41
+ return { y, height };
42
+ }
43
+ // RN's _relativeKeyboardHeight: how far up the view must move so it no longer overlaps the
44
+ // keyboard. keyboardY is the keyboard's top edge minus the caller's vertical offset; the inset
45
+ // is the overlap of the view's bottom past that edge, clamped at 0.
46
+ export function computeInset(frame, keyboard, verticalOffset) {
47
+ if (frame === undefined || keyboard === undefined)
48
+ return 0;
49
+ const keyboardY = keyboard.screenY - verticalOffset;
50
+ return Math.max(frame.y + frame.height - keyboardY, 0);
51
+ }
52
+ // Map the behavior + effective inset onto the wrapper/inner styles and the nesting decision:
53
+ // the framework-agnostic core of RN's render(). 'position' nests; the others adjust the
54
+ // wrapper directly. 'height' shrinks the wrapper from its initial measured height (only while
55
+ // the keyboard is up, matching RN).
56
+ export function resolveKeyboardAvoidingLayout(params) {
57
+ const { behavior, effectiveInset, initialHeight, style, contentContainerStyle } = params;
58
+ if (behavior === 'position') {
59
+ return {
60
+ kind: 'nested',
61
+ wrapperStyle: style,
62
+ innerStyle: [contentContainerStyle, { bottom: effectiveInset }],
63
+ };
64
+ }
65
+ if (behavior === 'padding') {
66
+ return { kind: 'wrapper', wrapperStyle: [style, { paddingBottom: effectiveInset }] };
67
+ }
68
+ if (behavior === 'height' && effectiveInset > 0 && initialHeight !== undefined) {
69
+ return {
70
+ kind: 'wrapper',
71
+ wrapperStyle: [style, { height: initialHeight - effectiveInset, flex: COLLAPSED_FLEX }],
72
+ };
73
+ }
74
+ return { kind: 'wrapper', wrapperStyle: style };
75
+ }
@@ -0,0 +1,23 @@
1
+ import { type IStyleProp, type IViewStyle } from '@symbiote-native/engine';
2
+ import { type IDescriptor } from '../descriptor';
3
+ export type IModalAnimationType = 'none' | 'slide' | 'fade';
4
+ export type IModalPresentationStyle = 'fullScreen' | 'pageSheet' | 'formSheet' | 'overFullScreen';
5
+ export type IModalOrientation = 'portrait' | 'portrait-upside-down' | 'landscape' | 'landscape-left' | 'landscape-right';
6
+ export interface IModalOrientationChangeEvent {
7
+ orientation: 'portrait' | 'landscape';
8
+ }
9
+ export type IModalViewProps = {
10
+ visible?: boolean;
11
+ transparent?: boolean;
12
+ backdropColor?: string;
13
+ animationType?: IModalAnimationType;
14
+ presentationStyle?: IModalPresentationStyle;
15
+ supportedOrientations?: ReadonlyArray<IModalOrientation>;
16
+ hardwareAccelerated?: boolean;
17
+ statusBarTranslucent?: boolean;
18
+ navigationBarTranslucent?: boolean;
19
+ allowSwipeDismissal?: boolean;
20
+ style?: IStyleProp<IViewStyle>;
21
+ passthrough: Record<string, unknown>;
22
+ };
23
+ export declare function renderModal(view: IModalViewProps): IDescriptor;
@@ -0,0 +1,70 @@
1
+ // Modal: the render half (framework-agnostic). RCTModalHostView is an ordinary Fabric host
2
+ // node: it lives in the SAME childSet and commits through the SAME completeRoot as the rest of
3
+ // the tree. The native iOS/Android view presents its own window internally; there is no second
4
+ // root or second surface on the JS side. So this is a thin render exactly like the others: it
5
+ // maps to the `symbiote-modal` intrinsic the host config routes to ModalHostView, wrapping a
6
+ // full-screen container View that holds the user children (injected by the adapter). Shared
7
+ // verbatim: React and Vue both bridge this Descriptor; the keep-alive state lives in state/modal.ts.
8
+ import { dlog } from '@symbiote-native/engine';
9
+ import { el } from '../descriptor';
10
+ // The full-screen box RN anchors the modal content in (Modal.js styles.container: [side]:0,
11
+ // top:0, flex:1, backgroundColor:'white'). It is NOT position:absolute, it is a flex child that
12
+ // fills the ModalHostView, whose shadow node self-sizes to the screen
13
+ // (ModalHostViewComponentDescriptor sets the node size to screenSize). An absolute container with
14
+ // only top/left would collapse to its content instead. The backdrop color is layered on at render
15
+ // time so transparent/backdropColor win.
16
+ const CONTAINER_STYLE = {
17
+ left: 0,
18
+ top: 0,
19
+ flex: 1,
20
+ };
21
+ // RN sets styles.modal (position:'absolute') on RCTModalHostView itself (Modal.js styles.modal +
22
+ // style={styles.modal} on the host).
23
+ const MODAL_HOST_STYLE = {
24
+ position: 'absolute',
25
+ };
26
+ const TRANSPARENT_BACKDROP = 'transparent';
27
+ const OPAQUE_BACKDROP = 'white';
28
+ const DEFAULT_ANIMATION_TYPE = 'none';
29
+ // presentationStyle default (Modal.js: undefined -> 'fullScreen', but transparent flips it to
30
+ // 'overFullScreen').
31
+ const PRESENTATION_FULL_SCREEN = 'fullScreen';
32
+ const PRESENTATION_OVER_FULL_SCREEN = 'overFullScreen';
33
+ export function renderModal(view) {
34
+ // Only override backgroundColor when transparent or backdropColor are explicitly set, so these
35
+ // Modal-specific props take precedence over the generic style prop (Modal.js: containerStyles
36
+ // composed LAST in [styles.container, props.style, containerStyles]).
37
+ const backdropOverride = view.transparent === true
38
+ ? { backgroundColor: TRANSPARENT_BACKDROP }
39
+ : view.backdropColor !== undefined
40
+ ? { backgroundColor: view.backdropColor }
41
+ : {};
42
+ const containerStyle = [
43
+ { ...CONTAINER_STYLE, backgroundColor: OPAQUE_BACKDROP },
44
+ view.style,
45
+ backdropOverride,
46
+ ];
47
+ const resolvedPresentationStyle = view.presentationStyle ??
48
+ (view.transparent === true ? PRESENTATION_OVER_FULL_SCREEN : PRESENTATION_FULL_SCREEN);
49
+ dlog('Modal visible -> committing ModalHostView(container View)');
50
+ // collapsable:false keeps the container as a real shadow node (RN sets this so the wrapper is
51
+ // never flattened away under the host). Empty structural children: the adapter injects the
52
+ // user children UNDER this container, never as a direct sibling of the host.
53
+ const container = el('symbiote-view', { style: containerStyle, collapsable: false }, []);
54
+ return el('symbiote-modal', {
55
+ ...view.passthrough,
56
+ style: MODAL_HOST_STYLE,
57
+ transparent: view.transparent,
58
+ animationType: view.animationType ?? DEFAULT_ANIMATION_TYPE,
59
+ presentationStyle: resolvedPresentationStyle,
60
+ // Platform props named-forwarded to match RCTModalHostView (Modal.js ~336-350): iOS
61
+ // supportedOrientations/allowSwipeDismissal, Android hardwareAccelerated/
62
+ // statusBarTranslucent/navigationBarTranslucent.
63
+ supportedOrientations: view.supportedOrientations,
64
+ hardwareAccelerated: view.hardwareAccelerated,
65
+ statusBarTranslucent: view.statusBarTranslucent,
66
+ navigationBarTranslucent: view.navigationBarTranslucent,
67
+ allowSwipeDismissal: view.allowSwipeDismissal,
68
+ visible: view.visible,
69
+ }, [container]);
70
+ }
@@ -0,0 +1,8 @@
1
+ import type { IAccessibilityStateValue } from '../accessibility-props';
2
+ import type { IPressHandlers } from '../state/pressable';
3
+ export declare function resolveDisabledAccessibilityState(accessibilityState: IAccessibilityStateValue | undefined, disabled: boolean | undefined): IAccessibilityStateValue | undefined;
4
+ export declare function buildPressableListeners(handlers: IPressHandlers, options: {
5
+ disabled?: boolean;
6
+ cancelable?: boolean;
7
+ }): Record<string, unknown>;
8
+ export declare function noteHoverNoop(onHoverIn: unknown, onHoverOut: unknown): void;
@@ -0,0 +1,42 @@
1
+ // Pressable: the render half (framework-agnostic). Pressable owns no host element of its own:
2
+ // it composes the adapter's View (so children stay framework nodes), so this layer does not paint
3
+ // a Descriptor. It resolves the two prop decisions that are identical across adapters: which
4
+ // listeners the responder View carries (gated on disabled + cancelable), and how `disabled` folds
5
+ // into accessibilityState. The adapter feeds these into its View element. Pure, no framework.
6
+ import { dlog } from '@symbiote-native/engine';
7
+ // RN merges `disabled` into the resolved accessibilityState so a disabled Pressable reports the
8
+ // disabled state even if the caller passed none (Pressable.js: disabled != null ? {...state,
9
+ // disabled} : state). Untouched when disabled is unset.
10
+ export function resolveDisabledAccessibilityState(accessibilityState, disabled) {
11
+ return disabled !== undefined ? { ...accessibilityState, disabled } : accessibilityState;
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).
17
+ export function buildPressableListeners(handlers, options) {
18
+ if (options.disabled === true) {
19
+ dlog('Pressable disabled — listeners suppressed');
20
+ return {};
21
+ }
22
+ const listeners = {
23
+ onPress: handlers.handlePress,
24
+ onPressIn: handlers.handlePressIn,
25
+ onPressOut: handlers.handlePressOut,
26
+ // Claim the responder so the move stream reaches this View; retention reads it.
27
+ onStartShouldSetResponder: () => true,
28
+ onResponderMove: handlers.handleResponderMove,
29
+ };
30
+ if (options.cancelable !== undefined) {
31
+ listeners.onResponderTerminationRequest = () => options.cancelable;
32
+ }
33
+ return listeners;
34
+ }
35
+ // Hover has no event on a touch host: there is no pointer-enter/leave. The adapter accepts and
36
+ // types the RN hover props but forwards nothing; this records the no-op so a missing hover
37
+ // callback on device is explained, not silent (RN onHoverIn/onHoverOut).
38
+ export function noteHoverNoop(onHoverIn, onHoverOut) {
39
+ if (onHoverIn !== undefined || onHoverOut !== undefined) {
40
+ dlog('Pressable hover is a no-op on this host (no pointer-enter/leave event)');
41
+ }
42
+ }
@@ -0,0 +1,24 @@
1
+ import type { AnimatedValue, ISymbioteEvent } from '@symbiote-native/engine';
2
+ export declare const STICKY_HEADER_Z_INDEX = 10;
3
+ export declare function stickyDebounceMs(os: string): number;
4
+ export type IStickyHeaderProps = {
5
+ nextHeaderLayoutY: number | undefined;
6
+ onLayout: (event: ISymbioteEvent) => void;
7
+ scrollAnimatedValue: AnimatedValue;
8
+ inverted: boolean | undefined;
9
+ scrollViewHeight: number | undefined;
10
+ };
11
+ export declare function readLayoutNumber(event: ISymbioteEvent, key: 'y' | 'height'): number | undefined;
12
+ export type IStickyInterpolationParams = {
13
+ measured: boolean;
14
+ inverted: boolean | undefined;
15
+ scrollViewHeight: number | undefined;
16
+ layoutY: number;
17
+ layoutHeight: number;
18
+ nextHeaderLayoutY: number | undefined;
19
+ };
20
+ export declare function computeStickyInterpolation(params: IStickyInterpolationParams): {
21
+ inputRange: number[];
22
+ outputRange: number[];
23
+ };
24
+ export declare function nextStickyHeaderY(stickyHeaderIndices: number[], indexOfIndex: number, headerLayoutYs: ReadonlyMap<number, number>): number | undefined;