@symbiote-native/vue 0.3.8 → 0.4.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.
@@ -1,23 +1,17 @@
1
- // ScrollView, the Vue lifecycle half. The Fabric tree is nested:
2
- // a scroll view wraps a content view that holds the children (RN's ScrollView.js shape). The
3
- // platform-invariant math (decelerationRate, the per-axis intrinsics/base style, the
4
- // content-size dedupe, the imperative handle, the aria/role fold) lives in @symbiote-native/components,
5
- // shared verbatim with React. Here Vue supplies only the reactivity: a shallowRef holds the host
6
- // node, a setup-scope `lastContentSize` dedupes onContentSizeChange, and expose() wires the
7
- // imperative handle. This is the Vue twin of the React adapter's useRef + buildScrollViewHandle.
1
+ // ScrollView, the Vue lifecycle half. The Fabric tree is nested: a scroll view wraps a
2
+ // content view holding the children (RN's ScrollView.js shape). Platform-invariant math
3
+ // (decelerationRate, per-axis intrinsics/base style, content-size dedupe, imperative handle,
4
+ // aria/role fold) lives in @symbiote-native/components, shared with React; Vue supplies only
5
+ // the reactivity - shallowRef host node, setup-scope lastContentSize dedupe, expose() handle -
6
+ // the twin of React's useRef + buildScrollViewHandle.
8
7
  //
9
- // Inputs arrive as attrs (untyped), so each is narrowed with a runtime guard rather than a cast.
10
- // contentSizeChange is synthesized as a typed Vue emit from the content onLayout. The legacy
11
- // onContentSizeChange callback key MUST be consumed if it arrives (it is NOT a ViewConfig event;
12
- // forwarding a function prop would reach Fabric and crash Android's folly::dynamic). Scroll events
13
- // (onScroll/onLayout/…) ARE ViewConfig events, so they forward raw and routeProp turns them into
14
- // listeners.
8
+ // Attrs are untyped, so every field is narrowed with a runtime guard, never a cast. The legacy
9
+ // onContentSizeChange callback key MUST be consumed if present: it is NOT a ViewConfig event,
10
+ // and forwarding a function prop reaches Fabric and crashes Android's folly::dynamic.
15
11
  //
16
- // RefreshControl is wired through the platform assemble (iOS sibling /
17
- // Android wrap). Sticky headers are real. The scroll AnimatedValue (markRaw, held by
18
- // identity), the headerLayoutYs cross-talk map + bump, the viewport-height capture, and the
19
- // onScroll composition (native attach vs Animated.event) all live here; the per-header component and
20
- // the children wrap live in scroll-view-sticky-header.ts (the Vue twin of the React file).
12
+ // Sticky headers are real: the scroll AnimatedValue, headerLayoutYs cross-talk map, viewport-
13
+ // height capture, and onScroll composition (native attach vs Animated.event) live here; the
14
+ // per-header component and children wrap live in sticky-header.ts.
21
15
  import { defineComponent, h, isVNode, markRaw, onBeforeUnmount, ref, shallowRef, watch, } from '@vue/runtime-core';
22
16
  import { attachStickyScroll, buildScrollViewHandle, didContentSizeChange, forwardScrollEvent, readLayoutDimension, resolveAccessibilityProps, resolveDecelerationRate, resolveScrollForwarding, selectScrollIntrinsics, } from '@symbiote-native/components';
23
17
  import { AnimatedValue, dlog, event as animatedEvent, isClassNameValue, isNativeAnimatedAvailable, isSymbioteNode, resolveClassName, } from '@symbiote-native/engine';
@@ -26,23 +20,15 @@ import { normalizeVueAttrs } from '../../utils/normalize-attrs.js';
26
20
  function isHandler(value) {
27
21
  return typeof value === 'function';
28
22
  }
29
- // Objects and arrays are valid StyleProp<ViewStyle> (ViewStyle | RecursiveArray | falsy; the
30
- // engine omits RN's RegisteredStyle brand, so no numeric form). Primitives/null degrade to
31
- // undefined; the engine flattens whatever object/array reaches it.
23
+ // Objects and arrays are valid StyleProp<ViewStyle>; primitives/null degrade to undefined.
32
24
  function isStyleProp(value) {
33
25
  return typeof value === 'object' && value !== null;
34
26
  }
35
- // `class` is never in HANDLED_ATTRS (inheritAttrs:false means it also isn't auto-merged onto a
36
- // root), so it forwards raw to the inner scroll-view node via forwardAttrs, resolved later by
37
- // the renderer's own patchProp — fine for the single-node Phase 1 path. But the Android
38
- // RefreshControl wrap (index.android.ts) reads userStyle alone to splitLayoutProps() the outer
39
- // wrapper's layout style, BEFORE that later resolution ever runs, so a class-only layout prop
40
- // (flex, height, gap, …) never reaches the wrapper and it collapses to nothing.
41
- // isClassNameProp is @symbiote-native/engine's own isClassNameValue guard (shared, not redeclared —
42
- // routeProp's centralized class+style merge needs the identical narrowing).
27
+ // `class` forwards raw via forwardAttrs, resolved later by the renderer's patchProp. But the
28
+ // Android RefreshControl wrap (index.android.ts) reads userStyle alone to splitLayoutProps()
29
+ // the outer wrapper BEFORE that later resolution runs, so a class-only layout prop (flex,
30
+ // height, gap, …) would otherwise never reach the wrapper.
43
31
  const isClassNameProp = isClassNameValue;
44
- // decelerationRate is resolved per-platform (resolveDecelerationRate), so it must be narrowed to
45
- // its declared shape before that call, unlike the raw pass-through props.
46
32
  function asDecelerationRate(value) {
47
33
  if (typeof value === 'number')
48
34
  return value;
@@ -50,21 +36,16 @@ function asDecelerationRate(value) {
50
36
  return value;
51
37
  return undefined;
52
38
  }
53
- // stickyHeaderIndices arrives untyped (attrs); narrow to a number[] before the wrap reads it.
54
39
  function isNumberArray(value) {
55
40
  return Array.isArray(value) && value.every(entry => typeof entry === 'number');
56
41
  }
57
- // A StickyHeaderComponent override is a Vue component: a function (functional) or an object
58
- // (defineComponent). Narrowed before it is handed to wrapStickyHeaders (no cast).
59
42
  function isComponent(value) {
60
43
  return typeof value === 'function' || (typeof value === 'object' && value !== null);
61
44
  }
62
- // The prop/handler keys the lifecycle consumes itself; everything else (the scroll events,
63
- // snap/keyboard/zoom families, accessibility, testID, …) forwards onto the scroll-view node.
64
- // onContentSizeChange is consumed (synthesized from the content onLayout, never forwarded);
65
- // refreshControl is consumed by the platform assemble; the sticky-header props (indices / invert /
66
- // StickyHeaderComponent) are lifecycle-consumed by the children wrap and must NEVER reach Fabric.
67
- // style / contentContainerStyle / horizontal / decelerationRate are recomposed.
45
+ // Prop/handler keys the lifecycle consumes itself; everything else forwards onto the scroll-view
46
+ // node. onContentSizeChange is consumed (synthesized from the content onLayout); refreshControl
47
+ // and the sticky-header props are consumed by the platform assemble / children wrap and must
48
+ // NEVER reach Fabric.
68
49
  const HANDLED_ATTRS = [
69
50
  'style',
70
51
  'contentContainerStyle',
@@ -86,39 +67,29 @@ function forwardAttrs(attrs) {
86
67
  }
87
68
  export function createScrollView(platform) {
88
69
  return defineComponent((_props, { slots, attrs: rawAttrs, expose, emit }) => {
89
- // shallowRef, NOT ref: the engine node must be held by IDENTITY. A plain ref() runs the
90
- // node through Vue's toReactive(), handing back a reactive Proxy, a different object than
91
- // the raw node the engine's mirror (a WeakMap) is keyed on, so dispatchViewCommand would
92
- // miss and every scrollTo/scrollToEnd/flashScrollIndicators silently no-op. This is the
93
- // same rule as the Switch host node.
70
+ // shallowRef, NOT ref: a plain ref() would run the node through Vue's toReactive(),
71
+ // handing back a Proxy the engine's WeakMap mirror doesn't recognize, so
72
+ // scrollTo/scrollToEnd/flashScrollIndicators would silently no-op. Same rule as Switch.
94
73
  const nodeRef = shallowRef(null);
95
74
  const setNodeRef = (el) => {
96
75
  nodeRef.value = isSymbioteNode(el) ? el : null;
97
76
  };
98
- // The imperative handle reads the node through a LAZY getter (() => nodeRef.value), not the
99
- // node captured once: it is null until the element commits, so an eager capture would freeze
100
- // null and every command would no-op. expose() makes it the value a parent ref
101
- // sees: the Vue twin of React's useImperativeHandle(forwardedRef, buildScrollViewHandle(…)).
77
+ // Lazy getter, not the node captured once: it is null until the element commits, so an
78
+ // eager capture would freeze null. Vue twin of useImperativeHandle(ref, buildScrollViewHandle).
102
79
  expose(buildScrollViewHandle(() => nodeRef.value));
103
- // The last-seen content size, kept here (setup scope, persists across renders) to dedupe
104
- // contentSizeChange: RN fires the content onLayout on every layout pass; only real size
105
- // changes emit to the user (didContentSizeChange).
80
+ // RN fires the content onLayout on every layout pass; only a real size change emits.
106
81
  let lastContentSize = null;
107
- // A single AnimatedValue tracks the scroll offset and drives every sticky header's translateY
108
- // (RN's _scrollAnimatedValue). A setup-scope const (allocated once, stable across renders) so
109
- // the headers' bindings survive re-renders, the Vue twin of React's useRef-stable value.
110
- // markRaw: it is an engine object, held by IDENTITY, never run through toReactive (the
111
- // reactivity rule; a deep ref would hand back a Proxy the engine's mirror misses). Allocated
112
- // unconditionally (like React's unconditional hook); unused when no sticky headers are flagged.
82
+ // Drives every sticky header's translateY (RN's _scrollAnimatedValue). markRaw: an engine
83
+ // object held by identity, never run through toReactive. Allocated unconditionally
84
+ // (unused when no sticky headers are flagged), like React's unconditional hook.
113
85
  const scrollAnimatedValue = markRaw(new AnimatedValue(0));
114
- // Inverted sticky headers stick to the BOTTOM, so they need the viewport height (RN reads it
115
- // in _handleLayout). Tracked here and fed back into the wrapped headers on the next render.
86
+ // Inverted sticky headers stick to the BOTTOM, so they need the viewport height (RN's
87
+ // _handleLayout). Fed back into the wrapped headers on the next render.
116
88
  const viewportHeight = ref(undefined);
117
- // Sticky-header cross-talk (RN ScrollView.js _headerLayoutYs, line 754): a child-index→measured-y
118
- // map the parent keeps so each header can learn where the NEXT sticky header starts (its push-off
119
- // collision point). The map is a setup-scope const mutated imperatively from each header's onLayout
120
- // (like RN's _onStickyHeaderLayout); a reactive bump ref forces the re-render that feeds the
121
- // freshly-recorded y forward into the previous header's nextHeaderLayoutY (via nextStickyHeaderY).
89
+ // Sticky-header cross-talk (RN ScrollView.js _headerLayoutYs): a child-index -> measured-y
90
+ // map so each header learns where the NEXT sticky header starts (its push-off collision
91
+ // point). Mutated imperatively from each header's onLayout; the bump ref forces the
92
+ // re-render that feeds the freshly-recorded y into the previous header's nextStickyHeaderY.
122
93
  const headerLayoutYs = new Map();
123
94
  const bumpHeaderLayout = ref(0);
124
95
  const onHeaderLayoutY = (index, y) => {
@@ -128,13 +99,11 @@ export function createScrollView(platform) {
128
99
  dlog(`Vue ScrollView sticky-header layoutY index=${index} y=${y}`);
129
100
  bumpHeaderLayout.value += 1;
130
101
  };
131
- // Native sticky-scroll attach (RN attachNativeEvent / _updateAnimatedNodeAttachment): when the
132
- // native module is available, the scroll value is driven on the UI thread so the interpolations
133
- // ride scroll natively (no JS jitter). A plain flag set in render (like createAnimatedComponent's
134
- // wantsNative: non-reactive so writing it in render triggers no effect); the post-commit watch
135
- // reads it once the node commits. The JS path needs no attach: Animated.event drives the value
136
- // each frame. flush:'post' so the engine has committed the node before attachStickyScroll reads
137
- // its Fabric handle. Detached on unmount (and re-detached if the node identity changes).
102
+ // Native sticky-scroll attach (RN attachNativeEvent): when the native module is available,
103
+ // the scroll value is driven on the UI thread so interpolations ride scroll natively (no
104
+ // JS jitter). A plain non-reactive flag set in render; the post-commit watch reads it once
105
+ // the node commits. flush:'post' so the node has a Fabric handle before attachStickyScroll
106
+ // reads it. Detached on unmount and re-detached if the node identity changes.
138
107
  let nativeStickyWanted = false;
139
108
  let detachStickyScroll;
140
109
  watch(() => nodeRef.value, node => {
@@ -153,24 +122,17 @@ export function createScrollView(platform) {
153
122
  return () => {
154
123
  // Read the bump so a recorded header y re-runs render and feeds nextStickyHeaderY forward.
155
124
  void bumpHeaderLayout.value;
156
- // Fold kebab template props (:content-container-style) to the RN camelCase contract; idiomatic
157
- // Vue templates use kebab, but the prop surface (and HANDLED_ATTRS below) is camelCase.
125
+ // Fold kebab template props (:content-container-style) to the camelCase prop surface.
158
126
  const attrs = normalizeVueAttrs(rawAttrs);
159
127
  const isHorizontal = attrs.horizontal === true;
160
128
  const userStyle = isStyleProp(attrs.style) ? attrs.style : undefined;
161
- // resolveClassName(undefined) is a cheap {} no-op, so this is safe with no class prop too.
162
129
  const classProp = isClassNameProp(attrs.class) ? attrs.class : undefined;
163
130
  const layoutSplitStyle = [resolveClassName(classProp), userStyle];
164
- // A class-name string resolves through the same style registry as `class`/`style` above;
165
- // an object/array is already style-shaped and passes through as-is.
166
131
  const contentContainerStyle = typeof attrs.contentContainerStyle === 'string'
167
132
  ? resolveClassName(attrs.contentContainerStyle)
168
133
  : isStyleProp(attrs.contentContainerStyle)
169
134
  ? attrs.contentContainerStyle
170
135
  : undefined;
171
- // Sticky headers are a pure-JS layer; the native scroll view ignores
172
- // stickyHeaderIndices, so we wrap the flagged children below and drive their translateY off
173
- // the scroll offset. invertStickyHeaders narrows to the inverted (stick-to-bottom) branch.
174
136
  const stickyHeaderIndices = isNumberArray(attrs.stickyHeaderIndices)
175
137
  ? attrs.stickyHeaderIndices
176
138
  : undefined;
@@ -180,10 +142,8 @@ export function createScrollView(platform) {
180
142
  ? attrs.StickyHeaderComponent
181
143
  : undefined;
182
144
  const { scrollViewIntrinsic, contentIntrinsic, scrollViewBaseStyle, contentStyle } = selectScrollIntrinsics(isHorizontal, contentContainerStyle);
183
- // Outer props: fold aria/role, then layer the lifecycle-managed values on top. RN defaults
184
- // nested scrolling ON (ScrollView.js `nestedScrollEnabled ?? true`); horizontal forwards
185
- // only when defined (load-bearing on iOS's RCTScrollView axis, ignored by Android's
186
- // dedicated manager); decelerationRate is resolved per-platform.
145
+ // RN defaults nested scrolling ON (ScrollView.js `nestedScrollEnabled ?? true`);
146
+ // horizontal forwards only when defined (load-bearing on iOS's RCTScrollView axis).
187
147
  const outerProps = {
188
148
  ...resolveAccessibilityProps(forwardAttrs(attrs)),
189
149
  };
@@ -194,10 +154,10 @@ export function createScrollView(platform) {
194
154
  const decel = asDecelerationRate(attrs.decelerationRate);
195
155
  if (decel !== undefined)
196
156
  outerProps.decelerationRate = resolveDecelerationRate(decel);
197
- // onScroll: when sticky headers are active, the offset must reach the AnimatedValue. RN does
198
- // the same with _scrollAnimatedValueAttachment. forwardAttrs already put the user's onScroll /
199
- // onLayout / scrollEventThrottle on outerProps; here we override them per the shared
200
- // resolveScrollForwarding DECISIONS (which path, the 1/16 throttle defaults, inverted capture).
157
+ // When sticky headers are active, the offset must reach the AnimatedValue (RN's
158
+ // _scrollAnimatedValueAttachment). forwardAttrs already put the user's onScroll/onLayout/
159
+ // scrollEventThrottle on outerProps; here we override per resolveScrollForwarding's
160
+ // decisions (which path, throttle default, inverted capture).
201
161
  const nativeStickyAvailable = hasStickyHeaders && isNativeAnimatedAvailable();
202
162
  nativeStickyWanted = nativeStickyAvailable;
203
163
  const userThrottle = typeof attrs.scrollEventThrottle === 'number' ? attrs.scrollEventThrottle : undefined;
@@ -212,20 +172,19 @@ export function createScrollView(platform) {
212
172
  if (hasStickyHeaders) {
213
173
  const userOnScroll = isHandler(attrs.onScroll) ? attrs.onScroll : undefined;
214
174
  if (forwarding.mode === 'sticky-js') {
215
- // JS fallback (no native module): Animated.event drives the value each frame and forwards
216
- // the user's handler as the listener passthrough. Correct, but lags a frame under fast
217
- // scroll (the jitter), which the native path removes on a real host.
175
+ // JS fallback (no native module): correct, but lags a frame under fast scroll, which
176
+ // the native path removes on a real host.
218
177
  outerProps.onScroll = animatedEvent([{ nativeEvent: { contentOffset: { y: scrollAnimatedValue } } }], userOnScroll === undefined
219
178
  ? undefined
220
179
  : { listener: (...args) => forwardScrollEvent(userOnScroll, args) });
221
180
  }
222
- // Native path: the value is driven on the UI thread by the post-commit watch above, so the
223
- // user onScroll (already on outerProps via forwardAttrs) forwards untouched, zero JS/frame.
181
+ // Native path: the value is driven on the UI thread by the post-commit watch above, so
182
+ // onScroll forwards untouched.
224
183
  if (forwarding.scrollEventThrottle !== undefined) {
225
184
  outerProps.scrollEventThrottle = forwarding.scrollEventThrottle;
226
185
  }
227
- // onLayout on the scroll-view node: capture the viewport height for inverted sticky headers
228
- // (RN _handleLayout), then call the user's handler. Non-inverted leaves onLayout as forwarded.
186
+ // Capture the viewport height for inverted sticky headers (RN _handleLayout), then call
187
+ // the user's handler.
229
188
  if (forwarding.capturesViewportHeight) {
230
189
  const userOnLayout = isHandler(attrs.onLayout) ? attrs.onLayout : undefined;
231
190
  outerProps.onLayout = (event) => {
@@ -238,18 +197,15 @@ export function createScrollView(platform) {
238
197
  }
239
198
  }
240
199
  dlog(`Vue ScrollView -> ${scrollViewIntrinsic} (horizontal=${isHorizontal} sticky=${hasStickyHeaders})`);
241
- // Content props: `collapsable: false` keeps the layout-only content view as a real native
242
- // view: Android Fabric view-flattens it away otherwise, hoisting the cells as direct
243
- // children of the scroll view (which hosts exactly one), an addViewAt crash. collapsableChildren
244
- // false also preserves the cell views maintainVisibleContentPosition / snapToAlignment
245
- // anchor against (RN preserveChildren). iOS never flattens; both are no-ops there.
200
+ // `collapsable: false` keeps the layout-only content view as a real native view: Android
201
+ // Fabric view-flattens it away otherwise, hoisting the cells as direct children of the
202
+ // scroll view (which hosts exactly one) - an addViewAt crash. iOS never flattens.
246
203
  const contentProps = { style: contentStyle, collapsable: false };
247
204
  if (forwarding.collapsableChildren) {
248
205
  contentProps.collapsableChildren = false;
249
206
  }
250
- // contentSizeChange is synthesized from the content view's own onLayout (RN
251
- // _handleContentOnLayout): read width/height and emit only on a real size change (dedupe via
252
- // the setup-scope lastContentSize).
207
+ // Synthesized from the content view's own onLayout (RN _handleContentOnLayout); emit only
208
+ // on a real size change.
253
209
  contentProps.onLayout = (event) => {
254
210
  const width = readLayoutDimension(event, 'width');
255
211
  const height = readLayoutDimension(event, 'height');
@@ -261,23 +217,17 @@ export function createScrollView(platform) {
261
217
  dlog(`Vue ScrollView contentSizeChange ${width}x${height}`);
262
218
  emit('contentSizeChange', width, height);
263
219
  };
264
- // Sticky headers are a pure-JS layer (the native scroll view ignores stickyHeaderIndices);
265
- // wrap the flagged children so they pin to the scroll offset. No-op when none are flagged.
266
220
  const slotChildren = slots.default !== undefined ? slots.default() : [];
267
221
  const contentChildren = hasStickyHeaders
268
222
  ? wrapStickyHeaders(slotChildren, stickyHeaderIndices, scrollAnimatedValue, invertStickyHeaders, viewportHeight.value, stickyHeaderComponent, headerLayoutYs, onHeaderLayoutY)
269
223
  : slotChildren;
270
224
  const content = h(contentIntrinsic, contentProps, contentChildren);
271
- // Base style UNDER user style so an explicit user value (height, flexDirection) still wins;
272
- // the scroll node carries overflow:'scroll' (frame clipping) + the per-axis flexDirection.
225
+ // Base style UNDER user style so an explicit user value (height, flexDirection) still wins.
273
226
  const scrollProps = {
274
227
  ...outerProps,
275
228
  style: [scrollViewBaseStyle, userStyle],
276
229
  ref: setNodeRef,
277
230
  };
278
- // refreshControl arrives as a Vue VNode the app passes (h(RefreshControl, …)); narrow it
279
- // with isVNode (no cast). Stripped from forwardAttrs (it is in HANDLED_ATTRS) so it never
280
- // reaches the host as a prop: it is lifecycle-consumed by the platform assemble.
281
231
  const refreshControl = isVNode(attrs.refreshControl) ? attrs.refreshControl : undefined;
282
232
  return platform.assemble({
283
233
  scrollViewIntrinsic,
@@ -1,16 +1,14 @@
1
1
  // Sticky headers: the Vue twin of adapters/react/src/scroll-view-sticky-header.tsx, the JS layer
2
2
  // RN implements in ScrollView.js / ScrollViewStickyHeader.js.
3
3
  //
4
- // Source-based: RN does stickiness PURELY IN JS. ScrollView.js (render, ~line 1690)
5
- // wraps each child whose index is in `stickyHeaderIndices` in a ScrollViewStickyHeader, fed by a
6
- // single `_scrollAnimatedValue` an Animated.event drives from `onScroll` (ScrollView.js ~line
7
- // 1095). The native Fabric scroll view does NOT honor the index array on its own, so forwarding
8
- // `stickyHeaderIndices` to native is a silent no-op. So we replicate the JS layer: subscribe each
9
- // flagged child to the scroll offset and translate it to stay pinned. The interpolation mirrors
10
- // ScrollViewStickyHeader.js (non-inverted + inverted branches) and lives, framework-agnostic, in
4
+ // RN does stickiness PURELY IN JS: ScrollView.js wraps each flagged child in a
5
+ // ScrollViewStickyHeader fed by a single _scrollAnimatedValue that Animated.event drives from
6
+ // onScroll. The native scroll view does NOT honor the index array itself, so forwarding
7
+ // stickyHeaderIndices to native is a silent no-op - we replicate the JS layer instead. The
8
+ // interpolation math (non-inverted + inverted) lives framework-agnostic in
11
9
  // @symbiote-native/components (computeStickyInterpolation); this file holds the Vue component
12
- // shell, the layout state, and the child-wrapping. Render shared verbatim with React via the math.
13
- // Vue supplies only the reactive lifecycle (refs/watch instead of useState/useEffect).
10
+ // shell, layout state, and child-wrapping. Vue supplies only the reactive lifecycle
11
+ // (refs/watch instead of useState/useEffect).
14
12
  import { defineComponent, h, isVNode, onBeforeUnmount, ref, shallowRef, watchEffect, markRaw, } from '@vue/runtime-core';
15
13
  import { AnimatedValue, AnimatedInterpolation, Platform, dlog, } from '@symbiote-native/engine';
16
14
  import { createInitialStickyState, nextStickyHeaderY, readLayoutNumber, reduceSticky, STICKY_HEADER_Z_INDEX, } from '@symbiote-native/components';
@@ -24,46 +22,39 @@ function isRecord(value) {
24
22
  function isAnimatedValue(value) {
25
23
  return value instanceof AnimatedValue;
26
24
  }
27
- // Read this header's wrapped child's own onLayout off its VNode props, so the sticky wrapper can
28
- // forward layout to it (RN ScrollViewStickyHeader.js calls the child's onLayout after its own).
25
+ // So the sticky wrapper can forward layout to the child (RN calls the child's onLayout after its own).
29
26
  function readChildOnLayout(child) {
30
27
  if (!isRecord(child.props))
31
28
  return undefined;
32
29
  const handler = child.props.onLayout;
33
30
  return isHandler(handler) ? handler : undefined;
34
31
  }
35
- // One sticky header. Measures its own y/height via onLayout, interpolates the shared scroll offset
36
- // into a translateY that keeps it pinned to the top (or bottom, inverted) until the next header
37
- // collides with it, and drives that translate through the native driver when available so the pin
38
- // tracks scroll on the UI thread (no JS jitter). Ported from ScrollViewStickyHeader.js,
39
- // including the Fabric ShadowTree debounce path. inheritAttrs:false so the IStickyHeaderProps
40
- // inputs (scrollAnimatedValue/nextHeaderLayoutY/…) never fall through onto Animated.View and reach
41
- // Fabric as props (scrollAnimatedValue on a host node would crash Android's folly::dynamic).
32
+ // One sticky header. Measures its own y/height via onLayout, interpolates the shared scroll
33
+ // offset into a translateY pinning it to the top (or bottom, inverted) until the next header
34
+ // collides with it, driven through the native driver when available (no JS jitter). Ported from
35
+ // ScrollViewStickyHeader.js, including the Fabric ShadowTree debounce path. inheritAttrs:false
36
+ // so the IStickyHeaderProps inputs never fall through onto Animated.View and reach Fabric as
37
+ // props (scrollAnimatedValue on a host node would crash Android's folly::dynamic).
42
38
  export const ScrollViewStickyHeader = defineComponent({
43
39
  name: 'ScrollViewStickyHeader',
44
40
  inheritAttrs: false,
45
41
  setup(_props, { attrs, slots }) {
46
- // The scroll-offset value the parent shares, read once (it is stable across renders, the same
47
- // markRaw'd AnimatedValue), held by IDENTITY in a const (never run through toReactive). A fresh
48
- // fallback keeps working if the invariant (wrapStickyHeaders always supplies it) ever breaks.
42
+ // Read once - stable across renders (the same markRaw'd AnimatedValue). Held by IDENTITY, never
43
+ // run through toReactive. A fresh fallback keeps working if wrapStickyHeaders ever fails to supply it.
49
44
  const scrollAnimatedValue = isAnimatedValue(attrs.scrollAnimatedValue)
50
45
  ? attrs.scrollAnimatedValue
51
46
  : markRaw(new AnimatedValue(0));
52
- // The one folded state cell, mutated in place by reduceSticky. A plain object (NOT a ref /
53
- // reactive): the render reads it gated by the reactive `version` bump + `animatedTranslateY`
54
- // shallowRef below, so it never needs Vue to proxy it. The DECISIONS — zero-swallow gate,
55
- // debounce delay, rebuild ranges — all live in reduceSticky.
47
+ // Mutated in place by reduceSticky. A plain object, not a ref: render reads it gated by the
48
+ // reactive `version` bump + `animatedTranslateY` below. DECISIONS (zero-swallow gate,
49
+ // debounce delay, rebuild ranges) all live in reduceSticky.
56
50
  const state = createInitialStickyState();
57
- // A reactive tick bumped when the reducer commits a new translateY, forcing the render to re-read
58
- // state.translateY (the debounced committed value).
51
+ // Bumped when the reducer commits a new translateY, forcing render to re-read state.translateY.
59
52
  const version = ref(0);
60
- // The animated node that drives the transform (RN's animatedTranslateY). Engine node →
61
- // shallowRef (held by identity, the reactivity rule); the un-measured identity stub until the
53
+ // Engine node -> shallowRef (identity rule); the un-measured identity stub until the
62
54
  // rebuild-interpolation effect below replaces it.
63
55
  const animatedTranslateY = shallowRef(scrollAnimatedValue.interpolate({ inputRange: [-1, 0], outputRange: [0, 0] }));
64
56
  let debounceTimer;
65
- // The current interpolation node + its listener id, held so the next rebuild detaches the old
66
- // listener (an engine call the reducer does not own) and onBeforeUnmount cleans up.
57
+ // Held so the next rebuild detaches the old listener and onBeforeUnmount cleans up.
67
58
  let interpolation;
68
59
  let listenerId;
69
60
  const inputs = () => ({
@@ -76,8 +67,7 @@ export const ScrollViewStickyHeader = defineComponent({
76
67
  for (const effect of effects) {
77
68
  switch (effect.kind) {
78
69
  case 'rebuild-interpolation': {
79
- // Detach the old listener, build a fresh interpolation onto the shared scroll value, and
80
- // wire the settled-value listener (symbiote is always Fabric; RN attaches it only there).
70
+ // Detach the old listener, build a fresh interpolation, and wire the settled-value listener.
81
71
  if (interpolation !== undefined && listenerId !== undefined) {
82
72
  interpolation.removeListener(listenerId);
83
73
  listenerId = undefined;
@@ -95,8 +85,8 @@ export const ScrollViewStickyHeader = defineComponent({
95
85
  break;
96
86
  }
97
87
  case 'schedule-debounce':
98
- // The animated value updates several times per frame; debounce the settled value into the
99
- // committed transform so hit detection stays current (a Fabric issue, worse on Android).
88
+ // The animated value updates several times per frame; debounce the settled value so
89
+ // hit detection stays current (a Fabric issue, worse on Android).
100
90
  if (debounceTimer !== undefined)
101
91
  clearTimeout(debounceTimer);
102
92
  debounceTimer = setTimeout(() => {
@@ -108,8 +98,7 @@ export const ScrollViewStickyHeader = defineComponent({
108
98
  version.value += 1;
109
99
  break;
110
100
  case 'record-header-y':
111
- // Vue records through attrs.onLayout (the wrapper closure), which honors the public
112
- // IStickyHeaderProps contract; the reducer emits no index for it.
101
+ // Vue records through attrs.onLayout (the wrapper closure); the reducer emits no index for it.
113
102
  break;
114
103
  }
115
104
  }
@@ -117,9 +106,8 @@ export const ScrollViewStickyHeader = defineComponent({
117
106
  const dispatch = (action) => {
118
107
  runEffects(reduceSticky(state, action, inputs()).effects);
119
108
  };
120
- // Rebuild whenever the collision/viewport inputs change (the Vue twin of React's inputs-changed
121
- // effect, tracking [inverted, scrollViewHeight, nextHeaderLayoutY] via inputs()); also the initial
122
- // build on mount. scrollAnimatedValue is the stable const (never changes for one ScrollView).
109
+ // Rebuild whenever the collision/viewport inputs change ([inverted, scrollViewHeight,
110
+ // nextHeaderLayoutY]); also runs once on mount.
123
111
  watchEffect(() => {
124
112
  dispatch({ kind: 'inputs-changed' });
125
113
  });
@@ -130,9 +118,8 @@ export const ScrollViewStickyHeader = defineComponent({
130
118
  if (debounceTimer !== undefined)
131
119
  clearTimeout(debounceTimer);
132
120
  });
133
- // Stable function ref (defined once, not re-created per render): dispatch the layout (record
134
- // own y/height, mark measured, rebuild), fire the wrapper's recorder (onHeaderLayoutY, in
135
- // attrs.onLayout), then the child's own onLayout. Matches RN ScrollViewStickyHeader.js._onLayout.
121
+ // Dispatch the layout, fire the wrapper's recorder (onHeaderLayoutY via attrs.onLayout), then
122
+ // the child's own onLayout. Matches RN ScrollViewStickyHeader.js._onLayout.
136
123
  const onLayout = (event) => {
137
124
  const y = readLayoutNumber(event, 'y');
138
125
  const height = readLayoutNumber(event, 'height');
@@ -151,13 +138,12 @@ export const ScrollViewStickyHeader = defineComponent({
151
138
  // Read the version bump so a committed translateY re-runs render.
152
139
  void version.value;
153
140
  // The EXPLICIT debounced translateY overrides the committed transform for hit-testing, while
154
- // `animatedTranslateY` does the smooth (native-driven) pin. See RN ScrollViewStickyHeader.js.
141
+ // animatedTranslateY does the smooth (native-driven) pin.
155
142
  const passthroughAnimatedPropExplicitValues = state.translateY !== null
156
143
  ? { style: { transform: [{ translateY: state.translateY }] } }
157
144
  : null;
158
145
  // collapsable:false keeps the wrapper a real Yoga node; zIndex makes the pinned header paint
159
- // OVER the rows scrolling under it. The interpolation node passes inside the transform; the
160
- // Animated wrapper's reduceProps rasterizes it into a numeric translateY for the committed tree.
146
+ // OVER the rows scrolling under it.
161
147
  return h(Animated.View, {
162
148
  style: {
163
149
  transform: [{ translateY: animatedTranslateY.value }],
@@ -171,15 +157,13 @@ export const ScrollViewStickyHeader = defineComponent({
171
157
  },
172
158
  });
173
159
  // Wrap each child flagged by `stickyHeaderIndices` in the sticky header component, fed by the
174
- // shared scroll AnimatedValue. Mirrors ScrollView.js's render-time children.map (~line 1690) and
175
- // the React adapter's wrapStickyHeaders. Returns the children unchanged when no indices are flagged.
160
+ // shared scroll AnimatedValue. Returns the children unchanged when no indices are flagged.
176
161
  //
177
- // Cross-talk plumbing (RN's _headerLayoutYs + _onStickyHeaderLayout, ScrollView.js:1115-1143):
178
- // `headerLayoutYs` is a child-index→measured-y map the parent keeps; each header reports its own y
179
- // through `onHeaderLayoutY` as it measures, and we feed every header the y of the NEXT flagged
180
- // header (the collision point past which it scrolls off) by looking up its successor's index in
181
- // `stickyHeaderIndices`. The LAST flagged header has no successor, so its `nextHeaderLayoutY` stays
182
- // undefined and it sticks indefinitely (correct).
162
+ // Cross-talk plumbing (RN's _headerLayoutYs + _onStickyHeaderLayout): headerLayoutYs is a
163
+ // child-index -> measured-y map; each header reports its own y through onHeaderLayoutY, and we
164
+ // feed every header the y of the NEXT flagged header (the collision point past which it scrolls
165
+ // off). The LAST flagged header has no successor, so its nextHeaderLayoutY stays undefined and
166
+ // it sticks indefinitely (correct).
183
167
  export function wrapStickyHeaders(children, stickyHeaderIndices, scrollAnimatedValue, invertStickyHeaders, scrollViewHeight, StickyHeaderComponent, headerLayoutYs, onHeaderLayoutY) {
184
168
  if (stickyHeaderIndices === undefined || stickyHeaderIndices.length === 0)
185
169
  return children;
@@ -188,17 +172,15 @@ export function wrapStickyHeaders(children, stickyHeaderIndices, scrollAnimatedV
188
172
  const indexOfIndex = stickyHeaderIndices.indexOf(index);
189
173
  if (indexOfIndex === -1 || !isVNode(child))
190
174
  return child;
191
- // The next flagged header's measured y, by index order in stickyHeaderIndices (RN
192
- // ScrollView.js:1695 nextIndex). undefined until that header has measured (or for the last).
175
+ // The next flagged header's measured y. undefined until that header has measured (or for the last).
193
176
  const nextIndex = stickyHeaderIndices[indexOfIndex + 1];
194
177
  const nextHeaderLayoutY = nextStickyHeaderY(stickyHeaderIndices, indexOfIndex, headerLayoutYs);
195
178
  dlog(`Vue ScrollView sticky-header wrap index=${index} next=${nextIndex} nextY=${nextHeaderLayoutY}`);
196
179
  const props = {
197
180
  key: child.key ?? `sticky-${index}`,
198
181
  nextHeaderLayoutY,
199
- // RN _onStickyHeaderLayout: record this header's own y, then push it to the previous header as
200
- // its nextHeaderLayoutY. We record into the parent map; the lookup above feeds it forward on
201
- // the resulting re-render (the headerLayoutYs bump in scroll-view-shared).
182
+ // Record this header's own y into the parent map; the lookup above feeds it forward to the
183
+ // previous header on the resulting re-render (the headerLayoutYs bump in scroll-view-shared).
202
184
  onLayout: (event) => {
203
185
  const y = readLayoutNumber(event, 'y');
204
186
  if (y !== undefined)
@@ -1,21 +1,18 @@
1
1
  // SectionList, the Vue public list-of-sections component. A thin wrapper over
2
2
  // VirtualizedSectionList, mirroring RN's layering (SectionList -> VirtualizedSectionList ->
3
- // VirtualizedList). All section-flattening / windowing / imperative-scroll logic lives below;
4
- // this layer re-exposes the same surface under the SectionList name and re-exposes the handle
5
- // (Vue resolves a parent ref to the exposed object, so the wrapper delegates). The Vue twin of
6
- // the React adapter's SectionList.
3
+ // VirtualizedList). All section-flattening/windowing/imperative-scroll logic lives below; this
4
+ // layer re-exposes the same surface and re-exposes the handle (Vue resolves a parent ref to the
5
+ // exposed object, so the wrapper delegates).
7
6
  //
8
- // Typed-emits generic component (mirrors FlatList / VirtualizedList): a GENERIC setup function
9
- // `<ItemT,>(props: ISectionListProps<ItemT>, ctx: ICtx<ISectionListEmits>)` so the section inputs
10
- // (sections/renderItem/…) infer ItemT at the call site. As a pure forwarder it consumes nothing
11
- // itself — every input rides through $attrs straight onto VirtualizedSectionList; only the three
12
- // synthesized events (endReached/startReached/refresh) are bridged, gated on listener presence so
13
- // the inner list keeps its RefreshControl / edge-reached gating.
7
+ // Typed-emits generic component: a GENERIC setup function so section inputs infer ItemT at the
8
+ // call site. As a pure forwarder it consumes nothing itself - every input rides through $attrs
9
+ // straight onto VirtualizedSectionList; only the three synthesized events are bridged, gated on
10
+ // listener presence so the inner list keeps its RefreshControl/edge-reached gating.
14
11
  import { defineComponent, getCurrentInstance, h, shallowRef, } from '@vue/runtime-core';
15
12
  import { VirtualizedSectionList, } from '../virtualized-section-list/index.js';
16
13
  import { normalizeVueAttrs } from '../../utils/normalize-attrs.js';
17
- // VirtualizedSectionList is a generic component (generic construct signature), which h()'s overloads
18
- // can't resolve. Drive it through a loose functional-component handle (generic-component h() limit).
14
+ // VirtualizedSectionList's generic construct signature can't be resolved by h()'s overloads, so
15
+ // drive it through a loose functional-component handle.
19
16
  const VirtualizedSectionListHost = VirtualizedSectionList;
20
17
  function isRecord(value) {
21
18
  return typeof value === 'object' && value !== null && !Array.isArray(value);
@@ -36,17 +33,15 @@ function buildDelegate(getInner) {
36
33
  };
37
34
  }
38
35
  export const SectionList = defineComponent(
39
- // _props is read only by the type system: ISectionListProps<ItemT> is what lets ItemT infer at
40
- // the call site. As a pure forwarder, SectionList consumes nothing at runtime — it spreads $attrs.
36
+ // _props is read only by the type system, to let ItemT infer at the call site.
41
37
  (_props, { attrs, expose, emit, slots }) => {
42
38
  const inner = shallowRef(null);
43
39
  const setInner = (instance) => {
44
40
  inner.value = isSectionHandle(instance) ? instance : null;
45
41
  };
46
42
  expose(buildDelegate(() => inner.value));
47
- // The three section-list events are emits, so Vue strips their onX listeners from $attrs. Detect
48
- // listener presence off the instance's own vnode props and bridge each ONLY when listened, so
49
- // the inner VirtualizedSectionList (and its inner VirtualizedList) keeps its on-demand gating.
43
+ // Emits are stripped from $attrs by Vue; detect listener presence off the instance's own
44
+ // vnode props and bridge each ONLY when listened.
50
45
  const instance = getCurrentInstance();
51
46
  const listens = (onName) => {
52
47
  const vnodeProps = instance?.vnode.props;
@@ -60,8 +55,6 @@ export const SectionList = defineComponent(
60
55
  ? (info) => emit('startReached', info)
61
56
  : undefined;
62
57
  const refresh = listens('onRefresh') ? () => emit('refresh') : undefined;
63
- // Pure forwarder: spread $attrs as props and pass the consumer's scoped slots (#item /
64
- // #sectionHeader / … ) straight down to VirtualizedSectionList untouched.
65
58
  return h(VirtualizedSectionListHost, {
66
59
  ...normalizeVueAttrs(attrs),
67
60
  ref: setInner,