@symbiote-native/vue 0.4.0 → 1.0.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 (57) hide show
  1. package/README.md +24 -9
  2. package/babel-jsx.cjs +53 -0
  3. package/babel-lower-host-primitives.cjs +225 -0
  4. package/build/bootstrap.js +1 -1
  5. package/build/components/button.js +4 -2
  6. package/build/components/flat-list/index.js +14 -5
  7. package/build/components/image-background.js +4 -5
  8. package/build/components/image.js +3 -1
  9. package/build/components/keyboard-avoiding-view.js +39 -14
  10. package/build/components/modal/index.js +5 -2
  11. package/build/components/pressable.d.ts +1 -1
  12. package/build/components/pressable.js +39 -20
  13. package/build/components/safe-area-view.d.ts +1 -0
  14. package/build/components/scroll-view/index.android.d.ts +1 -1
  15. package/build/components/scroll-view/index.android.js +3 -1
  16. package/build/components/scroll-view/index.ios.d.ts +1 -1
  17. package/build/components/scroll-view/index.ios.js +3 -1
  18. package/build/components/scroll-view/shared.js +33 -13
  19. package/build/components/scroll-view/sticky-header.js +18 -5
  20. package/build/components/section-list/index.js +4 -2
  21. package/build/components/switch/shared.js +12 -3
  22. package/build/components/text-input/index.js +25 -2
  23. package/build/components/touchable-native-feedback.js +11 -4
  24. package/build/components/touchable.d.ts +7 -1
  25. package/build/components/touchable.js +184 -33
  26. package/build/components/virtualized-list/index.js +42 -20
  27. package/build/components/virtualized-section-list/index.d.ts +5 -0
  28. package/build/components/virtualized-section-list/index.js +33 -4
  29. package/build/components.d.ts +2 -0
  30. package/build/components.js +25 -4
  31. package/build/composables/use-raw-attrs.d.ts +1 -0
  32. package/build/composables/use-raw-attrs.js +26 -0
  33. package/build/create-portal/index.d.ts +28 -0
  34. package/build/create-portal/index.js +42 -0
  35. package/build/host-instance/index.js +7 -6
  36. package/build/index.d.ts +9 -6
  37. package/build/index.js +14 -3
  38. package/build/modules/animated/create-animated-component.d.ts +1 -1
  39. package/build/modules/animated/create-animated-component.js +27 -5
  40. package/build/modules/animated/index.d.ts +2 -0
  41. package/build/modules/animated/index.js +18 -5
  42. package/build/modules/app-registry/index.js +4 -2
  43. package/build/modules/status-bar.js +4 -2
  44. package/build/register.d.ts +1 -0
  45. package/build/register.js +35 -0
  46. package/build/render.d.ts +3 -1
  47. package/build/render.js +62 -1
  48. package/build/renderer/index.js +58 -9
  49. package/build/runtime-helpers/index.d.ts +48 -24
  50. package/build/runtime-helpers/index.js +314 -30
  51. package/build/state-style.d.ts +1 -0
  52. package/build/state-style.js +13 -0
  53. package/build/utils/normalize-attrs.d.ts +1 -0
  54. package/build/utils/normalize-attrs.js +21 -13
  55. package/metro-css-parser.cjs +2 -1
  56. package/metro-vue-transformer.cjs +502 -101
  57. package/package.json +29 -8
@@ -0,0 +1,42 @@
1
+ // The Vue adapter's portal — Vue's OWN `<Teleport>`, not a parallel API. Teleport lives in
2
+ // @vue/runtime-core, not @vue/runtime-dom, and resolves its target through the RendererOptions we
3
+ // already supply: `resolveTarget` only consults `querySelector` for the STRING form of `to`, and
4
+ // returns an object target untouched. So stock Teleport drives this renderer as-is — target
5
+ // change, unmount, disabled, call-site provide/inject and reactive updates all measured working
6
+ // over the engine (create-portal.test.ts). This module adds exactly one thing on top: a runtime
7
+ // guard on `to`.
8
+ //
9
+ // Why the guard: `to` is untyped at the template boundary, and a wrong value (a CSS selector
10
+ // copied from web code, a forgotten `.value` on a ref, a plain object) would otherwise sink into
11
+ // insert/remove deep in the engine and corrupt the retained tree silently. Vue's own dev warning
12
+ // for the string form only fires in a dev build and still returns null, which paints nothing.
13
+ //
14
+ // Scope — same-surface targets only, the same PERMANENT boundary as React's createPortal
15
+ // (adapters/react/src/create-portal/index.ts): a portal MOVES host nodes, and only the surface
16
+ // that owns them ever commits them. Content whose target lives in a second, independently
17
+ // mount()ed surface needs createTunnel instead, which copies rather than moves.
18
+ import { defineComponent, h, Teleport as VueTeleport } from '@vue/runtime-core';
19
+ import { isSymbioteNode, SymbioteSurface, } from '@symbiote-native/engine';
20
+ export function isTeleportTarget(target) {
21
+ return target instanceof SymbioteSurface || isSymbioteNode(target);
22
+ }
23
+ export const Teleport = defineComponent({
24
+ name: 'Teleport',
25
+ inheritAttrs: false,
26
+ props: {
27
+ to: { type: null, default: null },
28
+ disabled: { type: Boolean, default: false },
29
+ },
30
+ setup(props, { slots }) {
31
+ return () => {
32
+ const { to } = props;
33
+ if (typeof to === 'string') {
34
+ throw new Error(`Teleport target must be a host node ref, not a CSS-selector string ("${to}") — symbiote has no querySelector. Pass a ref to an already-rendered element instead (e.g. <View ref="overlayHost" />, then :to="overlayHost").`);
35
+ }
36
+ if (to != null && !isTeleportTarget(to)) {
37
+ throw new Error('Teleport target is not a real host node — did you forget `.value` on a ref, or pass something symbiote never rendered?');
38
+ }
39
+ return h(VueTeleport, { to, disabled: props.disabled }, slots);
40
+ };
41
+ },
42
+ });
@@ -1,11 +1,10 @@
1
1
  // findNodeHandle, the Vue adapter twin of adapters/react/src/host-instance.ts. RN's
2
2
  // "ref/instance -> native reactTag" lookup, the seam imperative-interop libraries
3
3
  // (reanimated, gesture-handler, react-navigation) reach through. A Vue template/function ref
4
- // to a symbiote host element falls through to the raw engine SymbioteNode, and the Vue
5
- // renderer grafts the public-instance API onto it (toPublicInstance in renderer.ts), so the
6
- // node a ref receives carries measure / setNativeProps / focus exactly like React's
7
- // getPublicInstance. IHostInstance is therefore that grafted node — re-exported from the
8
- // engine so React and Vue share one definition.
4
+ // to a symbiote host element falls through to the raw engine SymbioteNode, which already carries
5
+ // measure / setNativeProps / focus on its prototype, exactly like React's getPublicInstance.
6
+ // IHostInstance is therefore that node — re-exported from the engine so React and Vue share one
7
+ // definition.
9
8
  //
10
9
  // The node -> tag resolution itself belongs to the engine (getNativeTag, keyed on the raw
11
10
  // node in the commit mirror); this only unwraps the Vue-shaped inputs (a Ref or a bare node)
@@ -13,7 +12,9 @@
13
12
  import { isRef, toRaw } from '@vue/runtime-core';
14
13
  import { getNativeTag, isSymbioteNode, dlog } from '@symbiote-native/engine';
15
14
  export function findNodeHandle(componentOrHandle) {
16
- const candidate = isRef(componentOrHandle) ? componentOrHandle.value : componentOrHandle;
15
+ const candidate = isRef(componentOrHandle)
16
+ ? componentOrHandle.value
17
+ : componentOrHandle;
17
18
  if (candidate === null || candidate === undefined)
18
19
  return null;
19
20
  if (typeof candidate === 'number')
package/build/index.d.ts CHANGED
@@ -1,4 +1,7 @@
1
- export { mount, unmount } from './render';
1
+ import './register';
2
+ export { mount, unmount, setAppConfigurator } from './render';
3
+ export type { IAppConfigurator } from './render';
4
+ export { Teleport, type ITeleportTarget } from './create-portal';
2
5
  export { createTunnel, type ITunnel } from './create-tunnel';
3
6
  export { View, Text } from './components';
4
7
  export type { IAccessibilityProps, IAriaProps, IAccessibilityRole, IRole, IAccessibilityStateValue, IAccessibilityValue, IAccessibilityActionInfo, IResponderProps, } from '@symbiote-native/components';
@@ -21,7 +24,7 @@ export type { INativeFeedbackBackground, IThemeAttrBackground, IRippleBackground
21
24
  export { Button } from './components/button';
22
25
  export type { IButtonProps } from './components/button';
23
26
  export { TextInput } from './components/text-input';
24
- export type { ITextInputProps, ITextInputHandle } from './components/text-input';
27
+ export type { ITextInputProps, ITextInputHandle, } from './components/text-input';
25
28
  export { VirtualizedList } from './components/virtualized-list';
26
29
  export type { IVirtualizedListProps, IVirtualizedListSlots, IVirtualizedListEmits, IVirtualizedListHandle, ISeparators, ISeparatorProps, IViewToken, IViewableItemsChangedInfo, IViewabilityConfig, IViewabilityConfigCallbackPair, ICellLayout, } from './components/virtualized-list';
27
30
  export { FlatList } from './components/flat-list';
@@ -33,7 +36,7 @@ export type { ISection, ISectionListProps, ISectionListSlots, ISectionListEmits,
33
36
  export { SafeAreaView } from './components/safe-area-view';
34
37
  export type { ISafeAreaViewProps } from './components/safe-area-view';
35
38
  export { RefreshControl } from './components/refresh-control';
36
- export type { IRefreshControlProps, IRefreshControlEmits } from './components/refresh-control';
39
+ export type { IRefreshControlProps, IRefreshControlEmits, } from './components/refresh-control';
37
40
  export { descriptorToVue } from './descriptor-to-vue';
38
41
  export { normalizeVueAttrs } from './utils/normalize-attrs';
39
42
  export { resolveModelValue, emitModelUpdate } from './utils/model-binding';
@@ -56,10 +59,10 @@ export { useWindowDimensions } from './composables/use-window-dimensions';
56
59
  export { Alert, Share, ActionSheetIOS, Linking, Vibration, ToastAndroid, Settings, I18nManager, Dimensions, Appearance, AppState, Keyboard, KEYBOARD_EVENT, BackHandler, PermissionsAndroid, PERMISSIONS, RESULTS, AccessibilityInfo, LayoutAnimation, } from '@symbiote-native/engine';
57
60
  export type { IAlertType, IAlertButtonStyle, IAlertButton, IAlertButtons, IAlertOptions, IShareContent, IShareOptions, IShareAction, IActionSheetIOSOptions, IShareActionSheetIOSOptions, IShareActionSheetError, IUrlEvent, II18nManagerConstants, IDisplayMetrics, IDisplayMetricsAndroid, IDimensionsPayload, IDimensionsSet, IDimensionsKey, IDimensionsChangeListener, IDimensionsStatic, IColorSchemeName, IColorSchemePreference, IAppStateStatus, IAppStateEvent, IKeyboardEventName, IKeyboardEvent, IKeyboardMetrics, IBackPressEventName, IBackPressHandler, IPermission, IPermissionStatus, IRationale, IAccessibilityChangeEvent, IAccessibilityChangeEventName, IAccessibilityChangeEventHandler, IAccessibilityAnnouncementFinishedEvent, IAnnounceForAccessibilityOptions, IAccessibilityEventType, ILayoutAnimationType, ILayoutAnimationProperty, ILayoutAnimationConfig, ILayoutAnimationAnim, } from '@symbiote-native/engine';
58
61
  export { Platform, StyleSheet, processColor, setColorProcessor, dlog, isDebug, } from '@symbiote-native/engine';
59
- export type { ISymbioteEvent, ISymbioteNode, IRootTag } from '@symbiote-native/engine';
62
+ export type { ISymbioteEvent, ISymbioteNode, IRootTag, } from '@symbiote-native/engine';
60
63
  export type { IViewStyle, ITextStyle, IFlexAlign, IFlexJustify, IPlatformStatic, IPlatformOSType, IPlatformConstantsIOS, IPlatformConstantsAndroid, IPlatformSelectSpec, } from '@symbiote-native/engine';
61
- export { setNativeViewConfigSource, setDeviceEventSource } from '@symbiote-native/engine';
62
- export type { INativeViewConfig, INativeViewConfigSource } from '@symbiote-native/engine';
64
+ export { setNativeViewConfigSource, setDeviceEventSource, } from '@symbiote-native/engine';
65
+ export type { INativeViewConfig, INativeViewConfigSource, } from '@symbiote-native/engine';
63
66
  export type { IEnterKeyHint, IInputMode, ISubmitBehavior, ITextInputSelection, IImageStatics, } from '@symbiote-native/components';
64
67
  export { PixelRatio, PanResponder, PlatformColor, DynamicColorIOS, InteractionManager, } from '@symbiote-native/engine';
65
68
  export type { IPixelRatioStatic } from '@symbiote-native/engine';
package/build/index.js CHANGED
@@ -1,8 +1,19 @@
1
1
  // @symbiote-native/vue: a thin Vue 3 reconciler over @symbiote-native/engine. createRenderer maps
2
2
  // each RendererOptions call onto the engine's mutation API; all Fabric clone-on-write
3
3
  // lives in the engine, shared with every other adapter. App code names only @symbiote-native/vue.
4
- export { mount, unmount } from './render.js';
5
- // Teleport stays same-surface-only by design - see create-tunnel.ts for why.
4
+ // Side-effect import, FIRST and deliberately not a re-export: it installs the engine-side press
5
+ // machine that a lowered `<symbiote-pressable>` needs. `export * from './register'` or a bare
6
+ // import sitting beside a re-export of the same specifier both go lazy under Metro's production
7
+ // `inlineRequires` and the registration silently never runs in a release build. See register.ts.
8
+ import './register.js';
9
+ export { mount, unmount, setAppConfigurator } from './render.js';
10
+ // The portal: Vue's own <Teleport>, guarded so `to` must be a node/surface this renderer actually
11
+ // mounted (there is no querySelector). It MOVES host nodes, so it reaches any already-mounted
12
+ // target in the SAME surface — including one you only hold a ref to — and keeps the content's
13
+ // reactive owner at the call site (provide/inject resolve from where it was written). Reaching a
14
+ // second, independently mount()ed surface is a different mechanism: createTunnel, which copies
15
+ // into an <Out/> the destination has to render. See create-portal/index.ts.
16
+ export { Teleport } from './create-portal/index.js';
6
17
  export { createTunnel } from './create-tunnel/index.js';
7
18
  export { View, Text } from './components.js';
8
19
  // IHostInstance is the raw engine node (Vue host refs fall through to it; imperative methods
@@ -50,7 +61,7 @@ export { Platform, StyleSheet, processColor, setColorProcessor, dlog, isDebug, }
50
61
  // Wired once by the app entry on a real host (like setColorProcessor): hands the engine
51
62
  // RN's ViewConfig registry so third-party Fabric views auto-derive their metadata.
52
63
  // setDeviceEventSource is the third seam of the same set and travels with them.
53
- export { setNativeViewConfigSource, setDeviceEventSource } from '@symbiote-native/engine';
64
+ export { setNativeViewConfigSource, setDeviceEventSource, } from '@symbiote-native/engine';
54
65
  // Pure utilities that moved to the engine (single source, both adapters re-export):
55
66
  // PixelRatio + PanResponder, plus the color builders and the interaction scheduler.
56
67
  export { PixelRatio, PanResponder, PlatformColor, DynamicColorIOS, InteractionManager, } from '@symbiote-native/engine';
@@ -1,4 +1,4 @@
1
1
  import { type Component } from '@vue/runtime-core';
2
- export declare function createAnimatedComponent(Component: Component): import("vue").DefineComponent<{}, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
2
+ export declare function createAnimatedComponent(Component: Component | string): import("vue").DefineComponent<{}, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
3
3
  [key: string]: any;
4
4
  }>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
@@ -12,9 +12,13 @@ import { normalizeVueAttrs } from '../../utils/normalize-attrs.js';
12
12
  // the COMMITTED props (sticky-header passthrough). Named once here so render/reconcile agree.
13
13
  const PASSTHROUGH_PROP = 'passthroughAnimatedPropExplicitValues';
14
14
  // Reads either a functional component's or a stateful defineComponent's display name, without a
15
- // cast, for the wrapper's devtools name.
15
+ // cast, for the wrapper's devtools name. A string base IS its own name — an intrinsic tag carries
16
+ // no displayName to read.
16
17
  function baseName(component) {
17
- if (component === null || (typeof component !== 'function' && typeof component !== 'object')) {
18
+ if (typeof component === 'string')
19
+ return component;
20
+ if (component === null ||
21
+ (typeof component !== 'function' && typeof component !== 'object')) {
18
22
  return 'Anonymous';
19
23
  }
20
24
  const display = Reflect.get(component, 'displayName');
@@ -25,6 +29,8 @@ function baseName(component) {
25
29
  return name;
26
30
  return 'Anonymous';
27
31
  }
32
+ // The base may be an intrinsic TAG, not only a component: `Animated.View` is built over `View`,
33
+ // and a primitive that becomes a public tag hands this a string. `h()` takes either.
28
34
  export function createAnimatedComponent(Component) {
29
35
  return defineComponent({
30
36
  name: `Animated(${baseName(Component)})`,
@@ -68,7 +74,9 @@ export function createAnimatedComponent(Component) {
68
74
  },
69
75
  has: (_target, key) => {
70
76
  const instance = instanceRef.value;
71
- return instance !== null && typeof instance === 'object' && Reflect.has(instance, key);
77
+ return (instance !== null &&
78
+ typeof instance === 'object' &&
79
+ Reflect.has(instance, key));
72
80
  },
73
81
  }));
74
82
  return () => {
@@ -96,10 +104,24 @@ export function createAnimatedComponent(Component) {
96
104
  const passthroughStyle = readPassthroughStyle(passthrough);
97
105
  if (passthroughStyle !== undefined) {
98
106
  reduced.style =
99
- reduced.style === undefined ? passthroughStyle : [reduced.style, passthroughStyle];
107
+ reduced.style === undefined
108
+ ? passthroughStyle
109
+ : [reduced.style, passthroughStyle];
100
110
  }
101
111
  reduced.ref = captureRef;
102
- return h(Component, reduced, slots.default !== undefined ? { default: slots.default } : undefined);
112
+ // The WHOLE slots object, for a component base AND for a tag base. Not just `default`:
113
+ // the list containers take their content through named scoped slots (`item`,
114
+ // `sectionHeader`, ...), which a default-only forward drops silently — Animated.FlatList
115
+ // would commit empty cells.
116
+ //
117
+ // A string base needs no branch here, and the reason is worth recording because the code
118
+ // that reads it looks like it does. For an element Vue unwraps the object itself, calling
119
+ // `children.default()` and recursing on the RESULT rather than going back through `h`
120
+ // (`normalizeChildren`, the `shapeFlag & (1 | 64)` branch) — so a slot returning a lone
121
+ // VNode would be re-read as an object, found to have no `.default`, and dropped, with no
122
+ // error. That case cannot arise: Vue wraps every slot at initSlots so `slots.default()`
123
+ // returns an ARRAY whatever the author's function returned. Measured both spellings.
124
+ return h(Component, reduced, slots);
103
125
  };
104
126
  },
105
127
  });
@@ -51,5 +51,7 @@ export declare const Animated: {
51
51
  [key: string]: any;
52
52
  }>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
53
53
  ScrollView: ReturnType<typeof createAnimatedComponent>;
54
+ FlatList: ReturnType<typeof createAnimatedComponent>;
55
+ SectionList: ReturnType<typeof createAnimatedComponent>;
54
56
  createAnimatedComponent: typeof createAnimatedComponent;
55
57
  };
@@ -7,6 +7,8 @@ import { AnimatedValue, AnimatedValueXY, AnimatedColor, AnimatedMock, Easing, Pl
7
7
  import { View, Text } from '../../components.js';
8
8
  import { Image } from '../../components/image.js';
9
9
  import { ScrollView } from '../../components/scroll-view';
10
+ import { FlatList } from '../../components/flat-list/index.js';
11
+ import { SectionList } from '../../components/section-list/index.js';
10
12
  import { createAnimatedComponent } from './create-animated-component.js';
11
13
  export { createAnimatedComponent } from './create-animated-component.js';
12
14
  // View/Text are pure host primitives; Image is the functional renderImage wrapper, and all expose
@@ -14,12 +16,15 @@ export { createAnimatedComponent } from './create-animated-component.js';
14
16
  const AnimatedView = createAnimatedComponent(View);
15
17
  const AnimatedText = createAnimatedComponent(Text);
16
18
  const AnimatedImage = createAnimatedComponent(Image);
17
- // LAZY, memoized getter, mirroring RN's `get ScrollView()`: ScrollView's module chain imports
18
- // this Animated namespace back (sticky headers), so a static createAnimatedComponent(ScrollView)
19
- // at init could read ScrollView inside its own TDZ. Deferring the wrap past module init avoids the cycle.
19
+ // LAZY, memoized getters, mirroring RN's `get ScrollView()`. Every scrolling container reaches
20
+ // scroll-view/sticky-header, which imports this Animated namespace back, so a wrap at module
21
+ // scope captures whatever the half-evaluated cycle holds at that instant. Under a bundler's ESM
22
+ // interop that is `undefined`, not a ReferenceError - the wrapper builds fine and then renders
23
+ // nothing. Deferring past module init is what keeps it from firing. FlatList and SectionList
24
+ // reach ScrollView through VirtualizedList, so they sit on the same cycle and take the same shape.
20
25
  let animatedScrollView;
21
- // Animated.FlatList / Animated.SectionList are intentionally OMITTED, not faked - a named gap,
22
- // not a silent drop.
26
+ let animatedFlatList;
27
+ let animatedSectionList;
23
28
  // The live, JS-driven driver namespace (real frames). RN's AnimatedImplementation.
24
29
  const liveDrivers = {
25
30
  Value: AnimatedValue,
@@ -57,6 +62,14 @@ export const Animated = {
57
62
  animatedScrollView ??= createAnimatedComponent(ScrollView);
58
63
  return animatedScrollView;
59
64
  },
65
+ get FlatList() {
66
+ animatedFlatList ??= createAnimatedComponent(FlatList);
67
+ return animatedFlatList;
68
+ },
69
+ get SectionList() {
70
+ animatedSectionList ??= createAnimatedComponent(SectionList);
71
+ return animatedSectionList;
72
+ },
60
73
  createAnimatedComponent,
61
74
  ...drivers,
62
75
  };
@@ -5,7 +5,7 @@
5
5
  // only the one Vue-specific seam - building a runnable from a component provider via a synthetic
6
6
  // functional root (h() has no createElement-style prop/children spread of its own targets, so
7
7
  // the wrap step is a render function rather than an object merge).
8
- import { h } from '@vue/runtime-core';
8
+ import { h, } from '@vue/runtime-core';
9
9
  import { createAppRegistry, dlog, } from '@symbiote-native/engine';
10
10
  import { mount } from '../../render.js';
11
11
  function runnableFor(componentProvider, getWrapperComponentProvider) {
@@ -20,7 +20,9 @@ function runnableFor(componentProvider, getWrapperComponentProvider) {
20
20
  const wrapperComponentProvider = getWrapperComponentProvider();
21
21
  const mounted = wrapperComponentProvider === undefined
22
22
  ? renderRoot
23
- : () => h(wrapperComponentProvider(appParameters), null, { default: renderRoot });
23
+ : () => h(wrapperComponentProvider(appParameters), null, {
24
+ default: renderRoot,
25
+ });
24
26
  mount(appParameters.rootTag, mounted);
25
27
  };
26
28
  }
@@ -6,14 +6,16 @@
6
6
  // prop change, with the imperative statics attached to the component object.
7
7
  //
8
8
  // Inputs arrive as attrs (untyped) and run through normalizeVueAttrs before each is narrowed by a guard.
9
- import { defineComponent, watchEffect } from '@vue/runtime-core';
9
+ import { defineComponent, watchEffect, } from '@vue/runtime-core';
10
10
  import { applyStatusBarProps, statusBarImperative, statusBarCurrentHeight, isOpaqueColorValue, } from '@symbiote-native/engine';
11
11
  import { normalizeVueAttrs } from '../utils/normalize-attrs.js';
12
12
  function asBoolean(value) {
13
13
  return typeof value === 'boolean' ? value : undefined;
14
14
  }
15
15
  function asBarStyle(value) {
16
- return value === 'default' || value === 'light-content' || value === 'dark-content'
16
+ return value === 'default' ||
17
+ value === 'light-content' ||
18
+ value === 'dark-content'
17
19
  ? value
18
20
  : undefined;
19
21
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,35 @@
1
+ // Side-effect ONLY. This module exports nothing, on purpose.
2
+ //
3
+ // A lowered `<symbiote-pressable>` is an element, so the press machine lives on the engine node
4
+ // instead of inside a component — and it gets there by `registerPressableBehavior()` having run
5
+ // before the first node of that tag is created. The entry imports this file as a bare
6
+ // `import './register';`.
7
+ //
8
+ // WHY IT CANNOT BE A RE-EXPORT, and why this file has no exports to re-export in the first place.
9
+ // Metro turns on `inlineRequires` for production only: it moves a `require()` down to the first
10
+ // place its binding is USED, and a barrel's `export { X } from './x'` compiles to a lazy getter. A
11
+ // module reached only that way never evaluates unless something names X as a VALUE, so its
12
+ // registration silently never happens — in RELEASE builds only, invisible to tsc, to vitest, and
13
+ // even to grepping the bundle, because the code IS bundled and simply never runs. A bare
14
+ // `import './register';` placed NEXT TO a re-export of the same specifier does not help either:
15
+ // Babel merges the two into one dependency and the merged one stays lazy. See CLAUDE.md, "Never
16
+ // make correctness depend on a module's load-time side effect".
17
+ //
18
+ // `adapters/vue/src/index.test.ts` pins the barrel against both halves of that.
19
+ import { registerImageBehavior, registerInputAccessoryViewBehavior, registerPressableBehavior, registerSwitchBehavior, registerTextInputBehavior, } from '@symbiote-native/components';
20
+ registerPressableBehavior();
21
+ // Only the LOWERED tags carry this — the wrapper renders `symbiote-text-input-managed` and
22
+ // keeps running its own lifecycle. One owner per node; see `component-names/shared.ts`.
23
+ registerTextInputBehavior();
24
+ // Same reason as TextInput: the wrapper renders `symbiote-switch-managed` and runs its own
25
+ // lastNativeReport/snap-back lifecycle, so the engine's copy attaches only to the bare tag.
26
+ registerSwitchBehavior();
27
+ // Image owns no runtime — its behavior is a prop FOLD and nothing else, and it is registered on the
28
+ // same `symbiote-image` the wrapper already emits rather than on a `-managed` twin. That is safe
29
+ // only because the mapping is idempotent, which `core/components/src/behaviors/image.test.ts`
30
+ // asserts rather than assumes; a wrapper-built node simply folds a second time and nothing moves.
31
+ registerImageBehavior();
32
+ // Fold-only, and it shares the wrapper's tag for the same reason Image does: the mapping has no
33
+ // aliasing at all, so a wrapper-built node folding a second time moves nothing.
34
+ // `core/components/src/behaviors/input-accessory-view.test.ts` asserts that rather than assuming it.
35
+ registerInputAccessoryViewBehavior();
package/build/render.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import { type IRootTag, type SymbioteSurface } from '@symbiote-native/engine';
2
- import type { Component } from '@vue/runtime-core';
2
+ import type { App, Component } from '@vue/runtime-core';
3
+ export type IAppConfigurator = (app: App) => void;
4
+ export declare function setAppConfigurator(configure: IAppConfigurator | undefined): void;
3
5
  export declare function mount(rootTag: IRootTag, rootComponent: Component): SymbioteSurface;
4
6
  export declare function unmount(rootTag: IRootTag): void;
5
7
  declare global {
package/build/render.js CHANGED
@@ -1,12 +1,71 @@
1
1
  // Mount a Vue app onto a Fabric surface. The native host hands us a rootTag; we create
2
2
  // a surface for it and let the Vue renderer drive the engine, which commits into
3
3
  // nativeFabricUIManager; RN's own renderer never in the path.
4
- import { createSurface, disposeRoot, dlog, } from '@symbiote-native/engine';
4
+ import { createSurface, disposeRoot, dlog, reportUncaughtError, } from '@symbiote-native/engine';
5
5
  import { createSymbioteRenderer } from './renderer/index.js';
6
6
  // One Vue app per surface, so a surface can be torn down (unmount) or cleanly
7
7
  // re-mounted on the same rootTag: the bridgeless host stops and restarts a surface on
8
8
  // Fast Refresh and on lifecycle/focus changes, reusing the rootTag.
9
9
  const apps = new Map();
10
+ // A stack this deep already names the screen; the rest is noise in a redbox.
11
+ const MAX_STACK_FRAMES = 20;
12
+ // SFC components carry their name on `__name` (the compiler's), plain ones on `name`; neither is
13
+ // on Vue's public `ConcreteComponent` type, hence the guarded read rather than a cast.
14
+ function displayName(type) {
15
+ for (const key of ['name', '__name']) {
16
+ const value = Reflect.get(type, key);
17
+ if (typeof value === 'string' && value.length > 0)
18
+ return value;
19
+ }
20
+ return 'Anonymous';
21
+ }
22
+ // Vue's own component trace (getComponentTrace, used by `warn`) is internal, so the frames are
23
+ // walked off the public instance's parent chain. LogBox renders whatever string it is handed;
24
+ // React's shape is `\n in <Name>`, and matching it keeps the redbox reading the same in an
25
+ // app that mixes adapters' errors in one console.
26
+ function componentStack(instance) {
27
+ if (instance === null)
28
+ return null;
29
+ const frames = [];
30
+ let current = instance.$;
31
+ while (current !== null && frames.length < MAX_STACK_FRAMES) {
32
+ frames.push(`\n in ${displayName(current.type)}`);
33
+ current = current.parent;
34
+ }
35
+ return frames.length === 0 ? null : frames.join('');
36
+ }
37
+ // The app-level error seam. Without it, an unclaimed error fell through to Vue's own `logError`
38
+ // (runtime-core errorHandling.ts), which does neither useful thing: in a dev bundle it RE-THROWS
39
+ // out of `app.mount()`, so it escapes this function, aborts the AppRegistry runnable and leaves
40
+ // the surface half-brought-up; in a release bundle it console.errors a bare error with no origin
41
+ // and no component context, never touching `global.ErrorUtils`, so nothing reaches the host.
42
+ // Setting a handler suppresses both branches and routes to the engine's shared channel instead —
43
+ // the native redbox on a device, console.error off one.
44
+ //
45
+ // CAUGHT vs UNCAUGHT — the asymmetry is Vue's own, and it costs us no code. `handleError` walks
46
+ // the `onErrorCaptured` chain BEFORE consulting `config.errorHandler`, and a hook returning
47
+ // `false` stops propagation and returns early (runtime-core 3.5 errorHandling.ts), so a boundary
48
+ // that claims the error never reaches this function at all. That is the behaviour we want and
49
+ // the same split the React adapter makes by hand: writing a boundary IS the developer saying
50
+ // "I am handling this", and answering it with a full-screen redbox over the fallback the app
51
+ // just rendered contradicts what the app asked for.
52
+ //
53
+ // So do NOT "fix" this to report from inside an `onErrorCaptured` of our own — that would
54
+ // re-report exactly the errors a boundary claimed. A hook that returns undefined instead of
55
+ // `false` still lands here, and correctly: Vue treats that error as still propagating.
56
+ function reportToHost(error, instance, info) {
57
+ // `info` is Vue's own phase label ('render function', 'setup function', 'native event
58
+ // handler'), which is the closest thing it has to React's "no error boundary" / "recovered"
59
+ // qualifier. Off a native host it prefixes the log line, where there is no redbox to carry it.
60
+ reportUncaughtError(error, {
61
+ origin: `vue render (${String(info)})`,
62
+ componentStack: componentStack(instance),
63
+ });
64
+ }
65
+ let configurator;
66
+ export function setAppConfigurator(configure) {
67
+ configurator = configure;
68
+ }
10
69
  function teardown(rootTag) {
11
70
  const app = apps.get(rootTag);
12
71
  if (app === undefined)
@@ -22,6 +81,8 @@ export function mount(rootTag, rootComponent) {
22
81
  const surface = createSurface(rootTag);
23
82
  const renderer = createSymbioteRenderer(surface);
24
83
  const app = renderer.createApp(rootComponent);
84
+ app.config.errorHandler = reportToHost;
85
+ configurator?.(app);
25
86
  apps.set(rootTag, app);
26
87
  // The surface IS the Vue container: a top-level mutation routes to surface.appendChild
27
88
  // (renderer.ts), and the engine wraps surface.children in its synthetic flex root.
@@ -3,14 +3,47 @@
3
3
  // drives the exact same retained tree React does: the proof the core is framework-
4
4
  // agnostic.
5
5
  import { createRenderer } from '@vue/runtime-core';
6
- import { appendChild, createAnchor, createElement, createRawText, dlog, insertBefore, removeChild, routeProp, setText, toPublicInstance, RAW_TEXT_COMPONENT, SymbioteSurface, } from '@symbiote-native/engine';
6
+ import { appendChild, createAnchor, createElement, createRawText, dlog, insertBefore, removeChild, routeProp, setProp, setText, toPublicInstance, RAW_TEXT_COMPONENT, SymbioteSurface, } from '@symbiote-native/engine';
7
7
  import { descriptorFor } from '@symbiote-native/components';
8
+ import { normalizeVueAttrKey } from '../utils/normalize-attrs.js';
8
9
  function isSurface(parent) {
9
10
  return parent instanceof SymbioteSurface;
10
11
  }
11
12
  function isRawText(node) {
12
13
  return node.component === RAW_TEXT_COMPONENT;
13
14
  }
15
+ // RN's Text.js applies two defaults on the way to native (core/components/src/text-props.ts:
16
+ // ellipsizeMode 'tail', allowFontScaling true unless literally false). The Vue <Text> wrapper
17
+ // folded them with resolveTextProps; a template that the SFC transformer lowered to the
18
+ // intrinsic `symbiote-text` has no wrapper, so the renderer seeds them instead. Without this a
19
+ // numberOfLines={1} line clips mid-word with no ellipsis — device-observed, and silent.
20
+ const TEXT_DEFAULTS = new Map([
21
+ ['ellipsizeMode', 'tail'],
22
+ ['allowFontScaling', true],
23
+ ]);
24
+ function seedTextDefaults(node) {
25
+ for (const [key, value] of TEXT_DEFAULTS)
26
+ setProp(node, key, value);
27
+ }
28
+ // RN's `id` is the modern W3C-named alias for `nativeID` — View.js copies it over
29
+ // (`processedProps.nativeID = id`), so the two name ONE native prop. React folds it in its
30
+ // component wrapper and Svelte and Solid in their transforms; Vue had it nowhere, so `<View
31
+ // id="x">` reached Fabric with an unknown `id` and no `nativeID`, silently and on device only.
32
+ // It lives in the renderer rather than in a transform because that covers all four Vue paths at
33
+ // once — lowered SFC, lowered TSX, the component wrapper, and a hand-written
34
+ // `h('symbiote-view', { id })` no compiler ever sees.
35
+ //
36
+ // Caveat, and it matches what Solid's compile-time rename already does: with BOTH `id` and
37
+ // `nativeID` on one element the last patchProp wins, where upstream gives `id` priority
38
+ // unconditionally. Honouring that needs per-node state to remember an `id` arrived; no example or
39
+ // test sets both, so the state is not worth carrying.
40
+ const PROP_ALIASES = new Map([['id', 'nativeID']]);
41
+ // An explicit `undefined` must NOT clear one of those defaults: RN treats a missing prop and an
42
+ // explicit undefined alike, and only a literal `false` opts out of allowFontScaling. Reached
43
+ // only when a value is already undefined, so it costs nothing on the hot path.
44
+ function textDefaultFor(node, key) {
45
+ return node.isText ? TEXT_DEFAULTS.get(key) : undefined;
46
+ }
14
47
  // One renderer per mounted surface: the options close over the surface so every mutation
15
48
  // can ask it to (microtask-coalesced) recommit. Vue has no resetAfterCommit; instead
16
49
  // requestCommit() collapses a burst of insert/patchProp within one tick into a single
@@ -19,13 +52,23 @@ export function createSymbioteRenderer(surface) {
19
52
  const options = {
20
53
  createElement(type) {
21
54
  const descriptor = descriptorFor(type);
22
- const node = createElement(descriptor.component, descriptor.isText);
23
- // Graft the imperative public-instance API (measure / setNativeProps / focus / …) onto
24
- // the raw node so a template/function ref to a host element exposes it exactly like
25
- // React's getPublicInstance. toPublicInstance mutates in place and returns the SAME node
26
- // identity, so the engine commit mirror (keyed on the raw node) still resolves it — the
27
- // ref must keep holding this raw node by identity (shallowRef), never a deep ref.
28
- dlog(`vue createElement ${descriptor.component} -> public instance`);
55
+ // `type` as the third argument, not just `descriptor.component`: the behavior registry is
56
+ // keyed by the INTRINSIC TAG (`symbiote-pressable`), while a node only ever carries the
57
+ // resolved Fabric name (`RCTView`). This is the one place that still holds both, so a
58
+ // lowered primitive whose machine lives on the engine node can be matched at all.
59
+ const node = createElement(descriptor.component, descriptor.isText, type);
60
+ if (descriptor.isText)
61
+ seedTextDefaults(node);
62
+ // The imperative public-instance API (measure / setNativeProps / focus / …) is already on
63
+ // the node's prototype, so a template/function ref to a host element exposes it exactly
64
+ // like React's getPublicInstance and toPublicInstance is the identity. The ref must keep
65
+ // holding this raw node by identity (shallowRef), never a deep ref — the engine commit
66
+ // mirror is keyed on it.
67
+ //
68
+ // The message is a THUNK, not a template literal: this runs once per node — 9 000 times on
69
+ // one benchmark press — and a literal is built at the call site before dlog can decide
70
+ // anything (see core/engine/src/debug.ts).
71
+ dlog(() => `vue createElement ${descriptor.component} -> public instance`);
29
72
  return toPublicInstance(node);
30
73
  },
31
74
  createText(text) {
@@ -110,11 +153,17 @@ export function createSymbioteRenderer(surface) {
110
153
  patchProp(el, key, _prev, next) {
111
154
  if (isSurface(el))
112
155
  return;
156
+ // Kebab -> camel happens HERE, not only inside a component wrapper: the SFC transformer
157
+ // lowers View/Text to their intrinsic tags (metro-vue-transformer.cjs), so those props
158
+ // arrive one key at a time with no component to fold the bag. Idempotent for the wrapped
159
+ // path, which already normalized.
160
+ const normalized = normalizeVueAttrKey(key);
161
+ const name = PROP_ALIASES.get(normalized) ?? normalized;
113
162
  // routeProp makes the prop-vs-event decision from the node's ViewConfig (onPress on a
114
163
  // View becomes a listener; onTintColor on a Switch stays a prop), shared with React. The
115
164
  // class/style merge (explicit :style always winning, regardless of which of Vue's two
116
165
  // independent patchProp calls lands last) is centralized there too (core/engine/src/node.ts).
117
- routeProp(el, key, next);
166
+ routeProp(el, name, next === undefined ? textDefaultFor(el, name) : next);
118
167
  surface.requestCommit();
119
168
  },
120
169
  // RN has no querySelector / scope-id / innerHTML. The first two are inert; static