@symbiote-native/components 0.4.0 → 1.0.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 (67) hide show
  1. package/README.md +30 -8
  2. package/build/accessibility-props.d.ts +11 -0
  3. package/build/accessibility-props.js +30 -117
  4. package/build/behaviors/image.d.ts +3 -0
  5. package/build/behaviors/image.js +123 -0
  6. package/build/behaviors/input-accessory-view.d.ts +3 -0
  7. package/build/behaviors/input-accessory-view.js +70 -0
  8. package/build/behaviors/pressable.d.ts +2 -0
  9. package/build/behaviors/pressable.js +310 -0
  10. package/build/behaviors/switch.d.ts +2 -0
  11. package/build/behaviors/switch.js +182 -0
  12. package/build/behaviors/text-input.d.ts +14 -0
  13. package/build/behaviors/text-input.js +291 -0
  14. package/build/bootstrap/index.js +1 -1
  15. package/build/component-names/index.android.js +23 -1
  16. package/build/component-names/index.ios.js +9 -1
  17. package/build/component-names/shared.d.ts +1 -1
  18. package/build/component-names/shared.js +40 -1
  19. package/build/descriptor.d.ts +8 -0
  20. package/build/descriptor.js +27 -0
  21. package/build/fold-host-bag.d.ts +15 -0
  22. package/build/fold-host-bag.js +99 -0
  23. package/build/index.d.ts +25 -12
  24. package/build/index.js +18 -6
  25. package/build/resolve-intrinsic.d.ts +7 -0
  26. package/build/resolve-intrinsic.js +49 -0
  27. package/build/state/pressable.d.ts +9 -0
  28. package/build/state/pressable.js +133 -37
  29. package/build/state/sticky-header-reducer.js +28 -5
  30. package/build/state/switch.js +1 -1
  31. package/build/state/text-input.d.ts +7 -1
  32. package/build/state/text-input.js +46 -8
  33. package/build/state/touchable.d.ts +30 -1
  34. package/build/state/touchable.js +94 -5
  35. package/build/state/virtualized-list-diagnostics.d.ts +31 -0
  36. package/build/state/virtualized-list-diagnostics.js +33 -0
  37. package/build/state/virtualized-list-reducer.d.ts +14 -0
  38. package/build/state/virtualized-list-reducer.js +179 -45
  39. package/build/state/virtualized-list.d.ts +5 -2
  40. package/build/state/virtualized-list.js +143 -34
  41. package/build/state-style.d.ts +15 -0
  42. package/build/state-style.js +47 -0
  43. package/build/text-props.d.ts +9 -0
  44. package/build/text-props.js +25 -0
  45. package/build/view/render-activity-indicator.js +37 -3
  46. package/build/view/render-image/index.d.ts +2 -0
  47. package/build/view/render-image/index.js +42 -5
  48. package/build/view/render-input-accessory-view.d.ts +2 -0
  49. package/build/view/render-input-accessory-view.js +41 -6
  50. package/build/view/render-keyboard-avoiding-view.d.ts +14 -2
  51. package/build/view/render-keyboard-avoiding-view.js +52 -6
  52. package/build/view/render-modal.js +4 -2
  53. package/build/view/render-pressable/index.js +3 -1
  54. package/build/view/render-scroll-sticky.js +1 -1
  55. package/build/view/render-scroll-view.js +9 -3
  56. package/build/view/render-switch.js +11 -2
  57. package/build/view/render-text-input.js +6 -2
  58. package/build/view/render-touchable-highlight.d.ts +11 -1
  59. package/build/view/render-touchable-highlight.js +11 -10
  60. package/build/view/render-touchable-native-feedback.js +5 -1
  61. package/host-primitives.cjs +380 -0
  62. package/host-primitives.d.cts +35 -0
  63. package/lowering-fixtures.cjs +259 -0
  64. package/lowering-fixtures.d.cts +17 -0
  65. package/package.json +42 -5
  66. package/specialize-state-style.cjs +219 -0
  67. package/specialize-state-style.d.cts +15 -0
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,38 @@ 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';
37
- export { createPressHandlers, createPressRuntime, normalizeRect, maxEdge, isTouchWithinRegion, readPoint, computeRegion, rippleProps, DEFAULT_DELAY_LONG_PRESS_MS, DEFAULT_PRESS_RECT_OFFSETS, } from './state/pressable';
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';
41
+ export { createPressHandlers, createPressRuntime, disposePressRuntime, normalizeRect, maxEdge, isTouchWithinRegion, readPoint, computeRegion, rippleProps, DEFAULT_DELAY_LONG_PRESS_MS, DEFAULT_MIN_PRESS_DURATION_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, DEFAULT_MIN_PRESS_DURATION_MS, } from './state/touchable';
41
- export type { IPressTimingProps, ITouchableHandler, ITouchableFeedbackRuntime, ITouchableFeedbackConfig, ITouchableFeedbackCallbacks, ITouchableFeedbackHandlers, } from './state/touchable';
42
- export { highlightPressedStyle } from './view/render-touchable-highlight';
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';
67
+ export { registerPressableBehavior, PRESSABLE_TAG, } from './behaviors/pressable';
68
+ export { foldImagePayload, IMAGE_TAG, registerImageBehavior, } from './behaviors/image';
69
+ export { foldInputAccessoryViewPayload, INPUT_ACCESSORY_VIEW_TAG, registerInputAccessoryViewBehavior, } from './behaviors/input-accessory-view';
70
+ export { registerTextInputBehavior, buildTextInputHandle, TEXT_INPUT_TAG, TEXT_INPUT_MULTILINE_TAG, } from './behaviors/text-input';
71
+ export { registerSwitchBehavior, SWITCH_TAG } from './behaviors/switch';
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,19 +42,19 @@ 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
46
51
  // only its reactive `pressed` cell + the View's measure handle (see the 3-layer split).
47
- export { createPressHandlers, createPressRuntime, normalizeRect, maxEdge, isTouchWithinRegion, readPoint, computeRegion, rippleProps, DEFAULT_DELAY_LONG_PRESS_MS, DEFAULT_PRESS_RECT_OFFSETS, } from './state/pressable.js';
52
+ export { createPressHandlers, createPressRuntime, disposePressRuntime, normalizeRect, maxEdge, isTouchWithinRegion, readPoint, computeRegion, rippleProps, DEFAULT_DELAY_LONG_PRESS_MS, DEFAULT_MIN_PRESS_DURATION_MS, DEFAULT_PRESS_RECT_OFFSETS, } from './state/pressable.js';
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, DEFAULT_MIN_PRESS_DURATION_MS, } from './state/touchable.js';
52
- export { highlightPressedStyle } from './view/render-touchable-highlight.js';
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,22 @@ 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';
82
+ export { registerPressableBehavior, PRESSABLE_TAG, } from './behaviors/pressable.js';
83
+ export { foldImagePayload, IMAGE_TAG, registerImageBehavior, } from './behaviors/image.js';
84
+ export { foldInputAccessoryViewPayload, INPUT_ACCESSORY_VIEW_TAG, registerInputAccessoryViewBehavior, } from './behaviors/input-accessory-view.js';
85
+ export { registerTextInputBehavior, buildTextInputHandle, TEXT_INPUT_TAG, TEXT_INPUT_MULTILINE_TAG, } from './behaviors/text-input.js';
86
+ export { registerSwitchBehavior, SWITCH_TAG } from './behaviors/switch.js';
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Resolve a primitive tag to the intrinsic it should actually commit, given its creation props.
3
+ *
4
+ * A tag with no `intrinsicWhen` — every primitive but `TextInput` today — passes through by
5
+ * identity, so a renderer may call this unconditionally without paying a lookup per prop.
6
+ */
7
+ export declare function resolveIntrinsicTag(tag: string, props: Readonly<Record<string, unknown>> | undefined): string;
@@ -0,0 +1,49 @@
1
+ // The RUNTIME half of `HOST_PRIMITIVES.intrinsicWhen` — choosing WHICH native view a primitive
2
+ // commits, from the props it was created with.
3
+ //
4
+ // Why this has to exist at runtime at all. `intrinsicWhen` lets one primitive map to two native
5
+ // views (`TextInput` -> `RCTSinglelineTextInputView` / `RCTMultilineTextInputView`), and until now
6
+ // only the lowering transforms read it — so the choice was made at COMPILE time, from the source
7
+ // text. That is why `dynamicIntrinsicChoice` had to be a refusal category: a transform seeing
8
+ // `multiline={isLong}` cannot know the value, so it refused to lower the element at all.
9
+ //
10
+ // A public primitive TAG has no transform in front of it on three of the five adapters, so the
11
+ // choice has to be made where the value is actually known — here, at element creation. That is a
12
+ // capability the compiler never had, and it is why the refusal category disappears WITH the
13
+ // transform rather than migrating into it.
14
+ //
15
+ // WHAT THIS DELIBERATELY DOES NOT DO: react to the prop CHANGING later. A node's native view is
16
+ // fixed at `createNode` and no prop write moves a node between view types, so flipping `multiline`
17
+ // on a mounted input needs the node re-created — exactly as a browser re-initialises on an
18
+ // `<input type>` change. Re-creation is the renderer's business, not this function's; until a
19
+ // renderer implements it, a runtime flip keeps the view it was created with.
20
+ import { HOST_PRIMITIVES } from '../host-primitives.cjs';
21
+ const CHOICE_BY_BASE_TAG = new Map(Object.values(HOST_PRIMITIVES).flatMap((primitive) => primitive.intrinsicWhen === undefined
22
+ ? []
23
+ : [
24
+ [
25
+ primitive.intrinsic,
26
+ {
27
+ prop: primitive.intrinsicWhen.prop,
28
+ alternate: primitive.intrinsicWhen.intrinsic,
29
+ },
30
+ ],
31
+ ]));
32
+ /**
33
+ * Resolve a primitive tag to the intrinsic it should actually commit, given its creation props.
34
+ *
35
+ * A tag with no `intrinsicWhen` — every primitive but `TextInput` today — passes through by
36
+ * identity, so a renderer may call this unconditionally without paying a lookup per prop.
37
+ */
38
+ export function resolveIntrinsicTag(tag, props) {
39
+ const choice = CHOICE_BY_BASE_TAG.get(tag);
40
+ if (choice === undefined || props === undefined)
41
+ return tag;
42
+ // Explicit rather than truthy, and the string form is not decoration: a template-driven adapter
43
+ // can deliver `multiline="true"` as an ATTRIBUTE STRING (Angular's `multiline="true"` vs
44
+ // `[multiline]="true"`), and plain truthiness would then read `multiline="false"` — also a
45
+ // non-empty string — as multiline. An absent prop keeps the base tag.
46
+ return props[choice.prop] === true || props[choice.prop] === 'true'
47
+ ? choice.alternate
48
+ : tag;
49
+ }
@@ -1,5 +1,6 @@
1
1
  import { type ISymbioteEvent } from '@symbiote-native/engine';
2
2
  export declare const DEFAULT_DELAY_LONG_PRESS_MS = 500;
3
+ export declare const DEFAULT_MIN_PRESS_DURATION_MS = 130;
3
4
  export declare const DEFAULT_PRESS_RECT_OFFSETS: {
4
5
  top: number;
5
6
  left: number;
@@ -56,19 +57,26 @@ export interface IPressRuntime {
56
57
  longPressCancel: (() => void) | undefined;
57
58
  longPressFired: boolean;
58
59
  pressDelayCancel: (() => void) | undefined;
60
+ pressOutCancel: (() => void) | undefined;
59
61
  pressOrigin: {
60
62
  x: number;
61
63
  y: number;
62
64
  } | undefined;
63
65
  driftedOut: boolean;
64
66
  region: IResponderRegion | undefined;
67
+ active: boolean;
68
+ activatedAt: number | undefined;
69
+ delayElapsed: boolean;
70
+ disposed: boolean;
65
71
  }
66
72
  export declare function createPressRuntime(): IPressRuntime;
73
+ export declare function disposePressRuntime(runtime: IPressRuntime): void;
67
74
  export type IFrameCallback = (x: number, y: number, width: number, height: number, pageX: number, pageY: number) => void;
68
75
  export interface IPressHost {
69
76
  setPressed: (pressed: boolean) => void;
70
77
  getMeasureFn: () => ((callback: IFrameCallback) => void) | undefined;
71
78
  schedule: (callback: () => void, ms: number) => () => void;
79
+ now: () => number;
72
80
  }
73
81
  export interface IPressMachineConfig {
74
82
  onPress?: IPressHandler;
@@ -78,6 +86,7 @@ export interface IPressMachineConfig {
78
86
  onLongPress?: IPressHandler;
79
87
  delayLongPress: number;
80
88
  unstable_pressDelay: number;
89
+ minPressDuration?: number;
81
90
  hitSlop?: IRectOffset;
82
91
  pressRetentionOffset?: IRectOffset;
83
92
  }
@@ -11,9 +11,16 @@
11
11
  // the decision of when each callback fires) is here, shared by every adapter.
12
12
  import { dlog, Platform } from '@symbiote-native/engine';
13
13
  export const DEFAULT_DELAY_LONG_PRESS_MS = 500;
14
+ // Pressability's default active-visual floor for a plain Pressable. Touchable* overrides this to 0.
15
+ export const DEFAULT_MIN_PRESS_DURATION_MS = 130;
14
16
  // RN's default extra slop kept around a press once it is active, before a drift fires pressOut.
15
17
  // The PressRect extension (Pressability.js DEFAULT_PRESS_RECT_OFFSETS). Per-edge, deeper bottom.
16
- export const DEFAULT_PRESS_RECT_OFFSETS = { top: 20, left: 20, bottom: 30, right: 20 };
18
+ export const DEFAULT_PRESS_RECT_OFFSETS = {
19
+ top: 20,
20
+ left: 20,
21
+ bottom: 30,
22
+ right: 20,
23
+ };
17
24
  // ---- geometry (pure) --------------------------------------------------------------------------
18
25
  // A rect offset is a per-edge object (not the scalar shorthand). Reads the asymmetric form
19
26
  // without a cast.
@@ -58,7 +65,12 @@ export function readPoint(event) {
58
65
  export function computeRegion(width, height, pageX, pageY) {
59
66
  if (!width && !height && !pageX && !pageY)
60
67
  return undefined;
61
- return { left: pageX, top: pageY, right: pageX + width, bottom: pageY + height };
68
+ return {
69
+ left: pageX,
70
+ top: pageY,
71
+ right: pageX + width,
72
+ bottom: pageY + height,
73
+ };
62
74
  }
63
75
  // Build the Android native-feedback prop the inner View carries from the ripple config. RN runs
64
76
  // the color through processColor → a native int; we have no native bridge in JS, so we keep the
@@ -81,11 +93,40 @@ export function createPressRuntime() {
81
93
  longPressCancel: undefined,
82
94
  longPressFired: false,
83
95
  pressDelayCancel: undefined,
96
+ pressOutCancel: undefined,
84
97
  pressOrigin: undefined,
85
98
  driftedOut: false,
86
99
  region: undefined,
100
+ active: false,
101
+ activatedAt: undefined,
102
+ delayElapsed: false,
103
+ disposed: false,
87
104
  };
88
105
  }
106
+ function cancelRuntimeTimer(runtime, key) {
107
+ const cancel = runtime[key];
108
+ if (cancel === undefined)
109
+ return;
110
+ cancel();
111
+ runtime[key] = undefined;
112
+ }
113
+ // RN Pressability.reset(): cancel every timer and make any already-queued callback inert. The
114
+ // adapter invokes this exactly once from its own unmount/destroy hook.
115
+ export function disposePressRuntime(runtime) {
116
+ if (runtime.disposed)
117
+ return;
118
+ runtime.disposed = true;
119
+ cancelRuntimeTimer(runtime, 'longPressCancel');
120
+ cancelRuntimeTimer(runtime, 'pressDelayCancel');
121
+ cancelRuntimeTimer(runtime, 'pressOutCancel');
122
+ runtime.longPressFired = false;
123
+ runtime.pressOrigin = undefined;
124
+ runtime.driftedOut = false;
125
+ runtime.region = undefined;
126
+ runtime.active = false;
127
+ runtime.activatedAt = undefined;
128
+ runtime.delayElapsed = false;
129
+ }
89
130
  // Measure the responder's on-screen frame and cache it as the retention region for the life of
90
131
  // the press (RN measures on responder grant, _measureResponderRegion). When measure is
91
132
  // unavailable (no node yet, an uncommitted node, or a host slot without a measure method,
@@ -113,7 +154,7 @@ function measureRegion(runtime, measureFn) {
113
154
  // (the React adapter's useMemo body) lives here, shared by every adapter. The adapter rebuilds
114
155
  // these per render (the closures capture the live config) while the runtime persists across them.
115
156
  export function createPressHandlers(config, runtime, host) {
116
- const { onPress, onPressIn, onPressOut, onPressMove, onLongPress, delayLongPress, unstable_pressDelay, } = config;
157
+ const { onPress, onPressIn, onPressOut, onPressMove, onLongPress, delayLongPress, unstable_pressDelay, minPressDuration = DEFAULT_MIN_PRESS_DURATION_MS, } = config;
117
158
  // Per-edge offsets for the measured-rect retention test (RN's hitSlop + pressRectOffset).
118
159
  // pressRetentionOffset defaults to RN's DEFAULT_PRESS_RECT_OFFSETS when unset; hitSlop to zero.
119
160
  const hitSlopRect = normalizeRect(config.hitSlop);
@@ -132,23 +173,31 @@ export function createPressHandlers(config, runtime, host) {
132
173
  const origin = runtime.pressOrigin;
133
174
  if (origin === undefined)
134
175
  return true;
135
- return Math.hypot(point.x - origin.x, point.y - origin.y) <= fallbackThreshold;
176
+ return (Math.hypot(point.x - origin.x, point.y - origin.y) <= fallbackThreshold);
136
177
  }
137
178
  function clearLongPress() {
138
- if (runtime.longPressCancel !== undefined) {
139
- runtime.longPressCancel();
140
- runtime.longPressCancel = undefined;
141
- }
179
+ cancelRuntimeTimer(runtime, 'longPressCancel');
180
+ }
181
+ function clearPressDelay() {
182
+ cancelRuntimeTimer(runtime, 'pressDelayCancel');
183
+ }
184
+ function clearPressOut() {
185
+ cancelRuntimeTimer(runtime, 'pressOutCancel');
142
186
  }
143
- // The real activation: flip pressed-state on, arm the long-press timer, fire onPressIn. Split
144
- // out so unstable_pressDelay can defer it behind a timer while an early release flushes it.
145
187
  function activate(event) {
188
+ if (runtime.disposed || runtime.active || runtime.driftedOut)
189
+ return;
190
+ // A new grant or drift-back-in supersedes a delayed out from the previous active interval.
191
+ clearPressOut();
192
+ runtime.active = true;
193
+ runtime.activatedAt = host.now();
146
194
  dlog('Pressable pressIn');
147
195
  host.setPressed(true);
148
- runtime.longPressFired = false;
149
- if (onLongPress) {
196
+ if (!runtime.longPressFired && onLongPress) {
150
197
  runtime.longPressCancel = host.schedule(() => {
151
198
  runtime.longPressCancel = undefined;
199
+ if (runtime.disposed || !runtime.active || runtime.driftedOut)
200
+ return;
152
201
  runtime.longPressFired = true;
153
202
  dlog('Pressable longPress timer fired');
154
203
  onLongPress(event);
@@ -156,51 +205,99 @@ export function createPressHandlers(config, runtime, host) {
156
205
  }
157
206
  onPressIn?.(event);
158
207
  }
159
- // Flush a still-pending pressDelay timer immediately, so a pressOut/press that arrives before
160
- // the delay elapsed still sees an activated press.
161
- function flushPressDelay(event) {
162
- if (runtime.pressDelayCancel !== undefined) {
163
- runtime.pressDelayCancel();
164
- runtime.pressDelayCancel = undefined;
165
- activate(event);
208
+ function deactivate(event) {
209
+ if (runtime.disposed || !runtime.active)
210
+ return;
211
+ runtime.active = false;
212
+ clearLongPress();
213
+ const activatedAt = runtime.activatedAt ?? host.now();
214
+ runtime.activatedAt = undefined;
215
+ const heldFor = Math.max(0, host.now() - activatedAt);
216
+ const wait = Math.max(minPressDuration - heldFor, 0);
217
+ const finish = () => {
218
+ runtime.pressOutCancel = undefined;
219
+ if (runtime.disposed)
220
+ return;
221
+ host.setPressed(false);
222
+ onPressOut?.(event);
223
+ };
224
+ if (wait > 0) {
225
+ dlog(`Pressable pressOut deferred ${wait}ms`);
226
+ runtime.pressOutCancel = host.schedule(finish, wait);
166
227
  }
228
+ else {
229
+ finish();
230
+ }
231
+ }
232
+ // An honest release arrives as press then pressOut. If it beat delayPressIn, activate now so the
233
+ // tap still flashes and calls onPressIn; a cancelled/out-of-bounds gesture never calls this path.
234
+ function completePressDelay(event) {
235
+ if (runtime.pressDelayCancel !== undefined)
236
+ clearPressDelay();
237
+ if (!runtime.delayElapsed)
238
+ runtime.delayElapsed = true;
239
+ if (!runtime.driftedOut)
240
+ activate(event);
241
+ }
242
+ function resetGesture() {
243
+ runtime.pressOrigin = undefined;
244
+ runtime.region = undefined;
245
+ runtime.driftedOut = false;
246
+ runtime.delayElapsed = false;
247
+ runtime.longPressFired = false;
167
248
  }
168
249
  return {
169
250
  handlePressIn(event) {
251
+ if (runtime.disposed)
252
+ return;
253
+ // A new grant cancels a delayed out from the prior gesture, matching Pressability's
254
+ // onResponderGrant -> _cancelPressOutDelayTimeout.
255
+ clearPressDelay();
256
+ clearLongPress();
257
+ clearPressOut();
258
+ runtime.active = false;
259
+ runtime.activatedAt = undefined;
260
+ runtime.longPressFired = false;
261
+ runtime.delayElapsed = false;
170
262
  runtime.pressOrigin = readPoint(event);
171
263
  runtime.driftedOut = false;
172
- // Measure now so the move stream tests against the real frame, not a press-start radius.
173
264
  measureRegion(runtime, host.getMeasureFn());
174
265
  if (unstable_pressDelay > 0) {
175
266
  dlog(`Pressable pressIn deferred ${unstable_pressDelay}ms`);
176
267
  runtime.pressDelayCancel = host.schedule(() => {
177
268
  runtime.pressDelayCancel = undefined;
178
- activate(event);
269
+ if (runtime.disposed)
270
+ return;
271
+ runtime.delayElapsed = true;
272
+ if (!runtime.driftedOut)
273
+ activate(event);
179
274
  }, unstable_pressDelay);
180
275
  return;
181
276
  }
277
+ runtime.delayElapsed = true;
182
278
  activate(event);
183
279
  },
184
280
  handlePressOut(event) {
281
+ if (runtime.disposed)
282
+ return;
185
283
  dlog('Pressable pressOut');
186
- flushPressDelay(event);
284
+ // A cancellation before delayPressIn must stay inert. An honest release already flushed the
285
+ // delay in handlePress, because the engine emits press immediately before pressOut.
286
+ clearPressDelay();
187
287
  clearLongPress();
188
- runtime.pressOrigin = undefined;
189
- runtime.region = undefined;
190
- host.setPressed(false);
191
- onPressOut?.(event);
288
+ deactivate(event);
289
+ resetGesture();
192
290
  },
193
291
  handlePress(event) {
292
+ if (runtime.disposed)
293
+ return;
194
294
  dlog('Pressable press');
195
- flushPressDelay(event);
295
+ completePressDelay(event);
196
296
  clearLongPress();
197
- // A drift past the retention region cancels the tap (RN); reset and suppress.
198
297
  if (runtime.driftedOut) {
199
- runtime.driftedOut = false;
200
298
  dlog('Pressable press suppressed by drift past retention region');
201
299
  return;
202
300
  }
203
- // A fired long press cancels the tap: reset the flag and suppress onPress.
204
301
  if (runtime.longPressFired) {
205
302
  runtime.longPressFired = false;
206
303
  dlog('Pressable press suppressed by prior longPress');
@@ -208,11 +305,9 @@ export function createPressHandlers(config, runtime, host) {
208
305
  }
209
306
  onPress?.(event);
210
307
  },
211
- // Responder-move stream of the Pressable's own View. onPressMove fires on every move while
212
- // the press is live (RN). Then the retention test: a move outside the measured region
213
- // (expanded by hitSlop+pressRectOffset) fires an early pressOut and marks the tap suppressed;
214
- // a move back inside re-activates.
215
308
  handleResponderMove(event) {
309
+ if (runtime.disposed)
310
+ return;
216
311
  onPressMove?.(event);
217
312
  const here = readPoint(event);
218
313
  if (!here)
@@ -222,14 +317,15 @@ export function createPressHandlers(config, runtime, host) {
222
317
  dlog('Pressable drifted past retention region — deactivating');
223
318
  runtime.driftedOut = true;
224
319
  clearLongPress();
225
- host.setPressed(false);
226
- onPressOut?.(event);
320
+ // Before delayPressIn there was no activation, so there is no onPressOut to emit.
321
+ deactivate(event);
227
322
  }
228
323
  }
229
324
  else if (runtime.driftedOut) {
230
325
  dlog('Pressable returned inside retention region — reactivating');
231
326
  runtime.driftedOut = false;
232
- activate(event);
327
+ if (runtime.delayElapsed)
328
+ activate(event);
233
329
  }
234
330
  },
235
331
  };
@@ -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];
@@ -68,7 +68,9 @@ function headerTag(state) {
68
68
  }
69
69
  export function reduceSticky(state, action, inputs) {
70
70
  dlog(`STICKY[reducer ${headerTag(state)}] action=${action.kind}` +
71
- (action.kind === 'layout' ? ` y=${action.y} height=${action.height}` : '') +
71
+ (action.kind === 'layout'
72
+ ? ` y=${action.y} height=${action.height}`
73
+ : '') +
72
74
  (action.kind === 'animated-tick' || action.kind === 'debounce-fired'
73
75
  ? ` value=${action.value}`
74
76
  : '') +
@@ -88,13 +90,19 @@ export function reduceSticky(state, action, inputs) {
88
90
  // native-driven rebuild (fresh AnimatedProps/AnimatedStyle graph, fresh native connect) commit a
89
91
  // fresh prop identity on every redundant layout, which can itself provoke another relayout pass —
90
92
  // an unbounded same-tick rebuild ping-pong that trips Svelte's effect_update_depth_exceeded guard.
91
- const alreadyAtThisGeometry = state.measured && state.layoutY === action.y && state.layoutHeight === action.height;
93
+ const alreadyAtThisGeometry = state.measured &&
94
+ state.layoutY === action.y &&
95
+ state.layoutHeight === action.height;
92
96
  state.layoutY = action.y;
93
97
  state.layoutHeight = action.height;
94
98
  state.measured = true;
95
99
  const effects = [];
96
100
  if (inputs.index !== undefined) {
97
- effects.push({ kind: 'record-header-y', index: inputs.index, y: action.y });
101
+ effects.push({
102
+ kind: 'record-header-y',
103
+ index: inputs.index,
104
+ y: action.y,
105
+ });
98
106
  }
99
107
  if (alreadyAtThisGeometry && state.rangesEmitted) {
100
108
  dlog(`STICKY[reducer ${headerTag(state)}] layout: redundant geometry, skipped rebuild`);
@@ -161,12 +169,27 @@ export function reduceSticky(state, action, inputs) {
161
169
  return {
162
170
  state,
163
171
  effects: [
164
- { kind: 'schedule-debounce', delay: stickyDebounceMs(inputs.os), value: action.value },
172
+ {
173
+ kind: 'schedule-debounce',
174
+ delay: stickyDebounceMs(inputs.os),
175
+ value: action.value,
176
+ },
165
177
  ],
166
178
  changed: false,
167
179
  };
168
180
  }
169
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
+ }
170
193
  // The debounce completed: commit the settled translateY. Once a NON-zero value commits, re-arm
171
194
  // the swallow gate so the next interpolation rebuild's spurious 0 is dropped (RN).
172
195
  dlog(`STICKY[reducer ${headerTag(state)}] debounce-fired: committing translateY=${action.value}`);
@@ -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 { IMeasureOnSuccess, IMeasureInWindowOnSuccess, IMeasureLayoutOnSuccess, ISymbioteNode, 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;
@@ -94,10 +95,15 @@ export type ITextInputProps = IAccessibilityProps & IAriaProps & {
94
95
  onSelectionChange?: ITextInputEventHandler;
95
96
  onContentSizeChange?: ITextInputEventHandler;
96
97
  };
98
+ export declare const TEXT_INPUT_CALLBACK_NAMES: readonly string[];
97
99
  export type ITextInputHandle = {
98
100
  focus(): void;
99
101
  blur(): void;
100
102
  clear(): void;
101
103
  isFocused(): boolean;
102
104
  setSelection(start: number, end: number): void;
105
+ measure(callback: IMeasureOnSuccess): void;
106
+ measureInWindow(callback: IMeasureInWindowOnSuccess): void;
107
+ measureLayout(relativeToNativeNode: ISymbioteNode | number, onSuccess: IMeasureLayoutOnSuccess, onFail?: () => void): void;
108
+ setNativeProps(nativeProps: Record<string, unknown>): void;
103
109
  };