@symbiote-native/components 3.1.1 → 3.1.3

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 (40) hide show
  1. package/build/behaviors/activity-indicator/shared.js +29 -71
  2. package/build/behaviors/button.d.ts +0 -9
  3. package/build/behaviors/button.js +47 -233
  4. package/build/behaviors/image-background.js +21 -81
  5. package/build/behaviors/image.js +3 -10
  6. package/build/behaviors/input-accessory-view.js +10 -51
  7. package/build/behaviors/pressable.d.ts +5 -51
  8. package/build/behaviors/pressable.js +101 -161
  9. package/build/behaviors/refresh-control.js +8 -2
  10. package/build/behaviors/scroll-view/index.android.js +10 -30
  11. package/build/behaviors/scroll-view/responder.d.ts +3 -2
  12. package/build/behaviors/scroll-view/responder.js +19 -21
  13. package/build/behaviors/scroll-view/shared.js +69 -185
  14. package/build/behaviors/scroll-view/sticky.d.ts +0 -8
  15. package/build/behaviors/scroll-view/sticky.js +54 -142
  16. package/build/behaviors/switch.js +18 -4
  17. package/build/behaviors/text-input.d.ts +0 -8
  18. package/build/behaviors/text-input.js +77 -162
  19. package/build/behaviors/touchable-highlight.js +14 -54
  20. package/build/behaviors/touchable-native-feedback.js +9 -32
  21. package/build/behaviors/touchable-opacity.d.ts +0 -7
  22. package/build/behaviors/touchable-opacity.js +65 -81
  23. package/build/behaviors/touchable-without-feedback.js +25 -41
  24. package/build/component-names/index.android.js +6 -8
  25. package/build/component-names/shared.js +12 -42
  26. package/build/index.js +13 -19
  27. package/build/scroll-view-commands.js +3 -11
  28. package/build/state/pressable.js +18 -43
  29. package/build/state/sticky-header-reducer.js +103 -149
  30. package/build/state/touchable.js +3 -5
  31. package/build/state/virtualized-list-reducer.js +21 -48
  32. package/build/state/virtualized-list.js +63 -148
  33. package/build/text-props.js +3 -13
  34. package/build/view/render-button.js +13 -44
  35. package/build/view/render-input-accessory-view.js +8 -24
  36. package/build/view/render-pressable/index.js +3 -4
  37. package/build/view/render-scroll-view.js +13 -23
  38. package/build/view/render-touchable-native-feedback.js +5 -14
  39. package/host-primitives.cjs +33 -207
  40. package/package.json +3 -3
@@ -1,87 +1,39 @@
1
- // The PRIMITIVE SPEC — one description of which intrinsic tag each primitive is, what it folds,
2
- // and which of its two Fabric views a prop selects. Data only: no AST, no framework, no code.
3
- //
4
- // WHY IT IS A `.cjs` AND NOT PART OF `src/`. Its consumers include build-tool files that run
5
- // before any TS exists and cannot import from `src/` — `adapters/vue/intrinsic-tags.cjs`, which
6
- // both Vue compilers read to answer element-vs-component. That constraint is why the map was
7
- // copied per adapter in the first place.
8
- //
9
- // WHAT IT DOES NOT UNIFY, stated here so nobody hunts for a contradiction that is not one: this
10
- // says WHAT a fold is, never WHICH LAYER applies it. Svelte folds Text's defaults in its DOM shim;
11
- // Vue and Solid apply the same fold in their renderers. Both are correct.
12
- //
13
- // Four transforms carried their own copy of this before it existed, and it had already produced a
14
- // real behaviour split — the `aliases` note below is what is left of it.
1
+ // Which intrinsic tag each primitive is, what it folds, which of its two Fabric views a prop
2
+ // selects. Data only. A `.cjs` (not `src/`) because build-tool consumers like
3
+ // `adapters/vue/intrinsic-tags.cjs` run before any TS exists and can't import from `src/`.
4
+
5
+ // `intrinsicWhen`: BOUNDARY IS IDENTITY not truthiness — only literal `true` picks the
6
+ // alternative view. Kept in sync by hand with `host-primitives.d.cts`; this `.cjs` compiles fine
7
+ // if they drift, only the one TS consumer's `tsc` catches it.
15
8
 
16
- // `intrinsicWhen` DECLARED AHEAD OF ITS FIRST ENTRY.
17
- //
18
- // WHAT IT IS FOR. `TextInput` is the first primitive whose TAG depends on a prop: `multiline`
19
- // selects between two different Fabric views, `text-input` and
20
- // `text-input-multiline` (`src/view/render-text-input.ts:33`), not between two values of
21
- // one view. `src/resolve-intrinsic.ts` reads it at element creation, where the value is known.
22
- //
23
- // ONE selector and ONE alternative, deliberately — not a map and not a list. There is exactly one
24
- // such prop in the whole surface, and a wider field would be invented rather than needed. Absent
25
- // `intrinsicWhen` means "one tag", so no existing entry changes.
26
- //
27
- // THE BOUNDARY IS IDENTITY, not truthiness: only `true` picks the alternative, and a truthy
28
- // non-boolean like `multiline={1}` does not. The spec types the selector as a boolean; guessing
29
- // past that commits the wrong native view, and no later prop write moves a node between views.
30
- //
31
- // THE TYPEDEF BELOW IS NOT WHAT TYPESCRIPT READS. `host-primitives.d.cts` is a hand-written
32
- // declaration file, and a field added here and not there compiles fine in every `.cjs` transform
33
- // while failing `tsc` in the one consumer written in TypeScript — measured 2026-08-31, when
34
- // `intrinsicWhen` landed here alone and reddened Svelte's preprocessor with the whole suite green
35
- // (vitest does not typecheck). The reverse is worse and silent: a field in the `.d.cts` and not
36
- // here typechecks everywhere and arrives `undefined` in all five transforms. Change both, together.
37
9
  /**
38
10
  * @typedef {{ prop: string, intrinsic: string }} IIntrinsicWhen
39
11
  * @typedef {{ intrinsic: string, intrinsicWhen?: IIntrinsicWhen }} IHostPrimitive
40
12
  */
41
13
 
42
- // `aliases` LEFT THIS SPEC ON 2026-09-18, and what it held was one pair — `id` -> `nativeID` —
43
- // repeated on all nineteen entries. It is `routeProp`'s now
44
- // (`core/engine/cpp/tests/js/id-alias-coverage.itest.ts`), which reaches every node rather than
45
- // every REGISTERED one, and resolves precedence the way upstream does instead of by write order.
14
+ // `id` -> `nativeID` aliasing is NOT here: it is `routeProp`'s, for every node rather than only
15
+ // a registered one (`core/engine/cpp/tests/js/id-alias-coverage.itest.ts`).
46
16
 
47
17
  /** @type {Record<string, IHostPrimitive>} */
48
18
  const HOST_PRIMITIVES = {
49
19
  View: {
50
20
  intrinsic: 'view',
51
21
  },
52
- // The five-way switch, thrown 2026-08-23 once all three transforms carried the refusals
53
- // (`observesState` below). `pressable` resolves to the SAME `RCTView` a plain view does
54
- // — the tag exists only so the host-behavior registry, which is keyed by TAG and never by
55
- // resolved name, can find the press machine.
56
- //
57
- // No defaults: the tag forwards its props untouched, and the machine reads them off `node.props`
58
- // at event time.
22
+ // Same `RCTView` as `view` — the tag exists only so the host-behavior registry (keyed by TAG)
23
+ // can find the press machine.
59
24
  Pressable: {
60
25
  intrinsic: 'pressable',
61
- // Turns on `stateInTemplate` and `renderPropChild`. Without them a render-prop button becomes
62
- // a tag with no machine — the whole reason this entry landed last.
26
+ // Needed for a render-prop style/child; without it the machine never attaches.
63
27
  observesState: true,
64
28
  },
65
- // The three that were WITHHELD until their wrappers collapsed, landed 2026-09-11 now that none
66
- // exists on any adapter. The condition the old note stated — "it lands when the wrappers collapse
67
- // to a forwarder over the tag" — was met by deleting them outright.
68
- //
69
- // What the entry buys, since the tag and its behavior already worked without one: a hand-written
70
- // `<scroll-view>` was resolving as a COMPONENT on Vue, because `adapters/vue/intrinsic-tags.cjs`
71
- // derives its element set from this table and both Vue compilers read it. Missing here, the tag
72
- // cost a dev-mode resolve warning per element plus the component codegen path — a slot closure
73
- // instead of `_createElementBlock`.
74
29
  TouchableOpacity: {
75
30
  intrinsic: 'touchable-opacity',
76
31
  },
77
32
  TouchableHighlight: {
78
33
  intrinsic: 'touchable-highlight',
79
34
  },
80
- // The second primitive whose TAG depends on a prop, and the first where the prop is one RN's own
81
- // API takes (`<ScrollView horizontal>`): the axis is a SEPARATE native ViewManager, not a flag on
82
- // one view (`behaviors/scroll-view/shared.ts:131`). So an app may write either spelling and
83
- // `resolveIntrinsicTag` picks the view, which is also what puts `horizontal-scroll-view` into
84
- // Vue's element set — a tag apps write directly and which would otherwise resolve as a component.
35
+ // Axis is a separate native ViewManager, not a runtime flag (`<ScrollView horizontal>` in RN
36
+ // terms) — an app may write either tag spelling.
85
37
  ScrollView: {
86
38
  intrinsic: 'scroll-view',
87
39
  intrinsicWhen: {
@@ -89,188 +41,62 @@ const HOST_PRIMITIVES = {
89
41
  intrinsic: 'horizontal-scroll-view',
90
42
  },
91
43
  },
92
- // Landed 2026-08-31, on the second attempt. The first threw the switch with the runtime half
93
- // unwired and was reverted the same hour; both gaps it exposed are closed here, and the record is
94
- // kept because the SEQUENCE is the reusable part — an entry here is a switch for four transforms
95
- // at once, so it goes in after every side is ready, never to prove the transforms work.
96
- //
97
- // `registerTextInputBehavior()` is called by every adapter's `src/register.ts`: the tag is the
98
- // only path an app has, so the machine has exactly one owner per node.
44
+ // `multiline` picks between two SEPARATE native views, decided at compile time — a wrong
45
+ // view here is uncorrectable by any later prop write.
99
46
  TextInput: {
100
47
  intrinsic: 'text-input',
101
- // `multiline` picks between two SEPARATE native views, not one view with a flag, so the tag is
102
- // decided at compile time and a runtime selector must refuse — a wrong view here is
103
- // uncorrectable by any later prop write.
104
48
  intrinsicWhen: {
105
49
  prop: 'multiline',
106
50
  intrinsic: 'text-input-multiline',
107
51
  },
108
52
  },
109
- // Landed 2026-09-01, same order as TextInput and Image: runtime half built
110
- // (`core/components/src/behaviors/switch.ts`), registered by every adapter, proven
111
- // against the wrapper's payload (positive + negative controls, a break-tested async-timing case)
112
- // before this key existed.
113
- //
114
- // The behavior carries a machine: it mirrors the last value native reported and sends a platform
115
- // snap-back command on disagreement.
116
- //
117
- // ITS FOLD IS NOT IDEMPOTENT, and that is safe for a reason Image's entry does not share. Switch
118
- // maps `trackColor`/`thumbColor`/`ios_backgroundColor` onto native prop names keyed on
119
- // `Platform.OS`, so running it twice would NOT be a no-op — the question never arises because a
120
- // payload reaches this fold once, on the one tag that carries the behavior. Unreachable by
121
- // construction, not idempotent by property.
122
- //
123
- // No `observesState`: nothing in Switch's public surface is a function-valued style or a
124
- // render-prop child (`style?: IStyleProp<IViewStyle>`, never a callback), so neither
125
- // `stateInTemplate` nor `renderPropChild` applies — unlike Pressable, whose machine is what
126
- // forced that flag.
127
53
  Switch: {
128
54
  intrinsic: 'switch',
129
55
  },
130
- // Filed as NOT LOWERABLE for a week under `.claude/rules/host-primitive-tier.md`'s "SECOND
131
- // disqualifier" — its own node is a single element, but its POSITION is decided by the ScrollView
132
- // and differs per platform (iOS a sibling before the content view, Android the scroll view's
133
- // PARENT), and a per-node behavior cannot own a decision another component makes.
134
- //
135
- // That is settled and it was settled elsewhere: the ScrollView states the placement as DATA
136
- // (`claimedChildren: { [REFRESH_CONTROL]: platform.claimMode }`, `behaviors/scroll-view/shared.ts`)
137
- // and the ENGINE moves the node in `appendChild`. So the claim needs nothing from this primitive's
138
- // own behavior — verified on a bare node with no wrapper anywhere, both platforms, in
139
- // `behaviors/refresh-control.test.ts`.
140
- //
141
- // What the behavior owes is therefore only the CONTROLLED HANDSHAKE, and no fold at all: four of
142
- // the five wrappers folded exactly `resolveAccessibilityProps`, which the engine already runs at
143
- // `fabricProps` on every path — the same reason SafeAreaView has no behavior file.
144
- //
145
- // `id` is declared on all five wrappers, which it was not until 2026-09-01: upstream's
146
- // RefreshControl spreads `...ViewProps` (RefreshControl.js:70), so its absence was a parity gap
147
- // rather than a decision. The rename itself is the engine's.
56
+ // Placement is decided by the owning ScrollView (`claimedChildren`,
57
+ // `behaviors/scroll-view/shared.ts`), not by this primitive — no behavior file needed.
148
58
  RefreshControl: {
149
59
  intrinsic: 'refresh-control',
150
- // None. RN seeds nothing: `refreshing` is required, and every other prop is per-platform
151
- // styling the native view defaults itself.
152
60
  },
153
- // `defaults` LEFT THIS SPEC ON 2026-09-18, and Text was the last entry that had any — RN's
154
- // `ellipsizeMode ?? 'tail'` and `allowFontScaling !== false` (`Text.js:289,291`). The other
155
- // eighteen entries had declared `{}` for months.
156
- //
157
- // It was the THIRD copy of that one rule. `applyTextDefaults` (`core/engine/src/fabric-props.ts`)
158
- // and its C++ twin apply both to every node committing as `RCTText`, whoever authored it and
159
- // whatever tag it carries — strictly WIDER than this table, which reached only a registered
160
- // primitive. Proven redundant rather than argued: emptying it turned ZERO itests red, and the
161
- // itests are the side that reads a real committed payload.
61
+ // No `defaults`: RN's Text defaults are `applyTextDefaults` (`core/engine/src/fabric-props.ts`)
62
+ // plus its C++ twin, applied to every `RCTText` regardless of tag.
162
63
  Text: {
163
64
  intrinsic: 'text',
164
65
  },
165
- // FOLD-ONLY: the behavior registered for this tag carries a prop fold and nothing else — no
166
- // listeners, no commit hook, no per-node runtime (`core/components/src/behaviors/image.ts`).
167
- //
168
- // `mapImageProps` is idempotent, asserted in `behaviors/image.test.ts` and break-tested, so a bag
169
- // that reaches the fold twice folds to no effect.
170
- //
171
- // The runtime half has to land before this key does: without the fold a raw `src` reaches Fabric,
172
- // a key no ViewConfig declares, which throws nothing and paints nothing.
173
- // THE ENTRY IS NOT OPTIONAL HERE, and the reason has nothing to do with folds: this table is what
174
- // `adapters/vue/intrinsic-tags.cjs` derives element-vs-component from, and a hyphenated tag it
175
- // does not name compiles to `resolveComponent("image-background")` — children become a slot the
176
- // element path never reads, so the subtree renders BLANK with no error. `image`/`view`/`text` are
177
- // real SVG element names and survive that gap; this one is not.
66
+ // MUST be registered here: unnamed, `adapters/vue/intrinsic-tags.cjs` compiles this hyphenated
67
+ // tag as a component and its children silently render BLANK.
178
68
  ImageBackground: {
179
69
  intrinsic: 'image-background',
180
- // None. The absolute-fill style, the box-dimension proxy and the Image mapping are all derived
181
- // from live props at commit, which a compile-time seed cannot express.
182
70
  },
183
71
  Image: {
184
72
  intrinsic: 'image',
185
- // None. Every default RN's Image applies is already inside the shared mapping (the source
186
- // array shape, the width/height style fold, `alt` -> accessibilityLabel), which the behavior
187
- // runs at commit — so there is nothing left for a compile-time seed to do.
188
73
  },
189
- // The ONLY primitive so far whose intrinsic resolves to a different Fabric component per
190
- // platform — `RCTInputAccessoryView` on iOS, a plain `RCTView` on Android. The fold is
191
- // platform-invariant on purpose; what it does NOT do is repair what sits underneath, where
192
- // upstream RN renders nothing at all off iOS. That divergence is with the owner as its own
193
- // decision.
74
+ // Only primitive whose intrinsic resolves to a DIFFERENT Fabric component per platform:
75
+ // `RCTInputAccessoryView` on iOS, plain `RCTView` on Android.
194
76
  InputAccessoryView: {
195
77
  intrinsic: 'input-accessory-view',
196
- // None. The mapping has no aliasing and no derived value — every consumed name leaves under the
197
- // same name — so there is nothing for a compile-time seed to do.
198
78
  },
199
- // The emptiest entry here, and deliberately so — the withholding protocol has nothing to protect
200
- // for this one. Every other primitive was held back until its runtime half existed and was proven
201
- // against the wrapper's payload; SafeAreaView has no runtime half to build. All five adapters fold
202
- // exactly one thing, `resolveAccessibilityProps`, and that fold already runs in the engine at
203
- // `fabricProps` on both commit paths (the `aria-bag-fold` row). So there is no
204
- // `behaviors/safe-area-view.ts`, and a reader who assumes one exists will go looking for a file
205
- // that was never needed.
206
- //
207
- // Counted before writing, which is the only thing standing behind that claim: five
208
- // implementations, zero shared, none synthesizing a node — each renders ONE
209
- // `safe-area-view` with children on its framework's own channel (React's third argument,
210
- // a Vue slot, a Solid JSX child, Angular's `<ng-content>`, a Svelte snippet). That clears the
211
- // disqualifier in `.claude/rules/host-primitive-tier.md`.
79
+ // No `behaviors/safe-area-view.ts`: its one fold (`resolveAccessibilityProps`) already runs
80
+ // in the engine at `fabricProps` on every commit path.
212
81
  SafeAreaView: {
213
82
  intrinsic: 'safe-area-view',
214
83
  },
215
- // The one primitive that commits NO NODE: its intrinsic resolves to the engine's anchor, and the
216
- // behavior (`src/behaviors/touchable-native-feedback.ts`) clones the owner's props onto the single
217
- // child instead — RN's own shape (TouchableNativeFeedback.js:289,339). Entered in the same commit
218
- // that deletes the five wrappers, because the registry is keyed by TAG: a wrapper still emitting
219
- // its own `pressable` while the behavior is registered would put two press machines on one tree.
220
- //
221
- // No `defaults`: RN's TNF seeds nothing at all — every value it derives (`accessible`,
222
- // `focusable`, `accessibilityState`, the ripple background) depends on ANOTHER prop or on a
223
- // listener, which is a fold and not a default.
84
+ // Commits NO node of its own: the intrinsic resolves to the engine's anchor, and the behavior
85
+ // clones the owner's props onto the single child instead (RN's own shape).
224
86
  TouchableNativeFeedback: {
225
87
  intrinsic: 'touchable-native-feedback',
226
88
  },
227
- // The SECOND primitive that commits no node, same anchor shape and same reason
228
- // (TouchableWithoutFeedback.js:229,286). Its clone list is not TNF's: the passthrough half is
229
- // copied only when SET (:281), there is no ripple, and `onBlur`/`onFocus` are cloned where TNF
230
- // drops them — read `src/behaviors/touchable-without-feedback.ts`'s header rather than inheriting
231
- // the neighbour's fold.
232
- //
233
- // Upstream's passthrough loop lets an explicit `nativeID` win over `id` here (:280-284, unlike
234
- // TNF's :373); NOT reproduced — the engine resolves that precedence once, on the way in, and a
235
- // per-primitive exception to it would be invisible from anywhere the app can see. No `defaults`:
236
- // every value TWF derives depends on another prop or on a listener, which is a fold and not a
237
- // default.
89
+ // Same anchor shape as TouchableNativeFeedback, a DIFFERENT clone list — see
90
+ // `behaviors/touchable-without-feedback.ts`'s header rather than assuming TNF's.
238
91
  TouchableWithoutFeedback: {
239
92
  intrinsic: 'touchable-without-feedback',
240
93
  },
241
- // RN's Button is a touchable wrapping a View wrapping a Text and takes NO children — `title` is a
242
- // string prop (Button.js:363-388) — so the behavior owns the whole subtree and the tag is the
243
- // only spelling. Entered in the same commit that deletes the five wrappers: the registry is keyed
244
- // by TAG, and a wrapper still building its own View/Text under a registered `button` would give
245
- // every existing Button a second copy of the subtree.
246
- //
247
- // `id` used to be PLATFORM-DEPENDENT here, and the record is worth keeping because it is what a
248
- // per-primitive rename costs. Button's touchable is swapped by platform (Button.js:281-284), only
249
- // the iOS arm renamed `id` itself, and with no entry in this table the committed payload read:
250
- //
251
- // iOS nativeID: 'from-id' id: absent the touchable-opacity fold
252
- // Android nativeID: undefined id: 'from-id' a key no ViewConfig declares -> dropped
253
- //
254
- // A rename that runs once, for every node, on the way in cannot produce that shape at all.
255
- //
256
- // No `defaults`: every value RN's Button seeds is derived from another prop or from a listener
257
- // (`accessible`, `focusable`, the greyed label, the uppercased title), which is a fold, not a
258
- // default.
94
+ // Owns its whole subtree (View wrapping Text); RN's Button takes no children, only `title`.
259
95
  Button: {
260
96
  intrinsic: 'button',
261
97
  },
262
- // RN wraps the native spinner in a centering `<View>` (ActivityIndicator.js:112), so this tag is
263
- // that View and the behavior builds `activity-indicator-spinner` under it. Entered in the same
264
- // commit that deletes the five wrappers: the registry is keyed by TAG, and a wrapper still
265
- // painting its own spinner while the behavior is registered would give every indicator two.
266
- //
267
- // `id` renames on the OWNER, before `slotPropsExcept` routes the survivor down — so the key that
268
- // reaches the spinner is `nativeID`, which is where RN's `...restProps` puts it too
269
- // (ActivityIndicator.js:99).
270
- //
271
- // No `defaults`: `animating` and `hidesWhenStopped` ARE `notFalse` folds, but they belong to the
272
- // SPINNER, and this table's ops are applied to the tag's own bag before any slot routing. They
273
- // live in the behavior's spinner fold instead, which is the only layer that can see that node.
98
+ // This tag IS RN's centering wrapper View (ActivityIndicator.js:112); the behavior builds
99
+ // `activity-indicator-spinner` under it.
274
100
  ActivityIndicator: {
275
101
  intrinsic: 'activity-indicator',
276
102
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@symbiote-native/components",
3
- "version": "3.1.1",
3
+ "version": "3.1.3",
4
4
  "description": "Framework-agnostic component logic (state machines + render functions) for SymbioteNative — written once, inherited by every adapter (React, Vue, Angular, ...).",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -59,11 +59,11 @@
59
59
  },
60
60
  "peerDependencies": {
61
61
  "react-native": ">=0.86",
62
- "@symbiote-native/engine": "^1.3.0"
62
+ "@symbiote-native/engine": "^1.4.0"
63
63
  },
64
64
  "devDependencies": {
65
65
  "@types/node": "^26.0.0",
66
- "@symbiote-native/engine": "1.3.0"
66
+ "@symbiote-native/engine": "1.4.0"
67
67
  },
68
68
  "scripts": {
69
69
  "typecheck": "tsc --build",