@symbiote-native/react 0.4.0 → 2.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 (102) hide show
  1. package/README.md +32 -16
  2. package/build/components/activity-indicator-props.d.ts +4 -0
  3. package/build/components/activity-indicator-props.js +1 -0
  4. package/build/components/{button.d.ts → button-props.d.ts} +0 -2
  5. package/build/components/button-props.js +1 -0
  6. package/build/components/flat-list/index.js +2 -2
  7. package/build/components/image/image-props.d.ts +4 -0
  8. package/build/components/image/image-props.js +1 -0
  9. package/build/components/image-background-props.d.ts +9 -0
  10. package/build/components/image-background-props.js +1 -0
  11. package/build/components/input-accessory-view-props.d.ts +10 -0
  12. package/build/components/input-accessory-view-props.js +1 -0
  13. package/build/components/keyboard-avoiding-view/index.js +4 -4
  14. package/build/components/modal/index.js +3 -3
  15. package/build/components/pressable/{index.d.ts → pressable-props.d.ts} +4 -4
  16. package/build/components/pressable/pressable-props.js +1 -0
  17. package/build/components/{refresh-control/index.d.ts → refresh-control-props.d.ts} +3 -3
  18. package/build/components/refresh-control-props.js +1 -0
  19. package/build/components/safe-area-view-props.d.ts +11 -0
  20. package/build/components/safe-area-view-props.js +1 -0
  21. package/build/components/scroll-view/{shared.d.ts → scroll-view-props.d.ts} +4 -26
  22. package/build/components/scroll-view/scroll-view-props.js +1 -0
  23. package/build/components/switch/switch-props.d.ts +4 -0
  24. package/build/components/switch/switch-props.js +1 -0
  25. package/build/components/text-input/text-input-props.d.ts +4 -0
  26. package/build/components/text-input/text-input-props.js +1 -0
  27. package/build/components/touchable/touchable-props.d.ts +17 -0
  28. package/build/components/touchable/touchable-props.js +1 -0
  29. package/build/components/touchable-native-feedback/touchable-native-feedback-props.d.ts +8 -0
  30. package/build/components/touchable-native-feedback/touchable-native-feedback-props.js +1 -0
  31. package/build/components/touchable-without-feedback/touchable-without-feedback-props.d.ts +6 -0
  32. package/build/components/touchable-without-feedback/touchable-without-feedback-props.js +1 -0
  33. package/build/components/virtualized-list/index.js +83 -38
  34. package/build/components.d.ts +3 -4
  35. package/build/components.js +5 -17
  36. package/build/create-portal/index.js +1 -1
  37. package/build/create-tunnel/index.js +1 -1
  38. package/build/descriptor-to-react/index.js +1 -1
  39. package/build/host-config.d.ts +2 -2
  40. package/build/host-config.js +51 -10
  41. package/build/host-instance.js +1 -1
  42. package/build/index.d.ts +23 -27
  43. package/build/index.js +14 -14
  44. package/build/jsx-dev-runtime.d.ts +3 -0
  45. package/build/jsx-dev-runtime.js +10 -0
  46. package/build/jsx-namespace.type-check.d.ts +1 -0
  47. package/build/jsx-namespace.type-check.js +7 -0
  48. package/build/jsx-runtime.d.ts +62 -0
  49. package/build/jsx-runtime.js +1 -0
  50. package/build/modules/animated/create-animated-component.d.ts +2 -1
  51. package/build/modules/animated/create-animated-component.js +7 -1
  52. package/build/modules/animated/index.d.ts +0 -1
  53. package/build/modules/animated/index.js +16 -22
  54. package/build/modules/image/index.d.ts +3 -0
  55. package/build/modules/image/index.js +14 -0
  56. package/build/register.d.ts +1 -0
  57. package/build/register.js +15 -0
  58. package/build/render.js +2 -4
  59. package/metro-css-parser.cjs +2 -1
  60. package/package.json +28 -8
  61. package/build/components/activity-indicator/index.android.d.ts +0 -3
  62. package/build/components/activity-indicator/index.android.js +0 -17
  63. package/build/components/activity-indicator/index.d.ts +0 -1
  64. package/build/components/activity-indicator/index.ios.d.ts +0 -3
  65. package/build/components/activity-indicator/index.ios.js +0 -16
  66. package/build/components/activity-indicator/index.js +0 -4
  67. package/build/components/activity-indicator/shared.d.ts +0 -6
  68. package/build/components/activity-indicator/shared.js +0 -22
  69. package/build/components/button.js +0 -42
  70. package/build/components/image/index.d.ts +0 -9
  71. package/build/components/image/index.js +0 -28
  72. package/build/components/image-background/index.d.ts +0 -10
  73. package/build/components/image-background/index.js +0 -46
  74. package/build/components/input-accessory-view/index.d.ts +0 -11
  75. package/build/components/input-accessory-view/index.js +0 -19
  76. package/build/components/pressable/index.js +0 -100
  77. package/build/components/refresh-control/index.js +0 -21
  78. package/build/components/safe-area-view/index.d.ts +0 -11
  79. package/build/components/safe-area-view/index.js +0 -18
  80. package/build/components/scroll-view/index.android.d.ts +0 -3
  81. package/build/components/scroll-view/index.android.js +0 -61
  82. package/build/components/scroll-view/index.d.ts +0 -1
  83. package/build/components/scroll-view/index.ios.d.ts +0 -3
  84. package/build/components/scroll-view/index.ios.js +0 -29
  85. package/build/components/scroll-view/index.js +0 -4
  86. package/build/components/scroll-view/shared.js +0 -240
  87. package/build/components/scroll-view/sticky-header.d.ts +0 -18
  88. package/build/components/scroll-view/sticky-header.js +0 -227
  89. package/build/components/switch/index.android.d.ts +0 -3
  90. package/build/components/switch/index.android.js +0 -18
  91. package/build/components/switch/index.d.ts +0 -1
  92. package/build/components/switch/index.ios.d.ts +0 -3
  93. package/build/components/switch/index.ios.js +0 -16
  94. package/build/components/switch/index.js +0 -6
  95. package/build/components/switch/shared.d.ts +0 -9
  96. package/build/components/switch/shared.js +0 -60
  97. package/build/components/text-input/index.d.ts +0 -50
  98. package/build/components/text-input/index.js +0 -172
  99. package/build/components/touchable/index.d.ts +0 -22
  100. package/build/components/touchable/index.js +0 -165
  101. package/build/components/touchable-native-feedback/index.d.ts +0 -18
  102. package/build/components/touchable-native-feedback/index.js +0 -32
@@ -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_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, 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;
@@ -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})`);
@@ -289,18 +311,20 @@ export function VirtualizedList(props) {
289
311
  dispatch({ kind: 'scroll-to-end', animated: params?.animated ?? true });
290
312
  },
291
313
  flashScrollIndicators: () => {
292
- scrollViewRef.current?.flashScrollIndicators?.();
314
+ scrollHandle.flashScrollIndicators?.();
293
315
  },
294
- getNativeScrollRef: () => scrollViewRef.current,
295
- getScrollableNode: () => scrollViewRef.current,
296
- getScrollResponder: () => scrollViewRef.current,
297
- getScrollNode: () => scrollViewRef.current?.getScrollNode() ?? null,
316
+ // Null until the tag commits, so these keep RN's "no scroll view yet" answer rather than
317
+ // handing back a handle that would silently no-op.
318
+ getNativeScrollRef: () => scrollNodeRef.current !== null ? scrollHandle : null,
319
+ getScrollableNode: () => scrollNodeRef.current !== null ? scrollHandle : null,
320
+ getScrollResponder: () => scrollNodeRef.current !== null ? scrollHandle : null,
321
+ getScrollNode: () => scrollNodeRef.current,
298
322
  // Manual trigger for RN's recordInteraction: flip the interaction flag so
299
323
  // waitForInteraction viewability configs start reporting.
300
324
  recordInteraction: () => {
301
325
  dispatch({ kind: 'record-interaction' });
302
326
  },
303
- }), [dispatch]);
327
+ }), [dispatch, scrollHandle]);
304
328
  // After-commit pass: run the deferred effects (batch fill, edge-reached, viewability,
305
329
  // initial-scroll, MVCP) in a LAYOUT effect so MVCP's shift lands before paint. Re-runs only when
306
330
  // the windowing signature changed — the same dedup key every adapter shares.
@@ -328,15 +352,14 @@ export function VirtualizedList(props) {
328
352
  const stickySet = stickyHeaderIndices !== undefined
329
353
  ? new Set(stickyHeaderIndices)
330
354
  : undefined;
331
- let renderedStickyIndices = [];
332
355
  const header = resolveElement(ListHeaderComponent);
333
356
  if (header !== undefined) {
334
- children.push(createElement('symbiote-view', { key: 'list-header' }, header));
357
+ children.push(createElement('view', { key: 'list-header' }, header));
335
358
  }
336
359
  if (count === FIRST_INDEX) {
337
360
  const empty = resolveElement(ListEmptyComponent);
338
361
  if (empty !== undefined) {
339
- children.push(createElement('symbiote-view', { key: 'list-empty' }, empty));
362
+ children.push(createElement('view', { key: 'list-empty' }, empty));
340
363
  }
341
364
  }
342
365
  else {
@@ -352,9 +375,14 @@ export function VirtualizedList(props) {
352
375
  stickyIndices: stickySet,
353
376
  hasHeader: header !== undefined,
354
377
  });
355
- renderedStickyIndices = plan.stickyChildPositions;
378
+ // `plan.stickyChildPositions` is deliberately NOT read. It was the input to the wrapper's own
379
+ // JS sticky pass, and the behavior honours `stickyHeaderIndices` by numbering the OWNER's paint
380
+ // children — which a windowed list cannot supply: it paints a header, a spacer and a slice, so
381
+ // data index 3 is almost never paint child 3, and the positions would have to be recomputed
382
+ // every time the window slides. The cells that should pin carry the `sticky-header` TAG
383
+ // instead, which pins by document order and survives windowing.
356
384
  if (plan.leadingExtent > EMPTY_OFFSET) {
357
- children.push(createElement('symbiote-view', {
385
+ children.push(createElement('view', {
358
386
  key: 'spacer-leading',
359
387
  style: horizontal
360
388
  ? { width: plan.leadingExtent }
@@ -374,7 +402,11 @@ export function VirtualizedList(props) {
374
402
  index: forcedIndex,
375
403
  separators: makeSeparators(forcedIndex),
376
404
  });
377
- children.push(createElement('symbiote-view', {
405
+ children.push(createElement(
406
+ // Sticky by construction — `forcedStickyCell` exists only for an index in `stickySet` —
407
+ // and the SAME tag the in-window branch below uses, so a cell crossing between the two
408
+ // keeps its element type as well as its key and never remounts.
409
+ 'sticky-header', {
378
410
  key: `cell-${plan.forcedStickyCell.key}`,
379
411
  onLayout: makeCellMeasure(forcedIndex),
380
412
  style: inverted
@@ -385,7 +417,7 @@ export function VirtualizedList(props) {
385
417
  }, forcedCell));
386
418
  }
387
419
  if (plan.gapExtent > EMPTY_OFFSET) {
388
- children.push(createElement('symbiote-view', {
420
+ children.push(createElement('view', {
389
421
  key: 'spacer-gap',
390
422
  style: horizontal
391
423
  ? { width: plan.gapExtent }
@@ -420,7 +452,13 @@ export function VirtualizedList(props) {
420
452
  : undefined;
421
453
  // Wrap each cell in a measuring View. When inverted, each cell carries the counter-flip so
422
454
  // its content reads upright inside the flipped content container.
423
- children.push(createElement('symbiote-view', {
455
+ //
456
+ // A cell the app flagged sticky is wrapped in the `sticky-header` TAG instead — same
457
+ // position, same key, same `onLayout` (the behavior forwards it rather than replacing it),
458
+ // and the engine finds this scroll view by walking up. Nothing here computes a child index:
459
+ // the collision point comes from the owner's DOCUMENT order, which is the one form that
460
+ // survives windowing.
461
+ children.push(createElement(stickySet?.has(planCell.index) === true ? 'sticky-header' : 'view', {
424
462
  key: `cell-${planCell.key}`,
425
463
  onLayout: makeCellMeasure(planCell.index),
426
464
  style: inverted
@@ -431,7 +469,7 @@ export function VirtualizedList(props) {
431
469
  }, cell, separator));
432
470
  }
433
471
  if (plan.trailingExtent > EMPTY_OFFSET) {
434
- children.push(createElement('symbiote-view', {
472
+ children.push(createElement('view', {
435
473
  key: 'spacer-trailing',
436
474
  style: horizontal
437
475
  ? { width: plan.trailingExtent }
@@ -441,7 +479,7 @@ export function VirtualizedList(props) {
441
479
  }
442
480
  const footer = resolveElement(ListFooterComponent);
443
481
  if (footer !== undefined) {
444
- children.push(createElement('symbiote-view', { key: 'list-footer' }, footer));
482
+ children.push(createElement('view', { key: 'list-footer' }, footer));
445
483
  }
446
484
  // A horizontal list pins the content container to the full row width so the row overflows for
447
485
  // iOS to scroll. The inversion flip rides ONLY the outer ScrollView style and each cell, never
@@ -458,10 +496,13 @@ export function VirtualizedList(props) {
458
496
  ...accessibilityRest,
459
497
  style: resolvedStyle,
460
498
  contentContainerStyle: resolvedContentContainerStyle,
461
- horizontal,
462
499
  onScroll,
463
500
  onLayout: onViewportLayout,
464
501
  };
502
+ // `horizontal` is NOT in the bag: the axis is the TAG (`horizontal-scroll-view`), because Android
503
+ // scrolls the two axes with different native ViewManagers. The behavior's own fold deletes the
504
+ // prop and rewrites it from the tag, so passing it would be at best redundant and at worst a
505
+ // contradiction it has to warn about.
465
506
  if (onScrollBeginDrag !== undefined)
466
507
  scrollProps.onScrollBeginDrag = onScrollBeginDrag;
467
508
  if (onScrollEndDrag !== undefined)
@@ -479,9 +520,6 @@ export function VirtualizedList(props) {
479
520
  // A pending imperative/initial scroll rides down as contentOffset (fresh identity each push).
480
521
  if (commandedOffset !== undefined)
481
522
  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
523
  // Forward maintainVisibleContentPosition to the native ScrollView so it anchors in-window cells.
486
524
  // minIndexForVisible is bumped by 1 when a ListHeaderComponent occupies child 0.
487
525
  if (maintainVisibleContentPosition !== undefined) {
@@ -491,16 +529,23 @@ export function VirtualizedList(props) {
491
529
  (header !== undefined ? 1 : 0),
492
530
  };
493
531
  }
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, {
532
+ // Pull-to-refresh: when onRefresh is set, the RefreshControl is an ordinary FIRST CHILD on both
533
+ // platforms rather than a prop. The scroll behavior CLAIMS it, so the engine keeps it beside the
534
+ // content view on iOS and inverts the tree on Android; everything after it lands in the content
535
+ // node the behavior built. refreshing is RN-required alongside onRefresh, default false.
536
+ const refreshControl = onRefresh !== undefined
537
+ ? createElement('refresh-control', {
538
+ key: 'refresh-control',
499
539
  refreshing: refreshing ?? false,
500
540
  onRefresh,
501
541
  progressViewOffset,
502
- });
542
+ })
543
+ : undefined;
544
+ if (refreshControl !== undefined) {
545
+ dlog('VirtualizedList wiring RefreshControl (onRefresh provided)');
503
546
  }
504
- // The ScrollView handle (ref) backs animated imperative scrolls via its native command.
505
- return createElement(ScrollView, { ...scrollProps, ref: scrollViewRef }, ...children);
547
+ // The scroll TAG, not a wrapper: the engine builds the content node, carries
548
+ // `contentContainerStyle` onto it and places the refresh control. `ref` hands back the engine
549
+ // node, which is what `buildScrollViewHandle` drives for the imperative scrolls.
550
+ return createElement(horizontal ? 'horizontal-scroll-view' : 'scroll-view', { ...scrollProps, ref: scrollNodeRef }, refreshControl, ...children);
506
551
  }
@@ -1,7 +1,7 @@
1
- import { type FC, type Ref, type ReactNode } from 'react';
1
+ import type { Ref, ReactNode } from 'react';
2
2
  import type { ISymbioteEvent } from '@symbiote-native/engine';
3
3
  import type { IHostInstance } from './host-instance';
4
- import { type IAccessibilityProps, type IAriaProps } from '@symbiote-native/components';
4
+ import type { IAccessibilityProps, IAriaProps } from '@symbiote-native/components';
5
5
  import type { IResponderProps } from './utils/responder-props';
6
6
  import type { IStyleProp, ITextStyle, IViewStyle } from './utils/styles';
7
7
  export interface IViewProps extends IAccessibilityProps, IAriaProps, IResponderProps {
@@ -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: FC<IViewProps>;
54
- export declare const Text: FC<ITextProps>;
@@ -1,17 +1,5 @@
1
- // Host primitives exposed to user code. They are thin wrappers that produce the
2
- // lowercase host elements the reconciler understands (`view` / `text`); the
3
- // reconciler maps those to shared's mutation API, which resolves them to Fabric
4
- // view names at commit.
5
- import { createElement } from 'react';
6
- import { resolveAccessibilityProps, resolveTextProps, } from '@symbiote-native/components';
7
- // RN's modern `id` is just a W3C-named alias for `nativeID`: View.js copies it over
8
- // (`processedProps.nativeID = id`), so `id` wins when both are set. We fold it here and
9
- // blank the alias so a raw `id` never reaches Fabric (every non-function prop passes
10
- // through to the slot otherwise).
11
- function resolveId({ id, ...rest }) {
12
- if (id === undefined)
13
- return rest;
14
- return { ...rest, nativeID: id };
15
- }
16
- export const View = props => createElement('symbiote-view', resolveAccessibilityProps(resolveId(props)));
17
- export const Text = props => createElement('symbiote-text', resolveTextProps(resolveAccessibilityProps(props)));
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';
@@ -1,7 +1,7 @@
1
1
  import createReconciler from 'react-reconciler';
2
2
  import { SymbioteSurface, type ISymbioteNode } from '@symbiote-native/engine';
3
- import { type IHostInstance } from './host-instance';
4
3
  type IContainer = SymbioteSurface | ISymbioteNode;
5
4
  export declare function withDiscretePriority(run: () => void): void;
6
- declare const reconciler: createReconciler.Reconciler<IContainer, ISymbioteNode, ISymbioteNode, never, unknown, IHostInstance>;
5
+ declare const reconciler: createReconciler.Reconciler<IContainer, ISymbioteNode, ISymbioteNode, never, unknown, ISymbioteNode>;
6
+ export declare function flushExternalUpdate(run: () => void): void;
7
7
  export default reconciler;
@@ -4,7 +4,7 @@
4
4
  // canary: a known-good driver exercising shared end to end.
5
5
  import createReconciler from 'react-reconciler';
6
6
  import { createContext } from 'react';
7
- import { appendChild, createElement, createRawText, insertBefore, removeChild, routeProp, setNodeHidden, setText, SymbioteSurface, } from '@symbiote-native/engine';
7
+ import { appendChild, createElement, createRawText, insertBefore, removeChild, routeProp, setNodeComponent, setNodeHidden, setText, SymbioteSurface, } from '@symbiote-native/engine';
8
8
  import { DefaultEventPriority, DiscreteEventPriority, NoEventPriority, } from './reconciler-constants.js';
9
9
  import { toPublicInstance } from './host-instance.js';
10
10
  // Intrinsic JSX type -> Fabric component name. The name table + resolver live once in
@@ -13,6 +13,14 @@ import { toPublicInstance } from './host-instance.js';
13
13
  // Adding a primitive is one entry in each name table there, plus its thin
14
14
  // component in components.ts: no host-config logic per primitive.
15
15
  import { descriptorFor } from '@symbiote-native/components';
16
+ // A bare intrinsic tag has no wrapper to apply RN's per-primitive prop folds (id -> nativeID,
17
+ // Text's ellipsizeMode / allowFontScaling defaults), so the renderer is the layer that must.
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.
23
+ import { resolveIntrinsicTag } from '@symbiote-native/components/resolve-intrinsic';
16
24
  // React-reserved prop keys that must never reach Fabric. `children` is the element
17
25
  // tree (handled via appendChild). `ref`/`key` are React 19 plain props: the reconciler
18
26
  // reads `props.ref` for commitAttachRef but does NOT strip it from the props it hands a
@@ -47,8 +55,9 @@ function isSurfaceContainer(container) {
47
55
  return container instanceof SymbioteSurface;
48
56
  }
49
57
  let currentUpdatePriority = NoEventPriority;
50
- // Run an externally-triggered update (a native event) at discrete priority so it
51
- // lands on the sync lane and flushSyncWork paints it immediately.
58
+ // Run the callback at discrete priority. Native events and component-owned timers both enter
59
+ // React outside its render loop; callers pair this with flushExternalUpdate when the result must
60
+ // paint synchronously.
52
61
  export function withDiscretePriority(run) {
53
62
  const previous = currentUpdatePriority;
54
63
  currentUpdatePriority = DiscreteEventPriority;
@@ -90,17 +99,35 @@ const reconciler = createReconciler({
90
99
  },
91
100
  shouldSetTextContent: () => false,
92
101
  createInstance(type, props, _container, hostContext) {
93
- const descriptor = descriptorFor(type);
102
+ // The INTRINSIC tag, kept in a binding rather than inlined, because it is needed twice: once
103
+ // to resolve the Fabric view name and once to hand the engine below. The host-behavior
104
+ // registry is keyed by TAG and never by Fabric name — `pressable` and a plain `View`
105
+ // both resolve to `RCTView`, so keying by the resolved name would put a press machine on every
106
+ // view in the app (`core/engine/src/host-behavior.ts`).
107
+ const tag = resolveIntrinsicTag(type, props);
108
+ const descriptor = descriptorFor(tag);
94
109
  if (hostContext.isInsideText && !descriptor.isText) {
95
- throw new Error(`<${type}> can't be nested inside <Text>`);
110
+ throw new Error(`<${type}> can't be nested inside <text>`);
96
111
  }
97
- const node = createElement(descriptor.component, descriptor.isText);
98
- applyProps(node, props);
112
+ // The third argument is what makes a host behavior reachable AT ALL on this adapter. Dropping
113
+ // it degrades silently: `createElement`'s default is `tag = component`, so the lookup runs
114
+ // against `RCTView` / `RCTImageView`, matches nothing, and every behavior simply never
115
+ // attaches — no throw, no log, and no test goes red, because React ships no `register` entry
116
+ // today and so has never had a behavior to lose. That absence is exactly what hid this: the
117
+ // other four adapters all pass it (vue `renderer/index.ts`, svelte `dom-shim/element.ts`,
118
+ // solid `renderer.ts`, angular `renderer/index.ts`), and React is the one that did not.
119
+ //
120
+ // Inert on landing and deliberately so — nothing registers a behavior under a tag React emits.
121
+ // The `-managed` split is what keeps it inert once something does: React's TextInput and
122
+ // Switch wrappers emit `text-input-managed` / `switch-managed`, so a machine
123
+ // registered on the lowered tag cannot also attach to a wrapper-built node and run twice.
124
+ const node = createElement(descriptor.component, descriptor.isText, tag);
125
+ applyProps(node, foldHostBag(type, props));
99
126
  return node;
100
127
  },
101
128
  createTextInstance(text, _container, hostContext) {
102
129
  if (!hostContext.isInsideText) {
103
- throw new Error(`Text string "${text}" must be rendered inside a <Text>`);
130
+ throw new Error(`Text string "${text}" must be rendered inside a <text>`);
104
131
  }
105
132
  return createRawText(text);
106
133
  },
@@ -127,8 +154,14 @@ const reconciler = createReconciler({
127
154
  removeChild(container, child);
128
155
  },
129
156
  finalizeInitialChildren: () => false,
130
- commitUpdate(node, _type, oldProps, newProps) {
131
- applyUpdate(node, oldProps, newProps);
157
+ commitUpdate(node, type, oldProps, newProps) {
158
+ // A primitive whose native view depends on a prop (TextInput's `multiline`) can change view on
159
+ // an update. The node keeps its identity — an app's ref stays live — and the commit walk
160
+ // re-creates the Fabric node because its viewName no longer matches the committed one.
161
+ setNodeComponent(node, descriptorFor(resolveIntrinsicTag(type, newProps)).component);
162
+ // BOTH sides folded, or the diff compares a raw `id` against a folded `nativeID` and writes
163
+ // the alias twice while never clearing the raw key.
164
+ applyUpdate(node, foldHostBag(type, oldProps), foldHostBag(type, newProps));
132
165
  },
133
166
  commitTextUpdate(node, _oldText, newText) {
134
167
  setText(node, newText);
@@ -174,4 +207,12 @@ const reconciler = createReconciler({
174
207
  suspendInstance: () => { },
175
208
  waitForCommitToBeReady: () => null,
176
209
  });
210
+ // Native-event and timer callbacks both update React from outside the reconciler. Put the update
211
+ // on the discrete lane and synchronously commit it; otherwise a delayed Pressable transition can
212
+ // mutate state but remain visually stale until some unrelated later event flushes React work.
213
+ export function flushExternalUpdate(run) {
214
+ withDiscretePriority(run);
215
+ // @ts-expect-error flushSyncWork exists at runtime in react-reconciler 0.33
216
+ reconciler.flushSyncWork();
217
+ }
177
218
  export default reconciler;
@@ -1,7 +1,7 @@
1
1
  // The public instance a host ref hands back, and the ref -> tag lookup. toPublicInstance and
2
2
  // IHostInstance live in @symbiote-native/engine (every adapter inherits the same public instance);
3
3
  // this module re-exports them so the React surface (findNodeHandle + IHostInstance, host-config's
4
- // toPublicInstance graft) is unchanged. findNodeHandle stays React-specific: it is the
4
+ // getPublicInstance) is unchanged. findNodeHandle stays React-specific: it is the
5
5
  // RN-shaped "ref -> native tag" lookup over the engine's getNativeTag.
6
6
  import { getNativeTag, isSymbioteNode, toPublicInstance, } from '@symbiote-native/engine';
7
7
  export { toPublicInstance };
package/build/index.d.ts CHANGED
@@ -1,37 +1,32 @@
1
- export { View, Text } from './components';
1
+ import './register';
2
+ export type { ISymbioteIntrinsicTag } from './jsx-runtime';
2
3
  export type { IViewProps, ITextProps } from './components';
3
4
  export type { IAccessibilityProps, IAriaProps, IAccessibilityRole, IRole, IAccessibilityStateValue, IAccessibilityValue, IAccessibilityActionInfo, } from '@symbiote-native/components';
4
5
  export type { IResponderProps } from './utils/responder-props';
5
- export { Image, setImageSourceResolver } from './components/image';
6
- export type { IImageProps, IImageSource, IImageSourceProp, IResizeMode, IImageSize, IImageCacheStatus, } from './components/image';
7
- export { ImageBackground } from './components/image-background';
8
- export type { IImageBackgroundProps } from './components/image-background';
9
- export { ScrollView } from './components/scroll-view';
10
- export type { IScrollViewProps, IScrollViewHandle, } from './components/scroll-view';
11
- export { TextInput } from './components/text-input';
12
- export type { ITextInputProps, ITextInputHandle, } from './components/text-input';
13
- export { InputAccessoryView } from './components/input-accessory-view';
14
- 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';
15
15
  export { KeyboardAvoidingView } from './components/keyboard-avoiding-view';
16
16
  export type { IKeyboardAvoidingViewProps, IKeyboardAvoidingBehavior, } from './components/keyboard-avoiding-view';
17
17
  export { StatusBar } from './modules/status-bar';
18
18
  export type { IStatusBarProps, IStatusBarStyle } from './modules/status-bar';
19
- export { Switch } from './components/switch';
20
- export type { ISwitchProps, ISwitchTrackColor } from './components/switch';
21
- export { ActivityIndicator } from './components/activity-indicator';
22
- export type { IActivityIndicatorProps } from './components/activity-indicator';
23
- export { SafeAreaView } from './components/safe-area-view';
24
- export type { ISafeAreaViewProps } from './components/safe-area-view';
25
- export { RefreshControl } from './components/refresh-control';
26
- 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';
27
24
  export { Modal } from './components/modal';
28
25
  export type { IModalProps, IModalAnimationType, IModalPresentationStyle, IModalOrientation, } from './components/modal';
29
- export { Pressable } from './components/pressable';
30
- export type { IPressableProps, IPressState } from './components/pressable';
31
- export { TouchableOpacity, TouchableHighlight, TouchableWithoutFeedback, } from './components/touchable';
32
- export type { ITouchableOpacityProps, ITouchableHighlightProps, ITouchableWithoutFeedbackProps, } from './components/touchable';
33
- export { Button } from './components/button';
34
- 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';
35
30
  export { FlatList } from './components/flat-list';
36
31
  export type { IFlatListProps, IFlatListHandle } from './components/flat-list';
37
32
  export { SectionList } from './components/section-list';
@@ -67,7 +62,8 @@ export { PanResponder } from '@symbiote-native/engine';
67
62
  export type { IPanResponderGestureState, IPanResponderCallbacks, IGestureResponderHandlers, IPanResponderInstance, } from '@symbiote-native/engine';
68
63
  export { InteractionManager } from '@symbiote-native/engine';
69
64
  export type { IInteractionEvent, ISimpleTask, IPromiseTask, ITask, IHandle, } from '@symbiote-native/engine';
70
- export { TouchableNativeFeedback } from './components/touchable-native-feedback';
71
- 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';
72
68
  export type { ISymbioteEvent, ISymbioteNode, IRootTag, } from '@symbiote-native/engine';
73
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;