@symbiote-native/solid 0.0.1 → 0.1.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 (105) hide show
  1. package/build/bootstrap.d.ts +7 -0
  2. package/build/bootstrap.js +18 -0
  3. package/build/components/activity-indicator/index.android.d.ts +2 -0
  4. package/build/components/activity-indicator/index.android.js +11 -0
  5. package/build/components/activity-indicator/index.d.ts +1 -0
  6. package/build/components/activity-indicator/index.ios.d.ts +2 -0
  7. package/build/components/activity-indicator/index.ios.js +10 -0
  8. package/build/components/activity-indicator/index.js +4 -0
  9. package/build/components/activity-indicator/shared.d.ts +8 -0
  10. package/build/components/activity-indicator/shared.js +51 -0
  11. package/build/components/button.d.ts +7 -0
  12. package/build/components/button.jsx +45 -0
  13. package/build/components/flat-list/index.d.ts +10 -0
  14. package/build/components/flat-list/index.jsx +118 -0
  15. package/build/components/image-background.d.ts +10 -0
  16. package/build/components/image-background.jsx +74 -0
  17. package/build/components/image.d.ts +9 -0
  18. package/build/components/image.js +75 -0
  19. package/build/components/input-accessory-view.d.ts +11 -0
  20. package/build/components/input-accessory-view.jsx +39 -0
  21. package/build/components/keyboard-avoiding-view.d.ts +15 -0
  22. package/build/components/keyboard-avoiding-view.jsx +152 -0
  23. package/build/components/modal.d.ts +24 -0
  24. package/build/components/modal.jsx +125 -0
  25. package/build/components/pressable.d.ts +30 -0
  26. package/build/components/pressable.jsx +185 -0
  27. package/build/components/refresh-control.d.ts +18 -0
  28. package/build/components/refresh-control.jsx +36 -0
  29. package/build/components/safe-area-view.d.ts +10 -0
  30. package/build/components/safe-area-view.jsx +24 -0
  31. package/build/components/scroll-view/index.android.d.ts +2 -0
  32. package/build/components/scroll-view/index.android.js +11 -0
  33. package/build/components/scroll-view/index.d.ts +2 -0
  34. package/build/components/scroll-view/index.ios.d.ts +2 -0
  35. package/build/components/scroll-view/index.ios.js +8 -0
  36. package/build/components/scroll-view/index.js +9 -0
  37. package/build/components/scroll-view/shared.d.ts +85 -0
  38. package/build/components/scroll-view/shared.jsx +334 -0
  39. package/build/components/scroll-view/sticky-header.d.ts +18 -0
  40. package/build/components/scroll-view/sticky-header.jsx +182 -0
  41. package/build/components/section-list/index.d.ts +6 -0
  42. package/build/components/section-list/index.jsx +8 -0
  43. package/build/components/switch/index.android.d.ts +2 -0
  44. package/build/components/switch/index.android.js +12 -0
  45. package/build/components/switch/index.d.ts +1 -0
  46. package/build/components/switch/index.ios.d.ts +2 -0
  47. package/build/components/switch/index.ios.js +11 -0
  48. package/build/components/switch/index.js +4 -0
  49. package/build/components/switch/shared.d.ts +11 -0
  50. package/build/components/switch/shared.js +87 -0
  51. package/build/components/text-input.d.ts +10 -0
  52. package/build/components/text-input.js +268 -0
  53. package/build/components/text.d.ts +26 -0
  54. package/build/components/text.jsx +27 -0
  55. package/build/components/touchable/index.d.ts +22 -0
  56. package/build/components/touchable/index.jsx +247 -0
  57. package/build/components/touchable-native-feedback/index.d.ts +16 -0
  58. package/build/components/touchable-native-feedback/index.jsx +68 -0
  59. package/build/components/view.d.ts +32 -0
  60. package/build/components/view.jsx +49 -0
  61. package/build/components/virtualized-list/index.android.d.ts +2 -0
  62. package/build/components/virtualized-list/index.android.js +9 -0
  63. package/build/components/virtualized-list/index.d.ts +1 -0
  64. package/build/components/virtualized-list/index.ios.d.ts +2 -0
  65. package/build/components/virtualized-list/index.ios.js +9 -0
  66. package/build/components/virtualized-list/index.js +4 -0
  67. package/build/components/virtualized-list/shared.d.ts +75 -0
  68. package/build/components/virtualized-list/shared.jsx +782 -0
  69. package/build/components/virtualized-section-list/index.d.ts +62 -0
  70. package/build/components/virtualized-section-list/index.jsx +145 -0
  71. package/build/components.d.ts +43 -0
  72. package/build/components.js +54 -0
  73. package/build/create-portal/index.d.ts +11 -0
  74. package/build/create-portal/index.jsx +88 -0
  75. package/build/create-tunnel/index.d.ts +13 -0
  76. package/build/create-tunnel/index.jsx +53 -0
  77. package/build/descriptor-to-solid.d.ts +3 -0
  78. package/build/descriptor-to-solid.js +91 -0
  79. package/build/host-instance.d.ts +2 -0
  80. package/build/host-instance.js +36 -0
  81. package/build/index.d.ts +30 -0
  82. package/build/index.js +96 -0
  83. package/build/jsx-runtime.d.ts +26 -0
  84. package/build/jsx-runtime.js +46 -0
  85. package/build/modules/animated/create-animated-component.d.ts +10 -0
  86. package/build/modules/animated/create-animated-component.jsx +105 -0
  87. package/build/modules/animated/index.d.ts +54 -0
  88. package/build/modules/animated/index.js +78 -0
  89. package/build/modules/app-registry/index.d.ts +7 -0
  90. package/build/modules/app-registry/index.js +37 -0
  91. package/build/modules/status-bar.d.ts +7 -0
  92. package/build/modules/status-bar.js +45 -0
  93. package/build/primitives/create-color-scheme.d.ts +3 -0
  94. package/build/primitives/create-color-scheme.js +33 -0
  95. package/build/primitives/create-window-dimensions.d.ts +3 -0
  96. package/build/primitives/create-window-dimensions.js +34 -0
  97. package/build/render.d.ts +7 -0
  98. package/build/render.js +91 -0
  99. package/build/renderer.d.ts +7 -0
  100. package/build/renderer.js +195 -0
  101. package/build/utils/host-ref.d.ts +3 -0
  102. package/build/utils/host-ref.js +22 -0
  103. package/build/utils/stable-keys.d.ts +1 -0
  104. package/build/utils/stable-keys.js +31 -0
  105. package/package.json +5 -5
@@ -0,0 +1,34 @@
1
+ // createWindowDimensions — the Solid twin of React's `useWindowDimensions` hook, Vue's composable,
2
+ // Svelte's rune and Angular's WindowDimensionsService, over the framework-agnostic Dimensions
3
+ // module (@symbiote-native/engine). See create-color-scheme.ts's header for why this bucket is
4
+ // `primitives/` and why the name is `create*` rather than `use*`.
5
+ //
6
+ // Returns an ACCESSOR, never a snapshot — a Solid component body runs once, so a returned metrics
7
+ // object would stay pinned to the boot orientation.
8
+ //
9
+ // Outside a component or `createRoot` the `onCleanup` has no owner: Solid warns and the Dimensions
10
+ // listener lives for the process, while the accessor itself keeps tracking.
11
+ import { createSignal, onCleanup } from 'solid-js';
12
+ import { Dimensions, } from '@symbiote-native/engine';
13
+ // Native re-emits a fresh metrics object on every 'change', including ones that touch the screen
14
+ // but not the window. Comparing by field instead of identity keeps an unchanged window from waking
15
+ // every consumer — the guard React/Vue/Angular/Svelte all write by hand, expressed here as the
16
+ // signal's own equality (the idiomatic Solid spelling).
17
+ function metricsEqual(a, b) {
18
+ return (a.width === b.width &&
19
+ a.height === b.height &&
20
+ a.scale === b.scale &&
21
+ a.fontScale === b.fontScale);
22
+ }
23
+ export function createWindowDimensions() {
24
+ const [dimensions, setDimensions] = createSignal(Dimensions.get('window'), { equals: metricsEqual });
25
+ // Seed and subscribe run in one synchronous tick, so — unlike the effect-based twins in the other
26
+ // adapters — there is no window between them for an update to be missed in, and no re-check.
27
+ const subscription = Dimensions.addEventListener('change', (set) => {
28
+ setDimensions(set.window);
29
+ });
30
+ onCleanup(() => {
31
+ subscription.remove();
32
+ });
33
+ return dimensions;
34
+ }
@@ -0,0 +1,7 @@
1
+ import { type IRootTag, type SymbioteSurface } from '@symbiote-native/engine';
2
+ import { type Component } from 'solid-js';
3
+ export declare function mount(rootTag: IRootTag, RootComponent: Component): SymbioteSurface;
4
+ export declare function unmount(rootTag: IRootTag): void;
5
+ declare global {
6
+ var RN$stopSurface: ((surfaceId: number) => void) | undefined;
7
+ }
@@ -0,0 +1,91 @@
1
+ // Mount a Solid app onto a Fabric surface. The native host hands us a rootTag; we create a surface
2
+ // for it and let Solid's universal renderer drive the engine, which commits into
3
+ // nativeFabricUIManager — RN's own renderer never in the path.
4
+ import { createSurface, disposeRoot, dlog, reportUncaughtError, } from '@symbiote-native/engine';
5
+ import { catchError, createRoot } from 'solid-js';
6
+ import { createComponent, insert, setActiveSurface } from './renderer.js';
7
+ // One Solid root per surface, so a surface can be torn down (unmount) or cleanly re-mounted on the
8
+ // same rootTag: the bridgeless host stops and restarts a surface on Fast Refresh and on
9
+ // lifecycle/focus changes, reusing the rootTag. The value is Solid's own dispose function — the
10
+ // whole reactive root goes with it, effects and cleanups included.
11
+ const roots = new Map();
12
+ // Insertion-ordered, so the last entry is the most recently mounted surface. Kept alongside
13
+ // `roots` only to answer one question: when a surface is torn down, is there another one still
14
+ // live for the renderer to keep committing into?
15
+ const surfaces = new Map();
16
+ let activeRootTag;
17
+ function teardown(rootTag) {
18
+ const dispose = roots.get(rootTag);
19
+ if (dispose === undefined)
20
+ return;
21
+ dispose();
22
+ roots.delete(rootTag);
23
+ surfaces.delete(rootTag);
24
+ // Cleared AFTER dispose() on purpose: disposing runs user cleanups, which can still mutate the
25
+ // tree, and those mutations should reach the surface they belong to rather than be dropped by
26
+ // renderer.ts's post-unmount guard.
27
+ //
28
+ // And cleared ONLY when the surface going away is the one the renderer is actually committing
29
+ // into. Clearing unconditionally killed a surface that was still mounted: with two surfaces up,
30
+ // unmounting the FIRST left the second alive but permanently uncommitted — every later mutation
31
+ // hit renderer.ts's "mutation after unmount" guard and was silently dropped. That is reachable
32
+ // from ordinary code the moment two surfaces exist, which is exactly the createTunnel case
33
+ // (../create-tunnel): the source surface stops, the target must keep painting.
34
+ if (activeRootTag !== rootTag) {
35
+ disposeRoot(rootTag);
36
+ return;
37
+ }
38
+ const remaining = Array.from(surfaces.entries()).pop();
39
+ activeRootTag = remaining?.[0];
40
+ setActiveSurface(remaining?.[1]);
41
+ disposeRoot(rootTag);
42
+ }
43
+ export function mount(rootTag, RootComponent) {
44
+ // A re-mount on a live rootTag starts clean; otherwise the stale root double-drives the surface.
45
+ teardown(rootTag);
46
+ const surface = createSurface(rootTag);
47
+ surfaces.set(rootTag, surface);
48
+ activeRootTag = rootTag;
49
+ setActiveSurface(surface);
50
+ // `createComponent(RootComponent, {})` is exactly what compiled JSX emits for `<RootComponent />`
51
+ // — written by hand here because this file is plain TS, deliberately: render.ts is the one module
52
+ // an app's entry point imports before any JSX has been compiled, so keeping it JSX-free means it
53
+ // needs no Babel pass of its own.
54
+ //
55
+ // The surface IS the render container: a top-level insert routes to surface.appendChild
56
+ // (renderer.ts's insertNode), and the engine wraps surface.children in its synthetic flex root.
57
+ //
58
+ // `createRoot` + `insert` IS renderer.ts's `render(code, surface)`, spelled out so `catchError`
59
+ // can sit BETWEEN them. Solid's own `render` runs `insert(element, code())` — the insert lands
60
+ // outside anything `code` wrapped, so a root component returning a bare accessor would build its
61
+ // render effect under an owner with no error handler and throw past this seam. Written this way
62
+ // the insert is inside, and every computation the mount creates inherits the handler.
63
+ let unwinding;
64
+ const dispose = createRoot(disposeRootScope => {
65
+ catchError(() => {
66
+ insert(surface, createComponent(RootComponent, {}));
67
+ }, error => {
68
+ if (unwinding?.error !== error) {
69
+ unwinding = { error };
70
+ reportUncaughtError(error, { origin: 'solid render' });
71
+ }
72
+ throw error;
73
+ });
74
+ return disposeRootScope;
75
+ });
76
+ roots.set(rootTag, dispose);
77
+ return surface;
78
+ }
79
+ // Tear down a surface by rootTag: the public pair of `mount`, and the JS half of the bridgeless
80
+ // `RN$stopSurface` contract (see installStopSurfaceGlobal below).
81
+ export function unmount(rootTag) {
82
+ dlog(`solid unmount root=${rootTag}`);
83
+ teardown(rootTag);
84
+ }
85
+ function installStopSurfaceGlobal() {
86
+ globalThis.RN$stopSurface = (surfaceId) => {
87
+ unmount(surfaceId);
88
+ };
89
+ dlog('installed global.RN$stopSurface');
90
+ }
91
+ installStopSurfaceGlobal();
@@ -0,0 +1,7 @@
1
+ import { SymbioteSurface, type ISymbioteNode } from '@symbiote-native/engine';
2
+ type IHostNode = ISymbioteNode | SymbioteSurface;
3
+ export declare function setActiveSurface(surface: SymbioteSurface | undefined): void;
4
+ export declare function replaceText(textNode: IHostNode, value: string): void;
5
+ export declare function removeNode(parent: IHostNode, node: IHostNode): void;
6
+ 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
+ export {};
@@ -0,0 +1,195 @@
1
+ // A Solid custom renderer over @symbiote-native/engine, built on solid-js/universal's official
2
+ // createRenderer — the framework-agnostic seam Solid itself ships for non-DOM targets. Each
3
+ // RendererOptions method maps onto the engine's tiny mutation API; the engine owns all Fabric
4
+ // clone-on-write, so Solid drives the exact same retained tree React, Vue, Angular and Svelte do.
5
+ //
6
+ // THIS MODULE IS ALSO A COMPILER TARGET, which is why it exports what it exports. babel-preset-solid
7
+ // with `generate: 'universal'` rewrites every JSX element into direct calls imported from the
8
+ // `moduleName` it was configured with (../babel-preset.cjs points it here, at the ./renderer
9
+ // subpath). A component like
10
+ //
11
+ // <symbiote-view style={{ flex: 1 }}>count {n()}</symbiote-view>
12
+ //
13
+ // compiles to `createElement('symbiote-view')` + `setProp(...)` + `createTextNode('count ')` +
14
+ // `insert(el, n, null)`, all imported from this file. So the named exports at the bottom are a
15
+ // hard contract with the compiler, not a matter of taste: drop one and the app fails to bundle
16
+ // with a module-not-found on an import nobody typed. The 11 names were verified by running
17
+ // babel.transformSync over representative JSX (elements, dynamic text, <Show>, <For>, spread,
18
+ // ref) rather than read off the docs.
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';
22
+ function isSurface(node) {
23
+ return node instanceof SymbioteSurface;
24
+ }
25
+ function isRawText(node) {
26
+ return !isSurface(node) && node.component === RAW_TEXT_COMPONENT;
27
+ }
28
+ // One active surface per process. This is FORCED here, not chosen: the compiled-JSX contract above
29
+ // means every mutation arrives through module-level functions with no surface argument, so there is
30
+ // nowhere for a per-surface renderer instance to be threaded through (Vue can close over its
31
+ // surface because Vue's createRenderer is called by US, per mount). Threading it per node instead
32
+ // would mean walking `parent` to the root on EVERY setProperty to find which surface to recommit —
33
+ // too hot for the prop path. Same single-root-per-process conclusion the Svelte adapter reached for
34
+ // its own reasons (svelte-adapter-dom-shim §10).
35
+ let activeSurface;
36
+ export function setActiveSurface(surface) {
37
+ activeSurface = surface;
38
+ }
39
+ // Every mutation asks the surface to (microtask-coalesced) recommit. Solid updates fine-grained and
40
+ // often — a single signal write can touch several props across several nodes — so a burst within one
41
+ // tick collapses into a single completeRoot, which is exactly what requestCommit() is for.
42
+ //
43
+ // A missing surface is not an error: Solid disposes its reactive graph asynchronously, so a
44
+ // cleanup-driven removeNode can legitimately land after unmount() cleared the surface. Dropping the
45
+ // commit is correct there (nothing to commit into), but it stays logged rather than silent.
46
+ function requestCommit() {
47
+ if (activeSurface === undefined) {
48
+ dlog('solid mutation after unmount — commit skipped');
49
+ return;
50
+ }
51
+ activeSurface.requestCommit();
52
+ }
53
+ function siblingsOf(node) {
54
+ if (node.parent !== undefined)
55
+ return node.parent.children;
56
+ return activeSurface?.children ?? [];
57
+ }
58
+ // Hoisted out of nodeOps and exported because descriptor-to-solid.ts needs the same text-update
59
+ // path for a Descriptor's string child, and createRenderer() only hands back the 11 names the JSX
60
+ // compiler imports — replaceText is not among them. Going through here rather than calling the
61
+ // engine's setText directly is what keeps the mutation paired with requestCommit().
62
+ export function replaceText(textNode, value) {
63
+ if (isSurface(textNode))
64
+ return;
65
+ setEngineText(textNode, asText(value));
66
+ requestCommit();
67
+ }
68
+ // solid-js/universal hands createTextNode/replaceText the JSX child value UNCONVERTED, not a
69
+ // stringified one: normalizeIncomingArray() pushes `createTextNode(item)` straight from the
70
+ // children array, so `<Text>{list().length} tiles</Text>` arrives here as the NUMBER 3. On the DOM
71
+ // that is invisible — document.createTextNode coerces — which is why upstream never had to, and why
72
+ // React's and Vue's adapters (whose frameworks stringify first) never saw it either. Here the value
73
+ // lands on RCTRawText's `text`, which Fabric parses as a std::string: a number fails the
74
+ // conversion, convertRawProp logs and falls back to the DEFAULT empty string, and an empty first
75
+ // fragment then ABORTS the app inside BaseTextShadowNode::buildAttributedString — a native SIGABRT
76
+ // with nothing in the JS stack to point at. Diagnosed on the iOS simulator 2026-08-19. The
77
+ // parameter is typed `string` by RendererOptions, so the guard reads as redundant; it is not, and
78
+ // `unknown` is what makes that honest rather than a cast.
79
+ function asText(value) {
80
+ return typeof value === 'string' ? value : String(value);
81
+ }
82
+ // Hoisted and exported for the same reason as replaceText above: createRenderer() hands back only
83
+ // the 11 names the JSX compiler imports, and removeNode is not among them — but create-portal.tsx
84
+ // has to DETACH its anchor host from the portal target on cleanup. Going through here rather than
85
+ // calling the engine's removeChild directly is what keeps the mutation paired with requestCommit(),
86
+ // and keeps the surface-vs-node branch in one place.
87
+ export function removeNode(parent, node) {
88
+ if (isSurface(node))
89
+ return;
90
+ if (isSurface(parent))
91
+ parent.removeChild(node);
92
+ else
93
+ removeEngineChild(parent, node);
94
+ requestCommit();
95
+ }
96
+ const nodeOps = {
97
+ createElement(tag) {
98
+ const descriptor = descriptorFor(tag);
99
+ const node = createEngineElement(descriptor.component, descriptor.isText);
100
+ // Graft the imperative public-instance API (measure / setNativeProps / focus / …) onto the raw
101
+ // node so a `ref` to a host element exposes it exactly like React's getPublicInstance.
102
+ // toPublicInstance mutates in place and returns the SAME node identity, so the engine's commit
103
+ // mirror (keyed on the raw node) still resolves it. Solid hands a plain `ref={el}` this object
104
+ // by assignment, so identity survives — but it must never be put in a createStore(), whose deep
105
+ // proxy would become a different WeakMap key and silently break every imperative command.
106
+ dlog(`solid createElement ${descriptor.component} -> public instance`);
107
+ return toPublicInstance(node);
108
+ },
109
+ // Two callers, one signature. Real content: `createTextNode('count ')` for a text literal, which
110
+ // is genuine RCTRawText and must live inside a <Text>. A PLACEHOLDER: solid-js/universal's
111
+ // cleanChildren() does `replacement || createTextNode("")` to hold the position of a dynamic
112
+ // expression (<Show>, <For>, any `{...}` with following siblings) while it has nothing to show.
113
+ // An empty RCTRawText would actually PAINT in Fabric, so an empty string maps to an engine
114
+ // anchor: a real retained node that keeps sibling order correct and is skipped by the commit walk.
115
+ // Identical call and identical fix as Vue's createText (adapters/vue/src/renderer/index.ts).
116
+ createTextNode(value) {
117
+ const text = asText(value);
118
+ return text === '' ? createAnchor() : createRawText(text);
119
+ },
120
+ replaceText,
121
+ // Answers "is this node one I can write a string into", NOT "did createTextNode make it" — and
122
+ // the difference is load-bearing. solid-js/universal's insertExpression() does
123
+ // `if (node && isTextNode(node)) replaceText(node, value)` where `node` is whatever cleanChildren
124
+ // left in place, i.e. usually the empty-string ANCHOR from createTextNode above. Answering true
125
+ // for an anchor would send the runtime down replaceText() to write text into a node that is not
126
+ // an RCTRawText and never reaches Fabric at all; answering false sends it down its other branch,
127
+ // which replaces the anchor with a fresh raw-text node — the correct outcome. So this checks the
128
+ // component name, and an anchor ('#anchor') is excluded by construction.
129
+ isTextNode(node) {
130
+ return isRawText(node);
131
+ },
132
+ setProperty(node, name, value) {
133
+ if (isSurface(node))
134
+ return;
135
+ // routeProp makes the prop-vs-event decision from the node's ViewConfig (onPress on a View
136
+ // becomes a listener; onTintColor on a Switch stays a prop), and centralizes the class+style
137
+ // merge. Shared with React and Vue — never re-implement an `onX` check here
138
+ // (symbiote-engine-core §2).
139
+ routeProp(node, name, value);
140
+ requestCommit();
141
+ },
142
+ insertNode(parent, node, anchor) {
143
+ // The surface is only ever the root CONTAINER, never a child or a sibling marker. Both guards
144
+ // are narrowing, not defensive: RendererOptions types every position as the same NodeType, so
145
+ // the union has to be discharged before the engine calls, which take a node.
146
+ if (isSurface(node))
147
+ return;
148
+ const before = anchor !== undefined && !isSurface(anchor) ? anchor : undefined;
149
+ if (isRawText(node) && (isSurface(parent) || !parent.isText)) {
150
+ // Fabric has no bare-text host: RCTRawText is only valid as a <Text> child. Reached by a
151
+ // dynamic expression that resolves to a string outside a <Text>, e.g.
152
+ // `<symbiote-view>{label()}</symbiote-view>`. Failing loudly at mount beats building an
153
+ // invalid tree that crashes deeper in native with a far less legible error.
154
+ throw new Error(`Text string "${String(node.props.text)}" must be rendered inside a <Text>`);
155
+ }
156
+ if (isSurface(parent)) {
157
+ if (before !== undefined)
158
+ parent.insertBefore(node, before);
159
+ else
160
+ parent.appendChild(node);
161
+ }
162
+ else if (before !== undefined) {
163
+ insertBefore(parent, node, before);
164
+ }
165
+ else {
166
+ appendChild(parent, node);
167
+ }
168
+ requestCommit();
169
+ },
170
+ removeNode,
171
+ getParentNode(node) {
172
+ if (isSurface(node))
173
+ return undefined;
174
+ return node.parent ?? activeSurface;
175
+ },
176
+ // Anchors are NOT filtered out of this or getNextSibling, deliberately. solid-js/universal keeps
177
+ // its own record of which nodes it put where and re-derives positions through these two lookups
178
+ // (cleanChildren walks getFirstChild to empty a parent; insertExpression reaches for
179
+ // getFirstChild(parent) to replace text in place). Hiding a node the runtime itself inserted
180
+ // desyncs that record from the real tree. Anchors are invisible to FABRIC — the commit walk skips
181
+ // them — not to tree traversal.
182
+ getFirstChild(node) {
183
+ return node.children[0];
184
+ },
185
+ getNextSibling(node) {
186
+ if (isSurface(node))
187
+ return undefined;
188
+ const siblings = siblingsOf(node);
189
+ const index = siblings.indexOf(node);
190
+ return index >= 0 ? siblings[index + 1] : undefined;
191
+ },
192
+ };
193
+ // The compiled-JSX contract. See the module header: these names are imported by generated code,
194
+ // so the list is fixed by babel-preset-solid, not by us.
195
+ export const { render, effect, memo, createComponent, createElement, createTextNode, insertNode, insert, spread, setProp, mergeProps, use, } = createRenderer(nodeOps);
@@ -0,0 +1,3 @@
1
+ import type { Ref } from 'solid-js';
2
+ import type { IHostInstance } from '../host-instance';
3
+ export declare function applyHostRef(ref: Ref<IHostInstance> | undefined, node: IHostInstance): void;
@@ -0,0 +1,22 @@
1
+ // How a caller's `ref` on one of our components reaches the host element.
2
+ //
3
+ // Solid's `ref` is a COMPILE-TIME construct, not a runtime object, and that is the whole reason
4
+ // this helper exists. On a component the compiler rewrites `ref={el}` into a callback prop:
5
+ //
6
+ // <View ref={el} /> -> createComponent(View, { ref(r$) { … el = r$ } })
7
+ //
8
+ // so by the time a component body reads `props.ref`, a variable target has ALREADY become a
9
+ // function. React's RefObject / Vue's template ref have no counterpart here — there is nothing to
10
+ // write a `.current` into, and nothing for the adapter to allocate. The component's only job is to
11
+ // call what it was handed with the host node.
12
+ //
13
+ // The declared type still has to be solid-js's own `Ref<T>` union (`T | ((val: T) => void)`),
14
+ // because type-checking happens on the SOURCE the author wrote: `ref={el}` passes a variable of
15
+ // type T, and a callback-only signature would reject it before the compiler ever gets to rewrite
16
+ // it. So the union is a call-site typing requirement, and the runtime value is always the
17
+ // function branch — a non-function can only arrive from a hand-written createComponent() call,
18
+ // where there is no variable binding to assign back into, so it is ignored rather than guessed at.
19
+ export function applyHostRef(ref, node) {
20
+ if (typeof ref === 'function')
21
+ ref(node);
22
+ }
@@ -0,0 +1 @@
1
+ export declare function withStableKeys(props: () => Record<string, unknown>): () => Record<string, unknown>;
@@ -0,0 +1,31 @@
1
+ // Solid's `spread` walks only the CURRENT key set (`for (const prop in props)` in its
2
+ // spreadExpression) and has no removal pass, so a key that VANISHES between runs keeps its last
3
+ // value on the native view forever. Not hypothetical: resolveAccessibilityProps has two branches
4
+ // with different key sets — it returns the input untouched while no aria-* alias holds a value,
5
+ // and `{...props, role: undefined, …every alias blanked, accessibilityLabel: <folded>}` once one
6
+ // does (hasAnyAriaKey tests the VALUE, not key presence). So a caller whose `aria-label` signal
7
+ // goes undefined drops the `accessibilityLabel` KEY, and a screen reader keeps announcing a label
8
+ // the app already removed. React and Vue never meet this: they hand their reconciler a whole new
9
+ // prop object, and the engine's diffProps sends a vanished key down as literal null
10
+ // (symbiote-engine-core §8). Widening the bag to every key ever seen restores that — a vanished
11
+ // key arrives as `undefined`, which routeProp treats as a delete, and `spread` keeps doing all
12
+ // the actual diffing. Never hand-roll that diff instead.
13
+ //
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.
18
+ export function withStableKeys(props) {
19
+ const seen = new Set();
20
+ return () => {
21
+ const next = props();
22
+ const widened = {};
23
+ for (const key of seen)
24
+ widened[key] = undefined;
25
+ for (const key of Object.keys(next)) {
26
+ seen.add(key);
27
+ widened[key] = next[key];
28
+ }
29
+ return widened;
30
+ };
31
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@symbiote-native/solid",
3
- "version": "0.0.1",
3
+ "version": "0.1.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
5
  "license": "MIT",
6
6
  "repository": {
@@ -52,8 +52,8 @@
52
52
  "dependencies": {
53
53
  "babel-preset-solid": "^1.9.12",
54
54
  "typescript": "~6.0.0",
55
- "@symbiote-native/css-parser": "0.3.0",
56
- "@symbiote-native/components": "0.4.0"
55
+ "@symbiote-native/components": "0.5.0",
56
+ "@symbiote-native/css-parser": "0.4.0"
57
57
  },
58
58
  "peerDependencies": {
59
59
  "@symbiote-native/engine": ">=0.1.7",
@@ -62,8 +62,8 @@
62
62
  },
63
63
  "devDependencies": {
64
64
  "solid-js": "^1.9.14",
65
- "@symbiote-native/engine": "0.2.0",
66
- "@symbiote-native/test-utils": "0.1.6"
65
+ "@symbiote-native/engine": "0.3.0",
66
+ "@symbiote-native/test-utils": "0.2.0"
67
67
  },
68
68
  "scripts": {
69
69
  "typecheck": "tsc --build",