@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,8 @@
1
+ // SectionList: the public preset over VirtualizedSectionList, mirroring RN's
2
+ // SectionList -> VirtualizedSectionList -> VirtualizedList layering. All section flattening,
3
+ // windowing and imperative-scroll logic lives one layer down; this re-exposes the same surface
4
+ // under the SectionList name, threading the imperative ref straight through.
5
+ import { VirtualizedSectionList, } from '../virtualized-section-list/index.jsx';
6
+ export function SectionList(props) {
7
+ return <VirtualizedSectionList {...props}/>;
8
+ }
@@ -0,0 +1,2 @@
1
+ export type { ISwitchProps, ISwitchTrackColor } from './shared';
2
+ export declare const Switch: (props: import("./shared").ISwitchProps) => import("@symbiote-native/solid/jsx-runtime").JSX.Element;
@@ -0,0 +1,12 @@
1
+ // Switch, Android host binding. AndroidSwitch takes trackColorForTrue / trackColorForFalse plus
2
+ // trackTintColor (the color for the CURRENT value, which RN computes as value ? true : false), and
3
+ // snaps native back via the `setNativeValue` command (Switch.js:221-225).
4
+ import { createSwitch } from './shared.js';
5
+ export const Switch = createSwitch({
6
+ snapBackCommand: 'setNativeValue',
7
+ trackColorProps: (value, trackColor) => ({
8
+ trackColorForFalse: trackColor?.false,
9
+ trackColorForTrue: trackColor?.true,
10
+ trackTintColor: value ? trackColor?.true : trackColor?.false,
11
+ }),
12
+ });
@@ -0,0 +1 @@
1
+ export * from './index.ios';
@@ -0,0 +1,2 @@
1
+ export type { ISwitchProps, ISwitchTrackColor } from './shared';
2
+ export declare const Switch: (props: import("./shared").ISwitchProps) => import("@symbiote-native/solid/jsx-runtime").JSX.Element;
@@ -0,0 +1,11 @@
1
+ // Switch, iOS host binding. iOS's native Switch takes onTintColor (ON-track) / tintColor
2
+ // (OFF-track), and snaps native back via the `setValue` command (Switch.js:221-225) — the same
3
+ // facts adapters/react's, adapters/vue's and adapters/svelte's iOS bindings encode.
4
+ import { createSwitch } from './shared.js';
5
+ export const Switch = createSwitch({
6
+ snapBackCommand: 'setValue',
7
+ trackColorProps: (_value, trackColor) => ({
8
+ onTintColor: trackColor?.true,
9
+ tintColor: trackColor?.false,
10
+ }),
11
+ });
@@ -0,0 +1,4 @@
1
+ // Base / default Switch: re-exports the iOS build. Metro overrides this with index.ios.ts /
2
+ // index.android.ts on a real host; under vitest / tsc the resolution lands here. Filename is the
3
+ // selector, no Platform.OS read.
4
+ export * from './index.ios.js';
@@ -0,0 +1,11 @@
1
+ import type { JSX } from '../../jsx-runtime';
2
+ import { type ISwitchPlatform, type ISwitchProps as ISwitchBaseProps } from '@symbiote-native/components';
3
+ import { type IClassNameValue } from '@symbiote-native/engine';
4
+ export type { ISwitchTrackColor } from '@symbiote-native/components';
5
+ export type ISwitchProps = ISwitchBaseProps & {
6
+ class?: IClassNameValue;
7
+ };
8
+ export type ISwitchHostPlatform = ISwitchPlatform & {
9
+ snapBackCommand: string;
10
+ };
11
+ export declare function createSwitch(platform: ISwitchHostPlatform): (props: ISwitchProps) => JSX.Element;
@@ -0,0 +1,87 @@
1
+ // Switch, the Solid lifecycle half. The logic (the lastNativeReport reducer, valueFromChange, the
2
+ // snap-back decision) lives in @symbiote-native/components/state and the render (value fold, track
3
+ // colors, ios_backgroundColor) in @symbiote-native/components/view — both shared verbatim with
4
+ // React, Vue and Svelte. Solid supplies only the reactivity: a signal holding what native last
5
+ // reported, an accessor per prop, and the effect that snaps native back when the parent rejects a
6
+ // toggle.
7
+ //
8
+ // Switch is controlled exactly like RN's: the parent's onValueChange MUST update `value` for the
9
+ // toggle to stick. If the handler is a no-op, native has already flipped its own grip, so JS
10
+ // commands the old value back down — a plain prop re-push cannot cover that case, because the prop
11
+ // never changed and the retained tree never diverged. The command name is platform-specific (iOS
12
+ // setValue / Android setNativeValue) and arrives via `platform`.
13
+ //
14
+ // NOTHING here destructures `props`. Solid props are getters; destructuring reads them once at
15
+ // setup and freezes the component at its mount-time values. Every read below happens inside an
16
+ // accessor (the descriptor memo, the snap-back effect, handleChange) so it re-runs when the caller's
17
+ // signal changes.
18
+ import { createEffect, createSignal } from 'solid-js';
19
+ import { createInitialSwitchState, renderSwitch, resolveAccessibilityProps, shouldSnapBack, switchReducer, valueFromChange, } from '@symbiote-native/components';
20
+ import { dispatchViewCommand, dlog, } from '@symbiote-native/engine';
21
+ import { descriptorToSolid } from '../../descriptor-to-solid.js';
22
+ // Read by the component itself; everything else forwards onto the host node. onValueChange is pure
23
+ // JS and must never reach Fabric — a function prop crashes Android's folly::dynamic serializer.
24
+ const HANDLED_PROPS = [
25
+ 'value',
26
+ 'onValueChange',
27
+ 'disabled',
28
+ 'trackColor',
29
+ 'thumbColor',
30
+ 'ios_backgroundColor',
31
+ 'style',
32
+ ];
33
+ // Switch owns its host element rather than rendering through a symbiote View, so it folds
34
+ // aria/role into the canonical accessibility* props here, exactly like React's hook does.
35
+ function forwardProps(props) {
36
+ const forwarded = {
37
+ ...resolveAccessibilityProps(props),
38
+ };
39
+ for (const key of HANDLED_PROPS)
40
+ delete forwarded[key];
41
+ return forwarded;
42
+ }
43
+ export function createSwitch(platform) {
44
+ return function Switch(props) {
45
+ const [nativeReport, setNativeReport] = createSignal(createInitialSwitchState());
46
+ // value is a real Fabric prop, folded to a strict boolean: RN sends `value === true`, so an
47
+ // absent prop reads as "off" rather than riding through as undefined.
48
+ const fabricValue = () => props.value === true;
49
+ const handleChange = (event) => {
50
+ const next = valueFromChange(event);
51
+ dlog(`Switch onChange value=${String(next)} eventCount=${String(event.nativeEvent.eventCount)}`);
52
+ if (next === undefined)
53
+ return;
54
+ props.onValueChange?.(next, event);
55
+ setNativeReport(current => switchReducer(current, { type: 'native-reported', value: next }));
56
+ };
57
+ // The node is created once and kept by identity — descriptorToSolid wires every prop through a
58
+ // render effect on this same node, so the imperative command below always has a live target and
59
+ // never chases a replaced node.
60
+ const node = descriptorToSolid(() => renderSwitch({
61
+ value: fabricValue(),
62
+ disabled: props.disabled,
63
+ trackColor: props.trackColor,
64
+ thumbColor: props.thumbColor,
65
+ ios_backgroundColor: props.ios_backgroundColor,
66
+ style: props.style,
67
+ passthrough: { ...forwardProps(props), onChange: handleChange },
68
+ }, platform));
69
+ // createEffect, not createRenderEffect: it runs after the tree is built, the same position
70
+ // React's useLayoutEffect and Vue's flush:'post' watch occupy. The engine commits on a
71
+ // microtask (renderer.ts's requestCommit), so the node has no Fabric tag yet on the first run —
72
+ // which is harmless and not worth a whenCommitted retry here, because shouldSnapBack is false
73
+ // until native has reported, and native cannot report before it exists. Every run that DOES
74
+ // dispatch is therefore downstream of a native event, i.e. long past the first commit.
75
+ createEffect(() => {
76
+ const value = fabricValue();
77
+ const reported = nativeReport();
78
+ if (!shouldSnapBack(reported, value)) {
79
+ dlog(`Switch snap-back no-op reported=${String(reported.lastNativeReport)} value=${value}`);
80
+ return;
81
+ }
82
+ dlog(`Switch ${platform.snapBackCommand} snap-back reported=${String(reported.lastNativeReport)} value=${value}`);
83
+ dispatchViewCommand(node, platform.snapBackCommand, [value]);
84
+ });
85
+ return node;
86
+ };
87
+ }
@@ -0,0 +1,10 @@
1
+ import type { Ref } from 'solid-js';
2
+ import type { JSX } from '../jsx-runtime';
3
+ import { type ITextInputHandle, type ITextInputProps as ITextInputBaseProps } from '@symbiote-native/components';
4
+ import { type IClassNameValue } from '@symbiote-native/engine';
5
+ export type { ITextInputHandle, ITextInputSelection, } from '@symbiote-native/components';
6
+ export type ITextInputProps = ITextInputBaseProps & {
7
+ class?: IClassNameValue;
8
+ ref?: Ref<ITextInputHandle>;
9
+ };
10
+ export declare function TextInput(props: ITextInputProps): JSX.Element;
@@ -0,0 +1,268 @@
1
+ // TextInput — the Solid lifecycle half. The folds/maps (value→text, the W3C/alias resolution) and
2
+ // the controlled-write predicate live in @symbiote-native/components/state; the render (intrinsic +
3
+ // native-prop mapping) in @symbiote-native/components/view. Both are shared verbatim with React,
4
+ // Vue and Svelte. Solid supplies only the reactivity: a signal for the acknowledged event count,
5
+ // setup-scope variables for the host node / the last text native holds / the focus flag, one effect
6
+ // for the controlled write, and the imperative handle handed to `ref`.
7
+ //
8
+ // THE CONTROLLED HANDSHAKE IS NOT A PROP RE-PUSH, and that is the whole subtlety. Native owns its
9
+ // own text and has already changed it by the time the change event arrives. When JS REFUSES that
10
+ // change the `value` prop never changes — so nothing re-commits, and the only thing that can
11
+ // correct native is an imperative setTextAndSelection carrying the ACKNOWLEDGED event count (the
12
+ // one native last reported), which native's eventLag check accepts. Same shape as Switch's
13
+ // snap-back; the counter arithmetic is documented in @symbiote-native/components/state/text-input.
14
+ //
15
+ // A FLAT MODULE, NOT A FOLDER: TextInput has no platform variant to select by filename. The
16
+ // iOS/Android differences (keyboardType, autoComplete vs textContentType, underlineColorAndroid)
17
+ // are resolved platform-AGNOSTICALLY by resolveTextInputProps, which emits both platforms' native
18
+ // props and lets the inert one ride — see its header. Only genuine platform/shared groups get a
19
+ // folder (symbiote-file-layout), so Switch has one and this does not.
20
+ //
21
+ // NOTHING here destructures `props`. Solid props are getters and a component body runs ONCE, so a
22
+ // destructure would freeze the input at its mount-time config; splitProps is the idiomatic split
23
+ // that keeps the rest reactive, and every read below sits inside an accessor, an effect or an event
24
+ // handler. The two deliberate exceptions are marked where they happen.
25
+ import { createEffect, createMemo, createSignal, onCleanup, splitProps, untrack, } from 'solid-js';
26
+ import { eventCountFromChange, foldText, renderTextInput, resolveAccessibilityProps, resolveTextInputProps, shouldCommandText, textFromChange, INITIAL_EVENT_COUNT, SELECTION_NONE, } from '@symbiote-native/components';
27
+ import { blurTextInput, dispatchViewCommand, dlog, setInputBlurred, setInputFocused, whenCommitted, } from '@symbiote-native/engine';
28
+ import { descriptorToSolid } from '../descriptor-to-solid.js';
29
+ // Read by the component itself; everything else forwards onto the host node. onValueChange/onFocus/
30
+ // onBlur must be split off — they are pure JS, and it is our WRAPPED handlers that go on the host
31
+ // (a raw function prop that is not a ViewConfig event crashes Android's folly::dynamic serializer).
32
+ // The alias props (inputMode, enterKeyHint, readOnly, submitBehavior, blurOnSubmit) are inert at
33
+ // the native layer and must be folded, never forwarded raw. Same list as React's destructure.
34
+ const HANDLED_PROPS = [
35
+ 'ref',
36
+ 'value',
37
+ 'defaultValue',
38
+ 'multiline',
39
+ 'selection',
40
+ 'onValueChange',
41
+ 'onFocus',
42
+ 'onBlur',
43
+ 'inputMode',
44
+ 'enterKeyHint',
45
+ 'readOnly',
46
+ 'submitBehavior',
47
+ 'blurOnSubmit',
48
+ 'cursorColor',
49
+ 'selectionColor',
50
+ 'selectionHandleColor',
51
+ 'keyboardType',
52
+ 'returnKeyType',
53
+ 'editable',
54
+ 'autoComplete',
55
+ 'textContentType',
56
+ 'autoFocus',
57
+ 'showSoftInputOnFocus',
58
+ 'underlineColorAndroid',
59
+ ];
60
+ export function TextInput(props) {
61
+ const [local, rest] = splitProps(props, HANDLED_PROPS);
62
+ // The count native last acknowledged. A signal because two readers need it live: the render (it
63
+ // rides down as mostRecentEventCount) and the controlled-write effect, which echoes it so
64
+ // native's eventLag lands on 0 and the write applies instead of being dropped as stale.
65
+ const [mostRecentEventCount, setMostRecentEventCount] = createSignal(INITIAL_EVENT_COUNT);
66
+ // The host node, held by IDENTITY in a plain variable — never a store or a deep proxy, which
67
+ // would be a different key than the one the engine's commit mirror holds, and every command below
68
+ // would silently no-op (symbiote-engine-core §3). Null only before the first build and after a
69
+ // multiline swap disposes the old one.
70
+ let host = null;
71
+ // The last text native holds, as far as JS knows. Deliberately NOT reactive: the controlled-write
72
+ // effect both reads and writes it, and a signal would make that a loop. Seeded by a one-time
73
+ // setup read of the mount-time value — the `text` prop already carries that value down via
74
+ // createNode, so the FIRST controlled value is not a divergence and must NOT re-command.
75
+ let lastNativeText = foldText(local.value, local.defaultValue);
76
+ // Mirrored from the focus/blur pair for isFocused(): native exposes no synchronous focus getter,
77
+ // so RN's own TextInputState keeps the same flag.
78
+ let focused = false;
79
+ // autoFocus fires once, on the first host node that commits; guarded so a later multiline swap
80
+ // does not steal focus back.
81
+ let autoFocused = false;
82
+ // The acknowledged count, read WITHOUT subscribing. The imperative handle can be called from
83
+ // anywhere, including inside a caller's effect or memo — a tracked read there would silently
84
+ // enroll their computation in our keystroke counter. React's handle closes over a render value
85
+ // and has the same non-reactive contract.
86
+ const acknowledgedCount = () => untrack(mostRecentEventCount);
87
+ const handleChange = (event) => {
88
+ // Event seam: the controlled handshake hinges on the change payload carrying `text`
89
+ // (+ `eventCount`). iOS and Android Fabric can key these differently, so log the actual shape
90
+ // here; a missing `text` means onValueChange never fires.
91
+ dlog(`TextInput change keys=[${Object.keys(event.nativeEvent).join(',')}] ` +
92
+ `text=${JSON.stringify(event.nativeEvent.text)} count=${JSON.stringify(event.nativeEvent.eventCount)}`);
93
+ const text = textFromChange(event);
94
+ if (text !== undefined) {
95
+ // Ordering matters: record the text first, then bump the acknowledged count, so the count
96
+ // never runs ahead of the text it stands for.
97
+ lastNativeText = text;
98
+ local.onValueChange?.(text, event);
99
+ }
100
+ const count = eventCountFromChange(event);
101
+ if (count !== undefined)
102
+ setMostRecentEventCount(count);
103
+ };
104
+ const handleFocus = (event) => {
105
+ focused = true;
106
+ // Track focus app-wide so Keyboard.dismiss can blur this input without a ref.
107
+ if (host !== null)
108
+ setInputFocused(host);
109
+ local.onFocus?.(event);
110
+ };
111
+ const handleBlur = (event) => {
112
+ focused = false;
113
+ if (host !== null)
114
+ setInputBlurred(host);
115
+ local.onBlur?.(event);
116
+ };
117
+ // One host node for one intrinsic. `multiline` arrives as an ARGUMENT, not as a prop read, so the
118
+ // descriptor's type is constant for this node's whole lifetime — descriptorToSolid builds the
119
+ // element once and only diffs prop VALUES afterwards, and a type that changed under it would
120
+ // throw rather than paint.
121
+ const buildHost = (multiline) => {
122
+ const node = descriptorToSolid(() => renderTextInput({
123
+ multiline,
124
+ text: foldText(local.value, local.defaultValue),
125
+ mostRecentEventCount: mostRecentEventCount(),
126
+ selection: local.selection,
127
+ folded: resolveTextInputProps({
128
+ inputMode: local.inputMode,
129
+ keyboardType: local.keyboardType,
130
+ enterKeyHint: local.enterKeyHint,
131
+ returnKeyType: local.returnKeyType,
132
+ readOnly: local.readOnly,
133
+ editable: local.editable,
134
+ submitBehavior: local.submitBehavior,
135
+ blurOnSubmit: local.blurOnSubmit,
136
+ multiline,
137
+ cursorColor: local.cursorColor,
138
+ selectionColor: local.selectionColor,
139
+ selectionHandleColor: local.selectionHandleColor,
140
+ autoComplete: local.autoComplete,
141
+ textContentType: local.textContentType,
142
+ showSoftInputOnFocus: local.showSoftInputOnFocus,
143
+ underlineColorAndroid: local.underlineColorAndroid,
144
+ }),
145
+ // TextInput owns its host element rather than rendering through a symbiote View, so it
146
+ // folds aria/role into the canonical accessibility* props here. The vanished-key widening
147
+ // that fold needs is descriptorToSolid's job (utils/stable-keys), applied to the whole
148
+ // Descriptor prop bag — nothing to repeat here.
149
+ passthrough: {
150
+ ...resolveAccessibilityProps(rest),
151
+ onChange: handleChange,
152
+ onFocus: handleFocus,
153
+ onBlur: handleBlur,
154
+ },
155
+ }));
156
+ host = node;
157
+ // Dropped together with the computations this build owns, so a command can never aim at a node
158
+ // that was swapped out and is no longer mounted.
159
+ onCleanup(() => {
160
+ if (host === node)
161
+ host = null;
162
+ });
163
+ // autoFocus is driven in JS, not as a native prop (RN does the same via TextInputState.focusInput,
164
+ // TextInput.js:538). The engine commits on a microtask here (renderer.ts's requestCommit), so at
165
+ // this point the node has no Fabric tag and a bare dispatchViewCommand would silently no-op —
166
+ // whenCommitted defers it to the commit that assigns the tag. Cancelled on cleanup so an
167
+ // un-committed pending focus cannot outlive the node.
168
+ if (local.autoFocus === true && !autoFocused) {
169
+ autoFocused = true;
170
+ dlog('TextInput autoFocus -> focus command');
171
+ const cancel = whenCommitted(node, () => {
172
+ dispatchViewCommand(node, 'focus', []);
173
+ });
174
+ onCleanup(cancel);
175
+ }
176
+ return node;
177
+ };
178
+ // Controlled write: when JS-side `value` diverges from what native reported, command the new text
179
+ // down with the acknowledged count. A plain prop re-push would race the user's keystrokes.
180
+ //
181
+ // READING THE COUNT IS LOAD-BEARING, not bookkeeping — it is what makes a REFUSED change snap
182
+ // back. On a refusal `value` never changes, so a value-only dependency (Vue's watch shape) would
183
+ // never re-run and native would keep the text JS rejected. The count bumps on every native
184
+ // change, which is React's `useLayoutEffect` with no deps expressed in Solid's terms.
185
+ createEffect(() => {
186
+ const value = local.value;
187
+ const count = mostRecentEventCount();
188
+ const selection = local.selection;
189
+ if (host === null)
190
+ return;
191
+ if (!shouldCommandText(lastNativeText, value))
192
+ return;
193
+ const selStart = selection?.start ?? SELECTION_NONE;
194
+ const selEnd = selection?.end ?? selection?.start ?? SELECTION_NONE;
195
+ dlog(`TextInput setTextAndSelection count=${count} text=${JSON.stringify(value)}`);
196
+ dispatchViewCommand(host, 'setTextAndSelection', [
197
+ count,
198
+ value,
199
+ selStart,
200
+ selEnd,
201
+ ]);
202
+ lastNativeText = value;
203
+ });
204
+ // The imperative API RN exposes on a TextInput ref. focus/blur drive native view commands; clear
205
+ // and setSelection reuse setTextAndSelection (the same stale-safe path as a controlled write),
206
+ // echoing the acknowledged count so native applies them. Every method reads `host` LIVE, so a
207
+ // multiline swap cannot leave the handle pointing at a dead node.
208
+ const handle = {
209
+ focus: () => {
210
+ if (host !== null)
211
+ dispatchViewCommand(host, 'focus', []);
212
+ },
213
+ blur: () => {
214
+ // Routes through TextInputState so the app-wide focus tracking clears too.
215
+ blurTextInput(host);
216
+ },
217
+ clear: () => {
218
+ if (host === null)
219
+ return;
220
+ dispatchViewCommand(host, 'setTextAndSelection', [
221
+ acknowledgedCount(),
222
+ '',
223
+ 0,
224
+ 0,
225
+ ]);
226
+ lastNativeText = '';
227
+ },
228
+ isFocused: () => focused,
229
+ setSelection: (start, end) => {
230
+ if (host === null)
231
+ return;
232
+ const current = lastNativeText ?? '';
233
+ dispatchViewCommand(host, 'setTextAndSelection', [
234
+ acknowledgedCount(),
235
+ current,
236
+ start,
237
+ end,
238
+ ]);
239
+ },
240
+ };
241
+ // Single- and multiline are DIFFERENT native views (RCTSinglelineTextInputView vs
242
+ // RCTMultilineTextInputView), so a runtime flip has to build a new host node rather than re-prop
243
+ // the old one — React remounts, Vue h()s a new type, Svelte swaps an {#if} branch. The memo is
244
+ // that swap: `insert` replaces the previous node with the new one and disposes the old build.
245
+ //
246
+ // The untrack is load-bearing. Building reads descriptorToSolid's own memo, so without it THIS
247
+ // memo would subscribe to every prop the render fn touches and rebuild the entire input — new
248
+ // native view, lost cursor and keyboard — on every keystroke. (Solid's own `createComponent`
249
+ // untracks for exactly this reason; there is no component here to inherit it from.)
250
+ const hostTree = createMemo(() => {
251
+ const multiline = local.multiline === true;
252
+ return untrack(() => buildHost(multiline));
253
+ });
254
+ // Solid's `ref` is a compile-time construct: on a component the compiler has already rewritten
255
+ // `ref={handle}` into a callback prop by the time we read it, so calling what we were handed is
256
+ // the whole job (utils/host-ref.ts explains the rewrite). Not applyHostRef, which is typed for a
257
+ // host node — this ref receives the handle, matching React's forwardRef contract.
258
+ //
259
+ // Called AFTER `hostTree` has built at least once, not right after `handle` is constructed -
260
+ // same reasoning as ScrollView's ref-ordering fix (scroll-view/shared.tsx). `handle`'s methods
261
+ // read `host` LIVE on every call, so calling ref early never caused a visible bug here (unlike
262
+ // ScrollView, whose handle a consumer - createAnimatedComponent's resolveHostNode - unwrapped
263
+ // eagerly, once, at ref-call time). Reordered anyway: a future Animated.TextInput would hit the
264
+ // same permanently-null capture, and matching the safe ordering now costs nothing.
265
+ if (typeof local.ref === 'function')
266
+ local.ref(handle);
267
+ return hostTree;
268
+ }
@@ -0,0 +1,26 @@
1
+ import { type Ref } from 'solid-js';
2
+ import type { JSX } from '../jsx-runtime';
3
+ import { type IAccessibilityProps, type IAriaProps } from '@symbiote-native/components';
4
+ import type { IClassNameValue, IStyleProp, ISymbioteEvent, ITextStyle } from '@symbiote-native/engine';
5
+ import type { IHostInstance } from '../host-instance';
6
+ export interface ITextProps extends IAccessibilityProps, IAriaProps {
7
+ style?: IStyleProp<ITextStyle>;
8
+ class?: IClassNameValue;
9
+ onPress?: (event: ISymbioteEvent) => void;
10
+ onLongPress?: (event: ISymbioteEvent) => void;
11
+ onPressIn?: (event: ISymbioteEvent) => void;
12
+ onPressOut?: (event: ISymbioteEvent) => void;
13
+ onLayout?: (event: ISymbioteEvent) => void;
14
+ onTextLayout?: (event: ISymbioteEvent) => void;
15
+ numberOfLines?: number;
16
+ ellipsizeMode?: 'head' | 'middle' | 'tail' | 'clip';
17
+ selectable?: boolean;
18
+ adjustsFontSizeToFit?: boolean;
19
+ minimumFontScale?: number;
20
+ allowFontScaling?: boolean;
21
+ maxFontSizeMultiplier?: number | null;
22
+ selectionColor?: string;
23
+ ref?: Ref<IHostInstance>;
24
+ children?: JSX.Element;
25
+ }
26
+ export declare function Text(props: ITextProps): JSX.Element;
@@ -0,0 +1,27 @@
1
+ // Text — the Solid host primitive for RN's <Text>. Same children/ref idiom as ./view.tsx; read
2
+ // that file's header first, it carries the reasoning both share.
3
+ //
4
+ // THE NESTING (`RCTText` vs `RCTVirtualText`) IS NOT THIS FILE'S JOB, and that is worth stating
5
+ // because it is the one place React's own renderer needs a context. React Native tracks a
6
+ // TextAncestor context so a <Text> inside another <Text> renders as a virtual span instead of a
7
+ // paragraph host. Here the retained tree already knows: the engine's commit walk carries a
8
+ // `hasTextAncestor` flag down and picks the view name itself
9
+ // (`viewNameFor` in core/engine/src/commit.ts — `node.isText && hasTextAncestor` -> RCTVirtualText),
10
+ // and it re-creates the node from scratch when that kind flips. So there is no Solid context, no
11
+ // provider, and nothing for an adapter to thread; every adapter emits the same flat
12
+ // `symbiote-text` and the engine resolves the position-dependent name for all of them. The
13
+ // Solid equivalent of TextAncestorContext is deliberately ABSENT, not missing.
14
+ import { splitProps } from 'solid-js';
15
+ import { resolveAccessibilityProps, resolveTextProps, } from '@symbiote-native/components';
16
+ import { applyHostRef } from '../utils/host-ref.js';
17
+ import { withStableKeys } from '../utils/stable-keys.js';
18
+ export function Text(props) {
19
+ const [local, rest] = splitProps(props, ['children', 'ref']);
20
+ const bag = withStableKeys(() => resolveTextProps({ ...resolveAccessibilityProps(rest) }));
21
+ const attachRef = (node) => {
22
+ applyHostRef(local.ref, node);
23
+ };
24
+ return (<symbiote-text ref={attachRef} {...bag()}>
25
+ {local.children}
26
+ </symbiote-text>);
27
+ }
@@ -0,0 +1,22 @@
1
+ import type { JSX } from '../../jsx-runtime';
2
+ import { type IPressTimingProps } from '@symbiote-native/components';
3
+ import { type IStyleProp, type IViewStyle } from '@symbiote-native/engine';
4
+ import { type IPressableProps } from '../pressable';
5
+ type ITouchableBaseProps = Omit<IPressableProps, 'style' | 'children'> & IPressTimingProps & {
6
+ style?: IStyleProp<IViewStyle>;
7
+ children?: JSX.Element;
8
+ };
9
+ export interface ITouchableOpacityProps extends ITouchableBaseProps {
10
+ activeOpacity?: number;
11
+ }
12
+ export declare function TouchableOpacity(props: ITouchableOpacityProps): JSX.Element;
13
+ export interface ITouchableHighlightProps extends ITouchableBaseProps {
14
+ activeOpacity?: number;
15
+ underlayColor?: string;
16
+ onShowUnderlay?: () => void;
17
+ onHideUnderlay?: () => void;
18
+ }
19
+ export declare function TouchableHighlight(props: ITouchableHighlightProps): JSX.Element;
20
+ export type ITouchableWithoutFeedbackProps = ITouchableBaseProps;
21
+ export declare function TouchableWithoutFeedback(props: ITouchableWithoutFeedbackProps): JSX.Element;
22
+ export {};