@symbiote-native/vue 0.3.7 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,30 +1,25 @@
1
- // FlatList, the Vue convenience surface over VirtualizedList. It takes a plain `data` array
2
- // and derives getItem/getItemCount; numColumns packs items into rows (a horizontal sub-View),
3
- // so the virtualized stream is rows, not items (RN's FlatList). All windowing / viewability /
4
- // batching / imperative scrolling are inherited from VirtualizedList; the data shaping and the
5
- // row/viewability/separator transforms are shared from @symbiote-native/components.
1
+ // FlatList, the Vue convenience surface over VirtualizedList. Takes a plain `data` array and
2
+ // derives getItem/getItemCount; numColumns packs items into rows (a horizontal sub-View), so the
3
+ // virtualized stream is rows, not items. All windowing/viewability/batching/imperative scrolling
4
+ // are inherited from VirtualizedList; the data shaping and row/viewability/separator transforms
5
+ // are shared from @symbiote-native/components.
6
6
  //
7
- // This is the FIRST generic component on the typed-emits pattern (the symbiote port of pd-web-kit):
8
- // a GENERIC setup function `<ItemT,>(props, ctx: ICtx<IFlatListEmits<ItemT>>)` so the events emit
9
- // with ItemT-typed payloads (`@viewable-items-changed` carries `ItemT`, not `unknown`). For that to
10
- // infer at the call site, the generic INPUTS (data/renderItem/keyExtractor/…) must be typed `props`,
11
- // not `$attrs` — so they are declared in the runtime `props` array; the long passthrough tail
12
- // (horizontal/inverted/style/getItemLayout/raw scroll/…) still rides through `$attrs` onto the inner
13
- // VirtualizedList. The inner list still GATES RefreshControl + viewability on callback presence, so
14
- // each emit bridge is wired to it ONLY when the consumer actually listens (read off the instance
15
- // vnode props), keeping behavior identical to the prop-callback era.
7
+ // Typed-emits generic component: a GENERIC setup function so events emit with ItemT-typed
8
+ // payloads. For that to infer at the call site, the generic INPUTS must be typed `props`, not
9
+ // $attrs - so they are declared in the runtime `props` array; the passthrough tail rides through
10
+ // $attrs onto the inner VirtualizedList. The inner list still GATES RefreshControl + viewability
11
+ // on callback presence, so each emit bridge is wired ONLY when the consumer actually listens.
16
12
  import { defineComponent, getCurrentInstance, h, shallowRef, } from '@vue/runtime-core';
17
13
  import { SINGLE_COLUMN, chunkIntoRows, expandRowViewability, firstItemOfRow, lastItemOfRow, rowKeyExtractor, } from '@symbiote-native/components';
18
14
  import { dlog, resolveClassName, } from '@symbiote-native/engine';
19
15
  import { VirtualizedList } from '../virtualized-list/index.js';
20
16
  import { normalizeVueAttrs } from '../../utils/normalize-attrs.js';
21
- // VirtualizedList is itself a generic component (generic construct signature), which h()'s overloads
22
- // can't resolve. Drive it through a loose functional-component handle (generic-component h()
23
- // limitation — the ItemT surface is proven at the typed FlatList boundary above).
17
+ // VirtualizedList's generic construct signature can't be resolved by h()'s overloads, so drive
18
+ // it through a loose functional-component handle (the ItemT surface is proven at the typed
19
+ // FlatList boundary above).
24
20
  const VirtualizedListHost = VirtualizedList;
25
- // The typed inputs FlatList reads off `props`; everything else falls through $attrs onto the inner
26
- // VirtualizedList. Listed for the runtime `props` declaration (keyof can't derive it: the index
27
- // signature widens keyof to `string`).
21
+ // Listed for the runtime `props` declaration (keyof can't derive it: the index signature widens
22
+ // keyof to `string`).
28
23
  const PROP_KEYS = [
29
24
  'data',
30
25
  'keyExtractor',
@@ -67,9 +62,8 @@ export const FlatList = defineComponent((props, { attrs, expose, emit, slots })
67
62
  inner.value = isVirtualizedListHandle(instance) ? instance : null;
68
63
  };
69
64
  expose(buildDelegateHandle(() => inner.value));
70
- // VirtualizedList gates RefreshControl + viewability on callback presence, so a bridge is wired
71
- // ONLY when the consumer actually listens. Declared emits are stripped from $attrs, so read the
72
- // listener off the instance's own vnode props (what the parent passed) instead.
65
+ // Declared emits are stripped from $attrs, so read the listener off the instance's own vnode
66
+ // props instead, gating each bridge to when the consumer actually listens.
73
67
  const instance = getCurrentInstance();
74
68
  const listens = (onName) => {
75
69
  const vnodeProps = instance?.vnode.props;
@@ -80,15 +74,9 @@ export const FlatList = defineComponent((props, { attrs, expose, emit, slots })
80
74
  const keyExtractor = props.keyExtractor;
81
75
  const numColumns = typeof props.numColumns === 'number' ? props.numColumns : SINGLE_COLUMN;
82
76
  const viewabilityPairs = props.viewabilityConfigCallbackPairs;
83
- // The chrome slots ride straight down to the inner list; #item/#separator are forwarded as-is
84
- // for a single column and re-wrapped per cell for multi-column below.
85
77
  const chromeSlots = { header: slots.header, footer: slots.footer, empty: slots.empty };
86
- // The passthrough tail: declared props + declared emits' onX are already removed from $attrs,
87
- // so what's left is the VirtualizedList passthrough (kebab-folded for the inner list).
88
78
  const forwarded = normalizeVueAttrs(attrs);
89
79
  dlog(`Vue FlatList over ${data.length} items, ${numColumns} column(s)`);
90
- // The synthesized list events become VL handlers ONLY when listened, so the inner list keeps
91
- // gating (no parasitic RefreshControl / viewability work for an unlistened event).
92
80
  const endReached = listens('onEndReached')
93
81
  ? (info) => emit('endReached', info)
94
82
  : undefined;
@@ -117,15 +105,11 @@ export const FlatList = defineComponent((props, { attrs, expose, emit, slots })
117
105
  onScrollToIndexFailed: scrollToIndexFailed,
118
106
  onViewableItemsChanged,
119
107
  viewabilityConfigCallbackPairs: viewabilityPairs,
120
- },
121
- // A single column forwards the consumer's slots untouched (#item info shape is identical).
122
- { ...chromeSlots, item: slots.item, separator: slots.separator });
108
+ }, { ...chromeSlots, item: slots.item, separator: slots.separator });
123
109
  }
124
110
  // Multi-column: the virtualized stream is rows. Each cell renders its items side by side
125
111
  // in a flex-row View so windowing accounts for whole rows.
126
112
  const rows = chunkIntoRows(data, numColumns);
127
- // A class-name string resolves through the shared style registry; an object/array is
128
- // already style-shaped and passes through as-is.
129
113
  const rowStyle = [
130
114
  { flexDirection: 'row' },
131
115
  typeof props.columnWrapperStyle === 'string'
@@ -1,12 +1,10 @@
1
1
  // ImageBackground: the Vue lifecycle half. The composition (the absolute-fill Image behind the
2
2
  // children, the dimension-proxy + style-merge math) lives framework-agnostic in
3
- // @symbiote-native/components/renderImageBackground and is shared verbatim with React; here Vue only
4
- // narrows the untyped attrs into the typed Image view, folds aria/role, bridges the Descriptor to
5
- // vnodes, and appends the slot children ON TOP of the inner image.
3
+ // @symbiote-native/components/renderImageBackground, shared verbatim with React; Vue narrows the
4
+ // untyped attrs into the typed Image view, folds aria/role, bridges the Descriptor to vnodes, and
5
+ // appends the slot children ON TOP of the inner image.
6
6
  //
7
- // FUNCTIONAL, not a stateful defineComponent: ImageBackground is render-only (no state). Inputs
8
- // arrive as attrs (untyped); the typed transform fields are narrowed with runtime guards, the
9
- // forward-only rest is folded so resolveAccessibilityProps lands aria-* onto the inner image.
7
+ // FUNCTIONAL, not a stateful defineComponent: render-only, no state.
10
8
  import { h } from '@vue/runtime-core';
11
9
  import { renderImageBackground, resolveAccessibilityProps, } from '@symbiote-native/components';
12
10
  import { resolveClassName, } from '@symbiote-native/engine';
@@ -18,8 +16,6 @@ function asString(value) {
18
16
  function asNumber(value) {
19
17
  return typeof value === 'number' ? value : undefined;
20
18
  }
21
- // A source is a structured object/array or an opaque require() id (number) the engine's injected
22
- // resolver expands; any object/array/number is a valid source to forward.
23
19
  function asSource(value) {
24
20
  if (typeof value === 'number')
25
21
  return value;
@@ -40,16 +36,12 @@ function asResizeMode(value) {
40
36
  function asCrossOrigin(value) {
41
37
  return value === 'anonymous' || value === 'use-credentials' ? value : undefined;
42
38
  }
43
- // Object OR array (a style list) passes through: the engine flattens either; primitives degrade
44
- // to undefined (parity with React, which preserves the StyleProp).
45
39
  function isStyleProp(value) {
46
40
  return typeof value === 'object' && value !== null;
47
41
  }
48
42
  function toChildVNode(child) {
49
43
  return typeof child === 'string' ? child : descriptorToVue(child);
50
44
  }
51
- // `style` is the WRAPPER View style; `imageStyle` targets the inner Image. The image transform
52
- // fields are consumed by the source/style fold; everything else forwards onto the inner image.
53
45
  const HANDLED_ATTRS = [
54
46
  'style',
55
47
  'imageStyle',
@@ -79,8 +71,6 @@ const ImageBackgroundComponent = (_props, { attrs: rawAttrs, slots }) => {
79
71
  const attrs = normalizeVueAttrs(rawAttrs);
80
72
  const wrapper = renderImageBackground({
81
73
  style: isStyleProp(attrs.style) ? attrs.style : undefined,
82
- // A class-name string resolves through the shared style registry, same as `class` above;
83
- // an object/array is already style-shaped and passes through as-is.
84
74
  imageStyle: typeof attrs.imageStyle === 'string'
85
75
  ? resolveClassName(attrs.imageStyle)
86
76
  : isStyleProp(attrs.imageStyle)
@@ -1,17 +1,12 @@
1
- // Image: the Vue lifecycle half. The full fold (source / src / srcSet resolution, the
2
- // width/height → style fold, resizeMode/tintColor, alt → accessibility, and the native source
3
- // array) lives framework-agnostic in @symbiote-native/components and is shared verbatim with React;
4
- // here Vue only narrows the untyped attrs into renderImage's typed view, folds aria/role, bridges
5
- // the Descriptor to a vnode, and carries the Image statics (getSize / prefetch / queryCache / …).
1
+ // Image: the Vue lifecycle half. The full fold (source/src/srcSet resolution, width/height ->
2
+ // style fold, resizeMode/tintColor, alt -> accessibility, and the native source array) lives
3
+ // framework-agnostic in @symbiote-native/components, shared verbatim with React; Vue narrows the
4
+ // untyped attrs into renderImage's typed view, folds aria/role, bridges the Descriptor to a
5
+ // vnode, and carries the Image statics (getSize/prefetch/queryCache/...).
6
6
  //
7
- // FUNCTIONAL, not a stateful defineComponent: Image is render-only, and Animated.Image wraps it
8
- // via createAnimatedComponent, which captures the host
9
- // node through a ref that only falls through on a functional component (a defineComponent's ref
10
- // resolves to a useless component proxy; see components.ts). So Image must stay functional.
11
- //
12
- // Inputs arrive as attrs (untyped). The typed transform fields are narrowed with runtime guards;
13
- // the forward-only rest (events, blurRadius, capInsets, testID, accessibility*) is typed as the
14
- // a11y intersection so resolveAccessibilityProps folds aria-* into accessibility* over it.
7
+ // FUNCTIONAL, not a stateful defineComponent: Animated.Image wraps it via createAnimatedComponent,
8
+ // which captures the host node through a ref that only falls through on a functional component
9
+ // (a defineComponent's ref resolves to a useless component proxy; see components.ts).
15
10
  import { imageStatics, renderImage, resolveAccessibilityProps, } from '@symbiote-native/components';
16
11
  import { descriptorToVue } from '../descriptor-to-vue.js';
17
12
  import { normalizeVueAttrs } from '../utils/normalize-attrs.js';
@@ -22,8 +17,6 @@ function asString(value) {
22
17
  function asNumber(value) {
23
18
  return typeof value === 'number' ? value : undefined;
24
19
  }
25
- // A source is a structured object/array or an opaque require() id (number) the engine's injected
26
- // resolver expands; any object/array/number is a valid source to forward (IImageSource is all-optional).
27
20
  function asSource(value) {
28
21
  if (typeof value === 'number')
29
22
  return value;
@@ -44,12 +37,9 @@ function asResizeMode(value) {
44
37
  function asCrossOrigin(value) {
45
38
  return value === 'anonymous' || value === 'use-credentials' ? value : undefined;
46
39
  }
47
- // Object OR array (a style list) passes through; readStyleString flattens either, and arrays must
48
- // survive so `style={[a, b]}` reaches Fabric (parity with React, which preserves the StyleProp).
49
40
  function isStyleProp(value) {
50
41
  return typeof value === 'object' && value !== null;
51
42
  }
52
- // The typed transform fields renderImage folds; everything else forwards via passthrough.
53
43
  const HANDLED_ATTRS = [
54
44
  'source',
55
45
  'defaultSource',
@@ -94,6 +84,5 @@ const ImageComponent = (_props, { attrs: rawAttrs }) => {
94
84
  };
95
85
  ImageComponent.displayName = 'Image';
96
86
  ImageComponent.inheritAttrs = false;
97
- // Statics attached like RN (Image.getSize / prefetch / …), shared verbatim with React via
98
- // the engine-resolved imageStatics. The component value doubles as the statics namespace.
87
+ // Statics attached like RN (Image.getSize/prefetch/...), shared verbatim with React.
99
88
  export const Image = Object.assign(ImageComponent, imageStatics);
@@ -1,28 +1,21 @@
1
- // KeyboardAvoidingView: the Vue lifecycle half. The inset math + the behavior → style/structure
2
- // decision live in @symbiote-native/components (render-keyboard-avoiding-view), shared verbatim with the
3
- // React adapter; Vue supplies only the reactivity: a ref holds the inset, onMounted subscribes to
4
- // the now-core Keyboard module (show / changeFrame / hide), onUnmounted tears the subscriptions
5
- // down, and onLayout measures the wrapper frame that feeds the next event's inset. This is the Vue
6
- // twin of React's useState + useEffect + onLayout. Full parity: behavior 'height'|'position'|
7
- // 'padding', enabled, keyboardVerticalOffset, contentContainerStyle, onLayout.
1
+ // KeyboardAvoidingView: the Vue lifecycle half. The inset math + the behavior->style/structure
2
+ // decision live in @symbiote-native/components (render-keyboard-avoiding-view), shared verbatim
3
+ // with React; Vue supplies only the reactivity: a ref holds the inset, onMounted subscribes to
4
+ // the Keyboard module (show/changeFrame/hide), onUnmounted tears the subscriptions down, and
5
+ // onLayout measures the wrapper frame that feeds the next event's inset.
8
6
  //
9
- // Inputs arrive as attrs (untyped), so each is narrowed with a runtime guard rather than a cast.
10
- // rawAttrs runs through normalizeVueAttrs (kebab→camel) so a template `:keyboard-vertical-offset`
11
- // and `:content-container-style` resolve; aria-*/data-* are preserved and forwarded to the host.
7
+ // Inputs arrive as attrs (untyped), so each is narrowed with a runtime guard, never a cast.
8
+ // rawAttrs runs through normalizeVueAttrs so a template `:keyboard-vertical-offset` resolves.
12
9
  import { defineComponent, h, ref, onMounted, onUnmounted } from '@vue/runtime-core';
13
10
  import { Keyboard, KEYBOARD_EVENT, dlog, } from '@symbiote-native/engine';
14
11
  import { computeInset, readKeyboardFrame, readLayoutFrame, resolveKeyboardAvoidingLayout, resolveAccessibilityProps, DEFAULT_VERTICAL_OFFSET, } from '@symbiote-native/components';
15
12
  import { normalizeVueAttrs } from '../utils/normalize-attrs.js';
16
- // A style prop is an object (a style record) or an array of them; numbers/strings/null degrade
17
- // to undefined (the engine flattens what it gets). A runtime guard, not a cast.
18
13
  function isStyleProp(value) {
19
14
  return typeof value === 'object' && value !== null;
20
15
  }
21
16
  function asBehavior(value) {
22
17
  return value === 'height' || value === 'position' || value === 'padding' ? value : undefined;
23
18
  }
24
- // The prop/handler keys the lifecycle consumes itself; everything else (accessibility, testID,
25
- // aria-*/data-*) forwards onto the wrapper host node.
26
19
  const HANDLED_ATTRS = [
27
20
  'behavior',
28
21
  'enabled',
@@ -40,11 +33,9 @@ function forwardAttrs(attrs) {
40
33
  return result;
41
34
  }
42
35
  export const KeyboardAvoidingView = defineComponent((_props, { attrs: rawAttrs, slots, emit }) => {
43
- // The inset is a number (plain data), so a plain ref is correct; no engine node is held here
44
- // (onLayout delivers the frame, so no imperative measure / host-node capture is needed).
45
36
  const inset = ref(0);
46
37
  // Mutable, not reactive: changing the measured frame alone shouldn't re-render; it feeds the
47
- // next keyboard event's inset math (React's frameRef / initialHeightRef).
38
+ // next keyboard event's inset math.
48
39
  let frame;
49
40
  let initialHeight;
50
41
  const verticalOffset = () => typeof rawAttrs.keyboardVerticalOffset === 'number'
@@ -103,8 +94,8 @@ export const KeyboardAvoidingView = defineComponent((_props, { attrs: rawAttrs,
103
94
  style: layout.wrapperStyle,
104
95
  onLayout: handleLayout,
105
96
  };
106
- // 'nested' ('position') pushes the children in an inner view by `bottom: inset`; the wrapper
107
- // modes adjust the single wrapper directly.
97
+ // 'nested' pushes the children in an inner view by `bottom: inset`; the wrapper modes
98
+ // adjust the single wrapper directly.
108
99
  if (layout.kind === 'nested') {
109
100
  return h('symbiote-view', wrapperProps, [
110
101
  h('symbiote-view', { style: layout.innerStyle }, childNodes),
@@ -1,5 +1,5 @@
1
- import { type IAccessibilityProps, type IAriaProps, type IModalAnimationType, type IModalOrientation, type IModalOrientationChangeEvent, type IModalPresentationStyle } from '@symbiote-native/components';
2
- import { type IClassNameValue, type IStyleProp, type IViewStyle } from '@symbiote-native/engine';
1
+ import { type IAccessibilityProps, type IAriaProps, type IModalAnimationType, type IModalOrientation, type IModalPresentationStyle } from '@symbiote-native/components';
2
+ import { type IClassNameValue, type IStyleProp, type ISymbioteEvent, type IViewStyle } from '@symbiote-native/engine';
3
3
  export type { IModalAnimationType, IModalPresentationStyle, IModalOrientation, IModalOrientationChangeEvent, } from '@symbiote-native/components';
4
4
  export interface IModalProps extends IAccessibilityProps, IAriaProps {
5
5
  visible?: boolean;
@@ -19,11 +19,11 @@ export type IModalEmits = {
19
19
  show: () => boolean;
20
20
  dismiss: () => boolean;
21
21
  requestClose: () => boolean;
22
- orientationChange: (event: IModalOrientationChangeEvent) => boolean;
22
+ orientationChange: (event: ISymbioteEvent) => boolean;
23
23
  };
24
24
  export declare const Modal: import("vue").DefineSetupFnComponent<IModalProps, IModalEmits, {}, IModalProps & {
25
25
  onShow?: (() => any) | undefined;
26
26
  onDismiss?: (() => any) | undefined;
27
27
  onRequestClose?: (() => any) | undefined;
28
- onOrientationChange?: ((event: IModalOrientationChangeEvent) => any) | undefined;
28
+ onOrientationChange?: ((event: ISymbioteEvent) => any) | undefined;
29
29
  }, import("vue").PublicProps>;
@@ -1,24 +1,22 @@
1
- // Pressable, the Vue lifecycle half. The press lifecycle (the long-press timer, unstable_press-
2
- // Delay deferral, the pressRetentionOffset drift test, the suppression flags) lives in
3
- // @symbiote-native/components/state as a pure machine over a runtime + host; the render decisions (the
4
- // responder listeners, the disabled→accessibilityState fold, the ripple prop) in
5
- // @symbiote-native/components/view, both shared verbatim with the React adapter. Here Vue supplies the
1
+ // Pressable, the Vue lifecycle half. The press lifecycle (long-press timer, unstable_press-
2
+ // Delay deferral, pressRetentionOffset drift test, suppression flags) lives in
3
+ // @symbiote-native/components/state as a pure machine over a runtime + host; the render
4
+ // decisions (responder listeners, disabled->accessibilityState fold, ripple prop) in
5
+ // @symbiote-native/components/view, both shared verbatim with React. Vue supplies the
6
6
  // reactivity: a `ref` holds `pressed`, a setup-scope object holds the press runtime, a function
7
- // ref grabs the responder View's host node, and the machine measures through it. This is the Vue
8
- // twin of the React adapter's useState + useRef(runtime) + useRef(viewRef).
7
+ // ref grabs the responder View's host node.
9
8
  //
10
- // Inputs arrive as attrs (untyped), so each is narrowed with a runtime guard rather than a cast.
11
- // The user's onPress / onPressIn / … are consumed by the machine and MUST be stripped from the
12
- // forwarded attrs (they are pure-JS callbacks; the machine's SYNTHESIZED handlers go on the View,
13
- // where routeProp turns press/pressIn/pressOut + the responder events into listeners). Children
14
- // arrive as a (scoped) default slot so `v-slot="{ pressed }"` mirrors React's children-as-function.
9
+ // Inputs arrive as attrs (untyped), so each is narrowed with a runtime guard, never a cast. The
10
+ // user's onPress/onPressIn/... are consumed by the machine and MUST be stripped from the
11
+ // forwarded attrs (pure-JS callbacks); the machine's SYNTHESIZED handlers go on the View instead.
12
+ // Children arrive as a (scoped) default slot so `v-slot="{ pressed }"` mirrors React's
13
+ // children-as-function.
15
14
  import { defineComponent, h, ref, shallowRef } from '@vue/runtime-core';
16
15
  import { createPressHandlers, createPressRuntime, rippleProps, buildPressableListeners, resolveDisabledAccessibilityState, noteHoverNoop, resolveAccessibilityProps, DEFAULT_DELAY_LONG_PRESS_MS, } from '@symbiote-native/components';
17
16
  import { measure, isSymbioteNode, } from '@symbiote-native/engine';
18
17
  import { View } from '../components.js';
19
18
  import { normalizeVueAttrs } from '../utils/normalize-attrs.js';
20
- // The emits-options validator for the full press surface. Reused verbatim as the `emits` of every
21
- // Touchable* wrapper (they expose the same events), so the runtime list lives in exactly one place.
19
+ // Reused verbatim as the `emits` of every Touchable* wrapper, so the runtime list lives once.
22
20
  export const PRESSABLE_EMITS = {
23
21
  press: (_event) => true,
24
22
  pressIn: (_event) => true,
@@ -29,9 +27,8 @@ export const PRESSABLE_EMITS = {
29
27
  hoverOut: (_event) => true,
30
28
  };
31
29
  // Bridge the whole press surface onto a child Pressable: each host onX callback re-emits the
32
- // matching event on the wrapper. Every Touchable* wraps Pressable, so spread this onto the child's
33
- // props. A wrapper that intercepts an event for its own state (e.g. TouchableOpacity's opacity
34
- // animation on press-in/out) overrides that one key AFTER the spread.
30
+ // matching event on the wrapper. A wrapper that intercepts an event for its own state (e.g.
31
+ // TouchableOpacity's opacity animation on press-in/out) overrides that one key AFTER the spread.
35
32
  export function emitPressableEvents(emit) {
36
33
  return {
37
34
  onPress: event => emit('press', event),
@@ -53,7 +50,7 @@ function numberOr(value, fallback) {
53
50
  return typeof value === 'number' ? value : fallback;
54
51
  }
55
52
  // A scalar offset, or the per-edge object; anything else is dropped (the machine reads undefined
56
- // as "no offset" → RN's defaults).
53
+ // as "no offset" -> RN's defaults).
57
54
  function asRectOffset(value) {
58
55
  if (typeof value === 'number')
59
56
  return value;
@@ -107,9 +104,8 @@ function resolveStyle(value, state) {
107
104
  return value(state);
108
105
  return value;
109
106
  }
110
- // The prop/handler keys the lifecycle consumes itself; everything else (aria/accessibility/
111
- // testID/nativeID/native props) forwards onto the View. The user press callbacks are pure JS and
112
- // must never reach the host; the machine's synthesized handlers go on via buildPressableListeners.
107
+ // Everything else forwards onto the View. User press callbacks are pure JS and must never reach
108
+ // the host; the machine's synthesized handlers go on via buildPressableListeners.
113
109
  const HANDLED_ATTRS = [
114
110
  'onPress',
115
111
  'onPressIn',
@@ -141,16 +137,16 @@ function forwardAttrs(attrs) {
141
137
  export const Pressable = defineComponent((_props, { slots, attrs: rawAttrs, emit }) => {
142
138
  const pressed = ref(false);
143
139
  // The mutable press runtime (timers, suppression flags, measured region). A plain setup-scope
144
- // object, never a ref: it is mutated by the machine, never reactively read.
140
+ // object, never a ref: mutated by the machine, never reactively read.
145
141
  const runtime = createPressRuntime();
146
- // shallowRef, NOT ref: the engine node is held by IDENTITY so measure() hits the engine's
147
- // WeakMap mirror (a plain ref would wrap it in a reactive Proxy → mirror miss → measure no-op).
142
+ // shallowRef, NOT ref: a plain ref would wrap the node in a reactive Proxy, missing the
143
+ // engine's WeakMap mirror and making measure() no-op.
148
144
  const nodeRef = shallowRef(null);
149
145
  const setNodeRef = (el) => {
150
146
  nodeRef.value = isSymbioteNode(el) ? el : null;
151
147
  };
152
148
  // The lifecycle seam the machine fills: flip the reactive `pressed`, and expose the responder
153
- // View's raw frame-measure (or undefined before the node commits → radius fallback).
149
+ // View's raw frame-measure (or undefined before the node commits, a radius fallback).
154
150
  const host = {
155
151
  setPressed: next => {
156
152
  pressed.value = next;
@@ -184,9 +180,8 @@ export const Pressable = defineComponent((_props, { slots, attrs: rawAttrs, emit
184
180
  const handlers = createPressHandlers(config, runtime, host);
185
181
  noteHoverNoop(undefined, undefined);
186
182
  const state = { pressed: pressed.value };
187
- // Fold the disabled state into the user's accessibilityState, then fold aria/role over the
188
- // forwarded attrs (the Vue View is a bare host primitive, so Pressable folds; React's View
189
- // folds for it). resolveAccessibilityProps merges aria into the accessibilityState we set.
183
+ // Vue's View is a bare host primitive, so Pressable folds disabled into accessibilityState
184
+ // and aria/role itself, rather than the View folding it (as React's does).
190
185
  const forwarded = forwardAttrs(attrs);
191
186
  forwarded.accessibilityState = resolveDisabledAccessibilityState(asAccessibilityState(attrs.accessibilityState), disabled);
192
187
  const folded = resolveAccessibilityProps(forwarded);
@@ -198,11 +193,9 @@ export const Pressable = defineComponent((_props, { slots, attrs: rawAttrs, emit
198
193
  if (typeof attrs.android_disableSound === 'boolean')
199
194
  viewProps.android_disableSound = attrs.android_disableSound;
200
195
  Object.assign(viewProps, buildPressableListeners(handlers, { disabled, cancelable }));
201
- // Children come from the (scoped) default slot, receiving the press state so a render-prop
202
- // child (`v-slot="{ pressed }"`) mirrors React's children-as-function.
203
196
  const content = slots.default !== undefined ? slots.default(state) : [];
204
197
  // android_ripple rides a dedicated inner View; on iOS the prop is undefined, so the child
205
- // renders unwrapped, no extra node. Mirrors the React Pressable + touchable-native-feedback.
198
+ // renders unwrapped, no extra node.
206
199
  const ripple = isRecord(attrs.android_ripple)
207
200
  ? rippleProps(asRippleConfig(attrs.android_ripple) ?? {})
208
201
  : undefined;
@@ -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,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,11 +18,8 @@ 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;
@@ -32,25 +28,20 @@ function isHostType(type) {
32
28
  export const ScrollView = createScrollView({
33
29
  assemble: input => {
34
30
  if (input.refreshControl === undefined) {
35
- // No refresh: the same single-child path as Phase 1; the node ref rides input.scrollProps.
36
31
  dlog('Vue ScrollView.ANDROID refreshControl=NONE(1child)');
37
32
  return h(input.scrollViewIntrinsic, input.scrollProps, [input.content]);
38
33
  }
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.
34
+ // RN splits the flattened style across the two boxes: LAYOUT props (margin/flex/size/
35
+ // position/...) drive the outer AndroidSwipeRefreshLayout frame; VISUAL props (background/
36
+ // padding/border/...) paint the inner scroll view - not a hardcoded flex:1 that would
37
+ // override an explicit user height/width.
44
38
  //
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.
39
+ // layoutSplitStyle (not userStyle): a class-only layout prop is invisible to userStyle (it
40
+ // never carries the resolved `class` value - see isClassNameProp in shared.ts), so splitting
41
+ // on userStyle alone would starve the wrapper of its layout style.
49
42
  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.
43
+ // `class` is stripped here: layoutSplitStyle already folded its resolved value into
44
+ // outer/inner above, so forwarding the raw prop too would re-apply its LAYOUT half a second time.
54
45
  const { class: _classAppliedViaSplit, ...innerScrollOuterProps } = input.scrollOuterProps;
55
46
  const innerScrollView = h(input.scrollViewIntrinsic, {
56
47
  ...innerScrollOuterProps,
@@ -60,15 +51,12 @@ export const ScrollView = createScrollView({
60
51
  }, [input.content]);
61
52
  const rc = input.refreshControl;
62
53
  if (!isHostType(rc.type)) {
63
- // refreshControl is always a component VNode; a symbol/nested-VNode type can't host children.
64
54
  // Degrade to the unwrapped scroll view rather than crash (the node ref is already on it).
65
55
  dlog('Vue ScrollView.ANDROID refreshControl has no hostable type, rendering scroll view unwrapped');
66
56
  return innerScrollView;
67
57
  }
68
58
  const rcProps = isRecord(rc.props) ? rc.props : {};
69
59
  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
60
  return h(rc.type, { ...rcProps, style: outer }, { default: () => [innerScrollView] });
73
61
  },
74
62
  });