@symbiote-native/components 2.0.0 → 3.0.1

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 (79) hide show
  1. package/README.md +8 -9
  2. package/build/accessibility-props.d.ts +1 -1
  3. package/build/accessibility-props.js +2 -2
  4. package/build/behaviors/activity-indicator/shared.js +41 -70
  5. package/build/behaviors/button.d.ts +11 -0
  6. package/build/behaviors/button.js +101 -89
  7. package/build/behaviors/image-background.d.ts +1 -0
  8. package/build/behaviors/image-background.js +97 -81
  9. package/build/behaviors/image.d.ts +0 -1
  10. package/build/behaviors/image.js +24 -105
  11. package/build/behaviors/input-accessory-view.d.ts +0 -1
  12. package/build/behaviors/input-accessory-view.js +47 -53
  13. package/build/behaviors/pressable.d.ts +1 -1
  14. package/build/behaviors/pressable.js +89 -100
  15. package/build/behaviors/refresh-control.js +15 -2
  16. package/build/behaviors/scroll-view/index.android.js +25 -37
  17. package/build/behaviors/scroll-view/index.d.ts +1 -0
  18. package/build/behaviors/scroll-view/index.js +3 -0
  19. package/build/behaviors/scroll-view/responder.d.ts +4 -0
  20. package/build/behaviors/scroll-view/responder.js +202 -0
  21. package/build/behaviors/scroll-view/shared.d.ts +1 -3
  22. package/build/behaviors/scroll-view/shared.js +95 -95
  23. package/build/behaviors/scroll-view/sticky.d.ts +1 -0
  24. package/build/behaviors/scroll-view/sticky.js +62 -49
  25. package/build/behaviors/switch.js +43 -86
  26. package/build/behaviors/text-input.js +219 -107
  27. package/build/behaviors/touchable-highlight.js +74 -61
  28. package/build/behaviors/touchable-native-feedback.js +43 -122
  29. package/build/behaviors/touchable-opacity.js +71 -59
  30. package/build/behaviors/touchable-without-feedback.js +35 -100
  31. package/build/component-names/index.android.js +7 -11
  32. package/build/component-names/index.ios.js +0 -7
  33. package/build/component-names/shared.d.ts +1 -1
  34. package/build/index.d.ts +15 -21
  35. package/build/index.js +21 -28
  36. package/build/resolve-intrinsic.js +3 -9
  37. package/build/scroll-view-commands.d.ts +1 -9
  38. package/build/scroll-view-commands.js +13 -74
  39. package/build/state/flat-list.d.ts +2 -2
  40. package/build/state/flat-list.js +10 -2
  41. package/build/state/pressable.d.ts +6 -1
  42. package/build/state/pressable.js +63 -28
  43. package/build/state/section-list.d.ts +2 -0
  44. package/build/state/section-list.js +14 -7
  45. package/build/state/text-input.d.ts +7 -40
  46. package/build/state/text-input.js +17 -186
  47. package/build/state/touchable.d.ts +1 -0
  48. package/build/state/touchable.js +11 -8
  49. package/build/state/virtualized-list-reducer.d.ts +2 -2
  50. package/build/state/virtualized-list.d.ts +6 -6
  51. package/build/state/virtualized-list.js +71 -37
  52. package/build/text-props.d.ts +0 -8
  53. package/build/text-props.js +14 -25
  54. package/build/view/render-button.d.ts +1 -29
  55. package/build/view/render-button.js +44 -81
  56. package/build/view/render-image/index.d.ts +14 -1
  57. package/build/view/render-image/index.js +22 -147
  58. package/build/view/render-input-accessory-view.d.ts +1 -5
  59. package/build/view/render-input-accessory-view.js +26 -48
  60. package/build/view/render-keyboard-avoiding-view.d.ts +7 -1
  61. package/build/view/render-keyboard-avoiding-view.js +40 -1
  62. package/build/view/render-modal.d.ts +1 -1
  63. package/build/view/render-modal.js +15 -5
  64. package/build/view/render-pressable/index.d.ts +1 -0
  65. package/build/view/render-pressable/index.js +4 -0
  66. package/build/view/render-scroll-view.d.ts +0 -4
  67. package/build/view/render-scroll-view.js +3 -49
  68. package/build/view/render-switch.d.ts +0 -14
  69. package/build/view/render-switch.js +4 -42
  70. package/build/view/render-touchable-highlight.d.ts +1 -0
  71. package/build/view/render-touchable-native-feedback.d.ts +0 -1
  72. package/build/view/render-touchable-native-feedback.js +15 -9
  73. package/host-primitives.cjs +49 -203
  74. package/host-primitives.d.cts +0 -1
  75. package/package.json +3 -7
  76. package/build/fold-host-bag.d.ts +0 -15
  77. package/build/fold-host-bag.js +0 -99
  78. package/build/view/render-text-input.d.ts +0 -11
  79. package/build/view/render-text-input.js +0 -39
@@ -3,8 +3,8 @@
3
3
  //
4
4
  // THE TWO-NODE SHAPE IS RN'S. `ImageBackground.js:74-103` opens a `<View>` carrying the app's
5
5
  // `style`, puts an absolutely-filled `<Image>` inside it, and lays the app's `{children}` AFTER
6
- // that image so they paint on top. The lowered form is the same two nodes — `image-background`
7
- // (an RCTView, the tag an app writes) with an RCTImageView built under it.
6
+ // that image so they paint on top. The tag commits the same two nodes — `image-background` (an
7
+ // RCTView, the tag an app writes) with an RCTImageView built under it.
8
8
  //
9
9
  // WHY THE SLOT TAKES NO CHILDREN, which is the one thing this primitive needed that ScrollView,
10
10
  // ActivityIndicator and Button did not. `childHost` answers two questions at once — which node an
@@ -18,10 +18,11 @@
18
18
  // importantForAccessibility, ...props` and spreads `...props` onto the Image
19
19
  // (`ImageBackground.js:62-81`), so the set that moves is OPEN — every event, every accessibility
20
20
  // prop, `testID`, `id`, whatever an app writes next — and only a complement can express it.
21
- // `IMAGE_BACKGROUND_HOST_PROPS` is the short list that stays behind, and it is shorter than RN's:
22
- // `importantForAccessibility` rides to the image alone, which is what all five wrappers did, so
23
- // the tag and the component it replaces commit the same payload. The divergence from RN predates
24
- // lowering and is unchanged by it.
21
+ // `IMAGE_BACKGROUND_HOST_PROPS` is that complement, and `importantForAccessibility` is IN it despite
22
+ // never being part of the spread: RN reapplies it explicitly to both nodes (`:76,82`), so it stays on
23
+ // the owner here too and the image gets its own copy derived from `ownerProps` (FIXED 2026-09-20 —
24
+ // it used to redirect to the image alone and never reach the owner at all, which the module's own
25
+ // comment rationalized as "the divergence from RN predates all of it").
25
26
  //
26
27
  // WHAT THE IMAGE'S FOLD OWES. Everything on the image arrives as a real prop write, so its payload
27
28
  // is built by the shared `foldImagePayload` like any other `image`. Two things cannot arrive that
@@ -29,95 +30,84 @@
29
30
  // wrapper's width/height onto the image so it fills the box rather than collapsing to the source's
30
31
  // intrinsic size), and `id`, whose rename to `nativeID` is applied per adapter on the tag THEY
31
32
  // create and so never reaches a node a behavior built.
32
- import { appendChild, createElement, flattenStyle, registerHostBehavior, } from '@symbiote-native/engine';
33
+ import { appendChild, createElement, registerHostBehavior, } from '@symbiote-native/engine';
33
34
  import { descriptorFor } from '../component-names';
34
- import { foldImagePayload, IMAGE_TAG } from './image.js';
35
+ import { IMAGE_TAG, registerImageBehavior } from './image.js';
35
36
  export const IMAGE_BACKGROUND_TAG = 'image-background';
36
- // The inner Image's positioning: absolute-fill behind the box's children.
37
- const ABSOLUTE_FILL = {
38
- position: 'absolute',
39
- left: 0,
40
- right: 0,
41
- top: 0,
42
- bottom: 0,
43
- };
37
+ // The inner image's own tag. Distinct from `image` because the absolute fill must NOT reach a bare
38
+ // `<image>`, and a tag is the only thing a per-node rule can branch on.
39
+ export const IMAGE_BACKGROUND_IMAGE_TAG = 'image-background-image';
44
40
  // The props RN keeps on the wrapper View (`ImageBackground.js:74-78`), plus the two spellings of a
45
41
  // class name — `routeProp`'s slot redirect runs above its own class branch, so an unlisted `class`
46
- // would style the image instead of the box.
47
- const IMAGE_BACKGROUND_HOST_PROPS = ['style', 'class', 'className'];
48
- // The owner props the image's payload is derived from. Only `style`, because a class name is
49
- // published INTO `node.props.style` by `pushClassStyle` — so a `class` write arrives here spelled
50
- // `style` and the proxied width/height follow a class-declared box as well as an inline one.
51
- const IMAGE_BACKGROUND_SLOT_DERIVED = ['style'];
42
+ // would style the image instead of the box. `importantForAccessibility` stays too: RN destructures
43
+ // it out of `...props` and reapplies it explicitly to BOTH the wrapper (:76) and the image (:82), so
44
+ // it is never part of the spread — the engine derives the image's own copy from this node
45
+ // (`foldImageBackgroundImageProps` in `SymbioteFabricProps.cpp`), the same seam the box proxy uses.
46
+ const IMAGE_BACKGROUND_HOST_PROPS = [
47
+ 'style',
48
+ 'class',
49
+ 'className',
50
+ 'importantForAccessibility',
51
+ ];
52
+ // The owner props the image's payload is derived from. `importantForAccessibility` joins `style`
53
+ // for the reason above: a late write to either must re-fold the image, not just the box.
54
+ const IMAGE_BACKGROUND_SLOT_DERIVED = ['style', 'importantForAccessibility'];
52
55
  // `imageStyle` is the wrapper's own name for the image's `style`. A bare class NAME is a legal
53
56
  // value for it — every adapter's wrapper resolved one — and `routeProp` routes a string landing on
54
57
  // `style` as `class` instead, so the registry resolves it on the image with nothing needed here.
55
58
  const IMAGE_BACKGROUND_SLOT_PROPS = { imageStyle: 'style' };
56
- // A StyleProp is an object, an array of them, or a registered class array — all of which
57
- // `flattenStyle` already handles one layer down. The only thing to exclude is a scalar.
58
- function styleOf(value) {
59
- if (typeof value !== 'object' || value === null)
60
- return undefined;
61
- if (Array.isArray(value))
62
- return value;
63
- return { ...value };
64
- }
65
- // RN opts the wrapper out of iOS's Smart Invert for its whole subtree (`ImageBackground.js:75`) —
66
- // a photograph inverted by accessibility settings is the case that prop exists for. None of the
67
- // five wrappers ever wrote it, so this closes a standing gap rather than reproducing one.
68
- const hostFold = props => ({
69
- ...props,
70
- accessibilityIgnoresInvertColors: true,
71
- });
72
- // Read one explicit dimension off the (already-flattened) box style. A dp number or a percentage
73
- // string is a valid IDimensionValue; anything else (auto / undefined) yields undefined.
74
- function readDimension(style, key) {
75
- const value = Object.hasOwn(style, key) ? Reflect.get(style, key) : undefined;
76
- if (typeof value === 'number' || typeof value === 'string')
77
- return value;
78
- return undefined;
79
- }
80
- function imageFold(owner) {
81
- return props => {
82
- // RN's own workaround, and its comment is worth reading before "simplifying" this
83
- // (`ImageBackground.js:86-96`): an RN Image overwrites its own width/height from the source's
84
- // intrinsic size, which fights the box's explicit dimensions, so they are proxied back on.
85
- // Reads the OWNER's live style — a class name lands there too, published by `pushClassStyle`.
86
- const box = flattenStyle(styleOf(owner.props.style));
87
- const next = {
88
- ...props,
89
- // `imageStyle` last, so a caller still wins over the fill and the proxy.
90
- style: [
91
- ABSOLUTE_FILL,
92
- {
93
- width: readDimension(box, 'width'),
94
- height: readDimension(box, 'height'),
95
- },
96
- styleOf(props.style),
97
- ],
98
- };
99
- // Unconditional priority when both are set, matching RN (`View.js:77-79`) and `foldHostBag`.
100
- // A raw `id` is a key no ViewConfig declares, so Fabric drops it and the nativeID is lost.
101
- if (Object.hasOwn(next, 'id')) {
102
- next.nativeID = next.id;
103
- delete next.id;
104
- }
105
- return foldImagePayload(next);
106
- };
107
- }
59
+ // The owner's fold is GONE (2026-09-18), not moved into this file under another name: its whole
60
+ // content was `accessibilityIgnoresInvertColors: true`, a function of the tag and nothing else, and
61
+ // it is `foldImageBackgroundProps` in `SymbioteFabricProps.cpp` now. The owner therefore pays no
62
+ // trip into JS at all, and a committed ImageBackground is down from two crossings to one — the
63
+ // remaining one is the inner image's, below, which derives its style from THIS node and so cannot be
64
+ // a per-node rule. Contract: `core/engine/cpp/tests/js/image-background-payload.itest.ts`.
65
+ // THE IMAGE'S FOLD IS GONE TOO (2026-09-18) — `foldImageBackgroundImageProps` in
66
+ // `SymbioteFabricProps.cpp`, and with it this primitive costs ZERO trips into JS on both nodes.
67
+ //
68
+ // It outlived every other fold in this file because both of its inputs live on the node ABOVE: the
69
+ // app writes `style` on the `<image-background>` and `IMAGE_BACKGROUND_HOST_PROPS` keeps it there.
70
+ // "A per-node rule cannot reach another node" is what this file used to say, and it was a fact about
71
+ // the JS FOLD rather than the engine — the tree is in C++, so `fabricProps` takes `ownerProps` from
72
+ // `node.parent` and the proxy reads it there.
73
+ //
74
+ // The `id -> nativeID` half went earlier still and was DEAD before this port: `foldIdAlias` applies
75
+ // to every tagged node and runs ahead of any fold, so by the time this ran the key was already
76
+ // renamed. Worth naming, because a fold that still spells a rule someone else now applies reads as
77
+ // load-bearing and is not.
78
+ //
79
+ // Contract: `core/engine/cpp/tests/js/image-background-image-payload.itest.ts`.
108
80
  // Returns the image, so `slotProps` / `slotPropsExcept` / `slotDerived` all point at it — and the
109
81
  // app's children stay on the owner because of `slotTakesNoChildren`, not because of what this
110
82
  // returns. The image is appended FIRST and nothing else is ever placed in front of it, which is
111
83
  // what makes the children paint over it.
112
84
  //
113
- // Built WITHOUT handing `IMAGE_TAG` to `createElement`, deliberately: that would attach Image's own
114
- // behavior and set `payloadFold` to `foldImagePayload` alone, and this node's style has to be
115
- // derived from the owner. `payloadFold` is a single slot, so the composition is spelled here — and
116
- // it still calls the one shared mapping rather than restating it.
85
+ // Built WITH `IMAGE_TAG` since 2026-09-18, which is the reverse of what it used to do and for a
86
+ // reason that reversed with it. It used to withhold the tag so the node would not get Image's
87
+ // `payloadFold` — a single slot this primitive needed for its own derived style — and call the
88
+ // shared mapping by hand at the end. The mapping is the ENGINE's now, reached off the tag, so the
89
+ // tag is how this node gets the platform half at all; the JS slot is free for the composition.
90
+ //
91
+ // ONE ORDERING DIFFERENCE FALLS OUT, and it is deliberate rather than overlooked. The image rule
92
+ // folds the image's own `width`/`height` PROPS under its style, and the background rule then layers
93
+ // the box's dimensions over that — where RN nests it the other way (`ImageBackground.js:83-98` puts
94
+ // the props under the proxied box size). So when an app sets BOTH a `width` prop on the
95
+ // ImageBackground and a conflicting width in its `style`, RN gives the style's and we give the
96
+ // prop's.
97
+ //
98
+ // It is left this way rather than reproduced: RN's own comment calls that nesting a "Temporary
99
+ // Workaround" for an Image that overwrites its own dimensions, and an explicit prop winning over an
100
+ // inherited box is the less surprising of the two. Pinned in
101
+ // `core/components/src/behaviors/image-background.test.ts` so it stays a decision.
102
+ //
103
+ // THE TAG IS ITS OWN, and that is what the C++ port needed. The node used to carry plain `image`,
104
+ // which is right for everything the ordinary image rule does and wrong for the fill — a bare
105
+ // `<image>` must NOT be absolutely positioned. `image-background-image` is served by BOTH rules, the
106
+ // ordinary one first, the same shape `usesPressableRule` gives `button` and the touchables. The
107
+ // COMPONENT still resolves through `IMAGE_TAG`, because the native view is an ordinary RCTImageView.
117
108
  function buildBackgroundImage(node) {
118
109
  const descriptor = descriptorFor(IMAGE_TAG);
119
- const image = createElement(descriptor.component, descriptor.isText);
120
- image.payloadFold = imageFold(node);
110
+ const image = createElement(descriptor.component, descriptor.isText, IMAGE_BACKGROUND_IMAGE_TAG);
121
111
  appendChild(node, image);
122
112
  return image;
123
113
  }
@@ -127,13 +117,39 @@ const imageBackgroundBehavior = {
127
117
  slotDerived: IMAGE_BACKGROUND_SLOT_DERIVED,
128
118
  slotTakesNoChildren: true,
129
119
  buildStructure: buildBackgroundImage,
130
- foldPayload: hostFold,
131
120
  // Required by the interface and deliberately empty: this primitive owns no timer, no listener
132
121
  // and no native handshake. Written out rather than shared with a `noop` so the emptiness reads
133
122
  // as a decision.
134
123
  attach() { },
135
124
  detach() { },
136
125
  };
126
+ // The inner image's registration, and it is NOT a formality twice over.
127
+ //
128
+ // A tag reaches C++ only through `recordSetTag`, which `attachHostBehavior` emits — so a tag nobody
129
+ // registered carries an EMPTY `tagName` in the host and no rule fires for it, however the rule is
130
+ // written. Same reason the ActivityIndicator spinner and the scroll content nodes carry one.
131
+ //
132
+ // And `resolvesImageSources` has to be repeated here rather than inherited from `image`: the lookup
133
+ // is by TAG, this node's tag is its own, and the flag is what makes `routeProp` run
134
+ // `require('./photo.png')` through Metro's asset registry on the way in. Without it an
135
+ // ImageBackground would commit the raw asset NUMBER and paint nothing — headless included, since
136
+ // the resolution is JS-side by necessity.
137
+ const backgroundImageBehavior = {
138
+ resolvesImageSources: true,
139
+ attach() { },
140
+ detach() { },
141
+ };
137
142
  export function registerImageBackgroundBehavior() {
143
+ // A REAL DEPENDENCY, declared rather than assumed. The inner node is a TAG now, so it gets its
144
+ // platform half — the engine's two rules, and the write-time source resolution — only through
145
+ // these registrations. It used to need nothing, because the fold was a function this file called
146
+ // directly.
147
+ //
148
+ // `register.ts` registers everything anyway, so nothing in an app depended on this; what depended
149
+ // on it was every test that registers one behavior and not the whole set, and a silent inner
150
+ // image with no rule is precisely the failure that would reach a device before it reached a
151
+ // suite. Idempotent, like every `register*` here.
152
+ registerImageBehavior();
153
+ registerHostBehavior(IMAGE_BACKGROUND_IMAGE_TAG, backgroundImageBehavior);
138
154
  registerHostBehavior(IMAGE_BACKGROUND_TAG, imageBackgroundBehavior);
139
155
  }
@@ -1,3 +1,2 @@
1
1
  export declare const IMAGE_TAG = "image";
2
- export declare function foldImagePayload(props: Readonly<Record<string, unknown>>): Record<string, unknown>;
3
2
  export declare function registerImageBehavior(): void;
@@ -1,111 +1,30 @@
1
- // Image's host behavior, and it carries NOTHING but a prop fold — no listeners, no timers, no
2
- // commit hook. That is what a "fold-only" primitive means: the wrapper's whole body was prop
3
- // mapping, so its lowered form owes exactly that and nothing else.
1
+ // Image's host behavior, and it carries no runtime at all — no listeners, no timers, no commit
2
+ // hook, and since 2026-09-18 no fold either. What is left is ONE declaration: that this tag's three
3
+ // source props are resolved on the way in.
4
4
  //
5
- // The fold itself is not written here. `mapImageProps` in `../view/render-image` is the one
6
- // implementation, and this file only narrows a flat prop bag into the typed view it takes — the
7
- // same shape `behaviors/text-input.ts` uses (`stringOf` / `booleanOf` guards, then hand the shared
8
- // resolver a typed object). A second copy of the mapping is the exact drift this primitive has
9
- // already paid for once: `adapters/svelte/src/components/image/image-logic.ts` reproduces it by
10
- // hand and says so in its own header, because nothing was exported to call.
5
+ // WHERE THE FOLD WENT: `foldImageProps` in `SymbioteFabricProps.cpp`, with
6
+ // `core/engine/cpp/tests/js/image-payload.itest.ts` as its contract. The `srcSet`/`src`/`source`
7
+ // precedence, the W3C header decoration, the `width`/`height` fold into style, `alt` becoming
8
+ // `accessibilityLabel` + `accessible`, `resizeMode`/`tintColor` falling back to style keys, and
9
+ // `loadingIndicatorSource` being plucked down to a bare uri — all of it is a function of the tag,
10
+ // which is what makes it the platform's.
11
11
  //
12
- // WHY THIS MAY SHARE THE WRAPPER'S TAG, where TextInput needed `-managed`. A behavior fold is keyed
13
- // on the tag, and `renderImage` emits `image` too — so on a wrapper-built node this fold
14
- // runs on ALREADY-FOLDED props. That is safe here and only here, because the mapping is idempotent:
15
- // every alias it consumes (`src`, `srcSet`, `alt`, `width`, `height`, …) is absent from its own
16
- // output, `source` comes back in the array shape `normalizeSource` guarantees, and
17
- // `loadingIndicatorSource` leaves under a DIFFERENT name (`loadingIndicatorSrc`), so a second pass
18
- // finds nothing left to fold. Idempotence is asserted in `image.test.ts` rather than assumed — the
19
- // audit rule's point is that a double fold is invisible precisely when it happens to be harmless,
20
- // so the property has to be pinned or it is an accident waiting to be broken.
12
+ // WHY THIS ONE DID NOT MOVE WHOLE, and it is the first that did not. `resolveAssetSource` turns the
13
+ // number `require('./logo.png')` returns into a `{uri, width, height, scale}` by asking METRO'S
14
+ // ASSET REGISTRY — a JS module populated at bundle time. There is no such table in C++ and there
15
+ // should not be: it belongs to the bundler, not to the platform.
16
+ //
17
+ // So the lookup moved EARLIER rather than across. `resolvesImageSources` tells `routeProp` to run
18
+ // the three source props through the resolver on the way IN, exactly as it already resolves
19
+ // `boxShadow`/`filter`/`transform` (`structured-style.ts`), and for the identical reason: a value
20
+ // resolved at payload-build time is resolved HEADLESS ONLY, because the C++ builder has no JS to
21
+ // call, and the device then commits the raw input for Fabric to drop in silence.
22
+ //
23
+ // The idempotence this file used to pin is GONE as a question rather than as a property. It
24
+ // mattered because `renderImage` folded too and a re-render could hand the fold its own output;
25
+ // there is one implementation now, at one point in the commit, so a bag cannot be folded twice.
21
26
  import { registerHostBehavior, } from '@symbiote-native/engine';
22
- import { IMAGE_VIEW_PROP_NAMES, mapImageProps, } from '../view/render-image/index.js';
23
27
  export const IMAGE_TAG = 'image';
24
- const RESIZE_MODES = new Set([
25
- 'cover',
26
- 'contain',
27
- 'stretch',
28
- 'repeat',
29
- 'center',
30
- ]);
31
- const CROSS_ORIGINS = new Set([
32
- 'anonymous',
33
- 'use-credentials',
34
- ]);
35
- const CONSUMED = new Set(IMAGE_VIEW_PROP_NAMES);
36
- function stringOf(value) {
37
- return typeof value === 'string' ? value : undefined;
38
- }
39
- function numberOf(value) {
40
- return typeof value === 'number' ? value : undefined;
41
- }
42
- function resizeModeOf(value) {
43
- if (typeof value !== 'string' || !RESIZE_MODES.has(value))
44
- return undefined;
45
- // Narrowed by the set membership above, then re-stated as the union through a switch rather than
46
- // a cast: the set and the type are two declarations of one list, and only this makes them agree.
47
- switch (value) {
48
- case 'cover':
49
- case 'contain':
50
- case 'stretch':
51
- case 'repeat':
52
- case 'center':
53
- return value;
54
- default:
55
- return undefined;
56
- }
57
- }
58
- function crossOriginOf(value) {
59
- if (typeof value !== 'string' || !CROSS_ORIGINS.has(value))
60
- return undefined;
61
- return value === 'anonymous' ? 'anonymous' : 'use-credentials';
62
- }
63
- // A source is an asset id (number), a `{uri}` object, or an array of those. Anything else cannot be
64
- // resolved and is dropped rather than forwarded — a malformed source reaching Fabric paints nothing
65
- // and reports nothing, which is worse than an image that is simply absent.
66
- function sourceOf(value) {
67
- if (typeof value === 'number')
68
- return value;
69
- if (typeof value !== 'object' || value === null)
70
- return undefined;
71
- if (Array.isArray(value))
72
- return value;
73
- if (typeof Reflect.get(value, 'uri') === 'string')
74
- return { ...value };
75
- return undefined;
76
- }
77
- // A StyleProp is an object, an array of them, or a registered class array — all of which
78
- // `flattenStyle` already handles. The only thing to exclude is a scalar.
79
- function styleOf(value) {
80
- if (typeof value !== 'object' || value === null)
81
- return undefined;
82
- if (Array.isArray(value))
83
- return value;
84
- return { ...value };
85
- }
86
- export function foldImagePayload(props) {
87
- const passthrough = {};
88
- for (const key of Object.keys(props)) {
89
- if (!CONSUMED.has(key))
90
- passthrough[key] = props[key];
91
- }
92
- return mapImageProps({
93
- source: sourceOf(props.source),
94
- defaultSource: sourceOf(props.defaultSource),
95
- loadingIndicatorSource: sourceOf(props.loadingIndicatorSource),
96
- style: styleOf(props.style),
97
- resizeMode: resizeModeOf(props.resizeMode),
98
- tintColor: stringOf(props.tintColor),
99
- src: stringOf(props.src),
100
- srcSet: stringOf(props.srcSet),
101
- alt: stringOf(props.alt),
102
- width: numberOf(props.width),
103
- height: numberOf(props.height),
104
- crossOrigin: crossOriginOf(props.crossOrigin),
105
- referrerPolicy: stringOf(props.referrerPolicy),
106
- passthrough,
107
- });
108
- }
109
28
  // Required by IHostBehavior and deliberately empty: Image owns no per-node runtime. Written out
110
29
  // rather than shared with a `noop` helper so the emptiness reads as a decision.
111
30
  function attach(_node) {
@@ -118,6 +37,6 @@ export function registerImageBehavior() {
118
37
  registerHostBehavior(IMAGE_TAG, {
119
38
  attach,
120
39
  detach,
121
- foldPayload: foldImagePayload,
40
+ resolvesImageSources: true,
122
41
  });
123
42
  }
@@ -1,3 +1,2 @@
1
1
  export declare const INPUT_ACCESSORY_VIEW_TAG = "input-accessory-view";
2
- export declare function foldInputAccessoryViewPayload(props: Readonly<Record<string, unknown>>): Record<string, unknown>;
3
2
  export declare function registerInputAccessoryViewBehavior(): void;
@@ -1,58 +1,56 @@
1
- // InputAccessoryView's host behavior — a prop fold and nothing else. No listeners, no timers, no
2
- // commit hook: the wrapper's whole body was prop mapping, so its lowered form owes exactly that.
1
+ // InputAccessoryView's host behavior. This file itself still owns no per-node runtime — the
2
+ // primitive's real rule lives in `foldInputAccessoryViewProps` (`SymbioteFabricProps.cpp`) now.
3
3
  //
4
- // The fold is NOT written here. `mapInputAccessoryViewProps` in
5
- // `../view/render-input-accessory-view` is the one implementation and this file only narrows a flat
6
- // bag into the typed view it takes, the `stringOf` / `styleOf` guard idiom `behaviors/image.ts`
7
- // uses. Counted before starting, which is step one of the fold-only recipe: unlike Image, which had
8
- // THREE implementations, all five adapters already call the shared render fn and Svelte's
9
- // `input-accessory-view-props.ts` is a type declaration rather than a second mapping. So there was
10
- // one implementation to extract, not three to collapse.
4
+ // THE OLD FOLD WAS GONE, not moved (2026-09-18): read end to end it split the bag into
5
+ // consumed/passthrough and reassembled it — there was no aliasing at all, every consumed name left
6
+ // under the same name, and the one thing it actually changed was dropping a `nativeID` or
7
+ // `backgroundColor` that was not a `string`. That is defensive narrowing of a typed view object,
8
+ // not a rule native cares about — and it was WRONG on `backgroundColor`, whose validAttributes
9
+ // entry carries `processColor` and therefore accepts a number.
11
10
  //
12
- // SHARES THE WRAPPER'S TAG, no `-managed` twin. The two questions the recipe separates both answer
13
- // no: the fold is idempotent (there is no aliasing at all — every consumed name leaves under the
14
- // same name, so a second pass rewrites the same values), and the behavior carries no machine, so
15
- // nothing can end up with two owners on one node. Idempotence is asserted rather than reasoned.
11
+ // A REAL RULE WAS FOUND WHILE READING VENDOR FOR THE PORT (2026-09-20):
12
+ // `InputAccessoryView.js`'s `styles.container = {position: 'absolute'}`, composed AFTER the app's
13
+ // own style (`[props.style, styles.container]`) so it wins over any `position` an app authors.
14
+ // Every InputAccessoryView is positioned absolutely; nothing about it is per-instance, so it is
15
+ // `foldInputAccessoryViewProps` now rather than anything here. Contract:
16
+ // `core/engine/cpp/tests/js/touchable-payload.itest.ts`.
17
+ //
18
+ // NOT YET PORTED, and recorded honestly rather than silently skipped: vendor also wraps the app's
19
+ // children in an inner `SafeAreaView` sized to `useWindowDimensions().width` (`InputAccessoryView.js`
20
+ // render body) and returns nothing at all when it has zero children. Both are real structural gaps
21
+ // — a composed child node plus a live window-dimension binding — genuinely larger than a tag-only
22
+ // rule, and are NOT covered by this pass.
23
+ //
24
+ // TODO(rn-parity): port both. Needs (a) a `buildStructure` composing an inner wrapper node styled
25
+ // `{flex: 1, width}` around the app's children, with `width` read from a live window-dimensions
26
+ // subscription (no `SafeAreaView` primitive exists in this codebase yet — it would need building),
27
+ // and (b) suppressing the whole node (both platforms, not just Android's void case already fixed
28
+ // here) when it has zero children, matching `React.Children.count(props.children) === 0`.
29
+ //
30
+ // The `id -> nativeID` alias — is `foldIdAlias` in `SymbioteFabricProps.cpp`, applied to every
31
+ // tagged node rather than per primitive.
16
32
  //
17
33
  // PLATFORM. This is the only primitive in its group that is not platform-invariant in what it
18
34
  // COMMITS TO: `input-accessory-view` resolves to `RCTInputAccessoryView` on iOS and to a
19
- // plain `RCTView` on Android. The fold itself is platform-invariant on purpose — it reproduces the
20
- // wrapper's mapping exactly, on both platforms, so the lowered and wrapped paths cannot diverge
21
- // per-platform. What it does NOT do is fix the pre-existing Android divergence underneath it:
22
- // upstream RN renders NOTHING there (`InputAccessoryView.js` — `console.warn('<InputAccessoryView>
23
- // is only supported on iOS.'); return null`), while we commit an RCTView, and `backgroundColor` is
24
- // a declared prop of the iOS view but a style key on RCTView. Both predate this behavior and are
25
- // identical on both paths; neither is in scope here, and a fold that "fixed" one silently would
26
- // make the two paths disagree.
35
+ // plain `RCTView` on Android. The fold itself is platform-invariant on purpose.
36
+ //
37
+ // FIXED (2026-09-20). `InputAccessoryView.js` on Android does `console.warn('<InputAccessoryView>
38
+ // is only supported on iOS.'); return null` — the WHOLE component, children included, renders
39
+ // NOTHING. We used to commit a real `RCTView` and its whole children subtree (the toolbar content
40
+ // an app wrapped in it) — an extra, laid-out, potentially visible view where a real device shows
41
+ // none. `backgroundColor` being a declared iOS prop vs an Android style key is the SAME already-
42
+ // narrow gap, unaffected by this.
43
+ //
44
+ // The fix needed a new engine primitive, because every existing "commits no node of its own"
45
+ // tier-2 primitive (`touchable-native-feedback`, `touchable-without-feedback`) is an ANCHOR — it
46
+ // hoists its single child up in its own place, which is the opposite of what this tag needs: its
47
+ // whole subtree must vanish. `VOID_COMPONENT` (`core/engine/src/node.ts`, `OP_CREATE_VOID` in
48
+ // `mutation-buffer.ts`) is that primitive — a node the commit walk stops at, recursively,
49
+ // contributing neither itself nor its children. Wired purely through
50
+ // `core/components/src/component-names/index.android.ts`'s per-platform table, the same seam
51
+ // `ANCHOR_COMPONENT` already used — this behavior file needed no change at all.
27
52
  import { registerHostBehavior, } from '@symbiote-native/engine';
28
- import { INPUT_ACCESSORY_VIEW_PROP_NAMES, mapInputAccessoryViewProps, } from '../view/render-input-accessory-view.js';
29
53
  export const INPUT_ACCESSORY_VIEW_TAG = 'input-accessory-view';
30
- const CONSUMED = new Set(INPUT_ACCESSORY_VIEW_PROP_NAMES);
31
- function stringOf(value) {
32
- return typeof value === 'string' ? value : undefined;
33
- }
34
- // A StyleProp is an object, an array of them, or a registered class array — all shapes
35
- // `flattenStyle` already handles. Only a scalar has to be excluded.
36
- function styleOf(value) {
37
- if (typeof value !== 'object' || value === null)
38
- return undefined;
39
- if (Array.isArray(value))
40
- return value;
41
- return { ...value };
42
- }
43
- export function foldInputAccessoryViewPayload(props) {
44
- const passthrough = {};
45
- for (const key of Object.keys(props)) {
46
- if (!CONSUMED.has(key))
47
- passthrough[key] = props[key];
48
- }
49
- return mapInputAccessoryViewProps({
50
- nativeID: stringOf(props.nativeID),
51
- backgroundColor: stringOf(props.backgroundColor),
52
- style: styleOf(props.style),
53
- passthrough,
54
- });
55
- }
56
54
  // Required by IHostBehavior and deliberately empty: this primitive owns no per-node runtime.
57
55
  // Written out rather than shared with a `noop` so the emptiness reads as a decision.
58
56
  function attach(_node) {
@@ -62,9 +60,5 @@ function detach(_node) {
62
60
  // nothing to release
63
61
  }
64
62
  export function registerInputAccessoryViewBehavior() {
65
- registerHostBehavior(INPUT_ACCESSORY_VIEW_TAG, {
66
- attach,
67
- detach,
68
- foldPayload: foldInputAccessoryViewPayload,
69
- });
63
+ registerHostBehavior(INPUT_ACCESSORY_VIEW_TAG, { attach, detach });
70
64
  }
@@ -56,5 +56,5 @@ export declare function detachPressMachine(node: ISymbioteNode): void;
56
56
  * WeakMap holding the machine's own state is keyed by node, so one node may hold exactly one of
57
57
  * these; a tag composing it therefore must not also register the plain `pressable` behavior.
58
58
  */
59
- export declare function createPressBehavior(refine?: IPressConfigRefinement, disabledOf?: IDisabledResolver): Pick<IHostBehavior, 'attach' | 'detach' | 'foldPayload' | 'ownedListeners'>;
59
+ export declare function createPressBehavior(refine?: IPressConfigRefinement, disabledOf?: IDisabledResolver): Pick<IHostBehavior, 'attach' | 'detach' | 'ownedListeners'>;
60
60
  export declare function registerPressableBehavior(): void;