@symbiote-native/vue 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +142 -0
  3. package/build/components/activity-indicator/index.android.d.ts +4 -0
  4. package/build/components/activity-indicator/index.android.js +10 -0
  5. package/build/components/activity-indicator/index.d.ts +1 -0
  6. package/build/components/activity-indicator/index.ios.d.ts +4 -0
  7. package/build/components/activity-indicator/index.ios.js +10 -0
  8. package/build/components/activity-indicator/index.js +4 -0
  9. package/build/components/activity-indicator/shared.d.ts +8 -0
  10. package/build/components/activity-indicator/shared.js +48 -0
  11. package/build/components/button.d.ts +14 -0
  12. package/build/components/button.js +59 -0
  13. package/build/components/drawer-layout-android/index.android.d.ts +8 -0
  14. package/build/components/drawer-layout-android/index.android.js +144 -0
  15. package/build/components/drawer-layout-android/index.d.ts +8 -0
  16. package/build/components/drawer-layout-android/index.js +36 -0
  17. package/build/components/drawer-layout-android/shared.d.ts +18 -0
  18. package/build/components/drawer-layout-android/shared.js +6 -0
  19. package/build/components/flat-list/index.d.ts +88 -0
  20. package/build/components/flat-list/index.js +189 -0
  21. package/build/components/image-background.d.ts +9 -0
  22. package/build/components/image-background.js +114 -0
  23. package/build/components/image.d.ts +9 -0
  24. package/build/components/image.js +99 -0
  25. package/build/components/input-accessory-view.d.ts +10 -0
  26. package/build/components/input-accessory-view.js +41 -0
  27. package/build/components/keyboard-avoiding-view.d.ts +18 -0
  28. package/build/components/keyboard-avoiding-view.js +121 -0
  29. package/build/components/modal.d.ts +29 -0
  30. package/build/components/modal.js +134 -0
  31. package/build/components/pressable.d.ts +58 -0
  32. package/build/components/pressable.js +221 -0
  33. package/build/components/refresh-control.d.ts +20 -0
  34. package/build/components/refresh-control.js +53 -0
  35. package/build/components/safe-area-view.d.ts +8 -0
  36. package/build/components/safe-area-view.js +33 -0
  37. package/build/components/scroll-view/index.android.d.ts +4 -0
  38. package/build/components/scroll-view/index.android.js +74 -0
  39. package/build/components/scroll-view/index.d.ts +1 -0
  40. package/build/components/scroll-view/index.ios.d.ts +4 -0
  41. package/build/components/scroll-view/index.ios.js +15 -0
  42. package/build/components/scroll-view/index.js +4 -0
  43. package/build/components/scroll-view/shared.d.ts +96 -0
  44. package/build/components/scroll-view/shared.js +297 -0
  45. package/build/components/scroll-view/sticky-header.d.ts +7 -0
  46. package/build/components/scroll-view/sticky-header.js +195 -0
  47. package/build/components/section-list/index.d.ts +39 -0
  48. package/build/components/section-list/index.js +77 -0
  49. package/build/components/switch/index.android.d.ts +12 -0
  50. package/build/components/switch/index.android.js +13 -0
  51. package/build/components/switch/index.d.ts +2 -0
  52. package/build/components/switch/index.ios.d.ts +12 -0
  53. package/build/components/switch/index.ios.js +11 -0
  54. package/build/components/switch/index.js +5 -0
  55. package/build/components/switch/shared.d.ts +23 -0
  56. package/build/components/switch/shared.js +129 -0
  57. package/build/components/text-input/index.d.ts +24 -0
  58. package/build/components/text-input/index.js +257 -0
  59. package/build/components/touchable-native-feedback.d.ts +24 -0
  60. package/build/components/touchable-native-feedback.js +75 -0
  61. package/build/components/touchable.d.ts +44 -0
  62. package/build/components/touchable.js +150 -0
  63. package/build/components/virtualized-list/index.d.ts +121 -0
  64. package/build/components/virtualized-list/index.js +687 -0
  65. package/build/components/virtualized-section-list/index.d.ts +96 -0
  66. package/build/components/virtualized-section-list/index.js +160 -0
  67. package/build/components.d.ts +51 -0
  68. package/build/components.js +26 -0
  69. package/build/composables/use-color-scheme.d.ts +3 -0
  70. package/build/composables/use-color-scheme.js +24 -0
  71. package/build/composables/use-window-dimensions.d.ts +3 -0
  72. package/build/composables/use-window-dimensions.js +34 -0
  73. package/build/create-tunnel.d.ts +9 -0
  74. package/build/create-tunnel.js +37 -0
  75. package/build/descriptor-to-vue.d.ts +3 -0
  76. package/build/descriptor-to-vue.js +14 -0
  77. package/build/host-instance/index.d.ts +2 -0
  78. package/build/host-instance/index.js +34 -0
  79. package/build/index.d.ts +67 -0
  80. package/build/index.js +90 -0
  81. package/build/modules/animated/create-animated-component.d.ts +4 -0
  82. package/build/modules/animated/create-animated-component.js +169 -0
  83. package/build/modules/animated/index.d.ts +55 -0
  84. package/build/modules/animated/index.js +66 -0
  85. package/build/modules/app-registry/index.d.ts +7 -0
  86. package/build/modules/app-registry/index.js +32 -0
  87. package/build/modules/status-bar.d.ts +17 -0
  88. package/build/modules/status-bar.js +65 -0
  89. package/build/render.d.ts +7 -0
  90. package/build/render.js +43 -0
  91. package/build/renderer.d.ts +4 -0
  92. package/build/renderer.js +129 -0
  93. package/build/runtime-helpers.d.ts +28 -0
  94. package/build/runtime-helpers.js +68 -0
  95. package/build/utils/component-helpers.d.ts +2 -0
  96. package/build/utils/component-helpers.js +13 -0
  97. package/build/utils/model-binding.d.ts +4 -0
  98. package/build/utils/model-binding.js +17 -0
  99. package/build/utils/normalize-attrs.d.ts +1 -0
  100. package/build/utils/normalize-attrs.js +30 -0
  101. package/build/utils/slots-to-render-props.d.ts +4 -0
  102. package/build/utils/slots-to-render-props.js +18 -0
  103. package/metro-css-parser.cjs +12 -0
  104. package/package.json +54 -0
@@ -0,0 +1,195 @@
1
+ // Sticky headers: the Vue twin of adapters/react/src/scroll-view-sticky-header.tsx, the JS layer
2
+ // RN implements in ScrollView.js / ScrollViewStickyHeader.js.
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
11
+ // @symbiote-native/components (computeStickyInterpolation, ADR 0024); 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).
14
+ import { defineComponent, h, isVNode, ref, shallowRef, watch, watchEffect, markRaw, } from '@vue/runtime-core';
15
+ import { AnimatedValue, AnimatedInterpolation, Platform, dlog, } from '@symbiote-native/engine';
16
+ import { computeStickyInterpolation, nextStickyHeaderY, readLayoutNumber, stickyDebounceMs, STICKY_HEADER_Z_INDEX, } from '@symbiote-native/components';
17
+ import { Animated } from '../../modules/animated';
18
+ function isHandler(value) {
19
+ return typeof value === 'function';
20
+ }
21
+ function isRecord(value) {
22
+ return typeof value === 'object' && value !== null;
23
+ }
24
+ function isAnimatedValue(value) {
25
+ return value instanceof AnimatedValue;
26
+ }
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).
29
+ function readChildOnLayout(child) {
30
+ if (!isRecord(child.props))
31
+ return undefined;
32
+ const handler = child.props.onLayout;
33
+ return isHandler(handler) ? handler : undefined;
34
+ }
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).
42
+ export const ScrollViewStickyHeader = defineComponent({
43
+ name: 'ScrollViewStickyHeader',
44
+ inheritAttrs: false,
45
+ 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.
49
+ const scrollAnimatedValue = isAnimatedValue(attrs.scrollAnimatedValue)
50
+ ? attrs.scrollAnimatedValue
51
+ : markRaw(new AnimatedValue(0));
52
+ const measured = ref(false);
53
+ const layoutY = ref(0);
54
+ const layoutHeight = ref(0);
55
+ // The animated node that drives the transform (RN's animatedTranslateY). When the scroll value
56
+ // is native (attachStickyScroll), this interpolation runs on the UI thread: the smooth pin.
57
+ // Engine node → shallowRef (held by identity, the reactivity rule); the un-measured identity
58
+ // stub until the effect below rebuilds it. See .claude/skills/vue-adapter-reactivity.
59
+ const animatedTranslateY = shallowRef(scrollAnimatedValue.interpolate({ inputRange: [-1, 0], outputRange: [0, 0] }));
60
+ // The debounced EXPLICIT translateY pushed to the committed transform via
61
+ // passthroughAnimatedPropExplicitValues, so the Fabric ShadowTree (hit-testing) knows the pinned
62
+ // position while the native driver animates. null until the listener first fires.
63
+ const translateY = ref(null);
64
+ let haveReceivedInitialZeroTranslateY = true;
65
+ let debounceTimer;
66
+ watch(translateY, value => {
67
+ if (value !== 0 && value !== null)
68
+ haveReceivedInitialZeroTranslateY = false;
69
+ });
70
+ // The animated value updates several times per frame during scroll; debounce it and push the
71
+ // settled value into the committed transform so hit detection stays current (RN: a Fabric-only
72
+ // issue; symbiote is always Fabric, and worse on Android).
73
+ const animatedValueListener = ({ value }) => {
74
+ if (typeof value !== 'number')
75
+ return;
76
+ const timeout = stickyDebounceMs(Platform.OS);
77
+ // A freshly-rebuilt interpolation re-emits 0 to its listeners; swallow that first zero (RN).
78
+ if (value === 0 && !haveReceivedInitialZeroTranslateY) {
79
+ haveReceivedInitialZeroTranslateY = true;
80
+ return;
81
+ }
82
+ if (debounceTimer !== undefined)
83
+ clearTimeout(debounceTimer);
84
+ debounceTimer = setTimeout(() => {
85
+ translateY.value = value;
86
+ }, timeout);
87
+ };
88
+ // Rebuild the interpolation whenever the layout / collision inputs change (the Vue twin of RN's
89
+ // effect, deps [measured, layoutY, layoutHeight, scrollViewHeight, nextHeaderLayoutY, inverted]).
90
+ // symbiote is always Fabric: listen to the settled value to keep the ShadowTree transform current
91
+ // for hit-testing (RN attaches this listener only under Fabric). scrollAnimatedValue is the stable
92
+ // const (RN lists it in deps; it never changes for one ScrollView, so the const is faithful).
93
+ watchEffect(onCleanup => {
94
+ const inverted = typeof attrs.inverted === 'boolean' ? attrs.inverted : undefined;
95
+ const scrollViewHeight = typeof attrs.scrollViewHeight === 'number' ? attrs.scrollViewHeight : undefined;
96
+ const nextHeaderLayoutY = typeof attrs.nextHeaderLayoutY === 'number' ? attrs.nextHeaderLayoutY : undefined;
97
+ const { inputRange, outputRange } = computeStickyInterpolation({
98
+ measured: measured.value,
99
+ inverted,
100
+ scrollViewHeight,
101
+ layoutY: layoutY.value,
102
+ layoutHeight: layoutHeight.value,
103
+ nextHeaderLayoutY,
104
+ });
105
+ const interpolation = scrollAnimatedValue.interpolate({ inputRange, outputRange });
106
+ const listenerId = interpolation.addListener(animatedValueListener);
107
+ animatedTranslateY.value = interpolation;
108
+ onCleanup(() => {
109
+ interpolation.removeListener(listenerId);
110
+ if (debounceTimer !== undefined)
111
+ clearTimeout(debounceTimer);
112
+ });
113
+ });
114
+ // Stable function ref (defined once, not re-created per render): record own y/height, mark
115
+ // measured, fire the wrapper's recorder (onHeaderLayoutY, in attrs.onLayout), then the child's
116
+ // own onLayout. Matches RN ScrollViewStickyHeader.js._onLayout order.
117
+ const onLayout = (event) => {
118
+ const y = readLayoutNumber(event, 'y');
119
+ const height = readLayoutNumber(event, 'height');
120
+ if (y !== undefined)
121
+ layoutY.value = y;
122
+ if (height !== undefined)
123
+ layoutHeight.value = height;
124
+ measured.value = true;
125
+ const recorder = attrs.onLayout;
126
+ if (isHandler(recorder))
127
+ recorder(event);
128
+ const children = slots.default !== undefined ? slots.default() : [];
129
+ const child = children[0];
130
+ const childOnLayout = isVNode(child) ? readChildOnLayout(child) : undefined;
131
+ if (childOnLayout !== undefined)
132
+ childOnLayout(event);
133
+ };
134
+ return () => {
135
+ // The EXPLICIT debounced translateY overrides the committed transform for hit-testing, while
136
+ // `animatedTranslateY` does the smooth (native-driven) pin. See RN ScrollViewStickyHeader.js.
137
+ const passthroughAnimatedPropExplicitValues = translateY.value !== null
138
+ ? { style: { transform: [{ translateY: translateY.value }] } }
139
+ : null;
140
+ // collapsable:false keeps the wrapper a real Yoga node; zIndex makes the pinned header paint
141
+ // OVER the rows scrolling under it. The interpolation node passes inside the transform; the
142
+ // Animated wrapper's reduceProps rasterizes it into a numeric translateY for the committed tree.
143
+ return h(Animated.View, {
144
+ style: {
145
+ transform: [{ translateY: animatedTranslateY.value }],
146
+ zIndex: STICKY_HEADER_Z_INDEX,
147
+ },
148
+ onLayout,
149
+ collapsable: false,
150
+ passthroughAnimatedPropExplicitValues,
151
+ }, { default: () => (slots.default !== undefined ? slots.default() : []) });
152
+ };
153
+ },
154
+ });
155
+ // Wrap each child flagged by `stickyHeaderIndices` in the sticky header component, fed by the
156
+ // shared scroll AnimatedValue. Mirrors ScrollView.js's render-time children.map (~line 1690) and
157
+ // the React adapter's wrapStickyHeaders. Returns the children unchanged when no indices are flagged.
158
+ //
159
+ // Cross-talk plumbing (RN's _headerLayoutYs + _onStickyHeaderLayout, ScrollView.js:1115-1143):
160
+ // `headerLayoutYs` is a child-index→measured-y map the parent keeps; each header reports its own y
161
+ // through `onHeaderLayoutY` as it measures, and we feed every header the y of the NEXT flagged
162
+ // header (the collision point past which it scrolls off) by looking up its successor's index in
163
+ // `stickyHeaderIndices`. The LAST flagged header has no successor, so its `nextHeaderLayoutY` stays
164
+ // undefined and it sticks indefinitely (correct).
165
+ export function wrapStickyHeaders(children, stickyHeaderIndices, scrollAnimatedValue, invertStickyHeaders, scrollViewHeight, StickyHeaderComponent, headerLayoutYs, onHeaderLayoutY) {
166
+ if (stickyHeaderIndices === undefined || stickyHeaderIndices.length === 0)
167
+ return children;
168
+ const Wrapper = StickyHeaderComponent ?? ScrollViewStickyHeader;
169
+ return children.map((child, index) => {
170
+ const indexOfIndex = stickyHeaderIndices.indexOf(index);
171
+ if (indexOfIndex === -1 || !isVNode(child))
172
+ return child;
173
+ // The next flagged header's measured y, by index order in stickyHeaderIndices (RN
174
+ // ScrollView.js:1695 nextIndex). undefined until that header has measured (or for the last).
175
+ const nextIndex = stickyHeaderIndices[indexOfIndex + 1];
176
+ const nextHeaderLayoutY = nextStickyHeaderY(stickyHeaderIndices, indexOfIndex, headerLayoutYs);
177
+ dlog(`Vue ScrollView sticky-header wrap index=${index} next=${nextIndex} nextY=${nextHeaderLayoutY}`);
178
+ const props = {
179
+ key: child.key ?? `sticky-${index}`,
180
+ nextHeaderLayoutY,
181
+ // RN _onStickyHeaderLayout: record this header's own y, then push it to the previous header as
182
+ // its nextHeaderLayoutY. We record into the parent map; the lookup above feeds it forward on
183
+ // the resulting re-render (the headerLayoutYs bump in scroll-view-shared).
184
+ onLayout: (event) => {
185
+ const y = readLayoutNumber(event, 'y');
186
+ if (y !== undefined)
187
+ onHeaderLayoutY(index, y);
188
+ },
189
+ scrollAnimatedValue,
190
+ inverted: invertStickyHeaders,
191
+ scrollViewHeight,
192
+ };
193
+ return h(Wrapper, props, { default: () => [child] });
194
+ });
195
+ }
@@ -0,0 +1,39 @@
1
+ import type { IVirtualizedSectionListHandle } from '@symbiote-native/components';
2
+ import { type IVirtualizedSectionListEmits, type IVirtualizedSectionListProps, type IVirtualizedSectionListSlots } from '../virtualized-section-list';
3
+ export type { ISection } from '../virtualized-section-list';
4
+ export type ISectionListHandle = IVirtualizedSectionListHandle;
5
+ export type ISectionListProps<ItemT> = IVirtualizedSectionListProps<ItemT>;
6
+ export type ISectionListEmits = IVirtualizedSectionListEmits;
7
+ export type ISectionListSlots<ItemT> = IVirtualizedSectionListSlots<ItemT>;
8
+ export declare const SectionList: new <ItemT>(props: (ISectionListProps<ItemT> & ({} | {
9
+ onRefresh?: (() => any) | undefined;
10
+ onEndReached?: ((info: {
11
+ distanceFromEnd: number;
12
+ }) => any) | undefined;
13
+ onStartReached?: ((info: {
14
+ distanceFromStart: number;
15
+ }) => any) | undefined;
16
+ })) & import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps) => import("vue").CreateComponentPublicInstanceWithMixins<ISectionListProps<ItemT> & ({} | {
17
+ onRefresh?: (() => any) | undefined;
18
+ onEndReached?: ((info: {
19
+ distanceFromEnd: number;
20
+ }) => any) | undefined;
21
+ onStartReached?: ((info: {
22
+ distanceFromStart: number;
23
+ }) => any) | undefined;
24
+ }), {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} | IVirtualizedSectionListEmits, import("vue").PublicProps, {}, false, {}, import("vue").SlotsType<ISectionListSlots<ItemT>>, {}, {}, string, {}, any, import("vue").ComponentProvideOptions, {
25
+ P: {};
26
+ B: {};
27
+ D: {};
28
+ C: {};
29
+ M: {};
30
+ Defaults: {};
31
+ }, (ISectionListProps<ItemT> & {}) | (ISectionListProps<ItemT> & {
32
+ onRefresh?: (() => any) | undefined;
33
+ onEndReached?: ((info: {
34
+ distanceFromEnd: number;
35
+ }) => any) | undefined;
36
+ onStartReached?: ((info: {
37
+ distanceFromStart: number;
38
+ }) => any) | undefined;
39
+ }), {}, {}, {}, {}, {}>;
@@ -0,0 +1,77 @@
1
+ // SectionList, the Vue public list-of-sections component. A thin wrapper over
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.
7
+ //
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.
14
+ import { defineComponent, getCurrentInstance, h, shallowRef, } from '@vue/runtime-core';
15
+ import { VirtualizedSectionList, } from '../virtualized-section-list';
16
+ import { normalizeVueAttrs } from '../../utils/normalize-attrs';
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).
19
+ const VirtualizedSectionListHost = VirtualizedSectionList;
20
+ function isRecord(value) {
21
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
22
+ }
23
+ function isSectionHandle(value) {
24
+ return isRecord(value) && typeof value.scrollToLocation === 'function';
25
+ }
26
+ const EMIT_KEYS = ['endReached', 'startReached', 'refresh'];
27
+ function buildDelegate(getInner) {
28
+ return {
29
+ scrollToLocation: params => getInner()?.scrollToLocation(params),
30
+ flashScrollIndicators: () => getInner()?.flashScrollIndicators(),
31
+ getNativeScrollRef: () => getInner()?.getNativeScrollRef() ?? null,
32
+ getScrollableNode: () => getInner()?.getScrollableNode() ?? null,
33
+ getScrollResponder: () => getInner()?.getScrollResponder() ?? null,
34
+ getScrollNode: () => getInner()?.getScrollNode() ?? null,
35
+ recordInteraction: () => getInner()?.recordInteraction(),
36
+ };
37
+ }
38
+ 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.
41
+ (_props, { attrs, expose, emit, slots }) => {
42
+ const inner = shallowRef(null);
43
+ const setInner = (instance) => {
44
+ inner.value = isSectionHandle(instance) ? instance : null;
45
+ };
46
+ 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.
50
+ const instance = getCurrentInstance();
51
+ const listens = (onName) => {
52
+ const vnodeProps = instance?.vnode.props;
53
+ return vnodeProps != null && typeof vnodeProps[onName] === 'function';
54
+ };
55
+ return () => {
56
+ const endReached = listens('onEndReached')
57
+ ? (info) => emit('endReached', info)
58
+ : undefined;
59
+ const startReached = listens('onStartReached')
60
+ ? (info) => emit('startReached', info)
61
+ : undefined;
62
+ 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
+ return h(VirtualizedSectionListHost, {
66
+ ...normalizeVueAttrs(attrs),
67
+ ref: setInner,
68
+ onEndReached: endReached,
69
+ onStartReached: startReached,
70
+ onRefresh: refresh,
71
+ }, slots);
72
+ };
73
+ }, {
74
+ name: 'SectionList',
75
+ inheritAttrs: false,
76
+ emits: EMIT_KEYS,
77
+ });
@@ -0,0 +1,12 @@
1
+ export declare const Switch: import("vue").DefineSetupFnComponent<import("./shared").ISwitchProps, {
2
+ valueChange: (value: boolean, event: import("@symbiote-native/engine").ISymbioteEvent) => boolean;
3
+ 'update:modelValue': (value: boolean) => boolean;
4
+ 'update:value': (value: boolean) => boolean;
5
+ }, {}, Omit<import("@symbiote-native/components").ISwitchProps, "onValueChange"> & {
6
+ modelValue?: boolean;
7
+ class?: import("@symbiote-native/engine").IClassNameValue;
8
+ } & {
9
+ "onUpdate:modelValue"?: ((value: boolean) => any) | undefined;
10
+ "onUpdate:value"?: ((value: boolean) => any) | undefined;
11
+ onValueChange?: ((value: boolean, event: import("@symbiote-native/engine").ISymbioteEvent) => any) | undefined;
12
+ }, import("vue").PublicProps>;
@@ -0,0 +1,13 @@
1
+ // Switch on Android: AndroidSwitch takes trackColorForTrue / trackColorForFalse plus
2
+ // trackTintColor (the color for the CURRENT value), and snaps native back via the
3
+ // `setNativeValue` command. Metro picks this on an Android host; no Platform.OS read.
4
+ // Mirrors the React adapter's Android binding. See ADR 0020.
5
+ import { createSwitch } from './shared';
6
+ export const Switch = createSwitch({
7
+ snapBackCommand: 'setNativeValue',
8
+ trackColorProps: (value, trackColor) => ({
9
+ trackColorForFalse: trackColor?.false,
10
+ trackColorForTrue: trackColor?.true,
11
+ trackTintColor: value ? trackColor?.true : trackColor?.false,
12
+ }),
13
+ });
@@ -0,0 +1,2 @@
1
+ export * from './index.ios';
2
+ export type { ISwitchProps, ISwitchTrackColor } from './shared';
@@ -0,0 +1,12 @@
1
+ export declare const Switch: import("vue").DefineSetupFnComponent<import("./shared").ISwitchProps, {
2
+ valueChange: (value: boolean, event: import("@symbiote-native/engine").ISymbioteEvent) => boolean;
3
+ 'update:modelValue': (value: boolean) => boolean;
4
+ 'update:value': (value: boolean) => boolean;
5
+ }, {}, Omit<import("@symbiote-native/components").ISwitchProps, "onValueChange"> & {
6
+ modelValue?: boolean;
7
+ class?: import("@symbiote-native/engine").IClassNameValue;
8
+ } & {
9
+ "onUpdate:modelValue"?: ((value: boolean) => any) | undefined;
10
+ "onUpdate:value"?: ((value: boolean) => any) | undefined;
11
+ onValueChange?: ((value: boolean, event: import("@symbiote-native/engine").ISymbioteEvent) => any) | undefined;
12
+ }, import("vue").PublicProps>;
@@ -0,0 +1,11 @@
1
+ // Switch on iOS: the native Switch takes onTintColor (ON-track) / tintColor (OFF-track) and
2
+ // snaps native back via the `setValue` command. Also the base (switch.ts re-exports it) for
3
+ // headless / web. Mirrors the React adapter's iOS binding. See ADR 0020.
4
+ import { createSwitch } from './shared';
5
+ export const Switch = createSwitch({
6
+ snapBackCommand: 'setValue',
7
+ trackColorProps: (_value, trackColor) => ({
8
+ onTintColor: trackColor?.true,
9
+ tintColor: trackColor?.false,
10
+ }),
11
+ });
@@ -0,0 +1,5 @@
1
+ // Base / default Switch. Re-exports the iOS build. Metro overrides this with switch.ios.ts /
2
+ // switch.android.ts on a real host; under tsx / tsc / web the host resolves here. Filename is
3
+ // the selector, no Platform.OS read. See ADR 0020. Switch is the first component to bring the
4
+ // state half (the reducer) into the Vue adapter: render-and-state, not render-only.
5
+ export * from './index.ios';
@@ -0,0 +1,23 @@
1
+ import { type ISwitchPlatform, type ISwitchProps as ISwitchBaseProps, type ISwitchTrackColor } from '@symbiote-native/components';
2
+ import { type IClassNameValue, type ISymbioteEvent } from '@symbiote-native/engine';
3
+ type ISwitchHostPlatform = ISwitchPlatform & {
4
+ snapBackCommand: string;
5
+ };
6
+ export type ISwitchProps = Omit<ISwitchBaseProps, 'onValueChange'> & {
7
+ modelValue?: boolean;
8
+ class?: IClassNameValue;
9
+ };
10
+ export type { ISwitchTrackColor };
11
+ type ISwitchEmits = {
12
+ valueChange: (value: boolean, event: ISymbioteEvent) => boolean;
13
+ 'update:modelValue': (value: boolean) => boolean;
14
+ 'update:value': (value: boolean) => boolean;
15
+ };
16
+ export declare function createSwitch(platform: ISwitchHostPlatform): import("vue").DefineSetupFnComponent<ISwitchProps, ISwitchEmits, {}, Omit<ISwitchBaseProps, "onValueChange"> & {
17
+ modelValue?: boolean;
18
+ class?: IClassNameValue;
19
+ } & {
20
+ "onUpdate:modelValue"?: ((value: boolean) => any) | undefined;
21
+ "onUpdate:value"?: ((value: boolean) => any) | undefined;
22
+ onValueChange?: ((value: boolean, event: ISymbioteEvent) => any) | undefined;
23
+ }, import("vue").PublicProps>;
@@ -0,0 +1,129 @@
1
+ // Switch, the Vue lifecycle half. The logic (the lastNativeReport reducer, valueFromChange,
2
+ // the snap-back decision) lives in @symbiote-native/components/state and the render in
3
+ // @symbiote-native/components/view, both shared verbatim with the React adapter. Here Vue supplies
4
+ // the reactivity: a ref holds what native last reported, a function ref grabs the host node,
5
+ // and a post-flush watch snaps native back when the parent rejects a toggle. This is the Vue
6
+ // twin of the React adapter's useReducer + useLayoutEffect + dispatchViewCommand.
7
+ //
8
+ // Inputs arrive as attrs (untyped), so each is narrowed with a runtime guard rather than a
9
+ // cast. onValueChange MUST be stripped from the forwarded attrs: it is not a ViewConfig event
10
+ // (it's derived from the native onChange), so leaking it would reach Fabric as a function prop
11
+ // and crash Android's folly::dynamic.
12
+ import { defineComponent, ref, shallowRef, watch } from '@vue/runtime-core';
13
+ import { renderSwitch, switchReducer, createInitialSwitchState, shouldSnapBack, valueFromChange, } from '@symbiote-native/components';
14
+ import { dispatchViewCommand, isSymbioteNode, dlog, } from '@symbiote-native/engine';
15
+ import { descriptorToVue } from '../../descriptor-to-vue';
16
+ import { normalizeVueAttrs } from '../../utils/normalize-attrs';
17
+ import { resolveModelValue, emitModelUpdate } from '../../utils/model-binding';
18
+ function isRecord(value) {
19
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
20
+ }
21
+ function asString(value) {
22
+ return typeof value === 'string' ? value : undefined;
23
+ }
24
+ function isBoolean(value) {
25
+ return typeof value === 'boolean';
26
+ }
27
+ // trackColor arrives untyped; keep only the string false/true entries the render fn reads.
28
+ function normalizeTrackColor(value) {
29
+ if (!isRecord(value))
30
+ return undefined;
31
+ const trackColor = {};
32
+ if (typeof value.false === 'string')
33
+ trackColor.false = value.false;
34
+ if (typeof value.true === 'string')
35
+ trackColor.true = value.true;
36
+ return trackColor;
37
+ }
38
+ // The render fn's style param is a plain object; array/registered styles degrade to undefined
39
+ // (the engine flattens those on its own). Same narrowing the Vue ActivityIndicator uses.
40
+ function isViewStyleObject(value) {
41
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
42
+ }
43
+ // The prop/handler keys the lifecycle consumes itself; everything else (accessibility,
44
+ // testID, …) forwards onto the host node. onValueChange is pure JS and must never reach Fabric.
45
+ const HANDLED_ATTRS = [
46
+ 'value',
47
+ 'modelValue',
48
+ 'disabled',
49
+ 'trackColor',
50
+ 'thumbColor',
51
+ 'ios_backgroundColor',
52
+ 'style',
53
+ 'onValueChange',
54
+ ];
55
+ function forwardAttrs(attrs) {
56
+ const result = {};
57
+ for (const key of Object.keys(attrs)) {
58
+ if (!HANDLED_ATTRS.includes(key))
59
+ result[key] = attrs[key];
60
+ }
61
+ return result;
62
+ }
63
+ export function createSwitch(platform) {
64
+ return defineComponent((_props, { attrs: rawAttrs, emit }) => {
65
+ // shallowRef, NOT ref: the engine node must be held by IDENTITY. A plain ref() runs the
66
+ // assigned object through Vue's toReactive(), so reading nodeRef.value back yields a
67
+ // reactive Proxy, a different object than the raw node. The engine's mirror is a WeakMap
68
+ // keyed on the raw node, so every imperative command (dispatchViewCommand / measure /
69
+ // setNativeProps) would miss on the Proxy and silently no-op. shallowRef stores the raw
70
+ // node untouched. General rule for this adapter: host nodes live in shallowRef / markRaw.
71
+ const nodeRef = shallowRef(null);
72
+ const state = ref(createInitialSwitchState());
73
+ // The host node our Vue renderer creates IS an engine SymbioteNode; Vue hands it to this
74
+ // function ref on mount. A stable function ref (defined once) isn't re-invoked per patch.
75
+ const setNodeRef = (el) => {
76
+ nodeRef.value = isSymbioteNode(el) ? el : null;
77
+ };
78
+ const handleChange = (event) => {
79
+ // Vue-facing event is emitted here; the host still receives this internal onChange
80
+ // handler so the controlled snap-back state stays synchronized with native.
81
+ const next = valueFromChange(event);
82
+ dlog(`Switch onChange value=${String(next)} eventCount=${String(event.nativeEvent.eventCount)}`);
83
+ if (next === undefined)
84
+ return;
85
+ emit('valueChange', next, event);
86
+ emitModelUpdate(emit, next);
87
+ state.value = switchReducer(state.value, { type: 'native-reported', value: next });
88
+ };
89
+ // Snap-back: when native reported a value the parent rejected (the value prop did not
90
+ // change), command the JS value back down: the controlled-Switch correction RN does via
91
+ // SwitchCommands.setValue. The decision is shared with React (shouldSnapBack); only the
92
+ // command name is platform-imperative. flush:'post' so the engine has committed the node
93
+ // before the command reads its Fabric handle.
94
+ watch(() => ({
95
+ fabricValue: resolveModelValue(rawAttrs, isBoolean) === true,
96
+ switchState: state.value,
97
+ }), ({ fabricValue, switchState }) => {
98
+ const node = nodeRef.value;
99
+ if (node === null)
100
+ return;
101
+ if (!shouldSnapBack(switchState, fabricValue)) {
102
+ dlog(`Switch snap-back no-op reported=${String(switchState.lastNativeReport)} value=${fabricValue}`);
103
+ return;
104
+ }
105
+ dlog(`Switch ${platform.snapBackCommand} snap-back reported=${String(switchState.lastNativeReport)} value=${fabricValue}`);
106
+ dispatchViewCommand(node, platform.snapBackCommand, [fabricValue]);
107
+ }, { flush: 'post' });
108
+ return () => {
109
+ const attrs = normalizeVueAttrs(rawAttrs);
110
+ return descriptorToVue(renderSwitch({
111
+ value: resolveModelValue(attrs, isBoolean) === true,
112
+ disabled: typeof attrs.disabled === 'boolean' ? attrs.disabled : undefined,
113
+ trackColor: normalizeTrackColor(attrs.trackColor),
114
+ thumbColor: asString(attrs.thumbColor),
115
+ ios_backgroundColor: asString(attrs.ios_backgroundColor),
116
+ style: isViewStyleObject(attrs.style) ? attrs.style : undefined,
117
+ passthrough: { ...forwardAttrs(attrs), ref: setNodeRef, onChange: handleChange },
118
+ }, platform));
119
+ };
120
+ }, {
121
+ name: 'Switch',
122
+ inheritAttrs: false,
123
+ emits: {
124
+ valueChange: (_value, _event) => true,
125
+ 'update:modelValue': (_value) => true,
126
+ 'update:value': (_value) => true,
127
+ },
128
+ });
129
+ }
@@ -0,0 +1,24 @@
1
+ import { type ITextInputProps as ITextInputBaseProps, type ITextInputHandle } from '@symbiote-native/components';
2
+ import { type IClassNameValue, type ISymbioteEvent } from '@symbiote-native/engine';
3
+ export type ITextInputProps = Omit<ITextInputBaseProps, 'onValueChange' | 'onFocus' | 'onBlur'> & {
4
+ modelValue?: string;
5
+ class?: IClassNameValue;
6
+ };
7
+ export type { ITextInputHandle };
8
+ type ITextInputEmits = {
9
+ valueChange: (text: string, event: ISymbioteEvent) => boolean;
10
+ focus: (event: ISymbioteEvent) => boolean;
11
+ blur: (event: ISymbioteEvent) => boolean;
12
+ 'update:modelValue': (text: string) => boolean;
13
+ 'update:value': (text: string) => boolean;
14
+ };
15
+ export declare const TextInput: import("vue").DefineSetupFnComponent<ITextInputProps, ITextInputEmits, {}, Omit<ITextInputBaseProps, "onFocus" | "onBlur" | "onValueChange"> & {
16
+ modelValue?: string;
17
+ class?: IClassNameValue;
18
+ } & {
19
+ "onUpdate:modelValue"?: ((text: string) => any) | undefined;
20
+ "onUpdate:value"?: ((text: string) => any) | undefined;
21
+ onValueChange?: ((text: string, event: ISymbioteEvent) => any) | undefined;
22
+ onFocus?: ((event: ISymbioteEvent) => any) | undefined;
23
+ onBlur?: ((event: ISymbioteEvent) => any) | undefined;
24
+ }, import("vue").PublicProps>;