@symbiote-native/vue 0.4.0 → 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 (42) hide show
  1. package/README.md +6 -6
  2. package/build/bootstrap.js +1 -1
  3. package/build/components/flat-list/index.js +14 -5
  4. package/build/components/image-background.js +4 -5
  5. package/build/components/image.js +3 -1
  6. package/build/components/keyboard-avoiding-view.js +39 -14
  7. package/build/components/modal/index.js +5 -2
  8. package/build/components/pressable.d.ts +1 -1
  9. package/build/components/pressable.js +1 -1
  10. package/build/components/scroll-view/index.android.d.ts +1 -1
  11. package/build/components/scroll-view/index.android.js +3 -1
  12. package/build/components/scroll-view/index.ios.d.ts +1 -1
  13. package/build/components/scroll-view/index.ios.js +3 -1
  14. package/build/components/scroll-view/shared.js +33 -13
  15. package/build/components/scroll-view/sticky-header.js +18 -5
  16. package/build/components/section-list/index.js +4 -2
  17. package/build/components/switch/shared.js +12 -3
  18. package/build/components/text-input/index.js +7 -2
  19. package/build/components/touchable-native-feedback.js +3 -1
  20. package/build/components/touchable.d.ts +7 -1
  21. package/build/components/touchable.js +181 -33
  22. package/build/components/virtualized-list/index.js +42 -20
  23. package/build/components/virtualized-section-list/index.d.ts +5 -0
  24. package/build/components/virtualized-section-list/index.js +33 -4
  25. package/build/components.js +8 -3
  26. package/build/create-portal/index.d.ts +28 -0
  27. package/build/create-portal/index.js +42 -0
  28. package/build/host-instance/index.js +3 -1
  29. package/build/index.d.ts +8 -6
  30. package/build/index.js +9 -3
  31. package/build/modules/animated/create-animated-component.js +12 -4
  32. package/build/modules/animated/index.d.ts +2 -0
  33. package/build/modules/animated/index.js +18 -5
  34. package/build/modules/app-registry/index.js +4 -2
  35. package/build/modules/status-bar.js +4 -2
  36. package/build/render.d.ts +3 -1
  37. package/build/render.js +62 -1
  38. package/build/runtime-helpers/index.d.ts +44 -24
  39. package/build/runtime-helpers/index.js +107 -28
  40. package/build/utils/normalize-attrs.js +3 -1
  41. package/metro-vue-transformer.cjs +138 -97
  42. package/package.json +5 -5
package/build/index.d.ts CHANGED
@@ -1,4 +1,6 @@
1
- export { mount, unmount } from './render';
1
+ export { mount, unmount, setAppConfigurator } from './render';
2
+ export type { IAppConfigurator } from './render';
3
+ export { Teleport, type ITeleportTarget } from './create-portal';
2
4
  export { createTunnel, type ITunnel } from './create-tunnel';
3
5
  export { View, Text } from './components';
4
6
  export type { IAccessibilityProps, IAriaProps, IAccessibilityRole, IRole, IAccessibilityStateValue, IAccessibilityValue, IAccessibilityActionInfo, IResponderProps, } from '@symbiote-native/components';
@@ -21,7 +23,7 @@ export type { INativeFeedbackBackground, IThemeAttrBackground, IRippleBackground
21
23
  export { Button } from './components/button';
22
24
  export type { IButtonProps } from './components/button';
23
25
  export { TextInput } from './components/text-input';
24
- export type { ITextInputProps, ITextInputHandle } from './components/text-input';
26
+ export type { ITextInputProps, ITextInputHandle, } from './components/text-input';
25
27
  export { VirtualizedList } from './components/virtualized-list';
26
28
  export type { IVirtualizedListProps, IVirtualizedListSlots, IVirtualizedListEmits, IVirtualizedListHandle, ISeparators, ISeparatorProps, IViewToken, IViewableItemsChangedInfo, IViewabilityConfig, IViewabilityConfigCallbackPair, ICellLayout, } from './components/virtualized-list';
27
29
  export { FlatList } from './components/flat-list';
@@ -33,7 +35,7 @@ export type { ISection, ISectionListProps, ISectionListSlots, ISectionListEmits,
33
35
  export { SafeAreaView } from './components/safe-area-view';
34
36
  export type { ISafeAreaViewProps } from './components/safe-area-view';
35
37
  export { RefreshControl } from './components/refresh-control';
36
- export type { IRefreshControlProps, IRefreshControlEmits } from './components/refresh-control';
38
+ export type { IRefreshControlProps, IRefreshControlEmits, } from './components/refresh-control';
37
39
  export { descriptorToVue } from './descriptor-to-vue';
38
40
  export { normalizeVueAttrs } from './utils/normalize-attrs';
39
41
  export { resolveModelValue, emitModelUpdate } from './utils/model-binding';
@@ -56,10 +58,10 @@ export { useWindowDimensions } from './composables/use-window-dimensions';
56
58
  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
59
  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
60
  export { Platform, StyleSheet, processColor, setColorProcessor, dlog, isDebug, } from '@symbiote-native/engine';
59
- export type { ISymbioteEvent, ISymbioteNode, IRootTag } from '@symbiote-native/engine';
61
+ export type { ISymbioteEvent, ISymbioteNode, IRootTag, } from '@symbiote-native/engine';
60
62
  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';
63
+ export { setNativeViewConfigSource, setDeviceEventSource, } from '@symbiote-native/engine';
64
+ export type { INativeViewConfig, INativeViewConfigSource, } from '@symbiote-native/engine';
63
65
  export type { IEnterKeyHint, IInputMode, ISubmitBehavior, ITextInputSelection, IImageStatics, } from '@symbiote-native/components';
64
66
  export { PixelRatio, PanResponder, PlatformColor, DynamicColorIOS, InteractionManager, } from '@symbiote-native/engine';
65
67
  export type { IPixelRatioStatic } from '@symbiote-native/engine';
package/build/index.js CHANGED
@@ -1,8 +1,14 @@
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
+ export { mount, unmount, setAppConfigurator } from './render.js';
5
+ // The portal: Vue's own <Teleport>, guarded so `to` must be a node/surface this renderer actually
6
+ // mounted (there is no querySelector). It MOVES host nodes, so it reaches any already-mounted
7
+ // target in the SAME surface — including one you only hold a ref to — and keeps the content's
8
+ // reactive owner at the call site (provide/inject resolve from where it was written). Reaching a
9
+ // second, independently mount()ed surface is a different mechanism: createTunnel, which copies
10
+ // into an <Out/> the destination has to render. See create-portal/index.ts.
11
+ export { Teleport } from './create-portal/index.js';
6
12
  export { createTunnel } from './create-tunnel/index.js';
7
13
  export { View, Text } from './components.js';
8
14
  // IHostInstance is the raw engine node (Vue host refs fall through to it; imperative methods
@@ -50,7 +56,7 @@ export { Platform, StyleSheet, processColor, setColorProcessor, dlog, isDebug, }
50
56
  // Wired once by the app entry on a real host (like setColorProcessor): hands the engine
51
57
  // RN's ViewConfig registry so third-party Fabric views auto-derive their metadata.
52
58
  // setDeviceEventSource is the third seam of the same set and travels with them.
53
- export { setNativeViewConfigSource, setDeviceEventSource } from '@symbiote-native/engine';
59
+ export { setNativeViewConfigSource, setDeviceEventSource, } from '@symbiote-native/engine';
54
60
  // Pure utilities that moved to the engine (single source, both adapters re-export):
55
61
  // PixelRatio + PanResponder, plus the color builders and the interaction scheduler.
56
62
  export { PixelRatio, PanResponder, PlatformColor, DynamicColorIOS, InteractionManager, } from '@symbiote-native/engine';
@@ -14,7 +14,8 @@ const PASSTHROUGH_PROP = 'passthroughAnimatedPropExplicitValues';
14
14
  // Reads either a functional component's or a stateful defineComponent's display name, without a
15
15
  // cast, for the wrapper's devtools name.
16
16
  function baseName(component) {
17
- if (component === null || (typeof component !== 'function' && typeof component !== 'object')) {
17
+ if (component === null ||
18
+ (typeof component !== 'function' && typeof component !== 'object')) {
18
19
  return 'Anonymous';
19
20
  }
20
21
  const display = Reflect.get(component, 'displayName');
@@ -68,7 +69,9 @@ export function createAnimatedComponent(Component) {
68
69
  },
69
70
  has: (_target, key) => {
70
71
  const instance = instanceRef.value;
71
- return instance !== null && typeof instance === 'object' && Reflect.has(instance, key);
72
+ return (instance !== null &&
73
+ typeof instance === 'object' &&
74
+ Reflect.has(instance, key));
72
75
  },
73
76
  }));
74
77
  return () => {
@@ -96,10 +99,15 @@ export function createAnimatedComponent(Component) {
96
99
  const passthroughStyle = readPassthroughStyle(passthrough);
97
100
  if (passthroughStyle !== undefined) {
98
101
  reduced.style =
99
- reduced.style === undefined ? passthroughStyle : [reduced.style, passthroughStyle];
102
+ reduced.style === undefined
103
+ ? passthroughStyle
104
+ : [reduced.style, passthroughStyle];
100
105
  }
101
106
  reduced.ref = captureRef;
102
- return h(Component, reduced, slots.default !== undefined ? { default: slots.default } : undefined);
107
+ // Forward the WHOLE slots object, not just `default`: the list containers take their
108
+ // content through named scoped slots (`item`, `sectionHeader`, ...), which a default-only
109
+ // forward drops silently - Animated.FlatList would commit empty cells.
110
+ return h(Component, reduced, slots);
103
111
  };
104
112
  },
105
113
  });
@@ -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
  }
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.
@@ -1,28 +1,48 @@
1
1
  export * from '@vue/runtime-core';
2
2
  import { type ObjectDirective } from '@vue/runtime-core';
3
3
  import { type ISymbioteNode } from '@symbiote-native/engine';
4
+ export { Teleport, type ITeleportTarget } from '../create-portal';
4
5
  export declare const vShow: ObjectDirective<ISymbioteNode, boolean>;
5
- export declare const Teleport: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
6
- to: {
7
- type: null;
8
- default: null;
9
- };
10
- disabled: {
11
- type: BooleanConstructor;
12
- default: boolean;
13
- };
14
- }>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
15
- [key: string]: any;
16
- }>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
17
- to: {
18
- type: null;
19
- default: null;
20
- };
21
- disabled: {
22
- type: BooleanConstructor;
23
- default: boolean;
24
- };
25
- }>> & Readonly<{}>, {
26
- disabled: boolean;
27
- to: any;
28
- }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
6
+ type ISystemModifier = 'ctrl' | 'shift' | 'alt' | 'meta';
7
+ type IEventModifier = ISystemModifier | 'stop' | 'prevent' | 'self' | 'left' | 'middle' | 'right' | 'exact';
8
+ type IModifierGuardableEvent = {
9
+ stopPropagation?: () => void;
10
+ preventDefault?: () => void;
11
+ target?: unknown;
12
+ currentTarget?: unknown;
13
+ button?: number;
14
+ ctrlKey?: boolean;
15
+ shiftKey?: boolean;
16
+ altKey?: boolean;
17
+ metaKey?: boolean;
18
+ };
19
+ type IEventHandler<TEvent> = ((event: TEvent, ...args: never[]) => unknown) & {
20
+ _withMods?: Record<string, IEventHandler<TEvent>>;
21
+ };
22
+ /**
23
+ * Render-function/compiled-template equivalent of `v-on.stop`/`.prevent`/`.self`/etc. `fn` is
24
+ * typed as required, matching upstream Vue's own declaration — its runtime-only `!fn` guard below
25
+ * (for a compiler-generated call site that could pass a falsy handler) isn't reflected in the
26
+ * type there either.
27
+ */
28
+ export declare function withModifiers<TEvent extends IModifierGuardableEvent>(fn: IEventHandler<TEvent>, modifiers: readonly IEventModifier[]): IEventHandler<TEvent>;
29
+ type IKeyedEvent = {
30
+ key?: string;
31
+ };
32
+ type IKeyedHandler<TEvent extends IKeyedEvent> = ((event: TEvent) => unknown) & {
33
+ _withKeys?: Record<string, (event: TEvent) => unknown>;
34
+ };
35
+ /**
36
+ * Render-function/compiled-template equivalent of `v-on.enter`/`.esc`/etc. No-ops on any event
37
+ * without a `.key` field (e.g. Symbiote's Pressable `onPress`) — same as upstream Vue's own
38
+ * behavior on a non-keyboard DOM event, not a Symbiote-specific gap. `TextInput`'s `onKeyPress`
39
+ * carries `nativeEvent.key`, so that is the applicable Symbiote event shape.
40
+ */
41
+ export declare function withKeys<TEvent extends IKeyedEvent>(fn: IKeyedHandler<TEvent>, modifiers: readonly string[]): (event: TEvent) => unknown;
42
+ /**
43
+ * Reads the class map a `<style module>` SFC block compiles onto the component's own options
44
+ * (`__cssModules`, set by @symbiote-native/css-parser's SFC compiler — see the
45
+ * `symbiote-sfc-style-compiler` skill). Warns and returns an empty map exactly like upstream Vue
46
+ * does when the current component has no such block, rather than throwing.
47
+ */
48
+ export declare function useCssModule(name?: string): Record<string, string>;
@@ -9,8 +9,13 @@
9
9
  // Scope: this covers only Vue's own template directives (v-show, Teleport); native-element
10
10
  // v-model is a separate, out-of-scope case.
11
11
  export * from '@vue/runtime-core';
12
- import { defineComponent, h, Teleport as VueTeleport, } from '@vue/runtime-core';
13
- import { isSymbioteNode, setNativeProps, whenCommitted, } from '@symbiote-native/engine';
12
+ import { getCurrentInstance, warn, } from '@vue/runtime-core';
13
+ import { setNativeProps, whenCommitted, } from '@symbiote-native/engine';
14
+ // Shadows runtime-core's own Teleport for every compiled `from 'vue'` import (see the header):
15
+ // same component, plus a runtime guard on `to`. Lives in ../create-portal so the adapter's portal
16
+ // sits where every other adapter's does; re-exported here because THIS is the module Metro
17
+ // rewrites `vue` to, and an `export *` alone would resolve `Teleport` to the unguarded original.
18
+ export { Teleport } from '../create-portal/index.js';
14
19
  // whenCommitted, not a direct call: Vue's `mounted` hook fires synchronously during the patch
15
20
  // pass, but this renderer coalesces the actual Fabric commit onto a microtask
16
21
  // (surface.requestCommit()), so the node may have no committed tag yet - the same async-commit
@@ -28,31 +33,105 @@ export const vShow = {
28
33
  // Drop a still-pending first-commit wait if the element unmounts before it ever lands.
29
34
  unmounted: el => pendingShowCommits.get(el)?.(),
30
35
  };
31
- // Teleport's `to` in the DOM world is usually a CSS-selector string ('body', '#modal-root'); we
32
- // have no querySelector, so `to` here must be an already-mounted host node - e.g. a ref to a
33
- // persistent "overlay host" View rendered once near the app root. This wrapper shadows
34
- // runtime-core's own Teleport purely to validate `to` BEFORE handing it to the real Teleport: a
35
- // wrong value throws immediately here instead of silently corrupting the retained tree deep
36
- // inside insert/remove. Scope: same-surface targets only (see the React createPortal twin,
37
- // create-portal.ts) - Vue's own Teleport internals work unmodified once `to` resolves to a real
38
- // ISymbioteNode.
39
- export const Teleport = defineComponent({
40
- name: 'Teleport',
41
- inheritAttrs: false,
42
- props: {
43
- to: { type: null, default: null },
44
- disabled: { type: Boolean, default: false },
36
+ const SYSTEM_MODIFIERS = [
37
+ 'ctrl',
38
+ 'shift',
39
+ 'alt',
40
+ 'meta',
41
+ ];
42
+ const MODIFIER_GUARDS = {
43
+ stop: event => {
44
+ event.stopPropagation?.();
45
+ return false;
45
46
  },
46
- setup(props, { slots }) {
47
- return () => {
48
- const { to } = props;
49
- if (typeof to === 'string') {
50
- 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").`);
51
- }
52
- if (to != null && !isSymbioteNode(to)) {
53
- throw new Error('Teleport target is not a real host node — did you forget `.value` on a ref, or pass something symbiote never rendered?');
54
- }
55
- return h(VueTeleport, { to, disabled: props.disabled }, slots);
56
- };
47
+ prevent: event => {
48
+ event.preventDefault?.();
49
+ return false;
57
50
  },
58
- });
51
+ self: event => event.target !== event.currentTarget,
52
+ ctrl: event => !event.ctrlKey,
53
+ shift: event => !event.shiftKey,
54
+ alt: event => !event.altKey,
55
+ meta: event => !event.metaKey,
56
+ left: event => event.button !== 0,
57
+ middle: event => event.button !== 1,
58
+ right: event => event.button !== 2,
59
+ exact: (event, modifiers) => SYSTEM_MODIFIERS.some(modifier => event[`${modifier}Key`] && !modifiers.includes(modifier)),
60
+ };
61
+ /**
62
+ * Render-function/compiled-template equivalent of `v-on.stop`/`.prevent`/`.self`/etc. `fn` is
63
+ * typed as required, matching upstream Vue's own declaration — its runtime-only `!fn` guard below
64
+ * (for a compiler-generated call site that could pass a falsy handler) isn't reflected in the
65
+ * type there either.
66
+ */
67
+ export function withModifiers(fn, modifiers) {
68
+ if (!fn)
69
+ return fn;
70
+ const cache = fn._withMods ?? (fn._withMods = {});
71
+ const cacheKey = modifiers.join('.');
72
+ return (cache[cacheKey] ??= ((event, ...args) => {
73
+ for (const modifier of modifiers) {
74
+ if (MODIFIER_GUARDS[modifier](event, modifiers))
75
+ return undefined;
76
+ }
77
+ return fn(event, ...args);
78
+ }));
79
+ }
80
+ const KEY_ALIASES = {
81
+ esc: 'escape',
82
+ space: ' ',
83
+ up: 'arrow-up',
84
+ left: 'arrow-left',
85
+ right: 'arrow-right',
86
+ down: 'arrow-down',
87
+ delete: 'backspace',
88
+ };
89
+ // \B (non-word-boundary), not a bare capture: without it, "Enter" hyphenates to "-enter" (a
90
+ // leading dash) instead of "enter", since the very first character is otherwise still a match.
91
+ function hyphenate(camelCase) {
92
+ return camelCase.replace(/\B([A-Z])/g, '-$1').toLowerCase();
93
+ }
94
+ /**
95
+ * Render-function/compiled-template equivalent of `v-on.enter`/`.esc`/etc. No-ops on any event
96
+ * without a `.key` field (e.g. Symbiote's Pressable `onPress`) — same as upstream Vue's own
97
+ * behavior on a non-keyboard DOM event, not a Symbiote-specific gap. `TextInput`'s `onKeyPress`
98
+ * carries `nativeEvent.key`, so that is the applicable Symbiote event shape.
99
+ */
100
+ export function withKeys(fn, modifiers) {
101
+ const cache = fn._withKeys ?? (fn._withKeys = {});
102
+ const cacheKey = modifiers.join('.');
103
+ return (cache[cacheKey] ??= (event) => {
104
+ if (event.key == null)
105
+ return undefined;
106
+ const eventKey = hyphenate(event.key);
107
+ if (modifiers.some(modifier => modifier === eventKey || KEY_ALIASES[modifier] === eventKey)) {
108
+ return fn(event);
109
+ }
110
+ return undefined;
111
+ });
112
+ }
113
+ /**
114
+ * Reads the class map a `<style module>` SFC block compiles onto the component's own options
115
+ * (`__cssModules`, set by @symbiote-native/css-parser's SFC compiler — see the
116
+ * `symbiote-sfc-style-compiler` skill). Warns and returns an empty map exactly like upstream Vue
117
+ * does when the current component has no such block, rather than throwing.
118
+ */
119
+ export function useCssModule(name = '$style') {
120
+ const instance = getCurrentInstance();
121
+ if (!instance) {
122
+ warn('useCssModule must be called inside setup()');
123
+ return {};
124
+ }
125
+ const modules = instance.type
126
+ .__cssModules;
127
+ if (!modules) {
128
+ warn('Current instance does not have CSS modules injected.');
129
+ return {};
130
+ }
131
+ const cssModule = modules[name];
132
+ if (!cssModule) {
133
+ warn(`Current instance does not have CSS module named "${name}".`);
134
+ return {};
135
+ }
136
+ return cssModule;
137
+ }
@@ -16,7 +16,9 @@ export function normalizeVueAttrs(attrs) {
16
16
  let changed = false;
17
17
  const out = {};
18
18
  for (const key of Object.keys(attrs)) {
19
- if (key.includes('-') && !key.startsWith('aria-') && !key.startsWith('data-')) {
19
+ if (key.includes('-') &&
20
+ !key.startsWith('aria-') &&
21
+ !key.startsWith('data-')) {
20
22
  out[toCamel(key)] = attrs[key];
21
23
  changed = true;
22
24
  }