@symbiote-native/solid 0.0.1 → 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/build/bootstrap.d.ts +7 -0
- package/build/bootstrap.js +18 -0
- package/build/components/activity-indicator/index.android.d.ts +2 -0
- package/build/components/activity-indicator/index.android.js +11 -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 +8 -0
- package/build/components/activity-indicator/shared.js +51 -0
- package/build/components/button.d.ts +7 -0
- package/build/components/button.jsx +45 -0
- package/build/components/flat-list/index.d.ts +10 -0
- package/build/components/flat-list/index.jsx +118 -0
- package/build/components/image-background.d.ts +10 -0
- package/build/components/image-background.jsx +74 -0
- package/build/components/image.d.ts +9 -0
- package/build/components/image.js +75 -0
- package/build/components/input-accessory-view.d.ts +11 -0
- package/build/components/input-accessory-view.jsx +39 -0
- package/build/components/keyboard-avoiding-view.d.ts +15 -0
- package/build/components/keyboard-avoiding-view.jsx +152 -0
- package/build/components/modal.d.ts +24 -0
- package/build/components/modal.jsx +125 -0
- package/build/components/pressable.d.ts +30 -0
- package/build/components/pressable.jsx +185 -0
- package/build/components/refresh-control.d.ts +18 -0
- package/build/components/refresh-control.jsx +36 -0
- package/build/components/safe-area-view.d.ts +10 -0
- package/build/components/safe-area-view.jsx +24 -0
- package/build/components/scroll-view/index.android.d.ts +2 -0
- package/build/components/scroll-view/index.android.js +11 -0
- package/build/components/scroll-view/index.d.ts +2 -0
- package/build/components/scroll-view/index.ios.d.ts +2 -0
- package/build/components/scroll-view/index.ios.js +8 -0
- package/build/components/scroll-view/index.js +9 -0
- package/build/components/scroll-view/shared.d.ts +85 -0
- package/build/components/scroll-view/shared.jsx +334 -0
- package/build/components/scroll-view/sticky-header.d.ts +18 -0
- package/build/components/scroll-view/sticky-header.jsx +182 -0
- package/build/components/section-list/index.d.ts +6 -0
- package/build/components/section-list/index.jsx +8 -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 +11 -0
- package/build/components/switch/index.js +4 -0
- package/build/components/switch/shared.d.ts +11 -0
- package/build/components/switch/shared.js +87 -0
- package/build/components/text-input.d.ts +10 -0
- package/build/components/text-input.js +268 -0
- package/build/components/text.d.ts +26 -0
- package/build/components/text.jsx +27 -0
- package/build/components/touchable/index.d.ts +22 -0
- package/build/components/touchable/index.jsx +247 -0
- package/build/components/touchable-native-feedback/index.d.ts +16 -0
- package/build/components/touchable-native-feedback/index.jsx +68 -0
- package/build/components/view.d.ts +32 -0
- package/build/components/view.jsx +49 -0
- package/build/components/virtualized-list/index.android.d.ts +2 -0
- package/build/components/virtualized-list/index.android.js +9 -0
- package/build/components/virtualized-list/index.d.ts +1 -0
- package/build/components/virtualized-list/index.ios.d.ts +2 -0
- package/build/components/virtualized-list/index.ios.js +9 -0
- package/build/components/virtualized-list/index.js +4 -0
- package/build/components/virtualized-list/shared.d.ts +75 -0
- package/build/components/virtualized-list/shared.jsx +782 -0
- package/build/components/virtualized-section-list/index.d.ts +62 -0
- package/build/components/virtualized-section-list/index.jsx +145 -0
- package/build/components.d.ts +43 -0
- package/build/components.js +54 -0
- package/build/create-portal/index.d.ts +11 -0
- package/build/create-portal/index.jsx +88 -0
- package/build/create-tunnel/index.d.ts +13 -0
- package/build/create-tunnel/index.jsx +53 -0
- package/build/descriptor-to-solid.d.ts +3 -0
- package/build/descriptor-to-solid.js +91 -0
- package/build/host-instance.d.ts +2 -0
- package/build/host-instance.js +36 -0
- package/build/index.d.ts +30 -0
- package/build/index.js +96 -0
- package/build/jsx-runtime.d.ts +26 -0
- package/build/jsx-runtime.js +46 -0
- package/build/modules/animated/create-animated-component.d.ts +10 -0
- package/build/modules/animated/create-animated-component.jsx +105 -0
- package/build/modules/animated/index.d.ts +54 -0
- package/build/modules/animated/index.js +78 -0
- package/build/modules/app-registry/index.d.ts +7 -0
- package/build/modules/app-registry/index.js +37 -0
- package/build/modules/status-bar.d.ts +7 -0
- package/build/modules/status-bar.js +45 -0
- package/build/primitives/create-color-scheme.d.ts +3 -0
- package/build/primitives/create-color-scheme.js +33 -0
- package/build/primitives/create-window-dimensions.d.ts +3 -0
- package/build/primitives/create-window-dimensions.js +34 -0
- package/build/render.d.ts +7 -0
- package/build/render.js +91 -0
- package/build/renderer.d.ts +7 -0
- package/build/renderer.js +195 -0
- package/build/utils/host-ref.d.ts +3 -0
- package/build/utils/host-ref.js +22 -0
- package/build/utils/stable-keys.d.ts +1 -0
- package/build/utils/stable-keys.js +31 -0
- package/package.json +5 -5
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { type Accessor, type Ref } from 'solid-js';
|
|
2
|
+
import type { JSX } from '../../jsx-runtime';
|
|
3
|
+
import { type ISection, type IAccessibilityProps, type IAriaProps, type ISeparatorProps, type ISeparators, type IVirtualizedSectionListHandle } from '@symbiote-native/components';
|
|
4
|
+
import { type IClassNameValue, type IStyleProp, type ISymbioteEvent, type IViewStyle } from '@symbiote-native/engine';
|
|
5
|
+
export type { ISection, IVirtualizedSectionListHandle };
|
|
6
|
+
export interface ISectionHeaderInfo<ItemT> {
|
|
7
|
+
section: ISection<ItemT>;
|
|
8
|
+
}
|
|
9
|
+
export interface ISectionCellInfo<ItemT> {
|
|
10
|
+
item: ItemT;
|
|
11
|
+
index: number;
|
|
12
|
+
section: ISection<ItemT>;
|
|
13
|
+
separators: ISeparators;
|
|
14
|
+
}
|
|
15
|
+
export interface IVirtualizedSectionListProps<ItemT> extends IAccessibilityProps, IAriaProps {
|
|
16
|
+
sections: ReadonlyArray<ISection<ItemT>>;
|
|
17
|
+
renderItem: (info: Accessor<ISectionCellInfo<ItemT>>) => JSX.Element;
|
|
18
|
+
renderSectionHeader?: (info: Accessor<ISectionHeaderInfo<ItemT>>) => JSX.Element;
|
|
19
|
+
renderSectionFooter?: (info: Accessor<ISectionHeaderInfo<ItemT>>) => JSX.Element;
|
|
20
|
+
stickySectionHeadersEnabled?: boolean;
|
|
21
|
+
SectionSeparatorComponent?: () => JSX.Element;
|
|
22
|
+
keyExtractor?: (item: ItemT, index: number) => string;
|
|
23
|
+
ItemSeparatorComponent?: (props: ISeparatorProps<ItemT>) => JSX.Element;
|
|
24
|
+
ref?: Ref<IVirtualizedSectionListHandle>;
|
|
25
|
+
ListHeaderComponent?: JSX.Element;
|
|
26
|
+
ListFooterComponent?: JSX.Element;
|
|
27
|
+
ListEmptyComponent?: JSX.Element;
|
|
28
|
+
extraData?: unknown;
|
|
29
|
+
onEndReached?: (info: {
|
|
30
|
+
distanceFromEnd: number;
|
|
31
|
+
}) => void;
|
|
32
|
+
onEndReachedThreshold?: number;
|
|
33
|
+
onStartReached?: (info: {
|
|
34
|
+
distanceFromStart: number;
|
|
35
|
+
}) => void;
|
|
36
|
+
onStartReachedThreshold?: number;
|
|
37
|
+
onRefresh?: () => void;
|
|
38
|
+
refreshing?: boolean | null;
|
|
39
|
+
progressViewOffset?: number;
|
|
40
|
+
initialNumToRender?: number;
|
|
41
|
+
initialScrollIndex?: number;
|
|
42
|
+
maxToRenderPerBatch?: number;
|
|
43
|
+
updateCellsBatchingPeriod?: number;
|
|
44
|
+
windowSize?: number;
|
|
45
|
+
inverted?: boolean;
|
|
46
|
+
maintainVisibleContentPosition?: {
|
|
47
|
+
minIndexForVisible: number;
|
|
48
|
+
autoscrollToTopThreshold?: number;
|
|
49
|
+
};
|
|
50
|
+
onScroll?: (event: ISymbioteEvent) => void;
|
|
51
|
+
onScrollBeginDrag?: (event: ISymbioteEvent) => void;
|
|
52
|
+
onScrollEndDrag?: (event: ISymbioteEvent) => void;
|
|
53
|
+
onMomentumScrollBegin?: (event: ISymbioteEvent) => void;
|
|
54
|
+
onMomentumScrollEnd?: (event: ISymbioteEvent) => void;
|
|
55
|
+
scrollEventThrottle?: number;
|
|
56
|
+
keyboardShouldPersistTaps?: boolean | 'always' | 'never' | 'handled';
|
|
57
|
+
keyboardDismissMode?: 'none' | 'on-drag' | 'interactive';
|
|
58
|
+
style?: IStyleProp<IViewStyle>;
|
|
59
|
+
contentContainerStyle?: IStyleProp<IViewStyle> | string;
|
|
60
|
+
class?: IClassNameValue;
|
|
61
|
+
}
|
|
62
|
+
export declare function VirtualizedSectionList<ItemT>(props: IVirtualizedSectionListProps<ItemT>): JSX.Element;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// VirtualizedSectionList: sections flattened into one virtualized stream. Each section contributes
|
|
2
|
+
// a header row, its item rows, then a footer row; the flattened tagged sequence feeds
|
|
3
|
+
// VirtualizedList as a single list, so headers, items and footers are windowed by the same
|
|
4
|
+
// machinery. The flattening, entry keying, separator-item unwrap and scrollToLocation mapping are
|
|
5
|
+
// shared verbatim from @symbiote-native/components; this file wires only Solid's lifecycle.
|
|
6
|
+
import { createMemo, splitProps } from 'solid-js';
|
|
7
|
+
import { flattenSections, resolveStickySectionHeaders, scrollLocationToFlatIndex, sectionEntryKey, unwrapEntryItem, } from '@symbiote-native/components';
|
|
8
|
+
import { Platform, dlog, } from '@symbiote-native/engine';
|
|
9
|
+
import { VirtualizedList, } from '../virtualized-list';
|
|
10
|
+
// Consumed by this layer; everything LEFT OVER is VirtualizedList's own surface (the scroll host,
|
|
11
|
+
// the list chrome, the windowing knobs, the accessibility props) and rides down untouched, exactly
|
|
12
|
+
// as React's VirtualizedSectionList spreads its `...rest` onto the inner list.
|
|
13
|
+
const HANDLED_PROPS = [
|
|
14
|
+
'sections',
|
|
15
|
+
'renderItem',
|
|
16
|
+
'renderSectionHeader',
|
|
17
|
+
'renderSectionFooter',
|
|
18
|
+
'SectionSeparatorComponent',
|
|
19
|
+
'ItemSeparatorComponent',
|
|
20
|
+
'keyExtractor',
|
|
21
|
+
'stickySectionHeadersEnabled',
|
|
22
|
+
'ref',
|
|
23
|
+
];
|
|
24
|
+
export function VirtualizedSectionList(props) {
|
|
25
|
+
const [, listRest] = splitProps(props, HANDLED_PROPS);
|
|
26
|
+
const flattened = createMemo(() => flattenSections(props.sections, props.SectionSeparatorComponent !== undefined));
|
|
27
|
+
const entries = () => flattened().entries;
|
|
28
|
+
// RN sticks section headers by default only on iOS; Android does not unless asked. The
|
|
29
|
+
// headerIndices are the flat positions of every section header, which the inner list wraps.
|
|
30
|
+
const stickyHeaderIndices = () => resolveStickySectionHeaders(props.stickySectionHeadersEnabled, flattened().headerIndices, Platform.OS);
|
|
31
|
+
// The entry under a row keeps its KIND for as long as the row lives — sectionEntryKey namespaces
|
|
32
|
+
// header/footer/separator keys away from item keys, so <For> never hands a row an entry of a
|
|
33
|
+
// different kind. `previous` covers only the tick where new sections have landed but the row has
|
|
34
|
+
// not been disposed yet, during which the row's index can point at a neighbouring entry.
|
|
35
|
+
function sectionInfo(info, initial) {
|
|
36
|
+
return createMemo((previous) => {
|
|
37
|
+
const entry = info().item;
|
|
38
|
+
return entry.kind === 'section-separator'
|
|
39
|
+
? previous
|
|
40
|
+
: { section: entry.section };
|
|
41
|
+
}, { section: initial });
|
|
42
|
+
}
|
|
43
|
+
function cellInfo(info, initial) {
|
|
44
|
+
return createMemo((previous) => {
|
|
45
|
+
const entry = info().item;
|
|
46
|
+
if (entry.kind !== 'item')
|
|
47
|
+
return previous;
|
|
48
|
+
return {
|
|
49
|
+
item: entry.item,
|
|
50
|
+
index: entry.itemIndex,
|
|
51
|
+
section: entry.section,
|
|
52
|
+
separators: info().separators,
|
|
53
|
+
};
|
|
54
|
+
}, initial);
|
|
55
|
+
}
|
|
56
|
+
// The inner list, held by identity in a plain variable: everything this handle adds over
|
|
57
|
+
// VirtualizedList's own is the (section, item) -> flat index resolution, and the rest routes
|
|
58
|
+
// straight through — the shared IScrollRoutingHandle tail both list handles extend.
|
|
59
|
+
let inner;
|
|
60
|
+
const handle = {
|
|
61
|
+
scrollToLocation: (params) => {
|
|
62
|
+
const flatIndex = scrollLocationToFlatIndex(flattened().headerIndices, params.sectionIndex, params.itemIndex);
|
|
63
|
+
if (flatIndex === undefined) {
|
|
64
|
+
dlog(`VirtualizedSectionList scrollToLocation: section ${params.sectionIndex} out of range`);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
dlog(`VirtualizedSectionList scrollToLocation section=${params.sectionIndex} ` +
|
|
68
|
+
`item=${params.itemIndex} -> flat ${flatIndex}`);
|
|
69
|
+
inner?.scrollToIndex({
|
|
70
|
+
index: flatIndex,
|
|
71
|
+
viewOffset: params.viewOffset,
|
|
72
|
+
viewPosition: params.viewPosition,
|
|
73
|
+
animated: params.animated,
|
|
74
|
+
});
|
|
75
|
+
},
|
|
76
|
+
flashScrollIndicators: () => {
|
|
77
|
+
inner?.flashScrollIndicators();
|
|
78
|
+
},
|
|
79
|
+
getNativeScrollRef: () => inner?.getNativeScrollRef() ?? null,
|
|
80
|
+
getScrollableNode: () => inner?.getScrollableNode() ?? null,
|
|
81
|
+
getScrollResponder: () => inner?.getScrollResponder() ?? null,
|
|
82
|
+
getScrollNode: () => inner?.getScrollNode() ?? null,
|
|
83
|
+
recordInteraction: () => {
|
|
84
|
+
inner?.recordInteraction();
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
if (typeof props.ref === 'function')
|
|
88
|
+
props.ref(handle);
|
|
89
|
+
// Section chrome keys off its section (and never reaches the user's extractor, which is typed on
|
|
90
|
+
// ItemT); an item keys off the user's extractor with its index INSIDE its section.
|
|
91
|
+
function entryKeyExtractor(entry, index) {
|
|
92
|
+
return sectionEntryKey(entry, index, props.keyExtractor);
|
|
93
|
+
}
|
|
94
|
+
function entrySeparatorProps(entryProps) {
|
|
95
|
+
return {
|
|
96
|
+
...entryProps,
|
|
97
|
+
leadingItem: unwrapEntryItem(entryProps.leadingItem),
|
|
98
|
+
trailingItem: unwrapEntryItem(entryProps.trailingItem),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
// The user's separator is typed on ItemT while the inner stream carries the entry wrapper, so
|
|
102
|
+
// each side is unwrapped back to its item — undefined for a gap next to section chrome, which has
|
|
103
|
+
// no item on that side.
|
|
104
|
+
const entrySeparatorComponent = createMemo(() => {
|
|
105
|
+
const ItemSeparator = props.ItemSeparatorComponent;
|
|
106
|
+
if (ItemSeparator === undefined)
|
|
107
|
+
return undefined;
|
|
108
|
+
// ONE bag, not a spread plus two overriding attributes: Solid's mergeProps resolves a key by
|
|
109
|
+
// scanning its sources back to front and taking the first NON-undefined value, so an override
|
|
110
|
+
// that unwraps to `undefined` (every gap next to section chrome) falls back to the raw entry
|
|
111
|
+
// instead of clearing it, and the user's `leadingItem` reads as the wrapper object. Building the
|
|
112
|
+
// whole bag in one function keeps a single source — and, being a spread of a CALL, it compiles
|
|
113
|
+
// to mergeProps(() => …) so the separator's props stay live.
|
|
114
|
+
return (entryProps) => (<ItemSeparator {...entrySeparatorProps(entryProps)}/>);
|
|
115
|
+
});
|
|
116
|
+
// Called ONCE per cell and already untracked by VirtualizedList's buildCell.
|
|
117
|
+
function renderEntry(info) {
|
|
118
|
+
const entry = info().item;
|
|
119
|
+
if (entry.kind === 'header') {
|
|
120
|
+
const header = props.renderSectionHeader;
|
|
121
|
+
return header === undefined
|
|
122
|
+
? undefined
|
|
123
|
+
: header(sectionInfo(info, entry.section));
|
|
124
|
+
}
|
|
125
|
+
if (entry.kind === 'footer') {
|
|
126
|
+
const footer = props.renderSectionFooter;
|
|
127
|
+
return footer === undefined
|
|
128
|
+
? undefined
|
|
129
|
+
: footer(sectionInfo(info, entry.section));
|
|
130
|
+
}
|
|
131
|
+
if (entry.kind === 'section-separator') {
|
|
132
|
+
const SectionSeparator = props.SectionSeparatorComponent;
|
|
133
|
+
return SectionSeparator === undefined ? undefined : <SectionSeparator />;
|
|
134
|
+
}
|
|
135
|
+
return props.renderItem(cellInfo(info, {
|
|
136
|
+
item: entry.item,
|
|
137
|
+
index: entry.itemIndex,
|
|
138
|
+
section: entry.section,
|
|
139
|
+
separators: info().separators,
|
|
140
|
+
}));
|
|
141
|
+
}
|
|
142
|
+
return (<VirtualizedList {...listRest} data={entries()} getItem={(_source, index) => entries()[index]} getItemCount={() => entries().length} renderItem={renderEntry} stickyHeaderIndices={stickyHeaderIndices()} keyExtractor={entryKeyExtractor} ItemSeparatorComponent={entrySeparatorComponent()} ref={list => {
|
|
143
|
+
inner = list;
|
|
144
|
+
}}/>);
|
|
145
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export { View } from './components/view';
|
|
2
|
+
export type { IViewProps } from './components/view';
|
|
3
|
+
export { Text } from './components/text';
|
|
4
|
+
export type { ITextProps } from './components/text';
|
|
5
|
+
export { SafeAreaView } from './components/safe-area-view';
|
|
6
|
+
export type { ISafeAreaViewProps } from './components/safe-area-view';
|
|
7
|
+
export { Image } from './components/image';
|
|
8
|
+
export type { IImageProps } from './components/image';
|
|
9
|
+
export { Pressable } from './components/pressable';
|
|
10
|
+
export type { IPressableProps } from './components/pressable';
|
|
11
|
+
export { ActivityIndicator } from './components/activity-indicator';
|
|
12
|
+
export type { IActivityIndicatorProps } from './components/activity-indicator';
|
|
13
|
+
export type { IImageStatics, IPressState, IPressableAndroidRippleConfig, } from '@symbiote-native/components';
|
|
14
|
+
export { Switch } from './components/switch';
|
|
15
|
+
export type { ISwitchProps, ISwitchTrackColor } from './components/switch';
|
|
16
|
+
export { TextInput } from './components/text-input';
|
|
17
|
+
export type { ITextInputProps, ITextInputHandle, ITextInputSelection, } from './components/text-input';
|
|
18
|
+
export { Modal } from './components/modal';
|
|
19
|
+
export type { IModalProps, IModalAnimationType, IModalPresentationStyle, IModalOrientation, IModalOrientationChangeEvent, } from './components/modal';
|
|
20
|
+
export { KeyboardAvoidingView } from './components/keyboard-avoiding-view';
|
|
21
|
+
export type { IKeyboardAvoidingViewProps, IKeyboardAvoidingBehavior, } from './components/keyboard-avoiding-view';
|
|
22
|
+
export { RefreshControl } from './components/refresh-control';
|
|
23
|
+
export type { IRefreshControlProps } from './components/refresh-control';
|
|
24
|
+
export { ScrollView } from './components/scroll-view';
|
|
25
|
+
export type { IScrollViewProps, IScrollViewHandle, } from './components/scroll-view';
|
|
26
|
+
export { VirtualizedList } from './components/virtualized-list';
|
|
27
|
+
export type { IVirtualizedListProps, IVirtualizedListHandle, IVirtualizedListCellInfo, IVirtualizedListRenderItem, ICellLayout, ISeparators, ISeparatorProps, IViewToken, IViewableItemsChangedInfo, IViewabilityConfig, IViewabilityConfigCallbackPair, } from './components/virtualized-list';
|
|
28
|
+
export { FlatList } from './components/flat-list';
|
|
29
|
+
export type { IFlatListProps, IFlatListHandle } from './components/flat-list';
|
|
30
|
+
export { VirtualizedSectionList } from './components/virtualized-section-list';
|
|
31
|
+
export type { IVirtualizedSectionListProps, IVirtualizedSectionListHandle, ISectionHeaderInfo, ISectionCellInfo, ISection, } from './components/virtualized-section-list';
|
|
32
|
+
export { SectionList } from './components/section-list';
|
|
33
|
+
export type { ISectionListProps, ISectionListHandle, } from './components/section-list';
|
|
34
|
+
export { TouchableOpacity, TouchableHighlight, TouchableWithoutFeedback, } from './components/touchable';
|
|
35
|
+
export type { ITouchableOpacityProps, ITouchableHighlightProps, ITouchableWithoutFeedbackProps, } from './components/touchable';
|
|
36
|
+
export { TouchableNativeFeedback } from './components/touchable-native-feedback';
|
|
37
|
+
export type { ITouchableNativeFeedbackProps, INativeFeedbackBackground, IThemeAttrBackground, IRippleBackground, } from './components/touchable-native-feedback';
|
|
38
|
+
export { Button } from './components/button';
|
|
39
|
+
export type { IButtonProps } from './components/button';
|
|
40
|
+
export { ImageBackground } from './components/image-background';
|
|
41
|
+
export type { IImageBackgroundProps } from './components/image-background';
|
|
42
|
+
export { InputAccessoryView } from './components/input-accessory-view';
|
|
43
|
+
export type { IInputAccessoryViewProps } from './components/input-accessory-view';
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// The component re-export barrel, flat at the package root like every other adapter's. Grouping
|
|
2
|
+
// under components/ is internal; this file plus src/index.ts is the whole public surface.
|
|
3
|
+
export { View } from './components/view.jsx';
|
|
4
|
+
export { Text } from './components/text.jsx';
|
|
5
|
+
export { SafeAreaView } from './components/safe-area-view.jsx';
|
|
6
|
+
export { Image } from './components/image.js';
|
|
7
|
+
export { Pressable } from './components/pressable.jsx';
|
|
8
|
+
export { ActivityIndicator } from './components/activity-indicator';
|
|
9
|
+
export { Switch } from './components/switch';
|
|
10
|
+
export { TextInput } from './components/text-input.js';
|
|
11
|
+
export { Modal } from './components/modal.jsx';
|
|
12
|
+
export { KeyboardAvoidingView } from './components/keyboard-avoiding-view.jsx';
|
|
13
|
+
export { RefreshControl } from './components/refresh-control.jsx';
|
|
14
|
+
// ScrollViewStickyHeader is NOT exported, matching react-native itself: it lives at
|
|
15
|
+
// Libraries/Components/ScrollView/ScrollViewStickyHeader.js and is absent from RN's public
|
|
16
|
+
// index.js, so it is ScrollView's internal, not part of the public surface. React's and Vue's
|
|
17
|
+
// barrels agree. Svelte's and Angular's do export it, but as a WORKAROUND rather than an API
|
|
18
|
+
// decision — Svelte documents `stickyHeaderIndices` as a KNOWN GAP, so its apps have to compose the
|
|
19
|
+
// wrapper by hand. This adapter auto-wraps flagged children the way React and Vue do, so the escape
|
|
20
|
+
// hatch has nothing to escape. `IStickyHeaderComponentType` stays internal for the same reason even
|
|
21
|
+
// though `IScrollViewProps.StickyHeaderComponent` is typed by it — React has the identical shape.
|
|
22
|
+
export { ScrollView } from './components/scroll-view';
|
|
23
|
+
// The shared list detail types (ISeparators, IViewToken, IViewabilityConfig…) come through the
|
|
24
|
+
// component module rather than straight from '@symbiote-native/components', matching React's own
|
|
25
|
+
// virtualized-list barrel: a consumer typing a renderItem callback or a viewability config reaches
|
|
26
|
+
// for the same import as the component. `IVirtualizedListComponent` stays internal — it is the
|
|
27
|
+
// platform factory's return type, not API, exactly as ScrollView keeps `IScrollViewHostPlatform`
|
|
28
|
+
// out of this barrel.
|
|
29
|
+
export { VirtualizedList } from './components/virtualized-list';
|
|
30
|
+
// FlatList carries no platform split of its own — the iOS/Android divergence is
|
|
31
|
+
// VirtualizedList's refreshControlMode, inherited by importing that folder, so Metro picks the
|
|
32
|
+
// right variant with nothing here to switch on. IFlatListProps is an Omit over
|
|
33
|
+
// IVirtualizedListProps rather than a fresh declaration, so the two cannot drift apart.
|
|
34
|
+
export { FlatList } from './components/flat-list/index.jsx';
|
|
35
|
+
// The section family. ISection rides the virtualized-section-list module here, where the type is
|
|
36
|
+
// declared; React re-exports it from its section-list module instead. Either way the barrel's NAME
|
|
37
|
+
// set is what parity is measured on — which module a type is re-exported through is internal.
|
|
38
|
+
export { VirtualizedSectionList } from './components/virtualized-section-list/index.jsx';
|
|
39
|
+
// SectionList declares no input of its own — it is the preset over VirtualizedSectionList, the same
|
|
40
|
+
// relation React's 25-line section-list has to its 262-line virtualized twin.
|
|
41
|
+
export { SectionList } from './components/section-list/index.jsx';
|
|
42
|
+
// The Touchable family, all composed over Pressable exactly as React's and Vue's are — so the
|
|
43
|
+
// press machine, the aria fold and the class+style merge each happen once, in Pressable/View.
|
|
44
|
+
export { TouchableOpacity, TouchableHighlight, TouchableWithoutFeedback, } from './components/touchable/index.jsx';
|
|
45
|
+
// Android's native ripple/theme-attr feedback. The background factories are the SHARED functions
|
|
46
|
+
// hung on the component value, not adapter wrappers, so every adapter offers the identical set.
|
|
47
|
+
export { TouchableNativeFeedback } from './components/touchable-native-feedback/index.jsx';
|
|
48
|
+
// IButtonProps is agnostic and lives in @symbiote-native/components; this re-exports it rather
|
|
49
|
+
// than redeclaring it, matching react/vue/svelte/angular (CLAUDE.md prop-type split).
|
|
50
|
+
export { Button } from './components/button.jsx';
|
|
51
|
+
// Both take `children`, a framework value, so their public prop types are declared per-adapter
|
|
52
|
+
// over the shared agnostic field base — never imported from another adapter.
|
|
53
|
+
export { ImageBackground } from './components/image-background.jsx';
|
|
54
|
+
export { InputAccessoryView } from './components/input-accessory-view.jsx';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { SymbioteSurface, type ISymbioteNode } from '@symbiote-native/engine';
|
|
2
|
+
import type { JSX } from '../jsx-runtime';
|
|
3
|
+
/** Where portaled content lands. React's `IPortalContainer` twin, same two members. */
|
|
4
|
+
export type IPortalTarget = ISymbioteNode | SymbioteSurface;
|
|
5
|
+
export interface IPortalProps {
|
|
6
|
+
/** An already-mounted host node in this surface — a `ref` off a rendered component — or the
|
|
7
|
+
* surface itself. Reactive: pointing it at a different target moves the content. */
|
|
8
|
+
mount: IPortalTarget;
|
|
9
|
+
children?: JSX.Element;
|
|
10
|
+
}
|
|
11
|
+
export declare function Portal(props: IPortalProps): JSX.Element;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Portal — the Solid adapter's same-surface portal, the twin of React's createPortal
|
|
2
|
+
// (adapters/react/src/create-portal/index.ts) and Angular's PortalDirective/PortalOutletDirective
|
|
3
|
+
// pair. Those two are the only other adapters that ship this capability at all; Vue and Svelte
|
|
4
|
+
// have none, in any spelling.
|
|
5
|
+
//
|
|
6
|
+
// WHY IT IS SPELLED `<Portal mount={…}>` AND NOT `createPortal(children, target)`. Three reasons,
|
|
7
|
+
// in order of weight. (1) Solid evaluates JSX eagerly at the position it is written, so a
|
|
8
|
+
// `createPortal(<Toast/>, target)` call would BUILD the toast's host nodes before anything could
|
|
9
|
+
// decide whether to portal them — a component's `children` prop is the only lazily-evaluated
|
|
10
|
+
// shape in the language (verified below). (2) It is the exact signature solid-js/web ships for
|
|
11
|
+
// the same job (`<Portal mount={node}>`), so a Solid author already knows it. (3) The per-adapter
|
|
12
|
+
// spelling is already the rule here, not the exception: Angular's answer to the same capability is
|
|
13
|
+
// a structural directive. <adapter_src_follows_framework_idioms>.
|
|
14
|
+
//
|
|
15
|
+
// WHY solid-js/web's OWN Portal CANNOT BE RE-EXPORTED. It is built on real DOM — it allocates a
|
|
16
|
+
// container with `document.createElement` (or a ShadowRoot) and appends it to `document.body` by
|
|
17
|
+
// default. A React Native program has no `document`. Everything ELSE in Solid's control flow (For,
|
|
18
|
+
// Show, Index, ErrorBoundary, Suspense) is pure reactivity and IS re-exported by this package's
|
|
19
|
+
// barrel; Portal and Dynamic were the two exceptions, and this file closes the first of them.
|
|
20
|
+
//
|
|
21
|
+
// IMPORT IT EXPLICITLY — a bare `<Portal>` with no import in scope does NOT reach this file.
|
|
22
|
+
// `Portal` is one of babel-plugin-jsx-dom-expressions' ten `builtIns` (For, Show, Switch, Match,
|
|
23
|
+
// Suspense, SuspenseList, Portal, Index, Dynamic, ErrorBoundary): for an UNDECLARED identifier the
|
|
24
|
+
// compiler injects `import { Portal } from '<moduleName>'`, which here is ../renderer, which does
|
|
25
|
+
// not export it. Measured against the installed babel-preset-solid 1.9.12 — with `import { Portal }
|
|
26
|
+
// from '@symbiote-native/solid'` present, the compiler emits `createComponent(Portal, …)` against
|
|
27
|
+
// that local binding and injects nothing. Same rule already governs For and Show in this adapter,
|
|
28
|
+
// so this is one convention, not a new one.
|
|
29
|
+
//
|
|
30
|
+
// SCOPE — same-surface only, matching React's boundary exactly. `mount` must be an
|
|
31
|
+
// already-mounted host node WITHIN THE SAME SURFACE as the Portal's call site (typically a `ref`
|
|
32
|
+
// to a persistent overlay-host View near the app root), or that surface itself. It is not a way to
|
|
33
|
+
// reach a second, independently-mount()-ed surface: the renderer commits ONE surface per process
|
|
34
|
+
// (renderer.ts's activeSurface), so mutating a foreign surface's tree would never repaint it —
|
|
35
|
+
// structurally the same silent no-paint React documents for its own resetAfterCommit. Cross-
|
|
36
|
+
// surface content sharing is a different mechanism entirely: `createTunnel` (../create-tunnel).
|
|
37
|
+
import { createMemo, createRenderEffect, onCleanup } from 'solid-js';
|
|
38
|
+
import { createAnchor, dlog, isSymbioteNode, SymbioteSurface, } from '@symbiote-native/engine';
|
|
39
|
+
import { insert, insertNode, removeNode } from '../renderer.js';
|
|
40
|
+
// The Solid flavour of React's isSymbioteNode guard, and it needs its own wording: React tells the
|
|
41
|
+
// caller to check for a forgotten `.current`, which does not exist here. A Solid `ref` is a plain
|
|
42
|
+
// variable the compiler assigns into, and the idiomatic way to hold one across a re-render is a
|
|
43
|
+
// signal — whose accessor must be CALLED. `mount={overlay}` (the accessor itself) and
|
|
44
|
+
// `mount={overlay()}` read before the target element exists are the two real mistakes.
|
|
45
|
+
function assertPortalTarget(target) {
|
|
46
|
+
if (target instanceof SymbioteSurface || isSymbioteNode(target))
|
|
47
|
+
return target;
|
|
48
|
+
throw new Error('Portal `mount` must be an already-mounted host node (a ref off a rendered <View>) or a surface — got something else. Did you pass the signal instead of CALLING it (`mount={overlay()}`), read it before the target element existed (gate the Portal behind <Show when={overlay()}>), or pass a CSS-selector-style string?');
|
|
49
|
+
}
|
|
50
|
+
export function Portal(props) {
|
|
51
|
+
// One engine ANCHOR per Portal instance, used as a fragment host: it is a real retained node, so
|
|
52
|
+
// the children have a stable exclusive parent to be reconciled under, and the commit walk
|
|
53
|
+
// FLATTENS an anchor's children into its parent (renderableChildren in core/engine/src/commit.ts)
|
|
54
|
+
// so nothing extra paints. That is what keeps portaled content a DIRECT Fabric child of the
|
|
55
|
+
// target, matching React's createPortal, instead of gaining the wrapper element solid-js/web's
|
|
56
|
+
// Portal creates because the DOM has no such flattening node.
|
|
57
|
+
//
|
|
58
|
+
// It is also what makes teardown one call: removing the anchor from the target removes the whole
|
|
59
|
+
// portaled subtree with it, so nothing has to track which nodes `insert` put where.
|
|
60
|
+
const host = createAnchor();
|
|
61
|
+
// Validated in a memo rather than at the top of the body: `mount` is a props getter, so reading
|
|
62
|
+
// it here would freeze the first target, and a component body runs once. The memo re-validates
|
|
63
|
+
// whenever the caller points the Portal somewhere else.
|
|
64
|
+
const target = createMemo(() => assertPortalTarget(props.mount));
|
|
65
|
+
// createRenderEffect, not createEffect: this must attach the host DURING the render pass that
|
|
66
|
+
// created it, so the portaled content reaches the SAME microtask commit as everything else the
|
|
67
|
+
// pass touched (renderer.ts's requestCommit coalesces them). solid-js/web can afford the
|
|
68
|
+
// deferred createEffect because a DOM append IS the paint.
|
|
69
|
+
createRenderEffect(() => {
|
|
70
|
+
const to = target();
|
|
71
|
+
dlog(`solid portal -> ${to instanceof SymbioteSurface ? 'surface' : to.component}`);
|
|
72
|
+
insertNode(to, host);
|
|
73
|
+
// Runs both on a `mount` change (before the effect re-attaches elsewhere) and on the Portal's
|
|
74
|
+
// own disposal — the content leaves the target either way.
|
|
75
|
+
onCleanup(() => {
|
|
76
|
+
removeNode(to, host);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
// The same call compiled JSX emits for a component's children (see components/view.tsx): the
|
|
80
|
+
// accessor is handed to the renderer's own insert, which owns the render effect that keeps the
|
|
81
|
+
// subtree in sync. `marker` is deliberately omitted — the anchor host is exclusively ours, so
|
|
82
|
+
// insert may own its entire child list.
|
|
83
|
+
insert(host, () => props.children);
|
|
84
|
+
// Nothing paints at the call site. React's createPortal returns a ReactPortal that renders
|
|
85
|
+
// nothing there for the same reason; solid-js/web returns a DOM marker text node only because
|
|
86
|
+
// it needs to hold a position it might later hydrate.
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { JSX } from '../jsx-runtime';
|
|
2
|
+
export interface ITunnelInProps {
|
|
3
|
+
children?: JSX.Element;
|
|
4
|
+
}
|
|
5
|
+
export interface ITunnel {
|
|
6
|
+
/** Registers its children under the tunnel from wherever it is mounted — any surface — and
|
|
7
|
+
* paints nothing itself. */
|
|
8
|
+
In: (props: ITunnelInProps) => JSX.Element;
|
|
9
|
+
/** Renders everything currently tunneled in, in registration order. Mount this in the component
|
|
10
|
+
* that should actually paint the content. */
|
|
11
|
+
Out: () => JSX.Element;
|
|
12
|
+
}
|
|
13
|
+
export declare function createTunnel(): ITunnel;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// createTunnel — the Solid twin of adapters/react/src/create-tunnel/index.tsx, and of the Vue,
|
|
2
|
+
// Svelte and Angular versions. It answers the case `Portal` (../create-portal) deliberately does
|
|
3
|
+
// not: content authored in one place painting on a GENUINELY different, independently mounted
|
|
4
|
+
// surface. The mechanism is the one pmndrs/tunnel-rat settled on and every adapter here copies —
|
|
5
|
+
// a plain shared store, never a reach into a foreign surface's tree. `Out` renders from whichever
|
|
6
|
+
// surface it is mounted on, through that surface's own ordinary render/commit; `In` only writes to
|
|
7
|
+
// the store and never touches a host node, so it needs no target, no ref, and no guard.
|
|
8
|
+
//
|
|
9
|
+
// SHAPE. `In`/`Out` are per-call COMPONENTS closed over this tunnel's own registry, the same shape
|
|
10
|
+
// React and Vue use — Solid components are ordinary functions, so a factory can mint a fresh pair
|
|
11
|
+
// per createTunnel() call. Svelte's version has to differ (a `.svelte` file compiles to one fixed
|
|
12
|
+
// top-level component, so its tunnel is a data object passed as a prop) and Angular's has to
|
|
13
|
+
// differ again (no runtime component synthesis without JIT). Solid needs neither workaround.
|
|
14
|
+
//
|
|
15
|
+
// React's In/Out had to be separate components to avoid a real infinite render loop:
|
|
16
|
+
// useSyncExternalStore's only lever is "re-render the whole component that subscribed", so a hook
|
|
17
|
+
// version re-entered its own writer. Solid cannot reproduce that bug — a component body runs ONCE
|
|
18
|
+
// and a signal write re-runs only the computations that READ it, which here is `Out`'s <For> and
|
|
19
|
+
// nothing else. Separate components remain the right shape anyway, because `In` takes markup and
|
|
20
|
+
// only a component has a children slot. The regression guard lives in create-tunnel.test.tsx all
|
|
21
|
+
// the same, pinned to In's body running exactly once.
|
|
22
|
+
import { createSignal, For, onCleanup } from 'solid-js';
|
|
23
|
+
import { dlog } from '@symbiote-native/engine';
|
|
24
|
+
export function createTunnel() {
|
|
25
|
+
// A signal over a frozen array, not createStore: every write replaces the whole list, and the
|
|
26
|
+
// only consumer is a keyed <For> that already diffs by row identity — a store's deep proxy would
|
|
27
|
+
// buy nothing and would wrap the entries.
|
|
28
|
+
const [entries, setEntries] = createSignal([]);
|
|
29
|
+
function In(props) {
|
|
30
|
+
// `() => props.children` and NOT `props.children`. The compiler emits children as a lazy
|
|
31
|
+
// getter (`{ get children() { … } }`, verified against babel-preset-solid 1.9.12), so reading
|
|
32
|
+
// it here would build the content under THIS component's owner, on the source surface, only
|
|
33
|
+
// for `Out` to adopt orphan nodes. Deferring the read means the content is created where it is
|
|
34
|
+
// rendered — and re-created honestly if a second `Out` renders the same tunnel.
|
|
35
|
+
const content = () => props.children;
|
|
36
|
+
// Registered synchronously in the body, not from onMount: an `Out` mounted LATER (a second
|
|
37
|
+
// surface, mounted after the source) must find the entry already there on its first render.
|
|
38
|
+
setEntries(current => [...current, content]);
|
|
39
|
+
// Logged WITHOUT reading entries() back: a component body runs inside untrack(), so a read here
|
|
40
|
+
// is harmless today, but it is one refactor away from making In a reader of the signal it
|
|
41
|
+
// writes — the self-notify shape that cost the React adapter an infinite render loop.
|
|
42
|
+
dlog('solid tunnel: entry registered');
|
|
43
|
+
onCleanup(() => {
|
|
44
|
+
setEntries(current => current.filter(item => item !== content));
|
|
45
|
+
dlog('solid tunnel: entry released');
|
|
46
|
+
});
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
function Out() {
|
|
50
|
+
return <For each={entries()}>{content => content()}</For>;
|
|
51
|
+
}
|
|
52
|
+
return { In, Out };
|
|
53
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// The Descriptor -> element bridge for Solid. A render function in @symbiote-native/components
|
|
2
|
+
// returns a framework-agnostic `Descriptor` tree; this materializes it as real engine nodes that
|
|
3
|
+
// flow on through the universal renderer -> engine -> Fabric, exactly like compiled JSX would.
|
|
4
|
+
//
|
|
5
|
+
// IT TAKES AN ACCESSOR, not a Descriptor — and that is the whole design, not a convenience.
|
|
6
|
+
// React's and Vue's bridges are plain recursive walkers that rebuild the element tree on every
|
|
7
|
+
// render because those frameworks re-run a component's body and diff the result. Solid runs a
|
|
8
|
+
// component body ONCE; there is no re-render and no diff. So a `descriptorToSolid(descriptor)`
|
|
9
|
+
// taking a value would paint the mount-time props and then freeze — every later `value` change
|
|
10
|
+
// invisible. Returning a fresh tree from an accessor instead is worse than frozen: Solid's
|
|
11
|
+
// insertExpression would REPLACE the node, destroying the identity that dispatchViewCommand, the
|
|
12
|
+
// commit mirror and native-owned state (a Switch's own grip, a TextInput's cursor) all key on.
|
|
13
|
+
//
|
|
14
|
+
// So the tree is built ONCE and every node's props are wired through the renderer's own `spread`,
|
|
15
|
+
// which is a render effect that diffs prop-by-prop against the previous values and calls
|
|
16
|
+
// setProperty -> routeProp only for the ones that actually changed. Same mechanism, same node
|
|
17
|
+
// identity, as compiled `<symbiote-switch value={v()} />`.
|
|
18
|
+
//
|
|
19
|
+
// This relies on `render-*.ts` producing a SHAPE-STABLE Descriptor: same type, same child count,
|
|
20
|
+
// text where text was — only prop VALUES vary between calls. That is already the contract the
|
|
21
|
+
// Svelte bridge depends on (svelte-adapter-dom-shim skill §15/§19); a violation throws here rather
|
|
22
|
+
// than silently painting a half-updated tree.
|
|
23
|
+
import { createMemo, createRenderEffect } from 'solid-js';
|
|
24
|
+
import { isSymbioteNode } from '@symbiote-native/engine';
|
|
25
|
+
import { createDescriptorShapeGuard } from '@symbiote-native/components';
|
|
26
|
+
import { createElement, createTextNode, insertNode, replaceText, spread, } from './renderer.js';
|
|
27
|
+
// The vanished-key widening the render effect below stands on. Shared with the host primitives
|
|
28
|
+
// (components/view, components/text), which fold their props through the same two-branch
|
|
29
|
+
// resolveAccessibilityProps — see that module's header.
|
|
30
|
+
import { withStableKeys } from './utils/stable-keys.js';
|
|
31
|
+
// The predicates belong to @symbiote-native/components, next to the Descriptor whose contract they
|
|
32
|
+
// guard — this bridge and Svelte's had each grown a private copy, and the two had already drifted
|
|
33
|
+
// to different coverage.
|
|
34
|
+
const shape = createDescriptorShapeGuard('descriptorToSolid');
|
|
35
|
+
function childAt(children, index) {
|
|
36
|
+
const child = children[index];
|
|
37
|
+
if (child === undefined)
|
|
38
|
+
throw shape.error(`child ${index} disappeared`);
|
|
39
|
+
return child;
|
|
40
|
+
}
|
|
41
|
+
// The tree's shape is read ONCE, at build, so every later run has to be re-checked against that
|
|
42
|
+
// first reading. Skipping this fails silently both ways: a changed `type` lands the new
|
|
43
|
+
// descriptor's props on the OLD host element, and a grown child list is simply never mounted.
|
|
44
|
+
// The Svelte bridge's syncChild guards these same two cases — matching its coverage is
|
|
45
|
+
// deliberate, since both bridges stand on the identical shape-stability contract.
|
|
46
|
+
function assertSameShape(initial, next) {
|
|
47
|
+
shape.assertType(initial.type, next.type);
|
|
48
|
+
shape.assertChildCount(initial.children.length, next.children.length);
|
|
49
|
+
return next;
|
|
50
|
+
}
|
|
51
|
+
// `spread(node, accessor, true)`: skipChildren, because a Descriptor keeps its children in
|
|
52
|
+
// `.children`, never in `.props.children`. spread also calls a `ref` found in the bag with the
|
|
53
|
+
// node — which is how a caller's own `ref={…}` on a component reaches the host element, since it
|
|
54
|
+
// rides down inside `passthrough` untouched.
|
|
55
|
+
function buildNode(descriptor) {
|
|
56
|
+
const initial = descriptor();
|
|
57
|
+
const node = createElement(initial.type);
|
|
58
|
+
// Narrowing, not defensive: createElement is typed over the renderer's IHostNode union (which
|
|
59
|
+
// includes the surface), while everything below needs a real host node.
|
|
60
|
+
if (!isSymbioteNode(node)) {
|
|
61
|
+
throw new Error(`descriptorToSolid: ${initial.type} did not create a host node`);
|
|
62
|
+
}
|
|
63
|
+
spread(node, withStableKeys(() => assertSameShape(initial, descriptor()).props), true);
|
|
64
|
+
mountChildren(node, () => descriptor().children);
|
|
65
|
+
return node;
|
|
66
|
+
}
|
|
67
|
+
function mountChildren(parent, children) {
|
|
68
|
+
children().forEach((initial, index) => {
|
|
69
|
+
const child = () => childAt(children(), index);
|
|
70
|
+
if (typeof initial === 'string') {
|
|
71
|
+
const textNode = createTextNode(initial);
|
|
72
|
+
insertNode(parent, textNode);
|
|
73
|
+
// The `previous` accumulator is why this is a render effect with a seed rather than a plain
|
|
74
|
+
// one: the first run must NOT re-write text createTextNode already carries, and a later run
|
|
75
|
+
// must skip a recomputed-but-unchanged string instead of paying a commit for it.
|
|
76
|
+
createRenderEffect(previous => {
|
|
77
|
+
const value = shape.asText(child());
|
|
78
|
+
if (value !== previous)
|
|
79
|
+
replaceText(textNode, value);
|
|
80
|
+
return value;
|
|
81
|
+
}, initial);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
insertNode(parent, buildNode(() => shape.asElement(child())));
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
// The memo is what keeps the render fn running ONCE per change no matter how many accessors read
|
|
88
|
+
// it — the root's props, every child's props, every text child all derive from the same call.
|
|
89
|
+
export function descriptorToSolid(descriptor) {
|
|
90
|
+
return buildNode(createMemo(descriptor));
|
|
91
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// findNodeHandle, the Solid adapter twin of adapters/react/src/host-instance.ts and
|
|
2
|
+
// adapters/vue/src/host-instance. RN's "ref/instance -> native reactTag" lookup, the seam
|
|
3
|
+
// imperative-interop libraries (reanimated, gesture-handler, react-navigation) reach through.
|
|
4
|
+
//
|
|
5
|
+
// A Solid `ref={el}` on a symbiote host tag assigns the raw engine SymbioteNode, and the renderer
|
|
6
|
+
// grafts the public-instance API onto it (toPublicInstance in renderer.ts), so the node a ref
|
|
7
|
+
// receives carries measure / setNativeProps / focus exactly like React's getPublicInstance.
|
|
8
|
+
//
|
|
9
|
+
// The node -> tag resolution itself belongs to the engine (getNativeTag, keyed on the raw node in the
|
|
10
|
+
// commit mirror); this only unwraps the Solid-shaped inputs onto it. An uncommitted or unknown input
|
|
11
|
+
// has no tag yet and surfaces as null.
|
|
12
|
+
import { getNativeTag, isSymbioteNode, dlog } from '@symbiote-native/engine';
|
|
13
|
+
// Solid's counterpart to Vue's isRef branch. Solid has no ref wrapper — `ref={el}` writes the node
|
|
14
|
+
// straight into a variable — but holding that node in a signal is idiomatic, and a signal reads as a
|
|
15
|
+
// zero-argument accessor. Calling it is the only way to unwrap one (there is no isSignal predicate:
|
|
16
|
+
// an accessor is an ordinary function). Note this UNWRAPS ONE LEVEL only, deliberately: recursing
|
|
17
|
+
// would turn a component that happens to be passed here into a call chain.
|
|
18
|
+
function isAccessor(value) {
|
|
19
|
+
return typeof value === 'function' && value.length === 0;
|
|
20
|
+
}
|
|
21
|
+
export function findNodeHandle(componentOrHandle) {
|
|
22
|
+
const candidate = isAccessor(componentOrHandle)
|
|
23
|
+
? componentOrHandle()
|
|
24
|
+
: componentOrHandle;
|
|
25
|
+
if (candidate === null || candidate === undefined)
|
|
26
|
+
return null;
|
|
27
|
+
if (typeof candidate === 'number')
|
|
28
|
+
return candidate;
|
|
29
|
+
if (isSymbioteNode(candidate)) {
|
|
30
|
+
const tag = getNativeTag(candidate) ?? null;
|
|
31
|
+
dlog(`findNodeHandle: component=${candidate.component} tag=${tag}`);
|
|
32
|
+
return tag;
|
|
33
|
+
}
|
|
34
|
+
dlog('findNodeHandle: input did not resolve to a symbiote host node');
|
|
35
|
+
return null;
|
|
36
|
+
}
|