@symbiote-native/solid 1.0.0 → 2.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 (106) hide show
  1. package/README.md +9 -10
  2. package/babel-preset.cjs +0 -5
  3. package/build/components/activity-indicator-props.d.ts +5 -0
  4. package/build/components/activity-indicator-props.js +1 -0
  5. package/build/components/button-props.d.ts +5 -0
  6. package/build/components/button-props.js +1 -0
  7. package/build/components/flat-list/index.jsx +3 -3
  8. package/build/components/{image-background.d.ts → image-background-props.d.ts} +2 -3
  9. package/build/components/image-background-props.js +1 -0
  10. package/build/components/image-props.d.ts +5 -0
  11. package/build/components/image-props.js +6 -0
  12. package/build/components/{input-accessory-view.d.ts → input-accessory-view-props.d.ts} +1 -2
  13. package/build/components/input-accessory-view-props.js +5 -0
  14. package/build/components/keyboard-avoiding-view.jsx +9 -4
  15. package/build/components/modal.jsx +6 -6
  16. package/build/components/{pressable.d.ts → pressable-props.d.ts} +8 -7
  17. package/build/components/pressable-props.js +13 -0
  18. package/build/components/{refresh-control.d.ts → refresh-control-props.d.ts} +3 -3
  19. package/build/components/refresh-control-props.js +7 -0
  20. package/build/components/safe-area-view-props.d.ts +10 -0
  21. package/build/components/safe-area-view-props.js +5 -0
  22. package/build/components/{scroll-view/shared.d.ts → scroll-view-props.d.ts} +7 -13
  23. package/build/components/scroll-view-props.js +29 -0
  24. package/build/components/switch-props.d.ts +6 -0
  25. package/build/components/switch-props.js +5 -0
  26. package/build/components/text-input-props.d.ts +8 -0
  27. package/build/components/text-input-props.js +1 -0
  28. package/build/components/{text.d.ts → text-props.d.ts} +3 -3
  29. package/build/components/text-props.js +11 -0
  30. package/build/components/touchable-native-feedback/touchable-native-feedback-props.d.ts +8 -0
  31. package/build/components/touchable-native-feedback/touchable-native-feedback-props.js +1 -0
  32. package/build/components/touchable-props.d.ts +18 -0
  33. package/build/components/touchable-props.js +1 -0
  34. package/build/components/touchable-without-feedback/touchable-without-feedback-props.d.ts +6 -0
  35. package/build/components/touchable-without-feedback/touchable-without-feedback-props.js +1 -0
  36. package/build/components/{view.d.ts → view-props.d.ts} +2 -3
  37. package/build/components/view-props.js +11 -0
  38. package/build/components/virtualized-list/index.android.js +5 -4
  39. package/build/components/virtualized-list/index.ios.js +6 -6
  40. package/build/components/virtualized-list/shared.d.ts +1 -3
  41. package/build/components/virtualized-list/shared.jsx +87 -98
  42. package/build/components/virtualized-list/sticky-header.d.ts +6 -0
  43. package/build/components/{scroll-view → virtualized-list}/sticky-header.jsx +28 -36
  44. package/build/components.d.ts +20 -30
  45. package/build/components.js +6 -33
  46. package/build/create-portal/index.jsx +1 -1
  47. package/build/descriptor-to-solid.js +1 -1
  48. package/build/index.d.ts +1 -0
  49. package/build/index.js +11 -3
  50. package/build/jsx-runtime.d.ts +39 -2
  51. package/build/modules/animated/index.d.ts +0 -4
  52. package/build/modules/animated/index.js +16 -22
  53. package/build/modules/image.d.ts +3 -0
  54. package/build/modules/image.js +12 -0
  55. package/build/register.d.ts +1 -1
  56. package/build/register.js +12 -28
  57. package/build/renderer.js +79 -16
  58. package/build/utils/stable-keys.d.ts +1 -1
  59. package/build/utils/stable-keys.js +6 -5
  60. package/metro-css-parser.cjs +2 -1
  61. package/package.json +5 -10
  62. package/babel-lower-host-primitives.cjs +0 -323
  63. package/build/components/activity-indicator/index.android.d.ts +0 -2
  64. package/build/components/activity-indicator/index.android.js +0 -11
  65. package/build/components/activity-indicator/index.d.ts +0 -1
  66. package/build/components/activity-indicator/index.ios.d.ts +0 -2
  67. package/build/components/activity-indicator/index.ios.js +0 -10
  68. package/build/components/activity-indicator/index.js +0 -4
  69. package/build/components/activity-indicator/shared.d.ts +0 -8
  70. package/build/components/activity-indicator/shared.js +0 -51
  71. package/build/components/button.d.ts +0 -7
  72. package/build/components/button.jsx +0 -45
  73. package/build/components/image-background.jsx +0 -74
  74. package/build/components/image.d.ts +0 -9
  75. package/build/components/image.js +0 -75
  76. package/build/components/input-accessory-view.jsx +0 -39
  77. package/build/components/pressable.jsx +0 -211
  78. package/build/components/refresh-control.jsx +0 -36
  79. package/build/components/safe-area-view.d.ts +0 -11
  80. package/build/components/safe-area-view.jsx +0 -24
  81. package/build/components/scroll-view/index.android.d.ts +0 -2
  82. package/build/components/scroll-view/index.android.js +0 -11
  83. package/build/components/scroll-view/index.d.ts +0 -2
  84. package/build/components/scroll-view/index.ios.d.ts +0 -2
  85. package/build/components/scroll-view/index.ios.js +0 -8
  86. package/build/components/scroll-view/index.js +0 -9
  87. package/build/components/scroll-view/shared.jsx +0 -334
  88. package/build/components/scroll-view/sticky-header.d.ts +0 -18
  89. package/build/components/switch/index.android.d.ts +0 -2
  90. package/build/components/switch/index.android.js +0 -12
  91. package/build/components/switch/index.d.ts +0 -1
  92. package/build/components/switch/index.ios.d.ts +0 -2
  93. package/build/components/switch/index.ios.js +0 -11
  94. package/build/components/switch/index.js +0 -4
  95. package/build/components/switch/shared.d.ts +0 -11
  96. package/build/components/switch/shared.js +0 -87
  97. package/build/components/text-input.d.ts +0 -10
  98. package/build/components/text-input.js +0 -290
  99. package/build/components/text.jsx +0 -27
  100. package/build/components/touchable/index.d.ts +0 -22
  101. package/build/components/touchable/index.jsx +0 -247
  102. package/build/components/touchable-native-feedback/index.d.ts +0 -16
  103. package/build/components/touchable-native-feedback/index.jsx +0 -68
  104. package/build/components/view.jsx +0 -49
  105. package/build/state-style.d.ts +0 -1
  106. package/build/state-style.js +0 -13
@@ -1,334 +0,0 @@
1
- // ScrollView — the Solid lifecycle half. The Fabric tree is nested: a scroll view wraps a content
2
- // view that holds the children (RN's own ScrollView.js shape). Everything platform- and
3
- // framework-invariant already exists in @symbiote-native/components and is CALLED, never re-derived:
4
- // `selectScrollIntrinsics` (per-axis tags + base styles), `resolveDecelerationRate`,
5
- // `resolveScrollForwarding` (which onScroll path, the 1/16 throttle defaults, the inverted viewport
6
- // capture, collapsableChildren), `didContentSizeChange`, `buildScrollViewHandle`,
7
- // `splitLayoutProps`, `attachStickyScroll`, `forwardScrollEvent`, `resolveAccessibilityProps`.
8
- // Solid supplies signals, effects and the element assembly.
9
- //
10
- // THREE THINGS ARE SOLID-SPECIFIC AND NONE OF THEM IS COSMETIC.
11
- //
12
- // 1. There is no reconciler between what this file returns and the host nodes. React diffs fibers,
13
- // Vue diffs vnodes, Svelte patches in place; Solid's `insert` REPLACES. So the tree is built ONCE
14
- // and every prop rides a `spread` render effect on the SAME node — the identity
15
- // dispatchViewCommand, the commit mirror and native-owned scroll offset all key on. The only
16
- // rebuild is when the host TAG itself has to change (see `treeShape` below).
17
- // 2. A JSX element prop is a GETTER that CREATES the element on read, so `refreshControl` is read
18
- // exactly once. A second read would build a second refresh-control node.
19
- // 3. The commit is microtask-coalesced (`requestCommit`), so at mount time the scroll node has no
20
- // Fabric tag. Anything native wired then — here, the sticky scroll attach — goes through
21
- // `whenCommitted` or it silently no-ops with no retry. The imperative handle is safe without it
22
- // for a different reason: `buildScrollViewHandle` takes a LAZY node getter and every command
23
- // re-reads it, so a handle handed out at setup keeps working once the node commits.
24
- //
25
- // NOTHING here destructures `props`. Solid props are getters and a component body runs ONCE;
26
- // `splitProps` is the idiomatic split that keeps the rest reactive, and every read below sits inside
27
- // an accessor, a memo, or an event handler.
28
- import { children, createEffect, createMemo, createRenderEffect, createSignal, on, onCleanup, splitProps, untrack, } from 'solid-js';
29
- import { attachStickyScroll, buildScrollViewHandle, didContentSizeChange, forwardScrollEvent, nextStickyHeaderY, readLayoutDimension, resolveAccessibilityProps, resolveDecelerationRate, resolveScrollForwarding, selectScrollIntrinsics, splitLayoutProps, } from '@symbiote-native/components';
30
- import { AnimatedValue, dlog, event as animatedEvent, isNativeAnimatedAvailable, isSymbioteNode, resolveClassName, whenCommitted, } from '@symbiote-native/engine';
31
- import { createElement, insert, insertNode, setProp, spread, } from '../../renderer.js';
32
- import { withStableKeys } from '../../utils/stable-keys.js';
33
- import { wrapStickyHeaders, } from './sticky-header.jsx';
34
- // Consumed by the lifecycle itself; everything else forwards onto the scroll-view host node.
35
- // onContentSizeChange is synthesized from the content view's onLayout and must never reach Fabric —
36
- // a function prop crashes Android's folly::dynamic serializer. Same for the sticky props, which
37
- // native ignores, and `refreshControl`, which is an element.
38
- const HANDLED_PROPS = [
39
- 'style',
40
- 'class',
41
- 'contentContainerStyle',
42
- 'horizontal',
43
- 'decelerationRate',
44
- 'refreshControl',
45
- 'stickyHeaderIndices',
46
- 'invertStickyHeaders',
47
- 'StickyHeaderComponent',
48
- 'onContentSizeChange',
49
- 'onLayout',
50
- 'onScroll',
51
- 'scrollEventThrottle',
52
- 'children',
53
- 'ref',
54
- ];
55
- // `maintainVisibleContentPosition`, `snapToAlignment` and `nestedScrollEnabled` are deliberately NOT
56
- // in that list: the lifecycle READS them (for resolveScrollForwarding and RN's nested-scroll
57
- // default) while native also needs them, so they forward through `rest` untouched.
58
- function hostElement(tag) {
59
- const node = createElement(tag);
60
- // Narrowing, not defensive: the renderer types createElement over its IHostNode union (which
61
- // includes the surface), while everything below needs a real host node.
62
- if (!isSymbioteNode(node))
63
- throw new Error(`ScrollView: ${tag} did not create a host node`);
64
- return node;
65
- }
66
- export function createScrollView(platform) {
67
- return function ScrollView(props) {
68
- const [local, rest] = splitProps(props, HANDLED_PROPS);
69
- // The scroll-view host node, held by IDENTITY in a plain variable. A store or any proxy wrapper
70
- // would become a different key than the one the engine's commit mirror holds, and every
71
- // imperative command would silently no-op (symbiote-engine-core §3).
72
- let scrollNode = null;
73
- // A LAZY getter, not the node captured once: it is null until buildTree runs, and null again
74
- // under it if the tree is ever rebuilt.
75
- const handle = buildScrollViewHandle(() => scrollNode);
76
- // Read ONCE and untracked — see the module header, point 2. Nothing reactive is lost: a
77
- // RefreshControl's live state is `refreshing`, which is a prop of the element itself.
78
- const refreshControlElement = untrack(() => local.refreshControl);
79
- const refreshControlNode = isSymbioteNode(refreshControlElement)
80
- ? refreshControlElement
81
- : undefined;
82
- const isWrappingRefreshControl = platform.refreshControlMode === 'wrap' &&
83
- refreshControlNode !== undefined;
84
- // Drives every sticky header's translateY (RN's _scrollAnimatedValue). Allocated
85
- // unconditionally, exactly like React's unconditional hook, and held by identity.
86
- const scrollAnimatedValue = new AnimatedValue(0);
87
- // Inverted sticky headers stick to the BOTTOM, so they need the viewport height (RN
88
- // _handleLayout). Captured off the scroll view's own onLayout.
89
- const [viewportHeight, setViewportHeight] = createSignal(undefined);
90
- // Sticky-header cross-talk (RN's _headerLayoutYs): a child-index -> measured-y map so each
91
- // header learns where the NEXT one starts — its push-off collision point. Mutated imperatively
92
- // from each header's onLayout; the version bump is what lets the PREVIOUS header re-read it.
93
- const headerLayoutYs = new Map();
94
- const [headerLayoutVersion, setHeaderLayoutVersion] = createSignal(0);
95
- // RN fires the content onLayout on every layout pass; only a real size change emits.
96
- let lastContentSize = null;
97
- const isHorizontal = () => local.horizontal === true;
98
- const stickyHeaderIndices = createMemo(() => local.stickyHeaderIndices);
99
- const hasStickyHeaders = () => {
100
- const indices = stickyHeaderIndices();
101
- return indices !== undefined && indices.length > 0;
102
- };
103
- const nativeStickyAvailable = () => hasStickyHeaders() && isNativeAnimatedAvailable();
104
- // A class-name string resolves through the shared registry before it reaches the intrinsic
105
- // selector, which only understands style objects/arrays.
106
- const resolvedContentContainerStyle = () => {
107
- const style = local.contentContainerStyle;
108
- return typeof style === 'string' ? resolveClassName(style) : style;
109
- };
110
- const intrinsics = createMemo(() => selectScrollIntrinsics(isHorizontal(), resolvedContentContainerStyle()));
111
- // `class` is normally forwarded raw and resolved per-node at commit time, but the Android
112
- // RefreshControl wrap has to splitLayoutProps() BEFORE that, so a class-only layout prop
113
- // (flex, height, gap, …) would otherwise never reach the wrapper and it would collapse to
114
- // nothing.
115
- const layoutSplitStyle = () => [
116
- resolveClassName(local.class),
117
- local.style,
118
- ];
119
- const splitStyles = createMemo(() => splitLayoutProps(layoutSplitStyle()));
120
- const forwarding = createMemo(() => resolveScrollForwarding({
121
- hasStickyHeaders: hasStickyHeaders(),
122
- nativeStickyAvailable: nativeStickyAvailable(),
123
- invertStickyHeaders: local.invertStickyHeaders,
124
- scrollEventThrottle: local.scrollEventThrottle,
125
- maintainVisibleContentPosition: props.maintainVisibleContentPosition,
126
- snapToAlignment: props.snapToAlignment,
127
- }));
128
- // onScroll: the JS-fallback path wraps the user's handler in Animated.event so the offset drives
129
- // the AnimatedValue each frame (RN's _scrollAnimatedValueAttachment); the native and plain paths
130
- // forward it as-is, because the native driver attaches the value on the UI thread instead.
131
- const scrollHandler = createMemo(() => {
132
- const userOnScroll = local.onScroll;
133
- if (forwarding().mode !== 'sticky-js')
134
- return userOnScroll;
135
- return animatedEvent([{ nativeEvent: { contentOffset: { y: scrollAnimatedValue } } }], userOnScroll === undefined
136
- ? undefined
137
- : {
138
- listener: (...args) => forwardScrollEvent(userOnScroll, args),
139
- });
140
- });
141
- // onLayout on the scroll-view node: capture the viewport height for inverted sticky headers
142
- // (RN _handleLayout), then call the user's handler. Passed through unchanged otherwise, so a
143
- // ScrollView with neither never raises the onLayout flag prop and native never measures it.
144
- const scrollLayoutHandler = createMemo(() => {
145
- const userOnLayout = local.onLayout;
146
- if (!forwarding().capturesViewportHeight)
147
- return userOnLayout;
148
- return (event) => {
149
- const height = readLayoutDimension(event, 'height');
150
- if (height !== undefined)
151
- setViewportHeight(height);
152
- userOnLayout?.(event);
153
- };
154
- });
155
- const handleContentLayout = (event) => {
156
- const width = readLayoutDimension(event, 'width');
157
- const height = readLayoutDimension(event, 'height');
158
- if (width === undefined || height === undefined)
159
- return;
160
- if (!didContentSizeChange(lastContentSize, { width, height }))
161
- return;
162
- lastContentSize = { width, height };
163
- dlog(`Solid ScrollView contentSizeChange ${width}x${height}`);
164
- local.onContentSizeChange?.(width, height);
165
- };
166
- const onHeaderLayoutY = (index, y) => {
167
- if (headerLayoutYs.get(index) === y)
168
- return;
169
- headerLayoutYs.set(index, y);
170
- dlog(`Solid ScrollView sticky-header layoutY index=${index} y=${y}`);
171
- setHeaderLayoutVersion(tick => tick + 1);
172
- };
173
- const readNextHeaderLayoutY = (indexOfIndex) => {
174
- // Read the bump FIRST so this stays a dependency of whichever header calls it — that is what
175
- // turns "a later header measured" into "the previous header rebuilds its collision range".
176
- headerLayoutVersion();
177
- const indices = stickyHeaderIndices();
178
- if (indices === undefined)
179
- return undefined;
180
- return nextStickyHeaderY(indices, indexOfIndex, headerLayoutYs);
181
- };
182
- // withStableKeys on BOTH bags: resolveAccessibilityProps has two branches with different key
183
- // sets, and several keys below are conditional. Solid's `spread` walks only the CURRENT keys and
184
- // has no removal pass, so a vanished key would keep its last value on the native view forever
185
- // (.claude/rules/solid-descriptor-bridge.md §1).
186
- const outerBag = withStableKeys(() => {
187
- const bag = {
188
- ...resolveAccessibilityProps(rest),
189
- };
190
- // RN defaults nested scrolling ON (ScrollView.js `nestedScrollEnabled ?? true`): Android needs
191
- // it for a scrollable nested inside another to scroll independently; iOS handles nesting
192
- // natively. The Android RefreshControl wrap forces it, so the inner scroll view takes the
193
- // gesture before the refresh parent.
194
- bag.nestedScrollEnabled = isWrappingRefreshControl
195
- ? true
196
- : (props.nestedScrollEnabled ?? true);
197
- // Load-bearing on iOS (it flips RCTScrollView's axis), ignored by Android's dedicated
198
- // horizontal manager — so forward it whenever it is defined.
199
- if (local.horizontal !== undefined)
200
- bag.horizontal = local.horizontal;
201
- const rate = local.decelerationRate;
202
- if (rate !== undefined)
203
- bag.decelerationRate = resolveDecelerationRate(rate);
204
- // Base style UNDER the user style, so an explicit height / flexDirection still wins. Under the
205
- // Android wrap only the VISUAL half stays here; the LAYOUT half moved to the wrapper.
206
- bag.style = [
207
- intrinsics().scrollViewBaseStyle,
208
- isWrappingRefreshControl ? splitStyles().inner : local.style,
209
- ];
210
- // Stripped under the wrap: layoutSplitStyle already folded the resolved class into
211
- // outer/inner, so forwarding it raw too would re-apply its LAYOUT half a second time.
212
- if (!isWrappingRefreshControl)
213
- bag.class = local.class;
214
- bag.onScroll = scrollHandler();
215
- bag.onLayout = scrollLayoutHandler();
216
- bag.scrollEventThrottle = forwarding().scrollEventThrottle;
217
- return bag;
218
- });
219
- // `collapsable: false` is load-bearing on Android: the content container is a layout-only view
220
- // that Fabric would otherwise flatten away, hoisting the cells up as DIRECT children of the
221
- // scroll view — which hosts exactly one ("addViewAt" crash). iOS never flattens.
222
- const contentBag = withStableKeys(() => {
223
- const bag = {
224
- style: intrinsics().contentStyle,
225
- collapsable: false,
226
- };
227
- // maintainVisibleContentPosition (and Android snapToAlignment) anchor against the metrics of
228
- // MOUNTED cell views, so RN keeps them un-flattened too (ScrollView.js preserveChildren).
229
- if (forwarding().collapsableChildren)
230
- bag.collapsableChildren = false;
231
- if (local.onContentSizeChange !== undefined)
232
- bag.onLayout = handleContentLayout;
233
- return bag;
234
- });
235
- function mountChildren(content) {
236
- if (!hasStickyHeaders()) {
237
- // The plain path hands `insert` the raw children accessor, so its own nested-effect
238
- // machinery keeps updates fine-grained — a <For> inside re-runs only its own insert.
239
- insert(content, () => local.children);
240
- return;
241
- }
242
- // Sticky wrapping needs an INDEXABLE list, which is what solid's `children()` helper resolves
243
- // to. It costs a coarser update (any child change re-runs the whole wrap), which is why it is
244
- // created ONLY here and not for every ScrollView — and it must not coexist with the accessor
245
- // above, since reading `local.children` twice would build the subtree twice.
246
- const resolved = children(() => local.children);
247
- insert(content, () => wrapStickyHeaders(resolved.toArray(), {
248
- stickyHeaderIndices: stickyHeaderIndices() ?? [],
249
- scrollAnimatedValue,
250
- readInverted: () => local.invertStickyHeaders,
251
- readScrollViewHeight: viewportHeight,
252
- readNextHeaderLayoutY,
253
- StickyHeaderComponent: local.StickyHeaderComponent,
254
- onHeaderLayoutY,
255
- }));
256
- }
257
- function buildTree() {
258
- const { scrollViewIntrinsic, contentIntrinsic } = intrinsics();
259
- dlog(`Solid ScrollView -> ${scrollViewIntrinsic} (horizontal=${String(isHorizontal())} sticky=${String(hasStickyHeaders())})`);
260
- const content = hostElement(contentIntrinsic);
261
- spread(content, contentBag, true);
262
- mountChildren(content);
263
- const scroll = hostElement(scrollViewIntrinsic);
264
- scrollNode = scroll;
265
- spread(scroll, outerBag, true);
266
- // iOS: the RefreshControl is a childless SIBLING placed BEFORE the content container (RN
267
- // ScrollView.js: {refreshControl}{contentContainer}).
268
- if (refreshControlNode !== undefined && !isWrappingRefreshControl) {
269
- insertNode(scroll, refreshControlNode);
270
- }
271
- insertNode(scroll, content);
272
- if (refreshControlNode === undefined || !isWrappingRefreshControl)
273
- return scroll;
274
- // Android: the RefreshControl WRAPS the scroll view. React does this with cloneElement and Vue
275
- // by re-invoking the VNode's type; Solid has neither, because the element arrived already
276
- // built — so the scroll view is nested into the existing node and the outer style is written
277
- // onto it. A render effect, not a one-shot set, so a later style/class change still moves.
278
- //
279
- // This OVERRIDES a `style` the app put on the RefreshControl itself, exactly as React's
280
- // cloneElement({style}) does. Unlike React's, the two writers are independent render effects,
281
- // so an app that styles BOTH boxes on Android has no defined winner — pass the ScrollView's
282
- // layout through `style`/`class` there and leave the control's own `style` alone.
283
- createRenderEffect(() => {
284
- setProp(refreshControlNode, 'style', splitStyles().outer);
285
- });
286
- insertNode(refreshControlNode, scroll);
287
- return refreshControlNode;
288
- }
289
- // The scroll AXIS picks a different host TAG (horizontal is a separate ViewManager on Android)
290
- // and sticky headers pick a different children pipeline. Solid cannot swap a tag under a live
291
- // node, so either flip REBUILDS — which is exactly what React does when an element type changes.
292
- // `on()` runs buildTree untracked, so every other read inside it (style, class, handlers,
293
- // contentContainerStyle) re-props the SAME nodes through `spread` instead of rebuilding them.
294
- const treeShape = createMemo(() => `${String(isHorizontal())}:${String(hasStickyHeaders())}`);
295
- const tree = createMemo(on(treeShape, () => buildTree()));
296
- // Solid's `ref` is a COMPILE-TIME construct: by the time a component body reads `props.ref`, a
297
- // `ref={scroller}` call site has already been rewritten into a callback. The full rationale (and
298
- // why the declared type still has to be the `Ref<T>` union) is in utils/host-ref.ts, whose
299
- // helper is typed for a host node — this hands back the imperative handle instead.
300
- //
301
- // Called AFTER `tree` has built at least once, not right after `handle` is constructed.
302
- // `createAnimatedComponent`'s captureRef (resolveHostNode) reads handle.getScrollNode()
303
- // EAGERLY, once, at ref-call time - it never re-reads the lazy getter later. React's
304
- // useImperativeHandle and Vue's expose() fire only after the child's own mount work, so they
305
- // never saw a null handle; Solid's `ref` is called by the component itself and has to be
306
- // sequenced by hand. Calling it before `buildTree()` gave captureRef a permanently-null node,
307
- // so Animated.ScrollView's onScroll never native-attached - a scroll-linked header fade froze
308
- // at its initial value on this adapter only.
309
- if (typeof local.ref === 'function')
310
- local.ref(handle);
311
- // Drive the sticky scroll value on the native UI thread (RN's attachNativeEvent /
312
- // _updateAnimatedNodeAttachment), so the header interpolations ride scroll natively with no JS
313
- // jitter. `tree()` is read for its dependency: a rebuild means a NEW scroll node to attach to.
314
- createEffect(() => {
315
- tree();
316
- if (!nativeStickyAvailable())
317
- return;
318
- const node = scrollNode;
319
- if (node === null)
320
- return;
321
- let detach;
322
- // The engine commits on a microtask, so this node has no Fabric tag yet on the first run and
323
- // the attach would no-op with no retry. whenCommitted is that retry.
324
- const cancel = whenCommitted(node, () => {
325
- detach = attachStickyScroll(node, scrollAnimatedValue);
326
- });
327
- onCleanup(() => {
328
- cancel();
329
- detach?.();
330
- });
331
- });
332
- return tree;
333
- };
334
- }
@@ -1,18 +0,0 @@
1
- import type { JSX } from '../../jsx-runtime';
2
- import { type IStickyHeaderProps } from '@symbiote-native/components';
3
- import { type AnimatedValue } from '@symbiote-native/engine';
4
- export type IStickyHeaderComponentProps = IStickyHeaderProps & {
5
- children?: JSX.Element;
6
- };
7
- export type IStickyHeaderComponentType = (props: IStickyHeaderComponentProps) => JSX.Element;
8
- export declare function ScrollViewStickyHeader(props: IStickyHeaderComponentProps): JSX.Element;
9
- export interface IStickyWrapInputs {
10
- stickyHeaderIndices: number[];
11
- scrollAnimatedValue: AnimatedValue;
12
- readInverted: () => boolean | undefined;
13
- readScrollViewHeight: () => number | undefined;
14
- readNextHeaderLayoutY: (indexOfIndex: number) => number | undefined;
15
- StickyHeaderComponent: IStickyHeaderComponentType | undefined;
16
- onHeaderLayoutY: (index: number, y: number) => void;
17
- }
18
- export declare function wrapStickyHeaders(children: readonly JSX.Element[], inputs: IStickyWrapInputs): JSX.Element[];
@@ -1,2 +0,0 @@
1
- export type { ISwitchProps, ISwitchTrackColor } from './shared';
2
- export declare const Switch: (props: import("./shared").ISwitchProps) => import("@symbiote-native/solid/jsx-runtime").JSX.Element;
@@ -1,12 +0,0 @@
1
- // Switch, Android host binding. AndroidSwitch takes trackColorForTrue / trackColorForFalse plus
2
- // trackTintColor (the color for the CURRENT value, which RN computes as value ? true : false), and
3
- // snaps native back via the `setNativeValue` command (Switch.js:221-225).
4
- import { createSwitch } from './shared.js';
5
- export const Switch = createSwitch({
6
- snapBackCommand: 'setNativeValue',
7
- trackColorProps: (value, trackColor) => ({
8
- trackColorForFalse: trackColor?.false,
9
- trackColorForTrue: trackColor?.true,
10
- trackTintColor: value ? trackColor?.true : trackColor?.false,
11
- }),
12
- });
@@ -1 +0,0 @@
1
- export * from './index.ios';
@@ -1,2 +0,0 @@
1
- export type { ISwitchProps, ISwitchTrackColor } from './shared';
2
- export declare const Switch: (props: import("./shared").ISwitchProps) => import("@symbiote-native/solid/jsx-runtime").JSX.Element;
@@ -1,11 +0,0 @@
1
- // Switch, iOS host binding. iOS's native Switch takes onTintColor (ON-track) / tintColor
2
- // (OFF-track), and snaps native back via the `setValue` command (Switch.js:221-225) — the same
3
- // facts adapters/react's, adapters/vue's and adapters/svelte's iOS bindings encode.
4
- import { createSwitch } from './shared.js';
5
- export const Switch = createSwitch({
6
- snapBackCommand: 'setValue',
7
- trackColorProps: (_value, trackColor) => ({
8
- onTintColor: trackColor?.true,
9
- tintColor: trackColor?.false,
10
- }),
11
- });
@@ -1,4 +0,0 @@
1
- // Base / default Switch: 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.
4
- export * from './index.ios.js';
@@ -1,11 +0,0 @@
1
- import type { JSX } from '../../jsx-runtime';
2
- import { type ISwitchPlatform, type ISwitchProps as ISwitchBaseProps } from '@symbiote-native/components';
3
- import { type IClassNameValue } from '@symbiote-native/engine';
4
- export type { ISwitchTrackColor } from '@symbiote-native/components';
5
- export type ISwitchProps = ISwitchBaseProps & {
6
- class?: IClassNameValue;
7
- };
8
- export type ISwitchHostPlatform = ISwitchPlatform & {
9
- snapBackCommand: string;
10
- };
11
- export declare function createSwitch(platform: ISwitchHostPlatform): (props: ISwitchProps) => JSX.Element;
@@ -1,87 +0,0 @@
1
- // Switch, the Solid lifecycle half. The logic (the lastNativeReport reducer, valueFromChange, the
2
- // snap-back decision) lives in @symbiote-native/components/state and the render (value fold, track
3
- // colors, ios_backgroundColor) in @symbiote-native/components/view — both shared verbatim with
4
- // React, Vue and Svelte. Solid supplies only the reactivity: a signal holding what native last
5
- // reported, an accessor per prop, and the effect that snaps native back when the parent rejects a
6
- // toggle.
7
- //
8
- // Switch is controlled exactly like RN's: the parent's onValueChange MUST update `value` for the
9
- // toggle to stick. If the handler is a no-op, native has already flipped its own grip, so JS
10
- // commands the old value back down — a plain prop re-push cannot cover that case, because the prop
11
- // never changed and the retained tree never diverged. The command name is platform-specific (iOS
12
- // setValue / Android setNativeValue) and arrives via `platform`.
13
- //
14
- // NOTHING here destructures `props`. Solid props are getters; destructuring reads them once at
15
- // setup and freezes the component at its mount-time values. Every read below happens inside an
16
- // accessor (the descriptor memo, the snap-back effect, handleChange) so it re-runs when the caller's
17
- // signal changes.
18
- import { createEffect, createSignal } from 'solid-js';
19
- import { createInitialSwitchState, renderSwitch, resolveAccessibilityProps, shouldSnapBack, switchReducer, valueFromChange, } from '@symbiote-native/components';
20
- import { dispatchViewCommand, dlog, } from '@symbiote-native/engine';
21
- import { descriptorToSolid } from '../../descriptor-to-solid.js';
22
- // Read by the component itself; everything else forwards onto the host node. onValueChange is pure
23
- // JS and must never reach Fabric — a function prop crashes Android's folly::dynamic serializer.
24
- const HANDLED_PROPS = [
25
- 'value',
26
- 'onValueChange',
27
- 'disabled',
28
- 'trackColor',
29
- 'thumbColor',
30
- 'ios_backgroundColor',
31
- 'style',
32
- ];
33
- // Switch owns its host element rather than rendering through a symbiote View, so it folds
34
- // aria/role into the canonical accessibility* props here, exactly like React's hook does.
35
- function forwardProps(props) {
36
- const forwarded = {
37
- ...resolveAccessibilityProps(props),
38
- };
39
- for (const key of HANDLED_PROPS)
40
- delete forwarded[key];
41
- return forwarded;
42
- }
43
- export function createSwitch(platform) {
44
- return function Switch(props) {
45
- const [nativeReport, setNativeReport] = createSignal(createInitialSwitchState());
46
- // value is a real Fabric prop, folded to a strict boolean: RN sends `value === true`, so an
47
- // absent prop reads as "off" rather than riding through as undefined.
48
- const fabricValue = () => props.value === true;
49
- const handleChange = (event) => {
50
- const next = valueFromChange(event);
51
- dlog(`Switch onChange value=${String(next)} eventCount=${String(event.nativeEvent.eventCount)}`);
52
- if (next === undefined)
53
- return;
54
- props.onValueChange?.(next, event);
55
- setNativeReport(current => switchReducer(current, { type: 'native-reported', value: next }));
56
- };
57
- // The node is created once and kept by identity — descriptorToSolid wires every prop through a
58
- // render effect on this same node, so the imperative command below always has a live target and
59
- // never chases a replaced node.
60
- const node = descriptorToSolid(() => renderSwitch({
61
- value: fabricValue(),
62
- disabled: props.disabled,
63
- trackColor: props.trackColor,
64
- thumbColor: props.thumbColor,
65
- ios_backgroundColor: props.ios_backgroundColor,
66
- style: props.style,
67
- passthrough: { ...forwardProps(props), onChange: handleChange },
68
- }, platform));
69
- // createEffect, not createRenderEffect: it runs after the tree is built, the same position
70
- // React's useLayoutEffect and Vue's flush:'post' watch occupy. The engine commits on a
71
- // microtask (renderer.ts's requestCommit), so the node has no Fabric tag yet on the first run —
72
- // which is harmless and not worth a whenCommitted retry here, because shouldSnapBack is false
73
- // until native has reported, and native cannot report before it exists. Every run that DOES
74
- // dispatch is therefore downstream of a native event, i.e. long past the first commit.
75
- createEffect(() => {
76
- const value = fabricValue();
77
- const reported = nativeReport();
78
- if (!shouldSnapBack(reported, value)) {
79
- dlog(`Switch snap-back no-op reported=${String(reported.lastNativeReport)} value=${value}`);
80
- return;
81
- }
82
- dlog(`Switch ${platform.snapBackCommand} snap-back reported=${String(reported.lastNativeReport)} value=${value}`);
83
- dispatchViewCommand(node, platform.snapBackCommand, [value]);
84
- });
85
- return node;
86
- };
87
- }
@@ -1,10 +0,0 @@
1
- import type { Ref } from 'solid-js';
2
- import type { JSX } from '../jsx-runtime';
3
- import { type ITextInputHandle, type ITextInputProps as ITextInputBaseProps } from '@symbiote-native/components';
4
- import { type IClassNameValue } from '@symbiote-native/engine';
5
- export type { ITextInputHandle, ITextInputSelection, } from '@symbiote-native/components';
6
- export type ITextInputProps = ITextInputBaseProps & {
7
- class?: IClassNameValue;
8
- ref?: Ref<ITextInputHandle>;
9
- };
10
- export declare function TextInput(props: ITextInputProps): JSX.Element;