@symbiote-native/solid 0.1.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 (106) hide show
  1. package/README.md +24 -14
  2. package/build/components/activity-indicator-props.d.ts +5 -0
  3. package/build/components/activity-indicator-props.js +1 -0
  4. package/build/components/button-props.d.ts +5 -0
  5. package/build/components/button-props.js +1 -0
  6. package/build/components/flat-list/index.jsx +3 -3
  7. package/build/components/{image-background.d.ts → image-background-props.d.ts} +2 -3
  8. package/build/components/image-background-props.js +1 -0
  9. package/build/components/image-props.d.ts +5 -0
  10. package/build/components/image-props.js +6 -0
  11. package/build/components/{input-accessory-view.d.ts → input-accessory-view-props.d.ts} +1 -2
  12. package/build/components/input-accessory-view-props.js +5 -0
  13. package/build/components/keyboard-avoiding-view.jsx +9 -4
  14. package/build/components/modal.jsx +6 -6
  15. package/build/components/{pressable.d.ts → pressable-props.d.ts} +8 -6
  16. package/build/components/pressable-props.js +13 -0
  17. package/build/components/{refresh-control.d.ts → refresh-control-props.d.ts} +3 -3
  18. package/build/components/refresh-control-props.js +7 -0
  19. package/build/components/safe-area-view-props.d.ts +10 -0
  20. package/build/components/safe-area-view-props.js +5 -0
  21. package/build/components/{scroll-view/shared.d.ts → scroll-view-props.d.ts} +7 -13
  22. package/build/components/scroll-view-props.js +29 -0
  23. package/build/components/switch-props.d.ts +6 -0
  24. package/build/components/switch-props.js +5 -0
  25. package/build/components/text-input-props.d.ts +8 -0
  26. package/build/components/text-input-props.js +1 -0
  27. package/build/components/{text.d.ts → text-props.d.ts} +3 -3
  28. package/build/components/text-props.js +11 -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-props.d.ts +18 -0
  32. package/build/components/touchable-props.js +1 -0
  33. package/build/components/touchable-without-feedback/touchable-without-feedback-props.d.ts +6 -0
  34. package/build/components/touchable-without-feedback/touchable-without-feedback-props.js +1 -0
  35. package/build/components/{view.d.ts → view-props.d.ts} +2 -3
  36. package/build/components/view-props.js +11 -0
  37. package/build/components/virtualized-list/index.android.js +5 -4
  38. package/build/components/virtualized-list/index.ios.js +6 -6
  39. package/build/components/virtualized-list/shared.d.ts +1 -3
  40. package/build/components/virtualized-list/shared.jsx +87 -98
  41. package/build/components/virtualized-list/sticky-header.d.ts +6 -0
  42. package/build/components/{scroll-view → virtualized-list}/sticky-header.jsx +28 -36
  43. package/build/components.d.ts +20 -30
  44. package/build/components.js +6 -33
  45. package/build/create-portal/index.jsx +1 -1
  46. package/build/descriptor-to-solid.js +1 -1
  47. package/build/host-instance.js +3 -3
  48. package/build/index.d.ts +2 -0
  49. package/build/index.js +13 -3
  50. package/build/jsx-runtime.d.ts +39 -2
  51. package/build/modules/animated/create-animated-component.d.ts +1 -1
  52. package/build/modules/animated/create-animated-component.jsx +22 -0
  53. package/build/modules/animated/index.d.ts +0 -4
  54. package/build/modules/animated/index.js +16 -22
  55. package/build/modules/image.d.ts +3 -0
  56. package/build/modules/image.js +12 -0
  57. package/build/register.d.ts +1 -0
  58. package/build/register.js +15 -0
  59. package/build/renderer.d.ts +1 -0
  60. package/build/renderer.js +133 -6
  61. package/build/utils/stable-keys.d.ts +1 -1
  62. package/build/utils/stable-keys.js +6 -5
  63. package/metro-css-parser.cjs +2 -1
  64. package/package.json +18 -7
  65. package/build/components/activity-indicator/index.android.d.ts +0 -2
  66. package/build/components/activity-indicator/index.android.js +0 -11
  67. package/build/components/activity-indicator/index.d.ts +0 -1
  68. package/build/components/activity-indicator/index.ios.d.ts +0 -2
  69. package/build/components/activity-indicator/index.ios.js +0 -10
  70. package/build/components/activity-indicator/index.js +0 -4
  71. package/build/components/activity-indicator/shared.d.ts +0 -8
  72. package/build/components/activity-indicator/shared.js +0 -51
  73. package/build/components/button.d.ts +0 -7
  74. package/build/components/button.jsx +0 -45
  75. package/build/components/image-background.jsx +0 -74
  76. package/build/components/image.d.ts +0 -9
  77. package/build/components/image.js +0 -75
  78. package/build/components/input-accessory-view.jsx +0 -39
  79. package/build/components/pressable.jsx +0 -185
  80. package/build/components/refresh-control.jsx +0 -36
  81. package/build/components/safe-area-view.d.ts +0 -10
  82. package/build/components/safe-area-view.jsx +0 -24
  83. package/build/components/scroll-view/index.android.d.ts +0 -2
  84. package/build/components/scroll-view/index.android.js +0 -11
  85. package/build/components/scroll-view/index.d.ts +0 -2
  86. package/build/components/scroll-view/index.ios.d.ts +0 -2
  87. package/build/components/scroll-view/index.ios.js +0 -8
  88. package/build/components/scroll-view/index.js +0 -9
  89. package/build/components/scroll-view/shared.jsx +0 -334
  90. package/build/components/scroll-view/sticky-header.d.ts +0 -18
  91. package/build/components/switch/index.android.d.ts +0 -2
  92. package/build/components/switch/index.android.js +0 -12
  93. package/build/components/switch/index.d.ts +0 -1
  94. package/build/components/switch/index.ios.d.ts +0 -2
  95. package/build/components/switch/index.ios.js +0 -11
  96. package/build/components/switch/index.js +0 -4
  97. package/build/components/switch/shared.d.ts +0 -11
  98. package/build/components/switch/shared.js +0 -87
  99. package/build/components/text-input.d.ts +0 -10
  100. package/build/components/text-input.js +0 -268
  101. package/build/components/text.jsx +0 -27
  102. package/build/components/touchable/index.d.ts +0 -22
  103. package/build/components/touchable/index.jsx +0 -247
  104. package/build/components/touchable-native-feedback/index.d.ts +0 -16
  105. package/build/components/touchable-native-feedback/index.jsx +0 -68
  106. package/build/components/view.jsx +0 -49
@@ -1,9 +1,46 @@
1
- import type { ISymbioteIntrinsic } from '@symbiote-native/components';
1
+ import type { ICrossTypedIntrinsics } from '@symbiote-native/components';
2
2
  import type { ISymbioteNode } from '@symbiote-native/engine';
3
+ import type { IViewProps } from './components/view-props';
4
+ import type { ITextProps } from './components/text-props';
5
+ import type { IPressableProps } from './components/pressable-props';
6
+ import type { IButtonProps } from './components/button-props';
7
+ import type { IImageProps } from './components/image-props';
8
+ import type { IImageBackgroundProps } from './components/image-background-props';
9
+ import type { IInputAccessoryViewProps } from './components/input-accessory-view-props';
10
+ import type { IRefreshControlProps } from './components/refresh-control-props';
11
+ import type { ISafeAreaViewProps } from './components/safe-area-view-props';
12
+ import type { ITouchableNativeFeedbackProps } from './components/touchable-native-feedback/touchable-native-feedback-props';
13
+ import type { ITouchableOpacityProps, ITouchableHighlightProps } from './components/touchable-props';
14
+ import type { ITouchableWithoutFeedbackProps } from './components/touchable-without-feedback/touchable-without-feedback-props';
15
+ import type { IScrollViewProps } from './components/scroll-view-props';
16
+ import type { ISwitchProps } from './components/switch-props';
17
+ import type { ITextInputProps } from './components/text-input-props';
18
+ import type { IModalProps } from './components/modal';
19
+ import type { IActivityIndicatorProps } from './components/activity-indicator-props';
3
20
  type ISymbioteHostAttributes = {
4
21
  readonly children?: JSX.Element;
5
22
  readonly [attribute: string]: unknown;
6
23
  };
24
+ interface ICrossedPrimitiveProps {
25
+ view: IViewProps;
26
+ text: ITextProps;
27
+ pressable: IPressableProps;
28
+ button: IButtonProps;
29
+ image: IImageProps;
30
+ 'image-background': IImageBackgroundProps;
31
+ 'input-accessory-view': IInputAccessoryViewProps;
32
+ 'refresh-control': IRefreshControlProps;
33
+ 'safe-area-view': ISafeAreaViewProps;
34
+ 'touchable-native-feedback': ITouchableNativeFeedbackProps;
35
+ 'touchable-opacity': ITouchableOpacityProps;
36
+ 'touchable-highlight': ITouchableHighlightProps;
37
+ 'touchable-without-feedback': ITouchableWithoutFeedbackProps;
38
+ 'scroll-view': IScrollViewProps;
39
+ switch: ISwitchProps;
40
+ 'text-input': ITextInputProps;
41
+ modal: IModalProps;
42
+ 'activity-indicator': IActivityIndicatorProps;
43
+ }
7
44
  export declare namespace JSX {
8
45
  type Element = ISymbioteNode | IArrayElement | IFunctionElement | (string & {}) | number | boolean | null | undefined;
9
46
  interface IArrayElement extends Array<Element> {
@@ -20,7 +57,7 @@ export declare namespace JSX {
20
57
  }
21
58
  interface IntrinsicAttributes {
22
59
  }
23
- interface IntrinsicElements extends Record<ISymbioteIntrinsic, ISymbioteHostAttributes> {
60
+ interface IntrinsicElements extends ICrossTypedIntrinsics<ISymbioteHostAttributes, ICrossedPrimitiveProps> {
24
61
  }
25
62
  }
26
63
  export {};
@@ -7,4 +7,4 @@ export interface IAnimatedComponentProps {
7
7
  children?: JSX.Element;
8
8
  [key: string]: unknown;
9
9
  }
10
- export declare function createAnimatedComponent(Base: Component<any>): (props: IAnimatedComponentProps) => JSX.Element;
10
+ export declare function createAnimatedComponent(Base: Component<any> | string): (props: IAnimatedComponentProps) => JSX.Element;
@@ -23,6 +23,7 @@
23
23
  // next reconcile drops.
24
24
  import { createEffect, mergeProps, onCleanup, splitProps, } from 'solid-js';
25
25
  import { createAnimatedLeafLifecycle, isNativeAnimatedAvailable, isSymbioteNode, readPassthroughStyle, reduceProps, resolveHostNode, whenCommitted, } from '@symbiote-native/engine';
26
+ import { createElement, spread } from '../../renderer.js';
26
27
  import { withStableKeys } from '../../utils/stable-keys.js';
27
28
  // RN's prop carrying already-rasterized values that OVERRIDE the animated prop in the committed
28
29
  // props, so the ShadowTree (hit-testing) stays current while the native driver animates.
@@ -31,6 +32,15 @@ const PASSTHROUGH_PROP = 'passthroughAnimatedPropExplicitValues';
31
32
  // from). A precise generic cannot work: the bag handed to the base is assembled at RUNTIME from a
32
33
  // key set that grows, so no static prop type describes it. The RETURN type stays precise, so a
33
34
  // caller of Animated.View still gets a real prop type.
35
+ // `Base` accepts a TAG as well as a component, and the string branch is not symmetry — it is the
36
+ // only shape that works once a primitive becomes a public intrinsic. `<Base {...props} />` on a
37
+ // capitalized identifier compiles to `createComponent(Base, …)`, which is `untrack(() => Comp(props))`
38
+ // in solid-js, so a string base is a `TypeError: Comp is not a function` at first paint. There is
39
+ // nothing to widen INTO either: `createRenderer()` from solid-js/universal returns twelve names and
40
+ // `Dynamic` is not among them (solid-js/web's Dynamic is DOM-only), so the element has to be built
41
+ // through this renderer's own createElement + spread — the same two calls solid's own Dynamic makes
42
+ // on its string branch. `spread` handles `ref` and `children` itself (universal's spreadExpression
43
+ // skips both in its prop loop and drives them separately), which is why nothing is threaded by hand.
34
44
  export function createAnimatedComponent(Base) {
35
45
  return function AnimatedComponent(props) {
36
46
  // The passthrough is CONSUMED (it only overrides the committed style below); `children` and
@@ -100,6 +110,18 @@ export function createAnimatedComponent(Base) {
100
110
  return local.children;
101
111
  },
102
112
  });
113
+ // Narrowed HERE and not around the whole factory: `Base` is a parameter captured by this inner
114
+ // function, so a check outside it does not reach the JSX below.
115
+ if (typeof Base === 'string') {
116
+ const element = createElement(Base);
117
+ spread(element, childProps, false);
118
+ // createElement's return type is the renderer's node union, which includes the SURFACE — a
119
+ // root the renderer is handed, never something a tag produces. Narrowed rather than cast, and
120
+ // thrown rather than defaulted: returning null here would paint nothing and stay green.
121
+ if (!isSymbioteNode(element))
122
+ throw new Error(`createAnimatedComponent: <${Base}> did not build a host node`);
123
+ return element;
124
+ }
103
125
  return <Base {...childProps}/>;
104
126
  };
105
127
  }
@@ -44,10 +44,6 @@ export declare const Animated: {
44
44
  event: typeof event;
45
45
  forkEvent: typeof forkEvent;
46
46
  unforkEvent: typeof unforkEvent;
47
- View: (props: import("./create-animated-component").IAnimatedComponentProps) => import("@symbiote-native/solid/jsx-runtime").JSX.Element;
48
- Text: (props: import("./create-animated-component").IAnimatedComponentProps) => import("@symbiote-native/solid/jsx-runtime").JSX.Element;
49
- Image: (props: import("./create-animated-component").IAnimatedComponentProps) => import("@symbiote-native/solid/jsx-runtime").JSX.Element;
50
- ScrollView: IAnimatedComponent;
51
47
  FlatList: IAnimatedComponent;
52
48
  SectionList: IAnimatedComponent;
53
49
  createAnimatedComponent: typeof createAnimatedComponent;
@@ -1,19 +1,25 @@
1
- // The Animated namespace for @symbiote-native/solid. createAnimatedComponent applied to this
2
- // adapter's primitives gives the six animated components RN exposes; the value graph, easing and
3
- // imperative drivers come from @symbiote-native/engine (framework-agnostic), so both halves meet
4
- // here in one object and `Animated.timing(new Animated.Value(0), …).start()` reads as it does in
5
- // RN. Solid's JSX takes the dotted form directly — `<Animated.View/>` compiles to
6
- // createComponent(Animated.View, …), no local alias needed.
1
+ // The Animated namespace for @symbiote-native/solid. The value graph, easing and imperative drivers
2
+ // come from @symbiote-native/engine (framework-agnostic), so both halves meet here in one object and
3
+ // `Animated.timing(new Animated.Value(0), …).start()` reads as it does in RN.
4
+ //
5
+ // THERE IS NO `Animated.View` / `Animated.Text` / `Animated.Image` / `Animated.ScrollView`, and
6
+ // nothing replaced any of them: `<view style={{ opacity: someAnimatedValue }}>` is the whole API
7
+ // for style, and `<scroll-view onScroll={Animated.event(…)}>` is the whole API for a native-driven
8
+ // scroll listener. The engine resolves an AnimatedNode written into any prop of any host node
9
+ // (`core/engine/src/animated/host-binding.ts`) — it publishes the current value, subscribes the
10
+ // leaf, writes each frame through its own targeted setNativeProps commit, and releases at the
11
+ // commit sweep; `setEventListener` calls `bindAnimatedEvent` on any `on*` prop for EVERY host node,
12
+ // not just a scroll one. Those four were aliases of the wrappers, and a wrapper is a tag now, which
13
+ // is not a value `<Animated.X>` can call.
14
+ //
15
+ // The two below REMAIN because they are tier 3: a render prop decides the list family's output
16
+ // shape in JS, so there is no tag for `<Animated.X>` to be an alias of.
7
17
  //
8
18
  // The NATIVE DRIVER is not this module's doing and must not be re-derived here. A
9
19
  // useNativeDriver animation promotes the value graph itself (animations/base.ts ->
10
20
  // value.__startNativeAnimation -> graph.__makeNative), and the props leaf follows automatically
11
21
  // through graph.__addChild. This file only assembles the surface.
12
22
  import { AnimatedValue, AnimatedValueXY, AnimatedColor, AnimatedMock, Easing, Platform, timing, spring, decay, parallel, sequence, stagger, loop, delay, add, subtract, multiply, divide, modulo, diffClamp, event, forkEvent, unforkEvent, } from '@symbiote-native/engine';
13
- import { View } from '../../components/view.jsx';
14
- import { Text } from '../../components/text.jsx';
15
- import { Image } from '../../components/image.js';
16
- import { ScrollView } from '../../components/scroll-view';
17
23
  import { FlatList } from '../../components/flat-list/index.jsx';
18
24
  import { SectionList } from '../../components/section-list/index.jsx';
19
25
  import { createAnimatedComponent } from './create-animated-component.jsx';
@@ -21,11 +27,6 @@ export { createAnimatedComponent } from './create-animated-component.jsx';
21
27
  // The pure graph leaves live in the engine (they extend AnimatedWithChildren, no framework);
22
28
  // re-exported so this adapter's Animated surface matches React's.
23
29
  export { AnimatedProps, AnimatedStyle, AnimatedTransform, } from '@symbiote-native/engine';
24
- // View/Text/Image are pure host primitives with no cycle back to here, so wrap them eagerly.
25
- const AnimatedView = createAnimatedComponent(View);
26
- const AnimatedText = createAnimatedComponent(Text);
27
- const AnimatedImage = createAnimatedComponent(Image);
28
- let animatedScrollView;
29
30
  let animatedFlatList;
30
31
  let animatedSectionList;
31
32
  // The live, JS-driven driver half. RN's AnimatedImplementation.
@@ -58,13 +59,6 @@ const liveDrivers = {
58
59
  // spreads `...Animated` over the same component getters.
59
60
  const drivers = Platform.isDisableAnimations ? AnimatedMock : liveDrivers;
60
61
  export const Animated = {
61
- View: AnimatedView,
62
- Text: AnimatedText,
63
- Image: AnimatedImage,
64
- get ScrollView() {
65
- animatedScrollView ??= createAnimatedComponent(ScrollView);
66
- return animatedScrollView;
67
- },
68
62
  get FlatList() {
69
63
  animatedFlatList ??= createAnimatedComponent(FlatList);
70
64
  return animatedFlatList;
@@ -0,0 +1,3 @@
1
+ import { type IImageStatics } from '@symbiote-native/components';
2
+ export type { IImageStatics };
3
+ export declare const Image: IImageStatics;
@@ -0,0 +1,12 @@
1
+ // RN's Image STATICS — `getSize`, `prefetch`, `queryCache`, `abortPrefetch`, `resolveAssetSource`.
2
+ //
3
+ // IN `modules/`, NOT `components/`. The primitive is the `<image>` tag and its fold runs in the
4
+ // engine (`behaviors/image.ts`); what survives under the name `Image` is an imperative API with no
5
+ // view, which is what this bucket is for. Sitting in `components/` was what made a statics object
6
+ // read as a component.
7
+ //
8
+ // A NAMESPACE OBJECT rather than five bare functions, deliberately: `Alert`, `Share`, `Linking`
9
+ // and `Vibration` all take that shape, and an app porting from RN writes `Image.getSize(...)`
10
+ // verbatim. Shared verbatim via `imageStatics`, the same source every other adapter attaches.
11
+ import { imageStatics } from '@symbiote-native/components';
12
+ export const Image = imageStatics;
@@ -0,0 +1 @@
1
+ import '@symbiote-native/components/register';
@@ -0,0 +1,15 @@
1
+ // Side-effect ONLY. Exports nothing, and must never be re-exported from the barrel.
2
+ //
3
+ // The fourteen calls live once, in `@symbiote-native/components/register` — they were identical in
4
+ // all five adapters, and each copy carried a migration ledger justifying a call against a wrapper
5
+ // that no longer exists. What is per-adapter is only the SPELLING of the import below: every
6
+ // `*-tag.test.*` here reaches the registry through `import './register';`, and this adapter's own
7
+ // barrel guard pins that line.
8
+ //
9
+ // WHY A SIDE-EFFECT SUBPATH AND NOT A BARREL EXPORT. Metro turns on `inlineRequires` for PRODUCTION
10
+ // only: it moves a `require` down to the first place its binding is used as a VALUE, and
11
+ // `export { X } from './x'` compiles to a lazy getter. A registration reached that way never
12
+ // evaluates in a Release build — invisible to tsc, to vitest, and to grepping the bundle. A bare
13
+ // `import` placed NEXT TO a re-export of the same specifier does not help either: Babel merges the
14
+ // two into one dependency and the merged dependency stays lazy.
15
+ import '@symbiote-native/components/register';
@@ -2,6 +2,7 @@ import { SymbioteSurface, type ISymbioteNode } from '@symbiote-native/engine';
2
2
  type IHostNode = ISymbioteNode | SymbioteSurface;
3
3
  export declare function setActiveSurface(surface: SymbioteSurface | undefined): void;
4
4
  export declare function replaceText(textNode: IHostNode, value: string): void;
5
+ export declare function setHostPressed(node: IHostNode, pressed: boolean): void;
5
6
  export declare function removeNode(parent: IHostNode, node: IHostNode): void;
6
7
  export declare const render: (code: () => IHostNode, node: IHostNode) => () => void, effect: <T>(fn: (prev?: T) => T, init?: T) => void, memo: <T>(fn: () => T, equal: boolean) => () => T, createComponent: <T>(Comp: (props: T) => IHostNode, props: T) => IHostNode, createElement: (tag: string) => IHostNode, createTextNode: (value: string) => IHostNode, insertNode: (parent: IHostNode, node: IHostNode, anchor?: IHostNode | undefined) => void, insert: <T>(parent: any, accessor: T | (() => T), marker?: any | null, initial?: any) => IHostNode, spread: <T>(node: any, accessor: (() => T) | T, skipChildren?: boolean) => void, setProp: <T>(node: IHostNode, name: string, value: T, prev?: T | undefined) => T, mergeProps: (...sources: unknown[]) => unknown, use: <A, T>(fn: (element: IHostNode, arg: A) => T, element: IHostNode, arg: A) => T;
7
8
  export {};
package/build/renderer.js CHANGED
@@ -17,8 +17,8 @@
17
17
  // babel.transformSync over representative JSX (elements, dynamic text, <Show>, <For>, spread,
18
18
  // ref) rather than read off the docs.
19
19
  import { createRenderer } from 'solid-js/universal';
20
- import { appendChild, createAnchor, createElement as createEngineElement, createRawText, dlog, insertBefore, removeChild as removeEngineChild, routeProp, setText as setEngineText, toPublicInstance, RAW_TEXT_COMPONENT, SymbioteSurface, } from '@symbiote-native/engine';
21
- import { descriptorFor } from '@symbiote-native/components';
20
+ import { appendChild, createAnchor, createElement as createEngineElement, createRawText, dlog, insertBefore, removeChild as removeEngineChild, routeProp, setNodePressed, setProp as setEngineProp, setText as setEngineText, toPublicInstance, RAW_TEXT_COMPONENT, SymbioteSurface, } from '@symbiote-native/engine';
21
+ import { descriptorFor, TEXT_INPUT_MULTILINE_TAG, } from '@symbiote-native/components';
22
22
  function isSurface(node) {
23
23
  return node instanceof SymbioteSurface;
24
24
  }
@@ -84,6 +84,23 @@ function asText(value) {
84
84
  // has to DETACH its anchor host from the portal target on cleanup. Going through here rather than
85
85
  // calling the engine's removeChild directly is what keeps the mutation paired with requestCommit(),
86
86
  // and keeps the surface-vs-node branch in one place.
87
+ // The REFUSED path's half of `:active`, and the reason a refusal costs the component instance
88
+ // rather than the pressed styling. A `<Pressable>` the lowering left as a component still owns its
89
+ // own press machine, and one call here puts the node into its pressed state so `.btn:active`
90
+ // applies exactly as it would on a lowered tag.
91
+ //
92
+ // Exported paired with requestCommit() for the same reason removeNode below is: the press arrives
93
+ // from a NATIVE EVENT, outside any renderer mutation, so nothing else schedules a commit. React's
94
+ // twin (`setNodeHidden` from hideInstance) needs none because its reconciler is already mid-commit
95
+ // when it calls; Solid's is not. Costs nothing when no `:active` rule is registered — the engine
96
+ // hands back the same style object and `isAlreadyPublished` turns the re-push away without dirtying
97
+ // the node.
98
+ export function setHostPressed(node, pressed) {
99
+ if (isSurface(node))
100
+ return;
101
+ setNodePressed(node, pressed);
102
+ requestCommit();
103
+ }
87
104
  export function removeNode(parent, node) {
88
105
  if (isSurface(node))
89
106
  return;
@@ -93,10 +110,89 @@ export function removeNode(parent, node) {
93
110
  removeEngineChild(parent, node);
94
111
  requestCommit();
95
112
  }
113
+ const TEXT_FOLDS = new Map([
114
+ ['ellipsizeMode', value => value ?? 'tail'],
115
+ ['allowFontScaling', value => value !== false],
116
+ ]);
117
+ function seedTextDefaults(node) {
118
+ for (const [key, fold] of TEXT_FOLDS)
119
+ setEngineProp(node, key, fold(undefined));
120
+ }
121
+ // Seeding at CREATE is not enough, and the gap is device-only. A framework that clears a prop it
122
+ // set earlier hands us an explicit `undefined` at PATCH time, and the default has to come BACK
123
+ // rather than stay cleared — RN treats a missing prop and an explicit undefined alike, and only a
124
+ // literal `false` opts out of allowFontScaling. Two Map lookups on text nodes only, and none at
125
+ // all on a View, so it stays off the hot path.
126
+ function foldTextValue(node, key, value) {
127
+ if (!node.isText)
128
+ return value;
129
+ const fold = TEXT_FOLDS.get(key);
130
+ return fold === undefined ? value : fold(value);
131
+ }
132
+ // The alias fold, at the RENDERER — the only place it can live now that an app writes the tag
133
+ // itself. A bare `<view id="x">` has no wrapper to inherit the fold from, and `id` is a key Fabric
134
+ // does not know, so it committed nothing while the component committed `nativeID`. Measured by
135
+ // mounting both forms and diffing committed key NAMES; totals were identical and said nothing.
136
+ //
137
+ // One string comparison rather than a Map lookup, because this sits on the per-prop write path —
138
+ // 32 001 prop writes on a benchmark create, where a Map.get is the kind of cost the engine spent
139
+ // this month removing. That is only safe while every primitive shares ONE alias pair, which is a
140
+ // property of the shared spec and not of this file, so `renderer-alias-fold.test.ts` re-derives
141
+ // both constants from `HOST_PRIMITIVES` and fails the moment a second pair appears.
142
+ const ALIAS_FROM = 'id';
143
+ const ALIAS_TO = 'nativeID';
144
+ // `multiline` selects between TWO Fabric views, so the TAG decides and no prop write moves a node
145
+ // between them. Two of the three paths that can build the node resolve it earlier — the wrapper
146
+ // CONSUMES the prop to pick its intrinsic, a lowering transform reads a literal at compile time —
147
+ // and an author writing the tag by hand has neither, which leaves two silent divergences.
148
+ //
149
+ // The shared behavior (`core/components/src/behaviors/text-input.ts`) already makes the PAYLOAD
150
+ // follow the tag. The complaint has to live here instead of there: `foldPayload` runs inside the
151
+ // commit, so a throw from it lands a tick later as an uncaught exception with no frame naming the
152
+ // call site — measured, a test awaiting the mount sees `nothing committed` rather than the error.
153
+ const MULTILINE_PROP = 'multiline';
154
+ function assertMultilineMatchesTag(node, value) {
155
+ if ((value === true) === (node.props[MULTILINE_PROP] === true))
156
+ return;
157
+ throw new Error(`multiline={${String(value)}} contradicts the tag: <text-input> and ` +
158
+ `<text-input-multiline> are different Fabric views and no prop write moves a node ` +
159
+ `between them. Pick the tag (a runtime choice needs a <Show> around both).`);
160
+ }
161
+ // RN gives `id` UNCONDITIONAL priority when both are set (View.js:77-79,
162
+ // `processedProps.nativeID = id`), and `foldHostBag` reproduces that by deleting the source key out
163
+ // of a whole bag. This renderer never sees a bag — it folds one key at a time — so precedence would
164
+ // otherwise come out as source order, and `<view id nativeID>` would keep the stale legacy value
165
+ // while `<view nativeID id>` would not. The wrapper hid that; a bare tag does not.
166
+ //
167
+ // So the node remembers that its nativeID came from an `id`, and a later raw `nativeID` write loses
168
+ // to it. Off the hot path in every ordinary case: nothing is touched unless the prop being written
169
+ // is one of these two names.
170
+ const aliasedNodes = new WeakSet();
171
+ function foldAliasKey(node, name, value) {
172
+ if (name === ALIAS_FROM) {
173
+ if (value === undefined)
174
+ aliasedNodes.delete(node);
175
+ else
176
+ aliasedNodes.add(node);
177
+ return ALIAS_TO;
178
+ }
179
+ return name;
180
+ }
96
181
  const nodeOps = {
97
182
  createElement(tag) {
98
183
  const descriptor = descriptorFor(tag);
99
- const node = createEngineElement(descriptor.component, descriptor.isText);
184
+ // The TAG goes over as well, not just the resolved Fabric name. The host-behavior registry is
185
+ // keyed by intrinsic tag while a node only ever carries the resolved view name — `symbiote-
186
+ // pressable` resolves to `RCTView` — so without this the lookup asks for `RCTView` and finds
187
+ // nothing, and the press machine silently never attaches. Registering under the Fabric name
188
+ // instead would be worse: every plain `View` would get a press machine.
189
+ const node = createEngineElement(descriptor.component, descriptor.isText, tag);
190
+ if (descriptor.isText)
191
+ seedTextDefaults(node);
192
+ // Only the multiline tag is seeded: writing `multiline: false` on the single-line one would add
193
+ // a key the wrapper's payload does not carry, i.e. a divergence in the other direction.
194
+ if (tag === TEXT_INPUT_MULTILINE_TAG)
195
+ setEngineProp(node, MULTILINE_PROP, true);
100
196
  // Graft the imperative public-instance API (measure / setNativeProps / focus / …) onto the raw
101
197
  // node so a `ref` to a host element exposes it exactly like React's getPublicInstance.
102
198
  // toPublicInstance mutates in place and returns the SAME node identity, so the engine's commit
@@ -132,11 +228,15 @@ const nodeOps = {
132
228
  setProperty(node, name, value) {
133
229
  if (isSurface(node))
134
230
  return;
231
+ if (name === MULTILINE_PROP)
232
+ assertMultilineMatchesTag(node, value);
233
+ if (name === ALIAS_TO && aliasedNodes.has(node))
234
+ return;
135
235
  // routeProp makes the prop-vs-event decision from the node's ViewConfig (onPress on a View
136
236
  // becomes a listener; onTintColor on a Switch stays a prop), and centralizes the class+style
137
237
  // merge. Shared with React and Vue — never re-implement an `onX` check here
138
238
  // (symbiote-engine-core §2).
139
- routeProp(node, name, value);
239
+ routeProp(node, foldAliasKey(node, name, value), foldTextValue(node, name, value));
140
240
  requestCommit();
141
241
  },
142
242
  insertNode(parent, node, anchor) {
@@ -168,10 +268,27 @@ const nodeOps = {
168
268
  requestCommit();
169
269
  },
170
270
  removeNode,
271
+ // Reverse of `getFirstChild`'s `childHost` redirect: a child under a composed primitive's slot
272
+ // has `node.parent` pointing at the SLOT, but `cleanChildren`'s `isParent = getParentNode(el)
273
+ // === parent` compares against the OWNER — the same reference the JSX-level `insert()` call was
274
+ // given. Left un-redirected, that comparison is false for every child of a bare `<scroll-view>`
275
+ // (or any other slotted primitive) with a following sibling, so `isParent` never fires,
276
+ // `removeNode` is never called on a full array clear, and the old children are orphaned: still
277
+ // committed to Fabric, still retained, and re-diffed against on the next reconciliation —
278
+ // reproduced as unbounded RAM growth and an increasingly slow "Clear" on the benchmark screen.
279
+ // One level up only: `childHost` is always a DIRECT child the owner built (see `buildStructure`),
280
+ // never a deeper descendant.
171
281
  getParentNode(node) {
172
282
  if (isSurface(node))
173
283
  return undefined;
174
- return node.parent ?? activeSurface;
284
+ const parent = node.parent ?? activeSurface;
285
+ if (parent === undefined || isSurface(parent))
286
+ return parent;
287
+ const grandparent = parent.parent;
288
+ if (grandparent !== undefined && grandparent.childHost === parent) {
289
+ return grandparent;
290
+ }
291
+ return parent;
175
292
  },
176
293
  // Anchors are NOT filtered out of this or getNextSibling, deliberately. solid-js/universal keeps
177
294
  // its own record of which nodes it put where and re-derives positions through these two lookups
@@ -179,8 +296,18 @@ const nodeOps = {
179
296
  // getFirstChild(parent) to replace text in place). Hiding a node the runtime itself inserted
180
297
  // desyncs that record from the real tree. Anchors are invisible to FABRIC — the commit walk skips
181
298
  // them — not to tree traversal.
299
+ //
300
+ // `childHost` redirected, same as the engine's own `appendChild`/`insertBefore`/`removeChild`
301
+ // (`hostFor`, core/engine/src/node.ts): a composed primitive's real children live on the slot it
302
+ // built, not on the owner it was asked to insert into, and `insert()`'s reconciliation reads this
303
+ // to know what is ALREADY there before deciding what to insert/move/remove. Reading the owner's
304
+ // own (single-child) array here would have every dynamic child on a bare `<scroll-view>` diffed
305
+ // against the wrong node — invisible on VirtualizedList, which never calls `insert()` on the
306
+ // owner directly, and wrong for any app writing `<scroll-view>{dynamicChildren}</scroll-view>`.
182
307
  getFirstChild(node) {
183
- return node.children[0];
308
+ if (isSurface(node))
309
+ return node.children[0];
310
+ return (node.childHost ?? node).children[0];
184
311
  },
185
312
  getNextSibling(node) {
186
313
  if (isSurface(node))
@@ -1 +1 @@
1
- export declare function withStableKeys(props: () => Record<string, unknown>): () => Record<string, unknown>;
1
+ export declare function withStableKeys<TProps extends object>(props: () => TProps): () => Record<string, unknown>;
@@ -12,9 +12,10 @@
12
12
  // the actual diffing. Never hand-roll that diff instead.
13
13
  //
14
14
  // Lives here rather than beside either caller because BOTH reach for it: descriptorToSolid wraps
15
- // a render fn's Descriptor props, and the host primitives (View, Text) wrap their own
16
- // resolveAccessibilityProps output — the same function, the same two-branch key set. Any future
17
- // Solid component that folds props through a whole-object transform belongs on this too.
15
+ // a render fn's Descriptor props, and a component that spreads a folded bag onto a bare `<view>`
16
+ // (Pressable, KeyboardAvoidingView) wraps its own — the same function, the same two-branch key
17
+ // set. This is the one capability a bare tag does NOT inherit from the wrappers that used to own
18
+ // it, so any Solid component folding props through a whole-object transform belongs on it too.
18
19
  export function withStableKeys(props) {
19
20
  const seen = new Set();
20
21
  return () => {
@@ -22,9 +23,9 @@ export function withStableKeys(props) {
22
23
  const widened = {};
23
24
  for (const key of seen)
24
25
  widened[key] = undefined;
25
- for (const key of Object.keys(next)) {
26
+ for (const [key, value] of Object.entries(next)) {
26
27
  seen.add(key);
27
- widened[key] = next[key];
28
+ widened[key] = value;
28
29
  }
29
30
  return widened;
30
31
  };
@@ -5,4 +5,5 @@
5
5
  // .cjs, not .js: this package is "type": "module", but Metro's babelTransformerPath loading needs
6
6
  // a require()-able file. createCssMetroTransformer() is a factory — Metro needs the actual
7
7
  // {transform, getCacheKey} object it returns, not the css-parser package barrel.
8
- module.exports = require('@symbiote-native/css-parser').createCssMetroTransformer();
8
+ module.exports =
9
+ require('@symbiote-native/css-parser').createCssMetroTransformer();
package/package.json CHANGED
@@ -1,7 +1,17 @@
1
1
  {
2
2
  "name": "@symbiote-native/solid",
3
- "version": "0.1.0",
3
+ "version": "2.0.0",
4
4
  "description": "SymbioteNative's Solid adapter — a solid-js/universal createRenderer driving real native iOS/Android views through the same engine as every other adapter.",
5
+ "keywords": [
6
+ "react-native",
7
+ "symbiote-native",
8
+ "solid",
9
+ "solid-js",
10
+ "fabric",
11
+ "cross-platform",
12
+ "ios",
13
+ "android"
14
+ ],
5
15
  "license": "MIT",
6
16
  "repository": {
7
17
  "type": "git",
@@ -52,18 +62,19 @@
52
62
  "dependencies": {
53
63
  "babel-preset-solid": "^1.9.12",
54
64
  "typescript": "~6.0.0",
55
- "@symbiote-native/components": "0.5.0",
56
- "@symbiote-native/css-parser": "0.4.0"
65
+ "@symbiote-native/components": "2.0.0",
66
+ "@symbiote-native/css-parser": "0.5.0"
57
67
  },
58
68
  "peerDependencies": {
59
- "@symbiote-native/engine": ">=0.1.7",
60
69
  "solid-js": ">=1.9.0",
61
- "react-native": ">=0.86"
70
+ "react-native": ">=0.86",
71
+ "@symbiote-native/engine": "^0.5.0"
62
72
  },
63
73
  "devDependencies": {
74
+ "@babel/core": "^7.25.2",
64
75
  "solid-js": "^1.9.14",
65
- "@symbiote-native/engine": "0.3.0",
66
- "@symbiote-native/test-utils": "0.2.0"
76
+ "@symbiote-native/engine": "0.5.0",
77
+ "@symbiote-native/test-utils": "0.3.0"
67
78
  },
68
79
  "scripts": {
69
80
  "typecheck": "tsc --build",
@@ -1,2 +0,0 @@
1
- export type { IActivityIndicatorProps, IActivityIndicatorSize } from './shared';
2
- export declare const ActivityIndicator: (props: import("./shared").IActivityIndicatorProps) => import("@symbiote-native/solid/jsx-runtime").JSX.Element;
@@ -1,11 +0,0 @@
1
- // ActivityIndicator on Android: AndroidProgressBar needs `styleAttr` (it drives its setStyle();
2
- // without it the view throws "setStyle() not called") plus `indeterminate: true`, and its default
3
- // color is the theme (null), which the render fn turns into an OMITTED color prop — a literal null
4
- // would be rejected by Fabric's color parser. Same facts the other adapters' Android bindings
5
- // encode. Metro picks this on an Android host; no Platform.OS read.
6
- import { createActivityIndicator } from './shared.js';
7
- export const ActivityIndicator = createActivityIndicator({
8
- // RN: `color = Platform.OS === 'ios' ? GRAY : null`; Android lets the theme color it.
9
- defaultColor: null,
10
- nativeExtras: { styleAttr: 'Normal', indeterminate: true },
11
- });
@@ -1 +0,0 @@
1
- export * from './index.ios';
@@ -1,2 +0,0 @@
1
- export type { IActivityIndicatorProps, IActivityIndicatorSize } from './shared';
2
- export declare const ActivityIndicator: (props: import("./shared").IActivityIndicatorProps) => import("@symbiote-native/solid/jsx-runtime").JSX.Element;
@@ -1,10 +0,0 @@
1
- // ActivityIndicator on iOS: RCTActivityIndicatorView takes the size enum plus a GRAY default color
2
- // and no extra native props — the same facts adapters/react's, adapters/vue's and adapters/svelte's
3
- // iOS bindings encode. Metro selects this file on an iOS host, no Platform.OS read.
4
- import { createActivityIndicator } from './shared.js';
5
- // RN's iOS default spinner color (Libraries/.../ActivityIndicator.js GRAY).
6
- const IOS_DEFAULT_COLOR = '#999999';
7
- export const ActivityIndicator = createActivityIndicator({
8
- defaultColor: IOS_DEFAULT_COLOR,
9
- nativeExtras: {},
10
- });
@@ -1,4 +0,0 @@
1
- // Base / default ActivityIndicator: re-exports the iOS build. Metro overrides this with
2
- // index.ios.ts / index.android.ts on a real host; under vitest / tsc the resolution lands here.
3
- // Filename is the selector, no Platform.OS read.
4
- export * from './index.ios.js';
@@ -1,8 +0,0 @@
1
- import type { JSX } from '../../jsx-runtime';
2
- import { type IActivityIndicatorPlatform, type IActivityIndicatorProps as IActivityIndicatorBaseProps } from '@symbiote-native/components';
3
- import type { IClassNameValue } from '@symbiote-native/engine';
4
- export type { IActivityIndicatorPlatform, IActivityIndicatorSize, } from '@symbiote-native/components';
5
- export type IActivityIndicatorProps = IActivityIndicatorBaseProps & {
6
- class?: IClassNameValue;
7
- };
8
- export declare function createActivityIndicator(platform: IActivityIndicatorPlatform): (props: IActivityIndicatorProps) => JSX.Element;
@@ -1,51 +0,0 @@
1
- // ActivityIndicator, the Solid lifecycle half. The render (the 'small'/'large' -> native size enum
2
- // translation, the fixed size boxes, the centering wrapper, the color omission) lives in
3
- // @symbiote-native/components/view and is shared verbatim with React, Vue and Svelte. There is no
4
- // state machine: the spinner animates natively, so Solid supplies only prop resolution and the
5
- // descriptor bridge.
6
- //
7
- // NOTHING here destructures `props`. Solid props are getters and a component body runs ONCE;
8
- // destructuring reads them at setup and freezes the component at its mount-time values. Every read
9
- // below happens inside the descriptor accessor, which descriptorToSolid re-runs as a render effect
10
- // on the SAME host nodes.
11
- //
12
- // The per-platform bits (iOS's GRAY default and no extras; Android's theme/null default plus
13
- // styleAttr + indeterminate) arrive via `platform` from index.ios.ts / index.android.ts.
14
- import { renderActivityIndicator, resolveAccessibilityProps, } from '@symbiote-native/components';
15
- import { descriptorToSolid } from '../../descriptor-to-solid.js';
16
- // Read by the component itself; everything else (testID, nativeID, the accessibility surface,
17
- // onLayout, class) forwards onto the centering wrapper View, which is where RN spreads `...props`.
18
- const HANDLED_PROPS = [
19
- 'animating',
20
- 'color',
21
- 'hidesWhenStopped',
22
- 'size',
23
- 'style',
24
- ];
25
- // The wrapper is a raw symbiote-view emitted by the render fn, not the View component, so it never
26
- // runs resolveAccessibilityProps itself — the fold happens here, exactly like React's hook does.
27
- // The two branches of that fold emit DIFFERENT key sets; descriptorToSolid runs the resulting bag
28
- // through withStableKeys, so a vanished key still reaches routeProp as a delete
29
- // (.claude/rules/solid-descriptor-bridge.md §1).
30
- function forwardProps(props) {
31
- const forwarded = {
32
- ...resolveAccessibilityProps(props),
33
- };
34
- for (const key of HANDLED_PROPS)
35
- delete forwarded[key];
36
- return forwarded;
37
- }
38
- export function createActivityIndicator(platform) {
39
- return function ActivityIndicator(props) {
40
- // `?? 'small'` / `?? true` rather than a `!== false` coercion: props are typed here, so this is
41
- // the exact semantics of React's destructuring defaults (only `undefined` falls back).
42
- return descriptorToSolid(() => renderActivityIndicator({
43
- animating: props.animating ?? true,
44
- hidesWhenStopped: props.hidesWhenStopped ?? true,
45
- size: props.size ?? 'small',
46
- color: props.color,
47
- style: props.style,
48
- passthrough: forwardProps(props),
49
- }, platform));
50
- };
51
- }
@@ -1,7 +0,0 @@
1
- import type { JSX } from '../jsx-runtime';
2
- import { type IButtonProps as IButtonBaseProps } from '@symbiote-native/components';
3
- import type { IClassNameValue } from '@symbiote-native/engine';
4
- export type IButtonProps = IButtonBaseProps & {
5
- class?: IClassNameValue;
6
- };
7
- export declare function Button(props: IButtonProps): JSX.Element;