@symbiote-native/react 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 (148) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +149 -0
  3. package/build/components/activity-indicator/index.android.d.ts +2 -0
  4. package/build/components/activity-indicator/index.android.js +13 -0
  5. package/build/components/activity-indicator/index.d.ts +1 -0
  6. package/build/components/activity-indicator/index.ios.d.ts +2 -0
  7. package/build/components/activity-indicator/index.ios.js +10 -0
  8. package/build/components/activity-indicator/index.js +4 -0
  9. package/build/components/activity-indicator/shared.d.ts +7 -0
  10. package/build/components/activity-indicator/shared.js +20 -0
  11. package/build/components/button.d.ts +6 -0
  12. package/build/components/button.js +42 -0
  13. package/build/components/drawer-layout-android/index.android.d.ts +3 -0
  14. package/build/components/drawer-layout-android/index.android.js +93 -0
  15. package/build/components/drawer-layout-android/index.d.ts +3 -0
  16. package/build/components/drawer-layout-android/index.js +19 -0
  17. package/build/components/drawer-layout-android/shared.d.ts +19 -0
  18. package/build/components/drawer-layout-android/shared.js +7 -0
  19. package/build/components/flat-list/index.d.ts +73 -0
  20. package/build/components/flat-list/index.js +86 -0
  21. package/build/components/image/index.d.ts +9 -0
  22. package/build/components/image/index.js +28 -0
  23. package/build/components/image-background/index.d.ts +10 -0
  24. package/build/components/image-background/index.js +46 -0
  25. package/build/components/input-accessory-view/index.d.ts +11 -0
  26. package/build/components/input-accessory-view/index.js +14 -0
  27. package/build/components/keyboard-avoiding-view/index.d.ts +17 -0
  28. package/build/components/keyboard-avoiding-view/index.js +85 -0
  29. package/build/components/modal/index.d.ts +24 -0
  30. package/build/components/modal/index.js +56 -0
  31. package/build/components/pressable/index.d.ts +30 -0
  32. package/build/components/pressable/index.js +100 -0
  33. package/build/components/refresh-control/index.d.ts +17 -0
  34. package/build/components/refresh-control/index.js +21 -0
  35. package/build/components/safe-area-view/index.d.ts +11 -0
  36. package/build/components/safe-area-view/index.js +18 -0
  37. package/build/components/scroll-view/index.android.d.ts +3 -0
  38. package/build/components/scroll-view/index.android.js +53 -0
  39. package/build/components/scroll-view/index.d.ts +1 -0
  40. package/build/components/scroll-view/index.ios.d.ts +3 -0
  41. package/build/components/scroll-view/index.ios.js +27 -0
  42. package/build/components/scroll-view/index.js +4 -0
  43. package/build/components/scroll-view/shared.d.ts +98 -0
  44. package/build/components/scroll-view/shared.js +195 -0
  45. package/build/components/scroll-view/sticky-header.d.ts +9 -0
  46. package/build/components/scroll-view/sticky-header.js +161 -0
  47. package/build/components/section-list/index.d.ts +8 -0
  48. package/build/components/section-list/index.js +12 -0
  49. package/build/components/switch/index.android.d.ts +2 -0
  50. package/build/components/switch/index.android.js +12 -0
  51. package/build/components/switch/index.d.ts +1 -0
  52. package/build/components/switch/index.ios.d.ts +2 -0
  53. package/build/components/switch/index.ios.js +10 -0
  54. package/build/components/switch/index.js +6 -0
  55. package/build/components/switch/shared.d.ts +10 -0
  56. package/build/components/switch/shared.js +59 -0
  57. package/build/components/text-input/index.d.ts +50 -0
  58. package/build/components/text-input/index.js +167 -0
  59. package/build/components/touchable/index.d.ts +20 -0
  60. package/build/components/touchable/index.js +95 -0
  61. package/build/components/touchable-native-feedback/index.d.ts +18 -0
  62. package/build/components/touchable-native-feedback/index.js +32 -0
  63. package/build/components/virtualized-list/index.d.ts +73 -0
  64. package/build/components/virtualized-list/index.js +552 -0
  65. package/build/components/virtualized-section-list/index.d.ts +66 -0
  66. package/build/components/virtualized-section-list/index.js +99 -0
  67. package/build/components.d.ts +54 -0
  68. package/build/components.js +17 -0
  69. package/build/create-portal.d.ts +4 -0
  70. package/build/create-portal.js +23 -0
  71. package/build/create-tunnel.d.ts +12 -0
  72. package/build/create-tunnel.js +60 -0
  73. package/build/descriptor-to-react/index.d.ts +3 -0
  74. package/build/descriptor-to-react/index.js +15 -0
  75. package/build/hooks/use-color-scheme.d.ts +2 -0
  76. package/build/hooks/use-color-scheme.js +13 -0
  77. package/build/hooks/use-window-dimensions.d.ts +2 -0
  78. package/build/hooks/use-window-dimensions.js +30 -0
  79. package/build/host-config.d.ts +7 -0
  80. package/build/host-config.js +169 -0
  81. package/build/host-instance.d.ts +3 -0
  82. package/build/host-instance.js +20 -0
  83. package/build/index.d.ts +98 -0
  84. package/build/index.js +85 -0
  85. package/build/modules/accessibility-info/index.android.d.ts +2 -0
  86. package/build/modules/accessibility-info/index.android.js +4 -0
  87. package/build/modules/accessibility-info/index.d.ts +1 -0
  88. package/build/modules/accessibility-info/index.ios.d.ts +2 -0
  89. package/build/modules/accessibility-info/index.ios.js +3 -0
  90. package/build/modules/accessibility-info/index.js +5 -0
  91. package/build/modules/accessibility-info/shared.d.ts +1 -0
  92. package/build/modules/accessibility-info/shared.js +1 -0
  93. package/build/modules/action-sheet-ios.d.ts +2 -0
  94. package/build/modules/action-sheet-ios.js +4 -0
  95. package/build/modules/alert.d.ts +2 -0
  96. package/build/modules/alert.js +5 -0
  97. package/build/modules/animated/create-animated-component.d.ts +12 -0
  98. package/build/modules/animated/create-animated-component.js +125 -0
  99. package/build/modules/animated/index.d.ts +52 -0
  100. package/build/modules/animated/index.js +84 -0
  101. package/build/modules/app-registry/index.d.ts +7 -0
  102. package/build/modules/app-registry/index.js +26 -0
  103. package/build/modules/app-state.d.ts +2 -0
  104. package/build/modules/app-state.js +3 -0
  105. package/build/modules/appearance.d.ts +2 -0
  106. package/build/modules/appearance.js +3 -0
  107. package/build/modules/back-handler.d.ts +2 -0
  108. package/build/modules/back-handler.js +3 -0
  109. package/build/modules/dimensions.d.ts +2 -0
  110. package/build/modules/dimensions.js +3 -0
  111. package/build/modules/i18n-manager.d.ts +2 -0
  112. package/build/modules/i18n-manager.js +4 -0
  113. package/build/modules/keyboard.d.ts +2 -0
  114. package/build/modules/keyboard.js +3 -0
  115. package/build/modules/layout-animation.d.ts +2 -0
  116. package/build/modules/layout-animation.js +3 -0
  117. package/build/modules/linking.d.ts +2 -0
  118. package/build/modules/linking.js +4 -0
  119. package/build/modules/permissions-android.d.ts +2 -0
  120. package/build/modules/permissions-android.js +3 -0
  121. package/build/modules/settings.d.ts +1 -0
  122. package/build/modules/settings.js +4 -0
  123. package/build/modules/share.d.ts +2 -0
  124. package/build/modules/share.js +4 -0
  125. package/build/modules/status-bar/index.android.d.ts +3 -0
  126. package/build/modules/status-bar/index.android.js +89 -0
  127. package/build/modules/status-bar/index.d.ts +1 -0
  128. package/build/modules/status-bar/index.ios.d.ts +3 -0
  129. package/build/modules/status-bar/index.ios.js +64 -0
  130. package/build/modules/status-bar/index.js +4 -0
  131. package/build/modules/status-bar/shared.d.ts +6 -0
  132. package/build/modules/status-bar/shared.js +6 -0
  133. package/build/modules/text-input-state.d.ts +1 -0
  134. package/build/modules/text-input-state.js +3 -0
  135. package/build/modules/toast-android.d.ts +1 -0
  136. package/build/modules/toast-android.js +4 -0
  137. package/build/modules/vibration.d.ts +1 -0
  138. package/build/modules/vibration.js +4 -0
  139. package/build/reconciler-constants.d.ts +7 -0
  140. package/build/reconciler-constants.js +12 -0
  141. package/build/render.d.ts +7 -0
  142. package/build/render.js +60 -0
  143. package/build/utils/responder-props.d.ts +1 -0
  144. package/build/utils/responder-props.js +1 -0
  145. package/build/utils/styles.d.ts +1 -0
  146. package/build/utils/styles.js +1 -0
  147. package/metro-css-parser.cjs +11 -0
  148. package/package.json +53 -0
@@ -0,0 +1,28 @@
1
+ import {} from 'react';
2
+ import { imageStatics, renderImage, resolveAccessibilityProps, } from '@symbiote-native/components';
3
+ import { descriptorToReact } from '../../descriptor-to-react';
4
+ // renderImage takes a pre-resolved view + passthrough (the ISwitchViewProps shape), so the
5
+ // adapter folds aria/role and splits the typed transform fields from the forward-only rest.
6
+ // The W3C source aliases (src/srcSet/crossOrigin/referrerPolicy) and width/height become typed
7
+ // view fields (consumed by the source/style fold), so they never leak to Fabric via passthrough.
8
+ const ImageComponent = rawProps => {
9
+ const { source, defaultSource, loadingIndicatorSource, style, resizeMode, tintColor, src, srcSet, alt, width, height, crossOrigin, referrerPolicy, ...passthrough } = resolveAccessibilityProps(rawProps);
10
+ return descriptorToReact(renderImage({
11
+ source,
12
+ defaultSource,
13
+ loadingIndicatorSource,
14
+ style,
15
+ resizeMode,
16
+ tintColor,
17
+ src,
18
+ srcSet,
19
+ alt,
20
+ width,
21
+ height,
22
+ crossOrigin,
23
+ referrerPolicy,
24
+ passthrough,
25
+ }));
26
+ };
27
+ export const Image = Object.assign(ImageComponent, imageStatics);
28
+ export { setImageSourceResolver } from '@symbiote-native/components';
@@ -0,0 +1,10 @@
1
+ import { type ReactElement, type ReactNode } from 'react';
2
+ import { type IImageProps } from '@symbiote-native/components';
3
+ import type { IStyleProp, IViewStyle } from '../../utils/styles';
4
+ export interface IImageBackgroundProps extends Omit<IImageProps, 'style'> {
5
+ style?: IStyleProp<IViewStyle>;
6
+ imageStyle?: IStyleProp<IViewStyle> | string;
7
+ className?: string;
8
+ children?: ReactNode;
9
+ }
10
+ export declare function ImageBackground(props: IImageBackgroundProps): ReactElement;
@@ -0,0 +1,46 @@
1
+ // ImageBackground: the React lifecycle half. The composition (the absolute-fill Image behind
2
+ // the children, the dimension-proxy + style-merge math) lives framework-agnostic in
3
+ // @symbiote-native/components/renderImageBackground and is shared verbatim with Vue; here React only
4
+ // folds aria/role, splits the typed Image transform fields from the forward-only rest, bridges
5
+ // the Descriptor to elements, and appends the user children ON TOP of the inner image.
6
+ import { createElement } from 'react';
7
+ import { resolveClassName } from '@symbiote-native/engine';
8
+ import { renderImageBackground, resolveAccessibilityProps, } from '@symbiote-native/components';
9
+ import { descriptorToReact } from '../../descriptor-to-react';
10
+ function toChild(child) {
11
+ return typeof child === 'string' ? child : descriptorToReact(child);
12
+ }
13
+ export function ImageBackground(props) {
14
+ // className is pulled out here, like style, and applied to the WRAPPER element below — left
15
+ // in ...imageProps it would fall into `passthrough` and land on the INNER Image instead
16
+ // (renderImageBackground's `image.passthrough`, not the wrapper's own props).
17
+ const { children, style, imageStyle, className, ...imageProps } = props;
18
+ // imageStyle targets the INNER image (renderImageBackground's own field), unlike className
19
+ // above which lands on the wrapper — resolve the string here, before it flows in below.
20
+ const resolvedImageStyle = typeof imageStyle === 'string' ? resolveClassName(imageStyle) : imageStyle;
21
+ // The W3C source aliases (src/srcSet/crossOrigin/referrerPolicy) and width/height become typed
22
+ // Image view fields (consumed by the source/style fold), so they never leak to Fabric via
23
+ // passthrough; everything else (events, blurRadius, the folded accessibility*, testID) forwards.
24
+ const { source, defaultSource, loadingIndicatorSource, resizeMode, tintColor, src, srcSet, alt, width, height, crossOrigin, referrerPolicy, ...passthrough } = resolveAccessibilityProps(imageProps);
25
+ const wrapper = renderImageBackground({
26
+ style,
27
+ imageStyle: resolvedImageStyle,
28
+ image: {
29
+ source,
30
+ defaultSource,
31
+ loadingIndicatorSource,
32
+ resizeMode,
33
+ tintColor,
34
+ src,
35
+ srcSet,
36
+ alt,
37
+ width,
38
+ height,
39
+ crossOrigin,
40
+ referrerPolicy,
41
+ passthrough,
42
+ },
43
+ });
44
+ // wrapper = symbiote-view > [imageDescriptor]; the user children paint AFTER the image (on top).
45
+ return createElement(wrapper.type, { key: wrapper.key, ...wrapper.props, className }, ...wrapper.children.map(toChild), children);
46
+ }
@@ -0,0 +1,11 @@
1
+ import { type FC, type ReactNode } from 'react';
2
+ import { type IAccessibilityProps, type IAriaProps } from '@symbiote-native/components';
3
+ import type { IStyleProp, IViewStyle } from '../../utils/styles';
4
+ export interface IInputAccessoryViewProps extends IAccessibilityProps, IAriaProps {
5
+ nativeID?: string;
6
+ backgroundColor?: string;
7
+ style?: IStyleProp<IViewStyle>;
8
+ className?: string;
9
+ children?: ReactNode;
10
+ }
11
+ export declare const InputAccessoryView: FC<IInputAccessoryViewProps>;
@@ -0,0 +1,14 @@
1
+ // InputAccessoryView: the React lifecycle half (iOS). The host-node assembly (nativeID /
2
+ // backgroundColor / style / accessibility forwarding) lives framework-agnostic in
3
+ // @symbiote-native/components/renderInputAccessoryView and is shared verbatim with Vue; here React only
4
+ // folds aria/role, bridges the Descriptor, and nests the user children under the host.
5
+ import { createElement } from 'react';
6
+ import { renderInputAccessoryView, resolveAccessibilityProps, } from '@symbiote-native/components';
7
+ export const InputAccessoryView = rawProps => {
8
+ // Owns its host element (symbiote-input-accessory-view), so it folds aria/role here; the
9
+ // resolved accessibility* surface rides the node via `...passthrough`.
10
+ const { nativeID, backgroundColor, style, children, ...passthrough } = resolveAccessibilityProps(rawProps);
11
+ const host = renderInputAccessoryView({ nativeID, backgroundColor, style, passthrough });
12
+ // host has no structural children; the user children nest directly under it.
13
+ return createElement(host.type, { key: host.key, ...host.props }, children);
14
+ };
@@ -0,0 +1,17 @@
1
+ import { type FC, type ReactNode } from 'react';
2
+ import { type ISymbioteEvent } from '@symbiote-native/engine';
3
+ import { type IKeyboardAvoidingBehavior } from '@symbiote-native/components';
4
+ import type { IAccessibilityProps, IAriaProps } from '@symbiote-native/components';
5
+ import type { IStyleProp, IViewStyle } from '../../utils/styles';
6
+ export type { IKeyboardAvoidingBehavior } from '@symbiote-native/components';
7
+ export interface IKeyboardAvoidingViewProps extends IAccessibilityProps, IAriaProps {
8
+ behavior?: IKeyboardAvoidingBehavior;
9
+ enabled?: boolean;
10
+ keyboardVerticalOffset?: number;
11
+ contentContainerStyle?: IStyleProp<IViewStyle>;
12
+ style?: IStyleProp<IViewStyle>;
13
+ className?: string;
14
+ children?: ReactNode;
15
+ onLayout?: (event: ISymbioteEvent) => void;
16
+ }
17
+ export declare const KeyboardAvoidingView: FC<IKeyboardAvoidingViewProps>;
@@ -0,0 +1,85 @@
1
+ // KeyboardAvoidingView: composes the host View and shifts it out of the keyboard's way as
2
+ // the keyboard shows/hides. It subscribes to the Keyboard module (native->JS events) and
3
+ // recomputes a bottom inset from the keyboard frame and the view's own measured frame.
4
+ // Mirrors RN's Libraries/Components/Keyboard/KeyboardAvoidingView.js, as a function component.
5
+ //
6
+ // The inset math + the behavior → style/structure decision are framework-agnostic and live in
7
+ // @symbiote-native/components (render-keyboard-avoiding-view), shared verbatim with the Vue adapter.
8
+ // React supplies only the lifecycle: useState for the inset, useRef for the measured frame, a
9
+ // useEffect subscription, and the descriptor-free element assembly around its children.
10
+ import { createElement, useEffect, useRef, useState, } from 'react';
11
+ import { dlog } from '@symbiote-native/engine';
12
+ import { computeInset, readKeyboardFrame, readLayoutFrame, resolveKeyboardAvoidingLayout, DEFAULT_VERTICAL_OFFSET, } from '@symbiote-native/components';
13
+ import { View } from '../../components';
14
+ import { Keyboard, KEYBOARD_EVENT } from '../../modules/keyboard';
15
+ export const KeyboardAvoidingView = props => {
16
+ const { behavior, enabled = true, keyboardVerticalOffset = DEFAULT_VERTICAL_OFFSET, contentContainerStyle, style, children, onLayout,
17
+ // The wrapper is the View FC, which runs resolveAccessibilityProps itself, so
18
+ // the raw aria/role + accessibility* props pass through untouched here and fold
19
+ // there once.
20
+ ...accessibilityRest } = props;
21
+ const [inset, setInset] = useState(0);
22
+ // Mutable, not state: changing the frame alone shouldn't re-render; it feeds the
23
+ // next keyboard event's inset math.
24
+ const frameRef = useRef(undefined);
25
+ const initialHeightRef = useRef(undefined);
26
+ useEffect(() => {
27
+ const onShow = (payload) => {
28
+ const keyboard = readKeyboardFrame(payload);
29
+ const next = computeInset(frameRef.current, keyboard, keyboardVerticalOffset);
30
+ dlog(`KeyboardAvoidingView show -> inset ${next}`);
31
+ setInset(next);
32
+ };
33
+ const onHide = () => {
34
+ dlog('KeyboardAvoidingView hide -> inset 0');
35
+ setInset(0);
36
+ };
37
+ const subscriptions = [
38
+ Keyboard.addListener(KEYBOARD_EVENT.didShow, onShow),
39
+ Keyboard.addListener(KEYBOARD_EVENT.didChangeFrame, onShow),
40
+ Keyboard.addListener(KEYBOARD_EVENT.didHide, onHide),
41
+ ];
42
+ return () => {
43
+ for (const subscription of subscriptions)
44
+ subscription.remove();
45
+ };
46
+ }, [keyboardVerticalOffset]);
47
+ const handleLayout = (event) => {
48
+ const frame = readLayoutFrame(event.nativeEvent.layout);
49
+ if (frame !== undefined) {
50
+ frameRef.current = frame;
51
+ if (initialHeightRef.current === undefined)
52
+ initialHeightRef.current = frame.height;
53
+ }
54
+ onLayout?.(event);
55
+ };
56
+ // When disabled the inset is forced to 0, so every behavior mode renders the view
57
+ // untouched (RN gates each bottomHeight/height computation on `enabled ?? true`).
58
+ const effectiveInset = enabled ? inset : 0;
59
+ const layout = resolveKeyboardAvoidingLayout({
60
+ behavior,
61
+ effectiveInset,
62
+ initialHeight: initialHeightRef.current,
63
+ style,
64
+ contentContainerStyle,
65
+ });
66
+ // 'nested' ('position') pushes the content in an inner View by `bottom: inset`; the wrapper
67
+ // modes adjust the single wrapper directly.
68
+ if (layout.kind === 'nested') {
69
+ return renderWrapper(layout.wrapperStyle, createElement(View, { style: layout.innerStyle }, children));
70
+ }
71
+ return renderWrapper(layout.wrapperStyle, children);
72
+ // The wrapper carries onLayout. The View FC's public props don't surface it, but
73
+ // `symbiote-view` routes the base layout event at runtime; widen the props through
74
+ // a typed variable (no inline-literal excess-property check, no `as`) so the
75
+ // onLayout reaches the host without editing View's public type.
76
+ function renderWrapper(wrapStyle, content) {
77
+ const wrapperProps = {
78
+ ...accessibilityRest,
79
+ style: wrapStyle,
80
+ onLayout: handleLayout,
81
+ children: content,
82
+ };
83
+ return createElement(View, wrapperProps);
84
+ }
85
+ };
@@ -0,0 +1,24 @@
1
+ import { type FC, type ReactNode } from 'react';
2
+ import { type IAccessibilityProps, type IAriaProps, type IModalAnimationType, type IModalOrientation, type IModalOrientationChangeEvent, type IModalPresentationStyle } from '@symbiote-native/components';
3
+ import type { IStyleProp, IViewStyle } from '../../utils/styles';
4
+ export type { IModalAnimationType, IModalPresentationStyle, IModalOrientation, IModalOrientationChangeEvent, } from '@symbiote-native/components';
5
+ export interface IModalProps extends IAccessibilityProps, IAriaProps {
6
+ visible?: boolean;
7
+ transparent?: boolean;
8
+ backdropColor?: string;
9
+ animationType?: IModalAnimationType;
10
+ presentationStyle?: IModalPresentationStyle;
11
+ supportedOrientations?: ReadonlyArray<IModalOrientation>;
12
+ hardwareAccelerated?: boolean;
13
+ statusBarTranslucent?: boolean;
14
+ navigationBarTranslucent?: boolean;
15
+ allowSwipeDismissal?: boolean;
16
+ onShow?: () => void;
17
+ onDismiss?: () => void;
18
+ onRequestClose?: () => void;
19
+ onOrientationChange?: (event: IModalOrientationChangeEvent) => void;
20
+ style?: IStyleProp<IViewStyle>;
21
+ className?: string;
22
+ children?: ReactNode;
23
+ }
24
+ export declare const Modal: FC<IModalProps>;
@@ -0,0 +1,56 @@
1
+ // Modal: the React lifecycle half. RCTModalHostView is an ordinary Fabric host node committing
2
+ // through the same childSet as the rest of the tree (no second JS surface). The style math (the
3
+ // backdrop override, the container/host styles, the presentationStyle default), the visible gate,
4
+ // and the iOS keep-alive reducer all live framework-agnostic in @symbiote-native/components and are
5
+ // shared verbatim with Vue; here React supplies only the lifecycle: useReducer over the keep-alive
6
+ // state machine + a post-render effect to drive the visible→hidden transition, and the Descriptor
7
+ // bridge, nesting the user children UNDER the container View.
8
+ //
9
+ // Deferred vs RN: RN's native exit-animation timing (the modalDismissed emitter on old-renderer
10
+ // iOS) is not reproduced; onDismiss is delivered as the native topDismiss DirectEvent via the
11
+ // host's onDismiss prop (it rides `...passthrough`), and the keep-alive holds the node mounted
12
+ // through the exit transition. The native exit-animation timing is what's deferred, not the
13
+ // callback contract.
14
+ import { createElement, useEffect, useReducer } from 'react';
15
+ import { dlog } from '@symbiote-native/engine';
16
+ import { createInitialModalState, modalReducer, renderModal, resolveAccessibilityProps, shouldRenderModal, } from '@symbiote-native/components';
17
+ export const Modal = rawProps => {
18
+ // Modal owns its host element (symbiote-modal), so it folds aria/role here; the resolved fields
19
+ // ride the host node via `...passthrough`. The events (onShow/onDismiss/onRequestClose/
20
+ // onOrientationChange) are real ViewConfig DirectEvents, so they too ride passthrough raw.
21
+ // className is pulled out here, like style, and applied to the CONTAINER element below — left in
22
+ // ...passthrough it would land on the outer symbiote-modal host instead (renderModal composes
23
+ // `style` into the container's style, not the host's).
24
+ const { visible, transparent, backdropColor, animationType, presentationStyle, supportedOrientations, hardwareAccelerated, statusBarTranslucent, navigationBarTranslucent, allowSwipeDismissal, style, className, children, ...passthrough } = resolveAccessibilityProps(rawProps);
25
+ // The iOS keep-alive: the effect runs AFTER this render, so a visible→hidden transition keeps the
26
+ // node mounted for one frame (state.isRendered still true here) before the next render unmounts it,
27
+ // the same shape as RN's componentDidUpdate setState (see state/modal.ts).
28
+ const [state, dispatch] = useReducer(modalReducer, visible === true, createInitialModalState);
29
+ useEffect(() => {
30
+ dispatch(visible === true ? { type: 'show' } : { type: 'hide' });
31
+ }, [visible]);
32
+ if (!shouldRenderModal(visible === true, state)) {
33
+ dlog('Modal hidden -> no node committed');
34
+ return null;
35
+ }
36
+ const root = renderModal({
37
+ visible,
38
+ transparent,
39
+ backdropColor,
40
+ animationType,
41
+ presentationStyle,
42
+ supportedOrientations,
43
+ hardwareAccelerated,
44
+ statusBarTranslucent,
45
+ navigationBarTranslucent,
46
+ allowSwipeDismissal,
47
+ style,
48
+ passthrough,
49
+ });
50
+ // root = symbiote-modal > [container]; the user children nest UNDER the container View, never as
51
+ // a direct sibling of the host (RN's modal content layout).
52
+ const [container] = root.children;
53
+ if (typeof container === 'string')
54
+ return null;
55
+ return createElement(root.type, { key: root.key, ...root.props }, createElement(container.type, { key: container.key, ...container.props, className }, children));
56
+ };
@@ -0,0 +1,30 @@
1
+ import { type FC, type ReactNode } from 'react';
2
+ import { type IPressState, type IPressHandler, type IRectOffset, type IPressableAndroidRippleConfig } from '@symbiote-native/components';
3
+ import type { IAccessibilityProps, IAriaProps } from '@symbiote-native/components';
4
+ import type { IStyleProp, IViewStyle } from '../../utils/styles';
5
+ export type { IPressState, IPressableAndroidRippleConfig } from '@symbiote-native/components';
6
+ type IPressableStyle = IStyleProp<IViewStyle> | ((state: IPressState) => IStyleProp<IViewStyle>);
7
+ type IChildrenProp = ReactNode | ((state: IPressState) => ReactNode);
8
+ export interface IPressableProps extends IAccessibilityProps, IAriaProps {
9
+ onPress?: IPressHandler;
10
+ onPressIn?: IPressHandler;
11
+ onPressOut?: IPressHandler;
12
+ onPressMove?: IPressHandler;
13
+ onLongPress?: IPressHandler;
14
+ delayLongPress?: number;
15
+ disabled?: boolean;
16
+ cancelable?: boolean;
17
+ hitSlop?: IRectOffset;
18
+ pressRetentionOffset?: IRectOffset;
19
+ unstable_pressDelay?: number;
20
+ android_ripple?: IPressableAndroidRippleConfig;
21
+ android_disableSound?: boolean;
22
+ onHoverIn?: IPressHandler;
23
+ onHoverOut?: IPressHandler;
24
+ delayHoverIn?: number;
25
+ delayHoverOut?: number;
26
+ style?: IPressableStyle;
27
+ className?: string;
28
+ children?: IChildrenProp;
29
+ }
30
+ export declare const Pressable: FC<IPressableProps>;
@@ -0,0 +1,100 @@
1
+ // Pressable: the React lifecycle half. The press lifecycle (the long-press timer, unstable_press-
2
+ // Delay deferral, the pressRetentionOffset drift test, the suppression flags) lives in
3
+ // @symbiote-native/components/state as a pure machine over a runtime + host; the render decisions (the
4
+ // responder listeners, the disabled→accessibilityState fold, the ripple prop) in
5
+ // @symbiote-native/components/view. Here React owns only the lifecycle: useState for `pressed`, a ref for
6
+ // the runtime, a ref for the View instance (the measure handle), and a useMemo that rebuilds the
7
+ // handlers when the config changes. This is the React twin of the Vue adapter's setup-scope
8
+ // runtime + setPressed ref.
9
+ //
10
+ // Three RN interaction props that change real app feel ride on top of the shared synthesis,
11
+ // entirely in JS: android_ripple (an Android native-feedback prop on a dedicated inner View, inert
12
+ // on iOS), unstable_pressDelay, and pressRetentionOffset (the drift region), all handled by the
13
+ // shared machine; here we only wire its host + render the View.
14
+ import { createElement, useMemo, useRef, useState } from 'react';
15
+ import { createPressHandlers, createPressRuntime, rippleProps, buildPressableListeners, resolveDisabledAccessibilityState, noteHoverNoop, DEFAULT_DELAY_LONG_PRESS_MS, } from '@symbiote-native/components';
16
+ import { View } from '../../components';
17
+ function resolveStyle(style, state) {
18
+ if (typeof style === 'function')
19
+ return style(state);
20
+ return style;
21
+ }
22
+ function resolveChildren(children, state) {
23
+ if (typeof children === 'function')
24
+ return children(state);
25
+ return children;
26
+ }
27
+ export const Pressable = props => {
28
+ const { onPress, onPressIn, onPressOut, onPressMove, onLongPress, delayLongPress = DEFAULT_DELAY_LONG_PRESS_MS, disabled, cancelable, hitSlop, pressRetentionOffset, unstable_pressDelay = 0, android_ripple, android_disableSound, onHoverIn, onHoverOut, delayHoverIn, delayHoverOut, accessibilityState, testID, style, children,
29
+ // The remaining accessibility / aria props forward to View untouched; View runs
30
+ // resolveAccessibilityProps, so aria/role fold there, once.
31
+ ...accessibilityRest } = props;
32
+ const [pressed, setPressed] = useState(false);
33
+ // The mutable press runtime (timers, suppression flags, measured region), a ref so mutating it
34
+ // never triggers a re-render; only setPressed does.
35
+ const runtime = useRef(createPressRuntime()).current;
36
+ // The View instance, so the machine can measure its on-screen rect on responder grant.
37
+ const viewRef = useRef(null);
38
+ // The lifecycle seam the machine fills: flip pressed-state, and expose the View's raw measure.
39
+ const host = useMemo(() => ({
40
+ setPressed,
41
+ getMeasureFn: () => {
42
+ const view = viewRef.current;
43
+ if (view === null)
44
+ return undefined;
45
+ return callback => view.measure(callback);
46
+ },
47
+ schedule: (callback, ms) => {
48
+ const id = setTimeout(callback, ms);
49
+ return () => clearTimeout(id);
50
+ },
51
+ }), []);
52
+ const handlers = useMemo(() => createPressHandlers({
53
+ onPress,
54
+ onPressIn,
55
+ onPressOut,
56
+ onPressMove,
57
+ onLongPress,
58
+ delayLongPress,
59
+ unstable_pressDelay,
60
+ hitSlop,
61
+ pressRetentionOffset,
62
+ }, runtime, host), [
63
+ onPress,
64
+ onPressIn,
65
+ onPressOut,
66
+ onPressMove,
67
+ onLongPress,
68
+ delayLongPress,
69
+ unstable_pressDelay,
70
+ hitSlop,
71
+ pressRetentionOffset,
72
+ runtime,
73
+ host,
74
+ ]);
75
+ noteHoverNoop(onHoverIn, onHoverOut);
76
+ void delayHoverIn;
77
+ void delayHoverOut;
78
+ const state = { pressed };
79
+ const resolvedAccessibilityState = resolveDisabledAccessibilityState(accessibilityState, disabled);
80
+ const viewProps = {
81
+ ...accessibilityRest,
82
+ // The ref is the handle the retention measure reaches through (measure on grant).
83
+ ref: viewRef,
84
+ style: resolveStyle(style, state),
85
+ hitSlop,
86
+ accessibilityState: resolvedAccessibilityState,
87
+ testID,
88
+ };
89
+ // Forward the Android tap-sound suppressor under RN's own key; inert on iOS.
90
+ if (android_disableSound !== undefined)
91
+ viewProps.android_disableSound = android_disableSound;
92
+ Object.assign(viewProps, buildPressableListeners(handlers, { disabled, cancelable }));
93
+ // android_ripple rides a dedicated inner View (the Pressable's own View only forwards a fixed
94
+ // prop set), mirroring touchable-native-feedback. On iOS the ripple prop is undefined, so the
95
+ // child renders unwrapped, no extra node.
96
+ const ripple = android_ripple !== undefined ? rippleProps(android_ripple) : undefined;
97
+ const content = resolveChildren(children, state);
98
+ const inner = ripple !== undefined ? createElement(View, ripple, content) : content;
99
+ return createElement(View, viewProps, inner);
100
+ };
@@ -0,0 +1,17 @@
1
+ import { type FC, type ReactNode } from 'react';
2
+ import { type IAccessibilityProps, type IAriaProps } from '@symbiote-native/components';
3
+ export interface IRefreshControlProps extends IAccessibilityProps, IAriaProps {
4
+ refreshing: boolean;
5
+ onRefresh?: () => void | Promise<void>;
6
+ tintColor?: string;
7
+ title?: string;
8
+ titleColor?: string;
9
+ progressViewOffset?: number;
10
+ colors?: readonly string[];
11
+ progressBackgroundColor?: string;
12
+ size?: 'default' | 'large';
13
+ enabled?: boolean;
14
+ children?: ReactNode;
15
+ className?: string;
16
+ }
17
+ export declare const RefreshControl: FC<IRefreshControlProps>;
@@ -0,0 +1,21 @@
1
+ // RefreshControl primitive. On iOS this is the PullToRefreshView Fabric node that
2
+ // lives INSIDE a ScrollView (a sibling of the content container), giving the
3
+ // pull-to-refresh gesture. `refreshing` is a controlled prop: the parent owns it
4
+ // and pushes it down each commit; native reports the gesture via the direct
5
+ // `topRefresh` event, which shared routes to the `refresh` listener (onRefresh).
6
+ import { createElement } from 'react';
7
+ import { dlog } from '@symbiote-native/engine';
8
+ import { resolveAccessibilityProps, } from '@symbiote-native/components';
9
+ export const RefreshControl = rawProps => {
10
+ // Owns its host element (symbiote-refresh-control), so it folds aria/role here;
11
+ // the resolved accessibility* fields ride down via `...nativeProps`.
12
+ const props = resolveAccessibilityProps(rawProps);
13
+ const { children, ...nativeProps } = props;
14
+ dlog('RefreshControl -> PullToRefreshView');
15
+ dlog(`RefreshControl refreshing=${String(props.refreshing)}`);
16
+ if (props.enabled !== undefined)
17
+ dlog(`RefreshControl enabled=${String(props.enabled)} (Android-only)`);
18
+ if (props.onRefresh !== undefined)
19
+ dlog('RefreshControl onRefresh listener wired');
20
+ return createElement('symbiote-refresh-control', { ...nativeProps }, children);
21
+ };
@@ -0,0 +1,11 @@
1
+ import { type FC, type ReactNode } from 'react';
2
+ import { type ISymbioteEvent } from '@symbiote-native/engine';
3
+ import { type IAccessibilityProps, type IAriaProps } from '@symbiote-native/components';
4
+ import type { IStyleProp, IViewStyle } from '../../utils/styles';
5
+ export interface ISafeAreaViewProps extends IAccessibilityProps, IAriaProps {
6
+ style?: IStyleProp<IViewStyle>;
7
+ className?: string;
8
+ children?: ReactNode;
9
+ onLayout?: (event: ISymbioteEvent) => void;
10
+ }
11
+ export declare const SafeAreaView: FC<ISafeAreaViewProps>;
@@ -0,0 +1,18 @@
1
+ // SafeAreaView primitive. A plain view whose native side insets its children to
2
+ // the safe area (notch, rounded corners, system bars). There is no JS-side
3
+ // translation; RN just renders the native RCTSafeAreaView and lets the host do
4
+ // the inset math, so this maps style + children straight onto the intrinsic.
5
+ import { createElement } from 'react';
6
+ import { dlog } from '@symbiote-native/engine';
7
+ import { resolveAccessibilityProps, } from '@symbiote-native/components';
8
+ export const SafeAreaView = rawProps => {
9
+ // Owns its host element (symbiote-safe-area-view), so it folds aria/role here;
10
+ // the resolved accessibility* surface rides the node via `...accessibilityRest`.
11
+ const props = resolveAccessibilityProps(rawProps);
12
+ const { style, children, onLayout, ...accessibilityRest } = props;
13
+ dlog('SafeAreaView -> SafeAreaView');
14
+ const nodeProps = { ...accessibilityRest, style };
15
+ if (onLayout !== undefined)
16
+ nodeProps.onLayout = onLayout;
17
+ return createElement('symbiote-safe-area-view', nodeProps, children);
18
+ };
@@ -0,0 +1,3 @@
1
+ import { type IScrollViewHandle, type IScrollViewProps } from './shared';
2
+ export type { IScrollViewProps, IScrollViewHandle } from './shared';
3
+ export declare const ScrollView: import("react").ForwardRefExoticComponent<IScrollViewProps & import("react").RefAttributes<IScrollViewHandle>>;
@@ -0,0 +1,53 @@
1
+ // ScrollView on Android: an Android ScrollView accepts only ONE child, so a RefreshControl
2
+ // can't be a sibling of the content the way iOS allows ("addViewAt: failed to insert view
3
+ // ... at index 1"). Instead the RefreshControl (AndroidSwipeRefreshLayout) WRAPS the
4
+ // scroll view, with the scroll view nested inside and nestedScrollEnabled so the inner
5
+ // scroll handles the gesture before the refresh parent, which mirrors RN's ScrollView.js
6
+ // android branch (cloneElement(refreshControl, {style}, <ScrollView nestedScrollEnabled
7
+ // style={flex:1}>{content}</ScrollView>)). Metro picks this on an Android host; no
8
+ // Platform.OS read. See ADR 0020.
9
+ // device-verify-pending: the wrap shape mirrors RN, proven on a real host by the absence
10
+ // of the "addViewAt: failed to insert" crash.
11
+ import { cloneElement, createElement, forwardRef, useImperativeHandle, useRef } from 'react';
12
+ import { dlog } from '@symbiote-native/engine';
13
+ import { buildScrollViewHandle, splitLayoutProps } from '@symbiote-native/components';
14
+ import { usePreparedScrollView, useNativeStickyScrollAttach, } from './shared';
15
+ export const ScrollView = forwardRef((props, forwardedRef) => {
16
+ const { scrollViewIntrinsic, scrollViewBaseStyle, outerProps, style, layoutSplitStyle, content, refreshControl, scrollAnimatedValue, nativeStickyAvailable, } = usePreparedScrollView(props);
17
+ // The node ref backs the imperative handle; it attaches to the inner scroll-view element
18
+ // (the wrap shape leaves the scroll view as the command target, not the RefreshControl).
19
+ const ref = useRef(null);
20
+ // Lazy getter, not the ref itself: the node is null until the element commits, so the handle
21
+ // must read ref.current on each command (ADR 0024 §3), an eager capture would freeze null.
22
+ useImperativeHandle(forwardedRef, () => buildScrollViewHandle(() => ref.current), []);
23
+ // Drive the sticky scroll value on the native UI thread (RN attachNativeEvent). No-op on a
24
+ // host without the native animated module. The JS sticky path stays in effect.
25
+ useNativeStickyScrollAttach(ref, scrollAnimatedValue, nativeStickyAvailable);
26
+ dlog('ScrollView.ANDROID refreshControl=' + (refreshControl === undefined ? 'NONE(1child)' : 'WRAP'));
27
+ if (refreshControl === undefined) {
28
+ // Base style (flexDirection: row for horizontal) under user style; undefined base
29
+ // (vertical) passes the user style through unchanged.
30
+ const scrollStyle = scrollViewBaseStyle ? [scrollViewBaseStyle, style] : style;
31
+ return createElement(scrollViewIntrinsic, { ...outerProps, style: scrollStyle, ref }, content);
32
+ }
33
+ // RN splits the flattened style across the two boxes (ScrollView.js android branch):
34
+ // LAYOUT props (margin/flex/size/position/transform/gap/…) drive the outer
35
+ // AndroidSwipeRefreshLayout frame; VISUAL props (background/padding/border/opacity/…) paint
36
+ // the inner scroll view. So the wrapper carries `outer`, and the inner scroll view its base
37
+ // (flexDirection/overflow) plus the visual `inner` composed over it, NOT a hardcoded flex:1
38
+ // that would override an explicit user height/width.
39
+ //
40
+ // layoutSplitStyle (not style): a class-only layout prop (flex/height/gap/…) is invisible to
41
+ // `style` (it never carries the resolved `className` value), so splitting on `style` alone
42
+ // starves the wrapper of its layout style and it collapses to nothing. layoutSplitStyle is
43
+ // `style` merged with the resolved className style (see usePreparedScrollView).
44
+ const { outer: outerStyle, inner: innerStyle } = splitLayoutProps(layoutSplitStyle);
45
+ const scrollStyle = scrollViewBaseStyle ? [scrollViewBaseStyle, innerStyle] : innerStyle;
46
+ // className is stripped from the spread here: layoutSplitStyle already folded its resolved
47
+ // value into `outer`/`inner` above, so forwarding the raw prop too would re-apply its LAYOUT
48
+ // half onto the inner scroll view a second time (on top of the outer wrapper).
49
+ const { className: _classNameAppliedViaSplit, ...innerOuterProps } = outerProps;
50
+ const scrollView = createElement(scrollViewIntrinsic, { ...innerOuterProps, style: scrollStyle, nestedScrollEnabled: true, ref }, content);
51
+ return cloneElement(refreshControl, { style: outerStyle }, scrollView);
52
+ });
53
+ ScrollView.displayName = 'ScrollView';
@@ -0,0 +1 @@
1
+ export * from './index.ios';
@@ -0,0 +1,3 @@
1
+ import { type IScrollViewHandle, type IScrollViewProps } from './shared';
2
+ export type { IScrollViewProps, IScrollViewHandle } from './shared';
3
+ export declare const ScrollView: import("react").ForwardRefExoticComponent<IScrollViewProps & import("react").RefAttributes<IScrollViewHandle>>;
@@ -0,0 +1,27 @@
1
+ // ScrollView on iOS: the RefreshControl is a CHILD of the scroll view, rendered as a
2
+ // sibling BEFORE the content container (RN ScrollView.js: {refreshControl}{contentContainer}).
3
+ // Also the base (scroll-view.ts re-exports it) for headless / web. See ADR 0020.
4
+ import { createElement, forwardRef, useImperativeHandle, useRef } from 'react';
5
+ import { buildScrollViewHandle } from '@symbiote-native/components';
6
+ import { usePreparedScrollView, useNativeStickyScrollAttach, } from './shared';
7
+ export const ScrollView = forwardRef((props, forwardedRef) => {
8
+ const { scrollViewIntrinsic, scrollViewBaseStyle, outerProps, style, content, refreshControl, scrollAnimatedValue, nativeStickyAvailable, } = usePreparedScrollView(props);
9
+ // The node ref backs the imperative handle; it attaches to the scroll-view element below
10
+ // (passing `ref` through createElement props binds it to the SymbioteNode, as TextInput does).
11
+ const ref = useRef(null);
12
+ // Lazy getter, not the ref itself: the node is null until the element commits, so the handle
13
+ // must read ref.current on each command (ADR 0024 §3), an eager capture would freeze null.
14
+ useImperativeHandle(forwardedRef, () => buildScrollViewHandle(() => ref.current), []);
15
+ // Drive the sticky scroll value on the native UI thread (RN attachNativeEvent). No-op on a
16
+ // host without the native animated module. The JS sticky path stays in effect.
17
+ useNativeStickyScrollAttach(ref, scrollAnimatedValue, nativeStickyAvailable);
18
+ // Base style under user style so an explicit user value wins; undefined base (vertical)
19
+ // passes the user style through unchanged.
20
+ const scrollStyle = scrollViewBaseStyle ? [scrollViewBaseStyle, style] : style;
21
+ const scrollProps = { ...outerProps, style: scrollStyle, ref };
22
+ if (refreshControl === undefined) {
23
+ return createElement(scrollViewIntrinsic, scrollProps, content);
24
+ }
25
+ return createElement(scrollViewIntrinsic, scrollProps, refreshControl, content);
26
+ });
27
+ ScrollView.displayName = 'ScrollView';
@@ -0,0 +1,4 @@
1
+ // Base / default ScrollView: re-exports the iOS build. Metro overrides this with
2
+ // scroll-view.ios.ts / .android.ts on a real host; under tsx / tsc / web the host config
3
+ // resolves here. Filename is the selector, no Platform.OS read. See ADR 0020.
4
+ export * from './index.ios';