@symbiote-native/components 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 +110 -0
- package/build/accessibility-props.d.ts +64 -0
- package/build/accessibility-props.js +150 -0
- package/build/component-names/index.android.d.ts +3 -0
- package/build/component-names/index.android.js +32 -0
- package/build/component-names/index.d.ts +1 -0
- package/build/component-names/index.ios.d.ts +3 -0
- package/build/component-names/index.ios.js +26 -0
- package/build/component-names/index.js +5 -0
- package/build/component-names/shared.d.ts +7 -0
- package/build/component-names/shared.js +36 -0
- package/build/descriptor.d.ts +11 -0
- package/build/descriptor.js +17 -0
- package/build/index.d.ts +51 -0
- package/build/index.js +63 -0
- package/build/responder-props.d.ts +18 -0
- package/build/responder-props.js +9 -0
- package/build/scroll-view-commands.d.ts +23 -0
- package/build/scroll-view-commands.js +123 -0
- package/build/state/drawer-layout-android.d.ts +22 -0
- package/build/state/drawer-layout-android.js +53 -0
- package/build/state/flat-list.d.ts +12 -0
- package/build/state/flat-list.js +40 -0
- package/build/state/modal.d.ts +11 -0
- package/build/state/modal.js +28 -0
- package/build/state/pressable.d.ts +90 -0
- package/build/state/pressable.js +236 -0
- package/build/state/section-list.d.ts +46 -0
- package/build/state/section-list.js +51 -0
- package/build/state/switch.d.ts +12 -0
- package/build/state/switch.js +31 -0
- package/build/state/text-input.d.ts +103 -0
- package/build/state/text-input.js +205 -0
- package/build/state/touchable.d.ts +15 -0
- package/build/state/touchable.js +22 -0
- package/build/state/virtualized-list.d.ts +161 -0
- package/build/state/virtualized-list.js +306 -0
- package/build/view/render-activity-indicator.d.ts +25 -0
- package/build/view/render-activity-indicator.js +54 -0
- package/build/view/render-button.d.ts +19 -0
- package/build/view/render-button.js +24 -0
- package/build/view/render-drawer-layout-android.d.ts +23 -0
- package/build/view/render-drawer-layout-android.js +56 -0
- package/build/view/render-image-background.d.ts +9 -0
- package/build/view/render-image-background.js +48 -0
- package/build/view/render-image.d.ts +86 -0
- package/build/view/render-image.js +298 -0
- package/build/view/render-input-accessory-view.d.ts +9 -0
- package/build/view/render-input-accessory-view.js +18 -0
- package/build/view/render-keyboard-avoiding-view.d.ts +30 -0
- package/build/view/render-keyboard-avoiding-view.js +75 -0
- package/build/view/render-modal.d.ts +23 -0
- package/build/view/render-modal.js +70 -0
- package/build/view/render-pressable.d.ts +8 -0
- package/build/view/render-pressable.js +42 -0
- package/build/view/render-scroll-sticky.d.ts +24 -0
- package/build/view/render-scroll-sticky.js +81 -0
- package/build/view/render-scroll-view.d.ts +18 -0
- package/build/view/render-scroll-view.js +85 -0
- package/build/view/render-switch.d.ts +29 -0
- package/build/view/render-switch.js +33 -0
- package/build/view/render-text-input.d.ts +11 -0
- package/build/view/render-text-input.js +35 -0
- package/build/view/render-touchable-native-feedback.d.ts +17 -0
- package/build/view/render-touchable-native-feedback.js +39 -0
- package/package.json +38 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ISymbioteEvent } from '@symbiote-native/engine';
|
|
2
|
+
type IResponderGate = (event: ISymbioteEvent) => boolean;
|
|
3
|
+
type IResponderHandler = (event: ISymbioteEvent) => void;
|
|
4
|
+
export interface IResponderProps {
|
|
5
|
+
onStartShouldSetResponder?: IResponderGate;
|
|
6
|
+
onStartShouldSetResponderCapture?: IResponderGate;
|
|
7
|
+
onMoveShouldSetResponder?: IResponderGate;
|
|
8
|
+
onMoveShouldSetResponderCapture?: IResponderGate;
|
|
9
|
+
onResponderGrant?: IResponderHandler;
|
|
10
|
+
onResponderReject?: IResponderHandler;
|
|
11
|
+
onResponderStart?: IResponderHandler;
|
|
12
|
+
onResponderMove?: IResponderHandler;
|
|
13
|
+
onResponderEnd?: IResponderHandler;
|
|
14
|
+
onResponderRelease?: IResponderHandler;
|
|
15
|
+
onResponderTerminate?: IResponderHandler;
|
|
16
|
+
onResponderTerminationRequest?: IResponderGate;
|
|
17
|
+
}
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// The gesture-responder handler props a View accepts. They are the public face of the
|
|
2
|
+
// responder negotiation @symbiote-native/engine's events run: the should-set gates decide who owns a
|
|
3
|
+
// touch (capture root->target, then bubble target->root, first true wins), and the lifecycle
|
|
4
|
+
// handlers receive the grant/move/release/terminate of the gesture. PanResponder produces
|
|
5
|
+
// exactly this shape as its `panHandlers`; a caller can also set any of these directly on a
|
|
6
|
+
// View. The should-set / termination-request gates return a boolean; the rest are
|
|
7
|
+
// side-effecting. Framework-agnostic (only ISymbioteEvent), so it lives in @symbiote-native/components
|
|
8
|
+
// and every adapter re-exports it as the base of its View props.
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type AnimatedValue, type IStyleProp, type ISymbioteEvent, type ISymbioteNode, type IViewStyle } from '@symbiote-native/engine';
|
|
2
|
+
type IScrollHandler = (event: ISymbioteEvent) => void;
|
|
3
|
+
export interface IScrollViewHandle {
|
|
4
|
+
scrollTo(options?: {
|
|
5
|
+
x?: number;
|
|
6
|
+
y?: number;
|
|
7
|
+
animated?: boolean;
|
|
8
|
+
}): void;
|
|
9
|
+
scrollToEnd(options?: {
|
|
10
|
+
animated?: boolean;
|
|
11
|
+
}): void;
|
|
12
|
+
flashScrollIndicators(): void;
|
|
13
|
+
getScrollNode(): ISymbioteNode | null;
|
|
14
|
+
}
|
|
15
|
+
export declare function splitLayoutProps(style: IStyleProp<IViewStyle> | undefined): {
|
|
16
|
+
outer: Record<string, unknown>;
|
|
17
|
+
inner: Record<string, unknown>;
|
|
18
|
+
};
|
|
19
|
+
export declare function isSymbioteEvent(value: unknown): value is ISymbioteEvent;
|
|
20
|
+
export declare function forwardScrollEvent(handler: IScrollHandler, args: readonly unknown[]): void;
|
|
21
|
+
export declare function buildScrollViewHandle(getNode: () => ISymbioteNode | null): IScrollViewHandle;
|
|
22
|
+
export declare function attachStickyScroll(node: ISymbioteNode, value: AnimatedValue): () => void;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// ScrollView: the imperative + style-routing module (framework-agnostic, no 3-layer split:
|
|
2
|
+
// ScrollView has no state machine). The imperative handle, the layout/visual style split for
|
|
3
|
+
// the Android RefreshControl wrap, the scroll-event guard/forwarder, and the native sticky
|
|
4
|
+
// scroll-attach are all platform- and framework-invariant, so they live here. The adapter
|
|
5
|
+
// supplies the lifecycle (the node getter, the effect) and re-exports these.
|
|
6
|
+
import { attachNativeEvent, dispatchViewCommand, dlog, flattenStyle, } from '@symbiote-native/engine';
|
|
7
|
+
// RN's splitLayoutProps key partition (StyleSheet/splitLayoutProps.js): the LAYOUT keys
|
|
8
|
+
// that belong on the OUTER box when a layout-affecting wrapper sits between the laid-out
|
|
9
|
+
// frame and the visual content. Everything NOT in this set (background*, padding*, border*,
|
|
10
|
+
// opacity, overflow, …) is VISUAL and stays on the inner view. Replicated exactly from RN's
|
|
11
|
+
// switch cases so the Android RefreshControl wrap routes style the way RN does.
|
|
12
|
+
const LAYOUT_KEYS = new Set([
|
|
13
|
+
'margin',
|
|
14
|
+
'marginHorizontal',
|
|
15
|
+
'marginVertical',
|
|
16
|
+
'marginBottom',
|
|
17
|
+
'marginTop',
|
|
18
|
+
'marginLeft',
|
|
19
|
+
'marginRight',
|
|
20
|
+
'flex',
|
|
21
|
+
'flexGrow',
|
|
22
|
+
'flexShrink',
|
|
23
|
+
'flexBasis',
|
|
24
|
+
'alignSelf',
|
|
25
|
+
'height',
|
|
26
|
+
'minHeight',
|
|
27
|
+
'maxHeight',
|
|
28
|
+
'width',
|
|
29
|
+
'minWidth',
|
|
30
|
+
'maxWidth',
|
|
31
|
+
'position',
|
|
32
|
+
'left',
|
|
33
|
+
'right',
|
|
34
|
+
'bottom',
|
|
35
|
+
'top',
|
|
36
|
+
'transform',
|
|
37
|
+
'transformOrigin',
|
|
38
|
+
'rowGap',
|
|
39
|
+
'columnGap',
|
|
40
|
+
'gap',
|
|
41
|
+
]);
|
|
42
|
+
// Split a flattened style into the LAYOUT props that drive the outer wrapper's frame and the
|
|
43
|
+
// VISUAL props that paint the inner content, RN's splitLayoutProps. The Android build uses
|
|
44
|
+
// this when a RefreshControl wraps the scroll view: layout (margin/flex/size/position/…) goes
|
|
45
|
+
// on the AndroidSwipeRefreshLayout wrapper, visual (background/padding/border/…) stays on the
|
|
46
|
+
// inner scroll view, instead of dumping the whole style on the wrapper and hardcoding flex:1.
|
|
47
|
+
export function splitLayoutProps(style) {
|
|
48
|
+
const outer = {};
|
|
49
|
+
const inner = {};
|
|
50
|
+
// Reads keys off the style, so flatten the StyleProp (array/nested) to one object first.
|
|
51
|
+
const flat = flattenStyle(style);
|
|
52
|
+
for (const key of Object.keys(flat)) {
|
|
53
|
+
const value = Reflect.get(flat, key);
|
|
54
|
+
if (LAYOUT_KEYS.has(key))
|
|
55
|
+
outer[key] = value;
|
|
56
|
+
else
|
|
57
|
+
inner[key] = value;
|
|
58
|
+
}
|
|
59
|
+
return { outer, inner };
|
|
60
|
+
}
|
|
61
|
+
export function isSymbioteEvent(value) {
|
|
62
|
+
if (typeof value !== 'object' || value === null)
|
|
63
|
+
return false;
|
|
64
|
+
const nativeEvent = Reflect.get(value, 'nativeEvent');
|
|
65
|
+
return typeof nativeEvent === 'object' && nativeEvent !== null;
|
|
66
|
+
}
|
|
67
|
+
// Forward a wrapped scroll event to the user's ScrollHandler. The Animated.event listener
|
|
68
|
+
// hands raw args; the first is the original SymbioteEvent, which we narrow with a runtime
|
|
69
|
+
// guard (no cast) and pass through unchanged so the user sees the same event RN would deliver.
|
|
70
|
+
export function forwardScrollEvent(handler, args) {
|
|
71
|
+
const first = args[0];
|
|
72
|
+
if (isSymbioteEvent(first))
|
|
73
|
+
handler(first);
|
|
74
|
+
}
|
|
75
|
+
// The imperative handle is identical across platforms: every method dispatches a view
|
|
76
|
+
// command on the SAME scroll-view node; only the surrounding element assembly diverges
|
|
77
|
+
// (iOS sibling RefreshControl vs Android wrap). So it is built once here and both platform
|
|
78
|
+
// files back it with their scroll node getter. Commands and arg order mirror RN's
|
|
79
|
+
// ScrollViewCommands: scrollTo [x, y, animated], scrollToEnd [animated], flashScrollIndicators [].
|
|
80
|
+
//
|
|
81
|
+
// `getNode` is a LAZY getter (React `() => ref.current`, Vue `() => nodeRef.value`), read on
|
|
82
|
+
// every call, NOT the node captured once. The node is null at mount and only set after the
|
|
83
|
+
// element commits, so an eager capture would freeze `null` and every command would no-op.
|
|
84
|
+
export function buildScrollViewHandle(getNode) {
|
|
85
|
+
return {
|
|
86
|
+
scrollTo: (options) => {
|
|
87
|
+
const node = getNode();
|
|
88
|
+
if (node === null)
|
|
89
|
+
return;
|
|
90
|
+
const x = options?.x ?? 0;
|
|
91
|
+
const y = options?.y ?? 0;
|
|
92
|
+
const animated = options?.animated ?? true;
|
|
93
|
+
dlog(`ScrollView.scrollTo x=${x} y=${y} animated=${animated}`);
|
|
94
|
+
dispatchViewCommand(node, 'scrollTo', [x, y, animated]);
|
|
95
|
+
},
|
|
96
|
+
scrollToEnd: (options) => {
|
|
97
|
+
const node = getNode();
|
|
98
|
+
if (node === null)
|
|
99
|
+
return;
|
|
100
|
+
const animated = options?.animated ?? true;
|
|
101
|
+
dlog(`ScrollView.scrollToEnd animated=${animated}`);
|
|
102
|
+
dispatchViewCommand(node, 'scrollToEnd', [animated]);
|
|
103
|
+
},
|
|
104
|
+
flashScrollIndicators: () => {
|
|
105
|
+
const node = getNode();
|
|
106
|
+
if (node === null)
|
|
107
|
+
return;
|
|
108
|
+
dlog('ScrollView.flashScrollIndicators');
|
|
109
|
+
dispatchViewCommand(node, 'flashScrollIndicators', []);
|
|
110
|
+
},
|
|
111
|
+
getScrollNode: () => getNode(),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
// Attach the scroll event to the scroll-offset value on the NATIVE driver, RN's
|
|
115
|
+
// _updateAnimatedNodeAttachment / AnimatedImplementation.attachNativeEvent (ScrollView.js:1087).
|
|
116
|
+
// The value then tracks scroll on the UI thread and the sticky-header interpolations ride it
|
|
117
|
+
// natively (no JS jitter). Returns the detach function the adapter's effect calls on cleanup.
|
|
118
|
+
export function attachStickyScroll(node, value) {
|
|
119
|
+
const attachment = attachNativeEvent(node, 'onScroll', [
|
|
120
|
+
{ nativeEvent: { contentOffset: { y: value } } },
|
|
121
|
+
]);
|
|
122
|
+
return () => attachment.detach();
|
|
123
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type ISymbioteEvent, type ISymbioteNode } from '@symbiote/engine';
|
|
2
|
+
export type IDrawerPosition = 'left' | 'right';
|
|
3
|
+
export type IDrawerLockMode = 'unlocked' | 'locked-closed' | 'locked-open';
|
|
4
|
+
export type IKeyboardDismissMode = 'none' | 'on-drag';
|
|
5
|
+
export type IDrawerState = 'Idle' | 'Dragging' | 'Settling';
|
|
6
|
+
export interface IDrawerSlideEvent {
|
|
7
|
+
offset: number;
|
|
8
|
+
}
|
|
9
|
+
export interface IDrawerLayoutAndroidHandle {
|
|
10
|
+
openDrawer(): void;
|
|
11
|
+
closeDrawer(): void;
|
|
12
|
+
}
|
|
13
|
+
export declare const DRAWER_VIEW_NAME = "AndroidDrawerLayout";
|
|
14
|
+
export declare const OPEN_DRAWER_COMMAND = "openDrawer";
|
|
15
|
+
export declare const CLOSE_DRAWER_COMMAND = "closeDrawer";
|
|
16
|
+
export declare const DRAWER_STATES: ReadonlyArray<IDrawerState>;
|
|
17
|
+
export declare const DEFAULT_DRAWER_BACKGROUND_COLOR = "white";
|
|
18
|
+
export declare const DEFAULT_DRAWER_POSITION: IDrawerPosition;
|
|
19
|
+
export declare function offsetFromSlide(event: ISymbioteEvent): number;
|
|
20
|
+
export declare function stateFromChange(event: ISymbioteEvent): IDrawerState;
|
|
21
|
+
export declare function dispatchDrawerCommand(node: ISymbioteNode | null, command: string): void;
|
|
22
|
+
export declare function buildDrawerHandle(getNode: () => ISymbioteNode | null): IDrawerLayoutAndroidHandle;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// DrawerLayoutAndroid: the framework-agnostic logic half. AndroidDrawerLayout is Android-only, so
|
|
2
|
+
// this module holds only the platform-invariant contract every adapter shares — the drawer-position
|
|
3
|
+
// / lock-mode / keyboard-dismiss / state TYPES, the native view + command NAMES, the slide/state
|
|
4
|
+
// event NORMALIZATION, and the imperative open/close HANDLE. The adapter supplies only its lifecycle
|
|
5
|
+
// (refs / reactivity) + the descriptor→element bridge; the view (style/prop) math lives in
|
|
6
|
+
// view/render-drawer-layout-android.ts. No Platform.OS read; the adapter's filename selects the build
|
|
7
|
+
// (ADR 0019). The wolf-tui twin shape is the shared state module pulled out of each reconciler.
|
|
8
|
+
import { dispatchViewCommand, dlog, } from '@symbiote/engine';
|
|
9
|
+
// The native view name registered by AndroidDrawerLayoutNativeComponent's
|
|
10
|
+
// codegenNativeComponent('AndroidDrawerLayout'): the derive-by-default name (any non-`symbiote-*`
|
|
11
|
+
// type flows through descriptorFor untouched, the engine deriving its events from the ViewConfig).
|
|
12
|
+
export const DRAWER_VIEW_NAME = 'AndroidDrawerLayout';
|
|
13
|
+
export const OPEN_DRAWER_COMMAND = 'openDrawer';
|
|
14
|
+
export const CLOSE_DRAWER_COMMAND = 'closeDrawer';
|
|
15
|
+
// RN's drawerState int -> string mapping (android DRAWER_STATES indexed by the native drawerState):
|
|
16
|
+
// 0=Idle, 1=Dragging, 2=Settling.
|
|
17
|
+
export const DRAWER_STATES = ['Idle', 'Dragging', 'Settling'];
|
|
18
|
+
export const DEFAULT_DRAWER_BACKGROUND_COLOR = 'white';
|
|
19
|
+
export const DEFAULT_DRAWER_POSITION = 'left';
|
|
20
|
+
// Slide-event normalization: pull the native drag offset (RN onDrawerSlide nativeEvent.offset).
|
|
21
|
+
export function offsetFromSlide(event) {
|
|
22
|
+
const offset = event.nativeEvent.offset;
|
|
23
|
+
return typeof offset === 'number' ? offset : 0;
|
|
24
|
+
}
|
|
25
|
+
// State-change normalization: map the native drawerState int onto its DRAWER_STATES label.
|
|
26
|
+
export function stateFromChange(event) {
|
|
27
|
+
const index = event.nativeEvent.drawerState;
|
|
28
|
+
if (typeof index === 'number' && index >= 0 && index < DRAWER_STATES.length) {
|
|
29
|
+
return DRAWER_STATES[index];
|
|
30
|
+
}
|
|
31
|
+
return 'Idle';
|
|
32
|
+
}
|
|
33
|
+
// Issue a drawer command against the committed host node, or log a silent no-op when there is no
|
|
34
|
+
// node yet (the first render has not committed). Mirrors Switch's dispatchViewCommand path.
|
|
35
|
+
export function dispatchDrawerCommand(node, command) {
|
|
36
|
+
if (node === null) {
|
|
37
|
+
dlog(`DrawerLayoutAndroid ${command} no-op: no committed host node`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
dlog(`DrawerLayoutAndroid dispatch ${command}`);
|
|
41
|
+
dispatchViewCommand(node, command, []);
|
|
42
|
+
}
|
|
43
|
+
// The imperative handle is identical across adapters: openDrawer / closeDrawer dispatch the matching
|
|
44
|
+
// view command on the SAME host node. Built once here; each adapter backs it with its lazy node
|
|
45
|
+
// getter (React `() => ref.current`, Vue `() => nodeRef.value`), read on every call — the node is
|
|
46
|
+
// null until the element commits, so an eager capture would freeze null and every command no-op.
|
|
47
|
+
// The Vue twin of React's useImperativeHandle(ref, …); mirrors buildScrollViewHandle.
|
|
48
|
+
export function buildDrawerHandle(getNode) {
|
|
49
|
+
return {
|
|
50
|
+
openDrawer: () => dispatchDrawerCommand(getNode(), OPEN_DRAWER_COMMAND),
|
|
51
|
+
closeDrawer: () => dispatchDrawerCommand(getNode(), CLOSE_DRAWER_COMMAND),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { IViewableItemsChangedInfo, IViewToken } from './virtualized-list';
|
|
2
|
+
export declare const SINGLE_COLUMN = 1;
|
|
3
|
+
export interface IRow<ItemT> {
|
|
4
|
+
items: ItemT[];
|
|
5
|
+
startIndex: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function chunkIntoRows<ItemT>(data: readonly ItemT[], columns: number): IRow<ItemT>[];
|
|
8
|
+
export declare function rowKeyExtractor<ItemT>(row: IRow<ItemT>): string;
|
|
9
|
+
export declare function expandRowToken<ItemT>(token: IViewToken<IRow<ItemT>>, keyExtractor?: (item: ItemT, index: number) => string): IViewToken<ItemT>[];
|
|
10
|
+
export declare function expandRowViewability<ItemT>(info: IViewableItemsChangedInfo<IRow<ItemT>>, keyExtractor?: (item: ItemT, index: number) => string): IViewableItemsChangedInfo<ItemT>;
|
|
11
|
+
export declare function lastItemOfRow<ItemT>(row: IRow<ItemT> | undefined): ItemT | undefined;
|
|
12
|
+
export declare function firstItemOfRow<ItemT>(row: IRow<ItemT> | undefined): ItemT | undefined;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// FlatList logic: the framework-agnostic data adaptation over VirtualizedList. A plain
|
|
2
|
+
// `data` array becomes getItem/getItemCount; numColumns packs items into rows so the
|
|
3
|
+
// virtualized stream is rows, not items (RN's FlatList). Viewability over rows expands
|
|
4
|
+
// back to per-item tokens, and the row separator unwraps to the flanking items. All of
|
|
5
|
+
// this is pure transform — every adapter (React, Vue) reuses it; the adapter supplies
|
|
6
|
+
// only the element creation (createElement / h) for a row and the ref wiring.
|
|
7
|
+
export const SINGLE_COLUMN = 1;
|
|
8
|
+
export function chunkIntoRows(data, columns) {
|
|
9
|
+
const rows = [];
|
|
10
|
+
for (let start = 0; start < data.length; start += columns) {
|
|
11
|
+
rows.push({ items: data.slice(start, start + columns), startIndex: start });
|
|
12
|
+
}
|
|
13
|
+
return rows;
|
|
14
|
+
}
|
|
15
|
+
export function rowKeyExtractor(row) {
|
|
16
|
+
return `row-${row.startIndex}`;
|
|
17
|
+
}
|
|
18
|
+
// Expand a row's viewable token to one token per item in that row, all sharing the row's
|
|
19
|
+
// isViewable flag, so the caller sees item-level visibility rather than row-level.
|
|
20
|
+
export function expandRowToken(token, keyExtractor) {
|
|
21
|
+
return token.item.items.map((item, column) => {
|
|
22
|
+
const index = token.item.startIndex + column;
|
|
23
|
+
const key = keyExtractor ? keyExtractor(item, index) : String(index);
|
|
24
|
+
return { item, key, index, isViewable: token.isViewable };
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
export function expandRowViewability(info, keyExtractor) {
|
|
28
|
+
return {
|
|
29
|
+
viewableItems: info.viewableItems.flatMap(token => expandRowToken(token, keyExtractor)),
|
|
30
|
+
changed: info.changed.flatMap(token => expandRowToken(token, keyExtractor)),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
// The divider between rows shows real items, not the IRow wrapper: its leading item is the
|
|
34
|
+
// LAST item of the row above, its trailing item the FIRST item of the row below.
|
|
35
|
+
export function lastItemOfRow(row) {
|
|
36
|
+
return row !== undefined ? row.items[row.items.length - 1] : undefined;
|
|
37
|
+
}
|
|
38
|
+
export function firstItemOfRow(row) {
|
|
39
|
+
return row !== undefined ? row.items[0] : undefined;
|
|
40
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type IModalState = {
|
|
2
|
+
isRendered: boolean;
|
|
3
|
+
};
|
|
4
|
+
export declare function createInitialModalState(isVisible: boolean): IModalState;
|
|
5
|
+
export type IModalAction = {
|
|
6
|
+
type: 'show';
|
|
7
|
+
} | {
|
|
8
|
+
type: 'hide';
|
|
9
|
+
};
|
|
10
|
+
export declare function modalReducer(state: IModalState, action: IModalAction): IModalState;
|
|
11
|
+
export declare function shouldRenderModal(isVisible: boolean, state: IModalState): boolean;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Modal: the logic half (framework-agnostic). RN keeps the modal mounted through its exit
|
|
2
|
+
// animation (Modal.js _shouldShowModal: visible===true || state.isRendered===true) so the
|
|
3
|
+
// native onDismiss event can arrive before the node unmounts. `isRendered` is PURELY that
|
|
4
|
+
// keep-alive; it never itself fires onDismiss (on Fabric onDismiss is a real native
|
|
5
|
+
// DirectEvent delivered via the host's onDismiss prop). The reducer mirrors RN's guarded
|
|
6
|
+
// transitions: arm the keep-alive on show, drop it on hide. The adapter drives the transition
|
|
7
|
+
// AFTER its render (React useEffect / Vue post-flush watch) so one keep-alive frame survives.
|
|
8
|
+
// On first render the keep-alive matches `visible`: a modal that starts visible is rendered,
|
|
9
|
+
// one that starts hidden contributes no node (the render gate returns null).
|
|
10
|
+
export function createInitialModalState(isVisible) {
|
|
11
|
+
return { isRendered: isVisible };
|
|
12
|
+
}
|
|
13
|
+
// Identity-stable when nothing changes (returns the same object) so the adapter's effect/watch
|
|
14
|
+
// triggers no spurious re-render, matching React's guarded setState in Modal.js's effect.
|
|
15
|
+
export function modalReducer(state, action) {
|
|
16
|
+
switch (action.type) {
|
|
17
|
+
case 'show':
|
|
18
|
+
return state.isRendered ? state : { isRendered: true };
|
|
19
|
+
case 'hide':
|
|
20
|
+
return state.isRendered ? { isRendered: false } : state;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
// The visible gate with the keep-alive: a fully hidden modal (not visible AND no longer
|
|
24
|
+
// rendered) contributes no node, exactly as RN's render returns null when _shouldShowModal()
|
|
25
|
+
// is false.
|
|
26
|
+
export function shouldRenderModal(isVisible, state) {
|
|
27
|
+
return isVisible || state.isRendered;
|
|
28
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { type ISymbioteEvent } from '@symbiote-native/engine';
|
|
2
|
+
export declare const DEFAULT_DELAY_LONG_PRESS_MS = 500;
|
|
3
|
+
export declare const DEFAULT_PRESS_RECT_OFFSETS: {
|
|
4
|
+
top: number;
|
|
5
|
+
left: number;
|
|
6
|
+
bottom: number;
|
|
7
|
+
right: number;
|
|
8
|
+
};
|
|
9
|
+
export interface IEdgeInsets {
|
|
10
|
+
top: number;
|
|
11
|
+
left: number;
|
|
12
|
+
bottom: number;
|
|
13
|
+
right: number;
|
|
14
|
+
}
|
|
15
|
+
export interface IResponderRegion {
|
|
16
|
+
top: number;
|
|
17
|
+
left: number;
|
|
18
|
+
bottom: number;
|
|
19
|
+
right: number;
|
|
20
|
+
}
|
|
21
|
+
export interface IPressState {
|
|
22
|
+
pressed: boolean;
|
|
23
|
+
}
|
|
24
|
+
export type IPressHandler = (event: ISymbioteEvent) => void;
|
|
25
|
+
export type IRectOffset = number | {
|
|
26
|
+
top?: number;
|
|
27
|
+
left?: number;
|
|
28
|
+
bottom?: number;
|
|
29
|
+
right?: number;
|
|
30
|
+
};
|
|
31
|
+
export interface IPressableAndroidRippleConfig {
|
|
32
|
+
color?: string;
|
|
33
|
+
borderless?: boolean;
|
|
34
|
+
radius?: number;
|
|
35
|
+
foreground?: boolean;
|
|
36
|
+
}
|
|
37
|
+
export interface IRippleBackground {
|
|
38
|
+
type: 'RippleAndroid';
|
|
39
|
+
color: string | null;
|
|
40
|
+
borderless: boolean;
|
|
41
|
+
rippleRadius?: number;
|
|
42
|
+
}
|
|
43
|
+
export declare function normalizeRect(offset: IRectOffset | undefined): IEdgeInsets;
|
|
44
|
+
export declare function maxEdge(insets: IEdgeInsets): number;
|
|
45
|
+
export declare function isTouchWithinRegion(point: {
|
|
46
|
+
x: number;
|
|
47
|
+
y: number;
|
|
48
|
+
}, region: IResponderRegion, hitSlop: IEdgeInsets, pressRectOffset: IEdgeInsets): boolean;
|
|
49
|
+
export declare function readPoint(event: ISymbioteEvent): {
|
|
50
|
+
x: number;
|
|
51
|
+
y: number;
|
|
52
|
+
} | undefined;
|
|
53
|
+
export declare function computeRegion(width: number, height: number, pageX: number, pageY: number): IResponderRegion | undefined;
|
|
54
|
+
export declare function rippleProps(config: IPressableAndroidRippleConfig): Record<string, IRippleBackground> | undefined;
|
|
55
|
+
export interface IPressRuntime {
|
|
56
|
+
longPressCancel: (() => void) | undefined;
|
|
57
|
+
longPressFired: boolean;
|
|
58
|
+
pressDelayCancel: (() => void) | undefined;
|
|
59
|
+
pressOrigin: {
|
|
60
|
+
x: number;
|
|
61
|
+
y: number;
|
|
62
|
+
} | undefined;
|
|
63
|
+
driftedOut: boolean;
|
|
64
|
+
region: IResponderRegion | undefined;
|
|
65
|
+
}
|
|
66
|
+
export declare function createPressRuntime(): IPressRuntime;
|
|
67
|
+
export type IFrameCallback = (x: number, y: number, width: number, height: number, pageX: number, pageY: number) => void;
|
|
68
|
+
export interface IPressHost {
|
|
69
|
+
setPressed: (pressed: boolean) => void;
|
|
70
|
+
getMeasureFn: () => ((callback: IFrameCallback) => void) | undefined;
|
|
71
|
+
schedule: (callback: () => void, ms: number) => () => void;
|
|
72
|
+
}
|
|
73
|
+
export interface IPressMachineConfig {
|
|
74
|
+
onPress?: IPressHandler;
|
|
75
|
+
onPressIn?: IPressHandler;
|
|
76
|
+
onPressOut?: IPressHandler;
|
|
77
|
+
onPressMove?: IPressHandler;
|
|
78
|
+
onLongPress?: IPressHandler;
|
|
79
|
+
delayLongPress: number;
|
|
80
|
+
unstable_pressDelay: number;
|
|
81
|
+
hitSlop?: IRectOffset;
|
|
82
|
+
pressRetentionOffset?: IRectOffset;
|
|
83
|
+
}
|
|
84
|
+
export interface IPressHandlers {
|
|
85
|
+
handlePressIn: IPressHandler;
|
|
86
|
+
handlePressOut: IPressHandler;
|
|
87
|
+
handlePress: IPressHandler;
|
|
88
|
+
handleResponderMove: IPressHandler;
|
|
89
|
+
}
|
|
90
|
+
export declare function createPressHandlers(config: IPressMachineConfig, runtime: IPressRuntime, host: IPressHost): IPressHandlers;
|