@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.
Files changed (105) hide show
  1. package/build/bootstrap.d.ts +7 -0
  2. package/build/bootstrap.js +18 -0
  3. package/build/components/activity-indicator/index.android.d.ts +2 -0
  4. package/build/components/activity-indicator/index.android.js +11 -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 +8 -0
  10. package/build/components/activity-indicator/shared.js +51 -0
  11. package/build/components/button.d.ts +7 -0
  12. package/build/components/button.jsx +45 -0
  13. package/build/components/flat-list/index.d.ts +10 -0
  14. package/build/components/flat-list/index.jsx +118 -0
  15. package/build/components/image-background.d.ts +10 -0
  16. package/build/components/image-background.jsx +74 -0
  17. package/build/components/image.d.ts +9 -0
  18. package/build/components/image.js +75 -0
  19. package/build/components/input-accessory-view.d.ts +11 -0
  20. package/build/components/input-accessory-view.jsx +39 -0
  21. package/build/components/keyboard-avoiding-view.d.ts +15 -0
  22. package/build/components/keyboard-avoiding-view.jsx +152 -0
  23. package/build/components/modal.d.ts +24 -0
  24. package/build/components/modal.jsx +125 -0
  25. package/build/components/pressable.d.ts +30 -0
  26. package/build/components/pressable.jsx +185 -0
  27. package/build/components/refresh-control.d.ts +18 -0
  28. package/build/components/refresh-control.jsx +36 -0
  29. package/build/components/safe-area-view.d.ts +10 -0
  30. package/build/components/safe-area-view.jsx +24 -0
  31. package/build/components/scroll-view/index.android.d.ts +2 -0
  32. package/build/components/scroll-view/index.android.js +11 -0
  33. package/build/components/scroll-view/index.d.ts +2 -0
  34. package/build/components/scroll-view/index.ios.d.ts +2 -0
  35. package/build/components/scroll-view/index.ios.js +8 -0
  36. package/build/components/scroll-view/index.js +9 -0
  37. package/build/components/scroll-view/shared.d.ts +85 -0
  38. package/build/components/scroll-view/shared.jsx +334 -0
  39. package/build/components/scroll-view/sticky-header.d.ts +18 -0
  40. package/build/components/scroll-view/sticky-header.jsx +182 -0
  41. package/build/components/section-list/index.d.ts +6 -0
  42. package/build/components/section-list/index.jsx +8 -0
  43. package/build/components/switch/index.android.d.ts +2 -0
  44. package/build/components/switch/index.android.js +12 -0
  45. package/build/components/switch/index.d.ts +1 -0
  46. package/build/components/switch/index.ios.d.ts +2 -0
  47. package/build/components/switch/index.ios.js +11 -0
  48. package/build/components/switch/index.js +4 -0
  49. package/build/components/switch/shared.d.ts +11 -0
  50. package/build/components/switch/shared.js +87 -0
  51. package/build/components/text-input.d.ts +10 -0
  52. package/build/components/text-input.js +268 -0
  53. package/build/components/text.d.ts +26 -0
  54. package/build/components/text.jsx +27 -0
  55. package/build/components/touchable/index.d.ts +22 -0
  56. package/build/components/touchable/index.jsx +247 -0
  57. package/build/components/touchable-native-feedback/index.d.ts +16 -0
  58. package/build/components/touchable-native-feedback/index.jsx +68 -0
  59. package/build/components/view.d.ts +32 -0
  60. package/build/components/view.jsx +49 -0
  61. package/build/components/virtualized-list/index.android.d.ts +2 -0
  62. package/build/components/virtualized-list/index.android.js +9 -0
  63. package/build/components/virtualized-list/index.d.ts +1 -0
  64. package/build/components/virtualized-list/index.ios.d.ts +2 -0
  65. package/build/components/virtualized-list/index.ios.js +9 -0
  66. package/build/components/virtualized-list/index.js +4 -0
  67. package/build/components/virtualized-list/shared.d.ts +75 -0
  68. package/build/components/virtualized-list/shared.jsx +782 -0
  69. package/build/components/virtualized-section-list/index.d.ts +62 -0
  70. package/build/components/virtualized-section-list/index.jsx +145 -0
  71. package/build/components.d.ts +43 -0
  72. package/build/components.js +54 -0
  73. package/build/create-portal/index.d.ts +11 -0
  74. package/build/create-portal/index.jsx +88 -0
  75. package/build/create-tunnel/index.d.ts +13 -0
  76. package/build/create-tunnel/index.jsx +53 -0
  77. package/build/descriptor-to-solid.d.ts +3 -0
  78. package/build/descriptor-to-solid.js +91 -0
  79. package/build/host-instance.d.ts +2 -0
  80. package/build/host-instance.js +36 -0
  81. package/build/index.d.ts +30 -0
  82. package/build/index.js +96 -0
  83. package/build/jsx-runtime.d.ts +26 -0
  84. package/build/jsx-runtime.js +46 -0
  85. package/build/modules/animated/create-animated-component.d.ts +10 -0
  86. package/build/modules/animated/create-animated-component.jsx +105 -0
  87. package/build/modules/animated/index.d.ts +54 -0
  88. package/build/modules/animated/index.js +78 -0
  89. package/build/modules/app-registry/index.d.ts +7 -0
  90. package/build/modules/app-registry/index.js +37 -0
  91. package/build/modules/status-bar.d.ts +7 -0
  92. package/build/modules/status-bar.js +45 -0
  93. package/build/primitives/create-color-scheme.d.ts +3 -0
  94. package/build/primitives/create-color-scheme.js +33 -0
  95. package/build/primitives/create-window-dimensions.d.ts +3 -0
  96. package/build/primitives/create-window-dimensions.js +34 -0
  97. package/build/render.d.ts +7 -0
  98. package/build/render.js +91 -0
  99. package/build/renderer.d.ts +7 -0
  100. package/build/renderer.js +195 -0
  101. package/build/utils/host-ref.d.ts +3 -0
  102. package/build/utils/host-ref.js +22 -0
  103. package/build/utils/stable-keys.d.ts +1 -0
  104. package/build/utils/stable-keys.js +31 -0
  105. 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,3 @@
1
+ import { type ISymbioteNode } from '@symbiote-native/engine';
2
+ import type { IDescriptor } from '@symbiote-native/components';
3
+ export declare function descriptorToSolid(descriptor: () => IDescriptor): ISymbioteNode;
@@ -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,2 @@
1
+ export type { IHostInstance } from '@symbiote-native/engine';
2
+ export declare function findNodeHandle(componentOrHandle: unknown): number | null;
@@ -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
+ }