@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,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,60 +22,59 @@ 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
- const animatedTranslateY = shallowRef(scrollAnimatedValue.interpolate({ inputRange: [-1, 0], outputRange: [0, 0] }));
55
+ const animatedTranslateY = shallowRef(scrollAnimatedValue.interpolate({
56
+ inputRange: [-1, 0],
57
+ outputRange: [0, 0],
58
+ }));
64
59
  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.
60
+ // Held so the next rebuild detaches the old listener and onBeforeUnmount cleans up.
67
61
  let interpolation;
68
62
  let listenerId;
69
63
  const inputs = () => ({
70
64
  os: Platform.OS,
71
65
  inverted: typeof attrs.inverted === 'boolean' ? attrs.inverted : undefined,
72
- scrollViewHeight: typeof attrs.scrollViewHeight === 'number' ? attrs.scrollViewHeight : undefined,
73
- nextHeaderLayoutY: typeof attrs.nextHeaderLayoutY === 'number' ? attrs.nextHeaderLayoutY : undefined,
66
+ scrollViewHeight: typeof attrs.scrollViewHeight === 'number'
67
+ ? attrs.scrollViewHeight
68
+ : undefined,
69
+ nextHeaderLayoutY: typeof attrs.nextHeaderLayoutY === 'number'
70
+ ? attrs.nextHeaderLayoutY
71
+ : undefined,
74
72
  });
75
73
  const runEffects = (effects) => {
76
74
  for (const effect of effects) {
77
75
  switch (effect.kind) {
78
76
  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).
77
+ // Detach the old listener, build a fresh interpolation, and wire the settled-value listener.
81
78
  if (interpolation !== undefined && listenerId !== undefined) {
82
79
  interpolation.removeListener(listenerId);
83
80
  listenerId = undefined;
@@ -95,8 +92,8 @@ export const ScrollViewStickyHeader = defineComponent({
95
92
  break;
96
93
  }
97
94
  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).
95
+ // The animated value updates several times per frame; debounce the settled value so
96
+ // hit detection stays current (a Fabric issue, worse on Android).
100
97
  if (debounceTimer !== undefined)
101
98
  clearTimeout(debounceTimer);
102
99
  debounceTimer = setTimeout(() => {
@@ -108,8 +105,7 @@ export const ScrollViewStickyHeader = defineComponent({
108
105
  version.value += 1;
109
106
  break;
110
107
  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.
108
+ // Vue records through attrs.onLayout (the wrapper closure); the reducer emits no index for it.
113
109
  break;
114
110
  }
115
111
  }
@@ -117,9 +113,8 @@ export const ScrollViewStickyHeader = defineComponent({
117
113
  const dispatch = (action) => {
118
114
  runEffects(reduceSticky(state, action, inputs()).effects);
119
115
  };
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).
116
+ // Rebuild whenever the collision/viewport inputs change ([inverted, scrollViewHeight,
117
+ // nextHeaderLayoutY]); also runs once on mount.
123
118
  watchEffect(() => {
124
119
  dispatch({ kind: 'inputs-changed' });
125
120
  });
@@ -130,20 +125,25 @@ export const ScrollViewStickyHeader = defineComponent({
130
125
  if (debounceTimer !== undefined)
131
126
  clearTimeout(debounceTimer);
132
127
  });
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.
128
+ // Dispatch the layout, fire the wrapper's recorder (onHeaderLayoutY via attrs.onLayout), then
129
+ // the child's own onLayout. Matches RN ScrollViewStickyHeader.js._onLayout.
136
130
  const onLayout = (event) => {
137
131
  const y = readLayoutNumber(event, 'y');
138
132
  const height = readLayoutNumber(event, 'height');
139
133
  // Keep the previous value when a field is absent (RN sets state only on a defined read).
140
- dispatch({ kind: 'layout', y: y ?? state.layoutY, height: height ?? state.layoutHeight });
134
+ dispatch({
135
+ kind: 'layout',
136
+ y: y ?? state.layoutY,
137
+ height: height ?? state.layoutHeight,
138
+ });
141
139
  const recorder = attrs.onLayout;
142
140
  if (isHandler(recorder))
143
141
  recorder(event);
144
142
  const children = slots.default !== undefined ? slots.default() : [];
145
143
  const child = children[0];
146
- const childOnLayout = isVNode(child) ? readChildOnLayout(child) : undefined;
144
+ const childOnLayout = isVNode(child)
145
+ ? readChildOnLayout(child)
146
+ : undefined;
147
147
  if (childOnLayout !== undefined)
148
148
  childOnLayout(event);
149
149
  };
@@ -151,13 +151,12 @@ export const ScrollViewStickyHeader = defineComponent({
151
151
  // Read the version bump so a committed translateY re-runs render.
152
152
  void version.value;
153
153
  // The EXPLICIT debounced translateY overrides the committed transform for hit-testing, while
154
- // `animatedTranslateY` does the smooth (native-driven) pin. See RN ScrollViewStickyHeader.js.
154
+ // animatedTranslateY does the smooth (native-driven) pin.
155
155
  const passthroughAnimatedPropExplicitValues = state.translateY !== null
156
156
  ? { style: { transform: [{ translateY: state.translateY }] } }
157
157
  : null;
158
158
  // 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.
159
+ // OVER the rows scrolling under it.
161
160
  return h(Animated.View, {
162
161
  style: {
163
162
  transform: [{ translateY: animatedTranslateY.value }],
@@ -171,15 +170,13 @@ export const ScrollViewStickyHeader = defineComponent({
171
170
  },
172
171
  });
173
172
  // 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.
173
+ // shared scroll AnimatedValue. Returns the children unchanged when no indices are flagged.
176
174
  //
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).
175
+ // Cross-talk plumbing (RN's _headerLayoutYs + _onStickyHeaderLayout): headerLayoutYs is a
176
+ // child-index -> measured-y map; each header reports its own y through onHeaderLayoutY, and we
177
+ // feed every header the y of the NEXT flagged header (the collision point past which it scrolls
178
+ // off). The LAST flagged header has no successor, so its nextHeaderLayoutY stays undefined and
179
+ // it sticks indefinitely (correct).
183
180
  export function wrapStickyHeaders(children, stickyHeaderIndices, scrollAnimatedValue, invertStickyHeaders, scrollViewHeight, StickyHeaderComponent, headerLayoutYs, onHeaderLayoutY) {
184
181
  if (stickyHeaderIndices === undefined || stickyHeaderIndices.length === 0)
185
182
  return children;
@@ -188,17 +185,15 @@ export function wrapStickyHeaders(children, stickyHeaderIndices, scrollAnimatedV
188
185
  const indexOfIndex = stickyHeaderIndices.indexOf(index);
189
186
  if (indexOfIndex === -1 || !isVNode(child))
190
187
  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).
188
+ // The next flagged header's measured y. undefined until that header has measured (or for the last).
193
189
  const nextIndex = stickyHeaderIndices[indexOfIndex + 1];
194
190
  const nextHeaderLayoutY = nextStickyHeaderY(stickyHeaderIndices, indexOfIndex, headerLayoutYs);
195
191
  dlog(`Vue ScrollView sticky-header wrap index=${index} next=${nextIndex} nextY=${nextHeaderLayoutY}`);
196
192
  const props = {
197
193
  key: child.key ?? `sticky-${index}`,
198
194
  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).
195
+ // Record this header's own y into the parent map; the lookup above feeds it forward to the
196
+ // previous header on the resulting re-render (the headerLayoutYs bump in scroll-view-shared).
202
197
  onLayout: (event) => {
203
198
  const y = readLayoutNumber(event, 'y');
204
199
  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.
41
- (_props, { attrs, expose, emit, slots }) => {
36
+ // _props is read only by the type system, to let ItemT infer at the call site.
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;
@@ -59,9 +54,9 @@ export const SectionList = defineComponent(
59
54
  const startReached = listens('onStartReached')
60
55
  ? (info) => emit('startReached', info)
61
56
  : 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.
57
+ const refresh = listens('onRefresh')
58
+ ? () => emit('refresh')
59
+ : undefined;
65
60
  return h(VirtualizedSectionListHost, {
66
61
  ...normalizeVueAttrs(attrs),
67
62
  ref: setInner,
@@ -1,14 +1,12 @@
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.
1
+ // Switch, the Vue lifecycle half. The logic (lastNativeReport reducer, valueFromChange, the
2
+ // snap-back decision) lives in @symbiote-native/components/state and the render in
3
+ // @symbiote-native/components/view, both shared verbatim with React. Vue supplies the
4
+ // reactivity: a ref holds what native last reported, a function ref grabs the host node, and a
5
+ // post-flush watch snaps native back when the parent rejects a toggle.
7
6
  //
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.
7
+ // Inputs arrive as attrs (untyped), so each is narrowed with a runtime guard, never a cast.
8
+ // onValueChange MUST be stripped from the forwarded attrs: it is not a ViewConfig event, so
9
+ // leaking it would reach Fabric as a function prop and crash Android's folly::dynamic.
12
10
  import { defineComponent, ref, shallowRef, watch } from '@vue/runtime-core';
13
11
  import { renderSwitch, switchReducer, createInitialSwitchState, shouldSnapBack, valueFromChange, } from '@symbiote-native/components';
14
12
  import { dispatchViewCommand, isSymbioteNode, dlog, } from '@symbiote-native/engine';
@@ -24,7 +22,6 @@ function asString(value) {
24
22
  function isBoolean(value) {
25
23
  return typeof value === 'boolean';
26
24
  }
27
- // trackColor arrives untyped; keep only the string false/true entries the render fn reads.
28
25
  function normalizeTrackColor(value) {
29
26
  if (!isRecord(value))
30
27
  return undefined;
@@ -36,12 +33,11 @@ function normalizeTrackColor(value) {
36
33
  return trackColor;
37
34
  }
38
35
  // 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.
36
+ // (the engine flattens those on its own).
40
37
  function isViewStyleObject(value) {
41
38
  return typeof value === 'object' && value !== null && !Array.isArray(value);
42
39
  }
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.
40
+ // Everything else forwards onto the host node. onValueChange is pure JS and must never reach Fabric.
45
41
  const HANDLED_ATTRS = [
46
42
  'value',
47
43
  'modelValue',
@@ -62,35 +58,30 @@ function forwardAttrs(attrs) {
62
58
  }
63
59
  export function createSwitch(platform) {
64
60
  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.
61
+ // shallowRef, NOT ref: a plain ref() would hand back a reactive Proxy the engine's WeakMap
62
+ // mirror doesn't recognize, so every imperative command would silently no-op. General rule
63
+ // for this adapter: host nodes live in shallowRef / markRaw.
71
64
  const nodeRef = shallowRef(null);
72
65
  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
66
  const setNodeRef = (el) => {
76
67
  nodeRef.value = isSymbioteNode(el) ? el : null;
77
68
  };
78
69
  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
70
  const next = valueFromChange(event);
82
71
  dlog(`Switch onChange value=${String(next)} eventCount=${String(event.nativeEvent.eventCount)}`);
83
72
  if (next === undefined)
84
73
  return;
85
74
  emit('valueChange', next, event);
86
75
  emitModelUpdate(emit, next);
87
- state.value = switchReducer(state.value, { type: 'native-reported', value: next });
76
+ state.value = switchReducer(state.value, {
77
+ type: 'native-reported',
78
+ value: next,
79
+ });
88
80
  };
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.
81
+ // Snap-back: when native reported a value the parent rejected, command the JS value back
82
+ // down (the controlled-Switch correction RN does via SwitchCommands.setValue). The
83
+ // decision is shared with React (shouldSnapBack); only the command name is platform-
84
+ // imperative. flush:'post' so the node is committed before the command reads its Fabric handle.
94
85
  watch(() => ({
95
86
  fabricValue: resolveModelValue(rawAttrs, isBoolean) === true,
96
87
  switchState: state.value,
@@ -109,12 +100,18 @@ export function createSwitch(platform) {
109
100
  const attrs = normalizeVueAttrs(rawAttrs);
110
101
  return descriptorToVue(renderSwitch({
111
102
  value: resolveModelValue(attrs, isBoolean) === true,
112
- disabled: typeof attrs.disabled === 'boolean' ? attrs.disabled : undefined,
103
+ disabled: typeof attrs.disabled === 'boolean'
104
+ ? attrs.disabled
105
+ : undefined,
113
106
  trackColor: normalizeTrackColor(attrs.trackColor),
114
107
  thumbColor: asString(attrs.thumbColor),
115
108
  ios_backgroundColor: asString(attrs.ios_backgroundColor),
116
109
  style: isViewStyleObject(attrs.style) ? attrs.style : undefined,
117
- passthrough: { ...forwardAttrs(attrs), ref: setNodeRef, onChange: handleChange },
110
+ passthrough: {
111
+ ...forwardAttrs(attrs),
112
+ ref: setNodeRef,
113
+ onChange: handleChange,
114
+ },
118
115
  }, platform));
119
116
  };
120
117
  }, {
@@ -1,18 +1,14 @@
1
- // TextInput, the Vue lifecycle half. The folds/maps (value->text, the W3C/alias resolution)
2
- // and the controlled-write predicate live in @symbiote-native/components/state, the render (intrinsic
3
- // + native-prop mapping) in @symbiote-native/components/view, both shared verbatim with the React
4
- // adapter. Here Vue supplies only the reactivity: a shallowRef holds the host node, a ref holds
5
- // the acknowledged event count (so the exposed handle echoes the latest), setup-scope `let`s
6
- // hold the last native text + focus flag, a post-flush watch runs the controlled-write command,
7
- // and expose() wires the imperative handle. The Vue twin of the React useState/useRef +
8
- // useLayoutEffect + useImperativeHandle.
1
+ // TextInput, the Vue lifecycle half. The folds/maps (value->text, W3C/alias resolution) and the
2
+ // controlled-write predicate live in @symbiote-native/components/state, the render in
3
+ // @symbiote-native/components/view, both shared verbatim with React. Vue supplies only the
4
+ // reactivity: a shallowRef holds the host node, a ref holds the acknowledged event count, setup-
5
+ // scope `let`s hold the last native text + focus flag, a post-flush watch runs the
6
+ // controlled-write command, and expose() wires the imperative handle.
9
7
  //
10
- // Inputs arrive as attrs (untyped), so each is narrowed with a runtime guard rather than a cast.
11
- // onValueChange MUST be stripped from the forwarded attrs: it is not a ViewConfig event (it's
12
- // derived from the native onChange), so leaking it would reach Fabric as a function prop and
13
- // crash Android's folly::dynamic. The imperative module (blurTextInput / setInput*) is imported
14
- // from @symbiote-native/engine, the same framework-agnostic singleton both adapters share.
15
- import { defineComponent, onBeforeUnmount, ref, shallowRef, watch } from '@vue/runtime-core';
8
+ // Inputs arrive as attrs (untyped), so each is narrowed with a runtime guard, never a cast.
9
+ // onValueChange MUST be stripped from the forwarded attrs: it is not a ViewConfig event, so
10
+ // leaking it would reach Fabric as a function prop and crash Android's folly::dynamic.
11
+ import { defineComponent, onBeforeUnmount, ref, shallowRef, watch, } from '@vue/runtime-core';
16
12
  import { resolveAccessibilityProps, resolveTextInputProps, renderTextInput, foldText, textFromChange, eventCountFromChange, shouldCommandText, INITIAL_EVENT_COUNT, SELECTION_NONE, } from '@symbiote-native/components';
17
13
  import { dispatchViewCommand, isSymbioteNode, dlog, blurTextInput, setInputFocused, setInputBlurred, whenCommitted, } from '@symbiote-native/engine';
18
14
  import { descriptorToVue } from '../../descriptor-to-vue.js';
@@ -30,7 +26,6 @@ function isString(value) {
30
26
  function asBoolean(value) {
31
27
  return typeof value === 'boolean' ? value : undefined;
32
28
  }
33
- // selection arrives untyped; keep only the numeric start/end the render fn + controlled write read.
34
29
  function normalizeSelection(value) {
35
30
  if (!isRecord(value))
36
31
  return undefined;
@@ -41,9 +36,7 @@ function normalizeSelection(value) {
41
36
  selection.end = value.end;
42
37
  return selection;
43
38
  }
44
- // The prop/handler keys the lifecycle consumes itself (mirrors the React adapter's destructure);
45
- // everything else (placeholder, secureTextEntry, the remaining native events, accessibility,
46
- // testID, style…) forwards onto the host node. onFocus/onBlur are re-supplied as our wrapped
39
+ // Everything else forwards onto the host node. onFocus/onBlur are re-supplied as our wrapped
47
40
  // handlers; onValueChange is pure JS and must never reach Fabric.
48
41
  const HANDLED_ATTRS = [
49
42
  'value',
@@ -80,38 +73,30 @@ function forwardAttrs(attrs) {
80
73
  return result;
81
74
  }
82
75
  export const TextInput = defineComponent((_props, { attrs: rawAttrs, emit, expose }) => {
83
- // shallowRef, NOT ref: the engine node must be held by IDENTITY. A plain ref() runs the node
84
- // through Vue's toReactive(), handing back a reactive Proxy, a different object than the raw
85
- // node the engine's WeakMap mirror is keyed on, so every imperative command
86
- // (dispatchViewCommand / focus / blur / setTextAndSelection) would miss and silently no-op.
87
- // Same rule applies to the Switch / ScrollView host node.
76
+ // shallowRef, NOT ref: a plain ref() would hand back a reactive Proxy the engine's WeakMap
77
+ // mirror doesn't recognize, so every imperative command would silently no-op. Same rule as
78
+ // the Switch / ScrollView host node.
88
79
  const nodeRef = shallowRef(null);
89
80
  const setNodeRef = (el) => {
90
81
  nodeRef.value = isSymbioteNode(el) ? el : null;
91
82
  };
92
- // The count native last acknowledged. A ref so the render echoes it back as
93
- // mostRecentEventCount and the exposed handle reads the latest; the controlled write commands
94
- // it so native's eventLag lands on 0.
83
+ // The count native last acknowledged. A ref so render echoes it as mostRecentEventCount; the
84
+ // controlled write commands it so native's eventLag lands on 0.
95
85
  const mostRecentEventCount = ref(INITIAL_EVENT_COUNT);
96
- // The last text native holds, as far as JS knows. Seeded from the mount-time value (the `text`
97
- // prop already carries it down via createNode, so the FIRST controlled value is not a
98
- // divergence and must NOT re-command). A setup-scope `let`: no render needed when it changes.
86
+ // Seeded from the mount-time value (the `text` prop already carries it down via createNode,
87
+ // so the FIRST controlled value is not a divergence and must NOT re-command).
99
88
  let lastNativeText = foldText(resolveModelValue(rawAttrs, isString), asString(rawAttrs.defaultValue));
100
- // JS-side focus state, mirrored from the focus/blur events for isFocused(): native exposes no
101
- // synchronous focus getter (RN's TextInputState holds the same).
89
+ // Mirrored from focus/blur events for isFocused(): native exposes no synchronous focus getter.
102
90
  let focused = false;
103
- // autoFocus fires once when the node first commits; guard so a later node identity change does
104
- // not re-focus.
91
+ // Guard so a later node identity change does not re-focus.
105
92
  let autoFocused = false;
106
93
  const handleChange = (event) => {
107
- // Event seam: the controlled handshake hinges on the change payload carrying `text`
108
- // (+ `eventCount`). iOS and Android Fabric can key these differently, so log the actual shape.
94
+ // iOS and Android Fabric can key the change payload differently, so log the actual shape.
109
95
  dlog(`TextInput change keys=[${Object.keys(event.nativeEvent).join(',')}] ` +
110
96
  `text=${JSON.stringify(event.nativeEvent.text)} count=${JSON.stringify(event.nativeEvent.eventCount)}`);
111
97
  const text = textFromChange(event);
112
98
  if (text !== undefined) {
113
- // Record the text first, then the count, so the count never runs ahead of the text it
114
- // stands for.
99
+ // Record the text first, so the count never runs ahead of the text it stands for.
115
100
  lastNativeText = text;
116
101
  emit('valueChange', text, event);
117
102
  emitModelUpdate(emit, text);
@@ -136,10 +121,8 @@ export const TextInput = defineComponent((_props, { attrs: rawAttrs, emit, expos
136
121
  emit('blur', event);
137
122
  };
138
123
  // Controlled write: when JS-side `value` diverges from what native reported, command the new
139
- // text down with the acknowledged count; a plain prop re-push would race the user's
140
- // keystrokes. Watching `value` covers every divergence (the parent only rewrites `value` after
141
- // a change). flush:'post' so the engine has committed the node before the command reads its
142
- // Fabric handle; the predicate makes it a no-op on mount (value === the seed).
124
+ // text down with the acknowledged count - a plain prop re-push would race the user's
125
+ // keystrokes. flush:'post' so the node is committed before the command reads its Fabric handle.
143
126
  watch(() => resolveModelValue(rawAttrs, isString), value => {
144
127
  const node = nodeRef.value;
145
128
  if (node === null)
@@ -158,11 +141,10 @@ export const TextInput = defineComponent((_props, { attrs: rawAttrs, emit, expos
158
141
  ]);
159
142
  lastNativeText = value;
160
143
  }, { flush: 'post' });
161
- // autoFocus is driven in JS, not as a native prop: once the node first commits, command `focus`
162
- // down once (RN does the same via TextInputState.focusInput). The watch fires when nodeRef is
163
- // set (mount), but under Vue's async-batched commit the node has no Fabric tag yet at post-flush,
164
- // so the focus command would be skipped with no retry. whenCommitted defers it to the commit that
165
- // assigns the tag. Cancelled on unmount so an un-committed pending focus can't leak.
144
+ // autoFocus is driven in JS: once the node first commits, command `focus` down once. Under
145
+ // Vue's async-batched commit the node has no Fabric tag yet at post-flush, so
146
+ // whenCommitted defers it to the commit that assigns the tag. Cancelled on unmount so an
147
+ // un-committed pending focus can't leak.
166
148
  let cancelAutoFocus;
167
149
  watch(nodeRef, node => {
168
150
  if (autoFocused || node === null || rawAttrs.autoFocus !== true)
@@ -172,10 +154,8 @@ export const TextInput = defineComponent((_props, { attrs: rawAttrs, emit, expos
172
154
  cancelAutoFocus = whenCommitted(node, () => dispatchViewCommand(node, 'focus', []));
173
155
  }, { flush: 'post' });
174
156
  onBeforeUnmount(() => cancelAutoFocus?.());
175
- // The imperative API RN exposes on the ref. The methods read nodeRef.value / the count LIVE,
176
- // so no stale capture: the Vue twin of React's useImperativeHandle. focus/blur drive native
177
- // view commands; clear and setSelection reuse setTextAndSelection (the same stale-safe path as
178
- // a controlled write) echoing the acknowledged event count.
157
+ // Methods read nodeRef.value / the count LIVE, so no stale capture. clear/setSelection reuse
158
+ // setTextAndSelection, the same path as a controlled write.
179
159
  expose({
180
160
  focus: () => {
181
161
  const node = nodeRef.value;
@@ -190,7 +170,12 @@ export const TextInput = defineComponent((_props, { attrs: rawAttrs, emit, expos
190
170
  const node = nodeRef.value;
191
171
  if (node === null)
192
172
  return;
193
- dispatchViewCommand(node, 'setTextAndSelection', [mostRecentEventCount.value, '', 0, 0]);
173
+ dispatchViewCommand(node, 'setTextAndSelection', [
174
+ mostRecentEventCount.value,
175
+ '',
176
+ 0,
177
+ 0,
178
+ ]);
194
179
  lastNativeText = '';
195
180
  },
196
181
  isFocused: () => focused,
@@ -32,7 +32,9 @@ function asBackground(value) {
32
32
  return {
33
33
  type: 'ThemeAttrAndroid',
34
34
  attribute: value.attribute,
35
- rippleRadius: typeof value.rippleRadius === 'number' ? value.rippleRadius : undefined,
35
+ rippleRadius: typeof value.rippleRadius === 'number'
36
+ ? value.rippleRadius
37
+ : undefined,
36
38
  };
37
39
  }
38
40
  return undefined;