@symbiote-native/vue 0.3.8 → 0.5.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 (46) hide show
  1. package/README.md +6 -6
  2. package/build/bootstrap.js +1 -1
  3. package/build/components/flat-list/index.js +32 -39
  4. package/build/components/image-background.js +8 -19
  5. package/build/components/image.js +12 -21
  6. package/build/components/keyboard-avoiding-view.js +47 -31
  7. package/build/components/modal/index.d.ts +4 -4
  8. package/build/components/modal/index.js +5 -2
  9. package/build/components/pressable.d.ts +1 -1
  10. package/build/components/pressable.js +25 -32
  11. package/build/components/refresh-control.js +12 -18
  12. package/build/components/scroll-view/index.android.d.ts +1 -1
  13. package/build/components/scroll-view/index.android.js +24 -34
  14. package/build/components/scroll-view/index.ios.d.ts +1 -1
  15. package/build/components/scroll-view/index.ios.js +3 -1
  16. package/build/components/scroll-view/shared.js +93 -123
  17. package/build/components/scroll-view/sticky-header.js +59 -64
  18. package/build/components/section-list/index.js +16 -21
  19. package/build/components/switch/shared.js +29 -32
  20. package/build/components/text-input/index.js +36 -51
  21. package/build/components/touchable-native-feedback.js +3 -1
  22. package/build/components/touchable.d.ts +7 -1
  23. package/build/components/touchable.js +181 -33
  24. package/build/components/virtualized-list/index.js +57 -18
  25. package/build/components/virtualized-section-list/index.d.ts +5 -0
  26. package/build/components/virtualized-section-list/index.js +50 -29
  27. package/build/components.js +9 -6
  28. package/build/create-portal/index.d.ts +28 -0
  29. package/build/create-portal/index.js +42 -0
  30. package/build/host-instance/index.js +5 -5
  31. package/build/index.d.ts +9 -6
  32. package/build/index.js +18 -46
  33. package/build/modules/animated/create-animated-component.js +46 -101
  34. package/build/modules/animated/index.d.ts +2 -0
  35. package/build/modules/animated/index.js +26 -17
  36. package/build/modules/app-registry/index.js +11 -13
  37. package/build/modules/status-bar.js +12 -17
  38. package/build/render.d.ts +3 -1
  39. package/build/render.js +62 -1
  40. package/build/renderer/index.js +11 -6
  41. package/build/runtime-helpers/index.d.ts +44 -24
  42. package/build/runtime-helpers/index.js +116 -47
  43. package/build/utils/normalize-attrs.js +3 -1
  44. package/metro-css-parser.cjs +9 -14
  45. package/metro-vue-transformer.cjs +196 -178
  46. package/package.json +5 -5
@@ -1,24 +1,20 @@
1
- // RefreshControl, the Vue lifecycle half. On iOS this is the PullToRefreshView
2
- // Fabric node that lives INSIDE a ScrollView (a sibling before the content container); on Android it
3
- // is AndroidSwipeRefreshLayout and WRAPS the scroll view (a ScrollView there hosts one child). The
4
- // Vue twin of the React adapter's RefreshControl. Vue takes the wrapped child via its DEFAULT SLOT
5
- // (the seam the Android scroll-view wrap re-invokes to host the scroll view inside it; iOS leaves the
6
- // slot empty), folds aria/role through the shared resolveAccessibilityProps, and forwards the native
7
- // props onto the symbiote-refresh-control host node.
1
+ // RefreshControl, the Vue lifecycle half. On iOS this is the PullToRefreshView Fabric node that
2
+ // lives INSIDE a ScrollView (a sibling before the content container); on Android it is
3
+ // AndroidSwipeRefreshLayout and WRAPS the scroll view (a ScrollView there hosts one child). Vue
4
+ // takes the wrapped child via its DEFAULT SLOT (the seam the Android scroll-view wrap re-invokes
5
+ // to host the scroll view inside it; iOS leaves the slot empty), folds aria/role through the
6
+ // shared resolveAccessibilityProps, and forwards the native props onto the
7
+ // symbiote-refresh-control host node.
8
8
  //
9
- // `refreshing` is a controlled prop: the parent owns it and pushes it down each commit; native
10
- // reports the gesture via the direct `topRefresh` event, which the engine routes to the host
11
- // onRefresh prop; the adapter turns that into the typed Vue `refresh` emit. Inputs arrive as attrs
12
- // (untyped), so the forwarded bag is BUILT at the a11y-intersection type (a genuine narrowing, not
13
- // a cast) before the aria fold.
9
+ // `refreshing` is a controlled prop the parent pushes down each commit; native reports the
10
+ // gesture via the direct `topRefresh` event, routed to the host onRefresh prop and turned into
11
+ // the typed Vue `refresh` emit.
14
12
  import { defineComponent, h } from '@vue/runtime-core';
15
13
  import { dlog } from '@symbiote-native/engine';
16
14
  import { resolveAccessibilityProps, } from '@symbiote-native/components';
17
15
  import { normalizeVueAttrs } from '../utils/normalize-attrs.js';
18
- // Copy every non-consumed attr into a bag typed as the a11y intersection (the accumulator is BUILT
19
- // at that type, a real narrowing, not a cast), then fold aria-*/role into the canonical
20
- // accessibility* props. Children cross via slots, never attrs. When the Android wrap re-invokes
21
- // this component, the injected `style` (the outer/layout half) rides through too.
16
+ // Built at the a11y-intersection type (a real narrowing, not a cast), then fold aria-*/role into
17
+ // the canonical accessibility* props. Children cross via slots, never attrs.
22
18
  function foldAttrs(attrs) {
23
19
  const bag = {};
24
20
  for (const key of Object.keys(attrs)) {
@@ -35,8 +31,6 @@ export const RefreshControl = defineComponent((_props, { attrs: rawAttrs, emit,
35
31
  if (nativeProps.enabled !== undefined)
36
32
  dlog(`RefreshControl enabled=${String(nativeProps.enabled)} (Android-only)`);
37
33
  dlog('RefreshControl refresh emit wired');
38
- // The default slot is the seam: empty on iOS (childless sibling), but the Android scroll-view
39
- // wrap re-invokes this component with the scroll view as its default slot, so it hosts it.
40
34
  return h('symbiote-refresh-control', {
41
35
  ...nativeProps,
42
36
  onRefresh: () => {
@@ -1,4 +1,4 @@
1
- export type { IScrollViewProps, IScrollViewEmits, IScrollViewHandle } from './shared';
1
+ export type { IScrollViewProps, IScrollViewEmits, IScrollViewHandle, } from './shared';
2
2
  export declare const ScrollView: import("vue").DefineSetupFnComponent<import("./shared").IScrollViewProps, import("./shared").IScrollViewEmits, {}, import("./shared").IScrollViewProps & {
3
3
  onContentSizeChange?: ((width: number, height: number) => any) | undefined;
4
4
  }, import("vue").PublicProps>;
@@ -1,15 +1,14 @@
1
- // ScrollView on Android. An Android ScrollView accepts only ONE child, so a
2
- // RefreshControl can't be a sibling of the content the way iOS allows ("addViewAt: failed to insert
3
- // view ... at index 1"). Instead the RefreshControl (AndroidSwipeRefreshLayout) WRAPS the scroll
4
- // view, with the scroll view nested inside and nestedScrollEnabled so the inner scroll handles the
5
- // gesture before the refresh parent, mirroring RN's ScrollView.js android branch.
1
+ // ScrollView on Android. An Android ScrollView accepts only ONE child, so a RefreshControl can't
2
+ // be a sibling of the content the way iOS allows ("addViewAt: failed to insert view ... at index
3
+ // 1"). Instead the RefreshControl (AndroidSwipeRefreshLayout) WRAPS the scroll view, nested
4
+ // inside with nestedScrollEnabled so it handles the gesture before the refresh parent, mirroring
5
+ // RN's ScrollView.js android branch.
6
6
  //
7
- // React does this with cloneElement(refreshControl, {style}, scrollView). Vue has NO cloneElement,
8
- // so the analog is to RE-INVOKE the user's RefreshControl component type via h(): same .type, its
9
- // own .props plus the injected outer/layout style, and the inner scroll view as its DEFAULT SLOT.
10
- // The slot is the seam: RefreshControl renders slots.default() as the host node's children. The
11
- // node ref stays on the INNER scroll view (not the wrapper), so dispatchViewCommand targets it.
12
- // Metro picks this on an Android host; no Platform.OS read.
7
+ // React does this with cloneElement(refreshControl, {style}, scrollView). Vue has no
8
+ // cloneElement, so the analog is to RE-INVOKE the user's RefreshControl component type via h():
9
+ // same .type, its own .props plus the injected outer/layout style, and the inner scroll view as
10
+ // its DEFAULT SLOT. The node ref stays on the INNER scroll view (not the wrapper), so
11
+ // dispatchViewCommand targets it. Metro picks this file on an Android host; no Platform.OS read.
13
12
  // device-verify-pending: the wrap shape mirrors RN, proven on a real host by the absence of the
14
13
  // "addViewAt: failed to insert" crash.
15
14
  import { h, isVNode } from '@vue/runtime-core';
@@ -19,38 +18,32 @@ import { createScrollView } from './shared.js';
19
18
  function isRecord(value) {
20
19
  return typeof value === 'object' && value !== null && !Array.isArray(value);
21
20
  }
22
- // A Vue VNode's `.type` is VNodeTypes (string | Component | nested VNode | the Fragment/Text/…
23
- // symbol constants). Re-invoking via h() needs it narrowed to what h accepts (string | Component):
24
- // the symbol constants fail the typeof checks and a nested VNode is ruled out by isVNode. A type
25
- // predicate (not a plain return) sidesteps the assignability gap; refreshControl is always a real
26
- // component element in practice.
21
+ // Narrows VNodeTypes (string | Component | nested VNode | the Fragment/Text/... symbol
22
+ // constants) to what h() accepts. refreshControl is always a real component element in practice.
27
23
  function isHostType(type) {
28
24
  if (typeof type === 'string')
29
25
  return true;
30
- return ((typeof type === 'object' || typeof type === 'function') && type !== null && !isVNode(type));
26
+ return ((typeof type === 'object' || typeof type === 'function') &&
27
+ type !== null &&
28
+ !isVNode(type));
31
29
  }
32
30
  export const ScrollView = createScrollView({
33
31
  assemble: input => {
34
32
  if (input.refreshControl === undefined) {
35
- // No refresh: the same single-child path as Phase 1; the node ref rides input.scrollProps.
36
33
  dlog('Vue ScrollView.ANDROID refreshControl=NONE(1child)');
37
34
  return h(input.scrollViewIntrinsic, input.scrollProps, [input.content]);
38
35
  }
39
- // RN splits the flattened style across the two boxes (splitLayoutProps): LAYOUT props
40
- // (margin/flex/size/position/…) drive the outer AndroidSwipeRefreshLayout frame; VISUAL props
41
- // (background/padding/border/…) paint the inner scroll view. So the wrapper carries `outer`, and
42
- // the inner scroll view its base (flexDirection/overflow) plus the visual `inner` composed over
43
- // it, NOT a hardcoded flex:1 that would override an explicit user height/width.
36
+ // RN splits the flattened style across the two boxes: LAYOUT props (margin/flex/size/
37
+ // position/...) drive the outer AndroidSwipeRefreshLayout frame; VISUAL props (background/
38
+ // padding/border/...) paint the inner scroll view - not a hardcoded flex:1 that would
39
+ // override an explicit user height/width.
44
40
  //
45
- // layoutSplitStyle (not userStyle): a class-only layout prop (flex/height/gap/…) is invisible
46
- // to userStyle (it never carries the resolved `class` value — see isClassNameProp in
47
- // shared.ts), so splitting on userStyle alone starves the wrapper of its layout style and it
48
- // collapses to nothing. layoutSplitStyle is userStyle merged with the resolved class style.
41
+ // layoutSplitStyle (not userStyle): a class-only layout prop is invisible to userStyle (it
42
+ // never carries the resolved `class` value - see isClassNameProp in shared.ts), so splitting
43
+ // on userStyle alone would starve the wrapper of its layout style.
49
44
  const { outer, inner } = splitLayoutProps(input.layoutSplitStyle);
50
- // `class` is stripped from the spread here: layoutSplitStyle already folded its resolved
51
- // value into `outer`/`inner` above, so forwarding the raw prop too would re-apply its LAYOUT
52
- // half onto the inner scroll view a second time (on top of the outer wrapper), the same
53
- // wrapper/inner leak `style` already avoids by living in HANDLED_ATTRS.
45
+ // `class` is stripped here: layoutSplitStyle already folded its resolved value into
46
+ // outer/inner above, so forwarding the raw prop too would re-apply its LAYOUT half a second time.
54
47
  const { class: _classAppliedViaSplit, ...innerScrollOuterProps } = input.scrollOuterProps;
55
48
  const innerScrollView = h(input.scrollViewIntrinsic, {
56
49
  ...innerScrollOuterProps,
@@ -60,15 +53,12 @@ export const ScrollView = createScrollView({
60
53
  }, [input.content]);
61
54
  const rc = input.refreshControl;
62
55
  if (!isHostType(rc.type)) {
63
- // refreshControl is always a component VNode; a symbol/nested-VNode type can't host children.
64
56
  // Degrade to the unwrapped scroll view rather than crash (the node ref is already on it).
65
57
  dlog('Vue ScrollView.ANDROID refreshControl has no hostable type, rendering scroll view unwrapped');
66
58
  return innerScrollView;
67
59
  }
68
60
  const rcProps = isRecord(rc.props) ? rc.props : {};
69
61
  dlog('Vue ScrollView.ANDROID refreshControl=WRAP');
70
- // Re-invoke the user's RefreshControl: same type, its own props + the outer/layout style, and the
71
- // inner scroll view as the default slot (the Vue analog of cloneElement(refreshControl, {style}, sv)).
72
62
  return h(rc.type, { ...rcProps, style: outer }, { default: () => [innerScrollView] });
73
63
  },
74
64
  });
@@ -1,4 +1,4 @@
1
- export type { IScrollViewProps, IScrollViewEmits, IScrollViewHandle } from './shared';
1
+ export type { IScrollViewProps, IScrollViewEmits, IScrollViewHandle, } from './shared';
2
2
  export declare const ScrollView: import("vue").DefineSetupFnComponent<import("./shared").IScrollViewProps, import("./shared").IScrollViewEmits, {}, import("./shared").IScrollViewProps & {
3
3
  onContentSizeChange?: ((width: number, height: number) => any) | undefined;
4
4
  }, import("vue").PublicProps>;
@@ -9,7 +9,9 @@ export const ScrollView = createScrollView({
9
9
  assemble: input => {
10
10
  // Sibling placement: RefreshControl before content (RN iOS). The node ref stays on the scroll
11
11
  // view via input.scrollProps, so the imperative handle targets it whether or not refresh is on.
12
- const children = input.refreshControl === undefined ? [input.content] : [input.refreshControl, input.content];
12
+ const children = input.refreshControl === undefined
13
+ ? [input.content]
14
+ : [input.refreshControl, input.content];
13
15
  return h(input.scrollViewIntrinsic, input.scrollProps, children);
14
16
  },
15
17
  });
@@ -1,48 +1,34 @@
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';
24
- import { wrapStickyHeaders } from './sticky-header.js';
18
+ import { wrapStickyHeaders, } from './sticky-header.js';
25
19
  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
- return Array.isArray(value) && value.every(entry => typeof entry === 'number');
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
- return typeof value === 'function' || (typeof value === 'object' && value !== null);
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,22 @@ 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
- const classProp = isClassNameProp(attrs.class) ? attrs.class : undefined;
163
- 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.
129
+ const classProp = isClassNameProp(attrs.class)
130
+ ? attrs.class
131
+ : undefined;
132
+ const layoutSplitStyle = [
133
+ resolveClassName(classProp),
134
+ userStyle,
135
+ ];
166
136
  const contentContainerStyle = typeof attrs.contentContainerStyle === 'string'
167
137
  ? resolveClassName(attrs.contentContainerStyle)
168
138
  : isStyleProp(attrs.contentContainerStyle)
169
139
  ? attrs.contentContainerStyle
170
140
  : 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
141
  const stickyHeaderIndices = isNumberArray(attrs.stickyHeaderIndices)
175
142
  ? attrs.stickyHeaderIndices
176
143
  : undefined;
@@ -179,28 +146,30 @@ export function createScrollView(platform) {
179
146
  const stickyHeaderComponent = isComponent(attrs.StickyHeaderComponent)
180
147
  ? attrs.StickyHeaderComponent
181
148
  : undefined;
182
- 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.
149
+ const { scrollViewIntrinsic, contentIntrinsic, scrollViewBaseStyle, contentStyle, } = selectScrollIntrinsics(isHorizontal, contentContainerStyle);
150
+ // RN defaults nested scrolling ON (ScrollView.js `nestedScrollEnabled ?? true`);
151
+ // horizontal forwards only when defined (load-bearing on iOS's RCTScrollView axis).
187
152
  const outerProps = {
188
153
  ...resolveAccessibilityProps(forwardAttrs(attrs)),
189
154
  };
190
155
  outerProps.nestedScrollEnabled =
191
- typeof attrs.nestedScrollEnabled === 'boolean' ? attrs.nestedScrollEnabled : true;
156
+ typeof attrs.nestedScrollEnabled === 'boolean'
157
+ ? attrs.nestedScrollEnabled
158
+ : true;
192
159
  if (attrs.horizontal !== undefined)
193
160
  outerProps.horizontal = attrs.horizontal;
194
161
  const decel = asDecelerationRate(attrs.decelerationRate);
195
162
  if (decel !== undefined)
196
163
  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).
164
+ // When sticky headers are active, the offset must reach the AnimatedValue (RN's
165
+ // _scrollAnimatedValueAttachment). forwardAttrs already put the user's onScroll/onLayout/
166
+ // scrollEventThrottle on outerProps; here we override per resolveScrollForwarding's
167
+ // decisions (which path, throttle default, inverted capture).
201
168
  const nativeStickyAvailable = hasStickyHeaders && isNativeAnimatedAvailable();
202
169
  nativeStickyWanted = nativeStickyAvailable;
203
- const userThrottle = typeof attrs.scrollEventThrottle === 'number' ? attrs.scrollEventThrottle : undefined;
170
+ const userThrottle = typeof attrs.scrollEventThrottle === 'number'
171
+ ? attrs.scrollEventThrottle
172
+ : undefined;
204
173
  const forwarding = resolveScrollForwarding({
205
174
  hasStickyHeaders,
206
175
  nativeStickyAvailable,
@@ -210,24 +179,29 @@ export function createScrollView(platform) {
210
179
  snapToAlignment: attrs.snapToAlignment,
211
180
  });
212
181
  if (hasStickyHeaders) {
213
- const userOnScroll = isHandler(attrs.onScroll) ? attrs.onScroll : undefined;
182
+ const userOnScroll = isHandler(attrs.onScroll)
183
+ ? attrs.onScroll
184
+ : undefined;
214
185
  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.
186
+ // JS fallback (no native module): correct, but lags a frame under fast scroll, which
187
+ // the native path removes on a real host.
218
188
  outerProps.onScroll = animatedEvent([{ nativeEvent: { contentOffset: { y: scrollAnimatedValue } } }], userOnScroll === undefined
219
189
  ? undefined
220
- : { listener: (...args) => forwardScrollEvent(userOnScroll, args) });
190
+ : {
191
+ listener: (...args) => forwardScrollEvent(userOnScroll, args),
192
+ });
221
193
  }
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.
194
+ // Native path: the value is driven on the UI thread by the post-commit watch above, so
195
+ // onScroll forwards untouched.
224
196
  if (forwarding.scrollEventThrottle !== undefined) {
225
197
  outerProps.scrollEventThrottle = forwarding.scrollEventThrottle;
226
198
  }
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.
199
+ // Capture the viewport height for inverted sticky headers (RN _handleLayout), then call
200
+ // the user's handler.
229
201
  if (forwarding.capturesViewportHeight) {
230
- const userOnLayout = isHandler(attrs.onLayout) ? attrs.onLayout : undefined;
202
+ const userOnLayout = isHandler(attrs.onLayout)
203
+ ? attrs.onLayout
204
+ : undefined;
231
205
  outerProps.onLayout = (event) => {
232
206
  const height = readLayoutDimension(event, 'height');
233
207
  if (height !== undefined)
@@ -238,18 +212,18 @@ export function createScrollView(platform) {
238
212
  }
239
213
  }
240
214
  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.
246
- const contentProps = { style: contentStyle, collapsable: false };
215
+ // `collapsable: false` keeps the layout-only content view as a real native view: Android
216
+ // Fabric view-flattens it away otherwise, hoisting the cells as direct children of the
217
+ // scroll view (which hosts exactly one) - an addViewAt crash. iOS never flattens.
218
+ const contentProps = {
219
+ style: contentStyle,
220
+ collapsable: false,
221
+ };
247
222
  if (forwarding.collapsableChildren) {
248
223
  contentProps.collapsableChildren = false;
249
224
  }
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).
225
+ // Synthesized from the content view's own onLayout (RN _handleContentOnLayout); emit only
226
+ // on a real size change.
253
227
  contentProps.onLayout = (event) => {
254
228
  const width = readLayoutDimension(event, 'width');
255
229
  const height = readLayoutDimension(event, 'height');
@@ -261,24 +235,20 @@ export function createScrollView(platform) {
261
235
  dlog(`Vue ScrollView contentSizeChange ${width}x${height}`);
262
236
  emit('contentSizeChange', width, height);
263
237
  };
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
238
  const slotChildren = slots.default !== undefined ? slots.default() : [];
267
239
  const contentChildren = hasStickyHeaders
268
240
  ? wrapStickyHeaders(slotChildren, stickyHeaderIndices, scrollAnimatedValue, invertStickyHeaders, viewportHeight.value, stickyHeaderComponent, headerLayoutYs, onHeaderLayoutY)
269
241
  : slotChildren;
270
242
  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.
243
+ // Base style UNDER user style so an explicit user value (height, flexDirection) still wins.
273
244
  const scrollProps = {
274
245
  ...outerProps,
275
246
  style: [scrollViewBaseStyle, userStyle],
276
247
  ref: setNodeRef,
277
248
  };
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
- const refreshControl = isVNode(attrs.refreshControl) ? attrs.refreshControl : undefined;
249
+ const refreshControl = isVNode(attrs.refreshControl)
250
+ ? attrs.refreshControl
251
+ : undefined;
282
252
  return platform.assemble({
283
253
  scrollViewIntrinsic,
284
254
  scrollProps,