@symbiote-native/solid 3.0.1 → 3.0.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.
package/README.md CHANGED
@@ -18,14 +18,27 @@ Metro, no custom Babel transformer path.
18
18
 
19
19
  ## Install
20
20
 
21
+ ```bash
22
+ npx @symbiote-native/cli new my-app --framework solid
23
+ ```
24
+
25
+ One command, nothing to wire by hand: scaffolds the Babel preset, Metro config, and
26
+ `jsxImportSource` below, plus `@symbiote-native/solid`/`react-native`/`solid-js` as your app's own
27
+ dependencies.
28
+
29
+ <details>
30
+ <summary>Manual install (no generator — an existing app, or you want to wire it yourself)</summary>
31
+
21
32
  ```bash
22
33
  npm install @symbiote-native/solid react-native solid-js
23
34
  ```
24
35
 
25
36
  `react-native` and `solid-js` stay your app's own top-level dependencies — this package only
26
- replaces the JS renderer that drives them. `npx @symbiote-native/cli new --framework solid` wires
27
- the Babel preset, Metro config, and `jsxImportSource` below for a new app; wiring them into an
28
- existing one still follows [`examples/solid`](../../examples/solid) rather than a generator.
37
+ replaces the JS renderer that drives them. Follow [`examples/solid`](../../examples/solid) for the
38
+ Babel preset, Metro config, and `jsxImportSource` below; there is no wiring script for an existing
39
+ app.
40
+
41
+ </details>
29
42
 
30
43
  Targets **Solid 1.9**, deliberately not 2.0 (RC as of 2026-08) — 2.0 moves the package to
31
44
  `@solidjs/universal`, changes `RendererOptions`, and changes the compiled-output shape. Moving to
@@ -36,29 +49,49 @@ it is a rewrite of `src/renderer.ts`, not a range bump.
36
49
  ## Use it
37
50
 
38
51
  The app is ordinary Solid — the native primitives are plain lowercase intrinsic tags, no import
39
- needed:
52
+ needed. Styling is a CSS class against a plain `.css` file — the convention every example app
53
+ here follows:
40
54
 
41
55
  ```jsx
42
56
  import { createSignal } from 'solid-js';
57
+ import './App.css';
43
58
 
44
59
  export default function App() {
45
60
  const [count, setCount] = createSignal(0);
46
61
  return (
47
- <view style={{ padding: 24 }}>
62
+ <safe-area-view class="screen">
48
63
  <text>Taps: {count()}</text>
49
64
  <pressable onPress={() => setCount(c => c + 1)}>
50
65
  <text>Tap me</text>
51
66
  </pressable>
52
- </view>
67
+ </safe-area-view>
53
68
  );
54
69
  }
55
70
  ```
56
71
 
72
+ ```css
73
+ /* App.css */
74
+ .screen {
75
+ flex: 1;
76
+ align-items: center;
77
+ justify-content: center;
78
+ padding: 24px;
79
+ }
80
+ ```
81
+
82
+ <details>
83
+ <summary>Native entry point (index.js) — already scaffolded by <code>npx @symbiote-native/cli new --framework solid</code></summary>
84
+
57
85
  The zero-config entry wires the RN-backed host seams and registers the app in one call — this is
58
86
  what [`examples/solid`](../../examples/solid) actually uses:
59
87
 
60
88
  ```js
61
89
  // index.js
90
+
91
+ // Registers host behaviors (Image, Pressable, Switch, ...) that /bootstrap alone doesn't
92
+ // reach; deleting this breaks them silently (Metro's production inlineRequires makes a
93
+ // side-effect-only barrel import go lazy, see register.ts).
94
+ import '@symbiote-native/solid';
62
95
  import { createApp } from '@symbiote-native/solid/bootstrap';
63
96
  import App from './App';
64
97
  import { name as appName } from './app.json';
@@ -81,6 +114,8 @@ AppRegistry.registerRunnable(appName, ({ rootTag }) => {
81
114
  });
82
115
  ```
83
116
 
117
+ </details>
118
+
84
119
  `babel.config.js` needs the adapter's preset LAST — Babel applies presets in reverse order, so
85
120
  listing it last runs it first, claiming the JSX before RN's own React-JSX transform can:
86
121
 
@@ -100,11 +135,11 @@ Nothing rewrites the source on the way there, so this adapter carries no Babel p
100
135
  ## `./renderer` is a compiler target, not a convenience export
101
136
 
102
137
  `babel-preset-solid` with `generate: 'universal'` rewrites JSX into direct calls imported from the
103
- `moduleName` it was given. So `src/renderer.ts` exports eleven specific names because generated code
138
+ `moduleName` it was given. So `src/renderer.ts` exports twelve specific names because generated code
104
139
  imports them; dropping one breaks bundling with a module-not-found on an import nobody wrote. The
105
- list (`createElement`, `createTextNode`, `insertNode`, `insert`, `setProp`, `use`, `effect`, `memo`,
106
- `createComponent`, `spread`, `mergeProps`) was verified by compiling representative JSX, not read off
107
- the docs.
140
+ list (`render`, `createElement`, `createTextNode`, `insertNode`, `insert`, `setProp`, `use`, `effect`,
141
+ `memo`, `createComponent`, `spread`, `mergeProps`) was verified by compiling representative JSX, not
142
+ read off the docs.
108
143
 
109
144
  ## Three things the universal runtime does that the seam has to answer correctly
110
145
 
@@ -25,8 +25,8 @@
25
25
  // caller receives that list's own handle — FlatList's handle IS VirtualizedList's (RN says the
26
26
  // same).
27
27
  import { Index, Show, createMemo, splitProps, untrack, } from 'solid-js';
28
- import { SINGLE_COLUMN, chunkIntoRows, expandRowViewability, firstItemOfRow, lastItemOfRow, rowKeyExtractor, } from '@symbiote-native/components';
29
- import { dlog, resolveClassName, } from '@symbiote-native/engine';
28
+ import { SINGLE_COLUMN, chunkIntoRows, expandRowViewability, firstItemOfRow, lastItemOfRow, removeClippedSubviewsOrDefault, rowKeyExtractor, } from '@symbiote-native/components';
29
+ import { Platform, dlog, resolveClassName, } from '@symbiote-native/engine';
30
30
  import { VirtualizedList, } from '../virtualized-list';
31
31
  // What FlatList itself consumes or re-shapes. Everything NOT listed here is passthrough and rides
32
32
  // down onto the inner list untouched — `ref` among it, which is why no delegate handle is needed.
@@ -46,6 +46,8 @@ const ROW_STYLE = { flexDirection: 'row' };
46
46
  const COLUMN_STYLE = { flex: 1 };
47
47
  export function FlatList(props) {
48
48
  const [, passthrough] = splitProps(props, OWN_PROPS);
49
+ // FlatList.js always sends it, defaulted per platform.
50
+ const removeClippedSubviews = () => removeClippedSubviewsOrDefault(props.removeClippedSubviews, Platform.OS);
49
51
  const columnCount = () => props.numColumns ?? SINGLE_COLUMN;
50
52
  // RN treats anything at or below one column as an ordinary single-column list rather than an
51
53
  // error, and so does adapters/react.
@@ -112,7 +114,7 @@ export function FlatList(props) {
112
114
  // soundly for an arbitrary ItemT. <Show> memoizes on the flip alone, so a numColumns change
113
115
  // rebuilds (RN documents changing it live as unsupported — "change the key prop" — which is the
114
116
  // same fresh start) while every other prop change re-props the live list.
115
- return (<Show when={isMultiColumn()} fallback={<VirtualizedList {...passthrough} data={props.data} getItem={(_source, index) => props.data[index]} getItemCount={() => props.data.length} renderItem={props.renderItem} keyExtractor={props.keyExtractor} ItemSeparatorComponent={props.ItemSeparatorComponent} onViewableItemsChanged={props.onViewableItemsChanged} viewabilityConfigCallbackPairs={props.viewabilityConfigCallbackPairs}/>}>
116
- <VirtualizedList {...passthrough} data={rows()} getItem={(_source, index) => rows()[index]} getItemCount={() => rows().length} renderItem={renderRow} keyExtractor={(row) => rowKeyExtractor(row, props.keyExtractor)} ItemSeparatorComponent={rowSeparatorComponent()} onViewableItemsChanged={rowViewability()} viewabilityConfigCallbackPairs={rowViewabilityPairs()}/>
117
+ return (<Show when={isMultiColumn()} fallback={<VirtualizedList {...passthrough} removeClippedSubviews={removeClippedSubviews()} data={props.data} getItem={(_source, index) => props.data[index]} getItemCount={() => props.data.length} renderItem={props.renderItem} keyExtractor={props.keyExtractor} ItemSeparatorComponent={props.ItemSeparatorComponent} onViewableItemsChanged={props.onViewableItemsChanged} viewabilityConfigCallbackPairs={props.viewabilityConfigCallbackPairs}/>}>
118
+ <VirtualizedList {...passthrough} removeClippedSubviews={removeClippedSubviews()} data={rows()} getItem={(_source, index) => rows()[index]} getItemCount={() => rows().length} renderItem={renderRow} keyExtractor={(row) => rowKeyExtractor(row, props.keyExtractor)} ItemSeparatorComponent={rowSeparatorComponent()} onViewableItemsChanged={rowViewability()} viewabilityConfigCallbackPairs={rowViewabilityPairs()}/>
117
119
  </Show>);
118
120
  }
@@ -34,8 +34,8 @@
34
34
  // SEED, which is a mount-time value by definition (React's useReducer lazy init and Svelte's
35
35
  // $state(...) initializer read it exactly once too).
36
36
  import { createEffect, createSignal, Show, splitProps } from 'solid-js';
37
- import { createDescriptorShapeGuard, createInitialModalState, modalReducer, renderModal, resolveAccessibilityProps, shouldRenderModal, } from '@symbiote-native/components';
38
- import { dlog, } from '@symbiote-native/engine';
37
+ import { createDescriptorShapeGuard, createInitialModalState, isModalVisible, modalReducer, modalVisibilityAction, renderModal, resolveAccessibilityProps, shouldRenderModal, } from '@symbiote-native/components';
38
+ import { dlog, Platform, } from '@symbiote-native/engine';
39
39
  import { withStableKeys } from '../utils/stable-keys.js';
40
40
  // Read by Modal itself; everything else (the four DirectEvents, testID/nativeID, every
41
41
  // accessibility* and aria-* field) forwards onto the host node through `passthrough`, exactly as
@@ -54,28 +54,30 @@ const HANDLED_PROPS = [
54
54
  'style',
55
55
  'class',
56
56
  'children',
57
+ 'onDismiss',
57
58
  ];
58
59
  const shape = createDescriptorShapeGuard('Modal');
59
60
  export function Modal(props) {
60
61
  const [local, rest] = splitProps(props, HANDLED_PROPS);
61
- const isVisible = () => local.visible === true;
62
+ const isVisible = () => isModalVisible(local.visible);
62
63
  // The keep-alive seed is a mount-time value by contract (createInitialModalState: a modal that
63
64
  // starts visible is rendered, one that starts hidden contributes no node), so this one read
64
65
  // outside an accessor is correct rather than the frozen-props bug.
65
66
  const [state, setState] = createSignal(createInitialModalState(isVisible()));
66
- // createEffect, not createRenderEffect: it runs AFTER the render that used the OLD state, which
67
- // is the position React's useEffect, Vue's flush:'post' watch and Svelte's $effect all occupy —
68
- // so a visible→hidden transition renders once more with isRendered still true (the keep-alive
69
- // frame) before this drops it. A render effect would unmount immediately and kill the keep-alive.
70
- // The reducer is identity-stable, so the mount run and any no-op transition write the same object
71
- // back and nothing downstream recomputes.
72
- //
73
- // As on Svelte, and for the same reason: under this adapter's microtask-coalesced requestCommit()
74
- // the whole cascade settles inside one flush, so the keep-alive frame is real but never lands as
75
- // its own Fabric commit the way React's synchronous per-render commit makes it.
67
+ // Arms the iOS keep-alive on show; a hide is left to the native dismiss (state/modal.ts). The
68
+ // reducer is identity-stable, so the mount run writes the same object back.
76
69
  createEffect(() => {
77
- setState(current => modalReducer(current, isVisible() ? { type: 'show' } : { type: 'hide' }));
70
+ const action = modalVisibilityAction(isVisible());
71
+ if (action !== undefined)
72
+ setState(current => modalReducer(current, action));
78
73
  });
74
+ // Modal.js: onDismiss is iOS-only — it drops the keep-alive, then tells the app.
75
+ const handleDismiss = () => {
76
+ if (Platform.OS !== 'ios')
77
+ return;
78
+ setState(current => modalReducer(current, { type: 'hide' }));
79
+ local.onDismiss?.();
80
+ };
79
81
  const shouldRender = () => shouldRenderModal(isVisible(), state());
80
82
  createEffect(() => {
81
83
  if (!shouldRender())
@@ -98,7 +100,10 @@ export function Modal(props) {
98
100
  navigationBarTranslucent: local.navigationBarTranslucent,
99
101
  allowSwipeDismissal: local.allowSwipeDismissal,
100
102
  style: local.style,
101
- passthrough: resolveAccessibilityProps(rest),
103
+ passthrough: {
104
+ ...resolveAccessibilityProps(rest),
105
+ onDismiss: handleDismiss,
106
+ },
102
107
  });
103
108
  // root = modal > [container]; the user children nest UNDER the container View, never as
104
109
  // a direct sibling of the host (RN's modal content layout, render-modal.ts).
@@ -16,8 +16,7 @@
16
16
  // behaviors/scroll-view/shared.ts). Horizontal scroll is a separate native
17
17
  // ViewManager on Android, so the axis is the tag; a prop contradicting it
18
18
  // is dropped with a dlog. Typed here for parity and for a ported app.
19
- // nestedScrollEnabled defaults to `true` in the behavior, on both platforms, which every
20
- // wrapper used to write by hand.
19
+ // nestedScrollEnabled defaults to `true` only under an Android RefreshControl, as in RN.
21
20
  //
22
21
  // NO `refreshControl` PROP, deliberately — write a `<refresh-control>` CHILD instead; the
23
22
  // behavior claims it and places it per platform. Nothing reads a prop by that name, so typing one
@@ -46,6 +46,8 @@ export interface IVirtualizedListProps<ItemT> extends IAccessibilityProps, IAria
46
46
  scrollEventThrottle?: number;
47
47
  keyboardShouldPersistTaps?: boolean | 'always' | 'never' | 'handled';
48
48
  keyboardDismissMode?: 'none' | 'on-drag' | 'interactive';
49
+ removeClippedSubviews?: boolean;
50
+ nestedScrollEnabled?: boolean;
49
51
  horizontal?: boolean;
50
52
  inverted?: boolean;
51
53
  style?: IStyleProp<IViewStyle>;
@@ -629,12 +629,16 @@ export function createVirtualizedList() {
629
629
  const outerBag = withStableKeys(() => {
630
630
  const bag = {
631
631
  ...resolveAccessibilityProps(accessibilityRest),
632
- style: [props.style, invertedStyle()],
632
+ // VirtualizedList.js: `[inversionStyle, style]` — the app's style can override the flip.
633
+ style: [invertedStyle(), props.style],
633
634
  class: props.class,
634
635
  contentContainerStyle: contentContainerStyleForContent(),
635
636
  onLayout: onViewportLayout,
636
637
  onScroll: scrollHandler(),
637
638
  };
639
+ // VirtualizedList.js:1111 — Android moves the scrollbar back after the `scale: -1` flip.
640
+ if (props.inverted === true)
641
+ bag.isInvertedVirtualizedList = true;
638
642
  // forwarding's throttle, not the raw prop: it folds in the sticky-mode default (1 native / 16
639
643
  // JS-fallback), without which a header rebuilt off too-sparse scroll events pins late.
640
644
  if (forwarding().scrollEventThrottle !== undefined)
@@ -55,6 +55,8 @@ export interface IVirtualizedSectionListProps<ItemT> extends IAccessibilityProps
55
55
  scrollEventThrottle?: number;
56
56
  keyboardShouldPersistTaps?: boolean | 'always' | 'never' | 'handled';
57
57
  keyboardDismissMode?: 'none' | 'on-drag' | 'interactive';
58
+ removeClippedSubviews?: boolean;
59
+ nestedScrollEnabled?: boolean;
58
60
  style?: IStyleProp<IViewStyle>;
59
61
  contentContainerStyle?: IStyleProp<IViewStyle> | string;
60
62
  class?: IClassNameValue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@symbiote-native/solid",
3
- "version": "3.0.1",
3
+ "version": "3.0.3",
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
5
  "keywords": [
6
6
  "react-native",
@@ -62,19 +62,19 @@
62
62
  "dependencies": {
63
63
  "babel-preset-solid": "^1.9.12",
64
64
  "typescript": "~6.0.0",
65
- "@symbiote-native/components": "3.0.1",
66
- "@symbiote-native/css-parser": "0.5.0"
65
+ "@symbiote-native/components": "3.1.1",
66
+ "@symbiote-native/css-parser": "0.5.1"
67
67
  },
68
68
  "peerDependencies": {
69
69
  "solid-js": ">=1.9.0",
70
70
  "react-native": ">=0.86",
71
- "@symbiote-native/engine": "^1.2.0"
71
+ "@symbiote-native/engine": "^1.3.0"
72
72
  },
73
73
  "devDependencies": {
74
74
  "@babel/core": "^7.25.2",
75
75
  "solid-js": "^1.9.14",
76
- "@symbiote-native/engine": "1.2.0",
77
- "@symbiote-native/test-utils": "0.4.1"
76
+ "@symbiote-native/engine": "1.3.0",
77
+ "@symbiote-native/test-utils": "0.4.3"
78
78
  },
79
79
  "scripts": {
80
80
  "typecheck": "tsc --build",