@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
@@ -0,0 +1,121 @@
1
+ // KeyboardAvoidingView: the Vue lifecycle half. The inset math + the behavior → style/structure
2
+ // decision live in @symbiote-native/components (render-keyboard-avoiding-view), shared verbatim with the
3
+ // React adapter; Vue supplies only the reactivity: a ref holds the inset, onMounted subscribes to
4
+ // the now-core Keyboard module (show / changeFrame / hide), onUnmounted tears the subscriptions
5
+ // down, and onLayout measures the wrapper frame that feeds the next event's inset. This is the Vue
6
+ // twin of React's useState + useEffect + onLayout. Full parity: behavior 'height'|'position'|
7
+ // 'padding', enabled, keyboardVerticalOffset, contentContainerStyle, onLayout.
8
+ //
9
+ // Inputs arrive as attrs (untyped), so each is narrowed with a runtime guard rather than a cast.
10
+ // rawAttrs runs through normalizeVueAttrs (kebab→camel) so a template `:keyboard-vertical-offset`
11
+ // and `:content-container-style` resolve; aria-*/data-* are preserved and forwarded to the host.
12
+ import { defineComponent, h, ref, onMounted, onUnmounted } from '@vue/runtime-core';
13
+ import { Keyboard, KEYBOARD_EVENT, dlog, } from '@symbiote-native/engine';
14
+ import { computeInset, readKeyboardFrame, readLayoutFrame, resolveKeyboardAvoidingLayout, resolveAccessibilityProps, DEFAULT_VERTICAL_OFFSET, } from '@symbiote-native/components';
15
+ import { normalizeVueAttrs } from '../utils/normalize-attrs';
16
+ // A style prop is an object (a style record) or an array of them; numbers/strings/null degrade
17
+ // to undefined (the engine flattens what it gets). A runtime guard, not a cast.
18
+ function isStyleProp(value) {
19
+ return typeof value === 'object' && value !== null;
20
+ }
21
+ function asBehavior(value) {
22
+ return value === 'height' || value === 'position' || value === 'padding' ? value : undefined;
23
+ }
24
+ // The prop/handler keys the lifecycle consumes itself; everything else (accessibility, testID,
25
+ // aria-*/data-*) forwards onto the wrapper host node.
26
+ const HANDLED_ATTRS = [
27
+ 'behavior',
28
+ 'enabled',
29
+ 'keyboardVerticalOffset',
30
+ 'contentContainerStyle',
31
+ 'style',
32
+ 'onLayout',
33
+ ];
34
+ function forwardAttrs(attrs) {
35
+ const result = {};
36
+ for (const key of Object.keys(attrs)) {
37
+ if (!HANDLED_ATTRS.includes(key))
38
+ result[key] = attrs[key];
39
+ }
40
+ return result;
41
+ }
42
+ export const KeyboardAvoidingView = defineComponent((_props, { attrs: rawAttrs, slots, emit }) => {
43
+ // The inset is a number (plain data), so a plain ref is correct; no engine node is held here
44
+ // (onLayout delivers the frame, so no imperative measure / host-node capture is needed).
45
+ const inset = ref(0);
46
+ // Mutable, not reactive: changing the measured frame alone shouldn't re-render; it feeds the
47
+ // next keyboard event's inset math (React's frameRef / initialHeightRef).
48
+ let frame;
49
+ let initialHeight;
50
+ const verticalOffset = () => typeof rawAttrs.keyboardVerticalOffset === 'number'
51
+ ? rawAttrs.keyboardVerticalOffset
52
+ : DEFAULT_VERTICAL_OFFSET;
53
+ const onShow = (payload) => {
54
+ const keyboard = readKeyboardFrame(payload);
55
+ const next = computeInset(frame, keyboard, verticalOffset());
56
+ dlog(`KeyboardAvoidingView show -> inset ${next}`);
57
+ inset.value = next;
58
+ };
59
+ const onHide = () => {
60
+ dlog('KeyboardAvoidingView hide -> inset 0');
61
+ inset.value = 0;
62
+ };
63
+ let subscriptions = [];
64
+ onMounted(() => {
65
+ subscriptions = [
66
+ Keyboard.addListener(KEYBOARD_EVENT.didShow, onShow),
67
+ Keyboard.addListener(KEYBOARD_EVENT.didChangeFrame, onShow),
68
+ Keyboard.addListener(KEYBOARD_EVENT.didHide, onHide),
69
+ ];
70
+ });
71
+ onUnmounted(() => {
72
+ for (const subscription of subscriptions)
73
+ subscription.remove();
74
+ subscriptions = [];
75
+ });
76
+ const handleLayout = (event) => {
77
+ const measured = readLayoutFrame(event.nativeEvent.layout);
78
+ if (measured !== undefined) {
79
+ frame = measured;
80
+ if (initialHeight === undefined)
81
+ initialHeight = measured.height;
82
+ }
83
+ emit('layout', event);
84
+ };
85
+ return () => {
86
+ const attrs = normalizeVueAttrs(rawAttrs);
87
+ const behavior = asBehavior(attrs.behavior);
88
+ // RN gates every inset on `enabled ?? true`; only an explicit `false` disables.
89
+ const isEnabled = attrs.enabled !== false;
90
+ const effectiveInset = isEnabled ? inset.value : 0;
91
+ const layout = resolveKeyboardAvoidingLayout({
92
+ behavior,
93
+ effectiveInset,
94
+ initialHeight,
95
+ style: isStyleProp(attrs.style) ? attrs.style : undefined,
96
+ contentContainerStyle: isStyleProp(attrs.contentContainerStyle)
97
+ ? attrs.contentContainerStyle
98
+ : undefined,
99
+ });
100
+ const childNodes = slots.default !== undefined ? slots.default() : undefined;
101
+ const wrapperProps = {
102
+ ...resolveAccessibilityProps(forwardAttrs(attrs)),
103
+ style: layout.wrapperStyle,
104
+ onLayout: handleLayout,
105
+ };
106
+ // 'nested' ('position') pushes the children in an inner view by `bottom: inset`; the wrapper
107
+ // modes adjust the single wrapper directly.
108
+ if (layout.kind === 'nested') {
109
+ return h('symbiote-view', wrapperProps, [
110
+ h('symbiote-view', { style: layout.innerStyle }, childNodes),
111
+ ]);
112
+ }
113
+ return h('symbiote-view', wrapperProps, childNodes);
114
+ };
115
+ }, {
116
+ name: 'KeyboardAvoidingView',
117
+ inheritAttrs: false,
118
+ emits: {
119
+ layout: (_event) => true,
120
+ },
121
+ });
@@ -0,0 +1,29 @@
1
+ import { type IAccessibilityProps, type IAriaProps, type IModalAnimationType, type IModalOrientation, type IModalOrientationChangeEvent, type IModalPresentationStyle } from '@symbiote-native/components';
2
+ import { type IClassNameValue, type IStyleProp, type IViewStyle } from '@symbiote-native/engine';
3
+ export type { IModalAnimationType, IModalPresentationStyle, IModalOrientation, IModalOrientationChangeEvent, } from '@symbiote-native/components';
4
+ export interface IModalProps extends IAccessibilityProps, IAriaProps {
5
+ visible?: boolean;
6
+ transparent?: boolean;
7
+ backdropColor?: string;
8
+ animationType?: IModalAnimationType;
9
+ presentationStyle?: IModalPresentationStyle;
10
+ supportedOrientations?: ReadonlyArray<IModalOrientation>;
11
+ hardwareAccelerated?: boolean;
12
+ statusBarTranslucent?: boolean;
13
+ navigationBarTranslucent?: boolean;
14
+ allowSwipeDismissal?: boolean;
15
+ style?: IStyleProp<IViewStyle>;
16
+ class?: IClassNameValue;
17
+ }
18
+ export type IModalEmits = {
19
+ show: () => boolean;
20
+ dismiss: () => boolean;
21
+ requestClose: () => boolean;
22
+ orientationChange: (event: IModalOrientationChangeEvent) => boolean;
23
+ };
24
+ export declare const Modal: import("vue").DefineSetupFnComponent<IModalProps, IModalEmits, {}, IModalProps & {
25
+ onShow?: (() => any) | undefined;
26
+ onDismiss?: (() => any) | undefined;
27
+ onRequestClose?: (() => any) | undefined;
28
+ onOrientationChange?: ((event: IModalOrientationChangeEvent) => any) | undefined;
29
+ }, import("vue").PublicProps>;
@@ -0,0 +1,134 @@
1
+ // Modal: the Vue lifecycle half. RCTModalHostView is an ordinary Fabric host node committing
2
+ // through the same childSet as the rest of the tree (no second JS surface). The style math (the
3
+ // backdrop override, the container/host styles, the presentationStyle default), the visible gate,
4
+ // and the iOS keep-alive reducer all live framework-agnostic in @symbiote-native/components and are shared
5
+ // verbatim with React; here Vue supplies only the lifecycle: a ref over the keep-alive state +
6
+ // a POST-flush watch that drives the visible→hidden transition AFTER render (so one keep-alive
7
+ // frame survives, the Vue twin of React's useEffect-after-render), and the Descriptor bridge,
8
+ // nesting the slot children UNDER the container View.
9
+ //
10
+ // Inputs arrive as attrs (untyped), each narrowed with a runtime guard rather than a cast. The
11
+ // events (onShow/onDismiss/onRequestClose/onOrientationChange) are real ViewConfig DirectEvents,
12
+ // so, unlike Switch's pure-JS onValueChange, they forward to the host raw via passthrough.
13
+ import { defineComponent, h, ref, watch } from '@vue/runtime-core';
14
+ import { createInitialModalState, modalReducer, renderModal, resolveAccessibilityProps, shouldRenderModal, } from '@symbiote-native/components';
15
+ import { dlog, } from '@symbiote-native/engine';
16
+ import { normalizeVueAttrs } from '../utils/normalize-attrs';
17
+ function asBoolean(value) {
18
+ return typeof value === 'boolean' ? value : undefined;
19
+ }
20
+ function asString(value) {
21
+ return typeof value === 'string' ? value : undefined;
22
+ }
23
+ function asAnimationType(value) {
24
+ return value === 'none' || value === 'slide' || value === 'fade' ? value : undefined;
25
+ }
26
+ function asPresentationStyle(value) {
27
+ return value === 'fullScreen' ||
28
+ value === 'pageSheet' ||
29
+ value === 'formSheet' ||
30
+ value === 'overFullScreen'
31
+ ? value
32
+ : undefined;
33
+ }
34
+ const ORIENTATIONS = [
35
+ 'portrait',
36
+ 'portrait-upside-down',
37
+ 'landscape',
38
+ 'landscape-left',
39
+ 'landscape-right',
40
+ ];
41
+ function isOrientation(value) {
42
+ // .includes() types its arg as IModalOrientation, so a bare string won't pass; .some keeps the
43
+ // ORIENTATIONS list as the single source of truth without an `as` cast.
44
+ return typeof value === 'string' && ORIENTATIONS.some(orientation => orientation === value);
45
+ }
46
+ function asSupportedOrientations(value) {
47
+ if (!Array.isArray(value))
48
+ return undefined;
49
+ return value.every(isOrientation) ? value : undefined;
50
+ }
51
+ function isStyleProp(value) {
52
+ return typeof value === 'object' && value !== null;
53
+ }
54
+ // The typed fields the render consumes and re-emits onto the host; everything else
55
+ // (accessibility, testID, raw native passthrough) forwards via passthrough.
56
+ const HANDLED_ATTRS = [
57
+ 'visible',
58
+ 'transparent',
59
+ 'backdropColor',
60
+ 'animationType',
61
+ 'presentationStyle',
62
+ 'supportedOrientations',
63
+ 'hardwareAccelerated',
64
+ 'statusBarTranslucent',
65
+ 'navigationBarTranslucent',
66
+ 'allowSwipeDismissal',
67
+ 'style',
68
+ 'class',
69
+ ];
70
+ function forwardAttrs(attrs) {
71
+ const result = {};
72
+ for (const key of Object.keys(attrs)) {
73
+ if (!HANDLED_ATTRS.includes(key))
74
+ result[key] = attrs[key];
75
+ }
76
+ return result;
77
+ }
78
+ export const Modal = defineComponent((_props, { attrs: rawAttrs, slots, emit }) => {
79
+ const state = ref(createInitialModalState(rawAttrs.visible === true));
80
+ // POST-flush so the transition fires AFTER the render that used the OLD state: on visible→hidden
81
+ // the node renders once more (state.isRendered still true → the keep-alive frame), then this
82
+ // drops it and the next render unmounts. flush:'pre' would unmount immediately, killing the
83
+ // keep-alive. The reducer is identity-stable, so a no-op transition triggers no extra render.
84
+ watch(() => rawAttrs.visible === true, isVisible => {
85
+ state.value = modalReducer(state.value, isVisible ? { type: 'show' } : { type: 'hide' });
86
+ }, { flush: 'post' });
87
+ return () => {
88
+ const isVisible = rawAttrs.visible === true;
89
+ if (!shouldRenderModal(isVisible, state.value)) {
90
+ dlog('Modal hidden -> no node committed');
91
+ return null;
92
+ }
93
+ const attrs = normalizeVueAttrs(rawAttrs);
94
+ const root = renderModal({
95
+ visible: asBoolean(attrs.visible),
96
+ transparent: asBoolean(attrs.transparent),
97
+ backdropColor: asString(attrs.backdropColor),
98
+ animationType: asAnimationType(attrs.animationType),
99
+ presentationStyle: asPresentationStyle(attrs.presentationStyle),
100
+ supportedOrientations: asSupportedOrientations(attrs.supportedOrientations),
101
+ hardwareAccelerated: asBoolean(attrs.hardwareAccelerated),
102
+ statusBarTranslucent: asBoolean(attrs.statusBarTranslucent),
103
+ navigationBarTranslucent: asBoolean(attrs.navigationBarTranslucent),
104
+ allowSwipeDismissal: asBoolean(attrs.allowSwipeDismissal),
105
+ style: isStyleProp(attrs.style) ? attrs.style : undefined,
106
+ passthrough: resolveAccessibilityProps(forwardAttrs(attrs)),
107
+ });
108
+ // root = symbiote-modal > [container]; the slot children nest UNDER the container View, never
109
+ // as a direct sibling of the host (RN's modal content layout).
110
+ const [container] = root.children;
111
+ if (typeof container === 'string')
112
+ return null;
113
+ const slotChildren = slots.default !== undefined ? slots.default() : [];
114
+ return h(root.type, {
115
+ ...root.props,
116
+ key: root.key,
117
+ onShow: () => emit('show'),
118
+ onDismiss: () => emit('dismiss'),
119
+ onRequestClose: () => emit('requestClose'),
120
+ onOrientationChange: (event) => emit('orientationChange', event),
121
+ }, [
122
+ h(container.type, { ...container.props, key: container.key, class: attrs.class }, slotChildren),
123
+ ]);
124
+ };
125
+ }, {
126
+ name: 'Modal',
127
+ inheritAttrs: false,
128
+ emits: {
129
+ show: () => true,
130
+ dismiss: () => true,
131
+ requestClose: () => true,
132
+ orientationChange: (_event) => true,
133
+ },
134
+ });
@@ -0,0 +1,58 @@
1
+ import { type EmitFn, type VNode } from '@vue/runtime-core';
2
+ import { type IPressState, type IRectOffset, type IPressableAndroidRippleConfig, type IAccessibilityProps, type IAriaProps } from '@symbiote-native/components';
3
+ import { type IClassNameValue, type ISymbioteEvent, type IStyleProp, type IViewStyle } from '@symbiote-native/engine';
4
+ export type { IPressState, IPressableAndroidRippleConfig } from '@symbiote-native/components';
5
+ export type IPressableEmits = {
6
+ press: (event: ISymbioteEvent) => boolean;
7
+ pressIn: (event: ISymbioteEvent) => boolean;
8
+ pressOut: (event: ISymbioteEvent) => boolean;
9
+ pressMove: (event: ISymbioteEvent) => boolean;
10
+ longPress: (event: ISymbioteEvent) => boolean;
11
+ hoverIn: (event: ISymbioteEvent) => boolean;
12
+ hoverOut: (event: ISymbioteEvent) => boolean;
13
+ };
14
+ export declare const PRESSABLE_EMITS: {
15
+ press: (_event: ISymbioteEvent) => boolean;
16
+ pressIn: (_event: ISymbioteEvent) => boolean;
17
+ pressOut: (_event: ISymbioteEvent) => boolean;
18
+ pressMove: (_event: ISymbioteEvent) => boolean;
19
+ longPress: (_event: ISymbioteEvent) => boolean;
20
+ hoverIn: (_event: ISymbioteEvent) => boolean;
21
+ hoverOut: (_event: ISymbioteEvent) => boolean;
22
+ };
23
+ export declare function emitPressableEvents(emit: EmitFn<IPressableEmits>): Record<string, (event: ISymbioteEvent) => void>;
24
+ export interface IPressableProps extends IAccessibilityProps, IAriaProps {
25
+ delayLongPress?: number;
26
+ disabled?: boolean;
27
+ cancelable?: boolean;
28
+ hitSlop?: IRectOffset;
29
+ pressRetentionOffset?: IRectOffset;
30
+ unstable_pressDelay?: number;
31
+ android_ripple?: IPressableAndroidRippleConfig;
32
+ android_disableSound?: boolean;
33
+ delayHoverIn?: number;
34
+ delayHoverOut?: number;
35
+ testID?: string;
36
+ style?: IStyleProp<IViewStyle> | ((state: IPressState) => IStyleProp<IViewStyle>);
37
+ class?: IClassNameValue;
38
+ }
39
+ export type IPressableSlots = {
40
+ default?: (state: IPressState) => VNode[] | VNode;
41
+ };
42
+ export declare const Pressable: import("vue").DefineSetupFnComponent<IPressableProps, {
43
+ press: (_event: ISymbioteEvent) => boolean;
44
+ pressIn: (_event: ISymbioteEvent) => boolean;
45
+ pressOut: (_event: ISymbioteEvent) => boolean;
46
+ pressMove: (_event: ISymbioteEvent) => boolean;
47
+ longPress: (_event: ISymbioteEvent) => boolean;
48
+ hoverIn: (_event: ISymbioteEvent) => boolean;
49
+ hoverOut: (_event: ISymbioteEvent) => boolean;
50
+ }, import("vue").SlotsType<IPressableSlots>, IPressableProps & {
51
+ onPress?: ((_event: ISymbioteEvent) => any) | undefined;
52
+ onPressIn?: ((_event: ISymbioteEvent) => any) | undefined;
53
+ onPressOut?: ((_event: ISymbioteEvent) => any) | undefined;
54
+ onPressMove?: ((_event: ISymbioteEvent) => any) | undefined;
55
+ onLongPress?: ((_event: ISymbioteEvent) => any) | undefined;
56
+ onHoverIn?: ((_event: ISymbioteEvent) => any) | undefined;
57
+ onHoverOut?: ((_event: ISymbioteEvent) => any) | undefined;
58
+ }, import("vue").PublicProps>;
@@ -0,0 +1,221 @@
1
+ // Pressable, the Vue lifecycle half. The press lifecycle (the long-press timer, unstable_press-
2
+ // Delay deferral, the pressRetentionOffset drift test, the suppression flags) lives in
3
+ // @symbiote-native/components/state as a pure machine over a runtime + host; the render decisions (the
4
+ // responder listeners, the disabled→accessibilityState fold, the ripple prop) in
5
+ // @symbiote-native/components/view, both shared verbatim with the React adapter. Here Vue supplies the
6
+ // reactivity: a `ref` holds `pressed`, a setup-scope object holds the press runtime, a function
7
+ // ref grabs the responder View's host node, and the machine measures through it. This is the Vue
8
+ // twin of the React adapter's useState + useRef(runtime) + useRef(viewRef).
9
+ //
10
+ // Inputs arrive as attrs (untyped), so each is narrowed with a runtime guard rather than a cast.
11
+ // The user's onPress / onPressIn / … are consumed by the machine and MUST be stripped from the
12
+ // forwarded attrs (they are pure-JS callbacks; the machine's SYNTHESIZED handlers go on the View,
13
+ // where routeProp turns press/pressIn/pressOut + the responder events into listeners). Children
14
+ // arrive as a (scoped) default slot so `v-slot="{ pressed }"` mirrors React's children-as-function.
15
+ import { defineComponent, h, ref, shallowRef } from '@vue/runtime-core';
16
+ import { createPressHandlers, createPressRuntime, rippleProps, buildPressableListeners, resolveDisabledAccessibilityState, noteHoverNoop, resolveAccessibilityProps, DEFAULT_DELAY_LONG_PRESS_MS, } from '@symbiote-native/components';
17
+ import { measure, isSymbioteNode, } from '@symbiote-native/engine';
18
+ import { View } from '../components';
19
+ import { normalizeVueAttrs } from '../utils/normalize-attrs';
20
+ // The emits-options validator for the full press surface. Reused verbatim as the `emits` of every
21
+ // Touchable* wrapper (they expose the same events), so the runtime list lives in exactly one place.
22
+ export const PRESSABLE_EMITS = {
23
+ press: (_event) => true,
24
+ pressIn: (_event) => true,
25
+ pressOut: (_event) => true,
26
+ pressMove: (_event) => true,
27
+ longPress: (_event) => true,
28
+ hoverIn: (_event) => true,
29
+ hoverOut: (_event) => true,
30
+ };
31
+ // Bridge the whole press surface onto a child Pressable: each host onX callback re-emits the
32
+ // matching event on the wrapper. Every Touchable* wraps Pressable, so spread this onto the child's
33
+ // props. A wrapper that intercepts an event for its own state (e.g. TouchableOpacity's opacity
34
+ // animation on press-in/out) overrides that one key AFTER the spread.
35
+ export function emitPressableEvents(emit) {
36
+ return {
37
+ onPress: event => emit('press', event),
38
+ onPressIn: event => emit('pressIn', event),
39
+ onPressOut: event => emit('pressOut', event),
40
+ onPressMove: event => emit('pressMove', event),
41
+ onLongPress: event => emit('longPress', event),
42
+ onHoverIn: event => emit('hoverIn', event),
43
+ onHoverOut: event => emit('hoverOut', event),
44
+ };
45
+ }
46
+ function isRecord(value) {
47
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
48
+ }
49
+ function isStyleFn(value) {
50
+ return typeof value === 'function';
51
+ }
52
+ function numberOr(value, fallback) {
53
+ return typeof value === 'number' ? value : fallback;
54
+ }
55
+ // A scalar offset, or the per-edge object; anything else is dropped (the machine reads undefined
56
+ // as "no offset" → RN's defaults).
57
+ function asRectOffset(value) {
58
+ if (typeof value === 'number')
59
+ return value;
60
+ if (!isRecord(value))
61
+ return undefined;
62
+ const rect = {};
63
+ if (typeof value.top === 'number')
64
+ rect.top = value.top;
65
+ if (typeof value.left === 'number')
66
+ rect.left = value.left;
67
+ if (typeof value.bottom === 'number')
68
+ rect.bottom = value.bottom;
69
+ if (typeof value.right === 'number')
70
+ rect.right = value.right;
71
+ return rect;
72
+ }
73
+ // android_ripple arrives untyped; keep only the fields the shared rippleProps reads.
74
+ function asRippleConfig(value) {
75
+ if (!isRecord(value))
76
+ return undefined;
77
+ const config = {};
78
+ if (typeof value.color === 'string')
79
+ config.color = value.color;
80
+ if (typeof value.borderless === 'boolean')
81
+ config.borderless = value.borderless;
82
+ if (typeof value.radius === 'number')
83
+ config.radius = value.radius;
84
+ if (typeof value.foreground === 'boolean')
85
+ config.foreground = value.foreground;
86
+ return config;
87
+ }
88
+ // The user's accessibilityState, narrowed to the known fields (the disabled merge happens after).
89
+ function asAccessibilityState(value) {
90
+ if (!isRecord(value))
91
+ return undefined;
92
+ const state = {};
93
+ if (typeof value.disabled === 'boolean')
94
+ state.disabled = value.disabled;
95
+ if (typeof value.selected === 'boolean')
96
+ state.selected = value.selected;
97
+ if (value.checked === 'mixed' || typeof value.checked === 'boolean')
98
+ state.checked = value.checked;
99
+ if (typeof value.busy === 'boolean')
100
+ state.busy = value.busy;
101
+ if (typeof value.expanded === 'boolean')
102
+ state.expanded = value.expanded;
103
+ return state;
104
+ }
105
+ function resolveStyle(value, state) {
106
+ if (isStyleFn(value))
107
+ return value(state);
108
+ return value;
109
+ }
110
+ // The prop/handler keys the lifecycle consumes itself; everything else (aria/accessibility/
111
+ // testID/nativeID/native props) forwards onto the View. The user press callbacks are pure JS and
112
+ // must never reach the host; the machine's synthesized handlers go on via buildPressableListeners.
113
+ const HANDLED_ATTRS = [
114
+ 'onPress',
115
+ 'onPressIn',
116
+ 'onPressOut',
117
+ 'onPressMove',
118
+ 'onLongPress',
119
+ 'delayLongPress',
120
+ 'disabled',
121
+ 'cancelable',
122
+ 'pressRetentionOffset',
123
+ 'unstable_pressDelay',
124
+ 'android_ripple',
125
+ 'android_disableSound',
126
+ 'onHoverIn',
127
+ 'onHoverOut',
128
+ 'delayHoverIn',
129
+ 'delayHoverOut',
130
+ 'style',
131
+ 'accessibilityState',
132
+ ];
133
+ function forwardAttrs(attrs) {
134
+ const result = {};
135
+ for (const key of Object.keys(attrs)) {
136
+ if (!HANDLED_ATTRS.includes(key))
137
+ result[key] = attrs[key];
138
+ }
139
+ return result;
140
+ }
141
+ export const Pressable = defineComponent((_props, { slots, attrs: rawAttrs, emit }) => {
142
+ const pressed = ref(false);
143
+ // The mutable press runtime (timers, suppression flags, measured region). A plain setup-scope
144
+ // object, never a ref: it is mutated by the machine, never reactively read.
145
+ const runtime = createPressRuntime();
146
+ // shallowRef, NOT ref: the engine node is held by IDENTITY so measure() hits the engine's
147
+ // WeakMap mirror (a plain ref would wrap it in a reactive Proxy → mirror miss → measure no-op).
148
+ const nodeRef = shallowRef(null);
149
+ const setNodeRef = (el) => {
150
+ nodeRef.value = isSymbioteNode(el) ? el : null;
151
+ };
152
+ // The lifecycle seam the machine fills: flip the reactive `pressed`, and expose the responder
153
+ // View's raw frame-measure (or undefined before the node commits → radius fallback).
154
+ const host = {
155
+ setPressed: next => {
156
+ pressed.value = next;
157
+ },
158
+ getMeasureFn: () => {
159
+ const node = nodeRef.value;
160
+ if (node === null)
161
+ return undefined;
162
+ return callback => measure(node, callback);
163
+ },
164
+ schedule: (callback, ms) => {
165
+ const id = setTimeout(callback, ms);
166
+ return () => clearTimeout(id);
167
+ },
168
+ };
169
+ return () => {
170
+ const attrs = normalizeVueAttrs(rawAttrs);
171
+ const disabled = attrs.disabled === true ? true : undefined;
172
+ const cancelable = typeof attrs.cancelable === 'boolean' ? attrs.cancelable : undefined;
173
+ const config = {
174
+ onPress: event => emit('press', event),
175
+ onPressIn: event => emit('pressIn', event),
176
+ onPressOut: event => emit('pressOut', event),
177
+ onPressMove: event => emit('pressMove', event),
178
+ onLongPress: event => emit('longPress', event),
179
+ delayLongPress: numberOr(attrs.delayLongPress, DEFAULT_DELAY_LONG_PRESS_MS),
180
+ unstable_pressDelay: numberOr(attrs.unstable_pressDelay, 0),
181
+ hitSlop: asRectOffset(attrs.hitSlop),
182
+ pressRetentionOffset: asRectOffset(attrs.pressRetentionOffset),
183
+ };
184
+ const handlers = createPressHandlers(config, runtime, host);
185
+ noteHoverNoop(undefined, undefined);
186
+ const state = { pressed: pressed.value };
187
+ // Fold the disabled state into the user's accessibilityState, then fold aria/role over the
188
+ // forwarded attrs (the Vue View is a bare host primitive, so Pressable folds; React's View
189
+ // folds for it). resolveAccessibilityProps merges aria into the accessibilityState we set.
190
+ const forwarded = forwardAttrs(attrs);
191
+ forwarded.accessibilityState = resolveDisabledAccessibilityState(asAccessibilityState(attrs.accessibilityState), disabled);
192
+ const folded = resolveAccessibilityProps(forwarded);
193
+ const viewProps = {
194
+ ...folded,
195
+ ref: setNodeRef,
196
+ style: resolveStyle(attrs.style, state),
197
+ };
198
+ if (typeof attrs.android_disableSound === 'boolean')
199
+ viewProps.android_disableSound = attrs.android_disableSound;
200
+ Object.assign(viewProps, buildPressableListeners(handlers, { disabled, cancelable }));
201
+ // Children come from the (scoped) default slot, receiving the press state so a render-prop
202
+ // child (`v-slot="{ pressed }"`) mirrors React's children-as-function.
203
+ const content = slots.default !== undefined ? slots.default(state) : [];
204
+ // android_ripple rides a dedicated inner View; on iOS the prop is undefined, so the child
205
+ // renders unwrapped, no extra node. Mirrors the React Pressable + touchable-native-feedback.
206
+ const ripple = isRecord(attrs.android_ripple)
207
+ ? rippleProps(asRippleConfig(attrs.android_ripple) ?? {})
208
+ : undefined;
209
+ const inner = ripple !== undefined ? [h(View, ripple, () => content)] : content;
210
+ // Children go to the host View as a FUNCTION slot, never a raw array: View is a
211
+ // functional component, and an array child makes Vue normalize it to a default
212
+ // slot with a dev warn ("Prefer function slots"). Benign under SFC, but in JSX the
213
+ // warn's trace formats the __self/__source dev props (native HostObjects) and that
214
+ // read throws, unwinding the whole mount → blank screen. A function slot skips it.
215
+ return h(View, viewProps, () => inner);
216
+ };
217
+ }, {
218
+ name: 'Pressable',
219
+ inheritAttrs: false,
220
+ emits: PRESSABLE_EMITS,
221
+ });
@@ -0,0 +1,20 @@
1
+ import { type IClassNameValue } from '@symbiote-native/engine';
2
+ import { type IAccessibilityProps, type IAriaProps } from '@symbiote-native/components';
3
+ export interface IRefreshControlProps extends IAccessibilityProps, IAriaProps {
4
+ refreshing: boolean;
5
+ tintColor?: string;
6
+ title?: string;
7
+ titleColor?: string;
8
+ progressViewOffset?: number;
9
+ colors?: readonly string[];
10
+ progressBackgroundColor?: string;
11
+ size?: 'default' | 'large';
12
+ enabled?: boolean;
13
+ class?: IClassNameValue;
14
+ }
15
+ export type IRefreshControlEmits = {
16
+ refresh: () => boolean;
17
+ };
18
+ export declare const RefreshControl: import("vue").DefineSetupFnComponent<IRefreshControlProps, IRefreshControlEmits, {}, IRefreshControlProps & {
19
+ onRefresh?: (() => any) | undefined;
20
+ }, import("vue").PublicProps>;
@@ -0,0 +1,53 @@
1
+ // RefreshControl, the Vue lifecycle half (ADR 0024 Phase 2). On iOS this is the PullToRefreshView
2
+ // Fabric node that lives INSIDE a ScrollView (a sibling before the content container); on Android it
3
+ // is AndroidSwipeRefreshLayout and WRAPS the scroll view (a ScrollView there hosts one child). The
4
+ // Vue twin of the React adapter's RefreshControl. Vue takes the wrapped child via its DEFAULT SLOT
5
+ // (the seam the Android scroll-view wrap re-invokes to host the scroll view inside it; iOS leaves the
6
+ // slot empty), folds aria/role through the shared resolveAccessibilityProps, and forwards the native
7
+ // props onto the symbiote-refresh-control host node.
8
+ //
9
+ // `refreshing` is a controlled prop: the parent owns it and pushes it down each commit; native
10
+ // reports the gesture via the direct `topRefresh` event, which the engine routes to the host
11
+ // onRefresh prop; the adapter turns that into the typed Vue `refresh` emit. Inputs arrive as attrs
12
+ // (untyped), so the forwarded bag is BUILT at the a11y-intersection type (a genuine narrowing, not
13
+ // a cast) before the aria fold.
14
+ import { defineComponent, h } from '@vue/runtime-core';
15
+ import { dlog } from '@symbiote-native/engine';
16
+ import { resolveAccessibilityProps, } from '@symbiote-native/components';
17
+ import { normalizeVueAttrs } from '../utils/normalize-attrs';
18
+ // Copy every non-consumed attr into a bag typed as the a11y intersection (the accumulator is BUILT
19
+ // at that type, a real narrowing, not a cast), then fold aria-*/role into the canonical
20
+ // accessibility* props. Children cross via slots, never attrs. When the Android wrap re-invokes
21
+ // this component, the injected `style` (the outer/layout half) rides through too.
22
+ function foldAttrs(attrs) {
23
+ const bag = {};
24
+ for (const key of Object.keys(attrs)) {
25
+ if (key !== 'onRefresh')
26
+ bag[key] = attrs[key];
27
+ }
28
+ return resolveAccessibilityProps(bag);
29
+ }
30
+ export const RefreshControl = defineComponent((_props, { attrs: rawAttrs, emit, slots }) => {
31
+ return () => {
32
+ const nativeProps = foldAttrs(normalizeVueAttrs(rawAttrs));
33
+ dlog('RefreshControl -> PullToRefreshView');
34
+ dlog(`RefreshControl refreshing=${String(nativeProps.refreshing)}`);
35
+ if (nativeProps.enabled !== undefined)
36
+ dlog(`RefreshControl enabled=${String(nativeProps.enabled)} (Android-only)`);
37
+ dlog('RefreshControl refresh emit wired');
38
+ // The default slot is the seam: empty on iOS (childless sibling), but the Android scroll-view
39
+ // wrap re-invokes this component with the scroll view as its default slot, so it hosts it.
40
+ return h('symbiote-refresh-control', {
41
+ ...nativeProps,
42
+ onRefresh: () => {
43
+ emit('refresh');
44
+ },
45
+ }, slots.default !== undefined ? slots.default() : undefined);
46
+ };
47
+ }, {
48
+ name: 'RefreshControl',
49
+ inheritAttrs: false,
50
+ emits: {
51
+ refresh: () => true,
52
+ },
53
+ });
@@ -0,0 +1,8 @@
1
+ import { type IClassNameValue } from '@symbiote-native/engine';
2
+ import { type IAccessibilityProps, type IAriaProps } from '@symbiote-native/components';
3
+ export type ISafeAreaViewProps = IAccessibilityProps & IAriaProps & {
4
+ class?: IClassNameValue;
5
+ };
6
+ export declare const SafeAreaView: 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>;