@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.
- package/LICENSE +21 -0
- package/README.md +149 -0
- package/build/components/activity-indicator/index.android.d.ts +2 -0
- package/build/components/activity-indicator/index.android.js +13 -0
- package/build/components/activity-indicator/index.d.ts +1 -0
- package/build/components/activity-indicator/index.ios.d.ts +2 -0
- package/build/components/activity-indicator/index.ios.js +10 -0
- package/build/components/activity-indicator/index.js +4 -0
- package/build/components/activity-indicator/shared.d.ts +7 -0
- package/build/components/activity-indicator/shared.js +20 -0
- package/build/components/button.d.ts +6 -0
- package/build/components/button.js +42 -0
- package/build/components/drawer-layout-android/index.android.d.ts +3 -0
- package/build/components/drawer-layout-android/index.android.js +93 -0
- package/build/components/drawer-layout-android/index.d.ts +3 -0
- package/build/components/drawer-layout-android/index.js +19 -0
- package/build/components/drawer-layout-android/shared.d.ts +19 -0
- package/build/components/drawer-layout-android/shared.js +7 -0
- package/build/components/flat-list/index.d.ts +73 -0
- package/build/components/flat-list/index.js +86 -0
- package/build/components/image/index.d.ts +9 -0
- package/build/components/image/index.js +28 -0
- package/build/components/image-background/index.d.ts +10 -0
- package/build/components/image-background/index.js +46 -0
- package/build/components/input-accessory-view/index.d.ts +11 -0
- package/build/components/input-accessory-view/index.js +14 -0
- package/build/components/keyboard-avoiding-view/index.d.ts +17 -0
- package/build/components/keyboard-avoiding-view/index.js +85 -0
- package/build/components/modal/index.d.ts +24 -0
- package/build/components/modal/index.js +56 -0
- package/build/components/pressable/index.d.ts +30 -0
- package/build/components/pressable/index.js +100 -0
- package/build/components/refresh-control/index.d.ts +17 -0
- package/build/components/refresh-control/index.js +21 -0
- package/build/components/safe-area-view/index.d.ts +11 -0
- package/build/components/safe-area-view/index.js +18 -0
- package/build/components/scroll-view/index.android.d.ts +3 -0
- package/build/components/scroll-view/index.android.js +53 -0
- package/build/components/scroll-view/index.d.ts +1 -0
- package/build/components/scroll-view/index.ios.d.ts +3 -0
- package/build/components/scroll-view/index.ios.js +27 -0
- package/build/components/scroll-view/index.js +4 -0
- package/build/components/scroll-view/shared.d.ts +98 -0
- package/build/components/scroll-view/shared.js +195 -0
- package/build/components/scroll-view/sticky-header.d.ts +9 -0
- package/build/components/scroll-view/sticky-header.js +161 -0
- package/build/components/section-list/index.d.ts +8 -0
- package/build/components/section-list/index.js +12 -0
- package/build/components/switch/index.android.d.ts +2 -0
- package/build/components/switch/index.android.js +12 -0
- package/build/components/switch/index.d.ts +1 -0
- package/build/components/switch/index.ios.d.ts +2 -0
- package/build/components/switch/index.ios.js +10 -0
- package/build/components/switch/index.js +6 -0
- package/build/components/switch/shared.d.ts +10 -0
- package/build/components/switch/shared.js +59 -0
- package/build/components/text-input/index.d.ts +50 -0
- package/build/components/text-input/index.js +167 -0
- package/build/components/touchable/index.d.ts +20 -0
- package/build/components/touchable/index.js +95 -0
- package/build/components/touchable-native-feedback/index.d.ts +18 -0
- package/build/components/touchable-native-feedback/index.js +32 -0
- package/build/components/virtualized-list/index.d.ts +73 -0
- package/build/components/virtualized-list/index.js +552 -0
- package/build/components/virtualized-section-list/index.d.ts +66 -0
- package/build/components/virtualized-section-list/index.js +99 -0
- package/build/components.d.ts +54 -0
- package/build/components.js +17 -0
- package/build/create-portal.d.ts +4 -0
- package/build/create-portal.js +23 -0
- package/build/create-tunnel.d.ts +12 -0
- package/build/create-tunnel.js +60 -0
- package/build/descriptor-to-react/index.d.ts +3 -0
- package/build/descriptor-to-react/index.js +15 -0
- package/build/hooks/use-color-scheme.d.ts +2 -0
- package/build/hooks/use-color-scheme.js +13 -0
- package/build/hooks/use-window-dimensions.d.ts +2 -0
- package/build/hooks/use-window-dimensions.js +30 -0
- package/build/host-config.d.ts +7 -0
- package/build/host-config.js +169 -0
- package/build/host-instance.d.ts +3 -0
- package/build/host-instance.js +20 -0
- package/build/index.d.ts +98 -0
- package/build/index.js +85 -0
- package/build/modules/accessibility-info/index.android.d.ts +2 -0
- package/build/modules/accessibility-info/index.android.js +4 -0
- package/build/modules/accessibility-info/index.d.ts +1 -0
- package/build/modules/accessibility-info/index.ios.d.ts +2 -0
- package/build/modules/accessibility-info/index.ios.js +3 -0
- package/build/modules/accessibility-info/index.js +5 -0
- package/build/modules/accessibility-info/shared.d.ts +1 -0
- package/build/modules/accessibility-info/shared.js +1 -0
- package/build/modules/action-sheet-ios.d.ts +2 -0
- package/build/modules/action-sheet-ios.js +4 -0
- package/build/modules/alert.d.ts +2 -0
- package/build/modules/alert.js +5 -0
- package/build/modules/animated/create-animated-component.d.ts +12 -0
- package/build/modules/animated/create-animated-component.js +125 -0
- package/build/modules/animated/index.d.ts +52 -0
- package/build/modules/animated/index.js +84 -0
- package/build/modules/app-registry/index.d.ts +7 -0
- package/build/modules/app-registry/index.js +26 -0
- package/build/modules/app-state.d.ts +2 -0
- package/build/modules/app-state.js +3 -0
- package/build/modules/appearance.d.ts +2 -0
- package/build/modules/appearance.js +3 -0
- package/build/modules/back-handler.d.ts +2 -0
- package/build/modules/back-handler.js +3 -0
- package/build/modules/dimensions.d.ts +2 -0
- package/build/modules/dimensions.js +3 -0
- package/build/modules/i18n-manager.d.ts +2 -0
- package/build/modules/i18n-manager.js +4 -0
- package/build/modules/keyboard.d.ts +2 -0
- package/build/modules/keyboard.js +3 -0
- package/build/modules/layout-animation.d.ts +2 -0
- package/build/modules/layout-animation.js +3 -0
- package/build/modules/linking.d.ts +2 -0
- package/build/modules/linking.js +4 -0
- package/build/modules/permissions-android.d.ts +2 -0
- package/build/modules/permissions-android.js +3 -0
- package/build/modules/settings.d.ts +1 -0
- package/build/modules/settings.js +4 -0
- package/build/modules/share.d.ts +2 -0
- package/build/modules/share.js +4 -0
- package/build/modules/status-bar/index.android.d.ts +3 -0
- package/build/modules/status-bar/index.android.js +89 -0
- package/build/modules/status-bar/index.d.ts +1 -0
- package/build/modules/status-bar/index.ios.d.ts +3 -0
- package/build/modules/status-bar/index.ios.js +64 -0
- package/build/modules/status-bar/index.js +4 -0
- package/build/modules/status-bar/shared.d.ts +6 -0
- package/build/modules/status-bar/shared.js +6 -0
- package/build/modules/text-input-state.d.ts +1 -0
- package/build/modules/text-input-state.js +3 -0
- package/build/modules/toast-android.d.ts +1 -0
- package/build/modules/toast-android.js +4 -0
- package/build/modules/vibration.d.ts +1 -0
- package/build/modules/vibration.js +4 -0
- package/build/reconciler-constants.d.ts +7 -0
- package/build/reconciler-constants.js +12 -0
- package/build/render.d.ts +7 -0
- package/build/render.js +60 -0
- package/build/utils/responder-props.d.ts +1 -0
- package/build/utils/responder-props.js +1 -0
- package/build/utils/styles.d.ts +1 -0
- package/build/utils/styles.js +1 -0
- package/metro-css-parser.cjs +11 -0
- package/package.json +53 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// VirtualizedSectionList: sections flattened into one virtualized stream. Each section
|
|
2
|
+
// contributes a header row, its item rows, then a footer row; the flattened sequence is fed to
|
|
3
|
+
// VirtualizedList as a single tagged list, so headers, items, and footers are all windowed by the
|
|
4
|
+
// same machinery. The flattening, entry keying, separator-item unwrap, and scrollToLocation
|
|
5
|
+
// mapping are shared from @symbiote-native/components; this file wires React's lifecycle (refs +
|
|
6
|
+
// useImperativeHandle + the per-entry render dispatch). Lower layer in RN's
|
|
7
|
+
// SectionList -> VirtualizedSectionList -> VirtualizedList stack.
|
|
8
|
+
import { createElement, useImperativeHandle, useRef, } from 'react';
|
|
9
|
+
import { dlog, Platform } from '@symbiote-native/engine';
|
|
10
|
+
import { flattenSections, scrollLocationToFlatIndex, sectionEntryKey, unwrapEntryItem, } from '@symbiote-native/components';
|
|
11
|
+
import { VirtualizedList, } from '../virtualized-list';
|
|
12
|
+
function resolveSeparator(component) {
|
|
13
|
+
if (component === undefined)
|
|
14
|
+
return undefined;
|
|
15
|
+
if (typeof component === 'function')
|
|
16
|
+
return createElement(component, {});
|
|
17
|
+
return component;
|
|
18
|
+
}
|
|
19
|
+
export function VirtualizedSectionList(props) {
|
|
20
|
+
const { ref, sections, renderItem, renderSectionHeader, renderSectionFooter, SectionSeparatorComponent,
|
|
21
|
+
// Pulled out of `rest`: the user's separator is typed on ItemT, but the inner VirtualizedList
|
|
22
|
+
// streams ISectionEntry<ItemT>, so we wrap it to unwrap each entry back to its ItemT.
|
|
23
|
+
ItemSeparatorComponent, keyExtractor, stickySectionHeadersEnabled, ...rest } = props;
|
|
24
|
+
const { entries, headerIndices } = flattenSections(sections, SectionSeparatorComponent !== undefined);
|
|
25
|
+
// RN sticks section headers by default only on iOS; Android does not unless asked. headerIndices
|
|
26
|
+
// are the flat positions of every section header; VirtualizedList forwards the in-window ones.
|
|
27
|
+
const stickyDefault = Platform.OS === 'ios';
|
|
28
|
+
const stickyEnabled = stickySectionHeadersEnabled ?? stickyDefault;
|
|
29
|
+
const stickyHeaderIndices = stickyEnabled ? headerIndices : undefined;
|
|
30
|
+
// The handle reaches into the inner VirtualizedList to drive scrollToIndex.
|
|
31
|
+
const listRef = useRef(null);
|
|
32
|
+
dlog(`VirtualizedSectionList: ${sections.length} sections flattened to ${entries.length} entries`);
|
|
33
|
+
useImperativeHandle(ref ?? null, () => ({
|
|
34
|
+
scrollToLocation: (params) => {
|
|
35
|
+
const flatIndex = scrollLocationToFlatIndex(headerIndices, params.sectionIndex, params.itemIndex);
|
|
36
|
+
if (flatIndex === undefined) {
|
|
37
|
+
dlog(`VirtualizedSectionList scrollToLocation: section ${params.sectionIndex} out of range`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
dlog(`VirtualizedSectionList scrollToLocation section=${params.sectionIndex} ` +
|
|
41
|
+
`item=${params.itemIndex} -> flat ${flatIndex}`);
|
|
42
|
+
listRef.current?.scrollToIndex({
|
|
43
|
+
index: flatIndex,
|
|
44
|
+
viewOffset: params.viewOffset,
|
|
45
|
+
viewPosition: params.viewPosition,
|
|
46
|
+
animated: params.animated,
|
|
47
|
+
});
|
|
48
|
+
},
|
|
49
|
+
flashScrollIndicators: () => {
|
|
50
|
+
listRef.current?.flashScrollIndicators();
|
|
51
|
+
},
|
|
52
|
+
getNativeScrollRef: () => listRef.current?.getNativeScrollRef() ?? null,
|
|
53
|
+
getScrollableNode: () => listRef.current?.getScrollableNode() ?? null,
|
|
54
|
+
getScrollResponder: () => listRef.current?.getScrollResponder() ?? null,
|
|
55
|
+
getScrollNode: () => listRef.current?.getScrollNode() ?? null,
|
|
56
|
+
recordInteraction: () => {
|
|
57
|
+
listRef.current?.recordInteraction();
|
|
58
|
+
},
|
|
59
|
+
}), [headerIndices]);
|
|
60
|
+
const renderEntry = (info) => {
|
|
61
|
+
const entry = info.item;
|
|
62
|
+
if (entry.kind === 'header') {
|
|
63
|
+
return renderSectionHeader ? renderSectionHeader({ section: entry.section }) : undefined;
|
|
64
|
+
}
|
|
65
|
+
if (entry.kind === 'footer') {
|
|
66
|
+
return renderSectionFooter ? renderSectionFooter({ section: entry.section }) : undefined;
|
|
67
|
+
}
|
|
68
|
+
if (entry.kind === 'section-separator') {
|
|
69
|
+
return resolveSeparator(SectionSeparatorComponent);
|
|
70
|
+
}
|
|
71
|
+
return renderItem({
|
|
72
|
+
item: entry.item,
|
|
73
|
+
index: entry.itemIndex,
|
|
74
|
+
section: entry.section,
|
|
75
|
+
separators: info.separators,
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
// The user's ItemSeparatorComponent is typed on ItemT; the inner stream is the entry wrapper, so
|
|
79
|
+
// unwrap each entry back to its ItemT (shared unwrapEntryItem) before handing it to the user.
|
|
80
|
+
const entrySeparatorComponent = ItemSeparatorComponent === undefined
|
|
81
|
+
? undefined
|
|
82
|
+
: (entryProps) => createElement(ItemSeparatorComponent, {
|
|
83
|
+
...entryProps,
|
|
84
|
+
leadingItem: unwrapEntryItem(entryProps.leadingItem),
|
|
85
|
+
trailingItem: unwrapEntryItem(entryProps.trailingItem),
|
|
86
|
+
});
|
|
87
|
+
const entryKeyExtractor = (entry, index) => sectionEntryKey(entry, index, keyExtractor);
|
|
88
|
+
return createElement((VirtualizedList), {
|
|
89
|
+
ref: listRef,
|
|
90
|
+
data: entries,
|
|
91
|
+
getItem: (_source, index) => entries[index],
|
|
92
|
+
getItemCount: () => entries.length,
|
|
93
|
+
renderItem: renderEntry,
|
|
94
|
+
keyExtractor: entryKeyExtractor,
|
|
95
|
+
stickyHeaderIndices,
|
|
96
|
+
ItemSeparatorComponent: entrySeparatorComponent,
|
|
97
|
+
...rest,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type FC, type Ref, type ReactNode } from 'react';
|
|
2
|
+
import type { ISymbioteEvent } from '@symbiote-native/engine';
|
|
3
|
+
import type { IHostInstance } from './host-instance';
|
|
4
|
+
import { type IAccessibilityProps, type IAriaProps } from '@symbiote-native/components';
|
|
5
|
+
import type { IResponderProps } from './utils/responder-props';
|
|
6
|
+
import type { IStyleProp, ITextStyle, IViewStyle } from './utils/styles';
|
|
7
|
+
export interface IViewProps extends IAccessibilityProps, IAriaProps, IResponderProps {
|
|
8
|
+
style?: IStyleProp<IViewStyle>;
|
|
9
|
+
className?: string;
|
|
10
|
+
onPress?: (event: ISymbioteEvent) => void;
|
|
11
|
+
onPressIn?: (event: ISymbioteEvent) => void;
|
|
12
|
+
onPressOut?: (event: ISymbioteEvent) => void;
|
|
13
|
+
onLayout?: (event: ISymbioteEvent) => void;
|
|
14
|
+
onFocus?: (event: ISymbioteEvent) => void;
|
|
15
|
+
onBlur?: (event: ISymbioteEvent) => void;
|
|
16
|
+
pointerEvents?: 'auto' | 'none' | 'box-none' | 'box-only';
|
|
17
|
+
hitSlop?: number | {
|
|
18
|
+
top?: number;
|
|
19
|
+
left?: number;
|
|
20
|
+
bottom?: number;
|
|
21
|
+
right?: number;
|
|
22
|
+
};
|
|
23
|
+
id?: string;
|
|
24
|
+
focusable?: boolean;
|
|
25
|
+
collapsable?: boolean;
|
|
26
|
+
removeClippedSubviews?: boolean;
|
|
27
|
+
renderToHardwareTextureAndroid?: boolean;
|
|
28
|
+
shouldRasterizeIOS?: boolean;
|
|
29
|
+
needsOffscreenAlphaCompositing?: boolean;
|
|
30
|
+
ref?: Ref<IHostInstance>;
|
|
31
|
+
children?: ReactNode;
|
|
32
|
+
}
|
|
33
|
+
export interface ITextProps extends IAccessibilityProps, IAriaProps {
|
|
34
|
+
style?: IStyleProp<ITextStyle>;
|
|
35
|
+
className?: string;
|
|
36
|
+
onPress?: (event: ISymbioteEvent) => void;
|
|
37
|
+
onLongPress?: (event: ISymbioteEvent) => void;
|
|
38
|
+
onPressIn?: (event: ISymbioteEvent) => void;
|
|
39
|
+
onPressOut?: (event: ISymbioteEvent) => void;
|
|
40
|
+
onLayout?: (event: ISymbioteEvent) => void;
|
|
41
|
+
onTextLayout?: (event: ISymbioteEvent) => void;
|
|
42
|
+
numberOfLines?: number;
|
|
43
|
+
ellipsizeMode?: 'head' | 'middle' | 'tail' | 'clip';
|
|
44
|
+
selectable?: boolean;
|
|
45
|
+
adjustsFontSizeToFit?: boolean;
|
|
46
|
+
minimumFontScale?: number;
|
|
47
|
+
allowFontScaling?: boolean;
|
|
48
|
+
maxFontSizeMultiplier?: number | null;
|
|
49
|
+
selectionColor?: string;
|
|
50
|
+
ref?: Ref<IHostInstance>;
|
|
51
|
+
children?: ReactNode;
|
|
52
|
+
}
|
|
53
|
+
export declare const View: FC<IViewProps>;
|
|
54
|
+
export declare const Text: FC<ITextProps>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Host primitives exposed to user code. They are thin wrappers that produce the
|
|
2
|
+
// lowercase host elements the reconciler understands (`view` / `text`); the
|
|
3
|
+
// reconciler maps those to shared's mutation API, which resolves them to Fabric
|
|
4
|
+
// view names at commit.
|
|
5
|
+
import { createElement } from 'react';
|
|
6
|
+
import { resolveAccessibilityProps, } from '@symbiote-native/components';
|
|
7
|
+
// RN's modern `id` is just a W3C-named alias for `nativeID`: View.js copies it over
|
|
8
|
+
// (`processedProps.nativeID = id`), so `id` wins when both are set. We fold it here and
|
|
9
|
+
// blank the alias so a raw `id` never reaches Fabric (every non-function prop passes
|
|
10
|
+
// through to the slot otherwise).
|
|
11
|
+
function resolveId({ id, ...rest }) {
|
|
12
|
+
if (id === undefined)
|
|
13
|
+
return rest;
|
|
14
|
+
return { ...rest, nativeID: id };
|
|
15
|
+
}
|
|
16
|
+
export const View = props => createElement('symbiote-view', resolveAccessibilityProps(resolveId(props)));
|
|
17
|
+
export const Text = props => createElement('symbiote-text', resolveAccessibilityProps(props));
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ReactNode, ReactPortal } from 'react';
|
|
2
|
+
import { SymbioteSurface, type ISymbioteNode } from '@symbiote-native/engine';
|
|
3
|
+
export type IPortalContainer = ISymbioteNode | SymbioteSurface;
|
|
4
|
+
export declare function createPortal(children: ReactNode, container: IPortalContainer, key?: string | null): ReactPortal;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// createPortal — react-reconciler's Fiber-level portal primitive (HostPortal), not a
|
|
2
|
+
// react-dom re-export. Stock React Native does NOT support this: real RN's Fabric host
|
|
3
|
+
// config runs in PERSISTENT mode and never implements the mutation-mode container ops
|
|
4
|
+
// (appendChildToContainer / insertInContainerBefore / removeChildFromContainer) createPortal
|
|
5
|
+
// needs — see facebook/react-native#36273 ("Portal children gets overriden in Fabric").
|
|
6
|
+
// @symbiote-native/react is deliberately MUTATION mode and already implements all three (host-config.ts),
|
|
7
|
+
// so a portal here structurally works where it never could in real RN.
|
|
8
|
+
//
|
|
9
|
+
// Scope (v1): the target must be an already-mounted node WITHIN THE SAME SURFACE as the portal's
|
|
10
|
+
// call site (e.g. a ref to a persistent "overlay host" View near your app root) — not a second,
|
|
11
|
+
// independently-mounted SymbioteSurface. Cross-surface targets aren't wired: resetAfterCommit only
|
|
12
|
+
// fires for the primary root's own container, so a different surface's tree would never repaint.
|
|
13
|
+
import { isSymbioteNode, SymbioteSurface } from '@symbiote-native/engine';
|
|
14
|
+
import reconciler from './host-config';
|
|
15
|
+
export function createPortal(children, container, key) {
|
|
16
|
+
if (!(container instanceof SymbioteSurface) && !isSymbioteNode(container)) {
|
|
17
|
+
throw new Error('createPortal target must be an already-mounted host node (e.g. a ref to a rendered <View>) — got something else. Did you forget `.current`/`.value`, or pass a CSS-selector-style string?');
|
|
18
|
+
}
|
|
19
|
+
// @ts-expect-error @types/react-reconciler's ReactPortal (containerInfo/implementation) and
|
|
20
|
+
// react's own ReactPortal (type/props, a ReactElement) are different interfaces describing the
|
|
21
|
+
// SAME runtime object; react-reconciler's is what reconciler.createPortal actually returns.
|
|
22
|
+
return reconciler.createPortal(children, container, null, key ?? null);
|
|
23
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
export interface ITunnel {
|
|
3
|
+
/** Renders nothing; registers its children under the tunnel from wherever it's mounted —
|
|
4
|
+
* any surface. */
|
|
5
|
+
In: (props: {
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
}) => null;
|
|
8
|
+
/** Renders everything currently tunneled in, in registration order. Mount this in the
|
|
9
|
+
* component that should actually paint the content. */
|
|
10
|
+
Out: () => ReactNode;
|
|
11
|
+
}
|
|
12
|
+
export declare function createTunnel(): ITunnel;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
// createTunnel — the cross-surface answer to createPortal's same-surface-only v1 scope
|
|
3
|
+
// (react-adapter-portal skill). Prior art: pmndrs/tunnel-rat exists precisely because a real
|
|
4
|
+
// createPortal cannot reach across two genuinely separate reconciler roots — see
|
|
5
|
+
// github.com/facebook/react/issues/17147 ("react-reconciler & portals: missing root
|
|
6
|
+
// instance"). The fix is the one the ecosystem already settled on: don't reach into a
|
|
7
|
+
// foreign surface's commit machinery — let that surface commit ITSELF, normally, by reading
|
|
8
|
+
// from a store it's already subscribed to.
|
|
9
|
+
//
|
|
10
|
+
// Both In and Out are COMPONENTS (the Vue twin's shape, and the same pattern as
|
|
11
|
+
// Context.Provider/Context.Consumer) — NOT hooks. This isn't just nicer call-site ergonomics:
|
|
12
|
+
// it's what makes the cascade in the first place structurally impossible. An earlier
|
|
13
|
+
// hook-based version (`useTunnelIn`/`useTunnelOut`, called directly inside one component)
|
|
14
|
+
// produced a genuine infinite render loop — a silent white screen on device, no thrown error
|
|
15
|
+
// — because notify() forces a re-render of whichever component's useSyncExternalStore call
|
|
16
|
+
// subscribed, and when THAT was the SAME component that also called useTunnelIn, its own
|
|
17
|
+
// effect re-ran and notified again, forever (this custom renderer's synchronous commit loop
|
|
18
|
+
// has no "Maximum update depth exceeded" guard to catch it). As separate components, notify()
|
|
19
|
+
// only forces Out's OWN render scope — never In's, even when both are children of the same
|
|
20
|
+
// parent — so the update has nowhere to bounce back to. `Out` lives inside whichever surface
|
|
21
|
+
// should PAINT the content (e.g. a persistent overlay-host surface); `In` lives ANYWHERE, same
|
|
22
|
+
// surface or a totally different one, and never touches a Fabric node directly, so there is
|
|
23
|
+
// no "target must already be mounted" guard to satisfy at all.
|
|
24
|
+
import { Fragment, useEffect, useId, useSyncExternalStore } from 'react';
|
|
25
|
+
export function createTunnel() {
|
|
26
|
+
const items = new Map();
|
|
27
|
+
const listeners = new Set();
|
|
28
|
+
// useSyncExternalStore bails out via Object.is on the snapshot reference, so it must stay
|
|
29
|
+
// stable between reads — rebuilt only when the Map actually changes, never inline.
|
|
30
|
+
let snapshot = [];
|
|
31
|
+
function notify() {
|
|
32
|
+
snapshot = Array.from(items.values());
|
|
33
|
+
listeners.forEach(listener => listener());
|
|
34
|
+
}
|
|
35
|
+
function In({ children }) {
|
|
36
|
+
const id = useId();
|
|
37
|
+
// Every render of In: keep this id's content in sync. Safe to run unconditionally (no
|
|
38
|
+
// dependency array) — In and Out are separate components, so notify()'s forced re-render
|
|
39
|
+
// of Out never bounces back into In (see the file header for why that matters).
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
// Wrapped in a keyed Fragment: Out renders `snapshot` as a list, and a bare ReactNode
|
|
42
|
+
// (possibly a fragment-less element or a string) has no key of its own.
|
|
43
|
+
items.set(id, _jsx(Fragment, { children: children }, id));
|
|
44
|
+
notify();
|
|
45
|
+
});
|
|
46
|
+
// Once, on real unmount: drop this id for good.
|
|
47
|
+
useEffect(() => () => {
|
|
48
|
+
items.delete(id);
|
|
49
|
+
notify();
|
|
50
|
+
}, [id]);
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
function Out() {
|
|
54
|
+
return useSyncExternalStore(listener => {
|
|
55
|
+
listeners.add(listener);
|
|
56
|
+
return () => listeners.delete(listener);
|
|
57
|
+
}, () => snapshot);
|
|
58
|
+
}
|
|
59
|
+
return { In, Out };
|
|
60
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// The descriptor→element bridge. A render function in @symbiote-native/components returns a
|
|
2
|
+
// framework-agnostic `Descriptor` tree; this maps it onto React elements. The resulting
|
|
3
|
+
// host element (`symbiote-view`, `symbiote-activity-indicator`, …) flows on through the
|
|
4
|
+
// react-reconciler host config → engine → Fabric, exactly like a hand-written JSX host
|
|
5
|
+
// element. The wolf-tui twin is `packages/react/src/wnode/wnode-to-react.tsx`.
|
|
6
|
+
import { createElement } from 'react';
|
|
7
|
+
export function descriptorToReact(node, index) {
|
|
8
|
+
const children = node.children.map(toChild);
|
|
9
|
+
// `key` first so a Descriptor that carries its own `key` in props still wins; list
|
|
10
|
+
// items (once components emit keyed children) diff correctly.
|
|
11
|
+
return createElement(node.type, { key: node.key ?? index, ...node.props }, ...children);
|
|
12
|
+
}
|
|
13
|
+
function toChild(child, index) {
|
|
14
|
+
return typeof child === 'string' ? child : descriptorToReact(child, index);
|
|
15
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// useColorScheme subscribes a component to the device color scheme and re-renders
|
|
2
|
+
// on change. Mirrors RN's Libraries/Utilities/useColorScheme.js: a thin
|
|
3
|
+
// useSyncExternalStore over Appearance's change event + current scheme read.
|
|
4
|
+
import { useSyncExternalStore } from 'react';
|
|
5
|
+
import { Appearance } from '../modules/appearance';
|
|
6
|
+
const subscribe = (onStoreChange) => {
|
|
7
|
+
const subscription = Appearance.addChangeListener(onStoreChange);
|
|
8
|
+
return () => subscription.remove();
|
|
9
|
+
};
|
|
10
|
+
const getSnapshot = () => Appearance.getColorScheme();
|
|
11
|
+
export function useColorScheme() {
|
|
12
|
+
return useSyncExternalStore(subscribe, getSnapshot);
|
|
13
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// useWindowDimensions is the preferred API for components that need window metrics,
|
|
2
|
+
// a port of RN's Libraries/Utilities/useWindowDimensions.js. It seeds from
|
|
3
|
+
// Dimensions.get('window'), subscribes to 'change', and re-checks once after
|
|
4
|
+
// subscribing to close the gap between the render-time get and the effect-time
|
|
5
|
+
// listener. Only the window metrics changing triggers a re-render.
|
|
6
|
+
import { useEffect, useState } from 'react';
|
|
7
|
+
import { Dimensions } from '../modules/dimensions';
|
|
8
|
+
export function useWindowDimensions() {
|
|
9
|
+
const [dimensions, setDimensions] = useState(() => Dimensions.get('window'));
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
function handleChange(window) {
|
|
12
|
+
if (dimensions.width !== window.width ||
|
|
13
|
+
dimensions.height !== window.height ||
|
|
14
|
+
dimensions.scale !== window.scale ||
|
|
15
|
+
dimensions.fontScale !== window.fontScale) {
|
|
16
|
+
setDimensions(window);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const subscription = Dimensions.addEventListener('change', (set) => {
|
|
20
|
+
handleChange(set.window);
|
|
21
|
+
});
|
|
22
|
+
// We may have missed an update between calling `get` in render and subscribing
|
|
23
|
+
// here; re-check now. If nothing changed, React filters the no-op set.
|
|
24
|
+
handleChange(Dimensions.get('window'));
|
|
25
|
+
return () => {
|
|
26
|
+
subscription.remove();
|
|
27
|
+
};
|
|
28
|
+
}, [dimensions]);
|
|
29
|
+
return dimensions;
|
|
30
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import createReconciler from 'react-reconciler';
|
|
2
|
+
import { SymbioteSurface, type ISymbioteNode } from '@symbiote-native/engine';
|
|
3
|
+
import { type IHostInstance } from './host-instance';
|
|
4
|
+
type IContainer = SymbioteSurface | ISymbioteNode;
|
|
5
|
+
export declare function withDiscretePriority(run: () => void): void;
|
|
6
|
+
declare const reconciler: createReconciler.Reconciler<IContainer, ISymbioteNode, ISymbioteNode, never, unknown, IHostInstance>;
|
|
7
|
+
export default reconciler;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// react-reconciler host config in MUTATION mode. Every host operation maps onto
|
|
2
|
+
// shared's tiny mutation API; shared owns all Fabric-specific work (tag
|
|
3
|
+
// allocation, view-name resolution, clone-on-write, events). This is the
|
|
4
|
+
// canary: a known-good driver exercising shared end to end.
|
|
5
|
+
import createReconciler from 'react-reconciler';
|
|
6
|
+
import { createContext } from 'react';
|
|
7
|
+
import { appendChild, createElement, createRawText, insertBefore, removeChild, routeProp, setText, SymbioteSurface, } from '@symbiote-native/engine';
|
|
8
|
+
import { DefaultEventPriority, DiscreteEventPriority, NoEventPriority, } from './reconciler-constants';
|
|
9
|
+
import { toPublicInstance } from './host-instance';
|
|
10
|
+
// Intrinsic JSX type -> Fabric component name. The name table + resolver live once in
|
|
11
|
+
// @symbiote-native/components, shared by every adapter so the names can't drift (one engine, one
|
|
12
|
+
// Fabric). The table is Metro-split (.ios/.android, filename selects, no Platform.OS read,
|
|
13
|
+
// per ADR 0020). Adding a primitive is one entry in each name table there, plus its thin
|
|
14
|
+
// component in components.ts: no host-config logic per primitive.
|
|
15
|
+
import { descriptorFor } from '@symbiote-native/components';
|
|
16
|
+
// React-reserved prop keys that must never reach Fabric. `children` is the element
|
|
17
|
+
// tree (handled via appendChild). `ref`/`key` are React 19 plain props: the reconciler
|
|
18
|
+
// reads `props.ref` for commitAttachRef but does NOT strip it from the props it hands a
|
|
19
|
+
// host config, so `ref.current` (the public instance, a bag of methods like measure /
|
|
20
|
+
// focus) would serialize into folly::dynamic and throw "not convertible to dynamic" on
|
|
21
|
+
// Android. Stripping here is the host config's job, exactly as RN's own renderer does.
|
|
22
|
+
function isReservedProp(key) {
|
|
23
|
+
return key === 'children' || key === 'ref' || key === 'key';
|
|
24
|
+
}
|
|
25
|
+
function applyProps(node, props) {
|
|
26
|
+
for (const [key, value] of Object.entries(props)) {
|
|
27
|
+
if (isReservedProp(key))
|
|
28
|
+
continue;
|
|
29
|
+
routeProp(node, key, value);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function applyUpdate(node, oldProps, newProps) {
|
|
33
|
+
for (const key of Object.keys(oldProps)) {
|
|
34
|
+
if (isReservedProp(key))
|
|
35
|
+
continue;
|
|
36
|
+
if (!Object.hasOwn(newProps, key))
|
|
37
|
+
routeProp(node, key, undefined);
|
|
38
|
+
}
|
|
39
|
+
for (const [key, value] of Object.entries(newProps)) {
|
|
40
|
+
if (isReservedProp(key))
|
|
41
|
+
continue;
|
|
42
|
+
if (value !== oldProps[key])
|
|
43
|
+
routeProp(node, key, value);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function isSurfaceContainer(container) {
|
|
47
|
+
return container instanceof SymbioteSurface;
|
|
48
|
+
}
|
|
49
|
+
let currentUpdatePriority = NoEventPriority;
|
|
50
|
+
// Run an externally-triggered update (a native event) at discrete priority so it
|
|
51
|
+
// lands on the sync lane and flushSyncWork paints it immediately.
|
|
52
|
+
export function withDiscretePriority(run) {
|
|
53
|
+
const previous = currentUpdatePriority;
|
|
54
|
+
currentUpdatePriority = DiscreteEventPriority;
|
|
55
|
+
try {
|
|
56
|
+
run();
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
currentUpdatePriority = previous;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const reconciler = createReconciler({
|
|
63
|
+
isPrimaryRenderer: true,
|
|
64
|
+
supportsMutation: true,
|
|
65
|
+
supportsPersistence: false,
|
|
66
|
+
supportsHydration: false,
|
|
67
|
+
noTimeout: -1,
|
|
68
|
+
scheduleTimeout: setTimeout,
|
|
69
|
+
cancelTimeout: clearTimeout,
|
|
70
|
+
getRootHostContext: () => ({ isInsideText: false }),
|
|
71
|
+
getChildHostContext(parentHostContext, type) {
|
|
72
|
+
const isInsideText = descriptorFor(type).isText;
|
|
73
|
+
return parentHostContext.isInsideText === isInsideText ? parentHostContext : { isInsideText };
|
|
74
|
+
},
|
|
75
|
+
getPublicInstance: instance => toPublicInstance(instance),
|
|
76
|
+
prepareForCommit: () => null,
|
|
77
|
+
// Called once per commit with the PRIMARY root's own container only — never with a portal's
|
|
78
|
+
// target (portal mutations recommit as part of the same surface's tree walk, since the target
|
|
79
|
+
// must be a node already inside that surface; see create-portal.ts).
|
|
80
|
+
resetAfterCommit: container => {
|
|
81
|
+
if (isSurfaceContainer(container))
|
|
82
|
+
container.commit();
|
|
83
|
+
},
|
|
84
|
+
preparePortalMount: () => { },
|
|
85
|
+
clearContainer: container => {
|
|
86
|
+
if (isSurfaceContainer(container))
|
|
87
|
+
container.clear();
|
|
88
|
+
},
|
|
89
|
+
shouldSetTextContent: () => false,
|
|
90
|
+
createInstance(type, props, _container, hostContext) {
|
|
91
|
+
const descriptor = descriptorFor(type);
|
|
92
|
+
if (hostContext.isInsideText && !descriptor.isText) {
|
|
93
|
+
throw new Error(`<${type}> can't be nested inside <Text>`);
|
|
94
|
+
}
|
|
95
|
+
const node = createElement(descriptor.component, descriptor.isText);
|
|
96
|
+
applyProps(node, props);
|
|
97
|
+
return node;
|
|
98
|
+
},
|
|
99
|
+
createTextInstance(text, _container, hostContext) {
|
|
100
|
+
if (!hostContext.isInsideText) {
|
|
101
|
+
throw new Error(`Text string "${text}" must be rendered inside a <Text>`);
|
|
102
|
+
}
|
|
103
|
+
return createRawText(text);
|
|
104
|
+
},
|
|
105
|
+
appendInitialChild: (parent, child) => appendChild(parent, child),
|
|
106
|
+
appendChild: (parent, child) => appendChild(parent, child),
|
|
107
|
+
appendChildToContainer: (container, child) => {
|
|
108
|
+
if (isSurfaceContainer(container))
|
|
109
|
+
container.appendChild(child);
|
|
110
|
+
else
|
|
111
|
+
appendChild(container, child);
|
|
112
|
+
},
|
|
113
|
+
insertBefore: (parent, child, before) => insertBefore(parent, child, before),
|
|
114
|
+
insertInContainerBefore: (container, child, before) => {
|
|
115
|
+
if (isSurfaceContainer(container))
|
|
116
|
+
container.insertBefore(child, before);
|
|
117
|
+
else
|
|
118
|
+
insertBefore(container, child, before);
|
|
119
|
+
},
|
|
120
|
+
removeChild: (parent, child) => removeChild(parent, child),
|
|
121
|
+
removeChildFromContainer: (container, child) => {
|
|
122
|
+
if (isSurfaceContainer(container))
|
|
123
|
+
container.removeChild(child);
|
|
124
|
+
else
|
|
125
|
+
removeChild(container, child);
|
|
126
|
+
},
|
|
127
|
+
finalizeInitialChildren: () => false,
|
|
128
|
+
commitUpdate(node, _type, oldProps, newProps) {
|
|
129
|
+
applyUpdate(node, oldProps, newProps);
|
|
130
|
+
},
|
|
131
|
+
commitTextUpdate(node, _oldText, newText) {
|
|
132
|
+
setText(node, newText);
|
|
133
|
+
},
|
|
134
|
+
resetTextContent: () => { },
|
|
135
|
+
hideTextInstance: node => setText(node, ''),
|
|
136
|
+
unhideTextInstance: (node, text) => setText(node, text),
|
|
137
|
+
hideInstance: () => { },
|
|
138
|
+
unhideInstance: () => { },
|
|
139
|
+
beforeActiveInstanceBlur: () => { },
|
|
140
|
+
afterActiveInstanceBlur: () => { },
|
|
141
|
+
detachDeletedInstance: () => { },
|
|
142
|
+
getInstanceFromNode: () => null,
|
|
143
|
+
prepareScopeUpdate: () => { },
|
|
144
|
+
getInstanceFromScope: () => null,
|
|
145
|
+
setCurrentUpdatePriority: priority => {
|
|
146
|
+
currentUpdatePriority = priority;
|
|
147
|
+
},
|
|
148
|
+
getCurrentUpdatePriority: () => currentUpdatePriority,
|
|
149
|
+
resolveUpdatePriority: () => currentUpdatePriority !== NoEventPriority ? currentUpdatePriority : DefaultEventPriority,
|
|
150
|
+
maySuspendCommit: () => false,
|
|
151
|
+
NotPendingTransition: null,
|
|
152
|
+
// react's Context IS the runtime value react-reconciler's ReactContext wants
|
|
153
|
+
// (it reads _currentValue off it); the two libraries' type defs model
|
|
154
|
+
// Consumer/Provider differently and cannot be reconciled structurally. This is
|
|
155
|
+
// a type-def mismatch only, flagged here so a future @types fix forces cleanup.
|
|
156
|
+
// @ts-expect-error cross-library Context type-def mismatch (runtime-correct)
|
|
157
|
+
HostTransitionContext: createContext(null),
|
|
158
|
+
resetFormInstance: () => { },
|
|
159
|
+
requestPostPaintCallback: () => { },
|
|
160
|
+
shouldAttemptEagerTransition: () => false,
|
|
161
|
+
trackSchedulerEvent: () => { },
|
|
162
|
+
resolveEventType: () => null,
|
|
163
|
+
resolveEventTimeStamp: () => -1.1,
|
|
164
|
+
preloadInstance: () => true,
|
|
165
|
+
startSuspendingCommit: () => { },
|
|
166
|
+
suspendInstance: () => { },
|
|
167
|
+
waitForCommitToBeReady: () => null,
|
|
168
|
+
});
|
|
169
|
+
export default reconciler;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { toPublicInstance, type ISymbioteNode, type IHostInstance } from '@symbiote-native/engine';
|
|
2
|
+
export { toPublicInstance, type IHostInstance };
|
|
3
|
+
export declare function findNodeHandle(componentOrHandle: IHostInstance | ISymbioteNode | number | null | undefined): number | null;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// The public instance a host ref hands back and the ref -> tag lookup. toPublicInstance and
|
|
2
|
+
// IHostInstance now live in @symbiote-native/engine (they depend only on engine internals, so every
|
|
3
|
+
// adapter inherits the SAME public instance); this module re-exports them so the React surface
|
|
4
|
+
// (`@symbiote-native/react` exports findNodeHandle + IHostInstance, host-config grafts via
|
|
5
|
+
// toPublicInstance) is unchanged. findNodeHandle stays here: it is the React-shaped "ref ->
|
|
6
|
+
// native tag" lookup over the engine's getNativeTag.
|
|
7
|
+
import { getNativeTag, isSymbioteNode, toPublicInstance, } from '@symbiote-native/engine';
|
|
8
|
+
export { toPublicInstance };
|
|
9
|
+
// The committed native tag of a host ref (or a bare tag, idempotent). RN's
|
|
10
|
+
// findNodeHandle: a ref/instance resolves to its reactTag, a number passes through,
|
|
11
|
+
// null/undefined yields null. Undefined-until-committed surfaces as null.
|
|
12
|
+
export function findNodeHandle(componentOrHandle) {
|
|
13
|
+
if (componentOrHandle === null || componentOrHandle === undefined)
|
|
14
|
+
return null;
|
|
15
|
+
if (typeof componentOrHandle === 'number')
|
|
16
|
+
return componentOrHandle;
|
|
17
|
+
if (isSymbioteNode(componentOrHandle))
|
|
18
|
+
return getNativeTag(componentOrHandle) ?? null;
|
|
19
|
+
return null;
|
|
20
|
+
}
|