@symbiote-native/vue 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +142 -0
  3. package/build/components/activity-indicator/index.android.d.ts +4 -0
  4. package/build/components/activity-indicator/index.android.js +10 -0
  5. package/build/components/activity-indicator/index.d.ts +1 -0
  6. package/build/components/activity-indicator/index.ios.d.ts +4 -0
  7. package/build/components/activity-indicator/index.ios.js +10 -0
  8. package/build/components/activity-indicator/index.js +4 -0
  9. package/build/components/activity-indicator/shared.d.ts +8 -0
  10. package/build/components/activity-indicator/shared.js +48 -0
  11. package/build/components/button.d.ts +14 -0
  12. package/build/components/button.js +59 -0
  13. package/build/components/drawer-layout-android/index.android.d.ts +8 -0
  14. package/build/components/drawer-layout-android/index.android.js +144 -0
  15. package/build/components/drawer-layout-android/index.d.ts +8 -0
  16. package/build/components/drawer-layout-android/index.js +36 -0
  17. package/build/components/drawer-layout-android/shared.d.ts +18 -0
  18. package/build/components/drawer-layout-android/shared.js +6 -0
  19. package/build/components/flat-list/index.d.ts +88 -0
  20. package/build/components/flat-list/index.js +189 -0
  21. package/build/components/image-background.d.ts +9 -0
  22. package/build/components/image-background.js +114 -0
  23. package/build/components/image.d.ts +9 -0
  24. package/build/components/image.js +99 -0
  25. package/build/components/input-accessory-view.d.ts +10 -0
  26. package/build/components/input-accessory-view.js +41 -0
  27. package/build/components/keyboard-avoiding-view.d.ts +18 -0
  28. package/build/components/keyboard-avoiding-view.js +121 -0
  29. package/build/components/modal.d.ts +29 -0
  30. package/build/components/modal.js +134 -0
  31. package/build/components/pressable.d.ts +58 -0
  32. package/build/components/pressable.js +221 -0
  33. package/build/components/refresh-control.d.ts +20 -0
  34. package/build/components/refresh-control.js +53 -0
  35. package/build/components/safe-area-view.d.ts +8 -0
  36. package/build/components/safe-area-view.js +33 -0
  37. package/build/components/scroll-view/index.android.d.ts +4 -0
  38. package/build/components/scroll-view/index.android.js +74 -0
  39. package/build/components/scroll-view/index.d.ts +1 -0
  40. package/build/components/scroll-view/index.ios.d.ts +4 -0
  41. package/build/components/scroll-view/index.ios.js +15 -0
  42. package/build/components/scroll-view/index.js +4 -0
  43. package/build/components/scroll-view/shared.d.ts +96 -0
  44. package/build/components/scroll-view/shared.js +297 -0
  45. package/build/components/scroll-view/sticky-header.d.ts +7 -0
  46. package/build/components/scroll-view/sticky-header.js +195 -0
  47. package/build/components/section-list/index.d.ts +39 -0
  48. package/build/components/section-list/index.js +77 -0
  49. package/build/components/switch/index.android.d.ts +12 -0
  50. package/build/components/switch/index.android.js +13 -0
  51. package/build/components/switch/index.d.ts +2 -0
  52. package/build/components/switch/index.ios.d.ts +12 -0
  53. package/build/components/switch/index.ios.js +11 -0
  54. package/build/components/switch/index.js +5 -0
  55. package/build/components/switch/shared.d.ts +23 -0
  56. package/build/components/switch/shared.js +129 -0
  57. package/build/components/text-input/index.d.ts +24 -0
  58. package/build/components/text-input/index.js +257 -0
  59. package/build/components/touchable-native-feedback.d.ts +24 -0
  60. package/build/components/touchable-native-feedback.js +75 -0
  61. package/build/components/touchable.d.ts +44 -0
  62. package/build/components/touchable.js +150 -0
  63. package/build/components/virtualized-list/index.d.ts +121 -0
  64. package/build/components/virtualized-list/index.js +687 -0
  65. package/build/components/virtualized-section-list/index.d.ts +96 -0
  66. package/build/components/virtualized-section-list/index.js +160 -0
  67. package/build/components.d.ts +51 -0
  68. package/build/components.js +26 -0
  69. package/build/composables/use-color-scheme.d.ts +3 -0
  70. package/build/composables/use-color-scheme.js +24 -0
  71. package/build/composables/use-window-dimensions.d.ts +3 -0
  72. package/build/composables/use-window-dimensions.js +34 -0
  73. package/build/create-tunnel.d.ts +9 -0
  74. package/build/create-tunnel.js +37 -0
  75. package/build/descriptor-to-vue.d.ts +3 -0
  76. package/build/descriptor-to-vue.js +14 -0
  77. package/build/host-instance/index.d.ts +2 -0
  78. package/build/host-instance/index.js +34 -0
  79. package/build/index.d.ts +67 -0
  80. package/build/index.js +90 -0
  81. package/build/modules/animated/create-animated-component.d.ts +4 -0
  82. package/build/modules/animated/create-animated-component.js +169 -0
  83. package/build/modules/animated/index.d.ts +55 -0
  84. package/build/modules/animated/index.js +66 -0
  85. package/build/modules/app-registry/index.d.ts +7 -0
  86. package/build/modules/app-registry/index.js +32 -0
  87. package/build/modules/status-bar.d.ts +17 -0
  88. package/build/modules/status-bar.js +65 -0
  89. package/build/render.d.ts +7 -0
  90. package/build/render.js +43 -0
  91. package/build/renderer.d.ts +4 -0
  92. package/build/renderer.js +129 -0
  93. package/build/runtime-helpers.d.ts +28 -0
  94. package/build/runtime-helpers.js +68 -0
  95. package/build/utils/component-helpers.d.ts +2 -0
  96. package/build/utils/component-helpers.js +13 -0
  97. package/build/utils/model-binding.d.ts +4 -0
  98. package/build/utils/model-binding.js +17 -0
  99. package/build/utils/normalize-attrs.d.ts +1 -0
  100. package/build/utils/normalize-attrs.js +30 -0
  101. package/build/utils/slots-to-render-props.d.ts +4 -0
  102. package/build/utils/slots-to-render-props.js +18 -0
  103. package/metro-css-parser.cjs +12 -0
  104. package/package.json +54 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 A. Prokopenko
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,142 @@
1
+ # @symbiote-native/vue
2
+
3
+ The **Vue 3 adapter** for [SymbioteJS](../../README.md) — render real native iOS/Android views from
4
+ Vue, on the *same* untouched core as React, with React Native's own renderer never in the path. It
5
+ is a `@vue/runtime-core` `createRenderer` whose nodeOps map each mutation onto the engine's
6
+ four-call API; `@symbiote-native/engine` does the clone-on-write commit into Fabric.
7
+
8
+ Vue is the **proof the core is genuinely framework-agnostic** (milestone R4): a second, non-React,
9
+ mutation-oriented framework driving the already-validated engine.
10
+
11
+ <div align="center">
12
+
13
+ ![Vue 3 driving real native iOS views through SymbioteJS](../../assets/vue-demo.gif)
14
+
15
+ </div>
16
+
17
+ > New to SymbioteJS? The [root README](../../README.md) has the architecture.
18
+
19
+ ---
20
+
21
+ ## Use it
22
+
23
+ The native entry reaches the *same* `registerRunnable` seam as React — only the adapter changes. It
24
+ hands the surface's `rootTag` to `mount` from `@symbiote-native/vue`, which drives the engine through Vue's
25
+ `createRenderer`:
26
+
27
+ ```js
28
+ // index.js
29
+ import { AppRegistry as RNAppRegistry } from 'react-native';
30
+ import { mount } from '@symbiote-native/vue';
31
+ import App from './App';
32
+ import { name as appName } from './app.json';
33
+
34
+ // registerRunnable (not registerComponent): RN stores a raw mount callback and never renders
35
+ // it with its own renderer. We mount the Vue app onto the surface's rootTag.
36
+ RNAppRegistry.registerRunnable(appName, ({ rootTag }) => {
37
+ mount(rootTag, App);
38
+ });
39
+ ```
40
+
41
+ The app is ordinary Vue — it just imports primitives from `@symbiote-native/vue` instead of
42
+ `react-native`. A tap→increment counter, authored in TSX:
43
+
44
+ ```jsx
45
+ import { ref } from '@vue/runtime-core';
46
+ import { View, Text, Pressable } from '@symbiote-native/vue';
47
+
48
+ export default {
49
+ setup() {
50
+ const count = ref(0);
51
+ return () => (
52
+ <View style={{ padding: 24 }}>
53
+ <Text>Taps: {count.value}</Text>
54
+ <Pressable onPress={() => count.value++}>
55
+ <Text>Tap me</Text>
56
+ </Pressable>
57
+ </View>
58
+ );
59
+ },
60
+ };
61
+ ```
62
+
63
+ ### Two example apps
64
+
65
+ The full canary exists in two flavors — both render the same surface as the React reference, and
66
+ the demo above is the first one running on the iOS simulator:
67
+
68
+ - [`examples/vue-tsx`](../../examples/vue-tsx) — authored in **TSX**.
69
+ - [`examples/vue-sfc`](../../examples/vue-sfc) — authored in **single-file components**, compiled by
70
+ a per-framework Metro transformer.
71
+
72
+ ---
73
+
74
+ ## Parity — and the one gap
75
+
76
+ Both adapters reach the same primitives, runtime modules, `Animated` on both drivers, gestures,
77
+ accessibility, and the `VirtualizedList` family. That parity is **structural, not hand-copied**: the
78
+ component logic (state machines + render functions) is written **once** in `@symbiote-native/components`,
79
+ and each adapter supplies only its lifecycle (Vue's `ref`/`watch` + the descriptor→`h()` bridge).
80
+
81
+ The one deliberate gap is third-party **React component** packages such as
82
+ `@react-native-community/slider` used directly. Their body calls React hooks off the React
83
+ dispatcher, so they run only under the React adapter — under Vue the dispatcher is null and they
84
+ throw. SymbioteJS makes the *native view* framework-agnostic, not the library's React *component*; such
85
+ a view becomes reachable from Vue only through a thin wrapper over the same `createNode`-by-ViewConfig
86
+ path SymbioteJS uses for its own primitives. `@symbiote-native/slider` (this repo's own wrapper around
87
+ `@react-native-community/slider`) *does* ship a real Vue build (`@symbiote-native/slider/vue`) through
88
+ exactly that path — it's what makes this one third-party native view usable from Vue at all; any
89
+ *other* React-only component package stays React-adapter-only until it gets the same treatment.
90
+
91
+ ---
92
+
93
+ ## A Vue-specific gotcha — async commit timing
94
+
95
+ Vue batches commits on a microtask (every mutation schedules one `completeRoot`), so a node's
96
+ Fabric tag is assigned *after* `onMounted` / `watch(flush:'post')` runs. A native bind that reads
97
+ the tag at lifecycle time (native-driver `Animated`, sticky-header scroll attach, `TextInput`
98
+ autoFocus) would race the commit and silently no-op — while the JS-path headless test stays green.
99
+ React doesn't hit this because `react-reconciler` commits synchronously.
100
+
101
+ The fix lives in the engine: `whenCommitted(node, action)` runs `action` now if the node already
102
+ has a tag, else after the commit that assigns it. Any native/imperative call wired at Vue lifecycle
103
+ time must go through it. This is the only place the Vue adapter's timing differs from React's — see
104
+ the `vue-adapter-reactivity` notes and `core/engine/src/post-commit.ts`.
105
+
106
+ ---
107
+
108
+ ## Run it
109
+
110
+ Each example is a stock React Native 0.86 app — the steps are identical to the
111
+ [React adapter](../react/README.md#run-it), just swap the directory:
112
+
113
+ ```bash
114
+ cd examples/vue-tsx # or examples/vue-sfc
115
+ npm install
116
+ bundle install # first time only — installs CocoaPods itself
117
+ bundle exec pod install # fetch native pods
118
+
119
+ # terminal 1 — Metro (DEBUG=1 turns on diagnostic logs)
120
+ DEBUG=1 npm start --reset-cache
121
+
122
+ # terminal 2 — build + launch
123
+ npm run ios # iOS simulator
124
+ npm run android # Android emulator
125
+ ```
126
+
127
+ ---
128
+
129
+ ## Test it
130
+
131
+ ```bash
132
+ pnpm test # vitest, from the workspace root — headless, fake Fabric slot
133
+
134
+ cd examples/vue-tsx
135
+ npm run e2e:build:ios # build the app for Detox
136
+ npm run e2e:test:ios # run the canary journeys on the iOS simulator
137
+ # …or the android equivalents: e2e:build:android / e2e:test:android
138
+ ```
139
+
140
+ The headless suite includes Vue regression guards for the async-commit gotcha above (native-driver
141
+ `Animated`, sticky scroll, autoFocus). Why testing comes for free: see
142
+ [Testing](../../README.md#testing).
@@ -0,0 +1,4 @@
1
+ export type { IActivityIndicatorProps } from './shared';
2
+ export declare const ActivityIndicator: import("vue").DefineComponent<{}, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
3
+ [key: string]: any;
4
+ }>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
@@ -0,0 +1,10 @@
1
+ // ActivityIndicator on Android: AndroidProgressBar needs `styleAttr` (it drives the
2
+ // ProgressBar's setStyle(); without it the view throws "setStyle() not called") and
3
+ // `indeterminate: true`, and its default color is the theme (null). Metro picks this on an
4
+ // Android host; no Platform.OS read. Mirrors the React adapter's Android binding. See ADR 0020.
5
+ import { createActivityIndicator } from './shared';
6
+ export const ActivityIndicator = createActivityIndicator({
7
+ // RN: `color = Platform.OS === 'ios' ? GRAY : null`. Android lets the theme color it.
8
+ defaultColor: null,
9
+ nativeExtras: { styleAttr: 'Normal', indeterminate: true },
10
+ });
@@ -0,0 +1 @@
1
+ export * from './index.ios';
@@ -0,0 +1,4 @@
1
+ export type { IActivityIndicatorProps } from './shared';
2
+ export declare const ActivityIndicator: import("vue").DefineComponent<{}, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
3
+ [key: string]: any;
4
+ }>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
@@ -0,0 +1,10 @@
1
+ // ActivityIndicator on iOS: RCTActivityIndicatorView takes the size enum + a GRAY default
2
+ // color and no extra native props. Also the base (activity-indicator.ts re-exports it) for
3
+ // headless / web. Mirrors the React adapter's iOS binding. See ADR 0020.
4
+ import { createActivityIndicator } from './shared';
5
+ // RN's iOS default spinner color (Libraries/.../ActivityIndicator.js GRAY).
6
+ const IOS_DEFAULT_COLOR = '#999999';
7
+ export const ActivityIndicator = createActivityIndicator({
8
+ defaultColor: IOS_DEFAULT_COLOR,
9
+ nativeExtras: {},
10
+ });
@@ -0,0 +1,4 @@
1
+ // Base / default ActivityIndicator. Re-exports the iOS build. Metro overrides this with
2
+ // activity-indicator.ios.ts / .android.ts on a real host; under tsx / tsc / web the host
3
+ // resolves here. Filename is the selector, no Platform.OS read. See ADR 0020.
4
+ export * from './index.ios';
@@ -0,0 +1,8 @@
1
+ import type { IActivityIndicatorPlatform, IActivityIndicatorProps as IActivityIndicatorBaseProps } from '@symbiote-native/components';
2
+ import type { IClassNameValue } from '@symbiote-native/engine';
3
+ export type IActivityIndicatorProps = IActivityIndicatorBaseProps & {
4
+ class?: IClassNameValue;
5
+ };
6
+ export declare function createActivityIndicator(platform: IActivityIndicatorPlatform): import("vue").DefineComponent<{}, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
7
+ [key: string]: any;
8
+ }>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
@@ -0,0 +1,48 @@
1
+ // ActivityIndicator, the Vue lifecycle half. The render (size translation, wrapper +
2
+ // spinner, color omission) lives framework-agnostic in @symbiote-native/components and is shared
3
+ // verbatim with the React adapter; here Vue only normalizes inputs, calls the render fn,
4
+ // and bridges the Descriptor to a vnode. No reactivity of its own; this component is
5
+ // render-only (the spinner animates natively).
6
+ //
7
+ // Inputs arrive as attrs, not a typed props block, the same passthrough shape as
8
+ // components.ts. RN's per-platform bits (iOS GRAY default; Android theme/null + styleAttr +
9
+ // indeterminate) are supplied by the .ios/.android files via `IActivityIndicatorPlatform`.
10
+ import { defineComponent } from '@vue/runtime-core';
11
+ import { renderActivityIndicator } from '@symbiote-native/components';
12
+ import { descriptorToVue } from '../../descriptor-to-vue';
13
+ import { normalizeVueAttrs } from '../../utils/normalize-attrs';
14
+ function normalizeSize(size) {
15
+ if (size === 'large')
16
+ return 'large';
17
+ if (typeof size === 'number')
18
+ return size;
19
+ return 'small';
20
+ }
21
+ // Narrow attrs.style to a plain ViewStyle object (assignable to the render fn's StyleProp
22
+ // param) without a cast. Array/registered styles degrade to undefined; the engine's own
23
+ // flatten handles those when an app passes them; the wrapper only needs the object form.
24
+ function isViewStyleObject(value) {
25
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
26
+ }
27
+ export function createActivityIndicator(platform) {
28
+ return defineComponent({
29
+ name: 'ActivityIndicator',
30
+ inheritAttrs: false,
31
+ setup(_props, { attrs: rawAttrs }) {
32
+ return () => {
33
+ // Defaults match RN (animating / hidesWhenStopped true, size 'small'); the rest of
34
+ // attrs (testID, accessibility, onLayout) forwards onto the wrapper via passthrough.
35
+ const attrs = normalizeVueAttrs(rawAttrs);
36
+ const { animating, color, hidesWhenStopped, size, style, ...passthrough } = attrs;
37
+ return descriptorToVue(renderActivityIndicator({
38
+ animating: animating !== false,
39
+ hidesWhenStopped: hidesWhenStopped !== false,
40
+ size: normalizeSize(size),
41
+ color: typeof color === 'string' ? color : undefined,
42
+ style: isViewStyleObject(style) ? style : undefined,
43
+ passthrough,
44
+ }, platform));
45
+ };
46
+ },
47
+ });
48
+ }
@@ -0,0 +1,14 @@
1
+ import { type IButtonProps as ICoreButtonProps } from '@symbiote-native/components';
2
+ import type { IClassNameValue, ISymbioteEvent } from '@symbiote-native/engine';
3
+ export type IButtonProps = Omit<ICoreButtonProps, 'onPress'> & {
4
+ class?: IClassNameValue;
5
+ };
6
+ type IButtonEmits = {
7
+ press: (event: ISymbioteEvent) => boolean;
8
+ };
9
+ export declare const Button: import("vue").DefineSetupFnComponent<IButtonProps, IButtonEmits, {}, Omit<ICoreButtonProps, "onPress"> & {
10
+ class?: IClassNameValue;
11
+ } & {
12
+ onPress?: ((event: ISymbioteEvent) => any) | undefined;
13
+ }, import("vue").PublicProps>;
14
+ export {};
@@ -0,0 +1,59 @@
1
+ // Button for Vue. The minimal cross-platform button in RN's iOS shape (Button.js): a
2
+ // TouchableOpacity wrapping a Text. The base text style, the role constant, and the color fold are
3
+ // shared in @symbiote-native/components/view; here Vue only composes its TouchableOpacity + Text and
4
+ // forwards the native-only props. The Vue twin of the React adapter's Button.
5
+ import { defineComponent, h } from '@vue/runtime-core';
6
+ import { BUTTON_ACCESSIBILITY_ROLE, resolveButtonTextStyle, } from '@symbiote-native/components';
7
+ import { Text } from '../components';
8
+ import { TouchableOpacity } from './touchable';
9
+ import { normalizeVueAttrs } from '../utils/normalize-attrs';
10
+ // The props Button consumes / re-maps itself; everything else (accessibility, aria, TV-focus,
11
+ // testID, nativeID) forwards to the TouchableOpacity. `title` becomes the Text child; onPress and
12
+ // disabled re-map; touchSoundDisabled maps to the pressable's android_disableSound.
13
+ const HANDLED = [
14
+ 'title',
15
+ 'color',
16
+ 'touchSoundDisabled',
17
+ 'accessibilityRole',
18
+ 'accessible',
19
+ 'accessibilityState',
20
+ ];
21
+ function forwardAttrs(attrs) {
22
+ const result = {};
23
+ for (const key of Object.keys(attrs)) {
24
+ if (!HANDLED.includes(key))
25
+ result[key] = attrs[key];
26
+ }
27
+ return result;
28
+ }
29
+ export const Button = defineComponent((_props, { attrs: rawAttrs, emit }) => {
30
+ return () => {
31
+ const attrs = normalizeVueAttrs(rawAttrs);
32
+ const title = typeof attrs.title === 'string' ? attrs.title : '';
33
+ const color = typeof attrs.color === 'string' ? attrs.color : undefined;
34
+ const disabled = attrs.disabled === true ? true : undefined;
35
+ const touchSoundDisabled = attrs.touchSoundDisabled === true ? true : undefined;
36
+ const textStyle = resolveButtonTextStyle(color, disabled);
37
+ // RN's Button sets role=button, is accessible, and propagates the disabled accessibility
38
+ // state. The caller's props pass through, but Button's fixed role / accessible / disabled-
39
+ // state win, applied after the forwarded spread. touchSoundDisabled → android_disableSound.
40
+ const touchableProps = {
41
+ ...forwardAttrs(attrs),
42
+ disabled,
43
+ android_disableSound: touchSoundDisabled,
44
+ accessibilityRole: BUTTON_ACCESSIBILITY_ROLE,
45
+ accessible: true,
46
+ accessibilityState: { disabled },
47
+ onPress: (event) => emit('press', event),
48
+ };
49
+ return h(TouchableOpacity, touchableProps, {
50
+ default: () => [h(Text, { style: textStyle }, () => title)],
51
+ });
52
+ };
53
+ }, {
54
+ name: 'Button',
55
+ inheritAttrs: false,
56
+ emits: {
57
+ press: (_event) => true,
58
+ },
59
+ });
@@ -0,0 +1,8 @@
1
+ import type { IDrawerLayoutAndroidEmits, IDrawerLayoutAndroidProps } from './shared';
2
+ export type { IDrawerPosition, IDrawerLockMode, IKeyboardDismissMode, IDrawerState, IDrawerSlideEvent, IDrawerLayoutAndroidEmits, IDrawerLayoutAndroidProps, IDrawerLayoutAndroidHandle, } from './shared';
3
+ export declare const DrawerLayoutAndroid: import("vue").DefineSetupFnComponent<IDrawerLayoutAndroidProps, IDrawerLayoutAndroidEmits, {}, IDrawerLayoutAndroidProps & {
4
+ onDrawerOpen?: (() => any) | undefined;
5
+ onDrawerClose?: (() => any) | undefined;
6
+ onDrawerSlide?: ((event: import("@symbiote/components").IDrawerSlideEvent) => any) | undefined;
7
+ onDrawerStateChanged?: ((state: import("@symbiote/components").IDrawerState) => any) | undefined;
8
+ }, import("vue").PublicProps>;
@@ -0,0 +1,144 @@
1
+ // DrawerLayoutAndroid on Android: the real build, the Vue twin of the React adapter's
2
+ // index.android.ts. AndroidDrawerLayout is an ordinary Fabric host node committing through the same
3
+ // childSet as the rest of the tree (no per-library glue; shared derives the view's events from its
4
+ // ViewConfig, so we render the RAW Fabric name 'AndroidDrawerLayout'). The platform-invariant math —
5
+ // the host prop bag + the content/navigation wrapper styles, the slide/state event normalization,
6
+ // the imperative open/close handle, and the view/command NAMES — lives in @symbiote/components,
7
+ // shared verbatim with React; here Vue supplies only the lifecycle: a shallowRef holds the host node
8
+ // (so the engine's mirror resolves it for the imperative commands), a `drawerOpened` ref gates the
9
+ // navigation wrapper's pointerEvents, and expose() wires the imperative handle.
10
+ //
11
+ // Child order matches RN exactly: content FIRST, navigation SECOND ({childrenWrapper}{drawerViewWrapper}).
12
+ // Content is the DEFAULT slot; the drawer is the `navigationView` slot (the Vue twin of React's
13
+ // children / renderNavigationView). Inputs arrive as attrs (untyped), each narrowed with a runtime
14
+ // guard rather than a cast. The onDrawer* handlers are wrapped (drawerOpened + normalization), so they
15
+ // are stripped from the forwarded attrs; everything else (testID / accessibility / aria-*) folds via
16
+ // resolveAccessibilityProps onto the host. Metro picks this file on an Android host; no Platform.OS read.
17
+ // device-verify-pending: the AndroidDrawerLayout name + openDrawer/closeDrawer commands are
18
+ // RN-source-confirmed, not yet exercised on a real Android host.
19
+ import { defineComponent, h, ref, shallowRef } from '@vue/runtime-core';
20
+ import { buildDrawerHandle, offsetFromSlide, resolveAccessibilityProps, resolveDrawerLayout, stateFromChange, } from '@symbiote/components';
21
+ import { dlog, isSymbioteNode, } from '@symbiote/engine';
22
+ import { View } from '../../components';
23
+ import { normalizeVueAttrs } from '../../utils/normalize-attrs';
24
+ function asNumber(value) {
25
+ return typeof value === 'number' ? value : undefined;
26
+ }
27
+ function asString(value) {
28
+ return typeof value === 'string' ? value : undefined;
29
+ }
30
+ function asDrawerPosition(value) {
31
+ return value === 'left' || value === 'right' ? value : undefined;
32
+ }
33
+ function asDrawerLockMode(value) {
34
+ return value === 'unlocked' || value === 'locked-closed' || value === 'locked-open'
35
+ ? value
36
+ : undefined;
37
+ }
38
+ function asKeyboardDismissMode(value) {
39
+ return value === 'none' || value === 'on-drag' ? value : undefined;
40
+ }
41
+ function isStyleProp(value) {
42
+ return typeof value === 'object' && value !== null;
43
+ }
44
+ // The typed fields the lifecycle consumes/re-emits (resolveDrawerLayout folds the drawer* props onto
45
+ // the host) and the onDrawer* handlers (wrapped here, never forwarded raw); everything else (testID,
46
+ // accessibility, aria-*) forwards onto the host via passthrough.
47
+ const HANDLED_ATTRS = [
48
+ 'drawerWidth',
49
+ 'drawerPosition',
50
+ 'drawerLockMode',
51
+ 'keyboardDismissMode',
52
+ 'drawerBackgroundColor',
53
+ 'statusBarBackgroundColor',
54
+ 'style',
55
+ 'onDrawerOpen',
56
+ 'onDrawerClose',
57
+ 'onDrawerSlide',
58
+ 'onDrawerStateChanged',
59
+ ];
60
+ function forwardAttrs(attrs) {
61
+ const result = {};
62
+ for (const key of Object.keys(attrs)) {
63
+ if (!HANDLED_ATTRS.includes(key))
64
+ result[key] = attrs[key];
65
+ }
66
+ return result;
67
+ }
68
+ export const DrawerLayoutAndroid = defineComponent((_props, { slots, attrs: rawAttrs, expose, emit }) => {
69
+ // shallowRef, NOT ref: the engine node must be held by IDENTITY. A plain ref() runs the node
70
+ // through Vue's toReactive(), handing back a reactive Proxy the engine's mirror (a WeakMap keyed
71
+ // on the raw node) can't resolve, so openDrawer/closeDrawer would silently no-op. Same rule as
72
+ // the Switch / ScrollView host node (see .claude/skills/vue-adapter-reactivity).
73
+ const nodeRef = shallowRef(null);
74
+ const setNodeRef = (el) => {
75
+ nodeRef.value = isSymbioteNode(el) ? el : null;
76
+ };
77
+ // drawerOpened gates the navigation wrapper's pointerEvents (RN _onDrawerOpen/_onDrawerClose
78
+ // setState): closed -> 'none' so the off-screen drawer never intercepts touches. A plain ref is
79
+ // fine here — it holds a boolean, not an engine node; the shallowRef rule is only for the node.
80
+ const drawerOpened = ref(false);
81
+ // The imperative handle reads the node through a LAZY getter (() => nodeRef.value), not the node
82
+ // captured once: it is null until the element commits. expose() makes it the value a parent ref
83
+ // sees — the Vue twin of React's useImperativeHandle(ref, buildDrawerHandle(…)).
84
+ expose(buildDrawerHandle(() => nodeRef.value));
85
+ return () => {
86
+ const attrs = normalizeVueAttrs(rawAttrs);
87
+ const handleDrawerOpen = () => {
88
+ dlog('Vue DrawerLayoutAndroid onDrawerOpen');
89
+ drawerOpened.value = true;
90
+ emit('drawerOpen');
91
+ };
92
+ const handleDrawerClose = () => {
93
+ dlog('Vue DrawerLayoutAndroid onDrawerClose');
94
+ drawerOpened.value = false;
95
+ emit('drawerClose');
96
+ };
97
+ const handleDrawerSlide = (event) => {
98
+ dlog('Vue DrawerLayoutAndroid onDrawerSlide');
99
+ emit('drawerSlide', { offset: offsetFromSlide(event) });
100
+ };
101
+ const handleDrawerStateChanged = (event) => {
102
+ emit('drawerStateChanged', stateFromChange(event));
103
+ };
104
+ const resolved = resolveDrawerLayout({
105
+ drawerWidth: asNumber(attrs.drawerWidth),
106
+ drawerPosition: asDrawerPosition(attrs.drawerPosition),
107
+ drawerLockMode: asDrawerLockMode(attrs.drawerLockMode),
108
+ keyboardDismissMode: asKeyboardDismissMode(attrs.keyboardDismissMode),
109
+ drawerBackgroundColor: asString(attrs.drawerBackgroundColor),
110
+ statusBarBackgroundColor: asString(attrs.statusBarBackgroundColor),
111
+ drawerOpened: drawerOpened.value,
112
+ style: isStyleProp(attrs.style) ? attrs.style : undefined,
113
+ passthrough: resolveAccessibilityProps(forwardAttrs(attrs)),
114
+ });
115
+ // RN's mainSubview: content wrapped in an absolute full-screen box (the default slot).
116
+ const content = h(View, { style: resolved.contentWrapperStyle }, () => slots.default !== undefined ? slots.default() : []);
117
+ // RN's drawerSubview: the navigation view (the `navigationView` slot) wrapped, drawerWidth-wide,
118
+ // painted with drawerBackgroundColor, untouchable until opened.
119
+ const navigation = h(View, {
120
+ style: resolved.navigationWrapperStyle,
121
+ pointerEvents: resolved.navigationPointerEvents,
122
+ }, () => (slots.navigationView !== undefined ? slots.navigationView() : []));
123
+ dlog(`Vue DrawerLayoutAndroid render width=${String(asNumber(attrs.drawerWidth))} ` +
124
+ `opened=${drawerOpened.value}`);
125
+ // Child order matches RN exactly: content FIRST, navigation SECOND.
126
+ return h(resolved.viewName, {
127
+ ...resolved.hostProps,
128
+ ref: setNodeRef,
129
+ onDrawerOpen: handleDrawerOpen,
130
+ onDrawerClose: handleDrawerClose,
131
+ onDrawerSlide: handleDrawerSlide,
132
+ onDrawerStateChanged: handleDrawerStateChanged,
133
+ }, [content, navigation]);
134
+ };
135
+ }, {
136
+ name: 'DrawerLayoutAndroid',
137
+ inheritAttrs: false,
138
+ emits: {
139
+ drawerOpen: () => true,
140
+ drawerClose: () => true,
141
+ drawerSlide: (_event) => true,
142
+ drawerStateChanged: (_state) => true,
143
+ },
144
+ });
@@ -0,0 +1,8 @@
1
+ import type { IDrawerLayoutAndroidEmits, IDrawerLayoutAndroidProps } from './shared';
2
+ export type { IDrawerPosition, IDrawerLockMode, IKeyboardDismissMode, IDrawerState, IDrawerSlideEvent, IDrawerLayoutAndroidEmits, IDrawerLayoutAndroidProps, IDrawerLayoutAndroidHandle, } from './shared';
3
+ export declare const DrawerLayoutAndroid: import("vue").DefineSetupFnComponent<IDrawerLayoutAndroidProps, IDrawerLayoutAndroidEmits, {}, IDrawerLayoutAndroidProps & {
4
+ onDrawerOpen?: (() => any) | undefined;
5
+ onDrawerClose?: (() => any) | undefined;
6
+ onDrawerSlide?: ((event: import("@symbiote/components").IDrawerSlideEvent) => any) | undefined;
7
+ onDrawerStateChanged?: ((state: import("@symbiote/components").IDrawerState) => any) | undefined;
8
+ }, import("vue").PublicProps>;
@@ -0,0 +1,36 @@
1
+ // DrawerLayoutAndroid: base / off-Android fallback (the Vue twin of the React adapter's index.ts).
2
+ // AndroidDrawerLayout is Android-only, so everywhere except an Android host — where Metro picks
3
+ // index.android.ts — we render the content (the default slot) in a plain container and drop the
4
+ // navigation slot (RN's DrawerLayoutAndroidFallback shape). The imperative open/close are silent
5
+ // no-ops; there is no drawer to drive. The filename is the selector; no Platform.OS read. The Vue
6
+ // barrel imports './drawer-layout-android', which resolves here under tsx/tsc and to the .android
7
+ // file under Metro. See ADR 0019.
8
+ import { defineComponent, h } from '@vue/runtime-core';
9
+ import { dlog } from '@symbiote/engine';
10
+ import { View } from '../../components';
11
+ import { normalizeVueAttrs } from '../../utils/normalize-attrs';
12
+ function isStyleProp(value) {
13
+ return typeof value === 'object' && value !== null;
14
+ }
15
+ export const DrawerLayoutAndroid = defineComponent((_props, { slots, attrs, expose }) => {
16
+ const handle = {
17
+ openDrawer: () => dlog('Vue DrawerLayoutAndroid.openDrawer no-op: off Android'),
18
+ closeDrawer: () => dlog('Vue DrawerLayoutAndroid.closeDrawer no-op: off Android'),
19
+ };
20
+ expose(handle);
21
+ return () => {
22
+ dlog('Vue DrawerLayoutAndroid fallback: off-Android host, rendering content only');
23
+ const normalized = normalizeVueAttrs(attrs);
24
+ const style = isStyleProp(normalized.style) ? normalized.style : undefined;
25
+ return h(View, { style }, () => (slots.default !== undefined ? slots.default() : []));
26
+ };
27
+ }, {
28
+ name: 'DrawerLayoutAndroid',
29
+ inheritAttrs: false,
30
+ emits: {
31
+ drawerOpen: () => true,
32
+ drawerClose: () => true,
33
+ drawerSlide: (_event) => true,
34
+ drawerStateChanged: (_state) => true,
35
+ },
36
+ });
@@ -0,0 +1,18 @@
1
+ import type { IStyleProp, IViewStyle } from '@symbiote/engine';
2
+ import type { IAccessibilityProps, IAriaProps, IDrawerLockMode, IDrawerPosition, IDrawerSlideEvent, IDrawerState, IKeyboardDismissMode } from '@symbiote/components';
3
+ export type { IDrawerPosition, IDrawerLockMode, IKeyboardDismissMode, IDrawerState, IDrawerSlideEvent, IDrawerLayoutAndroidHandle, } from '@symbiote/components';
4
+ export interface IDrawerLayoutAndroidProps extends IAccessibilityProps, IAriaProps {
5
+ drawerWidth?: number;
6
+ drawerPosition?: IDrawerPosition;
7
+ drawerLockMode?: IDrawerLockMode;
8
+ keyboardDismissMode?: IKeyboardDismissMode;
9
+ drawerBackgroundColor?: string;
10
+ statusBarBackgroundColor?: string;
11
+ style?: IStyleProp<IViewStyle>;
12
+ }
13
+ export type IDrawerLayoutAndroidEmits = {
14
+ drawerOpen: () => boolean;
15
+ drawerClose: () => boolean;
16
+ drawerSlide: (event: IDrawerSlideEvent) => boolean;
17
+ drawerStateChanged: (state: IDrawerState) => boolean;
18
+ };
@@ -0,0 +1,6 @@
1
+ // Shared types for the Vue DrawerLayoutAndroid: the same Props/Handle contract the real Android
2
+ // build (index.android.ts) and the off-Android fallback (index.ts) both implement. The
3
+ // platform-agnostic enums + the imperative handle live in @symbiote/components (shared verbatim with
4
+ // the React adapter, the single source of truth); this file adds only the Vue-facing prop surface.
5
+ // Per ADR 0019 the filename selects the build; no Platform.OS read anywhere.
6
+ export {};