@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.
- package/LICENSE +21 -0
- package/README.md +142 -0
- package/build/components/activity-indicator/index.android.d.ts +4 -0
- package/build/components/activity-indicator/index.android.js +10 -0
- package/build/components/activity-indicator/index.d.ts +1 -0
- package/build/components/activity-indicator/index.ios.d.ts +4 -0
- package/build/components/activity-indicator/index.ios.js +10 -0
- package/build/components/activity-indicator/index.js +4 -0
- package/build/components/activity-indicator/shared.d.ts +8 -0
- package/build/components/activity-indicator/shared.js +48 -0
- package/build/components/button.d.ts +14 -0
- package/build/components/button.js +59 -0
- package/build/components/drawer-layout-android/index.android.d.ts +8 -0
- package/build/components/drawer-layout-android/index.android.js +144 -0
- package/build/components/drawer-layout-android/index.d.ts +8 -0
- package/build/components/drawer-layout-android/index.js +36 -0
- package/build/components/drawer-layout-android/shared.d.ts +18 -0
- package/build/components/drawer-layout-android/shared.js +6 -0
- package/build/components/flat-list/index.d.ts +88 -0
- package/build/components/flat-list/index.js +189 -0
- package/build/components/image-background.d.ts +9 -0
- package/build/components/image-background.js +114 -0
- package/build/components/image.d.ts +9 -0
- package/build/components/image.js +99 -0
- package/build/components/input-accessory-view.d.ts +10 -0
- package/build/components/input-accessory-view.js +41 -0
- package/build/components/keyboard-avoiding-view.d.ts +18 -0
- package/build/components/keyboard-avoiding-view.js +121 -0
- package/build/components/modal.d.ts +29 -0
- package/build/components/modal.js +134 -0
- package/build/components/pressable.d.ts +58 -0
- package/build/components/pressable.js +221 -0
- package/build/components/refresh-control.d.ts +20 -0
- package/build/components/refresh-control.js +53 -0
- package/build/components/safe-area-view.d.ts +8 -0
- package/build/components/safe-area-view.js +33 -0
- package/build/components/scroll-view/index.android.d.ts +4 -0
- package/build/components/scroll-view/index.android.js +74 -0
- package/build/components/scroll-view/index.d.ts +1 -0
- package/build/components/scroll-view/index.ios.d.ts +4 -0
- package/build/components/scroll-view/index.ios.js +15 -0
- package/build/components/scroll-view/index.js +4 -0
- package/build/components/scroll-view/shared.d.ts +96 -0
- package/build/components/scroll-view/shared.js +297 -0
- package/build/components/scroll-view/sticky-header.d.ts +7 -0
- package/build/components/scroll-view/sticky-header.js +195 -0
- package/build/components/section-list/index.d.ts +39 -0
- package/build/components/section-list/index.js +77 -0
- package/build/components/switch/index.android.d.ts +12 -0
- package/build/components/switch/index.android.js +13 -0
- package/build/components/switch/index.d.ts +2 -0
- package/build/components/switch/index.ios.d.ts +12 -0
- package/build/components/switch/index.ios.js +11 -0
- package/build/components/switch/index.js +5 -0
- package/build/components/switch/shared.d.ts +23 -0
- package/build/components/switch/shared.js +129 -0
- package/build/components/text-input/index.d.ts +24 -0
- package/build/components/text-input/index.js +257 -0
- package/build/components/touchable-native-feedback.d.ts +24 -0
- package/build/components/touchable-native-feedback.js +75 -0
- package/build/components/touchable.d.ts +44 -0
- package/build/components/touchable.js +150 -0
- package/build/components/virtualized-list/index.d.ts +121 -0
- package/build/components/virtualized-list/index.js +687 -0
- package/build/components/virtualized-section-list/index.d.ts +96 -0
- package/build/components/virtualized-section-list/index.js +160 -0
- package/build/components.d.ts +51 -0
- package/build/components.js +26 -0
- package/build/composables/use-color-scheme.d.ts +3 -0
- package/build/composables/use-color-scheme.js +24 -0
- package/build/composables/use-window-dimensions.d.ts +3 -0
- package/build/composables/use-window-dimensions.js +34 -0
- package/build/create-tunnel.d.ts +9 -0
- package/build/create-tunnel.js +37 -0
- package/build/descriptor-to-vue.d.ts +3 -0
- package/build/descriptor-to-vue.js +14 -0
- package/build/host-instance/index.d.ts +2 -0
- package/build/host-instance/index.js +34 -0
- package/build/index.d.ts +67 -0
- package/build/index.js +90 -0
- package/build/modules/animated/create-animated-component.d.ts +4 -0
- package/build/modules/animated/create-animated-component.js +169 -0
- package/build/modules/animated/index.d.ts +55 -0
- package/build/modules/animated/index.js +66 -0
- package/build/modules/app-registry/index.d.ts +7 -0
- package/build/modules/app-registry/index.js +32 -0
- package/build/modules/status-bar.d.ts +17 -0
- package/build/modules/status-bar.js +65 -0
- package/build/render.d.ts +7 -0
- package/build/render.js +43 -0
- package/build/renderer.d.ts +4 -0
- package/build/renderer.js +129 -0
- package/build/runtime-helpers.d.ts +28 -0
- package/build/runtime-helpers.js +68 -0
- package/build/utils/component-helpers.d.ts +2 -0
- package/build/utils/component-helpers.js +13 -0
- package/build/utils/model-binding.d.ts +4 -0
- package/build/utils/model-binding.js +17 -0
- package/build/utils/normalize-attrs.d.ts +1 -0
- package/build/utils/normalize-attrs.js +30 -0
- package/build/utils/slots-to-render-props.d.ts +4 -0
- package/build/utils/slots-to-render-props.js +18 -0
- package/metro-css-parser.cjs +12 -0
- 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
|
+

|
|
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 {};
|