@symbiote-native/components 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -2
- package/build/accessibility-props.js +2 -1
- package/build/bootstrap/index.js +1 -1
- package/build/component-names/index.android.js +1 -1
- package/build/component-names/index.ios.js +1 -1
- package/build/component-names/shared.js +4 -1
- package/build/descriptor.d.ts +8 -0
- package/build/descriptor.js +27 -0
- package/build/index.d.ts +19 -11
- package/build/index.js +12 -5
- package/build/scroll-view-commands.js +5 -1
- package/build/state/pressable.js +13 -3
- package/build/state/sticky-header-reducer.d.ts +1 -0
- package/build/state/sticky-header-reducer.js +97 -5
- package/build/state/switch.js +1 -1
- package/build/state/text-input.d.ts +2 -1
- package/build/state/text-input.js +35 -8
- package/build/state/touchable.d.ts +30 -1
- package/build/state/touchable.js +94 -5
- package/build/state/virtualized-list-diagnostics.d.ts +31 -0
- package/build/state/virtualized-list-diagnostics.js +33 -0
- package/build/state/virtualized-list-reducer.d.ts +14 -0
- package/build/state/virtualized-list-reducer.js +179 -45
- package/build/state/virtualized-list.d.ts +7 -2
- package/build/state/virtualized-list.js +178 -37
- package/build/text-props.d.ts +9 -0
- package/build/text-props.js +24 -0
- package/build/view/render-activity-indicator.js +11 -3
- package/build/view/render-button.js +3 -4
- package/build/view/render-image/index.js +13 -3
- package/build/view/render-input-accessory-view.js +5 -2
- package/build/view/render-keyboard-avoiding-view.d.ts +14 -2
- package/build/view/render-keyboard-avoiding-view.js +52 -6
- package/build/view/render-modal.js +4 -2
- package/build/view/render-pressable/index.js +3 -1
- package/build/view/render-scroll-sticky.js +1 -1
- package/build/view/render-scroll-view.js +9 -3
- package/build/view/render-switch.js +4 -1
- package/build/view/render-touchable-highlight.d.ts +11 -1
- package/build/view/render-touchable-highlight.js +11 -10
- package/build/view/render-touchable-native-feedback.js +5 -1
- package/package.json +2 -2
- package/build/state/scroll-routing-handle/index.d.ts +0 -10
- package/build/state/scroll-routing-handle/index.js +0 -6
- package/build/view/layout-event/index.d.ts +0 -2
- package/build/view/layout-event/index.js +0 -12
package/README.md
CHANGED
|
@@ -51,14 +51,22 @@ types and wires the reducer/render pair from here. Calling the render function d
|
|
|
51
51
|
adapter itself does, to build its lifecycle wrapper:
|
|
52
52
|
|
|
53
53
|
```ts
|
|
54
|
-
import {
|
|
54
|
+
import {
|
|
55
|
+
renderSwitch,
|
|
56
|
+
createInitialSwitchState,
|
|
57
|
+
switchReducer,
|
|
58
|
+
} from '@symbiote-native/components';
|
|
55
59
|
|
|
56
60
|
// inside an adapter's own hook/composable:
|
|
57
61
|
const state = createInitialSwitchState();
|
|
58
62
|
const next = switchReducer(state, { type: 'native-reported', value: true });
|
|
59
63
|
const descriptor = renderSwitch(
|
|
60
64
|
{ value: true, disabled: false, passthrough: { onChange, ref } },
|
|
61
|
-
{
|
|
65
|
+
{
|
|
66
|
+
trackColorProps: (value, trackColor) => ({
|
|
67
|
+
/* platform-specific prop names */
|
|
68
|
+
}),
|
|
69
|
+
},
|
|
62
70
|
);
|
|
63
71
|
// descriptor is then handed to the adapter's own descriptorToReact / descriptorToVue bridge
|
|
64
72
|
```
|
|
@@ -95,7 +95,8 @@ export function resolveAccessibilityProps(props) {
|
|
|
95
95
|
'aria-valuenow': undefined,
|
|
96
96
|
'aria-valuetext': undefined,
|
|
97
97
|
};
|
|
98
|
-
if (ariaLabelledBy !== undefined &&
|
|
98
|
+
if (ariaLabelledBy !== undefined &&
|
|
99
|
+
next.accessibilityLabelledBy === undefined) {
|
|
99
100
|
next.accessibilityLabelledBy = ariaLabelledBy.split(/\s*,\s*/g);
|
|
100
101
|
}
|
|
101
102
|
if (ariaLabel !== undefined && next.accessibilityLabel === undefined) {
|
package/build/bootstrap/index.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// startup. Lives OUTSIDE the package's main barrel (see package.json's separate "./bootstrap"
|
|
5
5
|
// export): react-native's own source is Flow syntax Vitest's transform can't parse, so anything
|
|
6
6
|
// importing it directly must stay unreachable from the tested main index.ts.
|
|
7
|
-
import { processColor, DeviceEventEmitter, Image } from 'react-native';
|
|
7
|
+
import { processColor, DeviceEventEmitter, Image, } from 'react-native';
|
|
8
8
|
import { setColorProcessor, setDeviceEventSource, setImageSourceResolver, setNativeViewConfigSource, } from '@symbiote-native/engine';
|
|
9
9
|
// @ts-expect-error react-native ships no types for this internal path (plain .js) - the
|
|
10
10
|
// try/catch below is what actually proves the shape, not TS.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Each name is the ViewManager's REACT_CLASS in react-native/ReactAndroid/.../views/**.
|
|
3
3
|
// device-verify-pending: source-confirmed from RN's Android ViewManagers, proven on a
|
|
4
4
|
// real host by the absence of a "Can't find ViewManager '<name>'" red box.
|
|
5
|
-
import { buildDescriptors, makeDescriptorFor } from './shared.js';
|
|
5
|
+
import { buildDescriptors, makeDescriptorFor, } from './shared.js';
|
|
6
6
|
const ANDROID_NAMES = {
|
|
7
7
|
'symbiote-view': 'RCTView',
|
|
8
8
|
'symbiote-text': 'RCTText',
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// (component-names.ts re-exports it) for headless tsx / tsc / web fallback.
|
|
3
3
|
// Fabric names are the codegen spec's registered name (the new-arch name), not the legacy
|
|
4
4
|
// paperComponentName (RCTSwitch, …).
|
|
5
|
-
import { buildDescriptors, makeDescriptorFor } from './shared.js';
|
|
5
|
+
import { buildDescriptors, makeDescriptorFor, } from './shared.js';
|
|
6
6
|
const IOS_NAMES = {
|
|
7
7
|
'symbiote-view': 'RCTView',
|
|
8
8
|
'symbiote-text': 'RCTText',
|
|
@@ -13,7 +13,10 @@ const TEXT_INTRINSICS = new Set(['symbiote-text']);
|
|
|
13
13
|
export function buildDescriptors(names) {
|
|
14
14
|
const descriptors = {};
|
|
15
15
|
for (const [intrinsic, component] of Object.entries(names)) {
|
|
16
|
-
descriptors[intrinsic] = {
|
|
16
|
+
descriptors[intrinsic] = {
|
|
17
|
+
component,
|
|
18
|
+
isText: TEXT_INTRINSICS.has(intrinsic),
|
|
19
|
+
};
|
|
17
20
|
}
|
|
18
21
|
return descriptors;
|
|
19
22
|
}
|
package/build/descriptor.d.ts
CHANGED
|
@@ -9,3 +9,11 @@ export type IDescriptor = {
|
|
|
9
9
|
};
|
|
10
10
|
export declare function el(type: IDescriptorType, props?: IDescriptorProps, children?: IDescriptorChild[], key?: string): IDescriptor;
|
|
11
11
|
export declare function txt(props?: IDescriptorProps, children?: IDescriptorChild[]): IDescriptor;
|
|
12
|
+
export type IDescriptorShapeGuard = {
|
|
13
|
+
error(detail: string): Error;
|
|
14
|
+
assertType(expected: IDescriptorType, actual: IDescriptorType): void;
|
|
15
|
+
assertChildCount(expected: number, actual: number): void;
|
|
16
|
+
asText(child: IDescriptorChild): string;
|
|
17
|
+
asElement(child: IDescriptorChild): IDescriptor;
|
|
18
|
+
};
|
|
19
|
+
export declare function createDescriptorShapeGuard(bridge: string): IDescriptorShapeGuard;
|
package/build/descriptor.js
CHANGED
|
@@ -13,3 +13,30 @@ export function el(type, props = {}, children = [], key) {
|
|
|
13
13
|
export function txt(props = {}, children = []) {
|
|
14
14
|
return { type: 'symbiote-text', props, children };
|
|
15
15
|
}
|
|
16
|
+
// `bridge` names the caller (`descriptorToSolid`, `descriptorToSvelte`) so a device log says which
|
|
17
|
+
// one tripped without a stack to read.
|
|
18
|
+
export function createDescriptorShapeGuard(bridge) {
|
|
19
|
+
const error = (detail) => new Error(`${bridge}: Descriptor shape changed between renders (${detail}) — a render-*.ts fn must ` +
|
|
20
|
+
`produce a CONSTANT tree shape; only prop values may vary between calls.`);
|
|
21
|
+
return {
|
|
22
|
+
error,
|
|
23
|
+
assertType(expected, actual) {
|
|
24
|
+
if (expected !== actual)
|
|
25
|
+
throw error(`${expected} -> ${actual}`);
|
|
26
|
+
},
|
|
27
|
+
assertChildCount(expected, actual) {
|
|
28
|
+
if (expected !== actual)
|
|
29
|
+
throw error(`child count ${expected} -> ${actual}`);
|
|
30
|
+
},
|
|
31
|
+
asText(child) {
|
|
32
|
+
if (typeof child !== 'string')
|
|
33
|
+
throw error(`text -> ${child.type}`);
|
|
34
|
+
return child;
|
|
35
|
+
},
|
|
36
|
+
asElement(child) {
|
|
37
|
+
if (typeof child === 'string')
|
|
38
|
+
throw error('element -> text');
|
|
39
|
+
return child;
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
package/build/index.d.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
export { el, txt } from './descriptor';
|
|
2
2
|
export type { IDescriptor, IDescriptorType, IDescriptorProps, IDescriptorChild, } from './descriptor';
|
|
3
|
+
export { createDescriptorShapeGuard } from './descriptor';
|
|
4
|
+
export type { IDescriptorShapeGuard } from './descriptor';
|
|
3
5
|
export { resolveAccessibilityProps } from './accessibility-props';
|
|
6
|
+
export { resolveTextProps } from './text-props';
|
|
7
|
+
export type { IEllipsizeMode, ITextDefaultableProps } from './text-props';
|
|
4
8
|
export type { IAccessibilityProps, IAriaProps, IAccessibilityRole, IRole, IAccessibilityStateValue, IAccessibilityValue, IAccessibilityActionInfo, } from './accessibility-props';
|
|
5
9
|
export { descriptorFor, COMPONENT_DESCRIPTORS } from './component-names';
|
|
6
10
|
export { buildDescriptors, makeDescriptorFor } from './component-names/shared';
|
|
7
|
-
export type { ISymbioteIntrinsic, IComponentDescriptor } from './component-names/shared';
|
|
11
|
+
export type { ISymbioteIntrinsic, IComponentDescriptor, } from './component-names/shared';
|
|
8
12
|
export { renderActivityIndicator } from './view/render-activity-indicator';
|
|
9
13
|
export type { IActivityIndicatorProps, IActivityIndicatorViewProps, IActivityIndicatorPlatform, IActivityIndicatorSize, } from './view/render-activity-indicator';
|
|
10
14
|
export { renderSwitch } from './view/render-switch';
|
|
@@ -17,12 +21,12 @@ export type { IScrollIntrinsics, IContentSize, IScrollForwarding, IScrollForward
|
|
|
17
21
|
export { buildScrollViewHandle, splitLayoutProps, attachStickyScroll, isSymbioteEvent, forwardScrollEvent, } from './scroll-view-commands';
|
|
18
22
|
export type { IScrollViewHandle } from './scroll-view-commands';
|
|
19
23
|
export { computeStickyInterpolation, nextStickyHeaderY, stickyDebounceMs, readLayoutNumber, STICKY_HEADER_Z_INDEX, } from './view/render-scroll-sticky';
|
|
20
|
-
export type { IStickyHeaderProps, IStickyInterpolationParams } from './view/render-scroll-sticky';
|
|
24
|
+
export type { IStickyHeaderProps, IStickyInterpolationParams, } from './view/render-scroll-sticky';
|
|
21
25
|
export { reduceSticky, createInitialStickyState, stickyEffectSignature, } from './state/sticky-header-reducer';
|
|
22
26
|
export type { IStickyHeaderState, IStickyReducerInputs, IStickyAction, IStickyEffect, IStickyReduceResult, } from './state/sticky-header-reducer';
|
|
23
27
|
export type { IImageSource, IImageSourceProp, IResizeMode, IImageProps, IImageViewProps, } from './view/render-image';
|
|
24
28
|
export { renderImage } from './view/render-image';
|
|
25
|
-
export type { IImageStatics, IImageSize, IImageCacheStatus } from '@symbiote-native/engine';
|
|
29
|
+
export type { IImageStatics, IImageSize, IImageCacheStatus, } from '@symbiote-native/engine';
|
|
26
30
|
export { imageStatics, setImageSourceResolver } from '@symbiote-native/engine';
|
|
27
31
|
export { renderImageBackground } from './view/render-image-background';
|
|
28
32
|
export type { IImageBackgroundViewProps } from './view/render-image-background';
|
|
@@ -30,29 +34,33 @@ export { renderInputAccessoryView } from './view/render-input-accessory-view';
|
|
|
30
34
|
export type { IInputAccessoryViewViewProps } from './view/render-input-accessory-view';
|
|
31
35
|
export { renderModal } from './view/render-modal';
|
|
32
36
|
export type { IModalViewProps, IModalAnimationType, IModalPresentationStyle, IModalOrientation, IModalOrientationChangeEvent, } from './view/render-modal';
|
|
33
|
-
export { modalReducer, createInitialModalState, shouldRenderModal } from './state/modal';
|
|
37
|
+
export { modalReducer, createInitialModalState, shouldRenderModal, } from './state/modal';
|
|
34
38
|
export type { IModalState, IModalAction } from './state/modal';
|
|
35
|
-
export { computeInset, readKeyboardFrame, readLayoutFrame, resolveKeyboardAvoidingLayout, DEFAULT_VERTICAL_OFFSET, } from './view/render-keyboard-avoiding-view';
|
|
36
|
-
export type { IKeyboardAvoidingBehavior, IMeasuredFrame, IKeyboardFrame, IKeyboardAvoidingLayout, IResolveKeyboardAvoidingLayoutParams, } from './view/render-keyboard-avoiding-view';
|
|
39
|
+
export { computeInset, keyboardAvoidingEventNamesFor, readPrefersCrossFadeTransitions, readKeyboardFrame, readLayoutFrame, resolveKeyboardAvoidingLayout, DEFAULT_VERTICAL_OFFSET, } from './view/render-keyboard-avoiding-view';
|
|
40
|
+
export type { IComputeInsetOptions, IKeyboardAvoidingBehavior, IKeyboardAvoidingEventNames, IMeasuredFrame, IKeyboardFrame, IKeyboardAvoidingLayout, IResolveKeyboardAvoidingLayoutParams, } from './view/render-keyboard-avoiding-view';
|
|
37
41
|
export { createPressHandlers, createPressRuntime, normalizeRect, maxEdge, isTouchWithinRegion, readPoint, computeRegion, rippleProps, DEFAULT_DELAY_LONG_PRESS_MS, DEFAULT_PRESS_RECT_OFFSETS, } from './state/pressable';
|
|
38
42
|
export type { IPressState, IPressHandler, IPressHandlers, IPressHost, IPressRuntime, IPressMachineConfig, IFrameCallback, IRectOffset, IEdgeInsets, IResponderRegion, IPressableAndroidRippleConfig, IRippleBackground as IPressableRippleBackground, } from './state/pressable';
|
|
39
43
|
export { buildPressableListeners, resolveDisabledAccessibilityState, noteHoverNoop, shouldSuppressPress, shouldClaimResponder, isTerminationAllowed, } from './view/render-pressable';
|
|
40
|
-
export { computePressOutWait, createTouchableFeedbackRuntime, createTouchableFeedbackHandlers, DEFAULT_ACTIVE_OPACITY, OPACITY_ACTIVE_DURATION_MS, OPACITY_INACTIVE_DURATION_MS, RESTING_OPACITY, DEFAULT_HIGHLIGHT_CHILD_OPACITY, DEFAULT_UNDERLAY_COLOR,
|
|
41
|
-
export type { IPressTimingProps, ITouchableHandler, ITouchableFeedbackRuntime, ITouchableFeedbackConfig, ITouchableFeedbackCallbacks, ITouchableFeedbackHandlers, } from './state/touchable';
|
|
42
|
-
export {
|
|
44
|
+
export { computePressOutWait, createTouchableFeedbackRuntime, createTouchableFeedbackHandlers, DEFAULT_ACTIVE_OPACITY, OPACITY_ACTIVE_GRANT_DURATION_MS, OPACITY_ACTIVE_DURATION_MS, OPACITY_INACTIVE_DURATION_MS, RESTING_OPACITY, DEFAULT_HIGHLIGHT_CHILD_OPACITY, DEFAULT_UNDERLAY_COLOR, TOUCHABLE_MIN_PRESS_DURATION_MS, restingOpacityFromStyle, hasTouchablePressHandler, createHighlightUnderlayRuntime, createHighlightUnderlayHandlers, } from './state/touchable';
|
|
45
|
+
export type { IPressTimingProps, ITouchableHandler, ITouchableFeedbackRuntime, ITouchableFeedbackConfig, ITouchableFeedbackCallbacks, ITouchableFeedbackHandlers, ITouchablePressHandlerProps, IHighlightUnderlayRuntime, IHighlightUnderlayConfig, IHighlightUnderlayCallbacks, IHighlightUnderlayHandlers, } from './state/touchable';
|
|
46
|
+
export { resolveHighlightExtraStyles } from './view/render-touchable-highlight';
|
|
47
|
+
export type { ITouchableHighlightExtraStyles, ITouchableHighlightUnderlayView, } from './view/render-touchable-highlight';
|
|
43
48
|
export { backgroundProps, canUseNativeForeground, selectableBackground, selectableBackgroundBorderless, rippleBackground, } from './view/render-touchable-native-feedback';
|
|
44
49
|
export type { INativeFeedbackBackground, IThemeAttrBackground, IRippleBackground, } from './view/render-touchable-native-feedback';
|
|
45
50
|
export { BUTTON_ACCESSIBILITY_ROLE, buttonTextStyle, resolveButtonTextStyle, } from './view/render-button';
|
|
46
51
|
export type { IButtonProps } from './view/render-button';
|
|
47
52
|
export { resolveTextInputProps, foldText, foldAutoComplete, foldSubmitBehavior, mapAutoComplete, textFromChange, eventCountFromChange, shouldCommandText, INITIAL_EVENT_COUNT, SELECTION_NONE, } from './state/text-input';
|
|
48
53
|
export type { ITextInputProps, ITextInputHandle, ITextInputSelection, ITextInputEventHandler, ITextInputFoldInput, IFoldedTextInputProps, IInputMode, IEnterKeyHint, ISubmitBehavior, } from './state/text-input';
|
|
54
|
+
export { keyboardTypeForInputMode } from './state/text-input';
|
|
49
55
|
export { renderTextInput } from './view/render-text-input';
|
|
50
56
|
export type { ITextInputViewProps } from './view/render-text-input';
|
|
51
|
-
export { DEFAULT_WINDOW_SIZE, DEFAULT_INITIAL_NUM_TO_RENDER, DEFAULT_END_REACHED_THRESHOLD, DEFAULT_MAX_TO_RENDER_PER_BATCH, DEFAULT_UPDATE_CELLS_BATCHING_PERIOD, DEFAULT_VIEW_AREA_COVERAGE_PERCENT_THRESHOLD, DEFAULT_START_REACHED_THRESHOLD, FIRST_INDEX, EMPTY_OFFSET, NO_INDEX, NO_CONTENT_LENGTH_SENT, INVERTED_Y_STYLE, INVERTED_X_STYLE, readScrollOffset, readLayoutLength, buildOffsets, computeWindow, throttleWindow, visiblePercent, isCellViewable, offsetForIndex, averageMeasuredLength, highestMeasuredIndex, computeEndReached, computeStartReached, buildViewabilityPairs, computeViewableSet, diffViewable, maxMinimumViewTime, buildListPlan, computeMvcpAdjustment, resolveItemKey, indexOfItem, offsetForEnd, isSeparatorGapInRange, decideEdgeReached, resolveStickySectionHeaders, wrapFixedLayout, resolveAverageLength, } from './state/virtualized-list';
|
|
57
|
+
export { DEFAULT_WINDOW_SIZE, DEFAULT_INITIAL_NUM_TO_RENDER, DEFAULT_END_REACHED_THRESHOLD, DEFAULT_MAX_TO_RENDER_PER_BATCH, DEFAULT_UPDATE_CELLS_BATCHING_PERIOD, DEFAULT_VIEW_AREA_COVERAGE_PERCENT_THRESHOLD, DEFAULT_START_REACHED_THRESHOLD, FIRST_INDEX, EMPTY_OFFSET, NO_INDEX, NO_CONTENT_LENGTH_SENT, INVERTED_Y_STYLE, INVERTED_X_STYLE, readScrollOffset, readLayoutLength, readLayoutOffset, buildOffsets, computeWindow, throttleWindow, visiblePercent, isCellViewable, offsetForIndex, averageMeasuredLength, averageMeasuredStride, highestMeasuredIndex, computeEndReached, computeStartReached, buildViewabilityPairs, computeViewableSet, diffViewable, maxMinimumViewTime, buildListPlan, computeMvcpAdjustment, resolveItemKey, indexOfItem, offsetForEnd, isSeparatorGapInRange, decideEdgeReached, resolveStickySectionHeaders, wrapFixedLayout, resolveAverageLength, } from './state/virtualized-list';
|
|
52
58
|
export type { IMvcpAction, IMvcpAdjustmentParams, IMvcpAdjustmentResult, ICellLayout, ISeparators, ISeparatorProps, IViewToken, IViewableItemsChangedInfo, IViewabilityConfig, IViewabilityConfigCallbackPair, IVirtualizedListHandle, IViewableSetParams, IListCellPlan, IListPlan, IListPlanParams, } from './state/virtualized-list';
|
|
53
59
|
export { reduceList, createInitialListState, listEffectSignature, } from './state/virtualized-list-reducer';
|
|
54
60
|
export type { IListState, IListMetrics, IListReducerInputs, IListAction, IListEffect, IListReduceResult, } from './state/virtualized-list-reducer';
|
|
61
|
+
export { subscribeListDiagnostics } from './state/virtualized-list-diagnostics';
|
|
62
|
+
export type { IListDiagnosticFrame, IListDiagnosticMove, } from './state/virtualized-list-diagnostics';
|
|
55
63
|
export { SINGLE_COLUMN, chunkIntoRows, rowKeyExtractor, expandRowToken, expandRowViewability, lastItemOfRow, firstItemOfRow, } from './state/flat-list';
|
|
56
64
|
export type { IRow } from './state/flat-list';
|
|
57
65
|
export { flattenSections, unwrapEntryItem, sectionEntryKey, scrollLocationToFlatIndex, } from './state/section-list';
|
|
58
|
-
export type { ISection, ISectionEntry, IVirtualizedSectionListHandle } from './state/section-list';
|
|
66
|
+
export type { ISection, ISectionEntry, IVirtualizedSectionListHandle, } from './state/section-list';
|
package/build/index.js
CHANGED
|
@@ -3,10 +3,15 @@
|
|
|
3
3
|
// wires state→render with ITS lifecycle (React hooks / Vue reactivity) and maps the
|
|
4
4
|
// Descriptor onto its own element.
|
|
5
5
|
export { el, txt } from './descriptor.js';
|
|
6
|
+
// The shape-stability guard a fine-grained adapter's Descriptor bridge builds on. Internal to
|
|
7
|
+
// adapter bridges (Svelte's and Solid's), NOT part of an app's surface — so it stays out of the
|
|
8
|
+
// adapter barrels, unlike the passthrough names in tests/adapter-barrel-parity.test.ts.
|
|
9
|
+
export { createDescriptorShapeGuard } from './descriptor.js';
|
|
6
10
|
// Accessibility folding: the web-alias (aria-*/role) → canonical accessibility* transform
|
|
7
11
|
// and its types. Framework-agnostic, so React, Vue, and the next adapter all fold
|
|
8
12
|
// identically; moved here from @symbiote-native/react. RefreshControl/SafeAreaView/ScrollView consume it.
|
|
9
13
|
export { resolveAccessibilityProps } from './accessibility-props.js';
|
|
14
|
+
export { resolveTextProps } from './text-props.js';
|
|
10
15
|
// Intrinsic (`symbiote-*`) → Fabric view-name resolution. Shared by every adapter so the
|
|
11
16
|
// names CANNOT drift between them (one engine, one Fabric). The name tables are
|
|
12
17
|
// Metro-split (.ios/.android, filename selects, no Platform.OS read); the base
|
|
@@ -37,9 +42,9 @@ export { renderImageBackground } from './view/render-image-background.js';
|
|
|
37
42
|
export { renderInputAccessoryView } from './view/render-input-accessory-view.js';
|
|
38
43
|
// Modal: full 3-layer split (state machine gates the iOS keep-alive frame).
|
|
39
44
|
export { renderModal } from './view/render-modal.js';
|
|
40
|
-
export { modalReducer, createInitialModalState, shouldRenderModal } from './state/modal.js';
|
|
45
|
+
export { modalReducer, createInitialModalState, shouldRenderModal, } from './state/modal.js';
|
|
41
46
|
// KeyboardAvoidingView: pure inset/behavior math; the adapter owns the Keyboard subscription.
|
|
42
|
-
export { computeInset, readKeyboardFrame, readLayoutFrame, resolveKeyboardAvoidingLayout, DEFAULT_VERTICAL_OFFSET, } from './view/render-keyboard-avoiding-view.js';
|
|
47
|
+
export { computeInset, keyboardAvoidingEventNamesFor, readPrefersCrossFadeTransitions, readKeyboardFrame, readLayoutFrame, resolveKeyboardAvoidingLayout, DEFAULT_VERTICAL_OFFSET, } from './view/render-keyboard-avoiding-view.js';
|
|
43
48
|
// Pressable: the press state machine (logic) + render decisions. The pure lifecycle (timers,
|
|
44
49
|
// geometry, drift test, suppression flags) lives in state/pressable; the responder-listener +
|
|
45
50
|
// accessibilityState + ripple render decisions in view/render-pressable. Every adapter supplies
|
|
@@ -48,8 +53,8 @@ export { createPressHandlers, createPressRuntime, normalizeRect, maxEdge, isTouc
|
|
|
48
53
|
export { buildPressableListeners, resolveDisabledAccessibilityState, noteHoverNoop, shouldSuppressPress, shouldClaimResponder, isTerminationAllowed, } from './view/render-pressable/index.js';
|
|
49
54
|
// Touchable*: shared press-timing constants + the deactivation-floor math (the Animated feedback
|
|
50
55
|
// itself stays per-adapter), and TouchableNativeFeedback's pure static factories + background map.
|
|
51
|
-
export { computePressOutWait, createTouchableFeedbackRuntime, createTouchableFeedbackHandlers, DEFAULT_ACTIVE_OPACITY, OPACITY_ACTIVE_DURATION_MS, OPACITY_INACTIVE_DURATION_MS, RESTING_OPACITY, DEFAULT_HIGHLIGHT_CHILD_OPACITY, DEFAULT_UNDERLAY_COLOR,
|
|
52
|
-
export {
|
|
56
|
+
export { computePressOutWait, createTouchableFeedbackRuntime, createTouchableFeedbackHandlers, DEFAULT_ACTIVE_OPACITY, OPACITY_ACTIVE_GRANT_DURATION_MS, OPACITY_ACTIVE_DURATION_MS, OPACITY_INACTIVE_DURATION_MS, RESTING_OPACITY, DEFAULT_HIGHLIGHT_CHILD_OPACITY, DEFAULT_UNDERLAY_COLOR, TOUCHABLE_MIN_PRESS_DURATION_MS, restingOpacityFromStyle, hasTouchablePressHandler, createHighlightUnderlayRuntime, createHighlightUnderlayHandlers, } from './state/touchable.js';
|
|
57
|
+
export { resolveHighlightExtraStyles } from './view/render-touchable-highlight.js';
|
|
53
58
|
export { backgroundProps, canUseNativeForeground, selectableBackground, selectableBackgroundBorderless, rippleBackground, } from './view/render-touchable-native-feedback.js';
|
|
54
59
|
// Button: shared base text style, role constant, and color fold (the adapter composes its own
|
|
55
60
|
// TouchableOpacity + Text).
|
|
@@ -60,15 +65,17 @@ export { BUTTON_ACCESSIBILITY_ROLE, buttonTextStyle, resolveButtonTextStyle, } f
|
|
|
60
65
|
// resolved native props. Both shared verbatim across React and Vue; the adapter owns only the
|
|
61
66
|
// hooks/reactivity + the imperative handle.
|
|
62
67
|
export { resolveTextInputProps, foldText, foldAutoComplete, foldSubmitBehavior, mapAutoComplete, textFromChange, eventCountFromChange, shouldCommandText, INITIAL_EVENT_COUNT, SELECTION_NONE, } from './state/text-input.js';
|
|
68
|
+
export { keyboardTypeForInputMode } from './state/text-input.js';
|
|
63
69
|
export { renderTextInput } from './view/render-text-input.js';
|
|
64
70
|
// VirtualizedList family: the framework-agnostic windowing engine + data shapes. Lists
|
|
65
71
|
// have NO view/render-*.ts (the cell content is the framework's own children, so there is
|
|
66
72
|
// no Descriptor render fn) — the shared layer for lists is this STATE/logic, reused
|
|
67
73
|
// verbatim by React and Vue.
|
|
68
|
-
export { DEFAULT_WINDOW_SIZE, DEFAULT_INITIAL_NUM_TO_RENDER, DEFAULT_END_REACHED_THRESHOLD, DEFAULT_MAX_TO_RENDER_PER_BATCH, DEFAULT_UPDATE_CELLS_BATCHING_PERIOD, DEFAULT_VIEW_AREA_COVERAGE_PERCENT_THRESHOLD, DEFAULT_START_REACHED_THRESHOLD, FIRST_INDEX, EMPTY_OFFSET, NO_INDEX, NO_CONTENT_LENGTH_SENT, INVERTED_Y_STYLE, INVERTED_X_STYLE, readScrollOffset, readLayoutLength, buildOffsets, computeWindow, throttleWindow, visiblePercent, isCellViewable, offsetForIndex, averageMeasuredLength, highestMeasuredIndex, computeEndReached, computeStartReached, buildViewabilityPairs, computeViewableSet, diffViewable, maxMinimumViewTime, buildListPlan, computeMvcpAdjustment, resolveItemKey, indexOfItem, offsetForEnd, isSeparatorGapInRange, decideEdgeReached, resolveStickySectionHeaders, wrapFixedLayout, resolveAverageLength, } from './state/virtualized-list.js';
|
|
74
|
+
export { DEFAULT_WINDOW_SIZE, DEFAULT_INITIAL_NUM_TO_RENDER, DEFAULT_END_REACHED_THRESHOLD, DEFAULT_MAX_TO_RENDER_PER_BATCH, DEFAULT_UPDATE_CELLS_BATCHING_PERIOD, DEFAULT_VIEW_AREA_COVERAGE_PERCENT_THRESHOLD, DEFAULT_START_REACHED_THRESHOLD, FIRST_INDEX, EMPTY_OFFSET, NO_INDEX, NO_CONTENT_LENGTH_SENT, INVERTED_Y_STYLE, INVERTED_X_STYLE, readScrollOffset, readLayoutLength, readLayoutOffset, buildOffsets, computeWindow, throttleWindow, visiblePercent, isCellViewable, offsetForIndex, averageMeasuredLength, averageMeasuredStride, highestMeasuredIndex, computeEndReached, computeStartReached, buildViewabilityPairs, computeViewableSet, diffViewable, maxMinimumViewTime, buildListPlan, computeMvcpAdjustment, resolveItemKey, indexOfItem, offsetForEnd, isSeparatorGapInRange, decideEdgeReached, resolveStickySectionHeaders, wrapFixedLayout, resolveAverageLength, } from './state/virtualized-list.js';
|
|
69
75
|
// The orchestration reducer: the one pure state machine every list adapter drives, folding the
|
|
70
76
|
// after-commit effect skeleton (window recompute -> edge -> viewability -> initial-scroll -> MVCP)
|
|
71
77
|
// out of each adapter's reactive glue. The adapter maps events to actions and runs the effects.
|
|
72
78
|
export { reduceList, createInitialListState, listEffectSignature, } from './state/virtualized-list-reducer.js';
|
|
79
|
+
export { subscribeListDiagnostics } from './state/virtualized-list-diagnostics.js';
|
|
73
80
|
export { SINGLE_COLUMN, chunkIntoRows, rowKeyExtractor, expandRowToken, expandRowViewability, lastItemOfRow, firstItemOfRow, } from './state/flat-list.js';
|
|
74
81
|
export { flattenSections, unwrapEntryItem, sectionEntryKey, scrollLocationToFlatIndex, } from './state/section-list.js';
|
|
@@ -116,5 +116,9 @@ export function attachStickyScroll(node, value) {
|
|
|
116
116
|
const attachment = attachNativeEvent(node, 'onScroll', [
|
|
117
117
|
{ nativeEvent: { contentOffset: { y: value } } },
|
|
118
118
|
]);
|
|
119
|
-
|
|
119
|
+
dlog(`STICKY[attach] attachStickyScroll onScroll -> value#${value.__getNativeTag?.() ?? 'js'}`);
|
|
120
|
+
return () => {
|
|
121
|
+
dlog('STICKY[attach] attachStickyScroll detached');
|
|
122
|
+
attachment.detach();
|
|
123
|
+
};
|
|
120
124
|
}
|
package/build/state/pressable.js
CHANGED
|
@@ -13,7 +13,12 @@ import { dlog, Platform } from '@symbiote-native/engine';
|
|
|
13
13
|
export const DEFAULT_DELAY_LONG_PRESS_MS = 500;
|
|
14
14
|
// RN's default extra slop kept around a press once it is active, before a drift fires pressOut.
|
|
15
15
|
// The PressRect extension (Pressability.js DEFAULT_PRESS_RECT_OFFSETS). Per-edge, deeper bottom.
|
|
16
|
-
export const DEFAULT_PRESS_RECT_OFFSETS = {
|
|
16
|
+
export const DEFAULT_PRESS_RECT_OFFSETS = {
|
|
17
|
+
top: 20,
|
|
18
|
+
left: 20,
|
|
19
|
+
bottom: 30,
|
|
20
|
+
right: 20,
|
|
21
|
+
};
|
|
17
22
|
// ---- geometry (pure) --------------------------------------------------------------------------
|
|
18
23
|
// A rect offset is a per-edge object (not the scalar shorthand). Reads the asymmetric form
|
|
19
24
|
// without a cast.
|
|
@@ -58,7 +63,12 @@ export function readPoint(event) {
|
|
|
58
63
|
export function computeRegion(width, height, pageX, pageY) {
|
|
59
64
|
if (!width && !height && !pageX && !pageY)
|
|
60
65
|
return undefined;
|
|
61
|
-
return {
|
|
66
|
+
return {
|
|
67
|
+
left: pageX,
|
|
68
|
+
top: pageY,
|
|
69
|
+
right: pageX + width,
|
|
70
|
+
bottom: pageY + height,
|
|
71
|
+
};
|
|
62
72
|
}
|
|
63
73
|
// Build the Android native-feedback prop the inner View carries from the ripple config. RN runs
|
|
64
74
|
// the color through processColor → a native int; we have no native bridge in JS, so we keep the
|
|
@@ -132,7 +142,7 @@ export function createPressHandlers(config, runtime, host) {
|
|
|
132
142
|
const origin = runtime.pressOrigin;
|
|
133
143
|
if (origin === undefined)
|
|
134
144
|
return true;
|
|
135
|
-
return Math.hypot(point.x - origin.x, point.y - origin.y) <= fallbackThreshold;
|
|
145
|
+
return (Math.hypot(point.x - origin.x, point.y - origin.y) <= fallbackThreshold);
|
|
136
146
|
}
|
|
137
147
|
function clearLongPress() {
|
|
138
148
|
if (runtime.longPressCancel !== undefined) {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// ./view/render-scroll-sticky; this module composes them into the ordered transition every adapter
|
|
16
16
|
// shares. Ported from ScrollViewStickyHeader.js's effect.
|
|
17
17
|
import { dlog } from '@symbiote-native/engine';
|
|
18
|
-
import { computeStickyInterpolation, stickyDebounceMs } from '../view/render-scroll-sticky.js';
|
|
18
|
+
import { computeStickyInterpolation, stickyDebounceMs, } from '../view/render-scroll-sticky.js';
|
|
19
19
|
// The un-measured identity interpolation (RN: a fresh AnimatedInterpolation before the header has
|
|
20
20
|
// measured its own y/height). Kept as the reset the initial state and every rebuild start from.
|
|
21
21
|
const IDENTITY_INPUT_RANGE = [-1, 0];
|
|
@@ -30,6 +30,7 @@ export function createInitialStickyState() {
|
|
|
30
30
|
haveReceivedInitialZeroTranslateY: true,
|
|
31
31
|
inputRange: [...IDENTITY_INPUT_RANGE],
|
|
32
32
|
outputRange: [...IDENTITY_OUTPUT_RANGE],
|
|
33
|
+
rangesEmitted: false,
|
|
33
34
|
};
|
|
34
35
|
}
|
|
35
36
|
// A cheap signature over the render-relevant state (the ranges + the committed translateY). The
|
|
@@ -37,6 +38,11 @@ export function createInitialStickyState() {
|
|
|
37
38
|
export function stickyEffectSignature(state) {
|
|
38
39
|
return `${state.inputRange.join(',')}|${state.outputRange.join(',')}|${state.translateY}`;
|
|
39
40
|
}
|
|
41
|
+
function arraysEqual(a, b) {
|
|
42
|
+
if (a.length !== b.length)
|
|
43
|
+
return false;
|
|
44
|
+
return a.every((value, index) => value === b[index]);
|
|
45
|
+
}
|
|
40
46
|
// Recompute the derived interpolation ranges off the current state + inputs (wrapping the load-bearing
|
|
41
47
|
// computeStickyInterpolation math), store them, and return them for the rebuild effect.
|
|
42
48
|
function deriveRanges(state, inputs) {
|
|
@@ -54,27 +60,95 @@ function deriveRanges(state, inputs) {
|
|
|
54
60
|
}
|
|
55
61
|
// The single transition every sticky-header adapter shares. The adapter maps a native event / animated
|
|
56
62
|
// tick / timer fire to an action, calls this, stores the returned state, and executes the effects.
|
|
63
|
+
// DIAGNOSTIC-only, gated: identifies which header instance a log line belongs to across a
|
|
64
|
+
// device dump without needing full state dumps at every call — layoutY doubles as a stable
|
|
65
|
+
// per-header id once measured (0 before the first layout).
|
|
66
|
+
function headerTag(state) {
|
|
67
|
+
return `y=${state.layoutY}`;
|
|
68
|
+
}
|
|
57
69
|
export function reduceSticky(state, action, inputs) {
|
|
70
|
+
dlog(`STICKY[reducer ${headerTag(state)}] action=${action.kind}` +
|
|
71
|
+
(action.kind === 'layout'
|
|
72
|
+
? ` y=${action.y} height=${action.height}`
|
|
73
|
+
: '') +
|
|
74
|
+
(action.kind === 'animated-tick' || action.kind === 'debounce-fired'
|
|
75
|
+
? ` value=${action.value}`
|
|
76
|
+
: '') +
|
|
77
|
+
` inputs={inverted=${inputs.inverted} scrollViewHeight=${inputs.scrollViewHeight} nextHeaderLayoutY=${inputs.nextHeaderLayoutY}}`);
|
|
58
78
|
switch (action.kind) {
|
|
59
79
|
case 'layout': {
|
|
60
80
|
// Record own y/height, mark measured, rebuild the interpolation, and (when the reducer owns the
|
|
61
81
|
// cross-talk index) hand the parent this header's y so the PREVIOUS header learns its collision
|
|
62
82
|
// point. Matches RN ScrollViewStickyHeader.js._onLayout.
|
|
83
|
+
//
|
|
84
|
+
// Redundant-geometry guard: Yoga legitimately re-fires onLayout with the SAME y/height (relayout
|
|
85
|
+
// passes triggered by an unrelated sibling, a native-driven prop commit, ...) — every consumer
|
|
86
|
+
// must tolerate that. React's port gets this guard for FREE: `setLayoutY(sameValue)` is a no-op
|
|
87
|
+
// (React bails out of re-rendering on an unchanged primitive), so a redundant onLayout never
|
|
88
|
+
// reaches the rebuild `useEffect`. This reducer has no such implicit bail-out, so it must skip
|
|
89
|
+
// the rebuild explicitly — device-confirmed (2026-08-13) that omitting this guard lets a
|
|
90
|
+
// native-driven rebuild (fresh AnimatedProps/AnimatedStyle graph, fresh native connect) commit a
|
|
91
|
+
// fresh prop identity on every redundant layout, which can itself provoke another relayout pass —
|
|
92
|
+
// an unbounded same-tick rebuild ping-pong that trips Svelte's effect_update_depth_exceeded guard.
|
|
93
|
+
const alreadyAtThisGeometry = state.measured &&
|
|
94
|
+
state.layoutY === action.y &&
|
|
95
|
+
state.layoutHeight === action.height;
|
|
63
96
|
state.layoutY = action.y;
|
|
64
97
|
state.layoutHeight = action.height;
|
|
65
98
|
state.measured = true;
|
|
66
|
-
const { inputRange, outputRange } = deriveRanges(state, inputs);
|
|
67
99
|
const effects = [];
|
|
68
100
|
if (inputs.index !== undefined) {
|
|
69
|
-
effects.push({
|
|
101
|
+
effects.push({
|
|
102
|
+
kind: 'record-header-y',
|
|
103
|
+
index: inputs.index,
|
|
104
|
+
y: action.y,
|
|
105
|
+
});
|
|
70
106
|
}
|
|
107
|
+
if (alreadyAtThisGeometry && state.rangesEmitted) {
|
|
108
|
+
dlog(`STICKY[reducer ${headerTag(state)}] layout: redundant geometry, skipped rebuild`);
|
|
109
|
+
return { state, effects, changed: effects.length > 0 };
|
|
110
|
+
}
|
|
111
|
+
const { inputRange, outputRange } = deriveRanges(state, inputs);
|
|
112
|
+
state.rangesEmitted = true;
|
|
113
|
+
dlog(`STICKY[reducer ${headerTag(state)}] layout: measured=true inputRange=${JSON.stringify(inputRange)} ` +
|
|
114
|
+
`outputRange=${JSON.stringify(outputRange)}`);
|
|
71
115
|
effects.push({ kind: 'rebuild-interpolation', inputRange, outputRange });
|
|
72
116
|
return { state, effects, changed: true };
|
|
73
117
|
}
|
|
74
118
|
case 'inputs-changed': {
|
|
75
119
|
// A collision/viewport input changed (RN effect deps: inverted, scrollViewHeight,
|
|
76
120
|
// nextHeaderLayoutY): recompute the ranges and rebuild.
|
|
121
|
+
//
|
|
122
|
+
// Redundant-ranges guard (sibling of 'layout's `alreadyAtThisGeometry` above, same root
|
|
123
|
+
// cause): the adapter's own mount `$effect` re-dispatches 'inputs-changed' whenever its
|
|
124
|
+
// `inverted`/`scrollViewHeight`/`nextHeaderLayoutY` derived values re-evaluate — which can
|
|
125
|
+
// happen on an unrelated parent re-render (e.g. VirtualizedList re-deriving
|
|
126
|
+
// `nextHeaderLayoutYFor(cell.index)` off its cross-talk Map on every reactive pass) even when
|
|
127
|
+
// the COMPUTED value is identical. React gets no implicit protection here either (this is a
|
|
128
|
+
// real `useEffect` with real deps), but RN's own deps array only fires on an ACTUAL primitive
|
|
129
|
+
// change; a framework-agnostic caller re-dispatching on every derive needs the reducer itself
|
|
130
|
+
// to compare the RESULT (the ranges), not the raw inputs (which may recompute to the same
|
|
131
|
+
// ranges via different intermediate values) — device-confirmed (2026-08-13) this is a second,
|
|
132
|
+
// independent source of the same unbounded same-tick rebuild loop the 'layout' guard fixed.
|
|
133
|
+
const previousInputRange = state.inputRange;
|
|
134
|
+
const previousOutputRange = state.outputRange;
|
|
135
|
+
const hadEmitted = state.rangesEmitted;
|
|
77
136
|
const { inputRange, outputRange } = deriveRanges(state, inputs);
|
|
137
|
+
state.rangesEmitted = true;
|
|
138
|
+
// `hadEmitted` is load-bearing, not defensive: an unmeasured header derives exactly the
|
|
139
|
+
// identity ranges the initial state already holds, so without it the FIRST dispatch (Angular
|
|
140
|
+
// sends one from ngOnInit, before any layout) reads as redundant and the header never emits
|
|
141
|
+
// a rebuild at all — its wrapper then never commits. Regression-covered in
|
|
142
|
+
// sticky-header-reducer.test.ts.
|
|
143
|
+
if (hadEmitted &&
|
|
144
|
+
arraysEqual(previousInputRange, inputRange) &&
|
|
145
|
+
arraysEqual(previousOutputRange, outputRange)) {
|
|
146
|
+
dlog(`STICKY[reducer ${headerTag(state)}] inputs-changed: ranges unchanged, skipped rebuild`);
|
|
147
|
+
return { state, effects: [], changed: false };
|
|
148
|
+
}
|
|
149
|
+
dlog(`STICKY[reducer ${headerTag(state)}] inputs-changed: inputRange ${JSON.stringify(previousInputRange)}->` +
|
|
150
|
+
`${JSON.stringify(inputRange)} outputRange ${JSON.stringify(previousOutputRange)}->` +
|
|
151
|
+
`${JSON.stringify(outputRange)}`);
|
|
78
152
|
return {
|
|
79
153
|
state,
|
|
80
154
|
effects: [{ kind: 'rebuild-interpolation', inputRange, outputRange }],
|
|
@@ -87,20 +161,38 @@ export function reduceSticky(state, action, inputs) {
|
|
|
87
161
|
// settled value into the committed transform for hit-testing.
|
|
88
162
|
if (action.value === 0 && !state.haveReceivedInitialZeroTranslateY) {
|
|
89
163
|
state.haveReceivedInitialZeroTranslateY = true;
|
|
90
|
-
dlog(
|
|
164
|
+
dlog(`STICKY[reducer ${headerTag(state)}] animated-tick: swallowed re-emitted zero translateY`);
|
|
91
165
|
return { state, effects: [], changed: false };
|
|
92
166
|
}
|
|
167
|
+
dlog(`STICKY[reducer ${headerTag(state)}] animated-tick: scheduling debounce delay=${stickyDebounceMs(inputs.os)} ` +
|
|
168
|
+
`value=${action.value}`);
|
|
93
169
|
return {
|
|
94
170
|
state,
|
|
95
171
|
effects: [
|
|
96
|
-
{
|
|
172
|
+
{
|
|
173
|
+
kind: 'schedule-debounce',
|
|
174
|
+
delay: stickyDebounceMs(inputs.os),
|
|
175
|
+
value: action.value,
|
|
176
|
+
},
|
|
97
177
|
],
|
|
98
178
|
changed: false,
|
|
99
179
|
};
|
|
100
180
|
}
|
|
101
181
|
case 'debounce-fired': {
|
|
182
|
+
// Already sitting at this value: emit nothing. Same bail-out the 'layout' case above spells
|
|
183
|
+
// out, for the same reason - React's own header gets it free from setTranslateY (an unchanged
|
|
184
|
+
// primitive bails out of the re-render), a reducer has to say it. Device-confirmed 2026-08-18:
|
|
185
|
+
// without it a re-arriving settled value emits apply-passthrough, the adapter force-renders,
|
|
186
|
+
// the passthrough prop gets a fresh identity, the animated graph reconnects and re-emits into
|
|
187
|
+
// another tick -> another debounce -> another passthrough. One header survives it; a screen of
|
|
188
|
+
// 200 trips React's "Maximum update depth exceeded" and takes the app down.
|
|
189
|
+
if (state.translateY === action.value) {
|
|
190
|
+
dlog(`STICKY[reducer ${headerTag(state)}] debounce-fired: already at translateY=${action.value}, no-op`);
|
|
191
|
+
return { state, effects: [], changed: false };
|
|
192
|
+
}
|
|
102
193
|
// The debounce completed: commit the settled translateY. Once a NON-zero value commits, re-arm
|
|
103
194
|
// the swallow gate so the next interpolation rebuild's spurious 0 is dropped (RN).
|
|
195
|
+
dlog(`STICKY[reducer ${headerTag(state)}] debounce-fired: committing translateY=${action.value}`);
|
|
104
196
|
state.translateY = action.value;
|
|
105
197
|
if (action.value !== 0)
|
|
106
198
|
state.haveReceivedInitialZeroTranslateY = false;
|
package/build/state/switch.js
CHANGED
|
@@ -27,5 +27,5 @@ export function valueFromChange(event) {
|
|
|
27
27
|
// retained tree never diverges and nothing re-commits. The imperative command is the only
|
|
28
28
|
// correction path. No report yet (null) or agreement → no command.
|
|
29
29
|
export function shouldSnapBack(state, fabricValue) {
|
|
30
|
-
return state.lastNativeReport !== null && state.lastNativeReport !== fabricValue;
|
|
30
|
+
return (state.lastNativeReport !== null && state.lastNativeReport !== fabricValue);
|
|
31
31
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ISymbioteEvent, ITextStyle } from '@symbiote-native/engine';
|
|
1
|
+
import type { IPlatformOSType, ISymbioteEvent, ITextStyle } from '@symbiote-native/engine';
|
|
2
2
|
import type { IAccessibilityProps, IAriaProps } from '../accessibility-props';
|
|
3
3
|
export type IInputMode = 'none' | 'text' | 'decimal' | 'numeric' | 'tel' | 'search' | 'email' | 'url';
|
|
4
4
|
export type IEnterKeyHint = 'enter' | 'done' | 'go' | 'next' | 'previous' | 'search' | 'send';
|
|
@@ -10,6 +10,7 @@ export type ITextInputSelection = {
|
|
|
10
10
|
export type ITextInputEventHandler = (event: ISymbioteEvent) => void;
|
|
11
11
|
export declare const INITIAL_EVENT_COUNT = 0;
|
|
12
12
|
export declare const SELECTION_NONE = -1;
|
|
13
|
+
export declare function keyboardTypeForInputMode(inputMode: string, os: IPlatformOSType): string | undefined;
|
|
13
14
|
export declare function mapAutoComplete(map: Record<string, string>, token: string): string | undefined;
|
|
14
15
|
export declare function foldAutoComplete(token: string | undefined): {
|
|
15
16
|
autoComplete: string | undefined;
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
// latest count, while `lastNativeText` is bookkeeping the controlled-write effect mutates
|
|
12
12
|
// without a render. So the logic layer is the pure folds/maps + the controlled-write predicate;
|
|
13
13
|
// each adapter holds the two pieces in ITS own primitives (React useState/useRef, Vue ref/let).
|
|
14
|
+
import { Platform } from '@symbiote-native/engine';
|
|
14
15
|
// The acknowledged count starts at 0, native has reported nothing yet, so the first
|
|
15
16
|
// controlled write echoes 0 and lands on eventLag 0.
|
|
16
17
|
export const INITIAL_EVENT_COUNT = 0;
|
|
@@ -29,11 +30,25 @@ const inputModeToKeyboardType = {
|
|
|
29
30
|
email: 'email-address',
|
|
30
31
|
none: 'default',
|
|
31
32
|
numeric: 'number-pad',
|
|
32
|
-
search: 'default',
|
|
33
33
|
tel: 'phone-pad',
|
|
34
34
|
text: 'default',
|
|
35
35
|
url: 'url',
|
|
36
36
|
};
|
|
37
|
+
// `search` is the ONE inputMode token RN resolves per platform (TextInput.js:815-825):
|
|
38
|
+
// search: Platform.OS === 'ios' ? 'web-search' : 'default'
|
|
39
|
+
// iOS has a dedicated search keyboard whose return key is a magnifier; every other host falls back
|
|
40
|
+
// to the default one. It is deliberately absent from the map above, so there is one source for it
|
|
41
|
+
// rather than a map entry and an override that can drift apart.
|
|
42
|
+
//
|
|
43
|
+
// The host is an ARGUMENT rather than a `Platform.OS` read inside the body, because the headless
|
|
44
|
+
// Platform module always resolves to iOS (platform/index.ts re-exports index.ios) — a direct read
|
|
45
|
+
// would leave the android branch permanently unprovable. resolveTextInputProps supplies the real
|
|
46
|
+
// value. This costs the callers nothing: the exported fold signature is unchanged.
|
|
47
|
+
export function keyboardTypeForInputMode(inputMode, os) {
|
|
48
|
+
if (inputMode === 'search')
|
|
49
|
+
return os === 'ios' ? 'web-search' : 'default';
|
|
50
|
+
return mapAutoComplete(inputModeToKeyboardType, inputMode);
|
|
51
|
+
}
|
|
37
52
|
// RN's enterKeyHint -> returnKeyType map (TextInput.js:805). Note `enter` -> 'default'.
|
|
38
53
|
const enterKeyHintToReturnKeyType = {
|
|
39
54
|
done: 'done',
|
|
@@ -122,7 +137,9 @@ const autoCompleteWebToTextContentType = {
|
|
|
122
137
|
// if the token has no native equivalent. The caller decides the per-platform fallback. Named
|
|
123
138
|
// for its first user (autoComplete), but it is the one generic safe lookup every map shares.
|
|
124
139
|
export function mapAutoComplete(map, token) {
|
|
125
|
-
return Object.prototype.hasOwnProperty.call(map, token)
|
|
140
|
+
return Object.prototype.hasOwnProperty.call(map, token)
|
|
141
|
+
? map[token]
|
|
142
|
+
: undefined;
|
|
126
143
|
}
|
|
127
144
|
// RN folds W3C `autoComplete` per platform (TextInput.js:938): Android takes the mapped
|
|
128
145
|
// `autoComplete` token (falling back to the raw token), iOS takes the mapped `textContentType`
|
|
@@ -187,7 +204,7 @@ export function resolveTextInputProps(input) {
|
|
|
187
204
|
const folded = foldAutoComplete(input.autoComplete);
|
|
188
205
|
return {
|
|
189
206
|
keyboardType: input.inputMode !== undefined
|
|
190
|
-
?
|
|
207
|
+
? keyboardTypeForInputMode(input.inputMode, Platform.OS)
|
|
191
208
|
: input.keyboardType,
|
|
192
209
|
returnKeyType: input.enterKeyHint !== undefined
|
|
193
210
|
? mapAutoComplete(enterKeyHintToReturnKeyType, input.enterKeyHint)
|
|
@@ -195,11 +212,21 @@ export function resolveTextInputProps(input) {
|
|
|
195
212
|
editable: input.readOnly !== undefined ? !input.readOnly : input.editable,
|
|
196
213
|
submitBehavior: foldSubmitBehavior(input.submitBehavior, input.blurOnSubmit, input.multiline),
|
|
197
214
|
selectionColor: input.selectionColor,
|
|
198
|
-
cursorColor: input.cursorColor !== undefined
|
|
199
|
-
|
|
200
|
-
|
|
215
|
+
cursorColor: input.cursorColor !== undefined
|
|
216
|
+
? input.cursorColor
|
|
217
|
+
: input.selectionColor,
|
|
218
|
+
selectionHandleColor: input.selectionHandleColor !== undefined
|
|
219
|
+
? input.selectionHandleColor
|
|
220
|
+
: input.selectionColor,
|
|
221
|
+
underlineColorAndroid: input.underlineColorAndroid !== undefined
|
|
222
|
+
? input.underlineColorAndroid
|
|
223
|
+
: 'transparent',
|
|
201
224
|
autoComplete: folded.autoComplete,
|
|
202
|
-
textContentType: input.textContentType !== undefined
|
|
203
|
-
|
|
225
|
+
textContentType: input.textContentType !== undefined
|
|
226
|
+
? input.textContentType
|
|
227
|
+
: folded.textContentType,
|
|
228
|
+
showSoftInputOnFocus: input.inputMode !== undefined
|
|
229
|
+
? input.inputMode !== 'none'
|
|
230
|
+
: input.showSoftInputOnFocus,
|
|
204
231
|
};
|
|
205
232
|
}
|