@symbiote-native/solid 0.0.1 → 1.0.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 (112) hide show
  1. package/README.md +25 -14
  2. package/babel-lower-host-primitives.cjs +323 -0
  3. package/babel-preset.cjs +5 -0
  4. package/build/bootstrap.d.ts +7 -0
  5. package/build/bootstrap.js +18 -0
  6. package/build/components/activity-indicator/index.android.d.ts +2 -0
  7. package/build/components/activity-indicator/index.android.js +11 -0
  8. package/build/components/activity-indicator/index.d.ts +1 -0
  9. package/build/components/activity-indicator/index.ios.d.ts +2 -0
  10. package/build/components/activity-indicator/index.ios.js +10 -0
  11. package/build/components/activity-indicator/index.js +4 -0
  12. package/build/components/activity-indicator/shared.d.ts +8 -0
  13. package/build/components/activity-indicator/shared.js +51 -0
  14. package/build/components/button.d.ts +7 -0
  15. package/build/components/button.jsx +45 -0
  16. package/build/components/flat-list/index.d.ts +10 -0
  17. package/build/components/flat-list/index.jsx +118 -0
  18. package/build/components/image-background.d.ts +10 -0
  19. package/build/components/image-background.jsx +74 -0
  20. package/build/components/image.d.ts +9 -0
  21. package/build/components/image.js +75 -0
  22. package/build/components/input-accessory-view.d.ts +11 -0
  23. package/build/components/input-accessory-view.jsx +39 -0
  24. package/build/components/keyboard-avoiding-view.d.ts +15 -0
  25. package/build/components/keyboard-avoiding-view.jsx +152 -0
  26. package/build/components/modal.d.ts +24 -0
  27. package/build/components/modal.jsx +125 -0
  28. package/build/components/pressable.d.ts +31 -0
  29. package/build/components/pressable.jsx +211 -0
  30. package/build/components/refresh-control.d.ts +18 -0
  31. package/build/components/refresh-control.jsx +36 -0
  32. package/build/components/safe-area-view.d.ts +11 -0
  33. package/build/components/safe-area-view.jsx +24 -0
  34. package/build/components/scroll-view/index.android.d.ts +2 -0
  35. package/build/components/scroll-view/index.android.js +11 -0
  36. package/build/components/scroll-view/index.d.ts +2 -0
  37. package/build/components/scroll-view/index.ios.d.ts +2 -0
  38. package/build/components/scroll-view/index.ios.js +8 -0
  39. package/build/components/scroll-view/index.js +9 -0
  40. package/build/components/scroll-view/shared.d.ts +85 -0
  41. package/build/components/scroll-view/shared.jsx +334 -0
  42. package/build/components/scroll-view/sticky-header.d.ts +18 -0
  43. package/build/components/scroll-view/sticky-header.jsx +182 -0
  44. package/build/components/section-list/index.d.ts +6 -0
  45. package/build/components/section-list/index.jsx +8 -0
  46. package/build/components/switch/index.android.d.ts +2 -0
  47. package/build/components/switch/index.android.js +12 -0
  48. package/build/components/switch/index.d.ts +1 -0
  49. package/build/components/switch/index.ios.d.ts +2 -0
  50. package/build/components/switch/index.ios.js +11 -0
  51. package/build/components/switch/index.js +4 -0
  52. package/build/components/switch/shared.d.ts +11 -0
  53. package/build/components/switch/shared.js +87 -0
  54. package/build/components/text-input.d.ts +10 -0
  55. package/build/components/text-input.js +290 -0
  56. package/build/components/text.d.ts +26 -0
  57. package/build/components/text.jsx +27 -0
  58. package/build/components/touchable/index.d.ts +22 -0
  59. package/build/components/touchable/index.jsx +247 -0
  60. package/build/components/touchable-native-feedback/index.d.ts +16 -0
  61. package/build/components/touchable-native-feedback/index.jsx +68 -0
  62. package/build/components/view.d.ts +32 -0
  63. package/build/components/view.jsx +49 -0
  64. package/build/components/virtualized-list/index.android.d.ts +2 -0
  65. package/build/components/virtualized-list/index.android.js +9 -0
  66. package/build/components/virtualized-list/index.d.ts +1 -0
  67. package/build/components/virtualized-list/index.ios.d.ts +2 -0
  68. package/build/components/virtualized-list/index.ios.js +9 -0
  69. package/build/components/virtualized-list/index.js +4 -0
  70. package/build/components/virtualized-list/shared.d.ts +75 -0
  71. package/build/components/virtualized-list/shared.jsx +782 -0
  72. package/build/components/virtualized-section-list/index.d.ts +62 -0
  73. package/build/components/virtualized-section-list/index.jsx +145 -0
  74. package/build/components.d.ts +43 -0
  75. package/build/components.js +54 -0
  76. package/build/create-portal/index.d.ts +11 -0
  77. package/build/create-portal/index.jsx +88 -0
  78. package/build/create-tunnel/index.d.ts +13 -0
  79. package/build/create-tunnel/index.jsx +53 -0
  80. package/build/descriptor-to-solid.d.ts +3 -0
  81. package/build/descriptor-to-solid.js +91 -0
  82. package/build/host-instance.d.ts +2 -0
  83. package/build/host-instance.js +36 -0
  84. package/build/index.d.ts +31 -0
  85. package/build/index.js +98 -0
  86. package/build/jsx-runtime.d.ts +26 -0
  87. package/build/jsx-runtime.js +46 -0
  88. package/build/modules/animated/create-animated-component.d.ts +10 -0
  89. package/build/modules/animated/create-animated-component.jsx +127 -0
  90. package/build/modules/animated/index.d.ts +54 -0
  91. package/build/modules/animated/index.js +78 -0
  92. package/build/modules/app-registry/index.d.ts +7 -0
  93. package/build/modules/app-registry/index.js +37 -0
  94. package/build/modules/status-bar.d.ts +7 -0
  95. package/build/modules/status-bar.js +45 -0
  96. package/build/primitives/create-color-scheme.d.ts +3 -0
  97. package/build/primitives/create-color-scheme.js +33 -0
  98. package/build/primitives/create-window-dimensions.d.ts +3 -0
  99. package/build/primitives/create-window-dimensions.js +34 -0
  100. package/build/register.d.ts +1 -0
  101. package/build/register.js +31 -0
  102. package/build/render.d.ts +7 -0
  103. package/build/render.js +91 -0
  104. package/build/renderer.d.ts +8 -0
  105. package/build/renderer.js +259 -0
  106. package/build/state-style.d.ts +1 -0
  107. package/build/state-style.js +13 -0
  108. package/build/utils/host-ref.d.ts +3 -0
  109. package/build/utils/host-ref.js +22 -0
  110. package/build/utils/stable-keys.d.ts +1 -0
  111. package/build/utils/stable-keys.js +31 -0
  112. package/package.json +24 -8
@@ -0,0 +1,247 @@
1
+ // The Touchable* family for Solid, all built on Pressable. Ported against RN's own sources
2
+ // (.vendors/react-native/.../Components/Touchable/*.js), NOT against this repo's React adapter —
3
+ // an audit on 2026-08-19 found the React/Vue/Svelte/Angular ports diverge from RN in ten places.
4
+ // This adapter is the first on the RN-accurate shared API; the other four migrate in phase two,
5
+ // which is why @symbiote-native/components carries both forms for now.
6
+ //
7
+ // Shared (framework-agnostic) half: the press-scheduling machine, the underlay machine, the resting
8
+ // opacity math, the press-handler gate, the extra-style split. Solid owns only the lifecycle —
9
+ // signals, memos, the real timers, and the Animated wiring.
10
+ //
11
+ // NOTHING here destructures `props`. A Solid component body runs ONCE and props are getters, so a
12
+ // destructure would freeze the component at its mount-time config; every read below sits inside an
13
+ // accessor, a memo or an event handler.
14
+ //
15
+ // No conditional picks a branch in any of the three trees. Solid's `insert` REPLACES a subtree
16
+ // rather than diffing it, and a rebuild landing mid-gesture costs the native responder grant
17
+ // (.claude/rules/solid-descriptor-bridge.md §4). Every per-press value here is a prop-bag value,
18
+ // and a prop bag reaches the host through `spread` — a per-key diff on the SAME element.
19
+ import { createEffect, createMemo, createSignal, onCleanup, splitProps, untrack, } from 'solid-js';
20
+ import { createHighlightUnderlayHandlers, createHighlightUnderlayRuntime, createTouchableFeedbackHandlers, createTouchableFeedbackRuntime, hasTouchablePressHandler, resolveHighlightExtraStyles, restingOpacityFromStyle, DEFAULT_ACTIVE_OPACITY, OPACITY_ACTIVE_GRANT_DURATION_MS, OPACITY_INACTIVE_DURATION_MS, TOUCHABLE_MIN_PRESS_DURATION_MS, } from '@symbiote-native/components';
21
+ import { dlog, } from '@symbiote-native/engine';
22
+ import { Animated } from '../../modules/animated/index.js';
23
+ import { TouchablePressable as Pressable, } from '../pressable.jsx';
24
+ // The real timers both shared machines schedule on — core/components carries no timer globals, so
25
+ // scheduling is the adapter's half. Every canceller is retained so unmount cancels what is still in
26
+ // flight: a Solid body runs once, so there is no re-render that would otherwise drop a stale timer,
27
+ // and a fired timer would call back into a disposed scope.
28
+ function createTimeoutScheduler() {
29
+ const pending = new Set();
30
+ onCleanup(() => {
31
+ for (const cancel of [...pending])
32
+ cancel();
33
+ pending.clear();
34
+ });
35
+ return (callback, ms) => {
36
+ const id = setTimeout(() => {
37
+ pending.delete(cancel);
38
+ callback();
39
+ }, ms);
40
+ const cancel = () => {
41
+ clearTimeout(id);
42
+ pending.delete(cancel);
43
+ };
44
+ pending.add(cancel);
45
+ return cancel;
46
+ };
47
+ }
48
+ // `class` is split off with `style`: both describe the FEEDBACK node (the inner Animated.View),
49
+ // not the outer Pressable that owns the responder. Left in `rest` they would land on the wrong one.
50
+ const TOUCHABLE_OPACITY_HANDLED = [
51
+ 'activeOpacity',
52
+ 'style',
53
+ 'class',
54
+ 'children',
55
+ 'onPressIn',
56
+ 'onPressOut',
57
+ 'delayPressIn',
58
+ 'delayPressOut',
59
+ 'minPressDuration',
60
+ ];
61
+ export function TouchableOpacity(props) {
62
+ const [local, rest] = splitProps(props, TOUCHABLE_OPACITY_HANDLED);
63
+ // RN's _getChildStyleOpacityWithDefault: the fade settles at the opacity the CALLER's style asks
64
+ // for, not at a hard 1.
65
+ const restingOpacity = createMemo(() => restingOpacityFromStyle(local.style));
66
+ // One Animated.Value per mount, held by IDENTITY in a plain variable — a signal or store would
67
+ // wrap this engine object in a proxy, and the graph's bookkeeping is keyed on the raw instance
68
+ // (symbiote-engine-core §3). untrack because the body must not subscribe to the initial read.
69
+ const opacity = new Animated.Value(untrack(restingOpacity));
70
+ // The shared press-scheduling cell (delayPressIn canceller + activation clock). A setup-scope
71
+ // object, never reactive: the machine mutates it on every event and nothing reads it reactively.
72
+ const runtime = createTouchableFeedbackRuntime();
73
+ const schedule = createTimeoutScheduler();
74
+ function setOpacityTo(toValue, duration) {
75
+ dlog(`TouchableOpacity opacity -> ${toValue} over ${duration}ms`);
76
+ // useNativeDriver: true is RN's own (TouchableOpacity.js:242). opacity is natively drivable and
77
+ // the fade then survives a busy JS thread, which is the whole point of the feedback.
78
+ Animated.timing(opacity, {
79
+ toValue,
80
+ duration,
81
+ easing: Animated.Easing.inOut(Animated.Easing.quad),
82
+ useNativeDriver: true,
83
+ }).start();
84
+ }
85
+ // Rebuilt only when the TIMING config changes — the callbacks read activeOpacity / onPressIn /
86
+ // onPressOut when they fire, so a re-supplied callback is honored without churning the handler
87
+ // identities the host node holds. The runtime persists across rebuilds, so an in-flight
88
+ // delayPressIn timer survives one.
89
+ const handlers = createMemo(() => createTouchableFeedbackHandlers({
90
+ delayPressIn: local.delayPressIn ?? 0,
91
+ delayPressOut: local.delayPressOut ?? 0,
92
+ // RN's Touchables override Pressability's 130ms floor with 0; the active visual is held by
93
+ // the fade's own duration instead.
94
+ minPressDuration: local.minPressDuration ?? TOUCHABLE_MIN_PRESS_DURATION_MS,
95
+ schedule,
96
+ now: Date.now,
97
+ }, runtime, {
98
+ activate(event) {
99
+ setOpacityTo(local.activeOpacity ?? DEFAULT_ACTIVE_OPACITY, OPACITY_ACTIVE_GRANT_DURATION_MS);
100
+ local.onPressIn?.(event);
101
+ },
102
+ deactivate(event) {
103
+ setOpacityTo(restingOpacity(), OPACITY_INACTIVE_DURATION_MS);
104
+ local.onPressOut?.(event);
105
+ },
106
+ }));
107
+ // RN's componentDidUpdate: a changed `disabled` or a changed style opacity re-settles the view,
108
+ // so a Touchable disabled mid-press does not stay stuck at its active opacity. Skipped on the
109
+ // first run — RN does this on UPDATE only, and firing at mount would animate over the initial
110
+ // value the Animated.Value already holds.
111
+ createEffect(previous => {
112
+ const current = { disabled: rest.disabled, resting: restingOpacity() };
113
+ if (previous !== undefined &&
114
+ (previous.disabled !== current.disabled ||
115
+ previous.resting !== current.resting)) {
116
+ setOpacityTo(current.resting, OPACITY_INACTIVE_DURATION_MS);
117
+ }
118
+ return current;
119
+ }, undefined);
120
+ // RN's componentWillUnmount: stop the animation and drop the value back, so a teardown mid-fade
121
+ // leaves no driver running against a node that is gone.
122
+ onCleanup(() => {
123
+ opacity.resetAnimation();
124
+ });
125
+ // Depends on `style` alone. `opacity` is a stable graph node, so a press changes what the node
126
+ // HOLDS, never this array — the per-frame path is value.setValue -> AnimatedProps.update ->
127
+ // setNativeProps, which never re-enters this component.
128
+ const feedbackStyle = createMemo(() => [local.style, { opacity }]);
129
+ // A spread followed by explicit props is safe here only because neither override can be
130
+ // `undefined` — mergeProps takes the first NON-undefined value scanning back-to-front, so an
131
+ // optional override after a spread would silently lose (.claude/rules/solid-descriptor-bridge.md
132
+ // §6). handlers() always returns both functions.
133
+ return (<Pressable {...rest} onPressIn={handlers().handlePressIn} onPressOut={handlers().handlePressOut}>
134
+ <Animated.View style={feedbackStyle()} class={local.class}>
135
+ {local.children}
136
+ </Animated.View>
137
+ </Pressable>);
138
+ }
139
+ // onPress/onPressIn/onPressOut are INTERCEPTED, not forwarded: RN drives the underlay from those
140
+ // three callbacks and then calls the caller's. onLongPress stays in `rest` (Pressable fires it
141
+ // directly) and is only READ here, for the has-press-handler gate.
142
+ const TOUCHABLE_HIGHLIGHT_HANDLED = [
143
+ 'activeOpacity',
144
+ 'underlayColor',
145
+ 'style',
146
+ 'children',
147
+ 'onPress',
148
+ 'onPressIn',
149
+ 'onPressOut',
150
+ 'onShowUnderlay',
151
+ 'onHideUnderlay',
152
+ 'delayPressOut',
153
+ ];
154
+ export function TouchableHighlight(props) {
155
+ const [local, rest] = splitProps(props, TOUCHABLE_HIGHLIGHT_HANDLED);
156
+ // `shown` is NOT `pressed`: RN holds the underlay for delayPressOut past the tap so a fast press
157
+ // still flashes, and the pressed flag is already false by then.
158
+ const [shown, setShown] = createSignal(false);
159
+ const runtime = createHighlightUnderlayRuntime();
160
+ const schedule = createTimeoutScheduler();
161
+ const hasPressHandler = createMemo(() => hasTouchablePressHandler({
162
+ onPress: local.onPress,
163
+ onPressIn: local.onPressIn,
164
+ onPressOut: local.onPressOut,
165
+ onLongPress: rest.onLongPress,
166
+ }));
167
+ const underlay = createMemo(() => createHighlightUnderlayHandlers({
168
+ delayPressOut: local.delayPressOut ?? 0,
169
+ hasPressHandler: hasPressHandler(),
170
+ schedule,
171
+ }, runtime, {
172
+ setShown,
173
+ onShowUnderlay: () => local.onShowUnderlay?.(),
174
+ onHideUnderlay: () => local.onHideUnderlay?.(),
175
+ }));
176
+ const extraStyles = createMemo(() => resolveHighlightExtraStyles({
177
+ shown: shown(),
178
+ hasPressHandler: hasPressHandler(),
179
+ underlayColor: local.underlayColor,
180
+ activeOpacity: local.activeOpacity,
181
+ }));
182
+ // POINT 7 IS DELIBERATELY NOT FIXED HERE, and this is the whole of the divergence from RN.
183
+ // RN clones its single child with `extra.child` so the lowered opacity lands on the CHILD while
184
+ // the container keeps an opaque underlay. Solid has no cloneElement, so the only way to reach the
185
+ // child is a permanent wrapper view — which inserts a node into the flex chain between this
186
+ // container and the children, silently re-parenting any `flex` a child declares. The fake Fabric
187
+ // runs no Yoga, so no headless test can measure that damage, only assert the extra node exists.
188
+ // Shipping an unmeasurable layout change is worse than the visual approximation, so both styles
189
+ // stay on the container (the pre-audit behavior) and the child style is folded in last.
190
+ // resolveHighlightExtraStyles keeps them SEPARATE regardless, so React's cloneElement path and a
191
+ // future Solid fix are both unblocked. A layout-safe fix needs a real device check, not a test.
192
+ const containerStyle = createMemo(() => {
193
+ const extra = extraStyles();
194
+ if (extra === undefined)
195
+ return local.style;
196
+ return [local.style, extra.underlay, extra.child];
197
+ });
198
+ // Visual first, then the caller's callback — RN's order in _createPressabilityConfig.
199
+ const handlePressIn = (event) => {
200
+ underlay().handlePressIn(event);
201
+ local.onPressIn?.(event);
202
+ };
203
+ const handlePress = (event) => {
204
+ underlay().handlePress(event);
205
+ local.onPress?.(event);
206
+ };
207
+ const handlePressOut = (event) => {
208
+ underlay().handlePressOut(event);
209
+ local.onPressOut?.(event);
210
+ };
211
+ return (<Pressable {...rest} style={containerStyle()} onPress={handlePress} onPressIn={handlePressIn} onPressOut={handlePressOut}>
212
+ {local.children}
213
+ </Pressable>);
214
+ }
215
+ const TOUCHABLE_WITHOUT_FEEDBACK_HANDLED = [
216
+ 'children',
217
+ 'onPressIn',
218
+ 'onPressOut',
219
+ 'delayPressIn',
220
+ 'delayPressOut',
221
+ 'minPressDuration',
222
+ ];
223
+ export function TouchableWithoutFeedback(props) {
224
+ const [local, rest] = splitProps(props, TOUCHABLE_WITHOUT_FEEDBACK_HANDLED);
225
+ // RN's TouchableWithoutFeedback builds a FULL Pressability config with delayPressIn /
226
+ // delayPressOut / minPressDuration: 0 — "without feedback" means no VISUAL, not no timing. The
227
+ // same shared machine TouchableOpacity uses runs here with the visual half left empty.
228
+ const runtime = createTouchableFeedbackRuntime();
229
+ const schedule = createTimeoutScheduler();
230
+ const handlers = createMemo(() => createTouchableFeedbackHandlers({
231
+ delayPressIn: local.delayPressIn ?? 0,
232
+ delayPressOut: local.delayPressOut ?? 0,
233
+ minPressDuration: local.minPressDuration ?? TOUCHABLE_MIN_PRESS_DURATION_MS,
234
+ schedule,
235
+ now: Date.now,
236
+ }, runtime, {
237
+ activate(event) {
238
+ local.onPressIn?.(event);
239
+ },
240
+ deactivate(event) {
241
+ local.onPressOut?.(event);
242
+ },
243
+ }));
244
+ return (<Pressable {...rest} onPressIn={handlers().handlePressIn} onPressOut={handlers().handlePressOut}>
245
+ {local.children}
246
+ </Pressable>);
247
+ }
@@ -0,0 +1,16 @@
1
+ import type { JSX } from '../../jsx-runtime';
2
+ import { canUseNativeForeground, rippleBackground, selectableBackground, selectableBackgroundBorderless, type INativeFeedbackBackground } from '@symbiote-native/components';
3
+ import { type IPressableProps } from '../pressable';
4
+ export type { INativeFeedbackBackground, IThemeAttrBackground, IRippleBackground, } from '@symbiote-native/components';
5
+ export interface ITouchableNativeFeedbackProps extends Omit<IPressableProps, 'style' | 'children'> {
6
+ background?: INativeFeedbackBackground;
7
+ useForeground?: boolean;
8
+ children?: JSX.Element;
9
+ }
10
+ declare function TouchableNativeFeedbackImpl(props: ITouchableNativeFeedbackProps): JSX.Element;
11
+ export declare const TouchableNativeFeedback: typeof TouchableNativeFeedbackImpl & {
12
+ SelectableBackground: typeof selectableBackground;
13
+ SelectableBackgroundBorderless: typeof selectableBackgroundBorderless;
14
+ Ripple: typeof rippleBackground;
15
+ canUseNativeForeground: typeof canUseNativeForeground;
16
+ };
@@ -0,0 +1,68 @@
1
+ // TouchableNativeFeedback — Android's ripple / state-drawable touchable, built on Pressable like
2
+ // the rest of the family. RN clones its child into an RCTView carrying the native ripple props;
3
+ // we nest the child under a feedback view that carries them, inside a Pressable that owns the
4
+ // press wiring. `nativeBackgroundAndroid` / `nativeForegroundAndroid` are read by Android's
5
+ // ReactViewManager; on iOS they are inert props, so the component still renders its child with
6
+ // working press wiring — exactly what React's and Vue's do.
7
+ //
8
+ // EVERYTHING NON-LIFECYCLE IS SHARED. The static factories (selectableBackground,
9
+ // selectableBackgroundBorderless, rippleBackground), the platform gate (canUseNativeForeground)
10
+ // and the background→native-prop mapping (backgroundProps) all live in
11
+ // @symbiote-native/components; this file only attaches them to the component value and nests the
12
+ // feedback view. Press timing is Pressable's, which is the shared state machine.
13
+ //
14
+ // The feedback carrier is a raw `symbiote-view`, not the `View` component — the same choice
15
+ // Pressable already makes for its android_ripple wrapper. It carries native props and nothing
16
+ // else: no accessibility surface, no class, no ref, so View's aria fold and nativeID resolution
17
+ // would be pure overhead. The committed node is `RCTView` either way.
18
+ //
19
+ // NOTHING here destructures `props`. A Solid component body runs ONCE, so a destructure would
20
+ // freeze the background at its mount-time value; every read below sits inside an accessor.
21
+ import { splitProps } from 'solid-js';
22
+ import { backgroundProps, canUseNativeForeground, rippleBackground, selectableBackground, selectableBackgroundBorderless, } from '@symbiote-native/components';
23
+ import { dlog } from '@symbiote-native/engine';
24
+ import { withStableKeys } from '../../utils/stable-keys.js';
25
+ import { TouchablePressable as Pressable, } from '../pressable.jsx';
26
+ function TouchableNativeFeedbackImpl(props) {
27
+ const [local, rest] = splitProps(props, [
28
+ 'background',
29
+ 'useForeground',
30
+ 'children',
31
+ ]);
32
+ // ONE bag, and it goes through withStableKeys. backgroundProps returns a DIFFERENT single key
33
+ // per branch (nativeForegroundAndroid vs nativeBackgroundAndroid), and Solid's `spread` walks
34
+ // only the current key set with no removal pass — so flipping `useForeground` on an Android
35
+ // host would leave the abandoned key painting its old drawable forever. Widening restores what
36
+ // React and Vue get from their reconcilers: the vanished key arrives as `undefined`, which
37
+ // routeProp treats as a delete, and `spread` still does the actual diffing
38
+ // (.claude/rules/solid-descriptor-bridge.md §1).
39
+ const feedbackProps = withStableKeys(() => {
40
+ // RN never leaves a TouchableNativeFeedback without feedback: a missing background defaults
41
+ // to SelectableBackground().
42
+ const resolved = local.background ?? selectableBackground();
43
+ const useForeground = local.useForeground === true;
44
+ dlog(`TouchableNativeFeedback render ${resolved.type} useForeground ${useForeground}`);
45
+ // The no-op seam: RN silently routes a requested foreground back to the background slot off
46
+ // Android / below API 23, and a missing ripple on device looks like a broken component.
47
+ if (useForeground && !canUseNativeForeground()) {
48
+ dlog('TouchableNativeFeedback useForeground ignored: no native foreground here');
49
+ }
50
+ return backgroundProps(resolved, useForeground);
51
+ });
52
+ // Two single spreads, never spread-then-override: `mergeProps` (which the compiler uses for a
53
+ // spread followed by an explicit prop) keeps the FIRST non-undefined value, so an explicit
54
+ // `undefined` after a spread silently loses — the opposite of React's JSX spread
55
+ // (.claude/rules/solid-descriptor-bridge.md §6). `rest` and `feedbackProps()` are each the sole
56
+ // source for their element, so there is nothing to merge.
57
+ return (<Pressable {...rest}>
58
+ <symbiote-view {...feedbackProps()}>{local.children}</symbiote-view>
59
+ </Pressable>);
60
+ }
61
+ // The statics ride on the component value so callers reach TouchableNativeFeedback.Ripple(…)
62
+ // exactly like RN. They are the shared functions themselves, not wrappers.
63
+ export const TouchableNativeFeedback = Object.assign(TouchableNativeFeedbackImpl, {
64
+ SelectableBackground: selectableBackground,
65
+ SelectableBackgroundBorderless: selectableBackgroundBorderless,
66
+ Ripple: rippleBackground,
67
+ canUseNativeForeground,
68
+ });
@@ -0,0 +1,32 @@
1
+ import { type Ref } from 'solid-js';
2
+ import type { JSX } from '../jsx-runtime';
3
+ import { type IAccessibilityProps, type IAriaProps, type IResponderProps } from '@symbiote-native/components';
4
+ import type { IClassNameValue, IStyleProp, ISymbioteEvent, IViewStyle } from '@symbiote-native/engine';
5
+ import type { IHostInstance } from '../host-instance';
6
+ export interface IViewProps extends IAccessibilityProps, IAriaProps, IResponderProps {
7
+ style?: IStyleProp<IViewStyle>;
8
+ class?: IClassNameValue;
9
+ onPress?: (event: ISymbioteEvent) => void;
10
+ onPressIn?: (event: ISymbioteEvent) => void;
11
+ onPressOut?: (event: ISymbioteEvent) => void;
12
+ onLayout?: (event: ISymbioteEvent) => void;
13
+ onFocus?: (event: ISymbioteEvent) => void;
14
+ onBlur?: (event: ISymbioteEvent) => void;
15
+ pointerEvents?: 'auto' | 'none' | 'box-none' | 'box-only';
16
+ hitSlop?: number | {
17
+ top?: number;
18
+ left?: number;
19
+ bottom?: number;
20
+ right?: number;
21
+ };
22
+ id?: string;
23
+ focusable?: boolean;
24
+ collapsable?: boolean;
25
+ removeClippedSubviews?: boolean;
26
+ renderToHardwareTextureAndroid?: boolean;
27
+ shouldRasterizeIOS?: boolean;
28
+ needsOffscreenAlphaCompositing?: boolean;
29
+ ref?: Ref<IHostInstance>;
30
+ children?: JSX.Element;
31
+ }
32
+ export declare function View(props: IViewProps): JSX.Element;
@@ -0,0 +1,49 @@
1
+ // View — the Solid host primitive, and the file that fixes the children/ref idiom every other
2
+ // Solid component will copy.
3
+ //
4
+ // WHY THIS IS REAL JSX AND NOT A HAND-BUILT NODE. `symbiote-view` takes a live subtree of the
5
+ // user's components, and reducing that subtree to primitives IS Solid's reconciler. So the only
6
+ // two moving parts are (a) forwarding a prop bag onto one host element and (b) handing the
7
+ // children accessor to the renderer's `insert` — precisely what compiled JSX emits
8
+ // (`spread(el, mergeProps(bag), true)` + `insert(el, () => props.children)`, verified against
9
+ // babel-preset-solid's output). Writing those calls by hand would be the compiler's job done
10
+ // worse, and this is the file a Solid author reads first.
11
+ //
12
+ // There is NO renderView() in @symbiote-native/components, and there must not be
13
+ // (.claude/rules/component-render-fn-boundary.md). A render fn moves to core only when every
14
+ // input is a framework-agnostic VALUE; `children` is a live framework subtree that flows through
15
+ // the reconciler and is never converted back into a Descriptor. So descriptorToSolid has nothing
16
+ // to do here — View builds its host element directly, exactly as React's and Vue's do.
17
+ //
18
+ // NOTHING here destructures `props` at setup. Solid props are getters and a component body runs
19
+ // ONCE; splitProps is the idiomatic split that keeps the rest reactive, and every read of the
20
+ // remaining props happens inside the bag accessor, which `spread` re-runs as a render effect.
21
+ import { splitProps } from 'solid-js';
22
+ import { resolveAccessibilityProps, } from '@symbiote-native/components';
23
+ import { applyHostRef } from '../utils/host-ref.js';
24
+ import { withStableKeys } from '../utils/stable-keys.js';
25
+ export function View(props) {
26
+ // `children` and `ref` are pulled out rather than left to ride in the bag. Both are skipped by
27
+ // spreadExpression's prop loop anyway, but leaving them in would make the bag accessor depend on
28
+ // the children subtree (recomputing every prop when only a child changed) and would put `ref`
29
+ // behind spread's own ref effect, which re-runs — and so re-calls the caller's ref — on EVERY
30
+ // prop change. React calls a ref once per attach; this keeps that contract.
31
+ const [local, rest] = splitProps(props, ['children', 'ref', 'id']);
32
+ // RN's modern `id` is a W3C-named alias for `nativeID`: View.js copies it over
33
+ // (`processedProps.nativeID = id`), so `id` wins when both are set. Folded here, with the raw
34
+ // `id` key dropped so it never reaches Fabric — every non-function prop otherwise passes
35
+ // through to the slot untouched. `nativeID` is emitted UNCONDITIONALLY (undefined when neither
36
+ // is set) rather than conditionally like React's resolveId: a key that appears and disappears
37
+ // between runs is the exact hazard withStableKeys exists for, and not creating it is cheaper
38
+ // than widening it back.
39
+ const bag = withStableKeys(() => ({
40
+ ...resolveAccessibilityProps(rest),
41
+ nativeID: local.id ?? rest.nativeID,
42
+ }));
43
+ const attachRef = (node) => {
44
+ applyHostRef(local.ref, node);
45
+ };
46
+ return (<symbiote-view ref={attachRef} {...bag()}>
47
+ {local.children}
48
+ </symbiote-view>);
49
+ }
@@ -0,0 +1,2 @@
1
+ export type { IVirtualizedListProps, IVirtualizedListComponent, IVirtualizedListCellInfo, IVirtualizedListRenderItem, IVirtualizedListHandle, ICellLayout, ISeparators, ISeparatorProps, IViewToken, IViewableItemsChangedInfo, IViewabilityConfig, IViewabilityConfigCallbackPair, } from './shared';
2
+ export declare const VirtualizedList: import("./shared").IVirtualizedListComponent;
@@ -0,0 +1,9 @@
1
+ // VirtualizedList on Android. An Android ScrollView accepts only ONE child, so a RefreshControl
2
+ // cannot sit beside the content the way iOS allows ("addViewAt: failed to insert view … at index 1").
3
+ // Instead the RefreshControl (AndroidSwipeRefreshLayout) WRAPS the scroll host, which nests inside it
4
+ // so the inner scroll takes the gesture before the refresh parent — RN's own ScrollView.js android
5
+ // branch. Metro picks this file on an Android host; no Platform.OS read.
6
+ import { createVirtualizedList } from './shared.jsx';
7
+ export const VirtualizedList = createVirtualizedList({
8
+ refreshControlMode: 'wrap',
9
+ });
@@ -0,0 +1 @@
1
+ export * from './index.ios';
@@ -0,0 +1,2 @@
1
+ export type { IVirtualizedListProps, IVirtualizedListComponent, IVirtualizedListCellInfo, IVirtualizedListRenderItem, IVirtualizedListHandle, ICellLayout, ISeparators, ISeparatorProps, IViewToken, IViewableItemsChangedInfo, IViewabilityConfig, IViewabilityConfigCallbackPair, } from './shared';
2
+ export declare const VirtualizedList: import("./shared").IVirtualizedListComponent;
@@ -0,0 +1,9 @@
1
+ // VirtualizedList on iOS. The RefreshControl (PullToRefreshView) is a CHILD of the scroll host,
2
+ // rendered as a childless SIBLING BEFORE the content container — RN ScrollView.js's
3
+ // `{refreshControl}{contentContainer}`, the same fact this adapter's ScrollView and adapters/react's,
4
+ // adapters/vue's and adapters/svelte's iOS bindings encode. Metro picks this file on an iOS host; no
5
+ // Platform.OS read.
6
+ import { createVirtualizedList } from './shared.jsx';
7
+ export const VirtualizedList = createVirtualizedList({
8
+ refreshControlMode: 'sibling',
9
+ });
@@ -0,0 +1,4 @@
1
+ // Base / default VirtualizedList: re-exports the iOS build. Metro overrides this with index.ios.ts /
2
+ // index.android.ts on a real host; under vitest / tsc the resolution lands here. Filename is the
3
+ // selector, no Platform.OS read — the same shape scroll-view/index.ts has.
4
+ export * from './index.ios.js';
@@ -0,0 +1,75 @@
1
+ import { type Accessor, type Ref } from 'solid-js';
2
+ import type { JSX } from '../../jsx-runtime';
3
+ import { type IAccessibilityProps, type IAriaProps, type ICellLayout, type IViewabilityConfig, type IViewabilityConfigCallbackPair, type IViewableItemsChangedInfo, type ISeparatorProps, type ISeparators, type IViewToken, type IVirtualizedListHandle } from '@symbiote-native/components';
4
+ import { type IClassNameValue, type IStyleProp, type ISymbioteEvent, type IViewStyle } from '@symbiote-native/engine';
5
+ import type { IScrollViewHostPlatform } from '../scroll-view/shared';
6
+ export type { IScrollViewHostPlatform };
7
+ export type { ICellLayout, ISeparators, ISeparatorProps, IViewToken, IViewableItemsChangedInfo, IViewabilityConfig, IViewabilityConfigCallbackPair, IVirtualizedListHandle, };
8
+ export interface IVirtualizedListCellInfo<ItemT> {
9
+ item: ItemT;
10
+ index: number;
11
+ separators: ISeparators;
12
+ }
13
+ export type IVirtualizedListRenderItem<ItemT> = (info: Accessor<IVirtualizedListCellInfo<ItemT>>) => JSX.Element;
14
+ export interface IVirtualizedListProps<ItemT> extends IAccessibilityProps, IAriaProps {
15
+ data: unknown;
16
+ getItem: (data: unknown, index: number) => ItemT;
17
+ getItemCount: (data: unknown) => number;
18
+ renderItem: IVirtualizedListRenderItem<ItemT>;
19
+ getItemLayout?: (data: unknown, index: number) => {
20
+ length: number;
21
+ offset: number;
22
+ index: number;
23
+ };
24
+ initialNumToRender?: number;
25
+ windowSize?: number;
26
+ ListHeaderComponent?: JSX.Element;
27
+ ListFooterComponent?: JSX.Element;
28
+ ListEmptyComponent?: JSX.Element;
29
+ ItemSeparatorComponent?: (props: ISeparatorProps<ItemT>) => JSX.Element;
30
+ ref?: Ref<IVirtualizedListHandle>;
31
+ onEndReached?: (info: {
32
+ distanceFromEnd: number;
33
+ }) => void;
34
+ onEndReachedThreshold?: number;
35
+ onStartReached?: (info: {
36
+ distanceFromStart: number;
37
+ }) => void;
38
+ onStartReachedThreshold?: number;
39
+ keyExtractor?: (item: ItemT, index: number) => string;
40
+ onViewableItemsChanged?: (info: IViewableItemsChangedInfo<ItemT>) => void;
41
+ viewabilityConfig?: IViewabilityConfig;
42
+ viewabilityConfigCallbackPairs?: IViewabilityConfigCallbackPair<ItemT>[];
43
+ onScroll?: (event: ISymbioteEvent) => void;
44
+ onScrollBeginDrag?: (event: ISymbioteEvent) => void;
45
+ onScrollEndDrag?: (event: ISymbioteEvent) => void;
46
+ onMomentumScrollBegin?: (event: ISymbioteEvent) => void;
47
+ onMomentumScrollEnd?: (event: ISymbioteEvent) => void;
48
+ scrollEventThrottle?: number;
49
+ keyboardShouldPersistTaps?: boolean | 'always' | 'never' | 'handled';
50
+ keyboardDismissMode?: 'none' | 'on-drag' | 'interactive';
51
+ horizontal?: boolean;
52
+ inverted?: boolean;
53
+ style?: IStyleProp<IViewStyle>;
54
+ contentContainerStyle?: IStyleProp<IViewStyle> | string;
55
+ class?: IClassNameValue;
56
+ onRefresh?: () => void;
57
+ refreshing?: boolean | null;
58
+ progressViewOffset?: number;
59
+ stickyHeaderIndices?: number[];
60
+ maintainVisibleContentPosition?: {
61
+ minIndexForVisible: number;
62
+ autoscrollToTopThreshold?: number;
63
+ };
64
+ initialScrollIndex?: number;
65
+ maxToRenderPerBatch?: number;
66
+ updateCellsBatchingPeriod?: number;
67
+ extraData?: unknown;
68
+ onScrollToIndexFailed?: (info: {
69
+ index: number;
70
+ highestMeasuredFrameIndex: number;
71
+ averageItemLength: number;
72
+ }) => void;
73
+ }
74
+ export type IVirtualizedListComponent = <ItemT>(props: IVirtualizedListProps<ItemT>) => JSX.Element;
75
+ export declare function createVirtualizedList(platform: IScrollViewHostPlatform): IVirtualizedListComponent;