@symbiote-native/react 1.0.0 → 3.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 (100) hide show
  1. package/README.md +15 -14
  2. package/build/components/activity-indicator-props.d.ts +4 -0
  3. package/build/components/{button.d.ts → button-props.d.ts} +0 -2
  4. package/build/components/button-props.js +1 -0
  5. package/build/components/flat-list/index.js +3 -3
  6. package/build/components/image/image-props.d.ts +4 -0
  7. package/build/components/image/image-props.js +1 -0
  8. package/build/components/image-background-props.d.ts +9 -0
  9. package/build/components/image-background-props.js +1 -0
  10. package/build/components/input-accessory-view-props.d.ts +10 -0
  11. package/build/components/input-accessory-view-props.js +1 -0
  12. package/build/components/keyboard-avoiding-view/index.js +10 -5
  13. package/build/components/modal/index.js +3 -3
  14. package/build/components/pressable/{index.d.ts → pressable-props.d.ts} +6 -5
  15. package/build/components/pressable/pressable-props.js +1 -0
  16. package/build/components/{refresh-control/index.d.ts → refresh-control-props.d.ts} +3 -3
  17. package/build/components/refresh-control-props.js +1 -0
  18. package/build/components/safe-area-view-props.d.ts +11 -0
  19. package/build/components/safe-area-view-props.js +1 -0
  20. package/build/components/scroll-view/{shared.d.ts → scroll-view-props.d.ts} +4 -26
  21. package/build/components/scroll-view/scroll-view-props.js +1 -0
  22. package/build/components/switch/switch-props.d.ts +4 -0
  23. package/build/components/switch/switch-props.js +1 -0
  24. package/build/components/text-input/text-input-props.d.ts +4 -0
  25. package/build/components/text-input/text-input-props.js +1 -0
  26. package/build/components/touchable/touchable-props.d.ts +19 -0
  27. package/build/components/touchable/touchable-props.js +1 -0
  28. package/build/components/touchable-native-feedback/touchable-native-feedback-props.d.ts +9 -0
  29. package/build/components/touchable-native-feedback/touchable-native-feedback-props.js +1 -0
  30. package/build/components/touchable-without-feedback/touchable-without-feedback-props.d.ts +7 -0
  31. package/build/components/touchable-without-feedback/touchable-without-feedback-props.js +1 -0
  32. package/build/components/virtualized-list/index.js +90 -41
  33. package/build/components.d.ts +1 -2
  34. package/build/components.js +5 -19
  35. package/build/create-portal/index.js +1 -1
  36. package/build/create-tunnel/index.js +1 -1
  37. package/build/descriptor-to-react/index.js +1 -1
  38. package/build/host-config.js +23 -12
  39. package/build/index.d.ts +23 -28
  40. package/build/index.js +14 -14
  41. package/build/jsx-dev-runtime.d.ts +3 -0
  42. package/build/jsx-dev-runtime.js +10 -0
  43. package/build/jsx-namespace.type-check.d.ts +1 -0
  44. package/build/jsx-namespace.type-check.js +7 -0
  45. package/build/jsx-runtime.d.ts +62 -0
  46. package/build/jsx-runtime.js +1 -0
  47. package/build/modules/animated/index.d.ts +29 -20
  48. package/build/modules/animated/index.js +16 -22
  49. package/build/modules/image/index.d.ts +3 -0
  50. package/build/modules/image/index.js +14 -0
  51. package/build/register.d.ts +1 -0
  52. package/build/register.js +15 -0
  53. package/metro-css-parser.cjs +2 -1
  54. package/package.json +12 -8
  55. package/build/components/activity-indicator/index.android.d.ts +0 -3
  56. package/build/components/activity-indicator/index.android.js +0 -17
  57. package/build/components/activity-indicator/index.d.ts +0 -1
  58. package/build/components/activity-indicator/index.ios.d.ts +0 -3
  59. package/build/components/activity-indicator/index.ios.js +0 -16
  60. package/build/components/activity-indicator/index.js +0 -4
  61. package/build/components/activity-indicator/shared.d.ts +0 -6
  62. package/build/components/activity-indicator/shared.js +0 -22
  63. package/build/components/button.js +0 -42
  64. package/build/components/image/index.d.ts +0 -9
  65. package/build/components/image/index.js +0 -28
  66. package/build/components/image-background/index.d.ts +0 -10
  67. package/build/components/image-background/index.js +0 -46
  68. package/build/components/input-accessory-view/index.d.ts +0 -11
  69. package/build/components/input-accessory-view/index.js +0 -19
  70. package/build/components/pressable/index.js +0 -112
  71. package/build/components/refresh-control/index.js +0 -21
  72. package/build/components/safe-area-view/index.d.ts +0 -12
  73. package/build/components/safe-area-view/index.js +0 -18
  74. package/build/components/scroll-view/index.android.d.ts +0 -3
  75. package/build/components/scroll-view/index.android.js +0 -61
  76. package/build/components/scroll-view/index.d.ts +0 -1
  77. package/build/components/scroll-view/index.ios.d.ts +0 -3
  78. package/build/components/scroll-view/index.ios.js +0 -29
  79. package/build/components/scroll-view/index.js +0 -4
  80. package/build/components/scroll-view/shared.js +0 -240
  81. package/build/components/scroll-view/sticky-header.d.ts +0 -18
  82. package/build/components/scroll-view/sticky-header.js +0 -227
  83. package/build/components/switch/index.android.d.ts +0 -3
  84. package/build/components/switch/index.android.js +0 -18
  85. package/build/components/switch/index.d.ts +0 -1
  86. package/build/components/switch/index.ios.d.ts +0 -3
  87. package/build/components/switch/index.ios.js +0 -16
  88. package/build/components/switch/index.js +0 -6
  89. package/build/components/switch/shared.d.ts +0 -9
  90. package/build/components/switch/shared.js +0 -60
  91. package/build/components/text-input/index.d.ts +0 -50
  92. package/build/components/text-input/index.js +0 -180
  93. package/build/components/touchable/index.d.ts +0 -22
  94. package/build/components/touchable/index.js +0 -165
  95. package/build/components/touchable-native-feedback/index.d.ts +0 -18
  96. package/build/components/touchable-native-feedback/index.js +0 -32
  97. package/build/jsx.d.ts +0 -18
  98. package/build/state-style.d.ts +0 -1
  99. package/build/state-style.js +0 -13
  100. /package/build/{jsx.js → components/activity-indicator-props.js} +0 -0
@@ -1,4 +1,4 @@
1
- // VirtualizedList: real windowing over the existing ScrollView. Only the cells
1
+ // VirtualizedList: real windowing over the `scroll-view` TAG. Only the cells
2
2
  // whose computed offset falls inside the visible window (plus a leading/trailing
3
3
  // buffer) are rendered; everything above and below is collapsed into two spacer
4
4
  // Views whose sizes sum to the off-screen extent, so the scroll thumb and total
@@ -14,14 +14,31 @@
14
14
  // from a `commit` dispatched in a layout effect (before paint, so MVCP's shift lands without a
15
15
  // visible jump). Lists have no Descriptor render fn — the cell content is React's own children.
16
16
  //
17
- // Imperative scrolling resolves to an offset in the reducer and rides the ScrollView's native
18
- // scrollTo command via its handle ref, animated by default. The `contentOffset` prop is only a
19
- // fallback for the pre-mount window (handle not yet attached).
17
+ // Imperative scrolling resolves to an offset in the reducer and rides the scroll node's native
18
+ // scrollTo command, animated by default. The `contentOffset` prop is only a fallback for the
19
+ // pre-mount window (the node not yet attached).
20
+ //
21
+ // WHAT IS STILL THIS COMPONENT'S, now that the scroll view is a tag and the engine owns its
22
+ // content node, its sticky seam and its RefreshControl placement — checked rather than assumed,
23
+ // because "the wrapper is gone" and "nothing is left" are different claims:
24
+ //
25
+ // the WINDOW `reduceList` decides which indices are in view, and this file is what
26
+ // turns that into elements. The output SHAPE is decided in JS from the
27
+ // app's own data through `renderItem` — the one disqualifier that no
28
+ // engine seam removes (`.claude/rules/host-primitive-tier.md`, tier 3).
29
+ // the CELL WRAPPERS a measuring `view` per cell with the inversion counter-flip, the two
30
+ // spacers, the header/footer/empty slots, the separator INSIDE the cell.
31
+ // the LIFECYCLE native events -> actions, effects run with React primitives (timers, a
32
+ // forced re-render, a layout effect for MVCP), one state cell.
33
+ // the HANDLE `buildScrollViewHandle` over the node a `ref` hands back, plus the list's
34
+ // own imperative surface on top of it.
35
+ //
36
+ // What is NOT: the content node, `contentContainerStyle`'s routing, where a RefreshControl goes
37
+ // per platform, and how a sticky header pins. The list names `sticky-header` on a cell and knows
38
+ // nothing else about it.
20
39
  import { createElement, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useReducer, useRef, useState, } from 'react';
21
40
  import { dlog, } from '@symbiote-native/engine';
22
- import { DEFAULT_END_REACHED_THRESHOLD, DEFAULT_INITIAL_NUM_TO_RENDER, DEFAULT_MAX_TO_RENDER_PER_BATCH, DEFAULT_START_REACHED_THRESHOLD, DEFAULT_UPDATE_CELLS_BATCHING_PERIOD, DEFAULT_WINDOW_SIZE, EMPTY_OFFSET, FIRST_INDEX, INVERTED_X_STYLE, INVERTED_Y_STYLE, buildListPlan, buildViewabilityPairs, createInitialListState, isSeparatorGapInRange, listEffectSignature, readLayoutLength, readLayoutOffset, readScrollOffset, reduceList, resolveItemKey, } from '@symbiote-native/components';
23
- import { ScrollView, } from '../scroll-view';
24
- import { RefreshControl } from '../refresh-control/index.js';
41
+ import { DEFAULT_INITIAL_NUM_TO_RENDER, DEFAULT_MAX_TO_RENDER_PER_BATCH, DEFAULT_UPDATE_CELLS_BATCHING_PERIOD, DEFAULT_WINDOW_SIZE, EMPTY_OFFSET, FIRST_INDEX, INVERTED_X_STYLE, INVERTED_Y_STYLE, buildListPlan, buildScrollViewHandle, buildViewabilityPairs, createInitialListState, isSeparatorGapInRange, listEffectSignature, readLayoutLength, readLayoutOffset, readScrollOffset, reduceList, resolveItemKey, } from '@symbiote-native/components';
25
42
  function resolveElement(component) {
26
43
  if (component === undefined)
27
44
  return undefined;
@@ -48,7 +65,7 @@ function renderSeparatorElement(component, leadingItem, trailingItem, overrides)
48
65
  // generic). The ref is destructured here and wired through useImperativeHandle.
49
66
  export function VirtualizedList(props) {
50
67
  const forwardedRef = props.ref;
51
- const { data, getItem, getItemCount, renderItem, keyExtractor, getItemLayout, ItemSeparatorComponent, ListHeaderComponent, ListFooterComponent, ListEmptyComponent, horizontal = false, inverted = false, extraData, onEndReached, onEndReachedThreshold = DEFAULT_END_REACHED_THRESHOLD, onStartReached, onStartReachedThreshold = DEFAULT_START_REACHED_THRESHOLD, onRefresh, refreshing, progressViewOffset, onViewableItemsChanged, viewabilityConfig, viewabilityConfigCallbackPairs, onScrollToIndexFailed, initialNumToRender = DEFAULT_INITIAL_NUM_TO_RENDER, initialScrollIndex, maxToRenderPerBatch = DEFAULT_MAX_TO_RENDER_PER_BATCH, updateCellsBatchingPeriod = DEFAULT_UPDATE_CELLS_BATCHING_PERIOD, windowSize = DEFAULT_WINDOW_SIZE, stickyHeaderIndices, maintainVisibleContentPosition, style, contentContainerStyle,
68
+ const { data, getItem, getItemCount, renderItem, keyExtractor, getItemLayout, ItemSeparatorComponent, ListHeaderComponent, ListFooterComponent, ListEmptyComponent, horizontal = false, inverted = false, extraData, onEndReached, onEndReachedThreshold, onStartReached, onStartReachedThreshold, onRefresh, refreshing, progressViewOffset, onViewableItemsChanged, viewabilityConfig, viewabilityConfigCallbackPairs, onScrollToIndexFailed, initialNumToRender = DEFAULT_INITIAL_NUM_TO_RENDER, initialScrollIndex, maxToRenderPerBatch = DEFAULT_MAX_TO_RENDER_PER_BATCH, updateCellsBatchingPeriod = DEFAULT_UPDATE_CELLS_BATCHING_PERIOD, windowSize = DEFAULT_WINDOW_SIZE, stickyHeaderIndices, maintainVisibleContentPosition, style, contentContainerStyle,
52
69
  // Pulled out of the rest so the user's onScroll does NOT arrive raw via
53
70
  // ...accessibilityRest and overwrite the internal windowing handler. Composed below.
54
71
  onScroll: userOnScroll, onScrollBeginDrag, onScrollEndDrag, onMomentumScrollBegin, onMomentumScrollEnd, scrollEventThrottle, keyboardShouldPersistTaps, keyboardDismissMode,
@@ -101,7 +118,12 @@ export function VirtualizedList(props) {
101
118
  // The offset we are imperatively driving native to (scrollTo* before the handle attaches). Pushed
102
119
  // down as the ScrollView's contentOffset prop; fresh object identity each time. undefined = none.
103
120
  const [commandedOffset, setCommandedOffset] = useState(undefined);
104
- const scrollViewRef = useRef(null);
121
+ // The engine node of the scroll TAG. A `ref` on an intrinsic hands back the host instance, so
122
+ // there is no wrapper left to run a `useImperativeHandle` — `buildScrollViewHandle` is what turns
123
+ // the node into RN's scroll surface, and it is the same function every other adapter feeds from
124
+ // its own host-instance accessor. Lazy getter: the node is null until commit.
125
+ const scrollNodeRef = useRef(null);
126
+ const scrollHandle = useMemo(() => buildScrollViewHandle(() => scrollNodeRef.current), []);
105
127
  // Pending minimumViewTime debounce timer / the incremental-fill timer (adapter owns the timers;
106
128
  // the reducer only asks for a delay).
107
129
  const viewableTimerRef = useRef(null);
@@ -117,9 +139,9 @@ export function VirtualizedList(props) {
117
139
  const target = isHorizontal
118
140
  ? { x: clamped, y: EMPTY_OFFSET }
119
141
  : { x: EMPTY_OFFSET, y: clamped };
120
- if (scrollViewRef.current !== null) {
142
+ if (scrollNodeRef.current !== null) {
121
143
  dlog(`VirtualizedList scrollTo offset=${clamped} animated=${animated} (horizontal=${isHorizontal})`);
122
- scrollViewRef.current.scrollTo({ x: target.x, y: target.y, animated });
144
+ scrollHandle.scrollTo({ x: target.x, y: target.y, animated });
123
145
  return;
124
146
  }
125
147
  dlog(`VirtualizedList scrollTo offset=${clamped} pending-ref (horizontal=${isHorizontal})`);
@@ -166,8 +188,12 @@ export function VirtualizedList(props) {
166
188
  const info = effect.info;
167
189
  const map = effect.map;
168
190
  const fire = () => {
169
- for (const pair of pairs)
170
- pair.onViewableItemsChanged(info);
191
+ for (const pair of pairs) {
192
+ pair.onViewableItemsChanged({
193
+ ...info,
194
+ viewabilityConfig: pair.viewabilityConfig,
195
+ });
196
+ }
171
197
  dispatchRef.current({ kind: 'viewable-fired', map });
172
198
  };
173
199
  if (viewableTimerRef.current !== null) {
@@ -289,18 +315,20 @@ export function VirtualizedList(props) {
289
315
  dispatch({ kind: 'scroll-to-end', animated: params?.animated ?? true });
290
316
  },
291
317
  flashScrollIndicators: () => {
292
- scrollViewRef.current?.flashScrollIndicators?.();
318
+ scrollHandle.flashScrollIndicators?.();
293
319
  },
294
- getNativeScrollRef: () => scrollViewRef.current,
295
- getScrollableNode: () => scrollViewRef.current,
296
- getScrollResponder: () => scrollViewRef.current,
297
- getScrollNode: () => scrollViewRef.current?.getScrollNode() ?? null,
320
+ // Null until the tag commits, so these keep RN's "no scroll view yet" answer rather than
321
+ // handing back a handle that would silently no-op.
322
+ getNativeScrollRef: () => scrollNodeRef.current !== null ? scrollHandle : null,
323
+ getScrollableNode: () => scrollNodeRef.current !== null ? scrollHandle : null,
324
+ getScrollResponder: () => scrollNodeRef.current !== null ? scrollHandle : null,
325
+ getScrollNode: () => scrollNodeRef.current,
298
326
  // Manual trigger for RN's recordInteraction: flip the interaction flag so
299
327
  // waitForInteraction viewability configs start reporting.
300
328
  recordInteraction: () => {
301
329
  dispatch({ kind: 'record-interaction' });
302
330
  },
303
- }), [dispatch]);
331
+ }), [dispatch, scrollHandle]);
304
332
  // After-commit pass: run the deferred effects (batch fill, edge-reached, viewability,
305
333
  // initial-scroll, MVCP) in a LAYOUT effect so MVCP's shift lands before paint. Re-runs only when
306
334
  // the windowing signature changed — the same dedup key every adapter shares.
@@ -328,15 +356,14 @@ export function VirtualizedList(props) {
328
356
  const stickySet = stickyHeaderIndices !== undefined
329
357
  ? new Set(stickyHeaderIndices)
330
358
  : undefined;
331
- let renderedStickyIndices = [];
332
359
  const header = resolveElement(ListHeaderComponent);
333
360
  if (header !== undefined) {
334
- children.push(createElement('symbiote-view', { key: 'list-header' }, header));
361
+ children.push(createElement('view', { key: 'list-header' }, header));
335
362
  }
336
363
  if (count === FIRST_INDEX) {
337
364
  const empty = resolveElement(ListEmptyComponent);
338
365
  if (empty !== undefined) {
339
- children.push(createElement('symbiote-view', { key: 'list-empty' }, empty));
366
+ children.push(createElement('view', { key: 'list-empty' }, empty));
340
367
  }
341
368
  }
342
369
  else {
@@ -352,9 +379,14 @@ export function VirtualizedList(props) {
352
379
  stickyIndices: stickySet,
353
380
  hasHeader: header !== undefined,
354
381
  });
355
- renderedStickyIndices = plan.stickyChildPositions;
382
+ // `plan.stickyChildPositions` is deliberately NOT read. It was the input to the wrapper's own
383
+ // JS sticky pass, and the behavior honours `stickyHeaderIndices` by numbering the OWNER's paint
384
+ // children — which a windowed list cannot supply: it paints a header, a spacer and a slice, so
385
+ // data index 3 is almost never paint child 3, and the positions would have to be recomputed
386
+ // every time the window slides. The cells that should pin carry the `sticky-header` TAG
387
+ // instead, which pins by document order and survives windowing.
356
388
  if (plan.leadingExtent > EMPTY_OFFSET) {
357
- children.push(createElement('symbiote-view', {
389
+ children.push(createElement('view', {
358
390
  key: 'spacer-leading',
359
391
  style: horizontal
360
392
  ? { width: plan.leadingExtent }
@@ -374,7 +406,11 @@ export function VirtualizedList(props) {
374
406
  index: forcedIndex,
375
407
  separators: makeSeparators(forcedIndex),
376
408
  });
377
- children.push(createElement('symbiote-view', {
409
+ children.push(createElement(
410
+ // Sticky by construction — `forcedStickyCell` exists only for an index in `stickySet` —
411
+ // and the SAME tag the in-window branch below uses, so a cell crossing between the two
412
+ // keeps its element type as well as its key and never remounts.
413
+ 'sticky-header', {
378
414
  key: `cell-${plan.forcedStickyCell.key}`,
379
415
  onLayout: makeCellMeasure(forcedIndex),
380
416
  style: inverted
@@ -385,7 +421,7 @@ export function VirtualizedList(props) {
385
421
  }, forcedCell));
386
422
  }
387
423
  if (plan.gapExtent > EMPTY_OFFSET) {
388
- children.push(createElement('symbiote-view', {
424
+ children.push(createElement('view', {
389
425
  key: 'spacer-gap',
390
426
  style: horizontal
391
427
  ? { width: plan.gapExtent }
@@ -420,7 +456,13 @@ export function VirtualizedList(props) {
420
456
  : undefined;
421
457
  // Wrap each cell in a measuring View. When inverted, each cell carries the counter-flip so
422
458
  // its content reads upright inside the flipped content container.
423
- children.push(createElement('symbiote-view', {
459
+ //
460
+ // A cell the app flagged sticky is wrapped in the `sticky-header` TAG instead — same
461
+ // position, same key, same `onLayout` (the behavior forwards it rather than replacing it),
462
+ // and the engine finds this scroll view by walking up. Nothing here computes a child index:
463
+ // the collision point comes from the owner's DOCUMENT order, which is the one form that
464
+ // survives windowing.
465
+ children.push(createElement(stickySet?.has(planCell.index) === true ? 'sticky-header' : 'view', {
424
466
  key: `cell-${planCell.key}`,
425
467
  onLayout: makeCellMeasure(planCell.index),
426
468
  style: inverted
@@ -431,7 +473,7 @@ export function VirtualizedList(props) {
431
473
  }, cell, separator));
432
474
  }
433
475
  if (plan.trailingExtent > EMPTY_OFFSET) {
434
- children.push(createElement('symbiote-view', {
476
+ children.push(createElement('view', {
435
477
  key: 'spacer-trailing',
436
478
  style: horizontal
437
479
  ? { width: plan.trailingExtent }
@@ -441,7 +483,7 @@ export function VirtualizedList(props) {
441
483
  }
442
484
  const footer = resolveElement(ListFooterComponent);
443
485
  if (footer !== undefined) {
444
- children.push(createElement('symbiote-view', { key: 'list-footer' }, footer));
486
+ children.push(createElement('view', { key: 'list-footer' }, footer));
445
487
  }
446
488
  // A horizontal list pins the content container to the full row width so the row overflows for
447
489
  // iOS to scroll. The inversion flip rides ONLY the outer ScrollView style and each cell, never
@@ -458,10 +500,13 @@ export function VirtualizedList(props) {
458
500
  ...accessibilityRest,
459
501
  style: resolvedStyle,
460
502
  contentContainerStyle: resolvedContentContainerStyle,
461
- horizontal,
462
503
  onScroll,
463
504
  onLayout: onViewportLayout,
464
505
  };
506
+ // `horizontal` is NOT in the bag: the axis is the TAG (`horizontal-scroll-view`), because Android
507
+ // scrolls the two axes with different native ViewManagers. The behavior's own fold deletes the
508
+ // prop and rewrites it from the tag, so passing it would be at best redundant and at worst a
509
+ // contradiction it has to warn about.
465
510
  if (onScrollBeginDrag !== undefined)
466
511
  scrollProps.onScrollBeginDrag = onScrollBeginDrag;
467
512
  if (onScrollEndDrag !== undefined)
@@ -479,9 +524,6 @@ export function VirtualizedList(props) {
479
524
  // A pending imperative/initial scroll rides down as contentOffset (fresh identity each push).
480
525
  if (commandedOffset !== undefined)
481
526
  scrollProps.contentOffset = commandedOffset;
482
- // Headers in the window stick; an empty list leaves the prop off entirely.
483
- if (renderedStickyIndices.length > 0)
484
- scrollProps.stickyHeaderIndices = renderedStickyIndices;
485
527
  // Forward maintainVisibleContentPosition to the native ScrollView so it anchors in-window cells.
486
528
  // minIndexForVisible is bumped by 1 when a ListHeaderComponent occupies child 0.
487
529
  if (maintainVisibleContentPosition !== undefined) {
@@ -491,16 +533,23 @@ export function VirtualizedList(props) {
491
533
  (header !== undefined ? 1 : 0),
492
534
  };
493
535
  }
494
- // Pull-to-refresh: when onRefresh is set, build a RefreshControl for the ScrollView's
495
- // refreshControl prop. refreshing is RN-required alongside onRefresh, default false when nullish.
496
- if (onRefresh !== undefined) {
497
- dlog('VirtualizedList wiring RefreshControl (onRefresh provided)');
498
- scrollProps.refreshControl = createElement(RefreshControl, {
536
+ // Pull-to-refresh: when onRefresh is set, the RefreshControl is an ordinary FIRST CHILD on both
537
+ // platforms rather than a prop. The scroll behavior CLAIMS it, so the engine keeps it beside the
538
+ // content view on iOS and inverts the tree on Android; everything after it lands in the content
539
+ // node the behavior built. refreshing is RN-required alongside onRefresh, default false.
540
+ const refreshControl = onRefresh !== undefined
541
+ ? createElement('refresh-control', {
542
+ key: 'refresh-control',
499
543
  refreshing: refreshing ?? false,
500
544
  onRefresh,
501
545
  progressViewOffset,
502
- });
546
+ })
547
+ : undefined;
548
+ if (refreshControl !== undefined) {
549
+ dlog('VirtualizedList wiring RefreshControl (onRefresh provided)');
503
550
  }
504
- // The ScrollView handle (ref) backs animated imperative scrolls via its native command.
505
- return createElement(ScrollView, { ...scrollProps, ref: scrollViewRef }, ...children);
551
+ // The scroll TAG, not a wrapper: the engine builds the content node, carries
552
+ // `contentContainerStyle` onto it and places the refresh control. `ref` hands back the engine
553
+ // node, which is what `buildScrollViewHandle` drives for the imperative scrolls.
554
+ return createElement(horizontal ? 'horizontal-scroll-view' : 'scroll-view', { ...scrollProps, ref: scrollNodeRef }, refreshControl, ...children);
506
555
  }
@@ -42,6 +42,7 @@ export interface ITextProps extends IAccessibilityProps, IAriaProps {
42
42
  numberOfLines?: number;
43
43
  ellipsizeMode?: 'head' | 'middle' | 'tail' | 'clip';
44
44
  selectable?: boolean;
45
+ disabled?: boolean;
45
46
  adjustsFontSizeToFit?: boolean;
46
47
  minimumFontScale?: number;
47
48
  allowFontScaling?: boolean;
@@ -50,5 +51,3 @@ export interface ITextProps extends IAccessibilityProps, IAriaProps {
50
51
  ref?: Ref<IHostInstance>;
51
52
  children?: ReactNode;
52
53
  }
53
- export declare const View = "symbiote-view";
54
- export declare const Text = "symbiote-text";
@@ -1,19 +1,5 @@
1
- // Host primitives exposed to user code. They ARE the intrinsic tags — a capitalized export whose
2
- // value is the tag string — so an app writes `<View>` and React commits a host element directly,
3
- // with no component instance in between. The reconciler maps the tag through
4
- // `descriptorFor` to a Fabric view name at commit.
5
- // `View` and `Text` are the intrinsic TAGS, not components wrapping them. JSX resolves a
6
- // capitalized tag to the value in scope, and a value that is a STRING is a host element to React —
7
- // so `<View/>` compiles to `_jsx('symbiote-view', …)` with no component instance, while
8
- // `JSX.IntrinsicElements['symbiote-view']` still supplies the strict props (a bad prop is TS2322).
9
- //
10
- // The two folds their component bodies used to apply both moved down a layer, which is what let
11
- // the bodies go:
12
- //
13
- // aria / accessibility the engine folds every node in `fabricProps` (accessibility-props.ts)
14
- // id -> nativeID, the renderer folds by spec in `host-config`'s createInstance
15
- // Text's defaults (`foldHostBag`, driven by HOST_PRIMITIVES)
16
- //
17
- // A type annotation rather than `as const`: same literal type, no cast (`ts-js-best-practices`).
18
- export const View = 'symbiote-view';
19
- export const Text = 'symbiote-text';
1
+ // The public PROP TYPES for the two strict intrinsics, and nothing else. There is no component and
2
+ // no capitalized alias: an app writes `<view>` / `<text>`, React commits a host element directly,
3
+ // and `JSX.IntrinsicElements` (jsx-runtime.ts) is what supplies these types to it. The reconciler
4
+ // maps the tag through `descriptorFor` to a Fabric view name at commit.
5
+ export {};
@@ -14,7 +14,7 @@ import { isSymbioteNode, SymbioteSurface, } from '@symbiote-native/engine';
14
14
  import reconciler from '../host-config.js';
15
15
  export function createPortal(children, container, key) {
16
16
  if (!(container instanceof SymbioteSurface) && !isSymbioteNode(container)) {
17
- throw new Error('createPortal target must be an already-mounted host node (e.g. a ref to a rendered <View>) — got something else. Did you forget `.current`/`.value`, or pass a CSS-selector-style string?');
17
+ throw new Error('createPortal target must be an already-mounted host node (e.g. a ref to a rendered <view>) — got something else. Did you forget `.current`/`.value`, or pass a CSS-selector-style string?');
18
18
  }
19
19
  // @ts-expect-error @types/react-reconciler's ReactPortal (containerInfo/implementation) and
20
20
  // react's own ReactPortal (type/props, a ReactElement) are different interfaces describing the
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx } from "@symbiote-native/react/jsx-runtime";
2
2
  // createTunnel - the cross-surface answer to createPortal's same-surface-only scope. A real
3
3
  // createPortal cannot reach across two separate reconciler roots (see
4
4
  // github.com/facebook/react/issues/17147); the fix, same as pmndrs/tunnel-rat, is to not
@@ -1,6 +1,6 @@
1
1
  // The descriptor→element bridge. A render function in @symbiote-native/components returns a
2
2
  // framework-agnostic `Descriptor` tree; this maps it onto React elements. The resulting
3
- // host element (`symbiote-view`, `symbiote-activity-indicator`, …) flows on through the
3
+ // host element (`view`, `activity-indicator`, …) flows on through the
4
4
  // react-reconciler host config → engine → Fabric, exactly like a hand-written JSX host
5
5
  // element.
6
6
  import { createElement } from 'react';
@@ -16,10 +16,9 @@ import { descriptorFor } from '@symbiote-native/components';
16
16
  // A bare intrinsic tag has no wrapper to apply RN's per-primitive prop folds (id -> nativeID,
17
17
  // Text's ellipsizeMode / allowFontScaling defaults), so the renderer is the layer that must.
18
18
  // Shared with every other adapter, driven by the same HOST_PRIMITIVES spec.
19
- import { foldHostBag } from '@symbiote-native/components/fold-host-bag';
20
- // WHICH native view a primitive commits can depend on a prop (TextInput's `multiline`). A lowering
21
- // transform decides that from source text; a bare tag has no transform, so the choice is made here,
22
- // where the runtime value is known. Identity for every primitive that declares no alternative.
19
+ // WHICH native view a primitive commits can depend on a prop (TextInput's `multiline`), and that
20
+ // prop can be a runtime value — so the choice is made here, where it is known. Identity for every
21
+ // primitive that declares no alternative.
23
22
  import { resolveIntrinsicTag } from '@symbiote-native/components/resolve-intrinsic';
24
23
  // React-reserved prop keys that must never reach Fabric. `children` is the element
25
24
  // tree (handled via appendChild). `ref`/`key` are React 19 plain props: the reconciler
@@ -99,17 +98,31 @@ const reconciler = createReconciler({
99
98
  },
100
99
  shouldSetTextContent: () => false,
101
100
  createInstance(type, props, _container, hostContext) {
102
- const descriptor = descriptorFor(resolveIntrinsicTag(type, props));
101
+ // The INTRINSIC tag, kept in a binding rather than inlined, because it is needed twice: once
102
+ // to resolve the Fabric view name and once to hand the engine below. The host-behavior
103
+ // registry is keyed by TAG and never by Fabric name — `pressable` and a plain `View`
104
+ // both resolve to `RCTView`, so keying by the resolved name would put a press machine on every
105
+ // view in the app (`core/engine/src/host-behavior.ts`).
106
+ const tag = resolveIntrinsicTag(type, props);
107
+ const descriptor = descriptorFor(tag);
103
108
  if (hostContext.isInsideText && !descriptor.isText) {
104
- throw new Error(`<${type}> can't be nested inside <Text>`);
109
+ throw new Error(`<${type}> can't be nested inside <text>`);
105
110
  }
106
- const node = createElement(descriptor.component, descriptor.isText);
107
- applyProps(node, foldHostBag(type, props));
111
+ // The third argument is what makes a host behavior reachable AT ALL on this adapter. Dropping
112
+ // it degrades silently: `createElement`'s default is `tag = component`, so the lookup runs
113
+ // against `RCTView` / `RCTImageView`, matches nothing, and every behavior simply never
114
+ // attaches — no throw, no log, and no test goes red, because React ships no `register` entry
115
+ // today and so has never had a behavior to lose. That absence is exactly what hid this: the
116
+ // other four adapters all pass it (vue `renderer/index.ts`, svelte `dom-shim/element.ts`,
117
+ // solid `renderer.ts`, angular `renderer/index.ts`), and React is the one that did not.
118
+ //
119
+ const node = createElement(descriptor.component, descriptor.isText, tag);
120
+ applyProps(node, props);
108
121
  return node;
109
122
  },
110
123
  createTextInstance(text, _container, hostContext) {
111
124
  if (!hostContext.isInsideText) {
112
- throw new Error(`Text string "${text}" must be rendered inside a <Text>`);
125
+ throw new Error(`Text string "${text}" must be rendered inside a <text>`);
113
126
  }
114
127
  return createRawText(text);
115
128
  },
@@ -141,9 +154,7 @@ const reconciler = createReconciler({
141
154
  // an update. The node keeps its identity — an app's ref stays live — and the commit walk
142
155
  // re-creates the Fabric node because its viewName no longer matches the committed one.
143
156
  setNodeComponent(node, descriptorFor(resolveIntrinsicTag(type, newProps)).component);
144
- // BOTH sides folded, or the diff compares a raw `id` against a folded `nativeID` and writes
145
- // the alias twice while never clearing the raw key.
146
- applyUpdate(node, foldHostBag(type, oldProps), foldHostBag(type, newProps));
157
+ applyUpdate(node, oldProps, newProps);
147
158
  },
148
159
  commitTextUpdate(node, _oldText, newText) {
149
160
  setText(node, newText);
package/build/index.d.ts CHANGED
@@ -1,38 +1,32 @@
1
- export type { ISymbioteIntrinsicTag } from './jsx';
2
- export { View, Text } from './components';
1
+ import './register';
2
+ export type { ISymbioteIntrinsicTag } from './jsx-runtime';
3
3
  export type { IViewProps, ITextProps } from './components';
4
4
  export type { IAccessibilityProps, IAriaProps, IAccessibilityRole, IRole, IAccessibilityStateValue, IAccessibilityValue, IAccessibilityActionInfo, } from '@symbiote-native/components';
5
5
  export type { IResponderProps } from './utils/responder-props';
6
- export { Image, setImageSourceResolver } from './components/image';
7
- export type { IImageProps, IImageSource, IImageSourceProp, IResizeMode, IImageSize, IImageCacheStatus, } from './components/image';
8
- export { ImageBackground } from './components/image-background';
9
- export type { IImageBackgroundProps } from './components/image-background';
10
- export { ScrollView } from './components/scroll-view';
11
- export type { IScrollViewProps, IScrollViewHandle, } from './components/scroll-view';
12
- export { TextInput } from './components/text-input';
13
- export type { ITextInputProps, ITextInputHandle, } from './components/text-input';
14
- export { InputAccessoryView } from './components/input-accessory-view';
15
- export type { IInputAccessoryViewProps } from './components/input-accessory-view';
6
+ export { Image } from './modules/image';
7
+ export { setImageSourceResolver } from '@symbiote-native/components';
8
+ export type { IImageProps } from './components/image/image-props';
9
+ export type { IImageSource, IImageSourceProp, IResizeMode, IImageSize, IImageCacheStatus, } from '@symbiote-native/components';
10
+ export type { IImageBackgroundProps } from './components/image-background-props';
11
+ export type { IScrollViewProps, IScrollViewHandle, } from './components/scroll-view/scroll-view-props';
12
+ export type { ITextInputProps } from './components/text-input/text-input-props';
13
+ export type { ITextInputHandle, ITextInputChangeEvent, } from '@symbiote-native/components';
14
+ export type { IInputAccessoryViewProps } from './components/input-accessory-view-props';
16
15
  export { KeyboardAvoidingView } from './components/keyboard-avoiding-view';
17
16
  export type { IKeyboardAvoidingViewProps, IKeyboardAvoidingBehavior, } from './components/keyboard-avoiding-view';
18
17
  export { StatusBar } from './modules/status-bar';
19
18
  export type { IStatusBarProps, IStatusBarStyle } from './modules/status-bar';
20
- export { Switch } from './components/switch';
21
- export type { ISwitchProps, ISwitchTrackColor } from './components/switch';
22
- export { ActivityIndicator } from './components/activity-indicator';
23
- export type { IActivityIndicatorProps } from './components/activity-indicator';
24
- export { SafeAreaView } from './components/safe-area-view';
25
- export type { ISafeAreaViewProps } from './components/safe-area-view';
26
- export { RefreshControl } from './components/refresh-control';
27
- export type { IRefreshControlProps } from './components/refresh-control';
19
+ export type { ISwitchProps } from './components/switch/switch-props';
20
+ export type { ISwitchTrackColor, ISwitchChangeEvent, } from '@symbiote-native/components';
21
+ export type { IActivityIndicatorProps } from './components/activity-indicator-props';
22
+ export type { ISafeAreaViewProps } from './components/safe-area-view-props';
23
+ export type { IRefreshControlProps } from './components/refresh-control-props';
28
24
  export { Modal } from './components/modal';
29
25
  export type { IModalProps, IModalAnimationType, IModalPresentationStyle, IModalOrientation, } from './components/modal';
30
- export { Pressable } from './components/pressable';
31
- export type { IPressableProps, IPressState } from './components/pressable';
32
- export { TouchableOpacity, TouchableHighlight, TouchableWithoutFeedback, } from './components/touchable';
33
- export type { ITouchableOpacityProps, ITouchableHighlightProps, ITouchableWithoutFeedbackProps, } from './components/touchable';
34
- export { Button } from './components/button';
35
- export type { IButtonProps } from './components/button';
26
+ export type { IPressableProps, IPressState, } from './components/pressable/pressable-props';
27
+ export type { ITouchableOpacityProps, ITouchableHighlightProps, } from './components/touchable/touchable-props';
28
+ export type { ITouchableWithoutFeedbackProps } from './components/touchable-without-feedback/touchable-without-feedback-props';
29
+ export type { IButtonProps } from './components/button-props';
36
30
  export { FlatList } from './components/flat-list';
37
31
  export type { IFlatListProps, IFlatListHandle } from './components/flat-list';
38
32
  export { SectionList } from './components/section-list';
@@ -68,7 +62,8 @@ export { PanResponder } from '@symbiote-native/engine';
68
62
  export type { IPanResponderGestureState, IPanResponderCallbacks, IGestureResponderHandlers, IPanResponderInstance, } from '@symbiote-native/engine';
69
63
  export { InteractionManager } from '@symbiote-native/engine';
70
64
  export type { IInteractionEvent, ISimpleTask, IPromiseTask, ITask, IHandle, } from '@symbiote-native/engine';
71
- export { TouchableNativeFeedback } from './components/touchable-native-feedback';
72
- export type { ITouchableNativeFeedbackProps, INativeFeedbackBackground, IThemeAttrBackground, IRippleBackground, } from './components/touchable-native-feedback';
65
+ export { TouchableNativeFeedback } from '@symbiote-native/components';
66
+ export type { INativeFeedbackBackground, IThemeAttrBackground, IRippleBackground, } from '@symbiote-native/components';
67
+ export type { ITouchableNativeFeedbackProps } from './components/touchable-native-feedback/touchable-native-feedback-props';
73
68
  export type { ISymbioteEvent, ISymbioteNode, IRootTag, } from '@symbiote-native/engine';
74
69
  export type { ICellLayout, ISeparatorProps, ISeparators, IModalOrientationChangeEvent, IPressableAndroidRippleConfig, IEnterKeyHint, IInputMode, ISubmitBehavior, ITextInputSelection, IImageStatics, } from '@symbiote-native/components';
package/build/index.js CHANGED
@@ -1,22 +1,19 @@
1
1
  // @symbiote-native/react: a react-reconciler host config (mutation mode) over
2
2
  // @symbiote-native/engine. React is a known-good driver: it proves the native pipe
3
3
  // and the shared clone-on-write engine before any non-React adapter has to.
4
- export { View, Text } from './components.js';
5
- export { Image, setImageSourceResolver } from './components/image/index.js';
6
- export { ImageBackground } from './components/image-background/index.js';
7
- export { ScrollView } from './components/scroll-view';
8
- export { TextInput } from './components/text-input/index.js';
9
- export { InputAccessoryView } from './components/input-accessory-view/index.js';
4
+ // Bare side-effect import, deliberately NOT a re-export and deliberately not beside one of the
5
+ // same specifier: it installs the engine's host behaviors, which is what makes a bare
6
+ // `<pressable>` / `<text-input>` / `<switch>` carry its machine. Any other shape is dropped by
7
+ // Metro's inlineRequires in a release build — see register.ts.
8
+ import './register.js';
9
+ // `Image` is a TAG — `<image>` — and the name now carries only RN's STATICS
10
+ // (`Image.getSize(...)`), an imperative API with no view. The wrapper's whole source/style
11
+ // translation runs on the tag as the engine's `registerImageBehavior`.
12
+ export { Image } from './modules/image/index.js';
13
+ export { setImageSourceResolver } from '@symbiote-native/components';
10
14
  export { KeyboardAvoidingView } from './components/keyboard-avoiding-view/index.js';
11
15
  export { StatusBar } from './modules/status-bar';
12
- export { Switch } from './components/switch';
13
- export { ActivityIndicator } from './components/activity-indicator';
14
- export { SafeAreaView } from './components/safe-area-view/index.js';
15
- export { RefreshControl } from './components/refresh-control/index.js';
16
16
  export { Modal } from './components/modal/index.js';
17
- export { Pressable } from './components/pressable/index.js';
18
- export { TouchableOpacity, TouchableHighlight, TouchableWithoutFeedback, } from './components/touchable/index.js';
19
- export { Button } from './components/button.js';
20
17
  export { FlatList } from './components/flat-list/index.js';
21
18
  export { SectionList } from './components/section-list/index.js';
22
19
  export { VirtualizedSectionList } from './components/virtualized-section-list/index.js';
@@ -74,4 +71,7 @@ export { InteractionManager } from '@symbiote-native/engine';
74
71
  // Android-only surface (the second-platform pass): a thin JS shim over an Android Fabric view,
75
72
  // inert on iOS (no native view -> degrades to a plain container). The Android-only MODULES
76
73
  // (ToastAndroid, PermissionsAndroid, BackHandler) sit in the engine block above.
77
- export { TouchableNativeFeedback } from './components/touchable-native-feedback/index.js';
74
+ // `TouchableNativeFeedback` is now RN's STATIC NAMESPACE, not a component: the element is the tag
75
+ // `<touchable-native-feedback>`, which commits no node of its own and clones onto its single child.
76
+ // `TouchableNativeFeedback.Ripple(…)` / `.SelectableBackground(…)` are unchanged.
77
+ export { TouchableNativeFeedback } from '@symbiote-native/components';
@@ -0,0 +1,3 @@
1
+ export { jsxDEV } from 'react/jsx-dev-runtime';
2
+ export { Fragment } from 'react/jsx-runtime';
3
+ export type { JSX } from './jsx-runtime';
@@ -0,0 +1,10 @@
1
+ // The dev twin of `./jsx-runtime`, and it has to be its own file rather than an alias of it.
2
+ //
3
+ // TypeScript resolves the namespace from `<jsxImportSource>/jsx-dev-runtime` whenever `jsx` is
4
+ // `react-jsxdev`, and Babel's automatic runtime imports `jsxDEV` — not `jsx`/`jsxs` — from that
5
+ // same path in a development build. Solid points both subpaths at one file because its emitted
6
+ // runtime is empty either way; ours carries real factories, so the two entries export different
7
+ // names and pointing them at one file would leave a dev build importing a `jsxDEV` that is not
8
+ // there.
9
+ export { jsxDEV } from 'react/jsx-dev-runtime';
10
+ export { Fragment } from 'react/jsx-runtime';
@@ -0,0 +1 @@
1
+ export declare const JSX_NAMESPACE_CHECK: number;
@@ -0,0 +1,7 @@
1
+ import { jsx as _jsx } from "@symbiote-native/react/jsx-runtime";
2
+ const strictIntrinsicAcceptsItsOwnProp = (_jsx("view", { testID: "jsx-namespace-check" }));
3
+ const strictIntrinsicRejectsAnUnknownProp = (_jsx("view", { definitelyNotAViewProp: 1 }));
4
+ export const JSX_NAMESPACE_CHECK = [
5
+ strictIntrinsicAcceptsItsOwnProp,
6
+ strictIntrinsicRejectsAnUnknownProp,
7
+ ].length;
@@ -0,0 +1,62 @@
1
+ import type { ICrossTypedIntrinsics } from '@symbiote-native/components';
2
+ import type { Key } from 'react';
3
+ import type { IViewProps, ITextProps } from './components';
4
+ import type { IPressableProps } from './components/pressable/pressable-props';
5
+ import type { IButtonProps } from './components/button-props';
6
+ import type { IImageProps } from './components/image/image-props';
7
+ import type { IImageBackgroundProps } from './components/image-background-props';
8
+ import type { IInputAccessoryViewProps } from './components/input-accessory-view-props';
9
+ import type { IRefreshControlProps } from './components/refresh-control-props';
10
+ import type { ISafeAreaViewProps } from './components/safe-area-view-props';
11
+ import type { ITouchableNativeFeedbackProps } from './components/touchable-native-feedback/touchable-native-feedback-props';
12
+ import type { ITouchableOpacityProps, ITouchableHighlightProps } from './components/touchable/touchable-props';
13
+ import type { ITouchableWithoutFeedbackProps } from './components/touchable-without-feedback/touchable-without-feedback-props';
14
+ import type { IScrollViewProps } from './components/scroll-view/scroll-view-props';
15
+ import type { ISwitchProps } from './components/switch/switch-props';
16
+ import type { ITextInputProps } from './components/text-input/text-input-props';
17
+ import type { IModalProps } from './components/modal';
18
+ import type { IActivityIndicatorProps } from './components/activity-indicator-props';
19
+ export { Fragment, jsx, jsxs } from 'react/jsx-runtime';
20
+ interface IHostProps {
21
+ style?: unknown;
22
+ children?: import('react').ReactNode;
23
+ [key: string]: unknown;
24
+ }
25
+ export interface ICrossedPrimitiveProps {
26
+ view: IViewProps;
27
+ text: ITextProps;
28
+ pressable: IPressableProps;
29
+ button: IButtonProps;
30
+ image: IImageProps;
31
+ 'image-background': IImageBackgroundProps;
32
+ 'input-accessory-view': IInputAccessoryViewProps;
33
+ 'refresh-control': IRefreshControlProps;
34
+ 'safe-area-view': ISafeAreaViewProps;
35
+ 'touchable-native-feedback': ITouchableNativeFeedbackProps;
36
+ 'touchable-opacity': ITouchableOpacityProps;
37
+ 'touchable-highlight': ITouchableHighlightProps;
38
+ 'touchable-without-feedback': ITouchableWithoutFeedbackProps;
39
+ 'scroll-view': IScrollViewProps;
40
+ switch: ISwitchProps;
41
+ 'text-input': ITextInputProps;
42
+ modal: IModalProps;
43
+ 'activity-indicator': IActivityIndicatorProps;
44
+ }
45
+ type IWithKey<Props> = Props & {
46
+ key?: Key | null;
47
+ };
48
+ export declare namespace JSX {
49
+ type ElementType = import('react').JSX.ElementType;
50
+ type Element = import('react').JSX.Element;
51
+ type ElementClass = import('react').JSX.ElementClass;
52
+ type ElementAttributesProperty = import('react').JSX.ElementAttributesProperty;
53
+ type ElementChildrenAttribute = import('react').JSX.ElementChildrenAttribute;
54
+ type LibraryManagedAttributes<C, P> = import('react').JSX.LibraryManagedAttributes<C, P>;
55
+ type IntrinsicAttributes = import('react').JSX.IntrinsicAttributes;
56
+ type IntrinsicClassAttributes<T> = import('react').JSX.IntrinsicClassAttributes<T>;
57
+ interface IntrinsicElements extends ICrossTypedIntrinsics<IHostProps, {
58
+ [K in keyof ICrossedPrimitiveProps]: IWithKey<ICrossedPrimitiveProps[K]>;
59
+ }> {
60
+ }
61
+ }
62
+ export type ISymbioteIntrinsicTag = keyof JSX.IntrinsicElements;
@@ -0,0 +1 @@
1
+ export { Fragment, jsx, jsxs } from 'react/jsx-runtime';